@dfosco/storyboard-core 2.0.0 → 2.2.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.
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Svelte store that wraps the core tool registry API.
3
+ *
4
+ * Provides a readable store whose value updates whenever:
5
+ * - The active mode changes (different tools for each mode)
6
+ * - Tool state or actions change (setToolState, setToolAction)
7
+ *
8
+ * Groups tools into { tools, devTools } for the toolbar to consume.
9
+ */
10
+
11
+ import { writable, type Readable } from 'svelte/store'
12
+ import {
13
+ getCurrentMode,
14
+ getToolsForMode,
15
+ subscribeToMode,
16
+ subscribeToTools,
17
+ } from './types.js'
18
+ import type { ResolvedTool } from './types.js'
19
+
20
+ export interface ToolStoreState {
21
+ /** Tools in the 'tools' group for the current mode */
22
+ tools: ResolvedTool[]
23
+ /** Tools in the 'dev' group for the current mode */
24
+ devTools: ResolvedTool[]
25
+ }
26
+
27
+ function snapshot(): ToolStoreState {
28
+ const mode = getCurrentMode()
29
+ const allTools = getToolsForMode(mode)
30
+ return {
31
+ tools: allTools.filter((t) => t.group === 'tools'),
32
+ devTools: allTools.filter((t) => t.group === 'dev'),
33
+ }
34
+ }
35
+
36
+ function createToolStore(): Readable<ToolStoreState> {
37
+ const { subscribe: rawSubscribe, set } = writable<ToolStoreState>(snapshot())
38
+
39
+ const subscribe: Readable<ToolStoreState>['subscribe'] = (run, invalidate) => {
40
+ set(snapshot())
41
+
42
+ // Re-snapshot on mode changes OR tool state/action changes
43
+ const unsubMode = subscribeToMode(() => set(snapshot()))
44
+ const unsubTools = subscribeToTools(() => set(snapshot()))
45
+
46
+ const unsubStore = rawSubscribe(run, invalidate)
47
+
48
+ return () => {
49
+ unsubStore()
50
+ unsubMode()
51
+ unsubTools()
52
+ }
53
+ }
54
+
55
+ return { subscribe }
56
+ }
57
+
58
+ /**
59
+ * Readable Svelte store for the tool registry.
60
+ *
61
+ * ```svelte
62
+ * <script>
63
+ * import { toolState } from '../stores/toolStore.js'
64
+ * </script>
65
+ *
66
+ * {#each $toolState.tools as tool}
67
+ * <button onclick={tool.action} disabled={!tool.state.enabled}>{tool.label}</button>
68
+ * {/each}
69
+ * ```
70
+ */
71
+ export const toolState: Readable<ToolStoreState> = createToolStore()
@@ -19,14 +19,11 @@ export {
19
19
  on,
20
20
  off,
21
21
  emit,
22
+ getToolsForMode,
23
+ subscribeToTools,
24
+ getToolsSnapshot,
22
25
  } from '../../modes.js'
23
26
 
