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,7 +1,25 @@
1
1
  <script lang="ts">
2
+ import { zabiCommonStrings } from "../util/zabi-strings.js";
2
3
  import { generateId } from "../util/ssr-safe.js";
3
- import type { Snippet } from 'svelte';
4
+ import { onMount, setContext, type Snippet } from 'svelte';
4
5
  import { cn } from "../util/cn.js";
6
+ import {
7
+ DROPDOWN_CONTEXT_KEY,
8
+ opensAsSheet,
9
+ type DropdownContext,
10
+ type DropdownOption,
11
+ type DropdownPresentation,
12
+ } from "../util/dropdown.js";
13
+ import type { BottomSheetSnap } from "../util/bottom-sheet.js";
14
+ import {
15
+ measurePlacement,
16
+ unfitted,
17
+ watchViewport,
18
+ type PanelPlacement,
19
+ } from "../util/fit-in-viewport.js";
20
+ import { isInsideToastRegion } from "../util/focus-utils.js";
21
+ import BottomSheet from './BottomSheet.svelte';
22
+ import DropdownItem from './DropdownItem.svelte';
5
23
 
6
24
  export type DropdownTriggerProps = {
7
25
  'aria-expanded': boolean;
@@ -13,16 +31,55 @@
13
31
  /** Extra classes for the host element. */
14
32
  class?: string;
15
33
  isOpen?: boolean;
34
+ /**
35
+ * The side the menu opens on. It is where the menu goes whenever it
36
+ * fits there; near an edge of the screen it flips to the other side,
37
+ * and is narrowed or given its own scroll when neither side has room.
38
+ */
16
39
  placement?: 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end';
40
+ /**
41
+ * How the menu is shown. `popover`: under its trigger, as always.
42
+ * `sheet`: in a BottomSheet, with the same items, roles and keys; this
43
+ * is the action sheet ("Edit, Share, Delete" from the bottom of the
44
+ * screen). `auto`: a sheet on a phone (a touch screen narrower than
45
+ * 640px), the pop-over everywhere else. Decided each time the menu
46
+ * opens, in the browser.
47
+ */
48
+ presentation?: DropdownPresentation;
49
+ /**
50
+ * The host fills its container instead of being as wide as its
51
+ * trigger's content, and the menu is at least as wide as the host.
52
+ * For a trigger that is a field (Select); a menu on an icon button
53
+ * stays as it is.
54
+ */
55
+ fullWidth?: boolean;
56
+ /** Heading of the sheet. Without it, `ariaLabel` is. */
57
+ sheetTitle?: string;
58
+ /**
59
+ * The height the sheet opens at. Without it: half the screen for up
60
+ * to six `options`, all of it for more. The grip moves it either way.
61
+ */
62
+ sheetSnap?: BottomSheetSnap;
63
+ /** The sheet is as tall as the list, up to half the screen (BottomSheet `fit`); the grip then only drags. Off by default. */
64
+ sheetFit?: boolean;
65
+ /** Accessible name of the sheet's close button. */
66
+ sheetCloseLabel?: string;
67
+ /** Accessible name of the sheet's grip while its button takes the sheet up a step. */
68
+ sheetExpandLabel?: string;
69
+ /** The same, while it takes the sheet down a step. */
70
+ sheetCollapseLabel?: string;
17
71
  ariaLabel?: string;
72
+ /** Id of an element that names the list; wins over `ariaLabel` for the pop-over list. */
73
+ ariaLabelledby?: string;
18
74
  /** `listbox` for Select-style; `menu` for actions. */
19
75
  menuRole?: 'menu' | 'listbox';
20
76
  selectedValue?: string | number | null;
21
- options?: Array<{
22
- value: string | number;
23
- label: string;
24
- disabled?: boolean;
25
- }>;
77
+ /**
78
+ * Menu items, when not supplying children. Each takes an optional
79
+ * `icon`, `tone` (`danger`) and `description`; a disabled item stays
80
+ * focusable so its description can be read.
81
+ */
82
+ options?: DropdownOption[];
26
83
  onOptionClick?: (value: string | number) => void;
27
84
  trigger: Snippet<[DropdownTriggerProps]>;
28
85
  /** Rendered in the popup above the menu/listbox, outside its role (e.g. a search field). */
@@ -32,9 +89,18 @@
32
89
 
33
90
  let {
34
91
  class: className = "",
35
- isOpen = $bindable(false),
92
+ isOpen = $bindable<Exclude<Props["isOpen"], undefined>>(),
36
93
  placement = 'bottom-start',
94
+ presentation = 'popover',
95
+ fullWidth = false,
96
+ sheetTitle = undefined,
97
+ sheetSnap = undefined,
98
+ sheetFit = false,
99
+ sheetCloseLabel: sheetCloseLabelGiven,
100
+ sheetExpandLabel: sheetExpandLabelGiven,
101
+ sheetCollapseLabel: sheetCollapseLabelGiven,
37
102
  ariaLabel = 'Menu',
103
+ ariaLabelledby = undefined,
38
104
  menuRole = 'menu',
39
105
  selectedValue = null,
40
106
  options = [],
@@ -45,9 +111,48 @@
45
111
  ...restProps
46
112
  }: Props = $props();
47
113
 
114
+ // No fallback on a bindable prop: Svelte refuses `bind:…={undefined}` on
115
+ // one that has a fallback (`props_invalid_value`), and a page that throws
116
+ // while it hydrates never becomes interactive. The default is applied
117
+ // here instead: at once, for the server and the first render, and again
118
+ // whenever a parent hands back `undefined`.
119
+ const applyDefaults = () => {
120
+ if (isOpen === undefined) isOpen = false;
121
+ };
122
+ applyDefaults();
123
+ $effect.pre(applyDefaults);
124
+
125
+ /** Words many components share: a `ZabiStringsProvider` above this one may give them; else English. */
126
+ const common = zabiCommonStrings();
127
+ const sheetCloseLabel = $derived(sheetCloseLabelGiven ?? common().close);
128
+ const sheetExpandLabel = $derived(sheetExpandLabelGiven ?? common().expand);
129
+ const sheetCollapseLabel = $derived(sheetCollapseLabelGiven ?? common().collapse);
130
+
131
+ // `DropdownItem` reads its role from here, in `options` and in `children` alike.
132
+ setContext<DropdownContext>(DROPDOWN_CONTEXT_KEY, {
133
+ get itemRole() {
134
+ return menuRole === 'listbox' ? ('option' as const) : ('menuitem' as const);
135
+ },
136
+ });
137
+
48
138
  const menuId = generateId('dropdown-menu');
49
139
  let rootEl = $state<HTMLDivElement | null>(null);
50
140
 
141
+ /**
142
+ * Whether the open menu is in a sheet. Worked out when `isOpen` turns
143
+ * true and kept while it is open, so turning the phone does not swap one
144
+ * for the other under the finger. Never before the component has mounted:
145
+ * a menu rendered open on the server is a pop-over, and stays one through
146
+ * hydration.
147
+ */
148
+ let mounted = $state(false);
149
+ onMount(() => {
150
+ mounted = true;
151
+ });
152
+ const asSheet = $derived(isOpen && mounted && opensAsSheet(presentation));
153
+ /** The sheet's content, when the menu is in one: it is outside `rootEl`, in `<body>`. */
154
+ let sheetElement = $state<HTMLElement | null>(null);
155
+
51
156
  let openedViaKeyboard = $state(false);
52
157
 
53
158
  const triggerAria = $derived({
@@ -84,6 +189,12 @@
84
189
  return;
85
190
  }
86
191
 
192
+ // In a sheet, Escape and Tab are the sheet's: it closes itself and
193
+ // keeps Tab inside, on its grip and close button too.
194
+ if (asSheet && (event.key === 'Escape' || event.key === 'Tab')) return;
195
+
196
+ if (typedIntoList(event)) return;
197
+
87
198
  switch (event.key) {
88
199
  case 'Escape':
89
200
  event.preventDefault();
@@ -108,15 +219,128 @@
108
219
  event.preventDefault();
109
220
  focusLastItem();
110
221
  break;
111
- case 'Tab':
112
- isOpen = false;
222
+ case 'Tab': {
223
+ // The list is one stop and a field above it (Select's search)
224
+ // another: Shift+Tab from an item goes to the field, Tab from
225
+ // the field to the list. Any other Tab leaves, and closes.
226
+ const field = headerField();
227
+ const onItem = getMenuItems().includes(event.target as HTMLElement);
228
+ if (field && onItem && event.shiftKey) {
229
+ event.preventDefault();
230
+ field.focus();
231
+ } else if (field && event.target === field && !event.shiftKey && getMenuItems().length > 0) {
232
+ event.preventDefault();
233
+ tabStop()?.focus();
234
+ } else {
235
+ isOpen = false;
236
+ }
113
237
  break;
238
+ }
239
+ }
240
+ }
241
+
242
+ /** The text field in the popup's header, when there is one: Select's search. */
243
+ function headerField(): HTMLElement | null {
244
+ const popup = sheetElement ?? popoverElement();
245
+ if (!popup) return null;
246
+ return (
247
+ [...popup.querySelectorAll<HTMLElement>('input, textarea')].find(
248
+ (candidate) => isTextEntryTarget(candidate) && !(candidate as HTMLInputElement).disabled,
249
+ ) ?? null
250
+ );
251
+ }
252
+
253
+ /** The item that is the list's one Tab stop. */
254
+ function tabStop(): HTMLElement | undefined {
255
+ const items = getMenuItems();
256
+ return items.find((item) => item.tabIndex === 0) ?? items[0];
257
+ }
258
+
259
+ /** How long a pause ends a word typed on the list, in ms. */
260
+ const TYPEAHEAD_PAUSE = 600;
261
+ let typed = '';
262
+ let typedAt = 0;
263
+
264
+ /**
265
+ * A printable character typed while focus is on an item. With a text
266
+ * field above the list it is meant for that field: focus moves there and
267
+ * the character is typed into it. Without one it is typeahead: focus goes
268
+ * to the next item that starts with what has been typed. Returns true
269
+ * when the key was taken.
270
+ */
271
+ function typedIntoList(event: KeyboardEvent): boolean {
272
+ if (event.key.length !== 1 || event.ctrlKey || event.metaKey || event.altKey) return false;
273
+ const items = getMenuItems();
274
+ const at = items.indexOf(event.target as HTMLElement);
275
+ if (at === -1) return false;
276
+ const now = event.timeStamp || Date.now();
277
+ const word = now - typedAt > TYPEAHEAD_PAUSE ? '' : typed;
278
+ // A space activates the item, unless it is in the middle of a word.
279
+ if (event.key === ' ' && word === '') return false;
280
+
281
+ const field = headerField();
282
+ if (field) {
283
+ // Focus moves during keydown, so the character lands in the field.
284
+ field.focus();
285
+ return true;
114
286
  }
287
+
288
+ event.preventDefault();
289
+ typed = word + event.key.toLocaleLowerCase();
290
+ typedAt = now;
291
+ const starts = (item: HTMLElement) =>
292
+ (item.textContent ?? '').trim().toLocaleLowerCase().startsWith(typed);
293
+ // One letter again and again steps through the items that start with it.
294
+ const from = typed.length === 1 ? at + 1 : at;
295
+ const order = [...items.slice(from), ...items.slice(0, from)];
296
+ order.find(starts)?.focus();
297
+ return true;
115
298
  }
116
299
 
300
+ /**
301
+ * One Tab stop for the whole list, on the item that has focus, or the
302
+ * chosen one, or the first: the roving tabindex of a listbox or a menu.
303
+ * With every item a stop, a field above twelve options was twelve
304
+ * Shift+Tabs away. Kept as the items change (a search filters them).
305
+ */
306
+ $effect(() => {
307
+ const popup = sheetElement ?? menuElement;
308
+ if (!isOpen || !popup) return;
309
+ const rove = (current?: HTMLElement) => {
310
+ const items = getMenuItems();
311
+ const stop =
312
+ (current && items.includes(current) ? current : undefined) ??
313
+ items.find((item) => item === document.activeElement) ??
314
+ items.find((item) => item.tabIndex === 0 && item.hasAttribute('data-roving')) ??
315
+ items.find(
316
+ (item) =>
317
+ item.getAttribute('aria-selected') === 'true' ||
318
+ item.getAttribute('aria-checked') === 'true',
319
+ ) ??
320
+ items[0];
321
+ for (const item of items) {
322
+ item.tabIndex = item === stop ? 0 : -1;
323
+ item.setAttribute('data-roving', '');
324
+ }
325
+ };
326
+ rove();
327
+ const onFocusIn = (event: FocusEvent) => {
328
+ if (getMenuItems().includes(event.target as HTMLElement)) rove(event.target as HTMLElement);
329
+ };
330
+ popup.addEventListener('focusin', onFocusIn);
331
+ const changes = new MutationObserver(() => rove());
332
+ changes.observe(popup, { childList: true, subtree: true });
333
+ return () => {
334
+ popup.removeEventListener('focusin', onFocusIn);
335
+ changes.disconnect();
336
+ };
337
+ });
338
+
117
339
  let menuElement = $state<HTMLElement | null>(null);
340
+ const popoverElement = () => menuElement;
118
341
 
119
342
  function getMenuItems(): HTMLElement[] {
343
+ const menuElement = sheetElement ?? popoverElement();
120
344
  if (!menuElement) return [];
121
345
  // `menuitemradio` and `menuitemcheckbox` are menu items too. Matching
122
346
  // only `menuitem` left a menu built from them with nothing to focus:
@@ -171,10 +395,81 @@
171
395
  }
172
396
  });
