@dfosco/storyboard 0.8.1 → 0.9.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.
Files changed (114) hide show
  1. package/dist/storyboard-ui.css +1 -1
  2. package/dist/storyboard-ui.js +6420 -6336
  3. package/dist/storyboard-ui.js.map +1 -1
  4. package/dist/tailwind.css +1 -1
  5. package/dist/tiny-canvas.css +1 -1
  6. package/package.json +1 -1
  7. package/scaffold/src/library/_app.jsx +2 -14
  8. package/scaffold/src/library/_app.module.css +2 -8
  9. package/scaffold/src/library/mount.jsx +0 -1
  10. package/src/canvas/style.css +16 -16
  11. package/src/core/artifact/operations.js +35 -2
  12. package/src/core/artifact/schemas/canvas.schema.json +7 -1
  13. package/src/core/canvas/hot-pool.js +11 -22
  14. package/src/core/canvas/selectedWidgets.js +1 -1
  15. package/src/core/canvas/server.js +52 -31
  16. package/src/core/canvas/server.test.js +107 -0
  17. package/src/core/cli/artifact.js +2 -0
  18. package/src/core/cli/create.js +35 -5
  19. package/src/core/cli/schemas.js +6 -1
  20. package/src/core/comments/ui/comment-layout.css +14 -14
  21. package/src/core/index.js +18 -0
  22. package/src/core/inspector/highlighter.js +1 -1
  23. package/src/core/inspector/mouseMode.js +2 -2
  24. package/src/core/lib/components/ui/dialog/dialog.module.css +8 -8
  25. package/src/core/modes/modes.css +4 -4
  26. package/src/core/mountStoryboardCore.js +53 -9
  27. package/src/core/mountStoryboardCore.test.js +25 -1
  28. package/src/core/rename-watcher/config.json +1 -1
  29. package/src/core/rename-watcher/watcher.js +1 -1
  30. package/src/core/stores/canvasInteractionStore.js +120 -0
  31. package/src/core/stores/canvasInteractionStore.test.js +126 -0
  32. package/src/core/stores/configSchema.js +23 -6
  33. package/src/core/stores/configSchema.test.js +3 -0
  34. package/src/core/stores/customerModeConfig.js +24 -3
  35. package/src/core/stores/customerModeConfig.test.js +46 -0
  36. package/src/core/stores/presentationStore.js +118 -0
  37. package/src/core/stores/presentationStore.test.js +114 -0
  38. package/src/core/stores/uiConfig.js +10 -2
  39. package/src/core/styles/tailwind.css +138 -90
  40. package/src/core/styles/workspace-chrome.css +4 -21
  41. package/src/core/ui/AutosyncMenuButton.css +7 -7
  42. package/src/core/ui/BranchSelect.module.css +5 -5
  43. package/src/core/ui/ChromeSlot.jsx +92 -0
  44. package/src/core/ui/ChromeSlot.test.jsx +140 -0
  45. package/src/core/ui/CoreUIBar.css +5 -5
  46. package/src/core/ui/CoreUIBar.jsx +161 -136
  47. package/src/core/ui/InspectorPanel.css +5 -5
  48. package/src/core/ui/InspectorPanel.jsx +9 -9
  49. package/src/core/ui/PwaInstallBanner.css +5 -5
  50. package/src/core/ui/PwaInstallBanner.jsx +5 -5
  51. package/src/core/ui/core-ui-colors.css +28 -66
  52. package/src/core/ui/sidepanel.css +12 -12
  53. package/src/core/workshop/features/createCanvas/CreateCanvasForm.jsx +102 -13
  54. package/src/internals/ArtifactForm/ArtifactForm.module.css +7 -7
  55. package/src/internals/AuthModal/AuthModal.module.css +34 -34
  56. package/src/internals/BaseUiForm/BaseUiForm.module.css +85 -85
  57. package/src/internals/BranchBar/BranchBar.module.css +4 -4
  58. package/src/internals/BranchesDropdown/BranchesDropdown.module.css +9 -9
  59. package/src/internals/CommandPalette/CommandPalette.jsx +16 -10
  60. package/src/internals/CommandPalette/WidgetArtifactDialog.css +16 -16
  61. package/src/internals/CommandPalette/command-palette.css +14 -14
  62. package/src/internals/CreatePage/CreatePage.module.css +1 -1
  63. package/src/internals/FlowError.module.css +5 -5
  64. package/src/internals/PrototypeErrorBoundary.module.css +15 -15
  65. package/src/internals/SimpleWorkspace/SimpleWorkspace.jsx +5 -4
  66. package/src/internals/SimpleWorkspace/SimpleWorkspace.module.css +38 -38
  67. package/src/internals/Viewfinder.jsx +33 -16
  68. package/src/internals/Viewfinder.module.css +189 -189
  69. package/src/internals/canvas/CanvasControls.module.css +11 -11
  70. package/src/internals/canvas/CanvasPage.bridge.test.jsx +11 -37
  71. package/src/internals/canvas/CanvasPage.jsx +91 -93
  72. package/src/internals/canvas/CanvasPage.module.css +20 -19
  73. package/src/internals/canvas/CanvasToolbar.module.css +11 -11
  74. package/src/internals/canvas/ConnectorLayer.jsx +39 -2
  75. package/src/internals/canvas/ConnectorLayer.module.css +10 -10
  76. package/src/internals/canvas/PageSelector.module.css +28 -28
  77. package/src/internals/canvas/canvasTheme.js +22 -111
  78. package/src/internals/canvas/isolation/prototypeRoutes.jsx +12 -3
  79. package/src/internals/canvas/isolation/storyRoutes.jsx +1 -1
  80. package/src/internals/canvas/widgets/CodePenEmbed.jsx +16 -36
  81. package/src/internals/canvas/widgets/CodePenEmbed.module.css +12 -12
  82. package/src/internals/canvas/widgets/CropOverlay.module.css +23 -23
  83. package/src/internals/canvas/widgets/ExpandedPane.module.css +8 -8
  84. package/src/internals/canvas/widgets/FigmaEmbed.jsx +15 -36
  85. package/src/internals/canvas/widgets/FigmaEmbed.module.css +15 -15
  86. package/src/internals/canvas/widgets/FrozenTerminalOverlay.module.css +5 -5
  87. package/src/internals/canvas/widgets/ImageWidget.module.css +3 -3
  88. package/src/internals/canvas/widgets/LinkPreview.module.css +77 -78
  89. package/src/internals/canvas/widgets/MarkdownBlock.module.css +31 -31
  90. package/src/internals/canvas/widgets/PromptWidget.jsx +2 -16
  91. package/src/internals/canvas/widgets/PromptWidget.module.css +33 -33
  92. package/src/internals/canvas/widgets/PrototypeEmbed.jsx +23 -7
  93. package/src/internals/canvas/widgets/PrototypeEmbed.module.css +58 -56
  94. package/src/internals/canvas/widgets/PrototypeEmbed.test.jsx +4 -4
  95. package/src/internals/canvas/widgets/ResizeHandle.module.css +4 -4
  96. package/src/internals/canvas/widgets/StorySetWidget.jsx +45 -21
  97. package/src/internals/canvas/widgets/StoryWidget.jsx +53 -21
  98. package/src/internals/canvas/widgets/StoryWidget.module.css +22 -20
  99. package/src/internals/canvas/widgets/TerminalReadWidget.module.css +4 -4
  100. package/src/internals/canvas/widgets/TerminalWidget.jsx +56 -45
  101. package/src/internals/canvas/widgets/TerminalWidget.module.css +10 -10
  102. package/src/internals/canvas/widgets/TilesWidget.module.css +17 -17
  103. package/src/internals/canvas/widgets/WidgetChrome.module.css +61 -61
  104. package/src/internals/canvas/widgets/WidgetWrapper.module.css +2 -2
  105. package/src/internals/canvas/widgets/embedOverlay.module.css +14 -4
  106. package/src/internals/canvas/widgets/embedTheme.js +10 -102
  107. package/src/internals/canvas/widgets/widgetConfig.js +5 -5
  108. package/src/internals/story/ComponentSetPage.module.css +14 -14
  109. package/src/internals/story/StoryPage.module.css +2 -2
  110. package/src/internals/vite/data-plugin.js +5 -2
  111. package/src/internals/canvas/WebGLContextPool.jsx +0 -291
  112. package/src/internals/canvas/WebGLContextPool.test.jsx +0 -165
  113. package/src/internals/canvas/widgets/ComponentSetWidget.module.css +0 -89
  114. package/src/internals/canvas/widgets/StorySetWidget.module.css +0 -89
