entasis 0.7.1 → 0.9.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/README.md +7 -1
- package/dist/components/AppShell/appShell.theme.js +2 -2
- 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/Form/Form/form.state.svelte.d.ts +4 -0
- package/dist/components/Form/Form/visibility.d.ts +2 -0
- package/dist/components/Form/MultiStepForm/multiStepForm.state.svelte.d.ts +4 -0
- package/dist/components/Form/Select/Select.svelte +28 -2
- package/dist/components/Form/Select/select.align.d.ts +55 -0
- package/dist/components/Form/Select/select.align.js +41 -0
- package/dist/components/Form/Select/select.mcp.d.ts +1 -1
- package/dist/components/Form/Select/select.mcp.js +7 -3
- package/dist/components/Form/Select/select.props.d.ts +8 -0
- package/dist/components/Form/Select/select.state.svelte.d.ts +24 -0
- package/dist/components/Form/Select/select.state.svelte.js +111 -1
- package/dist/components/Popover/Popover.svelte +4 -0
- package/dist/components/Popover/popover.mcp.d.ts +1 -1
- package/dist/components/Popover/popover.mcp.js +1 -0
- package/dist/components/Popover/popover.props.d.ts +16 -0
- package/dist/components/Popover/popover.state.svelte.d.ts +1 -1
- package/dist/components/Popover/popover.state.svelte.js +13 -0
- package/dist/components/Sidebar/Sidebar.svelte +58 -10
- package/dist/components/Sidebar/Sidebar.svelte.d.ts +1 -1
- 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.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 +128 -5
- package/dist/components/Sidebar/sidebar.theme.js +69 -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 +6 -6
- 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
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import { SidebarResizeState } from './sidebar.resize.svelte.js';
|
|
19
19
|
import { SidebarStateController } from './sidebar.state.svelte.js';
|
|
20
20
|
import { useSidebarTheme } from './sidebar.theme.js';
|
|
21
|
+
import { SidebarViewsState } from './sidebar.views.svelte.js';
|
|
21
22
|
import { useDefaultColor } from '../Theme/theme.state.svelte.js';
|
|
22
23
|
let {
|
|
23
24
|
id: customId,
|
|
@@ -47,6 +48,10 @@
|
|
|
47
48
|
expandOnHover = false,
|
|
48
49
|
activityBar,
|
|
49
50
|
items,
|
|
51
|
+
views,
|
|
52
|
+
view = $bindable(),
|
|
53
|
+
defaultView,
|
|
54
|
+
onViewChange,
|
|
50
55
|
class: className,
|
|
51
56
|
collapseIcon = 'chevron',
|
|
52
57
|
tooltips = 'auto',
|
|
@@ -111,19 +116,26 @@
|
|
|
111
116
|
const navLabel = $derived(`${t.sidebar} ${t.navigation}`);
|
|
112
117
|
const classes = $derived(useSidebarTheme(theme));
|
|
113
118
|
const resolvedColor = $derived(useDefaultColor());
|
|
119
|
+
// Every view counts, not just the one on screen: icon collapse must not come and go as the
|
|
120
|
+
// view changes.
|
|
114
121
|
const canCollapseToIcon = $derived.by(() => {
|
|
115
|
-
|
|
122
|
+
const viewList = Object.values(views ?? {});
|
|
123
|
+
if (viewList.length ? viewList.some((entry) => entry.content) : content) return false;
|
|
116
124
|
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
...(
|
|
120
|
-
...(
|
|
121
|
-
|
|
125
|
+
const sources = [{ headerMenu, items, footerMenu, headerButton, footerButton }, ...viewList];
|
|
126
|
+
const menuEntries = sources.flatMap((source) => [
|
|
127
|
+
...(source.headerMenu ?? []),
|
|
128
|
+
...(source.items?.flatMap((group) => group.items ?? []) ?? []),
|
|
129
|
+
...(source.footerMenu ?? [])
|
|
130
|
+
]);
|
|
122
131
|
|
|
123
132
|
return (
|
|
124
133
|
menuEntries.every(hasIcon) &&
|
|
125
|
-
|
|
126
|
-
|
|
134
|
+
sources.every(
|
|
135
|
+
(source) =>
|
|
136
|
+
hasCollapsibleMedia(source.headerButton ?? undefined) &&
|
|
137
|
+
hasCollapsibleMedia(source.footerButton ?? undefined)
|
|
138
|
+
)
|
|
127
139
|
);
|
|
128
140
|
});
|
|
129
141
|
const resolvedCollapsible = $derived(
|
|
@@ -150,6 +162,32 @@
|
|
|
150
162
|
return onDisplayStateChange;
|
|
151
163
|
}
|
|
152
164
|
});
|
|
165
|
+
const panelViews = new SidebarViewsState({
|
|
166
|
+
get views() {
|
|
167
|
+
return views;
|
|
168
|
+
},
|
|
169
|
+
get view() {
|
|
170
|
+
return view;
|
|
171
|
+
},
|
|
172
|
+
get defaultView() {
|
|
173
|
+
return defaultView;
|
|
174
|
+
},
|
|
175
|
+
get onViewChange() {
|
|
176
|
+
return onViewChange;
|
|
177
|
+
},
|
|
178
|
+
get isMobile(): boolean {
|
|
179
|
+
return controller.isMobile;
|
|
180
|
+
},
|
|
181
|
+
get side() {
|
|
182
|
+
return side;
|
|
183
|
+
},
|
|
184
|
+
get root() {
|
|
185
|
+
return { headerButton, search, headerMenu, header, footerButton, footerMenu, footer };
|
|
186
|
+
},
|
|
187
|
+
setViewProp: (next) => {
|
|
188
|
+
view = next;
|
|
189
|
+
}
|
|
190
|
+
});
|
|
153
191
|
const controller = new SidebarStateController({
|
|
154
192
|
get mode() {
|
|
155
193
|
return mode;
|
|
@@ -169,7 +207,11 @@
|
|
|
169
207
|
get peeking() {
|
|
170
208
|
return hoverExpanded;
|
|
171
209
|
},
|
|
172
|
-
|
|
210
|
+
get view(): string | undefined {
|
|
211
|
+
return panelViews.current;
|
|
212
|
+
},
|
|
213
|
+
setDisplayState: displayStateBridge.setDisplayState,
|
|
214
|
+
setView: panelViews.setView
|
|
173
215
|
});
|
|
174
216
|
const resize = new SidebarResizeState({
|
|
175
217
|
get width() {
|
|
@@ -199,6 +241,9 @@
|
|
|
199
241
|
mode === 'panel' && controller.displayState === 'collapsed' ? widthIcon : width
|
|
200
242
|
);
|
|
201
243
|
const withBanner = $derived(!!banner);
|
|
244
|
+
// The page fills the frame only when nothing of the sidebar stays beside it: the panel is
|
|
245
|
+
// hidden and no activity bar remains on screen.
|
|
246
|
+
const pageFlush = $derived(controller.displayState === 'hidden' && !activityBar);
|
|
202
247
|
const rootClass = $derived(
|
|
203
248
|
classes.root({
|
|
204
249
|
variant,
|
|
@@ -251,6 +296,7 @@
|
|
|
251
296
|
{activeVariant}
|
|
252
297
|
{density}
|
|
253
298
|
label={navLabel}
|
|
299
|
+
views={panelViews.enabled ? panelViews : undefined}
|
|
254
300
|
{theme}
|
|
255
301
|
/>
|
|
256
302
|
{/snippet}
|
|
@@ -309,6 +355,7 @@
|
|
|
309
355
|
data-size={size}
|
|
310
356
|
data-density={density}
|
|
311
357
|
data-activity-bar={activityBar ? 'true' : undefined}
|
|
358
|
+
data-page-flush={pageFlush ? 'true' : undefined}
|
|
312
359
|
data-width-prehydrating={resize.isWidthInitializing ? 'true' : undefined}
|
|
313
360
|
style:--sidebar-width={resize.renderWidth}
|
|
314
361
|
style:--sidebar-width-icon={widthIcon}
|
|
@@ -399,7 +446,8 @@
|
|
|
399
446
|
variant,
|
|
400
447
|
side,
|
|
401
448
|
displayState: controller.displayState,
|
|
402
|
-
edgeRevealed
|
|
449
|
+
edgeRevealed,
|
|
450
|
+
flush: pageFlush
|
|
403
451
|
})}
|
|
404
452
|
>
|
|
405
453
|
{@render children?.(api)}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { SidebarProps } from './sidebar.props.js';
|
|
2
|
-
declare const Sidebar: import("svelte").Component<SidebarProps, {}, "width" | "ref" | "open" | "displayState">;
|
|
2
|
+
declare const Sidebar: import("svelte").Component<SidebarProps, {}, "view" | "width" | "ref" | "open" | "displayState">;
|
|
3
3
|
type Sidebar = ReturnType<typeof Sidebar>;
|
|
4
4
|
export default Sidebar;
|
|
@@ -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>
|