@octane-xplat/workshop 0.0.0-nightly.g2e22171
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/LICENSE +21 -0
- package/MOBILE.md +143 -0
- package/README.md +307 -0
- package/dist/node/index.js +50 -0
- package/package.json +127 -0
- package/src/catalog.ts +58 -0
- package/src/contract.ts +257 -0
- package/src/controls.ts +159 -0
- package/src/discover.mjs +584 -0
- package/src/errors.ts +47 -0
- package/src/fixture.ts +158 -0
- package/src/index.ts +6 -0
- package/src/macos/controller.ts +170 -0
- package/src/macos/index.macos.ts +2 -0
- package/src/macos/shell.macos.mjs +673 -0
- package/src/macos/stage.macos.ts +182 -0
- package/src/macos-node.mjs +95 -0
- package/src/macos-vite.mjs +47 -0
- package/src/mobile/controller.ts +134 -0
- package/src/mobile/entry.mobile.ts +607 -0
- package/src/mobile/index.mobile.ts +3 -0
- package/src/mobile/mount.mobile.ts +60 -0
- package/src/mobile/tree.mobile.tsrx +17 -0
- package/src/mobile-node.mjs +126 -0
- package/src/mobile-regex.mjs +60 -0
- package/src/node.d.ts +65 -0
- package/src/node.mjs +536 -0
- package/src/session.ts +520 -0
- package/src/vite-mobile.d.ts +21 -0
- package/src/vite-mobile.mjs +101 -0
- package/src/vite.d.ts +11 -0
- package/src/vite.mjs +315 -0
- package/src/web/entry.web.ts +270 -0
- package/src/web/index.web.ts +7 -0
- package/src/web/mount.web.ts +86 -0
- package/src/web/shell.css +449 -0
- package/src/web/shell.web.tsrx +759 -0
- package/src/windows/preflight.mjs +113 -0
- package/types/generated/.tsrx-typegen-manifest.json +24 -0
- package/types/generated/catalog.d.ts +30 -0
- package/types/generated/contract.d.ts +145 -0
- package/types/generated/controls.d.ts +28 -0
- package/types/generated/errors.d.ts +13 -0
- package/types/generated/fixture.d.ts +53 -0
- package/types/generated/index.d.ts +6 -0
- package/types/generated/macos/controller.d.ts +52 -0
- package/types/generated/macos/index.macos.d.ts +2 -0
- package/types/generated/macos/stage.macos.d.ts +10 -0
- package/types/generated/mobile/controller.d.ts +31 -0
- package/types/generated/mobile/entry.mobile.d.ts +22 -0
- package/types/generated/mobile/index.mobile.d.ts +3 -0
- package/types/generated/mobile/mount.mobile.d.ts +4 -0
- package/types/generated/mobile/tree.mobile.d.ts +4 -0
- package/types/generated/session.d.ts +84 -0
- package/types/generated/web/entry.web.d.ts +53 -0
- package/types/generated/web/index.web.d.ts +7 -0
- package/types/generated/web/mount.web.d.ts +2 -0
- package/types/generated/web/shell.web.d.ts +4 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineUniversalComponent,
|
|
3
|
+
universalChildren,
|
|
4
|
+
universalComponent,
|
|
5
|
+
universalTry,
|
|
6
|
+
} from 'octane/universal/native'
|
|
7
|
+
|
|
8
|
+
import { createMacOSRoot } from '@octane-xplat/macos-renderer'
|
|
9
|
+
import type { StageHost, MountRequest, MountedStory } from '../session'
|
|
10
|
+
|
|
11
|
+
const RENDERER = 'macos'
|
|
12
|
+
const UNIVERSAL_COMPONENT = Symbol.for('octane.universal.component')
|
|
13
|
+
|
|
14
|
+
/** Compiler output is already defined; plain functions need the stamp before
|
|
15
|
+
* the root will mount them (same rule hmrUniversalComponent applies). */
|
|
16
|
+
const asComponent = (component: any) =>
|
|
17
|
+
component?.[UNIVERSAL_COMPONENT]
|
|
18
|
+
? component
|
|
19
|
+
: defineUniversalComponent(RENDERER, component, {
|
|
20
|
+
module: '@octane-xplat/workshop/macos-stage',
|
|
21
|
+
} as any)
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Provider/story composition in explicit universal-runtime calls — the same
|
|
25
|
+
* ABI the TSRX compiler emits, but dynamic components stay data. Compiled
|
|
26
|
+
* components are invoked by the root, never called as plain functions.
|
|
27
|
+
*/
|
|
28
|
+
function storyTree(request: MountRequest, args: Record<string, unknown>) {
|
|
29
|
+
const component = asComponent(request.component)
|
|
30
|
+
let tree: unknown = universalComponent(RENDERER, component, args as any)
|
|
31
|
+
for (const provider of [...request.providers].reverse() as any[]) {
|
|
32
|
+
const inner = tree
|
|
33
|
+
tree = universalComponent(RENDERER, asComponent(provider), {
|
|
34
|
+
fixture: request.fixture,
|
|
35
|
+
children: universalChildren(RENDERER, () => inner as any),
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// The catch body runs inside the render pass — octane rejects store
|
|
40
|
+
// updates there, so report after the pass commits (same rule as web).
|
|
41
|
+
return universalTry(
|
|
42
|
+
() => tree as any,
|
|
43
|
+
null,
|
|
44
|
+
(error) => {
|
|
45
|
+
queueMicrotask(() => request.onRenderError(error))
|
|
46
|
+
return null
|
|
47
|
+
},
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Universal roots mount compiler-defined components only — the stage root
|
|
52
|
+
* is a static wrapper; story/props arrive through its props. */
|
|
53
|
+
const StoryTreeRoot = defineUniversalComponent(
|
|
54
|
+
RENDERER,
|
|
55
|
+
(p: { request: MountRequest; args: Record<string, unknown> }) => storyTree(p.request, p.args),
|
|
56
|
+
{ module: '@octane-xplat/workshop/macos-stage' } as any,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
export interface MacOSStageOptions {
|
|
60
|
+
/** An isolated native stage view; shell layout may move/resize it without remounting. */
|
|
61
|
+
view: any
|
|
62
|
+
/** The shell's window — story-owned windows and sheets anchor to it. */
|
|
63
|
+
parentWindow: any
|
|
64
|
+
fontFamily?: string
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Session-owned AppKit root. Full bags remove omitted props, including Unset. */
|
|
68
|
+
export function createMacOSStageHost(options: MacOSStageOptions): StageHost {
|
|
69
|
+
return {
|
|
70
|
+
mount(request: MountRequest): MountedStory {
|
|
71
|
+
const root = createMacOSRoot(options.view, { fontFamily: options.fontFamily })
|
|
72
|
+
let disposed = false
|
|
73
|
+
const bridge = globalThis as any
|
|
74
|
+
const previousOpen = bridge.__xplatAppKitOpenWindow
|
|
75
|
+
const previousSheet = bridge.__xplatAppKitMountSheet
|
|
76
|
+
const windows = new Set<any>()
|
|
77
|
+
const open = (settings: any = {}) => {
|
|
78
|
+
if (disposed || request.signal.aborted) {
|
|
79
|
+
throw new Error('Story generation has retired')
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (typeof previousOpen !== 'function') {
|
|
83
|
+
throw new Error('Consumer AppKit window service is unavailable')
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const win = previousOpen({
|
|
87
|
+
...settings,
|
|
88
|
+
parent: settings.parent ?? options.parentWindow,
|
|
89
|
+
...(settings.component
|
|
90
|
+
? {
|
|
91
|
+
component: StoryTreeRoot,
|
|
92
|
+
props: {
|
|
93
|
+
request: { ...request, component: settings.component },
|
|
94
|
+
args: { ...request.props, ...settings.props },
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
: {}),
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
if (!win || typeof win.close !== 'function') {
|
|
101
|
+
throw new Error(
|
|
102
|
+
'Consumer AppKit window service must return an owned closeable controller',
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
windows.add(win)
|
|
107
|
+
return win
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const sheet = (component: any, props: any, settings: any) =>
|
|
111
|
+
open({ ...settings, kind: 'dialog', component, props })
|
|
112
|
+
|
|
113
|
+
bridge.__xplatAppKitOpenWindow = open
|
|
114
|
+
bridge.__xplatAppKitMountSheet = sheet
|
|
115
|
+
const unmount = () => {
|
|
116
|
+
if (disposed) {
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
disposed = true
|
|
121
|
+
if (bridge.__xplatAppKitOpenWindow === open) {
|
|
122
|
+
bridge.__xplatAppKitOpenWindow = previousOpen
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (bridge.__xplatAppKitMountSheet === sheet) {
|
|
126
|
+
bridge.__xplatAppKitMountSheet = previousSheet
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const failures: unknown[] = []
|
|
130
|
+
for (const win of windows) {
|
|
131
|
+
try {
|
|
132
|
+
win.close()
|
|
133
|
+
} catch (error) {
|
|
134
|
+
failures.push(error)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
windows.clear()
|
|
139
|
+
try {
|
|
140
|
+
root.unmount()
|
|
141
|
+
} catch (error) {
|
|
142
|
+
failures.push(error)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (failures.length) {
|
|
146
|
+
throw new AggregateError(failures, 'AppKit story disposal failed')
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const render = (args: Record<string, unknown>) =>
|
|
151
|
+
root.render(
|
|
152
|
+
StoryTreeRoot as any,
|
|
153
|
+
{
|
|
154
|
+
request,
|
|
155
|
+
args,
|
|
156
|
+
parentWindow: options.parentWindow,
|
|
157
|
+
} as any,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
render(request.props)
|
|
162
|
+
} catch (error: any) {
|
|
163
|
+
try {
|
|
164
|
+
unmount()
|
|
165
|
+
} catch (cleanup) {
|
|
166
|
+
throw new AggregateError([error, cleanup], 'AppKit mount and rollback failed')
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
throw error
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
update(args) {
|
|
174
|
+
if (!disposed) {
|
|
175
|
+
render({ ...args })
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
unmount,
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { createRequire } from 'node:module'
|
|
2
|
+
import { mkdir, mkdtemp, writeFile, rm } from 'node:fs/promises'
|
|
3
|
+
import { join, relative, dirname } from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
|
|
6
|
+
/** Generate a separate consumer-owned cache profile; leave app entries untouched. */
|
|
7
|
+
export async function workshopMacOSDev({ root, config, story }) {
|
|
8
|
+
if (process.platform !== 'darwin' || process.arch !== 'arm64') {
|
|
9
|
+
throw new Error('The AppKit workshop requires a macOS arm64 execution host')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const require = createRequire(join(root, 'package.json'))
|
|
13
|
+
for (const name of [
|
|
14
|
+
'@octane-xplat/macos-renderer',
|
|
15
|
+
'@nativescript/macos-node-api',
|
|
16
|
+
'octane',
|
|
17
|
+
'@octanejs/vite-plugin',
|
|
18
|
+
'vite',
|
|
19
|
+
]) {
|
|
20
|
+
try {
|
|
21
|
+
require.resolve(name)
|
|
22
|
+
} catch {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`AppKit workshop prerequisite missing: ${name}; use the matching versions from your app's macOS setup`,
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const cache = join(root, 'node_modules/.cache/xplat/workshop-macos')
|
|
30
|
+
await mkdir(cache, { recursive: true })
|
|
31
|
+
const directory = await mkdtemp(join(cache, 'session-'))
|
|
32
|
+
const source = dirname(fileURLToPath(import.meta.url))
|
|
33
|
+
const catalogPlugin = join(source, 'macos-vite.mjs')
|
|
34
|
+
const shell = join(source, 'macos/shell.macos.mjs')
|
|
35
|
+
const bundle = join(directory, 'stories.cjs')
|
|
36
|
+
const shellBundle = join(directory, 'shell.cjs')
|
|
37
|
+
const catalogFile = join(directory, 'catalog.gen.mjs')
|
|
38
|
+
const common = `import { xplatMacOS } from '@octane-xplat/cli/macos/vite'
|
|
39
|
+
import { macOSCatalogPlugin } from ${JSON.stringify(catalogPlugin)}
|
|
40
|
+
const root = ${JSON.stringify(root)}
|
|
41
|
+
`
|
|
42
|
+
|
|
43
|
+
await writeFile(
|
|
44
|
+
join(directory, 'stories.config.mjs'),
|
|
45
|
+
common +
|
|
46
|
+
`
|
|
47
|
+
export default async ({ mode }) => {
|
|
48
|
+
const settings = await xplatMacOS(mode, { root, packaged: false, hmr: false })
|
|
49
|
+
return { ...settings, plugins: [macOSCatalogPlugin({ root, config: ${JSON.stringify(config)}, catalogFile: ${JSON.stringify(catalogFile)} }), ...settings.plugins],
|
|
50
|
+
build: { ...settings.build, outDir: ${JSON.stringify(directory)}, emptyOutDir: false,
|
|
51
|
+
lib: { entry: ${JSON.stringify(catalogFile)}, formats: ['cjs'], fileName: () => 'stories.cjs' } } }
|
|
52
|
+
}
|
|
53
|
+
`,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
await writeFile(
|
|
57
|
+
join(directory, 'entry.mjs'),
|
|
58
|
+
`import { startMacOSWorkshop } from ${JSON.stringify(shell)}
|
|
59
|
+
startMacOSWorkshop({ bundleFile: ${JSON.stringify(bundle)}, initialStoryId: ${JSON.stringify(story ?? null)},
|
|
60
|
+
offscreen: process.env.OCTANE_WORKSHOP_OFFSCREEN === '1' })
|
|
61
|
+
`,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
await writeFile(
|
|
65
|
+
join(directory, 'shell.config.mjs'),
|
|
66
|
+
common +
|
|
67
|
+
`
|
|
68
|
+
export default async ({ mode }) => {
|
|
69
|
+
const settings = await xplatMacOS(mode, { root, packaged: true, entry: ${JSON.stringify(join(directory, 'entry.mjs'))} })
|
|
70
|
+
return { ...settings, build: { ...settings.build, outDir: ${JSON.stringify(directory)}, emptyOutDir: false,
|
|
71
|
+
lib: { entry: ${JSON.stringify(join(directory, 'entry.mjs'))}, formats: ['cjs'], fileName: () => 'shell.cjs' } } }
|
|
72
|
+
}
|
|
73
|
+
`,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const { runMacOSDev } = await import('@octane-xplat/cli/macos')
|
|
78
|
+
return await runMacOSDev(root, {
|
|
79
|
+
workshop: true,
|
|
80
|
+
target: {
|
|
81
|
+
runtime: 'appkit-node-api',
|
|
82
|
+
renderer: 'appkit',
|
|
83
|
+
dev: {
|
|
84
|
+
productName: 'Octane component workshop',
|
|
85
|
+
viteConfig: relative(root, join(directory, 'stories.config.mjs')),
|
|
86
|
+
bundleFile: relative(root, bundle),
|
|
87
|
+
shellViteConfig: relative(root, join(directory, 'shell.config.mjs')),
|
|
88
|
+
shellBundleFile: relative(root, shellBundle),
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
})
|
|
92
|
+
} finally {
|
|
93
|
+
await rm(directory, { recursive: true, force: true })
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
import { discoverStories } from './discover.mjs'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Regenerates the real catalog module on every (re)build. Static applicability
|
|
7
|
+
* is resolved before any platform leaf enters the bundle — incompatible leaves
|
|
8
|
+
* keep a catalog entry but never a `load` import.
|
|
9
|
+
*/
|
|
10
|
+
export function macOSCatalogPlugin({ root, config = {}, catalogFile }) {
|
|
11
|
+
const generate = (ctx) => {
|
|
12
|
+
const { entries, diagnostics } = discoverStories(root, { ...config.stories, target: 'macos' })
|
|
13
|
+
for (const entry of entries) {
|
|
14
|
+
ctx?.addWatchFile?.(entry.file)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const providers = (config.providerModules ?? []).map(
|
|
18
|
+
(file, i) => `import * as preset${i} from ${JSON.stringify(resolve(root, file))}`,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
const catalog = entries.map(
|
|
22
|
+
(entry) => `{
|
|
23
|
+
...${JSON.stringify(entry)},
|
|
24
|
+
${entry.applicable ? `load: () => import(${JSON.stringify(entry.file)})` : ''}
|
|
25
|
+
}`,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
const next = `${providers.join('\n')}
|
|
29
|
+
export const catalog = [${catalog.join(',\n')}]
|
|
30
|
+
export const diagnostics = ${JSON.stringify(diagnostics)}
|
|
31
|
+
export const providers = [${(config.providerModules ?? []).map((_, i) => `...(preset${i}.providers ?? preset${i}.default ?? [])`).join(',')}]
|
|
32
|
+
`
|
|
33
|
+
|
|
34
|
+
// Same-content regeneration must not retrigger the watcher.
|
|
35
|
+
if (!existsSync(catalogFile) || readFileSync(catalogFile, 'utf8') !== next) {
|
|
36
|
+
writeFileSync(catalogFile, next)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
name: 'xplat-workshop-macos-catalog',
|
|
42
|
+
buildStart() {
|
|
43
|
+
// Runs on every rebuild; story modules stay lazily imported.
|
|
44
|
+
generate.call(this)
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { CatalogEntry } from '../catalog'
|
|
2
|
+
import type { StoryDefinition } from '../contract'
|
|
3
|
+
import { WorkshopSession } from '../session'
|
|
4
|
+
import { applyControlInput, type ControlValueInput } from '../controls'
|
|
5
|
+
|
|
6
|
+
/** Window geometry changes shell presentation only, never the story root. */
|
|
7
|
+
export function mobileShellMode(width: number, height: number): 'wide' | 'compact' | 'phone' {
|
|
8
|
+
if (width >= 1000 && height >= 500) {
|
|
9
|
+
return 'wide'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return width <= 600 ? 'phone' : 'compact'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class MobileController {
|
|
16
|
+
selectedId: string | null = null
|
|
17
|
+
catalog: readonly CatalogEntry[]
|
|
18
|
+
query = ''
|
|
19
|
+
panel: 'stories' | 'controls' | null = null
|
|
20
|
+
stageOnly = false
|
|
21
|
+
missingId: string | null = null
|
|
22
|
+
private generation = 0
|
|
23
|
+
private disposed = false
|
|
24
|
+
|
|
25
|
+
constructor(
|
|
26
|
+
readonly session: WorkshopSession,
|
|
27
|
+
catalog: readonly CatalogEntry[],
|
|
28
|
+
readonly changed: () => void,
|
|
29
|
+
) {
|
|
30
|
+
this.catalog = catalog
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async select(id: string, revision?: string): Promise<void> {
|
|
34
|
+
if (this.disposed) {
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const generation = ++this.generation
|
|
39
|
+
this.selectedId = id
|
|
40
|
+
this.panel = null
|
|
41
|
+
const entry = this.catalog.find((item) => item.id === id)
|
|
42
|
+
this.missingId = entry ? null : id
|
|
43
|
+
this.changed()
|
|
44
|
+
// Filtering is enforced again at the runtime boundary, before load.
|
|
45
|
+
if (!entry?.applicable || !entry.load) {
|
|
46
|
+
await this.session.dispose()
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
if (revision) {
|
|
52
|
+
this.session.markRebuilding()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const module = (await entry.load(revision)) as Record<string, unknown>
|
|
56
|
+
if (generation !== this.generation || this.disposed) {
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const story = module[entry.exportName] as StoryDefinition<any, any, any>
|
|
61
|
+
if (!story || story.id !== entry.id) {
|
|
62
|
+
throw new Error(`Story export ${entry.exportName} does not match ${entry.id}`)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (revision) {
|
|
66
|
+
await this.session.reload(story)
|
|
67
|
+
} else {
|
|
68
|
+
await this.session.select(story)
|
|
69
|
+
}
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (generation === this.generation) {
|
|
72
|
+
this.session.markCompileError(error)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
updateCatalog(
|
|
78
|
+
catalog: readonly CatalogEntry[],
|
|
79
|
+
diagnostics: readonly { message: string }[] = [],
|
|
80
|
+
): void {
|
|
81
|
+
if (this.disposed) {
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
++this.generation
|
|
86
|
+
this.catalog = catalog
|
|
87
|
+
this.changed()
|
|
88
|
+
if (diagnostics.length) {
|
|
89
|
+
this.session.markCompileError(diagnostics.map((d) => d.message).join('\n'))
|
|
90
|
+
} else if (this.selectedId) {
|
|
91
|
+
void this.select(this.selectedId, String(Date.now()))
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* One Back/Escape step: dismiss shell chrome before the host treats the key
|
|
97
|
+
* as navigation. The active panel closes first; a stage-only session exits
|
|
98
|
+
* next; nothing to dismiss returns null so the key keeps its host meaning.
|
|
99
|
+
* Story content is untouched — panel/flag changes never mount or dispose.
|
|
100
|
+
*/
|
|
101
|
+
dismissOverlay(): 'panel' | 'stage-only' | null {
|
|
102
|
+
if (this.panel) {
|
|
103
|
+
this.panel = null
|
|
104
|
+
return 'panel'
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (this.stageOnly) {
|
|
108
|
+
this.stageOnly = false
|
|
109
|
+
return 'stage-only'
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return null
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
control(key: string, value: ControlValueInput): string | undefined {
|
|
116
|
+
const story = this.session.currentStory
|
|
117
|
+
if (!story || this.session.state.phase !== 'ready') {
|
|
118
|
+
return 'Wait for the story to be ready'
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const result = applyControlInput(story, this.session.currentProps ?? {}, key, value)
|
|
122
|
+
if (result.props) {
|
|
123
|
+
this.session.applyProps(result.props)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return result.error?.message
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async dispose(): Promise<void> {
|
|
130
|
+
this.disposed = true
|
|
131
|
+
++this.generation
|
|
132
|
+
await this.session.dispose()
|
|
133
|
+
}
|
|
134
|
+
}
|