@dfosco/storyboard 0.6.0-beta.2 → 0.6.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.
Files changed (49) hide show
  1. package/dist/storyboard-ui.js +3112 -3098
  2. package/dist/storyboard-ui.js.map +1 -1
  3. package/mascot/frame-01-peek-left.txt +4 -0
  4. package/mascot/frame-02-eyes-open.txt +4 -0
  5. package/mascot/frame-03-peek-right.txt +4 -0
  6. package/mascot/frame-04-eyes-open.txt +4 -0
  7. package/mascot/frame-05-eyes-closed.txt +4 -0
  8. package/mascot/frame-06-eyes-open.txt +4 -0
  9. package/mascot.config.json +13 -0
  10. package/package.json +5 -2
  11. package/scaffold/AGENTS.md +1 -0
  12. package/scaffold/gitignore +12 -2
  13. package/scaffold/skills/design-system-catalog/SKILL.md +98 -0
  14. package/scaffold/skills/design-system-catalog/extract-components.mjs +441 -0
  15. package/scaffold/skills/design-system-catalog/generate-catalog.sh +255 -0
  16. package/scaffold/skills/migrate/SKILL.md +72 -50
  17. package/scaffold/terminal-agent.agent.md +8 -1
  18. package/src/core/canvas/agent-session.js +103 -17
  19. package/src/core/canvas/agent-session.test.js +29 -1
  20. package/src/core/canvas/collision.js +54 -45
  21. package/src/core/canvas/collision.test.js +39 -0
  22. package/src/core/canvas/configReader.js +110 -0
  23. package/src/core/canvas/hot-pool.js +5 -3
  24. package/src/core/canvas/server.js +32 -13
  25. package/src/core/canvas/terminal-server.js +156 -91
  26. package/src/core/cli/agent.js +86 -33
  27. package/src/core/cli/dev.js +303 -17
  28. package/src/core/cli/server.js +1 -1
  29. package/src/core/cli/setup.js +203 -60
  30. package/src/core/cli/terminal-welcome.js +5 -6
  31. package/src/core/cli/userState.js +63 -0
  32. package/src/core/stores/configSchema.js +1 -0
  33. package/src/core/stores/themeStore.ts +24 -0
  34. package/src/core/tools/handlers/devtools.test.js +1 -1
  35. package/src/core/vite/server-plugin.js +107 -10
  36. package/src/internals/CommandPalette/CommandPalette.jsx +1 -1
  37. package/src/internals/Viewfinder.jsx +10 -2
  38. package/src/internals/canvas/CanvasPage.jsx +30 -9
  39. package/src/internals/canvas/WebGLContextPool.jsx +6 -7
  40. package/src/internals/canvas/componentIsolate.jsx +7 -8
  41. package/src/internals/canvas/componentSetIsolate.jsx +7 -8
  42. package/src/internals/canvas/widgets/PrototypeEmbed.jsx +3 -1
  43. package/src/internals/canvas/widgets/StorySetWidget.jsx +19 -7
  44. package/src/internals/canvas/widgets/StoryWidget.jsx +9 -3
  45. package/src/internals/canvas/widgets/TerminalWidget.jsx +74 -13
  46. package/src/internals/canvas/widgets/expandUtils.js +4 -2
  47. package/src/internals/hooks/usePrototypeReloadGuard.js +9 -5
  48. package/src/internals/vite/data-plugin.js +126 -3
  49. package/terminal.config.json +66 -0
