@dfosco/storyboard-react 4.2.0-beta.2 → 4.2.0-beta.21
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 +9 -4
- package/src/AuthModal/AuthModal.jsx +6 -2
- 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 +478 -186
- package/src/CommandPalette/command-palette.css +142 -78
- package/src/Icon.jsx +157 -58
- package/src/Viewfinder.jsx +561 -191
- 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 +10 -6
- package/src/canvas/CanvasPage.jsx +738 -216
- package/src/canvas/CanvasPage.module.css +13 -15
- package/src/canvas/CanvasPage.multiselect.test.jsx +17 -6
- package/src/canvas/ConnectorLayer.jsx +121 -153
- 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/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 +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 +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 +112 -4
- package/src/canvas/widgets/LinkPreview.module.css +127 -0
- package/src/canvas/widgets/MarkdownBlock.jsx +164 -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 -38
- 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 +72 -15
- package/src/canvas/widgets/TerminalReadWidget.jsx +146 -0
- package/src/canvas/widgets/TerminalReadWidget.module.css +94 -0
- package/src/canvas/widgets/TerminalWidget.jsx +496 -69
- package/src/canvas/widgets/TerminalWidget.module.css +271 -8
- package/src/canvas/widgets/TilesWidget.jsx +302 -0
- package/src/canvas/widgets/TilesWidget.module.css +133 -0
- package/src/canvas/widgets/WidgetChrome.jsx +73 -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 +557 -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 +47 -19
- package/src/hooks/useConfig.js +14 -0
- package/src/hooks/usePrototypeReloadGuard.js +64 -0
- 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 +324 -30
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/* ComponentSetPage — grid layout for all exports of a story */
|
|
2
|
+
|
|
3
|
+
.grid {
|
|
4
|
+
background-color: var(--bgColor-muted, #f6f8fa);
|
|
5
|
+
display: flex;
|
|
6
|
+
flex-wrap: nowrap;
|
|
7
|
+
gap: 12px;
|
|
8
|
+
padding: 12px;
|
|
9
|
+
min-height: 100vh;
|
|
10
|
+
width: max-content;
|
|
11
|
+
min-width: 100%;
|
|
12
|
+
box-sizing: border-box;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.grid[data-layout="horizontal"] {
|
|
16
|
+
flex-direction: row;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.grid[data-layout="vertical"] {
|
|
20
|
+
flex-direction: column;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.cell {
|
|
24
|
+
flex: 0 0 auto;
|
|
25
|
+
display: flex;
|
|
26
|
+
flex-direction: column;
|
|
27
|
+
border: 2px solid var(--borderColor-muted, #d8dee4);
|
|
28
|
+
border-radius: 2px;
|
|
29
|
+
overflow: hidden;
|
|
30
|
+
transition: border-color 120ms ease, box-shadow 120ms ease;
|
|
31
|
+
position: relative;
|
|
32
|
+
background: var(--bgColor-default, #ffffff);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* In horizontal layout, each cell snaps to the widest component */
|
|
36
|
+
.grid[data-layout="horizontal"] .cell {
|
|
37
|
+
min-width: var(--cell-snap-w, 200px);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* In vertical layout, each cell snaps to the tallest component */
|
|
41
|
+
.grid[data-layout="vertical"] .cell {
|
|
42
|
+
min-height: var(--cell-snap-h, 120px);
|
|
43
|
+
min-width: 100%;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.cell[data-selected] {
|
|
47
|
+
border-color: var(--fgColor-accent, #0969da);
|
|
48
|
+
box-shadow: 0 0 0 1px var(--fgColor-accent, #0969da);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.cellLabel {
|
|
52
|
+
all: unset;
|
|
53
|
+
display: flex;
|
|
54
|
+
align-items: center;
|
|
55
|
+
gap: 6px;
|
|
56
|
+
padding: 6px 10px;
|
|
57
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
58
|
+
font-size: 11px;
|
|
59
|
+
font-weight: 600;
|
|
60
|
+
color: var(--fgColor-muted, #656d76);
|
|
61
|
+
background: var(--bgColor-muted, #f6f8fa);
|
|
62
|
+
border-bottom: 1px solid var(--borderColor-muted, #d8dee4);
|
|
63
|
+
cursor: pointer;
|
|
64
|
+
user-select: none;
|
|
65
|
+
transition: background 100ms ease, color 100ms ease;
|
|
66
|
+
flex-shrink: 0;
|
|
67
|
+
/* Round top corners to match cell */
|
|
68
|
+
border-radius: 6px 6px 0 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.cellLabel:hover {
|
|
72
|
+
background: var(--bgColor-neutral-muted, #eaeef2);
|
|
73
|
+
color: var(--fgColor-default, #1f2328);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.cellLabel[data-selected] {
|
|
77
|
+
color: var(--fgColor-accent, #0969da);
|
|
78
|
+
background: var(--bgColor-accent-muted, #ddf4ff);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.cellRadio {
|
|
82
|
+
display: inline-flex;
|
|
83
|
+
align-items: center;
|
|
84
|
+
justify-content: center;
|
|
85
|
+
width: 12px;
|
|
86
|
+
height: 12px;
|
|
87
|
+
border-radius: 50%;
|
|
88
|
+
border: 2px solid var(--borderColor-accent, #d8dee4);
|
|
89
|
+
flex-shrink: 0;
|
|
90
|
+
transition: border-color 100ms ease, background 100ms ease;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.cellRadio[data-selected] {
|
|
94
|
+
border-color: var(--fgColor-accent, #0969da);
|
|
95
|
+
background: var(--fgColor-accent, #0969da);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.cellContent {
|
|
99
|
+
flex: 1;
|
|
100
|
+
overflow: visible;
|
|
101
|
+
position: relative;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.error {
|
|
105
|
+
display: flex;
|
|
106
|
+
flex-direction: column;
|
|
107
|
+
gap: 4px;
|
|
108
|
+
padding: 1rem;
|
|
109
|
+
color: var(--fgColor-danger, #cf222e);
|
|
110
|
+
font-size: 0.875rem;
|
|
111
|
+
line-height: 1.5;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.loading {
|
|
115
|
+
display: flex;
|
|
116
|
+
align-items: center;
|
|
117
|
+
justify-content: center;
|
|
118
|
+
padding: 3rem;
|
|
119
|
+
color: var(--fgColor-muted, #656d76);
|
|
120
|
+
font-size: 0.875rem;
|
|
121
|
+
}
|
package/src/story/StoryPage.jsx
CHANGED
|
@@ -5,12 +5,14 @@
|
|
|
5
5
|
* When ?export=ExportName is present, renders that single export.
|
|
6
6
|
* Without ?export, renders all named exports stacked.
|
|
7
7
|
*/
|
|
8
|
-
import { useState, useEffect, useMemo } from 'react'
|
|
8
|
+
import { useState, useEffect, useMemo, lazy, Suspense } from 'react'
|
|
9
9
|
import { useLocation } from 'react-router-dom'
|
|
10
10
|
import { getStoryData } from '@dfosco/storyboard-core'
|
|
11
11
|
import { ThemeProvider, BaseStyles } from '@primer/react'
|
|
12
12
|
import styles from './StoryPage.module.css'
|
|
13
13
|
|
|
14
|
+
const ComponentSetPageLazy = lazy(() => import('./ComponentSetPage.jsx'))
|
|
15
|
+
|
|
14
16
|
function StoryErrorFallback({ name, error }) {
|
|
15
17
|
return (
|
|
16
18
|
<div className={styles.error}>
|
|
@@ -25,12 +27,31 @@ export default function StoryPage({ name }) {
|
|
|
25
27
|
const searchParams = new URLSearchParams(location.search)
|
|
26
28
|
const exportFilter = searchParams.get('export')
|
|
27
29
|
const isEmbed = searchParams.has('_sb_embed')
|
|
30
|
+
const isComponentSet = searchParams.has('_sb_component_set')
|
|
31
|
+
|
|
32
|
+
// When embedded as a canvas iframe, suppress HMR full-reloads.
|
|
33
|
+
// Story content updates via React Fast Refresh; a full-reload
|
|
34
|
+
// causes a visible flash and can create a reload loop when
|
|
35
|
+
// multiple story iframes are on the same canvas.
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (!isEmbed || !import.meta.hot) return
|
|
38
|
+
const msg = { active: true }
|
|
39
|
+
import.meta.hot.send('storyboard:canvas-hmr-guard', msg)
|
|
40
|
+
const interval = setInterval(() => {
|
|
41
|
+
import.meta.hot.send('storyboard:canvas-hmr-guard', msg)
|
|
42
|
+
}, 3000)
|
|
43
|
+
return () => {
|
|
44
|
+
clearInterval(interval)
|
|
45
|
+
import.meta.hot.send('storyboard:canvas-hmr-guard', { active: false })
|
|
46
|
+
}
|
|
47
|
+
}, [isEmbed])
|
|
28
48
|
|
|
29
49
|
const story = useMemo(() => getStoryData(name), [name])
|
|
30
50
|
const [exports, setExports] = useState(null)
|
|
31
51
|
const [error, setError] = useState(null)
|
|
32
52
|
|
|
33
53
|
useEffect(() => {
|
|
54
|
+
if (isComponentSet) return
|
|
34
55
|
if (!story?._storyImport) {
|
|
35
56
|
Promise.resolve().then(() => setError(`Story "${name}" not found or missing import`))
|
|
36
57
|
return
|
|
@@ -55,7 +76,7 @@ export default function StoryPage({ name }) {
|
|
|
55
76
|
})
|
|
56
77
|
|
|
57
78
|
return () => { cancelled = true }
|
|
58
|
-
}, [name, story])
|
|
79
|
+
}, [name, story, isComponentSet])
|
|
59
80
|
|
|
60
81
|
// Signal snapshot-ready after story renders in embed mode.
|
|
61
82
|
useEffect(() => {
|
|
@@ -67,6 +88,15 @@ export default function StoryPage({ name }) {
|
|
|
67
88
|
})
|
|
68
89
|
}, [isEmbed, exports])
|
|
69
90
|
|
|
91
|
+
// Delegate to ComponentSetPage for grid view (after all hooks)
|
|
92
|
+
if (isComponentSet) {
|
|
93
|
+
return (
|
|
94
|
+
<Suspense fallback={isEmbed ? null : <div className={styles.loading}>Loading component set…</div>}>
|
|
95
|
+
<ComponentSetPageLazy name={name} />
|
|
96
|
+
</Suspense>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
70
100
|
if (error) {
|
|
71
101
|
return (
|
|
72
102
|
<StoryErrorFallback name={name} error={error} />
|
package/src/vite/data-plugin.js
CHANGED
|
@@ -5,7 +5,9 @@ import { globSync } from 'glob'
|
|
|
5
5
|
import { parse as parseJsonc } from 'jsonc-parser'
|
|
6
6
|
import { materializeFromText } from '@dfosco/storyboard-core/canvas/materializer'
|
|
7
7
|
import { toCanvasId } from '@dfosco/storyboard-core/canvas/identity'
|
|
8
|
+
import { isCanvasWriteInFlight } from '@dfosco/storyboard-core/canvas/writeGuard'
|
|
8
9
|
import { getConfig } from '@dfosco/storyboard-core/config'
|
|
10
|
+
import { list as listRunningServers } from '@dfosco/storyboard-core/worktree/serverRegistry'
|
|
9
11
|
|
|
10
12
|
const VIRTUAL_MODULE_ID = 'virtual:storyboard-data-index'
|
|
11
13
|
const RESOLVED_ID = '\0' + VIRTUAL_MODULE_ID
|
|
@@ -526,6 +528,166 @@ function readModesConfig(root) {
|
|
|
526
528
|
return fallback
|
|
527
529
|
}
|
|
528
530
|
|
|
531
|
+
/**
|
|
532
|
+
* Read a JSON/JSONC file, returning null on failure.
|
|
533
|
+
*/
|
|
534
|
+
function readJsonFile(filePath) {
|
|
535
|
+
try {
|
|
536
|
+
const raw = fs.readFileSync(filePath, 'utf-8')
|
|
537
|
+
const errors = []
|
|
538
|
+
const parsed = parseJsonc(raw, errors)
|
|
539
|
+
return errors.length === 0 ? parsed : null
|
|
540
|
+
} catch {
|
|
541
|
+
return null
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Find a core config file from either the monorepo workspace or node_modules.
|
|
547
|
+
*/
|
|
548
|
+
function readCoreConfigFile(root, filename) {
|
|
549
|
+
const candidates = [
|
|
550
|
+
path.resolve(root, `packages/core/${filename}`),
|
|
551
|
+
path.resolve(root, `node_modules/@dfosco/storyboard-core/${filename}`),
|
|
552
|
+
]
|
|
553
|
+
for (const p of candidates) {
|
|
554
|
+
const parsed = readJsonFile(p)
|
|
555
|
+
if (parsed) return parsed
|
|
556
|
+
}
|
|
557
|
+
return null
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Deep-merge helper (same as loader.js deepMerge but available at build time).
|
|
562
|
+
* Arrays are replaced, not concatenated. Objects are recursively merged.
|
|
563
|
+
*/
|
|
564
|
+
function deepMergeBuild(target, source) {
|
|
565
|
+
if (!source || typeof source !== 'object') return target
|
|
566
|
+
if (!target || typeof target !== 'object') return source
|
|
567
|
+
const result = { ...target }
|
|
568
|
+
for (const key of Object.keys(source)) {
|
|
569
|
+
const sv = source[key]
|
|
570
|
+
const tv = target[key]
|
|
571
|
+
if (sv && typeof sv === 'object' && !Array.isArray(sv) && tv && typeof tv === 'object' && !Array.isArray(tv)) {
|
|
572
|
+
result[key] = deepMergeBuild(tv, sv)
|
|
573
|
+
} else {
|
|
574
|
+
result[key] = sv
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return result
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Build the unified config object by reading and merging all config sources.
|
|
582
|
+
*
|
|
583
|
+
* Priority (lowest → highest):
|
|
584
|
+
* core defaults → user widgets → user paste → user toolbar → user commandpalette → storyboard.config.json
|
|
585
|
+
*
|
|
586
|
+
* Returns { unified, warnings } where warnings is an array of overlap messages.
|
|
587
|
+
*/
|
|
588
|
+
function buildUnifiedConfig(root) {
|
|
589
|
+
const warnings = []
|
|
590
|
+
|
|
591
|
+
// 1. Read core defaults
|
|
592
|
+
const coreToolbar = readCoreConfigFile(root, 'toolbar.config.json') || {}
|
|
593
|
+
const coreCommandPalette = readCoreConfigFile(root, 'commandpalette.config.json') || {}
|
|
594
|
+
const corePaste = readCoreConfigFile(root, 'paste.config.json') || {}
|
|
595
|
+
const coreWidgets = readCoreConfigFile(root, 'widgets.config.json') || {}
|
|
596
|
+
|
|
597
|
+
// 2. Read user config files (priority order)
|
|
598
|
+
const userFiles = [
|
|
599
|
+
{ domain: 'widgets', filename: 'widgets.config.json', priority: 1 },
|
|
600
|
+
{ domain: 'paste', filename: 'paste.config.json', priority: 2 },
|
|
601
|
+
{ domain: 'toolbar', filename: 'toolbar.config.json', priority: 3 },
|
|
602
|
+
{ domain: 'commandPalette', filename: 'commandpalette.config.json', priority: 4 },
|
|
603
|
+
]
|
|
604
|
+
|
|
605
|
+
const userConfigs = {}
|
|
606
|
+
for (const { domain, filename } of userFiles) {
|
|
607
|
+
const filePath = path.resolve(root, filename)
|
|
608
|
+
const parsed = readJsonFile(filePath)
|
|
609
|
+
if (parsed) userConfigs[domain] = { data: parsed, filename }
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// 3. Read storyboard.config.json (highest priority)
|
|
613
|
+
// Use the schema-defaulted config for most things, but also read
|
|
614
|
+
// the raw file to know which keys were explicitly set by the user.
|
|
615
|
+
const { config: sbConfig } = readConfig(root)
|
|
616
|
+
const rawSbConfig = readJsonFile(path.resolve(root, 'storyboard.config.json')) || {}
|
|
617
|
+
|
|
618
|
+
// 4. Merge core defaults with user overrides per domain
|
|
619
|
+
const toolbar = userConfigs.toolbar
|
|
620
|
+
? deepMergeBuild(coreToolbar, userConfigs.toolbar.data)
|
|
621
|
+
: coreToolbar
|
|
622
|
+
const commandPalette = userConfigs.commandPalette
|
|
623
|
+
? deepMergeBuild(coreCommandPalette, userConfigs.commandPalette.data)
|
|
624
|
+
: coreCommandPalette
|
|
625
|
+
const paste = userConfigs.paste
|
|
626
|
+
? deepMergeBuild(corePaste, userConfigs.paste.data)
|
|
627
|
+
: corePaste
|
|
628
|
+
const widgets = userConfigs.widgets
|
|
629
|
+
? deepMergeBuild(coreWidgets, userConfigs.widgets.data)
|
|
630
|
+
: coreWidgets
|
|
631
|
+
|
|
632
|
+
// 5. Apply storyboard.config.json overrides (highest priority for all domains)
|
|
633
|
+
// Only merge when the user explicitly defined the key in storyboard.config.json
|
|
634
|
+
// (not from configSchema defaults, which would overwrite core config with empty arrays).
|
|
635
|
+
const finalToolbar = rawSbConfig.toolbar
|
|
636
|
+
? deepMergeBuild(toolbar, sbConfig.toolbar)
|
|
637
|
+
: toolbar
|
|
638
|
+
const finalCommandPalette = rawSbConfig.commandPalette
|
|
639
|
+
? deepMergeBuild(commandPalette, sbConfig.commandPalette)
|
|
640
|
+
: commandPalette
|
|
641
|
+
|
|
642
|
+
// 6. Detect overlaps between user config files and storyboard.config.json
|
|
643
|
+
if (rawSbConfig.toolbar && userConfigs.toolbar) {
|
|
644
|
+
const overlaps = findOverlappingKeys(userConfigs.toolbar.data, rawSbConfig.toolbar)
|
|
645
|
+
for (const key of overlaps) {
|
|
646
|
+
warnings.push(`Config overlap: "${key}" is defined in both toolbar.config.json and storyboard.config.json.toolbar — storyboard.config.json wins.`)
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
if (rawSbConfig.commandPalette && userConfigs.commandPalette) {
|
|
650
|
+
const overlaps = findOverlappingKeys(userConfigs.commandPalette.data, rawSbConfig.commandPalette)
|
|
651
|
+
for (const key of overlaps) {
|
|
652
|
+
warnings.push(`Config overlap: "${key}" is defined in both commandpalette.config.json and storyboard.config.json.commandPalette — storyboard.config.json wins.`)
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// 7. Build the unified config object
|
|
657
|
+
const unified = {
|
|
658
|
+
toolbar: finalToolbar,
|
|
659
|
+
commandPalette: finalCommandPalette,
|
|
660
|
+
paste,
|
|
661
|
+
widgets,
|
|
662
|
+
featureFlags: sbConfig?.featureFlags || {},
|
|
663
|
+
modes: sbConfig?.modes || {},
|
|
664
|
+
ui: sbConfig?.ui || {},
|
|
665
|
+
canvas: sbConfig?.canvas || {},
|
|
666
|
+
comments: sbConfig?.comments || {},
|
|
667
|
+
customerMode: sbConfig?.customerMode || {},
|
|
668
|
+
plugins: sbConfig?.plugins || {},
|
|
669
|
+
repository: sbConfig?.repository || {},
|
|
670
|
+
workshop: sbConfig?.workshop || {},
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
return { unified, warnings }
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Find top-level keys that exist in both objects (overlap detection).
|
|
678
|
+
*/
|
|
679
|
+
function findOverlappingKeys(a, b, prefix = '') {
|
|
680
|
+
const overlaps = []
|
|
681
|
+
if (!a || !b || typeof a !== 'object' || typeof b !== 'object') return overlaps
|
|
682
|
+
for (const key of Object.keys(a)) {
|
|
683
|
+
if (key in b) {
|
|
684
|
+
const path = prefix ? `${prefix}.${key}` : key
|
|
685
|
+
overlaps.push(path)
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
return overlaps
|
|
689
|
+
}
|
|
690
|
+
|
|
529
691
|
function generateModule({ index, protoFolders, flowRoutes, canvasRoutes, canvasAliases, canvasGroups, storyRoutes }, root) {
|
|
530
692
|
const declarations = []
|
|
531
693
|
const INDEX_KEYS = ['flow', 'object', 'record', 'prototype', 'folder', 'canvas']
|
|
@@ -585,18 +747,28 @@ function generateModule({ index, protoFolders, flowRoutes, canvasRoutes, canvasA
|
|
|
585
747
|
parsed = { ...parsed, folder: protoFolders[name] }
|
|
586
748
|
}
|
|
587
749
|
|
|
588
|
-
// Load
|
|
750
|
+
// Load prototype-level config overrides from the prototype directory.
|
|
751
|
+
// Any config file placed alongside the .prototype.json becomes an override
|
|
752
|
+
// for that domain when the prototype is active.
|
|
589
753
|
if (suffix === 'prototype') {
|
|
590
754
|
const protoDir = path.dirname(absPath)
|
|
591
|
-
const
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
755
|
+
const protoConfigFiles = [
|
|
756
|
+
{ filename: 'toolbar.config.json', key: 'toolbarConfig' },
|
|
757
|
+
{ filename: 'commandpalette.config.json', key: 'commandPaletteConfig' },
|
|
758
|
+
{ filename: 'widgets.config.json', key: 'widgetsConfig' },
|
|
759
|
+
{ filename: 'paste.config.json', key: 'pasteConfig' },
|
|
760
|
+
]
|
|
761
|
+
for (const { filename, key } of protoConfigFiles) {
|
|
762
|
+
const cfgPath = path.join(protoDir, filename)
|
|
763
|
+
if (fs.existsSync(cfgPath)) {
|
|
764
|
+
try {
|
|
765
|
+
const raw = fs.readFileSync(cfgPath, 'utf-8')
|
|
766
|
+
const cfg = parseJsonc(raw)
|
|
767
|
+
if (cfg) {
|
|
768
|
+
parsed = { ...parsed, [key]: cfg }
|
|
769
|
+
}
|
|
770
|
+
} catch { /* skip invalid config */ }
|
|
771
|
+
}
|
|
600
772
|
}
|
|
601
773
|
}
|
|
602
774
|
|
|
@@ -694,6 +866,14 @@ function generateModule({ index, protoFolders, flowRoutes, canvasRoutes, canvasA
|
|
|
694
866
|
const imports = [`import { init } from '@dfosco/storyboard-core'`]
|
|
695
867
|
const initCalls = [`init({ flows, objects, records, prototypes, folders, canvases, stories })`]
|
|
696
868
|
|
|
869
|
+
// Build unified config from all sources
|
|
870
|
+
const { unified: unifiedConfig, warnings: configWarnings } = buildUnifiedConfig(root)
|
|
871
|
+
for (const w of configWarnings) {
|
|
872
|
+
console.warn(`[storyboard] ⚠ ${w}`)
|
|
873
|
+
}
|
|
874
|
+
imports.push(`import { initConfig } from '@dfosco/storyboard-core'`)
|
|
875
|
+
initCalls.push(`initConfig(${JSON.stringify(unifiedConfig)})`)
|
|
876
|
+
|
|
697
877
|
// Feature flags from storyboard.config.json
|
|
698
878
|
const { config } = readConfig(root)
|
|
699
879
|
if (config?.featureFlags && Object.keys(config.featureFlags).length > 0) {
|
|
@@ -737,6 +917,20 @@ function generateModule({ index, protoFolders, flowRoutes, canvasRoutes, canvasA
|
|
|
737
917
|
initCalls.push(`initCustomerModeConfig(${JSON.stringify(config.customerMode)})`)
|
|
738
918
|
}
|
|
739
919
|
|
|
920
|
+
// Client toolbar overrides from root toolbar.config.json
|
|
921
|
+
const clientToolbarPath = path.resolve(root, 'toolbar.config.json')
|
|
922
|
+
try {
|
|
923
|
+
if (fs.existsSync(clientToolbarPath)) {
|
|
924
|
+
const raw = fs.readFileSync(clientToolbarPath, 'utf-8')
|
|
925
|
+
const errors = []
|
|
926
|
+
const parsed = parseJsonc(raw, errors)
|
|
927
|
+
if (parsed && errors.length === 0) {
|
|
928
|
+
imports.push(`import { setClientToolbarOverrides } from '@dfosco/storyboard-core'`)
|
|
929
|
+
initCalls.push(`setClientToolbarOverrides(${JSON.stringify(parsed)})`)
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
} catch { /* skip if unreadable */ }
|
|
933
|
+
|
|
740
934
|
// Log info when multiple flows target the same route
|
|
741
935
|
const routeGroups = {}
|
|
742
936
|
for (const [name, { route, isDefault }] of Object.entries(resolvedFlowRoutes)) {
|
|
@@ -833,7 +1027,7 @@ export default function storyboardDataPlugin() {
|
|
|
833
1027
|
// can't trace into its deps. Include the remark entry points so
|
|
834
1028
|
// Vite pre-bundles the full chain — covers all transitive CJS
|
|
835
1029
|
// packages (debug, extend, etc.) without whack-a-mole.
|
|
836
|
-
include: ['
|
|
1030
|
+
include: ['cmdk', 'remark', 'remark-gfm', 'remark-html', 'use-sync-external-store/shim', 'use-sync-external-store/shim/with-selector'],
|
|
837
1031
|
exclude: ['@dfosco/storyboard-react'],
|
|
838
1032
|
},
|
|
839
1033
|
}
|
|
@@ -961,21 +1155,40 @@ export default function storyboardDataPlugin() {
|
|
|
961
1155
|
// custom HMR event with updated metadata so the canvas page and
|
|
962
1156
|
// viewfinder can react in place.
|
|
963
1157
|
if (/\.canvas\.jsonl$/.test(normalized)) {
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1158
|
+
// If this file change was caused by the canvas server API, it has
|
|
1159
|
+
// already pushed an HMR event via pushCanvasUpdate(). Skip the
|
|
1160
|
+
// duplicate watcher-triggered event to prevent stale-data rollbacks.
|
|
1161
|
+
const absPath = path.resolve(root, filePath)
|
|
1162
|
+
if (!isCanvasWriteInFlight(absPath)) {
|
|
1163
|
+
const parsed = parseDataFile(filePath)
|
|
1164
|
+
if (parsed?.suffix === 'canvas' && parsed?.id) {
|
|
1165
|
+
const metadata = readCanvasMetadata(filePath, parsed)
|
|
1166
|
+
server.ws.send({
|
|
1167
|
+
type: 'custom',
|
|
1168
|
+
event: 'storyboard:canvas-file-changed',
|
|
1169
|
+
data: { canvasId: parsed.id, name: parsed.id, ...(metadata ? { metadata } : {}) },
|
|
1170
|
+
})
|
|
1171
|
+
}
|
|
972
1172
|
}
|
|
973
1173
|
softInvalidate()
|
|
974
1174
|
return
|
|
975
1175
|
}
|
|
976
1176
|
|
|
977
|
-
// Invalidate when
|
|
978
|
-
|
|
1177
|
+
// Invalidate when any config file inside a prototype changes
|
|
1178
|
+
const protoConfigPattern = /\/(toolbar|commandpalette|widgets|paste)\.config\.json$/
|
|
1179
|
+
if (protoConfigPattern.test(normalized) && normalized.includes('/prototypes/')) {
|
|
1180
|
+
buildResult = null
|
|
1181
|
+
const mod = server.moduleGraph.getModuleById(RESOLVED_ID)
|
|
1182
|
+
if (mod) {
|
|
1183
|
+
server.moduleGraph.invalidateModule(mod)
|
|
1184
|
+
server.ws.send({ type: 'full-reload' })
|
|
1185
|
+
}
|
|
1186
|
+
return
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
// Invalidate when root toolbar.config.json changes
|
|
1190
|
+
if (normalized === path.resolve(root, 'toolbar.config.json').split(path.sep).join('/') ||
|
|
1191
|
+
normalized === path.resolve(root, 'toolbar.config.json')) {
|
|
979
1192
|
buildResult = null
|
|
980
1193
|
const mod = server.moduleGraph.getModuleById(RESOLVED_ID)
|
|
981
1194
|
if (mod) {
|
|
@@ -989,6 +1202,20 @@ export default function storyboardDataPlugin() {
|
|
|
989
1202
|
// Also invalidate when files are added/removed inside .folder/ directories
|
|
990
1203
|
const inFolder = normalized.includes('.folder/')
|
|
991
1204
|
if (!parsed && !inFolder) return
|
|
1205
|
+
// Source files inside .folder/ dirs (jsx, css, etc.) are handled by
|
|
1206
|
+
// Vite's built-in HMR / React Fast Refresh — don't full-reload for them.
|
|
1207
|
+
if (!parsed && inFolder) return
|
|
1208
|
+
|
|
1209
|
+
// Story file content changes are handled by Vite's built-in HMR
|
|
1210
|
+
// (React Fast Refresh). Only soft-invalidate the virtual module so
|
|
1211
|
+
// the next page load picks up updated metadata — don't full-reload,
|
|
1212
|
+
// which would destroy canvas state and cause embedded iframes to
|
|
1213
|
+
// reload unnecessarily.
|
|
1214
|
+
if (parsed?.suffix === 'story') {
|
|
1215
|
+
softInvalidate()
|
|
1216
|
+
return
|
|
1217
|
+
}
|
|
1218
|
+
|
|
992
1219
|
// Rebuild index and invalidate virtual module
|
|
993
1220
|
buildResult = null
|
|
994
1221
|
const mod = server.moduleGraph.getModuleById(RESOLVED_ID)
|
|
@@ -1002,6 +1229,9 @@ export default function storyboardDataPlugin() {
|
|
|
1002
1229
|
const parsed = parseDataFile(filePath)
|
|
1003
1230
|
const inFolder = filePath.replace(/\\/g, '/').includes('.folder/')
|
|
1004
1231
|
if (!parsed && !inFolder) return
|
|
1232
|
+
// Source files (jsx, css, etc.) inside .folder/ dirs are handled by
|
|
1233
|
+
// Vite's built-in HMR — don't trigger a full-reload for them.
|
|
1234
|
+
if (!parsed && inFolder) return
|
|
1005
1235
|
|
|
1006
1236
|
// Canvas writers/editors can emit unlink+add for an in-place save.
|
|
1007
1237
|
// Treat canvas add/unlink as runtime data updates and never full-reload
|
|
@@ -1097,8 +1327,14 @@ export default function storyboardDataPlugin() {
|
|
|
1097
1327
|
// Watch storyboard.config.json for changes
|
|
1098
1328
|
const { configPath } = readConfig(root)
|
|
1099
1329
|
watcher.add(configPath)
|
|
1330
|
+
|
|
1331
|
+
// Watch root toolbar.config.json for changes
|
|
1332
|
+
const clientToolbarConfigPath = path.resolve(root, 'toolbar.config.json')
|
|
1333
|
+
watcher.add(clientToolbarConfigPath)
|
|
1334
|
+
|
|
1100
1335
|
const invalidateConfig = (filePath) => {
|
|
1101
|
-
|
|
1336
|
+
const resolved = path.resolve(filePath)
|
|
1337
|
+
if (resolved === configPath || resolved === clientToolbarConfigPath) {
|
|
1102
1338
|
buildResult = null
|
|
1103
1339
|
const mod = server.moduleGraph.getModuleById(RESOLVED_ID)
|
|
1104
1340
|
if (mod) {
|
|
@@ -1128,19 +1364,16 @@ export default function storyboardDataPlugin() {
|
|
|
1128
1364
|
},
|
|
1129
1365
|
|
|
1130
1366
|
// Inject __SB_BRANCHES__ into HTML so the Viewfinder branch selector works.
|
|
1131
|
-
//
|
|
1367
|
+
// Uses server registry (live running processes) instead of stale ports.json.
|
|
1132
1368
|
transformIndexHtml(html, ctx) {
|
|
1133
1369
|
// Only inject in dev mode
|
|
1134
1370
|
if (!ctx.server) return html
|
|
1135
1371
|
|
|
1136
1372
|
try {
|
|
1137
|
-
const
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
const branches = Object.entries(ports)
|
|
1142
|
-
.filter(([name]) => name !== 'main')
|
|
1143
|
-
.map(([name, port]) => ({ branch: name, folder: `branch--${name}`, port }))
|
|
1373
|
+
const servers = listRunningServers()
|
|
1374
|
+
const branches = servers
|
|
1375
|
+
.filter(srv => srv.worktree !== 'main')
|
|
1376
|
+
.map(srv => ({ branch: srv.worktree, folder: `branch--${srv.worktree}`, port: srv.port }))
|
|
1144
1377
|
|
|
1145
1378
|
if (branches.length === 0) return html
|
|
1146
1379
|
|
|
@@ -1158,5 +1391,66 @@ export default function storyboardDataPlugin() {
|
|
|
1158
1391
|
}
|
|
1159
1392
|
}
|
|
1160
1393
|
|
|
1394
|
+
/**
|
|
1395
|
+
* Vite plugin that copies terminal snapshots into the build output
|
|
1396
|
+
* so TerminalReadWidget can fetch them as static files in production.
|
|
1397
|
+
*
|
|
1398
|
+
* Sources (in priority order):
|
|
1399
|
+
* 1. assets/.storyboard-public/terminal-snapshots/<widgetId>.snapshot.json (new, flat)
|
|
1400
|
+
* 2. .storyboard/terminal-snapshots/<canvasDir>/<widgetId>.json (legacy, nested)
|
|
1401
|
+
*
|
|
1402
|
+
* Both are emitted to `_storyboard/terminal-snapshots/` in the build.
|
|
1403
|
+
* Tilde-prefixed files (~) are excluded (private).
|
|
1404
|
+
*/
|
|
1405
|
+
export function terminalSnapshotPlugin() {
|
|
1406
|
+
return {
|
|
1407
|
+
name: 'storyboard-terminal-snapshots',
|
|
1408
|
+
|
|
1409
|
+
generateBundle() {
|
|
1410
|
+
const emittedIds = new Set()
|
|
1411
|
+
|
|
1412
|
+
// 1. New public snapshots (flat structure)
|
|
1413
|
+
const publicDir = path.resolve('assets/.storyboard-public/terminal-snapshots')
|
|
1414
|
+
if (fs.existsSync(publicDir)) {
|
|
1415
|
+
for (const file of fs.readdirSync(publicDir)) {
|
|
1416
|
+
if (file.startsWith('~') || file.startsWith('.') || !file.endsWith('.json')) continue
|
|
1417
|
+
// Extract widgetId from filename: <widgetId>.snapshot.json
|
|
1418
|
+
const widgetId = file.replace(/\.snapshot\.json$/, '')
|
|
1419
|
+
if (widgetId) emittedIds.add(widgetId)
|
|
1420
|
+
this.emitFile({
|
|
1421
|
+
type: 'asset',
|
|
1422
|
+
fileName: `_storyboard/terminal-snapshots/${file}`,
|
|
1423
|
+
source: fs.readFileSync(path.join(publicDir, file), 'utf-8'),
|
|
1424
|
+
})
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
// 2. Legacy snapshots (nested by canvas dir) — skip if already emitted
|
|
1429
|
+
const legacyDir = path.resolve('.storyboard/terminal-snapshots')
|
|
1430
|
+
if (fs.existsSync(legacyDir)) {
|
|
1431
|
+
const walk = (dir) => {
|
|
1432
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
1433
|
+
for (const entry of entries) {
|
|
1434
|
+
const full = path.join(dir, entry.name)
|
|
1435
|
+
if (entry.isDirectory()) {
|
|
1436
|
+
walk(full)
|
|
1437
|
+
} else if (entry.name.endsWith('.json') && !entry.name.startsWith('~')) {
|
|
1438
|
+
const widgetId = entry.name.replace(/\.json$/, '')
|
|
1439
|
+
if (emittedIds.has(widgetId)) continue // new format takes priority
|
|
1440
|
+
const rel = path.relative(legacyDir, full).replace(/\\/g, '/')
|
|
1441
|
+
this.emitFile({
|
|
1442
|
+
type: 'asset',
|
|
1443
|
+
fileName: `_storyboard/terminal-snapshots/${rel}`,
|
|
1444
|
+
source: fs.readFileSync(full, 'utf-8'),
|
|
1445
|
+
})
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
walk(legacyDir)
|
|
1450
|
+
}
|
|
1451
|
+
},
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1161
1455
|
// Exported for testing
|
|
1162
1456
|
export { resolveTemplateVars, computeTemplateVars, parseDataFile }
|