@jxsuite/studio 0.27.0 → 0.28.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/dist/studio.js +33900 -56705
  2. package/dist/studio.js.map +128 -396
  3. package/package.json +4 -10
  4. package/src/browse/browse.ts +62 -22
  5. package/src/canvas/canvas-live-render.ts +57 -55
  6. package/src/canvas/canvas-render.ts +50 -21
  7. package/src/editor/context-menu.ts +4 -1
  8. package/src/editor/convert-to-component.ts +8 -2
  9. package/src/editor/inline-edit.ts +7 -2
  10. package/src/editor/insertion-helper.ts +12 -3
  11. package/src/editor/slash-menu.ts +5 -1
  12. package/src/files/file-ops.ts +121 -102
  13. package/src/files/files.ts +53 -35
  14. package/src/format/constraints.ts +55 -0
  15. package/src/format/format-host.ts +212 -0
  16. package/src/new-project/new-project-modal.ts +18 -3
  17. package/src/panels/ai-panel.ts +3 -1
  18. package/src/panels/block-action-bar.ts +4 -2
  19. package/src/panels/canvas-dnd.ts +37 -8
  20. package/src/panels/data-explorer.ts +9 -2
  21. package/src/panels/editors.ts +1 -1
  22. package/src/panels/git-panel.ts +18 -8
  23. package/src/panels/head-panel.ts +12 -5
  24. package/src/panels/left-panel.ts +7 -4
  25. package/src/panels/panel-events.ts +3 -1
  26. package/src/panels/properties-panel.ts +9 -3
  27. package/src/panels/quick-search.ts +10 -10
  28. package/src/panels/right-panel.ts +6 -2
  29. package/src/panels/shared.ts +6 -1
  30. package/src/panels/signals-panel.ts +35 -10
  31. package/src/panels/style-inputs.ts +5 -1
  32. package/src/panels/style-panel.ts +6 -2
  33. package/src/panels/stylebook-layers-panel.ts +12 -3
  34. package/src/panels/stylebook-panel.ts +24 -8
  35. package/src/panels/toolbar.ts +16 -3
  36. package/src/platforms/devserver.ts +32 -3
  37. package/src/resize-edges.ts +12 -2
  38. package/src/services/cem-export.ts +9 -3
  39. package/src/settings/content-types-editor.ts +7 -2
  40. package/src/settings/defs-editor.ts +6 -1
  41. package/src/settings/general-settings.ts +3 -1
  42. package/src/settings/schema-field-ui.ts +11 -3
  43. package/src/settings/settings-modal.ts +4 -1
  44. package/src/state.ts +5 -1
  45. package/src/studio.ts +27 -16
  46. package/src/tabs/tab.ts +29 -6
  47. package/src/tabs/transact.ts +4 -1
  48. package/src/types.ts +15 -6
  49. package/src/ui/button-group.ts +6 -1
  50. package/src/ui/expression-editor.ts +14 -3
  51. package/src/ui/layers.ts +5 -1
  52. package/src/ui/unit-selector.ts +6 -1
  53. package/src/utils/canvas-media.ts +1 -1
  54. package/src/utils/google-fonts.ts +4 -1
  55. package/src/utils/studio-utils.ts +7 -1
  56. package/src/view.ts +8 -2
  57. package/src/markdown/md-allowlist.ts +0 -104
  58. package/src/markdown/md-convert.ts +0 -846
