@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,96 @@
1
+ import { defaultDurationFor, defaultRequireInteraction, type MessagePayload, type MessageTransport } from "./types";
2
+ import { scheduleCronAfter } from "@celestia-island/hikari";
3
+
4
+ /**
5
+ * Browser Notifications API transport.
6
+ *
7
+ * Fires a native OS notification via `window.Notification`. Used when the page
8
+ * is hidden (user is in another tab/app) and the user has granted permission.
9
+ *
10
+ * Lifecycle notes:
11
+ * - `available()` only returns true when the API is present *and* the user
12
+ * has granted permission. Callers should call `requestPermission()` from
13
+ * a user-gesture-triggered flow (e.g. a settings toggle) before relying
14
+ * on this transport — browsers reject permission prompts that aren't
15
+ * initiated by a user gesture.
16
+ * - For error/warning severities we mirror the toast policy: the
17
+ * notification is auto-closed after the same 30s window so the two
18
+ * surfaces feel consistent. Errors additionally request sticky
19
+ * interaction (`requireInteraction: true`) so they survive OS auto-hide.
20
+ * - `onclick` focuses the originating window so the user lands back on the
21
+ * app — this is the bridge back into the SPA from the OS surface.
22
+ */
23
+ export const browserTransport: MessageTransport = {
24
+ name: "browser",
25
+ available: () =>
26
+ typeof Notification !== "undefined" && Notification.permission === "granted",
27
+ send(payload: MessagePayload) {
28
+ if (typeof Notification === "undefined") return;
29
+ if (Notification.permission !== "granted") return;
30
+
31
+ const severity = payload.severity;
32
+ const requireInteraction =
33
+ payload.requireInteraction ?? defaultRequireInteraction(severity);
34
+
35
+ const headlineSource = payload.title || payload.message;
36
+ const title = headlineSource.split("\n")[0]?.slice(0, 200) || headlineSource;
37
+ const body =
38
+ payload.body ||
39
+ (payload.message.length > title.length ? payload.message : undefined);
40
+
41
+ let notification: Notification;
42
+ try {
43
+ notification = new Notification(title, {
44
+ body,
45
+ tag: payload.tag,
46
+ requireInteraction,
47
+ data: payload.data,
48
+ });
49
+ } catch {
50
+ // Some browsers throw if the document isn't focused or service worker
51
+ // is unavailable. Swallow — the toast transport still covers the user.
52
+ return;
53
+ }
54
+
55
+ notification.onclick = () => {
56
+ try {
57
+ window.focus();
58
+ } catch {
59
+ // Cross-origin or no-op — ignore.
60
+ }
61
+ notification.close();
62
+ };
63
+
64
+ // Mirror the toast auto-close window for transient severities. Errors
65
+ // default to sticky (requireInteraction) and are skipped here.
66
+ if (!requireInteraction) {
67
+ const ttl = payload.duration ?? defaultDurationFor(severity);
68
+ if (ttl > 0) {
69
+ scheduleCronAfter(() => {
70
+ try {
71
+ notification.close();
72
+ } catch {
73
+ // Already closed — ignore.
74
+ }
75
+ }, ttl);
76
+ }
77
+ }
78
+ },
79
+ };
80
+
81
+ /**
82
+ * Request browser notification permission. Must be invoked from a user
83
+ * gesture (click handler) — browsers otherwise silently deny.
84
+ *
85
+ * Returns the resulting permission state. The shared permission ref inside
86
+ * {@link useMessaging} is updated as a side effect.
87
+ */
88
+ export async function requestBrowserPermission(): Promise<NotificationPermission> {
89
+ if (typeof Notification === "undefined") return "denied";
90
+ if (Notification.permission !== "default") return Notification.permission;
91
+ try {
92
+ return await Notification.requestPermission();
93
+ } catch {
94
+ return Notification.permission;
95
+ }
96
+ }
@@ -0,0 +1,204 @@
1
+ import { computed, readonly, ref } from "vue";
2
+
3
+ import { browserTransport, requestBrowserPermission } from "./browserTransport";
4
+ import { toastTransport } from "./toastTransport";
5
+ import { defaultCopyable, defaultDurationFor, type MessagePayload, type MessageSeverity, type MessageTransport, type NotifyOptions, type TransportName } from "./types";
6
+
7
+ // Re-export the messaging types from the public entry point so consumers
8
+ // can `import type { MessageSeverity } from "./messaging"`.
9
+ export type {
10
+ MessagePayload,
11
+ MessageSeverity,
12
+ MessageTransport,
13
+ NotifyOptions,
14
+ TransportName,
15
+ } from "./types";
16
+
17
+ /**
18
+ * Registry of all known transports, in priority order.
19
+ *
20
+ * Built-ins (`toast`, `browser`) are pre-registered. Native client bridges
21
+ * (Tauri desktop, mobile native, iOS APNs long-poll) push themselves here
22
+ * via {@link registerTransport} during boot — typically by reading
23
+ * `window.__nativeBridge` and wrapping it in a {@link MessageTransport}.
24
+ */
25
+ const transports: MessageTransport[] = [toastTransport, browserTransport];
26
+
27
+ /** Page visibility — drives the toast-vs-browser routing decision. */
28
+ const _pageHidden = ref(typeof document !== "undefined" && document.hidden);
29
+ if (typeof document !== "undefined") {
30
+ const onVis = () => { _pageHidden.value = document.hidden; };
31
+ document.addEventListener("visibilitychange", onVis);
32
+ }
33
+ const pageHidden = computed(() => _pageHidden.value);
34
+
35
+ /** Live browser notification permission. */
36
+ const browserPermission = ref<NotificationPermission>(
37
+ typeof Notification !== "undefined" ? Notification.permission : "denied",
38
+ );
39
+
40
+ function refreshPermission() {
41
+ if (typeof Notification !== "undefined") {
42
+ browserPermission.value = Notification.permission;
43
+ }
44
+ }
45
+
46
+ let nextId = 0;
47
+
48
+ /**
49
+ * Register a new transport (e.g. a native bridge). Idempotent — re-registering
50
+ * the same name replaces the prior instance and disposes it if supported.
51
+ *
52
+ * Returns an unregister handle so ephemeral bridges (e.g. a reconnecting
53
+ * WebSocket-based push subscription) can tear themselves down cleanly.
54
+ */
55
+ export function registerTransport(transport: MessageTransport): () => void {
56
+ const idx = transports.findIndex((t) => t.name === transport.name);
57
+ if (idx !== -1) {
58
+ const [prev] = transports.splice(idx, 1);
59
+ prev.dispose?.();
60
+ }
61
+ transports.push(transport);
62
+ return () => {
63
+ const i = transports.indexOf(transport);
64
+ if (i !== -1) {
65
+ const [removed] = transports.splice(i, 1);
66
+ removed.dispose?.();
67
+ }
68
+ };
69
+ }
70
+
71
+ /**
72
+ * Register a native bridge discovered on `window.__nativeBridge`. Safe to call
73
+ * unconditionally — no-ops if no bridge is present. Future native client
74
+ * integrations (Tauri `notification` plugin, HarmonyOS `NotificationService`,
75
+ * iOS long-poll subscriber) should hook in here.
76
+ */
77
+ export function registerNativeBridge(): () => void {
78
+ const bridge =
79
+ typeof window !== "undefined"
80
+ ? (window as unknown as { __nativeBridge?: MessageTransport }).__nativeBridge
81
+ : undefined;
82
+ if (!bridge || typeof bridge.send !== "function") return () => {};
83
+ if (bridge.name !== "native") {
84
+ // Coerce the name so routing allow-lists resolve consistently.
85
+ (bridge as MessageTransport).name = "native";
86
+ }
87
+ return registerTransport(bridge);
88
+ }
89
+
90
+ /**
91
+ * Decide which transports a given payload should fire on. Returns the
92
+ * filtered list (in registry order).
93
+ *
94
+ * Routing policy:
95
+ * - `toast`: ALWAYS (it's the in-app surface; cheap and visible when the
96
+ * user returns).
97
+ * - `browser`: ONLY when the page is hidden AND permission is granted AND
98
+ * the severity is actionable off-page (error/warning/success). Pure info
99
+ * pings don't earn an OS-level interruption.
100
+ * - `native`: ALWAYS when available (the native shell decides how to render
101
+ * based on its own focus state — we don't second-guess it from the web
102
+ * layer).
103
+ */
104
+ function routePayload(
105
+ payload: MessagePayload,
106
+ allowList: TransportName[] | "all",
107
+ ): MessageTransport[] {
108
+ const allow = allowList === "all" ? null : new Set(allowList);
109
+ const out: MessageTransport[] = [];
110
+ for (const t of transports) {
111
+ if (allow && !allow.has(t.name)) continue;
112
+ if (!t.available()) continue;
113
+ if (t.name === "browser") {
114
+ const actionable =
115
+ payload.severity === "error" ||
116
+ payload.severity === "warning" ||
117
+ payload.severity === "success";
118
+ if (!payload.pageHidden || !actionable) continue;
119
+ }
120
+ out.push(t);
121
+ }
122
+ return out;
123
+ }
124
+
125
+ /**
126
+ * Unified messaging context. Drop-in replacement for {@link useToast} at call
127
+ * sites that want cross-transport routing — the helper signatures
128
+ * (`error`/`warning`/`success`/`info`) mirror the toast API to make migration
129
+ * mechanical.
130
+ *
131
+ * @example
132
+ * const msg = useMessaging();
133
+ * msg.error(t("auth.login.failed")); // top-right toast + (if hidden) browser push
134
+ * msg.warning("Disk almost full", { tag: "disk" });
135
+ * msg.notify("Saved", { severity: "success", transports: ["toast"] }); // in-app only
136
+ */
137
+ export function useMessaging() {
138
+ function notify(message: string, options: NotifyOptions = {}): number {
139
+ const severity: MessageSeverity = options.severity || "info";
140
+ const payload: MessagePayload = {
141
+ id: ++nextId,
142
+ severity,
143
+ message,
144
+ title: options.title,
145
+ body: options.body,
146
+ tag: options.tag,
147
+ requireInteraction: options.requireInteraction,
148
+ copyable: options.copyable ?? defaultCopyable(severity),
149
+ duration: options.duration ?? defaultDurationFor(severity),
150
+ data: options.data,
151
+ timestamp: Date.now(),
152
+ pageHidden: pageHidden.value,
153
+ };
154
+
155
+ const routed = routePayload(payload, options.transports ?? "all");
156
+ for (const t of routed) {
157
+ try {
158
+ t.send(payload);
159
+ } catch {
160
+ // Transport failure must never propagate — the next transport still
161
+ // gets a chance.
162
+ }
163
+ }
164
+ return payload.id;
165
+ }
166
+
167
+ function error(message: string, options: Omit<NotifyOptions, "severity"> = {}) {
168
+ return notify(message, { ...options, severity: "error" });
169
+ }
170
+ function warning(message: string, options: Omit<NotifyOptions, "severity"> = {}) {
171
+ return notify(message, { ...options, severity: "warning" });
172
+ }
173
+ function success(message: string, options: Omit<NotifyOptions, "severity"> = {}) {
174
+ return notify(message, { ...options, severity: "success" });
175
+ }
176
+ function info(message: string, options: Omit<NotifyOptions, "severity"> = {}) {
177
+ return notify(message, { ...options, severity: "info" });
178
+ }
179
+
180
+ /**
181
+ * Request browser notification permission. Must be called from a user
182
+ * gesture handler (button click) — otherwise browsers silently deny.
183
+ */
184
+ async function ensureBrowserPermission(): Promise<NotificationPermission> {
185
+ const result = await requestBrowserPermission();
186
+ refreshPermission();
187
+ return result;
188
+ }
189
+
190
+ return {
191
+ notify,
192
+ error,
193
+ warning,
194
+ success,
195
+ info,
196
+ ensureBrowserPermission,
197
+ /** Whether the browser tab is currently hidden. Reactive. */
198
+ pageHidden: readonly(pageHidden),
199
+ /** Live browser notification permission. Reactive. */
200
+ browserPermission: readonly(browserPermission),
201
+ /** Register a transport at runtime (e.g. native bridge). */
202
+ registerTransport,
203
+ };
204
+ }
@@ -0,0 +1,256 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ import { useToast } from "@celestia-island/hikari";
4
+ import { registerNativeBridge, registerTransport, useMessaging } from "./index";
5
+ import type { MessagePayload, MessageTransport } from "./types";
6
+
7
+ function flushToasts() {
8
+ const { toasts, remove } = useToast();
9
+ while (toasts.length > 0) remove(toasts[0].id);
10
+ }
11
+
12
+ function makeStubTransport(name: MessageTransport["name"]): MessageTransport & {
13
+ calls: MessagePayload[];
14
+ } {
15
+ const calls: MessagePayload[] = [];
16
+ return {
17
+ name,
18
+ available: () => true,
19
+ send: (p) => calls.push(p),
20
+ calls,
21
+ };
22
+ }
23
+
24
+ describe("useMessaging", () => {
25
+ beforeEach(() => {
26
+ flushToasts();
27
+ });
28
+
29
+ afterEach(() => {
30
+ vi.restoreAllMocks();
31
+ });
32
+
33
+ describe("severity helpers", () => {
34
+ it("error() routes through the toast transport by default", () => {
35
+ const { toasts } = useToast();
36
+ const { error } = useMessaging();
37
+ error("boom");
38
+ expect(toasts).toHaveLength(1);
39
+ expect(toasts[0].type).toBe("error");
40
+ expect(toasts[0].messages[0].text).toBe("boom");
41
+ expect(toasts[0].duration).toBe(30_000);
42
+ expect(toasts[0].copyable).toBe(true);
43
+ });
44
+
45
+ it("warning() shares the 30s long-lived duration", () => {
46
+ const { toasts } = useToast();
47
+ const { warning } = useMessaging();
48
+ warning("careful");
49
+ expect(toasts[0].type).toBe("warning");
50
+ expect(toasts[0].duration).toBe(30_000);
51
+ });
52
+
53
+ it("success/info keep the short duration", () => {
54
+ const { toasts } = useToast();
55
+ const { success, info } = useMessaging();
56
+ success("yep");
57
+ expect(toasts[0].duration).toBe(3_000);
58
+ info("hi");
59
+ expect(toasts[1].duration).toBe(3_000);
60
+ });
61
+
62
+ it("notify() accepts an explicit duration override", () => {
63
+ const { toasts } = useToast();
64
+ const { notify } = useMessaging();
65
+ notify("manual", { severity: "error", duration: 1_000 });
66
+ expect(toasts[0].duration).toBe(1_000);
67
+ });
68
+ });
69
+
70
+ describe("transport allow-list", () => {
71
+ it("respects transports: ['toast'] to suppress other channels", () => {
72
+ const stub = makeStubTransport("native");
73
+ const unregister = registerTransport(stub);
74
+ const { notify } = useMessaging();
75
+ notify("hi", { severity: "info", transports: ["toast"] });
76
+ expect(stub.calls).toHaveLength(0);
77
+ unregister();
78
+ });
79
+
80
+ it("still routes to whitelisted transports", () => {
81
+ const stub = makeStubTransport("native");
82
+ const unregister = registerTransport(stub);
83
+ const { notify } = useMessaging();
84
+ notify("hi", { severity: "info", transports: ["toast", "native"] });
85
+ expect(stub.calls).toHaveLength(1);
86
+ unregister();
87
+ });
88
+ });
89
+
90
+ describe("registerTransport", () => {
91
+ it("is idempotent on name and replaces the prior transport", () => {
92
+ const a = makeStubTransport("native");
93
+ const b = makeStubTransport("native");
94
+ const unA = registerTransport(a);
95
+ const unB = registerTransport(b);
96
+ const { notify } = useMessaging();
97
+ notify("x", { severity: "info", transports: ["native"] });
98
+ expect(a.calls).toHaveLength(0);
99
+ expect(b.calls).toHaveLength(1);
100
+ unB();
101
+ unA();
102
+ });
103
+
104
+ it("unregister handle removes the transport", () => {
105
+ const stub = makeStubTransport("native");
106
+ const unregister = registerTransport(stub);
107
+ const { notify } = useMessaging();
108
+ notify("x", { severity: "info", transports: ["native"] });
109
+ expect(stub.calls).toHaveLength(1);
110
+ stub.calls.length = 0;
111
+ unregister();
112
+ notify("y", { severity: "info", transports: ["native"] });
113
+ expect(stub.calls).toHaveLength(0);
114
+ });
115
+
116
+ it("skips transports that report unavailable", () => {
117
+ const unavailableStub: MessageTransport = {
118
+ name: "native",
119
+ available: () => false,
120
+ send: vi.fn(),
121
+ };
122
+ const unregister = registerTransport(unavailableStub);
123
+ const { notify } = useMessaging();
124
+ notify("x", { severity: "info", transports: ["native"] });
125
+ expect(unavailableStub.send).not.toHaveBeenCalled();
126
+ unregister();
127
+ });
128
+
129
+ it("isolates a transport failure — other transports still fire", () => {
130
+ const exploding: MessageTransport = {
131
+ name: "native",
132
+ available: () => true,
133
+ send: () => {
134
+ throw new Error("boom");
135
+ },
136
+ };
137
+ const un = registerTransport(exploding);
138
+ const { toasts } = useToast();
139
+ const { notify } = useMessaging();
140
+ expect(() => notify("x", { severity: "info" })).not.toThrow();
141
+ // Toast transport should still have fired despite the native throw.
142
+ expect(toasts.length).toBeGreaterThan(0);
143
+ un();
144
+ });
145
+ });
146
+
147
+ describe("registerNativeBridge", () => {
148
+ it("no-ops when window.__nativeBridge is absent", () => {
149
+ const w = window as unknown as { __nativeBridge?: unknown };
150
+ const original = w.__nativeBridge;
151
+ delete w.__nativeBridge;
152
+ try {
153
+ const unregister = registerNativeBridge();
154
+ expect(typeof unregister).toBe("function");
155
+ // With no native bridge registered, a message restricted to the
156
+ // "native" allow-list must not be delivered to ANY transport —
157
+ // including the always-present toast surface.
158
+ const { toasts } = useToast();
159
+ const { notify } = useMessaging();
160
+ notify("native hi", { severity: "info", transports: ["native"] });
161
+ expect(toasts).toHaveLength(0);
162
+ unregister();
163
+ } finally {
164
+ if (original !== undefined) w.__nativeBridge = original;
165
+ }
166
+ });
167
+
168
+ it("registers a discovered bridge under the 'native' name", () => {
169
+ const calls: MessagePayload[] = [];
170
+ const bridge = {
171
+ name: "custom" as const,
172
+ available: () => true,
173
+ send: (p: MessagePayload) => calls.push(p),
174
+ };
175
+ const w = window as unknown as { __nativeBridge?: unknown };
176
+ const original = w.__nativeBridge;
177
+ w.__nativeBridge = bridge;
178
+ try {
179
+ const unregister = registerNativeBridge();
180
+ const { notify } = useMessaging();
181
+ notify("native hi", { severity: "warning", transports: ["native"] });
182
+ expect(bridge.name).toBe("native");
183
+ expect(calls).toHaveLength(1);
184
+ expect(calls[0].severity).toBe("warning");
185
+ unregister();
186
+ } finally {
187
+ if (original !== undefined) w.__nativeBridge = original;
188
+ else delete w.__nativeBridge;
189
+ }
190
+ });
191
+ });
192
+
193
+ describe("browser transport gating", () => {
194
+ it("does not throw when Notification is unavailable (SSR-like)", () => {
195
+ const w = window as unknown as { Notification?: unknown };
196
+ const original = w.Notification;
197
+ delete w.Notification;
198
+ try {
199
+ const { toasts } = useToast();
200
+ const { error } = useMessaging();
201
+ expect(() => error("no Notification API")).not.toThrow();
202
+ // Toast still covers the user.
203
+ expect(toasts).toHaveLength(1);
204
+ } finally {
205
+ if (original !== undefined) w.Notification = original;
206
+ }
207
+ });
208
+
209
+ it("defaults requireInteraction to true only for errors", async () => {
210
+ const created: Array<{ title: string; requireInteraction: boolean }> = [];
211
+ const w = window as unknown as {
212
+ Notification?: typeof Notification;
213
+ };
214
+ const Original = w.Notification;
215
+ class FakeNotification {
216
+ static permission: NotificationPermission = "granted";
217
+ static requestPermission = vi.fn().mockResolvedValue("granted");
218
+ title: string;
219
+ options?: NotificationOptions;
220
+ onclick: (() => void) | null = null;
221
+ close() {}
222
+ constructor(title: string, options?: NotificationOptions) {
223
+ this.title = title;
224
+ this.options = options;
225
+ created.push({ title, requireInteraction: !!options?.requireInteraction });
226
+ }
227
+ }
228
+ w.Notification = FakeNotification as unknown as typeof Notification;
229
+ try {
230
+ // Exercise the browser transport's `send()` directly to assert the
231
+ // severity-driven requireInteraction default (the public routing
232
+ // layer is covered elsewhere).
233
+ const { browserTransport } = await import("./browserTransport");
234
+ browserTransport.send({
235
+ id: 1,
236
+ severity: "error",
237
+ message: "boom",
238
+ timestamp: Date.now(),
239
+ pageHidden: true,
240
+ });
241
+ browserTransport.send({
242
+ id: 2,
243
+ severity: "warning",
244
+ message: "careful",
245
+ timestamp: Date.now(),
246
+ pageHidden: true,
247
+ });
248
+ expect(created).toHaveLength(2);
249
+ expect(created[0].requireInteraction).toBe(true); // error → sticky
250
+ expect(created[1].requireInteraction).toBe(false); // warning → transient
251
+ } finally {
252
+ if (Original !== undefined) w.Notification = Original;
253
+ }
254
+ });
255
+ });
256
+ });
@@ -0,0 +1,26 @@
1
+ import { useToast } from "@celestia-island/hikari";
2
+ import type { MessagePayload, MessageTransport } from "./types";
3
+
4
+ /**
5
+ * In-app toast transport. Always available while the SPA is mounted — the
6
+ * toast container lives in {@link AppShell} and is teleport-bound to
7
+ * `document.body`, so it renders even when individual route views are
8
+ * unmounted.
9
+ *
10
+ * The toast is the canonical surface for user feedback: even when the page
11
+ * is hidden and a browser notification fires, the same payload is also pushed
12
+ * here so the user sees it the moment they return (within the 30s
13
+ * warning/error window).
14
+ */
15
+ export const toastTransport: MessageTransport = {
16
+ name: "toast",
17
+ available: () => true,
18
+ send(payload: MessagePayload) {
19
+ const toast = useToast();
20
+ toast.show(payload.message, {
21
+ type: payload.severity,
22
+ duration: payload.duration,
23
+ copyable: payload.copyable,
24
+ });
25
+ },
26
+ };
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Unified messaging context — types & contracts.
3
+ *
4
+ * The messaging context routes user-facing notifications through one or more
5
+ * {@link MessageTransport} adapters. The routing decision is driven by the
6
+ * page-visibility state and the per-transport availability (e.g. browser
7
+ * notification permission).
8
+ *
9
+ * Built-in transports:
10
+ * - {@link "toast"} — always available; renders via {@link useToast} in the
11
+ * top-right corner.
12
+ * - {@link "browser"} — the Web Notifications API; used when the page is
13
+ * hidden and the user has granted permission.
14
+ *
15
+ * Future transports (desktop/mobile/iOS long-poll) will be registered via
16
+ * {@link registerTransport} and follow the same {@link MessageTransport}
17
+ * contract. Native bridges are expected to expose themselves on
18
+ * `window.__nativeBridge` and call {@link registerTransport} at boot — the
19
+ * messaging layer is transport-agnostic.
20
+ */
21
+
22
+ /** User-facing severity. Mirrors {@link ToastType} so toasts stay the source of truth. */
23
+ export type MessageSeverity = "error" | "warning" | "success" | "info" | "loading";
24
+
25
+ /** Transport identifiers used by the routing layer. */
26
+ export type TransportName = "toast" | "browser" | "native";
27
+
28
+ /**
29
+ * Options accepted by {@link useMessaging}'s helpers.
30
+ *
31
+ * Most fields are transport-agnostic; transports pick the ones they understand
32
+ * (e.g. the browser transport uses {@link title} as the notification headline
33
+ * while the toast transport only renders {@link message}).
34
+ */
35
+ export interface NotifyOptions {
36
+ severity?: MessageSeverity;
37
+ /** Short headline. Used by browser/native transports; toast falls back to {@link message}. */
38
+ title?: string;
39
+ /** Long-form body. Used by browser/native transports when set. */
40
+ body?: string;
41
+ /** Toast auto-close ms. `0` keeps the toast sticky. Defaults to severity-based duration. */
42
+ duration?: number;
43
+ /** Toast: show the copy button. Defaults to `true` for error/warning. */
44
+ copyable?: boolean;
45
+ /** Browser: dedup tag — same-tag notifications replace each other. */
46
+ tag?: string;
47
+ /** Browser: stay open until user dismisses. Defaults to severity-driven. */
48
+ requireInteraction?: boolean;
49
+ /**
50
+ * Restrict routing to a subset of transports. `"all"` (default) lets the
51
+ * visibility/permission gate decide. Useful for low-priority pings that
52
+ * should never interrupt the user off-page (e.g. `transports: ["toast"]`).
53
+ */
54
+ transports?: TransportName[] | "all";
55
+ /**
56
+ * Opaque payload reserved for future native transports (desktop IPC, mobile
57
+ * push subscription, iOS APNs long-poll). Passed through untouched.
58
+ */
59
+ data?: Record<string, unknown>;
60
+ }
61
+
62
+ /** Fully-resolved message handed to each transport. */
63
+ export interface MessagePayload {
64
+ id: number;
65
+ severity: MessageSeverity;
66
+ /** Primary text — always populated. */
67
+ message: string;
68
+ title?: string;
69
+ body?: string;
70
+ tag?: string;
71
+ requireInteraction?: boolean;
72
+ copyable?: boolean;
73
+ duration?: number;
74
+ data?: Record<string, unknown>;
75
+ /** Epoch ms — useful for native transports that queue/batch. */
76
+ timestamp: number;
77
+ /** Whether the page was hidden when this message was dispatched. */
78
+ pageHidden: boolean;
79
+ }
80
+
81
+ /**
82
+ * Contract every push channel must implement.
83
+ *
84
+ * Transports are stateless from the registry's perspective: they receive a
85
+ * resolved payload and decide for themselves how to render it. Availability
86
+ * (e.g. permission, native bridge presence) is checked before each send so
87
+ * transports can degrade gracefully without re-registration.
88
+ */
89
+ export interface MessageTransport {
90
+ /** Stable identifier — used by {@link NotifyOptions.transports} allow-lists. */
91
+ name: TransportName;
92
+ /** Whether this transport can be used right now (permission, bridge present, …). */
93
+ available(): boolean;
94
+ /** Issue the message. Must not throw — wrap failures in a no-op. */
95
+ send(payload: MessagePayload): void;
96
+ /** Optional teardown for transports that own resources (event listeners, etc.). */
97
+ dispose?(): void;
98
+ }
99
+
100
+ /** Convenience: severity → default toast duration (mirrors {@link TOAST_DURATION}). */
101
+ export function defaultDurationFor(severity: MessageSeverity): number {
102
+ if (severity === "error" || severity === "warning") return 30_000;
103
+ if (severity === "loading") return 0;
104
+ return 3_000;
105
+ }
106
+
107
+ /** Convenience: severity → whether a browser notification should stay sticky. */
108
+ export function defaultRequireInteraction(severity: MessageSeverity): boolean {
109
+ return severity === "error";
110
+ }
111
+
112
+ /** Convenience: severity → default copyable flag for the toast surface. */
113
+ export function defaultCopyable(severity: MessageSeverity): boolean {
114
+ return severity === "error" || severity === "warning";
115
+ }