@dfosco/storyboard-react 4.2.0-beta.3 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +10 -11
- package/src/AuthModal/AuthModal.jsx +6 -8
- package/src/BranchBar/BranchBar.jsx +20 -6
- package/src/BranchBar/BranchBar.module.css +13 -4
- package/src/BranchBar/useBranches.js +20 -6
- package/src/BranchBar/useBranches.test.js +68 -0
- package/src/CommandPalette/CommandPalette.jsx +480 -187
- package/src/CommandPalette/command-palette.css +142 -78
- package/src/Icon.jsx +157 -58
- package/src/Viewfinder.jsx +562 -207
- package/src/Viewfinder.module.css +434 -93
- package/src/Workspace.jsx +7 -0
- package/src/canvas/CanvasPage.bridge.test.jsx +14 -6
- package/src/canvas/CanvasPage.dragdrop.test.jsx +11 -7
- package/src/canvas/CanvasPage.jsx +739 -219
- package/src/canvas/CanvasPage.module.css +13 -15
- package/src/canvas/CanvasPage.multiselect.test.jsx +17 -6
- package/src/canvas/ConnectorLayer.jsx +121 -165
- package/src/canvas/ConnectorLayer.module.css +69 -0
- package/src/canvas/PageSelector.test.jsx +15 -6
- package/src/canvas/canvasApi.js +68 -2
- package/src/canvas/canvasReloadGuard.test.js +1 -1
- package/src/canvas/connectorGeometry.js +132 -0
- package/src/canvas/hotPoolDevLogs.js +25 -0
- package/src/canvas/useCanvas.js +1 -1
- package/src/canvas/useMarqueeSelect.js +30 -4
- package/src/canvas/widgets/CodePenEmbed.jsx +1 -0
- package/src/canvas/widgets/ComponentSetWidget.jsx +199 -0
- package/src/canvas/widgets/ComponentSetWidget.module.css +89 -0
- package/src/canvas/widgets/ComponentWidget.jsx +1 -0
- package/src/canvas/widgets/CropOverlay.jsx +219 -0
- package/src/canvas/widgets/CropOverlay.module.css +118 -0
- package/src/canvas/widgets/ExpandedPane.jsx +474 -0
- package/src/canvas/widgets/ExpandedPane.module.css +179 -0
- package/src/canvas/widgets/ExpandedPane.test.jsx +240 -0
- package/src/canvas/widgets/ExpandedPaneTopBar.jsx +111 -0
- package/src/canvas/widgets/ExpandedPaneTopBar.module.css +59 -0
- package/src/canvas/widgets/ExpandedPaneTopBar.test.jsx +45 -0
- package/src/canvas/widgets/FigmaEmbed.jsx +62 -47
- package/src/canvas/widgets/FigmaEmbed.module.css +61 -0
- package/src/canvas/widgets/ImageWidget.jsx +130 -9
- package/src/canvas/widgets/ImageWidget.module.css +30 -0
- package/src/canvas/widgets/LinkPreview.jsx +113 -5
- package/src/canvas/widgets/LinkPreview.module.css +127 -0
- package/src/canvas/widgets/MarkdownBlock.jsx +167 -17
- package/src/canvas/widgets/MarkdownBlock.module.css +148 -0
- package/src/canvas/widgets/PromptWidget.jsx +414 -0
- package/src/canvas/widgets/PromptWidget.module.css +273 -0
- package/src/canvas/widgets/PrototypeEmbed.jsx +77 -39
- package/src/canvas/widgets/PrototypeEmbed.module.css +117 -0
- package/src/canvas/widgets/PrototypeEmbed.test.jsx +2 -2
- package/src/canvas/widgets/ResizeHandle.jsx +17 -6
- package/src/canvas/widgets/StoryWidget.jsx +73 -15
- package/src/canvas/widgets/TerminalReadWidget.jsx +146 -0
- package/src/canvas/widgets/TerminalReadWidget.module.css +94 -0
- package/src/canvas/widgets/TerminalWidget.jsx +445 -67
- package/src/canvas/widgets/TerminalWidget.module.css +271 -8
- package/src/canvas/widgets/TilesWidget.jsx +300 -0
- package/src/canvas/widgets/TilesWidget.module.css +133 -0
- package/src/canvas/widgets/WidgetChrome.jsx +74 -153
- package/src/canvas/widgets/WidgetChrome.module.css +30 -1
- package/src/canvas/widgets/embedInteraction.test.jsx +24 -26
- package/src/canvas/widgets/expandUtils.js +560 -0
- package/src/canvas/widgets/expandUtils.test.js +155 -0
- package/src/canvas/widgets/index.js +9 -0
- package/src/canvas/widgets/snapshotDisplay.test.jsx +23 -71
- package/src/canvas/widgets/tilePool.js +23 -0
- package/src/canvas/widgets/tiles/diagonal-bl.png +0 -0
- package/src/canvas/widgets/tiles/diagonal-br.png +0 -0
- package/src/canvas/widgets/tiles/diagonal-tl.png +0 -0
- package/src/canvas/widgets/tiles/leaf.png +0 -0
- package/src/canvas/widgets/tiles/quarter-tl.png +0 -0
- package/src/canvas/widgets/tiles/quarter-tr.png +0 -0
- package/src/canvas/widgets/tiles/solid-a.png +0 -0
- package/src/canvas/widgets/tiles/solid-b.png +0 -0
- package/src/canvas/widgets/widgetConfig.js +55 -4
- package/src/canvas/widgets/widgetIcons.jsx +190 -0
- package/src/canvas/widgets/widgetProps.js +1 -0
- package/src/context.jsx +48 -20
- package/src/hooks/useConfig.js +14 -0
- package/src/hooks/usePrototypeReloadGuard.js +64 -0
- package/src/hooks/useSceneData.js +1 -0
- package/src/hooks/useThemeState.test.js +1 -1
- package/src/index.js +8 -2
- package/src/story/ComponentSetPage.jsx +186 -0
- package/src/story/ComponentSetPage.module.css +121 -0
- package/src/story/StoryPage.jsx +32 -2
- package/src/vite/data-plugin.js +363 -67
- package/src/vite/data-plugin.test.js +1 -1
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
|
|
@@ -47,10 +49,6 @@ function parseDataFile(filePath) {
|
|
|
47
49
|
const canvasCheck = normalized.match(/(?:^|\/)src\/canvas\//)
|
|
48
50
|
if (canvasCheck) {
|
|
49
51
|
const dirPath = normalized.substring(0, normalized.lastIndexOf('/'))
|
|
50
|
-
const routeBase = (dirPath + '/')
|
|
51
|
-
.replace(/^.*?src\/canvas\//, '')
|
|
52
|
-
.replace(/[^/]*\.folder\/?/g, '')
|
|
53
|
-
.replace(/\/$/, '')
|
|
54
52
|
// Path-based ID: include folder context for uniqueness.
|
|
55
53
|
// .folder dirs contribute their name (sans .folder suffix) to the ID.
|
|
56
54
|
const idBase = (dirPath + '/')
|
|
@@ -190,39 +188,6 @@ function parseDataFile(filePath) {
|
|
|
190
188
|
return { name, suffix, ext: match[3], inferredRoute }
|
|
191
189
|
}
|
|
192
190
|
|
|
193
|
-
/**
|
|
194
|
-
* Look up the git author who first created a file.
|
|
195
|
-
* Used to auto-fill the author field in .prototype.json when missing.
|
|
196
|
-
*/
|
|
197
|
-
function getGitAuthor(root, filePath) {
|
|
198
|
-
try {
|
|
199
|
-
const result = execSync(
|
|
200
|
-
`git log --follow --diff-filter=A --format="%aN" -- "${filePath}"`,
|
|
201
|
-
{ cwd: root, encoding: 'utf-8', timeout: 5000 },
|
|
202
|
-
).trim()
|
|
203
|
-
const lines = result.split('\n').filter(Boolean)
|
|
204
|
-
return lines.length > 0 ? lines[lines.length - 1] : null
|
|
205
|
-
} catch {
|
|
206
|
-
return null
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Look up the most recent commit date for any file in a directory.
|
|
212
|
-
* Returns an ISO 8601 timestamp, or null if unavailable.
|
|
213
|
-
*/
|
|
214
|
-
function getLastModified(root, dirPath) {
|
|
215
|
-
try {
|
|
216
|
-
const result = execSync(
|
|
217
|
-
`git log -1 --format="%aI" -- "${dirPath}"`,
|
|
218
|
-
{ cwd: root, encoding: 'utf-8', timeout: 5000 },
|
|
219
|
-
).trim()
|
|
220
|
-
return result || null
|
|
221
|
-
} catch {
|
|
222
|
-
return null
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
191
|
/**
|
|
227
192
|
* Batch-fetch git metadata (author + lastModified) for multiple files in a
|
|
228
193
|
* single subprocess, avoiding per-file git overhead during startup.
|
|
@@ -526,6 +491,194 @@ function readModesConfig(root) {
|
|
|
526
491
|
return fallback
|
|
527
492
|
}
|
|
528
493
|
|
|
494
|
+
/**
|
|
495
|
+
* Read a JSON/JSONC file, returning null on failure.
|
|
496
|
+
*/
|
|
497
|
+
function readJsonFile(filePath) {
|
|
498
|
+
try {
|
|
499
|
+
const raw = fs.readFileSync(filePath, 'utf-8')
|
|
500
|
+
const errors = []
|
|
501
|
+
const parsed = parseJsonc(raw, errors)
|
|
502
|
+
return errors.length === 0 ? parsed : null
|
|
503
|
+
} catch {
|
|
504
|
+
return null
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Find a core config file from either the monorepo workspace or node_modules.
|
|
510
|
+
*/
|
|
511
|
+
function readCoreConfigFile(root, filename) {
|
|
512
|
+
const candidates = [
|
|
513
|
+
path.resolve(root, `packages/core/${filename}`),
|
|
514
|
+
path.resolve(root, `node_modules/@dfosco/storyboard-core/${filename}`),
|
|
515
|
+
]
|
|
516
|
+
for (const p of candidates) {
|
|
517
|
+
const parsed = readJsonFile(p)
|
|
518
|
+
if (parsed) return parsed
|
|
519
|
+
}
|
|
520
|
+
return null
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Deep-merge helper (same as loader.js deepMerge but available at build time).
|
|
525
|
+
* Arrays are replaced, not concatenated. Objects are recursively merged.
|
|
526
|
+
*/
|
|
527
|
+
function deepMergeBuild(target, source) {
|
|
528
|
+
if (!source || typeof source !== 'object') return target
|
|
529
|
+
if (!target || typeof target !== 'object') return source
|
|
530
|
+
const result = { ...target }
|
|
531
|
+
for (const key of Object.keys(source)) {
|
|
532
|
+
const sv = source[key]
|
|
533
|
+
const tv = target[key]
|
|
534
|
+
if (sv && typeof sv === 'object' && !Array.isArray(sv) && tv && typeof tv === 'object' && !Array.isArray(tv)) {
|
|
535
|
+
result[key] = deepMergeBuild(tv, sv)
|
|
536
|
+
} else if (Array.isArray(sv) && Array.isArray(tv) && sv.length > 0 && tv.length > 0 && sv[0]?.id && tv[0]?.id) {
|
|
537
|
+
// Id-based array merge: override matching entries by id, keep the rest, append new ones
|
|
538
|
+
const targetMap = new Map(tv.map(item => [item.id, item]))
|
|
539
|
+
for (const item of sv) {
|
|
540
|
+
targetMap.set(item.id, item.id && targetMap.has(item.id)
|
|
541
|
+
? deepMergeBuild(targetMap.get(item.id), item)
|
|
542
|
+
: item)
|
|
543
|
+
}
|
|
544
|
+
result[key] = [...targetMap.values()]
|
|
545
|
+
} else {
|
|
546
|
+
result[key] = sv
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return result
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Build the unified config object by reading and merging all config sources.
|
|
554
|
+
*
|
|
555
|
+
* Priority (lowest → highest):
|
|
556
|
+
* configSchema defaults → core domain configs → storyboard.config.json → user domain configs
|
|
557
|
+
*
|
|
558
|
+
* Domain-specific config files (toolbar.config.json, commandpalette.config.json, etc.)
|
|
559
|
+
* always win over storyboard.config.json — specificity beats generality.
|
|
560
|
+
* Deep merge is used at every layer: objects are recursively merged (keys append),
|
|
561
|
+
* arrays and scalars are replaced.
|
|
562
|
+
*
|
|
563
|
+
* Returns { unified, warnings } where warnings is an array of overlap messages.
|
|
564
|
+
*/
|
|
565
|
+
function buildUnifiedConfig(root) {
|
|
566
|
+
const warnings = []
|
|
567
|
+
|
|
568
|
+
// 1. Read core defaults (lowest priority domain configs)
|
|
569
|
+
const coreToolbar = readCoreConfigFile(root, 'toolbar.config.json') || {}
|
|
570
|
+
const coreCommandPalette = readCoreConfigFile(root, 'commandpalette.config.json') || {}
|
|
571
|
+
const corePaste = readCoreConfigFile(root, 'paste.config.json') || {}
|
|
572
|
+
const coreWidgets = readCoreConfigFile(root, 'widgets.config.json') || {}
|
|
573
|
+
|
|
574
|
+
// 2. Read storyboard.config.json (middle priority)
|
|
575
|
+
// Use the schema-defaulted config for most things, but also read
|
|
576
|
+
// the raw file to know which keys were explicitly set by the user.
|
|
577
|
+
const { config: sbConfig } = readConfig(root)
|
|
578
|
+
const rawSbConfig = readJsonFile(path.resolve(root, 'storyboard.config.json')) || {}
|
|
579
|
+
|
|
580
|
+
// 3. Apply storyboard.config.json overrides on top of core domain configs.
|
|
581
|
+
// Only merge when the user explicitly defined the key in storyboard.config.json
|
|
582
|
+
// (not from configSchema defaults, which would overwrite core config with empty arrays).
|
|
583
|
+
const afterSbToolbar = rawSbConfig.toolbar
|
|
584
|
+
? deepMergeBuild(coreToolbar, sbConfig.toolbar)
|
|
585
|
+
: coreToolbar
|
|
586
|
+
const afterSbCommandPalette = rawSbConfig.commandPalette
|
|
587
|
+
? deepMergeBuild(coreCommandPalette, sbConfig.commandPalette)
|
|
588
|
+
: coreCommandPalette
|
|
589
|
+
const afterSbPaste = rawSbConfig.paste
|
|
590
|
+
? deepMergeBuild(corePaste, sbConfig.paste || {})
|
|
591
|
+
: corePaste
|
|
592
|
+
const afterSbWidgets = rawSbConfig.widgets
|
|
593
|
+
? deepMergeBuild(coreWidgets, sbConfig.widgets || {})
|
|
594
|
+
: coreWidgets
|
|
595
|
+
|
|
596
|
+
// 4. Read user domain config files (highest priority)
|
|
597
|
+
const userFiles = [
|
|
598
|
+
{ domain: 'widgets', filename: 'widgets.config.json' },
|
|
599
|
+
{ domain: 'paste', filename: 'paste.config.json' },
|
|
600
|
+
{ domain: 'toolbar', filename: 'toolbar.config.json' },
|
|
601
|
+
{ domain: 'commandPalette', filename: 'commandpalette.config.json' },
|
|
602
|
+
]
|
|
603
|
+
|
|
604
|
+
const userConfigs = {}
|
|
605
|
+
for (const { domain, filename } of userFiles) {
|
|
606
|
+
const filePath = path.resolve(root, filename)
|
|
607
|
+
const parsed = readJsonFile(filePath)
|
|
608
|
+
if (parsed) userConfigs[domain] = { data: parsed, filename }
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// 5. Apply user domain configs on top of everything (highest priority)
|
|
612
|
+
const finalToolbar = userConfigs.toolbar
|
|
613
|
+
? deepMergeBuild(afterSbToolbar, userConfigs.toolbar.data)
|
|
614
|
+
: afterSbToolbar
|
|
615
|
+
const finalCommandPalette = userConfigs.commandPalette
|
|
616
|
+
? deepMergeBuild(afterSbCommandPalette, userConfigs.commandPalette.data)
|
|
617
|
+
: afterSbCommandPalette
|
|
618
|
+
const finalPaste = userConfigs.paste
|
|
619
|
+
? deepMergeBuild(afterSbPaste, userConfigs.paste.data)
|
|
620
|
+
: afterSbPaste
|
|
621
|
+
const finalWidgets = userConfigs.widgets
|
|
622
|
+
? deepMergeBuild(afterSbWidgets, userConfigs.widgets.data)
|
|
623
|
+
: afterSbWidgets
|
|
624
|
+
|
|
625
|
+
// 6. Detect overlaps between storyboard.config.json and user domain configs
|
|
626
|
+
const domainOverlapChecks = [
|
|
627
|
+
{ sbKey: 'toolbar', domain: 'toolbar', label: 'toolbar.config.json' },
|
|
628
|
+
{ sbKey: 'commandPalette', domain: 'commandPalette', label: 'commandpalette.config.json' },
|
|
629
|
+
{ sbKey: 'paste', domain: 'paste', label: 'paste.config.json' },
|
|
630
|
+
{ sbKey: 'widgets', domain: 'widgets', label: 'widgets.config.json' },
|
|
631
|
+
]
|
|
632
|
+
for (const { sbKey, domain, label } of domainOverlapChecks) {
|
|
633
|
+
if (rawSbConfig[sbKey] && userConfigs[domain]) {
|
|
634
|
+
const overlaps = findOverlappingKeys(rawSbConfig[sbKey], userConfigs[domain].data)
|
|
635
|
+
for (const key of overlaps) {
|
|
636
|
+
warnings.push(`Config overlap: "${key}" is defined in both storyboard.config.json.${sbKey} and ${label} — ${label} wins.`)
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// 7. Build the unified config object
|
|
642
|
+
coreCPSections: coreCommandPalette?.sections?.length,
|
|
643
|
+
afterSbCPSections: afterSbCommandPalette?.sections?.length,
|
|
644
|
+
finalCPSections: finalCommandPalette?.sections?.length,
|
|
645
|
+
rawSbHasCP: !!rawSbConfig.commandPalette,
|
|
646
|
+
userHasCP: !!userConfigs.commandPalette,
|
|
647
|
+
})
|
|
648
|
+
const unified = {
|
|
649
|
+
toolbar: finalToolbar,
|
|
650
|
+
commandPalette: finalCommandPalette,
|
|
651
|
+
paste: finalPaste,
|
|
652
|
+
widgets: finalWidgets,
|
|
653
|
+
featureFlags: sbConfig?.featureFlags || {},
|
|
654
|
+
modes: sbConfig?.modes || {},
|
|
655
|
+
ui: sbConfig?.ui || {},
|
|
656
|
+
canvas: sbConfig?.canvas || {},
|
|
657
|
+
comments: sbConfig?.comments || {},
|
|
658
|
+
customerMode: sbConfig?.customerMode || {},
|
|
659
|
+
plugins: sbConfig?.plugins || {},
|
|
660
|
+
repository: sbConfig?.repository || {},
|
|
661
|
+
workshop: sbConfig?.workshop || {},
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
return { unified, warnings }
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Find top-level keys that exist in both objects (overlap detection).
|
|
669
|
+
*/
|
|
670
|
+
function findOverlappingKeys(a, b, prefix = '') {
|
|
671
|
+
const overlaps = []
|
|
672
|
+
if (!a || !b || typeof a !== 'object' || typeof b !== 'object') return overlaps
|
|
673
|
+
for (const key of Object.keys(a)) {
|
|
674
|
+
if (key in b) {
|
|
675
|
+
const path = prefix ? `${prefix}.${key}` : key
|
|
676
|
+
overlaps.push(path)
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
return overlaps
|
|
680
|
+
}
|
|
681
|
+
|
|
529
682
|
function generateModule({ index, protoFolders, flowRoutes, canvasRoutes, canvasAliases, canvasGroups, storyRoutes }, root) {
|
|
530
683
|
const declarations = []
|
|
531
684
|
const INDEX_KEYS = ['flow', 'object', 'record', 'prototype', 'folder', 'canvas']
|
|
@@ -585,18 +738,28 @@ function generateModule({ index, protoFolders, flowRoutes, canvasRoutes, canvasA
|
|
|
585
738
|
parsed = { ...parsed, folder: protoFolders[name] }
|
|
586
739
|
}
|
|
587
740
|
|
|
588
|
-
// Load
|
|
741
|
+
// Load prototype-level config overrides from the prototype directory.
|
|
742
|
+
// Any config file placed alongside the .prototype.json becomes an override
|
|
743
|
+
// for that domain when the prototype is active.
|
|
589
744
|
if (suffix === 'prototype') {
|
|
590
745
|
const protoDir = path.dirname(absPath)
|
|
591
|
-
const
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
746
|
+
const protoConfigFiles = [
|
|
747
|
+
{ filename: 'toolbar.config.json', key: 'toolbarConfig' },
|
|
748
|
+
{ filename: 'commandpalette.config.json', key: 'commandPaletteConfig' },
|
|
749
|
+
{ filename: 'widgets.config.json', key: 'widgetsConfig' },
|
|
750
|
+
{ filename: 'paste.config.json', key: 'pasteConfig' },
|
|
751
|
+
]
|
|
752
|
+
for (const { filename, key } of protoConfigFiles) {
|
|
753
|
+
const cfgPath = path.join(protoDir, filename)
|
|
754
|
+
if (fs.existsSync(cfgPath)) {
|
|
755
|
+
try {
|
|
756
|
+
const raw = fs.readFileSync(cfgPath, 'utf-8')
|
|
757
|
+
const cfg = parseJsonc(raw)
|
|
758
|
+
if (cfg) {
|
|
759
|
+
parsed = { ...parsed, [key]: cfg }
|
|
760
|
+
}
|
|
761
|
+
} catch { /* skip invalid config */ }
|
|
762
|
+
}
|
|
600
763
|
}
|
|
601
764
|
}
|
|
602
765
|
|
|
@@ -694,6 +857,14 @@ function generateModule({ index, protoFolders, flowRoutes, canvasRoutes, canvasA
|
|
|
694
857
|
const imports = [`import { init } from '@dfosco/storyboard-core'`]
|
|
695
858
|
const initCalls = [`init({ flows, objects, records, prototypes, folders, canvases, stories })`]
|
|
696
859
|
|
|
860
|
+
// Build unified config from all sources
|
|
861
|
+
const { unified: unifiedConfig, warnings: configWarnings } = buildUnifiedConfig(root)
|
|
862
|
+
for (const w of configWarnings) {
|
|
863
|
+
console.warn(`[storyboard] ⚠ ${w}`)
|
|
864
|
+
}
|
|
865
|
+
imports.push(`import { initConfig } from '@dfosco/storyboard-core'`)
|
|
866
|
+
initCalls.push(`initConfig(${JSON.stringify(unifiedConfig)})`)
|
|
867
|
+
|
|
697
868
|
// Feature flags from storyboard.config.json
|
|
698
869
|
const { config } = readConfig(root)
|
|
699
870
|
if (config?.featureFlags && Object.keys(config.featureFlags).length > 0) {
|
|
@@ -737,6 +908,20 @@ function generateModule({ index, protoFolders, flowRoutes, canvasRoutes, canvasA
|
|
|
737
908
|
initCalls.push(`initCustomerModeConfig(${JSON.stringify(config.customerMode)})`)
|
|
738
909
|
}
|
|
739
910
|
|
|
911
|
+
// Client toolbar overrides from root toolbar.config.json
|
|
912
|
+
const clientToolbarPath = path.resolve(root, 'toolbar.config.json')
|
|
913
|
+
try {
|
|
914
|
+
if (fs.existsSync(clientToolbarPath)) {
|
|
915
|
+
const raw = fs.readFileSync(clientToolbarPath, 'utf-8')
|
|
916
|
+
const errors = []
|
|
917
|
+
const parsed = parseJsonc(raw, errors)
|
|
918
|
+
if (parsed && errors.length === 0) {
|
|
919
|
+
imports.push(`import { setClientToolbarOverrides } from '@dfosco/storyboard-core'`)
|
|
920
|
+
initCalls.push(`setClientToolbarOverrides(${JSON.stringify(parsed)})`)
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
} catch { /* skip if unreadable */ }
|
|
924
|
+
|
|
740
925
|
// Log info when multiple flows target the same route
|
|
741
926
|
const routeGroups = {}
|
|
742
927
|
for (const [name, { route, isDefault }] of Object.entries(resolvedFlowRoutes)) {
|
|
@@ -833,7 +1018,7 @@ export default function storyboardDataPlugin() {
|
|
|
833
1018
|
// can't trace into its deps. Include the remark entry points so
|
|
834
1019
|
// Vite pre-bundles the full chain — covers all transitive CJS
|
|
835
1020
|
// packages (debug, extend, etc.) without whack-a-mole.
|
|
836
|
-
include: ['
|
|
1021
|
+
include: ['cmdk', 'remark', 'remark-gfm', 'remark-html', 'use-sync-external-store/shim', 'use-sync-external-store/shim/with-selector'],
|
|
837
1022
|
exclude: ['@dfosco/storyboard-react'],
|
|
838
1023
|
},
|
|
839
1024
|
}
|
|
@@ -961,21 +1146,40 @@ export default function storyboardDataPlugin() {
|
|
|
961
1146
|
// custom HMR event with updated metadata so the canvas page and
|
|
962
1147
|
// viewfinder can react in place.
|
|
963
1148
|
if (/\.canvas\.jsonl$/.test(normalized)) {
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1149
|
+
// If this file change was caused by the canvas server API, it has
|
|
1150
|
+
// already pushed an HMR event via pushCanvasUpdate(). Skip the
|
|
1151
|
+
// duplicate watcher-triggered event to prevent stale-data rollbacks.
|
|
1152
|
+
const absPath = path.resolve(root, filePath)
|
|
1153
|
+
if (!isCanvasWriteInFlight(absPath)) {
|
|
1154
|
+
const parsed = parseDataFile(filePath)
|
|
1155
|
+
if (parsed?.suffix === 'canvas' && parsed?.id) {
|
|
1156
|
+
const metadata = readCanvasMetadata(filePath, parsed)
|
|
1157
|
+
server.ws.send({
|
|
1158
|
+
type: 'custom',
|
|
1159
|
+
event: 'storyboard:canvas-file-changed',
|
|
1160
|
+
data: { canvasId: parsed.id, name: parsed.id, ...(metadata ? { metadata } : {}) },
|
|
1161
|
+
})
|
|
1162
|
+
}
|
|
972
1163
|
}
|
|
973
1164
|
softInvalidate()
|
|
974
1165
|
return
|
|
975
1166
|
}
|
|
976
1167
|
|
|
977
|
-
// Invalidate when
|
|
978
|
-
|
|
1168
|
+
// Invalidate when any config file inside a prototype changes
|
|
1169
|
+
const protoConfigPattern = /\/(toolbar|commandpalette|widgets|paste)\.config\.json$/
|
|
1170
|
+
if (protoConfigPattern.test(normalized) && normalized.includes('/prototypes/')) {
|
|
1171
|
+
buildResult = null
|
|
1172
|
+
const mod = server.moduleGraph.getModuleById(RESOLVED_ID)
|
|
1173
|
+
if (mod) {
|
|
1174
|
+
server.moduleGraph.invalidateModule(mod)
|
|
1175
|
+
server.ws.send({ type: 'full-reload' })
|
|
1176
|
+
}
|
|
1177
|
+
return
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// Invalidate when root toolbar.config.json changes
|
|
1181
|
+
if (normalized === path.resolve(root, 'toolbar.config.json').split(path.sep).join('/') ||
|
|
1182
|
+
normalized === path.resolve(root, 'toolbar.config.json')) {
|
|
979
1183
|
buildResult = null
|
|
980
1184
|
const mod = server.moduleGraph.getModuleById(RESOLVED_ID)
|
|
981
1185
|
if (mod) {
|
|
@@ -989,6 +1193,20 @@ export default function storyboardDataPlugin() {
|
|
|
989
1193
|
// Also invalidate when files are added/removed inside .folder/ directories
|
|
990
1194
|
const inFolder = normalized.includes('.folder/')
|
|
991
1195
|
if (!parsed && !inFolder) return
|
|
1196
|
+
// Source files inside .folder/ dirs (jsx, css, etc.) are handled by
|
|
1197
|
+
// Vite's built-in HMR / React Fast Refresh — don't full-reload for them.
|
|
1198
|
+
if (!parsed && inFolder) return
|
|
1199
|
+
|
|
1200
|
+
// Story file content changes are handled by Vite's built-in HMR
|
|
1201
|
+
// (React Fast Refresh). Only soft-invalidate the virtual module so
|
|
1202
|
+
// the next page load picks up updated metadata — don't full-reload,
|
|
1203
|
+
// which would destroy canvas state and cause embedded iframes to
|
|
1204
|
+
// reload unnecessarily.
|
|
1205
|
+
if (parsed?.suffix === 'story') {
|
|
1206
|
+
softInvalidate()
|
|
1207
|
+
return
|
|
1208
|
+
}
|
|
1209
|
+
|
|
992
1210
|
// Rebuild index and invalidate virtual module
|
|
993
1211
|
buildResult = null
|
|
994
1212
|
const mod = server.moduleGraph.getModuleById(RESOLVED_ID)
|
|
@@ -1002,6 +1220,9 @@ export default function storyboardDataPlugin() {
|
|
|
1002
1220
|
const parsed = parseDataFile(filePath)
|
|
1003
1221
|
const inFolder = filePath.replace(/\\/g, '/').includes('.folder/')
|
|
1004
1222
|
if (!parsed && !inFolder) return
|
|
1223
|
+
// Source files (jsx, css, etc.) inside .folder/ dirs are handled by
|
|
1224
|
+
// Vite's built-in HMR — don't trigger a full-reload for them.
|
|
1225
|
+
if (!parsed && inFolder) return
|
|
1005
1226
|
|
|
1006
1227
|
// Canvas writers/editors can emit unlink+add for an in-place save.
|
|
1007
1228
|
// Treat canvas add/unlink as runtime data updates and never full-reload
|
|
@@ -1097,8 +1318,20 @@ export default function storyboardDataPlugin() {
|
|
|
1097
1318
|
// Watch storyboard.config.json for changes
|
|
1098
1319
|
const { configPath } = readConfig(root)
|
|
1099
1320
|
watcher.add(configPath)
|
|
1321
|
+
|
|
1322
|
+
// Watch all root domain config files for changes
|
|
1323
|
+
const domainConfigFiles = [
|
|
1324
|
+
'toolbar.config.json',
|
|
1325
|
+
'commandpalette.config.json',
|
|
1326
|
+
'paste.config.json',
|
|
1327
|
+
'widgets.config.json',
|
|
1328
|
+
].map(f => path.resolve(root, f))
|
|
1329
|
+
const watchedConfigPaths = new Set([configPath, ...domainConfigFiles])
|
|
1330
|
+
for (const p of domainConfigFiles) watcher.add(p)
|
|
1331
|
+
|
|
1100
1332
|
const invalidateConfig = (filePath) => {
|
|
1101
|
-
|
|
1333
|
+
const resolved = path.resolve(filePath)
|
|
1334
|
+
if (watchedConfigPaths.has(resolved)) {
|
|
1102
1335
|
buildResult = null
|
|
1103
1336
|
const mod = server.moduleGraph.getModuleById(RESOLVED_ID)
|
|
1104
1337
|
if (mod) {
|
|
@@ -1128,19 +1361,16 @@ export default function storyboardDataPlugin() {
|
|
|
1128
1361
|
},
|
|
1129
1362
|
|
|
1130
1363
|
// Inject __SB_BRANCHES__ into HTML so the Viewfinder branch selector works.
|
|
1131
|
-
//
|
|
1364
|
+
// Uses server registry (live running processes) instead of stale ports.json.
|
|
1132
1365
|
transformIndexHtml(html, ctx) {
|
|
1133
1366
|
// Only inject in dev mode
|
|
1134
1367
|
if (!ctx.server) return html
|
|
1135
1368
|
|
|
1136
1369
|
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 }))
|
|
1370
|
+
const servers = listRunningServers()
|
|
1371
|
+
const branches = servers
|
|
1372
|
+
.filter(srv => srv.worktree !== 'main')
|
|
1373
|
+
.map(srv => ({ branch: srv.worktree, folder: `branch--${srv.worktree}`, port: srv.port }))
|
|
1144
1374
|
|
|
1145
1375
|
if (branches.length === 0) return html
|
|
1146
1376
|
|
|
@@ -1158,5 +1388,71 @@ export default function storyboardDataPlugin() {
|
|
|
1158
1388
|
}
|
|
1159
1389
|
}
|
|
1160
1390
|
|
|
1391
|
+
/**
|
|
1392
|
+
* Vite plugin that copies terminal snapshots into the build output
|
|
1393
|
+
* so TerminalReadWidget can fetch them as static files in production.
|
|
1394
|
+
*
|
|
1395
|
+
* Sources (in priority order):
|
|
1396
|
+
* 1. assets/.storyboard-public/terminal-snapshots/<widgetId>.snapshot.json (new, flat)
|
|
1397
|
+
* 2. assets/.storyboard-public/terminal-snapshots/<widgetId>.snapshot.txt (human-readable companion)
|
|
1398
|
+
* 3. .storyboard/terminal-snapshots/<canvasDir>/<widgetId>.json (legacy, nested)
|
|
1399
|
+
*
|
|
1400
|
+
* All are emitted to `_storyboard/terminal-snapshots/` in the build.
|
|
1401
|
+
* Tilde-prefixed files (~) are excluded (private).
|
|
1402
|
+
*/
|
|
1403
|
+
export function terminalSnapshotPlugin() {
|
|
1404
|
+
return {
|
|
1405
|
+
name: 'storyboard-terminal-snapshots',
|
|
1406
|
+
|
|
1407
|
+
generateBundle() {
|
|
1408
|
+
const emittedIds = new Set()
|
|
1409
|
+
|
|
1410
|
+
// 1. New public snapshots (flat structure) — .json and .txt
|
|
1411
|
+
const publicDir = path.resolve('assets/.storyboard-public/terminal-snapshots')
|
|
1412
|
+
if (fs.existsSync(publicDir)) {
|
|
1413
|
+
for (const file of fs.readdirSync(publicDir)) {
|
|
1414
|
+
if (file.startsWith('~') || file.startsWith('.')) continue
|
|
1415
|
+
const isJson = file.endsWith('.snapshot.json')
|
|
1416
|
+
const isTxt = file.endsWith('.snapshot.txt')
|
|
1417
|
+
if (!isJson && !isTxt) continue
|
|
1418
|
+
if (isJson) {
|
|
1419
|
+
const widgetId = file.replace(/\.snapshot\.json$/, '')
|
|
1420
|
+
if (widgetId) emittedIds.add(widgetId)
|
|
1421
|
+
}
|
|
1422
|
+
this.emitFile({
|
|
1423
|
+
type: 'asset',
|
|
1424
|
+
fileName: `_storyboard/terminal-snapshots/${file}`,
|
|
1425
|
+
source: fs.readFileSync(path.join(publicDir, file), 'utf-8'),
|
|
1426
|
+
})
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
// 2. Legacy snapshots (nested by canvas dir) — skip if already emitted
|
|
1431
|
+
const legacyDir = path.resolve('.storyboard/terminal-snapshots')
|
|
1432
|
+
if (fs.existsSync(legacyDir)) {
|
|
1433
|
+
const walk = (dir) => {
|
|
1434
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
1435
|
+
for (const entry of entries) {
|
|
1436
|
+
const full = path.join(dir, entry.name)
|
|
1437
|
+
if (entry.isDirectory()) {
|
|
1438
|
+
walk(full)
|
|
1439
|
+
} else if (entry.name.endsWith('.json') && !entry.name.startsWith('~')) {
|
|
1440
|
+
const widgetId = entry.name.replace(/\.json$/, '')
|
|
1441
|
+
if (emittedIds.has(widgetId)) continue // new format takes priority
|
|
1442
|
+
const rel = path.relative(legacyDir, full).replace(/\\/g, '/')
|
|
1443
|
+
this.emitFile({
|
|
1444
|
+
type: 'asset',
|
|
1445
|
+
fileName: `_storyboard/terminal-snapshots/${rel}`,
|
|
1446
|
+
source: fs.readFileSync(full, 'utf-8'),
|
|
1447
|
+
})
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
walk(legacyDir)
|
|
1452
|
+
}
|
|
1453
|
+
},
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1161
1457
|
// Exported for testing
|
|
1162
1458
|
export { resolveTemplateVars, computeTemplateVars, parseDataFile }
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdtempSync, writeFileSync, mkdirSync, rmSync
|
|
1
|
+
import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from 'node:fs'
|
|
2
2
|
import { tmpdir } from 'node:os'
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import storyboardDataPlugin, { resolveTemplateVars, computeTemplateVars, parseDataFile } from './data-plugin.js'
|