@dfosco/storyboard-react 4.0.0-beta.42 → 4.0.0-beta.44
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/package.json +4 -3
- package/src/CommandPalette/CommandPalette.jsx +918 -0
- package/src/CommandPalette/CreateDialog.jsx +219 -0
- package/src/CommandPalette/command-palette.css +66 -0
- package/src/Viewfinder.jsx +49 -11
- package/src/Viewfinder.module.css +43 -16
- package/src/canvas/CanvasPage.jsx +90 -4
- package/src/canvas/CanvasPage.module.css +25 -0
- package/src/canvas/MarqueeOverlay.jsx +20 -0
- package/src/canvas/PageSelector.jsx +20 -3
- package/src/canvas/componentIsolate.jsx +5 -5
- package/src/canvas/useCanvas.test.js +4 -4
- package/src/canvas/useMarqueeSelect.js +187 -0
- package/src/canvas/useMarqueeSelect.test.js +78 -0
- package/src/canvas/widgets/ComponentWidget.jsx +1 -1
- package/src/canvas/widgets/PrototypeEmbed.jsx +1 -1
- package/src/canvas/widgets/StoryWidget.jsx +1 -0
- package/src/canvas/widgets/embedInteraction.test.jsx +1 -1
- package/src/index.js +3 -0
- package/src/vite/data-plugin.js +2 -9
|
@@ -0,0 +1,918 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, useMemo } from 'react'
|
|
2
|
+
import 'react-cmdk/dist/cmdk.css'
|
|
3
|
+
import * as ReactCmdk from 'react-cmdk'
|
|
4
|
+
const CommandPalette = ReactCmdk.default || ReactCmdk
|
|
5
|
+
const { filterItems, getItemIndex } = ReactCmdk
|
|
6
|
+
import {
|
|
7
|
+
buildPrototypeIndex,
|
|
8
|
+
listStories,
|
|
9
|
+
getStoryData,
|
|
10
|
+
getActionsForMode,
|
|
11
|
+
executeAction,
|
|
12
|
+
getActionChildren,
|
|
13
|
+
getToolbarToolState,
|
|
14
|
+
getCurrentMode,
|
|
15
|
+
getRecent,
|
|
16
|
+
trackRecent,
|
|
17
|
+
getCommandPaletteConfig,
|
|
18
|
+
getToolbarConfig,
|
|
19
|
+
setTheme,
|
|
20
|
+
isExcludedByRoute,
|
|
21
|
+
} from '@dfosco/storyboard-core'
|
|
22
|
+
import CreateDialog from './CreateDialog.jsx'
|
|
23
|
+
import './command-palette.css'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Check if a tool should be hidden from the command palette on the current route.
|
|
27
|
+
* Uses the same pattern-matching logic as excludeRoutes.
|
|
28
|
+
*/
|
|
29
|
+
function isHiddenInPalette(tool, basePath) {
|
|
30
|
+
const val = tool.hideInCommandPalette
|
|
31
|
+
if (val === true) return true
|
|
32
|
+
if (!val || !Array.isArray(val) || val.length === 0) return false
|
|
33
|
+
if (typeof window === 'undefined') return false
|
|
34
|
+
let pathname = window.location.pathname
|
|
35
|
+
const base = (basePath || '/').replace(/\/+$/, '')
|
|
36
|
+
if (base && pathname.startsWith(base)) {
|
|
37
|
+
pathname = pathname.slice(base.length) || '/'
|
|
38
|
+
}
|
|
39
|
+
return val.some(pattern => new RegExp(pattern).test(pathname))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Build groups from commandPalette.sections config.
|
|
44
|
+
* Returns { groups, toolMenus } where toolMenus are entries with sub-pages.
|
|
45
|
+
*
|
|
46
|
+
* Section types:
|
|
47
|
+
* - Static items: { items: [...] }
|
|
48
|
+
* - Dynamic list: { source: "canvases"|"prototypes"|"stories"|"recent" }
|
|
49
|
+
* - Tool section: { source: "tools", toolIds: ["theme", "flows"] }
|
|
50
|
+
* - Tool-menu: { type: "tool-menu", options: [...] }
|
|
51
|
+
*/
|
|
52
|
+
function buildConfigSections(prefix, onNavigateToPage, onCreateAction) {
|
|
53
|
+
const config = getCommandPaletteConfig()
|
|
54
|
+
const sections = config?.sections || []
|
|
55
|
+
const groups = []
|
|
56
|
+
const toolMenus = []
|
|
57
|
+
const usedToolIds = new Set() // Track tools already listed by source:"tools" sections
|
|
58
|
+
const basePath = prefix || '/'
|
|
59
|
+
|
|
60
|
+
for (const section of sections) {
|
|
61
|
+
// Separator: id starts with "sep"
|
|
62
|
+
if (section.id?.startsWith('sep')) {
|
|
63
|
+
groups.push({ id: `cfg:${section.id}`, items: [{ id: `cfg:${section.id}:sep`, children: '', keywords: ['*'] }] })
|
|
64
|
+
continue
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (section.type === 'tool-menu') {
|
|
68
|
+
toolMenus.push(section)
|
|
69
|
+
continue
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (section.source) {
|
|
73
|
+
// Defer tool-subpages — needs usedToolIds from all other sections first
|
|
74
|
+
if (section.source === 'tool-subpages') continue
|
|
75
|
+
const result = buildDynamicSection(section, prefix, onNavigateToPage, onCreateAction)
|
|
76
|
+
if (result?.group) groups.push(result.group)
|
|
77
|
+
if (result?.subPages) toolMenus.push(...result.subPages)
|
|
78
|
+
if (result?.usedToolIds) result.usedToolIds.forEach(id => usedToolIds.add(id))
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (section.items && section.items.length > 0) {
|
|
83
|
+
groups.push({
|
|
84
|
+
heading: section.title,
|
|
85
|
+
id: `cfg:${section.id}`,
|
|
86
|
+
items: section.items.map((item, i) => {
|
|
87
|
+
const id = `cfg:${section.id}:${i}`
|
|
88
|
+
if (item.type === 'link') {
|
|
89
|
+
return {
|
|
90
|
+
id,
|
|
91
|
+
children: item.label,
|
|
92
|
+
keywords: item.keywords || [item.label],
|
|
93
|
+
onClick: () => {
|
|
94
|
+
const url = item.url?.startsWith('/') ? prefix + item.url : item.url
|
|
95
|
+
if (url) window.location.href = url
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (item.type === 'action') {
|
|
100
|
+
return {
|
|
101
|
+
id,
|
|
102
|
+
children: item.label,
|
|
103
|
+
keywords: item.keywords || [item.label],
|
|
104
|
+
onClick: () => { if (item.action) executeAction(item.action) },
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return { id, children: item.label, keywords: [item.label] }
|
|
108
|
+
}),
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Resolve tool-subpages sections (deferred — needs complete usedToolIds)
|
|
114
|
+
for (const section of sections) {
|
|
115
|
+
if (section.source !== 'tool-subpages') continue
|
|
116
|
+
|
|
117
|
+
// Scan all toolbar tools for sub-page candidates not already listed
|
|
118
|
+
const toolbarConfig = getToolbarConfig()
|
|
119
|
+
const allTools = toolbarConfig?.tools || {}
|
|
120
|
+
const mode = getCurrentMode() || 'default'
|
|
121
|
+
const actions = getActionsForMode(mode)
|
|
122
|
+
const remainingItems = []
|
|
123
|
+
|
|
124
|
+
for (const [toolId, tool] of Object.entries(allTools)) {
|
|
125
|
+
if (usedToolIds.has(toolId)) continue
|
|
126
|
+
const state = getToolbarToolState(toolId)
|
|
127
|
+
if (state === 'disabled' || state === 'hidden') continue
|
|
128
|
+
if (tool.disabled) continue
|
|
129
|
+
if (isHiddenInPalette(tool, basePath)) continue
|
|
130
|
+
|
|
131
|
+
const label = tool.label || tool.ariaLabel || toolId
|
|
132
|
+
const excluded = isExcludedByRoute(tool)
|
|
133
|
+
|
|
134
|
+
// Route-excluded tools show as disabled with hint
|
|
135
|
+
if (excluded) {
|
|
136
|
+
remainingItems.push({
|
|
137
|
+
id: `cfg:${section.id}:${toolId}`,
|
|
138
|
+
children: <><span>{label}</span><span style={{ marginLeft: 'auto', fontSize: '12px', opacity: 0.5 }}>Not available on this page</span></>,
|
|
139
|
+
keywords: [label, toolId].filter(Boolean),
|
|
140
|
+
showType: false,
|
|
141
|
+
disabled: true,
|
|
142
|
+
})
|
|
143
|
+
continue
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Tools with submenu children
|
|
147
|
+
if (tool.render === 'submenu' || tool.render === 'menu') {
|
|
148
|
+
const action = actions.find(a => a.toolKey === toolId)
|
|
149
|
+
if (action?.type === 'submenu') {
|
|
150
|
+
const children = getActionChildren(action.id)
|
|
151
|
+
if (children.length > 0) {
|
|
152
|
+
const pageId = `tool:${toolId}`
|
|
153
|
+
toolMenus.push({
|
|
154
|
+
id: pageId, label, title: label,
|
|
155
|
+
keywords: [label, toolId].filter(Boolean),
|
|
156
|
+
options: children.map(child => ({ label: child.label, execute: child.execute })),
|
|
157
|
+
})
|
|
158
|
+
remainingItems.push({
|
|
159
|
+
id: `cfg:${section.id}:${toolId}`,
|
|
160
|
+
children: label,
|
|
161
|
+
keywords: [label, toolId].filter(Boolean),
|
|
162
|
+
showType: false,
|
|
163
|
+
onClick: () => onNavigateToPage?.(pageId),
|
|
164
|
+
closeOnSelect: false,
|
|
165
|
+
})
|
|
166
|
+
continue
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// Declarative options
|
|
170
|
+
if (tool.options?.length > 0) {
|
|
171
|
+
const pageId = `tool:${toolId}`
|
|
172
|
+
toolMenus.push({
|
|
173
|
+
id: pageId, label, title: label,
|
|
174
|
+
keywords: [label, toolId].filter(Boolean),
|
|
175
|
+
options: tool.options.map(opt => ({ label: opt.label, toolHandler: tool.handler || `core:${toolId}`, value: opt.value })),
|
|
176
|
+
})
|
|
177
|
+
remainingItems.push({
|
|
178
|
+
id: `cfg:${section.id}:${toolId}`,
|
|
179
|
+
children: label,
|
|
180
|
+
keywords: [label, toolId].filter(Boolean),
|
|
181
|
+
showType: false,
|
|
182
|
+
onClick: () => onNavigateToPage?.(pageId),
|
|
183
|
+
closeOnSelect: false,
|
|
184
|
+
})
|
|
185
|
+
continue
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Inline actions (e.g. toggle-chrome for hide toolbars)
|
|
190
|
+
if (tool.inlineAction === 'toggle-chrome') {
|
|
191
|
+
remainingItems.push({
|
|
192
|
+
id: `cfg:${section.id}:${toolId}`,
|
|
193
|
+
children: label,
|
|
194
|
+
keywords: [label, toolId, 'hide', 'show', 'toolbar'].filter(Boolean),
|
|
195
|
+
showType: false,
|
|
196
|
+
onClick: () => {
|
|
197
|
+
document.documentElement.classList.toggle('storyboard-chrome-hidden')
|
|
198
|
+
},
|
|
199
|
+
})
|
|
200
|
+
continue
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Any remaining tools (all surfaces)
|
|
204
|
+
if (tool.render === 'link' && tool.url) {
|
|
205
|
+
remainingItems.push({
|
|
206
|
+
id: `cfg:${section.id}:${toolId}`,
|
|
207
|
+
children: label,
|
|
208
|
+
keywords: [label, toolId].filter(Boolean),
|
|
209
|
+
showType: false,
|
|
210
|
+
onClick: () => { window.location.href = tool.url },
|
|
211
|
+
})
|
|
212
|
+
} else {
|
|
213
|
+
// Menu tools: close palette and click the toolbar button to open the menu
|
|
214
|
+
if (tool.render === 'menu') {
|
|
215
|
+
const ariaLabel = tool.ariaLabel || tool.label || toolId
|
|
216
|
+
remainingItems.push({
|
|
217
|
+
id: `cfg:${section.id}:${toolId}`,
|
|
218
|
+
children: label,
|
|
219
|
+
keywords: [label, toolId].filter(Boolean),
|
|
220
|
+
showType: false,
|
|
221
|
+
onClick: () => {
|
|
222
|
+
// Find and click the toolbar button
|
|
223
|
+
setTimeout(() => {
|
|
224
|
+
const btn = document.querySelector(`[aria-label="${ariaLabel}"]`)
|
|
225
|
+
if (btn) btn.click()
|
|
226
|
+
}, 100)
|
|
227
|
+
},
|
|
228
|
+
})
|
|
229
|
+
} else {
|
|
230
|
+
// Fallback: click toolbar button or execute action
|
|
231
|
+
const action = actions.find(a => a.toolKey === toolId)
|
|
232
|
+
const ariaLabel = tool.ariaLabel || tool.label || toolId
|
|
233
|
+
remainingItems.push({
|
|
234
|
+
id: `cfg:${section.id}:${toolId}`,
|
|
235
|
+
children: label,
|
|
236
|
+
keywords: [label, toolId].filter(Boolean),
|
|
237
|
+
showType: false,
|
|
238
|
+
onClick: action
|
|
239
|
+
? () => executeAction(action.id)
|
|
240
|
+
: () => {
|
|
241
|
+
setTimeout(() => {
|
|
242
|
+
const btn = document.querySelector(`[aria-label="${ariaLabel}"]`)
|
|
243
|
+
if (btn) btn.click()
|
|
244
|
+
}, 100)
|
|
245
|
+
},
|
|
246
|
+
})
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Also include any toolMenus sub-pages not yet listed
|
|
252
|
+
for (const menu of toolMenus) {
|
|
253
|
+
const menuToolId = menu.id?.replace('tool:', '')
|
|
254
|
+
if (usedToolIds.has(menuToolId)) continue
|
|
255
|
+
if (remainingItems.some(i => i.id === `cfg:${section.id}:${menuToolId}`)) continue
|
|
256
|
+
remainingItems.push({
|
|
257
|
+
id: `cfg:${section.id}:${menuToolId || menu.id}`,
|
|
258
|
+
children: menu.label || menu.id,
|
|
259
|
+
keywords: menu.keywords || [menu.label || menu.id],
|
|
260
|
+
showType: false,
|
|
261
|
+
onClick: () => onNavigateToPage?.(menu.id),
|
|
262
|
+
closeOnSelect: false,
|
|
263
|
+
})
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (remainingItems.length === 0) continue
|
|
267
|
+
groups.push({
|
|
268
|
+
heading: section.title,
|
|
269
|
+
id: `cfg:${section.id}`,
|
|
270
|
+
items: remainingItems,
|
|
271
|
+
})
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return { groups, toolMenus }
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function buildDynamicSection(section, prefix, onNavigateToPage, onCreateAction) {
|
|
278
|
+
if (section.source === 'tools') {
|
|
279
|
+
return buildToolsSection(section, prefix, onNavigateToPage)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// --- Create source (dev-only workshop actions) ---
|
|
283
|
+
if (section.source === 'create') {
|
|
284
|
+
const isLocalDev = typeof window !== 'undefined' && window.__SB_LOCAL_DEV__ === true
|
|
285
|
+
if (!isLocalDev) return null
|
|
286
|
+
const createItems = [
|
|
287
|
+
{ id: 'create:canvas', children: 'New Canvas', keywords: ['create', 'canvas', 'new', 'board'], showType: false, onClick: () => onCreateAction?.('Canvas') },
|
|
288
|
+
{ id: 'create:prototype', children: 'New Prototype', keywords: ['create', 'prototype', 'new', 'page'], showType: false, onClick: () => onCreateAction?.('Prototype') },
|
|
289
|
+
{ id: 'create:component', children: 'New Component', keywords: ['create', 'component', 'new', 'story'], showType: false, onClick: () => onCreateAction?.('Component') },
|
|
290
|
+
{ id: 'create:flow', children: 'New Prototype Flow', keywords: ['create', 'flow', 'new', 'data'], showType: false, onClick: () => onCreateAction?.('Flow') },
|
|
291
|
+
{ id: 'create:page', children: 'New Prototype Page', keywords: ['create', 'page', 'new'], showType: false, onClick: () => onCreateAction?.('Page') },
|
|
292
|
+
]
|
|
293
|
+
return { group: { heading: section.title, id: `cfg:${section.id}`, items: createItems } }
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// --- Commands source (all registered toolbar actions) ---
|
|
297
|
+
if (section.source === 'commands') {
|
|
298
|
+
const mode = getCurrentMode() || 'default'
|
|
299
|
+
const actions = getActionsForMode(mode)
|
|
300
|
+
const commandItems = []
|
|
301
|
+
for (const action of actions) {
|
|
302
|
+
if (action.type === 'header' || action.type === 'separator' || action.type === 'footer') continue
|
|
303
|
+
if (action.toolKey) {
|
|
304
|
+
const state = getToolbarToolState(action.toolKey)
|
|
305
|
+
if (state === 'disabled' || state === 'hidden') continue
|
|
306
|
+
}
|
|
307
|
+
if (action.type === 'submenu') {
|
|
308
|
+
const children = getActionChildren(action.id)
|
|
309
|
+
for (const child of children) {
|
|
310
|
+
commandItems.push({
|
|
311
|
+
id: `cmd:${action.id}/${child.id || child.label}`,
|
|
312
|
+
children: child.label,
|
|
313
|
+
keywords: [action.label, child.label],
|
|
314
|
+
onClick: () => { if (child.execute) child.execute() },
|
|
315
|
+
})
|
|
316
|
+
}
|
|
317
|
+
} else if (action.type === 'link' && action.url) {
|
|
318
|
+
commandItems.push({
|
|
319
|
+
id: `cmd:${action.id}`,
|
|
320
|
+
children: action.label,
|
|
321
|
+
keywords: [action.label],
|
|
322
|
+
onClick: () => {
|
|
323
|
+
const url = action.url.startsWith('/') && !action.url.startsWith('//') ? prefix + action.url : action.url
|
|
324
|
+
window.location.href = url
|
|
325
|
+
},
|
|
326
|
+
})
|
|
327
|
+
} else {
|
|
328
|
+
commandItems.push({
|
|
329
|
+
id: `cmd:${action.id}`,
|
|
330
|
+
children: action.label,
|
|
331
|
+
keywords: [action.label],
|
|
332
|
+
onClick: () => executeAction(action.id),
|
|
333
|
+
})
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (commandItems.length === 0) return null
|
|
337
|
+
return { group: { heading: section.title, id: `cfg:${section.id}`, items: commandItems } }
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// --- Recent source: all artifact types from getRecent() ---
|
|
341
|
+
if (section.source === 'recent') {
|
|
342
|
+
const recent = getRecent()
|
|
343
|
+
if (recent.length === 0) return null
|
|
344
|
+
let items = recent
|
|
345
|
+
if (section.limit) items = items.slice(0, section.limit)
|
|
346
|
+
return {
|
|
347
|
+
group: {
|
|
348
|
+
heading: section.title,
|
|
349
|
+
id: `cfg:${section.id}`,
|
|
350
|
+
items: items.map(entry => ({
|
|
351
|
+
id: `cfg:${section.id}:${entry.type}:${entry.key}`,
|
|
352
|
+
children: entry.label,
|
|
353
|
+
keywords: [entry.type, entry.key, entry.label],
|
|
354
|
+
onClick: () => {
|
|
355
|
+
trackRecent(entry.type, entry.key, entry.label)
|
|
356
|
+
const route = resolveRecentRoute(entry, prefix)
|
|
357
|
+
if (route) window.location.href = route
|
|
358
|
+
},
|
|
359
|
+
})),
|
|
360
|
+
},
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// --- Artifact sources: canvases, prototypes, stories ---
|
|
365
|
+
const index = buildPrototypeIndex()
|
|
366
|
+
let sourceItems = []
|
|
367
|
+
|
|
368
|
+
if (section.source === 'canvases') {
|
|
369
|
+
for (const c of index.canvases) sourceItems.push({ name: c.name, route: `${prefix}${c.route}`, id: c.dirName, type: 'canvas' })
|
|
370
|
+
for (const f of index.folders) {
|
|
371
|
+
if (f.canvases) for (const c of f.canvases) sourceItems.push({ name: c.name, route: `${prefix}${c.route}`, id: c.dirName, type: 'canvas' })
|
|
372
|
+
}
|
|
373
|
+
} else if (section.source === 'prototypes') {
|
|
374
|
+
for (const p of index.prototypes) sourceItems.push({ name: p.name, route: `${prefix}/${p.dirName}`, id: p.dirName, type: 'prototype' })
|
|
375
|
+
for (const f of index.folders) {
|
|
376
|
+
for (const p of f.prototypes) sourceItems.push({ name: p.name, route: `${prefix}/${p.dirName}`, id: p.dirName, type: 'prototype' })
|
|
377
|
+
}
|
|
378
|
+
} else if (section.source === 'stories') {
|
|
379
|
+
for (const name of listStories()) {
|
|
380
|
+
const data = getStoryData(name)
|
|
381
|
+
const route = data?._route || `/components/${name}`
|
|
382
|
+
sourceItems.push({ name, route: `${prefix}${route}`, id: name, type: 'story' })
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (sourceItems.length === 0) return null
|
|
387
|
+
|
|
388
|
+
if (section.order === 'recent') {
|
|
389
|
+
const recent = getRecent()
|
|
390
|
+
const recentKeys = recent.map(r => r.key)
|
|
391
|
+
sourceItems.sort((a, b) => {
|
|
392
|
+
const ai = recentKeys.indexOf(a.id)
|
|
393
|
+
const bi = recentKeys.indexOf(b.id)
|
|
394
|
+
if (ai === -1 && bi === -1) return 0
|
|
395
|
+
if (ai === -1) return 1
|
|
396
|
+
if (bi === -1) return -1
|
|
397
|
+
return ai - bi
|
|
398
|
+
})
|
|
399
|
+
} else if (section.order === 'alphabetical') {
|
|
400
|
+
sourceItems.sort((a, b) => a.name.localeCompare(b.name))
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (section.limit) sourceItems = sourceItems.slice(0, section.limit)
|
|
404
|
+
|
|
405
|
+
return {
|
|
406
|
+
group: {
|
|
407
|
+
heading: section.title,
|
|
408
|
+
id: `cfg:${section.id}`,
|
|
409
|
+
items: sourceItems.map(item => ({
|
|
410
|
+
id: `cfg:${section.id}:${item.id}`,
|
|
411
|
+
children: item.name,
|
|
412
|
+
keywords: [item.name, item.id, item.type],
|
|
413
|
+
onClick: () => {
|
|
414
|
+
trackRecent(item.type, item.id, item.name)
|
|
415
|
+
window.location.href = item.route
|
|
416
|
+
},
|
|
417
|
+
})),
|
|
418
|
+
},
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Build a section from toolbar.config.json tools.
|
|
424
|
+
* If toolIds is provided, only include those tools in that order (with optional custom labels).
|
|
425
|
+
* Otherwise include all command-list tools.
|
|
426
|
+
*
|
|
427
|
+
* toolIds format: ["theme", "flows"] or [{ id: "theme", label: "Change theme" }]
|
|
428
|
+
*/
|
|
429
|
+
function buildToolsSection(section, prefix, onNavigateToPage) {
|
|
430
|
+
const toolbarConfig = getToolbarConfig()
|
|
431
|
+
const tools = toolbarConfig?.tools || {}
|
|
432
|
+
const mode = getCurrentMode() || 'default'
|
|
433
|
+
const actions = getActionsForMode(mode)
|
|
434
|
+
const basePath = prefix || '/'
|
|
435
|
+
|
|
436
|
+
let entries = []
|
|
437
|
+
|
|
438
|
+
if (section.toolIds && section.toolIds.length > 0) {
|
|
439
|
+
for (const entry of section.toolIds) {
|
|
440
|
+
const toolId = typeof entry === 'string' ? entry : entry.id
|
|
441
|
+
const customLabel = typeof entry === 'object' ? entry.label : null
|
|
442
|
+
const tool = tools[toolId]
|
|
443
|
+
if (!tool) continue
|
|
444
|
+
const state = getToolbarToolState(toolId)
|
|
445
|
+
if (state === 'disabled' || state === 'hidden') continue
|
|
446
|
+
if (isHiddenInPalette(tool, basePath)) continue
|
|
447
|
+
entries.push({ toolId, tool, label: customLabel || tool.label || toolId })
|
|
448
|
+
}
|
|
449
|
+
} else {
|
|
450
|
+
for (const [toolId, tool] of Object.entries(tools)) {
|
|
451
|
+
if (tool.surface !== 'command-list') continue
|
|
452
|
+
const state = getToolbarToolState(toolId)
|
|
453
|
+
if (state === 'disabled' || state === 'hidden') continue
|
|
454
|
+
if (isHiddenInPalette(tool, basePath)) continue
|
|
455
|
+
entries.push({ toolId, tool, label: tool.label || toolId })
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (entries.length === 0) return null
|
|
460
|
+
|
|
461
|
+
const items = []
|
|
462
|
+
const subPages = []
|
|
463
|
+
|
|
464
|
+
for (const { toolId, tool, label } of entries) {
|
|
465
|
+
// Inline actions
|
|
466
|
+
if (tool.inlineAction === 'toggle-chrome') {
|
|
467
|
+
items.push({
|
|
468
|
+
id: `cfg:${section.id}:${toolId}`,
|
|
469
|
+
children: label,
|
|
470
|
+
keywords: [label, toolId, 'hide', 'show', 'toolbar'].filter(Boolean),
|
|
471
|
+
showType: false,
|
|
472
|
+
onClick: () => {
|
|
473
|
+
document.documentElement.classList.toggle('storyboard-chrome-hidden')
|
|
474
|
+
},
|
|
475
|
+
})
|
|
476
|
+
continue
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (tool.render === 'link' && tool.url) {
|
|
480
|
+
items.push({
|
|
481
|
+
id: `cfg:${section.id}:${toolId}`,
|
|
482
|
+
children: label,
|
|
483
|
+
keywords: [label, toolId].filter(Boolean),
|
|
484
|
+
onClick: () => {
|
|
485
|
+
const url = tool.url.startsWith('/') ? prefix + tool.url : tool.url
|
|
486
|
+
window.location.href = url
|
|
487
|
+
},
|
|
488
|
+
})
|
|
489
|
+
continue
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (tool.render === 'submenu' || tool.render === 'menu') {
|
|
493
|
+
const action = actions.find(a => a.toolKey === toolId)
|
|
494
|
+
if (action?.type === 'submenu') {
|
|
495
|
+
const children = getActionChildren(action.id)
|
|
496
|
+
if (children.length > 0) {
|
|
497
|
+
const pageId = `tool:${toolId}`
|
|
498
|
+
subPages.push({
|
|
499
|
+
id: pageId,
|
|
500
|
+
label,
|
|
501
|
+
title: label,
|
|
502
|
+
keywords: [label, toolId].filter(Boolean),
|
|
503
|
+
options: children.map(child => ({
|
|
504
|
+
label: child.label,
|
|
505
|
+
execute: child.execute,
|
|
506
|
+
})),
|
|
507
|
+
})
|
|
508
|
+
items.push({
|
|
509
|
+
id: `cfg:${section.id}:${toolId}`,
|
|
510
|
+
children: label,
|
|
511
|
+
keywords: [label, toolId].filter(Boolean),
|
|
512
|
+
onClick: () => onNavigateToPage?.(pageId),
|
|
513
|
+
closeOnSelect: false,
|
|
514
|
+
showType: false,
|
|
515
|
+
})
|
|
516
|
+
continue
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Declarative options from toolbar.config.json (e.g. theme options)
|
|
521
|
+
if (tool.options && tool.options.length > 0) {
|
|
522
|
+
const pageId = `tool:${toolId}`
|
|
523
|
+
const handlerId = tool.handler || `core:${toolId}`
|
|
524
|
+
subPages.push({
|
|
525
|
+
id: pageId,
|
|
526
|
+
label,
|
|
527
|
+
title: label,
|
|
528
|
+
keywords: [label, toolId].filter(Boolean),
|
|
529
|
+
options: tool.options.map(opt => ({
|
|
530
|
+
label: opt.label,
|
|
531
|
+
// Lazy-execute via the handler's action system
|
|
532
|
+
toolHandler: handlerId,
|
|
533
|
+
value: opt.value,
|
|
534
|
+
})),
|
|
535
|
+
})
|
|
536
|
+
items.push({
|
|
537
|
+
id: `cfg:${section.id}:${toolId}`,
|
|
538
|
+
children: label,
|
|
539
|
+
keywords: [label, toolId].filter(Boolean),
|
|
540
|
+
onClick: () => onNavigateToPage?.(pageId),
|
|
541
|
+
closeOnSelect: false,
|
|
542
|
+
showType: false,
|
|
543
|
+
})
|
|
544
|
+
continue
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Menu tool without sub-items or options — click toolbar button
|
|
548
|
+
const ariaLabel = tool.ariaLabel || tool.label || toolId
|
|
549
|
+
items.push({
|
|
550
|
+
id: `cfg:${section.id}:${toolId}`,
|
|
551
|
+
children: label,
|
|
552
|
+
keywords: [label, toolId].filter(Boolean),
|
|
553
|
+
showType: false,
|
|
554
|
+
onClick: () => {
|
|
555
|
+
setTimeout(() => {
|
|
556
|
+
const btn = document.querySelector(`[aria-label="${ariaLabel}"]`)
|
|
557
|
+
if (btn) btn.click()
|
|
558
|
+
}, 100)
|
|
559
|
+
},
|
|
560
|
+
})
|
|
561
|
+
continue
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (tool.render === 'sidepanel' && tool.sidepanel) {
|
|
565
|
+
const action = actions.find(a => a.toolKey === toolId)
|
|
566
|
+
items.push({
|
|
567
|
+
id: `cfg:${section.id}:${toolId}`,
|
|
568
|
+
children: label,
|
|
569
|
+
keywords: [label, toolId].filter(Boolean),
|
|
570
|
+
onClick: () => { if (action) executeAction(action.id) },
|
|
571
|
+
})
|
|
572
|
+
continue
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
items.push({
|
|
576
|
+
id: `cfg:${section.id}:${toolId}`,
|
|
577
|
+
children: label,
|
|
578
|
+
keywords: [label, toolId].filter(Boolean),
|
|
579
|
+
onClick: () => executeAction(toolId),
|
|
580
|
+
})
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
return {
|
|
584
|
+
group: {
|
|
585
|
+
heading: section.title,
|
|
586
|
+
id: `cfg:${section.id}`,
|
|
587
|
+
items,
|
|
588
|
+
},
|
|
589
|
+
subPages,
|
|
590
|
+
usedToolIds: entries.map(e => e.toolId),
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function resolveRecentRoute(entry, prefix) {
|
|
595
|
+
switch (entry.type) {
|
|
596
|
+
case 'prototype':
|
|
597
|
+
return `${prefix}/${entry.key}`
|
|
598
|
+
case 'canvas':
|
|
599
|
+
return `${prefix}/canvas/${entry.key}`
|
|
600
|
+
case 'story': {
|
|
601
|
+
const data = getStoryData(entry.key)
|
|
602
|
+
const route = data?._route || `/components/${entry.key}`
|
|
603
|
+
return `${prefix}${route}`
|
|
604
|
+
}
|
|
605
|
+
default:
|
|
606
|
+
return null
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Build a map of author → artifacts from the prototype index.
|
|
612
|
+
* Returns { authorIndex: Map<lowercase-author, { author, items[] }> }
|
|
613
|
+
*/
|
|
614
|
+
function buildAuthorIndex(prefix) {
|
|
615
|
+
const index = buildPrototypeIndex()
|
|
616
|
+
const authorMap = new Map()
|
|
617
|
+
|
|
618
|
+
function addItem(author, item) {
|
|
619
|
+
const key = author.toLowerCase()
|
|
620
|
+
if (!authorMap.has(key)) authorMap.set(key, { author, items: [] })
|
|
621
|
+
authorMap.get(key).items.push(item)
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function processAuthors(authors, item) {
|
|
625
|
+
if (!authors) return
|
|
626
|
+
const list = Array.isArray(authors) ? authors : [authors]
|
|
627
|
+
for (const a of list) if (a) addItem(a, item)
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
for (const p of index.prototypes) {
|
|
631
|
+
processAuthors(p.author, { name: p.name, route: `${prefix}/${p.dirName}`, id: p.dirName, type: 'Prototype' })
|
|
632
|
+
}
|
|
633
|
+
for (const f of index.folders) {
|
|
634
|
+
for (const p of f.prototypes) {
|
|
635
|
+
processAuthors(p.author, { name: p.name, route: `${prefix}/${p.dirName}`, id: p.dirName, type: 'Prototype' })
|
|
636
|
+
}
|
|
637
|
+
if (f.canvases) {
|
|
638
|
+
for (const c of f.canvases) {
|
|
639
|
+
processAuthors(c.author, { name: c.name, route: `${prefix}${c.route}`, id: c.dirName, type: 'Canvas' })
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
for (const c of index.canvases) {
|
|
644
|
+
processAuthors(c.author, { name: c.name, route: `${prefix}${c.route}`, id: c.dirName, type: 'Canvas' })
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
return authorMap
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Build the JSON structure for react-cmdk from all data providers.
|
|
652
|
+
* Entirely config-driven — all sections come from commandPalette.sections.
|
|
653
|
+
*/
|
|
654
|
+
function buildPaletteItems(basePath, onCreateAction, onNavigateToPage) {
|
|
655
|
+
const base = (basePath || '/').replace(/\/+$/, '')
|
|
656
|
+
const prefix = base === '/' ? '' : base
|
|
657
|
+
|
|
658
|
+
const { groups, toolMenus } = buildConfigSections(prefix, onNavigateToPage, onCreateAction)
|
|
659
|
+
const authorIndex = buildAuthorIndex(prefix)
|
|
660
|
+
|
|
661
|
+
return { groups, toolMenus, authorIndex }
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* StoryboardCommandPalette — React command palette using react-cmdk.
|
|
666
|
+
* Mounted at app root, listens for custom events from Svelte CoreUIBar.
|
|
667
|
+
*/
|
|
668
|
+
export default function StoryboardCommandPalette({ basePath }) {
|
|
669
|
+
const [open, setOpen] = useState(false)
|
|
670
|
+
const [search, setSearch] = useState('')
|
|
671
|
+
const [items, setItems] = useState([])
|
|
672
|
+
const [toolMenus, setToolMenus] = useState([])
|
|
673
|
+
const [authorIndex, setAuthorIndex] = useState(new Map())
|
|
674
|
+
const [activePage, setActivePage] = useState('root')
|
|
675
|
+
const [createType, setCreateType] = useState(null)
|
|
676
|
+
|
|
677
|
+
function handleCreateAction(type) {
|
|
678
|
+
setOpen(false)
|
|
679
|
+
requestAnimationFrame(() => setCreateType(type))
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function handleNavigateToPage(pageId) {
|
|
683
|
+
setSearch('')
|
|
684
|
+
setActivePage(pageId)
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Listen for Cmd+K directly
|
|
688
|
+
// The Svelte CoreUIBar also handles Cmd+K by dispatching
|
|
689
|
+
// 'storyboard:toggle-palette'. We use rAF to detect if Svelte
|
|
690
|
+
// already fired the toggle event and skip to avoid double-toggle.
|
|
691
|
+
useEffect(() => {
|
|
692
|
+
let toggledByEvent = false
|
|
693
|
+
|
|
694
|
+
function handleToggleEvent() {
|
|
695
|
+
toggledByEvent = true
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function handleKeyDown(e) {
|
|
699
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
700
|
+
e.preventDefault()
|
|
701
|
+
toggledByEvent = false
|
|
702
|
+
requestAnimationFrame(() => {
|
|
703
|
+
if (toggledByEvent) return
|
|
704
|
+
const built = buildPaletteItems(basePath, handleCreateAction, handleNavigateToPage)
|
|
705
|
+
setItems(built.groups)
|
|
706
|
+
setToolMenus(built.toolMenus)
|
|
707
|
+
setAuthorIndex(built.authorIndex)
|
|
708
|
+
setSearch('')
|
|
709
|
+
setActivePage('root')
|
|
710
|
+
setOpen(prev => !prev)
|
|
711
|
+
})
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
document.addEventListener('storyboard:toggle-palette', handleToggleEvent)
|
|
716
|
+
document.addEventListener('keydown', handleKeyDown)
|
|
717
|
+
return () => {
|
|
718
|
+
document.removeEventListener('storyboard:toggle-palette', handleToggleEvent)
|
|
719
|
+
document.removeEventListener('keydown', handleKeyDown)
|
|
720
|
+
}
|
|
721
|
+
}, [basePath])
|
|
722
|
+
|
|
723
|
+
// Listen for toggle events from Svelte CoreUIBar
|
|
724
|
+
useEffect(() => {
|
|
725
|
+
function handleToggle() {
|
|
726
|
+
setOpen(prev => {
|
|
727
|
+
if (!prev) {
|
|
728
|
+
// Use setTimeout to set items after open state is committed
|
|
729
|
+
setTimeout(() => {
|
|
730
|
+
const built = buildPaletteItems(basePath, handleCreateAction, handleNavigateToPage)
|
|
731
|
+
setItems(built.groups)
|
|
732
|
+
setToolMenus(built.toolMenus)
|
|
733
|
+
setAuthorIndex(built.authorIndex)
|
|
734
|
+
setSearch('')
|
|
735
|
+
setActivePage('root')
|
|
736
|
+
}, 0)
|
|
737
|
+
}
|
|
738
|
+
return !prev
|
|
739
|
+
})
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function handleOpen() {
|
|
743
|
+
const built = buildPaletteItems(basePath, handleCreateAction, handleNavigateToPage)
|
|
744
|
+
setItems(built.groups)
|
|
745
|
+
setToolMenus(built.toolMenus)
|
|
746
|
+
setAuthorIndex(built.authorIndex)
|
|
747
|
+
setSearch('')
|
|
748
|
+
setActivePage('root')
|
|
749
|
+
setOpen(true)
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
document.addEventListener('storyboard:toggle-palette', handleToggle)
|
|
753
|
+
document.addEventListener('storyboard:open-palette', handleOpen)
|
|
754
|
+
return () => {
|
|
755
|
+
document.removeEventListener('storyboard:toggle-palette', handleToggle)
|
|
756
|
+
document.removeEventListener('storyboard:open-palette', handleOpen)
|
|
757
|
+
}
|
|
758
|
+
}, [basePath])
|
|
759
|
+
|
|
760
|
+
const handleChangeOpen = useCallback((value) => {
|
|
761
|
+
if (!value) {
|
|
762
|
+
setOpen(false)
|
|
763
|
+
setActivePage('root')
|
|
764
|
+
}
|
|
765
|
+
}, [])
|
|
766
|
+
|
|
767
|
+
// Flatten sub-page options into searchable groups so they appear in root search
|
|
768
|
+
const subPageGroups = useMemo(() => {
|
|
769
|
+
return toolMenus.map(menu => ({
|
|
770
|
+
heading: menu.label || menu.title || menu.id,
|
|
771
|
+
id: `subpage:${menu.id}`,
|
|
772
|
+
items: (menu.options || []).map((opt, i) => ({
|
|
773
|
+
id: `subpage:${menu.id}:${i}`,
|
|
774
|
+
children: opt.label,
|
|
775
|
+
keywords: [opt.label, menu.label || menu.id],
|
|
776
|
+
showType: false,
|
|
777
|
+
onClick: () => {
|
|
778
|
+
if (opt.execute) {
|
|
779
|
+
opt.execute()
|
|
780
|
+
} else if (opt.toolHandler === 'core:theme' && opt.value) {
|
|
781
|
+
setTheme(opt.value)
|
|
782
|
+
} else if (opt.action) {
|
|
783
|
+
executeAction(opt.action, opt.value)
|
|
784
|
+
}
|
|
785
|
+
setOpen(false)
|
|
786
|
+
setActivePage('root')
|
|
787
|
+
},
|
|
788
|
+
})),
|
|
789
|
+
})).filter(g => g.items.length > 0)
|
|
790
|
+
}, [toolMenus])
|
|
791
|
+
|
|
792
|
+
const filteredItems = useMemo(() => {
|
|
793
|
+
const base = filterItems(items, search)
|
|
794
|
+
if (!search) return base
|
|
795
|
+
const matchingSub = filterItems(subPageGroups, search)
|
|
796
|
+
const result = [...base, ...matchingSub]
|
|
797
|
+
|
|
798
|
+
// Author search: match usernames against author index
|
|
799
|
+
const q = search.toLowerCase()
|
|
800
|
+
for (const [key, { author, items: authorItems }] of authorIndex) {
|
|
801
|
+
if (!key.includes(q)) continue
|
|
802
|
+
// Avoid duplicates with already-shown artifact items
|
|
803
|
+
const shownIds = new Set(result.flatMap(g => g.items.map(i => i.id)))
|
|
804
|
+
const uniqueItems = authorItems.filter(item => !shownIds.has(`author:${item.id}`))
|
|
805
|
+
if (uniqueItems.length === 0) continue
|
|
806
|
+
result.push({
|
|
807
|
+
heading: `Artifacts by @${author}`,
|
|
808
|
+
id: `author:${key}`,
|
|
809
|
+
items: uniqueItems.map(item => ({
|
|
810
|
+
id: `author:${item.id}`,
|
|
811
|
+
children: (
|
|
812
|
+
<span style={{ display: 'flex', width: '100%', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
813
|
+
<span>{item.name}</span>
|
|
814
|
+
<span style={{ fontSize: '12px', color: 'var(--fgColor-muted, #999)' }}>{item.type}</span>
|
|
815
|
+
</span>
|
|
816
|
+
),
|
|
817
|
+
keywords: [item.name, item.id, item.type, author, `@${author}`],
|
|
818
|
+
showType: false,
|
|
819
|
+
onClick: () => {
|
|
820
|
+
trackRecent(item.type.toLowerCase(), item.id, item.name)
|
|
821
|
+
window.location.href = item.route
|
|
822
|
+
},
|
|
823
|
+
})),
|
|
824
|
+
})
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
return result
|
|
828
|
+
}, [items, search, subPageGroups, authorIndex])
|
|
829
|
+
|
|
830
|
+
// Items without separators — used for keyboard navigation indexing
|
|
831
|
+
const navigableItems = useMemo(
|
|
832
|
+
() => filteredItems.filter(list => !list.id?.startsWith('cfg:sep')),
|
|
833
|
+
[filteredItems]
|
|
834
|
+
)
|
|
835
|
+
|
|
836
|
+
const handleChangeSearch = useCallback((value) => {
|
|
837
|
+
setSearch(value)
|
|
838
|
+
}, [])
|
|
839
|
+
|
|
840
|
+
return (
|
|
841
|
+
<>
|
|
842
|
+
<CommandPalette
|
|
843
|
+
onChangeSearch={handleChangeSearch}
|
|
844
|
+
onChangeOpen={handleChangeOpen}
|
|
845
|
+
search={search}
|
|
846
|
+
isOpen={open}
|
|
847
|
+
page={activePage}
|
|
848
|
+
placeholder={activePage === 'root'
|
|
849
|
+
? 'Search commands, prototypes, canvases, stories...'
|
|
850
|
+
: `Search ${toolMenus.find(m => m.id === activePage)?.label || ''}...`
|
|
851
|
+
}
|
|
852
|
+
>
|
|
853
|
+
<CommandPalette.Page id="root">
|
|
854
|
+
{filteredItems.length ? (
|
|
855
|
+
filteredItems.map((list) => (
|
|
856
|
+
list.id?.startsWith('cfg:sep') ? (
|
|
857
|
+
!search && <hr key={list.id} style={{ border: 'none', borderTop: '1px solid var(--borderColor-muted, #e5e5e5)', margin: '4px 14px' }} />
|
|
858
|
+
) : (
|
|
859
|
+
<CommandPalette.List key={list.id} heading={list.heading}>
|
|
860
|
+
{list.items.map(({ id, ...rest }) => (
|
|
861
|
+
<CommandPalette.ListItem
|
|
862
|
+
key={id}
|
|
863
|
+
index={getItemIndex(navigableItems, id)}
|
|
864
|
+
{...rest}
|
|
865
|
+
/>
|
|
866
|
+
))}
|
|
867
|
+
</CommandPalette.List>
|
|
868
|
+
)
|
|
869
|
+
))
|
|
870
|
+
) : (
|
|
871
|
+
<div style={{ padding: '2rem', textAlign: 'center', color: '#6b7280' }}>
|
|
872
|
+
No results for “{search}”
|
|
873
|
+
</div>
|
|
874
|
+
)}
|
|
875
|
+
</CommandPalette.Page>
|
|
876
|
+
|
|
877
|
+
{/* Tool-menu sub-pages */}
|
|
878
|
+
{toolMenus.map(menu => (
|
|
879
|
+
<CommandPalette.Page
|
|
880
|
+
key={menu.id}
|
|
881
|
+
id={menu.id}
|
|
882
|
+
onEscape={() => { setActivePage('root'); setSearch('') }}
|
|
883
|
+
searchPrefix={[menu.label || menu.id]}
|
|
884
|
+
>
|
|
885
|
+
<CommandPalette.List heading={menu.title || menu.label || menu.id}>
|
|
886
|
+
{(menu.options || []).map((opt, i) => (
|
|
887
|
+
<CommandPalette.ListItem
|
|
888
|
+
key={`${menu.id}:${i}`}
|
|
889
|
+
index={i}
|
|
890
|
+
showType={false}
|
|
891
|
+
onClick={() => {
|
|
892
|
+
if (opt.execute) {
|
|
893
|
+
opt.execute()
|
|
894
|
+
} else if (opt.toolHandler === 'core:theme' && opt.value) {
|
|
895
|
+
setTheme(opt.value)
|
|
896
|
+
} else if (opt.action) {
|
|
897
|
+
executeAction(opt.action, opt.value)
|
|
898
|
+
}
|
|
899
|
+
setOpen(false)
|
|
900
|
+
setActivePage('root')
|
|
901
|
+
}}
|
|
902
|
+
>
|
|
903
|
+
{opt.label}
|
|
904
|
+
</CommandPalette.ListItem>
|
|
905
|
+
))}
|
|
906
|
+
</CommandPalette.List>
|
|
907
|
+
</CommandPalette.Page>
|
|
908
|
+
))}
|
|
909
|
+
</CommandPalette>
|
|
910
|
+
|
|
911
|
+
<CreateDialog
|
|
912
|
+
type={createType}
|
|
913
|
+
basePath={basePath}
|
|
914
|
+
onClose={() => setCreateType(null)}
|
|
915
|
+
/>
|
|
916
|
+
</>
|
|
917
|
+
)
|
|
918
|
+
}
|