zabi-components 8.0.0 → 8.1.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 (364) hide show
  1. package/CHANGELOG.md +1924 -0
  2. package/README.md +243 -10
  3. package/THEME.md +149 -332
  4. package/THEMING.md +1192 -266
  5. package/dist/atoms/ActionPanel.svelte +15 -5
  6. package/dist/atoms/Avatar.svelte +104 -0
  7. package/dist/atoms/Avatar.svelte.d.ts +34 -0
  8. package/dist/atoms/Badge.svelte +36 -7
  9. package/dist/atoms/Badge.svelte.d.ts +2 -2
  10. package/dist/atoms/Button.svelte +177 -27
  11. package/dist/atoms/Button.svelte.d.ts +30 -2
  12. package/dist/atoms/Card.svelte +4 -4
  13. package/dist/atoms/CardHeader.svelte +16 -4
  14. package/dist/atoms/Checkbox.svelte +48 -21
  15. package/dist/atoms/Checkbox.svelte.d.ts +4 -2
  16. package/dist/atoms/CodeBlock.svelte +20 -2
  17. package/dist/atoms/CodeBlock.svelte.d.ts +4 -0
  18. package/dist/atoms/ColorPicker.svelte +259 -49
  19. package/dist/atoms/ColorPicker.svelte.d.ts +3 -0
  20. package/dist/atoms/Container.svelte +16 -8
  21. package/dist/atoms/DateField.svelte +107 -0
  22. package/dist/atoms/DateField.svelte.d.ts +53 -0
  23. package/dist/atoms/FieldMessages.svelte +69 -0
  24. package/dist/atoms/FieldMessages.svelte.d.ts +23 -0
  25. package/dist/atoms/FloatingActionButton.svelte +174 -0
  26. package/dist/atoms/FloatingActionButton.svelte.d.ts +51 -0
  27. package/dist/atoms/Heading.svelte +37 -5
  28. package/dist/atoms/Heading.svelte.d.ts +7 -0
  29. package/dist/atoms/IconButton.svelte +220 -24
  30. package/dist/atoms/IconButton.svelte.d.ts +49 -4
  31. package/dist/atoms/Input.svelte +253 -52
  32. package/dist/atoms/Input.svelte.d.ts +32 -2
  33. package/dist/atoms/List.svelte +4 -1
  34. package/dist/atoms/ListItem.svelte +44 -11
  35. package/dist/atoms/ListItem.svelte.d.ts +5 -0
  36. package/dist/atoms/Progress.svelte +27 -8
  37. package/dist/atoms/Progress.svelte.d.ts +4 -0
  38. package/dist/atoms/Radio.svelte +19 -3
  39. package/dist/atoms/Radio.svelte.d.ts +4 -2
  40. package/dist/atoms/Rating.svelte +494 -0
  41. package/dist/atoms/Rating.svelte.d.ts +56 -0
  42. package/dist/atoms/Select.svelte +433 -91
  43. package/dist/atoms/Select.svelte.d.ts +49 -3
  44. package/dist/atoms/SelectionControl.svelte +94 -17
  45. package/dist/atoms/SelectionControl.svelte.d.ts +4 -3
  46. package/dist/atoms/Skeleton.svelte +1 -1
  47. package/dist/atoms/Slider.svelte +309 -0
  48. package/dist/atoms/Slider.svelte.d.ts +40 -0
  49. package/dist/atoms/Spinner.svelte +57 -0
  50. package/dist/atoms/Spinner.svelte.d.ts +19 -0
  51. package/dist/atoms/Table.svelte +151 -6
  52. package/dist/atoms/Table.svelte.d.ts +15 -3
  53. package/dist/atoms/TemporalField.svelte +140 -0
  54. package/dist/atoms/TemporalField.svelte.d.ts +32 -0
  55. package/dist/atoms/Text.svelte +23 -11
  56. package/dist/atoms/Text.svelte.d.ts +6 -3
  57. package/dist/atoms/Textarea.svelte +55 -44
  58. package/dist/atoms/Textarea.svelte.d.ts +14 -2
  59. package/dist/atoms/ThemeToggle.svelte +223 -44
  60. package/dist/atoms/ThemeToggle.svelte.d.ts +26 -3
  61. package/dist/atoms/TimeField.svelte +108 -0
  62. package/dist/atoms/TimeField.svelte.d.ts +54 -0
  63. package/dist/atoms/Toast.svelte +17 -4
  64. package/dist/atoms/Toast.svelte.d.ts +2 -0
  65. package/dist/atoms/Toggle.svelte +41 -8
  66. package/dist/atoms/Toggle.svelte.d.ts +11 -2
  67. package/dist/atoms/Tooltip.svelte +508 -23
  68. package/dist/atoms/Tooltip.svelte.d.ts +11 -0
  69. package/dist/atoms/ZabiStringsProvider.svelte +45 -0
  70. package/dist/atoms/ZabiStringsProvider.svelte.d.ts +27 -0
  71. package/dist/atoms/index.d.ts +17 -0
  72. package/dist/atoms/index.js +12 -0
  73. package/dist/atoms/selection-control.styles.d.ts +36 -5
  74. package/dist/atoms/selection-control.styles.js +40 -9
  75. package/dist/components/atoms/index.d.ts +17 -0
  76. package/dist/components/atoms/index.d.ts.map +1 -1
  77. package/dist/components/atoms/selection-control.styles.d.ts +36 -5
  78. package/dist/components/atoms/selection-control.styles.d.ts.map +1 -1
  79. package/dist/components/index.d.ts +80 -2
  80. package/dist/components/index.d.ts.map +1 -1
  81. package/dist/components/molecules/index.d.ts +46 -0
  82. package/dist/components/molecules/index.d.ts.map +1 -1
  83. package/dist/components/molecules/toast-store.d.ts +53 -7
  84. package/dist/components/molecules/toast-store.d.ts.map +1 -1
  85. package/dist/components/organisms/index.d.ts +5 -0
  86. package/dist/components/organisms/index.d.ts.map +1 -1
  87. package/dist/components/types/variants.d.ts +15 -1
  88. package/dist/components/types/variants.d.ts.map +1 -1
  89. package/dist/components/util/app-shell.d.ts +28 -0
  90. package/dist/components/util/app-shell.d.ts.map +1 -0
  91. package/dist/components/util/avatar.d.ts +36 -0
  92. package/dist/components/util/avatar.d.ts.map +1 -0
  93. package/dist/components/util/bottom-sheet.d.ts +30 -0
  94. package/dist/components/util/bottom-sheet.d.ts.map +1 -0
  95. package/dist/components/util/bottom-tab-bar.d.ts +35 -0
  96. package/dist/components/util/bottom-tab-bar.d.ts.map +1 -0
  97. package/dist/components/util/calendar.d.ts +56 -0
  98. package/dist/components/util/calendar.d.ts.map +1 -0
  99. package/dist/components/util/collapsible.d.ts +58 -0
  100. package/dist/components/util/collapsible.d.ts.map +1 -0
  101. package/dist/components/util/confirm-dialog.d.ts +12 -0
  102. package/dist/components/util/confirm-dialog.d.ts.map +1 -0
  103. package/dist/components/util/date.d.ts +96 -0
  104. package/dist/components/util/date.d.ts.map +1 -0
  105. package/dist/components/util/drawer.d.ts +13 -0
  106. package/dist/components/util/drawer.d.ts.map +1 -0
  107. package/dist/components/util/dropdown.d.ts +47 -0
  108. package/dist/components/util/dropdown.d.ts.map +1 -0
  109. package/dist/components/util/field.d.ts +39 -0
  110. package/dist/components/util/field.d.ts.map +1 -0
  111. package/dist/components/util/fit-in-viewport.d.ts +201 -0
  112. package/dist/components/util/fit-in-viewport.d.ts.map +1 -0
  113. package/dist/components/util/focus-utils.d.ts +75 -2
  114. package/dist/components/util/focus-utils.d.ts.map +1 -1
  115. package/dist/components/util/hydration.d.ts +45 -0
  116. package/dist/components/util/hydration.d.ts.map +1 -0
  117. package/dist/components/util/keyboard-inset.d.ts +18 -0
  118. package/dist/components/util/keyboard-inset.d.ts.map +1 -0
  119. package/dist/components/util/media-grid.d.ts +51 -0
  120. package/dist/components/util/media-grid.d.ts.map +1 -0
  121. package/dist/components/util/media.d.ts +6 -0
  122. package/dist/components/util/media.d.ts.map +1 -0
  123. package/dist/components/util/overlay-chrome.d.ts +53 -0
  124. package/dist/components/util/overlay-chrome.d.ts.map +1 -0
  125. package/dist/components/util/overlay.d.ts +72 -0
  126. package/dist/components/util/overlay.d.ts.map +1 -0
  127. package/dist/components/util/photo.d.ts +215 -0
  128. package/dist/components/util/photo.d.ts.map +1 -0
  129. package/dist/components/util/portal.d.ts +23 -0
  130. package/dist/components/util/portal.d.ts.map +1 -0
  131. package/dist/components/util/pull-to-refresh.d.ts +27 -0
  132. package/dist/components/util/pull-to-refresh.d.ts.map +1 -0
  133. package/dist/components/util/radio-keys.d.ts +25 -0
  134. package/dist/components/util/radio-keys.d.ts.map +1 -0
  135. package/dist/components/util/rating.d.ts +23 -0
  136. package/dist/components/util/rating.d.ts.map +1 -0
  137. package/dist/components/util/ready-made-strings.d.ts +77 -0
  138. package/dist/components/util/ready-made-strings.d.ts.map +1 -0
  139. package/dist/components/util/scroll-padding.d.ts +20 -0
  140. package/dist/components/util/scroll-padding.d.ts.map +1 -0
  141. package/dist/components/util/segmented-control.d.ts +16 -0
  142. package/dist/components/util/segmented-control.d.ts.map +1 -0
  143. package/dist/components/util/select.d.ts +41 -0
  144. package/dist/components/util/select.d.ts.map +1 -0
  145. package/dist/components/util/sheet-drag.d.ts +60 -0
  146. package/dist/components/util/sheet-drag.d.ts.map +1 -0
  147. package/dist/components/util/sidebar.d.ts +63 -0
  148. package/dist/components/util/sidebar.d.ts.map +1 -0
  149. package/dist/components/util/sortable-list.d.ts +65 -0
  150. package/dist/components/util/sortable-list.d.ts.map +1 -0
  151. package/dist/components/util/stepper.d.ts +40 -0
  152. package/dist/components/util/stepper.d.ts.map +1 -0
  153. package/dist/components/util/swipeable-list-item.d.ts +36 -0
  154. package/dist/components/util/swipeable-list-item.d.ts.map +1 -0
  155. package/dist/components/util/theme-mode.d.ts +80 -0
  156. package/dist/components/util/theme-mode.d.ts.map +1 -0
  157. package/dist/components/util/toaster.d.ts +93 -0
  158. package/dist/components/util/toaster.d.ts.map +1 -0
  159. package/dist/components/util/tooltip.d.ts +10 -0
  160. package/dist/components/util/tooltip.d.ts.map +1 -0
  161. package/dist/components/util/top-navbar.d.ts +11 -0
  162. package/dist/components/util/top-navbar.d.ts.map +1 -0
  163. package/dist/components/util/touch-target.d.ts +31 -0
  164. package/dist/components/util/touch-target.d.ts.map +1 -0
  165. package/dist/components/util/zabi-strings.d.ts +133 -0
  166. package/dist/components/util/zabi-strings.d.ts.map +1 -0
  167. package/dist/create-theme/cli.js +169 -0
  168. package/dist/create-theme/index.d.ts +176 -0
  169. package/dist/create-theme/index.js +817 -0
  170. package/dist/create-theme/lib/ramp-math.js +128 -0
  171. package/dist/create-theme/lib/resolve.js +141 -0
  172. package/dist/create-theme/theme-data.js +1116 -0
  173. package/dist/index.d.ts +80 -2
  174. package/dist/index.js +57 -2
  175. package/dist/molecules/Alert.svelte +33 -5
  176. package/dist/molecules/Alert.svelte.d.ts +2 -0
  177. package/dist/molecules/AppBar.svelte +415 -0
  178. package/dist/molecules/AppBar.svelte.d.ts +69 -0
  179. package/dist/molecules/AvatarGroup.svelte +116 -0
  180. package/dist/molecules/AvatarGroup.svelte.d.ts +40 -0
  181. package/dist/molecules/BottomSheet.svelte +540 -0
  182. package/dist/molecules/BottomSheet.svelte.d.ts +93 -0
  183. package/dist/molecules/BottomTabBar.svelte +297 -0
  184. package/dist/molecules/BottomTabBar.svelte.d.ts +64 -0
  185. package/dist/molecules/Calendar.svelte +597 -0
  186. package/dist/molecules/Calendar.svelte.d.ts +72 -0
  187. package/dist/molecules/Collapsible.svelte +243 -0
  188. package/dist/molecules/Collapsible.svelte.d.ts +44 -0
  189. package/dist/molecules/CollapsibleGroup.svelte +126 -0
  190. package/dist/molecules/CollapsibleGroup.svelte.d.ts +14 -0
  191. package/dist/molecules/ComponentDemo.svelte +16 -4
  192. package/dist/molecules/ComponentDemo.svelte.d.ts +3 -0
  193. package/dist/molecules/ConfirmDialog.svelte +253 -0
  194. package/dist/molecules/ConfirmDialog.svelte.d.ts +55 -0
  195. package/dist/molecules/ContactForm.svelte +34 -17
  196. package/dist/molecules/ContactForm.svelte.d.ts +8 -0
  197. package/dist/molecules/Drawer.svelte +394 -0
  198. package/dist/molecules/Drawer.svelte.d.ts +55 -0
  199. package/dist/molecules/Dropdown.svelte +509 -48
  200. package/dist/molecules/Dropdown.svelte.d.ts +45 -6
  201. package/dist/molecules/DropdownItem.svelte +122 -0
  202. package/dist/molecules/DropdownItem.svelte.d.ts +28 -0
  203. package/dist/molecules/EmptyState.svelte +55 -10
  204. package/dist/molecules/EmptyState.svelte.d.ts +8 -0
  205. package/dist/molecules/FormField.svelte +9 -1
  206. package/dist/molecules/FormField.svelte.d.ts +2 -0
  207. package/dist/molecules/Header.svelte +1 -1
  208. package/dist/molecules/ImageUpload.svelte +383 -70
  209. package/dist/molecules/ImageUpload.svelte.d.ts +47 -2
  210. package/dist/molecules/MediaGrid.svelte +527 -0
  211. package/dist/molecules/MediaGrid.svelte.d.ts +77 -0
  212. package/dist/molecules/Modal.svelte +288 -72
  213. package/dist/molecules/Modal.svelte.d.ts +51 -2
  214. package/dist/molecules/NavigationMenuContent.svelte +58 -1
  215. package/dist/molecules/NavigationMenuItem.svelte +0 -1
  216. package/dist/molecules/NavigationMenuLink.svelte +1 -1
  217. package/dist/molecules/NavigationMenuList.svelte +10 -3
  218. package/dist/molecules/NavigationMenuTrigger.svelte +2 -2
  219. package/dist/molecules/Page.svelte +32 -2
  220. package/dist/molecules/Page.svelte.d.ts +11 -0
  221. package/dist/molecules/PhotoGrid.svelte +433 -0
  222. package/dist/molecules/PhotoGrid.svelte.d.ts +62 -0
  223. package/dist/molecules/PhotoViewer.svelte +1116 -0
  224. package/dist/molecules/PhotoViewer.svelte.d.ts +56 -0
  225. package/dist/molecules/PropsTable.svelte +22 -10
  226. package/dist/molecules/PropsTable.svelte.d.ts +3 -0
  227. package/dist/molecules/PullToRefresh.svelte +263 -0
  228. package/dist/molecules/PullToRefresh.svelte.d.ts +52 -0
  229. package/dist/molecules/RadioGroup.svelte +52 -14
  230. package/dist/molecules/RadioGroup.svelte.d.ts +14 -3
  231. package/dist/molecules/Section.svelte +1 -1
  232. package/dist/molecules/SegmentedControl.svelte +323 -0
  233. package/dist/molecules/SegmentedControl.svelte.d.ts +34 -0
  234. package/dist/molecules/SidebarBrandHeader.svelte +11 -1
  235. package/dist/molecules/SidebarBrandHeader.svelte.d.ts +3 -0
  236. package/dist/molecules/SidebarFooter.svelte +29 -6
  237. package/dist/molecules/SidebarFooter.svelte.d.ts +3 -0
  238. package/dist/molecules/SlideUp.svelte +220 -61
  239. package/dist/molecules/SlideUp.svelte.d.ts +26 -0
  240. package/dist/molecules/SortableList.svelte +632 -0
  241. package/dist/molecules/SortableList.svelte.d.ts +69 -0
  242. package/dist/molecules/Stepper.svelte +548 -0
  243. package/dist/molecules/Stepper.svelte.d.ts +39 -0
  244. package/dist/molecules/StickyActionBar.svelte +191 -0
  245. package/dist/molecules/StickyActionBar.svelte.d.ts +65 -0
  246. package/dist/molecules/SwipeableListItem.svelte +334 -0
  247. package/dist/molecules/SwipeableListItem.svelte.d.ts +54 -0
  248. package/dist/molecules/Tabs.svelte +233 -9
  249. package/dist/molecules/Tabs.svelte.d.ts +7 -0
  250. package/dist/molecules/Toaster.svelte +350 -5
  251. package/dist/molecules/Toaster.svelte.d.ts +95 -2
  252. package/dist/molecules/ToasterToast.svelte +253 -67
  253. package/dist/molecules/ToasterToast.svelte.d.ts +9 -0
  254. package/dist/molecules/UnsavedChangesBar.svelte +212 -0
  255. package/dist/molecules/UnsavedChangesBar.svelte.d.ts +43 -0
  256. package/dist/molecules/index.d.ts +46 -0
  257. package/dist/molecules/index.js +27 -0
  258. package/dist/molecules/toast-store.d.ts +53 -7
  259. package/dist/molecules/toast-store.js +21 -0
  260. package/dist/organisms/AppShell.svelte +196 -0
  261. package/dist/organisms/AppShell.svelte.d.ts +61 -0
  262. package/dist/organisms/SidebarAccountPanel.svelte +107 -12
  263. package/dist/organisms/SidebarAccountPanel.svelte.d.ts +19 -0
  264. package/dist/organisms/SidebarNavigation.svelte +96 -15
  265. package/dist/organisms/SidebarNavigation.svelte.d.ts +29 -1
  266. package/dist/organisms/SidebarPanel.svelte +31 -10
  267. package/dist/organisms/SidebarShell.svelte +162 -11
  268. package/dist/organisms/SidebarShell.svelte.d.ts +56 -2
  269. package/dist/organisms/TopNavbar.svelte +256 -31
  270. package/dist/organisms/TopNavbar.svelte.d.ts +26 -0
  271. package/dist/organisms/index.d.ts +5 -0
  272. package/dist/organisms/index.js +3 -0
  273. package/dist/types/components.d.ts +4 -0
  274. package/dist/types/components.d.ts.map +1 -1
  275. package/dist/types/index.d.ts +605 -25
  276. package/dist/types/index.d.ts.map +1 -1
  277. package/dist/types/variants.ts +17 -1
  278. package/dist/util/app-shell.d.ts +27 -0
  279. package/dist/util/app-shell.js +72 -0
  280. package/dist/util/avatar.d.ts +35 -0
  281. package/dist/util/avatar.js +52 -0
  282. package/dist/util/bottom-sheet.d.ts +29 -0
  283. package/dist/util/bottom-sheet.js +43 -0
  284. package/dist/util/bottom-tab-bar.d.ts +34 -0
  285. package/dist/util/bottom-tab-bar.js +54 -0
  286. package/dist/util/calendar.d.ts +55 -0
  287. package/dist/util/calendar.js +74 -0
  288. package/dist/util/collapsible.d.ts +57 -0
  289. package/dist/util/collapsible.js +23 -0
  290. package/dist/util/confirm-dialog.d.ts +11 -0
  291. package/dist/util/confirm-dialog.js +2 -0
  292. package/dist/util/date.d.ts +95 -0
  293. package/dist/util/date.js +237 -0
  294. package/dist/util/drawer.d.ts +12 -0
  295. package/dist/util/drawer.js +7 -0
  296. package/dist/util/dropdown.d.ts +46 -0
  297. package/dist/util/dropdown.js +21 -0
  298. package/dist/util/field.d.ts +38 -0
  299. package/dist/util/field.js +38 -0
  300. package/dist/util/fit-in-viewport.d.ts +200 -0
  301. package/dist/util/fit-in-viewport.js +399 -0
  302. package/dist/util/focus-utils.d.ts +75 -2
  303. package/dist/util/focus-utils.js +182 -9
  304. package/dist/util/hydration.d.ts +44 -0
  305. package/dist/util/hydration.js +58 -0
  306. package/dist/util/keyboard-inset.d.ts +17 -0
  307. package/dist/util/keyboard-inset.js +53 -0
  308. package/dist/util/media-grid.d.ts +50 -0
  309. package/dist/util/media-grid.js +81 -0
  310. package/dist/util/media.d.ts +5 -0
  311. package/dist/util/media.js +10 -0
  312. package/dist/util/overlay-chrome.d.ts +52 -0
  313. package/dist/util/overlay-chrome.js +56 -0
  314. package/dist/util/overlay.d.ts +71 -0
  315. package/dist/util/overlay.js +240 -0
  316. package/dist/util/photo.d.ts +214 -0
  317. package/dist/util/photo.js +206 -0
  318. package/dist/util/portal.d.ts +22 -0
  319. package/dist/util/portal.js +50 -0
  320. package/dist/util/pull-to-refresh.d.ts +26 -0
  321. package/dist/util/pull-to-refresh.js +30 -0
  322. package/dist/util/radio-keys.d.ts +24 -0
  323. package/dist/util/radio-keys.js +49 -0
  324. package/dist/util/rating.d.ts +22 -0
  325. package/dist/util/rating.js +26 -0
  326. package/dist/util/ready-made-strings.d.ts +76 -0
  327. package/dist/util/ready-made-strings.js +67 -0
  328. package/dist/util/scroll-padding.d.ts +19 -0
  329. package/dist/util/scroll-padding.js +44 -0
  330. package/dist/util/segmented-control.d.ts +15 -0
  331. package/dist/util/segmented-control.js +1 -0
  332. package/dist/util/select.d.ts +40 -0
  333. package/dist/util/select.js +63 -0
  334. package/dist/util/sheet-drag.d.ts +59 -0
  335. package/dist/util/sheet-drag.js +241 -0
  336. package/dist/util/sidebar.d.ts +62 -0
  337. package/dist/util/sidebar.js +38 -0
  338. package/dist/util/sortable-list.d.ts +64 -0
  339. package/dist/util/sortable-list.js +53 -0
  340. package/dist/util/stepper.d.ts +39 -0
  341. package/dist/util/stepper.js +38 -0
  342. package/dist/util/swipeable-list-item.d.ts +35 -0
  343. package/dist/util/swipeable-list-item.js +32 -0
  344. package/dist/util/theme-mode.d.ts +79 -0
  345. package/dist/util/theme-mode.js +121 -0
  346. package/dist/util/toaster.d.ts +92 -0
  347. package/dist/util/toaster.js +75 -0
  348. package/dist/util/tooltip.d.ts +9 -0
  349. package/dist/util/tooltip.js +18 -0
  350. package/dist/util/top-navbar.d.ts +10 -0
  351. package/dist/util/top-navbar.js +5 -0
  352. package/dist/util/touch-target.d.ts +30 -0
  353. package/dist/util/touch-target.js +30 -0
  354. package/dist/util/zabi-strings.d.ts +132 -0
  355. package/dist/util/zabi-strings.js +105 -0
  356. package/dist/zabi-components-colors.css +5 -2
  357. package/dist/zabi-components-theme-dark-only.css +543 -241
  358. package/dist/zabi-components-theme-dark.css +543 -241
  359. package/dist/zabi-components-theme-only.css +1584 -367
  360. package/dist/zabi-components-theme.css +1584 -367
  361. package/dist/zabi-components.css +3885 -518
  362. package/docs/lucide-icons.md +40 -17
  363. package/docs/theme-imports.md +81 -11
  364. package/package.json +35 -8
