zabi-components 7.0.2 → 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 (433) hide show
  1. package/CHANGELOG.md +1924 -0
  2. package/README.md +287 -11
  3. package/THEME.md +149 -332
  4. package/THEMING.md +1329 -163
  5. package/dist/atoms/ActionPanel.svelte +24 -9
  6. package/dist/atoms/ActionPanel.svelte.d.ts +2 -0
  7. package/dist/atoms/Avatar.svelte +104 -0
  8. package/dist/atoms/Avatar.svelte.d.ts +34 -0
  9. package/dist/atoms/Badge.svelte +124 -58
  10. package/dist/atoms/Badge.svelte.d.ts +17 -5
  11. package/dist/atoms/Button.svelte +212 -62
  12. package/dist/atoms/Button.svelte.d.ts +33 -2
  13. package/dist/atoms/Card.svelte +31 -28
  14. package/dist/atoms/Card.svelte.d.ts +5 -2
  15. package/dist/atoms/CardContent.svelte +10 -2
  16. package/dist/atoms/CardContent.svelte.d.ts +2 -0
  17. package/dist/atoms/CardFooter.svelte +10 -2
  18. package/dist/atoms/CardFooter.svelte.d.ts +2 -0
  19. package/dist/atoms/CardHeader.svelte +26 -6
  20. package/dist/atoms/CardHeader.svelte.d.ts +2 -0
  21. package/dist/atoms/Checkbox.svelte +52 -21
  22. package/dist/atoms/Checkbox.svelte.d.ts +6 -2
  23. package/dist/atoms/CodeBlock.svelte +35 -9
  24. package/dist/atoms/CodeBlock.svelte.d.ts +13 -3
  25. package/dist/atoms/ColorPicker.svelte +272 -52
  26. package/dist/atoms/ColorPicker.svelte.d.ts +6 -1
  27. package/dist/atoms/Container.svelte +17 -10
  28. package/dist/atoms/DateField.svelte +107 -0
  29. package/dist/atoms/DateField.svelte.d.ts +53 -0
  30. package/dist/atoms/Divider.svelte +4 -4
  31. package/dist/atoms/FeatureCard.svelte +4 -3
  32. package/dist/atoms/FieldMessages.svelte +69 -0
  33. package/dist/atoms/FieldMessages.svelte.d.ts +23 -0
  34. package/dist/atoms/FloatingActionButton.svelte +174 -0
  35. package/dist/atoms/FloatingActionButton.svelte.d.ts +51 -0
  36. package/dist/atoms/Heading.svelte +93 -32
  37. package/dist/atoms/Heading.svelte.d.ts +24 -4
  38. package/dist/atoms/IconButton.svelte +242 -53
  39. package/dist/atoms/IconButton.svelte.d.ts +49 -4
  40. package/dist/atoms/Input.svelte +267 -74
  41. package/dist/atoms/Input.svelte.d.ts +32 -2
  42. package/dist/atoms/List.svelte +13 -2
  43. package/dist/atoms/List.svelte.d.ts +2 -0
  44. package/dist/atoms/ListItem.svelte +44 -11
  45. package/dist/atoms/ListItem.svelte.d.ts +5 -0
  46. package/dist/atoms/ListItemLeading.svelte +4 -2
  47. package/dist/atoms/ListItemLeading.svelte.d.ts +1 -1
  48. package/dist/atoms/OptimizedImage.svelte +11 -2
  49. package/dist/atoms/OptimizedImage.svelte.d.ts +2 -0
  50. package/dist/atoms/Progress.svelte +31 -9
  51. package/dist/atoms/Progress.svelte.d.ts +6 -0
  52. package/dist/atoms/Radio.svelte +23 -3
  53. package/dist/atoms/Radio.svelte.d.ts +6 -2
  54. package/dist/atoms/Rating.svelte +494 -0
  55. package/dist/atoms/Rating.svelte.d.ts +56 -0
  56. package/dist/atoms/Select.svelte +475 -135
  57. package/dist/atoms/Select.svelte.d.ts +52 -2
  58. package/dist/atoms/SelectionControl.svelte +99 -18
  59. package/dist/atoms/SelectionControl.svelte.d.ts +6 -3
  60. package/dist/atoms/Skeleton.svelte +5 -3
  61. package/dist/atoms/Slider.svelte +309 -0
  62. package/dist/atoms/Slider.svelte.d.ts +40 -0
  63. package/dist/atoms/Spinner.svelte +57 -0
  64. package/dist/atoms/Spinner.svelte.d.ts +19 -0
  65. package/dist/atoms/Table.svelte +152 -6
  66. package/dist/atoms/Table.svelte.d.ts +15 -3
  67. package/dist/atoms/TemporalField.svelte +140 -0
  68. package/dist/atoms/TemporalField.svelte.d.ts +32 -0
  69. package/dist/atoms/Text.svelte +56 -15
  70. package/dist/atoms/Text.svelte.d.ts +13 -4
  71. package/dist/atoms/Textarea.svelte +57 -45
  72. package/dist/atoms/Textarea.svelte.d.ts +14 -2
  73. package/dist/atoms/ThemeToggle.svelte +238 -56
  74. package/dist/atoms/ThemeToggle.svelte.d.ts +28 -3
  75. package/dist/atoms/TimeField.svelte +108 -0
  76. package/dist/atoms/TimeField.svelte.d.ts +54 -0
  77. package/dist/atoms/Toast.svelte +25 -10
  78. package/dist/atoms/Toast.svelte.d.ts +2 -0
  79. package/dist/atoms/Toggle.svelte +60 -19
  80. package/dist/atoms/Toggle.svelte.d.ts +18 -2
  81. package/dist/atoms/Tooltip.svelte +583 -40
  82. package/dist/atoms/Tooltip.svelte.d.ts +26 -0
  83. package/dist/atoms/ZabiStringsProvider.svelte +45 -0
  84. package/dist/atoms/ZabiStringsProvider.svelte.d.ts +27 -0
  85. package/dist/atoms/index.d.ts +17 -0
  86. package/dist/atoms/index.js +12 -0
  87. package/dist/atoms/selection-control.styles.d.ts +36 -5
  88. package/dist/atoms/selection-control.styles.js +40 -9
  89. package/dist/components/atoms/index.d.ts +17 -0
  90. package/dist/components/atoms/index.d.ts.map +1 -1
  91. package/dist/components/atoms/selection-control.styles.d.ts +36 -5
  92. package/dist/components/atoms/selection-control.styles.d.ts.map +1 -1
  93. package/dist/components/index.d.ts +83 -3
  94. package/dist/components/index.d.ts.map +1 -1
  95. package/dist/components/molecules/index.d.ts +46 -0
  96. package/dist/components/molecules/index.d.ts.map +1 -1
  97. package/dist/components/molecules/toast-store.d.ts +53 -7
  98. package/dist/components/molecules/toast-store.d.ts.map +1 -1
  99. package/dist/components/organisms/index.d.ts +6 -0
  100. package/dist/components/organisms/index.d.ts.map +1 -1
  101. package/dist/components/types/variants.d.ts +15 -1
  102. package/dist/components/types/variants.d.ts.map +1 -1
  103. package/dist/components/util/app-shell.d.ts +28 -0
  104. package/dist/components/util/app-shell.d.ts.map +1 -0
  105. package/dist/components/util/avatar.d.ts +36 -0
  106. package/dist/components/util/avatar.d.ts.map +1 -0
  107. package/dist/components/util/bottom-sheet.d.ts +30 -0
  108. package/dist/components/util/bottom-sheet.d.ts.map +1 -0
  109. package/dist/components/util/bottom-tab-bar.d.ts +35 -0
  110. package/dist/components/util/bottom-tab-bar.d.ts.map +1 -0
  111. package/dist/components/util/calendar.d.ts +56 -0
  112. package/dist/components/util/calendar.d.ts.map +1 -0
  113. package/dist/components/util/cn.d.ts +8 -0
  114. package/dist/components/util/cn.d.ts.map +1 -0
  115. package/dist/components/util/collapsible.d.ts +58 -0
  116. package/dist/components/util/collapsible.d.ts.map +1 -0
  117. package/dist/components/util/confirm-dialog.d.ts +12 -0
  118. package/dist/components/util/confirm-dialog.d.ts.map +1 -0
  119. package/dist/components/util/date.d.ts +96 -0
  120. package/dist/components/util/date.d.ts.map +1 -0
  121. package/dist/components/util/drawer.d.ts +13 -0
  122. package/dist/components/util/drawer.d.ts.map +1 -0
  123. package/dist/components/util/dropdown.d.ts +47 -0
  124. package/dist/components/util/dropdown.d.ts.map +1 -0
  125. package/dist/components/util/field.d.ts +39 -0
  126. package/dist/components/util/field.d.ts.map +1 -0
  127. package/dist/components/util/fit-in-viewport.d.ts +201 -0
  128. package/dist/components/util/fit-in-viewport.d.ts.map +1 -0
  129. package/dist/components/util/focus-utils.d.ts +75 -2
  130. package/dist/components/util/focus-utils.d.ts.map +1 -1
  131. package/dist/components/util/hydration.d.ts +45 -0
  132. package/dist/components/util/hydration.d.ts.map +1 -0
  133. package/dist/components/util/keyboard-inset.d.ts +18 -0
  134. package/dist/components/util/keyboard-inset.d.ts.map +1 -0
  135. package/dist/components/util/media-grid.d.ts +51 -0
  136. package/dist/components/util/media-grid.d.ts.map +1 -0
  137. package/dist/components/util/media.d.ts +6 -0
  138. package/dist/components/util/media.d.ts.map +1 -0
  139. package/dist/components/util/overlay-chrome.d.ts +53 -0
  140. package/dist/components/util/overlay-chrome.d.ts.map +1 -0
  141. package/dist/components/util/overlay.d.ts +72 -0
  142. package/dist/components/util/overlay.d.ts.map +1 -0
  143. package/dist/components/util/photo.d.ts +215 -0
  144. package/dist/components/util/photo.d.ts.map +1 -0
  145. package/dist/components/util/portal.d.ts +23 -0
  146. package/dist/components/util/portal.d.ts.map +1 -0
  147. package/dist/components/util/pull-to-refresh.d.ts +27 -0
  148. package/dist/components/util/pull-to-refresh.d.ts.map +1 -0
  149. package/dist/components/util/radio-keys.d.ts +25 -0
  150. package/dist/components/util/radio-keys.d.ts.map +1 -0
  151. package/dist/components/util/rating.d.ts +23 -0
  152. package/dist/components/util/rating.d.ts.map +1 -0
  153. package/dist/components/util/ready-made-strings.d.ts +77 -0
  154. package/dist/components/util/ready-made-strings.d.ts.map +1 -0
  155. package/dist/components/util/scroll-padding.d.ts +20 -0
  156. package/dist/components/util/scroll-padding.d.ts.map +1 -0
  157. package/dist/components/util/segmented-control.d.ts +16 -0
  158. package/dist/components/util/segmented-control.d.ts.map +1 -0
  159. package/dist/components/util/select.d.ts +41 -0
  160. package/dist/components/util/select.d.ts.map +1 -0
  161. package/dist/components/util/sheet-drag.d.ts +60 -0
  162. package/dist/components/util/sheet-drag.d.ts.map +1 -0
  163. package/dist/components/util/sidebar.d.ts +63 -0
  164. package/dist/components/util/sidebar.d.ts.map +1 -0
  165. package/dist/components/util/sortable-list.d.ts +65 -0
  166. package/dist/components/util/sortable-list.d.ts.map +1 -0
  167. package/dist/components/util/ssr-safe.d.ts +4 -4
  168. package/dist/components/util/ssr-safe.d.ts.map +1 -1
  169. package/dist/components/util/stepper.d.ts +40 -0
  170. package/dist/components/util/stepper.d.ts.map +1 -0
  171. package/dist/components/util/swipeable-list-item.d.ts +36 -0
  172. package/dist/components/util/swipeable-list-item.d.ts.map +1 -0
  173. package/dist/components/util/theme-mode.d.ts +80 -0
  174. package/dist/components/util/theme-mode.d.ts.map +1 -0
  175. package/dist/components/util/toaster.d.ts +93 -0
  176. package/dist/components/util/toaster.d.ts.map +1 -0
  177. package/dist/components/util/tooltip.d.ts +10 -0
  178. package/dist/components/util/tooltip.d.ts.map +1 -0
  179. package/dist/components/util/top-navbar.d.ts +11 -0
  180. package/dist/components/util/top-navbar.d.ts.map +1 -0
  181. package/dist/components/util/touch-target.d.ts +31 -0
  182. package/dist/components/util/touch-target.d.ts.map +1 -0
  183. package/dist/components/util/zabi-strings.d.ts +133 -0
  184. package/dist/components/util/zabi-strings.d.ts.map +1 -0
  185. package/dist/create-theme/cli.js +169 -0
  186. package/dist/create-theme/index.d.ts +176 -0
  187. package/dist/create-theme/index.js +817 -0
  188. package/dist/create-theme/lib/ramp-math.js +128 -0
  189. package/dist/create-theme/lib/resolve.js +141 -0
  190. package/dist/create-theme/theme-data.js +1116 -0
  191. package/dist/index.d.ts +83 -3
  192. package/dist/index.js +60 -8
  193. package/dist/molecules/Alert.svelte +73 -66
  194. package/dist/molecules/Alert.svelte.d.ts +9 -1
  195. package/dist/molecules/AppBar.svelte +415 -0
  196. package/dist/molecules/AppBar.svelte.d.ts +69 -0
  197. package/dist/molecules/AvatarGroup.svelte +116 -0
  198. package/dist/molecules/AvatarGroup.svelte.d.ts +40 -0
  199. package/dist/molecules/BottomSheet.svelte +540 -0
  200. package/dist/molecules/BottomSheet.svelte.d.ts +93 -0
  201. package/dist/molecules/BottomTabBar.svelte +297 -0
  202. package/dist/molecules/BottomTabBar.svelte.d.ts +64 -0
  203. package/dist/molecules/Calendar.svelte +597 -0
  204. package/dist/molecules/Calendar.svelte.d.ts +72 -0
  205. package/dist/molecules/Collapsible.svelte +243 -0
  206. package/dist/molecules/Collapsible.svelte.d.ts +44 -0
  207. package/dist/molecules/CollapsibleGroup.svelte +126 -0
  208. package/dist/molecules/CollapsibleGroup.svelte.d.ts +14 -0
  209. package/dist/molecules/ComponentDemo.svelte +25 -5
  210. package/dist/molecules/ComponentDemo.svelte.d.ts +5 -0
  211. package/dist/molecules/ConfirmDialog.svelte +253 -0
  212. package/dist/molecules/ConfirmDialog.svelte.d.ts +55 -0
  213. package/dist/molecules/ContactForm.svelte +46 -19
  214. package/dist/molecules/ContactForm.svelte.d.ts +10 -0
  215. package/dist/molecules/Drawer.svelte +394 -0
  216. package/dist/molecules/Drawer.svelte.d.ts +55 -0
  217. package/dist/molecules/Dropdown.svelte +540 -50
  218. package/dist/molecules/Dropdown.svelte.d.ts +49 -6
  219. package/dist/molecules/DropdownItem.svelte +122 -0
  220. package/dist/molecules/DropdownItem.svelte.d.ts +28 -0
  221. package/dist/molecules/EmptyState.svelte +56 -10
  222. package/dist/molecules/EmptyState.svelte.d.ts +8 -0
  223. package/dist/molecules/Form.svelte +25 -14
  224. package/dist/molecules/Form.svelte.d.ts +13 -5
  225. package/dist/molecules/FormField.svelte +11 -2
  226. package/dist/molecules/FormField.svelte.d.ts +2 -0
  227. package/dist/molecules/Header.svelte +13 -3
  228. package/dist/molecules/Header.svelte.d.ts +2 -0
  229. package/dist/molecules/ImageUpload.svelte +422 -70
  230. package/dist/molecules/ImageUpload.svelte.d.ts +61 -5
  231. package/dist/molecules/MediaGrid.svelte +527 -0
  232. package/dist/molecules/MediaGrid.svelte.d.ts +77 -0
  233. package/dist/molecules/Modal.svelte +340 -45
  234. package/dist/molecules/Modal.svelte.d.ts +59 -7
  235. package/dist/molecules/NavigationMenu.svelte +15 -3
  236. package/dist/molecules/NavigationMenu.svelte.d.ts +4 -0
  237. package/dist/molecules/NavigationMenuContent.svelte +91 -36
  238. package/dist/molecules/NavigationMenuContent.svelte.d.ts +2 -0
  239. package/dist/molecules/NavigationMenuItem.svelte +10 -3
  240. package/dist/molecules/NavigationMenuItem.svelte.d.ts +2 -0
  241. package/dist/molecules/NavigationMenuLink.svelte +10 -2
  242. package/dist/molecules/NavigationMenuLink.svelte.d.ts +2 -0
  243. package/dist/molecules/NavigationMenuList.svelte +19 -4
  244. package/dist/molecules/NavigationMenuList.svelte.d.ts +2 -0
  245. package/dist/molecules/NavigationMenuTrigger.svelte +16 -3
  246. package/dist/molecules/NavigationMenuTrigger.svelte.d.ts +2 -0
  247. package/dist/molecules/Page.svelte +38 -2
  248. package/dist/molecules/Page.svelte.d.ts +13 -0
  249. package/dist/molecules/PhotoGrid.svelte +433 -0
  250. package/dist/molecules/PhotoGrid.svelte.d.ts +62 -0
  251. package/dist/molecules/PhotoViewer.svelte +1116 -0
  252. package/dist/molecules/PhotoViewer.svelte.d.ts +56 -0
  253. package/dist/molecules/PropsTable.svelte +26 -11
  254. package/dist/molecules/PropsTable.svelte.d.ts +5 -0
  255. package/dist/molecules/PullToRefresh.svelte +263 -0
  256. package/dist/molecules/PullToRefresh.svelte.d.ts +52 -0
  257. package/dist/molecules/RadioGroup.svelte +57 -15
  258. package/dist/molecules/RadioGroup.svelte.d.ts +16 -3
  259. package/dist/molecules/Section.svelte +13 -5
  260. package/dist/molecules/Section.svelte.d.ts +3 -1
  261. package/dist/molecules/SegmentedControl.svelte +323 -0
  262. package/dist/molecules/SegmentedControl.svelte.d.ts +34 -0
  263. package/dist/molecules/SidebarBrandHeader.svelte +24 -5
  264. package/dist/molecules/SidebarBrandHeader.svelte.d.ts +5 -0
  265. package/dist/molecules/SidebarFooter.svelte +39 -8
  266. package/dist/molecules/SidebarFooter.svelte.d.ts +5 -0
  267. package/dist/molecules/SidebarNavSection.svelte +12 -4
  268. package/dist/molecules/SidebarNavSection.svelte.d.ts +2 -0
  269. package/dist/molecules/SlideUp.svelte +253 -30
  270. package/dist/molecules/SlideUp.svelte.d.ts +31 -4
  271. package/dist/molecules/SortableList.svelte +632 -0
  272. package/dist/molecules/SortableList.svelte.d.ts +69 -0
  273. package/dist/molecules/Stepper.svelte +548 -0
  274. package/dist/molecules/Stepper.svelte.d.ts +39 -0
  275. package/dist/molecules/StickyActionBar.svelte +191 -0
  276. package/dist/molecules/StickyActionBar.svelte.d.ts +65 -0
  277. package/dist/molecules/SwipeableListItem.svelte +334 -0
  278. package/dist/molecules/SwipeableListItem.svelte.d.ts +54 -0
  279. package/dist/molecules/Tabs.svelte +254 -24
  280. package/dist/molecules/Tabs.svelte.d.ts +15 -4
  281. package/dist/molecules/Toaster.svelte +352 -6
  282. package/dist/molecules/Toaster.svelte.d.ts +95 -2
  283. package/dist/molecules/ToasterToast.svelte +279 -62
  284. package/dist/molecules/ToasterToast.svelte.d.ts +11 -0
  285. package/dist/molecules/UnsavedChangesBar.svelte +212 -0
  286. package/dist/molecules/UnsavedChangesBar.svelte.d.ts +43 -0
  287. package/dist/molecules/index.d.ts +46 -0
  288. package/dist/molecules/index.js +27 -0
  289. package/dist/molecules/toast-store.d.ts +53 -7
  290. package/dist/molecules/toast-store.js +21 -0
  291. package/dist/organisms/AppShell.svelte +196 -0
  292. package/dist/organisms/AppShell.svelte.d.ts +61 -0
  293. package/dist/organisms/SidebarAccountPanel.svelte +111 -12
  294. package/dist/organisms/SidebarAccountPanel.svelte.d.ts +21 -0
  295. package/dist/organisms/SidebarNavigation.svelte +193 -69
  296. package/dist/organisms/SidebarNavigation.svelte.d.ts +31 -1
  297. package/dist/organisms/SidebarPanel.svelte +45 -16
  298. package/dist/organisms/SidebarPanel.svelte.d.ts +2 -0
  299. package/dist/organisms/SidebarShell.svelte +263 -0
  300. package/dist/organisms/SidebarShell.svelte.d.ts +106 -0
  301. package/dist/organisms/TopNavbar.svelte +329 -33
  302. package/dist/organisms/TopNavbar.svelte.d.ts +28 -0
  303. package/dist/organisms/index.d.ts +6 -0
  304. package/dist/organisms/index.js +4 -0
  305. package/dist/routes/lib/variant-utils.d.ts +26 -0
  306. package/dist/routes/lib/{variant-utils.ts → variant-utils.js} +15 -46
  307. package/dist/types/components.d.ts +4 -0
  308. package/dist/types/components.d.ts.map +1 -1
  309. package/dist/types/events.js +1 -0
  310. package/dist/types/index.d.ts +605 -25
  311. package/dist/types/index.d.ts.map +1 -1
  312. package/dist/types/index.js +3 -0
  313. package/dist/types/variants.ts +17 -1
  314. package/dist/util/app-shell.d.ts +27 -0
  315. package/dist/util/app-shell.js +72 -0
  316. package/dist/util/avatar.d.ts +35 -0
  317. package/dist/util/avatar.js +52 -0
  318. package/dist/util/bottom-sheet.d.ts +29 -0
  319. package/dist/util/bottom-sheet.js +43 -0
  320. package/dist/util/bottom-tab-bar.d.ts +34 -0
  321. package/dist/util/bottom-tab-bar.js +54 -0
  322. package/dist/util/calendar.d.ts +55 -0
  323. package/dist/util/calendar.js +74 -0
  324. package/dist/util/cn.d.ts +7 -0
  325. package/dist/util/cn.js +48 -0
  326. package/dist/util/collapsible.d.ts +57 -0
  327. package/dist/util/collapsible.js +23 -0
  328. package/dist/util/confirm-dialog.d.ts +11 -0
  329. package/dist/util/confirm-dialog.js +2 -0
  330. package/dist/util/date.d.ts +95 -0
  331. package/dist/util/date.js +237 -0
  332. package/dist/util/drawer.d.ts +12 -0
  333. package/dist/util/drawer.js +7 -0
  334. package/dist/util/dropdown.d.ts +46 -0
  335. package/dist/util/dropdown.js +21 -0
  336. package/dist/util/field.d.ts +38 -0
  337. package/dist/util/field.js +38 -0
  338. package/dist/util/fit-in-viewport.d.ts +200 -0
  339. package/dist/util/fit-in-viewport.js +399 -0
  340. package/dist/util/focus-utils.d.ts +75 -2
  341. package/dist/util/focus-utils.js +182 -9
  342. package/dist/util/hydration.d.ts +44 -0
  343. package/dist/util/hydration.js +58 -0
  344. package/dist/util/keyboard-inset.d.ts +17 -0
  345. package/dist/util/keyboard-inset.js +53 -0
  346. package/dist/util/media-grid.d.ts +50 -0
  347. package/dist/util/media-grid.js +81 -0
  348. package/dist/util/media.d.ts +5 -0
  349. package/dist/util/media.js +10 -0
  350. package/dist/util/overlay-chrome.d.ts +52 -0
  351. package/dist/util/overlay-chrome.js +56 -0
  352. package/dist/util/overlay.d.ts +71 -0
  353. package/dist/util/overlay.js +240 -0
  354. package/dist/util/photo.d.ts +214 -0
  355. package/dist/util/photo.js +206 -0
  356. package/dist/util/portal.d.ts +22 -0
  357. package/dist/util/portal.js +50 -0
  358. package/dist/util/pull-to-refresh.d.ts +26 -0
  359. package/dist/util/pull-to-refresh.js +30 -0
  360. package/dist/util/radio-keys.d.ts +24 -0
  361. package/dist/util/radio-keys.js +49 -0
  362. package/dist/util/rating.d.ts +22 -0
  363. package/dist/util/rating.js +26 -0
  364. package/dist/util/ready-made-strings.d.ts +76 -0
  365. package/dist/util/ready-made-strings.js +67 -0
  366. package/dist/util/scroll-padding.d.ts +19 -0
  367. package/dist/util/scroll-padding.js +44 -0
  368. package/dist/util/segmented-control.d.ts +15 -0
  369. package/dist/util/segmented-control.js +1 -0
  370. package/dist/util/select.d.ts +40 -0
  371. package/dist/util/select.js +63 -0
  372. package/dist/util/sheet-drag.d.ts +59 -0
  373. package/dist/util/sheet-drag.js +241 -0
  374. package/dist/util/sidebar.d.ts +62 -0
  375. package/dist/util/sidebar.js +38 -0
  376. package/dist/util/sortable-list.d.ts +64 -0
  377. package/dist/util/sortable-list.js +53 -0
  378. package/dist/util/ssr-safe.d.ts +4 -4
  379. package/dist/util/stepper.d.ts +39 -0
  380. package/dist/util/stepper.js +38 -0
  381. package/dist/util/swipeable-list-item.d.ts +35 -0
  382. package/dist/util/swipeable-list-item.js +32 -0
  383. package/dist/util/theme-mode.d.ts +79 -0
  384. package/dist/util/theme-mode.js +121 -0
  385. package/dist/util/toaster.d.ts +92 -0
  386. package/dist/util/toaster.js +75 -0
  387. package/dist/util/tooltip.d.ts +9 -0
  388. package/dist/util/tooltip.js +18 -0
  389. package/dist/util/top-navbar.d.ts +10 -0
  390. package/dist/util/top-navbar.js +5 -0
  391. package/dist/util/touch-target.d.ts +30 -0
  392. package/dist/util/touch-target.js +30 -0
  393. package/dist/util/zabi-strings.d.ts +132 -0
  394. package/dist/util/zabi-strings.js +105 -0
  395. package/dist/zabi-components-colors.css +5 -2
  396. package/dist/zabi-components-theme-dark-only.css +574 -225
  397. package/dist/zabi-components-theme-dark.css +574 -225
  398. package/dist/zabi-components-theme-only.css +1680 -317
  399. package/dist/zabi-components-theme.css +1680 -317
  400. package/dist/zabi-components.css +4473 -1158
  401. package/docs/lucide-icons.md +41 -16
  402. package/docs/theme-imports.md +81 -11
  403. package/package.json +51 -29
  404. package/dist/lib/layout-width-tokens.d.ts +0 -17
  405. package/dist/lib/layout-width-tokens.d.ts.map +0 -1
  406. package/dist/lib/showcase/component-docs/Button.d.ts +0 -3
  407. package/dist/lib/showcase/component-docs/Button.d.ts.map +0 -1
  408. package/dist/lib/showcase/component-docs/Input.d.ts +0 -3
  409. package/dist/lib/showcase/component-docs/Input.d.ts.map +0 -1
  410. package/dist/lib/showcase/component-docs/List.d.ts +0 -3
  411. package/dist/lib/showcase/component-docs/List.d.ts.map +0 -1
  412. package/dist/lib/showcase/component-docs/Modal.d.ts +0 -3
  413. package/dist/lib/showcase/component-docs/Modal.d.ts.map +0 -1
  414. package/dist/lib/showcase/component-docs/Radio.d.ts +0 -3
  415. package/dist/lib/showcase/component-docs/Radio.d.ts.map +0 -1
  416. package/dist/lib/showcase/component-docs/SidebarNavigation.d.ts +0 -3
  417. package/dist/lib/showcase/component-docs/SidebarNavigation.d.ts.map +0 -1
  418. package/dist/lib/showcase/component-docs/Skeleton.d.ts +0 -3
  419. package/dist/lib/showcase/component-docs/Skeleton.d.ts.map +0 -1
  420. package/dist/lib/showcase/component-docs/_shared.d.ts +0 -12
  421. package/dist/lib/showcase/component-docs/_shared.d.ts.map +0 -1
  422. package/dist/lib/showcase/component-docs/index.d.ts +0 -4
  423. package/dist/lib/showcase/component-docs/index.d.ts.map +0 -1
  424. package/dist/lib/showcase/components-catalog.d.ts +0 -3
  425. package/dist/lib/showcase/components-catalog.d.ts.map +0 -1
  426. package/dist/lib/showcase/components-showcase-constants.d.ts +0 -89
  427. package/dist/lib/showcase/components-showcase-constants.d.ts.map +0 -1
  428. package/dist/lib/showcase/docs-sidebar-helpers.d.ts +0 -15
  429. package/dist/lib/showcase/docs-sidebar-helpers.d.ts.map +0 -1
  430. package/dist/routes/lib/focus-utils.d.ts +0 -12
  431. package/dist/routes/lib/focus-utils.ts +0 -110
  432. package/dist/routes/lib/ssr-safe.ts +0 -58
  433. package/scripts/fix-lucide-svelte-icon-dts.js +0 -35
