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,72 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
+ import type { HTMLAttributes } from "svelte/elements";
4
+ import { cn } from "../util/cn.js";
5
+ import { displayVoice } from "../util/display-voice.js";
6
+
7
+ type StatSize = "sm" | "md" | "lg";
8
+
9
+ type Props = Omit<HTMLAttributes<HTMLDivElement>, "class"> & {
10
+ /** The figure, shown as given: the app formats numbers for its locale. */
11
+ value: string | number;
12
+ label?: string;
13
+ /** Drawn after the value, smaller, on the same baseline ("av 19"). */
14
+ unit?: string;
15
+ size?: StatSize;
16
+ align?: "start" | "center" | "end";
17
+ labelPosition?: "below" | "above";
18
+ class?: string;
19
+ /** Replaces `value` for a figure that needs markup. */
20
+ children?: Snippet;
21
+ };
22
+
23
+ let {
24
+ value,
25
+ label = "",
26
+ unit = "",
27
+ size = "md",
28
+ align = "start",
29
+ labelPosition = "below",
30
+ class: className = "",
31
+ children,
32
+ ...restProps
33
+ }: Props = $props();
34
+
35
+ /** Value 24 / 36 / 48px, unit 14 / 16 / 20px, label 12 / 14 / 14px. */
36
+ const sizeClasses: Record<StatSize, { value: string; unit: string; label: string }> = {
37
+ sm: { value: "text-2xl", unit: "text-sm", label: "text-xs" },
38
+ md: { value: "text-4xl", unit: "text-base", label: "text-sm" },
39
+ lg: { value: "text-5xl", unit: "text-xl", label: "text-sm" },
40
+ };
41
+ const alignClasses = {
42
+ start: "items-start text-start",
43
+ center: "items-center text-center",
44
+ end: "items-end text-end",
45
+ } as const;
46
+
47
+ const s = $derived(sizeClasses[size] ?? sizeClasses.md);
48
+ const hostClasses = $derived(
49
+ cn(`flex flex-col gap-1 ${alignClasses[align] ?? alignClasses.start} ${className}`),
50
+ );
51
+ </script>
52
+
53
+ {#snippet figure()}
54
+ <div class="inline-flex items-baseline gap-1 whitespace-nowrap">
55
+ <span class="font-display text-headline tabular-nums leading-none {displayVoice} {s.value}">
56
+ {#if children}{@render children()}{:else}{value}{/if}
57
+ </span>
58
+ {#if unit}
59
+ <span class="text-description font-medium {s.unit}">{unit}</span>
60
+ {/if}
61
+ </div>
62
+ {/snippet}
63
+
64
+ <div class={hostClasses} {...restProps}>
65
+ {#if labelPosition === "above"}
66
+ {#if label}<span class="text-description {s.label}">{label}</span>{/if}
67
+ {@render figure()}
68
+ {:else}
69
+ {@render figure()}
70
+ {#if label}<span class="text-description {s.label}">{label}</span>{/if}
71
+ {/if}
72
+ </div>
@@ -0,0 +1,19 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { HTMLAttributes } from "svelte/elements";
3
+ type StatSize = "sm" | "md" | "lg";
4
+ type Props = Omit<HTMLAttributes<HTMLDivElement>, "class"> & {
5
+ /** The figure, shown as given: the app formats numbers for its locale. */
6
+ value: string | number;
7
+ label?: string;
8
+ /** Drawn after the value, smaller, on the same baseline ("av 19"). */
9
+ unit?: string;
10
+ size?: StatSize;
11
+ align?: "start" | "center" | "end";
12
+ labelPosition?: "below" | "above";
13
+ class?: string;
14
+ /** Replaces `value` for a figure that needs markup. */
15
+ children?: Snippet;
16
+ };
17
+ declare const Stat: import("svelte").Component<Props, {}, "">;
18
+ type Stat = ReturnType<typeof Stat>;
19
+ export default Stat;
@@ -1,16 +1,59 @@
1
+ <script module lang="ts">
2
+ /**
3
+ * Remembered for the page's life: the platform's own picker refused to
4
+ * open (or is not there), so a field that wanted it shows its native input.
5
+ */
6
+ let nativePickerFailed = false;
7
+ </script>
8
+
1
9
  <script lang="ts">
10
+ import CalendarIcon from "@lucide/svelte/icons/calendar";
11
+ import Clock from "@lucide/svelte/icons/clock";
12
+ import { onMount, tick } from "svelte";
2
13
  import type { SizeVariant } from "../types/variants.js";
3
14
  import { cn } from "../util/cn.js";
15
+ import { clampDate, formatDate, formatTime, isIsoDate, monthOf, todayIso } from "../util/date.js";
16
+ import { mergeStrings } from "../util/ready-made-strings.js";
17
+ import { generateId } from "../util/ssr-safe.js";
18
+ import {
19
+ DATE_FIELD_STRINGS,
20
+ TIME_FIELD_STRINGS,
21
+ hasLocale,
22
+ hourChoices,
23
+ minuteChoices,
24
+ minuteStep,
25
+ openingTime,
26
+ parseHourMinute,
27
+ stepFitsLibraryPicker,
28
+ timeInRange,
29
+ type DateFieldStrings,
30
+ type TimeFieldStrings,
31
+ } from "../util/temporal-field.js";
32
+ import { zabiStringsFor } from "../util/zabi-strings.js";
33
+ import BottomSheet from "../molecules/BottomSheet.svelte";
34
+ import Calendar from "../molecules/Calendar.svelte";
35
+ import Button from "./Button.svelte";
4
36
  import Input from "./Input.svelte";
37
+ import PickerField from "./PickerField.svelte";
38
+ import TimeColumn from "./TimeColumn.svelte";
5
39
 
6
40
  /**
7
41
  * What DateField and TimeField share. Internal: it is not exported from
8
42
  * the package, and its props are those of the two fields.
9
43
  *
10
- * It is an `Input` with a native `type`, so the box, the sizes, the label,
11
- * the hint and the error message are Input's own and cannot drift from it.
12
- * What is added here is only what a native date or time input needs to
13
- * look like one: see the classes and the style block below.
44
+ * Without `locale` it is an `Input` with a native `type`, so the box, the
45
+ * sizes, the label, the hint and the error message are Input's own and
46
+ * cannot drift from it. What is added here is only what a native date or
47
+ * time input needs to look like one: see the classes and the style block
48
+ * below.
49
+ *
50
+ * With `locale`, the same on the server and until the page has run: the
51
+ * native input, which works and submits without scripts. Once mounted the
52
+ * native input stays as the form control, out of sight, the tab order and
53
+ * the accessibility tree, and a field-shaped button (PickerField) takes
54
+ * its place: it shows the value written by `Intl.DateTimeFormat` and opens
55
+ * the library's picker in a BottomSheet (a Calendar; two columns for a
56
+ * time), or, with `picker="native"`, the platform's own.
14
57
  */
15
58
  interface Props {
16
59
  kind: "date" | "time";
@@ -29,6 +72,12 @@
29
72
  readonly?: boolean;
30
73
  size?: SizeVariant;
31
74
  class?: string;
75
+ locale?: string | string[];
76
+ format?: Intl.DateTimeFormatOptions;
77
+ placeholder?: string;
78
+ picker?: "library" | "native";
79
+ strings?: Partial<DateFieldStrings> | Partial<TimeFieldStrings>;
80
+ openAt?: string;
32
81
  "aria-describedby"?: string | null;
33
82
  }
34
83
 
@@ -49,6 +98,12 @@
49
98
  readonly = false,
50
99
  size = "md",
51
100
  class: className = "",
101
+ locale,
102
+ format,
103
+ placeholder = "",
104
+ picker = "library",
105
+ strings,
106
+ openAt,
52
107
  "aria-describedby": describedBy,
53
108
  ...restProps
54
109
  }: Props = $props();
@@ -64,6 +119,195 @@
64
119
  applyDefaults();
65
120
  $effect.pre(applyDefaults);
66
121
 
122
+ /** The words of a provider above, then this field's own. */
123
+ const dateProvided = zabiStringsFor("dateField");
124
+ const timeProvided = zabiStringsFor("timeField");
125
+ const dateText = $derived(mergeStrings(DATE_FIELD_STRINGS, dateProvided(), strings as Partial<DateFieldStrings>));
126
+ const timeText = $derived(mergeStrings(TIME_FIELD_STRINGS, timeProvided(), strings as Partial<TimeFieldStrings>));
127
+
128
+ // The id of the field. Only a field with a locale needs to know it: the
129
+ // native input and the button must not share one once the button is in.
130
+ // svelte-ignore state_referenced_locally
131
+ const ownId = hasLocale(locale) ? generateId("input") : undefined;
132
+ const baseId = $derived(idProp ?? ownId);
133
+
134
+ let root: HTMLDivElement | undefined = $state();
135
+ let trigger = $state<HTMLButtonElement | HTMLAnchorElement | null>(null);
136
+
137
+ /** True once the page has run. The server and the first render never are. */
138
+ let mounted = $state(false);
139
+ let supportsNativePicker = $state(false);
140
+ let sessionBroken = $state(false);
141
+ /** This field's native input was put back in sight: the platform's picker would not open. */
142
+ let unhidden = $state(false);
143
+ let isOpen = $state(false);
144
+
145
+ onMount(() => {
146
+ supportsNativePicker = typeof HTMLInputElement !== "undefined" && typeof HTMLInputElement.prototype.showPicker === "function";
147
+ sessionBroken = nativePickerFailed;
148
+ mounted = true;
149
+ });
150
+
151
+ /** `library`, unless the app asked for the platform's, or the library's picker cannot serve this `step`. */
152
+ const effectivePicker = $derived(
153
+ picker === "native" || (kind === "time" && !stepFitsLibraryPicker(step)) ? "native" : "library",
154
+ );
155
+ const enhanced = $derived(
156
+ mounted &&
157
+ hasLocale(locale) &&
158
+ !unhidden &&
159
+ (effectivePicker === "library" || (supportsNativePicker && !sessionBroken)),
160
+ );
161
+
162
+ const display = $derived(
163
+ !value
164
+ ? ""
165
+ : kind === "date"
166
+ ? formatDate(value, locale, format)
167
+ : formatTime(value, locale, format),
168
+ );
169
+
170
+ const text = $derived(kind === "date" ? dateText : timeText);
171
+
172
+ /** What the button is named by when the label is not on the page: the label and the value. */
173
+ const hiddenLabelName = $derived(hideLabel && label ? [label, display || placeholder].filter(Boolean).join(", ") : undefined);
174
+
175
+ const nativeInput = () => root?.querySelector<HTMLInputElement>("input[data-temporal-native]") ?? null;
176
+
177
+ /**
178
+ * What the picker chose goes into the native input, which is the form
179
+ * control: its value, and real `input` and `change` events, so a listener
180
+ * the app put on the field hears it as it hears a native change. The
181
+ * binding follows from the `input` event.
182
+ */
183
+ function setValue(next: string) {
184
+ if (next === value) return;
185
+ const input = nativeInput();
186
+ if (input) {
187
+ input.value = next;
188
+ input.dispatchEvent(new Event("input", { bubbles: true }));
189
+ input.dispatchEvent(new Event("change", { bubbles: true }));
190
+ }
191
+ value = next;
192
+ }
193
+
194
+ async function closeAndFocus() {
195
+ isOpen = false;
196
+ await tick();
197
+ trigger?.focus();
198
+ }
199
+
200
+ // ---- the sheet -------------------------------------------------------
201
+
202
+ let calendarMonth = $state<string | undefined>();
203
+ let sheetSnap = $state<"half" | "full">("half");
204
+ let draftHour = $state("");
205
+ let draftMinute = $state("");
206
+ /** Where the columns stand while nothing is chosen. Set as the sheet opens; never written to `value`. */
207
+ let anchorHour = $state("");
208
+ let anchorMinute = $state("");
209
+
210
+ function openPicker() {
211
+ if (disabled || readonly) return;
212
+ if (effectivePicker === "native") {
213
+ openNative();
214
+ return;
215
+ }
216
+ if (kind === "date") {
217
+ calendarMonth = monthOf(isIsoDate(value) ? value : clampDate(todayIso(), min, max)) ?? undefined;
218
+ sheetSnap = typeof window !== "undefined" && window.innerHeight >= 900 ? "half" : "full";
219
+ } else {
220
+ const now = parseHourMinute(value);
221
+ draftHour = now ? String(now[0]).padStart(2, "0") : "";
222
+ draftMinute = now ? String(now[1]).padStart(2, "0") : "";
223
+ const clock = new Date();
224
+ [anchorHour, anchorMinute] = openingTime({
225
+ openAt,
226
+ now: [clock.getHours(), clock.getMinutes()],
227
+ stepMinutes: minuteStep(step),
228
+ min,
229
+ max,
230
+ });
231
+ sheetSnap = typeof window !== "undefined" && window.innerHeight >= 700 ? "half" : "full";
232
+ }
233
+ isOpen = true;
234
+ }
235
+
236
+ /** The platform's picker, from inside the press that asked for it. Where it is refused, the native input comes back. */
237
+ function openNative() {
238
+ const input = nativeInput();
239
+ try {
240
+ if (!input) throw new Error("no input");
241
+ input.showPicker();
242
+ } catch {
243
+ nativePickerFailed = true;
244
+ unhidden = true;
245
+ void tick().then(() => root?.querySelector<HTMLInputElement>("input")?.focus());
246
+ }
247
+ }
248
+
249
+ const canClear = $derived(!required && value !== "");
250
+ const sheetTitle = $derived(label || (kind === "date" ? dateText.chooseDate : timeText.chooseTime));
251
+
252
+ function chooseDay(date: string) {
253
+ setValue(date);
254
+ void closeAndFocus();
255
+ }
256
+
257
+ function clear() {
258
+ setValue("");
259
+ void closeAndFocus();
260
+ }
261
+
262
+ // ---- the time columns -----------------------------------------------
263
+
264
+ const hours = $derived(hourChoices(locale, min, max));
265
+ const minutes = $derived(
266
+ minuteChoices({ locale, stepMinutes: minuteStep(step), hour: draftHour, current: value, min, max }),
267
+ );
268
+
269
+ function chooseHour(hour: string) {
270
+ draftHour = hour;
271
+ const options = minuteChoices({ locale, stepMinutes: minuteStep(step), hour, current: value, min, max });
272
+ // A minute the new hour does not allow: the first one it does. None yet: the one
273
+ // `openAt` asked for if the hour allows it (not the one "now" happens to stand at), else the first.
274
+ if (!draftMinute && parseHourMinute(openAt) && options.find((option) => option.value === anchorMinute)?.disabled === false) {
275
+ draftMinute = anchorMinute;
276
+ } else if (!draftMinute || options.find((option) => option.value === draftMinute)?.disabled !== false) {
277
+ draftMinute = options.find((option) => !option.disabled)?.value ?? draftMinute;
278
+ }
279
+ }
280
+
281
+ const draftValid = $derived(
282
+ draftHour !== "" && draftMinute !== "" && timeInRange(Number(draftHour), Number(draftMinute), min, max),
283
+ );
284
+
285
+ function done() {
286
+ if (!draftValid) return;
287
+ const next = `${draftHour}:${draftMinute}`;
288
+ // The seconds of a value that is kept are kept.
289
+ const kept = parseHourMinute(value);
290
+ setValue(kept && `${String(kept[0]).padStart(2, "0")}:${String(kept[1]).padStart(2, "0")}` === next ? value : next);
291
+ void closeAndFocus();
292
+ }
293
+
294
+ // ---- the native input, out of sight once the button is in -----------
295
+
296
+ const HIDDEN_STYLE =
297
+ "position:absolute;inline-size:1px;block-size:1px;margin:-1px;padding:0;border:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;opacity:0;pointer-events:none";
298
+
299
+ /**
300
+ * The browser's own validation (`required` and empty) fires on the hidden
301
+ * input, where its bubble cannot be shown: that is cancelled, and focus
302
+ * goes to the button. The message people read is the field's `error`.
303
+ */
304
+ function handleInvalid(event: Event) {
305
+ (restProps as { oninvalid?: (event: Event) => void }).oninvalid?.(event);
306
+ if (!enhanced) return;
307
+ event.preventDefault();
308
+ trigger?.focus();
309
+ }
310
+
67
311
  /** What Input does not declare, but passes on to the `<input>`. */
68
312
  const native = $derived({
69
313
  min,
@@ -72,6 +316,15 @@
72
316
  readonly: readonly || undefined,
73
317
  "data-empty": value ? undefined : "",
74
318
  ...restProps,
319
+ ...(hasLocale(locale) ? { oninvalid: handleInvalid } : {}),
320
+ ...(enhanced
321
+ ? {
322
+ tabindex: -1,
323
+ "aria-hidden": "true" as const,
324
+ "data-temporal-native": "",
325
+ style: HIDDEN_STYLE,
326
+ }
327
+ : {}),
75
328
  });
76
329
 
77
330
  const fieldClasses = $derived(
@@ -89,25 +342,104 @@
89
342
  );
90
343
  </script>
91
344
 
92
- <div>
345
+ <div bind:this={root}>
346
+ {#if enhanced}
347
+ <PickerField
348
+ bind:element={trigger}
349
+ id={baseId}
350
+ label={hideLabel ? "" : label}
351
+ value={display}
352
+ {placeholder}
353
+ {size}
354
+ {disabled}
355
+ {hint}
356
+ {error}
357
+ expanded={isOpen}
358
+ aria-describedby={describedBy}
359
+ aria-label={hiddenLabelName}
360
+ data-readonly={readonly ? "" : undefined}
361
+ onclick={openPicker}
362
+ >
363
+ {#snippet trailing()}
364
+ {#if kind === "date"}
365
+ <CalendarIcon size={20} aria-hidden="true" />
366
+ {:else}
367
+ <Clock size={20} aria-hidden="true" />
368
+ {/if}
369
+ {/snippet}
370
+ </PickerField>
371
+ {/if}
93
372
  <Input
94
- id={idProp}
373
+ id={enhanced && baseId ? `${baseId}-native` : baseId}
95
374
  type={kind}
96
375
  bind:value
97
376
  {name}
98
377
  {label}
99
- {hideLabel}
378
+ hideLabel={hideLabel || enhanced}
100
379
  {required}
101
380
  {disabled}
102
381
  {size}
103
- {hint}
104
- {error}
105
- aria-describedby={describedBy}
382
+ hint={enhanced ? "" : hint}
383
+ error={enhanced ? "" : error}
384
+ aria-describedby={enhanced ? undefined : describedBy}
106
385
  class={fieldClasses}
107
386
  {...native}
108
387
  />
109
388
  </div>
110
389
 
390
+ {#snippet clearAction()}
391
+ <Button variant="ghost" onclick={clear}>{text.clear}</Button>
392
+ {/snippet}
393
+
394
+ {#snippet doneAction()}
395
+ {#if canClear}
396
+ {@render clearAction()}
397
+ {/if}
398
+ {#if draftHour !== "" && draftMinute !== ""}
399
+ <span data-chosen-time class="me-auto min-w-0 self-center text-sm text-body">
400
+ {timeText.chosenTime}
401
+ <span class="font-semibold">{formatTime(`${draftHour}:${draftMinute}`, locale, format)}</span>
402
+ </span>
403
+ {/if}
404
+ <Button variant="primary" disabled={!draftValid} onclick={done}>{timeText.done}</Button>
405
+ {/snippet}
406
+
407
+ {#if enhanced && effectivePicker === "library"}
408
+ {#if kind === "date"}
409
+ <BottomSheet
410
+ bind:isOpen
411
+ title={sheetTitle}
412
+ snap={sheetSnap}
413
+ initialFocus="[data-date][tabindex='0']"
414
+ data-temporal-sheet="date"
415
+ {...canClear ? { footer: clearAction } : {}}
416
+ >
417
+ <Calendar
418
+ bind:month={calendarMonth}
419
+ selected={isIsoDate(value) ? value : null}
420
+ locale={Array.isArray(locale) ? locale[0] : locale}
421
+ {min}
422
+ {max}
423
+ onselect={chooseDay}
424
+ />
425
+ </BottomSheet>
426
+ {:else}
427
+ <BottomSheet
428
+ bind:isOpen
429
+ title={sheetTitle}
430
+ snap={sheetSnap}
431
+ initialFocus="[role='option'][tabindex='0']"
432
+ data-temporal-sheet="time"
433
+ footer={doneAction}
434
+ >
435
+ <div class="flex gap-3">
436
+ <TimeColumn label={timeText.hours} options={hours} selected={draftHour} openAt={anchorHour} onselect={chooseHour} />
437
+ <TimeColumn label={timeText.minutes} options={minutes} selected={draftMinute} openAt={anchorMinute} onselect={(minute) => (draftMinute = minute)} />
438
+ </div>
439
+ </BottomSheet>
440
+ {/if}
441
+ {/if}
442
+
111
443
  <style>
112
444
  /*
113
445
  * The parts of a native date or time input that utilities cannot reach.
@@ -1,12 +1,22 @@
1
1
  import type { SizeVariant } from "../types/variants.js";
2
+ import { type DateFieldStrings, type TimeFieldStrings } from "../util/temporal-field.js";
2
3
  /**
3
4
  * What DateField and TimeField share. Internal: it is not exported from
4
5
  * the package, and its props are those of the two fields.
5
6
  *
6
- * It is an `Input` with a native `type`, so the box, the sizes, the label,
7
- * the hint and the error message are Input's own and cannot drift from it.
8
- * What is added here is only what a native date or time input needs to
9
- * look like one: see the classes and the style block below.
7
+ * Without `locale` it is an `Input` with a native `type`, so the box, the
8
+ * sizes, the label, the hint and the error message are Input's own and
9
+ * cannot drift from it. What is added here is only what a native date or
10
+ * time input needs to look like one: see the classes and the style block
11
+ * below.
12
+ *
13
+ * With `locale`, the same on the server and until the page has run: the
14
+ * native input, which works and submits without scripts. Once mounted the
15
+ * native input stays as the form control, out of sight, the tab order and
16
+ * the accessibility tree, and a field-shaped button (PickerField) takes
17
+ * its place: it shows the value written by `Intl.DateTimeFormat` and opens
18
+ * the library's picker in a BottomSheet (a Calendar; two columns for a
19
+ * time), or, with `picker="native"`, the platform's own.
10
20
  */
11
21
  interface Props {
12
22
  kind: "date" | "time";
@@ -25,6 +35,12 @@ interface Props {
25
35
  readonly?: boolean;
26
36
  size?: SizeVariant;
27
37
  class?: string;
38
+ locale?: string | string[];
39
+ format?: Intl.DateTimeFormatOptions;
40
+ placeholder?: string;
41
+ picker?: "library" | "native";
42
+ strings?: Partial<DateFieldStrings> | Partial<TimeFieldStrings>;
43
+ openAt?: string;
28
44
  "aria-describedby"?: string | null;
29
45
  }
30
46
  declare const TemporalField: import("svelte").Component<Props, {}, "value">;