@nqmcreative/ui 0.2.3 → 0.4.0
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 +209 -15
- package/cli/index.mjs +328 -112
- package/dist/core/color.d.ts +24 -0
- package/dist/core/color.js +39 -0
- package/dist/core/index.d.ts +8 -1
- package/dist/core/index.js +5 -1
- package/dist/core/locale.svelte.d.ts +8 -0
- package/dist/core/locale.svelte.js +14 -0
- package/dist/core/number.d.ts +22 -0
- package/dist/core/number.js +33 -0
- 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/core/toast.svelte.d.ts +3 -0
- package/dist/styles/matte/AccordionItem.svelte +5 -0
- package/dist/styles/matte/AccordionItem.svelte.d.ts +2 -0
- package/dist/styles/matte/Avatar.svelte +15 -0
- package/dist/styles/matte/Avatar.svelte.d.ts +3 -0
- package/dist/styles/matte/Badge.svelte +5 -0
- package/dist/styles/matte/Badge.svelte.d.ts +2 -0
- package/dist/styles/matte/Breadcrumb.svelte +11 -4
- package/dist/styles/matte/Breadcrumb.svelte.d.ts +3 -0
- package/dist/styles/matte/Button.svelte +58 -7
- package/dist/styles/matte/Button.svelte.d.ts +14 -2
- package/dist/styles/matte/Card.svelte +6 -1
- package/dist/styles/matte/Card.svelte.d.ts +2 -0
- package/dist/styles/matte/CheckboxGroup.svelte +6 -0
- package/dist/styles/matte/CheckboxGroup.svelte.d.ts +2 -0
- package/dist/styles/matte/ColorInput.svelte +110 -0
- package/dist/styles/matte/ColorInput.svelte.d.ts +16 -0
- package/dist/styles/matte/Combobox.svelte +5 -0
- package/dist/styles/matte/Combobox.svelte.d.ts +3 -0
- package/dist/styles/matte/CommandPalette.svelte +6 -0
- package/dist/styles/matte/CommandPalette.svelte.d.ts +3 -0
- package/dist/styles/matte/CurrencyInput.svelte +108 -0
- package/dist/styles/matte/CurrencyInput.svelte.d.ts +26 -0
- package/dist/styles/matte/Drawer.svelte +5 -0
- package/dist/styles/matte/Drawer.svelte.d.ts +2 -0
- package/dist/styles/matte/FileInput.svelte +194 -0
- package/dist/styles/matte/FileInput.svelte.d.ts +32 -0
- package/dist/styles/matte/Footer.svelte +6 -2
- package/dist/styles/matte/Footer.svelte.d.ts +2 -0
- package/dist/styles/matte/InputAddon.svelte +36 -0
- package/dist/styles/matte/InputAddon.svelte.d.ts +13 -0
- package/dist/styles/matte/Link.svelte +9 -0
- package/dist/styles/matte/Link.svelte.d.ts +4 -0
- package/dist/styles/matte/MenuItem.svelte +2 -1
- package/dist/styles/matte/Modal.svelte +5 -0
- package/dist/styles/matte/Modal.svelte.d.ts +2 -0
- package/dist/styles/matte/MultiSelect.svelte +7 -1
- package/dist/styles/matte/MultiSelect.svelte.d.ts +3 -0
- package/dist/styles/matte/Navbar.svelte +18 -4
- package/dist/styles/matte/Navbar.svelte.d.ts +2 -0
- package/dist/styles/matte/PinInput.svelte +137 -0
- package/dist/styles/matte/PinInput.svelte.d.ts +25 -0
- package/dist/styles/matte/PricingCard.svelte +16 -8
- package/dist/styles/matte/PricingCard.svelte.d.ts +2 -0
- package/dist/styles/matte/RadioGroup.svelte +6 -0
- package/dist/styles/matte/RadioGroup.svelte.d.ts +2 -0
- package/dist/styles/matte/SearchInput.svelte +87 -0
- package/dist/styles/matte/SearchInput.svelte.d.ts +24 -0
- package/dist/styles/matte/SegmentedControl.svelte +16 -2
- package/dist/styles/matte/SegmentedControl.svelte.d.ts +5 -0
- package/dist/styles/matte/Sidebar.svelte +2 -1
- package/dist/styles/matte/Stat.svelte +8 -1
- package/dist/styles/matte/Stat.svelte.d.ts +3 -0
- package/dist/styles/matte/Steps.svelte +7 -0
- package/dist/styles/matte/Steps.svelte.d.ts +3 -0
- package/dist/styles/matte/Table.svelte +153 -97
- package/dist/styles/matte/Table.svelte.d.ts +12 -0
- package/dist/styles/matte/Tabs.svelte +5 -0
- package/dist/styles/matte/Tabs.svelte.d.ts +3 -0
- package/dist/styles/matte/TagsInput.svelte +173 -0
- package/dist/styles/matte/TagsInput.svelte.d.ts +29 -0
- package/dist/styles/matte/TimeInput.svelte +129 -0
- package/dist/styles/matte/TimeInput.svelte.d.ts +21 -0
- package/dist/styles/matte/Toaster.svelte +3 -0
- package/dist/styles/matte/icon.d.ts +21 -0
- package/dist/styles/matte/icon.js +21 -0
- package/dist/styles/matte/index.d.ts +8 -2
- package/dist/styles/matte/index.js +8 -2
- package/dist/styles/paper/AccordionItem.svelte +5 -0
- package/dist/styles/paper/AccordionItem.svelte.d.ts +2 -0
- package/dist/styles/paper/Avatar.svelte +15 -0
- package/dist/styles/paper/Avatar.svelte.d.ts +3 -0
- package/dist/styles/paper/Badge.svelte +5 -0
- package/dist/styles/paper/Badge.svelte.d.ts +2 -0
- package/dist/styles/paper/Breadcrumb.svelte +8 -4
- package/dist/styles/paper/Breadcrumb.svelte.d.ts +3 -0
- package/dist/styles/paper/Button.svelte +58 -7
- package/dist/styles/paper/Button.svelte.d.ts +14 -2
- package/dist/styles/paper/Card.svelte +6 -1
- package/dist/styles/paper/Card.svelte.d.ts +2 -0
- package/dist/styles/paper/CheckboxGroup.svelte +6 -0
- package/dist/styles/paper/CheckboxGroup.svelte.d.ts +2 -0
- package/dist/styles/paper/ColorInput.svelte +112 -0
- package/dist/styles/paper/ColorInput.svelte.d.ts +16 -0
- package/dist/styles/paper/Combobox.svelte +5 -0
- package/dist/styles/paper/Combobox.svelte.d.ts +3 -0
- package/dist/styles/paper/CommandPalette.svelte +6 -0
- package/dist/styles/paper/CommandPalette.svelte.d.ts +3 -0
- package/dist/styles/paper/CurrencyInput.svelte +108 -0
- package/dist/styles/paper/CurrencyInput.svelte.d.ts +26 -0
- package/dist/styles/paper/Drawer.svelte +5 -0
- package/dist/styles/paper/Drawer.svelte.d.ts +2 -0
- package/dist/styles/paper/FileInput.svelte +204 -0
- package/dist/styles/paper/FileInput.svelte.d.ts +32 -0
- package/dist/styles/paper/Footer.svelte +6 -2
- package/dist/styles/paper/Footer.svelte.d.ts +2 -0
- package/dist/styles/paper/InputAddon.svelte +36 -0
- package/dist/styles/paper/InputAddon.svelte.d.ts +13 -0
- package/dist/styles/paper/Link.svelte +9 -0
- package/dist/styles/paper/Link.svelte.d.ts +4 -0
- package/dist/styles/paper/MenuItem.svelte +2 -1
- package/dist/styles/paper/Modal.svelte +5 -0
- package/dist/styles/paper/Modal.svelte.d.ts +2 -0
- package/dist/styles/paper/MultiSelect.svelte +7 -1
- package/dist/styles/paper/MultiSelect.svelte.d.ts +3 -0
- package/dist/styles/paper/Navbar.svelte +18 -4
- package/dist/styles/paper/Navbar.svelte.d.ts +2 -0
- package/dist/styles/paper/PinInput.svelte +137 -0
- package/dist/styles/paper/PinInput.svelte.d.ts +25 -0
- package/dist/styles/paper/PricingCard.svelte +23 -15
- package/dist/styles/paper/PricingCard.svelte.d.ts +2 -0
- package/dist/styles/paper/RadioGroup.svelte +6 -0
- package/dist/styles/paper/RadioGroup.svelte.d.ts +2 -0
- package/dist/styles/paper/SearchInput.svelte +92 -0
- package/dist/styles/paper/SearchInput.svelte.d.ts +24 -0
- package/dist/styles/paper/SegmentedControl.svelte +16 -2
- package/dist/styles/paper/SegmentedControl.svelte.d.ts +5 -0
- package/dist/styles/paper/Sidebar.svelte +2 -1
- package/dist/styles/paper/Stat.svelte +8 -1
- package/dist/styles/paper/Stat.svelte.d.ts +3 -0
- package/dist/styles/paper/Steps.svelte +7 -0
- package/dist/styles/paper/Steps.svelte.d.ts +3 -0
- package/dist/styles/paper/Table.svelte +153 -97
- package/dist/styles/paper/Table.svelte.d.ts +12 -0
- package/dist/styles/paper/Tabs.svelte +5 -0
- package/dist/styles/paper/Tabs.svelte.d.ts +3 -0
- package/dist/styles/paper/TagsInput.svelte +178 -0
- package/dist/styles/paper/TagsInput.svelte.d.ts +29 -0
- package/dist/styles/paper/TimeInput.svelte +155 -0
- package/dist/styles/paper/TimeInput.svelte.d.ts +21 -0
- package/dist/styles/paper/Toaster.svelte +3 -0
- package/dist/styles/paper/icon.d.ts +21 -0
- package/dist/styles/paper/icon.js +21 -0
- package/dist/styles/paper/index.d.ts +8 -2
- package/dist/styles/paper/index.js +8 -2
- package/dist/styles/sprout/AccordionItem.svelte +5 -0
- package/dist/styles/sprout/AccordionItem.svelte.d.ts +2 -0
- package/dist/styles/sprout/Avatar.svelte +15 -0
- package/dist/styles/sprout/Avatar.svelte.d.ts +3 -0
- package/dist/styles/sprout/Badge.svelte +5 -0
- package/dist/styles/sprout/Badge.svelte.d.ts +2 -0
- package/dist/styles/sprout/Breadcrumb.svelte +8 -4
- package/dist/styles/sprout/Breadcrumb.svelte.d.ts +3 -0
- package/dist/styles/sprout/Button.svelte +58 -7
- package/dist/styles/sprout/Button.svelte.d.ts +14 -2
- package/dist/styles/sprout/Card.svelte +6 -1
- package/dist/styles/sprout/Card.svelte.d.ts +2 -0
- package/dist/styles/sprout/CheckboxGroup.svelte +6 -0
- package/dist/styles/sprout/CheckboxGroup.svelte.d.ts +2 -0
- package/dist/styles/sprout/ColorInput.svelte +112 -0
- package/dist/styles/sprout/ColorInput.svelte.d.ts +16 -0
- package/dist/styles/sprout/Combobox.svelte +5 -0
- package/dist/styles/sprout/Combobox.svelte.d.ts +3 -0
- package/dist/styles/sprout/CommandPalette.svelte +6 -0
- package/dist/styles/sprout/CommandPalette.svelte.d.ts +3 -0
- package/dist/styles/sprout/CurrencyInput.svelte +108 -0
- package/dist/styles/sprout/CurrencyInput.svelte.d.ts +26 -0
- package/dist/styles/sprout/Drawer.svelte +5 -0
- package/dist/styles/sprout/Drawer.svelte.d.ts +2 -0
- package/dist/styles/sprout/FileInput.svelte +205 -0
- package/dist/styles/sprout/FileInput.svelte.d.ts +32 -0
- package/dist/styles/sprout/Footer.svelte +6 -2
- package/dist/styles/sprout/Footer.svelte.d.ts +2 -0
- package/dist/styles/sprout/InputAddon.svelte +37 -0
- package/dist/styles/sprout/InputAddon.svelte.d.ts +13 -0
- package/dist/styles/sprout/Link.svelte +9 -0
- package/dist/styles/sprout/Link.svelte.d.ts +4 -0
- package/dist/styles/sprout/MenuItem.svelte +2 -1
- package/dist/styles/sprout/Modal.svelte +5 -0
- package/dist/styles/sprout/Modal.svelte.d.ts +2 -0
- package/dist/styles/sprout/MultiSelect.svelte +7 -1
- package/dist/styles/sprout/MultiSelect.svelte.d.ts +3 -0
- package/dist/styles/sprout/Navbar.svelte +18 -4
- package/dist/styles/sprout/Navbar.svelte.d.ts +2 -0
- package/dist/styles/sprout/PinInput.svelte +138 -0
- package/dist/styles/sprout/PinInput.svelte.d.ts +25 -0
- package/dist/styles/sprout/PricingCard.svelte +23 -15
- package/dist/styles/sprout/PricingCard.svelte.d.ts +2 -0
- package/dist/styles/sprout/RadioGroup.svelte +6 -0
- package/dist/styles/sprout/RadioGroup.svelte.d.ts +2 -0
- package/dist/styles/sprout/SearchInput.svelte +92 -0
- package/dist/styles/sprout/SearchInput.svelte.d.ts +24 -0
- package/dist/styles/sprout/SegmentedControl.svelte +16 -2
- package/dist/styles/sprout/SegmentedControl.svelte.d.ts +5 -0
- package/dist/styles/sprout/Sidebar.svelte +2 -1
- package/dist/styles/sprout/Stat.svelte +8 -1
- package/dist/styles/sprout/Stat.svelte.d.ts +3 -0
- package/dist/styles/sprout/Steps.svelte +7 -0
- package/dist/styles/sprout/Steps.svelte.d.ts +3 -0
- package/dist/styles/sprout/Table.svelte +153 -97
- package/dist/styles/sprout/Table.svelte.d.ts +12 -0
- package/dist/styles/sprout/Tabs.svelte +5 -0
- package/dist/styles/sprout/Tabs.svelte.d.ts +3 -0
- package/dist/styles/sprout/TagsInput.svelte +179 -0
- package/dist/styles/sprout/TagsInput.svelte.d.ts +29 -0
- package/dist/styles/sprout/TimeInput.svelte +155 -0
- package/dist/styles/sprout/TimeInput.svelte.d.ts +21 -0
- package/dist/styles/sprout/Toaster.svelte +3 -0
- package/dist/styles/sprout/icon.d.ts +21 -0
- package/dist/styles/sprout/icon.js +21 -0
- package/dist/styles/sprout/index.d.ts +8 -2
- package/dist/styles/sprout/index.js +8 -2
- package/package.json +98 -26
- package/registry.json +143 -35
- package/dist/styles/matte/NumberInput.svelte +0 -96
- package/dist/styles/matte/NumberInput.svelte.d.ts +0 -15
- package/dist/styles/matte/PasswordInput.svelte +0 -95
- package/dist/styles/matte/PasswordInput.svelte.d.ts +0 -12
- package/dist/styles/paper/NumberInput.svelte +0 -96
- package/dist/styles/paper/NumberInput.svelte.d.ts +0 -15
- package/dist/styles/paper/PasswordInput.svelte +0 -100
- package/dist/styles/paper/PasswordInput.svelte.d.ts +0 -12
- package/dist/styles/sprout/NumberInput.svelte +0 -97
- package/dist/styles/sprout/NumberInput.svelte.d.ts +0 -15
- package/dist/styles/sprout/PasswordInput.svelte +0 -101
- package/dist/styles/sprout/PasswordInput.svelte.d.ts +0 -12
|
@@ -52,6 +52,14 @@ export interface Locale {
|
|
|
52
52
|
passwordStrength: [string, string, string, string, string];
|
|
53
53
|
decrease: string;
|
|
54
54
|
increase: string;
|
|
55
|
+
chooseFile: string;
|
|
56
|
+
noFile: string;
|
|
57
|
+
clearSearch: string;
|
|
58
|
+
/** Prefixed to the box number, so box three is labelled `Digit 3`. */
|
|
59
|
+
digit: string;
|
|
60
|
+
addTag: string;
|
|
61
|
+
duplicateTag: string;
|
|
62
|
+
pickColour: string;
|
|
55
63
|
/** BCP 47 tag handed to `Intl` for month and weekday names. */
|
|
56
64
|
dateLocale: string;
|
|
57
65
|
/** Order of the date parts the input accepts and prints. */
|
|
@@ -51,6 +51,13 @@ export const enUS = {
|
|
|
51
51
|
passwordStrength: ['Too short', 'Weak', 'Fair', 'Good', 'Strong'],
|
|
52
52
|
decrease: 'Decrease',
|
|
53
53
|
increase: 'Increase',
|
|
54
|
+
chooseFile: 'Choose file',
|
|
55
|
+
noFile: 'No file chosen',
|
|
56
|
+
clearSearch: 'Clear search',
|
|
57
|
+
digit: 'Digit',
|
|
58
|
+
addTag: 'Add a tag',
|
|
59
|
+
duplicateTag: 'Already added',
|
|
60
|
+
pickColour: 'Pick a colour',
|
|
54
61
|
dateLocale: 'en-US',
|
|
55
62
|
dateFormat: 'mdy',
|
|
56
63
|
today: 'Today',
|
|
@@ -103,6 +110,13 @@ export const idID = {
|
|
|
103
110
|
passwordStrength: ['Terlalu pendek', 'Lemah', 'Cukup', 'Baik', 'Kuat'],
|
|
104
111
|
decrease: 'Kurangi',
|
|
105
112
|
increase: 'Tambah',
|
|
113
|
+
chooseFile: 'Pilih berkas',
|
|
114
|
+
noFile: 'Belum ada berkas',
|
|
115
|
+
clearSearch: 'Hapus pencarian',
|
|
116
|
+
digit: 'Digit',
|
|
117
|
+
addTag: 'Tambah tag',
|
|
118
|
+
duplicateTag: 'Sudah ditambahkan',
|
|
119
|
+
pickColour: 'Pilih warna',
|
|
106
120
|
dateLocale: 'id-ID',
|
|
107
121
|
dateFormat: 'dmy',
|
|
108
122
|
today: 'Hari ini',
|
package/dist/core/number.d.ts
CHANGED
|
@@ -5,3 +5,25 @@ export declare function stepDecimals(step: number): number;
|
|
|
5
5
|
export declare function clampToStep(value: number, min: number, max: number, step: number): number;
|
|
6
6
|
/** Position of `value` within `min…max`, as 0–100. */
|
|
7
7
|
export declare function percentOf(value: number, min: number, max: number): number;
|
|
8
|
+
/**
|
|
9
|
+
* How a grouped number is written. Separators are the app's to choose rather
|
|
10
|
+
* than `Intl`'s, because the field has to *parse* what it prints — and a
|
|
11
|
+
* locale that groups with a non-breaking space produces a character nobody can
|
|
12
|
+
* type back in.
|
|
13
|
+
*/
|
|
14
|
+
export interface GroupOptions {
|
|
15
|
+
/** Thousands separator. */
|
|
16
|
+
group?: string;
|
|
17
|
+
/** Decimal separator. */
|
|
18
|
+
decimal?: string;
|
|
19
|
+
/** Fixed decimal places. `undefined` keeps whatever was typed. */
|
|
20
|
+
precision?: number;
|
|
21
|
+
}
|
|
22
|
+
/** `1234567.5` → `1,234,567.50`, or `1.234.567,50` in a locale that swaps them. */
|
|
23
|
+
export declare function formatGrouped(value: number, options?: GroupOptions): string;
|
|
24
|
+
/**
|
|
25
|
+
* The inverse, and deliberately forgiving: anything that is not a digit, a
|
|
26
|
+
* minus or the decimal separator is dropped, so a pasted `Rp 1.250.000,-`
|
|
27
|
+
* still reads as a number. `null` means there were no digits at all.
|
|
28
|
+
*/
|
|
29
|
+
export declare function parseGrouped(text: string, options?: GroupOptions): number | null;
|
package/dist/core/number.js
CHANGED
|
@@ -15,3 +15,36 @@ export function percentOf(value, min, max) {
|
|
|
15
15
|
return 0;
|
|
16
16
|
return Math.min(100, Math.max(0, ((value - min) / (max - min)) * 100));
|
|
17
17
|
}
|
|
18
|
+
/** `1234567.5` → `1,234,567.50`, or `1.234.567,50` in a locale that swaps them. */
|
|
19
|
+
export function formatGrouped(value, options = {}) {
|
|
20
|
+
const { group = ',', decimal = '.', precision } = options;
|
|
21
|
+
if (!Number.isFinite(value))
|
|
22
|
+
return '';
|
|
23
|
+
const fixed = precision === undefined ? String(value) : value.toFixed(precision);
|
|
24
|
+
const negative = fixed.startsWith('-');
|
|
25
|
+
const [whole, fraction] = (negative ? fixed.slice(1) : fixed).split('.');
|
|
26
|
+
// Replaced through a function so a separator that looks like `$&` stays put.
|
|
27
|
+
const grouped = whole.replace(/\B(?=(\d{3})+(?!\d))/g, () => group);
|
|
28
|
+
return `${negative ? '-' : ''}${grouped}${fraction ? decimal + fraction : ''}`;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The inverse, and deliberately forgiving: anything that is not a digit, a
|
|
32
|
+
* minus or the decimal separator is dropped, so a pasted `Rp 1.250.000,-`
|
|
33
|
+
* still reads as a number. `null` means there were no digits at all.
|
|
34
|
+
*/
|
|
35
|
+
export function parseGrouped(text, options = {}) {
|
|
36
|
+
const { decimal = '.' } = options;
|
|
37
|
+
let cleaned = '';
|
|
38
|
+
for (const char of text) {
|
|
39
|
+
if (char === decimal)
|
|
40
|
+
cleaned += '.';
|
|
41
|
+
else if (char >= '0' && char <= '9')
|
|
42
|
+
cleaned += char;
|
|
43
|
+
else if (char === '-' && cleaned === '')
|
|
44
|
+
cleaned += char;
|
|
45
|
+
}
|
|
46
|
+
if (!cleaned || cleaned === '-' || cleaned === '.')
|
|
47
|
+
return null;
|
|
48
|
+
const value = Number(cleaned);
|
|
49
|
+
return Number.isFinite(value) ? value : null;
|
|
50
|
+
}
|
|
@@ -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,6 +8,7 @@
|
|
|
8
8
|
* const id = toast.loading('Rendering…');
|
|
9
9
|
* toast.dismiss(id);
|
|
10
10
|
*/
|
|
11
|
+
import type { Snippet } from 'svelte';
|
|
11
12
|
import type { Tone } from './tones.js';
|
|
12
13
|
export interface ToastAction {
|
|
13
14
|
label: string;
|
|
@@ -19,6 +20,8 @@ export interface ToastOptions {
|
|
|
19
20
|
/** Milliseconds on screen. `0` keeps it until dismissed. Default 5000. */
|
|
20
21
|
duration?: number;
|
|
21
22
|
action?: ToastAction;
|
|
23
|
+
/** Replaces the tone's own glyph. */
|
|
24
|
+
icon?: Snippet;
|
|
22
25
|
/** Swaps the glyph for a spinner and defaults `duration` to 0. */
|
|
23
26
|
loading?: boolean;
|
|
24
27
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
4
|
import { toneText, type Tone } from '../../core/tones.js';
|
|
5
|
+
import { iconMd } from './icon.js';
|
|
5
6
|
|
|
6
7
|
interface Props extends HTMLAttributes<HTMLDetailsElement> {
|
|
7
8
|
title: string;
|
|
@@ -9,6 +10,8 @@
|
|
|
9
10
|
tone?: Tone;
|
|
10
11
|
/** Short text on the right of the summary row. */
|
|
11
12
|
meta?: string;
|
|
13
|
+
/** Leading icon on the summary row. */
|
|
14
|
+
icon?: Snippet;
|
|
12
15
|
children: Snippet;
|
|
13
16
|
}
|
|
14
17
|
|
|
@@ -17,6 +20,7 @@
|
|
|
17
20
|
open = $bindable(false),
|
|
18
21
|
tone = 'brand',
|
|
19
22
|
meta,
|
|
23
|
+
icon,
|
|
20
24
|
class: className = '',
|
|
21
25
|
children,
|
|
22
26
|
...rest
|
|
@@ -27,6 +31,7 @@
|
|
|
27
31
|
<summary
|
|
28
32
|
class="flex cursor-pointer list-none items-center gap-4 py-4 font-sans text-[15px] text-text transition-colors duration-150 ease-brand-out select-none hover:text-text-secondary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current [&::-webkit-details-marker]:hidden"
|
|
29
33
|
>
|
|
34
|
+
{#if icon}<span class={iconMd}>{@render icon()}</span>{/if}
|
|
30
35
|
<span class="flex-1 font-medium">{title}</span>
|
|
31
36
|
{#if meta}<span class="font-mono text-xs text-text-muted">{meta}</span>{/if}
|
|
32
37
|
<svg
|
|
@@ -7,6 +7,8 @@ interface Props extends HTMLAttributes<HTMLDetailsElement> {
|
|
|
7
7
|
tone?: Tone;
|
|
8
8
|
/** Short text on the right of the summary row. */
|
|
9
9
|
meta?: string;
|
|
10
|
+
/** Leading icon on the summary row. */
|
|
11
|
+
icon?: Snippet;
|
|
10
12
|
children: Snippet;
|
|
11
13
|
}
|
|
12
14
|
declare const AccordionItem: import("svelte").Component<Props, {}, "open">;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
2
3
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
4
|
import { toneSoft, type Tone } from '../../core/tones.js';
|
|
4
5
|
|
|
@@ -13,6 +14,8 @@
|
|
|
13
14
|
tone?: Tone;
|
|
14
15
|
/** Square instead of the default circle. */
|
|
15
16
|
squared?: boolean;
|
|
17
|
+
/** Drawn instead of the initials when there is no `src` — a user mark. */
|
|
18
|
+
fallback?: Snippet;
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
let {
|
|
@@ -22,6 +25,7 @@
|
|
|
22
25
|
size = 'md',
|
|
23
26
|
tone = 'brand',
|
|
24
27
|
squared = false,
|
|
28
|
+
fallback,
|
|
25
29
|
class: className = '',
|
|
26
30
|
...rest
|
|
27
31
|
}: Props = $props();
|
|
@@ -34,6 +38,15 @@
|
|
|
34
38
|
xl: 'size-16 text-lg'
|
|
35
39
|
};
|
|
36
40
|
|
|
41
|
+
// The mark tracks the initials: about half the circle, whatever the size.
|
|
42
|
+
const glyphs: Record<AvatarSize, string> = {
|
|
43
|
+
xs: 'grid place-items-center *:size-3',
|
|
44
|
+
sm: 'grid place-items-center *:size-4',
|
|
45
|
+
md: 'grid place-items-center *:size-5',
|
|
46
|
+
lg: 'grid place-items-center *:size-6',
|
|
47
|
+
xl: 'grid place-items-center *:size-8'
|
|
48
|
+
};
|
|
49
|
+
|
|
37
50
|
const initials = $derived(
|
|
38
51
|
name
|
|
39
52
|
.trim()
|
|
@@ -52,6 +65,8 @@
|
|
|
52
65
|
>
|
|
53
66
|
{#if src}
|
|
54
67
|
<img {src} alt={alt ?? name} class="size-full object-cover" />
|
|
68
|
+
{:else if fallback}
|
|
69
|
+
<span class={glyphs[size]}>{@render fallback()}</span>
|
|
55
70
|
{:else}
|
|
56
71
|
{initials}
|
|
57
72
|
{/if}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
1
2
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
3
|
import { type Tone } from '../../core/tones.js';
|
|
3
4
|
export type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
@@ -10,6 +11,8 @@ interface Props extends HTMLAttributes<HTMLSpanElement> {
|
|
|
10
11
|
tone?: Tone;
|
|
11
12
|
/** Square instead of the default circle. */
|
|
12
13
|
squared?: boolean;
|
|
14
|
+
/** Drawn instead of the initials when there is no `src` — a user mark. */
|
|
15
|
+
fallback?: Snippet;
|
|
13
16
|
}
|
|
14
17
|
declare const Avatar: import("svelte").Component<Props, {}, "">;
|
|
15
18
|
type Avatar = ReturnType<typeof Avatar>;
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
toneText,
|
|
10
10
|
type Tone
|
|
11
11
|
} from '../../core/tones.js';
|
|
12
|
+
import { iconSm } from './icon.js';
|
|
12
13
|
|
|
13
14
|
export type BadgeVariant = 'soft' | 'solid' | 'outline';
|
|
14
15
|
export type BadgeSize = 'sm' | 'md';
|
|
@@ -19,6 +20,8 @@
|
|
|
19
20
|
size?: BadgeSize;
|
|
20
21
|
/** Small filled circle before the label. */
|
|
21
22
|
dot?: boolean;
|
|
23
|
+
/** Leading icon, 14px — sits where the dot would. */
|
|
24
|
+
icon?: Snippet;
|
|
22
25
|
children: Snippet;
|
|
23
26
|
}
|
|
24
27
|
|
|
@@ -27,6 +30,7 @@
|
|
|
27
30
|
variant = 'soft',
|
|
28
31
|
size = 'md',
|
|
29
32
|
dot = false,
|
|
33
|
+
icon,
|
|
30
34
|
class: className = '',
|
|
31
35
|
children,
|
|
32
36
|
...rest
|
|
@@ -54,5 +58,6 @@
|
|
|
54
58
|
class="size-1.5 shrink-0 rounded-full {variant === 'solid' ? 'bg-current' : toneFill[tone]}"
|
|
55
59
|
></span>
|
|
56
60
|
{/if}
|
|
61
|
+
{#if icon}<span class={iconSm}>{@render icon()}</span>{/if}
|
|
57
62
|
{@render children()}
|
|
58
63
|
</span>
|
|
@@ -9,6 +9,8 @@ interface Props extends HTMLAttributes<HTMLSpanElement> {
|
|
|
9
9
|
size?: BadgeSize;
|
|
10
10
|
/** Small filled circle before the label. */
|
|
11
11
|
dot?: boolean;
|
|
12
|
+
/** Leading icon, 14px — sits where the dot would. */
|
|
13
|
+
icon?: Snippet;
|
|
12
14
|
children: Snippet;
|
|
13
15
|
}
|
|
14
16
|
declare const Badge: import("svelte").Component<Props, {}, "">;
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
2
3
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
4
|
import { toneHoverText, type Tone } from '../../core/tones.js';
|
|
4
5
|
import { useLocale } from '../../core/locale.svelte.js';
|
|
6
|
+
import { iconMd } from './icon.js';
|
|
5
7
|
|
|
6
8
|
export interface BreadcrumbItem {
|
|
7
9
|
label: string;
|
|
8
10
|
href?: string;
|
|
11
|
+
/** Leading icon — a home mark on the first crumb, say. */
|
|
12
|
+
icon?: Snippet;
|
|
9
13
|
}
|
|
10
14
|
|
|
11
15
|
interface Props extends HTMLAttributes<HTMLElement> {
|
|
@@ -29,15 +33,18 @@
|
|
|
29
33
|
{#if item.href && i < items.length - 1}
|
|
30
34
|
<a
|
|
31
35
|
href={item.href}
|
|
32
|
-
class="text-text-secondary transition-colors duration-150 ease-brand-out {toneHoverText[
|
|
36
|
+
class="inline-flex items-center gap-1.5 text-text-secondary transition-colors duration-150 ease-brand-out {toneHoverText[
|
|
33
37
|
tone
|
|
34
38
|
]}"
|
|
35
39
|
>
|
|
36
|
-
{item.label}
|
|
40
|
+
{#if item.icon}<span class={iconMd}>{@render item.icon()}</span>{/if}{item.label}
|
|
37
41
|
</a>
|
|
38
42
|
{:else}
|
|
39
|
-
<span
|
|
40
|
-
|
|
43
|
+
<span
|
|
44
|
+
class="inline-flex items-center gap-1.5 text-text"
|
|
45
|
+
aria-current={i === items.length - 1 ? 'page' : undefined}
|
|
46
|
+
>
|
|
47
|
+
{#if item.icon}<span class={iconMd}>{@render item.icon()}</span>{/if}{item.label}
|
|
41
48
|
</span>
|
|
42
49
|
{/if}
|
|
43
50
|
</li>
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
1
2
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
3
|
import { type Tone } from '../../core/tones.js';
|
|
3
4
|
export interface BreadcrumbItem {
|
|
4
5
|
label: string;
|
|
5
6
|
href?: string;
|
|
7
|
+
/** Leading icon — a home mark on the first crumb, say. */
|
|
8
|
+
icon?: Snippet;
|
|
6
9
|
}
|
|
7
10
|
interface Props extends HTMLAttributes<HTMLElement> {
|
|
8
11
|
items: BreadcrumbItem[];
|