@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
@@ -7,18 +7,19 @@ import { html, nothing } from "lit-html";
7
7
  import { live } from "lit-html/directives/live.js";
8
8
  import { ifDefined } from "lit-html/directives/if-defined.js";
9
9
  import {
10
- getState,
11
- update,
12
- updateUi,
13
10
  getNodeAtPath,
14
- updateStyle,
15
- updateMediaStyle,
16
- updateNestedStyle,
17
- updateMediaNestedStyle,
18
11
  COMMON_SELECTORS,
19
12
  isNestedSelector,
20
13
  debouncedStyleCommit,
21
14
  } from "../store.js";
15
+ import { activeTab } from "../workspace/workspace.js";
16
+ import {
17
+ transactDoc,
18
+ mutateUpdateStyle,
19
+ mutateUpdateMediaStyle,
20
+ mutateUpdateNestedStyle,
21
+ mutateUpdateMediaNestedStyle,
22
+ } from "../tabs/transact.js";
22
23
  import { inferInputType, propLabel } from "../utils/studio-utils.js";
23
24
  import { renderFieldRow } from "../ui/field-row.js";
24
25
  import { parseMediaEntries } from "../utils/canvas-media.js";
@@ -65,15 +66,15 @@ function renderStyleRow(
65
66
  }
66
67
 
67
68
  /**
68
- * @param {any} shortProp @param {any} entry @param {any} style @param {any} commitFn
69
+ * @param {any} shortProp @param {any} entry @param {any} style @param {any} mutateFn
69
70
  * @param {any} _deleteFn @param {Record<string, any>} inherited
70
71
  */
