@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.
Files changed (54) hide show
  1. package/README.md +84 -2
  2. package/dist/ActiveList.svelte +168 -0
  3. package/dist/ActiveList.svelte.d.ts +54 -0
  4. package/dist/AddRow.svelte +69 -0
  5. package/dist/AddRow.svelte.d.ts +8 -0
  6. package/dist/Checkbox.svelte +65 -0
  7. package/dist/Checkbox.svelte.d.ts +9 -0
  8. package/dist/CollapsibleSection.svelte +225 -0
  9. package/dist/CollapsibleSection.svelte.d.ts +34 -0
  10. package/dist/Combobox.svelte +201 -0
  11. package/dist/Combobox.svelte.d.ts +22 -0
  12. package/dist/DevWipeButton.svelte +36 -0
  13. package/dist/DevWipeButton.svelte.d.ts +10 -0
  14. package/dist/SearchBar.svelte +60 -0
  15. package/dist/SearchBar.svelte.d.ts +8 -0
  16. package/dist/Segmented.svelte +96 -0
  17. package/dist/Segmented.svelte.d.ts +33 -0
  18. package/dist/TextField.svelte +42 -0
  19. package/dist/TextField.svelte.d.ts +10 -0
  20. package/dist/Toggle.svelte +72 -0
  21. package/dist/Toggle.svelte.d.ts +9 -0
  22. package/dist/UndoButton.svelte +41 -0
  23. package/dist/UndoButton.svelte.d.ts +8 -0
  24. package/dist/account/AccountPanel.svelte +71 -0
  25. package/dist/account/AccountPanel.svelte.d.ts +15 -0
  26. package/dist/account/ContainerEditModal.svelte +284 -0
  27. package/dist/account/ContainerEditModal.svelte.d.ts +27 -0
  28. package/dist/account/LocalItemsPrompt.svelte +83 -0
  29. package/dist/account/LocalItemsPrompt.svelte.d.ts +15 -0
  30. package/dist/account/ShareModal.svelte +297 -0
  31. package/dist/account/ShareModal.svelte.d.ts +27 -0
  32. package/dist/account/SignInCard.svelte +178 -0
  33. package/dist/account/SignInCard.svelte.d.ts +14 -0
  34. package/dist/account/SignOutCard.svelte +75 -0
  35. package/dist/account/SignOutCard.svelte.d.ts +13 -0
  36. package/dist/account/SyncStatusCard.svelte +134 -0
  37. package/dist/account/SyncStatusCard.svelte.d.ts +14 -0
  38. package/dist/account/index.d.ts +8 -0
  39. package/dist/account/index.js +8 -0
  40. package/dist/account/types.d.ts +64 -0
  41. package/dist/account/types.js +17 -0
  42. package/dist/dnd.d.ts +32 -0
  43. package/dist/dnd.js +237 -0
  44. package/dist/index.d.ts +16 -0
  45. package/dist/index.js +16 -0
  46. package/dist/longpress.d.ts +11 -0
  47. package/dist/longpress.js +63 -0
  48. package/dist/press.d.ts +18 -0
  49. package/dist/press.js +36 -0
  50. package/dist/undo.svelte.d.ts +25 -0
  51. package/dist/undo.svelte.js +53 -0
  52. package/dist/wipe.d.ts +15 -0
  53. package/dist/wipe.js +24 -0
  54. package/package.json +49 -4
