@crowdedkingdoms/crowdyjs 12.0.0 → 12.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/MIGRATION.md +45 -0
  2. package/README.md +70 -1
  3. package/dist/crowdy-studio/agent-dom-shell.d.ts +29 -1
  4. package/dist/crowdy-studio/agent-dom-shell.d.ts.map +1 -1
  5. package/dist/crowdy-studio/agent-dom-shell.js +108 -44
  6. package/dist/crowdy-studio/dom-shell.d.ts +46 -3
  7. package/dist/crowdy-studio/dom-shell.d.ts.map +1 -1
  8. package/dist/crowdy-studio/dom-shell.js +551 -125
  9. package/dist/crowdy-studio/embed/dock.d.ts +56 -0
  10. package/dist/crowdy-studio/embed/dock.d.ts.map +1 -0
  11. package/dist/crowdy-studio/embed/dock.js +231 -0
  12. package/dist/crowdy-studio/embed/embed-styles.d.ts +12 -0
  13. package/dist/crowdy-studio/embed/embed-styles.d.ts.map +1 -0
  14. package/dist/crowdy-studio/embed/embed-styles.js +549 -0
  15. package/dist/crowdy-studio/embed/hud-layer.d.ts +40 -0
  16. package/dist/crowdy-studio/embed/hud-layer.d.ts.map +1 -0
  17. package/dist/crowdy-studio/embed/hud-layer.js +100 -0
  18. package/dist/crowdy-studio/embed/index.d.ts +5 -0
  19. package/dist/crowdy-studio/embed/index.d.ts.map +1 -0
  20. package/dist/crowdy-studio/embed/index.js +4 -0
  21. package/dist/crowdy-studio/embed/panel.d.ts +162 -0
  22. package/dist/crowdy-studio/embed/panel.d.ts.map +1 -0
  23. package/dist/crowdy-studio/embed/panel.js +642 -0
  24. package/dist/crowdy-studio/index.d.ts +3 -0
  25. package/dist/crowdy-studio/index.d.ts.map +1 -1
  26. package/dist/crowdy-studio/index.js +3 -0
  27. package/dist/crowdy-studio/layout.d.ts +50 -0
  28. package/dist/crowdy-studio/layout.d.ts.map +1 -0
  29. package/dist/crowdy-studio/layout.js +155 -0
  30. package/dist/crowdy-studio/mount.d.ts.map +1 -1
  31. package/dist/crowdy-studio/mount.js +3 -1
  32. package/dist/crowdy-studio/splitter.d.ts +33 -0
  33. package/dist/crowdy-studio/splitter.d.ts.map +1 -0
  34. package/dist/crowdy-studio/splitter.js +133 -0
  35. package/dist/crowdy-studio/styles.d.ts +1 -1
  36. package/dist/crowdy-studio/styles.d.ts.map +1 -1
  37. package/dist/crowdy-studio/styles.js +128 -13
  38. package/dist/domains/gameModel.d.ts +68 -1
  39. package/dist/domains/gameModel.d.ts.map +1 -1
  40. package/dist/domains/gameModel.js +88 -1
  41. package/dist/generated/graphql.d.ts +132 -15
  42. package/dist/generated/graphql.d.ts.map +1 -1
  43. package/dist/generated/graphql.js +12 -0
  44. package/dist/index.d.ts +4 -4
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +3 -3
  47. package/dist/kit/npcs.d.ts.map +1 -1
  48. package/dist/kit/social.d.ts.map +1 -1
  49. package/dist/player-host/control-banner.d.ts +40 -0
  50. package/dist/player-host/control-banner.d.ts.map +1 -0
  51. package/dist/player-host/control-banner.js +252 -0
  52. package/dist/player-host/control-gate.d.ts +77 -0
  53. package/dist/player-host/control-gate.d.ts.map +1 -0
  54. package/dist/player-host/control-gate.js +197 -0
  55. package/dist/player-host/index.d.ts +2 -0
  56. package/dist/player-host/index.d.ts.map +1 -1
  57. package/dist/player-host/index.js +2 -0
  58. package/package.json +6 -1
