@jxsuite/studio 0.28.2 → 0.28.4

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 (104) hide show
  1. package/dist/studio.js +86144 -83971
  2. package/dist/studio.js.map +229 -219
  3. package/package.json +5 -5
  4. package/src/browse/browse-modal.ts +9 -3
  5. package/src/browse/browse.ts +139 -82
  6. package/src/canvas/canvas-diff.ts +35 -18
  7. package/src/canvas/canvas-helpers.ts +55 -21
  8. package/src/canvas/canvas-live-render.ts +140 -89
  9. package/src/canvas/canvas-render.ts +127 -82
  10. package/src/canvas/canvas-utils.ts +68 -42
  11. package/src/canvas/nested-site-style.ts +15 -7
  12. package/src/editor/component-inline-edit.ts +90 -51
  13. package/src/editor/content-inline-edit.ts +75 -72
  14. package/src/editor/context-menu.ts +125 -71
  15. package/src/editor/convert-targets.ts +17 -13
  16. package/src/editor/convert-to-component.ts +51 -28
  17. package/src/editor/convert-to-repeater.ts +44 -19
  18. package/src/editor/inline-edit.ts +107 -52
  19. package/src/editor/inline-format.ts +128 -60
  20. package/src/editor/insertion-helper.ts +26 -14
  21. package/src/editor/shortcuts.ts +89 -43
  22. package/src/editor/slash-menu.ts +41 -26
  23. package/src/files/components.ts +9 -7
  24. package/src/files/file-ops.ts +53 -33
  25. package/src/files/files.ts +246 -143
  26. package/src/format/constraints.ts +25 -15
  27. package/src/format/format-host.ts +33 -10
  28. package/src/github/github-auth.ts +24 -14
  29. package/src/github/github-publish.ts +20 -14
  30. package/src/new-project/new-project-modal.ts +27 -18
  31. package/src/panels/activity-bar.ts +27 -26
  32. package/src/panels/ai-panel.ts +100 -44
  33. package/src/panels/block-action-bar.ts +84 -52
  34. package/src/panels/canvas-dnd.ts +47 -28
  35. package/src/panels/data-explorer.ts +24 -11
  36. package/src/panels/dnd.ts +148 -100
  37. package/src/panels/editors.ts +54 -37
  38. package/src/panels/elements-panel.ts +23 -13
  39. package/src/panels/events-panel.ts +48 -38
  40. package/src/panels/git-panel.ts +95 -60
  41. package/src/panels/head-panel.ts +138 -84
  42. package/src/panels/imports-panel.ts +66 -29
  43. package/src/panels/layers-panel.ts +80 -36
  44. package/src/panels/left-panel.ts +86 -53
  45. package/src/panels/overlays.ts +36 -19
  46. package/src/panels/panel-events.ts +48 -27
  47. package/src/panels/panel-scheduler.ts +41 -23
  48. package/src/panels/preview-render.ts +26 -26
  49. package/src/panels/properties-panel.ts +155 -118
  50. package/src/panels/pseudo-preview.ts +31 -15
  51. package/src/panels/quick-search.ts +28 -13
  52. package/src/panels/right-panel.ts +35 -24
  53. package/src/panels/shared.ts +46 -29
  54. package/src/panels/signals-panel.ts +303 -186
  55. package/src/panels/statusbar.ts +27 -12
  56. package/src/panels/style-inputs.ts +30 -24
  57. package/src/panels/style-panel.ts +130 -90
  58. package/src/panels/style-utils.ts +63 -23
  59. package/src/panels/stylebook-layers-panel.ts +27 -30
  60. package/src/panels/stylebook-panel.ts +140 -72
  61. package/src/panels/tab-strip.ts +21 -10
  62. package/src/panels/toolbar.ts +52 -40
  63. package/src/panels/welcome-screen.ts +1 -1
  64. package/src/platform.ts +2 -1
  65. package/src/platforms/devserver.ts +221 -105
  66. package/src/recent-projects.ts +15 -7
  67. package/src/resize-edges.ts +7 -5
  68. package/src/services/cem-export.ts +73 -57
  69. package/src/services/code-services.ts +52 -25
  70. package/src/services/monaco-setup.ts +5 -5
  71. package/src/settings/content-types-editor.ts +133 -62
  72. package/src/settings/css-vars-editor.ts +31 -14
  73. package/src/settings/defs-editor.ts +125 -63
  74. package/src/settings/general-settings.ts +11 -6
  75. package/src/settings/head-editor.ts +30 -15
  76. package/src/settings/schema-field-ui.ts +75 -35
  77. package/src/settings/settings-modal.ts +35 -17
  78. package/src/site-context.ts +80 -47
  79. package/src/state.ts +98 -65
  80. package/src/store.ts +42 -23
  81. package/src/studio.ts +165 -144
  82. package/src/tabs/tab.ts +51 -45
  83. package/src/tabs/transact.ts +230 -109
  84. package/src/types.ts +60 -51
  85. package/src/ui/button-group.ts +3 -2
  86. package/src/ui/color-selector.ts +33 -15
  87. package/src/ui/expression-editor.ts +139 -86
  88. package/src/ui/field-input.ts +35 -18
  89. package/src/ui/field-row.ts +1 -1
  90. package/src/ui/icons.ts +5 -6
  91. package/src/ui/layers.ts +33 -26
  92. package/src/ui/media-picker.ts +13 -7
  93. package/src/ui/panel-resize.ts +39 -20
  94. package/src/ui/spectrum.ts +4 -2
  95. package/src/ui/unit-selector.ts +20 -11
  96. package/src/ui/value-selector.ts +13 -7
  97. package/src/ui/widgets.ts +34 -18
  98. package/src/utils/canvas-media.ts +66 -33
  99. package/src/utils/edit-display.ts +43 -44
  100. package/src/utils/google-fonts.ts +10 -6
  101. package/src/utils/inherited-style.ts +22 -8
  102. package/src/utils/studio-utils.ts +88 -45
  103. package/src/view.ts +9 -5
  104. package/src/workspace/workspace.ts +49 -29
