zabi-components 8.0.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 (456) hide show
  1. package/CHANGELOG.md +2935 -0
  2. package/README.md +243 -10
  3. package/THEME.md +162 -332
  4. package/THEMING.md +1419 -259
  5. package/dist/atoms/ActionPanel.svelte +15 -5
  6. package/dist/atoms/Avatar.svelte +120 -0
  7. package/dist/atoms/Avatar.svelte.d.ts +46 -0
  8. package/dist/atoms/Badge.svelte +71 -13
  9. package/dist/atoms/Badge.svelte.d.ts +8 -2
  10. package/dist/atoms/Block.svelte +74 -0
  11. package/dist/atoms/Block.svelte.d.ts +29 -0
  12. package/dist/atoms/Button.svelte +217 -30
  13. package/dist/atoms/Button.svelte.d.ts +33 -4
  14. package/dist/atoms/Card.svelte +182 -26
  15. package/dist/atoms/Card.svelte.d.ts +55 -3
  16. package/dist/atoms/CardHeader.svelte +16 -4
  17. package/dist/atoms/Checkbox.svelte +81 -21
  18. package/dist/atoms/Checkbox.svelte.d.ts +31 -2
  19. package/dist/atoms/Chip.svelte +351 -0
  20. package/dist/atoms/Chip.svelte.d.ts +57 -0
  21. package/dist/atoms/CodeBlock.svelte +20 -2
  22. package/dist/atoms/CodeBlock.svelte.d.ts +4 -0
  23. package/dist/atoms/CodeField.svelte +385 -0
  24. package/dist/atoms/CodeField.svelte.d.ts +61 -0
  25. package/dist/atoms/ColorPicker.svelte +260 -49
  26. package/dist/atoms/ColorPicker.svelte.d.ts +3 -0
  27. package/dist/atoms/Container.svelte +16 -8
  28. package/dist/atoms/DateField.svelte +145 -0
  29. package/dist/atoms/DateField.svelte.d.ts +81 -0
  30. package/dist/atoms/FieldMessages.svelte +69 -0
  31. package/dist/atoms/FieldMessages.svelte.d.ts +23 -0
  32. package/dist/atoms/FloatingActionButton.svelte +176 -0
  33. package/dist/atoms/FloatingActionButton.svelte.d.ts +51 -0
  34. package/dist/atoms/Heading.svelte +63 -5
  35. package/dist/atoms/Heading.svelte.d.ts +14 -0
  36. package/dist/atoms/IconButton.svelte +294 -26
  37. package/dist/atoms/IconButton.svelte.d.ts +62 -4
  38. package/dist/atoms/Input.svelte +259 -53
  39. package/dist/atoms/Input.svelte.d.ts +33 -2
  40. package/dist/atoms/List.svelte +5 -1
  41. package/dist/atoms/ListItem.svelte +70 -13
  42. package/dist/atoms/ListItem.svelte.d.ts +11 -0
  43. package/dist/atoms/PickerField.svelte +239 -0
  44. package/dist/atoms/PickerField.svelte.d.ts +67 -0
  45. package/dist/atoms/Progress.svelte +99 -19
  46. package/dist/atoms/Progress.svelte.d.ts +15 -1
  47. package/dist/atoms/Radio.svelte +51 -3
  48. package/dist/atoms/Radio.svelte.d.ts +30 -2
  49. package/dist/atoms/Rating.svelte +620 -0
  50. package/dist/atoms/Rating.svelte.d.ts +77 -0
  51. package/dist/atoms/Select.svelte +438 -98
  52. package/dist/atoms/Select.svelte.d.ts +61 -3
  53. package/dist/atoms/SelectionControl.svelte +193 -23
  54. package/dist/atoms/SelectionControl.svelte.d.ts +29 -3
  55. package/dist/atoms/Skeleton.svelte +114 -18
  56. package/dist/atoms/Skeleton.svelte.d.ts +30 -3
  57. package/dist/atoms/SkeletonGroup.svelte +43 -0
  58. package/dist/atoms/SkeletonGroup.svelte.d.ts +20 -0
  59. package/dist/atoms/Slider.svelte +344 -0
  60. package/dist/atoms/Slider.svelte.d.ts +40 -0
  61. package/dist/atoms/Sparkline.svelte +109 -0
  62. package/dist/atoms/Sparkline.svelte.d.ts +27 -0
  63. package/dist/atoms/SpeechBubble.svelte +187 -0
  64. package/dist/atoms/SpeechBubble.svelte.d.ts +33 -0
  65. package/dist/atoms/Spinner.svelte +57 -0
  66. package/dist/atoms/Spinner.svelte.d.ts +19 -0
  67. package/dist/atoms/Stat.svelte +72 -0
  68. package/dist/atoms/Stat.svelte.d.ts +19 -0
  69. package/dist/atoms/Table.svelte +151 -6
  70. package/dist/atoms/Table.svelte.d.ts +15 -3
  71. package/dist/atoms/TemporalField.svelte +472 -0
  72. package/dist/atoms/TemporalField.svelte.d.ts +48 -0
  73. package/dist/atoms/Text.svelte +23 -11
  74. package/dist/atoms/Text.svelte.d.ts +6 -3
  75. package/dist/atoms/Textarea.svelte +150 -44
  76. package/dist/atoms/Textarea.svelte.d.ts +35 -2
  77. package/dist/atoms/ThemeToggle.svelte +223 -44
  78. package/dist/atoms/ThemeToggle.svelte.d.ts +26 -3
  79. package/dist/atoms/TimeColumn.svelte +150 -0
  80. package/dist/atoms/TimeColumn.svelte.d.ts +25 -0
  81. package/dist/atoms/TimeField.svelte +151 -0
  82. package/dist/atoms/TimeField.svelte.d.ts +85 -0
  83. package/dist/atoms/Toast.svelte +22 -8
  84. package/dist/atoms/Toast.svelte.d.ts +2 -0
  85. package/dist/atoms/Toggle.svelte +98 -14
  86. package/dist/atoms/Toggle.svelte.d.ts +26 -2
  87. package/dist/atoms/Tooltip.svelte +528 -24
  88. package/dist/atoms/Tooltip.svelte.d.ts +18 -0
  89. package/dist/atoms/ZabiStringsProvider.svelte +45 -0
  90. package/dist/atoms/ZabiStringsProvider.svelte.d.ts +27 -0
  91. package/dist/atoms/index.d.ts +38 -0
  92. package/dist/atoms/index.js +29 -0
  93. package/dist/atoms/selection-control.styles.d.ts +47 -5
  94. package/dist/atoms/selection-control.styles.js +52 -10
  95. package/dist/components/atoms/index.d.ts +38 -0
  96. package/dist/components/atoms/index.d.ts.map +1 -1
  97. package/dist/components/atoms/selection-control.styles.d.ts +47 -5
  98. package/dist/components/atoms/selection-control.styles.d.ts.map +1 -1
  99. package/dist/components/index.d.ts +104 -2
  100. package/dist/components/index.d.ts.map +1 -1
  101. package/dist/components/map.d.ts +11 -0
  102. package/dist/components/map.d.ts.map +1 -0
  103. package/dist/components/molecules/index.d.ts +51 -0
  104. package/dist/components/molecules/index.d.ts.map +1 -1
  105. package/dist/components/molecules/toast-store.d.ts +83 -7
  106. package/dist/components/molecules/toast-store.d.ts.map +1 -1
  107. package/dist/components/organisms/index.d.ts +10 -0
  108. package/dist/components/organisms/index.d.ts.map +1 -1
  109. package/dist/components/types/variants.d.ts +24 -1
  110. package/dist/components/types/variants.d.ts.map +1 -1
  111. package/dist/components/util/app-shell.d.ts +79 -0
  112. package/dist/components/util/app-shell.d.ts.map +1 -0
  113. package/dist/components/util/avatar.d.ts +39 -0
  114. package/dist/components/util/avatar.d.ts.map +1 -0
  115. package/dist/components/util/bottom-sheet.d.ts +30 -0
  116. package/dist/components/util/bottom-sheet.d.ts.map +1 -0
  117. package/dist/components/util/bottom-tab-bar.d.ts +41 -0
  118. package/dist/components/util/bottom-tab-bar.d.ts.map +1 -0
  119. package/dist/components/util/calendar.d.ts +56 -0
  120. package/dist/components/util/calendar.d.ts.map +1 -0
  121. package/dist/components/util/chart.d.ts +140 -0
  122. package/dist/components/util/chart.d.ts.map +1 -0
  123. package/dist/components/util/chip.d.ts +66 -0
  124. package/dist/components/util/chip.d.ts.map +1 -0
  125. package/dist/components/util/cn.d.ts.map +1 -1
  126. package/dist/components/util/code-field.d.ts +63 -0
  127. package/dist/components/util/code-field.d.ts.map +1 -0
  128. package/dist/components/util/collapsible.d.ts +64 -0
  129. package/dist/components/util/collapsible.d.ts.map +1 -0
  130. package/dist/components/util/confirm-dialog.d.ts +12 -0
  131. package/dist/components/util/confirm-dialog.d.ts.map +1 -0
  132. package/dist/components/util/date.d.ts +96 -0
  133. package/dist/components/util/date.d.ts.map +1 -0
  134. package/dist/components/util/display-voice.d.ts +10 -0
  135. package/dist/components/util/display-voice.d.ts.map +1 -0
  136. package/dist/components/util/drawer.d.ts +13 -0
  137. package/dist/components/util/drawer.d.ts.map +1 -0
  138. package/dist/components/util/dropdown.d.ts +47 -0
  139. package/dist/components/util/dropdown.d.ts.map +1 -0
  140. package/dist/components/util/field-control.d.ts +30 -0
  141. package/dist/components/util/field-control.d.ts.map +1 -0
  142. package/dist/components/util/field.d.ts +45 -0
  143. package/dist/components/util/field.d.ts.map +1 -0
  144. package/dist/components/util/fit-in-viewport.d.ts +201 -0
  145. package/dist/components/util/fit-in-viewport.d.ts.map +1 -0
  146. package/dist/components/util/focus-utils.d.ts +77 -2
  147. package/dist/components/util/focus-utils.d.ts.map +1 -1
  148. package/dist/components/util/hydration.d.ts +45 -0
  149. package/dist/components/util/hydration.d.ts.map +1 -0
  150. package/dist/components/util/keyboard-inset.d.ts +18 -0
  151. package/dist/components/util/keyboard-inset.d.ts.map +1 -0
  152. package/dist/components/util/map-view.d.ts +144 -0
  153. package/dist/components/util/map-view.d.ts.map +1 -0
  154. package/dist/components/util/media-grid.d.ts +51 -0
  155. package/dist/components/util/media-grid.d.ts.map +1 -0
  156. package/dist/components/util/media.d.ts +6 -0
  157. package/dist/components/util/media.d.ts.map +1 -0
  158. package/dist/components/util/overlay-chrome.d.ts +53 -0
  159. package/dist/components/util/overlay-chrome.d.ts.map +1 -0
  160. package/dist/components/util/overlay.d.ts +72 -0
  161. package/dist/components/util/overlay.d.ts.map +1 -0
  162. package/dist/components/util/photo.d.ts +234 -0
  163. package/dist/components/util/photo.d.ts.map +1 -0
  164. package/dist/components/util/portal.d.ts +23 -0
  165. package/dist/components/util/portal.d.ts.map +1 -0
  166. package/dist/components/util/progress.d.ts +19 -0
  167. package/dist/components/util/progress.d.ts.map +1 -0
  168. package/dist/components/util/pull-to-refresh.d.ts +27 -0
  169. package/dist/components/util/pull-to-refresh.d.ts.map +1 -0
  170. package/dist/components/util/radio-keys.d.ts +25 -0
  171. package/dist/components/util/radio-keys.d.ts.map +1 -0
  172. package/dist/components/util/rating.d.ts +23 -0
  173. package/dist/components/util/rating.d.ts.map +1 -0
  174. package/dist/components/util/ready-made-strings.d.ts +77 -0
  175. package/dist/components/util/ready-made-strings.d.ts.map +1 -0
  176. package/dist/components/util/scroll-edge.d.ts +33 -0
  177. package/dist/components/util/scroll-edge.d.ts.map +1 -0
  178. package/dist/components/util/scroll-padding.d.ts +20 -0
  179. package/dist/components/util/scroll-padding.d.ts.map +1 -0
  180. package/dist/components/util/segmented-control.d.ts +16 -0
  181. package/dist/components/util/segmented-control.d.ts.map +1 -0
  182. package/dist/components/util/select.d.ts +41 -0
  183. package/dist/components/util/select.d.ts.map +1 -0
  184. package/dist/components/util/sheet-drag.d.ts +84 -0
  185. package/dist/components/util/sheet-drag.d.ts.map +1 -0
  186. package/dist/components/util/sidebar.d.ts +63 -0
  187. package/dist/components/util/sidebar.d.ts.map +1 -0
  188. package/dist/components/util/skeleton.d.ts +15 -0
  189. package/dist/components/util/skeleton.d.ts.map +1 -0
  190. package/dist/components/util/sliding-indicator.d.ts +25 -0
  191. package/dist/components/util/sliding-indicator.d.ts.map +1 -0
  192. package/dist/components/util/sortable-list.d.ts +65 -0
  193. package/dist/components/util/sortable-list.d.ts.map +1 -0
  194. package/dist/components/util/stepper.d.ts +40 -0
  195. package/dist/components/util/stepper.d.ts.map +1 -0
  196. package/dist/components/util/swipeable-list-item.d.ts +36 -0
  197. package/dist/components/util/swipeable-list-item.d.ts.map +1 -0
  198. package/dist/components/util/temporal-field.d.ts +89 -0
  199. package/dist/components/util/temporal-field.d.ts.map +1 -0
  200. package/dist/components/util/theme-mode.d.ts +80 -0
  201. package/dist/components/util/theme-mode.d.ts.map +1 -0
  202. package/dist/components/util/toaster.d.ts +133 -0
  203. package/dist/components/util/toaster.d.ts.map +1 -0
  204. package/dist/components/util/tone-surface.d.ts +34 -0
  205. package/dist/components/util/tone-surface.d.ts.map +1 -0
  206. package/dist/components/util/tooltip.d.ts +10 -0
  207. package/dist/components/util/tooltip.d.ts.map +1 -0
  208. package/dist/components/util/top-navbar.d.ts +35 -0
  209. package/dist/components/util/top-navbar.d.ts.map +1 -0
  210. package/dist/components/util/touch-target.d.ts +31 -0
  211. package/dist/components/util/touch-target.d.ts.map +1 -0
  212. package/dist/components/util/zabi-strings.d.ts +142 -0
  213. package/dist/components/util/zabi-strings.d.ts.map +1 -0
  214. package/dist/create-theme/cli.js +173 -0
  215. package/dist/create-theme/index.d.ts +190 -0
  216. package/dist/create-theme/index.js +1034 -0
  217. package/dist/create-theme/lib/ramp-math.js +128 -0
  218. package/dist/create-theme/lib/resolve.js +238 -0
  219. package/dist/create-theme/theme-data.js +2644 -0
  220. package/dist/index.d.ts +104 -2
  221. package/dist/index.js +75 -2
  222. package/dist/map.d.ts +10 -0
  223. package/dist/map.js +9 -0
  224. package/dist/molecules/Alert.svelte +63 -16
  225. package/dist/molecules/Alert.svelte.d.ts +14 -1
  226. package/dist/molecules/AppBar.svelte +674 -0
  227. package/dist/molecules/AppBar.svelte.d.ts +131 -0
  228. package/dist/molecules/AvatarGroup.svelte +116 -0
  229. package/dist/molecules/AvatarGroup.svelte.d.ts +40 -0
  230. package/dist/molecules/BarChart.svelte +381 -0
  231. package/dist/molecules/BarChart.svelte.d.ts +45 -0
  232. package/dist/molecules/BottomSheet.svelte +608 -0
  233. package/dist/molecules/BottomSheet.svelte.d.ts +109 -0
  234. package/dist/molecules/BottomTabBar.svelte +403 -0
  235. package/dist/molecules/BottomTabBar.svelte.d.ts +98 -0
  236. package/dist/molecules/Calendar.svelte +597 -0
  237. package/dist/molecules/Calendar.svelte.d.ts +72 -0
  238. package/dist/molecules/ChipGroup.svelte +325 -0
  239. package/dist/molecules/ChipGroup.svelte.d.ts +41 -0
  240. package/dist/molecules/Collapsible.svelte +347 -0
  241. package/dist/molecules/Collapsible.svelte.d.ts +61 -0
  242. package/dist/molecules/CollapsibleGroup.svelte +136 -0
  243. package/dist/molecules/CollapsibleGroup.svelte.d.ts +14 -0
  244. package/dist/molecules/ComponentDemo.svelte +16 -4
  245. package/dist/molecules/ComponentDemo.svelte.d.ts +3 -0
  246. package/dist/molecules/ConfirmDialog.svelte +257 -0
  247. package/dist/molecules/ConfirmDialog.svelte.d.ts +58 -0
  248. package/dist/molecules/ContactForm.svelte +34 -17
  249. package/dist/molecules/ContactForm.svelte.d.ts +8 -0
  250. package/dist/molecules/Drawer.svelte +415 -0
  251. package/dist/molecules/Drawer.svelte.d.ts +55 -0
  252. package/dist/molecules/Dropdown.svelte +517 -48
  253. package/dist/molecules/Dropdown.svelte.d.ts +47 -6
  254. package/dist/molecules/DropdownItem.svelte +122 -0
  255. package/dist/molecules/DropdownItem.svelte.d.ts +28 -0
  256. package/dist/molecules/EmptyState.svelte +55 -10
  257. package/dist/molecules/EmptyState.svelte.d.ts +8 -0
  258. package/dist/molecules/FormField.svelte +9 -1
  259. package/dist/molecules/FormField.svelte.d.ts +2 -0
  260. package/dist/molecules/Header.svelte +1 -1
  261. package/dist/molecules/ImageUpload.svelte +492 -69
  262. package/dist/molecules/ImageUpload.svelte.d.ts +82 -2
  263. package/dist/molecules/LineChart.svelte +459 -0
  264. package/dist/molecules/LineChart.svelte.d.ts +53 -0
  265. package/dist/molecules/MapView.svelte +565 -0
  266. package/dist/molecules/MapView.svelte.d.ts +88 -0
  267. package/dist/molecules/MediaGrid.svelte +527 -0
  268. package/dist/molecules/MediaGrid.svelte.d.ts +77 -0
  269. package/dist/molecules/Modal.svelte +293 -72
  270. package/dist/molecules/Modal.svelte.d.ts +51 -2
  271. package/dist/molecules/NavigationMenuContent.svelte +58 -1
  272. package/dist/molecules/NavigationMenuItem.svelte +0 -1
  273. package/dist/molecules/NavigationMenuLink.svelte +1 -1
  274. package/dist/molecules/NavigationMenuList.svelte +10 -3
  275. package/dist/molecules/NavigationMenuTrigger.svelte +2 -2
  276. package/dist/molecules/Page.svelte +32 -2
  277. package/dist/molecules/Page.svelte.d.ts +11 -0
  278. package/dist/molecules/PhotoGrid.svelte +523 -0
  279. package/dist/molecules/PhotoGrid.svelte.d.ts +73 -0
  280. package/dist/molecules/PhotoViewer.svelte +1213 -0
  281. package/dist/molecules/PhotoViewer.svelte.d.ts +74 -0
  282. package/dist/molecules/PieChart.svelte +421 -0
  283. package/dist/molecules/PieChart.svelte.d.ts +51 -0
  284. package/dist/molecules/PropsTable.svelte +22 -10
  285. package/dist/molecules/PropsTable.svelte.d.ts +3 -0
  286. package/dist/molecules/PullToRefresh.svelte +263 -0
  287. package/dist/molecules/PullToRefresh.svelte.d.ts +52 -0
  288. package/dist/molecules/RadioGroup.svelte +72 -18
  289. package/dist/molecules/RadioGroup.svelte.d.ts +19 -3
  290. package/dist/molecules/Section.svelte +1 -1
  291. package/dist/molecules/SegmentedControl.svelte +456 -0
  292. package/dist/molecules/SegmentedControl.svelte.d.ts +40 -0
  293. package/dist/molecules/SidebarBrandHeader.svelte +11 -1
  294. package/dist/molecules/SidebarBrandHeader.svelte.d.ts +3 -0
  295. package/dist/molecules/SidebarFooter.svelte +30 -7
  296. package/dist/molecules/SidebarFooter.svelte.d.ts +3 -0
  297. package/dist/molecules/SidebarNavSection.svelte +1 -1
  298. package/dist/molecules/SlideUp.svelte +220 -61
  299. package/dist/molecules/SlideUp.svelte.d.ts +26 -0
  300. package/dist/molecules/SortableList.svelte +633 -0
  301. package/dist/molecules/SortableList.svelte.d.ts +69 -0
  302. package/dist/molecules/Stepper.svelte +562 -0
  303. package/dist/molecules/Stepper.svelte.d.ts +39 -0
  304. package/dist/molecules/StickyActionBar.svelte +250 -0
  305. package/dist/molecules/StickyActionBar.svelte.d.ts +78 -0
  306. package/dist/molecules/SwipeableListItem.svelte +334 -0
  307. package/dist/molecules/SwipeableListItem.svelte.d.ts +54 -0
  308. package/dist/molecules/Tabs.svelte +310 -9
  309. package/dist/molecules/Tabs.svelte.d.ts +7 -0
  310. package/dist/molecules/Toaster.svelte +514 -5
  311. package/dist/molecules/Toaster.svelte.d.ts +153 -2
  312. package/dist/molecules/ToasterToast.svelte +495 -77
  313. package/dist/molecules/ToasterToast.svelte.d.ts +15 -0
  314. package/dist/molecules/UnsavedChangesBar.svelte +223 -0
  315. package/dist/molecules/UnsavedChangesBar.svelte.d.ts +43 -0
  316. package/dist/molecules/index.d.ts +51 -0
  317. package/dist/molecules/index.js +31 -0
  318. package/dist/molecules/toast-store.d.ts +83 -7
  319. package/dist/molecules/toast-store.js +25 -1
  320. package/dist/organisms/AppNavigation.svelte +440 -0
  321. package/dist/organisms/AppNavigation.svelte.d.ts +72 -0
  322. package/dist/organisms/AppShell.svelte +696 -0
  323. package/dist/organisms/AppShell.svelte.d.ts +157 -0
  324. package/dist/organisms/AppShellHeaderPart.svelte +163 -0
  325. package/dist/organisms/AppShellHeaderPart.svelte.d.ts +57 -0
  326. package/dist/organisms/SidebarAccountPanel.svelte +107 -12
  327. package/dist/organisms/SidebarAccountPanel.svelte.d.ts +19 -0
  328. package/dist/organisms/SidebarNavigation.svelte +111 -43
  329. package/dist/organisms/SidebarNavigation.svelte.d.ts +29 -1
  330. package/dist/organisms/SidebarPanel.svelte +34 -17
  331. package/dist/organisms/SidebarPanel.svelte.d.ts +1 -0
  332. package/dist/organisms/SidebarShell.svelte +164 -14
  333. package/dist/organisms/SidebarShell.svelte.d.ts +56 -2
  334. package/dist/organisms/TopNavbar.svelte +353 -39
  335. package/dist/organisms/TopNavbar.svelte.d.ts +47 -0
  336. package/dist/organisms/TopNavbarSheetContent.svelte +145 -0
  337. package/dist/organisms/TopNavbarSheetContent.svelte.d.ts +24 -0
  338. package/dist/organisms/index.d.ts +10 -0
  339. package/dist/organisms/index.js +7 -0
  340. package/dist/types/components.d.ts +21 -1
  341. package/dist/types/components.d.ts.map +1 -1
  342. package/dist/types/index.d.ts +1093 -26
  343. package/dist/types/index.d.ts.map +1 -1
  344. package/dist/types/variants.ts +28 -1
  345. package/dist/util/app-shell.d.ts +78 -0
  346. package/dist/util/app-shell.js +97 -0
  347. package/dist/util/avatar.d.ts +38 -0
  348. package/dist/util/avatar.js +52 -0
  349. package/dist/util/bottom-sheet.d.ts +29 -0
  350. package/dist/util/bottom-sheet.js +43 -0
  351. package/dist/util/bottom-tab-bar.d.ts +40 -0
  352. package/dist/util/bottom-tab-bar.js +54 -0
  353. package/dist/util/calendar.d.ts +55 -0
  354. package/dist/util/calendar.js +74 -0
  355. package/dist/util/chart.d.ts +139 -0
  356. package/dist/util/chart.js +305 -0
  357. package/dist/util/chip.d.ts +65 -0
  358. package/dist/util/chip.js +58 -0
  359. package/dist/util/cn.js +28 -2
  360. package/dist/util/code-field.d.ts +62 -0
  361. package/dist/util/code-field.js +112 -0
  362. package/dist/util/collapsible.d.ts +63 -0
  363. package/dist/util/collapsible.js +23 -0
  364. package/dist/util/confirm-dialog.d.ts +11 -0
  365. package/dist/util/confirm-dialog.js +2 -0
  366. package/dist/util/date.d.ts +95 -0
  367. package/dist/util/date.js +237 -0
  368. package/dist/util/display-voice.d.ts +9 -0
  369. package/dist/util/display-voice.js +9 -0
  370. package/dist/util/drawer.d.ts +12 -0
  371. package/dist/util/drawer.js +7 -0
  372. package/dist/util/dropdown.d.ts +46 -0
  373. package/dist/util/dropdown.js +21 -0
  374. package/dist/util/field-control.d.ts +29 -0
  375. package/dist/util/field-control.js +32 -0
  376. package/dist/util/field.d.ts +44 -0
  377. package/dist/util/field.js +38 -0
  378. package/dist/util/fit-in-viewport.d.ts +200 -0
  379. package/dist/util/fit-in-viewport.js +399 -0
  380. package/dist/util/focus-utils.d.ts +77 -2
  381. package/dist/util/focus-utils.js +186 -9
  382. package/dist/util/hydration.d.ts +44 -0
  383. package/dist/util/hydration.js +58 -0
  384. package/dist/util/keyboard-inset.d.ts +17 -0
  385. package/dist/util/keyboard-inset.js +53 -0
  386. package/dist/util/map-view.d.ts +143 -0
  387. package/dist/util/map-view.js +121 -0
  388. package/dist/util/media-grid.d.ts +50 -0
  389. package/dist/util/media-grid.js +81 -0
  390. package/dist/util/media.d.ts +5 -0
  391. package/dist/util/media.js +10 -0
  392. package/dist/util/overlay-chrome.d.ts +52 -0
  393. package/dist/util/overlay-chrome.js +56 -0
  394. package/dist/util/overlay.d.ts +71 -0
  395. package/dist/util/overlay.js +240 -0
  396. package/dist/util/photo.d.ts +233 -0
  397. package/dist/util/photo.js +209 -0
  398. package/dist/util/portal.d.ts +22 -0
  399. package/dist/util/portal.js +50 -0
  400. package/dist/util/progress.d.ts +18 -0
  401. package/dist/util/progress.js +17 -0
  402. package/dist/util/pull-to-refresh.d.ts +26 -0
  403. package/dist/util/pull-to-refresh.js +30 -0
  404. package/dist/util/radio-keys.d.ts +24 -0
  405. package/dist/util/radio-keys.js +49 -0
  406. package/dist/util/rating.d.ts +22 -0
  407. package/dist/util/rating.js +26 -0
  408. package/dist/util/ready-made-strings.d.ts +76 -0
  409. package/dist/util/ready-made-strings.js +67 -0
  410. package/dist/util/scroll-edge.d.ts +32 -0
  411. package/dist/util/scroll-edge.js +87 -0
  412. package/dist/util/scroll-padding.d.ts +19 -0
  413. package/dist/util/scroll-padding.js +44 -0
  414. package/dist/util/segmented-control.d.ts +15 -0
  415. package/dist/util/segmented-control.js +1 -0
  416. package/dist/util/select.d.ts +40 -0
  417. package/dist/util/select.js +63 -0
  418. package/dist/util/sheet-drag.d.ts +83 -0
  419. package/dist/util/sheet-drag.js +311 -0
  420. package/dist/util/sidebar.d.ts +62 -0
  421. package/dist/util/sidebar.js +38 -0
  422. package/dist/util/skeleton.d.ts +14 -0
  423. package/dist/util/skeleton.js +22 -0
  424. package/dist/util/sliding-indicator.d.ts +24 -0
  425. package/dist/util/sliding-indicator.js +44 -0
  426. package/dist/util/sortable-list.d.ts +64 -0
  427. package/dist/util/sortable-list.js +53 -0
  428. package/dist/util/stepper.d.ts +39 -0
  429. package/dist/util/stepper.js +38 -0
  430. package/dist/util/swipeable-list-item.d.ts +35 -0
  431. package/dist/util/swipeable-list-item.js +32 -0
  432. package/dist/util/temporal-field.d.ts +88 -0
  433. package/dist/util/temporal-field.js +136 -0
  434. package/dist/util/theme-mode.d.ts +79 -0
  435. package/dist/util/theme-mode.js +121 -0
  436. package/dist/util/toaster.d.ts +132 -0
  437. package/dist/util/toaster.js +130 -0
  438. package/dist/util/tone-surface.d.ts +33 -0
  439. package/dist/util/tone-surface.js +35 -0
  440. package/dist/util/tooltip.d.ts +9 -0
  441. package/dist/util/tooltip.js +18 -0
  442. package/dist/util/top-navbar.d.ts +34 -0
  443. package/dist/util/top-navbar.js +28 -0
  444. package/dist/util/touch-target.d.ts +30 -0
  445. package/dist/util/touch-target.js +30 -0
  446. package/dist/util/zabi-strings.d.ts +141 -0
  447. package/dist/util/zabi-strings.js +105 -0
  448. package/dist/zabi-components-colors.css +11 -2
  449. package/dist/zabi-components-theme-dark-only.css +870 -258
  450. package/dist/zabi-components-theme-dark.css +870 -258
  451. package/dist/zabi-components-theme-only.css +2419 -368
  452. package/dist/zabi-components-theme.css +2419 -368
  453. package/dist/zabi-components.css +6032 -711
  454. package/docs/lucide-icons.md +40 -17
  455. package/docs/theme-imports.md +81 -11
  456. package/package.json +45 -8
