@celestia-island/hikari 0.4.4 → 0.4.5

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 (152) hide show
  1. package/README.md +1 -1
  2. package/package.json +16 -7
  3. package/src/DemoApp.vue +1 -1
  4. package/src/components/HkAdminHeader.tsx +205 -0
  5. package/src/components/HkAdminShell.tsx +85 -0
  6. package/src/components/HkAdminTablePage.tsx +70 -0
  7. package/src/components/HkAuthCard.tsx +36 -0
  8. package/src/components/HkAuthSubmitButton.scss +12 -0
  9. package/src/components/HkAuthSubmitButton.tsx +94 -0
  10. package/src/components/HkAvatar.scss +93 -0
  11. package/src/components/HkAvatar.tsx +1 -1
  12. package/src/components/HkBreadcrumb.scss +624 -0
  13. package/src/components/HkBreadcrumb.tsx +1 -1
  14. package/src/components/HkCard.scss +1 -1
  15. package/src/components/HkCheckbox.tsx +2 -1
  16. package/src/components/HkCollapse.scss +50 -0
  17. package/src/components/HkCollapse.tsx +1 -1
  18. package/src/components/HkColorSchemeDialog.scss +16 -0
  19. package/src/components/HkColorSchemeDialog.tsx +237 -0
  20. package/src/components/HkConnectionInfo.ts +80 -0
  21. package/src/components/HkConnectionStatus.tsx +133 -0
  22. package/src/components/HkCountdownDigit.tsx +97 -0
  23. package/src/components/HkDateTimePicker.tsx +7 -1
  24. package/src/components/HkErrorBoundary.tsx +4 -1
  25. package/src/components/HkIcon.scss +123 -0
  26. package/src/components/HkIcon.tsx +7 -23
  27. package/src/components/HkIconButton.scss +170 -0
  28. package/src/components/HkIconButton.tsx +2 -2
  29. package/src/components/HkIconButtonVars.scss +176 -0
  30. package/src/components/HkInput.scss +22 -3
  31. package/src/components/HkInput.tsx +41 -1
  32. package/src/components/HkKeywordSearchModal.tsx +10 -7
  33. package/src/components/HkLocalePicker.tsx +113 -0
  34. package/src/components/HkMediaControlBar.tsx +14 -7
  35. package/src/components/HkMinimap.tsx +10 -2
  36. package/src/components/HkModal.tsx +2 -23
  37. package/src/components/HkNavItem.tsx +2 -1
  38. package/src/components/HkNavSidebar.tsx +99 -0
  39. package/src/components/HkPageHeader.tsx +40 -0
  40. package/src/components/HkPasswordInput.scss +14 -12
  41. package/src/components/HkPasswordInput.test.ts +242 -0
  42. package/src/components/HkPasswordInput.tsx +236 -41
  43. package/src/components/HkPopupSelect.scss +3 -3
  44. package/src/components/HkPopupSelect.tsx +2 -1
  45. package/src/components/HkSearchInput.tsx +2 -2
  46. package/src/components/HkSecretRevealModal.scss +61 -0
  47. package/src/components/HkSecretRevealModal.tsx +91 -0
  48. package/src/components/HkSelect.scss +4 -4
  49. package/src/components/HkSelect.tsx +2 -0
  50. package/src/components/HkSelectionGrid.tsx +2 -1
  51. package/src/components/HkStatusBar.tsx +235 -0
  52. package/src/components/HkTag.tsx +2 -1
  53. package/src/components/HkThemeToggle.scss +130 -0
  54. package/src/components/HkThemeToggle.tsx +160 -0
  55. package/src/components/HkTimeline.tsx +2 -1
  56. package/src/components/HkToast.tsx +4 -1
  57. package/src/components/HkTrendChart.tsx +27 -16
  58. package/src/components/HkZoomToolbar.tsx +4 -1
  59. package/src/components/_hikari-vars.scss +4 -0
  60. package/src/composables/highlightLanguages.ts +109 -0
  61. package/src/composables/iconRegistry.ts +127 -0
  62. package/src/composables/messaging/browserTransport.ts +96 -0
  63. package/src/composables/messaging/index.ts +204 -0
  64. package/src/composables/messaging/messaging.test.ts +256 -0
  65. package/src/composables/messaging/toastTransport.ts +26 -0
  66. package/src/composables/messaging/types.ts +115 -0
  67. package/src/composables/useActionBar.ts +66 -0
  68. package/src/composables/useConnectionProbe.ts +126 -0
  69. package/src/composables/useDeferredTransition.ts +16 -0
  70. package/src/composables/useEngineHealth.ts +69 -0
  71. package/src/composables/useHighlight.ts +124 -0
  72. package/src/composables/usePageTitle.ts +37 -0
  73. package/src/composables/useReducedMotion.ts +34 -0
  74. package/src/composables/useResourceListModal.tsx +147 -0
  75. package/src/composables/useZoomPan.ts +192 -0
  76. package/src/i18n/locales/ar/components.json +12 -3
  77. package/src/i18n/locales/ar/components.toml +5 -0
  78. package/src/i18n/locales/ar/connection.json +25 -0
  79. package/src/i18n/locales/ar/platform.json +50 -0
  80. package/src/i18n/locales/de/components.json +12 -3
  81. package/src/i18n/locales/de/components.toml +5 -0
  82. package/src/i18n/locales/de/connection.json +25 -0
  83. package/src/i18n/locales/de/platform.json +50 -0
  84. package/src/i18n/locales/en/components.json +12 -3
  85. package/src/i18n/locales/en/components.toml +5 -0
  86. package/src/i18n/locales/en/connection.json +27 -0
  87. package/src/i18n/locales/en/platform.json +53 -0
  88. package/src/i18n/locales/es/components.json +12 -3
  89. package/src/i18n/locales/es/components.toml +5 -0
  90. package/src/i18n/locales/es/connection.json +25 -0
  91. package/src/i18n/locales/es/platform.json +50 -0
  92. package/src/i18n/locales/fr/components.json +12 -3
  93. package/src/i18n/locales/fr/components.toml +5 -0
  94. package/src/i18n/locales/fr/connection.json +25 -0
  95. package/src/i18n/locales/fr/platform.json +50 -0
  96. package/src/i18n/locales/ja/components.json +12 -3
  97. package/src/i18n/locales/ja/components.toml +5 -0
  98. package/src/i18n/locales/ja/connection.json +27 -0
  99. package/src/i18n/locales/ja/platform.json +53 -0
  100. package/src/i18n/locales/ko/components.json +12 -3
  101. package/src/i18n/locales/ko/components.toml +5 -0
  102. package/src/i18n/locales/ko/connection.json +27 -0
  103. package/src/i18n/locales/ko/platform.json +50 -0
  104. package/src/i18n/locales/pt/components.json +12 -3
  105. package/src/i18n/locales/pt/components.toml +5 -0
  106. package/src/i18n/locales/pt/connection.json +25 -0
  107. package/src/i18n/locales/pt/platform.json +50 -0
  108. package/src/i18n/locales/ru/components.json +12 -3
  109. package/src/i18n/locales/ru/components.toml +5 -0
  110. package/src/i18n/locales/ru/connection.json +25 -0
  111. package/src/i18n/locales/ru/platform.json +50 -0
  112. package/src/i18n/locales/zh-Hans/components.json +12 -3
  113. package/src/i18n/locales/zh-Hans/components.toml +5 -0
  114. package/src/i18n/locales/zh-Hans/connection.json +27 -0
  115. package/src/i18n/locales/zh-Hans/platform.json +53 -0
  116. package/src/i18n/locales/zh-Hant/components.json +12 -3
  117. package/src/i18n/locales/zh-Hant/components.toml +5 -0
  118. package/src/i18n/locales/zh-Hant/connection.json +27 -0
  119. package/src/i18n/locales/zh-Hant/platform.json +53 -0
  120. package/src/index.ts +67 -4
  121. package/src/plugins/bootScriptsPlugin.ts +47 -0
  122. package/src/plugins/buildHashPlugin.ts +67 -0
  123. package/src/plugins/index.ts +2 -0
  124. package/src/render.ts +1 -1
  125. package/src/runtime/env.ts +4 -0
  126. package/src/runtime/index.ts +0 -1
  127. package/src/runtime/useConfirm.ts +7 -1
  128. package/src/styles/admin-tokens.scss +634 -0
  129. package/src/styles/index.scss +1 -0
  130. package/src/theme/_field.scss +116 -0
  131. package/src/theme/index.ts +2 -0
  132. package/src/theme/presets.ts +11 -9
  133. package/src/theme/useTheme.ts +54 -5
  134. package/src/tokens.scss +76 -0
  135. package/src/utils/base64.ts +31 -0
  136. package/src/utils/connectivity.ts +51 -0
  137. package/src/utils/dom.ts +39 -0
  138. package/src/utils/download.ts +22 -0
  139. package/src/utils/fileType.ts +127 -0
  140. package/src/utils/fuzzy.ts +77 -0
  141. package/src/utils/fuzzySearch.test.ts +87 -0
  142. package/src/utils/objectPath.ts +59 -0
  143. package/src/utils/password.ts +53 -0
  144. package/src/utils/pow.test.ts +45 -0
  145. package/src/utils/pow.ts +226 -0
  146. package/src/utils/powNonce.ts +83 -0
  147. package/src/utils/validation.test.ts +78 -0
  148. package/src/composables/useBreakpoint.ts +0 -41
  149. package/src/composables/useClipboard.ts +0 -37
  150. package/src/composables/useConfirm.ts +0 -35
  151. package/src/composables/useToast.ts +0 -2
  152. package/src/runtime/useAsyncData.ts +0 -30
