tangled
alpha
login
or
join now
flo-bit.dev
/
blento
your personal website on atproto - mirror
blento.app
20
fork
atom
overview
issues
pulls
pipelines
commit
Florian
1 week ago
bb73ff93
cda2c9ee
+595
-145
5 changed files
expand all
collapse all
unified
split
src
lib
cards
BaseCard
BaseCard.svelte
BaseEditingCard.svelte
website
EditBar.svelte
EditableWebsite.svelte
context.ts
+11
-1
src/lib/cards/BaseCard/BaseCard.svelte
···
5
import type { Snippet } from 'svelte';
6
import type { HTMLAttributes } from 'svelte/elements';
7
import { getColor } from '..';
0
0
0
0
0
0
0
0
0
0
8
9
const colors = {
10
base: 'bg-base-200/50 dark:bg-base-950/50',
···
39
id={item.id}
40
data-flip-id={item.id}
41
bind:this={ref}
42
-
draggable={isEditing && !locked}
43
class={[
44
'card group/card selection:bg-accent-600/50 focus-within:outline-accent-500 @container/card absolute isolate z-0 rounded-3xl outline-offset-2 transition-all duration-200 focus-within:outline-2',
45
color ? (colors[color] ?? colors.accent) : colors.base,
···
5
import type { Snippet } from 'svelte';
6
import type { HTMLAttributes } from 'svelte/elements';
7
import { getColor } from '..';
8
+
import { getIsCoarse } from '$lib/website/context';
9
+
10
+
function tryGetIsCoarse(): (() => boolean) | undefined {
11
+
try {
12
+
return getIsCoarse();
13
+
} catch {
14
+
return undefined;
15
+
}
16
+
}
17
+
const isCoarse = tryGetIsCoarse();
18
19
const colors = {
20
base: 'bg-base-200/50 dark:bg-base-950/50',
···
49
id={item.id}
50
data-flip-id={item.id}
51
bind:this={ref}
52
+
draggable={isEditing && !locked && !isCoarse?.()}
53
class={[
54
'card group/card selection:bg-accent-600/50 focus-within:outline-accent-500 @container/card absolute isolate z-0 rounded-3xl outline-offset-2 transition-all duration-200 focus-within:outline-2',
55
color ? (colors[color] ?? colors.accent) : colors.base,
+36
-9
src/lib/cards/BaseCard/BaseEditingCard.svelte
···
7
import { ColorSelect } from '@foxui/colors';
8
import { AllCardDefinitions, CardDefinitionsByType, getColor } from '..';
9
import { COLUMNS } from '$lib';
10
-
import { getCanEdit, getIsMobile } from '$lib/website/context';
0
0
0
0
0
0
11
import PlainTextEditor from '$lib/components/PlainTextEditor.svelte';
12
import { fixAllCollisions, fixCollisions } from '$lib/helper';
13
···
53
54
let canEdit = getCanEdit();
55
let isMobile = getIsMobile();
0
0
0
0
0
0
56
57
let colorPopoverOpen = $state(false);
58
···
173
{item}
174
isEditing={true}
175
bind:ref
176
-
showOutline={isResizing}
177
locked={item.cardData?.locked}
178
-
class="scale-100 opacity-100 starting:scale-0 starting:opacity-0"
0
0
0
0
179
{...rest}
180
>
181
{#if !item.cardData?.locked}
182
-
<div class="absolute inset-0 cursor-grab"></div>
0
0
0
0
0
0
0
0
0
0
0
183
{/if}
184
{@render children?.()}
185
···
187
<div
188
class={cn(
189
'bg-base-200/50 dark:bg-base-900/50 absolute top-2 left-2 z-100 w-fit max-w-[calc(100%-1rem)] rounded-xl p-1 px-2 backdrop-blur-md',
190
-
!item.cardData.label && 'hidden group-hover/card:block'
191
)}
192
>
193
<PlainTextEditor
···
205
{#if changeOptions.length > 1}
206
<div
207
class={[
208
-
'absolute -top-3 -right-3 hidden group-focus-within:inline-flex group-hover/card:inline-flex',
209
changePopoverOpen ? 'inline-flex' : ''
210
]}
211
>
···
253
onclick={() => {
254
ondelete();
255
}}
256
-
class="absolute -top-3 -left-3 hidden group-focus-within:inline-flex group-hover/card:inline-flex"
257
>
258
<svg
259
xmlns="http://www.w3.org/2000/svg"
···
274
275
<div
276
class={[
277
-
'absolute -bottom-7 w-full items-center justify-center text-xs group-focus-within:inline-flex group-hover/card:inline-flex',
278
colorPopoverOpen || settingsPopoverOpen ? 'inline-flex' : 'hidden'
279
]}
280
>
···
411
<!-- Resize handle at bottom right corner -->
412
<div
413
onpointerdown={handleResizeStart}
414
-
class="bg-base-300/70 dark:bg-base-900/70 pointer-events-auto absolute right-0.5 bottom-0.5 hidden cursor-se-resize rounded-md rounded-br-3xl p-1 group-hover/card:block"
415
>
416
<svg
417
xmlns="http://www.w3.org/2000/svg"
···
7
import { ColorSelect } from '@foxui/colors';
8
import { AllCardDefinitions, CardDefinitionsByType, getColor } from '..';
9
import { COLUMNS } from '$lib';
10
+
import {
11
+
getCanEdit,
12
+
getIsCoarse,
13
+
getIsMobile,
14
+
getSelectedCardId,
15
+
getSelectCard
16
+
} from '$lib/website/context';
17
import PlainTextEditor from '$lib/components/PlainTextEditor.svelte';
18
import { fixAllCollisions, fixCollisions } from '$lib/helper';
19
···
59
60
let canEdit = getCanEdit();
61
let isMobile = getIsMobile();
62
+
let isCoarse = getIsCoarse();
63
+
64
+
let selectedCardId = getSelectedCardId();
65
+
let selectCard = getSelectCard();
66
+
let isSelected = $derived(selectedCardId?.() === item.id);
67
+
let isDimmed = $derived(isCoarse?.() && selectedCardId?.() != null && !isSelected);
68
69
let colorPopoverOpen = $state(false);
70
···
185
{item}
186
isEditing={true}
187
bind:ref
188
+
showOutline={isResizing || (isCoarse?.() && isSelected)}
189
locked={item.cardData?.locked}
190
+
class={[
191
+
'scale-100 starting:scale-0 starting:opacity-0',
192
+
isCoarse?.() && isSelected ? 'ring-accent-500 z-10 ring-2 ring-offset-2' : '',
193
+
isDimmed ? 'opacity-70' : 'opacity-100'
194
+
]}
195
{...rest}
196
>
197
{#if !item.cardData?.locked}
198
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
199
+
<div
200
+
role="button"
201
+
tabindex="-1"
202
+
class={['absolute inset-0', isCoarse?.() ? 'cursor-pointer' : 'cursor-grab']}
203
+
onclick={(e) => {
204
+
if (isCoarse?.()) {
205
+
e.stopPropagation();
206
+
selectCard?.(item.id);
207
+
}
208
+
}}
209
+
></div>
210
{/if}
211
{@render children?.()}
212
···
214
<div
215
class={cn(
216
'bg-base-200/50 dark:bg-base-900/50 absolute top-2 left-2 z-100 w-fit max-w-[calc(100%-1rem)] rounded-xl p-1 px-2 backdrop-blur-md',
217
+
!item.cardData.label && 'hidden lg:group-hover/card:block'
218
)}
219
>
220
<PlainTextEditor
···
232
{#if changeOptions.length > 1}
233
<div
234
class={[
235
+
'absolute -top-3 -right-3 hidden lg:group-focus-within:inline-flex lg:group-hover/card:inline-flex',
236
changePopoverOpen ? 'inline-flex' : ''
237
]}
238
>
···
280
onclick={() => {
281
ondelete();
282
}}
283
+
class="absolute -top-3 -left-3 hidden lg:group-focus-within:inline-flex lg:group-hover/card:inline-flex"
284
>
285
<svg
286
xmlns="http://www.w3.org/2000/svg"
···
301
302
<div
303
class={[
304
+
'absolute -bottom-7 w-full items-center justify-center text-xs lg:group-focus-within:inline-flex lg:group-hover/card:inline-flex',
305
colorPopoverOpen || settingsPopoverOpen ? 'inline-flex' : 'hidden'
306
]}
307
>
···
438
<!-- Resize handle at bottom right corner -->
439
<div
440
onpointerdown={handleResizeStart}
441
+
class="bg-base-300/70 dark:bg-base-900/70 pointer-events-auto absolute right-0.5 bottom-0.5 hidden cursor-se-resize rounded-md rounded-br-3xl p-1 lg:group-hover/card:block"
442
>
443
<svg
444
xmlns="http://www.w3.org/2000/svg"
+346
-119
src/lib/website/EditBar.svelte
···
1
<script lang="ts">
2
import { dev } from '$app/environment';
3
import { user } from '$lib/atproto';
4
-
import type { WebsiteData } from '$lib/types';
0
0
5
import { Button, Input, Navbar, Popover, Toggle, toast } from '@foxui/core';
0
6
7
let {
8
data,
···
17
save,
18
19
handleImageInputChange,
20
-
handleVideoInputChange
0
0
0
0
0
0
0
21
}: {
22
data: WebsiteData;
23
linkValue: string;
···
33
34
handleImageInputChange: (evt: Event) => void;
35
handleVideoInputChange: (evt: Event) => void;
0
0
0
0
0
0
0
36
} = $props();
37
38
let linkPopoverOpen = $state(false);
···
52
await navigator.clipboard.writeText(url);
53
toast.success('Link copied to clipboard!');
54
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
55
</script>
56
57
<input
···
74
75
{#if dev || (user.isLoggedIn && user.profile?.did === data.did)}
76
<Navbar
77
-
class={[
78
-
'dark:bg-base-900 bg-base-100 top-auto bottom-2 mx-4 mt-3 max-w-3xl rounded-full px-4 md:mx-auto lg:inline-flex',
79
-
!dev ? 'hidden' : ''
80
-
]}
81
>
82
-
<div class="flex items-center gap-2">
83
-
<Button
84
-
size="iconLg"
85
-
variant="ghost"
86
-
class="backdrop-blur-none"
87
-
onclick={() => {
88
-
newCard('section');
89
-
}}
90
-
>
91
-
<svg
92
-
xmlns="http://www.w3.org/2000/svg"
93
-
viewBox="0 0 24 24"
94
-
fill="none"
95
-
stroke="currentColor"
96
-
stroke-width="2"
97
-
stroke-linecap="round"
98
-
stroke-linejoin="round"
99
-
><path d="M6 12h12" /><path d="M6 20V4" /><path d="M18 20V4" /></svg
0
0
0
0
0
0
0
0
100
>
101
-
</Button>
0
0
0
0
0
0
0
0
0
0
0
0
0
0
102
103
-
<Button
104
-
size="iconLg"
105
-
variant="ghost"
106
-
class="backdrop-blur-none"
107
-
onclick={() => {
108
-
newCard('text');
109
-
}}
110
-
>
111
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
112
-
><path
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
113
fill="none"
114
stroke="currentColor"
0
115
stroke-linecap="round"
116
stroke-linejoin="round"
117
-
stroke-width="2"
118
-
d="m15 16l2.536-7.328a1.02 1.02 1 0 1 1.928 0L22 16m-6.303-2h5.606M2 16l4.039-9.69a.5.5 0 0 1 .923 0L11 16m-7.696-3h6.392"
119
-
/></svg
0
0
0
0
0
0
0
0
120
>
121
-
</Button>
0
0
0
0
0
0
0
0
0
0
122
123
-
<Popover sideOffset={16} bind:open={linkPopoverOpen} class="bg-base-100 dark:bg-base-900">
124
-
{#snippet child({ props })}
125
-
<Button
126
-
size="iconLg"
127
-
variant="ghost"
128
-
class="backdrop-blur-none"
129
-
onclick={() => {
130
-
newCard('link');
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
131
}}
132
-
{...props}
133
-
>
134
-
<svg
0
135
xmlns="http://www.w3.org/2000/svg"
136
fill="none"
137
-
viewBox="-2 -2 28 28"
138
stroke-width="2"
139
stroke="currentColor"
0
140
>
141
-
<path
142
-
stroke-linecap="round"
143
-
stroke-linejoin="round"
144
-
d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244"
145
-
/>
146
</svg>
147
</Button>
148
-
{/snippet}
149
-
<Input
150
-
spellcheck={false}
151
-
type="url"
152
-
bind:value={linkValue}
153
-
onkeydown={(event) => {
154
-
if (event.code === 'Enter') {
155
-
addLink(linkValue);
156
-
event.preventDefault();
157
-
}
158
}}
159
-
placeholder="Enter link"
160
-
/>
161
-
<Button onclick={() => addLink(linkValue)} size="icon"
162
-
><svg
163
xmlns="http://www.w3.org/2000/svg"
164
fill="none"
165
viewBox="0 0 24 24"
166
stroke-width="2"
167
stroke="currentColor"
168
-
class="size-6"
169
>
170
-
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
0
0
0
0
171
</svg>
172
</Button>
173
-
</Popover>
174
175
-
<Button
176
-
size="iconLg"
177
-
variant="ghost"
178
-
class="backdrop-blur-none"
179
-
onclick={() => {
180
-
imageInputRef?.click();
181
-
}}
182
-
>
183
-
<svg
184
-
xmlns="http://www.w3.org/2000/svg"
185
-
fill="none"
186
-
viewBox="0 0 24 24"
187
-
stroke-width="2"
188
-
stroke="currentColor"
189
-
>
190
-
<path
191
-
stroke-linecap="round"
192
-
stroke-linejoin="round"
193
-
d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z"
194
-
/>
195
-
</svg>
196
-
</Button>
0
0
197
198
-
{#if dev}
199
<Button
200
size="iconLg"
201
variant="ghost"
202
class="backdrop-blur-none"
203
-
onclick={() => {
204
-
videoInputRef?.click();
205
-
}}
206
>
207
<svg
208
xmlns="http://www.w3.org/2000/svg"
···
211
stroke-width="1.5"
212
stroke="currentColor"
213
>
214
-
<path
215
-
stroke-linecap="round"
216
-
stroke-linejoin="round"
217
-
d="m15.75 10.5 4.72-4.72a.75.75 0 0 1 1.28.53v11.38a.75.75 0 0 1-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 0 0 2.25-2.25v-9a2.25 2.25 0 0 0-2.25-2.25h-9A2.25 2.25 0 0 0 2.25 7.5v9a2.25 2.25 0 0 0 2.25 2.25Z"
218
-
/>
219
</svg>
220
</Button>
221
-
{/if}
222
-
223
-
<Button size="iconLg" variant="ghost" class="backdrop-blur-none" popovertarget="mobile-menu">
224
-
<svg
225
-
xmlns="http://www.w3.org/2000/svg"
226
-
fill="none"
227
-
viewBox="0 0 24 24"
228
-
stroke-width="1.5"
229
-
stroke="currentColor"
230
-
>
231
-
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
232
-
</svg>
233
-
</Button>
234
-
</div>
235
-
<div class="flex items-center gap-2">
236
<Toggle
237
class="hidden bg-transparent backdrop-blur-none lg:block dark:bg-transparent"
238
bind:pressed={showingMobileView}
···
1
<script lang="ts">
2
import { dev } from '$app/environment';
3
import { user } from '$lib/atproto';
4
+
import { COLUMNS } from '$lib';
5
+
import type { Item, WebsiteData } from '$lib/types';
6
+
import { CardDefinitionsByType } from '$lib/cards';
7
import { Button, Input, Navbar, Popover, Toggle, toast } from '@foxui/core';
8
+
import { ColorSelect } from '@foxui/colors';
9
10
let {
11
data,
···
20
save,
21
22
handleImageInputChange,
23
+
handleVideoInputChange,
24
+
25
+
selectedCard = null,
26
+
isMobile = false,
27
+
isCoarse = false,
28
+
ondeselect,
29
+
ondelete,
30
+
onsetsize
31
}: {
32
data: WebsiteData;
33
linkValue: string;
···
43
44
handleImageInputChange: (evt: Event) => void;
45
handleVideoInputChange: (evt: Event) => void;
46
+
47
+
selectedCard?: Item | null;
48
+
isMobile?: boolean;
49
+
isCoarse?: boolean;
50
+
ondeselect?: () => void;
51
+
ondelete?: () => void;
52
+
onsetsize?: (w: number, h: number) => void;
53
} = $props();
54
55
let linkPopoverOpen = $state(false);
···
69
await navigator.clipboard.writeText(url);
70
toast.success('Link copied to clipboard!');
71
}
72
+
73
+
let colorsChoices = [
74
+
{ class: 'text-base-500', label: 'base' },
75
+
{ class: 'text-accent-500', label: 'accent' },
76
+
{ class: 'text-base-300 dark:text-base-700', label: 'transparent' },
77
+
{ class: 'text-red-500', label: 'red' },
78
+
{ class: 'text-orange-500', label: 'orange' },
79
+
{ class: 'text-amber-500', label: 'amber' },
80
+
{ class: 'text-yellow-500', label: 'yellow' },
81
+
{ class: 'text-lime-500', label: 'lime' },
82
+
{ class: 'text-green-500', label: 'green' },
83
+
{ class: 'text-emerald-500', label: 'emerald' },
84
+
{ class: 'text-teal-500', label: 'teal' },
85
+
{ class: 'text-cyan-500', label: 'cyan' },
86
+
{ class: 'text-sky-500', label: 'sky' },
87
+
{ class: 'text-blue-500', label: 'blue' },
88
+
{ class: 'text-indigo-500', label: 'indigo' },
89
+
{ class: 'text-violet-500', label: 'violet' },
90
+
{ class: 'text-purple-500', label: 'purple' },
91
+
{ class: 'text-fuchsia-500', label: 'fuchsia' },
92
+
{ class: 'text-pink-500', label: 'pink' },
93
+
{ class: 'text-rose-500', label: 'rose' }
94
+
];
95
+
96
+
let selectedColor = $derived(
97
+
selectedCard
98
+
? colorsChoices.find((c) => (selectedCard!.color ?? 'base') === c.label)
99
+
: undefined
100
+
);
101
+
102
+
let cardDef = $derived(
103
+
selectedCard ? (CardDefinitionsByType[selectedCard.cardType] ?? null) : null
104
+
);
105
+
106
+
let colorPopoverOpen = $state(false);
107
+
let settingsPopoverOpen = $state(false);
108
+
109
+
const minW = $derived(cardDef?.minW ?? 2);
110
+
const minH = $derived(cardDef?.minH ?? 2);
111
+
const maxW = $derived(cardDef?.maxW ?? COLUMNS);
112
+
const maxH = $derived(cardDef?.maxH ?? (isMobile ? 12 : 6));
113
+
114
+
function canSetSize(w: number, h: number) {
115
+
if (!cardDef) return false;
116
+
if (isMobile) {
117
+
return w >= minW && w * 2 <= maxW && h >= minH && h * 2 <= maxH;
118
+
}
119
+
return w >= minW && w <= maxW && h >= minH && h <= maxH;
120
+
}
121
+
122
+
const showMobileEditControls = $derived(isCoarse && selectedCard);
123
</script>
124
125
<input
···
142
143
{#if dev || (user.isLoggedIn && user.profile?.did === data.did)}
144
<Navbar
145
+
class="dark:bg-base-900 bg-base-100 top-auto bottom-2 mx-4 mt-3 max-w-3xl rounded-full px-4 md:mx-auto"
0
0
0
146
>
147
+
{#if showMobileEditControls}
148
+
<!-- Mobile edit controls when a card is selected -->
149
+
<div class="flex items-center gap-1">
150
+
<Button
151
+
size="iconLg"
152
+
variant="ghost"
153
+
class="backdrop-blur-none"
154
+
onclick={() => ondeselect?.()}
155
+
>
156
+
<svg
157
+
xmlns="http://www.w3.org/2000/svg"
158
+
fill="none"
159
+
viewBox="0 0 24 24"
160
+
stroke-width="2"
161
+
stroke="currentColor"
162
+
class="size-5"
163
+
>
164
+
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
165
+
</svg>
166
+
</Button>
167
+
168
+
<Button
169
+
size="iconLg"
170
+
variant="ghost"
171
+
class="text-rose-500 backdrop-blur-none"
172
+
onclick={() => ondelete?.()}
173
>
174
+
<svg
175
+
xmlns="http://www.w3.org/2000/svg"
176
+
fill="none"
177
+
viewBox="0 0 24 24"
178
+
stroke-width="1.5"
179
+
stroke="currentColor"
180
+
class="size-5"
181
+
>
182
+
<path
183
+
stroke-linecap="round"
184
+
stroke-linejoin="round"
185
+
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
186
+
/>
187
+
</svg>
188
+
</Button>
189
190
+
{#if cardDef?.allowSetColor !== false}
191
+
<Popover bind:open={colorPopoverOpen}>
192
+
{#snippet child({ props })}
193
+
<button
194
+
{...props}
195
+
class={[
196
+
'm-1 size-5 cursor-pointer rounded-full',
197
+
!selectedCard?.color ||
198
+
selectedCard.color === 'base' ||
199
+
selectedCard.color === 'transparent'
200
+
? 'text-base-800 dark:text-base-200'
201
+
: 'text-accent-500'
202
+
]}
203
+
>
204
+
<svg
205
+
xmlns="http://www.w3.org/2000/svg"
206
+
viewBox="0 0 24 24"
207
+
fill="currentColor"
208
+
class="size-5"
209
+
>
210
+
<path
211
+
fill-rule="evenodd"
212
+
d="M20.599 1.5c-.376 0-.743.111-1.055.32l-5.08 3.385a18.747 18.747 0 0 0-3.471 2.987 10.04 10.04 0 0 1 4.815 4.815 18.748 18.748 0 0 0 2.987-3.472l3.386-5.079A1.902 1.902 0 0 0 20.599 1.5Zm-8.3 14.025a18.76 18.76 0 0 0 1.896-1.207 8.026 8.026 0 0 0-4.513-4.513A18.75 18.75 0 0 0 8.475 11.7l-.278.5a5.26 5.26 0 0 1 3.601 3.602l.502-.278ZM6.75 13.5A3.75 3.75 0 0 0 3 17.25a1.5 1.5 0 0 1-1.601 1.497.75.75 0 0 0-.7 1.123 5.25 5.25 0 0 0 9.8-2.62 3.75 3.75 0 0 0-3.75-3.75Z"
213
+
clip-rule="evenodd"
214
+
/>
215
+
</svg>
216
+
</button>
217
+
{/snippet}
218
+
<ColorSelect
219
+
selected={selectedColor}
220
+
colors={colorsChoices}
221
+
onselected={(color, previous) => {
222
+
if (typeof previous === 'string' || typeof color === 'string') {
223
+
return;
224
+
}
225
+
if (selectedCard) {
226
+
selectedCard.color = color.label;
227
+
}
228
+
}}
229
+
class="w-64"
230
+
/>
231
+
</Popover>
232
+
{/if}
233
+
234
+
{#if canSetSize(2, 2)}
235
+
<button
236
+
onclick={() => onsetsize?.(4, 4)}
237
+
class="hover:bg-accent-500/10 cursor-pointer rounded-xl p-2 text-xs font-bold"
238
+
>
239
+
S
240
+
</button>
241
+
{/if}
242
+
{#if canSetSize(4, 2)}
243
+
<button
244
+
onclick={() => onsetsize?.(8, 4)}
245
+
class="hover:bg-accent-500/10 cursor-pointer rounded-xl p-2 text-xs font-bold"
246
+
>
247
+
M
248
+
</button>
249
+
{/if}
250
+
{#if canSetSize(2, 4)}
251
+
<button
252
+
onclick={() => onsetsize?.(4, 8)}
253
+
class="hover:bg-accent-500/10 cursor-pointer rounded-xl p-2 text-xs font-bold"
254
+
>
255
+
L
256
+
</button>
257
+
{/if}
258
+
{#if canSetSize(4, 4)}
259
+
<button
260
+
onclick={() => onsetsize?.(8, 8)}
261
+
class="hover:bg-accent-500/10 cursor-pointer rounded-xl p-2 text-xs font-bold"
262
+
>
263
+
XL
264
+
</button>
265
+
{/if}
266
+
267
+
{#if cardDef?.settingsComponent && selectedCard}
268
+
<Popover bind:open={settingsPopoverOpen} class="bg-base-50 dark:bg-base-900">
269
+
{#snippet child({ props })}
270
+
<button {...props} class="hover:bg-accent-500/10 cursor-pointer rounded-xl p-2">
271
+
<svg
272
+
xmlns="http://www.w3.org/2000/svg"
273
+
fill="none"
274
+
viewBox="0 0 24 24"
275
+
stroke-width="2"
276
+
stroke="currentColor"
277
+
class="size-5"
278
+
>
279
+
<path
280
+
stroke-linecap="round"
281
+
stroke-linejoin="round"
282
+
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z"
283
+
/>
284
+
<path
285
+
stroke-linecap="round"
286
+
stroke-linejoin="round"
287
+
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
288
+
/>
289
+
</svg>
290
+
</button>
291
+
{/snippet}
292
+
<cardDef.settingsComponent
293
+
bind:item={selectedCard}
294
+
onclose={() => {
295
+
settingsPopoverOpen = false;
296
+
}}
297
+
/>
298
+
</Popover>
299
+
{/if}
300
+
</div>
301
+
{:else}
302
+
<!-- Normal add-card controls -->
303
+
<div class="flex items-center gap-2">
304
+
<Button
305
+
size="iconLg"
306
+
variant="ghost"
307
+
class="backdrop-blur-none"
308
+
onclick={() => {
309
+
newCard('section');
310
+
}}
311
+
>
312
+
<svg
313
+
xmlns="http://www.w3.org/2000/svg"
314
+
viewBox="0 0 24 24"
315
fill="none"
316
stroke="currentColor"
317
+
stroke-width="2"
318
stroke-linecap="round"
319
stroke-linejoin="round"
320
+
><path d="M6 12h12" /><path d="M6 20V4" /><path d="M18 20V4" /></svg
321
+
>
322
+
</Button>
323
+
324
+
<Button
325
+
size="iconLg"
326
+
variant="ghost"
327
+
class="backdrop-blur-none"
328
+
onclick={() => {
329
+
newCard('text');
330
+
}}
331
>
332
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
333
+
><path
334
+
fill="none"
335
+
stroke="currentColor"
336
+
stroke-linecap="round"
337
+
stroke-linejoin="round"
338
+
stroke-width="2"
339
+
d="m15 16l2.536-7.328a1.02 1.02 1 0 1 1.928 0L22 16m-6.303-2h5.606M2 16l4.039-9.69a.5.5 0 0 1 .923 0L11 16m-7.696-3h6.392"
340
+
/></svg
341
+
>
342
+
</Button>
343
344
+
<Popover sideOffset={16} bind:open={linkPopoverOpen} class="bg-base-100 dark:bg-base-900">
345
+
{#snippet child({ props })}
346
+
<Button
347
+
size="iconLg"
348
+
variant="ghost"
349
+
class="backdrop-blur-none"
350
+
onclick={() => {
351
+
newCard('link');
352
+
}}
353
+
{...props}
354
+
>
355
+
<svg
356
+
xmlns="http://www.w3.org/2000/svg"
357
+
fill="none"
358
+
viewBox="-2 -2 28 28"
359
+
stroke-width="2"
360
+
stroke="currentColor"
361
+
>
362
+
<path
363
+
stroke-linecap="round"
364
+
stroke-linejoin="round"
365
+
d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244"
366
+
/>
367
+
</svg>
368
+
</Button>
369
+
{/snippet}
370
+
<Input
371
+
spellcheck={false}
372
+
type="url"
373
+
bind:value={linkValue}
374
+
onkeydown={(event) => {
375
+
if (event.code === 'Enter') {
376
+
addLink(linkValue);
377
+
event.preventDefault();
378
+
}
379
}}
380
+
placeholder="Enter link"
381
+
/>
382
+
<Button onclick={() => addLink(linkValue)} size="icon"
383
+
><svg
384
xmlns="http://www.w3.org/2000/svg"
385
fill="none"
386
+
viewBox="0 0 24 24"
387
stroke-width="2"
388
stroke="currentColor"
389
+
class="size-6"
390
>
391
+
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
0
0
0
0
392
</svg>
393
</Button>
394
+
</Popover>
395
+
396
+
<Button
397
+
size="iconLg"
398
+
variant="ghost"
399
+
class="backdrop-blur-none"
400
+
onclick={() => {
401
+
imageInputRef?.click();
0
0
402
}}
403
+
>
404
+
<svg
0
0
405
xmlns="http://www.w3.org/2000/svg"
406
fill="none"
407
viewBox="0 0 24 24"
408
stroke-width="2"
409
stroke="currentColor"
0
410
>
411
+
<path
412
+
stroke-linecap="round"
413
+
stroke-linejoin="round"
414
+
d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z"
415
+
/>
416
</svg>
417
</Button>
0
418
419
+
{#if dev}
420
+
<Button
421
+
size="iconLg"
422
+
variant="ghost"
423
+
class="backdrop-blur-none"
424
+
onclick={() => {
425
+
videoInputRef?.click();
426
+
}}
427
+
>
428
+
<svg
429
+
xmlns="http://www.w3.org/2000/svg"
430
+
fill="none"
431
+
viewBox="0 0 24 24"
432
+
stroke-width="1.5"
433
+
stroke="currentColor"
434
+
>
435
+
<path
436
+
stroke-linecap="round"
437
+
stroke-linejoin="round"
438
+
d="m15.75 10.5 4.72-4.72a.75.75 0 0 1 1.28.53v11.38a.75.75 0 0 1-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 0 0 2.25-2.25v-9a2.25 2.25 0 0 0-2.25-2.25h-9A2.25 2.25 0 0 0 2.25 7.5v9a2.25 2.25 0 0 0 2.25 2.25Z"
439
+
/>
440
+
</svg>
441
+
</Button>
442
+
{/if}
443
0
444
<Button
445
size="iconLg"
446
variant="ghost"
447
class="backdrop-blur-none"
448
+
popovertarget="mobile-menu"
0
0
449
>
450
<svg
451
xmlns="http://www.w3.org/2000/svg"
···
454
stroke-width="1.5"
455
stroke="currentColor"
456
>
457
+
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
0
0
0
0
458
</svg>
459
</Button>
460
+
</div>
461
+
{/if}
462
+
<div class={['flex items-center gap-2', showMobileEditControls ? 'hidden' : '']}>
0
0
0
0
0
0
0
0
0
0
0
0
463
<Toggle
464
class="hidden bg-transparent backdrop-blur-none lg:block dark:bg-transparent"
465
bind:pressed={showingMobileView}
+199
-16
src/lib/website/EditableWebsite.svelte
···
26
import { tick, type Component } from 'svelte';
27
import type { CreationModalComponentProps } from '../cards/types';
28
import { dev } from '$app/environment';
29
-
import { setIsMobile } from './context';
30
import BaseEditingCard from '../cards/BaseCard/BaseEditingCard.svelte';
31
import Context from './Context.svelte';
32
import Head from './Head.svelte';
···
124
125
setIsMobile(() => isMobile);
126
0
0
0
0
0
0
0
0
0
0
0
0
0
127
const getY = (item: Item) => (isMobile ? (item.mobileY ?? item.y) : item.y);
128
const getH = (item: Item) => (isMobile ? (item.mobileH ?? item.h) : item.h);
129
···
140
}
141
142
function newCard(type: string = 'link', cardData?: any) {
0
0
143
// close sidebar if open
144
const popover = document.getElementById('mobile-menu');
145
if (popover) {
···
229
230
let debugPoint = $state({ x: 0, y: 0 });
231
232
-
function getDragXY(
233
-
e: DragEvent & {
234
-
currentTarget: EventTarget & HTMLDivElement;
235
-
}
236
):
237
| { x: number; y: number; swapWithId: string | null; placement: 'above' | 'below' | null }
238
| undefined {
239
if (!container || !activeDragElement.item) return;
240
241
// x, y represent the top-left corner of the dragged card
242
-
const x = e.clientX + activeDragElement.mouseDeltaX;
243
-
const y = e.clientY + activeDragElement.mouseDeltaY;
244
245
const rect = container.getBoundingClientRect();
246
const currentMargin = isMobile ? mobileMargin : margin;
···
362
return { x: gridX, y: gridY, swapWithId, placement };
363
}
364
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
365
let linkValue = $state('');
366
367
function addLink(url: string) {
···
676
<Account {data} />
677
678
<Context {data}>
679
-
{#if !dev}
680
-
<div
681
-
class="bg-base-200 dark:bg-base-800 fixed inset-0 z-50 inline-flex h-full w-full items-center justify-center p-4 text-center lg:hidden"
682
-
>
683
-
Editing on mobile is not supported yet. Please use a desktop browser.
684
-
</div>
685
-
{/if}
686
-
687
<Controls bind:data />
688
689
{#if showingMobileView}
···
732
]}
733
>
734
<div class="pointer-events-none"></div>
735
-
<!-- svelte-ignore a11y_no_static_element_interactions -->
736
<div
737
bind:this={container}
0
0
0
0
0
0
0
0
738
ondragover={(e) => {
739
e.preventDefault();
740
···
911
{save}
912
{handleImageInputChange}
913
{handleVideoInputChange}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
914
/>
915
916
<Toaster />
···
26
import { tick, type Component } from 'svelte';
27
import type { CreationModalComponentProps } from '../cards/types';
28
import { dev } from '$app/environment';
29
+
import { setIsCoarse, setIsMobile, setSelectedCardId, setSelectCard } from './context';
30
import BaseEditingCard from '../cards/BaseCard/BaseEditingCard.svelte';
31
import Context from './Context.svelte';
32
import Head from './Head.svelte';
···
124
125
setIsMobile(() => isMobile);
126
127
+
const isCoarse = typeof window !== 'undefined' && window.matchMedia('(pointer: coarse)').matches;
128
+
setIsCoarse(() => isCoarse);
129
+
130
+
let selectedCardId: string | null = $state(null);
131
+
let selectedCard = $derived(
132
+
selectedCardId ? (items.find((i) => i.id === selectedCardId) ?? null) : null
133
+
);
134
+
135
+
setSelectedCardId(() => selectedCardId);
136
+
setSelectCard((id: string | null) => {
137
+
selectedCardId = id;
138
+
});
139
+
140
const getY = (item: Item) => (isMobile ? (item.mobileY ?? item.y) : item.y);
141
const getH = (item: Item) => (isMobile ? (item.mobileH ?? item.h) : item.h);
142
···
153
}
154
155
function newCard(type: string = 'link', cardData?: any) {
156
+
selectedCardId = null;
157
+
158
// close sidebar if open
159
const popover = document.getElementById('mobile-menu');
160
if (popover) {
···
244
245
let debugPoint = $state({ x: 0, y: 0 });
246
247
+
function getGridPosition(
248
+
clientX: number,
249
+
clientY: number
0
250
):
251
| { x: number; y: number; swapWithId: string | null; placement: 'above' | 'below' | null }
252
| undefined {
253
if (!container || !activeDragElement.item) return;
254
255
// x, y represent the top-left corner of the dragged card
256
+
const x = clientX + activeDragElement.mouseDeltaX;
257
+
const y = clientY + activeDragElement.mouseDeltaY;
258
259
const rect = container.getBoundingClientRect();
260
const currentMargin = isMobile ? mobileMargin : margin;
···
376
return { x: gridX, y: gridY, swapWithId, placement };
377
}
378
379
+
function getDragXY(
380
+
e: DragEvent & {
381
+
currentTarget: EventTarget & HTMLDivElement;
382
+
}
383
+
) {
384
+
return getGridPosition(e.clientX, e.clientY);
385
+
}
386
+
387
+
// Touch drag system (instant drag on selected card)
388
+
let touchDragActive = $state(false);
389
+
390
+
function touchStart(e: TouchEvent) {
391
+
if (!selectedCardId || !container) return;
392
+
const touch = e.touches[0];
393
+
if (!touch) return;
394
+
395
+
// Check if the touch is on the selected card element
396
+
const target = (e.target as HTMLElement)?.closest?.('.card');
397
+
if (!target || target.id !== selectedCardId) return;
398
+
399
+
const item = items.find((i) => i.id === selectedCardId);
400
+
if (!item || item.cardData?.locked) return;
401
+
402
+
// Start dragging immediately
403
+
touchDragActive = true;
404
+
405
+
const cardEl = container.querySelector(`#${CSS.escape(selectedCardId)}`) as HTMLDivElement;
406
+
if (!cardEl) return;
407
+
408
+
activeDragElement.element = cardEl;
409
+
activeDragElement.w = item.w;
410
+
activeDragElement.h = item.h;
411
+
activeDragElement.item = item;
412
+
413
+
// Store original positions of all items
414
+
activeDragElement.originalPositions = new Map();
415
+
for (const it of items) {
416
+
activeDragElement.originalPositions.set(it.id, {
417
+
x: it.x,
418
+
y: it.y,
419
+
mobileX: it.mobileX,
420
+
mobileY: it.mobileY
421
+
});
422
+
}
423
+
424
+
const rect = cardEl.getBoundingClientRect();
425
+
activeDragElement.mouseDeltaX = rect.left - touch.clientX;
426
+
activeDragElement.mouseDeltaY = rect.top - touch.clientY;
427
+
}
428
+
429
+
function touchMove(e: TouchEvent) {
430
+
if (!touchDragActive) return;
431
+
432
+
const touch = e.touches[0];
433
+
if (!touch) return;
434
+
435
+
e.preventDefault();
436
+
437
+
const result = getGridPosition(touch.clientX, touch.clientY);
438
+
if (!result || !activeDragElement.item) return;
439
+
440
+
const draggedOrigPos = activeDragElement.originalPositions.get(activeDragElement.item.id);
441
+
442
+
// Reset all items to original positions first
443
+
for (const it of items) {
444
+
const origPos = activeDragElement.originalPositions.get(it.id);
445
+
if (origPos && it !== activeDragElement.item) {
446
+
if (isMobile) {
447
+
it.mobileX = origPos.mobileX;
448
+
it.mobileY = origPos.mobileY;
449
+
} else {
450
+
it.x = origPos.x;
451
+
it.y = origPos.y;
452
+
}
453
+
}
454
+
}
455
+
456
+
// Update dragged item position
457
+
if (isMobile) {
458
+
activeDragElement.item.mobileX = result.x;
459
+
activeDragElement.item.mobileY = result.y;
460
+
} else {
461
+
activeDragElement.item.x = result.x;
462
+
activeDragElement.item.y = result.y;
463
+
}
464
+
465
+
// Handle horizontal swap
466
+
if (result.swapWithId && draggedOrigPos) {
467
+
const swapTarget = items.find((it) => it.id === result.swapWithId);
468
+
if (swapTarget) {
469
+
if (isMobile) {
470
+
swapTarget.mobileX = draggedOrigPos.mobileX;
471
+
swapTarget.mobileY = draggedOrigPos.mobileY;
472
+
} else {
473
+
swapTarget.x = draggedOrigPos.x;
474
+
swapTarget.y = draggedOrigPos.y;
475
+
}
476
+
}
477
+
}
478
+
479
+
fixCollisions(items, activeDragElement.item, isMobile);
480
+
481
+
// Auto-scroll near edges
482
+
const scrollZone = 100;
483
+
const scrollSpeed = 10;
484
+
const viewportHeight = window.innerHeight;
485
+
486
+
if (touch.clientY < scrollZone) {
487
+
const intensity = 1 - touch.clientY / scrollZone;
488
+
window.scrollBy(0, -scrollSpeed * intensity);
489
+
} else if (touch.clientY > viewportHeight - scrollZone) {
490
+
const intensity = 1 - (viewportHeight - touch.clientY) / scrollZone;
491
+
window.scrollBy(0, scrollSpeed * intensity);
492
+
}
493
+
}
494
+
495
+
function touchEnd() {
496
+
if (touchDragActive && activeDragElement.item) {
497
+
// Finalize position
498
+
fixCollisions(items, activeDragElement.item, isMobile);
499
+
500
+
activeDragElement.x = -1;
501
+
activeDragElement.y = -1;
502
+
activeDragElement.element = null;
503
+
activeDragElement.item = null;
504
+
activeDragElement.lastTargetId = null;
505
+
activeDragElement.lastPlacement = null;
506
+
}
507
+
508
+
touchDragActive = false;
509
+
}
510
+
511
+
// Only register non-passive touchmove when actively dragging
512
+
$effect(() => {
513
+
const el = container;
514
+
if (!touchDragActive || !el) return;
515
+
516
+
el.addEventListener('touchmove', touchMove, { passive: false });
517
+
return () => {
518
+
el.removeEventListener('touchmove', touchMove);
519
+
};
520
+
});
521
+
522
let linkValue = $state('');
523
524
function addLink(url: string) {
···
833
<Account {data} />
834
835
<Context {data}>
0
0
0
0
0
0
0
0
836
<Controls bind:data />
837
838
{#if showingMobileView}
···
881
]}
882
>
883
<div class="pointer-events-none"></div>
884
+
<!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events -->
885
<div
886
bind:this={container}
887
+
onclick={(e) => {
888
+
// Deselect when tapping empty grid space
889
+
if (e.target === e.currentTarget || !(e.target as HTMLElement)?.closest?.('.card')) {
890
+
selectedCardId = null;
891
+
}
892
+
}}
893
+
ontouchstart={touchStart}
894
+
ontouchend={touchEnd}
895
ondragover={(e) => {
896
e.preventDefault();
897
···
1068
{save}
1069
{handleImageInputChange}
1070
{handleVideoInputChange}
1071
+
{selectedCard}
1072
+
{isMobile}
1073
+
{isCoarse}
1074
+
ondeselect={() => {
1075
+
selectedCardId = null;
1076
+
}}
1077
+
ondelete={() => {
1078
+
if (selectedCard) {
1079
+
items = items.filter((it) => it.id !== selectedCardId);
1080
+
compactItems(items, false);
1081
+
compactItems(items, true);
1082
+
selectedCardId = null;
1083
+
}
1084
+
}}
1085
+
onsetsize={(w: number, h: number) => {
1086
+
if (selectedCard) {
1087
+
if (isMobile) {
1088
+
selectedCard.mobileW = w;
1089
+
selectedCard.mobileH = h;
1090
+
} else {
1091
+
selectedCard.w = w;
1092
+
selectedCard.h = h;
1093
+
}
1094
+
fixCollisions(items, selectedCard, isMobile);
1095
+
}
1096
+
}}
1097
/>
1098
1099
<Toaster />
+3
src/lib/website/context.ts
···
7
export const [getCanEdit, setCanEdit] = createContext<() => boolean>();
8
export const [getAdditionalUserData, setAdditionalUserData] =
9
createContext<Record<string, unknown>>();
0
0
0
···
7
export const [getCanEdit, setCanEdit] = createContext<() => boolean>();
8
export const [getAdditionalUserData, setAdditionalUserData] =
9
createContext<Record<string, unknown>>();
10
+
export const [getIsCoarse, setIsCoarse] = createContext<() => boolean>();
11
+
export const [getSelectedCardId, setSelectedCardId] = createContext<() => string | null>();
12
+
export const [getSelectCard, setSelectCard] = createContext<(id: string | null) => void>();