@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,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
|
-
|
|
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
|
+
}
|