@jxsuite/studio 0.17.0 → 0.18.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.
@@ -1,11 +1,10 @@
1
1
  /**
2
- * Stylebook panel — renders the Settings mode canvas (element catalog, design variables,
3
- * definitions, content types). Extracted from studio.js Phase 4e.
2
+ * Stylebook panel — renders the Stylebook mode canvas (element catalog with per-file style
3
+ * defaults). Extracted from studio.js Phase 4e.
4
4
  */
5
5
 
6
6
  import { html, render as litRender, nothing } from "lit-html";
7
7
  import { ref } from "lit-html/directives/ref.js";
8
- import { styleMap } from "lit-html/directives/style-map.js";
9
8
 
10
9
  import {
11
10
  updateSession,
@@ -16,16 +15,12 @@ import {
16
15
  projectState,
17
16
  } from "../store.js";
18
17
  import { activeTab } from "../workspace/workspace.js";
19
- import { transactDoc, mutateUpdateStyle } from "../tabs/transact.js";
20
18
  import { view } from "../view.js";
21
19
  import { defineElement, setSkipServerFunctions } from "@jxsuite/runtime";
22
20
  import { componentRegistry } from "../files/components.js";
23
21
  import { getEffectiveStyle, getEffectiveMedia } from "../site-context.js";
24
22
  import { parseMediaEntries, activeBreakpointsForWidth } from "../utils/canvas-media.js";
25
23
  import { mediaDisplayName } from "./shared.js";
26
- import { friendlyNameToVar, varDisplayName } from "../utils/studio-utils.js";
27
- import { renderDefsEditor } from "../settings/defs-editor.js";
28
- import { renderContentTypesEditor } from "../settings/content-types-editor.js";
29
24
  import stylebookMeta from "../../data/stylebook-meta.json";
30
25
 
31
26
  export { stylebookMeta };
@@ -49,48 +44,7 @@ let _ctx = null;
49
44
  export function renderStylebookMode(ctx) {
50
45
  _ctx = ctx;
51
46
 
52
- const settingsTab = activeTab.value?.session.ui.settingsTab || "stylebook";
53
-
54
- const settingsChromeBarTpl = html`
55
- <div
56
- class="sb-chrome settings-top-chrome"
57
- style="position:absolute;top:0;left:0;right:0;z-index:16;background:var(--bg-panel);border-bottom:1px solid var(--border)"
58
- >
59
- <sp-tabs
60
- size="s"
61
- selected=${settingsTab}
62
- @change=${(/** @type {any} */ e) => {
63
- updateUi("settingsTab", e.target.selected);
64
- }}
65
- >
66
- <sp-tab label="Stylebook" value="stylebook"></sp-tab>
67
- <sp-tab label="Definitions" value="definitions"></sp-tab>
68
- <sp-tab label="Content Types" value="contentTypes"></sp-tab>
69
- </sp-tabs>
70
- </div>
71
- `;
72
-
73
- if (settingsTab === "definitions" || settingsTab === "contentTypes") {
74
- /** @type {any} */ (canvasWrap).style.overflow = "hidden";
75
-
76
- litRender(
77
- html`${settingsChromeBarTpl}
78
- <div
79
- class="settings-editor-container"
80
- style="position:absolute;inset:40px 0 0 0;overflow:auto"
81
- ></div>`,
82
- /** @type {any} */ (canvasWrap),
83
- );
84
-
85
- const container = /** @type {HTMLElement} */ (
86
- canvasWrap.querySelector(".settings-editor-container")
87
- );
88
- if (settingsTab === "definitions") renderDefsEditor(container);
89
- else renderContentTypesEditor(container);
90
- return;
91
- }
92
-
93
- // Stylebook tab — element catalog / variables
47
+ // Stylebook mode element catalog only
94
48
  view.stylebookElToTag = new WeakMap();
95
49
  const tab = activeTab.value;
96
50
  const rootStyle = getEffectiveStyle(tab?.doc.document?.style);
@@ -102,10 +56,6 @@ export function renderStylebookMode(ctx) {
102
56
  );
103
57
  const hasMedia = sizeBreakpoints.length > 0;
104
58
 
105
- const onTabClick = (/** @type {string} */ t) => {
106
- updateUi("stylebookTab", t);
107
- };
108
-
109
59
  const onFilterInput = (/** @type {any} */ e) => {
110
60
  updateUi("stylebookFilter", e.target.value);
111
61
  };
@@ -115,42 +65,25 @@ export function renderStylebookMode(ctx) {
115
65
  };
116
66
 
117
67
  const chromeBarTpl = html`
118
- ${settingsChromeBarTpl}
119
68
  <div
120
69
  class="sb-chrome"
121
- style="position:absolute;top:36px;left:0;right:0;z-index:15;background:var(--bg-panel);border-bottom:1px solid var(--border)"
70
+ style="position:absolute;top:0;left:0;right:0;z-index:15;background:var(--bg-panel);border-bottom:1px solid var(--border)"
122
71
  >
123
- <sp-tabs
124
- size="s"
125
- selected=${tab?.session.ui.stylebookTab || "elements"}
126
- @change=${(/** @type {any} */ e) => {
127
- onTabClick(e.target.selected);
128
- }}
129
- >
130
- ${["elements", "variables"].map(
131
- (t) => html`
132
- <sp-tab label=${t.charAt(0).toUpperCase() + t.slice(1)} value=${t}></sp-tab>
133
- `,
134
- )}
135
- </sp-tabs>
136
- ${tab?.session.ui.stylebookTab === "elements"
137
- ? html`
138
- <input
139
- class="field-input"
140
- style="flex:1;max-width:200px;margin-left:8px"
141
- placeholder="Filter…"
142
- .value=${tab?.session.ui.stylebookFilter}
143
- @input=${onFilterInput}
144
- />
145
- <button
146
- class="tb-toggle${tab?.session.ui.stylebookCustomizedOnly ? " active" : ""}"
147
- style="margin-left:4px"
148
- @click=${onCustomizedToggle}
149
- >
150
- Customized
151
- </button>
152
- `
153
- : nothing}
72
+ <div style="display:flex;align-items:center;padding:4px 8px;gap:4px">
73
+ <input
74
+ class="field-input"
75
+ style="flex:1;max-width:200px"
76
+ placeholder="Filter…"
77
+ .value=${tab?.session.ui.stylebookFilter}
78
+ @input=${onFilterInput}
79
+ />
80
+ <button
81
+ class="tb-toggle${tab?.session.ui.stylebookCustomizedOnly ? " active" : ""}"
82
+ @click=${onCustomizedToggle}
83
+ >
84
+ Customized
85
+ </button>
86
+ </div>
154
87
  </div>
155
88
  `;
156
89
 
@@ -177,22 +110,17 @@ export function renderStylebookMode(ctx) {
177
110
 
178
111
  const renderIntoPanel = (/** @type {any} */ panel, /** @type {any} */ activeBreakpoints) => {
179
112
  panel.canvas.classList.add("sb-canvas");
180
- if (tab?.session.ui.stylebookTab === "elements") {
181
- renderStylebookElementsIntoCanvas(
182
- panel.canvas,
183
- rootStyle,
184
- filter,
185
- customizedOnly,
186
- activeBreakpoints,
187
- );
188
- for (const child of panel.canvas.querySelectorAll("*")) {
189
- child.style.pointerEvents = "none";
190
- }
191
- registerStylebookPanelEvents(panel);
192
- } else {
193
- renderStylebookVarsIntoCanvas(panel.canvas, rootStyle);
194
- panel.overlayClk.style.pointerEvents = "none";
113
+ renderStylebookElementsIntoCanvas(
114
+ panel.canvas,
115
+ rootStyle,
116
+ filter,
117
+ customizedOnly,
118
+ activeBreakpoints,
119
+ );
120
+ for (const child of panel.canvas.querySelectorAll("*")) {
121
+ child.style.pointerEvents = "none";
195
122
  }
123
+ registerStylebookPanelEvents(panel);
196
124
  };
197
125
 
198
126
  /** @type {{ tpl: any; panel: any; activeSet: any }[]} */
@@ -221,7 +149,7 @@ export function renderStylebookMode(ctx) {
221
149
  ${chromeBarTpl}
222
150
  <div
223
151
  class="panzoom-wrap"
224
- style="transform-origin:0 0;padding-top:72px"
152
+ style="transform-origin:0 0;padding-top:40px"
225
153
  ${ref((el) => {
226
154
  if (el) view.panzoomWrap = /** @type {HTMLDivElement} */ (el);
227
155
  })}
@@ -584,433 +512,6 @@ export function renderStylebookElementsIntoCanvas(
584
512
  }
585
513
  }
586
514
 
587
- /**
588
- * Render variables into the canvas (card-based layout matching Elements tab)
589
- *
590
- * @param {any} canvasEl
591
- * @param {any} rootStyle
592
- */
593
- function renderStylebookVarsIntoCanvas(canvasEl, rootStyle) {
594
- const varCats = stylebookMeta.$variables;
595
-
596
- /** @type {Record<string, any>} */
597
- const groups = {};
598
- for (const key of Object.keys(varCats)) groups[key] = [];
599
- for (const [k, v] of Object.entries(rootStyle)) {
600
- if (!k.startsWith("--")) continue;
601
- if (typeof v !== "string" && typeof v !== "number") continue;
602
- if (k.startsWith("--color")) groups.color.push([k, v]);
603
- else if (k.startsWith("--font")) groups.font.push([k, v]);
604
- else if (k.startsWith("--size") || k.startsWith("--spacing") || k.startsWith("--radius"))
605
- groups.size.push([k, v]);
606
- else groups.other.push([k, v]);
607
- }
608
-
609
- /** @type {Map<string, HTMLElement | null>} */
610
- const bodyRefs = new Map();
611
-
612
- const sectionTemplates = Object.entries(varCats).map(([catKey, catMeta]) => {
613
- const vars = groups[catKey];
614
-
615
- const onAdd = () => {
616
- const bodyEl = bodyRefs.get(catKey);
617
- if (!bodyEl) return;
618
- const addBtn = bodyEl.querySelector(".sb-var-add-btn");
619
- const row = renderVarRow(catKey, /** @type {any} */ (catMeta), null, "", true);
620
- bodyEl.insertBefore(row, addBtn);
621
- if (addBtn) /** @type {any} */ (addBtn).style.display = "none";
622
- const nameField = /** @type {any} */ (row.querySelector("sp-textfield"));
623
- if (nameField) requestAnimationFrame(() => nameField.focus());
624
- };
625
-
626
- return html`
627
- <div class="sb-section">
628
- <div class="sb-label">${/** @type {any} */ (catMeta).label}</div>
629
- <div
630
- class="sb-body"
631
- ${ref((el) => {
632
- if (el) bodyRefs.set(catKey, /** @type {HTMLElement} */ (el));
633
- })}
634
- >
635
- ${vars.length > 0
636
- ? vars.map((/** @type {[string, any]} */ [varName, varVal]) =>
637
- renderVarRow(catKey, /** @type {any} */ (catMeta), varName, String(varVal), false),
638
- )
639
- : html`<div class="sb-var-empty">
640
- No ${/** @type {any} */ (catMeta).label.toLowerCase()} variables yet.
641
- </div>`}
642
- <button class="sb-var-add-btn" @click=${onAdd}>
643
- <span class="sb-var-add-icon">+</span> Add ${/** @type {any} */ (catMeta).label}
644
- </button>
645
- </div>
646
- </div>
647
- `;
648
- });
649
-
650
- litRender(html`${sectionTemplates}`, canvasEl);
651
- }
652
-
653
- /**
654
- * Render a single variable row — used for both existing and add-new.
655
- *
656
- * @param {string} catKey
657
- * @param {any} catMeta
658
- * @param {string | null} varName
659
- * @param {string} varVal
660
- * @param {boolean} isNew
661
- */
662
- function renderVarRow(catKey, catMeta, varName, varVal, isNew) {
663
- const row = document.createElement("div");
664
- row.className = isNew ? "sb-var-row is-new" : "sb-var-row";
665
-
666
- /** @type {any} */
667
- let colorPicker = null;
668
- /** @type {any} */
669
- let nameField = null;
670
- /** @type {any} */
671
- let getValueFn;
672
- /** @type {any} */
673
- let hexField = null;
674
-
675
- const swatchTpl =
676
- catKey === "color"
677
- ? html`
678
- <div
679
- class="sb-var-swatch"
680
- style=${styleMap({ backgroundColor: varVal || "var(--accent)" })}
681
- >
682
- <input
683
- type="color"
684
- .value=${varVal && varVal.startsWith("#") ? varVal : "#007acc"}
685
- ${ref((el) => {
686
- if (el) colorPicker = el;
687
- })}
688
- @input=${() => {
689
- if (!colorPicker || !hexField) return;
690
- hexField.value = colorPicker.value;
691
- const swatch = /** @type {any} */ (row.querySelector(".sb-var-swatch"));
692
- if (swatch) swatch.style.backgroundColor = colorPicker.value;
693
- if (!isNew && varName) {
694
- transactDoc(activeTab.value, (t) =>
695
- mutateUpdateStyle(t, [], varName, colorPicker.value),
696
- );
697
- }
698
- }}
699
- />
700
- </div>
701
- `
702
- : nothing;
703
-
704
- const namePlaceholder =
705
- catKey === "color"
706
- ? "Primary Blue"
707
- : catKey === "font"
708
- ? "Body Serif"
709
- : catKey === "size"
710
- ? "Spacing Large"
711
- : "Border Radius";
712
-
713
- const nameColTpl = isNew
714
- ? html`
715
- <div class="sb-var-col-name">
716
- <div class="sb-var-col-label">Name</div>
717
- <sp-textfield
718
- size="s"
719
- placeholder=${namePlaceholder}
720
- style="pointer-events:auto"
721
- ${ref((el) => {
722
- if (el) nameField = el;
723
- })}
724
- ></sp-textfield>
725
- </div>
726
- `
727
- : nothing;
728
-
729
- /** @type {any} */
730
- let valueContent;
731
-
732
- if (catKey === "color") {
733
- /** @type {any} */
734
- let debounce;
735
- valueContent = html`
736
- <sp-textfield
737
- size="s"
738
- .value=${varVal || "#007acc"}
739
- placeholder="#007acc"
740
- style="pointer-events:auto"
741
- ${ref((el) => {
742
- if (el) hexField = el;
743
- })}
744
- @input=${() => {
745
- clearTimeout(debounce);
746
- debounce = setTimeout(() => {
747
- if (!hexField) return;
748
- const v = hexField.value;
749
- try {
750
- if (colorPicker) colorPicker.value = v.startsWith("#") ? v : colorPicker.value;
751
- } catch {}
752
- const swatch = /** @type {any} */ (row.querySelector(".sb-var-swatch"));
753
- if (swatch) swatch.style.backgroundColor = v;
754
- if (!isNew && varName) {
755
- transactDoc(activeTab.value, (t) => mutateUpdateStyle(t, [], varName, v));
756
- }
757
- }, 400);
758
- }}
759
- ></sp-textfield>
760
- `;
761
- getValueFn = () => hexField?.value?.trim() || "";
762
- } else if (catKey === "size") {
763
- const ui = createUnitInput(varVal || "16px", {
764
- onChange: (/** @type {any} */ newVal) => {
765
- const bar = /** @type {any} */ (row.querySelector(".sb-var-size-bar"));
766
- if (bar) bar.style.width = newVal;
767
- if (!isNew && varName) {
768
- transactDoc(activeTab.value, (t) => mutateUpdateStyle(t, [], varName, newVal));
769
- }
770
- },
771
- });
772
- if (isNew) ui.textfield.value = "";
773
- valueContent = html`<div
774
- ${ref((el) => {
775
- if (el && !el.firstChild) el.appendChild(ui.wrap);
776
- })}
777
- ></div>`;
778
- getValueFn = () => ui.getValue();
779
- } else {
780
- /** @type {any} */
781
- let textFieldEl = null;
782
- /** @type {any} */
783
- let debounce;
784
- valueContent = html`
785
- <sp-textfield
786
- size="s"
787
- .value=${varVal}
788
- placeholder=${catMeta.placeholder}
789
- style="pointer-events:auto"
790
- ${ref((el) => {
791
- if (el) textFieldEl = el;
792
- })}
793
- @input=${() => {
794
- if (!textFieldEl || isNew || !varName) return;
795
- clearTimeout(debounce);
796
- debounce = setTimeout(() => {
797
- const v = textFieldEl.value;
798
- const fontPrev = /** @type {any} */ (row.querySelector(".sb-var-font-preview"));
799
- if (fontPrev) fontPrev.style.fontFamily = v;
800
- transactDoc(activeTab.value, (t) => mutateUpdateStyle(t, [], varName, v));
801
- }, 400);
802
- }}
803
- ></sp-textfield>
804
- `;
805
- getValueFn = () => textFieldEl?.value?.trim() || "";
806
- }
807
-
808
- const valColTpl = html`
809
- <div class="sb-var-col-value">
810
- ${isNew ? html`<div class="sb-var-col-label">Value</div>` : nothing} ${valueContent}
811
- </div>
812
- `;
813
-
814
- const actionsTpl = isNew
815
- ? html`
816
- <div class="sb-var-add-actions">
817
- <sp-action-button
818
- size="s"
819
- style="pointer-events:auto"
820
- @click=${() => {
821
- const name = (nameField?.value || "").trim();
822
- const val = getValueFn();
823
- const generatedVar = friendlyNameToVar(name, catMeta.prefix);
824
- if (!generatedVar || !val) return;
825
- transactDoc(activeTab.value, (t) => mutateUpdateStyle(t, [], generatedVar, val));
826
- }}
827
- >Add</sp-action-button
828
- >
829
- <sp-action-button
830
- size="s"
831
- quiet
832
- style="pointer-events:auto"
833
- @click=${() => {
834
- const body = row.parentElement;
835
- row.remove();
836
- const addBtn = /** @type {any} */ (body?.querySelector(".sb-var-add-btn"));
837
- if (addBtn) addBtn.style.display = "";
838
- }}
839
- >
840
- <sp-icon-close slot="icon"></sp-icon-close>
841
- </sp-action-button>
842
- </div>
843
- `
844
- : nothing;
845
-
846
- const headerTpl =
847
- !isNew && varName
848
- ? html`
849
- <div class="sb-var-row-header">
850
- <span class="sb-var-row-title">${varDisplayName(varName, catMeta.prefix)}</span>
851
- <span class="sb-var-row-ref">${varName}</span>
852
- <sp-action-button
853
- size="s"
854
- quiet
855
- class="sb-var-del"
856
- style="pointer-events:auto"
857
- @click=${() => {
858
- transactDoc(activeTab.value, (t) => mutateUpdateStyle(t, [], varName, undefined));
859
- }}
860
- >
861
- <sp-icon-delete slot="icon"></sp-icon-delete>
862
- </sp-action-button>
863
- </div>
864
- `
865
- : nothing;
866
-
867
- const addPreviewTpl = isNew
868
- ? html`
869
- <div
870
- class="sb-var-add-preview"
871
- ${ref((el) => {
872
- if (!el || !nameField) return;
873
- nameField.addEventListener("input", () => {
874
- el.textContent = friendlyNameToVar(nameField.value || "", catMeta.prefix);
875
- });
876
- })}
877
- ></div>
878
- `
879
- : nothing;
880
-
881
- const typePrevTpl =
882
- catKey === "font" && varVal
883
- ? html`
884
- <div class="sb-var-preview">
885
- <div class="sb-var-font-preview" style=${styleMap({ fontFamily: varVal })}>
886
- The quick brown fox jumps over the lazy dog
887
- </div>
888
- </div>
889
- `
890
- : catKey === "size" && varVal
891
- ? html`
892
- <div class="sb-var-preview">
893
- <div class="sb-var-size-track">
894
- <div class="sb-var-size-bar" style=${styleMap({ width: varVal })}></div>
895
- </div>
896
- </div>
897
- `
898
- : nothing;
899
-
900
- litRender(
901
- html`
902
- ${headerTpl}
903
- <div class="sb-var-input-row">${swatchTpl} ${nameColTpl} ${valColTpl} ${actionsTpl}</div>
904
- ${addPreviewTpl} ${typePrevTpl}
905
- `,
906
- row,
907
- );
908
-
909
- return row;
910
- }
911
-
912
- /**
913
- * Creates a combined textfield + quiet sp-picker for CSS values with units.
914
- *
915
- * @param {any} initialValue
916
- * @param {any} [options]
917
- */
918
- function createUnitInput(initialValue, { onChange, size = "s" } = {}) {
919
- const match = String(initialValue).match(
920
- /^(-?[\d.]+)\s*(px|em|rem|vw|vh|%|ch|ex|vmin|vmax|pt|cm|mm|in)?$/,
921
- );
922
- let numVal = match ? match[1] : initialValue;
923
- let unitVal = match ? match[2] || "px" : "";
924
- const isNumeric = !!match;
925
-
926
- const wrap = document.createElement("div");
927
- wrap.className = "sb-unit-input";
928
- wrap.style.pointerEvents = "auto";
929
-
930
- /** @type {any} */
931
- let textfield = null;
932
- /** @type {any} */
933
- let picker = null;
934
-
935
- const units = [
936
- { value: "px", label: "px" },
937
- { value: "rem", label: "rem" },
938
- { value: "em", label: "em" },
939
- { value: "%", label: "%" },
940
- { value: "vw", label: "vw" },
941
- { value: "vh", label: "vh" },
942
- { value: "ch", label: "ch" },
943
- { value: "pt", label: "pt" },
944
- { divider: true },
945
- { value: "auto", label: "auto" },
946
- { value: "fit-content", label: "fit-content" },
947
- ];
948
-
949
- /** @type {any} */
950
- let debounce;
951
-
952
- function getValue() {
953
- const num = textfield?.value;
954
- const unit = picker?.value;
955
- if (unit === "auto" || unit === "fit-content") return unit;
956
- return num ? `${num}${unit}` : "";
957
- }
958
-
959
- litRender(
960
- html`
961
- <sp-textfield
962
- .value=${numVal}
963
- size=${size}
964
- ${ref((el) => {
965
- if (el) textfield = el;
966
- })}
967
- @input=${() => {
968
- clearTimeout(debounce);
969
- const raw = textfield?.value?.trim();
970
- const looksNumeric = /^-?[\d.]+$/.test(raw || "");
971
- if (picker) picker.style.display = looksNumeric ? "" : "none";
972
- debounce = setTimeout(() => {
973
- if (onChange) onChange(looksNumeric ? `${raw}${picker?.value}` : raw);
974
- }, 400);
975
- }}
976
- ></sp-textfield>
977
- <sp-picker
978
- quiet
979
- size=${size}
980
- style=${styleMap({ display: isNumeric ? "" : "none" })}
981
- ${ref((el) => {
982
- if (el) {
983
- picker = el;
984
- requestAnimationFrame(() => {
985
- /** @type {any} */ (el).value = unitVal || "px";
986
- });
987
- }
988
- })}
989
- @change=${() => {
990
- const unit = picker?.value;
991
- if (unit === "auto" || unit === "fit-content") {
992
- if (textfield) textfield.value = unit;
993
- if (picker) picker.style.display = "none";
994
- if (onChange) onChange(unit);
995
- } else {
996
- unitVal = unit;
997
- if (onChange) onChange(getValue());
998
- }
999
- }}
1000
- >
1001
- ${units.map((u) =>
1002
- u.divider
1003
- ? html`<sp-menu-divider></sp-menu-divider>`
1004
- : html`<sp-menu-item value=${u.value}>${u.label}</sp-menu-item>`,
1005
- )}
1006
- </sp-picker>
1007
- `,
1008
- wrap,
1009
- );
1010
-
1011
- return { wrap, textfield, picker, getValue };
1012
- }
1013
-
1014
515
  /**
1015
516
  * Click handler for stylebook canvas — selects elements via elToPath/view.stylebookElToTag mapping
1016
517
  *
@@ -194,9 +194,12 @@ function toolbarTemplate() {
194
194
  { key: "design", label: "Design", iconTag: "sp-icon-artboard" },
195
195
  { key: "preview", label: "Preview", iconTag: "sp-icon-preview" },
196
196
  { key: "source", label: "Code", iconTag: "sp-icon-code" },
197
- { key: "settings", label: "Settings", iconTag: "sp-icon-gears" },
197
+ { key: "stylebook", label: "Stylebook", iconTag: "sp-icon-brush" },
198
198
  ];
199
199
 
200
+ const isProjectFile = S.documentPath === "project.json";
201
+ const allowedModes = isProjectFile ? new Set(["stylebook", "source"]) : null;
202
+
200
203
  const modeSwitcherTpl = html`
201
204
  <sp-action-group selects="single" size="s" compact>
202
205
  ${modes.map(
@@ -204,8 +207,10 @@ function toolbarTemplate() {
204
207
  <sp-action-button
205
208
  size="s"
206
209
  ?selected=${canvasMode === m.key}
210
+ ?disabled=${allowedModes && !allowedModes.has(m.key)}
207
211
  @click=${() => {
208
212
  if (canvasMode === m.key) return;
213
+ if (allowedModes && !allowedModes.has(m.key)) return;
209
214
  if (S.ui.editingFunction) {
210
215
  if (view.functionEditor) {
211
216
  view.functionEditor.dispose();
@@ -217,7 +222,7 @@ function toolbarTemplate() {
217
222
  view.panY = 0;
218
223
  /** @type {Record<string, any>} */
219
224
  const uiPatch = { editingFunction: null };
220
- if (m.key === "settings") uiPatch.rightTab = "style";
225
+ if (m.key === "stylebook") uiPatch.rightTab = "style";
221
226
  if (m.key === "manage") view.leftTab = "files";
222
227
  updateSession({ ui: uiPatch });
223
228
  _ctx.renderCanvas();