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
@@ -3,6 +3,7 @@
3
3
  import type { HTMLAttributes } from "svelte/elements";
4
4
  import { cn } from "../util/cn.js";
5
5
  import type { OnFillTone } from "../types/variants.js";
6
+ import { displayVoice } from "../util/display-voice.js";
6
7
 
7
8
  /** Semantic heading level — also the default visual size. */
8
9
  type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
@@ -23,6 +24,13 @@
23
24
  * `on-accent`.
24
25
  */
25
26
  tone?: "headline" | OnFillTone;
27
+ /**
28
+ * `heading` is the document heading voice. `display` is the second
29
+ * face (`--font-family-display`) with larger steps, for a hero, a
30
+ * score or a big number; its weight and tracking are the custom
31
+ * properties `--zabi-display-weight` and `--zabi-display-tracking`.
32
+ */
33
+ variant?: "heading" | "display";
26
34
  class?: string;
27
35
  children?: Snippet;
28
36
  };
@@ -32,6 +40,7 @@
32
40
  size,
33
41
  text = "",
34
42
  tone = "headline",
43
+ variant = "heading",
35
44
  class: className = "",
36
45
  children,
37
46
  ...restProps
@@ -61,8 +70,25 @@
61
70
  6: "text-base leading-6 font-semibold",
62
71
  };
63
72
 
73
+ /**
74
+ * Display steps: 60px (48 on a phone), 48 (36), 36 (30), 30, 24, 20. Weight and tracking come from
75
+ * `displayVoice`, so no size carries its own.
76
+ */
77
+ const displaySizeClasses: Record<HeadingLevel, string> = {
78
+ 1: "text-5xl sm:text-6xl leading-[1.0]",
79
+ 2: "text-4xl sm:text-5xl leading-[1.05]",
80
+ 3: "text-3xl sm:text-4xl leading-[1.1]",
81
+ 4: "text-3xl leading-[1.2]",
82
+ 5: "text-2xl leading-[1.2]",
83
+ 6: "text-xl leading-[1.2]",
84
+ };
85
+
64
86
  const headingClasses = $derived(
65
- cn(`${toneClasses[tone] ?? toneClasses.headline} ${sizeClasses[visualSize] ?? sizeClasses[6]} ${className}`),
87
+ variant === "display"
88
+ ? cn(
89
+ `${toneClasses[tone] ?? toneClasses.headline} font-display text-balance ${displayVoice} ${displaySizeClasses[visualSize] ?? displaySizeClasses[6]} ${className}`,
90
+ )
91
+ : cn(`${toneClasses[tone] ?? toneClasses.headline} ${sizeClasses[visualSize] ?? sizeClasses[6]} ${className}`),
66
92
  );
67
93
  </script>
68
94
 
@@ -19,6 +19,13 @@ type Props = Omit<HTMLAttributes<HTMLHeadingElement>, "class"> & {
19
19
  * `on-accent`.
20
20
  */
21
21
  tone?: "headline" | OnFillTone;
22
+ /**
23
+ * `heading` is the document heading voice. `display` is the second
24
+ * face (`--font-family-display`) with larger steps, for a hero, a
25
+ * score or a big number; its weight and tracking are the custom
26
+ * properties `--zabi-display-weight` and `--zabi-display-tracking`.
27
+ */
28
+ variant?: "heading" | "display";
22
29
  class?: string;
23
30
  children?: Snippet;
24
31
  };
@@ -3,6 +3,9 @@
3
3
  import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
4
4
  import type { ButtonVariant, SizeVariant } from "../types/variants.js";
5
5
  import { cn } from "../util/cn.js";
6
+ import { isDevBuild } from "../util/app-shell.js";
7
+ import { badgeText, defaultBadgeLabel } from "../util/bottom-tab-bar.js";
8
+ import Badge from "./Badge.svelte";
6
9
 
7
10
  /** The shared scale plus `xs`, which only the icon button has. */
8
11
  type IconButtonSize = SizeVariant | "xs";
@@ -56,6 +59,19 @@
56
59
  loading?: boolean;
