@celestia-island/hikari 0.4.4

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 (187) hide show
  1. package/README.md +54 -0
  2. package/package.json +63 -0
  3. package/src/DemoApp.vue +400 -0
  4. package/src/components/HkActionBar.tsx +15 -0
  5. package/src/components/HkAlert.scss +121 -0
  6. package/src/components/HkAlert.tsx +85 -0
  7. package/src/components/HkAvatar.tsx +29 -0
  8. package/src/components/HkBadge.scss +117 -0
  9. package/src/components/HkBadge.tsx +50 -0
  10. package/src/components/HkBreadcrumb.tsx +39 -0
  11. package/src/components/HkButton.scss +128 -0
  12. package/src/components/HkButton.tsx +74 -0
  13. package/src/components/HkCard.scss +49 -0
  14. package/src/components/HkCard.tsx +38 -0
  15. package/src/components/HkCheckbox.scss +102 -0
  16. package/src/components/HkCheckbox.tsx +85 -0
  17. package/src/components/HkCollapse.tsx +48 -0
  18. package/src/components/HkColorPicker.scss +148 -0
  19. package/src/components/HkColorPicker.tsx +204 -0
  20. package/src/components/HkConfirmDialog.scss +12 -0
  21. package/src/components/HkConfirmDialog.tsx +75 -0
  22. package/src/components/HkDateTimePicker.scss +361 -0
  23. package/src/components/HkDateTimePicker.tsx +424 -0
  24. package/src/components/HkDivider.scss +85 -0
  25. package/src/components/HkDivider.tsx +51 -0
  26. package/src/components/HkDraggableGrid.scss +132 -0
  27. package/src/components/HkDraggableGrid.tsx +291 -0
  28. package/src/components/HkDraggableList.scss +110 -0
  29. package/src/components/HkDraggableList.tsx +238 -0
  30. package/src/components/HkDrawer.scss +175 -0
  31. package/src/components/HkDrawer.tsx +171 -0
  32. package/src/components/HkEmptyState.scss +42 -0
  33. package/src/components/HkEmptyState.tsx +41 -0
  34. package/src/components/HkErrorBoundary.scss +64 -0
  35. package/src/components/HkErrorBoundary.tsx +82 -0
  36. package/src/components/HkGaugeRing.scss +4 -0
  37. package/src/components/HkGaugeRing.tsx +130 -0
  38. package/src/components/HkHoverRevealAction.scss +66 -0
  39. package/src/components/HkHoverRevealAction.tsx +77 -0
  40. package/src/components/HkIcon.tsx +43 -0
  41. package/src/components/HkIconButton.tsx +42 -0
  42. package/src/components/HkImageViewer.scss +33 -0
  43. package/src/components/HkImageViewer.tsx +250 -0
  44. package/src/components/HkInput.scss +172 -0
  45. package/src/components/HkInput.tsx +175 -0
  46. package/src/components/HkKbd.tsx +26 -0
  47. package/src/components/HkKeywordSearchModal.scss +148 -0
  48. package/src/components/HkKeywordSearchModal.tsx +308 -0
  49. package/src/components/HkListTransition.scss +131 -0
  50. package/src/components/HkListTransition.tsx +29 -0
  51. package/src/components/HkLocalePickerPopup.tsx +42 -0
  52. package/src/components/HkLogo.scss +36 -0
  53. package/src/components/HkLogo.tsx +93 -0
  54. package/src/components/HkMarkdownRenderer.scss +141 -0
  55. package/src/components/HkMarkdownRenderer.tsx +123 -0
  56. package/src/components/HkMediaControlBar.scss +65 -0
  57. package/src/components/HkMediaControlBar.tsx +115 -0
  58. package/src/components/HkMediaPlayer.scss +29 -0
  59. package/src/components/HkMediaPlayer.tsx +210 -0
  60. package/src/components/HkMediaSlider.scss +54 -0
  61. package/src/components/HkMediaSlider.tsx +81 -0
  62. package/src/components/HkMediaVisualizer.scss +7 -0
  63. package/src/components/HkMediaVisualizer.tsx +119 -0
  64. package/src/components/HkMinimap.scss +76 -0
  65. package/src/components/HkMinimap.tsx +255 -0
  66. package/src/components/HkModal.scss +318 -0
  67. package/src/components/HkModal.tsx +481 -0
  68. package/src/components/HkModalBreadcrumb.scss +55 -0
  69. package/src/components/HkModalBreadcrumb.tsx +113 -0
  70. package/src/components/HkMorphingTabs.scss +141 -0
  71. package/src/components/HkMorphingTabs.tsx +113 -0
  72. package/src/components/HkNavItem.scss +81 -0
  73. package/src/components/HkNavItem.tsx +157 -0
  74. package/src/components/HkNumberInput.scss +154 -0
  75. package/src/components/HkNumberInput.tsx +161 -0
  76. package/src/components/HkPasswordInput.scss +289 -0
  77. package/src/components/HkPasswordInput.tsx +533 -0
  78. package/src/components/HkPhaseTransition.scss +25 -0
  79. package/src/components/HkPhaseTransition.tsx +40 -0
  80. package/src/components/HkPopover.scss +82 -0
  81. package/src/components/HkPopover.tsx +410 -0
  82. package/src/components/HkPopupSelect.scss +136 -0
  83. package/src/components/HkPopupSelect.tsx +199 -0
  84. package/src/components/HkProgressBar.scss +106 -0
  85. package/src/components/HkProgressBar.tsx +61 -0
  86. package/src/components/HkProgressDialog.tsx +67 -0
  87. package/src/components/HkProgressRing.scss +56 -0
  88. package/src/components/HkProgressRing.tsx +82 -0
  89. package/src/components/HkRadio.scss +119 -0
  90. package/src/components/HkRadio.tsx +75 -0
  91. package/src/components/HkRollingNumber.scss +37 -0
  92. package/src/components/HkRollingNumber.tsx +97 -0
  93. package/src/components/HkScrollContainer.scss +101 -0
  94. package/src/components/HkScrollContainer.tsx +362 -0
  95. package/src/components/HkSearchInput.scss +132 -0
  96. package/src/components/HkSearchInput.tsx +117 -0
  97. package/src/components/HkSelect.scss +143 -0
  98. package/src/components/HkSelect.tsx +230 -0
  99. package/src/components/HkSelectionGrid.scss +177 -0
  100. package/src/components/HkSelectionGrid.tsx +99 -0
  101. package/src/components/HkSelectionWaterfall.scss +93 -0
  102. package/src/components/HkSelectionWaterfall.tsx +90 -0
  103. package/src/components/HkSidebar.scss +81 -0
  104. package/src/components/HkSidebar.tsx +45 -0
  105. package/src/components/HkSkeleton.scss +44 -0
  106. package/src/components/HkSkeleton.tsx +30 -0
  107. package/src/components/HkSkeletonList.tsx +35 -0
  108. package/src/components/HkSpinner.scss +67 -0
  109. package/src/components/HkSpinner.tsx +41 -0
  110. package/src/components/HkSplash.scss +60 -0
  111. package/src/components/HkSplash.tsx +40 -0
  112. package/src/components/HkSwitch.scss +157 -0
  113. package/src/components/HkSwitch.tsx +72 -0
  114. package/src/components/HkTable.scss +198 -0
  115. package/src/components/HkTable.tsx +246 -0
  116. package/src/components/HkTabs.scss +109 -0
  117. package/src/components/HkTabs.tsx +111 -0
  118. package/src/components/HkTag.scss +88 -0
  119. package/src/components/HkTag.tsx +44 -0
  120. package/src/components/HkTextarea.scss +72 -0
  121. package/src/components/HkTextarea.tsx +104 -0
  122. package/src/components/HkTimeline.scss +139 -0
  123. package/src/components/HkTimeline.tsx +95 -0
  124. package/src/components/HkToast.scss +168 -0
  125. package/src/components/HkToast.tsx +175 -0
  126. package/src/components/HkTooltip.scss +47 -0
  127. package/src/components/HkTooltip.tsx +108 -0
  128. package/src/components/HkTree.scss +188 -0
  129. package/src/components/HkTree.tsx +217 -0
  130. package/src/components/HkTrendChart.tsx +180 -0
  131. package/src/components/HkWindowedItem.scss +17 -0
  132. package/src/components/HkWindowedItem.tsx +107 -0
  133. package/src/components/HkZoomToolbar.scss +35 -0
  134. package/src/components/HkZoomToolbar.tsx +69 -0
  135. package/src/composables/useBreakpoint.ts +41 -0
  136. package/src/composables/useClipboard.ts +37 -0
  137. package/src/composables/useConfirm.ts +35 -0
  138. package/src/composables/useProgressDialog.ts +52 -0
  139. package/src/composables/useReportedTransition.ts +69 -0
  140. package/src/composables/useScrollWindow.ts +21 -0
  141. package/src/composables/useToast.ts +2 -0
  142. package/src/demo.scss +24 -0
  143. package/src/demo.ts +22 -0
  144. package/src/env.d.ts +6 -0
  145. package/src/i18n/context.ts +89 -0
  146. package/src/i18n/locales/ar/components.json +43 -0
  147. package/src/i18n/locales/ar/components.toml +40 -0
  148. package/src/i18n/locales/de/components.json +43 -0
  149. package/src/i18n/locales/de/components.toml +40 -0
  150. package/src/i18n/locales/en/components.json +43 -0
  151. package/src/i18n/locales/en/components.toml +40 -0
  152. package/src/i18n/locales/es/components.json +43 -0
  153. package/src/i18n/locales/es/components.toml +40 -0
  154. package/src/i18n/locales/fr/components.json +43 -0
  155. package/src/i18n/locales/fr/components.toml +40 -0
  156. package/src/i18n/locales/ja/components.json +43 -0
  157. package/src/i18n/locales/ja/components.toml +40 -0
  158. package/src/i18n/locales/ko/components.json +43 -0
  159. package/src/i18n/locales/ko/components.toml +40 -0
  160. package/src/i18n/locales/pt/components.json +43 -0
  161. package/src/i18n/locales/pt/components.toml +40 -0
  162. package/src/i18n/locales/ru/components.json +43 -0
  163. package/src/i18n/locales/ru/components.toml +40 -0
  164. package/src/i18n/locales/zh-Hans/components.json +43 -0
  165. package/src/i18n/locales/zh-Hans/components.toml +40 -0
  166. package/src/i18n/locales/zh-Hant/components.json +43 -0
  167. package/src/i18n/locales/zh-Hant/components.toml +40 -0
  168. package/src/index.ts +137 -0
  169. package/src/render.ts +18 -0
  170. package/src/runtime/animationBus.ts +278 -0
  171. package/src/runtime/cronBus.ts +25 -0
  172. package/src/runtime/index.ts +12 -0
  173. package/src/runtime/useAsyncData.ts +30 -0
  174. package/src/runtime/useBreakpoint.ts +62 -0
  175. package/src/runtime/useClipboard.ts +37 -0
  176. package/src/runtime/useConfirm.ts +35 -0
  177. package/src/runtime/useOverlay.ts +82 -0
  178. package/src/runtime/usePopupManager.ts +89 -0
  179. package/src/runtime/useToast.ts +140 -0
  180. package/src/styles/index.scss +10 -0
  181. package/src/theme/index.ts +5 -0
  182. package/src/theme/presets.ts +307 -0
  183. package/src/theme/theme.scss +17 -0
  184. package/src/theme/useBackgroundLuminance.ts +145 -0
  185. package/src/theme/useSolarTime.ts +101 -0
  186. package/src/theme/useTheme.ts +141 -0
  187. package/src/tokens.scss +26 -0
