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,12 +1,41 @@
1
1
  <script lang="ts">
2
- import type { Snippet } from "svelte";
3
- import type { HTMLButtonAttributes } from "svelte/elements";
4
- import type { ButtonVariant, SizeVariant } from "../types/variants.js";
2
+ import { tick, untrack, type Snippet } from "svelte";
3
+ import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
4
+ import type { ButtonSize, ButtonVariant } from "../types/variants.js";
5
5
  import { cn } from "../util/cn.js";
6
6
 
7
+ /**
8
+ * One type for the button and the link, not a union of the two elements'
9
+ * attributes: that union is too large for TypeScript to carry through a
10
+ * wrapper such as a Storybook `Meta`. Event handlers are typed for the
11
+ * button; on a link they are called with the same event.
12
+ */
7
13
  type Props = Omit<HTMLButtonAttributes, "class"> & {
14
+ /**
15
+ * Where the link goes. Makes this an `<a>` that looks the same;
16
+ * without it, it is a `<button>`. A link cannot be disabled: while
17
+ * `disabled` or `loading` it has no `href`, is `aria-disabled` and
18
+ * does nothing when pressed. While `loading` it stays a Tab stop.
19
+ */
20
+ href?: string;
21
+ /** With `href`: where the link opens. */
22
+ target?: HTMLAnchorAttributes["target"];
23
+ /** With `href`: the link's relationship, such as `noopener`. */
24
+ rel?: HTMLAnchorAttributes["rel"];
25
+ /** With `href`: download the address instead of opening it. */
26
+ download?: HTMLAnchorAttributes["download"];
27
+ hreflang?: HTMLAnchorAttributes["hreflang"];
28
+ referrerpolicy?: HTMLAnchorAttributes["referrerpolicy"];
29
+ ping?: HTMLAnchorAttributes["ping"];
8
30
  variant?: ButtonVariant;
9
- size?: SizeVariant;
31
+ /** `xl` is 56px, for a primary action on a phone. It is a Button size only. */
32
+ size?: ButtonSize;
33
+ /**
34
+ * Shows a spinner and makes the button unavailable without taking
35
+ * focus from it: `aria-disabled` and `aria-busy`, still a Tab stop,
36
+ * and a press does nothing. It is not `disabled`: match it in CSS
37
+ * with `[aria-busy="true"]`.
38
+ */
10
39
  loading?: boolean;
11
40
  text?: string;
12
41
  /** Stretch to the width of the container. */
@@ -22,7 +51,8 @@
22
51
  size = "md",
23
52
  disabled = false,
24
53
  loading = false,
25
- type = "button",
54
+ type,
55
+ href,
26
56
  text = "",
27
57
  fullWidth = false,
28
58
  isFullWidth = false,
@@ -32,25 +62,90 @@
32
62
  ...restProps
33
63
  }: Props = $props();
34
64
 
35
- const isDisabled = $derived(disabled || loading);
65
+ const isDisabled = $derived(!!disabled || loading);
66
+ /**
67
+ * Loading without `disabled`: unavailable, and still where focus is.
68
+ *
69
+ * `disabled` on a button, or no `href` on a link, makes the element
70
+ * unfocusable, and the browser then drops focus on `<body>`: whoever
71
+ * pressed Save with the keyboard was at the top of the page when the
72
+ * save came back. So a loading control says it the ARIA way
73
+ * (`aria-disabled`, `aria-busy`), stays a Tab stop, and swallows the
74
+ * press. `disabled` is the platform's: out of the Tab order.
75
+ */
76
+ const isBusy = $derived(loading && !disabled);
77
+ const isLink = $derived(href !== undefined && href !== null);
78
+
79
+ /**
80
+ * A loading link has no `href`, so it is a Tab stop only by `tabindex`.
81
+ * The browser takes focus from an element the moment it stops being
82
+ * focusable, so the two attributes must never both be missing: the
83
+ * `tabindex` goes on before the `href` comes off (their order in the
84
+ * markup), and when loading ends it stays until the `href` is back, one
85
+ * update longer.
86
+ */
87
+ let keepsTabStop = $state(false);
88
+ $effect.pre(() => {
89
+ if (isBusy) {
90
+ keepsTabStop = true;
91
+ } else if (untrack(() => keepsTabStop)) {
92
+ void tick().then(() => (keepsTabStop = false));
93
+ }
94
+ });
95
+
96
+ /** A text link in a sentence: `variant="link"` on a real link. */
97
+ const isInlineLink = $derived(isLink && variant === "link");
98
+
99
+ /** A disabled link has no `href`, but a pointer can still press it. */
100
+ function handleLinkClick(event: MouseEvent) {
101
+ if (isDisabled) {
102
+ event.preventDefault();
103
+ event.stopPropagation();
104
+ return;
105
+ }
106
+ (onclick as ((event: MouseEvent) => void) | null | undefined)?.(event);
107
+ }
108
+
109
+ /** A loading button is not `disabled`, so the press reaches it: it ends here, and submits nothing. */
110
+ function handleButtonClick(event: MouseEvent & { currentTarget: EventTarget & HTMLButtonElement }) {
111
+ if (isBusy) {
112
+ event.preventDefault();
113
+ event.stopPropagation();
114
+ return;
115
+ }
116
+ onclick?.(event);
117
+ }
36
118
 