57
60
  /** Required for icon-only usage (no visible text). */
58
61
  label?: string;
62
+ /**
63
+ * A count drawn on the button, in the attention colour, inside the
64
+ * button's own box (so a bar or an `overflow` ancestor cannot cut it).
65
+ * Nothing is drawn for 0, a negative number or when it is left out;
66
+ * while `loading` it is hidden. At `xs` and `sm` there is no room for
67
+ * a number: a dot is drawn instead. Needs `label`: the accessible name
68
+ * becomes the label, a comma and `countLabel(count)`.
69
+ */
70
+ count?: number;
71
+ /** What the count adds to the accessible name, after the label. Default: "3 new". */
72
+ countLabel?: (count: number) => string;
73
+ /** Above this the badge reads `99+`. The accessible name keeps the real count. */
74
+ countMax?: number;
59
75
  class?: string;
60
76
  children?: Snippet;
61
77
  };
@@ -70,6 +86,9 @@
70
86
  type,
71
87
  href,
72
88
  label = "",
89
+ count,
90
+ countLabel = defaultBadgeLabel,
91
+ countMax = 99,
73
92
  class: className = "",
74
93
  onclick,
75
94
  children,
@@ -77,6 +96,22 @@
77
96
  }: Props = $props();
78
97
 
79
98
  const isDisabled = $derived(!!disabled || loading);
