@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,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-time codes: what a character box accepts, and where a paste lands.
|
|
3
|
+
*
|
|
4
|
+
* The value is a plain compact string — `'0421'` means the first four boxes
|
|
5
|
+
* are filled and the rest are empty. Holes are impossible by construction,
|
|
6
|
+
* which is what lets the component always know which box to focus next.
|
|
7
|
+
*/
|
|
8
|
+
export type PinMode = 'numeric' | 'alphanumeric';
|
|
9
|
+
/** Strips whatever the mode refuses, then trims to `length` characters. */
|
|
10
|
+
export declare function sanitisePin(text: string, mode?: PinMode, length?: number): string;
|
|
11
|
+
/**
|
|
12
|
+
* Writes `text` into the boxes from `index` on.
|
|
13
|
+
*
|
|
14
|
+
* One keystroke replaces one box and leaves the tail alone; a six-character
|
|
15
|
+
* paste into the first box fills all six. Anything past `length` is dropped
|
|
16
|
+
* rather than wrapped, because the extra characters were never part of a code
|
|
17
|
+
* this long.
|
|
18
|
+
*/
|
|
19
|
+
export declare function fillFrom(value: string, index: number, text: string, length: number, mode?: PinMode): string;
|
|
20
|
+
/** Clears one box and everything after it — what Backspace on a filled box does. */
|
|
21
|
+
export declare function clearFrom(value: string, index: number): string;
|
|
22
|
+
/** The box the caret belongs in: the first empty one, or the last. */
|
|
23
|
+
export declare function caretIndex(value: string, length: number): number;
|
package/dist/core/pin.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-time codes: what a character box accepts, and where a paste lands.
|
|
3
|
+
*
|
|
4
|
+
* The value is a plain compact string — `'0421'` means the first four boxes
|
|
5
|
+
* are filled and the rest are empty. Holes are impossible by construction,
|
|
6
|
+
* which is what lets the component always know which box to focus next.
|
|
7
|
+
*/
|
|
8
|
+
const ALLOWED = {
|
|
9
|
+
numeric: /[^0-9]/g,
|
|
10
|
+
alphanumeric: /[^0-9a-zA-Z]/g
|
|
11
|
+
};
|
|
12
|
+
/** Strips whatever the mode refuses, then trims to `length` characters. */
|
|
13
|
+
export function sanitisePin(text, mode = 'numeric', length = Infinity) {
|
|
14
|
+
return text.replace(ALLOWED[mode], '').slice(0, length);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Writes `text` into the boxes from `index` on.
|
|
18
|
+
*
|
|
19
|
+
* One keystroke replaces one box and leaves the tail alone; a six-character
|
|
20
|
+
* paste into the first box fills all six. Anything past `length` is dropped
|
|
21
|
+
* rather than wrapped, because the extra characters were never part of a code
|
|
22
|
+
* this long.
|
|
23
|
+
*/
|
|
24
|
+
export function fillFrom(value, index, text, length, mode = 'numeric') {
|
|
25
|
+
const head = value.slice(0, index);
|
|
26
|
+
const clean = sanitisePin(text, mode, length - head.length);
|
|
27
|
+
if (!clean)
|
|
28
|
+
return value;
|
|
29
|
+
return (head + clean + value.slice(index + clean.length)).slice(0, length);
|
|
30
|
+
}
|
|
31
|
+
/** Clears one box and everything after it — what Backspace on a filled box does. */
|
|
32
|
+
export function clearFrom(value, index) {
|
|
33
|
+
return value.slice(0, index);
|
|
34
|
+
}
|
|
35
|
+
/** The box the caret belongs in: the first empty one, or the last. */
|
|
36
|
+
export function caretIndex(value, length) {
|
|
37
|
+
return Math.min(value.length, length - 1);
|
|
38
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Free-text tokens: what counts as a new tag, and why one was turned away.
|
|
3
|
+
*
|
|
4
|
+
* The rejection is returned rather than thrown or swallowed, so the component
|
|
5
|
+
* can say *why* nothing happened — a silent no-op on a duplicate reads like a
|
|
6
|
+
* broken field.
|
|
7
|
+
*/
|
|
8
|
+
export type TagRejection = 'empty' | 'short' | 'duplicate' | 'full';
|
|
9
|
+
export interface TagRules {
|
|
10
|
+
/** Trim the ends and collapse inner runs of whitespace. Default `true`. */
|
|
11
|
+
trim?: boolean;
|
|
12
|
+
/** Ignore case when refusing duplicates. Default `true`. */
|
|
13
|
+
caseInsensitive?: boolean;
|
|
14
|
+
/** Cap on how many tags may be held. `0` means no limit. */
|
|
15
|
+
max?: number;
|
|
16
|
+
/** Shortest a tag may be, after trimming. */
|
|
17
|
+
minLength?: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Sorts one commit into the next list, or the reason there isn't one.
|
|
21
|
+
*
|
|
22
|
+
* Order matters, the way it does in `sortFiles`: a tag is judged on being
|
|
23
|
+
* present, then long enough, then unique, then whether there is room — so the
|
|
24
|
+
* message a user sees is the first rule they actually broke.
|
|
25
|
+
*/
|
|
26
|
+
export declare function addTag(tags: string[], raw: string, rules?: TagRules): {
|
|
27
|
+
tags: string[];
|
|
28
|
+
rejected?: TagRejection;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Splits pasted text so one paste can commit several tags.
|
|
32
|
+
*
|
|
33
|
+
* Scanned character by character rather than with a RegExp: the separators are
|
|
34
|
+
* the app's to choose, and building a character class out of them means
|
|
35
|
+
* escaping every one that happens to be a metacharacter.
|
|
36
|
+
*/
|
|
37
|
+
export declare function splitTags(text: string, separators?: readonly string[]): string[];
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Free-text tokens: what counts as a new tag, and why one was turned away.
|
|
3
|
+
*
|
|
4
|
+
* The rejection is returned rather than thrown or swallowed, so the component
|
|
5
|
+
* can say *why* nothing happened — a silent no-op on a duplicate reads like a
|
|
6
|
+
* broken field.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Sorts one commit into the next list, or the reason there isn't one.
|
|
10
|
+
*
|
|
11
|
+
* Order matters, the way it does in `sortFiles`: a tag is judged on being
|
|
12
|
+
* present, then long enough, then unique, then whether there is room — so the
|
|
13
|
+
* message a user sees is the first rule they actually broke.
|
|
14
|
+
*/
|
|
15
|
+
export function addTag(tags, raw, rules = {}) {
|
|
16
|
+
const { trim = true, caseInsensitive = true, max = 0, minLength = 1 } = rules;
|
|
17
|
+
const value = trim ? raw.trim().replace(/\s+/g, ' ') : raw;
|
|
18
|
+
if (!value)
|
|
19
|
+
return { tags, rejected: 'empty' };
|
|
20
|
+
if (value.length < minLength)
|
|
21
|
+
return { tags, rejected: 'short' };
|
|
22
|
+
const fold = (text) => (caseInsensitive ? text.toLowerCase() : text);
|
|
23
|
+
if (tags.some((tag) => fold(tag) === fold(value)))
|
|
24
|
+
return { tags, rejected: 'duplicate' };
|
|
25
|
+
if (max && tags.length >= max)
|
|
26
|
+
return { tags, rejected: 'full' };
|
|
27
|
+
return { tags: [...tags, value] };
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Splits pasted text so one paste can commit several tags.
|
|
31
|
+
*
|
|
32
|
+
* Scanned character by character rather than with a RegExp: the separators are
|
|
33
|
+
* the app's to choose, and building a character class out of them means
|
|
34
|
+
* escaping every one that happens to be a metacharacter.
|
|
35
|
+
*/
|
|
36
|
+
export function splitTags(text, separators = [',']) {
|
|
37
|
+
const marks = new Set([...separators, '\n', '\r', '\t']);
|
|
38
|
+
const found = [];
|
|
39
|
+
let current = '';
|
|
40
|
+
for (const char of text) {
|
|
41
|
+
if (marks.has(char)) {
|
|
42
|
+
found.push(current);
|
|
43
|
+
current = '';
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
current += char;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
found.push(current);
|
|
50
|
+
return found.map((tag) => tag.trim()).filter(Boolean);
|
|
51
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clock times as `HH:MM` strings — the shape `<input type="time">` uses, so a
|
|
3
|
+
* form posts exactly what the browser would have posted.
|
|
4
|
+
*
|
|
5
|
+
* Everything here works in minutes since midnight. A day is 1440 of them, and
|
|
6
|
+
* nothing in this module crosses that boundary: stepping past 23:59 stops
|
|
7
|
+
* there rather than wrapping into a day this module cannot name.
|
|
8
|
+
*/
|
|
9
|
+
export declare const MINUTES_IN_DAY = 1440;
|
|
10
|
+
/**
|
|
11
|
+
* Reads `9:5`, `09:05`, `0905` or `09:05:30` into minutes since midnight.
|
|
12
|
+
* Returns `null` for anything that is not a time, including 24:00 and 12:60.
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseTime(text: string): number | null;
|
|
15
|
+
/** Minutes since midnight back to `HH:MM`, always four digits and a colon. */
|
|
16
|
+
export declare function formatTime(minutes: number): string;
|
|
17
|
+
/** Clamps into `min…max` and snaps to the nearest multiple of `step` minutes. */
|
|
18
|
+
export declare function clampTime(minutes: number, min?: string, max?: string, step?: number): number;
|
|
19
|
+
/**
|
|
20
|
+
* Moves by whole steps. Stops at the bounds instead of wrapping — a stepper
|
|
21
|
+
* that rolls 23:55 round to 00:00 turns a late meeting into an early one.
|
|
22
|
+
*/
|
|
23
|
+
export declare function stepTime(value: string, direction: 1 | -1, step?: number, min?: string, max?: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* `13:05` as the locale writes it — `1:05 PM` in en-US, `13.05` in id-ID.
|
|
26
|
+
*
|
|
27
|
+
* Display only. The bound value stays `HH:MM` whatever this returns, because a
|
|
28
|
+
* localised string is not something a form can post.
|
|
29
|
+
*/
|
|
30
|
+
export declare function formatClock(value: string, locale: string, hour12?: boolean): string;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clock times as `HH:MM` strings — the shape `<input type="time">` uses, so a
|
|
3
|
+
* form posts exactly what the browser would have posted.
|
|
4
|
+
*
|
|
5
|
+
* Everything here works in minutes since midnight. A day is 1440 of them, and
|
|
6
|
+
* nothing in this module crosses that boundary: stepping past 23:59 stops
|
|
7
|
+
* there rather than wrapping into a day this module cannot name.
|
|
8
|
+
*/
|
|
9
|
+
export const MINUTES_IN_DAY = 1440;
|
|
10
|
+
/**
|
|
11
|
+
* Reads `9:5`, `09:05`, `0905` or `09:05:30` into minutes since midnight.
|
|
12
|
+
* Returns `null` for anything that is not a time, including 24:00 and 12:60.
|
|
13
|
+
*/
|
|
14
|
+
export function parseTime(text) {
|
|
15
|
+
const trimmed = text.trim();
|
|
16
|
+
// Digits and colons only: `9a` and `later` are refusals, not a hint that
|
|
17
|
+
// someone meant 09:00. Without this a bare `Number('')` reads as midnight.
|
|
18
|
+
if (!trimmed || !/^[0-9:]+$/.test(trimmed))
|
|
19
|
+
return null;
|
|
20
|
+
const digits = trimmed.replace(/[^0-9]/g, '');
|
|
21
|
+
if (!digits)
|
|
22
|
+
return null;
|
|
23
|
+
let hours;
|
|
24
|
+
let minutes;
|
|
25
|
+
if (trimmed.includes(':')) {
|
|
26
|
+
const [rawHours, rawMinutes = '0'] = trimmed.split(':');
|
|
27
|
+
hours = Number(rawHours);
|
|
28
|
+
minutes = Number(rawMinutes);
|
|
29
|
+
}
|
|
30
|
+
else if (digits.length <= 2) {
|
|
31
|
+
// A bare number is an hour: `9` means 09:00.
|
|
32
|
+
hours = Number(digits);
|
|
33
|
+
minutes = 0;
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
hours = Number(digits.slice(0, digits.length - 2));
|
|
37
|
+
minutes = Number(digits.slice(-2));
|
|
38
|
+
}
|
|
39
|
+
if (!Number.isInteger(hours) || !Number.isInteger(minutes))
|
|
40
|
+
return null;
|
|
41
|
+
if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59)
|
|
42
|
+
return null;
|
|
43
|
+
return hours * 60 + minutes;
|
|
44
|
+
}
|
|
45
|
+
/** Minutes since midnight back to `HH:MM`, always four digits and a colon. */
|
|
46
|
+
export function formatTime(minutes) {
|
|
47
|
+
const total = Math.max(0, Math.min(MINUTES_IN_DAY - 1, Math.round(minutes)));
|
|
48
|
+
const hours = Math.floor(total / 60);
|
|
49
|
+
return `${String(hours).padStart(2, '0')}:${String(total % 60).padStart(2, '0')}`;
|
|
50
|
+
}
|
|
51
|
+
/** Clamps into `min…max` and snaps to the nearest multiple of `step` minutes. */
|
|
52
|
+
export function clampTime(minutes, min, max, step = 1) {
|
|
53
|
+
const floor = min ? (parseTime(min) ?? 0) : 0;
|
|
54
|
+
const ceiling = max ? (parseTime(max) ?? MINUTES_IN_DAY - 1) : MINUTES_IN_DAY - 1;
|
|
55
|
+
const snapped = step > 1 ? Math.round(minutes / step) * step : minutes;
|
|
56
|
+
return Math.max(floor, Math.min(ceiling, snapped));
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Moves by whole steps. Stops at the bounds instead of wrapping — a stepper
|
|
60
|
+
* that rolls 23:55 round to 00:00 turns a late meeting into an early one.
|
|
61
|
+
*/
|
|
62
|
+
export function stepTime(value, direction, step = 1, min, max) {
|
|
63
|
+
const current = parseTime(value) ?? clampTime(0, min, max, step);
|
|
64
|
+
return formatTime(clampTime(current + direction * step, min, max, step));
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* `13:05` as the locale writes it — `1:05 PM` in en-US, `13.05` in id-ID.
|
|
68
|
+
*
|
|
69
|
+
* Display only. The bound value stays `HH:MM` whatever this returns, because a
|
|
70
|
+
* localised string is not something a form can post.
|
|
71
|
+
*/
|
|
72
|
+
export function formatClock(value, locale, hour12) {
|
|
73
|
+
const minutes = parseTime(value);
|
|
74
|
+
if (minutes === null)
|
|
75
|
+
return '';
|
|
76
|
+
const date = new Date(2000, 0, 1, Math.floor(minutes / 60), minutes % 60);
|
|
77
|
+
return new Intl.DateTimeFormat(locale, { hour: '2-digit', minute: '2-digit', hour12 }).format(date);
|
|
78
|
+
}
|
|
@@ -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 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"
|
|
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-2">
|
|
28
25
|
{#each items as item, i (item.label)}
|
|
29
26
|
<li class="flex items-center gap-2">
|
|
@@ -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 { focusRing, toneFill, toneRing, toneText, type Tone } from '../../core/tones.js';
|
|
16
15
|
|
|
17
16
|
interface Props {
|
|
@@ -46,8 +45,6 @@
|
|
|
46
45
|
class: className = ''
|
|
47
46
|
}: Props = $props();
|
|
48
47
|
|
|
49
|
-
const t = useLocale();
|
|
50
|
-
|
|
51
48
|
const selected = $derived(fromISO(value));
|
|
52
49
|
const now = today();
|
|
53
50
|
|
|
@@ -64,7 +61,7 @@
|
|
|
64
61
|
});
|
|
65
62
|
|
|
66
63
|
const days = $derived(monthGrid(view, weekStart));
|
|
67
|
-
const weekdays = $derived(weekdayNames(
|
|
64
|
+
const weekdays = $derived(weekdayNames(undefined, weekStart));
|
|
68
65
|
|
|
69
66
|
function blocked(date: Date) {
|
|
70
67
|
return isOutOfRange(date, min, max) || (isDisabled?.(date) ?? false);
|
|
@@ -109,7 +106,7 @@
|
|
|
109
106
|
<button
|
|
110
107
|
type="button"
|
|
111
108
|
onclick={() => show(addMonths(view, -1))}
|
|
112
|
-
aria-label=
|
|
109
|
+
aria-label="Previous month"
|
|
113
110
|
class="inline-flex size-8 items-center justify-center text-text-secondary transition-colors duration-150 hover:bg-bg-inset hover:text-text {focusRing} {toneRing[
|
|
114
111
|
tone
|
|
115
112
|
]}"
|
|
@@ -120,13 +117,13 @@
|
|
|
120
117
|
</button>
|
|
121
118
|
|
|
122
119
|
<span aria-live="polite" class="text-sm font-medium text-text">
|
|
123
|
-
{monthLabel(view
|
|
120
|
+
{monthLabel(view)}
|
|
124
121
|
</span>
|
|
125
122
|
|
|
126
123
|
<button
|
|
127
124
|
type="button"
|
|
128
125
|
onclick={() => show(addMonths(view, 1))}
|
|
129
|
-
aria-label=
|
|
126
|
+
aria-label="Next month"
|
|
130
127
|
class="inline-flex size-8 items-center justify-center text-text-secondary transition-colors duration-150 hover:bg-bg-inset hover:text-text {focusRing} {toneRing[
|
|
131
128
|
tone
|
|
132
129
|
]}"
|
|
@@ -148,7 +145,7 @@
|
|
|
148
145
|
bind:this={grid}
|
|
149
146
|
role="grid"
|
|
150
147
|
tabindex="-1"
|
|
151
|
-
aria-label=
|
|
148
|
+
aria-label="Select a date"
|
|
152
149
|
onkeydown={onKeydown}
|
|
153
150
|
class="grid grid-cols-7 gap-px focus:outline-none"
|
|
154
151
|
>
|
|
@@ -190,7 +187,7 @@
|
|
|
190
187
|
tone
|
|
191
188
|
]} hover:underline hover:underline-offset-4 {focusRing} {toneRing[tone]}"
|
|
192
189
|
>
|
|
193
|
-
|
|
190
|
+
Today
|
|
194
191
|
</button>
|
|
195
192
|
{/if}
|
|
196
193
|
</div>
|
|
@@ -0,0 +1,107 @@
|
|
|
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]:font-mono [&_input]:uppercase"
|
|
73
|
+
{...rest}
|
|
74
|
+
>
|
|
75
|
+
{#snippet prefix()}
|
|
76
|
+
<span class="relative grid size-5 place-items-center border border-hairline">
|
|
77
|
+
<span class="absolute inset-0" style="background-color: {resolved}"></span>
|
|
78
|
+
<input
|
|
79
|
+
type="color"
|
|
80
|
+
value={resolved}
|
|
81
|
+
{disabled}
|
|
82
|
+
aria-label="Pick a colour"
|
|
83
|
+
oninput={(event) => choose(event.currentTarget.value)}
|
|
84
|
+
class="absolute inset-0 cursor-pointer opacity-0 disabled:cursor-not-allowed"
|
|
85
|
+
/>
|
|
86
|
+
</span>
|
|
87
|
+
{/snippet}
|
|
88
|
+
</Input>
|
|
89
|
+
|
|
90
|
+
{#if swatches.length}
|
|
91
|
+
<div class="flex flex-wrap gap-1.5">
|
|
92
|
+
{#each swatches as swatch (swatch)}
|
|
93
|
+
<button
|
|
94
|
+
type="button"
|
|
95
|
+
{disabled}
|
|
96
|
+
onclick={() => choose(swatch)}
|
|
97
|
+
title={swatch}
|
|
98
|
+
aria-label={swatch}
|
|
99
|
+
aria-pressed={resolved === normaliseHex(swatch)}
|
|
100
|
+
class="size-6 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
|
|
101
|
+
{resolved === normaliseHex(swatch) ? 'border-text' : 'border-hairline'}"
|
|
102
|
+
style="background-color: {swatch}"
|
|
103
|
+
></button>
|
|
104
|
+
{/each}
|
|
105
|
+
</div>
|
|
106
|
+
{/if}
|
|
107
|
+
</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 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"
|
|
184
181
|
>
|
|
185
182
|
<svg class="size-3.5" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
|
@@ -212,7 +209,7 @@
|
|
|
212
209
|
>
|
|
213
210
|
{#if flat.length === 0}
|
|
214
211
|
<p class="px-3 py-6 text-center text-sm text-text-muted">
|
|
215
|
-
{emptyText ??
|
|
212
|
+
{emptyText ?? 'No matches'}
|
|
216
213
|
</p>
|
|
217
214
|
{:else}
|
|
218
215
|
{#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-11 w-full min-w-0 bg-transparent font-sans text-[15px] 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-3.5 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="ghost" 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-44 flex-col overflow-y-auto border border-hairline bg-bg py-1 font-sans {menuClass}"
|
|
85
82
|
>
|