@@ -1,15 +1,54 @@
1
1
  <script lang="ts">
2
+ import { mergeStrings } from "../util/ready-made-strings.js";
3
+ import { zabiCommonStrings, zabiStringsFor } from "../util/zabi-strings.js";
2
4
  import Dropdown from "../molecules/Dropdown.svelte";
3
5
  import Input from "./Input.svelte";
6
+ import Check from "@lucide/svelte/icons/check";
7
+ import ChevronDown from "@lucide/svelte/icons/chevron-down";
8
+ import { onMount, tick, untrack } from "svelte";
9
+ import type { HTMLButtonAttributes } from "svelte/elements";
4
10
  import {
5
- ChevronDown,
6
- CheckCircle,
7
- AlertTriangle,
8
- AlertCircle,
9
- } from "@lucide/svelte";
11
+ DEFAULT_SELECT_STRINGS,
12
+ NATIVE_SELECT_ATTRIBUTE,
13
+ choiceBeforeMount,
14
+ type SelectPresentation,
15
+ type SelectStrings,
16
+ } from "../util/select.js";
10
17
  import { generateId } from "../util/ssr-safe.js";
18
+ import { isInsideToastRegion } from "../util/focus-utils.js";
19
+ import { cn } from "../util/cn.js";
20
+ import { fieldDescribedBy, fieldMessageState } from "../util/field.js";
21
+ import FieldMessages from "./FieldMessages.svelte";
11
22
 
