entasis 0.7.0 → 0.8.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 (48) hide show
  1. package/dist/components/FloatingWindow/FloatingWindow.svelte +26 -2
  2. package/dist/components/FloatingWindow/floatingWindow.dock.svelte.js +11 -2
  3. package/dist/components/FloatingWindow/floatingWindow.mcp.d.ts +1 -1
  4. package/dist/components/FloatingWindow/floatingWindow.mcp.js +7 -4
  5. package/dist/components/FloatingWindow/floatingWindow.props.d.ts +6 -0
  6. package/dist/components/FloatingWindow/floatingWindow.state.svelte.d.ts +3 -0
  7. package/dist/components/FloatingWindow/floatingWindow.state.svelte.js +18 -4
  8. package/dist/components/FloatingWindow/floatingWindow.theme.d.ts +3 -0
  9. package/dist/components/FloatingWindow/floatingWindow.theme.js +20 -7
  10. package/dist/components/Form/File/FileInput.svelte +6 -42
  11. package/dist/components/Sidebar/Sidebar.svelte +72 -12
  12. package/dist/components/Sidebar/Sidebar.svelte.d.ts +1 -1
  13. package/dist/components/Sidebar/SidebarActivityBar.svelte +9 -2
  14. package/dist/components/Sidebar/SidebarActivityBar.svelte.d.ts +5 -1
  15. package/dist/components/Sidebar/SidebarDesktopShell.svelte +12 -4
  16. package/dist/components/Sidebar/SidebarMenuItem.svelte +19 -2
  17. package/dist/components/Sidebar/SidebarMenuItem.svelte.d.ts +4 -0
  18. package/dist/components/Sidebar/SidebarPanel.svelte +199 -88
  19. package/dist/components/Sidebar/SidebarPanel.svelte.d.ts +3 -0
  20. package/dist/components/Sidebar/SidebarViewStage.svelte +103 -0
  21. package/dist/components/Sidebar/SidebarViewStage.svelte.d.ts +18 -0
  22. package/dist/components/Sidebar/index.d.ts +1 -1
  23. package/dist/components/Sidebar/sidebar-layout.d.ts +7 -2
  24. package/dist/components/Sidebar/sidebar-layout.js +17 -6
  25. package/dist/components/Sidebar/sidebar.mcp.d.ts +1 -1
  26. package/dist/components/Sidebar/sidebar.mcp.js +19 -2
  27. package/dist/components/Sidebar/sidebar.props.d.ts +56 -1
  28. package/dist/components/Sidebar/sidebar.state.svelte.d.ts +4 -0
  29. package/dist/components/Sidebar/sidebar.state.svelte.js +9 -1
  30. package/dist/components/Sidebar/sidebar.theme.d.ts +149 -5
  31. package/dist/components/Sidebar/sidebar.theme.js +96 -3
  32. package/dist/components/Sidebar/sidebar.views.svelte.d.ts +138 -0
  33. package/dist/components/Sidebar/sidebar.views.svelte.js +303 -0
  34. package/dist/components/Theme/theme.floatingWindows.d.ts +6 -1
  35. package/dist/components/Theme/theme.floatingWindows.js +7 -2
  36. package/dist/components/Theme/theme.mcp.d.ts +1 -1
  37. package/dist/components/Theme/theme.mcp.js +1 -0
  38. package/dist/generated/componentContract.d.ts +1 -1
  39. package/dist/generated/componentContract.js +1 -0
  40. package/dist/generated/componentMcpRegistry.d.ts +4 -4
  41. package/dist/tailwind/index.mcp.d.ts +1 -1
  42. package/dist/tailwind/index.mcp.js +4 -0
  43. package/dist/tailwind/scales.js +11 -3
  44. package/dist/tailwind/spacing.js +17 -0
  45. package/dist/utils/cva/merge.d.ts +7 -0
  46. package/dist/utils/cva/merge.js +6 -1
  47. package/dist/utils/pointerDrag.js +7 -1
  48. package/package.json +1 -1
@@ -286,9 +286,17 @@
286
286
  <div
287
287
  data-slot="sidebar-activity-bar-container"
288
288
  data-side={side}
289
- class={getSidebarActivityBarContainerClass(side, frame)}
289
+ class={getSidebarActivityBarContainerClass(side, frame, variant)}
290
290
  >
291
- <SidebarActivityBar {activityBar} {side} {size} {density} label={activityBarLabel} {theme} />
291
+ <SidebarActivityBar
292
+ {activityBar}
293
+ {side}
294
+ {size}
295
+ {density}
296
+ {variant}
297
+ label={activityBarLabel}
298
+ {theme}
299
+ />
292
300
  </div>
