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
@@ -1,10 +1,28 @@
1
1
  <script lang="ts">
2
- import { CheckCircle, AlertTriangle, AlertCircle } from '@lucide/svelte';
2
+ import type { HTMLTextareaAttributes } from 'svelte/elements';
3
3
  import type { SemanticVariant, SizeVariant } from '../types/variants.js';
4
4
  import { generateId } from "../util/ssr-safe.js";
5
5
  import { cn } from "../util/cn.js";
6
+ import { fieldDescribedBy, fieldMessageState, type TextareaCount } from "../util/field.js";
7
+ import FieldMessages from "./FieldMessages.svelte";
6
8
 
7
- interface Props {
9
+ /**
10
+ * Other attributes (`autocomplete`, `inputmode`, `maxlength`,
11
+ * `enterkeyhint`, `data-*`, `onchange`, ...) land on the `<textarea>`.
12
+ */
13
+ type Props = Omit<
14
+ HTMLTextareaAttributes,
15
+ | 'class'
16
+ | 'value'
17
+ | 'id'
18
+ | 'name'
19
+ | 'placeholder'
20
+ | 'required'
21
+ | 'disabled'
22
+ | 'rows'
23
+ | 'oninput'
24
+ | 'aria-describedby'
25
+ > & {
8
26
  id?: string;
9
27
  value?: string;
10
28
  name?: string;
@@ -16,15 +34,42 @@
16
34
  disabled?: boolean;
17
35
  loading?: boolean;
18
36
  rows?: number;
37
+ /**
38
+ * The resize grip. `auto` (default): the browser's vertical grip with a mouse or
39
+ * trackpad, none on a touch screen, where dragging it scrolls the page. `vertical`
40
+ * shows it everywhere, `none` never. With `autogrow` there is no grip.
41
+ */
42
+ resize?: 'auto' | 'none' | 'vertical';
43
+ /** The field grows with its content, from `rows` up to `maxRows`, and scrolls after that. */
44
+ autogrow?: boolean;
45
+ /** The tallest an `autogrow` field gets, in rows. Without it the field has no limit. */
46
+ maxRows?: number;
19
47
  size?: SizeVariant;
20
48
  variant?: SemanticVariant;
49
+ /** Status text under the field. Shown with a `success`, `warning` or `error` variant; for neutral help, use `hint`. */
21
50
  message?: string;
51
+ /** Help text under the field, read out with it. */
52
+ hint?: string;
53
+ /** An error under the field. It marks the field invalid and is announced; it wins over `variant` and `message`. */
54
+ error?: string;
55
+ /**
56
+ * A character count under the field, at the end of the line the hint is on. It needs
57
+ * `maxlength`; without one nothing is shown. It is part of the field's description
58
+ * (`aria-describedby`) and is not a live region, so it is not read out at every key.
59
+ */
60
+ count?: boolean;
61
+ /** The count is shown from this many characters on. `0` (default): always. */
62
+ countFrom?: number;
63
+ /** The words of the count, for the page's language. Default: "12 / 2000". */
64
+ countText?: (count: TextareaCount) => string;
22
65
  oninput?: (event: Event) => void;
23
- }
66
+ /** Ids of other elements that describe the field. The hint and the message are added after them. */
67
+ "aria-describedby"?: string | null;
68
+ };
24
69
 
25
70
  let {
26
71
  id: idProp,
27
- value = $bindable(''),
72
+ value = $bindable<Exclude<Props["value"], undefined>>(),
28
73
  name = '',
29
74
  class: className = '',
30
75
  label = '',
@@ -34,56 +79,113 @@
34
79
  disabled = false,
35
80
  loading = false,
36
81
  rows = 4,
82
+ resize = 'auto',
83
+ autogrow = false,
84
+ maxRows,
37
85
  size = 'md',
38
86
  variant = 'default',
39
87
  message = '',
88
+ hint = '',
89
+ error = '',
90
+ count = false,
91
+ countFrom = 0,
92
+ countText,
40
93
  oninput,
94
+ 'aria-describedby': describedBy,
41
95
  ...restProps
42
96
  }: Props = $props();
