@octane-xplat/workshop 0.1.0 → 0.16.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.
- package/LICENSE +21 -0
- package/MOBILE.md +143 -0
- package/README.md +305 -1
- package/dist/build/discover.js +404 -0
- package/dist/build/macos-node.js +77 -0
- package/dist/build/mobile-node.js +96 -0
- package/dist/build/mobile-regex.js +40 -0
- package/dist/build/node.js +426 -0
- package/dist/build/vite-mobile.js +75 -0
- package/dist/build/vite.js +230 -0
- package/dist/build/windows/preflight.js +83 -0
- package/dist/contract.js +50 -0
- package/dist/index.js +2 -0
- package/package.json +118 -5
- package/src/build/discover.ts +585 -0
- package/src/build/macos/shell.macos.ts +674 -0
- package/src/build/macos-node.ts +98 -0
- package/src/build/macos-vite.ts +48 -0
- package/src/build/mobile-node.ts +127 -0
- package/src/build/mobile-regex.ts +60 -0
- package/src/build/node.d.ts +65 -0
- package/src/build/node.ts +537 -0
- package/src/build/vite-mobile.d.ts +21 -0
- package/src/build/vite-mobile.ts +102 -0
- package/src/build/vite.d.ts +11 -0
- package/src/build/vite.ts +316 -0
- package/src/build/windows/preflight.ts +113 -0
- package/src/catalog.ts +58 -0
- package/src/contract.ts +257 -0
- package/src/controls.ts +159 -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/stage.macos.ts +182 -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/session.ts +520 -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/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,674 @@
|
|
|
1
|
+
// @ts-nocheck — .mjs→.ts port: annotations pending (transitional)
|
|
2
|
+
import '@nativescript/macos-node-api'
|
|
3
|
+
import * as native from 'octane/universal/native'
|
|
4
|
+
import * as signals from 'octane/signals'
|
|
5
|
+
import * as signalsClient from 'octane/signals/client'
|
|
6
|
+
import * as internal from 'octane/internal/client'
|
|
7
|
+
import * as renderer from '@octane-xplat/macos-renderer'
|
|
8
|
+
import { createMacOSStageHost } from '../../macos/stage.macos.ts'
|
|
9
|
+
import { MacOSWorkshopController } from '../../macos/controller.ts'
|
|
10
|
+
import { UNSET, isUnsettable } from '../../controls.ts'
|
|
11
|
+
|
|
12
|
+
// The compiled story bundle requires these exact instances after each edit.
|
|
13
|
+
// Never bundle a second renderer/core runtime into the story revision.
|
|
14
|
+
const modules = {
|
|
15
|
+
'octane/universal/native': native,
|
|
16
|
+
'octane/signals': signals,
|
|
17
|
+
'octane/signals/client': signalsClient,
|
|
18
|
+
'octane/internal/client': internal,
|
|
19
|
+
'@octane-xplat/macos-renderer': renderer,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const setAXLabel = (view, text) => {
|
|
23
|
+
try {
|
|
24
|
+
view.performSelectorWithObject?.('setAccessibilityLabel:', String(text))
|
|
25
|
+
} catch {}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const rect = (x, y, width, height) => ({ origin: { x, y }, size: { width, height } })
|
|
29
|
+
const frame = (view, x, y, width, height) => {
|
|
30
|
+
view.frame = rect(x, y, Math.max(0, width), Math.max(0, height))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
class WorkshopActions extends NSObject {
|
|
34
|
+
static ObjCExposedMethods = {
|
|
35
|
+
invoke: { returns: interop.types.void, params: [interop.types.id] },
|
|
36
|
+
}
|
|
37
|
+
static ObjCProtocols = [NSTextFieldDelegate, NSWindowDelegate]
|
|
38
|
+
static {
|
|
39
|
+
NativeClass(this)
|
|
40
|
+
}
|
|
41
|
+
invoke(sender) {
|
|
42
|
+
this.callbacks.get(sender)?.()
|
|
43
|
+
}
|
|
44
|
+
controlTextDidChange(notification) {
|
|
45
|
+
const sender = notification.object
|
|
46
|
+
if (sender.currentEditor?.hasMarkedText?.()) {
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
this.callbacks.get(sender)?.()
|
|
51
|
+
}
|
|
52
|
+
controlTextViewDoCommandBySelector(_control, _editor, selector) {
|
|
53
|
+
if (String(selector) === 'cancelOperation:' && this.dismiss()) {
|
|
54
|
+
return true
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return false
|
|
58
|
+
}
|
|
59
|
+
windowDidResize() {
|
|
60
|
+
this.layout()
|
|
61
|
+
}
|
|
62
|
+
windowShouldClose() {
|
|
63
|
+
this.close()
|
|
64
|
+
return false
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Minimal window service standing in for the consumer's AppKit window layer
|
|
70
|
+
* (see apps/macos appkit.ts). Requires an explicit compiled component; the
|
|
71
|
+
* stage host wraps story windows so providers/fixture reach the second root.
|
|
72
|
+
*/
|
|
73
|
+
function installWindowService({ parent, offscreen }) {
|
|
74
|
+
const owned = new Set()
|
|
75
|
+
const openWindowService = (options = {}) => {
|
|
76
|
+
if (typeof options.component !== 'function') {
|
|
77
|
+
throw new Error('Workshop windows require an explicit component')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const size = options.size ?? { width: 420, height: 320 }
|
|
81
|
+
const kind = options.kind ?? 'regular'
|
|
82
|
+
const mask =
|
|
83
|
+
kind === 'popup'
|
|
84
|
+
? (NSWindowStyleMask.Borderless ?? NSWindowStyleMask.Titled | NSWindowStyleMask.Closable)
|
|
85
|
+
: NSWindowStyleMask.Titled | NSWindowStyleMask.Closable | NSWindowStyleMask.Resizable
|
|
86
|
+
|
|
87
|
+
const win = NSWindow.alloc().initWithContentRectStyleMaskBackingDefer(
|
|
88
|
+
rect(0, 0, size.width, size.height),
|
|
89
|
+
mask,
|
|
90
|
+
NSBackingStoreType.Buffered,
|
|
91
|
+
false,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
win.releasedWhenClosed = false
|
|
95
|
+
if (options.title) {
|
|
96
|
+
win.title = String(options.title)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const root = renderer.createMacOSRoot(win.contentView, {})
|
|
100
|
+
root.render(options.component, { ...options.props, parentWindow: win })
|
|
101
|
+
let resolveClosed
|
|
102
|
+
const closed = new Promise((resolve) => {
|
|
103
|
+
resolveClosed = resolve
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
let isClosed = false
|
|
107
|
+
const anchor = options.parent ?? parent
|
|
108
|
+
const controller = {
|
|
109
|
+
window: win,
|
|
110
|
+
root,
|
|
111
|
+
closed,
|
|
112
|
+
get isClosed() {
|
|
113
|
+
return isClosed
|
|
114
|
+
},
|
|
115
|
+
close() {
|
|
116
|
+
if (isClosed) {
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
isClosed = true
|
|
121
|
+
try {
|
|
122
|
+
root.unmount()
|
|
123
|
+
} finally {
|
|
124
|
+
if (kind === 'dialog') {
|
|
125
|
+
anchor?.endSheet?.(win)
|
|
126
|
+
} else if (kind === 'popup') {
|
|
127
|
+
anchor?.removeChildWindow?.(win)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
win.close()
|
|
131
|
+
resolveClosed()
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (kind === 'dialog' && typeof anchor?.beginSheetCompletionHandler === 'function') {
|
|
137
|
+
anchor.beginSheetCompletionHandler(win, () => {})
|
|
138
|
+
} else if (kind === 'popup' && typeof anchor?.addChildWindowOrdered === 'function') {
|
|
139
|
+
anchor.addChildWindowOrdered(win, 1)
|
|
140
|
+
} else if (!offscreen) {
|
|
141
|
+
win.makeKeyAndOrderFront(null)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
owned.add(controller)
|
|
145
|
+
closed.finally(() => owned.delete(controller))
|
|
146
|
+
return controller
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
globalThis.__xplatAppKitOpenWindow = openWindowService
|
|
150
|
+
globalThis.__xplatAppKitMountSheet = (component, props, options = {}) =>
|
|
151
|
+
openWindowService({ ...options, kind: 'dialog', component, props })
|
|
152
|
+
|
|
153
|
+
return () => owned.size
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Small AppKit shell; all story content lives in a separate persistent view. */
|
|
157
|
+
export function startMacOSWorkshop({ bundleFile, initialStoryId = null, offscreen = false } = {}) {
|
|
158
|
+
globalThis.__xplatDevModules = modules
|
|
159
|
+
const app = NSApplication.sharedApplication
|
|
160
|
+
const window = NSWindow.alloc().initWithContentRectStyleMaskBackingDefer(
|
|
161
|
+
rect(0, 0, 1100, 720),
|
|
162
|
+
NSWindowStyleMask.Titled | NSWindowStyleMask.Closable | NSWindowStyleMask.Resizable,
|
|
163
|
+
NSBackingStoreType.Buffered,
|
|
164
|
+
false,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
window.title = 'Octane Xplat workshop — macOS (qualification preview)'
|
|
168
|
+
window.releasedWhenClosed = false
|
|
169
|
+
const content = window.contentView
|
|
170
|
+
const stage = NSView.alloc().initWithFrame(rect(0, 0, 600, 600))
|
|
171
|
+
content.addSubview(stage)
|
|
172
|
+
const errorCover = NSView.alloc().initWithFrame(rect(0, 0, 600, 600))
|
|
173
|
+
errorCover.hidden = true
|
|
174
|
+
const errorLabel = NSTextField.labelWithString('')
|
|
175
|
+
setAXLabel(errorLabel, 'Story error')
|
|
176
|
+
errorCover.addSubview(errorLabel)
|
|
177
|
+
content.addSubview(errorCover)
|
|
178
|
+
const openWindowCount = installWindowService({ parent: window, offscreen })
|
|
179
|
+
const actions = WorkshopActions.new()
|
|
180
|
+
actions.callbacks = new Map()
|
|
181
|
+
window.delegate = actions
|
|
182
|
+
let firstRevision = __hostRunFile(bundleFile)
|
|
183
|
+
const controller = new MacOSWorkshopController(
|
|
184
|
+
createMacOSStageHost({ view: stage, parentWindow: window }),
|
|
185
|
+
firstRevision.providers ?? [],
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
const chrome = []
|
|
189
|
+
const button = (parent, title, action) => {
|
|
190
|
+
const view = NSButton.alloc().initWithFrame(rect(0, 0, 100, 30))
|
|
191
|
+
view.title = title
|
|
192
|
+
view.bezelStyle = NSBezelStyle.Rounded
|
|
193
|
+
setAXLabel(view, title)
|
|
194
|
+
view.target = actions
|
|
195
|
+
view.action = 'invoke:'
|
|
196
|
+
actions.callbacks.set(view, action)
|
|
197
|
+
parent.addSubview(view)
|
|
198
|
+
return view
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const label = (parent, text) => {
|
|
202
|
+
const view = NSTextField.labelWithString(text)
|
|
203
|
+
setAXLabel(view, text)
|
|
204
|
+
parent.addSubview(view)
|
|
205
|
+
return view
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const scroll = () => {
|
|
209
|
+
const view = NSScrollView.alloc().initWithFrame(rect(0, 0, 240, 600))
|
|
210
|
+
view.hasVerticalScroller = true
|
|
211
|
+
view.hasHorizontalScroller = false
|
|
212
|
+
const body = NSView.alloc().initWithFrame(rect(0, 0, 240, 600))
|
|
213
|
+
view.documentView = body
|
|
214
|
+
content.addSubview(view)
|
|
215
|
+
chrome.push(view)
|
|
216
|
+
return { view, body, nodes: [] }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const catalog = scroll()
|
|
220
|
+
const controls = scroll()
|
|
221
|
+
const title = label(content, 'Choose a story')
|
|
222
|
+
const status = label(content, 'discovered')
|
|
223
|
+
const storiesButton = button(content, 'Stories', () => toggle('stories', storiesButton))
|
|
224
|
+
const controlsButton = button(content, 'Controls', () => toggle('controls', controlsButton))
|
|
225
|
+
const resetButton = button(content, 'Reset', () => {
|
|
226
|
+
void controller.reset()
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
const hideButton = button(content, 'Stage only', () => {
|
|
230
|
+
controller.stageOnly = !controller.stageOnly
|
|
231
|
+
controller.notify()
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
chrome.push(title, status, storiesButton, controlsButton)
|
|
235
|
+
let opener = null
|
|
236
|
+
let selectedDefinition
|
|
237
|
+
let lastCatalog
|
|
238
|
+
let lastQuery
|
|
239
|
+
let announced = ''
|
|
240
|
+
function toggle(panel, source) {
|
|
241
|
+
opener = source
|
|
242
|
+
controller.panel = controller.panel === panel ? null : panel
|
|
243
|
+
controller.notify()
|
|
244
|
+
if (controller.panel === 'stories') {
|
|
245
|
+
window.makeFirstResponder(search)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
actions.dismiss = () => {
|
|
250
|
+
if (!controller.panel) {
|
|
251
|
+
return false
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
controller.panel = null
|
|
255
|
+
controller.notify()
|
|
256
|
+
window.makeFirstResponder(opener)
|
|
257
|
+
return true
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const search = NSTextField.alloc().initWithFrame(rect(0, 0, 220, 30))
|
|
261
|
+
search.placeholderString = 'Search title or ID'
|
|
262
|
+
setAXLabel(search, 'Search stories')
|
|
263
|
+
search.delegate = actions
|
|
264
|
+
catalog.body.addSubview(search)
|
|
265
|
+
actions.callbacks.set(search, () => {
|
|
266
|
+
controller.query = String(search.stringValue)
|
|
267
|
+
controller.notify()
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
function clear(panel) {
|
|
271
|
+
for (const node of panel.nodes) {
|
|
272
|
+
actions.callbacks.delete(node)
|
|
273
|
+
node.delegate = null
|
|
274
|
+
node.removeFromSuperview()
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
panel.nodes = []
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function rebuildCatalog() {
|
|
281
|
+
clear(catalog)
|
|
282
|
+
const entries = controller.catalog.filter((e) =>
|
|
283
|
+
`${e.title} ${e.id}`.toLowerCase().includes(controller.query.toLowerCase()),
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
const height = Math.max(120, 46 + entries.length * 38)
|
|
287
|
+
frame(catalog.body, 0, 0, 240, height)
|
|
288
|
+
frame(search, 8, height - 38, 224, 30)
|
|
289
|
+
entries.forEach((entry, i) => {
|
|
290
|
+
const text = `${entry.title} — ${entry.id}${entry.applicable ? '' : ' (unsupported)'}`
|
|
291
|
+
const item = button(catalog.body, text, () => {
|
|
292
|
+
void controller.select(entry.id)
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
frame(item, 8, height - 80 - i * 38, 224, 32)
|
|
296
|
+
catalog.nodes.push(item)
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
if (!entries.length) {
|
|
300
|
+
const empty = label(catalog.body, 'No stories: check **/*.stories.tsrx')
|
|
301
|
+
frame(empty, 8, 40, 224, 40)
|
|
302
|
+
catalog.nodes.push(empty)
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function rebuildControls() {
|
|
307
|
+
clear(controls)
|
|
308
|
+
const story = controller.session.currentStory
|
|
309
|
+
if (!story) {
|
|
310
|
+
return
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const rows = Object.entries(story.controls ?? {})
|
|
314
|
+
const height = Math.max(200, rows.length * 122 + 50)
|
|
315
|
+
frame(controls.body, 0, 0, 240, height)
|
|
316
|
+
rows.forEach(([key, control], i) => {
|
|
317
|
+
const y = height - 50 - i * 122
|
|
318
|
+
const name = label(controls.body, key)
|
|
319
|
+
frame(name, 8, y, 224, 22)
|
|
320
|
+
controls.nodes.push(name)
|
|
321
|
+
let input
|
|
322
|
+
if (control.type === 'boolean') {
|
|
323
|
+
input = button(
|
|
324
|
+
controls.body,
|
|
325
|
+
String(controller.session.currentProps?.[key] ?? false),
|
|
326
|
+
() => {
|
|
327
|
+
controller.control(key, !controller.session.currentProps?.[key])
|
|
328
|
+
input.title = String(controller.session.currentProps?.[key] ?? false)
|
|
329
|
+
},
|
|
330
|
+
)
|
|
331
|
+
} else if (control.type === 'enum') {
|
|
332
|
+
input = NSPopUpButton.alloc().initWithFramePullsDown(rect(0, 0, 224, 30), false)
|
|
333
|
+
input.addItemsWithTitles(control.options.map(String))
|
|
334
|
+
input.selectItemWithTitle(String(controller.session.currentProps?.[key] ?? ''))
|
|
335
|
+
input.target = actions
|
|
336
|
+
input.action = 'invoke:'
|
|
337
|
+
actions.callbacks.set(input, () =>
|
|
338
|
+
controller.control(key, control.options[Number(input.indexOfSelectedItem)]),
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
controls.body.addSubview(input)
|
|
342
|
+
} else {
|
|
343
|
+
input = NSTextField.alloc().initWithFrame(rect(0, 0, 224, 30))
|
|
344
|
+
input.stringValue =
|
|
345
|
+
controller.drafts[key] ?? String(controller.session.currentProps?.[key] ?? '')
|
|
346
|
+
|
|
347
|
+
input.delegate = actions
|
|
348
|
+
actions.callbacks.set(input, () => controller.control(key, String(input.stringValue)))
|
|
349
|
+
controls.body.addSubview(input)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
setAXLabel(input, key)
|
|
353
|
+
frame(input, 8, y - 34, 224, 30)
|
|
354
|
+
controls.nodes.push(input)
|
|
355
|
+
const error = label(controls.body, controller.errors[key] ?? '')
|
|
356
|
+
error.__workshopKey = key
|
|
357
|
+
frame(error, 8, y - 62, 224, 24)
|
|
358
|
+
controls.nodes.push(error)
|
|
359
|
+
if (isUnsettable(story, key)) {
|
|
360
|
+
const unset = button(controls.body, `Unset ${key}`, () => {
|
|
361
|
+
controller.control(key, UNSET)
|
|
362
|
+
selectedDefinition = null
|
|
363
|
+
controller.notify()
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
frame(unset, 8, y - 98, 224, 30)
|
|
367
|
+
controls.nodes.push(unset)
|
|
368
|
+
}
|
|
369
|
+
})
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function layout() {
|
|
373
|
+
const { width, height } = content.bounds.size
|
|
374
|
+
controller.mode = width >= 1000 && height >= 480 ? 'wide' : width <= 560 ? 'phone' : 'compact'
|
|
375
|
+
const wide = controller.mode === 'wide' && !controller.stageOnly
|
|
376
|
+
const showCatalog = !controller.stageOnly && (wide || controller.panel === 'stories')
|
|
377
|
+
const showControls = !controller.stageOnly && (wide || controller.panel === 'controls')
|
|
378
|
+
catalog.view.hidden = !showCatalog
|
|
379
|
+
controls.view.hidden = !showControls
|
|
380
|
+
for (const node of chrome) {
|
|
381
|
+
if (node !== catalog.view && node !== controls.view) {
|
|
382
|
+
node.hidden = controller.stageOnly
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Compact panels reserve space rather than cover an interactive stage.
|
|
387
|
+
const left = showCatalog ? Math.min(240, width * 0.45) : 0
|
|
388
|
+
const right = showControls ? Math.min(240, width * 0.45) : 0
|
|
389
|
+
frame(catalog.view, 0, 48, left, height - 118)
|
|
390
|
+
frame(controls.view, width - right, 48, right, height - 118)
|
|
391
|
+
frame(stage, left + 8, 48, width - left - right - 16, height - 118)
|
|
392
|
+
frame(errorCover, left + 8, 48, width - left - right - 16, height - 118)
|
|
393
|
+
frame(errorLabel, 8, Math.max(0, (height - 118) / 2 - 20), width - left - right - 32, 40)
|
|
394
|
+
frame(title, 8, height - 30, width - 16, 24)
|
|
395
|
+
frame(status, 8, height - 64, width - 16, 28)
|
|
396
|
+
frame(storiesButton, 8, 8, 95, 32)
|
|
397
|
+
frame(controlsButton, 108, 8, 95, 32)
|
|
398
|
+
frame(resetButton, width - 100, 8, 92, 32)
|
|
399
|
+
frame(hideButton, width - 230, 8, 125, 32)
|
|
400
|
+
hideButton.title = controller.stageOnly ? 'Return to workshop' : 'Stage only'
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
actions.layout = layout
|
|
404
|
+
const unsubscribe = controller.subscribe(() => {
|
|
405
|
+
title.stringValue = controller.selectedId ?? 'Choose a story'
|
|
406
|
+
const state = controller.session.state
|
|
407
|
+
const message =
|
|
408
|
+
controller.catalogError ||
|
|
409
|
+
`${state.phase}${state.stale ? ' — stale revision' : ''}${state.failure ? ': ' + state.failure.message : ''}`
|
|
410
|
+
|
|
411
|
+
status.stringValue = message
|
|
412
|
+
setAXLabel(status, message)
|
|
413
|
+
const covered =
|
|
414
|
+
state.phase === 'failed' ||
|
|
415
|
+
state.phase === 'restart-required' ||
|
|
416
|
+
state.phase === 'disconnected'
|
|
417
|
+
|
|
418
|
+
errorCover.hidden = !covered
|
|
419
|
+
errorLabel.stringValue = covered ? message : ''
|
|
420
|
+
setAXLabel(errorLabel, covered ? `Story error: ${message}` : 'Story error')
|
|
421
|
+
if (message !== announced) {
|
|
422
|
+
announced = message
|
|
423
|
+
if (!offscreen && typeof NSAccessibilityPostNotificationWithUserInfo === 'function') {
|
|
424
|
+
NSAccessibilityPostNotificationWithUserInfo(
|
|
425
|
+
content,
|
|
426
|
+
'NSAccessibilityAnnouncementRequestedNotification',
|
|
427
|
+
{ NSAccessibilityAnnouncementKey: message, NSAccessibilityPriorityKey: 1 },
|
|
428
|
+
)
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (lastCatalog !== controller.catalog || lastQuery !== controller.query) {
|
|
433
|
+
lastCatalog = controller.catalog
|
|
434
|
+
lastQuery = controller.query
|
|
435
|
+
rebuildCatalog()
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (selectedDefinition !== controller.session.currentStory) {
|
|
439
|
+
selectedDefinition = controller.session.currentStory
|
|
440
|
+
rebuildControls()
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
for (const node of controls.nodes) {
|
|
444
|
+
if (node.__workshopKey) {
|
|
445
|
+
node.stringValue = controller.errors[node.__workshopKey] ?? ''
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
layout()
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
let reloads = Promise.resolve()
|
|
453
|
+
let closed = false
|
|
454
|
+
const reload = () => {
|
|
455
|
+
reloads = reloads
|
|
456
|
+
.then(async () => {
|
|
457
|
+
if (closed) {
|
|
458
|
+
return
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
try {
|
|
462
|
+
const loaded = firstRevision ?? __hostRunFile(bundleFile)
|
|
463
|
+
firstRevision = null
|
|
464
|
+
await controller.updateCatalog(loaded.catalog, loaded.diagnostics)
|
|
465
|
+
if (!controller.selectedId && initialStoryId) {
|
|
466
|
+
await controller.select(initialStoryId)
|
|
467
|
+
}
|
|
468
|
+
} catch (error) {
|
|
469
|
+
try {
|
|
470
|
+
console.log(
|
|
471
|
+
'WORKSHOP_ERROR ' +
|
|
472
|
+
String(error?.message ?? error) +
|
|
473
|
+
' :: ' +
|
|
474
|
+
String(error?.stack ?? ''),
|
|
475
|
+
)
|
|
476
|
+
} catch {}
|
|
477
|
+
|
|
478
|
+
controller.session.markCompileError(error)
|
|
479
|
+
}
|
|
480
|
+
})
|
|
481
|
+
.catch((error) => {
|
|
482
|
+
try {
|
|
483
|
+
console.log(
|
|
484
|
+
'WORKSHOP_ERROR ' +
|
|
485
|
+
String(error?.message ?? error) +
|
|
486
|
+
' :: ' +
|
|
487
|
+
String(error?.stack ?? ''),
|
|
488
|
+
)
|
|
489
|
+
} catch {}
|
|
490
|
+
|
|
491
|
+
controller.session.markCompileError(error)
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
return reloads
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
actions.close = async () => {
|
|
498
|
+
if (closed) {
|
|
499
|
+
return
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
closed = true
|
|
503
|
+
await reloads
|
|
504
|
+
await controller.dispose()
|
|
505
|
+
unsubscribe()
|
|
506
|
+
clear(catalog)
|
|
507
|
+
clear(controls)
|
|
508
|
+
actions.callbacks.clear()
|
|
509
|
+
window.delegate = null
|
|
510
|
+
window.close()
|
|
511
|
+
globalThis.__xplatStopHost?.()
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
let cmdSeq = 0
|
|
515
|
+
let dumpSeq = 0
|
|
516
|
+
const dump = () => {
|
|
517
|
+
try {
|
|
518
|
+
console.log(
|
|
519
|
+
'WORKSHOP_STATE ' +
|
|
520
|
+
JSON.stringify({
|
|
521
|
+
seq: cmdSeq,
|
|
522
|
+
d: ++dumpSeq,
|
|
523
|
+
...controller.inspect(),
|
|
524
|
+
openWindows: openWindowCount(),
|
|
525
|
+
}),
|
|
526
|
+
)
|
|
527
|
+
} catch (error) {
|
|
528
|
+
console.log('WORKSHOP_STATE {"inspectError":' + JSON.stringify(String(error)) + '}')
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const eachSubview = (view, visit) => {
|
|
533
|
+
if (visit(view)) {
|
|
534
|
+
return true
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const subs = view?.subviews
|
|
538
|
+
const count =
|
|
539
|
+
typeof subs?.count === 'function' ? subs.count() : (subs?.count ?? subs?.length ?? 0)
|
|
540
|
+
|
|
541
|
+
for (let i = 0; i < count; i++) {
|
|
542
|
+
const child = subs.objectAtIndex ? subs.objectAtIndex(i) : subs[i]
|
|
543
|
+
if (child && eachSubview(child, visit)) {
|
|
544
|
+
return true
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return false
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Dispatches through the view's real recognizer/control action. This is
|
|
552
|
+
// gesture-action dispatch for qualification, not OS-synthesized input.
|
|
553
|
+
const pressLabel = (label) => {
|
|
554
|
+
let target = null
|
|
555
|
+
eachSubview(stage, (view) => {
|
|
556
|
+
let name = ''
|
|
557
|
+
try {
|
|
558
|
+
name = String(
|
|
559
|
+
typeof view.accessibilityLabel === 'function'
|
|
560
|
+
? view.accessibilityLabel()
|
|
561
|
+
: (view.accessibilityLabel ?? ''),
|
|
562
|
+
)
|
|
563
|
+
} catch {}
|
|
564
|
+
|
|
565
|
+
if (name === label) {
|
|
566
|
+
target = view
|
|
567
|
+
return true
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return false
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
if (!target) {
|
|
574
|
+
console.log(`WORKSHOP_PRESS_MISS "${label}"`)
|
|
575
|
+
return
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const recs = target.gestureRecognizers
|
|
579
|
+
const count = typeof recs?.count === 'function' ? recs.count() : (recs?.count ?? 0)
|
|
580
|
+
for (let i = 0; i < count; i++) {
|
|
581
|
+
const rec = recs.objectAtIndex(i)
|
|
582
|
+
const receiver = rec.target
|
|
583
|
+
const invoke = receiver?.['viewPressed:'] ?? receiver?.viewPressed
|
|
584
|
+
if (typeof invoke === 'function') {
|
|
585
|
+
invoke.call(receiver, rec)
|
|
586
|
+
return
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (typeof target.performClick === 'function') {
|
|
591
|
+
target.performClick(target)
|
|
592
|
+
return
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
console.log(`WORKSHOP_PRESS_MISS "${label}"`)
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const run = async (command) => {
|
|
599
|
+
++cmdSeq
|
|
600
|
+
const [verb, ...rest] = command.split(' ')
|
|
601
|
+
const arg = rest.join(' ')
|
|
602
|
+
try {
|
|
603
|
+
if (verb === 'catalog') {
|
|
604
|
+
console.log(
|
|
605
|
+
'WORKSHOP_CATALOG ' +
|
|
606
|
+
JSON.stringify(
|
|
607
|
+
controller.catalog.map((entry) => ({
|
|
608
|
+
id: entry.id,
|
|
609
|
+
title: entry.title,
|
|
610
|
+
applicable: entry.applicable,
|
|
611
|
+
reason: entry.reason ?? null,
|
|
612
|
+
exportName: entry.exportName,
|
|
613
|
+
})),
|
|
614
|
+
),
|
|
615
|
+
)
|
|
616
|
+
} else if (verb === 'select' && arg) {
|
|
617
|
+
await controller.select(arg)
|
|
618
|
+
} else if (verb === 'control' && arg) {
|
|
619
|
+
const space = arg.indexOf(' ')
|
|
620
|
+
controller.control(arg.slice(0, space), JSON.parse(arg.slice(space + 1)))
|
|
621
|
+
} else if (verb === 'unset' && arg) {
|
|
622
|
+
controller.control(arg, UNSET)
|
|
623
|
+
} else if (verb === 'press' && arg) {
|
|
624
|
+
pressLabel(arg)
|
|
625
|
+
} else if (verb === 'reset') {
|
|
626
|
+
await controller.reset()
|
|
627
|
+
} else if (verb === 'query') {
|
|
628
|
+
controller.query = arg
|
|
629
|
+
controller.notify()
|
|
630
|
+
} else if (verb === 'panel') {
|
|
631
|
+
controller.panel = arg === 'none' ? null : arg
|
|
632
|
+
controller.notify()
|
|
633
|
+
} else if (verb === 'stage-only') {
|
|
634
|
+
controller.stageOnly = arg !== 'off'
|
|
635
|
+
controller.notify()
|
|
636
|
+
}
|
|
637
|
+
} catch (error) {
|
|
638
|
+
try {
|
|
639
|
+
console.log(
|
|
640
|
+
'WORKSHOP_ERROR ' + String(error?.message ?? error) + ' :: ' + String(error?.stack ?? ''),
|
|
641
|
+
)
|
|
642
|
+
} catch {}
|
|
643
|
+
|
|
644
|
+
controller.session.markCompileError(error)
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
dump()
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
globalThis.__xplatOnInput = (line) => {
|
|
651
|
+
if (line === 'reload') {
|
|
652
|
+
void reload().then(dump)
|
|
653
|
+
} else if (line === 'rebuilding') {
|
|
654
|
+
controller.session.markRebuilding()
|
|
655
|
+
} else if (line.startsWith('compile-error ')) {
|
|
656
|
+
controller.session.markCompileError(line.slice(14))
|
|
657
|
+
} else if (line === 'close') {
|
|
658
|
+
void actions.close()
|
|
659
|
+
} else {
|
|
660
|
+
void run(line)
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
layout()
|
|
665
|
+
void reload()
|
|
666
|
+
if (!offscreen) {
|
|
667
|
+
window.center()
|
|
668
|
+
window.makeKeyAndOrderFront(null)
|
|
669
|
+
app.finishLaunching()
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
console.log('WORKSHOP_MACOS_HOST AppKit/JavaScriptCore; native input qualification pending')
|
|
673
|
+
return { controller, window, stage, close: actions.close, reload }
|
|
674
|
+
}
|