@classic-homes/theme-svelte 1.15.2 → 1.16.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/__tests__/public-layout-drawer.test.js +116 -0
- package/dist/lib/components/DataPanel.svelte +13 -3
- package/dist/lib/components/layout/Header.svelte +3 -1
- package/dist/lib/components/layout/PublicLayout.svelte +151 -44
- package/dist/lib/components/layout/PublicLayout.svelte.d.ts +8 -10
- package/dist/lib/components/layout/Sidebar.svelte +7 -2
- package/package.json +3 -1
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PublicLayout mobileNavStyle="drawer": the hamburger opens the Sidebar drawer
|
|
3
|
+
* over the page instead of expanding a dropdown inside the header (which pushed
|
|
4
|
+
* the page down and, once scrolled, sat on the header's translucent bar).
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
7
|
+
import { mount, unmount, flushSync, createRawSnippet } from 'svelte';
|
|
8
|
+
import PublicLayout from '../lib/components/layout/PublicLayout.svelte';
|
|
9
|
+
const children = createRawSnippet(() => ({ render: () => '<p>Page</p>' }));
|
|
10
|
+
const navigation = [
|
|
11
|
+
{ id: 'inventory', name: 'Inventory', href: '/inventory', active: true },
|
|
12
|
+
{ id: 'pricing', name: 'Pricing', href: '/pricing' },
|
|
13
|
+
];
|
|
14
|
+
let instance = null;
|
|
15
|
+
let host;
|
|
16
|
+
function phoneWidth(matches) {
|
|
17
|
+
vi.spyOn(window, 'matchMedia').mockImplementation((query) => ({
|
|
18
|
+
matches: query.startsWith('(max-width') ? matches : false,
|
|
19
|
+
media: query,
|
|
20
|
+
onchange: null,
|
|
21
|
+
addEventListener: vi.fn(),
|
|
22
|
+
removeEventListener: vi.fn(),
|
|
23
|
+
addListener: vi.fn(),
|
|
24
|
+
removeListener: vi.fn(),
|
|
25
|
+
dispatchEvent: vi.fn(),
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
function mountLayout(props) {
|
|
29
|
+
host = document.createElement('div');
|
|
30
|
+
document.body.appendChild(host);
|
|
31
|
+
instance = mount(PublicLayout, { target: host, props: { navigation, children, ...props } });
|
|
32
|
+
flushSync();
|
|
33
|
+
const hamburger = () => host.querySelector('button[aria-label="Open menu"]');
|
|
34
|
+
return {
|
|
35
|
+
open: () => {
|
|
36
|
+
hamburger().click();
|
|
37
|
+
flushSync();
|
|
38
|
+
},
|
|
39
|
+
drawer: () => host.querySelector('aside[aria-label="Main navigation"]'),
|
|
40
|
+
dropdown: () => host.querySelector('nav[aria-label="Mobile navigation"]'),
|
|
41
|
+
header: () => host.querySelector('header'),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
if (instance)
|
|
46
|
+
unmount(instance);
|
|
47
|
+
instance = null;
|
|
48
|
+
host?.remove();
|
|
49
|
+
vi.restoreAllMocks();
|
|
50
|
+
});
|
|
51
|
+
describe('PublicLayout drawer nav', () => {
|
|
52
|
+
it('opens the Sidebar drawer over the page on a phone', () => {
|
|
53
|
+
phoneWidth(true);
|
|
54
|
+
const layout = mountLayout({ mobileNavStyle: 'drawer' });
|
|
55
|
+
// Closed drawer is off-canvas (jsdom does not reflect `inert`)
|
|
56
|
+
expect(layout.drawer()?.className).toContain('-translate-x-full');
|
|
57
|
+
layout.open();
|
|
58
|
+
expect(layout.drawer()?.className).not.toContain('-translate-x-full');
|
|
59
|
+
expect(layout.drawer()?.textContent).toContain('Pricing');
|
|
60
|
+
// No dropdown inside the header
|
|
61
|
+
expect(layout.dropdown()?.textContent ?? '').not.toContain('Pricing');
|
|
62
|
+
// Drawer and scrim lift over the elevated header and page sheets
|
|
63
|
+
const layer = layout.drawer().parentElement;
|
|
64
|
+
expect(layer.style.getPropertyValue('--z-sidebar')).toBe('70');
|
|
65
|
+
expect(layer.querySelector('[aria-hidden="true"].fixed.inset-0')).not.toBeNull();
|
|
66
|
+
});
|
|
67
|
+
it('renders no drawer above the breakpoint', () => {
|
|
68
|
+
phoneWidth(false);
|
|
69
|
+
const layout = mountLayout({ mobileNavStyle: 'drawer' });
|
|
70
|
+
expect(layout.drawer()).toBeNull();
|
|
71
|
+
});
|
|
72
|
+
it('keeps the dropdown by default, on a solid background', () => {
|
|
73
|
+
phoneWidth(true);
|
|
74
|
+
const layout = mountLayout({});
|
|
75
|
+
layout.open();
|
|
76
|
+
expect(layout.drawer()).toBeNull();
|
|
77
|
+
expect(layout.dropdown()?.textContent).toContain('Pricing');
|
|
78
|
+
expect(layout.dropdown()?.className).toContain('bg-background');
|
|
79
|
+
expect(layout.header().className).toContain('z-50');
|
|
80
|
+
});
|
|
81
|
+
it('gives a parent with children a chevron menu in the header, and keeps the link', () => {
|
|
82
|
+
phoneWidth(false);
|
|
83
|
+
host = document.createElement('div');
|
|
84
|
+
document.body.appendChild(host);
|
|
85
|
+
instance = mount(PublicLayout, {
|
|
86
|
+
target: host,
|
|
87
|
+
props: {
|
|
88
|
+
children,
|
|
89
|
+
navigation: [
|
|
90
|
+
{
|
|
91
|
+
id: 'inventory',
|
|
92
|
+
name: 'Inventory',
|
|
93
|
+
href: '/inventory',
|
|
94
|
+
active: true,
|
|
95
|
+
children: [
|
|
96
|
+
{ id: 'inventory-community', name: 'Community', href: '/inventory?view=community' },
|
|
97
|
+
{
|
|
98
|
+
id: 'inventory-collection',
|
|
99
|
+
name: 'Collection',
|
|
100
|
+
href: '/inventory?view=collection',
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
{ id: 'pricing', name: 'Pricing', href: '/pricing' },
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
flushSync();
|
|
109
|
+
const link = host.querySelector('header a[href="/inventory"]');
|
|
110
|
+
expect(link?.getAttribute('aria-current')).toBe('page');
|
|
111
|
+
const trigger = host.querySelector('header button[aria-label="Inventory options"]');
|
|
112
|
+
expect(trigger).not.toBeNull();
|
|
113
|
+
// Only parents with children get one
|
|
114
|
+
expect(host.querySelector('header button[aria-label="Pricing options"]')).toBeNull();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -280,6 +280,16 @@
|
|
|
280
280
|
*/
|
|
281
281
|
const isMobileCard = $derived(variant === 'card' && isMobile);
|
|
282
282
|
|
|
283
|
+
/**
|
|
284
|
+
* Bottom padding of the phone card: the home-indicator inset less the
|
|
285
|
+
* header's own 12px bottom padding and 8px more. The whole 34px iPhone inset
|
|
286
|
+
* stood the collapsed card 106px tall; this keeps the title about 26px above
|
|
287
|
+
* the screen edge, clear of the indicator, at 86px. The strip below the
|
|
288
|
+
* header is padding, so a swipe that starts there never reaches the header's
|
|
289
|
+
* pointer handler.
|
|
290
|
+
*/
|
|
291
|
+
const CARD_SAFE_AREA_PAD = 'max(0px, calc(env(safe-area-inset-bottom, 0px) - 20px))';
|
|
292
|
+
|
|
283
293
|
// Whether the panel is currently presented as a modal dialog (detached, or an
|
|
284
294
|
// expanded mobile card). In that state we trap focus for a11y parity with React.
|
|
285
295
|
// A desktop card is a non-modal floating sheet with no scrim, so it must not
|
|
@@ -1193,15 +1203,15 @@
|
|
|
1193
1203
|
// Full-width phone sheet at its expanded height; `height` above is how
|
|
1194
1204
|
// much of it shows. The part below the fold is slid off-screen with
|
|
1195
1205
|
// `translate` (composited, and it composes with the mount keyframe's
|
|
1196
|
-
// `transform`).
|
|
1206
|
+
// `transform`). CARD_SAFE_AREA_PAD is added to the sheet and padded
|
|
1197
1207
|
// out, so the collapsed header sits above the home indicator instead
|
|
1198
1208
|
// of inside the strip iOS claims for its swipe-to-leave gesture.
|
|
1199
1209
|
return {
|
|
1200
1210
|
bottom: '0',
|
|
1201
1211
|
left: `${defaultLeftOffset}px`,
|
|
1202
1212
|
width: `${defaultCardWidth}px`,
|
|
1203
|
-
height: `calc(${cardExpandedHeight}px +
|
|
1204
|
-
'padding-bottom':
|
|
1213
|
+
height: `calc(${cardExpandedHeight}px + ${CARD_SAFE_AREA_PAD})`,
|
|
1214
|
+
'padding-bottom': CARD_SAFE_AREA_PAD,
|
|
1205
1215
|
translate: `0 ${Math.max(0, cardExpandedHeight - height)}px`,
|
|
1206
1216
|
};
|
|
1207
1217
|
}
|
|
@@ -283,7 +283,9 @@
|
|
|
283
283
|
|
|
284
284
|
<!-- Mobile Navigation Drawer (appears below header bar when open) -->
|
|
285
285
|
{#if mobileNav && menuOpen}
|
|
286
|
-
|
|
286
|
+
<!-- Solid, unlike the translucent bar: the dropdown overlays the page once
|
|
287
|
+
the sticky header has scrolled -->
|
|
288
|
+
<nav class={cn(bp.mobileNavHide, 'bg-background')} aria-label="Mobile navigation">
|
|
287
289
|
{@render mobileNav()}
|
|
288
290
|
</nav>
|
|
289
291
|
{/if}
|
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
* - Main content area
|
|
9
9
|
* - Responsive design
|
|
10
10
|
* - Strong border options for brand consistency
|
|
11
|
+
* - Mobile nav as a dropdown under the header bar, or (`mobileNavStyle="drawer"`)
|
|
12
|
+
* the slide-out Sidebar drawer DashboardLayout apps use
|
|
11
13
|
*/
|
|
14
|
+
import { onMount } from 'svelte';
|
|
12
15
|
import type { Snippet } from 'svelte';
|
|
13
16
|
import type { NavItem, NavSection, HeaderSearchConfig } from '../../types/layout.js';
|
|
14
17
|
import { cn } from '../../utils.js';
|
|
@@ -16,6 +19,9 @@
|
|
|
16
19
|
import Footer from './Footer.svelte';
|
|
17
20
|
import Header from './Header.svelte';
|
|
18
21
|
import LogoMain from '../LogoMain.svelte';
|
|
22
|
+
import Sidebar from './Sidebar.svelte';
|
|
23
|
+
import DropdownMenu from '../DropdownMenu.svelte';
|
|
24
|
+
import Icon, { type IconName } from '../Icon.svelte';
|
|
19
25
|
|
|
20
26
|
type Breakpoint = 'sm' | 'md' | 'lg';
|
|
21
27
|
|
|
@@ -28,6 +34,14 @@
|
|
|
28
34
|
* more than ~768px: at iPad portrait widths (820px) they otherwise overlap.
|
|
29
35
|
*/
|
|
30
36
|
mobileBreakpoint?: Breakpoint;
|
|
37
|
+
/**
|
|
38
|
+
* How the hamburger opens the nav below `mobileBreakpoint`. `dropdown`
|
|
39
|
+
* (default) expands under the header bar, pushing the page down when the
|
|
40
|
+
* header is at the top. `drawer` slides the Sidebar in over the page with a
|
|
41
|
+
* scrim - the same drawer, focus trap, Escape and swipe-to-close as
|
|
42
|
+
* DashboardLayout apps.
|
|
43
|
+
*/
|
|
44
|
+
mobileNavStyle?: 'dropdown' | 'drawer';
|
|
31
45
|
/** Whether to show footer */
|
|
32
46
|
showFooter?: boolean;
|
|
33
47
|
/** Footer link sections */
|
|
@@ -64,6 +78,7 @@
|
|
|
64
78
|
let {
|
|
65
79
|
navigation = [],
|
|
66
80
|
mobileBreakpoint = 'md',
|
|
81
|
+
mobileNavStyle = 'dropdown',
|
|
67
82
|
showFooter = true,
|
|
68
83
|
footerLinks = [],
|
|
69
84
|
copyright,
|
|
@@ -81,6 +96,27 @@
|
|
|
81
96
|
|
|
82
97
|
let mobileMenuOpen = $state(false);
|
|
83
98
|
|
|
99
|
+
const breakpointPx: Record<Breakpoint, number> = { sm: 640, md: 768, lg: 1024 };
|
|
100
|
+
|
|
101
|
+
// The drawer only exists below the breakpoint: the Sidebar is a fixed
|
|
102
|
+
// desktop column otherwise. Starts false, so SSR and the first paint render
|
|
103
|
+
// no drawer; the hamburger it pairs with is hidden above the breakpoint.
|
|
104
|
+
let belowBreakpoint = $state(false);
|
|
105
|
+
onMount(() => {
|
|
106
|
+
const query = window.matchMedia(`(max-width: ${breakpointPx[mobileBreakpoint] - 0.02}px)`);
|
|
107
|
+
const update = () => {
|
|
108
|
+
belowBreakpoint = query.matches;
|
|
109
|
+
if (!query.matches) mobileMenuOpen = false;
|
|
110
|
+
};
|
|
111
|
+
update();
|
|
112
|
+
query.addEventListener('change', update);
|
|
113
|
+
return () => query.removeEventListener('change', update);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const useDrawer = $derived(mobileNavStyle === 'drawer');
|
|
117
|
+
const drawerNavigation = $derived<NavSection[]>([{ id: 'main', items: navigation }]);
|
|
118
|
+
const closeMenu = () => (mobileMenuOpen = false);
|
|
119
|
+
|
|
84
120
|
// Full class strings so Tailwind's scanner sees them.
|
|
85
121
|
const desktopOnly: Record<Breakpoint, string> = {
|
|
86
122
|
sm: 'hidden sm:contents',
|
|
@@ -89,6 +125,51 @@
|
|
|
89
125
|
};
|
|
90
126
|
</script>
|
|
91
127
|
|
|
128
|
+
<!-- One header nav link -->
|
|
129
|
+
{#snippet navLink(item: NavItem)}
|
|
130
|
+
<a
|
|
131
|
+
href={item.href}
|
|
132
|
+
class={cn(
|
|
133
|
+
// Touch target: p-3 around the 20px line box makes the anchor's own
|
|
134
|
+
// border box 44px tall, and -m-3 cancels it in layout so the nav's
|
|
135
|
+
// gap-6 is unchanged (adjacent boxes meet without overlapping).
|
|
136
|
+
'-m-3 flex items-center p-3 text-sm font-bold uppercase hover:text-primary',
|
|
137
|
+
// iPadOS reports hover:none even with a trackpad attached, so the preset's
|
|
138
|
+
// hoverOnlyWhenSupported gating means hover: never fires there. any-pointer:fine
|
|
139
|
+
// matches when any attached pointer is precise, giving trackpad users a cue.
|
|
140
|
+
'[@media(any-pointer:fine)]:[&:hover]:text-primary',
|
|
141
|
+
item.active ? 'text-primary' : 'text-muted-foreground'
|
|
142
|
+
)}
|
|
143
|
+
target={item.external ? '_blank' : undefined}
|
|
144
|
+
rel={item.external ? 'noopener noreferrer' : undefined}
|
|
145
|
+
aria-current={item.active ? 'page' : undefined}
|
|
146
|
+
>
|
|
147
|
+
{item.name}
|
|
148
|
+
</a>
|
|
149
|
+
{/snippet}
|
|
150
|
+
|
|
151
|
+
<!-- The drawer's footer: mobileNavEnd, closing the drawer after use -->
|
|
152
|
+
{#snippet drawerFooter()}
|
|
153
|
+
{@render mobileNavEnd?.({ close: closeMenu })}
|
|
154
|
+
{/snippet}
|
|
155
|
+
|
|
156
|
+
<!-- The drawer's logo: PublicLayout's own logo, closing the drawer on tap -->
|
|
157
|
+
{#snippet drawerLogo()}
|
|
158
|
+
<a href={logoHref} onclick={closeMenu} class="flex items-center">
|
|
159
|
+
{#if logo}
|
|
160
|
+
{@render logo()}
|
|
161
|
+
{:else}
|
|
162
|
+
<LogoMain
|
|
163
|
+
variant="horizontal"
|
|
164
|
+
color="dark"
|
|
165
|
+
size="md"
|
|
166
|
+
subtitle={logoSubtitle}
|
|
167
|
+
environment={logoEnvironment}
|
|
168
|
+
/>
|
|
169
|
+
{/if}
|
|
170
|
+
</a>
|
|
171
|
+
{/snippet}
|
|
172
|
+
|
|
92
173
|
<AppShell>
|
|
93
174
|
<Header
|
|
94
175
|
showMenuButton={navigation.length > 0}
|
|
@@ -123,25 +204,34 @@
|
|
|
123
204
|
|
|
124
205
|
{#snippet nav()}
|
|
125
206
|
{#each navigation as item}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
207
|
+
{#if item.children?.length}
|
|
208
|
+
<!-- Parent link plus a chevron menu of its children (e.g. the same
|
|
209
|
+
page opened in another view). The drawer shows the same children
|
|
210
|
+
as an expandable sub-list. -->
|
|
211
|
+
<div class="flex items-center gap-0.5">
|
|
212
|
+
{@render navLink(item)}
|
|
213
|
+
<DropdownMenu
|
|
214
|
+
items={item.children.map((child) => ({
|
|
215
|
+
label: child.name,
|
|
216
|
+
href: child.href,
|
|
217
|
+
icon: child.icon as IconName | undefined,
|
|
218
|
+
}))}
|
|
219
|
+
align="end"
|
|
220
|
+
triggerLabel={`${item.name} options`}
|
|
221
|
+
triggerClass={cn(
|
|
222
|
+
'-my-3 -mr-2 flex items-center rounded-md p-2 hover:text-primary',
|
|
223
|
+
'[@media(any-pointer:fine)]:[&:hover]:text-primary',
|
|
224
|
+
item.active ? 'text-primary' : 'text-muted-foreground'
|
|
225
|
+
)}
|
|
226
|
+
>
|
|
227
|
+
{#snippet trigger()}
|
|
228
|
+
<Icon name="chevron-down" size={16} aria-hidden="true" />
|
|
229
|
+
{/snippet}
|
|
230
|
+
</DropdownMenu>
|
|
231
|
+
</div>
|
|
232
|
+
{:else}
|
|
233
|
+
{@render navLink(item)}
|
|
234
|
+
{/if}
|
|
145
235
|
{/each}
|
|
146
236
|
{/snippet}
|
|
147
237
|
|
|
@@ -156,36 +246,53 @@
|
|
|
156
246
|
{/if}
|
|
157
247
|
{/snippet}
|
|
158
248
|
|
|
249
|
+
<!-- Dropdown style only; the drawer renders after the header -->
|
|
159
250
|
{#snippet mobileNav()}
|
|
160
|
-
|
|
161
|
-
{
|
|
162
|
-
|
|
163
|
-
<
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
251
|
+
{#if !useDrawer}
|
|
252
|
+
<ul class={cn('space-y-2 px-4 py-4', !mobileNavEnd && 'border-b border-black')}>
|
|
253
|
+
{#each navigation as item}
|
|
254
|
+
<li>
|
|
255
|
+
<a
|
|
256
|
+
href={item.href}
|
|
257
|
+
class={cn(
|
|
258
|
+
'block rounded-md px-3 py-3 text-sm font-bold uppercase',
|
|
259
|
+
item.active
|
|
260
|
+
? 'bg-primary/10 text-primary'
|
|
261
|
+
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
|
262
|
+
)}
|
|
263
|
+
target={item.external ? '_blank' : undefined}
|
|
264
|
+
rel={item.external ? 'noopener noreferrer' : undefined}
|
|
265
|
+
aria-current={item.active ? 'page' : undefined}
|
|
266
|
+
onclick={() => (mobileMenuOpen = false)}
|
|
267
|
+
>
|
|
268
|
+
{item.name}
|
|
269
|
+
</a>
|
|
270
|
+
</li>
|
|
271
|
+
{/each}
|
|
272
|
+
</ul>
|
|
273
|
+
{#if mobileNavEnd}
|
|
274
|
+
<div class="border-b border-black px-4 pb-4">
|
|
275
|
+
{@render mobileNavEnd({ close: () => (mobileMenuOpen = false) })}
|
|
276
|
+
</div>
|
|
277
|
+
{/if}
|
|
185
278
|
{/if}
|
|
186
279
|
{/snippet}
|
|
187
280
|
</Header>
|
|
188
281
|
|
|
282
|
+
{#if useDrawer && belowBreakpoint}
|
|
283
|
+
<!-- --z-sidebar 70 (scrim 60): over the elevated header (z-50) and any
|
|
284
|
+
page-level sheet or panel, which sit at z-50 and below -->
|
|
285
|
+
<div class="contents" style="--z-sidebar: 70">
|
|
286
|
+
<Sidebar
|
|
287
|
+
navigation={drawerNavigation}
|
|
288
|
+
isMobile
|
|
289
|
+
mobileOpen={mobileMenuOpen}
|
|
290
|
+
onClose={closeMenu}
|
|
291
|
+
logo={drawerLogo}
|
|
292
|
+
footer={mobileNavEnd ? drawerFooter : undefined}
|
|
293
|
+
/>
|
|
294
|
+
</div>
|
|
295
|
+
{/if}
|
|
189
296
|
<!-- Main Content -->
|
|
190
297
|
<main id="main-content" class="flex-1 bg-content-bg">
|
|
191
298
|
{@render children()}
|
|
@@ -1,13 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PublicLayout - Layout for public-facing pages
|
|
3
|
-
*
|
|
4
|
-
* Features:
|
|
5
|
-
* - Header with navigation (uppercase bold links)
|
|
6
|
-
* - Footer with link sections
|
|
7
|
-
* - Main content area
|
|
8
|
-
* - Responsive design
|
|
9
|
-
* - Strong border options for brand consistency
|
|
10
|
-
*/
|
|
11
1
|
import type { Snippet } from 'svelte';
|
|
12
2
|
import type { NavItem, NavSection, HeaderSearchConfig } from '../../types/layout.js';
|
|
13
3
|
type Breakpoint = 'sm' | 'md' | 'lg';
|
|
@@ -20,6 +10,14 @@ interface Props {
|
|
|
20
10
|
* more than ~768px: at iPad portrait widths (820px) they otherwise overlap.
|
|
21
11
|
*/
|
|
22
12
|
mobileBreakpoint?: Breakpoint;
|
|
13
|
+
/**
|
|
14
|
+
* How the hamburger opens the nav below `mobileBreakpoint`. `dropdown`
|
|
15
|
+
* (default) expands under the header bar, pushing the page down when the
|
|
16
|
+
* header is at the top. `drawer` slides the Sidebar in over the page with a
|
|
17
|
+
* scrim - the same drawer, focus trap, Escape and swipe-to-close as
|
|
18
|
+
* DashboardLayout apps.
|
|
19
|
+
*/
|
|
20
|
+
mobileNavStyle?: 'dropdown' | 'drawer';
|
|
23
21
|
/** Whether to show footer */
|
|
24
22
|
showFooter?: boolean;
|
|
25
23
|
/** Footer link sections */
|
|
@@ -458,11 +458,16 @@
|
|
|
458
458
|
}
|
|
459
459
|
</script>
|
|
460
460
|
|
|
461
|
-
<!-- Mobile Overlay:
|
|
461
|
+
<!-- Mobile Overlay: 10 below the sidebar (--z-sidebar, default 50, so z-40) but
|
|
462
|
+
above main content; a host that lifts --z-sidebar lifts the scrim with it.
|
|
462
463
|
Not focusable: the drawer traps focus and handles Escape itself. -->
|
|
463
464
|
{#if isMobile && mobileOpen}
|
|
464
465
|
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
|
|
465
|
-
<div
|
|
466
|
+
<div
|
|
467
|
+
class="fixed inset-0 z-[calc(var(--z-sidebar,50)_-_10)] bg-black/50"
|
|
468
|
+
onclick={() => onClose?.()}
|
|
469
|
+
aria-hidden="true"
|
|
470
|
+
></div>
|
|
466
471
|
{/if}
|
|
467
472
|
|
|
468
473
|
<!-- Sidebar: z-50 positions above overlay and content, below modals (z-100+) -->
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classic-homes/theme-svelte",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.0",
|
|
4
4
|
"description": "Svelte components for the Classic theme system",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"svelte": "./dist/lib/index.js",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"clean": "node ../../scripts/rm.mjs dist",
|
|
31
31
|
"typecheck": "svelte-check --tsconfig ./tsconfig.json",
|
|
32
32
|
"test": "vitest run",
|
|
33
|
+
"test:shard": "vitest run --shard=$VITEST_SHARD",
|
|
33
34
|
"test:watch": "vitest",
|
|
34
35
|
"test:a11y": "vitest run src/__tests__/a11y/"
|
|
35
36
|
},
|
|
@@ -72,6 +73,7 @@
|
|
|
72
73
|
"zod": "^3.23.0"
|
|
73
74
|
},
|
|
74
75
|
"devDependencies": {
|
|
76
|
+
"@classic-homes/notifications": "*",
|
|
75
77
|
"@sveltejs/package": "^2.3.0",
|
|
76
78
|
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
|
77
79
|
"@testing-library/jest-dom": "^6.6.0",
|