@@ -79,7 +79,12 @@ export function defaultDef(tag: string) {
79
79
  def.children = [
80
80
  {
81
81
  tagName: "thead",
82
- children: [{ tagName: "tr", children: [{ tagName: "th", textContent: "Header" }] }],
82
+ children: [
83
+ {
84
+ tagName: "tr",
85
+ children: [{ tagName: "th", textContent: "Header" }],
86
+ },
87
+ ],
83
88
  },
84
89
  {
85
90
  tagName: "tbody",
@@ -420,14 +420,20 @@ export function renderSignalsTemplate(S: SignalsPanelState, ctx: SignalsPanelCtx
420
420
  mutateAddDef(
421
421
  t,
422
422
  n,
423
- /** @type {Record<string, JsonValue>} */ ({ $prototype: protoName }),
423
+ /** @type {Record<string, JsonValue>} */ {
424
+ $prototype: protoName,
425
+ },
424
426
  ),
425
427
  );
426
428
  expandedSignal = n;
427
429
  if (src) {
428
430
  fetchPluginSchema(
429
431
  { $prototype: protoName, $src: src },
430
- { ...(S.documentPath != null && { documentPath: S.documentPath }) },
432
+ {
433
+ ...(S.documentPath != null && {
434
+ documentPath: S.documentPath,
435
+ }),
436
+ },
431
437
  ).then(() => ctx.renderLeftPanel());
432
438
  } else {
433
439
  ctx.renderLeftPanel();
@@ -683,7 +689,10 @@ function renderSignalEditorTemplate(
683
689
  exprNode,
684
690
  (newNode: any) =>
685
691
  transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { $expression: newNode })),
686
- { stateDefs: Object.keys(S.document.state || {}), allowEventRef: false },
692
+ {
693
+ stateDefs: Object.keys(S.document.state || {}),
694
+ allowEventRef: false,
695
+ },
687
696
  )}
688
697
  `;
689
698
  }
@@ -830,7 +839,9 @@ function renderFunctionFields(
830
839
  quiet
831
840
  title="Open in code editor"
832
841
  @click=${() => {
833
- ctx.updateSession({ ui: { editingFunction: { type: "def", defName: name } } });
842
+ ctx.updateSession({
843
+ ui: { editingFunction: { type: "def", defName: name } },
844
+ });
834
845
  ctx.renderCanvas();
835
846
  }}
836
847
  >
@@ -940,7 +951,10 @@ function renderParameterEditorTemplate(
940
951
  style="flex:1"
941
952
  @change=${(e: Event) => {
942
953
  const next = [...params];
943
- next[i] = { ...next[i], name: (e.target as HTMLInputElement).value };
954
+ next[i] = {
955
+ ...next[i],
956
+ name: (e.target as HTMLInputElement).value,
957
+ };
944
958
  transactDoc(activeTab.value, (t) =>
945
959
  mutateUpdateDef(t, name, { parameters: next }),
946
960
  );
@@ -995,7 +1009,9 @@ function renderParameterEditorTemplate(
995
1009
  @click=${() => {
996
1010
  const next = params.filter((_: unknown, j: number) => j !== i);
997
1011
  transactDoc(activeTab.value, (t) =>
998
- mutateUpdateDef(t, name, { parameters: next.length ? next : undefined }),
1012
+ mutateUpdateDef(t, name, {
1013
+ parameters: next.length ? next : undefined,
1014
+ }),
999
1015
  );
1000
1016
  }}
1001
1017
  >×</span
@@ -1007,7 +1023,9 @@ function renderParameterEditorTemplate(
1007
1023
  class="kv-add"
1008
1024
  @click=${() =>
1009
1025
  transactDoc(activeTab.value, (t) =>
1010
- mutateUpdateDef(t, name, { parameters: [...params, { name: "" }] }),
1026
+ mutateUpdateDef(t, name, {
1027
+ parameters: [...params, { name: "" }],
1028
+ }),
1011
1029
  )}
1012
1030
  >
1013
1031
  + Add parameter
@@ -1046,7 +1064,10 @@ function renderEmitsEditorTemplate(S: SignalsPanelState, name: string, def: Sign
1046
1064
  style="flex:1"
1047
1065
  @change=${(e: Event) => {
1048
1066
  const next = [...emits];
1049
- next[i] = { ...next[i], name: (e.target as HTMLInputElement).value };
1067
+ next[i] = {
1068
+ ...next[i],
1069
+ name: (e.target as HTMLInputElement).value,
1070
+ };
1050
1071
  transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { emits: next }));
1051
1072
  }}
1052
1073
  />
@@ -1289,7 +1310,9 @@ export function renderSchemaFieldsTemplate(
1289
1310
  ? parseInt((e.target as HTMLInputElement).value, 10)
1290
1311
  : parseFloat((e.target as HTMLInputElement).value);
1291
1312
  transactDoc(activeTab.value, (t) =>
1292
- mutateUpdateDef(t, name, { [prop]: isNaN(parsed) ? undefined : parsed }),
1313
+ mutateUpdateDef(t, name, {
1314
+ [prop]: isNaN(parsed) ? undefined : parsed,
1315
+ }),
1293
1316
  );
1294
1317
  }, 400);
1295
1318
  }}
@@ -1375,7 +1398,9 @@ export function renderSchemaFieldsTemplate(
1375
1398
  @click=${() => {
1376
1399
  const updated = rows.filter((_: unknown, i: number) => i !== idx);
1377
1400
  transactDoc(activeTab.value, (t) =>
1378
- mutateUpdateDef(t, name, { [prop]: updated.length ? updated : undefined }),
1401
+ mutateUpdateDef(t, name, {
1402
+ [prop]: updated.length ? updated : undefined,
1403
+ }),
1379
1404
  );
1380
1405
  ctx?.renderLeftPanel();
1381
1406
  }}
@@ -163,7 +163,11 @@ export function buildFontOptions(
163
163
  * @param {string} prop @param {string} value @param {(value: string) => void} onChange
164
164
  */
165
165
  export function renderComboboxInput(
166
- entry: { enum?: string[]; examples?: string[]; presets?: { title: string; value: string }[] },
166
+ entry: {
167
+ enum?: string[];
168
+ examples?: string[];
169
+ presets?: { title: string; value: string }[];
170
+ },
167
171
  prop: string,
168
172
  value: string,
169
173
  onChange: (value: string) => void,
@@ -246,7 +246,9 @@ function renderShorthandRow(
246
246
  mutateFn(t, shortProp, compress(vals as string[]));
247
247
  });
248
248
  },
249
- { placeholder: !lVal && inherited[name] ? String(inherited[name]) : "" },
249
+ {
250
+ placeholder: !lVal && inherited[name] ? String(inherited[name]) : "",
251
+ },
250
252
  )}
251
253
  </div>
252
254
  `;
@@ -773,7 +775,9 @@ function styleSidebarTemplate(
773
775
  style="flex:1;text-align:left;padding:6px 10px;background:var(--spectrum-gray-200, #1a1a1a);border:none;border-radius:4px;color:var(--spectrum-gray-900, #fafafa);font-size:12px;cursor:pointer"
774
776
  @click=${() => {
775
777
  const newSelector = activeSelector ? `${activeSelector} ${rule}` : rule;
776
- selectStylebookTag(newSelector, undefined, { panCanvas: true });
778
+ selectStylebookTag(newSelector, undefined, {
779
+ panCanvas: true,
780
+ });
777
781
  }}
778
782
  >
779
783
  ${rule}
@@ -57,7 +57,10 @@ export function renderStylebookLayersTemplate(ctx: {
57
57
  : [];
58
58
  return html`
59
59
  <div
60
- class=${classMap({ "layer-row": true, selected: tag === selectedLeaf })}
60
+ class=${classMap({
61
+ "layer-row": true,
62
+ selected: tag === selectedLeaf,
63
+ })}
61
64
  style="padding-left:${8 + depth * 16}px"
62
65
  @click=${(e: MouseEvent) => {
63
66
  e.stopPropagation();
@@ -91,8 +94,14 @@ export function renderStylebookLayersTemplate(ctx: {
91
94
  comp: import("../files/components.js").ComponentEntry,
92
95
  ) => html`
93
96
  <div
94
- class=${classMap({ "layer-row": true, selected: comp.tagName === selectedTag })}
95
- @click=${() => ctx.selectStylebookTag(comp.tagName, undefined, { panCanvas: true })}
97
+ class=${classMap({
98
+ "layer-row": true,
99
+ selected: comp.tagName === selectedTag,
100
+ })}
101
+ @click=${() =>
102
+ ctx.selectStylebookTag(comp.tagName, undefined, {
103
+ panCanvas: true,
104
+ })}
96
105
  >
97
106
  <span class="layer-tag component-tag" style="background:var(--accent)">⬡</span>
98
107
  <span class="layer-label">${comp.tagName}</span>
@@ -65,7 +65,13 @@ interface StylebookCtx {
65
65
  el: Element,
66
66
  type: string,
67
67
  panel: any,
68
- ) => { cls: string; top: string; left: string; width: string; height: string };
68
+ ) => {
69
+ cls: string;
70
+ top: string;
71
+ left: string;
72
+ width: string;
73
+ height: string;
74
+ };
69
75
  effectiveZoom: () => number;
70
76
  }
71
77
 
@@ -235,11 +241,11 @@ export function renderStylebookMode(ctx: StylebookCtx) {
235
241
  ${panelEntries.map((e) => e.tpl)}
236
242
  </div>
237
243
  `,
238
- /** @type {HTMLElement} */ (canvasWrap),
244
+ /** @type {HTMLElement} */ canvasWrap,
239
245
  );
240
246
 
241
247
  for (const { panel, activeSet } of panelEntries) {
242
- canvasPanels.push(/** @type {import("./canvas-dnd.js").CanvasPanel} */ (panel));
248
+ canvasPanels.push(/** @type {import("./canvas-dnd.js").CanvasPanel} */ panel);
243
249
  renderIntoPanel(panel, activeSet);
244
250
  }
245
251
  if (hasMedia) {
@@ -321,7 +327,7 @@ export function refreshStylebookStyles() {
321
327
  if (mediaName === "--") continue;
322
328
  if (activeBreakpoints.has(mediaName)) {
323
329
  const mediaTagStyle = _resolveNestedStyle(
324
- /** @type {Record<string, unknown>} */ (val),
330
+ /** @type {Record<string, unknown>} */ val,
325
331
  tag,
326
332
  );
327
333
  if (mediaTagStyle && typeof mediaTagStyle === "object") {
@@ -389,7 +395,11 @@ export function renderStylebookOverlays() {
389
395
 
390
396
  if (hoverTag && hoverTag !== selectedTag) {
391
397
  const el = findStylebookEl(panel.canvas, hoverTag);
392
- if (el) boxes.push({ ..._ctx.overlayBoxDescriptor(el, "hover", panel), label: undefined });
398
+ if (el)
399
+ boxes.push({
400
+ ..._ctx.overlayBoxDescriptor(el, "hover", panel),
401
+ label: undefined,
402
+ });
393
403
  }
394
404
 
395
405
  if (selectedTag) {
@@ -408,7 +418,12 @@ export function renderStylebookOverlays() {
408
418
  (b) => html`
409
419
  <div
410
420
  class=${b.cls}
411
- style=${styleMap({ top: b.top, left: b.left, width: b.width, height: b.height })}
421
+ style=${styleMap({
422
+ top: b.top,
423
+ left: b.left,
424
+ width: b.width,
425
+ height: b.height,
426
+ })}
412
427
  >
413
428
  ${b.label ? html`<div class="overlay-label">${b.label}</div>` : nothing}
414
429
  </div>
@@ -441,7 +456,7 @@ export function buildStylebookElement(
441
456
  if (entry.attributes) {
442
457
  for (const [k, v] of Object.entries(entry.attributes)) {
443
458
  try {
444
- el.setAttribute(k, /** @type {string} */ (v));
459
+ el.setAttribute(k, /** @type {string} */ v);
445
460
  } catch {}
446
461
  }
447
462
  }
@@ -519,7 +534,8 @@ export async function renderComponentPreview(comp: ComponentEntry) {
519
534
  return _componentFallback(comp.tagName);
520
535
  }
521
536
  } else {
522
- if (comp.path && /\.md$/i.test(comp.path)) {
537
+ if (comp.path && !comp.path.endsWith(".json")) {
538
+ // Format-class component sources (e.g. markdown) can't be imported as modules
523
539
  return _componentFallback(comp.tagName);
524
540
  }
525
541
  const root = projectState?.projectRoot;
@@ -27,7 +27,12 @@ interface ToolbarCtx {
27
27
  openFile?: (path: string) => void;
28
28
  saveFile: () => void;
29
29
  parseMediaEntries: (media: Record<string, string> | null | undefined) => {
30
- sizeBreakpoints: { name: string; query: string; width: number; type: string }[];
30
+ sizeBreakpoints: {
31
+ name: string;
32
+ query: string;
33
+ width: number;
34
+ type: string;
35
+ }[];
31
36
  featureQueries: { name: string; query: string }[];
32
37
  baseWidth: number;
33
38
  };
@@ -171,7 +176,11 @@ function minimalToolbarTemplate(ctx: ToolbarCtx) {
171
176
  const windowControls = (
172
177
  globalThis as unknown as {
173
178
  __jxPlatform?: {
174
- windowControls?: { minimize: () => void; maximize: () => void; close: () => void };
179
+ windowControls?: {
180
+ minimize: () => void;
181
+ maximize: () => void;
182
+ close: () => void;
183
+ };
175
184
  };
176
185
  }
177
186
  ).__jxPlatform?.windowControls;
@@ -381,7 +390,11 @@ function toolbarTemplate() {
381
390
  const windowControls = (
382
391
  globalThis as unknown as {
383
392
  __jxPlatform?: {
384
- windowControls?: { minimize: () => void; maximize: () => void; close: () => void };
393
+ windowControls?: {
394
+ minimize: () => void;
395
+ maximize: () => void;
396
+ close: () => void;
397
+ };
385
398
  };
386
399
  }
387
400
  ).__jxPlatform?.windowControls;
@@ -352,9 +352,13 @@ export function createDevServerPlatform() {
352
352
  return null;
353
353
  },
354
354
 
355
- /** @param {string} query */
356
- async searchFiles(query: string) {
357
- const glob = `**/*${query}*.{json,md}`;
355
+ /**
356
+ * @param {string} query
357
+ * @param {string[]} [extensions] — extra extensions beyond .json (from the format registry)
358
+ */
359
+ async searchFiles(query: string, extensions: string[] = []) {
360
+ const exts = ["json", ...extensions.map((e: string) => e.replace(/^\./, ""))];
361
+ const glob = `**/*${query}*.{${exts.join(",")}}`;
358
362
  const res = await fetch(
359
363
  `/__studio/files?dir=${encodeURIComponent(serverPath("."))}&glob=${encodeURIComponent(glob)}`,
360
364
  );
@@ -364,6 +368,31 @@ export function createDevServerPlatform() {
364
368
  return entries;
365
369
  },
366
370
 
371
+ // ─── Format registry ──────────────────────────────────────────────────
372
+
373
+ /** List the project's registered format classes (auto-discovered from imports). */
374
+ async listFormats() {
375
+ const res = await fetch(`/__studio/formats?dir=${encodeURIComponent(serverPath("."))}`);
376
+ if (!res.ok) return [];
377
+ return (await res.json()).formats ?? [];
378
+ },
379
+
380
+ /**
381
+ * Invoke a format capability (parse/serialize) server-side.
382
+ *
383
+ * @param {Record<string, unknown>} payload — { format, action, source?, doc?, options? }
384
+ */
385
+ async formatAction(payload: Record<string, unknown>) {
386
+ const res = await fetch("/__studio/format", {
387
+ method: "POST",
388
+ headers: { "Content-Type": "application/json" },
389
+ body: JSON.stringify({ ...payload, dir: serverPath(".") }),
390
+ });
391
+ const data = await res.json();
392
+ if (!res.ok) throw new Error(data.error || "Format action failed");
393
+ return data.result;
394
+ },
395
+
367
396
  // ─── Plugin schema ────────────────────────────────────────────────────
368
397
 
369
398
  /**
@@ -2,7 +2,12 @@
2
2
  const g = globalThis as unknown as {
3
3
  __jxPlatform?: {
4
4
  windowControls?: {
5
- getFrame: () => Promise<{ x: number; y: number; width: number; height: number }>;
5
+ getFrame: () => Promise<{
6
+ x: number;
7
+ y: number;
8
+ width: number;
9
+ height: number;
10
+ }>;
6
11
  setFrame: (x: number, y: number, w: number, h: number) => void;
7
12
  };
8
13
  };
@@ -54,7 +59,12 @@ async function startResize(
54
59
  e: MouseEvent,
55
60
  edge: string,
56
61
  wc: {
57
- getFrame: () => Promise<{ x: number; y: number; width: number; height: number }>;
62
+ getFrame: () => Promise<{
63
+ x: number;
64
+ y: number;
65
+ width: number;
66
+ height: number;
67
+ }>;
58
68
  setFrame: (x: number, y: number, w: number, h: number) => void;
59
69
  },
60
70
  ) {
@@ -54,7 +54,9 @@ export function exportCemManifest(
54
54
  ...(d.description ? { description: d.description } : {}),
55
55
  ...(d.parameters ? { parameters: d.parameters.map(normParam) } : {}),
56
56
  ...(d.deprecated
57
- ? { deprecated: typeof d.deprecated === "string" ? d.deprecated : true }
57
+ ? {
58
+ deprecated: typeof d.deprecated === "string" ? d.deprecated : true,
59
+ }
58
60
  : {}),
59
61
  });
60
62
  // Collect emits
@@ -80,7 +82,9 @@ export function exportCemManifest(
80
82
  ...(d.attribute ? { attribute: d.attribute } : {}),
81
83
  ...(d.reflects ? { reflects: true } : {}),
82
84
  ...(d.deprecated
83
- ? { deprecated: typeof d.deprecated === "string" ? d.deprecated : true }
85
+ ? {
86
+ deprecated: typeof d.deprecated === "string" ? d.deprecated : true,
87
+ }
84
88
  : {}),
85
89
  });
86
90
  if (d.attribute) {
@@ -132,7 +136,9 @@ export function exportCemManifest(
132
136
  ],
133
137
  };
134
138
 
135
- const blob = new Blob([JSON.stringify(manifest, null, 2)], { type: "application/json" });
139
+ const blob = new Blob([JSON.stringify(manifest, null, 2)], {
140
+ type: "application/json",
141
+ });
136
142
  const url = URL.createObjectURL(blob);
137
143
  const a = document.createElement("a");
138
144
  a.href = url;
@@ -466,7 +466,7 @@ export function renderContentTypesEditor(container: HTMLElement) {
466
466
  ([name, def]) =>
467
467
  fieldCardTpl(
468
468
  name,
469
- /** @type {import("./schema-field-ui.js").SchemaProperty} */ (def),
469
+ /** @type {import("./schema-field-ui.js").SchemaProperty} */ def,
470
470
  required.includes(name),
471
471
  handlers,
472
472
  contentTypeNames,
@@ -497,7 +497,12 @@ export function renderContentTypesEditor(container: HTMLElement) {
497
497
  onConfirm: () => handleAddField(rerender),
498
498
  onCancel: () => {
499
499
  showAddField = false;
500
- newFieldState = { name: "", type: "string", format: "", required: false };
500
+ newFieldState = {
501
+ name: "",
502
+ type: "string",
503
+ format: "",
504
+ required: false,
505
+ };
501
506
  rerender();
502
507
  },
503
508
  })
@@ -460,7 +460,12 @@ export function renderDefsEditor(container: HTMLElement) {
460
460
  onConfirm: () => handleAddField(rerender),
461
461
  onCancel: () => {
462
462
  showAddField = false;
463
- newFieldState = { name: "", type: "string", format: "", required: false };
463
+ newFieldState = {
464
+ name: "",
465
+ type: "string",
466
+ format: "",
467
+ required: false,
468
+ };
464
469
  rerender();
465
470
  },
466
471
  })
@@ -30,7 +30,9 @@ export function renderGeneralSettings(container: HTMLElement) {
30
30
  };
31
31
 
32
32
  const onAdapterChange = (e: Event) => {
33
- updateSiteConfig({ build: { ...config.build, adapter: (e.target as HTMLInputElement).value } });
33
+ updateSiteConfig({
34
+ build: { ...config.build, adapter: (e.target as HTMLInputElement).value },
35
+ });
34
36
  };
35
37
 
36
38
  const onEditGlobalStyles = () => {
@@ -157,7 +157,7 @@ export function fieldCardTpl(
157
157
  nestedFieldCardTpl(
158
158
  fieldName,
159
159
  name,
160
- /** @type {SchemaProperty} */ (sub),
160
+ /** @type {SchemaProperty} */ sub,
161
161
  nestedRequired.includes(name),
162
162
  handlers,
163
163
  ),
@@ -274,7 +274,11 @@ function nestedAddFieldTpl(parentName: string, handlers: FieldHandlers) {
274
274
  const typePicker = row?.querySelector("sp-picker") as HTMLInputElement | null;
275
275
  const type = typePicker?.value || "string";
276
276
  if (name && handlers.onAddNestedField) {
277
- handlers.onAddNestedField(parentName, { name, type, required: false });
277
+ handlers.onAddNestedField(parentName, {
278
+ name,
279
+ type,
280
+ required: false,
281
+ });
278
282
  target.value = "";
279
283
  }
280
284
  }
@@ -295,7 +299,11 @@ function nestedAddFieldTpl(parentName: string, handlers: FieldHandlers) {
295
299
  const name = nameInput?.value?.trim();
296
300
  const type = typePicker?.value || "string";
297
301
  if (name && handlers.onAddNestedField) {
298
- handlers.onAddNestedField(parentName, { name, type, required: false });
302
+ handlers.onAddNestedField(parentName, {
303
+ name,
304
+ type,
305
+ required: false,
306
+ });
299
307
  if (nameInput) nameInput.value = "";
300
308
  }
301
309
  }}
@@ -68,7 +68,10 @@ function renderModal() {
68
68
  ${sections.map(
69
69
  (s) => html`
70
70
  <button
71
- class=${classMap({ "settings-nav-item": true, active: _activeSection === s.key })}
71
+ class=${classMap({
72
+ "settings-nav-item": true,
73
+ active: _activeSection === s.key,
74
+ })}
72
75
  @click=${() => onNavClick(s.key)}
73
76
  >
74
77
  ${s.label}
package/src/state.ts CHANGED
@@ -33,7 +33,11 @@ export interface StudioState {
33
33
  mode: string;
34
34
  content: { frontmatter: Record<string, unknown> };
35
35
  ui: Record<string, unknown>;
36
- canvas: { status: string; scope: Record<string, unknown> | null; error: string | null };
36
+ canvas: {
37
+ status: string;
38
+ scope: Record<string, unknown> | null;
39
+ error: string | null;
40
+ };
37
41
  }
38
42
 
39
43
  interface StudioStackFrame {
package/src/studio.ts CHANGED
@@ -40,7 +40,8 @@ import {
40
40
  setStatusbarRenderer,
41
41
  mountStatusbar,
42
42
  } from "./panels/statusbar";
43
- import { loadMarkdown, saveFile, exportFile, serializeDocument } from "./files/file-ops";
43
+ import { parseSourceForPath, saveFile, exportFile, serializeDocument } from "./files/file-ops";
44
+ import { loadFormats, formatForPath, documentExtensions } from "./format/format-host";
44
45
  import {
45
46
  loadProject as _loadProject,
46
47
  openProject as _openProject,
@@ -168,7 +169,7 @@ async function navigateBack() {
168
169
  if (tab.doc.dirty && tab.documentPath) {
169
170
  try {
170
171
  const platform = getPlatform();
171
- await platform.writeFile(tab.documentPath, serializeDocument(tab));
172
+ await platform.writeFile(tab.documentPath, await serializeDocument(tab));
172
173
  } catch (e) {
173
174
  const err = e as Error;
174
175
  statusMessage(`Save error: ${err.message}`);
@@ -198,7 +199,7 @@ async function navigateToLevel(targetIndex: number) {
198
199
  if (tab.doc.dirty && tab.documentPath) {
199
200
  try {
200
201
  const platform = getPlatform();
201
- await platform.writeFile(tab.documentPath, serializeDocument(tab));
202
+ await platform.writeFile(tab.documentPath, await serializeDocument(tab));
202
203
  } catch (e) {
203
204
  const err = e as Error;
204
205
  statusMessage(`Save error: ${err.message}`);
@@ -222,9 +223,8 @@ async function navigateToLevel(targetIndex: number) {
222
223
  async function closeFunctionEditor() {
223
224
  const tab = activeTab.value;
224
225
  const editing =
225
- /** @type {{ type: string; defName?: string; path?: JxPath; eventKey?: string } | null} */ (
226
- tab?.session.ui.editingFunction
227
- );
226
+ /** @type {{ type: string; defName?: string; path?: JxPath; eventKey?: string } | null} */ tab
227
+ ?.session.ui.editingFunction;
228
228
  if (!editing || !tab) return;
229
229
  if (view.functionEditor) {
230
230
  const currentCode = view.functionEditor.getValue();
@@ -237,7 +237,7 @@ async function closeFunctionEditor() {
237
237
  const current = node?.[editing.eventKey as string] || {};
238
238
  transactDoc(tab, (t) =>
239
239
  mutateUpdateProperty(t, editing.path as JxPath, editing.eventKey as string, {
240
- .../** @type {object} */ (current),
240
+ .../** @type {object} */ current,
241
241
  $prototype: "Function",
242
242
  body: bodyToStore,
243
243
  }),
@@ -531,7 +531,12 @@ if (_projectParam) {
531
531
  // When opening project.json, default to home page instead
532
532
  if (fileRelPath === "project.json" || fileRelPath.endsWith("/project.json")) {
533
533
  let opened = false;
534
- for (const candidate of ["pages/index.md", "pages/index.json"]) {
534
+ await loadFormats();
535
+ const homeCandidates = [
536
+ ...documentExtensions("page").map((ext) => `pages/index${ext}`),
537
+ "pages/index.json",
538
+ ];
539
+ for (const candidate of homeCandidates) {
535
540
  try {
536
541
  await platform.readFile(candidate);
537
542
  fileRelPath = candidate;
@@ -544,12 +549,14 @@ if (_projectParam) {
544
549
 
545
550
  const content = await platform.readFile(fileRelPath);
546
551
  if (content) {
547
- let parsedDoc, frontmatter;
548
- const isMd = fileRelPath.endsWith(".md");
549
- if (isMd) {
550
- const result = await loadMarkdown(content);
552
+ let parsedDoc, frontmatter, parsedMode;
553
+ await loadFormats();
554
+ const fileFormat = formatForPath(fileRelPath);
555
+ if (fileFormat) {
556
+ const result = await parseSourceForPath(fileRelPath, content);
551
557
  parsedDoc = result.document;
552
558
  frontmatter = result.frontmatter;
559
+ parsedMode = result.mode;
553
560
  } else {
554
561
  parsedDoc = JSON.parse(content);
555
562
  }
@@ -560,10 +567,10 @@ if (_projectParam) {
560
567
  documentPath: fileRelPath,
561
568
  document: parsedDoc,
562
569
  ...(frontmatter != null && { frontmatter }),
563
- sourceFormat: isMd ? "md" : null,
570
+ sourceFormat: fileFormat?.name ?? null,
564
571
  });
565
572
 
566
- if (isMd && activeTab.value) activeTab.value.doc.mode = "content";
573
+ if (parsedMode === "content" && activeTab.value) activeTab.value.doc.mode = "content";
567
574
  if (fileRelPath === "project.json" && activeTab.value) {
568
575
  activeTab.value.session.ui.canvasMode = "stylebook";
569
576
  }
@@ -671,7 +678,11 @@ async function openRecentProject(root: string) {
671
678
  }
672
679
  }
673
680
  function renderFilesTemplate() {
674
- return _renderFilesTemplate({ openProject, openFileFromTree, renderLeftPanel });
681
+ return _renderFilesTemplate({
682
+ openProject,
683
+ openFileFromTree,
684
+ renderLeftPanel,
685
+ });
675
686
  }
676
687
  function openFileFromTree(path: string) {
677
688
  return openFileInTab(path);
@@ -718,7 +729,7 @@ function scheduleAutosave() {
718
729
  if (t?.fileHandle && t.doc.dirty && "createWritable" in t.fileHandle) {
719
730
  try {
720
731
  const writable = await t.fileHandle.createWritable();
721
- await writable.write(serializeDocument(t));
732
+ await writable.write(await serializeDocument(t));
722
733
  await writable.close();
723
734
  t.doc.dirty = false;
724
735
  statusMessage("Auto-saved");