37
119
  /**
38
120
  * One height scale, shared with Input, Select and IconButton, so controls
39
- * of the same size line up in a row. Heights are fixed (h-8 / h-10 / h-12)
40
- * rather than derived from padding + line-height, which is what let Button
41
- * and Input drift 14px apart at `lg`.
121
+ * of the same size line up in a row: 32 / 40 / 48px (and 56px, `xl`, which
122
+ * only Button has). It is a minimum
123
+ * height, not a fixed one, so a label that does not fit on one line wraps
124
+ * and the button grows; it used to paint outside the button (a long label
125
+ * at 200% text on a phone). One line of text and the vertical padding are
126
+ * never taller than the minimum, the outline variant's border included,
127
+ * so a label that fits is exactly on the scale:
128
+ * scripts/check-control-geometry.js holds the padding to that.
42
129
  *
43
130
  * Weight and tracking do NOT change with size: a large button used to drop
44
131
  * to font-normal, so the biggest button had the lightest label.
45
132
  */
46
133
  const sizeClass = $derived.by(() => {
134
+ // sm and md are 44px tall on a coarse pointer, like Input, Select and
135
+ // IconButton, so a row still lines up and the box is the touch target.
136
+ // No minimum width: text and padding make a button wide enough, and one
137
+ // would stop the actions of an ImageUpload shrinking in a narrow space.
47
138
  if (size === "sm") {
48
- return { box: "h-8 px-3", text: "text-sm", gap: "gap-2", spinner: "size-3.5" };
139
+ return { box: "min-h-8 px-3 py-1 pointer-coarse:min-h-11", text: "text-sm", gap: "gap-2", spinner: "size-3.5" };
140
+ }
141
+ // xl is 56px, like the FloatingActionButton; only Button has it.
142
+ if (size === "xl") {
143
+ return { box: "min-h-14 px-6 py-3", text: "text-base", gap: "gap-2", spinner: "size-5" };
49
144
  }
50
145
  if (size === "lg") {
51
- return { box: "h-12 px-5", text: "text-base", gap: "gap-2", spinner: "size-4" };
146
+ return { box: "min-h-12 px-5 py-2", text: "text-base", gap: "gap-2", spinner: "size-4" };
52
147
  }
53
- return { box: "h-10 px-4", text: "text-sm", gap: "gap-2", spinner: "size-4" };
148
+ return { box: "min-h-10 px-4 py-2 pointer-coarse:min-h-11", text: "text-sm", gap: "gap-2", spinner: "size-4" };
54
149
  });
55
150
 
56
151
  /**
@@ -58,48 +153,108 @@
58
153
  * made a disabled primary lighter and more colourful than an enabled
59
154
  * secondary. Every variant falls back to the same neutral disabled pair.
60
155
  */
