@wells-nelson/interactions 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -2
- package/dist/ActiveList.svelte +168 -0
- package/dist/ActiveList.svelte.d.ts +54 -0
- package/dist/AddRow.svelte +69 -0
- package/dist/AddRow.svelte.d.ts +8 -0
- package/dist/Checkbox.svelte +65 -0
- package/dist/Checkbox.svelte.d.ts +9 -0
- package/dist/CollapsibleSection.svelte +225 -0
- package/dist/CollapsibleSection.svelte.d.ts +34 -0
- package/dist/Combobox.svelte +201 -0
- package/dist/Combobox.svelte.d.ts +22 -0
- package/dist/DevWipeButton.svelte +36 -0
- package/dist/DevWipeButton.svelte.d.ts +10 -0
- package/dist/SearchBar.svelte +60 -0
- package/dist/SearchBar.svelte.d.ts +8 -0
- package/dist/Segmented.svelte +96 -0
- package/dist/Segmented.svelte.d.ts +33 -0
- package/dist/TextField.svelte +42 -0
- package/dist/TextField.svelte.d.ts +10 -0
- package/dist/Toggle.svelte +72 -0
- package/dist/Toggle.svelte.d.ts +9 -0
- package/dist/UndoButton.svelte +41 -0
- package/dist/UndoButton.svelte.d.ts +8 -0
- package/dist/account/AccountPanel.svelte +71 -0
- package/dist/account/AccountPanel.svelte.d.ts +15 -0
- package/dist/account/ContainerEditModal.svelte +284 -0
- package/dist/account/ContainerEditModal.svelte.d.ts +27 -0
- package/dist/account/LocalItemsPrompt.svelte +83 -0
- package/dist/account/LocalItemsPrompt.svelte.d.ts +15 -0
- package/dist/account/ShareModal.svelte +297 -0
- package/dist/account/ShareModal.svelte.d.ts +27 -0
- package/dist/account/SignInCard.svelte +178 -0
- package/dist/account/SignInCard.svelte.d.ts +14 -0
- package/dist/account/SignOutCard.svelte +75 -0
- package/dist/account/SignOutCard.svelte.d.ts +13 -0
- package/dist/account/SyncStatusCard.svelte +134 -0
- package/dist/account/SyncStatusCard.svelte.d.ts +14 -0
- package/dist/account/index.d.ts +8 -0
- package/dist/account/index.js +8 -0
- package/dist/account/types.d.ts +64 -0
- package/dist/account/types.js +17 -0
- package/dist/dnd.d.ts +32 -0
- package/dist/dnd.js +237 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +16 -0
- package/dist/longpress.d.ts +11 -0
- package/dist/longpress.js +63 -0
- package/dist/press.d.ts +18 -0
- package/dist/press.js +36 -0
- package/dist/undo.svelte.d.ts +25 -0
- package/dist/undo.svelte.js +53 -0
- package/dist/wipe.d.ts +15 -0
- package/dist/wipe.js +24 -0
- package/package.json +49 -4
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import SignInCard from './SignInCard.svelte';
|
|
3
|
+
import SignOutCard from './SignOutCard.svelte';
|
|
4
|
+
import SyncStatusCard from './SyncStatusCard.svelte';
|
|
5
|
+
import { capitalize, type AccountActions, type CloudStatus, type Nouns } from './types.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The whole profile page body: "not configured", sign-in, or the signed-in summary with sync
|
|
9
|
+
* status and sign-out. Use the cards directly for a different layout.
|
|
10
|
+
*/
|
|
11
|
+
let {
|
|
12
|
+
cloud,
|
|
13
|
+
account,
|
|
14
|
+
nouns,
|
|
15
|
+
appName,
|
|
16
|
+
returnUrl,
|
|
17
|
+
pendingName,
|
|
18
|
+
emailLink = true,
|
|
19
|
+
signInPitch
|
|
20
|
+
}: {
|
|
21
|
+
cloud: CloudStatus;
|
|
22
|
+
account: AccountActions;
|
|
23
|
+
nouns: Nouns;
|
|
24
|
+
appName: string;
|
|
25
|
+
returnUrl: () => string;
|
|
26
|
+
/** Display name of a container with pending changes (it may already be deleted). */
|
|
27
|
+
pendingName: (id: string) => string;
|
|
28
|
+
emailLink?: boolean;
|
|
29
|
+
signInPitch?: string;
|
|
30
|
+
} = $props();
|
|
31
|
+
|
|
32
|
+
const pending = $derived(cloud.pendingIds.map((id) => ({ id, name: pendingName(id) })));
|
|
33
|
+
const pitch = $derived(
|
|
34
|
+
signInPitch ??
|
|
35
|
+
`Sign in to sync ${nouns.plural} across your devices and share them with others. ` +
|
|
36
|
+
`${capitalize(nouns.plural)} you make while signed out stay local-only on this device.`
|
|
37
|
+
);
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
{#if !cloud.configured}
|
|
41
|
+
<section class="card section">
|
|
42
|
+
<h2>Cloud sync isn't set up</h2>
|
|
43
|
+
<p class="muted">
|
|
44
|
+
This copy of {appName} runs local-only: every {nouns.singular} stays on this device. Signing in becomes available once
|
|
45
|
+
Supabase is configured (see the README).
|
|
46
|
+
</p>
|
|
47
|
+
</section>
|
|
48
|
+
{:else if !cloud.user || cloud.sessionExpired}
|
|
49
|
+
<SignInCard {cloud} {account} {returnUrl} {pitch} {emailLink} />
|
|
50
|
+
{:else}
|
|
51
|
+
<SyncStatusCard {cloud} {account} {nouns} {pending} />
|
|
52
|
+
<SignOutCard {cloud} {account} {nouns} {pending} />
|
|
53
|
+
{/if}
|
|
54
|
+
|
|
55
|
+
<style>
|
|
56
|
+
.section {
|
|
57
|
+
display: flex;
|
|
58
|
+
flex-direction: column;
|
|
59
|
+
gap: 0.75rem;
|
|
60
|
+
padding: 1rem;
|
|
61
|
+
margin-bottom: 1rem;
|
|
62
|
+
}
|
|
63
|
+
.section p {
|
|
64
|
+
margin: 0;
|
|
65
|
+
line-height: 1.5;
|
|
66
|
+
}
|
|
67
|
+
h2 {
|
|
68
|
+
margin: 0;
|
|
69
|
+
font-size: 1.05rem;
|
|
70
|
+
}
|
|
71
|
+
</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type AccountActions, type CloudStatus, type Nouns } from './types.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
cloud: CloudStatus;
|
|
4
|
+
account: AccountActions;
|
|
5
|
+
nouns: Nouns;
|
|
6
|
+
appName: string;
|
|
7
|
+
returnUrl: () => string;
|
|
8
|
+
/** Display name of a container with pending changes (it may already be deleted). */
|
|
9
|
+
pendingName: (id: string) => string;
|
|
10
|
+
emailLink?: boolean;
|
|
11
|
+
signInPitch?: string;
|
|
12
|
+
};
|
|
13
|
+
declare const AccountPanel: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
14
|
+
type AccountPanel = ReturnType<typeof AccountPanel>;
|
|
15
|
+
export default AccountPanel;
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Button, ConfirmDialog, Icon, Modal } from '@wells-nelson/displays';
|
|
3
|
+
import { untrack } from 'svelte';
|
|
4
|
+
import TextField from '../TextField.svelte';
|
|
5
|
+
import Toggle from '../Toggle.svelte';
|
|
6
|
+
import {
|
|
7
|
+
capitalize,
|
|
8
|
+
errorText,
|
|
9
|
+
type CloudStatus,
|
|
10
|
+
type ContainerLocation,
|
|
11
|
+
type Member,
|
|
12
|
+
type MembersApi,
|
|
13
|
+
type Nouns,
|
|
14
|
+
type Role
|
|
15
|
+
} from './types.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The pencil menu of a container: rename, "open when the app starts", cloud-enabled toggle
|
|
19
|
+
* (with a disconnect warning), and delete or leave. A shared cloud container is left rather
|
|
20
|
+
* than deleted while other owners remain (without roles every member counts as an owner).
|
|
21
|
+
*/
|
|
22
|
+
let {
|
|
23
|
+
containerId,
|
|
24
|
+
name: currentName,
|
|
25
|
+
location,
|
|
26
|
+
ready = true,
|
|
27
|
+
role = 'owner',
|
|
28
|
+
roles = false,
|
|
29
|
+
cloud,
|
|
30
|
+
members: api,
|
|
31
|
+
nouns,
|
|
32
|
+
signInHref,
|
|
33
|
+
contentsLabel = 'everything in it',
|
|
34
|
+
isDefault,
|
|
35
|
+
ondefaultchange,
|
|
36
|
+
onrename,
|
|
37
|
+
onmakecloud,
|
|
38
|
+
onmakelocal,
|
|
39
|
+
ondelete,
|
|
40
|
+
onleave,
|
|
41
|
+
onclose
|
|
42
|
+
}: {
|
|
43
|
+
containerId: string;
|
|
44
|
+
name: string;
|
|
45
|
+
/** Undefined once the container is gone (deleted, or moved under a new id); the modal closes. */
|
|
46
|
+
location: ContainerLocation | undefined;
|
|
47
|
+
ready?: boolean;
|
|
48
|
+
role?: Role;
|
|
49
|
+
roles?: boolean;
|
|
50
|
+
cloud: CloudStatus;
|
|
51
|
+
members: Pick<MembersApi, 'fetchMembers'>;
|
|
52
|
+
nouns: Nouns;
|
|
53
|
+
signInHref: string;
|
|
54
|
+
/** What gets deleted along with it, e.g. "all its tasks and notes". */
|
|
55
|
+
contentsLabel?: string;
|
|
56
|
+
isDefault: boolean;
|
|
57
|
+
ondefaultchange: (isDefault: boolean) => void;
|
|
58
|
+
onrename: (name: string) => void;
|
|
59
|
+
onmakecloud: () => Promise<unknown>;
|
|
60
|
+
onmakelocal: () => Promise<unknown>;
|
|
61
|
+
ondelete: () => Promise<unknown>;
|
|
62
|
+
onleave: () => Promise<unknown>;
|
|
63
|
+
onclose: () => void;
|
|
64
|
+
} = $props();
|
|
65
|
+
|
|
66
|
+
const Singular = $derived(capitalize(nouns.singular));
|
|
67
|
+
const isCloud = $derived(location === 'cloud');
|
|
68
|
+
const isOwner = $derived(!roles || role === 'owner');
|
|
69
|
+
const signedIn = $derived(!!cloud.user);
|
|
70
|
+
|
|
71
|
+
let open = $state(true);
|
|
72
|
+
// svelte-ignore state_referenced_locally
|
|
73
|
+
let name = $state(currentName);
|
|
74
|
+
let error = $state<string | null>(null);
|
|
75
|
+
let busy = $state(false);
|
|
76
|
+
|
|
77
|
+
function close() {
|
|
78
|
+
open = false;
|
|
79
|
+
onclose();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
$effect(() => {
|
|
83
|
+
if (ready && !location && !busy) close();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
function save(e: SubmitEvent) {
|
|
87
|
+
e.preventDefault();
|
|
88
|
+
const next = name.trim();
|
|
89
|
+
if (location && isOwner && next && next !== currentName) onrename(next);
|
|
90
|
+
close();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ------------------------------------------------ members (decides delete vs. leave)
|
|
94
|
+
let list = $state<Member[] | null>(null);
|
|
95
|
+
const others = $derived((list ?? []).filter((m) => m.email !== cloud.email));
|
|
96
|
+
const otherOwners = $derived(others.filter((m) => !roles || m.role === 'owner').length);
|
|
97
|
+
|
|
98
|
+
$effect(() => {
|
|
99
|
+
if (!isCloud || !signedIn || !cloud.online) return;
|
|
100
|
+
// Only the conditions above should re-run this, not state the API happens to read.
|
|
101
|
+
untrack(() =>
|
|
102
|
+
api
|
|
103
|
+
.fetchMembers(containerId)
|
|
104
|
+
.then((m) => (list = m))
|
|
105
|
+
.catch(() => (list = null))
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// ------------------------------------------------ cloud / local
|
|
110
|
+
let confirmLocal = $state(false);
|
|
111
|
+
let cloudToggle = $state(false);
|
|
112
|
+
// Mirror the real state whenever no swap is pending (cancelled or failed swaps snap back).
|
|
113
|
+
$effect(() => {
|
|
114
|
+
if (!confirmLocal && !busy) cloudToggle = isCloud;
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
async function run(fn: () => Promise<unknown>) {
|
|
118
|
+
busy = true;
|
|
119
|
+
error = null;
|
|
120
|
+
try {
|
|
121
|
+
await fn();
|
|
122
|
+
} catch (err) {
|
|
123
|
+
error = errorText(err);
|
|
124
|
+
} finally {
|
|
125
|
+
busy = false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const localMessage = $derived(
|
|
130
|
+
`This disconnects “${currentName}” from the copies on your other devices` +
|
|
131
|
+
(others.length > 0 ? ' and from everyone it’s shared with' : '') +
|
|
132
|
+
'. From then on, changes made here won’t reach them and theirs won’t reach you.\n\n' +
|
|
133
|
+
(others.length > 0
|
|
134
|
+
? `Because it’s shared, the cloud ${nouns.singular} stays for the others; you get a separate local-only copy and leave it.`
|
|
135
|
+
: 'The cloud copy is deleted, so it also disappears from your other devices. This device keeps it.')
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
// ------------------------------------------------ delete / leave
|
|
139
|
+
let confirmDelete = $state(false);
|
|
140
|
+
let confirmLeave = $state(false);
|
|
141
|
+
/** Non-owners can only leave; owners leave instead of deleting while other owners remain. */
|
|
142
|
+
const leaveOnly = $derived(isCloud && (!isOwner || otherOwners > 0));
|
|
143
|
+
|
|
144
|
+
const doDelete = () =>
|
|
145
|
+
run(async () => {
|
|
146
|
+
await ondelete();
|
|
147
|
+
close();
|
|
148
|
+
});
|
|
149
|
+
const doLeave = () =>
|
|
150
|
+
run(async () => {
|
|
151
|
+
await onleave();
|
|
152
|
+
close();
|
|
153
|
+
});
|
|
154
|
+
</script>
|
|
155
|
+
|
|
156
|
+
<Modal bind:open title={`Edit ${nouns.singular}`} {onclose}>
|
|
157
|
+
<form id={`edit-${containerId}`} onsubmit={save}>
|
|
158
|
+
{#if isOwner}
|
|
159
|
+
<TextField label={`${Singular} name`} bind:value={name} enterkeyhint="done" />
|
|
160
|
+
{:else}
|
|
161
|
+
<p class="muted small">
|
|
162
|
+
You're {role === 'editor' ? 'an editor' : 'a viewer'} on “{currentName}”. Only owners can rename it.
|
|
163
|
+
</p>
|
|
164
|
+
{/if}
|
|
165
|
+
</form>
|
|
166
|
+
|
|
167
|
+
<Toggle
|
|
168
|
+
label={`Open this ${nouns.singular} when the app starts`}
|
|
169
|
+
checked={isDefault}
|
|
170
|
+
onchange={(on) => ondefaultchange(on)}
|
|
171
|
+
/>
|
|
172
|
+
|
|
173
|
+
<section class="group">
|
|
174
|
+
<h3>Sync</h3>
|
|
175
|
+
{#if signedIn}
|
|
176
|
+
<Toggle
|
|
177
|
+
label="Cloud-enabled"
|
|
178
|
+
bind:checked={cloudToggle}
|
|
179
|
+
disabled={busy}
|
|
180
|
+
onchange={(on) => (on ? run(onmakecloud) : (confirmLocal = true))}
|
|
181
|
+
/>
|
|
182
|
+
<p class="muted small">
|
|
183
|
+
{#if isCloud}
|
|
184
|
+
Synced to your account{cloud.hasPending(containerId) ? ' — changes are waiting to upload' : ''}.
|
|
185
|
+
{:else}
|
|
186
|
+
Only on this device. Turn on to sync{cloud.online ? '' : ' (uploads once you are back online)'}.
|
|
187
|
+
{/if}
|
|
188
|
+
</p>
|
|
189
|
+
{:else}
|
|
190
|
+
<p class="muted small">
|
|
191
|
+
<Icon name="wifi-off" size={16} /> Local-only. <a href={signInHref}>Sign in</a> to sync and share {nouns.plural}.
|
|
192
|
+
</p>
|
|
193
|
+
{/if}
|
|
194
|
+
</section>
|
|
195
|
+
|
|
196
|
+
{#if error}<p class="error">{error}</p>{/if}
|
|
197
|
+
|
|
198
|
+
{#if leaveOnly}
|
|
199
|
+
<Button
|
|
200
|
+
variant="ghost"
|
|
201
|
+
onclick={() => (confirmLeave = true)}
|
|
202
|
+
disabled={busy || !cloud.online}
|
|
203
|
+
style="align-self:flex-start;color:var(--danger)"
|
|
204
|
+
>
|
|
205
|
+
Leave {nouns.singular}
|
|
206
|
+
</Button>
|
|
207
|
+
{#if isOwner}
|
|
208
|
+
<p class="muted small">
|
|
209
|
+
{roles ? 'Other owners remain' : 'It’s shared'}, so the {nouns.singular} stays for them.
|
|
210
|
+
</p>
|
|
211
|
+
{/if}
|
|
212
|
+
{:else}
|
|
213
|
+
<Button
|
|
214
|
+
variant="ghost"
|
|
215
|
+
icon="trash"
|
|
216
|
+
onclick={() => (confirmDelete = true)}
|
|
217
|
+
disabled={busy || (isCloud && !cloud.online)}
|
|
218
|
+
style="align-self:flex-start;color:var(--danger)"
|
|
219
|
+
>
|
|
220
|
+
Delete {nouns.singular}
|
|
221
|
+
</Button>
|
|
222
|
+
{/if}
|
|
223
|
+
{#if isCloud && !cloud.online}
|
|
224
|
+
<p class="muted small">Reconnect to delete or leave a cloud-enabled {nouns.singular}.</p>
|
|
225
|
+
{/if}
|
|
226
|
+
|
|
227
|
+
{#snippet footer()}
|
|
228
|
+
<Button onclick={close}>Cancel</Button>
|
|
229
|
+
<Button variant="primary" type="submit" form={`edit-${containerId}`}>Save</Button>
|
|
230
|
+
{/snippet}
|
|
231
|
+
</Modal>
|
|
232
|
+
|
|
233
|
+
<ConfirmDialog
|
|
234
|
+
bind:open={confirmLocal}
|
|
235
|
+
title={`Disconnect this ${nouns.singular}?`}
|
|
236
|
+
message={localMessage}
|
|
237
|
+
confirmLabel="Make local-only"
|
|
238
|
+
danger
|
|
239
|
+
onconfirm={() => run(onmakelocal)}
|
|
240
|
+
/>
|
|
241
|
+
|
|
242
|
+
<ConfirmDialog
|
|
243
|
+
bind:open={confirmDelete}
|
|
244
|
+
title={`Delete this ${nouns.singular}?`}
|
|
245
|
+
message={`“${currentName}” and ${contentsLabel} will be permanently deleted${others.length ? ' for everyone it is shared with' : ''}.`}
|
|
246
|
+
confirmLabel={`Delete ${nouns.singular}`}
|
|
247
|
+
danger
|
|
248
|
+
onconfirm={doDelete}
|
|
249
|
+
/>
|
|
250
|
+
|
|
251
|
+
<ConfirmDialog
|
|
252
|
+
bind:open={confirmLeave}
|
|
253
|
+
title={`Leave this ${nouns.singular}?`}
|
|
254
|
+
message={`“${currentName}” is shared, so it stays for the others. It will be removed from your account and this device.` +
|
|
255
|
+
(roles ? ' Ask an owner to share it again if you need it back.' : '')}
|
|
256
|
+
confirmLabel={`Leave ${nouns.singular}`}
|
|
257
|
+
danger
|
|
258
|
+
onconfirm={doLeave}
|
|
259
|
+
/>
|
|
260
|
+
|
|
261
|
+
<style>
|
|
262
|
+
.group {
|
|
263
|
+
display: flex;
|
|
264
|
+
flex-direction: column;
|
|
265
|
+
gap: 0.5rem;
|
|
266
|
+
}
|
|
267
|
+
h3 {
|
|
268
|
+
margin: 0;
|
|
269
|
+
font-size: 0.95rem;
|
|
270
|
+
}
|
|
271
|
+
.small {
|
|
272
|
+
margin: 0;
|
|
273
|
+
font-size: 0.9rem;
|
|
274
|
+
line-height: 1.45;
|
|
275
|
+
}
|
|
276
|
+
.small :global(.icon) {
|
|
277
|
+
display: inline;
|
|
278
|
+
vertical-align: -3px;
|
|
279
|
+
}
|
|
280
|
+
.error {
|
|
281
|
+
margin: 0;
|
|
282
|
+
color: var(--danger);
|
|
283
|
+
}
|
|
284
|
+
</style>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type CloudStatus, type ContainerLocation, type MembersApi, type Nouns, type Role } from './types.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
containerId: string;
|
|
4
|
+
name: string;
|
|
5
|
+
/** Undefined once the container is gone (deleted, or moved under a new id); the modal closes. */
|
|
6
|
+
location: ContainerLocation | undefined;
|
|
7
|
+
ready?: boolean;
|
|
8
|
+
role?: Role;
|
|
9
|
+
roles?: boolean;
|
|
10
|
+
cloud: CloudStatus;
|
|
11
|
+
members: Pick<MembersApi, 'fetchMembers'>;
|
|
12
|
+
nouns: Nouns;
|
|
13
|
+
signInHref: string;
|
|
14
|
+
/** What gets deleted along with it, e.g. "all its tasks and notes". */
|
|
15
|
+
contentsLabel?: string;
|
|
16
|
+
isDefault: boolean;
|
|
17
|
+
ondefaultchange: (isDefault: boolean) => void;
|
|
18
|
+
onrename: (name: string) => void;
|
|
19
|
+
onmakecloud: () => Promise<unknown>;
|
|
20
|
+
onmakelocal: () => Promise<unknown>;
|
|
21
|
+
ondelete: () => Promise<unknown>;
|
|
22
|
+
onleave: () => Promise<unknown>;
|
|
23
|
+
onclose: () => void;
|
|
24
|
+
};
|
|
25
|
+
declare const ContainerEditModal: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
26
|
+
type ContainerEditModal = ReturnType<typeof ContainerEditModal>;
|
|
27
|
+
export default ContainerEditModal;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Button, Modal } from '@wells-nelson/displays';
|
|
3
|
+
import Checkbox from '../Checkbox.svelte';
|
|
4
|
+
import { errorText, type Nouns } from './types.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Shown after sign-in for local-only containers that were never offered before. Whatever isn't
|
|
8
|
+
* checked is never asked about again (the container's edit menu can still switch it).
|
|
9
|
+
*/
|
|
10
|
+
let {
|
|
11
|
+
items,
|
|
12
|
+
nouns,
|
|
13
|
+
onmakecloud,
|
|
14
|
+
onprompted
|
|
15
|
+
}: {
|
|
16
|
+
items: { id: string; name: string }[];
|
|
17
|
+
nouns: Nouns;
|
|
18
|
+
/** Make one container cloud-enabled. */
|
|
19
|
+
onmakecloud: (id: string) => Promise<unknown>;
|
|
20
|
+
/** Remember these were offered (checked or not) so they aren't asked about again. */
|
|
21
|
+
onprompted: (ids: string[]) => void;
|
|
22
|
+
} = $props();
|
|
23
|
+
|
|
24
|
+
let open = $state(true);
|
|
25
|
+
let selected = $state<Record<string, boolean>>({});
|
|
26
|
+
let busy = $state(false);
|
|
27
|
+
let error = $state<string | null>(null);
|
|
28
|
+
|
|
29
|
+
async function submit() {
|
|
30
|
+
busy = true;
|
|
31
|
+
error = null;
|
|
32
|
+
try {
|
|
33
|
+
for (const item of items) if (selected[item.id]) await onmakecloud(item.id);
|
|
34
|
+
onprompted(items.map((i) => i.id));
|
|
35
|
+
open = false;
|
|
36
|
+
} catch (err) {
|
|
37
|
+
error = errorText(err);
|
|
38
|
+
} finally {
|
|
39
|
+
busy = false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function skip() {
|
|
44
|
+
onprompted(items.map((i) => i.id));
|
|
45
|
+
open = false;
|
|
46
|
+
}
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<Modal bind:open title={`Sync your local ${nouns.plural}?`} onclose={skip}>
|
|
50
|
+
<p class="intro">
|
|
51
|
+
These {nouns.plural} are only on this device. Check any you want cloud-enabled so they sync across your devices and
|
|
52
|
+
can be shared.
|
|
53
|
+
</p>
|
|
54
|
+
<div class="items">
|
|
55
|
+
{#each items as item (item.id)}
|
|
56
|
+
<!-- Unchecked rows have no entry; bind:checked can't take undefined for a prop with a fallback. -->
|
|
57
|
+
<Checkbox
|
|
58
|
+
label={item.name}
|
|
59
|
+
bind:checked={() => selected[item.id] ?? false, (on) => (selected[item.id] = on)}
|
|
60
|
+
/>
|
|
61
|
+
{/each}
|
|
62
|
+
</div>
|
|
63
|
+
{#if error}<p class="error">{error}</p>{/if}
|
|
64
|
+
{#snippet footer()}
|
|
65
|
+
<Button onclick={skip} disabled={busy}>Not now</Button>
|
|
66
|
+
<Button variant="primary" onclick={submit} disabled={busy}>Make selected cloud-enabled</Button>
|
|
67
|
+
{/snippet}
|
|
68
|
+
</Modal>
|
|
69
|
+
|
|
70
|
+
<style>
|
|
71
|
+
.intro {
|
|
72
|
+
margin: 0;
|
|
73
|
+
line-height: 1.5;
|
|
74
|
+
}
|
|
75
|
+
.items {
|
|
76
|
+
display: flex;
|
|
77
|
+
flex-direction: column;
|
|
78
|
+
}
|
|
79
|
+
.error {
|
|
80
|
+
color: var(--danger);
|
|
81
|
+
margin: 0;
|
|
82
|
+
}
|
|
83
|
+
</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type Nouns } from './types.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
items: {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
}[];
|
|
7
|
+
nouns: Nouns;
|
|
8
|
+
/** Make one container cloud-enabled. */
|
|
9
|
+
onmakecloud: (id: string) => Promise<unknown>;
|
|
10
|
+
/** Remember these were offered (checked or not) so they aren't asked about again. */
|
|
11
|
+
onprompted: (ids: string[]) => void;
|
|
12
|
+
};
|
|
13
|
+
declare const LocalItemsPrompt: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
14
|
+
type LocalItemsPrompt = ReturnType<typeof LocalItemsPrompt>;
|
|
15
|
+
export default LocalItemsPrompt;
|