12
- interface Props {
23
+ /**
24
+ * Other attributes (`data-*`, `aria-*`, `onfocus`, ...) land on the
25
+ * trigger, which is a `<button>`: it is the control that takes focus and
26
+ * that the label names.
27
+ *
28
+ * The server's HTML also carries a native `<select>` with the same
29
+ * options. Until the component has mounted, and for good where there are
30
+ * no scripts, that is the control: it is what is seen, pressed and
31
+ * submitted. Once mounted the trigger and the list take over, and the
32
+ * native select stays, out of sight and out of reach, as the form control
33
+ * (`name`, `required`), kept at the same value. With
34
+ * `presentation="native"` it is the control throughout.
35
+ */
36
+ type Props = Omit<
37
+ HTMLButtonAttributes,
38
+ | "class"
39
+ | "value"
40
+ | "name"
41
+ | "disabled"
42
+ | "type"
43
+ | "onchange"
44
+ | "onclick"
45
+ | "aria-describedby"
46
+ | "children"
47
+ > & {
48
+ /** Extra classes for the host element. */
49
+ class?: string;
50
+ /** Id of the trigger. Omit to auto-generate. */
51
+ id?: string;
13
52
  value?: string | number | undefined;
14
53
  options?: Array<{
15
54
  value: string | number;
@@ -17,9 +56,26 @@
17
56
  disabled?: boolean;
18
57
  }>;
19
58
  searchable?: boolean;
59
+ /**
60
+ * The component's own texts, for another language. Whatever is left
61
+ * out keeps its English default. The six older props for single texts
62
+ * (`placeholder`, `searchPlaceholder`, `noResultsText`,
63
+ * `loadingText`, `emptyStateTitle`, `emptyStateDescription`) win
64
+ * over it.
65
+ */
66
+ strings?: Partial<SelectStrings>;
20
67
  searchPlaceholder?: string;
21
68
  maxMenuHeight?: string;
22
69
  menuWidth?: string;
70
+ /**
71
+ * How the list is shown. `auto`: in a BottomSheet on a phone (a touch
72
+ * screen narrower than 640px), under the trigger everywhere else.
73
+ * `popover` and `sheet` are always the one or the other. `native`:
74
+ * only the browser's own `<select>`, styled as the field, with the
75
+ * platform's picker. It shows the options' labels and nothing else:
76
+ * no search field, and none of the loading or empty states.
77
+ */
78
+ presentation?: SelectPresentation;
23
79
  noResultsText?: string;
24
80
  isLoading?: boolean;
25
81
  loadingText?: string;
@@ -28,127 +84,187 @@
28
84
  emptyStateActionLabel?: string;
29
85
  placeholder?: string;
30
86
  label?: string;
87
+ /** The name the chosen value is submitted under, by the native `<select>`. */
88
+ name?: string;
89
+ /**
90
+ * The form is not sent while nothing is chosen. Checked by the
91
+ * browser on the native `<select>`; once mounted, the browser's own
92
+ * message is shown under the field and focus goes to the trigger.
93
+ */
31
94
  required?: boolean;
32
95
  disabled?: boolean;
33
96
  size?: "sm" | "md" | "lg";
34
97
  variant?: "default" | "success" | "warning" | "error";
98
+ /** Status text under the field. Shown with a `success`, `warning` or `error` variant; for neutral help, use `hint`. */
35
99
  message?: string;
100
+ /** Help text under the field, read out with it. */
101
+ hint?: string;
102
+ /** An error under the field. It marks the field invalid and is announced; it wins over `variant` and `message`. */
103
+ error?: string;
36
104
  onchange?: (event: Event) => void;
37
105
  onEmptyStateAction?: () => void;
38
- }
106
+ /** Ids of other elements that describe the field. The hint and the message are added after them. */
107
+ "aria-describedby"?: string | null;
108
+ };
39
109
 
