@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.
Files changed (240) hide show
  1. package/README.md +241 -56
  2. package/dist/canvas/VectorCanvas.svelte +89 -8
  3. package/dist/canvas/VectorCanvas.svelte.d.ts +19 -1
  4. package/dist/canvas/types.d.ts +1 -1
  5. package/dist/charts/types.d.ts +1 -1
  6. package/dist/citations/list-format.js +3 -1
  7. package/dist/data/ChatBubble.svelte +48 -0
  8. package/dist/data/ChatBubble.svelte.d.ts +33 -0
  9. package/dist/data/DescriptionList.svelte +9 -3
  10. package/dist/data/DescriptionList.svelte.d.ts +1 -1
  11. package/dist/data/DescriptionRow.svelte +6 -5
  12. package/dist/data/MessageThread.svelte +29 -0
  13. package/dist/data/MessageThread.svelte.d.ts +17 -0
  14. package/dist/data/Pagination.svelte +5 -5
  15. package/dist/data/Pagination.svelte.d.ts +8 -0
  16. package/dist/data/PaginationFooter.svelte +2 -1
  17. package/dist/data/PaginationFooter.svelte.d.ts +2 -0
  18. package/dist/data/Table.svelte +9 -3
  19. package/dist/data/description-context.d.ts +12 -6
  20. package/dist/data/description-context.js +6 -5
  21. package/dist/data/index.d.ts +2 -0
  22. package/dist/data/index.js +2 -0
  23. package/dist/data/table-types.d.ts +7 -0
  24. package/dist/feedback/Dialog.svelte +19 -49
  25. package/dist/feedback/Dialog.svelte.d.ts +0 -1
  26. package/dist/feedback/DialogFrame.svelte +64 -0
  27. package/dist/feedback/DialogFrame.svelte.d.ts +23 -0
  28. package/dist/feedback/Drawer.svelte +43 -0
  29. package/dist/feedback/Drawer.svelte.d.ts +43 -0
  30. package/dist/feedback/SegmentBar.svelte.d.ts +1 -0
  31. package/dist/feedback/StatTile.svelte.d.ts +1 -0
  32. package/dist/feedback/Steps.svelte +72 -0
  33. package/dist/feedback/Steps.svelte.d.ts +61 -0
  34. package/dist/feedback/Timeline.svelte.d.ts +3 -0
  35. package/dist/feedback/index.d.ts +3 -0
  36. package/dist/feedback/index.js +2 -0
  37. package/dist/feedback/sizes.d.ts +14 -0
  38. package/dist/feedback/sizes.js +28 -0
  39. package/dist/fields/Calendar.svelte +60 -0
  40. package/dist/fields/Calendar.svelte.d.ts +11 -0
  41. package/dist/fields/CalendarMonths.svelte +86 -0
  42. package/dist/fields/CalendarMonths.svelte.d.ts +30 -0
  43. package/dist/fields/Checkbox.svelte +1 -1
  44. package/dist/fields/Checkbox.svelte.d.ts +6 -0
  45. package/dist/fields/Combobox.svelte +61 -54
  46. package/dist/fields/Combobox.svelte.d.ts +38 -3
  47. package/dist/fields/DateField.svelte +37 -0
  48. package/dist/fields/DateField.svelte.d.ts +20 -0
  49. package/dist/fields/DateInput.svelte +1 -1
  50. package/dist/fields/DateInput.svelte.d.ts +6 -0
  51. package/dist/fields/DatePicker.svelte +47 -0
  52. package/dist/fields/DatePicker.svelte.d.ts +25 -0
  53. package/dist/fields/DateRangePicker.svelte +70 -0
  54. package/dist/fields/DateRangePicker.svelte.d.ts +43 -0
  55. package/dist/fields/Dropzone.svelte +232 -0
  56. package/dist/fields/Dropzone.svelte.d.ts +103 -0
  57. package/dist/fields/Fieldset.svelte +7 -2
  58. package/dist/fields/Fieldset.svelte.d.ts +3 -2
  59. package/dist/fields/FileInput.svelte +3 -2
  60. package/dist/fields/FileInput.svelte.d.ts +6 -0
  61. package/dist/fields/FormField.svelte +27 -3
  62. package/dist/fields/ImageInput.svelte.d.ts +6 -0
  63. package/dist/fields/MultiSelect.svelte +112 -0
  64. package/dist/fields/MultiSelect.svelte.d.ts +75 -0
  65. package/dist/fields/NumberField.svelte +190 -0
  66. package/dist/fields/NumberField.svelte.d.ts +76 -0
  67. package/dist/fields/PasswordInput.svelte +95 -0
  68. package/dist/fields/PasswordInput.svelte.d.ts +58 -0
  69. package/dist/fields/PickerPopover.svelte +47 -0
  70. package/dist/fields/PickerPopover.svelte.d.ts +16 -0
  71. package/dist/fields/PinInput.svelte +83 -0
  72. package/dist/fields/PinInput.svelte.d.ts +66 -0
  73. package/dist/fields/RadioGroup.svelte +3 -0
  74. package/dist/fields/RangeCalendar.svelte +51 -0
  75. package/dist/fields/RangeCalendar.svelte.d.ts +23 -0
  76. package/dist/fields/Rating.svelte +127 -0
  77. package/dist/fields/Rating.svelte.d.ts +86 -0
  78. package/dist/fields/SearchInput.svelte +2 -1
  79. package/dist/fields/SegmentGroup.svelte +91 -0
  80. package/dist/fields/SegmentGroup.svelte.d.ts +20 -0
  81. package/dist/fields/Select.svelte +1 -1
  82. package/dist/fields/Select.svelte.d.ts +6 -0
  83. package/dist/fields/Slider.svelte +194 -0
  84. package/dist/fields/Slider.svelte.d.ts +100 -0
  85. package/dist/fields/TagsInput.svelte +256 -0
  86. package/dist/fields/TagsInput.svelte.d.ts +102 -0
  87. package/dist/fields/TextInput.svelte +1 -1
  88. package/dist/fields/TextInput.svelte.d.ts +6 -0
  89. package/dist/fields/Textarea.svelte +1 -1
  90. package/dist/fields/Textarea.svelte.d.ts +6 -0
  91. package/dist/fields/TimeField.svelte +30 -0
  92. package/dist/fields/TimeField.svelte.d.ts +19 -0
  93. package/dist/fields/Toggle.svelte +1 -1
  94. package/dist/fields/Toggle.svelte.d.ts +6 -0
  95. package/dist/fields/VideoInput.svelte.d.ts +6 -0
  96. package/dist/fields/calendar-props.d.ts +119 -0
  97. package/dist/fields/calendar-props.js +51 -0
  98. package/dist/fields/dates.d.ts +28 -0
  99. package/dist/fields/dates.js +26 -0
  100. package/dist/fields/field-types.d.ts +29 -0
  101. package/dist/fields/field-types.js +32 -0
  102. package/dist/fields/file-validation.d.ts +30 -0
  103. package/dist/fields/file-validation.js +51 -0
  104. package/dist/fields/index.d.ts +9 -0
  105. package/dist/fields/index.js +8 -0
  106. package/dist/fields/latest-search.svelte.d.ts +38 -0
  107. package/dist/fields/latest-search.svelte.js +85 -0
  108. package/dist/fields/number-format.d.ts +52 -0
  109. package/dist/fields/number-format.js +89 -0
  110. package/dist/fields/segmented-field.d.ts +128 -0
  111. package/dist/fields/segmented-field.js +52 -0
  112. package/dist/fields/slider-keys.d.ts +21 -0
  113. package/dist/fields/slider-keys.js +37 -0
  114. package/dist/formatting/Countdown.svelte +110 -0
  115. package/dist/formatting/Countdown.svelte.d.ts +35 -0
  116. package/dist/formatting/Duration.svelte +18 -0
  117. package/dist/formatting/Duration.svelte.d.ts +20 -0
  118. package/dist/formatting/Highlight.svelte +4 -0
  119. package/dist/formatting/Highlight.svelte.d.ts +4 -0
  120. package/dist/formatting/duration.d.ts +44 -0
  121. package/dist/formatting/duration.js +125 -0
  122. package/dist/formatting/index.d.ts +3 -0
  123. package/dist/formatting/index.js +3 -0
  124. package/dist/forms/EntityForm.svelte +53 -45
  125. package/dist/forms/EntityForm.svelte.d.ts +12 -3
  126. package/dist/hotkeys/index.d.ts +5 -0
  127. package/dist/hotkeys/index.js +5 -0
  128. package/dist/hotkeys/match.js +12 -10
  129. package/dist/hotkeys/types.d.ts +4 -2
  130. package/dist/index.d.ts +2 -1
  131. package/dist/index.js +2 -1
  132. package/dist/infobox/Infobox.svelte +2 -2
  133. package/dist/infobox/Infobox.svelte.d.ts +7 -0
  134. package/dist/infobox/InfoboxHeader.svelte +15 -7
  135. package/dist/infobox/InfoboxHeader.svelte.d.ts +7 -0
  136. package/dist/infobox/types.d.ts +5 -5
  137. package/dist/layout/AppShell.svelte +50 -0
  138. package/dist/layout/AppShell.svelte.d.ts +28 -0
  139. package/dist/layout/Cluster.svelte +28 -0
  140. package/dist/layout/Cluster.svelte.d.ts +27 -0
  141. package/dist/layout/Fits.svelte +27 -0
  142. package/dist/layout/Fits.svelte.d.ts +13 -0
  143. package/dist/layout/Flow.svelte +46 -0
  144. package/dist/layout/Flow.svelte.d.ts +16 -0
  145. package/dist/layout/Grid.svelte +23 -0
  146. package/dist/layout/Grid.svelte.d.ts +26 -0
  147. package/dist/layout/HStack.svelte +12 -0
  148. package/dist/layout/HStack.svelte.d.ts +34 -0
  149. package/dist/layout/Hero.svelte +76 -0
  150. package/dist/layout/Hero.svelte.d.ts +36 -0
  151. package/dist/layout/Navbar.svelte +2 -2
  152. package/dist/layout/Sidebar.svelte +1 -1
  153. package/dist/layout/Spacer.svelte +12 -0
  154. package/dist/layout/Spacer.svelte.d.ts +6 -0
  155. package/dist/layout/VStack.svelte +12 -0
  156. package/dist/layout/VStack.svelte.d.ts +29 -0
  157. package/dist/layout/index.d.ts +9 -0
  158. package/dist/layout/index.js +9 -0
  159. package/dist/layout/space.d.ts +44 -0
  160. package/dist/layout/space.js +76 -0
  161. package/dist/search/types.d.ts +3 -0
  162. package/dist/seo/og-image.d.ts +4 -3
  163. package/dist/seo/og-image.js +4 -3
  164. package/dist/tips/tip-stack.d.ts +1 -0
  165. package/dist/tips/tip-store.svelte.d.ts +2 -1
  166. package/dist/ui/Accordion.svelte +75 -0
  167. package/dist/ui/Accordion.svelte.d.ts +50 -0
  168. package/dist/ui/AccordionItem.svelte +73 -0
  169. package/dist/ui/AccordionItem.svelte.d.ts +18 -0
  170. package/dist/ui/Button.svelte +3 -2
  171. package/dist/ui/ButtonGroup.svelte +24 -0
  172. package/dist/ui/ButtonGroup.svelte.d.ts +17 -0
  173. package/dist/ui/ContextMenu.svelte +75 -0
  174. package/dist/ui/ContextMenu.svelte.d.ts +25 -0
  175. package/dist/ui/Disclosure.svelte +7 -0
  176. package/dist/ui/Disclosure.svelte.d.ts +2 -2
  177. package/dist/ui/Divider.svelte +47 -0
  178. package/dist/ui/Divider.svelte.d.ts +28 -0
  179. package/dist/ui/DropdownMenu.svelte +44 -0
  180. package/dist/ui/DropdownMenu.svelte.d.ts +30 -0
  181. package/dist/ui/Menu.svelte +17 -47
  182. package/dist/ui/Menu.svelte.d.ts +3 -24
  183. package/dist/ui/MenuCheckboxItem.svelte +33 -0
  184. package/dist/ui/MenuCheckboxItem.svelte.d.ts +20 -0
  185. package/dist/ui/MenuItem.svelte +29 -31
  186. package/dist/ui/MenuItem.svelte.d.ts +2 -2
  187. package/dist/ui/MenuRadioGroup.svelte +25 -0
  188. package/dist/ui/MenuRadioGroup.svelte.d.ts +20 -0
  189. package/dist/ui/MenuRadioItem.svelte +20 -0
  190. package/dist/ui/MenuRadioItem.svelte.d.ts +15 -0
  191. package/dist/ui/MenuSeparator.svelte +12 -11
  192. package/dist/ui/MenuSub.svelte +24 -0
  193. package/dist/ui/MenuSub.svelte.d.ts +15 -0
  194. package/dist/ui/Spinner.svelte +41 -0
  195. package/dist/ui/Spinner.svelte.d.ts +35 -0
  196. package/dist/ui/Stack.svelte +36 -0
  197. package/dist/ui/Stack.svelte.d.ts +48 -0
  198. package/dist/ui/ToggleButton.svelte +58 -0
  199. package/dist/ui/ToggleButton.svelte.d.ts +45 -0
  200. package/dist/ui/ToggleGroup.svelte +142 -0
  201. package/dist/ui/ToggleGroup.svelte.d.ts +86 -0
  202. package/dist/ui/Toolbar.svelte +40 -0
  203. package/dist/ui/Toolbar.svelte.d.ts +20 -0
  204. package/dist/ui/ToolbarButton.svelte +29 -0
  205. package/dist/ui/ToolbarButton.svelte.d.ts +21 -0
  206. package/dist/ui/ToolbarLink.svelte +21 -0
  207. package/dist/ui/ToolbarLink.svelte.d.ts +12 -0
  208. package/dist/ui/ToolbarSeparator.svelte +22 -0
  209. package/dist/ui/ToolbarSeparator.svelte.d.ts +12 -0
  210. package/dist/ui/ToolbarToggleGroup.svelte +66 -0
  211. package/dist/ui/ToolbarToggleGroup.svelte.d.ts +52 -0
  212. package/dist/ui/accordion-context.d.ts +26 -0
  213. package/dist/ui/accordion-context.js +23 -0
  214. package/dist/ui/context-menu.d.ts +13 -0
  215. package/dist/ui/context-menu.js +13 -0
  216. package/dist/ui/index.d.ts +19 -0
  217. package/dist/ui/index.js +18 -0
  218. package/dist/ui/menu-context.d.ts +13 -15
  219. package/dist/ui/menu-context.js +14 -18
  220. package/dist/ui/menu-parts/CheckboxItemPart.svelte +26 -0
  221. package/dist/ui/menu-parts/CheckboxItemPart.svelte.d.ts +5 -0
  222. package/dist/ui/menu-parts/ItemPart.svelte +29 -0
  223. package/dist/ui/menu-parts/ItemPart.svelte.d.ts +5 -0
  224. package/dist/ui/menu-parts/RadioGroupPart.svelte +24 -0
  225. package/dist/ui/menu-parts/RadioGroupPart.svelte.d.ts +5 -0
  226. package/dist/ui/menu-parts/RadioItemPart.svelte +22 -0
  227. package/dist/ui/menu-parts/RadioItemPart.svelte.d.ts +5 -0
  228. package/dist/ui/menu-parts/SeparatorPart.svelte +16 -0
  229. package/dist/ui/menu-parts/SeparatorPart.svelte.d.ts +5 -0
  230. package/dist/ui/menu-parts/SubPart.svelte +28 -0
  231. package/dist/ui/menu-parts/SubPart.svelte.d.ts +5 -0
  232. package/dist/ui/menu-parts/menu-parts.d.ts +104 -0
  233. package/dist/ui/menu-parts/menu-parts.js +31 -0
  234. package/dist/ui/menu-parts/parts.d.ts +12 -0
  235. package/dist/ui/menu-parts/parts.js +27 -0
  236. package/dist/ui/toggle-classes.d.ts +39 -0
  237. package/dist/ui/toggle-classes.js +36 -0
  238. package/dist/ui/tones.d.ts +35 -0
  239. package/dist/ui/tones.js +90 -0
  240. 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>