99
+
100
+ /** A count is drawn only for a positive number, and not while loading. */
101
+ const hasCount = $derived(typeof count === "number" && count > 0);
102
+ const showCount = $derived(hasCount && !loading);
103
+ /** `xs` and `sm` have no room for a number beside the icon: a dot. */
104
+ const countAsDot = $derived(size === "xs" || size === "sm");
105
+ const accessibleName = $derived(
106
+ showCount && label ? `${label}, ${countLabel(count as number)}` : label || undefined,
107
+ );
108
+
109
+ $effect(() => {
110
+ if (!isDevBuild() || !hasCount || label) return;
111
+ console.warn(
112
+ "[zabi-components] IconButton: `count` needs a `label`; without it the count has no accessible name.",
113
+ );
114
+ });
80
115
  /**
81
116
  * Loading without `disabled`: unavailable, and still where focus is.
82
117
  *
@@ -157,13 +192,18 @@
157
192
  });
158
193
 
159
194
  const disabledClass =
160
- "disabled:bg-action-disabled disabled:text-action-disabled-text disabled:border-transparent disabled:shadow-none disabled:cursor-not-allowed disabled:active:scale-100";
195
+ "disabled:bg-action-disabled disabled:text-action-disabled-text disabled:border-transparent disabled:bg-none disabled:inset-shadow-none disabled:shadow-none disabled:cursor-not-allowed disabled:active:scale-100";
161
196
 
162
197
  const isDangerTone = $derived(
163
198
  tone === "danger" && (variant === "ghost" || variant === "outline"),
164
199
  );
165
200
 
166
201
  const variantClass = $derived.by(() => {
202
+ // The veil of the solid fills, as on Button. Not when disabled, and
203
+ // not on a toggled-on button (`pressedClass`): that holds the pressed
204
+ // fill flat, with its outline as the shape.
205
+ const veil = disabled || (isToggle && pressed) ? "" : "bg-control-gradient ";
206
+ const veilAccent = disabled || (isToggle && pressed) ? "" : "bg-control-gradient-accent ";
167
207
  if (isDangerTone) {
168
208
  const fill =
169
209
  "text-error hover:bg-action-danger-subtle active:bg-action-danger-subtle-hover active:scale-[0.98] motion-reduce:active:scale-100 focus-ring--danger";
@@ -174,19 +214,22 @@
174
214
  switch (variant) {
175
215
  case "secondary":
176
216
  return "bg-action-secondary text-headline hover:bg-action-secondary-hover active:bg-action-secondary-active active:scale-[0.98] motion-reduce:active:scale-100";
217
+ case "tonal":
218
+ return "bg-action-tonal text-action-tonal active:scale-[0.98] motion-reduce:active:scale-100";
177
219
  case "danger":
178
- return "bg-action-danger text-action-danger-text hover:bg-action-danger-hover active:bg-action-danger-active active:scale-[0.98] motion-reduce:active:scale-100 focus-ring--danger";
220
+ return `${veil}bg-action-danger text-action-danger-text hover:bg-action-danger-hover active:bg-action-danger-active active:scale-[0.98] motion-reduce:active:scale-100 focus-ring--danger`;
179
221
  case "ghost":
180
222
  return "bg-transparent text-headline hover:bg-surface-hover active:bg-surface-active active:scale-[0.98] motion-reduce:active:scale-100 focus-ring--muted";
181
223
  case "outline":
182
- return "bg-transparent border border-border text-headline hover:bg-surface-hover hover:border-border-medium active:bg-surface-active active:scale-[0.98] motion-reduce:active:scale-100";
224
+ return "bg-transparent border border-action-outline text-headline hover:bg-surface-hover hover:border-action-outline-hover active:bg-surface-active active:scale-[0.98] motion-reduce:active:scale-100";
183
225
  case "link":
226
+ case "text":
184
227
  return "bg-transparent text-link hover:text-link-hover focus-ring--muted";
185
228
  case "accent":
186
- return "bg-accent text-on-accent hover:bg-accent-hover active:bg-accent-active active:scale-[0.98] motion-reduce:active:scale-100";
229
+ return `${veilAccent}bg-accent text-on-accent hover:bg-accent-hover active:bg-accent-active active:scale-[0.98] motion-reduce:active:scale-100`;
187
230
  case "primary":
188
231
  default:
189
- return "bg-action-primary text-action-primary hover:bg-action-primary-hover active:bg-action-primary-active active:scale-[0.98] motion-reduce:active:scale-100";
232
+ return `${veil}bg-action-primary text-action-primary hover:bg-action-primary-hover active:bg-action-primary-active active:scale-[0.98] motion-reduce:active:scale-100`;
190
233
  }
191
234
  });
192
235
 
@@ -215,7 +258,12 @@
215
258
  case "ghost":
216
259
  case "outline":
217
260
  case "link":
261
+ case "text":
218
262
  return `bg-action-primary-subtle text-link hover:bg-action-primary-subtle-hover active:bg-action-primary-subtle-active ${outline}`;
263
+ case "tonal":
264
+ // The tonal fill keeps its own hover and pressed steps; the
265
+ // outline is what shows it is on.
266
+ return outline;
219
267
  case "accent":
220
268
  return `bg-accent-active hover:bg-accent-active ${outline}`;
221
269
  case "primary":
@@ -226,7 +274,7 @@
226
274
 
227
275
  const buttonClasses = $derived.by(() => {
228
276
  const base =
229
- "inline-flex focus-ring items-center justify-center rounded-control shrink-0 transition-colors duration-150 cursor-pointer select-none";
277
+ "inline-flex focus-ring items-center justify-center rounded-control shrink-0 transition-colors duration-(--duration-base) cursor-pointer select-none";
230
278
  // `disabled:` does not match an `<a>`: a disabled link wears the
231
279
  // disabled pair itself, in place of its variant. So does a loading
232
280
  // button, which is not `:disabled` (it keeps focus): with the
@@ -236,7 +284,8 @@
236
284
  isDisabled && (isLink || isBusy)
237
285
  ? "bg-(--color-action-disabled) text-action-disabled-text border border-transparent cursor-not-allowed"
238
286
  : `${variantClass} ${isLink ? "" : pressedClass} ${disabledClass}`;
239
- return cn(`${base} ${sizeClass.box} ${state} ${className}`);
287
+ // Relative only for the badge: it is placed against the button's own box.
288
+ return cn(`${base} ${showCount ? "relative " : ""}${sizeClass.box} ${state} ${className}`);
240
289
  });
241
290
  </script>
242
291
 
@@ -249,6 +298,29 @@
249
298
  {:else if children}
250
299
  {@render children()}
251
300
  {/if}
301
+ {#if showCount}
302
+ <!-- Inside the box, in its upper end corner, whatever the size: the box
303
+ edge is the outer edge, so nothing outside it can cut it. Decorative; the
304
+ name carries the count. -->
305
+ {#if countAsDot}
306
+ <span
307
+ class="pointer-events-none absolute end-0.5 top-0.5 size-2 rounded-full bg-error"
308
+ aria-hidden="true"
309
+ data-icon-button-count="dot"
310
+ ></span>
311
+ {:else}
312
+ <Badge
313
+ size="sm"
314
+ emphasis="solid"
315
+ variant="error"
316
+ aria-hidden="true"
317
+ data-icon-button-count="badge"
318
+ class="pointer-events-none absolute end-0.5 top-0.5 h-[18px] min-h-[18px] min-w-[18px] px-[5px] py-0 text-[11px] leading-[18px] whitespace-nowrap"
319
+ >
320
+ {badgeText(count as number, countMax)}
321
+ </Badge>
322
+ {/if}
323
+ {/if}
252
324
  {/snippet}
253
325
 
254
326
  {#if isLink}
@@ -261,7 +333,7 @@
261
333
  role={isDisabled ? "link" : undefined}
262
334
  aria-disabled={isDisabled ? "true" : undefined}
263
335
  aria-busy={loading ? "true" : undefined}
264
- aria-label={label || undefined}
336
+ aria-label={accessibleName}
265
337
  {...(restProps as unknown as HTMLAnchorAttributes)}
266
338
  onclick={handleLinkClick}
267
339
  >
@@ -276,7 +348,7 @@
276
348
  aria-busy={loading ? "true" : undefined}
277
349
  aria-pressed={pressed}
278
350
  onclick={handleClick}
279
- aria-label={label || undefined}
351
+ aria-label={accessibleName}
280
352
  {...restProps}
281
353
  >
282
354
  {@render content()}
@@ -52,6 +52,19 @@ type Props = Omit<HTMLButtonAttributes, "class"> & {
52
52
  loading?: boolean;
53
53
  /** Required for icon-only usage (no visible text). */
