@dfosco/storyboard-core 3.3.2 → 3.5.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 (51) hide show
  1. package/dist/storyboard-ui.css +1 -1
  2. package/dist/storyboard-ui.js +14899 -11508
  3. package/dist/storyboard-ui.js.map +1 -1
  4. package/dist/tailwind.css +1 -1
  5. package/package.json +1 -1
  6. package/scaffold/toolbar.config.json +2 -2
  7. package/src/CanvasCreateMenu.svelte +1 -1
  8. package/src/CanvasZoomControl.svelte +105 -0
  9. package/src/CommandMenu.svelte +87 -25
  10. package/src/CoreUIBar.svelte +350 -347
  11. package/src/CreateMenuButton.svelte +6 -2
  12. package/src/InspectorPanel.svelte +123 -59
  13. package/src/SidePanel.svelte +1 -1
  14. package/src/ThemeMenuButton.svelte +35 -3
  15. package/src/commandActions.js +14 -0
  16. package/src/core-ui-colors.css +30 -2
  17. package/src/devtools.js +7 -1
  18. package/src/index.js +10 -1
  19. package/src/inspector/fiberWalker.js +49 -6
  20. package/src/inspector/highlighter.js +257 -33
  21. package/src/lib/components/ui/button/button.svelte +1 -1
  22. package/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte +1 -1
  23. package/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte +1 -1
  24. package/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte +1 -1
  25. package/src/lib/components/ui/trigger-button/trigger-button.svelte +31 -3
  26. package/src/modes.css +8 -0
  27. package/src/mountStoryboardCore.js +15 -1
  28. package/src/sidepanel.css +2 -2
  29. package/src/stores/themeStore.ts +66 -0
  30. package/src/svelte-plugin-ui/components/Viewfinder.svelte +16 -11
  31. package/src/toolRegistry.js +226 -0
  32. package/src/toolStateStore.js +180 -0
  33. package/src/toolStateStore.test.js +204 -0
  34. package/src/toolbarConfigStore.js +135 -0
  35. package/src/tools/handlers/canvasAddWidget.js +11 -0
  36. package/src/tools/handlers/canvasZoom.js +34 -0
  37. package/src/tools/handlers/comments.js +16 -0
  38. package/src/tools/handlers/create.js +39 -0
  39. package/src/tools/handlers/devtools.js +80 -0
  40. package/src/tools/handlers/docs.js +11 -0
  41. package/src/tools/handlers/featureFlags.js +21 -0
  42. package/src/tools/handlers/flows.js +62 -0
  43. package/src/tools/handlers/inspector.js +19 -0
  44. package/src/tools/handlers/theme.js +9 -0
  45. package/src/tools/registry.js +21 -0
  46. package/src/tools/surfaces/canvasToolbar.js +10 -0
  47. package/src/tools/surfaces/commandList.js +10 -0
  48. package/src/tools/surfaces/mainToolbar.js +11 -0
  49. package/src/tools/surfaces/registry.js +19 -0
  50. package/src/vite/server-plugin.js +36 -6
  51. package/toolbar.config.json +101 -48
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Toolbar Config Store — reactive toolbar configuration with layered overrides.
3
+ *
4
+ * Override priority (highest wins):
5
+ * core (toolbar.config.json) → custom (client repo) → prototype → user (future)
6
+ *
7
+ * Framework-agnostic (zero npm dependencies).
8
+ */
9
+
10
+ import { deepMerge } from './loader.js'
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Internal state
14
+ // ---------------------------------------------------------------------------
15
+
16
+ /** @type {object} Merged core + custom config (set once at startup) */
17
+ let _baseConfig = {}
18
+
19
+ /** @type {object|null} Active prototype toolbar overrides */
20
+ let _prototypeConfig = null
21
+
22
+ /** @type {object} Final merged config (base + prototype) */
23
+ let _mergedConfig = {}
24
+
25
+ /** @type {Set<Function>} */
26
+ const _listeners = new Set()
27
+
28
+ let _snapshotVersion = 0
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Initialization
32
+ // ---------------------------------------------------------------------------
33
+
34
+ /**
35
+ * Set the base toolbar config (core defaults merged with client overrides).
36
+ * Called once at app startup by mountStoryboardCore.
37
+ *
38
+ * @param {object} config - Already-merged core + custom toolbar config
39
+ */
40
+ export function initToolbarConfig(config) {
41
+ _baseConfig = config
42
+ _prototypeConfig = null
43
+ _recompute()
44
+ }
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Prototype overrides
48
+ // ---------------------------------------------------------------------------
49
+
50
+ /**
51
+ * Set toolbar overrides for the active prototype.
52
+ * Called on route change when entering a prototype that has a toolbar.config.json.
53
+ *
54
+ * @param {object|null} config - Prototype-level overrides, or null to clear
55
+ */
56
+ export function setPrototypeToolbarConfig(config) {
57
+ _prototypeConfig = config || null
58
+ _recompute()
59
+ }
60
+
61
+ /**
62
+ * Clear prototype overrides (e.g. when navigating to viewfinder or a
63
+ * prototype without its own toolbar.config.json).
64
+ */
65
+ export function clearPrototypeToolbarConfig() {
66
+ if (_prototypeConfig === null) return
67
+ _prototypeConfig = null
68
+ _recompute()
69
+ }
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // Access
73
+ // ---------------------------------------------------------------------------
74
+
75
+ /**
76
+ * Get the current merged toolbar config.
77
+ *
78
+ * @returns {object}
79
+ */
80
+ export function getToolbarConfig() {
81
+ return _mergedConfig
82
+ }
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // Reactivity
86
+ // ---------------------------------------------------------------------------
87
+
88
+ /**
89
+ * Subscribe to toolbar config changes. Compatible with Svelte stores.
90
+ *
91
+ * @param {Function} callback
92
+ * @returns {Function} Unsubscribe
93
+ */
94
+ export function subscribeToToolbarConfig(callback) {
95
+ _listeners.add(callback)
96
+ callback(_mergedConfig)
97
+ return () => _listeners.delete(callback)
98
+ }
99
+
100
+ /**
101
+ * Snapshot for useSyncExternalStore.
102
+ *
103
+ * @returns {string}
104
+ */
105
+ export function getToolbarConfigSnapshot() {
106
+ return String(_snapshotVersion)
107
+ }
108
+
109
+ // ---------------------------------------------------------------------------
110
+ // Internal
111
+ // ---------------------------------------------------------------------------
112
+
113
+ function _recompute() {
114
+ _mergedConfig = _prototypeConfig
115
+ ? deepMerge(_baseConfig, _prototypeConfig)
116
+ : _baseConfig
117
+ _snapshotVersion++
118
+ for (const cb of _listeners) {
119
+ try { cb(_mergedConfig) } catch (err) {
120
+ console.error('[storyboard] Error in toolbar config subscriber:', err)
121
+ }
122
+ }
123
+ }
124
+
125
+ // ---------------------------------------------------------------------------
126
+ // Test helpers
127
+ // ---------------------------------------------------------------------------
128
+
129
+ export function _resetToolbarConfig() {
130
+ _baseConfig = {}
131
+ _prototypeConfig = null
132
+ _mergedConfig = {}
133
+ _listeners.clear()
134
+ _snapshotVersion = 0
135
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Canvas add-widget tool module — dropdown menu for adding widgets to canvas.
3
+ *
4
+ * Wraps the CanvasCreateMenu component. Only active on canvas pages.
5
+ */
6
+ export const id = 'canvas-add-widget'
7
+
8
+ export async function component() {
9
+ const mod = await import('../../CanvasCreateMenu.svelte')
10
+ return mod.default
11
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Canvas zoom tool module — zoom in/out/reset controls for canvas pages.
3
+ *
4
+ * Provides zoom actions via custom events (Svelte↔React bridge).
5
+ * Uses the unique "zoom-control" render type.
6
+ */
7
+ export const id = 'canvas-zoom'
8
+
9
+ const ZOOM_STEP = 10
10
+ const ZOOM_MIN = 25
11
+ const ZOOM_MAX = 200
12
+
13
+ export async function handler() {
14
+ return {
15
+ zoomIn(currentZoom) {
16
+ const next = Math.min(ZOOM_MAX, currentZoom + ZOOM_STEP)
17
+ document.dispatchEvent(new CustomEvent('storyboard:canvas:set-zoom', { detail: { zoom: next } }))
18
+ },
19
+ zoomOut(currentZoom) {
20
+ const next = Math.max(ZOOM_MIN, currentZoom - ZOOM_STEP)
21
+ document.dispatchEvent(new CustomEvent('storyboard:canvas:set-zoom', { detail: { zoom: next } }))
22
+ },
23
+ zoomReset() {
24
+ document.dispatchEvent(new CustomEvent('storyboard:canvas:set-zoom', { detail: { zoom: 100 } }))
25
+ },
26
+ ZOOM_MIN,
27
+ ZOOM_MAX,
28
+ }
29
+ }
30
+
31
+ export async function component() {
32
+ const mod = await import('../../CanvasZoomControl.svelte')
33
+ return mod.default
34
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Comments tool module — comment pins and annotation system.
3
+ *
4
+ * Guard: only mounts if comments are enabled in storyboard.config.json.
5
+ */
6
+ export const id = 'comments'
7
+
8
+ export async function guard() {
9
+ const { isCommentsEnabled } = await import('../../comments/config.js')
10
+ return isCommentsEnabled()
11
+ }
12
+
13
+ export async function component() {
14
+ const mod = await import('../../CommentsMenuButton.svelte')
15
+ return mod.default
16
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Create tool module — workshop prototype/flow/canvas creation.
3
+ *
4
+ * Loads the workshop feature registry and CreateMenuButton component.
5
+ * Guard: only mounts if at least one create feature has an overlay.
6
+ */
7
+ export const id = 'create'
8
+
9
+ export async function setup(ctx) {
10
+ const { config } = ctx
11
+ const { features } = await import('../../workshop/features/registry.js')
12
+
13
+ const createActions = Array.isArray(config.actions) ? config.actions : []
14
+ const createFeatures = createActions
15
+ .filter(a => a.feature)
16
+ .map(a => {
17
+ const feat = features[a.feature]
18
+ if (!feat || !feat.overlayId || !feat.overlay) return null
19
+ return {
20
+ name: feat.name,
21
+ label: a.label || feat.label,
22
+ overlayId: feat.overlayId,
23
+ overlay: feat.overlay,
24
+ }
25
+ })
26
+ .filter(Boolean)
27
+
28
+ return { features: createFeatures }
29
+ }
30
+
31
+ export async function guard(ctx) {
32
+ const result = await setup(ctx)
33
+ return result.features.length > 0
34
+ }
35
+
36
+ export async function component() {
37
+ const mod = await import('../../CreateMenuButton.svelte')
38
+ return mod.default
39
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Devtools tool module — developer utilities submenu.
3
+ *
4
+ * Renders as a submenu in the command menu with:
5
+ * - Show flow info
6
+ * - Reset all params
7
+ * - Hide mode toggle
8
+ * - Logout (when authenticated)
9
+ */
10
+ export const id = 'devtools'
11
+
12
+ /**
13
+ * @param {object} ctx
14
+ * @param {Function} ctx.showFlowInfoDialog - callback to open flow info dialog
15
+ */
16
+ export async function handler(ctx) {
17
+ let loader = null
18
+ let hm = null
19
+ let commentsAuth = null
20
+ try { loader = await import('../../loader.js') } catch { /* optional */ }
21
+ try { hm = await import('../../hideMode.js') } catch { /* optional */ }
22
+ try { commentsAuth = await import('../../comments/auth.js') } catch { /* optional */ }
23
+
24
+ return {
25
+ getChildren: () => {
26
+ const children = []
27
+ if (loader) {
28
+ children.push({
29
+ id: 'core/show-flow-info',
30
+ label: 'Show flow info',
31
+ type: 'default',
32
+ execute: () => {
33
+ const p = new URLSearchParams(window.location.search)
34
+ const name = p.get('flow') || p.get('scene') || 'default'
35
+ try {
36
+ const data = loader.loadFlow(name)
37
+ if (ctx.showFlowInfoDialog) {
38
+ ctx.showFlowInfoDialog(name, JSON.stringify(data, null, 2), null)
39
+ }
40
+ } catch (e) {
41
+ if (ctx.showFlowInfoDialog) {
42
+ ctx.showFlowInfoDialog(name, '', e.message)
43
+ }
44
+ }
45
+ },
46
+ })
47
+ }
48
+ children.push({
49
+ id: 'core/reset-params',
50
+ label: 'Reset all params',
51
+ type: 'default',
52
+ execute: () => { window.location.hash = '' },
53
+ })
54
+ if (hm) {
55
+ children.push({
56
+ id: 'core/hide-mode',
57
+ label: 'Hide mode',
58
+ type: 'toggle',
59
+ active: hm.isHideMode(),
60
+ execute: () => {
61
+ if (hm.isHideMode()) hm.deactivateHideMode()
62
+ else hm.activateHideMode()
63
+ },
64
+ })
65
+ }
66
+ if (commentsAuth?.isAuthenticated()) {
67
+ children.push({
68
+ id: 'core/logout',
69
+ label: 'Logout (remove token)',
70
+ type: 'default',
71
+ execute: () => {
72
+ commentsAuth.clearToken()
73
+ console.log('[storyboard] Token removed')
74
+ },
75
+ })
76
+ }
77
+ return children
78
+ },
79
+ }
80
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Docs tool module — documentation sidepanel toggle.
3
+ */
4
+ export const id = 'docs'
5
+
6
+ // No component needed — sidepanel tools use generic TriggerButton
7
+
8
+ export async function handler() {
9
+ const { togglePanel } = await import('../../stores/sidePanelStore.js')
10
+ return () => togglePanel('docs')
11
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Feature Flags tool module — toggle feature flags submenu.
3
+ *
4
+ * Renders as a submenu in the command menu listing all declared flags.
5
+ */
6
+ export const id = 'feature-flags'
7
+
8
+ export async function handler() {
9
+ const ff = await import('../../featureFlags.js')
10
+
11
+ return {
12
+ getChildren: () =>
13
+ ff.getFlagKeys().map(key => ({
14
+ id: `flags/${key}`,
15
+ label: key,
16
+ type: 'toggle',
17
+ active: ff.getFlag(key),
18
+ execute: () => ff.toggleFlag(key),
19
+ })),
20
+ }
21
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Flows tool module — flow switcher action menu.
3
+ *
4
+ * Registers a command action handler that lists available flows
5
+ * for the current prototype and lets users switch between them.
6
+ */
7
+ export const id = 'flows'
8
+
9
+ export async function handler(ctx) {
10
+ const loader = await import('../../loader.js')
11
+ const vf = await import('../../viewfinder.js')
12
+ const { basePath = '/' } = ctx
13
+
14
+ return {
15
+ getChildren: () => {
16
+ let path = window.location.pathname
17
+ const base = basePath.replace(/\/+$/, '')
18
+ if (base && path.startsWith(base)) path = path.slice(base.length)
19
+ path = path.replace(/\/+$/, '') || '/'
20
+ const segments = path.split('/').filter(Boolean)
21
+
22
+ // Skip branch-- segment on deployed branch builds
23
+ const protoIdx = (segments[0] && segments[0].startsWith('branch--')) ? 1 : 0
24
+ const proto = segments[protoIdx] || null
25
+ if (!proto) return []
26
+
27
+ const params = new URLSearchParams(window.location.search)
28
+ const explicit = params.get('flow') || params.get('scene')
29
+ let active
30
+ if (explicit) {
31
+ active = loader.resolveFlowName(proto, explicit)
32
+ } else {
33
+ const pageFlow = path === '/' ? 'index' : (path.split('/').pop() || 'index')
34
+ const scoped = loader.resolveFlowName(proto, pageFlow)
35
+ if (loader.flowExists(scoped)) active = scoped
36
+ else {
37
+ const protoFlow = loader.resolveFlowName(proto, proto)
38
+ active = loader.flowExists(protoFlow) ? protoFlow : 'default'
39
+ }
40
+ }
41
+
42
+ const flows = loader.getFlowsForPrototype(proto)
43
+ if (flows.length <= 1) return []
44
+
45
+ return flows.map(f => {
46
+ const meta = vf.getFlowMeta(f.key)
47
+ return {
48
+ id: f.key,
49
+ label: meta?.title || f.name,
50
+ type: 'radio',
51
+ active: f.key === active,
52
+ execute: () => { window.location.href = vf.resolveFlowRoute(f.key) },
53
+ }
54
+ })
55
+ },
56
+ }
57
+ }
58
+
59
+ export async function component() {
60
+ const mod = await import('../../ActionMenuButton.svelte')
61
+ return mod.default
62
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Inspector tool module — component inspector sidepanel.
3
+ *
4
+ * Auto-opens the panel if ?inspect= is in the URL.
5
+ */
6
+ export const id = 'inspector'
7
+
8
+ export async function setup() {
9
+ const { openPanel } = await import('../../stores/sidePanelStore.js')
10
+
11
+ try {
12
+ const inspectParam = new URL(window.location.href).searchParams.get('inspect')
13
+ if (inspectParam) {
14
+ openPanel('inspector')
15
+ }
16
+ } catch { /* ignore */ }
17
+ }
18
+
19
+ // No component needed — sidepanel tools use generic TriggerButton
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Theme tool module — theme switcher menu.
3
+ */
4
+ export const id = 'theme'
5
+
6
+ export async function component() {
7
+ const mod = await import('../../ThemeMenuButton.svelte')
8
+ return mod.default
9
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Tool module registry — maps tool IDs to lazy-loaded handler modules.
3
+ *
4
+ * Each handler module exports: { id, component?, handler?, setup?, guard? }
5
+ * All imports are dynamic to enable code splitting.
6
+ */
7
+ export const coreHandlers = {
8
+ create: () => import('./handlers/create.js'),
9
+ theme: () => import('./handlers/theme.js'),
10
+ comments: () => import('./handlers/comments.js'),
11
+ flows: () => import('./handlers/flows.js'),
12
+ docs: () => import('./handlers/docs.js'),
13
+ inspector: () => import('./handlers/inspector.js'),
14
+ devtools: () => import('./handlers/devtools.js'),
15
+ 'feature-flags': () => import('./handlers/featureFlags.js'),
16
+ 'canvas-add-widget': () => import('./handlers/canvasAddWidget.js'),
17
+ 'canvas-zoom': () => import('./handlers/canvasZoom.js'),
18
+ }
19
+
20
+ // Keep legacy export name for backward compatibility
21
+ export const toolModules = coreHandlers
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Canvas Toolbar surface — floating bar at bottom-left on canvas pages.
3
+ *
4
+ * Only visible when a canvas page is active. Supports menus and
5
+ * custom render types like zoom-control.
6
+ */
7
+ export const id = 'canvas-toolbar'
8
+ export const label = 'Canvas Toolbar'
9
+ export const position = 'bottom-left'
10
+ export const renderTypes = ['menu', 'zoom-control']
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Command List surface — items rendered inside the ⌘K command menu.
3
+ *
4
+ * Tools here appear as menu items, submenus, or links within
5
+ * the command palette. They don't render as standalone buttons.
6
+ */
7
+ export const id = 'command-list'
8
+ export const label = 'Command Menu'
9
+ export const position = 'overlay'
10
+ export const renderTypes = ['link', 'submenu']
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Main Toolbar surface — the primary floating bar at bottom-right.
3
+ *
4
+ * Supports all standard render types. Tools appear in reverse JSON order
5
+ * (first in config = leftmost in toolbar). The command menu button is
6
+ * always the rightmost item and is not a tool.
7
+ */
8
+ export const id = 'main-toolbar'
9
+ export const label = 'Main Toolbar'
10
+ export const position = 'bottom-right'
11
+ export const renderTypes = ['button', 'menu', 'sidepanel', 'separator']
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Surface registry — exports all available rendering surfaces.
3
+ *
4
+ * Surfaces define WHERE a tool renders. Each surface has a position,
5
+ * supported render types, and rendering logic in CoreUIBar.
6
+ *
7
+ * To add a new surface:
8
+ * 1. Create a definition file in this directory
9
+ * 2. Export it from this registry
10
+ * 3. Add rendering logic in CoreUIBar.svelte
11
+ */
12
+ export { id as mainToolbar } from './mainToolbar.js'
13
+ export { id as canvasToolbar } from './canvasToolbar.js'
14
+ export { id as commandList } from './commandList.js'
15
+
16
+ /**
17
+ * All surface IDs for validation.
18
+ */
19
+ export const SURFACE_IDS = ['main-toolbar', 'canvas-toolbar', 'command-list']
@@ -64,6 +64,7 @@ export default function storyboardServer() {
64
64
  let root = ''
65
65
  let base = '/'
66
66
  let config = {}
67
+ let isDev = false
67
68
 
68
69
  // Route handler registry — plugins register here during setup
69
70
  const routeHandlers = new Map()
@@ -76,6 +77,7 @@ export default function storyboardServer() {
76
77
  root = viteConfig.root
77
78
  base = viteConfig.base || '/'
78
79
  config = readConfig(root)
80
+ isDev = viteConfig.command === 'serve'
79
81
  },
80
82
 
81
83
  configureServer(server) {
@@ -178,12 +180,14 @@ export default function storyboardServer() {
178
180
  transformIndexHtml() {
179
181
  const tags = []
180
182
 
181
- // Inject local dev flag so the UI can gate dev-only tools
182
- tags.push({
183
- tag: 'script',
184
- children: 'window.__SB_LOCAL_DEV__=true',
185
- injectTo: 'head',
186
- })
183
+ // Inject local dev flag only during dev server (not production builds)
184
+ if (isDev) {
185
+ tags.push({
186
+ tag: 'script',
187
+ children: 'window.__SB_LOCAL_DEV__=true',
188
+ injectTo: 'head',
189
+ })
190
+ }
187
191
 
188
192
  // Inject base path so the inspector UI can resolve static assets
189
193
  // (e.g. inspector.json) when deployed under a subpath
@@ -262,6 +266,32 @@ export default function storyboardServer() {
262
266
  }),
263
267
  })
264
268
 
269
+ // Emit README as static JSON so the docs panel works in deployed builds.
270
+ // Dev server serves this dynamically; production needs the static file.
271
+ let readmeContent = null
272
+ for (const candidate of ['README.md', 'readme.md', 'Readme.md']) {
273
+ try {
274
+ readmeContent = await fs.promises.readFile(path.join(root, candidate), 'utf-8')
275
+ break
276
+ } catch { /* try next */ }
277
+ }
278
+ if (readmeContent) {
279
+ this.emitFile({
280
+ type: 'asset',
281
+ fileName: '_storyboard/docs/readme',
282
+ source: JSON.stringify({ content: readmeContent, path: 'README.md' }),
283
+ })
284
+ }
285
+
286
+ // Emit repo info so the docs panel GitHub link works in deployed builds.
287
+ if (repo) {
288
+ this.emitFile({
289
+ type: 'asset',
290
+ fileName: '_storyboard/docs/repo',
291
+ source: JSON.stringify(repo),
292
+ })
293
+ }
294
+
265
295
  // GitHub Pages uses Jekyll which ignores _-prefixed directories.
266
296
  // Emit .nojekyll to ensure _storyboard/ is served.
267
297
  this.emitFile({