@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,377 @@
1
+ /**
2
+ * Central back-guard service — window-first back navigation.
3
+ *
4
+ * Priority rule (user directive): the back gesture — the mobile
5
+ * back button/gesture and the desktop browser back button alike —
6
+ * must be consumed by the topmost open WINDOW first (modal, drawer,
7
+ * menu sheet stack); only when every window has closed does the
8
+ * gesture reach page history and actually navigate back.
9
+ *
10
+ * Mechanism (a generalization of HkMenu's battle-tested Android
11
+ * modal-navigation mode): every window-like surface pushes marked
12
+ * history entries while open, so a back gesture lands on an entry
13
+ * below the window instead of leaving the page. ONE module-level
14
+ * popstate listener dispatches each gesture to the guard that should
15
+ * consume it, and programmatic closes (X / Escape / overlay tap)
16
+ * rewind the entries the window pushed so the page's own history
17
+ * stays clean.
18
+ *
19
+ * Traversal discipline (the subtle part):
20
+ * - pushes are SYNCHRONOUS (marker depth must stack immediately);
21
+ * - rewinds (pop / release) are DEFERRED to a macrotask and
22
+ * re-validated at execution time. A same-tick "close A, open B"
23
+ * (select a menu leaf → closeAll → modal opens) must not fire a
24
+ * go(-n) computed from a history top that B's push already
25
+ * replaced: at flush time a guard that no longer owns the current
26
+ * entry simply drops its claim (its markers stay buried and inert)
27
+ * instead of yanking B's live entry to the forward stack;
28
+ * - a popstate while a suppressed traversal is in flight is ours;
29
+ * any other is a user gesture. Listener teardown resets the
30
+ * expectation so a stale counter can never swallow a later,
31
+ * genuine gesture after the guard set changed.
32
+ *
33
+ * Dead markers (documented trade-off): when a window closes while a
34
+ * foreign entry sits above it (a router pushed during the window's
35
+ * lifetime), its markers are buried mid-stack and cannot be removed —
36
+ * the history API has no delete-entry primitive. They are inert and
37
+ * self-releasing: the next back that lands on one is treated as plain
38
+ * navigation (the router re-syncs from the URL) and the marker is
39
+ * de-marked in place. A back through such a marker can therefore cost
40
+ * one extra press before the URL actually changes — the unavoidable
41
+ * price of keeping the router's live state intact.
42
+ *
43
+ * State markers stamped into history.state:
44
+ * { __hkBack: <guardId>, __hkBackDepth: <0-based level> }
45
+ */
46
+
47
+ import type { ShallowRef } from "vue";
48
+ import { shallowRef } from "vue";
49
+
50
+ /** history.state key holding the owning guard id. */
51
+ export const BACK_GUARD_MARKER = "__hkBack";
52
+ /** history.state key holding the 0-based level of the pushed entry. */
53
+ export const BACK_GUARD_DEPTH = "__hkBackDepth";
54
+
55
+ interface BackState {
56
+ [BACK_GUARD_MARKER]?: string;
57
+ [BACK_GUARD_DEPTH]?: number;
58
+ }
59
+
60
+ export interface BackGuardOptions {
61
+ /**
62
+ * A user back gesture arrived while this guard is the topmost window.
63
+ * `depth` is the landed entry's level when it belongs to this guard
64
+ * (multi-level surfaces — menu sheet stacks — truncate to it);
65
+ * `null` means the landing was outside this guard's stack and the
66
+ * surface must collapse entirely. The service has already updated
67
+ * its entry bookkeeping before the call.
68
+ */
69
+ onBack: (depth: number | null) => void;
70
+ }
71
+
72
+ export interface BackGuard {
73
+ readonly id: string;
74
+ /** History entries this guard currently owns above the page base. */
75
+ readonly entries: number;
76
+ /** Push one marked entry (opening a window / adding a level). */
77
+ push(): void;
78
+ /**
79
+ * In-app one-level back (a sheet's own back button): a suppressed
80
+ * history.back() deferred like every rewind. The caller truncates
81
+ * its own visual stack; the service only maintains history.
82
+ */
83
+ pop(): void;
84
+ /**
85
+ * Programmatic full close (X / Escape / overlay / unmount): rewind
86
+ * every entry this guard pushed via one suppressed traversal. When a
87
+ * foreign entry sits on top (a router or another window pushed while
88
+ * open) the live state is left untouched — the buried markers simply
89
+ * become inert and are released if the user ever lands on them.
90
+ */
91
+ release(): void;
92
+ /**
93
+ * Drop every entry claim AND cancel any pending rewind WITHOUT
94
+ * touching history: for a close that is itself the consequence of an
95
+ * in-page action started from the surface — a menu leaf whose select
96
+ * handler opens a modal or drives an async router navigation. The
97
+ * deferred rewind would otherwise win the race (its flush runs on the
98
+ * next macrotask, before an async navigation commits its pushState)
99
+ * and yank the page back onto the closed surface's marker entry,
100
+ * silently discarding the action's navigation. The pushed markers
101
+ * stay where they lie as inert dead markers — released in place by
102
+ * the popstate cleanup if a later traversal ever lands on them.
103
+ */
104
+ abandon(): void;
105
+ /**
106
+ * Drop every entry claim WITHOUT touching history: for a surface
107
+ * that is already closed yet finds itself owning the current (spent)
108
+ * marker — de-marks it so a closed window never owns live history.
109
+ */
110
+ forget(): void;
111
+ /** True when the current history entry carries this guard's marker. */
112
+ ownsCurrent(): boolean;
113
+ /** Release and unregister from dispatch. */
114
+ destroy(): void;
115
+ }
116
+
117
+ interface GuardRecord {
118
+ id: string;
119
+ options: BackGuardOptions;
120
+ /** Live entry count — kept reactive for devtool-friendly consumers. */
121
+ count: ShallowRef<number>;
122
+ /** Entry count the surface WANTS (drives deferred rewinds). */
123
+ desired: number;
124
+ }
125
+
126
+ const hasWindow = typeof window !== "undefined";
127
+
128
+ /**
129
+ * Dispatch stack, ordered by LAST ACTIVATION (last = topmost window).
130
+ * Guards register at component setup — mount order, not open order —
131
+ * so every 0→1 entry transition re-stamps the activation order; a
132
+ * window opened later must consume the back gesture first.
133
+ */
134
+ const guards: GuardRecord[] = [];
135
+ /**
136
+ * Records with a pending rewind claim — INCLUDING destroyed ones: a
137
+ * guard unmounted between release() and the flush must still rewind
138
+ * its entries (its markers live in history regardless), or the dead
139
+ * marker strands as the current entry.
140
+ */
141
+ const rewindQueue = new Set<GuardRecord>();
142
+ /** Popstate events produced by our own suppressed traversals. */
143
+ let suppressCount = 0;
144
+ /** A suppressed traversal has been issued and expects one popstate. */
145
+ let traversalInFlight = false;
146
+ /** Macrotask-scheduled rewind flush (single-flight). */
147
+ let flushScheduled = false;
148
+ let listening = false;
149
+
150
+ function readState(): BackState | null {
151
+ if (!hasWindow) return null;
152
+ const st = window.history.state;
153
+ return st != null && typeof st === "object" ? (st as BackState) : null;
154
+ }
155
+
156
+ function onPopState(e: PopStateEvent): void {
157
+ if (suppressCount > 0) {
158
+ // Our own rewind traversal landing — not a user gesture.
159
+ suppressCount--;
160
+ traversalInFlight = false;
161
+ maybeDropListener();
162
+ return;
163
+ }
164
+ // Window priority: the topmost guard still owning entries consumes
165
+ // the gesture. Only when no window owns history does the gesture
166
+ // fall through to plain page navigation (handled by the router /
167
+ // the browser, not by us).
168
+ for (let i = guards.length - 1; i >= 0; i--) {
169
+ const g = guards[i];
170
+ if (g.count.value <= 0) continue;
171
+ const st =
172
+ e.state != null && typeof e.state === "object" ? (e.state as BackState) : null;
173
+ const own = st?.[BACK_GUARD_MARKER] === g.id;
174
+ const depth =
175
+ own && typeof st?.[BACK_GUARD_DEPTH] === "number" ? st[BACK_GUARD_DEPTH] : null;
176
+ g.count.value = depth === null ? 0 : depth + 1;
177
+ g.desired = g.count.value;
178
+ g.options.onBack(depth);
179
+ return;
180
+ }
181
+ // No window owns history: a forward gesture onto one of our spent
182
+ // marker entries releases the marker so closed surfaces never keep
183
+ // owning live history.
184
+ const st =
185
+ e.state != null && typeof e.state === "object" ? (e.state as BackState) : null;
186
+ if (typeof st?.[BACK_GUARD_MARKER] === "string") {
187
+ window.history.replaceState(null, "");
188
+ }
189
+ }
190
+
191
+ function ensureListener(): void {
192
+ if (!hasWindow || listening) return;
193
+ listening = true;
194
+ window.addEventListener("popstate", onPopState);
195
+ }
196
+
197
+ /**
198
+ * Drop the listener when nothing needs it — but NEVER while a
199
+ * suppressed traversal is still in flight (its landing popstate must
200
+ * be consumed here, or the expectation leaks into the next listener
201
+ * generation and swallows a genuine gesture). Teardown also clears a
202
+ * spent expectation: a landing nobody observes is harmless, a stale
203
+ * counter is not.
204
+ */
205
+ function maybeDropListener(): void {
206
+ if (!hasWindow || !listening) return;
207
+ if (guards.length > 0 || traversalInFlight) return;
208
+ listening = false;
209
+ window.removeEventListener("popstate", onPopState);
210
+ suppressCount = 0;
211
+ }
212
+
213
+ /**
214
+ * Deferred rewind flush, re-validated at execution time. Only the
215
+ * record that still OWNS the current entry may traverse — a foreign or
216
+ * newer window push above it makes its count unreliable, so it DROPS
217
+ * the claim instead (its markers stay buried, inert, self-releasing).
218
+ * Destroyed records keep their place in the queue: their markers live
219
+ * in history regardless of the component tree.
220
+ */
221
+ function scheduleRewind(record: GuardRecord): void {
222
+ if (!hasWindow) return;
223
+ rewindQueue.add(record);
224
+ if (flushScheduled) return;
225
+ flushScheduled = true;
226
+ setTimeout(() => {
227
+ flushScheduled = false;
228
+ // A record can sit in the rewind queue AND still be registered (the
229
+ // normalizer's release-then-push), so collect into a Set to visit
230
+ // each exactly once.
231
+ const candidates = new Set<GuardRecord>(rewindQueue);
232
+ for (const g of guards) {
233
+ if (g.count.value > g.desired) candidates.add(g);
234
+ }
235
+ rewindQueue.clear();
236
+ for (const g of candidates) {
237
+ if (g.count.value <= g.desired) continue;
238
+ const st = readState();
239
+ if (st?.[BACK_GUARD_MARKER] === g.id) {
240
+ const n = g.count.value - g.desired;
241
+ g.count.value = g.desired;
242
+ if (n > 0) {
243
+ if (listening) {
244
+ suppressCount++;
245
+ traversalInFlight = true;
246
+ }
247
+ // With no listener attached (every guard unmounted before
248
+ // this flush ran), the landing popstate goes unobserved —
249
+ // issuing suppression bookkeeping anyway would leak the
250
+ // expectation into the next listener generation and eat a
251
+ // genuine gesture.
252
+ window.history.go(-n);
253
+ }
254
+ return; // one traversal per flush; nothing else can own the top
255
+ }
256
+ // Not ours to rewind (a router or newer window pushed above):
257
+ // abandon the entries where they lie — dead markers are
258
+ // released by the landing cleanup whenever the user reaches
259
+ // them, and forward-stack entries simply die with the session.
260
+ g.count.value = g.desired;
261
+ }
262
+ }, 0);
263
+ }
264
+
265
+ /**
266
+ * Create a back-guard for one window-like surface. The guard must be
267
+ * `destroy()`ed on unmount; `onBack` fires only for user gestures that
268
+ * this guard — as the topmost window — should consume.
269
+ */
270
+ export function createBackGuard(options: BackGuardOptions): BackGuard {
271
+ const id = `hk-back-${Math.random().toString(36).slice(2, 10)}`;
272
+ const record: GuardRecord = { id, options, count: shallowRef(0), desired: 0 };
273
+ guards.push(record);
274
+ ensureListener();
275
+
276
+ function ownsCurrent(): boolean {
277
+ return readState()?.[BACK_GUARD_MARKER] === id;
278
+ }
279
+
280
+ return {
281
+ id,
282
+ get entries(): number {
283
+ return record.count.value;
284
+ },
285
+ push(): void {
286
+ if (!hasWindow) return;
287
+ window.history.pushState(
288
+ { [BACK_GUARD_MARKER]: id, [BACK_GUARD_DEPTH]: record.count.value },
289
+ "",
290
+ );
291
+ record.count.value++;
292
+ // desired advances by ONE level, never snaps to count: a pending
293
+ // release() (desired = 0, rewind deferred) followed by a same-tick
294
+ // push — the HkMenu normalizer's "desktop → mobile" flip — must
295
+ // keep the rewind claim, or the flush sees count == desired and
296
+ // strands the stale entry (N1). Absolute writes (pop / release /
297
+ // dispatch landing) remain absolute: they assert a state, not a delta.
298
+ record.desired++;
299
+ if (record.count.value === 1) {
300
+ // Open order wins over mount order for dispatch priority.
301
+ const i = guards.indexOf(record);
302
+ if (i >= 0 && i !== guards.length - 1) {
303
+ guards.splice(i, 1);
304
+ guards.push(record);
305
+ }
306
+ }
307
+ },
308
+ pop(): void {
309
+ if (!hasWindow || record.count.value <= 0) return;
310
+ // A foreign entry above ours means the history top is not ours to
311
+ // pop — the caller still truncates its visual stack locally.
312
+ if (!ownsCurrent()) return;
313
+ record.desired = record.count.value - 1;
314
+ scheduleRewind(record);
315
+ },
316
+ release(): void {
317
+ record.desired = 0;
318
+ if (!hasWindow) {
319
+ record.count.value = 0;
320
+ return;
321
+ }
322
+ if (record.count.value > 0) {
323
+ scheduleRewind(record);
324
+ } else {
325
+ record.count.value = 0;
326
+ }
327
+ },
328
+ abandon(): void {
329
+ // Snap both counters to zero and retract any queued rewind claim —
330
+ // the flush's `count > desired` re-check then skips this record, so
331
+ // no go(-n) fires and the markers are left buried (inert).
332
+ record.desired = 0;
333
+ record.count.value = 0;
334
+ rewindQueue.delete(record);
335
+ },
336
+ forget(): void {
337
+ record.count.value = 0;
338
+ record.desired = 0;
339
+ if (hasWindow && ownsCurrent()) {
340
+ // Spent marker on the live entry — release ownership in place.
341
+ window.history.replaceState(null, "");
342
+ }
343
+ },
344
+ ownsCurrent,
345
+ destroy(): void {
346
+ this.release();
347
+ // A destroyed guard's rewind cannot wait for the deferred flush:
348
+ // later mounts (the next view, a router swap) stack entries above
349
+ // ours before the macrotask runs, and the deferred candidate can
350
+ // then only abandon — stranding our dead markers mid-stack, where
351
+ // every later rewind stops one short of the page base. The
352
+ // unmount sweep runs before sibling mounts within one patch, so
353
+ // rewinding synchronously here is safe.
354
+ if (hasWindow && record.count.value > 0 && ownsCurrent()) {
355
+ const n = record.count.value;
356
+ record.count.value = 0;
357
+ record.desired = 0;
358
+ rewindQueue.delete(record);
359
+ suppressCount++;
360
+ traversalInFlight = true;
361
+ window.history.go(-n);
362
+ }
363
+ const i = guards.indexOf(record);
364
+ if (i >= 0) guards.splice(i, 1);
365
+ maybeDropListener();
366
+ },
367
+ };
368
+ }
369
+
370
+ /** Test hook: how many guards are registered (leak detection). */
371
+ export function __registeredBackGuards(): number {
372
+ return guards.length;
373
+ }
374
+ /** Test hook: does the module popstate listener currently exist? */
375
+ export function __backListenerActive(): boolean {
376
+ return listening;
377
+ }
@@ -0,0 +1,4 @@
1
+ /** True when running inside a Tauri webview. */
2
+ export function isTauri(): boolean {
3
+ return typeof window !== "undefined" && "__TAURI_INTERNALS__" in window;
4
+ }
@@ -1,12 +1,24 @@
1
1
  export * from "./animationBus";