40
110
  let {
111
+ class: className = "",
112
+ id: idProp,
41
113
  value = $bindable(undefined),
42
114
  options = [],
43
115
  searchable = true,
44
- searchPlaceholder = "Search options",
45
- maxMenuHeight = "60vh",
116
+ strings,
117
+ searchPlaceholder,
118
+ maxMenuHeight = "60dvh",
46
119
  menuWidth = "100%",
47
- noResultsText = "No results found",
120
+ presentation = "auto",
121
+ noResultsText,
48
122
  isLoading = false,
49
- loadingText = "Loading options...",
50
- emptyStateTitle = "No options available",
51
- emptyStateDescription = "Add an option to start making selections.",
123
+ loadingText,
124
+ emptyStateTitle,
125
+ emptyStateDescription,
52
126
  emptyStateActionLabel = "",
53
- placeholder = "Select an option",
127
+ placeholder,
54
128
  label = "",
129
+ name = "",
55
130
  required = false,
56
131
  disabled = false,
57
132
  size = "md",
58
133
  variant = "default",
59
134
  message = "",
135
+ hint = "",
136
+ error = "",
60
137
  onchange,
61
138
  onEmptyStateAction,
139
+ "aria-describedby": describedBy,
62
140
  ...restProps
63
141
  }: Props = $props();
