@dfosco/storyboard-core 4.0.0-beta.2 → 4.0.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.
- package/dist/storyboard-ui.css +1 -1
- package/dist/storyboard-ui.js +11882 -11126
- package/dist/storyboard-ui.js.map +1 -1
- package/dist/tailwind.css +1 -1
- package/package.json +11 -3
- package/paste.config.json +54 -0
- package/scaffold/deploy.yml +101 -0
- package/scaffold/githooks/pre-push +114 -0
- package/scaffold/manifest.json +11 -0
- package/scaffold/storyboard.config.json +4 -1
- package/src/ActionMenuButton.svelte +12 -2
- package/src/CanvasCreateMenu.svelte +228 -10
- package/src/CanvasSnap.svelte +2 -0
- package/src/CoreUIBar.svelte +152 -3
- package/src/CreateMenuButton.svelte +4 -1
- package/src/InspectorPanel.svelte +2 -0
- package/src/PwaInstallBanner.svelte +124 -0
- package/src/autosync/server.js +99 -111
- package/src/autosync/server.test.js +0 -7
- package/src/canvas/collision.js +206 -0
- package/src/canvas/collision.test.js +271 -0
- package/src/canvas/deriveCanvasId.test.js +40 -0
- package/src/canvas/identity.js +107 -0
- package/src/canvas/identity.test.js +100 -0
- package/src/canvas/server.js +285 -31
- package/src/canvasConfig.js +56 -0
- package/src/canvasConfig.test.js +42 -0
- package/src/cli/canvasAdd.js +185 -0
- package/src/cli/canvasRead.js +208 -0
- package/src/cli/code.js +67 -0
- package/src/cli/create.js +339 -72
- package/src/cli/dev-helpers.js +53 -0
- package/src/cli/dev-helpers.test.js +53 -0
- package/src/cli/dev.js +245 -26
- package/src/cli/flags.js +174 -0
- package/src/cli/flags.test.js +155 -0
- package/src/cli/index.js +84 -13
- package/src/cli/intro.js +37 -0
- package/src/cli/proxy.js +127 -6
- package/src/cli/proxy.test.js +63 -0
- package/src/cli/schemas.js +200 -0
- package/src/cli/serverUrl.js +56 -0
- package/src/cli/setup.js +130 -20
- package/src/cli/snapshots.js +335 -0
- package/src/cli/updateVersion.js +54 -3
- package/src/configSchema.js +125 -0
- package/src/configSchema.test.js +68 -0
- package/src/index.js +5 -0
- package/src/inspector/highlighter.js +10 -2
- package/src/lib/components/ui/trigger-button/trigger-button.svelte +1 -1
- package/src/loader.js +21 -2
- package/src/loader.test.js +63 -1
- package/src/mobileViewport.js +57 -0
- package/src/mobileViewport.test.js +68 -0
- package/src/mountStoryboardCore.js +61 -7
- package/src/rename-watcher/config.json +23 -0
- package/src/rename-watcher/watcher.js +538 -0
- package/src/svelte-plugin-ui/components/Viewfinder.svelte +6 -17
- package/src/tools/handlers/flows.js +6 -7
- package/src/viewfinder.js +21 -9
- package/src/viewfinder.test.js +2 -2
- package/src/vite/server-plugin.js +150 -7
- package/src/workshop/features/createCanvas/CreateCanvasForm.svelte +8 -2
- package/src/workshop/features/createFlow/CreateFlowForm.svelte +1 -1
- package/src/workshop/features/createPage/CreatePageForm.svelte +1 -1
- package/src/workshop/features/createPrototype/CreatePrototypeForm.svelte +2 -2
- package/src/workshop/features/createStory/CreateStoryForm.svelte +160 -0
- package/src/workshop/features/createStory/index.js +14 -0
- package/src/workshop/features/registry.js +2 -0
- package/src/worktree/port.js +57 -1
- package/src/worktree/port.test.js +91 -1
- package/toolbar.config.json +3 -3
- package/widgets.config.json +132 -27
package/src/cli/setup.js
CHANGED
|
@@ -6,16 +6,28 @@
|
|
|
6
6
|
|
|
7
7
|
import * as p from '@clack/prompts'
|
|
8
8
|
import { existsSync } from 'fs'
|
|
9
|
+
import path from 'path'
|
|
9
10
|
import { execSync } from 'child_process'
|
|
10
11
|
import { generateCaddyfile, isCaddyInstalled, isCaddyRunning, startCaddy, reloadCaddy } from './proxy.js'
|
|
12
|
+
import { gettingStartedLines, dim, magenta, bold, green, yellow } from './intro.js'
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
const
|
|
14
|
+
/**
|
|
15
|
+
* Run a potentially slow task with a spinner that only appears after 500ms.
|
|
16
|
+
* If the task completes quickly, shows the done message immediately.
|
|
17
|
+
*/
|
|
18
|
+
async function withSpin(label, doneMsg, fn) {
|
|
19
|
+
const spin = p.spinner()
|
|
20
|
+
const timer = setTimeout(() => spin.start(label), 500)
|
|
21
|
+
try {
|
|
22
|
+
await fn()
|
|
23
|
+
clearTimeout(timer)
|
|
24
|
+
spin.stop(doneMsg)
|
|
25
|
+
} catch (err) {
|
|
26
|
+
clearTimeout(timer)
|
|
27
|
+
spin.stop(`Failed: ${label}`)
|
|
28
|
+
throw err
|
|
29
|
+
}
|
|
30
|
+
}
|
|
19
31
|
|
|
20
32
|
function mascot() {
|
|
21
33
|
const d = dim('·')
|
|
@@ -114,7 +126,115 @@ if (hasBrew) {
|
|
|
114
126
|
}
|
|
115
127
|
}
|
|
116
128
|
|
|
117
|
-
// 5.
|
|
129
|
+
// 5. VS Code CLI
|
|
130
|
+
if (isInstalled('code')) {
|
|
131
|
+
p.log.success('VS Code CLI installed')
|
|
132
|
+
} else {
|
|
133
|
+
// Try to install the `code` CLI from VS Code's known locations
|
|
134
|
+
const codePaths = [
|
|
135
|
+
'/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code',
|
|
136
|
+
'/usr/local/bin/code',
|
|
137
|
+
]
|
|
138
|
+
let installed = false
|
|
139
|
+
for (const codePath of codePaths) {
|
|
140
|
+
if (existsSync(codePath)) {
|
|
141
|
+
p.log.success('VS Code CLI available (symlink exists)')
|
|
142
|
+
installed = true
|
|
143
|
+
break
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (!installed) {
|
|
147
|
+
// Try the VS Code shell command installer
|
|
148
|
+
const vsCodeApp = '/Applications/Visual Studio Code.app'
|
|
149
|
+
if (existsSync(vsCodeApp)) {
|
|
150
|
+
const shellScript = `${vsCodeApp}/Contents/Resources/app/bin/code`
|
|
151
|
+
if (existsSync(shellScript)) {
|
|
152
|
+
try {
|
|
153
|
+
// Create symlink in /usr/local/bin
|
|
154
|
+
run(`ln -sf "${shellScript}" /usr/local/bin/code`)
|
|
155
|
+
p.log.success('VS Code CLI installed (symlinked to /usr/local/bin/code)')
|
|
156
|
+
installed = true
|
|
157
|
+
} catch {
|
|
158
|
+
// Fall through to manual instructions
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (!installed) {
|
|
163
|
+
p.log.warning('VS Code CLI not found. Open VS Code and run:')
|
|
164
|
+
p.log.info(' Cmd+Shift+P → "Shell Command: Install \'code\' command in PATH"')
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 6. Git hooks
|
|
170
|
+
{
|
|
171
|
+
// Create .githooks/ if it doesn't exist — copy from scaffold template
|
|
172
|
+
if (!existsSync('.githooks')) {
|
|
173
|
+
const scaffoldHooks = path.resolve(import.meta.dirname, '..', '..', 'scaffold', 'githooks')
|
|
174
|
+
if (existsSync(scaffoldHooks)) {
|
|
175
|
+
try {
|
|
176
|
+
run(`cp -r "${scaffoldHooks}" .githooks`)
|
|
177
|
+
run('chmod +x .githooks/*')
|
|
178
|
+
} catch { /* fall through */ }
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (existsSync('.githooks')) {
|
|
183
|
+
try {
|
|
184
|
+
run('git config core.hooksPath .githooks')
|
|
185
|
+
if (existsSync('.githooks/pre-push')) {
|
|
186
|
+
run('chmod +x .githooks/pre-push')
|
|
187
|
+
}
|
|
188
|
+
p.log.success('Git hooks activated (.githooks/)')
|
|
189
|
+
} catch {
|
|
190
|
+
p.log.warning('Failed to set git hooks path')
|
|
191
|
+
}
|
|
192
|
+
} else {
|
|
193
|
+
p.log.info(dim('No .githooks/ directory — run storyboard-scaffold first'))
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 7. Asset directories
|
|
198
|
+
{
|
|
199
|
+
const dirs = ['assets/canvas/images', 'assets/canvas/snapshots']
|
|
200
|
+
for (const dir of dirs) {
|
|
201
|
+
if (!existsSync(dir)) {
|
|
202
|
+
try {
|
|
203
|
+
execSync(`mkdir -p ${dir}`, { stdio: 'pipe' })
|
|
204
|
+
} catch { /* ignore */ }
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
p.log.success('Canvas asset directories ready')
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// 8. Playwright (for canvas snapshots)
|
|
211
|
+
{
|
|
212
|
+
let hasPlaywright = false
|
|
213
|
+
try {
|
|
214
|
+
run('node -e "require(\'playwright\')"')
|
|
215
|
+
hasPlaywright = true
|
|
216
|
+
} catch { /* not installed */ }
|
|
217
|
+
|
|
218
|
+
if (hasPlaywright) {
|
|
219
|
+
p.log.success('Playwright installed')
|
|
220
|
+
} else {
|
|
221
|
+
try {
|
|
222
|
+
await withSpin(
|
|
223
|
+
'Installing Playwright + Chromium...',
|
|
224
|
+
'Playwright installed',
|
|
225
|
+
() => {
|
|
226
|
+
run('npm install --save-dev playwright')
|
|
227
|
+
run('npx playwright install chromium')
|
|
228
|
+
}
|
|
229
|
+
)
|
|
230
|
+
} catch {
|
|
231
|
+
p.log.warning('Install manually: npm install --save-dev playwright && npx playwright install chromium')
|
|
232
|
+
p.log.info(dim('Playwright is needed for `storyboard snapshots`'))
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// 9. Proxy
|
|
118
238
|
if (isCaddyInstalled()) {
|
|
119
239
|
const proxySpin = p.spinner()
|
|
120
240
|
const caddyfilePath = generateCaddyfile()
|
|
@@ -131,19 +251,9 @@ if (isCaddyInstalled()) {
|
|
|
131
251
|
|
|
132
252
|
p.note(
|
|
133
253
|
[
|
|
134
|
-
|
|
135
|
-
white(`collaborate on prototypes. Here's how to get started:`),
|
|
136
|
-
'',
|
|
137
|
-
` ${green('npx storyboard dev')} Start developing locally`,
|
|
138
|
-
` ${green('npx storyboard create prototype')} Create a prototype`,
|
|
139
|
-
` ${green('npx storyboard create canvas')} Create a canvas`,
|
|
140
|
-
'',
|
|
141
|
-
` ${dim('Using an AI assistant? You can also ask it to')}`,
|
|
142
|
-
` ${dim('"create a prototype" or "create a canvas" for you!')}`,
|
|
143
|
-
'',
|
|
144
|
-
` ${dim('Docs:')} ${cyan('https://github.com/dfosco/storyboard/blob/main/README.md')}`,
|
|
254
|
+
...gettingStartedLines(),
|
|
145
255
|
'',
|
|
146
|
-
` ${dim('
|
|
256
|
+
` ${dim('Run')} ${yellow('npx storyboard')} ${dim('to see all commands')}`,
|
|
147
257
|
].join('\n'),
|
|
148
258
|
'Getting started'
|
|
149
259
|
)
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* storyboard snapshots — batch-generate preview snapshots for canvas widgets.
|
|
3
|
+
*
|
|
4
|
+
* Fully standalone: reads JSONL from disk, spins up a temporary Vite server
|
|
5
|
+
* on an ephemeral port, uses Playwright to capture each embed at the widget's
|
|
6
|
+
* dimensions (both light and dark themes), writes images to src/canvas/images/,
|
|
7
|
+
* and appends a widgets_replaced event to the JSONL.
|
|
8
|
+
*
|
|
9
|
+
* Does NOT touch the user's running dev server or browser.
|
|
10
|
+
*
|
|
11
|
+
* Requires: playwright installed (globally or locally).
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* storyboard snapshots # all canvases
|
|
15
|
+
* storyboard snapshots <name> # specific canvas
|
|
16
|
+
* storyboard snapshots --force # regenerate even if snapshots exist
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import fs from 'node:fs'
|
|
20
|
+
import path from 'node:path'
|
|
21
|
+
import { materializeFromText, serializeEvent } from '../canvas/materializer.js'
|
|
22
|
+
import * as p from '@clack/prompts'
|
|
23
|
+
import { bold, dim, green, yellow, cyan } from './intro.js'
|
|
24
|
+
|
|
25
|
+
const THEMES = ['light', 'dark']
|
|
26
|
+
const SNAPSHOT_PORT = 19876
|
|
27
|
+
|
|
28
|
+
// ── Disk I/O helpers ──
|
|
29
|
+
|
|
30
|
+
function findCanvasFiles(root) {
|
|
31
|
+
const results = []
|
|
32
|
+
const ignore = new Set(['node_modules', 'dist', '.git', '.worktrees'])
|
|
33
|
+
function walk(dir, rel = '') {
|
|
34
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
if (ignore.has(entry.name)) continue
|
|
37
|
+
const fullPath = path.join(dir, entry.name)
|
|
38
|
+
const relPath = rel ? `${rel}/${entry.name}` : entry.name
|
|
39
|
+
if (entry.isDirectory()) {
|
|
40
|
+
walk(fullPath, relPath)
|
|
41
|
+
} else if (entry.name.endsWith('.canvas.jsonl')) {
|
|
42
|
+
results.push({ relPath, absPath: fullPath })
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
walk(root)
|
|
47
|
+
return results
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function toCanvasId(relPath) {
|
|
51
|
+
return relPath
|
|
52
|
+
.replace(/\.canvas\.jsonl$/, '')
|
|
53
|
+
.replace(/\\/g, '/')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function readCanvasState(absPath) {
|
|
57
|
+
const raw = fs.readFileSync(absPath, 'utf-8')
|
|
58
|
+
return materializeFromText(raw)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function writeImage(imagesDir, widgetId, theme, buffer) {
|
|
62
|
+
fs.mkdirSync(imagesDir, { recursive: true })
|
|
63
|
+
const now = new Date()
|
|
64
|
+
const pad = (n) => String(n).padStart(2, '0')
|
|
65
|
+
const dateStr = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}--${pad(now.getHours())}-${pad(now.getMinutes())}-${pad(now.getSeconds())}`
|
|
66
|
+
const filename = `snapshot-${widgetId}--${theme}--${dateStr}.png`
|
|
67
|
+
fs.writeFileSync(path.join(imagesDir, filename), buffer)
|
|
68
|
+
return filename
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function appendWidgetsReplaced(jsonlPath, widgets) {
|
|
72
|
+
const event = {
|
|
73
|
+
event: 'widgets_replaced',
|
|
74
|
+
timestamp: new Date().toISOString(),
|
|
75
|
+
widgets,
|
|
76
|
+
}
|
|
77
|
+
fs.appendFileSync(jsonlPath, serializeEvent(event) + '\n', 'utf-8')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── Main ──
|
|
81
|
+
|
|
82
|
+
async function run() {
|
|
83
|
+
const args = process.argv.slice(3)
|
|
84
|
+
const force = args.includes('--force')
|
|
85
|
+
const canvasFilter = args.find(a => !a.startsWith('--')) || null
|
|
86
|
+
|
|
87
|
+
p.intro(bold('storyboard snapshots'))
|
|
88
|
+
|
|
89
|
+
const root = process.cwd()
|
|
90
|
+
const imagesDir = path.join(root, 'assets', 'canvas', 'snapshots')
|
|
91
|
+
|
|
92
|
+
// Discover canvases from disk
|
|
93
|
+
const allFiles = findCanvasFiles(root)
|
|
94
|
+
const targets = canvasFilter
|
|
95
|
+
? allFiles.filter(f => {
|
|
96
|
+
const id = toCanvasId(f.relPath)
|
|
97
|
+
return id === canvasFilter || id.includes(canvasFilter)
|
|
98
|
+
})
|
|
99
|
+
: allFiles
|
|
100
|
+
|
|
101
|
+
if (targets.length === 0) {
|
|
102
|
+
p.log.warn(canvasFilter
|
|
103
|
+
? `No canvases matching "${canvasFilter}"`
|
|
104
|
+
: 'No canvases found')
|
|
105
|
+
process.exit(0)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
p.log.info(`Found ${bold(targets.length)} canvas${targets.length > 1 ? 'es' : ''}`)
|
|
109
|
+
|
|
110
|
+
// Import playwright
|
|
111
|
+
let chromium
|
|
112
|
+
try {
|
|
113
|
+
const pw = await import('playwright')
|
|
114
|
+
chromium = pw.chromium
|
|
115
|
+
} catch {
|
|
116
|
+
p.log.error('Playwright is required for snapshot generation.')
|
|
117
|
+
p.log.info('Install: ' + yellow('npx storyboard setup'))
|
|
118
|
+
p.log.info('Or manually: ' + yellow('npm install --save-dev playwright && npx playwright install chromium'))
|
|
119
|
+
process.exit(1)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Start a temporary Vite server on an ephemeral port
|
|
123
|
+
const serverSpin = p.spinner()
|
|
124
|
+
serverSpin.start('Starting temporary Vite server')
|
|
125
|
+
let server
|
|
126
|
+
try {
|
|
127
|
+
const { createServer } = await import('vite')
|
|
128
|
+
server = await createServer({
|
|
129
|
+
root,
|
|
130
|
+
server: { port: SNAPSHOT_PORT, strictPort: false },
|
|
131
|
+
logLevel: 'silent',
|
|
132
|
+
})
|
|
133
|
+
await server.listen()
|
|
134
|
+
const address = server.httpServer.address()
|
|
135
|
+
const serverUrl = `http://localhost:${address.port}`
|
|
136
|
+
serverSpin.stop(`Vite server ready at ${dim(serverUrl)}`)
|
|
137
|
+
|
|
138
|
+
const browser = await chromium.launch({ headless: true })
|
|
139
|
+
|
|
140
|
+
let totalWidgets = 0
|
|
141
|
+
let totalSnapshots = 0
|
|
142
|
+
let totalSkipped = 0
|
|
143
|
+
|
|
144
|
+
for (const { relPath, absPath } of targets) {
|
|
145
|
+
const canvasId = toCanvasId(relPath)
|
|
146
|
+
const spin = p.spinner()
|
|
147
|
+
spin.start(`Reading ${cyan(canvasId)}`)
|
|
148
|
+
|
|
149
|
+
let state
|
|
150
|
+
try {
|
|
151
|
+
state = readCanvasState(absPath)
|
|
152
|
+
} catch (err) {
|
|
153
|
+
spin.stop(`${canvasId}: failed to read — ${err.message}`)
|
|
154
|
+
continue
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Collect embeddable widgets (prototype + story + figma-embed)
|
|
158
|
+
const widgets = (state.widgets || []).filter(w =>
|
|
159
|
+
w.type === 'prototype' || w.type === 'story' || w.type === 'figma-embed'
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
if (widgets.length === 0) {
|
|
163
|
+
spin.stop(`${canvasId}: no embeddable widgets`)
|
|
164
|
+
continue
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
spin.stop(`${canvasId}: ${widgets.length} widget${widgets.length > 1 ? 's' : ''}`)
|
|
168
|
+
totalWidgets += widgets.length
|
|
169
|
+
let canvasDirty = false
|
|
170
|
+
|
|
171
|
+
for (const widget of widgets) {
|
|
172
|
+
const widgetLabel = widget.props?.label || widget.props?.exportName || widget.id
|
|
173
|
+
const rawW = widget.props?.width || 800
|
|
174
|
+
const rawH = widget.props?.height || 600
|
|
175
|
+
const isFigma = widget.type === 'figma-embed'
|
|
176
|
+
|
|
177
|
+
// Compute capture dimensions:
|
|
178
|
+
// - Story widgets have a 31px header above iframe content
|
|
179
|
+
// - Prototype widgets may have a zoom factor
|
|
180
|
+
// - Figma embeds use raw dimensions
|
|
181
|
+
const isStory = widget.type === 'story'
|
|
182
|
+
const zoom = widget.props?.zoom || 100
|
|
183
|
+
const scale = zoom / 100
|
|
184
|
+
const captureW = isStory ? rawW : isFigma ? rawW : Math.round(rawW / scale)
|
|
185
|
+
const captureH = isStory ? Math.max(rawH - 31, 100) : isFigma ? rawH : Math.round(rawH / scale)
|
|
186
|
+
|
|
187
|
+
// Figma embeds only need a single snapshot (no theme variants)
|
|
188
|
+
const themesNeeded = isFigma ? ['light'] : THEMES
|
|
189
|
+
|
|
190
|
+
// Check existing snapshots
|
|
191
|
+
const hasLight = !!widget.props?.snapshotLight
|
|
192
|
+
const hasDark = !!widget.props?.snapshotDark
|
|
193
|
+
const allExist = isFigma ? hasLight : (hasLight && hasDark)
|
|
194
|
+
if (allExist && !force) {
|
|
195
|
+
totalSkipped++
|
|
196
|
+
p.log.step(dim(` ${widgetLabel} — snapshots exist, skipping`))
|
|
197
|
+
continue
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const themesToCapture = force
|
|
201
|
+
? themesNeeded
|
|
202
|
+
: themesNeeded.filter(t => t === 'light' ? !hasLight : !hasDark)
|
|
203
|
+
|
|
204
|
+
const embedUrl = resolveEmbedUrl(serverUrl, widget)
|
|
205
|
+
if (!embedUrl) {
|
|
206
|
+
p.log.warn(` ${widgetLabel} — could not resolve embed URL, skipping`)
|
|
207
|
+
continue
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const updates = {}
|
|
211
|
+
|
|
212
|
+
for (const theme of themesToCapture) {
|
|
213
|
+
const themeUrl = isFigma ? embedUrl : appendThemeParam(embedUrl, theme)
|
|
214
|
+
const wspin = p.spinner()
|
|
215
|
+
wspin.start(` ${widgetLabel}${isFigma ? '' : ` (${theme})`} ${dim(`${captureW}×${captureH}`)}`)
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
const context = await browser.newContext({
|
|
219
|
+
viewport: { width: captureW, height: captureH },
|
|
220
|
+
deviceScaleFactor: 2,
|
|
221
|
+
colorScheme: theme === 'dark' ? 'dark' : 'light',
|
|
222
|
+
})
|
|
223
|
+
const page = await context.newPage()
|
|
224
|
+
|
|
225
|
+
await page.goto(themeUrl, { waitUntil: 'networkidle', timeout: 30000 })
|
|
226
|
+
await page.waitForTimeout(isFigma ? 4000 : 2000)
|
|
227
|
+
|
|
228
|
+
const buffer = await page.screenshot({ type: 'png' })
|
|
229
|
+
await context.close()
|
|
230
|
+
|
|
231
|
+
const filename = writeImage(imagesDir, widget.id, theme, buffer)
|
|
232
|
+
const imageUrl = `/_storyboard/canvas/images/${filename}`
|
|
233
|
+
const themeKey = theme === 'dark' ? 'snapshotDark' : 'snapshotLight'
|
|
234
|
+
updates[themeKey] = imageUrl
|
|
235
|
+
totalSnapshots++
|
|
236
|
+
wspin.stop(green(` ${widgetLabel} (${theme}) ✓`))
|
|
237
|
+
} catch (err) {
|
|
238
|
+
wspin.stop(` ${widgetLabel} (${theme}) — ${err.message}`)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (Object.keys(updates).length > 0) {
|
|
243
|
+
widget.props = { ...widget.props, ...updates }
|
|
244
|
+
canvasDirty = true
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Persist all snapshot updates in a single widgets_replaced event
|
|
249
|
+
if (canvasDirty) {
|
|
250
|
+
try {
|
|
251
|
+
appendWidgetsReplaced(absPath, state.widgets)
|
|
252
|
+
} catch (err) {
|
|
253
|
+
p.log.warn(` ${canvasId}: failed to write JSONL — ${err.message}`)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
await browser.close()
|
|
259
|
+
|
|
260
|
+
// Auto-commit snapshot files so they don't clutter git status
|
|
261
|
+
if (totalSnapshots > 0) {
|
|
262
|
+
try {
|
|
263
|
+
const { execSync } = await import(/* @vite-ignore */ 'node:child_process')
|
|
264
|
+
execSync('git add assets/canvas/snapshots/ src/canvas/', { cwd: root, stdio: 'pipe' })
|
|
265
|
+
execSync(
|
|
266
|
+
`git commit -m "chore: update canvas snapshots" --no-verify --allow-empty`,
|
|
267
|
+
{ cwd: root, stdio: 'pipe' }
|
|
268
|
+
)
|
|
269
|
+
p.log.step(dim('Snapshots committed'))
|
|
270
|
+
} catch {
|
|
271
|
+
// Not in a git repo or nothing to commit — that's fine
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
p.outro([
|
|
276
|
+
green('Done!'),
|
|
277
|
+
`${bold(totalSnapshots)} snapshot${totalSnapshots !== 1 ? 's' : ''} generated`,
|
|
278
|
+
totalSkipped > 0 ? dim(`${totalSkipped} skipped (already exist)`) : '',
|
|
279
|
+
`across ${bold(totalWidgets)} widget${totalWidgets !== 1 ? 's' : ''}`,
|
|
280
|
+
].filter(Boolean).join(' · '))
|
|
281
|
+
} finally {
|
|
282
|
+
if (server) await server.close()
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ── URL helpers ──
|
|
287
|
+
|
|
288
|
+
function resolveEmbedUrl(serverUrl, widget) {
|
|
289
|
+
const { type, props } = widget
|
|
290
|
+
if (type === 'prototype') {
|
|
291
|
+
const src = props?.src
|
|
292
|
+
if (!src) return null
|
|
293
|
+
if (/^https?:\/\//.test(src)) return null
|
|
294
|
+
const cleaned = src.replace(/^\/branch--[^/]+/, '')
|
|
295
|
+
const hashIdx = cleaned.indexOf('#')
|
|
296
|
+
const base = hashIdx >= 0 ? cleaned.slice(0, hashIdx) : cleaned
|
|
297
|
+
const hash = hashIdx >= 0 ? cleaned.slice(hashIdx) : ''
|
|
298
|
+
const sep = base.includes('?') ? '&' : '?'
|
|
299
|
+
return `${serverUrl}${base}${sep}_sb_embed${hash}`
|
|
300
|
+
}
|
|
301
|
+
if (type === 'story') {
|
|
302
|
+
const storyId = props?.storyId
|
|
303
|
+
const exportName = props?.exportName || ''
|
|
304
|
+
if (!storyId) return null
|
|
305
|
+
const params = new URLSearchParams({ _sb_embed: '1' })
|
|
306
|
+
if (exportName) params.set('export', exportName)
|
|
307
|
+
return `${serverUrl}/components/${storyId}?${params}`
|
|
308
|
+
}
|
|
309
|
+
if (type === 'figma-embed') {
|
|
310
|
+
const url = props?.url
|
|
311
|
+
if (!url) return null
|
|
312
|
+
// Convert figma.com URL to embed.figma.com URL
|
|
313
|
+
try {
|
|
314
|
+
const parsed = new URL(url)
|
|
315
|
+
if (!/^(www\.)?figma\.com$/.test(parsed.hostname)) return null
|
|
316
|
+
parsed.hostname = 'embed.figma.com'
|
|
317
|
+
parsed.searchParams.delete('t')
|
|
318
|
+
parsed.searchParams.set('embed-host', 'share')
|
|
319
|
+
return parsed.toString()
|
|
320
|
+
} catch {
|
|
321
|
+
return null
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return null
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function appendThemeParam(url, theme) {
|
|
328
|
+
const sep = url.includes('?') ? '&' : '?'
|
|
329
|
+
return `${url}${sep}_sb_theme_target=prototype&_sb_canvas_theme=${theme}`
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
run().catch((err) => {
|
|
333
|
+
p.log.error(err.message)
|
|
334
|
+
process.exit(1)
|
|
335
|
+
})
|
package/src/cli/updateVersion.js
CHANGED
|
@@ -56,18 +56,69 @@ const tag = channel || (targetVersion ? undefined : 'latest')
|
|
|
56
56
|
const suffix = targetVersion ? `@${targetVersion}` : `@${tag}`
|
|
57
57
|
const packages = [...storyboardPkgs].map(name => `${name}${suffix}`).join(' ')
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
// Resolve actual version from the registry when using a tag (channel or latest)
|
|
60
|
+
let resolvedVersion
|
|
61
|
+
if (tag) {
|
|
62
|
+
try {
|
|
63
|
+
const probe = [...storyboardPkgs][0]
|
|
64
|
+
resolvedVersion = execSync(`npm view ${probe}@${tag} version`, { encoding: 'utf8' }).trim()
|
|
65
|
+
} catch { /* fall back to showing the tag */ }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const displayVersion = resolvedVersion || targetVersion || tag
|
|
69
|
+
const label = resolvedVersion && channel ? `to ${resolvedVersion} (${channel})` : resolvedVersion ? `to ${resolvedVersion}` : channel ? `to ${channel}` : targetVersion ? `to ${targetVersion}` : ''
|
|
60
70
|
p.intro(`storyboard ${command}`)
|
|
61
71
|
p.log.info(`Updating ${storyboardPkgs.size} package(s)${label ? ` ${label}` : ''}…`)
|
|
62
72
|
for (const name of storyboardPkgs) {
|
|
63
|
-
p.log.message(` ${name}
|
|
73
|
+
p.log.message(` ${name}@${displayVersion}`)
|
|
64
74
|
}
|
|
65
75
|
|
|
66
76
|
try {
|
|
67
77
|
execSync(`npm install ${packages}`, { stdio: 'inherit', cwd: process.cwd() })
|
|
68
78
|
p.log.success('All storyboard packages updated')
|
|
69
|
-
p.outro('Done')
|
|
70
79
|
} catch {
|
|
71
80
|
p.log.error('Failed to update packages — see npm output above')
|
|
72
81
|
process.exit(1)
|
|
73
82
|
}
|
|
83
|
+
|
|
84
|
+
// Sync scaffold files (skills, scripts) from the updated package
|
|
85
|
+
p.log.info('Syncing scaffold files…')
|
|
86
|
+
try {
|
|
87
|
+
execSync('npx storyboard-scaffold', { stdio: 'inherit', cwd: process.cwd() })
|
|
88
|
+
} catch {
|
|
89
|
+
p.log.warn('Scaffold sync failed — run `npx storyboard-scaffold` manually')
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Auto-commit the version update (only if package.json or lock file changed)
|
|
93
|
+
try {
|
|
94
|
+
// Read the installed version from the core package
|
|
95
|
+
const corePkg = JSON.parse(readFileSync(resolve(process.cwd(), 'node_modules', '@dfosco', 'storyboard-core', 'package.json'), 'utf8'))
|
|
96
|
+
const installedVersion = corePkg.version || suffix.slice(1)
|
|
97
|
+
const commitMsg = `[storyboard-update] Update storyboard to ${installedVersion}`
|
|
98
|
+
|
|
99
|
+
// Only stage update-related files (package.json, lock files, scaffold outputs)
|
|
100
|
+
const filesToStage = [
|
|
101
|
+
'package.json',
|
|
102
|
+
'package-lock.json',
|
|
103
|
+
'yarn.lock',
|
|
104
|
+
'pnpm-lock.yaml',
|
|
105
|
+
'.github/skills',
|
|
106
|
+
'scripts',
|
|
107
|
+
]
|
|
108
|
+
for (const f of filesToStage) {
|
|
109
|
+
try { execSync(`git add ${f}`, { cwd: process.cwd(), stdio: 'pipe' }) } catch {}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Only commit if there are staged changes
|
|
113
|
+
try {
|
|
114
|
+
execSync('git diff --cached --quiet', { cwd: process.cwd(), stdio: 'pipe' })
|
|
115
|
+
p.log.message('No changes to commit — already up to date')
|
|
116
|
+
} catch {
|
|
117
|
+
execSync(`git commit -m "${commitMsg}"`, { cwd: process.cwd(), stdio: 'pipe' })
|
|
118
|
+
p.log.success(`Committed: ${commitMsg}`)
|
|
119
|
+
}
|
|
120
|
+
} catch (err) {
|
|
121
|
+
p.log.warn(`Auto-commit failed: ${err.message}`)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
p.outro('Done')
|