@@ -1,10 +1,37 @@
1
1
  <script lang="ts">
2
- import { CheckCircle, AlertTriangle, AlertCircle } from "@lucide/svelte";
2
+ import { zabiCommonStrings } from "../util/zabi-strings.js";
3
+ import Eye from "@lucide/svelte/icons/eye";
4
+ import EyeOff from "@lucide/svelte/icons/eye-off";
5
+ import { tick, type Snippet } from "svelte";
6
+ import type { HTMLInputAttributes } from "svelte/elements";
3
7
  import type { SemanticVariant, SizeVariant } from "../types/variants.js";
4
- import { generateId } from "../util/ssr-safe.js";
5
8
  import { cn } from "../util/cn.js";
9
+ import { fieldDescribedBy, fieldMessageState } from "../util/field.js";
10
+ import { generateId } from "../util/ssr-safe.js";
11
+ import { TOUCH_HIT_AREA } from "../util/touch-target.js";
12
+ import FieldMessages from "./FieldMessages.svelte";
13
+ import IconButton from "./IconButton.svelte";
6
14
 
7
- interface Props {
15
+ /**
16
+ * Other attributes (`autocomplete`, `inputmode`, `maxlength`,
17
+ * `enterkeyhint`, `data-*`, `onchange`, ...) land on the `<input>`.
18
+ */
19
+ type Props = Omit<
20
+ HTMLInputAttributes,
21
+ | "class"
22
+ | "size"
23
+ | "value"
24
+ | "type"
25
+ | "id"
26
+ | "name"
27
+ | "placeholder"
28
+ | "required"
29
+ | "disabled"
30
+ | "oninput"
31
+ | "onblur"
32
+ | "aria-label"
33
+ | "aria-describedby"
34
+ > & {
8
35
  /** Omit to auto-generate; pair with FormField `id` when used inside FormField. */
9
36
  id?: string;
10
37
  value?: string;
@@ -20,15 +47,39 @@
20
47
  loading?: boolean;
21
48
  size?: SizeVariant;
22
49
  variant?: SemanticVariant;
50
+ /** Status text under the field. Shown with a `success`, `warning` or `error` variant; for neutral help, use `hint`. */
23
51
  message?: string;
52
+ /** Help text under the field, read out with it. */
53
+ hint?: string;
54
+ /** An error under the field. It marks the field invalid and is announced; it wins over `variant` and `message`. */
55
+ error?: string;
56
+ /**
57
+ * Drawn inside the field, before the text: an icon, a unit. Decoration
58
+ * lets a press through to the field; a button or link in it is pressable.
59
+ */
60
+ leading?: Snippet;
61
+ /** Drawn inside the field, after the text and before the loading spinner's place. */
62
+ trailing?: Snippet;
63
+ /**
64
+ * With `type="password"`: adds a button at the end of the field that
65
+ * shows the password as text, and hides it again. Ignored for other types.
66
+ */
67
+ revealable?: boolean;
68
+ /**
69
+ * Accessible name of that button. It stays the same in both states;
70
+ * the button's pressed state says whether the password is showing.
71
+ */
72
+ revealLabel?: string;
24
73
  oninput?: (event: Event) => void;
25
74
  onblur?: (event: Event) => void;
26
75
  "aria-label"?: string;
27
- }
76
+ /** Ids of other elements that describe the field. The hint and the message are added after them. */
77
+ "aria-describedby"?: string | null;
78
+ };
28
79
 