54
54
  label?: string;
55
+ /**
56
+ * A count drawn on the button, in the attention colour, inside the
57
+ * button's own box (so a bar or an `overflow` ancestor cannot cut it).
58
+ * Nothing is drawn for 0, a negative number or when it is left out;
59
+ * while `loading` it is hidden. At `xs` and `sm` there is no room for
60
+ * a number: a dot is drawn instead. Needs `label`: the accessible name
61
+ * becomes the label, a comma and `countLabel(count)`.
62
+ */
63
+ count?: number;
64
+ /** What the count adds to the accessible name, after the label. Default: "3 new". */
65
+ countLabel?: (count: number) => string;
66
+ /** Above this the badge reads `99+`. The accessible name keeps the real count. */
67
+ countMax?: number;
55
68
  class?: string;
56
69
  children?: Snippet;
57
70
  };
@@ -61,8 +61,9 @@
61
61
  /** Drawn inside the field, after the text and before the loading spinner's place. */
62
62
  trailing?: Snippet;
63
63
  /**
64
- * With `type="password"`: adds a button at the end of the field that
65
- * shows the password as text, and hides it again. Ignored for other types.
64
+ * With `type="password"`: a button at the end of the field that shows
65
+ * the password as text, and hides it again. On by default; `false`
66
+ * leaves it out. Ignored for other types.
66
67
  */
67
68
  revealable?: boolean;
68
69
  /**
@@ -96,7 +97,7 @@
96
97
  error = "",
97
98
  leading,
98
99
  trailing,
99
- revealable = false,
100
+ revealable = true,
100
101
  revealLabel: revealLabelGiven,
101
102
  oninput,
102
103
  onblur,
@@ -254,9 +255,13 @@
254
255
  * field regardless of the layout around it.
255
256
  */
256
257
  const baseClasses =
