@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,170 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { addTag, splitTags, type TagRejection } from '../../core/tags.js';
|
|
3
|
+
import { toneFocusWithinBorder, toneSoft, type Tone } from '../../core/tones.js';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
/** Bindable list of tags, in the order they were added. */
|
|
7
|
+
tags?: string[];
|
|
8
|
+
placeholder?: string;
|
|
9
|
+
/** Cap on how many may be held. `0` means no limit. */
|
|
10
|
+
max?: number;
|
|
11
|
+
/** Shortest a tag may be, after trimming. */
|
|
12
|
+
minLength?: number;
|
|
13
|
+
/** Keys that commit the tag under the caret, besides Enter. */
|
|
14
|
+
separators?: string[];
|
|
15
|
+
/** Refuse a tag that differs from an existing one only in case. */
|
|
16
|
+
caseInsensitive?: boolean;
|
|
17
|
+
/** Commit whatever is half-typed when the field loses focus. */
|
|
18
|
+
commitOnBlur?: boolean;
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
invalid?: boolean;
|
|
21
|
+
tone?: Tone;
|
|
22
|
+
/** `name` for a plain form submit — one hidden field, comma-joined. */
|
|
23
|
+
name?: string;
|
|
24
|
+
onadd?: (tag: string) => void;
|
|
25
|
+
onremove?: (tag: string) => void;
|
|
26
|
+
onreject?: (reason: TagRejection, raw: string) => void;
|
|
27
|
+
class?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let {
|
|
31
|
+
tags = $bindable([]),
|
|
32
|
+
placeholder,
|
|
33
|
+
max = 0,
|
|
34
|
+
minLength = 1,
|
|
35
|
+
separators = [','],
|
|
36
|
+
caseInsensitive = true,
|
|
37
|
+
commitOnBlur = true,
|
|
38
|
+
disabled = false,
|
|
39
|
+
invalid = false,
|
|
40
|
+
tone = 'brand',
|
|
41
|
+
name,
|
|
42
|
+
onadd,
|
|
43
|
+
onremove,
|
|
44
|
+
onreject,
|
|
45
|
+
class: className = ''
|
|
46
|
+
}: Props = $props();
|
|
47
|
+
|
|
48
|
+
let draft = $state('');
|
|
49
|
+
let field: HTMLInputElement | null = $state(null);
|
|
50
|
+
/** The last refusal, cleared as soon as the next keystroke lands. */
|
|
51
|
+
let refused = $state<TagRejection | null>(null);
|
|
52
|
+
|
|
53
|
+
const full = $derived(max > 0 && tags.length >= max);
|
|
54
|
+
const rules = $derived({ max, minLength, caseInsensitive });
|
|
55
|
+
|
|
56
|
+
function commit(raw: string) {
|
|
57
|
+
const { tags: next, rejected } = addTag(tags, raw, rules);
|
|
58
|
+
if (rejected) {
|
|
59
|
+
// An empty commit is the Enter key on an empty field, not a mistake.
|
|
60
|
+
if (rejected !== 'empty') {
|
|
61
|
+
refused = rejected;
|
|
62
|
+
onreject?.(rejected, raw);
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
tags = next;
|
|
67
|
+
onadd?.(next[next.length - 1]);
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function remove(index: number) {
|
|
72
|
+
const [gone] = tags.slice(index, index + 1);
|
|
73
|
+
tags = tags.filter((_, at) => at !== index);
|
|
74
|
+
if (gone !== undefined) onremove?.(gone);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function onKeydown(event: KeyboardEvent) {
|
|
78
|
+
refused = null;
|
|
79
|
+
|
|
80
|
+
if (event.key === 'Enter' || separators.includes(event.key)) {
|
|
81
|
+
event.preventDefault();
|
|
82
|
+
if (commit(draft)) draft = '';
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Empty field: Backspace peels the last chip off, the way chips behave
|
|
87
|
+
// everywhere else in this library.
|
|
88
|
+
if (event.key === 'Backspace' && draft === '' && tags.length) {
|
|
89
|
+
event.preventDefault();
|
|
90
|
+
remove(tags.length - 1);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function onPaste(event: ClipboardEvent) {
|
|
95
|
+
const text = event.clipboardData?.getData('text') ?? '';
|
|
96
|
+
const parts = splitTags(text, separators);
|
|
97
|
+
if (parts.length <= 1) return;
|
|
98
|
+
|
|
99
|
+
event.preventDefault();
|
|
100
|
+
for (const part of parts) commit(part);
|
|
101
|
+
draft = '';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function onBlur() {
|
|
105
|
+
if (commitOnBlur && draft.trim() && commit(draft)) draft = '';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const messages: Record<TagRejection, string> = $derived({
|
|
109
|
+
empty: '',
|
|
110
|
+
short: `${minLength}+`,
|
|
111
|
+
duplicate: 'Already added',
|
|
112
|
+
full: `$selected ${tags.length}/${max}`
|
|
113
|
+
});
|
|
114
|
+
</script>
|
|
115
|
+
|
|
116
|
+
<div class="flex w-full flex-col gap-1.5 {className}">
|
|
117
|
+
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
|
|
118
|
+
<div
|
|
119
|
+
onclick={() => field?.focus()}
|
|
120
|
+
class="flex w-full flex-wrap items-center gap-1.5 border bg-bg py-1.5 pr-1.5 pl-2 transition-colors duration-150 ease-brand-out
|
|
121
|
+
{invalid
|
|
122
|
+
? 'border-danger focus-within:border-danger'
|
|
123
|
+
: `border-hairline ${toneFocusWithinBorder[tone]}`}
|
|
124
|
+
{disabled ? 'pointer-events-none opacity-50' : ''}"
|
|
125
|
+
>
|
|
126
|
+
{#each tags as tag, index (tag)}
|
|
127
|
+
<span
|
|
128
|
+
class="inline-flex items-center gap-1.5 rounded-full py-1 pr-1 pl-2.5 font-mono text-xs {toneSoft[
|
|
129
|
+
tone
|
|
130
|
+
]}"
|
|
131
|
+
>
|
|
132
|
+
{tag}
|
|
133
|
+
<button
|
|
134
|
+
type="button"
|
|
135
|
+
onclick={() => remove(index)}
|
|
136
|
+
aria-label="Remove {tag}"
|
|
137
|
+
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"
|
|
138
|
+
>
|
|
139
|
+
<svg class="size-2.5" viewBox="0 0 10 10" fill="none" aria-hidden="true">
|
|
140
|
+
<path d="m1.5 1.5 7 7M8.5 1.5l-7 7" stroke="currentColor" stroke-width="1.5" />
|
|
141
|
+
</svg>
|
|
142
|
+
</button>
|
|
143
|
+
</span>
|
|
144
|
+
{/each}
|
|
145
|
+
|
|
146
|
+
<input
|
|
147
|
+
bind:this={field}
|
|
148
|
+
bind:value={draft}
|
|
149
|
+
{disabled}
|
|
150
|
+
readonly={full}
|
|
151
|
+
placeholder={tags.length === 0 ? (placeholder ?? 'Add a tag') : ''}
|
|
152
|
+
aria-label={placeholder ?? 'Add a tag'}
|
|
153
|
+
aria-invalid={invalid ? 'true' : undefined}
|
|
154
|
+
onkeydown={onKeydown}
|
|
155
|
+
onpaste={onPaste}
|
|
156
|
+
onblur={onBlur}
|
|
157
|
+
class="h-7 min-w-24 flex-1 bg-transparent font-sans text-[15px] text-text placeholder:text-text-muted focus:outline-none"
|
|
158
|
+
/>
|
|
159
|
+
|
|
160
|
+
{#if name}
|
|
161
|
+
<input type="hidden" {name} value={tags.join(',')} />
|
|
162
|
+
{/if}
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
{#if refused && messages[refused]}
|
|
166
|
+
<span class="font-mono text-xs text-danger">{messages[refused]}</span>
|
|
167
|
+
{:else if max}
|
|
168
|
+
<span class="font-mono text-xs text-text-muted">{tags.length}/{max}</span>
|
|
169
|
+
{/if}
|
|
170
|
+
</div>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type TagRejection } from '../../core/tags.js';
|
|
2
|
+
import { type Tone } from '../../core/tones.js';
|
|
3
|
+
interface Props {
|
|
4
|
+
/** Bindable list of tags, in the order they were added. */
|
|
5
|
+
tags?: string[];
|
|
6
|
+
placeholder?: string;
|
|
7
|
+
/** Cap on how many may be held. `0` means no limit. */
|
|
8
|
+
max?: number;
|
|
9
|
+
/** Shortest a tag may be, after trimming. */
|
|
10
|
+
minLength?: number;
|
|
11
|
+
/** Keys that commit the tag under the caret, besides Enter. */
|
|
12
|
+
separators?: string[];
|
|
13
|
+
/** Refuse a tag that differs from an existing one only in case. */
|
|
14
|
+
caseInsensitive?: boolean;
|
|
15
|
+
/** Commit whatever is half-typed when the field loses focus. */
|
|
16
|
+
commitOnBlur?: boolean;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
invalid?: boolean;
|
|
19
|
+
tone?: Tone;
|
|
20
|
+
/** `name` for a plain form submit — one hidden field, comma-joined. */
|
|
21
|
+
name?: string;
|
|
22
|
+
onadd?: (tag: string) => void;
|
|
23
|
+
onremove?: (tag: string) => void;
|
|
24
|
+
onreject?: (reason: TagRejection, raw: string) => void;
|
|
25
|
+
class?: string;
|
|
26
|
+
}
|
|
27
|
+
declare const TagsInput: import("svelte").Component<Props, {}, "tags">;
|
|
28
|
+
type TagsInput = ReturnType<typeof TagsInput>;
|
|
29
|
+
export default TagsInput;
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
storedTheme,
|
|
11
11
|
type Theme
|
|
12
12
|
} from '../../core/theme.svelte.js';
|
|
13
|
-
import { useLocale } from '../../core/locale.svelte.js';
|
|
14
13
|
|
|
15
14
|
interface Props {
|
|
16
15
|
/** Bindable. `'system'` follows the OS until the visitor chooses. */
|
|
@@ -33,8 +32,6 @@
|
|
|
33
32
|
class: className = ''
|
|
34
33
|
}: Props = $props();
|
|
35
34
|
|
|
36
|
-
const t = useLocale();
|
|
37
|
-
|
|
38
35
|
$effect(() => {
|
|
39
36
|
applyTheme(theme);
|
|
40
37
|
if (persist) persistTheme(theme);
|
|
@@ -50,8 +47,8 @@
|
|
|
50
47
|
}
|
|
51
48
|
|
|
52
49
|
const options = $derived([
|
|
53
|
-
{ value: 'light' as Theme, label:
|
|
54
|
-
{ value: 'dark' as Theme, label:
|
|
50
|
+
{ value: 'light' as Theme, label: 'Light' },
|
|
51
|
+
{ value: 'dark' as Theme, label: 'Dark' },
|
|
55
52
|
{ value: 'system' as Theme, label: 'Auto' }
|
|
56
53
|
]);
|
|
57
54
|
</script>
|
|
@@ -85,7 +82,7 @@
|
|
|
85
82
|
{#if variant === 'segmented'}
|
|
86
83
|
<div
|
|
87
84
|
role="radiogroup"
|
|
88
|
-
aria-label=
|
|
85
|
+
aria-label="Toggle theme"
|
|
89
86
|
class="inline-flex items-center gap-0 border border-hairline bg-bg-alt p-1 {className}"
|
|
90
87
|
>
|
|
91
88
|
{#each options as option (option.value)}
|
|
@@ -109,8 +106,8 @@
|
|
|
109
106
|
<button
|
|
110
107
|
type="button"
|
|
111
108
|
onclick={cycle}
|
|
112
|
-
aria-label="
|
|
113
|
-
title=
|
|
109
|
+
aria-label="Toggle theme ({theme})"
|
|
110
|
+
title="Toggle theme"
|
|
114
111
|
class="inline-flex size-9 items-center justify-center border border-hairline transition-colors duration-150 ease-brand-out {focusRing} {toneRing[
|
|
115
112
|
tone
|
|
116
113
|
]} {toneSoft[tone]} {className}"
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
3
|
+
import { clampTime, formatTime, parseTime, stepTime } from '../../core/time.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' | 'min' | 'max' | 'step'
|
|
10
|
+
> {
|
|
11
|
+
/** Bindable `HH:MM`, or `''` for empty. */
|
|
12
|
+
value?: string;
|
|
13
|
+
/** Inclusive bounds, `HH:MM`. */
|
|
14
|
+
min?: string;
|
|
15
|
+
max?: string;
|
|
16
|
+
/** Minutes one press of a stepper moves, and what typing snaps to. */
|
|
17
|
+
step?: number;
|
|
18
|
+
size?: InputSize;
|
|
19
|
+
tone?: Tone;
|
|
20
|
+
invalid?: boolean;
|
|
21
|
+
/** Hide the steppers and let the field be typed into only. */
|
|
22
|
+
steppers?: boolean;
|
|
23
|
+
class?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let {
|
|
27
|
+
value = $bindable(''),
|
|
28
|
+
min,
|
|
29
|
+
max,
|
|
30
|
+
step = 15,
|
|
31
|
+
size = 'md',
|
|
32
|
+
tone = 'brand',
|
|
33
|
+
invalid = false,
|
|
34
|
+
disabled = false,
|
|
35
|
+
steppers = true,
|
|
36
|
+
class: className = '',
|
|
37
|
+
...rest
|
|
38
|
+
}: Props = $props();
|
|
39
|
+
|
|
40
|
+
// Typed text and the bound value are separate until the field settles:
|
|
41
|
+
// `9` is a valid thing to be halfway through typing and an invalid time.
|
|
42
|
+
let text = $state('');
|
|
43
|
+
let editing = $state(false);
|
|
44
|
+
|
|
45
|
+
$effect(() => {
|
|
46
|
+
if (!editing) text = value;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
function settle() {
|
|
50
|
+
editing = false;
|
|
51
|
+
const minutes = parseTime(text);
|
|
52
|
+
value = minutes === null ? '' : formatTime(clampTime(minutes, min, max, step));
|
|
53
|
+
text = value;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function nudge(direction: 1 | -1) {
|
|
57
|
+
value = stepTime(value, direction, step, min, max);
|
|
58
|
+
text = value;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function onKeydown(event: KeyboardEvent) {
|
|
62
|
+
if (event.key === 'ArrowUp') {
|
|
63
|
+
event.preventDefault();
|
|
64
|
+
nudge(1);
|
|
65
|
+
} else if (event.key === 'ArrowDown') {
|
|
66
|
+
event.preventDefault();
|
|
67
|
+
nudge(-1);
|
|
68
|
+
} else if (event.key === 'Enter') {
|
|
69
|
+
settle();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const stepper =
|
|
74
|
+
'grid size-6 place-items-center text-text-secondary transition-colors duration-150 hover:bg-bg-inset hover:text-text focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-current';
|
|
75
|
+
</script>
|
|
76
|
+
|
|
77
|
+
<Input
|
|
78
|
+
bind:value={text}
|
|
79
|
+
{size}
|
|
80
|
+
{tone}
|
|
81
|
+
{invalid}
|
|
82
|
+
{disabled}
|
|
83
|
+
inputmode="numeric"
|
|
84
|
+
placeholder="--:--"
|
|
85
|
+
onfocus={() => (editing = true)}
|
|
86
|
+
onblur={settle}
|
|
87
|
+
onkeydown={onKeydown}
|
|
88
|
+
class="[&_input]:tabular-nums {className}"
|
|
89
|
+
{...rest}
|
|
90
|
+
>
|
|
91
|
+
{#snippet prefix()}
|
|
92
|
+
<svg class="size-4" viewBox="0 0 20 20" fill="none" aria-hidden="true">
|
|
93
|
+
<circle cx="10" cy="10" r="6.5" stroke="currentColor" stroke-width="1.5" />
|
|
94
|
+
<path d="M10 6.5V10l2.5 1.5" stroke="currentColor" stroke-width="1.5" />
|
|
95
|
+
</svg>
|
|
96
|
+
{/snippet}
|
|
97
|
+
|
|
98
|
+
{#snippet suffix()}
|
|
99
|
+
{#if steppers}
|
|
100
|
+
<span class="-mr-2 flex flex-col">
|
|
101
|
+
<button
|
|
102
|
+
type="button"
|
|
103
|
+
class={stepper}
|
|
104
|
+
{disabled}
|
|
105
|
+
onclick={() => nudge(1)}
|
|
106
|
+
aria-label="Increase"
|
|
107
|
+
>
|
|
108
|
+
<svg class="size-3" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
|
109
|
+
<path d="m3 7.5 3-3 3 3" stroke="currentColor" stroke-width="1.5" />
|
|
110
|
+
</svg>
|
|
111
|
+
</button>
|
|
112
|
+
<button
|
|
113
|
+
type="button"
|
|
114
|
+
class={stepper}
|
|
115
|
+
{disabled}
|
|
116
|
+
onclick={() => nudge(-1)}
|
|
117
|
+
aria-label="Decrease"
|
|
118
|
+
>
|
|
119
|
+
<svg class="size-3" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
|
120
|
+
<path d="m3 4.5 3 3 3-3" stroke="currentColor" stroke-width="1.5" />
|
|
121
|
+
</svg>
|
|
122
|
+
</button>
|
|
123
|
+
</span>
|
|
124
|
+
{/if}
|
|
125
|
+
{/snippet}
|
|
126
|
+
</Input>
|
|
@@ -0,0 +1,21 @@
|
|
|
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' | 'min' | 'max' | 'step'> {
|
|
5
|
+
/** Bindable `HH:MM`, or `''` for empty. */
|
|
6
|
+
value?: string;
|
|
7
|
+
/** Inclusive bounds, `HH:MM`. */
|
|
8
|
+
min?: string;
|
|
9
|
+
max?: string;
|
|
10
|
+
/** Minutes one press of a stepper moves, and what typing snaps to. */
|
|
11
|
+
step?: number;
|
|
12
|
+
size?: InputSize;
|
|
13
|
+
tone?: Tone;
|
|
14
|
+
invalid?: boolean;
|
|
15
|
+
/** Hide the steppers and let the field be typed into only. */
|
|
16
|
+
steppers?: boolean;
|
|
17
|
+
class?: string;
|
|
18
|
+
}
|
|
19
|
+
declare const TimeInput: import("svelte").Component<Props, {}, "value">;
|
|
20
|
+
type TimeInput = ReturnType<typeof TimeInput>;
|
|
21
|
+
export default TimeInput;
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
import { toneBorderSoft, toneSurface, toneText } from '../../core/tones.js';
|
|
7
7
|
import { iconMd } from './icon.js';
|
|
8
8
|
import Spinner from './Spinner.svelte';
|
|
9
|
-
import { useLocale } from '../../core/locale.svelte.js';
|
|
10
9
|
|
|
11
10
|
export type ToasterPosition =
|
|
12
11
|
'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right';
|
|
@@ -18,8 +17,6 @@
|
|
|
18
17
|
|
|
19
18
|
let { position = 'bottom-right', class: className = '' }: Props = $props();
|
|
20
19
|
|
|
21
|
-
const t = useLocale();
|
|
22
|
-
|
|
23
20
|
const positions: Record<ToasterPosition, string> = {
|
|
24
21
|
'top-left': 'top-4 left-4 items-start',
|
|
25
22
|
'top-center': 'top-4 left-1/2 -translate-x-1/2 items-center',
|
|
@@ -41,7 +38,7 @@
|
|
|
41
38
|
<div
|
|
42
39
|
use:portal
|
|
43
40
|
aria-live="polite"
|
|
44
|
-
aria-label=
|
|
41
|
+
aria-label="Notifications"
|
|
45
42
|
class="pointer-events-none fixed z-[100] flex w-[min(24rem,calc(100vw-2rem))] flex-col gap-3 {positions[
|
|
46
43
|
position
|
|
47
44
|
]} {fromTop ? '' : 'flex-col-reverse'} {className}"
|
|
@@ -101,7 +98,7 @@
|
|
|
101
98
|
<button
|
|
102
99
|
type="button"
|
|
103
100
|
onclick={() => toast.dismiss(item.id)}
|
|
104
|
-
aria-label=
|
|
101
|
+
aria-label="Dismiss"
|
|
105
102
|
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-current"
|
|
106
103
|
>
|
|
107
104
|
<svg class="size-3.5" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
|
@@ -5,9 +5,8 @@
|
|
|
5
5
|
* `@nqmcreative/ui/core`, re-exported below so this one import covers
|
|
6
6
|
* the whole style.
|
|
7
7
|
*/
|
|
8
|
-
export { TONES, focusRing, peerFocusRing, toneBorder, toneBorderLeft, toneBorderSoft, toneFill, toneFocusBorder, toneFocusWithinBorder, toneHoverBorder, toneHoverText, tonePeerChecked, tonePeerFocus, toneRing, toneSoft, toneSoftHover, toneSolid, toneSolidHover, toneSurface, toneText, anchored, clickOutside, focusTrap, focusable, navigateList, portal, toast,
|
|
9
|
-
export type { Tone, Placement, AnchorOptions, ClickOutsideOptions, FocusTrapOptions, Toast, ToastOptions, ToastAction,
|
|
10
|
-
export { default as LocaleProvider } from './LocaleProvider.svelte';
|
|
8
|
+
export { TONES, focusRing, peerFocusRing, toneBorder, toneBorderLeft, toneBorderSoft, toneFill, toneFocusBorder, toneFocusWithinBorder, toneHoverBorder, toneHoverText, tonePeerChecked, tonePeerFocus, toneRing, toneSoft, toneSoftHover, toneSolid, toneSolidHover, toneSurface, toneText, anchored, clickOutside, focusTrap, focusable, navigateList, portal, toast, applyTheme, storedTheme, toISO, fromISO, today, isSameDay, addDays, addMonths, monthGrid, weekdayNames, monthLabel, isOutOfRange, formatISO, parseFormatted } from '../../core/index.js';
|
|
9
|
+
export type { Tone, Placement, AnchorOptions, ClickOutsideOptions, FocusTrapOptions, Toast, ToastOptions, ToastAction, Theme, DateFormat, TableSort, SortDirection, RejectedFile } from '../../core/index.js';
|
|
11
10
|
export { default as Button } from './Button.svelte';
|
|
12
11
|
export type { ButtonVariant, ButtonSize } from './Button.svelte';
|
|
13
12
|
export { default as Link } from './Link.svelte';
|
|
@@ -30,6 +29,14 @@ export type { CheckboxOption } from './CheckboxGroup.svelte';
|
|
|
30
29
|
export { default as SegmentedControl } from './SegmentedControl.svelte';
|
|
31
30
|
export type { SegmentOption, SegmentedSize } from './SegmentedControl.svelte';
|
|
32
31
|
export { default as InputGroup } from './InputGroup.svelte';
|
|
32
|
+
export { default as InputAddon } from './InputAddon.svelte';
|
|
33
|
+
export { default as SearchInput } from './SearchInput.svelte';
|
|
34
|
+
export { default as PinInput } from './PinInput.svelte';
|
|
35
|
+
export { default as TagsInput } from './TagsInput.svelte';
|
|
36
|
+
export { default as CurrencyInput } from './CurrencyInput.svelte';
|
|
37
|
+
export { default as TimeInput } from './TimeInput.svelte';
|
|
38
|
+
export { default as ColorInput } from './ColorInput.svelte';
|
|
39
|
+
export { default as FileInput } from './FileInput.svelte';
|
|
33
40
|
export { default as Combobox } from './Combobox.svelte';
|
|
34
41
|
export type { ComboboxOption } from './Combobox.svelte';
|
|
35
42
|
export { default as MultiSelect } from './MultiSelect.svelte';
|
|
@@ -6,9 +6,7 @@
|
|
|
6
6
|
* the whole style.
|
|
7
7
|
*/
|
|
8
8
|
/* --- shared behaviour --- */
|
|
9
|
-
export { TONES, focusRing, peerFocusRing, toneBorder, toneBorderLeft, toneBorderSoft, toneFill, toneFocusBorder, toneFocusWithinBorder, toneHoverBorder, toneHoverText, tonePeerChecked, tonePeerFocus, toneRing, toneSoft, toneSoftHover, toneSolid, toneSolidHover, toneSurface, toneText, anchored, clickOutside, focusTrap, focusable, navigateList, portal, toast,
|
|
10
|
-
/* --- locale --- */
|
|
11
|
-
export { default as LocaleProvider } from './LocaleProvider.svelte';
|
|
9
|
+
export { TONES, focusRing, peerFocusRing, toneBorder, toneBorderLeft, toneBorderSoft, toneFill, toneFocusBorder, toneFocusWithinBorder, toneHoverBorder, toneHoverText, tonePeerChecked, tonePeerFocus, toneRing, toneSoft, toneSoftHover, toneSolid, toneSolidHover, toneSurface, toneText, anchored, clickOutside, focusTrap, focusable, navigateList, portal, toast, applyTheme, storedTheme, toISO, fromISO, today, isSameDay, addDays, addMonths, monthGrid, weekdayNames, monthLabel, isOutOfRange, formatISO, parseFormatted } from '../../core/index.js';
|
|
12
10
|
/* --- actions --- */
|
|
13
11
|
export { default as Button } from './Button.svelte';
|
|
14
12
|
export { default as Link } from './Link.svelte';
|
|
@@ -26,6 +24,14 @@ export { default as RadioGroup } from './RadioGroup.svelte';
|
|
|
26
24
|
export { default as CheckboxGroup } from './CheckboxGroup.svelte';
|
|
27
25
|
export { default as SegmentedControl } from './SegmentedControl.svelte';
|
|
28
26
|
export { default as InputGroup } from './InputGroup.svelte';
|
|
27
|
+
export { default as InputAddon } from './InputAddon.svelte';
|
|
28
|
+
export { default as SearchInput } from './SearchInput.svelte';
|
|
29
|
+
export { default as PinInput } from './PinInput.svelte';
|
|
30
|
+
export { default as TagsInput } from './TagsInput.svelte';
|
|
31
|
+
export { default as CurrencyInput } from './CurrencyInput.svelte';
|
|
32
|
+
export { default as TimeInput } from './TimeInput.svelte';
|
|
33
|
+
export { default as ColorInput } from './ColorInput.svelte';
|
|
34
|
+
export { default as FileInput } from './FileInput.svelte';
|
|
29
35
|
export { default as Combobox } from './Combobox.svelte';
|
|
30
36
|
export { default as MultiSelect } from './MultiSelect.svelte';
|
|
31
37
|
export { default as DatePicker } from './DatePicker.svelte';
|
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
toneText,
|
|
9
9
|
type Tone
|
|
10
10
|
} from '../../core/tones.js';
|
|
11
|
-
import { useLocale } from '../../core/locale.svelte.js';
|
|
12
11
|
|
|
13
12
|
export type AlertVariant = 'soft' | 'outline' | 'accent';
|
|
14
13
|
|
|
@@ -36,7 +35,6 @@
|
|
|
36
35
|
...rest
|
|
37
36
|
}: Props = $props();
|
|
38
37
|
|
|
39
|
-
const t = useLocale();
|
|
40
38
|
let open = $state(true);
|
|
41
39
|
|
|
42
40
|
const variants = $derived({
|
|
@@ -82,7 +80,7 @@
|
|
|
82
80
|
<button
|
|
83
81
|
type="button"
|
|
84
82
|
onclick={dismiss}
|
|
85
|
-
aria-label=
|
|
83
|
+
aria-label="Dismiss"
|
|
86
84
|
class="-m-1 shrink-0 rounded p-1 text-text-muted transition-colors duration-150 hover:bg-bg/60 hover:text-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current"
|
|
87
85
|
>
|
|
88
86
|
<svg class="size-3.5" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
4
|
import { toneHoverText, type Tone } from '../../core/tones.js';
|
|
5
|
-
import { useLocale } from '../../core/locale.svelte.js';
|
|
6
5
|
import { iconMd } from './icon.js';
|
|
7
6
|
|
|
8
7
|
export interface BreadcrumbItem {
|
|
@@ -19,11 +18,9 @@
|
|
|
19
18
|
}
|
|
20
19
|
|
|
21
20
|
let { items, separator = '/', tone = 'brand', class: className = '', ...rest }: Props = $props();
|
|
22
|
-
|
|
23
|
-
const t = useLocale();
|
|
24
21
|
</script>
|
|
25
22
|
|
|
26
|
-
<nav aria-label=
|
|
23
|
+
<nav aria-label="Breadcrumb" class="font-sans text-sm {className}" {...rest}>
|
|
27
24
|
<ol class="flex flex-wrap items-center gap-1.5">
|
|
28
25
|
{#each items as item, i (item.label)}
|
|
29
26
|
<li class="flex items-center gap-1.5">
|
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
weekdayNames
|
|
12
12
|
} from '../../core/date.js';
|
|
13
13
|
import { calendarKeyMove } from '../../core/calendar.js';
|
|
14
|
-
import { useLocale } from '../../core/locale.svelte.js';
|
|
15
14
|
import {
|
|
16
15
|
focusRing,
|
|
17
16
|
toneFill,
|
|
@@ -53,8 +52,6 @@
|
|
|
53
52
|
class: className = ''
|
|
54
53
|
}: Props = $props();
|
|
55
54
|
|
|
56
|
-
const t = useLocale();
|
|
57
|
-
|
|
58
55
|
const selected = $derived(fromISO(value));
|
|
59
56
|
const now = today();
|
|
60
57
|
|
|
@@ -71,7 +68,7 @@
|
|
|
71
68
|
});
|
|
72
69
|
|
|
73
70
|
const days = $derived(monthGrid(view, weekStart));
|
|
74
|
-
const weekdays = $derived(weekdayNames(
|
|
71
|
+
const weekdays = $derived(weekdayNames(undefined, weekStart));
|
|
75
72
|
|
|
76
73
|
function blocked(date: Date) {
|
|
77
74
|
return isOutOfRange(date, min, max) || (isDisabled?.(date) ?? false);
|
|
@@ -119,7 +116,7 @@
|
|
|
119
116
|
<button
|
|
120
117
|
type="button"
|
|
121
118
|
onclick={() => show(addMonths(view, -1))}
|
|
122
|
-
aria-label=
|
|
119
|
+
aria-label="Previous month"
|
|
123
120
|
class="{arrow} {focusRing} {toneRing[tone]}"
|
|
124
121
|
>
|
|
125
122
|
<svg class="size-4" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
|
@@ -128,13 +125,13 @@
|
|
|
128
125
|
</button>
|
|
129
126
|
|
|
130
127
|
<span aria-live="polite" class="text-sm font-semibold text-text">
|
|
131
|
-
{monthLabel(view
|
|
128
|
+
{monthLabel(view)}
|
|
132
129
|
</span>
|
|
133
130
|
|
|
134
131
|
<button
|
|
135
132
|
type="button"
|
|
136
133
|
onclick={() => show(addMonths(view, 1))}
|
|
137
|
-
aria-label=
|
|
134
|
+
aria-label="Next month"
|
|
138
135
|
class="{arrow} {focusRing} {toneRing[tone]}"
|
|
139
136
|
>
|
|
140
137
|
<svg class="size-4" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
|
@@ -154,7 +151,7 @@
|
|
|
154
151
|
bind:this={grid}
|
|
155
152
|
role="grid"
|
|
156
153
|
tabindex="-1"
|
|
157
|
-
aria-label=
|
|
154
|
+
aria-label="Select a date"
|
|
158
155
|
onkeydown={onKeydown}
|
|
159
156
|
class="grid grid-cols-7 gap-0.5 focus:outline-none"
|
|
160
157
|
>
|
|
@@ -196,7 +193,7 @@
|
|
|
196
193
|
tone
|
|
197
194
|
]} hover:underline hover:underline-offset-2 {focusRing} {toneRing[tone]}"
|
|
198
195
|
>
|
|
199
|
-
|
|
196
|
+
Today
|
|
200
197
|
</button>
|
|
201
198
|
{/if}
|
|
202
199
|
</div>
|