29
80
  let {
30
81
  id: idProp,
31
- value = $bindable(""),
82
+ value = $bindable<Exclude<Props["value"], undefined>>(),
32
83
  type = "text",
33
84
  name = "",
34
85
  class: className = "",
@@ -41,43 +92,161 @@
41
92
  size = "md",
42
93
  variant = "default",
43
94
  message = "",
95
+ hint = "",
96
+ error = "",
97
+ leading,
98
+ trailing,
99
+ revealable = false,
100
+ revealLabel: revealLabelGiven,
44
101
  oninput,
45
102
  onblur,
103
+ "aria-describedby": describedBy,
46
104
  ...restProps
47
105
  }: Props = $props();
48
106
 
107
+ // No fallback on a bindable prop: Svelte refuses `bind:…={undefined}` on
108
+ // one that has a fallback (`props_invalid_value`), and a page that throws
109
+ // while it hydrates never becomes interactive. The default is applied
110
+ // here instead: at once, for the server and the first render, and again
111
+ // whenever a parent hands back `undefined`.
112
+ const applyDefaults = () => {
113
+ if (value === undefined) value = "";
114
+ };
115
+ applyDefaults();
116
+ $effect.pre(applyDefaults);
117
+
118
+ /** Words many components share: a `ZabiStringsProvider` above this one may give them; else English. */
119
+ const common = zabiCommonStrings();
120
+ const revealLabel = $derived(revealLabelGiven ?? common().showPassword);
121
+
49
122
  const fallbackId = generateId("input");
