@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.
- package/README.md +1 -1
- package/package.json +16 -7
- package/src/DemoApp.vue +1 -1
- package/src/components/HkAdminHeader.tsx +205 -0
- package/src/components/HkAdminShell.tsx +85 -0
- package/src/components/HkAdminTablePage.tsx +70 -0
- package/src/components/HkAuthCard.tsx +36 -0
- package/src/components/HkAuthSubmitButton.scss +12 -0
- package/src/components/HkAuthSubmitButton.tsx +94 -0
- package/src/components/HkAvatar.scss +93 -0
- package/src/components/HkAvatar.tsx +1 -1
- package/src/components/HkBreadcrumb.scss +624 -0
- package/src/components/HkBreadcrumb.tsx +1 -1
- package/src/components/HkCard.scss +1 -1
- package/src/components/HkCheckbox.tsx +2 -1
- package/src/components/HkCollapse.scss +50 -0
- package/src/components/HkCollapse.tsx +1 -1
- package/src/components/HkColorSchemeDialog.scss +16 -0
- package/src/components/HkColorSchemeDialog.tsx +237 -0
- package/src/components/HkConnectionInfo.ts +80 -0
- package/src/components/HkConnectionStatus.tsx +133 -0
- package/src/components/HkCountdownDigit.tsx +97 -0
- package/src/components/HkDateTimePicker.tsx +7 -1
- package/src/components/HkErrorBoundary.tsx +4 -1
- package/src/components/HkIcon.scss +123 -0
- package/src/components/HkIcon.tsx +7 -23
- package/src/components/HkIconButton.scss +170 -0
- package/src/components/HkIconButton.tsx +2 -2
- package/src/components/HkIconButtonVars.scss +176 -0
- package/src/components/HkInput.scss +22 -3
- package/src/components/HkInput.tsx +41 -1
- package/src/components/HkKeywordSearchModal.tsx +10 -7
- package/src/components/HkLocalePicker.tsx +113 -0
- package/src/components/HkMediaControlBar.tsx +14 -7
- package/src/components/HkMinimap.tsx +10 -2
- package/src/components/HkModal.tsx +2 -23
- package/src/components/HkNavItem.tsx +2 -1
- package/src/components/HkNavSidebar.tsx +99 -0
- package/src/components/HkPageHeader.tsx +40 -0
- package/src/components/HkPasswordInput.scss +14 -12
- package/src/components/HkPasswordInput.test.ts +242 -0
- package/src/components/HkPasswordInput.tsx +236 -41
- package/src/components/HkPopupSelect.scss +3 -3
- package/src/components/HkPopupSelect.tsx +2 -1
- package/src/components/HkSearchInput.tsx +2 -2
- package/src/components/HkSecretRevealModal.scss +61 -0
- package/src/components/HkSecretRevealModal.tsx +91 -0
- package/src/components/HkSelect.scss +4 -4
- package/src/components/HkSelect.tsx +2 -0
- package/src/components/HkSelectionGrid.tsx +2 -1
- package/src/components/HkStatusBar.tsx +235 -0
- package/src/components/HkTag.tsx +2 -1
- package/src/components/HkThemeToggle.scss +130 -0
- package/src/components/HkThemeToggle.tsx +160 -0
- package/src/components/HkTimeline.tsx +2 -1
- package/src/components/HkToast.tsx +4 -1
- package/src/components/HkTrendChart.tsx +27 -16
- package/src/components/HkZoomToolbar.tsx +4 -1
- package/src/components/_hikari-vars.scss +4 -0
- package/src/composables/highlightLanguages.ts +109 -0
- package/src/composables/iconRegistry.ts +127 -0
- package/src/composables/messaging/browserTransport.ts +96 -0
- package/src/composables/messaging/index.ts +204 -0
- package/src/composables/messaging/messaging.test.ts +256 -0
- package/src/composables/messaging/toastTransport.ts +26 -0
- package/src/composables/messaging/types.ts +115 -0
- package/src/composables/useActionBar.ts +66 -0
- package/src/composables/useConnectionProbe.ts +126 -0
- package/src/composables/useDeferredTransition.ts +16 -0
- package/src/composables/useEngineHealth.ts +69 -0
- package/src/composables/useHighlight.ts +124 -0
- package/src/composables/usePageTitle.ts +37 -0
- package/src/composables/useReducedMotion.ts +34 -0
- package/src/composables/useResourceListModal.tsx +147 -0
- package/src/composables/useZoomPan.ts +192 -0
- package/src/i18n/locales/ar/components.json +12 -3
- package/src/i18n/locales/ar/components.toml +5 -0
- package/src/i18n/locales/ar/connection.json +25 -0
- package/src/i18n/locales/ar/platform.json +50 -0
- package/src/i18n/locales/de/components.json +12 -3
- package/src/i18n/locales/de/components.toml +5 -0
- package/src/i18n/locales/de/connection.json +25 -0
- package/src/i18n/locales/de/platform.json +50 -0
- package/src/i18n/locales/en/components.json +12 -3
- package/src/i18n/locales/en/components.toml +5 -0
- package/src/i18n/locales/en/connection.json +27 -0
- package/src/i18n/locales/en/platform.json +53 -0
- package/src/i18n/locales/es/components.json +12 -3
- package/src/i18n/locales/es/components.toml +5 -0
- package/src/i18n/locales/es/connection.json +25 -0
- package/src/i18n/locales/es/platform.json +50 -0
- package/src/i18n/locales/fr/components.json +12 -3
- package/src/i18n/locales/fr/components.toml +5 -0
- package/src/i18n/locales/fr/connection.json +25 -0
- package/src/i18n/locales/fr/platform.json +50 -0
- package/src/i18n/locales/ja/components.json +12 -3
- package/src/i18n/locales/ja/components.toml +5 -0
- package/src/i18n/locales/ja/connection.json +27 -0
- package/src/i18n/locales/ja/platform.json +53 -0
- package/src/i18n/locales/ko/components.json +12 -3
- package/src/i18n/locales/ko/components.toml +5 -0
- package/src/i18n/locales/ko/connection.json +27 -0
- package/src/i18n/locales/ko/platform.json +50 -0
- package/src/i18n/locales/pt/components.json +12 -3
- package/src/i18n/locales/pt/components.toml +5 -0
- package/src/i18n/locales/pt/connection.json +25 -0
- package/src/i18n/locales/pt/platform.json +50 -0
- package/src/i18n/locales/ru/components.json +12 -3
- package/src/i18n/locales/ru/components.toml +5 -0
- package/src/i18n/locales/ru/connection.json +25 -0
- package/src/i18n/locales/ru/platform.json +50 -0
- package/src/i18n/locales/zh-Hans/components.json +12 -3
- package/src/i18n/locales/zh-Hans/components.toml +5 -0
- package/src/i18n/locales/zh-Hans/connection.json +27 -0
- package/src/i18n/locales/zh-Hans/platform.json +53 -0
- package/src/i18n/locales/zh-Hant/components.json +12 -3
- package/src/i18n/locales/zh-Hant/components.toml +5 -0
- package/src/i18n/locales/zh-Hant/connection.json +27 -0
- package/src/i18n/locales/zh-Hant/platform.json +53 -0
- package/src/index.ts +67 -4
- package/src/plugins/bootScriptsPlugin.ts +47 -0
- package/src/plugins/buildHashPlugin.ts +67 -0
- package/src/plugins/index.ts +2 -0
- package/src/render.ts +1 -1
- package/src/runtime/env.ts +4 -0
- package/src/runtime/index.ts +0 -1
- package/src/runtime/useConfirm.ts +7 -1
- package/src/styles/admin-tokens.scss +634 -0
- package/src/styles/index.scss +1 -0
- package/src/theme/_field.scss +116 -0
- package/src/theme/index.ts +2 -0
- package/src/theme/presets.ts +11 -9
- package/src/theme/useTheme.ts +54 -5
- package/src/tokens.scss +76 -0
- package/src/utils/base64.ts +31 -0
- package/src/utils/connectivity.ts +51 -0
- package/src/utils/dom.ts +39 -0
- package/src/utils/download.ts +22 -0
- package/src/utils/fileType.ts +127 -0
- package/src/utils/fuzzy.ts +77 -0
- package/src/utils/fuzzySearch.test.ts +87 -0
- package/src/utils/objectPath.ts +59 -0
- package/src/utils/password.ts +53 -0
- package/src/utils/pow.test.ts +45 -0
- package/src/utils/pow.ts +226 -0
- package/src/utils/powNonce.ts +83 -0
- package/src/utils/validation.test.ts +78 -0
- package/src/composables/useBreakpoint.ts +0 -41
- package/src/composables/useClipboard.ts +0 -37
- package/src/composables/useConfirm.ts +0 -35
- package/src/composables/useToast.ts +0 -2
- 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
|
+
}
|