zabi-components 7.0.2 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (433) hide show
  1. package/CHANGELOG.md +1924 -0
  2. package/README.md +287 -11
  3. package/THEME.md +149 -332
  4. package/THEMING.md +1329 -163
  5. package/dist/atoms/ActionPanel.svelte +24 -9
  6. package/dist/atoms/ActionPanel.svelte.d.ts +2 -0
  7. package/dist/atoms/Avatar.svelte +104 -0
  8. package/dist/atoms/Avatar.svelte.d.ts +34 -0
  9. package/dist/atoms/Badge.svelte +124 -58
  10. package/dist/atoms/Badge.svelte.d.ts +17 -5
  11. package/dist/atoms/Button.svelte +212 -62
  12. package/dist/atoms/Button.svelte.d.ts +33 -2
  13. package/dist/atoms/Card.svelte +31 -28
  14. package/dist/atoms/Card.svelte.d.ts +5 -2
  15. package/dist/atoms/CardContent.svelte +10 -2
  16. package/dist/atoms/CardContent.svelte.d.ts +2 -0
  17. package/dist/atoms/CardFooter.svelte +10 -2
  18. package/dist/atoms/CardFooter.svelte.d.ts +2 -0
  19. package/dist/atoms/CardHeader.svelte +26 -6
  20. package/dist/atoms/CardHeader.svelte.d.ts +2 -0
  21. package/dist/atoms/Checkbox.svelte +52 -21
  22. package/dist/atoms/Checkbox.svelte.d.ts +6 -2
  23. package/dist/atoms/CodeBlock.svelte +35 -9
  24. package/dist/atoms/CodeBlock.svelte.d.ts +13 -3
  25. package/dist/atoms/ColorPicker.svelte +272 -52
  26. package/dist/atoms/ColorPicker.svelte.d.ts +6 -1
  27. package/dist/atoms/Container.svelte +17 -10
  28. package/dist/atoms/DateField.svelte +107 -0
  29. package/dist/atoms/DateField.svelte.d.ts +53 -0
  30. package/dist/atoms/Divider.svelte +4 -4
  31. package/dist/atoms/FeatureCard.svelte +4 -3
  32. package/dist/atoms/FieldMessages.svelte +69 -0
  33. package/dist/atoms/FieldMessages.svelte.d.ts +23 -0
  34. package/dist/atoms/FloatingActionButton.svelte +174 -0
  35. package/dist/atoms/FloatingActionButton.svelte.d.ts +51 -0
  36. package/dist/atoms/Heading.svelte +93 -32
  37. package/dist/atoms/Heading.svelte.d.ts +24 -4
  38. package/dist/atoms/IconButton.svelte +242 -53
  39. package/dist/atoms/IconButton.svelte.d.ts +49 -4
  40. package/dist/atoms/Input.svelte +267 -74
  41. package/dist/atoms/Input.svelte.d.ts +32 -2
  42. package/dist/atoms/List.svelte +13 -2
  43. package/dist/atoms/List.svelte.d.ts +2 -0
  44. package/dist/atoms/ListItem.svelte +44 -11
  45. package/dist/atoms/ListItem.svelte.d.ts +5 -0
  46. package/dist/atoms/ListItemLeading.svelte +4 -2
  47. package/dist/atoms/ListItemLeading.svelte.d.ts +1 -1
  48. package/dist/atoms/OptimizedImage.svelte +11 -2
  49. package/dist/atoms/OptimizedImage.svelte.d.ts +2 -0
  50. package/dist/atoms/Progress.svelte +31 -9
  51. package/dist/atoms/Progress.svelte.d.ts +6 -0
  52. package/dist/atoms/Radio.svelte +23 -3
  53. package/dist/atoms/Radio.svelte.d.ts +6 -2
  54. package/dist/atoms/Rating.svelte +494 -0
  55. package/dist/atoms/Rating.svelte.d.ts +56 -0
  56. package/dist/atoms/Select.svelte +475 -135
  57. package/dist/atoms/Select.svelte.d.ts +52 -2
  58. package/dist/atoms/SelectionControl.svelte +99 -18
  59. package/dist/atoms/SelectionControl.svelte.d.ts +6 -3
  60. package/dist/atoms/Skeleton.svelte +5 -3
  61. package/dist/atoms/Slider.svelte +309 -0
  62. package/dist/atoms/Slider.svelte.d.ts +40 -0
  63. package/dist/atoms/Spinner.svelte +57 -0
  64. package/dist/atoms/Spinner.svelte.d.ts +19 -0
  65. package/dist/atoms/Table.svelte +152 -6
  66. package/dist/atoms/Table.svelte.d.ts +15 -3
  67. package/dist/atoms/TemporalField.svelte +140 -0
  68. package/dist/atoms/TemporalField.svelte.d.ts +32 -0
  69. package/dist/atoms/Text.svelte +56 -15
  70. package/dist/atoms/Text.svelte.d.ts +13 -4
  71. package/dist/atoms/Textarea.svelte +57 -45
  72. package/dist/atoms/Textarea.svelte.d.ts +14 -2
  73. package/dist/atoms/ThemeToggle.svelte +238 -56
  74. package/dist/atoms/ThemeToggle.svelte.d.ts +28 -3
  75. package/dist/atoms/TimeField.svelte +108 -0
  76. package/dist/atoms/TimeField.svelte.d.ts +54 -0
  77. package/dist/atoms/Toast.svelte +25 -10
  78. package/dist/atoms/Toast.svelte.d.ts +2 -0
  79. package/dist/atoms/Toggle.svelte +60 -19
  80. package/dist/atoms/Toggle.svelte.d.ts +18 -2
  81. package/dist/atoms/Tooltip.svelte +583 -40
  82. package/dist/atoms/Tooltip.svelte.d.ts +26 -0
  83. package/dist/atoms/ZabiStringsProvider.svelte +45 -0
  84. package/dist/atoms/ZabiStringsProvider.svelte.d.ts +27 -0
  85. package/dist/atoms/index.d.ts +17 -0
  86. package/dist/atoms/index.js +12 -0
  87. package/dist/atoms/selection-control.styles.d.ts +36 -5
  88. package/dist/atoms/selection-control.styles.js +40 -9
  89. package/dist/components/atoms/index.d.ts +17 -0
  90. package/dist/components/atoms/index.d.ts.map +1 -1
  91. package/dist/components/atoms/selection-control.styles.d.ts +36 -5
  92. package/dist/components/atoms/selection-control.styles.d.ts.map +1 -1
  93. package/dist/components/index.d.ts +83 -3
  94. package/dist/components/index.d.ts.map +1 -1
  95. package/dist/components/molecules/index.d.ts +46 -0
  96. package/dist/components/molecules/index.d.ts.map +1 -1
  97. package/dist/components/molecules/toast-store.d.ts +53 -7
  98. package/dist/components/molecules/toast-store.d.ts.map +1 -1
  99. package/dist/components/organisms/index.d.ts +6 -0
  100. package/dist/components/organisms/index.d.ts.map +1 -1
  101. package/dist/components/types/variants.d.ts +15 -1
  102. package/dist/components/types/variants.d.ts.map +1 -1
  103. package/dist/components/util/app-shell.d.ts +28 -0
  104. package/dist/components/util/app-shell.d.ts.map +1 -0
  105. package/dist/components/util/avatar.d.ts +36 -0
  106. package/dist/components/util/avatar.d.ts.map +1 -0
  107. package/dist/components/util/bottom-sheet.d.ts +30 -0
  108. package/dist/components/util/bottom-sheet.d.ts.map +1 -0
  109. package/dist/components/util/bottom-tab-bar.d.ts +35 -0
  110. package/dist/components/util/bottom-tab-bar.d.ts.map +1 -0
  111. package/dist/components/util/calendar.d.ts +56 -0
  112. package/dist/components/util/calendar.d.ts.map +1 -0
  113. package/dist/components/util/cn.d.ts +8 -0
  114. package/dist/components/util/cn.d.ts.map +1 -0
  115. package/dist/components/util/collapsible.d.ts +58 -0
  116. package/dist/components/util/collapsible.d.ts.map +1 -0
  117. package/dist/components/util/confirm-dialog.d.ts +12 -0
  118. package/dist/components/util/confirm-dialog.d.ts.map +1 -0
  119. package/dist/components/util/date.d.ts +96 -0
  120. package/dist/components/util/date.d.ts.map +1 -0
  121. package/dist/components/util/drawer.d.ts +13 -0
  122. package/dist/components/util/drawer.d.ts.map +1 -0
  123. package/dist/components/util/dropdown.d.ts +47 -0
  124. package/dist/components/util/dropdown.d.ts.map +1 -0
  125. package/dist/components/util/field.d.ts +39 -0
  126. package/dist/components/util/field.d.ts.map +1 -0
  127. package/dist/components/util/fit-in-viewport.d.ts +201 -0
  128. package/dist/components/util/fit-in-viewport.d.ts.map +1 -0
  129. package/dist/components/util/focus-utils.d.ts +75 -2
  130. package/dist/components/util/focus-utils.d.ts.map +1 -1
  131. package/dist/components/util/hydration.d.ts +45 -0
  132. package/dist/components/util/hydration.d.ts.map +1 -0
  133. package/dist/components/util/keyboard-inset.d.ts +18 -0
  134. package/dist/components/util/keyboard-inset.d.ts.map +1 -0
  135. package/dist/components/util/media-grid.d.ts +51 -0
  136. package/dist/components/util/media-grid.d.ts.map +1 -0
  137. package/dist/components/util/media.d.ts +6 -0
  138. package/dist/components/util/media.d.ts.map +1 -0
  139. package/dist/components/util/overlay-chrome.d.ts +53 -0
  140. package/dist/components/util/overlay-chrome.d.ts.map +1 -0
  141. package/dist/components/util/overlay.d.ts +72 -0
  142. package/dist/components/util/overlay.d.ts.map +1 -0
  143. package/dist/components/util/photo.d.ts +215 -0
  144. package/dist/components/util/photo.d.ts.map +1 -0
  145. package/dist/components/util/portal.d.ts +23 -0
  146. package/dist/components/util/portal.d.ts.map +1 -0
  147. package/dist/components/util/pull-to-refresh.d.ts +27 -0
  148. package/dist/components/util/pull-to-refresh.d.ts.map +1 -0
  149. package/dist/components/util/radio-keys.d.ts +25 -0
  150. package/dist/components/util/radio-keys.d.ts.map +1 -0
  151. package/dist/components/util/rating.d.ts +23 -0
  152. package/dist/components/util/rating.d.ts.map +1 -0
  153. package/dist/components/util/ready-made-strings.d.ts +77 -0
  154. package/dist/components/util/ready-made-strings.d.ts.map +1 -0
  155. package/dist/components/util/scroll-padding.d.ts +20 -0
  156. package/dist/components/util/scroll-padding.d.ts.map +1 -0
  157. package/dist/components/util/segmented-control.d.ts +16 -0
  158. package/dist/components/util/segmented-control.d.ts.map +1 -0
  159. package/dist/components/util/select.d.ts +41 -0
  160. package/dist/components/util/select.d.ts.map +1 -0
  161. package/dist/components/util/sheet-drag.d.ts +60 -0
  162. package/dist/components/util/sheet-drag.d.ts.map +1 -0
  163. package/dist/components/util/sidebar.d.ts +63 -0
  164. package/dist/components/util/sidebar.d.ts.map +1 -0
  165. package/dist/components/util/sortable-list.d.ts +65 -0
  166. package/dist/components/util/sortable-list.d.ts.map +1 -0
  167. package/dist/components/util/ssr-safe.d.ts +4 -4
  168. package/dist/components/util/ssr-safe.d.ts.map +1 -1
  169. package/dist/components/util/stepper.d.ts +40 -0
  170. package/dist/components/util/stepper.d.ts.map +1 -0
  171. package/dist/components/util/swipeable-list-item.d.ts +36 -0
  172. package/dist/components/util/swipeable-list-item.d.ts.map +1 -0
  173. package/dist/components/util/theme-mode.d.ts +80 -0
  174. package/dist/components/util/theme-mode.d.ts.map +1 -0
  175. package/dist/components/util/toaster.d.ts +93 -0
  176. package/dist/components/util/toaster.d.ts.map +1 -0
  177. package/dist/components/util/tooltip.d.ts +10 -0
  178. package/dist/components/util/tooltip.d.ts.map +1 -0
  179. package/dist/components/util/top-navbar.d.ts +11 -0
  180. package/dist/components/util/top-navbar.d.ts.map +1 -0
  181. package/dist/components/util/touch-target.d.ts +31 -0
  182. package/dist/components/util/touch-target.d.ts.map +1 -0
  183. package/dist/components/util/zabi-strings.d.ts +133 -0
  184. package/dist/components/util/zabi-strings.d.ts.map +1 -0
  185. package/dist/create-theme/cli.js +169 -0
  186. package/dist/create-theme/index.d.ts +176 -0
  187. package/dist/create-theme/index.js +817 -0
  188. package/dist/create-theme/lib/ramp-math.js +128 -0
  189. package/dist/create-theme/lib/resolve.js +141 -0
  190. package/dist/create-theme/theme-data.js +1116 -0
  191. package/dist/index.d.ts +83 -3
  192. package/dist/index.js +60 -8
  193. package/dist/molecules/Alert.svelte +73 -66
  194. package/dist/molecules/Alert.svelte.d.ts +9 -1
  195. package/dist/molecules/AppBar.svelte +415 -0
  196. package/dist/molecules/AppBar.svelte.d.ts +69 -0
  197. package/dist/molecules/AvatarGroup.svelte +116 -0
  198. package/dist/molecules/AvatarGroup.svelte.d.ts +40 -0
  199. package/dist/molecules/BottomSheet.svelte +540 -0
  200. package/dist/molecules/BottomSheet.svelte.d.ts +93 -0
  201. package/dist/molecules/BottomTabBar.svelte +297 -0
  202. package/dist/molecules/BottomTabBar.svelte.d.ts +64 -0
  203. package/dist/molecules/Calendar.svelte +597 -0
  204. package/dist/molecules/Calendar.svelte.d.ts +72 -0
  205. package/dist/molecules/Collapsible.svelte +243 -0
  206. package/dist/molecules/Collapsible.svelte.d.ts +44 -0
  207. package/dist/molecules/CollapsibleGroup.svelte +126 -0
  208. package/dist/molecules/CollapsibleGroup.svelte.d.ts +14 -0
  209. package/dist/molecules/ComponentDemo.svelte +25 -5
  210. package/dist/molecules/ComponentDemo.svelte.d.ts +5 -0
  211. package/dist/molecules/ConfirmDialog.svelte +253 -0
  212. package/dist/molecules/ConfirmDialog.svelte.d.ts +55 -0
  213. package/dist/molecules/ContactForm.svelte +46 -19
  214. package/dist/molecules/ContactForm.svelte.d.ts +10 -0
  215. package/dist/molecules/Drawer.svelte +394 -0
  216. package/dist/molecules/Drawer.svelte.d.ts +55 -0
  217. package/dist/molecules/Dropdown.svelte +540 -50
  218. package/dist/molecules/Dropdown.svelte.d.ts +49 -6
  219. package/dist/molecules/DropdownItem.svelte +122 -0
  220. package/dist/molecules/DropdownItem.svelte.d.ts +28 -0
  221. package/dist/molecules/EmptyState.svelte +56 -10
  222. package/dist/molecules/EmptyState.svelte.d.ts +8 -0
  223. package/dist/molecules/Form.svelte +25 -14
  224. package/dist/molecules/Form.svelte.d.ts +13 -5
  225. package/dist/molecules/FormField.svelte +11 -2
  226. package/dist/molecules/FormField.svelte.d.ts +2 -0
  227. package/dist/molecules/Header.svelte +13 -3
  228. package/dist/molecules/Header.svelte.d.ts +2 -0
  229. package/dist/molecules/ImageUpload.svelte +422 -70
  230. package/dist/molecules/ImageUpload.svelte.d.ts +61 -5
  231. package/dist/molecules/MediaGrid.svelte +527 -0
  232. package/dist/molecules/MediaGrid.svelte.d.ts +77 -0
  233. package/dist/molecules/Modal.svelte +340 -45
  234. package/dist/molecules/Modal.svelte.d.ts +59 -7
  235. package/dist/molecules/NavigationMenu.svelte +15 -3
  236. package/dist/molecules/NavigationMenu.svelte.d.ts +4 -0
  237. package/dist/molecules/NavigationMenuContent.svelte +91 -36
  238. package/dist/molecules/NavigationMenuContent.svelte.d.ts +2 -0
  239. package/dist/molecules/NavigationMenuItem.svelte +10 -3
  240. package/dist/molecules/NavigationMenuItem.svelte.d.ts +2 -0
  241. package/dist/molecules/NavigationMenuLink.svelte +10 -2
  242. package/dist/molecules/NavigationMenuLink.svelte.d.ts +2 -0
  243. package/dist/molecules/NavigationMenuList.svelte +19 -4
  244. package/dist/molecules/NavigationMenuList.svelte.d.ts +2 -0
  245. package/dist/molecules/NavigationMenuTrigger.svelte +16 -3
  246. package/dist/molecules/NavigationMenuTrigger.svelte.d.ts +2 -0
  247. package/dist/molecules/Page.svelte +38 -2
  248. package/dist/molecules/Page.svelte.d.ts +13 -0
  249. package/dist/molecules/PhotoGrid.svelte +433 -0
  250. package/dist/molecules/PhotoGrid.svelte.d.ts +62 -0
  251. package/dist/molecules/PhotoViewer.svelte +1116 -0
  252. package/dist/molecules/PhotoViewer.svelte.d.ts +56 -0
  253. package/dist/molecules/PropsTable.svelte +26 -11
  254. package/dist/molecules/PropsTable.svelte.d.ts +5 -0
  255. package/dist/molecules/PullToRefresh.svelte +263 -0
  256. package/dist/molecules/PullToRefresh.svelte.d.ts +52 -0
  257. package/dist/molecules/RadioGroup.svelte +57 -15
  258. package/dist/molecules/RadioGroup.svelte.d.ts +16 -3
  259. package/dist/molecules/Section.svelte +13 -5
  260. package/dist/molecules/Section.svelte.d.ts +3 -1
  261. package/dist/molecules/SegmentedControl.svelte +323 -0
  262. package/dist/molecules/SegmentedControl.svelte.d.ts +34 -0
  263. package/dist/molecules/SidebarBrandHeader.svelte +24 -5
  264. package/dist/molecules/SidebarBrandHeader.svelte.d.ts +5 -0
  265. package/dist/molecules/SidebarFooter.svelte +39 -8
  266. package/dist/molecules/SidebarFooter.svelte.d.ts +5 -0
  267. package/dist/molecules/SidebarNavSection.svelte +12 -4
  268. package/dist/molecules/SidebarNavSection.svelte.d.ts +2 -0
  269. package/dist/molecules/SlideUp.svelte +253 -30
  270. package/dist/molecules/SlideUp.svelte.d.ts +31 -4
  271. package/dist/molecules/SortableList.svelte +632 -0
  272. package/dist/molecules/SortableList.svelte.d.ts +69 -0
  273. package/dist/molecules/Stepper.svelte +548 -0
  274. package/dist/molecules/Stepper.svelte.d.ts +39 -0
  275. package/dist/molecules/StickyActionBar.svelte +191 -0
  276. package/dist/molecules/StickyActionBar.svelte.d.ts +65 -0
  277. package/dist/molecules/SwipeableListItem.svelte +334 -0
  278. package/dist/molecules/SwipeableListItem.svelte.d.ts +54 -0
  279. package/dist/molecules/Tabs.svelte +254 -24
  280. package/dist/molecules/Tabs.svelte.d.ts +15 -4
  281. package/dist/molecules/Toaster.svelte +352 -6
  282. package/dist/molecules/Toaster.svelte.d.ts +95 -2
  283. package/dist/molecules/ToasterToast.svelte +279 -62
  284. package/dist/molecules/ToasterToast.svelte.d.ts +11 -0
  285. package/dist/molecules/UnsavedChangesBar.svelte +212 -0
  286. package/dist/molecules/UnsavedChangesBar.svelte.d.ts +43 -0
  287. package/dist/molecules/index.d.ts +46 -0
  288. package/dist/molecules/index.js +27 -0
  289. package/dist/molecules/toast-store.d.ts +53 -7
  290. package/dist/molecules/toast-store.js +21 -0
  291. package/dist/organisms/AppShell.svelte +196 -0
  292. package/dist/organisms/AppShell.svelte.d.ts +61 -0
  293. package/dist/organisms/SidebarAccountPanel.svelte +111 -12
  294. package/dist/organisms/SidebarAccountPanel.svelte.d.ts +21 -0
  295. package/dist/organisms/SidebarNavigation.svelte +193 -69
  296. package/dist/organisms/SidebarNavigation.svelte.d.ts +31 -1
  297. package/dist/organisms/SidebarPanel.svelte +45 -16
  298. package/dist/organisms/SidebarPanel.svelte.d.ts +2 -0
  299. package/dist/organisms/SidebarShell.svelte +263 -0
  300. package/dist/organisms/SidebarShell.svelte.d.ts +106 -0
  301. package/dist/organisms/TopNavbar.svelte +329 -33
  302. package/dist/organisms/TopNavbar.svelte.d.ts +28 -0
  303. package/dist/organisms/index.d.ts +6 -0
  304. package/dist/organisms/index.js +4 -0
  305. package/dist/routes/lib/variant-utils.d.ts +26 -0
  306. package/dist/routes/lib/{variant-utils.ts → variant-utils.js} +15 -46
  307. package/dist/types/components.d.ts +4 -0
  308. package/dist/types/components.d.ts.map +1 -1
  309. package/dist/types/events.js +1 -0
  310. package/dist/types/index.d.ts +605 -25
  311. package/dist/types/index.d.ts.map +1 -1
  312. package/dist/types/index.js +3 -0
  313. package/dist/types/variants.ts +17 -1
  314. package/dist/util/app-shell.d.ts +27 -0
  315. package/dist/util/app-shell.js +72 -0
  316. package/dist/util/avatar.d.ts +35 -0
  317. package/dist/util/avatar.js +52 -0
  318. package/dist/util/bottom-sheet.d.ts +29 -0
  319. package/dist/util/bottom-sheet.js +43 -0
  320. package/dist/util/bottom-tab-bar.d.ts +34 -0
  321. package/dist/util/bottom-tab-bar.js +54 -0
  322. package/dist/util/calendar.d.ts +55 -0
  323. package/dist/util/calendar.js +74 -0
  324. package/dist/util/cn.d.ts +7 -0
  325. package/dist/util/cn.js +48 -0
  326. package/dist/util/collapsible.d.ts +57 -0
  327. package/dist/util/collapsible.js +23 -0
  328. package/dist/util/confirm-dialog.d.ts +11 -0
  329. package/dist/util/confirm-dialog.js +2 -0
  330. package/dist/util/date.d.ts +95 -0
  331. package/dist/util/date.js +237 -0
  332. package/dist/util/drawer.d.ts +12 -0
  333. package/dist/util/drawer.js +7 -0
  334. package/dist/util/dropdown.d.ts +46 -0
  335. package/dist/util/dropdown.js +21 -0
  336. package/dist/util/field.d.ts +38 -0
  337. package/dist/util/field.js +38 -0
  338. package/dist/util/fit-in-viewport.d.ts +200 -0
  339. package/dist/util/fit-in-viewport.js +399 -0
  340. package/dist/util/focus-utils.d.ts +75 -2
  341. package/dist/util/focus-utils.js +182 -9
  342. package/dist/util/hydration.d.ts +44 -0
  343. package/dist/util/hydration.js +58 -0
  344. package/dist/util/keyboard-inset.d.ts +17 -0
  345. package/dist/util/keyboard-inset.js +53 -0
  346. package/dist/util/media-grid.d.ts +50 -0
  347. package/dist/util/media-grid.js +81 -0
  348. package/dist/util/media.d.ts +5 -0
  349. package/dist/util/media.js +10 -0
  350. package/dist/util/overlay-chrome.d.ts +52 -0
  351. package/dist/util/overlay-chrome.js +56 -0
  352. package/dist/util/overlay.d.ts +71 -0
  353. package/dist/util/overlay.js +240 -0
  354. package/dist/util/photo.d.ts +214 -0
  355. package/dist/util/photo.js +206 -0
  356. package/dist/util/portal.d.ts +22 -0
  357. package/dist/util/portal.js +50 -0
  358. package/dist/util/pull-to-refresh.d.ts +26 -0
  359. package/dist/util/pull-to-refresh.js +30 -0
  360. package/dist/util/radio-keys.d.ts +24 -0
  361. package/dist/util/radio-keys.js +49 -0
  362. package/dist/util/rating.d.ts +22 -0
  363. package/dist/util/rating.js +26 -0
  364. package/dist/util/ready-made-strings.d.ts +76 -0
  365. package/dist/util/ready-made-strings.js +67 -0
  366. package/dist/util/scroll-padding.d.ts +19 -0
  367. package/dist/util/scroll-padding.js +44 -0
  368. package/dist/util/segmented-control.d.ts +15 -0
  369. package/dist/util/segmented-control.js +1 -0
  370. package/dist/util/select.d.ts +40 -0
  371. package/dist/util/select.js +63 -0
  372. package/dist/util/sheet-drag.d.ts +59 -0
  373. package/dist/util/sheet-drag.js +241 -0
  374. package/dist/util/sidebar.d.ts +62 -0
  375. package/dist/util/sidebar.js +38 -0
  376. package/dist/util/sortable-list.d.ts +64 -0
  377. package/dist/util/sortable-list.js +53 -0
  378. package/dist/util/ssr-safe.d.ts +4 -4
  379. package/dist/util/stepper.d.ts +39 -0
  380. package/dist/util/stepper.js +38 -0
  381. package/dist/util/swipeable-list-item.d.ts +35 -0
  382. package/dist/util/swipeable-list-item.js +32 -0
  383. package/dist/util/theme-mode.d.ts +79 -0
  384. package/dist/util/theme-mode.js +121 -0
  385. package/dist/util/toaster.d.ts +92 -0
  386. package/dist/util/toaster.js +75 -0
  387. package/dist/util/tooltip.d.ts +9 -0
  388. package/dist/util/tooltip.js +18 -0
  389. package/dist/util/top-navbar.d.ts +10 -0
  390. package/dist/util/top-navbar.js +5 -0
  391. package/dist/util/touch-target.d.ts +30 -0
  392. package/dist/util/touch-target.js +30 -0
  393. package/dist/util/zabi-strings.d.ts +132 -0
  394. package/dist/util/zabi-strings.js +105 -0
  395. package/dist/zabi-components-colors.css +5 -2
  396. package/dist/zabi-components-theme-dark-only.css +574 -225
  397. package/dist/zabi-components-theme-dark.css +574 -225
  398. package/dist/zabi-components-theme-only.css +1680 -317
  399. package/dist/zabi-components-theme.css +1680 -317
  400. package/dist/zabi-components.css +4473 -1158
  401. package/docs/lucide-icons.md +41 -16
  402. package/docs/theme-imports.md +81 -11
  403. package/package.json +51 -29
  404. package/dist/lib/layout-width-tokens.d.ts +0 -17
  405. package/dist/lib/layout-width-tokens.d.ts.map +0 -1
  406. package/dist/lib/showcase/component-docs/Button.d.ts +0 -3
  407. package/dist/lib/showcase/component-docs/Button.d.ts.map +0 -1
  408. package/dist/lib/showcase/component-docs/Input.d.ts +0 -3
  409. package/dist/lib/showcase/component-docs/Input.d.ts.map +0 -1
  410. package/dist/lib/showcase/component-docs/List.d.ts +0 -3
  411. package/dist/lib/showcase/component-docs/List.d.ts.map +0 -1
  412. package/dist/lib/showcase/component-docs/Modal.d.ts +0 -3
  413. package/dist/lib/showcase/component-docs/Modal.d.ts.map +0 -1
  414. package/dist/lib/showcase/component-docs/Radio.d.ts +0 -3
  415. package/dist/lib/showcase/component-docs/Radio.d.ts.map +0 -1
  416. package/dist/lib/showcase/component-docs/SidebarNavigation.d.ts +0 -3
  417. package/dist/lib/showcase/component-docs/SidebarNavigation.d.ts.map +0 -1
  418. package/dist/lib/showcase/component-docs/Skeleton.d.ts +0 -3
  419. package/dist/lib/showcase/component-docs/Skeleton.d.ts.map +0 -1
  420. package/dist/lib/showcase/component-docs/_shared.d.ts +0 -12
  421. package/dist/lib/showcase/component-docs/_shared.d.ts.map +0 -1
  422. package/dist/lib/showcase/component-docs/index.d.ts +0 -4
  423. package/dist/lib/showcase/component-docs/index.d.ts.map +0 -1
  424. package/dist/lib/showcase/components-catalog.d.ts +0 -3
  425. package/dist/lib/showcase/components-catalog.d.ts.map +0 -1
  426. package/dist/lib/showcase/components-showcase-constants.d.ts +0 -89
  427. package/dist/lib/showcase/components-showcase-constants.d.ts.map +0 -1
  428. package/dist/lib/showcase/docs-sidebar-helpers.d.ts +0 -15
  429. package/dist/lib/showcase/docs-sidebar-helpers.d.ts.map +0 -1
  430. package/dist/routes/lib/focus-utils.d.ts +0 -12
  431. package/dist/routes/lib/focus-utils.ts +0 -110
  432. package/dist/routes/lib/ssr-safe.ts +0 -58
  433. package/scripts/fix-lucide-svelte-icon-dts.js +0 -35
