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,8 +1,15 @@
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
+ 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";
6
13
 
7
14
  import { cn } from "../util/cn.js";
8
15
  type Props = Omit<HTMLAttributes<HTMLDivElement>, "class"> & {
@@ -25,6 +32,24 @@
25
32
  * at the rail edge and left only the arrow showing.
26
33
  */
27
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;
46
+ /**
47
+ * `material` (default): the bubble is the thick glass overlay material
48
+ * with body text and no arrow. `inverted`: the 8.1 bubble, a dark fill
49
+ * with light text (`--color-tooltip-bg` / `--color-tooltip-fg`) and an
50
+ * arrow.
51
+ */
52
+ tone?: "material" | "inverted";
28
53
  class?: string;
29
54
  children?: Snippet;
30
55
  };
@@ -32,6 +57,14 @@
32
57
  /** First matching focusable inside the slot receives `aria-describedby` (wrapper div is skipped). */
33
58
  const FOCUSABLE_SELECTOR =
34
59
  'button:not([disabled]), a[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
60
+ /**
61
+ * With nothing focusable in the slot, the first control of any kind is
62
+ * described instead: a disabled button is where a tooltip most often says
63
+ * something that matters (why it is disabled). It cannot take focus, so
64
+ * the keyboard never opens that tooltip: `aria-disabled="true"` on a
65
+ * button that stays focusable is the pattern that reaches everyone.
66
+ */
67
+ const CONTROL_SELECTOR = 'button, a[href], input, select, textarea, [role="button"], [tabindex]';
35
68
 
36
69
  let {
37
70
  content = "",
@@ -40,6 +73,8 @@
40
73
  disabled = false,
41
74
  block = false,
42
75
  fixed = false,
76
+ touchDuration = 0,
77
+ tone = "material",
43
78
  class: className = "",
44
79
  children,
45
80
  ...restProps
@@ -49,19 +84,125 @@
49
84
 
50
85
  /** Gap must match --tooltip-gap's default (0.5rem). */
51
86
  const FIXED_GAP = 8;
87
+ /** How close to the edge of the viewport the bubble may come, in px. */
88
+ const EDGE_MARGIN = 8;
89
+
90
+ /**
91
+ * The side the bubble is on when that is not `placement`: there was no
92
+ * room there, or the screen is too narrow to put anything beside the
93
+ * trigger. Decided each time the tooltip opens.
94
+ */
95
+ let flippedTo = $state<FloatingSide | null>(null);
96
+ const side = $derived(flippedTo ?? placement);
97
+ /** How far the bubble is moved along its side to stay on screen, in px. */
98
+ let shift = $state({ x: 0, y: 0 });
99
+ /** The arrow moves back by as much, so it still points at the trigger. */
100
+ let arrowShift = $state({ x: 0, y: 0 });
52
101
 
53
102
  function positionFixed(): void {
54
103
  if (!fixed || !triggerElement) return;
55
104
  const r = triggerElement.getBoundingClientRect();
105
+ // The styles centre the bubble on the cross axis only. Above and to
106
+ // the left it also has to clear the trigger by its own size, or it
107
+ // lies over it: `left` and `top` are where its near corner goes.
108
+ const width = bubbleElement?.offsetWidth ?? 0;
109
+ const height = bubbleElement?.offsetHeight ?? 0;
56
110
  const coords = {
57
- top: [r.left + r.width / 2, r.top - FIXED_GAP],
111
+ top: [r.left + r.width / 2, r.top - FIXED_GAP - height],
58
112
  bottom: [r.left + r.width / 2, r.bottom + FIXED_GAP],
59
- left: [r.left - FIXED_GAP, r.top + r.height / 2],
113
+ left: [r.left - FIXED_GAP - width, r.top + r.height / 2],
60
114
  right: [r.right + FIXED_GAP, r.top + r.height / 2],
61
- }[placement];
115
+ }[side];
62
116
  fixedStyle = `left:${coords[0]}px;top:${coords[1]}px;`;
63
117
  }
64
118
 