@@ -0,0 +1,308 @@
1
+ import { computed, defineComponent, ref, watch, type PropType } from "vue";
2
+
3
+ import "./HkKeywordSearchModal.scss";
4
+ import HModal from "./HkModal";
5
+ import { scheduleCronAfter, type CronHandle } from "../runtime/cronBus";
6
+
7
+ interface FuzzyMatch {
8
+ matched: boolean;
9
+ score: number;
10
+ }
11
+
12
+ function fuzzyScore(query: string, haystack: string): FuzzyMatch {
13
+ if (!query) return { matched: true, score: 0 };
14
+ const q = query.toLowerCase();
15
+ const h = haystack.toLowerCase();
16
+ if (!h) return { matched: false, score: 0 };
17
+
18
+ const subIdx = h.indexOf(q);
19
+ if (subIdx >= 0) {
20
+ const boundary = subIdx === 0 || /[\s_\-/.:]/.test(h[subIdx - 1] ?? " ");
21
+ return { matched: true, score: 1000 + q.length * 10 + (boundary ? 50 : 0) };
22
+ }
23
+
24
+ let qi = 0;
25
+ let score = 0;
26
+ let run = 0;
27
+ let prevMatchedIdx = -2;
28
+ for (let hi = 0; hi < h.length && qi < q.length; hi++) {
29
+ if (h[hi] === q[qi]) {
30
+ run = hi === prevMatchedIdx + 1 ? run + 1 : 1;
31
+ score += 10 + run * 4;
32
+ const prev = hi > 0 ? h[hi - 1] : " ";
33
+ if (/[\s_\-/.:,()]/.test(prev)) score += 15;
34
+ prevMatchedIdx = hi;
35
+ qi++;
36
+ }
37
+ }
38
+ return { matched: qi === q.length, score: qi === q.length ? score : 0 };
39
+ }
40
+
41
+ function fuzzyScoreFields(
42
+ query: string,
43
+ fields: { text: string; weight?: number }[],
44
+ ): { matched: boolean; score: number; fieldIndex: number } {
45
+ let best = { matched: false, score: 0, fieldIndex: -1 };
46
+ for (let i = 0; i < fields.length; i++) {
47
+ const { text, weight = 1 } = fields[i];
48
+ const res = fuzzyScore(query, text);
49
+ if (!res.matched) continue;
50
+ const weighted = res.score * weight;
51
+ if (weighted > best.score) {
52
+ best = { matched: true, score: weighted, fieldIndex: i };
53
+ }
54
+ }
55
+ return best;
56
+ }
57
+
58
+ function fuzzySearch<T>(
59
+ query: string,
60
+ records: T[],
61
+ fieldsOf: (rec: T) => { text: string; weight?: number }[],
62
+ ): { record: T; score: number }[] {
63
+ const terms = query.trim().toLowerCase().split(/\s+/).filter(Boolean);
64
+ if (terms.length === 0) {
65
+ return records.map((record) => ({ record, score: 0 }));
66
+ }
67
+ const out: { record: T; score: number }[] = [];
68
+ for (const record of records) {
69
+ const fields = fieldsOf(record);
70
+ let total = 0;
71
+ let allMatched = true;
72
+ for (const term of terms) {
73
+ const res = fuzzyScoreFields(term, fields);
74
+ if (!res.matched) { allMatched = false; break; }
75
+ total += res.score;
76
+ }
77
+ if (allMatched) out.push({ record, score: total });
78
+ }
79
+ out.sort((a, b) => b.score - a.score);
80
+ return out;
81
+ }
82
+
83
+ export interface SemanticSearchItem {
84
+ key: string;
85
+ title?: string;
86
+ snippet: string;
87
+ score?: number;
88
+ source?: string;
89
+ raw: unknown;
90
+ }
91
+
92
+ export default defineComponent({
93
+ name: "HkKeywordSearchModal",
94
+ props: {
95
+ modelValue: { type: Boolean, required: true },
96
+ title: { type: String, default: undefined },
97
+ placeholder: { type: String, default: undefined },
98
+ records: { type: Array as PropType<unknown[]>, default: () => [] },
99
+ fieldsOf: {
100
+ type: Function as PropType<(rec: unknown) => { text: string; weight?: number }[]>,
101
+ default: () => [],
102
+ },
103
+ keyOf: { type: Function as PropType<(rec: unknown) => string>, default: () => "0" },
104
+ emptyText: { type: String, default: undefined },
105
+ semanticSearch: {
106
+ type: [Function, null] as unknown as PropType<
107
+ ((query: string) => Promise<SemanticSearchItem[]>) | undefined
108
+ >,
109
+ default: undefined,
110
+ },
111
+ searchingText: { type: String, default: undefined },
112
+ },
113
+ emits: {
114
+ "update:modelValue": (_v: boolean) => true,
115
+ select: (_rec: unknown) => true,
116
+ },
117
+ setup(props, { slots, emit }) {
118
+ const query = ref("");
119
+ let debounceTimer: CronHandle | null = null;
120
+ const debouncedQuery = ref("");
121
+
122
+ function scheduleDebounce() {
123
+ debounceTimer?.disconnect();
124
+ debounceTimer = scheduleCronAfter(() => {
125
+ debouncedQuery.value = query.value;
126
+ }, 120);
127
+ }
128
+
129
+ watch(query, scheduleDebounce);
130
+
131
+ const semanticActive = computed(() => typeof props.semanticSearch === "function");
132
+ const semanticLoading = ref(false);
133
+ const semanticResults = ref<SemanticSearchItem[]>([]);
134
+ let semanticToken = 0;
135
+
136
+ async function runSemantic(q: string) {
137
+ if (!props.semanticSearch) {
138
+ semanticResults.value = [];
139
+ return;
140
+ }
141
+ const trimmed = q.trim();
142
+ if (!trimmed) {
143
+ semanticResults.value = [];
144
+ semanticLoading.value = false;
145
+ return;
146
+ }
147
+ const token = ++semanticToken;
148
+ semanticLoading.value = true;
149
+ try {
150
+ const items = await props.semanticSearch(trimmed);
151
+ if (token === semanticToken) semanticResults.value = items ?? [];
152
+ } catch {
153
+ if (token === semanticToken) semanticResults.value = [];
154
+ } finally {
155
+ if (token === semanticToken) semanticLoading.value = false;
156
+ }
157
+ }
158
+
159
+ watch(debouncedQuery, (q) => {
160
+ if (semanticActive.value) void runSemantic(q);
161
+ });
162
+
163
+ const results = computed(() => {
164
+ const q = debouncedQuery.value;
165
+ return fuzzySearch(q, props.records as unknown[], props.fieldsOf).map((r) => ({
166
+ key: props.keyOf(r.record),
167
+ record: r.record,
168
+ score: r.score,
169
+ }));
170
+ });
171
+
172
+ watch(
173
+ () => props.modelValue,
174
+ (open) => {
175
+ if (open) {
176
+ query.value = "";
177
+ debouncedQuery.value = "";
178
+ semanticResults.value = [];
179
+ semanticLoading.value = false;
180
+ }
181
+ },
182
+ );
183
+
184
+ function pick(rec: unknown) {
185
+ emit("select", rec);
186
+ emit("update:modelValue", false);
187
+ }
188
+
189
+ return () => (
190
+ <HModal
191
+ modelValue={props.modelValue}
192
+ onUpdate:modelValue={(v: boolean) => emit("update:modelValue", v)}
193
+ title={props.title ?? "Search"}
194
+ width="40rem"
195
+ >
196
+ <div class="hk-kw-search">
197
+ <div class="hk-kw-search-box">
198
+ <svg
199
+ class="hk-kw-search-icon"
200
+ width="15"
201
+ height="15"
202
+ viewBox="0 0 24 24"
203
+ fill="none"
204
+ stroke="currentColor"
205
+ stroke-width="2"
206
+ stroke-linecap="round"
207
+ >
208
+ <circle cx="11" cy="11" r="8" />
209
+ <line x1="21" y1="21" x2="16.65" y2="16.65" />
210
+ </svg>
211
+ <input
212
+ class="hk-kw-search-input"
213
+ type="text"
214
+ value={query.value}
215
+ placeholder={props.placeholder ?? "Search…"}
216
+ onInput={(e: Event) => { query.value = (e.target as HTMLInputElement).value; }}
217
+ autofocus
218
+ />
219
+ {query.value && (
220
+ <button
221
+ class="hk-kw-search-clear"
222
+ type="button"
223
+ aria-label="Clear"
224
+ onClick={() => {
225
+ query.value = "";
226
+ debouncedQuery.value = "";
227
+ semanticResults.value = [];
228
+ }}
229
+ >
230
+ <svg
231
+ width="14"
232
+ height="14"
233
+ viewBox="0 0 24 24"
234
+ fill="none"
235
+ stroke="currentColor"
236
+ stroke-width="2"
237
+ stroke-linecap="round"
238
+ >
239
+ <line x1="18" y1="6" x2="6" y2="18" />
240
+ <line x1="6" y1="6" x2="18" y2="18" />
241
+ </svg>
242
+ </button>
243
+ )}
244
+ </div>
245
+
246
+ <div class="hk-kw-search-results">
247
+ {semanticActive.value ? (
248
+ semanticLoading.value && semanticResults.value.length === 0 ? (
249
+ <div class="hk-kw-search-empty">
250
+ {props.searchingText ?? "Searching…"}
251
+ </div>
252
+ ) : semanticResults.value.length === 0 ? (
253
+ <div class="hk-kw-search-empty">
254
+ {debouncedQuery.value
255
+ ? (props.emptyText ?? "No matches")
256
+ : (props.placeholder ?? "Search…")}
257
+ </div>
258
+ ) : (
259
+ semanticResults.value.map((r) => (
260
+ <button
261
+ key={r.key}
262
+ class="hk-kw-search-result hk-kw-search-result-semantic"
263
+ type="button"
264
+ onClick={() => pick(r.raw)}
265
+ >
266
+ {slots.semanticResult?.({ item: r }) ?? (
267
+ <span class="hk-kw-search-semantic">
268
+ <span class="hk-kw-search-semantic-head">
269
+ {r.title && (
270
+ <span class="hk-kw-search-semantic-title">{r.title}</span>
271
+ )}
272
+ {typeof r.score === "number" && (
273
+ <span class="hk-kw-search-semantic-score">
274
+ {(r.score * 100).toFixed(0)}%
275
+ </span>
276
+ )}
277
+ </span>
278
+ <span class="hk-kw-search-semantic-snippet">{r.snippet}</span>
279
+ {r.source && (
280
+ <span class="hk-kw-search-semantic-source">{r.source}</span>
281
+ )}
282
+ </span>
283
+ )}
284
+ </button>
285
+ ))
286
+ )
287
+ ) : results.value.length === 0 ? (
288
+ <div class="hk-kw-search-empty">
289
+ {props.emptyText ?? "No matches"}
290
+ </div>
291
+ ) : (
292
+ results.value.map((r) => (
293
+ <button
294
+ key={r.key}
295
+ class="hk-kw-search-result"
296
+ type="button"
297
+ onClick={() => pick(r.record)}
298
+ >
299
+ {slots.result?.({ record: r.record })}
300
+ </button>
301
+ ))
302
+ )}
303
+ </div>
304
+ </div>
305
+ </HModal>
306
+ );
307
+ },
308
+ });
@@ -0,0 +1,131 @@
1
+ .hk-list-move {
2
+ transition: transform var(--hi-duration-normal, 0.3s) ease;
3
+ }
4
+
5
+ .hk-list-move-disabled {
6
+ transition: none !important;
7
+ }
8
+
9
+ .hk-list-pop-enter-active {
10
+ transition:
11
+ opacity var(--hi-duration-normal, 0.3s) cubic-bezier(0.16, 1, 0.3, 1),
12
+ transform var(--hi-duration-normal, 0.3s) cubic-bezier(0.16, 1, 0.3, 1);
13
+ }
14
+
15
+ .hk-list-pop-leave-active {
16
+ transition:
17
+ opacity var(--hi-duration-normal, 0.3s) ease-in,
18
+ transform var(--hi-duration-normal, 0.3s) cubic-bezier(0.5, 0, 0.75, 0);
19
+ position: absolute;
20
+ }
21
+
22
+ .hk-list-pop-enter-from {
23
+ opacity: 0;
24
+ transform: scale(0.85) translateY(4px);
25
+ }
26
+
27
+ .hk-list-pop-leave-to {
28
+ opacity: 0;
29
+ transform: scale(0.85);
30
+ }
31
+
32
+ .hk-list-slide-enter-active {
33
+ transition:
34
+ opacity var(--hi-duration-normal, 0.3s) ease,
35
+ transform var(--hi-duration-normal, 0.3s) cubic-bezier(0.16, 1, 0.3, 1);
36
+ }
37
+
38
+ .hk-list-slide-leave-active {
39
+ transition:
40
+ opacity var(--hi-duration-normal, 0.3s) ease-in,
41
+ transform var(--hi-duration-normal, 0.3s) ease-in;
42
+ position: absolute;
43
+ }
44
+
45
+ .hk-list-slide-enter-from {
46
+ opacity: 0;
47
+ transform: translateX(-12px);
48
+ }
49
+
50
+ .hk-list-slide-leave-to {
51
+ opacity: 0;
52
+ transform: translateX(12px);
53
+ }
54
+
55
+ .hk-list-fade-enter-active {
56
+ transition: opacity var(--hi-duration-normal, 0.3s) ease;
57
+ }
58
+
59
+ .hk-list-fade-leave-active {
60
+ transition: opacity var(--hi-duration-normal, 0.3s) ease-in;
61
+ position: absolute;
62
+ }
63
+
64
+ .hk-list-fade-enter-from,
65
+ .hk-list-fade-leave-to {
66
+ opacity: 0;
67
+ }
68
+
69
+ .hk-list-grow-enter-active {
70
+ transition:
71
+ transform var(--hi-duration-fast, 0.15s) cubic-bezier(0.16, 1, 0.3, 1),
72
+ opacity var(--hi-duration-fast, 0.15s) cubic-bezier(0.16, 1, 0.3, 1);
73
+ transition-delay: var(--hi-duration-fast, 0.15s);
74
+ pointer-events: none;
75
+ }
76
+
77
+ .hk-list-grow-enter-from {
78
+ transform: scale(0.95);
79
+ opacity: 0;
80
+ }
81
+
82
+ .hk-list-grow-leave-active {
83
+ transition:
84
+ transform var(--hi-duration-fast, 0.15s) cubic-bezier(0.5, 0, 0.75, 0),
85
+ opacity var(--hi-duration-fast, 0.15s) ease-in;
86
+ pointer-events: none;
87
+ position: absolute;
88
+ width: 100%;
89
+ }
90
+
91
+ .hk-list-grow-leave-to {
92
+ transform: scale(0.95);
93
+ opacity: 0;
94
+ }
95
+
96
+ .hk-list-grow-move {
97
+ transition: transform var(--hi-duration-fast, 0.15s) ease;
98
+ transition-delay: var(--hi-duration-fast, 0.15s);
99
+ }
100
+
101
+ .hk-list-reveal-enter-active {
102
+ interpolate-size: allow-keywords;
103
+ overflow: hidden;
104
+ transition:
105
+ height var(--hi-duration-fast, 0.15s) cubic-bezier(0.16, 1, 0.3, 1),
106
+ opacity var(--hi-duration-fast, 0.15s) cubic-bezier(0.16, 1, 0.3, 1);
107
+ }
108
+
109
+ .hk-list-reveal-leave-active {
110
+ interpolate-size: allow-keywords;
111
+ overflow: hidden;
112
+ transition:
113
+ height var(--hi-duration-fast, 0.15s) cubic-bezier(0.5, 0, 0.75, 0),
114
+ opacity var(--hi-duration-fast, 0.15s) ease-in;
115
+ }
116
+
117
+ .hk-list-reveal-enter-from,
118
+ .hk-list-reveal-leave-to {
119
+ opacity: 0;
120
+ height: calc-size(auto, size * 0.8);
121
+ }
122
+
123
+ .hk-list-reveal-move {
124
+ transition: transform var(--hi-duration-fast, 0.15s) ease;
125
+ }
126
+
127
+ .hk-list-none-enter-active,
128
+ .hk-list-none-leave-active,
129
+ .hk-list-none-move {
130
+ transition: none;
131
+ }
@@ -0,0 +1,29 @@
1
+ import { defineComponent, TransitionGroup, type PropType } from "vue";
2
+
3
+ import "./HkListTransition.scss";
4
+
5
+ type ListAnimVariant = "pop" | "slide" | "fade" | "grow" | "reveal";
6
+
7
+ export default defineComponent({
8
+ name: "HkListTransition",
9
+ props: {
10
+ tag: { type: String, default: "div" },
11
+ variant: { type: String as PropType<ListAnimVariant>, default: "pop" },
12
+ move: { type: Boolean, default: false },
13
+ disabled: { type: Boolean, default: false },
14
+ },
15
+ setup(props, { slots }) {
16
+ return () => {
17
+ const name = props.disabled ? "hk-list-none" : `hk-list-${props.variant}`;
18
+ return (
19
+ <TransitionGroup
20
+ tag={props.tag}
21
+ name={name}
22
+ moveClass={props.move ? undefined : "hk-list-move-disabled"}
23
+ >
24
+ {slots.default?.()}
25
+ </TransitionGroup>
26
+ );
27
+ };
28
+ },
29
+ });
@@ -0,0 +1,42 @@
1
+ import { defineComponent, type PropType } from "vue";
2
+ import type { Ref } from "vue";
3
+
4
+ const HkLocalePickerPopup = defineComponent({
5
+ name: "HkLocalePickerPopup",
6
+ props: {
7
+ open: { type: Boolean, required: true },
8
+ triggerRef: { type: [Object, null] as PropType<HTMLElement | Ref<HTMLElement | null> | null>, default: null },
9
+ placement: { type: String, default: "right-start" },
10
+ locales: { type: Array as PropType<Array<{ code: string; label: string; flag?: string }>>, required: true },
11
+ currentLocale: { type: String, default: "" },
12
+ t: { type: Function as PropType<(key: string) => string>, default: (k: string) => k },
13
+ },
14
+ emits: ["update:open", "select"],
15
+ setup(props, { emit }) {
16
+ return () => {
17
+ if (!props.open) return null;
18
+ return (
19
+ <div class="hk-locale-picker-popup" role="listbox">
20
+ {props.locales.map((loc) => (
21
+ <button
22
+ key={loc.code}
23
+ class="hk-locale-picker-item"
24
+ data-selected={loc.code === props.currentLocale || undefined}
25
+ role="option"
26
+ aria-selected={loc.code === props.currentLocale}
27
+ onClick={() => {
28
+ emit("select", loc.code);
29
+ emit("update:open", false);
30
+ }}
31
+ >
32
+ {loc.flag && <span class="hk-locale-picker-flag">{loc.flag}</span>}
33
+ <span class="hk-locale-picker-label">{loc.label}</span>
34
+ </button>
35
+ ))}
36
+ </div>
37
+ );
38
+ };
39
+ },
40
+ });
41
+
42
+ export default HkLocalePickerPopup;
@@ -0,0 +1,36 @@
1
+ .hk-logo {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ justify-content: center;
5
+ cursor: pointer;
6
+ border-radius: var(--hi-radius-md, 8px);
7
+ overflow: hidden;
8
+ flex-shrink: 0;
9
+ user-select: none;
10
+ }
11
+
12
+ a.hk-logo {
13
+ text-decoration: none;
14
+ }
15
+
16
+ .hk-logo-img {
17
+ display: block;
18
+ border-radius: var(--hi-radius-md, 8px);
19
+ object-fit: contain;
20
+ }
21
+
22
+ .hk-logo-placeholder {
23
+ display: flex;
24
+ align-items: center;
25
+ justify-content: center;
26
+ background: var(--hi-color-primary);
27
+ color: #fff;
28
+ border-radius: var(--hi-radius-md, 8px);
29
+ }
30
+
31
+ .hk-logo-initial {
32
+ font-weight: 700;
33
+ font-size: 50%;
34
+ line-height: 1;
35
+ user-select: none;
36
+ }
@@ -0,0 +1,93 @@
1
+ import { defineComponent, type PropType } from "vue";
2
+ import { useI18n } from "../i18n/context";
3
+ import "./HkLogo.scss";
4
+
5
+ const sizeMap: Record<string, string> = {
6
+ xs: "1.25rem",
7
+ sm: "1.625rem",
8
+ md: "2rem",
9
+ lg: "3.5rem",
10
+ xl: "5rem",
11
+ };
12
+
13
+ const placeholderSvg = (
14
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" width="60%" height="60%">
15
+ <path d="M12 2L2 7l10 5 10-5-10-5z" />
16
+ <path d="M2 17l10 5 10-5" />
17
+ <path d="M2 12l10 5 10-5" />
18
+ </svg>
19
+ );
20
+
21
+ export default defineComponent({
22
+ name: "HkLogo",
23
+ props: {
24
+ src: { type: String, default: undefined },
25
+ alt: { type: String, default: "" },
26
+ size: {
27
+ type: String as PropType<"xs" | "sm" | "md" | "lg" | "xl">,
28
+ default: "md",
29
+ },
30
+ href: { type: String, default: undefined },
31
+ },
32
+ emits: {
33
+ click: (_e: MouseEvent) => true,
34
+ },
35
+ setup(props, { emit }) {
36
+ const { t } = useI18n();
37
+ const logoSize = () => sizeMap[props.size] ?? sizeMap.md;
38
+
39
+ const renderLogo = () => {
40
+ if (props.src) {
41
+ return (
42
+ <img
43
+ class="hk-logo-img"
44
+ src={props.src}
45
+ alt={props.alt}
46
+ style={{ width: logoSize(), height: logoSize() }}
47
+ />
48
+ );
49
+ }
50
+
51
+ return (
52
+ <div
53
+ class="hk-logo-placeholder"
54
+ style={{
55
+ width: logoSize(),
56
+ height: logoSize(),
57
+ }}
58
+ role="img"
59
+ aria-label={props.alt || t("hk.logo.fallbackAlt", "Logo")}
60
+ >
61
+ {props.alt ? (
62
+ <span class="hk-logo-initial">{props.alt.charAt(0).toUpperCase()}</span>
63
+ ) : (
64
+ placeholderSvg
65
+ )}
66
+ </div>
67
+ );
68
+ };
69
+
70
+ if (props.href) {
71
+ return () => (
72
+ <a
73
+ class="hk-logo"
74
+ href={props.href}
75
+ onClick={(e: MouseEvent) => emit("click", e)}
76
+ style={{ lineHeight: 0 }}
77
+ >
78
+ {renderLogo()}
79
+ </a>
80
+ );
81
+ }
82
+
83
+ return () => (
84
+ <div
85
+ class="hk-logo"
86
+ onClick={(e: MouseEvent) => emit("click", e)}
87
+ style={{ lineHeight: 0 }}
88
+ >
89
+ {renderLogo()}
90
+ </div>
91
+ );
92
+ },
93
+ });