@@ -1,14 +1,48 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from "svelte";
3
3
  import type { HTMLAttributes } from "svelte/elements";
4
- import { onDestroy } from "svelte";
4
+ import { onDestroy, untrack } from "svelte";
5
5
  import { generateId } from "../util/ssr-safe.js";
6
-
6
+ import { claimOpenTooltip, releaseOpenTooltip } from "../util/tooltip.js";
7
+ import {
8
+ insideTriangle,
9
+ pickSide,
10
+ shiftIntoViewport,
11
+ type FloatingSide,
12
+ } from "../util/fit-in-viewport.js";
13
+
14
+ import { cn } from "../util/cn.js";
7
15
  type Props = Omit<HTMLAttributes<HTMLDivElement>, "class"> & {
8
16
  content?: string;
9
17
  placement?: "top" | "bottom" | "left" | "right";
10
18
  delay?: number;
11
19
  disabled?: boolean;
20
+ /**
21
+ * Lay the trigger out as a full-width block instead of an inline-block.
22
+ * A prop rather than a `class="block w-full"` override, because `block`
23
+ * and `inline-block` are equal-specificity utilities: the winner would
24
+ * be whichever Tailwind emits later, not whichever the caller passed.
25
+ */
26
+ block?: boolean;
27
+ /**
28
+ * Position the bubble against the viewport instead of the trigger's
29
+ * offset parent. An absolutely-positioned tooltip cannot escape an
30
+ * ancestor that scrolls: a collapsed sidebar rail sits inside an
31
+ * `overflow-y-auto overflow-x-hidden` list, which clipped the bubble
32
+ * at the rail edge and left only the arrow showing.
33
+ */
34
+ fixed?: boolean;
35
+ /**
36
+ * On a touch screen a tap on the trigger opens the tooltip, and the
37
+ * trigger still does what it does. It stays until a second tap, a tap
38
+ * elsewhere, Escape, scrolling or focus leaving: the tap also puts
39
+ * focus on the trigger, and a tooltip that went by itself while its
40
+ * trigger still had focus would be gone before it was read. Give a
41
+ * number of milliseconds to have it close by itself after that long
42
+ * as well, for a button whose tooltip is in the way of what the tap
43
+ * did. A mouse and a keyboard are not affected.
44
+ */
45
+ touchDuration?: number;
12
46
  class?: string;
13
47
  children?: Snippet;
14
48
  };