43
97
 
98
+ // No fallback on a bindable prop: Svelte refuses `bind:…={undefined}` on
99
+ // one that has a fallback (`props_invalid_value`), and a page that throws
100
+ // while it hydrates never becomes interactive. The default is applied
101
+ // here instead: at once, for the server and the first render, and again
102
+ // whenever a parent hands back `undefined`.
103
+ const applyDefaults = () => {
104
+ if (value === undefined) value = '';
105
+ };
106
+ applyDefaults();
107
+ $effect.pre(applyDefaults);
108
+
44
109
  const fallbackId = generateId('textarea');
45
110
  const textareaId = $derived(idProp ?? fallbackId);
46
111
  const isDisabled = $derived(disabled || loading);
47
112
 
113
+ const status = $derived(fieldMessageState({ variant, message, hint, error }));
114
+
115
+ // The count: only with a `maxlength`, and only once `countFrom` is reached.
116
+ // The length is the one `maxlength` counts (UTF-16 units).
117
+ const countMax = $derived(Number(restProps.maxlength));
118
+ const countLength = $derived((value ?? '').length);
119
+ const showCount = $derived(
120
+ count && Number.isFinite(countMax) && countMax > 0 && countLength >= countFrom,
121
+ );
122
+ const countLabel = $derived.by(() => {
123
+ if (!showCount) return '';
124
+ const state = { length: countLength, max: countMax, remaining: Math.max(0, countMax - countLength) };
125
+ return countText ? countText(state) : `${state.length} / ${state.max}`;
126
+ });
127
+ const countId = $derived(`${textareaId}-count`);
128
+ // The caller's ids, the hint and the message, then the count.
129
+ const fieldDescription = $derived(
130
+ [fieldDescribedBy(textareaId, status, describedBy), showCount ? countId : '']
131
+ .filter(Boolean)
132
+ .join(' ') || undefined,
133
+ );
134
+
48
135
  const variantClass = $derived(() => {
49
- return variant === 'success'
136
+ return status.variant === 'success'
50
137
  ? 'border-success focus-visible:border-success'
51
- : variant === 'warning'
138
+ : status.variant === 'warning'
52
139
  ? 'border-warning focus-visible:border-warning'
53
- : variant === 'error'
140
+ : status.variant === 'error'
54
141
  ? 'border-error focus-visible:border-error'
55
- : 'border-input-border';
142
+ : 'border-input-border enabled:hover:border-input-border-hover';
143
+ });
144
+
145
+ const resizeClass = $derived(
146
+ autogrow || resize === 'none'
147
+ ? 'resize-none'
148
+ : resize === 'vertical'
149
+ ? 'resize-y'
150
+ : 'resize-y pointer-coarse:resize-none',
151
+ );
152
+
153
+ // Autogrow: `field-sizing: content` where the browser has it, held between
154
+ // `rows` and `maxRows` lines (`leading-6`, plus `py-2` and the two borders,
155
+ // since the box is border-box). Elsewhere the height is measured from the
156
+ // content after each change; the same two limits clamp it.
157
+ const linesHeight = (lines: number) => `calc(${lines * 1.5 + 1}rem + 2px)`;
158
+ const minHeight = $derived(autogrow ? linesHeight(Math.max(1, rows)) : undefined);
159
+ const maxHeight = $derived(
160
+ autogrow && maxRows !== undefined ? linesHeight(Math.max(rows, maxRows)) : undefined,
161
+ );
162
+
163
+ let element: HTMLTextAreaElement | undefined = $state();
164
+
165
+ $effect(() => {
166
+ void value;
167
+ if (!autogrow || !element) return;
168
+ if (typeof CSS !== 'undefined' && CSS.supports?.('field-sizing', 'content')) return;
169
+ const field = element;
170
+ field.style.height = 'auto';
171
+ field.style.height = `${field.scrollHeight + field.offsetHeight - field.clientHeight}px`;
172
+ return () => {
173
+ field.style.height = '';
174
+ };
56
175
  });
57
176
 
177
+ // 16px below `sm`: iOS Safari zooms the page when a focused field is smaller.
58
178
  const textareaClasses = $derived(() => {
59
179
  const baseClasses =
60
- 'focus-ring w-full border bg-input hover:bg-input-hover focus-visible:bg-input-focus disabled:bg-input-disabled rounded-control transition-colors duration-150 placeholder:text-input-placeholder text-body focus:outline-none focus-visible:outline-none disabled:text-action-disabled-text disabled:cursor-not-allowed resize-y px-3 py-2 text-sm leading-6';
180
+ 'focus-ring w-full border bg-input hover:bg-input-hover focus-visible:bg-input-focus disabled:bg-input-disabled rounded-control transition-colors duration-150 placeholder:text-input-placeholder text-body focus:outline-none focus-visible:outline-none disabled:text-action-disabled-text disabled:cursor-not-allowed px-3 py-2 text-sm max-sm:text-base leading-6';
61
181
 
62
- return cn(`${baseClasses} ${variantClass()} ${className}`);
182
+ return cn(`${baseClasses} ${resizeClass} ${variantClass()} ${className}`);
63
183
  });
64
184
 
65
185
  const labelClasses = $derived(
66
186
  () => 'mb-1 block text-sm font-medium text-label',
67
187
  );
68
188
 
69
- const messageClasses = $derived(() => {
70
- if (variant === 'error') {
71
- return 'mt-1 flex items-center gap-2 text-sm text-error';
72
- } else if (variant === 'success') {
73
- return 'mt-1 flex items-center gap-2 text-sm text-success';
74
- } else if (variant === 'warning') {
75
- return 'mt-1 flex items-center gap-2 text-sm text-warning';
76
- }
77
- return 'mt-1 flex items-center gap-2 text-sm text-description';
78
- });
79
-
80
- const getIcon = $derived(() => {
81
- if (variant === 'error') return AlertCircle;
82
- if (variant === 'success') return CheckCircle;
83
- if (variant === 'warning') return AlertTriangle;
84
- return null;
85
- });
86
-
87
189
  function handleInput(event: Event) {
88
190
  const target = event.target as HTMLTextAreaElement;
89
191
  value = target.value;
@@ -97,6 +199,7 @@
97
199
  {/if}
98
200
  <div class="relative">
99
201
  <textarea
202
+ bind:this={element}
100
203
  id={textareaId}
101
204
  {name}
102
205
  bind:value
@@ -105,11 +208,15 @@
105
208
  disabled={isDisabled}
106
209
  {rows}
107
210
  class={textareaClasses()}
211
+ style:field-sizing={autogrow ? 'content' : undefined}
212
+ style:min-height={minHeight}
213
+ style:max-height={maxHeight}
214
+ style:overflow-y={maxHeight ? 'auto' : undefined}
108
215
  oninput={handleInput}
109
- aria-invalid={variant === 'error' ? 'true' : undefined}
216
+ aria-invalid={status.variant === 'error' ? 'true' : undefined}
110
217
  aria-required={required ? 'true' : undefined}
111
218
  aria-busy={loading ? 'true' : undefined}
112
- aria-describedby={message ? `${textareaId}-message` : undefined}
219
+ aria-describedby={fieldDescription}
113
220
  {...restProps}
114
221
  ></textarea>
115
222
  {#if loading}
@@ -118,23 +225,22 @@
118
225
  aria-hidden="true"
119
226
  >
120
227
  <span
121
- class="inline-block size-5 shrink-0 animate-spin rounded-full border-2 border-current border-t-transparent opacity-70"
228
+ class="inline-block size-5 shrink-0 animate-spin rounded-full border-2 border-current border-t-transparent opacity-70 motion-reduce:animate-pulse"
122
229
  ></span>
123
230
  </span>
124
231
  {/if}
125
232
  </div>
126
- {#if message && variant !== 'default'}
127
- <p
128
- id={`${textareaId}-message`}
129
- class={messageClasses()}
130
- role={variant === 'error' ? 'alert' : 'status'}
131
- aria-live={variant === 'error' ? 'assertive' : 'polite'}
132
- >
133
- {#if getIcon()}
134
- {@const Icon = getIcon()}
135
- <Icon size={14} class="shrink-0" />
136
- {/if}
137
- <span>{message}</span>
138
- </p>
233
+ {#if showCount}
234
+ <!-- The count ends the line the hint starts. Not a live region: it is read with the field. -->
235
+ <div class="flex items-start gap-3">
236
+ <div class="min-w-0 flex-1">
237
+ <FieldMessages fieldId={textareaId} {status} {hint} gap="mt-1" />
238
+ </div>
239
+ <p id={countId} class="mt-1 shrink-0 text-sm text-description tabular-nums" data-textarea-count>
240
+ {countLabel}
241
+ </p>
242
+ </div>
243
+ {:else}
244
+ <FieldMessages fieldId={textareaId} {status} {hint} gap="mt-1" />
139
245
  {/if}
140
246
  </div>
@@ -1,5 +1,11 @@
1
+ import type { HTMLTextareaAttributes } from 'svelte/elements';
1
2
  import type { SemanticVariant, SizeVariant } from '../types/variants.js';
2
- interface Props {
3
+ import { type TextareaCount } from "../util/field.js";
4
+ /**
5
+ * Other attributes (`autocomplete`, `inputmode`, `maxlength`,
6
+ * `enterkeyhint`, `data-*`, `onchange`, ...) land on the `<textarea>`.
7
+ */
8
+ type Props = Omit<HTMLTextareaAttributes, 'class' | 'value' | 'id' | 'name' | 'placeholder' | 'required' | 'disabled' | 'rows' | 'oninput' | 'aria-describedby'> & {
3
9
  id?: string;
4
10
  value?: string;
5
11
  name?: string;
@@ -11,11 +17,38 @@ interface Props {
11
17
  disabled?: boolean;
12
18
  loading?: boolean;
13
19
  rows?: number;
20
+ /**
21
+ * The resize grip. `auto` (default): the browser's vertical grip with a mouse or
22
+ * trackpad, none on a touch screen, where dragging it scrolls the page. `vertical`
23
+ * shows it everywhere, `none` never. With `autogrow` there is no grip.
24
+ */
25
+ resize?: 'auto' | 'none' | 'vertical';
26
+ /** The field grows with its content, from `rows` up to `maxRows`, and scrolls after that. */
27
+ autogrow?: boolean;
28
+ /** The tallest an `autogrow` field gets, in rows. Without it the field has no limit. */
29
+ maxRows?: number;
14
30
  size?: SizeVariant;
15
31
  variant?: SemanticVariant;
32
+ /** Status text under the field. Shown with a `success`, `warning` or `error` variant; for neutral help, use `hint`. */
16
33
  message?: string;
34
+ /** Help text under the field, read out with it. */
35
+ hint?: string;
36
+ /** An error under the field. It marks the field invalid and is announced; it wins over `variant` and `message`. */
37
+ error?: string;
38
+ /**
39
+ * A character count under the field, at the end of the line the hint is on. It needs
40
+ * `maxlength`; without one nothing is shown. It is part of the field's description
41
+ * (`aria-describedby`) and is not a live region, so it is not read out at every key.
42
+ */
43
+ count?: boolean;
44
+ /** The count is shown from this many characters on. `0` (default): always. */
45
+ countFrom?: number;
46
+ /** The words of the count, for the page's language. Default: "12 / 2000". */
47
+ countText?: (count: TextareaCount) => string;
17
48
  oninput?: (event: Event) => void;
18
- }
49
+ /** Ids of other elements that describe the field. The hint and the message are added after them. */
50
+ "aria-describedby"?: string | null;
51
+ };
19
52
  declare const Textarea: import("svelte").Component<Props, {}, "value">;
20
53
  type Textarea = ReturnType<typeof Textarea>;
21
54
  export default Textarea;
@@ -1,88 +1,251 @@
1
1
  <script lang="ts">
2
- import { onMount } from "svelte";
3
- import { Sun, Moon } from "@lucide/svelte";
2
+ import { mergeStrings } from "../util/ready-made-strings.js";
3
+ import { zabiStringsFor } from "../util/zabi-strings.js";
4
+ import { onMount, untrack } from "svelte";
5
+ import type { HTMLButtonAttributes } from "svelte/elements";
6
+ import Sun from "@lucide/svelte/icons/sun";
7
+ import Moon from "@lucide/svelte/icons/moon";
8
+ import Monitor from "@lucide/svelte/icons/monitor";
4
9
  import { FOCUS_BRAND_CLASS } from "../util/focus-utils.js";
5
10
  import { cn } from "../util/cn.js";
6
-
7
- function safeLocalStorage(): Storage | undefined {
8
- return typeof window !== "undefined" ? localStorage : undefined;
9
- }
10
-
11
- function safeDocument(): Document | undefined {
12
- return typeof window !== "undefined" ? document : undefined;
13
- }
11
+ import {
12
+ DEFAULT_THEME_STORAGE_KEY,
13
+ THEME_MODES,
14
+ getStoredThemeMode,
15
+ getThemeMode,
16
+ isThemeDark,
17
+ isThemeMode,
18
+ setThemeMode,
19
+ storeThemeMode,
20
+ type ThemeMode,
21
+ type ThemeToggleLabels,
22
+ } from "../util/theme-mode.js";
14
23
 
15
- interface Props {
24
+ /** Other attributes (`data-*`, `aria-*`, `id`, ...) land on the `<button>`. */
25
+ type Props = Omit<
26
+ HTMLButtonAttributes,
27
+ "class" | "disabled" | "type" | "onclick" | "children"
28
+ > & {
16
29
  /** Extra classes for the host element. */
17
30
  class?: string;
18
31
  size?: "sm" | "md" | "lg";
19
32
  variant?: "default" | "ghost" | "outline";
20
33
  disabled?: boolean;
34
+ /**
35
+ * `"two"` (the default) flips light and dark. `"three"` steps through
36
+ * system, light and dark, and always writes `data-theme` on `<html>`.
37
+ */
38
+ modes?: "two" | "three";
39
+ /**
40
+ * The page's mode: `"auto"` (follow the system), `"light"` or `"dark"`.
41
+ * Bindable. It is read from `<html>` when the button mounts and follows
42
+ * the page from then on; assigning it switches the page.
43
+ */
44
+ mode?: ThemeMode;
45
+ /** Called with the new mode when a press changes it. */
46
+ onmodechange?: (mode: ThemeMode) => void;
47
+ /**
48
+ * The `localStorage` key the choice is kept under and restored from when
49
+ * the button mounts. `null` keeps nothing.
50
+ */
51
+ storageKey?: string | null;
52
+ /** Replaces the texts of the accessible name, for another language. */
53
+ labels?: Partial<ThemeToggleLabels>;
21
54
  onclick?: (event: Event) => void;
22
- }
55
+ };
23
56
 
24
57
  let {
25
58
  class: className = "",
26
59
  size = "md",
27
60
  variant = "default",
28
61
  disabled = false,
62
+ modes = "two",
63
+ mode = $bindable(),
64
+ onmodechange,
65
+ storageKey = DEFAULT_THEME_STORAGE_KEY,
66
+ labels,
29
67
  onclick,
30
68
  ...restProps
31
69
  }: Props = $props();
32
70
 
71
+ const DEFAULT_LABELS: ThemeToggleLabels = {
72
+ auto: "system",
73
+ light: "light",
74
+ dark: "dark",
75
+ describe: (current, next) => `Theme: ${current}. Switch to ${next}`,
76
+ darkMode: "Dark mode",
77
+ beforeMount: "Theme toggle",
78
+ };
79
+ /** The app-wide words for this component, from a `ZabiStringsProvider` above it, if there is one. */
80
+ const provided = zabiStringsFor("themeToggle");
81
+ const text = $derived(mergeStrings(DEFAULT_LABELS, provided(), labels));
82
+
83
+ /** Whether the page is dark right now, whichever way it is switched. */
33
84
  let isDark = $state(false);
85
+ /** The page's mode as last read from `<html>`. */
86
+ let pageMode = $state<ThemeMode>("light");
34
87
  let mounted = $state(false);
35
88
 
89
+ const SYSTEM_DARK = "(prefers-color-scheme: dark)";
90
+
91
+ /** A page that carries `data-theme` on `<html>` is switched through it; any other page through the class. */
92
+ function usesAttribute(root: HTMLElement): boolean {
93
+ return root.hasAttribute("data-theme");
94
+ }
95
+
96
+ /** Reads the page into the button. The only place `mode` is set from the page. */
97
+ function readPage() {
98
+ isDark = isThemeDark();
99
+ pageMode = getThemeMode();
100
+ mode = pageMode;
101
+ }
102
+
103
+ /**
104
+ * Puts the page in `next`. With three modes that is always `data-theme`.
105
+ * With two, "auto" can only be said with the attribute, and light or dark
106
+ * go to whichever mechanism the page uses, as they always have.
107
+ */
108
+ function applyMode(next: ThemeMode) {
109
+ if (modes === "three" || next === "auto") {
110
+ setThemeMode(next, { storageKey });
111
+ } else {
112
+ isDark = next === "dark";
113
+ updateTheme();
114
+ storeThemeMode(next, storageKey);
115
+ }
116
+ readPage();
117
+ }
118
+
36
119
  onMount(() => {
37
120
  mounted = true;
38
- const doc = safeDocument();
39
- if (doc) {
40
- isDark = doc.documentElement.classList.contains("dark");
121
+ if (typeof document === "undefined") return;
122
+ const root = document.documentElement;
123
+
124
+ clearInlineScheme(root);
125
+
126
+ // The choice made last time, unless a script in <head> has applied it
127
+ // already (`themeInitScript`) or the app passed a mode of its own.
128
+ const asked = isThemeMode(mode) ? mode : getStoredThemeMode(storageKey);
129
+ if (asked && asked !== getThemeMode() && (modes === "three" || asked !== "auto" || usesAttribute(root))) {
130
+ applyMode(asked);
131
+ } else {
132
+ readPage();
41
133
  }
42
134
 
43
- if (!window.matchMedia) return;
135
+ // The page can be switched by something else (another control bound to
136
+ // the same helpers, the app's own script): the icon and the name follow.
137
+ const observer = typeof MutationObserver === "undefined" ? undefined : new MutationObserver(readPage);
138
+ observer?.observe(root, { attributes: true, attributeFilter: ["class", "data-theme"] });
44
139
 
45
- const media = window.matchMedia("(prefers-color-scheme: dark)");
140
+ const media = window.matchMedia?.(SYSTEM_DARK);
46
141
  const handleSystemChange = (event: MediaQueryListEvent) => {
47
- const storage = safeLocalStorage();
48
- if (storage && storage.getItem("theme")) return;
142
+ if (modes === "three" || usesAttribute(root)) {
143
+ // `data-theme="auto"` follows the system in the stylesheet, and
144
+ // "light" or "dark" is a choice: either way only the reading changes.
145
+ readPage();
146
+ return;
147
+ }
148
+ if (getStoredThemeMode(storageKey)) return;
49
149
  isDark = event.matches;
50
150
  updateTheme();
151
+ readPage();
152
+ };
153
+ media?.addEventListener("change", handleSystemChange);
154
+
155
+ return () => {
156
+ observer?.disconnect();
157
+ media?.removeEventListener("change", handleSystemChange);
51
158
  };
159
+ });
52
160
 
53
- media.addEventListener("change", handleSystemChange);
54
- return () => media.removeEventListener("change", handleSystemChange);
161
+ // `mode` assigned from outside (a bound value, the app's own control).
162
+ $effect(() => {
163
+ const wanted = mode;
164
+ if (!mounted || !isThemeMode(wanted)) return;
165
+ untrack(() => {
166
+ if (wanted !== pageMode) applyMode(wanted);
167
+ });
55
168
  });
56
169
 
170
+ /** What a press leads to. */
171
+ const nextMode = $derived<ThemeMode>(
172
+ modes === "three"
173
+ ? THEME_MODES[(THEME_MODES.indexOf(pageMode) + 1) % THEME_MODES.length]
174
+ : isDark
175
+ ? "light"
176
+ : "dark",
177
+ );
178
+
179
+ /**
180
+ * Two modes: a switch with one name, "Dark mode", and `aria-pressed` for its
181
+ * state. The name used to flip as well ("Switch to dark mode", then "Switch
182
+ * to light mode, pressed"), and "pressed" on "Switch to light mode" says
183
+ * two things at once. Three modes are steps, not on and off, so there the
184
+ * name carries the state and there is no `aria-pressed`.
185
+ */
186
+ const accessibleName = $derived(modes === "three" ? text.describe(text[pageMode], text[nextMode]) : text.darkMode);
187
+
57
188
  function toggleTheme(event: Event) {
58
189
  if (disabled) return;
59
- isDark = !isDark;
60
- updateTheme();
61
- const storage = safeLocalStorage();
62
- if (mounted && storage) {
63
- storage.setItem("theme", isDark ? "dark" : "light");
64
- }
65
-
190
+ applyMode(nextMode);
191
+ onmodechange?.(pageMode);
66
192
  onclick?.(event);
67
193
  }
68
194
 
195
+ /** Two modes, light or dark: the attribute where the page has it, the class otherwise. */
69
196
  function updateTheme() {
70
- const doc = safeDocument();
71
- if (mounted && doc) {
72
- if (isDark) {
73
- doc.documentElement.classList.add("dark");
74
- doc.documentElement.style.colorScheme = "dark";
75
- } else {
76
- doc.documentElement.classList.remove("dark");
77
- doc.documentElement.style.colorScheme = "light";
78
- }
197
+ if (!mounted || typeof document === "undefined") return;
198
+ const root = document.documentElement;
199
+ if (usesAttribute(root)) {
200
+ // An explicit choice, also when the page was on "auto". The theme
201
+ // sets `color-scheme` for each value, so no inline style is written.
202
+ root.setAttribute("data-theme", isDark ? "dark" : "light");
203
+ // A leftover class would keep the page dark under data-theme="light".
204
+ if (!isDark) root.classList.remove("dark");
205
+ return;
79
206
  }
207
+ // The class only. The theme sets `color-scheme` for it (dark under
208
+ // `.dark`, light on the root otherwise), so no inline value is written:
209
+ // one left behind would outrank the stylesheet, and the next time the
210
+ // app added or removed the class itself the tokens would change and
211
+ // the native controls would not.
212
+ root.classList.toggle("dark", isDark);
213
+ clearInlineScheme(root);
214
+ }
215
+
216
+ /**
217
+ * Removes an inline `color-scheme` of "light" or "dark" from `<html>`: what
218
+ * this button wrote before the theme set the scheme itself, and what a
219
+ * start-up script copied from the old docs writes. Any other inline value
220
+ * is the app's own and is left alone.
221
+ */
222
+ function clearInlineScheme(root: HTMLElement) {
223
+ const inline = root.style.colorScheme;
224
+ if (inline === "light" || inline === "dark") root.style.removeProperty("color-scheme");
80
225
  }
81
226
 
227
+ /**
228
+ * Before mount every icon is rendered and CSS shows one, by the selectors
229
+ * the theme itself is switched with. Written out in full, because Tailwind
230
+ * only generates a class it can read in the source.
231
+ *
232
+ * Two modes: sun on a light page, moon on a dark one (the class,
233
+ * `data-theme="dark"`, or `data-theme="auto"` on a dark system).
234
+ * Three modes show the mode, not the result: the monitor for
235
+ * `data-theme="auto"` whatever the system is.
236
+ */
237
+ const SUN_BEFORE_MOUNT =
238
+ "[.dark_&]:hidden [[data-theme=dark]_&]:hidden [@media(prefers-color-scheme:dark)]:[[data-theme=auto]_&]:hidden";
239
+ const MOON_BEFORE_MOUNT =
240
+ "hidden [.dark_&]:block [[data-theme=dark]_&]:block [@media(prefers-color-scheme:dark)]:[[data-theme=auto]_&]:block";
241
+ const SUN_BEFORE_MOUNT_THREE = "[.dark_&]:hidden [[data-theme=dark]_&]:hidden [[data-theme=auto]_&]:hidden";
242
+ const MOON_BEFORE_MOUNT_THREE = "hidden [.dark_&]:block [[data-theme=dark]_&]:block";
243
+ const SYSTEM_BEFORE_MOUNT_THREE = "hidden [[data-theme=auto]_&]:block";
244
+
82
245
  const sizeClass = $derived(() => {
83
246
  if (size === "sm") {
84
247
  return {
85
- button: "w-8 h-8",
248
+ button: "w-8 h-8 pointer-coarse:min-h-11 pointer-coarse:min-w-11",
86
249
  icon: 16
87
250
  };
88
251
  } else if (size === "lg") {
@@ -92,7 +255,7 @@
92
255
  };
93
256
  } else {
94
257
  return {
95
- button: "w-10 h-10",
258
+ button: "w-10 h-10 pointer-coarse:min-h-11 pointer-coarse:min-w-11",
96
259
  icon: 20
97
260
  };
98
261
  }
@@ -135,13 +298,16 @@
135
298
  <button
136
299
  onclick={toggleTheme}
137
300
  class="{buttonClasses()} {className}"
138
- aria-label={isDark ? "Switch to light mode" : "Switch to dark mode"}
139
- aria-pressed={isDark}
301
+ aria-label={accessibleName}
302
+ aria-pressed={modes === "three" ? undefined : isDark}
303
+ data-theme-mode={modes === "three" ? pageMode : undefined}
140
304
  type="button"
141
305
  {disabled}
142
306
  {...restProps}
143
307
  >
144
- {#if isDark}
308
+ {#if modes === "three" && pageMode === "auto"}
309
+ <Monitor size={sizeClass().icon} class="text-label" />
310
+ {:else if modes === "three" ? pageMode === "dark" : isDark}
145
311
  <Moon size={sizeClass().icon} class="text-label" />
146
312
  {:else}
147
313
  <Sun size={sizeClass().icon} class="text-label" />
@@ -150,11 +316,24 @@
150
316
  {:else}
151
317
  <button
152
318
  class="{sizeClass().button} {variantClass()} rounded-control flex items-center justify-center text-label cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
153
- aria-label="Theme toggle"
319
+ aria-label={text.beforeMount}
154
320
  type="button"
155
321
  {disabled}
156
322
  {...restProps}
157
323
  >
158
- <Sun size={sizeClass().icon} class="text-label" />
324
+ <!-- Before mount there is no state to read: the server cannot know the
325
+ theme, and hydration has not run yet. Every icon is in the markup and
326
+ the stylesheet picks one, so a dark page never shows the Sun first:
327
+ by the `dark` class, by `data-theme="dark"`, or by `data-theme="auto"`
328
+ (the moon on a dark system with two modes, the monitor with three).
329
+ The icon matches the mounted button. -->
330
+ {#if modes === "three"}
331
+ <Sun size={sizeClass().icon} class="text-label {SUN_BEFORE_MOUNT_THREE}" />
332
+ <Moon size={sizeClass().icon} class="text-label {MOON_BEFORE_MOUNT_THREE}" />
333
+ <Monitor size={sizeClass().icon} class="text-label {SYSTEM_BEFORE_MOUNT_THREE}" />
334
+ {:else}
335
+ <Sun size={sizeClass().icon} class="text-label {SUN_BEFORE_MOUNT}" />
336
+ <Moon size={sizeClass().icon} class="text-label {MOON_BEFORE_MOUNT}" />
337
+ {/if}
159
338
  </button>
160
339
  {/if}