@nqmcreative/ui 0.3.0 → 0.4.1
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 +220 -28
- package/cli/index.mjs +247 -70
- package/dist/core/calendar.d.ts +1 -1
- package/dist/core/calendar.js +1 -1
- package/dist/core/color.d.ts +24 -0
- package/dist/core/color.js +39 -0
- package/dist/core/date.d.ts +3 -3
- package/dist/core/date.js +1 -1
- package/dist/core/files.d.ts +2 -3
- package/dist/core/files.js +2 -3
- package/dist/core/index.d.ts +9 -4
- package/dist/core/index.js +6 -4
- package/dist/core/number.d.ts +22 -0
- package/dist/core/number.js +33 -0
- package/dist/core/password.d.ts +1 -1
- package/dist/core/password.js +1 -1
- package/dist/core/pin.d.ts +23 -0
- package/dist/core/pin.js +38 -0
- package/dist/core/tags.d.ts +37 -0
- package/dist/core/tags.js +51 -0
- package/dist/core/time.d.ts +30 -0
- package/dist/core/time.js +78 -0
- package/dist/styles/matte/Alert.svelte +1 -3
- package/dist/styles/matte/Breadcrumb.svelte +1 -4
- package/dist/styles/matte/Calendar.svelte +6 -9
- package/dist/styles/matte/ColorInput.svelte +107 -0
- package/dist/styles/matte/ColorInput.svelte.d.ts +16 -0
- package/dist/styles/matte/Combobox.svelte +3 -6
- package/dist/styles/matte/CommandPalette.svelte +3 -6
- package/dist/styles/matte/ConfirmDialog.svelte +2 -5
- package/dist/styles/matte/ContextMenu.svelte +1 -4
- package/dist/styles/matte/CurrencyInput.svelte +108 -0
- package/dist/styles/matte/CurrencyInput.svelte.d.ts +26 -0
- package/dist/styles/matte/DatePicker.svelte +9 -9
- package/dist/styles/matte/DatePicker.svelte.d.ts +3 -0
- package/dist/styles/matte/Drawer.svelte +1 -3
- package/dist/styles/matte/Dropdown.svelte +2 -5
- package/dist/styles/matte/Dropdown.svelte.d.ts +1 -1
- package/dist/styles/matte/Dropzone.svelte +9 -12
- package/dist/styles/matte/Dropzone.svelte.d.ts +1 -1
- package/dist/styles/matte/FileInput.svelte +191 -0
- package/dist/styles/matte/FileInput.svelte.d.ts +32 -0
- package/dist/styles/matte/Footer.svelte +1 -1
- package/dist/styles/matte/Footer.svelte.d.ts +1 -1
- package/dist/styles/matte/InputAddon.svelte +36 -0
- package/dist/styles/matte/InputAddon.svelte.d.ts +13 -0
- package/dist/styles/matte/Modal.svelte +1 -3
- package/dist/styles/matte/MultiSelect.svelte +5 -8
- package/dist/styles/matte/Navbar.svelte +3 -9
- package/dist/styles/matte/Pagination.svelte +3 -6
- package/dist/styles/matte/PinInput.svelte +134 -0
- package/dist/styles/matte/PinInput.svelte.d.ts +25 -0
- package/dist/styles/matte/SearchInput.svelte +84 -0
- package/dist/styles/matte/SearchInput.svelte.d.ts +24 -0
- package/dist/styles/matte/Sidebar.svelte +3 -6
- package/dist/styles/matte/Spinner.svelte +2 -5
- package/dist/styles/matte/Spinner.svelte.d.ts +1 -1
- package/dist/styles/matte/Table.svelte +5 -8
- package/dist/styles/matte/TagsInput.svelte +170 -0
- package/dist/styles/matte/TagsInput.svelte.d.ts +29 -0
- package/dist/styles/matte/ThemeToggle.svelte +5 -8
- package/dist/styles/matte/TimeInput.svelte +126 -0
- package/dist/styles/matte/TimeInput.svelte.d.ts +21 -0
- package/dist/styles/matte/Toaster.svelte +2 -5
- package/dist/styles/matte/index.d.ts +10 -3
- package/dist/styles/matte/index.js +9 -3
- package/dist/styles/paper/Alert.svelte +1 -3
- package/dist/styles/paper/Breadcrumb.svelte +1 -4
- package/dist/styles/paper/Calendar.svelte +6 -9
- package/dist/styles/paper/ColorInput.svelte +109 -0
- package/dist/styles/paper/ColorInput.svelte.d.ts +16 -0
- package/dist/styles/paper/Combobox.svelte +3 -6
- package/dist/styles/paper/CommandPalette.svelte +3 -6
- package/dist/styles/paper/ConfirmDialog.svelte +2 -5
- package/dist/styles/paper/ContextMenu.svelte +1 -4
- package/dist/styles/paper/CurrencyInput.svelte +108 -0
- package/dist/styles/paper/CurrencyInput.svelte.d.ts +26 -0
- package/dist/styles/paper/DatePicker.svelte +9 -9
- package/dist/styles/paper/DatePicker.svelte.d.ts +3 -0
- package/dist/styles/paper/Drawer.svelte +1 -3
- package/dist/styles/paper/Dropdown.svelte +2 -5
- package/dist/styles/paper/Dropdown.svelte.d.ts +1 -1
- package/dist/styles/paper/Dropzone.svelte +9 -12
- package/dist/styles/paper/Dropzone.svelte.d.ts +1 -1
- package/dist/styles/paper/FileInput.svelte +201 -0
- package/dist/styles/paper/FileInput.svelte.d.ts +32 -0
- package/dist/styles/paper/Footer.svelte +1 -1
- package/dist/styles/paper/Footer.svelte.d.ts +1 -1
- package/dist/styles/paper/InputAddon.svelte +36 -0
- package/dist/styles/paper/InputAddon.svelte.d.ts +13 -0
- package/dist/styles/paper/Modal.svelte +1 -3
- package/dist/styles/paper/MultiSelect.svelte +5 -8
- package/dist/styles/paper/Navbar.svelte +3 -9
- package/dist/styles/paper/Pagination.svelte +3 -6
- package/dist/styles/paper/PinInput.svelte +134 -0
- package/dist/styles/paper/PinInput.svelte.d.ts +25 -0
- package/dist/styles/paper/SearchInput.svelte +89 -0
- package/dist/styles/paper/SearchInput.svelte.d.ts +24 -0
- package/dist/styles/paper/Sidebar.svelte +3 -6
- package/dist/styles/paper/Spinner.svelte +2 -5
- package/dist/styles/paper/Spinner.svelte.d.ts +1 -1
- package/dist/styles/paper/Table.svelte +5 -8
- package/dist/styles/paper/TagsInput.svelte +175 -0
- package/dist/styles/paper/TagsInput.svelte.d.ts +29 -0
- package/dist/styles/paper/ThemeToggle.svelte +5 -8
- package/dist/styles/paper/TimeInput.svelte +152 -0
- package/dist/styles/paper/TimeInput.svelte.d.ts +21 -0
- package/dist/styles/paper/Toaster.svelte +3 -6
- package/dist/styles/paper/index.d.ts +10 -3
- package/dist/styles/paper/index.js +9 -3
- package/dist/styles/sprout/Alert.svelte +1 -3
- package/dist/styles/sprout/Breadcrumb.svelte +1 -4
- package/dist/styles/sprout/Calendar.svelte +6 -9
- package/dist/styles/sprout/ColorInput.svelte +109 -0
- package/dist/styles/sprout/ColorInput.svelte.d.ts +16 -0
- package/dist/styles/sprout/Combobox.svelte +3 -6
- package/dist/styles/sprout/CommandPalette.svelte +3 -6
- package/dist/styles/sprout/ConfirmDialog.svelte +2 -5
- package/dist/styles/sprout/ContextMenu.svelte +1 -4
- package/dist/styles/sprout/CurrencyInput.svelte +108 -0
- package/dist/styles/sprout/CurrencyInput.svelte.d.ts +26 -0
- package/dist/styles/sprout/DatePicker.svelte +9 -9
- package/dist/styles/sprout/DatePicker.svelte.d.ts +3 -0
- package/dist/styles/sprout/Drawer.svelte +1 -3
- package/dist/styles/sprout/Dropdown.svelte +2 -5
- package/dist/styles/sprout/Dropdown.svelte.d.ts +1 -1
- package/dist/styles/sprout/Dropzone.svelte +9 -12
- package/dist/styles/sprout/Dropzone.svelte.d.ts +1 -1
- package/dist/styles/sprout/FileInput.svelte +202 -0
- package/dist/styles/sprout/FileInput.svelte.d.ts +32 -0
- package/dist/styles/sprout/Footer.svelte +1 -1
- package/dist/styles/sprout/Footer.svelte.d.ts +1 -1
- package/dist/styles/sprout/InputAddon.svelte +37 -0
- package/dist/styles/sprout/InputAddon.svelte.d.ts +13 -0
- package/dist/styles/sprout/Modal.svelte +1 -3
- package/dist/styles/sprout/MultiSelect.svelte +5 -8
- package/dist/styles/sprout/Navbar.svelte +3 -9
- package/dist/styles/sprout/Pagination.svelte +3 -6
- package/dist/styles/sprout/PinInput.svelte +135 -0
- package/dist/styles/sprout/PinInput.svelte.d.ts +25 -0
- package/dist/styles/sprout/SearchInput.svelte +89 -0
- package/dist/styles/sprout/SearchInput.svelte.d.ts +24 -0
- package/dist/styles/sprout/Sidebar.svelte +3 -6
- package/dist/styles/sprout/Spinner.svelte +2 -5
- package/dist/styles/sprout/Spinner.svelte.d.ts +1 -1
- package/dist/styles/sprout/Table.svelte +5 -8
- package/dist/styles/sprout/TagsInput.svelte +176 -0
- package/dist/styles/sprout/TagsInput.svelte.d.ts +29 -0
- package/dist/styles/sprout/ThemeToggle.svelte +5 -8
- package/dist/styles/sprout/TimeInput.svelte +152 -0
- package/dist/styles/sprout/TimeInput.svelte.d.ts +21 -0
- package/dist/styles/sprout/Toaster.svelte +3 -6
- package/dist/styles/sprout/index.d.ts +10 -3
- package/dist/styles/sprout/index.js +9 -3
- package/package.json +98 -19
- package/registry.json +139 -38
- package/dist/core/locale.svelte.d.ts +0 -93
- package/dist/core/locale.svelte.js +0 -153
- package/dist/styles/matte/LocaleProvider.svelte +0 -24
- package/dist/styles/matte/LocaleProvider.svelte.d.ts +0 -10
- package/dist/styles/paper/LocaleProvider.svelte +0 -24
- package/dist/styles/paper/LocaleProvider.svelte.d.ts +0 -10
- package/dist/styles/sprout/LocaleProvider.svelte +0 -24
- package/dist/styles/sprout/LocaleProvider.svelte.d.ts +0 -10
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import { iconLg } from './icon.js';
|
|
4
4
|
import type { HTMLDialogAttributes } from 'svelte/elements';
|
|
5
|
-
import { useLocale } from '../../core/locale.svelte.js';
|
|
6
5
|
import { isBackdropClick, syncDialog } from '../../core/dialog.svelte.js';
|
|
7
6
|
|
|
8
7
|
export type ModalSize = 'sm' | 'md' | 'lg';
|
|
@@ -40,7 +39,6 @@
|
|
|
40
39
|
...rest
|
|
41
40
|
}: Props = $props();
|
|
42
41
|
|
|
43
|
-
const t = useLocale();
|
|
44
42
|
let dialog: HTMLDialogElement | null = $state(null);
|
|
45
43
|
|
|
46
44
|
const closable = $derived(showClose ?? dismissible);
|
|
@@ -92,7 +90,7 @@
|
|
|
92
90
|
<button
|
|
93
91
|
type="button"
|
|
94
92
|
onclick={close}
|
|
95
|
-
aria-label=
|
|
93
|
+
aria-label="Close"
|
|
96
94
|
class="-m-1 shrink-0 p-1 text-text-muted transition-colors duration-150 hover:text-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand"
|
|
97
95
|
>
|
|
98
96
|
<svg class="size-4" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
import { anchored } from '../../core/actions/anchor.js';
|
|
17
17
|
import { clickOutside, portal } from '../../core/actions/dismissable.js';
|
|
18
18
|
import { ListCursor, groupItems, matchQuery, revealIndex } from '../../core/list.svelte.js';
|
|
19
|
-
import { useLocale } from '../../core/locale.svelte.js';
|
|
20
19
|
import { iconMd, iconSm } from './icon.js';
|
|
21
20
|
import { toneFocusWithinBorder, toneSoft, toneText, type Tone } from '../../core/tones.js';
|
|
22
21
|
|
|
@@ -58,8 +57,6 @@
|
|
|
58
57
|
class: className = ''
|
|
59
58
|
}: Props = $props();
|
|
60
59
|
|
|
61
|
-
const t = useLocale();
|
|
62
|
-
|
|
63
60
|
let open = $state(false);
|
|
64
61
|
let query = $state('');
|
|
65
62
|
let wrapper: HTMLElement | null = $state(null);
|
|
@@ -149,7 +146,7 @@
|
|
|
149
146
|
class="inline-flex items-center rounded-full px-2.5 py-1 font-mono text-xs {toneSoft[tone]}"
|
|
150
147
|
>
|
|
151
148
|
{chosen.length}
|
|
152
|
-
|
|
149
|
+
selected
|
|
153
150
|
</span>
|
|
154
151
|
{:else}
|
|
155
152
|
{#each chosen as option (option.value)}
|
|
@@ -162,7 +159,7 @@
|
|
|
162
159
|
<button
|
|
163
160
|
type="button"
|
|
164
161
|
onclick={() => remove(option)}
|
|
165
|
-
aria-label="
|
|
162
|
+
aria-label="Remove {option.label}"
|
|
166
163
|
class="inline-flex size-4 items-center justify-center rounded-full transition-colors duration-150 hover:bg-bg focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-current"
|
|
167
164
|
>
|
|
168
165
|
<svg class="size-2.5" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
|
@@ -184,7 +181,7 @@
|
|
|
184
181
|
aria-expanded={open}
|
|
185
182
|
aria-controls="{id ?? 'multiselect'}-listbox"
|
|
186
183
|
aria-invalid={invalid ? 'true' : undefined}
|
|
187
|
-
placeholder={chosen.length ? '' : (placeholder ??
|
|
184
|
+
placeholder={chosen.length ? '' : (placeholder ?? 'Search…')}
|
|
188
185
|
oninput={() => {
|
|
189
186
|
open = true;
|
|
190
187
|
cursor.index = 0;
|
|
@@ -206,7 +203,7 @@
|
|
|
206
203
|
onchange?.([]);
|
|
207
204
|
inputEl?.focus();
|
|
208
205
|
}}
|
|
209
|
-
aria-label=
|
|
206
|
+
aria-label="Clear selection"
|
|
210
207
|
class="shrink-0 p-1.5 text-text-muted transition-colors duration-150 hover:text-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current"
|
|
211
208
|
>
|
|
212
209
|
<svg class="size-3.5" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
|
@@ -229,7 +226,7 @@
|
|
|
229
226
|
>
|
|
230
227
|
{#if flat.length === 0}
|
|
231
228
|
<p class="px-3 py-6 text-center text-sm text-text-muted">
|
|
232
|
-
{emptyText ??
|
|
229
|
+
{emptyText ?? 'No matches'}
|
|
233
230
|
</p>
|
|
234
231
|
{:else}
|
|
235
232
|
{#each groups as [group, items] (group)}
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
|
|
17
17
|
<script lang="ts">
|
|
18
18
|
import type { Snippet } from 'svelte';
|
|
19
|
-
import { useLocale } from '../../core/locale.svelte.js';
|
|
20
19
|
import { iconMd } from './icon.js';
|
|
21
20
|
import { focusRing, toneRing, toneText, type Tone } from '../../core/tones.js';
|
|
22
21
|
import Drawer from './Drawer.svelte';
|
|
@@ -53,8 +52,6 @@
|
|
|
53
52
|
class: className = ''
|
|
54
53
|
}: Props = $props();
|
|
55
54
|
|
|
56
|
-
const t = useLocale();
|
|
57
|
-
|
|
58
55
|
const link =
|
|
59
56
|
'inline-flex items-center gap-2 font-sans text-[15px] transition-colors duration-150 ease-brand-out';
|
|
60
57
|
</script>
|
|
@@ -64,10 +61,7 @@
|
|
|
64
61
|
? 'border-b border-hairline'
|
|
65
62
|
: ''} {className}"
|
|
66
63
|
>
|
|
67
|
-
<nav
|
|
68
|
-
aria-label={t.current.navigation}
|
|
69
|
-
class="mx-auto flex h-16 w-full max-w-6xl items-center gap-6 px-6"
|
|
70
|
-
>
|
|
64
|
+
<nav aria-label="Navigation" class="mx-auto flex h-16 w-full max-w-6xl items-center gap-6 px-6">
|
|
71
65
|
{#if brand}
|
|
72
66
|
<div class="flex shrink-0 items-center">{@render brand()}</div>
|
|
73
67
|
{/if}
|
|
@@ -138,7 +132,7 @@
|
|
|
138
132
|
<button
|
|
139
133
|
type="button"
|
|
140
134
|
onclick={() => (menuOpen = true)}
|
|
141
|
-
aria-label=
|
|
135
|
+
aria-label="Open menu"
|
|
142
136
|
aria-expanded={menuOpen}
|
|
143
137
|
class="inline-flex size-9 items-center justify-center text-text-secondary transition-colors duration-150 hover:text-text md:hidden {focusRing} {toneRing[
|
|
144
138
|
tone
|
|
@@ -152,7 +146,7 @@
|
|
|
152
146
|
</nav>
|
|
153
147
|
</header>
|
|
154
148
|
|
|
155
|
-
<Drawer bind:open={menuOpen} side="right" size="sm" title=
|
|
149
|
+
<Drawer bind:open={menuOpen} side="right" size="sm" title="Navigation">
|
|
156
150
|
<ul class="flex flex-col">
|
|
157
151
|
{#each items as item (item.label)}
|
|
158
152
|
<li class="border-b border-hairline last:border-b-0">
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
3
|
import { focusRing, toneRing, toneSolid, type Tone } from '../../core/tones.js';
|
|
4
|
-
import { useLocale } from '../../core/locale.svelte.js';
|
|
5
4
|
import { pageRange } from '../../core/pagination.js';
|
|
6
5
|
|
|
7
6
|
interface Props extends Omit<HTMLAttributes<HTMLElement>, 'onchange'> {
|
|
@@ -25,8 +24,6 @@
|
|
|
25
24
|
...rest
|
|
26
25
|
}: Props = $props();
|
|
27
26
|
|
|
28
|
-
const t = useLocale();
|
|
29
|
-
|
|
30
27
|
const pages = $derived(pageRange(page, total, siblings));
|
|
31
28
|
|
|
32
29
|
function go(next: number) {
|
|
@@ -39,12 +36,12 @@
|
|
|
39
36
|
const cell = `inline-flex h-9 min-w-9 items-center justify-center px-2 font-mono text-[13px] transition-colors duration-150 ease-brand-out disabled:opacity-40 disabled:pointer-events-none ${focusRing}`;
|
|
40
37
|
</script>
|
|
41
38
|
|
|
42
|
-
<nav aria-label=
|
|
39
|
+
<nav aria-label="Pagination" class="flex items-center gap-1 {className}" {...rest}>
|
|
43
40
|
<button
|
|
44
41
|
type="button"
|
|
45
42
|
class="{cell} {toneRing[tone]} text-text-secondary hover:bg-bg-inset"
|
|
46
43
|
disabled={page <= 1}
|
|
47
|
-
aria-label=
|
|
44
|
+
aria-label="Previous page"
|
|
48
45
|
onclick={() => go(page - 1)}
|
|
49
46
|
>
|
|
50
47
|
←
|
|
@@ -71,7 +68,7 @@
|
|
|
71
68
|
type="button"
|
|
72
69
|
class="{cell} {toneRing[tone]} text-text-secondary hover:bg-bg-inset"
|
|
73
70
|
disabled={page >= total}
|
|
74
|
-
aria-label=
|
|
71
|
+
aria-label="Next page"
|
|
75
72
|
onclick={() => go(page + 1)}
|
|
76
73
|
>
|
|
77
74
|
→
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { caretIndex, clearFrom, fillFrom, sanitisePin, type PinMode } from '../../core/pin.js';
|
|
3
|
+
import { toneFocusBorder, type Tone } from '../../core/tones.js';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
/** Bindable code. Shorter than `length` means still incomplete. */
|
|
7
|
+
value?: string;
|
|
8
|
+
/** How many boxes. */
|
|
9
|
+
length?: number;
|
|
10
|
+
/** What a box accepts. */
|
|
11
|
+
mode?: PinMode;
|
|
12
|
+
/** Render the characters as dots. */
|
|
13
|
+
mask?: boolean;
|
|
14
|
+
/** Draw a gap after this many boxes — `3` gives `123 456`. */
|
|
15
|
+
groupAfter?: number;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
invalid?: boolean;
|
|
18
|
+
tone?: Tone;
|
|
19
|
+
/** `name` for a plain form submit — one hidden field holding the code. */
|
|
20
|
+
name?: string;
|
|
21
|
+
/** Fires once the last box is filled. */
|
|
22
|
+
oncomplete?: (value: string) => void;
|
|
23
|
+
class?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let {
|
|
27
|
+
value = $bindable(''),
|
|
28
|
+
length = 6,
|
|
29
|
+
mode = 'numeric',
|
|
30
|
+
mask = false,
|
|
31
|
+
groupAfter = 0,
|
|
32
|
+
disabled = false,
|
|
33
|
+
invalid = false,
|
|
34
|
+
tone = 'brand',
|
|
35
|
+
name,
|
|
36
|
+
oncomplete,
|
|
37
|
+
class: className = ''
|
|
38
|
+
}: Props = $props();
|
|
39
|
+
|
|
40
|
+
let boxes: HTMLInputElement[] = $state([]);
|
|
41
|
+
|
|
42
|
+
const chars = $derived(Array.from({ length }, (_, i) => value[i] ?? ''));
|
|
43
|
+
|
|
44
|
+
function focusAt(index: number) {
|
|
45
|
+
boxes[Math.max(0, Math.min(length - 1, index))]?.focus();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The DOM is written back on every keystroke, not only when the value
|
|
50
|
+
* changed: a refused character leaves the box holding text the state never
|
|
51
|
+
* accepted, and nothing else would clear it.
|
|
52
|
+
*/
|
|
53
|
+
function sync(box: HTMLInputElement, index: number) {
|
|
54
|
+
box.value = value[index] ?? '';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function commit(next: string, from: number) {
|
|
58
|
+
const was = value;
|
|
59
|
+
value = next;
|
|
60
|
+
if (value.length === length && was.length < length) oncomplete?.(value);
|
|
61
|
+
// Land on the first empty box, or the one after whatever was just typed.
|
|
62
|
+
focusAt(Math.min(Math.max(from, caretIndex(value, length)), length - 1));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function onInput(event: Event & { currentTarget: HTMLInputElement }, index: number) {
|
|
66
|
+
const typed = sanitisePin(event.currentTarget.value, mode);
|
|
67
|
+
if (!typed) {
|
|
68
|
+
sync(event.currentTarget, index);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const next = fillFrom(value, index, typed, length, mode);
|
|
72
|
+
commit(next, index + typed.length);
|
|
73
|
+
for (const [at, box] of boxes.entries()) sync(box, at);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function onKeydown(event: KeyboardEvent, index: number) {
|
|
77
|
+
if (event.key === 'Backspace') {
|
|
78
|
+
event.preventDefault();
|
|
79
|
+
// An empty box deletes the one before it, the way a caret would.
|
|
80
|
+
const target = value[index] ? index : index - 1;
|
|
81
|
+
if (target < 0) return;
|
|
82
|
+
value = clearFrom(value, target);
|
|
83
|
+
for (const [at, box] of boxes.entries()) sync(box, at);
|
|
84
|
+
focusAt(target);
|
|
85
|
+
} else if (event.key === 'ArrowLeft') {
|
|
86
|
+
event.preventDefault();
|
|
87
|
+
focusAt(index - 1);
|
|
88
|
+
} else if (event.key === 'ArrowRight') {
|
|
89
|
+
event.preventDefault();
|
|
90
|
+
focusAt(index + 1);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function onPaste(event: ClipboardEvent, index: number) {
|
|
95
|
+
event.preventDefault();
|
|
96
|
+
const text = event.clipboardData?.getData('text') ?? '';
|
|
97
|
+
const clean = sanitisePin(text, mode);
|
|
98
|
+
if (!clean) return;
|
|
99
|
+
commit(fillFrom(value, index, clean, length, mode), index + clean.length);
|
|
100
|
+
for (const [at, box] of boxes.entries()) sync(box, at);
|
|
101
|
+
}
|
|
102
|
+
</script>
|
|
103
|
+
|
|
104
|
+
<div class="flex items-center gap-2 {className}">
|
|
105
|
+
{#each chars as char, index (index)}
|
|
106
|
+
{#if groupAfter && index > 0 && index % groupAfter === 0}
|
|
107
|
+
<span class="h-px w-2 bg-hairline-strong" aria-hidden="true"></span>
|
|
108
|
+
{/if}
|
|
109
|
+
<input
|
|
110
|
+
bind:this={boxes[index]}
|
|
111
|
+
value={char}
|
|
112
|
+
{disabled}
|
|
113
|
+
type={mask ? 'password' : 'text'}
|
|
114
|
+
inputmode={mode === 'numeric' ? 'numeric' : 'text'}
|
|
115
|
+
autocomplete={index === 0 ? 'one-time-code' : 'off'}
|
|
116
|
+
autocapitalize="characters"
|
|
117
|
+
spellcheck="false"
|
|
118
|
+
maxlength={length}
|
|
119
|
+
aria-label="Digit {index + 1}"
|
|
120
|
+
aria-invalid={invalid ? 'true' : undefined}
|
|
121
|
+
oninput={(event) => onInput(event, index)}
|
|
122
|
+
onkeydown={(event) => onKeydown(event, index)}
|
|
123
|
+
onpaste={(event) => onPaste(event, index)}
|
|
124
|
+
onfocus={(event) => event.currentTarget.select()}
|
|
125
|
+
class="size-11 border bg-bg text-center font-mono text-base text-text transition-colors duration-150 ease-brand-out focus:outline-none
|
|
126
|
+
{invalid ? 'border-danger' : `border-hairline ${toneFocusBorder[tone]}`}
|
|
127
|
+
{disabled ? 'pointer-events-none opacity-50' : ''}"
|
|
128
|
+
/>
|
|
129
|
+
{/each}
|
|
130
|
+
|
|
131
|
+
{#if name}
|
|
132
|
+
<input type="hidden" {name} {value} />
|
|
133
|
+
{/if}
|
|
134
|
+
</div>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type PinMode } from '../../core/pin.js';
|
|
2
|
+
import { type Tone } from '../../core/tones.js';
|
|
3
|
+
interface Props {
|
|
4
|
+
/** Bindable code. Shorter than `length` means still incomplete. */
|
|
5
|
+
value?: string;
|
|
6
|
+
/** How many boxes. */
|
|
7
|
+
length?: number;
|
|
8
|
+
/** What a box accepts. */
|
|
9
|
+
mode?: PinMode;
|
|
10
|
+
/** Render the characters as dots. */
|
|
11
|
+
mask?: boolean;
|
|
12
|
+
/** Draw a gap after this many boxes — `3` gives `123 456`. */
|
|
13
|
+
groupAfter?: number;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
invalid?: boolean;
|
|
16
|
+
tone?: Tone;
|
|
17
|
+
/** `name` for a plain form submit — one hidden field holding the code. */
|
|
18
|
+
name?: string;
|
|
19
|
+
/** Fires once the last box is filled. */
|
|
20
|
+
oncomplete?: (value: string) => void;
|
|
21
|
+
class?: string;
|
|
22
|
+
}
|
|
23
|
+
declare const PinInput: import("svelte").Component<Props, {}, "value">;
|
|
24
|
+
type PinInput = ReturnType<typeof PinInput>;
|
|
25
|
+
export default PinInput;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
4
|
+
import type { Tone } from '../../core/tones.js';
|
|
5
|
+
import Input, { type InputSize } from './Input.svelte';
|
|
6
|
+
|
|
7
|
+
interface Props extends Omit<HTMLInputAttributes, 'size' | 'prefix' | 'type'> {
|
|
8
|
+
value?: string;
|
|
9
|
+
size?: InputSize;
|
|
10
|
+
tone?: Tone;
|
|
11
|
+
invalid?: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Milliseconds to wait after the last keystroke before `onsearch` fires.
|
|
14
|
+
* `0` fires on every one.
|
|
15
|
+
*/
|
|
16
|
+
debounce?: number;
|
|
17
|
+
/** The debounced value. The bound `value` still updates immediately. */
|
|
18
|
+
onsearch?: (value: string) => void;
|
|
19
|
+
onclear?: () => void;
|
|
20
|
+
/** Replaces the magnifier. */
|
|
21
|
+
icon?: Snippet;
|
|
22
|
+
class?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let {
|
|
26
|
+
value = $bindable(''),
|
|
27
|
+
size = 'md',
|
|
28
|
+
tone = 'brand',
|
|
29
|
+
invalid = false,
|
|
30
|
+
disabled = false,
|
|
31
|
+
debounce = 0,
|
|
32
|
+
onsearch,
|
|
33
|
+
onclear,
|
|
34
|
+
icon,
|
|
35
|
+
class: className = '',
|
|
36
|
+
...rest
|
|
37
|
+
}: Props = $props();
|
|
38
|
+
|
|
39
|
+
// The timer is cleared by the effect's own teardown, so a keystroke that
|
|
40
|
+
// lands mid-wait restarts the clock rather than queueing a second call.
|
|
41
|
+
$effect(() => {
|
|
42
|
+
const next = value;
|
|
43
|
+
if (!onsearch) return;
|
|
44
|
+
if (debounce <= 0) {
|
|
45
|
+
onsearch(next);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const timer = setTimeout(() => onsearch(next), debounce);
|
|
49
|
+
return () => clearTimeout(timer);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
function clear() {
|
|
53
|
+
value = '';
|
|
54
|
+
onclear?.();
|
|
55
|
+
}
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<Input bind:value type="search" {size} {tone} {invalid} {disabled} class={className} {...rest}>
|
|
59
|
+
{#snippet prefix()}
|
|
60
|
+
{#if icon}
|
|
61
|
+
{@render icon()}
|
|
62
|
+
{:else}
|
|
63
|
+
<svg class="size-4" viewBox="0 0 20 20" fill="none" aria-hidden="true">
|
|
64
|
+
<circle cx="9" cy="9" r="5.5" stroke="currentColor" stroke-width="1.5" />
|
|
65
|
+
<path d="m13 13 3.5 3.5" stroke="currentColor" stroke-width="1.5" />
|
|
66
|
+
</svg>
|
|
67
|
+
{/if}
|
|
68
|
+
{/snippet}
|
|
69
|
+
|
|
70
|
+
{#snippet suffix()}
|
|
71
|
+
{#if value}
|
|
72
|
+
<button
|
|
73
|
+
type="button"
|
|
74
|
+
onclick={clear}
|
|
75
|
+
aria-label="Clear search"
|
|
76
|
+
class="-mr-1 p-1 text-text-muted transition-colors duration-150 hover:text-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current"
|
|
77
|
+
>
|
|
78
|
+
<svg class="size-3.5" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
|
79
|
+
<path d="m2 2 10 10M12 2 2 12" stroke="currentColor" stroke-width="1.5" />
|
|
80
|
+
</svg>
|
|
81
|
+
</button>
|
|
82
|
+
{/if}
|
|
83
|
+
{/snippet}
|
|
84
|
+
</Input>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
3
|
+
import type { Tone } from '../../core/tones.js';
|
|
4
|
+
import { type InputSize } from './Input.svelte';
|
|
5
|
+
interface Props extends Omit<HTMLInputAttributes, 'size' | 'prefix' | 'type'> {
|
|
6
|
+
value?: string;
|
|
7
|
+
size?: InputSize;
|
|
8
|
+
tone?: Tone;
|
|
9
|
+
invalid?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Milliseconds to wait after the last keystroke before `onsearch` fires.
|
|
12
|
+
* `0` fires on every one.
|
|
13
|
+
*/
|
|
14
|
+
debounce?: number;
|
|
15
|
+
/** The debounced value. The bound `value` still updates immediately. */
|
|
16
|
+
onsearch?: (value: string) => void;
|
|
17
|
+
onclear?: () => void;
|
|
18
|
+
/** Replaces the magnifier. */
|
|
19
|
+
icon?: Snippet;
|
|
20
|
+
class?: string;
|
|
21
|
+
}
|
|
22
|
+
declare const SearchInput: import("svelte").Component<Props, {}, "value">;
|
|
23
|
+
type SearchInput = ReturnType<typeof SearchInput>;
|
|
24
|
+
export default SearchInput;
|
|
@@ -35,7 +35,6 @@
|
|
|
35
35
|
</script>
|
|
36
36
|
|
|
37
37
|
<script lang="ts">
|
|
38
|
-
import { useLocale } from '../../core/locale.svelte.js';
|
|
39
38
|
import { iconMd } from './icon.js';
|
|
40
39
|
import { focusRing, toneRing, toneSoft, type Tone } from '../../core/tones.js';
|
|
41
40
|
|
|
@@ -69,8 +68,6 @@
|
|
|
69
68
|
class: className = ''
|
|
70
69
|
}: Props = $props();
|
|
71
70
|
|
|
72
|
-
const t = useLocale();
|
|
73
|
-
|
|
74
71
|
const keyOf = (item: SidebarItem) => item.id ?? item.href ?? item.label;
|
|
75
72
|
|
|
76
73
|
/** Groups start open when asked, or when they contain the active item. */
|
|
@@ -141,7 +138,7 @@
|
|
|
141
138
|
</div>
|
|
142
139
|
{/if}
|
|
143
140
|
|
|
144
|
-
<nav aria-label=
|
|
141
|
+
<nav aria-label="Navigation" class="flex-1 overflow-y-auto py-3">
|
|
145
142
|
{#each sections as section, i (section.label ?? i)}
|
|
146
143
|
<div class="flex flex-col gap-0.5 {i > 0 ? 'mt-4' : ''}">
|
|
147
144
|
{#if section.label && !collapsed}
|
|
@@ -230,7 +227,7 @@
|
|
|
230
227
|
<button
|
|
231
228
|
type="button"
|
|
232
229
|
onclick={() => (collapsed = !collapsed)}
|
|
233
|
-
aria-label={collapsed ?
|
|
230
|
+
aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
|
234
231
|
class="inline-flex h-8 items-center justify-center gap-2 font-sans text-xs text-text-muted transition-colors duration-150 hover:text-text {focusRing} {toneRing[
|
|
235
232
|
tone
|
|
236
233
|
]}"
|
|
@@ -248,7 +245,7 @@
|
|
|
248
245
|
stroke-linecap="square"
|
|
249
246
|
/>
|
|
250
247
|
</svg>
|
|
251
|
-
{#if !collapsed}<span>
|
|
248
|
+
{#if !collapsed}<span>Collapse sidebar</span>{/if}
|
|
252
249
|
</button>
|
|
253
250
|
{/if}
|
|
254
251
|
</div>
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
3
|
import { toneText, type Tone } from '../../core/tones.js';
|
|
4
|
-
import { useLocale } from '../../core/locale.svelte.js';
|
|
5
4
|
|
|
6
5
|
export type SpinnerSize = 'xs' | 'sm' | 'md' | 'lg';
|
|
7
6
|
|
|
@@ -9,12 +8,10 @@
|
|
|
9
8
|
size?: SpinnerSize;
|
|
10
9
|
/** Omit to inherit the parent's `currentColor` (e.g. inside a Button). */
|
|
11
10
|
tone?: Tone;
|
|
12
|
-
/** Defaults to
|
|
11
|
+
/** Defaults to `Loading`; pass `''` to hide it. */
|
|
13
12
|
label?: string;
|
|
14
13
|
}
|
|
15
14
|
|
|
16
|
-
const t = useLocale();
|
|
17
|
-
|
|
18
15
|
let { size = 'md', tone, label, class: className = '', ...rest }: Props = $props();
|
|
19
16
|
|
|
20
17
|
const sizes: Record<SpinnerSize, string> = {
|
|
@@ -27,7 +24,7 @@
|
|
|
27
24
|
|
|
28
25
|
<span
|
|
29
26
|
role="status"
|
|
30
|
-
aria-label={label ??
|
|
27
|
+
aria-label={label ?? 'Loading'}
|
|
31
28
|
class="inline-block shrink-0 animate-spin rounded-full border-current border-t-transparent
|
|
32
29
|
{sizes[size]} {tone ? toneText[tone] : ''} {className}"
|
|
33
30
|
{...rest}
|
|
@@ -5,7 +5,7 @@ interface Props extends HTMLAttributes<HTMLSpanElement> {
|
|
|
5
5
|
size?: SpinnerSize;
|
|
6
6
|
/** Omit to inherit the parent's `currentColor` (e.g. inside a Button). */
|
|
7
7
|
tone?: Tone;
|
|
8
|
-
/** Defaults to
|
|
8
|
+
/** Defaults to `Loading`; pass `''` to hide it. */
|
|
9
9
|
label?: string;
|
|
10
10
|
}
|
|
11
11
|
declare const Spinner: import("svelte").Component<Props, {}, "">;
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
import Checkbox from './Checkbox.svelte';
|
|
17
17
|
import type { Tone } from '../../core/tones.js';
|
|
18
18
|
import { nextSort, sortRows, toggleKey, type TableSort } from '../../core/table.js';
|
|
19
|
-
import { useLocale } from '../../core/locale.svelte.js';
|
|
20
19
|
|
|
21
20
|
interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'onselect'> {
|
|
22
21
|
columns: TableColumn[];
|
|
@@ -82,8 +81,6 @@
|
|
|
82
81
|
...rest
|
|
83
82
|
}: Props = $props();
|
|
84
83
|
|
|
85
|
-
const t = useLocale();
|
|
86
|
-
|
|
87
84
|
const aligns = {
|
|
88
85
|
left: 'text-left',
|
|
89
86
|
center: 'text-center',
|
|
@@ -136,14 +133,14 @@
|
|
|
136
133
|
>
|
|
137
134
|
<span class="font-mono text-xs font-medium tracking-wide text-text uppercase">
|
|
138
135
|
{selected.length}
|
|
139
|
-
|
|
136
|
+
selected
|
|
140
137
|
</span>
|
|
141
138
|
<button
|
|
142
139
|
type="button"
|
|
143
140
|
onclick={clearSelection}
|
|
144
141
|
class="font-mono text-xs tracking-wide text-text-muted uppercase transition-colors duration-150 hover:text-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current"
|
|
145
142
|
>
|
|
146
|
-
|
|
143
|
+
Clear selection
|
|
147
144
|
</button>
|
|
148
145
|
<div class="ml-auto flex flex-wrap items-center gap-2">
|
|
149
146
|
{@render bulkActions?.({ selected, clear: clearSelection })}
|
|
@@ -173,7 +170,7 @@
|
|
|
173
170
|
checked={allSelected}
|
|
174
171
|
indeterminate={someSelected}
|
|
175
172
|
{tone}
|
|
176
|
-
aria-label=
|
|
173
|
+
aria-label="Select all rows"
|
|
177
174
|
onchange={toggleAll}
|
|
178
175
|
/>
|
|
179
176
|
</th>
|
|
@@ -220,7 +217,7 @@
|
|
|
220
217
|
colspan={columns.length + (selectable ? 1 : 0)}
|
|
221
218
|
class="px-3.5 py-10 text-center text-sm text-text-muted"
|
|
222
219
|
>
|
|
223
|
-
{#if empty}{@render empty()}{:else}{
|
|
220
|
+
{#if empty}{@render empty()}{:else}No data{/if}
|
|
224
221
|
</td>
|
|
225
222
|
</tr>
|
|
226
223
|
{:else}
|
|
@@ -237,7 +234,7 @@
|
|
|
237
234
|
<Checkbox
|
|
238
235
|
checked={isSelected}
|
|
239
236
|
{tone}
|
|
240
|
-
aria-label=
|
|
237
|
+
aria-label="Select row"
|
|
241
238
|
onchange={() => toggleRow(row)}
|
|
242
239
|
/>
|
|
243
240
|
</td>
|