@@ -16,23 +50,246 @@
16
50
  /** First matching focusable inside the slot receives `aria-describedby` (wrapper div is skipped). */
17
51
  const FOCUSABLE_SELECTOR =
18
52
  'button:not([disabled]), a[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
53
+ /**
54
+ * With nothing focusable in the slot, the first control of any kind is
55
+ * described instead: a disabled button is where a tooltip most often says
56
+ * something that matters (why it is disabled). It cannot take focus, so
57
+ * the keyboard never opens that tooltip: `aria-disabled="true"` on a
58
+ * button that stays focusable is the pattern that reaches everyone.
59
+ */
60
+ const CONTROL_SELECTOR = 'button, a[href], input, select, textarea, [role="button"], [tabindex]';
19
61
 
20
62
  let {
21
63
  content = "",
22
64
  placement = "top",
23
65
  delay = 0,
24
66
  disabled = false,
67
+ block = false,
68
+ fixed = false,
69
+ touchDuration = 0,
70
+ class: className = "",
25
71
  children,
26
72
  ...restProps
27
73
  }: Props = $props();
28
74
 
75
+ let fixedStyle = $state("");
76
+
77
+ /** Gap must match --tooltip-gap's default (0.5rem). */
78
+ const FIXED_GAP = 8;
79
+ /** How close to the edge of the viewport the bubble may come, in px. */
80
+ const EDGE_MARGIN = 8;
81
+
82
+ /**
83
+ * The side the bubble is on when that is not `placement`: there was no
84
+ * room there, or the screen is too narrow to put anything beside the
85
+ * trigger. Decided each time the tooltip opens.
86
+ */
87
+ let flippedTo = $state<FloatingSide | null>(null);
88
+ const side = $derived(flippedTo ?? placement);
89
+ /** How far the bubble is moved along its side to stay on screen, in px. */
90
+ let shift = $state({ x: 0, y: 0 });
91
+ /** The arrow moves back by as much, so it still points at the trigger. */
92
+ let arrowShift = $state({ x: 0, y: 0 });
93
+
94
+ function positionFixed(): void {
95
+ if (!fixed || !triggerElement) return;
96
+ const r = triggerElement.getBoundingClientRect();
97
+ // The styles centre the bubble on the cross axis only. Above and to
98
+ // the left it also has to clear the trigger by its own size, or it
99
+ // lies over it: `left` and `top` are where its near corner goes.
100
+ const width = bubbleElement?.offsetWidth ?? 0;
101
+ const height = bubbleElement?.offsetHeight ?? 0;
102
+ const coords = {
103
+ top: [r.left + r.width / 2, r.top - FIXED_GAP - height],
104
+ bottom: [r.left + r.width / 2, r.bottom + FIXED_GAP],
105
+ left: [r.left - FIXED_GAP - width, r.top + r.height / 2],
106
+ right: [r.right + FIXED_GAP, r.top + r.height / 2],
107
+ }[side];
108
+ fixedStyle = `left:${coords[0]}px;top:${coords[1]}px;`;
109
+ }
110
+
111
+ /**
112
+ * Picks the side and the shift that keep the bubble in the viewport. It is
113
+ * worked out from the trigger's rectangle and the bubble's size, not from
114
+ * where the bubble is drawn: that is mid-transition when this runs.
115
+ */
116
+ function fitToViewport(): void {
117
+ const trigger = triggerElement;
118
+ const bubble = bubbleElement;
119
+ if (!trigger || !bubble) return;
120
+ const width = bubble.offsetWidth;
121
+ const height = bubble.offsetHeight;
122
+ // No layout (a test DOM, a hidden subtree): nothing to fit.
123
+ if (!width || !height) return;
124
+ const root = document.documentElement;
125
+ const viewport = {
126
+ width: root.clientWidth || window.innerWidth,
127
+ height: root.clientHeight || window.innerHeight,
128
+ };
129
+ const r = trigger.getBoundingClientRect();
130
+ // Below 640px nothing fits beside a trigger: above it, or below.
131
+ const narrow =
132
+ typeof window.matchMedia === "function" &&
133
+ window.matchMedia("(max-width: 640px)").matches;
134
+ const preferred: FloatingSide =
135
+ narrow && (placement === "left" || placement === "right") ? "top" : placement;
136
+ // `left` and `right` are the start and the end side in the styles
137
+ // below, so in a right-to-left page they are the other way round on
138
+ // screen. The fixed strategy places by `left` and `top` and is not.
139
+ const mirrored = !fixed && getComputedStyle(trigger).direction === "rtl";
140
+ isMirrored = mirrored;
141
+ const onScreen = (name: FloatingSide): FloatingSide =>
142
+ mirrored && name === "left" ? "right" : mirrored && name === "right" ? "left" : name;
143
+ const next = pickSide(onScreen(preferred), r, { width, height }, viewport, {
144
+ gap: FIXED_GAP,
145
+ margin: EDGE_MARGIN,
146
+ });
147
+ // Back to the name the styles know it by.
148
+ const named = onScreen(next);
149
+ flippedTo = named === placement ? null : named;
150
+
151
+ const centreX = r.left + r.width / 2;
152
+ const centreY = r.top + r.height / 2;
153
+ const [left, top] = {
154
+ top: [centreX - width / 2, r.top - FIXED_GAP - height],
155
+ bottom: [centreX - width / 2, r.bottom + FIXED_GAP],
156
+ left: [r.left - FIXED_GAP - width, centreY - height / 2],
157
+ right: [r.right + FIXED_GAP, centreY - height / 2],
158
+ }[next];
159
+ const move = shiftIntoViewport(
160
+ { left, top, right: left + width, bottom: top + height },
161
+ viewport,
162
+ EDGE_MARGIN,
163
+ );
164
+ // Rounded, so a sub-pixel difference does not blur the text.
165
+ const x = Math.round(move.x);
166
+ const y = Math.round(move.y);
167
+ if (x !== shift.x || y !== shift.y) shift = { x, y };
168
+ // The arrow stays on the bubble: 12px clear of its corners.
169
+ const reach = (extent: number) => Math.max(0, extent / 2 - 12);
170
+ const vertical = next === "top" || next === "bottom";
171
+ const arrowX = vertical ? -Math.max(-reach(width), Math.min(reach(width), x)) : 0;
172
+ const arrowY = vertical ? 0 : -Math.max(-reach(height), Math.min(reach(height), y));
173
+ if (arrowX !== arrowShift.x || arrowY !== arrowShift.y) {
174
+ arrowShift = { x: arrowX, y: arrowY };
175
+ }
176
+ }
177
+
178
+ $effect(() => {
179
+ if (!isVisible) return;
180
+ untrack(fitToViewport);
181
+ const onMove = () => fitToViewport();
182
+ window.addEventListener("scroll", onMove, true);
183
+ window.addEventListener("resize", onMove);
184
+ return () => {
185
+ window.removeEventListener("scroll", onMove, true);
186
+ window.removeEventListener("resize", onMove);
187
+ };
188
+ });
189
+
190
+ const bubbleStyle = $derived(
191
+ (fixed ? fixedStyle : "") +
192
+ (shift.x || shift.y ? `translate:${shift.x}px ${shift.y}px;` : "") +
193
+ (arrowShift.x || arrowShift.y
194
+ ? `--tooltip-arrow-shift:${arrowShift.x}px ${arrowShift.y}px;`
195
+ : ""),
196
+ );
197
+
198
+ $effect(() => {
199
+ if (!fixed || !isVisible) return;
200
+ positionFixed();
201
+ const onMove = () => positionFixed();
202
+ // `true` so ancestor scrolls (the nav list itself) are caught too.
203
+ window.addEventListener("scroll", onMove, true);
204
+ window.addEventListener("resize", onMove);
205
+ return () => {
206
+ window.removeEventListener("scroll", onMove, true);
207
+ window.removeEventListener("resize", onMove);
208
+ };
209
+ });
210
+
29
211
  const triggerId = generateId("tooltip-trigger");
