@lokvis/ui-react 0.2.0 → 0.2.2
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/dist/components/AssetPanel.d.ts.map +1 -1
- package/dist/components/AssetPanel.js +14 -14
- package/dist/components/AssetPanel.js.map +1 -1
- package/dist/components/Canvas.d.ts +4 -4
- package/dist/components/Canvas.d.ts.map +1 -1
- package/dist/components/Canvas.js +8 -8
- package/dist/components/Canvas.js.map +1 -1
- package/dist/components/CommandPalette.d.ts +7 -7
- package/dist/components/CommandPalette.d.ts.map +1 -1
- package/dist/components/CommandPalette.js +10 -10
- package/dist/components/CommandPalette.js.map +1 -1
- package/dist/components/CompareSlider.d.ts +6 -6
- package/dist/components/CompareSlider.d.ts.map +1 -1
- package/dist/components/CompareSlider.js +7 -7
- package/dist/components/CompareSlider.js.map +1 -1
- package/dist/components/DownloadPanel.d.ts +3 -3
- package/dist/components/DownloadPanel.d.ts.map +1 -1
- package/dist/components/DownloadPanel.js +9 -9
- package/dist/components/DownloadPanel.js.map +1 -1
- package/dist/components/ErrorBanner.d.ts +2 -2
- package/dist/components/ErrorBanner.d.ts.map +1 -1
- package/dist/components/ErrorBanner.js +3 -3
- package/dist/components/ErrorBanner.js.map +1 -1
- package/dist/components/ExifPanel.d.ts +1 -1
- package/dist/components/ExifPanel.d.ts.map +1 -1
- package/dist/components/ExifPanel.js +2 -2
- package/dist/components/ExifPanel.js.map +1 -1
- package/dist/components/GlobalDropzone.d.ts +5 -5
- package/dist/components/GlobalDropzone.d.ts.map +1 -1
- package/dist/components/GlobalDropzone.js +6 -6
- package/dist/components/GlobalDropzone.js.map +1 -1
- package/dist/components/HistoryPanel.d.ts +2 -2
- package/dist/components/HistoryPanel.d.ts.map +1 -1
- package/dist/components/HistoryPanel.js +23 -23
- package/dist/components/HistoryPanel.js.map +1 -1
- package/dist/components/Inspector.d.ts.map +1 -1
- package/dist/components/Inspector.js +6 -6
- package/dist/components/Inspector.js.map +1 -1
- package/dist/components/ParamForm.d.ts.map +1 -1
- package/dist/components/ParamForm.js +7 -7
- package/dist/components/ParamForm.js.map +1 -1
- package/dist/components/PipelineBar.d.ts.map +1 -1
- package/dist/components/PipelineBar.js +13 -13
- package/dist/components/PipelineBar.js.map +1 -1
- package/dist/components/ProgressBar.d.ts +4 -4
- package/dist/components/ProgressBar.d.ts.map +1 -1
- package/dist/components/ProgressBar.js +8 -8
- package/dist/components/ProgressBar.js.map +1 -1
- package/dist/components/StatusBar.d.ts +3 -3
- package/dist/components/StatusBar.d.ts.map +1 -1
- package/dist/components/StatusBar.js +11 -11
- package/dist/components/StatusBar.js.map +1 -1
- package/dist/components/ThemeToggle.d.ts +4 -4
- package/dist/components/ThemeToggle.d.ts.map +1 -1
- package/dist/components/ThemeToggle.js +7 -7
- package/dist/components/ThemeToggle.js.map +1 -1
- package/dist/components/Toolbar.d.ts.map +1 -1
- package/dist/components/Toolbar.js +2 -2
- package/dist/components/Toolbar.js.map +1 -1
- package/dist/components/WorkflowEditor.d.ts +6 -6
- package/dist/components/WorkflowEditor.d.ts.map +1 -1
- package/dist/components/WorkflowEditor.js +25 -25
- package/dist/components/WorkflowEditor.js.map +1 -1
- package/dist/components/WorkflowTemplates.d.ts +2 -2
- package/dist/components/WorkflowTemplates.d.ts.map +1 -1
- package/dist/components/WorkflowTemplates.js +3 -3
- package/dist/components/WorkflowTemplates.js.map +1 -1
- package/dist/components/Workspace.d.ts +22 -22
- package/dist/components/Workspace.d.ts.map +1 -1
- package/dist/components/Workspace.js +30 -30
- package/dist/components/Workspace.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
- package/src/components/AssetPanel.tsx +263 -263
- package/src/components/Canvas.tsx +174 -174
- package/src/components/CommandPalette.tsx +242 -242
- package/src/components/CompareSlider.tsx +163 -163
- package/src/components/DownloadPanel.tsx +203 -203
- package/src/components/ErrorBanner.tsx +64 -64
- package/src/components/ExifPanel.tsx +154 -154
- package/src/components/GlobalDropzone.tsx +170 -170
- package/src/components/HistoryPanel.tsx +219 -219
- package/src/components/Inspector.tsx +136 -136
- package/src/components/ParamForm.tsx +136 -136
- package/src/components/PipelineBar.tsx +98 -98
- package/src/components/ProgressBar.tsx +78 -78
- package/src/components/StatusBar.tsx +148 -148
- package/src/components/ThemeToggle.tsx +98 -98
- package/src/components/Toolbar.tsx +65 -65
- package/src/components/WorkflowEditor.tsx +321 -321
- package/src/components/WorkflowTemplates.tsx +77 -77
- package/src/components/Workspace.tsx +223 -223
- package/src/css-modules.d.ts +3 -0
- package/src/index.ts +4 -0
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* CommandPalette - ⌘K 工具选择器(W9.2)
|
|
3
3
|
*
|
|
4
4
|
* Command Palette 风格的快捷工具入口:
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* - ⌘K / Ctrl+K 打开
|
|
6
|
+
* - ESC 关闭
|
|
7
|
+
* - 输入关键词过滤能力列表
|
|
8
|
+
* - ↑↓ 选择,Enter 添加到工作流,or 鼠标点击
|
|
9
9
|
*
|
|
10
10
|
* 数据源:`store.capabilities`,与 Inspector 右侧能力列表同源。
|
|
11
11
|
* 选中后调用 `store.addNode(cap.name)` 添加到工作流,并关闭面板。
|
|
@@ -19,238 +19,238 @@ import type { Capability } from '@lokvis/schema';
|
|
|
19
19
|
import { useWorkspaceStore } from '../store/index.js';
|
|
20
20
|
|
|
21
21
|
export interface CommandPaletteProps {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
/** 是否打开(受控) */
|
|
23
|
+
open: boolean;
|
|
24
|
+
/** 关闭回调 */
|
|
25
|
+
onClose: () => void;
|
|
26
|
+
className?: string;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export function CommandPalette({ open, onClose, className = '' }: CommandPaletteProps) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
const capabilities = useWorkspaceStore((s) => s.capabilities);
|
|
31
|
+
const addNode = useWorkspaceStore((s) => s.addNode);
|
|
32
|
+
const undo = useWorkspaceStore((s) => s.undo);
|
|
33
|
+
const redo = useWorkspaceStore((s) => s.redo);
|
|
34
|
+
const clearWorkflow = useWorkspaceStore((s) => s.clearWorkflow);
|
|
35
|
+
const running = useWorkspaceStore((s) => s.running);
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
const [query, setQuery] = React.useState('');
|
|
38
|
+
const [activeIndex, setActiveIndex] = React.useState(0);
|
|
39
|
+
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
40
|
+
const listRef = React.useRef<HTMLUListElement>(null);
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
// 命令项:capability 添加 + 工作流操作
|
|
43
|
+
type Command =
|
|
44
|
+
| { kind: 'capability'; cap: Capability }
|
|
45
|
+
| { kind: 'action'; id: string; label: string; description: string; run: () => void; disabled?: boolean };
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
47
|
+
const commands: Command[] = React.useMemo(() => {
|
|
48
|
+
const caps = capabilities.map((cap) => ({ kind: 'capability' as const, cap }));
|
|
49
|
+
const actions: Command[] = [
|
|
50
|
+
{
|
|
51
|
+
kind: 'action',
|
|
52
|
+
id: 'undo',
|
|
53
|
+
label: 'Undo',
|
|
54
|
+
description: '回退到上一步历史',
|
|
55
|
+
run: () => void undo(),
|
|
56
|
+
disabled: false,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
kind: 'action',
|
|
60
|
+
id: 'redo',
|
|
61
|
+
label: 'Redo',
|
|
62
|
+
description: '重做一步',
|
|
63
|
+
run: () => void redo(),
|
|
64
|
+
disabled: false,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
kind: 'action',
|
|
68
|
+
id: 'clear',
|
|
69
|
+
label: 'Clear Workflow',
|
|
70
|
+
description: '清空当前工作流节点',
|
|
71
|
+
run: () => clearWorkflow(),
|
|
72
|
+
disabled: running,
|
|
73
|
+
},
|
|
74
|
+
];
|
|
75
|
+
return [...caps, ...actions];
|
|
76
|
+
}, [capabilities, undo, redo, clearWorkflow, running]);
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
78
|
+
const filtered = React.useMemo(() => {
|
|
79
|
+
const q = query.trim().toLowerCase();
|
|
80
|
+
if (!q) return commands;
|
|
81
|
+
return commands.filter((cmd) => {
|
|
82
|
+
if (cmd.kind === 'capability') {
|
|
83
|
+
return (
|
|
84
|
+
cmd.cap.name.toLowerCase().includes(q) ||
|
|
85
|
+
cmd.cap.description.toLowerCase().includes(q)
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
return cmd.label.toLowerCase().includes(q) || cmd.description.toLowerCase().includes(q);
|
|
89
|
+
});
|
|
90
|
+
}, [commands, query]);
|
|
91
91
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
92
|
+
// 打开时重置 query + activeIndex + 聚焦输入
|
|
93
|
+
React.useEffect(() => {
|
|
94
|
+
if (!open) return;
|
|
95
|
+
setQuery('');
|
|
96
|
+
setActiveIndex(0);
|
|
97
|
+
// Dialog 聚焦首个可聚焦元素(input)后,input ref 才有值
|
|
98
|
+
// 用 timer 句柄 + cleanup 避免卸载后 setState 警告
|
|
99
|
+
const t = setTimeout(() => inputRef.current?.focus(), 0);
|
|
100
|
+
return () => clearTimeout(t);
|
|
101
|
+
}, [open]);
|
|
102
102
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
103
|
+
// filtered 变化时校正 activeIndex 越界
|
|
104
|
+
React.useEffect(() => {
|
|
105
|
+
if (activeIndex >= filtered.length) {
|
|
106
|
+
setActiveIndex(filtered.length === 0 ? 0 : filtered.length - 1);
|
|
107
|
+
}
|
|
108
|
+
}, [filtered.length, activeIndex]);
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
110
|
+
// 滚动 active 项进入视区
|
|
111
|
+
React.useEffect(() => {
|
|
112
|
+
if (!open || !listRef.current) return;
|
|
113
|
+
const active = listRef.current.querySelector<HTMLElement>(`[data-index="${activeIndex}"]`);
|
|
114
|
+
active?.scrollIntoView({ block: 'nearest' });
|
|
115
|
+
}, [activeIndex, open]);
|
|
116
116
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
117
|
+
function handleSelect(cmd: Command) {
|
|
118
|
+
if (cmd.kind === 'capability') {
|
|
119
|
+
addNode(cmd.cap.name);
|
|
120
|
+
} else {
|
|
121
|
+
if (cmd.disabled) return;
|
|
122
|
+
cmd.run();
|
|
123
|
+
}
|
|
124
|
+
onClose();
|
|
125
|
+
}
|
|
126
126
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
127
|
+
function handleKeyDown(e: React.KeyboardEvent) {
|
|
128
|
+
if (e.key === 'ArrowDown') {
|
|
129
|
+
e.preventDefault();
|
|
130
|
+
setActiveIndex((i) => (i + 1) % Math.max(1, filtered.length));
|
|
131
|
+
} else if (e.key === 'ArrowUp') {
|
|
132
|
+
e.preventDefault();
|
|
133
|
+
setActiveIndex((i) => (i - 1 + filtered.length) % Math.max(1, filtered.length));
|
|
134
|
+
} else if (e.key === 'Enter') {
|
|
135
|
+
e.preventDefault();
|
|
136
|
+
const cmd = filtered[activeIndex];
|
|
137
|
+
if (cmd) handleSelect(cmd);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
140
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
141
|
+
return (
|
|
142
|
+
<Dialog open={open} onClose={onClose} size="md" closeOnOverlay>
|
|
143
|
+
<div className={`-mx-5 -my-5 ${className}`}>
|
|
144
|
+
{/* 搜索输入 */}
|
|
145
|
+
<div className="flex items-center gap-2 border-b border-[var(--lokvis-border)] px-4 py-3">
|
|
146
|
+
<Icon size={16} className="text-[var(--lokvis-fg-subtle)]">
|
|
147
|
+
<circle cx="11" cy="11" r="8" />
|
|
148
|
+
<path d="m21 21-4.3-4.3" />
|
|
149
|
+
</Icon>
|
|
150
|
+
<input
|
|
151
|
+
ref={inputRef}
|
|
152
|
+
type="text"
|
|
153
|
+
value={query}
|
|
154
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
155
|
+
onKeyDown={handleKeyDown}
|
|
156
|
+
placeholder="Search commands or capabilities..."
|
|
157
|
+
aria-label="Search commands"
|
|
158
|
+
className="flex-1 bg-transparent text-sm text-[var(--lokvis-fg)] placeholder:text-[var(--lokvis-fg-subtle)] focus:outline-none"
|
|
159
|
+
autoComplete="off"
|
|
160
|
+
spellCheck={false}
|
|
161
|
+
/>
|
|
162
|
+
<kbd className="rounded border border-[var(--lokvis-border)] px-1.5 py-0.5 text-[9px] font-medium text-[var(--lokvis-fg-subtle)]">
|
|
163
|
+
ESC
|
|
164
|
+
</kbd>
|
|
165
|
+
</div>
|
|
166
166
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
167
|
+
{/* 命令列表 */}
|
|
168
|
+
<ul
|
|
169
|
+
ref={listRef}
|
|
170
|
+
className="max-h-80 overflow-y-auto py-1"
|
|
171
|
+
role="listbox"
|
|
172
|
+
aria-label="Available commands"
|
|
173
|
+
>
|
|
174
|
+
{filtered.length === 0 ? (
|
|
175
|
+
<li className="px-4 py-6 text-center text-xs text-[var(--lokvis-fg-subtle)]">
|
|
176
|
+
No matching commands
|
|
177
|
+
</li>
|
|
178
|
+
) : (
|
|
179
|
+
filtered.map((cmd, i) => {
|
|
180
|
+
const isActive = i === activeIndex;
|
|
181
|
+
const isCap = cmd.kind === 'capability';
|
|
182
|
+
const primary = isCap ? cmd.cap.name : cmd.label;
|
|
183
|
+
const secondary = isCap ? cmd.cap.description : cmd.description;
|
|
184
|
+
const disabled = !isCap && cmd.disabled;
|
|
185
|
+
return (
|
|
186
|
+
<li
|
|
187
|
+
key={isCap ? `cap:${cmd.cap.name}` : `act:${cmd.id}`}
|
|
188
|
+
data-index={i}
|
|
189
|
+
role="option"
|
|
190
|
+
aria-selected={isActive}
|
|
191
|
+
aria-disabled={disabled}
|
|
192
|
+
>
|
|
193
|
+
<button
|
|
194
|
+
type="button"
|
|
195
|
+
onMouseEnter={() => setActiveIndex(i)}
|
|
196
|
+
onClick={() => !disabled && handleSelect(cmd)}
|
|
197
|
+
disabled={disabled}
|
|
198
|
+
className={`flex w-full items-center gap-3 px-4 py-2 text-left transition-colors ${
|
|
199
|
+
isActive && !disabled
|
|
200
|
+
? 'bg-[var(--lokvis-primary)]/10'
|
|
201
|
+
: ''
|
|
202
|
+
} ${disabled ? 'cursor-not-allowed opacity-40' : 'hover:bg-[var(--lokvis-surface)]'}`}
|
|
203
|
+
>
|
|
204
|
+
<span
|
|
205
|
+
className={`flex h-6 w-6 shrink-0 items-center justify-center rounded ${
|
|
206
|
+
isCap
|
|
207
|
+
? 'bg-[var(--lokvis-surface-muted)] text-[var(--lokvis-fg-muted)]'
|
|
208
|
+
: 'bg-[var(--lokvis-primary)]/15 text-[var(--lokvis-primary)]'
|
|
209
|
+
}`}
|
|
210
|
+
>
|
|
211
|
+
<Icon size={12} strokeWidth={2}>
|
|
212
|
+
{isCap ? (
|
|
213
|
+
<path d="M12 4.5v15m7.5-7.5h-15" />
|
|
214
|
+
) : (
|
|
215
|
+
<path d="M13 2L3 14h7l-1 8 10-12h-7l1-8z" />
|
|
216
|
+
)}
|
|
217
|
+
</Icon>
|
|
218
|
+
</span>
|
|
219
|
+
<div className="min-w-0 flex-1">
|
|
220
|
+
<div className="flex items-center justify-between gap-2">
|
|
221
|
+
<span className="truncate text-xs font-medium text-[var(--lokvis-fg)]">
|
|
222
|
+
{primary}
|
|
223
|
+
</span>
|
|
224
|
+
{isCap && (
|
|
225
|
+
<span className="shrink-0 rounded px-1 py-0.5 text-[9px] font-medium uppercase bg-[var(--lokvis-surface-muted)] text-[var(--lokvis-fg-muted)]">
|
|
226
|
+
{cmd.cap.performance}
|
|
227
|
+
</span>
|
|
228
|
+
)}
|
|
229
|
+
</div>
|
|
230
|
+
<p className="truncate text-[10px] text-[var(--lokvis-fg-muted)]">
|
|
231
|
+
{secondary}
|
|
232
|
+
</p>
|
|
233
|
+
</div>
|
|
234
|
+
</button>
|
|
235
|
+
</li>
|
|
236
|
+
);
|
|
237
|
+
})
|
|
238
|
+
)}
|
|
239
|
+
</ul>
|
|
240
240
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
241
|
+
{/* 底部提示 */}
|
|
242
|
+
<div className="flex items-center justify-between border-t border-[var(--lokvis-border)] px-4 py-2 text-[10px] text-[var(--lokvis-fg-subtle)]">
|
|
243
|
+
<span className="flex items-center gap-1">
|
|
244
|
+
<kbd className="rounded border border-[var(--lokvis-border)] px-1">↑↓</kbd>
|
|
245
|
+
<span>navigate</span>
|
|
246
|
+
<kbd className="ml-2 rounded border border-[var(--lokvis-border)] px-1">↵</kbd>
|
|
247
|
+
<span>select</span>
|
|
248
|
+
</span>
|
|
249
|
+
<span>{filtered.length} command{filtered.length !== 1 ? 's' : ''}</span>
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
</Dialog>
|
|
253
|
+
);
|
|
254
254
|
}
|
|
255
255
|
|
|
256
256
|
/**
|
|
@@ -266,35 +266,35 @@ export function CommandPalette({ open, onClose, className = '' }: CommandPalette
|
|
|
266
266
|
* // 或禁用快捷键(仍可受控使用 open/setOpen):
|
|
267
267
|
* const [open, setOpen] = useCommandPalette({ enabled: false });
|
|
268
268
|
* return (
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
269
|
+
* <>
|
|
270
|
+
* <CommandPalette open={open} onClose={() => setOpen(false)} />
|
|
271
|
+
* </>
|
|
272
272
|
* );
|
|
273
273
|
* ```
|
|
274
274
|
*/
|
|
275
275
|
export interface UseCommandPaletteOptions {
|
|
276
|
-
|
|
277
|
-
|
|
276
|
+
/** 是否注册 ⌘K 全局快捷键(默认 true)。设为 false 时仍可受控使用 open/setOpen */
|
|
277
|
+
enabled?: boolean;
|
|
278
278
|
}
|
|
279
279
|
|
|
280
280
|
export function useCommandPalette(
|
|
281
|
-
|
|
281
|
+
options: UseCommandPaletteOptions = {}
|
|
282
282
|
): [boolean, React.Dispatch<React.SetStateAction<boolean>>] {
|
|
283
|
-
|
|
284
|
-
|
|
283
|
+
const { enabled = true } = options;
|
|
284
|
+
const [open, setOpen] = React.useState(false);
|
|
285
285
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
286
|
+
React.useEffect(() => {
|
|
287
|
+
if (!enabled) return;
|
|
288
|
+
const onKey = (e: KeyboardEvent) => {
|
|
289
|
+
// ⌘K (mac) / Ctrl+K (win/linux),忽略纯 K(避免拦截输入)
|
|
290
|
+
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
|
|
291
|
+
e.preventDefault();
|
|
292
|
+
setOpen((v) => !v);
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
document.addEventListener('keydown', onKey);
|
|
296
|
+
return () => document.removeEventListener('keydown', onKey);
|
|
297
|
+
}, [enabled]);
|
|
298
298
|
|
|
299
|
-
|
|
299
|
+
return [open, setOpen];
|
|
300
300
|
}
|