71
- function renderShorthandRow(shortProp, entry, style, commitFn, _deleteFn, inherited = {}) {
72
- const S = getState();
72
+ function renderShorthandRow(shortProp, entry, style, mutateFn, _deleteFn, inherited = {}) {
73
+ const tab = activeTab.value;
73
74
  const longhands = getLonghands(shortProp);
74
75
  const shortVal = style[shortProp];
75
76
  const hasLonghands = longhands.some((/** @type {any} */ l) => style[l.name] !== undefined);
76
- const isExpanded = S.ui.styleShorthands[shortProp] ?? hasLonghands;
77
+ const isExpanded = tab.session.ui.styleShorthands[shortProp] ?? hasLonghands;
77
78
  const hasAnyVal =
78
79
  shortVal !== undefined || longhands.some((/** @type {any} */ l) => style[l.name] !== undefined);
79
80
 
@@ -86,12 +87,12 @@ function renderShorthandRow(shortProp, entry, style, commitFn, _deleteFn, inheri
86
87
  title="Clear ${shortProp}"
87
88
  @click=${(/** @type {any} */ e) => {
88
89
  e.stopPropagation();
89
- let s = getState();
90
- if (shortVal !== undefined) s = commitFn(s, shortProp, undefined);
91
- for (const l of longhands) {
92
- if (style[l.name] !== undefined) s = commitFn(s, l.name, undefined);
93
- }
94
- update(s);
90
+ transactDoc(activeTab.value, (t) => {
91
+ if (shortVal !== undefined) mutateFn(t, shortProp, undefined);
92
+ for (const l of longhands) {
93
+ if (style[l.name] !== undefined) mutateFn(t, l.name, undefined);
94
+ }
95
+ });
95
96
  }}
96
97
  ></span>`
97
98
  : nothing}
@@ -109,12 +110,12 @@ function renderShorthandRow(shortProp, entry, style, commitFn, _deleteFn, inheri
109
110
  ? longhands.map((/** @type {any} */ l) => inherited[l.name] || "0").join(" ")
110
111
  : ""}
111
112
  @input=${debouncedStyleCommit(`short:${shortProp}`, 400, (/** @type {any} */ e) => {
112
- let s = getState();
113
- for (const l of longhands) {
114
- if (style[l.name] !== undefined) s = commitFn(s, l.name, undefined);
115
- }
116
- s = commitFn(s, shortProp, e.target.value || undefined);
117
- update(s);
113
+ transactDoc(activeTab.value, (t) => {
114
+ for (const l of longhands) {
115
+ if (style[l.name] !== undefined) mutateFn(t, l.name, undefined);
116
+ }
117
+ mutateFn(t, shortProp, e.target.value || undefined);
118
+ });
118
119
  })}
119
120
  ></sp-textfield>
120
121
  <sp-action-button
@@ -122,10 +123,10 @@ function renderShorthandRow(shortProp, entry, style, commitFn, _deleteFn, inheri
122
123
  quiet
123
124
  @click=${(/** @type {any} */ e) => {
124
125
  e.stopPropagation();
125
- updateUi("styleShorthands", {
126
- ...getState().ui.styleShorthands,
126
+ activeTab.value.session.ui.styleShorthands = {
127
+ ...activeTab.value.session.ui.styleShorthands,
127
128
  [shortProp]: !isExpanded,
128
- });
129
+ };
129
130
  }}
130
131
  >
131
132
  ${isExpanded
@@ -162,12 +163,12 @@ function renderShorthandRow(shortProp, entry, style, commitFn, _deleteFn, inheri
162
163
  ? emptyVal
163
164
  : (style[l.name] ?? (expanded ? expanded[i] : emptyVal)),
164
165
  );
165
- let s = getState();
166
- for (const l of longhands) {
167
- if (style[l.name] !== undefined) s = commitFn(s, l.name, undefined);
168
- }
169
- s = commitFn(s, shortProp, compress(vals));
170
- update(s);
166
+ transactDoc(activeTab.value, (t) => {
167
+ for (const l of longhands) {
168
+ if (style[l.name] !== undefined) mutateFn(t, l.name, undefined);
169
+ }
170
+ mutateFn(t, shortProp, compress(vals));
171
+ });
171
172
  }}
172
173
  ></span>`
173
174
  : nothing}
@@ -186,12 +187,12 @@ function renderShorthandRow(shortProp, entry, style, commitFn, _deleteFn, inheri
186
187
  ? newVal || emptyVal
187
188
  : (style[l.name] ?? (expanded ? expanded[i] : emptyVal)),
188
189
  );
189
- let s = getState();
190
- for (const l of longhands) {
191
- if (style[l.name] !== undefined) s = commitFn(s, l.name, undefined);
192
- }
193
- s = commitFn(s, shortProp, compress(vals));
194
- update(s);
190
+ transactDoc(activeTab.value, (t) => {
191
+ for (const l of longhands) {
192
+ if (style[l.name] !== undefined) mutateFn(t, l.name, undefined);
193
+ }
194
+ mutateFn(t, shortProp, compress(vals));
195
+ });
195
196
  },
196
197
  { placeholder: !lVal && inherited[name] ? String(inherited[name]) : "" },
197
198
  )}