30
212
  const tooltipId = generateId("tooltip");
31
213
  let isVisible = $state(false);
214
+ let containerElement: HTMLElement | null = $state(null);
32
215
  let triggerElement: HTMLElement | null = $state(null);
216
+ let bubbleElement: HTMLElement | null = $state(null);
33
217
  let showDelayTimeout: ReturnType<typeof setTimeout> | null = null;
34
218
  let hideBlurTimeout: ReturnType<typeof setTimeout> | null = null;
35
219
 
220
+ /**
221
+ * A hidden bubble still takes part in layout, and one that reaches past
222
+ * the right edge of a 320px screen makes the whole page scroll sideways.
223
+ * While it is closed it is parked in the corner of the viewport, where it
224
+ * widens nothing; it is parked once it has faded out, not before.
225
+ */
226
+ let parked = $state(true);
227
+ let parkTimeout: ReturnType<typeof setTimeout> | null = null;
228
+ /** Longer than the 200ms fade. */
229
+ const PARK_AFTER = 250;
230
+
231
+ /** The pointer that last touched the trigger: "mouse", "touch" or "pen". */
232
+ let lastPointerType = "";
233
+ /** When a finger last went down on, or came up from, the trigger. */
234
+ let touchedAt = 0;
235
+ /** Open because of a tap: closes on a tap elsewhere, on scroll, and after `touchDuration`. */
236
+ let openedByTouch = $state(false);
237
+ let touchHideTimeout: ReturnType<typeof setTimeout> | null = null;
238
+ /** A tap also focuses the trigger; for this long that focus is the tap's, not new input. */
239
+ const TOUCH_ECHO = 700;
240
+ /** True while a mouse is over the trigger, the bubble or the gap between them. */
241
+ let hovering = false;
242
+ /** The finger that is down on the trigger, and where it went down. */
243
+ let touchStart: { id: number; x: number; y: number } | null = null;
244
+ /** Further than this, in px, and it was a drag, not a tap. */
245
+ const TAP_SLOP = 10;
246
+
247
+ function clearTouchHide(): void {
248
+ if (touchHideTimeout !== null) {
249
+ clearTimeout(touchHideTimeout);
250
+ touchHideTimeout = null;
251
+ }
252
+ }
253
+
254
+ /** True in a right-to-left page, where the start side is the right: the side arrows turn round. */
255
+ let isMirrored = $state(false);
256
+
257
+ function show(): void {
258
+ stopCrossing();
259
+ // One at a time: the tooltip that was open closes.
260
+ claimOpenTooltip(hide);
261
+ if (parkTimeout !== null) {
262
+ clearTimeout(parkTimeout);
263
+ parkTimeout = null;
264
+ }
265
+ // Placed before it is let out of the corner: laid out where the styles
266
+ // alone put it, a bubble near the edge would widen the page for a
267
+ // moment, and in a right-to-left page that moves what is under the finger.
268
+ if (!isVisible) fitToViewport();
269
+ parked = false;
270
+ isVisible = true;
271
+ }
272
+
273
+ function hide(): void {
274
+ clearTouchHide();
275
+ stopCrossing();
276
+ openedByTouch = false;
277
+ releaseOpenTooltip(hide);
278
+ if (!isVisible) return;
279
+ isVisible = false;
280
+ if (parkTimeout !== null) clearTimeout(parkTimeout);
281
+ parkTimeout = setTimeout(() => {
282
+ parkTimeout = null;
283
+ parked = true;
284
+ flippedTo = null;
285
+ shift = { x: 0, y: 0 };
286
+ arrowShift = { x: 0, y: 0 };
287
+ }, PARK_AFTER);
288
+ }
289
+
290
+ const isTouch = (type: string) => type === "touch" || type === "pen";
291
+ const afterTouch = () => isTouch(lastPointerType) && Date.now() - touchedAt < TOUCH_ECHO;
292
+
36
293
  function clearShowDelay(): void {
37
294
  if (showDelayTimeout !== null) {
38
295
  clearTimeout(showDelayTimeout);
@@ -48,7 +305,9 @@
48
305
  }
49
306
 
50
307
  function findDescribedTarget(root: HTMLElement): HTMLElement | null {
51
- const found = root.querySelector<HTMLElement>(FOCUSABLE_SELECTOR);
308
+ const found =
309
+ root.querySelector<HTMLElement>(FOCUSABLE_SELECTOR) ??
310
+ root.querySelector<HTMLElement>(CONTROL_SELECTOR);
52
311
  if (found) {
53
312
  return found;
54
313
  }
@@ -86,28 +345,60 @@
86
345
  if (!content || disabled) {
87
346
  clearShowDelay();
88
347
  clearHideBlurTimeout();
348
+ // Switched off while open: closed, and no longer listening for a tap elsewhere.
349
+ untrack(hide);
89
350
  }
90
351
  });