257
- "focus-ring w-full border bg-input hover:bg-input-hover focus-visible:bg-input-focus disabled:bg-input-disabled rounded-control transition-colors duration-150 placeholder:text-input-placeholder text-body focus:outline-none focus-visible:outline-none disabled:text-action-disabled-text disabled:cursor-not-allowed";
258
+ "focus-ring w-full border bg-input hover:bg-input-hover focus-visible:bg-input-focus disabled:bg-input-disabled rounded-control transition-colors duration-150 placeholder:text-input-placeholder placeholder-shown:text-ellipsis text-body focus:outline-none focus-visible:outline-none disabled:text-action-disabled-text disabled:cursor-not-allowed";
258
259
 
259
- return cn(`${baseClasses} ${sizeStyles.box} ${spinnerPad} ${sizeStyles.text} ${variantClass()} ${className}`);
260
+ // A search field that brings its own clear button in `trailing` would
261
+ // show two crosses: the browser's is taken away.
262
+ const nativeClear = trailing ? "[&::-webkit-search-cancel-button]:appearance-none" : "";
263
+
264
+ return cn(`${baseClasses} ${sizeStyles.box} ${spinnerPad} ${sizeStyles.text} ${variantClass()} ${nativeClear} ${className}`);
260
265
  });
261
266
 
262
267
  const labelClasses = $derived(
@@ -35,8 +35,9 @@ type Props = Omit<HTMLInputAttributes, "class" | "size" | "value" | "type" | "id
35
35
  /** Drawn inside the field, after the text and before the loading spinner's place. */
36
36
  trailing?: Snippet;
37
37
  /**
38
- * With `type="password"`: adds a button at the end of the field that
39
- * shows the password as text, and hides it again. Ignored for other types.
38
+ * With `type="password"`: a button at the end of the field that shows
39
+ * the password as text, and hides it again. On by default; `false`
40
+ * leaves it out. Ignored for other types.
40
41
  */
41
42
  revealable?: boolean;
42
43
  /**
@@ -34,8 +34,9 @@
34
34
  const listClasses = $derived(
35
35
  // The same radius as its rows. Not `overflow-hidden`: the rows fill
36
36
  // the list edge to edge, so clipping here cut a row's focus ring off
37
- // on every side but the gap to the next row.
38
- cn(`space-y-1 rounded-[var(--zabi-list-row-radius,var(--radius-container))] ${className}`),
37
+ // on every side but the gap to the next row. `list-dividers` draws the
38
+ // hairline in the gap between rows (app.css).
39
+ cn(`list-dividers space-y-1 rounded-[var(--zabi-list-row-radius,var(--radius-container))] ${className}`),
39
40
  );
40
41
  </script>
41
42
 
@@ -13,6 +13,12 @@
13
13
  avatar?: string;
14
14
  /** `avatar` alt text; `""` if decorative. */
15
15
  avatarAlt?: string;
16
+ /**
17
+ * Leading picture drawn in the round, in place of an address: an
18
+ * inline SVG. Wins over `avatar` and `icon`. Decorative unless
19
+ * `avatarAlt` is set, which then names it.
20
+ */
21
+ avatarPicture?: Snippet;
16
22
  target?: "_self" | "_blank" | "_parent" | "_top";
17
23
  rel?: string;
18
24
  disabled?: boolean;
@@ -52,9 +58,14 @@
52
58
  const baseClasses =
53
59
  // The container radius, or the one a `.list-group` shell around the list
54
60
  // hands down so the row is concentric with it (see app.css).
55
- "group flex w-full items-center gap-3 rounded-[var(--zabi-list-row-radius,var(--radius-container))] border px-4 py-3 pr-5 text-left transition-all duration-150";
61
+ // The 1px border is kept so the row's height and text do not move, but
62
+ // it is transparent: rows are told apart by the divider between them
63
+ // (app.css), and a bordered row inside a bordered shell was two
64
+ // outlines. `--zabi-list-row-border-color` puts the 8.1 edge back.
65
+ "group flex w-full items-center gap-3 rounded-[var(--zabi-list-row-radius,var(--radius-container))] border px-4 py-3 pr-5 text-left transition-colors duration-(--duration-base)";
66
+ const edge = "border-[color:var(--zabi-list-row-border-color,transparent)]";
56
67
  if (!interactive) {
57
- const tone = selected ? "bg-action-primary-subtle border-action-primary" : "border-border";
68
+ const tone = selected ? "bg-action-primary-subtle border-action-primary" : edge;
58
69
  return `${baseClasses} ${tone} ${item.disabled ? "opacity-50" : ""}`.trim();
59
70
  }
60
71
  const cursorClasses = item.disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer";
@@ -69,8 +80,8 @@
69
80
  const stateClasses = selected
70
81
  ? "bg-action-primary-subtle border-action-primary active:bg-action-primary-subtle-active"
71
82
  : item.disabled
72
- ? "border-border"
73
- : "border-border hover:bg-surface-hover focus-visible:bg-surface-hover active:bg-surface-active";
83
+ ? edge
84
+ : `${edge} hover:bg-surface-hover focus-visible:bg-surface-hover active:bg-surface-active`;
74
85
  return `focus-ring ${baseClasses} ${cursorClasses} ${stateClasses}`.trim();
75
86
  });
