@dfosco/storyboard-react 4.2.0-beta.4 → 4.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +10 -11
- package/src/AuthModal/AuthModal.jsx +6 -8
- package/src/BranchBar/BranchBar.jsx +20 -6
- package/src/BranchBar/BranchBar.module.css +13 -4
- package/src/BranchBar/useBranches.js +20 -6
- package/src/BranchBar/useBranches.test.js +68 -0
- package/src/CommandPalette/CommandPalette.jsx +480 -187
- package/src/CommandPalette/command-palette.css +142 -78
- package/src/Icon.jsx +157 -58
- package/src/Viewfinder.jsx +562 -207
- package/src/Viewfinder.module.css +434 -93
- package/src/Workspace.jsx +7 -0
- package/src/canvas/CanvasPage.bridge.test.jsx +14 -6
- package/src/canvas/CanvasPage.dragdrop.test.jsx +11 -7
- package/src/canvas/CanvasPage.jsx +739 -219
- package/src/canvas/CanvasPage.module.css +13 -15
- package/src/canvas/CanvasPage.multiselect.test.jsx +17 -6
- package/src/canvas/ConnectorLayer.jsx +121 -165
- package/src/canvas/ConnectorLayer.module.css +69 -0
- package/src/canvas/PageSelector.test.jsx +15 -6
- package/src/canvas/canvasApi.js +68 -2
- package/src/canvas/canvasReloadGuard.test.js +1 -1
- package/src/canvas/connectorGeometry.js +132 -0
- package/src/canvas/hotPoolDevLogs.js +25 -0
- package/src/canvas/useCanvas.js +1 -1
- package/src/canvas/useMarqueeSelect.js +30 -4
- package/src/canvas/widgets/CodePenEmbed.jsx +1 -0
- package/src/canvas/widgets/ComponentSetWidget.jsx +199 -0
- package/src/canvas/widgets/ComponentSetWidget.module.css +89 -0
- package/src/canvas/widgets/ComponentWidget.jsx +1 -0
- package/src/canvas/widgets/CropOverlay.jsx +219 -0
- package/src/canvas/widgets/CropOverlay.module.css +118 -0
- package/src/canvas/widgets/ExpandedPane.jsx +474 -0
- package/src/canvas/widgets/ExpandedPane.module.css +179 -0
- package/src/canvas/widgets/ExpandedPane.test.jsx +240 -0
- package/src/canvas/widgets/ExpandedPaneTopBar.jsx +111 -0
- package/src/canvas/widgets/ExpandedPaneTopBar.module.css +59 -0
- package/src/canvas/widgets/ExpandedPaneTopBar.test.jsx +45 -0
- package/src/canvas/widgets/FigmaEmbed.jsx +62 -47
- package/src/canvas/widgets/FigmaEmbed.module.css +61 -0
- package/src/canvas/widgets/ImageWidget.jsx +130 -9
- package/src/canvas/widgets/ImageWidget.module.css +30 -0
- package/src/canvas/widgets/LinkPreview.jsx +113 -5
- package/src/canvas/widgets/LinkPreview.module.css +127 -0
- package/src/canvas/widgets/MarkdownBlock.jsx +167 -17
- package/src/canvas/widgets/MarkdownBlock.module.css +148 -0
- package/src/canvas/widgets/PromptWidget.jsx +414 -0
- package/src/canvas/widgets/PromptWidget.module.css +273 -0
- package/src/canvas/widgets/PrototypeEmbed.jsx +77 -39
- package/src/canvas/widgets/PrototypeEmbed.module.css +117 -0
- package/src/canvas/widgets/PrototypeEmbed.test.jsx +2 -2
- package/src/canvas/widgets/ResizeHandle.jsx +17 -6
- package/src/canvas/widgets/StoryWidget.jsx +73 -15
- package/src/canvas/widgets/TerminalReadWidget.jsx +146 -0
- package/src/canvas/widgets/TerminalReadWidget.module.css +94 -0
- package/src/canvas/widgets/TerminalWidget.jsx +445 -67
- package/src/canvas/widgets/TerminalWidget.module.css +271 -8
- package/src/canvas/widgets/TilesWidget.jsx +300 -0
- package/src/canvas/widgets/TilesWidget.module.css +133 -0
- package/src/canvas/widgets/WidgetChrome.jsx +74 -153
- package/src/canvas/widgets/WidgetChrome.module.css +30 -1
- package/src/canvas/widgets/embedInteraction.test.jsx +24 -26
- package/src/canvas/widgets/expandUtils.js +560 -0
- package/src/canvas/widgets/expandUtils.test.js +155 -0
- package/src/canvas/widgets/index.js +9 -0
- package/src/canvas/widgets/snapshotDisplay.test.jsx +23 -71
- package/src/canvas/widgets/tilePool.js +23 -0
- package/src/canvas/widgets/tiles/diagonal-bl.png +0 -0
- package/src/canvas/widgets/tiles/diagonal-br.png +0 -0
- package/src/canvas/widgets/tiles/diagonal-tl.png +0 -0
- package/src/canvas/widgets/tiles/leaf.png +0 -0
- package/src/canvas/widgets/tiles/quarter-tl.png +0 -0
- package/src/canvas/widgets/tiles/quarter-tr.png +0 -0
- package/src/canvas/widgets/tiles/solid-a.png +0 -0
- package/src/canvas/widgets/tiles/solid-b.png +0 -0
- package/src/canvas/widgets/widgetConfig.js +55 -4
- package/src/canvas/widgets/widgetIcons.jsx +190 -0
- package/src/canvas/widgets/widgetProps.js +1 -0
- package/src/context.jsx +48 -20
- package/src/hooks/useConfig.js +14 -0
- package/src/hooks/usePrototypeReloadGuard.js +64 -0
- package/src/hooks/useSceneData.js +1 -0
- package/src/hooks/useThemeState.test.js +1 -1
- package/src/index.js +8 -2
- package/src/story/ComponentSetPage.jsx +186 -0
- package/src/story/ComponentSetPage.module.css +121 -0
- package/src/story/StoryPage.jsx +32 -2
- package/src/vite/data-plugin.js +407 -67
- package/src/vite/data-plugin.test.js +1 -1
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,188 @@ 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
|
+
const unified = {
|
|
643
|
+
toolbar: finalToolbar,
|
|
644
|
+
commandPalette: finalCommandPalette,
|
|
645
|
+
paste: finalPaste,
|
|
646
|
+
widgets: finalWidgets,
|
|
647
|
+
featureFlags: sbConfig?.featureFlags || {},
|
|
648
|
+
modes: sbConfig?.modes || {},
|
|
649
|
+
ui: sbConfig?.ui || {},
|
|
650
|
+
canvas: sbConfig?.canvas || {},
|
|
651
|
+
comments: sbConfig?.comments || {},
|
|
652
|
+
customerMode: sbConfig?.customerMode || {},
|
|
653
|
+
plugins: sbConfig?.plugins || {},
|
|
654
|
+
repository: sbConfig?.repository || {},
|
|
655
|
+
workshop: sbConfig?.workshop || {},
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return { unified, warnings }
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Find top-level keys that exist in both objects (overlap detection).
|
|
663
|
+
*/
|
|
664
|
+
function findOverlappingKeys(a, b, prefix = '') {
|
|
665
|
+
const overlaps = []
|
|
666
|
+
if (!a || !b || typeof a !== 'object' || typeof b !== 'object') return overlaps
|
|
667
|
+
for (const key of Object.keys(a)) {
|
|
668
|
+
if (key in b) {
|
|
669
|
+
const path = prefix ? `${prefix}.${key}` : key
|
|
670
|
+
overlaps.push(path)
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return overlaps
|
|
674
|
+
}
|
|
675
|
+
|
|
529
676
|
function generateModule({ index, protoFolders, flowRoutes, canvasRoutes, canvasAliases, canvasGroups, storyRoutes }, root) {
|
|
530
677
|
const declarations = []
|
|
531
678
|
const INDEX_KEYS = ['flow', 'object', 'record', 'prototype', 'folder', 'canvas']
|
|
@@ -585,18 +732,28 @@ function generateModule({ index, protoFolders, flowRoutes, canvasRoutes, canvasA
|
|
|
585
732
|
parsed = { ...parsed, folder: protoFolders[name] }
|
|
586
733
|
}
|
|
587
734
|
|
|
588
|
-
// Load
|
|
735
|
+
// Load prototype-level config overrides from the prototype directory.
|
|
736
|
+
// Any config file placed alongside the .prototype.json becomes an override
|
|
737
|
+
// for that domain when the prototype is active.
|
|
589
738
|
if (suffix === 'prototype') {
|
|
590
739
|
const protoDir = path.dirname(absPath)
|
|
591
|
-
const
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
740
|
+
const protoConfigFiles = [
|
|
741
|
+
{ filename: 'toolbar.config.json', key: 'toolbarConfig' },
|
|
742
|
+
{ filename: 'commandpalette.config.json', key: 'commandPaletteConfig' },
|
|
743
|
+
{ filename: 'widgets.config.json', key: 'widgetsConfig' },
|
|
744
|
+
{ filename: 'paste.config.json', key: 'pasteConfig' },
|
|
745
|
+
]
|
|
746
|
+
for (const { filename, key } of protoConfigFiles) {
|
|
747
|
+
const cfgPath = path.join(protoDir, filename)
|
|
748
|
+
if (fs.existsSync(cfgPath)) {
|
|
749
|
+
try {
|
|
750
|
+
const raw = fs.readFileSync(cfgPath, 'utf-8')
|
|
751
|
+
const cfg = parseJsonc(raw)
|
|
752
|
+
if (cfg) {
|
|
753
|
+
parsed = { ...parsed, [key]: cfg }
|
|
754
|
+
}
|
|
755
|
+
} catch { /* skip invalid config */ }
|
|
756
|
+
}
|
|
600
757
|
}
|
|
601
758
|
}
|
|
602
759
|
|
|
@@ -694,6 +851,14 @@ function generateModule({ index, protoFolders, flowRoutes, canvasRoutes, canvasA
|
|
|
694
851
|
const imports = [`import { init } from '@dfosco/storyboard-core'`]
|
|
695
852
|
const initCalls = [`init({ flows, objects, records, prototypes, folders, canvases, stories })`]
|
|
696
853
|
|
|
854
|
+
// Build unified config from all sources
|
|
855
|
+
const { unified: unifiedConfig, warnings: configWarnings } = buildUnifiedConfig(root)
|
|
856
|
+
for (const w of configWarnings) {
|
|
857
|
+
console.warn(`[storyboard] ⚠ ${w}`)
|
|
858
|
+
}
|
|
859
|
+
imports.push(`import { initConfig } from '@dfosco/storyboard-core'`)
|
|
860
|
+
initCalls.push(`initConfig(${JSON.stringify(unifiedConfig)})`)
|
|
861
|
+
|
|
697
862
|
// Feature flags from storyboard.config.json
|
|
698
863
|
const { config } = readConfig(root)
|
|
699
864
|
if (config?.featureFlags && Object.keys(config.featureFlags).length > 0) {
|
|
@@ -737,6 +902,20 @@ function generateModule({ index, protoFolders, flowRoutes, canvasRoutes, canvasA
|
|
|
737
902
|
initCalls.push(`initCustomerModeConfig(${JSON.stringify(config.customerMode)})`)
|
|
738
903
|
}
|
|
739
904
|
|
|
905
|
+
// Client toolbar overrides from root toolbar.config.json
|
|
906
|
+
const clientToolbarPath = path.resolve(root, 'toolbar.config.json')
|
|
907
|
+
try {
|
|
908
|
+
if (fs.existsSync(clientToolbarPath)) {
|
|
909
|
+
const raw = fs.readFileSync(clientToolbarPath, 'utf-8')
|
|
910
|
+
const errors = []
|
|
911
|
+
const parsed = parseJsonc(raw, errors)
|
|
912
|
+
if (parsed && errors.length === 0) {
|
|
913
|
+
imports.push(`import { setClientToolbarOverrides } from '@dfosco/storyboard-core'`)
|
|
914
|
+
initCalls.push(`setClientToolbarOverrides(${JSON.stringify(parsed)})`)
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
} catch { /* skip if unreadable */ }
|
|
918
|
+
|
|
740
919
|
// Log info when multiple flows target the same route
|
|
741
920
|
const routeGroups = {}
|
|
742
921
|
for (const [name, { route, isDefault }] of Object.entries(resolvedFlowRoutes)) {
|
|
@@ -833,7 +1012,7 @@ export default function storyboardDataPlugin() {
|
|
|
833
1012
|
// can't trace into its deps. Include the remark entry points so
|
|
834
1013
|
// Vite pre-bundles the full chain — covers all transitive CJS
|
|
835
1014
|
// packages (debug, extend, etc.) without whack-a-mole.
|
|
836
|
-
include: ['
|
|
1015
|
+
include: ['cmdk', 'remark', 'remark-gfm', 'remark-html', 'use-sync-external-store/shim', 'use-sync-external-store/shim/with-selector'],
|
|
837
1016
|
exclude: ['@dfosco/storyboard-react'],
|
|
838
1017
|
},
|
|
839
1018
|
}
|
|
@@ -961,21 +1140,40 @@ export default function storyboardDataPlugin() {
|
|
|
961
1140
|
// custom HMR event with updated metadata so the canvas page and
|
|
962
1141
|
// viewfinder can react in place.
|
|
963
1142
|
if (/\.canvas\.jsonl$/.test(normalized)) {
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1143
|
+
// If this file change was caused by the canvas server API, it has
|
|
1144
|
+
// already pushed an HMR event via pushCanvasUpdate(). Skip the
|
|
1145
|
+
// duplicate watcher-triggered event to prevent stale-data rollbacks.
|
|
1146
|
+
const absPath = path.resolve(root, filePath)
|
|
1147
|
+
if (!isCanvasWriteInFlight(absPath)) {
|
|
1148
|
+
const parsed = parseDataFile(filePath)
|
|
1149
|
+
if (parsed?.suffix === 'canvas' && parsed?.id) {
|
|
1150
|
+
const metadata = readCanvasMetadata(filePath, parsed)
|
|
1151
|
+
server.ws.send({
|
|
1152
|
+
type: 'custom',
|
|
1153
|
+
event: 'storyboard:canvas-file-changed',
|
|
1154
|
+
data: { canvasId: parsed.id, name: parsed.id, ...(metadata ? { metadata } : {}) },
|
|
1155
|
+
})
|
|
1156
|
+
}
|
|
972
1157
|
}
|
|
973
1158
|
softInvalidate()
|
|
974
1159
|
return
|
|
975
1160
|
}
|
|
976
1161
|
|
|
977
|
-
// Invalidate when
|
|
978
|
-
|
|
1162
|
+
// Invalidate when any config file inside a prototype changes
|
|
1163
|
+
const protoConfigPattern = /\/(toolbar|commandpalette|widgets|paste)\.config\.json$/
|
|
1164
|
+
if (protoConfigPattern.test(normalized) && normalized.includes('/prototypes/')) {
|
|
1165
|
+
buildResult = null
|
|
1166
|
+
const mod = server.moduleGraph.getModuleById(RESOLVED_ID)
|
|
1167
|
+
if (mod) {
|
|
1168
|
+
server.moduleGraph.invalidateModule(mod)
|
|
1169
|
+
server.ws.send({ type: 'full-reload' })
|
|
1170
|
+
}
|
|
1171
|
+
return
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// Invalidate when root toolbar.config.json changes
|
|
1175
|
+
if (normalized === path.resolve(root, 'toolbar.config.json').split(path.sep).join('/') ||
|
|
1176
|
+
normalized === path.resolve(root, 'toolbar.config.json')) {
|
|
979
1177
|
buildResult = null
|
|
980
1178
|
const mod = server.moduleGraph.getModuleById(RESOLVED_ID)
|
|
981
1179
|
if (mod) {
|
|
@@ -989,6 +1187,20 @@ export default function storyboardDataPlugin() {
|
|
|
989
1187
|
// Also invalidate when files are added/removed inside .folder/ directories
|
|
990
1188
|
const inFolder = normalized.includes('.folder/')
|
|
991
1189
|
if (!parsed && !inFolder) return
|
|
1190
|
+
// Source files inside .folder/ dirs (jsx, css, etc.) are handled by
|
|
1191
|
+
// Vite's built-in HMR / React Fast Refresh — don't full-reload for them.
|
|
1192
|
+
if (!parsed && inFolder) return
|
|
1193
|
+
|
|
1194
|
+
// Story file content changes are handled by Vite's built-in HMR
|
|
1195
|
+
// (React Fast Refresh). Only soft-invalidate the virtual module so
|
|
1196
|
+
// the next page load picks up updated metadata — don't full-reload,
|
|
1197
|
+
// which would destroy canvas state and cause embedded iframes to
|
|
1198
|
+
// reload unnecessarily.
|
|
1199
|
+
if (parsed?.suffix === 'story') {
|
|
1200
|
+
softInvalidate()
|
|
1201
|
+
return
|
|
1202
|
+
}
|
|
1203
|
+
|
|
992
1204
|
// Rebuild index and invalidate virtual module
|
|
993
1205
|
buildResult = null
|
|
994
1206
|
const mod = server.moduleGraph.getModuleById(RESOLVED_ID)
|
|
@@ -1002,6 +1214,9 @@ export default function storyboardDataPlugin() {
|
|
|
1002
1214
|
const parsed = parseDataFile(filePath)
|
|
1003
1215
|
const inFolder = filePath.replace(/\\/g, '/').includes('.folder/')
|
|
1004
1216
|
if (!parsed && !inFolder) return
|
|
1217
|
+
// Source files (jsx, css, etc.) inside .folder/ dirs are handled by
|
|
1218
|
+
// Vite's built-in HMR — don't trigger a full-reload for them.
|
|
1219
|
+
if (!parsed && inFolder) return
|
|
1005
1220
|
|
|
1006
1221
|
// Canvas writers/editors can emit unlink+add for an in-place save.
|
|
1007
1222
|
// Treat canvas add/unlink as runtime data updates and never full-reload
|
|
@@ -1097,8 +1312,20 @@ export default function storyboardDataPlugin() {
|
|
|
1097
1312
|
// Watch storyboard.config.json for changes
|
|
1098
1313
|
const { configPath } = readConfig(root)
|
|
1099
1314
|
watcher.add(configPath)
|
|
1315
|
+
|
|
1316
|
+
// Watch all root domain config files for changes
|
|
1317
|
+
const domainConfigFiles = [
|
|
1318
|
+
'toolbar.config.json',
|
|
1319
|
+
'commandpalette.config.json',
|
|
1320
|
+
'paste.config.json',
|
|
1321
|
+
'widgets.config.json',
|
|
1322
|
+
].map(f => path.resolve(root, f))
|
|
1323
|
+
const watchedConfigPaths = new Set([configPath, ...domainConfigFiles])
|
|
1324
|
+
for (const p of domainConfigFiles) watcher.add(p)
|
|
1325
|
+
|
|
1100
1326
|
const invalidateConfig = (filePath) => {
|
|
1101
|
-
|
|
1327
|
+
const resolved = path.resolve(filePath)
|
|
1328
|
+
if (watchedConfigPaths.has(resolved)) {
|
|
1102
1329
|
buildResult = null
|
|
1103
1330
|
const mod = server.moduleGraph.getModuleById(RESOLVED_ID)
|
|
1104
1331
|
if (mod) {
|
|
@@ -1128,19 +1355,16 @@ export default function storyboardDataPlugin() {
|
|
|
1128
1355
|
},
|
|
1129
1356
|
|
|
1130
1357
|
// Inject __SB_BRANCHES__ into HTML so the Viewfinder branch selector works.
|
|
1131
|
-
//
|
|
1358
|
+
// Uses server registry (live running processes) instead of stale ports.json.
|
|
1132
1359
|
transformIndexHtml(html, ctx) {
|
|
1133
1360
|
// Only inject in dev mode
|
|
1134
1361
|
if (!ctx.server) return html
|
|
1135
1362
|
|
|
1136
1363
|
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 }))
|
|
1364
|
+
const servers = listRunningServers()
|
|
1365
|
+
const branches = servers
|
|
1366
|
+
.filter(srv => srv.worktree !== 'main')
|
|
1367
|
+
.map(srv => ({ branch: srv.worktree, folder: `branch--${srv.worktree}`, port: srv.port }))
|
|
1144
1368
|
|
|
1145
1369
|
if (branches.length === 0) return html
|
|
1146
1370
|
|
|
@@ -1155,6 +1379,122 @@ export default function storyboardDataPlugin() {
|
|
|
1155
1379
|
buildStart() {
|
|
1156
1380
|
buildResult = null
|
|
1157
1381
|
},
|
|
1382
|
+
|
|
1383
|
+
// Emit terminal snapshots into the build so TerminalReadWidget can
|
|
1384
|
+
// fetch them as static files in production (no dev-server API).
|
|
1385
|
+
generateBundle() {
|
|
1386
|
+
const emittedIds = new Set()
|
|
1387
|
+
|
|
1388
|
+
// 1. New public snapshots (flat structure) — .json and .txt
|
|
1389
|
+
const publicDir = path.resolve('assets/.storyboard-public/terminal-snapshots')
|
|
1390
|
+
if (fs.existsSync(publicDir)) {
|
|
1391
|
+
for (const file of fs.readdirSync(publicDir)) {
|
|
1392
|
+
if (file.startsWith('~') || file.startsWith('.')) continue
|
|
1393
|
+
const isJson = file.endsWith('.snapshot.json')
|
|
1394
|
+
const isTxt = file.endsWith('.snapshot.txt')
|
|
1395
|
+
if (!isJson && !isTxt) continue
|
|
1396
|
+
if (isJson) {
|
|
1397
|
+
const widgetId = file.replace(/\.snapshot\.json$/, '')
|
|
1398
|
+
if (widgetId) emittedIds.add(widgetId)
|
|
1399
|
+
}
|
|
1400
|
+
this.emitFile({
|
|
1401
|
+
type: 'asset',
|
|
1402
|
+
fileName: `_storyboard/terminal-snapshots/${file}`,
|
|
1403
|
+
source: fs.readFileSync(path.join(publicDir, file), 'utf-8'),
|
|
1404
|
+
})
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
// 2. Legacy snapshots (nested by canvas dir) — skip if already emitted
|
|
1409
|
+
const legacyDir = path.resolve('.storyboard/terminal-snapshots')
|
|
1410
|
+
if (fs.existsSync(legacyDir)) {
|
|
1411
|
+
const walk = (dir) => {
|
|
1412
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
1413
|
+
for (const entry of entries) {
|
|
1414
|
+
const full = path.join(dir, entry.name)
|
|
1415
|
+
if (entry.isDirectory()) {
|
|
1416
|
+
walk(full)
|
|
1417
|
+
} else if (entry.name.endsWith('.json') && !entry.name.startsWith('~')) {
|
|
1418
|
+
const widgetId = entry.name.replace(/\.json$/, '')
|
|
1419
|
+
if (emittedIds.has(widgetId)) continue
|
|
1420
|
+
const rel = path.relative(legacyDir, full).replace(/\\/g, '/')
|
|
1421
|
+
this.emitFile({
|
|
1422
|
+
type: 'asset',
|
|
1423
|
+
fileName: `_storyboard/terminal-snapshots/${rel}`,
|
|
1424
|
+
source: fs.readFileSync(full, 'utf-8'),
|
|
1425
|
+
})
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
walk(legacyDir)
|
|
1430
|
+
}
|
|
1431
|
+
},
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
/**
|
|
1436
|
+
* Vite plugin that copies terminal snapshots into the build output
|
|
1437
|
+
* so TerminalReadWidget can fetch them as static files in production.
|
|
1438
|
+
*
|
|
1439
|
+
* Sources (in priority order):
|
|
1440
|
+
* 1. assets/.storyboard-public/terminal-snapshots/<widgetId>.snapshot.json (new, flat)
|
|
1441
|
+
* 2. assets/.storyboard-public/terminal-snapshots/<widgetId>.snapshot.txt (human-readable companion)
|
|
1442
|
+
* 3. .storyboard/terminal-snapshots/<canvasDir>/<widgetId>.json (legacy, nested)
|
|
1443
|
+
*
|
|
1444
|
+
* All are emitted to `_storyboard/terminal-snapshots/` in the build.
|
|
1445
|
+
* Tilde-prefixed files (~) are excluded (private).
|
|
1446
|
+
*/
|
|
1447
|
+
export function terminalSnapshotPlugin() {
|
|
1448
|
+
return {
|
|
1449
|
+
name: 'storyboard-terminal-snapshots',
|
|
1450
|
+
|
|
1451
|
+
generateBundle() {
|
|
1452
|
+
const emittedIds = new Set()
|
|
1453
|
+
|
|
1454
|
+
// 1. New public snapshots (flat structure) — .json and .txt
|
|
1455
|
+
const publicDir = path.resolve('assets/.storyboard-public/terminal-snapshots')
|
|
1456
|
+
if (fs.existsSync(publicDir)) {
|
|
1457
|
+
for (const file of fs.readdirSync(publicDir)) {
|
|
1458
|
+
if (file.startsWith('~') || file.startsWith('.')) continue
|
|
1459
|
+
const isJson = file.endsWith('.snapshot.json')
|
|
1460
|
+
const isTxt = file.endsWith('.snapshot.txt')
|
|
1461
|
+
if (!isJson && !isTxt) continue
|
|
1462
|
+
if (isJson) {
|
|
1463
|
+
const widgetId = file.replace(/\.snapshot\.json$/, '')
|
|
1464
|
+
if (widgetId) emittedIds.add(widgetId)
|
|
1465
|
+
}
|
|
1466
|
+
this.emitFile({
|
|
1467
|
+
type: 'asset',
|
|
1468
|
+
fileName: `_storyboard/terminal-snapshots/${file}`,
|
|
1469
|
+
source: fs.readFileSync(path.join(publicDir, file), 'utf-8'),
|
|
1470
|
+
})
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
// 2. Legacy snapshots (nested by canvas dir) — skip if already emitted
|
|
1475
|
+
const legacyDir = path.resolve('.storyboard/terminal-snapshots')
|
|
1476
|
+
if (fs.existsSync(legacyDir)) {
|
|
1477
|
+
const walk = (dir) => {
|
|
1478
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
1479
|
+
for (const entry of entries) {
|
|
1480
|
+
const full = path.join(dir, entry.name)
|
|
1481
|
+
if (entry.isDirectory()) {
|
|
1482
|
+
walk(full)
|
|
1483
|
+
} else if (entry.name.endsWith('.json') && !entry.name.startsWith('~')) {
|
|
1484
|
+
const widgetId = entry.name.replace(/\.json$/, '')
|
|
1485
|
+
if (emittedIds.has(widgetId)) continue // new format takes priority
|
|
1486
|
+
const rel = path.relative(legacyDir, full).replace(/\\/g, '/')
|
|
1487
|
+
this.emitFile({
|
|
1488
|
+
type: 'asset',
|
|
1489
|
+
fileName: `_storyboard/terminal-snapshots/${rel}`,
|
|
1490
|
+
source: fs.readFileSync(full, 'utf-8'),
|
|
1491
|
+
})
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
walk(legacyDir)
|
|
1496
|
+
}
|
|
1497
|
+
},
|
|
1158
1498
|
}
|
|
1159
1499
|
}
|
|
1160
1500
|
|
|
@@ -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'
|