119
+ /**
120
+ * Picks the side and the shift that keep the bubble in the viewport. It is
121
+ * worked out from the trigger's rectangle and the bubble's size, not from
122
+ * where the bubble is drawn: that is mid-transition when this runs.
123
+ */
124
+ function fitToViewport(): void {
125
+ const trigger = triggerElement;
126
+ const bubble = bubbleElement;
127
+ if (!trigger || !bubble) return;
128
+ const width = bubble.offsetWidth;
129
+ const height = bubble.offsetHeight;
130
+ // No layout (a test DOM, a hidden subtree): nothing to fit.
131
+ if (!width || !height) return;
132
+ const root = document.documentElement;
133
+ const viewport = {
134
+ width: root.clientWidth || window.innerWidth,
135
+ height: root.clientHeight || window.innerHeight,
136
+ };
137
+ const r = trigger.getBoundingClientRect();
138
+ // Below 640px nothing fits beside a trigger: above it, or below.
139
+ const narrow =
140
+ typeof window.matchMedia === "function" &&
141
+ window.matchMedia("(max-width: 640px)").matches;
142
+ const preferred: FloatingSide =
143
+ narrow && (placement === "left" || placement === "right") ? "top" : placement;
144
+ // `left` and `right` are the start and the end side in the styles
145
+ // below, so in a right-to-left page they are the other way round on
146
+ // screen. The fixed strategy places by `left` and `top` and is not.
147
+ const mirrored = !fixed && getComputedStyle(trigger).direction === "rtl";
148
+ isMirrored = mirrored;
149
+ const onScreen = (name: FloatingSide): FloatingSide =>
150
+ mirrored && name === "left" ? "right" : mirrored && name === "right" ? "left" : name;
151
+ const next = pickSide(onScreen(preferred), r, { width, height }, viewport, {
152
+ gap: FIXED_GAP,
153
+ margin: EDGE_MARGIN,
154
+ });
155
+ // Back to the name the styles know it by.
156
+ const named = onScreen(next);
157
+ flippedTo = named === placement ? null : named;
158
+
159
+ const centreX = r.left + r.width / 2;
160
+ const centreY = r.top + r.height / 2;
161
+ const [left, top] = {
162
+ top: [centreX - width / 2, r.top - FIXED_GAP - height],
163
+ bottom: [centreX - width / 2, r.bottom + FIXED_GAP],
164
+ left: [r.left - FIXED_GAP - width, centreY - height / 2],
165
+ right: [r.right + FIXED_GAP, centreY - height / 2],
166
+ }[next];
167
+ const move = shiftIntoViewport(
168
+ { left, top, right: left + width, bottom: top + height },
169
+ viewport,
170
+ EDGE_MARGIN,
171
+ );
172
+ // Rounded, so a sub-pixel difference does not blur the text.
173
+ const x = Math.round(move.x);
174
+ const y = Math.round(move.y);
175
+ if (x !== shift.x || y !== shift.y) shift = { x, y };
176
+ // The arrow stays on the bubble: 12px clear of its corners.
177
+ const reach = (extent: number) => Math.max(0, extent / 2 - 12);
178
+ const vertical = next === "top" || next === "bottom";
179
+ const arrowX = vertical ? -Math.max(-reach(width), Math.min(reach(width), x)) : 0;
180
+ const arrowY = vertical ? 0 : -Math.max(-reach(height), Math.min(reach(height), y));
181
+ if (arrowX !== arrowShift.x || arrowY !== arrowShift.y) {
182
+ arrowShift = { x: arrowX, y: arrowY };
183
+ }
184
+ }
185
+
186
+ $effect(() => {
187
+ if (!isVisible) return;
188
+ untrack(fitToViewport);
189
+ const onMove = () => fitToViewport();
190
+ window.addEventListener("scroll", onMove, true);
191
+ window.addEventListener("resize", onMove);
192
+ return () => {
193
+ window.removeEventListener("scroll", onMove, true);
194
+ window.removeEventListener("resize", onMove);
195
+ };
196
+ });
197
+
198
+ const bubbleStyle = $derived(
199
+ (fixed ? fixedStyle : "") +
200
+ (shift.x || shift.y ? `translate:${shift.x}px ${shift.y}px;` : "") +
201
+ (arrowShift.x || arrowShift.y
202
+ ? `--tooltip-arrow-shift:${arrowShift.x}px ${arrowShift.y}px;`
203
+ : ""),
204
+ );
205
+
65
206
  $effect(() => {
66
207
  if (!fixed || !isVisible) return;
67
208
  positionFixed();
@@ -78,10 +219,85 @@
78
219
  const triggerId = generateId("tooltip-trigger");
79
220
  const tooltipId = generateId("tooltip");
80
221
  let isVisible = $state(false);
222
+ let containerElement: HTMLElement | null = $state(null);
81
223
  let triggerElement: HTMLElement | null = $state(null);
224
+ let bubbleElement: HTMLElement | null = $state(null);
82
225
  let showDelayTimeout: ReturnType<typeof setTimeout> | null = null;
83
226
  let hideBlurTimeout: ReturnType<typeof setTimeout> | null = null;
84
227
 
228
+ /**
229
+ * A hidden bubble still takes part in layout, and one that reaches past
230
+ * the right edge of a 320px screen makes the whole page scroll sideways.
231
+ * While it is closed it is parked in the corner of the viewport, where it
232
+ * widens nothing; it is parked once it has faded out, not before.
233
+ */
234
+ let parked = $state(true);
235
+ let parkTimeout: ReturnType<typeof setTimeout> | null = null;
236
+ /** Longer than the 200ms fade. */
237
+ const PARK_AFTER = 250;
238
+
239
+ /** The pointer that last touched the trigger: "mouse", "touch" or "pen". */
240
+ let lastPointerType = "";
241
+ /** When a finger last went down on, or came up from, the trigger. */
242
+ let touchedAt = 0;
243
+ /** Open because of a tap: closes on a tap elsewhere, on scroll, and after `touchDuration`. */
244
+ let openedByTouch = $state(false);
245
+ let touchHideTimeout: ReturnType<typeof setTimeout> | null = null;
246
+ /** A tap also focuses the trigger; for this long that focus is the tap's, not new input. */
247
+ const TOUCH_ECHO = 700;
248
+ /** True while a mouse is over the trigger, the bubble or the gap between them. */
249
+ let hovering = false;
250
+ /** The finger that is down on the trigger, and where it went down. */
251
+ let touchStart: { id: number; x: number; y: number } | null = null;
252
+ /** Further than this, in px, and it was a drag, not a tap. */
253
+ const TAP_SLOP = 10;
254
+
255
+ function clearTouchHide(): void {
256
+ if (touchHideTimeout !== null) {
257
+ clearTimeout(touchHideTimeout);
258
+ touchHideTimeout = null;
259
+ }
260
+ }
261
+
262
+ /** True in a right-to-left page, where the start side is the right: the side arrows turn round. */
263
+ let isMirrored = $state(false);
264
+
265
+ function show(): void {
266
+ stopCrossing();
267
+ // One at a time: the tooltip that was open closes.
268
+ claimOpenTooltip(hide);
269
+ if (parkTimeout !== null) {
270
+ clearTimeout(parkTimeout);
271
+ parkTimeout = null;
272
+ }
273
+ // Placed before it is let out of the corner: laid out where the styles
274
+ // alone put it, a bubble near the edge would widen the page for a
275
+ // moment, and in a right-to-left page that moves what is under the finger.
276
+ if (!isVisible) fitToViewport();
277
+ parked = false;
278
+ isVisible = true;
279
+ }
280
+
281
+ function hide(): void {
282
+ clearTouchHide();
283
+ stopCrossing();
284
+ openedByTouch = false;
285
+ releaseOpenTooltip(hide);
286
+ if (!isVisible) return;
287
+ isVisible = false;
288
+ if (parkTimeout !== null) clearTimeout(parkTimeout);
289
+ parkTimeout = setTimeout(() => {
290
+ parkTimeout = null;
291
+ parked = true;
292
+ flippedTo = null;
293
+ shift = { x: 0, y: 0 };
294
+ arrowShift = { x: 0, y: 0 };
295
+ }, PARK_AFTER);
296
+ }
297
+
298
+ const isTouch = (type: string) => type === "touch" || type === "pen";
299
+ const afterTouch = () => isTouch(lastPointerType) && Date.now() - touchedAt < TOUCH_ECHO;
300
+
85
301
  function clearShowDelay(): void {
86
302
  if (showDelayTimeout !== null) {
87
303
  clearTimeout(showDelayTimeout);
@@ -97,7 +313,9 @@
97
313
  }
98
314
 
99
315
  function findDescribedTarget(root: HTMLElement): HTMLElement | null {
100
- const found = root.querySelector<HTMLElement>(FOCUSABLE_SELECTOR);
316
+ const found =
317
+ root.querySelector<HTMLElement>(FOCUSABLE_SELECTOR) ??
318
+ root.querySelector<HTMLElement>(CONTROL_SELECTOR);
101
319
  if (found) {
102
320
  return found;
103
321
  }
@@ -135,19 +353,45 @@
135
353
  if (!content || disabled) {
136
354
  clearShowDelay();
137
355
  clearHideBlurTimeout();
356
+ // Switched off while open: closed, and no longer listening for a tap elsewhere.
357
+ untrack(hide);
138
358
  }
139
359
  });
140
360
 
141
361
  onDestroy(() => {
362
+ stopCrossing();
363
+ releaseOpenTooltip(hide);
142
364
  clearShowDelay();
143
365
  clearHideBlurTimeout();
366
+ clearTouchHide();
367
+ if (parkTimeout !== null) clearTimeout(parkTimeout);
368
+ });
369
+
370
+ // A tooltip opened by a tap has no hover to end and, where a tap does not
371
+ // focus a button (Safari), no blur either: a tap anywhere else closes it,
372
+ // and so does scrolling, which would otherwise carry it over other content.
373
+ $effect(() => {
374
+ if (!isVisible || !openedByTouch) return;
375
+ const onPointerDown = (event: PointerEvent) => {
376
+ if (!containerElement?.contains(event.target as Node | null)) hide();
377
+ };
378
+ const onScroll = () => hide();
379
+ document.addEventListener("pointerdown", onPointerDown, true);
380
+ window.addEventListener("scroll", onScroll, true);
381
+ return () => {
382
+ document.removeEventListener("pointerdown", onPointerDown, true);
383
+ window.removeEventListener("scroll", onScroll, true);
384
+ };
144
385
  });
145
386
 
146
387
  function handleKeydown(event: KeyboardEvent) {
147
388
  if (event.key === "Escape" && isVisible) {
389
+ // Capture phase, and marked as handled: the first Escape dismisses
390
+ // the tooltip only, not the modal or menu it sits in (WCAG 1.4.13).
391
+ event.preventDefault();
148
392
  clearShowDelay();
149
393
  clearHideBlurTimeout();
150
- isVisible = false;
394
+ hide();
151
395
  // Keep focus on the described control; never steal it when the tooltip was hover-only.
152
396
  const target = triggerElement ? findDescribedTarget(triggerElement) : null;
153
397
  if (target && triggerElement?.contains(document.activeElement)) {
@@ -159,8 +403,10 @@
159
403
  function handleFocus() {
160
404
  clearShowDelay();
161
405
  clearHideBlurTimeout();
406
+ // The focus a tap gives the trigger: the tap has already opened or closed it.
407
+ if (afterTouch()) return;
162
408
  if (!disabled && content) {
163
- isVisible = true;
409
+ show();
164
410
  }
165
411
  }
166
412
 
@@ -169,33 +415,165 @@
169
415
  // Defer hide so brief focus moves within the trigger subtree do not flash the tooltip off.
170
416
  hideBlurTimeout = setTimeout(() => {
171
417
  hideBlurTimeout = null;
172
- isVisible = false;
418
+ // A press on the bubble itself (to select its text) takes focus
419
+ // off the trigger. The pointer is still on the tooltip: it stays,
420
+ // and leaving it, a press elsewhere or Escape closes it.
421
+ if (hovering || containerElement?.contains(document.activeElement)) return;
422
+ hide();
173
423
  }, 100);
174
424
  }
175
425
 
176
426
  function handleMouseEnter() {
427
+ // The mouse events a browser makes up after a tap. Opening here would
428
+ // undo a tap that closed the tooltip, and on iOS a hover handler that
429
+ // shows something makes the first tap a hover and drops its click.
430
+ if (isTouch(lastPointerType)) return;
431
+ hovering = true;
432
+ stopCrossing();
177
433
  clearShowDelay();
178
434
  clearHideBlurTimeout();
179
435
  if (!disabled && content) {
180
436
  if (delay > 0) {
181
437
  showDelayTimeout = setTimeout(() => {
182
438
  showDelayTimeout = null;
183
- isVisible = true;
439
+ show();
184
440
  }, delay);
185
441
  } else {
186
- isVisible = true;
442
+ show();
187
443
  }
188
444
  }
189
445
  }
190
446
 
191
- function handleMouseLeave() {
447
+ /** Stops watching a pointer that is on its way from the trigger to the bubble. */
448
+ let stopCrossing: () => void = () => {};
449
+ /** A pointer that has stopped short of the bubble is given this long without moving, in ms. */
450
+ const CROSSING_TIME = 300;
451
+
452
+ /**
453
+ * The pointer has left the tooltip. Straight across the gap it never
454
+ * does, but from a small trigger to the far corner of a wide bubble the
455
+ * way is diagonal and leaves through the side. While it stays in the
456
+ * triangle between where it left and the near edge of the bubble it is
457
+ * still coming, and the tooltip waits: until it arrives, strays, or stops.
458
+ */
459
+ function handleMouseLeave(event: MouseEvent) {
460
+ hovering = false;
461
+ if (isTouch(lastPointerType)) return;
192
462
  clearShowDelay();
193
463
  clearHideBlurTimeout();
194
- isVisible = false;
464
+ const box = isVisible ? bubbleElement?.getBoundingClientRect() : undefined;
465
+ // No bubble on screen (or no layout to measure): nothing to cross to.
466
+ if (!box || box.width === 0 || box.height === 0) {
467
+ hide();
468
+ return;
469
+ }
470
+ const left = { x: event.clientX, y: event.clientY };
471
+ // The edge of the bubble that faces the trigger, a little wider than it is.
472
+ const reach = 4;
473
+ const [near, far] = {
474
+ top: [
475
+ { x: box.left - reach, y: box.bottom },
476
+ { x: box.right + reach, y: box.bottom },
477
+ ],
478
+ bottom: [
479
+ { x: box.left - reach, y: box.top },
480
+ { x: box.right + reach, y: box.top },
481
+ ],
482
+ left: [
483
+ { x: box.right, y: box.top - reach },
484
+ { x: box.right, y: box.bottom + reach },
485
+ ],
486
+ right: [
487
+ { x: box.left, y: box.top - reach },
488
+ { x: box.left, y: box.bottom + reach },
489
+ ],
490
+ }[
491
+ // On screen: the start side is the right in a right-to-left page.
492
+ isMirrored && side === "left" ? "right" : isMirrored && side === "right" ? "left" : side
493
+ ];
494
+ stopCrossing();
495
+ let timer = setTimeout(hide, CROSSING_TIME);
496
+ const onMove = (move: MouseEvent) => {
497
+ if (insideTriangle({ x: move.clientX, y: move.clientY }, left, near, far)) {
498
+ // Still coming: the wait starts again.
499
+ clearTimeout(timer);
500
+ timer = setTimeout(hide, CROSSING_TIME);
501
+ return;
502
+ }
503
+ // Not on the way: unless it has arrived, which `mouseenter` says.
504
+ if (containerElement?.contains(move.target as Node | null)) return;
505
+ hide();
506
+ };
507
+ document.addEventListener("mousemove", onMove, true);
508
+ stopCrossing = () => {
509
+ clearTimeout(timer);
510
+ document.removeEventListener("mousemove", onMove, true);
511
+ stopCrossing = () => {};
512
+ };
513
+ }
514
+
515
+ /** Which kind of pointer is over the trigger; a mouse arriving after a finger counts again. */
516
+ function handlePointerEnter(event: PointerEvent) {
517
+ lastPointerType = event.pointerType;
518
+ }
519
+
520
+ /**
521
+ * A tap of a finger or a pen on the trigger toggles the tooltip: when the
522
+ * finger comes up where it went down, not when it goes down. A finger
523
+ * that goes down to scroll the page would otherwise open the tooltip and
524
+ * close it again as the scroll began. Nothing is prevented: the tap goes
525
+ * on to become the trigger's own click.
526
+ */
527
+ function handlePointerDown(event: PointerEvent) {
528
+ lastPointerType = event.pointerType;
529
+ if (!isTouch(event.pointerType)) return;
530
+ touchedAt = Date.now();
531
+ // On the bubble itself: neither a tap on the trigger nor one elsewhere.
532
+ if (bubbleElement?.contains(event.target as Node | null)) {
533
+ touchStart = null;
534
+ // It may take focus off the trigger; the tooltip stays, and from
535
+ // here on a tap elsewhere or scrolling is what closes it.
536
+ clearHideBlurTimeout();
537
+ hovering = true;
538
+ if (isVisible) openedByTouch = true;
539
+ return;
540
+ }
541
+ hovering = false;
542
+ touchStart = { id: event.pointerId, x: event.clientX, y: event.clientY };
543
+ }
544
+
545
+ function handlePointerUp(event: PointerEvent) {
546
+ if (!isTouch(event.pointerType)) return;
547
+ touchedAt = Date.now();
548
+ const start = touchStart;
549
+ touchStart = null;
550
+ if (!start || start.id !== event.pointerId) return;
551
+ if (Math.hypot(event.clientX - start.x, event.clientY - start.y) > TAP_SLOP) return;
552
+ clearShowDelay();
553
+ clearHideBlurTimeout();
554
+ if (isVisible) {
555
+ hide();
556
+ return;
557
+ }
558
+ if (disabled || !content) return;
559
+ show();
560
+ openedByTouch = true;
561
+ clearTouchHide();
562
+ if (touchDuration > 0) {
563
+ touchHideTimeout = setTimeout(() => {
564
+ touchHideTimeout = null;
565
+ hide();
566
+ }, touchDuration);
567
+ }
568
+ }
569
+
570
+ /** The browser took the touch for itself: to scroll, most often. */
571
+ function handlePointerCancel() {
572
+ touchStart = null;
195
573
  }
196
574
  </script>
197
575
 
198
- <svelte:window onkeydown={handleKeydown} />
576
+ <svelte:window onkeydowncapture={handleKeydown} />
199
577
 
200
578
  <div
201
579
  class={cn(
@@ -203,11 +581,17 @@
203
581
  block ? "block w-full" : "inline-block",
204
582
  className,
205
583
  )}
206
- data-placement={placement}
584
+ bind:this={containerElement}
585
+ data-placement={side}
207
586
  data-strategy={fixed ? "fixed" : "absolute"}
587
+ data-mirrored={isMirrored ? "true" : undefined}
208
588
  data-disabled={disabled}
209
589
  onmouseenter={handleMouseEnter}
210
590
  onmouseleave={handleMouseLeave}
591
+ onpointerenter={handlePointerEnter}
592
+ onpointerdown={handlePointerDown}
593
+ onpointerup={handlePointerUp}
594
+ onpointercancel={handlePointerCancel}
211
595
  onfocusin={handleFocus}
212
596
  onfocusout={handleBlur}
213
597
  {...restProps}
@@ -218,13 +602,19 @@
218
602
 
219
603
  {#if content && !disabled}
220
604
  <div
605
+ bind:this={bubbleElement}
221
606
  id={tooltipId}
222
- 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"
607
+ class={cn(
608
+ "tooltip pointer-events-none invisible absolute z-tooltip whitespace-normal wrap-break-word rounded-control px-3 py-2 text-sm leading-5 opacity-0 transition-[opacity,visibility,transform] duration-(--duration-moderate) ease-standard",
609
+ tone === "inverted" ? "bg-tooltip-bg text-tooltip-fg" : "material-thick text-body",
610
+ )}
223
611
  role="tooltip"
224
612
  aria-hidden={!isVisible}
613
+ data-tone={tone}
225
614
  data-visible={isVisible}
226
- data-placement={placement}
227
- style={fixed ? fixedStyle : undefined}
615
+ data-parked={parked}
616
+ data-placement={side}
617
+ style={bubbleStyle || undefined}
228
618
  >
229
619
  {content}
230
620
  </div>
@@ -240,9 +630,11 @@
240
630
  max-width: var(--tooltip-max-width, min(24rem, calc(100vw - 2rem)));
241
631
  }
242
632
 
633
+ /* Centred over the trigger with `left`, not `inset-inline-start`: the
634
+ shift back by half is to the left whichever way the text runs. */
243
635
  .tooltip-container[data-placement="top"] .tooltip {
244
636
  inset-block-end: 100%;
245
- inset-inline-start: 50%;
637
+ left: 50%;
246
638
  transform: translateX(-50%) translateY(4px) scale(0.95);
247
639
  margin-block-end: var(--tooltip-gap, 0.5rem);
248
640
  }
@@ -255,7 +647,7 @@
255
647
 
256
648
  .tooltip-container[data-placement="bottom"] .tooltip {
257
649
  inset-block-start: 100%;
258
- inset-inline-start: 50%;
650
+ left: 50%;
259
651
  transform: translateX(-50%) translateY(-4px) scale(0.95);
260
652
  margin-block-start: var(--tooltip-gap, 0.5rem);
261
653
  }
@@ -301,24 +693,99 @@
301
693
  margin: 0;
302
694
  }
303
695
 
696
+ /* Closed and faded out: parked in the corner of the viewport, so a bubble
697
+ that would reach past the edge of a narrow screen does not widen the
698
+ page while nobody can see it. The selector outweighs the placement
699
+ rules, the fixed strategy and the narrow-screen rules below. */
700
+ .tooltip-container[data-strategy] .tooltip[data-parked="true"] {
701
+ position: fixed !important;
702
+ inset-block: 0 auto !important;
703
+ inset-inline: 0 auto !important;
704
+ margin: 0 !important;
705
+ }
706
+
707
+ /* Open, the bubble can be pointed at, so the pointer can move onto it and
708
+ its text can be selected (WCAG 1.4.13). The strip behind it covers the
709
+ gap to the trigger: the pointer never leaves the tooltip on the way
710
+ across. Closed or fading, neither takes the pointer. */
711
+ .tooltip[data-visible="true"] {
712
+ pointer-events: auto;
713
+ }
714
+
715
+ .tooltip::after {
716
+ content: "";
717
+ position: absolute;
718
+ pointer-events: none;
719
+ }
720
+
721
+ .tooltip[data-visible="true"]::after {
722
+ pointer-events: auto;
723
+ }
724
+
725
+ .tooltip-container[data-placement="top"] .tooltip::after {
726
+ inset-inline: 0;
727
+ inset-block-start: 100%;
728
+ height: var(--tooltip-gap, 0.5rem);
729
+ }
730
+
731
+ .tooltip-container[data-placement="bottom"] .tooltip::after {
732
+ inset-inline: 0;
733
+ inset-block-end: 100%;
734
+ height: var(--tooltip-gap, 0.5rem);
735
+ }
736
+
737
+ .tooltip-container[data-placement="left"] .tooltip::after {
738
+ inset-block: 0;
739
+ inset-inline-start: 100%;
740
+ width: var(--tooltip-gap, 0.5rem);
741
+ }
742
+
743
+ .tooltip-container[data-placement="right"] .tooltip::after {
744
+ inset-block: 0;
745
+ inset-inline-end: 100%;
746
+ width: var(--tooltip-gap, 0.5rem);
747
+ }
748
+
749
+ /* The fixed strategy places the bubble by `left` and `top`, so in a
750
+ right-to-left page it is still on the side it is named after: the strip
751
+ is on the physical side, not the logical one. */
752
+ .tooltip-container[data-strategy="fixed"][data-placement="left"] .tooltip::after {
753
+ right: auto;
754
+ left: 100%;
755
+ }
756
+
757
+ .tooltip-container[data-strategy="fixed"][data-placement="right"] .tooltip::after {
758
+ right: 100%;
759
+ left: auto;
760
+ }
761
+
762
+ /* The arrow is a second shape on a translucent fill: its blur edge would
763
+ show a seam where it meets the bubble, and HIG tooltips have none. Only
764
+ the inverted bubble, which is opaque, keeps it. */
765
+ .tooltip[data-tone="material"]::before {
766
+ display: none;
767
+ }
768
+
304
769
  .tooltip::before {
305
770
  content: "";
306
771
  position: absolute;
307
772
  width: calc(var(--tooltip-arrow-size, 4px) * 2);
308
773
  height: calc(var(--tooltip-arrow-size, 4px) * 2);
309
774
  background-color: var(--color-tooltip-bg);
775
+ /* Set when the bubble was moved to stay on screen: the arrow moves back. */
776
+ translate: var(--tooltip-arrow-shift, 0px 0px);
310
777
  }
311
778
 
312
779
  .tooltip-container[data-placement="top"] .tooltip::before {
313
780
  inset-block-start: 100%;
314
- inset-inline-start: 50%;
781
+ left: 50%;
315
782
  transform: translateX(-50%);
316
783
  clip-path: polygon(50% 100%, 0 0, 100% 0);
317
784
  }
318
785
 
319
786
  .tooltip-container[data-placement="bottom"] .tooltip::before {
320
787
  inset-block-end: 100%;
321
- inset-inline-start: 50%;
788
+ left: 50%;
322
789
  transform: translateX(-50%);
323
790
  clip-path: polygon(0 100%, 50% 0, 100% 100%);
324
791
  }
@@ -337,16 +804,44 @@
337
804
  clip-path: polygon(0 50%, 100% 0, 100% 100%);
338
805
  }
339
806
 
807
+ /* In a right-to-left page `left` is the start side, which is the right:
808
+ the bubble is on the other side of the trigger, and so is the arrow,
809
+ which has to point the other way too. */
810
+ .tooltip-container[data-mirrored="true"][data-placement="left"] .tooltip::before {
811
+ clip-path: polygon(0 50%, 100% 0, 100% 100%);
812
+ }
813
+
814
+ .tooltip-container[data-mirrored="true"][data-placement="right"] .tooltip::before {
815
+ clip-path: polygon(0 0, 100% 50%, 0 100%);
816
+ }
817
+
818
+ /* As for the strip above: the fixed strategy is not mirrored, so its
819
+ arrow is on the physical side that faces the trigger. */
820
+ .tooltip-container[data-strategy="fixed"][data-placement="left"] .tooltip::before {
821
+ right: auto;
822
+ left: 100%;
823
+ }
824
+
825
+ .tooltip-container[data-strategy="fixed"][data-placement="right"] .tooltip::before {
826
+ right: 100%;
827
+ left: auto;
828
+ }
829
+
340
830
  @media (max-width: 640px) {
341
831
  .tooltip {
342
832
  --tooltip-max-width: calc(100vw - 2rem);
343
- inset-inline-start: 50% !important;
344
- inset-inline-end: auto !important;
833
+ }
834
+
835
+ /* Not the fixed strategy: its bubble is placed by inline left and top,
836
+ which an !important inset would override. */
837
+ .tooltip-container:not([data-strategy="fixed"]) .tooltip {
838
+ left: 50% !important;
839
+ right: auto !important;
345
840
  transform: translateX(-50%) scale(0.95) !important;
346
841
  margin: var(--tooltip-gap, 0.5rem) 0 !important;
347
842
  }
348
843
 
349
- .tooltip[data-visible="true"] {
844
+ .tooltip-container:not([data-strategy="fixed"]) .tooltip[data-visible="true"] {
350
845
  transform: translateX(-50%) scale(1) !important;
351
846
  }
352
847
 
@@ -361,6 +856,15 @@
361
856
  }
362
857
  }
363
858
 
859
+ /* Forced colours repaint the fill as the page's own canvas, and the
860
+ bubble has no border: without an edge its text lies loose over the
861
+ content under it. An outline takes no room, so nothing moves. */
862
+ @media (forced-colors: active) {
863
+ .tooltip {
864
+ outline: 1px solid CanvasText;
865
+ }
866
+ }
867
+
364
868
  @media (prefers-reduced-motion: reduce) {
365
869
  .tooltip {
366
870
  transition: