@dfosco/storyboard-core 3.3.0 → 3.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dfosco/storyboard-core",
3
- "version": "3.3.0",
3
+ "version": "3.3.2",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -48,7 +48,7 @@
48
48
  let canvasActive = $state(false)
49
49
  let activeCanvasName = $state('')
50
50
  let canvasZoom = $state(100)
51
- const canvasToolbarConfig = (config as any).canvasToolbar || {}
51
+ const canvasToolbarConfig = $derived((config as any).canvasToolbar || {})
52
52
 
53
53
  const ZOOM_STEP = 10
54
54
  const ZOOM_MIN = 25
@@ -57,18 +57,21 @@
57
57
  // Roving tabindex: only one button in the toolbar is tabbable at a time
58
58
  let activeToolbarIndex = $state(-1)
59
59
 
60
- const commandMenuConfig = isMenuHidden('command') ? null : config.menus?.command
61
- const shortcutsConfig = (config as any).shortcuts || {}
60
+ const isLocalDev = typeof window !== 'undefined' && (window as any).__SB_LOCAL_DEV__ === true
61
+
62
+ const commandMenuConfig = $derived(isMenuHidden('command') ? null : config.menus?.command)
63
+ const shortcutsConfig = $derived((config as any).shortcuts || {})
62
64
 
63
65
  // Build ordered menu list from JSON key order (excluding command, which is always rightmost)
64
- const allMenus = (config.menus || {}) as Record<string, any>
65
- const orderedMenus = Object.entries(allMenus)
66
+ const allMenus = $derived((config.menus || {}) as Record<string, any>)
67
+ const orderedMenus = $derived(Object.entries(allMenus)
66
68
  .filter(([key]) => key !== 'command')
67
69
  .filter(([key]) => !isMenuHidden(key))
68
- .map(([key, menu]) => ({ key, ...menu }))
70
+ .filter(([, menu]) => !menu.localOnly || isLocalDev)
71
+ .map(([key, menu]) => ({ key, ...menu })))
69
72
 
70
73
  // Discover menus with sidepanel property
71
- const sidepanelMenus = orderedMenus.filter(menu => menu.sidepanel)
74
+ const sidepanelMenus = $derived(orderedMenus.filter(menu => menu.sidepanel))
72
75
 