@@ -141,7 +141,7 @@ function writeFolderMeta(folderDir, meta) {
141
141
  */
142
142
  function findCanvasFiles(root) {
143
143
  const results = []
144
- const ignore = new Set(['node_modules', 'dist', '.git', '.worktrees'])
144
+ const ignore = new Set(['node_modules', 'dist', '.git', '.worktrees', 'worktrees', 'workleafs'])
145
145
 
146
146
  function walk(dir, rel) {
147
147
  let entries
@@ -168,7 +168,7 @@ function findCanvasFiles(root) {
168
168
  */
169
169
  function findStoryFiles(root) {
170
170
  const results = []
171
- const ignore = new Set(['node_modules', 'dist', '.git', '.worktrees'])
171
+ const ignore = new Set(['node_modules', 'dist', '.git', '.worktrees', 'worktrees', 'workleafs'])
172
172
  const ROUTABLE_DIRS = ['src/canvas', 'src/components']
173
173
 
174
174
  function walk(dir, rel) {
@@ -846,18 +846,18 @@ export function createCanvasHandler(ctx) {
846
846
  }
847
847
 
848
848
  /**
849
- * Non-mutating probe of a hot pool — returns webgl-readiness without
850
- * claiming a slot. Use this when the canvas API just needs to tell the
851
- * client whether to render in webgl-ready mode; the actual session claim
852
- * happens later in terminal-server when the WebSocket connects.
849
+ * Non-mutating probe of a hot pool — returns readiness without claiming
850
+ * a slot. Use this when the canvas API just needs to tell the client
851
+ * whether a hot session is available; the actual session claim happens
852
+ * later in terminal-server when the WebSocket connects.
853
853
  * @param {Object|null} hotPool — HotPoolManager instance
854
854
  * @param {string} poolId — pool to peek at
855
855
  * @param {string} [mode] — 'auto' (default), 'hot', or 'cold'
856
- * @returns {{ ready: boolean, webglReady: boolean }}
856
+ * @returns {{ ready: boolean }}
857
857
  */
858
858
  function peekPool(hotPool, poolId, mode) {
859
- if (!hotPool || mode === 'cold') return { ready: false, webglReady: false }
860
- if (!hotPool.has(poolId)) return { ready: false, webglReady: false }
859
+ if (!hotPool || mode === 'cold') return { ready: false }
860
+ if (!hotPool.has(poolId)) return { ready: false }
861
861
  return hotPool.peek(poolId)
862
862
  }
863
863
 
@@ -942,15 +942,20 @@ export function createCanvasHandler(ctx) {
942
942
  if (routePath === '/folders' && method === 'GET') {
943
943
  const canvasDir = path.join(root, 'src', 'canvas')
944
944
  let folders = []
945
+ let entries = []
945
946
  try {
946
947
  if (fs.existsSync(canvasDir)) {
947
- const entries = fs.readdirSync(canvasDir, { withFileTypes: true })
948
- // .folder directories (existing behavior)
949
- const folderDirs = entries
948
+ const dirEntries = fs.readdirSync(canvasDir, { withFileTypes: true })
949
+ // .folder directories — workspace grouping (sibling canvases shown
950
+ // under a folder header in the workspace, each addressable on its
951
+ // own /canvas/<folder>/<name> route).
952
+ const folderDirs = dirEntries
950
953
  .filter((d) => d.isDirectory() && d.name.endsWith('.folder'))
951
954
  .map((d) => d.name.replace('.folder', ''))
952
- // Plain directories containing .canvas.jsonl files
953
- const plainDirs = entries
955
+ // Plain directories containing .canvas.jsonl files — multi-page
956
+ // canvas grouping (each .canvas.jsonl is a "page" of the parent
957
+ // group, surfaced via PageSelector inside the canvas).
958
+ const plainDirs = dirEntries
954
959
  .filter((d) => {
955
960
  if (!d.isDirectory() || d.name.endsWith('.folder') || d.name.startsWith('_')) return false
956
961
  const files = fs.readdirSync(path.join(canvasDir, d.name))
@@ -958,9 +963,16 @@ export function createCanvasHandler(ctx) {
958
963
  })
959
964
  .map((d) => d.name)
960
965
  folders = [...folderDirs, ...plainDirs]
966
+ entries = [
967
+ ...folderDirs.map((name) => ({ name, kind: 'workspace' })),
968
+ ...plainDirs.map((name) => ({ name, kind: 'pages' })),
969
+ ]
961
970
  }
962
971
  } catch { /* empty */ }
963
- sendJson(res, 200, { folders })
972
+ // `folders` is the legacy flat name list; `entries` is the new tagged
973
+ // list. Clients can prefer `entries` when they need to know whether a
974
+ // folder is a `.folder/` (workspace) or plain (multi-page) dir.
975
+ sendJson(res, 200, { folders, entries })
964
976
  return
965
977
  }
966
978
 
@@ -1273,11 +1285,11 @@ export function createCanvasHandler(ctx) {
1273
1285
 
1274
1286
  // Hot pool readiness probe for terminal/agent widgets — non-mutating.
1275
1287
  // The actual session claim happens later in terminal-server when the
1276
- // WS connects. Probing here only tells the client whether to render
1277
- // in webgl-ready mode immediately. (Previously this called
1278
- // acquireFromPool, which leaked a #acquired slot per widget creation
1279
- // because no consume/release ever fired against this acquisition.)
1280
- let hotProbe = { ready: false, webglReady: false }
1288
+ // WS connects. Probing here only tells the client whether a hot
1289
+ // session is available. (Previously this called acquireFromPool,
1290
+ // which leaked a #acquired slot per widget creation because no
1291
+ // consume/release ever fired against this acquisition.)
1292
+ let hotProbe = { ready: false }
1281
1293
  if ((type === 'terminal' || type === 'agent') && pool !== 'cold') {
1282
1294
  const poolId = resolvePoolId(type, props)
1283
1295
  hotProbe = peekPool(hotPool, poolId, pool)
@@ -1296,7 +1308,7 @@ export function createCanvasHandler(ctx) {
1296
1308
  })
1297
1309
 
1298
1310
  const response = { success: true, widget, eventId }
1299
- if (hotProbe.ready) response.hotSession = { id: null, tmuxName: null, webglReady: hotProbe.webglReady }
1311
+ if (hotProbe.ready) response.hotSession = { id: null, tmuxName: null }
1300
1312
  sendJson(res, 201, response)
1301
1313
  pushCanvasUpdate(name, filePath, __viteWs)
1302
1314
  } catch (err) {
@@ -2064,7 +2076,7 @@ export function createCanvasHandler(ctx) {
2064
2076
  const widgetId = generateWidgetId(type)
2065
2077
  await prepareTerminalWidget({ type, props, widgetId, canvasName: name, req })
2066
2078
 
2067
- let hotProbe = { ready: false, webglReady: false }
2079
+ let hotProbe = { ready: false }
2068
2080
  if ((type === 'terminal' || type === 'agent') && pool !== 'cold') {
2069
2081
  const poolId = resolvePoolId(type, props)
2070
2082
  hotProbe = peekPool(hotPool, poolId, pool)
@@ -2081,7 +2093,7 @@ export function createCanvasHandler(ctx) {
2081
2093
  if (ref) refs[ref] = widgetId
2082
2094
 
2083
2095
  const result = { index: i, op: 'create-widget', ref: ref || undefined, widgetId, widget, eventId }
2084
- if (hotProbe.ready) result.hotSession = { id: null, tmuxName: null, webglReady: hotProbe.webglReady }
2096
+ if (hotProbe.ready) result.hotSession = { id: null, tmuxName: null }
2085
2097
  results.push(result)
2086
2098
  break
2087
2099
  }
@@ -2596,6 +2608,7 @@ export function createCanvasHandler(ctx) {
2596
2608
  name,
2597
2609
  title,
2598
2610
  folder,
2611
+ folderKind,
2599
2612
  convertFrom,
2600
2613
  author,
2601
2614
  description,
@@ -2730,7 +2743,12 @@ export function createCanvasHandler(ctx) {
2730
2743
  }
2731
2744
 
2732
2745
  // ── Standard canvas creation ─────────────────────────────────────
2733
- // Determine target directory
2746
+ // Determine target directory. When `folder` is set:
2747
+ // - existing `<folder>.folder/` wins → workspace grouping
2748
+ // - existing `<folder>/` wins → multi-page canvas grouping
2749
+ // - otherwise: create a new dir; `folderKind` controls which kind.
2750
+ // Default kind is "pages" for backward compatibility (the legacy
2751
+ // behavior). Pass `"workspace"` to create `<folder>.folder/`.
2734
2752
  const canvasDir = path.join(root, 'src', 'canvas')
2735
2753
  let targetDir = canvasDir
2736
2754
 
@@ -2739,25 +2757,28 @@ export function createCanvasHandler(ctx) {
2739
2757
  const plainDir = path.join(canvasDir, folder)
2740
2758
 
2741
2759
  if (fs.existsSync(dotFolderDir)) {
2742
- // Existing .folder/ directory
2760
+ // Existing .folder/ directory (workspace grouping)
2743
2761
  targetDir = dotFolderDir
2744
2762
  } else if (fs.existsSync(plainDir) && fs.statSync(plainDir).isDirectory()) {
2745
- // Existing plain directory
2763
+ // Existing plain directory (multi-page grouping)
2746
2764
  targetDir = plainDir
2747
2765
  } else {
2748
- // Create new plain directory
2766
+ // Create a new directory according to the requested kind.
2767
+ const useWorkspace = folderKind === 'workspace'
2768
+ const newDir = useWorkspace ? dotFolderDir : plainDir
2749
2769
  try {
2750
- fs.mkdirSync(plainDir, { recursive: true })
2751
- // Write .meta.json if meta was provided
2770
+ fs.mkdirSync(newDir, { recursive: true })
2771
+ // Write .meta.json if meta was provided (only meaningful for
2772
+ // multi-page groups today; harmless inside a .folder/).
2752
2773
  if (meta && typeof meta === 'object') {
2753
- const metaPath = path.join(plainDir, `${folder}.meta.json`)
2774
+ const metaPath = path.join(newDir, `${folder}.meta.json`)
2754
2775
  fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2) + '\n', 'utf-8')
2755
2776
  }
2756
2777
  } catch (err) {
2757
2778
  sendJson(res, 500, { error: `Failed to create directory: ${err.message}` })
2758
2779
  return
2759
2780
  }
2760
- targetDir = plainDir
2781
+ targetDir = newDir
2761
2782
  }
2762
2783
  }
2763
2784
 
@@ -740,3 +740,110 @@ describe('image routes — drafts/ subdirectory privacy layer', () => {
740
740
  expect(lastResponse.body.filename).toMatch(/^drafts\/storyboarding--/)
741
741
  })
742
742
  })
743
+
744
+ describe('GET /folders and POST /create with folderKind', () => {
745
+ let root, canvasDir, invoke, lastResponse
746
+
747
+ beforeEach(() => {
748
+ ({ invoke, root, canvasDir, lastResponse } = setup())
749
+ })
750
+
751
+ afterEach(() => {
752
+ fs.rmSync(root, { recursive: true, force: true })
753
+ })
754
+
755
+ it('GET /folders returns tagged entries distinguishing workspace from pages', async () => {
756
+ // Workspace folder (`.folder/` suffix) containing one canvas
757
+ fs.mkdirSync(path.join(canvasDir, 'marketing.folder'), { recursive: true })
758
+ writeCanvas(path.join(canvasDir, 'marketing.folder'), 'landing')
759
+ // Plain dir (multi-page canvas group) containing two canvases
760
+ fs.mkdirSync(path.join(canvasDir, 'tour'), { recursive: true })
761
+ writeCanvas(path.join(canvasDir, 'tour'), 'overview')
762
+ writeCanvas(path.join(canvasDir, 'tour'), 'detail')
763
+
764
+ await invoke('/folders', 'GET')
765
+
766
+ expect(lastResponse.status).toBe(200)
767
+ // Legacy flat list still present for back-compat
768
+ expect(lastResponse.body.folders).toContain('marketing')
769
+ expect(lastResponse.body.folders).toContain('tour')
770
+ // New tagged entries carry the kind
771
+ const entries = lastResponse.body.entries
772
+ expect(entries).toEqual(expect.arrayContaining([
773
+ { name: 'marketing', kind: 'workspace' },
774
+ { name: 'tour', kind: 'pages' },
775
+ ]))
776
+ })
777
+
778
+ it('POST /create with folderKind:"workspace" creates a new .folder/ dir', async () => {
779
+ await invoke('/create', 'POST', {
780
+ name: 'landing',
781
+ folder: 'marketing',
782
+ folderKind: 'workspace',
783
+ title: 'Landing',
784
+ })
785
+
786
+ expect(lastResponse.status).toBe(201)
787
+ expect(fs.existsSync(path.join(canvasDir, 'marketing.folder', 'landing.canvas.jsonl'))).toBe(true)
788
+ // Plain dir must NOT have been created
789
+ expect(fs.existsSync(path.join(canvasDir, 'marketing'))).toBe(false)
790
+ })
791
+
792
+ it('POST /create with folderKind:"pages" (or omitted) creates a plain dir', async () => {
793
+ await invoke('/create', 'POST', {
794
+ name: 'overview',
795
+ folder: 'tour',
796
+ folderKind: 'pages',
797
+ title: 'Overview',
798
+ })
799
+
800
+ expect(lastResponse.status).toBe(201)
801
+ expect(fs.existsSync(path.join(canvasDir, 'tour', 'overview.canvas.jsonl'))).toBe(true)
802
+ // .folder/ variant must NOT have been created
803
+ expect(fs.existsSync(path.join(canvasDir, 'tour.folder'))).toBe(false)
804
+ })
805
+
806
+ it('POST /create defaults to plain dir when folderKind is omitted (backward compat)', async () => {
807
+ await invoke('/create', 'POST', {
808
+ name: 'overview',
809
+ folder: 'tour',
810
+ title: 'Overview',
811
+ })
812
+
813
+ expect(lastResponse.status).toBe(201)
814
+ expect(fs.existsSync(path.join(canvasDir, 'tour', 'overview.canvas.jsonl'))).toBe(true)
815
+ expect(fs.existsSync(path.join(canvasDir, 'tour.folder'))).toBe(false)
816
+ })
817
+
818
+ it('POST /create reuses existing .folder/ regardless of folderKind', async () => {
819
+ fs.mkdirSync(path.join(canvasDir, 'marketing.folder'), { recursive: true })
820
+
821
+ // Even with folderKind:"pages", the existing .folder/ wins (filesystem
822
+ // truth beats a hint).
823
+ await invoke('/create', 'POST', {
824
+ name: 'landing',
825
+ folder: 'marketing',
826
+ folderKind: 'pages',
827
+ title: 'Landing',
828
+ })
829
+
830
+ expect(lastResponse.status).toBe(201)
831
+ expect(fs.existsSync(path.join(canvasDir, 'marketing.folder', 'landing.canvas.jsonl'))).toBe(true)
832
+ expect(fs.existsSync(path.join(canvasDir, 'marketing'))).toBe(false)
833
+ })
834
+
835
+ it('POST /create reuses existing plain dir regardless of folderKind', async () => {
836
+ fs.mkdirSync(path.join(canvasDir, 'tour'), { recursive: true })
837
+
838
+ await invoke('/create', 'POST', {
839
+ name: 'overview',
840
+ folder: 'tour',
841
+ folderKind: 'workspace',
842
+ title: 'Overview',
843
+ })
844
+
845
+ expect(lastResponse.status).toBe(201)
846
+ expect(fs.existsSync(path.join(canvasDir, 'tour', 'overview.canvas.jsonl'))).toBe(true)
847
+ expect(fs.existsSync(path.join(canvasDir, 'tour.folder'))).toBe(false)
848
+ })
849
+ })
@@ -296,6 +296,8 @@ function buildValues(flags) {
296
296
  d: 'description',
297
297
  a: 'author',
298
298
  p: 'prototype',
299
+ // kebab-case CLI flag → camelCase schema field
300
+ 'folder-kind': 'folderKind',
299
301
  }
300
302
 
301
303
  for (const [key, val] of Object.entries(flags)) {
@@ -362,11 +362,16 @@ async function createCanvas() {
362
362
  p.intro('storyboard create canvas')
363
363
  await ensureDevServer()
364
364
 
365
- // Fetch available folders
366
- let folders = []
365
+ // Fetch available folders (tagged when the server supports it)
366
+ let folderEntries = []
367
367
  try {
368
368
  const data = await serverGet('/_storyboard/canvas/folders')
369
- folders = data.folders || data || []
369
+ if (Array.isArray(data.entries)) {
370
+ folderEntries = data.entries
371
+ } else {
372
+ const legacy = data.folders || data || []
373
+ folderEntries = legacy.map((name) => ({ name, kind: 'pages' }))
374
+ }
370
375
  } catch {
371
376
  // Continue without folders
372
377
  }
@@ -388,14 +393,38 @@ async function createCanvas() {
388
393
  })))
389
394
 
390
395
  let folder = flags.folder || ''
391
- if (!folder && !flagMode && Array.isArray(folders) && folders.length > 0) {
396
+ let folderKind = flags['folder-kind'] || undefined
397
+ if (!folder && !flagMode && Array.isArray(folderEntries) && folderEntries.length > 0) {
398
+ const NEW = '__new__'
392
399
  folder = await promptOrCancel(p.select({
393
400
  message: 'Folder',
394
401
  options: [
395
402
  { value: '', label: 'None (root)' },
396
- ...folders.map((f) => ({ value: f, label: f })),
403
+ ...folderEntries.map((f) => ({
404
+ value: f.name,
405
+ label: `${f.name}${f.kind === 'workspace' ? ' — workspace folder' : ' — canvas pages'}`,
406
+ })),
407
+ { value: NEW, label: '+ Create new folder…' },
397
408
  ],
398
409
  }))
410
+ if (folder === NEW) {
411
+ folder = await promptOrCancel(p.text({
412
+ message: 'New folder name (kebab-case)',
413
+ placeholder: 'my-folder',
414
+ validate: (v) => {
415
+ if (!v) return 'Folder name is required'
416
+ if (/[A-Z\s]/.test(v)) return 'Use kebab-case'
417
+ },
418
+ }))
419
+ folderKind = await promptOrCancel(p.select({
420
+ message: 'Folder type',
421
+ options: [
422
+ { value: 'workspace', label: 'Workspace folder (sibling canvases grouped in workspace)' },
423
+ { value: 'pages', label: 'Canvas pages (multi-page canvas; sibling files become pages)' },
424
+ ],
425
+ initialValue: 'workspace',
426
+ }))
427
+ }
399
428
  }
400
429
 
401
430
  const grid = flags.grid ?? (flagMode ? true : await promptOrCancel(p.confirm({
@@ -422,6 +451,7 @@ async function createCanvas() {
422
451
  name,
423
452
  title: title || name,
424
453
  folder: folder || undefined,
454
+ folderKind: folder && folderKind ? folderKind : undefined,
425
455
  grid,
426
456
  includeJsx,
427
457
  description: description || undefined,
@@ -65,9 +65,14 @@ export const canvasSchema = {
65
65
  },
66
66
  folder: {
67
67
  type: 'string',
68
- description: 'Parent .folder directory',
68
+ description: 'Parent folder directory (name only, no .folder suffix)',
69
69
  aliases: ['f'],
70
70
  },
71
+ 'folder-kind': {
72
+ type: 'string',
73
+ description: 'When creating a new folder: "workspace" → <folder>.folder/ (workspace grouping), "pages" → <folder>/ (multi-page canvas). Ignored when the folder already exists.',
74
+ default: 'pages',
75
+ },
71
76
  grid: {
72
77
  type: 'boolean',
73
78
  default: true,
@@ -38,21 +38,21 @@
38
38
  pointer-events: auto;
39
39
  overflow: hidden;
40
40
  box-shadow: 0 2px 6px rgba(0,0,0,0.2);
41
- background: var(--sb--color-popover, #fff);
41
+ background: var(--color-popover, #fff);
42
42
  }
43
43
  .sb-comment-pin:active { cursor: grabbing; }
44
44
  .sb-pin-img { width: 100%; height: 100%; object-fit: cover; display: block; border-radius: 50%; }
45
45
  .sb-comment-pin[data-resolved="true"] {
46
- border-color: var(--sb--color-muted-foreground, #848d97);
46
+ border-color: var(--color-foreground-muted, #848d97);
47
47
  opacity: 0.5;
48
48
  }
49
49
  .sb-comment-pin-pending {
50
- border-color: var(--sb--color-muted-foreground, #848d97) !important;
50
+ border-color: var(--color-foreground-muted, #848d97) !important;
51
51
  opacity: 0.6;
52
52
  animation: sb-pin-pulse 1.2s ease-in-out infinite;
53
53
  }
54
54
  .sb-comment-pin-failed {
55
- border-color: var(--sb--color-destructive, #d1242f) !important;
55
+ border-color: var(--color-destructive, #d1242f) !important;
56
56
  cursor: pointer;
57
57
  animation: sb-pin-shake 0.4s ease-in-out;
58
58
  }
@@ -71,9 +71,9 @@
71
71
  position: absolute;
72
72
  width: 280px;
73
73
  z-index: 100001;
74
- background: var(--sb--color-popover, #fff);
75
- color: var(--sb--color-popover-foreground, #1f2328);
76
- border: 1px solid var(--sb--color-border, #d1d5db);
74
+ background: var(--color-popover, #fff);
75
+ color: var(--color-popover-foreground, #1f2328);
76
+ border: 1px solid var(--color-border, #d1d5db);
77
77
  border-radius: 0.5rem;
78
78
  box-shadow: 0 8px 24px rgba(0,0,0,0.15);
79
79
  overflow: hidden;
@@ -86,9 +86,9 @@
86
86
  max-height: 480px;
87
87
  z-index: 100001;
88
88
  scrollbar-width: none;
89
- background: var(--sb--color-popover, #fff);
90
- color: var(--sb--color-popover-foreground, #1f2328);
91
- border: 1px solid var(--sb--color-border, #d1d5db);
89
+ background: var(--color-popover, #fff);
90
+ color: var(--color-popover-foreground, #1f2328);
91
+ border: 1px solid var(--color-border, #d1d5db);
92
92
  border-radius: 0.5rem;
93
93
  box-shadow: 0 8px 24px rgba(0,0,0,0.15);
94
94
  overflow: hidden;
@@ -110,8 +110,8 @@
110
110
  z-index: 99998;
111
111
  width: 420px;
112
112
  max-width: 90vw;
113
- background: var(--sb--color-background, #fff);
114
- color: var(--sb--color-foreground, #1f2328);
113
+ background: var(--color-background, #fff);
114
+ color: var(--color-foreground, #1f2328);
115
115
  }
116
116
 
117
117
  /* Drawer animation */
@@ -126,8 +126,8 @@
126
126
  position: fixed;
127
127
  bottom: 12px; left: 50%; transform: translateX(-50%);
128
128
  z-index: 99999;
129
- background: var(--sb--color-popover, #fff);
130
- color: var(--sb--color-popover-foreground, #1f2328);
129
+ background: var(--color-popover, #fff);
130
+ color: var(--color-popover-foreground, #1f2328);
131
131
  padding: 6px 16px; border-radius: 8px; font-size: 13px;
132
132
  line-height: 1.4; backdrop-filter: blur(12px);
133
133
  pointer-events: none;
package/src/core/index.js CHANGED
@@ -100,6 +100,24 @@ export { initConfig, getConfig, setOverrides, clearOverrides, clearAllOverrides,
100
100
  // Customer mode config
101
101
  export { initCustomerModeConfig, getCustomerModeConfig, isCustomerMode } from './stores/customerModeConfig.js'
102
102
 
103
+ // Presentation overrides (advanced, undocumented power-user API)
104
+ export {
105
+ initPresentation,
106
+ setPresentation,
107
+ getPresentation,
108
+ subscribeToPresentation,
109
+ getPresentationSnapshot,
110
+ } from './stores/presentationStore.js'
111
+
112
+ // Canvas interaction (advanced, undocumented — scroll axis + zoom gesture gates)
113
+ export {
114
+ initCanvasInteraction,
115
+ setCanvasInteraction,
116
+ getCanvasInteraction,
117
+ subscribeToCanvasInteraction,
118
+ getCanvasInteractionSnapshot,
119
+ } from './stores/canvasInteractionStore.js'
120
+
103
121
  // Theme
104
122
  export {
105
123
  setTheme,
@@ -270,7 +270,7 @@ export async function createInspectorHighlighter() {
270
270
  return `<span class="${classes.join(' ')}">${numSpan}${line}</span>`
271
271
  }).join('\n')
272
272
 
273
- return `<pre style="background:${colors.bg};color:${colors.fg};margin:0;padding:var(--base-size-8);overflow-x:auto"><code>${wrappedLines}</code></pre>`
273
+ return `<pre style="background:${colors.bg};color:${colors.fg};margin:0;padding:8px;overflow-x:auto"><code>${wrappedLines}</code></pre>`
274
274
  },
275
275
  }
276
276
  }
@@ -57,7 +57,7 @@ export function createMouseMode(options = {}) {
57
57
  position: 'fixed',
58
58
  pointerEvents: 'none',
59
59
  zIndex: '9997',
60
- border: '2px solid var(--sb--color-purple, #7655a4)',
60
+ border: '2px solid var(--color-foreground-accent, #7655a4)',
61
61
  backgroundColor: 'rgba(118, 85, 164, 0.08)',
62
62
  borderRadius: '3px',
63
63
  transition: 'top 0.06s ease, left 0.06s ease, width 0.06s ease, height 0.06s ease',
@@ -73,7 +73,7 @@ export function createMouseMode(options = {}) {
73
73
  position: 'fixed',
74
74
  pointerEvents: 'none',
75
75
  zIndex: '9997',
76
- backgroundColor: 'var(--sb--color-purple, #7655a4)',
76
+ backgroundColor: 'var(--color-foreground-accent, #7655a4)',
77
77
  color: '#fff',
78
78
  fontSize: '11px',
79
79
  fontFamily: '"Hubot Sans", ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace',
@@ -15,9 +15,9 @@
15
15
  top: 50%;
16
16
  left: 50%;
17
17
  transform: translate(-50%, -50%);
18
- background: var(--bgColor-default, #fff);
19
- color: var(--fgColor-default, #1f2328);
20
- border: 1px solid var(--borderColor-default, #d0d7de);
18
+ background: var(--color-background, #fff);
19
+ color: var(--color-foreground, #1f2328);
20
+ border: 1px solid var(--color-border, #d0d7de);
21
21
  border-radius: 12px;
22
22
  box-shadow: 0 16px 32px rgba(0, 0, 0, 0.2);
23
23
  display: flex;
@@ -49,7 +49,7 @@
49
49
  justify-content: space-between;
50
50
  gap: 12px;
51
51
  padding: 16px 20px 12px;
52
- border-bottom: 1px solid var(--borderColor-muted, #d0d7de40);
52
+ border-bottom: 1px solid var(--color-border-muted, #d0d7de40);
53
53
  }
54
54
 
55
55
  .titleGroup {
@@ -68,7 +68,7 @@
68
68
 
69
69
  .subtitle {
70
70
  font-size: 13px;
71
- color: var(--fgColor-muted, #59636e);
71
+ color: var(--color-foreground-muted, #59636e);
72
72
  margin: 0;
73
73
  line-height: 1.4;
74
74
  }
@@ -77,7 +77,7 @@
77
77
  appearance: none;
78
78
  background: transparent;
79
79
  border: 0;
80
- color: var(--fgColor-muted, #59636e);
80
+ color: var(--color-foreground-muted, #59636e);
81
81
  cursor: pointer;
82
82
  padding: 4px;
83
83
  margin: -4px -4px 0 0;
@@ -89,8 +89,8 @@
89
89
  }
90
90
 
91
91
  .closeBtn:hover {
92
- background: var(--bgColor-muted, #f6f8fa);
93
- color: var(--fgColor-default, #1f2328);
92
+ background: var(--color-background-muted, #f6f8fa);
93
+ color: var(--color-foreground, #1f2328);
94
94
  }
95
95
 
96
96
  .body {
@@ -14,10 +14,10 @@
14
14
  --sb--color-dark: #2a2a2a;
15
15
  }
16
16
 
17
- html.storyboard-mode-present { --sb--mode-color: var(--sb--color-green); }
17
+ html.storyboard-mode-present { --sb--mode-color: var(--color-foreground-success); }
18
18
  html.storyboard-mode-plan { --sb--mode-color: var(--sb--color-blue); }
19
- html.storyboard-mode-inspect { --sb--mode-color: var(--sb--color-purple); }
20
- html.storyboard-mode-prototype { --sb--mode-color: var(--sb--color-dark); }
19
+ html.storyboard-mode-inspect { --sb--mode-color: var(--color-foreground-accent); }
20
+ html.storyboard-mode-prototype { --sb--mode-color: var(--color-background-inverse); }
21
21
 
22
22
  html.storyboard-mode-present,
23
23
  html.storyboard-mode-plan,
@@ -51,7 +51,7 @@ html.storyboard-mode-plan > body > #root,
51
51
  html.storyboard-mode-inspect > body > #root {
52
52
  overflow-x: hidden;
53
53
  overflow-y: hidden;
54
- border-radius: var(--borderRadius-default);
54
+ border-radius: var(--radius);
55
55
  /* box-shadow: 0 0 80px 40px var(--sb--mode-color); */
56
56
  }
57
57