@stacksjs/stx 0.2.119 → 0.2.121

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 (62) hide show
  1. package/dist/builtins/attrs.d.ts +16 -0
  2. package/dist/builtins/attrs.js +16 -0
  3. package/dist/builtins/safe-image.js +2 -9
  4. package/dist/builtins/stx-image.js +2 -9
  5. package/dist/builtins/stx-link.js +2 -9
  6. package/dist/cli.js +1869 -863
  7. package/dist/client-script-bundler.d.ts +9 -7
  8. package/dist/client-script-bundler.js +43 -4
  9. package/dist/client-script.d.ts +24 -0
  10. package/dist/client-script.js +59 -5
  11. package/dist/component-library.js +30 -12
  12. package/dist/component-registry.d.ts +1 -0
  13. package/dist/component-renderer.js +13 -5
  14. package/dist/composables/index.d.ts +1 -0
  15. package/dist/composables/index.js +1 -0
  16. package/dist/composables/use-scroll-lock.d.ts +13 -0
  17. package/dist/composables/use-scroll-lock.js +55 -0
  18. package/dist/composition-api.d.ts +1 -1
  19. package/dist/composition-api.js +13 -4
  20. package/dist/craft-bridge.d.ts +80 -1
  21. package/dist/craft-bridge.js +80 -1
  22. package/dist/craft-components.d.ts +77 -3
  23. package/dist/craft-components.js +138 -9
  24. package/dist/craft.js +12708 -118
  25. package/dist/dev-server/crosswind.d.ts +15 -1
  26. package/dist/dev-server/crosswind.js +12 -3
  27. package/dist/dev-server/serve-app.js +3 -3
  28. package/dist/document-shell.d.ts +17 -0
  29. package/dist/document-shell.js +72 -2
  30. package/dist/expressions.js +14 -7
  31. package/dist/head.d.ts +8 -0
  32. package/dist/head.js +11 -8
  33. package/dist/html-masking.d.ts +12 -0
  34. package/dist/html-masking.js +50 -15
  35. package/dist/index.d.ts +9 -1
  36. package/dist/index.js +9 -1
  37. package/dist/native-build.d.ts +6 -0
  38. package/dist/native-build.js +77 -60
  39. package/dist/process.js +11 -3
  40. package/dist/pwa.js +1232 -508
  41. package/dist/render.js +6 -1
  42. package/dist/runtime-globals.d.ts +7 -4
  43. package/dist/runtime-globals.js +10 -2
  44. package/dist/runtime-injection.js +15 -1
  45. package/dist/signal-processing.js +19 -28
  46. package/dist/signals-api.d.ts +2 -0
  47. package/dist/signals-api.js +9 -0
  48. package/dist/signals.js +785 -62
  49. package/dist/ssg.js +1176 -452
  50. package/dist/state-management.d.ts +1 -1
  51. package/dist/state-management.js +1 -1
  52. package/dist/strict-lint.d.ts +1 -1
  53. package/dist/strict-lint.js +1 -1
  54. package/dist/stx.d.ts +6 -0
  55. package/dist/style-scoping.js +10 -0
  56. package/dist/utils.js +5 -3
  57. package/dist/variable-extractor.js +14 -22
  58. package/dist/vue-template.js +3 -2
  59. package/dist/x-element.d.ts +2 -2
  60. package/dist/x-element.js +9 -8
  61. package/package.json +5 -5
  62. package/stx.d.ts +6 -0
@@ -159,6 +159,12 @@ else if (data.type === 'event' && data.method) {
159
159
  });
160
160
  }
161
161
 