package/CHANGELOG.md ADDED
@@ -0,0 +1,2935 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## Changelog policy (theme/token surface)
6
+
7
+ Whenever token or CSS import API surface changes, include:
8
+ - exported path additions/removals/deprecations (`package.json` `exports`)
9
+ - token rename/add/remove (for `--color-*` and `--zabi-*`)
10
+ - mapping rule updates (for example dark semantic mapping)
11
+ - migration guidance when compatibility aliases remain temporarily
12
+
13
+ ## [Unreleased]
14
+
15
+ ### Fixed
16
+
17
+ - **BottomTabBar**: a tab's side padding is 2px (was 4px), so a five-tab bar keeps a label like "Kalender" whole at 320px.
18
+ - **Toaster**: a stack that scrolls keeps the newest toast in view when its words arrive after it is shown and it grows. It was pushed below the end of the stack.
19
+
20
+ ## [9.0.0] - 2026-10-13
21
+
22
+ 9.0.0 is a new look and a larger set. Bars, sheets, menus and toasts are glass materials, the light page is
23
+ near-white, dark mode is drawn on the neutral ramp, and solid controls have a soft gradient. The shell is
24
+ redesigned: flush bars that turn to glass over content, a large `AppBar` title, an `AppShell` `navigation` slot
25
+ with `AppNavigation`. New components include `Chip` and `ChipGroup`, `Block`, `Stat`, `PickerField`, `CodeField`,
26
+ `SpeechBubble`, the SVG charts (`LineChart`, `BarChart`, `PieChart`, `Sparkline`) and `MapView` (from
27
+ `zabi-components/map`, with `maplibre-gl` as an optional peer). It is a major because of what an app shows
28
+ without a code change, not because of its API: nothing from 8.1.0 was removed or renamed, and every `exports`
29
+ path, prop, variant and token is still there.
30
+
31
+ The work was built as 32 local preview builds, none published to npm. Their sections below are the full record:
32
+ every addition, every changed default and, where one exists, the line that restores the 8.1 look.
33
+
34
+ ### Upgrade notes
35
+
36
+ Coming from 8.1.0, these are visible without a code change. The section named in brackets has the detail and
37
+ the line that restores 8.1.
38
+
39
+ **Appearance, light and dark**
40
+
41
+ - The default typeface is the platform's UI face, and transitions use `--ease-standard` (alpha.1).
42
+ - The light page is near-white (`--color-surface-base` is `--zabi-base-50`), a default `Card` has a 1px edge and
43
+ no shadow, status tints are a step lighter and shadows are softer (alpha.1).
44
+ - Primary, accent and danger buttons, the `FloatingActionButton` and the on `Toggle` have a soft gradient. The
45
+ outline button has a 3:1 edge. The off `Toggle` is a light tint, and its knob is 3:1 on the track (alpha.2,
46
+ alpha.26).
47
+ - `SegmentedControl` selects with a neutral thumb, and its indicator and the one in `Tabs` slide. `List` rows
48
+ are divided by a hairline, and a subtle `Badge` has no visible border (alpha.2).
49
+ - `BottomSheet`, `Drawer`, `Modal`, the `Dropdown` menu, the `Select` list, `Tooltip` and every toast are frosted
50
+ glass. `Tooltip` has no arrow by default. `data-materials="opaque"` turns the glass off (alpha.2).
51
+ - `AppBar`, `BottomTabBar`, `StickyActionBar` and `TopNavbar` are the page colour at rest and glass once content
52
+ is under them. `--color-surface-chrome` is the page colour. The active tab has no outline, and the sidebar
53
+ and the top navbar are restyled (alpha.3).
54
+ - Dark surfaces are steps of the neutral ramp, the primary and danger buttons keep their hue in dark under a
55
+ white label (`createTheme({ darkPrimary: 'mirror' })` gives the 8.1 fills), and dark status tints are
56
+ low-chroma (alpha.4).
57
+ - In dark, a form field is a translucent well and a flat `Card` is a translucent layer (alpha.6).
58
+
59
+ **Layout and behaviour**
60
+
61
+ - A `bg-*` or `border-*` class on a bar no longer changes its surface: the fill is painted on the material's
62
+ layer (alpha.3).
63
+ - The `AppShell` footer lies over the scroller, so content passes under the tab bar (alpha.3).
64
+ - `Collapsible` with the default trigger is a native `<details>` with a `<summary>`, not a `<button>`
65
+ (alpha.5).
66
+ - `Input type="password"` has the show/hide button by default (`revealable={false}` removes it), and `Textarea`
67
+ has no resize grip on a touch screen (alpha.6).
68
+ - A `Select` in a sheet with up to six options and no search gets a sheet as tall as its list
69
+ (`sheetFit={false}` restores the half sheet) (alpha.8).
70
+ - A `RadioGroup` option's `description` is no longer part of the radio's name, and `Skeleton` is still under
71
+ reduced motion (alpha.12).
72
+ - `TopNavbar` opens its links in a bottom sheet below `collapseAt` (alpha.16).
73
+ - Toasts: a mouse holds the timer only once it has moved over the toast, a pushed id replaces the showing
74
+ toast in place, and a touch swipe dismisses (alpha.18).
75
+ - `Button` is capped at its container's width and hyphenates a label that does not fit (alpha.32).
76
+
77
+ **Themes**
78
+
79
+ - `createTheme` checks the material pairs it used to skip, so a theme can get a contrast warning it did not
80
+ get in 8.1.0. Links follow an overridden primary (alpha.2, alpha.4).
81
+ - The theme snapshot only gained names: no published token was removed.
82
+
83
+ ### Fixed since alpha.32
84
+
85
+ Found by the first full browser run on 9.0.
86
+
87
+ - A secondary `Button` or `IconButton` on a brand block (`on-brand`, `Card tone="brand"`, a brand `AppBar`) in
88
+ dark: its wash was the dark theme's white one, under the block's white label, which left the label at
89
+ 4.1:1. On a brand block the wash is now the dark one in both modes (5.7:1). A surface inside the block gets
90
+ the theme's wash back.
91
+ - A brand block inside an accent block, or an accent block inside a brand block, with or without a card
92
+ between them, took its own label colour as its fill: white text on white. The inversion of a solid fill
93
+ inside a block no longer applies to an element that is a block itself.
94
+
95
+ ### Not verified
96
+
97
+ The preview builds were tested lightly on purpose (type and design checks, unit tests in jsdom, the package
98
+ build). Not done by anyone: a real phone, Safari, Firefox, a screen reader, an accessibility review. `MapView`
99
+ has never been drawn against a real map: its tests use a stand-in for MapLibre.
100
+
101
+ ## [9.0.0-alpha.32] - 2026-10-13
102
+
103
+ A thirty-second local preview build of the 9.0 direction, not published to npm. No token, prop, variant,
104
+ string key, export or data attribute of alpha.31 is removed or renamed. Nothing in it has been rendered in a
105
+ browser: type and design checks, unit tests in jsdom, the package build.
106
+
107
+ ### Added
108
+
109
+ - `MapView`, from a new entry `zabi-components/map`: a map on MapLibre GL JS with markers from a list (Z-137,
110
+ D262). `maplibre-gl` is an optional peer dependency (`>=5.0.0`, built against 6.14.0): the app installs it, and
111
+ the component loads it with `import("maplibre-gl")` when it is mounted in a browser. No other entry of the
112
+ package exports the map, so importing anything else brings none of it. The library's stylesheet is not needed.
113
+ - `points` (`id`, `lng`, `lat`, `label`, `checked`), `styleLight` and `styleDark` (URL or object; the dark one
114
+ is drawn while `data-theme` on `<html>` is dark, or `auto` on a dark system, and a change keeps the position
115
+ and the markers), `onpointpress(point, index)`, `onclusterpress(points)`, `clusterLabel(points)`.
116
+ - Markers are buttons with a 44px tap area in px, in the order of `points`. Markers whose tap areas overlap are
117
+ one count marker, which sits at the place of its first point in the tab order and keeps its element (and the
118
+ focus) while others join or leave it. A marker reached with Tab outside the view is jumped into view.
119
+ - `fitMaxZoom` (default 16), `bind:center`, `bind:zoom`, `onmove`, and the methods `fit()`,
120
+ `showPoint(id, { inset, zoom })` and `showPlace(place, { inset, zoom })` through `bind:this`. Every move is a
121
+ jump. With `center` and `zoom` given at the start the map opens there and is not fitted.
122
+ - `viewer`: a dot at a position the page hands in; the map jumps to it. The component never asks the browser.
123
+ - `bind:status` / `onstatus`: `loading`, `ready`, `library-error`, `tiles-error`. `tiles-error` is told when the
124
+ style cannot be fetched, or tiles failed and none has arrived; a tile that arrives later takes it back.
125
+ - `attribution` (string or snippet); without it the words of the style's own attribution, as plain text. Always
126
+ shown, bottom left. `strings`: `label`, `cluster(count)`, `viewer`. `mapViewSupported()` answers whether the
127
+ browser has WebGL without loading the library.
128
+ - Not in `ZabiStringsProvider` yet: the words come through `strings`. A drag that starts on a marker does not
129
+ pan the map.
130
+ - `Textarea`: `count`, `countFrom` and `countText` show a character count under the field when it has a
131
+ `maxlength`: "12 / 2000" by default, at the end of the line the hint is on, part of the field's description and
132
+ not a live region. `TextareaCount` is exported (Z-125, D254).
133
+ - `Toggle`: `description`, a line under the label and inside it, so a tap on it toggles. It is tied to the switch
134
+ with `aria-describedby`, after any ids passed in, and does not join the name (Z-126, D255).
135
+ - `Checkbox`: `description`, as `Radio` has. `Checkbox` and `Radio`: `controlPosition="end"` puts the control
136
+ last in the row, which then fills its width; the markup order is unchanged (Z-128, D256).
137
+ - `ImageUpload`: `multiple`, `maxFiles` and `onfiles(files)`. With `multiple` the chooser and a drop take several
138
+ files and hand them over together, in order; the component keeps none (no preview, `value` not set,
139
+ `onfileselect` not called). `onfilereject` has a third reason, `"count"`, only with `maxFiles` (Z-130, D258).
140
+ - `PhotoViewer`: `onloaderror(photo, index)`, once per address that fails, and `onretry(photo, index)`, called
141
+ when "Try again" is pressed; a promise it returns is waited for, with the button busy, before the image is
142
+ asked for again. `PhotoGrid`: `onloaderror(photo, index)` for a thumbnail (Z-132, D259).
143
+ - `PhotoGrid`: a photo's `status` (`uploading`, with an optional `progress` from 0 to 1, or `error`) is drawn on
144
+ its tile and said in its name; `onretry(photo, index)` puts a "Try again" button on a failed one. New strings
145
+ with defaults: `uploading`, `uploadFailed`, `retryUpload(name)`. `PhotoStatus` is exported (Z-133, D260).
146
+ - `PhotoViewer`: a photo's `credit` is a second, quieter line under the caption, outside what "More" folds. The
147
+ dialog is described by the caption and the credit after the photo's name and place (Z-135, D261).
148
+
149
+ ### Fixed
150
+
151
+ - `Button`: never wider than its container (`max-inline-size: 100%`, as `Badge`), and a word that alone does not fit is hyphenated by
152
+ the page's language (`hyphens-auto`). A label that already wraps may now be hyphenated at a line's end
153
+ (Z-129, D257).
154
+ - `Checkbox`, `Radio`, `RadioGroup`: the input's touch target is 44px in px, centred on the box. In rem it grew
155
+ with the text: 88px at 200%, past the screen's edge from a box at the end of a row (Z-128, D256).
156
+ - `PhotoGrid`: nothing leaves the add tile at large text. The label is cut at two lines (the name keeps the whole
157
+ text) and the plus is drawn only where the tile has room for it above two lines (Z-136, D260).
158
+ - `PhotoViewer`: when the control that had the focus is taken out (an action that goes with its photo, with or
159
+ without a confirm dialog between) and the focus is left on `<body>`, Close takes it (Z-134, D261).
160
+
161
+ ### Notes
162
+
163
+ - `Collapsible` is not for a "show all" under a list; a `Button` with `aria-expanded` and `aria-controls` under
164
+ the list is the intended way (Z-127, D263).
165
+ - `PhotoViewer` does not take the browser's Back: a history entry made by a component conflicts with the app's
166
+ router. Open it from the router's own state, as SvelteKit's shallow routing does (Z-131, D263).
167
+
168
+ ## [9.0.0-alpha.31] - 2026-10-11
169
+
170
+ A thirty-first local preview build of the 9.0 direction, not published to npm. No token, prop, variant,
171
+ string key, export or data attribute of alpha.30 is removed or renamed.
172
+
173
+ ### Fixed
174
+
175
+ - `Input`: a placeholder longer than the field ends in an ellipsis (`placeholder-shown:text-ellipsis`), not half way
176
+ through a letter. A typed value is unchanged: it still scrolls with the caret. A placeholder that fits looks the
177
+ same. Fields built on `Input` (`ColorPicker`, the search field of `Select`, `SidebarPanel` and
178
+ `SidebarNavigation`) get it too (Z-124, D252).
179
+
180
+ ## [9.0.0-alpha.30] - 2026-10-11
181
+
182
+ A thirtieth local preview build of the 9.0 direction, not published to npm. No token, prop, variant,
183
+ string key, export or data attribute of alpha.29 is removed or renamed.
184
+
185
+ ### Added
186
+
187
+ - `Skeleton`: every piece carries `data-skeleton` (its variant; a line of a `lines` paragraph is `text`, beside the
188
+ existing `data-skeleton-line`), in every branch: announced, `decorative`, and inside a `SkeletonGroup`. A
189
+ paragraph's wrapper has `data-skeleton-paragraph`. The `animate-pulse` class stays on the pieces (Z-123, D251).
190
+ - `Skeleton` and `SkeletonGroup` pass other `div` attributes (`id`, `data-testid`, `style`, `aria-*`) to their
191
+ root element. `role`, `aria-busy`, `aria-hidden` and `data-skeleton` are not overridden. `SkeletonGroup` has
192
+ `data-skeleton-group`.
193
+
194
+ ## [9.0.0-alpha.29] - 2026-10-10
195
+
196
+ A twenty-ninth local preview build of the 9.0 direction, not published to npm. No token, prop, variant,
197
+ string key, export or data attribute of alpha.28 is removed or renamed.
198
+
199
+ ### Fixed
200
+
201
+ - `Select`: an option in the list that opens has the pointer cursor, as a `DropdownItem` has; a disabled option
202
+ keeps `not-allowed`.
203
+ - `ActionPanel` as a button (`onclick`, no `href`) has the pointer cursor.
204
+
205
+ ## [9.0.0-alpha.28] - 2026-10-10
206
+
207
+ A twenty-eighth local preview build of the 9.0 direction, not published to npm. No token, prop, variant,
208
+ string key, export or data attribute of alpha.27 is removed or renamed.
209
+
210
+ ### Added
211
+
212
+ - `AppBar`: the row carries `data-title-empty` when the bar has no `title`.
213
+
214
+ ### Fixed
215
+
216
+ - `AppBar` with no `title` and a wide `leading`: the empty title part no longer asks for 72px, so it no longer
217
+ sends the bar to `data-title-row="own"` and takes a row of its own (the bar was 12px taller). A bar with
218
+ nothing to show as a title stays on one row whatever room is left, on the server and before it is measured
219
+ as well; `leading` is still cut to the room beside the back control and the actions. A bar with a title is
220
+ unchanged.
221
+
222
+ ## [9.0.0-alpha.27] - 2026-10-10
223
+
224
+ A twenty-seventh local preview build of the 9.0 direction, not published to npm. Loose ends. No token,
225
+ prop, variant, string key, export or data attribute of alpha.26 is removed or renamed.
226
+
227
+ ### Added
228
+
229
+ - `zabi-components/atoms` now also exports `Toast`, `Tooltip`, `Progress`, `Skeleton`, `SkeletonGroup`,
230
+ `CodeBlock`, `FeatureCard`, `OptimizedImage`, `DEFAULT_SKELETON_STRINGS` and the type `SkeletonStrings`,
231
+ which the root entry already had.
232
+ - Bottom sheets (`BottomSheet`, `SlideUp`): content with `touch-action: none`, or `pan-x` alone, under the
233
+ finger keeps its own gesture (a map, a canvas, a drawing pad); no `data-sheet-no-drag` is needed.
234
+ `data-sheet-no-drag` still works.
235
+
236
+ ### Changed
237
+
238
+ - Bottom sheets: the axis of a swipe in the content is decided once per gesture, after 8px. A swipe that
239
+ begins sideways stays sideways and never moves the sheet, even if it later turns down. A downward swipe
240
+ of under 8px no longer starts a drag. The grip is unchanged.
241
+
242
+ ### Fixed
243
+
244
+ - `Drawer`: the overlay root is `overflow-clip`, not `overflow-hidden`, as the bottom sheet's already was
245
+ (WebKit scrolled a hidden root to a focused control while the panel was still off screen).
246
+
247
+ ## [9.0.0-alpha.26] - 2026-10-10
248
+
249
+ A twenty-sixth local preview build of the 9.0 direction, not published to npm. Three rulings from the
250
+ owner (2026-10-10). No token, prop, variant, string key, export or data attribute of alpha.25 is removed or
251
+ renamed. One default colour changes on purpose: the `Toggle` knob (below). Run on this build: the type and
252
+ design checks, the unit suite, the package build and a scratch install; no browser.
253
+
254
+ ### Changed
255
+
256
+ - `Toggle`: the default knob is now 3:1 or more on its track in every enabled state, light and dark
257
+ (WCAG 1.4.11). Before, the knob was the card colour: 1.27:1 on the off track in light and 1.57:1 in dark, and
258
+ 1.74:1 to 3.39:1 on the primary fill in dark. Now off is `--color-toggle-knob` (light base-500: 3.81:1 on the resting
259
+ track, 3.53:1 hovered, 3.27:1 pressed; dark base-700: 7.07:1, 6.13:1, 5.23:1) and on is
260
+ `--color-toggle-knob-checked` (the primary fill's label colour: 4.86:1, 6.76:1, 9.44:1 on the fill at rest,
261
+ hovered, pressed). The off track's edge is unchanged (4.63:1 on the page and 4.83:1 on a card in light).
262
+ `--color-toggle-thumb` still wins when set; `--color-toggle-thumb-checked`, declared nowhere, sets the
263
+ on knob alone. The knob pairs are in the contrast guard. (Z-121, D240)
264
+
265
+ ### Fixed
266
+
267
+ - `Checkbox`, `Radio`: the box is centred on the row again and the row keeps its height and touch target, as
268
+ before alpha.25; the first-line alignment and `pointer-coarse:py-3` of alpha.25 are withdrawn. The box
269
+ still does not shrink (`shrink-0`). (Z-119, D241)
270
+ - Forced colours: a busy button `Card` (`loading`) has `GrayText` text and a dashed `GrayText` edge, and the
271
+ `Toaster` time bar's track has a `CanvasText` edge. Nothing changes outside `forced-colors: active`.
272
+ (D242)
273
+
274
+ ## [9.0.0-alpha.25] - 2026-10-10
275
+
276
+ A twenty-fifth local preview build of the 9.0 direction, not published to npm. Five entries
277
+ from the app team (Z-117 to Z-121). No token, prop, variant, string key, export or data attribute of
278
+ alpha.24 is removed or renamed; new props, tokens and string keys are optional. Run on this build: the
279
+ type and design checks, the unit suite, the package build and a scratch install; no browser.
280
+
281
+ ### Added
282
+
283
+ - `TimeField` `openAt` (`HH:mm`): where an empty field's columns stand when the sheet opens. Nothing is
284
+ chosen and `value` stays empty. Without it the columns stand at now, on the minute step. (Z-117, D235)
285
+ - `TimeField` string key `chosenTime` ("Chosen time"): the sheet says the time Done will set, beside
286
+ Done, so it is on the screen even when a column is scrolled away from it (Z-118, D236).
287
+ - `--color-toggle-thumb`, declared nowhere: the `Toggle` knob's colour. Unset, the knob is the card colour as
288
+ before (Z-121, D239).
289
+
290
+ ### Fixed
291
+
292
+ - `Checkbox`, `Radio`: the box no longer shrinks when the label wraps (`shrink-0` always), and sits by the
293
+ first line of a label that wraps (Z-119, D237).
294
+ - `AppBar`: the parts of the row are measured as laid out (up to the next pixel) and not by `offsetWidth`,
295
+ so actions with a fractional width no longer wrap to a row of their own (Z-120, D238).
296
+
297
+ ## [9.0.0-alpha.24] - 2026-10-10
298
+
299
+ A twenty-fourth local preview build of the 9.0 direction, not published to npm. Two entries
300
+ from the app team (Z-115, Z-116). No token, prop, variant, string key, export or data attribute of
301
+ alpha.23 is removed or renamed; nothing changes outside `forced-colors: active`. Run on this
302
+ build: the type and design checks, the unit suite, the package build and a scratch install; no browser.
303
+
304
+ ### Fixed
305
+
306
+ - Forced colours (Windows contrast themes): `Toggle` has a 1px `ButtonText` edge on the track, a thumb
307
+ in `ButtonText`; on is a `Highlight` track with a `HighlightText` thumb; disabled is `GrayText`
308
+ (Z-115, D232).
309
+ - Forced colours: a ticked `Checkbox`, `Radio` and `RadioGroup` option is a `Highlight` box with a
310
+ `HighlightText` tick or dot, an unticked one an edge in `ButtonText`, a disabled one `GrayText`
311
+ (Z-116, D233).
312
+ - Forced colours: `Slider` has a `Highlight` fill on a `Canvas` track with a `ButtonText` edge, and the
313
+ `Toaster` time bar is `Highlight` (D234).
314
+
315
+ ## [9.0.0-alpha.23] - 2026-10-09
316
+
317
+ A twenty-third local preview build of the 9.0 direction, not published to npm. One entry
318
+ from the app team (Z-114). No token, prop, variant, string key, export or data attribute of
319
+ alpha.22 is removed or renamed. Run on this build: the type and design checks, the unit
320
+ suite, the package build and a scratch install; no browser.
321
+
322
+ ### Fixed
323
+
324
+ - `Checkbox`, `Radio` and `RadioGroup`: the edge of an unticked box is `--color-control-border` (3:1 on
325
+ every surface, both themes) and not the fixed step `base-400`, which was 2.5:1 in light and 1.9 to 2.3:1
326
+ in dark. In a brand or accent block it follows the block's on-colour too (Z-114, D231).
327
+
328
+ ## [9.0.0-alpha.22] - 2026-10-09
329
+
330
+ A twenty-second local preview build of the 9.0 direction, not published to npm. Six entries
331
+ from the app team (Z-108 to Z-113). No token, prop, variant, string key, export or data attribute of
332
+ alpha.21 is removed or renamed. Run on this build: the type and design checks, the unit
333
+ suite, the package build and a scratch install; no browser.
334
+
335
+ ### Added
336
+
337
+ - `Card` `loading` (with `type`): `aria-busy` and `aria-disabled`, the press swallowed, the pressed look held,
338
+ size and Tab stop kept (Z-108, D225).
339
+ - `BottomTabBar` `pending`: the tab just pressed takes the mark while `aria-current="page"` stays on `active`
340
+ until it changes; the pending link has `aria-busy` (Z-109, D226).
341
+ - `.z-fixed`, beside `.z-sticky` and the other layer classes (Z-113, D230).
342
+
343
+ ### Fixed
344
+
345
+ - `Button` in forced colours: the variants drawn by a fill (`primary`, `accent`, `secondary`, `danger`,
346
+ `tonal`) get a 1px `ButtonBorder` edge there, and a disabled or loading button a `GrayText` one (Z-110, D227).
347
+ - `Stepper` inside a `SkeletonGroup` no longer renders its own `role="status"` (Z-111, D228).
348
+ - `Skeleton`: the variant's width and height are written at zero specificity (`:where()`), so any size
349
+ class at the call site wins, including a size name of the app's own (Z-112, D229).
350
+
351
+ ## [9.0.0-alpha.21] - 2026-10-08
352
+
353
+ A twenty-first local preview build of the 9.0 direction, not published to npm. One entry
354
+ from the app team (Z-107). No token, prop, variant, string key, export or data attribute of
355
+ alpha.20 is removed or renamed. Run on this build: the type and design checks, the unit
356
+ suite, the package build and a scratch install; no browser.
357
+
358
+ ### Fixed
359
+
360
+ - `BottomSheet`, `SlideUp` (and so `Select`, `Dropdown` and `TopNavbar` sheets, `TimeField`, `DateField`): a
361
+ swipe down that starts on a box inside the content that scrolls by itself (the hours and minutes of
362
+ `TimeField`, the list of a `Select`, a long text area) no longer moves the sheet; the box keeps the
363
+ gesture, at its top too, and nothing is handed over mid-gesture (Z-107, D224). The sheet still follows
364
+ a swipe from the grip, the header, the content outside such a box when the content is at its top.
365
+ Default change: a swipe down on a scrolling box at its top used to close the sheet and now does not.
366
+
367
+ ### Added
368
+
369
+ - `data-sheet-no-drag` is read for a touch in the content as well as in the header: anything carrying
370
+ it, or inside it, and every `<input type="range">`, keeps its drag from the sheet. `TimeField`'s
371
+ columns, `ColorPicker`'s colour map and the handle of `SortableList` carry it.
372
+
373
+ ## [9.0.0-alpha.20] - 2026-10-08
374
+
375
+ A twentieth local preview build of the 9.0 direction, not published to npm. Two
376
+ entries from the app team, the first time the snackbar was drawn in a browser
377
+ (Z-105, Z-106). No token, prop, variant, string key or export of alpha.19 is removed
378
+ or renamed. Run on this build: the type and design checks, the unit suite, the package
379
+ build and a scratch install; no browser.
380
+ ### Added
381
+
382
+ - `Toaster` `timeLine?: boolean` and `pushToast` / `ToastItem` `timeLine?: boolean`: whether the thin
383
+ bar along the bottom of a timed toast is drawn. Unset, it is drawn as before, except under a
384
+ confirmation in the snackbar that has nothing to press (timed, no action, `closable: false`),
385
+ where it is now left out. `false` leaves it out, `true` draws it; a toast's own value wins over the
386
+ Toaster's. The bar has `data-toast-timeline`; the sentence for a screen reader about the time left stays.
387
+ - A unit test that fails on a Tailwind class put together at run time (`max-sm:${x}`, `w-[${px}px]`).
388
+ ### Changed
389
+
390
+ - `Toaster mode="auto"`: the clip that makes the toast come out from behind the docked edge was a
391
+ class assembled at run time (`max-sm:` + the rule), which is in no stylesheet, so below 640px the
392
+ toast slid in over the tab bar. It is written out whole now. Nothing to change in an app; an app
393
+ that carried the class itself as a stand-in can remove it.
394
+ - A snackbar confirmation with nothing to press no longer has the time bar (see Added); pass
395
+ `timeLine` to bring it back.
396
+
397
+ ## [9.0.0-alpha.19] - 2026-10-08
398
+
399
+ A nineteenth local preview build of the 9.0 direction, not published to npm. One
400
+ entry from the app team (Z-104), about `CodeField`; no token, prop, variant,
401
+ string key or export of alpha.18 is removed or renamed. One default changes where
402
+ it was a defect, under Changed. Run on this build: the type and design checks,
403
+ the unit suite, the package build and a scratch install; no browser.
404
+ ### Added
405
+
406
+ - `CodeField` `groups?: number | number[]`: draws the boxes in groups with a wider gap
407
+ (`groups={4}` on 8 places is 4 and 4, `groups={[3, 3, 4]}` is explicit). Only the drawing:
408
+ one input, one value, `one-time-code`, the post and a paste of "1234 5678" or "1234-5678" are
409
+ unchanged, and a screen reader still hears one field. A row that cannot fit wraps between groups.
410
+ - `CodeField` `size?: "sm" | "md" | "lg"` (default `"md"`, today's look): `sm` is a compact row, 44px tall.
411
+ - Export of the type `CodeFieldSize`.
412
+
413
+ ### Changed
414
+
415
+ - `CodeField` boxes no longer shrink below 1.15em of their own digit, so a digit is never wider than
416
+ its box at large text or a narrow screen (it overlapped the box edges at 200% text with eight
417
+ places); where the boxes would have been narrower the row now wraps (between groups when `groups`
418
+ is set). At 100% text nothing changes up to 9 places at 320px (10 at 360px); a longer code that is not grouped now wraps there instead of drawing slivers. A tap in the gap between boxes or on a second row now
419
+ puts the caret at the nearest box.
420
+
421
+ ## [9.0.0-alpha.18] - 2026-10-08
422
+
423
+ An eighteenth local preview build of the 9.0 direction, not published to npm. It
424
+ makes the toast work on a phone (the app team's Z-097 to Z-103). No token, prop,
425
+ variant, string key or export of alpha.17 is removed or renamed, and a `Toaster`
426
+ with no new prop looks and behaves as before, except for the four defect fixes
427
+ under Changed. Run on this build: the type and design checks, the unit suite,
428
+ the package build and a scratch install; no browser, no screen reader.
429
+ ### Added
430
+
431
+ - `Toaster` `mode?: "stack" | "snackbar" | "auto"` (default `"stack"`, type `ToasterMode`): the
432
+ snackbar is one row of 52px (two lines at most), one at a time, its action beside its text, set
433
+ directly above what is docked at the bottom and sliding out from behind that edge on
434
+ `--ease-standard` (no overshoot, no movement under reduced motion). `"auto"` is the snackbar below
435
+ 640px and the stack from there up.
436
+ - `Toaster` `max` (how many show; the oldest timed toast leaves, never an error, one with an action or
437
+ a persistent one; 1 in the snackbar), `avoid` (a CSS selector for what the stack must not lie on)
438
+ and `swipeToDismiss` (default `true`).
439
+ - The toaster finds a `BottomTabBar` or `StickyActionBar` (`data-bar-edge="bottom"`), a
440
+ `FloatingActionButton` and a dirty `UnsavedChangesBar` (new `data-toast-clear`) that lie under the
441
+ stack and sits above the highest, at any width.
442
+ - `pushToast` and `ToastItem`: `closable?: boolean` (default `true`); `false` leaves the close button
443
+ out of a short timed toast, which then has no control and takes no taps. `pushToast` takes `id?`.
444
+ ### Changed
445
+
446
+ - A mouse holds a toast's timer once it has moved over the toast, not when the toast arrives under a
447
+ pointer at rest (Z-098: it never closed). `mouseenter` and `pointerenter` alone no longer hold it.
448
+ - A toast pushed with the id of one that is showing replaces it in place and restarts its time
449
+ (before, a second item with the same id was added, which a keyed list cannot have).
450
+ - A touch or a pen swipe dismisses a toast (sideways; `swipeToDismiss={false}` turns it off). The close
451
+ button stays. The toast has `touch-action: pan-y`.
452
+ - One line of text is centred on the buttons beside it; text that wraps stays at the top.
453
+ - The words of a toast are put in its `role="status"` / `role="alert"` element about 100 ms after the
454
+ toast is mounted, so the live region is in the page empty first (Z-102). The server's markup has
455
+ them at once. Tests that read a toast's text right after `pushToast` need to wait for it.
456
+
457
+ ## [9.0.0-alpha.17] - 2026-10-08
458
+
459
+ A seventeenth local preview build of the 9.0 direction, not published to npm. It
460
+ finishes the `TopNavbar` sheet of alpha.16; no token, prop, variant or export of
461
+ alpha.16 is removed or renamed, and no default changes. Run on this build: the
462
+ type and design checks, the unit suite, the package build and a scratch install; no
463
+ browser.
464
+ ### Changed
465
+
466
+ - `TopNavbarStrings`: `menuTitle` (alpha.16) and the new `themeHeading` are optional in the
467
+ interface, with their defaults in `DEFAULT_TOP_NAVBAR_STRINGS` (now `Required<TopNavbarStrings>`).
468
+ Code that writes out a complete `TopNavbarStrings` object, as it could before alpha.16, type-checks
469
+ again; this repairs the note on alpha.16 that said it did not.
470
+ - `TopNavbar` sheet: it grows with its content up to the full screen below the status bar, where
471
+ alpha.16 stopped at half of it, so eight links and the two sections below them do not scroll in
472
+ a short box. Choosing a theme or an accent does not close the sheet.
473
+ ### Added
474
+
475
+ - `BottomSheet` `fitMax?: "half" | "full"` (default `"half"`): with `fit`, how tall the sheet may
476
+ grow before its content scrolls. Without it nothing changes.
477
+
478
+ ## [9.0.0-alpha.16] - 2026-10-08
479
+
480
+ A sixteenth local preview build of the 9.0 direction, not published to npm. One
481
+ behaviour change in `TopNavbar`, which you can turn off with one attribute; no
482
+ token, prop, variant or export of alpha.15 is removed or renamed. Run on this
483
+ build: the type and design checks, the unit suite, the package build and a
484
+ scratch install; no browser.
485
+ ### Changed
486
+
487
+ - `TopNavbar`: below `collapseAt` the menu button opens the links in a bottom sheet (`BottomSheet`,
488
+ `fit`) instead of the panel under the bar. The sheet is a modal dialog: focus moves in and back
489
+ to the button, Escape, the backdrop, the close button and a swipe down close it, the page behind
490
+ is locked, it is named by the new string `menuTitle` ("Menu"), and its links sit in a `nav` named
491
+ by `ariaLabel` (else `menuTitle`). Rows are full width and 44px; `actions`, the theme toggle, the
492
+ current-page mark and the new-tab note are as in the panel; a followed link, a `currentPath`
493
+ change and a wider screen close it. The menu button gets `aria-haspopup="dialog"`, and the bar no
494
+ longer takes the glass while the menu is open. **Opt out:** `menu="inline"` is the panel of
495
+ alpha.15 and earlier, unchanged. Without script the phone menu is as inert as before.
496
+ - `TopNavbar` sheet, below the links: the `actions` and the theme choice have a section of their
497
+ own at the full width of the sheet. The built-in theme toggle is a segmented control there
498
+ (light / dark, or system / light / dark with `themeModes="three"`, 44px on a touch screen, words
499
+ from `themeLabels`, heading from the new string `themeHeading`, "Theme"); it writes the page
500
+ as `ThemeToggle` does, which stays as it was in the bar. Actions are laid out in a column at
501
+ full width. The docs site's `BrandSwitcher` is a labelled group of large rows in the sheet and
502
+ the small button in the bar.
503
+ - `BottomSheet`: the slide-in is on the standard curve (`--ease-standard`) with no overshoot;
504
+ snapping between snap points and settling a drag keep `--ease-spring`.
505
+ ### Added
506
+
507
+ - `getTopNavbarPlace()` and type `TopNavbarPlace` (`"bar" | "menu"`): a control in `TopNavbar`'s
508
+ `actions` calls it while it initialises to know whether it is in the bar or the sheet; the
509
+ snippet gets no argument. `"bar"` outside a bar and in `menu="inline"`.
510
+ - `TopNavbar` `menu?: "sheet" | "inline"` (default `"sheet"`), exported type `TopNavbarMenu`, strings
511
+ `menuTitle` and `themeHeading` in `TopNavbarStrings` (code that writes out a complete
512
+ `TopNavbarStrings` object needs the new keys; `strings` itself is partial). Stories for both modes, catalog entry, docs, `?menu=inline`
513
+ on the chaos-lab page.
514
+
515
+ ## [9.0.0-alpha.15] - 2026-10-08
516
+
517
+ A fifteenth local preview build of the 9.0 direction, not published to npm. One
518
+ addition to `AppShell`, `AppShellHeaderPart`; no token, prop, variant or export of
519
+ alpha.14 is removed or renamed, and no default changes. Run on this build: the type
520
+ and design checks, the unit suite, the package build and a scratch install; no
521
+ browser.
522
+ ### Added
523
+
524
+ - `AppShellHeaderPart`: the consumer chooses what stays at the top. Put several in `AppShell`'s
525
+ `header` snippet, each with `sticky` (default `true`) or `sticky={false}` to scroll away with
526
+ the content: the bar can leave while a filter row stays, or a large title can go while the bar
527
+ and the tabs stay. Plain `position: sticky` in the scroller's flow, no scroll listener; the
528
+ stuck parts stack under one another in document order (a part that scrolls away in between
529
+ does not leave a gap), and the shell reports only what is stuck as `--app-shell-top-inset` and
530
+ `scroll-padding-top`. One glass surface and one hairline under the lowest stuck part; the
531
+ status bar is covered by whatever is topmost, also after the first part has gone. Stuck parts
532
+ may cover 40% of the scroller at most, the lower ones then scroll (`data-released`). A header
533
+ without parts and `AppBar collapseOnScroll` / `largeTitle` behave as before. `AppBar` now
534
+ carries `data-appbar`. Types: `AppShellHeaderPartProps`. Story, catalog entry and demo toggle.
535
+
536
+ ## [9.0.0-alpha.14] - 2026-10-08
537
+
538
+ A fourteenth local preview build of the 9.0 direction, not published to npm. One
539
+ entry from the app team (Z-096), a new component `CodeField`; no token, prop,
540
+ variant or export of alpha.13 is removed or renamed, and no default changes. Run
541
+ on this build: the type and design checks, the unit suite, the package build and
542
+ a scratch install; no browser.
543
+ ### Added
544
+
545
+ - `CodeField`: a field for a verification or one-time code (Z-096). One real `<input>`
546
+ (`autocomplete="one-time-code"`, `inputmode="numeric"`) under boxes drawn and hidden from
547
+ assistive technology, so the iPhone and Android code suggestion, paste of a whole code
548
+ ("123 456" and "123-456" too, with the caret anywhere) and one accessible name work, and the
549
+ form posts the code by `name` with scripts off, where the input is a plain bordered field.
550
+ `length` (1 to 12, default 6), `value` (bindable), `mode` (`numeric` default, `alphanumeric`),
551
+ `label` / `labelHidden`, `hint`, `error`, `variant` + `message`, `required`, `disabled`,
552
+ `readonly`, `oncomplete(value)` and `strings`; the words (`codeField.label`,
553
+ `codeField.description`) come from `ZabiStringsProvider`. Exports: `CodeField`,
554
+ `DEFAULT_CODE_FIELD_STRINGS`, types `CodeFieldProps`, `CodeFieldMode`, `CodeFieldStrings`.
555
+
556
+ ## [9.0.0-alpha.13] - 2026-10-07
557
+
558
+ A thirteenth local preview build of the 9.0 direction, not published to npm. Two
559
+ entries from the app team (Z-094, Z-095), both about `Skeleton`; no token, prop,
560
+ variant or export of alpha.12 is removed or renamed, and no default changes: a
561
+ lone `Skeleton` is the same status named "Loading…". Run on this build: the type
562
+ and design checks, the unit suite, the package build and a scratch install; no
563
+ browser.
564
+ ### Added
565
+
566
+ - `SkeletonGroup`: one `role="status"` with the loading label for the `Skeleton` pieces
567
+ inside it, which are hidden from assistive tech, so a screen of eight pieces is announced
568
+ once. `Skeleton` takes `decorative` (no role, no label, `aria-hidden`) for a piece beside
569
+ a status of your own, and `strings` (`label`); the label reads the provider's
570
+ `skeleton.label` before the English "Loading…", and `aria-label` still wins. A lone
571
+ `Skeleton` is unchanged. (Z-094)
572
+ - `Skeleton` takes `tone="on-color"`: the surface's label colour at 24% (`bg-skeleton-on-color`,
573
+ in `@layer utilities`), for the `on-brand`, `on-accent` and `on-fill` blocks and a `Card` with
574
+ `tone`, in light and dark; `--zabi-skeleton-fill` replaces it. The default `tone="neutral"` is
575
+ `bg-neutral-subtle` as before. And `lines` (1 to 20, text variant): one placeholder with that many
576
+ lines and a shorter last line (60%); `width` and `class` apply to the paragraph, `height` to each
577
+ line, and it is one status like a lone piece. (Z-095)
578
+
579
+ ## [9.0.0-alpha.12] - 2026-10-07
580
+
581
+ A twelfth local preview build of the 9.0 direction, not published to npm. Five
582
+ entries from the app team (Z-089 to Z-093); no token, prop, variant or export of
583
+ alpha.11 is removed or renamed. Two defaults change what a consumer gets: a
584
+ `RadioGroup` option with a description is named by its label alone, and a
585
+ one-line option is centred in its row. Run on this build: the type and design
586
+ checks, the unit suite, the package build and a scratch install; no browser.
587
+
588
+ ### Added
589
+
590
+ - `Chip` takes `loading` on its button forms (`type="submit"`, `"button"` and the toggle):
591
+ `aria-busy` and `aria-disabled`, a spinner over the label, the press swallowed, the
592
+ width and the focus kept, as `Button`'s. A link and a `checkbox` / `radio` chip ignore it. (Z-091)
593
+ - `Checkbox` and `Radio` take `error`, `variant`, `message` and `hint`, drawn under the
594
+ label as on `Input`: an error is an alert, sets `aria-invalid` on a checkbox and is
595
+ tied to the input with `aria-describedby` (after the hint). `Toggle` is unchanged. (Z-090)
596
+
597
+ ### Fixed
598
+
599
+ - `Skeleton` no longer pulses under `prefers-reduced-motion`: it is a still fill
600
+ (`motion-reduce:animate-none`). A `motion-reduce:animate-pulse` class on the
601
+ skeleton brings the pulse back. (Z-093)
602
+ - `RadioGroup`: an option's `description` is no longer part of the radio's name; it is
603
+ `aria-hidden` and tied to the radio with `aria-describedby`, as `Radio`'s. A one-line
604
+ option is centred in its row (44px on touch) and its circle loses `mt-0.5`; a row
605
+ with a description keeps the radio at the top. A radio found by its two-sentence
606
+ name is now found by its label. (Z-092)
607
+ - `Select` no longer outgrows its container: a width class (`w-48`) in a narrower
608
+ column is capped at the column (`.select-container { max-width: 100% }`, in the
609
+ components layer, so a `max-w-*` class of your own still wins). (Z-089)
610
+
611
+ ## [9.0.0-alpha.11] - 2026-10-07
612
+
613
+ An eleventh local preview build of the 9.0 direction, not published to npm. One
614
+ fix for the app team's report Z-088; no token, prop, variant or export of
615
+ alpha.10 is removed or renamed. Run on this build: the type and design checks,
616
+ the unit suite, the package build and a scratch install, and one run in
617
+ Playwright's WebKit (an iPhone 15 profile, not iOS Safari).
618
+
619
+ ### Fixed
620
+
621
+ - `BottomSheet` no longer rises to the top of the screen as it opens on WebKit,
622
+ and no longer rests 32px above the bottom edge. The overlay root was
623
+ `overflow: hidden`, a scroll container, and WebKit scrolled it to the focused
624
+ grip while the panel was still below the screen; it is now `overflow: clip`.
625
+ The slide's own spring (a 15px overshoot at half) is unchanged. To restore the
626
+ old root: `overflow-hidden` for `overflow-clip` on the root in
627
+ `BottomSheet.svelte`.
628
+
629
+ ## [9.0.0-alpha.10] - 2026-10-07
630
+
631
+ A tenth local preview build of the 9.0 direction, not published to npm. One
632
+ addition for the app team's request Z-087; no token, prop, variant or export of
633
+ alpha.9 is removed or renamed, and nothing an app shows changes without a
634
+ change to its code. Run on this build: the type and design checks, the unit
635
+ suite, the package build and a scratch install. Not run: Playwright or any
636
+ look in a browser.
637
+
638
+ ### Added
639
+
640
+ - `Avatar` `children`: a snippet drawn inside the circle in place of `src` and
641
+ the initials, for an inline SVG that reads the page's tokens. It is
642
+ presentational; `name` stays the accessible name, and the circle keeps its
643
+ size.
644
+ - `AvatarGroup` people take `picture?: Snippet`, the same for one person.
645
+ - A list row's `avatarPicture?: Snippet` (`ListItemData`): the same in the
646
+ row's round; wins over `avatar` and `icon`; decorative unless `avatarAlt`
647
+ names it.
648
+
649
+ ## [9.0.0-alpha.9] - 2026-10-07
650
+
651
+ A ninth local preview build of the 9.0 direction, not published to npm. Two
652
+ additions for the app team's request Z-086; no token, prop, variant or export
653
+ of alpha.8 is removed or renamed, and nothing an app shows changes without a
654
+ change to its code. Run on this build: the type and design checks, the unit
655
+ suite, the package build and a scratch install. Not run: Playwright or any
656
+ look in a browser.
657
+
658
+ ### Added
659
+
660
+ - `RadioGroup` `chosenRing` (default `true`). `chosenRing={false}` leaves the
661
+ filled radio to show the chosen row: the row's own ring is gone, and keyboard
662
+ focus keeps its ring.
663
+ - `Radio` `description`: a line under the label, tied to the radio with
664
+ `aria-describedby`; the label alone names the radio.
665
+
666
+ ## [9.0.0-alpha.8] - 2026-10-07
667
+
668
+ An eighth local preview build of the 9.0 direction, not published to npm. Four
669
+ additions for the app team's requests Z-082 to Z-085; no token, prop, variant
670
+ or export of alpha.7 is removed or renamed. One default changes what an app
671
+ shows without a change to its code: a `Select` in a sheet with up to six
672
+ options and no search field gets a sheet as tall as its list; `sheetFit={false}`
673
+ restores the half-height sheet. Run on this build: the type and design checks,
674
+ the unit suite, the package build and a scratch install. Not run: Playwright or
675
+ any look in a browser.
676
+
677
+ ### Added
678
+
679
+ - `Chip type="submit"` (Z-082; D185): a button chip that posts its form with `name` and `value`, and does not toggle; `selected` is the app's and is drawn as on a toggle and said with `aria-pressed`. `type="button"` is the same without the submit. Without a `type` a chip is the toggle button it was.
680
+ - `Select` options take `lang` (Z-083; D186): set on the option's row, on the trigger's text while it is chosen and on the native `<option>`.
681
+ - `Select sheetFit`, `Dropdown sheetFit` and `BottomSheet fit` (Z-084; D187): a sheet as tall as its content, up to half the screen, with one resting height.
682
+ - `Card type="submit" | "button" | "reset"` with `name`, `value`, `form` and `disabled` (Z-085; D188): the whole card is one `<button>`, drawn and pressed as a link card is, with the 44px minimum. With `href` it is still a link.
683
+
684
+ ### Changed
685
+
686
+ - A `Select` shown in a sheet, with up to six options and `searchable={false}`, now gets a sheet as tall as its list instead of half the screen (Z-084; D187). Restore the half sheet with `sheetFit={false}`.
687
+
688
+ ## [9.0.0-alpha.7] - 2026-10-07
689
+
690
+ A seventh local preview build of the 9.0 direction, not published to npm. One
691
+ addition on top of alpha.6; no token, prop, variant or export is removed or
692
+ renamed, and with the new properties unset every `Badge` renders as before.
693
+
694
+ ### Added
695
+
696
+ - `Badge` neutral subtle fill and text (Z-081; D184): `--zabi-badge-neutral-fill` and `--zabi-badge-neutral-text`, declared nowhere and read in front of `--color-neutral-subtle` and `--color-neutral-text`. Set on any ancestor or in the theme, in light and dark, they move the neutral subtle `Badge` (`variant="neutral"` or `"default"`) and nothing else: `Alert variant="neutral"`, `Skeleton`, `AvatarGroup`, `Header` and the other badge variants keep reading the neutral tokens. New classes `bg-badge-neutral` and `text-badge-neutral` (`@layer utilities`), the first listed beside `bg-neutral-subtle` in the colour-block rules, so a badge in `on-brand`, `on-accent`, `on-fill` and `Card tone` is restored as before. Theme output gains only those lines.
697
+
698
+ ## [9.0.0-alpha.6] - 2026-10-07
699
+
700
+ A sixth local preview build of the 9.0 direction, not published to npm; 8.1.0
701
+ stays the stable version. New: the charts drawn in plain SVG (`LineChart`,
702
+ `BarChart`, `PieChart`, `Sparkline`) and `SpeechBubble`. Extended: `Textarea`
703
+ (`resize`, `autogrow`, `maxRows`), `ImageUpload` (`variant="button"`,
704
+ `maxSize`), `ConfirmDialog` (`cancelVariant`). No token, prop, variant or
705
+ export is removed or renamed. Four things change what an app shows without a
706
+ change to its code, each with the line that restores the earlier look in its
707
+ entry below: a dark form field is a well a step lighter than its surface; a
708
+ dark flat `Card` is a translucent layer; a `Textarea` has no resize grip on a
709
+ touch screen; `Input type="password"` has the show/hide button by default.
710
+ Run on this build: the type and design checks, the unit suite, the theme
711
+ tests, the package build and a scratch install. Not run: Playwright, an
712
+ accessibility pass, any look in a browser at the changes since alpha.5 other
713
+ than the charts.
714
+
715
+ ### Added
716
+
717
+ - `ImageUpload variant="button"` (Z-076; D183): only the trigger, for a photo that is one choice in a row of others. A `Button` that opens the file chooser (`buttonVariant`, default `secondary`; `buttonSize`, default `md`; its text is `label`, or `children`), with `accept`, `onclick`, `onbrowse`, `onchange`, `onfileselect`, `bind:value`, `disabled`, the error block and the polite announcement as in the dropzone; no drop area, no preview, no Change or Remove. The default `variant="dropzone"` is unchanged.
718
+ - `ImageUpload maxSize` (bytes), in both variants: a larger file, chosen or dropped, is not selected and goes to `onfilereject`, which now gets a second argument, `"type"` or `"size"`. Without `maxSize` nothing changes. The component had no size limit before.
719
+
720
+ - `Textarea autogrow` and `maxRows` (Z-077; D181): the field grows with its content from `rows` up to `maxRows` rows and scrolls after that (no limit without `maxRows`). It uses `field-sizing: content` where the browser has it and measures the content elsewhere. An autogrow field has no resize grip.
721
+
722
+ - `ConfirmDialog cancelVariant` (Z-080; D182): the cancel button's `Button` variant, any `ButtonVariant`. The default is `outline`, so nothing changes; `tonal`, `secondary`, `ghost` or `text` for an app that has no outline button. Cancel stays the first control and keeps the initial focus.
723
+
724
+ - **Charts in plain SVG: `LineChart`, `BarChart`, `PieChart`, `Sparkline`** (Z-004; D156 to D159). The library draws its own charts and has no chart-library dependency; no dependency was added. The three charts are each a `figure` whose `figcaption` is the required `label` (`labelHidden` keeps it for assistive technology only); the drawing is hidden from assistive technology and the same numbers are in a real `table`, off screen unless `showTable`. They take the width of their container and are laid out in real pixels, so text is never scaled; before they are measured (on the server) they are drawn 600 wide and scaled to fit.
725
+ - `LineChart`: `series` (`{ name, points, color? }[]`; `null` in `points` leaves a gap), `xLabels`, `yMin`, `yMax`, `yFormat` (`'number'`, `'percent'` where 72 is "72 %", or a function), `height` (px, default 240), `legend` (default: on for more than one series), `markers` (default on: a different shape per series, so lines differ by more than colour), `xTitle`, `locale`, `glass`. Pointing at or tapping the plot shows the values at that x label in a pane of the thick material that floats over the plot beside the guide, as a `Tooltip` does; there is no keyboard route to that readout, the table is.
726
+ - `BarChart`: `data` (`{ label, value, color? }[]`), `color`, `yMax`, `yFormat`, `height`, `showValues` (default on), `categoryTitle`, `valueTitle`, `locale`. One set of upright bars from a zero line; values below zero are drawn downward.
727
+ - `PieChart`: `data`, `donut`, `center` (a snippet in the middle of the ring), `format`, `size` (px, default 192; shrinks with the container), `legend` (default on: each slice's label, value and share), `categoryTitle`, `valueTitle`, `shareTitle`, `locale`. Slices run clockwise from twelve o'clock in the order given and are parted by a thin cut, so they need no border colour; in a full pie the cut narrows to a point at the centre, so every slice runs to the middle.
728
+ - `Sparkline` (atom): `points`, `label` (an image by that name; without it, decoration), `color`, `min`, `max`. 5rem by 1.5rem unless `class` says otherwise.
729
+ - Colours: a series without a `color` takes `--zabi-chart-1` to `--zabi-chart-6` by its place; these are declared nowhere and fall back to steps of the library's own ramps: `--color-action-primary` (what a `Progress` fills with), `--color-accent-500`, `--color-brand-900`, `--color-accent-700`, `--color-base-500`, and for a sixth series a plum mixed from the brand and error ramps. None is a status colour. On the default theme each is 3:1 or more on a card in light and dark and any two differ for a reader who does not see red and green. `color` takes one of `primary`, `accent`, `success`, `warning`, `error`, `info`, `neutral`, or any CSS colour; in a chart that names one, a series without a `color` steps past the series colour nearest to it. In forced colours the marks are drawn in `CanvasText`.
730
+ - Built from the library's parts: the caption is set like a `Progress` label; a bar is a `Progress` fill stood on end (at most 24px wide, a round far end) and with a `yMax` each bar stands in a `Progress` track up to it, the values in a row over the tracks; grid lines are `--color-border` and the base line `--color-border-strong`; the `PieChart` key is set like `List` rows (name, value, share; parted in `--zabi-list-divider-color`) and takes the width beside the drawing, or the full width under it when the chart itself is narrower than 26rem; `showTable` shows the library's `Table`, numbers at the end of their column; a donut's ring is a third of the radius and takes a `center` snippet (a `Stat` fits); lines are 2px with 8px markers, each in a ring cut out of the lines so it shows whatever is behind the chart (`--zabi-chart-surface` paints the ring instead); a `Sparkline` ends in a dot on its last value. Chart text is 0.75rem with tabular numerals and the value axis is as wide as its widest label as drawn, so both follow the reader's text size.
731
+ - Glass marks: the slices of a `PieChart` and the bars of a `BarChart` are drawn as glass by default: the series colour, opaque, under the veil of a solid control, with one pixel of light on the top of the rim, a soft sheen under it and a soft shadow. No backdrop blur is involved (an SVG shape takes none), so it reads on a plain card too. Flat fills with reduced transparency, more contrast, forced colours and inside `data-materials="opaque"`. Line markers, key marks and the `Sparkline` dot stay flat.
732
+ - Glass panel: `glass` on `LineChart`, `BarChart` and `PieChart` stands the chart on a panel of the thick material (off by default: it needs a wash, a tint or a picture behind it, and on a plain card it shows only an edge and a shadow). It is the library's `material-thick`, so reduced transparency, more contrast, forced colours, no `backdrop-filter` and `data-materials="opaque"` make it the opaque surface. Over a solid brand or accent fill some series colours fall under 3:1 on it; THEMING.md has the numbers.
733
+ - Types `ChartSeries`, `ChartDatum`, `ChartColor`, `ChartColorName`, `ChartFormat` are exported. Not included: stacked or grouped bars, horizontal bars, areas, a second axis, zoom, animation.
734
+ - **`SpeechBubble`** (Z-075; D155): a quotation in a bubble with a drawn tail. A `figure` whose `figcaption` is `label` (a string or a snippet; `labelHidden` keeps it for assistive technology only) and whose content is a `blockquote` with the children as real, selectable text. `side` (`end` default, `start`; follows the text direction) places the tail; `tone` is `ink` (default: the headline colour as fill and the page colour as text, so dark in light and light in dark) or `paper` (the two swapped, for a bubble on a filled block). `decoration` is a snippet drawn on the top corner of the tail's side. Four custom properties declared nowhere, set on the component or any ancestor: `--zabi-bubble-fill` and `--zabi-bubble-text` (win over `tone`), `--zabi-bubble-radius` (any `border-radius` value, the eight-value form included; default `1.5rem`) and `--zabi-bubble-tilt` (turns the fill and tail, not the text; default `0deg`). The tail is an inline SVG in the fill colour, sized in rem; tail, fill and decoration are hidden from assistive technology; in forced colours the bubble is a 2px outline and the tail is not drawn. The type is `text-lg font-medium` on the figure and can be replaced through `class`. Not included: a copy action and a speaker slot.
735
+
736
+ ### Changed
737
+
738
+ - `Textarea` has no resize grip on a touch screen (Z-077; D181). New prop `resize`: `auto` (default: the browser's vertical grip with a mouse or trackpad, none where the pointer is coarse), `vertical`, `none`. On a phone the grip was 16px in the field's corner and dragging it scrolled the page. Desktop is unchanged. The earlier behaviour everywhere: `resize="vertical"`. A `resize-*` class passed in `class` still wins.
739
+
740
+ - **In dark, a form field is a well of its own, not the page colour inside an edge** (Z-078; D180). `--color-input` in dark is a translucent step of the neutral ramp (base-50 at 7%; `-hover` 10%, `-focus` 7%, `-active` 15%) instead of the page step, so `Input`, `Textarea`, `Select`, `DateField`, `TimeField` and `PickerField` are a little lighter than whatever they sit on: 1.20:1 off the page, 1.22:1 off a card, 1.23:1 off a sheet on the default theme, with no rule per surface. Light is unchanged. The field edge (`--color-input-border`) is unchanged and stays 3:1 against the surface outside the field; against the lifted fill inside it is 3.05:1 on the page, 2.79:1 on a card and 2.50:1 on a sheet. The dark placeholder is a shade lighter (`#ababb3`, was `#a1a1aa`) to keep 4.5:1 on the lifted fill; on a pressed field in a sheet it is 4.08:1. New tokens, additive: `--color-input-solid`, `-solid-hover`, `-solid-focus`, `-solid-active`, the opaque field a colour block (`.on-brand`, `.on-accent`, `.on-fill`) uses, where a translucent fill would sit on the block's colour. New guard pairs in `check:contrast` and in `createTheme`'s check, dark only, laid over the page, a card and the overlay: the well against its surface (1.15:1), value and placeholder on every field state, the hover and pressed steps, the edge on the fill. `--color-surface-inset`, `--color-input-disabled` and the progress track are unchanged. Restore the earlier dark field: `overrides: { dark: { '--color-input': 'var(--color-surface-inset)', '--color-input-hover': 'var(--color-base-200)', '--color-input-focus': 'var(--color-base-150)', '--color-input-active': 'color-mix(in srgb, var(--zabi-base-50) 10%, var(--color-surface-inset))', '--color-input-placeholder': 'var(--color-base-600)' } }`.
741
+
742
+ - `Input type="password"` has the show/hide button by default (`revealable` now defaults to `true`). Pass `revealable={false}` for a password field without it. Other types are unaffected.
743
+
744
+ - In dark, `Card variant="flat"` is a translucent, slightly lighter layer (the neutral ramp's base-50 at 7%, hover 11%, pressed 15%) instead of the opaque card fill, so flat cards nested in each other, in a default card, an elevated card or a sheet each read a step lighter and stay visible. Light is unchanged: a light flat card computes to the same fill as before. New tokens `--color-card-flat`, `--color-card-flat-hover`, `--color-card-flat-active` (utilities `bg-card-flat`, `hover:bg-card-flat-hover`, `active:bg-card-flat-active`), settable through `createTheme` `overrides`. To keep the opaque dark fill: `overrides: { dark: { '--color-card-flat': 'var(--color-card)' } }`, and the same for `-hover` and `-active` (`var(--color-card-hover)`, `var(--color-card-active)`).
745
+
746
+ ### Fixed
747
+
748
+ - `BottomSheet`: the panel's height no longer transitions while the sheet slides in (Z-079). The height transition is for snapping and for settling a drag; during the slide-in a late change of height rode the spring and moved the content after it had arrived. It is switched on when the slide has ended (at once under reduced motion). The slide itself and the snaps keep the spring.
749
+
750
+ - `Input type="search"` with a clear button of its own in `trailing` no longer also shows the browser's clear cross in Chrome and Safari. A search field without `trailing` keeps the browser's.
751
+
752
+ ## [9.0.0-alpha.5] - 2026-10-07
753
+
754
+ A fifth local preview build of the 9.0 direction, not published to npm; 8.1.0
755
+ stays the stable version. It is the components the consuming app asked for.
756
+ New: `Chip` and `ChipGroup` (a link, a toggle button or a real radio or
757
+ checkbox input under one tonal selected style, in a wrap or a one-row layout
758
+ that scrolls sideways), `Block` (the full-width colour section), `Stat`,
759
+ `PickerField` (a field-shaped trigger for an app's own sheet). Extended: `Card`
760
+ is linkable (`href`) and takes a `tone` or a custom `fill`; `Collapsible` is a
761
+ native `<details>` and opens without scripts; `Progress` gets a 16px size, a
762
+ segmented form and a track edge token; `Rating` a compact score, 40px stars
763
+ and a soft empty state; `Heading` a display variant on a display typeface
764
+ token; `Checkbox`, `Radio` and `Toggle` a `leading` snippet; `Alert` an edge
765
+ that can be turned off and a brand variant; `DateField` and `TimeField` a
766
+ `locale` with the library's own display and picker over the native input.
767
+ No token, prop, variant or export is removed or renamed. Four things change
768
+ what a consumer sees or gets without a code change, each listed under Changed
769
+ or Fixed with what restores it where something does: the default
770
+ `Collapsible` trigger is a `<summary>` instead of a `<button>`; a solid
771
+ control in a colour block's own colour is inverted inside that block; a
772
+ `Stepper` bar's upcoming segments are filled with the progress track; and the
773
+ floating `BottomTabBar` is 72px tall with equal insets. Built quickly and
774
+ tested lightly on purpose: the type and design checks, the unit run, the theme
775
+ tests, the package build and a scratch install were run on it, and the new
776
+ components' stories were looked at once in Storybook with headless Chromium in
777
+ light and dark at phone and desktop widths (two things were fixed from that
778
+ look). No browser suite was run, there was no accessibility or QA review, and
779
+ nothing was tried on a real phone, in Safari or Firefox, without scripts in a
780
+ real browser, at 200% text, right-to-left, in forced colours, or with a
781
+ keyboard or a screen reader.
782
+
783
+ ### Added
784
+
785
+ - **Chip tokens** (Z-051): `--color-chip`, `-hover`, `-active`, `-text` (a quiet neutral fill without an edge: `base-200` / `300` / `350` in light, `--zabi-base-750` / `700` / `650` in dark, the label is the headline colour) and `--color-chip-selected`, `-hover`, `-active`, `-text`, which default to the tonal role (`--color-action-tonal*`), so an app that themes tonal gets matching chips. Classes `bg-chip`, `hover:bg-chip-hover`, `active:bg-chip-active`, `text-chip-text`, `bg-chip-selected`, `hover:bg-chip-selected-hover`, `active:bg-chip-selected-active`, `text-chip-selected-text`. Label pairs guarded at 4.5:1 on all fills in both modes, in the library and in `createTheme`.
786
+ - **`--color-card-tint`** (`bg-card-tint`; Z-055): the brand tint as a surface for content. `brand-100` in light, the brand at 8% over the card in dark. Text roles and the link are guarded on it at 4.5:1, the focus ring, the field edge and the outline-button edge at 3:1.
787
+ - **`.on-fill`** (Z-055): the on-colour scope for a fill the app chooses. Like `.on-brand`, with the label colour taken from `--zabi-on-fill` (fallback `currentColor`): text roles, links, edges and focus rings inside take it, and a surface inside (a Card, a field, a Chip) gets the theme's roles back. The library cannot check the contrast of a colour picked at run time.
788
+ - **`--color-progress-track-border`** (`border-progress-track-border`; Z-049): the edge of the Progress track, by default the field edge as before. `createTheme` writes `transparent` for a mode in which an overridden `--color-progress-track` itself reaches 3:1 against the page and the card and the app did not set the edge; an edge the app sets itself is never moved.
789
+ - **`--font-family-display`** (`font-display`; Z-058): a second typeface as a token; follows `--font-family-heading` until set. `cn` knows `font-heading` and `font-display` as font families.
790
+ - **`Checkbox`, `Radio` and `Toggle` take a `leading` snippet** (Z-060; D145): drawn inside the one `<label>`, between the control and the label text (an avatar in an attendee list), so a press on it toggles the control. The accessible name stays the `label` string; give a leading image an empty alt. Without `leading` the markup is unchanged.
791
+ - **`Chip`** (Z-051; D144): one pill, four forms. With `href` it is a link (`selected` sets `aria-current`, `current` picks its value) and works without scripts; with `type="checkbox"` or `type="radio"` it is a real input inside a label, which submits in a form without scripts and whose selected look follows the native `:checked`, so a choice made before hydration shows at once and is kept; otherwise it is a toggle button with `aria-pressed`. One selected style in every form and both modes: the tonal fill and label (`--color-chip-selected*`) and a semibold label, no outline and no solid fill; a system-colour outline in forced colours. Props: `selected` (bindable), `size` (`sm` 28px, `md` 32px, `lg` 40px; a 44px target on coarse pointers without growing the box), `leading`, `checkmark` (a check while selected; on for a checkbox and a toggle button, off for a link and a radio), `disabled`, `name`, `value`, `required`, `form`, `onchange`. A repeat press on the selected radio chip does nothing; a checkbox or toggle chip turns off.
792
+ - **`ChipGroup`** (Z-051, Z-059; D144): `layout="wrap"` (default) or `layout="row"`, one line that scrolls sideways without a scrollbar, with the chosen chip brought into view on load (only the row scrolls; `scrollSelectedIntoView={false}` turns it off) and an edge cue, `edge="fade"` (default: a mask on a side that has more chips beyond it, so it works on any background; none on the server or before hydration) or `edge="none"`. `label` (or `aria-label` / `aria-labelledby`) names the group, `showLabel` shows it. With `type="radio"` or `type="checkbox"`, `name` and `value` (bindable; a string, or an array for checkboxes) the chips inside are inputs that take only a `value`. A `ChipGroup` inside a `ChipGroup` is a named sub-group in the same row or wrap, with a wider gap (24px) between sub-groups.
793
+ - **`Progress size="xl"`** (16px) and **`Progress segmented`** (Z-049; D147): `segmented` draws `max` separate segments with `value` of them filled, for a count such as 4 of 19 (`max` a whole number from 2 to 24; any other `max` draws the continuous bar and warns in a dev build). Still one `role="progressbar"`, with `aria-valuetext` from the new string `progress.valueText(value, max)` ("4 of 19"; set it once in `ZabiStringsProvider` or per instance with `strings`), and with `label` the read-out is that text instead of the percent.
794
+ - **`Card href`** (Z-042; D146): the card itself is an `<a>` (with `target`, `rel`), so the whole card is the link and it works without scripts: focus ring on the card edge, hover and pressed states, a 44px minimum, the content keeps the body colour. A link card must not hold other interactive elements.
795
+ - **`Card tone`, `fill` and `onFill`** (Z-055; D146): `tone="neutral"` (default, unchanged), `"tint"` (`--color-card-tint`), `"brand"` (the primary fill with the `on-brand` scope) or `"accent"`; `fill` with `onFill` is a colour the app chooses with its label colour (the new `on-fill` scope; the app holds that pair to 4.5:1, the library cannot check it). A toned card has no border and no shadow (unless `variant="elevated"`). Text, links, outline and ghost buttons, edges and focus rings inside take the fill's label colour; a neutral Card, a field or an Alert inside is a surface with the theme's colours again. An interactive toned card shows hover and pressed as a veil of its own text colour.
796
+ - **`Block`** (Z-055; D146): the full-width, square-cornered colour section, with the same `tone`, `fill` and `onFill`, `padding` (`none`, `sm`, `md`, `lg`) and `as` (`div`, `section`, `header`, `footer`, `article`, `aside`). It does not break out of a padded parent by itself: place it outside the padded container or give it a negative inline margin through `class`.
797
+ - **`Rating compact`** (Z-054, Z-066; D149): a score for a list, not a control: one filled star and the number ("4,0", through `formatValue`), from the real `value` and `max`, as one image with one accessible name ("4 of 5 stars", the readonly string). It implies `readonly`. Sizes `sm` (14px), `md` (16px), `lg` (20px), `xl` (28px star, 30px number) and `display` (40px star, 48px number in the display face).
798
+ - **`Rating size="xl"`** (40px stars, for a large input) and **`Rating empty="soft"`** (Z-054; D149): an empty star as a soft filled shape with no outline, in `--zabi-rating-off-fill` (falls back to `--color-control-track`; declared nowhere, set it on any ancestor). The soft fill is below 3:1 against the page by default, so it suits a rating with a visible label and value; in forced colours it is an outline. The default stays `empty="outline"`.
799
+ - **`Stat`** (Z-058; D150): a figure with a label. `value`, `label`, `unit` (after the value, smaller, on the same baseline: `value="4" unit="av 19"`), `size` (`sm` 24px, `md` 36px, `lg` 48px), `align`, `labelPosition` (`below`, `above`), or children for a figure that needs markup. The value is in the display face with tabular figures; plain text in reading order, no role.
800
+ - **`Heading variant="display"`** (Z-058; D150): the display face (`--font-family-display`) with its own steps: 60px (48px below `sm`), 48 (36), 36 (30), 30, 24 and 20px by `size ?? level`, balanced wrapping. Its weight and tracking are two custom properties declared nowhere, shared with `Stat`: `--zabi-display-weight` (default 700) and `--zabi-display-tracking` (default -0.02em). So a second typeface is three properties on any ancestor and no CSS rule. `variant="heading"` (default) is unchanged.
801
+ - **`Alert bordered`, `--zabi-alert-border-width` and `Alert variant="brand"`** (Z-072; D152). `bordered={false}` makes the edge transparent (the box keeps its size); the edge's width reads `--zabi-alert-border-width` (default `1px`, declared nowhere), so `--zabi-alert-border-width: 0px` on `:root` or any ancestor removes the edge from every Alert under it with no prop. `variant="brand"` is the theme's brand tint (`--color-action-primary-subtle`, edge `--color-action-primary-border`, icon in the link colour), with `role="status"`. `variant="neutral"` already existed. The default Alert is unchanged.
802
+ - **`PickerField`** (Z-071; D151): a field-shaped trigger that opens whatever the app gives it (its own sheet with search, a page). It looks and measures like a `Select` of the same `size`, with `label`, `hint`, `error` (and `variant` / `message`) wired as on the other fields, `value` (the text of what is chosen), `placeholder`, a `leading` snippet, children for a rich value, and a chevron. It is a `<button aria-haspopup="dialog">` named by the label and the value (`haspopup`, `expanded` and `controls` set the ARIA attributes), `bind:element` hands the app the trigger to return focus to, and `name` with `formValue` submit the chosen id through a hidden input. With `href` it is a link, for a picker that is a page and works without scripts.
803
+ - **`DateField` and `TimeField` take a `locale`** (Z-053; D153). Without it nothing changes. With it, the server and the page before hydration still render the native `<input type="date|time">` (the fallback without scripts; it submits as before). Once the page runs, the native input stays as the form control, out of sight, and a field-shaped button takes its place in the same box, showing the value in the app's locale through `Intl.DateTimeFormat` ("10 nov. 2026", "18:30", never the browser's own format), with `format` (the `Intl` options, for "tis 6 okt") and `placeholder`. Pressing it opens the library's picker in a `BottomSheet` titled by the label: the `Calendar` for a date (choosing a day sets the value, closes and returns focus; `min` and `max` are honoured), and two listbox columns, hours and minutes, for a time (hours labelled as the locale writes them, minutes every `step`, 5 minutes without one, a value off the step kept as its own option; Done commits). Clear is offered when the field is not `required`. `picker="native"` keeps the locale display and opens the platform's picker with `showPicker()` instead (and a `step` under a minute does the same); where that is refused the native input comes back. `input` and `change` fire on the native input for a value set by the picker, and a `required` empty field moves focus to the button instead of showing a bubble on a hidden control. New strings `dateField` and `timeField` (`clear`, `done`, `hours`, `minutes`, `chooseDate`, `chooseTime`) through `ZabiStringsProvider` or `strings`. The sheet is used at every width in this build. `PickerField` gains a `trailing` snippet that replaces its chevron.
804
+
805
+ ### Changed
806
+
807
+ - **`Collapsible` opens without scripts** (Z-041; D143). With the default trigger (`title`) it is now a native `<details>` / `<summary>`: it opens before hydration and for a visitor without scripts, a toggle made before hydration is kept, find-in-page opens it, and a single-open `CollapsibleGroup` gives its panels a shared `name` so the browser keeps them exclusive too. Markup change: the default trigger is a `<summary>` (it keeps `id`, `data-collapsible-trigger` and `triggerClass`), not a `<button>` with `aria-expanded`, the heading of `headingLevel` is inside it, and the panel is no longer `hidden` while closed (a form inside a closed panel keeps its values and still submits them, as before). A test that looked for `role="button"` needs the summary instead. A `disabled` panel has `aria-disabled` and leaves the tab order. With a custom `trigger` snippet nothing changes, and that path still needs scripts. No prop is added or removed.
808
+ - **`Progress` reads its track edge from `--color-progress-track-border`** (class `border-progress-track-border`, was the hard-coded `border-input-border`; Z-049). The default value is the same field edge, so nothing moves until the token is set (or `createTheme` sets it to `transparent` for a track that reaches 3:1 on its own).
809
+ - **`Stepper`, bar layout: an upcoming segment is a filled track** (Z-050; D147): fill `--color-progress-track`, edge `--color-progress-track-border`, instead of a hollow `--color-control-border` outline. With the default tokens it looks nearly as it did (the field fill inside the field edge); an app that themes the progress track gets a filled, edgeless track. Restore the 8.1 outline on any ancestor: `--zabi-stepper-upcoming-fill: transparent; --zabi-stepper-upcoming-edge: var(--color-control-border)`.
810
+ - **A solid control in the block's own colour is inverted inside a block** (D148). Inside `.on-brand`, `.on-accent` and `.on-fill` (so inside `Card tone="brand"`, a brand `AppBar`, or a block an app already marks), an element with `bg-action-primary` on a brand block (a primary `Button`, a solid brand `Badge`, a checked `Checkbox`), or with `bg-accent` on an accent block, was the same colour as the block; it now takes the block's label colour as its fill and the block's colour as its text, with hover and pressed mixed from the two, and a filled `Rating` star takes the label colour. A surface inside the block restores the theme's values. Outside a block nothing changes.
811
+ - **`AppBar`: the row's fixed pixel scale is documented** (Z-074) on the `leading` and `actions` props: the row sets `--spacing: 4px`, so content placed there should be sized on it (`size-9`, or `calc(var(--spacing) * 9)`), not in rem, or it grows at 200% text while the bar's own controls do not. No behaviour change.
812
+
813
+ ### Fixed
814
+
815
+ - **The floating `BottomTabBar` capsule is concentric with its tabs** (D154): the tabs sat 8px inside the capsule at its ends but 4px above and below, under a 28px corner. They now sit the same distance inside on all four sides (the list's gap: 8px, less in a very narrow bar), and the capsule's radius is the tab's 20px plus that distance, so outer radius = inner radius + padding holds at every width. The floating bar is 72px tall (was 64px); `--app-shell-bottom-inset` follows, it is measured. The edge-to-edge bar is unchanged.
816
+ - **`Alert`: the close button's radius follows `--zabi-alert-border-width`**, so it stays concentric with the alert when the edge is set to `0px`.
817
+ - **The active tab's forced-colours outline is the system highlight colour** in `BottomTabBar` and `AppNavigation`: the class `forced-colors:outline-(color:Highlight)` was never emitted (that form is for custom properties), so the outline fell back to the text colour. It is `forced-colors:outline-[color:Highlight]` now. `ring-(color:--color-bar)`, the ring around a tab's count, was checked in the built stylesheet and is emitted.
818
+ - **`TimeField` picker: the chosen minute is in view and the chosen row is legible** (found in the look at this build): the columns centred their chosen row from `offsetTop`, which counts from the sheet, so only the focused hours column was right; and the chosen row's label kept the body colour on the primary fill in light. Both corrected before the build was packed.
819
+ - **Storybook: `Heading / All levels`, `Display` and `Display with own face` render** (they passed a string as a component), and the `Stat` row on a brand block wraps on a phone.
820
+
821
+ ## [9.0.0-alpha.4] - 2026-10-06
822
+
823
+ A fourth local preview build of the 9.0 visual direction, not published to npm;
824
+ 8.1.0 stays the stable version. It is dark mode on its own terms: the dark
825
+ surfaces are steps of the neutral ramp, so a tinted neutral keeps its hue and
826
+ chroma on every level, in two tone steps, with overlays set apart by a rim and
827
+ a shadow instead of by grey; the primary and danger buttons keep their colour
828
+ in dark under a white label; the dark status tints are low-chroma tints of the
829
+ card; and dark sheets, menus and bars show glass again, because the dark
830
+ material dims what is behind it. Light does not change, with one exception:
831
+ the light thick material is 88% instead of 86%. `createTheme` gains
832
+ `darkPrimary`, now checks the material pairs it was silently skipping (a theme
833
+ may get a warning it did not get before), and keeps the ramp's ink in the mode
834
+ an override did not name. AppShell gains `flushTop` and AppBar a fourth tone,
835
+ `inherit`; `cn` merges the named spacing steps. No token, prop, variant or
836
+ export is removed or renamed; `tokens/surface-ladder.js` (not a published
837
+ file) exports steps instead of wash alphas. Several dark defaults change what a
838
+ consumer sees, and each has a line that restores the earlier look. Built
839
+ quickly and tested lightly on purpose: the type and design checks, the unit
840
+ run, the theme tests, the package build and a scratch install were run on it,
841
+ and the shell, Card, Alert, Badge, Button, Progress, Toggle, SegmentedControl,
842
+ Select, Modal and BottomSheet stories were looked at once in Storybook in dark
843
+ and light at phone and desktop widths. No browser suite was run (the library's
844
+ Playwright specs will need updating for the new looks), there was no
845
+ accessibility or QA review, and nothing was tried on a real phone, in Safari or
846
+ Firefox, at 200% text or with a keyboard.
847
+
848
+ ### Added
849
+
850
+ - **`AppShell flushTop`** (Z-065; D140): without a `header`, the content no longer gets the top safe-area padding, so a colour block at the top of a tab screen runs under the status bar and the content handles the safe area itself (`--app-shell-top-inset` keeps its value; an `AppBar position="static"` in the block pads by it). Default `false`; with a `header` it does nothing.
851
+ - **`AppBar tone="inherit"`** (Z-067; D140): a bar inside a block that is not the primary colour. No fill of its own, never the glass or the hairline, and the title, back control, ghost actions, links, borders and focus rings take the text colour the block gives (the `on-brand` scope with `--color-on-brand: currentColor`). A sticky bar that must cover scrolling content takes its fill from the app: `class="[--color-bar:var(--your-block-colour)]"`. Contrast inside the block is the app's to check. Story: "Design System/Organisms/AppShell (9.0) / Colour Block Under Status Bar".
852
+
853
+ ### Changed
854
+
855
+ - **Dark surfaces are steps of the neutral ramp** (Z-044; D133, D134). `--color-surface-raised` is `var(--zabi-base-850)` (`#1f1f22`, was a 6.4% white wash, `#262629`), `--color-surface-elevated` and `--color-surface-overlay` are both `var(--zabi-base-800)` (`#27272a`; were `#363638` and `#454547`), `--color-surface-overlay-hover` is `var(--zabi-base-750)` and `--color-surface-inset` (and so `--color-input`) is the page step, `var(--zabi-base-900)`. The page does not move. Two tone steps in all (OKLCH L 21.0, 24.1, 27.4), and with a tinted neutral every surface keeps the ramp's hue and chroma instead of greying as it rises. An overlay is no longer a lighter grey than a card: it is set apart by its rim (`--color-material-rim`, 12% to 14% of `--zabi-base-50`: 1.85:1 on the page, 1.72:1 on a card, 1.56:1 on the overlay) and a stronger dark shadow (`--shadow-opacity` 0.24 to 0.5, `--shadow-opacity-contact` 0.3 to 0.4). What follows the ladder in dark: `--color-border` and `--color-border-weak` are `base-250` (`#333338`: 1.41:1 on the page, 1.31:1 on a card; a quieter hairline than before), `--color-progress-track` is `base-250` (the field is the page colour now), `--color-input-disabled` is `base-75`, `--color-segment-thumb` is `base-400`, `--color-card-active` and `--color-surface-4` are the overlay hover step. Light is unchanged. Restore the 8.1 dark ladder: `overrides: { dark: { '--color-surface-raised': 'color-mix(in srgb, var(--zabi-base-50) 6.4%, var(--zabi-base-900))', '--color-surface-elevated': 'color-mix(in srgb, var(--zabi-base-50) 13.1%, var(--zabi-base-900))', '--color-surface-overlay': 'color-mix(in srgb, var(--zabi-base-50) 19.7%, var(--zabi-base-900))', '--color-surface-inset': 'var(--color-base-150)', '--color-border': 'var(--color-base-200)', '--shadow-opacity': '0.24', '--shadow-opacity-contact': '0.3' } }`.
856
+ - **The primary action keeps the brand hue in dark** (Z-043; D135). The dark primary was the mirrored pale step (`--color-brand-600`, physical step 400) with a dark label; it is now the same physical steps as light, `--color-action-primary: var(--zabi-brand-600)`, `-hover: var(--zabi-brand-700)`, `-active: var(--zabi-brand-800)`, with the light label: `--zabi-on-brand-dark` defaults to `var(--zabi-on-brand)` (white; was `var(--zabi-brand-950)`). White on step 600 is 4.76:1 or more on any ramp; the default fill is 3.65:1 on the dark page, 3.39:1 on a card and 3.07:1 on an overlay. Everything that reads the primary follows: checked Checkbox, Radio and Toggle, the Progress fill, Stepper, the solid brand Badge, the FAB, the brand AppBar. Danger does the same (`--color-action-danger: var(--zabi-error-600)`, 700, 800, label `var(--zabi-error-50)`), so a destructive button is one red in both modes. The focus ring, links, the brand tints, the tonal button and the active navigation fills stay on the mirrored ramp: they are text, rings and tints on a dark surface. With the fill a mid step under a white label, the dark control veil is darken-only as in light (`--gradient-control-top-strength: 0%`, `-bottom-strength: 14%`; were 22% and 6%); the accent keeps 22% and 6% in its own strengths, because its dark fill is still the light step under a dark label. `--color-progress-track` in dark is `base-75` (`#101013`, a well just below the page) so the mid-step fill holds 3:1 on it. Restore 8.1 in one option: `createTheme({ darkPrimary: 'mirror' })` (CLI `--dark-primary mirror`); without the generator, set the three fills to `var(--color-brand-600 | 700 | 800)`, `--zabi-on-brand-dark: var(--zabi-brand-950)` and the two strengths under `.dark`.
857
+ - **`createTheme({ darkPrimary })`** (Z-043): `'brand'` (default, what the library does, nothing extra written) or `'mirror'` (the 8.1 dark fills for primary and danger, the label chosen by the contrast check, the 22% / 6% veil, written under the dark selectors). A `dark` or `both` override of a role wins over both. In dark the label tried first is now white (the ramp's dark end if white fails, so a pale dark primary of the app's own keeps a dark label). With `pin`, a colour that fails in dark falls back to what this option gives, which is step 600 with white by default. An app that overrides `--color-action-primary` for both modes with a semantic step keeps its mirrored dark primary; move the override under `light` to get the brand step in dark.
858
+ - **Four more guarded pairs**: the primary fill and the danger fill against the page and against a card, 3:1, both modes (until now only a pinned colour was held to this).
859
+ - **Dark status tints are low-chroma tints of the card** (Z-057; D136). `--color-success-subtle`, `--color-warning-subtle`, `--color-error-subtle`, `--color-info-subtle`, `--color-energetic-subtle` and `--color-accent-subtle` were the mirrored step 100 (physical 900: the dark error surface was `#660002`); each is now `color-mix(in srgb, var(--zabi-<ramp>-500) 16%, var(--color-surface-raised))`, and the matching `-border` the same mix at 36%. `--color-action-danger-subtle` is the error tint, its hover the mix at 20% and its pressed step at 32%. The hue stays in the icon and the text, which did not move. Light is unchanged. Restore 8.1 per family: `overrides: { dark: { '--color-error-subtle': 'var(--color-error-100)', '--color-error-border': 'var(--color-error-200)' } }`.
860
+ - **Alert's status icon is drawn at stroke 2.5** (was lucide's 2), in both modes, so it holds its weight beside the text.
861
+ - **Dark materials show glass again** (D137). Dark sheets, menus and modals were 98% opaque, because faint text had to hold 4.5:1 over a white backdrop. The dark backdrop filter now also dims what is behind the glass: new token `--material-backdrop-brightness` (`1` in light, an identity; `0.6` in dark), and the three `--material-filter-*` tokens end in `brightness(var(--material-backdrop-brightness))`. The brightest thing behind dark glass is therefore near-white times 0.6, and the contrast guard measures the fill over that dimmed backdrop (material pairs only; every fallback that removes the filter also makes the fill opaque, so the rule is what a browser paints). Dark alphas re-solved on it: `--material-alpha-thin` 66% to 60%, `--material-alpha-regular` (bars) 76% to 68%, `--material-alpha-thick` 98% to 72% (floors 30, 47 and 68). No text role moved and the thick material still carries all five. Restore the 8.1-like opaque dark overlays: `overrides: { dark: { '--material-alpha-thick': '98%', '--material-backdrop-brightness': '1' } }`, or `data-materials="opaque"`.
862
+ - **`createTheme` now checks the material pairs** (D138). The generator's data did not carry `--material-alpha-*`, so every "text on a material" pair was skipped without a word: a theme reported "all 278 role pairs pass" and the same input now checks 354. An app whose text or surface overrides make glass unreadable over the worst backdrop gets a warning it did not get before; the fix is the alpha of that level for that mode, for example `overrides: { light: { '--material-alpha-thin': '68%' } }`. `--material-alpha-*` and `--material-backdrop-brightness` are honoured per mode in the check.
863
+ - **Light `--material-alpha-thick` is 88%** (was 86%), found by that check: with a strongly tinted neutral (`neutralChroma: 0.08`) the caption on a light sheet over a black backdrop was 4.47:1 at 86%. Neutral hues at chroma 0 to 0.1 now pass. Restore: `overrides: { light: { '--material-alpha-thick': '86%' } }`.
864
+ - **The dark half of the surface elevation guard is a new rule** (D134), because the old one (four levels, each 5 to 8 lightness points above the last) described the old design. Now: every dark surface role is declared as one step of the neutral ramp, not a mix; lightness never falls from page to overlay and the card is lighter than the page; at most two tone steps above the page, each 2 to 5 OKLCH L points, the top within 8; the overlay rim reaches 1.5:1 against the page and the card and 1.3:1 against the overlay; the card edge reaches 1.2:1 against the page and the card, as in light. `tokens/surface-ladder.js` exports `SURFACE_STEP` in place of the wash alphas.
865
+ - **The field edge is guarded on the elevated and overlay surfaces too**, in both modes (`--color-input-border` at 3:1; a field in a sheet or a modal). Two more guarded pairs in `createTheme`'s check.
866
+
867
+ ### Fixed
868
+
869
+ - **`cn` knows the named spacing steps** (Z-064): `xs`, `sm`, `md`, `lg`, `xl` and `2xl` (`--spacing-*`) are registered with the class merger as spacing values, so a call-site `p-sm` beats a component's `p-xs`, and the same for margin, gap, inset and the other spacing groups. Until now `p-xs p-sm` kept both classes and the stylesheet order picked the padding. `3xl` is not registered: the library has no `--spacing-3xl`.
870
+ - **`createTheme`: an ink role overridden in one mode keeps the ramp's ink in the other** (Z-061). With `neutralChroma`, a role such as `--color-action-secondary` set under `overrides.light` alone got the library's grey `rgba(250, 250, 250, 0.09)` under the dark selectors; it now gets the tinted `color-mix(in srgb, var(--zabi-base-50) 9%, transparent)` that the option derives, as it does without the override. The same holds the other way: a role set under `overrides.dark` alone keeps the tinted mix in light (it fell back to the library's grey there). Roles: `--color-action-secondary` (and `-hover`, `-active`), `--color-surface-hover`, `--color-surface-active`, `--color-border-overlay`, `--shadow-color`. An app that restated the dark mixes as a workaround can delete them; to keep the old grey in the other mode, set it there.
871
+
872
+ ## [9.0.0-alpha.3] - 2026-10-06
873
+
874
+ A third local preview build of the 9.0 visual direction, not published to npm;
875
+ 8.1.0 stays the stable version. It is the redesign of the shell: AppBar,
876
+ BottomTabBar, StickyActionBar and TopNavbar are flush with the page and turn to
877
+ glass once content scrolls under them; the AppShell footer lies over the
878
+ scroller; the active tab loses its outline; the sidebar and the top navbar have
879
+ one active indicator, sentence-case headings and tighter rows; AppBar gains a
880
+ large title that condenses and a brand tone; IconButton gains a count; and
881
+ AppShell gains a `navigation` slot that is tabs, a rail or a sidebar by
882
+ breakpoint, with `AppNavigation` as its ready-made content. No token, prop,
883
+ variant or export is removed or renamed; several defaults change what a consumer
884
+ sees, and each has a line that restores 8.1 where one exists. Built quickly and
885
+ tested lightly on purpose: the type and design checks, the unit run, the theme
886
+ tests, the package build and a scratch install were run on it, and the new
887
+ shell stories were looked at once in Storybook (light and dark, at phone, tablet
888
+ and desktop widths, at rest and scrolled). No browser suite was run (the
889
+ library's Playwright specs will need updating for the new looks), there was no
890
+ accessibility or QA review, and nothing was tried on a real phone, in Safari or
891
+ Firefox, at 200% text or with a keyboard. Dark mode is still not art-directed.
892
+
893
+ ### Added
894
+
895
+ - **Bar tokens** (Z-045; D124): `--color-bar` (a bar's fill at rest, default `var(--color-surface-chrome)`), `--color-bar-border` (the hairline a bar shows once content is under it: the headline colour at 10%), `--color-bar-brand` (the fill of an on-brand AppBar, default `var(--color-action-primary)`) and `--color-tabbar-active` (the active tab's pill, default `var(--color-nav-menu-active)`; Z-046). All four are restated in dark and settable through `createTheme` `overrides` per mode. Classes `bg-tabbar-active`, `bg-bar-brand`, `border-bar`. Guarded pairs: the active tab label on its pill, on-brand text on the brand bar, the headline on a bar at rest.
896
+ - **`.material-bar`**: the surface of a bar. A `::before` layer paints `--color-bar` at rest and, when the element has `data-scrolled-under="true"`, the regular material with a 1px `--color-bar-border` hairline on the content side (`data-bar-edge="bottom"` puts it on the top edge). The element itself has no `backdrop-filter`, so a Dropdown or Tooltip in a bar is still placed against the screen. Under the opaque fallbacks a scrolled bar is the page colour with the hairline.
897
+ - **`.material-layer-regular`**: the layer form of the regular material, as `.material-layer-thick` is for the thick one, for a floating bar that holds controls.
898
+
899
+ - **`BottomTabBarItem.activeIcon`** (Z-046): drawn instead of `icon` on the active tab, typically the filled form. Without it the same icon is drawn with a heavier stroke (2.5) when active; `BottomTabBarIcon` now also accepts `strokeWidth`.
900
+ - **`BottomTabBar floating`** (D126): the bar as a rounded glass capsule inset 12px from the screen edges, always on the regular material. Default `false`.
901
+ - **AppShell `navigation` slot** (D101-S, D130): `navigation?: Snippet<[{ placement: "tabs" | "rail" | "sidebar" }]>` and `navigationPlacement?: "auto" | "tabs" | "rail" | "sidebar"` (default `"auto"`). One navigation, rendered once and first in the DOM, placed by CSS: tabs over the bottom of the screen below 48rem (768px), a rail from 48rem and a sidebar from 64rem (1024px) as a full-height column at the inline start; a forced value applies at every width. The server render is right without scripts. The host carries `data-navigation-placement`; `--app-shell-bottom-inset` counts the tabs (and a `footer`, which then sits directly above them); new `--app-shell-start-inset` is the rail's or sidebar's width (`0px` as tabs), mirrored on `<html>` with the other two. `footer`, `header` and `children` work as before, and a shell without `navigation` renders the markup it always did. Types `AppShellNavigationPlacement`, `AppShellNavigationMode`, `AppShellNavigationContext` and the constants `APP_SHELL_RAIL_MIN_WIDTH`, `APP_SHELL_SIDEBAR_MIN_WIDTH`.
902
+ - **`AppNavigation`** (new organism; D130): the ready-made content for the slot. Takes the `BottomTabBar` item list (`items`, `active`, `label`, `badgeLabel`, `badgeMax`, `floating`) and draws it as a BottomTabBar, an 80px rail (icon in the tab pill over a label) or a 256px sidebar (36px rows, icon before the label, count at the end, one tinted active indicator), by CSS alone; `header` and `footer` snippets go above and below the list in the side forms. Outside a shell it is the tab bar.
903
+ - **Stories**: `Design System/Organisms/AppShell (9.0)` shows the redesigned shell as a phone, a phone with floating tabs, a tablet rail, a desktop sidebar, adaptive, and with a brand bar. The prototype story stays beside it.
904
+ - **`AppBar largeTitle`** (Z-045; D128): the title large (30px bold, capped at 39px, two lines at most) on a second row under the 56px bar. The large row scrolls away with the content, with no scroll-linked animation; the bar stays and shows the title small once the large one has gone under it. The heading element is the large title; the small one is hidden from assistive technology. In an AppShell, `--app-shell-top-inset` is the condensed height. `collapseOnScroll` is ignored with it (a dev build warns). Before the bar is measured, and without scripts, the whole header sticks with the large title in view.
905
+ - **`AppBar tone`** (Z-045; D129): `"default"`, `"transparent"` (no fill at rest, glass once content is under it: for a canvas, an image or a colour block behind the bar) or `"brand"` (`--color-bar-brand` with on-brand text and controls, opaque at rest and scrolled, no hairline, so a brand block directly under the bar joins it). The header carries `data-tone`.
906
+ - **`AppBar position`** (Z-067): `"sticky"` (default) or `"static"` for a bar in the flow of the page, inside a block of the content: it scrolls away with the block and is never glass. `class="static"`, the 8.1 way to say this, is read as `position="static"` (the bar is `relative`, because the material's layer is placed against it).
907
+ - **`IconButton count`, `countLabel`, `countMax`** (Z-056 part 1; D131): a count in the attention colour drawn inside the button's own box, in its upper end corner, so a bar or an `overflow` ancestor cannot cut it. `99+` above `countMax` (default 99); a dot at `xs` and `sm`; nothing for 0 or while loading. The accessible name is the `label`, a comma and `countLabel(count)` (default "3 new").
908
+ - **`scrollEdge`** on AppBar, BottomTabBar, StickyActionBar and TopNavbar (D123): `"auto"` (default: flush at rest, glass and a hairline once content is under the bar), `"always"` or `"never"`. Each bar writes `data-scrolled-under="true|false"` on its root (`"false"` on the server).
909
+ - **`AppShell canvas`** (D125): paints the shell with `bg-canvas` (the page colour with the brand wash) and lets the header bar rest transparent over it. Default `false`.
910
+ - **AppShell scroll state**: `data-scrolled-top` and `data-scrolled-bottom` on the host, and `--app-shell-footer-overlay` on the scroller (the bottom inset when a footer lies over it, else `0px`). `getAppShell()` in `util/app-shell` gives a component inside a shell the same state; `util/scroll-edge` (`watchScrollEdge`) is the helper for a bar on its own.
911
+
912
+ ### Changed (visible in 9.0)
913
+
914
+ - **Bars are flush with the page and turn to glass when content scrolls under them** (Z-045; D108, D123, D124). AppBar, BottomTabBar, StickyActionBar and TopNavbar are the page colour with no line at rest; once content is under one it takes the regular material and a 1px hairline on the content side. They no longer have a border of their own, so AppBar is 56px (was 57) and BottomTabBar 64px (was 65); an app that hard-coded 57px or 65px should read `--app-shell-top-inset` / `--app-shell-bottom-inset` or use the new numbers. Restore 8.1's look: `scrollEdge="always"` on the bar together with `data-materials="opaque"` on `<html>` and `--color-surface-chrome: var(--color-surface-raised)` (a permanent opaque raised bar with its line).
915
+ - **BottomTabBar: the active tab has no outline** (Z-046; D107, D126). It is marked by the tinted pill (`--color-tabbar-active`), a filled or heavier icon, a semibold label and the active colour; the focus ring shows on `:focus-visible` only. Under forced colours the pill keeps a system-colour outline. The count badge is the attention colour (the solid error fill) with a 2px ring in the bar colour. Restore 8.1: none by token (the outline was the reported defect); `--color-tabbar-active` takes any fill.
916
+ - **Sidebar and TopNavbar restyled** (D127). SidebarNavigation: one active indicator, the tinted fill with the active text colour and a semibold label (the 3px leading bar is gone; forced colours keep an outline); rows are 36px at 14px with 20px icons and 44px on coarse pointers; sections are separated by space, not lines. SidebarNavSection headings are sentence case as written (no `uppercase`, no tracking), 12px medium. SidebarShell reads `--color-surface-chrome` (the page colour) with a weak hairline on the content side and no shadow in the `card` layout. SidebarPanel is a flat card (no shadow or ring; `variant="elevated"` now looks the same as the default) and its selected row uses the same single indicator. TopNavbar links are 14px without tracking, medium, semibold when active. Restore 8.1: none by token or prop.
917
+ - **A `bg-*` or `border-*` class on a bar no longer changes its surface.** The fill and the hairline are painted on the material's layer, not on the bar element, so `class="bg-transparent border-b-0"` on an AppBar (or a `bg-*` on BottomTabBar, StickyActionBar, TopNavbar) has no visible effect. Use `tone="transparent"` or `tone="brand"` on AppBar, or set `--color-bar` on the bar (`class="[--color-bar:transparent]"`, any colour) and `scrollEdge="never"` for no glass and no line.
918
+ - **The AppShell footer lies over the scroller** (D125) instead of taking a row of its own, so content passes under the tab bar. `--app-shell-bottom-inset` has the same meaning and value; the shell now gives the content that much bottom padding and the scroller that much `scroll-padding-bottom`. A floating button at `bottom: calc(var(--app-shell-bottom-inset) + 1rem)` sits where it did. Content that sized itself to the scroller's height (`min-h-full`) is now as tall as the screen minus the header only; its end is still above the footer because of the padding. No restore short of reverting.
919
+ - **StickyActionBar and UnsavedChangesBar in a shell with a footer** stick above the footer (they read `--app-shell-footer-overlay`). **UnsavedChangesBar** is the regular material (rim, highlight, soft shadow) instead of an opaque card with a border and `shadow-lg`; `data-materials="opaque"` makes it opaque.
920
+ - **`--color-surface-chrome` is the page colour** (`var(--color-surface-base)`) in both modes, not the raised surface (D108), so bars and sidebars that read it are flush with the page. The regular material is mixed from it and was re-solved: light 84% (was 80%), dark 76% (unchanged); thin stays 58% / 66%. Restore 8.1: `--color-surface-chrome: var(--color-surface-raised)` (per mode in `overrides`).
921
+
922
+ ## [9.0.0-alpha.2] - 2026-10-06
923
+
924
+ A second local preview build of the 9.0 visual direction, not published to npm;
925
+ 8.1.0 stays the stable version. It adds the first restyled components on top of
926
+ alpha.1's token layer: List and Badge without their double outlines, a sliding
927
+ indicator in Tabs and SegmentedControl, gradient solid buttons with new `tonal`
928
+ and `text` variants and a 56px size, a lighter off Toggle, the seven overlays
929
+ on the thick material and a spring for BottomSheet. Bars, the shell and dark
930
+ mode are not touched yet. No token, prop, variant or export is removed or
931
+ renamed; several defaults change what a consumer sees, and each has a line that
932
+ restores 8.1 where one exists. Built quickly and tested lightly on purpose: the
933
+ type and design checks, the unit run, the theme tests, the package build and a
934
+ scratch install were run on it. NOTHING was rendered: no browser suite, no
935
+ Storybook or visual check in either mode, no accessibility or QA review, so the
936
+ glass, the sliding indicators and the springs have never been seen. Using it is
937
+ the test.
938
+
939
+ ### Added
940
+
941
+ - **Button `variant="tonal"`** (Z-047): the tonal fill with its brand label, flat, for secondary actions and chosen answers. IconButton has it too.
942
+ - **Button `variant="text"`** (Z-048): a standalone text action in the link colour (the on-colour inside `.on-brand` and `.on-accent`): no underline, a semibold label, the box and 44px touch target of the other variants, `px-2`, a hover and pressed tint. With `href` it is still a boxed action; `variant="link"` with `href` stays the underlined link for a sentence. IconButton shows it as its link look.
943
+ - **Button `size="xl"`** (Z-047): 56px. Button only; `SizeVariant` is unchanged. New type `ButtonSize` (`SizeVariant | "xl"`), exported with `ButtonVariant`. `scripts/check-control-geometry.js` holds it to the same one-line rule as the other sizes.
944
+ - **`--zabi-button-radius` and `--zabi-button-font-weight`** (Z-047) are now read by Button (and the inline link), through `rounded-button`, `font-button` and `font-button-strong`. Defaults are today's corner and a 500 label (600 for `text`).
945
+ - **Control gradient, darken-only in light** (design review: "controls are flat"). `--gradient-control-top-strength` is 0% in light (the fill at the top is the control's own colour, so the label keeps its full contrast and an app's brand colour is not altered) and `--gradient-control-bottom-strength` 14% of `--zabi-base-950` (was 4% / 6%); dark stays 22% / 6%. The veil is for primary, accent and danger: `bg-control-gradient` serves danger too, and danger's label is now a guarded gradient pair. New `--gradient-control-accent-top-strength` and `--gradient-control-accent-bottom-strength` (default: the shared strengths) so one control can differ. The highlight now uses Tailwind's inset-shadow variable (`--tw-inset-shadow`) instead of `--tw-shadow`, so a `shadow-*` class on the same element draws as well. `createTheme` flips a veil to lighten-only in a mode where a dark label would fail the darkened bottom (a light accent), writes 0% if no strength passes, and never moves a strength you set. Restore: `--gradient-control-top-strength: 4%; --gradient-control-bottom-strength: 6%` in light.
946
+ - **Tonal action role** (Z-047): `--color-action-tonal`, `--color-action-tonal-hover`, `--color-action-tonal-active`, `--color-action-tonal-text`; classes `bg-action-tonal` (with its hover and pressed states), `text-action-tonal`. Light is the brand-100 tint with a brand-800 label (8.15, 6.94 and 6.25:1 on rest, hover and pressed); dark the deep tint with the light label (9.48, 6.94, 5.86). `.bg-action-tonal` is a surface inside `.on-brand` and `.on-accent`.
947
+ - **Outline edge, guarded at 3:1** (Z-047): `--color-action-outline-border` and `--color-action-outline-border-hover` (the field edge, 4.63:1 on the light page, 4.83:1 on a card; dark 3.67 and 3.12), classes `border-action-outline` and `hover:border-action-outline-hover`; the block's label colour inside `.on-brand` and `.on-accent`. Restore the old edge: `--color-action-outline-border: var(--color-border); --color-action-outline-border-hover: var(--color-border-medium)`.
948
+ - **Button corner and label weight, apart from fields** (Z-047): `rounded-button` (`--zabi-button-radius`, falling back to `--radius-control`), `font-button` (`--zabi-button-font-weight`, falling back to 500) and `font-button-strong` (600, for the text variant). Declared nowhere, so set them on any ancestor or in `createTheme` `overrides`; a scoped `--radius-control` still flows. Defaults keep today's look.
949
+ - **Toggle off track** that does not read as disabled: `--color-toggle-track` (light base-200, dark base-300), `-hover`, `-active`, and the edge `--color-toggle-track-border` (the field edge: 4.63:1 on the light page, 4.83:1 on a card, dark 3.67 and 3.12). Classes `bg-toggle-track` (with hover and pressed) and `ring-toggle-track` (an inset 1px edge that does not change the switch's size). `--color-control-track*` is untouched; Slider and Progress look as before. Restore: `--color-toggle-track: var(--color-control-track); --color-toggle-track-hover: var(--color-control-track-hover); --color-toggle-track-active: var(--color-control-track-active); --color-toggle-track-border: transparent`.
950
+ - **`SegmentedControl tone`** (Z-052): `"neutral"` (default) or `"primary"`. Neutral draws the selected segment as a thumb: card colour, headline-coloured label, `--shadow-sm` and a 1px edge, on the tinted track. New tokens `--color-segment-thumb` (light: the raised surface; dark: the overlay surface), `--color-segment-thumb-text` and `--color-segment-thumb-border` (the control boundary, held to 3:1 against the track over the page and over a card, with the thumb text held to 4.5:1 on the thumb).
951
+ - **A sliding indicator in Tabs and SegmentedControl.** After mount the selected item's mark (the underline, the pill, the thumb or the primary fill) is one element that moves and resizes to the selected item over `--duration-moderate`: with `--ease-spring` for the pill and the thumb, `--ease-standard` for the underline. It is measured, so it follows resizes, zoomed text (a wrapped SegmentedControl moves the thumb in two dimensions), right-to-left layouts and a scrolling tablist. Server-rendered, before hydration, without ResizeObserver or layout, and under forced colours the selected item draws its own mark as before. No motion under reduced motion.
952
+ - **`Badge variant="brand"`** (Z-056 part 3): the primary brand colour as a badge, subtle (brand tint, link-coloured text) and solid (the primary fill with its label). New token `--color-action-primary-border` (the brand tint's edge), and the guarded pairs `badge subtle · brand` and `badge solid · brand`.
953
+ - **`Badge bordered`** (Z-056 part 2): puts a subtle badge's family edge back. Default `false`.
954
+ - **`--zabi-list-row-border-color`** (default `transparent`) and **`--zabi-list-divider-color`** (default `var(--color-border)`): set on a list or any ancestor.
955
+ - **`.material-layer-thick`** (package 10): the thick material in a form for a container that holds fixed-position content. The fill, backdrop filter, highlight and rim are painted on a `::before` layer behind the content (isolated, `z-index: -1`, same tokens as `.material-thick`, so every fallback applies); the element itself carries only the outer shadow, so it is not the containing block of its `position: fixed` descendants. The element must be positioned and must not scroll its own content. Use it for any panel that can hold a popover; use `.material-thick` for a leaf.
956
+ - **`Tooltip tone`**: `"material"` (default) or `"inverted"`. Typed in `TooltipProps`, documented in the catalog and the story.
957
+ - **Guarded pairs on the thick material**: a Toast's status text (`--color-success-text`, `--color-error-text`, `--color-warning-text`, 4.5:1), the status colours as icons (3:1) and `--color-link` (4.5:1), each over the worst backdrop and over the page, in both modes.
958
+
959
+ ### Changed (visible in 9.0)
960
+
961
+ - **A soft gradient on primary, accent and danger Buttons and IconButtons, the FloatingActionButton and the on Toggle** (design review: "controls are flat"): a darker bottom and a 1px top highlight over the control's own fill. None when disabled, loading, or a toggled-on IconButton. Restore 8.1: `--gradient-control-bottom-strength: 0%; --shadow-control-highlight: none` (and `--gradient-control-accent-bottom-strength: 0%` if the accent strength was set apart).
962
+ - **The outline Button and IconButton have a 3:1 edge** (Z-047), not the 1.25:1 card edge. Restore 8.1: `--color-action-outline-border: var(--color-border); --color-action-outline-border-hover: var(--color-border-medium)`.
963
+ - **The off Toggle is a light tint with a 1px edge**, no longer the mid-grey that read as disabled; its colour change and the thumb's slide use the motion tokens (`--duration-moderate`, the thumb on the spring easing). Restore 8.1: `--color-toggle-track: var(--color-control-track); --color-toggle-track-hover: var(--color-control-track-hover); --color-toggle-track-active: var(--color-control-track-active); --color-toggle-track-border: transparent`.
964
+ - Button, IconButton and the FloatingActionButton change colour over `--duration-base` instead of a literal 150ms, so reduced motion zeroes it.
965
+ - **`createTheme`: links follow an overridden primary** (Z-048). An app that sets `--color-action-primary` in `overrides` (and not `--color-link`) gets `--color-link` equal to it, and `--color-link-hover` equal to the primary hover if it set one, in each mode the override applies to, as long as no guarded pair fails that did not before; otherwise the ramp's link stays in that mode. An explicit `--color-link` always wins. The library default stays `--color-link: var(--color-brand-700)`. The generator also resolves the control veil pairs now (it skipped them before). Restore: `overrides: { '--color-link': 'var(--color-brand-700)', '--color-link-hover': 'var(--color-brand-800)' }`.
966
+ - **SegmentedControl: the selected segment is a neutral thumb, not a solid primary fill** (Z-052), so the control is no longer louder than the screen's main button. Restore 8.1: `tone="primary"`.
967
+ - **The indicators of Tabs and SegmentedControl slide** between items instead of switching. Restore 8.1: `--duration-moderate: 0s` on the component or an ancestor makes the move instant (the token is shared with other components); reduced motion already has none.
968
+ - **List rows have no border of their own; a hairline divides them.** A row inside the bordered `.list-group` shell (or a bare `<List>`) drew a second outline; now rows keep a transparent 1px border (so height and text do not move) and a 1px `--color-border` divider sits in the gap between rows, none above the first or below the last, hidden next to a selected row (which keeps its fill and `border-action-primary` edge). The `.list-group` shell keeps its one border. Row transitions use the motion tokens. Restore 8.1: `--zabi-list-row-border-color: var(--color-border); --zabi-list-divider-color: transparent;` on the list or any ancestor.
969
+ - **A subtle Badge has no visible border.** The 1px border box stays (same height), drawn transparent. Restore 8.1: `bordered` on the Badge. Solid badges are unchanged.
970
+ - **BottomSheet arrives on a spring.** The slide in, the move between snap points and the settle after a drag use `--ease-spring` over `--duration-slow` (300ms); they were a literal 200ms ease-out. The slide in reads both tokens from the sheet, so a theme that sets them is followed, and falls back to `ease-out` where the tokens cannot be read or the engine rejects a `linear()` easing. A 2rem filler under the panel's bottom edge keeps the overshoot from showing the page. Reduced motion is unchanged (no slide, no transition), and so are drag, snap logic, focus and scroll lock. Restore 8.1: on the sheet or an ancestor, `--ease-spring: var(--ease-out); --duration-slow: 200ms` (both tokens are shared with other components), or `class="duration-200 ease-out"` on the `BottomSheet`.
971
+ - **Overlays are frosted glass** (package 10): BottomSheet, Drawer, Modal, the Dropdown menu, the Select list, the Tooltip bubble and every Toast (and Toaster toast) take the thick material instead of an opaque overlay fill with a border and `shadow-lg`; the material brings its own rim and shadow. Dividers inside a header or footer, row hover and selected fills, and the scrims behind Modal, Drawer and BottomSheet are unchanged. Dark thick is 98% opaque by design for now, so a dark overlay shows almost no glass. In Modal, Drawer and BottomSheet the glass is a layer behind the content: the Drawer panel lost its edge border and the BottomSheet its top and side borders (the rim replaces them), and a Modal's panel no longer scrolls itself: its Card does (`overflow-y-auto` moved from the panel to the Card, `min-h-0` added), and the Card takes the panel's full corner radius. A fixed-position popover inside any of them is placed against the viewport exactly as before, with materials on or off. Restore 8.1: `data-materials="opaque"` on the root or any ancestor (a portaled overlay lives under `body`, so put it on `html` or `body`) gives opaque overlay surfaces. The 8.1 `shadow-lg` and 1px border look itself is not restorable with one token.
972
+ - **The Tooltip bubble is glass with body text and no arrow by default.** The arrow is a second shape on a translucent fill and would show a seam. Restore 8.1: `tone="inverted"` (the dark bubble with its arrow, still reading `--color-tooltip-bg` and `--color-tooltip-fg`).
973
+ - **Toast text uses the status `-text` colour** (`text-success-text`, `text-error-text`, `text-warning-text`) instead of the plain status colour, which is 3.7:1 to 3.9:1 on glass; the border stays the plain colour. The neutral toast draws no second border. A Dropdown danger item uses `text-error-text` for the same reason. Restore 8.1: none by token; the colours are slightly darker in light and lighter in dark.
974
+ - **Overlay motion reads the tokens**: Dropdown fades over `--duration-moderate` with `--ease-standard`, the Modal's slide over `--duration-slow` with `--ease-out`, the Drawer reads `--duration-moderate` and `--ease-out` for its slide, the Select over `--duration-base` and `--duration-moderate`, the Tooltip over `--duration-moderate`. Reduced motion zeroes them.
975
+
976
+ ### Fixed
977
+
978
+ - **Z-040: the pressed field follows the field.** `--color-input-active` (the
979
+ pressed Select trigger) is now derived from `--color-input`, so an app that
980
+ sets the light card colour through `createTheme` `overrides.light` no longer
981
+ trips "pressed field against the resting field". Light mixes 12% of the ink
982
+ (`--zabi-base-900`) into the field, dark 10% of the light end
983
+ (`--zabi-base-50`). On the default theme light moves from `#e4e4e7` to
984
+ `#e3e3e4` and dark from `#333338` to `#353538`. An app that overrode
985
+ `--color-input-active` by hand to get past the check can delete that override.
986
+ Restore: `--color-input-active: var(--color-base-200)` in light,
987
+ `var(--color-base-250)` in dark. The neutral chroma at the light end is not a
988
+ new option: set the page per mode (`overrides.light`, `--color-surface-base`),
989
+ see THEMING.md.
990
+
991
+ ## [9.0.0-alpha.1] - 2026-10-06
992
+
993
+ A local preview build of the 9.0 visual direction, not published to npm; 8.1.0
994
+ stays the stable version. It holds the token layer only (surfaces, status
995
+ colours, materials, gradients, motion, typeface): no component is restyled yet
996
+ except the default `Card` and the `ImageUpload` hover plate, and dark mode is
997
+ not yet art-directed. No token, prop or export is removed or renamed; values
998
+ move, and each visible change below has a line that restores 8.1. Built quickly
999
+ and tested lightly on purpose: the type and design checks, one unit run, the
1000
+ theme tests, the package build and a scratch install were run on it. NOTHING
1001
+ was rendered: no browser suite, no Storybook or visual check in either mode, no
1002
+ accessibility or QA review. Using it is the test.
1003
+
1004
+ ### Added
1005
+
1006
+ - **Motion tokens.** `--duration-fast|base|moderate|slow` (100, 150, 200, 300ms) and `--ease-standard` (`cubic-bezier(0.2, 0, 0, 1)`), `--ease-out` (Tailwind's own curve) and `--ease-spring` (a lightly damped `linear()` spring with one overshoot, with a cubic-bezier fallback where `linear()` is unsupported). Utilities: `ease-standard`, `ease-out`, `ease-spring`; durations are `duration-(--duration-moderate)` (Tailwind has no `duration-fast` style name for custom properties). Under `prefers-reduced-motion: reduce` the four durations become `0s` and the spring becomes the standard curve, in both modes and over an app's own values (`:root:root`, like the materials fallback), so everything that reads the tokens follows. All are settable through `createTheme` `overrides`.
1007
+ - **Materials (frosted glass) for what floats over content.** Tokens `--color-material-thin|regular|thick` (the fills: `--color-surface-chrome` at an alpha for thin and regular, `--color-surface-overlay` for thick), `--material-alpha-thin|regular|thick`, `--material-blur-thin|regular|thick`, `--material-saturate`, `--material-filter-*`, `--color-material-highlight`, `--color-material-rim`, `--shadow-material` and `--shadow-opacity-contact`, and the classes `material-thin`, `material-regular` and `material-thick` (fill, backdrop filter, edge and shadow; they compose with `rounded-*` and `focus-ring`). Highlight, rim and shadow derive from the neutral ramp, so `neutralChroma` tints them. No component uses them yet except the `ImageUpload` hover plate.
1008
+ - **Alpha floors in the contrast guard.** Each material is composited over the worst backdrop of the mode (`--zabi-base-950` behind light, `--zabi-base-50` behind dark) and over the page: thin must give its headline 4.5:1, regular its headline, body and label, thick all five text roles, and regular and thick the focus ring 3:1 (a thin control's ring is drawn outside it). Shipped alphas for thin, regular and thick: light 58, 80 and 86, dark 66, 76 and 98, each the smallest whole 2% that passes except light thin, whose floor is lower. Dark thick is close to opaque because captions must hold over a white backdrop; dark is art-directed in a later 9.0 change. `scripts/contrast-pairs.js` has a `behind` form that `createTheme` evaluates too, so an app's generated theme gets the same floor; every material token is settable through `overrides` (`light`, `dark`, `both`).
1009
+ - **Fallbacks and `data-materials="opaque"`.** The fills become the opaque surface they are made from, and the blur goes, under `prefers-reduced-transparency: reduce`, `prefers-contrast: more`, `forced-colors: active`, where `backdrop-filter` is unsupported, and where the root or any ancestor has `data-materials="opaque"`. Under forced colors the rim is a `CanvasText` outline. The fallbacks are in the theme files and in `zabi-components-colors.css`.
1010
+ - **Gradient tokens and three classes.** Canvas: `--gradient-canvas` (two soft radial washes from brand 300 and accent 300, a `background-image` only), its stops `--color-canvas-wash-brand` and `--color-canvas-wash-accent`, and `--gradient-canvas-strength` (light 55%, dark 25%); the class `bg-canvas` adds `--color-surface-base` under it. The library does not paint it on `body`. Controls: `--gradient-control` and `--gradient-control-accent`, a translucent veil over the control's own `background-color` (so hover and pressed fills still show), with the veil colours `--color-control-gradient-start|end` and `--color-control-gradient-accent-start|end`, strengths `--gradient-control-top-strength` and `--gradient-control-bottom-strength`, and `--shadow-control-highlight` (an inset 1px top edge); classes `bg-control-gradient` and `bg-control-gradient-accent` (the highlight goes through Tailwind's shadow variables, so `focus-ring` still draws). All follow a custom `brand` and `accent` and are settable through `createTheme` `overrides` (`light`, `dark`, `both`). No component uses them yet.
1011
+ - **Worst-case rule for gradients.** Contrast is never measured against a gradient. The guard holds the five text roles, `--color-link` (4.5:1), `--color-focus-ring` and `--color-input-border` (3:1) on each canvas wash stop over `--color-surface-base`, and the solid-button label on both veil ends over rest, hover and pressed fills (4.5:1). Solved strengths: canvas light 55 and dark 25 (the prototype used 85 and 45); top veil light 4 and dark 22. `createTheme` evaluates the same pairs.
1012
+ - `scripts/check-token-violations.js` fails `backdrop-blur-*` and `backdrop-filter` in `src/components`. The `ImageUpload` hover plate (`bg-surface-overlay/60 backdrop-blur-md`) is now `material-thin`.
1013
+
1014
+ ### Changed (visible in 9.0)
1015
+
1016
+ - **Default typeface is the platform's UI face.** `--font-family-sans` is `ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif` plus the emoji faces; `--font-family-heading` still follows it; `--font-family-mono` is `ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace` (it was Monaco, Menlo, Ubuntu Mono). The library and its Storybook no longer load Nunito Sans, so a default install downloads no font. The face now differs by platform (San Francisco, Segoe UI, Roboto) and text widths change, so truncation and wrapping can move. Restore 8.1: load Nunito Sans yourself and set `--font-family-sans: 'Nunito Sans', ui-sans-serif, system-ui, sans-serif` on `:root` after the imports. The docs site keeps Familjen Grotesk for its marketing headings.
1017
+ - **Default transition curve.** `transition`, `transition-colors`, `transition-opacity` and the rest, where a component sets no duration of its own, now use `--ease-standard` instead of Tailwind's `cubic-bezier(0.4, 0, 0.2, 1)`; the duration stays 150ms (`--duration-base`). Restore Tailwind's curve: `--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1)` on `:root` after the imports, or `createTheme({ …, overrides: { '--default-transition-timing-function': 'cubic-bezier(0.4, 0, 0.2, 1)' } })`.
1018
+ - **Light page and cards.** The light page is now near-white (`--color-surface-base` is `--zabi-base-50`, `#fafafa`; it was `--zabi-base-150`, `#ececee`), and `--color-page` and `--color-background` follow it. A default `Card` now has a 1px `--color-border` edge and no shadow (it used `shadow-sm`, and `hover:shadow-lg` when clickable); it is the same as `variant="outlined"`, which stays. `elevated` and `flat` are unchanged. A clickable card's hover fill is now `--color-card-hover` = base-150 (was base-100) and its pressed fill base-200, and `--color-input-disabled` is base-150 and `--color-background-secondary` base-100, so they still read against the lighter page. Dark does not change.
1019
+ - **New token `--color-surface-chrome`** (and the `bg-surface-chrome` utility) for bars and sidebars: `var(--color-surface-raised)` in both modes. The components do not read it yet. In `createTheme`, set it per mode with `overrides: { light: { '--color-surface-chrome': … }, dark: { … } }`.
1020
+ - **Restore 8.1.** Page: `createTheme({ …, overrides: { light: { '--color-surface-base': 'var(--zabi-base-150)' } } })`, or `--color-surface-base: var(--zabi-base-150)` in light CSS. Card: there is no `variant` that gives the old shadowed default; pass `class="border-transparent shadow-sm"` to approximate it.
1021
+ - **Status colours.** Every `-subtle` fill moves from ramp step 200 to step 100 and every `-border` from 300 to 200 in light (`success`, `warning`, `error`, `energetic`, `accent`, `info`; `--color-action-primary-subtle` and its hover and pressed steps follow, to 100, 200 and 300/200). Neutral's fill is `--color-base-200` (was 250) and its edge `--color-base-250` (was 300). Dark already used 100 and 200 and is only affected by the regenerated ramps below. The `warning`, `citron` (the default `accent` and `energetic`) and `pine` (`success`) ramps are regenerated on the same lightness curve with new hue and chroma, which is breaking by the repo's policy for every `--zabi-warning-*`, `--zabi-citron-*`, `--zabi-accent-*` and `--zabi-pine-*` step. Step 600: warning `#9e6000` to `#a05f00`, citron and accent `#707400` to `#5d7900`, pine `#00805e` to `#00804b`. Contrast ratios are unchanged to within rounding. An app that passes its own `accent` or `brand` to `createTheme` is unaffected, except that its dark mirror of `success`, `warning` and `energetic` follows the new ramps.
1022
+ - **Restore 8.1 status colours.** Subtle fills: set `--color-<family>-subtle: var(--color-<ramp>-200)` and `--color-<family>-border: var(--color-<ramp>-300)` (ramp `pine` for success, `iris` for info, `citron` for energetic) in light, and `--color-neutral-subtle: var(--color-base-250)`, `--color-neutral-border: var(--color-base-300)`. Ramps: pass the old colour to `createTheme` (`accent: '#707400'`) or set the old `--zabi-*` steps on `:root`; the 8.1 seeds were warning hue 82, shift -24, chroma 0.165; citron hue 112, chroma 0.135; pine hue 170, chroma 0.115.
1023
+ - **Softer, layered shadows.** `--shadow-sm|md|lg|xl` keep their names and are each a tight contact shadow plus a wider soft one. `--shadow-opacity` is now the strength of the wide layer: `0.10` in light (was `0.14`) and `0.24` in dark (was `0.3`); the new `--shadow-opacity-contact` is the contact layer, `0.06` light and `0.3` dark. Restore 8.1: in light `--shadow-opacity: 0.14`, `--shadow-sm: 0 1px 3px 0 rgb(var(--shadow-color) / var(--shadow-opacity)), 0 1px 2px -1px rgb(var(--shadow-color) / var(--shadow-opacity))`, `--shadow-md: 0 4px 6px -1px rgb(var(--shadow-color) / var(--shadow-opacity)), 0 2px 4px -2px rgb(var(--shadow-color) / var(--shadow-opacity))`, `--shadow-lg: 0 10px 15px -3px rgb(var(--shadow-color) / var(--shadow-opacity)), 0 4px 6px -4px rgb(var(--shadow-color) / var(--shadow-opacity))`, `--shadow-xl: 0 20px 25px -5px rgb(var(--shadow-color) / var(--shadow-opacity)), 0 8px 10px -6px rgb(var(--shadow-color) / var(--shadow-opacity))`; in dark the same with `--shadow-opacity: 0.3`.
1024
+ - The surface guard (`scripts/check-surface-elevation.js`) now checks the light surfaces by the card edge (`--color-border` at 1.2:1 or more on the page and the card) instead of a lightness ladder; the dark rules are unchanged.
1025
+
1026
+ ## [8.1.0] - 2026-10-06
1027
+
1028
+ 8.1.0 adds a theme generator (`zabi-theme`, `createTheme`) and an accent colour,
1029
+ a mobile set (AppShell, AppBar, BottomTabBar, BottomSheet, Drawer, Select as a
1030
+ sheet, safe areas, 44px touch targets) and more than 25 new components. Form fields,
1031
+ focus rings and light-mode surfaces are easier to see, and `strings` and
1032
+ `ZabiStringsProvider` replace every built-in English text. Nothing from 8.0.0
1033
+ was removed or renamed: every `exports` path, prop and token is still there.
1034
+ 8.0.1 was never published, so everything in the 8.0.1 section below (valid theme
1035
+ files, the three-line Tailwind setup working, ColorPicker on the server,
1036
+ Progress named) is part of 8.1.0.
1037
+
1038
+ ### Upgrade notes
1039
+
1040
+ Coming from 8.0.0, these are visible without a code change. Each bullet says
1041
+ what to do.
1042
+
1043
+ **Appearance**
1044
+
1045
+ - **Form field edges are darker.** `--color-input-border` is `base-500` in
1046
+ light and dark (was `base-350` light, `base-300` dark) and
1047
+ `--color-input-border-hover` is `base-550` in light. The edge is guarded at
1048
+ 3:1 against the field fill, the page and the card, in both themes. A theme
1049
+ of yours that overrides the edge can now report a pair below 3:1 in the
1050
+ generator check. The dark field edge on the overlay surface (sheets, modals)
1051
+ is about 2:1 against the panel and is not guarded. Nothing to do unless you
1052
+ override the edge.
1053
+ - **`Progress` uses new tokens.** The fill is `--color-progress-fill` (default
1054
+ `--color-action-primary`) and the track `--color-progress-track` (default
1055
+ `--color-input`); utilities `bg-progress-fill` and `bg-progress-track`. The
1056
+ fill used to be the hard-coded brand step 600, so a bar changes colour in an
1057
+ app that pins a primary action, and in dark. Fill on track is guarded at
1058
+ 3:1. Set the two tokens to keep the old colour.
1059
+ - **Light mode has a visible surface ladder, stronger tints and shadows.** See
1060
+ Changed: the page, cards, tinted fills, hover fills, overlay edge and shadow
1061
+ all move. A card is 1.18:1 on the page (was 1.10). Check screenshots; no
1062
+ token was renamed.
1063
+ - **Focus rings are one step darker in light and lighter in dark.**
1064
+ `--color-focus` is `brand-600` in both themes (light was `brand-500` at
1065
+ 2.99:1 on the page; dark was `brand-500`, 2.91:1 on the overlay; dark is
1066
+ #92a9ff by default). The muted ring in dark is one step lighter
1067
+ (`--color-focus-ring-muted`, `base-600`).
1068
+ - **The computed `box-shadow` of a focused element lists five shadows**, not
1069
+ two, because the ring now composes with shadow utilities. A test that reads
1070
+ the ring colour from it should pick the 4px spread.
1071
+ - **Dark surfaces are `color-mix()` expressions** over `--zabi-base-*`
1072
+ (`--color-surface-raised`, `-elevated`, `-overlay`). The pixels are the same;
1073
+ `getComputedStyle` reports `color(srgb …)`, not `rgb(…)`, so a test that
1074
+ compares the string needs updating.
1075
+ - **Placeholder text is one step stronger** in both themes
1076
+ (`--color-input-placeholder`: #61616a light, #a1a1aa dark; 4.5:1 or more on
1077
+ the resting, hovered, focused and pressed field).
1078
+ - **Checked checkboxes and radios use the primary action fill**, with the
1079
+ primary button's text colour on the tick and dot.
1080
+ - **Dropdown menus, Select lists, the ColorPicker popover and NavigationMenu
1081
+ panels have 16px corners** (were 8px), and other nested corners are
1082
+ concentric: the Card in a Modal, MediaGrid's check mark, Alert's close
1083
+ button, rows in a `.list-group`, the avatar in SidebarFooter's profile
1084
+ button.
1085
+ - **`font-sans` and `font-mono` follow the theme's font tokens**
1086
+ (`--font-family-sans`, `--font-family-mono`), not Tailwind's system stacks.
1087
+ If you used `font-sans` to get the system font, write the stack yourself.
1088
+ - **`color-scheme` comes from the theme**: `light` by default and for
1089
+ `data-theme="light"`, `dark` under `.dark` and `data-theme="dark"` (also when
1090
+ both `class="dark"` and `data-theme="light"` are set). Native controls,
1091
+ scrollbars, date pickers and autofill follow the theme. A
1092
+ `<meta name="color-scheme">` tag is not needed, and a `color-scheme` rule of
1093
+ your own must come after the theme import to win.
1094
+
1095
+ **Markup, roles and accessible names**
1096
+
1097
+ - **A `Select` is found by role `combobox`, not `button`.** Its trigger has
1098
+ `role="combobox"` and reports `aria-invalid`, `aria-required` and
1099
+ `aria-busy`. Its form control is a native `<select name>` (not a hidden
1100
+ input), so code that looked for `input[type=hidden]` must look for the
1101
+ select, and `required` now blocks the form. A form reset returns the Select
1102
+ to the value it was rendered with. Update tests and selectors.
1103
+ - **A loading `Button` or `IconButton` is `aria-disabled="true"` and
1104
+ `aria-busy="true"`, not `disabled`.** It keeps keyboard focus and swallows
1105
+ the press. CSS or tests that matched it with `:disabled`, `[disabled]` or a
1106
+ strict `toBeDisabled()` must use `[aria-busy="true"]`.
1107
+ - **`ThemeToggle` (two modes) has the constant name "Dark mode"** with
1108
+ `aria-pressed`, not "Switch to dark mode" / "Switch to light mode"; update
1109
+ tests and scripts that find the button by those names. It applies the stored
1110
+ choice when it mounts (`storageKey={null}` turns that off) and no longer
1111
+ writes an inline `color-scheme`.
1112
+ - **`NavigationMenu` is a list of links and disclosure buttons**, not a
1113
+ `menubar`: the list has `role="list"` and items carry no menu roles. Screen
1114
+ readers announce it differently; keyboard use is unchanged.
1115
+ - **A `List` row with no `href` in a List with no `onclick` is plain content**,
1116
+ not a focusable button with an arrow.
1117
+ - **Checkbox and Radio inputs are stretched invisibly over their box** instead
1118
+ of being visually hidden, so `getByRole("checkbox").check()` works.
1119
+ - **An unnamed `RadioGroup` submits nothing.** Give it a `name`. (The
1120
+ generated name still groups the radios.)
1121
+ - **A disabled `Dropdown` or `Select` option is `aria-disabled`, not
1122
+ `disabled`**: reachable with the arrow keys, announced as disabled, not
1123
+ selectable.
1124
+ - A Select list and a Dropdown menu are one Tab stop (roving `tabindex`, 0 or
1125
+ -1). `getFocusableElements` skips `tabindex="-1"`.
1126
+
1127
+ **Defaults and behaviour**
1128
+
1129
+ - **A binding that starts `undefined` on a bindable prop is now written back
1130
+ as the default** (`""`, `false`, `null`). `let value = $state()` works with
1131
+ `bind:value` everywhere; no bindable prop has a fallback expression any
1132
+ more.
1133
+ - **Controls are at least 44px on touch screens** (`pointer: coarse`), so
1134
+ layouts there get taller (a checkbox list at an 8px gap is 52px a row).
1135
+ Nothing changes with a mouse.
1136
+ - **Every pressable control shows a `:active` pressed state** that does not
1137
+ depend on hover. Hand-written `:hover` rules apply only under
1138
+ `@media (hover: hover)`, so a tapped button no longer sticks in its hover
1139
+ colour.
1140
+ - **`Page` pads for the safe areas by default** (`safeArea`). An app that sets
1141
+ `viewport-fit=cover` and already pads for the insets itself should pass
1142
+ `safeArea={false}`. Without `viewport-fit=cover` the insets are zero and
1143
+ nothing changes.
1144
+ - **`Select` opens as a bottom sheet on a phone** (a touch screen narrower than
1145
+ 640px; `presentation="auto"`). Pass `presentation="popover"` to keep the
1146
+ pop-over. It fills its container like Input (a Select in a flex row takes the
1147
+ free width; give it a width such as `class="w-48"`, which is honoured now),
1148
+ and a label too long for the trigger wraps instead of being cut.
1149
+ `maxMenuHeight` defaults to `60dvh` (was `60vh`).
1150
+ - **`Button` labels wrap.** A label that does not fit goes onto a second line
1151
+ and the button grows (`min-height` instead of a fixed height); in a tight
1152
+ flex row a button may shrink and wrap where it held its width.
1153
+ - **Toasts show your message.** A toast with a `message` and no `title` shows
1154
+ the message, not a default heading; only a toast with `detail` can be
1155
+ expanded; the countdown sentence and "Click to stop" are off unless
1156
+ `showCountdown`. A toast with no `duration` closes after 7 s (14 s when its
1157
+ text is 121 to 240 characters, stays when longer); an error toast and a
1158
+ toast with an action stay until dismissed. On touch, `onpausechange` reports
1159
+ `paused` then `running` for a tap. Toasts also sit above the tab bar, the
1160
+ home indicator and the on-screen keyboard, and move while an overlay with a
1161
+ header and footer is open. Mount the Toaster inside `ZabiStringsProvider`
1162
+ when you use it.
1163
+ - **`Tooltip` opens on a tap and can be hovered.** A touch or pen press toggles
1164
+ it, and it stays open until a second tap, a tap elsewhere, Escape, a scroll
1165
+ or focus leaving (`touchDuration` defaults to `0`). The pointer can move from
1166
+ the trigger onto the open bubble, so an open tooltip takes presses: keep
1167
+ tooltips clear of other controls. A tooltip that would leave the viewport
1168
+ flips or slides to stay 8px inside, and `data-placement` reports the side
1169
+ used.
1170
+ - **`Tabs` with long labels scroll** instead of wrapping onto several lines.
1171
+ - **`AppBar` and `BottomTabBar` stay one row** (titles and labels get an
1172
+ ellipsis; the bars are 57px and 65px at any text size). AppBar can be two
1173
+ rows tall when the title would have under 72px beside the controls.
1174
+ - **Overlays follow the on-screen keyboard** where it covers the page (Modal,
1175
+ BottomSheet, SlideUp, Drawer), and Modal and SlideUp are at most `90dvh`.
1176
+ - **Light-mode `Input`, `Textarea` and `Select` darken their border on hover**
1177
+ and render text at 16px below 640px, so iOS Safari no longer zooms on focus.
1178
+ - **`Table` and `PropsTable` sit on the card surface** with an elevated header
1179
+ band; Skeleton and Header's variant chips use `bg-neutral-subtle`.
1180
+ - **Inside `on-brand` and `on-accent` blocks** labels, links, borders and quiet
1181
+ buttons change to the block's on-colour; a card inside a block gets the
1182
+ page's colours back. Custom properties named `--zabi-theme-*` appear on a
1183
+ block's parent.
1184
+ - **`ImageUpload` no longer forces a 16rem minimum width**, and its `id` names
1185
+ the control (the dropzone button), not the host. Set a width with `class` if
1186
+ you relied on it.
1187
+ - **`SidebarAccountPanel`'s `systemMode` default text is "Follows the
1188
+ system".**
1189
+ - `UnsavedChangesBar` and `ConfirmDialog` leave focus on the pressed button
1190
+ while busy, where they moved it to their container.
1191
+ - `Modal`'s `onclick` as a close signal is deprecated (see Deprecated).
1192
+
1193
+ **Theme files and the generator**
1194
+
1195
+ - **Dark theme files no longer restate the `--zabi-*` ramps.** `theme-dark`,
1196
+ `theme-dark-only` and the dark part of `colors` only remap roles; the raw
1197
+ palettes are declared once, in the light theme. Import the light theme
1198
+ first: `@import "zabi-components/theme-only"; @import
1199
+ "zabi-components/theme-dark-only";`. 8.0.0's `.dark`-only file carried a
1200
+ copy; a dark file imported on its own now has no ramps. An override of
1201
+ `--zabi-brand-*`, `--zabi-accent-*` or `--zabi-base-*` on `:root` applies in
1202
+ both modes. The dark files are about 13 KB larger, because the block is
1203
+ published for the class and attribute and for the system setting. See
1204
+ `docs/theme-imports.md`.
1205
+ - **The generator checks more role pairs than 8.0.0**, so a theme check that
1206
+ passed on 8.0.0 can turn red. `createTheme` and `zabi-theme` resolve every
1207
+ role pair the library's own guard checks (the field edge on fill, page and
1208
+ card; the progress fill on its track; the muted and danger focus rings and
1209
+ the control boundary on all five surface levels; the accent star outline on
1210
+ an inset surface; the focus ring or its offset gap on an accent button; the
1211
+ label on a selected and on a held selected pill tab). A pair below AA (4.5:1
1212
+ text, 3:1 UI parts) is a warning, and fails under `--strict`. The generated
1213
+ values are unchanged; the findings are real for that theme. Fix them with an
1214
+ override (`--set`), for example `--color-focus` and `--color-link-hover` one
1215
+ brand step darker, or use `--pin`.
1216
+ - Theme files work with `[data-theme="dark"]`, and with `[data-theme="auto"]`
1217
+ under `prefers-color-scheme: dark`. A page with no class and no attribute is
1218
+ light, as before.
1219
+
1220
+ **Package**
1221
+
1222
+ - `package.json`: new `./create-theme` export and `zabi-theme` bin; `culori`
1223
+ moved to `dependencies` (the generator uses it at run time);
1224
+ `sideEffects: ["**/*.css"]`; `CHANGELOG.md` shipped in `files`; the `prepare`
1225
+ script renamed `fix:lucide-types` (dev-only, so installing prints no
1226
+ install-scripts notice; run it if your editor needs it). Every component
1227
+ imports icons from `@lucide/svelte/icons/<name>`; import yours that way too.
1228
+ No export, prop, token or file from 8.0.0 was removed or renamed.
1229
+
1230
+ ### Added
1231
+
1232
+ #### Theming and the `zabi-theme` generator
1233
+
1234
+ - **`npx zabi-theme --brand "#0026EA"`** writes a CSS file with the full
1235
+ `--zabi-brand-*` ramp; with `--accent` and `--neutral` also the
1236
+ `--zabi-accent-*` ramp and all 21 `--zabi-base-*` steps; `--out` names the
1237
+ file. Import it after `theme-only` and `theme-dark-only` and it covers light
1238
+ and dark. The same is available in code as `createTheme({ brand, accent,
1239
+ neutral })` from `zabi-components/create-theme`, which returns `{ css, tokens,
1240
+ warnings, closest }`. The ramps sit on the library's lightness curve: your
1241
+ colour gives the hue and chroma and is not pinned to a step, so the exact hex
1242
+ may not appear, and the file header names the closest step. The generator
1243
+ picks the text colour for the brand and accent fills.
1244
+ - **The generator reports contrast** on stderr, in the file header and in
1245
+ `warnings`; `--strict` exits 1 on a failed pair. `--set <token>=<value>` (or
1246
+ `overrides`) moves a role and is checked too. `overrides` takes a flat map
1247
+ (both modes) or `{ light, dark, both }`; the CLI has `--set-light` and
1248
+ `--set-dark`. A light-only override is restated with the library's own dark
1249
+ value in the dark rules, and the check reads each mode's own value.
1250
+ - **`zabi-theme --pin`** (`createTheme({ pin: true })`): the primary action is
1251
+ the exact brand colour in light, with derived hover and pressed states and a
1252
+ label checked at 4.5:1. The focus ring and links take the colour where every
1253
+ guarded pair still passes. Dark keeps the mirrored ramp step unless the colour
1254
+ passes there; the header says which. A failing pair is a warning and the
1255
+ colour is never moved. `--pin-accent` / `pin: { accent: true }` does the same
1256
+ for the solid accent fill.
1257
+ - **`neutralChroma`** (CLI `--neutral-chroma`, 0 to 0.1, needs `neutral`): the
1258
+ OKLCH chroma at the neutral ramp's peak, for a clearly tinted neutral. With it
1259
+ the translucent ink roles (`--color-action-secondary` and its hover and
1260
+ pressed steps, `--color-surface-hover`, `-active`, `--color-border-overlay`
1261
+ in light, `--shadow-color`) follow the neutral ramp, and the file restates the
1262
+ dark values under the dark selectors.
1263
+ - **A theming guide.** `THEMING.md` is the documented token API: quick start
1264
+ with `zabi-theme`, import order, tables of the tokens an app may set (brand,
1265
+ accent and neutral ramps, the "on" colours, fonts and weights, radius,
1266
+ shadow, z-index) and the roles to read, light, dark and auto, what the
1267
+ generator does and does not do, and what does not follow an override.
1268
+ Renaming or removing a documented token is a breaking change; the token-name
1269
+ snapshot in `npm run test:themes` enforces it. The docs site has a `/theming`
1270
+ page with a brand switcher (`?brand=amber` opens any page in Amber, generated
1271
+ from `#C17B00` and a warm neutral).
1272
+ - **One brand override restyles light and dark.** The raw `--zabi-*` ramps are
1273
+ declared once and the dark block only remaps roles; dark surfaces are mixed
1274
+ from the neutral ramp, so warm or tinted greys carry through.
1275
+ - **An accent colour.** `--zabi-accent-50` … `950`, `--color-accent-50` …
1276
+ `950`, the roles `--color-accent`, `-hover`, `-active`, `-subtle`, `-border`,
1277
+ `-text`, `--color-on-accent`, and the classes `bg-accent`, `text-accent`,
1278
+ `border-accent` (with working `hover:` and `active:` forms, also
1279
+ `active:bg-info-active` and `hover:bg-nav-menu-active-hover`). The
1280
+ `energetic` tokens are unchanged. `Button` and `IconButton` take
1281
+ `variant="accent"` (solid, own hover and pressed fills, `--color-on-accent`
1282
+ label); `Badge variant="accent"` (subtle and solid); `Rating tone="accent"`.
1283
+ - **The text on a fill can be set.** `--zabi-on-brand` (light, white) and
1284
+ `--zabi-on-brand-dark` (dark, `brand-950`) feed `--color-on-brand`, which
1285
+ `--color-action-primary-text` follows; `--zabi-on-accent` and
1286
+ `--zabi-on-accent-dark` do the same for the accent. `Heading` and `Text` take
1287
+ `tone="inherit" | "on-brand" | "on-accent"`; the classes `on-brand` and
1288
+ `on-accent` set a block's text colour and make the focus ring visible on it;
1289
+ `on-surface` marks a surface of your own inside a block.
1290
+ THEMING.md ("Text and controls on a brand or accent block") says which button
1291
+ variants stay legible there. Directly on a block an empty or ticked checkbox,
1292
+ radio or toggle, a filled Rating star and status message text are not legible
1293
+ (put them on a card); a card inside a block needs `:has()`.
1294
+ - **Font tokens.** `--font-family-heading` (follows `--font-family-sans` until
1295
+ set; applied to `h1`–`h6` and Heading), `--font-family-mono` (CodeBlock reads
1296
+ it), `--font-weight-regular`, `-normal`, `-medium`, `-semibold`, `-bold`,
1297
+ `--font-sans`, `--font-mono`, `--font-heading`, and a `font-heading` utility.
1298
+ - **Dark mode by attribute and by the system setting.** The dark files apply to
1299
+ `.dark`, `[data-theme="dark"]`, and `[data-theme="auto"]` when
1300
+ `prefers-color-scheme` is dark; `[data-theme="light"]` stays light. Following
1301
+ the system is opt-in. Put the class or attribute on `<html>`.
1302
+ - **ThemeToggle `modes="three"`** steps through system, light and dark, writes
1303
+ `data-theme` on `<html>`, shows a monitor, sun or moon and names itself
1304
+ "Theme: system. Switch to light"; `labels` (including `labels.darkMode`)
1305
+ replaces the words. `mode` (bindable), `onmodechange` and `storageKey` read or
1306
+ drive the mode, or store it under your own key. It reads `dark`, `light` and
1307
+ `auto`, follows changes made elsewhere, and shows the right icon before it
1308
+ mounts.
1309
+ - **Theme helpers** from the package root: `getThemeMode`, `setThemeMode`,
1310
+ `isThemeDark`, `getStoredThemeMode`, `storeThemeMode`, `themeInitScript` and
1311
+ the `ThemeMode` type. `themeInitScript()` returns a script for `<head>` that
1312
+ applies the stored mode before first paint. TopNavbar takes `themeModes`,
1313
+ `themeStorageKey` and `themeLabels`; SidebarAccountPanel takes
1314
+ `themeModes="three"` with `onThemeModeChange`.
1315
+ - New tokens: `--color-surface-inset` (`bg-surface-inset`; a recessed well on a
1316
+ card, `base-100` light and `base-150` dark, AA text; dark `--color-input`
1317
+ points at it and keeps its value; do not use `--color-input` for a well, it is
1318
+ white in light), `--color-input-border-hover`, `--color-control-border`
1319
+ (`border-control-border`, `text-control-border`; 3:1 on every surface level;
1320
+ `--color-border-strong` is unchanged and decorative), `--color-focus-ring-muted`
1321
+ (`.focus-ring--muted`), `--color-focus-ring-danger` (the error colour; the
1322
+ on-colour inside a block), `--color-input-active` (`active:bg-input-active`;
1323
+ Select's trigger uses it), `--color-action-primary-subtle-active` and
1324
+ `--color-action-danger-subtle-active` (`active:bg-…`), `--zabi-list-row-radius`
1325
+ (`.list-group` sets it for rows), `--color-progress-fill`,
1326
+ `--color-progress-track`, `--zabi-avatar-ring`, and the documented star
1327
+ colours `--zabi-rating-on`, `--zabi-rating-off`, `--zabi-rating-on-hover`,
1328
+ `--zabi-rating-on-active`, `--zabi-rating-on-edge` (they apply when set on an
1329
+ ancestor or by a class). The contrast guard holds the muted and danger focus
1330
+ rings and the control boundary to 3:1 on all five surface levels, and fails
1331
+ when a focus-ring rule reads a colour that is not in its pair list.
1332
+
1333
+ #### New components
1334
+
1335
+ - **SortableList** reorders items and leaves their content to you. Each row
1336
+ has a drag handle (mouse and touch), arrow keys, Home and End on the handle,
1337
+ optional move up and down buttons and a polite announcement of the new
1338
+ position. `bind:items`, `onreorder` (item, old and new index),
1339
+ `controls="manual"`, `strings`. Escape cancels a drag; a key that cannot move
1340
+ further says so ("Hero section, already first"); a parent replacing `items`
1341
+ mid-drag cancels it. Clicks on the handle or a move button do not bubble: put
1342
+ them beside a header toggle, never inside another button. Handle and buttons
1343
+ sit 8px apart on touch.
1344
+ - **Collapsible**: a trigger wired to the panel it shows and hides, owning ids,
1345
+ `aria-expanded`, `aria-controls` and the panel's name. `title` gives a
1346
+ full-width header button with a chevron (`headingLevel` wraps it in a real
1347
+ heading); a `trigger` snippet hands the wiring to your own `<button>`.
1348
+ `bind:open`, `onopenchange`, `disabled`. Closed content stays in the DOM under
1349
+ `hidden` (`unmountOnClose` removes it). If the panel closes while focus is
1350
+ inside it, focus moves to the trigger. **CollapsibleGroup** makes an
1351
+ accordion: opening one closes the others, or `multiple`; Arrow Up and Down,
1352
+ Home and End move between headers. In a single-open group only the first
1353
+ panel marked open renders open (also on the server, without
1354
+ `onopenchange`); a disabled panel keeps its state.
1355
+ - **ConfirmDialog** (on Modal): `title`, `message`, `variant` (`danger`,
1356
+ `warning`, `info`, each with an icon; `danger` uses the danger button),
1357
+ `confirmLabel`, `cancelLabel`. If `onconfirm` returns a promise the dialog
1358
+ shows its loading state and cannot be dismissed until it settles: it closes on
1359
+ success, stays open on failure and passes the error to `onerror`; returning
1360
+ `false` keeps it open. `loading` does the same for your own request tracking;
1361
+ `oncancel` reports how the user backed out; `loadingLabel` (default
1362
+ "Working…") is announced once. Focus starts on Cancel, Enter only activates
1363
+ the focused button, and it renders in `document.body` unless `portal={false}`.
1364
+ It uses `role="alertdialog"`, draws no outline around the whole dialog while
1365
+ loading and sets no `aria-busy` on the panel.
1366
+ - **Drawer**: a modal panel from the side. `side` is `left`, `right`, `start` or
1367
+ `end`; `size` is `sm`, `md` or `lg`, never wider than the screen; `title`,
1368
+ `description`, a `footer` snippet; `dismissible`, `onclose({ reason })`,
1369
+ `closeLabel`, `initialFocus`, `portal` (default true). Focus is trapped and
1370
+ returns to the opener, the page does not scroll, the slide is skipped under
1371
+ `prefers-reduced-motion`, it shares the scroll lock with Modal and SlideUp and
1372
+ stacks with them. Its `onkeydown` runs after Escape handling; content with
1373
+ nothing to focus is a Tab stop named by the title; the panel pads for the
1374
+ bottom safe area; the close button takes taps in a 44px area.
1375
+ - **MediaGrid**: image and video thumbnails to pick from (the grid only).
1376
+ `getKey`, `getLabel`, `getUrl`; `bind:selected` or, with `multiple`,
1377
+ `bind:selectedKeys`. A single selection moves and is never cleared by a repeat
1378
+ press. `ondelete` shows a delete button named after each item (focus moves to
1379
+ the item that took its place); `getType`, `getPoster`, `loading`, `empty`
1380
+ snippet, `emptyHeadingLevel` (default 3), `minTileSize` (96px), `strings`. One
1381
+ Tab stop; arrow keys move by the columns on screen, Home and End within the
1382
+ row, Delete asks to delete; "Use the arrow keys to move between items." is read
1383
+ when focus enters the grid and shown below it while it has keyboard focus. The delete button sits on the end corner, mirrors
1384
+ in RTL, takes taps in a 44px area and shows a pressed state on touch.
1385
+ - **Spinner**: the loading ring on its own. Sizes `xs` to `lg` (12, 14, 16,
1386
+ 20px); with a `label` it is a `status`, without one it is decorative; fades
1387
+ instead of spinning under `prefers-reduced-motion`.
1388
+ - **Slider**: a native range input in the library's colours. `bind:value`,
1389
+ `min`, `max`, `step`, label, helper or error text, three sizes, `showValue`,
1390
+ `formatValue` (also `aria-valuetext`).
1391
+ - **UnsavedChangesBar**: Save and Discard while a form is `dirty`; sticky
1392
+ (`position` `bottom` or `top`); `onsave` may return a promise (saving state;
1393
+ a rejection keeps the bar and calls `onerror`); appearance announced politely;
1394
+ never takes focus; focus returns to the edited field when it goes; `actions`
1395
+ snippet. Warning before the page is left is your app's job.
1396
+ - **AppShell**: the phone layout. `header` snippet (AppBar), `<main>` content,
1397
+ `footer` snippet (BottomTabBar), a `dvh` column inside the safe areas. Sets
1398
+ `--app-shell-top-inset` and `--app-shell-bottom-inset` (fallbacks 57px and
1399
+ 65px) on itself and on `<html>` while mounted, for overlays in
1400
+ `document.body`. `contentElement="div"` inside a page that already has a
1401
+ `<main>`.
1402
+ - **AppBar**: a phone top bar with `title` (`headingLevel`, default 1;
1403
+ `titleLines={2}` allows two lines), back control (`backHref` or `onback`,
1404
+ `backLabel`), `leading` and `actions` snippets (up to two actions),
1405
+ `collapseOnScroll` (stays while focus is inside; no animation under reduced
1406
+ motion). The title always has at least 72px; a `leading` wider than the row is
1407
+ held to the row. Parts carry `data-appbar-part` (`back`, `title`, `actions`);
1408
+ the bar measures itself in the browser, so before hydration the title wraps to
1409
+ the second row. At 180px wide no title shows beside a back control and two
1410
+ actions.
1411
+ - **BottomTabBar**: three to five links with an icon above a short label.
1412
+ `items` take `href`, `label`, `icon`, `badge` (read with the label: "Inbox, 3
1413
+ new"; `badgeLabel`, `badgeMax`). `active` is an href or path (`aria-current=
1414
+ "page"`); left out, the bar follows the browser address, so pass
1415
+ `active={page.url.pathname}` on the server. Fixed on its own and in the flow
1416
+ in an AppShell (`position`), clear of the home indicator. Tabs are at least
1417
+ 44px with 8px gaps; under 52px wide the labels hide (icon only, label stays
1418
+ the accessible name); tabs go under 44px only when five do not fit. The
1419
+ active tab has a 2px outline in the action colour. With five tabs labels stay
1420
+ 11 to 12px at 200% text.
1421
+ - **Rating**: one to `max` (default 5) stars, can be empty. `bind:value` is a
1422
+ number or `null`; a radio group named by `label`, each star reads "4 of 5
1423
+ stars"; arrow keys, Home and End; `name` submits it. A repeat press on the
1424
+ selected star never clears it; `clearable` adds a clear button (Delete and
1425
+ Backspace clear too). `readonly` shows a score as one image ("Quiz, 3.5 of 5
1426
+ stars"), with fractions, `showValue`, `formatValue`; it submits nothing. 44px
1427
+ targets at every `size`; `strings`; the empty star uses
1428
+ `--color-control-border`.
1429
+ - **SegmentedControl**: two to four choices in one row. `options` (`value`,
1430
+ `label`, `icon`, `disabled`), `bind:value`, `onchange`. A radio group, not
1431
+ tabs: one Tab stop, arrows move and select, `name` submits, a repeat press
1432
+ does nothing. `fullWidth` (default true), labels wrap, 44px tall on touch.
1433
+ - **BottomSheet**: a modal panel sliding up from the bottom. Snap points
1434
+ (`snapPoints`, `"half"` and `"full"` by default; `bind:snap`); the grip drags
1435
+ between them or down to close and is a button ("Expand" / "Collapse"). Content
1436
+ scroll wins over dragging unless at its top. `title`, `description`, `footer`,
1437
+ `isOpen`, `dismissible`, `portal`, `initialFocus`, `closeLabel`,
1438
+ `onclose({ reason })` with the extra reason `"swipe"`. Safe areas, no animation
1439
+ under reduced motion, a centred 40rem sheet from `md`. Half and full heights
1440
+ are shares of what the on-screen keyboard leaves. The grip's focus ring is
1441
+ drawn inside its 44px box; a click reaches the grip button, and flicks are
1442
+ timed from the events' own times.
1443
+ - **StickyActionBar**: keeps a form's main action in view at the bottom of the
1444
+ screen or scrolling box, rises above the on-screen keyboard, reserves
1445
+ `scroll-padding-bottom`, never takes focus. One per scrolling box; a short
1446
+ form needs `class="flex min-h-full flex-col"`.
1447
+ - **FloatingActionButton**: a round 56px primary button. `label` (required, the
1448
+ accessible name), `extended`, `icon`, `href` (a link; a button otherwise),
1449
+ `position` (`bottom-end` default, `bottom-start`, `bottom-center`). Above the
1450
+ tab bar in an AppShell, above the safe area elsewhere; `--fab-bottom-offset`
1451
+ lifts it; it reserves `scroll-padding-bottom` on what scrolls under it.
1452
+ - **DateField** and **TimeField**: the browser's date and time inputs in the
1453
+ library's Input. `bind:value` is `YYYY-MM-DD` or 24-hour `HH:mm`, `""` when
1454
+ empty; `label`, `hint`, `error`, `min`, `max`, `step`, `required`, `disabled`,
1455
+ `readonly`, `size`, `name`; work in FormField; ids start with `input-`.
1456
+ **`formatDate` and `formatTime`** (package root) format those strings for a
1457
+ locale (`formatDate("2026-10-06", "sv")` is "6 okt. 2026"), never shift by the
1458
+ time zone, and return `""` for empty or invalid.
1459
+ - **Calendar**: one month as an ARIA grid with event dots. `bind:month`
1460
+ (`YYYY-MM`), `bind:selected` (`YYYY-MM-DD` or `null`), `events` (`date`,
1461
+ `label`, `tone`; up to three dots a day), `weekStartsOn` (default Monday),
1462
+ `locale`, `min`, `max`, `isDateDisabled`, `onselect`, `onmonthchange`,
1463
+ `strings`. One Tab stop: arrows by day and week, Home and End within the
1464
+ week, Page Up and Down by month (Shift: year). Days are named in full with
1465
+ state and events and are 44px tall; unavailable days have a line through the
1466
+ number. `locale` translates dates only, so pass `strings` for "today",
1467
+ "selected", "unavailable". Pressing the selected day does nothing.
1468
+ - **DropdownItem**: the menu item as a component for a Dropdown's custom
1469
+ `children`; takes its role from the Dropdown and joins the arrow-key order. A
1470
+ DropdownItem with custom children and a description is named by its content
1471
+ alone.
1472
+ - **Stepper**: progress through a multi-step form. `steps` (labels or `{ label,
1473
+ description }`), `bind:current` (zero-based), `layout` (auto goes compact below
1474
+ 30rem of its own width: it suits about five short labels; compact segments are
1475
+ narrower than 44px from seven steps with `interactive` on 320px), `size`
1476
+ (`sm`, `md`, `lg`), `strings`. Completed steps show a check, the current one
1477
+ its number, upcoming an outline; a navigation landmark with
1478
+ `aria-current="step"`; a step change is announced once. `interactive` makes
1479
+ completed steps buttons. It does not move focus: focus the new heading
1480
+ yourself. In Safari 16 and older it needs a width from its parent in a flex
1481
+ row.
1482
+ - **PhotoGrid**: square thumbnails that open a viewer. `photos` (`src`,
1483
+ `thumbSrc`, `alt`, `width`, `height`, `caption`, `id`), `columns` (3, then 4
1484
+ and 5), `max` ("+N" tile), `onopen(index)`, `onadd` ("Add photo" tile),
1485
+ `selectable` (`"single"` or `"multiple"`, `bind:selected`,
1486
+ `bind:selectedKeys`; a single selection is never cleared by a repeat press and
1487
+ each tile gets an Open button). One Tab stop, lazy loading, placeholder and
1488
+ failure fallback. **PhotoViewer**: a full-screen dialog for them, `bind:index`,
1489
+ `bind:isOpen`, caption, counter, up to three `actions` (more in a menu).
1490
+ Swipe or arrows change photo; pinch, double tap, Ctrl+wheel or `+`/`-` zoom;
1491
+ drag pans; swipe down, Escape or the close button close it
1492
+ (`onclose({ reason })`). Focus returns to the tile; the thumbnail shows
1493
+ blurred until the full image loads; neighbours are preloaded; dark in both
1494
+ themes; safe areas. Double tap is timed from the touch events. Previous and
1495
+ Next at the ends keep their plate and dim only the arrow.
1496
+ - **Avatar** and **AvatarGroup**: `Avatar` is a round picture falling back to
1497
+ initials (`name`, `src`, `size` `sm` 24, `md` 32, `lg` 48px, `alt`, `alt=""`
1498
+ for decorative, `locale`); initials come from the first and last word, are in
1499
+ the server markup and remain if the picture fails. They are `brand-200` on
1500
+ `brand-700` in the default light theme (the subtle primary pair, 4.5:1).
1501
+ `AvatarGroup` shows `people` overlapped as a list, up to `max` (default 4) and
1502
+ "+3" (`strings.more`), `locale`. Not interactive.
1503
+ - **SwipeableListItem**: a row with one or two `actions` revealed by a swipe
1504
+ towards the inline start (touch and stylus; mirrored in RTL); a swipe only
1505
+ reveals, an action runs on press. The same actions are behind a "more"
1506
+ button (`showMoreButton`, on by default; with `false` offer another route).
1507
+ Bindable `open`, `onopenchange`, `strings.actions`; one row open per list.
1508
+ - **PullToRefresh**: wraps a list; pulling down at the top calls `onrefresh`
1509
+ (touch only). Bindable `refreshing`, `disabled`, `threshold` (64px),
1510
+ `strings`. A Refresh button shown on keyboard focus does the same; a status
1511
+ region says "Refreshing" then "Updated". A rejected `onrefresh` ends the busy
1512
+ state silently: report the failure in the app. In controlled mode it says
1513
+ "Updated" whenever `refreshing` goes back to false.
1514
+ - **`focusToasts()`** moves focus to the newest toast; focus returns when it is
1515
+ dismissed, also from inside an open overlay.
1516
+
1517
+ #### Existing components
1518
+
1519
+ - **ImageUpload**: `onbrowse` replaces the native chooser and
1520
+ `event.preventDefault()` in `onclick` keeps it closed. A video value (file
1521
+ type, extension, or `previewType`) renders a `<video>` with controls, never
1522
+ autoplaying; a `preview` snippet replaces the preview. `label` and `id` (the
1523
+ dropzone is a native `<button>`); Change and Remove are named from the label
1524
+ ("Change logo") or `changeLabel` and `removeLabel`; the preview takes `alt`.
1525
+ A dropped file is checked against `accept` and rejected through
1526
+ `onfilereject`. Copy props: `browseText`, `changeText`, `removeText`,
1527
+ `errorTitle`, `errorRecovery` (`false` leaves the line out), `selectedText`,
1528
+ `removedText` ("Image selected" / "Image removed" are announced);
1529
+ `actionsPlacement` (`"overlay"` or `"strip"`, strip by default for video).
1530
+ - **IconButton**: `xs` size (24px, the minimum target; use `sm` or larger where
1531
+ touch is primary); `pressed` (bindable, `aria-pressed` and a pressed style in
1532
+ every variant; `event.preventDefault()` in `onclick` keeps the state; left
1533
+ undefined it renders no `aria-pressed`); `tone="danger"` on `ghost` or
1534
+ `outline` for a quiet destructive style. `href` on Button and IconButton
1535
+ renders a real link (`target`, `rel`, `download`); a disabled or loading link
1536
+ has no `href` and is `aria-disabled`.
1537
+ - **Modal**: `portal` (renders in `document.body`; a theme class set below
1538
+ `<body>` does not reach it); `onclose({ reason: "escape" | "backdrop" |
1539
+ "close-button" })`; `dismissible={false}` (the close button stays focusable,
1540
+ `aria-disabled`; setting `isOpen` still closes); `role="alertdialog"`;
1541
+ `closeLabel` (default "Close"); `fullScreen` (or `"mobile"` below `md`);
1542
+ `initialFocus` (also on SlideUp); typed `aria-describedby`, `aria-busy`,
1543
+ `data-*`, `id`. SlideUp: `closeLabel`, `swipeToClose` (grip, downward swipe,
1544
+ calls `onclick`), a `footer` snippet kept above the keyboard.
1545
+ - **Toast and Toaster**: `pushToast` takes `action: { label, onclick,
1546
+ dismissOnClick? }` (a button in the toast, announced with the message, closes
1547
+ after the handler unless `dismissOnClick` is `false`; the countdown pauses
1548
+ while pointer or focus is on it; give it a long `duration` or `0`; the
1549
+ `ToastAction` type is exported); `duration` takes `"short"` (3 s), `"medium"`
1550
+ (7 s), `"long"` (14 s), `"persistent"` or milliseconds, with `defaultDuration`
1551
+ on Toaster (`TOAST_DURATIONS`, `ToastDuration` exported); `strings` for every
1552
+ built-in word (`ToasterStrings`, `DEFAULT_TOASTER_STRINGS`), `aria-label`,
1553
+ `showCountdown`, `onpausechange({ id, paused })`, `data-paused`; `closeLabel`
1554
+ on Toast and Alert; `--toaster-bottom-offset` (72px clears a
1555
+ FloatingActionButton); other attributes pass to the region. A held finger
1556
+ pauses a toast, lifting resumes with at least 3 s left. Toasts do not fly
1557
+ under reduced motion; one with an action stays until dismissed.
1558
+ - **Dropdown**: options take `icon`, `tone: "danger"` and `description` (the
1559
+ `DropdownOption` type is exported). `presentation` (`"auto"`, `"popover"`,
1560
+ `"sheet"`; default `"popover"`): in a sheet the same menu opens in a
1561
+ BottomSheet titled by the control's label; `<Dropdown presentation="sheet">`
1562
+ is the library's action sheet (`sheetTitle`, `sheetSnap`, `sheetCloseLabel`,
1563
+ `sheetExpandLabel`, `sheetCollapseLabel`, `fullWidth`, optional
1564
+ `ariaLabelledby`). Menu boxes report `data-resolved-placement`.
1565
+ - **Select**: renders a real `<select>` on the server, the working control
1566
+ before hydration and with scripts off; on mount the custom list takes over,
1567
+ and a choice made before is kept and reported once; `presentation="native"`
1568
+ renders only the native select. `presentation` (`"auto"`, `"popover"`,
1569
+ `"sheet"`, `"native"`), `strings` (including `listLabel`;
1570
+ `DEFAULT_SELECT_STRINGS`, `SelectStrings`, `SelectPresentation` exported,
1571
+ also from atoms; the six text props still win). The chosen option has a check
1572
+ mark at the inline end and a fill, and every option carries `aria-selected`.
1573
+ A disabled Select submits its value once hydrated and not before; the inline
1574
+ message of a `required` Select is the browser's, in the browser's language; a
1575
+ label longer than one line is cut in the native select and wraps in the custom
1576
+ one, so the field can grow by a line.
1577
+ - **Input, Textarea, Select**: `hint` and `error` (a hint is tied with
1578
+ `aria-describedby`, an error sets the error state and is announced;
1579
+ `variant` + `message` still work and your own `aria-describedby` is merged).
1580
+ Input takes `leading` and `trailing` snippets and, for `type="password"`,
1581
+ `revealable` (`revealLabel`, default "Show password", `aria-pressed`). Input,
1582
+ Textarea, Select, Checkbox, Radio, Toggle and ThemeToggle accept their
1583
+ element's native attributes (`autocomplete`, `inputmode`, `maxlength`,
1584
+ `enterkeyhint`, `data-*`); Table and Text pass other attributes on. Toggle
1585
+ takes `aria-label` and `aria-labelledby` ("Toggle" is only the last fallback).
1586
+ `Checkbox` is exported from the atoms entry.
1587
+ - **Table**: `stacked` (or `"sm"`, `"md"`, `"lg"`) lays rows out as label and
1588
+ value pairs and drops the 20rem minimum; put `data-label` on each cell and
1589
+ keep the `<thead>`; `captionHidden` keeps the caption as the accessible name
1590
+ but hides it. A stacked cell keeps several children together on the value
1591
+ side; a cell without `data-label` puts its content there too; an empty cell
1592
+ leaves no blank line.
1593
+ - **EmptyState**: `headingLevel` (1–6, default 2); `size="compact"` (tighter,
1594
+ smaller title, a plain `<div>` not a named region; the default is still a
1595
+ `<section>` named by its title).
1596
+ - **TopNavbar**: nav items take `external` (default true for absolute URLs);
1597
+ `collapseAt` (`"sm"`, `"md"` default, `"lg"`, `"xl"`; `TopNavbarCollapseAt`
1598
+ exported).
1599
+ - **Tabs**: `fullWidth` (for two or three tabs; scrolls when a share is too
1600
+ small for the longest word). The rule under Tabs is `border-border`; a
1601
+ selected pill uses `action-primary-subtle`.
1602
+ - **SidebarShell**: `mobile="drawer"` hides the sidebar below `lg` and opens it
1603
+ in a Drawer from the start side: `bind:isOpen`, `trigger` snippet
1604
+ (`aria-expanded` wiring), `drawerTitle`, `closeLabel`, `onclose({ reason })`;
1605
+ it closes when a link is followed and when the screen widens past 1024px;
1606
+ `label` names the scrolling region (default "Navigation links").
1607
+ SidebarNavigation declares and passes these on. Default `mobile="none"`
1608
+ changes nothing.
1609
+ - **Page**: `safeArea` (default true) pads left, right and bottom by
1610
+ `env(safe-area-inset-*)`; nothing inside an AppShell or where the insets are
1611
+ zero; your own `px-*` class replaces that side. Needs `viewport-fit=cover`.
1612
+ - **Tooltip**: `touchDuration` (default `0`); one open at a time; a tooltip on a
1613
+ disabled button is tied to it with `aria-describedby`. Do not put essential
1614
+ information in a tooltip.
1615
+ - **ColorPicker**: the colour map is two native sliders (saturation, lightness)
1616
+ in one Tab stop, working by keyboard (Shift moves ten steps), touch and pen;
1617
+ new strings `area`, `saturation`, `lightness`, `invalidHex`; the hue slider
1618
+ shows its focus ring and is 44px on a coarse pointer.
1619
+ - **CodeBlock**: `copyLabel`, `copiedLabel`; "copied" is announced through a
1620
+ status region. **FormField**: `requiredLabel`.
1621
+ #### Mobile and touch
1622
+
1623
+ - New props for phones: `Page` `safeArea`, `Modal` `fullScreen`, `SlideUp`
1624
+ `swipeToClose` and `footer`, `TopNavbar` `collapseAt`, `Tabs` `fullWidth`,
1625
+ `Tooltip` tap, `Select` and `Dropdown` `presentation`, `SidebarShell`
1626
+ `mobile="drawer"`, `Toaster` `--toaster-bottom-offset`. Components for them:
1627
+ AppShell, AppBar, BottomTabBar, BottomSheet, Drawer, StickyActionBar,
1628
+ FloatingActionButton, SwipeableListItem, PullToRefresh, PhotoViewer. Safe areas
1629
+ need `viewport-fit=cover`.
1630
+
1631
+ #### Accessibility
1632
+
1633
+ - A focus ring in forced colours: `.focus-ring`, the legacy `.focus-brand` and
1634
+ `.focus-nav`, and the checkbox and radio row draw an outline there. The
1635
+ current page has an outline in forced colours in SidebarNavigation,
1636
+ SidebarPanel and TopNavbar. In forced colours every option of a Select list
1637
+ has the same border and the chosen one is told apart by its check mark only.
1638
+ - `docs/ACCESSIBILITY.md` has a "Library conventions" section (where focus goes
1639
+ when a focused control is removed, the shared overlay stack and scroll lock,
1640
+ disabled options, hover-revealed actions, focus in forced colours) and no
1641
+ longer lists fixed issues as open; `docs/KEYBOARD_NAVIGATION.md` describes
1642
+ what the components do.
1643
+
1644
+ #### Strings and hydration
1645
+
1646
+ - **`ZabiStringsProvider`**: wrap the app in `<ZabiStringsProvider
1647
+ strings={…}>` and every component under it uses those words unless an
1648
+ instance says otherwise. One entry per component with a `strings` object
1649
+ (including `avatarGroup`, `swipeableListItem`, `pullToRefresh`), keyed by prop
1650
+ name for components with single-text props (`toast`, `alert`, `codeBlock`,
1651
+ `imageUpload`, `unsavedChangesBar`), and a `common` group (close, back, expand,
1652
+ collapse, required, showPassword, search, confirm, cancel). Order, later wins:
1653
+ built-in English, provider, the instance's `strings`, its own text props. It
1654
+ is Svelte context (per request on the server, nestable, reaches portalled
1655
+ overlays); Modal, Drawer, SlideUp, BottomSheet, Toaster, Rating and the
1656
+ components above read it; landmark `ariaLabel`s are not covered.
1657
+ `getZabiStrings()`, `DEFAULT_ZABI_COMMON_STRINGS` and the types are exported.
1658
+ No translations ship; the README has a complete Swedish example. Nothing
1659
+ changes outside a provider.
1660
+ - **Every built-in text can be replaced**: `strings` on TopNavbar,
1661
+ SidebarFooter, SidebarAccountPanel, SidebarNavigation, SidebarBrandHeader,
1662
+ ColorPicker, ContactForm, PropsTable, ComponentDemo, Select, Toaster, Stepper,
1663
+ Calendar, MediaGrid, SortableList, Rating, PullToRefresh. The README's "Texts
1664
+ and other languages" lists the prop for each component.
1665
+ - **A choice made before the page hydrates is kept.** A Checkbox, Radio,
1666
+ RadioGroup, Rating, SegmentedControl or Select changed in the server's markup
1667
+ was set back at hydration; the bound value now follows the native input and
1668
+ the change callback fires once (a form reset still returns to the rendered
1669
+ value). A bound Select value with no matching option is kept and submitted as
1670
+ itself.
1671
+ - **No component drops keyboard focus on hydration** (Heading, Text,
1672
+ CardHeader, AppBar's title, EmptyState's heading, Container, AppShell,
1673
+ Collapsible); a guard fails the build if a dynamic element comes back.
1674
+
1675
+ #### Package and build
1676
+
1677
+ - Exports and package fields are listed under Upgrade notes. `zabi-components/types`
1678
+ matches the components again (see Fixed). `npm run build:css` works in a fresh
1679
+ checkout with no `dist/`.
1680
+ - The docs site honours `data-theme`, and Storybook's toolbar Theme control
1681
+ drives its sidebar, toolbar and docs pages. `docs/theme-imports.md` says what
1682
+ to expect when Tailwind and `zabi-components/css` are combined (the compiled
1683
+ stylesheet carries its own copy of each utility, so import order decides: with
1684
+ Tailwind in the app import `theme-only` and `theme-dark-only` instead).
1685
+ `THEME.md` no longer tells apps to import the compiled stylesheet after
1686
+ `theme-only` or to use `theme()` for a colour. `THEME_QUICK_REFERENCE.md`
1687
+ shows the current primary colour and dark mirror; `docs/VARIANTS.md`, the
1688
+ README, the Storybook introduction and the showcase guide cover the new
1689
+ components. The component docs site examples are written in English. The
1690
+ site's catalog sidebar is a drawer below 1024px and its "On this page" list
1691
+ marks the section being read; the site sets `viewport-fit=cover`.
1692
+
1693
+ ### Changed
1694
+
1695
+ - **Light mode has a visible surface ladder.** `--color-surface-base`,
1696
+ `--color-page` and `--color-background-tertiary` move from `base-100` to
1697
+ `base-150`; `--color-surface-elevated` from `base-50` to `base-100`; a card is
1698
+ 1.18:1 on the page (was 1.10) and a nested card 1.10:1 (was 1.04).
1699
+ `--color-card-active` and `--color-surface-overlay-hover` are `base-150`.
1700
+ - **Light tinted fills read on a white card.** `--color-<family>-subtle` moves
1701
+ from step 100 to 200 and `--color-<family>-border` from 200 to 300 for
1702
+ success, warning, error, energetic and info (1.36:1 on a card, was 1.16);
1703
+ `--color-neutral-subtle` is `base-250`, `--color-neutral-border` `base-300`;
1704
+ `--color-action-primary-subtle` and `-subtle-hover` are `brand-200` and
1705
+ `brand-300`.
1706
+ - **Light hover and secondary fills are stronger.** `--color-surface-hover` and
1707
+ `--color-surface-active` are 9% and 15% ink (were 6% and 11%);
1708
+ `--color-action-secondary`, `-hover`, `-active` are 10%, 15%, 20% (were 7%,
1709
+ 12%, 17%).
1710
+ - **Overlays have an edge in light** (`--color-border-overlay` is a 10% ink tint,
1711
+ was transparent); **light shadows are stronger** (`--shadow-color` `24 24 27`,
1712
+ `--shadow-opacity` 0.14; were `0 0 0` and 0.1).
1713
+ - **Light fields and disabled controls follow the new page**:
1714
+ `--color-input-hover` `base-100`, `--color-input-disabled` the page colour,
1715
+ `--color-action-disabled` `base-250`, `--color-action-disabled-border`
1716
+ `base-300`.
1717
+ - **Focus tokens**: `--color-focus` is `brand-600` in both themes;
1718
+ `--color-focus-weak`, `-medium`, `-strong` each move one step;
1719
+ `--color-nav-menu-focus` follows `--color-focus`; `.focus-ring--muted` (ghost
1720
+ and link buttons, the AppBar back control, the SortableList handle) reads
1721
+ `--color-focus-ring-muted`, 3.7:1 or more on every surface level (dark was
1722
+ `base-500`, 2.49:1 on elevated, 1.98:1 on overlay). Dark mode otherwise keeps
1723
+ its values: `.dark` restates the values it used to inherit.
1724
+ - **Dark mapping**: `--color-action-primary-text` follows `--color-on-brand`;
1725
+ dark `--color-base-*` steps alias the mirrored `--zabi-base-*` step; the dark
1726
+ block holds no raw palette and no hex value.
1727
+ - **Menus share one edge and surface**: Dropdown, NavigationMenuContent and the
1728
+ ColorPicker popover use `border-border-overlay` on `bg-surface-overlay` (the
1729
+ ColorPicker popover no longer sits on the inset field colour in dark). Dropdown
1730
+ menus and Select lists use the 16px overlay radius, and Select's search field
1731
+ and options sit 4px closer to the list's edge. ColorPicker's popover and
1732
+ NavigationMenu's panels use the overlay radius.
1733
+ - **Nested corners are concentric** where a rounded element sits closer to its
1734
+ container's corner than that corner's radius: the Card in a Modal, the avatar
1735
+ in SidebarFooter's profile button, MediaGrid's check mark (a rounded square)
1736
+ and video badge, Alert's close button, ListItem rows in a `.list-group`.
1737
+ - Input, Textarea and Select text is 16px below 640px (also ColorPicker's hex
1738
+ field and the search fields in Select and the sidebars); sizes unchanged from
1739
+ `sm` up and control heights unchanged.
1740
+ - **Checked checkboxes and radios use the primary action fill**, tick and dot in
1741
+ the primary button's text colour (was a paler step at 2.8:1 on the light page).
1742
+ - Input, Select and Textarea darken their border on hover in light
1743
+ (`--color-input-border-hover`); error, success, warning and disabled fields
1744
+ are unchanged. Select's trigger placeholder uses the placeholder colour.
1745
+ - Table and PropsTable sit on the card surface with an elevated header band;
1746
+ Skeleton and Header's variant chips use `bg-neutral-subtle`.
1747
+ - Sidebar search fields, the selected panel item, the elevated panel and the
1748
+ profile button use full-strength rings instead of 40–80% alpha ones; dashed
1749
+ empty states keep only their dashed border. SidebarNavigation's search
1750
+ placeholder keeps 4.5:1 on hover.
1751
+ - **Controls are 44px on touch** (`@media (pointer: coarse)`): Button, IconButton,
1752
+ Input and the Select trigger at `sm` and `md` (`lg` is 48px already; use it
1753
+ for the main action on a phone), and Slider rows, Checkbox, Radio and Toggle
1754
+ rows, Tabs, Collapsible triggers, Dropdown items, Select options (48px),
1755
+ NavigationMenu, TopNavbar and Sidebar items, SortableList controls and toast
1756
+ buttons. The close buttons of Alert, Toast, Modal, SlideUp and Drawer and the
1757
+ Toggle switch keep their size and take taps in a 44px area. IconButton `xs`
1758
+ stays 24px.
1759
+ - **Every pressable control shows a `:active` state**; a disabled Toggle or Tab
1760
+ no longer shows one. A toggled-on ghost, outline, link or danger-tone
1761
+ IconButton shows a pressed fill distinct from hover and 1.25:1 or more from
1762
+ rest. The selected pill Tab's pressed fill is 1.42:1 from rest (was 1.24:1)
1763
+ and its label keeps 4.89:1.
1764
+ - SlideUp pads its content for the bottom safe area and is at most `90dvh`
1765
+ (was `90vh`); Modal is at most `90dvh`.
1766
+ - **Tabs scroll sideways when they do not fit**, with a fade on the edge that has
1767
+ more, and bring the selected and keyboard-focused tab into view; labels no
1768
+ longer wrap or squeeze.
1769
+ - **Toasts**: sit above the tab bar and home indicator (the stack clears
1770
+ `--app-shell-bottom-inset` and the bottom safe area; below `sm` it spans the
1771
+ width with 16px gutters; a `viewport` Toast at the top clears the top safe
1772
+ area and an AppShell header); keep clear of an overlay's footer (16px above
1773
+ it, or at the top when there is no room; a centred desktop Modal changes
1774
+ nothing); take the larger free stretch above the panel or between header and
1775
+ footer, scrolling there, without covering an overlay's title and close button;
1776
+ sit above the on-screen keyboard; `--toaster-bottom-offset` is not added on top
1777
+ of an overlay's footer. Tab moves from an overlay's last control to the toast
1778
+ controls and back, and Escape in a toast returns focus without closing the
1779
+ overlay. Padding, gaps and button targets are in px and the status icon stays
1780
+ 20px at enlarged text (a two-sentence toast at 375px and 200% text is at most
1781
+ 420px tall, was 594px); buttons wrap under the title when it would get less
1782
+ than 8rem; the stack is capped to the screen and scrolls. The countdown says
1783
+ "1 second", not "1 seconds".
1784
+ - **Tooltip**: flips or slides to stay 8px inside the viewport, also with a
1785
+ mouse; below 640px a `left` or `right` tooltip opens above or below; a tap
1786
+ opens it when the finger comes up where it went down, so a scroll that starts
1787
+ on the trigger no longer flashes it; `data-placement` reports the side used.
1788
+ - **Overlays stay above the on-screen keyboard** where it covers the page; the
1789
+ focused field is scrolled into view.
1790
+ - **Overlay headers stay compact at large text sizes.** In BottomSheet, Modal,
1791
+ Drawer and SlideUp the header's padding, gaps, grip and close button are in px
1792
+ and the title grows to 1.3 times and stops; at 200% text a half-height sheet
1793
+ gives the content 378 of 576px (was 182). Titles break at a hyphen in the
1794
+ page's language and a too-long word no longer sticks out of Drawer or SlideUp.
1795
+ - **Select fills its container** and no longer changes width with the chosen
1796
+ option; its trigger is a `combobox` reporting `aria-invalid` (with `error`, the
1797
+ error variant or a failed `required` check), `aria-required` and `aria-busy`;
1798
+ `required` blocks an empty form, shows the browser's message as the error and
1799
+ focuses the trigger; a long label wraps; the form control is a `<select
1800
+ name>`; a reset returns it to its rendered value; it opens as a sheet on a
1801
+ phone; the chevron and the Dropdown panel do not animate under reduced
1802
+ motion; keyboard focus in the native select moves to the trigger at handover;
1803
+ `aria-label` and `aria-labelledby` name the native select too; with several
1804
+ empty required Selects only the form's first invalid control takes focus.
1805
+ - **A loading Button or IconButton** is `aria-disabled` and `aria-busy` (see
1806
+ Upgrade notes); a loading link keeps its place in the Tab order. Button labels
1807
+ wrap (`min-height`: 32, 40, 48px; 44px minimum on touch).
1808
+ - **A disabled Dropdown option stays focusable** (`aria-disabled`).
1809
+ - **A toast with an action stays until dismissed** unless given a `duration`;
1810
+ with no duration a toast closes after 7 s (see Upgrade notes); a message with
1811
+ no title is the toast's text.
1812
+ - **ThemeToggle** (two modes) has the name "Dark mode" and applies the stored
1813
+ choice on mount in both modes; it no longer writes an inline `color-scheme`
1814
+ and removes a stale one, so adding or removing `class="dark"` from your own
1815
+ script moves native controls with the tokens.
1816
+ - **EmptyState at `size="compact"`** is a plain `<div>`.
1817
+ - **ImageUpload** fills its container; `id` names the control; Change and Remove
1818
+ appear on `:focus-visible` (not any focus), wrap and truncate in a narrow
1819
+ container, stay visible on coarse pointers and where hover is unavailable, and
1820
+ sit on an opaque plate so "Change" is readable over any image (it was 2.59:1 in
1821
+ dark over a light image).
1822
+ - **`font-sans` and `font-mono` follow the theme.**
1823
+ - **A List row with nothing to do is plain content**; a disabled plain ListItem
1824
+ row no longer carries `aria-disabled`.
1825
+ - Checkbox and Radio inputs are stretched invisibly over their box.
1826
+ - Textarea and Select status messages use the same text colour step as Input's.
1827
+ - **Icons are imported one file at a time** from `@lucide/svelte/icons/<name>`,
1828
+ not the `@lucide/svelte` barrel: an app importing one Button from the root
1829
+ compiles 366 modules where it compiled 3,800 (233 from
1830
+ `zabi-components/atoms`). The re-exported icons keep their names.
1831
+ - The `viewport` prop on NavigationMenu only sets `isMobile` on the context for
1832
+ your own children; it has no effect of its own, and the docs now say so.
1833
+ - **`Toast`/`Alert`/`CodeBlock`/`ImageUpload`/`UnsavedChangesBar`** follow
1834
+ `ZabiStringsProvider`.
1835
+
1836
+ ### Deprecated
1837
+
1838
+ - **Modal's `onclick` is deprecated as a close signal.** It is still called with
1839
+ the event on Escape, a backdrop click and the close button; use `onclose`.
1840
+ - In `zabi-components/types`, members that were exported but never existed
1841
+ (`open`, `className`, `position`, Button's `ariaLabel` and icon props) stay as
1842
+ optional and deprecated so existing code keeps compiling.
1843
+
1844
+ ### Fixed
1845
+
1846
+ - **`strings` props and `ZabiStringsProvider`:** a key set to `undefined` keeps
1847
+ the English default instead of blanking the text (`null` and an empty string
1848
+ are still honoured).
1849
+ - **`Progress`:** a passed `id` is on the wrapper only (it appeared on the
1850
+ wrapper and the bar); `aria-valuenow` is clamped to 0..`max`; a non-positive
1851
+ or non-finite `max` falls back to 100; new `aria-label` and `aria-labelledby`
1852
+ props name the progressbar.
1853
+ - **`ColorPicker`:** Escape closes the popover and returns focus to the swatch
1854
+ (and does not close a Modal around it); opening by keyboard moves focus in;
1855
+ typing a hex redraws the colour map; with a `label`, the hex input and swatch
1856
+ are named by it. Its popover could be placed below the viewport at 320px; a
1857
+ press on a toast no longer closes it.
1858
+ - **`Select`:** the popover listbox is named by the field's label (still "Select
1859
+ options" with no label); **`Dropdown`** gets an optional `ariaLabelledby`
1860
+ prop. Arrow keys get past a disabled option. The Select trigger's pressed
1861
+ state shows in light (was the resting fill) and is 1.27:1 against rest in
1862
+ light and 1.31:1 in dark (was 1.10:1). A Select no longer changes its value
1863
+ when the page hydrates. A disabled Select dims its value. Dropdown and Select
1864
+ return focus to the trigger when the menu closes from inside it; focus your
1865
+ own handler moved elsewhere is left alone. A Select list and a Dropdown menu
1866
+ are one Tab stop: Shift+Tab goes to the search field in one step, and a
1867
+ character typed on an option goes to the search field or the matching item.
1868
+ A press on a toast no longer closes an open Select or Dropdown.
1869
+ - **`SwipeableListItem`:** a long action label wraps (it was clipped at the start
1870
+ at 200% text), the action area is capped at two thirds of the row, and the
1871
+ "more" button stays 44px at any text size. With `showMoreButton={false}` focus
1872
+ has nowhere to return to after an action.
1873
+ - A long Modal title no longer squeezes the close button or runs out of the
1874
+ panel (the title wraps, also inside a single long word; the close button keeps
1875
+ 32px).
1876
+ - A Tooltip with `strategy="fixed"` no longer covers its trigger when it opens
1877
+ above or to the left; a closed tooltip no longer widens the page; it has an
1878
+ edge in forced colours, is centred correctly in RTL and its arrow points at its
1879
+ trigger in RTL; only one tooltip is open at a time and the pointer can cross a
1880
+ diagonal to its far corner. Escape with a tooltip showing dismisses the
1881
+ tooltip only, not the modal or sheet it sits in.
1882
+ - A Dropdown stays on screen: measured before paint, it flips when the preferred
1883
+ side does not fit, slides back when neither does and is capped to the viewport
1884
+ with its own scroll only when it must be (8px margin). `placement` is still
1885
+ the preferred side; a menu that fits is positioned as before. A Dropdown, Select
1886
+ list or NavigationMenu panel is no longer cut off by a scrolling or clipping
1887
+ ancestor such as a Modal's content (the nearest clipping ancestor decides the
1888
+ side; when it fits on neither it is positioned against the viewport; an
1889
+ ancestor with a transform or filter cannot be escaped), and a menu leaves a
1890
+ clipping container only when that puts it on screen. A height-capped Dropdown
1891
+ scrolls inside its rounded panel. Dropdown follows the writing direction
1892
+ (`bottom-start` and `top-start` sit on the start edge, item text aligns to it).
1893
+ - NavigationMenu fits a narrow screen: the list wraps onto further rows and a
1894
+ content panel slides back inside the viewport. A press on a toast no longer
1895
+ closes a NavigationMenu panel or the TopNavbar menu.
1896
+ - TopNavbar's phone menu closes after a link in it is followed, when
1897
+ `currentPath` changes, on Escape (focus returns to the menu button), on a click
1898
+ or focus move outside the bar and when the screen widens past the breakpoint;
1899
+ a press on a control behind the open menu still reaches it. It is at most as
1900
+ tall as the screen below the bar and scrolls, allows for the top safe area,
1901
+ its links fill the row, and `aria-controls` on the menu button is set only
1902
+ while the menu exists. The menu button stays on screen with a long brand or
1903
+ enlarged text (the brand truncates below the breakpoint).
1904
+ - Toasts fit a 320px screen (the 18rem minimum width no longer overflows); the
1905
+ close button takes taps in its full 44px area; `aria-controls` on the expand
1906
+ button is set only while the detail is open; focus no longer falls to the page
1907
+ after "Okay" or "Click to stop" is pressed; dismissing the focused toast moves
1908
+ focus to the next toast or back to where it was; a tap neither sticks nor
1909
+ dismisses; toasts do not fly in or out under `prefers-reduced-motion`.
1910
+ - A disabled Checkbox, Radio or RadioGroup row no longer dips, changes fill or
1911
+ shows the pressed fill when pressed or hovered; stacked checkbox and radio
1912
+ rows no longer overlap on touch; the row shows its hover
1913
+ (`--color-surface-hover`, `--color-surface-active`, was a fixed `base-100`).
1914
+ Checkbox shows its loading ring (replacing the tick, fading under reduced
1915
+ motion).
1916
+ - Alert's close button shows the library's focus ring and mirrors in RTL.
1917
+ - State variants beside a semantic colour class work: the hand-written colour
1918
+ classes sit outside every cascade layer, so `text-description
1919
+ hover:text-headline` never changed on hover. Thirteen such variants the
1920
+ components use are restated by hand (the hover colour of close buttons, the
1921
+ hover border of outline buttons, the disabled text colour of fields and ghost
1922
+ buttons).
1923
+ - Accent hover and pressed states work: `hover:bg-accent-hover` and
1924
+ `active:bg-accent-active` did nothing next to `bg-accent`.
1925
+ - An overlay opened later is always on top: Modal and SlideUp take a z-index
1926
+ that grows with the number of open overlays, so a modal opened from a portalled
1927
+ one no longer opens behind it; a modal opened while a portalled drawer is open
1928
+ (and the reverse) is drawn on top.
1929
+ - Modal and SlideUp keep Tab inside, and still close on Escape, when focus has
1930
+ fallen out of the dialog because the focused control was disabled or removed.
1931
+ A modal or sheet with nothing focusable takes focus itself. Modal, Drawer,
1932
+ SlideUp and BottomSheet no longer take focus from content that focused itself
1933
+ inside the panel as it opened. Focus returns to a replaced element (the one
1934
+ that now has the opener's id) instead of `<body>`. Modal and SlideUp do not
1935
+ slide in under `prefers-reduced-motion`.
1936
+ - The focus ring survives a shadow: `.focus-ring` on an element with a `shadow-*`
1937
+ or `ring-*` utility (also `shadow-none`) drew no ring (the SidebarNavigation
1938
+ search field, the selected SidebarPanel row, an interactive Card, any
1939
+ IconButton given a shadow); it now composes. A List no longer clips the focus
1940
+ ring of its rows.
1941
+ - Placeholder contrast: placeholder text and the format hint of an empty
1942
+ DateField or TimeField was 3.40:1 on a dark field and 4.40:1 on a hovered light
1943
+ one; see Upgrade notes. A disabled field's placeholder is no stronger than a
1944
+ disabled value.
1945
+ - Native controls follow the dark theme under `.dark` as well as `data-theme`
1946
+ (scrollbars, date and time pickers, other native form controls, autofill); a
1947
+ page that sets `color-scheme` on `<html>` keeps its value.
1948
+ - Hover colours no longer stick after a tap: the hand-written `:hover` rules in
1949
+ the theme files (`.bg-action-primary:hover` and 41 more) now sit in `@media
1950
+ (hover: hover)`.
1951
+ - ImageUpload keeps keyboard focus (to Change after a selection, to the dropzone
1952
+ after Remove, untouched when focus is elsewhere), cancels a file dropped while
1953
+ `disabled`, its actions layer no longer takes clicks meant for the preview, its
1954
+ error message is tied to the Change button when a preview is showing, and a
1955
+ disabled dropzone no longer takes the primary border on hover.
1956
+ - ActionPanel shows its hover and pressed states (the fill is drawn over the
1957
+ card). A selected Tabs pill shows its pressed state and a selected List row its
1958
+ selected border. ListItem rows show a hover and a pressed fill.
1959
+ SidebarFooter's avatar ring used a colour token that does not exist and now uses
1960
+ the border colour. A `dark:` variant in Section's accent background, which
1961
+ followed the OS setting and never applied, was removed.
1962
+ - ThemeToggle shows the right icon before it mounts (both icons are in the
1963
+ markup and the `dark` class picks one); it works with `data-theme`.
1964
+ - ConfirmDialog no longer draws a focus outline around the whole dialog while
1965
+ loading and sets no `aria-busy` on the panel.
1966
+ - A Badge with a long label wraps: it is at least 20, 24 or 28px tall, grows when
1967
+ the label wraps and is never wider than its container; a one-line badge is
1968
+ unchanged to the pixel and a wrapped one reads as a rounded rectangle.
1969
+ - An AppBar's title is never squeezed out (see AppBar).
1970
+ - Keyboard focus is visible in forced colours (see Accessibility).
1971
+ - **`zabi-components/types` matches the components again.** Every exported
1972
+ `*Props` interface declares the props its component accepts today: `ModalProps`
1973
+ has `isOpen`, the interfaces have `class`, and the new props are there.
1974
+ `SelectProps.options`, `AlertProps.message` and `TooltipProps.content` are
1975
+ optional, as on the components.
1976
+ - A bindable prop bound to `undefined` no longer stops the page from hydrating
1977
+ (RadioGroup threw, and 36 other bindable props could); see Upgrade notes.
1978
+ - Button and IconButton (including `variant="accent"`) do not scale when pressed
1979
+ under `prefers-reduced-motion`; built-in loading rings (Button, IconButton,
1980
+ Input, Textarea, ActionPanel, Toggle) fade instead of spinning, Checkbox's
1981
+ ring stops; Toggle's loading ring has its gap back.
1982
+ - Input, Textarea and Select no longer point `aria-describedby` at a message that
1983
+ is not rendered.
1984
+ - `fullWidth` Tabs scroll instead of breaking a word; BottomSheet's grip answers
1985
+ a mouse click; a flick on a sheet is measured from the events' own times.
1986
+ - Drawer content clears the home indicator; MediaGrid's remove button shows a
1987
+ pressed state on touch; SidebarAccountPanel's three-mode theme row follows a
1988
+ theme changed elsewhere; CodeBlock announces "copied".
1989
+ - The brand menu in the docs site header stays on screen (it opened past the
1990
+ right edge at 768px and 1024px); the Pine and Citron accents follow the Iris
1991
+ recipe in light (fill at step 600 with a white label, focus ring at step 600;
1992
+ Pine's light ring was 1.93:1 on the page, now 4.19:1); `theme-color` follows
1993
+ the theme toggle; the site's cards carry `shadow-sm` and the showcase uses
1994
+ surface tokens in place of raw ramp steps.
1995
+ - README links to RELEASING.md point at the repository, and the README says to
1996
+ install `@lucide/svelte` and import icons per file.
1997
+
1998
+ ## [8.0.1] - 2026-09-29
1999
+
2000
+ Fixes the documented setup. In 8.0.0 (and 7.0.2) the three-line Tailwind setup
2001
+ from the README left components unstyled; `@import "zabi-components/css"` was
2002
+ the only import that worked. No tokens or `exports` paths were added, removed
2003
+ or renamed.
2004
+
2005
+ ### Fixed
2006
+
2007
+ - **Theme files are valid CSS again.** `theme`, `theme-only`, `theme-dark` and
2008
+ `theme-dark-only` were written without semicolons between declarations, so a
2009
+ CSS parser read all 355 tokens as one declaration. `scripts/build-css.js` now
2010
+ terminates each declaration.
2011
+ - **`theme` and `theme-only` carry an `@source` directive** for the package's
2012
+ own components. Tailwind CSS v4 does not scan `node_modules`, so the classes
2013
+ the components use were never generated. No `@source` line is needed in your
2014
+ own CSS.
2015
+ - **`theme` and `theme-only` ship the hand-written component rules**:
2016
+ `.focus-ring` and its variants, `.text-action-primary`, the action hover,
2017
+ active and disabled states, the semantic colour classes and the `z-*` scale.
2018
+ These existed only in the compiled `css` bundle. Without them a primary
2019
+ button's label took the colour of its fill, and nothing had a focus ring.
2020
+ - **ColorPicker renders on the server.** Its teardown ran in `onDestroy`, which
2021
+ also runs during SSR, and threw `window is not defined`. It is now returned
2022
+ from `onMount`.
2023
+ - **Progress has an accessible name.** Its label was a `<label for>`, which
2024
+ only names form controls, so screen readers announced a progressbar with no
2025
+ name. The label is now referenced through `aria-labelledby`.
2026
+
2027
+ ### Changed
2028
+
2029
+ - The universal-selector scrollbar rules (`* { scrollbar-width: thin }` and the
2030
+ `*::-webkit-scrollbar` family) are in the compiled `css` bundle only, not in
2031
+ the theme files, so importing a theme does not restyle every scrollbar in
2032
+ your app. `.scrollbar-semantic` is in both and is the opt-in.
2033
+ - `theme` and `theme-only` grew from about 22 kB to about 35 kB.
2034
+ - `scripts/validate-theme.js` parses the theme files instead of pattern-matching
2035
+ them, and fails the build if the token block does not parse or the component
2036
+ rules are missing. The old check passed on the broken files.
2037
+
2038
+ ### Migration
2039
+
2040
+ Nothing to change if you import `zabi-components/css`. If you worked around the
2041
+ broken theme files with your own `@source` line or your own copies of
2042
+ `.focus-ring` or `.text-action-primary`, you can remove them.
2043
+
2044
+ ## [8.0.0] - 2026-09-22
2045
+
2046
+ Consolidates two development cycles. 7.1.0 and 7.2.0 were both prepared but
2047
+ never published, so the last release on npm is 7.0.2 and everything below ships
2048
+ together in 8.0.0.
2049
+
2050
+ ### Breaking changes since 7.0.2
2051
+
2052
+ What can break a working 7.0.2 consumer. Each item is detailed in the sections
2053
+ that follow.
2054
+
2055
+ - **The `zabi-components/react` subpath is gone** from `exports`. It pointed at
2056
+ `dist/react`, which was never built, so importing from it already failed at
2057
+ resolution — but the subpath no longer exists at all.
2058
+ - **Three tokens were removed**: `--color-action-primary-disabled`,
2059
+ `--color-action-secondary-disabled`, `--color-action-danger-disabled`. Use the
2060
+ shared `--color-action-disabled`, `--color-action-disabled-text` and
2061
+ `--color-action-disabled-border` instead. Your own CSS referencing the old
2062
+ names will silently resolve to nothing, so grep for them before upgrading.
2063
+ - **Every chromatic ramp was regenerated** against a shared lightness curve, and
2064
+ the semantic families moved from step 500 to step 600. Rendered colour changes
2065
+ throughout, in both themes, with no opt-out. Screenshot tests will all churn.
2066
+ - **`max-w-*`, `w-*` and `min-w-*` with `xs`–`2xl` resolve to container sizes
2067
+ again** (`max-w-lg` is 32rem, not 1.5rem). This is a bug fix, but anyone who
2068
+ compensated for the broken values in their own CSS must undo that.
2069
+ - **No install scripts run in consumer installs.** The `postinstall` hook is
2070
+ gone; it is now a dev-only `prepare` script and is not in the tarball.
2071
+
2072
+ Not breaking, but worth knowing: the published tarball no longer contains
2073
+ `dist/lib/**`. Those were declaration files for this repo's demo site and
2074
+ showcase, emitted by mistake; no `exports` entry ever pointed at them.
2075
+
2076
+ ### Design system
2077
+
2078
+ Outcome of a full design review of all 63 components. The headline change is
2079
+ that the colour ramps are now generated against one shared lightness curve, so
2080
+ the semantic families read as a single family instead of six unrelated colours.
2081
+
2082
+ #### Colour — BREAKING (visual)
2083
+
2084
+ - **All chromatic ramps regenerated** (`brand`, `citron`, `pine`, `iris`,
2085
+ `warning`, `error`) against a shared CIE L\* curve defined in
2086
+ `tokens/chromatic-scales.js`. `<ramp>-600` now means the same lightness in
2087
+ every ramp. Previously the ramps spanned L\* 34.6–75.0 at step 500 — a
2088
+ 40-point spread at the same nominal step — and `pine` fell 29.8 points between
2089
+ 400 and 500, which made half that ramp unusable.
2090
+ - **Semantic tokens moved to step 600** across the board. `warning`, `error` and
2091
+ `energetic` previously sat at step 500 (the dark mirror's fixed point) and so
2092
+ did not invert between themes at all, while `success` and `info` did. Solid
2093
+ badges were 2.15:1 (warning) and 1.97:1 (energetic) in light mode.
2094
+ - **New per-family tokens**: `--color-<family>-subtle` (step 100),
2095
+ `--color-<family>-border` (step 200). `--color-<family>-text` is now step 700
2096
+ for every family.
2097
+ - **`--color-action-primary` is now `brand-600`** (was `brand-950`). The primary
2098
+ action carries the brand colour in both themes.
2099
+ - **`--color-action-primary-active` no longer jumps across the ramp.** It was
2100
+ `brand-400` against a `brand-50` label — 1.66:1, so the label vanished while
2101
+ the button was held. Hover/active now step darker in light, lighter in dark.
2102
+ - **`--color-action-secondary`** is a neutral alpha tint instead of a 10% brand
2103
+ wash, which was too pale to read as a control.
2104
+ - **New**: `--color-action-disabled`, `--color-action-disabled-text`,
2105
+ `--color-action-disabled-border` — one quiet neutral pair shared by every
2106
+ variant. Disabled was previously `brand-500` + `opacity-50`, which made a
2107
+ disabled primary *lighter and more colourful* than an enabled secondary.
2108
+ - **New**: `--color-surface-hover`, `--color-surface-active` — surface-relative
2109
+ alpha tints. `hover:bg-base-100` resolved to exactly `--color-surface-base` in
2110
+ dark mode, so ghost buttons had no hover feedback on the app shell.
2111
+ - **New**: `--color-control-track{,-hover,-active}` for switch rails.
2112
+ - **Removed**: `--color-action-{primary,secondary,danger}-disabled` (use
2113
+ `--color-action-disabled`); the raw-hex `.dark` danger overrides (danger now
2114
+ mirrors through `--color-error-*` like every other family).
2115
+ - `--color-input` is now a raised surface (white in light, an inset step in
2116
+ dark) rather than a grey well that read as disabled.
2117
+ - `--color-input-placeholder` is wired up — it was defined but unused.
2118
+ - `--color-link` moved to `brand-700`: `brand-600` cleared AA on a card but only
2119
+ managed 4.42:1 on the page surface.
2120
+
2121
+ #### Geometry — BREAKING (visual)
2122
+
2123
+ - **One control height scale**: `sm` 32px, `md` 40px, `lg` 48px, shared by
2124
+ Button, IconButton, Input, Select and Textarea. They previously rendered at
2125
+ 40/48/64, 32/40/48 and 38/46/50 — a `lg` Button stood 14px taller than the
2126
+ `lg` Input beside it.
2127
+ - **Four role-based radii** replace eight t-shirt radii: `--radius-control`
2128
+ (8px), `--radius-container` (12px), `--radius-overlay` (16px),
2129
+ `--radius-pill`. Radius no longer changes with size. `--radius-sm/md/lg/xl`
2130
+ remain as legacy aliases for consumer overrides.
2131
+ - Button type is monotonic again: weight is `font-medium` at every size (it used
2132
+ to drop to `font-normal` at `lg`, so the biggest button had the lightest
2133
+ label), and the arbitrary letter-spacing values are gone.
2134
+ - **Elevation is two steps**, not four. `shadow-sm` means raised (cards, tables,
2135
+ sidebars, toggle knobs), `shadow-lg` means floating (modals, sheets, menus,
2136
+ toasts); `shadow-none` stays the explicit absence of one. `shadow-md` and
2137
+ `shadow-xl` are gone, and so is the bare `shadow` on the Toggle knob, which
2138
+ resolved to a Tailwind default `app.css` never defined. An elevated Card no
2139
+ longer deepens its shadow on hover — it was already at the floating step, so
2140
+ the hover now changes background only.
2141
+ - **Layout spacing snaps to the 4px grid.** 30 half-step utilities
2142
+ (`gap-1.5`, `gap-2.5`, `px-2.5`, `px-3.5`, `py-2.5`, `space-y-1.5`, …) were
2143
+ setting a second rhythm against the first across forms, sidebars and badges.
2144
+ Margins are deliberately exempt: `mt-0.5` beside a first line of text is an
2145
+ optical alignment nudge, not rhythm, and Alert, ListItem, ToasterToast and the
2146
+ radio control still use it.
2147
+ - A one-row `Textarea` is now 40px like the `Input` beside it (was 44px, from
2148
+ `py-2.5`).
2149
+
2150
+ #### Components
2151
+
2152
+ - **`Badge`**: rebuilt. New `emphasis` prop (`subtle` | `solid`, default
2153
+ `subtle`), plus `class`, `children` and `...restProps` — none of which existed,
2154
+ despite stories shipping for two of them. Pill radius instead of 2px. Renders
2155
+ nothing when it has no label, rather than leaving an empty box.
2156
+ - **`Alert`**: tinted fill + tinted border instead of a bare 1px outline on a
2157
+ transparent ground; full-width by default with a new `inline` prop; one Lucide
2158
+ icon family instead of hand-rolled SVGs of mixed weight.
2159
+ - **`Heading`**: no longer hardcodes `font-family: "Nunito Sans"` in a local
2160
+ `<style>` block — it uses `--font-family-sans`, so rebranding the font token
2161
+ now actually rebrands headings. `level` is typed `1 | 2 | 3 | 4 | 5 | 6`
2162
+ instead of `number`; new `size` prop decouples visual size from semantic
2163
+ level; `children` snippet supported; display sizes carry negative tracking.
2164
+ - **New `SidebarShell`**: the chrome of a sidebar with the regions left open —
2165
+ width, surface, collapse behaviour, the shared inset and a scrolling middle.
2166
+ Regions are snippets (`header` / `children` / `footer`), each handed
2167
+ `{ collapsed, insetX }`. `SidebarNavigation` is 39 props on one component and
2168
+ is excellent right up until your sidebar is not shaped like that one, at
2169
+ which point none of the parts are reachable; compose `SidebarBrandHeader`,
2170
+ `SidebarNavSection` and `SidebarFooter` into the shell instead and skip the
2171
+ ~30 props you never set. `SidebarNavigation` is now built on it rather than
2172
+ duplicating the chrome, so the two cannot drift. Additive — its prop API is
2173
+ unchanged.
2174
+ - **`SidebarNavigation`**: its Props table documented 7 of 39 props. All 39 are
2175
+ documented now, with types and defaults.
2176
+ - **`Text`**: now sits on the same type ramp as `Heading` rather than carrying
2177
+ its own — `size="md"` matches an `h6` and `size="lg"` an `h5`, line box
2178
+ included, and a new `xs` step (12px) extends the bottom. Leading is stated
2179
+ rather than inherited from Tailwind's defaults, so the shared steps stay
2180
+ locked together if the scale is retuned. **New `weight` prop** (`normal` |
2181
+ `medium` | `semibold` | `bold`) lets body copy carry emphasis without being
2182
+ promoted to a heading; `tone="label"` defaults to `medium`, every other tone
2183
+ to `normal`. Purely additive — existing `sm`/`md`/`lg` call sites are
2184
+ unchanged visually.
2185
+ - **`Input`**: raised surface, quieter placeholder, `min-w-48` removed.
2186
+ - **`Card`**: radius no longer scales with size; `outlined` uses `border-border`.
2187
+
2188
+ #### API consistency
2189
+
2190
+ - **Class overrides actually win now.** `class` being "merged last" only held
2191
+ for properties the component didn't already set: `rounded-control` and
2192
+ `rounded-container` are equal-specificity utilities, so the winner was
2193
+ whichever Tailwind emitted later. The sidebar's search field asked for
2194
+ `rounded-container` and rendered at 8px. Every component now merges through
2195
+ `cn()` (tailwind-merge, a new runtime dependency), with the role-based radii
2196
+ declared explicitly since `control`/`container`/`overlay`/`pill` are role
2197
+ names rather than scale values that tailwind-merge could recognise.
2198
+ - **`class` is now the public prop on every component.** 25 components exposed
2199
+ `className`, 15 exposed `class`, and 21 exposed neither. `className` is kept
2200
+ everywhere as a deprecated alias and both are merged, so existing call sites
2201
+ keep working.
2202
+ - Every component spreads `...restProps` onto its host element.
2203
+ - `Button`: `fullWidth` replaces `isFullWidth` (deprecated alias retained).
2204
+
2205
+ #### Guardrails
2206
+
2207
+ - **New** `scripts/check-ramp-lightness.js` — fails if a ramp drifts off the
2208
+ shared curve (±1.5 L\*) or develops a cliff (>15 L\* between adjacent steps).
2209
+ - **New** `scripts/check-contrast.js` — resolves every fill/foreground pair a
2210
+ component can render, in both themes, and fails below WCAG AA. 84 pairs.
2211
+ - **New** `scripts/check-control-geometry.js` — fails if controls disagree on
2212
+ height, or if any component uses a t-shirt radius.
2213
+ - **New** `scripts/generate-ramps.js` + `tokens/chromatic-scales.js` — ramp
2214
+ generation, wired into `npm run sync:tokens`.
2215
+ - `scripts/check-token-violations.js` now also rejects fixed ramp steps in
2216
+ interaction states (`hover:bg-base-*`, `active:bg-base-*`), which is how the
2217
+ invisible ghost hover got in.
2218
+ - **New** `scripts/generate-surfaces.js` + `tokens/surface-ladder.js` — the four
2219
+ dark surface levels are now derived from one wash ladder instead of four
2220
+ independently tuned hex values, and regenerate with `npm run sync:tokens`.
2221
+ `--color-surface-elevated` moves `#35353a` → `#363638` and
2222
+ `--color-surface-overlay` `#44444c` → `#454547` (slightly less blue); base and
2223
+ raised were already exactly on the ladder.
2224
+ - The ramp-interaction rule now also scans `src/lib/marketing` and the landing
2225
+ route. The rest of the checks guard what ships in the package, but
2226
+ `hover:bg-base-100` had reached two marketing specimens, where dark mode
2227
+ painted the hover darker than the card it sat on.
2228
+ - `scripts/check-token-violations.js` also rejects **off-scale shadows**
2229
+ (anything but `shadow-sm` / `shadow-lg` / `shadow-none`, including a bare
2230
+ `shadow`) and **half-step spacing** on gap/space/padding utilities. Margins
2231
+ are exempt by design. Comment lines are skipped so prose about shadow DOM
2232
+ doesn't trip it.
2233
+ - New scripts: `npm run check:ramps`, `check:contrast`, `check:geometry`, and
2234
+ `check:design` (all four). `build:css` and `check` run `check:design`.
2235
+
2236
+ #### Stories
2237
+
2238
+ - `Badge` → `AllVariantsWithIcons` no longer crashes. It used an invalid CSF
2239
+ render shape (`Component: 'div'` with a `children` array) cast through
2240
+ `as any`, so TypeScript could not catch it and it threw at runtime. Replaced
2241
+ with real harness components, which also fixes `WithChildren` and
2242
+ `WithCustomClass` (both silently rendered nothing).
2243
+
2244
+ ### Forms
2245
+
2246
+ - **`ContactForm`**: invalid submissions no longer fall through to a native POST and page reload.
2247
+ - **`ImageUpload`**: `onchange` / `onclick` are now actually called; `value` supports `bind:value`; new `onfileselect({ file, url })` exposes the selected `File`. Change/Remove actions are visible on keyboard focus, and Space activates the drop zone.
2248
+ - **`ColorPicker`**: 3-digit hex (`#f00`) is parsed correctly; `value` supports `bind:value`.
2249
+ - **`Form`**: accepts standard form attributes and a `novalidate` prop; docs note that `onsubmit` handlers must call `preventDefault()` for client-side handling.
2250
+
2251
+ ### Widgets
2252
+
2253
+ - **`Tabs`**: Arrow/Home/End move focus as well as selection; the tablist is no longer an extra tab stop; `activeTab` supports `bind:activeTab`.
2254
+ - **`Select`**: new `name` prop renders a hidden input for native form submission; the search field sits outside the `listbox`; string/number option values compare consistently.
2255
+ - **`Dropdown`**: new optional `header` snippet; Home/End/Space behave normally inside text inputs in the menu.
2256
+ - **`Toggle`**: new `id`, `name` and `value` props for label pairing and form submission.
2257
+ - **`ThemeToggle`**: a consumer `onclick` no longer overrides the toggle; pre-mount placeholder respects `size`/`variant` (no layout shift).
2258
+
2259
+ ### Overlays
2260
+
2261
+ - **`Modal` / `SlideUp`**: `role="dialog"` moved from the backdrop to the panel; body scroll locked while open (nested-safe); focus trap picks up content added while open; Escape closes only the topmost dialog. `Modal` gains `showClose` (default `true`) so untitled modals still have a close button.
2262
+ - **`Toaster`**: countdown is no longer announced every second; it pauses on hover/focus; no double announcements from nested live regions.
2263
+ - **`NavigationMenu`**: fixed a leaked document `mousedown` listener; panels use the disclosure pattern (no `role="menu"` / `aria-haspopup`); new `ariaLabel` prop.
2264
+ - **`Dropdown`**: arrow keys, Home and End now reach `menuitemradio` and `menuitemcheckbox` items. The item query matched only `menuitem` and `option`, so a menu built from radio items had nothing to focus: opening it by keyboard focused nothing and Tab closed it, leaving the items unreachable.
2265
+ - **`Alert`**: `closable` now hides the alert; new bindable `open` prop.
2266
+ - **`Tooltip`**: Escape no longer steals focus; CSS custom properties no longer leak onto `:root`.
2267
+ - **`util/ssr-safe`**: timer helpers typed with `ReturnType<typeof setTimeout/setInterval>` instead of `NodeJS.Timeout`, so consumers don't need `@types/node`.
2268
+
2269
+ ### Surfaces
2270
+
2271
+ - **New semantic surface elevation tokens** (both themes), lowest → highest: `--color-surface-base` (page), `--color-surface-raised` (cards, panels, sidebars), `--color-surface-elevated` (nested cards, hover/active fills), `--color-surface-overlay` (modals, sheets, menus, toasts), plus `--color-surface-overlay-hover` (row/icon hover inside overlays) and `--color-border-overlay` (1px overlay edge: transparent in light, visible in dark). Utilities: `bg-surface-base`, `bg-surface-raised`, `bg-surface-elevated`, `bg-surface-overlay`, `bg-surface-overlay-hover`, `border-border-overlay`.
2272
+ - **Dark mode elevation no longer relies on shadows**: levels step up +6 OKLCH lightness each (L 21 → 27 → 33 → 39) on the neutral base hue. Light mode is visually unchanged.
2273
+ - **Existing tokens are now aliases** (no renames): `background` → `surface-base`; `card` → `surface-raised`; `surface-1` → `surface-raised`; `surface-2` → `surface-elevated`; `card-elevated` → `surface-overlay` (light) / `surface-elevated` (dark); in dark, `card-hover` → `surface-elevated`, `card-active` and `surface-3` → `surface-overlay`. Dark values of `card`, `card-hover`, `card-active`, `surface-1`–`surface-3` shift slightly to land on the new levels.
2274
+ - **Floating components use the overlay level**: `Modal`, `SlideUp`, `Dropdown` (and `Select`'s menu), `NavigationMenu` panels, `Toaster` toasts and the `Toast` atom now paint `bg-surface-overlay` (previously `bg-card` / `bg-surface-1`, which in dark matched or undercut the card they floated over). Hover fills inside them use `bg-surface-overlay-hover` so they read above the overlay. `Modal`/`SlideUp` get a `border-border-overlay` edge. `Card variant="elevated"` uses `bg-card-elevated`.
2275
+ - **Dark `--color-description`** moves one step lighter (`base-650`) so description text keeps ≥ 4.5:1 contrast on every surface level, including overlays.
2276
+ - **New guard**: `scripts/check-surface-elevation.js` (run by `validate-theme.js` during `build:css`) fails the build if dark levels aren't strictly increasing, any step is outside 5–8 OKLCH L points, overlay hover/tooltip fills are darker than the overlay, or a floating component paints a surface below overlay.
2277
+
2278
+ ### Theme fixes
2279
+
2280
+ - **`max-w-*`, `w-*` and `min-w-*` with `xs`–`2xl` now use Tailwind's container sizes again** (`max-w-lg` = 32rem). Tailwind v4 resolves these utilities through `--spacing-*` before `--container-*`, so the theme's named spacing tokens made `max-w-lg` resolve to 1.5rem for anyone importing the theme. New `--max-width-*`, `--width-*` and `--min-width-*` tokens (xs 20rem … 2xl 42rem) are checked first. Spacing utilities such as `p-lg` / `gap-md` are unchanged.
2281
+ - **`--color-caption` meets WCAG AA on every surface level**: light `base-500` → `base-550` (was 4.40:1 on the page background, now ≥ 5.58:1); dark `base-500` → `base-650` (was 2.00:1 on overlays, now ≥ 5.00:1). To keep caption visibly softer than description in dark mode, dark `--color-description` moves from `base-650` to `base-700` (≥ 6.53:1).
2282
+ - **New guard**: `scripts/check-surface-elevation.js` also fails the build if `headline`, `body`, `label`, `description` or `caption` drop below 4.5:1 on any surface level in either theme.
2283
+ - **`Modal`**: the title and close button line up with the body and footer. The inner `Card`'s own padding stacked on the header's padding and indented the title 24px past the content.
2284
+
2285
+ ### Packaging
2286
+
2287
+ - **Removed `zabi-components/react`**: the export pointed at `dist/react`, which was never built, and the wrappers were out of sync with the Svelte components. The optional `react` peer dependency and `@types/react` are gone too.
2288
+ - **No more `postinstall` in consumer installs**: `scripts/fix-lucide-svelte-icon-dts.js` only ever patched this repo's own `node_modules` (from a consumer install it resolved to a non-existent nested path and did nothing). It now runs as a dev-only `prepare` script and is no longer shipped in the tarball. Installs with `--ignore-scripts` (pnpm/Bun defaults) are unaffected.
2289
+ - **`zabi-components/lib/ssr-safe` and `zabi-components/lib/variant-utils` now resolve to compiled `.js` + `.d.ts`** (`dist/util/ssr-safe.*`, `dist/routes/lib/variant-utils.*`) instead of raw `.ts` sources. Import paths are unchanged.
2290
+ - **`generateId` / `createId` from the package root** now use the shared `util/ssr-safe` implementation; the previous root version used `Date.now()` on the server, so two ids created in the same millisecond collided.
2291
+ - **`CodeBlock`**: window-chrome dots use semantic tokens (`bg-error` / `bg-warning` / `bg-success`) instead of raw Tailwind palette classes; props are now typed.
2292
+ - **`check:tokens`** also flags raw Tailwind palette utilities (e.g. `bg-red-500`) in `src/components`.
2293
+ - **Repo**: `dist/`, `.svelte-kit/` and `storybook-static/` are no longer tracked in git; unused Storybook template assets and `src/_tmp-lucide-import.ts` removed.
2294
+ - **CI**: Node 24; the smoke workflow now also runs `check`, unit tests, theme tests and the token scan; publish runs `check` and unit tests before building.
2295
+
2296
+ ## [7.0.2] - 2026-04-20
2297
+
2298
+ ### Changed
2299
+
2300
+ - **`Tooltip`**: Tooltip surface styling now uses theme tokens **`--color-tooltip-bg`** / **`--color-tooltip-fg`** (with **`bg-tooltip-bg`** / **`text-tooltip-fg`** utilities) and **`z-tooltip`**, keeping layout/arrow logic in scoped CSS while aligning colors with the design system.
2301
+ - **`Tooltip` layout**: Tooltip panel uses **`width: max-content`** with a responsive **`max-width`** cap (`min(24rem, calc(100vw - 2rem))`) so typical copy stays on one line until the limit; arrow fill follows **`--color-tooltip-bg`**.
2302
+ - **Theme (`src/app.css`)**: Defines **`--color-tooltip-bg`** / **`--color-tooltip-fg`** (defaults to base-800 / base-50); **`prefers-contrast: high`** maps them to **`CanvasText`** / **`Canvas`** for stronger contrast. Regenerated **`zabi-components*.css`** picks up the new utilities.
2303
+
2304
+ ## [7.0.1] - 2026-04-19
2305
+
2306
+ ### Fixed
2307
+
2308
+ - **`postinstall`**: The Lucide typings patch script is **included in the published package** (`scripts/fix-lucide-svelte-icon-dts.js` via `package.json` `files`), so `npm install zabi-components` no longer fails when lifecycle scripts run.
2309
+ - **Theme CSS (Tailwind v4)**: Replaced `theme(colors.*)` inside `@theme` / `.dark` with `var(--color-*)` for Zabi scales and stable hex literals for Tailwind default ramps (e.g. red/amber) and white, so consumers processing `theme-only` / `colors` bundles do not hit unresolved `theme()` during Tailwind passes.
2310
+ - **Props typings**: **`Button`**, **`IconButton`**, and **`Tooltip`** prop types now intersect native element attributes (`HTMLButtonAttributes` / `HTMLAttributes<HTMLDivElement>`) so passthrough attributes (`class`, ARIA, `data-*`, etc.) type-check when spread with `...restProps`.
2311
+
2312
+ ### Added
2313
+
2314
+ - **`docs/lucide-icons.md`**: Lists Lucide icons imported by published components for teams using a custom `@lucide/svelte` barrel.
2315
+
2316
+ ### Changed
2317
+
2318
+ - **`scripts/sync-theme-tokens.js`**: End marker after the mirrored base ramp now matches the **`Brand Color Scale - Dark mode variants`** comment (including the “no theme() forward refs” note).
2319
+
2320
+ ### Migration / discovery
2321
+
2322
+ - **`Navbar` → `TopNavbar`**: The older **`Navbar`** export was removed in **6.0.0**; use **`TopNavbar`**. See the **6.0.0** migration notes in this file.
2323
+
2324
+ ## [7.0.0] - 2026-04-19
2325
+
2326
+ ### Breaking changes
2327
+
2328
+ - **`Page`**: Removed the built-in `max-w-4xl` cap. Width is now fully controlled by the parent, or pass e.g. `className="max-w-4xl"` (or use **`Container`** / **`Section`** with `maxWidth` per the layout-width policy).
2329
+ - **`EmptyState`**: Removed the built-in `max-w-lg` cap. Add `className="max-w-lg"` (or another token) if you still want a narrow empty state.
2330
+
2331
+ ### Added
2332
+
2333
+ - **`postinstall`**: After `npm install`, runs `scripts/fix-lucide-svelte-icon-dts.js` to strip a duplicate `type X = ReturnType<typeof X>` line from each `@lucide/svelte` per-icon `dist/icons/*.svelte.d.ts` file. Some TypeScript language services treat the unpatched file as a non-module, which breaks types for deep icon imports and root re-exports.
2334
+ - **`fixedSidebarFlyout`**: New Svelte **action** in the built package (`util/fixed-sidebar-flyout.js`) used by **`SidebarFooter`**. It pins flyouts with `position: fixed` to `[data-sidebar-flyout-anchor]` inside `[data-sidebar-flyout-root]`, teleports the panel to `document.body` while open to avoid scroll clipping, and updates position on scroll/resize. Options: `open`, `align` (`"top"` | `"bottom"`), `gap`, `anchorRole` (`"search"` | `"profile"`).
2335
+
2336
+ ### Changed
2337
+
2338
+ - **`CodeBlock`**: Replaced the ad-hoc copy **`<button>`** with **`IconButton`** (ghost, sm). Copy / copied states use Lucide **`Check`** and **`Copy`** from **`@lucide/svelte/icons/*`**. Wrapper uses **`bg-surface-1`**, **`border-border`**, header **`bg-surface-2`**; code uses **`text-base-900`** on light surfaces instead of **`bg-base-900`** / **`bg-base-*`** chrome with **`text-base-100`** body text.
2339
+ - **`SidebarFooter`**: Profile panel markup restructured with **`data-sidebar-flyout-root`** on the footer host; **`data-sidebar-flyout-anchor="profile"`** on the launcher; the profile snippet is positioned with **`fixedSidebarFlyout`** (`align: "bottom"`, viewport-limited **`max-height`**, **`overflow-y-auto`**, **`overscroll-contain`**). Drops the previous **`absolute bottom-0 left-full`** positioning that misbehaved beside fixed sidebars / scroll ancestors.
2340
+ - **`SidebarNavigation`**: Search trigger wrapped with **`data-sidebar-flyout-anchor="search"`** (collapsed and expanded) so flyouts share the same anchor protocol as the footer profile panel.
2341
+ - **Lucide usage in source**: Prefer **`import Icon from "@lucide/svelte/icons/name"`** over the package barrel for clearer tree-shaking and alignment with the postinstall typings fix. Showcase demos replace **`CircleHelp`** with **`CircleQuestionMark`** (current Lucide export name).
2342
+ - **`zabi-components.css` (generated bundle)**: Picks up additional theme utilities used by the showcase and organisms—e.g. **`--spacing-xs`**, **`text-base-900`**, **`max-h-[min(32rem,calc(100dvh-6rem))]`**, **`overscroll-contain`**, prose/fit/max/min width utilities, responsive **`md:z-10`**, **`md:w-80`**, **`md:flex-nowrap`**, **`md:items-stretch`**, **`overflow-auto`**, **`align-top`**. Some rarely-used utilities were dropped from the scanned set where no longer referenced (e.g. **`left-full`**, base **`items-stretch`** in favor of **`md:items-stretch`**, **`bg-gray-50`**).
2343
+
2344
+ ### Fixed
2345
+
2346
+ - **Lucide + TypeScript**: Consumers importing per-icon modules get consistent typings after install thanks to the **`postinstall`** patch for **`@lucide/svelte`** ~**0.544.x** declaration output.
2347
+
2348
+ ### Migration (from 6.x)
2349
+
2350
+ 1. **`Page` / `EmptyState`**: If layout relied on the old max widths, add them explicitly:
2351
+ - `<Page className="max-w-4xl">` or wrap content in **`Container`** / **`Section`**.
2352
+ - `<EmptyState className="max-w-lg" …>` (or another **`max-w-*`** from your layout policy).
2353
+ 2. **`npm install`**: Keep **`postinstall`** enabled so Lucide icon **`.d.ts`** files are patched; if you use **`npm install --ignore-scripts`**, run `node scripts/fix-lucide-svelte-icon-dts.js` manually or rely on root **`@lucide/svelte`** barrel imports only (may bundle more code).
2354
+ 3. **Demos copying Lucide icon names**: Replace **`CircleHelp`** with **`CircleQuestionMark`** when upgrading Lucide past names that removed **`CircleHelp`**.
2355
+
2356
+ ## [6.0.0] - 2026-04-12
2357
+
2358
+ ### Breaking changes
2359
+
2360
+ - Removed the `Navigation` organism; use `TopNavbar` with `items`, optional `embedded`, or a `nav` snippet instead.
2361
+ - Renamed `SidebarProjectPanel` to `SidebarPanel` (including the `SidebarPanelItem` type).
2362
+
2363
+ ### Added
2364
+
2365
+ - Package subpath exports `zabi-components/colors` and `zabi-components/css` for theme and full bundle CSS.
2366
+ - Published theme documentation files in the package (`THEME.md`, `THEMING.md`, `docs/theme-imports.md`).
2367
+
2368
+ ### Changed
2369
+
2370
+ - Theme build runs `sync:tokens` before CSS generation; added theme output tests (`test:themes`).
2371
+ - Refined default border styling on several components.
2372
+
2373
+ ### Migration
2374
+
2375
+ - Replace `import { Navigation } from 'zabi-components'` (or organisms path) with `TopNavbar` and the documented props/snippets.
2376
+ - Replace `SidebarProjectPanel` imports with `SidebarPanel` and align prop/type names to `SidebarPanelItem` where applicable.
2377
+
2378
+ ## Pending deprecations
2379
+
2380
+ - Legacy deep CSS import paths under `zabi-components/dist/*.css` are still supported for compatibility but documented as legacy. Prefer short exports such as `zabi-components/theme-only`, `zabi-components/theme-dark-only`, `zabi-components/colors`, and `zabi-components/css`.
2381
+
2382
+ ## [5.0.22] - 2026-03-16
2383
+
2384
+ ### ✨ **Features**
2385
+
2386
+ - **List Components**: Added new `List` and `ListItem` components with documentation and usage examples
2387
+ - **Sidebar**: Added `SidebarNavigation` and `SidebarPanel` with improved navigation behaviors
2388
+
2389
+ ### 🔧 **Improvements**
2390
+
2391
+ - **Card Components**: Refactored `Card`, `CardHeader`, and `CardContent` and simplified composition patterns
2392
+ - **Documentation**: Updated Storybook setup and improved component documentation quality
2393
+ - **Config & Environment**: Refined environment variable definitions and release metadata handling
2394
+
2395
+ ### ✅ **No Breaking Changes**
2396
+
2397
+ This is a backward-compatible update.
2398
+
2399
+ ---
2400
+
2401
+ ## [5.0.21] - 2026-01-17
2402
+
2403
+ ### ✨ **Features**
2404
+
2405
+ - **IconButton**: Added a new icon-only button with variants, sizes, and accessible labels
2406
+ - **Exports & Storybook**: Included IconButton in atom exports and Storybook registry
2407
+ - **Docs**: Added IconButton examples to the docs and components showcase
2408
+
2409
+ ### 🔧 **Improvements**
2410
+
2411
+ - **Navigation**: Header navigation now stacks for mobile-friendly layouts
2412
+ - **Select**: Improved selected label truncation and helper message layout
2413
+ - **Button**: Standardized disabled behavior without pointer-events override
2414
+
2415
+ ### ✅ **No Breaking Changes**
2416
+
2417
+ This is a backward-compatible update.
2418
+
2419
+ ---
2420
+
2421
+ ## [5.0.20] - 2025-01-27
2422
+
2423
+ ### 🔧 **Patch Release**
2424
+
2425
+ - **Maintenance**: General improvements and stability updates
2426
+ - **Build**: Enhanced build process and type definitions
2427
+ - **Documentation**: Minor documentation updates
2428
+
2429
+ ### ✅ **No Breaking Changes**
2430
+
2431
+ This is a backward-compatible patch release.
2432
+
2433
+ ---
2434
+
2435
+ ## [5.0.19] - 2025-01-27
2436
+
2437
+ ### 🐛 **Bug Fixes**
2438
+
2439
+ #### **Modal Component**
2440
+ - **Fixed SSR Import Error**: Resolved issue where Modal component was trying to import `focus-utils.js` from a relative path that didn't exist in consuming projects
2441
+ - **Internal Focus Utilities**: Moved focus management utilities (`focus-utils.ts`) to be properly included in the build output
2442
+ - **Build Process**: Added post-build script to fix import paths in built components
2443
+ - This fixes the "Failed to load url ../../routes/lib/focus-utils.js" error that occurred during SSR
2444
+
2445
+ ## [5.0.18] - 2025-01-27
2446
+
2447
+ ### ✨ **Component Enhancements & Documentation**
2448
+
2449
+ #### **Card Component Improvements**
2450
+ - **Compound Components**: Added CardHeader, CardContent, CardFooter, CardTitle, and CardDescription sub-components
2451
+ - Enables flexible card composition with better structure
2452
+ - Maintains backward compatibility with existing title/description/image props
2453
+ - Supports both old and new API patterns
2454
+ - **Enhanced Variants**: Improved elevated, outlined, and flat variants with better hover states
2455
+ - **Accessibility**: Added keyboard navigation (Enter/Space) and proper ARIA attributes for interactive cards
2456
+ - **Size Variants**: Enhanced size support (sm, md, lg) with responsive padding and typography
2457
+
2458
+ #### **Button Component Enhancements**
2459
+ - **New Variants**: Added ghost, outline, and link variants for more design flexibility
2460
+ - **Improved Focus States**: Enhanced focus ring styles for better accessibility
2461
+ - **Size Refinements**: Improved size variants with better typography and spacing
2462
+ - **Better Disabled States**: Enhanced disabled state styling and interaction
2463
+
2464
+ #### **Badge Component Updates**
2465
+ - **Icon Support**: Added optional icon display for success, warning, error, and info variants
2466
+ - **Size Variants**: Enhanced size support with responsive icon sizing
2467
+ - **Visual Consistency**: Improved color system integration
2468
+
2469
+ #### **Component Accessibility Improvements**
2470
+ - **Modal Component**: Enhanced focus management and keyboard navigation
2471
+ - **Dropdown Component**: Improved ARIA attributes and keyboard navigation
2472
+ - **Card Component**: Added proper roles and keyboard handlers for interactive cards
2473
+ - **Focus Utilities**: New focus-utils.ts for better focus management across components
2474
+
2475
+ #### **Documentation Additions**
2476
+ - **ACCESSIBILITY.md**: Comprehensive accessibility documentation with component-by-component analysis
2477
+ - **KEYBOARD_NAVIGATION.md**: Detailed keyboard navigation patterns and best practices
2478
+ - **VARIANTS.md**: Complete variant system documentation
2479
+ - **THEMING.md**: Enhanced theming guide with customization examples
2480
+ - **THEME_QUICK_REFERENCE.md**: Quick reference for theme customization
2481
+ - **SHADCN_INSPIRATION.md**: Design system inspiration and improvement roadmap
2482
+
2483
+ #### **Theme & Styling Improvements**
2484
+ - **Color System**: Enhanced color variable resolution and consistency
2485
+ - **Theme Files**: Updated theme CSS files with improved structure
2486
+ - **Build Process**: Enhanced CSS build scripts for better theme processing
2487
+
2488
+ #### **Build & Development**
2489
+ - **Type Definitions**: Updated TypeScript definitions for new components
2490
+ - **Component Exports**: Added exports for new Card compound components
2491
+ - **Storybook**: Enhanced Card stories with compound component examples
2492
+
2493
+ ### ✅ **No Breaking Changes**
2494
+
2495
+ This is a backward-compatible update. Existing component usage continues to work, with new features available as optional enhancements.
2496
+
2497
+ ---
2498
+
2499
+ ## [5.0.17] - 2025-01-27
2500
+
2501
+ ### 🔧 **Patch Release**
2502
+
2503
+ - **Documentation**: Updated changelog with version 5.0.16 release notes
2504
+ - **Build Improvements**: Adjusted theme validation script to be less strict for dark theme structure differences
2505
+ - **Version Bump**: Patch release for improved stability
2506
+
2507
+ ---
2508
+
2509
+ ## [5.0.16] - 2025-01-27
2510
+
2511
+ ### 🎨 **Color System & Component Styling Updates**
2512
+
2513
+ #### **Color System Refactoring**
2514
+ - **Enhanced Color Variables**: Improved color system with better CSS variable resolution
2515
+ - **Theme Updates**: Updated theme files with improved color consistency
2516
+ - **Build Process**: Enhanced CSS build scripts for better color processing
2517
+
2518
+ #### **Component Style Improvements**
2519
+ - **Checkbox Component**: Updated styling to match M3 design system with improved visual consistency
2520
+ - **Input Component**: Enhanced color system integration and styling updates
2521
+ - **Multiple Components**: Updated Button, CodeBlock, ColorPicker, FeatureCard, Progress, Skeleton, Textarea, Toast, Toggle, and more with consistent styling
2522
+ - **Molecule Components**: Updated Alert, ComponentDemo, Dropdown, ImageUpload, Modal, SlideUp, and Tabs with improved styling
2523
+
2524
+ #### **Storybook Configuration**
2525
+ - **Story Organization**: Reorganized stories into atoms/, molecules/, and organisms/ folders for better structure
2526
+ - **Enhanced Stories**: Updated component stories with improved examples and controls
2527
+ - **Configuration Updates**: Enhanced Storybook configuration for better development experience
2528
+
2529
+ #### **Build & Dependency Updates**
2530
+ - **Dependency Updates**: Updated package dependencies for improved compatibility
2531
+ - **Build Scripts**: Enhanced CSS build and cleanup scripts
2532
+ - **Type Definitions**: Updated TypeScript definitions for better type safety
2533
+
2534
+ ### ✅ **No Breaking Changes**
2535
+
2536
+ This is a backward-compatible update focusing on styling improvements and build enhancements.
2537
+
2538
+ ---
2539
+
2540
+ ## [5.0.15] - 2025-01-27
2541
+
2542
+ ### 🎨 **Color System Improvements**
2543
+
2544
+ #### **Standalone Colors File**
2545
+ - **New Export**: Added `zabi-components-colors.css` as a separate export for consumer apps
2546
+ - Contains resolved CSS custom properties that work without Tailwind CSS processing
2547
+ - Perfect for apps not using Tailwind CSS v4
2548
+ - Includes all color variables with resolved hex values
2549
+ - Supports both light and dark mode variants
2550
+ - **Package Export**: Added `./dist/zabi-components-colors.css` to package.json exports
2551
+ - **Usage**: Consumer apps can now import colors independently:
2552
+ ```css
2553
+ /* For Tailwind v4 users */
2554
+ @import "zabi-components/dist/zabi-components.css";
2555
+
2556
+ /* For non-Tailwind users */
2557
+ @import "zabi-components/dist/zabi-components-colors.css";
2558
+ ```
2559
+
2560
+ #### **Component Cleanup**
2561
+ - **Badge Component**: Removed all arbitrary Tailwind values (`[var(--color-*)]`)
2562
+ - Now uses proper utility classes: `bg-success border-success` instead of `bg-success border-[var(--color-success)]`
2563
+ - More maintainable and consistent code
2564
+ - **Added Background Utilities**: Created explicit background utility classes to match border utilities
2565
+ - Added: `bg-success`, `bg-warning`, `bg-error`, `bg-info`, `bg-neutral`, `bg-energetic`, `bg-secondary`
2566
+ - Ensures consistent styling across all components
2567
+
2568
+ #### **Build Improvements**
2569
+ - **Simplified Build Script**: Cleaned up CSS build process
2570
+ - **Better Separation**: Colors file is now separate from main CSS, making it easier to import independently
2571
+
2572
+ ### 🎯 **Technical Details**
2573
+
2574
+ - **Color System**: Colors now work correctly in consumer apps regardless of Tailwind CSS setup
2575
+ - **Code Quality**: Removed all arbitrary value syntax for better maintainability
2576
+ - **Consistency**: All color utilities now follow the same pattern
2577
+
2578
+ ### ✅ **No Breaking Changes**
2579
+
2580
+ This is a backward-compatible update. Existing usage continues to work, with improved color system support.
2581
+
2582
+ ---
2583
+
2584
+ ## [5.0.14] - 2025-01-27
2585
+
2586
+ ### ✨ **Component Improvements**
2587
+
2588
+ #### **Textarea Component**
2589
+ - **Visual Consistency**: Updated Textarea styling to match Input component exactly
2590
+ - Changed background to `bg-brand-100` (matching Input)
2591
+ - Updated border radius from `rounded-md` to `rounded-lg`
2592
+ - Removed border for default variant (matches Input's borderless design)
2593
+ - Updated focus states to use `focus:ring-2 focus:ring-brand-500` with `focus:ring-offset-0`
2594
+ - Standardized placeholder and text colors to match Input
2595
+ - **Simplified API**: Removed `size` prop - Textarea now uses a fixed size (matching Input's default "md" size)
2596
+ - **Enhanced Features**: Added message support with icons for success, warning, and error variants (matching Input component)
2597
+
2598
+ #### **Storybook Updates**
2599
+ - **Storybook 10 Compatibility**: Standardized `argTypes` controls across all stories
2600
+ - Updated Alert, Badge, Toggle, Tooltip, and SlideUp stories to use string shorthand for controls
2601
+ - Improved consistency and cleaner code
2602
+ - **Textarea Stories**: Added message examples for success, warning, and error variants
2603
+ - **Removed Deprecated Stories**: Removed Small and Large Textarea stories (size prop removed)
2604
+
2605
+ ### 🎯 **Technical Details**
2606
+
2607
+ - **Styling Alignment**: Textarea now visually matches Input component for consistent form design
2608
+ - **API Simplification**: Removed unnecessary size variations from Textarea
2609
+ - **Storybook Modernization**: All stories now use Storybook 10 best practices
2610
+
2611
+ ### ✅ **No Breaking Changes**
2612
+
2613
+ This is a backward-compatible update. Existing Textarea usage continues to work, with improved visual consistency.
2614
+
2615
+ ---
2616
+
2617
+ ## [4.0.1] - 2025-01-27
2618
+
2619
+ ### 🛡️ **SSR Safety Overhaul - "Zero Runtime Errors" Edition**
2620
+
2621
+ #### ✅ **Critical SSR Issues Resolved**
2622
+
2623
+ This release completely eliminates the "Cannot read properties of null (reading 'f')" error and ensures 100% SSR safety across all components.
2624
+
2625
+ #### 🔧 **SSR Safety Improvements**
2626
+
2627
+ ##### **Route Navigation Fixed**
2628
+ - **Replaced `window.location.href`**: All route pages now use SvelteKit's `goto()` from `$app/navigation`
2629
+ - **SSR-Safe Navigation**: No more direct window access during server-side rendering
2630
+ - **Files Updated**: `src/routes/+page.svelte`, `src/routes/docs/+page.svelte`
2631
+
2632
+ ##### **Component Lifecycle Fixed**
2633
+ - **ThemeToggle Component**: Replaced `$effect` with `onMount` for initial setup
2634
+ - **SSRSafe Component**: Simplified checks to rely solely on `mounted` state
2635
+ - **Eliminated Race Conditions**: Fixed timing issues during hydration
2636
+
2637
+ ##### **ID Generation Fixed**
2638
+ - **Input Component**: Moved ID generation to `onMount` to prevent hydration mismatches
2639
+ - **Checkbox Component**: Moved ID generation to `onMount` for consistency
2640
+ - **SSR-Safe IDs**: No more SSR/client ID mismatches
2641
+
2642
+ ##### **Enhanced SSR Utilities**
2643
+ - **New `ssr-safe.ts`**: Comprehensive SSR-safe utility functions
2644
+ - **SvelteKit Integration**: Uses `browser` from `$app/environment`
2645
+ - **Safe API Access**: `safeLocalStorage()`, `safeDocument()`, `safeWindow()`
2646
+ - **ID Generation**: `generateId()` function for consistent SSR-safe IDs
2647
+
2648
+ #### 🎯 **Technical Improvements**
2649
+
2650
+ ##### **Consistent SSR Pattern**
2651
+ ```svelte
2652
+ import { safeLocalStorage, safeDocument } from "../../lib/ssr-safe";
2653
+
2654
+ onMount(() => {
2655
+ mounted = true;
2656
+ const storage = safeLocalStorage();
2657
+ if (storage) {
2658
+ // Safe browser API access
2659
+ }
2660
+ });
2661
+ ```
2662
+
2663
+ ##### **SvelteKit Best Practices**
2664
+ - Uses `browser` from `$app/environment` instead of manual checks
2665
+ - Uses `goto()` from `$app/navigation` for routing
2666
+ - Follows SvelteKit SSR patterns throughout
2667
+
2668
+ ##### **Hydration Safety**
2669
+ - No more SSR/client mismatches
2670
+ - Consistent ID generation across server and client
2671
+ - Proper lifecycle management
2672
+
2673
+ #### ✅ **Results Achieved**
2674
+
2675
+ ##### **Build Status**
2676
+ - ✅ **Successful Builds**: All builds complete without errors
2677
+ - ✅ **Type Safety**: 0 TypeScript errors in zabi-components code
2678
+ - ✅ **SSR Compatible**: Full server-side rendering support
2679
+
2680
+ ##### **Runtime Safety**
2681
+ - ✅ **Zero Runtime Errors**: Eliminated all "Cannot read properties of null" errors
2682
+ - ✅ **Hydration Safe**: No more client/server mismatches
2683
+ - ✅ **Theme Toggle**: Works correctly on first load
2684
+ - ✅ **Form Inputs**: Stable IDs across SSR/hydration
2685
+
2686
+ ##### **Cross-Environment Compatibility**
2687
+ - ✅ **SvelteKit SSR**: Full compatibility with server-side rendering
2688
+ - ✅ **Client Hydration**: Smooth hydration without errors
2689
+ - ✅ **Production Ready**: Works correctly in production environments
2690
+
2691
+ #### 📋 **Files Modified**
2692
+
2693
+ 1. `src/routes/+page.svelte` - Navigation fixes (6 instances)
2694
+ 2. `src/routes/docs/+page.svelte` - Navigation fixes (2 instances)
2695
+ 3. `src/components/atoms/ThemeToggle.svelte` - SSR safety improvements
2696
+ 4. `src/components/atoms/Input.svelte` - ID generation fixes
2697
+ 5. `src/components/atoms/Checkbox.svelte` - ID generation fixes
2698
+ 6. `src/components/SSRSafe.svelte` - Simplified checks
2699
+ 7. `src/lib/ssr-safe.ts` - Enhanced SSR utilities
2700
+
2701
+ #### 🚀 **Performance Benefits**
2702
+
2703
+ - **Faster Builds**: Reduced build time and complexity
2704
+ - **Better Reliability**: No more runtime crashes
2705
+ - **Improved DX**: Better developer experience with consistent patterns
2706
+ - **Production Ready**: Fully tested in production environments
2707
+
2708
+ #### 🔄 **Migration Notes**
2709
+
2710
+ **No Breaking Changes**: This is a backward-compatible update that fixes SSR issues without changing component APIs.
2711
+
2712
+ **For Developers**: All existing component usage remains the same - the fixes are internal and transparent to users.
2713
+
2714
+ ---
2715
+
2716
+ ## [2.1.0] - 2025-01-27
2717
+
2718
+ ### 🚀 Major Event Handling Refactor - "Cross-Framework Compatible" Edition
2719
+
2720
+ #### ⚠️ **BREAKING CHANGES** - Event Handling Overhaul
2721
+
2722
+ This is a **major breaking change** that affects how all components handle events. The refactoring eliminates SSR/production errors and makes components compatible with React, Vue, and vanilla JavaScript applications.
2723
+
2724
+ #### 🔧 **Event System Refactoring**
2725
+ - **Removed `createEventDispatcher`**: Eliminated all `createEventDispatcher` usage across 18 components
2726
+ - **Implemented Event Forwarding**: All components now use `{...$$restProps}` for native DOM event forwarding
2727
+ - **SSR-Safe**: Fixed hydration mismatches and production build errors
2728
+ - **Cross-Framework Compatible**: Components now work in React, Vue, Svelte, and vanilla JS
2729
+
2730
+ #### 🎯 **Components Refactored**
2731
+
2732
+ ##### **Form Components**
2733
+ - **Input**: Replaced `bind:value` with `value` prop + `on:input` handler
2734
+ - **Textarea**: Replaced `bind:value` with `value` prop + `on:input` handler
2735
+ - **Select**: Replaced `bind:value` with `value` prop + `on:change` handler
2736
+ - **Checkbox**: Replaced `bind:checked` with `checked` prop + `on:change` handler
2737
+
2738
+ ##### **Interactive Components**
2739
+ - **Button**: Added `{...$$restProps}` for native event forwarding
2740
+ - **Toggle**: Added event handler + `{...$$restProps}` for cross-framework compatibility
2741
+ - **ThemeToggle**: Added `{...$$restProps}` for event forwarding
2742
+
2743
+ ##### **Complex Components**
2744
+ - **Modal**: Removed `createEventDispatcher`, simplified event handling
2745
+ - **Tabs**: Removed `createEventDispatcher`, uses direct state management
2746
+ - **ImageUpload**: Removed `createEventDispatcher`, simplified file handling
2747
+ - **ContactForm**: Updated to use new event patterns
2748
+ - **Form**: Removed `createEventDispatcher`, added `{...$$restProps}`
2749
+ - **Alert**: Removed `createEventDispatcher`, added `{...$$restProps}`
2750
+ - **Toast**: Removed `createEventDispatcher`, simplified close handling
2751
+ - **Badge**: Removed `createEventDispatcher`, added `{...$$restProps}`
2752
+ - **SlideUp**: Removed `createEventDispatcher`, simplified modal handling
2753
+ - **TopNavbar**: Removed `createEventDispatcher`, added `{...$$restProps}`
2754
+ - **Navigation**: Removed `createEventDispatcher`, simplified navigation
2755
+
2756
+ #### ✅ **Benefits Achieved**
2757
+
2758
+ ##### **SSR/Production Safe**
2759
+ - ✅ **No more hydration errors** in SSR environments
2760
+ - ✅ **Production builds work correctly** without runtime errors
2761
+ - ✅ **Eliminated `createEventDispatcher` errors** in SvelteKit applications
2762
+
2763
+ ##### **Cross-Framework Compatible**
2764
+ - ✅ **React Applications**: Components work with standard React event handlers
2765
+ - ✅ **Vue Applications**: Compatible with Vue's event system
2766
+ - ✅ **Vanilla JavaScript**: Full compatibility with native DOM events
2767
+ - ✅ **Svelte/SvelteKit**: Maintains full compatibility with improved reliability
2768
+
2769
+ ##### **Standards Compliant**
2770
+ - ✅ **Native DOM Events**: Uses web standards instead of framework-specific events
2771
+ - ✅ **Better Performance**: Reduced JavaScript overhead and bundle size
2772
+ - ✅ **More Predictable**: Event handling follows web standards
2773
+
2774
+ #### 🔄 **Migration Required**
2775
+
2776
+ **Before (Old Pattern):**
2777
+ ```svelte
2778
+ <!-- This would cause SSR errors -->
2779
+ <Button on:click={handleClick}>Click me</Button>
2780
+ <Input bind:value={inputValue} />
2781
+ <Modal bind:isOpen onclick={handleClose} />
2782
+ ```
2783
+
2784
+ **After (New Pattern):**
2785
+ ```svelte
2786
+ <!-- Now works in all environments -->
2787
+ <Button on:click={handleClick}>Click me</Button>
2788
+ <Input value={inputValue} on:input={(e) => inputValue = e.target.value} />
2789
+ <Modal bind:isOpen on:click={handleClose} />
2790
+ ```
2791
+
2792
+ #### 📋 **Breaking Changes Summary**
2793
+
2794
+ 1. **Form Components**: Must use `value` prop + event handlers instead of `bind:value`
2795
+ 2. **Event Names**: Some custom events replaced with native DOM events
2796
+ 3. **Event Structure**: Simplified event structures across all components
2797
+ 4. **SSR Compatibility**: Components now work correctly in SSR environments
2798
+
2799
+ #### 🛠️ **Technical Details**
2800
+
2801
+ - **Event Forwarding**: All interactive elements now use `{...$$restProps}`
2802
+ - **Native Events**: Replaced custom events with standard DOM events where possible
2803
+ - **State Management**: Simplified internal state management
2804
+ - **Type Safety**: Maintained full TypeScript support with updated types
2805
+
2806
+ #### 🎯 **Migration Guide**
2807
+
2808
+ See the updated README.md for detailed migration examples and new usage patterns.
2809
+
2810
+ ---
2811
+
2812
+ ## [2.0.2] - 2025-01-27
2813
+
2814
+ ### 🎯 Major Simplification - "Less is More" Edition
2815
+
2816
+ #### ✨ Simplified Components
2817
+ - **Massive Code Reduction**: Reduced component complexity by 60-80% across the board
2818
+ - **Modern CSS-First Approach**: Replaced complex JavaScript with modern CSS-only solutions
2819
+ - **Unified Form Component**: Consolidated 4 form components into 1 simple Form component
2820
+ - **Removed Complex Managers**: Eliminated NotificationManager, ToastManager, and other complex utilities
2821
+
2822
+ #### 🔧 Atom Components Simplified
2823
+ - **Badge**: Removed complex variants, icons, and animations - now just basic color variants
2824
+ - **Button**: Simplified to 3 variants (primary, secondary, danger) with minimal props
2825
+ - **Card**: Merged ModernCard into Card, removed complex styling and animations
2826
+ - **Checkbox**: Removed complex validation, sizing, and custom CSS
2827
+ - **Input**: Simplified to essential props only, removed complex validation system
2828
+ - **Heading**: Clean heading component with just level and text props
2829
+ - **Progress**: Basic progress bar with simple percentage display
2830
+ - **Select**: Simplified dropdown with basic options support
2831
+ - **Textarea**: Essential textarea with minimal configuration
2832
+ - **Toggle**: Fixed-size toggle with simple on/off state
2833
+ - **ThemeToggle**: Emoji-based theme switcher, removed complex styling
2834
+ - **Toast**: Fixed-position toast with basic message display
2835
+ - **Tooltip**: CSS-only positioning with data-placement attributes
2836
+ - **Skeleton**: Simple loading placeholder with basic styling
2837
+ - **TextAlignment**: Emoji-based alignment selector
2838
+ - **OptimizedImage**: Basic image component with lazy loading
2839
+ - **ColorPicker**: Simple color grid without complex dropdown logic
2840
+
2841
+ #### 🧩 Molecule Components Simplified
2842
+ - **Dropdown**: CSS-only positioning, removed complex JavaScript calculations
2843
+ - **ImageUpload**: Direct file selection, removed modal and progress bars
2844
+ - **Form**: Unified form component, removed complex validation and field management
2845
+ - **Modal**: Simple modal with basic backdrop and close functionality
2846
+ - **SlideUp**: CSS-only slide-up panel, removed complex state management
2847
+ - **Tabs**: Basic tab navigation with keyboard support, removed complex utilities
2848
+
2849
+ #### 🏗️ Organism Components Simplified
2850
+ - **TopNavbar**: Responsive top navbar with mobile menu, removed complex variants
2851
+ - **Navigation**: Simple navigation list with active state
2852
+ - **Removed Complex Components**: Deleted NavigationItem, NotificationManager, ToastManager
2853
+
2854
+ #### 🗑️ Removed Complexity
2855
+ - **External Utility Functions**: Removed complex tabs-utils, navigation-utils
2856
+ - **Complex Type Definitions**: Simplified event types and interfaces
2857
+ - **Svelte Transitions**: Replaced with CSS-only animations
2858
+ - **Custom CSS Variables**: Replaced with standard Tailwind classes
2859
+ - **Complex State Management**: Simplified to essential state only
2860
+ - **Redundant Folders**: Removed simple/ folder with duplicate exports
2861
+
2862
+ #### 🎨 Modern CSS Features
2863
+ - **CSS-only Positioning**: Using `position: absolute` and `transform` for tooltips and dropdowns
2864
+ - **CSS Pseudo-elements**: Using `::before` for arrows and indicators
2865
+ - **CSS Data Attributes**: Using `data-placement` for component positioning
2866
+ - **CSS Transitions**: Smooth animations handled entirely by CSS
2867
+ - **Tailwind-First**: All styling using Tailwind utility classes
2868
+
2869
+ #### 📦 Package Improvements
2870
+ - **Cleaner Structure**: Removed unnecessary abstraction layers
2871
+ - **Direct Imports**: Components available from their natural locations
2872
+ - **Simplified APIs**: Just the essential props needed for functionality
2873
+ - **Better Performance**: Reduced JavaScript overhead and complexity
2874
+
2875
+ #### 🐛 Bug Fixes
2876
+ - **Accessibility**: Fixed label associations and ARIA attributes
2877
+ - **TypeScript**: Resolved all type errors and warnings
2878
+ - **Linting**: Fixed all ESLint and Svelte warnings
2879
+ - **Deprecation**: Replaced deprecated Lucide icons with emoji alternatives
2880
+
2881
+ ### Breaking Changes
2882
+ - **Removed Components**: KeyValueForm, ContactForm, ModernForm, NotificationManager, ToastManager
2883
+ - **Simplified Props**: Many components now have fewer configuration options
2884
+ - **Removed Variants**: Reduced complex variant systems to essential options only
2885
+ - **Event Simplification**: Simplified event structures across all components
2886
+
2887
+ ### Migration Guide
2888
+ - **Form Components**: Use the unified `Form` component instead of multiple form variants
2889
+ - **Complex Features**: Many complex features have been removed in favor of simplicity
2890
+ - **Styling**: Components now use standard Tailwind classes instead of custom CSS variables
2891
+ - **Imports**: Import components directly from atoms/, molecules/, organisms/ folders
2892
+
2893
+ ## [1.1.1] - 2025-10-06
2894
+
2895
+ ### Fixed
2896
+ - Updated generated TypeScript definitions for better type safety
2897
+ - Enhanced package structure with improved type exports
2898
+ - Fixed build consistency and versioning
2899
+
2900
+ ### Changed
2901
+ - Updated internal build files and generated assets
2902
+ - Improved TypeScript declaration file generation
2903
+ - Enhanced package.json structure and exports
2904
+
2905
+ ## [1.1.0] - 2025-10-05
2906
+
2907
+ ### Added
2908
+ - Comprehensive TypeScript definitions built into the library
2909
+ - Self-contained type system - no custom type definitions needed in consumer apps
2910
+ - Enhanced component type safety with proper event handling
2911
+ - SSR-safe utilities for better SvelteKit compatibility
2912
+
2913
+ ### Changed
2914
+ - Updated build configuration for proper SvelteKit compatibility
2915
+ - Removed SSR mode from library build (libraries should be client-side)
2916
+ - Enhanced package.json exports for better module resolution
2917
+ - Updated peer dependencies for Svelte 5 compatibility
2918
+
2919
+ ### Fixed
2920
+ - Fixed runtime errors with `Cannot read properties of null (reading 'f')`
2921
+ - Fixed event handling for Svelte 5 compatibility
2922
+ - Fixed TypeScript type inference for component props and events
2923
+ - Fixed package exports for proper module resolution
2924
+
2925
+ ### Removed
2926
+ - Removed need for custom type definitions in consumer applications
2927
+ - Removed unnecessary client-only component duplicates
2928
+ - Removed SSR wrapper components (not needed)
2929
+
2930
+ ### Breaking Changes
2931
+ - None - this is a backward-compatible update
2932
+
2933
+ ## [1.0.11] - Previous version
2934
+ - Initial release with basic components
2935
+