@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,75 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
import { type FieldOption, type FieldOptionInput, type FieldSize } from './field-types.ts';
|
|
4
|
+
interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'id'> {
|
|
5
|
+
/** The options; bare strings are both value and label. */
|
|
6
|
+
options: readonly FieldOptionInput[];
|
|
7
|
+
/**
|
|
8
|
+
* The selected values, in option order. Bindable.
|
|
9
|
+
* @defaultValue `[]`
|
|
10
|
+
*/
|
|
11
|
+
values?: string[] | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* Trigger text while nothing is selected.
|
|
14
|
+
* @defaultValue `'Select…'`
|
|
15
|
+
*/
|
|
16
|
+
placeholder?: string | undefined;
|
|
17
|
+
/** Posts each selected value under this name, one hidden input per value. */
|
|
18
|
+
name?: string | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Show at most this many labels in the trigger, then the overflow count.
|
|
21
|
+
* @defaultValue `2`
|
|
22
|
+
*/
|
|
23
|
+
maxShown?: number | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* Formats the overflow count, e.g. `(n) => \`+${n} more\``.
|
|
26
|
+
* @defaultValue `(n) => \`+${n}\``
|
|
27
|
+
*/
|
|
28
|
+
formatOverflow?: ((hidden: number) => string) | undefined;
|
|
29
|
+
/** Renders one option's body (an icon, a description); defaults to its label. */
|
|
30
|
+
option?: Snippet<[FieldOption, {
|
|
31
|
+
selected: boolean;
|
|
32
|
+
}]> | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* Render the listbox into `<body>`.
|
|
35
|
+
* @defaultValue `true`
|
|
36
|
+
*/
|
|
37
|
+
portal?: boolean | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* Disables the trigger.
|
|
40
|
+
* @defaultValue `false`
|
|
41
|
+
*/
|
|
42
|
+
disabled?: boolean | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* daisyUI size step of the trigger.
|
|
45
|
+
* @defaultValue `'md'`
|
|
46
|
+
*/
|
|
47
|
+
size?: FieldSize | undefined;
|
|
48
|
+
/** `FieldControl`: the trigger's `id`, which `FormField`'s label points at. Minted when omitted. */
|
|
49
|
+
id?: string | undefined;
|
|
50
|
+
/**
|
|
51
|
+
* `FieldControl`: the visible label's id. Not used — the trigger is a
|
|
52
|
+
* `<button>`, so the `FormField`'s `<label for>` already names it; taken
|
|
53
|
+
* here so a spread `FieldControl` does not leak it as an attribute.
|
|
54
|
+
*/
|
|
55
|
+
labelId?: string | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* `FieldControl`: space-separated ids for `aria-describedby`.
|
|
58
|
+
* @defaultValue `''`
|
|
59
|
+
*/
|
|
60
|
+
describedBy?: string | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* `FieldControl`: the field is in error — sets `aria-invalid` and `select-error`.
|
|
63
|
+
* @defaultValue `false`
|
|
64
|
+
*/
|
|
65
|
+
invalid?: boolean | undefined;
|
|
66
|
+
/**
|
|
67
|
+
* `FieldControl`: sets `aria-required` on the trigger.
|
|
68
|
+
* @defaultValue `false`
|
|
69
|
+
*/
|
|
70
|
+
required?: boolean | undefined;
|
|
71
|
+
}
|
|
72
|
+
/** Several choices from a known list in a compact trigger that summarises them, on bits-ui's `Select` (`type="multiple"`). */
|
|
73
|
+
declare const MultiSelect: import("svelte").Component<Props, {}, "values">;
|
|
74
|
+
type MultiSelect = ReturnType<typeof MultiSelect>;
|
|
75
|
+
export default MultiSelect;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
<!-- @component
|
|
2
|
+
An exact number the reader types or nudges, shown locale-formatted (units, percent, currency) with − / + stepper buttons: the APG spinbutton.
|
|
3
|
+
-->
|
|
4
|
+
<!--
|
|
5
|
+
NumberField — a `role="spinbutton"` text input showing its value through
|
|
6
|
+
`Intl.NumberFormat`, plus − / + buttons, stepping clamped to `min` / `max`.
|
|
7
|
+
|
|
8
|
+
Hand-rolled, and why that is acceptable here (AGENTS.md asks for the
|
|
9
|
+
reason): bits-ui 2.20 has no number field, and the APG spinbutton pattern is
|
|
10
|
+
one input plus two buttons — no popover, no roving focus, no ARIA
|
|
11
|
+
relationship beyond `aria-controls` — so every part of the contract is
|
|
12
|
+
small and pinned by `tests/fields/NumberField.test.ts`. It replaces the
|
|
13
|
+
native `<input type="number">` for callers who need what that cannot do:
|
|
14
|
+
locale formatting and parsing ("1.234,5" in `de-DE`), a `number` value
|
|
15
|
+
rather than a string, no accidental scroll-wheel changes (there is no wheel
|
|
16
|
+
handling at all), and stepper buttons that are labelled and styleable.
|
|
17
|
+
|
|
18
|
+
Editing: while focused the input holds the text as typed. On blur or Enter
|
|
19
|
+
it is parsed with the locale's separators (`number-format.ts`), clamped to
|
|
20
|
+
`min` / `max`, rounded to the step's or the format's precision, written to
|
|
21
|
+
`value`, re-formatted, and passed to `onCommit`. Unparseable text reverts
|
|
22
|
+
to the last valid value; clearing the field commits `undefined`.
|
|
23
|
+
|
|
24
|
+
ARIA contract (APG spinbutton): the input is `role="spinbutton"` with
|
|
25
|
+
`aria-valuenow`, `aria-valuemin` / `aria-valuemax` when set, and
|
|
26
|
+
`aria-valuetext` — the formatted string, so "45%" is read rather than
|
|
27
|
+
"0.45" — plus `inputmode` (`numeric` when the format has no fraction
|
|
28
|
+
digits, else `decimal`) and the `FieldControl` wiring. The − / + are real
|
|
29
|
+
buttons named by `decrementLabel` / `incrementLabel`, `aria-controls` the
|
|
30
|
+
input, `tabindex="-1"` so the field stays one tab stop, and disabled at the
|
|
31
|
+
bounds. Keys: Arrow Up / Down ± `step`, Page Up / Down ± `largeStep`,
|
|
32
|
+
Home / End to `min` / `max` (when set), Enter commits.
|
|
33
|
+
|
|
34
|
+
Press-and-hold on − / + auto-repeats: one step on press, then after
|
|
35
|
+
`REPEAT_DELAY_MS` it repeats, speeding up to `REPEAT_MIN_MS`, as GTK's spin
|
|
36
|
+
button, WinUI's NumberBox and SwiftUI's Stepper do. The pointer is captured,
|
|
37
|
+
so moving off the button keeps the capture and `pointerup` /
|
|
38
|
+
`pointercancel` / `lostpointercapture` (or reaching a bound) stops it. A
|
|
39
|
+
pointer press never moves focus off the input. A click with no pointer
|
|
40
|
+
press before it (assistive technology activating the button) steps once.
|
|
41
|
+
|
|
42
|
+
`name` posts the raw number through a hidden input, never the formatted
|
|
43
|
+
text, so a server never parses "1.234,5 cm".
|
|
44
|
+
-->
|
|
45
|
+
<script lang="ts">import { Minus, Plus } from "@lucide/svelte";
|
|
46
|
+
import { getComponentsContext } from "../context.svelte.js";
|
|
47
|
+
import { inputButtonSize, inputSize } from "./field-types.js";
|
|
48
|
+
import { clamp, formatNumber, parseNumber, roundTo, valuePrecision } from "./number-format.js";
|
|
49
|
+
/** How long a press is held before it starts repeating. */
|
|
50
|
+
const REPEAT_DELAY_MS = 400;
|
|
51
|
+
/** The first repeat interval; each repeat shortens it towards `REPEAT_MIN_MS`. */
|
|
52
|
+
const REPEAT_START_MS = 150;
|
|
53
|
+
const REPEAT_MIN_MS = 40;
|
|
54
|
+
let { value = $bindable(), min, max, step = 1, largeStep, formatOptions, locales = getComponentsContext().locale(), showStepper = true, decrementLabel = "Decrease", incrementLabel = "Increase", onCommit, name, size = "md", disabled = false, id: idProp, describedBy = "", labelId: _labelId, invalid = false, class: className, onkeydown, onblur, ...rest } = $props();
|
|
55
|
+
const uid = $props.id();
|
|
56
|
+
const id = $derived(idProp ?? uid);
|
|
57
|
+
const formatted = $derived(formatNumber(value, locales, formatOptions));
|
|
58
|
+
/** The input's text: the formatted value, overwritten by typing until the next commit. */
|
|
59
|
+
let text = $derived(formatted);
|
|
60
|
+
const precision = $derived(valuePrecision(step, locales, formatOptions));
|
|
61
|
+
const atMin = $derived(value !== undefined && min !== undefined && value <= min);
|
|
62
|
+
const atMax = $derived(value !== undefined && max !== undefined && value >= max);
|
|
63
|
+
let input = $state(null);
|
|
64
|
+
let repeat;
|
|
65
|
+
/** Write a new value: clamp, round, re-format, and tell `onCommit`. */
|
|
66
|
+
function commit(next) {
|
|
67
|
+
value = next === undefined ? undefined : roundTo(clamp(next, min, max), precision);
|
|
68
|
+
text = formatNumber(value, locales, formatOptions);
|
|
69
|
+
onCommit?.(value);
|
|
70
|
+
}
|
|
71
|
+
function commitText() {
|
|
72
|
+
if (text === formatted) return;
|
|
73
|
+
const parsed = parseNumber(text, locales, formatOptions);
|
|
74
|
+
if (parsed !== undefined && Number.isNaN(parsed)) {
|
|
75
|
+
text = formatted;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
commit(parsed);
|
|
79
|
+
}
|
|
80
|
+
/** Move by `delta`; from empty, land on `min` (or 0) rather than stepping. */
|
|
81
|
+
function stepBy(delta) {
|
|
82
|
+
commit(value === undefined ? min ?? 0 : value + delta);
|
|
83
|
+
}
|
|
84
|
+
const keySteps = {
|
|
85
|
+
ArrowUp: () => step,
|
|
86
|
+
ArrowDown: () => -step,
|
|
87
|
+
PageUp: () => largeStep ?? step * 10,
|
|
88
|
+
PageDown: () => -(largeStep ?? step * 10)
|
|
89
|
+
};
|
|
90
|
+
function handleKeydown(event) {
|
|
91
|
+
const delta = keySteps[event.key]?.();
|
|
92
|
+
if (delta !== undefined) {
|
|
93
|
+
event.preventDefault();
|
|
94
|
+
stepBy(delta);
|
|
95
|
+
} else if (event.key === "Home" && min !== undefined) {
|
|
96
|
+
event.preventDefault();
|
|
97
|
+
commit(min);
|
|
98
|
+
} else if (event.key === "End" && max !== undefined) {
|
|
99
|
+
event.preventDefault();
|
|
100
|
+
commit(max);
|
|
101
|
+
} else if (event.key === "Enter") {
|
|
102
|
+
commitText();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function stopRepeat() {
|
|
106
|
+
clearTimeout(repeat);
|
|
107
|
+
repeat = undefined;
|
|
108
|
+
}
|
|
109
|
+
/** Step now, then keep stepping while held, faster each time, until a bound. */
|
|
110
|
+
function startRepeat(event, delta) {
|
|
111
|
+
if (event.button !== 0 || disabled) return;
|
|
112
|
+
event.preventDefault();
|
|
113
|
+
input?.focus();
|
|
114
|
+
try {
|
|
115
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
116
|
+
} catch {}
|
|
117
|
+
stepBy(delta);
|
|
118
|
+
const tick = (interval) => {
|
|
119
|
+
repeat = setTimeout(() => {
|
|
120
|
+
if (delta > 0 && atMax || delta < 0 && atMin) return stopRepeat();
|
|
121
|
+
stepBy(delta);
|
|
122
|
+
tick(Math.max(REPEAT_MIN_MS, interval * .8));
|
|
123
|
+
}, interval);
|
|
124
|
+
};
|
|
125
|
+
stopRepeat();
|
|
126
|
+
repeat = setTimeout(() => tick(REPEAT_START_MS), REPEAT_DELAY_MS - REPEAT_START_MS);
|
|
127
|
+
}
|
|
128
|
+
/** A click that no pointer press preceded (`detail === 0`): assistive technology, step once. */
|
|
129
|
+
function clickStep(event, delta) {
|
|
130
|
+
if (event.detail === 0) stepBy(delta);
|
|
131
|
+
}
|
|
132
|
+
</script>
|
|
133
|
+
|
|
134
|
+
{#snippet stepper(delta: number, label: string, bounded: boolean)}
|
|
135
|
+
<button
|
|
136
|
+
type="button"
|
|
137
|
+
tabindex="-1"
|
|
138
|
+
aria-controls={id}
|
|
139
|
+
disabled={disabled || bounded}
|
|
140
|
+
class={['btn btn-square btn-ghost', inputButtonSize[size]]}
|
|
141
|
+
onpointerdown={(event) => startRepeat(event, delta)}
|
|
142
|
+
onpointerup={stopRepeat}
|
|
143
|
+
onpointercancel={stopRepeat}
|
|
144
|
+
onlostpointercapture={stopRepeat}
|
|
145
|
+
onclick={(event) => clickStep(event, delta)}
|
|
146
|
+
>
|
|
147
|
+
{#if delta < 0}
|
|
148
|
+
<Minus class="size-4" aria-hidden="true" />
|
|
149
|
+
{:else}
|
|
150
|
+
<Plus class="size-4" aria-hidden="true" />
|
|
151
|
+
{/if}
|
|
152
|
+
<span class="sr-only">{label}</span>
|
|
153
|
+
</button>
|
|
154
|
+
{/snippet}
|
|
155
|
+
|
|
156
|
+
<div class={['input w-full pe-1', inputSize[size], invalid && 'input-error', className]}>
|
|
157
|
+
<input
|
|
158
|
+
{...rest}
|
|
159
|
+
bind:this={input}
|
|
160
|
+
{id}
|
|
161
|
+
type="text"
|
|
162
|
+
role="spinbutton"
|
|
163
|
+
autocomplete="off"
|
|
164
|
+
inputmode={formatOptions?.maximumFractionDigits === 0 ? 'numeric' : 'decimal'}
|
|
165
|
+
{disabled}
|
|
166
|
+
aria-valuenow={value}
|
|
167
|
+
aria-valuemin={min}
|
|
168
|
+
aria-valuemax={max}
|
|
169
|
+
aria-valuetext={value === undefined ? undefined : formatted}
|
|
170
|
+
aria-describedby={describedBy === '' ? undefined : describedBy}
|
|
171
|
+
aria-invalid={invalid ? 'true' : undefined}
|
|
172
|
+
bind:value={text}
|
|
173
|
+
onkeydown={(event) => {
|
|
174
|
+
handleKeydown(event);
|
|
175
|
+
onkeydown?.(event);
|
|
176
|
+
}}
|
|
177
|
+
onblur={(event) => {
|
|
178
|
+
stopRepeat();
|
|
179
|
+
commitText();
|
|
180
|
+
onblur?.(event);
|
|
181
|
+
}}
|
|
182
|
+
/>
|
|
183
|
+
{#if showStepper}
|
|
184
|
+
{@render stepper(-step, decrementLabel, atMin)}
|
|
185
|
+
{@render stepper(step, incrementLabel, atMax)}
|
|
186
|
+
{/if}
|
|
187
|
+
{#if name !== undefined}
|
|
188
|
+
<input type="hidden" {name} value={value === undefined ? '' : String(value)} {disabled} />
|
|
189
|
+
{/if}
|
|
190
|
+
</div>
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
2
|
+
import { type FieldSize } from './field-types.ts';
|
|
3
|
+
import { type NumberLocales } from './number-format.ts';
|
|
4
|
+
interface Props extends Omit<HTMLInputAttributes, 'value' | 'type' | 'size' | 'min' | 'max' | 'step' | 'inputmode' | 'children'> {
|
|
5
|
+
/** The number, or `undefined` while the field is empty. Bindable. */
|
|
6
|
+
value?: number | undefined;
|
|
7
|
+
/** The lowest value; also what Home goes to. */
|
|
8
|
+
min?: number | undefined;
|
|
9
|
+
/** The highest value; also what End goes to. */
|
|
10
|
+
max?: number | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* Increment for the buttons and the arrow keys.
|
|
13
|
+
* @defaultValue `1`
|
|
14
|
+
*/
|
|
15
|
+
step?: number | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* Increment for Page Up / Page Down.
|
|
18
|
+
* @defaultValue `step * 10`
|
|
19
|
+
*/
|
|
20
|
+
largeStep?: number | undefined;
|
|
21
|
+
/** How the value is displayed and parsed, e.g. `{ style: 'unit', unit: 'centimeter' }`, `{ style: 'percent' }`, `{ style: 'currency', currency: 'AUD' }`. */
|
|
22
|
+
formatOptions?: Intl.NumberFormatOptions | undefined;
|
|
23
|
+
/** BCP 47 locale(s) to format and parse in; falls back to the context's `locale()`. */
|
|
24
|
+
locales?: NumberLocales;
|
|
25
|
+
/**
|
|
26
|
+
* Show the − / + buttons.
|
|
27
|
+
* @defaultValue `true`
|
|
28
|
+
*/
|
|
29
|
+
showStepper?: boolean | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* Accessible name of the − button.
|
|
32
|
+
* @defaultValue `'Decrease'`
|
|
33
|
+
*/
|
|
34
|
+
decrementLabel?: string | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Accessible name of the + button.
|
|
37
|
+
* @defaultValue `'Increase'`
|
|
38
|
+
*/
|
|
39
|
+
incrementLabel?: string | undefined;
|
|
40
|
+
/** Called with the committed number on blur, Enter or a step — not on every keystroke. */
|
|
41
|
+
onCommit?: ((value: number | undefined) => void) | undefined;
|
|
42
|
+
/** Posts the raw number (not the formatted text) under this name, through a hidden input. */
|
|
43
|
+
name?: string | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* daisyUI size step.
|
|
46
|
+
* @defaultValue `'md'`
|
|
47
|
+
*/
|
|
48
|
+
size?: FieldSize | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* Disables the input and both buttons.
|
|
51
|
+
* @defaultValue `false`
|
|
52
|
+
*/
|
|
53
|
+
disabled?: boolean | undefined;
|
|
54
|
+
/** `FieldControl`: the input's `id`, which `FormField`'s label points at. Minted when omitted. */
|
|
55
|
+
id?: string | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* `FieldControl`: space-separated ids for `aria-describedby`.
|
|
58
|
+
* @defaultValue `''`
|
|
59
|
+
*/
|
|
60
|
+
describedBy?: string | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* `FieldControl`: the visible label's id. Not used — the input is
|
|
63
|
+
* labelable, so the `FormField`'s `<label for>` already names it; taken
|
|
64
|
+
* here so a spread `FieldControl` does not leak it as an attribute.
|
|
65
|
+
*/
|
|
66
|
+
labelId?: string | undefined;
|
|
67
|
+
/**
|
|
68
|
+
* `FieldControl`: the field is in error — sets `aria-invalid` and `input-error`.
|
|
69
|
+
* @defaultValue `false`
|
|
70
|
+
*/
|
|
71
|
+
invalid?: boolean | undefined;
|
|
72
|
+
}
|
|
73
|
+
/** An exact number the reader types or nudges, shown locale-formatted (units, percent, currency) with − / + stepper buttons: the APG spinbutton. */
|
|
74
|
+
declare const NumberField: import("svelte").Component<Props, {}, "value">;
|
|
75
|
+
type NumberField = ReturnType<typeof NumberField>;
|
|
76
|
+
export default NumberField;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<!-- @component
|
|
2
|
+
A password field with a "Show password" toggle and the `autocomplete` a password manager needs to fill or generate it.
|
|
3
|
+
-->
|
|
4
|
+
<!--
|
|
5
|
+
PasswordInput — a `TextInput`-styled password field inside a daisyUI
|
|
6
|
+
`input` composite, with a trailing toggle that flips the input between
|
|
7
|
+
`type="password"` and `type="text"`, and a `purpose` prop that sets
|
|
8
|
+
`autocomplete` to `current-password` or `new-password`, which is what makes
|
|
9
|
+
a password manager fill (sign-in) or offer to generate (sign-up).
|
|
10
|
+
|
|
11
|
+
Why not a `revealable` prop on `TextInput`: `TextInput` IS a bare `<input>`,
|
|
12
|
+
and a toggle needs a wrapper and a button, which would move where `class`
|
|
13
|
+
and `...rest` land for every existing caller. So this follows `SearchInput`,
|
|
14
|
+
the composite-input precedent: a `<div class="input">` (not a `<label>`,
|
|
15
|
+
which would fold the toggle's name into the input's accessible name when
|
|
16
|
+
the field sits inside `FormField`), the native input inside it, a trailing
|
|
17
|
+
button. `...rest` (`id`, `name`, `minlength`, `aria-*`, `data-*`) goes on
|
|
18
|
+
the inner input, the element a label, a test and a form need; `class` goes
|
|
19
|
+
on the wrapper and `inputClass` on the input. `passwordrules` is passed
|
|
20
|
+
through when given, never defaulted.
|
|
21
|
+
|
|
22
|
+
ARIA contract: the toggle is an APG button with `aria-pressed` — a real
|
|
23
|
+
`<button type="button">` (so it never submits the form) with a CONSTANT
|
|
24
|
+
name, `showLabel`, and a pressed state ("Show password, toggle button,
|
|
25
|
+
pressed"), rather than a name that swaps between "Show" and "Hide", which
|
|
26
|
+
some screen readers do not re-announce. It `aria-controls` the input; its
|
|
27
|
+
icon is `aria-hidden`. Activating it leaves focus on the toggle and keeps
|
|
28
|
+
the input's selection range across the type flip. The input keeps native
|
|
29
|
+
semantics plus `FieldControl`'s `aria-describedby`, `aria-invalid` and
|
|
30
|
+
`required`.
|
|
31
|
+
|
|
32
|
+
The field is hidden again when its form submits, so a revealed password
|
|
33
|
+
does not survive into the browser's form-restore cache.
|
|
34
|
+
-->
|
|
35
|
+
<script lang="ts">import { Eye, EyeOff } from "@lucide/svelte";
|
|
36
|
+
import { inputButtonSize, inputSize } from "./field-types.js";
|
|
37
|
+
let { value = $bindable(""), purpose = "current", revealed = $bindable(false), revealable = true, showLabel = "Show password", size = "md", inputClass, id: idProp, describedBy = "", labelId: _labelId, invalid = false, class: className, ...rest } = $props();
|
|
38
|
+
const uid = $props.id();
|
|
39
|
+
const id = $derived(idProp ?? uid);
|
|
40
|
+
let input = $state(null);
|
|
41
|
+
/**
|
|
42
|
+
* Flip `revealed`, keeping the caret / selection where it was. Chromium
|
|
43
|
+
* rebuilds a text field's inner editor when its `type` changes and drops the
|
|
44
|
+
* selection at the next rendering step, so the range is put back in the
|
|
45
|
+
* following animation frame.
|
|
46
|
+
*/
|
|
47
|
+
function toggle() {
|
|
48
|
+
const start = input?.selectionStart ?? null;
|
|
49
|
+
const end = input?.selectionEnd ?? null;
|
|
50
|
+
revealed = !revealed;
|
|
51
|
+
if (start === null || end === null) return;
|
|
52
|
+
requestAnimationFrame(() => input?.setSelectionRange(start, end));
|
|
53
|
+
}
|
|
54
|
+
// Hide again on submit, so a revealed password is not restored with the form.
|
|
55
|
+
$effect(() => {
|
|
56
|
+
const form = input?.form;
|
|
57
|
+
if (form === null || form === undefined) return;
|
|
58
|
+
const hide = () => {
|
|
59
|
+
revealed = false;
|
|
60
|
+
};
|
|
61
|
+
form.addEventListener("submit", hide);
|
|
62
|
+
return () => form.removeEventListener("submit", hide);
|
|
63
|
+
});
|
|
64
|
+
</script>
|
|
65
|
+
|
|
66
|
+
<div class={['input w-full', inputSize[size], invalid && 'input-error', revealable && 'pe-1', className]}>
|
|
67
|
+
<input
|
|
68
|
+
{...rest}
|
|
69
|
+
bind:this={input}
|
|
70
|
+
{id}
|
|
71
|
+
type={revealed ? 'text' : 'password'}
|
|
72
|
+
autocomplete={purpose === 'new' ? 'new-password' : 'current-password'}
|
|
73
|
+
bind:value
|
|
74
|
+
aria-invalid={invalid ? 'true' : undefined}
|
|
75
|
+
aria-describedby={describedBy === '' ? undefined : describedBy}
|
|
76
|
+
class={inputClass}
|
|
77
|
+
/>
|
|
78
|
+
{#if revealable}
|
|
79
|
+
<button
|
|
80
|
+
type="button"
|
|
81
|
+
aria-pressed={revealed}
|
|
82
|
+
aria-controls={id}
|
|
83
|
+
disabled={rest.disabled === true}
|
|
84
|
+
class={['btn btn-square btn-ghost', inputButtonSize[size]]}
|
|
85
|
+
onclick={toggle}
|
|
86
|
+
>
|
|
87
|
+
{#if revealed}
|
|
88
|
+
<EyeOff class="size-4" aria-hidden="true" />
|
|
89
|
+
{:else}
|
|
90
|
+
<Eye class="size-4" aria-hidden="true" />
|
|
91
|
+
{/if}
|
|
92
|
+
<span class="sr-only">{showLabel}</span>
|
|
93
|
+
</button>
|
|
94
|
+
{/if}
|
|
95
|
+
</div>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
2
|
+
import { type FieldSize } from './field-types.ts';
|
|
3
|
+
interface Props extends Omit<HTMLInputAttributes, 'type' | 'value' | 'size' | 'autocomplete'> {
|
|
4
|
+
/**
|
|
5
|
+
* The password. Bindable.
|
|
6
|
+
* @defaultValue `''`
|
|
7
|
+
*/
|
|
8
|
+
value?: string | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* Which password: sets `autocomplete` to `current-password` (sign-in) or `new-password` (sign-up, change password).
|
|
11
|
+
* @defaultValue `'current'`
|
|
12
|
+
*/
|
|
13
|
+
purpose?: 'current' | 'new' | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* Whether the text is currently shown. Bindable.
|
|
16
|
+
* @defaultValue `false`
|
|
17
|
+
*/
|
|
18
|
+
revealed?: boolean | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Render the show/hide toggle; without it the field is a styled `type="password"`.
|
|
21
|
+
* @defaultValue `true`
|
|
22
|
+
*/
|
|
23
|
+
revealable?: boolean | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* The toggle's accessible name; its `aria-pressed` state says whether it is on.
|
|
26
|
+
* @defaultValue `'Show password'`
|
|
27
|
+
*/
|
|
28
|
+
showLabel?: string | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* daisyUI size step.
|
|
31
|
+
* @defaultValue `'md'`
|
|
32
|
+
*/
|
|
33
|
+
size?: FieldSize | undefined;
|
|
34
|
+
/** Extra classes on the inner `<input>`; `class` goes on the wrapper. */
|
|
35
|
+
inputClass?: string | undefined;
|
|
36
|
+
/** `FieldControl`: the input's `id`, which `FormField`'s label points at. Minted when omitted. */
|
|
37
|
+
id?: string | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* `FieldControl`: space-separated ids for `aria-describedby`.
|
|
40
|
+
* @defaultValue `''`
|
|
41
|
+
*/
|
|
42
|
+
describedBy?: string | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* `FieldControl`: the visible label's id. Not used — the input is
|
|
45
|
+
* labelable, so the `FormField`'s `<label for>` already names it; taken
|
|
46
|
+
* here so a spread `FieldControl` does not leak it as an attribute.
|
|
47
|
+
*/
|
|
48
|
+
labelId?: string | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* `FieldControl`: the field is in error — sets `aria-invalid` and `input-error`.
|
|
51
|
+
* @defaultValue `false`
|
|
52
|
+
*/
|
|
53
|
+
invalid?: boolean | undefined;
|
|
54
|
+
}
|
|
55
|
+
/** A password field with a "Show password" toggle and the `autocomplete` a password manager needs to fill or generate it. */
|
|
56
|
+
declare const PasswordInput: import("svelte").Component<Props, {}, "value" | "revealed">;
|
|
57
|
+
type PasswordInput = ReturnType<typeof PasswordInput>;
|
|
58
|
+
export default PasswordInput;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<!-- @component
|
|
2
|
+
The trigger and calendar popover `DatePicker` and `DateRangePicker` share; internal, not exported.
|
|
3
|
+
-->
|
|
4
|
+
<!--
|
|
5
|
+
PickerPopover — the calendar button that sits in a picker's shell, and
|
|
6
|
+
the popover it opens. `parts` is `DatePicker` or `DateRangePicker`; both
|
|
7
|
+
expose `Trigger`, `Portal`, `Content` and `Calendar` with the same props,
|
|
8
|
+
and their calendar body is `CalendarMonths`, the same one `Calendar`
|
|
9
|
+
renders inline.
|
|
10
|
+
|
|
11
|
+
The trigger is icon-only, so it is named by `triggerLabel` ("Choose
|
|
12
|
+
date"), the glyph `aria-hidden`; bits-ui gives it `aria-haspopup="dialog"`
|
|
13
|
+
and `aria-expanded`. The popover is a `role="dialog"` named by
|
|
14
|
+
`calendarLabel`; bits-ui moves focus into the grid, closes on Escape and
|
|
15
|
+
returns focus to the trigger.
|
|
16
|
+
-->
|
|
17
|
+
<script lang="ts">import { Calendar as CalendarIcon } from "@lucide/svelte";
|
|
18
|
+
import { Portal } from "bits-ui";
|
|
19
|
+
import CalendarMonths from "./CalendarMonths.svelte";
|
|
20
|
+
let { parts, triggerLabel, calendarLabel, months, disabled } = $props();
|
|
21
|
+
// SAFETY: both namespaces' Trigger / Content / Calendar are the same
|
|
22
|
+
// bits-ui popover and calendar parts under two names. (`DateRangePicker`
|
|
23
|
+
// exports no `Portal`, so the shared utility is used for both.)
|
|
24
|
+
const P = $derived(parts);
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<P.Trigger {disabled}>
|
|
28
|
+
{#snippet child({ props })}
|
|
29
|
+
<button {...props} type="button" class="btn btn-ghost btn-xs btn-square ms-1" aria-label={triggerLabel}>
|
|
30
|
+
<CalendarIcon class="size-4" aria-hidden="true" />
|
|
31
|
+
</button>
|
|
32
|
+
{/snippet}
|
|
33
|
+
</P.Trigger>
|
|
34
|
+
<Portal>
|
|
35
|
+
<P.Content
|
|
36
|
+
sideOffset={6}
|
|
37
|
+
role="dialog"
|
|
38
|
+
aria-label={calendarLabel}
|
|
39
|
+
class="card card-border bg-base-100 rounded-box z-50 p-3 shadow-md"
|
|
40
|
+
>
|
|
41
|
+
<P.Calendar>
|
|
42
|
+
{#snippet children({ months: grid, weekdays })}
|
|
43
|
+
<CalendarMonths {parts} {...months} months={grid} {weekdays} />
|
|
44
|
+
{/snippet}
|
|
45
|
+
</P.Calendar>
|
|
46
|
+
</P.Content>
|
|
47
|
+
</Portal>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type DatePicker, type DateRangePicker } from 'bits-ui';
|
|
2
|
+
interface Props {
|
|
3
|
+
parts: typeof DatePicker | typeof DateRangePicker;
|
|
4
|
+
triggerLabel: string;
|
|
5
|
+
calendarLabel: string;
|
|
6
|
+
months: {
|
|
7
|
+
readonly locale: string;
|
|
8
|
+
readonly previousLabel: string;
|
|
9
|
+
readonly nextLabel: string;
|
|
10
|
+
};
|
|
11
|
+
disabled: boolean;
|
|
12
|
+
}
|
|
13
|
+
/** The trigger and calendar popover `DatePicker` and `DateRangePicker` share; internal, not exported. */
|
|
14
|
+
declare const PickerPopover: import("svelte").Component<Props, {}, "">;
|
|
15
|
+
type PickerPopover = ReturnType<typeof PickerPopover>;
|
|
16
|
+
export default PickerPopover;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
<!-- @component
|
|
2
|
+
A fixed-length code — a sign-in code, a TOTP, an invite code — typed one character per visible cell into one real input, on bits-ui's `PinInput`.
|
|
3
|
+
-->
|
|
4
|
+
<!--
|
|
5
|
+
PinInput — one real `<input>` (bits-ui `PinInput`) drawn as a row of
|
|
6
|
+
cells. Being one input is the whole point: paste, SMS autofill
|
|
7
|
+
(`autocomplete="one-time-code"`), password managers and form posting all
|
|
8
|
+
treat it as one field, and a screen reader hears one text field rather
|
|
9
|
+
than six unlabelled one-character edits — the failure mode of the
|
|
10
|
+
hand-rolled six-`<input maxlength=1>` pattern.
|
|
11
|
+
|
|
12
|
+
ARIA contract: there is no APG pattern for segmented code entry; the
|
|
13
|
+
contract is "a single text input". The input carries the `FieldControl` id
|
|
14
|
+
(bits-ui `inputId`), so `FormField`'s `<label for>` names it, plus
|
|
15
|
+
`aria-describedby`, `aria-invalid`, `required`, `inputmode`,
|
|
16
|
+
`autocomplete` and `maxlength`. The cells are `aria-hidden` presentation
|
|
17
|
+
and nothing in them is focusable. The keyboard is the platform's: typing
|
|
18
|
+
advances, Backspace deletes, arrows move the caret, paste fills the code;
|
|
19
|
+
bits-ui draws the fake caret in the active cell.
|
|
20
|
+
|
|
21
|
+
`charset` maps to `inputmode` and to bits-ui's own patterns
|
|
22
|
+
(`REGEXP_ONLY_DIGITS` / `REGEXP_ONLY_DIGITS_AND_CHARS`), which both reject
|
|
23
|
+
a keystroke that does not match and become the input's `pattern`.
|
|
24
|
+
`pasteTransformer` defaults to stripping whitespace and hyphens, so a code
|
|
25
|
+
pasted from an email as "123 456" or "123-456" lands as "123456".
|
|
26
|
+
|
|
27
|
+
Styling decision (the issue asked for one to be recorded): bits-ui's
|
|
28
|
+
explicit `PinInput.Cell` elements, styled with `input`-like utilities and
|
|
29
|
+
the literal `pinCellSize` table, rather than daisyUI `otp` on the hidden
|
|
30
|
+
input. bits-ui renders the real input transparent over the cells, so
|
|
31
|
+
`otp`'s CSS-drawn cells would sit on an invisible element, and bits-ui's
|
|
32
|
+
fake caret and its password-manager badge offset
|
|
33
|
+
(`pushPasswordManagerStrategy`) are positioned against the cells.
|
|
34
|
+
|
|
35
|
+
`masked` renders `•` in each filled cell in place of the character (bits-ui
|
|
36
|
+
2.20 has no masking prop). The input itself is unchanged — it keeps
|
|
37
|
+
`autocomplete` so a password manager can still fill it, and it is
|
|
38
|
+
transparent, so the characters are never drawn.
|
|
39
|
+
-->
|
|
40
|
+
<script lang="ts">import { PinInput, REGEXP_ONLY_DIGITS, REGEXP_ONLY_DIGITS_AND_CHARS } from "bits-ui";
|
|
41
|
+
import { pinCellSize } from "./field-types.js";
|
|
42
|
+
let { value = $bindable(""), length = 6, charset = "numeric", masked = false, onComplete, pasteTransformer = (text) => text.replace(/[\s-]/g, ""), autocomplete = "one-time-code", size = "md", disabled = false, id, describedBy = "", labelId: _labelId, invalid = false, class: className, ...rest } = $props();
|
|
43
|
+
const uid = $props.id();
|
|
44
|
+
const inputId = $derived(id ?? uid);
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
<PinInput.Root
|
|
48
|
+
{...rest}
|
|
49
|
+
bind:value
|
|
50
|
+
{inputId}
|
|
51
|
+
maxlength={length}
|
|
52
|
+
pattern={charset === 'numeric' ? REGEXP_ONLY_DIGITS : REGEXP_ONLY_DIGITS_AND_CHARS}
|
|
53
|
+
inputmode={charset === 'numeric' ? 'numeric' : 'text'}
|
|
54
|
+
{autocomplete}
|
|
55
|
+
{disabled}
|
|
56
|
+
{pasteTransformer}
|
|
57
|
+
onComplete={(code: string) => onComplete?.(code)}
|
|
58
|
+
aria-describedby={describedBy === '' ? undefined : describedBy}
|
|
59
|
+
aria-invalid={invalid ? 'true' : undefined}
|
|
60
|
+
class={['flex items-center gap-2 has-disabled:opacity-50', className]}
|
|
61
|
+
>
|
|
62
|
+
{#snippet children({ cells })}
|
|
63
|
+
{#each cells as cell, index (index)}
|
|
64
|
+
<PinInput.Cell
|
|
65
|
+
{cell}
|
|
66
|
+
aria-hidden="true"
|
|
67
|
+
class={[
|
|
68
|
+
'relative flex items-center justify-center rounded-field border bg-base-100 font-mono',
|
|
69
|
+
'data-active:outline-2 data-active:outline-offset-2',
|
|
70
|
+
invalid ? 'border-error data-active:outline-error' : 'border-base-content/20 data-active:outline-primary',
|
|
71
|
+
pinCellSize[size],
|
|
72
|
+
]}
|
|
73
|
+
>
|
|
74
|
+
{#if cell.char !== null && cell.char !== undefined}
|
|
75
|
+
{masked ? '•' : cell.char}
|
|
76
|
+
{/if}
|
|
77
|
+
{#if cell.hasFakeCaret}
|
|
78
|
+
<span class="h-1/2 w-px animate-pulse bg-base-content motion-reduce:animate-none"></span>
|
|
79
|
+
{/if}
|
|
80
|
+
</PinInput.Cell>
|
|
81
|
+
{/each}
|
|
82
|
+
{/snippet}
|
|
83
|
+
</PinInput.Root>
|