@jxsuite/studio 0.11.0 → 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 (47) hide show
  1. package/dist/studio.js +4248 -3412
  2. package/dist/studio.js.map +49 -43
  3. package/package.json +5 -3
  4. package/src/canvas/canvas-diff.js +184 -0
  5. package/src/canvas/canvas-helpers.js +10 -14
  6. package/src/canvas/canvas-live-render.js +3 -2
  7. package/src/canvas/canvas-render.js +152 -20
  8. package/src/canvas/canvas-utils.js +21 -23
  9. package/src/editor/component-inline-edit.js +54 -41
  10. package/src/editor/content-inline-edit.js +46 -47
  11. package/src/editor/context-menu.js +63 -39
  12. package/src/editor/convert-to-component.js +11 -14
  13. package/src/editor/insertion-helper.js +8 -10
  14. package/src/editor/shortcuts.js +69 -39
  15. package/src/files/components.js +15 -4
  16. package/src/files/files.js +72 -24
  17. package/src/panels/activity-bar.js +29 -7
  18. package/src/panels/block-action-bar.js +104 -80
  19. package/src/panels/dnd.js +32 -28
  20. package/src/panels/editors.js +7 -14
  21. package/src/panels/elements-panel.js +9 -3
  22. package/src/panels/events-panel.js +44 -39
  23. package/src/panels/git-panel.js +45 -3
  24. package/src/panels/layers-panel.js +16 -11
  25. package/src/panels/left-panel.js +108 -43
  26. package/src/panels/overlays.js +91 -41
  27. package/src/panels/panel-events.js +9 -12
  28. package/src/panels/properties-panel.js +179 -104
  29. package/src/panels/right-panel.js +85 -37
  30. package/src/panels/shared.js +0 -22
  31. package/src/panels/signals-panel.js +125 -54
  32. package/src/panels/statusbar.js +23 -8
  33. package/src/panels/style-inputs.js +4 -2
  34. package/src/panels/style-panel.js +128 -105
  35. package/src/panels/stylebook-panel.js +11 -15
  36. package/src/panels/tab-strip.js +124 -0
  37. package/src/panels/toolbar.js +39 -9
  38. package/src/reactivity.js +28 -0
  39. package/src/settings/content-types-editor.js +56 -7
  40. package/src/settings/defs-editor.js +56 -7
  41. package/src/settings/schema-field-ui.js +60 -25
  42. package/src/state.js +0 -456
  43. package/src/store.js +97 -124
  44. package/src/studio.js +112 -121
  45. package/src/tabs/tab.js +153 -0
  46. package/src/tabs/transact.js +406 -0
  47. package/src/workspace/workspace.js +89 -0
@@ -2,24 +2,19 @@
2
2
 
3
3
  import { html, nothing } from "lit-html";
4
4
  import { live } from "lit-html/directives/live.js";
5
+ import { getNodeAtPath, debouncedStyleCommit, renderOnly, projectState } from "../store.js";
5
6
  import {
6
- getState,
7
- selectNode,
8
- getNodeAtPath,
9
- update,
10
- updateProperty,
11
- updateAttribute,
12
- updateProp,
13
- updateMedia,
14
- updateFrontmatter,
15
- addSwitchCase,
16
- removeSwitchCase,
17
- renameSwitchCase,
18
- debouncedStyleCommit,
19
- renderOnly,
20
- updateUi,
21
- projectState,
22
- } from "../store.js";
7
+ transactDoc,
8
+ mutateUpdateProperty,
9
+ mutateUpdateAttribute,
10
+ mutateUpdateProp,
11
+ mutateUpdateFrontmatter,
12
+ mutateUpdateMedia,
13
+ mutateAddSwitchCase,
14
+ mutateRemoveSwitchCase,
15
+ mutateRenameSwitchCase,
16
+ } from "../tabs/transact.js";
17
+ import { activeTab } from "../workspace/workspace.js";
23
18
  import { view } from "../view.js";
24
19
  import { componentRegistry } from "../files/components.js";
25
20
  import { widgetForType } from "./style-inputs.js";
@@ -36,6 +31,7 @@ import { isCustomElementDoc, collectCssParts } from "./signals-panel.js";
36
31
  import { mediaDisplayName } from "./shared.js";
37
32
  import { getCssInitialMap } from "./style-utils.js";
38
33
  import { renderMediaPicker } from "../ui/media-picker.js";
34
+ import { renderColorSelector } from "../ui/color-selector.js";
39
35
  import { getEffectiveLayoutPath, invalidateLayoutCache } from "../site-context.js";
40
36
  import { getPlatform } from "../platform.js";
41
37
  import htmlMeta from "../../data/html-meta.json";