73
76
  function menuVisibleInMode(menu: any, mode: string): boolean {
74
77
  if (!menu?.modes) return false
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Consumer-safe proxy for mountDevTools.
3
+ *
4
+ * Delegates to the compiled UI bundle (@dfosco/storyboard-core/ui-runtime)
5
+ * so consumers don't need svelte installed. The real mountDevTools in
6
+ * devtools.js imports svelte directly and is only usable in the source repo
7
+ * or via the compiled UI bundle.
8
+ */
9
+
10
+ export async function mountDevTools(options = {}) {
11
+ const ui = await import('@dfosco/storyboard-core/ui-runtime')
12
+ return ui.mountDevTools(options)
13
+ }
14
+
15
+ export async function unmountDevTools() {
16
+ const ui = await import('@dfosco/storyboard-core/ui-runtime')
17
+ return ui.unmountDevTools()
18
+ }
19
+
20
+ /** @deprecated Use mountDevTools instead. */
21
+ export function mountFlowDebug(options = {}) {
22
+ return mountDevTools(options)
23
+ }
24
+
25
+ /** @deprecated Use mountDevTools instead. */
26
+ export function mountSceneDebug(options = {}) {
27
+ return mountDevTools(options)
28
+ }
package/src/index.js CHANGED
@@ -49,7 +49,9 @@ export { registerMode, unregisterMode, getRegisteredModes, getCurrentMode, activ
49
49
  export { initTools, setToolAction, setToolState, getToolState, getToolsForMode, subscribeToTools, getToolsSnapshot } from './modes.js'
50
50
 
51
51
  // Dev tools (vanilla JS, framework-agnostic)
52
- export { mountDevTools } from './devtools.js'
52
+ // mountDevTools delegates to the compiled UI bundle so consumers
53
+ // don't need svelte installed — svelte is bundled into ui-runtime.
54
+ export { mountDevTools } from './devtools-consumer.js'
53
55
  export { mountFlowDebug } from './sceneDebug.js'
54
56
  // Deprecated alias
55
57
  export { mountSceneDebug } from './sceneDebug.js'
@@ -4,23 +4,40 @@
4
4
  * Uses shiki/core with only the four languages the inspector needs,
5
5
  * avoiding the full shiki bundle that registers 200+ lazy-loaded
6
6
  * language chunks (which break in deployed/static environments).
7
+ *
8
+ * Import specifiers are computed via template literals so consumer
9
+ * bundlers (Rollup, esbuild) can't statically analyze them — they
10
+ * skip dynamic imports with variable specifiers instead of erroring.
11
+ * Returns null when shiki is unavailable.
7
12
  */
13
+
14
+ // Variable indirection prevents any bundler from statically resolving
15
+ const SHIKI = 'shiki'
16
+
8
17
  export async function createInspectorHighlighter() {
9
- const [{ createHighlighterCore }, { createOnigurumaEngine }, tsx, jsx, javascript, typescript, githubDark, wasm] =
10
- await Promise.all([
11
- import('shiki/core'),
12
- import('shiki/engine/oniguruma'),
13
- import('shiki/dist/langs/tsx.mjs'),
14
- import('shiki/dist/langs/jsx.mjs'),
15
- import('shiki/dist/langs/javascript.mjs'),
16
- import('shiki/dist/langs/typescript.mjs'),
17
- import('shiki/dist/themes/github-dark.mjs'),
18
- import('shiki/wasm'),
19
- ])
18
+ try {
19
+ const [shikiCore, oniguruma, tsx, jsx, javascript, typescript, githubDark, wasm] =
20
+ await Promise.all([
21
+ import(/* @vite-ignore */ `${SHIKI}/core`).catch(() => null),
22
+ import(/* @vite-ignore */ `${SHIKI}/engine/oniguruma`).catch(() => null),
23
+ import(/* @vite-ignore */ `${SHIKI}/dist/langs/tsx.mjs`).catch(() => null),
24
+ import(/* @vite-ignore */ `${SHIKI}/dist/langs/jsx.mjs`).catch(() => null),
25
+ import(/* @vite-ignore */ `${SHIKI}/dist/langs/javascript.mjs`).catch(() => null),
26
+ import(/* @vite-ignore */ `${SHIKI}/dist/langs/typescript.mjs`).catch(() => null),
27
+ import(/* @vite-ignore */ `${SHIKI}/dist/themes/github-dark.mjs`).catch(() => null),
28
+ import(/* @vite-ignore */ `${SHIKI}/wasm`).catch(() => null),
29
+ ])
30
+
31
+ if (!shikiCore || !oniguruma || !tsx || !jsx || !javascript || !typescript || !githubDark || !wasm) {
32
+ return null
33
+ }
20
34
 
21
- return createHighlighterCore({
22
- themes: [githubDark.default],
23
- langs: [tsx.default, jsx.default, javascript.default, typescript.default],
24
- engine: createOnigurumaEngine(wasm),
25
- })
35
+ return shikiCore.createHighlighterCore({
36
+ themes: [githubDark.default],
37
+ langs: [tsx.default, jsx.default, javascript.default, typescript.default],
38
+ engine: oniguruma.createOnigurumaEngine(wasm),
39
+ })
40
+ } catch {
41
+ return null
42
+ }
26
43
  }
@@ -176,13 +176,32 @@ export default function storyboardServer() {
176
176
  },
177
177
 
178
178
  transformIndexHtml() {
179
- if (clientScripts.length === 0) return []
179
+ const tags = []
180
180
 
181
- return clientScripts.map((src) => ({
181
+ // Inject local dev flag so the UI can gate dev-only tools
182
+ tags.push({
182
183
  tag: 'script',
183
- attrs: { type: 'module', src: base + src.replace(/^\//, '') },
184
- injectTo: 'body',
185
- }))
184
+ children: 'window.__SB_LOCAL_DEV__=true',
185
+ injectTo: 'head',
186
+ })
187
+
188
+ // Inject base path so the inspector UI can resolve static assets
189
+ // (e.g. inspector.json) when deployed under a subpath
190
+ tags.push({
191
+ tag: 'script',
192
+ children: `window.__STORYBOARD_BASE_PATH__=${JSON.stringify(base)}`,
193
+ injectTo: 'head',
194
+ })
195
+
196
+ for (const src of clientScripts) {
197
+ tags.push({
198
+ tag: 'script',
199
+ attrs: { type: 'module', src: base + src.replace(/^\//, '') },
200
+ injectTo: 'body',
201
+ })
202
+ }
203
+
204
+ return tags
186
205
  },
187
206
 
188
207
  // Build-time: emit a static JSON with source files so the inspector
@@ -62,8 +62,8 @@
62
62
  const canSubmit = $derived(!!kebabName && !nameError && !submitting && (!isExternal || (!!externalUrl.trim() && !urlError)))
63
63
 
64
64
  const templateLabel = $derived(partial ? partials.find(p => p.name === partial)?.name ?? partial : 'No template')
65
- const templates = $derived(partials.filter(p => p.directory === 'template'))
66
- const recipes = $derived(partials.filter(p => p.directory === 'recipe'))
65
+ const templates = $derived(partials.filter(p => p.directory === 'template' || p.directory === 'templates'))
66
+ const recipes = $derived(partials.filter(p => p.directory === 'recipe' || p.directory === 'recipes'))
67
67
  let templateMenuOpen = $state(false)
68
68
 
69
69
  function getApiUrl() {
@@ -20,10 +20,12 @@ import path from 'node:path'
20
20
 
21
21
  const FLOW_SKELETON = JSON.stringify({ $global: [] }, null, 2) + '\n'
22
22
 
23
- /** Map partial directory value → source directory name */
24
- const DIR_MAP = {
25
- recipe: 'recipes',
26
- template: 'templates',
23
+ /**
24
+ * Check whether a partial entry is a template (vs recipe).
25
+ * Accepts both singular and plural forms: "template" or "templates".
26
+ */
27
+ function isTemplate(partialEntry) {
28
+ return partialEntry.directory === 'template' || partialEntry.directory === 'templates'
27
29
  }
28
30
 
29
31
  // ---------------------------------------------------------------------------
@@ -118,10 +120,9 @@ function generateBlankIndexJsx(componentName, title) {
118
120
  * @param {string} title - Human-readable title
119
121
  */
120
122
  function generateIndexJsx({ partialEntry, componentFile, componentName, title }) {
121
- const typeDir = DIR_MAP[partialEntry.directory]
122
- const importPath = `@/${typeDir}/${partialEntry.name}/${componentFile}`
123
+ const importPath = `@/${partialEntry.directory}/${partialEntry.name}/${componentFile}`
123
124
 
124
- if (partialEntry.directory === 'template') {
125
+ if (isTemplate(partialEntry)) {
125
126
  return `import ${componentFile} from '${importPath}'
126
127
 
127
128
  export default function ${componentName}() {
@@ -303,16 +304,10 @@ export function createPrototypesHandler(ctx) {
303
304
  if (!partialEntry) {
304
305
  content = generateBlankIndexJsx(componentName, title)
305
306
  } else {
306
- const typeDir = DIR_MAP[partialEntry.directory]
307
- if (!typeDir) {
308
- sendJson(res, 400, { error: `Invalid directory "${partialEntry.directory}". Must be "recipe" or "template".` })
309
- return
310
- }
311
-
312
- const partialDir = path.join(root, 'src', typeDir, partialEntry.name)
307
+ const partialDir = path.join(root, 'src', partialEntry.directory, partialEntry.name)
313
308
  const componentFile = findComponentFile(partialDir)
314
309
  if (!componentFile) {
315
- sendJson(res, 400, { error: `No .jsx or .tsx file found in src/${typeDir}/${partialEntry.name}/` })
310
+ sendJson(res, 400, { error: `No .jsx or .tsx file found in src/${partialEntry.directory}/${partialEntry.name}/` })
316
311
  return
317
312
  }
318
313
 
@@ -34,6 +34,7 @@
34
34
  "icon": "iconoir/plus-circle",
35
35
  "meta": { "strokeWeight": 2, "scale": 1.1 },
36
36
  "modes": ["*"],
37
+ "localOnly": true,
37
38
  "menuWidth": "260px",
38
39
  "actions": [
39
40
  { "type": "header", "label": "Create" },