@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.
- package/dist/storyboard-ui.css +1 -1
- package/dist/storyboard-ui.js +6420 -6336
- package/dist/storyboard-ui.js.map +1 -1
- package/dist/tailwind.css +1 -1
- package/dist/tiny-canvas.css +1 -1
- package/package.json +1 -1
- package/scaffold/src/library/_app.jsx +2 -14
- package/scaffold/src/library/_app.module.css +2 -8
- package/scaffold/src/library/mount.jsx +0 -1
- package/src/canvas/style.css +16 -16
- package/src/core/artifact/operations.js +35 -2
- package/src/core/artifact/schemas/canvas.schema.json +7 -1
- package/src/core/canvas/hot-pool.js +11 -22
- package/src/core/canvas/selectedWidgets.js +1 -1
- package/src/core/canvas/server.js +52 -31
- package/src/core/canvas/server.test.js +107 -0
- package/src/core/cli/artifact.js +2 -0
- package/src/core/cli/create.js +35 -5
- package/src/core/cli/schemas.js +6 -1
- package/src/core/comments/ui/comment-layout.css +14 -14
- package/src/core/index.js +18 -0
- package/src/core/inspector/highlighter.js +1 -1
- package/src/core/inspector/mouseMode.js +2 -2
- package/src/core/lib/components/ui/dialog/dialog.module.css +8 -8
- package/src/core/modes/modes.css +4 -4
- package/src/core/mountStoryboardCore.js +53 -9
- package/src/core/mountStoryboardCore.test.js +25 -1
- package/src/core/rename-watcher/config.json +1 -1
- package/src/core/rename-watcher/watcher.js +1 -1
- package/src/core/stores/canvasInteractionStore.js +120 -0
- package/src/core/stores/canvasInteractionStore.test.js +126 -0
- package/src/core/stores/configSchema.js +23 -6
- package/src/core/stores/configSchema.test.js +3 -0
- package/src/core/stores/customerModeConfig.js +24 -3
- package/src/core/stores/customerModeConfig.test.js +46 -0
- package/src/core/stores/presentationStore.js +118 -0
- package/src/core/stores/presentationStore.test.js +114 -0
- package/src/core/stores/uiConfig.js +10 -2
- package/src/core/styles/tailwind.css +138 -90
- package/src/core/styles/workspace-chrome.css +4 -21
- package/src/core/ui/AutosyncMenuButton.css +7 -7
- package/src/core/ui/BranchSelect.module.css +5 -5
- package/src/core/ui/ChromeSlot.jsx +92 -0
- package/src/core/ui/ChromeSlot.test.jsx +140 -0
- package/src/core/ui/CoreUIBar.css +5 -5
- package/src/core/ui/CoreUIBar.jsx +161 -136
- package/src/core/ui/InspectorPanel.css +5 -5
- package/src/core/ui/InspectorPanel.jsx +9 -9
- package/src/core/ui/PwaInstallBanner.css +5 -5
- package/src/core/ui/PwaInstallBanner.jsx +5 -5
- package/src/core/ui/core-ui-colors.css +28 -66
- package/src/core/ui/sidepanel.css +12 -12
- package/src/core/workshop/features/createCanvas/CreateCanvasForm.jsx +102 -13
- package/src/internals/ArtifactForm/ArtifactForm.module.css +7 -7
- package/src/internals/AuthModal/AuthModal.module.css +34 -34
- package/src/internals/BaseUiForm/BaseUiForm.module.css +85 -85
- package/src/internals/BranchBar/BranchBar.module.css +4 -4
- package/src/internals/BranchesDropdown/BranchesDropdown.module.css +9 -9
- package/src/internals/CommandPalette/CommandPalette.jsx +16 -10
- package/src/internals/CommandPalette/WidgetArtifactDialog.css +16 -16
- package/src/internals/CommandPalette/command-palette.css +14 -14
- package/src/internals/CreatePage/CreatePage.module.css +1 -1
- package/src/internals/FlowError.module.css +5 -5
- package/src/internals/PrototypeErrorBoundary.module.css +15 -15
- package/src/internals/SimpleWorkspace/SimpleWorkspace.jsx +5 -4
- package/src/internals/SimpleWorkspace/SimpleWorkspace.module.css +38 -38
- package/src/internals/Viewfinder.jsx +33 -16
- package/src/internals/Viewfinder.module.css +189 -189
- package/src/internals/canvas/CanvasControls.module.css +11 -11
- package/src/internals/canvas/CanvasPage.bridge.test.jsx +11 -37
- package/src/internals/canvas/CanvasPage.jsx +91 -93
- package/src/internals/canvas/CanvasPage.module.css +20 -19
- package/src/internals/canvas/CanvasToolbar.module.css +11 -11
- package/src/internals/canvas/ConnectorLayer.jsx +39 -2
- package/src/internals/canvas/ConnectorLayer.module.css +10 -10
- package/src/internals/canvas/PageSelector.module.css +28 -28
- package/src/internals/canvas/canvasTheme.js +22 -111
- package/src/internals/canvas/isolation/prototypeRoutes.jsx +12 -3
- package/src/internals/canvas/isolation/storyRoutes.jsx +1 -1
- package/src/internals/canvas/widgets/CodePenEmbed.jsx +16 -36
- package/src/internals/canvas/widgets/CodePenEmbed.module.css +12 -12
- package/src/internals/canvas/widgets/CropOverlay.module.css +23 -23
- package/src/internals/canvas/widgets/ExpandedPane.module.css +8 -8
- package/src/internals/canvas/widgets/FigmaEmbed.jsx +15 -36
- package/src/internals/canvas/widgets/FigmaEmbed.module.css +15 -15
- package/src/internals/canvas/widgets/FrozenTerminalOverlay.module.css +5 -5
- package/src/internals/canvas/widgets/ImageWidget.module.css +3 -3
- package/src/internals/canvas/widgets/LinkPreview.module.css +77 -78
- package/src/internals/canvas/widgets/MarkdownBlock.module.css +31 -31
- package/src/internals/canvas/widgets/PromptWidget.jsx +2 -16
- package/src/internals/canvas/widgets/PromptWidget.module.css +33 -33
- package/src/internals/canvas/widgets/PrototypeEmbed.jsx +23 -7
- package/src/internals/canvas/widgets/PrototypeEmbed.module.css +58 -56
- package/src/internals/canvas/widgets/PrototypeEmbed.test.jsx +4 -4
- package/src/internals/canvas/widgets/ResizeHandle.module.css +4 -4
- package/src/internals/canvas/widgets/StorySetWidget.jsx +45 -21
- package/src/internals/canvas/widgets/StoryWidget.jsx +53 -21
- package/src/internals/canvas/widgets/StoryWidget.module.css +22 -20
- package/src/internals/canvas/widgets/TerminalReadWidget.module.css +4 -4
- package/src/internals/canvas/widgets/TerminalWidget.jsx +56 -45
- package/src/internals/canvas/widgets/TerminalWidget.module.css +10 -10
- package/src/internals/canvas/widgets/TilesWidget.module.css +17 -17
- package/src/internals/canvas/widgets/WidgetChrome.module.css +61 -61
- package/src/internals/canvas/widgets/WidgetWrapper.module.css +2 -2
- package/src/internals/canvas/widgets/embedOverlay.module.css +14 -4
- package/src/internals/canvas/widgets/embedTheme.js +10 -102
- package/src/internals/canvas/widgets/widgetConfig.js +5 -5
- package/src/internals/story/ComponentSetPage.module.css +14 -14
- package/src/internals/story/StoryPage.module.css +2 -2
- package/src/internals/vite/data-plugin.js +5 -2
- package/src/internals/canvas/WebGLContextPool.jsx +0 -291
- package/src/internals/canvas/WebGLContextPool.test.jsx +0 -165
- package/src/internals/canvas/widgets/ComponentSetWidget.module.css +0 -89
- 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
|
|
850
|
-
*
|
|
851
|
-
*
|
|
852
|
-
*
|
|
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
|
|
856
|
+
* @returns {{ ready: boolean }}
|
|
857
857
|
*/
|
|
858
858
|
function peekPool(hotPool, poolId, mode) {
|
|
859
|
-
if (!hotPool || mode === 'cold') return { ready: false
|
|
860
|
-
if (!hotPool.has(poolId)) return { ready: 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
|
|
948
|
-
// .folder directories (
|
|
949
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1277
|
-
//
|
|
1278
|
-
//
|
|
1279
|
-
//
|
|
1280
|
-
let hotProbe = { ready: 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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
+
})
|
package/src/core/cli/artifact.js
CHANGED
package/src/core/cli/create.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
...
|
|
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,
|
package/src/core/cli/schemas.js
CHANGED
|
@@ -65,9 +65,14 @@ export const canvasSchema = {
|
|
|
65
65
|
},
|
|
66
66
|
folder: {
|
|
67
67
|
type: 'string',
|
|
68
|
-
description: 'Parent .folder
|
|
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(--
|
|
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(--
|
|
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(--
|
|
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(--
|
|
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(--
|
|
75
|
-
color: var(--
|
|
76
|
-
border: 1px solid var(--
|
|
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(--
|
|
90
|
-
color: var(--
|
|
91
|
-
border: 1px solid var(--
|
|
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(--
|
|
114
|
-
color: var(--
|
|
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(--
|
|
130
|
-
color: var(--
|
|
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:
|
|
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(--
|
|
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(--
|
|
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(--
|
|
19
|
-
color: var(--
|
|
20
|
-
border: 1px solid var(--
|
|
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(--
|
|
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(--
|
|
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(--
|
|
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(--
|
|
93
|
-
color: var(--
|
|
92
|
+
background: var(--color-background-muted, #f6f8fa);
|
|
93
|
+
color: var(--color-foreground, #1f2328);
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
.body {
|
package/src/core/modes/modes.css
CHANGED
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
--sb--color-dark: #2a2a2a;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
html.storyboard-mode-present { --sb--mode-color: var(--
|
|
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(--
|
|
20
|
-
html.storyboard-mode-prototype { --sb--mode-color: var(--
|
|
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(--
|
|
54
|
+
border-radius: var(--radius);
|
|
55
55
|
/* box-shadow: 0 0 80px 40px var(--sb--mode-color); */
|
|
56
56
|
}
|
|
57
57
|
|