91
352
 
92
353
  onDestroy(() => {
354
+ stopCrossing();
355
+ releaseOpenTooltip(hide);
93
356
  clearShowDelay();
94
357
  clearHideBlurTimeout();
358
+ clearTouchHide();
359
+ if (parkTimeout !== null) clearTimeout(parkTimeout);
360
+ });
361
+
362
+ // A tooltip opened by a tap has no hover to end and, where a tap does not
363
+ // focus a button (Safari), no blur either: a tap anywhere else closes it,
364
+ // and so does scrolling, which would otherwise carry it over other content.
365
+ $effect(() => {
366
+ if (!isVisible || !openedByTouch) return;
367
+ const onPointerDown = (event: PointerEvent) => {
368
+ if (!containerElement?.contains(event.target as Node | null)) hide();
369
+ };
370
+ const onScroll = () => hide();
371
+ document.addEventListener("pointerdown", onPointerDown, true);
372
+ window.addEventListener("scroll", onScroll, true);
373
+ return () => {
374
+ document.removeEventListener("pointerdown", onPointerDown, true);
375
+ window.removeEventListener("scroll", onScroll, true);
376
+ };
95
377
  });
96
378
 
97
379
  function handleKeydown(event: KeyboardEvent) {
98
380
  if (event.key === "Escape" && isVisible) {
381
+ // Capture phase, and marked as handled: the first Escape dismisses
382
+ // the tooltip only, not the modal or menu it sits in (WCAG 1.4.13).
383
+ event.preventDefault();
99
384
  clearShowDelay();
100
385
  clearHideBlurTimeout();
101
- isVisible = false;
102
- triggerElement?.focus();
386
+ hide();
387
+ // Keep focus on the described control; never steal it when the tooltip was hover-only.
388
+ const target = triggerElement ? findDescribedTarget(triggerElement) : null;
389
+ if (target && triggerElement?.contains(document.activeElement)) {
390
+ target.focus();
391
+ }
103
392
  }
104
393
  }
