@dfosco/storyboard-react 4.2.0-beta.1 → 4.2.0-beta.17
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 +5 -4
- package/src/AuthModal/AuthModal.jsx +6 -2
- package/src/BranchBar/BranchBar.jsx +17 -5
- package/src/BranchBar/BranchBar.module.css +11 -2
- package/src/CommandPalette/CommandPalette.jsx +267 -164
- package/src/CommandPalette/command-palette.css +130 -78
- package/src/Icon.jsx +112 -48
- package/src/Viewfinder.jsx +511 -61
- package/src/Viewfinder.module.css +414 -2
- package/src/canvas/CanvasPage.bridge.test.jsx +14 -6
- package/src/canvas/CanvasPage.dragdrop.test.jsx +10 -6
- package/src/canvas/CanvasPage.jsx +157 -174
- package/src/canvas/CanvasPage.module.css +0 -15
- package/src/canvas/CanvasPage.multiselect.test.jsx +10 -6
- package/src/canvas/ConnectorLayer.jsx +5 -5
- package/src/canvas/PageSelector.test.jsx +15 -6
- package/src/canvas/useCanvas.js +1 -1
- package/src/canvas/widgets/ActionWidget.jsx +200 -0
- package/src/canvas/widgets/ActionWidget.module.css +122 -0
- package/src/canvas/widgets/FigmaEmbed.jsx +97 -29
- package/src/canvas/widgets/FigmaEmbed.module.css +61 -0
- package/src/canvas/widgets/ImageWidget.jsx +1 -1
- package/src/canvas/widgets/LinkPreview.jsx +64 -5
- package/src/canvas/widgets/LinkPreview.module.css +127 -0
- package/src/canvas/widgets/MarkdownBlock.jsx +39 -17
- package/src/canvas/widgets/MarkdownBlock.module.css +123 -0
- package/src/canvas/widgets/PrototypeEmbed.jsx +183 -20
- package/src/canvas/widgets/PrototypeEmbed.module.css +117 -0
- package/src/canvas/widgets/PrototypeEmbed.test.jsx +2 -2
- package/src/canvas/widgets/SplitExpandModal.jsx +234 -0
- package/src/canvas/widgets/SplitExpandModal.module.css +335 -0
- package/src/canvas/widgets/SplitScreenTopBar.jsx +30 -0
- package/src/canvas/widgets/SplitScreenTopBar.module.css +58 -0
- package/src/canvas/widgets/StoryWidget.jsx +7 -4
- package/src/canvas/widgets/TerminalReadWidget.jsx +140 -0
- package/src/canvas/widgets/TerminalReadWidget.module.css +92 -0
- package/src/canvas/widgets/TerminalWidget.jsx +299 -49
- package/src/canvas/widgets/TerminalWidget.module.css +155 -1
- package/src/canvas/widgets/WidgetChrome.jsx +19 -14
- package/src/canvas/widgets/WidgetChrome.module.css +10 -0
- package/src/canvas/widgets/embedInteraction.test.jsx +24 -26
- package/src/canvas/widgets/expandUtils.js +188 -0
- package/src/canvas/widgets/index.js +5 -0
- package/src/canvas/widgets/snapshotDisplay.test.jsx +23 -71
- package/src/canvas/widgets/widgetConfig.js +19 -1
- package/src/hooks/useConfig.js +14 -0
- package/src/index.js +4 -0
- package/src/vite/data-plugin.js +264 -14
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { useState, useEffect, useCallback, useMemo } from 'react'
|
|
2
|
-
import '
|
|
3
|
-
import
|
|
4
|
-
const CommandPalette = ReactCmdk.default || ReactCmdk
|
|
5
|
-
const { filterItems, getItemIndex } = ReactCmdk
|
|
2
|
+
import { Command } from 'cmdk'
|
|
3
|
+
import Icon from '../Icon.jsx'
|
|
6
4
|
import {
|
|
7
5
|
buildPrototypeIndex,
|
|
8
6
|
listStories,
|
|
@@ -26,6 +24,32 @@ import BranchBar from '../BranchBar/BranchBar.jsx'
|
|
|
26
24
|
import AuthModal from '../AuthModal/AuthModal.jsx'
|
|
27
25
|
import './command-palette.css'
|
|
28
26
|
|
|
27
|
+
// Icon size for all palette items
|
|
28
|
+
const ICON_SIZE = 16
|
|
29
|
+
|
|
30
|
+
function getIconMap() {
|
|
31
|
+
const config = getCommandPaletteConfig()
|
|
32
|
+
return config?.icons || {}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function ItemIcon({ type, toolIcon }) {
|
|
36
|
+
const icons = getIconMap()
|
|
37
|
+
const iconName = toolIcon || icons[type] || icons.fallback || 'feather/hexagon'
|
|
38
|
+
return <Icon name={iconName} size={ICON_SIZE} color="var(--fgColor-muted, #656d76)" />
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function AvatarIcon({ username }) {
|
|
42
|
+
return (
|
|
43
|
+
<img
|
|
44
|
+
src={`https://github.com/${username}.png?size=32`}
|
|
45
|
+
alt={username}
|
|
46
|
+
width={ICON_SIZE}
|
|
47
|
+
height={ICON_SIZE}
|
|
48
|
+
style={{ flexShrink: 0, borderRadius: '50%' }}
|
|
49
|
+
/>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
29
53
|
/**
|
|
30
54
|
* Check if a tool should be hidden from the command palette on the current route.
|
|
31
55
|
* Uses the same pattern-matching logic as excludeRoutes.
|
|
@@ -58,7 +82,8 @@ function buildConfigSections(prefix, onNavigateToPage, onCreateAction) {
|
|
|
58
82
|
const sections = config?.sections || []
|
|
59
83
|
const groups = []
|
|
60
84
|
const toolMenus = []
|
|
61
|
-
const usedToolIds = new Set()
|
|
85
|
+
const usedToolIds = new Set()
|
|
86
|
+
const hiddenFromSearchIds = new Set()
|
|
62
87
|
const basePath = prefix || '/'
|
|
63
88
|
|
|
64
89
|
for (const section of sections) {
|
|
@@ -80,6 +105,7 @@ function buildConfigSections(prefix, onNavigateToPage, onCreateAction) {
|
|
|
80
105
|
if (result?.group) groups.push(result.group)
|
|
81
106
|
if (result?.subPages) toolMenus.push(...result.subPages)
|
|
82
107
|
if (result?.usedToolIds) result.usedToolIds.forEach(id => usedToolIds.add(id))
|
|
108
|
+
if (result?.hiddenFromSearchIds) result.hiddenFromSearchIds.forEach(id => hiddenFromSearchIds.add(id))
|
|
83
109
|
continue
|
|
84
110
|
}
|
|
85
111
|
|
|
@@ -204,6 +230,11 @@ function buildConfigSections(prefix, onNavigateToPage, onCreateAction) {
|
|
|
204
230
|
continue
|
|
205
231
|
}
|
|
206
232
|
|
|
233
|
+
if (tool.inlineAction === 'open-palette') {
|
|
234
|
+
// Skip — no point opening the palette from within itself
|
|
235
|
+
continue
|
|
236
|
+
}
|
|
237
|
+
|
|
207
238
|
// Any remaining tools (all surfaces)
|
|
208
239
|
if (tool.render === 'link' && tool.url) {
|
|
209
240
|
remainingItems.push({
|
|
@@ -275,7 +306,7 @@ function buildConfigSections(prefix, onNavigateToPage, onCreateAction) {
|
|
|
275
306
|
})
|
|
276
307
|
}
|
|
277
308
|
|
|
278
|
-
return { groups, toolMenus }
|
|
309
|
+
return { groups, toolMenus, hiddenFromSearchIds }
|
|
279
310
|
}
|
|
280
311
|
|
|
281
312
|
function buildDynamicSection(section, prefix, onNavigateToPage, onCreateAction) {
|
|
@@ -288,11 +319,11 @@ function buildDynamicSection(section, prefix, onNavigateToPage, onCreateAction)
|
|
|
288
319
|
const isLocalDev = typeof window !== 'undefined' && window.__SB_LOCAL_DEV__ === true
|
|
289
320
|
if (!isLocalDev) return null
|
|
290
321
|
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'],
|
|
322
|
+
{ id: 'create:canvas', children: 'Canvas', keywords: ['create', 'canvas', 'new', 'board'], itemType: 'create', onClick: () => onCreateAction?.('Canvas') },
|
|
323
|
+
{ id: 'create:prototype', children: 'Prototype', keywords: ['create', 'prototype', 'new', 'page'], itemType: 'create', onClick: () => onCreateAction?.('Prototype') },
|
|
324
|
+
{ id: 'create:component', children: 'Component', keywords: ['create', 'component', 'new', 'story'], itemType: 'create', onClick: () => onCreateAction?.('Component') },
|
|
325
|
+
{ id: 'create:flow', children: 'Prototype Flow', keywords: ['create', 'flow', 'new', 'data'], itemType: 'create', onClick: () => onCreateAction?.('Flow') },
|
|
326
|
+
{ id: 'create:page', children: 'Prototype Page', keywords: ['create', 'page', 'new'], itemType: 'create', onClick: () => onCreateAction?.('Page') },
|
|
296
327
|
]
|
|
297
328
|
return { group: { heading: section.title, id: `cfg:${section.id}`, items: createItems } }
|
|
298
329
|
}
|
|
@@ -303,11 +334,12 @@ function buildDynamicSection(section, prefix, onNavigateToPage, onCreateAction)
|
|
|
303
334
|
if (!isLocalDev) return null
|
|
304
335
|
const isCanvasRoute = typeof window !== 'undefined' && window.location.pathname.includes('/canvas/')
|
|
305
336
|
if (!isCanvasRoute) return null
|
|
306
|
-
const
|
|
337
|
+
const hiddenTypes = new Set(['link-preview', 'image', 'figma-embed', 'codepen-embed', 'story', 'terminal-read'])
|
|
338
|
+
const items = Object.entries(widgetTypes).filter(([type]) => !hiddenTypes.has(type)).map(([type, def]) => ({
|
|
307
339
|
id: `create-widget:${type}`,
|
|
308
340
|
children: def.label,
|
|
309
341
|
keywords: ['add', 'widget', 'create', type, def.label.toLowerCase()],
|
|
310
|
-
|
|
342
|
+
itemType: 'create',
|
|
311
343
|
onClick: () => {
|
|
312
344
|
document.dispatchEvent(new CustomEvent('storyboard:canvas:add-widget', { detail: { type } }))
|
|
313
345
|
},
|
|
@@ -346,7 +378,7 @@ function buildDynamicSection(section, prefix, onNavigateToPage, onCreateAction)
|
|
|
346
378
|
id: `starred:${id}`,
|
|
347
379
|
children: artifact.name,
|
|
348
380
|
keywords: ['starred', 'star', artifact.name.toLowerCase()],
|
|
349
|
-
|
|
381
|
+
itemType: artifact._type === 'canvas' ? 'canvas' : 'prototype',
|
|
350
382
|
onClick: () => {
|
|
351
383
|
if (artifact.isExternal) {
|
|
352
384
|
window.open(route, '_blank')
|
|
@@ -378,6 +410,7 @@ function buildDynamicSection(section, prefix, onNavigateToPage, onCreateAction)
|
|
|
378
410
|
id: `cmd:${action.id}/${child.id || child.label}`,
|
|
379
411
|
children: child.label,
|
|
380
412
|
keywords: [action.label, child.label],
|
|
413
|
+
itemType: 'command',
|
|
381
414
|
onClick: () => { if (child.execute) child.execute() },
|
|
382
415
|
})
|
|
383
416
|
}
|
|
@@ -386,6 +419,7 @@ function buildDynamicSection(section, prefix, onNavigateToPage, onCreateAction)
|
|
|
386
419
|
id: `cmd:${action.id}`,
|
|
387
420
|
children: action.label,
|
|
388
421
|
keywords: [action.label],
|
|
422
|
+
itemType: 'link',
|
|
389
423
|
onClick: () => {
|
|
390
424
|
const url = action.url.startsWith('/') && !action.url.startsWith('//') ? prefix + action.url : action.url
|
|
391
425
|
window.location.href = url
|
|
@@ -396,6 +430,7 @@ function buildDynamicSection(section, prefix, onNavigateToPage, onCreateAction)
|
|
|
396
430
|
id: `cmd:${action.id}`,
|
|
397
431
|
children: action.label,
|
|
398
432
|
keywords: [action.label],
|
|
433
|
+
itemType: 'command',
|
|
399
434
|
onClick: () => executeAction(action.id),
|
|
400
435
|
})
|
|
401
436
|
}
|
|
@@ -418,6 +453,7 @@ function buildDynamicSection(section, prefix, onNavigateToPage, onCreateAction)
|
|
|
418
453
|
id: `cfg:${section.id}:${entry.type}:${entry.key}`,
|
|
419
454
|
children: entry.label,
|
|
420
455
|
keywords: [entry.type, entry.key, entry.label],
|
|
456
|
+
itemType: entry.type,
|
|
421
457
|
onClick: () => {
|
|
422
458
|
trackRecent(entry.type, entry.key, entry.label)
|
|
423
459
|
const route = resolveRecentRoute(entry, prefix)
|
|
@@ -477,6 +513,7 @@ function buildDynamicSection(section, prefix, onNavigateToPage, onCreateAction)
|
|
|
477
513
|
id: `cfg:${section.id}:${item.id}`,
|
|
478
514
|
children: item.name,
|
|
479
515
|
keywords: [item.name, item.id, item.type],
|
|
516
|
+
itemType: item.type,
|
|
480
517
|
onClick: () => {
|
|
481
518
|
trackRecent(item.type, item.id, item.name)
|
|
482
519
|
window.location.href = item.route
|
|
@@ -489,7 +526,7 @@ function buildDynamicSection(section, prefix, onNavigateToPage, onCreateAction)
|
|
|
489
526
|
/**
|
|
490
527
|
* Build a section from toolbar.config.json tools.
|
|
491
528
|
* If toolIds is provided, only include those tools in that order (with optional custom labels).
|
|
492
|
-
* Otherwise include all command-
|
|
529
|
+
* Otherwise include all command-palette tools.
|
|
493
530
|
*
|
|
494
531
|
* toolIds format: ["theme", "flows"] or [{ id: "theme", label: "Change theme" }]
|
|
495
532
|
*/
|
|
@@ -511,15 +548,15 @@ function buildToolsSection(section, prefix, onNavigateToPage) {
|
|
|
511
548
|
const state = getToolbarToolState(toolId)
|
|
512
549
|
if (state === 'disabled' || state === 'hidden') continue
|
|
513
550
|
if (isHiddenInPalette(tool, basePath)) continue
|
|
514
|
-
entries.push({ toolId, tool, label: customLabel || tool.label || toolId })
|
|
551
|
+
entries.push({ toolId, tool, label: customLabel || tool.label || toolId, toolIcon: tool.icon })
|
|
515
552
|
}
|
|
516
553
|
} else {
|
|
517
554
|
for (const [toolId, tool] of Object.entries(tools)) {
|
|
518
|
-
if (tool.surface !== 'command-
|
|
555
|
+
if (tool.surface !== 'command-palette') continue
|
|
519
556
|
const state = getToolbarToolState(toolId)
|
|
520
557
|
if (state === 'disabled' || state === 'hidden') continue
|
|
521
558
|
if (isHiddenInPalette(tool, basePath)) continue
|
|
522
|
-
entries.push({ toolId, tool, label: tool.label || toolId })
|
|
559
|
+
entries.push({ toolId, tool, label: tool.label || toolId, toolIcon: tool.icon })
|
|
523
560
|
}
|
|
524
561
|
}
|
|
525
562
|
|
|
@@ -528,16 +565,35 @@ function buildToolsSection(section, prefix, onNavigateToPage) {
|
|
|
528
565
|
const items = []
|
|
529
566
|
const subPages = []
|
|
530
567
|
|
|
531
|
-
for (const { toolId, tool, label } of entries) {
|
|
568
|
+
for (const { toolId, tool, label, toolIcon } of entries) {
|
|
532
569
|
// Inline actions
|
|
533
570
|
if (tool.inlineAction === 'toggle-chrome') {
|
|
571
|
+
const isHidden = document.documentElement.classList.contains('storyboard-chrome-hidden')
|
|
534
572
|
items.push({
|
|
535
573
|
id: `cfg:${section.id}:${toolId}`,
|
|
536
|
-
|
|
574
|
+
toolIcon,
|
|
575
|
+
children: <span style={{ display: 'flex', width: '100%', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
576
|
+
<span>{label}</span>
|
|
577
|
+
<span>{isHidden ? '✓' : ''}</span>
|
|
578
|
+
</span>,
|
|
537
579
|
keywords: [label, toolId, 'hide', 'show', 'toolbar'].filter(Boolean),
|
|
538
580
|
showType: false,
|
|
539
581
|
onClick: () => {
|
|
540
582
|
document.documentElement.classList.toggle('storyboard-chrome-hidden')
|
|
583
|
+
setRefreshKey(k => k + 1)
|
|
584
|
+
},
|
|
585
|
+
})
|
|
586
|
+
continue
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (tool.inlineAction === 'open-palette') {
|
|
590
|
+
items.push({
|
|
591
|
+
id: `cfg:${section.id}:${toolId}`,
|
|
592
|
+
children: label,
|
|
593
|
+
keywords: [label, toolId, 'command', 'palette', 'search'].filter(Boolean),
|
|
594
|
+
showType: false,
|
|
595
|
+
onClick: () => {
|
|
596
|
+
document.dispatchEvent(new CustomEvent('storyboard:open-palette'))
|
|
541
597
|
},
|
|
542
598
|
})
|
|
543
599
|
continue
|
|
@@ -570,6 +626,8 @@ function buildToolsSection(section, prefix, onNavigateToPage) {
|
|
|
570
626
|
options: children.map(child => ({
|
|
571
627
|
label: child.label,
|
|
572
628
|
execute: child.execute,
|
|
629
|
+
type: child.type,
|
|
630
|
+
active: child.active,
|
|
573
631
|
})),
|
|
574
632
|
})
|
|
575
633
|
items.push({
|
|
@@ -647,6 +705,15 @@ function buildToolsSection(section, prefix, onNavigateToPage) {
|
|
|
647
705
|
})
|
|
648
706
|
}
|
|
649
707
|
|
|
708
|
+
// Add toolIcon to all items from their entry
|
|
709
|
+
const iconByToolId = new Map(entries.map(e => [e.toolId, e.toolIcon]))
|
|
710
|
+
for (const item of items) {
|
|
711
|
+
if (!item.toolIcon) {
|
|
712
|
+
const match = item.id?.match(/cfg:[^:]+:(.+)/)
|
|
713
|
+
if (match && iconByToolId.has(match[1])) item.toolIcon = iconByToolId.get(match[1])
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
650
717
|
return {
|
|
651
718
|
group: {
|
|
652
719
|
heading: section.title,
|
|
@@ -722,10 +789,10 @@ function buildPaletteItems(basePath, onCreateAction, onNavigateToPage) {
|
|
|
722
789
|
const base = (basePath || '/').replace(/\/+$/, '')
|
|
723
790
|
const prefix = base === '/' ? '' : base
|
|
724
791
|
|
|
725
|
-
const { groups, toolMenus } = buildConfigSections(prefix, onNavigateToPage, onCreateAction)
|
|
792
|
+
const { groups, toolMenus, hiddenFromSearchIds } = buildConfigSections(prefix, onNavigateToPage, onCreateAction)
|
|
726
793
|
const authorIndex = buildAuthorIndex(prefix)
|
|
727
794
|
|
|
728
|
-
return { groups, toolMenus, authorIndex }
|
|
795
|
+
return { groups, toolMenus, authorIndex, hiddenFromSearchIds }
|
|
729
796
|
}
|
|
730
797
|
|
|
731
798
|
/**
|
|
@@ -738,9 +805,11 @@ export default function StoryboardCommandPalette({ basePath }) {
|
|
|
738
805
|
const [items, setItems] = useState([])
|
|
739
806
|
const [toolMenus, setToolMenus] = useState([])
|
|
740
807
|
const [authorIndex, setAuthorIndex] = useState(new Map())
|
|
808
|
+
const [hiddenFromSearchIds, setHiddenFromSearchIds] = useState(new Set())
|
|
741
809
|
const [activePage, setActivePage] = useState('root')
|
|
742
810
|
const [createType, setCreateType] = useState(null)
|
|
743
811
|
const [currentTheme, setCurrentTheme] = useState(() => getTheme())
|
|
812
|
+
const [refreshKey, setRefreshKey] = useState(0)
|
|
744
813
|
|
|
745
814
|
// Keep currentTheme in sync when theme changes
|
|
746
815
|
useEffect(() => {
|
|
@@ -759,43 +828,29 @@ export default function StoryboardCommandPalette({ basePath }) {
|
|
|
759
828
|
setActivePage(pageId)
|
|
760
829
|
}
|
|
761
830
|
|
|
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.
|
|
831
|
+
// Listen for Cmd+K directly to toggle the palette
|
|
766
832
|
useEffect(() => {
|
|
767
|
-
let toggledByEvent = false
|
|
768
|
-
|
|
769
|
-
function handleToggleEvent() {
|
|
770
|
-
toggledByEvent = true
|
|
771
|
-
}
|
|
772
|
-
|
|
773
833
|
function handleKeyDown(e) {
|
|
774
834
|
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
775
835
|
e.preventDefault()
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
setActivePage('root')
|
|
785
|
-
setOpen(prev => !prev)
|
|
786
|
-
})
|
|
836
|
+
const built = buildPaletteItems(basePath, handleCreateAction, handleNavigateToPage)
|
|
837
|
+
setItems(built.groups)
|
|
838
|
+
setToolMenus(built.toolMenus)
|
|
839
|
+
setAuthorIndex(built.authorIndex)
|
|
840
|
+
setHiddenFromSearchIds(built.hiddenFromSearchIds || new Set())
|
|
841
|
+
setSearch('')
|
|
842
|
+
setActivePage('root')
|
|
843
|
+
setOpen(prev => !prev)
|
|
787
844
|
}
|
|
788
845
|
}
|
|
789
846
|
|
|
790
|
-
document.addEventListener('storyboard:toggle-palette', handleToggleEvent)
|
|
791
847
|
document.addEventListener('keydown', handleKeyDown)
|
|
792
848
|
return () => {
|
|
793
|
-
document.removeEventListener('storyboard:toggle-palette', handleToggleEvent)
|
|
794
849
|
document.removeEventListener('keydown', handleKeyDown)
|
|
795
850
|
}
|
|
796
851
|
}, [basePath])
|
|
797
852
|
|
|
798
|
-
// Listen for toggle events from
|
|
853
|
+
// Listen for toggle/open events from toolbar buttons (e.g. CommandPaletteTrigger)
|
|
799
854
|
useEffect(() => {
|
|
800
855
|
function handleToggle() {
|
|
801
856
|
setOpen(prev => {
|
|
@@ -806,6 +861,7 @@ export default function StoryboardCommandPalette({ basePath }) {
|
|
|
806
861
|
setItems(built.groups)
|
|
807
862
|
setToolMenus(built.toolMenus)
|
|
808
863
|
setAuthorIndex(built.authorIndex)
|
|
864
|
+
setHiddenFromSearchIds(built.hiddenFromSearchIds || new Set())
|
|
809
865
|
setSearch('')
|
|
810
866
|
setActivePage('root')
|
|
811
867
|
}, 0)
|
|
@@ -819,6 +875,7 @@ export default function StoryboardCommandPalette({ basePath }) {
|
|
|
819
875
|
setItems(built.groups)
|
|
820
876
|
setToolMenus(built.toolMenus)
|
|
821
877
|
setAuthorIndex(built.authorIndex)
|
|
878
|
+
setHiddenFromSearchIds(built.hiddenFromSearchIds || new Set())
|
|
822
879
|
setSearch('')
|
|
823
880
|
setActivePage('root')
|
|
824
881
|
setOpen(true)
|
|
@@ -832,6 +889,14 @@ export default function StoryboardCommandPalette({ basePath }) {
|
|
|
832
889
|
}
|
|
833
890
|
}, [basePath])
|
|
834
891
|
|
|
892
|
+
// Rebuild palette items when a toggle is clicked (refreshKey changes)
|
|
893
|
+
useEffect(() => {
|
|
894
|
+
if (refreshKey === 0) return
|
|
895
|
+
const built = buildPaletteItems(basePath, handleCreateAction, handleNavigateToPage)
|
|
896
|
+
setItems(built.groups)
|
|
897
|
+
setToolMenus(built.toolMenus)
|
|
898
|
+
}, [refreshKey, basePath])
|
|
899
|
+
|
|
835
900
|
const handleChangeOpen = useCallback((value) => {
|
|
836
901
|
if (!value) {
|
|
837
902
|
setOpen(false)
|
|
@@ -846,12 +911,12 @@ export default function StoryboardCommandPalette({ basePath }) {
|
|
|
846
911
|
id: `subpage:${menu.id}`,
|
|
847
912
|
items: (menu.options || []).map((opt, i) => ({
|
|
848
913
|
id: `subpage:${menu.id}:${i}`,
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
914
|
+
label: opt.label,
|
|
915
|
+
isToggle: opt.type === 'toggle',
|
|
916
|
+
isActiveToggle: opt.type === 'toggle' && opt.active,
|
|
917
|
+
isActiveTheme: opt.toolHandler === 'core:theme' && opt.value === currentTheme,
|
|
852
918
|
keywords: [opt.label, menu.label || menu.id],
|
|
853
|
-
|
|
854
|
-
onClick: () => {
|
|
919
|
+
onSelect: () => {
|
|
855
920
|
if (opt.execute) {
|
|
856
921
|
opt.execute()
|
|
857
922
|
} else if (opt.toolHandler === 'core:theme' && opt.value) {
|
|
@@ -859,147 +924,185 @@ export default function StoryboardCommandPalette({ basePath }) {
|
|
|
859
924
|
} else if (opt.action) {
|
|
860
925
|
executeAction(opt.action, opt.value)
|
|
861
926
|
}
|
|
862
|
-
|
|
863
|
-
|
|
927
|
+
if (opt.type === 'toggle') {
|
|
928
|
+
setRefreshKey(k => k + 1)
|
|
929
|
+
} else {
|
|
930
|
+
setOpen(false)
|
|
931
|
+
setActivePage('root')
|
|
932
|
+
}
|
|
864
933
|
},
|
|
865
934
|
})),
|
|
866
935
|
})).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({
|
|
936
|
+
}, [toolMenus, currentTheme, refreshKey])
|
|
937
|
+
|
|
938
|
+
// Build author groups from the index
|
|
939
|
+
const authorGroups = useMemo(() => {
|
|
940
|
+
const groups = []
|
|
941
|
+
for (const [, { author, items: authorItems }] of authorIndex) {
|
|
942
|
+
groups.push({
|
|
885
943
|
heading: `Artifacts by @${author}`,
|
|
886
|
-
id: `author:${
|
|
887
|
-
|
|
944
|
+
id: `author:${author.toLowerCase()}`,
|
|
945
|
+
author,
|
|
946
|
+
items: authorItems.map(item => ({
|
|
888
947
|
id: `author:${item.id}`,
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
<span>{item.name}</span>
|
|
892
|
-
<span style={{ fontSize: '12px', color: 'var(--fgColor-muted, #999)' }}>{item.type}</span>
|
|
893
|
-
</span>
|
|
894
|
-
),
|
|
948
|
+
label: item.name,
|
|
949
|
+
type: item.type,
|
|
895
950
|
keywords: [item.name, item.id, item.type, author, `@${author}`],
|
|
896
|
-
|
|
897
|
-
onClick: () => {
|
|
951
|
+
onSelect: () => {
|
|
898
952
|
trackRecent(item.type.toLowerCase(), item.id, item.name)
|
|
899
953
|
window.location.href = item.route
|
|
900
954
|
},
|
|
901
955
|
})),
|
|
902
956
|
})
|
|
903
957
|
}
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
}, [items, search, subPageGroups, authorIndex])
|
|
958
|
+
return groups
|
|
959
|
+
}, [authorIndex])
|
|
907
960
|
|
|
908
961
|
// Remove consecutive separators and leading/trailing separators
|
|
909
|
-
const
|
|
962
|
+
const cleanedItems = useMemo(() => {
|
|
910
963
|
const result = []
|
|
911
|
-
for (const item of
|
|
964
|
+
for (const item of items) {
|
|
912
965
|
const isSep = item.id?.startsWith('cfg:sep')
|
|
913
966
|
if (isSep && (result.length === 0 || result[result.length - 1].id?.startsWith('cfg:sep'))) continue
|
|
914
967
|
result.push(item)
|
|
915
968
|
}
|
|
916
|
-
// Remove trailing separator
|
|
917
969
|
while (result.length > 0 && result[result.length - 1].id?.startsWith('cfg:sep')) result.pop()
|
|
918
970
|
return result
|
|
919
|
-
}, [
|
|
920
|
-
|
|
921
|
-
//
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
}, [])
|
|
971
|
+
}, [items])
|
|
972
|
+
|
|
973
|
+
// Build search value string from keywords array
|
|
974
|
+
function itemValue(item) {
|
|
975
|
+
const parts = []
|
|
976
|
+
if (typeof item.children === 'string') parts.push(item.children)
|
|
977
|
+
if (item.label) parts.push(item.label)
|
|
978
|
+
if (item.keywords) parts.push(...item.keywords)
|
|
979
|
+
return parts.filter(Boolean).join(' ')
|
|
980
|
+
}
|
|
930
981
|
|
|
931
982
|
return (
|
|
932
983
|
<>
|
|
933
|
-
<
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
984
|
+
<Command.Dialog
|
|
985
|
+
open={open}
|
|
986
|
+
onOpenChange={handleChangeOpen}
|
|
987
|
+
label="Command Menu"
|
|
988
|
+
className="command-palette"
|
|
989
|
+
shouldFilter={activePage === 'root'}
|
|
990
|
+
onKeyDown={(e) => {
|
|
991
|
+
if (e.key === 'Escape' && activePage !== 'root') {
|
|
992
|
+
e.preventDefault()
|
|
993
|
+
e.stopPropagation()
|
|
994
|
+
setActivePage('root')
|
|
995
|
+
setSearch('')
|
|
996
|
+
}
|
|
997
|
+
}}
|
|
943
998
|
>
|
|
944
|
-
<
|
|
945
|
-
{
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
999
|
+
<Command.Input
|
|
1000
|
+
placeholder={activePage === 'root'
|
|
1001
|
+
? 'Search commands, prototypes, canvases, stories...'
|
|
1002
|
+
: `Search ${toolMenus.find(m => m.id === activePage)?.label || ''}...`
|
|
1003
|
+
}
|
|
1004
|
+
value={search}
|
|
1005
|
+
onValueChange={setSearch}
|
|
1006
|
+
/>
|
|
1007
|
+
<Command.List>
|
|
1008
|
+
<Command.Empty>No results found.</Command.Empty>
|
|
1009
|
+
|
|
1010
|
+
{activePage === 'root' ? (
|
|
1011
|
+
<>
|
|
1012
|
+
{/* Main config-driven groups */}
|
|
1013
|
+
{cleanedItems.map((list) => (
|
|
1014
|
+
list.id?.startsWith('cfg:sep') ? (
|
|
1015
|
+
!search && <Command.Separator key={list.id} />
|
|
1016
|
+
) : (
|
|
1017
|
+
<Command.Group key={list.id} heading={list.heading}>
|
|
1018
|
+
{list.items.map(({ id, children, keywords, onClick, itemType, toolIcon, ...rest }) => {
|
|
1019
|
+
if (hiddenFromSearchIds.size > 0) {
|
|
1020
|
+
for (const toolId of hiddenFromSearchIds) {
|
|
1021
|
+
if (id?.includes(toolId)) return null
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
return (
|
|
1025
|
+
<Command.Item
|
|
1026
|
+
key={id}
|
|
1027
|
+
value={itemValue({ children, keywords })}
|
|
1028
|
+
onSelect={() => onClick?.()}
|
|
1029
|
+
>
|
|
1030
|
+
<ItemIcon type={itemType} toolIcon={toolIcon} />
|
|
1031
|
+
{children}
|
|
1032
|
+
</Command.Item>
|
|
1033
|
+
)
|
|
1034
|
+
})}
|
|
1035
|
+
</Command.Group>
|
|
1036
|
+
)
|
|
1037
|
+
))}
|
|
1038
|
+
|
|
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>
|
|
957
1053
|
))}
|
|
958
|
-
</
|
|
959
|
-
)
|
|
960
|
-
|
|
1054
|
+
</Command.Group>
|
|
1055
|
+
))}
|
|
1056
|
+
|
|
1057
|
+
{/* Author groups */}
|
|
1058
|
+
{search && authorGroups.map(group => (
|
|
1059
|
+
<Command.Group key={group.id} heading={group.heading}>
|
|
1060
|
+
{group.items.map(item => (
|
|
1061
|
+
<Command.Item
|
|
1062
|
+
key={item.id}
|
|
1063
|
+
value={itemValue(item)}
|
|
1064
|
+
onSelect={item.onSelect}
|
|
1065
|
+
>
|
|
1066
|
+
<AvatarIcon username={group.author} />
|
|
1067
|
+
<span style={{ display: 'flex', width: '100%', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
1068
|
+
<span>{item.label}</span>
|
|
1069
|
+
<span style={{ fontSize: '12px', color: 'var(--fgColor-muted, #999)' }}>{item.type}</span>
|
|
1070
|
+
</span>
|
|
1071
|
+
</Command.Item>
|
|
1072
|
+
))}
|
|
1073
|
+
</Command.Group>
|
|
1074
|
+
))}
|
|
1075
|
+
</>
|
|
961
1076
|
) : (
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
1077
|
+
/* Tool-menu sub-pages */
|
|
1078
|
+
toolMenus.filter(menu => menu.id === activePage).map(menu => (
|
|
1079
|
+
<Command.Group key={menu.id} heading={menu.title || menu.label || menu.id}>
|
|
1080
|
+
{(menu.options || []).map((opt, i) => (
|
|
1081
|
+
<Command.Item
|
|
1082
|
+
key={`${menu.id}:${i}`}
|
|
1083
|
+
value={opt.label}
|
|
1084
|
+
onSelect={() => {
|
|
1085
|
+
if (opt.execute) {
|
|
1086
|
+
opt.execute()
|
|
1087
|
+
} else if (opt.toolHandler === 'core:theme' && opt.value) {
|
|
1088
|
+
setTheme(opt.value)
|
|
1089
|
+
} else if (opt.action) {
|
|
1090
|
+
executeAction(opt.action, opt.value)
|
|
1091
|
+
}
|
|
1092
|
+
setOpen(false)
|
|
1093
|
+
setActivePage('root')
|
|
1094
|
+
}}
|
|
1095
|
+
>
|
|
1096
|
+
{opt.toolHandler === 'core:theme' && opt.value === currentTheme
|
|
1097
|
+
? <span style={{ display: 'flex', width: '100%', justifyContent: 'space-between', alignItems: 'center' }}><span>{opt.label}</span><span>✓</span></span>
|
|
1098
|
+
: opt.label}
|
|
1099
|
+
</Command.Item>
|
|
1100
|
+
))}
|
|
1101
|
+
</Command.Group>
|
|
1102
|
+
))
|
|
965
1103
|
)}
|
|
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>
|
|
1104
|
+
</Command.List>
|
|
1105
|
+
</Command.Dialog>
|
|
1003
1106
|
|
|
1004
1107
|
<CreateDialog
|
|
1005
1108
|
type={createType}
|