64
142
 
143
+ /** The defaults, then a provider's words, then `strings`, then the older props where they are set. */
144
+ const provided = zabiStringsFor("select");
145
+ const common = zabiCommonStrings();
146
+ const text = $derived<SelectStrings>({
147
+ ...mergeStrings(
148
+ {
149
+ ...DEFAULT_SELECT_STRINGS,
150
+ // The sheet's three buttons say what every sheet's do.
151
+ closeLabel: common().close,
152
+ expandLabel: common().expand,
153
+ collapseLabel: common().collapse,
154
+ },
155
+ provided(),
156
+ strings,
157
+ ),
158
+ ...(placeholder !== undefined && { placeholder }),
159
+ ...(searchPlaceholder !== undefined && { searchPlaceholder }),
160
+ ...(noResultsText !== undefined && { noResults: noResultsText }),
161
+ ...(loadingText !== undefined && { loading: loadingText }),
162
+ ...(emptyStateTitle !== undefined && { emptyTitle: emptyStateTitle }),
163
+ ...(emptyStateDescription !== undefined && { emptyDescription: emptyStateDescription }),
164
+ });
165
+
166
+ const isNative = $derived(presentation === "native");
167
+ /**
168
+ * False on the server and until the component has mounted: the native
169
+ * select is the control. True after: the trigger is. The two have the
170
+ * same box, so nothing moves when one replaces the other.
171
+ */
172
+ let mounted = $state(false);
173
+ /** Whether the native select is what is seen and pressed. */
174
+ const nativeShown = $derived(isNative || !mounted);
175
+
176
+ let nativeElement = $state<HTMLSelectElement | null>(null);
177
+ /** What the browser said was wrong with the hidden native select (`required`), in its own words. */
178
+ let nativeError = $state("");
179
+
180
+ const status = $derived(
181
+ fieldMessageState({ variant, message, hint, error: error || nativeError }),
182
+ );
183
+
65
184
  let isOpen = $state(false);
66
185
  let selectContainer: HTMLDivElement;
67
186
  let searchQuery = $state("");
68
187
 
188
+ // Same fixed height scale as Button, IconButton and Input (32 / 40 / 48),
189
+ // and the same 16px text below `sm`, so a Select beside an Input matches it.
69
190
  const sizeClass = $derived(() => {
70
- if (size === "sm") {
71
- return {
72
- padding: "px-4 py-2",
73
- text: "text-sm",
74
- leading: "leading-5",
75
- };
76
- } else if (size === "lg") {
77
- return {
78
- padding: "px-4 py-3",
79
- text: "text-base",
80
- leading: "leading-6",
81
- };
82
- } else {
83
- return {
84
- padding: "px-4 py-2.5",
85
- text: "text-base",
86
- leading: "leading-6",
87
- };
88
- }
191
+ // A minimum height, so a label that wraps makes the trigger taller
192
+ // instead of being cut. One line is exactly 32, 40 or 48px: the
193
+ // padding leaves room for the line and the border inside it, and is
194
+ // a step smaller below `sm`, where the text is 16px on a 24px line. `native` is the same box for the native
195
+ // select, which is always one line.
196
+ if (size === "sm") return { box: "min-h-8 px-3 py-1 max-sm:py-0 pointer-coarse:min-h-11", text: "text-sm max-sm:text-base", native: "h-8 ps-3 pe-9 pointer-coarse:h-11", chevron: "end-3" };
197
+ if (size === "lg") return { box: "min-h-12 px-4 py-2", text: "text-base", native: "h-12 ps-4 pe-10", chevron: "end-4" };
198
+ return { box: "min-h-10 px-3 py-2 max-sm:py-1 pointer-coarse:min-h-11", text: "text-sm max-sm:text-base", native: "h-10 ps-3 pe-9 pointer-coarse:h-11", chevron: "end-3" };
89
199
  });
90
200
 
91
201
  const variantClass = $derived(() => {
92
- return variant === "success"
202
+ return status.variant === "success"
93
203
  ? "border-success focus-visible:border-success"
94
- : variant === "warning"
204
+ : status.variant === "warning"
95
205
  ? "border-warning focus-visible:border-warning"
96
- : variant === "error"
206
+ : status.variant === "error"
97
207
  ? "border-error focus-visible:border-error"
98
- : "border-input-border";
208
+ : "border-input-border enabled:hover:border-input-border-hover";
99
209
  });
100
210
 
