@octane-xplat/workshop 0.0.0-nightly.g14544fe

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 (67) hide show
  1. package/LICENSE +21 -0
  2. package/MOBILE.md +143 -0
  3. package/README.md +307 -0
  4. package/dist/build/discover.js +404 -0
  5. package/dist/build/macos-node.js +77 -0
  6. package/dist/build/mobile-node.js +96 -0
  7. package/dist/build/mobile-regex.js +40 -0
  8. package/dist/build/node.js +426 -0
  9. package/dist/build/vite-mobile.js +75 -0
  10. package/dist/build/vite.js +230 -0
  11. package/dist/build/windows/preflight.js +83 -0
  12. package/dist/contract.js +50 -0
  13. package/dist/index.js +2 -0
  14. package/package.json +127 -0
  15. package/src/build/discover.ts +585 -0
  16. package/src/build/macos/shell.macos.ts +674 -0
  17. package/src/build/macos-node.ts +98 -0
  18. package/src/build/macos-vite.ts +48 -0
  19. package/src/build/mobile-node.ts +127 -0
  20. package/src/build/mobile-regex.ts +60 -0
  21. package/src/build/node.d.ts +65 -0
  22. package/src/build/node.ts +537 -0
  23. package/src/build/vite-mobile.d.ts +21 -0
  24. package/src/build/vite-mobile.ts +102 -0
  25. package/src/build/vite.d.ts +11 -0
  26. package/src/build/vite.ts +316 -0
  27. package/src/build/windows/preflight.ts +113 -0
  28. package/src/catalog.ts +58 -0
  29. package/src/contract.ts +257 -0
  30. package/src/controls.ts +159 -0
  31. package/src/errors.ts +47 -0
  32. package/src/fixture.ts +158 -0
  33. package/src/index.ts +6 -0
  34. package/src/macos/controller.ts +170 -0
  35. package/src/macos/index.macos.ts +2 -0
  36. package/src/macos/stage.macos.ts +182 -0
  37. package/src/mobile/controller.ts +134 -0
  38. package/src/mobile/entry.mobile.ts +607 -0
  39. package/src/mobile/index.mobile.ts +3 -0
  40. package/src/mobile/mount.mobile.ts +60 -0
  41. package/src/mobile/tree.mobile.tsrx +17 -0
  42. package/src/session.ts +520 -0
  43. package/src/web/entry.web.ts +270 -0
  44. package/src/web/index.web.ts +7 -0
  45. package/src/web/mount.web.ts +86 -0
  46. package/src/web/shell.css +449 -0
  47. package/src/web/shell.web.tsrx +759 -0
  48. package/types/generated/.tsrx-typegen-manifest.json +24 -0
  49. package/types/generated/catalog.d.ts +30 -0
  50. package/types/generated/contract.d.ts +145 -0
  51. package/types/generated/controls.d.ts +28 -0
  52. package/types/generated/errors.d.ts +13 -0
  53. package/types/generated/fixture.d.ts +53 -0
  54. package/types/generated/index.d.ts +6 -0
  55. package/types/generated/macos/controller.d.ts +52 -0
  56. package/types/generated/macos/index.macos.d.ts +2 -0
  57. package/types/generated/macos/stage.macos.d.ts +10 -0
  58. package/types/generated/mobile/controller.d.ts +31 -0
  59. package/types/generated/mobile/entry.mobile.d.ts +22 -0
  60. package/types/generated/mobile/index.mobile.d.ts +3 -0
  61. package/types/generated/mobile/mount.mobile.d.ts +4 -0
  62. package/types/generated/mobile/tree.mobile.d.ts +4 -0
  63. package/types/generated/session.d.ts +84 -0
  64. package/types/generated/web/entry.web.d.ts +53 -0
  65. package/types/generated/web/index.web.d.ts +7 -0
  66. package/types/generated/web/mount.web.d.ts +2 -0
  67. 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
+ }