@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
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
3
|
+
import { normaliseHex } from '../../core/color.js';
|
|
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' | 'suffix' | 'type'> {
|
|
8
|
+
/** Bindable `#rrggbb`. Shorthand and alpha are normalised away on blur. */
|
|
9
|
+
value?: string;
|
|
10
|
+
size?: InputSize;
|
|
11
|
+
tone?: Tone;
|
|
12
|
+
invalid?: boolean;
|
|
13
|
+
/** Fixed choices under the field. Empty hides the row. */
|
|
14
|
+
swatches?: string[];
|
|
15
|
+
class?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let {
|
|
19
|
+
value = $bindable('#000000'),
|
|
20
|
+
size = 'md',
|
|
21
|
+
tone = 'brand',
|
|
22
|
+
invalid = false,
|
|
23
|
+
disabled = false,
|
|
24
|
+
swatches = [],
|
|
25
|
+
class: className = '',
|
|
26
|
+
...rest
|
|
27
|
+
}: Props = $props();
|
|
28
|
+
|
|
29
|
+
// The text can be mid-typing (`#a2`) while the swatch needs a real colour,
|
|
30
|
+
// so the swatch follows the last value that parsed rather than the field.
|
|
31
|
+
let text = $state('');
|
|
32
|
+
let editing = $state(false);
|
|
33
|
+
|
|
34
|
+
$effect(() => {
|
|
35
|
+
if (!editing) text = value;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const resolved = $derived(normaliseHex(text) ?? normaliseHex(value) ?? '#000000');
|
|
39
|
+
|
|
40
|
+
// Same reason as CurrencyInput: at input time the bound `text` is one
|
|
41
|
+
// keystroke behind, so the element is the only current source.
|
|
42
|
+
function onText(event: Event & { currentTarget: HTMLInputElement }) {
|
|
43
|
+
const hex = normaliseHex(event.currentTarget.value);
|
|
44
|
+
if (hex) value = hex;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function settle() {
|
|
48
|
+
editing = false;
|
|
49
|
+
value = normaliseHex(text) ?? value;
|
|
50
|
+
text = value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function choose(hex: string) {
|
|
54
|
+
value = normaliseHex(hex) ?? value;
|
|
55
|
+
text = value;
|
|
56
|
+
}
|
|
57
|
+
</script>
|
|
58
|
+
|
|
59
|
+
<div class="flex w-full flex-col gap-2 {className}">
|
|
60
|
+
<Input
|
|
61
|
+
bind:value={text}
|
|
62
|
+
{size}
|
|
63
|
+
{tone}
|
|
64
|
+
{invalid}
|
|
65
|
+
{disabled}
|
|
66
|
+
spellcheck="false"
|
|
67
|
+
autocapitalize="none"
|
|
68
|
+
placeholder="#000000"
|
|
69
|
+
oninput={onText}
|
|
70
|
+
onfocus={() => (editing = true)}
|
|
71
|
+
onblur={settle}
|
|
72
|
+
class="[&_input]:uppercase [&_input]:tabular-nums"
|
|
73
|
+
{...rest}
|
|
74
|
+
>
|
|
75
|
+
{#snippet prefix()}
|
|
76
|
+
<span
|
|
77
|
+
class="relative grid size-5 place-items-center overflow-hidden rounded border border-hairline-strong"
|
|
78
|
+
>
|
|
79
|
+
<span class="absolute inset-0" style="background-color: {resolved}"></span>
|
|
80
|
+
<input
|
|
81
|
+
type="color"
|
|
82
|
+
value={resolved}
|
|
83
|
+
{disabled}
|
|
84
|
+
aria-label="Pick a colour"
|
|
85
|
+
oninput={(event) => choose(event.currentTarget.value)}
|
|
86
|
+
class="absolute inset-0 cursor-pointer opacity-0 disabled:cursor-not-allowed"
|
|
87
|
+
/>
|
|
88
|
+
</span>
|
|
89
|
+
{/snippet}
|
|
90
|
+
</Input>
|
|
91
|
+
|
|
92
|
+
{#if swatches.length}
|
|
93
|
+
<div class="flex flex-wrap gap-1.5">
|
|
94
|
+
{#each swatches as swatch (swatch)}
|
|
95
|
+
<button
|
|
96
|
+
type="button"
|
|
97
|
+
{disabled}
|
|
98
|
+
onclick={() => choose(swatch)}
|
|
99
|
+
title={swatch}
|
|
100
|
+
aria-label={swatch}
|
|
101
|
+
aria-pressed={resolved === normaliseHex(swatch)}
|
|
102
|
+
class="size-6 rounded-md border transition-transform duration-150 hover:-translate-y-0.5 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current disabled:pointer-events-none disabled:opacity-50
|
|
103
|
+
{resolved === normaliseHex(swatch) ? 'border-text' : 'border-hairline-strong'}"
|
|
104
|
+
style="background-color: {swatch}"
|
|
105
|
+
></button>
|
|
106
|
+
{/each}
|
|
107
|
+
</div>
|
|
108
|
+
{/if}
|
|
109
|
+
</div>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
2
|
+
import type { Tone } from '../../core/tones.js';
|
|
3
|
+
import { type InputSize } from './Input.svelte';
|
|
4
|
+
interface Props extends Omit<HTMLInputAttributes, 'size' | 'prefix' | 'suffix' | 'type'> {
|
|
5
|
+
/** Bindable `#rrggbb`. Shorthand and alpha are normalised away on blur. */
|
|
6
|
+
value?: string;
|
|
7
|
+
size?: InputSize;
|
|
8
|
+
tone?: Tone;
|
|
9
|
+
invalid?: boolean;
|
|
10
|
+
/** Fixed choices under the field. Empty hides the row. */
|
|
11
|
+
swatches?: string[];
|
|
12
|
+
class?: string;
|
|
13
|
+
}
|
|
14
|
+
declare const ColorInput: import("svelte").Component<Props, {}, "value">;
|
|
15
|
+
type ColorInput = ReturnType<typeof ColorInput>;
|
|
16
|
+
export default ColorInput;
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
import { clickOutside, portal } from '../../core/actions/dismissable.js';
|
|
5
5
|
import { ListCursor, groupItems, matchQuery, revealIndex } from '../../core/list.svelte.js';
|
|
6
6
|
import { toneFocusWithinBorder, toneSoft, toneText, type Tone } from '../../core/tones.js';
|
|
7
|
-
import { useLocale } from '../../core/locale.svelte.js';
|
|
8
7
|
import { iconMd } from './icon.js';
|
|
9
8
|
|
|
10
9
|
export interface ComboboxOption {
|
|
@@ -51,8 +50,6 @@
|
|
|
51
50
|
class: className = ''
|
|
52
51
|
}: Props = $props();
|
|
53
52
|
|
|
54
|
-
const t = useLocale();
|
|
55
|
-
|
|
56
53
|
let open = $state(false);
|
|
57
54
|
let query = $state('');
|
|
58
55
|
let wrapper: HTMLElement | null = $state(null);
|
|
@@ -161,7 +158,7 @@
|
|
|
161
158
|
aria-controls="{id ?? 'combobox'}-listbox"
|
|
162
159
|
aria-activedescendant={open && flat[active] ? `${id ?? 'combobox'}-opt-${active}` : undefined}
|
|
163
160
|
aria-invalid={invalid ? 'true' : undefined}
|
|
164
|
-
placeholder={selected ? selected.label : (placeholder ??
|
|
161
|
+
placeholder={selected ? selected.label : (placeholder ?? 'Search…')}
|
|
165
162
|
value={open ? query : (selected?.label ?? '')}
|
|
166
163
|
oninput={(event) => {
|
|
167
164
|
query = event.currentTarget.value;
|
|
@@ -179,7 +176,7 @@
|
|
|
179
176
|
<button
|
|
180
177
|
type="button"
|
|
181
178
|
onclick={clear}
|
|
182
|
-
aria-label=
|
|
179
|
+
aria-label="Clear selection"
|
|
183
180
|
class="shrink-0 rounded p-1 text-text-muted transition-colors duration-150 hover:bg-bg-inset hover:text-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current"
|
|
184
181
|
>
|
|
185
182
|
<svg class="size-3.5" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
|
@@ -217,7 +214,7 @@
|
|
|
217
214
|
>
|
|
218
215
|
{#if flat.length === 0}
|
|
219
216
|
<p class="px-3 py-6 text-center text-sm text-text-muted">
|
|
220
|
-
{emptyText ??
|
|
217
|
+
{emptyText ?? 'No matches'}
|
|
221
218
|
</p>
|
|
222
219
|
{:else}
|
|
223
220
|
{#each groups as [group, items] (group)}
|
|
@@ -23,7 +23,6 @@
|
|
|
23
23
|
import { focusTrap } from '../../core/actions/dismissable.js';
|
|
24
24
|
import { syncDialog } from '../../core/dialog.svelte.js';
|
|
25
25
|
import { ListCursor, groupItems, matchQuery, revealIndex } from '../../core/list.svelte.js';
|
|
26
|
-
import { useLocale } from '../../core/locale.svelte.js';
|
|
27
26
|
import { toneSoft, type Tone } from '../../core/tones.js';
|
|
28
27
|
import { isTypingTarget } from '../../core/trigger.js';
|
|
29
28
|
import { iconMd } from './icon.js';
|
|
@@ -61,8 +60,6 @@
|
|
|
61
60
|
class: className = ''
|
|
62
61
|
}: Props = $props();
|
|
63
62
|
|
|
64
|
-
const t = useLocale();
|
|
65
|
-
|
|
66
63
|
let dialog: HTMLDialogElement | null = $state(null);
|
|
67
64
|
let inputEl: HTMLInputElement | null = $state(null);
|
|
68
65
|
let listEl: HTMLElement | null = $state(null);
|
|
@@ -182,8 +179,8 @@
|
|
|
182
179
|
oninput={() => cursor.reset()}
|
|
183
180
|
type="text"
|
|
184
181
|
autocomplete="off"
|
|
185
|
-
aria-label={placeholder ??
|
|
186
|
-
placeholder={placeholder ??
|
|
182
|
+
aria-label={placeholder ?? 'Type a command or search…'}
|
|
183
|
+
placeholder={placeholder ?? 'Type a command or search…'}
|
|
187
184
|
class="h-12 w-full min-w-0 bg-transparent font-sans text-sm text-text placeholder:text-text-muted focus:outline-none"
|
|
188
185
|
/>
|
|
189
186
|
<Kbd>esc</Kbd>
|
|
@@ -197,7 +194,7 @@
|
|
|
197
194
|
>
|
|
198
195
|
{#if flat.length === 0}
|
|
199
196
|
<p class="px-4 py-10 text-center font-sans text-sm text-text-muted">
|
|
200
|
-
{emptyText ??
|
|
197
|
+
{emptyText ?? 'No results'}
|
|
201
198
|
</p>
|
|
202
199
|
{:else}
|
|
203
200
|
{#each groups as [group, groupItems] (group)}
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import Button from './Button.svelte';
|
|
4
4
|
import Modal from './Modal.svelte';
|
|
5
|
-
import { useLocale } from '../../core/locale.svelte.js';
|
|
6
5
|
import { toneSoft, type Tone } from '../../core/tones.js';
|
|
7
6
|
|
|
8
7
|
interface Props {
|
|
@@ -37,8 +36,6 @@
|
|
|
37
36
|
children
|
|
38
37
|
}: Props = $props();
|
|
39
38
|
|
|
40
|
-
const t = useLocale();
|
|
41
|
-
|
|
42
39
|
async function confirm() {
|
|
43
40
|
try {
|
|
44
41
|
loading = true;
|
|
@@ -88,10 +85,10 @@
|
|
|
88
85
|
|
|
89
86
|
{#snippet footer()}
|
|
90
87
|
<Button variant="outline" tone="neutral" onclick={cancel} disabled={loading}>
|
|
91
|
-
{cancelLabel ??
|
|
88
|
+
{cancelLabel ?? 'Cancel'}
|
|
92
89
|
</Button>
|
|
93
90
|
<Button {tone} onclick={confirm} {loading}>
|
|
94
|
-
{confirmLabel ??
|
|
91
|
+
{confirmLabel ?? 'Confirm'}
|
|
95
92
|
</Button>
|
|
96
93
|
{/snippet}
|
|
97
94
|
</Modal>
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import { anchored } from '../../core/actions/anchor.js';
|
|
4
4
|
import { clickOutside, focusTrap, navigateList, portal } from '../../core/actions/dismissable.js';
|
|
5
5
|
import { menuItems } from '../../core/trigger.js';
|
|
6
|
-
import { useLocale } from '../../core/locale.svelte.js';
|
|
7
6
|
|
|
8
7
|
interface Props {
|
|
9
8
|
/** The region that responds to a right-click. */
|
|
@@ -27,8 +26,6 @@
|
|
|
27
26
|
menuClass = ''
|
|
28
27
|
}: Props = $props();
|
|
29
28
|
|
|
30
|
-
const t = useLocale();
|
|
31
|
-
|
|
32
29
|
let menuEl: HTMLElement | null = $state(null);
|
|
33
30
|
/**
|
|
34
31
|
* A 0×0 element parked at the pointer, so the same `anchored` action that
|
|
@@ -79,7 +76,7 @@
|
|
|
79
76
|
if ((event.target as HTMLElement).closest('[role="menuitem"]:not([disabled])')) open = false;
|
|
80
77
|
}}
|
|
81
78
|
role="menu"
|
|
82
|
-
aria-label={label ??
|
|
79
|
+
aria-label={label ?? 'Menu'}
|
|
83
80
|
tabindex="-1"
|
|
84
81
|
class="z-50 flex max-h-[min(24rem,80vh)] min-w-48 flex-col overflow-y-auto rounded-lg border border-hairline bg-bg py-1 font-sans shadow-lg {menuClass}"
|
|
85
82
|
>
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
3
|
+
import { formatGrouped, parseGrouped } from '../../core/number.js';
|
|
4
|
+
import type { Tone } from '../../core/tones.js';
|
|
5
|
+
import Input, { type InputSize } from './Input.svelte';
|
|
6
|
+
|
|
7
|
+
interface Props extends Omit<
|
|
8
|
+
HTMLInputAttributes,
|
|
9
|
+
'size' | 'prefix' | 'suffix' | 'value' | 'min' | 'max'
|
|
10
|
+
> {
|
|
11
|
+
/** Bindable amount. `null` is an empty field, which is not the same as 0. */
|
|
12
|
+
value?: number | null;
|
|
13
|
+
min?: number;
|
|
14
|
+
max?: number;
|
|
15
|
+
/** Thousands separator. */
|
|
16
|
+
group?: string;
|
|
17
|
+
/** Decimal separator. */
|
|
18
|
+
decimal?: string;
|
|
19
|
+
/** Decimal places the amount settles to when the field loses focus. */
|
|
20
|
+
precision?: number;
|
|
21
|
+
/** Leading mark — `Rp`, `$`, `€`. */
|
|
22
|
+
currency?: string;
|
|
23
|
+
/** Trailing mark instead, for the regions that write it that way. */
|
|
24
|
+
unit?: string;
|
|
25
|
+
size?: InputSize;
|
|
26
|
+
tone?: Tone;
|
|
27
|
+
invalid?: boolean;
|
|
28
|
+
class?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let {
|
|
32
|
+
value = $bindable(null),
|
|
33
|
+
min,
|
|
34
|
+
max,
|
|
35
|
+
group = ',',
|
|
36
|
+
decimal = '.',
|
|
37
|
+
precision = 2,
|
|
38
|
+
currency,
|
|
39
|
+
unit,
|
|
40
|
+
size = 'md',
|
|
41
|
+
tone = 'brand',
|
|
42
|
+
invalid = false,
|
|
43
|
+
disabled = false,
|
|
44
|
+
class: className = '',
|
|
45
|
+
...rest
|
|
46
|
+
}: Props = $props();
|
|
47
|
+
|
|
48
|
+
const options = $derived({ group, decimal, precision });
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* What the field shows. Two states on purpose: grouped and padded while the
|
|
52
|
+
* caret is elsewhere, and exactly what was typed while it is not — grouping
|
|
53
|
+
* a number as it is typed moves the caret out from under the fingers.
|
|
54
|
+
*/
|
|
55
|
+
let text = $state('');
|
|
56
|
+
let editing = $state(false);
|
|
57
|
+
|
|
58
|
+
$effect(() => {
|
|
59
|
+
if (editing) return;
|
|
60
|
+
text = value === null || value === undefined ? '' : formatGrouped(value, options);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Read off the element, not off `text`: the binding that feeds `text` has
|
|
64
|
+
// not flushed yet when this fires, so it still holds the previous keystroke.
|
|
65
|
+
function onInput(event: Event & { currentTarget: HTMLInputElement }) {
|
|
66
|
+
const parsed = parseGrouped(event.currentTarget.value, options);
|
|
67
|
+
value = parsed === null ? null : clamp(parsed);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function clamp(amount: number) {
|
|
71
|
+
if (min !== undefined && amount < min) return min;
|
|
72
|
+
if (max !== undefined && amount > max) return max;
|
|
73
|
+
return amount;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function settle() {
|
|
77
|
+
editing = false;
|
|
78
|
+
const parsed = parseGrouped(text, options);
|
|
79
|
+
value = parsed === null ? null : clamp(parsed);
|
|
80
|
+
text = value === null ? '' : formatGrouped(value, options);
|
|
81
|
+
}
|
|
82
|
+
</script>
|
|
83
|
+
|
|
84
|
+
<Input
|
|
85
|
+
bind:value={text}
|
|
86
|
+
{size}
|
|
87
|
+
{tone}
|
|
88
|
+
{invalid}
|
|
89
|
+
{disabled}
|
|
90
|
+
inputmode="decimal"
|
|
91
|
+
oninput={onInput}
|
|
92
|
+
onfocus={() => (editing = true)}
|
|
93
|
+
onblur={settle}
|
|
94
|
+
class={className}
|
|
95
|
+
{...rest}
|
|
96
|
+
>
|
|
97
|
+
{#snippet prefix()}
|
|
98
|
+
{#if currency}
|
|
99
|
+
<span class="text-xs font-medium text-text-muted">{currency}</span>
|
|
100
|
+
{/if}
|
|
101
|
+
{/snippet}
|
|
102
|
+
|
|
103
|
+
{#snippet suffix()}
|
|
104
|
+
{#if unit}
|
|
105
|
+
<span class="text-xs font-medium text-text-muted">{unit}</span>
|
|
106
|
+
{/if}
|
|
107
|
+
{/snippet}
|
|
108
|
+
</Input>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
2
|
+
import type { Tone } from '../../core/tones.js';
|
|
3
|
+
import { type InputSize } from './Input.svelte';
|
|
4
|
+
interface Props extends Omit<HTMLInputAttributes, 'size' | 'prefix' | 'suffix' | 'value' | 'min' | 'max'> {
|
|
5
|
+
/** Bindable amount. `null` is an empty field, which is not the same as 0. */
|
|
6
|
+
value?: number | null;
|
|
7
|
+
min?: number;
|
|
8
|
+
max?: number;
|
|
9
|
+
/** Thousands separator. */
|
|
10
|
+
group?: string;
|
|
11
|
+
/** Decimal separator. */
|
|
12
|
+
decimal?: string;
|
|
13
|
+
/** Decimal places the amount settles to when the field loses focus. */
|
|
14
|
+
precision?: number;
|
|
15
|
+
/** Leading mark — `Rp`, `$`, `€`. */
|
|
16
|
+
currency?: string;
|
|
17
|
+
/** Trailing mark instead, for the regions that write it that way. */
|
|
18
|
+
unit?: string;
|
|
19
|
+
size?: InputSize;
|
|
20
|
+
tone?: Tone;
|
|
21
|
+
invalid?: boolean;
|
|
22
|
+
class?: string;
|
|
23
|
+
}
|
|
24
|
+
declare const CurrencyInput: import("svelte").Component<Props, {}, "value">;
|
|
25
|
+
type CurrencyInput = ReturnType<typeof CurrencyInput>;
|
|
26
|
+
export default CurrencyInput;
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
import { anchored } from '../../core/actions/anchor.js';
|
|
3
3
|
import { clickOutside, portal } from '../../core/actions/dismissable.js';
|
|
4
4
|
import { dateHint } from '../../core/calendar.js';
|
|
5
|
-
import { formatISO, parseFormatted } from '../../core/date.js';
|
|
6
|
-
import { useLocale } from '../../core/locale.svelte.js';
|
|
5
|
+
import { formatISO, parseFormatted, type DateFormat } from '../../core/date.js';
|
|
7
6
|
import { toneFocusWithinBorder, type Tone } from '../../core/tones.js';
|
|
8
7
|
import Calendar from './Calendar.svelte';
|
|
9
8
|
|
|
@@ -15,6 +14,8 @@
|
|
|
15
14
|
max?: string;
|
|
16
15
|
isDisabled?: (date: Date) => boolean;
|
|
17
16
|
weekStart?: 0 | 1;
|
|
17
|
+
/** Order of the date parts the field accepts and prints. */
|
|
18
|
+
format?: DateFormat;
|
|
18
19
|
tone?: Tone;
|
|
19
20
|
invalid?: boolean;
|
|
20
21
|
disabled?: boolean;
|
|
@@ -33,6 +34,7 @@
|
|
|
33
34
|
max,
|
|
34
35
|
isDisabled,
|
|
35
36
|
weekStart = 1,
|
|
37
|
+
format = 'dmy',
|
|
36
38
|
tone = 'brand',
|
|
37
39
|
invalid = false,
|
|
38
40
|
disabled = false,
|
|
@@ -44,8 +46,6 @@
|
|
|
44
46
|
class: className = ''
|
|
45
47
|
}: Props = $props();
|
|
46
48
|
|
|
47
|
-
const t = useLocale();
|
|
48
|
-
|
|
49
49
|
let open = $state(false);
|
|
50
50
|
let text = $state('');
|
|
51
51
|
let editing = $state(false);
|
|
@@ -53,8 +53,8 @@
|
|
|
53
53
|
let inputEl: HTMLInputElement | null = $state(null);
|
|
54
54
|
|
|
55
55
|
/** While typing the field shows the raw text; otherwise the formatted value. */
|
|
56
|
-
const display = $derived(editing ? text : formatISO(value,
|
|
57
|
-
const hint = $derived(placeholder ?? dateHint(
|
|
56
|
+
const display = $derived(editing ? text : formatISO(value, format));
|
|
57
|
+
const hint = $derived(placeholder ?? dateHint(format));
|
|
58
58
|
|
|
59
59
|
function commit() {
|
|
60
60
|
editing = false;
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
}
|
|
66
66
|
return;
|
|
67
67
|
}
|
|
68
|
-
const parsed = parseFormatted(text,
|
|
68
|
+
const parsed = parseFormatted(text, format);
|
|
69
69
|
if (parsed && parsed !== value) {
|
|
70
70
|
value = parsed;
|
|
71
71
|
onchange?.(parsed);
|
|
@@ -132,7 +132,7 @@
|
|
|
132
132
|
onchange?.('');
|
|
133
133
|
inputEl?.focus();
|
|
134
134
|
}}
|
|
135
|
-
aria-label=
|
|
135
|
+
aria-label="Clear selection"
|
|
136
136
|
class="shrink-0 rounded p-1 text-text-muted transition-colors duration-150 hover:bg-bg-inset hover:text-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current"
|
|
137
137
|
>
|
|
138
138
|
<svg class="size-3.5" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
|
@@ -150,7 +150,7 @@
|
|
|
150
150
|
type="button"
|
|
151
151
|
{disabled}
|
|
152
152
|
onclick={() => (open = !open)}
|
|
153
|
-
aria-label=
|
|
153
|
+
aria-label="Select a date"
|
|
154
154
|
aria-expanded={open}
|
|
155
155
|
class="shrink-0 rounded p-1.5 text-text-muted transition-colors duration-150 hover:bg-bg-inset hover:text-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current"
|
|
156
156
|
>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type DateFormat } from '../../core/date.js';
|
|
1
2
|
import { type Tone } from '../../core/tones.js';
|
|
2
3
|
interface Props {
|
|
3
4
|
/** Bindable `YYYY-MM-DD` — the same shape as `<input type="date">`. */
|
|
@@ -7,6 +8,8 @@ interface Props {
|
|
|
7
8
|
max?: string;
|
|
8
9
|
isDisabled?: (date: Date) => boolean;
|
|
9
10
|
weekStart?: 0 | 1;
|
|
11
|
+
/** Order of the date parts the field accepts and prints. */
|
|
12
|
+
format?: DateFormat;
|
|
10
13
|
tone?: Tone;
|
|
11
14
|
invalid?: boolean;
|
|
12
15
|
disabled?: boolean;
|
|
@@ -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 DrawerSide = 'left' | 'right' | 'top' | 'bottom';
|
|
@@ -39,7 +38,6 @@
|
|
|
39
38
|
...rest
|
|
40
39
|
}: Props = $props();
|
|
41
40
|
|
|
42
|
-
const t = useLocale();
|
|
43
41
|
let dialog: HTMLDialogElement | null = $state(null);
|
|
44
42
|
|
|
45
43
|
const horizontal = $derived(side === 'left' || side === 'right');
|
|
@@ -109,7 +107,7 @@
|
|
|
109
107
|
<button
|
|
110
108
|
type="button"
|
|
111
109
|
onclick={close}
|
|
112
|
-
aria-label=
|
|
110
|
+
aria-label="Close"
|
|
113
111
|
class="-m-1 shrink-0 rounded-md p-1 text-text-muted transition-colors duration-150 hover:bg-bg-inset hover:text-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand"
|
|
114
112
|
>
|
|
115
113
|
<svg class="size-4" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import { anchored, type Placement } from '../../core/actions/anchor.js';
|
|
4
4
|
import { clickOutside, focusTrap, navigateList, portal } from '../../core/actions/dismissable.js';
|
|
5
5
|
import { describeTrigger, menuItems, popupTrigger } from '../../core/trigger.js';
|
|
6
|
-
import { useLocale } from '../../core/locale.svelte.js';
|
|
7
6
|
|
|
8
7
|
interface Props {
|
|
9
8
|
/** The control that opens the menu — put a `Button` in here. */
|
|
@@ -17,7 +16,7 @@
|
|
|
17
16
|
matchWidth?: boolean;
|
|
18
17
|
/** Close as soon as an item inside is activated. Default true. */
|
|
19
18
|
closeOnSelect?: boolean;
|
|
20
|
-
/** Accessible name for the menu. Defaults to
|
|
19
|
+
/** Accessible name for the menu. Defaults to `Menu`. */
|
|
21
20
|
label?: string;
|
|
22
21
|
class?: string;
|
|
23
22
|
/** Classes for the wrapper around the trigger. */
|
|
@@ -37,8 +36,6 @@
|
|
|
37
36
|
triggerClass = ''
|
|
38
37
|
}: Props = $props();
|
|
39
38
|
|
|
40
|
-
const t = useLocale();
|
|
41
|
-
|
|
42
39
|
let anchorEl: HTMLElement | null = $state(null);
|
|
43
40
|
let menuEl: HTMLElement | null = $state(null);
|
|
44
41
|
|
|
@@ -85,7 +82,7 @@
|
|
|
85
82
|
onkeydown={onMenuKeydown}
|
|
86
83
|
onclick={onMenuClick}
|
|
87
84
|
role="menu"
|
|
88
|
-
aria-label={label ??
|
|
85
|
+
aria-label={label ?? 'Menu'}
|
|
89
86
|
tabindex="-1"
|
|
90
87
|
class="z-50 flex max-h-[min(24rem,80vh)] min-w-48 flex-col overflow-y-auto rounded-lg border border-hairline bg-bg py-1 font-sans shadow-lg {className}"
|
|
91
88
|
>
|
|
@@ -12,7 +12,7 @@ interface Props {
|
|
|
12
12
|
matchWidth?: boolean;
|
|
13
13
|
/** Close as soon as an item inside is activated. Default true. */
|
|
14
14
|
closeOnSelect?: boolean;
|
|
15
|
-
/** Accessible name for the menu. Defaults to
|
|
15
|
+
/** Accessible name for the menu. Defaults to `Menu`. */
|
|
16
16
|
label?: string;
|
|
17
17
|
class?: string;
|
|
18
18
|
/** Classes for the wrapper around the trigger. */
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import { toneBorder, toneSoft, toneSurface, toneText, type Tone } from '../../core/tones.js';
|
|
4
|
-
import { useLocale } from '../../core/locale.svelte.js';
|
|
5
4
|
import { formatSize as toReadableSize, sortFiles, type RejectedFile } from '../../core/files.js';
|
|
6
5
|
|
|
7
6
|
interface Props {
|
|
@@ -16,7 +15,7 @@
|
|
|
16
15
|
maxFiles?: number;
|
|
17
16
|
disabled?: boolean;
|
|
18
17
|
tone?: Tone;
|
|
19
|
-
/** Headline inside the zone. Defaults to
|
|
18
|
+
/** Headline inside the zone. Defaults to `Drop files here`. */
|
|
20
19
|
label?: string;
|
|
21
20
|
hint?: string;
|
|
22
21
|
/** Hide the built-in file list to render your own. */
|
|
@@ -45,15 +44,13 @@
|
|
|
45
44
|
class: className = ''
|
|
46
45
|
}: Props = $props();
|
|
47
46
|
|
|
48
|
-
const t = useLocale();
|
|
49
|
-
|
|
50
47
|
let input: HTMLInputElement | null = $state(null);
|
|
51
48
|
let dragging = $state(false);
|
|
52
49
|
let rejected = $state<RejectedFile[]>([]);
|
|
53
50
|
/** Nested elements fire dragleave too, so count depth instead of toggling. */
|
|
54
51
|
let depth = 0;
|
|
55
52
|
|
|
56
|
-
const formatSize = (bytes: number) => toReadableSize(bytes
|
|
53
|
+
const formatSize = (bytes: number) => toReadableSize(bytes);
|
|
57
54
|
|
|
58
55
|
const defaultHint = $derived.by(() => {
|
|
59
56
|
if (hint) return hint;
|
|
@@ -96,9 +93,9 @@
|
|
|
96
93
|
}
|
|
97
94
|
|
|
98
95
|
const reasons = $derived({
|
|
99
|
-
type:
|
|
100
|
-
size:
|
|
101
|
-
count:
|
|
96
|
+
type: 'wrong file type',
|
|
97
|
+
size: 'too large',
|
|
98
|
+
count: 'over the file limit'
|
|
102
99
|
});
|
|
103
100
|
</script>
|
|
104
101
|
|
|
@@ -146,10 +143,10 @@
|
|
|
146
143
|
{/if}
|
|
147
144
|
</span>
|
|
148
145
|
<span class="flex flex-col gap-1">
|
|
149
|
-
<span class="text-sm font-semibold text-text">{label ??
|
|
146
|
+
<span class="text-sm font-semibold text-text">{label ?? 'Drop files here'}</span>
|
|
150
147
|
<span class="text-[13px] text-text-secondary">
|
|
151
|
-
|
|
152
|
-
<span class="underline underline-offset-2">
|
|
148
|
+
or
|
|
149
|
+
<span class="underline underline-offset-2">browse</span>
|
|
153
150
|
</span>
|
|
154
151
|
{#if defaultHint}
|
|
155
152
|
<span class="text-xs text-text-muted">{defaultHint}</span>
|
|
@@ -191,7 +188,7 @@
|
|
|
191
188
|
<button
|
|
192
189
|
type="button"
|
|
193
190
|
onclick={() => remove(i)}
|
|
194
|
-
aria-label="
|
|
191
|
+
aria-label="Remove {file.name}"
|
|
195
192
|
class="-m-1 shrink-0 rounded p-1 text-text-muted transition-colors duration-150 hover:bg-bg-inset hover:text-danger focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-danger"
|
|
196
193
|
>
|
|
197
194
|
<svg class="size-3.5" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
|
@@ -13,7 +13,7 @@ interface Props {
|
|
|
13
13
|
maxFiles?: number;
|
|
14
14
|
disabled?: boolean;
|
|
15
15
|
tone?: Tone;
|
|
16
|
-
/** Headline inside the zone. Defaults to
|
|
16
|
+
/** Headline inside the zone. Defaults to `Drop files here`. */
|
|
17
17
|
label?: string;
|
|
18
18
|
hint?: string;
|
|
19
19
|
/** Hide the built-in file list to render your own. */
|