@celestia-island/hikari 0.4.4 → 0.30.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 (385) hide show
  1. package/README.md +1 -1
  2. package/package.json +22 -7
  3. package/src/DemoApp.tsx +491 -0
  4. package/src/animation/registerAnimations.test.ts +94 -0
  5. package/src/animation/registerAnimations.ts +103 -0
  6. package/src/components/HkAboutModal.scss +95 -0
  7. package/src/components/HkAboutModal.tsx +114 -0
  8. package/src/components/HkAdaptiveDialog.scss +22 -0
  9. package/src/components/HkAdaptiveDialog.test.tsx +271 -0
  10. package/src/components/HkAdaptiveDialog.tsx +167 -0
  11. package/src/components/HkAdminHeader.test.tsx +185 -0
  12. package/src/components/HkAdminHeader.tsx +308 -0
  13. package/src/components/HkAdminShell.test.tsx +237 -0
  14. package/src/components/HkAdminShell.tsx +129 -0
  15. package/src/components/HkAdminTablePage.tsx +70 -0
  16. package/src/components/HkAdminWidgets.test.ts +89 -0
  17. package/src/components/HkAlert.tsx +1 -1
  18. package/src/components/HkAltSignIn.scss +55 -0
  19. package/src/components/HkAltSignIn.test.tsx +88 -0
  20. package/src/components/HkAltSignIn.tsx +92 -0
  21. package/src/components/HkAttachmentModal.scss +49 -0
  22. package/src/components/HkAttachmentModal.tsx +318 -0
  23. package/src/components/HkAuthCard.tsx +50 -0
  24. package/src/components/HkAuthSubmitButton.scss +12 -0
  25. package/src/components/HkAuthSubmitButton.tsx +94 -0
  26. package/src/components/HkAvatar.scss +93 -0
  27. package/src/components/HkAvatar.tsx +2 -2
  28. package/src/components/HkBlockingToast.scss +123 -0
  29. package/src/components/HkBlockingToast.test.tsx +221 -0
  30. package/src/components/HkBlockingToast.tsx +182 -0
  31. package/src/components/HkBreadcrumb.scss +624 -0
  32. package/src/components/HkBreadcrumb.tsx +2 -2
  33. package/src/components/HkButton.scss +16 -2
  34. package/src/components/HkCaptchaModal.scss +14 -0
  35. package/src/components/HkCaptchaModal.tsx +62 -0
  36. package/src/components/HkCaptchaWidget.scss +30 -0
  37. package/src/components/HkCaptchaWidget.tsx +181 -0
  38. package/src/components/HkCard.scss +1 -1
  39. package/src/components/HkChatTypes.ts +65 -0
  40. package/src/components/HkCheckbox.scss +4 -9
  41. package/src/components/HkCheckbox.tsx +18 -6
  42. package/src/components/HkColorPicker.scss +120 -3
  43. package/src/components/HkColorPicker.tsx +130 -19
  44. package/src/components/HkColorPickerTouch.test.tsx +145 -0
  45. package/src/components/HkColorSchemeDialog.scss +52 -0
  46. package/src/components/HkColorSchemeDialog.test.ts +201 -0
  47. package/src/components/HkColorSchemeDialog.tsx +96 -0
  48. package/src/components/HkColorSchemeEditor.test.ts +270 -0
  49. package/src/components/HkColorSchemeEditor.tsx +410 -0
  50. package/src/components/HkConfirmDialog.scss +43 -3
  51. package/src/components/HkConfirmDialog.tsx +7 -4
  52. package/src/components/HkConnectionInfo.ts +80 -0
  53. package/src/components/HkConnectionStatus.tsx +158 -0
  54. package/src/components/HkCookieConsent.scss +22 -0
  55. package/src/components/HkCookieConsent.test.tsx +70 -0
  56. package/src/components/HkCookieConsent.tsx +56 -0
  57. package/src/components/HkCountdownDigit.tsx +105 -0
  58. package/src/components/HkDatePicker.scss +361 -0
  59. package/src/components/HkDatePicker.test.ts +459 -0
  60. package/src/components/HkDatePicker.tsx +568 -0
  61. package/src/components/HkDateTimePicker.scss +65 -37
  62. package/src/components/HkDateTimePicker.test.ts +344 -0
  63. package/src/components/HkDateTimePicker.tsx +249 -82
  64. package/src/components/HkDraggableGrid.scss +4 -1
  65. package/src/components/HkDraggableList.scss +4 -1
  66. package/src/components/HkDrawer.scss +27 -13
  67. package/src/components/HkDrawer.test.tsx +73 -0
  68. package/src/components/HkDrawer.tsx +157 -10
  69. package/src/components/HkErrorBoundary.scss +5 -2
  70. package/src/components/HkErrorBoundary.tsx +7 -4
  71. package/src/components/HkExpansionPanel.scss +143 -0
  72. package/src/components/HkExpansionPanel.test.tsx +109 -0
  73. package/src/components/HkExpansionPanel.tsx +101 -0
  74. package/src/components/HkFab.scss +162 -0
  75. package/src/components/HkFab.test.tsx +217 -0
  76. package/src/components/HkFab.tsx +214 -0
  77. package/src/components/HkHoverRevealAction.scss +6 -0
  78. package/src/components/HkHoverRevealAction.test.ts +68 -0
  79. package/src/components/HkHoverRevealAction.tsx +84 -6
  80. package/src/components/HkIcon.scss +123 -0
  81. package/src/components/HkIcon.tsx +7 -23
  82. package/src/components/HkIconButton.scss +170 -0
  83. package/src/components/HkIconButton.test.tsx +75 -0
  84. package/src/components/HkIconButton.tsx +4 -3
  85. package/src/components/HkIconButtonVars.scss +180 -0
  86. package/src/components/HkInput.affix.test.ts +253 -0
  87. package/src/components/HkInput.enter.test.ts +88 -0
  88. package/src/components/HkInput.scss +127 -8
  89. package/src/components/HkInput.tsx +245 -13
  90. package/src/components/HkKeywordSearchModal.scss +13 -0
  91. package/src/components/HkKeywordSearchModal.tsx +46 -9
  92. package/src/components/HkLabel.scss +34 -0
  93. package/src/components/HkLabel.test.tsx +61 -0
  94. package/src/components/HkLabel.tsx +45 -0
  95. package/src/components/HkListTransition.scss +26 -0
  96. package/src/components/HkListTransition.tsx +57 -0
  97. package/src/components/HkLocalePicker.scss +37 -0
  98. package/src/components/HkLocalePicker.tsx +133 -0
  99. package/src/components/HkLocalePickerPopup.tsx +35 -19
  100. package/src/components/HkLocalizedInput.scss +277 -0
  101. package/src/components/HkLocalizedInput.test.tsx +693 -0
  102. package/src/components/HkLocalizedInput.tsx +495 -0
  103. package/src/components/HkLogWindow.scss +121 -0
  104. package/src/components/HkLogWindow.tsx +195 -0
  105. package/src/components/HkLogo.scss +1 -1
  106. package/src/components/HkLogo.tsx +1 -1
  107. package/src/components/HkMarkdownRenderer.scss +22 -2
  108. package/src/components/HkMarkdownRenderer.tsx +33 -3
  109. package/src/components/HkMediaControlBar.tsx +20 -13
  110. package/src/components/HkMenu.scss +200 -0
  111. package/src/components/HkMenu.test.ts +779 -0
  112. package/src/components/HkMenu.tsx +770 -0
  113. package/src/components/HkMenuActionItem.scss +58 -0
  114. package/src/components/HkMenuActionItem.tsx +41 -0
  115. package/src/components/HkMenuIdentityItem.scss +70 -0
  116. package/src/components/HkMenuIdentityItem.tsx +42 -0
  117. package/src/components/HkMenuPanel.scss +20 -0
  118. package/src/components/HkMenuPanel.test.tsx +118 -0
  119. package/src/components/HkMenuPanel.tsx +32 -0
  120. package/src/components/HkMinimap.tsx +16 -8
  121. package/src/components/HkModal.autofollow.test.ts +44 -0
  122. package/src/components/HkModal.scss +170 -53
  123. package/src/components/HkModal.sheetgap.test.ts +82 -0
  124. package/src/components/HkModal.test.tsx +334 -0
  125. package/src/components/HkModal.tsx +314 -68
  126. package/src/components/HkModalBreadcrumb.scss +4 -1
  127. package/src/components/HkModalBreadcrumb.test.tsx +111 -0
  128. package/src/components/HkModalBreadcrumb.tsx +34 -11
  129. package/src/components/HkModelCatalog.ts +98 -0
  130. package/src/components/HkModelTag.scss +130 -0
  131. package/src/components/HkModelTag.test.tsx +111 -0
  132. package/src/components/HkModelTag.tsx +190 -0
  133. package/src/components/HkNavItem.scss +13 -2
  134. package/src/components/HkNavItem.tsx +2 -1
  135. package/src/components/HkNavSidebar.tsx +99 -0
  136. package/src/components/HkNumberInput.scss +12 -0
  137. package/src/components/HkNumberInput.tsx +15 -8
  138. package/src/components/HkPageHeader.scss +79 -0
  139. package/src/components/HkPageHeader.test.ts +67 -0
  140. package/src/components/HkPageHeader.tsx +51 -0
  141. package/src/components/HkPasswordInput.scss +29 -17
  142. package/src/components/HkPasswordInput.test.ts +281 -0
  143. package/src/components/HkPasswordInput.tsx +337 -59
  144. package/src/components/HkPickerPane.scss +158 -0
  145. package/src/components/HkPlaceholderMarquee.scss +109 -0
  146. package/src/components/HkPlaceholderMarquee.test.ts +305 -0
  147. package/src/components/HkPlaceholderMarquee.tsx +170 -0
  148. package/src/components/HkPopover.scss +134 -10
  149. package/src/components/HkPopover.sheetdock.test.ts +34 -0
  150. package/src/components/HkPopover.surfacevars.test.ts +20 -0
  151. package/src/components/HkPopover.titleSuppress.test.tsx +265 -0
  152. package/src/components/HkPopover.tsx +225 -50
  153. package/src/components/HkPopoverSheet.test.tsx +130 -0
  154. package/src/components/HkPopupSelect.scss +30 -10
  155. package/src/components/HkPopupSelect.tsx +21 -1
  156. package/src/components/HkProgressDialog.scss +31 -0
  157. package/src/components/HkProgressDialog.tsx +37 -19
  158. package/src/components/HkProtocolModal.scss +26 -0
  159. package/src/components/HkProtocolModal.tsx +112 -0
  160. package/src/components/HkRadio.scss +1 -16
  161. package/src/components/HkRadio.tsx +5 -2
  162. package/src/components/HkRichInput.scss +162 -0
  163. package/src/components/HkRichInput.tsx +335 -0
  164. package/src/components/HkRollingNumber.test.ts +90 -0
  165. package/src/components/HkRollingNumber.tsx +39 -1
  166. package/src/components/HkScrollContainer.scss +71 -54
  167. package/src/components/HkScrollContainer.test.ts +160 -0
  168. package/src/components/HkScrollContainer.tsx +187 -160
  169. package/src/components/HkSearchInput.tsx +3 -3
  170. package/src/components/HkSecretRevealModal.scss +61 -0
  171. package/src/components/HkSecretRevealModal.tsx +91 -0
  172. package/src/components/HkSelect.scss +263 -12
  173. package/src/components/HkSelect.sheetdock.test.ts +30 -0
  174. package/src/components/HkSelect.test.tsx +88 -0
  175. package/src/components/HkSelect.tsx +59 -74
  176. package/src/components/HkSelectPanel.test.tsx +666 -0
  177. package/src/components/HkSelectPanel.tsx +595 -0
  178. package/src/components/HkSelectSheet.test.tsx +132 -0
  179. package/src/components/HkSelectionGrid.scss +1 -1
  180. package/src/components/HkSelectionGrid.tsx +2 -1
  181. package/src/components/HkSelectionWaterfall.scss +1 -1
  182. package/src/components/HkShareBar.scss +40 -0
  183. package/src/components/HkShareBar.tsx +50 -0
  184. package/src/components/HkSidebar.scss +15 -10
  185. package/src/components/HkSidebar.tsx +26 -2
  186. package/src/components/HkSignInCard.test.ts +154 -0
  187. package/src/components/HkSignInCard.tsx +155 -0
  188. package/src/components/HkSlider.scss +99 -0
  189. package/src/components/HkSlider.test.tsx +155 -0
  190. package/src/components/HkSlider.tsx +173 -0
  191. package/src/components/HkSpinner.scss +36 -1
  192. package/src/components/HkSpinner.test.tsx +63 -0
  193. package/src/components/HkSpinner.tsx +16 -1
  194. package/src/components/HkSplash.scss +1 -1
  195. package/src/components/HkSplash.tsx +3 -1
  196. package/src/components/HkStatCard.scss +73 -0
  197. package/src/components/HkStatCard.tsx +46 -0
  198. package/src/components/HkStatusBar.test.tsx +178 -0
  199. package/src/components/HkStatusBar.tsx +379 -0
  200. package/src/components/HkStatusPill.scss +73 -0
  201. package/src/components/HkStatusPill.tsx +41 -0
  202. package/src/components/HkStepFlow.scss +75 -0
  203. package/src/components/HkStepFlow.test.tsx +246 -0
  204. package/src/components/HkStepFlow.tsx +115 -0
  205. package/src/components/HkSwitch.scss +5 -17
  206. package/src/components/HkSwitch.tsx +6 -3
  207. package/src/components/HkTable.scss +20 -1
  208. package/src/components/HkTable.tsx +28 -3
  209. package/src/components/HkTabs.scss +222 -48
  210. package/src/components/HkTabs.test.ts +517 -0
  211. package/src/components/HkTabs.tsx +567 -56
  212. package/src/components/HkTag.tsx +2 -1
  213. package/src/components/HkTextarea.tsx +5 -0
  214. package/src/components/HkThemeToggle.scss +232 -0
  215. package/src/components/HkThemeToggle.test.ts +231 -0
  216. package/src/components/HkThemeToggle.tsx +289 -0
  217. package/src/components/HkTimeline.scss +134 -1
  218. package/src/components/HkTimeline.test.tsx +319 -0
  219. package/src/components/HkTimeline.tsx +303 -54
  220. package/src/components/HkToast.scss +34 -5
  221. package/src/components/HkToast.test.tsx +109 -0
  222. package/src/components/HkToast.tsx +57 -8
  223. package/src/components/HkTokenUsageBadge.scss +53 -0
  224. package/src/components/HkTokenUsageBadge.test.tsx +59 -0
  225. package/src/components/HkTokenUsageBadge.tsx +54 -0
  226. package/src/components/HkTokenUsagePanel.scss +95 -0
  227. package/src/components/HkTokenUsagePanel.tsx +140 -0
  228. package/src/components/HkToolBlock.scss +428 -0
  229. package/src/components/HkToolBlock.tsx +639 -0
  230. package/src/components/HkTooltip.scss +14 -2
  231. package/src/components/HkTooltip.tsx +25 -2
  232. package/src/components/HkTrendChart.tsx +27 -16
  233. package/src/components/HkVoiceInputPopup.scss +77 -0
  234. package/src/components/HkVoiceInputPopup.tsx +97 -0
  235. package/src/components/HkZoomToolbar.scss +4 -1
  236. package/src/components/HkZoomToolbar.tsx +7 -4
  237. package/src/components/_hikari-vars.scss +6 -0
  238. package/src/composables/highlightLanguages.ts +109 -0
  239. package/src/composables/iconRegistry.ts +127 -0
  240. package/src/composables/messaging/browserTransport.ts +96 -0
  241. package/src/composables/messaging/index.ts +204 -0
  242. package/src/composables/messaging/messaging.test.ts +256 -0
  243. package/src/composables/messaging/toastTransport.ts +26 -0
  244. package/src/composables/messaging/types.ts +115 -0
  245. package/src/composables/useActionBar.ts +66 -0
  246. package/src/composables/useAttachments.ts +154 -0
  247. package/src/composables/useConnectionProbe.ts +142 -0
  248. package/src/composables/useDeferredTransition.ts +16 -0
  249. package/src/composables/useEngineHealth.ts +69 -0
  250. package/src/composables/useHighlight.ts +124 -0
  251. package/src/composables/useMeasuredHighlight.ts +150 -0
  252. package/src/composables/useOverlayScrollbar.test.ts +221 -0
  253. package/src/composables/useOverlayScrollbar.ts +296 -0
  254. package/src/composables/usePageTitle.ts +37 -0
  255. package/src/composables/useReducedMotion.ts +34 -0
  256. package/src/composables/useResourceListModal.tsx +147 -0
  257. package/src/composables/useSafeArea.ts +137 -0
  258. package/src/composables/useSendShortcut.ts +35 -0
  259. package/src/composables/useSizeMorph.test.ts +156 -0
  260. package/src/composables/useSizeMorph.ts +149 -0
  261. package/src/composables/useSurfaceTransition.test.ts +96 -0
  262. package/src/composables/useSurfaceTransition.ts +71 -0
  263. package/src/composables/useZoomPan.ts +192 -0
  264. package/src/demo.scss +9 -1
  265. package/src/demo.ts +5 -1
  266. package/src/i18n/locales/ar/chat.json +38 -0
  267. package/src/i18n/locales/ar/components.json +141 -40
  268. package/src/i18n/locales/ar/components.toml +59 -0
  269. package/src/i18n/locales/ar/platform.json +65 -0
  270. package/src/i18n/locales/de/chat.json +38 -0
  271. package/src/i18n/locales/de/components.json +139 -38
  272. package/src/i18n/locales/de/components.toml +59 -0
  273. package/src/i18n/locales/de/platform.json +65 -0
  274. package/src/i18n/locales/en/chat.json +38 -0
  275. package/src/i18n/locales/en/components.json +104 -3
  276. package/src/i18n/locales/en/components.toml +59 -0
  277. package/src/i18n/locales/en/platform.json +65 -0
  278. package/src/i18n/locales/es/chat.json +38 -0
  279. package/src/i18n/locales/es/components.json +141 -40
  280. package/src/i18n/locales/es/components.toml +59 -0
  281. package/src/i18n/locales/es/platform.json +65 -0
  282. package/src/i18n/locales/fr/chat.json +38 -0
  283. package/src/i18n/locales/fr/components.json +137 -36
  284. package/src/i18n/locales/fr/components.toml +59 -0
  285. package/src/i18n/locales/fr/platform.json +65 -0
  286. package/src/i18n/locales/ja/chat.json +38 -0
  287. package/src/i18n/locales/ja/components.json +104 -3
  288. package/src/i18n/locales/ja/components.toml +59 -0
  289. package/src/i18n/locales/ja/platform.json +65 -0
  290. package/src/i18n/locales/ko/chat.json +38 -0
  291. package/src/i18n/locales/ko/components.json +104 -3
  292. package/src/i18n/locales/ko/components.toml +59 -0
  293. package/src/i18n/locales/ko/platform.json +65 -0
  294. package/src/i18n/locales/pt/chat.json +38 -0
  295. package/src/i18n/locales/pt/components.json +141 -40
  296. package/src/i18n/locales/pt/components.toml +59 -0
  297. package/src/i18n/locales/pt/platform.json +65 -0
  298. package/src/i18n/locales/ru/chat.json +38 -0
  299. package/src/i18n/locales/ru/components.json +141 -40
  300. package/src/i18n/locales/ru/components.toml +59 -0
  301. package/src/i18n/locales/ru/platform.json +65 -0
  302. package/src/i18n/locales/zh-Hans/chat.json +38 -0
  303. package/src/i18n/locales/zh-Hans/components.json +109 -4
  304. package/src/i18n/locales/zh-Hans/components.toml +59 -0
  305. package/src/i18n/locales/zh-Hans/platform.json +65 -0
  306. package/src/i18n/locales/zh-Hant/chat.json +38 -0
  307. package/src/i18n/locales/zh-Hant/components.json +109 -4
  308. package/src/i18n/locales/zh-Hant/components.toml +59 -0
  309. package/src/i18n/locales/zh-Hant/platform.json +65 -0
  310. package/src/index.ts +225 -6
  311. package/src/plugins/bootScriptsPlugin.ts +47 -0
  312. package/src/plugins/buildHashPlugin.ts +67 -0
  313. package/src/plugins/index.ts +2 -0
  314. package/src/render.ts +1 -1
  315. package/src/runtime/animationBus.test.ts +112 -0
  316. package/src/runtime/animationBus.ts +33 -2
  317. package/src/runtime/backStack.test.ts +334 -0
  318. package/src/runtime/backStack.ts +377 -0
  319. package/src/runtime/env.ts +4 -0
  320. package/src/runtime/index.ts +15 -3
  321. package/src/runtime/intervalBus.test.ts +118 -0
  322. package/src/runtime/intervalBus.ts +189 -0
  323. package/src/runtime/mobileViewport.test.ts +132 -0
  324. package/src/runtime/mobileViewport.ts +157 -0
  325. package/src/runtime/pageLifecycle.ts +91 -0
  326. package/src/runtime/useBlockingToast.ts +112 -0
  327. package/src/runtime/useClipboard.ts +28 -0
  328. package/src/runtime/useConfirm.ts +15 -7
  329. package/src/runtime/useMediaQuery.test.ts +67 -0
  330. package/src/runtime/useMediaQuery.ts +89 -0
  331. package/src/runtime/useOverlay.test.ts +160 -0
  332. package/src/runtime/useOverlay.ts +59 -12
  333. package/src/runtime/usePopupManager.test.ts +200 -0
  334. package/src/runtime/usePopupManager.ts +106 -10
  335. package/src/runtime/useToast.test.ts +90 -0
  336. package/src/runtime/useToast.ts +20 -2
  337. package/src/styles/admin-tokens.scss +744 -0
  338. package/src/styles/index.scss +5 -3
  339. package/src/styles/theme/_glass.scss +33 -0
  340. package/src/styles/theme/_layout.scss +123 -0
  341. package/src/styles/theme/_scrollbar.scss +73 -0
  342. package/src/styles/theme/_tokens.scss +69 -0
  343. package/src/styles/theme/base.scss +617 -0
  344. package/src/styles/theme/foundation.scss +219 -0
  345. package/src/styles/theme/mixins.scss +793 -0
  346. package/src/styles/theme/themes.scss +25 -0
  347. package/src/styles/theme/variables.scss +125 -0
  348. package/src/theme/_field.scss +116 -0
  349. package/src/theme/fontContext.test.ts +179 -0
  350. package/src/theme/fontContext.ts +169 -0
  351. package/src/theme/index.ts +19 -2
  352. package/src/theme/presets.ts +86 -18
  353. package/src/theme/theme.scss +61 -1
  354. package/src/theme/tokenGroups.test.ts +534 -0
  355. package/src/theme/tokenGroups.ts +552 -0
  356. package/src/theme/useSolarTime.test.ts +113 -0
  357. package/src/theme/useSolarTime.ts +95 -20
  358. package/src/theme/useTheme.test.ts +72 -0
  359. package/src/theme/useTheme.ts +144 -9
  360. package/src/tokens.scss +110 -0
  361. package/src/utils/base64.ts +31 -0
  362. package/src/utils/connectivity.ts +51 -0
  363. package/src/utils/dom.test.ts +131 -0
  364. package/src/utils/dom.ts +133 -0
  365. package/src/utils/download.ts +22 -0
  366. package/src/utils/fileType.ts +127 -0
  367. package/src/utils/format.ts +98 -0
  368. package/src/utils/fuzzy.ts +77 -0
  369. package/src/utils/fuzzySearch.test.ts +87 -0
  370. package/src/utils/healthProbe.ts +64 -0
  371. package/src/utils/objectPath.ts +59 -0
  372. package/src/utils/password.ts +53 -0
  373. package/src/utils/pow.test.ts +45 -0
  374. package/src/utils/pow.ts +226 -0
  375. package/src/utils/powNonce.ts +95 -0
  376. package/src/utils/validation.test.ts +78 -0
  377. package/src/DemoApp.vue +0 -400
  378. package/src/components/HkCollapse.tsx +0 -48
  379. package/src/components/HkMorphingTabs.scss +0 -141
  380. package/src/components/HkMorphingTabs.tsx +0 -113
  381. package/src/composables/useBreakpoint.ts +0 -41
  382. package/src/composables/useClipboard.ts +0 -37
  383. package/src/composables/useConfirm.ts +0 -35
  384. package/src/composables/useToast.ts +0 -2
  385. package/src/runtime/useAsyncData.ts +0 -30