@@ -0,0 +1,109 @@
1
+ // Static map of highlight.js language id → lazy dynamic import.
2
+ //
3
+ // Each entry is a LITERAL `import("highlight.js/lib/languages/<id>")` so Vite
4
+ // emits it as its own chunk, fetched only when a code block actually requests
5
+ // that language. A variable dynamic import (`import(`...languages/${name}`)`)
6
+ // does NOT get bundled by Vite for node_modules, so the map must be explicit.
7
+ //
8
+ // Covers the languages encountered in practice; anything not listed falls
9
+ // back to plaintext (per "没加载就是保持不变色"). Extend freely — each added
10
+ // entry is just one more on-demand chunk.
11
+
12
+ import type { LanguageFn } from "highlight.js";
13
+
14
+ type Loader = () => Promise<{ default: LanguageFn }>;
15
+
16
+ export const LANGUAGE_LOADERS: Record<string, Loader> = {
17
+ // ── general-purpose programming ──
18
+ javascript: () => import("highlight.js/lib/languages/javascript"),
19
+ typescript: () => import("highlight.js/lib/languages/typescript"),
20
+ python: () => import("highlight.js/lib/languages/python"),
21
+ pythonRepl: () => import("highlight.js/lib/languages/python-repl"),
22
+ java: () => import("highlight.js/lib/languages/java"),
23
+ kotlin: () => import("highlight.js/lib/languages/kotlin"),
24
+ scala: () => import("highlight.js/lib/languages/scala"),
25
+ groovy: () => import("highlight.js/lib/languages/groovy"),
26
+ c: () => import("highlight.js/lib/languages/c"),
27
+ cpp: () => import("highlight.js/lib/languages/cpp"),
28
+ csharp: () => import("highlight.js/lib/languages/csharp"),
29
+ fsharp: () => import("highlight.js/lib/languages/fsharp"),
30
+ go: () => import("highlight.js/lib/languages/go"),
31
+ rust: () => import("highlight.js/lib/languages/rust"),
32
+ ruby: () => import("highlight.js/lib/languages/ruby"),
33
+ php: () => import("highlight.js/lib/languages/php"),
34
+ swift: () => import("highlight.js/lib/languages/swift"),
35
+ objectivec: () => import("highlight.js/lib/languages/objectivec"),
36
+ dart: () => import("highlight.js/lib/languages/dart"),
37
+ elixir: () => import("highlight.js/lib/languages/elixir"),
38
+ erlang: () => import("highlight.js/lib/languages/erlang"),
39
+ clojure: () => import("highlight.js/lib/languages/clojure"),
40
+ lisp: () => import("highlight.js/lib/languages/lisp"),
41
+ scheme: () => import("highlight.js/lib/languages/scheme"),
42
+ haskell: () => import("highlight.js/lib/languages/haskell"),
43
+ elm: () => import("highlight.js/lib/languages/elm"),
44
+ lua: () => import("highlight.js/lib/languages/lua"),
45
+ perl: () => import("highlight.js/lib/languages/perl"),
46
+ r: () => import("highlight.js/lib/languages/r"),
47
+ matlab: () => import("highlight.js/lib/languages/matlab"),
48
+ julia: () => import("highlight.js/lib/languages/julia"),
49
+ nim: () => import("highlight.js/lib/languages/nim"),
50
+ crystal: () => import("highlight.js/lib/languages/crystal"),
51
+ ocaml: () => import("highlight.js/lib/languages/ocaml"),
52
+ vbnet: () => import("highlight.js/lib/languages/vbnet"),
53
+ delphi: () => import("highlight.js/lib/languages/delphi"),
54
+ basic: () => import("highlight.js/lib/languages/basic"),
55
+ fortran: () => import("highlight.js/lib/languages/fortran"),
56
+ coffeescript: () => import("highlight.js/lib/languages/coffeescript"),
57
+ livescript: () => import("highlight.js/lib/languages/livescript"),
58
+
59
+ // ── web / markup / templating ──
60
+ xml: () => import("highlight.js/lib/languages/xml"),
61
+ css: () => import("highlight.js/lib/languages/css"),
62
+ scss: () => import("highlight.js/lib/languages/scss"),
63
+ less: () => import("highlight.js/lib/languages/less"),
64
+ json: () => import("highlight.js/lib/languages/json"),
65
+ yaml: () => import("highlight.js/lib/languages/yaml"),
66
+ markdown: () => import("highlight.js/lib/languages/markdown"),
67
+ handlebars: () => import("highlight.js/lib/languages/handlebars"),
68
+ twig: () => import("highlight.js/lib/languages/twig"),
69
+ dust: () => import("highlight.js/lib/languages/dust"),
70
+ haml: () => import("highlight.js/lib/languages/haml"),
71
+ erb: () => import("highlight.js/lib/languages/erb"),
72
+ graphql: () => import("highlight.js/lib/languages/graphql"),
73
+ http: () => import("highlight.js/lib/languages/http"),
74
+
75
+ // ── shell / scripting / build ──
76
+ bash: () => import("highlight.js/lib/languages/bash"),
77
+ shell: () => import("highlight.js/lib/languages/shell"),
78
+ powershell: () => import("highlight.js/lib/languages/powershell"),
79
+ dos: () => import("highlight.js/lib/languages/dos"),
80
+ makefile: () => import("highlight.js/lib/languages/makefile"),
81
+ cmake: () => import("highlight.js/lib/languages/cmake"),
82
+ dockerfile: () => import("highlight.js/lib/languages/dockerfile"),
83
+ nginx: () => import("highlight.js/lib/languages/nginx"),
84
+ gradle: () => import("highlight.js/lib/languages/gradle"),
85
+ awk: () => import("highlight.js/lib/languages/awk"),
86
+ tcl: () => import("highlight.js/lib/languages/tcl"),
87
+ vim: () => import("highlight.js/lib/languages/vim"),
88
+
89
+ // ── data / config ──
90
+ ini: () => import("highlight.js/lib/languages/ini"),
91
+ toml: () => import("highlight.js/lib/languages/ini"),
92
+ properties: () => import("highlight.js/lib/languages/properties"),
93
+ protobuf: () => import("highlight.js/lib/languages/protobuf"),
94
+ thrift: () => import("highlight.js/lib/languages/thrift"),
95
+ dns: () => import("highlight.js/lib/languages/dns"),
96
+
97
+ // ── systems / hardware / other ──
98
+ sql: () => import("highlight.js/lib/languages/sql"),
99
+ pgsql: () => import("highlight.js/lib/languages/pgsql"),
100
+ glsl: () => import("highlight.js/lib/languages/glsl"),
101
+ wasm: () => import("highlight.js/lib/languages/wasm"),
102
+ armasm: () => import("highlight.js/lib/languages/armasm"),
103
+ x86asm: () => import("highlight.js/lib/languages/x86asm"),
104
+ verilog: () => import("highlight.js/lib/languages/verilog"),
105
+ vhdl: () => import("highlight.js/lib/languages/vhdl"),
106
+ latex: () => import("highlight.js/lib/languages/latex"),
107
+ diff: () => import("highlight.js/lib/languages/diff"),
108
+ plaintext: () => import("highlight.js/lib/languages/plaintext"),
109
+ };
@@ -0,0 +1,127 @@
1
+ // Shared, tree-shakeable lucide icon registry.
2
+ //
3
+ // `import * as LucideIcons` (wildcard) defeats tree-shaking and pulls the
4
+ // ENTIRE lucide library (~1500 icons, 800KB+) into the bundle. This module
5
+ // imports only the icons the app actually uses — plus a small set of common
6
+ // status/indicator icons server-pushed badges may reference — keyed by their
7
+ // PascalCase lucide export name so dynamic `byName(str)` lookups (breadcrumb
8
+ // badges, nav tags) resolve without the wildcard import. Unknown names fall
9
+ // back to `Info`, matching the previous `|| LucideIcons.Info` behavior.
10
+
11
+ import { Activity, ArrowDown, ArrowLeft, ArrowRight, ArrowUp, Battery, Bell, Bot, Box, Brain, Cable, Calendar, ChartBarBig as BarChart3, Check, ChevronDown, ChevronLeft, ChevronRight, ChevronUp, Circle, CircleAlert as AlertCircle, CircleCheck as CheckCircle, CircleCheckBig as CheckCircle2, CircleDot, Clock, Cpu, Database, Dot, Ellipsis as MoreHorizontal, ExternalLink, Eye, FileArchive, FileCode, FileImage, FileText, Flame, FolderOpen, Gauge, Globe, HardDrive, Info, Kanban, Key, Layers, LayoutDashboard, LoaderCircle as Loader2, Lock, Maximize, Maximize2, MessageSquare, Mic, MicOff, Minimize, Monitor, Network, Package, Pause, Pencil, Play, Plug, Plus, Radio, RefreshCw, Search, Send, Server, Settings, Share2, Shield, Star, Table, Tag, Thermometer, Trash2, TriangleAlert as AlertTriangle, Volume2, VolumeX, Webhook, Wind, Workflow, Wrench, X, Zap, ZoomIn, ZoomOut } from "lucide-vue-next";
12
+ import type { Component } from "vue";
13
+
14
+
15
+
16
+
17
+
18
+
19
+
20
+
21
+
22
+
23
+
24
+
25
+
26
+
27
+
28
+
29
+
30
+
31
+
32
+
33
+
34
+
35
+
36
+
37
+
38
+
39
+
40
+
41
+
42
+
43
+
44
+
45
+
46
+
47
+
48
+
49
+
50
+
51
+
52
+
53
+
54
+
55
+
56
+
57
+
58
+
59
+
60
+
61
+
62
+
63
+
64
+
65
+
66
+
67
+
68
+
69
+
70
+
71
+
72
+
73
+
74
+
75
+
76
+
77
+
78
+
79
+
80
+
81
+
82
+
83
+
84
+
85
+
86
+
87
+
88
+
89
+
90
+
91
+
92
+
93
+
94
+
95
+
96
+
97
+
98
+
99
+ const REGISTRY: Record<string, Component> = {
100
+ Activity, AlertCircle, AlertTriangle, ArrowDown, ArrowLeft, ArrowRight,
101
+ ArrowUp, BarChart3, Battery, Bell, Bot, Box, Brain, Cable, Calendar,
102
+ Check, CheckCircle, CheckCircle2, ChevronDown, ChevronLeft, ChevronRight,
103
+ ChevronUp, Circle, CircleDot, Clock, Cpu, Database, Dot, ExternalLink, Eye,
104
+ FileArchive, FileCode, FileImage, FileText, Flame,
105
+ FolderOpen, Gauge, Globe, HardDrive, Info, Kanban, Key, Layers,
106
+ LayoutDashboard, Loader2, Lock, Maximize, Maximize2, MessageSquare, Mic,
107
+ MicOff, Minimize, Monitor, MoreHorizontal, Network, Package, Pause, Pencil,
108
+ Play, Plug, Plus, Radio, RefreshCw, Search, Send, Server, Settings, Share2,
109
+ Shield, Star, Table, Tag, Thermometer, Trash2, Volume2, VolumeX, Webhook,
110
+ Wind, Workflow, Wrench, X, Zap, ZoomIn, ZoomOut,
111
+ };
112
+
113
+ /**
114
+ * Resolve a lucide icon by its PascalCase export name (e.g. `"Cpu"`,
115
+ * `"Activity"`). Returns `Info` for unknown / falsy names so callers can
116
+ * always render something.
117
+ */
118
+ export function iconByName(name: string | undefined | null): Component {
119
+ if (name) {
120
+ const hit = REGISTRY[name];
121
+ if (hit) return hit;
122
+ // Tolerate lowercase / kebab server strings ("cpu", "check-circle").
123
+ const alt = REGISTRY[name.replace(/(^|[-_])(.)/g, (_m, _s, c: string) => c.toUpperCase())];
124
+ if (alt) return alt;
125
+ }
126
+ return Info;
127
+ }
@@ -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
+ }