293
301
  {/if}
294
302
  <!-- A hidden panel is parked off screen: `inert` keeps Tab out of it, so focus cannot land
@@ -379,8 +387,8 @@
379
387
  className:
380
388
  activityBar &&
381
389
  (side === 'left'
382
- ? '!left-[var(--sidebar-width-activity,3rem)]'
383
- : '!right-[var(--sidebar-width-activity,3rem)]')
390
+ ? '!left-[var(--sidebar-activity-offset,3rem)]'
391
+ : '!right-[var(--sidebar-activity-offset,3rem)]')
384
392
  })}
385
393
  onpointerenter={() => {
386
394
  if (!resize.isEdgeRevealSuppressed) edgeRevealed = true;
@@ -30,6 +30,8 @@
30
30
  size,
31
31
  activeVariant,
32
32
  density,
33
+ back = false,
34
+ ariaLabel,
33
35
  theme
34
36
  }: {
35
37
  item: SidebarMenuEntry;
@@ -39,6 +41,10 @@
39
41
  size: SidebarSize;
40
42
  activeVariant: SidebarActiveVariant;
41
43
  density: SidebarDensity;
44
+ /** The row a nested view opens on, back to its parent: a leading back arrow, no chevron. */
45
+ back?: boolean;
46
+ /** Accessible name when the visible label alone does not say what the row does. */
47
+ ariaLabel?: string;
42
48
  theme?: SidebarThemeProps;
43
49
  } = $props();
44
50
 
@@ -62,7 +68,9 @@
62
68
  api.displayState === 'collapsed' && !api.isMobile && !api.isPeeking
63
69
  );
64
70
  const showTooltip = $derived((tooltips === 'always' || isIconCollapsed) && !api.isMobile);
65
- const tooltipContent = $derived(showTooltip ? (item.tooltip ?? item.label) : undefined);
71
+ const tooltipContent = $derived(
72
+ showTooltip ? (item.tooltip ?? ariaLabel ?? item.label) : undefined
73
+ );
66
74
  const hasSubmenu = $derived(!!item.items?.length);
67
75
  // A bare glyph stays bare: the wrapper only appears when the entry asks for a role tint or a
68
76
  // tile, so an untinted row never inherits the ambient `data-color`.
@@ -97,6 +105,7 @@
97
105
  return;
98
106
  }
99
107
  item.onclick?.(event);
108
+ if (item.view !== undefined && !event.defaultPrevented) api.setView(item.view);
100
109
  }
101
110
  </script>
102
111
 
@@ -110,7 +119,7 @@
110
119
  <SidebarIcon icon={item.icon} />
111
120
  </span>
112
121
  {:else}
113
- <SidebarIcon icon={item.icon} />
122
+ <SidebarIcon icon={item.icon} class={back ? 'rtl:-scale-x-100' : undefined} />
114
123
  {/if}
115
124
  <span class={classes.menuLabel()}>{item.label}</span>
116
125
  {/snippet}
@@ -170,6 +179,8 @@
170
179
  data-active={item.isActive ? 'true' : undefined}
171
180
  data-active-variant={activeVariant}
172
181
  aria-current={item.isActive ? 'page' : undefined}
182
+ aria-label={ariaLabel}
183
+ data-sidebar-view-target={item.view}
173
184
  disabled={item.disabled || undefined}