@@ -0,0 +1,200 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { watch } from "vue";
3
+
4
+ import { POPUP_Z_BANDS, POPUP_Z_STEP, usePopupManager } from "./usePopupManager";
5
+
6
+ const managers: ReturnType<typeof usePopupManager>[] = [];
7
+
8
+ function freshManager() {
9
+ const m = usePopupManager();
10
+ managers.push(m);
11
+ return m;
12
+ }
13
+
14
+ afterEach(() => {
15
+ // The registry is a module singleton shared across usePopupManager()
16
+ // instances — unregister everything so tests do not leak z state.
17
+ for (const m of managers.splice(0)) {
18
+ for (const entry of [...m.registry.value.values()]) {
19
+ m.unregister(entry.id);
20
+ }
21
+ }
22
+ document.body.style.overflow = "";
23
+ });
24
+
25
+ describe("usePopupManager z bands", () => {
26
+ it("bands are ordered overlay < dropdown < tooltip < toast", () => {
27
+ expect(POPUP_Z_BANDS.modal).toBe(POPUP_Z_BANDS.drawer);
28
+ expect(POPUP_Z_BANDS.modal).toBeLessThan(POPUP_Z_BANDS.dropdown);
29
+ expect(POPUP_Z_BANDS.dropdown).toBeLessThan(POPUP_Z_BANDS.tooltip);
30
+ expect(POPUP_Z_BANDS.tooltip).toBeLessThan(POPUP_Z_BANDS.toast);
31
+ });
32
+
33
+ it("keeps a toast registered first above overlays opened later", () => {
34
+ // The mobile regression: HkToast mounts once at shell level, so the
35
+ // old registration-order ladder gave every later modal a higher z and
36
+ // buried the toast under the phone bottom sheet. Bands must hold the
37
+ // toast on top regardless of registration order.
38
+ const m = freshManager();
39
+ const toast = m.register("toast", false);
40
+ expect(toast.zIndex).toBe(POPUP_Z_BANDS.toast);
41
+
42
+ const modal = m.register("modal", true);
43
+ const drawer = m.register("drawer", true);
44
+ const dropdown = m.register("dropdown", false);
45
+ expect(modal.zIndex).toBeLessThan(toast.zIndex);
46
+ expect(drawer.zIndex).toBeLessThan(toast.zIndex);
47
+ expect(dropdown.zIndex).toBeLessThan(toast.zIndex);
48
+ });
49
+
50
+ it("stacks in open order within a band and reclaims freed slots", () => {
51
+ const m = freshManager();
52
+ const a = m.register("modal", true);
53
+ const b = m.register("modal", true);
54
+ expect(a.zIndex).toBe(POPUP_Z_BANDS.modal);
55
+ expect(b.zIndex).toBe(a.zIndex + POPUP_Z_STEP);
56
+
57
+ m.unregister(b.id);
58
+ // C must reclaim B's slot — NOT drift upward past it.
59
+ const c = m.register("modal", true);
60
+ expect(c.zIndex).toBe(b.zIndex);
61
+
62
+ m.unregister(a.id);
63
+ m.unregister(c.id);
64
+ // Band empty again → back to the band base.
65
+ const d = m.register("modal", true);
66
+ expect(d.zIndex).toBe(POPUP_Z_BANDS.modal);
67
+ });
68
+
69
+ it("shares the overlay band between modal and drawer in open order", () => {
70
+ const m = freshManager();
71
+ const modal = m.register("modal", true);
72
+ const drawer = m.register("drawer", true);
73
+ // Same band: the drawer opened over the modal paints above it, and a
74
+ // replacement drawer reclaims the freed slot instead of drifting.
75
+ expect(modal.zIndex).toBe(POPUP_Z_BANDS.modal);
76
+ expect(drawer.zIndex).toBe(modal.zIndex + POPUP_Z_STEP);
77
+
78
+ m.unregister(drawer.id);
79
+ const drawer2 = m.register("drawer", true);
80
+ expect(drawer2.zIndex).toBe(drawer.zIndex);
81
+ });
82
+
83
+ it("keeps a dropdown opened from within an overlay above it", () => {
84
+ // Select panels portal to <body>; opened from inside a modal they
85
+ // must paint above the modal that contains them — the whole reason
86
+ // the dropdown band sits above the overlay band.
87
+ const m = freshManager();
88
+ const modal = m.register("modal", true);
89
+ const panel = m.register("dropdown", false);
90
+ expect(panel.zIndex).toBeGreaterThan(modal.zIndex);
91
+
92
+ m.unregister(panel.id);
93
+ m.unregister(modal.id);
94
+
95
+ // Same guarantee inverted: with the dropdown registered FIRST, the
96
+ // later modal still lands on its own (lower) overlay band — band
97
+ // membership, not registration order, decides.
98
+ const dropdown = m.register("dropdown", false);
99
+ const modal2 = m.register("modal", true);
100
+ expect(dropdown.zIndex).toBe(POPUP_Z_BANDS.dropdown);
101
+ expect(modal2.zIndex).toBe(POPUP_Z_BANDS.modal);
102
+ expect(modal2.zIndex).toBeLessThan(dropdown.zIndex);
103
+ });
104
+
105
+ it("keeps tooltips above overlays and below toasts", () => {
106
+ const m = freshManager();
107
+ const modal = m.register("modal", true);
108
+ const tooltip = m.register("tooltip", false);
109
+ const toast = m.register("toast", false);
110
+ expect(tooltip.zIndex).toBe(POPUP_Z_BANDS.tooltip);
111
+ expect(tooltip.zIndex).toBeGreaterThan(modal.zIndex);
112
+ expect(toast.zIndex).toBeGreaterThan(tooltip.zIndex);
113
+ });
114
+
115
+ it("keeps the scroll lock counter balanced across register/unregister", () => {
116
+ const m = freshManager();
117
+ const modal = m.register("modal", true);
118
+ expect(document.body.style.overflow).toBe("hidden");
119
+
120
+ const dropdown = m.register("dropdown", false);
121
+ m.unregister(dropdown.id);
122
+ // The modal still locks scroll.
123
+ expect(document.body.style.overflow).toBe("hidden");
124
+
125
+ m.unregister(modal.id);
126
+ expect(document.body.style.overflow).toBe("");
127
+ });
128
+ });
129
+
130
+ describe("usePopupManager blocking flag (breadcrumb levels)", () => {
131
+ it("defaults to non-blocking and stores explicit blocking", () => {
132
+ const m = freshManager();
133
+ const anchored = m.register("dropdown", false);
134
+ const sheet = m.register("dropdown", false, "Sheet", true);
135
+ const modal = m.register("modal", true, "Window");
136
+ expect(m.registry.value.get(anchored.id)!.blocking).toBe(false);
137
+ expect(m.registry.value.get(sheet.id)!.blocking).toBe(true);
138
+ // Window kinds list by kind alone; the flag stays false.
139
+ expect(m.registry.value.get(modal.id)!.blocking).toBe(false);
140
+ });
141
+
142
+ it("setBlocking flips the flag reactively (anchor ↔ sheet morph)", () => {
143
+ const m = freshManager();
144
+ const popup = m.register("dropdown", false, "Menu");
145
+ // Vue reactivity: the reassigned Map must notify registry watchers.
146
+ const flips: boolean[] = [];
147
+ const stop = watch(
148
+ () => m.registry.value.get(popup.id)?.blocking,
149
+ (v) => { if (v !== undefined) flips.push(v); },
150
+ { flush: "sync" },
151
+ );
152
+ m.setBlocking(popup.id, true);
153
+ expect(m.registry.value.get(popup.id)!.blocking).toBe(true);
154
+ m.setBlocking(popup.id, false);
155
+ expect(m.registry.value.get(popup.id)!.blocking).toBe(false);
156
+ expect(flips).toEqual([true, false]);
157
+ stop();
158
+ // Flipping to the same value is a no-op (no map churn).
159
+ const before = m.registry.value;
160
+ m.setBlocking(popup.id, false);
161
+ expect(m.registry.value).toBe(before);
162
+ });
163
+
164
+ it("dev-warns when a breadcrumb-visible popup registers untitled", () => {
165
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
166
+ try {
167
+ const m = freshManager();
168
+ m.register("dropdown", false); // anchored popover: fine, hidden level
169
+ expect(warn).not.toHaveBeenCalled();
170
+
171
+ m.register("dropdown", false, undefined, true); // blocking sheet: unnamed
172
+ expect(warn).toHaveBeenCalledTimes(1);
173
+ expect(warn.mock.calls[0][0]).toMatch(/without a title/);
174
+
175
+ m.register("modal", true); // window kind: unnamed
176
+ m.register("drawer", true); // window kind: unnamed
177
+ expect(warn).toHaveBeenCalledTimes(3);
178
+
179
+ warn.mockClear();
180
+ m.register("modal", true, "Named");
181
+ m.register("dropdown", false, "Named sheet", true);
182
+ expect(warn).not.toHaveBeenCalled();
183
+ } finally {
184
+ warn.mockRestore();
185
+ }
186
+ });
187
+
188
+ it("dev-warns when setBlocking promotes an untitled popup", () => {
189
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
190
+ try {
191
+ const m = freshManager();
192
+ const popup = m.register("dropdown", false);
193
+ expect(warn).not.toHaveBeenCalled();
194
+ m.setBlocking(popup.id, true);
195
+ expect(warn).toHaveBeenCalledTimes(1);
196
+ } finally {
197
+ warn.mockRestore();
198
+ }
199
+ });
200
+ });
@@ -2,16 +2,93 @@ import { readonly, ref } from "vue";
2
2
 