156
+ const DISABLED_LINK =
157
+ "bg-(--color-action-disabled) text-action-disabled-text border border-transparent cursor-not-allowed";
158
+
159
+ /**
160
+ * A loading button wears the same pair. It is not `:disabled`, so the
161
+ * variant's classes are left out instead of overridden: none of its hover
162
+ * or pressed fills can show. What the variant gives the box is kept (the
163
+ * outline's border, the link's missing side padding), so the button does
164
+ * not change size when it starts loading. It keeps the focus ring: it
165
+ * can be focused.
166
+ */
167
+ const BUSY_BUTTON = "bg-(--color-action-disabled) text-action-disabled-text cursor-not-allowed";
168
+
169
+ /**
170
+ * Forced colours (a Windows contrast theme) remove every fill, so a button
171
+ * told apart by its fill alone is told apart by nothing. The variants that
172
+ * are drawn by a fill get a 1px edge in a system colour there only;
173
+ * `ghost`, `link` and `text` stay bare, and `outline` has its own border.
174
+ * A disabled or loading button is the flat disabled fill, so it gets an
175
+ * edge in the disabled text colour. The `disabled:` copy beats
176
+ * `disabled:border-transparent`, which is the more specific rule.
177
+ */
178
+ const FORCED_EDGE = "forced-colors:border forced-colors:border-[color:ButtonBorder]";
179
+ const FORCED_EDGE_DISABLED_ONLY = "disabled:forced-colors:border disabled:forced-colors:border-[color:GrayText]";
180
+ const FORCED_EDGE_DISABLED =
181
+ "forced-colors:border forced-colors:border-[color:GrayText] disabled:forced-colors:border-[color:GrayText]";
182
+
61
183
  const disabledClass =
62
- "disabled:bg-action-disabled disabled:text-action-disabled-text disabled:border-transparent disabled:no-underline disabled:shadow-none disabled:cursor-not-allowed disabled:active:scale-100";
184
+ "disabled:bg-action-disabled disabled:text-action-disabled-text disabled:border-transparent disabled:no-underline disabled:bg-none disabled:inset-shadow-none disabled:shadow-none disabled:cursor-not-allowed disabled:active:scale-100";
63
185
 
64
186
  const variantClass = $derived.by(() => {
187
+ // The veil of the solid fills (a top highlight and a darker bottom). A
188
+ // disabled button is the flat neutral pair, so it is left off rather
189
+ // than cleared; `disabledClass` also clears it for a button that is
190
+ // disabled from outside (a `fieldset`).
191
+ const veil = disabled ? "" : "bg-control-gradient ";
192
+ const veilAccent = disabled ? "" : "bg-control-gradient-accent ";
65
193
  switch (variant) {
66
194
  case "secondary":
67
- return "bg-action-secondary text-headline hover:bg-action-secondary-hover active:bg-action-secondary-active active:scale-[0.98]";
195
+ return "bg-action-secondary text-headline hover:bg-action-secondary-hover active:bg-action-secondary-active active:scale-[0.98] motion-reduce:active:scale-100";
196
+ case "tonal":
197
+ return "bg-action-tonal text-action-tonal active:scale-[0.98] motion-reduce:active:scale-100";
68
198
  case "danger":
69
- return "bg-action-danger text-action-danger-text hover:bg-action-danger-hover active:bg-action-danger-active active:scale-[0.98] focus-ring--danger";
199
+ return `${veil}bg-action-danger text-action-danger-text hover:bg-action-danger-hover active:bg-action-danger-active active:scale-[0.98] motion-reduce:active:scale-100 focus-ring--danger`;
70
200
  case "ghost":
71
- return "bg-transparent text-headline hover:bg-surface-hover active:bg-surface-active active:scale-[0.98] focus-ring--muted";
201
+ return "bg-transparent text-headline hover:bg-surface-hover active:bg-surface-active active:scale-[0.98] motion-reduce:active:scale-100 focus-ring--muted";
72
202
  case "outline":
73
- return "bg-transparent border border-border text-headline hover:bg-surface-hover hover:border-border-medium active:bg-surface-active active:scale-[0.98]";
203
+ return "bg-transparent border border-action-outline text-headline hover:bg-surface-hover hover:border-action-outline-hover active:bg-surface-active active:scale-[0.98] motion-reduce:active:scale-100";
74
204
  case "link":
75
205
  // A link variant has to look like a link at rest, not only on
76
206
  // hover — otherwise it is indistinguishable from `ghost`.
77
207
  return "bg-transparent text-link hover:text-link-hover underline underline-offset-4 decoration-1 hover:decoration-2 px-0 focus-ring--muted";
208
+ case "text":
209
+ // A standalone action, not a link in a sentence: no underline,
210
+ // a stronger label, and the box of the other variants. The
211
+ // link role gives the primary colour, and the block's own
212
+ // on-colour inside `.on-brand` / `.on-accent`.
213
+ return "bg-transparent text-link px-2 hover:bg-surface-hover active:bg-surface-active active:scale-[0.98] motion-reduce:active:scale-100 focus-ring--muted";
214
+ case "accent":
215
+ return `${veilAccent}bg-accent text-on-accent hover:bg-accent-hover active:bg-accent-active active:scale-[0.98] motion-reduce:active:scale-100`;
78
216
  case "primary":
79
217
  default:
80
- return "bg-action-primary text-action-primary hover:bg-action-primary-hover active:bg-action-primary-active active:scale-[0.98]";
218
+ return `${veil}bg-action-primary text-action-primary hover:bg-action-primary-hover active:bg-action-primary-active active:scale-[0.98] motion-reduce:active:scale-100`;
81
219
  }
82
220
  });