@@ -1,15 +1,39 @@
1
1
  /// <reference lib="dom" />
2
2
  import { toRaw } from "../reactivity";
3
- import { getNodeAtPath, parentElementPath, childIndex, pathsEqual, isAncestor } from "../state";
3
+ import { jsonClone } from "../utils/studio-utils";
4
+ import { childIndex, getNodeAtPath, isAncestor, parentElementPath, pathsEqual } from "../state";
4
5
 
5
6
  import type { Tab } from "../tabs/tab";
6
7
  import type { JxPath } from "../state";
7
8
 
8
9
  import type { JsonValue } from "../types";
9
- import type { JxMutableNode } from "@jxsuite/schema/types";
10
+ import type { JxEventBinding, JxMutableNode, JxStateObject, JxStyle } from "@jxsuite/schema/types";
11
+ import { ensureNestedStyle, getNestedStyle } from "@jxsuite/schema/guards";
10
12
 
11
13
  const HISTORY_LIMIT = 100;
12
14
 
15
+ /** Any value that can sit at a document node key (undefined/null/"" deletes it). */
16
+ export type JxNodeValue =
17
+ | JsonValue
18
+ | JxMutableNode
19
+ | (JxMutableNode | string)[]
20
+ | JxEventBinding
21
+ | undefined;
22
+
23
+ /**
24
+ * The editable children array of a node, created when absent. Mapped-array children (`$prototype:
25
+ * "Array"`) cannot be index-mutated — fail loudly instead of corrupting.
26
+ */
27
+ function childArray(node: JxMutableNode): (JxMutableNode | string)[] {
28
+ if (!node.children) {
29
+ node.children = [];
30
+ }
31
+ if (!Array.isArray(node.children)) {
32
+ throw new TypeError("Cannot insert into mapped-array children; edit the map template instead");
33
+ }
34
+ return node.children;
35
+ }
36
+
13
37
  // ─── Transactional layer ─────────────────────────────────────────────────────
14
38
 