2
2
  export * from "./cronBus";
3
+ export * from "./intervalBus";
4
+ export * from "./pageLifecycle";
5
+ export * from "./mobileViewport";
6
+ export {
7
+ createBackGuard,
8
+ BACK_GUARD_MARKER,
9
+ BACK_GUARD_DEPTH,
10
+ type BackGuard,
11
+ type BackGuardOptions,
12
+ } from "./backStack";
13
+ export { useMediaQuery, releaseMediaQuery } from "./useMediaQuery";
3
14
  export { useOverlay, closeAll, isOverlayOpen, type OverlayHandle, type UseOverlayOptions } from "./useOverlay";
4
- export { usePopupManager, type PopupHandle, type PopupKind } from "./usePopupManager";
15
+ export { usePopupManager, POPUP_Z_BANDS, POPUP_Z_STEP, type PopupHandle, type PopupKind } from "./usePopupManager";
5
16
  export { useToast, TOAST_DURATION, type ToastItem, type ToastMessage, type ToastType } from "./useToast";
6
17
  export { useConfirm } from "./useConfirm";
18
+ export { useBlockingToast, showBlockingToast, resolveBlockingToast, clearBlockingToasts, type BlockingToastItem, type BlockingToastOptions, type BlockingToastVariant } from "./useBlockingToast";
7
19
  export { useBreakpoint } from "./useBreakpoint";
