@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
@@ -8,6 +8,8 @@ import {
8
8
  type PropType,
9
9
  Teleport,
10
10
  } from "vue";
11
+
12
+
11
13
  import { ChevronDown, Search } from "lucide-vue-next";
12
14
  import "./HkSelect.scss";
13
15
 
@@ -1,5 +1,6 @@
1
- import { defineComponent, type PropType } from "vue";
2
1
  import { Check } from "lucide-vue-next";
2
+ import { defineComponent, type PropType } from "vue";
3
+
3
4
  import "./HkSelectionGrid.scss";
4
5
 
5
6
  export interface SelectionGridItem {
@@ -0,0 +1,235 @@
1
+ import { defineComponent, ref, onMounted, type PropType } from "vue";
2
+ import { HPopover, useI18n } from "@celestia-island/hikari";
3
+ import { Wifi, WifiOff, Globe, Cable, Cpu } from "lucide-vue-next";
4
+ import type { HkConnectionInfo } from "./HkConnectionInfo";
5
+ import { PCountdownDigit } from "./HkCountdownDigit";
6
+
7
+
8
+ const regionLabel: Record<string, string> = {
9
+ CN: "\u4e2d\u56fd\u5927\u9646", JP: "\u65e5\u672c", KR: "\u97e9\u56fd",
10
+ US: "\u7f8e\u56fd", GB: "\u82f1\u56fd", DE: "\u5fb7\u56fd",
11
+ FR: "\u6cd5\u56fd", SA: "\u6c99\u7279", TW: "\u4e2d\u56fd\u53f0\u6e7e",
12
+ HK: "\u4e2d\u56fd\u9999\u6e2f", MO: "\u4e2d\u56fd\u6fb3\u95e8",
13
+ BR: "\u5df4\u897f", RU: "\u4fc4\u7f57\u65af",
14
+ CA: "\u52a0\u62ff\u5927", AU: "\u6fb3\u5927\u5229\u4e9a",
15
+ PT: "\u8461\u8404\u7259", ES: "\u897f\u73ed\u7259",
16
+ };
17
+
18
+ function latencyColor(ms: number | null): string {
19
+ if (ms === null) return "var(--color-muted)";
20
+ if (ms < 30) return "rgb(var(--color-success))";
21
+ if (ms < 100) return "rgb(var(--color-warning))";
22
+ return "rgb(var(--color-error))";
23
+ }
24
+
25
+ function qualityIcon(quality: string, tier: string, isLocalhost: boolean, size: number) {
26
+ if (isLocalhost) return <Cable size={size} />;
27
+ if (quality === "excellent" || quality === "good") return <Wifi size={size} />;
28
+ if (quality === "unknown") return <Wifi size={size} style={{ opacity: 0.4 }} />;
29
+ return <WifiOff size={size} />;
30
+ }
31
+
32
+ function fmtVer(v: string, hash?: string): string {
33
+ if (hash) return `${v} ${hash}`;
34
+ return v;
35
+ }
36
+
37
+ export const HkStatusBar = defineComponent({
38
+ name: "HkStatusBar",
39
+ props: {
40
+ version: { type: String, default: "0.1.0" },
41
+ engineVersion: { type: String as PropType<string | null>, default: null },
42
+ panelBuildHash: { type: String as PropType<string | undefined>, default: undefined },
43
+ engineBuildHash: { type: String as PropType<string | undefined>, default: undefined },
44
+ connectionStatus: {
45
+ type: String as PropType<"connected" | "reconnecting" | "disconnected" | "connecting">,
46
+ default: "disconnected",
47
+ },
48
+ connectionInfo: {
49
+ type: Object as PropType<HkConnectionInfo | null>,
50
+ default: null,
51
+ },
52
+ standalone: { type: Boolean, default: true },
53
+ onRetry: { type: Function as PropType<() => void>, default: undefined },
54
+ latencyMs: { type: Number, default: null },
55
+ transportTier: { type: String as PropType<string>, default: undefined },
56
+ attemptNumber: { type: Number, default: undefined },
57
+ countdown: { type: Number, default: undefined },
58
+ },
59
+ setup(props) {
60
+ const popupOpen = ref(false);
61
+ const anchorRef = ref<HTMLElement | null>(null);
62
+ let closeTimer: ReturnType<typeof setTimeout> | null = null;
63
+
64
+ const dotColorMap: Record<string, string> = {
65
+ connected: "rgb(var(--color-success))",
66
+ connecting: "rgb(var(--color-warning))",
67
+ reconnecting: "rgb(var(--color-warning))",
68
+ disconnected: "rgb(var(--color-error))",
69
+ };
70
+
71
+ function onTagEnter() {
72
+ if (closeTimer) { clearTimeout(closeTimer); closeTimer = null; }
73
+ popupOpen.value = true;
74
+ }
75
+ function onTagLeave() {
76
+ closeTimer = setTimeout(() => { popupOpen.value = false; }, 250);
77
+ }
78
+ function onPopupEnter() {
79
+ if (closeTimer) { clearTimeout(closeTimer); closeTimer = null; }
80
+ }
81
+ function onPopupLeave() {
82
+ popupOpen.value = false;
83
+ }
84
+ function onTagClick() {
85
+ if (props.connectionStatus !== "connected") {
86
+ props.onRetry?.();
87
+ }
88
+ }
89
+
90
+ return () => {
91
+ const { t } = useI18n();
92
+ const info = props.connectionInfo;
93
+ const latency = props.latencyMs ?? info?.latencyMs ?? null;
94
+ const mode = props.connectionStatus;
95
+ const tier = props.transportTier ?? info?.tier ?? "ws";
96
+ const attempt = props.attemptNumber ?? info?.attemptNumber ?? 0;
97
+ const countdown = props.countdown ?? info?.countdown ?? 0;
98
+
99
+ const tierLabelKey = `hikari::statusBar.tier.${tier}`;
100
+ const statusText = mode === "connected" ? t("hikari::statusBar.connected", "Connected")
101
+ : mode === "reconnecting" || mode === "connecting" ? t("hikari::statusBar.connecting", "Connecting...")
102
+ : t("hikari::statusBar.disconnected", "Disconnected");
103
+
104
+ const connecting = mode === "reconnecting" || mode === "connecting";
105
+
106
+ const pv = fmtVer(props.version, props.panelBuildHash);
107
+ const ev = props.engineVersion;
108
+ // Two nowrap version parts inside a flex-wrap container: on wide
109
+ // screens they sit side by side ("pv | Engine ev"), and when the
110
+ // width runs out the engine part wraps to its own line instead of
111
+ // breaking mid-token (mobile footer vertical-fragmentation fix).
112
+ const enginePart = ev
113
+ ? `${t("hikari::statusBar.engine", "Engine")} ${fmtVer(ev, props.engineBuildHash)}`
114
+ : null;
115
+
116
+ const tagClass = connecting
117
+ ? "s-status-bar-tag s-status-bar-tag-reconnecting"
118
+ : "s-status-bar-tag";
119
+
120
+ const inner = (
121
+ <>
122
+ <span
123
+ ref={anchorRef}
124
+ class={tagClass}
125
+ role="button"
126
+ tabindex={0}
127
+ onMouseenter={onTagEnter}
128
+ onMouseleave={onTagLeave}
129
+ onClick={onTagClick}
130
+ onKeydown={(e: KeyboardEvent) => {
131
+ if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onTagClick(); }
132
+ }}
133
+ style={{
134
+ position: "relative", zIndex: 51,
135
+ }}
136
+ >
137
+ <span class="s-status-bar-dot" style={{
138
+ background: dotColorMap[mode] ?? dotColorMap.disconnected,
139
+ }} />
140
+ <span class="s-status-bar-tag-label">{t("hikari::statusBar.panel", "Panel")}</span>
141
+ <span class="s-status-bar-tag-value">
142
+ <span class="s-status-bar-version">{pv}</span>
143
+ {enginePart && (
144
+ <span class="s-status-bar-version">
145
+ <span class="s-status-bar-version-sep" aria-hidden="true">| </span>
146
+ {enginePart}
147
+ </span>
148
+ )}
149
+ </span>
150
+ </span>
151
+
152
+ <HPopover
153
+ modelValue={popupOpen.value}
154
+ onUpdate:modelValue={(v: boolean) => { popupOpen.value = v; }}
155
+ placement="top-start"
156
+ backdrop={false}
157
+ closeOnBackdrop={false}
158
+ anchorRef={anchorRef.value}
159
+ >
160
+ <div
161
+ onMouseenter={onPopupEnter}
162
+ onMouseleave={onPopupLeave}
163
+ style={{
164
+ minWidth: "220px", padding: "10px 14px",
165
+ fontSize: "0.75rem", lineHeight: 1.6,
166
+ color: "rgb(var(--color-text))",
167
+ }}
168
+ >
169
+ {info ? (
170
+ <>
171
+ <div style={{ display: "flex", alignItems: "center", gap: "6px", marginBottom: "6px", fontWeight: 600, fontSize: "0.8125rem" }}>
172
+ {qualityIcon(info.quality || (mode === "connected" ? "good" : "unknown"), tier, info.isLocalhost, 14)}
173
+ <span style={{ color: dotColorMap[mode] ?? dotColorMap.disconnected }}>
174
+ {statusText}
175
+ </span>
176
+ {latency !== null && (
177
+ <span style={{ marginLeft: "auto", color: latencyColor(latency), fontFamily: "var(--font-mono, monospace)", fontWeight: 600, fontSize: "0.6875rem" }}>
178
+ {latency} ms
179
+ </span>
180
+ )}
181
+ </div>
182
+ {connecting && attempt > 0 && (
183
+ <div style={{ display: "flex", alignItems: "center", gap: "4px", color: "rgb(var(--color-warning))", fontSize: "0.6875rem", marginBottom: "4px" }}>
184
+ <span>
185
+ {t("hikari::statusBar.retrying", "Retrying {retryCount} / {maxRetries}")
186
+ .replace("{retryCount}", String(attempt))
187
+ .replace("{maxRetries}", String(info.maxRetries > 0 ? info.maxRetries : 3))}
188
+ </span>
189
+ {countdown > 0 && (
190
+ <span style={{ display: "inline-flex", alignItems: "center", gap: "4px", fontFamily: "var(--font-mono, monospace)", marginLeft: "8px" }}>
191
+ <PCountdownDigit value={countdown} />
192
+ </span>
193
+ )}
194
+ </div>
195
+ )}
196
+ {mode === "disconnected" && (
197
+ <div style={{ fontStyle: "italic", fontSize: "0.6875rem", marginBottom: "4px", opacity: 0.7 }}>
198
+ {t("hikari::statusBar.clickReconnect", "Click to retry")}
199
+ </div>
200
+ )}
201
+ <div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
202
+ <Cpu size={12} style={{ opacity: 0.5, flexShrink: 0 }} />
203
+ <span style={{ opacity: 0.5, marginRight: "auto" }}>{t("hikari::statusBar.protocol", "Protocol")}</span>
204
+ {connecting ? (
205
+ <span style={{ color: "rgb(var(--color-warning))" }}>
206
+ {t("hikari::statusBar.probing", "Probing...")}
207
+ </span>
208
+ ) : (
209
+ <span>{t(tierLabelKey, tier)}</span>
210
+ )}
211
+ </div>
212
+ <div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
213
+ <Globe size={12} style={{ opacity: 0.5, flexShrink: 0 }} />
214
+ <span style={{ opacity: 0.5, marginRight: "auto" }}>{t("hikari::statusBar.network", "Network")}</span>
215
+ <span>{regionLabel[info.region] ?? info.region}{info.asn != null ? ` · AS${info.asn}` : ""}{info.isLocalhost ? " · " + t("hikari::statusBar.local", "Local") : ""}</span>
216
+ </div>
217
+ </>
218
+ ) : (
219
+ <div style={{ opacity: 0.5 }}>{t("hikari::statusBar.fetching", "Fetching connection info...")}</div>
220
+ )}
221
+ </div>
222
+ </HPopover>
223
+ </>
224
+ );
225
+
226
+ if (!props.standalone) return inner;
227
+
228
+ return (
229
+ <footer class="s-status-bar">
230
+ {inner}
231
+ </footer>
232
+ );
233
+ };
234
+ },
235
+ });
@@ -1,5 +1,6 @@
1
- import { computed, defineComponent, type PropType } from "vue";
2
1
  import { X } from "lucide-vue-next";
