@kine-design/core 0.0.1-beta.11 → 0.0.1-beta.13

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 (67) hide show
  1. package/components/base/image/__tests__/useImage.test.ts +61 -0
  2. package/components/base/image/api.ts +2 -2
  3. package/components/base/image/index.ts +2 -2
  4. package/components/base/image/props.d.ts +15 -4
  5. package/components/base/image/useImage.ts +27 -16
  6. package/components/base/tooltip/api.ts +3 -2
  7. package/components/base/tooltip/index.ts +3 -2
  8. package/components/base/tooltip/props.d.ts +12 -6
  9. package/components/base/tooltip/useTooltip.ts +175 -56
  10. package/compositions/commandPalette/index.ts +11 -0
  11. package/compositions/commandPalette/types.ts +29 -0
  12. package/compositions/commandPalette/useCommandPalette.ts +135 -0
  13. package/compositions/contextMenu/index.ts +10 -0
  14. package/compositions/contextMenu/types.ts +21 -0
  15. package/compositions/contextMenu/useContextMenu.ts +101 -0
  16. package/compositions/editor/index.ts +18 -0
  17. package/compositions/editor/types.ts +147 -0
  18. package/compositions/editor/useEditor.ts +224 -0
  19. package/compositions/index.ts +15 -0
  20. package/compositions/overlay/index.ts +14 -0
  21. package/compositions/overlay/useOverlayStack.ts +146 -0
  22. package/compositions/peekView/index.ts +10 -0
  23. package/compositions/peekView/usePeekView.ts +99 -0
  24. package/compositions/theme/index.ts +17 -0
  25. package/compositions/theme/presets/compact.ts +117 -0
  26. package/compositions/theme/presets/dark.ts +113 -0
  27. package/compositions/theme/presets/index.ts +11 -0
  28. package/compositions/theme/presets/light.ts +113 -0
  29. package/compositions/theme/types.ts +46 -0
  30. package/compositions/theme/useTheme.ts +269 -0
  31. package/compositions/toast/index.ts +10 -0
  32. package/compositions/toast/useToast.ts +176 -0
  33. package/compositions/tooltip/index.ts +9 -0
  34. package/compositions/tooltip/useTooltip.ts +10 -0
  35. package/dist/components/base/image/index.d.ts +2 -2
  36. package/dist/components/base/image/useImage.d.ts +7 -0
  37. package/dist/components/base/tooltip/index.d.ts +1 -0
  38. package/dist/components/base/tooltip/useTooltip.d.ts +20 -17
  39. package/dist/compositions/commandPalette/index.d.ts +11 -0
  40. package/dist/compositions/commandPalette/types.d.ts +20 -0
  41. package/dist/compositions/commandPalette/useCommandPalette.d.ts +32 -0
  42. package/dist/compositions/contextMenu/index.d.ts +10 -0
  43. package/dist/compositions/contextMenu/types.d.ts +12 -0
  44. package/dist/compositions/contextMenu/useContextMenu.d.ts +52 -0
  45. package/dist/compositions/editor/index.d.ts +10 -0
  46. package/dist/compositions/editor/types.d.ts +132 -0
  47. package/dist/compositions/editor/useEditor.d.ts +13 -0
  48. package/dist/compositions/index.d.ts +15 -0
  49. package/dist/compositions/overlay/index.d.ts +10 -0
  50. package/dist/compositions/overlay/useOverlayStack.d.ts +188 -0
  51. package/dist/compositions/peekView/index.d.ts +10 -0
  52. package/dist/compositions/peekView/usePeekView.d.ts +16 -0
  53. package/dist/compositions/theme/index.d.ts +11 -0
  54. package/dist/compositions/theme/presets/compact.d.ts +6 -0
  55. package/dist/compositions/theme/presets/dark.d.ts +2 -0
  56. package/dist/compositions/theme/presets/index.d.ts +11 -0
  57. package/dist/compositions/theme/presets/light.d.ts +2 -0
  58. package/dist/compositions/theme/types.d.ts +41 -0
  59. package/dist/compositions/theme/useTheme.d.ts +167 -0
  60. package/dist/compositions/toast/index.d.ts +10 -0
  61. package/dist/compositions/toast/useToast.d.ts +71 -0
  62. package/dist/compositions/tooltip/index.d.ts +9 -0
  63. package/dist/compositions/tooltip/useTooltip.d.ts +10 -0
  64. package/dist/core.js +836 -56
  65. package/dist/index.d.ts +1 -0
  66. package/index.ts +2 -0
  67. package/package.json +13 -2
