zabi-components 8.1.0 → 9.0.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 (273) hide show
  1. package/CHANGELOG.md +1011 -0
  2. package/README.md +1 -1
  3. package/THEME.md +18 -5
  4. package/THEMING.md +308 -74
  5. package/dist/atoms/ActionPanel.svelte +1 -1
  6. package/dist/atoms/Avatar.svelte +19 -3
  7. package/dist/atoms/Avatar.svelte.d.ts +12 -0
  8. package/dist/atoms/Badge.svelte +37 -8
  9. package/dist/atoms/Badge.svelte.d.ts +6 -0
  10. package/dist/atoms/Block.svelte +74 -0
  11. package/dist/atoms/Block.svelte.d.ts +29 -0
  12. package/dist/atoms/Button.svelte +52 -15
  13. package/dist/atoms/Button.svelte.d.ts +3 -2
  14. package/dist/atoms/Card.svelte +181 -25
  15. package/dist/atoms/Card.svelte.d.ts +55 -3
  16. package/dist/atoms/Checkbox.svelte +33 -0
  17. package/dist/atoms/Checkbox.svelte.d.ts +27 -0
  18. package/dist/atoms/Chip.svelte +351 -0
  19. package/dist/atoms/Chip.svelte.d.ts +57 -0
  20. package/dist/atoms/CodeField.svelte +385 -0
  21. package/dist/atoms/CodeField.svelte.d.ts +61 -0
  22. package/dist/atoms/ColorPicker.svelte +1 -0
  23. package/dist/atoms/DateField.svelte +41 -3
  24. package/dist/atoms/DateField.svelte.d.ts +31 -3
  25. package/dist/atoms/FloatingActionButton.svelte +5 -3
  26. package/dist/atoms/FloatingActionButton.svelte.d.ts +1 -1
  27. package/dist/atoms/Heading.svelte +27 -1
  28. package/dist/atoms/Heading.svelte.d.ts +7 -0
  29. package/dist/atoms/IconButton.svelte +81 -9
  30. package/dist/atoms/IconButton.svelte.d.ts +13 -0
  31. package/dist/atoms/Input.svelte +10 -5
  32. package/dist/atoms/Input.svelte.d.ts +3 -2
  33. package/dist/atoms/List.svelte +3 -2
  34. package/dist/atoms/ListItem.svelte +31 -7
  35. package/dist/atoms/ListItem.svelte.d.ts +6 -0
  36. package/dist/atoms/PickerField.svelte +239 -0
  37. package/dist/atoms/PickerField.svelte.d.ts +67 -0
  38. package/dist/atoms/Progress.svelte +78 -17
  39. package/dist/atoms/Progress.svelte.d.ts +11 -1
  40. package/dist/atoms/Radio.svelte +32 -0
  41. package/dist/atoms/Radio.svelte.d.ts +26 -0
  42. package/dist/atoms/Rating.svelte +132 -6
  43. package/dist/atoms/Rating.svelte.d.ts +24 -3
  44. package/dist/atoms/Select.svelte +24 -26
  45. package/dist/atoms/Select.svelte.d.ts +12 -0
  46. package/dist/atoms/SelectionControl.svelte +99 -6
  47. package/dist/atoms/SelectionControl.svelte.d.ts +25 -0
  48. package/dist/atoms/Skeleton.svelte +114 -18
  49. package/dist/atoms/Skeleton.svelte.d.ts +30 -3
  50. package/dist/atoms/SkeletonGroup.svelte +43 -0
  51. package/dist/atoms/SkeletonGroup.svelte.d.ts +20 -0
  52. package/dist/atoms/Slider.svelte +35 -0
  53. package/dist/atoms/Sparkline.svelte +109 -0
  54. package/dist/atoms/Sparkline.svelte.d.ts +27 -0
  55. package/dist/atoms/SpeechBubble.svelte +187 -0
  56. package/dist/atoms/SpeechBubble.svelte.d.ts +33 -0
  57. package/dist/atoms/Stat.svelte +72 -0
  58. package/dist/atoms/Stat.svelte.d.ts +19 -0
  59. package/dist/atoms/TemporalField.svelte +342 -10
  60. package/dist/atoms/TemporalField.svelte.d.ts +20 -4
  61. package/dist/atoms/Textarea.svelte +100 -5
  62. package/dist/atoms/Textarea.svelte.d.ts +21 -0
  63. package/dist/atoms/TimeColumn.svelte +150 -0
  64. package/dist/atoms/TimeColumn.svelte.d.ts +25 -0
  65. package/dist/atoms/TimeField.svelte +46 -3
  66. package/dist/atoms/TimeField.svelte.d.ts +34 -3
  67. package/dist/atoms/Toast.svelte +6 -5
  68. package/dist/atoms/Toggle.svelte +61 -10
  69. package/dist/atoms/Toggle.svelte.d.ts +15 -0
  70. package/dist/atoms/Tooltip.svelte +20 -1
  71. package/dist/atoms/Tooltip.svelte.d.ts +7 -0
  72. package/dist/atoms/index.d.ts +21 -0
  73. package/dist/atoms/index.js +17 -0
  74. package/dist/atoms/selection-control.styles.d.ts +15 -4
  75. package/dist/atoms/selection-control.styles.js +16 -5
  76. package/dist/components/atoms/index.d.ts +21 -0
  77. package/dist/components/atoms/index.d.ts.map +1 -1
  78. package/dist/components/atoms/selection-control.styles.d.ts +15 -4
  79. package/dist/components/atoms/selection-control.styles.d.ts.map +1 -1
  80. package/dist/components/index.d.ts +27 -3
  81. package/dist/components/index.d.ts.map +1 -1
  82. package/dist/components/map.d.ts +11 -0
  83. package/dist/components/map.d.ts.map +1 -0
  84. package/dist/components/molecules/index.d.ts +7 -2
  85. package/dist/components/molecules/index.d.ts.map +1 -1
  86. package/dist/components/molecules/toast-store.d.ts +30 -0
  87. package/dist/components/molecules/toast-store.d.ts.map +1 -1
  88. package/dist/components/organisms/index.d.ts +6 -1
  89. package/dist/components/organisms/index.d.ts.map +1 -1
  90. package/dist/components/types/variants.d.ts +12 -3
  91. package/dist/components/types/variants.d.ts.map +1 -1
  92. package/dist/components/util/app-shell.d.ts +55 -4
  93. package/dist/components/util/app-shell.d.ts.map +1 -1
  94. package/dist/components/util/avatar.d.ts +3 -0
  95. package/dist/components/util/avatar.d.ts.map +1 -1
  96. package/dist/components/util/bottom-tab-bar.d.ts +6 -0
  97. package/dist/components/util/bottom-tab-bar.d.ts.map +1 -1
  98. package/dist/components/util/chart.d.ts +140 -0
  99. package/dist/components/util/chart.d.ts.map +1 -0
  100. package/dist/components/util/chip.d.ts +66 -0
  101. package/dist/components/util/chip.d.ts.map +1 -0
  102. package/dist/components/util/cn.d.ts.map +1 -1
  103. package/dist/components/util/code-field.d.ts +63 -0
  104. package/dist/components/util/code-field.d.ts.map +1 -0
  105. package/dist/components/util/collapsible.d.ts +6 -0
  106. package/dist/components/util/collapsible.d.ts.map +1 -1
  107. package/dist/components/util/display-voice.d.ts +10 -0
  108. package/dist/components/util/display-voice.d.ts.map +1 -0
  109. package/dist/components/util/field-control.d.ts +30 -0
  110. package/dist/components/util/field-control.d.ts.map +1 -0
  111. package/dist/components/util/field.d.ts +6 -0
  112. package/dist/components/util/field.d.ts.map +1 -1
  113. package/dist/components/util/focus-utils.d.ts +2 -0
  114. package/dist/components/util/focus-utils.d.ts.map +1 -1
  115. package/dist/components/util/map-view.d.ts +144 -0
  116. package/dist/components/util/map-view.d.ts.map +1 -0
  117. package/dist/components/util/photo.d.ts +19 -0
  118. package/dist/components/util/photo.d.ts.map +1 -1
  119. package/dist/components/util/progress.d.ts +19 -0
  120. package/dist/components/util/progress.d.ts.map +1 -0
  121. package/dist/components/util/scroll-edge.d.ts +33 -0
  122. package/dist/components/util/scroll-edge.d.ts.map +1 -0
  123. package/dist/components/util/sheet-drag.d.ts +25 -1
  124. package/dist/components/util/sheet-drag.d.ts.map +1 -1
  125. package/dist/components/util/skeleton.d.ts +15 -0
  126. package/dist/components/util/skeleton.d.ts.map +1 -0
  127. package/dist/components/util/sliding-indicator.d.ts +25 -0
  128. package/dist/components/util/sliding-indicator.d.ts.map +1 -0
  129. package/dist/components/util/temporal-field.d.ts +89 -0
  130. package/dist/components/util/temporal-field.d.ts.map +1 -0
  131. package/dist/components/util/toaster.d.ts +40 -0
  132. package/dist/components/util/toaster.d.ts.map +1 -1
  133. package/dist/components/util/tone-surface.d.ts +34 -0
  134. package/dist/components/util/tone-surface.d.ts.map +1 -0
  135. package/dist/components/util/top-navbar.d.ts +26 -2
  136. package/dist/components/util/top-navbar.d.ts.map +1 -1
  137. package/dist/components/util/zabi-strings.d.ts +9 -0
  138. package/dist/components/util/zabi-strings.d.ts.map +1 -1
  139. package/dist/create-theme/cli.js +6 -2
  140. package/dist/create-theme/index.d.ts +17 -3
  141. package/dist/create-theme/index.js +252 -35
  142. package/dist/create-theme/lib/resolve.js +97 -0
  143. package/dist/create-theme/theme-data.js +1618 -90
  144. package/dist/index.d.ts +27 -3
  145. package/dist/index.js +18 -0
  146. package/dist/map.d.ts +10 -0
  147. package/dist/map.js +9 -0
  148. package/dist/molecules/Alert.svelte +31 -12
  149. package/dist/molecules/Alert.svelte.d.ts +12 -1
  150. package/dist/molecules/AppBar.svelte +287 -28
  151. package/dist/molecules/AppBar.svelte.d.ts +62 -0
  152. package/dist/molecules/AvatarGroup.svelte +1 -1
  153. package/dist/molecules/BarChart.svelte +381 -0
  154. package/dist/molecules/BarChart.svelte.d.ts +45 -0
  155. package/dist/molecules/BottomSheet.svelte +88 -20
  156. package/dist/molecules/BottomSheet.svelte.d.ts +17 -1
  157. package/dist/molecules/BottomTabBar.svelte +134 -28
  158. package/dist/molecules/BottomTabBar.svelte.d.ts +36 -2
  159. package/dist/molecules/Calendar.svelte.d.ts +1 -1
  160. package/dist/molecules/ChipGroup.svelte +325 -0
  161. package/dist/molecules/ChipGroup.svelte.d.ts +41 -0
  162. package/dist/molecules/Collapsible.svelte +158 -54
  163. package/dist/molecules/Collapsible.svelte.d.ts +23 -6
  164. package/dist/molecules/CollapsibleGroup.svelte +16 -6
  165. package/dist/molecules/ConfirmDialog.svelte +5 -1
  166. package/dist/molecules/ConfirmDialog.svelte.d.ts +3 -0
  167. package/dist/molecules/Drawer.svelte +33 -12
  168. package/dist/molecules/Dropdown.svelte +9 -1
  169. package/dist/molecules/Dropdown.svelte.d.ts +2 -0
  170. package/dist/molecules/DropdownItem.svelte +1 -1
  171. package/dist/molecules/ImageUpload.svelte +230 -120
  172. package/dist/molecules/ImageUpload.svelte.d.ts +38 -3
  173. package/dist/molecules/LineChart.svelte +459 -0
  174. package/dist/molecules/LineChart.svelte.d.ts +53 -0
  175. package/dist/molecules/MapView.svelte +565 -0
  176. package/dist/molecules/MapView.svelte.d.ts +88 -0
  177. package/dist/molecules/Modal.svelte +11 -6
  178. package/dist/molecules/PhotoGrid.svelte +100 -10
  179. package/dist/molecules/PhotoGrid.svelte.d.ts +11 -0
  180. package/dist/molecules/PhotoViewer.svelte +121 -24
  181. package/dist/molecules/PhotoViewer.svelte.d.ts +18 -0
  182. package/dist/molecules/PieChart.svelte +421 -0
  183. package/dist/molecules/PieChart.svelte.d.ts +51 -0
  184. package/dist/molecules/RadioGroup.svelte +20 -4
  185. package/dist/molecules/RadioGroup.svelte.d.ts +5 -0
  186. package/dist/molecules/SegmentedControl.svelte +136 -3
  187. package/dist/molecules/SegmentedControl.svelte.d.ts +6 -0
  188. package/dist/molecules/SidebarFooter.svelte +1 -1
  189. package/dist/molecules/SidebarNavSection.svelte +1 -1
  190. package/dist/molecules/SortableList.svelte +1 -0
  191. package/dist/molecules/Stepper.svelte +21 -7
  192. package/dist/molecules/StickyActionBar.svelte +67 -8
  193. package/dist/molecules/StickyActionBar.svelte.d.ts +14 -1
  194. package/dist/molecules/Tabs.svelte +79 -2
  195. package/dist/molecules/Toaster.svelte +171 -7
  196. package/dist/molecules/Toaster.svelte.d.ts +60 -2
  197. package/dist/molecules/ToasterToast.svelte +292 -60
  198. package/dist/molecules/ToasterToast.svelte.d.ts +6 -0
  199. package/dist/molecules/UnsavedChangesBar.svelte +13 -2
  200. package/dist/molecules/index.d.ts +7 -2
  201. package/dist/molecules/index.js +4 -0
  202. package/dist/molecules/toast-store.d.ts +30 -0
  203. package/dist/molecules/toast-store.js +4 -1
  204. package/dist/organisms/AppNavigation.svelte +440 -0
  205. package/dist/organisms/AppNavigation.svelte.d.ts +72 -0
  206. package/dist/organisms/AppShell.svelte +539 -39
  207. package/dist/organisms/AppShell.svelte.d.ts +102 -6
  208. package/dist/organisms/AppShellHeaderPart.svelte +163 -0
  209. package/dist/organisms/AppShellHeaderPart.svelte.d.ts +57 -0
  210. package/dist/organisms/SidebarNavigation.svelte +23 -36
  211. package/dist/organisms/SidebarPanel.svelte +10 -14
  212. package/dist/organisms/SidebarPanel.svelte.d.ts +1 -0
  213. package/dist/organisms/SidebarShell.svelte +2 -3
  214. package/dist/organisms/TopNavbar.svelte +117 -28
  215. package/dist/organisms/TopNavbar.svelte.d.ts +22 -1
  216. package/dist/organisms/TopNavbarSheetContent.svelte +145 -0
  217. package/dist/organisms/TopNavbarSheetContent.svelte.d.ts +24 -0
  218. package/dist/organisms/index.d.ts +6 -1
  219. package/dist/organisms/index.js +4 -0
  220. package/dist/types/components.d.ts +17 -1
  221. package/dist/types/components.d.ts.map +1 -1
  222. package/dist/types/index.d.ts +497 -10
  223. package/dist/types/index.d.ts.map +1 -1
  224. package/dist/types/variants.ts +14 -3
  225. package/dist/util/app-shell.d.ts +55 -4
  226. package/dist/util/app-shell.js +32 -7
  227. package/dist/util/avatar.d.ts +3 -0
  228. package/dist/util/bottom-tab-bar.d.ts +6 -0
  229. package/dist/util/chart.d.ts +139 -0
  230. package/dist/util/chart.js +305 -0
  231. package/dist/util/chip.d.ts +65 -0
  232. package/dist/util/chip.js +58 -0
  233. package/dist/util/cn.js +28 -2
  234. package/dist/util/code-field.d.ts +62 -0
  235. package/dist/util/code-field.js +112 -0
  236. package/dist/util/collapsible.d.ts +6 -0
  237. package/dist/util/display-voice.d.ts +9 -0
  238. package/dist/util/display-voice.js +9 -0
  239. package/dist/util/field-control.d.ts +29 -0
  240. package/dist/util/field-control.js +32 -0
  241. package/dist/util/field.d.ts +6 -0
  242. package/dist/util/focus-utils.d.ts +2 -0
  243. package/dist/util/focus-utils.js +4 -0
  244. package/dist/util/map-view.d.ts +143 -0
  245. package/dist/util/map-view.js +121 -0
  246. package/dist/util/photo.d.ts +19 -0
  247. package/dist/util/photo.js +3 -0
  248. package/dist/util/progress.d.ts +18 -0
  249. package/dist/util/progress.js +17 -0
  250. package/dist/util/scroll-edge.d.ts +32 -0
  251. package/dist/util/scroll-edge.js +87 -0
  252. package/dist/util/sheet-drag.d.ts +25 -1
  253. package/dist/util/sheet-drag.js +71 -1
  254. package/dist/util/skeleton.d.ts +14 -0
  255. package/dist/util/skeleton.js +22 -0
  256. package/dist/util/sliding-indicator.d.ts +24 -0
  257. package/dist/util/sliding-indicator.js +44 -0
  258. package/dist/util/temporal-field.d.ts +88 -0
  259. package/dist/util/temporal-field.js +136 -0
  260. package/dist/util/toaster.d.ts +40 -0
  261. package/dist/util/toaster.js +55 -0
  262. package/dist/util/tone-surface.d.ts +33 -0
  263. package/dist/util/tone-surface.js +35 -0
  264. package/dist/util/top-navbar.d.ts +26 -2
  265. package/dist/util/top-navbar.js +23 -0
  266. package/dist/util/zabi-strings.d.ts +9 -0
  267. package/dist/zabi-components-colors.css +9 -3
  268. package/dist/zabi-components-theme-dark-only.css +474 -164
  269. package/dist/zabi-components-theme-dark.css +474 -164
  270. package/dist/zabi-components-theme-only.css +933 -99
  271. package/dist/zabi-components-theme.css +933 -99
  272. package/dist/zabi-components.css +2275 -321
  273. package/package.json +11 -1