76
87
 
@@ -96,7 +107,18 @@
96
107
  </script>
97
108
 
98
109
  {#snippet rowContent()}
99
- {#if item.avatar}
110
+ {#if item.avatarPicture}
111
+ <ListItemLeading class="rounded-full">
112
+ <span
113
+ class="flex size-full items-center justify-center"
114
+ role={item.avatarAlt ? "img" : undefined}
115
+ aria-label={item.avatarAlt || undefined}
116
+ aria-hidden={item.avatarAlt ? undefined : "true"}
117
+ >
118
+ {@render item.avatarPicture()}
119
+ </span>
120
+ </ListItemLeading>
121
+ {:else if item.avatar}
100
122
  <ListItemLeading class="rounded-full">
101
123
  <img
102
124
  src={item.avatar}
@@ -130,7 +152,7 @@
130
152
  {#if showArrow && interactive}
131
153
  <ArrowRight
132
154
  size={16}
133
- class="shrink-0 text-description transition-transform duration-150 group-hover:translate-x-1 group-focus-visible:translate-x-1"
155
+ class="shrink-0 text-description transition-transform duration-(--duration-base) group-hover:translate-x-1 group-focus-visible:translate-x-1"
134
156
  aria-hidden="true"
135
157
  />
136
158
  {/if}
@@ -145,6 +167,7 @@
145
167
  aria-disabled={item.disabled ? "true" : undefined}
146
168
  tabindex={item.disabled ? -1 : undefined}
147
169
  class={itemClasses}
170
+ data-selected={selected ? "true" : undefined}
148
171
  onclick={handleItemClick}
149
172
  {...restProps}
150
173
  >
@@ -154,6 +177,7 @@
154
177
  <button
155
178
  type="button"
156
179
  class={itemClasses}
180
+ data-selected={selected ? "true" : undefined}
157
181
  disabled={item.disabled}
158
182
  onclick={handleItemClick}
159
183
  {...restProps}
@@ -164,7 +188,7 @@
164
188
  <!-- A plain row has no role, so `aria-disabled` on it would say nothing
165
189
  to anyone: a disabled one is only drawn dimmed. There is nothing in it to
166
190
  press, enabled or not. -->
167
- <div class={itemClasses} {...restProps}>
191
+ <div class={itemClasses} data-selected={selected ? "true" : undefined} {...restProps}>
168
192
  {@render rowContent()}
169
193
  </div>
170
194
  {/if}
@@ -12,6 +12,12 @@ export interface ListItemData {
12
12
  avatar?: string;
13
13
  /** `avatar` alt text; `""` if decorative. */
14
14
  avatarAlt?: string;
15
+ /**
16
+ * Leading picture drawn in the round, in place of an address: an
17
+ * inline SVG. Wins over `avatar` and `icon`. Decorative unless
18
+ * `avatarAlt` is set, which then names it.
19
+ */
20
+ avatarPicture?: Snippet;
15
21
  target?: "_self" | "_blank" | "_parent" | "_top";
16
22
  rel?: string;
17
23
  disabled?: boolean;