zabi-components 8.0.0 → 9.0.0

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