@thomasfosterau/components 0.1.0 → 0.2.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 +241 -56
- package/dist/canvas/VectorCanvas.svelte +89 -8
- package/dist/canvas/VectorCanvas.svelte.d.ts +19 -1
- package/dist/canvas/types.d.ts +1 -1
- package/dist/charts/types.d.ts +1 -1
- package/dist/citations/list-format.js +3 -1
- package/dist/data/ChatBubble.svelte +48 -0
- package/dist/data/ChatBubble.svelte.d.ts +33 -0
- package/dist/data/DescriptionList.svelte +9 -3
- package/dist/data/DescriptionList.svelte.d.ts +1 -1
- package/dist/data/DescriptionRow.svelte +6 -5
- package/dist/data/MessageThread.svelte +29 -0
- package/dist/data/MessageThread.svelte.d.ts +17 -0
- package/dist/data/Pagination.svelte +5 -5
- package/dist/data/Pagination.svelte.d.ts +8 -0
- package/dist/data/PaginationFooter.svelte +2 -1
- package/dist/data/PaginationFooter.svelte.d.ts +2 -0
- package/dist/data/Table.svelte +9 -3
- package/dist/data/description-context.d.ts +12 -6
- package/dist/data/description-context.js +6 -5
- package/dist/data/index.d.ts +2 -0
- package/dist/data/index.js +2 -0
- package/dist/data/table-types.d.ts +7 -0
- package/dist/feedback/Dialog.svelte +19 -49
- package/dist/feedback/Dialog.svelte.d.ts +0 -1
- package/dist/feedback/DialogFrame.svelte +64 -0
- package/dist/feedback/DialogFrame.svelte.d.ts +23 -0
- package/dist/feedback/Drawer.svelte +43 -0
- package/dist/feedback/Drawer.svelte.d.ts +43 -0
- package/dist/feedback/SegmentBar.svelte.d.ts +1 -0
- package/dist/feedback/StatTile.svelte.d.ts +1 -0
- package/dist/feedback/Steps.svelte +72 -0
- package/dist/feedback/Steps.svelte.d.ts +61 -0
- package/dist/feedback/Timeline.svelte.d.ts +3 -0
- package/dist/feedback/index.d.ts +3 -0
- package/dist/feedback/index.js +2 -0
- package/dist/feedback/sizes.d.ts +14 -0
- package/dist/feedback/sizes.js +28 -0
- package/dist/fields/Calendar.svelte +60 -0
- package/dist/fields/Calendar.svelte.d.ts +11 -0
- package/dist/fields/CalendarMonths.svelte +86 -0
- package/dist/fields/CalendarMonths.svelte.d.ts +30 -0
- package/dist/fields/Checkbox.svelte +1 -1
- package/dist/fields/Checkbox.svelte.d.ts +6 -0
- package/dist/fields/Combobox.svelte +61 -54
- package/dist/fields/Combobox.svelte.d.ts +38 -3
- package/dist/fields/DateField.svelte +37 -0
- package/dist/fields/DateField.svelte.d.ts +20 -0
- package/dist/fields/DateInput.svelte +1 -1
- package/dist/fields/DateInput.svelte.d.ts +6 -0
- package/dist/fields/DatePicker.svelte +47 -0
- package/dist/fields/DatePicker.svelte.d.ts +25 -0
- package/dist/fields/DateRangePicker.svelte +70 -0
- package/dist/fields/DateRangePicker.svelte.d.ts +43 -0
- package/dist/fields/Dropzone.svelte +232 -0
- package/dist/fields/Dropzone.svelte.d.ts +103 -0
- package/dist/fields/Fieldset.svelte +7 -2
- package/dist/fields/Fieldset.svelte.d.ts +3 -2
- package/dist/fields/FileInput.svelte +3 -2
- package/dist/fields/FileInput.svelte.d.ts +6 -0
- package/dist/fields/FormField.svelte +27 -3
- package/dist/fields/ImageInput.svelte.d.ts +6 -0
- package/dist/fields/MultiSelect.svelte +112 -0
- package/dist/fields/MultiSelect.svelte.d.ts +75 -0
- package/dist/fields/NumberField.svelte +190 -0
- package/dist/fields/NumberField.svelte.d.ts +76 -0
- package/dist/fields/PasswordInput.svelte +95 -0
- package/dist/fields/PasswordInput.svelte.d.ts +58 -0
- package/dist/fields/PickerPopover.svelte +47 -0
- package/dist/fields/PickerPopover.svelte.d.ts +16 -0
- package/dist/fields/PinInput.svelte +83 -0
- package/dist/fields/PinInput.svelte.d.ts +66 -0
- package/dist/fields/RadioGroup.svelte +3 -0
- package/dist/fields/RangeCalendar.svelte +51 -0
- package/dist/fields/RangeCalendar.svelte.d.ts +23 -0
- package/dist/fields/Rating.svelte +127 -0
- package/dist/fields/Rating.svelte.d.ts +86 -0
- package/dist/fields/SearchInput.svelte +2 -1
- package/dist/fields/SegmentGroup.svelte +91 -0
- package/dist/fields/SegmentGroup.svelte.d.ts +20 -0
- package/dist/fields/Select.svelte +1 -1
- package/dist/fields/Select.svelte.d.ts +6 -0
- package/dist/fields/Slider.svelte +194 -0
- package/dist/fields/Slider.svelte.d.ts +100 -0
- package/dist/fields/TagsInput.svelte +256 -0
- package/dist/fields/TagsInput.svelte.d.ts +102 -0
- package/dist/fields/TextInput.svelte +1 -1
- package/dist/fields/TextInput.svelte.d.ts +6 -0
- package/dist/fields/Textarea.svelte +1 -1
- package/dist/fields/Textarea.svelte.d.ts +6 -0
- package/dist/fields/TimeField.svelte +30 -0
- package/dist/fields/TimeField.svelte.d.ts +19 -0
- package/dist/fields/Toggle.svelte +1 -1
- package/dist/fields/Toggle.svelte.d.ts +6 -0
- package/dist/fields/VideoInput.svelte.d.ts +6 -0
- package/dist/fields/calendar-props.d.ts +119 -0
- package/dist/fields/calendar-props.js +51 -0
- package/dist/fields/dates.d.ts +28 -0
- package/dist/fields/dates.js +26 -0
- package/dist/fields/field-types.d.ts +29 -0
- package/dist/fields/field-types.js +32 -0
- package/dist/fields/file-validation.d.ts +30 -0
- package/dist/fields/file-validation.js +51 -0
- package/dist/fields/index.d.ts +9 -0
- package/dist/fields/index.js +8 -0
- package/dist/fields/latest-search.svelte.d.ts +38 -0
- package/dist/fields/latest-search.svelte.js +85 -0
- package/dist/fields/number-format.d.ts +52 -0
- package/dist/fields/number-format.js +89 -0
- package/dist/fields/segmented-field.d.ts +128 -0
- package/dist/fields/segmented-field.js +52 -0
- package/dist/fields/slider-keys.d.ts +21 -0
- package/dist/fields/slider-keys.js +37 -0
- package/dist/formatting/Countdown.svelte +110 -0
- package/dist/formatting/Countdown.svelte.d.ts +35 -0
- package/dist/formatting/Duration.svelte +18 -0
- package/dist/formatting/Duration.svelte.d.ts +20 -0
- package/dist/formatting/Highlight.svelte +4 -0
- package/dist/formatting/Highlight.svelte.d.ts +4 -0
- package/dist/formatting/duration.d.ts +44 -0
- package/dist/formatting/duration.js +125 -0
- package/dist/formatting/index.d.ts +3 -0
- package/dist/formatting/index.js +3 -0
- package/dist/forms/EntityForm.svelte +53 -45
- package/dist/forms/EntityForm.svelte.d.ts +12 -3
- package/dist/hotkeys/index.d.ts +5 -0
- package/dist/hotkeys/index.js +5 -0
- package/dist/hotkeys/match.js +12 -10
- package/dist/hotkeys/types.d.ts +4 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/infobox/Infobox.svelte +2 -2
- package/dist/infobox/Infobox.svelte.d.ts +7 -0
- package/dist/infobox/InfoboxHeader.svelte +15 -7
- package/dist/infobox/InfoboxHeader.svelte.d.ts +7 -0
- package/dist/infobox/types.d.ts +5 -5
- package/dist/layout/AppShell.svelte +50 -0
- package/dist/layout/AppShell.svelte.d.ts +28 -0
- package/dist/layout/Cluster.svelte +28 -0
- package/dist/layout/Cluster.svelte.d.ts +27 -0
- package/dist/layout/Fits.svelte +27 -0
- package/dist/layout/Fits.svelte.d.ts +13 -0
- package/dist/layout/Flow.svelte +46 -0
- package/dist/layout/Flow.svelte.d.ts +16 -0
- package/dist/layout/Grid.svelte +23 -0
- package/dist/layout/Grid.svelte.d.ts +26 -0
- package/dist/layout/HStack.svelte +12 -0
- package/dist/layout/HStack.svelte.d.ts +34 -0
- package/dist/layout/Hero.svelte +76 -0
- package/dist/layout/Hero.svelte.d.ts +36 -0
- package/dist/layout/Navbar.svelte +2 -2
- package/dist/layout/Sidebar.svelte +1 -1
- package/dist/layout/Spacer.svelte +12 -0
- package/dist/layout/Spacer.svelte.d.ts +6 -0
- package/dist/layout/VStack.svelte +12 -0
- package/dist/layout/VStack.svelte.d.ts +29 -0
- package/dist/layout/index.d.ts +9 -0
- package/dist/layout/index.js +9 -0
- package/dist/layout/space.d.ts +44 -0
- package/dist/layout/space.js +76 -0
- package/dist/search/types.d.ts +3 -0
- package/dist/seo/og-image.d.ts +4 -3
- package/dist/seo/og-image.js +4 -3
- package/dist/tips/tip-stack.d.ts +1 -0
- package/dist/tips/tip-store.svelte.d.ts +2 -1
- package/dist/ui/Accordion.svelte +75 -0
- package/dist/ui/Accordion.svelte.d.ts +50 -0
- package/dist/ui/AccordionItem.svelte +73 -0
- package/dist/ui/AccordionItem.svelte.d.ts +18 -0
- package/dist/ui/Button.svelte +3 -2
- package/dist/ui/ButtonGroup.svelte +24 -0
- package/dist/ui/ButtonGroup.svelte.d.ts +17 -0
- package/dist/ui/ContextMenu.svelte +75 -0
- package/dist/ui/ContextMenu.svelte.d.ts +25 -0
- package/dist/ui/Disclosure.svelte +7 -0
- package/dist/ui/Disclosure.svelte.d.ts +2 -2
- package/dist/ui/Divider.svelte +47 -0
- package/dist/ui/Divider.svelte.d.ts +28 -0
- package/dist/ui/DropdownMenu.svelte +44 -0
- package/dist/ui/DropdownMenu.svelte.d.ts +30 -0
- package/dist/ui/Menu.svelte +17 -47
- package/dist/ui/Menu.svelte.d.ts +3 -24
- package/dist/ui/MenuCheckboxItem.svelte +33 -0
- package/dist/ui/MenuCheckboxItem.svelte.d.ts +20 -0
- package/dist/ui/MenuItem.svelte +29 -31
- package/dist/ui/MenuItem.svelte.d.ts +2 -2
- package/dist/ui/MenuRadioGroup.svelte +25 -0
- package/dist/ui/MenuRadioGroup.svelte.d.ts +20 -0
- package/dist/ui/MenuRadioItem.svelte +20 -0
- package/dist/ui/MenuRadioItem.svelte.d.ts +15 -0
- package/dist/ui/MenuSeparator.svelte +12 -11
- package/dist/ui/MenuSub.svelte +24 -0
- package/dist/ui/MenuSub.svelte.d.ts +15 -0
- package/dist/ui/Spinner.svelte +41 -0
- package/dist/ui/Spinner.svelte.d.ts +35 -0
- package/dist/ui/Stack.svelte +36 -0
- package/dist/ui/Stack.svelte.d.ts +48 -0
- package/dist/ui/ToggleButton.svelte +58 -0
- package/dist/ui/ToggleButton.svelte.d.ts +45 -0
- package/dist/ui/ToggleGroup.svelte +142 -0
- package/dist/ui/ToggleGroup.svelte.d.ts +86 -0
- package/dist/ui/Toolbar.svelte +40 -0
- package/dist/ui/Toolbar.svelte.d.ts +20 -0
- package/dist/ui/ToolbarButton.svelte +29 -0
- package/dist/ui/ToolbarButton.svelte.d.ts +21 -0
- package/dist/ui/ToolbarLink.svelte +21 -0
- package/dist/ui/ToolbarLink.svelte.d.ts +12 -0
- package/dist/ui/ToolbarSeparator.svelte +22 -0
- package/dist/ui/ToolbarSeparator.svelte.d.ts +12 -0
- package/dist/ui/ToolbarToggleGroup.svelte +66 -0
- package/dist/ui/ToolbarToggleGroup.svelte.d.ts +52 -0
- package/dist/ui/accordion-context.d.ts +26 -0
- package/dist/ui/accordion-context.js +23 -0
- package/dist/ui/context-menu.d.ts +13 -0
- package/dist/ui/context-menu.js +13 -0
- package/dist/ui/index.d.ts +19 -0
- package/dist/ui/index.js +18 -0
- package/dist/ui/menu-context.d.ts +13 -15
- package/dist/ui/menu-context.js +14 -18
- package/dist/ui/menu-parts/CheckboxItemPart.svelte +26 -0
- package/dist/ui/menu-parts/CheckboxItemPart.svelte.d.ts +5 -0
- package/dist/ui/menu-parts/ItemPart.svelte +29 -0
- package/dist/ui/menu-parts/ItemPart.svelte.d.ts +5 -0
- package/dist/ui/menu-parts/RadioGroupPart.svelte +24 -0
- package/dist/ui/menu-parts/RadioGroupPart.svelte.d.ts +5 -0
- package/dist/ui/menu-parts/RadioItemPart.svelte +22 -0
- package/dist/ui/menu-parts/RadioItemPart.svelte.d.ts +5 -0
- package/dist/ui/menu-parts/SeparatorPart.svelte +16 -0
- package/dist/ui/menu-parts/SeparatorPart.svelte.d.ts +5 -0
- package/dist/ui/menu-parts/SubPart.svelte +28 -0
- package/dist/ui/menu-parts/SubPart.svelte.d.ts +5 -0
- package/dist/ui/menu-parts/menu-parts.d.ts +104 -0
- package/dist/ui/menu-parts/menu-parts.js +31 -0
- package/dist/ui/menu-parts/parts.d.ts +12 -0
- package/dist/ui/menu-parts/parts.js +27 -0
- package/dist/ui/toggle-classes.d.ts +39 -0
- package/dist/ui/toggle-classes.js +36 -0
- package/dist/ui/tones.d.ts +35 -0
- package/dist/ui/tones.js +90 -0
- package/package.json +34 -18
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The search lifecycle `Combobox` and `TagsInput` share: debounce,
|
|
3
|
+
* latest-wins and stale-while-revalidate.
|
|
4
|
+
*
|
|
5
|
+
* debounce a keystroke does not start a request; `debounceMs` of
|
|
6
|
+
* quiet does.
|
|
7
|
+
* latest-wins each request gets an `AbortController`, and starting a
|
|
8
|
+
* new one aborts the last, so a slow first response cannot
|
|
9
|
+
* overwrite a fast second one.
|
|
10
|
+
* stale-while- `results` keeps the previous answer while the next is in
|
|
11
|
+
* revalidate flight, with `pending` true, so a dropdown does not
|
|
12
|
+
* strobe on every keystroke.
|
|
13
|
+
*
|
|
14
|
+
* Reactive (`$state`) so a component reads `results`, `pending` and `error`
|
|
15
|
+
* straight into its template. Call `dispose()` from an `$effect` teardown.
|
|
16
|
+
*
|
|
17
|
+
* @module
|
|
18
|
+
*/
|
|
19
|
+
/** Debounced, latest-wins search state for one input. */
|
|
20
|
+
export class LatestSearch {
|
|
21
|
+
/** What the last settled search returned. */
|
|
22
|
+
results = $state([]);
|
|
23
|
+
/** A search is scheduled or in flight. */
|
|
24
|
+
pending = $state(false);
|
|
25
|
+
/** The last search's failure message, if it failed. */
|
|
26
|
+
error = $state();
|
|
27
|
+
#search;
|
|
28
|
+
#debounceMs;
|
|
29
|
+
#timer;
|
|
30
|
+
#inFlight;
|
|
31
|
+
/** Getters, so the component's current props are read at call time. */
|
|
32
|
+
constructor(search, debounceMs) {
|
|
33
|
+
this.#search = search;
|
|
34
|
+
this.#debounceMs = debounceMs;
|
|
35
|
+
}
|
|
36
|
+
/** Search for `query` after the debounce, superseding anything earlier. */
|
|
37
|
+
schedule(query) {
|
|
38
|
+
if (this.#timer !== undefined)
|
|
39
|
+
clearTimeout(this.#timer);
|
|
40
|
+
this.#timer = setTimeout(() => {
|
|
41
|
+
this.#timer = undefined;
|
|
42
|
+
this.#run(query);
|
|
43
|
+
}, this.#debounceMs());
|
|
44
|
+
}
|
|
45
|
+
/** Cancel anything scheduled or in flight and empty the results. */
|
|
46
|
+
clear() {
|
|
47
|
+
this.dispose();
|
|
48
|
+
this.#inFlight = undefined;
|
|
49
|
+
this.pending = false;
|
|
50
|
+
this.results = [];
|
|
51
|
+
}
|
|
52
|
+
/** Cancel anything scheduled or in flight; for an `$effect` teardown. */
|
|
53
|
+
dispose() {
|
|
54
|
+
if (this.#timer !== undefined)
|
|
55
|
+
clearTimeout(this.#timer);
|
|
56
|
+
this.#timer = undefined;
|
|
57
|
+
this.#inFlight?.abort();
|
|
58
|
+
}
|
|
59
|
+
#run(query) {
|
|
60
|
+
const search = this.#search();
|
|
61
|
+
if (search === undefined)
|
|
62
|
+
return;
|
|
63
|
+
this.#inFlight?.abort();
|
|
64
|
+
const own = new AbortController();
|
|
65
|
+
this.#inFlight = own;
|
|
66
|
+
this.pending = true;
|
|
67
|
+
this.error = undefined;
|
|
68
|
+
// A superseded request must not write: `#inFlight` is the identity check,
|
|
69
|
+
// `signal.aborted` catches the caller aborting its own work.
|
|
70
|
+
const current = () => !own.signal.aborted && this.#inFlight === own;
|
|
71
|
+
void search(query, own.signal)
|
|
72
|
+
.then((next) => {
|
|
73
|
+
if (!current())
|
|
74
|
+
return;
|
|
75
|
+
this.results = next;
|
|
76
|
+
this.pending = false;
|
|
77
|
+
})
|
|
78
|
+
.catch((error) => {
|
|
79
|
+
if (!current())
|
|
80
|
+
return;
|
|
81
|
+
this.pending = false;
|
|
82
|
+
this.error = error instanceof Error ? error.message : String(error);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locale-aware number formatting and parsing for `NumberField`.
|
|
3
|
+
*
|
|
4
|
+
* Formatting is `Intl.NumberFormat`. Parsing is the inverse the platform does
|
|
5
|
+
* not ship: the locale's group and decimal separators, and any literal text
|
|
6
|
+
* the options add (a unit, a currency symbol, a percent sign), are learnt
|
|
7
|
+
* from `formatToParts` on a sample number, so "1.234,5 cm" in `de-DE` and
|
|
8
|
+
* "1,234.5 cm" in `en-AU` both read back as `1234.5`.
|
|
9
|
+
*
|
|
10
|
+
* Only ASCII digits are parsed. A locale whose formatter writes other digits
|
|
11
|
+
* (Arabic-Indic, Devanagari) formats correctly but does not read its own
|
|
12
|
+
* output back; widening the parser is a later decision (#145).
|
|
13
|
+
*
|
|
14
|
+
* Pure functions, no module-level state, so the file stays tree-shakeable.
|
|
15
|
+
*
|
|
16
|
+
* @module
|
|
17
|
+
*/
|
|
18
|
+
/** The locales argument every function here accepts, as `Intl.NumberFormat` does. */
|
|
19
|
+
export type NumberLocales = string | readonly string[] | undefined;
|
|
20
|
+
/** What a locale and a set of options write around the digits. */
|
|
21
|
+
export interface NumberSymbols {
|
|
22
|
+
/** The grouping separator, e.g. `','` in `en-AU`, `'.'` in `de-DE`, a narrow no-break space in `fr-FR`. */
|
|
23
|
+
readonly group: string;
|
|
24
|
+
/** The decimal separator. */
|
|
25
|
+
readonly decimal: string;
|
|
26
|
+
/** Every other non-digit string the format adds: unit, currency, percent sign, literal spacing. */
|
|
27
|
+
readonly literals: readonly string[];
|
|
28
|
+
}
|
|
29
|
+
/** Learn the separators and literal text a locale and options produce. */
|
|
30
|
+
export declare function numberSymbols(locales: NumberLocales, options?: Intl.NumberFormatOptions): NumberSymbols;
|
|
31
|
+
/** Format `value` for display; `''` for `undefined`. */
|
|
32
|
+
export declare function formatNumber(value: number | undefined, locales: NumberLocales, options?: Intl.NumberFormatOptions): string;
|
|
33
|
+
/**
|
|
34
|
+
* Parse text written in `locales` with `options` back into a number.
|
|
35
|
+
*
|
|
36
|
+
* Returns `undefined` for empty (or whitespace-only) text and `NaN` for text
|
|
37
|
+
* that is not a number, so a caller can tell "cleared" from "garbage".
|
|
38
|
+
* Percent-style input is divided by 100, the inverse of the formatter.
|
|
39
|
+
*/
|
|
40
|
+
export declare function parseNumber(text: string, locales: NumberLocales, options?: Intl.NumberFormatOptions): number | undefined;
|
|
41
|
+
/** Number of decimal places in `n` as written, e.g. 2 for `0.25`, 0 for `5`. */
|
|
42
|
+
export declare function decimalPlaces(n: number): number;
|
|
43
|
+
/** Round `n` to `places` decimal places without binary drift (`0.1 + 0.2` → `0.3`). */
|
|
44
|
+
export declare function roundTo(n: number, places: number): number;
|
|
45
|
+
/**
|
|
46
|
+
* The decimal places a committed value keeps: the more of the step's and the
|
|
47
|
+
* formatter's own (a percent shows two more places of the fraction than it
|
|
48
|
+
* displays), so a value is never rounded finer than the reader can see.
|
|
49
|
+
*/
|
|
50
|
+
export declare function valuePrecision(step: number, locales: NumberLocales, options?: Intl.NumberFormatOptions): number;
|
|
51
|
+
/** Clamp `n` into `[min, max]`; either bound may be absent. */
|
|
52
|
+
export declare function clamp(n: number, min: number | undefined, max: number | undefined): number;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locale-aware number formatting and parsing for `NumberField`.
|
|
3
|
+
*
|
|
4
|
+
* Formatting is `Intl.NumberFormat`. Parsing is the inverse the platform does
|
|
5
|
+
* not ship: the locale's group and decimal separators, and any literal text
|
|
6
|
+
* the options add (a unit, a currency symbol, a percent sign), are learnt
|
|
7
|
+
* from `formatToParts` on a sample number, so "1.234,5 cm" in `de-DE` and
|
|
8
|
+
* "1,234.5 cm" in `en-AU` both read back as `1234.5`.
|
|
9
|
+
*
|
|
10
|
+
* Only ASCII digits are parsed. A locale whose formatter writes other digits
|
|
11
|
+
* (Arabic-Indic, Devanagari) formats correctly but does not read its own
|
|
12
|
+
* output back; widening the parser is a later decision (#145).
|
|
13
|
+
*
|
|
14
|
+
* Pure functions, no module-level state, so the file stays tree-shakeable.
|
|
15
|
+
*
|
|
16
|
+
* @module
|
|
17
|
+
*/
|
|
18
|
+
/** One formatter per call site; `Intl.NumberFormat` wants a mutable array. */
|
|
19
|
+
function formatter(locales, options) {
|
|
20
|
+
return new Intl.NumberFormat(typeof locales === 'string' || locales === undefined ? locales : [...locales], options);
|
|
21
|
+
}
|
|
22
|
+
/** Learn the separators and literal text a locale and options produce. */
|
|
23
|
+
export function numberSymbols(locales, options) {
|
|
24
|
+
const parts = formatter(locales, { ...options, useGrouping: true }).formatToParts(-12345.6);
|
|
25
|
+
const of = (type) => parts.find((part) => part.type === type)?.value ?? '';
|
|
26
|
+
const literals = parts
|
|
27
|
+
.filter((part) => ['literal', 'unit', 'currency', 'percentSign', 'plusSign'].includes(part.type))
|
|
28
|
+
.map((part) => part.value.trim())
|
|
29
|
+
.filter((value) => value !== '');
|
|
30
|
+
return { group: of('group'), decimal: of('decimal') || '.', literals };
|
|
31
|
+
}
|
|
32
|
+
/** Format `value` for display; `''` for `undefined`. */
|
|
33
|
+
export function formatNumber(value, locales, options) {
|
|
34
|
+
return value === undefined ? '' : formatter(locales, options).format(value);
|
|
35
|
+
}
|
|
36
|
+
/** Spaces a locale may group with, all read as the same separator. */
|
|
37
|
+
const SPACES = /[\s ]/g;
|
|
38
|
+
/**
|
|
39
|
+
* Parse text written in `locales` with `options` back into a number.
|
|
40
|
+
*
|
|
41
|
+
* Returns `undefined` for empty (or whitespace-only) text and `NaN` for text
|
|
42
|
+
* that is not a number, so a caller can tell "cleared" from "garbage".
|
|
43
|
+
* Percent-style input is divided by 100, the inverse of the formatter.
|
|
44
|
+
*/
|
|
45
|
+
export function parseNumber(text, locales, options) {
|
|
46
|
+
if (text.trim() === '')
|
|
47
|
+
return undefined;
|
|
48
|
+
const { group, decimal, literals } = numberSymbols(locales, options);
|
|
49
|
+
let rest = text;
|
|
50
|
+
// Longest first, so "US$" is removed before "$".
|
|
51
|
+
for (const literal of [...literals].sort((a, b) => b.length - a.length)) {
|
|
52
|
+
rest = rest.split(literal).join('');
|
|
53
|
+
}
|
|
54
|
+
if (group.trim() === '')
|
|
55
|
+
rest = rest.replace(SPACES, '');
|
|
56
|
+
else
|
|
57
|
+
rest = rest.split(group).join('').replace(SPACES, '');
|
|
58
|
+
rest = rest
|
|
59
|
+
.split(decimal)
|
|
60
|
+
.join('.')
|
|
61
|
+
.replace(/[−‒–]/g, '-');
|
|
62
|
+
if (!/^-?(\d+\.?\d*|\.\d+)$/.test(rest))
|
|
63
|
+
return Number.NaN;
|
|
64
|
+
const parsed = Number(rest);
|
|
65
|
+
return options?.style === 'percent' ? roundTo(parsed / 100, 15) : parsed;
|
|
66
|
+
}
|
|
67
|
+
/** Number of decimal places in `n` as written, e.g. 2 for `0.25`, 0 for `5`. */
|
|
68
|
+
export function decimalPlaces(n) {
|
|
69
|
+
const [, fraction = ''] = String(n).split('.');
|
|
70
|
+
const exponent = /e-(\d+)$/.exec(String(n));
|
|
71
|
+
return exponent === null ? fraction.length : Number(exponent[1]) + fraction.length;
|
|
72
|
+
}
|
|
73
|
+
/** Round `n` to `places` decimal places without binary drift (`0.1 + 0.2` → `0.3`). */
|
|
74
|
+
export function roundTo(n, places) {
|
|
75
|
+
return Number(n.toFixed(Math.min(100, Math.max(0, places))));
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The decimal places a committed value keeps: the more of the step's and the
|
|
79
|
+
* formatter's own (a percent shows two more places of the fraction than it
|
|
80
|
+
* displays), so a value is never rounded finer than the reader can see.
|
|
81
|
+
*/
|
|
82
|
+
export function valuePrecision(step, locales, options) {
|
|
83
|
+
const shown = formatter(locales, options).resolvedOptions().maximumFractionDigits ?? 0;
|
|
84
|
+
return Math.max(decimalPlaces(step), shown + (options?.style === 'percent' ? 2 : 0));
|
|
85
|
+
}
|
|
86
|
+
/** Clamp `n` into `[min, max]`; either bound may be absent. */
|
|
87
|
+
export function clamp(n, min, max) {
|
|
88
|
+
return Math.min(max ?? Number.POSITIVE_INFINITY, Math.max(min ?? Number.NEGATIVE_INFINITY, n));
|
|
89
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `DateField` and `TimeField` share when handing their props to bits-ui:
|
|
3
|
+
* the props themselves, and the split between the bits-ui root and the
|
|
4
|
+
* segment group `SegmentGroup` renders.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
import type { CalendarDateTime, DateValue, Time, ZonedDateTime } from '@internationalized/date';
|
|
9
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
10
|
+
import type { FieldSize } from './field-types.ts';
|
|
11
|
+
/** A time of day, alone or on a date: what a `TimeField` holds. */
|
|
12
|
+
export type TimeValue = Time | CalendarDateTime | ZonedDateTime;
|
|
13
|
+
/** The props `DateField` and `TimeField` share: everything but the value, its bounds and the granularity. */
|
|
14
|
+
export interface SegmentedFieldProps extends Omit<HTMLAttributes<HTMLDivElement>, 'placeholder' | 'children' | 'id'> {
|
|
15
|
+
/** Force a 12- or 24-hour clock; omitted, the locale's. */
|
|
16
|
+
hourCycle?: 12 | 24 | undefined;
|
|
17
|
+
/** Form field name: bits-ui renders a hidden input with the ISO value. */
|
|
18
|
+
name?: string | undefined;
|
|
19
|
+
/** BCP 47 locale(s); the first is used. Falls back to the context's `locale()`. */
|
|
20
|
+
locales?: string | readonly string[] | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* No interaction at all.
|
|
23
|
+
* @defaultValue `false`
|
|
24
|
+
*/
|
|
25
|
+
disabled?: boolean | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Focusable and read out, but the value cannot change.
|
|
28
|
+
* @defaultValue `false`
|
|
29
|
+
*/
|
|
30
|
+
readonly?: boolean | undefined;
|
|
31
|
+
/** @defaultValue `'md'` */
|
|
32
|
+
size?: FieldSize | undefined;
|
|
33
|
+
/** `FieldControl`: the segment group's `id`. */
|
|
34
|
+
id?: string | undefined;
|
|
35
|
+
/** `FieldControl`: the visible label's id; the group and each segment are `aria-labelledby` it. */
|
|
36
|
+
labelId?: string | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* `FieldControl`: space-separated ids for the group's `aria-describedby`.
|
|
39
|
+
* @defaultValue `''`
|
|
40
|
+
*/
|
|
41
|
+
describedBy?: string | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* `FieldControl`: the field is in error — `aria-invalid` and the error border.
|
|
44
|
+
* @defaultValue `false`
|
|
45
|
+
*/
|
|
46
|
+
invalid?: boolean | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* `FieldControl`: a value is required for the form to submit.
|
|
49
|
+
* @defaultValue `false`
|
|
50
|
+
*/
|
|
51
|
+
required?: boolean | undefined;
|
|
52
|
+
}
|
|
53
|
+
/** What `SegmentGroup` renders the group with. */
|
|
54
|
+
export interface SegmentGroupOptions {
|
|
55
|
+
readonly id: string | undefined;
|
|
56
|
+
readonly name: string | undefined;
|
|
57
|
+
readonly labelId: string | undefined;
|
|
58
|
+
readonly describedBy: string;
|
|
59
|
+
readonly invalid: boolean;
|
|
60
|
+
readonly disabled: boolean;
|
|
61
|
+
readonly size: FieldSize;
|
|
62
|
+
/** The caller's remaining attributes, spread onto the group. */
|
|
63
|
+
readonly attributes: Record<string, unknown>;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Split the shared props into the bits-ui root's options (defaults applied,
|
|
67
|
+
* unset options dropped) and the segment group's. One function, so
|
|
68
|
+
* `DateField` and `TimeField` cannot disagree about a default.
|
|
69
|
+
*/
|
|
70
|
+
export declare function segmentedField(props: SegmentedFieldProps, contextLocale: string | readonly string[] | undefined): {
|
|
71
|
+
readonly root: Record<string, unknown>;
|
|
72
|
+
readonly group: SegmentGroupOptions;
|
|
73
|
+
};
|
|
74
|
+
/** The props `DatePicker` and `DateRangePicker` add to the segmented field: the calendar popover's. */
|
|
75
|
+
export interface PickerProps {
|
|
76
|
+
/** Whether the calendar popover is open. Bindable. */
|
|
77
|
+
open?: boolean | undefined;
|
|
78
|
+
/** Days that cannot be chosen at all; rendered disabled. */
|
|
79
|
+
isDateDisabled?: ((date: DateValue) => boolean) | undefined;
|
|
80
|
+
/** Days shown but marked unavailable: booked, frost, closed. */
|
|
81
|
+
isDateUnavailable?: ((date: DateValue) => boolean) | undefined;
|
|
82
|
+
/** 0 = Sunday … 6 = Saturday; omitted, the locale's own week start. */
|
|
83
|
+
weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined;
|
|
84
|
+
/**
|
|
85
|
+
* Months shown side by side in the popover.
|
|
86
|
+
* @defaultValue `1`
|
|
87
|
+
*/
|
|
88
|
+
numberOfMonths?: number | undefined;
|
|
89
|
+
/**
|
|
90
|
+
* Accessible name of the calendar trigger button.
|
|
91
|
+
* @defaultValue `'Choose date'`
|
|
92
|
+
*/
|
|
93
|
+
triggerLabel?: string | undefined;
|
|
94
|
+
/**
|
|
95
|
+
* Accessible name of the popover's calendar.
|
|
96
|
+
* @defaultValue `'Calendar'`
|
|
97
|
+
*/
|
|
98
|
+
calendarLabel?: string | undefined;
|
|
99
|
+
/**
|
|
100
|
+
* Accessible name of the previous-month button.
|
|
101
|
+
* @defaultValue `'Previous month'`
|
|
102
|
+
*/
|
|
103
|
+
previousLabel?: string | undefined;
|
|
104
|
+
/**
|
|
105
|
+
* Accessible name of the next-month button.
|
|
106
|
+
* @defaultValue `'Next month'`
|
|
107
|
+
*/
|
|
108
|
+
nextLabel?: string | undefined;
|
|
109
|
+
}
|
|
110
|
+
/** What `pickerField` hands a picker: the bits-ui root's options, the segment group's, and the popover's labels. */
|
|
111
|
+
export interface PickerField {
|
|
112
|
+
readonly root: Record<string, unknown>;
|
|
113
|
+
readonly group: SegmentGroupOptions;
|
|
114
|
+
readonly triggerLabel: string;
|
|
115
|
+
readonly calendarLabel: string;
|
|
116
|
+
readonly months: {
|
|
117
|
+
readonly locale: string;
|
|
118
|
+
readonly previousLabel: string;
|
|
119
|
+
readonly nextLabel: string;
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Split a picker's props (the segmented field's plus the popover's) into
|
|
124
|
+
* what the bits-ui root, `SegmentGroup` and `PickerPopover` take, defaults
|
|
125
|
+
* applied. One function, so `DatePicker` and `DateRangePicker` cannot
|
|
126
|
+
* disagree about a default.
|
|
127
|
+
*/
|
|
128
|
+
export declare function pickerField(props: SegmentedFieldProps & Omit<PickerProps, 'open'>, contextLocale: string | readonly string[] | undefined): PickerField;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `DateField` and `TimeField` share when handing their props to bits-ui:
|
|
3
|
+
* the props themselves, and the split between the bits-ui root and the
|
|
4
|
+
* segment group `SegmentGroup` renders.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
import { definedProps, firstLocale } from "./calendar-props.js";
|
|
9
|
+
/**
|
|
10
|
+
* Split the shared props into the bits-ui root's options (defaults applied,
|
|
11
|
+
* unset options dropped) and the segment group's. One function, so
|
|
12
|
+
* `DateField` and `TimeField` cannot disagree about a default.
|
|
13
|
+
*/
|
|
14
|
+
export function segmentedField(props, contextLocale) {
|
|
15
|
+
const { hourCycle, name, locales = contextLocale, disabled = false, readonly = false, size = 'md', id, labelId, describedBy = '', invalid = false, required = false, ...attributes } = props;
|
|
16
|
+
return {
|
|
17
|
+
root: {
|
|
18
|
+
...definedProps({ hourCycle, locale: firstLocale(locales) }),
|
|
19
|
+
disabled,
|
|
20
|
+
readonly,
|
|
21
|
+
required,
|
|
22
|
+
},
|
|
23
|
+
group: { id, name, labelId, describedBy, invalid, disabled, size, attributes },
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Split a picker's props (the segmented field's plus the popover's) into
|
|
28
|
+
* what the bits-ui root, `SegmentGroup` and `PickerPopover` take, defaults
|
|
29
|
+
* applied. One function, so `DatePicker` and `DateRangePicker` cannot
|
|
30
|
+
* disagree about a default.
|
|
31
|
+
*/
|
|
32
|
+
export function pickerField(props, contextLocale) {
|
|
33
|
+
const { isDateDisabled, isDateUnavailable, weekStartsOn, numberOfMonths = 1, triggerLabel = 'Choose date', calendarLabel = 'Calendar', previousLabel = 'Previous month', nextLabel = 'Next month', ...fieldProps } = props;
|
|
34
|
+
const field = segmentedField(fieldProps, contextLocale);
|
|
35
|
+
return {
|
|
36
|
+
root: {
|
|
37
|
+
...field.root,
|
|
38
|
+
...definedProps({ isDateDisabled, isDateUnavailable, weekStartsOn }),
|
|
39
|
+
numberOfMonths,
|
|
40
|
+
calendarLabel,
|
|
41
|
+
weekdayFormat: 'short',
|
|
42
|
+
},
|
|
43
|
+
group: field.group,
|
|
44
|
+
triggerLabel,
|
|
45
|
+
calendarLabel,
|
|
46
|
+
months: {
|
|
47
|
+
locale: firstLocale(fieldProps.locales ?? contextLocale) ?? 'en',
|
|
48
|
+
previousLabel,
|
|
49
|
+
nextLabel,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Page Up / Page Down arithmetic behind `Slider`, which bits-ui 2.20's
|
|
3
|
+
* thumb keyboard handler does not cover. Pure, so the component's key handler
|
|
4
|
+
* stays a dispatch and the arithmetic is testable on its own.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
/** The scale a slider moves over: its bounds and its step (a number, or explicit snap points). */
|
|
9
|
+
export interface SliderScale {
|
|
10
|
+
readonly min: number;
|
|
11
|
+
readonly max: number;
|
|
12
|
+
readonly step: number | readonly number[];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The value one page away from `current` in `direction` (`1` up, `-1` down):
|
|
16
|
+
* a tenth of the scale, rounded to whole steps and never less than one. For
|
|
17
|
+
* snap points, a tenth of the points, never less than one.
|
|
18
|
+
*/
|
|
19
|
+
export declare function pageStep(current: number, direction: 1 | -1, scale: SliderScale): number;
|
|
20
|
+
/** Clamp `next` for thumb `index` between its neighbours (or the scale's ends), so thumbs never cross. */
|
|
21
|
+
export declare function clampToNeighbours(values: readonly number[], index: number, next: number, scale: SliderScale): number;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Page Up / Page Down arithmetic behind `Slider`, which bits-ui 2.20's
|
|
3
|
+
* thumb keyboard handler does not cover. Pure, so the component's key handler
|
|
4
|
+
* stays a dispatch and the arithmetic is testable on its own.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
/** Index of the snap point nearest `value`. */
|
|
9
|
+
function nearestIndex(points, value) {
|
|
10
|
+
let best = 0;
|
|
11
|
+
points.forEach((point, index) => {
|
|
12
|
+
if (Math.abs(point - value) < Math.abs((points[best] ?? point) - value))
|
|
13
|
+
best = index;
|
|
14
|
+
});
|
|
15
|
+
return best;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The value one page away from `current` in `direction` (`1` up, `-1` down):
|
|
19
|
+
* a tenth of the scale, rounded to whole steps and never less than one. For
|
|
20
|
+
* snap points, a tenth of the points, never less than one.
|
|
21
|
+
*/
|
|
22
|
+
export function pageStep(current, direction, scale) {
|
|
23
|
+
const { min, max, step } = scale;
|
|
24
|
+
if (typeof step === 'number') {
|
|
25
|
+
return current + direction * step * Math.max(1, Math.round((max - min) / 10 / step));
|
|
26
|
+
}
|
|
27
|
+
const points = [...step].sort((a, b) => a - b);
|
|
28
|
+
const jump = Math.max(1, Math.round(points.length / 10));
|
|
29
|
+
const target = Math.min(points.length - 1, Math.max(0, nearestIndex(points, current) + direction * jump));
|
|
30
|
+
return points[target] ?? current;
|
|
31
|
+
}
|
|
32
|
+
/** Clamp `next` for thumb `index` between its neighbours (or the scale's ends), so thumbs never cross. */
|
|
33
|
+
export function clampToNeighbours(values, index, next, scale) {
|
|
34
|
+
const lower = values[index - 1] ?? scale.min;
|
|
35
|
+
const upper = values[index + 1] ?? scale.max;
|
|
36
|
+
return Math.min(upper, Math.max(lower, next));
|
|
37
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
<!-- @component
|
|
2
|
+
The time left until an instant, ticking every second in daisyUI's `countdown` digits, read out coarsely and announced once at zero.
|
|
3
|
+
-->
|
|
4
|
+
<!--
|
|
5
|
+
Countdown — a seed-swap opening, an auction close, a watering timer.
|
|
6
|
+
|
|
7
|
+
ARIA contract (the part that matters): a `role="timer"` named by `label`.
|
|
8
|
+
A second-by-second announcement is unusable, so the ticking digits are
|
|
9
|
+
`aria-hidden`, and what assistive technology reads is a separate
|
|
10
|
+
visually hidden text at a coarse granularity that changes far less often:
|
|
11
|
+
whole minutes under an hour ("4 minutes"), whole hours above it ("2
|
|
12
|
+
days, 4 hours"). The granularity is fixed, not a prop. `role="timer"` is
|
|
13
|
+
implicitly `aria-live="off"`, so even that is read on demand, not pushed.
|
|
14
|
+
At zero a polite `role="status"` announces `completeLabel` once. With
|
|
15
|
+
`prefers-reduced-motion: reduce` the digit roll does not animate
|
|
16
|
+
(WCAG 2.2.2).
|
|
17
|
+
|
|
18
|
+
The clock starts in an `$effect`, never at module load (`sideEffects:
|
|
19
|
+
false`), and stops on destroy or at zero. The first render uses the
|
|
20
|
+
current time, and the client re-syncs after mount. An `until` already in
|
|
21
|
+
the past renders the complete state without calling `onComplete`: that
|
|
22
|
+
only fires when the countdown is seen reaching zero.
|
|
23
|
+
-->
|
|
24
|
+
<script lang="ts">import { getComponentsContext } from "../context.svelte.js";
|
|
25
|
+
import { formatDuration } from "./duration.js";
|
|
26
|
+
let { until, label, largestUnit = "days", display = "digits", completeLabel = "Time is up", onComplete, locales = getComponentsContext().locale(), class: className, ...rest } = $props();
|
|
27
|
+
const SECOND = 1e3;
|
|
28
|
+
const MINUTE = 60 * SECOND;
|
|
29
|
+
const HOUR = 60 * MINUTE;
|
|
30
|
+
const DAY = 24 * HOUR;
|
|
31
|
+
const SIZES = {
|
|
32
|
+
days: DAY,
|
|
33
|
+
hours: HOUR,
|
|
34
|
+
minutes: MINUTE,
|
|
35
|
+
seconds: SECOND
|
|
36
|
+
};
|
|
37
|
+
const ORDER = [
|
|
38
|
+
"days",
|
|
39
|
+
"hours",
|
|
40
|
+
"minutes",
|
|
41
|
+
"seconds"
|
|
42
|
+
];
|
|
43
|
+
let now = $state(Date.now());
|
|
44
|
+
const target = $derived(new Date(until).getTime());
|
|
45
|
+
const remaining = $derived(Math.max(0, target - now));
|
|
46
|
+
const complete = $derived(remaining === 0);
|
|
47
|
+
/** The units shown, from `largestUnit` down to seconds, each with its count. */
|
|
48
|
+
const parts = $derived.by(() => {
|
|
49
|
+
let rest = Math.ceil(remaining / SECOND) * SECOND;
|
|
50
|
+
return ORDER.slice(ORDER.indexOf(largestUnit)).map((unit) => {
|
|
51
|
+
const count = Math.floor(rest / SIZES[unit]);
|
|
52
|
+
rest -= count * SIZES[unit];
|
|
53
|
+
return {
|
|
54
|
+
unit,
|
|
55
|
+
count
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
/** The spoken, coarse rendering: minutes under an hour, hours above. */
|
|
60
|
+
const spoken = $derived.by(() => {
|
|
61
|
+
if (remaining >= HOUR) {
|
|
62
|
+
const days = Math.floor(remaining / DAY);
|
|
63
|
+
const hours = Math.floor(remaining % DAY / HOUR);
|
|
64
|
+
const value = days > 0 ? {
|
|
65
|
+
days,
|
|
66
|
+
hours
|
|
67
|
+
} : { hours };
|
|
68
|
+
return formatDuration(value, locales, "long");
|
|
69
|
+
}
|
|
70
|
+
return formatDuration({ minutes: Math.ceil(remaining / MINUTE) }, locales, "long");
|
|
71
|
+
});
|
|
72
|
+
const text = $derived(formatDuration(Object.fromEntries(parts.map(({ unit, count }) => [unit, count])), locales, "short"));
|
|
73
|
+
/** The narrow unit label after each digit group ("d", "h", "m", "s" in English). */
|
|
74
|
+
const unitLabel = (unit, count) => new Intl.NumberFormat(locales, {
|
|
75
|
+
style: "unit",
|
|
76
|
+
unit: unit.slice(0, -1),
|
|
77
|
+
unitDisplay: "narrow"
|
|
78
|
+
}).formatToParts(count).filter((part) => part.type === "unit").map((part) => part.value).join("");
|
|
79
|
+
$effect(() => {
|
|
80
|
+
now = Date.now();
|
|
81
|
+
if (target <= now) return;
|
|
82
|
+
let done = false;
|
|
83
|
+
const timer = setInterval(() => {
|
|
84
|
+
now = Date.now();
|
|
85
|
+
if (!done && now >= target) {
|
|
86
|
+
done = true;
|
|
87
|
+
clearInterval(timer);
|
|
88
|
+
onComplete?.();
|
|
89
|
+
}
|
|
90
|
+
}, SECOND);
|
|
91
|
+
return () => clearInterval(timer);
|
|
92
|
+
});
|
|
93
|
+
</script>
|
|
94
|
+
|
|
95
|
+
<span {...rest} role="timer" aria-label={label} class={['inline-flex flex-wrap items-baseline gap-2', className]}>
|
|
96
|
+
<span aria-hidden="true">{label}</span>
|
|
97
|
+
<span aria-hidden="true" class="inline-flex items-baseline gap-1 font-mono tabular-nums">
|
|
98
|
+
{#if display === 'digits'}
|
|
99
|
+
{#each parts as { unit, count } (unit)}
|
|
100
|
+
<span class="countdown motion-reduce:*:before:transition-none"
|
|
101
|
+
><span style:--value={count} style:--digits={unit === largestUnit ? undefined : 2}>{count}</span></span
|
|
102
|
+
>{unitLabel(unit, count)}
|
|
103
|
+
{/each}
|
|
104
|
+
{:else}
|
|
105
|
+
{text}
|
|
106
|
+
{/if}
|
|
107
|
+
</span>
|
|
108
|
+
<span class="sr-only">{complete ? '' : spoken}</span>
|
|
109
|
+
<span role="status" class="sr-only">{complete ? completeLabel : ''}</span>
|
|
110
|
+
</span>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
|
+
/** The largest unit a `Countdown` shows. */
|
|
3
|
+
type CountdownUnit = 'days' | 'hours' | 'minutes';
|
|
4
|
+
interface Props extends Omit<HTMLAttributes<HTMLSpanElement>, 'children'> {
|
|
5
|
+
/** The instant counted down to: a `Date`, epoch milliseconds or an ISO string. */
|
|
6
|
+
until: Date | number | string;
|
|
7
|
+
/** Accessible name and visible lead-in, e.g. `'Seed swap opens in'`. */
|
|
8
|
+
label: string;
|
|
9
|
+
/**
|
|
10
|
+
* Largest unit shown; larger amounts fold into it (50 hours with `'hours'`).
|
|
11
|
+
* @defaultValue `'days'`
|
|
12
|
+
*/
|
|
13
|
+
largestUnit?: CountdownUnit | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* daisyUI digit roll, or formatted text.
|
|
16
|
+
* @defaultValue `'digits'`
|
|
17
|
+
*/
|
|
18
|
+
display?: 'digits' | 'text' | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Announced once at zero.
|
|
21
|
+
* @defaultValue `'Time is up'`
|
|
22
|
+
*/
|
|
23
|
+
completeLabel?: string | undefined;
|
|
24
|
+
/** Called once when the countdown is seen reaching zero. */
|
|
25
|
+
onComplete?: (() => void) | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Locale(s) for the unit words.
|
|
28
|
+
* @defaultValue the components context's `locale()`
|
|
29
|
+
*/
|
|
30
|
+
locales?: string | readonly string[] | undefined;
|
|
31
|
+
}
|
|
32
|
+
/** The time left until an instant, ticking every second in daisyUI's `countdown` digits, read out coarsely and announced once at zero. */
|
|
33
|
+
declare const Countdown: import("svelte").Component<Props, {}, "">;
|
|
34
|
+
type Countdown = ReturnType<typeof Countdown>;
|
|
35
|
+
export default Countdown;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<!-- @component
|
|
2
|
+
A length of time ("2 hr, 30 min") in a `<time>` element whose `datetime` is the ISO 8601 duration.
|
|
3
|
+
-->
|
|
4
|
+
<!--
|
|
5
|
+
Duration — the length-of-time counterpart of `DateTime` (an instant) and
|
|
6
|
+
`RelativeTime` (an instant relative to now), formatted by
|
|
7
|
+
`formatDuration` (see `duration.ts`).
|
|
8
|
+
|
|
9
|
+
ARIA contract: none beyond the native `<time>`. Its `datetime` holds the
|
|
10
|
+
HTML duration string (`PT2H30M`), the content the localised text.
|
|
11
|
+
-->
|
|
12
|
+
<script lang="ts">import { getComponentsContext } from "../context.svelte.js";
|
|
13
|
+
import { durationFromMilliseconds, formatDuration, toIsoDuration } from "./duration.js";
|
|
14
|
+
let { value, style = "short", locales = getComponentsContext().locale(), ...rest } = $props();
|
|
15
|
+
const duration = $derived(typeof value === "number" ? durationFromMilliseconds(value) : value);
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<time {...rest} datetime={toIsoDuration(duration)}>{formatDuration(duration, locales, style)}</time>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { HTMLTimeAttributes } from 'svelte/elements';
|
|
2
|
+
import { type DurationStyle, type DurationValue } from './duration.ts';
|
|
3
|
+
interface Props extends Omit<HTMLTimeAttributes, 'children'> {
|
|
4
|
+
/** A `DurationValue`, or milliseconds. */
|
|
5
|
+
value: DurationValue | number;
|
|
6
|
+
/**
|
|
7
|
+
* How it is spelled.
|
|
8
|
+
* @defaultValue `'short'`
|
|
9
|
+
*/
|
|
10
|
+
style?: DurationStyle | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* Locale(s) for `Intl` formatting.
|
|
13
|
+
* @defaultValue the components context's `locale()`
|
|
14
|
+
*/
|
|
15
|
+
locales?: string | readonly string[] | undefined;
|
|
16
|
+
}
|
|
17
|
+
/** A length of time ("2 hr, 30 min") in a `<time>` element whose `datetime` is the ISO 8601 duration. */
|
|
18
|
+
declare const Duration: import("svelte").Component<Props, {}, "">;
|
|
19
|
+
type Duration = ReturnType<typeof Duration>;
|
|
20
|
+
export default Duration;
|
|
@@ -13,6 +13,10 @@ Best-effort client-side highlighting of query tokens in a search result.
|
|
|
13
13
|
<script lang="ts" module>function escapeRegex(value) {
|
|
14
14
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Split `text` into segments, each flagged `match` when it is a case-insensitive hit for one of `query`'s whitespace-separated tokens.
|
|
18
|
+
* A blank `query` or `text` returns the whole text as one unmatched segment.
|
|
19
|
+
*/
|
|
16
20
|
export function tokenise(text, query) {
|
|
17
21
|
const tokens = query.trim().split(/\s+/).filter((token) => token.length > 0).map(escapeRegex);
|
|
18
22
|
if (tokens.length === 0 || text.length === 0) {
|