2
+ import { computed, defineComponent, type PropType } from "vue";
3
+
3
4
  import "./HkTag.scss";
4
5
 
5
6
  export type TagVariant = "default" | "primary" | "success" | "warning" | "danger" | "info";
@@ -0,0 +1,130 @@
1
+ .s-theme-toggle {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ gap: 2px;
5
+ }
6
+
7
+ .s-theme-toggle-btn {
8
+ display: inline-flex;
9
+ align-items: center;
10
+ justify-content: center;
11
+ height: 1.75rem;
12
+ min-width: 1.75rem;
13
+ padding: 0 var(--space-6, 0.375rem);
14
+ border: 1px solid var(--border-faint, rgb(var(--color-border) / 12%));
15
+ background: rgb(var(--color-surface));
16
+ color: rgb(var(--color-text-secondary, var(--color-muted)));
17
+ cursor: pointer;
18
+ transition: background 0.15s ease, color 0.15s ease;
19
+ }
20
+
21
+ .s-theme-toggle-btn:hover {
22
+ background: rgb(var(--color-primary) / 12%);
23
+ color: rgb(var(--color-primary));
24
+ }
25
+
26
+ .s-theme-toggle-btn[data-variant="main"] {
27
+ border-radius: var(--radius-sm, 6px) 0 0 var(--radius-sm, 6px);
28
+ border-right: none;
29
+ }
30
+
31
+ .s-theme-toggle-btn[data-variant="arrow"] {
32
+ border-radius: 0 var(--radius-sm, 6px) var(--radius-sm, 6px) 0;
33
+ }
34
+
35
+ .s-theme-menu {
36
+ display: flex;
37
+ flex-direction: column;
38
+ gap: var(--space-4, 0.25rem);
39
+ min-width: 13rem;
40
+ padding: var(--space-8, 0.5rem);
41
+ }
42
+
43
+ .s-theme-menu-label {
44
+ font-size: var(--text-xs, 0.75rem);
45
+ font-weight: 600;
46
+ color: rgb(var(--color-muted));
47
+ padding: var(--space-4, 0.25rem) var(--space-6, 0.375rem);
48
+ }
49
+
50
+ .s-theme-mode-row {
51
+ display: flex;
52
+ gap: var(--space-4, 0.25rem);
53
+ padding: 0 var(--space-6, 0.375rem) var(--space-4, 0.25rem);
54
+ }
55
+
56
+ .s-theme-mode-btn {
57
+ display: inline-flex;
58
+ align-items: center;
59
+ gap: var(--space-4, 0.25rem);
60
+ padding: var(--space-4, 0.25rem) var(--space-8, 0.5rem);
61
+ border: 1px solid var(--border-faint, rgb(var(--color-border) / 12%));
62
+ border-radius: var(--radius-sm, 6px);
63
+ background: transparent;
64
+ color: rgb(var(--color-muted));
65
+ font-size: var(--text-xs, 0.75rem);
66
+ cursor: pointer;
67
+ }
68
+
69
+ .s-theme-mode-btn:hover {
70
+ background: rgb(var(--color-primary) / 12%);
71
+ color: rgb(var(--color-primary));
72
+ }
73
+
74
+ .s-theme-mode-btn[data-active] {
75
+ background: rgb(var(--color-primary));
76
+ border-color: rgb(var(--color-primary));
77
+ color: rgb(var(--color-on-primary));
78
+ }
79
+
80
+ .s-theme-item-row {
81
+ display: flex;
82
+ align-items: center;
83
+ gap: var(--space-4, 0.25rem);
84
+ }
85
+
86
+ .s-theme-item-btn {
87
+ display: flex;
88
+ align-items: center;
89
+ justify-content: space-between;
90
+ gap: var(--space-8, 0.5rem);
91
+ flex: 1;
92
+ padding: var(--space-6, 0.375rem) var(--space-8, 0.5rem);
93
+ border: none;
94
+ border-radius: var(--radius-sm, 6px);
95
+ background: transparent;
96
+ color: rgb(var(--color-text));
97
+ font-size: var(--text-sm, 0.8125rem);
98
+ text-align: left;
99
+ cursor: pointer;
100
+ }
101
+
102
+ .s-theme-item-btn:hover {
103
+ background: rgb(var(--color-primary) / 12%);
104
+ color: rgb(var(--color-primary));
105
+ }
106
+
107
+ .s-theme-item-btn[data-active] {
108
+ background: rgb(var(--color-primary) / 12%);
109
+ color: rgb(var(--color-primary));
110
+ font-weight: 600;
111
+ }
112
+
113
+ .s-theme-item-delete {
114
+ display: inline-flex;
115
+ align-items: center;
116
+ justify-content: center;
117
+ width: 1.5rem;
118
+ height: 1.5rem;
119
+ flex-shrink: 0;
120
+ border: none;
121
+ border-radius: var(--radius-sm, 6px);
122
+ background: transparent;
123
+ color: rgb(var(--color-muted));
124
+ cursor: pointer;
125
+ }
126
+
127
+ .s-theme-item-delete:hover {
128
+ background: rgb(var(--color-error) / 15%);
129
+ color: rgb(var(--color-error));
130
+ }
@@ -0,0 +1,160 @@
1
+ import { computed, defineComponent, onMounted, ref, type PropType } from "vue";
2
+ import { Check, ChevronDown, Monitor, Moon, Palette, Sun, Trash as Trash2 } from "lucide-vue-next";
3
+ import { HDivider, HPopover, useI18n, useTheme, type PopupPlacement, type ThemeId } from "@celestia-island/hikari";
4
+
5
+ import { HColorSchemeDialog, type HCustomTheme } from "./HkColorSchemeDialog";
6
+ import "./HkThemeToggle.scss";
7
+
8
+
9
+ /**
10
+ * HkThemeToggle — light/dark/auto theme control over hikari's theme engine.
11
+ *
12
+ * Composes hikari's `useTheme()` (theme presets + custom themes + mode
13
+ * persistence); it does NOT reimplement the theme engine. A main button
14
+ * cycles light/dark (auto keeps a Monitor glyph); the popover offers
15
+ * explicit Light/Dark/Auto modes, preset/custom theme selection (custom
16
+ * themes are removable), and opens HColorSchemeDialog to create a new
17
+ * custom scheme.
18
+ */
19
+ export const HkThemeToggle = defineComponent({
20
+ name: "HkThemeToggle",
21
+ props: {
22
+ /** Popover placement. */
23
+ popoverPlacement: { type: String as PropType<PopupPlacement>, default: "bottom-end" },
24
+ },
25
+ emits: {
26
+ "update:scheme": (_theme: HCustomTheme) => true,
27
+ },
28
+ setup(props, { emit, slots }) {
29
+ const { t } = useI18n();
30
+ const { currentTheme, currentMode, effectiveMode, setTheme, setMode, toggleMode, allThemeList, addCustomTheme, removeCustomTheme } = useTheme();
31
+
32
+ const menuOpen = ref(false);
33
+ const triggerRef = ref<HTMLElement | null>(null);
34
+ const schemeDialogOpen = ref(false);
35
+
36
+ const modeLabel = computed(() => {
37
+ const map: Record<string, string> = {
38
+ system: t("hikari::theme.modeAuto"),
39
+ light: t("hikari::theme.modeLight"),
40
+ dark: t("hikari::theme.modeDark"),
41
+ };
42
+ return map[currentMode.value] ?? currentMode.value;
43
+ });
44
+
45
+ const modeOptions = computed(() => [
46
+ { key: "light", label: t("hikari::theme.modeLight"), icon: () => <Sun size={12} /> },
47
+ { key: "dark", label: t("hikari::theme.modeDark"), icon: () => <Moon size={12} /> },
48
+ { key: "system", label: t("hikari::theme.modeAuto"), icon: () => <Monitor size={12} /> },
49
+ ]);
50
+
51
+ function onSelectTheme(id: ThemeId) {
52
+ setTheme(id);
53
+ menuOpen.value = false;
54
+ }
55
+
56
+ function onConfirmScheme(theme: HCustomTheme) {
57
+ addCustomTheme(theme);
58
+ setTheme(theme.id);
59
+ emit("update:scheme", theme);
60
+ }
61
+
62
+ return () => (
63
+ <div class="s-theme-toggle" ref={triggerRef}>
64
+ <button
65
+ type="button"
66
+ class="s-theme-toggle-btn"
67
+ data-variant="main"
68
+ onClick={toggleMode}
69
+ title={modeLabel.value}
70
+ aria-label={t("hikari::theme.mode")}
71
+ >
72
+ {currentMode.value === "system" ? (
73
+ <Monitor size={14} />
74
+ ) : effectiveMode.value === "dark" ? (
75
+ <Moon size={14} />
76
+ ) : (
77
+ <Sun size={14} />
78
+ )}
79
+ </button>
80
+ <button
81
+ type="button"
82
+ class="s-theme-toggle-btn"
83
+ data-variant="arrow"
84
+ onClick={() => { menuOpen.value = !menuOpen.value; }}
85
+ aria-label={t("hikari::theme.themes")}
86
+ >
87
+ <ChevronDown size={12} />
88
+ </button>
89
+
90
+ <HPopover
91
+ modelValue={menuOpen.value}
92
+ onUpdate:modelValue={(v: boolean) => { menuOpen.value = v; }}
93
+ placement={props.popoverPlacement}
94
+ anchorRef={triggerRef.value ?? null}
95
+ >
96
+ <div class="s-theme-menu">
97
+ <div class="s-theme-menu-label">{t("hikari::theme.mode")}</div>
98
+ <div class="s-theme-mode-row">
99
+ {modeOptions.value.map((opt) => (
100
+ <button
101
+ key={opt.key}
102
+ type="button"
103
+ class="s-theme-mode-btn"
104
+ data-active={currentMode.value === opt.key || undefined}
105
+ onClick={() => setMode(opt.key as "light" | "dark" | "system")}
106
+ >
107
+ {opt.icon()}
108
+ {opt.label}
109
+ </button>
110
+ ))}
111
+ </div>
112
+
113
+ <HDivider spacing="md" />
114
+
115
+ <div class="s-theme-menu-label">{t("hikari::theme.themes")}</div>
116
+ {allThemeList.value.map((th) => (
117
+ <div key={th.id} class="s-theme-item-row">
118
+ <button
119
+ type="button"
120
+ class="s-theme-item-btn"
121
+ data-active={currentTheme.value === th.id || undefined}
122
+ onClick={() => onSelectTheme(th.id)}
123
+ >
124
+ <span>{th.name}</span>
125
+ {currentTheme.value === th.id && <Check size={14} />}
126
+ </button>
127
+ {th.isCustom && (
128
+ <button
129
+ type="button"
130
+ class="s-theme-item-delete"
131
+ title={t("hikari::theme.deleteTheme")}
132
+ onClick={() => removeCustomTheme(th.id)}
133
+ >
134
+ <Trash2 size={12} />
135
+ </button>
136
+ )}
137
+ </div>
138
+ ))}
139
+
140
+ <button
141
+ type="button"
142
+ class="s-theme-item-btn"
143
+ onClick={() => { schemeDialogOpen.value = true; menuOpen.value = false; }}
144
+ >
145
+ <Palette size={14} />
146
+ <span>{t("hikari::theme.editScheme")}</span>
147
+ </button>
148
+ </div>
149
+ {slots["menu-extra"]?.()}
150
+ </HPopover>
151
+
152
+ <HColorSchemeDialog
153
+ modelValue={schemeDialogOpen.value}
154
+ onUpdate:modelValue={(v: boolean) => { schemeDialogOpen.value = v; }}
155
+ onConfirm={onConfirmScheme}
156
+ />
157
+ </div>
158
+ );
159
+ },
160
+ });
@@ -1,5 +1,6 @@
1
- import { defineComponent, type PropType } from "vue";
2
1
  import { Check } from "lucide-vue-next";
