@jxsuite/studio 0.28.5 → 0.30.1

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 (86) hide show
  1. package/dist/studio.css +98 -98
  2. package/dist/studio.js +9368 -6586
  3. package/dist/studio.js.map +109 -83
  4. package/dist/workers/editor.worker.js +79 -79
  5. package/dist/workers/json.worker.js +109 -109
  6. package/dist/workers/ts.worker.js +82 -82
  7. package/package.json +11 -10
  8. package/src/browse/browse-modal.ts +2 -2
  9. package/src/browse/browse.ts +20 -19
  10. package/src/canvas/canvas-diff.ts +3 -3
  11. package/src/canvas/canvas-helpers.ts +15 -2
  12. package/src/canvas/canvas-live-render.ts +168 -86
  13. package/src/canvas/canvas-patcher.ts +656 -0
  14. package/src/canvas/canvas-perf.ts +68 -0
  15. package/src/canvas/canvas-render.ts +135 -23
  16. package/src/canvas/canvas-subtree-render.ts +113 -0
  17. package/src/canvas/canvas-utils.ts +4 -0
  18. package/src/editor/component-inline-edit.ts +4 -35
  19. package/src/editor/context-menu.ts +96 -76
  20. package/src/editor/convert-to-component.ts +11 -2
  21. package/src/editor/convert-to-repeater.ts +4 -5
  22. package/src/editor/inline-edit.ts +9 -9
  23. package/src/editor/inline-format.ts +11 -11
  24. package/src/editor/merge-tags.ts +120 -0
  25. package/src/editor/shortcuts.ts +4 -4
  26. package/src/files/components.ts +37 -0
  27. package/src/files/file-ops.ts +28 -7
  28. package/src/files/files.ts +68 -24
  29. package/src/files/fs-events.ts +162 -0
  30. package/src/github/github-auth.ts +14 -2
  31. package/src/github/github-publish.ts +12 -2
  32. package/src/panels/activity-bar.ts +1 -1
  33. package/src/panels/ai-panel.ts +67 -39
  34. package/src/panels/block-action-bar.ts +72 -1
  35. package/src/panels/canvas-dnd.ts +55 -26
  36. package/src/panels/data-explorer.ts +5 -3
  37. package/src/panels/dnd.ts +14 -17
  38. package/src/panels/editors.ts +5 -23
  39. package/src/panels/elements-panel.ts +2 -12
  40. package/src/panels/events-panel.ts +1 -1
  41. package/src/panels/git-panel.ts +6 -6
  42. package/src/panels/head-panel.ts +1 -1
  43. package/src/panels/layers-panel.ts +177 -147
  44. package/src/panels/preview-render.ts +15 -0
  45. package/src/panels/properties-panel.ts +16 -27
  46. package/src/panels/quick-search.ts +2 -2
  47. package/src/panels/right-panel.ts +2 -6
  48. package/src/panels/shared.ts +3 -0
  49. package/src/panels/signals-panel.ts +43 -33
  50. package/src/panels/statusbar.ts +15 -6
  51. package/src/panels/style-panel.ts +3 -3
  52. package/src/panels/style-utils.ts +3 -3
  53. package/src/panels/stylebook-panel.ts +4 -4
  54. package/src/panels/tab-bar.ts +198 -0
  55. package/src/panels/tab-strip.ts +2 -2
  56. package/src/panels/toolbar.ts +6 -75
  57. package/src/platforms/devserver.ts +113 -35
  58. package/src/reactivity.ts +2 -0
  59. package/src/services/cem-export.ts +23 -2
  60. package/src/services/code-services.ts +16 -2
  61. package/src/services/monaco-setup.ts +2 -1
  62. package/src/settings/content-types-editor.ts +16 -16
  63. package/src/settings/css-vars-editor.ts +2 -2
  64. package/src/settings/defs-editor.ts +14 -14
  65. package/src/settings/general-settings.ts +6 -6
  66. package/src/settings/head-editor.ts +2 -2
  67. package/src/site-context.ts +1 -1
  68. package/src/state.ts +66 -12
  69. package/src/store.ts +15 -3
  70. package/src/studio.ts +88 -35
  71. package/src/tabs/patch-ops.ts +143 -0
  72. package/src/tabs/tab.ts +15 -1
  73. package/src/tabs/transact.ts +454 -27
  74. package/src/types.ts +71 -1
  75. package/src/ui/color-selector.ts +7 -7
  76. package/src/ui/icons.ts +0 -30
  77. package/src/ui/media-picker.ts +2 -2
  78. package/src/ui/panel-resize.ts +14 -8
  79. package/src/ui/unit-selector.ts +5 -2
  80. package/src/ui/value-selector.ts +3 -3
  81. package/src/utils/canvas-media.ts +6 -6
  82. package/src/utils/edit-display.ts +125 -83
  83. package/src/utils/google-fonts.ts +1 -1
  84. package/src/utils/inherited-style.ts +3 -2
  85. package/src/utils/studio-utils.ts +1 -1
  86. package/src/view.ts +4 -1
