@jxsuite/studio 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/studio.js CHANGED
@@ -37,7 +37,6 @@ import {
37
37
  isAncestor,
38
38
  canvasWrap,
39
39
  leftPanel,
40
- rightPanel,
41
40
  toolbarEl,
42
41
  elToPath,
43
42
  canvasPanels,
@@ -55,6 +54,7 @@ import {
55
54
  runUpdateMiddleware,
56
55
  addPostRenderHook,
57
56
  runPostRenderHooks,
57
+ notify,
58
58
  projectState,
59
59
  setProjectState,
60
60
  updateFrontmatter,
@@ -99,7 +99,12 @@ import {
99
99
  varDisplayName,
100
100
  parseCemType,
101
101
  } from "./utils/studio-utils.js";
102
- import { renderStatusbar, statusMessage, setStatusbarRenderer } from "./panels/statusbar.js";
102
+ import {
103
+ renderStatusbar,
104
+ statusMessage,
105
+ setStatusbarRenderer,
106
+ mountStatusbar,
107
+ } from "./panels/statusbar.js";
103
108
  import {
104
109
  openFile as _openFile,
105
110
  loadMarkdown as _loadMarkdown,
@@ -119,6 +124,13 @@ import { renderHeadTemplate } from "./panels/head-panel.js";
119
124
  import { exportCemManifest as _exportCemManifest } from "./services/cem-export.js";
120
125
 
121
126
  import { registerPlatform, getPlatform, hasPlatform } from "./platform.js";
127
+ import {
128
+ parseMediaEntries,
129
+ activeBreakpointsForWidth,
130
+ applyCanvasStyle,
131
+ collectMediaOverrides,
132
+ applyOverridesToCanvas,
133
+ } from "./utils/canvas-media.js";
122
134
  import { createDevServerPlatform } from "./platforms/devserver.js";
123
135
  import { codeService, setLintMarkers, getFunctionArgs } from "./services/code-services.js";
124
136
  import {
@@ -170,7 +182,6 @@ import { renderDataExplorerTemplate } from "./panels/data-explorer.js";
170
182
  // by Bun's bundler despite sideEffects declarations in Spectrum's package.json.
171
183
  import { components as _swc } from "./ui/spectrum.js"; // eslint-disable-line no-unused-vars
172
184
  import { renderFieldRow } from "./ui/field-row.js";
173
- import { isColorPopoverOpen } from "./ui/color-selector.js";
174
185
  import { widgetForType as _widgetForType } from "./ui/widgets.js";
175
186
  import { computeInheritedStyle } from "./utils/inherited-style.js";
176
187
  import "./ui/panel-resize.js";
@@ -739,9 +750,6 @@ document.body.appendChild(zoomIndicatorHost);
739
750
 
740
751
  // ─── Module-level UI state (must be before render() call) ─────────────────────
741
752
 
742
- let elementsCollapsed = new Set();
743
- let elementsFilter = "";
744
-
745
753
  // ─── Bootstrap ────────────────────────────────────────────────────────────────
746
754
 
747
755
  // Register the dev server platform adapter (PAL) as default if none pre-registered
@@ -804,6 +812,7 @@ registerRenderer("rightPanel", () => rightPanelMod.render());
804
812
  registerRenderer("overlays", () => overlaysPanel.render());
805
813
  registerRenderer("statusbar", () => renderStatusbar(S));
806
814
  setStatusbarRenderer(() => renderStatusbar(S));
815
+ mountStatusbar();
807
816
 
808
817
  function safeRenderLeftPanel() {
809
818
  try {
@@ -853,12 +862,6 @@ setUpdateFn(function _update(/** @type {any} */ newState) {
853
862
  const leftUiChanged =
854
863
  uiChanged && (prev.ui?.leftTab !== S.ui?.leftTab || prev.ui?.settingsTab !== S.ui?.settingsTab);
855
864
 
856
- try {
857
- renderToolbar();
858
- } catch (e) {
859
- console.error("renderToolbar error:", e);
860
- }
861
-
862
865
  if (docChanged || modeChanged || canvasUiChanged) {
863
866
  try {
864
867
  renderCanvas();
@@ -874,38 +877,16 @@ setUpdateFn(function _update(/** @type {any} */ newState) {
874
877
  updateActivePanelHeaders();
875
878
  }
876
879
 
877
- // Skip right-panel rebuild when an input inside it is focused (user is typing)
878
- // unless the selection changed — that always needs a full re-render
879
- // Also re-render when color popover is open (changes come from outside rightPanel)
880
- const colorPopoverOpen = isColorPopoverOpen();
881
- const activeTag = document.activeElement?.tagName;
882
- const rightHasFocus =
883
- !colorPopoverOpen &&
884
- rightPanel.contains(document.activeElement) &&
885
- (activeTag === "INPUT" ||
886
- activeTag === "TEXTAREA" ||
887
- activeTag === "SP-TEXTFIELD" ||
888
- activeTag === "SP-NUMBER-FIELD" ||
889
- activeTag === "SP-PICKER" ||
890
- activeTag === "SP-COMBOBOX" ||
891
- activeTag === "SP-SEARCH");
892
- if (!rightHasFocus || selChanged || uiChanged) {
893
- safeRenderRightPanel();
894
- }
895
-
896
- try {
897
- renderOverlays();
898
- } catch (e) {
899
- console.error("renderOverlays error:", e);
900
- }
901
- try {
902
- renderStatusbar(S);
903
- } catch (e) {
904
- console.error("renderStatusbar error:", e);
905
- }
906
-
907
880
  runPostRenderHooks(prevDoc, prevSel);
908
881
  runUpdateMiddleware(S);
882
+
883
+ notify({
884
+ doc: docChanged,
885
+ selection: selChanged,
886
+ hover: false,
887
+ ui: uiChanged,
888
+ mode: modeChanged,
889
+ });
909
890
  });
910
891
 
911
892
  // Register session dispatch — lightweight path for selection/hover/ui changes
@@ -934,12 +915,6 @@ setUpdateSessionFn(function _updateSession(/** @type {any} */ patch) {
934
915
  uiChanged &&
935
916
  (prev.ui?.leftTab !== session.ui?.leftTab || prev.ui?.settingsTab !== session.ui?.settingsTab);
936
917
 
937
- try {
938
- renderToolbar();
939
- } catch (e) {
940
- console.error("renderToolbar error:", e);
941
- }
942
-
943
918
  if (canvasUiChanged) {
944
919
  try {
945
920
  renderCanvas();
@@ -955,22 +930,10 @@ setUpdateSessionFn(function _updateSession(/** @type {any} */ patch) {
955
930
  updateActivePanelHeaders();
956
931
  }
957
932
 
958
- if (selChanged || uiChanged) {
959
- safeRenderRightPanel();
960
- }
961
-
962
- try {
963
- renderOverlays();
964
- } catch (e) {
965
- console.error("renderOverlays error:", e);
966
- }
967
- try {
968
- renderStatusbar(S);
969
- } catch (e) {
970
- console.error("renderStatusbar error:", e);
971
- }
972
-
973
933
  runPostRenderHooks(doc.document, prev.selection);
934
+
935
+ const hoverChanged = prev.hover !== session.hover;
936
+ notify({ doc: false, selection: selChanged, hover: hoverChanged, ui: uiChanged, mode: false });
974
937
  });
975
938
 
976
939
  // Register post-render hook for pseudo-state preview
@@ -1084,86 +1047,6 @@ if (_openParam) {
1084
1047
 
1085
1048
  // ─── Media helpers ────────────────────────────────────────────────────────────
1086
1049
 
1087
- /**
1088
- * Classify $media entries into size breakpoints (get a canvas each) and feature queries (rendered
1089
- * as toolbar toggles).
1090
- *
1091
- * @param {any} mediaDef
1092
- */
1093
- function parseMediaEntries(mediaDef) {
1094
- if (!mediaDef) return { sizeBreakpoints: [], featureQueries: [], baseWidth: 320 };
1095
- const sizes = [],
1096
- features = [];
1097
- let baseWidth = 320;
1098
- for (const [name, query] of Object.entries(mediaDef)) {
1099
- if (name === "--") {
1100
- const wm = String(query).match(/^(\d+)\s*px$/);
1101
- baseWidth = wm ? parseFloat(wm[1]) : 320;
1102
- continue;
1103
- }
1104
- const minMatch = query.match(/min-width:\s*([\d.]+)px/);
1105
- const maxMatch = query.match(/max-width:\s*([\d.]+)px/);
1106
- if (minMatch) sizes.push({ name, query, width: parseFloat(minMatch[1]), type: "min" });
1107
- else if (maxMatch) sizes.push({ name, query, width: parseFloat(maxMatch[1]), type: "max" });
1108
- else features.push({ name, query });
1109
- }
1110
- sizes.sort((a, b) => (a.type === "min" ? a.width - b.width : b.width - a.width));
1111
- return { sizeBreakpoints: sizes, featureQueries: features, baseWidth };
1112
- }
1113
-
1114
- /**
1115
- * Compute which named breakpoints are active at a given canvas width. For min-width canvases: all
1116
- * breakpoints with min-width <= canvasWidth are active. For max-width canvases: all breakpoints
1117
- * with max-width >= canvasWidth are active.
1118
- *
1119
- * @param {any} sizeBreakpoints
1120
- * @param {any} canvasWidth
1121
- */
1122
- function activeBreakpointsForWidth(sizeBreakpoints, canvasWidth) {
1123
- const active = new Set();
1124
- for (const bp of sizeBreakpoints) {
1125
- if (bp.type === "min" && canvasWidth >= bp.width) active.add(bp.name);
1126
- else if (bp.type === "max" && canvasWidth <= bp.width) active.add(bp.name);
1127
- }
1128
- return active;
1129
- }
1130
-
1131
- /**
1132
- * Apply styles to a canvas element, including active media overrides. Base (flat) styles applied
1133
- * first, then matching media overrides in source order.
1134
- *
1135
- * @param {any} el
1136
- * @param {any} styleDef
1137
- * @param {any} activeBreakpoints
1138
- * @param {any} featureToggles
1139
- */
1140
- function applyCanvasStyle(el, styleDef, activeBreakpoints, featureToggles) {
1141
- if (!styleDef || typeof styleDef !== "object") return;
1142
- for (const [prop, val] of Object.entries(styleDef)) {
1143
- if (typeof val === "string" || typeof val === "number") {
1144
- try {
1145
- if (prop.startsWith("--")) el.style.setProperty(prop, String(val));
1146
- else /** @type {any} */ (el.style)[prop] = val;
1147
- } catch {}
1148
- }
1149
- }
1150
- for (const [key, val] of Object.entries(styleDef)) {
1151
- if (!key.startsWith("@") || typeof val !== "object") continue;
1152
- const mediaName = key.slice(1);
1153
- if (mediaName === "--") continue; // skip base canvas width key
1154
- if (activeBreakpoints.has(mediaName) || featureToggles[mediaName]) {
1155
- for (const [prop, v] of Object.entries(/** @type {any} */ (val))) {
1156
- if (typeof v === "string" || typeof v === "number") {
1157
- try {
1158
- if (prop.startsWith("--")) el.style.setProperty(prop, String(v));
1159
- else /** @type {any} */ (el.style)[prop] = v;
1160
- } catch {}
1161
- }
1162
- }
1163
- }
1164
- }
1165
- }
1166
-
1167
1050
  /**
1168
1051
  * After a runtime render, apply active media overrides as inline styles so they beat the base
1169
1052
  * inline styles the runtime already set. The runtime uses @media CSS rules for overrides, but those
@@ -1174,26 +1057,13 @@ function applyCanvasStyle(el, styleDef, activeBreakpoints, featureToggles) {
1174
1057
  */
1175
1058
  function applyCanvasMediaOverrides(canvasEl, activeBreakpoints) {
1176
1059
  if (!activeBreakpoints.size) return;
1177
- for (const el of /** @type {NodeListOf<HTMLElement>} */ (canvasEl.querySelectorAll("*"))) {
1178
- const path = elToPath.get(el);
1179
- if (!path) continue;
1180
- const node = getNodeAtPath(S.document, path);
1181
- if (!node?.style) continue;
1182
- for (const [key, val] of Object.entries(node.style)) {
1183
- if (!key.startsWith("@") || typeof val !== "object") continue;
1184
- const mediaName = key.slice(1);
1185
- if (mediaName === "--") continue;
1186
- if (!activeBreakpoints.has(mediaName)) continue;
1187
- for (const [prop, v] of Object.entries(/** @type {any} */ (val))) {
1188
- if (typeof v === "string" || typeof v === "number") {
1189
- try {
1190
- if (prop.startsWith("--")) el.style.setProperty(prop, String(v));
1191
- else /** @type {any} */ (el.style)[prop] = v;
1192
- } catch {}
1193
- }
1194
- }
1195
- }
1060
+ const docMedia = getEffectiveMedia(S.document.$media || {});
1061
+ const validBreakpoints = new Set();
1062
+ for (const name of activeBreakpoints) {
1063
+ if (docMedia[name]) validBreakpoints.add(name);
1196
1064
  }
1065
+ const overrides = collectMediaOverrides(document.styleSheets, validBreakpoints);
1066
+ applyOverridesToCanvas(canvasEl, overrides);
1197
1067
  }
1198
1068
 
1199
1069
  // ─── Canvas ───────────────────────────────────────────────────────────────────
@@ -1926,7 +1796,6 @@ function renderCanvasNode(node, path, parent, activeBreakpoints, featureToggles)
1926
1796
  *
1927
1797
  * @type {any}
1928
1798
  */
1929
- let lastDragInput = null;
1930
1799
 
1931
1800
  /**
1932
1801
  * Register all canvas elements in a panel as DnD drop targets.
@@ -1946,12 +1815,12 @@ function registerPanelDnD(panel) {
1946
1815
  for (const p of canvasPanels) p.overlayClk.style.pointerEvents = "none";
1947
1816
  },
1948
1817
  onDrag({ location }) {
1949
- lastDragInput = location.current.input;
1818
+ view.lastDragInput = location.current.input;
1950
1819
  },
1951
1820
  onDrop() {
1952
1821
  // Hide all drop lines
1953
1822
  for (const p of canvasPanels) p.dropLine.style.display = "none";
1954
- lastDragInput = null;
1823
+ view.lastDragInput = null;
1955
1824
  for (const el of canvas.querySelectorAll("*")) {
1956
1825
  /** @type {any} */ (el).style.pointerEvents = "none";
1957
1826
  }
@@ -2006,8 +1875,8 @@ function registerPanelDnD(panel) {
2006
1875
  */
2007
1876
  function getCanvasDropInstruction(el, elPath, isVoid) {
2008
1877
  const rect = el.getBoundingClientRect();
2009
- if (!lastDragInput) return null;
2010
- const y = lastDragInput.clientY;
1878
+ if (!view.lastDragInput) return null;
1879
+ const y = view.lastDragInput.clientY;
2011
1880
  const relY = (y - rect.top) / rect.height;
2012
1881
 
2013
1882
  if (elPath.length === 0) return { type: "make-child" };
@@ -2127,15 +1996,10 @@ function onBarMousedown(e) {
2127
1996
  e.preventDefault();
2128
1997
  }
2129
1998
 
2130
- /**
2131
- * Saved selection range for format button mousedown→click flow
2132
- *
2133
- * @type {any}
2134
- */
2135
- let savedRange = null;
1999
+ /** Saved selection range for format button mousedown→click flow */
2136
2000
  function captureSelectionRange() {
2137
2001
  const sel = window.getSelection();
2138
- if (sel && sel.rangeCount) savedRange = sel.getRangeAt(0).cloneRange();
2002
+ if (sel && sel.rangeCount) view.savedRange = sel.getRangeAt(0).cloneRange();
2139
2003
  }
2140
2004
 
2141
2005
  /**
@@ -2146,16 +2010,16 @@ function onFormatClick(e, action) {
2146
2010
  e.stopPropagation();
2147
2011
  if (action.command === "link") {
2148
2012
  showLinkPopover(e.target.closest("sp-action-button"));
2149
- } else if (savedRange) {
2013
+ } else if (view.savedRange) {
2150
2014
  const sel = /** @type {any} */ (window.getSelection());
2151
- const anchor = savedRange.startContainer;
2015
+ const anchor = view.savedRange.startContainer;
2152
2016
  const editableRoot = (
2153
2017
  anchor?.nodeType === Node.ELEMENT_NODE ? anchor : anchor?.parentElement
2154
2018
  )?.closest("[contenteditable]");
2155
2019
  if (editableRoot) {
2156
2020
  editableRoot.focus();
2157
2021
  sel.removeAllRanges();
2158
- sel.addRange(savedRange);
2022
+ sel.addRange(view.savedRange);
2159
2023
  applyInlineFormat(action);
2160
2024
  }
2161
2025
  }
@@ -3683,7 +3547,7 @@ function registerLayersDnD() {
3683
3547
  },
3684
3548
  onDragStart() {
3685
3549
  row.classList.add("dragging");
3686
- layerDragSourceHeight = row.offsetHeight;
3550
+ view.layerDragSourceHeight = row.offsetHeight;
3687
3551
  },
3688
3552
  onDrop() {
3689
3553
  row.classList.remove("dragging");
@@ -3787,8 +3651,6 @@ function registerComponentsDnD() {
3787
3651
  }
3788
3652
 
3789
3653
  /** @type {any} */
3790
- let _currentDropTargetRow = null;
3791
- let layerDragSourceHeight = 0;
3792
3654
 
3793
3655
  /**
3794
3656
  * @param {any} rowEl
@@ -3799,8 +3661,8 @@ function showLayerDropGap(rowEl, data, container) {
3799
3661
  const instruction = extractInstruction(data);
3800
3662
 
3801
3663
  // Clear previous drop-target highlight
3802
- if (_currentDropTargetRow && _currentDropTargetRow !== rowEl) {
3803
- _currentDropTargetRow.classList.remove("drop-target");
3664
+ if (view._currentDropTargetRow && view._currentDropTargetRow !== rowEl) {
3665
+ view._currentDropTargetRow.classList.remove("drop-target");
3804
3666
  }
3805
3667
 
3806
3668
  if (!instruction || instruction.type === "instruction-blocked") {
@@ -3811,17 +3673,17 @@ function showLayerDropGap(rowEl, data, container) {
3811
3673
  if (instruction.type === "make-child") {
3812
3674
  clearLayerDropGap(container);
3813
3675
  rowEl.classList.add("drop-target");
3814
- _currentDropTargetRow = rowEl;
3676
+ view._currentDropTargetRow = rowEl;
3815
3677
  return;
3816
3678
  }
3817
3679
 
3818
3680
  rowEl.classList.remove("drop-target");
3819
- _currentDropTargetRow = rowEl;
3681
+ view._currentDropTargetRow = rowEl;
3820
3682
 
3821
3683
  // Shift rows to create gap
3822
3684
  const rows = Array.from(container.querySelectorAll(".layers-tree .layer-row"));
3823
3685
  const targetIdx = rows.indexOf(rowEl);
3824
- const gap = layerDragSourceHeight;
3686
+ const gap = view.layerDragSourceHeight;
3825
3687
 
3826
3688
  for (let i = 0; i < rows.length; i++) {
3827
3689
  if (rows[i].classList.contains("dragging")) continue;
@@ -3835,9 +3697,9 @@ function showLayerDropGap(rowEl, data, container) {
3835
3697
 
3836
3698
  /** @param {any} container */
3837
3699
  function clearLayerDropGap(container) {
3838
- if (_currentDropTargetRow) {
3839
- _currentDropTargetRow.classList.remove("drop-target");
3840
- _currentDropTargetRow = null;
3700
+ if (view._currentDropTargetRow) {
3701
+ view._currentDropTargetRow.classList.remove("drop-target");
3702
+ view._currentDropTargetRow = null;
3841
3703
  }
3842
3704
  const rows = container.querySelectorAll(".layers-tree .layer-row");
3843
3705
  for (const r of rows) r.style.transform = "";
@@ -3850,25 +3712,22 @@ function clearLayerDropGap(container) {
3850
3712
  * @param {string | null} [media]
3851
3713
  */
3852
3714
  function selectStylebookTag(tag, media) {
3853
- S = {
3854
- ...S,
3715
+ updateSession({
3855
3716
  selection: [],
3856
3717
  ui: {
3857
- ...S.ui,
3858
3718
  stylebookSelection: tag,
3859
3719
  rightTab: "style",
3860
3720
  activeSelector: `& ${tag}`,
3861
3721
  ...(media !== undefined ? { activeMedia: media } : {}),
3862
3722
  },
3863
- };
3723
+ });
3864
3724
  renderStylebookOverlays();
3865
- renderRightPanel();
3866
- renderLeftPanel();
3867
- renderToolbar();
3868
- if (canvasPanels.length > 0) {
3869
- const el = findStylebookEl(canvasPanels[0].canvas, tag);
3870
- if (el) el.scrollIntoView({ behavior: "smooth", block: "center" });
3871
- }
3725
+ requestAnimationFrame(() => {
3726
+ if (canvasPanels.length > 0) {
3727
+ const el = findStylebookEl(canvasPanels[0].canvas, tag);
3728
+ if (el) el.scrollIntoView({ behavior: "smooth", block: "center" });
3729
+ }
3730
+ });
3872
3731
  }
3873
3732
 
3874
3733
  function renderStylebookLayersTemplate() {
@@ -4121,18 +3980,18 @@ const unsafeTags = new Set(["script", "style", "link", "iframe", "object", "embe
4121
3980
  function renderElementsTemplate() {
4122
3981
  const categories = Object.entries(webdata.elements).map(
4123
3982
  (/** @type {any} */ [category, elements]) => {
4124
- const filtered = elementsFilter
4125
- ? elements.filter((/** @type {any} */ e) => e.tag.includes(elementsFilter))
3983
+ const filtered = view.elementsFilter
3984
+ ? elements.filter((/** @type {any} */ e) => e.tag.includes(view.elementsFilter))
4126
3985
  : elements;
4127
3986
  if (filtered.length === 0) return nothing;
4128
3987
 
4129
3988
  return html`
4130
3989
  <sp-accordion-item
4131
3990
  label=${category}
4132
- ?open=${!elementsCollapsed.has(category)}
3991
+ ?open=${!view.elementsCollapsed.has(category)}
4133
3992
  @sp-accordion-item-toggle=${(/** @type {any} */ e) => {
4134
- if (e.target.open) elementsCollapsed.delete(category);
4135
- else elementsCollapsed.add(category);
3993
+ if (e.target.open) view.elementsCollapsed.delete(category);
3994
+ else view.elementsCollapsed.add(category);
4136
3995
  }}
4137
3996
  >
4138
3997
  ${filtered.map((/** @type {any} */ { tag }) => {
@@ -4184,7 +4043,7 @@ function renderElementsTemplate() {
4184
4043
  .filter((/** @type {any} */ c) => c.source !== "npm" || enabledTags.has(c.tagName))
4185
4044
  .filter(
4186
4045
  (/** @type {any} */ c) =>
4187
- !elementsFilter || c.tagName.toLowerCase().includes(elementsFilter),
4046
+ !view.elementsFilter || c.tagName.toLowerCase().includes(view.elementsFilter),
4188
4047
  )
4189
4048
  : [];
4190
4049
 
@@ -4193,10 +4052,10 @@ function renderElementsTemplate() {
4193
4052
  ? html`
4194
4053
  <sp-accordion-item
4195
4054
  label="Components"
4196
- ?open=${!elementsCollapsed.has("Components")}
4055
+ ?open=${!view.elementsCollapsed.has("Components")}
4197
4056
  @sp-accordion-item-toggle=${(/** @type {any} */ e) => {
4198
- if (e.target.open) elementsCollapsed.delete("Components");
4199
- else elementsCollapsed.add("Components");
4057
+ if (e.target.open) view.elementsCollapsed.delete("Components");
4058
+ else view.elementsCollapsed.add("Components");
4200
4059
  }}
4201
4060
  >
4202
4061
  <div class="components-section">
@@ -4242,9 +4101,9 @@ function renderElementsTemplate() {
4242
4101
  <sp-search
4243
4102
  size="s"
4244
4103
  placeholder="Filter elements…"
4245
- value=${elementsFilter}
4104
+ value=${view.elementsFilter}
4246
4105
  @input=${(/** @type {any} */ e) => {
4247
- elementsFilter = e.target.value.toLowerCase();
4106
+ view.elementsFilter = e.target.value.toLowerCase();
4248
4107
  renderLeftPanel();
4249
4108
  }}
4250
4109
  ></sp-search>
@@ -4283,7 +4142,6 @@ function registerElementsDnD() {
4283
4142
  // ─── Stylebook ───────────────────────────────────────────────────────────────
4284
4143
 
4285
4144
  /** Map from rendered stylebook DOM elements to their tag names */
4286
- let stylebookElToTag = new WeakMap();
4287
4145
 
4288
4146
  /**
4289
4147
  * Build a DOM element tree from a stylebook-meta.json entry. Applies any existing tag-scoped styles
@@ -4431,7 +4289,7 @@ function renderSettings() {
4431
4289
  }
4432
4290
 
4433
4291
  // Stylebook tab — existing behavior
4434
- stylebookElToTag = new WeakMap();
4292
+ view.stylebookElToTag = new WeakMap();
4435
4293
  const rootStyle = getEffectiveStyle(S.document.style);
4436
4294
  const filter = (S.ui.stylebookFilter || "").toLowerCase();
4437
4295
  const customizedOnly = S.ui.stylebookCustomizedOnly;
@@ -4626,12 +4484,12 @@ function renderStylebookElementsIntoCanvas(
4626
4484
  class="element-card"
4627
4485
  ${ref((card) => {
4628
4486
  if (!card) return;
4629
- stylebookElToTag.set(card, entry.tag);
4487
+ view.stylebookElToTag.set(card, entry.tag);
4630
4488
  elToPath.set(card, ["__sb", entry.tag]);
4631
4489
  for (const child of el.querySelectorAll("*")) {
4632
4490
  const tag = child.tagName.toLowerCase();
4633
- if (!stylebookElToTag.has(child)) {
4634
- stylebookElToTag.set(child, tag);
4491
+ if (!view.stylebookElToTag.has(child)) {
4492
+ view.stylebookElToTag.set(child, tag);
4635
4493
  elToPath.set(child, ["__sb", tag]);
4636
4494
  }
4637
4495
  }
@@ -4673,7 +4531,7 @@ function renderStylebookElementsIntoCanvas(
4673
4531
  style="display:inline-flex;width:auto"
4674
4532
  ${ref((card) => {
4675
4533
  if (!card) return;
4676
- stylebookElToTag.set(card, comp.tagName);
4534
+ view.stylebookElToTag.set(card, comp.tagName);
4677
4535
  elToPath.set(card, ["__sb", comp.tagName]);
4678
4536
  })}
4679
4537
  >
@@ -5154,7 +5012,8 @@ function createUnitInput(initialValue, { onChange, size = "s" } = {}) {
5154
5012
  }
5155
5013
 
5156
5014
  /**
5157
- * Click handler for stylebook canvas — selects elements via the elToPath/stylebookElToTag mapping
5015
+ * Click handler for stylebook canvas — selects elements via the elToPath/view.stylebookElToTag
5016
+ * mapping
5158
5017
  *
5159
5018
  * @param {any} panel
5160
5019
  */
@@ -5175,7 +5034,7 @@ function registerStylebookPanelEvents(panel) {
5175
5034
  if (!canvas.contains(el) || el === canvas) continue;
5176
5035
  let cur = /** @type {any} */ (el);
5177
5036
  while (cur && cur !== canvas) {
5178
- const tag = stylebookElToTag.get(cur);
5037
+ const tag = view.stylebookElToTag.get(cur);
5179
5038
  if (tag) {
5180
5039
  const newMedia = panel.mediaName === "base" ? null : (panel.mediaName ?? null);
5181
5040
  selectStylebookTag(tag, newMedia);
@@ -5203,7 +5062,7 @@ function registerStylebookPanelEvents(panel) {
5203
5062
  if (!canvas.contains(el) || el === canvas) continue;
5204
5063
  let cur = /** @type {any} */ (el);
5205
5064
  while (cur && cur !== canvas) {
5206
- const tag = stylebookElToTag.get(cur);
5065
+ const tag = view.stylebookElToTag.get(cur);
5207
5066
  if (tag) {
5208
5067
  hoverTag = tag;
5209
5068
  break;
@@ -5273,7 +5132,7 @@ function renderStylebookOverlays() {
5273
5132
  /** Find a stylebook element by tag in the canvas */
5274
5133
  function findStylebookEl(/** @type {any} */ canvasEl, /** @type {any} */ tag) {
5275
5134
  for (const child of canvasEl.querySelectorAll("*")) {
5276
- if (stylebookElToTag.get(child) === tag) return child;
5135
+ if (view.stylebookElToTag.get(child) === tag) return child;
5277
5136
  }
5278
5137
  return null;
5279
5138
  }
@@ -5571,11 +5430,7 @@ function propertiesSidebarTemplate() {
5571
5430
 
5572
5431
  function toggleSection(/** @type {any} */ key) {
5573
5432
  const current = isSectionOpen(key);
5574
- S = {
5575
- ...S,
5576
- ui: { ...S.ui, inspectorSections: { ...S.ui.inspectorSections, [key]: !current } },
5577
- };
5578
- renderRightPanel();
5433
+ updateUi("inspectorSections", { ...S.ui.inspectorSections, [key]: !current });
5579
5434
  }
5580
5435
 
5581
5436
  // ── Build section templates ─────────────────────────────────────────
@@ -6271,8 +6126,6 @@ function renderCustomAttrsFieldsTemplate(
6271
6126
  }
6272
6127
 
6273
6128
  /** Media breakpoint fields template */
6274
- let showAddBreakpointForm = false;
6275
- let addBreakpointPreview = "";
6276
6129
 
6277
6130
  function renderMediaFieldsTemplate(/** @type {any} */ node) {
6278
6131
  const media = node.$media || {};
@@ -6308,14 +6161,14 @@ function renderMediaFieldsTemplate(/** @type {any} */ node) {
6308
6161
  <div>
6309
6162
  <span
6310
6163
  class="kv-add"
6311
- style=${showAddBreakpointForm ? "display:none" : ""}
6164
+ style=${view.showAddBreakpointForm ? "display:none" : ""}
6312
6165
  @click=${(/** @type {any} */ _e) => {
6313
- showAddBreakpointForm = true;
6166
+ view.showAddBreakpointForm = true;
6314
6167
  renderRightPanel();
6315
6168
  }}
6316
6169
  >+ Add breakpoint</span
6317
6170
  >
6318
- ${showAddBreakpointForm
6171
+ ${view.showAddBreakpointForm
6319
6172
  ? html`
6320
6173
  <div style="margin-top:4px">
6321
6174
  <div style="display:flex;gap:4px;margin-bottom:3px;align-items:center">
@@ -6324,13 +6177,13 @@ function renderMediaFieldsTemplate(/** @type {any} */ node) {
6324
6177
  placeholder="Name (e.g. Tablet)"
6325
6178
  style="flex:1"
6326
6179
  @input=${(/** @type {any} */ e) => {
6327
- addBreakpointPreview = friendlyNameToMedia(e.target.value) || "";
6180
+ view.addBreakpointPreview = friendlyNameToMedia(e.target.value) || "";
6328
6181
  renderRightPanel();
6329
6182
  }}
6330
6183
  />
6331
6184
  <span
6332
6185
  style="font-size:10px;color:var(--fg-dim);font-family:'SF Mono','Fira Code',monospace;white-space:nowrap"
6333
- >${addBreakpointPreview}</span
6186
+ >${view.addBreakpointPreview}</span
6334
6187
  >
6335
6188
  </div>
6336
6189
  <div style="display:flex;gap:4px;margin-bottom:3px;align-items:center">
@@ -6346,8 +6199,8 @@ function renderMediaFieldsTemplate(/** @type {any} */ node) {
6346
6199
  const queryVal = wrap.querySelector(".add-bp-query")?.value?.trim();
6347
6200
  const key = friendlyNameToMedia(nameVal);
6348
6201
  if (key && queryVal) {
6349
- showAddBreakpointForm = false;
6350
- addBreakpointPreview = "";
6202
+ view.showAddBreakpointForm = false;
6203
+ view.addBreakpointPreview = "";
6351
6204
  update(updateMedia(S, key, queryVal));
6352
6205
  }
6353
6206
  }}
@@ -6358,8 +6211,8 @@ function renderMediaFieldsTemplate(/** @type {any} */ node) {
6358
6211
  class="kv-add"
6359
6212
  style="padding:2px 10px;cursor:pointer;color:var(--fg-dim)"
6360
6213
  @click=${() => {
6361
- showAddBreakpointForm = false;
6362
- addBreakpointPreview = "";
6214
+ view.showAddBreakpointForm = false;
6215
+ view.addBreakpointPreview = "";
6363
6216
  renderRightPanel();
6364
6217
  }}
6365
6218
  >
@@ -6868,14 +6721,7 @@ function renderShorthandRow(
6868
6721
  quiet
6869
6722
  @click=${(/** @type {any} */ e) => {
6870
6723
  e.stopPropagation();
6871
- S = {
6872
- ...S,
6873
- ui: {
6874
- ...S.ui,
6875
- styleShorthands: { ...S.ui.styleShorthands, [shortProp]: !isExpanded },
6876
- },
6877
- };
6878
- renderRightPanel();
6724
+ updateUi("styleShorthands", { ...S.ui.styleShorthands, [shortProp]: !isExpanded });
6879
6725
  }}
6880
6726
  >
6881
6727
  ${isExpanded
@@ -7181,10 +7027,7 @@ function styleSidebarTemplate(
7181
7027
  label=${sec.label}
7182
7028
  .open=${isOpen}
7183
7029
  @sp-accordion-item-toggle=${(/** @type {any} */ e) => {
7184
- S = {
7185
- ...S,
7186
- ui: { ...S.ui, styleSections: { ...S.ui.styleSections, [sec.key]: e.target.open } },
7187
- };
7030
+ updateUi("styleSections", { ...S.ui.styleSections, [sec.key]: e.target.open });
7188
7031
  }}
7189
7032
  >
7190
7033
  ${sectionActiveProps.length > 0
@@ -7225,10 +7068,7 @@ function styleSidebarTemplate(
7225
7068
  label="Custom"
7226
7069
  .open=${customIsOpen}
7227
7070
  @sp-accordion-item-toggle=${(/** @type {any} */ e) => {
7228
- S = {
7229
- ...S,
7230
- ui: { ...S.ui, styleSections: { ...S.ui.styleSections, other: e.target.open } },
7231
- };
7071
+ updateUi("styleSections", { ...S.ui.styleSections, other: e.target.open });
7232
7072
  }}
7233
7073
  >
7234
7074
  <div>
@@ -7679,10 +7519,9 @@ function getFunctionBody(/** @type {any} */ editing) {
7679
7519
  }
7680
7520
 
7681
7521
  // Register Monaco JS completion provider for state scope variables (once)
7682
- let _completionRegistered = false;
7683
7522
  function registerFunctionCompletions() {
7684
- if (_completionRegistered) return;
7685
- _completionRegistered = true;
7523
+ if (view._completionRegistered) return;
7524
+ view._completionRegistered = true;
7686
7525
  monaco.languages.registerCompletionItemProvider("javascript", {
7687
7526
  triggerCharacters: ["."],
7688
7527
  provideCompletionItems(model, position) {
@@ -7816,13 +7655,12 @@ initShortcuts(() => ({
7816
7655
  // ─── Autosave (registered as update middleware) ──────────────────────────────
7817
7656
 
7818
7657
  /** @type {any} */
7819
- let autosaveTimer;
7820
7658
  const AUTO_SAVE_DELAY = 2000;
7821
7659
 
7822
7660
  function scheduleAutosave() {
7823
7661
  if (!S.fileHandle || !S.dirty) return;
7824
- clearTimeout(autosaveTimer);
7825
- autosaveTimer = setTimeout(async () => {
7662
+ clearTimeout(view.autosaveTimer);
7663
+ view.autosaveTimer = setTimeout(async () => {
7826
7664
  if (S.fileHandle && S.dirty && "createWritable" in S.fileHandle) {
7827
7665
  try {
7828
7666
  const writable = await S.fileHandle.createWritable();