@@ -173,4 +173,65 @@ describe('useImage', () => {
173
173
  expect(previewScale.value).toBe(1);
174
174
  expect(previewRotate.value).toBe(0);
175
175
  });
176
+
177
+ // ── 混合媒体预览 ──
178
+
179
+ it('纯字符串列表 currentPreviewItem.type 为 image', () => {
180
+ const emit = createEmit();
181
+ const props = { ...defaultProps, src: 'a.png', previewSrcList: ['a.png', 'b.png'] };
182
+ const { currentPreviewItem, openPreview } = useImage(props, emit);
183
+
184
+ openPreview();
185
+ expect(currentPreviewItem.value).toEqual({ src: 'a.png', type: 'image' });
186
+ });
187
+
188
+ it('混合列表定位到 PreviewItem 对象', () => {
189
+ const emit = createEmit();
190
+ const props = {
191
+ ...defaultProps,
192
+ src: 'video.mp4',
193
+ previewSrcList: [
194
+ 'a.png',
195
+ { src: 'video.mp4', type: 'video' as const },
196
+ 'c.png',
197
+ ],
198
+ };
199
+ const { currentPreviewItem, currentPreviewIsVideo, previewIndex, openPreview } = useImage(props, emit);
200
+
201
+ openPreview();
202
+ expect(previewIndex.value).toBe(1);
203
+ expect(currentPreviewItem.value).toEqual({ src: 'video.mp4', type: 'video' });
204
+ expect(currentPreviewIsVideo.value).toBe(true);
205
+ });
206
+
207
+ it('切换时 currentPreviewIsVideo 跟随变化', () => {
208
+ const emit = createEmit();
209
+ const props = {
210
+ ...defaultProps,
211
+ src: 'a.png',
212
+ previewSrcList: [
213
+ 'a.png',
214
+ { src: 'video.mp4', type: 'video' as const },
215
+ ],
216
+ };
217
+ const { currentPreviewIsVideo, openPreview, previewNext } = useImage(props, emit);
218
+
219
+ openPreview();
220
+ expect(currentPreviewIsVideo.value).toBe(false);
221
+
222
+ previewNext();
223
+ expect(currentPreviewIsVideo.value).toBe(true);
224
+ });
225
+
226
+ it('PreviewItem 不指定 type 时默认为 image', () => {
227
+ const emit = createEmit();
228
+ const props = {
229
+ ...defaultProps,
230
+ previewSrcList: [{ src: 'photo.jpg' }],
231
+ };
232
+ const { currentPreviewItem, openPreview } = useImage(props, emit);
233
+
234
+ openPreview();
235
+ expect(currentPreviewItem.value.type).toBe('image');
236
+ });
176
237
  });
@@ -7,7 +7,7 @@
7
7
  * 江湖的业务千篇一律,复杂的代码好几百行。
8
8
  */
9
9
  import { MCOPO, MPropType } from '../../types/props';
10
- import { ImageProps } from './props';
10
+ import { ImageProps, PreviewSrcListItem } from './props';
11
11
 
