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
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  import User from "@lucide/svelte/icons/user";
3
- import { untrack } from "svelte";
3
+ import { untrack, type Snippet } from "svelte";
4
4
  import type { HTMLAttributes } from "svelte/elements";
5
5
  import { initialsOf, type AvatarSize } from "../util/avatar.js";
6
6
  import { cn } from "../util/cn.js";
@@ -12,6 +12,10 @@
12
12
  * and the icon inside are presentational. Not interactive: put it in a
13
13
  * link or a button of your own.
14
14
  *
15
+ * A picture that is not an address (an inline drawing that reads the
16
+ * page's tokens) goes in as children and fills the circle in place of the
17
+ * initials and `src`.
18
+ *
15
19
  * Other attributes (`id`, `data-*`, ...) land on the root element.
16
20
  */
17
21
  type Props = Omit<HTMLAttributes<HTMLSpanElement>, "class" | "role" | "aria-label"> & {
@@ -32,6 +36,13 @@
32
36
  * read: pass it where that matters.
33
37
  */
34
38
  locale?: string;
39
+ /**
40
+ * The picture, drawn inside the circle at the avatar's size: an inline
41
+ * SVG, say. It takes the place of `src` and the initials, is
42
+ * presentational (the name is still `name`), and is clipped to the
43
+ * circle.
44
+ */
45
+ children?: Snippet;
35
46
  /** Extra classes for the root element. */
36
47
  class?: string;
37
48
  };
@@ -42,6 +53,7 @@
42
53
  size = "md",
43
54
  alt,
44
55
  locale,
56
+ children,
45
57
  class: className = "",
46
58
  ...restProps
47
59
  }: Props = $props();
@@ -86,12 +98,16 @@ the fill is gone and a transparent border is drawn. -->
86
98
  >
87
99
  <!-- Under the picture from the start, in the server's markup too: nothing
88
100
  moves when the picture arrives, or when it does not. -->