15
39
  /**
@@ -25,7 +49,9 @@ export function transactDoc(
25
49
  mutationFn: (tab: Tab) => void,
26
50
  { skipHistory = false }: { skipHistory?: boolean } = {},
27
51
  ) {
28
- if (!tab) return;
52
+ if (!tab) {
53
+ return;
54
+ }
29
55
  mutationFn(tab);
30
56
 
31
57
  // Replace the document root reference so effects tracking tab.doc.document re-trigger.
@@ -35,12 +61,14 @@ export function transactDoc(
35
61
 
36
62
  if (!skipHistory) {
37
63
  const snapshot = {
38
- document: JSON.parse(JSON.stringify(raw)),
64
+ document: jsonClone(raw),
39
65
  selection: tab.session.selection ? [...tab.session.selection] : null,
40
66
  };
41
67
  const truncated = tab.history.snapshots.slice(0, tab.history.index + 1);
42
68
  truncated.push(snapshot);
43
- if (truncated.length > HISTORY_LIMIT) truncated.shift();
69
+ if (truncated.length > HISTORY_LIMIT) {
70
+ truncated.shift();
71
+ }
44
72
  tab.history.snapshots = truncated;
45
73
  tab.history.index = truncated.length - 1;
46
74
  }
@@ -67,8 +95,10 @@ export function transact(
67
95
 
68
96
  /** @param {Tab} tab */