12
12
  export const props: MCOPO<ImageProps> = {
13
13
  src: { type: String, required: true },
@@ -20,6 +20,6 @@ export const props: MCOPO<ImageProps> = {
20
20
  width: { type: [String, Number], default: undefined },
21
21
  height: { type: [String, Number], default: undefined },
22
22
  lazy: { type: Boolean, default: false },
23
- previewSrcList: { type: Array as MPropType<string[]>, default: () => [] },
23
+ previewSrcList: { type: Array as MPropType<PreviewSrcListItem[]>, default: () => [] },
24
24
  zIndex: { type: Number, default: 2000 },
25
25
  };
@@ -14,5 +14,5 @@ export const ImageCore = {
14
14
  useImage,
15
15
  };
16
16
 
17
- export type { ImageProps, ImageEvents } from './props';
18
- export type { ImageLoadStatus } from './useImage';
17
+ export type { ImageProps, ImageEvents, PreviewItem, PreviewSrcListItem } from './props';
18
+ export type { ImageLoadStatus, NormalizedPreviewItem } from './useImage';
@@ -6,11 +6,21 @@
6
6
  *
7
7
  * @name m-image
8
8
  * @docDescription Image component with lazy loading and preview support.
9
- * 图片组件,支持懒加载和全屏预览。
9
+ * 图片组件,支持懒加载和全屏预览。支持图片与视频混合预览。
10
10
  *
11
11
  * 江湖的业务千篇一律,复杂的代码好几百行。
12
12
  */
13
13
 
14
+ /**
15
+ * @description 预览列表项,支持纯字符串(向后兼容,视为图片)或结构化对象
16
+ */
17
+ export declare type PreviewItem = {
18
+ src: string,
19
+ type?: 'image' | 'video',
20
+ };
21
+
22
+ export declare type PreviewSrcListItem = string | PreviewItem;
23
+
14
24
  export declare type ImageProps = {
15
25
  /**
16
26
  * @description 图片地址
@@ -37,11 +47,12 @@ export declare type ImageProps = {
37
47
  */
38
48
  lazy?: boolean,
39
49
  /**
40
- * @description 开启预览的图片列表,传入后点击可全屏预览
41
- * @type string[]
50
+ * @description 开启预览的媒体列表,传入后点击可全屏预览。
51
+ * 支持纯字符串(向后兼容,视为图片)或 { src, type } 对象。
52
+ * @type (string | PreviewItem)[]
42
53
  * @default []
43
54
  */
44
- previewSrcList?: string[],
55
+ previewSrcList?: PreviewSrcListItem[],
45
56
  /**
46
57
  * @description 图片宽度
47
58
  * @type string | number
@@ -6,27 +6,44 @@
6
6
  *
7
7
  * 江湖的业务千篇一律,复杂的代码好几百行。
8
8
  */
9
- import { ref, watch, onMounted } from 'vue';
10
- import { ImageProps } from './props';
9
+ import { ref, computed, watch, onMounted } from 'vue';
10
+ import { ImageProps, PreviewSrcListItem } from './props';
11
11
 
12
12
  /** 图片加载状态 */
13
13
  export type ImageLoadStatus = 'loading' | 'loaded' | 'error';
14
14
 
15
+ /** 正规化后的预览项 */
16
+ export type NormalizedPreviewItem = { src: string; type: 'image' | 'video' };
17
+
18
+ function normalizeItem(item: PreviewSrcListItem): NormalizedPreviewItem {
19
+ if (typeof item === 'string') {
20
+ return { src: item, type: 'image' };
21
+ }
22
+ return { src: item.src, type: item.type ?? 'image' };
23
+ }
24
+
25
+ function getSrc(item: PreviewSrcListItem): string {
26
+ return typeof item === 'string' ? item : item.src;
27
+ }
28
+
15
29
  export function useImage(
16
30
  props: Required<ImageProps>,
17
31
  emit: (event: 'load' | 'error' | 'switch', payload: Event | number) => void,
18
32
  ) {
19
33
  const status = ref<ImageLoadStatus>('loading');
20
- // 预览弹层是否可见
21
34
  const previewVisible = ref(false);
22
- // 预览当前索引(在 previewSrcList 中的位置)
23
35
  const previewIndex = ref(0);
24
- // 预览缩放比例
25
36
  const previewScale = ref(1);
26
- // 预览旋转角度
27
37
  const previewRotate = ref(0);
28
38
 
29
- /** 重置加载状态(src 变化时调用) */
39
+ const currentPreviewItem = computed<NormalizedPreviewItem>(() => {
40
+ const list = props.previewSrcList;
41
+ if (!list || list.length === 0) { return { src: '', type: 'image' }; }
42
+ return normalizeItem(list[previewIndex.value] ?? list[0]);
43
+ });
44
+
45
+ const currentPreviewIsVideo = computed(() => currentPreviewItem.value.type === 'video');
46
+
30
47
  const reset = () => {
31
48
  status.value = 'loading';
32
49
  };
@@ -41,10 +58,9 @@ export function useImage(
41
58
  emit('error', e);
42
59
  };
43
60
 
44
- /** 打开全屏预览,定位到 src 在 previewSrcList 中的位置 */
45
61
  const openPreview = () => {
46
62
  if (!props.previewSrcList || props.previewSrcList.length === 0) { return; }
47
- const idx = props.previewSrcList.indexOf(props.src);
63
+ const idx = props.previewSrcList.findIndex(item => getSrc(item) === props.src);
48
64
  previewIndex.value = idx >= 0 ? idx : 0;
49
65
  previewScale.value = 1;
50
66
  previewRotate.value = 0;
@@ -55,7 +71,6 @@ export function useImage(
55
71
  previewVisible.value = false;
56
72
  };
57
73
 
58
- /** 预览切换到上一张 */
59
74
  const previewPrev = () => {
60
75
  const total = props.previewSrcList.length;
61
76
  if (total === 0) { return; }
@@ -65,7 +80,6 @@ export function useImage(
65
80
  emit('switch', previewIndex.value);
66
81
  };
67
82
 
68
- /** 预览切换到下一张 */
69
83
  const previewNext = () => {
70
84
  const total = props.previewSrcList.length;
71
85
  if (total === 0) { return; }
@@ -75,27 +89,22 @@ export function useImage(
75
89
  emit('switch', previewIndex.value);
76
90
  };
77
91
 
78
- /** 预览放大 */
79
92
  const zoomIn = () => {
80
93
  previewScale.value = Math.min(previewScale.value + 0.25, 5);
81
94
  };
82
95
 
83
- /** 预览缩小 */
84
96
  const zoomOut = () => {
85
97
  previewScale.value = Math.max(previewScale.value - 0.25, 0.25);
86
98
  };
87
99
 
88
- /** 顺时针旋转 90° */
89
100
  const rotate = () => {
90
101
  previewRotate.value = (previewRotate.value + 90) % 360;
91
102
  };
92
103
 
93
- // src 变化时重置加载状态
94
104
  watch(() => props.src, reset);
95
105
 
96
106
  onMounted(() => {
97
107
  if (props.lazy) {
98
- // 懒加载:交由 img 标签的 loading="lazy" 处理
99
108
  status.value = 'loading';
100
109
  }
101
110
  });
@@ -106,6 +115,8 @@ export function useImage(
106
115
  previewIndex,
107
116
  previewScale,
108
117
  previewRotate,
118
+ currentPreviewItem,
119
+ currentPreviewIsVideo,
109
120
  handleLoad,
110
121
  handleError,
111
122
  openPreview,
@@ -1,8 +1,8 @@
1
1
  /**
2
- * @description tooltip 运行时 props
2
+ * @description tooltip runtime props
3
3
  * @author 阿怪
4
4
  * @date 2026/2/25
5
- * @version v1.0.0
5
+ * @version v2.0.0
6
6
  *
7
7
  * 江湖的业务千篇一律,复杂的代码好几百行。
8
8
  */
@@ -15,6 +15,7 @@ export const props: MCOPO<TooltipProps> = {
15
15
  type: String as MPropType<NonNullable<TooltipProps['placement']>>,
16
16
  default: 'top',
17
17
  },
18
+ delay: { type: Number, default: 300 },
18
19
  disabled: { type: Boolean, default: false },
19
20
  maxWidth: { type: String, default: undefined },
20
21
  };
@@ -1,8 +1,8 @@
1
1
  /**
2
- * @description tooltip core 导出
2
+ * @description tooltip core barrel export
3
3
  * @author 阿怪
4
4
  * @date 2026/2/25
5
- * @version v1.0.0
5
+ * @version v2.0.0
6
6
  *
7
7
  * 江湖的业务千篇一律,复杂的代码好几百行。
8
8
  */
@@ -15,3 +15,4 @@ export const TooltipCore = {
15
15
  };
16
16
 
17
17
  export type { TooltipProps } from './props';
18
+ export type { TooltipPlacement, TooltipPositionData } from './useTooltip';
@@ -1,8 +1,8 @@
1
1
  /**
2
- * @description tooltip 类型定义
2
+ * @description tooltip type definitions
3
3
  * @author 阿怪
4
4
  * @date 2026/2/25
5
- * @version v1.0.0
5
+ * @version v2.0.0
6
6
  *
7
7
  * @name m-tooltip
8
8
  * @docDescription Tooltip component.
@@ -14,25 +14,31 @@
14
14
 
15
15
  export declare type TooltipProps = {
16
16
  /**
17
- * @description 提示内容,简单文本可直接通过 prop 传入
17
+ * @description tooltip text content
18
18
  * @type string
19
19
  * @default ''
20
20
  */
21
21
  content?: string;
22
22
  /**
23
- * @description 弹出位置
23
+ * @description preferred placement direction
24
24
  * @type 'top' | 'bottom' | 'left' | 'right'
25
25
  * @default 'top'
26
26
  */
27
27
  placement?: 'top' | 'bottom' | 'left' | 'right';
28
28
  /**
29
- * @description 是否禁用
29
+ * @description show delay in milliseconds
30
+ * @type number
31
+ * @default 300
32
+ */
33
+ delay?: number;
34
+ /**
35
+ * @description whether the tooltip is disabled
30
36
  * @type boolean
31
37
  * @default false
32
38
  */
33
39
  disabled?: boolean;
34
40
  /**
35
- * @description max width of tooltip content. 浮层最大宽度
41
+ * @description max width of tooltip content
36
42
  * @type string
37
43
  * @default undefined
38
44
  */
@@ -1,89 +1,208 @@
1
1
  /**
2
- * @description tooltip composable,管理 hover 显示/隐藏及定位
2
+ * @description tooltip composable manages show/hide with delay, positioning via usePopper, singleton, scroll/interaction dismissal
3
3
  * @author 阿怪
4
4
  * @date 2026/2/25
5
- * @version v1.0.0
5
+ * @version v2.0.0
6
6
  *
7
7
  * 江湖的业务千篇一律,复杂的代码好几百行。
8
8
  */
9
- import { ref, nextTick } from 'vue';
9
+ import { ref, onBeforeUnmount } from 'vue';
10
+ import { flip, offset, shift } from '@floating-ui/dom';
11
+ import { usePopper, type Placement, type PositionStyle } from '../../../compositions/popper/usePopper';
10
12
  import { TooltipProps } from './props';
11
13
 
12
- export type TooltipStyle = {
13
- position: string;
14
- left: string;
15
- top: string;
16
- zIndex: string;
17
- };
14
+ const HIDE_DELAY = 100;
18
15
 
19
- /**
20
- * 根据 trigger 元素的 getBoundingClientRect placement 计算 tooltip 的 fixed 定位样式
21
- */
22
- function calcPosition(
23
- rect: DOMRect,
24
- tooltipEl: HTMLElement,
25
- placement: NonNullable<TooltipProps['placement']>,
26
- ): TooltipStyle {
27
- const gap = 8;
28
- const tooltipRect = tooltipEl.getBoundingClientRect();
29
- let left = 0;
30
- let top = 0;
31
-
32
- switch (placement) {
33
- case 'top':
34
- left = rect.left + rect.width / 2 - tooltipRect.width / 2;
35
- top = rect.top - tooltipRect.height - gap;
36
- break;
37
- case 'bottom':
38
- left = rect.left + rect.width / 2 - tooltipRect.width / 2;
39
- top = rect.bottom + gap;
40
- break;
41
- case 'left':
42
- left = rect.left - tooltipRect.width - gap;
43
- top = rect.top + rect.height / 2 - tooltipRect.height / 2;
44
- break;
45
- case 'right':
46
- left = rect.right + gap;
47
- top = rect.top + rect.height / 2 - tooltipRect.height / 2;
48
- break;
49
- }
16
+ /** Singleton: only one tooltip visible at a time */
17
+ let activeHide: (() => void) | null = null;
50
18
 
51
- return {
52
- position: 'fixed',
53
- left: `${left}px`,
54
- top: `${top}px`,
55
- zIndex: '9999',
56
- };
57
- }
19
+ export type TooltipPlacement = Placement;
20
+
21
+ export type TooltipPositionData = {
22
+ style: Record<string, string>;
23
+ arrowStyle: Record<string, string>;
24
+ placement: Placement;
25
+ };
58
26
 
59
27
  export function useTooltip(props: Required<TooltipProps>) {
60
28
  const visible = ref(false);
61
- const tooltipStyle = ref<TooltipStyle | Record<string, never>>({});
29
+ const positionData = ref<TooltipPositionData | null>(null);
62
30
 
63
31
  const triggerRef = ref<HTMLElement | null>(null);
64
32
  const tooltipRef = ref<HTMLElement | null>(null);
33
+ const arrowRef = ref<HTMLElement | null>(null);
34
+
35
+ let showTimer: ReturnType<typeof setTimeout> | null = null;
36
+ let hideTimer: ReturnType<typeof setTimeout> | null = null;
37
+ let popperCleanup: (() => void) | null = null;
38
+
39
+ const tooltipId = `k-tooltip-${Math.random().toString(36).slice(2, 10)}`;
40
+
41
+ /** Clean up popper auto-update subscription */
42
+ const destroyPopper = () => {
43
+ if (popperCleanup) {
44
+ popperCleanup();
45
+ popperCleanup = null;
46
+ }
47
+ };
65
48
 
66
- const show = async () => {
49
+ /** Immediately hide no delay */
50
+ const hideImmediate = () => {
51
+ clearTimers();
52
+ visible.value = false;
53
+ positionData.value = null;
54
+ destroyPopper();
55
+ if (activeHide === hideImmediate) {
56
+ activeHide = null;
57
+ }
58
+ removeInteractionListeners();
59
+ removeScrollListeners();
60
+ };
61
+
62
+ /** Show tooltip after delay */
63
+ const show = () => {
67
64
  if (props.disabled) return;
65
+
66
+ // Dismiss any existing tooltip (singleton)
67
+ if (activeHide && activeHide !== hideImmediate) {
68
+ activeHide();
69
+ }
70
+
71
+ if (hideTimer) {
72
+ clearTimeout(hideTimer);
73
+ hideTimer = null;
74
+ }
75
+
76
+ if (visible.value) return;
77
+
78
+ const delay = props.delay ?? 300;
79
+ showTimer = setTimeout(() => {
80
+ showTimer = null;
81
+ if (props.disabled) return;
82
+ doShow();
83
+ }, delay);
84
+ };
85
+
86
+ /** Actually display the tooltip and set up popper */
87
+ const doShow = () => {
68
88
  visible.value = true;
69
- // 等待 tooltip DOM 渲染后再计算位置
70
- await nextTick();
89
+ activeHide = hideImmediate;
90
+ addInteractionListeners();
91
+ addScrollListeners();
92
+ };
93
+
94
+ /** Initialize popper positioning — called after tooltip DOM is mounted */
95
+ const initPopper = () => {
71
96
  if (!triggerRef.value || !tooltipRef.value) return;
72
- const rect = triggerRef.value.getBoundingClientRect();
73
- tooltipStyle.value = calcPosition(rect, tooltipRef.value, props.placement);
97
+
98
+ destroyPopper();
99
+
100
+ const { clear } = usePopper(
101
+ triggerRef.value,
102
+ tooltipRef.value,
103
+ (data: PositionStyle) => {
104
+ positionData.value = {
105
+ style: { ...data.style },
106
+ arrowStyle: { ...data.arrowStyle },
107
+ placement: data.placement,
108
+ };
109
+ },
110
+ arrowRef.value ?? undefined,
111
+ {
112
+ placement: props.placement as Placement,
113
+ middleware: [
114
+ offset(8),
115
+ flip(),
116
+ shift({ padding: 8 }),
117
+ ],
118
+ },
119
+ );
120
+
121
+ popperCleanup = clear;
74
122
  };
75
123
 
124
+ /** Hide tooltip with a short delay (allows cursor to move between trigger and tooltip) */
76
125
  const hide = () => {
77
- visible.value = false;
78
- tooltipStyle.value = {};
126
+ if (showTimer) {
127
+ clearTimeout(showTimer);
128
+ showTimer = null;
129
+ }
130
+
131
+ if (!visible.value) return;
132
+
133
+ hideTimer = setTimeout(() => {
134
+ hideTimer = null;
135
+ hideImmediate();
136
+ }, HIDE_DELAY);
137
+ };
138
+
139
+ const clearTimers = () => {
140
+ if (showTimer) {
141
+ clearTimeout(showTimer);
142
+ showTimer = null;
143
+ }
144
+ if (hideTimer) {
145
+ clearTimeout(hideTimer);
146
+ hideTimer = null;
147
+ }
79
148
  };
80
149
 
150
+ /* ── Interaction listeners: dismiss on click / keydown ── */
151
+ const onInteraction = () => {
152
+ hideImmediate();
153
+ };
154
+
155
+ const addInteractionListeners = () => {
156
+ document.addEventListener('mousedown', onInteraction, true);
157
+ document.addEventListener('keydown', onInteraction, true);
158
+ };
159
+
160
+ const removeInteractionListeners = () => {
161
+ document.removeEventListener('mousedown', onInteraction, true);
162
+ document.removeEventListener('keydown', onInteraction, true);
163
+ };
164
+
165
+ /* ── Scroll listeners: dismiss when trigger scrolls out of view ── */
166
+ const onScroll = () => {
167
+ if (!triggerRef.value) {
168
+ hideImmediate();
169
+ return;
170
+ }
171
+ const rect = triggerRef.value.getBoundingClientRect();
172
+ const inView =
173
+ rect.bottom > 0 &&
174
+ rect.top < window.innerHeight &&
175
+ rect.right > 0 &&
176
+ rect.left < window.innerWidth;
177
+ if (!inView) {
178
+ hideImmediate();
179
+ }
180
+ };
181
+
182
+ const addScrollListeners = () => {
183
+ window.addEventListener('scroll', onScroll, true);
184
+ window.addEventListener('resize', onScroll, true);
185
+ };
186
+
187
+ const removeScrollListeners = () => {
188
+ window.removeEventListener('scroll', onScroll, true);
189
+ window.removeEventListener('resize', onScroll, true);
190
+ };
191
+
192
+ onBeforeUnmount(() => {
193
+ hideImmediate();
194
+ });
195
+
81
196
  return {
82
197
  visible,
83
- tooltipStyle,
198
+ positionData,
84
199
  triggerRef,
85
200
  tooltipRef,
201
+ arrowRef,
202
+ tooltipId,
86
203
  show,
87
204
  hide,
205
+ hideImmediate,
206
+ initPopper,
88
207
  };
89
208
  }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @description commandPalette composition barrel export
3
+ * @author kine-design
4
+ * @date 2026/5/22
5
+ * @version v1.0.0
6
+ *
7
+ * 江湖的业务千篇一律,复杂的代码好几百行。
8
+ */
9
+ export { useCommandPalette } from './useCommandPalette';
10
+ export type { UseCommandPaletteReturn } from './useCommandPalette';
11
+ export type { Command } from './types';
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @description CommandPalette command type definitions
3
+ * @author kine-design
4
+ * @date 2026/5/22
5
+ * @version v1.0.0
6
+ *
7
+ * 江湖的业务千篇一律,复杂的代码好几百行。
8
+ */
9
+ import type { VNode } from 'vue';
10
+
11
+ /**
12
+ * A single command registered in the palette.
13
+ */
14
+ export interface Command {
15
+ /** Unique identifier */
16
+ id: string;
17
+ /** Display label (also used for search) */
18
+ label: string;
19
+ /** Optional icon render function */
20
+ icon?: () => VNode;
21
+ /** Additional search terms beyond the label */
22
+ keywords?: string[];
23
+ /** Grouping category (e.g. "Navigation", "Actions") */
24
+ group?: string;
25
+ /** Shortcut hint text displayed on the right side of the item */
26
+ shortcut?: string;
27
+ /** Callback executed when the command is selected */
28
+ action: () => void;
29
+ }