211
+ /** What the trigger and the visible native select share: the field's surface, edge and states. */
212
+ const fieldSurface =
213
+ "focus-ring w-full min-w-0 cursor-pointer rounded-control border bg-input text-body transition-colors duration-150 hover:bg-input-hover active:bg-input-active focus-visible:bg-input-focus focus:outline-none focus-visible:outline-none disabled:cursor-not-allowed disabled:bg-input-disabled disabled:text-action-disabled-text";
214
+
101
215
  const triggerClasses = $derived(() => {
102
216
  const sizeStyles = sizeClass();
103
- const baseClasses =
104
- "focus-ring flex w-full cursor-pointer items-center justify-between rounded-lg border bg-input text-body transition-all duration-200 hover:bg-input-hover active:bg-input-focus focus-visible:bg-input-focus focus:outline-none focus-visible:outline-none disabled:cursor-not-allowed disabled:bg-input-disabled disabled:opacity-50";
217
+ const baseClasses = `${fieldSurface} flex items-center justify-between gap-2`;
105
218
 
106
- return `${baseClasses} ${sizeStyles.padding} ${sizeStyles.text} ${sizeStyles.leading} ${variantClass()}`.trim();
219
+ return cn(
220
+ `${baseClasses} ${sizeStyles.box} ${sizeStyles.text} ${variantClass()}`,
221
+ // Until it has mounted the native select stands in its place.
222
+ !mounted && "hidden",
223
+ );
107
224
  });
108
225
 