83
221
 
84
222
  const buttonClasses = $derived.by(() => {
85
223
  const s = sizeClass;
86
224
  const layout = fullWidth || isFullWidth ? "flex w-full" : "inline-flex";
87
- const base = `${layout} focus-ring items-center justify-center rounded-control font-medium whitespace-nowrap transition-colors duration-150 cursor-pointer select-none`;
88
- return cn(`${base} ${s.box} ${s.text} ${s.gap} ${variantClass} ${disabledClass} ${className}`);
225
+ if (isInlineLink) {
226
+ // Flows with the sentence it is in: no box, no minimum height, and
227
+ // it breaks across lines as a link does.
228
+ const state = isDisabled
229
+ ? "text-action-disabled-text no-underline cursor-not-allowed"
230
+ : "text-link hover:text-link-hover underline underline-offset-4 decoration-1 hover:decoration-2 cursor-pointer";
231
+ return cn(
232
+ `focus-ring focus-ring--muted inline rounded-button font-button transition-colors duration-(--duration-base) ${s.text} ${state} ${fullWidth || isFullWidth ? "block w-full" : ""} ${className}`,
233
+ );
234
+ }
235
+ // The label wraps when it has to, balanced over its lines; the icon
236
+ // and the spinner stay centred beside it. The button is never wider
237
+ // than what it is in: a word that alone does not fit is hyphenated by
238
+ // the page's language (Z-129).
239
+ const base = `${layout} [max-inline-size:100%] hyphens-auto focus-ring items-center justify-center rounded-button text-center ${variant === "text" ? "font-button-strong" : "font-button"} text-balance transition-colors duration-(--duration-base) cursor-pointer select-none`;
240
+ // `disabled:` does not match an `<a>`: a disabled link wears the
241
+ // disabled pair itself, in place of its variant.
242
+ const busyBox = variant === "outline" ? "border border-transparent" : variant === "link" ? "px-0" : variant === "text" ? "px-2" : "";
243
+ const drawnByFill = variant !== "ghost" && variant !== "link" && variant !== "text" && variant !== "outline";
244
+ const state =
245
+ isLink && isDisabled
246
+ ? `${DISABLED_LINK} ${FORCED_EDGE_DISABLED}`
247
+ : isBusy
248
+ ? `${BUSY_BUTTON} ${busyBox} ${FORCED_EDGE_DISABLED}`
249
+ : `${variantClass} ${disabledClass} ${drawnByFill ? FORCED_EDGE : ""} ${FORCED_EDGE_DISABLED_ONLY}`;
250
+ return cn(`${base} ${s.box} ${s.text} ${s.gap} ${state} ${className}`);
89
251
  });
90
252
  </script>
91
253
 
