@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,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="font-mono text-xs tracking-wide">{currency}</span>
|
|
100
|
+
{/if}
|
|
101
|
+
{/snippet}
|
|
102
|
+
|
|
103
|
+
{#snippet suffix()}
|
|
104
|
+
{#if unit}
|
|
105
|
+
<span class="font-mono text-xs tracking-wide">{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 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"
|
|
137
137
|
>
|
|
138
138
|
<svg class="size-3.5" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
|
@@ -145,7 +145,7 @@
|
|
|
145
145
|
type="button"
|
|
146
146
|
{disabled}
|
|
147
147
|
onclick={() => (open = !open)}
|
|
148
|
-
aria-label=
|
|
148
|
+
aria-label="Select a date"
|
|
149
149
|
aria-expanded={open}
|
|
150
150
|
class="shrink-0 p-2 text-text-muted transition-colors duration-150 hover:text-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current"
|
|
151
151
|
>
|
|
@@ -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 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"
|
|
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-44 flex-col overflow-y-auto border border-hairline bg-bg py-1 font-sans {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,8 +44,6 @@
|
|
|
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[]>([]);
|
|
@@ -63,7 +60,7 @@
|
|
|
63
60
|
return parts.join(' · ');
|
|
64
61
|
});
|
|
65
62
|
|
|
66
|
-
const formatSize = (bytes: number) => toReadableSize(bytes
|
|
63
|
+
const formatSize = (bytes: number) => toReadableSize(bytes);
|
|
67
64
|
|
|
68
65
|
function add(incoming: FileList | File[] | null) {
|
|
69
66
|
if (!incoming || disabled) return;
|
|
@@ -96,9 +93,9 @@
|
|
|
96
93
|
}
|
|
97
94
|
|
|
98
95
|
const reasons: Record<RejectedFile['reason'], string> = $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
|
|
|
@@ -135,10 +132,10 @@
|
|
|
135
132
|
{/if}
|
|
136
133
|
</span>
|
|
137
134
|
<span class="flex flex-col gap-1">
|
|
138
|
-
<span class="text-[15px] font-medium text-text">{label ??
|
|
135
|
+
<span class="text-[15px] font-medium text-text">{label ?? 'Drop files here'}</span>
|
|
139
136
|
<span class="text-sm text-text-secondary">
|
|
140
|
-
|
|
141
|
-
<span class="underline underline-offset-4">
|
|
137
|
+
or
|
|
138
|
+
<span class="underline underline-offset-4">browse</span>
|
|
142
139
|
</span>
|
|
143
140
|
{#if defaultHint}
|
|
144
141
|
<span class="font-mono text-xs text-text-muted">{defaultHint}</span>
|
|
@@ -180,7 +177,7 @@
|
|
|
180
177
|
<button
|
|
181
178
|
type="button"
|
|
182
179
|
onclick={() => remove(i)}
|
|
183
|
-
aria-label="
|
|
180
|
+
aria-label="Remove {file.name}"
|
|
184
181
|
class="-m-1 shrink-0 p-1 text-text-muted transition-colors duration-150 hover:text-danger focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-danger"
|
|
185
182
|
>
|
|
186
183
|
<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. */
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { formatSize as toReadableSize, sortFiles, type RejectedFile } from '../../core/files.js';
|
|
3
|
+
import { toneFocusWithinBorder, type Tone } from '../../core/tones.js';
|
|
4
|
+
import type { InputSize } from './Input.svelte';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
/** Bindable list of accepted files. */
|
|
8
|
+
files?: File[];
|
|
9
|
+
/** Same syntax as the input's `accept` — `'image/*,.pdf'`. */
|
|
10
|
+
accept?: string;
|
|
11
|
+
multiple?: boolean;
|
|
12
|
+
/** Per-file ceiling in bytes. `0` means no limit. */
|
|
13
|
+
maxSize?: number;
|
|
14
|
+
/** Cap on how many files may be held at once. `0` means no limit. */
|
|
15
|
+
maxFiles?: number;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
size?: InputSize;
|
|
18
|
+
tone?: Tone;
|
|
19
|
+
invalid?: boolean;
|
|
20
|
+
/** Text on the button. Defaults to `Choose file`. */
|
|
21
|
+
label?: string;
|
|
22
|
+
/** Shown while nothing is chosen. Defaults to `No file chosen`. */
|
|
23
|
+
placeholder?: string;
|
|
24
|
+
/** Show the list of held files below the row. */
|
|
25
|
+
showList?: boolean;
|
|
26
|
+
/** The `name` the hidden input posts under, for a plain form submit. */
|
|
27
|
+
name?: string;
|
|
28
|
+
onaccept?: (files: File[]) => void;
|
|
29
|
+
onreject?: (rejected: RejectedFile[]) => void;
|
|
30
|
+
class?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let {
|
|
34
|
+
files = $bindable([]),
|
|
35
|
+
accept,
|
|
36
|
+
multiple = false,
|
|
37
|
+
maxSize = 0,
|
|
38
|
+
maxFiles = 0,
|
|
39
|
+
disabled = false,
|
|
40
|
+
size = 'md',
|
|
41
|
+
tone = 'brand',
|
|
42
|
+
invalid = false,
|
|
43
|
+
label,
|
|
44
|
+
placeholder,
|
|
45
|
+
showList = true,
|
|
46
|
+
name,
|
|
47
|
+
onaccept,
|
|
48
|
+
onreject,
|
|
49
|
+
class: className = ''
|
|
50
|
+
}: Props = $props();
|
|
51
|
+
|
|
52
|
+
let input: HTMLInputElement | null = $state(null);
|
|
53
|
+
let rejected = $state<RejectedFile[]>([]);
|
|
54
|
+
|
|
55
|
+
const formatSize = (bytes: number) => toReadableSize(bytes);
|
|
56
|
+
|
|
57
|
+
/** One file reads as its name and weight; several read as a count. */
|
|
58
|
+
const summary = $derived.by(() => {
|
|
59
|
+
if (files.length === 0) return placeholder ?? 'No file chosen';
|
|
60
|
+
if (files.length === 1) return `${files[0].name} · ${formatSize(files[0].size)}`;
|
|
61
|
+
return `${files.length} $selected`;
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
function add(incoming: FileList | File[] | null) {
|
|
65
|
+
if (!incoming || disabled) return;
|
|
66
|
+
|
|
67
|
+
const { accepted, rejected: bad } = sortFiles(incoming, {
|
|
68
|
+
accept,
|
|
69
|
+
maxSize,
|
|
70
|
+
maxFiles,
|
|
71
|
+
multiple,
|
|
72
|
+
held: files.length
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
rejected = bad;
|
|
76
|
+
if (bad.length) onreject?.(bad);
|
|
77
|
+
if (accepted.length === 0) return;
|
|
78
|
+
|
|
79
|
+
files = multiple ? [...files, ...accepted] : accepted.slice(0, 1);
|
|
80
|
+
onaccept?.(accepted);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function clear() {
|
|
84
|
+
files = [];
|
|
85
|
+
rejected = [];
|
|
86
|
+
if (input) input.value = '';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const reasons: Record<RejectedFile['reason'], string> = $derived({
|
|
90
|
+
type: 'wrong file type',
|
|
91
|
+
size: 'too large',
|
|
92
|
+
count: 'over the file limit'
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const sizes: Record<InputSize, string> = {
|
|
96
|
+
sm: 'h-8 text-[13px]',
|
|
97
|
+
md: 'h-10 text-[15px]',
|
|
98
|
+
lg: 'h-11 text-base'
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const buttons: Record<InputSize, string> = {
|
|
102
|
+
sm: 'h-6 px-2 text-[11px]',
|
|
103
|
+
md: 'h-7 px-2.5 text-xs',
|
|
104
|
+
lg: 'h-8 px-3 text-xs'
|
|
105
|
+
};
|
|
106
|
+
</script>
|
|
107
|
+
|
|
108
|
+
<div class="flex w-full flex-col gap-3 {className}">
|
|
109
|
+
<span
|
|
110
|
+
class="inline-flex w-full items-center gap-2 border bg-bg pr-1 pl-1 transition-colors duration-150 ease-brand-out
|
|
111
|
+
{sizes[size]}
|
|
112
|
+
{invalid
|
|
113
|
+
? 'border-danger focus-within:border-danger'
|
|
114
|
+
: `border-hairline ${toneFocusWithinBorder[tone]}`}
|
|
115
|
+
{disabled ? 'pointer-events-none opacity-50' : ''}"
|
|
116
|
+
>
|
|
117
|
+
<button
|
|
118
|
+
type="button"
|
|
119
|
+
{disabled}
|
|
120
|
+
onclick={() => input?.click()}
|
|
121
|
+
class="shrink-0 border border-hairline bg-bg-alt font-mono tracking-wide text-text-secondary transition-colors duration-150 hover:bg-bg-inset hover:text-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current
|
|
122
|
+
{buttons[size]}"
|
|
123
|
+
>
|
|
124
|
+
{label ?? 'Choose file'}
|
|
125
|
+
</button>
|
|
126
|
+
|
|
127
|
+
<span
|
|
128
|
+
class="min-w-0 flex-1 truncate font-sans {files.length ? 'text-text' : 'text-text-muted'}"
|
|
129
|
+
>
|
|
130
|
+
{summary}
|
|
131
|
+
</span>
|
|
132
|
+
|
|
133
|
+
{#if files.length}
|
|
134
|
+
<button
|
|
135
|
+
type="button"
|
|
136
|
+
onclick={clear}
|
|
137
|
+
aria-label="Remove"
|
|
138
|
+
class="mr-1 shrink-0 p-1 text-text-muted transition-colors duration-150 hover:text-danger focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-danger"
|
|
139
|
+
>
|
|
140
|
+
<svg class="size-3.5" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
|
141
|
+
<path d="m2 2 10 10M12 2 2 12" stroke="currentColor" stroke-width="1.5" />
|
|
142
|
+
</svg>
|
|
143
|
+
</button>
|
|
144
|
+
{/if}
|
|
145
|
+
</span>
|
|
146
|
+
|
|
147
|
+
<input
|
|
148
|
+
bind:this={input}
|
|
149
|
+
type="file"
|
|
150
|
+
{name}
|
|
151
|
+
{accept}
|
|
152
|
+
{multiple}
|
|
153
|
+
{disabled}
|
|
154
|
+
aria-invalid={invalid ? 'true' : undefined}
|
|
155
|
+
class="sr-only"
|
|
156
|
+
onchange={(event) => add(event.currentTarget.files)}
|
|
157
|
+
/>
|
|
158
|
+
|
|
159
|
+
{#if rejected.length}
|
|
160
|
+
<ul class="flex flex-col gap-1">
|
|
161
|
+
{#each rejected as item (item.file.name + item.reason)}
|
|
162
|
+
<li class="font-sans text-sm text-danger">
|
|
163
|
+
{item.file.name} — {reasons[item.reason]}
|
|
164
|
+
</li>
|
|
165
|
+
{/each}
|
|
166
|
+
</ul>
|
|
167
|
+
{/if}
|
|
168
|
+
|
|
169
|
+
{#if showList && multiple && files.length > 1}
|
|
170
|
+
<ul class="flex flex-col border border-hairline">
|
|
171
|
+
{#each files as file, i (file.name + file.size + i)}
|
|
172
|
+
<li
|
|
173
|
+
class="flex items-center gap-3 border-b border-hairline px-3.5 py-2.5 font-sans last:border-b-0"
|
|
174
|
+
>
|
|
175
|
+
<span class="min-w-0 flex-1 truncate text-sm text-text">{file.name}</span>
|
|
176
|
+
<span class="shrink-0 font-mono text-xs text-text-muted">{formatSize(file.size)}</span>
|
|
177
|
+
<button
|
|
178
|
+
type="button"
|
|
179
|
+
onclick={() => (files = files.filter((_, at) => at !== i))}
|
|
180
|
+
aria-label="Remove {file.name}"
|
|
181
|
+
class="-m-1 shrink-0 p-1 text-text-muted transition-colors duration-150 hover:text-danger focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-danger"
|
|
182
|
+
>
|
|
183
|
+
<svg class="size-3.5" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
|
184
|
+
<path d="m2 2 10 10M12 2 2 12" stroke="currentColor" stroke-width="1.5" />
|
|
185
|
+
</svg>
|
|
186
|
+
</button>
|
|
187
|
+
</li>
|
|
188
|
+
{/each}
|
|
189
|
+
</ul>
|
|
190
|
+
{/if}
|
|
191
|
+
</div>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type RejectedFile } from '../../core/files.js';
|
|
2
|
+
import { type Tone } from '../../core/tones.js';
|
|
3
|
+
import type { InputSize } from './Input.svelte';
|
|
4
|
+
interface Props {
|
|
5
|
+
/** Bindable list of accepted files. */
|
|
6
|
+
files?: File[];
|
|
7
|
+
/** Same syntax as the input's `accept` — `'image/*,.pdf'`. */
|
|
8
|
+
accept?: string;
|
|
9
|
+
multiple?: boolean;
|
|
10
|
+
/** Per-file ceiling in bytes. `0` means no limit. */
|
|
11
|
+
maxSize?: number;
|
|
12
|
+
/** Cap on how many files may be held at once. `0` means no limit. */
|
|
13
|
+
maxFiles?: number;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
size?: InputSize;
|
|
16
|
+
tone?: Tone;
|
|
17
|
+
invalid?: boolean;
|
|
18
|
+
/** Text on the button. Defaults to `Choose file`. */
|
|
19
|
+
label?: string;
|
|
20
|
+
/** Shown while nothing is chosen. Defaults to `No file chosen`. */
|
|
21
|
+
placeholder?: string;
|
|
22
|
+
/** Show the list of held files below the row. */
|
|
23
|
+
showList?: boolean;
|
|
24
|
+
/** The `name` the hidden input posts under, for a plain form submit. */
|
|
25
|
+
name?: string;
|
|
26
|
+
onaccept?: (files: File[]) => void;
|
|
27
|
+
onreject?: (rejected: RejectedFile[]) => void;
|
|
28
|
+
class?: string;
|
|
29
|
+
}
|
|
30
|
+
declare const FileInput: import("svelte").Component<Props, {}, "files">;
|
|
31
|
+
type FileInput = ReturnType<typeof FileInput>;
|
|
32
|
+
export default FileInput;
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
columns?: FooterColumn[];
|
|
23
23
|
/** Logo, wordmark and a line of copy on the left. */
|
|
24
24
|
brand?: Snippet;
|
|
25
|
-
/** Right side of the bottom bar — social links, a
|
|
25
|
+
/** Right side of the bottom bar — social links, a language picker. */
|
|
26
26
|
bottom?: Snippet;
|
|
27
27
|
/** Left side of the bottom bar. Ignored when `bottom` covers both. */
|
|
28
28
|
copyright?: string;
|
|
@@ -15,7 +15,7 @@ interface Props {
|
|
|
15
15
|
columns?: FooterColumn[];
|
|
16
16
|
/** Logo, wordmark and a line of copy on the left. */
|
|
17
17
|
brand?: Snippet;
|
|
18
|
-
/** Right side of the bottom bar — social links, a
|
|
18
|
+
/** Right side of the bottom bar — social links, a language picker. */
|
|
19
19
|
bottom?: Snippet;
|
|
20
20
|
/** Left side of the bottom bar. Ignored when `bottom` covers both. */
|
|
21
21
|
copyright?: string;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
+
import type { InputSize } from './Input.svelte';
|
|
5
|
+
|
|
6
|
+
interface Props extends HTMLAttributes<HTMLSpanElement> {
|
|
7
|
+
/** Match the Input it sits beside — the heights have to agree. */
|
|
8
|
+
size?: InputSize;
|
|
9
|
+
/** Paint it like a field instead of a fixed label. */
|
|
10
|
+
variant?: 'muted' | 'plain';
|
|
11
|
+
children: Snippet;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let {
|
|
15
|
+
size = 'md',
|
|
16
|
+
variant = 'muted',
|
|
17
|
+
class: className = '',
|
|
18
|
+
children,
|
|
19
|
+
...rest
|
|
20
|
+
}: Props = $props();
|
|
21
|
+
|
|
22
|
+
const sizes: Record<InputSize, string> = {
|
|
23
|
+
sm: 'h-8 px-2.5 text-[13px]',
|
|
24
|
+
md: 'h-10 px-3.5 text-[15px]',
|
|
25
|
+
lg: 'h-11 px-3.5 text-base'
|
|
26
|
+
};
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<span
|
|
30
|
+
class="inline-flex shrink-0 items-center gap-1.5 border border-hairline font-mono whitespace-nowrap text-text-secondary
|
|
31
|
+
{sizes[size]}
|
|
32
|
+
{variant === 'muted' ? 'bg-bg-alt' : 'bg-bg'} {className}"
|
|
33
|
+
{...rest}
|
|
34
|
+
>
|
|
35
|
+
{@render children()}
|
|
36
|
+
</span>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
import type { InputSize } from './Input.svelte';
|
|
4
|
+
interface Props extends HTMLAttributes<HTMLSpanElement> {
|
|
5
|
+
/** Match the Input it sits beside — the heights have to agree. */
|
|
6
|
+
size?: InputSize;
|
|
7
|
+
/** Paint it like a field instead of a fixed label. */
|
|
8
|
+
variant?: 'muted' | 'plain';
|
|
9
|
+
children: Snippet;
|
|
10
|
+
}
|
|
11
|
+
declare const InputAddon: import("svelte").Component<Props, {}, "">;
|
|
12
|
+
type InputAddon = ReturnType<typeof InputAddon>;
|
|
13
|
+
export default InputAddon;
|