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