@jxsuite/studio 0.15.0 → 0.16.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.
@@ -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"];
package/src/studio.js CHANGED
@@ -35,7 +35,13 @@ import {
35
35
  setStatusbarRenderer,
36
36
  mountStatusbar,
37
37
  } from "./panels/statusbar.js";
38
- import { openFile, loadMarkdown, saveFile, exportFile } from "./files/file-ops.js";
38
+ import {
39
+ openFile,
40
+ loadMarkdown,
41
+ saveFile,
42
+ exportFile,
43
+ serializeDocument,
44
+ } from "./files/file-ops.js";
39
45
  import {
40
46
  loadProject as _loadProject,
41
47
  openProject as _openProject,
@@ -128,6 +134,7 @@ async function navigateToComponent(componentPath) {
128
134
  documentPath: tab.documentPath,
129
135
  dirty: tab.doc.dirty,
130
136
  mode: tab.doc.mode,
137
+ sourceFormat: tab.doc.sourceFormat,
131
138
  };
132
139
  if (!tab.session.documentStack) tab.session.documentStack = [];
133
140
  tab.session.documentStack.push(frame);
@@ -136,6 +143,7 @@ async function navigateToComponent(componentPath) {
136
143
  tab.doc.document = parsed;
137
144
  tab.doc.dirty = false;
138
145
  tab.doc.mode = /** @type {any} */ (null);
146
+ tab.doc.sourceFormat = null;
139
147
  tab.documentPath = componentPath;
140
148
  tab.session.selection = null;
141
149
  view.leftTab = "layers";
@@ -156,7 +164,7 @@ async function navigateBack() {
156
164
  if (tab.doc.dirty && tab.documentPath) {
157
165
  try {
158
166
  const platform = getPlatform();
159
- await platform.writeFile(tab.documentPath, JSON.stringify(tab.doc.document, null, 2));
167
+ await platform.writeFile(tab.documentPath, serializeDocument(tab));
160
168
  } catch (/** @type {any} */ e) {
161
169
  const err = /** @type {any} */ (e);
162
170
  statusMessage(`Save error: ${err.message}`);
@@ -169,6 +177,7 @@ async function navigateBack() {
169
177
  tab.doc.document = frame.document;
170
178
  tab.doc.dirty = frame.dirty;
171
179
  tab.doc.mode = frame.mode;
180
+ tab.doc.sourceFormat = frame.sourceFormat;
172
181
  tab.documentPath = frame.documentPath;
173
182
  tab.session.selection = frame.selection;
174
183
  view.leftTab = "layers";
@@ -300,7 +309,9 @@ initCanvasRender({
300
309
  setCanvasMode,
301
310
  openFileFromTree,
302
311
  exportFile,
303
- gitDiffState,
312
+ get gitDiffState() {
313
+ return gitDiffState;
314
+ },
304
315
  setGitDiffState: (/** @type {any} */ state) => {
305
316
  gitDiffState = state;
306
317
  },
@@ -359,6 +370,9 @@ leftPanelMod.mount({
359
370
  registerElementsDnD,
360
371
  registerComponentsDnD,
361
372
  setupTreeKeyboard,
373
+ setGitDiffState: (/** @type {any} */ state) => {
374
+ gitDiffState = state;
375
+ },
362
376
  });
363
377
 
364
378
  // Register all renderers with the store so render()/renderOnly() work
@@ -578,7 +592,7 @@ function scheduleAutosave() {
578
592
  if (t?.fileHandle && t.doc.dirty && "createWritable" in t.fileHandle) {
579
593
  try {
580
594
  const writable = await t.fileHandle.createWritable();
581
- await writable.write(JSON.stringify(t.doc.document, null, 2));
595
+ await writable.write(serializeDocument(t));
582
596
  await writable.close();
583
597
  t.doc.dirty = false;
584
598
  statusMessage("Auto-saved");
@@ -33,7 +33,7 @@ export function transactDoc(tab, mutationFn, { skipHistory = false } = {}) {
33
33
 
34
34
  if (!skipHistory) {
35
35
  const snapshot = {
36
- document: structuredClone(raw),
36
+ document: JSON.parse(JSON.stringify(raw)),
37
37
  selection: tab.session.selection ? [...tab.session.selection] : null,
38
38
  };
39
39
  const truncated = tab.history.snapshots.slice(0, tab.history.index + 1);
@@ -319,7 +319,11 @@ export function mutateRemoveDef(tab, name) {
319
319
  export function mutateUpdateDef(tab, name, updates) {
320
320
  const doc = tab.doc.document;
321
321
  if (!doc.state) doc.state = {};
322
- if (!doc.state[name]) doc.state[name] = {};
322
+ if (doc.state[name] == null) {
323
+ doc.state[name] = {};
324
+ } else if (typeof doc.state[name] !== "object") {
325
+ doc.state[name] = { default: doc.state[name] };
326
+ }
323
327
  Object.assign(doc.state[name], updates);
324
328
  for (const k of Object.keys(doc.state[name])) {
325
329
  if (doc.state[name][k] === undefined || doc.state[name][k] === null) {
@@ -84,6 +84,50 @@ export function closeTab(tabId) {
84
84
  }
85
85
  }
86
86
 
87
+ /** Close all open tabs, disposing each. Defers reactivity until fully cleared. */
88
+ export function closeAllTabs() {
89
+ const tabs = [...workspace.tabs.values()];
90
+ workspace.tabs.clear();
91
+ workspace.tabOrder = [];
92
+ workspace.activeTabId = null;
93
+ for (const tab of tabs) {
94
+ disposeTab(tab);
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Replace all existing tabs with a new one atomically. Ensures activeTab is never null during the
100
+ * transition.
101
+ *
102
+ * @param {{
103
+ * id: string;
104
+ * documentPath?: string | null;
105
+ * document: Record<string, any>;
106
+ * frontmatter?: Record<string, unknown>;
107
+ * sourceFormat?: string | null;
108
+ * }} newTabOpts
109
+ * @returns {import("../tabs/tab.js").Tab}
110
+ */
111
+ export function replaceAllTabs(newTabOpts) {
112
+ const oldIds = [...workspace.tabs.keys()];
113
+ const oldTabs = [...workspace.tabs.values()];
114
+
115
+ const newTab = createTab(newTabOpts);
116
+ workspace.tabs.set(newTab.id, newTab);
117
+ workspace.activeTabId = newTab.id;
118
+ workspace.tabOrder = [newTab.id];
119
+
120
+ for (const id of oldIds) {
121
+ if (id === newTab.id) continue;
122
+ workspace.tabs.delete(id);
123
+ }
124
+ for (const tab of oldTabs) {
125
+ disposeTab(tab);
126
+ }
127
+
128
+ return newTab;
129
+ }
130
+
87
131
  /**
88
132
  * Switch to an existing tab.
89
133
  *