105
394
 
106
395
  function handleFocus() {
107
396
  clearShowDelay();
108
397
  clearHideBlurTimeout();
398
+ // The focus a tap gives the trigger: the tap has already opened or closed it.
399
+ if (afterTouch()) return;
109
400
  if (!disabled && content) {
110
- isVisible = true;
401
+ show();
111
402
  }
112
403
  }
113
404
 
@@ -116,56 +407,202 @@
116
407
  // Defer hide so brief focus moves within the trigger subtree do not flash the tooltip off.
117
408
  hideBlurTimeout = setTimeout(() => {
118
409
  hideBlurTimeout = null;
119
- isVisible = false;
410
+ // A press on the bubble itself (to select its text) takes focus
411
+ // off the trigger. The pointer is still on the tooltip: it stays,
412
+ // and leaving it, a press elsewhere or Escape closes it.
413
+ if (hovering || containerElement?.contains(document.activeElement)) return;
414
+ hide();
120
415
  }, 100);
121
416
  }
122
417
 
123
418
  function handleMouseEnter() {
419
+ // The mouse events a browser makes up after a tap. Opening here would
420
+ // undo a tap that closed the tooltip, and on iOS a hover handler that
421
+ // shows something makes the first tap a hover and drops its click.
422
+ if (isTouch(lastPointerType)) return;
423
+ hovering = true;
424
+ stopCrossing();
124
425
  clearShowDelay();
125
426
  clearHideBlurTimeout();
126
427
  if (!disabled && content) {
127
428
  if (delay > 0) {
128
429
  showDelayTimeout = setTimeout(() => {
129
430
  showDelayTimeout = null;
130
- isVisible = true;
431
+ show();
131
432
  }, delay);
132
433
  } else {
133
- isVisible = true;
434
+ show();
134
435
  }
135
436
  }
136
437
  }
137
438
 
