@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
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@celestia-island/hikari",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.5",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Hikari Vue 3 component library — production-grade UI components based on shittim-chest design system",
|
|
@@ -26,6 +26,10 @@
|
|
|
26
26
|
},
|
|
27
27
|
"./styles": {
|
|
28
28
|
"import": "./src/styles/index.scss"
|
|
29
|
+
},
|
|
30
|
+
"./plugins": {
|
|
31
|
+
"import": "./src/plugins/index.ts",
|
|
32
|
+
"types": "./src/plugins/index.ts"
|
|
29
33
|
}
|
|
30
34
|
},
|
|
31
35
|
"files": [
|
|
@@ -46,18 +50,23 @@
|
|
|
46
50
|
"dompurify": "^*",
|
|
47
51
|
"echarts": "^*",
|
|
48
52
|
"lucide-vue-next": "^*",
|
|
49
|
-
"marked": "^*"
|
|
53
|
+
"marked": "^*",
|
|
54
|
+
"highlight.js": "^*"
|
|
50
55
|
},
|
|
51
56
|
"devDependencies": {
|
|
52
57
|
"@vitejs/plugin-vue": "^*",
|
|
53
58
|
"@vitejs/plugin-vue-jsx": "^*",
|
|
54
59
|
"@vue/server-renderer": "^*",
|
|
60
|
+
"happy-dom": "^*",
|
|
55
61
|
"sass": "^*",
|
|
56
|
-
"typescript": "
|
|
62
|
+
"typescript": "^*",
|
|
57
63
|
"vite": "^*",
|
|
58
|
-
"vue-tsc": "^*"
|
|
64
|
+
"vue-tsc": "^*",
|
|
65
|
+
"vitest": "^*",
|
|
66
|
+
"@types/node": "^*"
|
|
59
67
|
},
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
|
|
68
|
+
"sideEffects": [
|
|
69
|
+
"**/*.scss",
|
|
70
|
+
"**/*.css"
|
|
71
|
+
]
|
|
63
72
|
}
|
package/src/DemoApp.vue
CHANGED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { defineComponent, ref, type PropType, type VNode } from "vue";
|
|
2
|
+
import { Camera, Languages, LogOut, Menu } from "lucide-vue-next";
|
|
3
|
+
import { HBadge, HButton, HDivider, HPopover } from "@celestia-island/hikari";
|
|
4
|
+
|
|
5
|
+
export interface LocaleOption {
|
|
6
|
+
code: string;
|
|
7
|
+
label: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const HkAdminHeader = defineComponent({
|
|
11
|
+
name: "HkAdminHeader",
|
|
12
|
+
props: {
|
|
13
|
+
title: { type: String, required: true },
|
|
14
|
+
showHamburger: { type: Boolean, default: false },
|
|
15
|
+
compact: { type: Boolean, default: false },
|
|
16
|
+
actions: { type: Array as PropType<VNode[]>, default: () => [] },
|
|
17
|
+
username: { type: String, default: "" },
|
|
18
|
+
avatarUrl: { type: String, default: "" },
|
|
19
|
+
userEmail: { type: String, default: "" },
|
|
20
|
+
userGroups: { type: Array as PropType<{ id: string; name: string }[]>, default: () => [] },
|
|
21
|
+
showEmergencyStop: { type: Boolean, default: false },
|
|
22
|
+
emergencyStopActive: { type: Boolean, default: false },
|
|
23
|
+
emergencyStopActiveLabel: { type: String, default: "" },
|
|
24
|
+
emergencyStopActiveTitle: { type: String, default: "" },
|
|
25
|
+
emergencyStopLabel: { type: String, default: "" },
|
|
26
|
+
emergencyStopTitle: { type: String, default: "" },
|
|
27
|
+
emergencyStopLoading: { type: Boolean, default: false },
|
|
28
|
+
avatarMenuLabel: { type: String, default: "Avatar" },
|
|
29
|
+
localeMenuLabel: { type: String, default: "Language" },
|
|
30
|
+
logoutLabel: { type: String, default: "Logout" },
|
|
31
|
+
adminGroupLabel: { type: String, default: "Administrators" },
|
|
32
|
+
},
|
|
33
|
+
emits: {
|
|
34
|
+
logout: () => true,
|
|
35
|
+
hamburger: () => true,
|
|
36
|
+
emergencyStop: () => true,
|
|
37
|
+
avatarClick: () => true,
|
|
38
|
+
},
|
|
39
|
+
setup(props, { emit, slots }) {
|
|
40
|
+
const userMenuOpen = ref(false);
|
|
41
|
+
const userTriggerRef = ref<HTMLElement>();
|
|
42
|
+
const avatarModalOpen = ref(false);
|
|
43
|
+
const avatarFailed = ref(false);
|
|
44
|
+
const localeMenuOpen = ref(false);
|
|
45
|
+
const localeTriggerRef = ref<HTMLElement | null>(null);
|
|
46
|
+
|
|
47
|
+
return () => (
|
|
48
|
+
<header
|
|
49
|
+
class={[
|
|
50
|
+
"s-glass-header",
|
|
51
|
+
props.compact ? "px-4 gap-2" : "px-6 gap-3",
|
|
52
|
+
]}
|
|
53
|
+
>
|
|
54
|
+
{props.showHamburger && (
|
|
55
|
+
<HButton
|
|
56
|
+
variant="ghost"
|
|
57
|
+
size="sm"
|
|
58
|
+
onClick={() => emit("hamburger")}
|
|
59
|
+
>
|
|
60
|
+
<Menu size={20} class="w-5 h-5" />
|
|
61
|
+
</HButton>
|
|
62
|
+
)}
|
|
63
|
+
|
|
64
|
+
<div ref={userTriggerRef} class="flex items-center gap-2">
|
|
65
|
+
<button
|
|
66
|
+
class={[
|
|
67
|
+
"w-7 h-7 rounded-full overflow-hidden shrink-0 cursor-pointer transition-opacity relative group p-0 border-0",
|
|
68
|
+
props.avatarUrl
|
|
69
|
+
? ""
|
|
70
|
+
: "bg-primary/10 border-2 border-primary/15 hover:border-primary/30",
|
|
71
|
+
]}
|
|
72
|
+
onClick={(e) => {
|
|
73
|
+
e.stopPropagation();
|
|
74
|
+
avatarModalOpen.value = true;
|
|
75
|
+
emit("avatarClick");
|
|
76
|
+
}}
|
|
77
|
+
>
|
|
78
|
+
{props.avatarUrl && !avatarFailed.value ? (
|
|
79
|
+
<img
|
|
80
|
+
src={props.avatarUrl}
|
|
81
|
+
alt={props.username}
|
|
82
|
+
class="w-full h-full object-cover"
|
|
83
|
+
onError={() => { avatarFailed.value = true; }}
|
|
84
|
+
/>
|
|
85
|
+
) : (
|
|
86
|
+
<span class="text-xs font-bold text-primary/70">
|
|
87
|
+
{props.username?.charAt(0).toUpperCase() || "?"}
|
|
88
|
+
</span>
|
|
89
|
+
)}
|
|
90
|
+
<div class="absolute inset-0 rounded-full bg-black/30 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
|
|
91
|
+
<Camera size={10} class="text-white" />
|
|
92
|
+
</div>
|
|
93
|
+
</button>
|
|
94
|
+
<span class="text-sm font-semibold text-text truncate max-w-[8rem]">
|
|
95
|
+
{props.username}
|
|
96
|
+
</span>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<HPopover
|
|
100
|
+
modelValue={userMenuOpen.value}
|
|
101
|
+
onUpdate:modelValue={(v: boolean) => { userMenuOpen.value = v; }}
|
|
102
|
+
placement="bottom-start"
|
|
103
|
+
anchorRef={userTriggerRef.value ?? null}
|
|
104
|
+
class="w-40"
|
|
105
|
+
>
|
|
106
|
+
<div class="py-1">
|
|
107
|
+
{props.userGroups && props.userGroups.length > 0 && (
|
|
108
|
+
<>
|
|
109
|
+
<div class="px-3 py-1.5 flex flex-wrap gap-1">
|
|
110
|
+
{props.userGroups.map((g: { id: string; name: string }) => (
|
|
111
|
+
<HBadge
|
|
112
|
+
key={g.id}
|
|
113
|
+
variant={g.name === "Administrators" ? "error" : "primary"}
|
|
114
|
+
size="sm"
|
|
115
|
+
>
|
|
116
|
+
{g.name === "Administrators" ? props.adminGroupLabel : g.name}
|
|
117
|
+
</HBadge>
|
|
118
|
+
))}
|
|
119
|
+
</div>
|
|
120
|
+
<HDivider spacing="sm" />
|
|
121
|
+
</>
|
|
122
|
+
)}
|
|
123
|
+
<button
|
|
124
|
+
class="s-popup-menu-item"
|
|
125
|
+
onClick={() => {
|
|
126
|
+
userMenuOpen.value = false;
|
|
127
|
+
avatarModalOpen.value = true;
|
|
128
|
+
}}
|
|
129
|
+
>
|
|
130
|
+
<Camera size={14} />
|
|
131
|
+
{props.avatarMenuLabel}
|
|
132
|
+
</button>
|
|
133
|
+
<div ref={localeTriggerRef}>
|
|
134
|
+
<button
|
|
135
|
+
class="s-popup-menu-item"
|
|
136
|
+
onClick={() => (localeMenuOpen.value = !localeMenuOpen.value)}
|
|
137
|
+
>
|
|
138
|
+
<Languages size={14} />
|
|
139
|
+
{props.localeMenuLabel}
|
|
140
|
+
</button>
|
|
141
|
+
{slots["locale-picker"]?.({
|
|
142
|
+
open: localeMenuOpen.value,
|
|
143
|
+
onUpdateOpen: (v: boolean) => {
|
|
144
|
+
localeMenuOpen.value = v;
|
|
145
|
+
if (!v) userMenuOpen.value = false;
|
|
146
|
+
},
|
|
147
|
+
triggerRef: localeTriggerRef.value,
|
|
148
|
+
})}
|
|
149
|
+
</div>
|
|
150
|
+
{slots["user-menu-extra"]?.()}
|
|
151
|
+
<HDivider spacing="sm" />
|
|
152
|
+
<button
|
|
153
|
+
class="s-popup-menu-item"
|
|
154
|
+
onClick={() => {
|
|
155
|
+
userMenuOpen.value = false;
|
|
156
|
+
emit("logout");
|
|
157
|
+
}}
|
|
158
|
+
>
|
|
159
|
+
<LogOut size={14} />
|
|
160
|
+
{props.logoutLabel}
|
|
161
|
+
</button>
|
|
162
|
+
</div>
|
|
163
|
+
</HPopover>
|
|
164
|
+
|
|
165
|
+
<h2 class="text-sm font-semibold text-text">
|
|
166
|
+
{props.title}
|
|
167
|
+
</h2>
|
|
168
|
+
<div class="ml-auto flex items-center gap-1.5">
|
|
169
|
+
{props.showEmergencyStop && (
|
|
170
|
+
<button
|
|
171
|
+
class={[
|
|
172
|
+
"px-3 py-1 rounded-md text-xs font-bold border transition-all",
|
|
173
|
+
props.emergencyStopLoading ? "opacity-50 cursor-wait" : "cursor-pointer",
|
|
174
|
+
props.emergencyStopActive
|
|
175
|
+
? "bg-red-600 text-white border-red-700 animate-pulse"
|
|
176
|
+
: "bg-red-600/10 text-red-500 border-red-500/40 hover:bg-red-600/25",
|
|
177
|
+
]}
|
|
178
|
+
disabled={props.emergencyStopLoading}
|
|
179
|
+
title={props.emergencyStopActive
|
|
180
|
+
? props.emergencyStopActiveTitle
|
|
181
|
+
: props.emergencyStopTitle}
|
|
182
|
+
onClick={() => emit("emergencyStop")}
|
|
183
|
+
>
|
|
184
|
+
{props.emergencyStopActive
|
|
185
|
+
? props.emergencyStopActiveLabel
|
|
186
|
+
: props.emergencyStopLabel}
|
|
187
|
+
</button>
|
|
188
|
+
)}
|
|
189
|
+
{slots["emergency-stop-extra"]?.()}
|
|
190
|
+
{(props.actions || []).map((vnode, i) => (
|
|
191
|
+
<span key={i} class="flex items-center gap-1">
|
|
192
|
+
{vnode}
|
|
193
|
+
</span>
|
|
194
|
+
))}
|
|
195
|
+
{slots["theme-toggle"]?.()}
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
{slots["avatar-modal"]?.({
|
|
199
|
+
open: avatarModalOpen.value,
|
|
200
|
+
onUpdateOpen: (v: boolean) => { avatarModalOpen.value = v; },
|
|
201
|
+
})}
|
|
202
|
+
</header>
|
|
203
|
+
);
|
|
204
|
+
},
|
|
205
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { defineComponent, ref } from "vue";
|
|
2
|
+
import { HDrawer, HScrollContainer, useBreakpoint } from "@celestia-island/hikari";
|
|
3
|
+
import { provideActionBar } from "../composables/useActionBar";
|
|
4
|
+
|
|
5
|
+
export const HkAdminShell = defineComponent({
|
|
6
|
+
name: "HkAdminShell",
|
|
7
|
+
props: {
|
|
8
|
+
sidebarCollapsed: { type: Boolean, default: false },
|
|
9
|
+
sidebarWidth: { type: String, default: "224px" },
|
|
10
|
+
mobileBreakpoint: { type: Number, default: 768 },
|
|
11
|
+
footerHeight: { type: String, default: "var(--s-footer-height)" },
|
|
12
|
+
navTitle: { type: String, default: "Navigation" },
|
|
13
|
+
},
|
|
14
|
+
setup(props, { slots }) {
|
|
15
|
+
const { isDesktop } = useBreakpoint();
|
|
16
|
+
const sidebarOpen = ref(false);
|
|
17
|
+
|
|
18
|
+
const actionBar = provideActionBar();
|
|
19
|
+
|
|
20
|
+
const toggleHamburger = () => {
|
|
21
|
+
sidebarOpen.value = !sidebarOpen.value;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const closeSidebar = () => {
|
|
25
|
+
sidebarOpen.value = false;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
return () => (
|
|
29
|
+
<div class="flex flex-col h-full w-full overflow-hidden">
|
|
30
|
+
{slots.header && (
|
|
31
|
+
<div style={{ flexShrink: 0 }}>
|
|
32
|
+
{slots.header({
|
|
33
|
+
isDesktop: isDesktop.value,
|
|
34
|
+
showHamburger: !isDesktop.value,
|
|
35
|
+
compact: !isDesktop.value,
|
|
36
|
+
actions: actionBar.actions.value ? actionBar.actions.value() : [],
|
|
37
|
+
onHamburger: toggleHamburger,
|
|
38
|
+
})}
|
|
39
|
+
</div>
|
|
40
|
+
)}
|
|
41
|
+
|
|
42
|
+
<div class="flex flex-1 min-h-0" style={{ paddingBottom: props.footerHeight }}>
|
|
43
|
+
{isDesktop.value && !props.sidebarCollapsed && slots.sidebar && (
|
|
44
|
+
<aside
|
|
45
|
+
style={{
|
|
46
|
+
width: props.sidebarWidth,
|
|
47
|
+
flexShrink: 0,
|
|
48
|
+
borderRight: "1px solid var(--border-faint, rgb(var(--color-border) / 10%))",
|
|
49
|
+
background: "rgb(var(--color-surface))",
|
|
50
|
+
overflow: "hidden",
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
{slots.sidebar({ collapsed: false, onNavigate: closeSidebar })}
|
|
54
|
+
</aside>
|
|
55
|
+
)}
|
|
56
|
+
<main class="flex-1 flex flex-col min-w-0 min-h-0">
|
|
57
|
+
<HScrollContainer class="flex-1 p-6 min-h-0">
|
|
58
|
+
{slots.content?.()}
|
|
59
|
+
</HScrollContainer>
|
|
60
|
+
</main>
|
|
61
|
+
|
|
62
|
+
{!isDesktop.value && (
|
|
63
|
+
<HDrawer
|
|
64
|
+
modelValue={sidebarOpen.value}
|
|
65
|
+
onUpdate:modelValue={(v: boolean) => (sidebarOpen.value = v)}
|
|
66
|
+
side="left"
|
|
67
|
+
size="280px"
|
|
68
|
+
title={props.navTitle}
|
|
69
|
+
>
|
|
70
|
+
{slots.sidebar?.({ collapsed: false, onNavigate: closeSidebar })}
|
|
71
|
+
</HDrawer>
|
|
72
|
+
)}
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
{slots.footer && (
|
|
76
|
+
<footer class="s-status-bar" style={{ position: "fixed", bottom: 0, left: 0, right: 0, zIndex: 40 }}>
|
|
77
|
+
{slots.footer()}
|
|
78
|
+
</footer>
|
|
79
|
+
)}
|
|
80
|
+
|
|
81
|
+
{slots.overlays}
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
},
|
|
85
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { defineComponent, type PropType } from "vue";
|
|
2
|
+
import { HAlert, HCard, HEmptyState, HSpinner, HTable, useI18n } from "@celestia-island/hikari";
|
|
3
|
+
import { HkPageHeader } from "./HkPageHeader";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
/** Column definition passed through to HTable. */
|
|
7
|
+
export interface HTableColumn {
|
|
8
|
+
key: string;
|
|
9
|
+
title: string;
|
|
10
|
+
width?: string;
|
|
11
|
+
sortable?: boolean;
|
|
12
|
+
align?: "left" | "center" | "right";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* CRUD table-page scaffold: HkPageHeader + loading spinner + error alert +
|
|
17
|
+
* empty state + HTable. Slots:
|
|
18
|
+
* - `actions` — header actions (e.g. a "Create" button)
|
|
19
|
+
* - `cell-<key>` — per-column cell templates, forwarded to HTable
|
|
20
|
+
* - `create-modal` — create dialog, rendered after the table
|
|
21
|
+
* - `edit-modal` — edit dialog, rendered after the table
|
|
22
|
+
*/
|
|
23
|
+
export const HkAdminTablePage = defineComponent({
|
|
24
|
+
name: "HkAdminTablePage",
|
|
25
|
+
props: {
|
|
26
|
+
title: { type: String, default: "" },
|
|
27
|
+
loading: { type: Boolean, default: false },
|
|
28
|
+
error: { type: String as PropType<string | undefined>, default: undefined },
|
|
29
|
+
rows: { type: Array as PropType<Record<string, unknown>[]>, required: true },
|
|
30
|
+
columns: { type: Array as PropType<HTableColumn[]>, required: true },
|
|
31
|
+
rowKey: { type: String, default: "id" },
|
|
32
|
+
emptyTitle: { type: String, default: "" },
|
|
33
|
+
emptyDescription: { type: String as PropType<string | undefined>, default: undefined },
|
|
34
|
+
},
|
|
35
|
+
setup(props, { slots }) {
|
|
36
|
+
|
|
37
|
+
return () => {
|
|
38
|
+
const { t } = useI18n();
|
|
39
|
+
const emptyTitle = props.emptyTitle || t("hikari::tablePage.emptyTitle", "No data");
|
|
40
|
+
return (
|
|
41
|
+
<div>
|
|
42
|
+
{props.title ? (
|
|
43
|
+
<HkPageHeader title={props.title}>
|
|
44
|
+
{{ actions: () => slots.actions?.() }}
|
|
45
|
+
</HkPageHeader>
|
|
46
|
+
) : slots.actions ? (
|
|
47
|
+
<div style={{ display: "flex", justifyContent: "flex-end", marginBottom: "var(--space-16, 1rem)" }}>
|
|
48
|
+
{slots.actions()}
|
|
49
|
+
</div>
|
|
50
|
+
) : null}
|
|
51
|
+
{props.error ? (
|
|
52
|
+
<HAlert message={props.error} />
|
|
53
|
+
) : props.loading && !props.rows.length ? (
|
|
54
|
+
<HSpinner center />
|
|
55
|
+
) : !props.rows.length ? (
|
|
56
|
+
<HEmptyState title={emptyTitle} description={props.emptyDescription} />
|
|
57
|
+
) : (
|
|
58
|
+
<HCard padded={false}>
|
|
59
|
+
<HTable columns={props.columns} rows={props.rows} rowKey={props.rowKey}>
|
|
60
|
+
{slots}
|
|
61
|
+
</HTable>
|
|
62
|
+
</HCard>
|
|
63
|
+
)}
|
|
64
|
+
{slots["create-modal"]?.()}
|
|
65
|
+
{slots["edit-modal"]?.()}
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { defineComponent, type PropType } from "vue";
|
|
2
|
+
|
|
3
|
+
export const HkAuthCard = defineComponent({
|
|
4
|
+
name: "HkAuthCard",
|
|
5
|
+
props: {
|
|
6
|
+
title: { type: String, required: true },
|
|
7
|
+
subtitle: { type: String, default: "" },
|
|
8
|
+
},
|
|
9
|
+
setup(props, { slots }) {
|
|
10
|
+
return () => (
|
|
11
|
+
<div class="s-auth-card">
|
|
12
|
+
{slots.logo && (
|
|
13
|
+
<div class="s-auth-header">
|
|
14
|
+
{slots.logo()}
|
|
15
|
+
<h1 class="s-auth-title">{props.title}</h1>
|
|
16
|
+
{props.subtitle && <p class="s-auth-subtitle">{props.subtitle}</p>}
|
|
17
|
+
</div>
|
|
18
|
+
)}
|
|
19
|
+
{!slots.logo && (
|
|
20
|
+
<div class="s-auth-header">
|
|
21
|
+
<h1 class="s-auth-title">{props.title}</h1>
|
|
22
|
+
{props.subtitle && <p class="s-auth-subtitle">{props.subtitle}</p>}
|
|
23
|
+
</div>
|
|
24
|
+
)}
|
|
25
|
+
<div class="s-auth-form">
|
|
26
|
+
{slots.default?.()}
|
|
27
|
+
</div>
|
|
28
|
+
{slots.footer && (
|
|
29
|
+
<div class="s-auth-footer">
|
|
30
|
+
{slots.footer()}
|
|
31
|
+
</div>
|
|
32
|
+
)}
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
},
|
|
36
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { computed, defineComponent, ref, type PropType } from "vue";
|
|
2
|
+
import { HButton, useI18n } from "@celestia-island/hikari";
|
|
3
|
+
import { solvePow, type PowChallenge, type PowSolution } from "../utils/pow";
|
|
4
|
+
|
|
5
|
+
export interface AuthSubmitContext {
|
|
6
|
+
/** Solved PoW for the challenge the backend issued (if any). */
|
|
7
|
+
pow?: PowSolution;
|
|
8
|
+
/** Captcha verification token (if the flow requires one). */
|
|
9
|
+
captchaToken?: string | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Submit button with an auth-challenge lifecycle (the plana-ui half of the
|
|
14
|
+
* auth kit; hikari provides the base input variants, the backend provides
|
|
15
|
+
* the challenge/captcha facilities this wraps):
|
|
16
|
+
*
|
|
17
|
+
* 1. `onGetChallenge` (optional) — ask the backend for a `{ seed, bits }`
|
|
18
|
+
* PoW challenge.
|
|
19
|
+
* 2. Solve it with the built-in `solvePow` (or a custom `solver`).
|
|
20
|
+
* 3. `onCaptcha` (optional) — acquire a captcha verification token.
|
|
21
|
+
* 4. Call `onSubmit` with the assembled `AuthSubmitContext`.
|
|
22
|
+
*
|
|
23
|
+
* While solving, the button shows its own spinner; `loading` stays
|
|
24
|
+
* external so callers can reflect server-side submission progress.
|
|
25
|
+
*/
|
|
26
|
+
export default defineComponent({
|
|
27
|
+
name: "HkAuthSubmitButton",
|
|
28
|
+
props: {
|
|
29
|
+
label: { type: String, required: true },
|
|
30
|
+
loading: { type: Boolean, default: false },
|
|
31
|
+
disabled: { type: Boolean, default: false },
|
|
32
|
+
error: { type: String, default: undefined },
|
|
33
|
+
block: { type: Boolean, default: false },
|
|
34
|
+
size: { type: String as () => "sm" | "md" | "lg", default: "md" },
|
|
35
|
+
variant: { type: String as () => "primary" | "secondary" | "ghost", default: "primary" },
|
|
36
|
+
/** Fetch the backend PoW challenge; return null to skip PoW. */
|
|
37
|
+
onGetChallenge: { type: Function as PropType<() => Promise<PowChallenge | null>>, default: undefined },
|
|
38
|
+
/** Custom solver; defaults to the built-in subtle-based solvePow. */
|
|
39
|
+
solver: { type: Function as PropType<(c: PowChallenge) => Promise<number>>, default: undefined },
|
|
40
|
+
/** Acquire a captcha token; return null to skip captcha. */
|
|
41
|
+
onCaptcha: { type: Function as PropType<() => Promise<string | null>>, default: undefined },
|
|
42
|
+
/** Invoked with the assembled challenge context when ready. */
|
|
43
|
+
doSubmit: { type: Function as PropType<(ctx: AuthSubmitContext) => Promise<void>>, required: true },
|
|
44
|
+
},
|
|
45
|
+
emits: {
|
|
46
|
+
error: (_message: string) => true,
|
|
47
|
+
},
|
|
48
|
+
setup(props, { emit }) {
|
|
49
|
+
const { t } = useI18n();
|
|
50
|
+
const solving = ref(false);
|
|
51
|
+
|
|
52
|
+
const busy = computed(() => props.loading || solving.value);
|
|
53
|
+
|
|
54
|
+
async function handleClick() {
|
|
55
|
+
if (busy.value) return;
|
|
56
|
+
solving.value = true;
|
|
57
|
+
try {
|
|
58
|
+
const ctx: AuthSubmitContext = { captchaToken: null };
|
|
59
|
+
const challenge = props.onGetChallenge ? await props.onGetChallenge() : null;
|
|
60
|
+
if (challenge && challenge.bits > 0) {
|
|
61
|
+
const solve = props.solver ?? solvePow;
|
|
62
|
+
const counter = await solve(challenge);
|
|
63
|
+
ctx.pow = { seed: challenge.seed, bits: challenge.bits, counter };
|
|
64
|
+
}
|
|
65
|
+
if (props.onCaptcha) {
|
|
66
|
+
ctx.captchaToken = await props.onCaptcha();
|
|
67
|
+
}
|
|
68
|
+
await props.doSubmit(ctx);
|
|
69
|
+
} catch (e) {
|
|
70
|
+
emit("error", e instanceof Error ? e.message : String(e));
|
|
71
|
+
} finally {
|
|
72
|
+
solving.value = false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return () => (
|
|
77
|
+
<div class="s-auth-submit">
|
|
78
|
+
<HButton
|
|
79
|
+
variant={props.variant}
|
|
80
|
+
size={props.size}
|
|
81
|
+
block={props.block}
|
|
82
|
+
loading={busy.value}
|
|
83
|
+
disabled={props.disabled || busy.value}
|
|
84
|
+
onClick={() => void handleClick()}
|
|
85
|
+
>
|
|
86
|
+
{solving.value
|
|
87
|
+
? t("hikari::auth.solvingChallenge", "Verifying challenge…")
|
|
88
|
+
: props.label}
|
|
89
|
+
</HButton>
|
|
90
|
+
{props.error && <p class="s-auth-submit-error">{props.error}</p>}
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
},
|
|
94
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// ------
|
|
2
|
+
// Hikari Avatar Component
|
|
3
|
+
// ------
|
|
4
|
+
|
|
5
|
+
.hk-avatar {
|
|
6
|
+
display: inline-flex;
|
|
7
|
+
align-items: center;
|
|
8
|
+
justify-content: center;
|
|
9
|
+
overflow: hidden;
|
|
10
|
+
background-color: var(--hi-color-gray-200, #E5E7EB);
|
|
11
|
+
color: var(--hi-color-gray-600, #4B5563);
|
|
12
|
+
font-weight: 500;
|
|
13
|
+
transition: all 0.2s ease;
|
|
14
|
+
|
|
15
|
+
img {
|
|
16
|
+
width: 100%;
|
|
17
|
+
height: 100%;
|
|
18
|
+
object-fit: cover;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.hk-avatar-xs {
|
|
23
|
+
width: 24px;
|
|
24
|
+
height: 24px;
|
|
25
|
+
font-size: 0.625rem;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.hk-avatar-sm {
|
|
29
|
+
width: 32px;
|
|
30
|
+
height: 32px;
|
|
31
|
+
font-size: 0.75rem;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.hk-avatar-md {
|
|
35
|
+
width: 40px;
|
|
36
|
+
height: 40px;
|
|
37
|
+
font-size: 0.875rem;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.hk-avatar-lg {
|
|
41
|
+
width: 48px;
|
|
42
|
+
height: 48px;
|
|
43
|
+
font-size: 1rem;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.hk-avatar-xl {
|
|
47
|
+
width: 64px;
|
|
48
|
+
height: 64px;
|
|
49
|
+
font-size: 1.25rem;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.hk-avatar-circular {
|
|
53
|
+
border-radius: 50%;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.hk-avatar-rounded {
|
|
57
|
+
border-radius: 8px;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.hk-avatar-square {
|
|
61
|
+
border-radius: 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.hk-avatar-fallback {
|
|
65
|
+
width: 100%;
|
|
66
|
+
height: 100%;
|
|
67
|
+
display: flex;
|
|
68
|
+
align-items: center;
|
|
69
|
+
justify-content: center;
|
|
70
|
+
user-select: none;
|
|
71
|
+
font-weight: 600;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.hk-avatar-icon {
|
|
75
|
+
color: var(--hi-color-text-secondary);
|
|
76
|
+
opacity: 0.8;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.hk-avatar-img {
|
|
80
|
+
display: block;
|
|
81
|
+
width: 100%;
|
|
82
|
+
height: 100%;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
[data-theme="dark"] .hk-avatar {
|
|
86
|
+
background-color: var(--hi-surface);
|
|
87
|
+
color: var(--hi-color-text-secondary);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
[data-theme="dark"] .hk-avatar-icon {
|
|
91
|
+
color: var(--hi-color-text-secondary);
|
|
92
|
+
opacity: 0.6;
|
|
93
|
+
}
|