@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.
Files changed (60) hide show
  1. package/components/base/tooltip/api.ts +3 -2
  2. package/components/base/tooltip/index.ts +3 -2
  3. package/components/base/tooltip/props.d.ts +12 -6
  4. package/components/base/tooltip/useTooltip.ts +175 -56
  5. package/compositions/commandPalette/index.ts +11 -0
  6. package/compositions/commandPalette/types.ts +29 -0
  7. package/compositions/commandPalette/useCommandPalette.ts +135 -0
  8. package/compositions/contextMenu/index.ts +10 -0
  9. package/compositions/contextMenu/types.ts +21 -0
  10. package/compositions/contextMenu/useContextMenu.ts +101 -0
  11. package/compositions/editor/index.ts +18 -0
  12. package/compositions/editor/types.ts +147 -0
  13. package/compositions/editor/useEditor.ts +224 -0
  14. package/compositions/index.ts +15 -0
  15. package/compositions/overlay/index.ts +14 -0
  16. package/compositions/overlay/useOverlayStack.ts +146 -0
  17. package/compositions/peekView/index.ts +10 -0
  18. package/compositions/peekView/usePeekView.ts +99 -0
  19. package/compositions/theme/index.ts +17 -0
  20. package/compositions/theme/presets/compact.ts +117 -0
  21. package/compositions/theme/presets/dark.ts +113 -0
  22. package/compositions/theme/presets/index.ts +11 -0
  23. package/compositions/theme/presets/light.ts +113 -0
  24. package/compositions/theme/types.ts +46 -0
  25. package/compositions/theme/useTheme.ts +269 -0
  26. package/compositions/toast/index.ts +10 -0
  27. package/compositions/toast/useToast.ts +176 -0
  28. package/compositions/tooltip/index.ts +9 -0
  29. package/compositions/tooltip/useTooltip.ts +10 -0
  30. package/dist/components/base/tooltip/index.d.ts +1 -0
  31. package/dist/components/base/tooltip/useTooltip.d.ts +20 -17
  32. package/dist/compositions/commandPalette/index.d.ts +11 -0
  33. package/dist/compositions/commandPalette/types.d.ts +20 -0
  34. package/dist/compositions/commandPalette/useCommandPalette.d.ts +32 -0
  35. package/dist/compositions/contextMenu/index.d.ts +10 -0
  36. package/dist/compositions/contextMenu/types.d.ts +12 -0
  37. package/dist/compositions/contextMenu/useContextMenu.d.ts +52 -0
  38. package/dist/compositions/editor/index.d.ts +10 -0
  39. package/dist/compositions/editor/types.d.ts +132 -0
  40. package/dist/compositions/editor/useEditor.d.ts +13 -0
  41. package/dist/compositions/index.d.ts +15 -0
  42. package/dist/compositions/overlay/index.d.ts +10 -0
  43. package/dist/compositions/overlay/useOverlayStack.d.ts +188 -0
  44. package/dist/compositions/peekView/index.d.ts +10 -0
  45. package/dist/compositions/peekView/usePeekView.d.ts +16 -0
  46. package/dist/compositions/theme/index.d.ts +11 -0
  47. package/dist/compositions/theme/presets/compact.d.ts +6 -0
  48. package/dist/compositions/theme/presets/dark.d.ts +2 -0
  49. package/dist/compositions/theme/presets/index.d.ts +11 -0
  50. package/dist/compositions/theme/presets/light.d.ts +2 -0
  51. package/dist/compositions/theme/types.d.ts +41 -0
  52. package/dist/compositions/theme/useTheme.d.ts +167 -0
  53. package/dist/compositions/toast/index.d.ts +10 -0
  54. package/dist/compositions/toast/useToast.d.ts +71 -0
  55. package/dist/compositions/tooltip/index.d.ts +9 -0
  56. package/dist/compositions/tooltip/useTooltip.d.ts +10 -0
  57. package/dist/core.js +811 -48
  58. package/dist/index.d.ts +1 -0
  59. package/index.ts +2 -0
  60. package/package.json +13 -2
@@ -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
+ }
@@ -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,10 @@
1
+ /**
2
+ * @description context menu composables barrel export
3
+ * @author kine-design
4
+ * @date 2026/5/22
5
+ * @version v1.0.0
6
+ *
7
+ * 江湖的业务千篇一律,复杂的代码好几百行。
8
+ */
9
+ export { useContextMenu } from './useContextMenu';
10
+ export type { MenuItem } from './types';
@@ -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';