@@ -70,8 +66,8 @@ function bindableFieldRow(
70
66
  /** @type {any} */ filterFn = null,
71
67
  /** @type {any} */ extraSignals = null,
72
68
  ) {
73
- const S = getState();
74
- const defs = S.document.state || {};
69
+ const tab = activeTab.value;
70
+ const defs = tab.doc.document.state || {};
75
71
  const isBound = typeof rawValue === "object" && rawValue !== null && rawValue.$ref;
76
72
 
77
73
  const signalDefs = Object.entries(defs).filter(([, d]) =>
@@ -211,9 +207,9 @@ function kvRow(
211
207
 
212
208
  /** Frontmatter-only panel shown in content mode when no element is selected */
213
209
  function renderFrontmatterOnlyPanel() {
214
- const S = getState();
215
- const fm = S.content?.frontmatter || {};
216
- const col = findContentTypeSchema(S.documentPath, projectState?.projectConfig);
210
+ const tab = activeTab.value;
211
+ const fm = tab.doc.content?.frontmatter || {};
212
+ const col = findContentTypeSchema(tab.documentPath, projectState?.projectConfig);
217
213
  const schemaProps = col?.schema?.properties;
218
214
  const requiredFields = new Set(col?.schema?.required || []);
219
215
 
@@ -248,7 +244,7 @@ function renderFrontmatterOnlyPanel() {
248
244
  return html`<div class="empty-state">No frontmatter. Select an element to inspect.</div>`;
249
245
  }
250
246
 
251
- const pageT = renderPageSection(S.document || {});
247
+ const pageT = renderPageSection(tab.doc.document || {});
252
248
 
253
249
  return html`
254
250
  <div class="style-sidebar">
@@ -271,12 +267,12 @@ function renderFmFieldRow(
271
267
  /** @type {any} */ value,
272
268
  /** @type {Set<string>} */ requiredFields,
273
269
  ) {
274
- const S = getState();
275
270
  const isRequired = requiredFields.has(field);
276
271
  const label = field.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase());
277
272
  const displayLabel = label + (isRequired ? " *" : "");
278
273
  const hasVal = value !== undefined && value !== "" && value !== false;
279
- const onClear = () => update(updateFrontmatter(S, field, undefined));
274
+ const onClear = () =>
275
+ transactDoc(activeTab.value, (t) => mutateUpdateFrontmatter(t, field, undefined));
280
276
 
281
277
  if (entry.type === "boolean") {
282
278
  return renderFieldRow({
@@ -289,7 +285,9 @@ function renderFmFieldRow(
289
285
  size="s"
290
286
  .checked=${live(!!value)}
291
287
  @change=${(/** @type {any} */ e) =>
292
- update(updateFrontmatter(getState(), field, e.target.checked || undefined))}
288
+ transactDoc(activeTab.value, (t) =>
289
+ mutateUpdateFrontmatter(t, field, e.target.checked || undefined),
290
+ )}
293
291
  ></sp-checkbox>
294
292
  `,
295
293
  });
@@ -314,7 +312,7 @@ function renderFmFieldRow(
314
312
  .map((/** @type {string} */ s) => s.trim())
315
313
  .filter(Boolean)
316
314
  : undefined;
317
- update(updateFrontmatter(getState(), field, arr));
315
+ transactDoc(activeTab.value, (t) => mutateUpdateFrontmatter(t, field, arr));
318
316
  })}
319
317
  ></sp-textfield>
320
318
  `,
@@ -332,7 +330,9 @@ function renderFmFieldRow(
332
330
  size="s"
333
331
  .value=${live(value || "")}
334
332
  @change=${(/** @type {any} */ e) =>
335
- update(updateFrontmatter(getState(), field, e.target.value || undefined))}
333
+ transactDoc(activeTab.value, (t) =>
334
+ mutateUpdateFrontmatter(t, field, e.target.value || undefined),
335
+ )}
336
336
  >
337
337
  ${entry.enum.map(
338
338
  (/** @type {string} */ opt) => html`<sp-menu-item value=${opt}>${opt}</sp-menu-item>`,
@@ -349,7 +349,7 @@ function renderFmFieldRow(
349
349
  hasValue: hasVal,
350
350
  onClear,
351
351
  widget: renderMediaPicker(field, value, (v) =>
352
- update(updateFrontmatter(getState(), field, v || undefined)),
352
+ transactDoc(activeTab.value, (t) => mutateUpdateFrontmatter(t, field, v || undefined)),
353
353
  ),
354
354
  });
355
355
  }
@@ -374,7 +374,9 @@ function renderFmFieldRow(
374
374
  title="Remove"
375
375
  @click=${() => {
376
376
  const next = images.filter((_, idx) => idx !== i);
377
- update(updateFrontmatter(getState(), field, next.length ? next : undefined));
377
+ transactDoc(activeTab.value, (t) =>
378
+ mutateUpdateFrontmatter(t, field, next.length ? next : undefined),
379
+ );
378
380
  }}
379
381
  >
380
382
  <sp-icon-close slot="icon"></sp-icon-close>
@@ -386,7 +388,7 @@ function renderFmFieldRow(
386
388
  ${renderMediaPicker(`${field}:add`, "", (v) => {
387
389
  if (!v) return;
388
390
  const next = [...images, v];
389
- update(updateFrontmatter(getState(), field, next));
391
+ transactDoc(activeTab.value, (t) => mutateUpdateFrontmatter(t, field, next));
390
392
  })}
391
393
  </div>
392
394
  `,
