@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,132 @@
1
+ .hk-search-input {
2
+ display: flex;
3
+ align-items: center;
4
+ background: var(--hi-color-surface);
5
+ border: 1px solid var(--hi-color-border);
6
+ border-radius: var(--hi-radius-md, 6px);
7
+ overflow: hidden;
8
+ transition:
9
+ background-color var(--hi-transition-base, 200ms cubic-bezier(0.4, 0, 0.2, 1)),
10
+ border-color var(--hi-transition-base, 200ms cubic-bezier(0.4, 0, 0.2, 1)),
11
+ box-shadow var(--hi-transition-base, 200ms cubic-bezier(0.4, 0, 0.2, 1));
12
+
13
+ &:hover {
14
+ border-color: var(--hi-color-primary);
15
+ }
16
+
17
+ &:focus-within {
18
+ border-color: var(--hi-color-primary);
19
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--hi-color-primary) 12%, transparent);
20
+ }
21
+
22
+ &-disabled {
23
+ opacity: 0.5;
24
+ pointer-events: none;
25
+ }
26
+ }
27
+
28
+ .hk-search-input-icon {
29
+ display: flex;
30
+ align-items: center;
31
+ justify-content: center;
32
+ flex-shrink: 0;
33
+ padding-left: 0.75rem;
34
+ color: var(--hi-color-muted);
35
+
36
+ svg {
37
+ width: 16px;
38
+ height: 16px;
39
+ }
40
+ }
41
+
42
+ .hk-search-input-field {
43
+ flex: 1;
44
+ min-width: 0;
45
+ background: transparent;
46
+ border: none;
47
+ outline: none;
48
+ color: var(--hi-color-text-primary);
49
+ font-family: inherit;
50
+ font-size: 0.875rem;
51
+ line-height: 1.5;
52
+ padding: 0 0.5rem;
53
+
54
+ &::placeholder {
55
+ color: var(--hi-color-muted);
56
+ opacity: 0.6;
57
+ }
58
+
59
+ &::-webkit-search-cancel-button,
60
+ &::-webkit-search-decoration {
61
+ -webkit-appearance: none;
62
+ appearance: none;
63
+ }
64
+ }
65
+
66
+ .hk-search-input-clear {
67
+ display: flex;
68
+ align-items: center;
69
+ justify-content: center;
70
+ flex-shrink: 0;
71
+ width: 1.75rem;
72
+ height: 100%;
73
+ padding: 0;
74
+ background: transparent;
75
+ border: none;
76
+ border-radius: 0;
77
+ color: var(--hi-color-muted);
78
+ cursor: pointer;
79
+ transition:
80
+ background-color var(--hi-duration-fast, 0.15s) ease,
81
+ color var(--hi-duration-fast, 0.15s) ease;
82
+
83
+ svg {
84
+ width: 14px;
85
+ height: 14px;
86
+ }
87
+
88
+ &:hover {
89
+ background: color-mix(in srgb, var(--hi-color-surface) 60%, transparent);
90
+ color: var(--hi-color-text-primary);
91
+ }
92
+ }
93
+
94
+ /* --- Size variants --- */
95
+
96
+ .hk-search-input-sm .hk-search-input-field {
97
+ font-size: 0.75rem;
98
+ height: 1.75rem;
99
+ }
100
+
101
+ .hk-search-input-sm .hk-search-input-icon {
102
+ padding-left: 0.5rem;
103
+
104
+ svg {
105
+ width: 14px;
106
+ height: 14px;
107
+ }
108
+ }
109
+
110
+ .hk-search-input-md .hk-search-input-field {
111
+ font-size: 0.875rem;
112
+ height: 2.25rem;
113
+ }
114
+
115
+ .hk-search-input-lg .hk-search-input-field {
116
+ font-size: 1rem;
117
+ height: 2.75rem;
118
+ }
119
+
120
+ .hk-search-input-lg .hk-search-input-icon {
121
+ padding-left: 1rem;
122
+
123
+ svg {
124
+ width: 18px;
125
+ height: 18px;
126
+ }
127
+ }
128
+
129
+ .hk-search-input-lg .hk-search-input-clear svg {
130
+ width: 16px;
131
+ height: 16px;
132
+ }
@@ -0,0 +1,117 @@
1
+ import { defineComponent, onUnmounted, ref, watch, type PropType } from "vue";
2
+ import { useI18n } from "../i18n/context";
3
+ import "./HkSearchInput.scss";
4
+
5
+ const searchIcon = (
6
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
7
+ <circle cx="11" cy="11" r="8" />
8
+ <line x1="21" y1="21" x2="16.65" y2="16.65" />
9
+ </svg>
10
+ );
11
+
12
+ const clearIcon = (
13
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
14
+ <line x1="18" y1="6" x2="6" y2="18" />
15
+ <line x1="6" y1="6" x2="18" y2="18" />
16
+ </svg>
17
+ );
18
+
19
+ export default defineComponent({
20
+ name: "HkSearchInput",
21
+ props: {
22
+ modelValue: { type: String, default: "" },
23
+ placeholder: { type: String, default: "Search..." },
24
+ disabled: { type: Boolean, default: false },
25
+ size: {
26
+ type: String as PropType<"sm" | "md" | "lg">,
27
+ default: "md",
28
+ },
29
+ debounce: { type: Number, default: 0 },
30
+ clearAriaLabel: { type: String, default: "" },
31
+ },
32
+ emits: {
33
+ "update:modelValue": (_value: string) => true,
34
+ search: (_value: string) => true,
35
+ clear: () => true,
36
+ },
37
+ setup(props, { emit }) {
38
+ const { t } = useI18n();
39
+ const inputRef = ref<HTMLInputElement | null>(null);
40
+ let debounceTimer: ReturnType<typeof setTimeout> | null = null;
41
+
42
+ function flushSearch(val: string) {
43
+ emit("search", val);
44
+ }
45
+
46
+ function debouncedSearch(val: string) {
47
+ if (debounceTimer) clearTimeout(debounceTimer);
48
+ if (props.debounce > 0) {
49
+ debounceTimer = setTimeout(() => flushSearch(val), props.debounce);
50
+ } else {
51
+ flushSearch(val);
52
+ }
53
+ }
54
+
55
+ watch(
56
+ () => props.modelValue,
57
+ (val) => {
58
+ debouncedSearch(val);
59
+ },
60
+ );
61
+
62
+ onUnmounted(() => {
63
+ if (debounceTimer) clearTimeout(debounceTimer);
64
+ });
65
+
66
+ function onInput(e: Event) {
67
+ const val = (e.target as HTMLInputElement).value;
68
+ emit("update:modelValue", val);
69
+ }
70
+
71
+ function onKeydown(e: KeyboardEvent) {
72
+ if (e.key === "Enter") {
73
+ if (debounceTimer) clearTimeout(debounceTimer);
74
+ emit("search", (e.target as HTMLInputElement).value);
75
+ }
76
+ }
77
+
78
+ function clearValue() {
79
+ emit("update:modelValue", "");
80
+ emit("clear");
81
+ inputRef.value?.focus();
82
+ }
83
+
84
+ const wrapperClass = [
85
+ "hk-search-input",
86
+ `hk-search-input-${props.size}`,
87
+ props.disabled ? "hk-search-input-disabled" : "",
88
+ ];
89
+
90
+ return () => (
91
+ <div class={wrapperClass}>
92
+ <span class="hk-search-input-icon">{searchIcon}</span>
93
+ <input
94
+ ref={inputRef}
95
+ type="search"
96
+ class="hk-search-input-field"
97
+ value={props.modelValue}
98
+ placeholder={props.placeholder ?? "Search..."}
99
+ disabled={props.disabled}
100
+ onInput={onInput}
101
+ onKeydown={onKeydown}
102
+ />
103
+ {props.modelValue && !props.disabled ? (
104
+ <button
105
+ type="button"
106
+ class="hk-search-input-clear"
107
+ onClick={clearValue}
108
+ tabindex={-1}
109
+ aria-label={props.clearAriaLabel || t("hk.searchInput.clear", "Clear search")}
110
+ >
111
+ {clearIcon}
112
+ </button>
113
+ ) : null}
114
+ </div>
115
+ );
116
+ },
117
+ });
@@ -0,0 +1,143 @@
1
+ @use "../tokens";
2
+
3
+ /* HkSelect — popup select component */
4
+
5
+ .hk-select-wrapper {
6
+ display: block;
7
+ width: 100%;
8
+ }
9
+
10
+ .hk-select-label {
11
+ display: block;
12
+ font-size: var(--text-base);
13
+ font-weight: 500;
14
+ margin-bottom: var(--space-6);
15
+ color: rgb(var(--color-text));
16
+ }
17
+
18
+ .hk-select-required {
19
+ margin-left: var(--space-2);
20
+ color: rgb(var(--color-error));
21
+ }
22
+
23
+ .hk-select-trigger {
24
+ display: flex;
25
+ align-items: center;
26
+ width: 100%;
27
+ padding: var(--space-8) var(--space-32) var(--space-8) var(--space-12);
28
+ min-height: 2.5rem;
29
+ background: rgb(var(--color-surface) / var(--opacity-half));
30
+ backdrop-filter: blur(var(--blur-sm));
31
+ border: 1px solid var(--border-input);
32
+ border-radius: var(--radius-md);
33
+ font-family: inherit;
34
+ font-size: var(--text-base);
35
+ line-height: 1.5;
36
+ color: rgb(var(--color-text));
37
+ text-align: center;
38
+ cursor: pointer;
39
+ outline: none;
40
+ transition: background-color, border-color, color, box-shadow, opacity, transform, filter var(--duration-normal) var(--ease-standard);
41
+ position: relative;
42
+ }
43
+
44
+ .hk-select-trigger:hover:not([data-disabled]) {
45
+ border-color: var(--c-primary-strong);
46
+ }
47
+
48
+ .hk-select-trigger:focus,
49
+ .hk-select-trigger[data-state="open"] {
50
+ border-color: rgb(var(--color-focused-border));
51
+ box-shadow: var(--shadow-focus);
52
+ background: rgb(var(--color-surface));
53
+ }
54
+
55
+ .hk-select-trigger[data-error] {
56
+ border-color: var(--c-error-strong);
57
+ }
58
+
59
+ .hk-select-trigger[data-error]:focus,
60
+ .hk-select-trigger[data-error][data-state="open"] {
61
+ border-color: rgb(var(--color-error));
62
+ box-shadow: var(--shadow-focus-error);
63
+ }
64
+
65
+ .hk-select-trigger[data-disabled] {
66
+ opacity: 0.5;
67
+ cursor: not-allowed;
68
+ }
69
+
70
+ .hk-select-value {
71
+ flex: 1;
72
+ white-space: nowrap;
73
+ overflow: hidden;
74
+ text-overflow: ellipsis;
75
+ }
76
+
77
+ .hk-select-arrow {
78
+ position: absolute;
79
+ right: 0.75rem;
80
+ color: color-mix(in srgb, rgb(var(--color-muted)) 70%, rgb(var(--color-background)));
81
+ pointer-events: none;
82
+ flex-shrink: 0;
83
+ transition: transform var(--duration-normal) ease;
84
+ }
85
+
86
+ .hk-select-trigger[data-state="open"] .hk-select-arrow {
87
+ transform: rotate(180deg);
88
+ }
89
+
90
+ .hk-select-popout {
91
+ position: fixed;
92
+ z-index: 1100;
93
+ min-width: 180px;
94
+ max-height: 240px;
95
+ display: flex;
96
+ flex-direction: column;
97
+ padding: var(--space-4);
98
+ border-radius: var(--radius-md);
99
+ overflow-y: auto;
100
+ background: rgb(var(--color-surface) / var(--opacity-half));
101
+ backdrop-filter: blur(var(--blur-md));
102
+ border: 1px solid var(--border-subtle);
103
+ box-shadow: var(--shadow-dropdown);
104
+ }
105
+
106
+ .hk-select-option {
107
+ display: flex;
108
+ align-items: center;
109
+ gap: var(--space-8);
110
+ padding: var(--space-10) var(--space-16);
111
+ font-size: var(--text-base);
112
+ color: rgb(var(--color-text));
113
+ border: 1px solid transparent;
114
+ border-radius: var(--radius-md);
115
+ cursor: pointer;
116
+ outline: none;
117
+ transition: background-color, border-color, color, box-shadow, opacity, transform, filter var(--duration-normal) ease;
118
+ text-align: left;
119
+ }
120
+
121
+ .hk-select-option:hover,
122
+ .hk-select-option[data-highlighted] {
123
+ background: var(--c-primary-subtle);
124
+ border-color: var(--c-primary-medium);
125
+ }
126
+
127
+ .hk-select-option[data-checked] {
128
+ background: var(--c-primary-dim);
129
+ border-color: var(--c-primary-strong);
130
+ color: rgb(var(--color-primary));
131
+ font-weight: 600;
132
+ }
133
+
134
+ .hk-select-option[data-disabled] {
135
+ opacity: 0.4;
136
+ cursor: not-allowed;
137
+ }
138
+
139
+ .hk-select-error {
140
+ margin-top: var(--space-4);
141
+ font-size: var(--text-xs);
142
+ color: rgb(var(--color-error));
143
+ }
@@ -0,0 +1,230 @@
1
+ import {
2
+ computed,
3
+ defineComponent,
4
+ nextTick,
5
+ onBeforeUnmount,
6
+ ref,
7
+ watch,
8
+ type PropType,
9
+ Teleport,
10
+ } from "vue";
11
+ import { ChevronDown, Search } from "lucide-vue-next";
12
+ import "./HkSelect.scss";
13
+
14
+ export interface HkSelectOption {
15
+ value: string;
16
+ label: string;
17
+ disabled?: boolean;
18
+ }
19
+
20
+ export default defineComponent({
21
+ name: "HkSelect",
22
+ props: {
23
+ modelValue: { type: String, default: "" },
24
+ label: { type: String, default: undefined },
25
+ placeholder: { type: String, default: "" },
26
+ error: { type: String, default: undefined },
27
+ disabled: { type: Boolean, default: false },
28
+ required: { type: Boolean, default: false },
29
+ options: {
30
+ type: Array as PropType<HkSelectOption[]>,
31
+ default: () => [] as HkSelectOption[],
32
+ },
33
+ },
34
+ emits: {
35
+ "update:modelValue": (_value: string) => true,
36
+ },
37
+ setup(props, { emit, slots }) {
38
+ const isOpen = ref(false);
39
+ const triggerRef = ref<HTMLElement>();
40
+ const panelRef = ref<HTMLElement>();
41
+ const highlightedIndex = ref(-1);
42
+
43
+ function onDocumentClick(e: MouseEvent) {
44
+ if (!isOpen.value) return;
45
+ const target = e.target as Node;
46
+ if (triggerRef.value?.contains(target)) return;
47
+ if (panelRef.value?.contains(target)) return;
48
+ isOpen.value = false;
49
+ }
50
+
51
+ watch(isOpen, (open) => {
52
+ if (open) {
53
+ document.addEventListener("click", onDocumentClick, true);
54
+ } else {
55
+ document.removeEventListener("click", onDocumentClick, true);
56
+ }
57
+ });
58
+
59
+ onBeforeUnmount(() => {
60
+ document.removeEventListener("click", onDocumentClick, true);
61
+ });
62
+
63
+ const normalizedOptions = computed<HkSelectOption[]>(
64
+ () => props.options ?? [],
65
+ );
66
+
67
+ const displayLabel = computed(() => {
68
+ if (!props.modelValue) return props.placeholder || "";
69
+ const opt = normalizedOptions.value.find(
70
+ (o) => o.value === props.modelValue,
71
+ );
72
+ return opt?.label ?? props.modelValue;
73
+ });
74
+
75
+ function toggle() {
76
+ if (props.disabled) return;
77
+ isOpen.value = !isOpen.value;
78
+ }
79
+
80
+ function select(value: string) {
81
+ emit("update:modelValue", value);
82
+ isOpen.value = false;
83
+ }
84
+
85
+ function onTriggerKeydown(e: KeyboardEvent) {
86
+ if (props.disabled) return;
87
+ if (e.key === "Enter" || e.key === " ") {
88
+ e.preventDefault();
89
+ toggle();
90
+ } else if (e.key === "ArrowDown") {
91
+ e.preventDefault();
92
+ if (!isOpen.value) {
93
+ isOpen.value = true;
94
+ highlightedIndex.value = 0;
95
+ } else {
96
+ highlightedIndex.value =
97
+ (highlightedIndex.value + 1) % normalizedOptions.value.length;
98
+ }
99
+ } else if (e.key === "ArrowUp") {
100
+ e.preventDefault();
101
+ if (!isOpen.value) {
102
+ isOpen.value = true;
103
+ } else {
104
+ highlightedIndex.value =
105
+ (highlightedIndex.value - 1 + normalizedOptions.value.length) %
106
+ normalizedOptions.value.length;
107
+ }
108
+ } else if (e.key === "Escape") {
109
+ isOpen.value = false;
110
+ }
111
+ }
112
+
113
+ function onPopoutKeydown(e: KeyboardEvent) {
114
+ if (e.key === "ArrowDown") {
115
+ e.preventDefault();
116
+ highlightedIndex.value =
117
+ (highlightedIndex.value + 1) % normalizedOptions.value.length;
118
+ } else if (e.key === "ArrowUp") {
119
+ e.preventDefault();
120
+ highlightedIndex.value =
121
+ (highlightedIndex.value - 1 + normalizedOptions.value.length) %
122
+ normalizedOptions.value.length;
123
+ } else if (e.key === "Enter") {
124
+ e.preventDefault();
125
+ const opt = normalizedOptions.value[highlightedIndex.value];
126
+ if (opt && !opt.disabled) {
127
+ select(opt.value);
128
+ }
129
+ } else if (e.key === "Escape") {
130
+ e.preventDefault();
131
+ isOpen.value = false;
132
+ }
133
+ }
134
+
135
+ watch(isOpen, (open) => {
136
+ if (open) {
137
+ const idx = normalizedOptions.value.findIndex(
138
+ (o) => o.value === props.modelValue,
139
+ );
140
+ highlightedIndex.value = idx;
141
+ nextTick(() => {
142
+ scrollToHighlighted();
143
+ });
144
+ } else {
145
+ highlightedIndex.value = -1;
146
+ }
147
+ });
148
+
149
+ function scrollToHighlighted() {
150
+ const el = panelRef.value?.querySelector(
151
+ `[data-select-index="${highlightedIndex.value}"]`,
152
+ );
153
+ if (el) {
154
+ el.scrollIntoView({ block: "nearest" });
155
+ }
156
+ }
157
+
158
+ watch(highlightedIndex, () => {
159
+ scrollToHighlighted();
160
+ });
161
+
162
+ const triggerCoords = computed(() => {
163
+ if (!triggerRef.value) return {};
164
+ const rect = triggerRef.value.getBoundingClientRect();
165
+ return {
166
+ position: "fixed" as const,
167
+ top: `${rect.bottom + 4}px`,
168
+ left: `${rect.left}px`,
169
+ minWidth: `${rect.width}px`,
170
+ };
171
+ });
172
+
173
+ return () => (
174
+ <div class="hk-select-wrapper">
175
+ {props.label ? (
176
+ <label class="hk-select-label">
177
+ {props.label}
178
+ {props.required && <span class="hk-select-required">*</span>}
179
+ </label>
180
+ ) : null}
181
+ <button
182
+ ref={triggerRef}
183
+ type="button"
184
+ class="hk-select-trigger"
185
+ data-error={props.error || undefined}
186
+ data-disabled={props.disabled || undefined}
187
+ data-state={isOpen.value ? "open" : "closed"}
188
+ disabled={props.disabled}
189
+ onClick={toggle}
190
+ onKeydown={onTriggerKeydown}
191
+ >
192
+ <span class="hk-select-value">{displayLabel.value || props.placeholder}</span>
193
+ <ChevronDown size={16} class="hk-select-arrow" />
194
+ </button>
195
+ {isOpen.value && (
196
+ <Teleport to="body">
197
+ <div
198
+ ref={panelRef}
199
+ class="hk-select-popout"
200
+ style={triggerCoords.value}
201
+ onKeydown={onPopoutKeydown}
202
+ >
203
+ {slots.default
204
+ ? slots.default()
205
+ : normalizedOptions.value.map((opt, i) => (
206
+ <div
207
+ key={opt.value}
208
+ class="hk-select-option"
209
+ data-highlighted={highlightedIndex.value === i || undefined}
210
+ data-select-index={i}
211
+ data-checked={opt.value === props.modelValue || undefined}
212
+ data-disabled={opt.disabled || undefined}
213
+ onClick={() => {
214
+ if (!opt.disabled) select(opt.value);
215
+ }}
216
+ onPointerenter={() => {
217
+ highlightedIndex.value = i;
218
+ }}
219
+ >
220
+ <span>{opt.label}</span>
221
+ </div>
222
+ ))}
223
+ </div>
224
+ </Teleport>
225
+ )}
226
+ {props.error ? <p class="hk-select-error">{props.error}</p> : null}
227
+ </div>
228
+ );
229
+ },
230
+ });