173
397
 
398
+ /**
399
+ * Closing removes the popup, and with it the focused item: the browser
400
+ * then drops focus on `<body>` and a keyboard user loses their place.
401
+ * Whether focus was in the popup has to be read before the DOM changes.
402
+ */
403
+ let focusWasInPopup = false;
404
+ $effect.pre(() => {
405
+ if (!isOpen) {
406
+ focusWasInPopup =
407
+ !!menuElement && menuElement.contains(document.activeElement);
408
+ }
409
+ });
410
+
411
+ /**
412
+ * In a sheet. Focus goes to the chosen item, or the first one, as soon as
413
+ * the sheet is in place: the sheet itself would start on its grip. (It
414
+ * leaves focus alone when something inside already has it.) Focusing also
415
+ * scrolls the chosen item into view.
416
+ */
417
+ let closedFromSheet = false;
418
+ $effect(() => {
419
+ if (!asSheet || !sheetElement) return;
420
+ closedFromSheet = true;
421
+ queueMicrotask(() => {
422
+ const items = getMenuItems();
423
+ const chosen = items.find(
424
+ (item) =>
425
+ item.getAttribute('aria-selected') === 'true' ||
426
+ item.getAttribute('aria-checked') === 'true',
427
+ );
428
+ (chosen ?? items[0])?.focus();
429
+ });
430
+ });
431
+
432
+ // Closed from a sheet, focus goes back to the trigger. The sheet returns
433
+ // it to whatever had focus when it opened, but a tap on a button does not
434
+ // focus it in every browser, and then that is `<body>`.
435
+ $effect(() => {
436
+ if (isOpen || !closedFromSheet) return;
437
+ closedFromSheet = false;
438
+ const timer = setTimeout(() => {
439
+ const active = document.activeElement;
440
+ if (!active || active === document.body) triggerElement()?.focus();
441
+ }, 0);
442
+ return () => clearTimeout(timer);
443
+ });
444
+
445
+ /** The control that opened the menu: what `aria-controls` is spread on, else the first control. */
446
+ function triggerElement(): HTMLElement | null {
447
+ if (!rootEl) return null;
448
+ return (
449
+ rootEl.querySelector<HTMLElement>(`[aria-controls="${menuId}"]`) ??
450
+ rootEl.querySelector<HTMLElement>(
451
+ 'button:not([disabled]), [href], [tabindex]:not([tabindex="-1"])',
452
+ )
453
+ );
454
+ }
455
+
456
+ $effect(() => {
457
+ if (isOpen || !focusWasInPopup) return;
458
+ focusWasInPopup = false;
459
+ // Only when the close left focus nowhere. If the handler that closed
460
+ // the menu moved focus itself (it opened a dialog, say), that wins.
461
+ const active = document.activeElement;
462
+ if (!active || active === document.body) {
463
+ triggerElement()?.focus();
464
+ }
465
+ });
466
+
174
467
  $effect(() => {
175
- if (!isOpen) return;
468
+ // A sheet has a backdrop of its own, and is not inside `rootEl`.
469
+ if (!isOpen || asSheet) return;
176
470
  function onDocMouseDown(e: MouseEvent) {
177
471
  const t = e.target as Node;
472
+ if (isInsideToastRegion(t)) return;
178
473
  if (rootEl && !rootEl.contains(t)) {
179
474
  isOpen = false;
180
475
  }
@@ -183,15 +478,71 @@
183
478
  return () => document.removeEventListener('mousedown', onDocMouseDown);
184
479
  });
185
480
 
481
+ /** Room kept between the menu and the edge of the screen. */
482
+ const VIEWPORT_MARGIN = 8;
483
+
484
+ /** The room between the menu and its trigger: `mt-2` or `mb-2`, so it grows with the text size. */
485
+ function triggerGap(panel: HTMLElement): number {
486
+ // A menu placed with `position: fixed` has its margin taken away inline:
487
+ // read the one its classes give it, or the gap would be 0 from then on.
488
+ const inline = panel.style.margin;
489
+ panel.style.margin = '';
490
+ const style = getComputedStyle(panel);
491
+ const gap = Math.max(parseFloat(style.marginTop), parseFloat(style.marginBottom));
492
+ panel.style.margin = inline;
493
+ return Number.isFinite(gap) ? gap : 8;
494
+ }
495
+
496
+ const preferred = $derived({
497
+ block: placement.startsWith('top') ? ('top' as const) : ('bottom' as const),
498
+ inline: placement.endsWith('end') ? ('end' as const) : ('start' as const),
499
+ });
500
+
501
+ /**
502
+ * Where the menu actually is. It starts, and stays, on the side asked for
503
+ * whenever the menu fits there; `measurePlacement` only moves it when it
504
+ * would leave the screen, or be cut off by a box that scrolls around it
505
+ * (a Modal's body, say). In the second case it may be given
506
+ * `position: fixed` (`fit.fixed`), which such a box does not clip. It
507
+ * never leaves its place in the DOM, so the keys, focus and a dialog's
508
+ * focus trap all still see it as part of this Dropdown.
509
+ */
510
+ let measured = $state<PanelPlacement | null>(null);
511
+ const fit = $derived<PanelPlacement>(
512
+ measured ?? { ...unfitted(preferred.block, preferred.inline), fixed: null },
513
+ );
514
+ const resolvedPlacement = $derived(`${fit.block}-${fit.inline}` as typeof placement);
515
+
516
+ // Runs after the menu is in the DOM and before the browser paints it, so
517
+ // it is never seen on the wrong side; then again when the screen changes.
518
+ $effect(() => {
519
+ const side = preferred;
520
+ if (!isOpen || !menuElement || !rootEl) {
521
+ measured = null;
522
+ return;
523
+ }
524
+ const anchor = rootEl;
525
+ const panel = menuElement;
526
+ const update = () => {
527
+ measured = measurePlacement(anchor, panel, side, {
528
+ margin: VIEWPORT_MARGIN,
529
+ gap: triggerGap(panel),
530
+ });
531
+ };
532
+ update();
533
+ return watchViewport(update, panel);
534
+ });
535
+
186
536
  const placementClasses = $derived(() => {
187
537
  const base = 'absolute z-dropdown min-w-[12rem]';
188
538
  const positioning: Record<typeof placement, string> = {
189
- 'bottom-start': 'top-full left-0 mt-2',
190
- 'bottom-end': 'top-full right-0 mt-2',
191
- 'top-start': 'bottom-full left-0 mb-2',
192
- 'top-end': 'bottom-full right-0 mb-2',
539
+ // Logical: `start` is the left edge, and the right one in a right-to-left page.
540
+ 'bottom-start': 'top-full start-0 mt-2',
541
+ 'bottom-end': 'top-full end-0 mt-2',
542
+ 'top-start': 'bottom-full start-0 mb-2',
543
+ 'top-end': 'bottom-full end-0 mb-2',
193
544
  };
194
- return `${base} ${positioning[placement]}`;
545
+ return `${base} ${positioning[resolvedPlacement]}`;
195
546
  });
196
547
 
197
548
  const transformClasses = $derived(() => {
@@ -210,61 +561,179 @@
210
561
  const dropdownContentClasses = $derived(() => {
211
562
  return [
212
563
  placementClasses(),
213
- 'rounded-control border border-border bg-surface-overlay py-2 shadow-lg transition-all duration-200 ease-in-out',
564
+ // A menu is an overlay (16px). Its items are 8px, 9px inside
565
+ // the corner: within a pixel of concentric.
566
+ // A column, so that the list below can be the part that scrolls
567
+ // when the menu is limited in height.
568
+ // The layer form of the material: a menu may hold content that
569
+ // places itself with `position: fixed` (a nested Dropdown, a
570
+ // Tooltip), and a backdrop-filter on the menu itself would make it
571
+ // the containing block of that content.
572
+ 'material-layer-thick flex flex-col rounded-overlay py-2 transition-[opacity,translate] duration-(--duration-moderate) ease-standard motion-reduce:transition-none',
214
573
  transformClasses(),
215
574
  ]
216
575
  .join(' ')
217
576
  .replace(/\s+/g, ' ')
218
577
  .trim();
219
578
  });
220
-
221
- const itemRole = $derived(menuRole === 'listbox' ? 'option' : 'menuitem');
222
-
223
- const optionClasses =
224
- 'focus-ring flex w-full items-center justify-start rounded-control px-3 py-2 text-left text-sm text-body transition-colors hover:bg-surface-overlay-hover focus:outline-none disabled:cursor-not-allowed disabled:opacity-50';
225
579
  </script>
226
580
 
227
581
  <div
228
582
  bind:this={rootEl}
229
- class={cn("relative inline-block", className)}
583
+ class={cn("relative", fullWidth ? "block w-full min-w-0" : "inline-block", className)}
230
584
  data-placement={placement}
231
585
  onkeydown={handleKeydown}
232
586
  {...restProps}
233
587
  >
234
588
  {@render trigger(triggerAria)}
235
589
 
236
- {#if isOpen}
237
- <div bind:this={menuElement} class={dropdownContentClasses()}>
590
+ {#if asSheet}
591
+ <!-- The same menu in a BottomSheet: the element with the menu or
592
+ listbox role, its items and the header are the ones the pop-over has,
593
+ so roles, names and the arrow keys are too. The sheet brings the focus
594
+ trap, Escape, the backdrop, the swipe and its place in the stack of
595
+ overlays. It is rendered in `<body>`, outside this component's root, so
596
+ the keys are listened to here as well. -->
597
+ <BottomSheet
598
+ bind:isOpen
599
+ snap={sheetSnap ?? (options.length > 6 ? 'full' : 'half')}
600
+ fit={sheetFit}
601
+ title={sheetTitle || ariaLabel}
602
+ closeLabel={sheetCloseLabel}
603
+ expandLabel={sheetExpandLabel}
604
+ collapseLabel={sheetCollapseLabel}
605
+ >
606
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
607
+ <div
608
+ bind:this={sheetElement}
609
+ class="dropdown-sheet"
610
+ data-dropdown-sheet
611
+ onkeydown={handleKeydown}
612
+ >
613
+ {#if header}
614
+ <div class="dropdown-sheet-header">
615
+ {@render header()}
616
+ </div>
617
+ {/if}
618
+ <div
619
+ id={menuId}
620
+ role={menuRole === 'listbox' ? 'listbox' : 'menu'}
621
+ aria-label={ariaLabel}
622
+ >
623
+ {@render items()}
624
+ </div>
625
+ </div>
626
+ </BottomSheet>
627
+ {:else if isOpen}
628
+ <!-- The inline styles are only set when the menu would leave the screen,
629
+ or be cut off by a scrolling box around it (`fit.fixed`: placed against
630
+ the viewport instead, with the classes' own offsets switched off).
631
+ `min-width`: `min-w-[12rem]` is wider than a 320px screen once the text
632
+ is at 200%, and a minimum beats `max-width`; a narrowed menu is exactly
633
+ as wide as the room there is.
634
+ A menu limited in height does not scroll itself: the list inside it
635
+ does. A scrollbar on the panel was drawn in the panel's square box, on
636
+ and outside its 16px corners; the list is 9px inside them (the panel's
637
+ padding and border), and a header (Select's search field) stays put. -->
638
+ <div
639
+ bind:this={menuElement}
640
+ class={dropdownContentClasses()}
641
+ data-resolved-placement={resolvedPlacement}
642
+ style:inset-inline-start={!fit.fixed && fit.inline === 'start' && fit.inlineOffset !== 0
643
+ ? `${fit.inlineOffset}px`
644
+ : undefined}
645
+ style:inset-inline-end={!fit.fixed && fit.inline === 'end' && fit.inlineOffset !== 0
646
+ ? `${fit.inlineOffset}px`
647
+ : undefined}
648
+ style:position={fit.fixed ? 'fixed' : undefined}
649
+ style:top={fit.fixed ? `${fit.fixed.top}px` : undefined}
650
+ style:left={fit.fixed ? `${fit.fixed.left}px` : undefined}
651
+ style:right={fit.fixed ? 'auto' : undefined}
652
+ style:bottom={fit.fixed ? 'auto' : undefined}
653
+ style:margin={fit.fixed ? '0' : undefined}
654
+ style:width={fit.fixed ? `${fit.fixed.width}px` : undefined}
655
+ style:max-width={fit.maxWidth !== null ? `${fit.maxWidth}px` : undefined}
656
+ style:min-width={fit.maxWidth !== null
657
+ ? `${fit.maxWidth}px`
658
+ : fullWidth
659
+ ? 'max(12rem, 100%)'
660
+ : undefined}
661
+ style:max-height={fit.maxHeight !== null ? `${fit.maxHeight}px` : undefined}
662
+ style:overflow={fit.maxWidth !== null ? 'hidden' : undefined}
663
+ >
238
664
  {@render header?.()}
665
+ <!-- `mx-1` and `px-1` are the 0.5rem the items always had from the
666
+ panel's edge, split so that the scrolling box is 4px in from it:
667
+ room for an item's focus ring inside the box, and a scrollbar
668
+ clear of the corners. -->
239
669
  <div
240
670
  id={menuId}
241
671
  role={menuRole === 'listbox' ? 'listbox' : 'menu'}
242
- aria-label={ariaLabel}
672
+ aria-label={ariaLabelledby ? undefined : ariaLabel}
673
+ aria-labelledby={ariaLabelledby}
674
+ class={options.length > 0 ? 'mx-1 min-h-0' : 'min-h-0'}
675
+ data-dropdown-scroller
676
+ style:overflow={fit.maxWidth !== null || fit.maxHeight !== null ? 'auto' : undefined}
677
+ style:overscroll-behavior={fit.maxHeight !== null ? 'contain' : undefined}
243
678
  >
244
- {#if options.length > 0}
245
- <div class="px-2 py-1">
246
- {#each options as option (option.value)}
247
- <button
248
- type="button"
249
- role={itemRole}
250
- aria-selected={menuRole === 'listbox' &&
251
- selectedValue !== null &&
252
- String(selectedValue) === String(option.value)
253
- ? true
254
- : undefined}
255
- data-value={String(option.value)}
256
- disabled={option.disabled}
257
- class={optionClasses}
258
- onclick={() => onOptionClick?.(option.value)}
259
- >
260
- {option.label}
261
- </button>
262
- {/each}
263
- </div>
264
- {:else if children}
265
- {@render children?.()}
266
- {/if}
679
+ {@render items()}
267
680
  </div>
268
681
  </div>
269
682
  {/if}
270
683
  </div>
684
+
685
+ {#snippet items()}
686
+ {#if options.length > 0}
687
+ <div class="px-1 py-1">
688
+ {#each options as option (option.value)}
689
+ <DropdownItem
690
+ label={option.label}
691
+ description={option.description}
692
+ icon={option.icon}
693
+ tone={option.tone}
694
+ disabled={option.disabled}
695
+ selected={selectedValue !== null &&
696
+ String(selectedValue) === String(option.value)}
697
+ data-value={String(option.value)}
698
+ onclick={() => onOptionClick?.(option.value)}
699
+ />
700
+ {/each}
701
+ </div>
702
+ {:else if children}
703
+ {@render children?.()}
704
+ {/if}
705
+ {/snippet}
706
+
707
+ <style>
708
+ /*
709
+ * In a sheet the rows are 48px, for a thumb, and as wide as the sheet.
710
+ * px, not rem: a row that grows with the text grows anyway, by its text.
711
+ */
712
+ .dropdown-sheet :global(:is([role="menuitem"], [role="menuitemradio"], [role="menuitemcheckbox"], [role="option"])) {
713
+ min-height: 48px;
714
+ align-items: center;
715
+ }
716
+
717
+ /* The sheet scrolls, so a list with a height limit and a scrollbar of its
718
+ own (Select's) has neither here, and a width set for the pop-over
719
+ (`menuWidth`) is the sheet's. */
720
+ .dropdown-sheet :global([data-menu-scroll]) {
721
+ max-height: none !important;
722
+ overflow: visible !important;
723
+ }
724
+ .dropdown-sheet :global([data-menu-width]) {
725
+ width: 100% !important;
726
+ }
727
+
728
+ /* A header (Select's search field) stays under the sheet's own header
729
+ while the list scrolls beneath it. Wider than the list by the room a
730
+ focus ring takes, so no ring shows beside it. */
731
+ .dropdown-sheet-header {
732
+ position: sticky;
733
+ top: 0;
734
+ z-index: 1;
735
+ margin-inline: -4px;
736
+ padding-inline: 4px;
737
+ background-color: var(--color-surface-overlay);
738
+ }
739
+ </style>