@dfosco/storyboard-react 4.2.0-beta.17 → 4.2.0-beta.19
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 +7 -3
- package/src/BranchBar/BranchBar.jsx +3 -1
- package/src/BranchBar/BranchBar.module.css +2 -2
- package/src/BranchBar/useBranches.js +20 -6
- package/src/BranchBar/useBranches.test.js +68 -0
- package/src/CommandPalette/CommandPalette.jsx +250 -61
- package/src/CommandPalette/command-palette.css +12 -0
- package/src/Icon.jsx +46 -11
- package/src/Viewfinder.jsx +53 -133
- package/src/Viewfinder.module.css +20 -91
- package/src/Workspace.jsx +7 -0
- package/src/canvas/CanvasPage.jsx +601 -62
- package/src/canvas/CanvasPage.module.css +15 -2
- package/src/canvas/CanvasPage.multiselect.test.jsx +7 -0
- package/src/canvas/ConnectorLayer.jsx +120 -152
- package/src/canvas/ConnectorLayer.module.css +69 -0
- package/src/canvas/canvasApi.js +68 -2
- package/src/canvas/connectorGeometry.js +132 -0
- package/src/canvas/hotPoolDevLogs.js +25 -0
- 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 +472 -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 +49 -102
- package/src/canvas/widgets/ImageWidget.jsx +129 -8
- package/src/canvas/widgets/ImageWidget.module.css +30 -0
- package/src/canvas/widgets/LinkPreview.jsx +93 -44
- package/src/canvas/widgets/MarkdownBlock.jsx +141 -16
- package/src/canvas/widgets/MarkdownBlock.module.css +25 -0
- package/src/canvas/widgets/PromptWidget.jsx +414 -0
- package/src/canvas/widgets/PromptWidget.module.css +273 -0
- package/src/canvas/widgets/PrototypeEmbed.jsx +46 -170
- package/src/canvas/widgets/ResizeHandle.jsx +17 -6
- package/src/canvas/widgets/StoryWidget.jsx +65 -11
- package/src/canvas/widgets/TerminalReadWidget.jsx +11 -5
- package/src/canvas/widgets/TerminalReadWidget.module.css +3 -1
- package/src/canvas/widgets/TerminalWidget.jsx +301 -124
- package/src/canvas/widgets/TerminalWidget.module.css +121 -12
- package/src/canvas/widgets/TilesWidget.jsx +302 -0
- package/src/canvas/widgets/TilesWidget.module.css +133 -0
- package/src/canvas/widgets/WidgetChrome.jsx +67 -152
- package/src/canvas/widgets/WidgetChrome.module.css +20 -1
- package/src/canvas/widgets/expandUtils.js +385 -16
- package/src/canvas/widgets/expandUtils.test.js +155 -0
- package/src/canvas/widgets/index.js +6 -2
- 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 +37 -4
- package/src/canvas/widgets/widgetIcons.jsx +190 -0
- package/src/canvas/widgets/widgetProps.js +1 -0
- package/src/context.jsx +47 -19
- package/src/hooks/usePrototypeReloadGuard.js +64 -0
- package/src/index.js +4 -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 +79 -35
- package/src/canvas/widgets/ActionWidget.jsx +0 -200
- package/src/canvas/widgets/ActionWidget.module.css +0 -122
- package/src/canvas/widgets/SplitExpandModal.jsx +0 -234
- package/src/canvas/widgets/SplitExpandModal.module.css +0 -335
- package/src/canvas/widgets/SplitScreenTopBar.jsx +0 -30
- package/src/canvas/widgets/SplitScreenTopBar.module.css +0 -58
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback, useMemo } from 'react'
|
|
1
|
+
import { useState, useEffect, useCallback, useMemo, useRef } from 'react'
|
|
2
2
|
import { Command } from 'cmdk'
|
|
3
|
+
import * as VisuallyHidden from '@radix-ui/react-visually-hidden'
|
|
4
|
+
import * as DialogPrimitive from '@radix-ui/react-dialog'
|
|
3
5
|
import Icon from '../Icon.jsx'
|
|
4
6
|
import {
|
|
5
7
|
buildPrototypeIndex,
|
|
@@ -14,9 +16,11 @@ import {
|
|
|
14
16
|
trackRecent,
|
|
15
17
|
getCommandPaletteConfig,
|
|
16
18
|
getToolbarConfig,
|
|
19
|
+
getConfig,
|
|
17
20
|
setTheme,
|
|
18
21
|
getTheme,
|
|
19
22
|
isExcludedByRoute,
|
|
23
|
+
scoreMatch,
|
|
20
24
|
} from '@dfosco/storyboard-core'
|
|
21
25
|
import { widgetTypes } from '../canvas/widgets/widgetConfig.js'
|
|
22
26
|
import CreateDialog from './CreateDialog.jsx'
|
|
@@ -32,10 +36,12 @@ function getIconMap() {
|
|
|
32
36
|
return config?.icons || {}
|
|
33
37
|
}
|
|
34
38
|
|
|
35
|
-
function ItemIcon({ type, toolIcon }) {
|
|
39
|
+
function ItemIcon({ type, toolIcon, toolMeta }) {
|
|
36
40
|
const icons = getIconMap()
|
|
37
|
-
const
|
|
38
|
-
|
|
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 || {})} />
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
function AvatarIcon({ username }) {
|
|
@@ -116,13 +122,14 @@ function buildConfigSections(prefix, onNavigateToPage, onCreateAction) {
|
|
|
116
122
|
items: section.items.map((item, i) => {
|
|
117
123
|
const id = `cfg:${section.id}:${i}`
|
|
118
124
|
if (item.type === 'link') {
|
|
125
|
+
const resolvedUrl = item.url?.startsWith('/') ? prefix + item.url : item.url
|
|
119
126
|
return {
|
|
120
127
|
id,
|
|
121
128
|
children: item.label,
|
|
122
129
|
keywords: item.keywords || [item.label],
|
|
130
|
+
url: resolvedUrl,
|
|
123
131
|
onClick: () => {
|
|
124
|
-
|
|
125
|
-
if (url) window.location.href = url
|
|
132
|
+
if (resolvedUrl) window.location.href = resolvedUrl
|
|
126
133
|
},
|
|
127
134
|
}
|
|
128
135
|
}
|
|
@@ -237,12 +244,14 @@ function buildConfigSections(prefix, onNavigateToPage, onCreateAction) {
|
|
|
237
244
|
|
|
238
245
|
// Any remaining tools (all surfaces)
|
|
239
246
|
if (tool.render === 'link' && tool.url) {
|
|
247
|
+
const resolvedUrl = tool.url.startsWith('/') ? prefix + tool.url : tool.url
|
|
240
248
|
remainingItems.push({
|
|
241
249
|
id: `cfg:${section.id}:${toolId}`,
|
|
242
250
|
children: label,
|
|
243
251
|
keywords: [label, toolId].filter(Boolean),
|
|
244
252
|
showType: false,
|
|
245
|
-
|
|
253
|
+
url: resolvedUrl,
|
|
254
|
+
onClick: () => { window.location.href = resolvedUrl },
|
|
246
255
|
})
|
|
247
256
|
} else {
|
|
248
257
|
// Menu tools: close palette and click the toolbar button to open the menu
|
|
@@ -283,9 +292,10 @@ function buildConfigSections(prefix, onNavigateToPage, onCreateAction) {
|
|
|
283
292
|
}
|
|
284
293
|
}
|
|
285
294
|
|
|
286
|
-
// Also include any
|
|
295
|
+
// Also include any tool sub-pages not yet listed (skip non-tool sub-pages like create-widget)
|
|
287
296
|
for (const menu of toolMenus) {
|
|
288
|
-
|
|
297
|
+
if (!menu.id?.startsWith('tool:')) continue
|
|
298
|
+
const menuToolId = menu.id.replace('tool:', '')
|
|
289
299
|
if (usedToolIds.has(menuToolId)) continue
|
|
290
300
|
if (remainingItems.some(i => i.id === `cfg:${section.id}:${menuToolId}`)) continue
|
|
291
301
|
remainingItems.push({
|
|
@@ -299,6 +309,18 @@ function buildConfigSections(prefix, onNavigateToPage, onCreateAction) {
|
|
|
299
309
|
}
|
|
300
310
|
|
|
301
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
|
+
|
|
302
324
|
groups.push({
|
|
303
325
|
heading: section.title,
|
|
304
326
|
id: `cfg:${section.id}`,
|
|
@@ -334,22 +356,66 @@ function buildDynamicSection(section, prefix, onNavigateToPage, onCreateAction)
|
|
|
334
356
|
if (!isLocalDev) return null
|
|
335
357
|
const isCanvasRoute = typeof window !== 'undefined' && window.location.pathname.includes('/canvas/')
|
|
336
358
|
if (!isCanvasRoute) return null
|
|
337
|
-
const hiddenTypes = new Set(['link-preview', 'image', 'figma-embed', 'codepen-embed', 'story', 'terminal-read'])
|
|
359
|
+
const hiddenTypes = new Set(['link-preview', 'image', 'figma-embed', 'codepen-embed', 'story', 'terminal-read', 'agent'])
|
|
338
360
|
const items = Object.entries(widgetTypes).filter(([type]) => !hiddenTypes.has(type)).map(([type, def]) => ({
|
|
339
361
|
id: `create-widget:${type}`,
|
|
340
362
|
children: def.label,
|
|
341
363
|
keywords: ['add', 'widget', 'create', type, def.label.toLowerCase()],
|
|
342
|
-
itemType:
|
|
364
|
+
itemType: type,
|
|
343
365
|
onClick: () => {
|
|
344
366
|
document.dispatchEvent(new CustomEvent('storyboard:canvas:add-widget', { detail: { type } }))
|
|
345
367
|
},
|
|
346
368
|
}))
|
|
347
|
-
|
|
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 }
|
|
348
414
|
}
|
|
349
415
|
|
|
350
416
|
// --- Starred source (reads from viewfinder localStorage) ---
|
|
351
417
|
if (section.source === 'starred') {
|
|
352
|
-
const STARRED_KEY = 'sb-
|
|
418
|
+
const STARRED_KEY = 'sb-workspace-starred'
|
|
353
419
|
let starredIds = []
|
|
354
420
|
try { starredIds = JSON.parse(localStorage.getItem(STARRED_KEY)) || [] } catch {}
|
|
355
421
|
if (starredIds.length === 0) return null
|
|
@@ -379,6 +445,7 @@ function buildDynamicSection(section, prefix, onNavigateToPage, onCreateAction)
|
|
|
379
445
|
children: artifact.name,
|
|
380
446
|
keywords: ['starred', 'star', artifact.name.toLowerCase()],
|
|
381
447
|
itemType: artifact._type === 'canvas' ? 'canvas' : 'prototype',
|
|
448
|
+
url: route,
|
|
382
449
|
onClick: () => {
|
|
383
450
|
if (artifact.isExternal) {
|
|
384
451
|
window.open(route, '_blank')
|
|
@@ -415,14 +482,15 @@ function buildDynamicSection(section, prefix, onNavigateToPage, onCreateAction)
|
|
|
415
482
|
})
|
|
416
483
|
}
|
|
417
484
|
} else if (action.type === 'link' && action.url) {
|
|
485
|
+
const resolvedUrl = action.url.startsWith('/') && !action.url.startsWith('//') ? prefix + action.url : action.url
|
|
418
486
|
commandItems.push({
|
|
419
487
|
id: `cmd:${action.id}`,
|
|
420
488
|
children: action.label,
|
|
421
489
|
keywords: [action.label],
|
|
422
490
|
itemType: 'link',
|
|
491
|
+
url: resolvedUrl,
|
|
423
492
|
onClick: () => {
|
|
424
|
-
|
|
425
|
-
window.location.href = url
|
|
493
|
+
window.location.href = resolvedUrl
|
|
426
494
|
},
|
|
427
495
|
})
|
|
428
496
|
} else {
|
|
@@ -449,17 +517,20 @@ function buildDynamicSection(section, prefix, onNavigateToPage, onCreateAction)
|
|
|
449
517
|
group: {
|
|
450
518
|
heading: section.title,
|
|
451
519
|
id: `cfg:${section.id}`,
|
|
452
|
-
items: items.map(entry =>
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
+
}),
|
|
463
534
|
},
|
|
464
535
|
}
|
|
465
536
|
}
|
|
@@ -514,6 +585,7 @@ function buildDynamicSection(section, prefix, onNavigateToPage, onCreateAction)
|
|
|
514
585
|
children: item.name,
|
|
515
586
|
keywords: [item.name, item.id, item.type],
|
|
516
587
|
itemType: item.type,
|
|
588
|
+
url: item.route,
|
|
517
589
|
onClick: () => {
|
|
518
590
|
trackRecent(item.type, item.id, item.name)
|
|
519
591
|
window.location.href = item.route
|
|
@@ -543,12 +615,14 @@ function buildToolsSection(section, prefix, onNavigateToPage) {
|
|
|
543
615
|
for (const entry of section.toolIds) {
|
|
544
616
|
const toolId = typeof entry === 'string' ? entry : entry.id
|
|
545
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
|
|
546
620
|
const tool = tools[toolId]
|
|
547
621
|
if (!tool) continue
|
|
548
622
|
const state = getToolbarToolState(toolId)
|
|
549
623
|
if (state === 'disabled' || state === 'hidden') continue
|
|
550
624
|
if (isHiddenInPalette(tool, basePath)) continue
|
|
551
|
-
entries.push({ toolId, tool, label: customLabel || tool.label || toolId, toolIcon: tool.icon })
|
|
625
|
+
entries.push({ toolId, tool, label: customLabel || tool.label || toolId, toolIcon: tool.icon, toolMeta: iconMeta, closeOnSelect: closeOnSelect ?? tool.closeOnSelect })
|
|
552
626
|
}
|
|
553
627
|
} else {
|
|
554
628
|
for (const [toolId, tool] of Object.entries(tools)) {
|
|
@@ -556,7 +630,7 @@ function buildToolsSection(section, prefix, onNavigateToPage) {
|
|
|
556
630
|
const state = getToolbarToolState(toolId)
|
|
557
631
|
if (state === 'disabled' || state === 'hidden') continue
|
|
558
632
|
if (isHiddenInPalette(tool, basePath)) continue
|
|
559
|
-
entries.push({ toolId, tool, label: tool.label || toolId, toolIcon: tool.icon })
|
|
633
|
+
entries.push({ toolId, tool, label: tool.label || toolId, toolIcon: tool.icon, toolMeta: undefined, closeOnSelect: tool.closeOnSelect })
|
|
560
634
|
}
|
|
561
635
|
}
|
|
562
636
|
|
|
@@ -565,22 +639,22 @@ function buildToolsSection(section, prefix, onNavigateToPage) {
|
|
|
565
639
|
const items = []
|
|
566
640
|
const subPages = []
|
|
567
641
|
|
|
568
|
-
for (const { toolId, tool, label, toolIcon } of entries) {
|
|
642
|
+
for (const { toolId, tool, label, toolIcon, toolMeta, closeOnSelect: entryCloseOnSelect } of entries) {
|
|
569
643
|
// Inline actions
|
|
570
644
|
if (tool.inlineAction === 'toggle-chrome') {
|
|
571
645
|
const isHidden = document.documentElement.classList.contains('storyboard-chrome-hidden')
|
|
572
646
|
items.push({
|
|
573
647
|
id: `cfg:${section.id}:${toolId}`,
|
|
574
|
-
toolIcon,
|
|
648
|
+
toolIcon: isHidden ? 'primer/light-bulb' : 'primer/light-bulb',
|
|
575
649
|
children: <span style={{ display: 'flex', width: '100%', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
576
|
-
<span>
|
|
650
|
+
<span>Hide toolbars</span>
|
|
577
651
|
<span>{isHidden ? '✓' : ''}</span>
|
|
578
652
|
</span>,
|
|
579
653
|
keywords: [label, toolId, 'hide', 'show', 'toolbar'].filter(Boolean),
|
|
580
654
|
showType: false,
|
|
655
|
+
closeOnSelect: entryCloseOnSelect,
|
|
581
656
|
onClick: () => {
|
|
582
657
|
document.documentElement.classList.toggle('storyboard-chrome-hidden')
|
|
583
|
-
setRefreshKey(k => k + 1)
|
|
584
658
|
},
|
|
585
659
|
})
|
|
586
660
|
continue
|
|
@@ -600,13 +674,15 @@ function buildToolsSection(section, prefix, onNavigateToPage) {
|
|
|
600
674
|
}
|
|
601
675
|
|
|
602
676
|
if (tool.render === 'link' && tool.url) {
|
|
677
|
+
const resolvedUrl = tool.url.startsWith('/') ? prefix + tool.url : tool.url
|
|
603
678
|
items.push({
|
|
604
679
|
id: `cfg:${section.id}:${toolId}`,
|
|
605
680
|
children: label,
|
|
606
681
|
keywords: [label, toolId].filter(Boolean),
|
|
682
|
+
url: resolvedUrl,
|
|
683
|
+
closeOnSelect: entryCloseOnSelect,
|
|
607
684
|
onClick: () => {
|
|
608
|
-
|
|
609
|
-
window.location.href = url
|
|
685
|
+
window.location.href = resolvedUrl
|
|
610
686
|
},
|
|
611
687
|
})
|
|
612
688
|
continue
|
|
@@ -669,7 +745,7 @@ function buildToolsSection(section, prefix, onNavigateToPage) {
|
|
|
669
745
|
continue
|
|
670
746
|
}
|
|
671
747
|
|
|
672
|
-
// Menu tool without sub-items or options —
|
|
748
|
+
// Menu tool without sub-items or options — dispatch open event, fall back to clicking toolbar button
|
|
673
749
|
const ariaLabel = tool.ariaLabel || tool.label || toolId
|
|
674
750
|
items.push({
|
|
675
751
|
id: `cfg:${section.id}:${toolId}`,
|
|
@@ -678,6 +754,7 @@ function buildToolsSection(section, prefix, onNavigateToPage) {
|
|
|
678
754
|
showType: false,
|
|
679
755
|
onClick: () => {
|
|
680
756
|
setTimeout(() => {
|
|
757
|
+
document.dispatchEvent(new CustomEvent(`storyboard:open-${toolId}`))
|
|
681
758
|
const btn = document.querySelector(`[aria-label="${ariaLabel}"]`)
|
|
682
759
|
if (btn) btn.click()
|
|
683
760
|
}, 100)
|
|
@@ -692,6 +769,7 @@ function buildToolsSection(section, prefix, onNavigateToPage) {
|
|
|
692
769
|
id: `cfg:${section.id}:${toolId}`,
|
|
693
770
|
children: label,
|
|
694
771
|
keywords: [label, toolId].filter(Boolean),
|
|
772
|
+
closeOnSelect: entryCloseOnSelect,
|
|
695
773
|
onClick: () => { if (action) executeAction(action.id) },
|
|
696
774
|
})
|
|
697
775
|
continue
|
|
@@ -701,16 +779,21 @@ function buildToolsSection(section, prefix, onNavigateToPage) {
|
|
|
701
779
|
id: `cfg:${section.id}:${toolId}`,
|
|
702
780
|
children: label,
|
|
703
781
|
keywords: [label, toolId].filter(Boolean),
|
|
782
|
+
closeOnSelect: entryCloseOnSelect,
|
|
704
783
|
onClick: () => executeAction(toolId),
|
|
705
784
|
})
|
|
706
785
|
}
|
|
707
786
|
|
|
708
|
-
// Add toolIcon to all items from their entry
|
|
709
|
-
const iconByToolId = new Map(entries.map(e => [e.toolId, e.toolIcon]))
|
|
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 }]))
|
|
710
789
|
for (const item of items) {
|
|
711
790
|
if (!item.toolIcon) {
|
|
712
791
|
const match = item.id?.match(/cfg:[^:]+:(.+)/)
|
|
713
|
-
if (match && iconByToolId.has(match[1]))
|
|
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
|
+
}
|
|
714
797
|
}
|
|
715
798
|
}
|
|
716
799
|
|
|
@@ -811,6 +894,24 @@ export default function StoryboardCommandPalette({ basePath }) {
|
|
|
811
894
|
const [currentTheme, setCurrentTheme] = useState(() => getTheme())
|
|
812
895
|
const [refreshKey, setRefreshKey] = useState(0)
|
|
813
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
|
+
}, [])
|
|
914
|
+
|
|
814
915
|
// Keep currentTheme in sync when theme changes
|
|
815
916
|
useEffect(() => {
|
|
816
917
|
const handler = (e) => setCurrentTheme(e.detail.theme)
|
|
@@ -899,10 +1000,16 @@ export default function StoryboardCommandPalette({ basePath }) {
|
|
|
899
1000
|
|
|
900
1001
|
const handleChangeOpen = useCallback((value) => {
|
|
901
1002
|
if (!value) {
|
|
1003
|
+
// Escape from a sub-page goes back to root instead of closing
|
|
1004
|
+
if (activePage !== 'root') {
|
|
1005
|
+
setActivePage('root')
|
|
1006
|
+
setSearch('')
|
|
1007
|
+
return
|
|
1008
|
+
}
|
|
902
1009
|
setOpen(false)
|
|
903
1010
|
setActivePage('root')
|
|
904
1011
|
}
|
|
905
|
-
}, [])
|
|
1012
|
+
}, [activePage])
|
|
906
1013
|
|
|
907
1014
|
// Flatten sub-page options into searchable groups so they appear in root search
|
|
908
1015
|
const subPageGroups = useMemo(() => {
|
|
@@ -912,6 +1019,7 @@ export default function StoryboardCommandPalette({ basePath }) {
|
|
|
912
1019
|
items: (menu.options || []).map((opt, i) => ({
|
|
913
1020
|
id: `subpage:${menu.id}:${i}`,
|
|
914
1021
|
label: opt.label,
|
|
1022
|
+
icon: opt.icon,
|
|
915
1023
|
isToggle: opt.type === 'toggle',
|
|
916
1024
|
isActiveToggle: opt.type === 'toggle' && opt.active,
|
|
917
1025
|
isActiveTheme: opt.toolHandler === 'core:theme' && opt.value === currentTheme,
|
|
@@ -947,9 +1055,12 @@ export default function StoryboardCommandPalette({ basePath }) {
|
|
|
947
1055
|
id: `author:${item.id}`,
|
|
948
1056
|
label: item.name,
|
|
949
1057
|
type: item.type,
|
|
1058
|
+
url: item.route,
|
|
950
1059
|
keywords: [item.name, item.id, item.type, author, `@${author}`],
|
|
951
1060
|
onSelect: () => {
|
|
952
1061
|
trackRecent(item.type.toLowerCase(), item.id, item.name)
|
|
1062
|
+
setOpen(false)
|
|
1063
|
+
setActivePage('root')
|
|
953
1064
|
window.location.href = item.route
|
|
954
1065
|
},
|
|
955
1066
|
})),
|
|
@@ -979,6 +1090,49 @@ export default function StoryboardCommandPalette({ basePath }) {
|
|
|
979
1090
|
return parts.filter(Boolean).join(' ')
|
|
980
1091
|
}
|
|
981
1092
|
|
|
1093
|
+
// Custom filter using scoreMatch for better ranking.
|
|
1094
|
+
// scoreMatch tiers: prefix (100) > word-boundary (75) > substring (50) > fuzzy (5-25).
|
|
1095
|
+
// Normalizes to 0-1 for cmdk; weak fuzzy matches (score < 10) are hidden to
|
|
1096
|
+
// prevent garbage results from dominating above exact matches in other groups.
|
|
1097
|
+
const MAX_SCORE = 110
|
|
1098
|
+
const cmdkFilter = useCallback((value, search) => {
|
|
1099
|
+
if (!search) return 1
|
|
1100
|
+
const score = scoreMatch(value, search.toLowerCase().trim())
|
|
1101
|
+
if (score < 10) return 0
|
|
1102
|
+
return Math.min(1, score / MAX_SCORE)
|
|
1103
|
+
}, [])
|
|
1104
|
+
|
|
1105
|
+
function showCenterToast(message) {
|
|
1106
|
+
const el = document.createElement('div')
|
|
1107
|
+
Object.assign(el.style, {
|
|
1108
|
+
position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%, -50%)',
|
|
1109
|
+
zIndex: '10000', padding: '0.625rem 1rem', borderRadius: '0.5rem',
|
|
1110
|
+
background: 'var(--bgColor-emphasis, #1f2328)', color: 'var(--fgColor-onEmphasis, #fff)',
|
|
1111
|
+
fontSize: '0.8125rem', fontFamily: 'var(--fontStack-sansSerif, system-ui)',
|
|
1112
|
+
opacity: '0', transition: 'opacity 0.15s ease',
|
|
1113
|
+
pointerEvents: 'none',
|
|
1114
|
+
})
|
|
1115
|
+
el.textContent = message
|
|
1116
|
+
document.body.appendChild(el)
|
|
1117
|
+
requestAnimationFrame(() => { el.style.opacity = '1' })
|
|
1118
|
+
setTimeout(() => { el.style.opacity = '0'; setTimeout(() => el.remove(), 200) }, 1800)
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
function copyLinkToClipboard(url, itemType) {
|
|
1122
|
+
const fullUrl = url.startsWith('/') ? window.location.origin + url : url
|
|
1123
|
+
const isCanvasRoute = typeof window !== 'undefined' && window.location.pathname.includes('/canvas/')
|
|
1124
|
+
const isPasteable = itemType === 'prototype' || itemType === 'story'
|
|
1125
|
+
const shouldPaste = isCanvasRoute && isPasteable
|
|
1126
|
+
|
|
1127
|
+
navigator.clipboard.writeText(fullUrl).then(() => {
|
|
1128
|
+
showCenterToast(shouldPaste ? 'Link copied and pasted' : 'Link copied to clipboard')
|
|
1129
|
+
})
|
|
1130
|
+
|
|
1131
|
+
if (shouldPaste) {
|
|
1132
|
+
document.dispatchEvent(new CustomEvent('storyboard:canvas:paste-url', { detail: { url: fullUrl } }))
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
982
1136
|
return (
|
|
983
1137
|
<>
|
|
984
1138
|
<Command.Dialog
|
|
@@ -987,6 +1141,8 @@ export default function StoryboardCommandPalette({ basePath }) {
|
|
|
987
1141
|
label="Command Menu"
|
|
988
1142
|
className="command-palette"
|
|
989
1143
|
shouldFilter={activePage === 'root'}
|
|
1144
|
+
filter={cmdkFilter}
|
|
1145
|
+
aria-describedby={undefined}
|
|
990
1146
|
onKeyDown={(e) => {
|
|
991
1147
|
if (e.key === 'Escape' && activePage !== 'root') {
|
|
992
1148
|
e.preventDefault()
|
|
@@ -996,6 +1152,10 @@ export default function StoryboardCommandPalette({ basePath }) {
|
|
|
996
1152
|
}
|
|
997
1153
|
}}
|
|
998
1154
|
>
|
|
1155
|
+
<VisuallyHidden.Root asChild>
|
|
1156
|
+
<DialogPrimitive.Title>Command Menu</DialogPrimitive.Title>
|
|
1157
|
+
</VisuallyHidden.Root>
|
|
1158
|
+
<DialogPrimitive.Description className="sr-only" style={{ display: 'none' }} />
|
|
999
1159
|
<Command.Input
|
|
1000
1160
|
placeholder={activePage === 'root'
|
|
1001
1161
|
? 'Search commands, prototypes, canvases, stories...'
|
|
@@ -1009,13 +1169,35 @@ export default function StoryboardCommandPalette({ basePath }) {
|
|
|
1009
1169
|
|
|
1010
1170
|
{activePage === 'root' ? (
|
|
1011
1171
|
<>
|
|
1172
|
+
{/* Sub-page options flattened for root search — rendered first
|
|
1173
|
+
so high-scoring items (e.g. "Copilot CLI" for query "cop")
|
|
1174
|
+
appear above weaker matches in later groups. */}
|
|
1175
|
+
{search && subPageGroups.map(group => (
|
|
1176
|
+
<Command.Group key={group.id} heading={group.heading}>
|
|
1177
|
+
{group.items.map(item => (
|
|
1178
|
+
<Command.Item
|
|
1179
|
+
key={item.id}
|
|
1180
|
+
value={itemValue(item)}
|
|
1181
|
+
onSelect={item.onSelect}
|
|
1182
|
+
>
|
|
1183
|
+
{item.icon && <Icon name={item.icon} size={ICON_SIZE} color="var(--fgColor-muted, #656d76)" />}
|
|
1184
|
+
<span style={{ display: 'flex', width: '100%', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
1185
|
+
<span>{item.label}</span>
|
|
1186
|
+
{(item.isActiveToggle || item.isActiveTheme) && <span>✓</span>}
|
|
1187
|
+
</span>
|
|
1188
|
+
</Command.Item>
|
|
1189
|
+
))}
|
|
1190
|
+
</Command.Group>
|
|
1191
|
+
))}
|
|
1192
|
+
|
|
1012
1193
|
{/* Main config-driven groups */}
|
|
1013
1194
|
{cleanedItems.map((list) => (
|
|
1014
1195
|
list.id?.startsWith('cfg:sep') ? (
|
|
1015
1196
|
!search && <Command.Separator key={list.id} />
|
|
1016
1197
|
) : (
|
|
1017
1198
|
<Command.Group key={list.id} heading={list.heading}>
|
|
1018
|
-
{list.items.map(({ id, children, keywords, onClick, itemType, toolIcon, ...rest }) => {
|
|
1199
|
+
{list.items.map(({ id, children, keywords, onClick, itemType, toolIcon, toolMeta, closeOnSelect, hideFromSearch, url, ...rest }) => {
|
|
1200
|
+
if (search && hideFromSearch) return null
|
|
1019
1201
|
if (hiddenFromSearchIds.size > 0) {
|
|
1020
1202
|
for (const toolId of hiddenFromSearchIds) {
|
|
1021
1203
|
if (id?.includes(toolId)) return null
|
|
@@ -1025,9 +1207,21 @@ export default function StoryboardCommandPalette({ basePath }) {
|
|
|
1025
1207
|
<Command.Item
|
|
1026
1208
|
key={id}
|
|
1027
1209
|
value={itemValue({ children, keywords })}
|
|
1028
|
-
onSelect={() =>
|
|
1210
|
+
onSelect={() => {
|
|
1211
|
+
if (url && altHeldRef.current) {
|
|
1212
|
+
copyLinkToClipboard(url, itemType)
|
|
1213
|
+
} else if (url && modifierHeldRef.current) {
|
|
1214
|
+
window.open(url, '_blank')
|
|
1215
|
+
} else {
|
|
1216
|
+
onClick?.()
|
|
1217
|
+
}
|
|
1218
|
+
if (closeOnSelect !== false) {
|
|
1219
|
+
setOpen(false)
|
|
1220
|
+
setActivePage('root')
|
|
1221
|
+
}
|
|
1222
|
+
}}
|
|
1029
1223
|
>
|
|
1030
|
-
<ItemIcon type={itemType} toolIcon={toolIcon} />
|
|
1224
|
+
<ItemIcon type={itemType} toolIcon={toolIcon} toolMeta={toolMeta} />
|
|
1031
1225
|
{children}
|
|
1032
1226
|
</Command.Item>
|
|
1033
1227
|
)
|
|
@@ -1036,24 +1230,6 @@ export default function StoryboardCommandPalette({ basePath }) {
|
|
|
1036
1230
|
)
|
|
1037
1231
|
))}
|
|
1038
1232
|
|
|
1039
|
-
{/* Sub-page options flattened for root search */}
|
|
1040
|
-
{search && subPageGroups.map(group => (
|
|
1041
|
-
<Command.Group key={group.id} heading={group.heading}>
|
|
1042
|
-
{group.items.map(item => (
|
|
1043
|
-
<Command.Item
|
|
1044
|
-
key={item.id}
|
|
1045
|
-
value={itemValue(item)}
|
|
1046
|
-
onSelect={item.onSelect}
|
|
1047
|
-
>
|
|
1048
|
-
<span style={{ display: 'flex', width: '100%', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
1049
|
-
<span>{item.label}</span>
|
|
1050
|
-
{(item.isActiveToggle || item.isActiveTheme) && <span>✓</span>}
|
|
1051
|
-
</span>
|
|
1052
|
-
</Command.Item>
|
|
1053
|
-
))}
|
|
1054
|
-
</Command.Group>
|
|
1055
|
-
))}
|
|
1056
|
-
|
|
1057
1233
|
{/* Author groups */}
|
|
1058
1234
|
{search && authorGroups.map(group => (
|
|
1059
1235
|
<Command.Group key={group.id} heading={group.heading}>
|
|
@@ -1061,7 +1237,19 @@ export default function StoryboardCommandPalette({ basePath }) {
|
|
|
1061
1237
|
<Command.Item
|
|
1062
1238
|
key={item.id}
|
|
1063
1239
|
value={itemValue(item)}
|
|
1064
|
-
onSelect={
|
|
1240
|
+
onSelect={() => {
|
|
1241
|
+
if (item.url && altHeldRef.current) {
|
|
1242
|
+
copyLinkToClipboard(item.url, item.type?.toLowerCase())
|
|
1243
|
+
setOpen(false)
|
|
1244
|
+
setActivePage('root')
|
|
1245
|
+
} else if (item.url && modifierHeldRef.current) {
|
|
1246
|
+
window.open(item.url, '_blank')
|
|
1247
|
+
setOpen(false)
|
|
1248
|
+
setActivePage('root')
|
|
1249
|
+
} else {
|
|
1250
|
+
item.onSelect()
|
|
1251
|
+
}
|
|
1252
|
+
}}
|
|
1065
1253
|
>
|
|
1066
1254
|
<AvatarIcon username={group.author} />
|
|
1067
1255
|
<span style={{ display: 'flex', width: '100%', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
@@ -1093,6 +1281,7 @@ export default function StoryboardCommandPalette({ basePath }) {
|
|
|
1093
1281
|
setActivePage('root')
|
|
1094
1282
|
}}
|
|
1095
1283
|
>
|
|
1284
|
+
{opt.icon && <Icon name={opt.icon} size={ICON_SIZE} color="var(--fgColor-muted, #656d76)" />}
|
|
1096
1285
|
{opt.toolHandler === 'core:theme' && opt.value === currentTheme
|
|
1097
1286
|
? <span style={{ display: 'flex', width: '100%', justifyContent: 'space-between', alignItems: 'center' }}><span>{opt.label}</span><span>✓</span></span>
|
|
1098
1287
|
: opt.label}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
inset: 0;
|
|
11
11
|
background: rgba(0, 0, 0, 0.5);
|
|
12
12
|
z-index: 10000;
|
|
13
|
+
animation: cmdk-overlay-in 120ms ease-out;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
/* ─── Dialog content (Radix Content wrapper) ─── */
|
|
@@ -29,6 +30,17 @@
|
|
|
29
30
|
border: 1px solid var(--borderColor-muted, #d1d9e0);
|
|
30
31
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
31
32
|
outline: none;
|
|
33
|
+
animation: cmdk-dialog-in 120ms ease-out;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@keyframes cmdk-overlay-in {
|
|
37
|
+
from { opacity: 0; }
|
|
38
|
+
to { opacity: 1; }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@keyframes cmdk-dialog-in {
|
|
42
|
+
from { opacity: 0; transform: translateX(-50%) scale(0.97); }
|
|
43
|
+
to { opacity: 1; transform: translateX(-50%) scale(1); }
|
|
32
44
|
}
|
|
33
45
|
|
|
34
46
|
/* ─── Input ─── */
|