162
+ // Live component handles, keyed by id, so an event coming back from the host
163
+ // can be routed to the callbacks registered on the right one. Without this
164
+ // the onSelect/onSpaceChange callbacks were stored and never invoked.
165
+ const sidebars = new Map();
166
+ const spacesSidebars = new Map();
167
+
162
168
  function createNativeUIFacade() {
163
169
  class Sidebar {
164
170
  constructor(id) {
@@ -178,10 +184,42 @@ else if (data.type === 'event' && data.method) {
178
184
  return this;
179
185
  }
180
186
  destroy() {
187
+ sidebars.delete(this.id);
181
188
  sendNativeUI('destroyComponent', { id: this.id, type: 'sidebar' });
182
189
  }
183
190
  }
184
191
 
192
+ /**
193
+ * An Arc-style sidebar of swipeable spaces.
194
+ *
195
+ * Separate from Sidebar rather than an option on it: a source list selects
196
+ * one row out of many, while a spaces sidebar selects one whole SCENE, and
197
+ * a host that renders the switcher natively (a segmented control in the
198
+ * toolbar, a Touch Bar strip) needs the space list, not the item list.
199
+ */
200
+ class SpacesSidebar {
201
+ constructor(id) {
202
+ this.id = id;
203
+ this._spaceCallbacks = [];
204
+ }
205
+ setSpaces(spaces) {
206
+ sendNativeUI('setSidebarSpaces', { sidebarId: this.id, spaces });
207
+ return this;
208
+ }
209
+ setActiveSpace(spaceId) {
210
+ sendNativeUI('setActiveSpace', { sidebarId: this.id, spaceId });
211
+ return this;
212
+ }
213
+ onSpaceChange(callback) {
214
+ this._spaceCallbacks.push(callback);
215
+ return this;
216
+ }
217
+ destroy() {
218
+ spacesSidebars.delete(this.id);
219
+ sendNativeUI('destroyComponent', { id: this.id, type: 'spacesSidebar' });
220
+ }
221
+ }
222
+
185
223
  class FileBrowser {
186
224
  constructor(id) {
187
225
  this.id = id;
@@ -218,12 +256,33 @@ else if (data.type === 'event' && data.method) {
218
256
  }
219
257
  }
220
258
 
259
+ // Host -> page. The host names the component in the payload; we look up the
260
+ // handle and fan out to whatever the page registered on it.
261
+ on('sidebar.select', function(params) {
262
+ const sidebar = sidebars.get(params && params.sidebarId);
263
+ if (sidebar) sidebar._selectCallbacks.forEach(fn => fn(params.itemId));
264
+ });
265
+ on('sidebar.spaceChange', function(params) {
266
+ const sidebar = spacesSidebars.get(params && params.sidebarId);
267
+ if (sidebar) sidebar._spaceCallbacks.forEach(fn => fn(params.spaceId));
268
+ });
269
+
221
270
  return {
222
271
  createSidebar(options) {
223
272
  const opts = options || {};
224
273
  const id = opts.id || 'sidebar-' + Date.now() + '-' + Math.random().toString(36).slice(2);
225
274
  sendNativeUI('createSidebar', Object.assign({}, opts, { id }));
226
- return new Sidebar(id);
275
+ const sidebar = new Sidebar(id);
276
+ sidebars.set(id, sidebar);
277
+ return sidebar;
278
+ },
279
+ createSpacesSidebar(options) {
280
+ const opts = options || {};
281
+ const id = opts.id || 'spaces-' + Date.now() + '-' + Math.random().toString(36).slice(2);
282
+ sendNativeUI('createSpacesSidebar', Object.assign({}, opts, { id }));
283
+ const sidebar = new SpacesSidebar(id);
284
+ spacesSidebars.set(id, sidebar);
285
+ return sidebar;
227
286
  },
228
287
  createFileBrowser(options) {
229
288
  const opts = options || {};
@@ -372,6 +431,26 @@ else if (data.type === 'event' && data.method) {
372
431
  platform: () => request('process.platform'),
373
432
  },
374
433
 
434
+ // Gesture stream
435
+ //
436
+ // A webview's wheel events carry no phase information, so a page can only
437
+ // guess where a trackpad swipe begins and ends \u2014 typically with an idle
438
+ // timeout, which mistimes the release and cannot see momentum at all. The
439
+ // host holds the real NSEvent, so it forwards the phases instead:
440
+ //
441
+ // { axis: 'horizontal' | 'vertical',
442
+ // phase: 'begin' | 'change' | 'end',
443
+ // deltaX, deltaY, // points since the last event
444
+ // velocityX, velocityY, // points per second, at 'end'
445
+ // momentum: boolean } // the OS is coasting, not the finger
446
+ //
447
+ // Consumers must stay correct when nothing ever arrives: a host without
448
+ // gesture support simply never emits, so keep the wheel path bound and let
449
+ // a begin phase take over from it.
450
+ gestures: {
451
+ onSwipe: (callback) => on('gesture.swipe', callback),
452
+ },
453
+
375
454
  // Native component helpers
376
455
  nativeUI,
377
456
  components: componentAPI
@@ -123,7 +123,14 @@ export declare const CRAFT_COMPONENTS: {
123
123
  nativeType: 'craft-sidebar';
124
124
  fallbackTag: 'aside';
125
125
  fallbackClasses: 'craft-sidebar craft-sidebar-tahoe';
126
- nativeProps: readonly ['id', 'variant', 'material', 'backgroundEffect', 'allowsVibrancy', 'materialOpacity', 'materialDarkOpacity', 'minWidth', 'maxWidth', 'canCollapse', 'searchPlaceholder', 'selectedItem', 'sections'];
126
+ nativeProps: readonly ['id', 'variant', 'material', 'backgroundEffect', 'allowsVibrancy', 'materialOpacity', 'materialDarkOpacity', 'minWidth', 'maxWidth', 'canCollapse', 'searchPlaceholder', 'selectedItem', 'sections', 'spaces', 'selectedSpace'];
127
+ render: unknown
128
+ };
129
+ 'sidebar-space': {
130
+ nativeType: 'craft-sidebar-space';
131
+ fallbackTag: 'section';
132
+ fallbackClasses: 'craft-sidebar-space';
133
+ nativeProps: readonly ['id', 'label', 'icon', 'tint'];
127
134
  render: unknown
128
135
  };
129
136
  'sidebar-section': {
@@ -503,6 +510,48 @@ export declare const CRAFT_COMPONENT_STYLES: `
503
510
  background: rgba(255, 255, 255, 0.24);
504
511
  }
505
512
 
513
+ /* Spaces fallback. Stacked in document order behind a rail — legible without
514
+ a native host, and deliberately not a second swipe implementation. */
515
+ .craft-sidebar-spaces {
516
+ position: relative;
517
+ z-index: 1;
518
+ flex: 1;
519
+ min-height: 0;
520
+ overflow-y: auto;
521
+ }
522
+ .craft-sidebar-space-title {
523
+ padding: 10px 8px 4px;
524
+ font-size: 13px;
525
+ font-weight: 600;
526
+ }
527
+ .craft-sidebar-space-switcher {
528
+ position: relative;
529
+ z-index: 1;
530
+ display: flex;
531
+ align-items: center;
532
+ gap: 1px;
533
+ padding-top: 6px;
534
+ }
535
+ .craft-sidebar-space-tab {
536
+ display: grid;
537
+ place-items: center;
538
+ width: 28px;
539
+ height: 26px;
540
+ border: 0;
541
+ border-radius: 7px;
542
+ background: transparent;
543
+ color: inherit;
544
+ font: inherit;
545
+ font-size: 11px;
546
+ font-weight: 700;
547
+ opacity: 0.4;
548
+ cursor: default;
549
+ }
550
+ .craft-sidebar-space-tab[data-selected="true"] {
551
+ opacity: 1;
552
+ background: rgba(255, 255, 255, 0.55);
553
+ }
554
+
506
555
  .craft-code-editor {
507
556
  font-family: 'SF Mono', 'Fira Code', monospace;
508
557
  font-size: 13px;
@@ -578,11 +627,36 @@ export declare const CRAFT_COMPONENT_RUNTIME: `
578
627
  var config = parseConfig(sidebar);
579
628
  if (!config) return;
580
629
 
630
+ var hasSpaces = !!(config.spaces && config.spaces.length);
631
+ // Spaces need a host that can swipe between whole scenes. Falling back to
632
+ // createSidebar would flatten every space into one list, which is worse
633
+ // than the web rendering already on the page — so leave that in place.
634
+ if (hasSpaces && typeof nativeUI.createSpacesSidebar !== 'function') return;
635
+
581
636
  try {
582
637
  var nativeConfig = Object.assign({}, config);
583
638
  delete nativeConfig.sections;
584
- var nativeSidebar = nativeUI.createSidebar(nativeConfig);
585
- if (nativeSidebar && config.sections) {
639
+ delete nativeConfig.spaces;
640
+
641
+ var nativeSidebar = hasSpaces
642
+ ? nativeUI.createSpacesSidebar(nativeConfig)
643
+ : nativeUI.createSidebar(nativeConfig);
644
+
645
+ if (nativeSidebar && hasSpaces && nativeSidebar.setSpaces) {
646
+ nativeSidebar.setSpaces(config.spaces);
647
+ }
648
+ if (nativeSidebar && hasSpaces && config.selectedSpace && nativeSidebar.setActiveSpace) {
649
+ nativeSidebar.setActiveSpace(config.selectedSpace);
650
+ }
651
+ if (nativeSidebar && hasSpaces && nativeSidebar.onSpaceChange) {
652
+ nativeSidebar.onSpaceChange(function(spaceId) {
653
+ sidebar.dispatchEvent(new CustomEvent('craft-sidebar:space', {
654
+ bubbles: true,
655
+ detail: { spaceId: spaceId, sidebarId: config.id }
656
+ }));
657
+ });
658
+ }
659
+ if (nativeSidebar && !hasSpaces && config.sections) {
586
660
  config.sections.forEach(function(section) {
587
661
  if (nativeSidebar.addSection) nativeSidebar.addSection(section);
588
662
  });
@@ -218,10 +218,19 @@ export const CRAFT_COMPONENTS = {
218
218
  "canCollapse",
219
219
  "searchPlaceholder",
220
220
  "selectedItem",
221
- "sections"
221
+ "sections",
222
+ "spaces",
223
+ "selectedSpace"
222
224
  ],
223
225
  render: renderCraftSidebar
224
226
  },
227
+ "sidebar-space": {
228
+ nativeType: "craft-sidebar-space",
229
+ fallbackTag: "section",
230
+ fallbackClasses: "craft-sidebar-space",
231
+ nativeProps: ["id", "label", "icon", "tint"],
232
+ render: renderCraftSidebarSpace
233
+ },
225
234
  "sidebar-section": {
226
235
  nativeType: "craft-sidebar-section",
227
236
  fallbackTag: "section",
@@ -620,6 +629,48 @@ export const CRAFT_COMPONENTS = {
620
629
  background: rgba(255, 255, 255, 0.24);
621
630
  }
622
631
 
632
+ /* Spaces fallback. Stacked in document order behind a rail \u2014 legible without
633
+ a native host, and deliberately not a second swipe implementation. */
634
+ .craft-sidebar-spaces {
635
+ position: relative;
636
+ z-index: 1;
637
+ flex: 1;
638
+ min-height: 0;
639
+ overflow-y: auto;
640
+ }
641
+ .craft-sidebar-space-title {
642
+ padding: 10px 8px 4px;
643
+ font-size: 13px;
644
+ font-weight: 600;
645
+ }
646
+ .craft-sidebar-space-switcher {
647
+ position: relative;
648
+ z-index: 1;
649
+ display: flex;
650
+ align-items: center;
651
+ gap: 1px;
652
+ padding-top: 6px;
653
+ }
654
+ .craft-sidebar-space-tab {
655
+ display: grid;
656
+ place-items: center;
657
+ width: 28px;
658
+ height: 26px;
659
+ border: 0;
660
+ border-radius: 7px;
661
+ background: transparent;
662
+ color: inherit;
663
+ font: inherit;
664
+ font-size: 11px;
665
+ font-weight: 700;
666
+ opacity: 0.4;
667
+ cursor: default;
668
+ }
669
+ .craft-sidebar-space-tab[data-selected="true"] {
670
+ opacity: 1;
671
+ background: rgba(255, 255, 255, 0.55);
672
+ }
673
+
623
674
  .craft-code-editor {
624
675
  font-family: 'SF Mono', 'Fira Code', monospace;
625
676
  font-size: 13px;
@@ -694,11 +745,36 @@ export const CRAFT_COMPONENTS = {
694
745
  var config = parseConfig(sidebar);
695
746
  if (!config) return;
696
747
 
748
+ var hasSpaces = !!(config.spaces && config.spaces.length);
749
+ // Spaces need a host that can swipe between whole scenes. Falling back to
750
+ // createSidebar would flatten every space into one list, which is worse
751
+ // than the web rendering already on the page \u2014 so leave that in place.
752
+ if (hasSpaces && typeof nativeUI.createSpacesSidebar !== 'function') return;
753
+
697
754
  try {
698
755
  var nativeConfig = Object.assign({}, config);
699
756
  delete nativeConfig.sections;
700
- var nativeSidebar = nativeUI.createSidebar(nativeConfig);
701
- if (nativeSidebar && config.sections) {
757
+ delete nativeConfig.spaces;
758
+
759
+ var nativeSidebar = hasSpaces
760
+ ? nativeUI.createSpacesSidebar(nativeConfig)
761
+ : nativeUI.createSidebar(nativeConfig);
762
+
763
+ if (nativeSidebar && hasSpaces && nativeSidebar.setSpaces) {
764
+ nativeSidebar.setSpaces(config.spaces);
765
+ }
766
+ if (nativeSidebar && hasSpaces && config.selectedSpace && nativeSidebar.setActiveSpace) {
767
+ nativeSidebar.setActiveSpace(config.selectedSpace);
768
+ }
769
+ if (nativeSidebar && hasSpaces && nativeSidebar.onSpaceChange) {
770
+ nativeSidebar.onSpaceChange(function(spaceId) {
771
+ sidebar.dispatchEvent(new CustomEvent('craft-sidebar:space', {
772
+ bubbles: true,
773
+ detail: { spaceId: spaceId, sidebarId: config.id }
774
+ }));
775
+ });
776
+ }
777
+ if (nativeSidebar && !hasSpaces && config.sections) {
702
778
  config.sections.forEach(function(section) {
703
779
  if (nativeSidebar.addSection) nativeSidebar.addSection(section);
704
780
  });
@@ -755,10 +831,10 @@ export function processCraftComponents(content, config = {}) {
755
831
  </body>`);
756
832
  else
757
833
  content += CRAFT_COMPONENT_RUNTIME;
758
- content = content.replace(/<@craft-sidebar\s*([^>]*)>([\s\S]*?)<\/@craft-sidebar>/g, (match, propsStr, children) => {
834
+ content = content.replace(/<@craft-sidebar(?![-\w])\s*([^>]*)>([\s\S]*?)<\/@craft-sidebar>/g, (match, propsStr, children) => {
759
835
  return renderCraftComponent("sidebar", propsStr, children.trim(), config, classPrefix);
760
836
  });
761
- content = content.replace(/<@craft-sidebar\s*([^>]*?)\/>/g, (match, propsStr) => {
837
+ content = content.replace(/<@craft-sidebar(?![-\w])\s*([^>]*?)\/>/g, (match, propsStr) => {
762
838
  return renderCraftComponent("sidebar", propsStr, "", config, classPrefix);
763
839
  });
764
840
  content = content.replace(/<@craft-(\w+[-\w]*)\s*([^>]*?)\/>/g, (match, componentName, propsStr) => {
@@ -806,7 +882,7 @@ function parseProps(propsStr) {
806
882
  return props;
807
883
  }
808
884
  function renderCraftSidebar(props, children) {
809
- const id = normalizeId(String(props.id || `craft-sidebar-${hashString(children || JSON.stringify(props))}`)), sections = normalizeSidebarSections(props.sections) || extractSidebarSections(children), selectedItem = String(props.selectedItem || props.selected || sections[0]?.items[0]?.id || ""), config = {
885
+ const id = normalizeId(String(props.id || `craft-sidebar-${hashString(children || JSON.stringify(props))}`)), spaces = normalizeSidebarSpaces(props.spaces) || extractSidebarSpaces(children), sections = spaces.length ? [] : normalizeSidebarSections(props.sections) || extractSidebarSections(children), selectedSpace = String(props.selectedSpace || spaces[0]?.id || ""), openSpace = spaces.find((space) => space.id === selectedSpace) || spaces[0], selectedItem = String(props.selectedItem || props.selected || (spaces.length ? openSpace?.sections[0]?.items[0]?.id : sections[0]?.items[0]?.id) || ""), config = {
810
886
  id,
811
887
  variant: String(props.variant || "desktop"),
812
888
  material: String(props.material || "sidebar"),
@@ -819,14 +895,30 @@ function renderCraftSidebar(props, children) {
819
895
  canCollapse: props.canCollapse !== !1 && props.canCollapse !== "false",
820
896
  searchPlaceholder: props.searchPlaceholder ? String(props.searchPlaceholder) : void 0,
821
897
  selectedItem,
822
- sections
823
- }, fallbackClasses = String(props.__fallbackClasses || "craft-sidebar craft-sidebar-tahoe"), width = Number(props.width || props.sidebarWidth || 286), native = props.native === !1 || props.native === "false" ? "false" : "true", search = config.searchPlaceholder ? `<div class="craft-sidebar-search"><input type="search" placeholder="${escapeHtml(config.searchPlaceholder)}"></div>` : "", sectionHtml = sections.length ? sections.map((section) => renderSidebarSectionFallback(section, selectedItem)).join("") : children;
898
+ sections,
899
+ spaces,
900
+ selectedSpace: selectedSpace || void 0
901
+ }, fallbackClasses = String(props.__fallbackClasses || "craft-sidebar craft-sidebar-tahoe"), width = Number(props.width || props.sidebarWidth || 286), native = props.native === !1 || props.native === "false" ? "false" : "true", search = config.searchPlaceholder ? `<div class="craft-sidebar-search"><input type="search" placeholder="${escapeHtml(config.searchPlaceholder)}"></div>` : "";
902
+ let body;
903
+ if (spaces.length)
904
+ body = renderSidebarSpacesFallback(spaces, selectedSpace, selectedItem);
905
+ else if (sections.length)
906
+ body = sections.map((section) => renderSidebarSectionFallback(section, selectedItem)).join("");
907
+ else
908
+ body = children;
824
909
  return `<aside class="${escapeAttribute(fallbackClasses)}" data-craft="craft-sidebar" data-craft-native-sidebar data-craft-sidebar-id="${escapeAttribute(id)}" data-native="${native}" style="--craft-sidebar-width:${width}px">
825
910
  ${search}
826
- ${sectionHtml}
911
+ ${body}
827
912
  </aside>
828
913
  <script type="application/json" data-craft-sidebar-config="${escapeAttribute(id)}">${escapeScriptJson(config)}</script>`;
829
914
  }
915
+ function renderCraftSidebarSpace(props, children) {
916
+ const id = normalizeId(String(props.id || props.label || "space"));
917
+ return `<section class="craft-sidebar-space" data-craft="craft-sidebar-space" data-space-id="${escapeAttribute(id)}">
918
+ ${props.label ? `<div class="craft-sidebar-space-title">${escapeHtml(String(props.label))}</div>` : ""}
919
+ ${children}
920
+ </section>`;
921
+ }
830
922
  function renderCraftSidebarSection(props, children) {
831
923
  const title = String(props.header || props.title || "");
832
924
  return `<section class="craft-sidebar-section" data-craft="craft-sidebar-section" data-section-id="${escapeAttribute(String(props.id || "section"))}">
@@ -868,6 +960,43 @@ function extractSidebarItems(content) {
868
960
  }
869
961
  return items;
870
962
  }
963
+ function extractSidebarSpaces(children) {
964
+ const spaces = [], spacePattern = /<@craft-sidebar-space\s*([^>]*?)(?:\/>|>([\s\S]*?)<\/@craft-sidebar-space>)/g;
965
+ let spaceMatch;
966
+ while ((spaceMatch = spacePattern.exec(children)) !== null) {
967
+ const props = parseProps(spaceMatch[1] || ""), body = spaceMatch[2] || "", label = String(props.label || props.title || `Space ${spaces.length + 1}`);
968
+ spaces.push({
969
+ id: String(props.id || normalizeId(label)),
970
+ label,
971
+ icon: props.icon ? String(props.icon) : void 0,
972
+ tint: props.tint ? String(props.tint) : void 0,
973
+ sections: extractSidebarSections(body)
974
+ });
975
+ }
976
+ return spaces;
977
+ }
978
+ function normalizeSidebarSpaces(value) {
979
+ if (!value)
980
+ return;
981
+ if (Array.isArray(value))
982
+ return value;
983
+ if (typeof value !== "string")
984
+ return;
985
+ try {
986
+ const parsed = JSON.parse(value);
987
+ return Array.isArray(parsed) ? parsed : void 0;
988
+ } catch {
989
+ return;
990
+ }
991
+ }
992
+ function renderSidebarSpacesFallback(spaces, selectedSpace, selectedItem) {
993
+ const rail = spaces.map((space) => `<button class="craft-sidebar-space-tab" type="button" data-space-target="${escapeAttribute(space.id)}" data-selected="${space.id === selectedSpace ? "true" : "false"}" title="${escapeAttribute(space.label || space.id)}">${space.icon ? `<span class="craft-sidebar-item-icon" data-icon="${escapeAttribute(space.icon)}"></span>` : escapeHtml((space.label || space.id).charAt(0))}</button>`).join("");
994
+ return `<div class="craft-sidebar-spaces">${spaces.map((space) => `<section class="craft-sidebar-space" data-space-id="${escapeAttribute(space.id)}" data-selected="${space.id === selectedSpace ? "true" : "false"}">
995
+ ${space.label ? `<div class="craft-sidebar-space-title">${escapeHtml(space.label)}</div>` : ""}
996
+ ${space.sections.map((section) => renderSidebarSectionFallback(section, selectedItem)).join("")}
997
+ </section>`).join("")}</div>
998
+ <div class="craft-sidebar-space-switcher">${rail}</div>`;
999
+ }
871
1000
  function normalizeSidebarSections(value) {
872
1001
  if (!value)
873
1002
  return;