@jxsuite/studio 0.15.0 → 0.16.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/studio",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "Jx Studio — visual builder for Jx documents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -30,7 +30,7 @@
30
30
  "dependencies": {
31
31
  "@atlaskit/pragmatic-drag-and-drop": "^1.8.1",
32
32
  "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0",
33
- "@jxsuite/runtime": "^0.12.0",
33
+ "@jxsuite/runtime": "^0.15.0",
34
34
  "@spectrum-web-components/accordion": "^1.12.1",
35
35
  "@spectrum-web-components/action-bar": "1.12.1",
36
36
  "@spectrum-web-components/action-button": "^1.12.1",
@@ -10,6 +10,7 @@ import * as monaco from "monaco-editor/esm/vs/editor/editor.api.js";
10
10
  import { canvasWrap, canvasPanels, updateCanvas } from "../store.js";
11
11
  import { activeTab } from "../workspace/workspace.js";
12
12
  import { view } from "../view.js";
13
+ import { loadMarkdown } from "../files/file-ops.js";
13
14
  import {
14
15
  canvasPanelTemplate,
15
16
  applyTransform,
@@ -144,6 +145,7 @@ export function renderCanvas() {
144
145
  // Reset inline style overrides from other modes
145
146
  canvasWrap.style.padding = "";
146
147
  canvasWrap.style.alignItems = "";
148
+ canvasWrap.style.flexDirection = "";
147
149
  canvasWrap.style.display = "";
148
150
  canvasWrap.style.overflow = "";
149
151
  canvasWrap.style.overflow = "";
@@ -247,72 +249,79 @@ export function renderCanvas() {
247
249
  return;
248
250
  }
249
251
 
250
- // Git diff mode — render original (left) and current (right) side-by-side
252
+ // Git diff mode — render original (left) and current (right) side-by-side on panzoom surface
251
253
  if (canvasMode === "git-diff") {
252
254
  if (!_ctx.gitDiffState) {
253
- // Fallback to design mode if diff state is missing
254
255
  _ctx.setCanvasMode("design");
255
256
  renderCanvas();
256
257
  return;
257
258
  }
258
259
 
259
- canvasWrap.style.padding = "0";
260
- canvasWrap.style.overflow = "hidden";
260
+ if (modeChanged) {
261
+ canvasWrap.style.padding = "0";
262
+ canvasWrap.style.overflow = "hidden";
263
+ }
261
264
 
262
265
  const gitDiffState = _ctx.gitDiffState;
263
- const { tpl: origPanelTpl, panel: origPanel } = canvasPanelTemplate(
266
+ const panelWidth = 800;
267
+
268
+ const { tpl: origTpl, panel: origPanel } = canvasPanelTemplate(
264
269
  "git-diff-original",
265
270
  "Original",
266
271
  false,
267
- "50%",
272
+ panelWidth,
268
273
  );
269
- const { tpl: currPanelTpl, panel: currPanel } = canvasPanelTemplate(
274
+ const { tpl: currTpl, panel: currPanel } = canvasPanelTemplate(
270
275
  "git-diff-current",
271
276
  "Current",
272
277
  false,
273
- "50%",
278
+ panelWidth,
274
279
  );
275
280
 
276
- const diffTpl = html`
277
- <div style="display: flex; height: 100%; gap: 0;">
278
- <div style="flex: 1; overflow: hidden; display: flex; flex-direction: column;">
279
- ${origPanelTpl}
280
- </div>
281
- <div style="flex: 1; overflow: hidden; display: flex; flex-direction: column;">
282
- ${currPanelTpl}
281
+ litRender(
282
+ html`
283
+ <div
284
+ class="panzoom-wrap"
285
+ style="transform-origin:0 0"
286
+ ${ref((el) => {
287
+ if (el) view.panzoomWrap = /** @type {HTMLDivElement} */ (el);
288
+ })}
289
+ >
290
+ ${origTpl} ${currTpl}
283
291
  </div>
284
- </div>
285
- `;
292
+ `,
293
+ canvasWrap,
294
+ );
286
295
 
287
- litRender(diffTpl, canvasWrap);
288
296
  canvasPanels.push(/** @type {any} */ (origPanel));
289
297
  canvasPanels.push(/** @type {any} */ (currPanel));
290
298
 
291
- // Parse original document from git content
292
- let originalDoc = null;
293
- try {
299
+ /** @param {string} content */
300
+ const parseContent = (content) => {
294
301
  if (gitDiffState.isMarkdown) {
295
- // For markdown, we need to convert it to Jx format
296
- // For now, use a simple placeholder until we have markdown parsing
297
- originalDoc = {
298
- tagName: "div",
299
- children: [{ tagName: "p", textContent: "Original (markdown)" }],
300
- };
301
- } else {
302
- // Parse as JSON
303
- originalDoc = JSON.parse(gitDiffState.originalContent);
302
+ return loadMarkdown(content).then((r) => r.document);
304
303
  }
305
- } catch {
306
- originalDoc = {
307
- tagName: "div",
308
- children: [{ tagName: "p", textContent: "Failed to parse original" }],
309
- };
310
- }
311
-
312
- const currDoc = S.document;
304
+ return Promise.resolve().then(() => {
305
+ try {
306
+ return JSON.parse(content);
307
+ } catch {
308
+ return { tagName: "div", children: [{ tagName: "p", textContent: "Failed to parse" }] };
309
+ }
310
+ });
311
+ };
312
+
313
+ const featureToggles = S.ui.featureToggles;
314
+ Promise.all([
315
+ parseContent(gitDiffState.originalContent || ""),
316
+ parseContent(gitDiffState.currentContent || ""),
317
+ ]).then(([originalDoc, currentDoc]) => {
318
+ renderCanvasIntoPanel(origPanel, new Set(), featureToggles, originalDoc, gitDiffState);
319
+ renderCanvasIntoPanel(currPanel, new Set(), featureToggles, currentDoc, gitDiffState);
320
+ });
313
321
 
314
- renderCanvasIntoPanel(origPanel, new Set(), S.ui.featureToggles, originalDoc, gitDiffState);
315
- renderCanvasIntoPanel(currPanel, new Set(), S.ui.featureToggles, currDoc, gitDiffState);
322
+ applyTransform();
323
+ if (modeChanged) observeCenterUntilStable();
324
+ renderZoomIndicator();
316
325
  return;
317
326
  }
318
327
 
@@ -498,6 +507,7 @@ function renderCanvasIntoPanel(
498
507
  } else {
499
508
  // Fallback to structural preview
500
509
  updateCanvas({ status: "ready", scope: null, error: null });
510
+ panel.canvas.innerHTML = "";
501
511
  renderCanvasNode(docToRender, [], panel.canvas, activeBreakpoints, featureToggles);
502
512
  }
503
513
  registerPanelDnD(panel);
@@ -174,9 +174,11 @@ export async function exportFile() {
174
174
  * @param {import("../tabs/tab.js").Tab} tab
175
175
  * @returns {string}
176
176
  */
177
- function serializeDocument(tab) {
177
+ export function serializeDocument(tab) {
178
178
  if (tab.doc.sourceFormat === "md") {
179
- return jxDocToMd(tab.doc.document);
179
+ const fm = tab.doc.content?.frontmatter || {};
180
+ const fullDoc = { ...fm, children: tab.doc.document.children ?? [] };
181
+ return jxDocToMd(fullDoc);
180
182
  }
181
183
  if (tab.doc.mode === "content") {
182
184
  const mdast = jxToMd(tab.doc.document);
@@ -383,83 +383,83 @@ export function setupTreeKeyboard(/** @type {HTMLElement} */ tree) {
383
383
  // ─── Context menu ─────────────────────────────────────────────────────────────
384
384
 
385
385
  /** @type {HTMLElement | null} */
386
- let fileContextPopover = null;
386
+ let _fileCtxHost = null;
387
+
388
+ function getFileCtxHost() {
389
+ if (!_fileCtxHost) {
390
+ _fileCtxHost = document.createElement("div");
391
+ _fileCtxHost.style.display = "contents";
392
+ (document.querySelector("sp-theme") || document.body).appendChild(_fileCtxHost);
393
+ document.addEventListener("click", dismissFileContextMenu);
394
+ }
395
+ return _fileCtxHost;
396
+ }
397
+
398
+ function dismissFileContextMenu() {
399
+ const host = _fileCtxHost;
400
+ if (!host) return;
401
+ const popover = host.querySelector("sp-popover");
402
+ if (popover) popover.removeAttribute("open");
403
+ }
387
404
 
388
405
  function showFileContextMenu(
389
406
  /** @type {MouseEvent} */ e,
390
407
  /** @type {{ name: string; path: string; type: string }} */ entry,
391
408
  /** @type {{ openFileFn: (path: string) => void; renderLeftPanel: () => void }} */ ctx,
392
409
  ) {
393
- if (fileContextPopover) {
394
- fileContextPopover.remove();
395
- fileContextPopover = null;
396
- }
397
-
410
+ e.preventDefault();
411
+ const host = getFileCtxHost();
398
412
  const isDir = entry.type === "directory";
399
- fileContextPopover = document.createElement("div");
400
413
 
401
- const tpl = html`
402
- <sp-popover
403
- placement="right-start"
404
- open
405
- style="position:fixed;left:${e.clientX}px;top:${e.clientY}px;z-index:9999"
406
- >
407
- <sp-menu style="min-width:160px">
408
- ${!isDir
409
- ? html`<sp-menu-item
410
- @click=${() => {
411
- closeFileContextMenu();
412
- ctx.openFileFn(entry.path);
413
- }}
414
- >Open</sp-menu-item
415
- >`
416
- : nothing}
417
- ${isDir
418
- ? html`<sp-menu-item
419
- @click=${() => {
420
- closeFileContextMenu();
421
- createNewFile(entry.path, ctx.renderLeftPanel);
422
- }}
423
- >New File…</sp-menu-item
424
- >`
425
- : nothing}
426
- <sp-menu-divider></sp-menu-divider>
427
- <sp-menu-item
428
- @click=${() => {
429
- closeFileContextMenu();
430
- renameFile(entry, ctx.renderLeftPanel);
431
- }}
432
- >Rename…</sp-menu-item
433
- >
434
- <sp-menu-item
435
- style="color:var(--danger)"
436
- @click=${() => {
437
- closeFileContextMenu();
438
- deleteFile(entry, ctx.renderLeftPanel);
439
- }}
440
- >Delete</sp-menu-item
441
- >
442
- </sp-menu>
443
- </sp-popover>
444
- `;
445
-
446
- litRender(tpl, fileContextPopover);
447
- document.body.appendChild(fileContextPopover);
448
-
449
- const closeHandler = (/** @type {MouseEvent} */ ev) => {
450
- if (!fileContextPopover?.contains(/** @type {Node} */ (ev.target))) {
451
- closeFileContextMenu();
452
- document.removeEventListener("mousedown", closeHandler, true);
453
- }
454
- };
455
- setTimeout(() => document.addEventListener("mousedown", closeHandler, true), 0);
456
- }
414
+ /** @type {{ label: string; action?: () => void; danger?: boolean }[]} */
415
+ const items = [];
457
416
 
458
- function closeFileContextMenu() {
459
- if (fileContextPopover) {
460
- fileContextPopover.remove();
461
- fileContextPopover = null;
417
+ if (!isDir) {
418
+ items.push({ label: "Open", action: () => ctx.openFileFn(entry.path) });
462
419
  }
420
+ if (isDir) {
421
+ items.push({
422
+ label: "New File\u2026",
423
+ action: () => createNewFile(entry.path, ctx.renderLeftPanel),
424
+ });
425
+ }
426
+ items.push({ label: "\u2014" });
427
+ items.push({ label: "Rename\u2026", action: () => renameFile(entry, ctx.renderLeftPanel) });
428
+ items.push({
429
+ label: "Delete",
430
+ action: () => deleteFile(entry, ctx.renderLeftPanel),
431
+ danger: true,
432
+ });
433
+
434
+ litRender(
435
+ html`<sp-popover style="position:fixed;z-index:10000">
436
+ <sp-menu>
437
+ ${items.map((item) =>
438
+ item.label === "\u2014"
439
+ ? html`<sp-menu-divider></sp-menu-divider>`
440
+ : html`<sp-menu-item
441
+ style=${item.danger ? "color: var(--danger)" : ""}
442
+ @click=${() => {
443
+ dismissFileContextMenu();
444
+ item.action?.();
445
+ }}
446
+ >${item.label}</sp-menu-item
447
+ >`,
448
+ )}
449
+ </sp-menu>
450
+ </sp-popover>`,
451
+ host,
452
+ );
453
+
454
+ const popover = /** @type {HTMLElement} */ (host.querySelector("sp-popover"));
455
+ popover.setAttribute("open", "");
456
+ const menuRect = popover.getBoundingClientRect();
457
+ let x = e.clientX,
458
+ y = e.clientY;
459
+ if (x + menuRect.width > window.innerWidth) x = window.innerWidth - menuRect.width - 4;
460
+ if (y + menuRect.height > window.innerHeight) y = window.innerHeight - menuRect.height - 4;
461
+ popover.style.left = `${x}px`;
462
+ popover.style.top = `${y}px`;
463
463
  }
464
464
 
465
465
  // ─── File CRUD ────────────────────────────────────────────────────────────────
@@ -57,6 +57,8 @@ export function renderFunctionEditor() {
57
57
 
58
58
  litRender(nothing, canvasWrap);
59
59
  canvasWrap.style.padding = "0";
60
+ canvasWrap.style.flexDirection = "column";
61
+ canvasWrap.style.alignItems = "stretch";
60
62
 
61
63
  // Toolbar breadcrumb handles context display — re-render it
62
64
  renderOnly("toolbar");
@@ -65,12 +67,14 @@ export function renderFunctionEditor() {
65
67
  /** @type {HTMLDivElement | null} */
66
68
  let editorContainer = null;
67
69
  litRender(
68
- html`<div
69
- class="source-editor"
70
- ${ref((el) => {
71
- if (el) editorContainer = /** @type {HTMLDivElement} */ (el);
72
- })}
73
- ></div>`,
70
+ html`<div class="source-wrap">
71
+ <div
72
+ class="source-editor"
73
+ ${ref((el) => {
74
+ if (el) editorContainer = /** @type {HTMLDivElement} */ (el);
75
+ })}
76
+ ></div>
77
+ </div>`,
74
78
  canvasWrap,
75
79
  );
76
80
 
@@ -7,6 +7,7 @@ import { html, nothing } from "lit-html";
7
7
  import { live } from "lit-html/directives/live.js";
8
8
  import { getPlatform } from "../platform.js";
9
9
  import { updateUi, renderOnly } from "../store.js";
10
+ import { activeTab } from "../workspace/workspace.js";
10
11
  import { view } from "../view.js";
11
12
 
12
13
  async function refreshGitStatus() {
@@ -70,7 +71,8 @@ export function renderGitPanel(S, ctx) {
70
71
  const totalChanges = status?.files?.length || 0;
71
72
 
72
73
  const doCommit = async () => {
73
- const msg = S.ui.gitCommitMessage?.trim();
74
+ const tab = activeTab.value;
75
+ const msg = tab?.session.ui.gitCommitMessage?.trim();
74
76
  if (!msg) return;
75
77
  updateUi("gitCommitMessage", "");
76
78
  await gitAction("gitCommit", msg);
@@ -145,11 +147,7 @@ export function renderGitPanel(S, ctx) {
145
147
  }
146
148
  }}
147
149
  ></sp-textfield>
148
- <sp-action-button
149
- class="git-commit-btn"
150
- @click=${doCommit}
151
- ?disabled=${!S.ui.gitCommitMessage?.trim() || loading}
152
- >
150
+ <sp-action-button class="git-commit-btn" @click=${doCommit} ?disabled=${loading}>
153
151
  <sp-icon-checkmark slot="icon" size="xs"></sp-icon-checkmark>
154
152
  Commit
155
153
  </sp-action-button>
@@ -162,29 +160,33 @@ export function renderGitPanel(S, ctx) {
162
160
  const dir = parts.join("/");
163
161
 
164
162
  const onFileClick = async () => {
165
- // Only support diff for modified/added files
166
163
  if (file.status !== "M" && file.status !== "A") return;
167
164
  if (!file.path.endsWith(".md") && !file.path.endsWith(".json")) return;
168
165
 
169
166
  try {
170
167
  const plat = getPlatform();
171
168
  updateUi("gitLoading", true);
172
- const originalContent = await plat.gitShow({ path: file.path, ref: "HEAD" });
173
169
 
174
- // Determine if it's markdown or JSON
175
- const isMarkdown = file.path.endsWith(".md");
170
+ const [originalContent, currentContent] = await Promise.all([
171
+ file.status === "A"
172
+ ? Promise.resolve("")
173
+ : plat.gitShow({ path: file.path, ref: "HEAD" }),
174
+ plat.readFile(file.path),
175
+ ]);
176
176
 
177
- // For now, we'll need to pass the original content to the canvas
178
- // This requires state management which we'll add to studio.js
179
- updateUi("gitDiffState", {
177
+ const isMarkdown = file.path.endsWith(".md");
178
+ const diffState = {
180
179
  filePath: file.path,
181
180
  originalContent,
181
+ currentContent,
182
182
  isMarkdown,
183
183
  fileStatus: file.status,
184
- });
184
+ };
185
+
186
+ updateUi("gitDiffState", diffState);
185
187
 
186
- // Trigger diff mode via context
187
188
  if (ctx?.setCanvasMode) {
189
+ if (ctx.setGitDiffState) ctx.setGitDiffState(diffState);
188
190
  ctx.setCanvasMode("git-diff");
189
191
  }
190
192
  } catch (/** @type {any} */ e) {
@@ -40,6 +40,7 @@ import { selectStylebookTag, stylebookMeta } from "./stylebook-panel.js";
40
40
  * registerElementsDnD: () => void;
41
41
  * registerComponentsDnD: () => void;
42
42
  * setupTreeKeyboard: (tree: HTMLElement) => void;
43
+ * setGitDiffState: (state: any) => void;
43
44
  * }} LeftPanelCtx
44
45
  */
45
46
 
@@ -69,6 +70,9 @@ export function mount(ctx) {
69
70
  void tab.doc.mode;
70
71
  void tab.session.selection;
71
72
  void tab.session.ui.settingsTab;
73
+ void tab.session.ui.gitStatus;
74
+ void tab.session.ui.gitLoading;
75
+ void tab.session.ui.gitError;
72
76
  render();
73
77
  });
74
78
  });
@@ -14,6 +14,7 @@ import {
14
14
  mutateRenameDef,
15
15
  } from "../tabs/transact.js";
16
16
  import { renderFieldRow } from "../ui/field-row.js";
17
+ import { renderMediaPicker } from "../ui/media-picker.js";
17
18
  import { fetchPluginSchema, pluginSchemaCache } from "../services/code-services.js";
18
19
 
19
20
  // ─── Module-local state ─────────────────────────────────────────────────────
@@ -343,6 +344,9 @@ function renderSignalEditorTemplate(
343
344
  /** @type {any} */ def,
344
345
  /** @type {any} */ ctx,
345
346
  ) {
347
+ if (typeof def !== "object" || def === null) {
348
+ def = { default: def };
349
+ }
346
350
  const cat = defCategory(def);
347
351
 
348
352
  // Helper for picker rows
@@ -470,20 +474,31 @@ function renderSignalEditorTemplate(
470
474
  ),
471
475
  )
472
476
  : nothing}
473
- ${signalFieldRow("Default", defaultVal, (/** @type {any} */ v) => {
474
- let parsed = v;
475
- if (def.type === "integer") parsed = parseInt(v, 10) || 0;
476
- else if (def.type === "number") parsed = parseFloat(v) || 0;
477
- else if (def.type === "boolean") parsed = v === "true";
478
- else if (def.type === "array" || def.type === "object") {
479
- try {
480
- parsed = JSON.parse(v);
481
- } catch {
482
- parsed = v;
483
- }
484
- }
485
- transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { default: parsed }));
486
- })}
477
+ ${def.format === "image"
478
+ ? renderFieldRow({
479
+ prop: "Default",
480
+ label: "Default",
481
+ hasValue: false,
482
+ widget: renderMediaPicker("default", defaultVal, (/** @type {any} */ v) => {
483
+ transactDoc(activeTab.value, (t) =>
484
+ mutateUpdateDef(t, name, { default: v || undefined }),
485
+ );
486
+ }),
487
+ })
488
+ : signalFieldRow("Default", defaultVal, (/** @type {any} */ v) => {
489
+ let parsed = v;
490
+ if (def.type === "integer") parsed = parseInt(v, 10) || 0;
491
+ else if (def.type === "number") parsed = parseFloat(v) || 0;
492
+ else if (def.type === "boolean") parsed = v === "true";
493
+ else if (def.type === "array" || def.type === "object") {
494
+ try {
495
+ parsed = JSON.parse(v);
496
+ } catch {
497
+ parsed = v;
498
+ }
499
+ }
500
+ transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { default: parsed }));
501
+ })}
487
502
  ${signalFieldRow("Description", def.description || "", (/** @type {any} */ v) =>
488
503
  transactDoc(activeTab.value, (t) =>
489
504
  mutateUpdateDef(t, name, { description: v || undefined }),
@@ -644,7 +659,16 @@ function renderFunctionFields(
644
659
  /** @type {any} */ textareaRow,
645
660
  /** @type {any} */ ctx,
646
661
  ) {
647
- const srcFields = def.$src
662
+ const descriptionField = signalFieldRow(
663
+ "Description",
664
+ def.description || "",
665
+ (/** @type {any} */ v) =>
666
+ transactDoc(activeTab.value, (t) =>
667
+ mutateUpdateDef(t, name, { description: v || undefined }),
668
+ ),
669
+ );
670
+
671
+ const bodyField = def.$src
648
672
  ? html`
649
673
  ${signalFieldRow("Source", def.$src || "", (/** @type {any} */ v) =>
650
674
  transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { $src: v || undefined })),
@@ -655,36 +679,35 @@ function renderFunctionFields(
655
679
  ),
656
680
  )}
657
681
  `
658
- : textareaRow(
659
- "Body",
660
- def.body || "",
661
- (/** @type {any} */ v) =>
662
- transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { body: v })),
663
- { minHeight: "60px", mono: true },
664
- );
665
-
666
- return html`
667
- ${srcFields} ${renderParameterEditorTemplate(S, name, def, ctx)}
668
- ${isCustomElementDoc(S) ? renderEmitsEditorTemplate(S, name, def) : nothing}
669
- ${!def.$src
670
- ? html`
671
- <button
672
- class="kv-add"
673
- style="margin-top:4px"
682
+ : html`
683
+ <div style="display:flex;align-items:center;gap:4px">
684
+ <span class="field-label" style="flex:1">Body</span>
685
+ <sp-action-button
686
+ size="xs"
687
+ quiet
688
+ title="Open in code editor"
674
689
  @click=${() => {
675
690
  ctx.updateSession({ ui: { editingFunction: { type: "def", defName: name } } });
676
691
  ctx.renderCanvas();
677
692
  }}
678
693
  >
679
- Open in editor
680
- </button>
681
- `
682
- : nothing}
683
- ${signalFieldRow("Description", def.description || "", (/** @type {any} */ v) =>
684
- transactDoc(activeTab.value, (t) =>
685
- mutateUpdateDef(t, name, { description: v || undefined }),
686
- ),
687
- )}
694
+ <sp-icon-code slot="icon"></sp-icon-code>
695
+ </sp-action-button>
696
+ </div>
697
+ <textarea
698
+ class="field-input"
699
+ style="min-height:60px;font-family:monospace;font-size:11px"
700
+ .value=${def.body || ""}
701
+ @input=${(/** @type {any} */ e) => {
702
+ const v = e.target.value;
703
+ transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { body: v }));
704
+ }}
705
+ ></textarea>
706
+ `;
707
+
708
+ return html`
709
+ ${descriptionField} ${renderParameterEditorTemplate(S, name, def, ctx)}
710
+ ${isCustomElementDoc(S) ? renderEmitsEditorTemplate(S, name, def) : nothing} ${bodyField}
688
711
  `;
689
712
  }
690
713
 
@@ -434,6 +434,16 @@ export function createDevServerPlatform() {
434
434
  return await res.json();
435
435
  },
436
436
 
437
+ /** @param {{ path: string; ref?: string }} opts */
438
+ async gitShow(opts) {
439
+ const params = new URLSearchParams({ path: opts.path });
440
+ if (opts.ref) params.set("ref", opts.ref);
441
+ const res = await fetch(`/__studio/git/show?${params}`);
442
+ if (!res.ok) throw new Error(await res.text());
443
+ const data = await res.json();
444
+ return data.content;
445
+ },
446
+
437
447
  /** @param {string[]} files */
438
448
  async gitDiscard(files) {
439
449
  const res = await fetch("/__studio/git/discard", {
@@ -85,13 +85,13 @@ export function setLintMarkers(editor, diagnostics) {
85
85
 
86
86
  /**
87
87
  * @param {any} editing
88
- * @param {any} state
88
+ * @param {any} document
89
89
  */
90
- export function getFunctionArgs(editing, state) {
90
+ export function getFunctionArgs(editing, document) {
91
91
  if (editing.type === "def") {
92
- return state.document.state?.[editing.defName]?.parameters || ["state", "event"];
92
+ return document?.state?.[editing.defName]?.parameters || ["state", "event"];
93
93
  } else if (editing.type === "event") {
94
- const node = getNodeAtPath(state.document, editing.path);
94
+ const node = getNodeAtPath(document, editing.path);
95
95
  return node?.[editing.eventKey]?.parameters || ["state", "event"];
96
96
  }
97
97
  return ["state", "event"];