89
- {#if initials}
101
+ {#if children}
102
+ <span class="absolute inset-0 flex items-center justify-center" aria-hidden="true" data-avatar-picture>
103
+ {@render children()}
104
+ </span>
105
+ {:else if initials}
90
106
  <span aria-hidden="true" data-avatar-initials>{initials}</span>
91
107
  {:else}
92
108
  <User class="size-[60%]" aria-hidden="true" />
93
109
  {/if}
94
- {#if src && !failed}
110
+ {#if !children && src && !failed}
95
111
  <img
96
112
  {src}
97
113
  alt=""
@@ -1,3 +1,4 @@
1
+ import { type Snippet } from "svelte";
1
2
  import type { HTMLAttributes } from "svelte/elements";
2
3
  /**
3
4
  * A round picture of a person, which falls back to their initials.
@@ -6,6 +7,10 @@ import type { HTMLAttributes } from "svelte/elements";
6
7
  * and the icon inside are presentational. Not interactive: put it in a
7
8
  * link or a button of your own.
8
9
  *
10
+ * A picture that is not an address (an inline drawing that reads the
11
+ * page's tokens) goes in as children and fills the circle in place of the
12
+ * initials and `src`.
13
+ *
9
14
  * Other attributes (`id`, `data-*`, ...) land on the root element.
10
15
  */
11
16
  type Props = Omit<HTMLAttributes<HTMLSpanElement>, "class" | "role" | "aria-label"> & {
@@ -26,6 +31,13 @@ type Props = Omit<HTMLAttributes<HTMLSpanElement>, "class" | "role" | "aria-labe
26
31
  * read: pass it where that matters.
27
32
  */
28
33
  locale?: string;
34
+ /**
35
+ * The picture, drawn inside the circle at the avatar's size: an inline
36
+ * SVG, say. It takes the place of `src` and the initials, is
37
+ * presentational (the name is still `name`), and is clipped to the
38
+ * circle.
39
+ */
40
+ children?: Snippet;
29
41
  /** Extra classes for the root element. */
30
42
  class?: string;
31
43
  };
@@ -23,6 +23,12 @@
23
23
  variant?: BadgeVariant;
24
24
  size?: SizeVariant;
25
25
  emphasis?: BadgeEmphasis;
26
+ /**
27
+ * Draws the family's edge on a subtle badge. Off by default: the fill
28
+ * and the text already carry it, and a same-hue outline on every badge
29
+ * is one more line per row. Solid badges never have one.
30
+ */
31
+ bordered?: boolean;
26
32
  /** Label text. Ignored when `children` is provided. */
27
33
  text?: string;
28
34
  showIcon?: boolean;
@@ -34,6 +40,7 @@
34
40
  variant = "default",
35
41
  size = "md",
36
42
  emphasis = "subtle",
43
+ bordered = false,
37
44
  text = "",
38
45
  showIcon = false,
39
46
  class: className = "",
@@ -49,7 +56,9 @@
49
56
  *
50
57
  * The vertical padding is what is left of the height after one line of
51
58
  * text and the 1px border on each side (16 + 2 + 2, 16 + 6 + 2 and
52
- * 20 + 6 + 2), so a one-line badge is exactly as tall as it was.
59
+ * 20 + 6 + 2), so a one-line badge is exactly as tall as it was. The
60
+ * border box stays when the border is not drawn (transparent), so
61
+ * `bordered` does not move the badge by a pixel.
53
62
  *
54
63
  * The corner is half of that one-line height, not the pill radius: for
55
64
  * one line that is the same full round end, and a badge of two lines
@@ -67,43 +76,63 @@
67
76
  * every variant reads at the same weight. Subtle is 100/200/800 (8.2:1),
68
77
  * solid is 600 with its `-text` foreground (4.9:1). Before the ramps were
69
78
  * calibrated, a solid warning badge sat at 2.15:1 and energetic at 1.97:1.
79
+ *
80
+ * A subtle badge has a transparent edge unless `bordered` is set, which
81
+ * puts the family's `-border` step back (the 8.1 look). The class is
82
+ * written out for each case so Tailwind can see it.
83
+ *
84
+ * The neutral subtle badge reads `--zabi-badge-neutral-fill` and
85
+ * `--zabi-badge-neutral-text` (src/app.css) in front of the neutral
86
+ * tokens: set on any ancestor or in the theme, they move this badge and
87
+ * no other reader of `--color-neutral-subtle` or `--color-neutral-text`.
70
88
  */
71
89
  const variantClass = $derived.by(() => {
72
90
  const solid = emphasis === "solid";
91
+ const edge = (drawn: string) => (bordered ? drawn : "border-transparent");
73
92
  switch (variant) {
74
93
  case "success":
75
94
  return solid
76
95
  ? "bg-success text-card border-transparent"
77
- : "bg-success-subtle text-success-text border-success-border";
96
+ : `bg-success-subtle text-success-text ${edge("border-success-border")}`;
78
97
  case "warning":
79
98
  return solid
80
99
  ? "bg-warning text-card border-transparent"
81
- : "bg-warning-subtle text-warning-text border-warning-border";
100
+ : `bg-warning-subtle text-warning-text ${edge("border-warning-border")}`;
82
101
  case "error":
83
102
  return solid
84
103
  ? "bg-error text-card border-transparent"
85
- : "bg-error-subtle text-error-text border-error-border";
104
+ : `bg-error-subtle text-error-text ${edge("border-error-border")}`;
86
105
  case "info":
87
106
  return solid
88
107
  ? "bg-info text-card border-transparent"
89
- : "bg-info-subtle text-info-text border-info-border";
108
+ : `bg-info-subtle text-info-text ${edge("border-info-border")}`;
90
109
  case "energetic":
91
110
  return solid
92
111
  ? "bg-energetic text-card border-transparent"
93
- : "bg-energetic-subtle text-energetic-text border-energetic-border";
112
+ : `bg-energetic-subtle text-energetic-text ${edge("border-energetic-border")}`;
94
113
  // The app's second brand colour. Its solid label is the "on accent"
95
114
  // role, not the card surface: an app with a light accent (a
96
115
  // yellow) sets a dark label there, and white would be unreadable.
97
116
  case "accent":
98
117
  return solid
99
118
  ? "bg-accent text-on-accent border-transparent"
100
- : "bg-accent-subtle text-accent-text border-accent-border";
119
+ : `bg-accent-subtle text-accent-text ${edge("border-accent-border")}`;
120
+ // The primary brand colour, for the neutral-but-branded label
121
+ // ("Member"). Subtle text is the link role, guarded on this fill
122
+ // as the label of a selected pill tab; solid carries the label
123
+ // of the primary fill. The colour is read from the token, not
124
+ // through `text-link`: that class darkens on hover and while
125
+ // pressed, and a badge is not a link.
126
+ case "brand":
127
+ return solid
128
+ ? "bg-action-primary text-action-primary border-transparent"
129
+ : `bg-action-primary-subtle text-(color:--color-link) ${edge("border-action-primary-border")}`;
101
130
  case "neutral":
102
131
  case "default":
103
132
  default:
104
133
  return solid
105
134
  ? "bg-neutral text-card border-transparent"
106
- : "bg-neutral-subtle text-neutral-text border-neutral-border";
135
+ : `bg-badge-neutral text-badge-neutral ${edge("border-neutral-border")}`;
107
136
  }
108
137
  });
109
138
 
@@ -11,6 +11,12 @@ type Props = Omit<HTMLAttributes<HTMLSpanElement>, "class"> & {
11
11
  variant?: BadgeVariant;
12
12
  size?: SizeVariant;
13
13
  emphasis?: BadgeEmphasis;
14
+ /**
15
+ * Draws the family's edge on a subtle badge. Off by default: the fill
16
+ * and the text already carry it, and a same-hue outline on every badge
17
+ * is one more line per row. Solid badges never have one.
18
+ */
19
+ bordered?: boolean;
14
20
  /** Label text. Ignored when `children` is provided. */
15
21
  text?: string;
16
22
  showIcon?: boolean;
@@ -0,0 +1,74 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
+ import type { HTMLAttributes } from "svelte/elements";
4
+ import type { SurfaceTone } from "../types/variants.js";
5
+
6
+ import { cn } from "../util/cn.js";
7
+ import { toneSurface } from "../util/tone-surface.js";
8
+
9
+ type Props = Omit<HTMLAttributes<HTMLElement>, "class"> & {
10
+ /**
11
+ * The block's fill: `neutral` (the default, the card surface), `tint`,
12
+ * `brand` or `accent`. On `brand` and `accent` the text, links, controls
13
+ * and focus rings inside take the fill's label colour.
14
+ */
15
+ tone?: SurfaceTone;
16
+ /** A fill the app chooses, any CSS colour. Wins over `tone`. The app must check the contrast of `fill` and `onFill`. */
17
+ fill?: string;
18
+ /** The label colour on `fill`, any CSS colour. */
19
+ onFill?: string;
20
+ /** Inner space: `none`, `sm` (16 / 16), `md` (16 inline, 24 block; the default) or `lg` (24 inline, 32 block). */
21
+ padding?: "none" | "sm" | "md" | "lg";
22
+ /** The element: `div` by default; a landmark (`section` needs a name, `aria-label` or `aria-labelledby`). */
23
+ as?: "div" | "section" | "header" | "footer" | "article" | "aside";
24
+ /**
25
+ * A Block is full width and square-cornered, and does not break out of
26
+ * a padded parent by itself: put it outside the padded container, or
27
+ * give it a negative inline margin here (for example `-mx-4`).
28
+ */
29
+ class?: string;
30
+ children?: Snippet;
31
+ };
32
+
33
+ let {
34
+ tone = "neutral",
35
+ fill,
36
+ onFill,
37
+ padding = "md",
38
+ as = "div",
39
+ class: classProp = "",
40
+ style: styleProp,
41
+ children,
42
+ ...restProps
43
+ }: Props = $props();
44
+
45
+ const surface = $derived(toneSurface({ tone, fill, onFill }));
46
+
47
+ const paddingClass = $derived(
48
+ padding === "none" ? "" : padding === "sm" ? "px-4 py-4" : padding === "lg" ? "px-6 py-8" : "px-4 py-6",
49
+ );
50
+
51
+ const classes = $derived(
52
+ cn("block w-full rounded-none border-none shadow-none", surface.filled ? surface.classes : "bg-card", paddingClass, classProp),
53
+ );
54
+
55
+ const style = $derived(
56
+ [surface.style, typeof styleProp === "string" ? styleProp : undefined].filter(Boolean).join("; ") || undefined,
57
+ );
58
+ </script>
59
+
60
+ <!-- One branch per element, not a dynamic element: hydration takes a dynamic
61
+ element out and puts it back (see Heading). -->
62
+ {#if as === "section"}
63
+ <section class={classes} {style} {...restProps}>{@render children?.()}</section>
64
+ {:else if as === "header"}
65
+ <header class={classes} {style} {...restProps}>{@render children?.()}</header>
66
+ {:else if as === "footer"}
67
+ <footer class={classes} {style} {...restProps}>{@render children?.()}</footer>
68
+ {:else if as === "article"}
69
+ <article class={classes} {style} {...restProps}>{@render children?.()}</article>
70
+ {:else if as === "aside"}
71
+ <aside class={classes} {style} {...restProps}>{@render children?.()}</aside>
72
+ {:else}
73
+ <div class={classes} {style} {...restProps}>{@render children?.()}</div>
74
+ {/if}
@@ -0,0 +1,29 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { HTMLAttributes } from "svelte/elements";
3
+ import type { SurfaceTone } from "../types/variants.js";
4
+ type Props = Omit<HTMLAttributes<HTMLElement>, "class"> & {
5
+ /**
6
+ * The block's fill: `neutral` (the default, the card surface), `tint`,
7
+ * `brand` or `accent`. On `brand` and `accent` the text, links, controls
8
+ * and focus rings inside take the fill's label colour.
9
+ */
10
+ tone?: SurfaceTone;
11
+ /** A fill the app chooses, any CSS colour. Wins over `tone`. The app must check the contrast of `fill` and `onFill`. */
12
+ fill?: string;
13
+ /** The label colour on `fill`, any CSS colour. */
14
+ onFill?: string;
15
+ /** Inner space: `none`, `sm` (16 / 16), `md` (16 inline, 24 block; the default) or `lg` (24 inline, 32 block). */
16
+ padding?: "none" | "sm" | "md" | "lg";
17
+ /** The element: `div` by default; a landmark (`section` needs a name, `aria-label` or `aria-labelledby`). */
18
+ as?: "div" | "section" | "header" | "footer" | "article" | "aside";
19
+ /**
20
+ * A Block is full width and square-cornered, and does not break out of
21
+ * a padded parent by itself: put it outside the padded container, or
22
+ * give it a negative inline margin here (for example `-mx-4`).
23
+ */
24
+ class?: string;
25
+ children?: Snippet;
26
+ };
27
+ declare const Block: import("svelte").Component<Props, {}, "">;
28
+ type Block = ReturnType<typeof Block>;
29
+ export default Block;
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { tick, untrack, type Snippet } from "svelte";
3
3
  import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
4
- import type { ButtonVariant, SizeVariant } from "../types/variants.js";
4
+ import type { ButtonSize, ButtonVariant } from "../types/variants.js";
5
5
  import { cn } from "../util/cn.js";
6
6
 
7
7
  /**
@@ -28,7 +28,8 @@
28
28
  referrerpolicy?: HTMLAnchorAttributes["referrerpolicy"];
29
29
  ping?: HTMLAnchorAttributes["ping"];
30
30
  variant?: ButtonVariant;
31
- size?: SizeVariant;
31
+ /** `xl` is 56px, for a primary action on a phone. It is a Button size only. */
32
+ size?: ButtonSize;
32
33
  /**
33
34
  * Shows a spinner and makes the button unavailable without taking
34
35
  * focus from it: `aria-disabled` and `aria-busy`, still a Tab stop,
@@ -117,7 +118,8 @@
117
118
 
118
119
  /**
119
120
  * One height scale, shared with Input, Select and IconButton, so controls
120
- * of the same size line up in a row: 32 / 40 / 48px. It is a minimum
121
+ * of the same size line up in a row: 32 / 40 / 48px (and 56px, `xl`, which
122
+ * only Button has). It is a minimum
121
123
  * height, not a fixed one, so a label that does not fit on one line wraps
122
124
  * and the button grows; it used to paint outside the button (a long label
123
125
  * at 200% text on a phone). One line of text and the vertical padding are
@@ -136,6 +138,10 @@
136
138
  if (size === "sm") {
137
139
  return { box: "min-h-8 px-3 py-1 pointer-coarse:min-h-11", text: "text-sm", gap: "gap-2", spinner: "size-3.5" };
138
140
  }
141
+ // xl is 56px, like the FloatingActionButton; only Button has it.
142
+ if (size === "xl") {
143
+ return { box: "min-h-14 px-6 py-3", text: "text-base", gap: "gap-2", spinner: "size-5" };
144
+ }
139
145
  if (size === "lg") {
140
146
  return { box: "min-h-12 px-5 py-2", text: "text-base", gap: "gap-2", spinner: "size-4" };
141
147
  }
@@ -160,28 +166,56 @@
160
166
  */
161
167
  const BUSY_BUTTON = "bg-(--color-action-disabled) text-action-disabled-text cursor-not-allowed";
162
168
 
169
+ /**
170
+ * Forced colours (a Windows contrast theme) remove every fill, so a button
171
+ * told apart by its fill alone is told apart by nothing. The variants that
172
+ * are drawn by a fill get a 1px edge in a system colour there only;
173
+ * `ghost`, `link` and `text` stay bare, and `outline` has its own border.
174
+ * A disabled or loading button is the flat disabled fill, so it gets an
175
+ * edge in the disabled text colour. The `disabled:` copy beats
176
+ * `disabled:border-transparent`, which is the more specific rule.
177
+ */
178
+ const FORCED_EDGE = "forced-colors:border forced-colors:border-[color:ButtonBorder]";
179
+ const FORCED_EDGE_DISABLED_ONLY = "disabled:forced-colors:border disabled:forced-colors:border-[color:GrayText]";
180
+ const FORCED_EDGE_DISABLED =
181
+ "forced-colors:border forced-colors:border-[color:GrayText] disabled:forced-colors:border-[color:GrayText]";
182
+
163
183
  const disabledClass =
164
- "disabled:bg-action-disabled disabled:text-action-disabled-text disabled:border-transparent disabled:no-underline disabled:shadow-none disabled:cursor-not-allowed disabled:active:scale-100";
184
+ "disabled:bg-action-disabled disabled:text-action-disabled-text disabled:border-transparent disabled:no-underline disabled:bg-none disabled:inset-shadow-none disabled:shadow-none disabled:cursor-not-allowed disabled:active:scale-100";
165
185
 
166
186
  const variantClass = $derived.by(() => {
187
+ // The veil of the solid fills (a top highlight and a darker bottom). A
188
+ // disabled button is the flat neutral pair, so it is left off rather
189
+ // than cleared; `disabledClass` also clears it for a button that is
190
+ // disabled from outside (a `fieldset`).
191
+ const veil = disabled ? "" : "bg-control-gradient ";
192
+ const veilAccent = disabled ? "" : "bg-control-gradient-accent ";
167
193
  switch (variant) {
168
194
  case "secondary":
169
195
  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";
196
+ case "tonal":
197
+ return "bg-action-tonal text-action-tonal active:scale-[0.98] motion-reduce:active:scale-100";
170
198
  case "danger":
171
- 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";
199
+ 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`;
172
200
  case "ghost":
173
201
  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";
174
202
  case "outline":
175
- 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";
203
+ 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";
176
204
  case "link":
177
205
  // A link variant has to look like a link at rest, not only on
178
206
  // hover — otherwise it is indistinguishable from `ghost`.
179
207
  return "bg-transparent text-link hover:text-link-hover underline underline-offset-4 decoration-1 hover:decoration-2 px-0 focus-ring--muted";
208
+ case "text":
209
+ // A standalone action, not a link in a sentence: no underline,
210
+ // a stronger label, and the box of the other variants. The
211
+ // link role gives the primary colour, and the block's own
212
+ // on-colour inside `.on-brand` / `.on-accent`.
213
+ return "bg-transparent text-link px-2 hover:bg-surface-hover active:bg-surface-active active:scale-[0.98] motion-reduce:active:scale-100 focus-ring--muted";
180
214
  case "accent":
181
- return "bg-accent text-on-accent hover:bg-accent-hover active:bg-accent-active active:scale-[0.98] motion-reduce:active:scale-100";
215
+ return `${veilAccent}bg-accent text-on-accent hover:bg-accent-hover active:bg-accent-active active:scale-[0.98] motion-reduce:active:scale-100`;
182
216
  case "primary":
183
217
  default:
184
- 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";
218
+ 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`;
185
219
  }
186
220
  });
187
221
 
@@ -195,21 +229,24 @@
195
229
  ? "text-action-disabled-text no-underline cursor-not-allowed"
196
230
  : "text-link hover:text-link-hover underline underline-offset-4 decoration-1 hover:decoration-2 cursor-pointer";
197
231
  return cn(
198
- `focus-ring focus-ring--muted inline rounded-control font-medium transition-colors duration-150 ${s.text} ${state} ${fullWidth || isFullWidth ? "block w-full" : ""} ${className}`,
232
+ `focus-ring focus-ring--muted inline rounded-button font-button transition-colors duration-(--duration-base) ${s.text} ${state} ${fullWidth || isFullWidth ? "block w-full" : ""} ${className}`,
199
233
  );
200
234
  }
201
235
  // The label wraps when it has to, balanced over its lines; the icon
202
- // and the spinner stay centred beside it.
203
- const base = `${layout} focus-ring items-center justify-center rounded-control text-center font-medium text-balance transition-colors duration-150 cursor-pointer select-none`;
236
+ // and the spinner stay centred beside it. The button is never wider
237
+ // than what it is in: a word that alone does not fit is hyphenated by
238
+ // the page's language (Z-129).
239
+ const base = `${layout} [max-inline-size:100%] hyphens-auto focus-ring items-center justify-center rounded-button text-center ${variant === "text" ? "font-button-strong" : "font-button"} text-balance transition-colors duration-(--duration-base) cursor-pointer select-none`;
204
240
  // `disabled:` does not match an `<a>`: a disabled link wears the
205
241
  // disabled pair itself, in place of its variant.
206
- const busyBox = variant === "outline" ? "border border-transparent" : variant === "link" ? "px-0" : "";
242
+ const busyBox = variant === "outline" ? "border border-transparent" : variant === "link" ? "px-0" : variant === "text" ? "px-2" : "";
243
+ const drawnByFill = variant !== "ghost" && variant !== "link" && variant !== "text" && variant !== "outline";
207
244
  const state =
208
245
  isLink && isDisabled
209
- ? DISABLED_LINK
246
+ ? `${DISABLED_LINK} ${FORCED_EDGE_DISABLED}`
210
247
  : isBusy
211
- ? `${BUSY_BUTTON} ${busyBox}`
212
- : `${variantClass} ${disabledClass}`;
248
+ ? `${BUSY_BUTTON} ${busyBox} ${FORCED_EDGE_DISABLED}`
249
+ : `${variantClass} ${disabledClass} ${drawnByFill ? FORCED_EDGE : ""} ${FORCED_EDGE_DISABLED_ONLY}`;
213
250
  return cn(`${base} ${s.box} ${s.text} ${s.gap} ${state} ${className}`);
214
251
  });
215
252
  </script>
@@ -1,6 +1,6 @@
1
1
  import { type Snippet } from "svelte";
2
2
  import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
3
- import type { ButtonVariant, SizeVariant } from "../types/variants.js";
3
+ import type { ButtonSize, ButtonVariant } from "../types/variants.js";
4
4
  /**
5
5
  * One type for the button and the link, not a union of the two elements'
6
6
  * attributes: that union is too large for TypeScript to carry through a
@@ -25,7 +25,8 @@ type Props = Omit<HTMLButtonAttributes, "class"> & {
25
25
  referrerpolicy?: HTMLAnchorAttributes["referrerpolicy"];
26
26
  ping?: HTMLAnchorAttributes["ping"];
27
27
  variant?: ButtonVariant;
28
- size?: SizeVariant;
28
+ /** `xl` is 56px, for a primary action on a phone. It is a Button size only. */
29
+ size?: ButtonSize;
29
30
  /**
30
31
  * Shows a spinner and makes the button unavailable without taking
31
32
  * focus from it: `aria-disabled` and `aria-busy`, still a Tab stop,