92
- <button
93
- {type}
94
- class={buttonClasses}
95
- disabled={isDisabled}
96
- aria-busy={loading ? "true" : undefined}
97
- {onclick}
98
- {...restProps}
99
- >
254
+ {#snippet content()}
100
255
  {#if loading}
101
256
  <span
102
- class="inline-block {sizeClass.spinner} shrink-0 animate-spin rounded-full border-2 border-current border-t-transparent opacity-80"
257
+ class="inline-block {sizeClass.spinner} shrink-0 animate-spin rounded-full border-2 border-current border-t-transparent opacity-80 motion-reduce:animate-pulse"
103
258
  aria-hidden="true"
104
259
  ></span>
105
260
  {/if}
@@ -108,4 +263,36 @@
108
263
  {:else if children}
109
264
  {@render children()}
110
265
  {/if}
111
- </button>
266
+ {/snippet}
267
+
268
+ {#if isLink}
269
+ <!-- A link that cannot be followed keeps its role and says it is
270
+ unavailable; without an `href` it is not a Tab stop, as a disabled button
271
+ is not. A loading one is given the Tab stop back, so focus stays on it:
272
+ `tabindex` is written before `href` is taken away, in that order (see
273
+ `keepsTabStop`). -->
274
+ <a
275
+ tabindex={isBusy || keepsTabStop ? 0 : undefined}
276
+ href={isDisabled ? undefined : href}
277
+ class={buttonClasses}
278
+ role={isDisabled ? "link" : undefined}
279
+ aria-disabled={isDisabled ? "true" : undefined}
280
+ aria-busy={loading ? "true" : undefined}
281
+ {...(restProps as unknown as HTMLAnchorAttributes)}
282
+ onclick={handleLinkClick}
283
+ >
284
+ {@render content()}
285
+ </a>
286
+ {:else}
287
+ <button
288
+ type={type ?? "button"}
289
+ class={buttonClasses}
290
+ disabled={!!disabled}
291
+ aria-disabled={isBusy ? "true" : undefined}
292
+ aria-busy={loading ? "true" : undefined}
293
+ {...restProps}
294
+ onclick={handleButtonClick}
295
+ >
296
+ {@render content()}
297
+ </button>
298
+ {/if}
@@ -1,9 +1,38 @@
1
- import type { Snippet } from "svelte";
2
- import type { HTMLButtonAttributes } from "svelte/elements";
3
- import type { ButtonVariant, SizeVariant } from "../types/variants.js";
1
+ import { type Snippet } from "svelte";
2
+ import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
3
+ import type { ButtonSize, ButtonVariant } from "../types/variants.js";
4
+ /**
5
+ * One type for the button and the link, not a union of the two elements'
6
+ * attributes: that union is too large for TypeScript to carry through a
7
+ * wrapper such as a Storybook `Meta`. Event handlers are typed for the
8
+ * button; on a link they are called with the same event.
9
+ */
4
10
  type Props = Omit<HTMLButtonAttributes, "class"> & {
11
+ /**
12
+ * Where the link goes. Makes this an `<a>` that looks the same;
13
+ * without it, it is a `<button>`. A link cannot be disabled: while
14
+ * `disabled` or `loading` it has no `href`, is `aria-disabled` and
15
+ * does nothing when pressed. While `loading` it stays a Tab stop.
16
+ */
17
+ href?: string;
18
+ /** With `href`: where the link opens. */
19
+ target?: HTMLAnchorAttributes["target"];
20
+ /** With `href`: the link's relationship, such as `noopener`. */
21
+ rel?: HTMLAnchorAttributes["rel"];
22
+ /** With `href`: download the address instead of opening it. */
23
+ download?: HTMLAnchorAttributes["download"];
24
+ hreflang?: HTMLAnchorAttributes["hreflang"];
25
+ referrerpolicy?: HTMLAnchorAttributes["referrerpolicy"];
26
+ ping?: HTMLAnchorAttributes["ping"];
5
27
  variant?: ButtonVariant;
6
- size?: SizeVariant;
28
+ /** `xl` is 56px, for a primary action on a phone. It is a Button size only. */
29
+ size?: ButtonSize;
30
+ /**
31
+ * Shows a spinner and makes the button unavailable without taking
32
+ * focus from it: `aria-disabled` and `aria-busy`, still a Tab stop,
33
+ * and a press does nothing. It is not `disabled`: match it in CSS
34
+ * with `[aria-busy="true"]`.
35
+ */
7
36
  loading?: boolean;
8
37
  text?: string;
9
38
  /** Stretch to the width of the container. */
@@ -1,18 +1,71 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from "svelte";
3
- import type { HTMLAttributes } from "svelte/elements";
4
- import type { CardVariant, SizeVariant } from "../types/variants.js";
3
+ import type { HTMLAnchorAttributes, HTMLAttributes } from "svelte/elements";
4
+ import type { CardVariant, SizeVariant, SurfaceTone } from "../types/variants.js";
5
5
 
6
6
  import { cn } from "../util/cn.js";
7
+ import { toneSurface } from "../util/tone-surface.js";
7
8
  type Props = Omit<HTMLAttributes<HTMLDivElement>, "class"> & {
8
9
  onclick?: (event: MouseEvent) => void | Promise<void>;
9
10
  size?: SizeVariant;
11
+ /** The look of a `neutral` card. With a `tone` or a `fill` the fill is the edge: no border, and a shadow only for `elevated`. */
10
12
  variant?: CardVariant;
13
+ /**
14
+ * What the card is filled with: `neutral` (the default, the card surface),
15
+ * `tint` (the brand tint), `brand` or `accent`. On `brand` and `accent`
16
+ * the text, links, controls and focus rings inside take the fill's label
17
+ * colour; a Card, field or Alert inside is a surface again.
18
+ */
19
+ tone?: SurfaceTone;
20
+ /**
21
+ * A fill the app chooses, any CSS colour (`var(--group-colour)`). Wins
22
+ * over `tone`. Set `onFill`, the label colour on it. The library cannot
23
+ * check that pair's contrast: the app must hold it to 4.5:1.
24
+ */
25
+ fill?: string;
26
+ /** The label colour on `fill`, any CSS colour. */
27
+ onFill?: string;
28
+ /**
29
+ * With `href` the card itself is an `<a>`: the whole card is the link,
30
+ * and it works without scripts. A link card must not contain other
31
+ * interactive elements (a link or a button in a link is invalid and
32
+ * unreachable); a card with actions inside takes `onclick` or none.
33
+ * With `onclick` too, it is a link and `onclick` runs on activation.
34
+ */
35
+ href?: string;
36
+ /** With `href`: where the link opens. */
37
+ target?: HTMLAnchorAttributes["target"];
38
+ /** With `href`: the link's relationship, such as `noopener`. */
39
+ rel?: string;
40
+ /**
41
+ * The card itself is a `<button>`: the whole card is the control, and
42
+ * `submit` posts the form around it without scripts. With `href`
43
+ * the card is a link and this is ignored. Like a link card it must not
44
+ * contain other interactive elements. `onclick`
45
+ * runs on a press, as on any button.
46
+ */
47
+ type?: "submit" | "button" | "reset";
48
+ /** With `type`: the name the button submits under. */
49
+ name?: string;
50
+ /** With `type`: what the button submits. */
51
+ value?: string;
52
+ /** With `type`: the id of the form it belongs to, when it is not inside one. */
53
+ form?: string;
54
+ /** With `type`: the card cannot be pressed, is dimmed, and is skipped by the keyboard. */
55
+ disabled?: boolean;
56
+ /**
57
+ * With `type`: the card's action is on its way, as `Button`'s and
58
+ * `Chip`'s `loading`. `aria-busy` and `aria-disabled` (not `disabled`),
59
+ * the press swallowed (it submits nothing and does not reach
60
+ * `onclick`), the pressed look held, the size and the Tab stop kept.
61
+ * No spinner. Ignored by a link card and a plain card.
62
+ */
63
+ loading?: boolean;
11
64
  fullWidth?: boolean;
12
65
  class?: string;
13
66
  /** @deprecated use `class` — kept so 7.x call sites keep working. */
14
67
  className?: string;
15
- /** Required when `onclick` is set (no visible label). */
68
+ /** Names the card when it is interactive and its content does not (the content names a link well enough in most cases). */
16
69
  ariaLabel?: string;
17
70
  children?: Snippet;
18
71
  };
@@ -21,10 +74,23 @@
21
74
  onclick,
22
75
  size = "md",
23
76
  variant = "default",
77
+ tone = "neutral",
78
+ fill,
79
+ onFill,
80
+ href,
81
+ target,
82
+ rel,
83
+ type: buttonType,
84
+ name,
85
+ value,
86
+ form,
87
+ disabled = false,
88
+ loading = false,
24
89
  fullWidth = true,
25
90
  class: classProp = "",
26
91
  className = "",
27
92
  ariaLabel,
93
+ style: styleProp,
28
94
  children,
29
95
  ...restProps
30
96
  }: Props = $props();
@@ -35,36 +101,79 @@
35
101
  size === "sm" ? "p-4" : size === "lg" ? "p-8" : "p-6",
36
102
  );
37
103
 
104
+ const surface = $derived(toneSurface({ tone, fill, onFill }));
105
+
106
+ const isLink = $derived(href !== undefined && href !== null);
107
+ const isButton = $derived(!isLink && buttonType !== undefined);
108
+
38
109
  const variantClasses = $derived(
39
- variant === "elevated"
40
- ? "bg-card-elevated shadow-lg"
41
- : variant === "outlined"
42
- ? "bg-card border border-border shadow-none"
110
+ surface.filled
111
+ ? // The fill is the edge: no border, flat unless the variant lifts it.
112
+ `${surface.classes} ${variant === "elevated" ? "shadow-lg" : "shadow-none"}`
113
+ : variant === "elevated"
114
+ ? "bg-card-elevated shadow-lg"
43
115
  : variant === "flat"
44
- ? "bg-card shadow-none border-none"
45
- : "bg-card shadow-sm",
116
+ ? "bg-card-flat shadow-none border-none"
117
+ : "bg-card border border-border shadow-none",
46
118
  );
47
119
 
120
+ /**
121
+ * Hover and pressed on a fill the card does not know: a veil of the card's
122
+ * own text colour (`bg-current`), so it reads on a tint, the brand, the
123
+ * accent and any colour an app passes. The card is `relative` for it.
124
+ */
125
+ const STATE_LAYER =
126
+ "relative after:absolute after:inset-0 after:rounded-[inherit] after:bg-current after:opacity-0 after:pointer-events-none after:transition-opacity after:duration-(--duration-base) motion-reduce:after:transition-none hover:after:opacity-8 active:after:opacity-12";
127
+
128
+ /** Loading without `disabled`: unavailable, and still where focus is (see Button). */
129
+ const isBusy = $derived(isButton && loading && !disabled);
130
+
131
+ const interactive = $derived(!!onclick || isLink || isButton);
132
+
48
133
  const interactiveClasses = $derived.by(() => {
49
- if (!onclick) return "";
134
+ if (!interactive) return "";
135
+ if (isBusy) {
136
+ // The pressed look, held: no hover or pressed rule is left to change it.
137
+ // Forced colours drop the fills the held look is made of: GrayText text and a dashed inset edge say it there.
138
+ const held =
139
+ "cursor-progress focus-ring forced-colors:text-[color:GrayText] forced-colors:outline-1 forced-colors:outline-dashed forced-colors:-outline-offset-1 forced-colors:outline-[color:GrayText]";
140
+ if (surface.filled) return `${held} relative after:absolute after:inset-0 after:rounded-[inherit] after:bg-current after:opacity-12 after:pointer-events-none`;
141
+ switch (variant) {
142
+ case "elevated":
143
+ return `${held} bg-card-active`;
144
+ case "flat":
145
+ return `${held} bg-card-flat-active`;
146
+ default:
147
+ return `${held} border-border-medium bg-card-active`;
148
+ }
149
+ }
50
150
  const shared = "cursor-pointer focus-ring";
151
+ if (surface.filled) return `${shared} ${STATE_LAYER}`;
51
152
  switch (variant) {
52
153
  case "elevated":
53
- return `${shared} hover:bg-card-hover`;
54
- case "outlined":
55
- return `${shared} hover:border-border-medium hover:bg-card-hover`;
154
+ return `${shared} hover:bg-card-hover active:bg-card-active`;
56
155
  case "flat":
57
- return `${shared} hover:bg-card-hover`;
156
+ return `${shared} hover:bg-card-flat-hover active:bg-card-flat-active`;
58
157
  default:
59
- return `${shared} hover:shadow-lg hover:bg-card-hover`;
158
+ return `${shared} hover:border-border-medium hover:bg-card-hover active:bg-card-active`;
60
159
  }
61
160
  });
62
161
 
162
+ /** A link card keeps the body colour (its whole content is not link-coloured); a scoped fill sets its own. */
163
+ const linkClasses = $derived(
164
+ isLink
165
+ ? `block no-underline min-h-11 ${surface.scoped ? "" : "text-body"}`
166
+ : isButton
167
+ ? `block text-start min-h-11 ${surface.scoped ? "" : "text-body"} ${disabled ? "cursor-not-allowed opacity-50 pointer-events-none" : ""}`
168
+ : "",
169
+ );
170
+
63
171
  const cardClasses = $derived(
64
172
  cn(
65
173
  "rounded-container transition-all duration-150",
66
174
  variantClasses,
67
175
  interactiveClasses,
176
+ linkClasses,
68
177
  fullWidth ? "w-full" : "",
69
178
  sizeClass,
70
179
  classProp,
@@ -72,6 +181,20 @@
72
181
  ),
73
182
  );
74
183
 
184
+ const cardStyle = $derived(
185
+ [surface.style, typeof styleProp === "string" ? styleProp : undefined].filter(Boolean).join("; ") || undefined,
186
+ );
187
+
188
+ /** A busy card is not `disabled`, so the press reaches it: it ends here, and submits nothing. */
189
+ function handleButtonClick(event: MouseEvent) {
190
+ if (isBusy) {
191
+ event.preventDefault();
192
+ event.stopPropagation();
193
+ return;
194
+ }
195
+ onclick?.(event);
196
+ }
197
+
75
198
  function handleKeydown(event: KeyboardEvent) {
76
199
  if (onclick && (event.key === "Enter" || event.key === " ")) {
77
200
  event.preventDefault();
@@ -82,14 +205,47 @@
82
205
  const cardRole = $derived(onclick ? "button" : undefined);
83
206
  </script>
84
207
 
85
- <div
86
- class={cardClasses}
87
- {onclick}
88
- role={cardRole}
89
- {...onclick ? { tabindex: 0 } : {}}
90
- aria-label={ariaLabel}
91
- onkeydown={onclick ? handleKeydown : undefined}
92
- {...restProps}
93
- >
94
- {@render children?.()}
95
- </div>
208
+ {#if isLink}
209
+ <a
210
+ class={cardClasses}
211
+ style={cardStyle}
212
+ {href}
213
+ {target}
214
+ {rel}
215
+ {onclick}
216
+ aria-label={ariaLabel}
217
+ {...restProps as HTMLAnchorAttributes}
218
+ >
219
+ {@render children?.()}
220
+ </a>
221
+ {:else if isButton}
222
+ <button
223
+ type={buttonType}
224
+ {name}
225
+ {value}
226
+ {form}
227
+ {disabled}
228
+ class={cardClasses}
229
+ style={cardStyle}
230
+ aria-label={ariaLabel}
231
+ aria-busy={isBusy ? "true" : undefined}
232
+ aria-disabled={isBusy ? "true" : undefined}
233
+ {...restProps as HTMLAttributes<HTMLButtonElement>}
234
+ onclick={handleButtonClick}
235
+ >
236
+ {@render children?.()}
237
+ </button>
238
+ {:else}
239
+ <div
240
+ class={cardClasses}
241
+ style={cardStyle}
242
+ {onclick}
243
+ role={cardRole}
244
+ {...onclick ? { tabindex: 0 } : {}}
245
+ aria-label={ariaLabel}
246
+ onkeydown={onclick ? handleKeydown : undefined}
247
+ {...restProps}
248
+ >
249
+ {@render children?.()}
250
+ </div>
251
+ {/if}