@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.
- package/components/base/image/__tests__/useImage.test.ts +61 -0
- package/components/base/image/api.ts +2 -2
- package/components/base/image/index.ts +2 -2
- package/components/base/image/props.d.ts +15 -4
- package/components/base/image/useImage.ts +27 -16
- package/components/base/tooltip/api.ts +3 -2
- package/components/base/tooltip/index.ts +3 -2
- package/components/base/tooltip/props.d.ts +12 -6
- package/components/base/tooltip/useTooltip.ts +175 -56
- package/compositions/commandPalette/index.ts +11 -0
- package/compositions/commandPalette/types.ts +29 -0
- package/compositions/commandPalette/useCommandPalette.ts +135 -0
- package/compositions/contextMenu/index.ts +10 -0
- package/compositions/contextMenu/types.ts +21 -0
- package/compositions/contextMenu/useContextMenu.ts +101 -0
- package/compositions/editor/index.ts +18 -0
- package/compositions/editor/types.ts +147 -0
- package/compositions/editor/useEditor.ts +224 -0
- package/compositions/index.ts +15 -0
- package/compositions/overlay/index.ts +14 -0
- package/compositions/overlay/useOverlayStack.ts +146 -0
- package/compositions/peekView/index.ts +10 -0
- package/compositions/peekView/usePeekView.ts +99 -0
- package/compositions/theme/index.ts +17 -0
- package/compositions/theme/presets/compact.ts +117 -0
- package/compositions/theme/presets/dark.ts +113 -0
- package/compositions/theme/presets/index.ts +11 -0
- package/compositions/theme/presets/light.ts +113 -0
- package/compositions/theme/types.ts +46 -0
- package/compositions/theme/useTheme.ts +269 -0
- package/compositions/toast/index.ts +10 -0
- package/compositions/toast/useToast.ts +176 -0
- package/compositions/tooltip/index.ts +9 -0
- package/compositions/tooltip/useTooltip.ts +10 -0
- package/dist/components/base/image/index.d.ts +2 -2
- package/dist/components/base/image/useImage.d.ts +7 -0
- package/dist/components/base/tooltip/index.d.ts +1 -0
- package/dist/components/base/tooltip/useTooltip.d.ts +20 -17
- package/dist/compositions/commandPalette/index.d.ts +11 -0
- package/dist/compositions/commandPalette/types.d.ts +20 -0
- package/dist/compositions/commandPalette/useCommandPalette.d.ts +32 -0
- package/dist/compositions/contextMenu/index.d.ts +10 -0
- package/dist/compositions/contextMenu/types.d.ts +12 -0
- package/dist/compositions/contextMenu/useContextMenu.d.ts +52 -0
- package/dist/compositions/editor/index.d.ts +10 -0
- package/dist/compositions/editor/types.d.ts +132 -0
- package/dist/compositions/editor/useEditor.d.ts +13 -0
- package/dist/compositions/index.d.ts +15 -0
- package/dist/compositions/overlay/index.d.ts +10 -0
- package/dist/compositions/overlay/useOverlayStack.d.ts +188 -0
- package/dist/compositions/peekView/index.d.ts +10 -0
- package/dist/compositions/peekView/usePeekView.d.ts +16 -0
- package/dist/compositions/theme/index.d.ts +11 -0
- package/dist/compositions/theme/presets/compact.d.ts +6 -0
- package/dist/compositions/theme/presets/dark.d.ts +2 -0
- package/dist/compositions/theme/presets/index.d.ts +11 -0
- package/dist/compositions/theme/presets/light.d.ts +2 -0
- package/dist/compositions/theme/types.d.ts +41 -0
- package/dist/compositions/theme/useTheme.d.ts +167 -0
- package/dist/compositions/toast/index.d.ts +10 -0
- package/dist/compositions/toast/useToast.d.ts +71 -0
- package/dist/compositions/tooltip/index.d.ts +9 -0
- package/dist/compositions/tooltip/useTooltip.d.ts +10 -0
- package/dist/core.js +836 -56
- package/dist/index.d.ts +1 -0
- package/index.ts +2 -0
- 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<
|
|
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
|
-
*
|
|
50
|
+
* @description 开启预览的媒体列表,传入后点击可全屏预览。
|
|
51
|
+
* 支持纯字符串(向后兼容,视为图片)或 { src, type } 对象。
|
|
52
|
+
* @type (string | PreviewItem)[]
|
|
42
53
|
* @default []
|
|
43
54
|
*/
|
|
44
|
-
previewSrcList?:
|
|
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
|
-
|
|
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.
|
|
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
|
|
2
|
+
* @description tooltip runtime props
|
|
3
3
|
* @author 阿怪
|
|
4
4
|
* @date 2026/2/25
|
|
5
|
-
* @version
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
5
|
+
* @version v2.0.0
|
|
6
6
|
*
|
|
7
7
|
* 江湖的业务千篇一律,复杂的代码好几百行。
|
|
8
8
|
*/
|
|
9
|
-
import { ref,
|
|
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
|
-
|
|
13
|
-
position: string;
|
|
14
|
-
left: string;
|
|
15
|
-
top: string;
|
|
16
|
-
zIndex: string;
|
|
17
|
-
};
|
|
14
|
+
const HIDE_DELAY = 100;
|
|
18
15
|
|
|
19
|
-
/**
|
|
20
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
70
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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
|
+
}
|