@@ -397,7 +399,7 @@ function renderFmFieldRow(
397
399
  const targetName = entry.$ref.replace("#/contentTypes/", "");
398
400
  const targetDef = projectState?.projectConfig?.contentTypes?.[targetName];
399
401
  const picker = renderReferencePicker(field, value, targetName, targetDef, (v) =>
400
- update(updateFrontmatter(getState(), field, v || undefined)),
402
+ transactDoc(activeTab.value, (t) => mutateUpdateFrontmatter(t, field, v || undefined)),
401
403
  );
402
404
  return renderFieldRow({
403
405
  prop: field,
@@ -421,7 +423,9 @@ function renderFmFieldRow(
421
423
  .value=${live(value !== undefined ? Number(value) : undefined)}
422
424
  @change=${debouncedStyleCommit(`fm:${field}`, 400, (/** @type {any} */ e) => {
423
425
  const v = e.target.value;
424
- update(updateFrontmatter(getState(), field, isNaN(v) ? undefined : Number(v)));
426
+ transactDoc(activeTab.value, (t) =>
427
+ mutateUpdateFrontmatter(t, field, isNaN(v) ? undefined : Number(v)),
428
+ );
425
429
  })}
426
430
  ></sp-number-field>
427
431
  `,
@@ -439,7 +443,9 @@ function renderFmFieldRow(
439
443
  placeholder=${entry.format === "date" ? "YYYY-MM-DD" : ""}
440
444
  .value=${live(value || "")}
441
445
  @input=${debouncedStyleCommit(`fm:${field}`, 400, (/** @type {any} */ e) => {
442
- update(updateFrontmatter(getState(), field, e.target.value || undefined));
446
+ transactDoc(activeTab.value, (t) =>
447
+ mutateUpdateFrontmatter(t, field, e.target.value || undefined),
448
+ );
443
449
  })}
444
450
  ></sp-textfield>
445
451
  `,
@@ -526,33 +532,42 @@ function renderRepeaterFieldsTemplate(
526
532
  /** @type {any} */ path,
527
533
  /** @type {any} */ _mapSignals,
528
534
  ) {
529
- const S = getState();
530
535
  return html`
531
536
  ${bindableFieldRow("Items", "text", node.items, (/** @type {any} */ v) =>
532
- update(updateProperty(getState(), path, "items", v)),
537
+ transactDoc(activeTab.value, (t) => mutateUpdateProperty(t, path, "items", v)),
533
538
  )}
534
539
  ${node.filter
535
540
  ? bindableFieldRow("Filter", "text", node.filter, (/** @type {any} */ v) =>
536
- update(updateProperty(getState(), path, "filter", v || undefined)),
541
+ transactDoc(activeTab.value, (t) =>
542
+ mutateUpdateProperty(t, path, "filter", v || undefined),
543
+ ),
537
544
  )
538
545
  : nothing}
539
546
  ${node.sort
540
547
  ? bindableFieldRow("Sort", "text", node.sort, (/** @type {any} */ v) =>
541
- update(updateProperty(getState(), path, "sort", v || undefined)),
548
+ transactDoc(activeTab.value, (t) =>
549
+ mutateUpdateProperty(t, path, "sort", v || undefined),
550
+ ),
542
551
  )
543
552
  : nothing}
544
553
  <div style="display:flex;gap:8px;margin-top:4px">
545
554
  ${!node.filter
546
555
  ? html`<span
547
556
  class="kv-add"
548
- @click=${() => update(updateProperty(getState(), path, "filter", { $ref: "#/state/" }))}
557
+ @click=${() =>
558
+ transactDoc(activeTab.value, (t) =>
559
+ mutateUpdateProperty(t, path, "filter", { $ref: "#/state/" }),
560
+ )}
549
561
  >+ Add filter</span
550
562
  >`
551
563
  : nothing}
552
564
  ${!node.sort
553
565
  ? html`<span
554
566
  class="kv-add"
555
- @click=${() => update(updateProperty(getState(), path, "sort", { $ref: "#/state/" }))}
567
+ @click=${() =>
568
+ transactDoc(activeTab.value, (t) =>
569
+ mutateUpdateProperty(t, path, "sort", { $ref: "#/state/" }),
570
+ )}
556
571
  >+ Add sort</span
557
572
  >`
558
573
  : nothing}
@@ -562,7 +577,9 @@ function renderRepeaterFieldsTemplate(
562
577
  <sp-action-button
563
578
  size="s"
564
579
  style="margin-top:8px;width:100%"
565
- @click=${() => update(selectNode(S, [...path, "map"]))}
580
+ @click=${() => {
581
+ activeTab.value.session.selection = [...path, "map"];
582
+ }}
566
583
  >Edit template →</sp-action-button
567
584
  >
568
585
  `
@@ -582,7 +599,8 @@ function renderSwitchFieldsTemplate(
582
599
  "Expression",
583
600
  "text",
584
601
  node.$switch,
585
- (/** @type {any} */ v) => update(updateProperty(getState(), path, "$switch", v)),
602
+ (/** @type {any} */ v) =>
603
+ transactDoc(activeTab.value, (t) => mutateUpdateProperty(t, path, "$switch", v)),
586
604
  null,
587
605
  mapSignals,
588
606
  )}
@@ -604,7 +622,9 @@ function renderSwitchFieldsTemplate(
604
622
  clearTimeout(debounce);
605
623
  debounce = setTimeout(() => {
606
624
  if (e.target.value && e.target.value !== caseName)
607
- update(renameSwitchCase(getState(), path, caseName, e.target.value));
625
+ transactDoc(activeTab.value, (t) =>
626
+ mutateRenameSwitchCase(t, path, caseName, e.target.value),
627
+ );
608
628
  }, 500);
609
629
  }}
610
630
  />
@@ -614,7 +634,7 @@ function renderSwitchFieldsTemplate(
614
634
  style="cursor:pointer"
615
635
  @click=${(/** @type {any} */ e) => {
616
636
  e.stopPropagation();
617
- update(selectNode(getState(), [...path, "cases", caseName]));
637
+ activeTab.value.session.selection = [...path, "cases", caseName];
618
638
  }}
619
639
  >→</span
620
640
  >
@@ -622,7 +642,7 @@ function renderSwitchFieldsTemplate(
622
642
  style="cursor:pointer;color:var(--danger);font-size:11px"
623
643
  @click=${(/** @type {any} */ e) => {
624
644
  e.stopPropagation();
625
- update(removeSwitchCase(getState(), path, caseName));
645
+ transactDoc(activeTab.value, (t) => mutateRemoveSwitchCase(t, path, caseName));
626
646
  }}
627
647
  >✕</span
628
648
  >
@@ -632,7 +652,9 @@ function renderSwitchFieldsTemplate(
632
652
  <span
633
653
  class="kv-add"
634
654
  @click=${() => {
635
- update(addSwitchCase(getState(), path, `case${caseNames.length + 1}`));
655
+ transactDoc(activeTab.value, (t) =>
656
+ mutateAddSwitchCase(t, path, `case${caseNames.length + 1}`),
657
+ );
636
658
  }}
637
659
  >+ Add case</span
638
660
  >
@@ -646,18 +668,20 @@ function renderComponentPropsFieldsTemplate(
646
668
  /** @type {any} */ mapSignals,
647
669
  /** @type {(path: string) => void} */ navigateToComponent,
648
670
  ) {
649
- const S = getState();
671
+ const tab = activeTab.value;
650
672
  const comp = componentRegistry.find((c) => c.tagName === node.tagName);
651
- if (!comp) return html`<div class="empty-state">Component not found</div>`;
673
+ if (!comp || !comp.props) return html`<div class="empty-state">Component not found</div>`;
652
674
  const isNpm = comp.source === "npm";
653
675
  const currentVals = isNpm ? node.attributes || {} : node.$props || {};
654
676
  const updateFn = isNpm
655
677
  ? (/** @type {string} */ name, /** @type {any} */ v) =>
656
- update(updateAttribute(getState(), path, name, v === "" ? undefined : v))
678
+ transactDoc(activeTab.value, (t) =>
679
+ mutateUpdateAttribute(t, path, name, v === "" ? undefined : v),
680
+ )
657
681
  : (/** @type {string} */ name, /** @type {any} */ v) =>
658
- update(updateProp(getState(), path, name, v));
682
+ transactDoc(activeTab.value, (t) => mutateUpdateProp(t, path, name, v));
659
683
 
660
- const defs = S.document.state || {};
684
+ const defs = tab.doc.document.state || {};
661
685
  const signalDefs = Object.entries(defs).filter(
662
686
  ([, d]) => !d.$handler && d.$prototype !== "Function",
663
687
  );
@@ -727,7 +751,21 @@ function renderComponentPropsFieldsTemplate(
727
751
  const staticVal = isBound ? "" : (rawValue ?? "");
728
752
  /** @type {any} */
729
753
  let widgetTpl;
730
- if (parsed.kind === "boolean") {
754
+ if (prop.format === "image") {
755
+ widgetTpl = renderMediaPicker(prop.name, staticVal, onChange);
756
+ } else if (prop.format === "color") {
757
+ widgetTpl = renderColorSelector(prop.name, staticVal, onChange);
758
+ } else if (prop.format === "date") {
759
+ widgetTpl = html`<sp-textfield
760
+ size="s"
761
+ placeholder="YYYY-MM-DD"
762
+ value=${staticVal}
763
+ @input=${(/** @type {any} */ e) => {
764
+ clearTimeout(debounce);
765
+ debounce = setTimeout(() => onChange(e.target.value), 400);
766
+ }}
767
+ ></sp-textfield>`;
768
+ } else if (parsed.kind === "boolean") {
731
769
  widgetTpl = html`<sp-checkbox
732
770
  size="s"
733
771
  .checked=${live(!!staticVal)}
@@ -806,19 +844,22 @@ function renderCustomAttrsFieldsTemplate(
806
844
  attr,
807
845
  String(val),
808
846
  (/** @type {any} */ newAttr, /** @type {any} */ newVal) => {
809
- const S = getState();
810
847
  if (newAttr !== attr) {
811
- let s = updateAttribute(S, path, attr, undefined);
812
- s = updateAttribute(s, path, newAttr, newVal);
813
- update(s);
848
+ transactDoc(activeTab.value, (t) => {
849
+ mutateUpdateAttribute(t, path, attr, undefined);
850
+ mutateUpdateAttribute(t, path, newAttr, newVal);
851
+ });
814
852
  } else {
815
- update(updateAttribute(S, path, attr, newVal));
853
+ transactDoc(activeTab.value, (t) => mutateUpdateAttribute(t, path, attr, newVal));
816
854
  }
817
855
  },
818
- () => update(updateAttribute(getState(), path, attr, undefined)),
856
+ () => transactDoc(activeTab.value, (t) => mutateUpdateAttribute(t, path, attr, undefined)),
819
857
  ),
820
858
  )}
821
- <span class="kv-add" @click=${() => update(updateAttribute(getState(), path, "data-", ""))}
859
+ <span
860
+ class="kv-add"
861
+ @click=${() =>
862
+ transactDoc(activeTab.value, (t) => mutateUpdateAttribute(t, path, "data-", ""))}
822
863
  >+ Add attribute</span
823
864
  >
824
865
  `;
@@ -845,12 +886,15 @@ function renderMediaFieldsTemplate(/** @type {any} */ node) {
845
886
  clearTimeout(baseDebounce);
846
887
  baseDebounce = setTimeout(() => {
847
888
  const val = e.target.value.trim();
848
- update(updateMedia(getState(), "--", val || undefined));
889
+ transactDoc(activeTab.value, (t) => mutateUpdateMedia(t, "--", val || undefined));
849
890
  }, 400);
850
891
  }}
851
892
  />
852
893
  ${media["--"]
853
- ? html`<span class="kv-del" @click=${() => update(updateMedia(getState(), "--", undefined))}
894
+ ? html`<span
895
+ class="kv-del"
896
+ @click=${() =>
897
+ transactDoc(activeTab.value, (t) => mutateUpdateMedia(t, "--", undefined))}
854
898
  >✕</span
855
899
  >`
856
900
  : nothing}
@@ -901,7 +945,7 @@ function renderMediaFieldsTemplate(/** @type {any} */ node) {
901
945
  if (key && queryVal) {
902
946
  view.showAddBreakpointForm = false;
903
947
  view.addBreakpointPreview = "";
904
- update(updateMedia(getState(), key, queryVal));
948
+ transactDoc(activeTab.value, (t) => mutateUpdateMedia(t, key, queryVal));
905
949
  }
906
950
  }}
907
951
  >
@@ -949,10 +993,10 @@ function mediaBreakpointRowTemplate(/** @type {any} */ name, /** @type {any} */
949
993
  const queryEl = e.target
950
994
  .closest("div[style]")
951
995
  ?.parentElement?.querySelector(".bp-query-input");
952
- const S = getState();
953
- let s = updateMedia(S, name, undefined);
954
- s = updateMedia(s, newKey, queryEl?.value || query);
955
- update(s);
996
+ transactDoc(activeTab.value, (t) => {
997
+ mutateUpdateMedia(t, name, undefined);
998
+ mutateUpdateMedia(t, newKey, queryEl?.value || query);
999
+ });
956
1000
  }
957
1001
  }, 600);
958
1002
  }}
@@ -962,7 +1006,9 @@ function mediaBreakpointRowTemplate(/** @type {any} */ name, /** @type {any} */
962
1006
  style="font-size:10px;color:var(--fg-dim);font-family:'SF Mono','Fira Code',monospace;white-space:nowrap"
963
1007
  >${name}</span
964
1008
  >
965
- <span class="kv-del" @click=${() => update(updateMedia(getState(), name, undefined))}
1009
+ <span
1010
+ class="kv-del"
1011
+ @click=${() => transactDoc(activeTab.value, (t) => mutateUpdateMedia(t, name, undefined))}
966
1012
  >✕</span
967
1013
  >
968
1014
  </div>
@@ -974,7 +1020,7 @@ function mediaBreakpointRowTemplate(/** @type {any} */ name, /** @type {any} */
974
1020
  @input=${(/** @type {any} */ e) => {
975
1021
  clearTimeout(debounceTimer);
976
1022
  debounceTimer = setTimeout(
977
- () => update(updateMedia(getState(), name, e.target.value)),
1023
+ () => transactDoc(activeTab.value, (t) => mutateUpdateMedia(t, name, e.target.value)),
978
1024
  400,
979
1025
  );
980
1026
  }}
@@ -1015,8 +1061,8 @@ function isPageDocument(/** @type {any} */ documentPath) {
1015
1061
  }
1016
1062
 
1017
1063
  function renderPageSection(/** @type {any} */ node) {
1018
- const S = getState();
1019
- if (!isPageDocument(S.documentPath)) return nothing;
1064
+ const tab = activeTab.value;
1065
+ if (!isPageDocument(tab.documentPath)) return nothing;
1020
1066
 
1021
1067
  if (layoutEntries === null) {
1022
1068
  loadLayoutEntries();
@@ -1040,7 +1086,9 @@ function renderPageSection(/** @type {any} */ node) {
1040
1086
  title="Reset to default"
1041
1087
  @click=${(/** @type {any} */ e) => {
1042
1088
  e.stopPropagation();
1043
- update(updateProperty(getState(), [], "$layout", undefined));
1089
+ transactDoc(activeTab.value, (t) =>
1090
+ mutateUpdateProperty(t, [], "$layout", undefined),
1091
+ );
1044
1092
  }}
1045
1093
  ></span>`
1046
1094
  : nothing}
@@ -1052,11 +1100,13 @@ function renderPageSection(/** @type {any} */ node) {
1052
1100
  @change=${(/** @type {any} */ e) => {
1053
1101
  const val = e.target.value;
1054
1102
  if (val === "__default__") {
1055
- update(updateProperty(getState(), [], "$layout", undefined));
1103
+ transactDoc(activeTab.value, (t) =>
1104
+ mutateUpdateProperty(t, [], "$layout", undefined),
1105
+ );
1056
1106
  } else if (val === "__none__") {
1057
- update(updateProperty(getState(), [], "$layout", false));
1107
+ transactDoc(activeTab.value, (t) => mutateUpdateProperty(t, [], "$layout", false));
1058
1108
  } else {
1059
- update(updateProperty(getState(), [], "$layout", val));
1109
+ transactDoc(activeTab.value, (t) => mutateUpdateProperty(t, [], "$layout", val));
1060
1110
  }
1061
1111
  invalidateLayoutCache();
1062
1112
  }}
@@ -1135,23 +1185,24 @@ function renderLayoutSelectionPanel(/** @type {any} */ ctx) {
1135
1185
  * @param {{ navigateToComponent: (path: string) => void }} ctx
1136
1186
  */
1137
1187
  export function renderPropertiesPanelTemplate(ctx) {
1138
- const S = getState();
1188
+ const tab = activeTab.value;
1189
+ if (!tab) return html`<div class="empty-state">No document loaded</div>`;
1139
1190
 
1140
1191
  // Layout element selected — show read-only info with link to open layout
1141
1192
  if (view.layoutSelection) {
1142
1193
  return renderLayoutSelectionPanel(ctx);
1143
1194
  }
1144
1195
 
1145
- if (!S.selection) {
1146
- if (S.mode === "content") {
1196
+ if (!tab.session.selection) {
1197
+ if (tab.doc.mode === "content") {
1147
1198
  return renderFrontmatterOnlyPanel();
1148
1199
  }
1149
1200
  return html`<div class="empty-state">Select an element to inspect</div>`;
1150
1201
  }
1151
- const node = getNodeAtPath(S.document, S.selection);
1202
+ const node = getNodeAtPath(tab.doc.document, tab.session.selection);
1152
1203
  if (!node) return html`<div class="empty-state">Node not found</div>`;
1153
1204
 
1154
- const path = S.selection;
1205
+ const path = tab.session.selection;
1155
1206
  const isMapNode = node.$prototype === "Array";
1156
1207
  const isMapParent =
1157
1208
  node.children && typeof node.children === "object" && node.children.$prototype === "Array";
@@ -1181,13 +1232,16 @@ export function renderPropertiesPanelTemplate(ctx) {
1181
1232
  prop: attr,
1182
1233
  label: attrLabel(entry, attr),
1183
1234
  hasValue: hasVal,
1184
- onClear: () => update(updateAttribute(getState(), path, attr, undefined)),
1235
+ onClear: () =>
1236
+ transactDoc(activeTab.value, (t) => mutateUpdateAttribute(t, path, attr, undefined)),
1185
1237
  widget: html`
1186
1238
  <sp-checkbox
1187
1239
  size="s"
1188
1240
  .checked=${live(!!value)}
1189
1241
  @change=${(/** @type {any} */ e) =>
1190
- update(updateAttribute(getState(), path, attr, e.target.checked || undefined))}
1242
+ transactDoc(activeTab.value, (t) =>
1243
+ mutateUpdateAttribute(t, path, attr, e.target.checked || undefined),
1244
+ )}
1191
1245
  >
1192
1246
  </sp-checkbox>
1193
1247
  `,
@@ -1198,9 +1252,10 @@ export function renderPropertiesPanelTemplate(ctx) {
1198
1252
  prop: attr,
1199
1253
  label: attrLabel(entry, attr),
1200
1254
  hasValue: hasVal,
1201
- onClear: () => update(updateAttribute(getState(), path, attr, undefined)),
1255
+ onClear: () =>
1256
+ transactDoc(activeTab.value, (t) => mutateUpdateAttribute(t, path, attr, undefined)),
1202
1257
  widget: widgetForType(type, entry, attr, value || "", (/** @type {any} */ v) =>
1203
- update(updateAttribute(getState(), path, attr, v || undefined)),
1258
+ transactDoc(activeTab.value, (t) => mutateUpdateAttribute(t, path, attr, v || undefined)),
1204
1259
  ),
1205
1260
  });
1206
1261
  }
@@ -1228,7 +1283,7 @@ export function renderPropertiesPanelTemplate(ctx) {
1228
1283
  const knownAttrNames = new Set(Object.keys(applicableAttrs));
1229
1284
  if (isCustomInstance) {
1230
1285
  const comp = componentRegistry.find((c) => c.tagName === node.tagName);
1231
- if (comp) for (const p of comp.props) knownAttrNames.add(p.name);
1286
+ if (comp?.props) for (const p of comp.props) knownAttrNames.add(p.name);
1232
1287
  }
1233
1288
  const customAttrs = Object.entries(attrs).filter(([k]) => !knownAttrNames.has(k));
1234
1289
 
@@ -1240,13 +1295,17 @@ export function renderPropertiesPanelTemplate(ctx) {
1240
1295
  if (customAttrs.length > 0) autoOpen.add("__custom");
1241
1296
 
1242
1297
  function isSectionOpen(/** @type {any} */ key) {
1243
- if (S.ui.inspectorSections[key] !== undefined) return S.ui.inspectorSections[key];
1298
+ if (tab.session.ui.inspectorSections[key] !== undefined)
1299
+ return tab.session.ui.inspectorSections[key];
1244
1300
  return autoOpen.has(key);
1245
1301
  }
1246
1302
 
1247
1303
  function toggleSection(/** @type {any} */ key) {
1248
1304
  const current = isSectionOpen(key);
1249
- updateUi("inspectorSections", { ...S.ui.inspectorSections, [key]: !current });
1305
+ activeTab.value.session.ui.inspectorSections = {
1306
+ ...activeTab.value.session.ui.inspectorSections,
1307
+ [key]: !current,
1308
+ };
1250
1309
  }
1251
1310
 
1252
1311
  // ── Build section templates ─────────────────────────────────────────
@@ -1268,7 +1327,9 @@ export function renderPropertiesPanelTemplate(ctx) {
1268
1327
  autocomplete="off"
1269
1328
  list="tag-names"
1270
1329
  @input=${debouncedStyleCommit("prop:tagName", 400, (/** @type {any} */ e) => {
1271
- update(updateProperty(getState(), path, "tagName", e.target.value || undefined));
1330
+ transactDoc(activeTab.value, (t) =>
1331
+ mutateUpdateProperty(t, path, "tagName", e.target.value || undefined),
1332
+ );
1272
1333
  })}
1273
1334
  ></sp-textfield>
1274
1335
  </div>
@@ -1280,7 +1341,9 @@ export function renderPropertiesPanelTemplate(ctx) {
1280
1341
  title="Clear $id"
1281
1342
  @click=${(/** @type {any} */ e) => {
1282
1343
  e.stopPropagation();
1283
- update(updateProperty(getState(), path, "$id", undefined));
1344
+ transactDoc(activeTab.value, (t) =>
1345
+ mutateUpdateProperty(t, path, "$id", undefined),
1346
+ );
1284
1347
  }}
1285
1348
  ></span>`
1286
1349
  : nothing}
@@ -1290,7 +1353,9 @@ export function renderPropertiesPanelTemplate(ctx) {
1290
1353
  size="s"
1291
1354
  .value=${live(node.$id || "")}
1292
1355
  @input=${debouncedStyleCommit("prop:$id", 400, (/** @type {any} */ e) => {
1293
- update(updateProperty(getState(), path, "$id", e.target.value || undefined));
1356
+ transactDoc(activeTab.value, (t) =>
1357
+ mutateUpdateProperty(t, path, "$id", e.target.value || undefined),
1358
+ );
1294
1359
  })}
1295
1360
  ></sp-textfield>
1296
1361
  </div>
@@ -1302,7 +1367,9 @@ export function renderPropertiesPanelTemplate(ctx) {
1302
1367
  title="Clear class"
1303
1368
  @click=${(/** @type {any} */ e) => {
1304
1369
  e.stopPropagation();
1305
- update(updateProperty(getState(), path, "className", undefined));
1370
+ transactDoc(activeTab.value, (t) =>
1371
+ mutateUpdateProperty(t, path, "className", undefined),
1372
+ );
1306
1373
  }}
1307
1374
  ></span>`
1308
1375
  : nothing}
@@ -1312,7 +1379,9 @@ export function renderPropertiesPanelTemplate(ctx) {
1312
1379
  size="s"
1313
1380
  .value=${live(node.className || "")}
1314
1381
  @input=${debouncedStyleCommit("prop:className", 400, (/** @type {any} */ e) => {
1315
- update(updateProperty(getState(), path, "className", e.target.value || undefined));
1382
+ transactDoc(activeTab.value, (t) =>
1383
+ mutateUpdateProperty(t, path, "className", e.target.value || undefined),
1384
+ );
1316
1385
  })}
1317
1386
  ></sp-textfield>
1318
1387
  </div>
@@ -1326,7 +1395,9 @@ export function renderPropertiesPanelTemplate(ctx) {
1326
1395
  title="Clear text"
1327
1396
  @click=${(/** @type {any} */ e) => {
1328
1397
  e.stopPropagation();
1329
- update(updateProperty(getState(), path, "textContent", undefined));
1398
+ transactDoc(activeTab.value, (t) =>
1399
+ mutateUpdateProperty(t, path, "textContent", undefined),
1400
+ );
1330
1401
  }}
1331
1402
  ></span>`
1332
1403
  : nothing}
@@ -1341,8 +1412,8 @@ export function renderPropertiesPanelTemplate(ctx) {
1341
1412
  : (node.textContent ?? ""),
1342
1413
  )}
1343
1414
  @input=${debouncedStyleCommit("prop:textContent", 400, (/** @type {any} */ e) => {
1344
- update(
1345
- updateProperty(getState(), path, "textContent", e.target.value || undefined),
1415
+ transactDoc(activeTab.value, (t) =>
1416
+ mutateUpdateProperty(t, path, "textContent", e.target.value || undefined),
1346
1417
  );
1347
1418
  })}
1348
1419
  ></sp-textfield>
@@ -1357,7 +1428,9 @@ export function renderPropertiesPanelTemplate(ctx) {
1357
1428
  title="Clear hidden"
1358
1429
  @click=${(/** @type {any} */ e) => {
1359
1430
  e.stopPropagation();
1360
- update(updateProperty(getState(), path, "hidden", undefined));
1431
+ transactDoc(activeTab.value, (t) =>
1432
+ mutateUpdateProperty(t, path, "hidden", undefined),
1433
+ );
1361
1434
  }}
1362
1435
  ></span>`
1363
1436
  : nothing}
@@ -1367,7 +1440,9 @@ export function renderPropertiesPanelTemplate(ctx) {
1367
1440
  size="s"
1368
1441
  .checked=${live(!!node.hidden)}
1369
1442
  @change=${(/** @type {any} */ e) =>
1370
- update(updateProperty(getState(), path, "hidden", e.target.checked || undefined))}
1443
+ transactDoc(activeTab.value, (t) =>
1444
+ mutateUpdateProperty(t, path, "hidden", e.target.checked || undefined),
1445
+ )}
1371
1446
  >
1372
1447
  </sp-checkbox>
1373
1448
  </div>
@@ -1403,9 +1478,9 @@ export function renderPropertiesPanelTemplate(ctx) {
1403
1478
  : nothing;
1404
1479
 
1405
1480
  const observedAttrsT =
1406
- isCustomElementDoc(S) && isRoot
1481
+ isCustomElementDoc({ document: tab.doc.document }) && isRoot
1407
1482
  ? (() => {
1408
- const state = S.document.state || {};
1483
+ const state = tab.doc.document.state || {};
1409
1484
  const entries = Object.entries(state).filter(([, d]) => d.attribute);
1410
1485
  return html`
1411
1486
  <sp-accordion-item label="Observed Attributes" ?open=${isSectionOpen("__observed")}>
@@ -1508,7 +1583,7 @@ export function renderPropertiesPanelTemplate(ctx) {
1508
1583
  : nothing;
1509
1584
 
1510
1585
  const cssPropsT =
1511
- isCustomElementDoc(S) && isRoot
1586
+ isCustomElementDoc({ document: tab.doc.document }) && isRoot
1512
1587
  ? (() => {
1513
1588
  const style = node.style || {};
1514
1589
  const cssProps = Object.entries(style).filter(([k]) => k.startsWith("--"));
@@ -1537,9 +1612,9 @@ export function renderPropertiesPanelTemplate(ctx) {
1537
1612
  : nothing;
1538
1613
 
1539
1614
  const cssPartsT =
1540
- isCustomElementDoc(S) && isRoot
1615
+ isCustomElementDoc({ document: tab.doc.document }) && isRoot
1541
1616
  ? (() => {
1542
- const parts = collectCssParts(S.document);
1617
+ const parts = collectCssParts(tab.doc.document);
1543
1618
  if (parts.length === 0) return nothing;
1544
1619
  return html`
1545
1620
  <sp-accordion-item
@@ -1565,10 +1640,10 @@ export function renderPropertiesPanelTemplate(ctx) {
1565
1640
  : nothing;
1566
1641
 
1567
1642
  const frontmatterT =
1568
- S.mode === "content"
1643
+ tab.doc.mode === "content"
1569
1644
  ? (() => {
1570
- const fm = S.content?.frontmatter || {};
1571
- const col = findContentTypeSchema(S.documentPath, projectState?.projectConfig);
1645
+ const fm = tab.doc.content?.frontmatter || {};
1646
+ const col = findContentTypeSchema(tab.documentPath, projectState?.projectConfig);
1572
1647
  const schemaProps = col?.schema?.properties;
1573
1648
  const requiredFields = new Set(col?.schema?.required || []);
1574
1649