2
+ import { defineComponent, type PropType } from "vue";
3
+
3
4
 
4
5
  import "./HkTimeline.scss";
5
6
 
@@ -1,5 +1,8 @@
1
+ import { CircleX as XCircle, X } from "lucide-vue-next";
1
2
  import { computed, defineComponent, ref, Teleport, Transition, TransitionGroup, watch } from "vue";
2
- import { AlertTriangle, CheckCircle, Copy, Info, X, XCircle } from "lucide-vue-next";
3
+ import { AlertTriangle, CheckCircle, Copy, Info } from "lucide-vue-next";
4
+
5
+
3
6
  import { useToast, type ToastItem, type ToastMessage, type ToastType } from "../runtime/useToast";
4
7
  import { useClipboard } from "../runtime/useClipboard";
5
8
  import { useI18n } from "../i18n/context";
@@ -1,19 +1,11 @@
1
1
  import { defineComponent, onMounted, onUnmounted, ref, watch, type PropType } from "vue";
2
2
 
3
- import { LineChart } from "echarts/charts";
4
- import { DataZoomComponent, GridComponent, LegendComponent, MarkLineComponent, TooltipComponent } from "echarts/components";
5
- import * as echarts from "echarts/core";
6
- import { CanvasRenderer } from "echarts/renderers";
7
-
8
- echarts.use([
9
- LineChart,
10
- GridComponent,
11
- TooltipComponent,
12
- LegendComponent,
13
- DataZoomComponent,
14
- MarkLineComponent,
15
- CanvasRenderer,
16
- ]);
3
+ // echarts is loaded lazily (dynamic import in onMounted) so a consumer that
4
+ // merely imports this component — or anything else from the hikari package —
5
+ // never pays for echarts in the initial bundle. The static import graph
6
+ // previously dragged the whole echarts module tree into the shared chunk,
7
+ // which every entry page (e.g. the login screen) had to download and parse.
8
+ import type { ECharts } from "echarts/core";
17
9
 
