@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,34 @@
1
+ import { type Snippet } from 'svelte';
2
+ import { type DragData, type DropZoneOptions } from './dnd.js';
3
+ type $$ComponentProps = {
4
+ title: string;
5
+ count: number;
6
+ countLabel?: (count: number) => string;
7
+ collapsed: boolean;
8
+ ontogglecollapse: () => void;
9
+ /** Opens the section's settings (pencil icon, right of the plus). */
10
+ onedit?: () => void;
11
+ editLabel?: string;
12
+ onadd?: () => void;
13
+ addLabel?: string;
14
+ /** Set to make the whole section long-press draggable (reordering sections). */
15
+ dragData?: DragData;
16
+ ondragstart?: () => void;
17
+ ondragend?: () => void;
18
+ /** Accept dropped rows. */
19
+ drop?: DropZoneOptions;
20
+ /** `subtle` gives the header a muted background (e.g. an "unallocated" bucket). */
21
+ variant?: 'default' | 'subtle';
22
+ /**
23
+ * `card` (default): a rounded card. `lines`: fills its container edge to edge with no card,
24
+ * so sections and rows read like lines on paper. The pinned header needs an opaque
25
+ * background: set `--section-header-bg` to whatever it sits on (default `--bg`).
26
+ */
27
+ appearance?: 'card' | 'lines';
28
+ /** The fixed app header, measured when scrolling a collapsed section back into view. */
29
+ appHeaderSelector?: string;
30
+ children: Snippet;
31
+ };
32
+ declare const CollapsibleSection: import("svelte").Component<$$ComponentProps, {}, "">;
33
+ type CollapsibleSection = ReturnType<typeof CollapsibleSection>;
34
+ export default CollapsibleSection;
@@ -0,0 +1,201 @@
1
+ <script lang="ts" module>
2
+ export type ComboChoice = { id: string } | { newName: string } | null;
3
+ export interface ComboOption {
4
+ id: string;
5
+ label: string;
6
+ }
7
+
8
+ export function sameName(a: string, b: string) {
9
+ return a.trim().toLocaleLowerCase() === b.trim().toLocaleLowerCase();
10
+ }
11
+ </script>
12
+
13
+ <script lang="ts">
14
+ /**
15
+ * Dropdown with type-to-filter. Typing text that doesn't exactly match an option (ignoring
16
+ * case and surrounding spaces) offers to create a new entry with that name.
17
+ */
18
+ let {
19
+ label,
20
+ options,
21
+ choice = $bindable(null),
22
+ placeholder = 'Select or type to create…',
23
+ createLabel = 'Create',
24
+ error,
25
+ hideLabel = false
26
+ }: {
27
+ label: string;
28
+ options: ComboOption[];
29
+ choice?: ComboChoice;
30
+ placeholder?: string;
31
+ createLabel?: string;
32
+ error?: string | null;
33
+ hideLabel?: boolean;
34
+ } = $props();
35
+
36
+ const id = $props.id();
37
+ const listId = `${id}-list`;
38
+
39
+ function labelFor(c: ComboChoice) {
40
+ if (!c) return '';
41
+ if ('newName' in c) return c.newName;
42
+ return options.find((o) => o.id === c.id)?.label ?? '';
43
+ }
44
+
45
+ let text = $state(labelFor(choice));
46
+ let open = $state(false);
47
+ let highlighted = $state(0);
48
+ let filtering = $state(false);
49
+
50
+ const query = $derived(text.trim());
51
+ const filtered = $derived(
52
+ filtering && query
53
+ ? options.filter((o) => o.label.toLocaleLowerCase().includes(query.toLocaleLowerCase()))
54
+ : options
55
+ );
56
+ const exact = $derived(options.find((o) => sameName(o.label, query)));
57
+ const canCreate = $derived(query.length > 0 && !exact);
58
+ const rows = $derived<Array<{ kind: 'option'; option: ComboOption } | { kind: 'create' }>>([
59
+ ...filtered.map((option) => ({ kind: 'option' as const, option })),
60
+ ...(canCreate ? [{ kind: 'create' as const }] : [])
61
+ ]);
62
+
63
+ function select(row: (typeof rows)[number]) {
64
+ if (row.kind === 'option') {
65
+ choice = { id: row.option.id };
66
+ text = row.option.label;
67
+ } else {
68
+ choice = { newName: query };
69
+ text = query;
70
+ }
71
+ open = false;
72
+ filtering = false;
73
+ }
74
+
75
+ function onInput() {
76
+ open = true;
77
+ filtering = true;
78
+ highlighted = 0;
79
+ // Keep the choice in step with what's typed.
80
+ choice = exact ? { id: exact.id } : query ? { newName: query } : null;
81
+ }
82
+
83
+ function onKeydown(e: KeyboardEvent) {
84
+ if (e.key === 'ArrowDown') {
85
+ e.preventDefault();
86
+ open = true;
87
+ highlighted = Math.min(highlighted + 1, rows.length - 1);
88
+ } else if (e.key === 'ArrowUp') {
89
+ e.preventDefault();
90
+ highlighted = Math.max(highlighted - 1, 0);
91
+ } else if (e.key === 'Enter' && open && rows[highlighted]) {
92
+ e.preventDefault();
93
+ select(rows[highlighted]);
94
+ } else if (e.key === 'Escape' && open) {
95
+ e.preventDefault();
96
+ e.stopPropagation();
97
+ open = false;
98
+ }
99
+ }
100
+
101
+ let blurTimer: ReturnType<typeof setTimeout> | undefined;
102
+ // Don't let a pending blur timer run after the component is gone (e.g. its modal closed).
103
+ $effect(() => () => clearTimeout(blurTimer));
104
+
105
+ function onBlur() {
106
+ // Let a click on an option land before closing.
107
+ clearTimeout(blurTimer);
108
+ blurTimer = setTimeout(() => {
109
+ open = false;
110
+ filtering = false;
111
+ text = labelFor(choice);
112
+ }, 150);
113
+ }
114
+ </script>
115
+
116
+ <div class="combo">
117
+ <label for={id} class:visually-hidden={hideLabel}>{label}</label>
118
+ <input
119
+ {id}
120
+ type="text"
121
+ role="combobox"
122
+ autocomplete="off"
123
+ aria-expanded={open}
124
+ aria-controls={listId}
125
+ aria-autocomplete="list"
126
+ aria-invalid={error ? 'true' : undefined}
127
+ {placeholder}
128
+ bind:value={text}
129
+ oninput={onInput}
130
+ onfocus={() => (open = true)}
131
+ onclick={() => (open = true)}
132
+ onblur={onBlur}
133
+ onkeydown={onKeydown}
134
+ />
135
+ {#if open && rows.length > 0}
136
+ <ul id={listId} role="listbox" class="options">
137
+ {#each rows as row, i (row.kind === 'option' ? row.option.id : '__create')}
138
+ <li
139
+ role="option"
140
+ aria-selected={i === highlighted}
141
+ class:highlighted={i === highlighted}
142
+ class:create={row.kind === 'create'}
143
+ onmousedown={(e) => {
144
+ e.preventDefault();
145
+ select(row);
146
+ }}
147
+ onmouseenter={() => (highlighted = i)}
148
+ >
149
+ {#if row.kind === 'option'}{row.option.label}{:else}{createLabel} “{query}”{/if}
150
+ </li>
151
+ {/each}
152
+ </ul>
153
+ {/if}
154
+ {#if error}<p class="error">{error}</p>{/if}
155
+ </div>
156
+
157
+ <style>
158
+ .combo {
159
+ position: relative;
160
+ display: flex;
161
+ flex-direction: column;
162
+ gap: 0.35rem;
163
+ }
164
+ label {
165
+ font-weight: 600;
166
+ font-size: 0.9rem;
167
+ }
168
+ .options {
169
+ position: absolute;
170
+ z-index: 20;
171
+ top: 100%;
172
+ left: 0;
173
+ right: 0;
174
+ margin: 0.25rem 0 0;
175
+ padding: 0.25rem;
176
+ list-style: none;
177
+ max-height: 14rem;
178
+ overflow-y: auto;
179
+ background: var(--surface);
180
+ border: 1px solid var(--border-strong);
181
+ border-radius: var(--radius);
182
+ box-shadow: var(--shadow-lg);
183
+ }
184
+ li {
185
+ padding: 0.55rem 0.75rem;
186
+ border-radius: calc(var(--radius) - 2px);
187
+ cursor: pointer;
188
+ }
189
+ li.highlighted {
190
+ background: var(--surface-hover);
191
+ }
192
+ li.create {
193
+ color: var(--accent-text);
194
+ font-weight: 600;
195
+ }
196
+ .error {
197
+ margin: 0;
198
+ color: var(--danger);
199
+ font-size: 0.85rem;
200
+ }
201
+ </style>
@@ -0,0 +1,22 @@
1
+ export type ComboChoice = {
2
+ id: string;
3
+ } | {
4
+ newName: string;
5
+ } | null;
6
+ export interface ComboOption {
7
+ id: string;
8
+ label: string;
9
+ }
10
+ export declare function sameName(a: string, b: string): boolean;
11
+ type $$ComponentProps = {
12
+ label: string;
13
+ options: ComboOption[];
14
+ choice?: ComboChoice;
15
+ placeholder?: string;
16
+ createLabel?: string;
17
+ error?: string | null;
18
+ hideLabel?: boolean;
19
+ };
20
+ declare const Combobox: import("svelte").Component<$$ComponentProps, {}, "choice">;
21
+ type Combobox = ReturnType<typeof Combobox>;
22
+ export default Combobox;
@@ -0,0 +1,36 @@
1
+ <script lang="ts">
2
+ import { ConfirmDialog, IconButton } from '@wells-nelson/displays';
3
+
4
+ /**
5
+ * Development-only "wipe everything" button (trash icon, danger color) with a confirmation.
6
+ * Pair it with `wipeAllData`; render it only in dev builds.
7
+ */
8
+ let {
9
+ onconfirm,
10
+ message = 'Development only: deletes every database and setting in this browser, signs you out here, and reloads so the app starts from scratch.\n\nNothing is deleted on the server.',
11
+ title = 'Wipe all data?',
12
+ label = 'Wipe all local data (dev only)',
13
+ confirmLabel = 'Wipe everything'
14
+ }: {
15
+ onconfirm: () => void | Promise<void>;
16
+ message?: string;
17
+ title?: string;
18
+ label?: string;
19
+ confirmLabel?: string;
20
+ } = $props();
21
+
22
+ let confirming = $state(false);
23
+ </script>
24
+
25
+ <span class="dev-wipe">
26
+ <IconButton icon="trash" {label} onclick={() => (confirming = true)} />
27
+ </span>
28
+
29
+ <ConfirmDialog bind:open={confirming} {title} {message} {confirmLabel} danger {onconfirm} />
30
+
31
+ <style>
32
+ .dev-wipe {
33
+ display: inline-flex;
34
+ color: var(--danger);
35
+ }
36
+ </style>
@@ -0,0 +1,10 @@
1
+ type $$ComponentProps = {
2
+ onconfirm: () => void | Promise<void>;
3
+ message?: string;
4
+ title?: string;
5
+ label?: string;
6
+ confirmLabel?: string;
7
+ };
8
+ declare const DevWipeButton: import("svelte").Component<$$ComponentProps, {}, "">;
9
+ type DevWipeButton = ReturnType<typeof DevWipeButton>;
10
+ export default DevWipeButton;
@@ -0,0 +1,60 @@
1
+ <script lang="ts">
2
+ import { Icon, IconButton } from '@wells-nelson/displays';
3
+
4
+ let { value = $bindable(''), placeholder = 'Search', clearLabel = 'Clear search' }: { value?: string; placeholder?: string; clearLabel?: string } =
5
+ $props();
6
+ </script>
7
+
8
+ <div class="search-bar">
9
+ <div class="inner">
10
+ <Icon name="search" />
11
+ <input type="search" bind:value {placeholder} aria-label={placeholder} enterkeyhint="search" />
12
+ {#if value}
13
+ <IconButton icon="x" label={clearLabel} size={18} onclick={() => (value = '')} />
14
+ {/if}
15
+ </div>
16
+ </div>
17
+ <div class="spacer" aria-hidden="true"></div>
18
+
19
+ <style>
20
+ .search-bar {
21
+ position: fixed;
22
+ left: 0;
23
+ right: 0;
24
+ bottom: 0;
25
+ z-index: 40;
26
+ padding: 0.5rem 1rem calc(0.5rem + env(safe-area-inset-bottom, 0px));
27
+ background: linear-gradient(to top, var(--bg) 70%, transparent);
28
+ }
29
+ .inner {
30
+ display: flex;
31
+ align-items: center;
32
+ gap: 0.5rem;
33
+ max-width: var(--content-w);
34
+ margin: 0 auto;
35
+ padding: 0 0.25rem 0 0.9rem;
36
+ background: var(--surface);
37
+ border: 1px solid var(--border-strong);
38
+ border-radius: 2rem;
39
+ box-shadow: var(--shadow);
40
+ color: var(--muted);
41
+ }
42
+ .inner:focus-within {
43
+ border-color: var(--accent);
44
+ }
45
+ input {
46
+ flex: 1;
47
+ min-width: 0;
48
+ border: none;
49
+ background: transparent;
50
+ padding: 0.7rem 0;
51
+ color: var(--text);
52
+ box-shadow: none !important;
53
+ }
54
+ input::-webkit-search-cancel-button {
55
+ display: none;
56
+ }
57
+ .spacer {
58
+ height: calc(4.5rem + env(safe-area-inset-bottom, 0px));
59
+ }
60
+ </style>
@@ -0,0 +1,8 @@
1
+ type $$ComponentProps = {
2
+ value?: string;
3
+ placeholder?: string;
4
+ clearLabel?: string;
5
+ };
6
+ declare const SearchBar: import("svelte").Component<$$ComponentProps, {}, "value">;
7
+ type SearchBar = ReturnType<typeof SearchBar>;
8
+ export default SearchBar;
@@ -0,0 +1,96 @@
1
+ <script lang="ts" generics="T extends string | number | boolean">
2
+ /** A row of mutually exclusive options (radio group styled as joined buttons). */
3
+ let {
4
+ label,
5
+ options,
6
+ value = $bindable(),
7
+ disabled = false,
8
+ onchange
9
+ }: {
10
+ label: string;
11
+ options: { value: T; label: string }[];
12
+ value: T;
13
+ disabled?: boolean;
14
+ onchange?: (value: T) => void;
15
+ } = $props();
16
+
17
+ const id = $props.id();
18
+ </script>
19
+
20
+ <fieldset class="segmented" {disabled}>
21
+ <legend>{label}</legend>
22
+ <div class="options">
23
+ {#each options as option, i (i)}
24
+ <label class:selected={option.value === value}>
25
+ <input
26
+ type="radio"
27
+ name={id}
28
+ checked={option.value === value}
29
+ onchange={() => {
30
+ value = option.value;
31
+ onchange?.(option.value);
32
+ }}
33
+ />
34
+ {option.label}
35
+ </label>
36
+ {/each}
37
+ </div>
38
+ </fieldset>
39
+
40
+ <style>
41
+ .segmented {
42
+ border: none;
43
+ margin: 0;
44
+ padding: 0;
45
+ display: flex;
46
+ flex-direction: column;
47
+ gap: 0.35rem;
48
+ }
49
+ legend {
50
+ font-weight: 600;
51
+ font-size: 0.9rem;
52
+ padding: 0;
53
+ margin-bottom: 0.35rem;
54
+ }
55
+ .options {
56
+ display: flex;
57
+ border: 1px solid var(--border-strong);
58
+ border-radius: var(--radius);
59
+ overflow: hidden;
60
+ }
61
+ label {
62
+ position: relative;
63
+ flex: 1;
64
+ display: grid;
65
+ place-items: center;
66
+ min-height: 2.5rem;
67
+ padding: 0 0.5rem;
68
+ text-align: center;
69
+ cursor: pointer;
70
+ background: var(--surface);
71
+ font-weight: 500;
72
+ }
73
+ label + label {
74
+ border-left: 1px solid var(--border-strong);
75
+ }
76
+ label.selected {
77
+ background: var(--accent);
78
+ color: var(--on-accent);
79
+ font-weight: 700;
80
+ }
81
+ input {
82
+ position: absolute;
83
+ inset: 0;
84
+ opacity: 0;
85
+ margin: 0;
86
+ cursor: pointer;
87
+ }
88
+ label:has(input:focus-visible) {
89
+ outline: 2px solid var(--accent);
90
+ outline-offset: -3px;
91
+ }
92
+ fieldset:disabled label {
93
+ opacity: 0.55;
94
+ cursor: not-allowed;
95
+ }
96
+ </style>
@@ -0,0 +1,33 @@
1
+ declare function $$render<T extends string | number | boolean>(): {
2
+ props: {
3
+ label: string;
4
+ options: {
5
+ value: T;
6
+ label: string;
7
+ }[];
8
+ value: T;
9
+ disabled?: boolean;
10
+ onchange?: (value: T) => void;
11
+ };
12
+ exports: {};
13
+ bindings: "value";
14
+ slots: {};
15
+ events: {};
16
+ };
17
+ declare class __sveltets_Render<T extends string | number | boolean> {
18
+ props(): ReturnType<typeof $$render<T>>['props'];
19
+ events(): ReturnType<typeof $$render<T>>['events'];
20
+ slots(): ReturnType<typeof $$render<T>>['slots'];
21
+ bindings(): "value";
22
+ exports(): {};
23
+ }
24
+ interface $$IsomorphicComponent {
25
+ new <T extends string | number | boolean>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
26
+ $$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
27
+ } & ReturnType<__sveltets_Render<T>['exports']>;
28
+ <T extends string | number | boolean>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
29
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
30
+ }
31
+ declare const Segmented: $$IsomorphicComponent;
32
+ type Segmented<T extends string | number | boolean> = InstanceType<typeof Segmented<T>>;
33
+ export default Segmented;
@@ -0,0 +1,42 @@
1
+ <script lang="ts">
2
+ import type { HTMLInputAttributes } from 'svelte/elements';
3
+
4
+ let {
5
+ label,
6
+ value = $bindable(''),
7
+ error,
8
+ hint,
9
+ ...rest
10
+ }: HTMLInputAttributes & { label: string; value?: string; error?: string | null; hint?: string } = $props();
11
+
12
+ const id = $props.id();
13
+ </script>
14
+
15
+ <div class="field">
16
+ <label for={id}>{label}</label>
17
+ <input {id} bind:value aria-invalid={error ? 'true' : undefined} {...rest} />
18
+ {#if error}<p class="error">{error}</p>{:else if hint}<p class="hint">{hint}</p>{/if}
19
+ </div>
20
+
21
+ <style>
22
+ .field {
23
+ display: flex;
24
+ flex-direction: column;
25
+ gap: 0.35rem;
26
+ }
27
+ label {
28
+ font-weight: 600;
29
+ font-size: 0.9rem;
30
+ }
31
+ .error,
32
+ .hint {
33
+ margin: 0;
34
+ font-size: 0.85rem;
35
+ }
36
+ .error {
37
+ color: var(--danger);
38
+ }
39
+ .hint {
40
+ color: var(--muted);
41
+ }
42
+ </style>
@@ -0,0 +1,10 @@
1
+ import type { HTMLInputAttributes } from 'svelte/elements';
2
+ type $$ComponentProps = HTMLInputAttributes & {
3
+ label: string;
4
+ value?: string;
5
+ error?: string | null;
6
+ hint?: string;
7
+ };
8
+ declare const TextField: import("svelte").Component<$$ComponentProps, {}, "value">;
9
+ type TextField = ReturnType<typeof TextField>;
10
+ export default TextField;
@@ -0,0 +1,72 @@
1
+ <script lang="ts">
2
+ let {
3
+ checked = $bindable(false),
4
+ label,
5
+ disabled = false,
6
+ onchange
7
+ }: { checked?: boolean; label: string; disabled?: boolean; onchange?: (checked: boolean) => void } = $props();
8
+ </script>
9
+
10
+ <label class="toggle" class:disabled>
11
+ <span class="text">{label}</span>
12
+ <input type="checkbox" role="switch" bind:checked {disabled} onchange={() => onchange?.(checked)} />
13
+ <span class="track" aria-hidden="true"><span class="thumb"></span></span>
14
+ </label>
15
+
16
+ <style>
17
+ .toggle {
18
+ position: relative;
19
+ display: inline-flex;
20
+ align-items: center;
21
+ gap: 0.6rem;
22
+ cursor: pointer;
23
+ user-select: none;
24
+ }
25
+ .disabled {
26
+ opacity: 0.5;
27
+ cursor: not-allowed;
28
+ }
29
+ /* The real input covers the whole control (invisible), so every tap lands on it. */
30
+ input {
31
+ position: absolute;
32
+ inset: 0;
33
+ width: 100%;
34
+ height: 100%;
35
+ margin: 0;
36
+ opacity: 0;
37
+ cursor: pointer;
38
+ z-index: 1;
39
+ }
40
+ input:disabled {
41
+ cursor: not-allowed;
42
+ }
43
+ .track {
44
+ position: relative;
45
+ width: 2.6rem;
46
+ height: 1.5rem;
47
+ border-radius: 1rem;
48
+ background: var(--border-strong);
49
+ transition: background-color 0.15s;
50
+ }
51
+ .thumb {
52
+ position: absolute;
53
+ top: 0.15rem;
54
+ left: 0.15rem;
55
+ width: 1.2rem;
56
+ height: 1.2rem;
57
+ border-radius: 50%;
58
+ background: #fff;
59
+ box-shadow: 0 1px 2px rgb(0 0 0 / 0.3);
60
+ transition: transform 0.15s;
61
+ }
62
+ input:checked + .track {
63
+ background: var(--accent);
64
+ }
65
+ input:checked + .track .thumb {
66
+ transform: translateX(1.1rem);
67
+ }
68
+ input:focus-visible + .track {
69
+ outline: 2px solid var(--accent);
70
+ outline-offset: 2px;
71
+ }
72
+ </style>
@@ -0,0 +1,9 @@
1
+ type $$ComponentProps = {
2
+ checked?: boolean;
3
+ label: string;
4
+ disabled?: boolean;
5
+ onchange?: (checked: boolean) => void;
6
+ };
7
+ declare const Toggle: import("svelte").Component<$$ComponentProps, {}, "checked">;
8
+ type Toggle = ReturnType<typeof Toggle>;
9
+ export default Toggle;
@@ -0,0 +1,41 @@
1
+ <script lang="ts">
2
+ import { Icon } from '@wells-nelson/displays';
3
+ import type { UndoStack } from './undo.svelte.js';
4
+
5
+ /** Floating Undo button, bottom left (above a `SearchBar`), shown while `stack.visible`. */
6
+ let { stack, label = 'Undo' }: { stack: UndoStack; label?: string } = $props();
7
+ </script>
8
+
9
+ {#if stack.visible}
10
+ <button type="button" class="undo" onclick={() => stack.undo()}>
11
+ <Icon name="undo" size={18} />
12
+ {label}
13
+ </button>
14
+ {/if}
15
+
16
+ <style>
17
+ .undo {
18
+ position: fixed;
19
+ left: max(1rem, calc(50vw - var(--content-w) / 2 + 1rem));
20
+ bottom: calc(4.75rem + env(safe-area-inset-bottom, 0px));
21
+ z-index: 45;
22
+ display: inline-flex;
23
+ align-items: center;
24
+ gap: 0.4rem;
25
+ padding: 0.6rem 1rem;
26
+ border: none;
27
+ border-radius: 2rem;
28
+ background: var(--text);
29
+ color: var(--bg);
30
+ font-weight: 600;
31
+ box-shadow: var(--shadow-lg);
32
+ cursor: pointer;
33
+ animation: pop 0.15s ease-out;
34
+ }
35
+ @keyframes pop {
36
+ from {
37
+ transform: translateY(0.5rem);
38
+ opacity: 0;
39
+ }
40
+ }
41
+ </style>
@@ -0,0 +1,8 @@
1
+ import type { UndoStack } from './undo.svelte.js';
2
+ type $$ComponentProps = {
3
+ stack: UndoStack;
4
+ label?: string;
5
+ };
6
+ declare const UndoButton: import("svelte").Component<$$ComponentProps, {}, "">;
7
+ type UndoButton = ReturnType<typeof UndoButton>;
8
+ export default UndoButton;