@@ -1,7 +1,12 @@
1
1
  /**
2
2
  * Prototype reload guard — suppresses Vite HMR full-reloads for non-canvas pages.
3
3
  *
4
- * Controlled by the "prototype-auto-reload" feature flag (default: true).
4
+ * Controlled by the "prototype-auto-reload" feature flag (default: false).
5
+ *
6
+ * The default is false (guard ON) because the dev server emits frequent
7
+ * full-reload broadcasts from data-plugin watchers; without the guard,
8
+ * prototype pages get stuck in a reload loop. Users can opt in via the
9
+ * devtools menu.
5
10
  * When the flag is false (user opted out), sends heartbeat messages to the
6
11
  * Vite dev server which suppresses full-reload and update payloads for this
7
12
  * client. Custom storyboard events (canvas file changes, story changes, etc.)
@@ -51,10 +56,9 @@ export default function usePrototypeReloadGuard() {
51
56
  // Initial sync
52
57
  sync()
53
58
 
54
- // Re-sync when the flag changes in localStorage (e.g. toggled from devtools)
55
- const unsub = subscribeToStorage((key) => {
56
- if (key === 'flag.' + FLAG_KEY) sync()
57
- })
59
+ // Re-sync when any storyboard storage entry changes (subscribeToStorage
60
+ // fires without a key arg, so we just re-read the flag every time).
61
+ const unsub = subscribeToStorage(() => sync())
58
62
 
59
63
  return () => {
60
64
  stop()
@@ -547,6 +547,90 @@ function readCoreConfigFile(root, filename) {
547
547
  return null
548
548
  }
549
549
 
550
+ /**
551
+ * Resolve the absolute path of a library config file (returns the first
552
+ * candidate that exists, or null). Used by syncScaffoldDir to copy raw
553
+ * file contents (including comments) rather than re-serializing parsed JSON.
554
+ */
555
+ function resolveCoreConfigFilePath(root, filename) {
556
+ const candidates = [
557
+ path.resolve(root, `packages/storyboard/${filename}`),
558
+ path.resolve(root, `node_modules/@dfosco/storyboard/${filename}`),
559
+ ]
560
+ for (const p of candidates) {
561
+ try { fs.accessSync(p); return p } catch { /* try next */ }
562
+ }
563
+ return null
564
+ }
565
+
566
+ const SCAFFOLD_README = `# .storyboard/scaffold/
567
+
568
+ This directory is **always rewritten on dev-server boot** to reflect the
569
+ library's current default config files. The Storyboard server **never reads
570
+ config from this directory** — these files are reference copies for you to
571
+ customize.
572
+
573
+ ## How to customize
574
+
575
+ 1. Pick the config file you want to override (e.g. \`terminal.config.json\`).
576
+ 2. **Copy it to your project root** (next to \`storyboard.config.json\`).
577
+ 3. Edit only the keys you care about — leaf-level merge means everything
578
+ else continues to inherit the library defaults, so future updates
579
+ (new agents, new readiness signals, etc.) reach you automatically.
580
+
581
+ ## Why a separate directory?
582
+
583
+ - Customers who don't want to customize don't see config clutter at the root.
584
+ - Customers who do want to customize have all the defaults available as a
585
+ living reference, version-bumped with every storyboard release.
586
+ - Files at the root override the defaults; missing files mean "use library
587
+ defaults". No empty placeholder files cluttering the project.
588
+
589
+ ## What's in here
590
+
591
+ | File | What it covers |
592
+ |------|----------------|
593
+ | \`terminal.config.json\` | Terminal widgets + canvas agent CLIs (copilot/claude/codex) |
594
+ | \`toolbar.config.json\` | Toolbar tool registry + visibility |
595
+ | \`commandpalette.config.json\` | Command palette entries |
596
+ | \`paste.config.json\` | URL → widget paste rules |
597
+ | \`widgets.config.json\` | Widget defaults (size, behavior) |
598
+
599
+ Don't edit files in this directory — your changes will be overwritten on
600
+ the next dev-server boot. Always copy to the project root first.
601
+ `
602
+
603
+ const SCAFFOLD_FILES = [
604
+ 'terminal.config.json',
605
+ 'toolbar.config.json',
606
+ 'commandpalette.config.json',
607
+ 'paste.config.json',
608
+ 'widgets.config.json',
609
+ ]
610
+
611
+ /**
612
+ * Sync `.storyboard/scaffold/` with the library's current default config
613
+ * files. Always overwrites — users must copy to the project root to
614
+ * customize. Idempotent and best-effort.
615
+ */
616
+ function syncScaffoldDir(root) {
617
+ const scaffoldDir = path.resolve(root, '.storyboard', 'scaffold')
618
+ try { fs.mkdirSync(scaffoldDir, { recursive: true }) } catch { /* empty */ }
619
+
620
+ const readmePath = path.resolve(scaffoldDir, 'README.md')
621
+ try { fs.writeFileSync(readmePath, SCAFFOLD_README) } catch { /* empty */ }
622
+
623
+ for (const filename of SCAFFOLD_FILES) {
624
+ const src = resolveCoreConfigFilePath(root, filename)
625
+ if (!src) continue
626
+ const dest = path.resolve(scaffoldDir, filename)
627
+ try {
628
+ const raw = fs.readFileSync(src, 'utf-8')
629
+ fs.writeFileSync(dest, raw)
630
+ } catch { /* skip on error */ }
631
+ }
632
+ }
633
+
550
634
  /**
551
635
  * Deep-merge helper (same as loader.js deepMerge but available at build time).
552
636
  * Arrays are replaced, not concatenated. Objects are recursively merged.
@@ -597,6 +681,7 @@ function buildUnifiedConfig(root) {
597
681
  const coreCommandPalette = readCoreConfigFile(root, 'commandpalette.config.json') || {}
598
682
  const corePaste = readCoreConfigFile(root, 'paste.config.json') || {}
599
683
  const coreWidgets = readCoreConfigFile(root, 'widgets.config.json') || {}
684
+ const coreTerminal = readCoreConfigFile(root, 'terminal.config.json') || {}
600
685
 
601
686
  // 2. Read storyboard.config.json (middle priority)
602
687
  // Use the schema-defaulted config for most things, but also read
@@ -619,6 +704,17 @@ function buildUnifiedConfig(root) {
619
704
  const afterSbWidgets = rawSbConfig.widgets
620
705
  ? deepMergeBuild(coreWidgets, sbConfig.widgets || {})
621
706
  : coreWidgets
707
+ // For terminal/agents, slot canvas.terminal + canvas.agents from storyboard.config.json
708
+ // into the same shape as terminal.config.json so the merge is uniform.
709
+ const sbTerminalLike = (rawSbConfig.canvas && (rawSbConfig.canvas.terminal || rawSbConfig.canvas.agents))
710
+ ? {
711
+ ...(rawSbConfig.canvas.terminal ? { terminal: sbConfig.canvas.terminal } : {}),
712
+ ...(rawSbConfig.canvas.agents ? { agents: sbConfig.canvas.agents } : {}),
713
+ }
714
+ : null
715
+ const afterSbTerminal = sbTerminalLike
716
+ ? deepMergeBuild(coreTerminal, sbTerminalLike)
717
+ : coreTerminal
622
718
 
623
719
  // 4. Read user domain config files (highest priority)
624
720
  const userFiles = [
@@ -626,6 +722,7 @@ function buildUnifiedConfig(root) {
626
722
  { domain: 'paste', filename: 'paste.config.json' },
627
723
  { domain: 'toolbar', filename: 'toolbar.config.json' },
628
724
  { domain: 'commandPalette', filename: 'commandpalette.config.json' },
725
+ { domain: 'terminal', filename: 'terminal.config.json' },
629
726
  ]
630
727
 
631
728
  const userConfigs = {}
@@ -648,6 +745,9 @@ function buildUnifiedConfig(root) {
648
745
  const finalWidgets = userConfigs.widgets
649
746
  ? deepMergeBuild(afterSbWidgets, userConfigs.widgets.data)
650
747
  : afterSbWidgets
748
+ const finalTerminal = userConfigs.terminal
749
+ ? deepMergeBuild(afterSbTerminal, userConfigs.terminal.data)
750
+ : afterSbTerminal
651
751
 
652
752
  // 6. Detect overlaps between storyboard.config.json and user domain configs
653
753
  const domainOverlapChecks = [
@@ -664,18 +764,32 @@ function buildUnifiedConfig(root) {
664
764
  }
665
765
  }
666
766
  }
767
+ // Terminal overlap check: storyboard.config.json.canvas.{terminal,agents} vs terminal.config.json
768
+ if (sbTerminalLike && userConfigs.terminal) {
769
+ const overlaps = findOverlappingKeys(sbTerminalLike, userConfigs.terminal.data)
770
+ for (const key of overlaps) {
771
+ warnings.push(`Config overlap: "${key}" is defined in both storyboard.config.json.canvas and terminal.config.json — terminal.config.json wins.`)
772
+ }
773
+ }
667
774
 
668
775
  // 7. Build the unified config object.
669
776
  // Start from the schema-defaulted sbConfig so every top-level key from
670
777
  // storyboard.config.json (and every schema default) flows to initConfig().
671
778
  // Then override the domain-specific slices that have their own dedicated
672
- // config files merged above (toolbar/commandPalette/paste/widgets).
779
+ // config files merged above (toolbar/commandPalette/paste/widgets/terminal).
780
+ const sbCanvas = sbConfig?.canvas || {}
673
781
  const unified = {
674
- ...sbConfig,
782
+ ...(sbConfig || {}),
675
783
  toolbar: finalToolbar,
676
784
  commandPalette: finalCommandPalette,
677
785
  paste: finalPaste,
678
786
  widgets: finalWidgets,
787
+ canvas: {
788
+ ...sbCanvas,
789
+ terminal: deepMergeBuild(sbCanvas.terminal || {}, finalTerminal.terminal || {}),
790
+ agents: deepMergeBuild(sbCanvas.agents || {}, finalTerminal.agents || {}),
791
+ ...(finalTerminal.showAgentsInAddMenu !== undefined ? { showAgentsInAddMenu: finalTerminal.showAgentsInAddMenu } : {}),
792
+ },
679
793
  }
680
794
 
681
795
  return { unified, warnings }
@@ -1063,6 +1177,14 @@ export default function storyboardDataPlugin() {
1063
1177
  // dev so users can hit their routes, excluded from production builds
1064
1178
  // so private experiments don't ship.
1065
1179
  includeTilde = config.command === 'serve'
1180
+
1181
+ // On dev boot, sync .storyboard/scaffold/ with the library's current
1182
+ // default config files so users always have an up-to-date copy-source
1183
+ // for customizations. Files in .storyboard/scaffold/ are NEVER read by
1184
+ // the server — only files at the project root are. Always overwrites.
1185
+ if (config.command === 'serve') {
1186
+ try { syncScaffoldDir(root) } catch { /* best-effort */ }
1187
+ }
1066
1188
  },
