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,239 @@
1
+ <script lang="ts">
2
+ import ChevronDown from "@lucide/svelte/icons/chevron-down";
3
+ import type { Snippet } from "svelte";
4
+ import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
5
+ import { cn } from "../util/cn.js";
6
+ import { fieldDescribedBy, fieldMessageState } from "../util/field.js";
7
+ import { FIELD_CONTROL_TRIGGER, fieldControlEdge, fieldControlSize } from "../util/field-control.js";
8
+ import { generateId } from "../util/ssr-safe.js";
9
+ import FieldMessages from "./FieldMessages.svelte";
10
+
11
+ /**
12
+ * A field-shaped trigger that opens whatever the app gives it, usually its
13
+ * own BottomSheet: the app owns the list, the search and the value. It
14
+ * looks like a Select of the same `size` and is one control: a `<button>`
15
+ * (a link with `href`) named by its label and its current value.
16
+ *
17
+ * Other attributes (`data-*`, `aria-*`, `onfocus`, ...) land on that
18
+ * element. The component does not open anything: `onclick` does, and the
19
+ * app sets `expanded` while its sheet is open.
20
+ */
21
+ type Props = Omit<
22
+ HTMLButtonAttributes,
23
+ | "class"
24
+ | "value"
25
+ | "name"
26
+ | "disabled"
27
+ | "type"
28
+ | "onclick"
29
+ | "aria-describedby"
30
+ | "aria-haspopup"
31
+ | "aria-expanded"
32
+ | "aria-controls"
33
+ | "children"
34
+ > & {
35
+ /** Extra classes for the host element. */
36
+ class?: string;
37
+ /** Id of the control. Omit to auto-generate. */
38
+ id?: string;
39
+ /** The label above the field. It is part of the control's name. */
40
+ label?: string;
41
+ /** The text of what is chosen. It is display text; `formValue` is what a form sends. */
42
+ value?: string;
43
+ /** Shown in the placeholder colour while there is no `value`. */
44
+ placeholder?: string;
45
+ /** An icon or an avatar before the text. */
46
+ leading?: Snippet;
47
+ /** Replaces the chevron at the end: a calendar or a clock, say. It is decoration, so it should be `aria-hidden`. */
48
+ trailing?: Snippet;
49
+ /** Replaces the value text, for rich content. The control is still named by `value`, so give that too. */
50
+ children?: Snippet;
51
+ size?: "sm" | "md" | "lg";
52
+ disabled?: boolean;
53
+ /** Status text under the field. Shown with a `success`, `warning` or `error` variant; for neutral help, use `hint`. */
54
+ message?: string;
55
+ variant?: "default" | "success" | "warning" | "error";
56
+ /** Help text under the field, read out with it. */
57
+ hint?: string;
58
+ /**
59
+ * An error under the field, announced. A button cannot take
60
+ * `aria-invalid`, so the error is the error edge colour and the
61
+ * described-by message; it wins over `variant` and `message`.
62
+ */
63
+ error?: string;
64
+ /** What the control opens: `aria-haspopup`. Default `"dialog"`. Not put on a link. */
65
+ haspopup?: "dialog" | "listbox" | "menu" | "true";
66
+ /** Whether what it opens is open now: `aria-expanded`, and the chevron turns. Leave it out and neither is set. Not put on a link. */
67
+ expanded?: boolean;
68
+ /** Id of what it opens: `aria-controls`. */
69
+ controls?: string;
70
+ /** Makes the control a link to this address, which works without scripts: a page that does the picking. With scripts, `onclick` may call `preventDefault()` and open a sheet instead. */
71
+ href?: string;
72
+ /** With `href`: where the link opens. */
73
+ target?: HTMLAnchorAttributes["target"];
74
+ /** With `href`: the link's relationship, such as `noopener`. */
75
+ rel?: HTMLAnchorAttributes["rel"];
76
+ /** The name a hidden input submits `formValue` under. Without it there is no hidden input. */
77
+ name?: string;
78
+ /** What the form submits for the choice, such as an id. */
79
+ formValue?: string;
80
+ /** The inner `<button>` (or `<a>`). Bindable: the app focuses it when its sheet closes, so a keyboard user is back where they were. */
81
+ element?: HTMLButtonElement | HTMLAnchorElement | null;
82
+ /** Ids of other elements that describe the field. The hint and the message are added after them. */
83
+ "aria-describedby"?: string | null;
84
+ onclick?: (event: MouseEvent) => void;
85
+ };
86
+
87
+ let {
88
+ class: className = "",
89
+ id: idProp,
90
+ label = "",
91
+ value = "",
92
+ placeholder = "",
93
+ leading,
94
+ trailing,
95
+ children,
96
+ size = "md",
97
+ disabled = false,
98
+ variant = "default",
99
+ message = "",
100
+ hint = "",
101
+ error = "",
102
+ haspopup = "dialog",
103
+ expanded,
104
+ controls,
105
+ href,
106
+ target,
107
+ rel,
108
+ name = "",
109
+ formValue = "",
110
+ element = $bindable(),
111
+ "aria-describedby": describedBy,
112
+ onclick,
113
+ ...restProps
114
+ }: Props = $props();
115
+
116
+ const fallbackId = generateId("picker-field");
117
+ const fieldId = $derived(idProp ?? fallbackId);
118
+ const labelId = $derived(`${fieldId}-label`);
119
+ const valueId = $derived(`${fieldId}-value`);
120
+
121
+ const isLink = $derived(href !== undefined && href !== null);
122
+ const status = $derived(fieldMessageState({ variant, message, hint, error }));
123
+ const isEmpty = $derived(value === "");
124
+ const sizeClass = $derived(fieldControlSize(size));
125
+
126
+ const controlClasses = $derived(
127
+ cn(
128
+ FIELD_CONTROL_TRIGGER,
129
+ sizeClass.box,
130
+ sizeClass.text,
131
+ fieldControlEdge(status.variant),
132
+ // `disabled:` does not match an `<a>`: a disabled link wears the pair itself.
133
+ isLink && disabled && "cursor-not-allowed border-input-border bg-input-disabled text-action-disabled-text hover:bg-input-disabled active:bg-input-disabled",
134
+ ),
135
+ );
136
+
137
+ /**
138
+ * The name is the label and the value (the placeholder while there is
139
+ * none), as a Select's is. Without a label the content names it. A
140
+ * caller's own `aria-label` or `aria-labelledby` wins.
141
+ */
142
+ const labelledBy = $derived(label ? `${labelId} ${valueId}` : undefined);
143
+
144
+ /** A disabled link has no `href`, but a pointer can still press it. */
145
+ function handleLinkClick(event: MouseEvent) {
146
+ if (disabled) {
147
+ event.preventDefault();
148
+ event.stopPropagation();
149
+ return;
150
+ }
151
+ onclick?.(event);
152
+ }
153
+ </script>
154
+
155
+ {#snippet content()}
156
+ {#if leading}
157
+ <span class="flex shrink-0 items-center" data-picker-field-leading>{@render leading()}</span>
158
+ {/if}
159
+ <!-- A value has no colour class of its own, and a disabled control none
160
+ at all: the control's `text-body` and its disabled colour are inherited
161
+ (a colour class here beat the disabled one in Select). -->
162
+ <span
163
+ id={valueId}
164
+ class={cn("min-w-0 flex-1 truncate text-start", isEmpty && !disabled && !children && "text-input-placeholder")}
165
+ >
166
+ {#if children}
167
+ {@render children()}
168
+ {:else}
169
+ {isEmpty ? placeholder : value}
170
+ {/if}
171
+ </span>
172
+ {#if trailing}
173
+ <span class="flex shrink-0 items-center text-description" data-picker-field-trailing>{@render trailing()}</span>
174
+ {:else}
175
+ <ChevronDown
176
+ size={20}
177
+ class={cn(
178
+ "shrink-0 text-description transition-transform duration-(--duration-moderate) motion-reduce:transition-none",
179
+ expanded && "rotate-180",
180
+ )}
181
+ aria-hidden="true"
182
+ />
183
+ {/if}
184
+ {/snippet}
185
+
186
+ <div class={cn("w-full min-w-0", className)}>
187
+ {#if label}
188
+ {#if isLink}
189
+ <!-- A link cannot be a label's control. -->
190
+ <span id={labelId} class="mb-2 block text-sm font-medium text-label">{label}</span>
191
+ {:else}
192
+ <label id={labelId} for={fieldId} class="mb-2 block text-sm font-medium text-label">{label}</label>
193
+ {/if}
194
+ {/if}
195
+
196
+ {#if isLink}
197
+ <!-- With `href` the control is a link with its role left native, and
198
+ no `aria-haspopup` or `aria-expanded`: it goes to a page. Disabled, it
199
+ has no `href` and says it is unavailable, as a disabled Button link. -->
200
+ <a
201
+ bind:this={element}
202
+ id={fieldId}
203
+ href={disabled ? undefined : href}
204
+ {target}
205
+ {rel}
206
+ class={controlClasses}
207
+ role={disabled ? "link" : undefined}
208
+ aria-disabled={disabled ? "true" : undefined}
209
+ aria-labelledby={labelledBy}
210
+ aria-describedby={fieldDescribedBy(fieldId, status, describedBy)}
211
+ {...(restProps as unknown as HTMLAnchorAttributes)}
212
+ onclick={handleLinkClick}
213
+ >
214
+ {@render content()}
215
+ </a>
216
+ {:else}
217
+ <button
218
+ bind:this={element}
219
+ type="button"
220
+ id={fieldId}
221
+ {disabled}
222
+ class={controlClasses}
223
+ aria-haspopup={haspopup}
224
+ aria-expanded={expanded}
225
+ aria-controls={controls}
226
+ aria-labelledby={labelledBy}
227
+ aria-describedby={fieldDescribedBy(fieldId, status, describedBy)}
228
+ {...restProps}
229
+ {onclick}
230
+ >
231
+ {@render content()}
232
+ </button>
233
+ {/if}
234
+
235
+ {#if name}
236
+ <input type="hidden" {name} value={formValue} {disabled} />
237
+ {/if}
238
+ <FieldMessages {fieldId} {status} {hint} gap="mt-1" messageClass="w-full" />
239
+ </div>
@@ -0,0 +1,67 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
3
+ /**
4
+ * A field-shaped trigger that opens whatever the app gives it, usually its
5
+ * own BottomSheet: the app owns the list, the search and the value. It
6
+ * looks like a Select of the same `size` and is one control: a `<button>`
7
+ * (a link with `href`) named by its label and its current value.
8
+ *
9
+ * Other attributes (`data-*`, `aria-*`, `onfocus`, ...) land on that
10
+ * element. The component does not open anything: `onclick` does, and the
11
+ * app sets `expanded` while its sheet is open.
12
+ */
13
+ type Props = Omit<HTMLButtonAttributes, "class" | "value" | "name" | "disabled" | "type" | "onclick" | "aria-describedby" | "aria-haspopup" | "aria-expanded" | "aria-controls" | "children"> & {
14
+ /** Extra classes for the host element. */
15
+ class?: string;
16
+ /** Id of the control. Omit to auto-generate. */
17
+ id?: string;
18
+ /** The label above the field. It is part of the control's name. */
19
+ label?: string;
20
+ /** The text of what is chosen. It is display text; `formValue` is what a form sends. */
21
+ value?: string;
22
+ /** Shown in the placeholder colour while there is no `value`. */
23
+ placeholder?: string;
24
+ /** An icon or an avatar before the text. */
25
+ leading?: Snippet;
26
+ /** Replaces the chevron at the end: a calendar or a clock, say. It is decoration, so it should be `aria-hidden`. */
27
+ trailing?: Snippet;
28
+ /** Replaces the value text, for rich content. The control is still named by `value`, so give that too. */
29
+ children?: Snippet;
30
+ size?: "sm" | "md" | "lg";
31
+ disabled?: boolean;
32
+ /** Status text under the field. Shown with a `success`, `warning` or `error` variant; for neutral help, use `hint`. */
33
+ message?: string;
34
+ variant?: "default" | "success" | "warning" | "error";
35
+ /** Help text under the field, read out with it. */
36
+ hint?: string;
37
+ /**
38
+ * An error under the field, announced. A button cannot take
39
+ * `aria-invalid`, so the error is the error edge colour and the
40
+ * described-by message; it wins over `variant` and `message`.
41
+ */
42
+ error?: string;
43
+ /** What the control opens: `aria-haspopup`. Default `"dialog"`. Not put on a link. */
44
+ haspopup?: "dialog" | "listbox" | "menu" | "true";
45
+ /** Whether what it opens is open now: `aria-expanded`, and the chevron turns. Leave it out and neither is set. Not put on a link. */
46
+ expanded?: boolean;
47
+ /** Id of what it opens: `aria-controls`. */
48
+ controls?: string;
49
+ /** Makes the control a link to this address, which works without scripts: a page that does the picking. With scripts, `onclick` may call `preventDefault()` and open a sheet instead. */
50
+ href?: string;
51
+ /** With `href`: where the link opens. */
52
+ target?: HTMLAnchorAttributes["target"];
53
+ /** With `href`: the link's relationship, such as `noopener`. */
54
+ rel?: HTMLAnchorAttributes["rel"];
55
+ /** The name a hidden input submits `formValue` under. Without it there is no hidden input. */
56
+ name?: string;
57
+ /** What the form submits for the choice, such as an id. */
58
+ formValue?: string;
59
+ /** The inner `<button>` (or `<a>`). Bindable: the app focuses it when its sheet closes, so a keyboard user is back where they were. */
60
+ element?: HTMLButtonElement | HTMLAnchorElement | null;
61
+ /** Ids of other elements that describe the field. The hint and the message are added after them. */
62
+ "aria-describedby"?: string | null;
63
+ onclick?: (event: MouseEvent) => void;
64
+ };
65
+ declare const PickerField: import("svelte").Component<Props, {}, "element">;
66
+ type PickerField = ReturnType<typeof PickerField>;
67
+ export default PickerField;
@@ -1,12 +1,30 @@
1
1
  <script lang="ts">
2
+ import { isDevBuild } from "../util/app-shell.js";
3
+ import {
4
+ PROGRESS_STRINGS,
5
+ canSegment,
6
+ filledSegments,
7
+ type ProgressStrings,
8
+ } from "../util/progress.js";
9
+ import { mergeStrings } from "../util/ready-made-strings.js";
2
10
  import { generateId } from "../util/ssr-safe.js";
11
+ import { zabiStringsFor } from "../util/zabi-strings.js";
3
12
 
4
13
  interface Props {
5
14
  /** Extra classes for the host element. */
6
15
  class?: string;
7
16
  value?: number;
8
17
  max?: number;
9
- size?: "sm" | "md" | "lg";
18
+ /** Height of the bar: 4, 8, 12 and 16px. */
19
+ size?: "sm" | "md" | "lg" | "xl";
20
+ /**
21
+ * Draws `max` separate segments, `value` of them filled, for a count
22
+ * such as 4 of 19. `max` must be a whole number from 2 to 24; any
23
+ * other `max` draws the continuous bar.
24
+ */
25
+ segmented?: boolean;
26
+ /** Overrides for the built-in strings. */
27
+ strings?: Partial<ProgressStrings>;
10
28
  label?: string;
11
29
  /** Names the progressbar when there is no visible `label`. Reaches the element with role="progressbar". */
12
30
  "aria-label"?: string;
@@ -19,6 +37,8 @@
19
37
  value = 0,
20
38
  max = 100,
21
39
  size = "md",
40
+ segmented = false,
41
+ strings,
22
42
  label = "",
23
43
  "aria-label": ariaLabel,
24
44
  "aria-labelledby": ariaLabelledby,
@@ -39,10 +59,27 @@
39
59
  ariaLabelledby ?? (ariaLabel || !label ? undefined : labelId),
40
60
  );
41
61
 
42
- let sizeClasses = $derived({
62
+ const sizeClasses = {
43
63
  sm: "h-1",
44
64
  md: "h-2",
45
65
  lg: "h-3",
66
+ xl: "h-4",
67
+ };
68
+
69
+ /** The app-wide words for this component, from a `ZabiStringsProvider` above it, if there is one. */
70
+ const provided = zabiStringsFor("progress");
71
+ const text = $derived(mergeStrings(PROGRESS_STRINGS, provided(), strings));
72
+
73
+ /** Segmented only where `max` can be drawn as segments; otherwise the continuous bar. */
74
+ let asSegments = $derived(segmented && canSegment(max));
75
+ let filled = $derived(filledSegments(value, safeMax));
76
+ let valueText = $derived(asSegments ? text.valueText(filled, safeMax) : undefined);
77
+
78
+ $effect(() => {
79
+ if (!isDevBuild() || !segmented || canSegment(max)) return;
80
+ console.warn(
81
+ "[zabi-components] Progress: `segmented` needs a whole-number `max` from 2 to 24; drawing the continuous bar instead.",
82
+ );
46
83
  });
47
84
  </script>
48
85
 
@@ -54,23 +91,47 @@
54
91
  <span id={labelId} class="text-sm font-medium text-label"
55
92
  >{label}</span
56
93
  >
57
- <span class="text-sm text-caption">{Math.round(percentage)}%</span>
94
+ <span class="text-sm text-caption">{asSegments ? valueText : `${Math.round(percentage)}%`}</span>
58
95
  </div>
59
96
  {/if}
60
97
 
61
- <div
62
- id={progressId}
63
- class="w-full border border-input-border bg-progress-track rounded-full overflow-hidden {sizeClasses[size]}"
64
- role="progressbar"
65
- aria-label={ariaLabel}
66
- aria-labelledby={labelledby}
67
- aria-valuenow={safeValue}
68
- aria-valuemin="0"
69
- aria-valuemax={safeMax}
70
- >
98
+ {#if asSegments}
71
99
  <div
72
- class="h-full bg-progress-fill rounded-full"
73
- style="width: {percentage}%"
74
- ></div>
75
- </div>
100
+ id={progressId}
101
+ class="flex w-full {safeMax > 12 ? 'gap-[2px]' : 'gap-1'} {sizeClasses[size]}"
102
+ role="progressbar"
103
+ aria-label={ariaLabel}
104
+ aria-labelledby={labelledby}
105
+ aria-valuenow={filled}
106
+ aria-valuemin="0"
107
+ aria-valuemax={safeMax}
108
+ aria-valuetext={valueText}
109
+ >
110
+ {#each { length: safeMax } as _, index (index)}
111
+ <!-- Presentational: the count is said once, by the progressbar. -->
112
+ <span
113
+ class="h-full flex-1 min-w-0 rounded-full {index < filled
114
+ ? 'bg-progress-fill forced-colors:bg-[Highlight]'
115
+ : 'bg-progress-track border border-progress-track-border forced-colors:border-[CanvasText]'}"
116
+ aria-hidden="true"
117
+ ></span>
118
+ {/each}
119
+ </div>
120
+ {:else}
121
+ <div
122
+ id={progressId}
123
+ class="w-full border border-progress-track-border bg-progress-track rounded-full overflow-hidden {sizeClasses[size]}"
124
+ role="progressbar"
125
+ aria-label={ariaLabel}
126
+ aria-labelledby={labelledby}
127
+ aria-valuenow={safeValue}
128
+ aria-valuemin="0"
129
+ aria-valuemax={safeMax}
130
+ >
131
+ <div
132
+ class="h-full bg-progress-fill rounded-full"
133
+ style="width: {percentage}%"
134
+ ></div>
135
+ </div>
136
+ {/if}
76
137
  </div>
@@ -1,9 +1,19 @@
1
+ import { type ProgressStrings } from "../util/progress.js";
1
2
  interface Props {
2
3
  /** Extra classes for the host element. */
3
4
  class?: string;
4
5
  value?: number;
5
6
  max?: number;
6
- size?: "sm" | "md" | "lg";
7
+ /** Height of the bar: 4, 8, 12 and 16px. */
8
+ size?: "sm" | "md" | "lg" | "xl";
9
+ /**
10
+ * Draws `max` separate segments, `value` of them filled, for a count
11
+ * such as 4 of 19. `max` must be a whole number from 2 to 24; any
12
+ * other `max` draws the continuous bar.
13
+ */
14
+ segmented?: boolean;
15
+ /** Overrides for the built-in strings. */
16
+ strings?: Partial<ProgressStrings>;
7
17
  label?: string;
8
18
  /** Names the progressbar when there is no visible `label`. Reaches the element with role="progressbar". */
9
19
  "aria-label"?: string;
@@ -1,5 +1,7 @@
1
1
  <script lang="ts">
2
+ import type { Snippet } from "svelte";
2
3
  import type { HTMLInputAttributes } from "svelte/elements";
4
+ import type { SemanticVariant } from "../types/variants.js";
3
5
  import SelectionControl from "./SelectionControl.svelte";
4
6
  import type { SelectionControlMarkProps } from "./SelectionControl.svelte";
5
7
  import { RADIO_CHECKED_DOT_CLASSES } from "./selection-control.styles";
@@ -20,6 +22,30 @@
20
22
  checked?: boolean;
21
23
  onChange?: (event: Event) => void;
22
24
  onchange?: (event: Event) => void;
25
+ /**
26
+ * Drawn inside the one label, between the box and the label text, as
27
+ * decoration for the name `label` already gives. A leading image
28
+ * should have an empty alt: the label names the row.
29
+ */
30
+ leading?: Snippet;
31
+ /**
32
+ * A line under the label, tied to the radio with `aria-describedby`.
33
+ * The label names the radio; the description does not join the name.
34
+ */
35
+ description?: string;
36
+ /**
37
+ * Where the radio sits in the row. `end` puts it after the leading,
38
+ * the label and the description, at the far edge; the row is still
39
+ * one label and fills the width it is given.
40
+ */
41
+ controlPosition?: "start" | "end";
42
+ variant?: SemanticVariant;
43
+ /** Status text under the control. Shown with a `success`, `warning` or `error` variant; for neutral help, use `hint`. */
44
+ message?: string;
45
+ /** Help text under the control, read out with it. */
46
+ hint?: string;
47
+ /** An error under the control. It marks the input invalid and is announced; it wins over `variant` and `message`. */
48
+ error?: string;
23
49
  };
24
50
 
25
51
  let {
@@ -33,6 +59,9 @@
33
59
  checked = $bindable<Exclude<Props["checked"], undefined>>(),
34
60
  onChange,
35
61
  onchange,
62
+ leading,
63
+ description,
64
+ controlPosition = "start",
36
65
  ...restProps
37
66
  }: Props = $props();
38
67
 
@@ -65,6 +94,9 @@
65
94
  {onChange}
66
95
  {onchange}
67
96
  {mark}
97
+ {leading}
98
+ {description}
99
+ {controlPosition}
68
100
  class={className}
69
101
  {...restProps}
70
102
  />
@@ -1,4 +1,6 @@
1
+ import type { Snippet } from "svelte";
1
2
  import type { HTMLInputAttributes } from "svelte/elements";
3
+ import type { SemanticVariant } from "../types/variants.js";
2
4
  /** Other attributes (`required`, `data-*`, `aria-*`, ...) land on the `<input>`. */
3
5
  type Props = Omit<HTMLInputAttributes, "class" | "id" | "name" | "value" | "disabled" | "checked" | "type" | "onchange"> & {
4
6
  /** Extra classes for the host element. */
@@ -12,6 +14,30 @@ type Props = Omit<HTMLInputAttributes, "class" | "id" | "name" | "value" | "disa
12
14
  checked?: boolean;
13
15
  onChange?: (event: Event) => void;
14
16
  onchange?: (event: Event) => void;
17
+ /**
18
+ * Drawn inside the one label, between the box and the label text, as
19
+ * decoration for the name `label` already gives. A leading image
20
+ * should have an empty alt: the label names the row.
21
+ */
22
+ leading?: Snippet;
23
+ /**
24
+ * A line under the label, tied to the radio with `aria-describedby`.
25
+ * The label names the radio; the description does not join the name.
26
+ */
27
+ description?: string;
28
+ /**
29
+ * Where the radio sits in the row. `end` puts it after the leading,
30
+ * the label and the description, at the far edge; the row is still
31
+ * one label and fills the width it is given.
32
+ */
33
+ controlPosition?: "start" | "end";
34
+ variant?: SemanticVariant;
35
+ /** Status text under the control. Shown with a `success`, `warning` or `error` variant; for neutral help, use `hint`. */
36
+ message?: string;
37
+ /** Help text under the control, read out with it. */
38
+ hint?: string;
39
+ /** An error under the control. It marks the input invalid and is announced; it wins over `variant` and `message`. */
40
+ error?: string;
15
41
  };
16
42
  declare const Radio: import("svelte").Component<Props, {}, "checked">;
17
43
  type Radio = ReturnType<typeof Radio>;