@dfosco/storyboard-react 4.2.0-beta.4 → 4.2.1
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 +10 -11
- package/src/AuthModal/AuthModal.jsx +6 -8
- package/src/BranchBar/BranchBar.jsx +20 -6
- package/src/BranchBar/BranchBar.module.css +13 -4
- package/src/BranchBar/useBranches.js +20 -6
- package/src/BranchBar/useBranches.test.js +68 -0
- package/src/CommandPalette/CommandPalette.jsx +480 -187
- package/src/CommandPalette/command-palette.css +142 -78
- package/src/Icon.jsx +157 -58
- package/src/Viewfinder.jsx +562 -207
- package/src/Viewfinder.module.css +434 -93
- package/src/Workspace.jsx +7 -0
- package/src/canvas/CanvasPage.bridge.test.jsx +14 -6
- package/src/canvas/CanvasPage.dragdrop.test.jsx +11 -7
- package/src/canvas/CanvasPage.jsx +739 -219
- package/src/canvas/CanvasPage.module.css +13 -15
- package/src/canvas/CanvasPage.multiselect.test.jsx +17 -6
- package/src/canvas/ConnectorLayer.jsx +121 -165
- package/src/canvas/ConnectorLayer.module.css +69 -0
- package/src/canvas/PageSelector.test.jsx +15 -6
- package/src/canvas/canvasApi.js +68 -2
- package/src/canvas/canvasReloadGuard.test.js +1 -1
- package/src/canvas/connectorGeometry.js +132 -0
- package/src/canvas/hotPoolDevLogs.js +25 -0
- package/src/canvas/useCanvas.js +1 -1
- package/src/canvas/useMarqueeSelect.js +30 -4
- package/src/canvas/widgets/CodePenEmbed.jsx +1 -0
- package/src/canvas/widgets/ComponentSetWidget.jsx +199 -0
- package/src/canvas/widgets/ComponentSetWidget.module.css +89 -0
- package/src/canvas/widgets/ComponentWidget.jsx +1 -0
- package/src/canvas/widgets/CropOverlay.jsx +219 -0
- package/src/canvas/widgets/CropOverlay.module.css +118 -0
- package/src/canvas/widgets/ExpandedPane.jsx +474 -0
- package/src/canvas/widgets/ExpandedPane.module.css +179 -0
- package/src/canvas/widgets/ExpandedPane.test.jsx +240 -0
- package/src/canvas/widgets/ExpandedPaneTopBar.jsx +111 -0
- package/src/canvas/widgets/ExpandedPaneTopBar.module.css +59 -0
- package/src/canvas/widgets/ExpandedPaneTopBar.test.jsx +45 -0
- package/src/canvas/widgets/FigmaEmbed.jsx +62 -47
- package/src/canvas/widgets/FigmaEmbed.module.css +61 -0
- package/src/canvas/widgets/ImageWidget.jsx +130 -9
- package/src/canvas/widgets/ImageWidget.module.css +30 -0
- package/src/canvas/widgets/LinkPreview.jsx +113 -5
- package/src/canvas/widgets/LinkPreview.module.css +127 -0
- package/src/canvas/widgets/MarkdownBlock.jsx +167 -17
- package/src/canvas/widgets/MarkdownBlock.module.css +148 -0
- package/src/canvas/widgets/PromptWidget.jsx +414 -0
- package/src/canvas/widgets/PromptWidget.module.css +273 -0
- package/src/canvas/widgets/PrototypeEmbed.jsx +77 -39
- package/src/canvas/widgets/PrototypeEmbed.module.css +117 -0
- package/src/canvas/widgets/PrototypeEmbed.test.jsx +2 -2
- package/src/canvas/widgets/ResizeHandle.jsx +17 -6
- package/src/canvas/widgets/StoryWidget.jsx +73 -15
- package/src/canvas/widgets/TerminalReadWidget.jsx +146 -0
- package/src/canvas/widgets/TerminalReadWidget.module.css +94 -0
- package/src/canvas/widgets/TerminalWidget.jsx +445 -67
- package/src/canvas/widgets/TerminalWidget.module.css +271 -8
- package/src/canvas/widgets/TilesWidget.jsx +300 -0
- package/src/canvas/widgets/TilesWidget.module.css +133 -0
- package/src/canvas/widgets/WidgetChrome.jsx +74 -153
- package/src/canvas/widgets/WidgetChrome.module.css +30 -1
- package/src/canvas/widgets/embedInteraction.test.jsx +24 -26
- package/src/canvas/widgets/expandUtils.js +560 -0
- package/src/canvas/widgets/expandUtils.test.js +155 -0
- package/src/canvas/widgets/index.js +9 -0
- package/src/canvas/widgets/snapshotDisplay.test.jsx +23 -71
- package/src/canvas/widgets/tilePool.js +23 -0
- package/src/canvas/widgets/tiles/diagonal-bl.png +0 -0
- package/src/canvas/widgets/tiles/diagonal-br.png +0 -0
- package/src/canvas/widgets/tiles/diagonal-tl.png +0 -0
- package/src/canvas/widgets/tiles/leaf.png +0 -0
- package/src/canvas/widgets/tiles/quarter-tl.png +0 -0
- package/src/canvas/widgets/tiles/quarter-tr.png +0 -0
- package/src/canvas/widgets/tiles/solid-a.png +0 -0
- package/src/canvas/widgets/tiles/solid-b.png +0 -0
- package/src/canvas/widgets/widgetConfig.js +55 -4
- package/src/canvas/widgets/widgetIcons.jsx +190 -0
- package/src/canvas/widgets/widgetProps.js +1 -0
- package/src/context.jsx +48 -20
- package/src/hooks/useConfig.js +14 -0
- package/src/hooks/usePrototypeReloadGuard.js +64 -0
- package/src/hooks/useSceneData.js +1 -0
- package/src/hooks/useThemeState.test.js +1 -1
- package/src/index.js +8 -2
- package/src/story/ComponentSetPage.jsx +186 -0
- package/src/story/ComponentSetPage.module.css +121 -0
- package/src/story/StoryPage.jsx +32 -2
- package/src/vite/data-plugin.js +407 -67
- package/src/vite/data-plugin.test.js +1 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback, useMemo } from 'react'
|
|
2
|
-
import '
|
|
3
|
-
import * as
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { useState, useEffect, useCallback, useMemo, useRef } from 'react'
|
|
2
|
+
import { Command } from 'cmdk'
|
|
3
|
+
import * as VisuallyHidden from '@radix-ui/react-visually-hidden'
|
|
4
|
+
import * as DialogPrimitive from '@radix-ui/react-dialog'
|
|
5
|
+
import Icon from '../Icon.jsx'
|
|
6
6
|
import {
|
|
7
7
|
buildPrototypeIndex,
|
|
8
8
|
listStories,
|
|
@@ -16,9 +16,11 @@ import {
|
|
|
16
16
|
trackRecent,
|
|
17
17
|
getCommandPaletteConfig,
|
|
18
18
|
getToolbarConfig,
|
|
19
|
+
getConfig,
|
|
19
20
|
setTheme,
|
|
20
21
|
getTheme,
|
|
21
22
|
isExcludedByRoute,
|
|
23
|
+
scoreMatch,
|
|
22
24
|
} from '@dfosco/storyboard-core'
|
|
23
25
|
import { widgetTypes } from '../canvas/widgets/widgetConfig.js'
|
|
24
26
|
import CreateDialog from './CreateDialog.jsx'
|
|
@@ -26,6 +28,34 @@ import BranchBar from '../BranchBar/BranchBar.jsx'
|
|
|
26
28
|
import AuthModal from '../AuthModal/AuthModal.jsx'
|
|
27
29
|
import './command-palette.css'
|
|
28
30
|
|
|
31
|
+
// Icon size for all palette items
|
|
32
|
+
const ICON_SIZE = 16
|
|
33
|
+
|
|
34
|
+
function getIconMap() {
|
|
35
|
+
const config = getCommandPaletteConfig()
|
|
36
|
+
return config?.icons || {}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function ItemIcon({ type, toolIcon, toolMeta }) {
|
|
40
|
+
const icons = getIconMap()
|
|
41
|
+
const entry = toolIcon || icons[type] || icons.fallback || 'feather/hexagon'
|
|
42
|
+
const iconName = typeof entry === 'object' ? entry.name : entry
|
|
43
|
+
const meta = toolMeta || (typeof entry === 'object' ? entry.meta : undefined)
|
|
44
|
+
return <Icon name={iconName} size={ICON_SIZE} color="var(--fgColor-muted, #656d76)" {...(meta || {})} />
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function AvatarIcon({ username }) {
|
|
48
|
+
return (
|
|
49
|
+
<img
|
|
50
|
+
src={`https://github.com/${username}.png?size=32`}
|
|
51
|
+
alt={username}
|
|
52
|
+
width={ICON_SIZE}
|
|
53
|
+
height={ICON_SIZE}
|
|
54
|
+
style={{ flexShrink: 0, borderRadius: '50%' }}
|
|
55
|
+
/>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
29
59
|
/**
|
|
30
60
|
* Check if a tool should be hidden from the command palette on the current route.
|
|
31
61
|
* Uses the same pattern-matching logic as excludeRoutes.
|
|
@@ -58,7 +88,8 @@ function buildConfigSections(prefix, onNavigateToPage, onCreateAction) {
|
|
|
58
88
|
const sections = config?.sections || []
|
|
59
89
|
const groups = []
|
|
60
90
|
const toolMenus = []
|
|
61
|
-
const usedToolIds = new Set()
|
|
91
|
+
const usedToolIds = new Set()
|
|
92
|
+
const hiddenFromSearchIds = new Set()
|
|
62
93
|
const basePath = prefix || '/'
|
|
63
94
|
|
|
64
95
|
for (const section of sections) {
|
|
@@ -80,6 +111,7 @@ function buildConfigSections(prefix, onNavigateToPage, onCreateAction) {
|
|
|
80
111
|
if (result?.group) groups.push(result.group)
|
|
81
112
|
if (result?.subPages) toolMenus.push(...result.subPages)
|
|
82
113
|
if (result?.usedToolIds) result.usedToolIds.forEach(id => usedToolIds.add(id))
|
|
114
|
+
if (result?.hiddenFromSearchIds) result.hiddenFromSearchIds.forEach(id => hiddenFromSearchIds.add(id))
|
|
83
115
|
continue
|
|
84
116
|
}
|
|
85
117
|
|
|
@@ -90,13 +122,14 @@ function buildConfigSections(prefix, onNavigateToPage, onCreateAction) {
|
|
|
90
122
|
items: section.items.map((item, i) => {
|
|
91
123
|
const id = `cfg:${section.id}:${i}`
|
|
92
124
|
if (item.type === 'link') {
|
|
125
|
+
const resolvedUrl = item.url?.startsWith('/') ? prefix + item.url : item.url
|
|
93
126
|
return {
|
|
94
127
|
id,
|
|
95
128
|
children: item.label,
|
|
96
129
|
keywords: item.keywords || [item.label],
|
|
130
|
+
url: resolvedUrl,
|
|
97
131
|
onClick: () => {
|
|
98
|
-
|
|
99
|
-
if (url) window.location.href = url
|
|
132
|
+
if (resolvedUrl) window.location.href = resolvedUrl
|
|
100
133
|
},
|
|
101
134
|
}
|
|
102
135
|
}
|
|
@@ -204,14 +237,21 @@ function buildConfigSections(prefix, onNavigateToPage, onCreateAction) {
|
|
|
204
237
|
continue
|
|
205
238
|
}
|
|
206
239
|
|
|
240
|
+
if (tool.inlineAction === 'open-palette') {
|
|
241
|
+
// Skip — no point opening the palette from within itself
|
|
242
|
+
continue
|
|
243
|
+
}
|
|
244
|
+
|
|
207
245
|
// Any remaining tools (all surfaces)
|
|
208
246
|
if (tool.render === 'link' && tool.url) {
|
|
247
|
+
const resolvedUrl = tool.url.startsWith('/') ? prefix + tool.url : tool.url
|
|
209
248
|
remainingItems.push({
|
|
210
249
|
id: `cfg:${section.id}:${toolId}`,
|
|
211
250
|
children: label,
|
|
212
251
|
keywords: [label, toolId].filter(Boolean),
|
|
213
252
|
showType: false,
|
|
214
|
-
|
|
253
|
+
url: resolvedUrl,
|
|
254
|
+
onClick: () => { window.location.href = resolvedUrl },
|
|
215
255
|
})
|
|
216
256
|
} else {
|
|
217
257
|
// Menu tools: close palette and click the toolbar button to open the menu
|
|
@@ -252,9 +292,10 @@ function buildConfigSections(prefix, onNavigateToPage, onCreateAction) {
|
|
|
252
292
|
}
|
|
253
293
|
}
|
|
254
294
|
|
|
255
|
-
// Also include any
|
|
295
|
+
// Also include any tool sub-pages not yet listed (skip non-tool sub-pages like create-widget)
|
|
256
296
|
for (const menu of toolMenus) {
|
|
257
|
-
|
|
297
|
+
if (!menu.id?.startsWith('tool:')) continue
|
|
298
|
+
const menuToolId = menu.id.replace('tool:', '')
|
|
258
299
|
if (usedToolIds.has(menuToolId)) continue
|
|
259
300
|
if (remainingItems.some(i => i.id === `cfg:${section.id}:${menuToolId}`)) continue
|
|
260
301
|
remainingItems.push({
|
|
@@ -268,6 +309,18 @@ function buildConfigSections(prefix, onNavigateToPage, onCreateAction) {
|
|
|
268
309
|
}
|
|
269
310
|
|
|
270
311
|
if (remainingItems.length === 0) continue
|
|
312
|
+
|
|
313
|
+
// Stamp toolIcon from toolbar config onto remaining items
|
|
314
|
+
for (const item of remainingItems) {
|
|
315
|
+
if (!item.toolIcon) {
|
|
316
|
+
const match = item.id?.match(/cfg:[^:]+:(.+)/)
|
|
317
|
+
if (match) {
|
|
318
|
+
const t = allTools[match[1]]
|
|
319
|
+
if (t?.icon) item.toolIcon = t.icon
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
271
324
|
groups.push({
|
|
272
325
|
heading: section.title,
|
|
273
326
|
id: `cfg:${section.id}`,
|
|
@@ -275,7 +328,7 @@ function buildConfigSections(prefix, onNavigateToPage, onCreateAction) {
|
|
|
275
328
|
})
|
|
276
329
|
}
|
|
277
330
|
|
|
278
|
-
return { groups, toolMenus }
|
|
331
|
+
return { groups, toolMenus, hiddenFromSearchIds }
|
|
279
332
|
}
|
|
280
333
|
|
|
281
334
|
function buildDynamicSection(section, prefix, onNavigateToPage, onCreateAction) {
|
|
@@ -288,11 +341,11 @@ function buildDynamicSection(section, prefix, onNavigateToPage, onCreateAction)
|
|
|
288
341
|
const isLocalDev = typeof window !== 'undefined' && window.__SB_LOCAL_DEV__ === true
|
|
289
342
|
if (!isLocalDev) return null
|
|
290
343
|
const createItems = [
|
|
291
|
-
{ id: 'create:canvas', children: 'Canvas', keywords: ['create', 'canvas', 'new', 'board'],
|
|
292
|
-
{ id: 'create:prototype', children: 'Prototype', keywords: ['create', 'prototype', 'new', 'page'],
|
|
293
|
-
{ id: 'create:component', children: 'Component', keywords: ['create', 'component', 'new', 'story'],
|
|
294
|
-
{ id: 'create:flow', children: 'Prototype Flow', keywords: ['create', 'flow', 'new', 'data'],
|
|
295
|
-
{ id: 'create:page', children: 'Prototype Page', keywords: ['create', 'page', 'new'],
|
|
344
|
+
{ id: 'create:canvas', children: 'Canvas', keywords: ['create', 'canvas', 'new', 'board'], itemType: 'create', onClick: () => onCreateAction?.('Canvas') },
|
|
345
|
+
{ id: 'create:prototype', children: 'Prototype', keywords: ['create', 'prototype', 'new', 'page'], itemType: 'create', onClick: () => onCreateAction?.('Prototype') },
|
|
346
|
+
{ id: 'create:component', children: 'Component', keywords: ['create', 'component', 'new', 'story'], itemType: 'create', onClick: () => onCreateAction?.('Component') },
|
|
347
|
+
{ id: 'create:flow', children: 'Prototype Flow', keywords: ['create', 'flow', 'new', 'data'], itemType: 'create', onClick: () => onCreateAction?.('Flow') },
|
|
348
|
+
{ id: 'create:page', children: 'Prototype Page', keywords: ['create', 'page', 'new'], itemType: 'create', onClick: () => onCreateAction?.('Page') },
|
|
296
349
|
]
|
|
297
350
|
return { group: { heading: section.title, id: `cfg:${section.id}`, items: createItems } }
|
|
298
351
|
}
|
|
@@ -303,23 +356,68 @@ function buildDynamicSection(section, prefix, onNavigateToPage, onCreateAction)
|
|
|
303
356
|
if (!isLocalDev) return null
|
|
304
357
|
const isCanvasRoute = typeof window !== 'undefined' && window.location.pathname.includes('/canvas/')
|
|
305
358
|
if (!isCanvasRoute) return null
|
|
306
|
-
const
|
|
359
|
+
const hiddenTypes = new Set(['link-preview', 'image', 'figma-embed', 'codepen-embed', 'story', 'terminal-read', 'agent'])
|
|
360
|
+
const items = Object.entries(widgetTypes).filter(([type]) => !hiddenTypes.has(type)).map(([type, def]) => ({
|
|
307
361
|
id: `create-widget:${type}`,
|
|
308
362
|
children: def.label,
|
|
309
363
|
keywords: ['add', 'widget', 'create', type, def.label.toLowerCase()],
|
|
310
|
-
|
|
364
|
+
itemType: type,
|
|
311
365
|
onClick: () => {
|
|
312
366
|
document.dispatchEvent(new CustomEvent('storyboard:canvas:add-widget', { detail: { type } }))
|
|
313
367
|
},
|
|
314
368
|
}))
|
|
315
|
-
|
|
369
|
+
|
|
370
|
+
// Build agent submenu from canvas.agents config
|
|
371
|
+
const subPages = []
|
|
372
|
+
const canvasConfig = getConfig('canvas')
|
|
373
|
+
const agentsConfig = canvasConfig?.agents
|
|
374
|
+
if (agentsConfig && typeof agentsConfig === 'object') {
|
|
375
|
+
const agentEntries = Object.entries(agentsConfig)
|
|
376
|
+
if (agentEntries.length > 0) {
|
|
377
|
+
const pageId = 'create-widget:agents'
|
|
378
|
+
subPages.push({
|
|
379
|
+
id: pageId,
|
|
380
|
+
label: 'Add agent to canvas',
|
|
381
|
+
title: 'Add agent to canvas',
|
|
382
|
+
keywords: ['agent', 'add', 'widget', 'copilot', 'claude', 'codex'],
|
|
383
|
+
options: agentEntries.map(([id, cfg]) => ({
|
|
384
|
+
label: cfg.label || id,
|
|
385
|
+
icon: cfg.icon,
|
|
386
|
+
execute: () => {
|
|
387
|
+
document.dispatchEvent(new CustomEvent('storyboard:canvas:add-widget', {
|
|
388
|
+
detail: {
|
|
389
|
+
type: 'agent',
|
|
390
|
+
props: {
|
|
391
|
+
agentId: id,
|
|
392
|
+
startupCommand: cfg.startupCommand || id,
|
|
393
|
+
...(cfg.defaultWidth ? { width: cfg.defaultWidth } : {}),
|
|
394
|
+
...(cfg.defaultHeight ? { height: cfg.defaultHeight } : {}),
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
}))
|
|
398
|
+
},
|
|
399
|
+
})),
|
|
400
|
+
})
|
|
401
|
+
items.push({
|
|
402
|
+
id: 'create-widget:agent',
|
|
403
|
+
children: 'Agent',
|
|
404
|
+
keywords: ['add', 'widget', 'create', 'agent'],
|
|
405
|
+
itemType: 'agent',
|
|
406
|
+
hideFromSearch: true,
|
|
407
|
+
onClick: () => onNavigateToPage?.(pageId),
|
|
408
|
+
closeOnSelect: false,
|
|
409
|
+
})
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return { group: { heading: section.title, id: `cfg:${section.id}`, items }, subPages }
|
|
316
414
|
}
|
|
317
415
|
|
|
318
416
|
// --- Starred source (reads from viewfinder localStorage) ---
|
|
319
417
|
if (section.source === 'starred') {
|
|
320
|
-
const STARRED_KEY = 'sb-
|
|
418
|
+
const STARRED_KEY = 'sb-workspace-starred'
|
|
321
419
|
let starredIds = []
|
|
322
|
-
try { starredIds = JSON.parse(localStorage.getItem(STARRED_KEY)) || [] } catch {}
|
|
420
|
+
try { starredIds = JSON.parse(localStorage.getItem(STARRED_KEY)) || [] } catch { /* empty */ }
|
|
323
421
|
if (starredIds.length === 0) return null
|
|
324
422
|
|
|
325
423
|
const index = buildPrototypeIndex()
|
|
@@ -346,7 +444,8 @@ function buildDynamicSection(section, prefix, onNavigateToPage, onCreateAction)
|
|
|
346
444
|
id: `starred:${id}`,
|
|
347
445
|
children: artifact.name,
|
|
348
446
|
keywords: ['starred', 'star', artifact.name.toLowerCase()],
|
|
349
|
-
|
|
447
|
+
itemType: artifact._type === 'canvas' ? 'canvas' : 'prototype',
|
|
448
|
+
url: route,
|
|
350
449
|
onClick: () => {
|
|
351
450
|
if (artifact.isExternal) {
|
|
352
451
|
window.open(route, '_blank')
|
|
@@ -378,17 +477,20 @@ function buildDynamicSection(section, prefix, onNavigateToPage, onCreateAction)
|
|
|
378
477
|
id: `cmd:${action.id}/${child.id || child.label}`,
|
|
379
478
|
children: child.label,
|
|
380
479
|
keywords: [action.label, child.label],
|
|
480
|
+
itemType: 'command',
|
|
381
481
|
onClick: () => { if (child.execute) child.execute() },
|
|
382
482
|
})
|
|
383
483
|
}
|
|
384
484
|
} else if (action.type === 'link' && action.url) {
|
|
485
|
+
const resolvedUrl = action.url.startsWith('/') && !action.url.startsWith('//') ? prefix + action.url : action.url
|
|
385
486
|
commandItems.push({
|
|
386
487
|
id: `cmd:${action.id}`,
|
|
387
488
|
children: action.label,
|
|
388
489
|
keywords: [action.label],
|
|
490
|
+
itemType: 'link',
|
|
491
|
+
url: resolvedUrl,
|
|
389
492
|
onClick: () => {
|
|
390
|
-
|
|
391
|
-
window.location.href = url
|
|
493
|
+
window.location.href = resolvedUrl
|
|
392
494
|
},
|
|
393
495
|
})
|
|
394
496
|
} else {
|
|
@@ -396,6 +498,7 @@ function buildDynamicSection(section, prefix, onNavigateToPage, onCreateAction)
|
|
|
396
498
|
id: `cmd:${action.id}`,
|
|
397
499
|
children: action.label,
|
|
398
500
|
keywords: [action.label],
|
|
501
|
+
itemType: 'command',
|
|
399
502
|
onClick: () => executeAction(action.id),
|
|
400
503
|
})
|
|
401
504
|
}
|
|
@@ -414,16 +517,20 @@ function buildDynamicSection(section, prefix, onNavigateToPage, onCreateAction)
|
|
|
414
517
|
group: {
|
|
415
518
|
heading: section.title,
|
|
416
519
|
id: `cfg:${section.id}`,
|
|
417
|
-
items: items.map(entry =>
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
520
|
+
items: items.map(entry => {
|
|
521
|
+
const route = resolveRecentRoute(entry, prefix)
|
|
522
|
+
return {
|
|
523
|
+
id: `cfg:${section.id}:${entry.type}:${entry.key}`,
|
|
524
|
+
children: entry.label,
|
|
525
|
+
keywords: [entry.type, entry.key, entry.label],
|
|
526
|
+
itemType: entry.type,
|
|
527
|
+
url: route || undefined,
|
|
528
|
+
onClick: () => {
|
|
529
|
+
trackRecent(entry.type, entry.key, entry.label)
|
|
530
|
+
if (route) window.location.href = route
|
|
531
|
+
},
|
|
532
|
+
}
|
|
533
|
+
}),
|
|
427
534
|
},
|
|
428
535
|
}
|
|
429
536
|
}
|
|
@@ -477,6 +584,8 @@ function buildDynamicSection(section, prefix, onNavigateToPage, onCreateAction)
|
|
|
477
584
|
id: `cfg:${section.id}:${item.id}`,
|
|
478
585
|
children: item.name,
|
|
479
586
|
keywords: [item.name, item.id, item.type],
|
|
587
|
+
itemType: item.type,
|
|
588
|
+
url: item.route,
|
|
480
589
|
onClick: () => {
|
|
481
590
|
trackRecent(item.type, item.id, item.name)
|
|
482
591
|
window.location.href = item.route
|
|
@@ -489,7 +598,7 @@ function buildDynamicSection(section, prefix, onNavigateToPage, onCreateAction)
|
|
|
489
598
|
/**
|
|
490
599
|
* Build a section from toolbar.config.json tools.
|
|
491
600
|
* If toolIds is provided, only include those tools in that order (with optional custom labels).
|
|
492
|
-
* Otherwise include all command-
|
|
601
|
+
* Otherwise include all command-palette tools.
|
|
493
602
|
*
|
|
494
603
|
* toolIds format: ["theme", "flows"] or [{ id: "theme", label: "Change theme" }]
|
|
495
604
|
*/
|
|
@@ -506,20 +615,22 @@ function buildToolsSection(section, prefix, onNavigateToPage) {
|
|
|
506
615
|
for (const entry of section.toolIds) {
|
|
507
616
|
const toolId = typeof entry === 'string' ? entry : entry.id
|
|
508
617
|
const customLabel = typeof entry === 'object' ? entry.label : null
|
|
618
|
+
const closeOnSelect = typeof entry === 'object' ? entry.closeOnSelect : undefined
|
|
619
|
+
const iconMeta = typeof entry === 'object' ? entry.meta : undefined
|
|
509
620
|
const tool = tools[toolId]
|
|
510
621
|
if (!tool) continue
|
|
511
622
|
const state = getToolbarToolState(toolId)
|
|
512
623
|
if (state === 'disabled' || state === 'hidden') continue
|
|
513
624
|
if (isHiddenInPalette(tool, basePath)) continue
|
|
514
|
-
entries.push({ toolId, tool, label: customLabel || tool.label || toolId })
|
|
625
|
+
entries.push({ toolId, tool, label: customLabel || tool.label || toolId, toolIcon: tool.icon, toolMeta: iconMeta, closeOnSelect: closeOnSelect ?? tool.closeOnSelect })
|
|
515
626
|
}
|
|
516
627
|
} else {
|
|
517
628
|
for (const [toolId, tool] of Object.entries(tools)) {
|
|
518
|
-
if (tool.surface !== 'command-
|
|
629
|
+
if (tool.surface !== 'command-palette') continue
|
|
519
630
|
const state = getToolbarToolState(toolId)
|
|
520
631
|
if (state === 'disabled' || state === 'hidden') continue
|
|
521
632
|
if (isHiddenInPalette(tool, basePath)) continue
|
|
522
|
-
entries.push({ toolId, tool, label: tool.label || toolId })
|
|
633
|
+
entries.push({ toolId, tool, label: tool.label || toolId, toolIcon: tool.icon, toolMeta: undefined, closeOnSelect: tool.closeOnSelect })
|
|
523
634
|
}
|
|
524
635
|
}
|
|
525
636
|
|
|
@@ -528,14 +639,20 @@ function buildToolsSection(section, prefix, onNavigateToPage) {
|
|
|
528
639
|
const items = []
|
|
529
640
|
const subPages = []
|
|
530
641
|
|
|
531
|
-
for (const { toolId, tool, label } of entries) {
|
|
642
|
+
for (const { toolId, tool, label, closeOnSelect: entryCloseOnSelect } of entries) {
|
|
532
643
|
// Inline actions
|
|
533
644
|
if (tool.inlineAction === 'toggle-chrome') {
|
|
645
|
+
const isHidden = document.documentElement.classList.contains('storyboard-chrome-hidden')
|
|
534
646
|
items.push({
|
|
535
647
|
id: `cfg:${section.id}:${toolId}`,
|
|
536
|
-
|
|
648
|
+
toolIcon: isHidden ? 'primer/light-bulb' : 'primer/light-bulb',
|
|
649
|
+
children: <span style={{ display: 'flex', width: '100%', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
650
|
+
<span>Hide toolbars</span>
|
|
651
|
+
<span>{isHidden ? '✓' : ''}</span>
|
|
652
|
+
</span>,
|
|
537
653
|
keywords: [label, toolId, 'hide', 'show', 'toolbar'].filter(Boolean),
|
|
538
654
|
showType: false,
|
|
655
|
+
closeOnSelect: entryCloseOnSelect,
|
|
539
656
|
onClick: () => {
|
|
540
657
|
document.documentElement.classList.toggle('storyboard-chrome-hidden')
|
|
541
658
|
},
|
|
@@ -543,14 +660,29 @@ function buildToolsSection(section, prefix, onNavigateToPage) {
|
|
|
543
660
|
continue
|
|
544
661
|
}
|
|
545
662
|
|
|
663
|
+
if (tool.inlineAction === 'open-palette') {
|
|
664
|
+
items.push({
|
|
665
|
+
id: `cfg:${section.id}:${toolId}`,
|
|
666
|
+
children: label,
|
|
667
|
+
keywords: [label, toolId, 'command', 'palette', 'search'].filter(Boolean),
|
|
668
|
+
showType: false,
|
|
669
|
+
onClick: () => {
|
|
670
|
+
document.dispatchEvent(new CustomEvent('storyboard:open-palette'))
|
|
671
|
+
},
|
|
672
|
+
})
|
|
673
|
+
continue
|
|
674
|
+
}
|
|
675
|
+
|
|
546
676
|
if (tool.render === 'link' && tool.url) {
|
|
677
|
+
const resolvedUrl = tool.url.startsWith('/') ? prefix + tool.url : tool.url
|
|
547
678
|
items.push({
|
|
548
679
|
id: `cfg:${section.id}:${toolId}`,
|
|
549
680
|
children: label,
|
|
550
681
|
keywords: [label, toolId].filter(Boolean),
|
|
682
|
+
url: resolvedUrl,
|
|
683
|
+
closeOnSelect: entryCloseOnSelect,
|
|
551
684
|
onClick: () => {
|
|
552
|
-
|
|
553
|
-
window.location.href = url
|
|
685
|
+
window.location.href = resolvedUrl
|
|
554
686
|
},
|
|
555
687
|
})
|
|
556
688
|
continue
|
|
@@ -570,6 +702,8 @@ function buildToolsSection(section, prefix, onNavigateToPage) {
|
|
|
570
702
|
options: children.map(child => ({
|
|
571
703
|
label: child.label,
|
|
572
704
|
execute: child.execute,
|
|
705
|
+
type: child.type,
|
|
706
|
+
active: child.active,
|
|
573
707
|
})),
|
|
574
708
|
})
|
|
575
709
|
items.push({
|
|
@@ -611,7 +745,7 @@ function buildToolsSection(section, prefix, onNavigateToPage) {
|
|
|
611
745
|
continue
|
|
612
746
|
}
|
|
613
747
|
|
|
614
|
-
// Menu tool without sub-items or options —
|
|
748
|
+
// Menu tool without sub-items or options — dispatch open event, fall back to clicking toolbar button
|
|
615
749
|
const ariaLabel = tool.ariaLabel || tool.label || toolId
|
|
616
750
|
items.push({
|
|
617
751
|
id: `cfg:${section.id}:${toolId}`,
|
|
@@ -620,6 +754,7 @@ function buildToolsSection(section, prefix, onNavigateToPage) {
|
|
|
620
754
|
showType: false,
|
|
621
755
|
onClick: () => {
|
|
622
756
|
setTimeout(() => {
|
|
757
|
+
document.dispatchEvent(new CustomEvent(`storyboard:open-${toolId}`))
|
|
623
758
|
const btn = document.querySelector(`[aria-label="${ariaLabel}"]`)
|
|
624
759
|
if (btn) btn.click()
|
|
625
760
|
}, 100)
|
|
@@ -634,6 +769,7 @@ function buildToolsSection(section, prefix, onNavigateToPage) {
|
|
|
634
769
|
id: `cfg:${section.id}:${toolId}`,
|
|
635
770
|
children: label,
|
|
636
771
|
keywords: [label, toolId].filter(Boolean),
|
|
772
|
+
closeOnSelect: entryCloseOnSelect,
|
|
637
773
|
onClick: () => { if (action) executeAction(action.id) },
|
|
638
774
|
})
|
|
639
775
|
continue
|
|
@@ -643,10 +779,24 @@ function buildToolsSection(section, prefix, onNavigateToPage) {
|
|
|
643
779
|
id: `cfg:${section.id}:${toolId}`,
|
|
644
780
|
children: label,
|
|
645
781
|
keywords: [label, toolId].filter(Boolean),
|
|
782
|
+
closeOnSelect: entryCloseOnSelect,
|
|
646
783
|
onClick: () => executeAction(toolId),
|
|
647
784
|
})
|
|
648
785
|
}
|
|
649
786
|
|
|
787
|
+
// Add toolIcon and toolMeta to all items from their entry
|
|
788
|
+
const iconByToolId = new Map(entries.map(e => [e.toolId, { icon: e.toolIcon, meta: e.toolMeta }]))
|
|
789
|
+
for (const item of items) {
|
|
790
|
+
if (!item.toolIcon) {
|
|
791
|
+
const match = item.id?.match(/cfg:[^:]+:(.+)/)
|
|
792
|
+
if (match && iconByToolId.has(match[1])) {
|
|
793
|
+
const entry = iconByToolId.get(match[1])
|
|
794
|
+
item.toolIcon = entry.icon
|
|
795
|
+
if (!item.toolMeta && entry.meta) item.toolMeta = entry.meta
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
650
800
|
return {
|
|
651
801
|
group: {
|
|
652
802
|
heading: section.title,
|
|
@@ -722,10 +872,10 @@ function buildPaletteItems(basePath, onCreateAction, onNavigateToPage) {
|
|
|
722
872
|
const base = (basePath || '/').replace(/\/+$/, '')
|
|
723
873
|
const prefix = base === '/' ? '' : base
|
|
724
874
|
|
|
725
|
-
const { groups, toolMenus } = buildConfigSections(prefix, onNavigateToPage, onCreateAction)
|
|
875
|
+
const { groups, toolMenus, hiddenFromSearchIds } = buildConfigSections(prefix, onNavigateToPage, onCreateAction)
|
|
726
876
|
const authorIndex = buildAuthorIndex(prefix)
|
|
727
877
|
|
|
728
|
-
return { groups, toolMenus, authorIndex }
|
|
878
|
+
return { groups, toolMenus, authorIndex, hiddenFromSearchIds }
|
|
729
879
|
}
|
|
730
880
|
|
|
731
881
|
/**
|
|
@@ -738,9 +888,29 @@ export default function StoryboardCommandPalette({ basePath }) {
|
|
|
738
888
|
const [items, setItems] = useState([])
|
|
739
889
|
const [toolMenus, setToolMenus] = useState([])
|
|
740
890
|
const [authorIndex, setAuthorIndex] = useState(new Map())
|
|
891
|
+
const [hiddenFromSearchIds, setHiddenFromSearchIds] = useState(new Set())
|
|
741
892
|
const [activePage, setActivePage] = useState('root')
|
|
742
893
|
const [createType, setCreateType] = useState(null)
|
|
743
894
|
const [currentTheme, setCurrentTheme] = useState(() => getTheme())
|
|
895
|
+
const [refreshKey, setRefreshKey] = useState(0)
|
|
896
|
+
|
|
897
|
+
// Track modifier keys for link items (cmd/ctrl → new tab, alt → copy link).
|
|
898
|
+
// Updated from the most recent keyboard/mouse event via a capturing listener
|
|
899
|
+
// on the document so it fires before cmdk's own handlers.
|
|
900
|
+
const modifierHeldRef = useRef(false)
|
|
901
|
+
const altHeldRef = useRef(false)
|
|
902
|
+
useEffect(() => {
|
|
903
|
+
const track = (e) => { modifierHeldRef.current = e.metaKey || e.ctrlKey; altHeldRef.current = e.altKey }
|
|
904
|
+
const reset = () => { modifierHeldRef.current = false; altHeldRef.current = false }
|
|
905
|
+
document.addEventListener('keydown', track, true)
|
|
906
|
+
document.addEventListener('keyup', reset, true)
|
|
907
|
+
document.addEventListener('mousedown', track, true)
|
|
908
|
+
return () => {
|
|
909
|
+
document.removeEventListener('keydown', track, true)
|
|
910
|
+
document.removeEventListener('keyup', reset, true)
|
|
911
|
+
document.removeEventListener('mousedown', track, true)
|
|
912
|
+
}
|
|
913
|
+
}, [])
|
|
744
914
|
|
|
745
915
|
// Keep currentTheme in sync when theme changes
|
|
746
916
|
useEffect(() => {
|
|
@@ -759,43 +929,29 @@ export default function StoryboardCommandPalette({ basePath }) {
|
|
|
759
929
|
setActivePage(pageId)
|
|
760
930
|
}
|
|
761
931
|
|
|
762
|
-
// Listen for Cmd+K directly
|
|
763
|
-
// The Svelte CoreUIBar also handles Cmd+K by dispatching
|
|
764
|
-
// 'storyboard:toggle-palette'. We use rAF to detect if Svelte
|
|
765
|
-
// already fired the toggle event and skip to avoid double-toggle.
|
|
932
|
+
// Listen for Cmd+K directly to toggle the palette
|
|
766
933
|
useEffect(() => {
|
|
767
|
-
let toggledByEvent = false
|
|
768
|
-
|
|
769
|
-
function handleToggleEvent() {
|
|
770
|
-
toggledByEvent = true
|
|
771
|
-
}
|
|
772
|
-
|
|
773
934
|
function handleKeyDown(e) {
|
|
774
935
|
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
775
936
|
e.preventDefault()
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
setActivePage('root')
|
|
785
|
-
setOpen(prev => !prev)
|
|
786
|
-
})
|
|
937
|
+
const built = buildPaletteItems(basePath, handleCreateAction, handleNavigateToPage)
|
|
938
|
+
setItems(built.groups)
|
|
939
|
+
setToolMenus(built.toolMenus)
|
|
940
|
+
setAuthorIndex(built.authorIndex)
|
|
941
|
+
setHiddenFromSearchIds(built.hiddenFromSearchIds || new Set())
|
|
942
|
+
setSearch('')
|
|
943
|
+
setActivePage('root')
|
|
944
|
+
setOpen(prev => !prev)
|
|
787
945
|
}
|
|
788
946
|
}
|
|
789
947
|
|
|
790
|
-
document.addEventListener('storyboard:toggle-palette', handleToggleEvent)
|
|
791
948
|
document.addEventListener('keydown', handleKeyDown)
|
|
792
949
|
return () => {
|
|
793
|
-
document.removeEventListener('storyboard:toggle-palette', handleToggleEvent)
|
|
794
950
|
document.removeEventListener('keydown', handleKeyDown)
|
|
795
951
|
}
|
|
796
952
|
}, [basePath])
|
|
797
953
|
|
|
798
|
-
// Listen for toggle events from
|
|
954
|
+
// Listen for toggle/open events from toolbar buttons (e.g. CommandPaletteTrigger)
|
|
799
955
|
useEffect(() => {
|
|
800
956
|
function handleToggle() {
|
|
801
957
|
setOpen(prev => {
|
|
@@ -806,6 +962,7 @@ export default function StoryboardCommandPalette({ basePath }) {
|
|
|
806
962
|
setItems(built.groups)
|
|
807
963
|
setToolMenus(built.toolMenus)
|
|
808
964
|
setAuthorIndex(built.authorIndex)
|
|
965
|
+
setHiddenFromSearchIds(built.hiddenFromSearchIds || new Set())
|
|
809
966
|
setSearch('')
|
|
810
967
|
setActivePage('root')
|
|
811
968
|
}, 0)
|
|
@@ -819,6 +976,7 @@ export default function StoryboardCommandPalette({ basePath }) {
|
|
|
819
976
|
setItems(built.groups)
|
|
820
977
|
setToolMenus(built.toolMenus)
|
|
821
978
|
setAuthorIndex(built.authorIndex)
|
|
979
|
+
setHiddenFromSearchIds(built.hiddenFromSearchIds || new Set())
|
|
822
980
|
setSearch('')
|
|
823
981
|
setActivePage('root')
|
|
824
982
|
setOpen(true)
|
|
@@ -832,12 +990,27 @@ export default function StoryboardCommandPalette({ basePath }) {
|
|
|
832
990
|
}
|
|
833
991
|
}, [basePath])
|
|
834
992
|
|
|
993
|
+
// Rebuild palette items when a toggle is clicked (refreshKey changes)
|
|
994
|
+
useEffect(() => {
|
|
995
|
+
if (refreshKey === 0) return
|
|
996
|
+
const built = buildPaletteItems(basePath, handleCreateAction, handleNavigateToPage)
|
|
997
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
998
|
+
setItems(built.groups)
|
|
999
|
+
setToolMenus(built.toolMenus)
|
|
1000
|
+
}, [refreshKey, basePath])
|
|
1001
|
+
|
|
835
1002
|
const handleChangeOpen = useCallback((value) => {
|
|
836
1003
|
if (!value) {
|
|
1004
|
+
// Escape from a sub-page goes back to root instead of closing
|
|
1005
|
+
if (activePage !== 'root') {
|
|
1006
|
+
setActivePage('root')
|
|
1007
|
+
setSearch('')
|
|
1008
|
+
return
|
|
1009
|
+
}
|
|
837
1010
|
setOpen(false)
|
|
838
1011
|
setActivePage('root')
|
|
839
1012
|
}
|
|
840
|
-
}, [])
|
|
1013
|
+
}, [activePage])
|
|
841
1014
|
|
|
842
1015
|
// Flatten sub-page options into searchable groups so they appear in root search
|
|
843
1016
|
const subPageGroups = useMemo(() => {
|
|
@@ -846,12 +1019,13 @@ export default function StoryboardCommandPalette({ basePath }) {
|
|
|
846
1019
|
id: `subpage:${menu.id}`,
|
|
847
1020
|
items: (menu.options || []).map((opt, i) => ({
|
|
848
1021
|
id: `subpage:${menu.id}:${i}`,
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
1022
|
+
label: opt.label,
|
|
1023
|
+
icon: opt.icon,
|
|
1024
|
+
isToggle: opt.type === 'toggle',
|
|
1025
|
+
isActiveToggle: opt.type === 'toggle' && opt.active,
|
|
1026
|
+
isActiveTheme: opt.toolHandler === 'core:theme' && opt.value === currentTheme,
|
|
852
1027
|
keywords: [opt.label, menu.label || menu.id],
|
|
853
|
-
|
|
854
|
-
onClick: () => {
|
|
1028
|
+
onSelect: () => {
|
|
855
1029
|
if (opt.execute) {
|
|
856
1030
|
opt.execute()
|
|
857
1031
|
} else if (opt.toolHandler === 'core:theme' && opt.value) {
|
|
@@ -859,147 +1033,266 @@ export default function StoryboardCommandPalette({ basePath }) {
|
|
|
859
1033
|
} else if (opt.action) {
|
|
860
1034
|
executeAction(opt.action, opt.value)
|
|
861
1035
|
}
|
|
862
|
-
|
|
863
|
-
|
|
1036
|
+
if (opt.type === 'toggle') {
|
|
1037
|
+
setRefreshKey(k => k + 1)
|
|
1038
|
+
} else {
|
|
1039
|
+
setOpen(false)
|
|
1040
|
+
setActivePage('root')
|
|
1041
|
+
}
|
|
864
1042
|
},
|
|
865
1043
|
})),
|
|
866
1044
|
})).filter(g => g.items.length > 0)
|
|
867
|
-
}, [toolMenus, currentTheme])
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
const
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
// Author search: match usernames against author index
|
|
876
|
-
const q = search.toLowerCase()
|
|
877
|
-
const authorQ = q.startsWith('@') ? q.slice(1) : q
|
|
878
|
-
for (const [key, { author, items: authorItems }] of authorIndex) {
|
|
879
|
-
if (!key.includes(authorQ)) continue
|
|
880
|
-
// Avoid duplicates with already-shown artifact items
|
|
881
|
-
const shownIds = new Set(result.flatMap(g => g.items.map(i => i.id)))
|
|
882
|
-
const uniqueItems = authorItems.filter(item => !shownIds.has(`author:${item.id}`))
|
|
883
|
-
if (uniqueItems.length === 0) continue
|
|
884
|
-
result.push({
|
|
1045
|
+
}, [toolMenus, currentTheme, refreshKey])
|
|
1046
|
+
|
|
1047
|
+
// Build author groups from the index
|
|
1048
|
+
const authorGroups = useMemo(() => {
|
|
1049
|
+
const groups = []
|
|
1050
|
+
for (const [, { author, items: authorItems }] of authorIndex) {
|
|
1051
|
+
groups.push({
|
|
885
1052
|
heading: `Artifacts by @${author}`,
|
|
886
|
-
id: `author:${
|
|
887
|
-
|
|
1053
|
+
id: `author:${author.toLowerCase()}`,
|
|
1054
|
+
author,
|
|
1055
|
+
items: authorItems.map(item => ({
|
|
888
1056
|
id: `author:${item.id}`,
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
<span style={{ fontSize: '12px', color: 'var(--fgColor-muted, #999)' }}>{item.type}</span>
|
|
893
|
-
</span>
|
|
894
|
-
),
|
|
1057
|
+
label: item.name,
|
|
1058
|
+
type: item.type,
|
|
1059
|
+
url: item.route,
|
|
895
1060
|
keywords: [item.name, item.id, item.type, author, `@${author}`],
|
|
896
|
-
|
|
897
|
-
onClick: () => {
|
|
1061
|
+
onSelect: () => {
|
|
898
1062
|
trackRecent(item.type.toLowerCase(), item.id, item.name)
|
|
1063
|
+
setOpen(false)
|
|
1064
|
+
setActivePage('root')
|
|
899
1065
|
window.location.href = item.route
|
|
900
1066
|
},
|
|
901
1067
|
})),
|
|
902
1068
|
})
|
|
903
1069
|
}
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
}, [items, search, subPageGroups, authorIndex])
|
|
1070
|
+
return groups
|
|
1071
|
+
}, [authorIndex])
|
|
907
1072
|
|
|
908
1073
|
// Remove consecutive separators and leading/trailing separators
|
|
909
|
-
const
|
|
1074
|
+
const cleanedItems = useMemo(() => {
|
|
910
1075
|
const result = []
|
|
911
|
-
for (const item of
|
|
1076
|
+
for (const item of items) {
|
|
912
1077
|
const isSep = item.id?.startsWith('cfg:sep')
|
|
913
1078
|
if (isSep && (result.length === 0 || result[result.length - 1].id?.startsWith('cfg:sep'))) continue
|
|
914
1079
|
result.push(item)
|
|
915
1080
|
}
|
|
916
|
-
// Remove trailing separator
|
|
917
1081
|
while (result.length > 0 && result[result.length - 1].id?.startsWith('cfg:sep')) result.pop()
|
|
918
1082
|
return result
|
|
919
|
-
}, [
|
|
920
|
-
|
|
921
|
-
//
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
1083
|
+
}, [items])
|
|
1084
|
+
|
|
1085
|
+
// Build search value string from keywords array
|
|
1086
|
+
function itemValue(item) {
|
|
1087
|
+
const parts = []
|
|
1088
|
+
if (typeof item.children === 'string') parts.push(item.children)
|
|
1089
|
+
if (item.label) parts.push(item.label)
|
|
1090
|
+
if (item.keywords) parts.push(...item.keywords)
|
|
1091
|
+
return parts.filter(Boolean).join(' ')
|
|
1092
|
+
}
|
|
926
1093
|
|
|
927
|
-
|
|
928
|
-
|
|
1094
|
+
// Custom filter using scoreMatch for better ranking.
|
|
1095
|
+
// scoreMatch tiers: prefix (100) > word-boundary (75) > substring (50) > fuzzy (5-25).
|
|
1096
|
+
// Normalizes to 0-1 for cmdk; weak fuzzy matches (score < 10) are hidden to
|
|
1097
|
+
// prevent garbage results from dominating above exact matches in other groups.
|
|
1098
|
+
const MAX_SCORE = 110
|
|
1099
|
+
const cmdkFilter = useCallback((value, search) => {
|
|
1100
|
+
if (!search) return 1
|
|
1101
|
+
const score = scoreMatch(value, search.toLowerCase().trim())
|
|
1102
|
+
if (score < 10) return 0
|
|
1103
|
+
return Math.min(1, score / MAX_SCORE)
|
|
929
1104
|
}, [])
|
|
930
1105
|
|
|
1106
|
+
function showCenterToast(message) {
|
|
1107
|
+
const el = document.createElement('div')
|
|
1108
|
+
Object.assign(el.style, {
|
|
1109
|
+
position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%, -50%)',
|
|
1110
|
+
zIndex: '10000', padding: '0.625rem 1rem', borderRadius: '0.5rem',
|
|
1111
|
+
background: 'var(--bgColor-emphasis, #1f2328)', color: 'var(--fgColor-onEmphasis, #fff)',
|
|
1112
|
+
fontSize: '0.8125rem', fontFamily: 'var(--fontStack-sansSerif, system-ui)',
|
|
1113
|
+
opacity: '0', transition: 'opacity 0.15s ease',
|
|
1114
|
+
pointerEvents: 'none',
|
|
1115
|
+
})
|
|
1116
|
+
el.textContent = message
|
|
1117
|
+
document.body.appendChild(el)
|
|
1118
|
+
requestAnimationFrame(() => { el.style.opacity = '1' })
|
|
1119
|
+
setTimeout(() => { el.style.opacity = '0'; setTimeout(() => el.remove(), 200) }, 1800)
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
function copyLinkToClipboard(url, itemType) {
|
|
1123
|
+
const fullUrl = url.startsWith('/') ? window.location.origin + url : url
|
|
1124
|
+
const isCanvasRoute = typeof window !== 'undefined' && window.location.pathname.includes('/canvas/')
|
|
1125
|
+
const isPasteable = itemType === 'prototype' || itemType === 'story'
|
|
1126
|
+
const shouldPaste = isCanvasRoute && isPasteable
|
|
1127
|
+
|
|
1128
|
+
navigator.clipboard.writeText(fullUrl).then(() => {
|
|
1129
|
+
showCenterToast(shouldPaste ? 'Link copied and pasted' : 'Link copied to clipboard')
|
|
1130
|
+
})
|
|
1131
|
+
|
|
1132
|
+
if (shouldPaste) {
|
|
1133
|
+
document.dispatchEvent(new CustomEvent('storyboard:canvas:paste-url', { detail: { url: fullUrl } }))
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
931
1137
|
return (
|
|
932
1138
|
<>
|
|
933
|
-
<
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
1139
|
+
<Command.Dialog
|
|
1140
|
+
open={open}
|
|
1141
|
+
onOpenChange={handleChangeOpen}
|
|
1142
|
+
label="Command Menu"
|
|
1143
|
+
className="command-palette"
|
|
1144
|
+
shouldFilter={activePage === 'root'}
|
|
1145
|
+
filter={cmdkFilter}
|
|
1146
|
+
aria-describedby={undefined}
|
|
1147
|
+
onKeyDown={(e) => {
|
|
1148
|
+
if (e.key === 'Escape' && activePage !== 'root') {
|
|
1149
|
+
e.preventDefault()
|
|
1150
|
+
e.stopPropagation()
|
|
1151
|
+
setActivePage('root')
|
|
1152
|
+
setSearch('')
|
|
1153
|
+
}
|
|
1154
|
+
}}
|
|
943
1155
|
>
|
|
944
|
-
<
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
1156
|
+
<VisuallyHidden.Root asChild>
|
|
1157
|
+
<DialogPrimitive.Title>Command Menu</DialogPrimitive.Title>
|
|
1158
|
+
</VisuallyHidden.Root>
|
|
1159
|
+
<DialogPrimitive.Description className="sr-only" style={{ display: 'none' }} />
|
|
1160
|
+
<Command.Input
|
|
1161
|
+
placeholder={activePage === 'root'
|
|
1162
|
+
? 'Search commands, prototypes, canvases, stories...'
|
|
1163
|
+
: `Search ${toolMenus.find(m => m.id === activePage)?.label || ''}...`
|
|
1164
|
+
}
|
|
1165
|
+
value={search}
|
|
1166
|
+
onValueChange={setSearch}
|
|
1167
|
+
/>
|
|
1168
|
+
<Command.List>
|
|
1169
|
+
<Command.Empty>No results found.</Command.Empty>
|
|
1170
|
+
|
|
1171
|
+
{activePage === 'root' ? (
|
|
1172
|
+
<>
|
|
1173
|
+
{/* Sub-page options flattened for root search — rendered first
|
|
1174
|
+
so high-scoring items (e.g. "Copilot CLI" for query "cop")
|
|
1175
|
+
appear above weaker matches in later groups. */}
|
|
1176
|
+
{search && subPageGroups.map(group => (
|
|
1177
|
+
<Command.Group key={group.id} heading={group.heading}>
|
|
1178
|
+
{group.items.map(item => (
|
|
1179
|
+
<Command.Item
|
|
1180
|
+
key={item.id}
|
|
1181
|
+
value={itemValue(item)}
|
|
1182
|
+
onSelect={item.onSelect}
|
|
1183
|
+
>
|
|
1184
|
+
{item.icon && <Icon name={item.icon} size={ICON_SIZE} color="var(--fgColor-muted, #656d76)" />}
|
|
1185
|
+
<span style={{ display: 'flex', width: '100%', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
1186
|
+
<span>{item.label}</span>
|
|
1187
|
+
{(item.isActiveToggle || item.isActiveTheme) && <span>✓</span>}
|
|
1188
|
+
</span>
|
|
1189
|
+
</Command.Item>
|
|
957
1190
|
))}
|
|
958
|
-
</
|
|
959
|
-
)
|
|
960
|
-
|
|
1191
|
+
</Command.Group>
|
|
1192
|
+
))}
|
|
1193
|
+
|
|
1194
|
+
{/* Main config-driven groups */}
|
|
1195
|
+
{cleanedItems.map((list) => (
|
|
1196
|
+
list.id?.startsWith('cfg:sep') ? (
|
|
1197
|
+
!search && <Command.Separator key={list.id} />
|
|
1198
|
+
) : (
|
|
1199
|
+
<Command.Group key={list.id} heading={list.heading}>
|
|
1200
|
+
{list.items.map(({ id, children, keywords, onClick, itemType, toolIcon, toolMeta, closeOnSelect, hideFromSearch, url }) => {
|
|
1201
|
+
if (search && hideFromSearch) return null
|
|
1202
|
+
if (hiddenFromSearchIds.size > 0) {
|
|
1203
|
+
for (const toolId of hiddenFromSearchIds) {
|
|
1204
|
+
if (id?.includes(toolId)) return null
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
return (
|
|
1208
|
+
<Command.Item
|
|
1209
|
+
key={id}
|
|
1210
|
+
value={itemValue({ children, keywords })}
|
|
1211
|
+
onSelect={() => {
|
|
1212
|
+
if (url && altHeldRef.current) {
|
|
1213
|
+
copyLinkToClipboard(url, itemType)
|
|
1214
|
+
} else if (url && modifierHeldRef.current) {
|
|
1215
|
+
window.open(url, '_blank')
|
|
1216
|
+
} else {
|
|
1217
|
+
onClick?.()
|
|
1218
|
+
}
|
|
1219
|
+
if (closeOnSelect !== false) {
|
|
1220
|
+
setOpen(false)
|
|
1221
|
+
setActivePage('root')
|
|
1222
|
+
}
|
|
1223
|
+
}}
|
|
1224
|
+
>
|
|
1225
|
+
<ItemIcon type={itemType} toolIcon={toolIcon} toolMeta={toolMeta} />
|
|
1226
|
+
{children}
|
|
1227
|
+
</Command.Item>
|
|
1228
|
+
)
|
|
1229
|
+
})}
|
|
1230
|
+
</Command.Group>
|
|
1231
|
+
)
|
|
1232
|
+
))}
|
|
1233
|
+
|
|
1234
|
+
{/* Author groups */}
|
|
1235
|
+
{search && authorGroups.map(group => (
|
|
1236
|
+
<Command.Group key={group.id} heading={group.heading}>
|
|
1237
|
+
{group.items.map(item => (
|
|
1238
|
+
<Command.Item
|
|
1239
|
+
key={item.id}
|
|
1240
|
+
value={itemValue(item)}
|
|
1241
|
+
onSelect={() => {
|
|
1242
|
+
if (item.url && altHeldRef.current) {
|
|
1243
|
+
copyLinkToClipboard(item.url, item.type?.toLowerCase())
|
|
1244
|
+
setOpen(false)
|
|
1245
|
+
setActivePage('root')
|
|
1246
|
+
} else if (item.url && modifierHeldRef.current) {
|
|
1247
|
+
window.open(item.url, '_blank')
|
|
1248
|
+
setOpen(false)
|
|
1249
|
+
setActivePage('root')
|
|
1250
|
+
} else {
|
|
1251
|
+
item.onSelect()
|
|
1252
|
+
}
|
|
1253
|
+
}}
|
|
1254
|
+
>
|
|
1255
|
+
<AvatarIcon username={group.author} />
|
|
1256
|
+
<span style={{ display: 'flex', width: '100%', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
1257
|
+
<span>{item.label}</span>
|
|
1258
|
+
<span style={{ fontSize: '12px', color: 'var(--fgColor-muted, #999)' }}>{item.type}</span>
|
|
1259
|
+
</span>
|
|
1260
|
+
</Command.Item>
|
|
1261
|
+
))}
|
|
1262
|
+
</Command.Group>
|
|
1263
|
+
))}
|
|
1264
|
+
</>
|
|
961
1265
|
) : (
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
1266
|
+
/* Tool-menu sub-pages */
|
|
1267
|
+
toolMenus.filter(menu => menu.id === activePage).map(menu => (
|
|
1268
|
+
<Command.Group key={menu.id} heading={menu.title || menu.label || menu.id}>
|
|
1269
|
+
{(menu.options || []).map((opt, i) => (
|
|
1270
|
+
<Command.Item
|
|
1271
|
+
key={`${menu.id}:${i}`}
|
|
1272
|
+
value={opt.label}
|
|
1273
|
+
onSelect={() => {
|
|
1274
|
+
if (opt.execute) {
|
|
1275
|
+
opt.execute()
|
|
1276
|
+
} else if (opt.toolHandler === 'core:theme' && opt.value) {
|
|
1277
|
+
setTheme(opt.value)
|
|
1278
|
+
} else if (opt.action) {
|
|
1279
|
+
executeAction(opt.action, opt.value)
|
|
1280
|
+
}
|
|
1281
|
+
setOpen(false)
|
|
1282
|
+
setActivePage('root')
|
|
1283
|
+
}}
|
|
1284
|
+
>
|
|
1285
|
+
{opt.icon && <Icon name={opt.icon} size={ICON_SIZE} color="var(--fgColor-muted, #656d76)" />}
|
|
1286
|
+
{opt.toolHandler === 'core:theme' && opt.value === currentTheme
|
|
1287
|
+
? <span style={{ display: 'flex', width: '100%', justifyContent: 'space-between', alignItems: 'center' }}><span>{opt.label}</span><span>✓</span></span>
|
|
1288
|
+
: opt.label}
|
|
1289
|
+
</Command.Item>
|
|
1290
|
+
))}
|
|
1291
|
+
</Command.Group>
|
|
1292
|
+
))
|
|
965
1293
|
)}
|
|
966
|
-
</
|
|
967
|
-
|
|
968
|
-
{/* Tool-menu sub-pages */}
|
|
969
|
-
{toolMenus.map(menu => (
|
|
970
|
-
<CommandPalette.Page
|
|
971
|
-
key={menu.id}
|
|
972
|
-
id={menu.id}
|
|
973
|
-
onEscape={() => { setActivePage('root'); setSearch('') }}
|
|
974
|
-
searchPrefix={[menu.label || menu.id]}
|
|
975
|
-
>
|
|
976
|
-
<CommandPalette.List heading={menu.title || menu.label || menu.id}>
|
|
977
|
-
{(menu.options || []).map((opt, i) => (
|
|
978
|
-
<CommandPalette.ListItem
|
|
979
|
-
key={`${menu.id}:${i}`}
|
|
980
|
-
index={i}
|
|
981
|
-
showType={false}
|
|
982
|
-
onClick={() => {
|
|
983
|
-
if (opt.execute) {
|
|
984
|
-
opt.execute()
|
|
985
|
-
} else if (opt.toolHandler === 'core:theme' && opt.value) {
|
|
986
|
-
setTheme(opt.value)
|
|
987
|
-
} else if (opt.action) {
|
|
988
|
-
executeAction(opt.action, opt.value)
|
|
989
|
-
}
|
|
990
|
-
setOpen(false)
|
|
991
|
-
setActivePage('root')
|
|
992
|
-
}}
|
|
993
|
-
>
|
|
994
|
-
{opt.toolHandler === 'core:theme' && opt.value === currentTheme
|
|
995
|
-
? <span style={{ display: 'flex', width: '100%', justifyContent: 'space-between', alignItems: 'center' }}><span>{opt.label}</span><span>✓</span></span>
|
|
996
|
-
: opt.label}
|
|
997
|
-
</CommandPalette.ListItem>
|
|
998
|
-
))}
|
|
999
|
-
</CommandPalette.List>
|
|
1000
|
-
</CommandPalette.Page>
|
|
1001
|
-
))}
|
|
1002
|
-
</CommandPalette>
|
|
1294
|
+
</Command.List>
|
|
1295
|
+
</Command.Dialog>
|
|
1003
1296
|
|
|
1004
1297
|
<CreateDialog
|
|
1005
1298
|
type={createType}
|