8
- export { useClipboard } from "./useClipboard";
9
- export { useAsyncData, type UseAsyncDataReturn } from "./useAsyncData";
20
+ export { useClipboard, useClipboardWithToast } from "./useClipboard";
10
21
  export { provideScrollWindow, useScrollWindow, type ScrollWindowContext } from "../composables/useScrollWindow";
11
22
  export { useReportedTransition, type ReportedTransition, type ReportedTransitionTrack } from "../composables/useReportedTransition";
12
23
  export { showProgressDialog, useProgressDialog, type ProgressDialogHandle, type ProgressDialogState } from "../composables/useProgressDialog";
24
+ export { useSafeArea, type SafeAreaInsets } from "../composables/useSafeArea";
@@ -0,0 +1,118 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ import {
4
+ scheduleInterval,
5
+ scheduleIntervalAfter,
6
+ } from "./intervalBus";
7
+ import { onPageLifecycle, pageLifecycleState } from "./pageLifecycle";
8
+
9
+ describe("pageLifecycle", () => {
10
+ it("reports initial state from document", () => {
11
+ const state = pageLifecycleState();
12
+ expect(state.visible).toBe(!document.hidden);
13
+ expect(typeof state.online).toBe("boolean");
14
+ });
15
+
16
+ it("notifies listeners on visibilitychange", () => {
17
+ const seen: boolean[] = [];
18
+ const unsub = onPageLifecycle((s) => seen.push(s.visible));
19
+ const initial = document.hidden;
20
+ Object.defineProperty(document, "hidden", { value: !initial, configurable: true });
21
+ document.dispatchEvent(new Event("visibilitychange"));
22
+ expect(seen[seen.length - 1]).toBe(initial); // flipped
23
+ Object.defineProperty(document, "hidden", { value: initial, configurable: true });
24
+ document.dispatchEvent(new Event("visibilitychange"));
25
+ unsub();
26
+ const count = seen.length;
27
+ document.dispatchEvent(new Event("visibilitychange"));
28
+ expect(seen.length).toBe(count); // unsubscribed
29
+ });
30
+ });
31
+
32
+ describe("scheduleInterval", () => {
33
+ beforeEach(() => {
34
+ vi.useFakeTimers();
35
+ });
36
+ afterEach(() => {
37
+ vi.useRealTimers();
38
+ });
39
+
40
+ it("fires on cadence and stops on disconnect", () => {
41
+ const cb = vi.fn();
42
+ const handle = scheduleInterval(cb, 100);
43
+ vi.advanceTimersByTime(350);
44
+ expect(cb.mock.calls.length).toBe(3);
45
+ handle.disconnect();
46
+ vi.advanceTimersByTime(1000);
47
+ expect(cb.mock.calls.length).toBe(3);
48
+ });
49
+
50
+ it("parks while hidden and catches up once on visibility", () => {
51
+ const cb = vi.fn();
52
+ const handle = scheduleInterval(cb, 100);
53
+ vi.advanceTimersByTime(100);
54
+ expect(cb.mock.calls.length).toBe(1);
55
+
56
+ // Hide — pending timer parked.
57
+ Object.defineProperty(document, "hidden", { value: true, configurable: true });
58
+ document.dispatchEvent(new Event("visibilitychange"));
59
+ vi.advanceTimersByTime(1000);
60
+ expect(cb.mock.calls.length).toBe(1); // no background firing
61
+
62
+ // Visible again — elapsed schedule fires once (catch-up), no burst.
63
+ Object.defineProperty(document, "hidden", { value: false, configurable: true });
64
+ document.dispatchEvent(new Event("visibilitychange"));
65
+ expect(cb.mock.calls.length).toBe(2);
66
+ vi.advanceTimersByTime(100);
67
+ expect(cb.mock.calls.length).toBe(3);
68
+ handle.disconnect();
69
+ });
70
+
71
+ it("stays independent of the reduced-motion switch (data polling is not animation)", () => {
72
+ // Interval bus is intentionally NOT wired to setReducedMotion — data
73
+ // polling must survive motion preferences. Guard against accidental
74
+ // coupling:
75
+ const cb = vi.fn();
76
+ const handle = scheduleInterval(cb, 50);
77
+ vi.advanceTimersByTime(150);
78
+ expect(cb.mock.calls.length).toBe(3);
79
+ handle.disconnect();
80
+ });
81
+ });
82
+
83
+ describe("scheduleIntervalAfter", () => {
84
+ beforeEach(() => {
85
+ vi.useFakeTimers();
86
+ });
87
+ afterEach(() => {
88
+ vi.useRealTimers();
89
+ });
90
+
91
+ it("fires once after the delay", () => {
92
+ const cb = vi.fn();
93
+ const handle = scheduleIntervalAfter(cb, 100);
94
+ vi.advanceTimersByTime(99);
95
+ expect(cb).not.toHaveBeenCalled();
96
+ vi.advanceTimersByTime(1);
97
+ expect(cb).toHaveBeenCalledTimes(1);
98
+ vi.advanceTimersByTime(500);
99
+ expect(cb).toHaveBeenCalledTimes(1);
100
+ handle.disconnect();
101
+ });
102
+
103
+ it("holds the countdown while hidden", () => {
104
+ const cb = vi.fn();
105
+ const handle = scheduleIntervalAfter(cb, 200);
106
+ vi.advanceTimersByTime(100);
107
+ Object.defineProperty(document, "hidden", { value: true, configurable: true });
108
+ document.dispatchEvent(new Event("visibilitychange"));
109
+ vi.advanceTimersByTime(1000); // hidden: timer parked, no fire
110
+ expect(cb).not.toHaveBeenCalled();
111
+ Object.defineProperty(document, "hidden", { value: false, configurable: true });
112
+ document.dispatchEvent(new Event("visibilitychange"));
113
+ // nextDue was 100ms away at park time; on resume it fires after that.
114
+ vi.advanceTimersByTime(101);
115
+ expect(cb).toHaveBeenCalledTimes(1);
116
+ handle.disconnect();
117
+ });
118
+ });