3
3
  export type PopupKind = "dropdown" | "modal" | "drawer" | "tooltip" | "toast";
4
4
 
5
- interface PopupEntry {
5
+ /**
6
+ * Kind-priority z bands — the single source of truth for popup stacking.
7
+ *
8
+ * Every registered popup lands in its kind's band, so layering is decided
9
+ * by WHAT a surface is, never by WHEN it happened to register. Bands are
10
+ * ordered low → high:
11
+ *
12
+ * modal (1000) centered dialogs + phone bottom sheets
13
+ * drawer (1000) edge drawers share the overlay band with modals —
14
+ * inside the band they stack in open order, so a
15
+ * drawer opened over a modal still paints above it
16
+ * dropdown (2000) anchor-attached popovers, select panels, menus —
17
+ * ABOVE the overlay band on purpose: a select panel
18
+ * opened from inside a modal portals to <body> and
19
+ * must paint above the modal that contains it (the
20
+ * common in-modal form flow). A page-level dropdown
21
+ * can only coexist with a modal programmatically (the
22
+ * modal overlay intercepts pointers), so the rare
23
+ * stale-dropdown-over-modal case is accepted, matching
24
+ * Ant Design
25
+ * tooltip (3000) transient annotations must stay visible above the
26
+ * surfaces they annotate
27
+ * toast (4000) ALWAYS the topmost surface — a toast must never be
28
+ * buried under a modal/sheet, no matter which opened
29
+ * first (the long-lived HkToast stack registers once
30
+ * at shell mount and must keep its band forever)
31
+ *
32
+ * Within a band, entries stack in open order (max live slot + step), and
33
+ * freed slots are reclaimed automatically because the next z derives from
34
+ * the CURRENT live entries of that band only — no monotonic counter, no
35
+ * cross-band coupling (a persistent toast/tooltip registration can no
36
+ * longer push later modals up the ladder; that ordering bug is what this
37
+ * band scheme replaces). Overlay roots are spaced one Z_STEP apart so each
38
+ * overlay's +1 content/panel layer always has a free slot above its own
39
+ * root and below the next overlay.
40
+ *
41
+ * Keep the SCSS fallbacks (`--hi-z-*` / `--hk-z-*` in theme.scss and the
42
+ * component sheets) in sync with these numbers.
43
+ */
44
+ export const POPUP_Z_BANDS: Record<PopupKind, number> = {
45
+ modal: 1000,
46
+ drawer: 1000,
47
+ dropdown: 2000,
48
+ tooltip: 3000,
49
+ toast: 4000,
50
+ };
51
+
52
+ /** In-band stacking step. Even numbers leave the odd slot free for the
53
+ * +1 content/panel layer each overlay puts above its own root. */
54
+ export const POPUP_Z_STEP = 2;
55
+
56
+ export interface PopupEntry {
6
57
  id: string;
7
58
  kind: PopupKind;
8
59
  locksScroll: boolean;
9
60
  zIndex: number;
10
61
  title?: string;
62
+ /**
63
+ * True while the popup is a blocking window the user "navigates" —
64
+ * a mobile bottom sheet that rose from a dropdown-kind surface. The
65
+ * modal-stack breadcrumb lists window kinds (modal/drawer) always and
66
+ * dropdown kinds only while they block like this; an anchored desktop
67
+ * popover stays a hidden level. Named surfaces only: a blocking popup
68
+ * without a title is a naming bug (dev warn at registration).
69
+ */
70
+ blocking: boolean;
11
71
  }
12
72
 
13
- const Z_BASE = 1000;
14
- const Z_STEP = 2;
73
+ /**
74
+ * Dev-time naming enforcement for the window-stack breadcrumb. Any popup
75
+ * that participates in the breadcrumb (window kinds, or a dropdown that
76
+ * currently blocks as a sheet) must carry a real, i18n-resolved title —
77
+ * a bare "Layer N" style fallback is exactly what the strip must never
78
+ * show. Production builds stay silent; the breadcrumb falls back to a
79
+ * generic localized label there.
80
+ */
81
+ function warnUntitled(kind: PopupKind, blocking: boolean, title?: string): void {
82
+ const stacks = kind === "modal" || kind === "drawer" || blocking;
83
+ if (!stacks || title) return;
84
+ if (import.meta.env?.DEV) {
85
+ console.warn(
86
+ `[hikari] popup registered without a title (kind: ${kind}). ` +
87
+ "Window surfaces must pass an i18n-resolved title so the " +
88
+ "modal-stack breadcrumb can label their layer.",
89
+ );
90
+ }
91
+ }
15
92
 
16
93
  function uid(): string {
17
94
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
@@ -21,7 +98,6 @@ function uid(): string {
21
98
  }
22
99
 
23
100
  const registry = ref<Map<string, PopupEntry>>(new Map());
24
- let nextZ = Z_BASE;
25
101
  let scrollLockCount = 0;
26
102
 
27
103
  function updateBodyScroll() {
@@ -42,12 +118,21 @@ export function usePopupManager() {
42
118
  kind: PopupKind,
43
119
  locksScroll = false,
44
120
  title?: string,
121
+ blocking = false,
45
122
  ): PopupHandle {
46
123
  const id = uid();
47
- const zIndex = nextZ;
48
- nextZ += Z_STEP;
49
- const entry: PopupEntry = { id, kind, locksScroll, zIndex, title };
124
+ const band = POPUP_Z_BANDS[kind];
125
+ // Next slot = one step above the highest LIVE entry of the same band.
126
+ let maxSlot = -1;
127
+ for (const entry of registry.value.values()) {
128
+ if (POPUP_Z_BANDS[entry.kind] !== band) continue;
129
+ const slot = (entry.zIndex - band) / POPUP_Z_STEP;
130
+ if (slot > maxSlot) maxSlot = slot;
131
+ }
132
+ const zIndex = band + (maxSlot + 1) * POPUP_Z_STEP;
133
+ const entry: PopupEntry = { id, kind, locksScroll, zIndex, title, blocking };
50
134
  registry.value.set(id, entry);
135
+ warnUntitled(kind, blocking, title);
51
136
  if (locksScroll) {
52
137
  scrollLockCount++;
53
138
  updateBodyScroll();
@@ -62,6 +147,19 @@ export function usePopupManager() {
62
147
  registry.value = new Map(registry.value);
63
148
  }
64
149
 
150
+ /**
151
+ * Flip the blocking flag while the popup stays open — a dropdown that
152
+ * docks as a bottom sheet when the viewport crosses the mobile
153
+ * breakpoint becomes a breadcrumb level mid-flight (and back).
154
+ */
155
+ function setBlocking(id: string, blocking: boolean) {
156
+ const entry = registry.value.get(id);
157
+ if (!entry || entry.blocking === blocking) return;
158
+ entry.blocking = blocking;
159
+ registry.value = new Map(registry.value);
160
+ if (blocking) warnUntitled(entry.kind, true, entry.title);
161
+ }
162
+
65
163
  function unregister(id: string) {
66
164
  const entry = registry.value.get(id);
67
165
  if (!entry) return;
@@ -70,9 +168,6 @@ export function usePopupManager() {
70
168
  scrollLockCount = Math.max(0, scrollLockCount - 1);
71
169
  updateBodyScroll();
72
170
  }
73
- if (registry.value.size === 0) {
74
- nextZ = Z_BASE;
75
- }
76
171
  }
77
172
 
78
173
  function isOpen(id: string): boolean {
@@ -83,6 +178,7 @@ export function usePopupManager() {
83
178
  registry: readonly(registry),
84
179
  register,
85
180
  setTitle,
181
+ setBlocking,
86
182
  unregister,
87
183
  isOpen,
88
184
  };
@@ -0,0 +1,90 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ import {
4
+ MAX_TOASTS_PER_SLOT,
5
+ useToast,
6
+ } from "./useToast";
7
+
8
+ function clearAll() {
9
+ const { toasts, remove } = useToast();
10
+ for (const t of [...toasts]) remove(t.id);
11
+ }
12
+
13
+ describe("useToast stacking and dedupe", () => {
14
+ beforeEach(() => {
15
+ vi.useFakeTimers();
16
+ clearAll();
17
+ });
18
+ afterEach(() => {
19
+ clearAll();
20
+ vi.useRealTimers();
21
+ });
22
+
23
+ it("dedupes identical consecutive messages in the same slot", () => {
24
+ const toast = useToast();
25
+ const firstId = toast.error("backend unreachable");
26
+ const secondId = toast.error("backend unreachable");
27
+
28
+ expect(toast.toasts).toHaveLength(1);
29
+ expect(toast.toasts[0]!.messages).toHaveLength(1);
30
+ // Replace semantics: the second push returns the existing entry id.
31
+ expect(secondId).toBe(firstId);
32
+ expect(toast.toasts[0]!.messages[0]!.text).toBe("backend unreachable");
33
+ });
34
+
35
+ it("keeps distinct consecutive messages", () => {
36
+ const toast = useToast();
37
+ toast.error("first");
38
+ toast.error("second");
39
+
40
+ expect(toast.toasts[0]!.messages.map((m) => m.text)).toEqual([
41
+ "first",
42
+ "second",
43
+ ]);
44
+ });
45
+
46
+ it("dedupes only consecutive duplicates", () => {
47
+ const toast = useToast();
48
+ toast.error("a");
49
+ toast.error("b");
50
+ toast.error("b");
51
+
52
+ expect(toast.toasts[0]!.messages.map((m) => m.text)).toEqual(["a", "b"]);
53
+ });
54
+
55
+ it("caps messages per slot at MAX_TOASTS_PER_SLOT, dropping the oldest", () => {
56
+ const toast = useToast();
57
+ for (let i = 0; i < MAX_TOASTS_PER_SLOT + 3; i++) {
58
+ toast.error(`msg-${i}`);
59
+ }
60
+
61
+ expect(toast.toasts[0]!.messages).toHaveLength(MAX_TOASTS_PER_SLOT);
62
+ const texts = toast.toasts[0]!.messages.map((m) => m.text);
63
+ expect(texts[0]).toBe("msg-3"); // 0..2 dropped (oldest)
64
+ expect(texts[texts.length - 1]).toBe(`msg-${MAX_TOASTS_PER_SLOT + 2}`);
65
+ });
66
+
67
+ it("caps each slot independently", () => {
68
+ const toast = useToast();
69
+ for (let i = 0; i < MAX_TOASTS_PER_SLOT + 2; i++) toast.error(`e-${i}`);
70
+ for (let i = 0; i < MAX_TOASTS_PER_SLOT + 2; i++) toast.success(`s-${i}`);
71
+
72
+ const errorSlot = toast.toasts.find((t) => t.type === "error")!;
73
+ const successSlot = toast.toasts.find((t) => t.type === "success")!;
74
+ expect(errorSlot.messages).toHaveLength(MAX_TOASTS_PER_SLOT);
75
+ expect(successSlot.messages).toHaveLength(MAX_TOASTS_PER_SLOT);
76
+ });
77
+
78
+ it("dedupe still restarts the auto-dismiss clock (loading never dismisses)", () => {
79
+ const toast = useToast();
80
+ toast.info("pulse");
81
+ const first = toast.toasts[0]!;
82
+ // info duration is 3000ms — after 1000ms a duplicate push keeps a
83
+ // single entry; the slot is still present at 2500ms (timer restarted).
84
+ vi.advanceTimersByTime(1000);
85
+ toast.info("pulse");
86
+ expect(toast.toasts[0]!.messages).toHaveLength(1);
87
+ vi.advanceTimersByTime(2499);
88
+ expect(toast.toasts.some((t) => t.type === "info")).toBe(true);
89
+ });
90
+ });
@@ -33,6 +33,11 @@ export interface ToastItem {
33
33
  copyable?: boolean;
34
34
  }
35
35
 
36
+ /** Hard cap on stacked messages per toast slot. Beyond it the oldest
37
+ * messages are dropped so a noisy caller cannot bury the surface in
38
+ * unbounded history. */
39
+ export const MAX_TOASTS_PER_SLOT = 5;
40
+
36
41
  const state = reactive<{ toasts: ToastItem[] }>({ toasts: [] });
37
42
 
38
43
  let nextSlotId = 0;
@@ -86,8 +91,21 @@ function push(
86
91
  } else if (options.copyable === true && !slot.copyable) {
87
92
  slot.copyable = true;
88
93
  }
89
- const msgId = ++nextMsgId;
90
- slot.messages.push({ id: msgId, text });
94
+ let msgId: number;
95
+ const last = slot.messages[slot.messages.length - 1];
96
+ if (last && last.text === text) {
97
+ // Dedupe: an identical consecutive message (same slot + same text in
98
+ // the current stack) replaces the previous entry instead of appending —
99
+ // the surface keeps a single entry and the auto-dismiss clock restarts.
100
+ msgId = last.id;
101
+ } else {
102
+ msgId = ++nextMsgId;
103
+ slot.messages.push({ id: msgId, text });
104
+ }
105
+ // Cap: drop the oldest messages beyond the per-slot cap.
106
+ while (slot.messages.length > MAX_TOASTS_PER_SLOT) {
107
+ slot.messages.shift();
108
+ }
91
109
  scheduleAutoDismiss(slot);
92
110
  return msgId;
93
111
  }