@kine-design/core 0.0.1-beta.11 → 0.0.1-beta.12
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/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/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 +811 -48
- package/dist/index.d.ts +1 -0
- package/index.ts +2 -0
- package/package.json +13 -2
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description CommandPalette composable — global search + command execution
|
|
3
|
+
* @author kine-design
|
|
4
|
+
* @date 2026/5/22
|
|
5
|
+
* @version v1.0.0
|
|
6
|
+
*
|
|
7
|
+
* 江湖的业务千篇一律,复杂的代码好几百行。
|
|
8
|
+
*
|
|
9
|
+
* Manages open/close state, command registration, query filtering, and execution.
|
|
10
|
+
* Does NOT participate in overlayStack — fixed z-index 9500.
|
|
11
|
+
*/
|
|
12
|
+
import { ref, computed, type Ref, type ComputedRef } from 'vue';
|
|
13
|
+
import type { Command } from './types';
|
|
14
|
+
|
|
15
|
+
export interface UseCommandPaletteReturn {
|
|
16
|
+
/** Whether the palette is open */
|
|
17
|
+
isOpen: Ref<boolean>;
|
|
18
|
+
/** Current search query */
|
|
19
|
+
query: Ref<string>;
|
|
20
|
+
/** Index of the currently highlighted item */
|
|
21
|
+
activeIndex: Ref<number>;
|
|
22
|
+
/** All registered commands */
|
|
23
|
+
commands: Ref<Command[]>;
|
|
24
|
+
/** Filtered commands based on query */
|
|
25
|
+
filtered: ComputedRef<Command[]>;
|
|
26
|
+
/** Open the palette */
|
|
27
|
+
open: () => void;
|
|
28
|
+
/** Close the palette */
|
|
29
|
+
close: () => void;
|
|
30
|
+
/** Toggle open/close */
|
|
31
|
+
toggle: () => void;
|
|
32
|
+
/** Register a command */
|
|
33
|
+
registerCommand: (cmd: Command) => void;
|
|
34
|
+
/** Unregister a command by id */
|
|
35
|
+
unregisterCommand: (id: string) => void;
|
|
36
|
+
/** Execute the command at a given index in the filtered list, then close */
|
|
37
|
+
execute: (index: number) => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* CommandPalette composable
|
|
42
|
+
*
|
|
43
|
+
* Provides reactive state for opening, searching, and executing commands.
|
|
44
|
+
*/
|
|
45
|
+
export function useCommandPalette(): UseCommandPaletteReturn {
|
|
46
|
+
const isOpen = ref(false);
|
|
47
|
+
const query = ref('');
|
|
48
|
+
const activeIndex = ref(0);
|
|
49
|
+
const commands = ref<Command[]>([]);
|
|
50
|
+
|
|
51
|
+
/** element that had focus before the palette opened */
|
|
52
|
+
let previousActiveElement: Element | null = null;
|
|
53
|
+
|
|
54
|
+
const open = () => {
|
|
55
|
+
previousActiveElement = document.activeElement;
|
|
56
|
+
isOpen.value = true;
|
|
57
|
+
query.value = '';
|
|
58
|
+
activeIndex.value = 0;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const close = () => {
|
|
62
|
+
isOpen.value = false;
|
|
63
|
+
query.value = '';
|
|
64
|
+
activeIndex.value = 0;
|
|
65
|
+
// restore focus to the previously focused element
|
|
66
|
+
if (previousActiveElement && previousActiveElement instanceof HTMLElement) {
|
|
67
|
+
previousActiveElement.focus();
|
|
68
|
+
}
|
|
69
|
+
previousActiveElement = null;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const toggle = () => {
|
|
73
|
+
if (isOpen.value) {
|
|
74
|
+
close();
|
|
75
|
+
} else {
|
|
76
|
+
open();
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const registerCommand = (cmd: Command) => {
|
|
81
|
+
// prevent duplicates
|
|
82
|
+
const exists = commands.value.some(c => c.id === cmd.id);
|
|
83
|
+
if (!exists) {
|
|
84
|
+
commands.value.push(cmd);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const unregisterCommand = (id: string) => {
|
|
89
|
+
const idx = commands.value.findIndex(c => c.id === id);
|
|
90
|
+
if (idx !== -1) {
|
|
91
|
+
commands.value.splice(idx, 1);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Simple case-insensitive substring match on label + keywords.
|
|
97
|
+
*/
|
|
98
|
+
const filtered = computed<Command[]>(() => {
|
|
99
|
+
const q = query.value.trim().toLowerCase();
|
|
100
|
+
if (!q) {
|
|
101
|
+
return commands.value;
|
|
102
|
+
}
|
|
103
|
+
return commands.value.filter(cmd => {
|
|
104
|
+
if (cmd.label.toLowerCase().includes(q)) {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
if (cmd.keywords) {
|
|
108
|
+
return cmd.keywords.some(kw => kw.toLowerCase().includes(q));
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const execute = (index: number) => {
|
|
115
|
+
const cmd = filtered.value[index];
|
|
116
|
+
if (cmd) {
|
|
117
|
+
close();
|
|
118
|
+
cmd.action();
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
isOpen,
|
|
124
|
+
query,
|
|
125
|
+
activeIndex,
|
|
126
|
+
commands,
|
|
127
|
+
filtered,
|
|
128
|
+
open,
|
|
129
|
+
close,
|
|
130
|
+
toggle,
|
|
131
|
+
registerCommand,
|
|
132
|
+
unregisterCommand,
|
|
133
|
+
execute,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description context menu types
|
|
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
|
+
export interface MenuItem {
|
|
12
|
+
key: string;
|
|
13
|
+
label: string;
|
|
14
|
+
icon?: () => VNode;
|
|
15
|
+
action: () => void;
|
|
16
|
+
visible?: boolean;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
danger?: boolean;
|
|
19
|
+
children?: MenuItem[];
|
|
20
|
+
separator?: boolean;
|
|
21
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description context menu composable — manages open/close state, positioning, and keyboard navigation
|
|
3
|
+
* @author kine-design
|
|
4
|
+
* @date 2026/5/22
|
|
5
|
+
* @version v1.0.0
|
|
6
|
+
*
|
|
7
|
+
* 江湖的业务千篇一律,复杂的代码好几百行。
|
|
8
|
+
*/
|
|
9
|
+
import { ref, computed } from 'vue';
|
|
10
|
+
import type { MenuItem } from './types';
|
|
11
|
+
|
|
12
|
+
export function useContextMenu() {
|
|
13
|
+
const isOpen = ref(false);
|
|
14
|
+
const position = ref({ x: 0, y: 0 });
|
|
15
|
+
const items = ref<MenuItem[]>([]);
|
|
16
|
+
const activeIndex = ref(-1);
|
|
17
|
+
|
|
18
|
+
const visibleItems = computed(() =>
|
|
19
|
+
items.value.filter(item => item.visible !== false),
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const open = (event: MouseEvent, menuItems: MenuItem[]) => {
|
|
23
|
+
event.preventDefault();
|
|
24
|
+
event.stopPropagation();
|
|
25
|
+
|
|
26
|
+
items.value = menuItems;
|
|
27
|
+
activeIndex.value = -1;
|
|
28
|
+
|
|
29
|
+
// Calculate position with viewport edge flipping
|
|
30
|
+
const x = event.clientX;
|
|
31
|
+
const y = event.clientY;
|
|
32
|
+
|
|
33
|
+
position.value = { x, y };
|
|
34
|
+
isOpen.value = true;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const close = () => {
|
|
38
|
+
isOpen.value = false;
|
|
39
|
+
activeIndex.value = -1;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const moveUp = () => {
|
|
43
|
+
const visible = visibleItems.value;
|
|
44
|
+
if (visible.length === 0) return;
|
|
45
|
+
|
|
46
|
+
let next = activeIndex.value - 1;
|
|
47
|
+
// Skip separators
|
|
48
|
+
while (next >= 0 && visible[next]?.separator) {
|
|
49
|
+
next--;
|
|
50
|
+
}
|
|
51
|
+
if (next < 0) {
|
|
52
|
+
// Wrap to last non-separator item
|
|
53
|
+
next = visible.length - 1;
|
|
54
|
+
while (next >= 0 && visible[next]?.separator) {
|
|
55
|
+
next--;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
activeIndex.value = next;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const moveDown = () => {
|
|
62
|
+
const visible = visibleItems.value;
|
|
63
|
+
if (visible.length === 0) return;
|
|
64
|
+
|
|
65
|
+
let next = activeIndex.value + 1;
|
|
66
|
+
// Skip separators
|
|
67
|
+
while (next < visible.length && visible[next]?.separator) {
|
|
68
|
+
next++;
|
|
69
|
+
}
|
|
70
|
+
if (next >= visible.length) {
|
|
71
|
+
// Wrap to first non-separator item
|
|
72
|
+
next = 0;
|
|
73
|
+
while (next < visible.length && visible[next]?.separator) {
|
|
74
|
+
next++;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
activeIndex.value = next;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const executeCurrent = () => {
|
|
81
|
+
const visible = visibleItems.value;
|
|
82
|
+
const item = visible[activeIndex.value];
|
|
83
|
+
if (item && !item.disabled && !item.separator) {
|
|
84
|
+
item.action();
|
|
85
|
+
close();
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
isOpen,
|
|
91
|
+
position,
|
|
92
|
+
items,
|
|
93
|
+
activeIndex,
|
|
94
|
+
visibleItems,
|
|
95
|
+
open,
|
|
96
|
+
close,
|
|
97
|
+
moveUp,
|
|
98
|
+
moveDown,
|
|
99
|
+
executeCurrent,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description Editor composition barrel export
|
|
3
|
+
* @author kine-design
|
|
4
|
+
* @date 2026/5/22
|
|
5
|
+
* @version v1.0.0
|
|
6
|
+
*
|
|
7
|
+
* Headless editor composable — provides logic, no rendering.
|
|
8
|
+
*/
|
|
9
|
+
export { useEditor } from './useEditor';
|
|
10
|
+
export type {
|
|
11
|
+
UseEditorOptions,
|
|
12
|
+
EditorConfig,
|
|
13
|
+
EditorToolbarConfig,
|
|
14
|
+
EditorExtensionConfig,
|
|
15
|
+
EditorToolbarActions,
|
|
16
|
+
EditorActiveState,
|
|
17
|
+
UseEditorReturn,
|
|
18
|
+
} from './types';
|