@jxsuite/studio 0.10.2 → 0.13.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 (53) hide show
  1. package/README.md +82 -0
  2. package/dist/studio.js +5114 -3424
  3. package/dist/studio.js.map +65 -49
  4. package/package.json +6 -3
  5. package/src/browse/browse.js +27 -3
  6. package/src/canvas/canvas-diff.js +184 -0
  7. package/src/canvas/canvas-helpers.js +10 -14
  8. package/src/canvas/canvas-live-render.js +136 -17
  9. package/src/canvas/canvas-render.js +154 -21
  10. package/src/canvas/canvas-utils.js +21 -23
  11. package/src/editor/component-inline-edit.js +54 -41
  12. package/src/editor/content-inline-edit.js +46 -47
  13. package/src/editor/context-menu.js +63 -39
  14. package/src/editor/convert-to-component.js +11 -14
  15. package/src/editor/insertion-helper.js +8 -10
  16. package/src/editor/shortcuts.js +69 -39
  17. package/src/files/components.js +15 -4
  18. package/src/files/files.js +72 -24
  19. package/src/panels/activity-bar.js +29 -7
  20. package/src/panels/block-action-bar.js +104 -80
  21. package/src/panels/canvas-dnd.js +132 -50
  22. package/src/panels/dnd.js +32 -28
  23. package/src/panels/editors.js +7 -14
  24. package/src/panels/elements-panel.js +9 -3
  25. package/src/panels/events-panel.js +44 -39
  26. package/src/panels/git-panel.js +45 -3
  27. package/src/panels/layers-panel.js +16 -11
  28. package/src/panels/left-panel.js +108 -43
  29. package/src/panels/overlays.js +97 -35
  30. package/src/panels/panel-events.js +18 -11
  31. package/src/panels/properties-panel.js +467 -98
  32. package/src/panels/right-panel.js +85 -37
  33. package/src/panels/shared.js +0 -22
  34. package/src/panels/signals-panel.js +125 -54
  35. package/src/panels/statusbar.js +23 -8
  36. package/src/panels/style-inputs.js +4 -2
  37. package/src/panels/style-panel.js +128 -105
  38. package/src/panels/stylebook-panel.js +42 -17
  39. package/src/panels/tab-strip.js +124 -0
  40. package/src/panels/toolbar.js +39 -9
  41. package/src/reactivity.js +28 -0
  42. package/src/settings/content-types-editor.js +78 -8
  43. package/src/settings/defs-editor.js +56 -7
  44. package/src/settings/schema-field-ui.js +99 -11
  45. package/src/site-context.js +105 -0
  46. package/src/state.js +0 -456
  47. package/src/store.js +105 -124
  48. package/src/studio.js +112 -121
  49. package/src/tabs/tab.js +153 -0
  50. package/src/tabs/transact.js +406 -0
  51. package/src/ui/spectrum.js +2 -0
  52. package/src/view.js +3 -0
  53. package/src/workspace/workspace.js +89 -0
@@ -8,6 +8,7 @@ import { ref } from "lit-html/directives/ref.js";
8
8
  import * as monaco from "monaco-editor/esm/vs/editor/editor.api.js";
9
9
 
10
10
  import { canvasWrap, canvasPanels, updateCanvas } from "../store.js";
11
+ import { activeTab } from "../workspace/workspace.js";
11
12
  import { view } from "../view.js";