50
123
  const inputId = $derived(idProp ?? fallbackId);
51
124
  const isDisabled = $derived(disabled || loading);
52
125
 
126
+ const status = $derived(fieldMessageState({ variant, message, hint, error }));
127
+
128
+ /** Only a password can be revealed: on any other type the button would have nothing to do. */
129
+ const canReveal = $derived(revealable && type === "password");
130
+ let revealed = $state(false);
131
+ const shownType = $derived(canReveal && revealed ? "text" : type);
132
+
133
+ let input: HTMLInputElement | undefined = $state();
134
+
135
+ /**
136
+ * Changing `type` can drop the caret in some browsers. When the field has
137
+ * focus (a mouse or a finger pressed the button: see `keepFocus`), the
138
+ * selection is put back where it was.
139
+ */
140
+ async function restoreCaret() {
141
+ const field = input;
142
+ if (!field || document.activeElement !== field) return;
143
+ const { selectionStart, selectionEnd, selectionDirection } = field;
144
+ if (selectionStart === null || selectionEnd === null) return;
145
+ const restore = () => {
146
+ if (document.activeElement !== field) return;
147
+ try {
148
+ field.setSelectionRange(selectionStart, selectionEnd, selectionDirection ?? undefined);
149
+ } catch {
150
+ // A type without a text selection: nothing to put back.
151
+ }
152
+ };
153
+ await tick();
154
+ restore();
155
+ // Chromium moves the caret to the start once more after the type has
156
+ // changed, so it is put back again when the frame is done.
157
+ requestAnimationFrame(restore);
158
+ }
159
+
160
+ /**
161
+ * A press with a pointer does not take focus from the field: on a phone
162
+ * that would close the keyboard in the middle of typing a password. From
163
+ * the keyboard the button has focus and keeps it.
164
+ */
165
+ function keepFocus(event: Event) {
166
+ event.preventDefault();
167
+ }
168
+
53
169
  /**
54
170
  * Same fixed height scale as Button, IconButton and Select (32 / 40 / 48),
55
171
  * so a field and its submit button are the same height in a row. These
56
172
  * used to be 38 / 46 / 50 while Button was 40 / 48 / 64.
173
+ *
174
+ * Below the `sm` breakpoint the text is 16px at every size: iOS Safari
175
+ * zooms the page when a focused field is smaller. Only the font grows;
176
+ * the box keeps its height.
57
177
  */
