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.
- package/dist/components/FloatingWindow/FloatingWindow.svelte +26 -2
- package/dist/components/FloatingWindow/floatingWindow.dock.svelte.js +11 -2
- package/dist/components/FloatingWindow/floatingWindow.mcp.d.ts +1 -1
- package/dist/components/FloatingWindow/floatingWindow.mcp.js +7 -4
- package/dist/components/FloatingWindow/floatingWindow.props.d.ts +6 -0
- package/dist/components/FloatingWindow/floatingWindow.state.svelte.d.ts +3 -0
- package/dist/components/FloatingWindow/floatingWindow.state.svelte.js +18 -4
- package/dist/components/FloatingWindow/floatingWindow.theme.d.ts +3 -0
- package/dist/components/FloatingWindow/floatingWindow.theme.js +20 -7
- package/dist/components/Form/File/FileInput.svelte +6 -42
- package/dist/components/Sidebar/Sidebar.svelte +72 -12
- package/dist/components/Sidebar/Sidebar.svelte.d.ts +1 -1
- package/dist/components/Sidebar/SidebarActivityBar.svelte +9 -2
- package/dist/components/Sidebar/SidebarActivityBar.svelte.d.ts +5 -1
- package/dist/components/Sidebar/SidebarDesktopShell.svelte +12 -4
- package/dist/components/Sidebar/SidebarMenuItem.svelte +19 -2
- package/dist/components/Sidebar/SidebarMenuItem.svelte.d.ts +4 -0
- package/dist/components/Sidebar/SidebarPanel.svelte +199 -88
- package/dist/components/Sidebar/SidebarPanel.svelte.d.ts +3 -0
- package/dist/components/Sidebar/SidebarViewStage.svelte +103 -0
- package/dist/components/Sidebar/SidebarViewStage.svelte.d.ts +18 -0
- package/dist/components/Sidebar/index.d.ts +1 -1
- package/dist/components/Sidebar/sidebar-layout.d.ts +7 -2
- package/dist/components/Sidebar/sidebar-layout.js +17 -6
- package/dist/components/Sidebar/sidebar.mcp.d.ts +1 -1
- package/dist/components/Sidebar/sidebar.mcp.js +19 -2
- package/dist/components/Sidebar/sidebar.props.d.ts +56 -1
- package/dist/components/Sidebar/sidebar.state.svelte.d.ts +4 -0
- package/dist/components/Sidebar/sidebar.state.svelte.js +9 -1
- package/dist/components/Sidebar/sidebar.theme.d.ts +149 -5
- package/dist/components/Sidebar/sidebar.theme.js +96 -3
- package/dist/components/Sidebar/sidebar.views.svelte.d.ts +138 -0
- package/dist/components/Sidebar/sidebar.views.svelte.js +303 -0
- package/dist/components/Theme/theme.floatingWindows.d.ts +6 -1
- package/dist/components/Theme/theme.floatingWindows.js +7 -2
- package/dist/components/Theme/theme.mcp.d.ts +1 -1
- package/dist/components/Theme/theme.mcp.js +1 -0
- package/dist/generated/componentContract.d.ts +1 -1
- package/dist/generated/componentContract.js +1 -0
- package/dist/generated/componentMcpRegistry.d.ts +4 -4
- package/dist/tailwind/index.mcp.d.ts +1 -1
- package/dist/tailwind/index.mcp.js +4 -0
- package/dist/tailwind/scales.js +11 -3
- package/dist/tailwind/spacing.js +17 -0
- package/dist/utils/cva/merge.d.ts +7 -0
- package/dist/utils/cva/merge.js +6 -1
- package/dist/utils/pointerDrag.js +7 -1
- 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
|
|
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-
|
|
383
|
-
: '!right-[var(--sidebar-
|
|
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(
|
|
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
|
-
{#
|
|
89
|
-
<
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
{
|
|
95
|
-
|
|
96
|
-
{
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
{
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
{
|
|
127
|
-
{
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
{
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
211
|
+
{/snippet}
|
|
168
212
|
|
|
169
|
-
{#
|
|
170
|
-
|
|
171
|
-
{
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
/**
|
|
10
|
-
|
|
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)]
|
|
31
|
-
|
|
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
|
-
/**
|
|
50
|
-
|
|
51
|
-
|
|
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
|
}
|