69
97
  export function undo(tab: Tab) {
70
- if (tab.history.index <= 0) return;
71
- tab.history.index--;
98
+ if (tab.history.index <= 0) {
99
+ return;
100
+ }
101
+ tab.history.index -= 1;
72
102
  const snap = tab.history.snapshots[tab.history.index];
73
103
  tab.doc.document = structuredClone(toRaw(snap.document));
74
104
  tab.session.selection = snap.selection ? [...toRaw(snap.selection)] : null;
@@ -77,8 +107,10 @@ export function undo(tab: Tab) {
77
107
 
78
108
  /** @param {Tab} tab */
79
109
  export function redo(tab: Tab) {
80
- if (tab.history.index >= tab.history.snapshots.length - 1) return;
81
- tab.history.index++;
110
+ if (tab.history.index >= tab.history.snapshots.length - 1) {
111
+ return;
112
+ }
113
+ tab.history.index += 1;
82
114
  const snap = tab.history.snapshots[tab.history.index];
83
115
  tab.doc.document = structuredClone(toRaw(snap.document));
84
116
  tab.session.selection = snap.selection ? [...toRaw(snap.selection)] : null;
@@ -100,9 +132,10 @@ export function mutateInsertNode(
100
132
  nodeDef: JxMutableNode,
101
133
  ) {
102
134
  const parent = getNodeAtPath(tab.doc.document, parentPath);
103
- if (!parent) return;
104
- if (!parent.children) parent.children = [];
105
- parent.children.splice(index, 0, structuredClone(nodeDef));
135
+ if (!parent) {
136
+ return;
137
+ }
138
+ childArray(parent).splice(index, 0, structuredClone(nodeDef));
106
139
  }
107
140
 
108
141
  /**
@@ -110,7 +143,9 @@ export function mutateInsertNode(
110
143
  * @param {JxPath} path
111
144
  */
112
145
  export function mutateRemoveNode(tab: Tab, path: JxPath) {
113
- if (!path || path.length < 2) return;
146
+ if (!path || path.length < 2) {
147
+ return;
148
+ }
114
149
  const elemPath = parentElementPath(path) as JxPath;
115
150
  const idx = childIndex(path) as number;
116
151
  (getNodeAtPath(tab.doc.document, elemPath).children as JxMutableNode[]).splice(idx, 1);
@@ -124,9 +159,13 @@ export function mutateRemoveNode(tab: Tab, path: JxPath) {
124
159
  * @param {JxPath} path
125
160
  */
126
161
  export function mutateDuplicateNode(tab: Tab, path: JxPath) {
127
- if (!path || path.length < 2) return;
162
+ if (!path || path.length < 2) {
163
+ return;
164
+ }
128
165
  const node = getNodeAtPath(tab.doc.document, path);
129
- if (!node) return;
166
+ if (!node) {
167
+ return;
168
+ }
130
169
  const elemPath = parentElementPath(path) as JxPath;
131
170
  const idx = childIndex(path) as number;
132
171
  const clone = structuredClone(toRaw(node));
@@ -139,15 +178,19 @@ export function mutateDuplicateNode(tab: Tab, path: JxPath) {
139
178
  * @param {JxPath} path
140
179
  * @param {string} wrapperTag
141
180
  */
142
- export function mutateWrapNode(tab: Tab, path: JxPath, wrapperTag: string = "div") {
143
- if (!path || path.length < 2) return;
181
+ export function mutateWrapNode(tab: Tab, path: JxPath, wrapperTag = "div") {
182
+ if (!path || path.length < 2) {
183
+ return;
184
+ }
144
185
  const node = getNodeAtPath(tab.doc.document, path);
145
- if (!node) return;
186
+ if (!node) {
187
+ return;
188
+ }
146
189
  const elemPath = parentElementPath(path) as JxPath;
147
190
  const idx = childIndex(path) as number;
148
191
  const wrapper = {
149
- tagName: wrapperTag,
150
192
  children: [structuredClone(toRaw(node))],
193
+ tagName: wrapperTag,
151
194
  };
152
195
  (getNodeAtPath(tab.doc.document, elemPath).children as JxMutableNode[]).splice(idx, 1, wrapper);
153
196
  tab.session.selection = [...elemPath, "children", idx];
@@ -163,16 +206,23 @@ export function mutateMoveNode(tab: Tab, fromPath: JxPath, toParentPath: JxPath,
163
206
  const doc = tab.doc.document;
164
207
  const fromParentPath = parentElementPath(fromPath) as JxPath;
165
208
  const fromIdx = childIndex(fromPath) as number;
166
- if (!fromParentPath || typeof fromIdx !== "number") return;
209
+ if (!fromParentPath || typeof fromIdx !== "number") {
210
+ return;
211
+ }
167
212
  const fromParent = getNodeAtPath(doc, fromParentPath);
168
213
  const toParent = getNodeAtPath(doc, toParentPath);
169
- if (!fromParent || !Array.isArray(fromParent.children) || !toParent) return;
170
- const [node] = (fromParent.children as JxMutableNode[]).splice(fromIdx, 1);
171
- if (node === undefined) return;
172
- if (!toParent.children) toParent.children = [];
214
+ if (!fromParent || !Array.isArray(fromParent.children) || !toParent) {
215
+ return;
216
+ }
217
+ const [node] = fromParent.children.splice(fromIdx, 1);
218
+ if (node === undefined) {
219
+ return;
220
+ }
173
221
  let adjustedIndex = toIndex;
174
- if (fromParent === toParent && fromIdx < toIndex) adjustedIndex--;
175
- toParent.children.splice(adjustedIndex, 0, node);
222
+ if (fromParent === toParent && fromIdx < toIndex) {
223
+ adjustedIndex -= 1;
224
+ }
225
+ childArray(toParent).splice(adjustedIndex, 0, node);
176
226
 
177
227
  if (pathsEqual(tab.session.selection, fromPath)) {
178
228
  let idx = toIndex;
@@ -191,12 +241,15 @@ export function mutateMoveNode(tab: Tab, fromPath: JxPath, toParentPath: JxPath,
191
241
  * @param {Tab} tab
192
242
  * @param {JxPath} path
193
243
  * @param {string} key
194
- * @param {JsonValue} value
244
+ * @param {JxNodeValue} value
195
245
  */
196
- export function mutateUpdateProperty(tab: Tab, path: JxPath, key: string, value: JsonValue) {
246
+ export function mutateUpdateProperty(tab: Tab, path: JxPath, key: string, value?: JxNodeValue) {
197
247
  const node = getNodeAtPath(tab.doc.document, path);
198
- if (value === undefined || value === null || value === "") delete node[key];
199
- else node[key] = value;
248
+ if (value === undefined || value === null || value === "") {
249
+ delete node[key];
250
+ } else {
251
+ node[key] = value;
252
+ }
200
253
  }
201
254
 
202
255
  /**
@@ -207,10 +260,17 @@ export function mutateUpdateProperty(tab: Tab, path: JxPath, key: string, value:
207
260
  */
208
261
  export function mutateUpdateStyle(tab: Tab, path: JxPath, prop: string, value: string | undefined) {
209
262
  const node = getNodeAtPath(tab.doc.document, path);
210
- if (!node.style) node.style = {};
211
- if (value === undefined || value === "") delete node.style[prop];
212
- else node.style[prop] = value;
213
- if (Object.keys(node.style).length === 0) delete node.style;
263
+ if (!node.style) {
264
+ node.style = {};
265
+ }
266
+ if (value === undefined || value === "") {
267
+ delete node.style[prop];
268
+ } else {
269
+ node.style[prop] = value;
270
+ }
271
+ if (Object.keys(node.style).length === 0) {
272
+ delete node.style;
273
+ }
214
274
  }
215
275
 
216
276
  /**
@@ -223,13 +283,20 @@ export function mutateUpdateAttribute(
223
283
  tab: Tab,
224
284
  path: JxPath,
225
285
  attr: string,
226
- value: string | undefined,
286
+ value?: string | undefined,
227
287
  ) {
228
288
  const node = getNodeAtPath(tab.doc.document, path);
229
- if (!node.attributes) node.attributes = {};
230
- if (value === undefined || value === "") delete node.attributes[attr];
231
- else node.attributes[attr] = value;
232
- if (Object.keys(node.attributes).length === 0) delete node.attributes;
289
+ if (!node.attributes) {
290
+ node.attributes = {};
291
+ }
292
+ if (value === undefined || value === "") {
293
+ delete node.attributes[attr];
294
+ } else {
295
+ node.attributes[attr] = value;
296
+ }
297
+ if (Object.keys(node.attributes).length === 0) {
298
+ delete node.attributes;
299
+ }
233
300
  }
234
301
 
235
302
  /**
@@ -246,18 +313,26 @@ export function mutateUpdateMediaStyle(
246
313
  prop: string,
247
314
  value: string | undefined,
248
315
  ) {
249
- if (!mediaName) return mutateUpdateStyle(tab, path, prop, value);
316
+ if (!mediaName) {
317
+ return mutateUpdateStyle(tab, path, prop, value);
318
+ }
250
319
  const node = getNodeAtPath(tab.doc.document, path);
251
- if (!node.style) node.style = {};
320
+ if (!node.style) {
321
+ node.style = {};
322
+ }
252
323
  const key = `@${mediaName}`;
253
- if (!node.style[key]) node.style[key] = {};
324
+ const media = ensureNestedStyle(node.style, key);
254
325
  if (value === undefined || value === "") {
255
- delete node.style[key][prop];
256
- if (Object.keys(node.style[key]).length === 0) delete node.style[key];
326
+ delete media[prop];
327
+ if (Object.keys(media).length === 0) {
328
+ delete node.style[key];
329
+ }
257
330
  } else {
258
- node.style[key][prop] = value;
331
+ media[prop] = value;
332
+ }
333
+ if (Object.keys(node.style).length === 0) {
334
+ delete node.style;
259
335
  }
260
- if (Object.keys(node.style).length === 0) delete node.style;
261
336
  }
262
337
 
263
338
  /**
@@ -275,15 +350,21 @@ export function mutateUpdateNestedStyle(
275
350
  value: string | undefined,
276
351
  ) {
277
352
  const node = getNodeAtPath(tab.doc.document, path);
278
- if (!node.style) node.style = {};
279
- if (!node.style[selector]) node.style[selector] = {};
353
+ if (!node.style) {
354
+ node.style = {};
355
+ }
356
+ const block = ensureNestedStyle(node.style, selector);
280
357
  if (value === undefined || value === "") {
281
- delete node.style[selector][prop];
282
- if (Object.keys(node.style[selector]).length === 0) delete node.style[selector];
358
+ delete block[prop];
359
+ if (Object.keys(block).length === 0) {
360
+ delete node.style[selector];
361
+ }
283
362
  } else {
284
- node.style[selector][prop] = value;
363
+ block[prop] = value;
364
+ }
365
+ if (Object.keys(node.style).length === 0) {
366
+ delete node.style;
285
367
  }
286
- if (Object.keys(node.style).length === 0) delete node.style;
287
368
  }
288
369
 
289
370
  /**
@@ -303,18 +384,26 @@ export function mutateUpdateMediaNestedStyle(
303
384
  value: string | undefined,
304
385
  ) {
305
386
  const node = getNodeAtPath(tab.doc.document, path);
306
- if (!node.style) node.style = {};
387
+ if (!node.style) {
388
+ node.style = {};
389
+ }
307
390
  const key = `@${mediaName}`;
308
- if (!node.style[key]) node.style[key] = {};
309
- if (!node.style[key][selector]) node.style[key][selector] = {};
391
+ const media = ensureNestedStyle(node.style, key);
392
+ const block = ensureNestedStyle(media, selector);
310
393
  if (value === undefined || value === "") {
311
- delete node.style[key][selector][prop];
312
- if (Object.keys(node.style[key][selector]).length === 0) delete node.style[key][selector];
313
- if (Object.keys(node.style[key]).length === 0) delete node.style[key];
394
+ delete block[prop];
395
+ if (Object.keys(block).length === 0) {
396
+ delete media[selector];
397
+ }
398
+ if (Object.keys(media).length === 0) {
399
+ delete node.style[key];
400
+ }
314
401
  } else {
315
- node.style[key][selector][prop] = value;
402
+ block[prop] = value;
403
+ }
404
+ if (Object.keys(node.style).length === 0) {
405
+ delete node.style;
316
406
  }
317
- if (Object.keys(node.style).length === 0) delete node.style;
318
407
  }
319
408
 
320
409
  /**
@@ -335,19 +424,20 @@ export function mutateUpdateNestedStylePath(
335
424
  value: string | undefined,
336
425
  ) {
337
426
  const node = getNodeAtPath(tab.doc.document, path);
338
- if (!node.style) node.style = {};
427
+ if (!node.style) {
428
+ node.style = {};
429
+ }
339
430
  let obj = node.style;
340
431
  for (const seg of stylePath) {
341
- if (!obj[seg] || typeof obj[seg] !== "object") obj[seg] = {};
342
- obj = obj[seg];
432
+ obj = ensureNestedStyle(obj, seg);
343
433
  }
344
434
  if (value === undefined || value === "") {
345
435
  delete obj[prop];
346
436
  // Clean up empty parent objects
347
- let cur = node.style;
348
- for (let i = 0; i < stylePath.length; i++) {
349
- const child = cur[stylePath[i]];
350
- if (child && typeof child === "object" && Object.keys(child).length === 0) {
437
+ let cur: JxStyle | undefined = node.style;
438
+ for (let i = 0; i < stylePath.length && cur; i++) {
439
+ const child = getNestedStyle(cur, stylePath[i]);
440
+ if (child && Object.keys(child).length === 0) {
351
441
  delete cur[stylePath[i]];
352
442
  break;
353
443
  }
@@ -356,7 +446,9 @@ export function mutateUpdateNestedStylePath(
356
446
  } else {
357
447
  obj[prop] = value;
358
448
  }
359
- if (Object.keys(node.style).length === 0) delete node.style;
449
+ if (Object.keys(node.style).length === 0) {
450
+ delete node.style;
451
+ }
360
452
  }
361
453
 
362
454
  /**
@@ -378,30 +470,35 @@ export function mutateUpdateMediaNestedStylePath(
378
470
  value: string | undefined,
379
471
  ) {
380
472
  const node = getNodeAtPath(tab.doc.document, path);
381
- if (!node.style) node.style = {};
473
+ if (!node.style) {
474
+ node.style = {};
475
+ }
382
476
  const key = `@${mediaName}`;
383
- if (!node.style[key]) node.style[key] = {};
384
- let obj = node.style[key];
477
+ const media = ensureNestedStyle(node.style, key);
478
+ let obj = media;
385
479
  for (const seg of stylePath) {
386
- if (!obj[seg] || typeof obj[seg] !== "object") obj[seg] = {};
387
- obj = obj[seg];
480
+ obj = ensureNestedStyle(obj, seg);
388
481
  }
389
482
  if (value === undefined || value === "") {
390
483
  delete obj[prop];
391
- let cur = node.style[key];
392
- for (let i = 0; i < stylePath.length; i++) {
393
- const child = cur[stylePath[i]];
394
- if (child && typeof child === "object" && Object.keys(child).length === 0) {
484
+ let cur: JxStyle | undefined = media;
485
+ for (let i = 0; i < stylePath.length && cur; i++) {
486
+ const child = getNestedStyle(cur, stylePath[i]);
487
+ if (child && Object.keys(child).length === 0) {
395
488
  delete cur[stylePath[i]];
396
489
  break;
397
490
  }
398
491
  cur = child;
399
492
  }
400
- if (Object.keys(node.style[key]).length === 0) delete node.style[key];
493
+ if (Object.keys(media).length === 0) {
494
+ delete node.style[key];
495
+ }
401
496
  } else {
402
497
  obj[prop] = value;
403
498
  }
404
- if (Object.keys(node.style).length === 0) delete node.style;
499
+ if (Object.keys(node.style).length === 0) {
500
+ delete node.style;
501
+ }
405
502
  }
406
503
 
407
504
  /**
@@ -409,11 +506,7 @@ export function mutateUpdateMediaNestedStylePath(
409
506
  * @param {JxPath} path
410
507
  * @param {Record<string, string | undefined> | undefined} style
411
508
  */
412
- export function mutateReplaceStyle(
413
- tab: Tab,
414
- path: JxPath,
415
- style: Record<string, string | undefined> | undefined,
416
- ) {
509
+ export function mutateReplaceStyle(tab: Tab, path: JxPath, style: JxStyle | undefined) {
417
510
  const node = getNodeAtPath(tab.doc.document, path);
418
511
  if (style && Object.keys(style).length > 0) {
419
512
  node.style = style;
@@ -429,7 +522,9 @@ export function mutateReplaceStyle(
429
522
  */
430
523
  export function mutateAddDef(tab: Tab, name: string, def: Record<string, JsonValue>) {
431
524
  const doc = tab.doc.document;
432
- if (!doc.state) doc.state = {};
525
+ if (!doc.state) {
526
+ doc.state = {};
527
+ }
433
528
  doc.state[name] = def;
434
529
  }
435
530
 
@@ -441,7 +536,9 @@ export function mutateRemoveDef(tab: Tab, name: string) {
441
536
  const doc = tab.doc.document;
442
537
  if (doc.state) {
443
538
  delete doc.state[name];
444
- if (Object.keys(doc.state).length === 0) delete doc.state;
539
+ if (Object.keys(doc.state).length === 0) {
540
+ delete doc.state;
541
+ }
445
542
  }
446
543
  }
447
544
 
@@ -450,18 +547,23 @@ export function mutateRemoveDef(tab: Tab, name: string) {
450
547
  * @param {string} name
451
548
  * @param {Record<string, JsonValue>} updates
452
549
  */
453
- export function mutateUpdateDef(tab: Tab, name: string, updates: Record<string, JsonValue>) {
550
+ export function mutateUpdateDef(tab: Tab, name: string, updates: Record<string, unknown>) {
454
551
  const doc = tab.doc.document;
455
- if (!doc.state) doc.state = {};
456
- if (doc.state[name] == null) {
457
- doc.state[name] = {};
458
- } else if (typeof doc.state[name] !== "object") {
459
- doc.state[name] = { default: doc.state[name] };
460
- }
461
- Object.assign(doc.state[name], updates);
462
- for (const k of Object.keys(doc.state[name])) {
463
- if (doc.state[name][k] === undefined || doc.state[name][k] === null) {
464
- delete doc.state[name][k];
552
+ if (!doc.state) {
553
+ doc.state = {};
554
+ }
555
+ const existing = doc.state[name];
556
+ const entry: JxStateObject =
557
+ existing == null
558
+ ? {}
559
+ : typeof existing !== "object" || Array.isArray(existing)
560
+ ? { default: existing }
561
+ : (existing as JxStateObject);
562
+ doc.state[name] = entry;
563
+ Object.assign(entry, updates);
564
+ for (const k of Object.keys(entry)) {
565
+ if (entry[k] === undefined || entry[k] === null) {
566
+ delete entry[k];
465
567
  }
466
568
  }
467
569
  }
@@ -473,7 +575,9 @@ export function mutateUpdateDef(tab: Tab, name: string, updates: Record<string,
473
575
  */
474
576
  export function mutateRenameDef(tab: Tab, oldName: string, newName: string) {
475
577
  const doc = tab.doc.document;
476
- if (!doc.state || !doc.state[oldName]) return;
578
+ if (!doc.state || !doc.state[oldName]) {
579
+ return;
580
+ }
477
581
  doc.state[newName] = doc.state[oldName];
478
582
  delete doc.state[oldName];
479
583
  }
@@ -483,12 +587,16 @@ export function mutateRenameDef(tab: Tab, oldName: string, newName: string) {
483
587
  * @param {string} name
484
588
  * @param {string | undefined} query
485
589
  */
486
- export function mutateUpdateMedia(tab: Tab, name: string, query: string | undefined) {
590
+ export function mutateUpdateMedia(tab: Tab, name: string, query?: string | undefined) {
487
591
  const doc = tab.doc.document;
488
- if (!doc.$media) doc.$media = {};
592
+ if (!doc.$media) {
593
+ doc.$media = {};
594
+ }
489
595
  if (query === undefined || query === "") {
490
596
  delete doc.$media[name];
491
- if (Object.keys(doc.$media).length === 0) delete doc.$media;
597
+ if (Object.keys(doc.$media).length === 0) {
598
+ delete doc.$media;
599
+ }
492
600
  } else {
493
601
  doc.$media[name] = query;
494
602
  }
@@ -502,10 +610,17 @@ export function mutateUpdateMedia(tab: Tab, name: string, query: string | undefi
502
610
  */
503
611
  export function mutateUpdateProp(tab: Tab, path: JxPath, propName: string, value: JsonValue) {
504
612
  const node = getNodeAtPath(tab.doc.document, path);
505
- if (!node.$props) node.$props = {};
506
- if (value === undefined || value === null || value === "") delete node.$props[propName];
507
- else node.$props[propName] = value;
508
- if (Object.keys(node.$props).length === 0) delete node.$props;
613
+ if (!node.$props) {
614
+ node.$props = {};
615
+ }
616
+ if (value === undefined || value === null || value === "") {
617
+ delete node.$props[propName];
618
+ } else {
619
+ node.$props[propName] = value;
620
+ }
621
+ if (Object.keys(node.$props).length === 0) {
622
+ delete node.$props;
623
+ }
509
624
  }
510
625
 
511
626
  /**
@@ -521,7 +636,9 @@ export function mutateAddSwitchCase(
521
636
  caseDef?: JxMutableNode,
522
637
  ) {
523
638
  const node = getNodeAtPath(tab.doc.document, path);
524
- if (!node.cases) node.cases = {};
639
+ if (!node.cases) {
640
+ node.cases = {};
641
+ }
525
642
  node.cases[caseName] = caseDef || { tagName: "div", textContent: caseName };
526
643
  }
527
644
 
@@ -532,7 +649,9 @@ export function mutateAddSwitchCase(
532
649
  */
533
650
  export function mutateRemoveSwitchCase(tab: Tab, path: JxPath, caseName: string) {
534
651
  const node = getNodeAtPath(tab.doc.document, path);
535
- if (node.cases) delete node.cases[caseName];
652
+ if (node.cases) {
653
+ delete node.cases[caseName];
654
+ }
536
655
  }
537
656
 
538
657
  /**
@@ -543,7 +662,9 @@ export function mutateRemoveSwitchCase(tab: Tab, path: JxPath, caseName: string)
543
662
  */
544
663
  export function mutateRenameSwitchCase(tab: Tab, path: JxPath, oldName: string, newName: string) {
545
664
  const node = getNodeAtPath(tab.doc.document, path);
546
- if (!node.cases || !node.cases[oldName]) return;
665
+ if (!node.cases || !node.cases[oldName]) {
666
+ return;
667
+ }
547
668
  node.cases[newName] = node.cases[oldName];
548
669
  delete node.cases[oldName];
549
670
  }
@@ -557,7 +678,7 @@ export function mutateRenameSwitchCase(tab: Tab, path: JxPath, oldName: string,
557
678
  * @param {string} field
558
679
  * @param {JsonValue} value
559
680
  */
560
- export function mutateUpdateFrontmatter(tab: Tab, field: string, value: JsonValue) {
681
+ export function mutateUpdateFrontmatter(tab: Tab, field: string, value?: JsonValue) {
561
682
  if (value === undefined || value === null || value === "") {
562
683
  delete tab.doc.content.frontmatter[field];
563
684
  } else {