tangled
alpha
login
or
join now
flo-bit.dev
/
blento
21
fork
atom
your personal website on atproto - mirror
blento.app
21
fork
atom
overview
issues
pulls
pipelines
flip card
Florian
1 month ago
44bd2f00
d426f2df
+366
-1
5 changed files
expand all
collapse all
unified
split
src
lib
cards
core
FlipCard
EditingFlipCard.svelte
FlipCard.svelte
FlipCardSettings.svelte
index.ts
index.ts
+63
src/lib/cards/core/FlipCard/EditingFlipCard.svelte
···
1
1
+
<script lang="ts">
2
2
+
import type { Item } from '$lib/types';
3
3
+
import type { Editor } from '@tiptap/core';
4
4
+
import { textAlignClasses, textSizeClasses, verticalAlignClasses } from '../TextCard';
5
5
+
import type { ContentComponentProps } from '../../types';
6
6
+
import MarkdownTextEditor from '$lib/components/MarkdownTextEditor.svelte';
7
7
+
import { cn } from '@foxui/core';
8
8
+
9
9
+
let { item = $bindable<Item>() }: ContentComponentProps = $props();
10
10
+
11
11
+
let frontEditor: Editor | null = $state(null);
12
12
+
let backEditor: Editor | null = $state(null);
13
13
+
</script>
14
14
+
15
15
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
16
16
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
17
17
+
<div class="flex h-full flex-col">
18
18
+
<div class="flex min-h-0 flex-1 flex-col">
19
19
+
<span class="text-base-500 dark:text-base-400 px-6 pt-2 text-xs font-medium">Front</span>
20
20
+
<div
21
21
+
class={cn(
22
22
+
'prose dark:prose-invert prose-neutral prose-sm prose-a:no-underline prose-a:text-accent-600 dark:prose-a:text-accent-400 accent:prose-a:text-accent-950 accent:prose-a:underline accent:prose-p:text-base-900 hover:bg-base-700/5 accent:hover:bg-accent-300/20 prose-p:first:mt-0 prose-p:last:mb-0 inline-flex min-h-0 w-full max-w-none flex-1 cursor-text overflow-y-scroll rounded-md px-6 py-4 text-lg transition-colors duration-150',
23
23
+
textAlignClasses[item.cardData.textAlign as string],
24
24
+
verticalAlignClasses[item.cardData.verticalAlign as string],
25
25
+
textSizeClasses[(item.cardData.textSize ?? 0) as number]
26
26
+
)}
27
27
+
onclick={() => {
28
28
+
if (frontEditor?.isFocused) return;
29
29
+
frontEditor?.commands.focus('end');
30
30
+
}}
31
31
+
>
32
32
+
<MarkdownTextEditor
33
33
+
bind:contentDict={item.cardData}
34
34
+
key="frontText"
35
35
+
bind:editor={frontEditor}
36
36
+
/>
37
37
+
</div>
38
38
+
</div>
39
39
+
40
40
+
<div class="border-base-200 dark:border-base-700 mx-4 border-t"></div>
41
41
+
42
42
+
<div class="flex min-h-0 flex-1 flex-col">
43
43
+
<span class="text-base-500 dark:text-base-400 px-6 pt-2 text-xs font-medium">Back</span>
44
44
+
<div
45
45
+
class={cn(
46
46
+
'prose dark:prose-invert prose-neutral prose-sm prose-a:no-underline prose-a:text-accent-600 dark:prose-a:text-accent-400 accent:prose-a:text-accent-950 accent:prose-a:underline accent:prose-p:text-base-900 hover:bg-base-700/5 accent:hover:bg-accent-300/20 prose-p:first:mt-0 prose-p:last:mb-0 inline-flex min-h-0 w-full max-w-none flex-1 cursor-text overflow-y-scroll rounded-md px-6 py-4 text-lg transition-colors duration-150',
47
47
+
textAlignClasses[item.cardData.textAlign as string],
48
48
+
verticalAlignClasses[item.cardData.verticalAlign as string],
49
49
+
textSizeClasses[(item.cardData.textSize ?? 0) as number]
50
50
+
)}
51
51
+
onclick={() => {
52
52
+
if (backEditor?.isFocused) return;
53
53
+
backEditor?.commands.focus('end');
54
54
+
}}
55
55
+
>
56
56
+
<MarkdownTextEditor
57
57
+
bind:contentDict={item.cardData}
58
58
+
key="backText"
59
59
+
bind:editor={backEditor}
60
60
+
/>
61
61
+
</div>
62
62
+
</div>
63
63
+
</div>
+101
src/lib/cards/core/FlipCard/FlipCard.svelte
···
1
1
+
<script lang="ts">
2
2
+
import { marked } from 'marked';
3
3
+
import { sanitize } from '$lib/sanitize';
4
4
+
import type { ContentComponentProps } from '../../types';
5
5
+
import { textAlignClasses, textSizeClasses, verticalAlignClasses } from '../TextCard';
6
6
+
import { cn } from '@foxui/core';
7
7
+
import { getColor } from '../../index';
8
8
+
9
9
+
let { item }: ContentComponentProps = $props();
10
10
+
11
11
+
let flipped = $state(false);
12
12
+
13
13
+
const colors: Record<string, string> = {
14
14
+
base: 'bg-base-200/50 dark:bg-base-950/50',
15
15
+
accent: 'bg-accent-400 dark:bg-accent-500 accent',
16
16
+
transparent: 'bg-base-200/50 dark:bg-base-950/50'
17
17
+
};
18
18
+
19
19
+
let color = $derived(getColor(item));
20
20
+
21
21
+
let colorClasses = $derived.by(() => {
22
22
+
const bgClasses = colors[color] ?? colors.accent;
23
23
+
const colorName =
24
24
+
color !== 'accent' && color !== 'base' && color !== 'transparent' ? color : '';
25
25
+
const lightClass = color !== 'base' && color !== 'transparent' ? 'light' : '';
26
26
+
return cn(bgClasses, colorName, lightClass);
27
27
+
});
28
28
+
29
29
+
const renderer = new marked.Renderer();
30
30
+
renderer.link = ({ href, title, text }) =>
31
31
+
`<a target="_blank" href="${href}" title="${title ?? ''}">${text}</a>`;
32
32
+
33
33
+
const proseClasses =
34
34
+
'prose dark:prose-invert prose-neutral prose-sm prose-a:no-underline prose-a:text-accent-600 dark:prose-a:text-accent-400 accent:prose-a:text-accent-950 accent:prose-a:underline accent:prose-p:text-base-900 prose-p:first:mt-0 prose-p:last:mb-0 prose-headings:first:mt-0 prose-headings:last:mb-0 inline-flex h-full min-h-full w-full max-w-none overflow-x-hidden overflow-y-scroll rounded-md px-6 py-4 text-lg';
35
35
+
</script>
36
36
+
37
37
+
<div
38
38
+
class="h-full w-full cursor-pointer perspective-[1000px]"
39
39
+
role="button"
40
40
+
tabindex="0"
41
41
+
onclick={() => (flipped = !flipped)}
42
42
+
onkeydown={(e) => {
43
43
+
if (e.key === 'Enter' || e.key === ' ') {
44
44
+
e.preventDefault();
45
45
+
flipped = !flipped;
46
46
+
}
47
47
+
}}
48
48
+
>
49
49
+
<div
50
50
+
class={cn(
51
51
+
'relative h-full w-full [transition:transform_0.6s] transform-3d',
52
52
+
flipped && 'transform-[rotateY(180deg)]'
53
53
+
)}
54
54
+
>
55
55
+
<!-- Front -->
56
56
+
<div
57
57
+
class={cn(
58
58
+
'text-base-900 dark:text-base-50 absolute inset-0 rounded-[23px] backface-hidden',
59
59
+
colorClasses
60
60
+
)}
61
61
+
>
62
62
+
<div
63
63
+
class={cn(
64
64
+
proseClasses,
65
65
+
textAlignClasses?.[item.cardData.textAlign as string],
66
66
+
verticalAlignClasses[item.cardData.verticalAlign as string],
67
67
+
textSizeClasses[(item.cardData.textSize ?? 0) as number]
68
68
+
)}
69
69
+
>
70
70
+
<span
71
71
+
>{@html sanitize(marked.parse(item.cardData.frontText ?? '', { renderer }) as string, {
72
72
+
ADD_ATTR: ['target']
73
73
+
})}</span
74
74
+
>
75
75
+
</div>
76
76
+
</div>
77
77
+
78
78
+
<!-- Back -->
79
79
+
<div
80
80
+
class={cn(
81
81
+
'text-base-900 dark:text-base-50 absolute inset-0 transform-[rotateY(180deg)] rounded-[23px] backface-hidden',
82
82
+
colorClasses
83
83
+
)}
84
84
+
>
85
85
+
<div
86
86
+
class={cn(
87
87
+
proseClasses,
88
88
+
textAlignClasses?.[item.cardData.textAlign as string],
89
89
+
verticalAlignClasses[item.cardData.verticalAlign as string],
90
90
+
textSizeClasses[(item.cardData.textSize ?? 0) as number]
91
91
+
)}
92
92
+
>
93
93
+
<span
94
94
+
>{@html sanitize(marked.parse(item.cardData.backText ?? '', { renderer }) as string, {
95
95
+
ADD_ATTR: ['target']
96
96
+
})}</span
97
97
+
>
98
98
+
</div>
99
99
+
</div>
100
100
+
</div>
101
101
+
</div>
+171
src/lib/cards/core/FlipCard/FlipCardSettings.svelte
···
1
1
+
<script lang="ts">
2
2
+
import type { Item } from '$lib/types';
3
3
+
import type { ContentComponentProps } from '../../types';
4
4
+
import { ToggleGroup, ToggleGroupItem, Button } from '@foxui/core';
5
5
+
6
6
+
let { item = $bindable<Item>() }: ContentComponentProps = $props();
7
7
+
8
8
+
const classes = 'size-8 min-w-8 [&_svg]:size-3 cursor-pointer';
9
9
+
</script>
10
10
+
11
11
+
<div class="flex flex-col gap-2">
12
12
+
<ToggleGroup
13
13
+
type="single"
14
14
+
bind:value={
15
15
+
() => {
16
16
+
return item.cardData.verticalAlign ?? 'top';
17
17
+
},
18
18
+
(value) => {
19
19
+
if (!value) return;
20
20
+
item.cardData.verticalAlign = value;
21
21
+
}
22
22
+
}
23
23
+
>
24
24
+
<ToggleGroupItem size="sm" value="top" class={classes}
25
25
+
><svg
26
26
+
xmlns="http://www.w3.org/2000/svg"
27
27
+
viewBox="0 0 24 24"
28
28
+
fill="none"
29
29
+
stroke="currentColor"
30
30
+
stroke-width="2"
31
31
+
stroke-linecap="round"
32
32
+
stroke-linejoin="round"
33
33
+
><rect width="6" height="16" x="4" y="6" rx="2" /><rect
34
34
+
width="6"
35
35
+
height="9"
36
36
+
x="14"
37
37
+
y="6"
38
38
+
rx="2"
39
39
+
/><path d="M22 2H2" /></svg
40
40
+
>
41
41
+
</ToggleGroupItem>
42
42
+
<ToggleGroupItem size="sm" value="center" class={classes}
43
43
+
><svg
44
44
+
xmlns="http://www.w3.org/2000/svg"
45
45
+
viewBox="0 0 24 24"
46
46
+
fill="none"
47
47
+
stroke="currentColor"
48
48
+
stroke-width="2"
49
49
+
stroke-linecap="round"
50
50
+
stroke-linejoin="round"
51
51
+
><rect width="10" height="6" x="7" y="9" rx="2" /><path d="M22 20H2" /><path
52
52
+
d="M22 4H2"
53
53
+
/></svg
54
54
+
></ToggleGroupItem
55
55
+
>
56
56
+
<ToggleGroupItem size="sm" value="bottom" class={classes}
57
57
+
><svg
58
58
+
xmlns="http://www.w3.org/2000/svg"
59
59
+
viewBox="0 0 24 24"
60
60
+
fill="none"
61
61
+
stroke="currentColor"
62
62
+
stroke-width="2"
63
63
+
stroke-linecap="round"
64
64
+
stroke-linejoin="round"
65
65
+
><rect width="14" height="6" x="5" y="12" rx="2" /><rect
66
66
+
width="10"
67
67
+
height="6"
68
68
+
x="7"
69
69
+
y="2"
70
70
+
rx="2"
71
71
+
/><path d="M2 22h20" /></svg
72
72
+
></ToggleGroupItem
73
73
+
>
74
74
+
</ToggleGroup>
75
75
+
76
76
+
<ToggleGroup
77
77
+
type="single"
78
78
+
bind:value={
79
79
+
() => {
80
80
+
return item.cardData.textAlign ?? 'left';
81
81
+
},
82
82
+
(value) => {
83
83
+
if (!value) return;
84
84
+
item.cardData.textAlign = value;
85
85
+
}
86
86
+
}
87
87
+
>
88
88
+
<ToggleGroupItem size="sm" value="left" class={classes}
89
89
+
><svg
90
90
+
xmlns="http://www.w3.org/2000/svg"
91
91
+
viewBox="0 0 24 24"
92
92
+
fill="none"
93
93
+
stroke="currentColor"
94
94
+
stroke-width="2"
95
95
+
stroke-linecap="round"
96
96
+
stroke-linejoin="round"><path d="M21 5H3" /><path d="M15 12H3" /><path d="M17 19H3" /></svg
97
97
+
></ToggleGroupItem
98
98
+
>
99
99
+
<ToggleGroupItem size="sm" value="center" class={classes}
100
100
+
><svg
101
101
+
xmlns="http://www.w3.org/2000/svg"
102
102
+
viewBox="0 0 24 24"
103
103
+
fill="none"
104
104
+
stroke="currentColor"
105
105
+
stroke-width="2"
106
106
+
stroke-linecap="round"
107
107
+
stroke-linejoin="round"><path d="M21 5H3" /><path d="M17 12H7" /><path d="M19 19H5" /></svg
108
108
+
></ToggleGroupItem
109
109
+
>
110
110
+
<ToggleGroupItem size="sm" value="right" class={classes}
111
111
+
><svg
112
112
+
xmlns="http://www.w3.org/2000/svg"
113
113
+
viewBox="0 0 24 24"
114
114
+
fill="none"
115
115
+
stroke="currentColor"
116
116
+
stroke-width="2"
117
117
+
stroke-linecap="round"
118
118
+
stroke-linejoin="round"><path d="M21 5H3" /><path d="M21 12H9" /><path d="M21 19H7" /></svg
119
119
+
></ToggleGroupItem
120
120
+
>
121
121
+
</ToggleGroup>
122
122
+
123
123
+
<div>
124
124
+
<Button
125
125
+
variant="ghost"
126
126
+
onclick={() => {
127
127
+
item.cardData.textSize = Math.max((item.cardData.textSize ?? 0) - 1, 0);
128
128
+
}}
129
129
+
disabled={(item.cardData.textSize ?? 0) < 1}
130
130
+
>
131
131
+
<svg
132
132
+
xmlns="http://www.w3.org/2000/svg"
133
133
+
width="24"
134
134
+
height="24"
135
135
+
viewBox="0 0 24 24"
136
136
+
fill="none"
137
137
+
stroke="currentColor"
138
138
+
stroke-width="2"
139
139
+
stroke-linecap="round"
140
140
+
stroke-linejoin="round"
141
141
+
class="lucide lucide-aarrow-down-icon lucide-a-arrow-down"
142
142
+
><path d="m14 12 4 4 4-4" /><path d="M18 16V7" /><path
143
143
+
d="m2 16 4.039-9.69a.5.5 0 0 1 .923 0L11 16"
144
144
+
/><path d="M3.304 13h6.392" /></svg
145
145
+
>
146
146
+
</Button>
147
147
+
<Button
148
148
+
variant="ghost"
149
149
+
onclick={() => {
150
150
+
item.cardData.textSize = Math.min((item.cardData.textSize ?? 0) + 1, 5);
151
151
+
}}
152
152
+
disabled={(item.cardData.textSize ?? 0) > 4}
153
153
+
>
154
154
+
<svg
155
155
+
xmlns="http://www.w3.org/2000/svg"
156
156
+
width="24"
157
157
+
height="24"
158
158
+
viewBox="0 0 24 24"
159
159
+
fill="none"
160
160
+
stroke="currentColor"
161
161
+
stroke-width="2"
162
162
+
stroke-linecap="round"
163
163
+
stroke-linejoin="round"
164
164
+
class="lucide lucide-aarrow-up-icon lucide-a-arrow-up"
165
165
+
><path d="m14 11 4-4 4 4" /><path d="M18 16V7" /><path
166
166
+
d="m2 16 4.039-9.69a.5.5 0 0 1 .923 0L11 16"
167
167
+
/><path d="M3.304 13h6.392" /></svg
168
168
+
>
169
169
+
</Button>
170
170
+
</div>
171
171
+
</div>
+28
src/lib/cards/core/FlipCard/index.ts
···
1
1
+
import type { CardDefinition } from '../../types';
2
2
+
import EditingFlipCard from './EditingFlipCard.svelte';
3
3
+
import FlipCard from './FlipCard.svelte';
4
4
+
import FlipCardSettings from './FlipCardSettings.svelte';
5
5
+
6
6
+
export const FlipCardDefinition = {
7
7
+
type: 'flipCard',
8
8
+
contentComponent: FlipCard,
9
9
+
editingContentComponent: EditingFlipCard,
10
10
+
createNew: (card) => {
11
11
+
card.cardType = 'flipCard';
12
12
+
card.cardData = {
13
13
+
frontText: 'Front',
14
14
+
backText: 'Back'
15
15
+
};
16
16
+
},
17
17
+
18
18
+
settingsComponent: FlipCardSettings,
19
19
+
20
20
+
defaultColor: 'transparent',
21
21
+
22
22
+
name: 'Flip Card',
23
23
+
24
24
+
keywords: ['flip', 'flashcard', 'two-sided', 'reveal'],
25
25
+
groups: ['Core'],
26
26
+
27
27
+
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="size-4"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 3H5a2 2 0 0 0-2 2v14c0 1.1.9 2 2 2h3M16 3h3a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-3M12 20V4m-4 8h8" /></svg>`
28
28
+
} as CardDefinition & { type: 'flipCard' };
+3
-1
src/lib/cards/index.ts
···
46
46
import { LastFMTopTracksCardDefinition } from './media/LastFMCard/LastFMTopTracksCard';
47
47
import { LastFMTopAlbumsCardDefinition } from './media/LastFMCard/LastFMTopAlbumsCard';
48
48
import { LastFMProfileCardDefinition } from './media/LastFMCard/LastFMProfileCard';
49
49
+
import { FlipCardDefinition } from './core/FlipCard';
49
50
// import { Model3DCardDefinition } from './visual/Model3DCard';
50
51
51
52
export const AllCardDefinitions = [
···
96
97
LastFMRecentTracksCardDefinition,
97
98
LastFMTopTracksCardDefinition,
98
99
LastFMTopAlbumsCardDefinition,
99
99
-
LastFMProfileCardDefinition
100
100
+
LastFMProfileCardDefinition,
101
101
+
FlipCardDefinition
100
102
] as const;
101
103
102
104
export const CardDefinitionsByType = AllCardDefinitions.reduce(