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
@@ -0,0 +1,19 @@
1
+ import type { HTMLAttributes } from "svelte/elements";
2
+ type Props = Omit<HTMLAttributes<HTMLSpanElement>, "class"> & {
3
+ /**
4
+ * Diameter, on the scale the built-in loading states use: 12, 14, 16
5
+ * and 20px. For another size pass a `size-*` utility through `class`.
6
+ */
7
+ size?: "xs" | "sm" | "md" | "lg";
8
+ /**
9
+ * What is loading. With a label the spinner is a `status` and the
10
+ * label is read out; without one it is decorative and hidden from
11
+ * assistive technology, for use beside text that already says so.
12
+ */
13
+ label?: string;
14
+ /** Extra classes for the ring: a size, a colour, an opacity. */
15
+ class?: string;
16
+ };
17
+ declare const Spinner: import("svelte").Component<Props, {}, "">;
18
+ type Spinner = ReturnType<typeof Spinner>;
19
+ export default Spinner;
@@ -1,23 +1,168 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
+ import type { HTMLTableAttributes } from 'svelte/elements';
3
4
  import { cn } from "../util/cn.js";
5
+ import { generateId } from "../util/ssr-safe.js";
4
6
 
5
- interface Props {
6
- /** Visible `<caption>` when set. */
7
+ /** `true` stacks at every width; a breakpoint name stacks below it. */
8
+ export type TableStacked = boolean | "sm" | "md" | "lg";
9
+
10
+ /** Other attributes (`id`, `data-*`, `aria-*`, ...) land on the `<table>`. */
11
+ type Props = Omit<HTMLTableAttributes, 'class' | 'children'> & {
12
+ /** `<caption>`, and the table's accessible name, when set. */
7
13
  caption?: string;
14
+ /** Keeps the caption as the accessible name but hides it from view. */
15
+ captionHidden?: boolean;
16
+ /**
17
+ * Lays each row out as a block of label and value pairs instead of
18
+ * scrolling sideways. The label of a cell is its `data-label`
19
+ * attribute, which the caller sets on every `<td>`.
20
+ */
21
+ stacked?: TableStacked;
8
22
  class?: string;
9
23
  children?: Snippet;
24
+ };
25
+
26
+ let {
27
+ caption,
28
+ captionHidden = false,
29
+ stacked = false,
30
+ class: className = '',
31
+ children,
32
+ ...restProps
33
+ }: Props = $props();
34
+
35
+ const captionId = generateId("table-caption");
36
+ let tableElement = $state<HTMLTableElement>();
37
+
38
+ /**
39
+ * The rows arrive as `children`, so the stacked layout is written from the
40
+ * table down. Tailwind only generates classes it can read whole, which is
41
+ * why each breakpoint repeats the list rather than building it.
42
+ *
43
+ * The header row is hidden from view, not removed, so cells keep their
44
+ * column header. The visible label is the cell's `data-label` drawn in
45
+ * `::before`; where the browser supports alternative text for generated
46
+ * content it is given an empty one, so a screen reader reads the column
47
+ * header once instead of the header and then the label.
48
+ *
49
+ * The label takes the start of the line with an auto margin after it, and
50
+ * the cell packs everything else at the end. So a cell's own children stay
51
+ * together however many there are, and a cell with no label still has its
52
+ * value on the value side. An empty cell collapses to nothing but is not
53
+ * hidden: taking it out of the tree would shift the cells after it under
54
+ * the wrong column headers.
55
+ */
56
+ const STACKED_CLASS = {
57
+ always:
58
+ "block min-w-0 [&>:not(thead)]:block [&>thead]:sr-only [&>*>tr]:block [&>*>tr]:py-2 " +
59
+ "[&>*>tr>*]:flex [&>*>tr>*]:items-baseline [&>*>tr>*]:justify-end " +
60
+ "[&>*>tr>*]:gap-4 [&>*>tr>*]:py-1 [&>*>tr>*]:text-end " +
61
+ "[&>*>tr>:empty]:py-0 [&>*>tr>[data-label]:not(:empty)]:before:me-auto " +
62
+ "[&>*>tr>[data-label]:not(:empty)]:before:shrink-0 " +
63
+ "[&>*>tr>[data-label]:not(:empty)]:before:text-start " +
64
+ "[&>*>tr>[data-label]:not(:empty)]:before:font-medium " +
65
+ "[&>*>tr>[data-label]:not(:empty)]:before:text-headline " +
66
+ "[&>*>tr>[data-label]:not(:empty)]:before:content-[attr(data-label)] " +
67
+ "supports-[content:'x'/'']:[&>*>tr>[data-label]:not(:empty)]:before:content-[attr(data-label)/'']",
68
+ sm:
69
+ "max-sm:block max-sm:min-w-0 max-sm:[&>:not(thead)]:block max-sm:[&>thead]:sr-only " +
70
+ "max-sm:[&>*>tr]:block max-sm:[&>*>tr]:py-2 max-sm:[&>*>tr>*]:flex " +
71
+ "max-sm:[&>*>tr>*]:items-baseline " +
72
+ "max-sm:[&>*>tr>*]:justify-end max-sm:[&>*>tr>*]:gap-4 " +
73
+ "max-sm:[&>*>tr>*]:py-1 max-sm:[&>*>tr>*]:text-end max-sm:[&>*>tr>:empty]:py-0 " +
74
+ "max-sm:[&>*>tr>[data-label]:not(:empty)]:before:me-auto " +
75
+ "max-sm:[&>*>tr>[data-label]:not(:empty)]:before:shrink-0 " +
76
+ "max-sm:[&>*>tr>[data-label]:not(:empty)]:before:text-start " +
77
+ "max-sm:[&>*>tr>[data-label]:not(:empty)]:before:font-medium " +
78
+ "max-sm:[&>*>tr>[data-label]:not(:empty)]:before:text-headline " +
79
+ "max-sm:[&>*>tr>[data-label]:not(:empty)]:before:content-[attr(data-label)] " +
80
+ "max-sm:supports-[content:'x'/'']:[&>*>tr>[data-label]:not(:empty)]:before:content-[attr(data-label)/'']",
81
+ md:
82
+ "max-md:block max-md:min-w-0 max-md:[&>:not(thead)]:block max-md:[&>thead]:sr-only " +
83
+ "max-md:[&>*>tr]:block max-md:[&>*>tr]:py-2 max-md:[&>*>tr>*]:flex " +
84
+ "max-md:[&>*>tr>*]:items-baseline " +
85
+ "max-md:[&>*>tr>*]:justify-end max-md:[&>*>tr>*]:gap-4 " +
86
+ "max-md:[&>*>tr>*]:py-1 max-md:[&>*>tr>*]:text-end max-md:[&>*>tr>:empty]:py-0 " +
87
+ "max-md:[&>*>tr>[data-label]:not(:empty)]:before:me-auto " +
88
+ "max-md:[&>*>tr>[data-label]:not(:empty)]:before:shrink-0 " +
89
+ "max-md:[&>*>tr>[data-label]:not(:empty)]:before:text-start " +
90
+ "max-md:[&>*>tr>[data-label]:not(:empty)]:before:font-medium " +
91
+ "max-md:[&>*>tr>[data-label]:not(:empty)]:before:text-headline " +
92
+ "max-md:[&>*>tr>[data-label]:not(:empty)]:before:content-[attr(data-label)] " +
93
+ "max-md:supports-[content:'x'/'']:[&>*>tr>[data-label]:not(:empty)]:before:content-[attr(data-label)/'']",
94
+ lg:
95
+ "max-lg:block max-lg:min-w-0 max-lg:[&>:not(thead)]:block max-lg:[&>thead]:sr-only " +
96
+ "max-lg:[&>*>tr]:block max-lg:[&>*>tr]:py-2 max-lg:[&>*>tr>*]:flex " +
97
+ "max-lg:[&>*>tr>*]:items-baseline " +
98
+ "max-lg:[&>*>tr>*]:justify-end max-lg:[&>*>tr>*]:gap-4 " +
99
+ "max-lg:[&>*>tr>*]:py-1 max-lg:[&>*>tr>*]:text-end max-lg:[&>*>tr>:empty]:py-0 " +
100
+ "max-lg:[&>*>tr>[data-label]:not(:empty)]:before:me-auto " +
101
+ "max-lg:[&>*>tr>[data-label]:not(:empty)]:before:shrink-0 " +
102
+ "max-lg:[&>*>tr>[data-label]:not(:empty)]:before:text-start " +
103
+ "max-lg:[&>*>tr>[data-label]:not(:empty)]:before:font-medium " +
104
+ "max-lg:[&>*>tr>[data-label]:not(:empty)]:before:text-headline " +
105
+ "max-lg:[&>*>tr>[data-label]:not(:empty)]:before:content-[attr(data-label)] " +
106
+ "max-lg:supports-[content:'x'/'']:[&>*>tr>[data-label]:not(:empty)]:before:content-[attr(data-label)/'']",
107
+ } as const;
108
+
109
+ const stackedClass = $derived(
110
+ stacked === true ? STACKED_CLASS.always : stacked ? STACKED_CLASS[stacked] : "",
111
+ );
112
+
113
+ /**
114
+ * Some browsers stop treating a table as one once its boxes are no longer
115
+ * table boxes, and the rows then reach a screen reader as loose text.
116
+ * Explicit roles keep the structure whatever `display` says. The rows are
117
+ * the caller's markup, so the roles are set on the rendered elements and
118
+ * kept up as rows come and go; a role the caller set is left alone.
119
+ */
120
+ function restoreTableRoles(table: HTMLTableElement) {
121
+ const set = (element: Element, role: string) => {
122
+ if (!element.hasAttribute("role")) element.setAttribute("role", role);
123
+ };
124
+ for (const group of table.querySelectorAll(":scope > :is(thead, tbody, tfoot)")) {
125
+ set(group, "rowgroup");
126
+ for (const row of group.querySelectorAll(":scope > tr")) {
127
+ set(row, "row");
128
+ for (const cell of row.querySelectorAll(":scope > :is(td, th)")) {
129
+ if (cell.tagName === "TD") set(cell, "cell");
130
+ else if (cell.getAttribute("scope") === "row") set(cell, "rowheader");
131
+ else if (cell.getAttribute("scope") === "col") set(cell, "columnheader");
132
+ else set(cell, group.tagName === "THEAD" ? "columnheader" : "rowheader");
133
+ }
134
+ }
135
+ }
10
136
  }
11
137
 
12
- let { caption, class: className = '', children }: Props = $props();
138
+ $effect(() => {
139
+ const table = tableElement;
140
+ if (!table || !stacked) return;
141
+
142
+ restoreTableRoles(table);
143
+ const observer = new MutationObserver(() => restoreTableRoles(table));
144
+ observer.observe(table, { childList: true, subtree: true });
145
+ return () => observer.disconnect();
146
+ });
13
147
  </script>
14
148
 
15
149
  <div
16
- class={cn("overflow-x-auto rounded-control border border-border shadow-sm", className)}
150
+ class={cn("overflow-x-auto rounded-control border border-border bg-card shadow-sm", className)}
17
151
  >
18
- <table class="w-full min-w-[20rem] border-collapse text-left text-sm text-body">
152
+ <table
153
+ bind:this={tableElement}
154
+ class={cn("w-full min-w-[20rem] border-collapse text-left text-sm text-body", stackedClass)}
155
+ role={stacked ? "table" : undefined}
156
+ aria-labelledby={stacked && caption ? captionId : undefined}
157
+ {...restProps}
158
+ >
19
159
  {#if caption}
20
- <caption class="border-b border-border bg-base-50 px-4 py-3 text-left text-headline">
160
+ <caption
161
+ id={captionId}
162
+ class={captionHidden
163
+ ? "sr-only"
164
+ : "border-b border-border bg-surface-elevated px-4 py-3 text-left text-headline"}
165
+ >
21
166
  {caption}
22
167
  </caption>
23
168
  {/if}
@@ -1,10 +1,22 @@
1
1
  import type { Snippet } from 'svelte';
2
- interface Props {
3
- /** Visible `<caption>` when set. */
2
+ import type { HTMLTableAttributes } from 'svelte/elements';
3
+ /** `true` stacks at every width; a breakpoint name stacks below it. */
4
+ export type TableStacked = boolean | "sm" | "md" | "lg";
5
+ /** Other attributes (`id`, `data-*`, `aria-*`, ...) land on the `<table>`. */
6
+ type Props = Omit<HTMLTableAttributes, 'class' | 'children'> & {
7
+ /** `<caption>`, and the table's accessible name, when set. */
4
8
  caption?: string;
9
+ /** Keeps the caption as the accessible name but hides it from view. */
10
+ captionHidden?: boolean;
11
+ /**
12
+ * Lays each row out as a block of label and value pairs instead of
13
+ * scrolling sideways. The label of a cell is its `data-label`
14
+ * attribute, which the caller sets on every `<td>`.
15
+ */
16
+ stacked?: TableStacked;
5
17
  class?: string;
6
18
  children?: Snippet;
7
- }
19
+ };
8
20
  declare const Table: import("svelte").Component<Props, {}, "">;
9
21
  type Table = ReturnType<typeof Table>;
10
22
  export default Table;
@@ -0,0 +1,140 @@
1
+ <script lang="ts">
2
+ import type { SizeVariant } from "../types/variants.js";
3
+ import { cn } from "../util/cn.js";
4
+ import Input from "./Input.svelte";
5
+
6
+ /**
7
+ * What DateField and TimeField share. Internal: it is not exported from
8
+ * the package, and its props are those of the two fields.
9
+ *
10
+ * It is an `Input` with a native `type`, so the box, the sizes, the label,
11
+ * the hint and the error message are Input's own and cannot drift from it.
12
+ * What is added here is only what a native date or time input needs to
13
+ * look like one: see the classes and the style block below.
14
+ */
15
+ interface Props {
16
+ kind: "date" | "time";
17
+ id?: string;
18
+ value?: string;
19
+ name?: string;
20
+ label?: string;
21
+ hideLabel?: boolean;
22
+ hint?: string;
23
+ error?: string;
24
+ min?: string;
25
+ max?: string;
26
+ step?: number | "any";
27
+ required?: boolean;
28
+ disabled?: boolean;
29
+ readonly?: boolean;
30
+ size?: SizeVariant;
31
+ class?: string;
32
+ "aria-describedby"?: string | null;
33
+ }
34
+
35
+ let {
36
+ kind,
37
+ id: idProp,
38
+ value = $bindable<Exclude<Props["value"], undefined>>(),
39
+ name = "",
40
+ label = "",
41
+ hideLabel = false,
42
+ hint = "",
43
+ error = "",
44
+ min,
45
+ max,
46
+ step,
47
+ required = false,
48
+ disabled = false,
49
+ readonly = false,
50
+ size = "md",
51
+ class: className = "",
52
+ "aria-describedby": describedBy,
53
+ ...restProps
54
+ }: Props = $props();
55
+
56
+ // No fallback on a bindable prop: Svelte refuses `bind:…={undefined}` on
57
+ // one that has a fallback (`props_invalid_value`), and a page that throws
58
+ // while it hydrates never becomes interactive. The default is applied
59
+ // here instead: at once, for the server and the first render, and again
60
+ // whenever a parent hands back `undefined`.
61
+ const applyDefaults = () => {
62
+ if (value === undefined) value = "";
63
+ };
64
+ applyDefaults();
65
+ $effect.pre(applyDefaults);
66
+
67
+ /** What Input does not declare, but passes on to the `<input>`. */
68
+ const native = $derived({
69
+ min,
70
+ max,
71
+ step,
72
+ readonly: readonly || undefined,
73
+ "data-empty": value ? undefined : "",
74
+ ...restProps,
75
+ });
76
+
77
+ const fieldClasses = $derived(
78
+ cn(
79
+ "zabi-temporal-field",
80
+ // Its own look off: WebKit on iOS otherwise draws a date input as
81
+ // a button, with its own height and centred text. `block` and
82
+ // `min-w-0`: an empty one there has no intrinsic width.
83
+ "block min-w-0 appearance-none",
84
+ // The format the browser shows in an empty field ("mm/dd/yyyy")
85
+ // is a hint, not a value: in the placeholder colour.
86
+ !value && "text-input-placeholder",
87
+ className,
88
+ ),
89
+ );
90
+ </script>
91
+
92
+ <div>
93
+ <Input
94
+ id={idProp}
95
+ type={kind}
96
+ bind:value
97
+ {name}
98
+ {label}
99
+ {hideLabel}
100
+ {required}
101
+ {disabled}
102
+ {size}
103
+ {hint}
104
+ {error}
105
+ aria-describedby={describedBy}
106
+ class={fieldClasses}
107
+ {...native}
108
+ />
109
+ </div>
110
+
111
+ <style>
112
+ /*
113
+ * The parts of a native date or time input that utilities cannot reach.
114
+ * Global selectors under one class: the `<input>` is rendered by Input,
115
+ * so a scoped selector would not match it.
116
+ */
117
+
118
+ /* WebKit on iOS centres the value, and gives an empty one no height. */
119
+ :global(.zabi-temporal-field::-webkit-date-and-time-value) {
120
+ min-height: 1.5em;
121
+ text-align: start;
122
+ }
123
+
124
+ /* The editable segments in Chromium and desktop WebKit: no extra box
125
+ inside the field, so the text sits where an Input's does. */
126
+ :global(.zabi-temporal-field::-webkit-datetime-edit) {
127
+ padding: 0;
128
+ }
129
+
130
+ /* The browser's calendar or clock button. It follows `color-scheme`,
131
+ which the theme's dark block sets (see app.css); nothing here has to. */
132
+ :global(.zabi-temporal-field::-webkit-calendar-picker-indicator) {
133
+ cursor: pointer;
134
+ }
135
+ :global(.zabi-temporal-field:disabled::-webkit-calendar-picker-indicator),
136
+ :global(.zabi-temporal-field:read-only::-webkit-calendar-picker-indicator) {
137
+ cursor: default;
138
+ }
139
+
140
+ </style>
@@ -0,0 +1,32 @@
1
+ import type { SizeVariant } from "../types/variants.js";
2
+ /**
3
+ * What DateField and TimeField share. Internal: it is not exported from
4
+ * the package, and its props are those of the two fields.
5
+ *
6
+ * It is an `Input` with a native `type`, so the box, the sizes, the label,
7
+ * the hint and the error message are Input's own and cannot drift from it.
8
+ * What is added here is only what a native date or time input needs to
9
+ * look like one: see the classes and the style block below.
10
+ */
11
+ interface Props {
12
+ kind: "date" | "time";
13
+ id?: string;
14
+ value?: string;
15
+ name?: string;
16
+ label?: string;
17
+ hideLabel?: boolean;
18
+ hint?: string;
19
+ error?: string;
20
+ min?: string;
21
+ max?: string;
22
+ step?: number | "any";
23
+ required?: boolean;
24
+ disabled?: boolean;
25
+ readonly?: boolean;
26
+ size?: SizeVariant;
27
+ class?: string;
28
+ "aria-describedby"?: string | null;
29
+ }
30
+ declare const TemporalField: import("svelte").Component<Props, {}, "value">;
31
+ type TemporalField = ReturnType<typeof TemporalField>;
32
+ export default TemporalField;
@@ -1,12 +1,15 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
+ import type { HTMLAttributes } from 'svelte/elements';
3
4
 
4
5
  import { cn } from "../util/cn.js";
5
- type Tone = 'body' | 'description' | 'caption' | 'headline' | 'label' | 'error';
6
+ import type { OnFillTone } from "../types/variants.js";
7
+ type Tone = 'body' | 'description' | 'caption' | 'headline' | 'label' | 'error' | OnFillTone;
6
8
  type Size = 'xs' | 'sm' | 'md' | 'lg';
7
9
  type Weight = 'normal' | 'medium' | 'semibold' | 'bold';
8
10
 
9
- interface Props {
11
+ /** Other attributes (`id`, `data-*`, `aria-*`, ...) land on the element. */
12
+ type Props = Omit<HTMLAttributes<HTMLElement>, 'class'> & {
10
13
  as?: 'p' | 'span' | 'div';
11
14
  tone?: Tone;
12
15
  size?: Size;
@@ -17,7 +20,7 @@
17
20
  weight?: Weight;
18
21
  class?: string;
19
22
  children?: Snippet;
20
- }
23
+ };
21
24
 
22
25
  let {
23
26
  as: Tag = 'p',
@@ -26,6 +29,7 @@
26
29
  weight,
27
30
  class: className = '',
28
31
  children,
32
+ ...restProps
29
33
  }: Props = $props();
30
34
 
31
35
  const toneClass: Record<Tone, string> = {
@@ -35,6 +39,9 @@
35
39
  headline: 'text-headline',
36
40
  label: 'text-label',
37
41
  error: 'text-error',
42
+ inherit: 'text-inherit',
43
+ 'on-brand': 'text-on-brand',
44
+ 'on-accent': 'text-on-accent',
38
45
  };
39
46
 
40
47
  /**
@@ -63,13 +70,18 @@
63
70
  /** Labels read as labels by default; callers can still override. */
64
71
  const defaultWeight: Weight = $derived(tone === 'label' ? 'medium' : 'normal');
65
72
  const resolvedWeight = $derived(weight ?? defaultWeight);
73
+ const classes = $derived(
74
+ cn(toneClass[tone], sizeClass[size], weightClass[resolvedWeight], className),
75
+ );
66
76
  </script>
67
77
 
68
- <svelte:element
69
- this={Tag}
70
- class={cn(toneClass[tone], sizeClass[size], weightClass[resolvedWeight], className)}
71
- >
72
- {#if children}
73
- {@render children()}
74
- {/if}
75
- </svelte:element>
78
+ <!-- One branch per tag, not a dynamic element: hydration takes a dynamic
79
+ element out and puts it back, which blurs a link or a field inside it that the
80
+ user had already tabbed to. -->
81
+ {#if Tag === 'span'}
82
+ <span class={classes} {...restProps}>{@render children?.()}</span>
83
+ {:else if Tag === 'div'}
84
+ <div class={classes} {...restProps}>{@render children?.()}</div>
85
+ {:else}
86
+ <p class={classes} {...restProps}>{@render children?.()}</p>
87
+ {/if}
@@ -1,8 +1,11 @@
1
1
  import type { Snippet } from 'svelte';
2
- type Tone = 'body' | 'description' | 'caption' | 'headline' | 'label' | 'error';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import type { OnFillTone } from "../types/variants.js";
4
+ type Tone = 'body' | 'description' | 'caption' | 'headline' | 'label' | 'error' | OnFillTone;
3
5
  type Size = 'xs' | 'sm' | 'md' | 'lg';
4
6
  type Weight = 'normal' | 'medium' | 'semibold' | 'bold';
5
- interface Props {
7
+ /** Other attributes (`id`, `data-*`, `aria-*`, ...) land on the element. */
8
+ type Props = Omit<HTMLAttributes<HTMLElement>, 'class'> & {
6
9
  as?: 'p' | 'span' | 'div';
7
10
  tone?: Tone;
8
11
  size?: Size;
@@ -13,7 +16,7 @@ interface Props {
13
16
  weight?: Weight;
14
17
  class?: string;
15
18
  children?: Snippet;
16
- }
19
+ };
17
20
  declare const Text: import("svelte").Component<Props, {}, "">;
18
21
  type Text = ReturnType<typeof Text>;
19
22
  export default Text;
@@ -1,10 +1,28 @@
1
1
  <script lang="ts">
2
- import { CheckCircle, AlertTriangle, AlertCircle } from '@lucide/svelte';
2
+ import type { HTMLTextareaAttributes } from 'svelte/elements';
3
3
  import type { SemanticVariant, SizeVariant } from '../types/variants.js';
4
4
  import { generateId } from "../util/ssr-safe.js";
5
5
  import { cn } from "../util/cn.js";
6
+ import { fieldDescribedBy, fieldMessageState } from "../util/field.js";
7
+ import FieldMessages from "./FieldMessages.svelte";
6
8
 
7
- interface Props {
9
+ /**
10
+ * Other attributes (`autocomplete`, `inputmode`, `maxlength`,
11
+ * `enterkeyhint`, `data-*`, `onchange`, ...) land on the `<textarea>`.
12
+ */
13
+ type Props = Omit<
14
+ HTMLTextareaAttributes,
15
+ | 'class'
16
+ | 'value'
17
+ | 'id'
18
+ | 'name'
19
+ | 'placeholder'
20
+ | 'required'
21
+ | 'disabled'
22
+ | 'rows'
23
+ | 'oninput'
24
+ | 'aria-describedby'
25
+ > & {
8
26
  id?: string;
9
27
  value?: string;
10
28
  name?: string;
@@ -18,13 +36,20 @@
18
36
  rows?: number;
19
37
  size?: SizeVariant;
20
38
  variant?: SemanticVariant;
39
+ /** Status text under the field. Shown with a `success`, `warning` or `error` variant; for neutral help, use `hint`. */
21
40
  message?: string;
41
+ /** Help text under the field, read out with it. */
42
+ hint?: string;
43
+ /** An error under the field. It marks the field invalid and is announced; it wins over `variant` and `message`. */
44
+ error?: string;
22
45
  oninput?: (event: Event) => void;
23
- }
46
+ /** Ids of other elements that describe the field. The hint and the message are added after them. */
47
+ "aria-describedby"?: string | null;
48
+ };
24
49
 
25
50
  let {
26
51
  id: idProp,
27
- value = $bindable(''),
52
+ value = $bindable<Exclude<Props["value"], undefined>>(),
28
53
  name = '',
29
54
  class: className = '',
30
55
  label = '',
@@ -37,27 +62,44 @@
37
62
  size = 'md',
38
63
  variant = 'default',
39
64
  message = '',
65
+ hint = '',
66
+ error = '',
40
67
  oninput,
68
+ 'aria-describedby': describedBy,
41
69
  ...restProps
42
70
  }: Props = $props();
43
71
 
72
+ // No fallback on a bindable prop: Svelte refuses `bind:…={undefined}` on
73
+ // one that has a fallback (`props_invalid_value`), and a page that throws
74
+ // while it hydrates never becomes interactive. The default is applied
75
+ // here instead: at once, for the server and the first render, and again
76
+ // whenever a parent hands back `undefined`.
77
+ const applyDefaults = () => {
78
+ if (value === undefined) value = '';
79
+ };
80
+ applyDefaults();
81
+ $effect.pre(applyDefaults);
82
+
44
83
  const fallbackId = generateId('textarea');
45
84
  const textareaId = $derived(idProp ?? fallbackId);
46
85
  const isDisabled = $derived(disabled || loading);
47
86
 
87
+ const status = $derived(fieldMessageState({ variant, message, hint, error }));
88
+
48
89
  const variantClass = $derived(() => {
49
- return variant === 'success'
90
+ return status.variant === 'success'
50
91
  ? 'border-success focus-visible:border-success'
51
- : variant === 'warning'
92
+ : status.variant === 'warning'
52
93
  ? 'border-warning focus-visible:border-warning'
53
- : variant === 'error'
94
+ : status.variant === 'error'
54
95
  ? 'border-error focus-visible:border-error'
55
- : 'border-input-border';
96
+ : 'border-input-border enabled:hover:border-input-border-hover';
56
97
  });
57
98
 
99
+ // 16px below `sm`: iOS Safari zooms the page when a focused field is smaller.
58
100
  const textareaClasses = $derived(() => {
59
101
  const baseClasses =
60
- 'focus-ring w-full border bg-input hover:bg-input-hover focus-visible:bg-input-focus disabled:bg-input-disabled rounded-control transition-colors duration-150 placeholder:text-input-placeholder text-body focus:outline-none focus-visible:outline-none disabled:text-action-disabled-text disabled:cursor-not-allowed resize-y px-3 py-2 text-sm leading-6';
102
+ 'focus-ring w-full border bg-input hover:bg-input-hover focus-visible:bg-input-focus disabled:bg-input-disabled rounded-control transition-colors duration-150 placeholder:text-input-placeholder text-body focus:outline-none focus-visible:outline-none disabled:text-action-disabled-text disabled:cursor-not-allowed resize-y px-3 py-2 text-sm max-sm:text-base leading-6';
61
103
 
62
104
  return cn(`${baseClasses} ${variantClass()} ${className}`);
63
105
  });
@@ -66,24 +108,6 @@
66
108
  () => 'mb-1 block text-sm font-medium text-label',
67
109
  );
68
110
 
69
- const messageClasses = $derived(() => {
70
- if (variant === 'error') {
71
- return 'mt-1 flex items-center gap-2 text-sm text-error';
72
- } else if (variant === 'success') {
73
- return 'mt-1 flex items-center gap-2 text-sm text-success';
74
- } else if (variant === 'warning') {
75
- return 'mt-1 flex items-center gap-2 text-sm text-warning';
76
- }
77
- return 'mt-1 flex items-center gap-2 text-sm text-description';
78
- });
79
-
80
- const getIcon = $derived(() => {
81
- if (variant === 'error') return AlertCircle;
82
- if (variant === 'success') return CheckCircle;
83
- if (variant === 'warning') return AlertTriangle;
84
- return null;
85
- });
86
-
87
111
  function handleInput(event: Event) {
88
112
  const target = event.target as HTMLTextAreaElement;
89
113
  value = target.value;
@@ -106,10 +130,10 @@
106
130
  {rows}
107
131
  class={textareaClasses()}
108
132
  oninput={handleInput}
109
- aria-invalid={variant === 'error' ? 'true' : undefined}
133
+ aria-invalid={status.variant === 'error' ? 'true' : undefined}
110
134
  aria-required={required ? 'true' : undefined}
111
135
  aria-busy={loading ? 'true' : undefined}
112
- aria-describedby={message ? `${textareaId}-message` : undefined}
136
+ aria-describedby={fieldDescribedBy(textareaId, status, describedBy)}
113
137
  {...restProps}
114
138
  ></textarea>
115
139
  {#if loading}
@@ -118,23 +142,10 @@
118
142
  aria-hidden="true"
119
143
  >
120
144
  <span
121
- class="inline-block size-5 shrink-0 animate-spin rounded-full border-2 border-current border-t-transparent opacity-70"
145
+ class="inline-block size-5 shrink-0 animate-spin rounded-full border-2 border-current border-t-transparent opacity-70 motion-reduce:animate-pulse"
122
146
  ></span>
123
147
  </span>
124
148
  {/if}
125
149
  </div>
126
- {#if message && variant !== 'default'}
127
- <p
128
- id={`${textareaId}-message`}
129
- class={messageClasses()}
130
- role={variant === 'error' ? 'alert' : 'status'}
131
- aria-live={variant === 'error' ? 'assertive' : 'polite'}
132
- >
133
- {#if getIcon()}
134
- {@const Icon = getIcon()}
135
- <Icon size={14} class="shrink-0" />
136
- {/if}
137
- <span>{message}</span>
138
- </p>
139
- {/if}
150
+ <FieldMessages fieldId={textareaId} {status} {hint} gap="mt-1" />
140
151
  </div>