@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,237 @@
1
+ import { computed, defineComponent, nextTick, reactive, ref, watch } from "vue";
2
+ import {
3
+ HColorPicker,
4
+ HInput,
5
+ HModal,
6
+ HMorphingTabs,
7
+
8
+ useI18n,
9
+ useTheme,
10
+ type ModalAction,
11
+ type ThemeSchemeTokens,
12
+ } from "@celestia-island/hikari";
13
+
14
+ import "./HkColorSchemeDialog.scss";
15
+
16
+
17
+ const defaultDark: ThemeSchemeTokens = {
18
+ primary: { r: 255, g: 107, b: 157 },
19
+ secondary: { r: 199, g: 146, b: 234 },
20
+ accent: { r: 253, g: 235, b: 139 },
21
+ text: { r: 228, g: 228, b: 231 },
22
+ muted: { r: 180, g: 180, b: 180 },
23
+ border: { r: 255, g: 255, b: 255 },
24
+ focusedBorder: { r: 255, g: 107, b: 157 },
25
+ background: { r: 14, g: 14, b: 30 },
26
+ surface: { r: 24, g: 24, b: 42 },
27
+ selectedBackground: { r: 70, g: 70, b: 85 },
28
+ selectedText: { r: 240, g: 240, b: 240 },
29
+ statusBarBackground: { r: 24, g: 24, b: 42 },
30
+ success: { r: 114, g: 241, b: 184 },
31
+ error: { r: 255, g: 107, b: 107 },
32
+ warning: { r: 253, g: 235, b: 139 },
33
+ info: { r: 110, g: 231, b: 239 },
34
+ };
35
+
36
+ const defaultLight: ThemeSchemeTokens = {
37
+ primary: { r: 214, g: 51, b: 132 },
38
+ secondary: { r: 156, g: 106, b: 222 },
39
+ accent: { r: 230, g: 167, b: 0 },
40
+ text: { r: 30, g: 30, b: 30 },
41
+ muted: { r: 80, g: 80, b: 80 },
42
+ border: { r: 0, g: 0, b: 0 },
43
+ focusedBorder: { r: 214, g: 51, b: 132 },
44
+ background: { r: 245, g: 245, b: 240 },
45
+ surface: { r: 255, g: 255, b: 255 },
46
+ selectedBackground: { r: 200, g: 200, b: 205 },
47
+ selectedText: { r: 40, g: 40, b: 45 },
48
+ statusBarBackground: { r: 230, g: 230, b: 225 },
49
+ success: { r: 16, g: 185, b: 129 },
50
+ error: { r: 239, g: 68, b: 68 },
51
+ warning: { r: 245, g: 158, b: 11 },
52
+ info: { r: 6, g: 182, b: 212 },
53
+ };
54
+
55
+ type ColorTokenKey = keyof ThemeSchemeTokens;
56
+
57
+ const editableTokens: ColorTokenKey[] = ["primary", "secondary", "accent", "success", "error", "warning", "info"];
58
+ const derivedTokens: ColorTokenKey[] = ["background", "surface", "text", "muted", "border", "focusedBorder", "selectedBackground", "selectedText", "statusBarBackground"];
59
+
60
+ function clamp(v: number): number {
61
+ return Math.max(0, Math.min(255, v));
62
+ }
63
+
64
+ function deriveSurface(tokens: ThemeSchemeTokens, isDark: boolean): ThemeSchemeTokens {
65
+ const p = tokens.primary;
66
+ const base = isDark ? 14 : 245;
67
+ return {
68
+ ...tokens,
69
+ background: { r: clamp(base), g: clamp(base), b: clamp(base + (isDark ? 16 : -5)) },
70
+ surface: { r: clamp(base + (isDark ? 10 : 10)), g: clamp(base + (isDark ? 10 : 10)), b: clamp(base + (isDark ? 18 : 5)) },
71
+ text: { r: clamp(isDark ? 228 : 30), g: clamp(isDark ? 228 : 30), b: clamp(isDark ? 231 : 30) },
72
+ muted: { r: clamp(isDark ? 180 : 80), g: clamp(isDark ? 180 : 80), b: clamp(isDark ? 180 : 80) },
73
+ border: { r: clamp(isDark ? 255 : 0), g: clamp(isDark ? 255 : 0), b: clamp(isDark ? 255 : 0) },
74
+ focusedBorder: { r: p.r, g: p.g, b: p.b },
75
+ selectedBackground: { r: clamp(p.r * 0.27), g: clamp(p.g * 0.27), b: clamp(p.b * 0.35) },
76
+ selectedText: { r: clamp(isDark ? 240 : 40), g: clamp(isDark ? 240 : 40), b: clamp(isDark ? 240 : 45) },
77
+ statusBarBackground: { r: clamp(base + (isDark ? 10 : -15)), g: clamp(base + (isDark ? 10 : -15)), b: clamp(base + (isDark ? 18 : -5)) },
78
+ };
79
+ }
80
+
81
+ interface HkTabItem {
82
+ key: string;
83
+ label: string;
84
+ }
85
+
86
+ /**
87
+ * HCustomTheme — custom color scheme payload, structurally identical to
88
+ * hikari's `CustomThemePreset` (which hikari does not re-export from its
89
+ * root yet). `useTheme().addCustomTheme(theme)` accepts it directly.
90
+ */
91
+ export interface HCustomTheme {
92
+ id: string;
93
+ name: string;
94
+ dark: ThemeSchemeTokens;
95
+ light: ThemeSchemeTokens;
96
+ }
97
+
98
+ /**
99
+ * HColorSchemeDialog — custom color scheme editor over hikari's theme
100
+ * presets/custom themes.
101
+ *
102
+ * Edits the seven accent tokens (primary/secondary/accent/success/error/
103
+ * warning/info) per mode (dark/light); surface tokens are derived. Emits
104
+ * `confirm` with a `HCustomTheme` (a hikari `CustomThemePreset`); the
105
+ * caller persists it via `useTheme().addCustomTheme()` and applies it
106
+ * with `setTheme()`.
107
+ */
108
+ export const HColorSchemeDialog = defineComponent({
109
+ name: "HkColorSchemeDialog",
110
+ props: {
111
+ modelValue: { type: Boolean, required: true },
112
+ /** Prefill dark tokens; defaults to the hikari synthwave dark scheme. */
113
+ initialDark: { type: Object, default: undefined },
114
+ /** Prefill light tokens; defaults to the hikari synthwave light scheme. */
115
+ initialLight: { type: Object, default: undefined },
116
+ },
117
+ emits: {
118
+ "update:modelValue": (_v: boolean) => true,
119
+ confirm: (_theme: HCustomTheme) => true,
120
+ },
121
+ setup(props, { emit }) {
122
+ const { t } = useI18n();
123
+ const { effectiveMode } = useTheme();
124
+ const modeTab = ref<string>(effectiveMode.value);
125
+ const themeName = ref("");
126
+
127
+ const dark = reactive<ThemeSchemeTokens>({ ...defaultDark });
128
+ const light = reactive<ThemeSchemeTokens>({ ...defaultLight });
129
+
130
+ const currentTokens = computed(() => (modeTab.value === "dark" ? dark : light));
131
+
132
+ function rederiveSurface() {
133
+ const target = modeTab.value === "dark" ? dark : light;
134
+ const derived = deriveSurface(target, modeTab.value === "dark");
135
+ derivedTokens.forEach((k) => {
136
+ target[k] = { ...derived[k] };
137
+ });
138
+ }
139
+
140
+ watch(
141
+ () => [
142
+ dark.primary, dark.secondary, dark.accent, dark.success, dark.error, dark.warning, dark.info,
143
+ light.primary, light.secondary, light.accent, light.success, light.error, light.warning, light.info,
144
+ ],
145
+ () => rederiveSurface(),
146
+ { deep: true },
147
+ );
148
+
149
+ watch(modeTab, () => nextTick(() => rederiveSurface()));
150
+
151
+ watch(
152
+ () => props.modelValue,
153
+ (open) => {
154
+ if (!open) return;
155
+ modeTab.value = effectiveMode.value;
156
+ themeName.value = t("hikari::theme.customThemeName");
157
+ const initialDark = props.initialDark as ThemeSchemeTokens | undefined;
158
+ const initialLight = props.initialLight as ThemeSchemeTokens | undefined;
159
+ Object.assign(dark, initialDark ?? defaultDark);
160
+ Object.assign(light, initialLight ?? defaultLight);
161
+ rederiveSurface();
162
+ },
163
+ );
164
+
165
+ function updateToken(key: ColorTokenKey, rgb: { r: number; g: number; b: number }) {
166
+ const target = modeTab.value === "dark" ? dark : light;
167
+ target[key] = { ...rgb };
168
+ }
169
+
170
+ function handleConfirm() {
171
+ const id = `custom-theme-${Date.now()}`;
172
+ emit("confirm", {
173
+ id,
174
+ name: themeName.value || t("hikari::theme.customThemeName"),
175
+ dark: { ...dark },
176
+ light: { ...light },
177
+ });
178
+ emit("update:modelValue", false);
179
+ }
180
+
181
+ const footerActions = computed<ModalAction[]>(() => [
182
+ {
183
+ label: t("hikari::protocol.decline"),
184
+ variant: "secondary" as const,
185
+ onClick: () => emit("update:modelValue", false),
186
+ },
187
+ {
188
+ label: t("hikari::theme.save"),
189
+ variant: "primary" as const,
190
+ onClick: handleConfirm,
191
+ },
192
+ ]);
193
+
194
+ const modeTabs = computed<HkTabItem[]>(() => [
195
+ { key: "dark", label: t("hikari::theme.modeDark") },
196
+ { key: "light", label: t("hikari::theme.modeLight") },
197
+ ]);
198
+
199
+ return () => (
200
+ <HModal
201
+ modelValue={props.modelValue}
202
+ onUpdate:modelValue={(v: boolean) => emit("update:modelValue", v)}
203
+ title={t("hikari::theme.editScheme")}
204
+ width="36rem"
205
+ footerActions={footerActions.value}
206
+ >
207
+ <div class="s-scheme-dialog">
208
+ <HInput
209
+ modelValue={themeName.value}
210
+ onUpdate:modelValue={(v: string) => { themeName.value = v; }}
211
+ label={t("hikari::theme.themeName")}
212
+ placeholder={t("hikari::theme.customThemeName")}
213
+ />
214
+ <HMorphingTabs
215
+ tag="div"
216
+ class="s-scheme-mode-switch"
217
+ modelValue={modeTab.value}
218
+ onUpdate:modelValue={(v: string) => { modeTab.value = v; }}
219
+ tabs={modeTabs.value}
220
+ />
221
+ <div class="s-scheme-colors">
222
+ {editableTokens.map((key) => (
223
+ <HColorPicker
224
+ key={key}
225
+ r={currentTokens.value[key].r}
226
+ g={currentTokens.value[key].g}
227
+ b={currentTokens.value[key].b}
228
+ label={t(`hikari::theme.tokens.${key}`)}
229
+ onChange={(rgb: { r: number; g: number; b: number }) => updateToken(key, rgb)}
230
+ />
231
+ ))}
232
+ </div>
233
+ </div>
234
+ </HModal>
235
+ );
236
+ },
237
+ });
@@ -0,0 +1,80 @@
1
+ import { computed, type Ref } from "vue";
2
+
3
+ function detectRegion(): string {
4
+ const lang = navigator.language;
5
+ const regionMap: Record<string, string> = {
6
+ zh: "CN", "zh-CN": "CN", "zh-Hans": "CN",
7
+ "zh-TW": "TW", "zh-HK": "HK",
8
+ ja: "JP", ko: "KR", de: "DE", fr: "FR", es: "ES", pt: "PT",
9
+ ar: "SA", ru: "RU", en: "US",
10
+ };
11
+ return regionMap[lang] ?? lang.split("-")[1]?.toUpperCase() ?? lang.toUpperCase();
12
+ }
13
+
14
+ function isLocalhostUrl(): boolean {
15
+ try {
16
+ const host = window.location.hostname;
17
+ return host === "localhost" || host === "127.0.0.1" || host === "[::1]";
18
+ } catch { return false; }
19
+ }
20
+
21
+ export type ConnectionStateInput =
22
+ | "connected"
23
+ | "disconnected"
24
+ | "connecting"
25
+ | "reconnecting"
26
+ | "failed";
27
+
28
+ export interface HkConnectionInfo {
29
+ state: "connected" | "reconnecting" | "disconnected";
30
+ tier: string;
31
+ quality: string;
32
+ latencyMs: number | null;
33
+ isLocalhost: boolean;
34
+ region: string;
35
+ retryCount: number;
36
+ maxRetries: number;
37
+ asn: number | null;
38
+ attemptNumber: number;
39
+ countdown: number;
40
+ }
41
+
42
+ export function useConnectionInfo(
43
+ connectionState: Ref<ConnectionStateInput>,
44
+ transportTier?: Ref<string>,
45
+ retryCount?: Ref<number>,
46
+ maxRetries?: Ref<number>,
47
+ attemptNumber?: Ref<number>,
48
+ countdown?: Ref<number>,
49
+ latencyMs?: Ref<number | null>,
50
+ ): { connectionInfo: Ref<HkConnectionInfo> } {
51
+ const info = computed<HkConnectionInfo>(() => {
52
+ const s = connectionState.value;
53
+ let state: HkConnectionInfo["state"] = "disconnected";
54
+ if (s === "connected") state = "connected";
55
+ else if (s === "connecting" || s === "reconnecting") state = "reconnecting";
56
+ else if (s === "failed") state = "disconnected";
57
+
58
+ const tierValue = transportTier?.value ?? (isLocalhostUrl() ? "local" : "ws");
59
+ let quality = "unknown";
60
+ if (s === "connected") {
61
+ quality = tierValue === "local" ? "excellent" : tierValue === "ws" ? "good" : "fair";
62
+ }
63
+
64
+ return {
65
+ state,
66
+ tier: tierValue,
67
+ quality,
68
+ latencyMs: latencyMs?.value ?? null,
69
+ isLocalhost: isLocalhostUrl(),
70
+ region: detectRegion(),
71
+ retryCount: retryCount?.value ?? 0,
72
+ maxRetries: maxRetries?.value ?? 3,
73
+ asn: null,
74
+ attemptNumber: attemptNumber?.value ?? 0,
75
+ countdown: countdown?.value ?? 0,
76
+ };
77
+ });
78
+
79
+ return { connectionInfo: info };
80
+ }
@@ -0,0 +1,133 @@
1
+ import { computed, defineComponent, onMounted, type PropType } from "vue";
2
+ import { useI18n } from "@celestia-island/hikari";
3
+ import { HkStatusBar } from "./HkStatusBar";
4
+ import {
5
+ useConnectionInfo,
6
+ type ConnectionStateInput,
7
+ type HkConnectionInfo,
8
+ } from "./HkConnectionInfo";
9
+ import { useConnectionProbe } from "../composables/useConnectionProbe";
10
+ import { useEngineHealth } from "../composables/useEngineHealth";
11
+
12
+
13
+ /** Secondary "engine/backend health" traffic-light segment. */
14
+ export interface HBackendStatus {
15
+ label: string;
16
+ state: "ok" | "warn" | "error" | "unknown";
17
+ }
18
+
19
+ const backendStateColor: Record<HBackendStatus["state"], string> = {
20
+ ok: "rgb(var(--color-success))",
21
+ warn: "rgb(var(--color-warning))",
22
+ error: "rgb(var(--color-error))",
23
+ unknown: "var(--color-muted)",
24
+ };
25
+
26
+ /**
27
+ * Batteries-included connection status footer: composes useConnectionProbe +
28
+ * useEngineHealth + useConnectionInfo + HkStatusBar. Slots:
29
+ * - `left` — extras after the status tags (e.g. a mock badge)
30
+ * - `right` — extras on the far right (e.g. an engine version tag)
31
+ */
32
+ export const HkConnectionStatus = defineComponent({
33
+ name: "HkConnectionStatus",
34
+ props: {
35
+ version: { type: String, default: "0.1.0" },
36
+ panelBuildHash: { type: String as PropType<string | undefined>, default: undefined },
37
+ /** API origin for the /api/health probe (defaults to same-origin ""). */
38
+ baseUrl: { type: String as PropType<string | undefined>, default: undefined },
39
+ backendStatus: { type: Object as PropType<HBackendStatus | undefined>, default: undefined },
40
+ standalone: { type: Boolean, default: true },
41
+ },
42
+ setup(props, { slots }) {
43
+
44
+ const { result: probe, retryNow } = useConnectionProbe();
45
+ const { health } = useEngineHealth(props.baseUrl);
46
+
47
+ const connectionState = computed<ConnectionStateInput>(() => probe.value.state);
48
+ const transportTier = computed(() => probe.value.transportTier);
49
+ const attemptNumber = computed(() => probe.value.attemptNumber);
50
+ const retryTotal = computed(() => probe.value.retryTotal);
51
+ const countdown = computed(() => probe.value.countdown);
52
+ const latencyMs = computed(() => probe.value.latencyMs);
53
+
54
+ const { connectionInfo } = useConnectionInfo(
55
+ connectionState,
56
+ transportTier,
57
+ attemptNumber,
58
+ retryTotal,
59
+ attemptNumber,
60
+ countdown,
61
+ latencyMs,
62
+ );
63
+
64
+ // Overlay the server-reported network context (region/asn) when available;
65
+ // "XX" is the backend's unknown-region sentinel, keep the local guess then.
66
+ const mergedInfo = computed<HkConnectionInfo>(() => {
67
+ const base = connectionInfo.value;
68
+ const net = health.value?.network;
69
+ if (!net) return base;
70
+ return {
71
+ ...base,
72
+ region: net.region && net.region !== "XX" ? net.region : base.region,
73
+ asn: net.asn ?? base.asn,
74
+ };
75
+ });
76
+
77
+ const connectionStatus = computed<"connected" | "reconnecting" | "disconnected">(() => {
78
+ const s = probe.value.state;
79
+ if (s === "connected") return "connected";
80
+ if (s === "connecting" || s === "reconnecting") return "reconnecting";
81
+ return "disconnected";
82
+ });
83
+
84
+ return () => {
85
+ const { t } = useI18n();
86
+ const bs = props.backendStatus;
87
+ const inner = (
88
+ <>
89
+ <div class="s-status-bar-left">
90
+ <HkStatusBar
91
+ standalone={false}
92
+ version={props.version}
93
+ panelBuildHash={props.panelBuildHash}
94
+ engineVersion={health.value?.engineVersion ?? null}
95
+ engineBuildHash={health.value?.engineBuildHash ?? undefined}
96
+ connectionStatus={connectionStatus.value}
97
+ connectionInfo={mergedInfo.value}
98
+ onRetry={retryNow}
99
+ transportTier={probe.value.transportTier}
100
+ attemptNumber={probe.value.attemptNumber}
101
+ countdown={probe.value.countdown}
102
+ />
103
+ {bs && (
104
+ <span class="s-status-bar-tag" style={{ cursor: "default" }}>
105
+ <span
106
+ class="s-status-bar-dot"
107
+ style={{ background: backendStateColor[bs.state] ?? backendStateColor.unknown }}
108
+ />
109
+ <span class="s-status-bar-tag-label">{bs.label}</span>
110
+ <span class="s-status-bar-tag-value">
111
+ {t(`hikari::statusBar.backend.${bs.state}`, bs.state)}
112
+ </span>
113
+ </span>
114
+ )}
115
+ {slots.left?.()}
116
+ </div>
117
+ <div class="s-status-bar-center" />
118
+ <div class="s-status-bar-right">
119
+ {slots.right?.()}
120
+ </div>
121
+ </>
122
+ );
123
+
124
+ if (!props.standalone) return inner;
125
+
126
+ return (
127
+ <footer class="s-status-bar">
128
+ {inner}
129
+ </footer>
130
+ );
131
+ };
132
+ },
133
+ });
@@ -0,0 +1,97 @@
1
+ import { defineComponent, ref, watch } from "vue";
2
+
3
+ export const PCountdownDigit = defineComponent({
4
+ name: "HkCountdownDigit",
5
+ props: {
6
+ value: { type: Number, default: 0 },
7
+ },
8
+ setup(props) {
9
+ const prevValue = ref(props.value);
10
+ const key = ref(0);
11
+ const oldDigit = ref(String(props.value));
12
+ const newDigit = ref(String(props.value));
13
+
14
+ watch(
15
+ () => props.value,
16
+ (next, old) => {
17
+ if (next !== old) {
18
+ oldDigit.value = String(old);
19
+ newDigit.value = String(next);
20
+ key.value++;
21
+ }
22
+ },
23
+ );
24
+
25
+ return () => (
26
+ <span
27
+ class="plana-countdown-digit"
28
+ style={{
29
+ display: "inline-flex",
30
+ alignItems: "center",
31
+ gap: "0",
32
+ fontVariantNumeric: "tabular-nums",
33
+ height: "1em",
34
+ lineHeight: 1,
35
+ fontWeight: 700,
36
+ }}
37
+ >
38
+ <span
39
+ key={key.value}
40
+ class="plana-countdown-digit-slot"
41
+ style={{
42
+ position: "relative",
43
+ display: "inline-flex",
44
+ flexDirection: "column",
45
+ alignItems: "center",
46
+ justifyContent: "center",
47
+ overflow: "hidden",
48
+ height: "1em",
49
+ width: "1ch",
50
+ }}
51
+ >
52
+ <span
53
+ class="plana-flip-out"
54
+ style={{
55
+ display: "block",
56
+ lineHeight: 1,
57
+ animation: "plana-flip-out 0.3s cubic-bezier(0.4, 0, 0.2, 1) both",
58
+ }}
59
+ >
60
+ {oldDigit.value}
61
+ </span>
62
+ <span
63
+ class="plana-flip-in"
64
+ style={{
65
+ display: "block",
66
+ lineHeight: 1,
67
+ position: "absolute",
68
+ inset: 0,
69
+ animation: "plana-flip-in 0.3s cubic-bezier(0.4, 0, 0.2, 1) both",
70
+ }}
71
+ >
72
+ {newDigit.value}
73
+ </span>
74
+ </span>
75
+ <span
76
+ style={{
77
+ fontSize: "0.75em",
78
+ opacity: 0.6,
79
+ fontWeight: 400,
80
+ }}
81
+ >
82
+ s
83
+ </span>
84
+ <style>{`
85
+ @keyframes plana-flip-out {
86
+ 0% { transform: translateY(0); opacity: 1; }
87
+ 100% { transform: translateY(-1em); opacity: 0; }
88
+ }
89
+ @keyframes plana-flip-in {
90
+ 0% { transform: translateY(1em); opacity: 0; }
91
+ 100% { transform: translateY(0); opacity: 1; }
92
+ }
93
+ `}</style>
94
+ </span>
95
+ );
96
+ },
97
+ });
@@ -1,5 +1,11 @@
1
- import { computed, defineComponent, onBeforeUnmount, ref, Transition, watch, type PropType } from "vue";
2
1
  import { ArrowLeft, Calendar, ChevronDown, ChevronLeft, ChevronRight, ChevronUp } from "lucide-vue-next";
2
+ import { computed, defineComponent, onBeforeUnmount, ref, Transition, watch, type PropType } from "vue";
3
+
4
+
5
+
6
+
7
+
8
+
3
9
 
4
10
  import { useI18n } from "../i18n/context";
5
11
  import "./HkDateTimePicker.scss";
@@ -1,5 +1,8 @@
1
+ import { Copy, RefreshCw, TriangleAlert as AlertTriangle } from "lucide-vue-next";
1
2
  import { defineComponent, onErrorCaptured, ref, type PropType, type VNode } from "vue";
2
- import { AlertTriangle, Copy, RefreshCw } from "lucide-vue-next";
3
+
4
+
5
+
3
6
  import { useClipboard } from "../runtime/useClipboard";
4
7
  import { useI18n } from "../i18n/context";
5
8
  import HButton from "./HkButton";
@@ -0,0 +1,123 @@
1
+ // Icon component styles
2
+ //
3
+ // These styles enable icons to inherit theme colors and support
4
+ // custom color overrides.
5
+
6
+ // ------
7
+ // Base Icon Styles
8
+ // ------
9
+
10
+ .hk-icon {
11
+ display: inline-flex;
12
+ align-items: center;
13
+ justify-content: center;
14
+
15
+ // Default: inherit color from parent element
16
+ // This allows buttons and other containers to control icon color
17
+ color: inherit;
18
+
19
+ // SVG inherits color; fill/stroke controlled by each icon's inline attributes
20
+ svg {
21
+ width: 100%;
22
+ height: 100%;
23
+ // MDI fill-based icons set fill="currentColor" in their SVG data
24
+ // Lucide/Feather stroke-based icons set stroke="currentColor" + fill="none"
25
+ // We must NOT override these here
26
+ }
27
+ }
28
+
29
+ // ------
30
+ // Icon Size Variants
31
+ // ------
32
+
33
+ .hk-icon-16 {
34
+ width: 1rem;
35
+ height: 1rem;
36
+ }
37
+
38
+ .hk-icon-20 {
39
+ width: 1.25rem;
40
+ height: 1.25rem;
41
+ }
42
+
43
+ .hk-icon-24 {
44
+ width: 1.5rem;
45
+ height: 1.5rem;
46
+ }
47
+
48
+ .hk-icon-32 {
49
+ width: 2rem;
50
+ height: 2rem;
51
+ }
52
+
53
+ .hk-icon-40 {
54
+ width: 2.5rem;
55
+ height: 2.5rem;
56
+ }
57
+
58
+ .hk-icon-48 {
59
+ width: 3rem;
60
+ height: 3rem;
61
+ }
62
+
63
+ .hk-icon-64 {
64
+ width: 4rem;
65
+ height: 4rem;
66
+ }
67
+
68
+ // ------
69
+ // Icon Color Variants
70
+ // ------
71
+
72
+ // Use theme primary color
73
+ .hk-icon-primary {
74
+ color: var(--hi-color-primary);
75
+ }
76
+
77
+ // Use theme secondary color
78
+ .hk-icon-secondary {
79
+ color: var(--hi-color-secondary);
80
+ }
81
+
82
+ // Use theme accent color
83
+ .hk-icon-accent {
84
+ color: var(--hi-color-accent);
85
+ }
86
+
87
+ // Use theme success color
88
+ .hk-icon-success {
89
+ color: var(--hi-color-success);
90
+ }
91
+
92
+ // Use theme warning color
93
+ .hk-icon-warning {
94
+ color: var(--hi-color-warning);
95
+ }
96
+
97
+ // Use theme danger color
98
+ .hk-icon-danger {
99
+ color: var(--hi-color-danger);
100
+ }
101
+
102
+ // Use secondary text color
103
+ .hk-icon-muted {
104
+ color: var(--hi-color-text-secondary);
105
+ }
106
+
107
+ // ------
108
+ // Transitions
109
+ // ------
110
+
111
+ .hk-icon-transition {
112
+ transition: color 150ms ease-in-out;
113
+
114
+ svg path,
115
+ svg circle,
116
+ svg rect,
117
+ svg ellipse,
118
+ svg polygon,
119
+ svg polyline,
120
+ svg line {
121
+ transition: fill 150ms ease-in-out, stroke 150ms ease-in-out;
122
+ }
123
+ }