174
185
  class={classes.menuButton({
175
186
  variant: item.variant,
@@ -183,6 +194,12 @@
183
194
  onclick={handleClick}
184
195
  >
185
196
  {@render entryContent()}
197
+ {#if item.view !== undefined && !back}
198
+ <SidebarIcon
199
+ icon={caretRightIcon}
200
+ class={classes.menuTrailing({ size, className: 'rtl:-scale-x-100' })}
201
+ />
202
+ {/if}
186
203
  </button>
187
204
  {/if}
188
205
  {/snippet}
@@ -9,6 +9,10 @@ type $$ComponentProps = {
9
9
  size: SidebarSize;
10
10
  activeVariant: SidebarActiveVariant;
11
11
  density: SidebarDensity;
12
+ /** The row a nested view opens on, back to its parent: a leading back arrow, no chevron. */
13
+ back?: boolean;
14
+ /** Accessible name when the visible label alone does not say what the row does. */
15
+ ariaLabel?: string;
12
16
  theme?: SidebarThemeProps;
13
17
  };
14
18
  declare const SidebarMenuItem: import("svelte").Component<$$ComponentProps, {}, "">;
@@ -16,8 +16,12 @@
16
16
  import SidebarGroupComponent from './SidebarGroup.svelte';
17
17
  import SidebarIcon from './SidebarIcon.svelte';
18
18
  import SidebarMenuButton from './SidebarMenuButton.svelte';
19
+ import SidebarMenuItem from './SidebarMenuItem.svelte';
19
20
  import SidebarMenuList from './SidebarMenuList.svelte';
21
+ import SidebarViewStage from './SidebarViewStage.svelte';
22
+ import { arrowLeftIcon } from '../Icons/arrowLeft.js';
20
23
  import { useSidebarTheme, type SidebarThemeProps } from './sidebar.theme.js';
24
+ import { type SidebarSlotValues, type SidebarViewsState } from './sidebar.views.svelte.js';
21
25
 
22
26
  let {
23
27
  api,
@@ -36,6 +40,7 @@
36
40
  activeVariant,
37
41
  density,
38
42
  label,
43
+ views,
39
44
  theme
40
45
  }: {
41
46
  api: SidebarApi;
@@ -55,9 +60,27 @@
55
60
  density: SidebarDensity;
56
61
  /** Accessible name for the body navigation landmark. */
57
62
  label: string;
63
+ /** Named views; when enabled, each region slides between them instead of rendering the props. */
64
+ views?: SidebarViewsState;
58
65
  theme?: SidebarThemeProps;
59
66
  } = $props();
60
67
 
68
+ const rootSource = $derived<SidebarSlotValues>({
69
+ items,
70
+ content,
71
+ headerButton,
72
+ search,
73
+ headerMenu,
74
+ header,
75
+ footerButton,
76
+ footerMenu,
77
+ footer
78
+ });
79
+ const hasHeader = (source: SidebarSlotValues) =>
80
+ !!(source.headerButton || source.search || source.headerMenu || source.header);
81
+ const hasFooter = (source: SidebarSlotValues) =>
82
+ !!(source.footerButton || source.footerMenu || source.footer);
83
+
61
84
  const classes = $derived(useSidebarTheme(theme));
62
85
  const t = $derived(useI18n());
63
86
  // A hover peek renders the collapsed panel at full width, so icon-mode behaviour has to
@@ -85,66 +108,87 @@
85
108
  });
86
109
  </script>
87
110
 
88
- {#if headerButton || search || headerMenu || header}
89
- <div data-slot="sidebar-header" data-sidebar="header" class={classes.header({ density })}>
90
- <!--
91
- The `header` snippet renders first, so a custom workspace card sits above the built-in
92
- search and menu instead of under them. Consumers never need an order override.
93
- -->
94
- {#if header}
95
- {@render header(api)}
96
- {/if}
97
- {#if headerButton}
98
- <SidebarMenuButton
99
- {...headerButton}
100
- {api}
101
- mobile={api.isMobile}
102
- defaultAlign="start"
103
- {size}
104
- {density}
105
- {theme}
106
- />
107
- {/if}
108
- {#if search}
109
- <form
110
- bind:this={searchRef}
111
- data-slot="sidebar-search"
112
- class={classes.searchContainer({ size, collapsed })}
113
- inert={collapsed ? true : undefined}
114
- aria-hidden={collapsed ? 'true' : undefined}
115
- onsubmit={(event) => event.preventDefault()}
116
- >
117
- <input
118
- placeholder={search.placeholder}
119
- aria-label={search.label ?? t.search}
120
- value={search.value}
121
- oninput={search.oninput}
122
- class={classes.search({ size, density, className: search.class })}
123
- />
124
- <SidebarIcon icon={magnifyingGlassIcon} class={classes.searchIcon({ size, density })} />
125
- </form>
126
- {/if}
127
- {#if headerMenu}
128
- <SidebarMenuList
129
- items={headerMenu}
130
- {api}
131
- {collapseIcon}
132
- {tooltips}
133
- {size}
134
- {activeVariant}
135
- {density}
136
- {theme}
137
- />
138
- {/if}
139
- </div>
140
- {/if}
111
+ {#snippet headerButtonSlot(headerButton: SidebarMenuButtonItem)}
112
+ <SidebarMenuButton
113
+ {...headerButton}
114
+ {api}
115
+ mobile={api.isMobile}
116
+ defaultAlign="start"
117
+ {size}
118
+ {density}
119
+ {theme}
120
+ />
121
+ {/snippet}
122
+
123
+ {#snippet searchSlot(searchBox: SidebarSearch)}
124
+ <form
125
+ bind:this={searchRef}
126
+ data-slot="sidebar-search"
127
+ class={classes.searchContainer({ size, collapsed })}
128
+ inert={collapsed ? true : undefined}
129
+ aria-hidden={collapsed ? 'true' : undefined}
130
+ onsubmit={(event) => event.preventDefault()}
131
+ >
132
+ <input
133
+ placeholder={searchBox.placeholder}
134
+ aria-label={searchBox.label ?? t.search}
135
+ value={searchBox.value}
136
+ oninput={searchBox.oninput}
137
+ class={classes.search({ size, density, className: searchBox.class })}
138
+ />
139
+ <SidebarIcon icon={magnifyingGlassIcon} class={classes.searchIcon({ size, density })} />
140
+ </form>
141
+ {/snippet}
142
+
143
+ {#snippet menuSlot(items: SidebarMenuEntry[])}
144
+ <SidebarMenuList
145
+ {items}
146
+ {api}
147
+ {collapseIcon}
148
+ {tooltips}
149
+ {size}
150
+ {activeVariant}
151
+ {density}
152
+ {theme}
153
+ />
154
+ {/snippet}
155
+
156
+ {#snippet footerButtonSlot(footerButton: SidebarMenuButtonItem)}
157
+ <SidebarMenuButton
158
+ {...footerButton}
159
+ {api}
160
+ mobile={api.isMobile}
161
+ defaultAlign="end"
162
+ {size}
163
+ {density}
164
+ {theme}
165
+ />
166
+ {/snippet}
141
167
 
142
- <!-- The activity bar is a second nav landmark, so this one needs its own name to tell them apart. -->
143
- <nav data-slot="sidebar-nav" data-sidebar="nav" aria-label={label} class={classes.nav({ density })}>
144
- {#if content}
145
- {@render content(api)}
146
- {:else if items}
147
- {#each items as group, index (group.label ?? `group-${index}`)}
168
+ {#snippet headerRegion(source: SidebarSlotValues)}
169
+ <!--
170
+ The `header` snippet renders first, so a custom workspace card sits above the built-in
171
+ search and menu instead of under them. Consumers never need an order override.
172
+ -->
173
+ {#if source.header}
174
+ {@render source.header(api)}
175
+ {/if}
176
+ {#if source.headerButton}
177
+ {@render headerButtonSlot(source.headerButton)}
178
+ {/if}
179
+ {#if source.search}
180
+ {@render searchSlot(source.search)}
181
+ {/if}
182
+ {#if source.headerMenu}
183
+ {@render menuSlot(source.headerMenu)}
184
+ {/if}
185
+ {/snippet}
186
+
187
+ {#snippet bodyRegion(source: SidebarSlotValues)}
188
+ {#if source.content}
189
+ {@render source.content(api)}
190
+ {:else if source.items}
191
+ {#each source.items as group, index (group.label ?? `group-${index}`)}
148
192
  {#if group.separator && index > 0}
149
193
  <div
150
194
  data-slot="sidebar-separator"
@@ -164,35 +208,102 @@
164
208
  />
165
209
  {/each}
166
210
  {/if}
167
- </nav>
211
+ {/snippet}
168
212
 
169
- {#if footerButton || footerMenu || footer}
170
- <div data-slot="sidebar-footer" data-sidebar="footer" class={classes.footer({ density })}>
171
- {#if footerButton}
172
- <SidebarMenuButton
173
- {...footerButton}
174
- {api}
175
- mobile={api.isMobile}
176
- defaultAlign="end"
177
- {size}
178
- {density}
179
- {theme}
180
- />
181
- {/if}
182
- {#if footerMenu}
183
- <SidebarMenuList
184
- items={footerMenu}
185
- {api}
186
- {collapseIcon}
187
- {tooltips}
188
- {size}
189
- {activeVariant}
190
- {density}
213
+ {#snippet footerRegion(source: SidebarSlotValues)}
214
+ {#if source.footerButton}
215
+ {@render footerButtonSlot(source.footerButton)}
216
+ {/if}
217
+ {#if source.footerMenu}
218
+ {@render menuSlot(source.footerMenu)}
219
+ {/if}
220
+ {#if source.footer}
221
+ {@render source.footer(api)}
222
+ {/if}
223
+ {/snippet}
224
+
225
+ {#if views?.enabled}
226
+ <!--
227
+ Views with the same header and footer share one panel layer, so only the menu inside slides;
228
+ a view that changes them slides the whole panel as one page.
229
+ -->
230
+ <SidebarViewStage
231
+ {views}
232
+ kind="panel"
233
+ layers={views.layers('panel')}
234
+ layerClass="flex min-h-0 flex-col"
235
+ {theme}
236
+ >
237
+ {#snippet children(panelLayer)}
238
+ {@const chrome = views.chrome(panelLayer.view)}
239
+ {#if hasHeader(chrome)}
240
+ <div data-slot="sidebar-header" data-sidebar="header" class={classes.header({ density })}>
241
+ {@render headerRegion(chrome)}
242
+ </div>
243
+ {/if}
244
+ <!-- The activity bar is a second nav landmark, so this one needs its own name to tell them apart. -->
245
+ <SidebarViewStage
246
+ {views}
247
+ kind="body"
248
+ layers={panelLayer.role
249
+ ? [{ key: panelLayer.view, view: panelLayer.view }]
250
+ : views.layers('body')}
251
+ layerClass={classes.nav({ density })}
252
+ {label}
191
253
  {theme}
192
- />
193
- {/if}
194
- {#if footer}
195
- {@render footer(api)}
196
- {/if}
197
- </div>
254
+ >
255
+ {#snippet children(bodyLayer)}
256
+ {@const parent = views.parentOf(bodyLayer.view)}
257
+ {#if parent !== undefined}
258
+ {@const parentLabel = views.labelOf(parent)}
259
+ <!-- A nested view opens on a row back to its parent, named after it like a back button. -->
260
+ <div data-slot="sidebar-group" data-sidebar="group" class={classes.group({ density })}>
261
+ <ul data-slot="sidebar-menu" data-sidebar="menu" class={classes.menu({ density })}>
262
+ <SidebarMenuItem
263
+ item={{ label: parentLabel ?? t.back, icon: arrowLeftIcon, view: parent }}
264
+ back
265
+ ariaLabel={parentLabel ? `${t.back}, ${parentLabel}` : undefined}
266
+ {api}
267
+ {collapseIcon}
268
+ {tooltips}
269
+ {size}
270
+ {activeVariant}
271
+ {density}
272
+ {theme}
273
+ />
274
+ </ul>
275
+ </div>
276
+ {/if}
277
+ {@render bodyRegion(views.body(bodyLayer.view))}
278
+ {/snippet}
279
+ </SidebarViewStage>
280
+ {#if hasFooter(chrome)}
281
+ <div data-slot="sidebar-footer" data-sidebar="footer" class={classes.footer({ density })}>
282
+ {@render footerRegion(chrome)}
283
+ </div>
284
+ {/if}
285
+ {/snippet}
286
+ </SidebarViewStage>
287
+ {:else}
288
+ {#if hasHeader(rootSource)}
289
+ <div data-slot="sidebar-header" data-sidebar="header" class={classes.header({ density })}>
290
+ {@render headerRegion(rootSource)}
291
+ </div>
292
+ {/if}
293
+
294
+ <!-- The activity bar is a second nav landmark, so this one needs its own name to tell them apart. -->
295
+ <nav
296
+ data-slot="sidebar-nav"
297
+ data-sidebar="nav"
298
+ aria-label={label}
299
+ class={classes.nav({ density })}
300
+ >
301
+ {@render bodyRegion(rootSource)}
302
+ </nav>
303
+
304
+ {#if hasFooter(rootSource)}
305
+ <div data-slot="sidebar-footer" data-sidebar="footer" class={classes.footer({ density })}>
306
+ {@render footerRegion(rootSource)}
307
+ </div>
308
+ {/if}
198
309
  {/if}
@@ -1,6 +1,7 @@
1
1
  import type { DisclosureIndicator } from '../../types/theme.js';
2
2
  import type { SidebarActiveVariant, SidebarApi, SidebarGroup, SidebarMenuButtonItem, SidebarMenuEntry, SidebarSearch, SidebarDensity, SidebarSize, SidebarTooltipMode } from './sidebar.props.js';
3
3
  import { type SidebarThemeProps } from './sidebar.theme.js';
4
+ import { type SidebarViewsState } from './sidebar.views.svelte.js';
4
5
  type $$ComponentProps = {
5
6
  api: SidebarApi;
6
7
  items?: SidebarGroup[];
@@ -19,6 +20,8 @@ type $$ComponentProps = {
19
20
  density: SidebarDensity;
20
21
  /** Accessible name for the body navigation landmark. */
21
22
  label: string;
23
+ /** Named views; when enabled, each region slides between them instead of rendering the props. */
24
+ views?: SidebarViewsState;
22
25
  theme?: SidebarThemeProps;
23
26
  };
24
27
  declare const SidebarPanel: import("svelte").Component<$$ComponentProps, {}, "">;
@@ -0,0 +1,103 @@
1
+ <script lang="ts">
2
+ import { untrack, type Snippet } from 'svelte';
3
+ import type { Attachment } from 'svelte/attachments';
4
+ import {
5
+ sidebarViewTransition,
6
+ type SidebarViewKind,
7
+ type SidebarViewLayer,
8
+ type SidebarViewsState
9
+ } from './sidebar.views.svelte.js';
10
+ import { useSidebarMotion, useSidebarTheme, type SidebarThemeProps } from './sidebar.theme.js';
11
+
12
+ let {
13
+ views,
14
+ kind,
15
+ layers,
16
+ layerClass,
17
+ label,
18
+ theme,
19
+ children
20
+ }: {
21
+ views: SidebarViewsState;
22
+ /** `panel` slides the whole panel and owns the swipe; `body` slides the menu inside it. */
23
+ kind: SidebarViewKind;
24
+ layers: SidebarViewLayer[];
25
+ /** Classes each layer wears on top of `viewLayer`. */
26
+ layerClass?: string;
27
+ /** Landmark name; the body stage is the nav landmark. */
28
+ label?: string;
29
+ theme?: SidebarThemeProps;
30
+ children: Snippet<[layer: SidebarViewLayer]>;
31
+ } = $props();
32
+
33
+ const classes = $derived(useSidebarTheme(theme));
34
+ // The `view` motion of the Sidebar motion slot: travel, rest opacity, duration and easing come
35
+ // from it, so a `<Theme motion>` retune and a reduced-motion preference reach the view slide.
36
+ const resolveMotion = useSidebarMotion();
37
+ const motion = $derived(resolveMotion({ part: 'view' }, { motion: theme?.motion }));
38
+ const duration = $derived(motion.in.duration ?? 0);
39
+ const travel = $derived(motion.in.x ?? 0);
40
+ const restOpacity = $derived(motion.in.opacity ?? 0);
41
+ const isPanel = $derived(kind === 'panel');
42
+ const swipe = $derived(views.canSwipeBack);
43
+ const dragging = $derived(views.swipe?.phase === 'drag');
44
+
45
+ // A settling swipe (commit or cancel) ends once its layers have eased to rest.
46
+ $effect(() => {
47
+ const phase = views.swipe?.phase;
48
+ if (!isPanel || !phase || phase === 'drag') return;
49
+ const timer = setTimeout(() => views.settleSwipe(), duration);
50
+ return () => clearTimeout(timer);
51
+ });
52
+
53
+ // Focus that sat in the view being replaced lands in the new one: on the row that opens the view
54
+ // just left (a back row after going deeper, the row that opened it after coming back), else on
55
+ // the first control. Focus anywhere else stays put, so a route change never pulls it here.
56
+ const arrive: Attachment<HTMLElement> = (node) =>
57
+ untrack(() => {
58
+ const stage = node.parentElement;
59
+ const active = document.activeElement;
60
+ if (node.dataset.swipe || !stage || !(active instanceof HTMLElement)) return;
61
+ if (!stage.contains(active) || node.contains(active)) return;
62
+ const from = views.lastChange.from;
63
+ const target =
64
+ (from !== undefined &&
65
+ node.querySelector<HTMLElement>(`[data-sidebar-view-target="${CSS.escape(from)}"]`)) ||
66
+ node.querySelector<HTMLElement>(
67
+ 'a[href], button:not([disabled]), input:not([disabled]), [tabindex]:not([tabindex="-1"])'
68
+ );
69
+ target?.focus({ preventScroll: true });
70
+ });
71
+ </script>
72
+
73
+ <svelte:element
74
+ this={isPanel ? 'div' : 'nav'}
75
+ data-slot="sidebar-view-stage"
76
+ data-sidebar={isPanel ? undefined : 'nav'}
77
+ data-view-kind={kind}
78
+ aria-label={label}
79
+ data-no-swipe={isPanel && views.ownsDrawerSwipe ? '' : undefined}
80
+ class={classes.viewStage()}
81
+ {@attach isPanel ? views.swipeAttachment : undefined}
82
+ >
83
+ {#each layers as layer (layer.key)}
84
+ {@const swipeStyle = views.swipeStyle(layer.role, travel, restOpacity)}
85
+ <div
86
+ data-slot="sidebar-view-layer"
87
+ data-sidebar="view-layer"
88
+ data-view={layer.view}
89
+ data-swipe={layer.role}
90
+ data-dragging={layer.role && dragging ? 'true' : undefined}
91
+ inert={layer.role === 'under' && dragging ? true : undefined}
92
+ class={[layerClass, classes.viewLayer({ swipe })]}
93
+ style:translate={swipeStyle.translate}
94
+ style:opacity={swipeStyle.opacity}
95
+ style:transition-duration={`${duration}ms`}
96
+ in:sidebarViewTransition={{ phase: 'in', direction: views.lastChange.direction, motion }}
97
+ out:sidebarViewTransition={{ phase: 'out', direction: views.lastChange.direction, motion }}
98
+ {@attach arrive}
99
+ >
100
+ {@render children(layer)}
101
+ </div>
102
+ {/each}
103
+ </svelte:element>
@@ -0,0 +1,18 @@
1
+ import { type Snippet } from 'svelte';
2
+ import { type SidebarViewKind, type SidebarViewLayer, type SidebarViewsState } from './sidebar.views.svelte.js';
3
+ import { type SidebarThemeProps } from './sidebar.theme.js';
4
+ type $$ComponentProps = {
5
+ views: SidebarViewsState;
6
+ /** `panel` slides the whole panel and owns the swipe; `body` slides the menu inside it. */
7
+ kind: SidebarViewKind;
8
+ layers: SidebarViewLayer[];
9
+ /** Classes each layer wears on top of `viewLayer`. */
10
+ layerClass?: string;
11
+ /** Landmark name; the body stage is the nav landmark. */
12
+ label?: string;
13
+ theme?: SidebarThemeProps;
14
+ children: Snippet<[layer: SidebarViewLayer]>;
15
+ };
16
+ declare const SidebarViewStage: import("svelte").Component<$$ComponentProps, {}, "">;
17
+ type SidebarViewStage = ReturnType<typeof SidebarViewStage>;
18
+ export default SidebarViewStage;
@@ -1,5 +1,5 @@
1
1
  export { default as Sidebar } from './Sidebar.svelte';
2
2
  export { default as SidebarMenuButton } from './SidebarMenuButton.svelte';
3
- export type { SidebarActiveVariant, SidebarActivityBar, SidebarActivityBarItem, SidebarActivityBarSelectPayload, SidebarApi, SidebarCollapsible, SidebarDisplayState, SidebarDensity, SidebarGroup, SidebarIcon, SidebarIconVariant, SidebarFrame, SidebarMenuActionDescriptor, SidebarMenuAlign, SidebarMenuButtonItem, SidebarMenuButtonSize, SidebarMenuButtonVariant, SidebarMenuEntry, SidebarMenuSide, SidebarMenuSubEntry, SidebarProps, SidebarRail, SidebarResizable, SidebarResizableOptions, SidebarSearch, SidebarSide, SidebarSize, SidebarMode, SidebarState, SidebarTooltipMode, SidebarTreeNode, SidebarVariant, SidebarWidthChangePayload } from './sidebar.props.js';
3
+ export type { SidebarActiveVariant, SidebarActivityBar, SidebarActivityBarItem, SidebarActivityBarSelectPayload, SidebarApi, SidebarCollapsible, SidebarDisplayState, SidebarDensity, SidebarGroup, SidebarIcon, SidebarIconVariant, SidebarFrame, SidebarMenuActionDescriptor, SidebarMenuAlign, SidebarMenuButtonItem, SidebarMenuButtonSize, SidebarMenuButtonVariant, SidebarMenuEntry, SidebarMenuSide, SidebarMenuSubEntry, SidebarProps, SidebarRail, SidebarResizable, SidebarResizableOptions, SidebarSearch, SidebarSide, SidebarSize, SidebarMode, SidebarState, SidebarTooltipMode, SidebarTreeNode, SidebarVariant, SidebarView, SidebarWidthChangePayload } from './sidebar.props.js';
4
4
  export { sidebarTheme, setSidebarTheme, useSidebarTheme, type SidebarTheme, type SidebarThemeProps } from './sidebar.theme.js';
5
5
  export { sidebarDescription } from './sidebar.mcp.js';
@@ -6,5 +6,10 @@ export declare function getSidebarContainerClass(side: SidebarSide, variant: Sid
6
6
  * panels already own a border and shadow, exactly as the edge-reveal path leaves them alone.
7
7
  */
8
8
  export declare function getSidebarPanelPeekClass(variant: SidebarVariant): "group-data-[peek=true]:raised-3" | undefined;
9
- /** Fixed/absolute placement for the activity bar column, pinned outside the panel. */
10
- export declare function getSidebarActivityBarContainerClass(side: SidebarSide, frame: SidebarFrame): string;
9
+ /**
10
+ * Fixed/absolute placement for the activity bar column, pinned outside the panel. The column is
11
+ * the reserved offset, and it takes the panel container's gutters so the rail lines up with the
12
+ * panel beside it: none for admin and framed, a vertical one for inset, the outer and vertical
13
+ * ones for floating and split (the panel's own gutter spaces the pair).
14
+ */
15
+ export declare function getSidebarActivityBarContainerClass(side: SidebarSide, frame: SidebarFrame, variant: SidebarVariant): string;
@@ -25,10 +25,16 @@ export function getSidebarContainerClass(side, variant, isEdgeRevealed, frame) {
25
25
  const panelOwnsShadow = variant === 'floating' || variant === 'split';
26
26
  return cx('inset-y-0 z-10 hidden w-[var(--sidebar-width)] bg-transparent transition-[left,right,width] duration-normal ease-linear group-data-[width-prehydrating=true]/sidebar-wrapper:!transition-none group-data-[resizing=true]:!transition-none md:flex', frame === 'viewport' ? 'fixed h-window' : 'absolute h-full',
27
27
  // The offcanvas offset slides the panel away relative to itself: the activity bar keeps
28
- // its own inset, so it never leaves the screen with the panel.
28
+ // its own inset, so it never leaves the screen with the panel. Floating and split park it
29
+ // fully past the edge instead: their rail stands in a gutter, and a panel parked under it
30
+ // would show through that gutter.
29
31
  side === 'left'
30
- ? 'left-[var(--sidebar-activity-offset,0px)] group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-activity-offset,0px)-var(--sidebar-width))]'
31
- : 'right-[var(--sidebar-activity-offset,0px)] group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-activity-offset,0px)-var(--sidebar-width))]', getContainerGeometryClass(variant),
32
+ ? cx('left-[var(--sidebar-activity-offset,0px)]', panelOwnsShadow
33
+ ? 'group-data-[collapsible=offcanvas]:-left-[var(--sidebar-width)]'
34
+ : 'group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-activity-offset,0px)-var(--sidebar-width))]')
35
+ : cx('right-[var(--sidebar-activity-offset,0px)]', panelOwnsShadow
36
+ ? 'group-data-[collapsible=offcanvas]:-right-[var(--sidebar-width)]'
37
+ : 'group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-activity-offset,0px)-var(--sidebar-width))]'), getContainerGeometryClass(variant),
32
38
  // Hover peek: full width over the page, lifted so it reads as a temporary drawer. The
33
39
  // elevation itself belongs to the panel (see `getSidebarPanelPeekClass`), not to this
34
40
  // transparent wrapper with its padding gutter.
@@ -46,7 +52,12 @@ export function getSidebarPanelPeekClass(variant) {
46
52
  const panelOwnsShadow = variant === 'floating' || variant === 'split';
47
53
  return panelOwnsShadow ? undefined : 'group-data-[peek=true]:raised-3';
48
54
  }
49
- /** Fixed/absolute placement for the activity bar column, pinned outside the panel. */
50
- export function getSidebarActivityBarContainerClass(side, frame) {
51
- return cx('inset-y-0 z-30 hidden w-[var(--sidebar-width-activity,3rem)] md:flex', frame === 'viewport' ? 'fixed h-window' : 'absolute h-full', side === 'left' ? 'left-0' : 'right-0');
55
+ /**
56
+ * Fixed/absolute placement for the activity bar column, pinned outside the panel. The column is
57
+ * the reserved offset, and it takes the panel container's gutters so the rail lines up with the
58
+ * panel beside it: none for admin and framed, a vertical one for inset, the outer and vertical
59
+ * ones for floating and split (the panel's own gutter spaces the pair).
60
+ */
61
+ export function getSidebarActivityBarContainerClass(side, frame, variant) {
62
+ return cx('inset-y-0 z-30 hidden w-[var(--sidebar-activity-offset,3rem)] md:flex', frame === 'viewport' ? 'fixed h-window' : 'absolute h-full', side === 'left' ? 'left-0' : 'right-0', variant === 'inset' && 'py-2', (variant === 'floating' || variant === 'split') && (side === 'left' ? 'py-2 pl-2' : 'py-2 pr-2'));
52
63
  }