@@ -5,13 +5,10 @@
5
5
  */
6
6
 
7
7
  import { html, render as litRender, nothing } from "lit-html";
8
- import { updateSession, updateUi } from "../store";
8
+ import { updateSession } from "../store";
9
9
  import { redo as tabRedo, undo as tabUndo } from "../tabs/transact";
10
10
  import { effect, effectScope } from "../reactivity";
11
11
  import { activeTab } from "../workspace/workspace";
12
- import type { DocumentStackEntry } from "../types";
13
- import { getEffectiveMedia } from "../site-context";
14
- import { mediaDisplayName } from "./shared";
15
12
  import { applyPanelCollapse, view } from "../view";
16
13
  import { getRecentProjects } from "../recent-projects";
17
14
  import { openQuickSearch } from "./quick-search";
@@ -23,21 +20,9 @@ import type { EffectScope } from "@vue/reactivity";
23
20
  import type { TemplateResult } from "lit-html";
24
21
 
25
22
  interface ToolbarCtx {
26
- navigateBack: () => void;
27
- navigateToLevel: (level: number) => void;
28
23
  openProject: () => void;
29
24
  openFile?: (path: string) => void;
30
25
  saveFile: () => void;
31
- parseMediaEntries: (media: Record<string, string> | null | undefined) => {
32
- sizeBreakpoints: {
33
- name: string;
34
- query: string;
35
- width: number;
36
- type: string;
37
- }[];
38
- featureQueries: { name: string; query: string }[];
39
- baseWidth: number;
40
- };
41
26
  getCanvasMode: () => string;
42
27
  setCanvasMode: (mode: string) => void;
43
28
  renderCanvas: () => void;
@@ -54,7 +39,6 @@ let _scope: EffectScope | null = null;
54
39
 
55
40
  const toolbarIconMap = {
56
41
  "sp-icon-artboard": html`<sp-icon-artboard slot="icon"></sp-icon-artboard>`,
57
- "sp-icon-back": html`<sp-icon-back slot="icon"></sp-icon-back>`,
58
42
  "sp-icon-brush": html`<sp-icon-brush slot="icon"></sp-icon-brush>`,
59
43
  "sp-icon-code": html`<sp-icon-code slot="icon"></sp-icon-code>`,
60
44
  "sp-icon-delete": html`<sp-icon-delete slot="icon"></sp-icon-delete>`,
@@ -160,9 +144,9 @@ function minimalToolbarTemplate(ctx: ToolbarCtx) {
160
144
  @change=${(e: Event) => {
161
145
  const val = (e.target as unknown as HTMLInputElement).value;
162
146
  if (val === "__new__") {
163
- handleNewProject();
147
+ void handleNewProject();
164
148
  } else {
165
- ctx.openRecentProject(val);
149
+ void ctx.openRecentProject(val);
166
150
  }
167
151
  }}
168
152
  >
@@ -298,65 +282,12 @@ function toolbarTemplate() {
298
282
 
299
283
  const S = {
300
284
  dirty: tab.doc.dirty,
301
- document: tab.doc.document,
302
- documentPath: tab.documentPath,
303
- documentStack: tab.session.documentStack,
304
285
  fileHandle: tab.fileHandle,
305
286
  mode: tab.doc.mode,
306
287
  selection: tab.session.selection,
307
288
  ui: tab.session.ui,
308
289
  };
309
290
  const canvasMode = ctx.getCanvasMode();
310
- const hasStack = S.documentStack && S.documentStack.length > 0;
311
-
312
- const breadcrumbTpl = hasStack
313
- ? html`
314
- <div class="breadcrumb">
315
- <sp-action-button size="s" title="Return to parent document" @click=${ctx.navigateBack}>
316
- ${toolbarIconMap["sp-icon-back"]}Back
317
- </sp-action-button>
318
- ${S.documentStack.map(
319
- (frame: DocumentStackEntry, i: number) => html`
320
- <span class="breadcrumb-item clickable" @click=${() => ctx.navigateToLevel(i)}
321
- >${frame.documentPath?.split("/").pop() || "untitled"}</span
322
- >
323
- <span class="breadcrumb-sep"> › </span>
324
- `,
325
- )}
326
- <span class="breadcrumb-item current">
327
- ${S.documentPath?.split("/").pop() || S.document.tagName || "document"}
328
- </span>
329
- </div>
330
- `
331
- : nothing;
332
-
333
- const { featureQueries } = ctx.parseMediaEntries(getEffectiveMedia(S.document.$media));
334
- const togglesTpl =
335
- featureQueries.length > 0
336
- ? html`
337
- <sp-action-group compact size="s">
338
- ${featureQueries.map(
339
- (/** @type {{ name: string; query: string }} */ { name, query }) => html`
340
- <sp-action-button
341
- toggles
342
- size="s"
343
- title=${query}
344
- ?selected=${Boolean(S.ui.featureToggles[name])}
345
- @click=${() => {
346
- const newToggles = {
347
- ...S.ui.featureToggles,
348
- [name]: !S.ui.featureToggles[name],
349
- };
350
- updateUi("featureToggles", newToggles);
351
- }}
352
- >
353
- ${mediaDisplayName(name)}
354
- </sp-action-button>
355
- `,
356
- )}
357
- </sp-action-group>
358
- `
359
- : nothing;
360
291
 
361
292
  const modeSwitcherTpl = html`
362
293
  <sp-action-group selects="single" size="s" compact>
@@ -488,9 +419,9 @@ function toolbarTemplate() {
488
419
  @change=${(e: Event) => {
489
420
  const val = (e.target as unknown as HTMLInputElement).value;
490
421
  if (val === "__new__") {
491
- handleNewProject();
422
+ void handleNewProject();
492
423
  } else {
493
- ctx.openRecentProject(val);
424
+ void ctx.openRecentProject(val);
494
425
  }
495
426
  }}
496
427
  >
@@ -543,7 +474,7 @@ function toolbarTemplate() {
543
474
  </sp-action-button>`
544
475
  : nothing}
545
476
  <div class="tb-spacer"></div>
546
- ${breadcrumbTpl} ${togglesTpl} ${modeSwitcherTpl}
477
+ ${modeSwitcherTpl}
547
478
  <sp-action-button
548
479
  quiet
549
480
  size="s"
@@ -8,6 +8,26 @@
8
8
  * See spec/desktop.md §8 for the full specification.
9
9
  */
10
10
 
11
+ import type { ProjectConfig } from "@jxsuite/schema/types";
12
+ import type { DirEntry, FsEvent, RenameResult } from "../types";
13
+
14
+ /** A directory entry from the server, tolerating extra wire fields. */
15
+ type WireDirEntry = DirEntry & Record<string, unknown>;
16
+
17
+ /** Parse a fetch Response body as JSON, asserting the expected shape at the boundary. */
18
+ async function readJson<T>(res: Response): Promise<T> {
19
+ return (await res.json()) as T;
20
+ }
21
+
22
+ interface ErrorBody {
23
+ error?: string;
24
+ }
25
+
26
+ interface SiteEntry {
27
+ config: unknown;
28
+ path: string;
29
+ }
30
+
11
31
  /**
12
32
  * Create a DevServerPlatform instance.
13
33
  *
@@ -58,7 +78,7 @@ export function createDevServerPlatform() {
58
78
  set projectRoot(v) {
59
79
  _projectRoot = v || "";
60
80
  if (_projectRoot) {
61
- this.activate(_projectRoot);
81
+ void this.activate(_projectRoot);
62
82
  }
63
83
  },
64
84
 
@@ -109,19 +129,16 @@ export function createDevServerPlatform() {
109
129
  }
110
130
 
111
131
  const file = await siteHandle.getFile();
112
- const config = JSON.parse(await file.text());
132
+ const config = JSON.parse(await file.text()) as ProjectConfig;
113
133
 
114
134
  // Resolve server-relative path by matching against known sites
115
135
  const sitesRes = await fetch("/__studio/sites");
116
136
  if (!sitesRes.ok) {
117
137
  throw new Error("Failed to fetch site list from server");
118
138
  }
119
- const sites = await sitesRes.json();
139
+ const sites = await readJson<SiteEntry[]>(sitesRes);
120
140
  const match = sites.find(
121
- /** @param {{ config: unknown; path: string }} s */ (s: {
122
- config: unknown;
123
- path: string;
124
- }) => JSON.stringify(s.config) === JSON.stringify(config),
141
+ (s: SiteEntry) => JSON.stringify(s.config) === JSON.stringify(config),
125
142
  );
126
143
 
127
144
  if (!match) {
@@ -132,7 +149,7 @@ export function createDevServerPlatform() {
132
149
  if (!findRes.ok) {
133
150
  throw new Error("Could not locate project on disk");
134
151
  }
135
- const found = await findRes.json();
152
+ const found = await readJson<{ path?: string }>(findRes);
136
153
  if (!found.path) {
137
154
  throw new Error(`Could not find project directory "${dirHandle.name}"`);
138
155
  }
@@ -147,7 +164,7 @@ export function createDevServerPlatform() {
147
164
  return {
148
165
  config,
149
166
  handle: {
150
- name: config.name || _projectRoot.split("/").pop(),
167
+ name: config.name || _projectRoot.split("/").pop()!,
151
168
  projectConfig: config,
152
169
  root: _projectRoot,
153
170
  },
@@ -164,8 +181,17 @@ export function createDevServerPlatform() {
164
181
  fetch("/__studio/project"),
165
182
  fetch("/__studio/project-info?dir=."),
166
183
  ]);
167
- const meta = projectRes.ok ? await projectRes.json() : { name: "project", root: "." };
168
- const info = infoRes.ok ? await infoRes.json() : { isSiteProject: false };
184
+ const meta = projectRes.ok
185
+ ? await readJson<{ name: string; root: string }>(projectRes)
186
+ : { name: "project", root: "." };
187
+ const info = infoRes.ok
188
+ ? await readJson<{
189
+ isSiteProject: boolean;
190
+ projectConfig?: ProjectConfig | null;
191
+ directories?: string[];
192
+ [key: string]: unknown;
193
+ }>(infoRes)
194
+ : { isSiteProject: false };
169
195
  return { info, meta };
170
196
  } catch {
171
197
  return null;
@@ -196,7 +222,7 @@ export function createDevServerPlatform() {
196
222
  method: "POST",
197
223
  });
198
224
  if (!res.ok) {
199
- const data = await res.json();
225
+ const data = await readJson<ErrorBody>(res);
200
226
  throw new Error(data.error || "Failed to create project");
201
227
  }
202
228
  return await res.json();
@@ -210,7 +236,7 @@ export function createDevServerPlatform() {
210
236
  if (!res.ok) {
211
237
  throw new Error(`Failed to list directory: ${dir}`);
212
238
  }
213
- const entries = await res.json();
239
+ const entries = await readJson<WireDirEntry[]>(res);
214
240
  for (const e of entries) {
215
241
  e.path = stripRoot(e.path);
216
242
  }
@@ -223,7 +249,7 @@ export function createDevServerPlatform() {
223
249
  if (!res.ok) {
224
250
  throw new Error(`Failed to read file: ${path}`);
225
251
  }
226
- const data = await res.json();
252
+ const data = await readJson<{ content: string }>(res);
227
253
  return data.content;
228
254
  },
229
255
 
@@ -272,7 +298,7 @@ export function createDevServerPlatform() {
272
298
  * @param {string} from
273
299
  * @param {string} to
274
300
  */
275
- async renameFile(from: string, to: string) {
301
+ async renameFile(from: string, to: string): Promise<RenameResult> {
276
302
  const res = await fetch("/__studio/file/rename", {
277
303
  body: JSON.stringify({ from: serverPath(from), to: serverPath(to) }),
278
304
  headers: { "Content-Type": "application/json" },
@@ -281,6 +307,58 @@ export function createDevServerPlatform() {
281
307
  if (!res.ok) {
282
308
  throw new Error(`Failed to rename: ${from} → ${to}`);
283
309
  }
310
+ const report = await readJson<RenameResult>(res);
311
+ // Map server-root-relative report paths back to project-relative for the studio.
312
+ if (typeof report.from === "string") {
313
+ report.from = stripRoot(report.from);
314
+ }
315
+ if (typeof report.to === "string") {
316
+ report.to = stripRoot(report.to);
317
+ }
318
+ for (const f of report.references?.files ?? []) {
319
+ f.path = stripRoot(f.path);
320
+ }
321
+ for (const e of report.errors ?? []) {
322
+ e.path = stripRoot(e.path);
323
+ }
324
+ return report;
325
+ },
326
+
327
+ /**
328
+ * Subscribe to filesystem change events over the dev server's SSE stream. Listens for the named
329
+ * "fs" event (the preview iframe's default `onmessage` ignores it), strips paths to
330
+ * project-relative, and drops events for sibling projects outside the active root.
331
+ */
332
+ subscribeFileEvents(handler: (events: FsEvent[]) => void) {
333
+ if (typeof EventSource === "undefined") {
334
+ return () => {};
335
+ }
336
+ const es = new EventSource("/__reload");
337
+ es.addEventListener("fs", (ev: MessageEvent) => {
338
+ let payload: { events?: FsEvent[] };
339
+ try {
340
+ payload = JSON.parse(ev.data as string) as { events?: FsEvent[] };
341
+ } catch {
342
+ return;
343
+ }
344
+ const events: FsEvent[] = [];
345
+ for (const e of payload.events ?? []) {
346
+ const raw = e.path.replaceAll("\\", "/");
347
+ if (_projectRoot && raw !== _projectRoot && !raw.startsWith(`${_projectRoot}/`)) {
348
+ continue;
349
+ }
350
+ const path = stripRoot(raw);
351
+ if (path && !path.startsWith("..")) {
352
+ events.push({ isDir: e.isDir, path, type: e.type });
353
+ }
354
+ }
355
+ if (events.length > 0) {
356
+ handler(events);
357
+ }
358
+ });
359
+ return () => {
360
+ es.close();
361
+ };
284
362
  },
285
363
 
286
364
  /** @param {string} _path */
@@ -392,7 +470,7 @@ export function createDevServerPlatform() {
392
470
  method: "POST",
393
471
  });
394
472
  if (res.ok) {
395
- const body = await res.json();
473
+ const body = await readJson<{ path?: string }>(res);
396
474
  return body.path || null;
397
475
  }
398
476
  } catch {}
@@ -412,7 +490,7 @@ export function createDevServerPlatform() {
412
490
  if (!res.ok) {
413
491
  return [];
414
492
  }
415
- const entries = await res.json();
493
+ const entries = await readJson<WireDirEntry[]>(res);
416
494
  for (const e of entries) {
417
495
  e.path = stripRoot(e.path);
418
496
  }
@@ -427,7 +505,7 @@ export function createDevServerPlatform() {
427
505
  if (!res.ok) {
428
506
  return [];
429
507
  }
430
- const body = await res.json();
508
+ const body = await readJson<{ formats?: unknown[] }>(res);
431
509
  return body.formats ?? [];
432
510
  },
433
511
 
@@ -442,7 +520,7 @@ export function createDevServerPlatform() {
442
520
  headers: { "Content-Type": "application/json" },
443
521
  method: "POST",
444
522
  });
445
- const data = await res.json();
523
+ const data = await readJson<{ error?: string; result?: unknown }>(res);
446
524
  if (!res.ok) {
447
525
  throw new Error(data.error || "Format action failed");
448
526
  }
@@ -468,7 +546,7 @@ export function createDevServerPlatform() {
468
546
  if (!res.ok) {
469
547
  return null;
470
548
  }
471
- const { schema } = await res.json();
549
+ const { schema } = await readJson<{ schema: unknown }>(res);
472
550
  return schema;
473
551
  },
474
552
 
@@ -508,7 +586,7 @@ export function createDevServerPlatform() {
508
586
  method: "POST",
509
587
  });
510
588
  if (!res.ok) {
511
- const body = await res.json();
589
+ const body = await readJson<ErrorBody>(res);
512
590
  throw new Error(body.error);
513
591
  }
514
592
  return await res.json();
@@ -522,7 +600,7 @@ export function createDevServerPlatform() {
522
600
  method: "POST",
523
601
  });
524
602
  if (!res.ok) {
525
- const body = await res.json();
603
+ const body = await readJson<ErrorBody>(res);
526
604
  throw new Error(body.error);
527
605
  }
528
606
  return await res.json();
@@ -536,7 +614,7 @@ export function createDevServerPlatform() {
536
614
  method: "POST",
537
615
  });
538
616
  if (!res.ok) {
539
- const body = await res.json();
617
+ const body = await readJson<ErrorBody>(res);
540
618
  throw new Error(body.error);
541
619
  }
542
620
  return await res.json();
@@ -550,7 +628,7 @@ export function createDevServerPlatform() {
550
628
  method: "POST",
551
629
  });
552
630
  if (!res.ok) {
553
- const body = await res.json();
631
+ const body = await readJson<ErrorBody>(res);
554
632
  throw new Error(body.error);
555
633
  }
556
634
  return await res.json();
@@ -559,7 +637,7 @@ export function createDevServerPlatform() {
559
637
  async gitPull() {
560
638
  const res = await fetch("/__studio/git/pull", { method: "POST" });
561
639
  if (!res.ok) {
562
- const body = await res.json();
640
+ const body = await readJson<ErrorBody>(res);
563
641
  throw new Error(body.error);
564
642
  }
565
643
  return await res.json();
@@ -568,7 +646,7 @@ export function createDevServerPlatform() {
568
646
  async gitFetch() {
569
647
  const res = await fetch("/__studio/git/fetch", { method: "POST" });
570
648
  if (!res.ok) {
571
- const body = await res.json();
649
+ const body = await readJson<ErrorBody>(res);
572
650
  throw new Error(body.error);
573
651
  }
574
652
  return await res.json();
@@ -582,7 +660,7 @@ export function createDevServerPlatform() {
582
660
  method: "POST",
583
661
  });
584
662
  if (!res.ok) {
585
- const body = await res.json();
663
+ const body = await readJson<ErrorBody>(res);
586
664
  throw new Error(body.error);
587
665
  }
588
666
  return await res.json();
@@ -596,7 +674,7 @@ export function createDevServerPlatform() {
596
674
  method: "POST",
597
675
  });
598
676
  if (!res.ok) {
599
- const body = await res.json();
677
+ const body = await readJson<ErrorBody>(res);
600
678
  throw new Error(body.error);
601
679
  }
602
680
  return await res.json();
@@ -621,7 +699,7 @@ export function createDevServerPlatform() {
621
699
  if (!res.ok) {
622
700
  throw new Error(await res.text());
623
701
  }
624
- const data = await res.json();
702
+ const data = await readJson<{ content: string }>(res);
625
703
  return data.content;
626
704
  },
627
705
 
@@ -633,7 +711,7 @@ export function createDevServerPlatform() {
633
711
  method: "POST",
634
712
  });
635
713
  if (!res.ok) {
636
- const body = await res.json();
714
+ const body = await readJson<ErrorBody>(res);
637
715
  throw new Error(body.error);
638
716
  }
639
717
  return await res.json();
@@ -647,7 +725,7 @@ export function createDevServerPlatform() {
647
725
  method: "POST",
648
726
  });
649
727
  if (!res.ok) {
650
- const body = await res.json();
728
+ const body = await readJson<ErrorBody>(res);
651
729
  throw new Error(body.error);
652
730
  }
653
731
  return await res.json();
@@ -656,7 +734,7 @@ export function createDevServerPlatform() {
656
734
  async gitInit() {
657
735
  const res = await fetch("/__studio/git/init", { method: "POST" });
658
736
  if (!res.ok) {
659
- const body = await res.json();
737
+ const body = await readJson<ErrorBody>(res);
660
738
  throw new Error(body.error);
661
739
  }
662
740
  },
@@ -672,7 +750,7 @@ export function createDevServerPlatform() {
672
750
  method: "POST",
673
751
  });
674
752
  if (!res.ok) {
675
- const body = await res.json();
753
+ const body = await readJson<ErrorBody>(res);
676
754
  throw new Error(body.error);
677
755
  }
678
756
  },
@@ -692,7 +770,7 @@ export function createDevServerPlatform() {
692
770
  method: "POST",
693
771
  });
694
772
  if (!res.ok) {
695
- const body = await res.json();
773
+ const body = await readJson<ErrorBody>(res);
696
774
  throw new Error(body.error);
697
775
  }
698
776
  return await res.json();
@@ -706,7 +784,7 @@ export function createDevServerPlatform() {
706
784
  method: "POST",
707
785
  });
708
786
  if (!res.ok) {
709
- const body = await res.json();
787
+ const body = await readJson<ErrorBody>(res);
710
788
  throw new Error(body.error);
711
789
  }
712
790
  return await res.json();
package/src/reactivity.ts CHANGED
@@ -26,3 +26,5 @@ export {
26
26
  isReactive,
27
27
  isRef,
28
28
  } from "@vue/reactivity";
29
+
30
+ export type { EffectScope } from "@vue/reactivity";
@@ -2,11 +2,11 @@
2
2
  import type { CemParameter, JxMutableNode, JxStateObject } from "@jxsuite/schema/types";
3
3
  import { isFunctionDef, isJsonObject } from "@jxsuite/schema/guards";
4
4
 
5
- /** Collect slot elements from the document tree. */
5
+ /** Collect slot elements from the document tree. Whitespace-only names count as unnamed. */
6
6
  export function collectSlots(node?: JxMutableNode | null, slots: string[] = []) {
7
7
  if (node?.tagName === "slot") {
8
8
  const name = node.attributes?.name;
9
- slots.push(typeof name === "string" ? name : "");
9
+ slots.push(typeof name === "string" ? name.trim() : "");
10
10
  }
11
11
  if (Array.isArray(node?.children)) {
12
12
  for (const c of node.children) {
@@ -16,6 +16,27 @@ export function collectSlots(node?: JxMutableNode | null, slots: string[] = [])
16
16
  return slots;
17
17
  }
18
18
 
19
+ /**
20
+ * Validate slot usage in a component definition. Returns a warning message or null when valid. Only
21
+ * static children are walked — slots inside $switch cases are mutually exclusive branches and are
22
+ * not counted (v1 limitation).
23
+ */
24
+ export function validateComponentSlots(doc: JxMutableNode): string | null {
25
+ const names = collectSlots(doc);
26
+ const unnamed = names.filter((n) => n === "").length;
27
+ if (unnamed > 1) {
28
+ return `Component has ${unnamed} unnamed <slot> elements — only one default slot is allowed. Give extra slots a name attribute.`;
29
+ }
30
+ const seen = new Set<string>();
31
+ for (const n of names) {
32
+ if (n && seen.has(n)) {
33
+ return `Duplicate slot name "${n}" — slot names must be unique within a component.`;
34
+ }
35
+ seen.add(n);
36
+ }
37
+ return null;
38
+ }
39
+
19
40
  /**
20
41
  * Generate and download a CEM 2.1.0 manifest for the current document.
21
42
  *
@@ -43,8 +43,18 @@ export async function locateDocument(name: string) {
43
43
  return platform.locateFile(name);
44
44
  }
45
45
 
46
+ /** Schema returned by a plugin's `$prototype` module (loose superset used across panels). */
47
+ export interface PluginSchema {
48
+ type?: string;
49
+ description?: string;
50
+ properties?: Record<string, unknown>;
51
+ required?: string[];
52
+ returns?: { type?: string };
53
+ [key: string]: unknown;
54
+ }
55
+
46
56
  /** Cache of plugin schemas keyed by "$src::$prototype". */
47
- export const pluginSchemaCache = new Map();
57
+ export const pluginSchemaCache = new Map<string, PluginSchema | null>();
48
58
 
49
59
  /**
50
60
  * Fetch and cache the schema for a $prototype module via the server. Works for both external
@@ -77,7 +87,11 @@ export async function fetchPluginSchema(
77
87
  }
78
88
  const base =
79
89
  !importedPath && state.documentPath ? `${location.origin}/${state.documentPath}` : undefined;
80
- const schema = await platform.fetchPluginSchema(src, def.$prototype, base);
90
+ const schema = (await platform.fetchPluginSchema(
91
+ src,
92
+ def.$prototype,
93
+ base,
94
+ )) as PluginSchema | null;
81
95
  pluginSchemaCache.set(cacheKey, schema);
82
96
  return schema;
83
97
  } catch {
@@ -18,11 +18,12 @@ const WORKER_PATHS: Record<string, string> = {
18
18
 
19
19
  self.MonacoEnvironment = {
20
20
  getWorker(_, label: string) {
21
- const path = WORKER_PATHS[label] || WORKER_PATHS.editorWorkerService;
21
+ const path = WORKER_PATHS[label] || WORKER_PATHS.editorWorkerService!;
22
22
  return new Worker(path, { type: "module" });
23
23
  },
24
24
  };
25
25
 
26
+ // oxlint-disable-next-line typescript/no-unsafe-call, typescript/no-unsafe-member-access -- jsonDefaults is imported from Monaco's untyped ESM contribution (see @ts-expect-error above); no type declarations exist for this named export
26
27
  jsonDefaults.setDiagnosticsOptions({
27
28
  allowComments: false,
28
29
  schemas: [