@@ -0,0 +1,385 @@
1
+ <script lang="ts">
2
+ import type { HTMLInputAttributes } from "svelte/elements";
3
+ import type { SemanticVariant } from "../types/variants.js";
4
+ import { cn } from "../util/cn.js";
5
+ import type { SizeVariant } from "../types/variants.js";
6
+ import {
7
+ activePlace,
8
+ caretAfterSanitise,
9
+ CODE_FIELD_GAP_REM,
10
+ CODE_FIELD_GROUP_GAP_REM,
11
+ CODE_FIELD_SIZES,
12
+ codeGroups,
13
+ codeLength,
14
+ DEFAULT_CODE_FIELD_STRINGS,
15
+ isCodeChar,
16
+ nearestBox,
17
+ pasteInto,
18
+ replaceAt,
19
+ sanitiseCode,
20
+ type CodeFieldMode,
21
+ type CodeFieldStrings,
22
+ } from "../util/code-field.js";
23
+ import { fieldDescribedBy, fieldMessageState } from "../util/field.js";
24
+ import { mergeStrings } from "../util/ready-made-strings.js";
25
+ import { generateId } from "../util/ssr-safe.js";
26
+ import { zabiStringsFor } from "../util/zabi-strings.js";
27
+ import FieldMessages from "./FieldMessages.svelte";
28
+
29
+ /**
30
+ * A field for a one-time or verification code. One real `<input>` holds
31
+ * the code, so autofill from a message, paste, the numeric keyboard, form
32
+ * posting by `name` and a single accessible name all work as on `Input`;
33
+ * the boxes are drawn under it and hidden from assistive technology.
34
+ * Other attributes (`autocomplete`, `inputmode`, `data-*`, ...) land on
35
+ * the `<input>`.
36
+ */
37
+ type Props = Omit<
38
+ HTMLInputAttributes,
39
+ | "class"
40
+ | "size"
41
+ | "value"
42
+ | "type"
43
+ | "id"
44
+ | "name"
45
+ | "required"
46
+ | "disabled"
47
+ | "readonly"
48
+ | "maxlength"
49
+ | "oninput"
50
+ | "onblur"
51
+ | "aria-label"
52
+ | "aria-describedby"
53
+ > & {
54
+ /** Omit to auto-generate. */
55
+ id?: string;
56
+ /** The code typed so far. Bindable. */
57
+ value?: string;
58
+ /** The name the code is posted under, with or without scripts. */
59
+ name?: string;
60
+ class?: string;
61
+ /** Places in the code. A whole number from 1 to 12. */
62
+ length?: number;
63
+ /** `numeric` (default): digits only. `alphanumeric` also takes letters. */
64
+ mode?: CodeFieldMode;
65
+ /** Box size: `sm` (44px tall, a compact row), `md` (default), `lg`. */
66
+ size?: SizeVariant;
67
+ /**
68
+ * Draws the boxes in groups with a wider gap between them: a number is
69
+ * the size of each group (`4` makes 4 and 4 of 8), a list gives each
70
+ * group's size. Only the drawing changes: one input, one value, and a
71
+ * paste with spaces or dashes still fills it. A row that cannot fit at
72
+ * large text or a narrow width wraps between groups.
73
+ */
74
+ groups?: number | number[];
75
+ /** Names the field. Defaults to the provider's `codeField.label`, then "Verification code". */
76
+ label?: string;
77
+ /** Keeps the label for assistive technology and takes it off the screen. */
78
+ labelHidden?: boolean;
79
+ required?: boolean;
80
+ disabled?: boolean;
81
+ readonly?: boolean;
82
+ variant?: SemanticVariant;
83
+ /** Status text under the field. Shown with a `success`, `warning` or `error` variant; for neutral help, use `hint`. */
84
+ message?: string;
85
+ /** Help text under the field, read out with it. */
86
+ hint?: string;
87
+ /** An error under the field. It marks the field invalid and is announced; it wins over `variant` and `message`. */
88
+ error?: string;
89
+ /** Called with the code when the last place is filled by typing, pasting or autofill. It does not submit anything. */
90
+ oncomplete?: (value: string) => void;
91
+ /** Overrides for the built-in strings: label, description. */
92
+ strings?: Partial<CodeFieldStrings>;
93
+ oninput?: (event: Event) => void;
94
+ onblur?: (event: Event) => void;
95
+ "aria-label"?: string;
96
+ /** Ids of other elements that describe the field. The length, the hint and the message are added after them. */
97
+ "aria-describedby"?: string | null;
98
+ };
99
+
100
+ /** An event on the input, as Svelte types the handlers of an element. */
101
+ type OnInput<E extends Event> = E & { currentTarget: EventTarget & HTMLInputElement };
102
+
103
+ let {
104
+ id: idProp,
105
+ value = $bindable<Exclude<Props["value"], undefined>>(),
106
+ name = "",
107
+ class: className = "",
108
+ length: lengthProp = 6,
109
+ mode = "numeric",
110
+ size = "md",
111
+ groups,
112
+ label,
113
+ labelHidden = false,
114
+ required = false,
115
+ disabled = false,
116
+ readonly = false,
117
+ variant = "default",
118
+ message = "",
119
+ hint = "",
120
+ error = "",
121
+ oncomplete,
122
+ strings,
123
+ oninput,
124
+ onblur,
125
+ "aria-label": ariaLabel,
126
+ "aria-describedby": describedBy,
127
+ ...restProps
128
+ }: Props = $props();
129
+
130
+ // No fallback on a bindable prop (see Input): the default is applied here,
131
+ // at once and whenever a parent hands back `undefined`.
132
+ const applyDefaults = () => {
133
+ if (value === undefined) value = "";
134
+ };
135
+ applyDefaults();
136
+ $effect.pre(applyDefaults);
137
+
138
+ const length = $derived(codeLength(lengthProp));
139
+ const sizing = $derived(CODE_FIELD_SIZES[size] ?? CODE_FIELD_SIZES.md);
140
+ const groupSizes = $derived(codeGroups(groups, length));
141
+ const grouped = $derived(groupSizes.length > 1);
142
+ /** Which group each box is in, for the drawing, and the first box of each. */
143
+ const groupStarts = $derived(groupSizes.map((_, index) => groupSizes.slice(0, index).reduce((sum, n) => sum + n, 0)));
144
+ const provided = zabiStringsFor("codeField");
145
+ const text = $derived(mergeStrings(DEFAULT_CODE_FIELD_STRINGS, provided(), strings));
146
+ const shownLabel = $derived(label || text.label);
147
+
148
+ const fallbackId = generateId("code");
149
+ const inputId = $derived(idProp ?? fallbackId);
150
+ const lengthId = $derived(`${inputId}-length`);
151
+ const status = $derived(fieldMessageState({ variant, message, hint, error }));
152
+
153
+ let input: HTMLInputElement | undefined = $state();
154
+ let cells: HTMLElement | undefined = $state();
155
+ let focused = $state(false);
156
+ let caret = $state(0);
157
+
158
+ /**
159
+ * Without scripts the input is a plain bordered field showing what is
160
+ * typed, which posts like any other. Once the page is interactive it
161
+ * becomes the invisible layer over the boxes.
162
+ */
163
+ let enhanced = $state(false);
164
+ $effect(() => {
165
+ enhanced = true;
166
+ });
167
+
168
+ const places = $derived(Array.from({ length }, (_, index) => value?.[index] ?? ""));
169
+ const active = $derived(enhanced && focused && !disabled && !readonly ? activePlace(caret, length) : -1);
170
+
171
+ const variantClass = $derived(
172
+ status.variant === "success"
173
+ ? "border-success"
174
+ : status.variant === "warning"
175
+ ? "border-warning"
176
+ : status.variant === "error"
177
+ ? "border-error"
178
+ : "border-input-border",
179
+ );
180
+
181
+ /** The focus ring of `.focus-ring`, drawn on the box the caret is in. */
182
+ const RING = "shadow-[0_0_0_2px_var(--color-focus-ring-offset),0_0_0_4px_var(--color-focus-ring)]";
183
+
184
+ let complete = "";
185
+
186
+ /** Sets the code, and calls `oncomplete` when this makes it whole. */
187
+ function commit(next: string, caretAt: number) {
188
+ const previous = value ?? "";
189
+ if (next !== previous) value = next;
190
+ if (input) {
191
+ if (input.value !== next) input.value = next;
192
+ try {
193
+ input.setSelectionRange(caretAt, caretAt);
194
+ } catch {
195
+ // A browser that cannot place the caret: typing still works.
196
+ }
197
+ }
198
+ caret = caretAt;
199
+ if (next.length === length) {
200
+ if (next !== complete) {
201
+ complete = next;
202
+ oncomplete?.(next);
203
+ }
204
+ } else {
205
+ complete = "";
206
+ }
207
+ }
208
+
209
+ function syncCaret() {
210
+ if (input) caret = input.selectionStart ?? input.value.length;
211
+ }
212
+
213
+ function handleInput(event: OnInput<Event>) {
214
+ oninput?.(event);
215
+ const field = event.currentTarget;
216
+ const raw = field.value;
217
+ const clean = sanitiseCode(raw, mode, length);
218
+ const at = caretAfterSanitise(raw, field.selectionStart ?? raw.length, mode, length);
219
+ commit(clean, Math.min(at, clean.length));
220
+ }
221
+
222
+ /**
223
+ * A character typed over a filled place replaces it; one typed at the end
224
+ * is added. A character that does not belong never gets in.
225
+ */
226
+ function handleKeydown(event: OnInput<KeyboardEvent>) {
227
+ restProps.onkeydown?.(event);
228
+ if (event.defaultPrevented || readonly || disabled) return;
229
+ if (event.ctrlKey || event.metaKey || event.altKey || event.key.length !== 1) return;
230
+ const field = event.currentTarget;
231
+ if (!isCodeChar(event.key, mode)) {
232
+ event.preventDefault();
233
+ return;
234
+ }
235
+ const start = field.selectionStart ?? 0;
236
+ const end = field.selectionEnd ?? start;
237
+ if (start === end && start < field.value.length) {
238
+ event.preventDefault();
239
+ commit(replaceAt(field.value, start, event.key), start + 1);
240
+ } else if (start === end && field.value.length >= length) {
241
+ event.preventDefault();
242
+ }
243
+ }
244
+
245
+ /** Takes the whole code from the clipboard, wherever the caret is; "123 456" and "123-456" are 123456. */
246
+ function handlePaste(event: OnInput<ClipboardEvent>) {
247
+ restProps.onpaste?.(event);
248
+ if (event.defaultPrevented || readonly || disabled) return;
249
+ const field = event.currentTarget;
250
+ event.preventDefault();
251
+ const pasted = event.clipboardData?.getData("text") ?? "";
252
+ const start = field.selectionStart ?? field.value.length;
253
+ const end = field.selectionEnd ?? start;
254
+ const next = pasteInto(field.value, start, end, pasted, mode, length);
255
+ if (next === field.value && sanitiseCode(pasted, mode, length) === "") return;
256
+ commit(next, next.length === length ? length : Math.min(next.length, start + sanitiseCode(pasted, mode, length).length));
257
+ }
258
+
259
+ /** A tap on a box puts the caret there, or at the end of the code when that box is still empty. */
260
+ function handleClick(event: OnInput<MouseEvent>) {
261
+ restProps.onclick?.(event);
262
+ if (!input || !cells) return;
263
+ const boxes = Array.from(cells.querySelectorAll<HTMLElement>("[data-code-cell]"));
264
+ let index = nearestBox(
265
+ boxes.map((box) => box.getBoundingClientRect()),
266
+ event.clientX,
267
+ event.clientY,
268
+ );
269
+ if (index < 0) return syncCaret();
270
+ index = Math.min(index, input.value.length);
271
+ try {
272
+ input.setSelectionRange(index, index);
273
+ } catch {
274
+ // Nothing to place.
275
+ }
276
+ syncCaret();
277
+ }
278
+
279
+ const fieldClass = $derived(
280
+ enhanced
281
+ ? "absolute inset-0 size-full min-w-0 cursor-text border-0 bg-transparent p-0 text-base text-transparent caret-transparent outline-none selection:bg-transparent disabled:cursor-not-allowed"
282
+ : cn(
283
+ sizing.box,
284
+ "w-full rounded-control border bg-input px-3 text-center text-xl font-medium tracking-[0.35em] text-body tabular-nums hover:bg-input-hover disabled:cursor-not-allowed disabled:bg-input-disabled disabled:text-action-disabled-text focus-ring",
285
+ variantClass,
286
+ ),
287
+ );
288
+ </script>
289
+
290
+ {#snippet cell(place: string, index: number)}
291
+ <span
292
+ data-code-cell
293
+ data-active={index === active ? "" : undefined}
294
+ style:max-inline-size="{sizing.maxRem}rem"
295
+ style:min-inline-size="{sizing.minEm}em"
296
+ class={cn(
297
+ "flex flex-1 items-center justify-center rounded-control border bg-input font-medium text-body tabular-nums transition-colors duration-150",
298
+ sizing.box,
299
+ sizing.text,
300
+ variantClass,
301
+ disabled && "cursor-not-allowed bg-input-disabled text-action-disabled-text",
302
+ index === active && RING,
303
+ )}
304
+ >
305
+ {place}
306
+ </span>
307
+ {/snippet}
308
+
309
+ <div class={cn("w-full", className)}>
310
+ <label for={inputId} class={cn("mb-2 block text-sm font-medium text-label", labelHidden && "sr-only")}>
311
+ {shownLabel}
312
+ </label>
313
+ <div
314
+ class="relative"
315
+ style:max-inline-size="calc({length} * {sizing.maxRem}rem + {length - 1 - Math.max(groupSizes.length - 1, 0)} * {CODE_FIELD_GAP_REM}rem + {Math.max(groupSizes.length - 1, 0)} * {CODE_FIELD_GROUP_GAP_REM}rem)"
316
+ bind:this={cells}
317
+ >
318
+ {#if enhanced}
319
+ <div
320
+ class={cn("flex flex-wrap", grouped ? "gap-x-4 gap-y-2" : "gap-2")}
321
+ aria-hidden="true"
322
+ data-code-cells
323
+ data-grouped={grouped ? "" : undefined}
324
+ >
325
+ {#if grouped}
326
+ {#each groupSizes as groupSize, group (group)}
327
+ <div
328
+ data-code-group
329
+ class="flex gap-2"
330
+ style:flex="{groupSize} 1 0"
331
+ style:min-inline-size="calc({groupSize} * {sizing.minEm * sizing.digitRem}rem + {groupSize - 1} * {CODE_FIELD_GAP_REM}rem)"
332
+ style:max-inline-size="calc({groupSize} * {sizing.maxRem}rem + {groupSize - 1} * {CODE_FIELD_GAP_REM}rem)"
333
+ >
334
+ {#each places.slice(groupStarts[group], groupStarts[group] + groupSize) as place, offset (offset)}
335
+ {@render cell(place, groupStarts[group] + offset)}
336
+ {/each}
337
+ </div>
338
+ {/each}
339
+ {:else}
340
+ {#each places as place, index (index)}
341
+ {@render cell(place, index)}
342
+ {/each}
343
+ {/if}
344
+ </div>
345
+ {/if}
346
+ <input
347
+ bind:this={input}
348
+ id={inputId}
349
+ type="text"
350
+ {name}
351
+ bind:value
352
+ {required}
353
+ {disabled}
354
+ {readonly}
355
+ inputmode={mode === "numeric" ? "numeric" : "text"}
356
+ autocomplete="one-time-code"
357
+ autocapitalize="off"
358
+ autocorrect="off"
359
+ spellcheck={false}
360
+ class={fieldClass}
361
+ aria-label={ariaLabel}
362
+ aria-invalid={status.variant === "error" ? "true" : undefined}
363
+ aria-required={required ? "true" : undefined}
364
+ aria-describedby={fieldDescribedBy(inputId, status, [describedBy, lengthId].filter(Boolean).join(" "))}
365
+ {...restProps}
366
+ oninput={handleInput}
367
+ onkeydown={handleKeydown}
368
+ onpaste={handlePaste}
369
+ onclick={handleClick}
370
+ onkeyup={syncCaret}
371
+ onselect={syncCaret}
372
+ onfocus={(event) => {
373
+ focused = true;
374
+ syncCaret();
375
+ restProps.onfocus?.(event);
376
+ }}
377
+ onblur={(event) => {
378
+ focused = false;
379
+ onblur?.(event);
380
+ }}
381
+ />
382
+ </div>
383
+ <span id={lengthId} class="sr-only">{text.description(length, mode)}</span>
384
+ <FieldMessages fieldId={inputId} {status} {hint} />
385
+ </div>
@@ -0,0 +1,61 @@
1
+ import type { HTMLInputAttributes } from "svelte/elements";
2
+ import type { SemanticVariant } from "../types/variants.js";
3
+ import type { SizeVariant } from "../types/variants.js";
4
+ import { type CodeFieldMode, type CodeFieldStrings } from "../util/code-field.js";
5
+ /**
6
+ * A field for a one-time or verification code. One real `<input>` holds
7
+ * the code, so autofill from a message, paste, the numeric keyboard, form
8
+ * posting by `name` and a single accessible name all work as on `Input`;
9
+ * the boxes are drawn under it and hidden from assistive technology.
10
+ * Other attributes (`autocomplete`, `inputmode`, `data-*`, ...) land on
11
+ * the `<input>`.
12
+ */
13
+ type Props = Omit<HTMLInputAttributes, "class" | "size" | "value" | "type" | "id" | "name" | "required" | "disabled" | "readonly" | "maxlength" | "oninput" | "onblur" | "aria-label" | "aria-describedby"> & {
14
+ /** Omit to auto-generate. */
15
+ id?: string;
16
+ /** The code typed so far. Bindable. */
17
+ value?: string;
18
+ /** The name the code is posted under, with or without scripts. */
19
+ name?: string;
20
+ class?: string;
21
+ /** Places in the code. A whole number from 1 to 12. */
22
+ length?: number;
23
+ /** `numeric` (default): digits only. `alphanumeric` also takes letters. */
24
+ mode?: CodeFieldMode;
25
+ /** Box size: `sm` (44px tall, a compact row), `md` (default), `lg`. */
26
+ size?: SizeVariant;
27
+ /**
28
+ * Draws the boxes in groups with a wider gap between them: a number is
29
+ * the size of each group (`4` makes 4 and 4 of 8), a list gives each
30
+ * group's size. Only the drawing changes: one input, one value, and a
31
+ * paste with spaces or dashes still fills it. A row that cannot fit at
32
+ * large text or a narrow width wraps between groups.
33
+ */
34
+ groups?: number | number[];
35
+ /** Names the field. Defaults to the provider's `codeField.label`, then "Verification code". */
36
+ label?: string;
37
+ /** Keeps the label for assistive technology and takes it off the screen. */
38
+ labelHidden?: boolean;
39
+ required?: boolean;
40
+ disabled?: boolean;
41
+ readonly?: boolean;
42
+ variant?: SemanticVariant;
43
+ /** Status text under the field. Shown with a `success`, `warning` or `error` variant; for neutral help, use `hint`. */
44
+ message?: string;
45
+ /** Help text under the field, read out with it. */
46
+ hint?: string;
47
+ /** An error under the field. It marks the field invalid and is announced; it wins over `variant` and `message`. */
48
+ error?: string;
49
+ /** Called with the code when the last place is filled by typing, pasting or autofill. It does not submit anything. */
50
+ oncomplete?: (value: string) => void;
51
+ /** Overrides for the built-in strings: label, description. */
52
+ strings?: Partial<CodeFieldStrings>;
53
+ oninput?: (event: Event) => void;
54
+ onblur?: (event: Event) => void;
55
+ "aria-label"?: string;
56
+ /** Ids of other elements that describe the field. The length, the hint and the message are added after them. */
57
+ "aria-describedby"?: string | null;
58
+ };
59
+ declare const CodeField: import("svelte").Component<Props, {}, "value">;
60
+ type CodeField = ReturnType<typeof CodeField>;
61
+ export default CodeField;
@@ -488,6 +488,7 @@
488
488
  use:setColorMapContainer
489
489
  class="relative w-full h-48 rounded-container cursor-crosshair touch-none select-none {RING_WITHIN}"
490
490
  role="group"
491
+ data-sheet-no-drag
491
492
  aria-label={text.area}
492
493
  onpointerdown={handleMapPointerDown}
493
494
  onpointermove={handleMapPointerMove}
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { HTMLInputAttributes } from "svelte/elements";
3
3
  import type { SizeVariant } from "../types/variants.js";
4
+ import type { DateFieldStrings } from "../util/temporal-field.js";
4
5
  import TemporalField from "./TemporalField.svelte";
5
6
 
6
7
  /**
@@ -10,15 +11,28 @@
10
11
  * calendar, Android its calendar, a desktop browser its own. `value` is
11
12
  * always `YYYY-MM-DD`, or `""` while empty, whatever the field shows.
12
13
  *
13
- * What the field shows is the browser's decision: the date in the format
14
- * of the browser or device, not of the page, and no attribute changes
15
- * that. To show a date as text in the page's language, use `formatDate`:
14
+ * What the field shows is the browser's decision, unless it has a
15
+ * `locale`: without one, the date is in the format of the browser or
16
+ * device, not of the page, and no attribute changes that. To show a date
17
+ * as text in the page's language, use `formatDate`:
16
18
  *
17
19
  * ```svelte
18
20
  * <DateField label="Date" bind:value={date} min="2026-01-01" />
19
21
  * <p>{formatDate(date, "sv")}</p> <!-- 6 okt. 2026 -->
20
22
  * ```
21
23
  *
24
+ * With `locale`, the field shows the value in that language and its own
25
+ * picker opens: a Calendar in a BottomSheet (at every width), with Clear
26
+ * unless the field is `required`. Until the page has run, and without
27
+ * scripts, it is the native input as above and submits as before; once
28
+ * it has, the native input stays as the form control, out of sight, and a
29
+ * button in its place shows the date. `picker="native"` opens the
30
+ * platform's own picker instead.
31
+ *
32
+ * ```svelte
33
+ * <DateField label="Datum" locale="sv" bind:value format={{ weekday: "short", day: "numeric", month: "short" }} />
34
+ * ```
35
+ *
22
36
  * Other attributes (`autocomplete`, `data-*`, `onchange`, ...) land on the
23
37
  * `<input>`.
24
38
  */
@@ -53,6 +67,20 @@
53
67
  size?: SizeVariant;
54
68
  /** Extra classes for the `<input>`. */
55
69
  class?: string;
70
+ /**
71
+ * Language of the field, as a BCP 47 tag (`sv`, `en-GB`) or a list of them. With it the
72
+ * field shows the value in that language, whatever the browser's is, and opens the
73
+ * library's own picker; see above. Left out, the field is the native input.
74
+ */
75
+ locale?: string | string[];
76
+ /** With `locale`: how the value is shown, as options of `Intl.DateTimeFormat`. Default: what formatDate gives for the locale. */
77
+ format?: Intl.DateTimeFormatOptions;
78
+ /** With `locale`: shown in the placeholder colour while the field is empty. */
79
+ placeholder?: string;
80
+ /** With `locale`: `library` (default) opens the library's calendar in a sheet; `native` opens the platform's own picker. */
81
+ picker?: "library" | "native";
82
+ /** With `locale`: the words the field says; replace any of them to translate. */
83
+ strings?: Partial<DateFieldStrings>;
56
84
  };
57
85
 
58
86
  let {
@@ -71,6 +99,11 @@
71
99
  readonly = false,
72
100
  size = "md",
73
101
  class: className = "",
102
+ locale,
103
+ format,
104
+ placeholder,
105
+ picker = "library",
106
+ strings,
74
107
  ...restProps
75
108
  }: Props = $props();
76
109
 
@@ -103,5 +136,10 @@
103
136
  {readonly}
104
137
  {size}
105
138
  class={className}
139
+ {locale}
140
+ {format}
141
+ {placeholder}
142
+ {picker}
143
+ {strings}
106
144
  {...restProps}
107
145
  />
@@ -1,5 +1,6 @@
1
1
  import type { HTMLInputAttributes } from "svelte/elements";
2
2
  import type { SizeVariant } from "../types/variants.js";
3
+ import type { DateFieldStrings } from "../util/temporal-field.js";
3
4
  /**
4
5
  * A date, picked with the device's own date picker and styled like Input.
5
6
  *
@@ -7,15 +8,28 @@ import type { SizeVariant } from "../types/variants.js";
7
8
  * calendar, Android its calendar, a desktop browser its own. `value` is
8
9
  * always `YYYY-MM-DD`, or `""` while empty, whatever the field shows.
9
10
  *
10
- * What the field shows is the browser's decision: the date in the format
11
- * of the browser or device, not of the page, and no attribute changes
12
- * that. To show a date as text in the page's language, use `formatDate`:
11
+ * What the field shows is the browser's decision, unless it has a
12
+ * `locale`: without one, the date is in the format of the browser or
13
+ * device, not of the page, and no attribute changes that. To show a date
14
+ * as text in the page's language, use `formatDate`:
13
15
  *
14
16
  * ```svelte
15
17
  * <DateField label="Date" bind:value={date} min="2026-01-01" />
16
18
  * <p>{formatDate(date, "sv")}</p> <!-- 6 okt. 2026 -->
17
19
  * ```
18
20
  *
21
+ * With `locale`, the field shows the value in that language and its own
22
+ * picker opens: a Calendar in a BottomSheet (at every width), with Clear
23
+ * unless the field is `required`. Until the page has run, and without
24
+ * scripts, it is the native input as above and submits as before; once
25
+ * it has, the native input stays as the form control, out of sight, and a
26
+ * button in its place shows the date. `picker="native"` opens the
27
+ * platform's own picker instead.
28
+ *
29
+ * ```svelte
30
+ * <DateField label="Datum" locale="sv" bind:value format={{ weekday: "short", day: "numeric", month: "short" }} />
31
+ * ```
32
+ *
19
33
  * Other attributes (`autocomplete`, `data-*`, `onchange`, ...) land on the
20
34
  * `<input>`.
21
35
  */
@@ -47,6 +61,20 @@ type Props = Omit<HTMLInputAttributes, "class" | "type" | "value" | "size" | "mi
47
61
  size?: SizeVariant;
48
62
  /** Extra classes for the `<input>`. */
49
63
  class?: string;
64
+ /**
65
+ * Language of the field, as a BCP 47 tag (`sv`, `en-GB`) or a list of them. With it the
66
+ * field shows the value in that language, whatever the browser's is, and opens the
67
+ * library's own picker; see above. Left out, the field is the native input.
68
+ */
69
+ locale?: string | string[];
70
+ /** With `locale`: how the value is shown, as options of `Intl.DateTimeFormat`. Default: what formatDate gives for the locale. */
71
+ format?: Intl.DateTimeFormatOptions;
72
+ /** With `locale`: shown in the placeholder colour while the field is empty. */
73
+ placeholder?: string;
74
+ /** With `locale`: `library` (default) opens the library's calendar in a sheet; `native` opens the platform's own picker. */
75
+ picker?: "library" | "native";
76
+ /** With `locale`: the words the field says; replace any of them to translate. */
77
+ strings?: Partial<DateFieldStrings>;
50
78
  };
51
79
  declare const DateField: import("svelte").Component<Props, {}, "value">;
52
80
  type DateField = ReturnType<typeof DateField>;
@@ -20,7 +20,7 @@
20
20
  * <FloatingActionButton
21
21
  * label="New quiz"
22
22
  * onclick={create}
23
- * style="--fab-bottom-offset: 65px"
23
+ * style="--fab-bottom-offset: 64px"
24
24
  * />
25
25
  * ```
26
26
  *
@@ -125,8 +125,8 @@
125
125
  extended ? "min-h-14 min-w-14" : "size-[56px]",
126
126
  // Transparent until forced-colors mode draws it: the fill is gone there.
127
127
  "border border-transparent",
128
- "bg-action-primary text-action-primary",
129
- "transition-[background-color,scale] duration-150 active:scale-[0.96] motion-reduce:transition-none motion-reduce:active:scale-100",
128
+ "bg-action-primary bg-control-gradient text-action-primary",
129
+ "transition-[background-color,scale] duration-(--duration-base) active:scale-[0.96] motion-reduce:transition-none motion-reduce:active:scale-100",
130
130
  extended ? "px-5 py-2 text-sm font-medium" : "p-0",
131
131
  // Never wider than the screen it floats on, margins included.
132
132
  "[max-inline-size:calc(100%_-_32px)]",
@@ -156,6 +156,7 @@
156
156
  class={classes}
157
157
  aria-label={extended ? undefined : label}
158
158
  data-position={position}
159
+ data-toast-clear
159
160
  {...restProps}
160
161
  >
161
162
  {@render content()}
@@ -167,6 +168,7 @@
167
168
  class={classes}
168
169
  aria-label={extended ? undefined : label}
169
170
  data-position={position}
171
+ data-toast-clear
170
172
  {...restProps}
171
173
  >
172
174
  {@render content()}
@@ -14,7 +14,7 @@ import type { HTMLAttributes } from "svelte/elements";
14
14
  * <FloatingActionButton
15
15
  * label="New quiz"
16
16
  * onclick={create}
17
- * style="--fab-bottom-offset: 65px"
17
+ * style="--fab-bottom-offset: 64px"
18
18
  * />
19
19
  * ```
20
20
  *