109
- const labelClasses = $derived(
110
- () => "block text-sm font-medium text-label mb-1",
111
- );
112
-
113
- const messageClasses = $derived(() => {
114
- if (variant === "error") {
115
- return "text-error text-sm mt-1 flex items-center gap-1.5 w-full";
116
- } else if (variant === "success") {
117
- return "text-success text-sm mt-1 flex items-center gap-1.5 w-full";
118
- } else if (variant === "warning") {
119
- return "text-warning text-sm mt-1 flex items-center gap-1.5 w-full";
226
+ const nativeClasses = $derived.by(() => {
227
+ const sizeStyles = sizeClass();
228
+ if (!nativeShown) {
229
+ // Mounted, with a list of our own: still the form control, laid
230
+ // over the trigger so the browser's validation has a place to
231
+ // point at, but not seen, pressed or reached. `invisible`
232
+ // (`visibility: hidden`) is what takes it out of the tab order
233
+ // and the accessibility tree for certain; a form still sends and
234
+ // validates it.
235
+ return "pointer-events-none invisible absolute inset-0 h-full w-full";
120
236
  }
121
- return "text-description text-sm mt-1 flex items-center gap-1.5 w-full";
122
- });
123
-
124
- const getIcon = $derived(() => {
125
- if (variant === "error") return AlertCircle;
126
- if (variant === "success") return CheckCircle;
127
- if (variant === "warning") return AlertTriangle;
128
- return null;
237
+ return cn(
238
+ `${fieldSurface} block appearance-none truncate ${sizeStyles.native} ${sizeStyles.text} ${variantClass()}`,
239
+ !disabled && isEmpty() && "text-input-placeholder",
240
+ );
129
241
  });
130
242
 
131
- const selectId = generateId("select");
243
+ const labelClasses = $derived(
244
+ () => "block text-sm font-medium text-label mb-2",
245
+ );
132
246
 
133
- const selectedLabel = $derived(() => {
134
- if (isEmpty()) {
135
- return String(placeholder || "Select an option");
136
- }
137
- const selected = options.find((opt) => opt.value === value);
138
- return selected?.label
139
- ? String(selected.label)
140
- : String(placeholder || "Select an option");
141
- });
247
+ const fallbackId = generateId("select");
248
+ const selectId = $derived(idProp ?? fallbackId);
142
249
 
143
250
  const isEmpty = $derived(() => {
144
251
  return value === undefined || value === null || value === "";
145
252
  });
146
253
 
147
- const normalizedOptions = $derived(() => {
148
- const fallbackOptions = (restProps as { options?: Props["options"] })
149
- .options;
150
- return Array.from(options ?? fallbackOptions ?? []);
151
- });
254
+ /** String comparison so `"2"` and `2` match — same rule as Dropdown's `selectedValue`. */
255
+ function isSameValue(
256
+ a: string | number | null | undefined,
257
+ b: string | number | null | undefined,
258
+ ): boolean {
259
+ if (a === undefined || a === null || b === undefined || b === null) {
260
+ return false;
261
+ }
262
+ return String(a) === String(b);
263
+ }
264
+
265
+ const selectedOption = $derived.by(() =>
266
+ options.find((opt) => isSameValue(opt.value, value)),
267
+ );
152
268
 
153
269
  const hasSearchQuery = $derived(() => {
154
270
  return searchQuery.trim().length > 0;
@@ -156,7 +272,7 @@
156
272
 
157
273
  const filteredOptions = $derived(() => {
158
274
  const query = searchQuery.trim().toLowerCase();
159
- const availableOptions = normalizedOptions();
275
+ const availableOptions = options;
160
276
  if (!searchable || query.length === 0) {
161
277
  return availableOptions;
162
278
  }
@@ -173,8 +289,14 @@
173
289
 
174
290
  function handleOptionClick(optionValue: string | number) {
175
291
  if (disabled || isLoading) return;
176
- value = optionValue;
292
+ if (options.find((option) => option.value === optionValue)?.disabled) return;
177
293
  isOpen = false;
294
+ choose(optionValue);
295
+ }
296
+
297
+ /** Sets the value and tells the app, as a change of a form control does. */
298
+ function choose(optionValue: string | number | undefined) {
299
+ value = optionValue;
178
300
 
179
301
  if (onchange) {
180
302
  const syntheticEvent = new Event("change", { bubbles: true });
@@ -186,6 +308,124 @@
186
308
  }
187
309
  }
188
310
 
311
+ /**
312
+ * The native select's value as one of the options' own: a number stays a
313
+ * number. The empty first option is "nothing chosen".
314
+ */
315
+ function fromNative(raw: string): string | number | undefined {
316
+ if (raw === "") return undefined;
317
+ return options.find((option) => String(option.value) === raw)?.value ?? raw;
318
+ }
319
+
320
+ /** The native select was changed: by the visitor, in `native`, or by the browser restoring a form. */
321
+ function handleNativeChange(event: Event) {
322
+ const raw = (event.currentTarget as HTMLSelectElement).value;
323
+ if (raw === (isEmpty() ? "" : String(value))) return;
324
+ choose(fromNative(raw));
325
+ }
326
+
327
+ /**
328
+ * The browser found the hidden native select invalid (`required`, and
329
+ * nothing chosen). Its own bubble would point at a control nobody can
330
+ * see, and focus would go into it: the message is shown under the field
331
+ * instead, in the browser's words, and focus goes to the trigger.
332
+ */
333
+ function handleNativeInvalid(event: Event) {
334
+ if (nativeShown) return;
335
+ event.preventDefault();
336
+ const element = event.currentTarget as HTMLSelectElement;
337
+ nativeError = element.validationMessage;
338
+ // The browser tells every invalid control, and would focus the first.
339
+ // So does this: each Select shows its message, and only the first
340
+ // invalid control of the form takes focus.
341
+ const controls = [...(element.form?.elements ?? [])] as (Element & Partial<HTMLSelectElement>)[];
342
+ const first = controls.find((control) => control.willValidate && control.validity?.valid === false);
343
+ if (!first || first === element) document.getElementById(selectId)?.focus();
344
+ }
345
+
346
+ $effect(() => {
347
+ // A choice answers it.
348
+ void value;
349
+ nativeError = "";
350
+ });
351
+
352
+ /**
353
+ * The trigger takes over from the native select. If the keyboard was in
354
+ * the native select, which now leaves the tab order, focus goes to the
355
+ * trigger and does not fall to the page.
356
+ */
357
+ function handOver() {
358
+ const hadFocus = !isNative && nativeElement !== null && document.activeElement === nativeElement;
359
+ mounted = true;
360
+ if (hadFocus) void tick().then(() => document.getElementById(selectId)?.focus());
361
+ }
362
+
363
+ /** The value the Select was rendered with: what a form reset goes back to. */
364
+ const initialValue = untrack(() => value);
365
+
366
+ /**
367
+ * The form around the Select was reset. The native select goes back to
368
+ * what its markup says by itself, which the component would not know of:
369
+ * the value is put back to the one it was rendered with, and that is
370
+ * reported once if it is a change. After the browser has done its own
371
+ * resetting, which is after the event.
372
+ */
373
+ function handleFormReset(event: Event) {
374
+ setTimeout(() => {
375
+ if (event.defaultPrevented) return;
376
+ const initiallyEmpty = initialValue === undefined || initialValue === null || initialValue === "";
377
+ const unchanged = initiallyEmpty ? isEmpty() : isSameValue(value, initialValue);
378
+ if (!unchanged) choose(initiallyEmpty ? undefined : initialValue);
379
+ void tick().then(() => {
380
+ if (nativeElement) nativeElement.value = initiallyEmpty ? "" : String(initialValue);
381
+ });
382
+ });
383
+ }
384
+
385
+ /** Whether the browser's own list of the native select is open, where the browser can say (`:open`). */
386
+ function nativePickerOpen(element: HTMLSelectElement): boolean {
387
+ try {
388
+ return element.matches(":open");
389
+ } catch {
390
+ return false;
391
+ }
392
+ }
393
+
394
+ onMount(() => {
395
+ // A choice made in the native select before the scripts arrived is
396
+ // the visitor's: it becomes the value, and is reported once. Only a
397
+ // choice: a select nobody touched keeps the value it was given.
398
+ const before = nativeElement ? choiceBeforeMount(nativeElement) : undefined;
399
+ if (before !== undefined && before !== (isEmpty() ? "" : String(value))) {
400
+ choose(fromNative(before));
401
+ }
402
+
403
+ const element = nativeElement;
404
+ const form = element?.form ?? null;
405
+ form?.addEventListener("reset", handleFormReset);
406
+ const stopListening = () => form?.removeEventListener("reset", handleFormReset);
407
+
408
+ // Someone is choosing in the browser's own list right now: taking the
409
+ // select away would close it under their finger. They keep it until
410
+ // they have chosen or left, and the trigger takes over then.
411
+ if (!isNative && element && document.activeElement === element && nativePickerOpen(element)) {
412
+ const finish = () => {
413
+ element.removeEventListener("change", finish);
414
+ element.removeEventListener("blur", finish);
415
+ handOver();
416
+ };
417
+ element.addEventListener("change", finish);
418
+ element.addEventListener("blur", finish);
419
+ return () => {
420
+ element.removeEventListener("change", finish);
421
+ element.removeEventListener("blur", finish);
422
+ stopListening();
423
+ };
424
+ }
425
+ handOver();
426
+ return stopListening;
427
+ });
428
+
189
429
  function handleTriggerClick(event: MouseEvent) {
190
430
  if (disabled || isLoading) return;
191
431
  event.stopPropagation();
@@ -196,6 +436,10 @@
196
436
  if (!isOpen) return;
197
437
 
198
438
  const target = event.target as HTMLElement;
439
+ if (isInsideToastRegion(target)) return;
440
+ // The list in a sheet is in `<body>`, not in this container: a press
441
+ // in that sheet (an option, the grip, the search field) is not outside.
442
+ if (target.closest('[role="dialog"]')?.querySelector("[data-dropdown-sheet]")) return;
199
443
  const clickedSelectContainer = target.closest(".select-container");
200
444
 
201
445
  if (
@@ -215,115 +459,222 @@
215
459
 
216
460
  <svelte:window onclick={handleClickOutside} onkeydown={handleKeydown} />
217
461
 
218
- <div bind:this={selectContainer} class="w-full select-container">
462
+ <div bind:this={selectContainer} class={cn("w-full min-w-0 select-container", className)}>
219
463
  {#if label}
220
464
  <label for={selectId} class={labelClasses()}>{label}</label>
221
465
  {/if}
222
466
 
467
+ <div class="relative w-full min-w-0">
468
+ <!-- The native select. While it is the control it has the field's id, so
469
+ the label and the messages are its; once the trigger has taken over it is
470
+ hidden from assistive technology and out of the tab order, and only the
471
+ form talks to it. A disabled Select has always sent its value, so the
472
+ hidden one is not disabled; what is not sent is its `required`. -->
473
+ <select
474
+ bind:this={nativeElement}
475
+ id={nativeShown ? selectId : `${selectId}-native`}
476
+ name={name || undefined}
477
+ class={nativeClasses}
478
+ required={required && (nativeShown || !(disabled || isLoading))}
479
+ disabled={nativeShown && (disabled || isLoading)}
480
+ tabindex={nativeShown ? undefined : -1}
481
+ aria-hidden={nativeShown ? undefined : "true"}
482
+ aria-invalid={nativeShown && status.variant === "error" ? "true" : undefined}
483
+ aria-describedby={nativeShown ? fieldDescribedBy(selectId, status, describedBy) : undefined}
484
+ onchange={handleNativeChange}
485
+ oninvalid={handleNativeInvalid}
486
+ aria-label={nativeShown ? restProps["aria-label"] : undefined}
487
+ aria-labelledby={nativeShown ? restProps["aria-labelledby"] : undefined}
488
+ {...{ [NATIVE_SELECT_ATTRIBUTE]: "" }}
489
+ {...isNative ? (restProps as Record<string, unknown>) : {}}
490
+ >
491
+ {#if isEmpty()}
492
+ <option value="" selected>{isLoading ? text.loading : text.placeholder}</option>
493
+ {:else if !selectedOption}
494
+ <!-- The value has no option of its own: its options are still on
495
+ their way, or it is not among them. It is still the value, so the
496
+ select holds it and a form sends it; it is not offered in the list
497
+ where the browser lets an option be hidden. -->
498
+ <option value={String(value)} selected hidden>{isLoading ? text.loading : String(value)}</option>
499
+ {/if}
500
+ {#each options as option (option.value)}
501
+ <option
502
+ value={String(option.value)}
503
+ disabled={option.disabled}
504
+ selected={isSameValue(value, option.value)}
505
+ >
506
+ {option.label}
507
+ </option>
508
+ {/each}
509
+ </select>
510
+ {#if nativeShown}
511
+ <!-- `appearance-none` takes the browser's arrow away with its chrome. -->
512
+ <span
513
+ class={cn(
514
+ "pointer-events-none absolute top-1/2 me-px flex -translate-y-1/2 text-description",
515
+ sizeClass().chevron,
516
+ )}
517
+ aria-hidden="true"
518
+ >
519
+ <ChevronDown size={20} />
520
+ </span>
521
+ {/if}
522
+
523
+ {#if !isNative}
223
524
  <Dropdown
224
525
  bind:isOpen={isOpen}
225
526
  placement="bottom-start"
226
527
  selectedValue={value}
227
528
  onOptionClick={handleOptionClick}
228
- ariaLabel="Select options"
529
+ ariaLabel={strings?.listLabel || label || (restProps["aria-label"] as string | undefined) || text.listLabel}
530
+ ariaLabelledby={strings?.listLabel || label ? undefined : (restProps["aria-labelledby"] as string | undefined)}
229
531
  menuRole="listbox"
532
+ fullWidth
533
+ presentation={presentation === "native" ? "popover" : presentation}
534
+ sheetTitle={label || text.placeholder}
535
+ sheetCloseLabel={text.closeLabel}
536
+ sheetExpandLabel={text.expandLabel}
537
+ sheetCollapseLabel={text.collapseLabel}
538
+ sheetSnap={searchable && options.length > 6 ? "full" : "half"}
230
539
  >
231
540
  {#snippet trigger(aria)}
541
+ <!-- A select-only combobox, as the ARIA practices describe it:
542
+ a button could not say that it is invalid or required
543
+ (`aria-invalid` and `aria-required` are not a button's), so a
544
+ Select with an error was announced like one without. The keys,
545
+ `aria-haspopup="listbox"`, `aria-expanded` and `aria-controls` are
546
+ as they were. -->
232
547
  <button
233
548
  type="button"
234
- id={selectId}
549
+ id={mounted ? selectId : `${selectId}-trigger`}
235
550
  class={triggerClasses()}
236
551
  {disabled}
237
552
  onclick={handleTriggerClick}
553
+ {...restProps}
238
554
  {...aria}
239
- aria-describedby={message ? `${selectId}-message` : undefined}
555
+ role="combobox"
556
+ aria-invalid={status.variant === "error" ? "true" : undefined}
557
+ aria-required={required ? "true" : undefined}
558
+ aria-busy={isLoading ? "true" : undefined}
559
+ aria-describedby={fieldDescribedBy(selectId, status, describedBy)}
240
560
  >
561
+ <!-- An empty trigger shows its placeholder in the placeholder
562
+ colour, as Input does. A value has no colour class of its own,
563
+ and a disabled trigger none at all: the button's `text-body`
564
+ and its disabled colour are inherited. A colour class here
565
+ beat the button's `disabled:` one, and a disabled Select read
566
+ as enabled. -->
567
+ <!-- The label wraps: cut off, there was nowhere on the closed
568
+ control to read the rest. The chevron stays in the middle of
569
+ the box, however many lines there are. -->
241
570
  <span
242
- class="text-left flex-1 truncate {isLoading
243
- ? 'text-description'
244
- : isEmpty()
245
- ? 'text-description'
246
- : 'text-body'}"
571
+ class="min-w-0 flex-1 text-start [overflow-wrap:anywhere] {disabled
572
+ ? ''
573
+ : isLoading
574
+ ? 'text-description'
575
+ : isEmpty()
576
+ ? 'text-input-placeholder'
577
+ : ''}"
247
578
  >
248
579
  {#if isLoading}
249
- {loadingText}
580
+ {text.loading}
250
581
  {:else}
251
582
  {isEmpty()
252
- ? placeholder
253
- : options.find((opt) => opt.value === value)?.label ||
254
- placeholder}
583
+ ? text.placeholder
584
+ : selectedOption?.label || text.placeholder}
255
585
  {/if}
256
586
  </span>
257
587
  <ChevronDown
258
588
  size={20}
259
- class="text-description transition-transform duration-200 {isOpen
589
+ class="shrink-0 text-description transition-transform duration-200 motion-reduce:transition-none {isOpen
260
590
  ? 'rotate-180'
261
591
  : ''}"
262
592
  />
263
593
  </button>
264
594
  {/snippet}
595
+ {#snippet header()}
596
+ {#if searchable && !isLoading}
597
+ <!-- Outside role="listbox": a textbox is not a valid listbox child. -->
598
+ <!-- 0.5rem and the panel's 1px border from the corner, as the
599
+ options below and a Dropdown's items are: an 8px field 9px
600
+ inside the panel's 16px corner is concentric with it. -->
601
+ <div class="px-2 pb-2 pt-1" style:width={menuWidth} data-menu-width>
602
+ <Input
603
+ type="text"
604
+ size="sm"
605
+ class="min-w-0"
606
+ placeholder={text.searchPlaceholder}
607
+ bind:value={searchQuery}
608
+ aria-label={text.searchPlaceholder}
609
+ {disabled}
610
+ />
611
+ </div>
612
+ {/if}
613
+ {/snippet}
265
614
  {#snippet children()}
266
- <div class="px-2 pb-2 pt-1" style:width={menuWidth}>
267
- {#if searchable && !isLoading}
268
- <div class="px-1 pb-2">
269
- <Input
270
- type="text"
271
- size="sm"
272
- class="min-w-0"
273
- placeholder={searchPlaceholder}
274
- bind:value={searchQuery}
275
- aria-label={searchPlaceholder}
276
- {disabled}
277
- />
278
- </div>
279
- {/if}
615
+ <!-- `px-1` twice: the inner one is room for an option's focus
616
+ ring inside the scrolling box, and together with the panel's
617
+ border they put the options 9px from its corner. -->
618
+ <div class="px-1 pb-2 pt-1" style:width={menuWidth} data-menu-width>
280
619
  <div
281
620
  class="overflow-y-auto px-1"
621
+ data-menu-scroll
282
622
  style:max-height={maxMenuHeight}
283
623
  >
284
624
  {#if isLoading}
285
625
  <div class="space-y-2 px-2 py-2" role="status" aria-live="polite">
286
- <div class="h-8 w-full animate-pulse rounded-md bg-base-200"></div>
287
- <div class="h-8 w-full animate-pulse rounded-md bg-base-200"></div>
288
- <p class="text-xs text-description">{loadingText}</p>
626
+ <div class="h-8 w-full animate-pulse rounded-control bg-base-200"></div>
627
+ <div class="h-8 w-full animate-pulse rounded-control bg-base-200"></div>
628
+ <p class="text-xs text-description">{text.loading}</p>
289
629
  </div>
290
630
  {:else if filteredOptions().length > 0}
291
631
  {#each filteredOptions() as option (option.value)}
292
632
  {@const buttonRestProps = {
293
633
  "data-value": String(option.value),
294
634
  } as Record<string, string>}
295
- <div class="w-full my-0.5">
635
+ <div class="w-full my-1">
636
+ <!-- `aria-disabled`, not `disabled`: a natively disabled button cannot take focus, so the arrow keys stopped at the option before it. The click is refused in `handleOptionClick`. -->
296
637
  <button
297
638
  type="button"
298
639
  role="option"
299
- aria-selected={value === option.value
300
- ? true
301
- : undefined}
302
- class="focus-ring flex w-full items-center justify-start rounded-md border-2 px-3 py-2 text-left text-sm font-medium transition-colors focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 {value ===
303
- option.value
304
- ? 'border-action-primary bg-transparent text-headline'
305
- : 'border-transparent bg-transparent text-body hover:bg-base-100'}"
306
- disabled={option.disabled}
640
+ aria-selected={isSameValue(value, option.value)}
641
+ class="focus-ring flex w-full items-center justify-start gap-2 rounded-control border-2 px-3 py-2 pointer-coarse:py-3 text-start text-sm font-medium transition-colors focus:outline-none aria-disabled:cursor-not-allowed aria-disabled:opacity-50 {isSameValue(
642
+ value,
643
+ option.value,
644
+ )
645
+ ? 'border-transparent bg-surface-overlay-hover text-headline active:bg-surface-active'
646
+ : option.disabled
647
+ ? 'border-transparent bg-transparent text-body'
648
+ : 'border-transparent bg-transparent text-body hover:bg-surface-overlay-hover active:bg-surface-active'}"
649
+ aria-disabled={option.disabled ? "true" : undefined}
307
650
  {...buttonRestProps}
308
651
  onclick={() =>
309
652
  handleOptionClick(option.value)}
310
653
  >
311
- {option.label}
654
+ <span class="min-w-0 flex-1">{option.label}</span>
655
+ <!-- The chosen option's mark: a check and a
656
+ fill. It used to be an edge in the action
657
+ colour, which is what the focus ring draws:
658
+ the chosen and the focused option could
659
+ not be told apart. -->
660
+ {#if isSameValue(value, option.value)}
661
+ <Check size={16} class="shrink-0" aria-hidden="true" />
662
+ {/if}
312
663
  </button>
313
664
  </div>
314
665
  {/each}
315
666
  {:else if hasSearchQuery()}
316
667
  <div class="px-3 py-2 text-sm text-description">
317
- {noResultsText}
668
+ {text.noResults}
318
669
  </div>
319
670
  {:else}
320
- <div class="rounded-md border border-border bg-base-50 px-3 py-3 text-sm">
321
- <p class="font-medium text-headline">{emptyStateTitle}</p>
322
- <p class="mt-1 text-description">{emptyStateDescription}</p>
671
+ <div class="rounded-control border border-border bg-surface-overlay-hover px-3 py-3 text-sm">
672
+ <p class="font-medium text-headline">{text.emptyTitle}</p>
673
+ <p class="mt-1 text-description">{text.emptyDescription}</p>
323
674
  {#if emptyStateActionLabel && onEmptyStateAction}
324
675
  <button
325
676
  type="button"
326
- class="mt-3 inline-flex min-h-11 cursor-pointer items-center rounded-lg bg-action-primary px-3 py-2 text-sm text-action-primary"
677
+ class="mt-3 inline-flex min-h-11 cursor-pointer items-center rounded-control bg-action-primary px-3 py-2 text-sm text-action-primary"
327
678
  onclick={onEmptyStateAction}
328
679
  >
329
680
  {emptyStateActionLabel}
@@ -335,18 +686,7 @@
335
686
  </div>
336
687
  {/snippet}
337
688
  </Dropdown>
338
- {#if message && variant !== "default"}
339
- <p
340
- id={`${selectId}-message`}
341
- class={messageClasses()}
342
- role={variant === "error" ? "alert" : "status"}
343
- aria-live={variant === "error" ? "assertive" : "polite"}
344
- >
345
- {#if getIcon()}
346
- {@const Icon = getIcon()}
347
- <Icon size={14} class="shrink-0" />
348
- {/if}
349
- <span>{message}</span>
350
- </p>
351
689
  {/if}
690
+ </div>
691
+ <FieldMessages fieldId={selectId} {status} {hint} gap="mt-1" messageClass="w-full" />
352
692
  </div>