1067
1189
 
1068
1190
  resolveId(id) {
@@ -1209,7 +1331,7 @@ export default function storyboardDataPlugin() {
1209
1331
  }
1210
1332
 
1211
1333
  // Invalidate when any config file inside a prototype changes
1212
- const protoConfigPattern = /\/(toolbar|commandpalette|widgets|paste)\.config\.json$/
1334
+ const protoConfigPattern = /\/(toolbar|commandpalette|widgets|paste|terminal)\.config\.json$/
1213
1335
  if (protoConfigPattern.test(normalized) && normalized.includes('/prototypes/')) {
1214
1336
  buildResult = null
1215
1337
  const mod = server.moduleGraph.getModuleById(RESOLVED_ID)
@@ -1368,6 +1490,7 @@ export default function storyboardDataPlugin() {
1368
1490
  'commandpalette.config.json',
1369
1491
  'paste.config.json',
1370
1492
  'widgets.config.json',
1493
+ 'terminal.config.json',
1371
1494
  ].map(f => path.resolve(root, f))
1372
1495
  const watchedConfigPaths = new Set([configPath, ...domainConfigFiles])
1373
1496
  for (const p of domainConfigFiles) watcher.add(p)
@@ -0,0 +1,66 @@
1
+ {
2
+ "$schema": "./terminal.schema.json",
3
+ "$comment": "Defaults for terminal widgets and canvas agents. Users can override at the project root by copying this file from .storyboard/scaffold/terminal.config.json. Leaf-level merge: only the specific keys you set are overridden, everything else inherits the library defaults — so future agent additions or readinessSignal tweaks reach you automatically.",
4
+ "terminal": {
5
+ "resizable": true,
6
+ "defaultWidth": 1000,
7
+ "defaultHeight": 800,
8
+ "fontSize": 14,
9
+ "fontFamily": "'SF Mono', 'Menlo', 'Monaco', 'Courier New', monospace",
10
+ "prompt": "❯ ",
11
+ "startupCommand": null,
12
+ "defaultStartupSequence": null
13
+ },
14
+ "agents": {
15
+ "copilot": {
16
+ "label": "Copilot CLI",
17
+ "default": true,
18
+ "icon": "primer/copilot",
19
+ "startupCommand": "copilot --agent terminal-agent",
20
+ "resumeCommand": "copilot --resume={id} --agent terminal-agent",
21
+ "resumeLastCommand": "copilot --continue --agent terminal-agent",
22
+ "sessionIdEnv": "COPILOT_AGENT_SESSION_ID",
23
+ "postStartup": "/allow-all on",
24
+ "resizable": true
25
+ },
26
+ "claude": {
27
+ "label": "Claude Code",
28
+ "icon": "claude",
29
+ "startupCommand": "claude --agent terminal-agent --dangerously-skip-permissions",
30
+ "resumeCommand": "claude --resume {id} --agent terminal-agent --dangerously-skip-permissions",
31
+ "resumeLastCommand": "claude --continue --agent terminal-agent --dangerously-skip-permissions",
32
+ "sessionIdEnv": "CLAUDE_SESSION_ID",
33
+ "sessionStateGlob": "~/.claude/projects/*/{id}.jsonl",
34
+ "readinessSignal": "bypass permissions",
35
+ "resizable": true
36
+ },
37
+ "codex": {
38
+ "label": "Codex CLI",
39
+ "icon": "codex",
40
+ "startupCommand": "codex --dangerously-bypass-approvals-and-sandbox",
41
+ "resumeCommand": "codex resume {id} --dangerously-bypass-approvals-and-sandbox",
42
+ "resumeLastCommand": "codex resume --last --dangerously-bypass-approvals-and-sandbox",
43
+ "sessionIdEnv": "CODEX_SESSION_ID",
44
+ "sessionStateGlob": "~/.codex/sessions/**/rollout-*-{id}.jsonl",
45
+ "configFiles": [".codex/config.toml"],
46
+ "readinessSignal": "YOLO mode",
47
+ "resizable": true
48
+ }
49
+ },
50
+ "showAgentsInAddMenu": false,
51
+ "hotPool": {
52
+ "enabled": true,
53
+ "verbose": false,
54
+ "default_pool_size": 1,
55
+ "default_max_pool_size": 3,
56
+ "load_balancer": true,
57
+ "load_balancer_cooldown_mins": 10,
58
+ "pools": {
59
+ "terminal": { "pool_size": 1 },
60
+ "copilot": { "pool_size": 1 },
61
+ "claude": { "pool_size": 1 },
62
+ "codex": { "pool_size": 0 },
63
+ "prompt": { "pool_size": 1 }
64
+ }
65
+ }
66
+ }