@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,291 @@
1
+ import { defineComponent, onBeforeUnmount, ref, shallowRef, watch, type PropType } from "vue";
2
+ import { onceFrame } from "../runtime/animationBus";
3
+ import { useReportedTransition } from "../composables/useReportedTransition";
4
+ import "./HkDraggableGrid.scss";
5
+
6
+ const MAX_COLS = 3;
7
+
8
+ export interface GridItem {
9
+ key: string;
10
+ locked?: boolean;
11
+ [k: string]: unknown;
12
+ }
13
+
14
+ export interface GridCellAddress {
15
+ row: number;
16
+ col: number;
17
+ }
18
+
19
+ interface DragState {
20
+ from: GridCellAddress;
21
+ placeholder: GridCellAddress;
22
+ grabOffsetX: number;
23
+ grabOffsetY: number;
24
+ }
25
+
26
+ export default defineComponent({
27
+ name: "HkDraggableGrid",
28
+ props: {
29
+ rows: { type: Array as PropType<GridItem[][]>, required: true },
30
+ lockedKeys: { type: Array as PropType<string[]>, default: () => [] },
31
+ },
32
+ emits: {
33
+ reorder: (_from: GridCellAddress, _to: GridCellAddress) => true,
34
+ },
35
+ setup(props, { emit, slots }) {
36
+ function isLocked(key: string): boolean {
37
+ return props.lockedKeys.includes(key);
38
+ }
39
+
40
+ const drag = shallowRef<DragState | null>(null);
41
+ const tick = ref(0);
42
+ const rowEls = ref<(HTMLElement | null)[]>([]);
43
+ const cellEls = ref<(HTMLElement | null)[][]>([]);
44
+
45
+ function setRowEl(row: number, el: Element | null) {
46
+ rowEls.value[row] = (el as HTMLElement | null) ?? null;
47
+ }
48
+ function setCellEl(row: number, col: number, el: Element | null) {
49
+ if (!cellEls.value[row]) cellEls.value[row] = [];
50
+ cellEls.value[row][col] = (el as HTMLElement | null) ?? null;
51
+ }
52
+
53
+ let ghostNode: HTMLElement | null = null;
54
+
55
+ const anim = useReportedTransition(250);
56
+
57
+ function bump() { tick.value++; }
58
+
59
+ watch(
60
+ () => props.rows,
61
+ (rows) => {
62
+ rowEls.value = rowEls.value.slice(0, rows.length);
63
+ cellEls.value = cellEls.value.slice(0, rows.length);
64
+ },
65
+ { deep: true },
66
+ );
67
+
68
+ // ── Drag lifecycle ──
69
+
70
+ function onPointerDown(e: PointerEvent, row: number, col: number, item: GridItem) {
71
+ if (e.button !== 0) return;
72
+ if (isLocked(item.key)) return;
73
+ const handle = (e.currentTarget as HTMLElement);
74
+ const isHandle = handle.classList.contains("hk-draggable-grid-handle");
75
+ if (!isHandle) {
76
+ const target = e.target as HTMLElement | null;
77
+ if (target && target.closest("button, a, input, textarea, select, [contenteditable]")) {
78
+ return;
79
+ }
80
+ }
81
+
82
+ const cell = cellEls.value[row]?.[col];
83
+ if (!cell) return;
84
+
85
+ e.preventDefault();
86
+ const rect = cell.getBoundingClientRect();
87
+
88
+ const snapshot = cell.outerHTML
89
+ .replace(/\sdraggable="[^"]*"/g, "")
90
+ .replace(/\sdata-dragging="[^"]*"/g, "");
91
+
92
+ const state: DragState = {
93
+ from: { row, col },
94
+ placeholder: { row, col },
95
+ grabOffsetX: e.clientX - rect.left,
96
+ grabOffsetY: e.clientY - rect.top,
97
+ };
98
+ drag.value = state;
99
+ bump();
100
+
101
+ onceFrame(() => {
102
+ if (!drag.value) return;
103
+ attachGhost(rect, snapshot);
104
+ anim.run();
105
+ });
106
+
107
+ window.addEventListener("pointermove", onPointerMove, { passive: false });
108
+ window.addEventListener("pointerup", onPointerUp, { once: true });
109
+ window.addEventListener("pointercancel", onPointerUp, { once: true });
110
+ }
111
+
112
+ function attachGhost(sourceRect: DOMRect, html: string) {
113
+ detachGhost();
114
+ const node = document.createElement("div");
115
+ node.className = "hk-draggable-grid-ghost";
116
+ node.innerHTML = html;
117
+ node.style.width = `${sourceRect.width}px`;
118
+ node.style.transform = `translate3d(${sourceRect.left}px, ${sourceRect.top}px, 0)`;
119
+ node.style.pointerEvents = "none";
120
+ document.body.appendChild(node);
121
+ ghostNode = node;
122
+ }
123
+
124
+ function detachGhost() {
125
+ if (ghostNode) {
126
+ ghostNode.remove();
127
+ ghostNode = null;
128
+ }
129
+ }
130
+
131
+ function moveGhost(clientX: number, clientY: number) {
132
+ const s = drag.value;
133
+ if (!s || !ghostNode) return;
134
+ const gx = clientX - s.grabOffsetX;
135
+ const gy = clientY - s.grabOffsetY;
136
+ ghostNode.style.transform = `translate3d(${gx}px, ${gy}px, 0)`;
137
+ }
138
+
139
+ function sameAddress(a: GridCellAddress, b: GridCellAddress): boolean {
140
+ return a.row === b.row && a.col === b.col;
141
+ }
142
+
143
+ function recomputePlaceholder(clientX: number, clientY: number) {
144
+ const s = drag.value;
145
+ if (!s) return;
146
+ const rows = props.rows;
147
+
148
+ let row = -1;
149
+ for (let r = 0; r < rows.length; r++) {
150
+ const el = rowEls.value[r];
151
+ if (!el) continue;
152
+ const rect = el.getBoundingClientRect();
153
+ const midY = rect.top + rect.height / 2;
154
+ if (clientY < midY) { row = r; break; }
155
+ row = r;
156
+ }
157
+ if (row === -1) row = s.from.row;
158
+
159
+ const cells = rows[row] ?? [];
160
+
161
+ const isFull = cells.length >= MAX_COLS;
162
+ const sourceInThisRow = s.from.row === row;
163
+ if (isFull && !sourceInThisRow) {
164
+ const target = { row: row + 1, col: 0 };
165
+ if (!sameAddress(target, s.placeholder)) {
166
+ s.placeholder = target;
167
+ bump();
168
+ anim.run();
169
+ }
170
+ return;
171
+ }
172
+
173
+ let col = cells.length;
174
+ for (let c = 0; c < cells.length; c++) {
175
+ const el = cellEls.value[row]?.[c];
176
+ if (!el) continue;
177
+ if (sourceInThisRow && c === s.from.col) continue;
178
+ const rect = el.getBoundingClientRect();
179
+ const midX = rect.left + rect.width / 2;
180
+ if (clientX < midX) { col = c; break; }
181
+ }
182
+
183
+ const target = { row, col };
184
+ if (!sameAddress(target, s.placeholder)) {
185
+ s.placeholder = target;
186
+ bump();
187
+ anim.run();
188
+ }
189
+ }
190
+
191
+ function onPointerMove(e: PointerEvent) {
192
+ const s = drag.value;
193
+ if (!s) return;
194
+ e.preventDefault();
195
+ moveGhost(e.clientX, e.clientY);
196
+ recomputePlaceholder(e.clientX, e.clientY);
197
+ }
198
+
199
+ function onPointerUp() {
200
+ const s = drag.value;
201
+ window.removeEventListener("pointermove", onPointerMove);
202
+
203
+ if (s && !sameAddress(s.from, s.placeholder)) {
204
+ emit("reorder", s.from, s.placeholder);
205
+ }
206
+ anim.run();
207
+ drag.value = null;
208
+ bump();
209
+ onceFrame(detachGhost);
210
+ }
211
+
212
+ onBeforeUnmount(() => {
213
+ window.removeEventListener("pointermove", onPointerMove);
214
+ detachGhost();
215
+ });
216
+
217
+ // ── Render ──
218
+
219
+ return () => {
220
+ void tick.value;
221
+ const s = drag.value;
222
+
223
+ const phRow = s?.placeholder.row;
224
+ const phCol = s?.placeholder.col;
225
+
226
+ return (
227
+ <div class="hk-draggable-grid" data-dragging={s ? "" : undefined}>
228
+ {props.rows.map((cells, row) => {
229
+ const cols = Math.max(1, cells.length);
230
+ const isPhRowAfter = s != null && phRow === row && phCol != null && phCol >= cells.length;
231
+ return (
232
+ <div
233
+ key={row}
234
+ ref={(el) => setRowEl(row, el as Element | null)}
235
+ class="hk-draggable-grid-row"
236
+ data-cols={cols}
237
+ data-placeholder-row={isPhRowAfter ? "after" : undefined}
238
+ style={{ "--hk-grid-cols": cols }}
239
+ >
240
+ {cells.map((item, col) => {
241
+ const locked = isLocked(item.key);
242
+ const isSource = s != null && s.from.row === row && s.from.col === col;
243
+ const isPhBefore = s != null && phRow === row && phCol === col && !isSource;
244
+ return (
245
+ <div
246
+ key={item.key}
247
+ ref={(el) => setCellEl(row, col, el as Element | null)}
248
+ class="hk-draggable-grid-cell"
249
+ data-locked={locked || undefined}
250
+ data-dragging={isSource || undefined}
251
+ data-placeholder={isPhBefore ? "before" : undefined}
252
+ onPointerdown={(e: PointerEvent) => onPointerDown(e, row, col, item)}
253
+ >
254
+ <span
255
+ class="hk-draggable-grid-handle"
256
+ data-hidden={locked || undefined}
257
+ onPointerdown={(e: PointerEvent) => {
258
+ e.stopPropagation();
259
+ onPointerDown(e, row, col, item);
260
+ }}
261
+ >
262
+ {slots.handle ? slots.handle() : (
263
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor">
264
+ <circle cx="4" cy="3" r="1.2" />
265
+ <circle cx="4" cy="7" r="1.2" />
266
+ <circle cx="4" cy="11" r="1.2" />
267
+ <circle cx="10" cy="3" r="1.2" />
268
+ <circle cx="10" cy="7" r="1.2" />
269
+ <circle cx="10" cy="11" r="1.2" />
270
+ </svg>
271
+ )}
272
+ </span>
273
+ <div class="hk-draggable-grid-content">
274
+ {slots.cell?.({ item, row, col, locked })}
275
+ </div>
276
+ </div>
277
+ );
278
+ })}
279
+ </div>
280
+ );
281
+ })}
282
+ {s != null && phRow != null && phRow >= props.rows.length && (
283
+ <div class="hk-draggable-grid-row hk-draggable-grid-row-phantom" data-cols={1}>
284
+ <div class="hk-draggable-grid-cell hk-draggable-grid-cell-placeholder-target" />
285
+ </div>
286
+ )}
287
+ </div>
288
+ );
289
+ };
290
+ },
291
+ });
@@ -0,0 +1,110 @@
1
+ .hk-draggable-list {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: 4px;
5
+ }
6
+
7
+ .hk-draggable-list-item {
8
+ display: flex;
9
+ align-items: center;
10
+ gap: 0.5rem;
11
+ min-height: 40px;
12
+ padding: 0.5rem 0.625rem;
13
+ border-radius: 4px;
14
+ background: var(--hi-color-bg-subtle, #21262d);
15
+ border: 1px solid var(--hi-color-border, #30363d);
16
+ transition:
17
+ transform 150ms cubic-bezier(0.16, 1, 0.3, 1),
18
+ opacity 150ms cubic-bezier(0.16, 1, 0.3, 1),
19
+ border-color 150ms ease,
20
+ background 150ms ease;
21
+ user-select: none;
22
+ -webkit-user-select: none;
23
+ transform-origin: center;
24
+
25
+ &:hover {
26
+ border-color: var(--hi-color-text-tertiary, #8b949e);
27
+ }
28
+
29
+ &[data-dragging] {
30
+ opacity: 0.35;
31
+ transform: scale(0.96);
32
+ border-style: dashed;
33
+ border-color: var(--hi-color-primary, #58a6ff);
34
+ background: rgb(from var(--hi-color-primary, #58a6ff) r g b / 0.04);
35
+ cursor: grabbing;
36
+ z-index: 0;
37
+ }
38
+
39
+ &[data-placeholder="before"] {
40
+ box-shadow: inset 0 2px 0 0 var(--hi-color-primary, #58a6ff);
41
+ }
42
+ &[data-placeholder="after"] {
43
+ box-shadow: inset 0 -2px 0 0 var(--hi-color-primary, #58a6ff);
44
+ }
45
+
46
+ &[data-locked] {
47
+ opacity: 0.65;
48
+ }
49
+
50
+ .hk-draggable-list[data-dragging] & {
51
+ &:hover {
52
+ border-color: var(--hi-color-border, #30363d);
53
+ }
54
+ }
55
+ }
56
+
57
+ .hk-draggable-list-handle {
58
+ display: flex;
59
+ align-items: center;
60
+ justify-content: center;
61
+ width: 20px;
62
+ flex-shrink: 0;
63
+ color: var(--hi-color-text-tertiary, #8b949e);
64
+ cursor: grab;
65
+ touch-action: none;
66
+
67
+ &:active {
68
+ cursor: grabbing;
69
+ }
70
+
71
+ &[data-hidden] {
72
+ visibility: hidden;
73
+ }
74
+ }
75
+
76
+ .hk-draggable-list-content {
77
+ flex: 1;
78
+ min-width: 0;
79
+ display: flex;
80
+ align-items: center;
81
+ gap: 0.5rem;
82
+ }
83
+
84
+ .hk-draggable-list-ghost {
85
+ position: fixed;
86
+ top: 0;
87
+ left: 0;
88
+ margin: 0;
89
+ transform: translate3d(0, 0, 0) scale(1.02);
90
+ opacity: 0.85;
91
+ box-shadow:
92
+ 0 8px 24px rgb(0 0 0 / 28%),
93
+ 0 2px 6px rgb(0 0 0 / 18%);
94
+ border-radius: 4px;
95
+ background: var(--hi-color-bg-elevated, #161b22);
96
+ backdrop-filter: blur(2px);
97
+ pointer-events: none;
98
+ z-index: 9999;
99
+ will-change: transform;
100
+
101
+ > .hk-draggable-list-item {
102
+ margin: 0;
103
+ border-color: transparent;
104
+ background: transparent;
105
+ box-shadow: none;
106
+ transform: none;
107
+ opacity: 1;
108
+ width: 100%;
109
+ }
110
+ }
@@ -0,0 +1,238 @@
1
+ import { defineComponent, onBeforeUnmount, ref, shallowRef, type PropType } from "vue";
2
+ import { onceFrame } from "../runtime/animationBus";
3
+ import { useReportedTransition } from "../composables/useReportedTransition";
4
+ import "./HkDraggableList.scss";
5
+
6
+ export interface DragListItem {
7
+ key: string;
8
+ locked?: boolean;
9
+ [k: string]: unknown;
10
+ }
11
+
12
+ interface DragState {
13
+ fromIndex: number;
14
+ placeholderIndex: number;
15
+ originX: number;
16
+ grabOffsetX: number;
17
+ grabOffsetY: number;
18
+ }
19
+
20
+ export default defineComponent({
21
+ name: "HkDraggableList",
22
+ props: {
23
+ items: { type: Array as PropType<DragListItem[]>, required: true },
24
+ lockedKeys: { type: Array as PropType<string[]>, default: () => [] },
25
+ },
26
+ emits: {
27
+ reorder: (_fromIndex: number, _toIndex: number) => true,
28
+ },
29
+ setup(props, { emit, slots }) {
30
+ function isLocked(key: string): boolean {
31
+ return props.lockedKeys.includes(key);
32
+ }
33
+
34
+ const drag = shallowRef<DragState | null>(null);
35
+ const tick = ref(0);
36
+ const rowEls = ref<(HTMLElement | null)[]>([]);
37
+
38
+ function setRowEl(index: number, el: Element | null) {
39
+ rowEls.value[index] = (el as HTMLElement | null) ?? null;
40
+ }
41
+
42
+ let ghostNode: HTMLElement | null = null;
43
+
44
+ const anim = useReportedTransition(250);
45
+
46
+ function bump() { tick.value++; }
47
+
48
+ // ── Drag lifecycle ──
49
+
50
+ function onPointerDown(e: PointerEvent, index: number, item: DragListItem) {
51
+ if (e.button !== 0) return;
52
+ if (isLocked(item.key)) return;
53
+
54
+ const handle = (e.currentTarget as HTMLElement);
55
+ const isHandle = handle.classList.contains("hk-draggable-list-handle");
56
+ if (!isHandle) {
57
+ const target = e.target as HTMLElement | null;
58
+ if (target && target.closest("button, a, input, textarea, select, [contenteditable]")) {
59
+ return;
60
+ }
61
+ }
62
+
63
+ const row = rowEls.value[index];
64
+ if (!row) return;
65
+
66
+ e.preventDefault();
67
+ const rect = row.getBoundingClientRect();
68
+
69
+ const snapshot = row.outerHTML
70
+ .replace(/\sdraggable="[^"]*"/g, "")
71
+ .replace(/\sdata-dragging="[^"]*"/g, "");
72
+
73
+ const state: DragState = {
74
+ fromIndex: index,
75
+ placeholderIndex: index,
76
+ originX: e.clientX,
77
+ grabOffsetX: e.clientX - rect.left,
78
+ grabOffsetY: e.clientY - rect.top,
79
+ };
80
+ drag.value = state;
81
+ bump();
82
+
83
+ onceFrame(() => {
84
+ if (!drag.value) return;
85
+ attachGhost(rect, snapshot);
86
+ anim.run();
87
+ });
88
+
89
+ window.addEventListener("pointermove", onPointerMove, { passive: false });
90
+ window.addEventListener("pointerup", onPointerUp, { once: true });
91
+ window.addEventListener("pointercancel", onPointerUp, { once: true });
92
+ }
93
+
94
+ function attachGhost(sourceRect: DOMRect, html: string) {
95
+ detachGhost();
96
+ const node = document.createElement("div");
97
+ node.className = "hk-draggable-list-ghost";
98
+ node.innerHTML = html;
99
+ node.style.width = `${sourceRect.width}px`;
100
+ node.style.transform = `translate3d(${sourceRect.left}px, ${sourceRect.top}px, 0)`;
101
+ node.style.pointerEvents = "none";
102
+ document.body.appendChild(node);
103
+ ghostNode = node;
104
+ }
105
+
106
+ function detachGhost() {
107
+ if (ghostNode) {
108
+ ghostNode.remove();
109
+ ghostNode = null;
110
+ }
111
+ }
112
+
113
+ function moveGhost(clientY: number) {
114
+ const s = drag.value;
115
+ if (!s || !ghostNode) return;
116
+ const gx = s.originX - s.grabOffsetX;
117
+ const gy = clientY - s.grabOffsetY;
118
+ ghostNode.style.transform = `translate3d(${gx}px, ${gy}px, 0)`;
119
+ }
120
+
121
+ function recomputePlaceholder(clientY: number) {
122
+ const s = drag.value;
123
+ if (!s) return;
124
+ const rows = rowEls.value;
125
+ let target = s.fromIndex;
126
+ let fallback = s.fromIndex;
127
+
128
+ for (let i = 0; i < rows.length; i++) {
129
+ if (i === s.fromIndex) continue;
130
+ const item = props.items[i];
131
+ if (!item || isLocked(item.key)) continue;
132
+ const el = rows[i];
133
+ if (!el) continue;
134
+
135
+ const rect = el.getBoundingClientRect();
136
+ const mid = rect.top + rect.height / 2;
137
+ fallback = i;
138
+
139
+ if (clientY < mid) {
140
+ target = i;
141
+ break;
142
+ }
143
+ target = i;
144
+ }
145
+
146
+ if (target === s.fromIndex && fallback !== s.fromIndex) {
147
+ target = fallback;
148
+ }
149
+
150
+ if (target !== s.placeholderIndex) {
151
+ s.placeholderIndex = target;
152
+ bump();
153
+ anim.run();
154
+ }
155
+ }
156
+
157
+ function onPointerMove(e: PointerEvent) {
158
+ const s = drag.value;
159
+ if (!s) return;
160
+ e.preventDefault();
161
+ moveGhost(e.clientY);
162
+ recomputePlaceholder(e.clientY);
163
+ }
164
+
165
+ function onPointerUp() {
166
+ const s = drag.value;
167
+ window.removeEventListener("pointermove", onPointerMove);
168
+
169
+ if (s) {
170
+ if (s.placeholderIndex !== s.fromIndex) {
171
+ emit("reorder", s.fromIndex, s.placeholderIndex);
172
+ }
173
+ }
174
+ anim.run();
175
+ drag.value = null;
176
+ bump();
177
+ onceFrame(detachGhost);
178
+ }
179
+
180
+ onBeforeUnmount(() => {
181
+ window.removeEventListener("pointermove", onPointerMove);
182
+ detachGhost();
183
+ });
184
+
185
+ // ── Render ──
186
+
187
+ return () => {
188
+ void tick.value;
189
+ const s = drag.value;
190
+ return (
191
+ <div class="hk-draggable-list" data-dragging={s ? "" : undefined}>
192
+ {props.items.map((item, index) => {
193
+ const locked = isLocked(item.key);
194
+ const isSource = s != null && s.fromIndex === index;
195
+ const placeholderDir =
196
+ s != null && !isSource && s.placeholderIndex === index
197
+ ? (s.placeholderIndex < s.fromIndex ? "before" : "after")
198
+ : null;
199
+ return (
200
+ <div
201
+ key={item.key}
202
+ ref={(el) => setRowEl(index, el as Element | null)}
203
+ class="hk-draggable-list-item"
204
+ data-locked={locked || undefined}
205
+ data-dragging={isSource || undefined}
206
+ data-placeholder={placeholderDir || undefined}
207
+ onPointerdown={(e: PointerEvent) => onPointerDown(e, index, item)}
208
+ >
209
+ <span
210
+ class="hk-draggable-list-handle"
211
+ data-hidden={locked || undefined}
212
+ onPointerdown={(e: PointerEvent) => {
213
+ e.stopPropagation();
214
+ onPointerDown(e, index, item);
215
+ }}
216
+ >
217
+ {slots.handle ? slots.handle() : (
218
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor">
219
+ <circle cx="4" cy="3" r="1.2" />
220
+ <circle cx="4" cy="7" r="1.2" />
221
+ <circle cx="4" cy="11" r="1.2" />
222
+ <circle cx="10" cy="3" r="1.2" />
223
+ <circle cx="10" cy="7" r="1.2" />
224
+ <circle cx="10" cy="11" r="1.2" />
225
+ </svg>
226
+ )}
227
+ </span>
228
+ <div class="hk-draggable-list-content">
229
+ {slots.item?.({ item, index, locked })}
230
+ </div>
231
+ </div>
232
+ );
233
+ })}
234
+ </div>
235
+ );
236
+ };
237
+ },
238
+ });