58
178
  const sizeClass = $derived(() => {
59
179
  if (size === "sm") {
60
- return { box: "h-8 px-3", text: "text-sm", spinner: "size-3.5" };
180
+ return { box: "h-8 px-3 pointer-coarse:min-h-11", text: "text-sm max-sm:text-base", spinner: "size-3.5" };
61
181
  } else if (size === "lg") {
62
182
  return { box: "h-12 px-4", text: "text-base", spinner: "size-5" };
63
183
  } else {
64
- return { box: "h-10 px-3", text: "text-sm", spinner: "size-4" };
184
+ return { box: "h-10 px-3 pointer-coarse:min-h-11", text: "text-sm max-sm:text-base", spinner: "size-4" };
65
185
  }
66
186
  });
67
187
 
188
+ /**
189
+ * What stands inside the field at either end sits in a slot 4px in from
190
+ * the field's edge and 8px smaller than the field: 24, 32 and 40px. An
191
+ * icon is centred in it, which puts it where the text's own padding would;
192
+ * a button fills it.
193
+ */
194
+ const SLOT_INSET = 4;
195
+ const SLOTS = {
196
+ sm: { px: 24, min: "min-w-[24px]", reveal: "xs", icon: 14 },
197
+ md: { px: 32, min: "min-w-[32px]", reveal: "sm", icon: 16 },
198
+ lg: { px: 40, min: "min-w-[40px]", reveal: "md", icon: 16 },
199
+ } as const;
200
+ const slot = $derived(SLOTS[size].px);
201
+ const slotClass = $derived(SLOTS[size].min);
202
+ /** The reveal button is one size down from the field, so it fits the slot: 24, 32 and 40px. */
203
+ const revealSize = $derived(SLOTS[size].reveal);
204
+
205
+ const hasLeading = $derived(!!leading);
206
+ /** Whether the end of the field holds more than the spinner. */
207
+ const hasTrailing = $derived(!!trailing || canReveal);
208
+
209
+ /** Measured once mounted; until then, and on the server, the slots are assumed to be their least size. */
210
+ let leadingWidth = $state(0);
211
+ let trailingWidth = $state(0);
212
+
213
+ /**
214
+ * Reports the width of what stands at one end of the field, and again
215
+ * when it changes. Where there is no `ResizeObserver` (an old browser, a
216
+ * test without layout) the assumed size stays.
217
+ */
218
+ function measured(report: (width: number) => void) {
219
+ return (element: HTMLElement) => {
220
+ if (typeof ResizeObserver === "undefined") return;
221
+ const observer = new ResizeObserver(() => report(element.offsetWidth));
222
+ observer.observe(element);
223
+ return () => observer.disconnect();
224
+ };
225
+ }
226
+ const trailingCount = $derived((trailing ? 1 : 0) + (canReveal ? 1 : 0) + (loading ? 1 : 0));
227
+ const leadingPad = $derived(
228
+ hasLeading ? `${(leadingWidth || SLOT_INSET + slot) + SLOT_INSET}px` : undefined,
229
+ );
230
+ const trailingPad = $derived(
231
+ hasTrailing
232
+ ? `${(trailingWidth || SLOT_INSET + trailingCount * slot + (trailingCount - 1) * SLOT_INSET) + SLOT_INSET}px`
233
+ : undefined,
234
+ );
235
+
68
236
  const variantClass = $derived(() => {
69
- return variant === "success"
237
+ return status.variant === "success"
70
238
  ? "border-success focus-visible:border-success"
71
- : variant === "warning"
239
+ : status.variant === "warning"
72
240
  ? "border-warning focus-visible:border-warning"
73
- : variant === "error"
241
+ : status.variant === "error"
74
242
  ? "border-error focus-visible:border-error"
75
- : "border-input-border focus-visible:border-input-border";
243
+ : "border-input-border enabled:hover:border-input-border-hover focus-visible:border-input-border";
76
244
  });