138
- function handleMouseLeave() {
439
+ /** Stops watching a pointer that is on its way from the trigger to the bubble. */
440
+ let stopCrossing: () => void = () => {};
441
+ /** A pointer that has stopped short of the bubble is given this long without moving, in ms. */
442
+ const CROSSING_TIME = 300;
443
+
444
+ /**
445
+ * The pointer has left the tooltip. Straight across the gap it never
446
+ * does, but from a small trigger to the far corner of a wide bubble the
447
+ * way is diagonal and leaves through the side. While it stays in the
448
+ * triangle between where it left and the near edge of the bubble it is
449
+ * still coming, and the tooltip waits: until it arrives, strays, or stops.
450
+ */
451
+ function handleMouseLeave(event: MouseEvent) {
452
+ hovering = false;
453
+ if (isTouch(lastPointerType)) return;
139
454
  clearShowDelay();
140
455
  clearHideBlurTimeout();
141
- isVisible = false;
456
+ const box = isVisible ? bubbleElement?.getBoundingClientRect() : undefined;
457
+ // No bubble on screen (or no layout to measure): nothing to cross to.
458
+ if (!box || box.width === 0 || box.height === 0) {
459
+ hide();
460
+ return;
461
+ }
462
+ const left = { x: event.clientX, y: event.clientY };
463
+ // The edge of the bubble that faces the trigger, a little wider than it is.
464
+ const reach = 4;
465
+ const [near, far] = {
466
+ top: [
467
+ { x: box.left - reach, y: box.bottom },
468
+ { x: box.right + reach, y: box.bottom },
469
+ ],
470
+ bottom: [
471
+ { x: box.left - reach, y: box.top },
472
+ { x: box.right + reach, y: box.top },
473
+ ],
474
+ left: [
475
+ { x: box.right, y: box.top - reach },
476
+ { x: box.right, y: box.bottom + reach },
477
+ ],
478
+ right: [
479
+ { x: box.left, y: box.top - reach },
480
+ { x: box.left, y: box.bottom + reach },
481
+ ],
482
+ }[
483
+ // On screen: the start side is the right in a right-to-left page.
484
+ isMirrored && side === "left" ? "right" : isMirrored && side === "right" ? "left" : side
485
+ ];
486
+ stopCrossing();
487
+ let timer = setTimeout(hide, CROSSING_TIME);
488
+ const onMove = (move: MouseEvent) => {
489
+ if (insideTriangle({ x: move.clientX, y: move.clientY }, left, near, far)) {
490
+ // Still coming: the wait starts again.
491
+ clearTimeout(timer);
492
+ timer = setTimeout(hide, CROSSING_TIME);
493
+ return;
494
+ }
495
+ // Not on the way: unless it has arrived, which `mouseenter` says.
496
+ if (containerElement?.contains(move.target as Node | null)) return;
497
+ hide();
498
+ };
499
+ document.addEventListener("mousemove", onMove, true);
500
+ stopCrossing = () => {
501
+ clearTimeout(timer);
502
+ document.removeEventListener("mousemove", onMove, true);
503
+ stopCrossing = () => {};
504
+ };
505
+ }
506
+
507
+ /** Which kind of pointer is over the trigger; a mouse arriving after a finger counts again. */
508
+ function handlePointerEnter(event: PointerEvent) {
509
+ lastPointerType = event.pointerType;
510
+ }
511
+
512
+ /**
513
+ * A tap of a finger or a pen on the trigger toggles the tooltip: when the
514
+ * finger comes up where it went down, not when it goes down. A finger
515
+ * that goes down to scroll the page would otherwise open the tooltip and
516
+ * close it again as the scroll began. Nothing is prevented: the tap goes
517
+ * on to become the trigger's own click.
518
+ */
519
+ function handlePointerDown(event: PointerEvent) {
520
+ lastPointerType = event.pointerType;
521
+ if (!isTouch(event.pointerType)) return;
522
+ touchedAt = Date.now();
523
+ // On the bubble itself: neither a tap on the trigger nor one elsewhere.
524
+ if (bubbleElement?.contains(event.target as Node | null)) {
525
+ touchStart = null;
526
+ // It may take focus off the trigger; the tooltip stays, and from
527
+ // here on a tap elsewhere or scrolling is what closes it.
528
+ clearHideBlurTimeout();
529
+ hovering = true;
530
+ if (isVisible) openedByTouch = true;
531
+ return;
532
+ }
533
+ hovering = false;
534
+ touchStart = { id: event.pointerId, x: event.clientX, y: event.clientY };
535
+ }
536
+
537
+ function handlePointerUp(event: PointerEvent) {
538
+ if (!isTouch(event.pointerType)) return;
539
+ touchedAt = Date.now();
540
+ const start = touchStart;
541
+ touchStart = null;
542
+ if (!start || start.id !== event.pointerId) return;
543
+ if (Math.hypot(event.clientX - start.x, event.clientY - start.y) > TAP_SLOP) return;
544
+ clearShowDelay();
545
+ clearHideBlurTimeout();
546
+ if (isVisible) {
547
+ hide();
548
+ return;
549
+ }
550
+ if (disabled || !content) return;
551
+ show();
552
+ openedByTouch = true;
553
+ clearTouchHide();
554
+ if (touchDuration > 0) {
555
+ touchHideTimeout = setTimeout(() => {
556
+ touchHideTimeout = null;
557
+ hide();
558
+ }, touchDuration);
559
+ }
560
+ }
561
+
562
+ /** The browser took the touch for itself: to scroll, most often. */
563
+ function handlePointerCancel() {
564
+ touchStart = null;
142
565
  }
143
566
  </script>
144
567
 
145
- <svelte:window onkeydown={handleKeydown} />
568
+ <svelte:window onkeydowncapture={handleKeydown} />
146
569
 
147
570
  <div
148
- class="tooltip-container relative inline-block"
149
- data-placement={placement}
571
+ class={cn(
572
+ "tooltip-container relative",
573
+ block ? "block w-full" : "inline-block",
574
+ className,
575
+ )}
576
+ bind:this={containerElement}
577
+ data-placement={side}
578
+ data-strategy={fixed ? "fixed" : "absolute"}
579
+ data-mirrored={isMirrored ? "true" : undefined}
150
580
  data-disabled={disabled}
151
581
  onmouseenter={handleMouseEnter}
152
582
  onmouseleave={handleMouseLeave}
583
+ onpointerenter={handlePointerEnter}
584
+ onpointerdown={handlePointerDown}
585
+ onpointerup={handlePointerUp}
586
+ onpointercancel={handlePointerCancel}
153
587
  onfocusin={handleFocus}
154
588
  onfocusout={handleBlur}
155
589
  {...restProps}
156
590
  >
157
- <div bind:this={triggerElement} id={triggerId}>
591
+ <div bind:this={triggerElement} id={triggerId} class={block ? "w-full" : undefined}>
158
592
  {@render children?.()}
159
593
  </div>
160
594
 
161
595
  {#if content && !disabled}
162
596
  <div
597
+ bind:this={bubbleElement}
163
598
  id={tooltipId}
164
- class="tooltip pointer-events-none invisible absolute z-tooltip whitespace-normal wrap-break-word rounded-lg bg-tooltip-bg px-3 py-2 text-sm leading-5 text-tooltip-fg opacity-0 transition-[opacity,visibility,transform] duration-200 ease-in-out"
599
+ class="tooltip pointer-events-none invisible absolute z-tooltip whitespace-normal wrap-break-word rounded-control bg-tooltip-bg px-3 py-2 text-sm leading-5 text-tooltip-fg opacity-0 transition-[opacity,visibility,transform] duration-200 ease-in-out"
165
600
  role="tooltip"
166
601
  aria-hidden={!isVisible}
167
602
  data-visible={isVisible}
168
- data-placement={placement}
603
+ data-parked={parked}
604
+ data-placement={side}
605
+ style={bubbleStyle || undefined}
169
606
  >
170
607
  {content}
171
608
  </div>
@@ -173,13 +610,7 @@
173
610
  </div>
174
611
 
175
612
  <style>
176
- :root {
177
- /* Cap only very long copy; width:max-content keeps typical sentences on one line until this limit */
178
- --tooltip-max-width: min(24rem, calc(100vw - 2rem));
179
- --tooltip-gap: 0.5rem;
180
- --tooltip-arrow-size: 4px;
181
- }
182
-
613
+ /* Tunables (override on :root or any ancestor): --tooltip-max-width, --tooltip-gap, --tooltip-arrow-size. Defaults live in the var() fallbacks. */
183
614
  /* max-width: policy forbids max-w-* utilities on packaged atoms; variable used by sm: override */
184
615
  .tooltip {
185
616
  width: -moz-max-content;
@@ -187,11 +618,13 @@
187
618
  max-width: var(--tooltip-max-width, min(24rem, calc(100vw - 2rem)));
188
619
  }
189
620
 
621
+ /* Centred over the trigger with `left`, not `inset-inline-start`: the
622
+ shift back by half is to the left whichever way the text runs. */
190
623
  .tooltip-container[data-placement="top"] .tooltip {
191
624
  inset-block-end: 100%;
192
- inset-inline-start: 50%;
625
+ left: 50%;
193
626
  transform: translateX(-50%) translateY(4px) scale(0.95);
194
- margin-block-end: var(--tooltip-gap);
627
+ margin-block-end: var(--tooltip-gap, 0.5rem);
195
628
  }
196
629
 
197
630
  .tooltip-container[data-placement="top"] .tooltip[data-visible="true"] {
@@ -202,9 +635,9 @@
202
635
 
203
636
  .tooltip-container[data-placement="bottom"] .tooltip {
204
637
  inset-block-start: 100%;
205
- inset-inline-start: 50%;
638
+ left: 50%;
206
639
  transform: translateX(-50%) translateY(-4px) scale(0.95);
207
- margin-block-start: var(--tooltip-gap);
640
+ margin-block-start: var(--tooltip-gap, 0.5rem);
208
641
  }
209
642
 
210
643
  .tooltip-container[data-placement="bottom"] .tooltip[data-visible="true"] {
@@ -217,7 +650,7 @@
217
650
  inset-inline-end: 100%;
218
651
  inset-block-start: 50%;
219
652
  transform: translateY(-50%) translateX(4px) scale(0.95);
220
- margin-inline-end: var(--tooltip-gap);
653
+ margin-inline-end: var(--tooltip-gap, 0.5rem);
221
654
  }
222
655
 
223
656
  .tooltip-container[data-placement="left"] .tooltip[data-visible="true"] {
@@ -230,7 +663,7 @@
230
663
  inset-inline-start: 100%;
231
664
  inset-block-start: 50%;
232
665
  transform: translateY(-50%) translateX(-4px) scale(0.95);
233
- margin-inline-start: var(--tooltip-gap);
666
+ margin-inline-start: var(--tooltip-gap, 0.5rem);
234
667
  }
235
668
 
236
669
  .tooltip-container[data-placement="right"] .tooltip[data-visible="true"] {
@@ -239,24 +672,101 @@
239
672
  transform: translateY(-50%) translateX(0) scale(1);
240
673
  }
241
674
 
675
+ /* Fixed strategy: the bubble is placed against the viewport from the
676
+ trigger's rect, so a scrolling ancestor cannot clip it. Insets and
677
+ margins are reset; the transforms above still do the centring. */
678
+ .tooltip-container[data-strategy="fixed"] .tooltip {
679
+ position: fixed;
680
+ inset: auto;
681
+ margin: 0;
682
+ }
683
+
684
+ /* Closed and faded out: parked in the corner of the viewport, so a bubble
685
+ that would reach past the edge of a narrow screen does not widen the
686
+ page while nobody can see it. The selector outweighs the placement
687
+ rules, the fixed strategy and the narrow-screen rules below. */
688
+ .tooltip-container[data-strategy] .tooltip[data-parked="true"] {
689
+ position: fixed !important;
690
+ inset-block: 0 auto !important;
691
+ inset-inline: 0 auto !important;
692
+ margin: 0 !important;
693
+ }
694
+
695
+ /* Open, the bubble can be pointed at, so the pointer can move onto it and
696
+ its text can be selected (WCAG 1.4.13). The strip behind it covers the
697
+ gap to the trigger: the pointer never leaves the tooltip on the way
698
+ across. Closed or fading, neither takes the pointer. */
699
+ .tooltip[data-visible="true"] {
700
+ pointer-events: auto;
701
+ }
702
+
703
+ .tooltip::after {
704
+ content: "";
705
+ position: absolute;
706
+ pointer-events: none;
707
+ }
708
+
709
+ .tooltip[data-visible="true"]::after {
710
+ pointer-events: auto;
711
+ }
712
+
713
+ .tooltip-container[data-placement="top"] .tooltip::after {
714
+ inset-inline: 0;
715
+ inset-block-start: 100%;
716
+ height: var(--tooltip-gap, 0.5rem);
717
+ }
718
+
719
+ .tooltip-container[data-placement="bottom"] .tooltip::after {
720
+ inset-inline: 0;
721
+ inset-block-end: 100%;
722
+ height: var(--tooltip-gap, 0.5rem);
723
+ }
724
+
725
+ .tooltip-container[data-placement="left"] .tooltip::after {
726
+ inset-block: 0;
727
+ inset-inline-start: 100%;
728
+ width: var(--tooltip-gap, 0.5rem);
729
+ }
730
+
731
+ .tooltip-container[data-placement="right"] .tooltip::after {
732
+ inset-block: 0;
733
+ inset-inline-end: 100%;
734
+ width: var(--tooltip-gap, 0.5rem);
735
+ }
736
+
737
+ /* The fixed strategy places the bubble by `left` and `top`, so in a
738
+ right-to-left page it is still on the side it is named after: the strip
739
+ is on the physical side, not the logical one. */
740
+ .tooltip-container[data-strategy="fixed"][data-placement="left"] .tooltip::after {
741
+ right: auto;
742
+ left: 100%;
743
+ }
744
+
745
+ .tooltip-container[data-strategy="fixed"][data-placement="right"] .tooltip::after {
746
+ right: 100%;
747
+ left: auto;
748
+ }
749
+
242
750
  .tooltip::before {
243
751
  content: "";
244
752
  position: absolute;
245
- width: calc(var(--tooltip-arrow-size) * 2);
246
- height: calc(var(--tooltip-arrow-size) * 2);
753
+ width: calc(var(--tooltip-arrow-size, 4px) * 2);
754
+ height: calc(var(--tooltip-arrow-size, 4px) * 2);
247
755
  background-color: var(--color-tooltip-bg);
756
+ /* Set when the bubble was moved to stay on screen: the arrow moves back. */
757
+ translate: var(--tooltip-arrow-shift, 0px 0px);
248
758
  }
249
759
 
250
760
  .tooltip-container[data-placement="top"] .tooltip::before {
251
761
  inset-block-start: 100%;
252
- inset-inline-start: 50%;
762
+ left: 50%;
253
763
  transform: translateX(-50%);
254
764
  clip-path: polygon(50% 100%, 0 0, 100% 0);
255
765
  }
256
766
 
257
767
  .tooltip-container[data-placement="bottom"] .tooltip::before {
258
768
  inset-block-end: 100%;
259
- inset-inline-start: 50%;
769
+ left: 50%;
260
770
  transform: translateX(-50%);
261
771
  clip-path: polygon(0 100%, 50% 0, 100% 100%);
262
772
  }
@@ -275,20 +785,44 @@
275
785
  clip-path: polygon(0 50%, 100% 0, 100% 100%);
276
786
  }
277
787
 
278
- .tooltip-container[data-delay] .tooltip {
279
- transition-delay: calc(var(--tooltip-delay, 0) * 1ms);
788
+ /* In a right-to-left page `left` is the start side, which is the right:
789
+ the bubble is on the other side of the trigger, and so is the arrow,
790
+ which has to point the other way too. */
791
+ .tooltip-container[data-mirrored="true"][data-placement="left"] .tooltip::before {
792
+ clip-path: polygon(0 50%, 100% 0, 100% 100%);
793
+ }
794
+
795
+ .tooltip-container[data-mirrored="true"][data-placement="right"] .tooltip::before {
796
+ clip-path: polygon(0 0, 100% 50%, 0 100%);
797
+ }
798
+
799
+ /* As for the strip above: the fixed strategy is not mirrored, so its
800
+ arrow is on the physical side that faces the trigger. */
801
+ .tooltip-container[data-strategy="fixed"][data-placement="left"] .tooltip::before {
802
+ right: auto;
803
+ left: 100%;
804
+ }
805
+
806
+ .tooltip-container[data-strategy="fixed"][data-placement="right"] .tooltip::before {
807
+ right: 100%;
808
+ left: auto;
280
809
  }
281
810
 
282
811
  @media (max-width: 640px) {
283
812
  .tooltip {
284
813
  --tooltip-max-width: calc(100vw - 2rem);
285
- inset-inline-start: 50% !important;
286
- inset-inline-end: auto !important;
814
+ }
815
+
816
+ /* Not the fixed strategy: its bubble is placed by inline left and top,
817
+ which an !important inset would override. */
818
+ .tooltip-container:not([data-strategy="fixed"]) .tooltip {
819
+ left: 50% !important;
820
+ right: auto !important;
287
821
  transform: translateX(-50%) scale(0.95) !important;
288
- margin: var(--tooltip-gap) 0 !important;
822
+ margin: var(--tooltip-gap, 0.5rem) 0 !important;
289
823
  }
290
824
 
291
- .tooltip[data-visible="true"] {
825
+ .tooltip-container:not([data-strategy="fixed"]) .tooltip[data-visible="true"] {
292
826
  transform: translateX(-50%) scale(1) !important;
293
827
  }
294
828
 
@@ -303,6 +837,15 @@
303
837
  }
304
838
  }
305
839
 
840
+ /* Forced colours repaint the fill as the page's own canvas, and the
841
+ bubble has no border: without an edge its text lies loose over the
842
+ content under it. An outline takes no room, so nothing moves. */
843
+ @media (forced-colors: active) {
844
+ .tooltip {
845
+ outline: 1px solid CanvasText;
846
+ }
847
+ }
848
+
306
849
  @media (prefers-reduced-motion: reduce) {
307
850
  .tooltip {
308
851
  transition: