@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
package/README.md CHANGED
@@ -51,4 +51,4 @@ import { HkButton, HkCard, HkInput } from "@celestia-island/hikari";
51
51
 
52
52
  ## License
53
53
 
54
- BUSL-1.1
54
+ Licensed under the [Synthetic Source License (SySL), Version 1.0](../../LICENSE).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@celestia-island/hikari",
3
- "version": "0.4.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": "~6.0",
62
+ "typescript": "^*",
57
63
  "vite": "^*",
58
- "vue-tsc": "^*"
64
+ "vue-tsc": "^*",
65
+ "vitest": "^*",
66
+ "@types/node": "^*"
59
67
  },
60
- "overrides": {
61
- "typescript": "~6.0"
62
- }
68
+ "sideEffects": [
69
+ "**/*.scss",
70
+ "**/*.css"
71
+ ]
63
72
  }
package/src/DemoApp.vue CHANGED
@@ -250,7 +250,7 @@
250
250
  </section>
251
251
 
252
252
  <section class="demo-footer">
253
- <p>Hikari v0.4.4 — Powered by celestia-island</p>
253
+ <p>Hikari v0.4.5 — Powered by celestia-island</p>
254
254
  </section>
255
255
  </div>
256
256
  </template>
@@ -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,12 @@
1
+ .s-auth-submit {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: 6px;
5
+ width: 100%;
6
+ }
7
+
8
+ .s-auth-submit-error {
9
+ margin: 0;
10
+ font-size: 0.8125rem;
11
+ color: rgb(var(--color-danger, 229 62 62));
12
+ }
@@ -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
+ }
@@ -1,6 +1,6 @@
1
1
  import { computed, defineComponent, type PropType } from "vue";
2
2
  import { useI18n } from "../i18n/context";
3
- import "../../../components/src/styles/components/avatar.scss";
3
+ import "./HkAvatar.scss";
4
4
 
5
5
  export default defineComponent({
6
6
  name: "HkAvatar",