77
245
 
78
246
  const inputClasses = $derived(() => {
79
247
  const sizeStyles = sizeClass();
80
- const trailingPad = loading ? (size === "sm" ? "pr-9" : "pr-10") : "";
248
+ // Room for the spinner alone. With more at the end, the measured width is used instead.
249
+ const spinnerPad = loading && !hasTrailing ? SPINNER_PAD[size] : "";
81
250
  /**
82
251
  * The field is a raised surface (`bg-input` = white in light, an inset
83
252
  * step in dark). It used to be a grey fill on a grey page, which read
@@ -87,29 +256,22 @@
87
256
  const baseClasses =
88
257
  "focus-ring w-full border bg-input hover:bg-input-hover focus-visible:bg-input-focus disabled:bg-input-disabled rounded-control transition-colors duration-150 placeholder:text-input-placeholder text-body focus:outline-none focus-visible:outline-none disabled:text-action-disabled-text disabled:cursor-not-allowed";
89
258
 
90
- return cn(`${baseClasses} ${sizeStyles.box} ${trailingPad} ${sizeStyles.text} ${variantClass()} ${className}`);
259
+ return cn(`${baseClasses} ${sizeStyles.box} ${spinnerPad} ${sizeStyles.text} ${variantClass()} ${className}`);
91
260
  });
92
261
 
93
262
  const labelClasses = $derived(
94
263
  () => "block text-sm font-medium text-label mb-2",
95
264
  );
96
265
 
97
- // Message text uses the `-text` step (700), which clears 4.5:1 on a page
98
- // surface; the solid fill step (600) is for fills, not for small text.
99
- const messageClasses = $derived(() => {
100
- const base = "text-sm mt-2 flex items-center gap-2";
101
- if (variant === "error") return `text-error-text ${base}`;
102
- if (variant === "success") return `text-success-text ${base}`;
103
- if (variant === "warning") return `text-warning-text ${base}`;
104
- return `text-description ${base}`;
105
- });
266
+ /**
267
+ * Both ends are laid over the field, which stays one box with one focus
268
+ * ring. They let a press through to it, except on something in them that
269
+ * can itself be pressed.
270
+ */
271
+ const SPINNER_PAD = { sm: "pe-9", md: "pe-10", lg: "pe-10" } as const;
106
272
 
107
- const getIcon = $derived(() => {
108
- if (variant === "error") return AlertCircle;
109
- if (variant === "success") return CheckCircle;
110
- if (variant === "warning") return AlertTriangle;
111
- return null;
112
- });
273
+ const END_BASE =
274
+ "pointer-events-none absolute inset-y-0 flex items-center gap-[4px] text-description [&_:is(a,button,input,select,textarea,[tabindex])]:pointer-events-auto";
113
275
 
114
276
  function handleInput(event: Event) {
115
277
  if (oninput) {
@@ -124,46 +286,85 @@
124
286
  {/if}
125
287
  <div class="relative">
126
288
  <input
289
+ bind:this={input}
127
290
  id={inputId}
128
- {type}
291
+ type={shownType}
129
292
  {name}
130
293
  bind:value
131
294
  {placeholder}
132
295
  {required}
133
296
  disabled={isDisabled}
134
297
  class={inputClasses()}
298
+ style:padding-inline-start={leadingPad}
299
+ style:padding-inline-end={trailingPad}
135
300
  oninput={handleInput}
136
301
  {onblur}
137
- aria-invalid={variant === "error" ? "true" : undefined}
302
+ aria-invalid={status.variant === "error" ? "true" : undefined}
138
303
  aria-required={required ? "true" : undefined}
139
304
  aria-busy={loading ? "true" : undefined}
140
- aria-describedby={message ? `${inputId}-message` : undefined}
305
+ aria-describedby={fieldDescribedBy(inputId, status, describedBy)}
141
306
  {...restProps}
142
307
  />
143
- {#if loading}
308
+ {#if leading}
144
309
  <span
145
- class="pointer-events-none absolute inset-y-0 right-3 flex items-center text-description"
146
- aria-hidden="true"
310
+ class={cn(END_BASE, "start-0 ps-[4px]")}
311
+ {@attach measured((width) => (leadingWidth = width))}
312
+ data-input-leading
147
313
  >
148
- <span
149
- class="inline-block {sizeClass()
150
- .spinner} shrink-0 animate-spin rounded-full border-2 border-current border-t-transparent opacity-70"
151
- ></span>
314
+ <span class={cn("flex items-center justify-center", slotClass)}>
315
+ {@render leading()}
316
+ </span>
317
+ </span>
318
+ {/if}
319
+ {#if loading || hasTrailing}
320
+ <span
321
+ class={cn(END_BASE, "end-0 pe-[4px]")}
322
+ {@attach measured((width) => (trailingWidth = width))}
323
+ data-input-trailing
324
+ >
325
+ {#if trailing}
326
+ <span class={cn("flex items-center justify-center", slotClass)}>
327
+ {@render trailing()}
328
+ </span>
329
+ {/if}
330
+ {#if canReveal}
331
+ <!-- A toggle with one name: "Show password", pressed or not.
332
+ A name that changed with the state would read as "Hide
333
+ password, pressed". The visible box is 4px inside the field
334
+ at every size, so its corners are the field's less 4px; on a
335
+ touch screen the layer around it is the 44px target. -->
336
+ <IconButton
337
+ variant="ghost"
338
+ size={revealSize}
339
+ bind:pressed={revealed}
340
+ label={revealLabel}
341
+ disabled={isDisabled}
342
+ class={cn(
343
+ "rounded-[calc(var(--radius-control)-4px)] pointer-coarse:relative pointer-coarse:min-h-0 pointer-coarse:min-w-0",
344
+ TOUCH_HIT_AREA,
345
+ )}
346
+ onmousedown={keepFocus}
347
+ onclick={restoreCaret}
348
+ data-input-reveal
349
+ >
350
+ {#if revealed}
351
+ <EyeOff size={SLOTS[size].icon} aria-hidden="true" />
352
+ {:else}
353
+ <Eye size={SLOTS[size].icon} aria-hidden="true" />
354
+ {/if}
355
+ </IconButton>
356
+ {/if}
357
+ {#if loading}
358
+ <!-- 12px from the field's edge, where it has always been. -->
359
+ <span class="flex items-center ps-[4px] pe-[8px]" aria-hidden="true">
360
+ <span
361
+ class="inline-block {sizeClass()
362
+ .spinner} shrink-0 animate-spin rounded-full border-2 border-current border-t-transparent opacity-70 motion-reduce:animate-pulse"
363
+ ></span>
364
+ </span>
365
+ {/if}
152
366
  </span>
153
367
  {/if}
154
368
  </div>
155
- {#if message && variant !== "default"}
156
- <p
157
- id={`${inputId}-message`}
158
- class={messageClasses()}
159
- role={variant === "error" ? "alert" : "status"}
160
- aria-live={variant === "error" ? "assertive" : "polite"}
161
- >
162
- {#if getIcon()}
163
- {@const Icon = getIcon()}
164
- <Icon size={14} class="shrink-0" />
165
- {/if}
166
- <span>{message}</span>
167
- </p>
168
- {/if}
369
+ <FieldMessages fieldId={inputId} {status} {hint} />
169
370
  </div>
@@ -1,5 +1,11 @@
1
+ import { type Snippet } from "svelte";
2
+ import type { HTMLInputAttributes } from "svelte/elements";
1
3
  import type { SemanticVariant, SizeVariant } from "../types/variants.js";
2
- interface Props {
4
+ /**
5
+ * Other attributes (`autocomplete`, `inputmode`, `maxlength`,
6
+ * `enterkeyhint`, `data-*`, `onchange`, ...) land on the `<input>`.
7
+ */
8
+ type Props = Omit<HTMLInputAttributes, "class" | "size" | "value" | "type" | "id" | "name" | "placeholder" | "required" | "disabled" | "oninput" | "onblur" | "aria-label" | "aria-describedby"> & {
3
9
  /** Omit to auto-generate; pair with FormField `id` when used inside FormField. */
4
10
  id?: string;
5
11
  value?: string;
@@ -15,11 +21,35 @@ interface Props {
15
21
  loading?: boolean;
16
22
  size?: SizeVariant;
17
23
  variant?: SemanticVariant;
24
+ /** Status text under the field. Shown with a `success`, `warning` or `error` variant; for neutral help, use `hint`. */
18
25
  message?: string;
26
+ /** Help text under the field, read out with it. */
27
+ hint?: string;
28
+ /** An error under the field. It marks the field invalid and is announced; it wins over `variant` and `message`. */
29
+ error?: string;
30
+ /**
31
+ * Drawn inside the field, before the text: an icon, a unit. Decoration
32
+ * lets a press through to the field; a button or link in it is pressable.
33
+ */
34
+ leading?: Snippet;
35
+ /** Drawn inside the field, after the text and before the loading spinner's place. */
36
+ trailing?: Snippet;
37
+ /**
38
+ * With `type="password"`: adds a button at the end of the field that
39
+ * shows the password as text, and hides it again. Ignored for other types.
40
+ */
41
+ revealable?: boolean;
42
+ /**
43
+ * Accessible name of that button. It stays the same in both states;
44
+ * the button's pressed state says whether the password is showing.
45
+ */
46
+ revealLabel?: string;
19
47
  oninput?: (event: Event) => void;
20
48
  onblur?: (event: Event) => void;
21
49
  "aria-label"?: string;
22
- }
50
+ /** Ids of other elements that describe the field. The hint and the message are added after them. */
51
+ "aria-describedby"?: string | null;
52
+ };
23
53
  declare const Input: import("svelte").Component<Props, {}, "value">;
24
54
  type Input = ReturnType<typeof Input>;
25
55
  export default Input;
@@ -32,7 +32,10 @@
32
32
  const className = $derived(cn(`${classAttr} ${legacyClass}`));
33
33
 
34
34
  const listClasses = $derived(
35
- cn(`space-y-1 overflow-hidden rounded-container ${className}`),
35
+ // The same radius as its rows. Not `overflow-hidden`: the rows fill
36
+ // the list edge to edge, so clipping here cut a row's focus ring off
37
+ // on every side but the gap to the next row.
38
+ cn(`space-y-1 rounded-[var(--zabi-list-row-radius,var(--radius-container))] ${className}`),
36
39
  );
37
40
  </script>
38
41
 
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { ArrowRight } from "@lucide/svelte";
2
+ import ArrowRight from "@lucide/svelte/icons/arrow-right";
3
3
  import type { Component, Snippet } from "svelte";
4
4
  import ListItemLeading from "./ListItemLeading.svelte";
5
5
 
@@ -21,9 +21,14 @@
21
21
  interface Props {
22
22
  item: ListItemData;
23
23
  selected?: boolean;
24
+ /** The arrow at the end of a row that can be pressed. A row with nothing to do never has one. */
24
25
  showArrow?: boolean;
25
26
  /** Shown before the chevron when `showArrow` is true. */
26
27
  trailing?: Snippet;
28
+ /**
29
+ * Called when the row is pressed. A row with neither this nor
30
+ * `item.href` has nothing to do: it is plain content, not a button.
31
+ */
27
32
  onclick?: (item: ListItemData, event: MouseEvent) => void;
28
33
  }
29
34
 
@@ -36,16 +41,37 @@
36
41
  ...restProps
37
42
  }: Props = $props();
38
43
 
44
+ /**
45
+ * A row is a link with `item.href`, a button with `onclick`, and otherwise
46
+ * plain content: it used to be a focusable button with a pointer cursor
47
+ * and an arrow, and nothing happened when it was pressed.
48
+ */
49
+ const interactive = $derived(!!item.href || !!onclick);
50
+
39
51
  const itemClasses = $derived.by(() => {
40
52
  const baseClasses =
41
- "group focus-ring flex w-full items-center gap-3 rounded-container border border-border px-4 py-3 pr-5 text-left transition-all duration-150";
42
- const interactiveClasses = item.disabled
43
- ? "cursor-not-allowed opacity-50"
44
- : "cursor-pointer focus-visible:bg-surface-hover";
45
- const selectedClasses = selected
46
- ? "bg-action-primary-subtle border-brand-500"
47
- : "";
48
- return `${baseClasses} ${interactiveClasses} ${selectedClasses}`.trim();
53
+ // The container radius, or the one a `.list-group` shell around the list
54
+ // hands down so the row is concentric with it (see app.css).
55
+ "group flex w-full items-center gap-3 rounded-[var(--zabi-list-row-radius,var(--radius-container))] border px-4 py-3 pr-5 text-left transition-all duration-150";
56
+ if (!interactive) {
57
+ const tone = selected ? "bg-action-primary-subtle border-action-primary" : "border-border";
58
+ return `${baseClasses} ${tone} ${item.disabled ? "opacity-50" : ""}`.trim();
59
+ }
60
+ const cursorClasses = item.disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer";
61
+ // A selected row keeps its fill under keyboard focus, which the focus
62
+ // ring already shows; the hover tint is for rows with no fill. The two
63
+ // never share an element, and neither does the selected border with
64
+ // `border-border`: beside each other the second of each pair lost.
65
+ //
66
+ // Hover is for a pointer that can hover (the hand-written rule is
67
+ // gated on it, so the tint does not stay on after a tap); the pressed
68
+ // fill is for every pointer, and is what a finger sees.
69
+ const stateClasses = selected
70
+ ? "bg-action-primary-subtle border-action-primary active:bg-action-primary-subtle-active"
71
+ : item.disabled
72
+ ? "border-border"
73
+ : "border-border hover:bg-surface-hover focus-visible:bg-surface-hover active:bg-surface-active";
74
+ return `focus-ring ${baseClasses} ${cursorClasses} ${stateClasses}`.trim();
49
75
  });
50
76
 
51
77
  const ariaCurrent = $derived(item.href && selected ? "page" : undefined);
@@ -101,7 +127,7 @@
101
127
  {@render trailing()}
102
128
  </span>
103
129
  {/if}
104
- {#if showArrow}
130
+ {#if showArrow && interactive}
105
131
  <ArrowRight
106
132
  size={16}
107
133
  class="shrink-0 text-description transition-transform duration-150 group-hover:translate-x-1 group-focus-visible:translate-x-1"
@@ -124,7 +150,7 @@
124
150
  >
125
151
  {@render rowContent()}
126
152
  </a>
127
- {:else}
153
+ {:else if onclick}
128
154
  <button
129
155
  type="button"
130
156
  class={itemClasses}
@@ -134,4 +160,11 @@
134
160
  >
135
161
  {@render rowContent()}
136
162
  </button>
163
+ {:else}
164
+ <!-- A plain row has no role, so `aria-disabled` on it would say nothing
165
+ to anyone: a disabled one is only drawn dimmed. There is nothing in it to
166
+ press, enabled or not. -->
167
+ <div class={itemClasses} {...restProps}>
168
+ {@render rowContent()}
169
+ </div>
137
170
  {/if}
@@ -19,9 +19,14 @@ export interface ListItemData {
19
19
  interface Props {
20
20
  item: ListItemData;
21
21
  selected?: boolean;
22
+ /** The arrow at the end of a row that can be pressed. A row with nothing to do never has one. */
22
23
  showArrow?: boolean;
23
24
  /** Shown before the chevron when `showArrow` is true. */
24
25
  trailing?: Snippet;
26
+ /**
27
+ * Called when the row is pressed. A row with neither this nor
28
+ * `item.href` has nothing to do: it is plain content, not a button.
29
+ */
25
30
  onclick?: (item: ListItemData, event: MouseEvent) => void;
26
31
  }
27
32
  declare const ListItem: Component<Props, {}, "">;
@@ -8,6 +8,10 @@
8
8
  max?: number;
9
9
  size?: "sm" | "md" | "lg";
10
10
  label?: string;
11
+ /** Names the progressbar when there is no visible `label`. Reaches the element with role="progressbar". */
12
+ "aria-label"?: string;
13
+ /** Points the progressbar at another element's id for its name. Reaches the element with role="progressbar". */
14
+ "aria-labelledby"?: string;
11
15
  }
12
16
 
13
17
  let {
@@ -16,12 +20,24 @@
16
20
  max = 100,
17
21
  size = "md",
18
22
  label = "",
23
+ "aria-label": ariaLabel,
24
+ "aria-labelledby": ariaLabelledby,
19
25
  ...restProps
20
26
  }: Props = $props();
21
27
 
22
28
  const progressId = generateId("progress");
29
+ const labelId = `${progressId}-label`;
23
30
 
24
- let percentage = $derived(Math.min(Math.max((value / max) * 100, 0), 100));
31
+ // A max that is not a positive, finite number would give an empty or full
32
+ // bar with aria-valuemax=0: fall back to the default.
33
+ let safeMax = $derived(Number.isFinite(max) && max > 0 ? max : 100);
34
+ let safeValue = $derived(
35
+ Number.isFinite(value) ? Math.min(Math.max(value, 0), safeMax) : 0,
36
+ );
37
+ let percentage = $derived((safeValue / safeMax) * 100);
38
+ let labelledby = $derived(
39
+ ariaLabelledby ?? (ariaLabel || !label ? undefined : labelId),
40
+ );
25
41
 
26
42
  let sizeClasses = $derived({
27
43
  sm: "h-1",
@@ -33,8 +49,10 @@
33
49
  <div class={className} {...restProps}>
34
50
  {#if label}
35
51
  <div class="flex justify-between items-center mb-2">
36
- <label for={progressId} class="text-sm font-medium text-label"
37
- >{label}</label
52
+ <!-- A span named through aria-labelledby, not a label: `for` only
53
+ names form controls, so it left the progressbar without a name. -->
54
+ <span id={labelId} class="text-sm font-medium text-label"
55
+ >{label}</span
38
56
  >
39
57
  <span class="text-sm text-caption">{Math.round(percentage)}%</span>
40
58
  </div>
@@ -42,15 +60,16 @@
42
60
 
43
61
  <div
44
62
  id={progressId}
45
- class="w-full border border-input-border bg-input rounded-full overflow-hidden {sizeClasses[size]}"
63
+ class="w-full border border-input-border bg-progress-track rounded-full overflow-hidden {sizeClasses[size]}"
46
64
  role="progressbar"
47
- aria-valuenow={value}
65
+ aria-label={ariaLabel}
66
+ aria-labelledby={labelledby}
67
+ aria-valuenow={safeValue}
48
68
  aria-valuemin="0"
49
- aria-valuemax={max}
50
- {...restProps}
69
+ aria-valuemax={safeMax}
51
70
  >
52
71
  <div
53
- class="h-full bg-brand-600 rounded-full"
72
+ class="h-full bg-progress-fill rounded-full"
54
73
  style="width: {percentage}%"
55
74
  ></div>
56
75
  </div>
@@ -5,6 +5,10 @@ interface Props {
5
5
  max?: number;
6
6
  size?: "sm" | "md" | "lg";
7
7
  label?: string;
8
+ /** Names the progressbar when there is no visible `label`. Reaches the element with role="progressbar". */
9
+ "aria-label"?: string;
10
+ /** Points the progressbar at another element's id for its name. Reaches the element with role="progressbar". */
11
+ "aria-labelledby"?: string;
8
12
  }
9
13
  declare const Progress: import("svelte").Component<Props, {}, "">;
10
14
  type Progress = ReturnType<typeof Progress>;