24
- export interface ModeToolConfig {
25
- id: string
26
- label: string
27
- action: () => void
28
- }
29
-
30
27
  export interface ModeConfig {
31
28
  name: string
32
29
  label: string
@@ -34,6 +31,23 @@ export interface ModeConfig {
34
31
  className?: string | string[]
35
32
  onActivate?: (options?: Record<string, unknown>) => void
36
33
  onDeactivate?: () => void
37
- tools?: ModeToolConfig[]
38
- devTools?: ModeToolConfig[]
34
+ }
35
+
36
+ export interface ToolState {
37
+ enabled: boolean
38
+ active: boolean
39
+ busy: boolean
40
+ hidden: boolean
41
+ badge: string | number | null
42
+ }
43
+
44
+ export interface ResolvedTool {
45
+ id: string
46
+ label: string
47
+ group: 'tools' | 'dev'
48
+ icon: string | null
49
+ order: number
50
+ modes: string[]
51
+ state: ToolState
52
+ action: (() => void) | null
39
53
  }
@@ -1,19 +1,20 @@
1
1
  /**
2
- * Design-modes plugin mount entry point.
2
+ * Design-modes UI mount entry point.
3
3
  *
4
4
  * Call mountDesignModesUI() once at app startup to render the ModeSwitch
5
5
  * and ToolbarShell components. Framework-agnostic — works from any JS
6
- * context (React _app.jsx, vanilla JS, etc.).
6
+ * context (React StoryboardProvider, vanilla JS, etc.).
7
7
  *
8
8
  * Usage:
9
- * import { mountDesignModesUI } from '@dfosco/storyboard-core/svelte-plugin-ui/design-modes'
9
+ * import { mountDesignModesUI } from '@dfosco/storyboard-core/ui/design-modes'
10
10
  * mountDesignModesUI() // mounts to document.body
11
11
  * mountDesignModesUI(document.getElementById('my-container'))
12
12
  */
13
13
 
14
- import { mountSveltePlugin, type PluginHandle } from '../mount.js'
15
- import ModeSwitch from '../components/ModeSwitch.svelte'
16
- import ToolbarShell from '../components/ToolbarShell.svelte'
14
+ import { mountSveltePlugin, type PluginHandle } from '../svelte-plugin-ui/mount.js'
15
+ import ModeSwitch from '../svelte-plugin-ui/components/ModeSwitch.svelte'
16
+ // TODO: Re-enable after migrating devtools features into tool registry
17
+ // import ToolbarShell from '../svelte-plugin-ui/components/ToolbarShell.svelte'
17
18
 
18
19
  let handles: PluginHandle[] = []
19
20
 
@@ -35,7 +36,8 @@ export function mountDesignModesUI(
35
36
 
36
37
  handles.push(
37
38
  mountSveltePlugin(target, ModeSwitch),
38
- mountSveltePlugin(target, ToolbarShell),
39
+ // TODO: Re-enable after migrating devtools features into tool registry
40
+ // mountSveltePlugin(target, ToolbarShell),
39
41
  )
40
42
 
41
43
  return unmountDesignModesUI
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Viewfinder UI mount entry point.
3
+ *
4
+ * Call mountViewfinder() to render the prototype index dashboard.
5
+ * Framework-agnostic — works from any JS context (React wrapper,
6
+ * vanilla JS, etc.).
7
+ *
8
+ * Usage:
9
+ * import { mountViewfinder } from '@dfosco/storyboard-core/ui/viewfinder'
10
+ * const handle = mountViewfinder(document.getElementById('root'), {
11
+ * title: 'My Storyboard',
12
+ * knownRoutes: ['Dashboard', 'Settings'],
13
+ * })
14
+ * // later: handle.destroy()
15
+ */
16
+
17
+ import { mountSveltePlugin, type PluginHandle } from '../svelte-plugin-ui/mount.js'
18
+ import Viewfinder from '../svelte-plugin-ui/components/Viewfinder.svelte'
19
+
20
+ export interface ViewfinderProps {
21
+ title?: string
22
+ subtitle?: string
23
+ basePath?: string
24
+ knownRoutes?: string[]
25
+ showThumbnails?: boolean
26
+ hideDefaultFlow?: boolean
27
+ }
28
+
29
+ let handle: PluginHandle | null = null
30
+
31
+ /**
32
+ * Mount the Viewfinder dashboard into a DOM container.
33
+ *
34
+ * Idempotent — calling multiple times is a no-op.
35
+ * Returns a handle with destroy() method.
36
+ *
37
+ * @param container - DOM element to mount into
38
+ * @param props - Viewfinder configuration
39
+ */
40
+ export function mountViewfinder(
41
+ container: HTMLElement,
42
+ props?: ViewfinderProps,
43
+ ): PluginHandle {
44
+ if (handle) return handle
45
+
46
+ handle = mountSveltePlugin(container, Viewfinder as any, props as any)
47
+ return handle
48
+ }
49
+
50
+ /**
51
+ * Remove the Viewfinder from the DOM.
52
+ */
53
+ export function unmountViewfinder(): void {
54
+ if (handle) {
55
+ handle.destroy()
56
+ handle = null
57
+ }
58
+ }
package/src/viewfinder.js CHANGED
@@ -1,4 +1,4 @@
1
- import { loadScene } from './loader.js'
1
+ import { loadFlow, listFlows, listPrototypes, getPrototypeMetadata } from './loader.js'
2
2
 
3
3
  /**
4
4
  * Deterministic hash from a string — used for seeding generative placeholders.
@@ -14,52 +14,132 @@ export function hash(str) {
14
14
  }
15
15
 
16
16
  /**
17
- * Resolve the target route path for a scene.
17
+ * Resolve the target route path for a flow.
18
18
  *
19
- * 1. If scene name matches a known route (case-insensitive), use that route
20
- * 2. If scene data has a `sceneMeta.route` or `route` key, use that
19
+ * 1. If flow name matches a known route (case-insensitive), use that route
20
+ * 2. If flow data has a top-level `route`, or `meta.route` / `sceneMeta.route`, use that
21
21
  * 3. Fall back to root "/"
22
22
  *
23
- * @param {string} sceneName
23
+ * @param {string} flowName
24
24
  * @param {string[]} knownRoutes - Array of route names (e.g. ["Dashboard", "Repositories"])
25
- * @returns {string} Full path with ?scene= param
25
+ * @returns {string} Full path with ?flow= param
26
26
  */
27
- export function resolveSceneRoute(sceneName, knownRoutes = []) {
27
+ export function resolveFlowRoute(flowName, knownRoutes = []) {
28
28
  // Case-insensitive match against known routes
29
29
  for (const route of knownRoutes) {
30
- if (route.toLowerCase() === sceneName.toLowerCase()) {
31
- // Scene name matches the route — no ?scene= needed,
30
+ if (route.toLowerCase() === flowName.toLowerCase()) {
31
+ // Flow name matches the route — no ?flow= needed,
32
32
  // StoryboardProvider auto-matches by page name
33
33
  return `/${route}`
34
34
  }
35
35
  }
36
36
 
37
- // Check for explicit route in sceneMeta or top-level route key
37
+ // Check for explicit route: top-level `route`, then meta.route, then legacy sceneMeta.route
38
38
  try {
39
- const data = loadScene(sceneName)
40
- const route = data?.sceneMeta?.route || data?.route
39
+ const data = loadFlow(flowName)
40
+ const route = data?.route || data?.meta?.route || data?.flowMeta?.route || data?.sceneMeta?.route
41
41
  if (route) {
42
42
  const normalized = route.startsWith('/') ? route : `/${route}`
43
- return `${normalized}?scene=${encodeURIComponent(sceneName)}`
43
+ return `${normalized}?flow=${encodeURIComponent(flowName)}`
44
44
  }
45
45
  } catch {
46
46
  // ignore load errors
47
47
  }
48
48
 
49
- return `/?scene=${encodeURIComponent(sceneName)}`
49
+ return `/?flow=${encodeURIComponent(flowName)}`
50
50
  }
51
51
 
52
+ /** @deprecated Use resolveFlowRoute() */
53
+ export const resolveSceneRoute = resolveFlowRoute
54
+
52
55
  /**
53
- * Get sceneMeta for a scene (route, author, etc).
56
+ * Get meta for a flow (title, description, author, etc).
54
57
  *
55
- * @param {string} sceneName
56
- * @returns {{ route?: string, author?: string | string[] } | null}
58
+ * @param {string} flowName
59
+ * @returns {{ title?: string, description?: string, author?: string | string[] } | null}
57
60
  */
58
- export function getSceneMeta(sceneName) {
61
+ export function getFlowMeta(flowName) {
59
62
  try {
60
- const data = loadScene(sceneName)
61
- return data?.sceneMeta || null
63
+ const data = loadFlow(flowName)
64
+ return data?.meta || data?.flowMeta || data?.sceneMeta || null
62
65
  } catch {
63
66
  return null
64
67
  }
65
68
  }
69
+
70
+ /** @deprecated Use getFlowMeta() */
71
+ export const getSceneMeta = getFlowMeta
72
+
73
+ /**
74
+ * Build a structured prototype index grouping flows by prototype.
75
+ *
76
+ * Returns an object with:
77
+ * - prototypes: array of prototype entries with metadata and their flows
78
+ * - globalFlows: flows not belonging to any prototype
79
+ *
80
+ * @param {string[]} [knownRoutes] - Array of known route names
81
+ * @returns {{ prototypes: Array, globalFlows: Array }}
82
+ */
83
+ export function buildPrototypeIndex(knownRoutes = []) {
84
+ const flows = listFlows()
85
+ const protoMap = {}
86
+ const globalFlows = []
87
+
88
+ // Seed from .prototype.json metadata (even prototypes with no flows appear)
89
+ for (const name of listPrototypes()) {
90
+ const raw = getPrototypeMetadata(name)
91
+ const meta = raw?.meta || raw || {}
92
+ protoMap[name] = {
93
+ name: meta.title || name,
94
+ dirName: name,
95
+ description: meta.description || null,
96
+ author: meta.author || null,
97
+ gitAuthor: raw?.gitAuthor || null,
98
+ icon: meta.icon || null,
99
+ team: meta.team || null,
100
+ tags: meta.tags || null,
101
+ flows: [],
102
+ }
103
+ }
104
+
105
+ for (const flowName of flows) {
106
+ const slashIdx = flowName.indexOf('/')
107
+ if (slashIdx > 0) {
108
+ const protoName = flowName.substring(0, slashIdx)
109
+ const shortName = flowName.substring(slashIdx + 1)
110
+
111
+ if (!protoMap[protoName]) {
112
+ protoMap[protoName] = {
113
+ name: protoName,
114
+ dirName: protoName,
115
+ description: null,
116
+ author: null,
117
+ gitAuthor: null,
118
+ icon: null,
119
+ team: null,
120
+ tags: null,
121
+ flows: [],
122
+ }
123
+ }
124
+
125
+ protoMap[protoName].flows.push({
126
+ key: flowName,
127
+ name: shortName,
128
+ route: resolveFlowRoute(flowName, knownRoutes),
129
+ meta: getFlowMeta(flowName),
130
+ })
131
+ } else {
132
+ globalFlows.push({
133
+ key: flowName,
134
+ name: flowName,
135
+ route: resolveFlowRoute(flowName, knownRoutes),
136
+ meta: getFlowMeta(flowName),
137
+ })
138
+ }
139
+ }
140
+
141
+ return {
142
+ prototypes: Object.values(protoMap),
143
+ globalFlows,
144
+ }
145
+ }
@@ -1,17 +1,17 @@
1
1
  import { init } from './loader.js'
2
- import { hash, resolveSceneRoute, getSceneMeta } from './viewfinder.js'
2
+ import { hash, resolveFlowRoute, getFlowMeta, resolveSceneRoute, getSceneMeta } from './viewfinder.js'
3
3
 
4
4
  const makeIndex = () => ({
5
- scenes: {
5
+ flows: {
6
6
  default: { title: 'Default Scene' },
7
7
  Dashboard: { heading: 'Dashboard' },
8
8
  'custom-route': { route: 'Overview', title: 'Custom' },
9
9
  'absolute-route': { route: '/Forms', title: 'Absolute' },
10
10
  'no-route': { title: 'No route key' },
11
- 'meta-route': { sceneMeta: { route: 'Repositories' }, title: 'Meta Route' },
12
- 'meta-author': { sceneMeta: { author: 'dfosco' }, title: 'With Author' },
13
- 'meta-authors': { sceneMeta: { author: ['dfosco', 'heyamie', 'branonconor'] }, title: 'Multi Author' },
14
- 'meta-both': { sceneMeta: { route: '/Overview', author: 'octocat' }, title: 'Both' },
11
+ 'meta-route': { flowMeta: { route: 'Repositories' }, title: 'Meta Route' },
12
+ 'meta-author': { flowMeta: { author: 'dfosco' }, title: 'With Author' },
13
+ 'meta-authors': { flowMeta: { author: ['dfosco', 'heyamie', 'branonconor'] }, title: 'Multi Author' },
14
+ 'meta-both': { flowMeta: { route: '/Overview', author: 'octocat' }, title: 'Both' },
15
15
  },
16
16
  objects: {},
17
17
  records: {},
@@ -41,90 +41,112 @@ describe('hash', () => {
41
41
  })
42
42
  })
43
43
 
44
- describe('resolveSceneRoute', () => {
44
+ describe('resolveFlowRoute', () => {
45
45
  const routes = ['Dashboard', 'Overview', 'Forms', 'Repositories']
46
46
 
47
- it('matches scene name to route (exact case)', () => {
48
- expect(resolveSceneRoute('Dashboard', routes)).toBe('/Dashboard')
47
+ it('matches flow name to route (exact case)', () => {
48
+ expect(resolveFlowRoute('Dashboard', routes)).toBe('/Dashboard')
49
49
  })
50
50
 
51
- it('matches scene name to route (case-insensitive)', () => {
52
- expect(resolveSceneRoute('dashboard', routes)).toBe('/Dashboard')
51
+ it('matches flow name to route (case-insensitive)', () => {
52
+ expect(resolveFlowRoute('dashboard', routes)).toBe('/Dashboard')
53
53
  })
54
54
 
55
- it('uses route key from scene data when no route matches', () => {
56
- expect(resolveSceneRoute('custom-route', routes)).toBe('/Overview?scene=custom-route')
55
+ it('uses route key from flow data when no route matches', () => {
56
+ expect(resolveFlowRoute('custom-route', routes)).toBe('/Overview?flow=custom-route')
57
57
  })
58
58
 
59
59
  it('handles absolute route key (with leading slash)', () => {
60
- expect(resolveSceneRoute('absolute-route', routes)).toBe('/Forms?scene=absolute-route')
60
+ expect(resolveFlowRoute('absolute-route', routes)).toBe('/Forms?flow=absolute-route')
61
61
  })
62
62
 
63
63
  it('falls back to root when no match and no route key', () => {
64
- expect(resolveSceneRoute('no-route', routes)).toBe('/?scene=no-route')
64
+ expect(resolveFlowRoute('no-route', routes)).toBe('/?flow=no-route')
65
65
  })
66
66
 
67
- it('falls back to root for default scene', () => {
68
- expect(resolveSceneRoute('default', routes)).toBe('/?scene=default')
67
+ it('falls back to root for default flow', () => {
68
+ expect(resolveFlowRoute('default', routes)).toBe('/?flow=default')
69
69
  })
70
70
 
71
- it('falls back to root when scene does not exist', () => {
72
- expect(resolveSceneRoute('nonexistent', routes)).toBe('/?scene=nonexistent')
71
+ it('falls back to root when flow does not exist', () => {
72
+ expect(resolveFlowRoute('nonexistent', routes)).toBe('/?flow=nonexistent')
73
73
  })
74
74
 
75
75
  it('works with empty routes array', () => {
76
- expect(resolveSceneRoute('Dashboard', [])).toBe('/?scene=Dashboard')
76
+ expect(resolveFlowRoute('Dashboard', [])).toBe('/?flow=Dashboard')
77
77
  })
78
78
 
79
79
  it('works with no routes argument', () => {
80
- expect(resolveSceneRoute('custom-route')).toBe('/Overview?scene=custom-route')
80
+ expect(resolveFlowRoute('custom-route')).toBe('/Overview?flow=custom-route')
81
81
  })
82
82
 
83
- it('encodes special characters in scene name', () => {
83
+ it('encodes special characters in flow name', () => {
84
84
  init({
85
- scenes: { 'has spaces': { title: 'Spaces' } },
85
+ flows: { 'has spaces': { title: 'Spaces' } },
86
86
  objects: {},
87
87
  records: {},
88
88
  })
89
- expect(resolveSceneRoute('has spaces', [])).toBe('/?scene=has%20spaces')
89
+ expect(resolveFlowRoute('has spaces', [])).toBe('/?flow=has%20spaces')
90
90
  })
91
91
 
92
- it('uses sceneMeta.route when no route matches', () => {
93
- expect(resolveSceneRoute('meta-route', routes)).toBe('/Repositories?scene=meta-route')
92
+ it('uses flowMeta.route when no route matches', () => {
93
+ expect(resolveFlowRoute('meta-route', routes)).toBe('/Repositories?flow=meta-route')
94
94
  })
95
95
 
96
- it('uses sceneMeta.route with absolute path', () => {
97
- expect(resolveSceneRoute('meta-both', routes)).toBe('/Overview?scene=meta-both')
96
+ it('uses flowMeta.route with absolute path', () => {
97
+ expect(resolveFlowRoute('meta-both', routes)).toBe('/Overview?flow=meta-both')
98
98
  })
99
99
 
100
- it('prefers sceneMeta.route over top-level route key', () => {
100
+ it('prefers top-level route over flowMeta.route', () => {
101
101
  init({
102
- scenes: { conflict: { route: 'Forms', sceneMeta: { route: 'Dashboard' } } },
102
+ flows: { conflict: { route: 'Forms', flowMeta: { route: 'Dashboard' } } },
103
103
  objects: {},
104
104
  records: {},
105
105
  })
106
- expect(resolveSceneRoute('conflict', [])).toBe('/Dashboard?scene=conflict')
106
+ expect(resolveFlowRoute('conflict', [])).toBe('/Forms?flow=conflict')
107
107
  })
108
108
  })
109
109
 
110
- describe('getSceneMeta', () => {
111
- it('returns sceneMeta when present', () => {
112
- expect(getSceneMeta('meta-author')).toEqual({ author: 'dfosco' })
110
+ describe('getFlowMeta', () => {
111
+ it('returns flowMeta when present', () => {
112
+ expect(getFlowMeta('meta-author')).toEqual({ author: 'dfosco' })
113
+ })
114
+
115
+ it('returns flowMeta with both fields', () => {
116
+ expect(getFlowMeta('meta-both')).toEqual({ route: '/Overview', author: 'octocat' })
113
117
  })
114
118
 
115
- it('returns sceneMeta with both fields', () => {
116
- expect(getSceneMeta('meta-both')).toEqual({ route: '/Overview', author: 'octocat' })
119
+ it('returns flowMeta with array author', () => {
120
+ expect(getFlowMeta('meta-authors')).toEqual({ author: ['dfosco', 'heyamie', 'branonconor'] })
117
121
  })
118
122
 
119
- it('returns sceneMeta with array author', () => {
120
- expect(getSceneMeta('meta-authors')).toEqual({ author: ['dfosco', 'heyamie', 'branonconor'] })
123
+ it('returns null when no flowMeta', () => {
124
+ expect(getFlowMeta('default')).toBeNull()
121
125
  })
122
126
 
123
- it('returns null when no sceneMeta', () => {
124
- expect(getSceneMeta('default')).toBeNull()
127
+ it('returns null for nonexistent flow', () => {
128
+ expect(getFlowMeta('nonexistent')).toBeNull()
129
+ })
130
+ })
131
+
132
+ // ── Deprecated aliases ──
133
+
134
+ describe('resolveSceneRoute (deprecated alias)', () => {
135
+ it('is the same function as resolveFlowRoute', () => {
136
+ expect(resolveSceneRoute).toBe(resolveFlowRoute)
125
137
  })
126
138
 
127
- it('returns null for nonexistent scene', () => {
128
- expect(getSceneMeta('nonexistent')).toBeNull()
139
+ it('resolves a flow route', () => {
140
+ expect(resolveSceneRoute('Dashboard', ['Dashboard'])).toBe('/Dashboard')
141
+ })
142
+ })
143
+
144
+ describe('getSceneMeta (deprecated alias)', () => {
145
+ it('is the same function as getFlowMeta', () => {
146
+ expect(getSceneMeta).toBe(getFlowMeta)
147
+ })
148
+
149
+ it('returns flow meta', () => {
150
+ expect(getSceneMeta('meta-author')).toEqual({ author: 'dfosco' })
129
151
  })
130
152
  })
@@ -9,7 +9,7 @@
9
9
  import fs from 'node:fs'
10
10
  import path from 'node:path'
11
11
 
12
- const SCENE_SKELETON = JSON.stringify({ $global: [] }, null, 2) + '\n'
12
+ const FLOW_SKELETON = JSON.stringify({ $global: [] }, null, 2) + '\n'
13
13
 
14
14
  /**
15
15
  * Convert a raw name to PascalCase for use as component name + filename.
@@ -66,7 +66,7 @@ function renderTemplate(templatesDir, templateName, pageName) {
66
66
  * List all existing page files in src/pages/.
67
67
  */
68
68
  function listPages(root) {
69
- const pagesDir = path.join(root, 'src', 'pages')
69
+ const pagesDir = path.join(root, 'src', 'prototypes')
70
70
  if (!fs.existsSync(pagesDir)) return []
71
71
 
72
72
  return fs.readdirSync(pagesDir)
@@ -103,7 +103,7 @@ export function createPagesHandler(ctx, templatesDir) {
103
103
  }
104
104
 
105
105
  const { pascalName } = validation
106
- const pagesDir = path.join(root, 'src', 'pages')
106
+ const pagesDir = path.join(root, 'src', 'prototypes')
107
107
  const pagePath = path.join(pagesDir, `${pascalName}.jsx`)
108
108
 
109
109
  if (fs.existsSync(pagePath)) {
@@ -124,18 +124,18 @@ export function createPagesHandler(ctx, templatesDir) {
124
124
 
125
125
  const result = {
126
126
  success: true,
127
- path: `src/pages/${pascalName}.jsx`,
127
+ path: `src/prototypes/${pascalName}.jsx`,
128
128
  route: `/${pascalName}`,
129
129
  }
130
130
 
131
131
  if (createScene) {
132
132
  const dataDir = path.join(root, 'src', 'data')
133
- const scenePath = path.join(dataDir, `${pascalName}.scene.json`)
133
+ const flowPath = path.join(dataDir, `${pascalName}.flow.json`)
134
134
 
135
- if (!fs.existsSync(scenePath)) {
135
+ if (!fs.existsSync(flowPath)) {
136
136
  fs.mkdirSync(dataDir, { recursive: true })
137
- fs.writeFileSync(scenePath, SCENE_SKELETON, 'utf-8')
138
- result.scenePath = `src/data/${pascalName}.scene.json`
137
+ fs.writeFileSync(flowPath, FLOW_SKELETON, 'utf-8')
138
+ result.flowPath = `src/data/${pascalName}.flow.json`
139
139
  }
140
140
  }
141
141