@@ -0,0 +1,297 @@
1
+ <script lang="ts">
2
+ import { Badge, Button, IconButton, Modal } from '@wells-nelson/displays';
3
+ import { untrack } from 'svelte';
4
+ import Segmented from '../Segmented.svelte';
5
+ import {
6
+ EMAIL_PATTERN,
7
+ ROLE_OPTIONS,
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
+ * Who a container is shared with: invite by email, active / pending members, remove members.
19
+ * Offers sign-in or making the container cloud-enabled first. With `roles`, owners pick a role
20
+ * per member and the last owner can't be removed or demoted.
21
+ */
22
+ let {
23
+ containerId,
24
+ name,
25
+ location,
26
+ ready = true,
27
+ role = 'owner',
28
+ roles = false,
29
+ cloud,
30
+ members: api,
31
+ nouns,
32
+ appName,
33
+ signInHref,
34
+ signInPitch,
35
+ description,
36
+ onmakecloud,
37
+ onclose
38
+ }: {
39
+ containerId: string;
40
+ name: string;
41
+ /** Undefined once the container is gone (deleted, or moved under a new id); the modal closes. */
42
+ location: ContainerLocation | undefined;
43
+ /** False while the app is still loading its containers (don't treat "missing" as gone). */
44
+ ready?: boolean;
45
+ /** This user's role on the container (ignored without `roles`). */
46
+ role?: Role;
47
+ roles?: boolean;
48
+ cloud: CloudStatus & { setRole?(containerId: string, role: Role): void };
49
+ members: MembersApi;
50
+ nouns: Nouns;
51
+ appName: string;
52
+ signInHref: string;
53
+ signInPitch?: string;
54
+ /** Who can do what, shown above the invite form. */
55
+ description?: string;
56
+ onmakecloud: () => Promise<unknown>;
57
+ onclose: () => void;
58
+ } = $props();
59
+
60
+ const isCloud = $derived(location === 'cloud');
61
+ const isOwner = $derived(!roles || role === 'owner');
62
+ const signedIn = $derived(!!cloud.user);
63
+ const uploaded = $derived(isCloud && !cloud.hasPending(containerId));
64
+
65
+ let open = $state(true);
66
+ let list = $state<Member[] | null>(null);
67
+ let error = $state<string | null>(null);
68
+ let email = $state('');
69
+ let inviteRole = $state<Role>('viewer');
70
+ let busy = $state(false);
71
+
72
+ const others = $derived((list ?? []).filter((m) => m.email !== cloud.email));
73
+ /** With roles everyone is listed (including you, to show your role); without, only the others. */
74
+ const shown = $derived(roles ? (list ?? []) : others);
75
+ const ownerCount = $derived((list ?? []).filter((m) => m.role === 'owner').length);
76
+ const emailValid = $derived(EMAIL_PATTERN.test(email.trim()));
77
+ const isLastOwner = (m: Member) => roles && m.role === 'owner' && ownerCount <= 1;
78
+
79
+ const pitch = $derived(signInPitch ?? `Sign in to share ${nouns.plural} with other people.`);
80
+ const explainer = $derived(
81
+ description ??
82
+ (roles
83
+ ? `Viewers can see everything. Editors can also make changes. Owners can also rename or delete the ${nouns.singular} and manage sharing.`
84
+ : `People you share with can do everything you can, including renaming and deleting the ${nouns.singular}.`)
85
+ );
86
+
87
+ function close() {
88
+ open = false;
89
+ onclose();
90
+ }
91
+
92
+ $effect(() => {
93
+ if (ready && !location && !busy) close();
94
+ });
95
+
96
+ async function load() {
97
+ if (!isCloud || !signedIn) return;
98
+ if (!cloud.online) {
99
+ error = 'You are offline. Sharing details load once you reconnect.';
100
+ return;
101
+ }
102
+ try {
103
+ error = null;
104
+ list = await api.fetchMembers(containerId);
105
+ const me = list.find((m) => m.email === cloud.email);
106
+ if (roles && me?.role) cloud.setRole?.(containerId, me.role);
107
+ } catch (err) {
108
+ error = errorText(err);
109
+ }
110
+ }
111
+
112
+ // Reload when connectivity or the location changes, not on whatever state `load` happens to read.
113
+ $effect(() => {
114
+ void cloud.online;
115
+ void isCloud;
116
+ void untrack(load);
117
+ });
118
+
119
+ async function act(fn: () => Promise<unknown>) {
120
+ busy = true;
121
+ error = null;
122
+ try {
123
+ await fn();
124
+ } catch (err) {
125
+ error = errorText(err);
126
+ } finally {
127
+ busy = false;
128
+ await load();
129
+ }
130
+ }
131
+
132
+ function invite(e: SubmitEvent) {
133
+ e.preventDefault();
134
+ if (!emailValid) return;
135
+ void act(async () => {
136
+ await api.inviteMember(containerId, email, roles ? inviteRole : undefined);
137
+ email = '';
138
+ inviteRole = 'viewer';
139
+ });
140
+ }
141
+ </script>
142
+
143
+ <Modal bind:open title={`Share “${name}”`} {onclose}>
144
+ {#if !cloud.configured}
145
+ <p class="muted">Sharing isn't available because cloud sync isn't set up for this copy of {appName}.</p>
146
+ {:else if !signedIn}
147
+ <p>{pitch}</p>
148
+ <a href={signInHref} onclick={close}>Go to sign in</a>
149
+ {:else if !isCloud}
150
+ <p>Only cloud-enabled {nouns.plural} can be shared. This one is only on this device.</p>
151
+ <Button variant="primary" icon="cloud" onclick={() => act(onmakecloud)} disabled={busy}>Make cloud-enabled</Button>
152
+ {:else}
153
+ <p class="muted small">{explainer}</p>
154
+
155
+ {#if isOwner}
156
+ {#if !uploaded}
157
+ <p class="muted small">Sharing is available once this {nouns.singular} has finished uploading.</p>
158
+ {:else}
159
+ <form class="invite" class:with-role={roles} onsubmit={invite}>
160
+ <input type="email" placeholder="name@example.com" bind:value={email} aria-label="Email to share with" />
161
+ {#if roles}
162
+ <Segmented label="Role for the invite" bind:value={inviteRole} options={ROLE_OPTIONS} />
163
+ {/if}
164
+ <Button variant="primary" type="submit" icon="share" disabled={!emailValid || busy || !cloud.online}>Share</Button>
165
+ </form>
166
+ {/if}
167
+ {:else}
168
+ <p class="muted small">Only owners can change who this {nouns.singular} is shared with.</p>
169
+ {/if}
170
+
171
+ {#if shown.length > 0}
172
+ <ul class="members">
173
+ {#each shown as m (m.id)}
174
+ {@const me = m.email === cloud.email}
175
+ <li>
176
+ <div class="who">
177
+ <span class="email">{m.email}{me ? ' (you)' : ''}</span>
178
+ {#if !m.active}
179
+ <Badge tone="warning">Pending</Badge>
180
+ {:else if !roles}
181
+ <Badge tone="accent">Active</Badge>
182
+ {/if}
183
+ </div>
184
+ {#if roles && isOwner}
185
+ <select
186
+ aria-label={`Role for ${m.email}`}
187
+ value={m.role}
188
+ disabled={busy || !cloud.online || isLastOwner(m) || !api.setMemberRole}
189
+ title={isLastOwner(m)
190
+ ? `A ${nouns.singular} needs at least one owner. Make someone else an owner first.`
191
+ : undefined}
192
+ onchange={(e) => act(() => api.setMemberRole!(m.id, (e.currentTarget as HTMLSelectElement).value as Role))}
193
+ >
194
+ {#each ROLE_OPTIONS as r (r.value)}<option value={r.value}>{r.label}</option>{/each}
195
+ </select>
196
+ {:else if roles}
197
+ <span class="role">{ROLE_OPTIONS.find((r) => r.value === m.role)?.label}</span>
198
+ {/if}
199
+ {#if isOwner && !me}
200
+ <IconButton
201
+ icon="x"
202
+ label={roles ? `Remove ${m.email}` : `Stop sharing with ${m.email}`}
203
+ disabled={busy || !cloud.online || isLastOwner(m)}
204
+ onclick={() => act(() => api.removeMember(m.id))}
205
+ />
206
+ {/if}
207
+ </li>
208
+ {/each}
209
+ </ul>
210
+ {#if roles && isOwner && ownerCount <= 1}
211
+ <p class="muted small">You're the only owner. To step down, make someone else an owner first.</p>
212
+ {/if}
213
+ <p class="muted small">
214
+ Pending means they haven't signed in to {appName} yet; they get the {nouns.singular} the first time they do.
215
+ </p>
216
+ {:else if list}
217
+ <p class="muted small">Not shared with anyone yet.</p>
218
+ {/if}
219
+ {/if}
220
+ {#if error}<p class="error">{error}</p>{/if}
221
+
222
+ {#snippet footer()}
223
+ <Button onclick={close}>Done</Button>
224
+ {/snippet}
225
+ </Modal>
226
+
227
+ <style>
228
+ p {
229
+ margin: 0;
230
+ line-height: 1.5;
231
+ }
232
+ .small {
233
+ font-size: 0.9rem;
234
+ }
235
+ .error {
236
+ color: var(--danger);
237
+ }
238
+ .invite {
239
+ display: flex;
240
+ gap: 0.5rem;
241
+ }
242
+ .invite input {
243
+ flex: 1;
244
+ min-width: 0;
245
+ }
246
+ .invite.with-role {
247
+ flex-direction: column;
248
+ gap: 0.6rem;
249
+ }
250
+ .invite :global(legend) {
251
+ position: absolute;
252
+ width: 1px;
253
+ height: 1px;
254
+ overflow: hidden;
255
+ clip: rect(0 0 0 0);
256
+ }
257
+ .members {
258
+ list-style: none;
259
+ margin: 0;
260
+ padding: 0;
261
+ border: 1px solid var(--border);
262
+ border-radius: var(--radius);
263
+ }
264
+ .members li {
265
+ display: flex;
266
+ align-items: center;
267
+ gap: 0.5rem;
268
+ padding: 0.25rem 0.25rem 0.25rem 0.9rem;
269
+ min-height: 3rem;
270
+ }
271
+ .members li + li {
272
+ border-top: 1px solid var(--border);
273
+ }
274
+ .who {
275
+ flex: 1;
276
+ min-width: 0;
277
+ display: flex;
278
+ flex-wrap: wrap;
279
+ align-items: center;
280
+ gap: 0.25rem 0.5rem;
281
+ }
282
+ .email {
283
+ overflow-wrap: anywhere;
284
+ }
285
+ select {
286
+ font: inherit;
287
+ color: inherit;
288
+ padding: 0.35rem 0.5rem;
289
+ border: 1px solid var(--border-strong);
290
+ border-radius: var(--radius);
291
+ background: var(--surface);
292
+ }
293
+ .role {
294
+ color: var(--muted);
295
+ padding-right: 0.75rem;
296
+ }
297
+ </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
+ /** False while the app is still loading its containers (don't treat "missing" as gone). */
8
+ ready?: boolean;
9
+ /** This user's role on the container (ignored without `roles`). */
10
+ role?: Role;
11
+ roles?: boolean;
12
+ cloud: CloudStatus & {
13
+ setRole?(containerId: string, role: Role): void;
14
+ };
15
+ members: MembersApi;
16
+ nouns: Nouns;
17
+ appName: string;
18
+ signInHref: string;
19
+ signInPitch?: string;
20
+ /** Who can do what, shown above the invite form. */
21
+ description?: string;
22
+ onmakecloud: () => Promise<unknown>;
23
+ onclose: () => void;
24
+ };
25
+ declare const ShareModal: import("svelte").Component<$$ComponentProps, {}, "">;
26
+ type ShareModal = ReturnType<typeof ShareModal>;
27
+ export default ShareModal;
@@ -0,0 +1,178 @@
1
+ <script lang="ts">
2
+ import { Button, Icon } from '@wells-nelson/displays';
3
+ import { EMAIL_PATTERN, errorText, type AccountActions, type CloudStatus } from './types.js';
4
+
5
+ /**
6
+ * "Continue with Google" plus, optionally, "Email me a sign-in link". Shows the expired-session
7
+ * notice and the offline state.
8
+ */
9
+ let {
10
+ cloud,
11
+ account,
12
+ returnUrl,
13
+ pitch,
14
+ emailLink = true,
15
+ googleLabel = 'Continue with Google'
16
+ }: {
17
+ cloud: CloudStatus;
18
+ account: Pick<AccountActions, 'signInWithGoogle' | 'signInWithEmail'>;
19
+ /** Absolute URL to come back to after signing in (usually the profile page). */
20
+ returnUrl: () => string;
21
+ /** Why to sign in, e.g. "Sign in to sync projects across your devices…". */
22
+ pitch: string;
23
+ emailLink?: boolean;
24
+ googleLabel?: string;
25
+ } = $props();
26
+
27
+ let error = $state<string | null>(null);
28
+ let busy = $state(false);
29
+ let email = $state('');
30
+ let linkSentTo = $state<string | null>(null);
31
+ const emailValid = $derived(EMAIL_PATTERN.test(email.trim()));
32
+ const inputId = $props.id();
33
+
34
+ async function google() {
35
+ error = null;
36
+ busy = true;
37
+ try {
38
+ await account.signInWithGoogle(returnUrl());
39
+ } catch (err) {
40
+ error = errorText(err);
41
+ busy = false;
42
+ }
43
+ }
44
+
45
+ async function sendLink(e: SubmitEvent) {
46
+ e.preventDefault();
47
+ if (!emailValid) return;
48
+ error = null;
49
+ busy = true;
50
+ try {
51
+ await account.signInWithEmail(email, returnUrl());
52
+ linkSentTo = email.trim();
53
+ } catch (err) {
54
+ error = errorText(err);
55
+ } finally {
56
+ busy = false;
57
+ }
58
+ }
59
+ </script>
60
+
61
+ <section class="card section">
62
+ {#if cloud.sessionExpired}
63
+ <div class="notice"><Icon name="alert" /> Your session expired. Sign in again to resume syncing.</div>
64
+ {/if}
65
+ <h2>Sign in</h2>
66
+ <p class="muted">{pitch}</p>
67
+ <Button variant="primary" onclick={google} disabled={busy || !cloud.online}>
68
+ <svg width="18" height="18" viewBox="0 0 48 48" aria-hidden="true">
69
+ <path
70
+ fill="#FFC107"
71
+ d="M43.6 20.5H42V20H24v8h11.3C33.7 32.7 29.2 36 24 36c-6.6 0-12-5.4-12-12s5.4-12 12-12c3.1 0 5.9 1.2 8 3.1l5.7-5.7C34 6.1 29.3 4 24 4 12.9 4 4 12.9 4 24s8.9 20 20 20 20-8.9 20-20c0-1.3-.1-2.4-.4-3.5z"
72
+ />
73
+ <path
74
+ fill="#FF3D00"
75
+ d="M6.3 14.7l6.6 4.8C14.7 15.1 19 12 24 12c3.1 0 5.9 1.2 8 3.1l5.7-5.7C34 6.1 29.3 4 24 4 16.3 4 9.7 8.3 6.3 14.7z"
76
+ />
77
+ <path
78
+ fill="#4CAF50"
79
+ d="M24 44c5.2 0 9.9-2 13.4-5.2l-6.2-5.2C29.2 35.1 26.7 36 24 36c-5.2 0-9.6-3.3-11.3-7.9l-6.5 5C9.5 39.6 16.2 44 24 44z"
80
+ />
81
+ <path
82
+ fill="#1976D2"
83
+ d="M43.6 20.5H42V20H24v8h11.3c-.8 2.2-2.2 4.2-4.1 5.6l6.2 5.2C37 39.2 44 34 44 24c0-1.3-.1-2.4-.4-3.5z"
84
+ />
85
+ </svg>
86
+ {googleLabel}
87
+ </Button>
88
+
89
+ {#if emailLink}
90
+ <div class="or muted">or</div>
91
+
92
+ {#if linkSentTo}
93
+ <div class="sent">
94
+ <Icon name="mail" />
95
+ <p>
96
+ Check your email for a sign-in link. We sent it to <strong>{linkSentTo}</strong>. Open it in this browser.
97
+ <button type="button" class="link" onclick={() => (linkSentTo = null)}>Use a different email</button>
98
+ </p>
99
+ </div>
100
+ {:else}
101
+ <form class="email" onsubmit={sendLink}>
102
+ <label for={inputId}>Email me a sign-in link</label>
103
+ <div class="row">
104
+ <input id={inputId} type="email" autocomplete="email" placeholder="name@example.com" bind:value={email} />
105
+ <Button type="submit" disabled={!emailValid || busy || !cloud.online}>Send link</Button>
106
+ </div>
107
+ </form>
108
+ {/if}
109
+ {/if}
110
+ {#if !cloud.online}<p class="muted small">You're offline. Connect to sign in.</p>{/if}
111
+ {#if error}<p class="error">{error}</p>{/if}
112
+ </section>
113
+
114
+ <style>
115
+ .section {
116
+ display: flex;
117
+ flex-direction: column;
118
+ gap: 0.75rem;
119
+ padding: 1rem;
120
+ margin-bottom: 1rem;
121
+ }
122
+ .section p {
123
+ margin: 0;
124
+ line-height: 1.5;
125
+ }
126
+ h2 {
127
+ margin: 0;
128
+ font-size: 1.05rem;
129
+ }
130
+ .small {
131
+ font-size: 0.9rem;
132
+ }
133
+ .or {
134
+ text-align: center;
135
+ font-size: 0.9rem;
136
+ }
137
+ .email {
138
+ display: flex;
139
+ flex-direction: column;
140
+ gap: 0.35rem;
141
+ }
142
+ .email label {
143
+ font-weight: 600;
144
+ font-size: 0.9rem;
145
+ }
146
+ .row {
147
+ display: flex;
148
+ gap: 0.5rem;
149
+ }
150
+ .row input {
151
+ flex: 1;
152
+ min-width: 0;
153
+ }
154
+ .sent {
155
+ display: flex;
156
+ gap: 0.6rem;
157
+ padding: 0.75rem 1rem;
158
+ border-radius: var(--radius);
159
+ background: var(--accent-soft);
160
+ }
161
+ .sent :global(.icon) {
162
+ color: var(--accent-text);
163
+ margin-top: 0.15rem;
164
+ }
165
+ .link {
166
+ display: block;
167
+ margin-top: 0.35rem;
168
+ padding: 0;
169
+ border: none;
170
+ background: none;
171
+ color: var(--accent-text);
172
+ text-decoration: underline;
173
+ cursor: pointer;
174
+ }
175
+ .error {
176
+ color: var(--danger);
177
+ }
178
+ </style>
@@ -0,0 +1,14 @@
1
+ import { type AccountActions, type CloudStatus } from './types.js';
2
+ type $$ComponentProps = {
3
+ cloud: CloudStatus;
4
+ account: Pick<AccountActions, 'signInWithGoogle' | 'signInWithEmail'>;
5
+ /** Absolute URL to come back to after signing in (usually the profile page). */
6
+ returnUrl: () => string;
7
+ /** Why to sign in, e.g. "Sign in to sync projects across your devices…". */
8
+ pitch: string;
9
+ emailLink?: boolean;
10
+ googleLabel?: string;
11
+ };
12
+ declare const SignInCard: import("svelte").Component<$$ComponentProps, {}, "">;
13
+ type SignInCard = ReturnType<typeof SignInCard>;
14
+ export default SignInCard;
@@ -0,0 +1,75 @@
1
+ <script lang="ts">
2
+ import { Button, ConfirmDialog } from '@wells-nelson/displays';
3
+ import { errorText, type AccountActions, type CloudStatus, type Nouns } from './types.js';
4
+
5
+ /** Sign out, with a warning listing anything that hasn't reached the cloud yet. */
6
+ let {
7
+ cloud,
8
+ account,
9
+ nouns,
10
+ pending
11
+ }: {
12
+ cloud: CloudStatus;
13
+ account: Pick<AccountActions, 'signOut'>;
14
+ nouns: Nouns;
15
+ pending: { id: string; name: string }[];
16
+ } = $props();
17
+
18
+ let busy = $state(false);
19
+ let confirming = $state(false);
20
+ let error = $state<string | null>(null);
21
+
22
+ function request() {
23
+ if (cloud.hasAnyPending) confirming = true;
24
+ else void signOut();
25
+ }
26
+
27
+ async function signOut() {
28
+ busy = true;
29
+ error = null;
30
+ try {
31
+ await account.signOut();
32
+ } catch (err) {
33
+ error = errorText(err);
34
+ } finally {
35
+ busy = false;
36
+ }
37
+ }
38
+ </script>
39
+
40
+ <section class="card section">
41
+ <p class="muted small">
42
+ Signing out removes your cloud-enabled {nouns.plural} from this device. Local-only {nouns.plural} stay.
43
+ </p>
44
+ <Button onclick={request} disabled={busy}>Sign out</Button>
45
+ {#if error}<p class="error">{error}</p>{/if}
46
+ </section>
47
+
48
+ <ConfirmDialog
49
+ bind:open={confirming}
50
+ title="Sign out with unsynced changes?"
51
+ message={`These ${nouns.plural} have changes that haven't reached the cloud yet:\n${pending.map((p) => `• ${p.name}`).join('\n')}\n\nIf you sign out now those changes will be lost. Reconnect and use "Force re-sync" first to keep them.`}
52
+ confirmLabel="Sign out anyway"
53
+ danger
54
+ onconfirm={signOut}
55
+ />
56
+
57
+ <style>
58
+ .section {
59
+ display: flex;
60
+ flex-direction: column;
61
+ gap: 0.75rem;
62
+ padding: 1rem;
63
+ margin-bottom: 1rem;
64
+ }
65
+ .section p {
66
+ margin: 0;
67
+ line-height: 1.5;
68
+ }
69
+ .small {
70
+ font-size: 0.9rem;
71
+ }
72
+ .error {
73
+ color: var(--danger);
74
+ }
75
+ </style>
@@ -0,0 +1,13 @@
1
+ import { type AccountActions, type CloudStatus, type Nouns } from './types.js';
2
+ type $$ComponentProps = {
3
+ cloud: CloudStatus;
4
+ account: Pick<AccountActions, 'signOut'>;
5
+ nouns: Nouns;
6
+ pending: {
7
+ id: string;
8
+ name: string;
9
+ }[];
10
+ };
11
+ declare const SignOutCard: import("svelte").Component<$$ComponentProps, {}, "">;
12
+ type SignOutCard = ReturnType<typeof SignOutCard>;
13
+ export default SignOutCard;