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,351 @@
1
+ <script lang="ts">
2
+ import { getContext, onMount, tick, type Snippet } from "svelte";
3
+ import type { HTMLAnchorAttributes, HTMLButtonAttributes, HTMLInputAttributes } from "svelte/elements";
4
+ import Check from "@lucide/svelte/icons/check";
5
+ import { cn } from "../util/cn.js";
6
+ import { groupBinding } from "../util/hydration.js";
7
+ import { CHIP_GROUP_CONTEXT_KEY, type ChipGroupContext } from "../util/chip.js";
8
+ import { TOUCH_HIT_AREA } from "../util/touch-target.js";
9
+
10
+ /**
11
+ * One type for the four forms, as Button has one for the button and the
12
+ * link: a union of the elements' attributes is too large to carry through
13
+ * a wrapper. Other attributes land on the element that is the control: the
14
+ * `<a>`, the `<button>` or the `<input>`; `class` is always the chip's.
15
+ */
16
+ type Props = Omit<HTMLButtonAttributes, "class" | "type" | "value" | "onchange" | "children"> & {
17
+ /** Makes the chip a link to this address, which works without scripts. */
18
+ href?: string;
19
+ target?: HTMLAnchorAttributes["target"];
20
+ rel?: HTMLAnchorAttributes["rel"];
21
+ download?: HTMLAnchorAttributes["download"];
22
+ hreflang?: HTMLAnchorAttributes["hreflang"];
23
+ /** With `href`: what `aria-current` says while `selected`. */
24
+ current?: "page" | "true" | "step" | "location" | "date" | "time";
25
+ /**
26
+ * `checkbox` or `radio`: a real input under the chip, which submits in
27
+ * a form without scripts. Without it (and without `href`) the chip is a
28
+ * toggle button. Inside a ChipGroup with a `type`, the group's.
29
+ * `submit` or `button`: a button that does not toggle. A press posts
30
+ * the form (`submit`, with `name` and `value`) or only calls `onclick`;
31
+ * `selected` is drawn as on a toggle and said with `aria-pressed`, and
32
+ * the app decides it, as a server's answer does.
33
+ */
34
+ type?: "checkbox" | "radio" | "submit" | "button";
35
+ /** Name the input is submitted under. Inside a ChipGroup with a `name`, the group's. */
36
+ name?: string;
37
+ /** What the input submits, and what a ChipGroup's `value` holds when it is chosen. */
38
+ value?: string;
39
+ /** The chip is chosen. Bindable; the one prop for `checked`, `aria-pressed` and `aria-current`. */
40
+ selected?: boolean;
41
+ required?: boolean;
42
+ form?: string;
43
+ /** 28, 32 or 40px tall. On a coarse pointer the touch target reaches 44px; the box does not grow. */
44
+ size?: "sm" | "md" | "lg";
45
+ /** An icon or an avatar before the label. */
46
+ leading?: Snippet;
47
+ /** A check icon before the label while selected. On for a checkbox and a toggle button, off for a link and a radio. */
48
+ checkmark?: boolean;
49
+ /**
50
+ * A button chip (`type="submit"`, `"button"` or a toggle) while its
51
+ * action is on its way, as Button's: `aria-busy` and `aria-disabled`,
52
+ * a spinner over the label, the press swallowed. The chip keeps its
53
+ * width and stays a Tab stop, so focus is not lost. Ignored by a link
54
+ * and by `checkbox` / `radio`, which are not actions.
55
+ */
56
+ loading?: boolean;
57
+ /** Called with the new `selected` when a person changes it. */
58
+ onchange?: (selected: boolean) => void;
59
+ class?: string;
60
+ children?: Snippet;
61
+ };
62
+
63
+ let {
64
+ href,
65
+ target,
66
+ rel,
67
+ download,
68
+ hreflang,
69
+ current = "true",
70
+ type: typeProp,
71
+ name: nameProp,
72
+ value,
73
+ selected = $bindable<Exclude<Props["selected"], undefined>>(),
74
+ disabled = false,
75
+ required,
76
+ form,
77
+ size = "md",
78
+ leading,
79
+ checkmark,
80
+ loading = false,
81
+ onchange,
82
+ onclick,
83
+ class: className = "",
84
+ children,
85
+ ...restProps
86
+ }: Props = $props();
87
+
88
+ // No fallback on a bindable prop: Svelte refuses `bind:…={undefined}` on
89
+ // one that has a fallback (`props_invalid_value`), and a page that throws
90
+ // while it hydrates never becomes interactive. The default is applied
91
+ // here instead: at once, for the server and the first render, and again
92
+ // whenever a parent hands back `undefined`.
93
+ const applyDefaults = () => {
94
+ if (selected === undefined) selected = false;
95
+ };
96
+ applyDefaults();
97
+ $effect.pre(applyDefaults);
98
+
99
+ const group = getContext<ChipGroupContext | undefined>(CHIP_GROUP_CONTEXT_KEY);
100
+ /** Inside a group that has a `type`, the group decides what is chosen. */
101
+ const grouped = $derived(group?.type !== undefined && typeProp === undefined && href === undefined);
102
+ const type = $derived(grouped ? group?.type : typeProp);
103
+ const name = $derived(nameProp ?? (grouped ? group?.name : undefined));
104
+ const isLink = $derived(href !== undefined && href !== null);
105
+ const isInput = $derived(!isLink && (type === "checkbox" || type === "radio"));
106
+ /** A button that does not toggle: the app says what is selected. */
107
+ const isFixedButton = $derived(!isLink && (typeProp === "submit" || typeProp === "button"));
108
+ const isDisabled = $derived(!!disabled || (grouped && !!group?.disabled));
109
+ const chosen = $derived(grouped ? !!group?.isSelected(value ?? "") : !!selected);
110
+ const showCheckmark = $derived(checkmark ?? (!isLink && type !== "radio"));
111
+
112
+ function setChosen(next: boolean, report: boolean) {
113
+ if (grouped) group?.change(value ?? "", next, report);
114
+ else selected = next;
115
+ }
116
+
117
+ let input: HTMLInputElement | undefined = $state();
118
+
119
+ /**
120
+ * The input is bound, so a choice made before the page hydrated is kept:
121
+ * Svelte's binding finds the input changed and hands its state over here
122
+ * instead of overwriting it (see util/hydration.ts). A choice adopted that
123
+ * way came without a `change` event that anything heard, so it is reported
124
+ * once, with a real `change` on the input, when the component has settled:
125
+ * the callbacks and a form around it hear it as they hear a press.
126
+ */
127
+ let settling = true;
128
+ let adopted = false;
129
+
130
+ function take(next: boolean) {
131
+ if (next === chosen) return;
132
+ setChosen(next, false);
133
+ if (settling) adopted = true;
134
+ }
135
+
136
+ const NOT_CHOSEN = {};
137
+ /** A radio on its own: its own value while it is checked. */
138
+ const aloneBinding = groupBinding<unknown>(
139
+ () => (selected ? value : NOT_CHOSEN),
140
+ (picked) => take(picked === value),
141
+ () => take(false),
142
+ );
143
+ /** A radio in a group: the group's value. */
144
+ const groupedBinding = groupBinding<string>(
145
+ () => group?.radioValue,
146
+ (picked) => {
147
+ if (group?.radioValue === picked) return;
148
+ group?.change(picked, true, false);
149
+ if (settling) adopted = true;
150
+ },
151
+ () => group?.restore(),
152
+ );
153
+
154
+ onMount(() => {
155
+ // The bindings adopt in a microtask of their own, queued before this one.
156
+ void tick().then(() => {
157
+ settling = false;
158
+ if (adopted) input?.dispatchEvent(new Event("change", { bubbles: true }));
159
+ });
160
+ });
161
+
162
+ function handleInputChange(event: Event) {
163
+ if (isDisabled) return;
164
+ // A real change is its own report: nothing is left to tell.
165
+ adopted = false;
166
+ const checked = (event.currentTarget as HTMLInputElement).checked;
167
+ setChosen(checked, true);
168
+ onchange?.(checked);
169
+ }
170
+
171
+ /** Loading without `disabled`: unavailable, and still where focus is (see Button). */
172
+ const isBusy = $derived(loading && !disabled);
173
+
174
+ function handleButtonClick(event: MouseEvent & { currentTarget: EventTarget & HTMLButtonElement }) {
175
+ if (isBusy) {
176
+ // The press ends here: it does not submit, toggle or reach `onclick`.
177
+ event.preventDefault();
178
+ event.stopPropagation();
179
+ return;
180
+ }
181
+ onclick?.(event);
182
+ if (event.defaultPrevented || isDisabled) return;
183
+ if (isFixedButton) return;
184
+ selected = !selected;
185
+ onchange?.(selected);
186
+ }
187
+
188
+ /** A disabled link has no `href`, but a pointer can still press it. */
189
+ function handleLinkClick(event: MouseEvent) {
190
+ if (isDisabled) {
191
+ event.preventDefault();
192
+ event.stopPropagation();
193
+ return;
194
+ }
195
+ (onclick as ((event: MouseEvent) => void) | null | undefined)?.(event);
196
+ }
197
+
198
+ /**
199
+ * 28 / 32 / 40px, a fixed height: a chip never wraps its label, so one
200
+ * line is all it ever holds. The padding is 10 / 12 / 16px.
201
+ */
202
+ const SIZE = { sm: "h-7 px-[10px]", md: "h-8 px-3", lg: "h-10 px-4" } as const;
203
+ const FORCED = "forced-colors:outline-2 forced-colors:-outline-offset-2 forced-colors:outline-[color:Highlight]";
204
+
205
+ const base = $derived(
206
+ `group/chip relative inline-flex shrink-0 cursor-pointer select-none items-center justify-center gap-[6px] whitespace-nowrap rounded-pill text-sm transition-colors duration-(--duration-base) motion-reduce:transition-none ${SIZE[size] ?? SIZE.md} ${TOUCH_HIT_AREA} ${isDisabled ? "pointer-events-none cursor-not-allowed opacity-50" : ""}`,
207
+ );
208
+
209
+ /**
210
+ * A link and a button read `selected`. The hand-written chip colours are
211
+ * outside every layer, so they cannot be overridden by a variant of
212
+ * another colour: the two states are two class lists, never both.
213
+ */
214
+ const elementClasses = $derived(
215
+ cn(
216
+ base,
217
+ "focus-ring",
218
+ chosen
219
+ ? `bg-chip-selected hover:bg-chip-selected-hover active:bg-chip-selected-active text-chip-selected-text font-semibold ${FORCED}`
220
+ : "bg-chip hover:bg-chip-hover active:bg-chip-active text-chip-text font-medium",
221
+ isBusy && "cursor-progress",
222
+ className,
223
+ ),
224
+ );
225
+
226
+ /**
227
+ * An input is chosen by the browser, and the look follows its `:checked`
228
+ * (not `selected`), so a choice made before hydration shows at once.
229
+ * Every colour is a `has-` or `not-has-` variant: with the plain
230
+ * hand-written class on the label, the unlayered rule would win over the
231
+ * selected variant.
232
+ */
233
+ const labelClasses = $derived(
234
+ cn(
235
+ base,
236
+ "focus-ring font-medium not-has-[:checked]:bg-chip not-has-[:checked]:text-chip-text not-has-[:checked]:hover:bg-chip-hover not-has-[:checked]:active:bg-chip-active has-[:checked]:bg-chip-selected has-[:checked]:text-chip-selected-text has-[:checked]:font-semibold has-[:checked]:hover:bg-chip-selected-hover has-[:checked]:active:bg-chip-selected-active",
237
+ "has-[:checked]:forced-colors:outline-2 has-[:checked]:forced-colors:-outline-offset-2 has-[:checked]:forced-colors:outline-[color:Highlight]",
238
+ "has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-offset-2 has-[:focus-visible]:outline-focus-ring",
239
+ className,
240
+ ),
241
+ );
242
+
243
+ /** The real input, stretched over the chip and transparent, so a press lands on it. */
244
+ const INPUT_CLASSES = "absolute inset-0 m-0 size-full cursor-pointer appearance-none rounded-pill opacity-0 outline-none disabled:cursor-not-allowed";
245
+ </script>
246
+
247
+ {#snippet inner(checkClass: string)}
248
+ {#if showCheckmark}
249
+ <Check class="size-3.5 shrink-0 {checkClass}" aria-hidden="true" />
250
+ {/if}
251
+ {#if leading}
252
+ <span class="inline-flex shrink-0 items-center">{@render leading()}</span>
253
+ {/if}
254
+ {#if children}
255
+ <span class="min-w-0 truncate">{@render children()}</span>
256
+ {/if}
257
+ {/snippet}
258
+
259
+ {#if isLink}
260
+ <!-- A link that cannot be followed keeps its role and says it is unavailable, as Button's does. -->
261
+ <a
262
+ href={isDisabled ? undefined : href}
263
+ {target}
264
+ {rel}
265
+ {download}
266
+ {hreflang}
267
+ class={elementClasses}
268
+ role={isDisabled ? "link" : undefined}
269
+ aria-disabled={isDisabled ? "true" : undefined}
270
+ aria-current={chosen ? current : undefined}
271
+ {...restProps as unknown as HTMLAnchorAttributes}
272
+ onclick={handleLinkClick}
273
+ >
274
+ {@render inner(chosen ? "" : "hidden")}
275
+ </a>
276
+ {:else if isInput}
277
+ <label class={labelClasses} data-chip-form={type}>
278
+ <!-- One branch per type: a binding needs a type it can read. -->
279
+ {#if type === "checkbox"}
280
+ <input
281
+ bind:this={input}
282
+ type="checkbox"
283
+ {name}
284
+ {value}
285
+ {required}
286
+ {form}
287
+ bind:checked={() => chosen, take}
288
+ disabled={isDisabled}
289
+ class={INPUT_CLASSES}
290
+ onchange={handleInputChange}
291
+ {...restProps as unknown as HTMLInputAttributes}
292
+ />
293
+ {:else if grouped}
294
+ <input
295
+ bind:this={input}
296
+ type="radio"
297
+ {name}
298
+ {value}
299
+ {required}
300
+ {form}
301
+ bind:group={groupedBinding.value}
302
+ disabled={isDisabled}
303
+ class={INPUT_CLASSES}
304
+ onchange={handleInputChange}
305
+ {...restProps as unknown as HTMLInputAttributes}
306
+ />
307
+ {:else}
308
+ <input
309
+ bind:this={input}
310
+ type="radio"
311
+ {name}
312
+ {value}
313
+ {required}
314
+ {form}
315
+ bind:group={aloneBinding.value}
316
+ disabled={isDisabled}
317
+ class={INPUT_CLASSES}
318
+ onchange={handleInputChange}
319
+ {...restProps as unknown as HTMLInputAttributes}
320
+ />
321
+ {/if}
322
+ {@render inner("hidden group-has-checked/chip:block")}
323
+ </label>
324
+ {:else}
325
+ <button
326
+ type={typeProp === "submit" ? "submit" : "button"}
327
+ {name}
328
+ {value}
329
+ {form}
330
+ class={elementClasses}
331
+ {disabled}
332
+ aria-pressed={chosen}
333
+ aria-busy={isBusy ? "true" : undefined}
334
+ aria-disabled={isBusy ? "true" : undefined}
335
+ {...restProps}
336
+ onclick={handleButtonClick}
337
+ >
338
+ {#if isBusy}
339
+ <!-- The label stays in the chip, only unseen, so the width and the name do not change. -->
340
+ <span class="inline-flex min-w-0 items-center justify-center gap-[6px] opacity-0">
341
+ {@render inner(chosen ? "" : "hidden")}
342
+ </span>
343
+ <span
344
+ class="pointer-events-none absolute inline-block size-3.5 animate-spin rounded-full border-2 border-current border-t-transparent motion-reduce:animate-pulse"
345
+ aria-hidden="true"
346
+ ></span>
347
+ {:else}
348
+ {@render inner(chosen ? "" : "hidden")}
349
+ {/if}
350
+ </button>
351
+ {/if}
@@ -0,0 +1,57 @@
1
+ import { type Snippet } from "svelte";
2
+ import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
3
+ /**
4
+ * One type for the four forms, as Button has one for the button and the
5
+ * link: a union of the elements' attributes is too large to carry through
6
+ * a wrapper. Other attributes land on the element that is the control: the
7
+ * `<a>`, the `<button>` or the `<input>`; `class` is always the chip's.
8
+ */
9
+ type Props = Omit<HTMLButtonAttributes, "class" | "type" | "value" | "onchange" | "children"> & {
10
+ /** Makes the chip a link to this address, which works without scripts. */
11
+ href?: string;
12
+ target?: HTMLAnchorAttributes["target"];
13
+ rel?: HTMLAnchorAttributes["rel"];
14
+ download?: HTMLAnchorAttributes["download"];
15
+ hreflang?: HTMLAnchorAttributes["hreflang"];
16
+ /** With `href`: what `aria-current` says while `selected`. */
17
+ current?: "page" | "true" | "step" | "location" | "date" | "time";
18
+ /**
19
+ * `checkbox` or `radio`: a real input under the chip, which submits in
20
+ * a form without scripts. Without it (and without `href`) the chip is a
21
+ * toggle button. Inside a ChipGroup with a `type`, the group's.
22
+ * `submit` or `button`: a button that does not toggle. A press posts
23
+ * the form (`submit`, with `name` and `value`) or only calls `onclick`;
24
+ * `selected` is drawn as on a toggle and said with `aria-pressed`, and
25
+ * the app decides it, as a server's answer does.
26
+ */
27
+ type?: "checkbox" | "radio" | "submit" | "button";
28
+ /** Name the input is submitted under. Inside a ChipGroup with a `name`, the group's. */
29
+ name?: string;
30
+ /** What the input submits, and what a ChipGroup's `value` holds when it is chosen. */
31
+ value?: string;
32
+ /** The chip is chosen. Bindable; the one prop for `checked`, `aria-pressed` and `aria-current`. */
33
+ selected?: boolean;
34
+ required?: boolean;
35
+ form?: string;
36
+ /** 28, 32 or 40px tall. On a coarse pointer the touch target reaches 44px; the box does not grow. */
37
+ size?: "sm" | "md" | "lg";
38
+ /** An icon or an avatar before the label. */
39
+ leading?: Snippet;
40
+ /** A check icon before the label while selected. On for a checkbox and a toggle button, off for a link and a radio. */
41
+ checkmark?: boolean;
42
+ /**
43
+ * A button chip (`type="submit"`, `"button"` or a toggle) while its
44
+ * action is on its way, as Button's: `aria-busy` and `aria-disabled`,
45
+ * a spinner over the label, the press swallowed. The chip keeps its
46
+ * width and stays a Tab stop, so focus is not lost. Ignored by a link
47
+ * and by `checkbox` / `radio`, which are not actions.
48
+ */
49
+ loading?: boolean;
50
+ /** Called with the new `selected` when a person changes it. */
51
+ onchange?: (selected: boolean) => void;
52
+ class?: string;
53
+ children?: Snippet;
54
+ };
55
+ declare const Chip: import("svelte").Component<Props, {}, "selected">;
56
+ type Chip = ReturnType<typeof Chip>;
57
+ export default Chip;