@@ -0,0 +1,642 @@
1
+ import { mountCrowdyStudio, } from '../mount.js';
2
+ import { CROWDY_STUDIO_EMBED_NARROW_BREAKPOINT_PX, CrowdyStudioEmbedDock, addBodyClass, removeBodyClass, } from './dock.js';
3
+ import { ensureCrowdyStudioEmbedStyles } from './embed-styles.js';
4
+ const TITLE_ID = 'ck-crowdy-studio-embed-title';
5
+ const DESCRIPTION_ID = 'ck-crowdy-studio-embed-description';
6
+ const SHELL_ID = 'ck-crowdy-studio-embed-shell';
7
+ const DRAWER_ID = 'ck-crowdy-studio-embed-drawer';
8
+ /**
9
+ * Responsive game shell around CrowdyJS's project-first Crowdy Studio.
10
+ * Desktop uses a resizable right dock; narrow screens use a focus-trapped
11
+ * modal. Construct once per game and `toggle(context)` per open.
12
+ */
13
+ export class CrowdyStudioEmbed {
14
+ constructor(options) {
15
+ this.options = options;
16
+ this.root = null;
17
+ this.mountElement = null;
18
+ this.statusElement = null;
19
+ this.drawerElement = null;
20
+ this.drawerButton = null;
21
+ this.handle = null;
22
+ this.context = null;
23
+ this.previousFocus = null;
24
+ this.restoreGameplayInput = null;
25
+ this.unmountHudPreview = null;
26
+ this.mountGeneration = 0;
27
+ this.displayMode = null;
28
+ this.viewportResize = () => {
29
+ this.updateDisplayMode();
30
+ };
31
+ this.keyDown = (event) => {
32
+ this.handleDialogKey(event);
33
+ };
34
+ this.documentKeyDown = (event) => {
35
+ if (!this.root || this.root.contains(event.target))
36
+ return;
37
+ this.handleDialogKey(event);
38
+ };
39
+ this.captureEscape = (event) => {
40
+ if (!this.root || !this.modal || event.code !== 'Escape' || event.repeat) {
41
+ return;
42
+ }
43
+ event.preventDefault();
44
+ event.stopPropagation();
45
+ this.close();
46
+ };
47
+ this.stopKeyboardEvent = (event) => {
48
+ event.stopPropagation();
49
+ };
50
+ this.documentKeyUp = (event) => {
51
+ if (this.root && !this.root.contains(event.target)) {
52
+ event.stopPropagation();
53
+ }
54
+ };
55
+ this.keepFocusInside = (event) => {
56
+ if (!this.root || this.root.contains(event.target))
57
+ return;
58
+ const first = focusableElements(this.root)[0];
59
+ if (first)
60
+ first.focus();
61
+ else
62
+ this.root.focus();
63
+ };
64
+ this.dock = new CrowdyStudioEmbedDock((width) => {
65
+ this.applyDockWidth(width);
66
+ }, options.dockStorage === undefined ? undefined : options.dockStorage);
67
+ }
68
+ get open() {
69
+ return this.root !== null;
70
+ }
71
+ get modal() {
72
+ return this.displayMode === 'fullscreen';
73
+ }
74
+ get mode() {
75
+ return this.displayMode;
76
+ }
77
+ toggle(context) {
78
+ if (this.root)
79
+ this.close();
80
+ else
81
+ this.show(context);
82
+ }
83
+ destroy() {
84
+ this.close();
85
+ this.dock.destroy();
86
+ }
87
+ close() {
88
+ if (!this.root)
89
+ return;
90
+ const context = this.context;
91
+ this.mountGeneration++;
92
+ this.options.onAgentUnmounted?.();
93
+ this.handle?.destroy();
94
+ this.handle = null;
95
+ this.unmountHudPreview?.();
96
+ this.unmountHudPreview = null;
97
+ this.leaveModalMode();
98
+ this.dock.deactivate();
99
+ window.removeEventListener('resize', this.viewportResize);
100
+ this.root.removeEventListener('keydown', this.keyDown);
101
+ this.root.removeEventListener('keyup', this.stopKeyboardEvent);
102
+ this.root.removeEventListener('keypress', this.stopKeyboardEvent);
103
+ this.root.remove();
104
+ removeBodyClass('ck-crowdy-studio-embed-open', 'ck-crowdy-studio-embed-docked', 'ck-crowdy-studio-embed-fullscreen', 'ck-crowdy-studio-embed-resizing');
105
+ this.clearDockWidth();
106
+ this.root = null;
107
+ this.mountElement = null;
108
+ this.statusElement = null;
109
+ this.drawerElement = null;
110
+ this.drawerButton = null;
111
+ this.context = null;
112
+ this.displayMode = null;
113
+ if (context)
114
+ this.options.onLayoutChange?.();
115
+ const restore = this.previousFocus;
116
+ this.previousFocus = null;
117
+ if (restore?.isConnected)
118
+ restore.focus();
119
+ this.options.onClosed?.();
120
+ }
121
+ show(context) {
122
+ ensureCrowdyStudioEmbedStyles();
123
+ this.context = context;
124
+ this.previousFocus =
125
+ document.activeElement instanceof HTMLElement
126
+ ? document.activeElement
127
+ : null;
128
+ const root = document.createElement('div');
129
+ root.className = 'ck-crowdy-studio-embed';
130
+ root.setAttribute('aria-labelledby', TITLE_ID);
131
+ root.setAttribute('aria-describedby', DESCRIPTION_ID);
132
+ root.tabIndex = -1;
133
+ root.addEventListener('keydown', this.keyDown);
134
+ root.addEventListener('keyup', this.stopKeyboardEvent);
135
+ root.addEventListener('keypress', this.stopKeyboardEvent);
136
+ root.addEventListener('mousedown', stopEventPropagation);
137
+ root.addEventListener('pointerdown', stopEventPropagation);
138
+ root.addEventListener('pointerup', stopEventPropagation);
139
+ root.addEventListener('click', stopEventPropagation);
140
+ root.addEventListener('dblclick', stopEventPropagation);
141
+ root.addEventListener('wheel', stopEventPropagation);
142
+ root.addEventListener('contextmenu', stopEventPropagation);
143
+ const shell = document.createElement('section');
144
+ shell.className = 'ck-crowdy-studio-embed-shell';
145
+ shell.id = SHELL_ID;
146
+ const header = this.header(context);
147
+ const workspace = document.createElement('div');
148
+ workspace.className = 'ck-crowdy-studio-embed-workspace';
149
+ const main = document.createElement('main');
150
+ main.className = 'ck-crowdy-studio-embed-main';
151
+ const status = document.createElement('div');
152
+ status.className = 'ck-crowdy-studio-embed-status is-loading';
153
+ status.setAttribute('role', 'status');
154
+ status.setAttribute('aria-live', 'polite');
155
+ const mountElement = document.createElement('div');
156
+ mountElement.className = 'ck-crowdy-studio-embed-mount';
157
+ mountElement.setAttribute('data-ck-crowdy-studio-embed-root', '');
158
+ main.append(status, mountElement);
159
+ const drawer = this.drawer(context);
160
+ workspace.append(main, drawer);
161
+ shell.append(header, workspace);
162
+ this.dock.separator.setAttribute('aria-controls', SHELL_ID);
163
+ root.append(this.dock.separator, shell);
164
+ document.body.appendChild(root);
165
+ addBodyClass('ck-crowdy-studio-embed-open');
166
+ this.root = root;
167
+ this.mountElement = mountElement;
168
+ this.statusElement = status;
169
+ this.drawerElement = drawer;
170
+ window.addEventListener('resize', this.viewportResize);
171
+ this.updateDisplayMode();
172
+ header
173
+ .querySelector('.ck-crowdy-studio-embed-close')
174
+ ?.focus();
175
+ void this.mountStudio();
176
+ }
177
+ /**
178
+ * Single compact header row: title, context pill, on-demand Context drawer
179
+ * toggle, Close. Project context and permission details live in the drawer
180
+ * so every workflow starts with maximum studio space.
181
+ */
182
+ header(context) {
183
+ const header = document.createElement('header');
184
+ header.className = 'ck-crowdy-studio-embed-header';
185
+ const title = document.createElement('h1');
186
+ title.id = TITLE_ID;
187
+ title.textContent = this.options.title ?? 'Crowdy Studio';
188
+ const description = document.createElement('p');
189
+ description.id = DESCRIPTION_ID;
190
+ description.className = 'ck-crowdy-studio-embed-visually-hidden';
191
+ description.textContent = this.accessibleDescription(context.gridId);
192
+ const pill = contextPill('Grid', context.gridId);
193
+ const contextButton = document.createElement('button');
194
+ contextButton.type = 'button';
195
+ contextButton.className = 'ck-crowdy-studio-embed-context-toggle';
196
+ contextButton.textContent = 'Context';
197
+ contextButton.title = context.hud
198
+ ? 'Grid bounds, permissions, and HUD preview'
199
+ : 'Grid bounds and permissions';
200
+ contextButton.setAttribute('aria-expanded', 'false');
201
+ contextButton.setAttribute('aria-controls', DRAWER_ID);
202
+ contextButton.addEventListener('click', () => this.toggleDrawer());
203
+ this.drawerButton = contextButton;
204
+ const close = document.createElement('button');
205
+ close.type = 'button';
206
+ close.className = 'ck-crowdy-studio-embed-close';
207
+ close.setAttribute('aria-label', 'Close Crowdy Studio');
208
+ close.title = this.closeKeyLabel()
209
+ ? `Close Crowdy Studio (Escape or ${this.closeKeyLabel()})`
210
+ : 'Close Crowdy Studio (Escape)';
211
+ close.textContent = 'Close';
212
+ close.addEventListener('click', () => this.close());
213
+ header.append(title, description, pill, contextButton, close);
214
+ return header;
215
+ }
216
+ /**
217
+ * On-demand context drawer: grid context, effective permissions, and (when
218
+ * a HUD sink is supplied) a lazily mounted HUD preview. Hidden by default;
219
+ * toggled from the header.
220
+ */
221
+ drawer(context) {
222
+ const aside = document.createElement('aside');
223
+ aside.className = 'ck-crowdy-studio-embed-drawer';
224
+ aside.id = DRAWER_ID;
225
+ aside.setAttribute('aria-label', 'Crowdy Studio context');
226
+ aside.hidden = true;
227
+ const contextTitle = document.createElement('h2');
228
+ contextTitle.textContent = 'Project context';
229
+ const grid = document.createElement('dl');
230
+ grid.className = 'ck-crowdy-studio-embed-grid-context';
231
+ appendDefinition(grid, 'Grid', context.gridId);
232
+ if (context.grid) {
233
+ appendDefinition(grid, 'Low chunk', formatChunk(context.grid.low));
234
+ appendDefinition(grid, 'High chunk', formatChunk(context.grid.high));
235
+ }
236
+ const permissionsTitle = document.createElement('h2');
237
+ permissionsTitle.textContent = 'Effective permissions';
238
+ const summary = document.createElement('p');
239
+ summary.textContent = permissionSummary(context.targetPermissions);
240
+ const permissions = document.createElement('div');
241
+ permissions.className = 'ck-crowdy-studio-embed-permissions';
242
+ permissions.append(permissionCard('SERVER', context.targetPermissions.SERVER), permissionCard('CLIENT', context.targetPermissions.CLIENT));
243
+ aside.append(contextTitle, grid, permissionsTitle, summary, permissions);
244
+ if (context.permissionsNote) {
245
+ const source = document.createElement('p');
246
+ source.className = 'ck-crowdy-studio-embed-permission-source';
247
+ source.textContent = context.permissionsNote;
248
+ aside.append(source);
249
+ }
250
+ if (context.hud) {
251
+ const hud = context.hud;
252
+ const previewDisclosure = document.createElement('details');
253
+ previewDisclosure.className =
254
+ 'ck-crowdy-studio-embed-hud-preview-disclosure';
255
+ const previewSummary = document.createElement('summary');
256
+ previewSummary.textContent = 'HUD preview';
257
+ const preview = document.createElement('div');
258
+ preview.className = 'ck-crowdy-studio-embed-hud-preview';
259
+ preview.setAttribute('aria-live', 'polite');
260
+ previewDisclosure.append(previewSummary, preview);
261
+ previewDisclosure.addEventListener('toggle', () => {
262
+ if (previewDisclosure.open) {
263
+ this.unmountHudPreview ?? (this.unmountHudPreview = hud.mountPreview(preview, this.presentationSource(context.gridId)));
264
+ }
265
+ else {
266
+ this.unmountHudPreview?.();
267
+ this.unmountHudPreview = null;
268
+ }
269
+ });
270
+ aside.append(previewDisclosure);
271
+ }
272
+ const hint = document.createElement('p');
273
+ hint.className = 'ck-crowdy-studio-embed-shortcuts';
274
+ hint.textContent = this.shortcutHint();
275
+ aside.append(hint);
276
+ return aside;
277
+ }
278
+ toggleDrawer(force) {
279
+ const drawer = this.drawerElement;
280
+ if (!drawer)
281
+ return;
282
+ const open = force ?? drawer.hidden;
283
+ drawer.hidden = !open;
284
+ this.drawerButton?.setAttribute('aria-expanded', String(open));
285
+ if (!open) {
286
+ const disclosure = drawer.querySelector('.ck-crowdy-studio-embed-hud-preview-disclosure');
287
+ if (disclosure)
288
+ disclosure.open = false;
289
+ this.unmountHudPreview?.();
290
+ this.unmountHudPreview = null;
291
+ }
292
+ }
293
+ async mountStudio() {
294
+ const context = this.context;
295
+ const mountElement = this.mountElement;
296
+ const status = this.statusElement;
297
+ if (!context || !mountElement || !status)
298
+ return;
299
+ const generation = ++this.mountGeneration;
300
+ this.options.onAgentUnmounted?.();
301
+ this.handle?.destroy();
302
+ this.handle = null;
303
+ mountElement.replaceChildren();
304
+ status.replaceChildren();
305
+ status.className = 'ck-crowdy-studio-embed-status is-loading';
306
+ const spinner = document.createElement('span');
307
+ spinner.className = 'ck-crowdy-studio-embed-spinner';
308
+ spinner.setAttribute('aria-hidden', 'true');
309
+ const loading = document.createElement('span');
310
+ loading.textContent = 'Opening your projects…';
311
+ status.append(spinner, loading);
312
+ try {
313
+ const handle = await this.mountSdkStudio(mountElement, context);
314
+ if (generation !== this.mountGeneration || this.root === null) {
315
+ handle.destroy();
316
+ return;
317
+ }
318
+ this.handle = handle;
319
+ this.options.onAgentMounted?.(handle);
320
+ status.className = 'ck-crowdy-studio-embed-status hidden';
321
+ status.replaceChildren();
322
+ this.root.dataset.crowdyStudioApi = handle.api;
323
+ }
324
+ catch (error) {
325
+ if (generation !== this.mountGeneration || this.root === null)
326
+ return;
327
+ this.options.onAgentUnavailable?.(errorMessage(error));
328
+ status.className = 'ck-crowdy-studio-embed-status is-error';
329
+ status.replaceChildren();
330
+ const title = document.createElement('strong');
331
+ title.textContent = 'Crowdy Studio could not open';
332
+ const message = document.createElement('span');
333
+ message.textContent =
334
+ `${errorMessage(error)}. Your grid access is unchanged and no ` +
335
+ 'project data was removed.';
336
+ const retry = document.createElement('button');
337
+ retry.type = 'button';
338
+ retry.className = 'ck-crowdy-studio-embed-retry';
339
+ retry.textContent = 'Retry';
340
+ retry.addEventListener('click', () => void this.mountStudio());
341
+ status.append(title, message, retry);
342
+ }
343
+ }
344
+ /** Assemble mount options from the game client + per-open context. */
345
+ async mountSdkStudio(element, context) {
346
+ const { client } = this.options;
347
+ const appId = this.currentAppId();
348
+ element.className += ' ck-crowdy-studio-embed-sdk-host';
349
+ const hud = context.hud;
350
+ const onPresentation = context.onPresentation ??
351
+ (hud
352
+ ? (presentation) => {
353
+ if (presentation.channel !== 'hud')
354
+ return;
355
+ hud.set({
356
+ source: this.presentationSource(context.gridId),
357
+ label: 'Crowdy Studio preview',
358
+ payload: presentation.payload,
359
+ });
360
+ }
361
+ : undefined);
362
+ const handle = await mountCrowdyStudio(element, {
363
+ projectProvider: client.crowdyStudio,
364
+ playerCompute: client.playerCompute,
365
+ playerWallet: client.playerWallet,
366
+ appId,
367
+ gridId: context.gridId,
368
+ grid: context.grid,
369
+ workerUrl: context.workerUrl,
370
+ targetPermissions: context.targetPermissions,
371
+ onHostCall: context.onHostCall,
372
+ onPresentation,
373
+ ...(client.crowdyStudioAgent && context.playerHost
374
+ ? {
375
+ agent: {
376
+ transport: client.crowdyStudioAgent,
377
+ createSession: {
378
+ appId,
379
+ gridId: context.gridId,
380
+ mode: this.options.agentSession?.mode ?? 'ASK',
381
+ providerDataConsent: this.options.agentSession?.providerDataConsent ?? false,
382
+ idempotencyKey: this.agentIdempotencyKey(),
383
+ },
384
+ playerHost: context.playerHost,
385
+ controlGate: this.options.controlGate,
386
+ onLocalPreempt: this.options.onLocalPreempt,
387
+ },
388
+ }
389
+ : {}),
390
+ ...this.options.runtimeOverrides,
391
+ });
392
+ element.dataset.crowdyStudioApi = 'project-first';
393
+ let destroyed = false;
394
+ return {
395
+ api: 'crowdy-studio',
396
+ controller: handle.controller,
397
+ agent: handle.agent,
398
+ controlLeaseManager: handle.controlLeaseManager,
399
+ destroy: () => {
400
+ if (destroyed)
401
+ return;
402
+ destroyed = true;
403
+ handle.destroy();
404
+ },
405
+ };
406
+ }
407
+ currentAppId() {
408
+ const { appId } = this.options;
409
+ return typeof appId === 'function' ? appId() : appId;
410
+ }
411
+ agentIdempotencyKey() {
412
+ const prefix = this.options.agentSession?.idempotencyKeyPrefix ?? 'ck-agent-session:';
413
+ const random = typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
414
+ ? crypto.randomUUID()
415
+ : `${Date.now()}-${Math.random().toString(16).slice(2)}`;
416
+ return `${prefix}${random}`;
417
+ }
418
+ presentationSource(gridId) {
419
+ return `crowdy-studio:${gridId}`;
420
+ }
421
+ accessibleDescription(gridId) {
422
+ const game = this.options.gameName ?? 'This game';
423
+ return (`${game} live coding studio for grid ${gridId}. ` +
424
+ 'Edits autosave; Test draft or Deploy live applies them. ' +
425
+ `${this.shortcutHint()}`);
426
+ }
427
+ shortcutHint() {
428
+ const key = this.closeKeyLabel();
429
+ return key
430
+ ? `Escape closes. ${key} closes from chrome; ${key} still types in editors.`
431
+ : 'Escape closes.';
432
+ }
433
+ closeKeyLabel() {
434
+ const code = this.closeKeyCode();
435
+ if (!code)
436
+ return null;
437
+ return code.startsWith('Key') ? code.slice(3) : code;
438
+ }
439
+ closeKeyCode() {
440
+ return this.options.closeKeyCode === undefined
441
+ ? 'KeyM'
442
+ : this.options.closeKeyCode;
443
+ }
444
+ updateDisplayMode() {
445
+ if (!this.root)
446
+ return;
447
+ const next = window.innerWidth < CROWDY_STUDIO_EMBED_NARROW_BREAKPOINT_PX
448
+ ? 'fullscreen'
449
+ : 'docked';
450
+ if (next === this.displayMode) {
451
+ if (next === 'docked')
452
+ this.dock.refresh();
453
+ return;
454
+ }
455
+ const previous = this.displayMode;
456
+ this.displayMode = next;
457
+ this.root.dataset.mode = next;
458
+ this.root.className = `ck-crowdy-studio-embed ${next === 'docked' ? 'is-docked' : 'is-fullscreen'}`;
459
+ if (next === 'fullscreen') {
460
+ this.dock.deactivate();
461
+ removeBodyClass('ck-crowdy-studio-embed-docked');
462
+ addBodyClass('ck-crowdy-studio-embed-fullscreen');
463
+ this.clearDockWidth();
464
+ this.root.setAttribute('role', 'dialog');
465
+ this.root.setAttribute('aria-modal', 'true');
466
+ this.enterModalMode();
467
+ this.options.onLayoutChange?.();
468
+ return;
469
+ }
470
+ this.leaveModalMode();
471
+ removeBodyClass('ck-crowdy-studio-embed-fullscreen');
472
+ addBodyClass('ck-crowdy-studio-embed-docked');
473
+ this.root.setAttribute('role', 'complementary');
474
+ this.root.removeAttribute('aria-modal');
475
+ if (previous === null && document.pointerLockElement) {
476
+ void document.exitPointerLock();
477
+ }
478
+ this.dock.activate();
479
+ }
480
+ enterModalMode() {
481
+ if (!this.root || !this.context || this.restoreGameplayInput)
482
+ return;
483
+ this.restoreGameplayInput =
484
+ this.options.suppressGameplayInput?.() ?? (() => { });
485
+ if (document.pointerLockElement)
486
+ void document.exitPointerLock();
487
+ document.addEventListener('keydown', this.documentKeyDown);
488
+ document.addEventListener('keydown', this.captureEscape, true);
489
+ document.addEventListener('keyup', this.documentKeyUp);
490
+ document.addEventListener('focusin', this.keepFocusInside);
491
+ if (!this.root.contains(document.activeElement)) {
492
+ this.root
493
+ .querySelector('.ck-crowdy-studio-embed-close')
494
+ ?.focus();
495
+ }
496
+ }
497
+ leaveModalMode() {
498
+ document.removeEventListener('keydown', this.documentKeyDown);
499
+ document.removeEventListener('keydown', this.captureEscape, true);
500
+ document.removeEventListener('keyup', this.documentKeyUp);
501
+ document.removeEventListener('focusin', this.keepFocusInside);
502
+ this.restoreGameplayInput?.();
503
+ this.restoreGameplayInput = null;
504
+ }
505
+ applyDockWidth(width) {
506
+ if (!this.root || this.displayMode !== 'docked')
507
+ return;
508
+ document.body.style.setProperty('--ck-crowdy-studio-embed-dock-width', `${width}px`);
509
+ document.body.style.setProperty('--ck-game-right-inset', `${width}px`);
510
+ this.options.onLayoutChange?.();
511
+ }
512
+ clearDockWidth() {
513
+ document.body.style.removeProperty('--ck-crowdy-studio-embed-dock-width');
514
+ document.body.style.removeProperty('--ck-game-right-inset');
515
+ }
516
+ handleDialogKey(event) {
517
+ if (!this.root)
518
+ return;
519
+ if (event.code === 'Escape' && !event.repeat) {
520
+ event.preventDefault();
521
+ event.stopPropagation();
522
+ this.close();
523
+ return;
524
+ }
525
+ const closeKey = this.closeKeyCode();
526
+ if (closeKey !== null &&
527
+ event.code === closeKey &&
528
+ !event.repeat &&
529
+ !event.altKey &&
530
+ !event.ctrlKey &&
531
+ !event.metaKey &&
532
+ !isTextEntryTarget(event.target)) {
533
+ event.preventDefault();
534
+ event.stopPropagation();
535
+ this.close();
536
+ return;
537
+ }
538
+ if (event.code === 'Tab' && this.modal)
539
+ this.trapTab(event);
540
+ event.stopPropagation();
541
+ }
542
+ trapTab(event) {
543
+ if (!this.root)
544
+ return;
545
+ const focusable = focusableElements(this.root);
546
+ if (focusable.length === 0) {
547
+ event.preventDefault();
548
+ this.root.focus();
549
+ return;
550
+ }
551
+ const active = document.activeElement;
552
+ const index = focusable.indexOf(active);
553
+ if (event.shiftKey && index <= 0) {
554
+ event.preventDefault();
555
+ focusable.at(-1)?.focus();
556
+ }
557
+ else if (!event.shiftKey &&
558
+ (index < 0 || index === focusable.length - 1)) {
559
+ event.preventDefault();
560
+ focusable[0]?.focus();
561
+ }
562
+ }
563
+ }
564
+ /** Convenience factory mirroring the class constructor. */
565
+ export function createCrowdyStudioEmbed(options) {
566
+ return new CrowdyStudioEmbed(options);
567
+ }
568
+ function contextPill(label, value) {
569
+ const pill = document.createElement('span');
570
+ pill.className = 'ck-crowdy-studio-embed-context-pill';
571
+ const key = document.createElement('small');
572
+ key.textContent = label;
573
+ const text = document.createElement('strong');
574
+ text.textContent = value;
575
+ pill.append(key, text);
576
+ return pill;
577
+ }
578
+ function appendDefinition(list, label, value) {
579
+ const term = document.createElement('dt');
580
+ term.textContent = label;
581
+ const description = document.createElement('dd');
582
+ description.textContent = value;
583
+ list.append(term, description);
584
+ }
585
+ function permissionCard(target, permissions) {
586
+ const card = document.createElement('section');
587
+ card.className =
588
+ !permissions.canWrite && !permissions.canRun
589
+ ? 'ck-crowdy-studio-embed-permission is-disabled'
590
+ : 'ck-crowdy-studio-embed-permission';
591
+ const title = document.createElement('strong');
592
+ title.textContent = target;
593
+ const write = permissionBadge('Write', permissions.canWrite);
594
+ const run = permissionBadge('Run', permissions.canRun);
595
+ card.append(title, write, run);
596
+ return card;
597
+ }
598
+ function permissionBadge(label, allowed) {
599
+ const badge = document.createElement('span');
600
+ badge.className = allowed ? 'is-allowed' : 'is-denied';
601
+ badge.textContent = `${label} ${allowed ? 'allowed' : 'unavailable'}`;
602
+ return badge;
603
+ }
604
+ function permissionSummary(permissions) {
605
+ const target = (label, value) => {
606
+ const capabilities = [
607
+ value.canWrite ? 'write' : '',
608
+ value.canRun ? 'run' : '',
609
+ ].filter(Boolean);
610
+ return `${label} ${capabilities.join('/') || 'unavailable'}`;
611
+ };
612
+ return [
613
+ target('SERVER', permissions.SERVER),
614
+ target('CLIENT', permissions.CLIENT),
615
+ ].join(' · ');
616
+ }
617
+ function formatChunk(chunk) {
618
+ return `${chunk.x}, ${chunk.y}, ${chunk.z}`;
619
+ }
620
+ function focusableElements(root) {
621
+ return Array.from(root.querySelectorAll('button:not([disabled]), a[href], input:not([disabled]), ' +
622
+ 'select:not([disabled]), textarea:not([disabled]), ' +
623
+ '[contenteditable="true"], [tabindex]:not([tabindex="-1"])')).filter((element) => !element.hidden &&
624
+ element.getAttribute('aria-hidden') !== 'true' &&
625
+ element.closest('.hidden') === null);
626
+ }
627
+ function isTextEntryTarget(target) {
628
+ if (!(target instanceof Element))
629
+ return false;
630
+ return (target instanceof HTMLInputElement ||
631
+ target instanceof HTMLTextAreaElement ||
632
+ target instanceof HTMLSelectElement ||
633
+ (target instanceof HTMLElement && target.isContentEditable) ||
634
+ target.closest('[contenteditable="true"], [role="textbox"], .monaco-editor, ' +
635
+ '.ck-crowdy-studio-editor') !== null);
636
+ }
637
+ function errorMessage(error) {
638
+ return error instanceof Error ? error.message : String(error);
639
+ }
640
+ function stopEventPropagation(event) {
641
+ event.stopPropagation();
642
+ }
@@ -8,6 +8,9 @@ export { EMBEDDED_PLATFORM_INDEX, MAX_PLATFORM_CRATES, MAX_PLATFORM_CRATE_FIELD_
8
8
  export { DEFAULT_VFS_LIMITS, VfsLimitError, VirtualFileSystem, offsetAt, type VirtualDocument, type VirtualFileSystemLimits, } from '../live-coding/vfs.js';
9
9
  export { WorkerLanguageClient, WorkerMessageReader, WorkerMessageWriter, createDefaultRustLanguageWorker, type LanguageWorkerLike, type WorkerLanguageClientOptions, } from '../live-coding/worker-transport.js';
10
10
  export { CrowdyStudioAgentDomShell, type CrowdyStudioAgentDomShellOptions, } from './agent-dom-shell.js';
11
+ export { STUDIO_LAYOUT_STORAGE_KEY, STUDIO_PANE_IDS, StudioLayoutController, clampStudioPaneSize, studioPaneSizeRange, type StudioLayoutControllerOptions, type StudioLayoutListener, type StudioLayoutState, type StudioLayoutStorage, type StudioPaneId, type StudioPaneSizeRange, } from './layout.js';
12
+ export { createPaneSplitter, type PaneSplitterHandle, type PaneSplitterOptions, type PaneSplitterRange, } from './splitter.js';
13
+ export * from './embed/index.js';
11
14
  export * from '../crowdy-agent/index.js';
12
15
  export * from '../player-host/index.js';
13
16
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/crowdy-studio/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EACtB,KAAK,6BAA6B,EAClC,KAAK,kBAAkB,EACvB,KAAK,4BAA4B,EACjC,KAAK,0BAA0B,EAC/B,KAAK,wBAAwB,EAC7B,KAAK,wBAAwB,EAC7B,KAAK,iBAAiB,EACtB,KAAK,yBAAyB,EAC9B,KAAK,eAAe,EACpB,KAAK,yBAAyB,EAC9B,KAAK,uBAAuB,EAC5B,KAAK,4BAA4B,EACjC,KAAK,yBAAyB,EAC9B,KAAK,wBAAwB,EAC7B,KAAK,iBAAiB,EACtB,KAAK,sBAAsB,EAC3B,KAAK,yBAAyB,EAC9B,KAAK,0BAA0B,GAChC,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,iBAAiB,EACjB,KAAK,kBAAkB,EACvB,KAAK,6BAA6B,EAClC,KAAK,wBAAwB,GAC9B,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,wBAAwB,EACxB,iCAAiC,EACjC,wBAAwB,EACxB,mBAAmB,EACnB,mBAAmB,EACnB,yBAAyB,EACzB,cAAc,EACd,KAAK,8BAA8B,EACnC,KAAK,oCAAoC,EACzC,KAAK,4BAA4B,EACjC,KAAK,4BAA4B,EACjC,KAAK,6BAA6B,EAClC,KAAK,0BAA0B,EAC/B,KAAK,8BAA8B,EACnC,KAAK,kCAAkC,EACvC,KAAK,mCAAmC,EACxC,KAAK,mBAAmB,EACxB,KAAK,6BAA6B,EAClC,KAAK,mBAAmB,EACxB,KAAK,uBAAuB,EAC5B,KAAK,uBAAuB,EAC5B,KAAK,2BAA2B,EAChC,KAAK,2BAA2B,EAChC,KAAK,2BAA2B,EAChC,KAAK,wBAAwB,EAC7B,KAAK,0BAA0B,EAC/B,KAAK,yBAAyB,EAC9B,KAAK,qBAAqB,EAC1B,KAAK,mCAAmC,EACxC,KAAK,kCAAkC,EACvC,KAAK,0BAA0B,EAC/B,KAAK,kBAAkB,EACvB,KAAK,gCAAgC,EACrC,KAAK,4BAA4B,GAClC,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,gCAAgC,EAChC,KAAK,6BAA6B,GACnC,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,qBAAqB,EACrB,KAAK,sBAAsB,EAC3B,KAAK,8BAA8B,EACnC,KAAK,4BAA4B,GAClC,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,0BAA0B,EAC1B,KAAK,+BAA+B,GACrC,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,uBAAuB,EACvB,mBAAmB,EACnB,+BAA+B,EAC/B,wBAAwB,EACxB,+BAA+B,EAC/B,iBAAiB,EACjB,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,kBAAkB,GACxB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EACL,kBAAkB,EAClB,aAAa,EACb,iBAAiB,EACjB,QAAQ,EACR,KAAK,eAAe,EACpB,KAAK,uBAAuB,GAC7B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,oBAAoB,EACpB,mBAAmB,EACnB,mBAAmB,EACnB,+BAA+B,EAC/B,KAAK,kBAAkB,EACvB,KAAK,2BAA2B,GACjC,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EACL,yBAAyB,EACzB,KAAK,gCAAgC,GACtC,MAAM,sBAAsB,CAAC;AAC9B,cAAc,0BAA0B,CAAC;AACzC,cAAc,yBAAyB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/crowdy-studio/index.ts"],"names":[],"mappings":"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"}
@@ -8,5 +8,8 @@ export { EMBEDDED_PLATFORM_INDEX, MAX_PLATFORM_CRATES, MAX_PLATFORM_CRATE_FIELD_
8
8
  export { DEFAULT_VFS_LIMITS, VfsLimitError, VirtualFileSystem, offsetAt, } from '../live-coding/vfs.js';
9
9
  export { WorkerLanguageClient, WorkerMessageReader, WorkerMessageWriter, createDefaultRustLanguageWorker, } from '../live-coding/worker-transport.js';
10
10
  export { CrowdyStudioAgentDomShell, } from './agent-dom-shell.js';
11
+ export { STUDIO_LAYOUT_STORAGE_KEY, STUDIO_PANE_IDS, StudioLayoutController, clampStudioPaneSize, studioPaneSizeRange, } from './layout.js';
12
+ export { createPaneSplitter, } from './splitter.js';
13
+ export * from './embed/index.js';
11
14
  export * from '../crowdy-agent/index.js';
12
15
  export * from '../player-host/index.js';