18
10
  export interface TrendPoint {
19
11
  time: number;
@@ -50,7 +42,7 @@ export default defineComponent({
50
42
  },
51
43
  setup(props) {
52
44
  const chartEl = ref<HTMLElement>();
53
- let chart: echarts.ECharts | null = null;
45
+ let chart: ECharts | null = null;
54
46
  let resizeCleanup: (() => void) | undefined;
55
47
 
56
48
  function renderChart() {
@@ -141,8 +133,27 @@ export default defineComponent({
141
133
  chart?.resize();
142
134
  }
143
135
 
144
- onMounted(() => {
136
+ onMounted(async () => {
145
137
  if (chartEl.value) {
138
+ const echarts = await import("echarts/core");
139
+ const { LineChart } = await import("echarts/charts");
140
+ const {
141
+ DataZoomComponent,
142
+ GridComponent,
143
+ LegendComponent,
144
+ MarkLineComponent,
145
+ TooltipComponent,
146
+ } = await import("echarts/components");
147
+ const { CanvasRenderer } = await import("echarts/renderers");
148
+ echarts.use([
149
+ LineChart,
150
+ GridComponent,
151
+ TooltipComponent,
152
+ LegendComponent,
153
+ DataZoomComponent,
154
+ MarkLineComponent,
155
+ CanvasRenderer,
156
+ ]);
146
157
  chart = echarts.init(chartEl.value);
147
158
  renderChart();
148
159
  // Keep the canvas in sync with its container. echarts doesn't
@@ -1,5 +1,8 @@
1
- import { defineComponent } from "vue";
2
1
  import { Maximize2, ZoomIn, ZoomOut } from "lucide-vue-next";
2
+ import { defineComponent } from "vue";
3
+
4
+
5
+
3
6
 
4
7
  import HButton from "./HkButton";
5
8
  import { useI18n } from "../i18n/context";
@@ -0,0 +1,4 @@
1
+ $hikari-font-family-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
2
+ $hikari-font-size-sm: 0.875rem;
3
+ $hikari-radius-fui-sm: 4px;
4
+ $hikari-radius-fui-md: 6px;