12
13
  import {
13
14
  canvasPanelTemplate,
@@ -29,6 +30,7 @@ import { renderCanvasLive } from "./canvas-live-render.js";
29
30
  import { renderCanvasNode } from "../panels/preview-render.js";
30
31
  import { registerPanelDnD } from "../panels/canvas-dnd.js";
31
32
  import { registerPanelEvents } from "../panels/panel-events.js";
33
+ import { computeDocumentDiff } from "./canvas-diff.js";
32
34
  import { updateForcedPseudoPreview } from "../panels/pseudo-preview.js";
33
35
  import { renderStylebookMode } from "../panels/stylebook-panel.js";
34
36
  import { dismissLinkPopover, dismissBlockActionBar } from "../panels/block-action-bar.js";
@@ -49,10 +51,10 @@ let _ctx = null;
49
51
  * @param {{
50
52
  * getCanvasMode: () => string;
51
53
  * setCanvasMode: (mode: string) => void;
52
- * getState: () => any;
53
- * update: (s: any) => void;
54
54
  * openFileFromTree: (path: string) => void;
55
55
  * exportFile: () => void;
56
+ * gitDiffState: any;
57
+ * setGitDiffState: (state: any) => void;
56
58
  * }} ctx
57
59
  */
58
60
  export function initCanvasRender(ctx) {
@@ -60,7 +62,9 @@ export function initCanvasRender(ctx) {
60
62
  }
61
63
 
62
64
  export function renderCanvas() {
63
- const S = _ctx.getState();
65
+ const tab = activeTab.value;
66
+ if (!tab) return;
67
+ const S = { document: tab.doc.document, ui: tab.session.ui, mode: tab.doc.mode };
64
68
  const canvasMode = _ctx.getCanvasMode();
65
69
 
66
70
  // Advance render generation so stale async renders from the previous cycle bail out
@@ -230,7 +234,9 @@ export function renderCanvas() {
230
234
  debounce = setTimeout(() => {
231
235
  try {
232
236
  const parsed = JSON.parse(view.monacoEditor.getValue());
233
- _ctx.update({ ..._ctx.getState(), document: parsed, dirty: true });
237
+ const tab = activeTab.value;
238
+ tab.doc.document = parsed;
239
+ tab.doc.dirty = true;
234
240
  } catch {
235
241
  // Invalid JSON — don't update state
236
242
  }
@@ -239,6 +245,75 @@ export function renderCanvas() {
239
245
  return;
240
246
  }
241
247
 
248
+ // Git diff mode — render original (left) and current (right) side-by-side
249
+ if (canvasMode === "git-diff") {
250
+ if (!_ctx.gitDiffState) {
251
+ // Fallback to design mode if diff state is missing
252
+ _ctx.setCanvasMode("design");
253
+ renderCanvas();
254
+ return;
255
+ }
256
+
257
+ canvasWrap.style.padding = "0";
258
+ canvasWrap.style.overflow = "hidden";
259
+
260
+ const gitDiffState = _ctx.gitDiffState;
261
+ const { tpl: origPanelTpl, panel: origPanel } = canvasPanelTemplate(
262
+ "git-diff-original",
263
+ "Original",
264
+ false,
265
+ "50%",
266
+ );
267
+ const { tpl: currPanelTpl, panel: currPanel } = canvasPanelTemplate(
268
+ "git-diff-current",
269
+ "Current",
270
+ false,
271
+ "50%",
272
+ );
273
+
274
+ const diffTpl = html`
275
+ <div style="display: flex; height: 100%; gap: 0;">
276
+ <div style="flex: 1; overflow: hidden; display: flex; flex-direction: column;">
277
+ ${origPanelTpl}
278
+ </div>
279
+ <div style="flex: 1; overflow: hidden; display: flex; flex-direction: column;">
280
+ ${currPanelTpl}
281
+ </div>
282
+ </div>
283
+ `;
284
+
285
+ litRender(diffTpl, canvasWrap);
286
+ canvasPanels.push(/** @type {any} */ (origPanel));
287
+ canvasPanels.push(/** @type {any} */ (currPanel));
288
+
289
+ // Parse original document from git content
290
+ let originalDoc = null;
291
+ try {
292
+ if (gitDiffState.isMarkdown) {
293
+ // For markdown, we need to convert it to Jx format
294
+ // For now, use a simple placeholder until we have markdown parsing
295
+ originalDoc = {
296
+ tagName: "div",
297
+ children: [{ tagName: "p", textContent: "Original (markdown)" }],
298
+ };
299
+ } else {
300
+ // Parse as JSON
301
+ originalDoc = JSON.parse(gitDiffState.originalContent);
302
+ }
303
+ } catch {
304
+ originalDoc = {
305
+ tagName: "div",
306
+ children: [{ tagName: "p", textContent: "Failed to parse original" }],
307
+ };
308
+ }
309
+
310
+ const currDoc = S.document;
311
+
312
+ renderCanvasIntoPanel(origPanel, new Set(), S.ui.featureToggles, originalDoc, gitDiffState);
313
+ renderCanvasIntoPanel(currPanel, new Set(), S.ui.featureToggles, currDoc, gitDiffState);
314
+ return;
315
+ }
316
+
242
317
  // Edit (content) mode — centered column, no panzoom, always 100%
243
318
  if (canvasMode === "edit") {
244
319
  if (modeChanged) {
@@ -249,14 +324,15 @@ export function renderCanvas() {
249
324
  resetZoomIndicator();
250
325
  }
251
326
 
327
+ const { baseWidth } = parseMediaEntries(getEffectiveMedia(S.document.$media));
252
328
  const { tpl: panelTpl, panel } = canvasPanelTemplate(null, null, true);
253
329
  const editTpl = html`
254
330
  <div class="content-edit-canvas">
255
- <div class="content-edit-column">${panelTpl}</div>
331
+ <div class="content-edit-column" style="max-width:${baseWidth}px">${panelTpl}</div>
256
332
  </div>
257
333
  `;
258
334
  litRender(editTpl, canvasWrap);
259
- canvasPanels.push(panel);
335
+ canvasPanels.push(/** @type {any} */ (panel));
260
336
  renderCanvasIntoPanel(panel, new Set(), S.ui.featureToggles);
261
337
  return;
262
338
  }
@@ -301,7 +377,7 @@ export function renderCanvas() {
301
377
  `,
302
378
  canvasWrap,
303
379
  );
304
- canvasPanels.push(panel);
380
+ canvasPanels.push(/** @type {any} */ (panel));
305
381
  renderCanvasIntoPanel(panel, new Set(), featureToggles);
306
382
  applyTransform();
307
383
  if (modeChanged) {
@@ -353,7 +429,7 @@ export function renderCanvas() {
353
429
  );
354
430
 
355
431
  for (const { panel, activeSet } of panelEntries) {
356
- canvasPanels.push(panel);
432
+ canvasPanels.push(/** @type {any} */ (panel));
357
433
  renderCanvasIntoPanel(panel, activeSet, featureToggles);
358
434
  }
359
435
 
@@ -375,29 +451,51 @@ export function renderCanvas() {
375
451
  * structural preview.
376
452
  *
377
453
  * @param {any} panel
378
- * @param {any} activeBreakpoints
454
+ * @param {Set<string>} activeBreakpoints
379
455
  * @param {any} featureToggles
456
+ * @param {any} [docOverride] - Optional document to render (for diff mode). Uses active tab doc if
457
+ * not provided.
458
+ * @param {any} [gitDiffState] - Optional diff state. If provided, computes and applies diff
459
+ * highlighting.
380
460
  */
381
- function renderCanvasIntoPanel(panel, activeBreakpoints, featureToggles) {
461
+ function renderCanvasIntoPanel(
462
+ panel,
463
+ activeBreakpoints,
464
+ featureToggles,
465
+ docOverride = null,
466
+ gitDiffState = null,
467
+ ) {
382
468
  const gen = view.renderGeneration;
383
- const S = _ctx.getState();
384
- renderCanvasLive(gen, S.document, panel.canvas).then((/** @type {any} */ scope) => {
469
+ const tab = activeTab.value;
470
+ const docToRender = docOverride || tab?.doc.document;
471
+
472
+ renderCanvasLive(gen, docToRender, panel.canvas).then((/** @type {any} */ scope) => {
385
473
  // Skip post-render setup if a newer render has started
386
474
  if (gen !== view.renderGeneration) return;
387
475
  if (scope) {
388
476
  updateCanvas({ status: "ready", scope, error: null });
389
477
  applyCanvasMediaOverrides(panel.canvas, activeBreakpoints);
390
478
  statusMessage("Runtime render OK", 1500);
479
+
480
+ // Apply diff highlighting if in git-diff mode
481
+ if (gitDiffState && docOverride) {
482
+ // Determine which document is original and which is current
483
+ const isOriginal = docOverride === (gitDiffState.originalDoc || gitDiffState.original);
484
+ const origDoc = isOriginal ? docOverride : gitDiffState.currentDoc || tab?.doc.document;
485
+ const currDoc = isOriginal ? gitDiffState.currentDoc || tab?.doc.document : docOverride;
486
+
487
+ const { byPath: diffMap } = computeDocumentDiff(origDoc, currDoc);
488
+
489
+ // Can't iterate WeakMap, so apply styling by walking the canvas
490
+ const { elToPath } = scope;
491
+ if (elToPath instanceof WeakMap) {
492
+ applyDiffHighlightToCanvas(panel.canvas, diffMap);
493
+ }
494
+ }
391
495
  } else {
392
496
  // Fallback to structural preview
393
497
  updateCanvas({ status: "ready", scope: null, error: null });
394
- renderCanvasNode(
395
- _ctx.getState().document,
396
- [],
397
- panel.canvas,
398
- activeBreakpoints,
399
- featureToggles,
400
- );
498
+ renderCanvasNode(docToRender, [], panel.canvas, activeBreakpoints, featureToggles);
401
499
  }
402
500
  registerPanelDnD(panel);
403
501
  registerPanelEvents(panel);
@@ -406,6 +504,40 @@ function renderCanvasIntoPanel(panel, activeBreakpoints, featureToggles) {
406
504
  });
407
505
  }
408
506
 
507
+ /**
508
+ * Apply diff highlighting to canvas elements based on elToPath mapping. Walks the canvas DOM and
509
+ * applies classes based on diff status.
510
+ *
511
+ * @param {HTMLElement} canvas
512
+ * @param {Map<string, "added" | "removed" | "modified">} diffMap
513
+ */
514
+ function applyDiffHighlightToCanvas(canvas, diffMap) {
515
+ if (!diffMap || diffMap.size === 0) return;
516
+
517
+ // Walk all elements in canvas and check their data attributes or other markers
518
+ const walkCanvas = (/** @type {HTMLElement} */ el, /** @type {string} */ path = "") => {
519
+ const pathKey = path || "/";
520
+
521
+ if (diffMap.has(pathKey)) {
522
+ const status = diffMap.get(pathKey);
523
+ if (status === "added") el.classList.add("element-diff-added");
524
+ else if (status === "removed") el.classList.add("element-diff-removed");
525
+ else if (status === "modified") el.classList.add("element-diff-modified");
526
+ }
527
+
528
+ // Check for child elements (heuristic: children array markers)
529
+ let childIdx = 0;
530
+ for (const child of el.children) {
531
+ const childPath =
532
+ pathKey === "/" ? `children/${childIdx}` : `${pathKey}/children/${childIdx}`;
533
+ walkCanvas(/** @type {HTMLElement} */ (child), childPath);
534
+ childIdx++;
535
+ }
536
+ };
537
+
538
+ walkCanvas(canvas, "");
539
+ }
540
+
409
541
  /**
410
542
  * Apply media query overrides as inline styles on matching canvas elements. Needed because the
411
543
  * runtime renders base styles as inline — @media CSS rules in the injected stylesheet can't win
@@ -416,8 +548,9 @@ function renderCanvasIntoPanel(panel, activeBreakpoints, featureToggles) {
416
548
  */
417
549
  function applyCanvasMediaOverrides(canvasEl, activeBreakpoints) {
418
550
  if (!activeBreakpoints.size) return;
419
- const S = _ctx.getState();
420
- const docMedia = getEffectiveMedia(S.document.$media || {});
551
+ const tab = activeTab.value;
552
+ if (!tab) return;
553
+ const docMedia = getEffectiveMedia(tab.doc.document.$media || {});
421
554
  const validBreakpoints = new Set();
422
555
  for (const name of activeBreakpoints) {
423
556
  if (docMedia[name]) validBreakpoints.add(name);
@@ -8,8 +8,8 @@ import { ref } from "lit-html/directives/ref.js";
8
8
  import { styleMap } from "lit-html/directives/style-map.js";
9
9
  import { ifDefined } from "lit-html/directives/if-defined.js";
10
10
 
11
- import { getState, renderOnly, updateUi, canvasWrap, canvasPanels } from "../store.js";
12
- import { ensureLitState } from "../panels/shared.js";
11
+ import { renderOnly, updateUi, canvasWrap, canvasPanels } from "../store.js";
12
+ import { activeTab } from "../workspace/workspace.js";
13
13
  import { view } from "../view.js";
14
14
 
15
15
  /** @type {any} */
@@ -44,14 +44,14 @@ export function initCanvasUtils(ctx) {
44
44
  export function canvasPanelTemplate(mediaName, label, fullWidth, width = null) {
45
45
  /**
46
46
  * @type {{
47
- * mediaName: any;
48
- * element: Element | null;
49
- * canvas: Element | null;
50
- * overlay: Element | null;
51
- * overlayClk: Element | null;
52
- * viewport: Element | null;
53
- * dropLine: Element | null;
54
- * _width: any;
47
+ * mediaName: string;
48
+ * element: HTMLElement | null;
49
+ * canvas: HTMLElement | null;
50
+ * overlay: HTMLElement | null;
51
+ * overlayClk: HTMLElement | null;
52
+ * viewport: HTMLElement | null;
53
+ * dropLine: HTMLElement | null;
54
+ * _width: number | null;
55
55
  * }}
56
56
  */
57
57
  const panel = {
@@ -69,7 +69,7 @@ export function canvasPanelTemplate(mediaName, label, fullWidth, width = null) {
69
69
  class=${`canvas-panel${fullWidth ? " full-width" : ""}`}
70
70
  data-media=${ifDefined(mediaName !== null ? mediaName : undefined)}
71
71
  ${ref((el) => {
72
- if (el) panel.element = el;
72
+ if (el) panel.element = /** @type {HTMLElement} */ (el);
73
73
  })}
74
74
  >
75
75
  ${label
@@ -88,34 +88,34 @@ export function canvasPanelTemplate(mediaName, label, fullWidth, width = null) {
88
88
  class="canvas-panel-viewport"
89
89
  style=${styleMap({ width: width && !fullWidth ? `${width}px` : "" })}
90
90
  ${ref((el) => {
91
- if (el) panel.viewport = el;
91
+ if (el) panel.viewport = /** @type {HTMLElement} */ (el);
92
92
  })}
93
93
  >
94
94
  <div
95
95
  class="canvas-panel-canvas"
96
96
  style=${styleMap({ width: width ? `${width}px` : "" })}
97
97
  ${ref((el) => {
98
- if (el) panel.canvas = el;
98
+ if (el) panel.canvas = /** @type {HTMLElement} */ (el);
99
99
  })}
100
100
  ></div>
101
101
  <div
102
102
  class="canvas-panel-overlay"
103
103
  ${ref((el) => {
104
- if (el) panel.overlay = el;
104
+ if (el) panel.overlay = /** @type {HTMLElement} */ (el);
105
105
  })}
106
106
  >
107
107
  <div
108
108
  class="canvas-drop-indicator"
109
109
  style="display:none"
110
110
  ${ref((el) => {
111
- if (el) panel.dropLine = el;
111
+ if (el) panel.dropLine = /** @type {HTMLElement} */ (el);
112
112
  })}
113
113
  ></div>
114
114
  </div>
115
115
  <div
116
116
  class="canvas-panel-click"
117
117
  ${ref((el) => {
118
- if (el) panel.overlayClk = el;
118
+ if (el) panel.overlayClk = /** @type {HTMLElement} */ (el);
119
119
  })}
120
120
  ></div>
121
121
  </div>
@@ -207,7 +207,6 @@ export function fitToScreen() {
207
207
 
208
208
  /** Reset the zoom indicator (clear its content). Called when switching to non-panzoom modes. */
209
209
  export function resetZoomIndicator() {
210
- ensureLitState(zoomIndicatorHost);
211
210
  litRender(nothing, zoomIndicatorHost);
212
211
  }
213
212
 
@@ -220,7 +219,6 @@ export function renderZoomIndicator() {
220
219
  if (!zoomIndicatorHost.isConnected) {
221
220
  document.body.appendChild(zoomIndicatorHost);
222
221
  }
223
- ensureLitState(zoomIndicatorHost);
224
222
  litRender(
225
223
  html`
226
224
  <div class="zoom-indicator">
@@ -263,14 +261,14 @@ export function positionZoomIndicator() {
263
261
 
264
262
  /** Toggle "active" class on canvas panel headers based on activeMedia. */
265
263
  export function updateActivePanelHeaders() {
266
- const S = getState();
264
+ const activeMedia = activeTab.value?.session.ui.activeMedia ?? null;
267
265
  for (const p of canvasPanels) {
268
- const header = p.element.querySelector(".canvas-panel-header");
266
+ const header = p.element?.querySelector(".canvas-panel-header");
269
267
  if (header) {
270
268
  const isActive =
271
- (S.ui.activeMedia === null && p.mediaName === "base") ||
272
- (S.ui.activeMedia === null && p.mediaName === null) ||
273
- S.ui.activeMedia === p.mediaName;
269
+ (activeMedia === null && p.mediaName === "base") ||
270
+ (activeMedia === null && p.mediaName === null) ||
271
+ activeMedia === p.mediaName;
274
272
  header.classList.toggle("active", isActive);
275
273
  }
276
274
  }
@@ -5,19 +5,21 @@
5
5
 
6
6
  import {
7
7
  getState,
8
- update,
9
8
  updateUi,
10
9
  renderOnly,
11
10
  getNodeAtPath,
12
- selectNode,
13
- removeNode,
14
- insertNode,
15
- updateProperty,
16
11
  parentElementPath,
17
12
  childIndex,
18
13
  canvasPanels,
19
14
  elToPath,
20
15
  } from "../store.js";
16
+ import { activeTab } from "../workspace/workspace.js";
17
+ import {
18
+ transactDoc,
19
+ mutateRemoveNode,
20
+ mutateInsertNode,
21
+ mutateUpdateProperty,
22
+ } from "../tabs/transact.js";
21
23
  import { view } from "../view.js";
22
24
  import { isSlashMenuOpen, showSlashMenu, dismissSlashMenu } from "./slash-menu.js";
23
25
  import { renderBlockActionBar } from "../panels/block-action-bar.js";
@@ -59,7 +61,9 @@ export function enterComponentInlineEdit(el, path) {
59
61
  if (voids.has(node.tagName)) return;
60
62
 
61
63
  for (const p of canvasPanels) {
62
- const boxes = p.overlay.querySelectorAll(".overlay-box");
64
+ const boxes = /** @type {NodeListOf<HTMLElement>} */ (
65
+ p.overlay.querySelectorAll(".overlay-box")
66
+ );
63
67
  for (const box of boxes) {
64
68
  box.style.border = "none";
65
69
  }
@@ -104,10 +108,11 @@ export function enterComponentInlineEdit(el, path) {
104
108
  if (view.blockActionBarEl && view.blockActionBarEl.contains(evt.target)) return;
105
109
  document.removeEventListener("mousedown", outsideHandler, true);
106
110
 
111
+ /** @type {(string | number)[] | null} */
107
112
  let hitPath = null,
108
113
  hitMedia = null;
109
114
  for (const p of canvasPanels) {
110
- const els = p.canvas.querySelectorAll("*");
115
+ const els = /** @type {NodeListOf<HTMLElement>} */ (p.canvas.querySelectorAll("*"));
111
116
  for (const el of els) el.style.pointerEvents = "auto";
112
117
  p.overlayClk.style.display = "none";
113
118
  const found = document.elementsFromPoint(evt.clientX, evt.clientY);
@@ -132,37 +137,44 @@ export function enterComponentInlineEdit(el, path) {
132
137
 
133
138
  const isEmpty = !newText;
134
139
  const pPath = parentElementPath(editPath);
135
- const S = getState();
136
140
 
137
141
  if (hitPath) {
142
+ let hp = hitPath;
138
143
  const media = hitMedia === "base" ? null : (hitMedia ?? null);
139
- updateUi("pendingInlineEdit", { path: hitPath, mediaName: hitMedia });
140
- const withMedia = { ...S, ui: { ...S.ui, activeMedia: media } };
144
+ updateUi("pendingInlineEdit", { path: hp, mediaName: hitMedia });
145
+ activeTab.value.session.ui.activeMedia = media;
141
146
  if (isEmpty && pPath) {
142
- let s = removeNode(withMedia, editPath);
143
- const removedIdx = /** @type {number} */ (childIndex(editPath));
144
- const hitIdx = /** @type {number} */ (childIndex(hitPath));
145
- const hitParent = parentElementPath(hitPath);
146
- if (hitParent && pPath && hitParent.join("/") === pPath.join("/") && hitIdx > removedIdx) {
147
- hitPath = [...pPath, "children", hitIdx - 1];
148
- updateUi("pendingInlineEdit", { path: hitPath, mediaName: hitMedia });
149
- }
150
- update(selectNode(s, hitPath));
147
+ transactDoc(activeTab.value, (t) => {
148
+ mutateRemoveNode(t, editPath);
149
+ const removedIdx = /** @type {number} */ (childIndex(editPath));
150
+ const hitIdx = /** @type {number} */ (childIndex(hp));
151
+ const hitParent = parentElementPath(hp);
152
+ if (
153
+ hitParent &&
154
+ pPath &&
155
+ hitParent.join("/") === pPath.join("/") &&
156
+ hitIdx > removedIdx
157
+ ) {
158
+ hp = [...pPath, "children", hitIdx - 1];
159
+ updateUi("pendingInlineEdit", { path: hp, mediaName: hitMedia });
160
+ }
161
+ t.session.selection = hp;
162
+ });
151
163
  } else if (newText !== originalText) {
152
- update(
153
- selectNode(
154
- updateProperty(withMedia, editPath, "textContent", newText || undefined),
155
- hitPath,
156
- ),
157
- );
164
+ transactDoc(activeTab.value, (t) => {
165
+ mutateUpdateProperty(t, editPath, "textContent", newText || undefined);
166
+ t.session.selection = hp;
167
+ });
158
168
  } else {
159
- update(selectNode(withMedia, hitPath));
169
+ activeTab.value.session.selection = hp;
160
170
  }
161
171
  } else {
162
172
  if (isEmpty && pPath) {
163
- update(removeNode(S, editPath));
173
+ transactDoc(activeTab.value, (t) => mutateRemoveNode(t, editPath));
164
174
  } else if (newText !== originalText) {
165
- update(updateProperty(S, editPath, "textContent", newText || undefined));
175
+ transactDoc(activeTab.value, (t) =>
176
+ mutateUpdateProperty(t, editPath, "textContent", newText || undefined),
177
+ );
166
178
  } else {
167
179
  renderOnly("canvas");
168
180
  renderOnly("overlays");
@@ -219,13 +231,13 @@ function splitParagraph() {
219
231
 
220
232
  cleanupComponentInlineEdit(el);
221
233
 
222
- const S = getState();
223
- let s = updateProperty(S, path, "textContent", textBefore || undefined);
224
- s = insertNode(s, pPath, idx + 1, newDef);
225
- s = selectNode(s, newPath);
234
+ transactDoc(activeTab.value, (t) => {
235
+ mutateUpdateProperty(t, path, "textContent", textBefore || undefined);
236
+ mutateInsertNode(t, pPath, idx + 1, newDef);
237
+ t.session.selection = newPath;
238
+ });
226
239
 
227
240
  updateUi("pendingInlineEdit", { path: newPath, mediaName });
228
- update(s);
229
241
  }
230
242
 
231
243
  function _commitComponentInlineEdit() {
@@ -235,12 +247,13 @@ function _commitComponentInlineEdit() {
235
247
 
236
248
  cleanupComponentInlineEdit(el);
237
249
 
238
- const S = getState();
239
250
  const pPath = parentElementPath(path);
240
251
  if (!newText && pPath) {
241
- update(removeNode(S, path));
252
+ transactDoc(activeTab.value, (t) => mutateRemoveNode(t, path));
242
253
  } else if (newText !== originalText) {
243
- update(updateProperty(S, path, "textContent", newText || undefined));
254
+ transactDoc(activeTab.value, (t) =>
255
+ mutateUpdateProperty(t, path, "textContent", newText || undefined),
256
+ );
244
257
  } else {
245
258
  renderOnly("canvas");
246
259
  renderOnly("overlays");
@@ -303,15 +316,15 @@ function handleComponentSlashSelect(cmd) {
303
316
 
304
317
  cleanupComponentInlineEdit(el);
305
318
 
306
- const S = getState();
307
319
  const newDef = defaultDef(cmd.tag);
308
320
  const newPath = [...pPath, "children", idx];
309
321
 
310
- let s = removeNode(S, path);
311
- s = insertNode(s, pPath, idx, newDef);
312
- s = selectNode(s, newPath);
322
+ transactDoc(activeTab.value, (t) => {
323
+ mutateRemoveNode(t, path);
324
+ mutateInsertNode(t, pPath, idx, newDef);
325
+ t.session.selection = newPath;
326
+ });
313
327
 
314
328
  const hasText = newDef.textContent != null;
315
329
  if (hasText) updateUi("pendingInlineEdit", { path: newPath, mediaName });
316
- update(s);
317
330
  }