@@ -212,11 +213,12 @@ function renderShorthandRow(shortProp, entry, style, commitFn, _deleteFn, inheri
212
213
  * @param {any} activeSelector
213
214
  */
214
215
  function styleSidebarTemplate(node, activeMediaTab, activeSelector) {
215
- const S = getState();
216
+ const tab = activeTab.value;
217
+ const sel = /** @type {import("../state.js").JxPath} */ (tab.session.selection);
216
218
  const style = node.style || {};
217
- const { sizeBreakpoints } = parseMediaEntries(getEffectiveMedia(S.document.$media));
219
+ const { sizeBreakpoints } = parseMediaEntries(getEffectiveMedia(tab.doc.document.$media));
218
220
  const mediaNames = sizeBreakpoints.map((bp) => bp.name);
219
- const activeTab = activeMediaTab;
221
+ const mediaTab = activeMediaTab;
220
222
 
221
223
  // ── Media tabs template ──────────────────────────────────────────────────
222
224
  const mediaTabsT =
@@ -224,12 +226,12 @@ function styleSidebarTemplate(node, activeMediaTab, activeSelector) {
224
226
  ? html`
225
227
  <sp-tabs
226
228
  size="s"
227
- selected=${activeTab || "base"}
229
+ selected=${mediaTab || "base"}
228
230
  @change=${(/** @type {any} */ e) => {
229
231
  const val = e.target.selected;
230
232
  const newMedia = val === "base" ? null : val;
231
- if (newMedia !== S.ui.activeMedia) {
232
- updateUi("activeMedia", newMedia);
233
+ if (newMedia !== tab.session.ui.activeMedia) {
234
+ tab.session.ui.activeMedia = newMedia;
233
235
  }
234
236
  }}
235
237
  >
@@ -242,7 +244,7 @@ function styleSidebarTemplate(node, activeMediaTab, activeSelector) {
242
244
  : nothing;
243
245
 
244
246
  // ── Selector dropdown ──────────────────────────────────────────────────────
245
- const contextStyle = activeTab ? style[`@${activeTab}`] || {} : style;
247
+ const contextStyle = mediaTab ? style[`@${mediaTab}`] || {} : style;
246
248
  const existingSelectors = Object.keys(contextStyle).filter(isNestedSelector);
247
249
  const existingSet = new Set(existingSelectors);
248
250
  const commonSet = new Set(COMMON_SELECTORS);
@@ -281,7 +283,7 @@ function styleSidebarTemplate(node, activeMediaTab, activeSelector) {
281
283
  inp.remove();
282
284
  picker.style.display = "";
283
285
  if (accept && v && isNestedSelector(v)) {
284
- updateUi("activeSelector", v);
286
+ activeTab.value.session.ui.activeSelector = v;
285
287
  }
286
288
  };
287
289
  inp.addEventListener("keydown", (ev) => {
@@ -292,7 +294,7 @@ function styleSidebarTemplate(node, activeMediaTab, activeSelector) {
292
294
  return;
293
295
  }
294
296
  const newSelector = val === "__base__" ? null : val;
295
- updateUi("activeSelector", newSelector);
297
+ activeTab.value.session.ui.activeSelector = newSelector;
296
298
  }}
297
299
  >
298
300
  <sp-menu-item value="__base__">(base)</sp-menu-item>
@@ -328,14 +330,19 @@ function styleSidebarTemplate(node, activeMediaTab, activeSelector) {
328
330
  size="s"
329
331
  class="style-filter-input"
330
332
  placeholder="Filter properties…"
331
- .value=${live(S.ui.styleFilter || "")}
332
- @input=${(/** @type {any} */ e) => updateUi("styleFilter", e.target.value)}
333
+ .value=${live(tab.session.ui.styleFilter || "")}
334
+ @input=${(/** @type {any} */ e) => {
335
+ activeTab.value.session.ui.styleFilter = e.target.value;
336
+ }}
333
337
  ></sp-textfield>
334
338
  <sp-action-button
335
339
  size="xs"
336
340
  class="style-filter-toggle"
337
- ?selected=${S.ui.styleFilterActive}
338
- @click=${() => updateUi("styleFilterActive", !S.ui.styleFilterActive)}
341
+ ?selected=${tab.session.ui.styleFilterActive}
342
+ @click=${() => {
343
+ activeTab.value.session.ui.styleFilterActive =
344
+ !activeTab.value.session.ui.styleFilterActive;
345
+ }}
339
346
  >
340
347
  Active
341
348
  </sp-action-button>
@@ -345,40 +352,50 @@ function styleSidebarTemplate(node, activeMediaTab, activeSelector) {
345
352
  // ── Determine the active style object ──────────────────────────────────────
346
353
  /** @type {Record<string, any>} */
347
354
  let activeStyle;
348
- /** @type {any} */
355
+ /** @type {(prop: any, val: any) => void} */
349
356
  let commitStyle;
350
- if (activeSelector && activeTab && mediaNames.length > 0) {
351
- activeStyle = (style[`@${activeTab}`] || {})[activeSelector] || {};
352
- commitStyle = (/** @type {any} */ s, /** @type {any} */ prop, /** @type {any} */ val) =>
353
- updateMediaNestedStyle(s, S.selection, activeTab, activeSelector, prop, val);
357
+ /** @type {(t: any, prop: any, val: any) => void} */
358
+ let commitMutate;
359
+ if (activeSelector && mediaTab && mediaNames.length > 0) {
360
+ activeStyle = (style[`@${mediaTab}`] || {})[activeSelector] || {};
361
+ commitMutate = (/** @type {any} */ t, /** @type {any} */ prop, /** @type {any} */ val) =>
362
+ mutateUpdateMediaNestedStyle(t, sel, mediaTab, activeSelector, prop, val);
363
+ commitStyle = (/** @type {any} */ prop, /** @type {any} */ val) =>
364
+ transactDoc(activeTab.value, (t) => commitMutate(t, prop, val));
354
365
  } else if (activeSelector) {
355
366
  activeStyle = style[activeSelector] || {};
356
- commitStyle = (/** @type {any} */ s, /** @type {any} */ prop, /** @type {any} */ val) =>
357
- updateNestedStyle(s, S.selection, activeSelector, prop, val);
358
- } else if (activeTab !== null && mediaNames.length > 0) {
367
+ commitMutate = (/** @type {any} */ t, /** @type {any} */ prop, /** @type {any} */ val) =>
368
+ mutateUpdateNestedStyle(t, sel, activeSelector, prop, val);
369
+ commitStyle = (/** @type {any} */ prop, /** @type {any} */ val) =>
370
+ transactDoc(activeTab.value, (t) => commitMutate(t, prop, val));
371
+ } else if (mediaTab !== null && mediaNames.length > 0) {
359
372
  activeStyle = {};
360
- for (const [p, v] of Object.entries(style[`@${activeTab}`] || {})) {
373
+ for (const [p, v] of Object.entries(style[`@${mediaTab}`] || {})) {
361
374
  if (typeof v !== "object") activeStyle[p] = v;
362
375
  }
363
- commitStyle = (/** @type {any} */ s, /** @type {any} */ prop, /** @type {any} */ val) =>
364
- updateMediaStyle(s, S.selection, activeTab, prop, val);
376
+ commitMutate = (/** @type {any} */ t, /** @type {any} */ prop, /** @type {any} */ val) =>
377
+ mutateUpdateMediaStyle(t, sel, mediaTab, prop, val);
378
+ commitStyle = (/** @type {any} */ prop, /** @type {any} */ val) =>
379
+ transactDoc(activeTab.value, (t) => commitMutate(t, prop, val));
365
380
  } else {
366
381
  activeStyle = {};
367
382
  for (const [p, v] of Object.entries(style)) {
368
383
  if (typeof v !== "object") activeStyle[p] = v;
369
384
  }
370
- commitStyle = (/** @type {any} */ s, /** @type {any} */ prop, /** @type {any} */ val) =>
371
- updateStyle(s, S.selection, prop, val);
385
+ commitMutate = (/** @type {any} */ t, /** @type {any} */ prop, /** @type {any} */ val) =>
386
+ mutateUpdateStyle(t, sel, prop, val);
387
+ commitStyle = (/** @type {any} */ prop, /** @type {any} */ val) =>
388
+ transactDoc(activeTab.value, (t) => commitMutate(t, prop, val));
372
389
  }
373
390
 
374
391
  // ── Compute inherited style from higher breakpoints ──────────────────────
375
392
  /** @type {Record<string, any>} */
376
- const inheritedStyle = computeInheritedStyle(style, mediaNames, activeTab, activeSelector);
393
+ const inheritedStyle = computeInheritedStyle(style, mediaNames, mediaTab, activeSelector);
377
394
 
378
395
  // Auto-open sections that have properties
379
- const newSections = autoOpenSections({ style: activeStyle }, S.ui.styleSections);
380
- if (JSON.stringify(newSections) !== JSON.stringify(S.ui.styleSections)) {
381
- updateUi("styleSections", newSections);
396
+ const newSections = autoOpenSections({ style: activeStyle }, tab.session.ui.styleSections);
397
+ if (JSON.stringify(newSections) !== JSON.stringify(tab.session.ui.styleSections)) {
398
+ tab.session.ui.styleSections = newSections;
382
399
  }
383
400
 
384
401
  // Partition properties into sections
@@ -402,8 +419,8 @@ function styleSidebarTemplate(node, activeMediaTab, activeSelector) {
402
419
  }
403
420
 
404
421
  // ── Filter state ─────────────────────────────────────────────────────────
405
- const filterText = (S.ui.styleFilter || "").toLowerCase();
406
- const filterActive = S.ui.styleFilterActive;
422
+ const filterText = (tab.session.ui.styleFilter || "").toLowerCase();
423
+ const filterActive = tab.session.ui.styleFilterActive;
407
424
  const isFiltering = filterText.length > 0 || filterActive;
408
425
 
409
426
  // ── Section templates ────────────────────────────────────────────────────
@@ -449,7 +466,7 @@ function styleSidebarTemplate(node, activeMediaTab, activeSelector) {
449
466
  hasVal || longhands.some((/** @type {any} */ l) => activeStyle[l.name] !== undefined);
450
467
  if (!hasAny && !condMet) continue;
451
468
  rows.push(
452
- renderShorthandRow(prop, entry, activeStyle, commitStyle, () => {}, inheritedStyle),
469
+ renderShorthandRow(prop, entry, activeStyle, commitMutate, () => {}, inheritedStyle),
453
470
  );
454
471
  } else {
455
472
  const isWarning = hasVal && !condMet;
@@ -459,9 +476,8 @@ function styleSidebarTemplate(node, activeMediaTab, activeSelector) {
459
476
  entry,
460
477
  prop,
461
478
  val ?? "",
462
- (/** @type {any} */ newVal) =>
463
- update(commitStyle(getState(), prop, newVal || undefined)),
464
- () => update(commitStyle(getState(), prop, undefined)),
479
+ (/** @type {any} */ newVal) => commitStyle(prop, newVal || undefined),
480
+ () => commitStyle(prop, undefined),
465
481
  isWarning,
466
482
  sec.$layout === "grid",
467
483
  inheritedStyle[prop],
@@ -472,14 +488,17 @@ function styleSidebarTemplate(node, activeMediaTab, activeSelector) {
472
488
  }
473
489
 
474
490
  if (isFiltering && rows.length === 0) return nothing;
475
- const isOpen = isFiltering ? true : (S.ui.styleSections[sec.key] ?? false);
491
+ const isOpen = isFiltering ? true : (tab.session.ui.styleSections[sec.key] ?? false);
476
492
 
477
493
  return html`
478
494
  <sp-accordion-item
479
495
  label=${sec.label}
480
496
  .open=${isOpen}
481
497
  @sp-accordion-item-toggle=${(/** @type {any} */ e) => {
482
- updateUi("styleSections", { ...getState().ui.styleSections, [sec.key]: e.target.open });
498
+ activeTab.value.session.ui.styleSections = {
499
+ ...activeTab.value.session.ui.styleSections,
500
+ [sec.key]: e.target.open,
501
+ };
483
502
  }}
484
503
  >
485
504
  ${sectionActiveProps.length > 0
@@ -492,17 +511,17 @@ function styleSidebarTemplate(node, activeMediaTab, activeSelector) {
492
511
  @click=${(/** @type {any} */ e) => {
493
512
  e.stopPropagation();
494
513
  e.preventDefault();
495
- let s = getState();
496
- for (const { prop, entry } of sectionActiveProps) {
497
- if (activeStyle[prop] !== undefined) s = commitStyle(s, prop, undefined);
498
- if (inferInputType(entry) === "shorthand") {
499
- for (const l of getLonghands(prop)) {
500
- if (activeStyle[l.name] !== undefined)
501
- s = commitStyle(s, l.name, undefined);
514
+ transactDoc(activeTab.value, (t) => {
515
+ for (const { prop, entry } of sectionActiveProps) {
516
+ if (activeStyle[prop] !== undefined) commitMutate(t, prop, undefined);
517
+ if (inferInputType(entry) === "shorthand") {
518
+ for (const l of getLonghands(prop)) {
519
+ if (activeStyle[l.name] !== undefined)
520
+ commitMutate(t, l.name, undefined);
521
+ }
502
522
  }
503
523
  }
504
- }
505
- update(s);
524
+ });
506
525
  }}
507
526
  ></span>
508
527
  </span>
@@ -515,13 +534,16 @@ function styleSidebarTemplate(node, activeMediaTab, activeSelector) {
515
534
 
516
535
  // ── Custom section ─────────────────────────────────────────────────────────
517
536
  const cssInitialMap = getCssInitialMap();
518
- const customIsOpen = S.ui.styleSections.other ?? otherProps.length > 0;
537
+ const customIsOpen = tab.session.ui.styleSections.other ?? otherProps.length > 0;
519
538
  const customSectionT = html`
520
539
  <sp-accordion-item
521
540
  label="Custom"
522
541
  .open=${customIsOpen}
523
542
  @sp-accordion-item-toggle=${(/** @type {any} */ e) => {
524
- updateUi("styleSections", { ...getState().ui.styleSections, other: e.target.open });
543
+ activeTab.value.session.ui.styleSections = {
544
+ ...activeTab.value.session.ui.styleSections,
545
+ other: e.target.open,
546
+ };
525
547
  }}
526
548
  >
527
549
  <div>
@@ -535,9 +557,10 @@ function styleSidebarTemplate(node, activeMediaTab, activeSelector) {
535
557
  @change=${(/** @type {any} */ e) => {
536
558
  const newProp = e.target.value.trim();
537
559
  if (newProp && newProp !== prop) {
538
- let s = commitStyle(getState(), prop, undefined);
539
- s = commitStyle(s, newProp, String(activeStyle[prop]));
540
- update(s);
560
+ transactDoc(activeTab.value, (t) => {
561
+ commitMutate(t, prop, undefined);
562
+ commitMutate(t, newProp, String(activeStyle[prop]));
563
+ });
541
564
  }
542
565
  }}
543
566
  ></sp-textfield>
@@ -547,14 +570,10 @@ function styleSidebarTemplate(node, activeMediaTab, activeSelector) {
547
570
  .value=${live(String(activeStyle[prop]))}
548
571
  placeholder=${ifDefined(cssInitialMap.get(prop))}
549
572
  @input=${debouncedStyleCommit(`custom:${prop}`, 400, (/** @type {any} */ e) => {
550
- update(commitStyle(getState(), prop, e.target.value));
573
+ commitStyle(prop, e.target.value);
551
574
  })}
552
575
  ></sp-textfield>
553
- <sp-action-button
554
- size="xs"
555
- quiet
556
- @click=${() => update(commitStyle(getState(), prop, undefined))}
557
- >
576
+ <sp-action-button size="xs" quiet @click=${() => commitStyle(prop, undefined)}>
558
577
  <sp-icon-close slot="icon"></sp-icon-close>
559
578
  </sp-action-button>
560
579
  </div>
@@ -571,7 +590,7 @@ function styleSidebarTemplate(node, activeMediaTab, activeSelector) {
571
590
  const prop = e.target.value.trim();
572
591
  if (prop) {
573
592
  const initial = cssInitialMap.get(prop) || "";
574
- update(commitStyle(getState(), prop, initial || ""));
593
+ commitStyle(prop, initial || "");
575
594
  e.target.value = "";
576
595
  }
577
596
  }
@@ -599,19 +618,23 @@ function styleSidebarTemplate(node, activeMediaTab, activeSelector) {
599
618
  * @returns {import("lit-html").TemplateResult}
600
619
  */
601
620
  export function renderStylePanelTemplate(ctx) {
602
- const S = getState();
603
- if (ctx.getCanvasMode() === "settings" && S.ui.stylebookSelection) {
604
- const node = S.document;
621
+ const tab = activeTab.value;
622
+ if (!tab) return html`<div class="empty-state">No document loaded</div>`;
623
+ if (ctx.getCanvasMode() === "settings" && tab.session.ui.stylebookSelection) {
624
+ const node = tab.doc.document;
605
625
  if (!node) return html`<div class="empty-state">No document loaded</div>`;
606
626
  return html`
607
- <div class="stylebook-style-header">Styling: &lt;${S.ui.stylebookSelection}&gt;</div>
608
- ${styleSidebarTemplate(node, S.ui.activeMedia, S.ui.activeSelector)}
627
+ <div class="stylebook-style-header">
628
+ Styling: &lt;${tab.session.ui.stylebookSelection}&gt;
629
+ </div>
630
+ ${styleSidebarTemplate(node, tab.session.ui.activeMedia, tab.session.ui.activeSelector)}
609
631
  `;
610
632
  }
611
- if (!S.selection) return html`<div class="empty-state">Select an element to style</div>`;
612
- const node = getNodeAtPath(S.document, S.selection);
633
+ if (!tab.session.selection)
634
+ return html`<div class="empty-state">Select an element to style</div>`;
635
+ const node = getNodeAtPath(tab.doc.document, tab.session.selection);
613
636
  if (!node) return html`<div class="empty-state">Select an element to style</div>`;
614
- return styleSidebarTemplate(node, S.ui.activeMedia, S.ui.activeSelector);
637
+ return styleSidebarTemplate(node, tab.session.ui.activeMedia, tab.session.ui.activeSelector);
615
638
  }
616
639
 
617
640
  /** Single property input row (generic field row helper) */
@@ -9,8 +9,6 @@ import { styleMap } from "lit-html/directives/style-map.js";
9
9
 
10
10
  import {
11
11
  getState,
12
- update,
13
- updateStyle,
14
12
  updateSession,
15
13
  updateUi,
16
14
  canvasWrap,
@@ -18,8 +16,10 @@ import {
18
16
  elToPath,
19
17
  projectState,
20
18
  } from "../store.js";
19
+ import { activeTab } from "../workspace/workspace.js";
20
+ import { transactDoc, mutateUpdateStyle } from "../tabs/transact.js";
21
21
  import { view } from "../view.js";
22
- import { defineElement } from "@jxsuite/runtime";
22
+ import { defineElement, setSkipServerFunctions } from "@jxsuite/runtime";
23
23
  import { componentRegistry } from "../files/components.js";
24
24
  import { getEffectiveStyle, getEffectiveMedia } from "../site-context.js";
25
25
  import { parseMediaEntries, activeBreakpointsForWidth } from "../utils/canvas-media.js";
@@ -272,7 +272,7 @@ export function renderStylebookOverlays() {
272
272
  const selectedTag = S.ui.stylebookSelection;
273
273
 
274
274
  for (const panel of canvasPanels) {
275
- const hoverTag = panel._lastHoverTag;
275
+ const hoverTag = /** @type {any} */ (panel)._lastHoverTag;
276
276
  /**
277
277
  * @type {{
278
278
  * cls: string;
@@ -351,6 +351,7 @@ export function buildStylebookElement(entry, rootStyle, activeBreakpoints, paren
351
351
  }
352
352
  }
353
353
  if (activeBreakpoints) {
354
+ // Check media overrides nested inside the tag style (selector wraps media)
354
355
  for (const [key, val] of Object.entries(tagStyle)) {
355
356
  if (!key.startsWith("@") || typeof val !== "object") continue;
356
357
  const mediaName = key.slice(1);
@@ -367,6 +368,27 @@ export function buildStylebookElement(entry, rootStyle, activeBreakpoints, paren
367
368
  }
368
369
  }
369
370
  }
371
+ // Check top-level @media keys for tag-specific overrides (media wraps selector)
372
+ if (activeBreakpoints) {
373
+ const selector = compoundSelector || `& ${entry.tag}`;
374
+ for (const [key, val] of Object.entries(rootStyle)) {
375
+ if (!key.startsWith("@") || typeof val !== "object") continue;
376
+ const mediaName = key.slice(1);
377
+ if (mediaName === "--") continue;
378
+ if (activeBreakpoints.has(mediaName)) {
379
+ const mediaTagStyle = /** @type {any} */ (val)[selector];
380
+ if (mediaTagStyle && typeof mediaTagStyle === "object") {
381
+ for (const [prop, v] of Object.entries(mediaTagStyle)) {
382
+ if (typeof v === "string" || typeof v === "number") {
383
+ try {
384
+ /** @type {any} */ (el.style)[prop] = v;
385
+ } catch {}
386
+ }
387
+ }
388
+ }
389
+ }
390
+ }
391
+ }
370
392
  if (entry.children) {
371
393
  for (const child of entry.children) {
372
394
  el.appendChild(buildStylebookElement(child, rootStyle, activeBreakpoints, entry.tag));
@@ -382,6 +404,7 @@ export function buildStylebookElement(entry, rootStyle, activeBreakpoints, paren
382
404
  * @returns {Promise<HTMLElement>}
383
405
  */
384
406
  export async function renderComponentPreview(comp) {
407
+ setSkipServerFunctions(true);
385
408
  try {
386
409
  if (comp.source === "npm") {
387
410
  if (!customElements.get(comp.tagName)) {
@@ -416,7 +439,13 @@ export async function renderComponentPreview(comp) {
416
439
  */
417
440
  function hasTagStyle(rootStyle, tag) {
418
441
  const s = rootStyle[`& ${tag}`];
419
- return s && typeof s === "object" && Object.keys(s).length > 0;
442
+ if (s && typeof s === "object" && Object.keys(s).length > 0) return true;
443
+ const selector = `& ${tag}`;
444
+ for (const [key, val] of Object.entries(rootStyle)) {
445
+ if (!key.startsWith("@") || typeof val !== "object") continue;
446
+ if (/** @type {any} */ (val)[selector]) return true;
447
+ }
448
+ return false;
420
449
  }
421
450
 
422
451
  /**
@@ -662,8 +691,9 @@ function renderVarRow(catKey, catMeta, varName, varVal, isNew) {
662
691
  const swatch = /** @type {any} */ (row.querySelector(".sb-var-swatch"));
663
692
  if (swatch) swatch.style.backgroundColor = colorPicker.value;
664
693
  if (!isNew && varName) {
665
- const S = getState();
666
- update(updateStyle(S, [], varName, colorPicker.value));
694
+ transactDoc(activeTab.value, (t) =>
695
+ mutateUpdateStyle(t, [], varName, colorPicker.value),
696
+ );
667
697
  }
668
698
  }}
669
699
  />
@@ -722,8 +752,7 @@ function renderVarRow(catKey, catMeta, varName, varVal, isNew) {
722
752
  const swatch = /** @type {any} */ (row.querySelector(".sb-var-swatch"));
723
753
  if (swatch) swatch.style.backgroundColor = v;
724
754
  if (!isNew && varName) {
725
- const S = getState();
726
- update(updateStyle(S, [], varName, v));
755
+ transactDoc(activeTab.value, (t) => mutateUpdateStyle(t, [], varName, v));
727
756
  }
728
757
  }, 400);
729
758
  }}
@@ -736,8 +765,7 @@ function renderVarRow(catKey, catMeta, varName, varVal, isNew) {
736
765
  const bar = /** @type {any} */ (row.querySelector(".sb-var-size-bar"));
737
766
  if (bar) bar.style.width = newVal;
738
767
  if (!isNew && varName) {
739
- const S = getState();
740
- update(updateStyle(S, [], varName, newVal));
768
+ transactDoc(activeTab.value, (t) => mutateUpdateStyle(t, [], varName, newVal));
741
769
  }
742
770
  },
743
771
  });
@@ -769,8 +797,7 @@ function renderVarRow(catKey, catMeta, varName, varVal, isNew) {
769
797
  const v = textFieldEl.value;
770
798
  const fontPrev = /** @type {any} */ (row.querySelector(".sb-var-font-preview"));
771
799
  if (fontPrev) fontPrev.style.fontFamily = v;
772
- const S = getState();
773
- update(updateStyle(S, [], varName, v));
800
+ transactDoc(activeTab.value, (t) => mutateUpdateStyle(t, [], varName, v));
774
801
  }, 400);
775
802
  }}
776
803
  ></sp-textfield>
@@ -795,8 +822,7 @@ function renderVarRow(catKey, catMeta, varName, varVal, isNew) {
795
822
  const val = getValueFn();
796
823
  const generatedVar = friendlyNameToVar(name, catMeta.prefix);
797
824
  if (!generatedVar || !val) return;
798
- const S = getState();
799
- update(updateStyle(S, [], generatedVar, val));
825
+ transactDoc(activeTab.value, (t) => mutateUpdateStyle(t, [], generatedVar, val));
800
826
  }}
801
827
  >Add</sp-action-button
802
828
  >
@@ -829,8 +855,7 @@ function renderVarRow(catKey, catMeta, varName, varVal, isNew) {
829
855
  class="sb-var-del"
830
856
  style="pointer-events:auto"
831
857
  @click=${() => {
832
- const S = getState();
833
- update(updateStyle(S, [], varName, undefined));
858
+ transactDoc(activeTab.value, (t) => mutateUpdateStyle(t, [], varName, undefined));
834
859
  }}
835
860
  >
836
861
  <sp-icon-delete slot="icon"></sp-icon-delete>