@fuzdev/fuz_app 0.116.0 → 0.117.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/ui/AppShell.svelte +82 -24
- package/dist/ui/AppShell.svelte.d.ts +27 -5
- package/dist/ui/AppShell.svelte.d.ts.map +1 -1
- package/dist/ui/CLAUDE.md +39 -12
- package/dist/ui/sidebar_state.svelte.d.ts +12 -1
- package/dist/ui/sidebar_state.svelte.d.ts.map +1 -1
- package/dist/ui/sidebar_state.svelte.js +4 -1
- package/package.json +1 -1
package/dist/ui/AppShell.svelte
CHANGED
|
@@ -16,10 +16,26 @@
|
|
|
16
16
|
* default, so the browser's close requests stay intact; it does nothing
|
|
17
17
|
* while a modal (`:modal`) is open, or when a window `keydown` listener
|
|
18
18
|
* registered before the overlay opened swallows it (a contextmenu, say).
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
19
|
+
*
|
|
20
|
+
* The open overlay is a modal dialog: the sidebar takes `role="dialog"`,
|
|
21
|
+
* `aria-modal`, `tabindex="-1"`, and `sidebar_label` as its name, and
|
|
22
|
+
* focus moves into it, to its first control that takes focus, else to the
|
|
23
|
+
* sidebar itself — the content went inert and dropped it. The attributes
|
|
24
|
+
* key on the open overlay, which reads the viewport — that's what keeps
|
|
25
|
+
* the wide sidebar from ever being a dialog — and the server render
|
|
26
|
+
* matches hydration because the overlay always starts closed, as with the
|
|
27
|
+
* content's `inert`. `aria-modal` lets assistive tech hide everything
|
|
28
|
+
* outside the sidebar, the built-in toggle included, so a sidebar whose
|
|
29
|
+
* screen-reader users need a close control beyond Escape and navigation
|
|
30
|
+
* renders its own.
|
|
31
|
+
*
|
|
32
|
+
* Closing with Escape, the keyboard shortcut, or a scrim click returns
|
|
33
|
+
* focus to what opened the overlay, or else to the first element whose
|
|
34
|
+
* `aria-controls` names the sidebar's id and that takes it — when focus
|
|
35
|
+
* was inside the sidebar or had fallen to the body. The id is the state's
|
|
36
|
+
* `sidebar_id` when set, so a toggle outside the shell can carry
|
|
37
|
+
* `aria-controls` too; Safari doesn't focus a clicked button, so without
|
|
38
|
+
* it a mouse-opened overlay has no opener to return to.
|
|
23
39
|
*
|
|
24
40
|
* The shell's rules sit in two cascade layers nested under `fuz_app`.
|
|
25
41
|
* `fuz_app.app_shell_base`, in the scoped style, holds the base and state
|
|
@@ -69,10 +85,15 @@
|
|
|
69
85
|
|
|
70
86
|
import { SidebarState, sidebar_state_context } from './sidebar_state.svelte.js';
|
|
71
87
|
|
|
88
|
+
// what can take focus from the keyboard, narrowed further by `tabIndex`
|
|
89
|
+
const FOCUSABLE_SELECTOR =
|
|
90
|
+
'a[href], area[href], button, input, select, textarea, iframe, summary, [tabindex], [contenteditable]';
|
|
91
|
+
|
|
72
92
|
const {
|
|
73
93
|
children,
|
|
74
94
|
sidebar,
|
|
75
95
|
sidebar_width = 180,
|
|
96
|
+
sidebar_label = 'sidebar',
|
|
76
97
|
sidebar_state: sidebar_state_prop,
|
|
77
98
|
keyboard_shortcut = false,
|
|
78
99
|
show_toggle = true,
|
|
@@ -87,6 +108,12 @@
|
|
|
87
108
|
* @default 180
|
|
88
109
|
*/
|
|
89
110
|
sidebar_width?: number;
|
|
111
|
+
/**
|
|
112
|
+
* Accessible name for the sidebar while it's the narrow-mode overlay,
|
|
113
|
+
* announced as a modal dialog.
|
|
114
|
+
* @default 'sidebar'
|
|
115
|
+
*/
|
|
116
|
+
sidebar_label?: string;
|
|
90
117
|
/** Optional pre-built `SidebarState` for sharing visibility across shells. */
|
|
91
118
|
sidebar_state?: SidebarState;
|
|
92
119
|
/**
|
|
@@ -108,7 +135,7 @@
|
|
|
108
135
|
* `class="app-shell-toggle"` so it stacks above the sidebar and scrim
|
|
109
136
|
* (it renders as a direct child of the root) — the z-index applies only
|
|
110
137
|
* to a positioned button, so position it (e.g. `position: fixed`) — and
|
|
111
|
-
* `aria-controls={sidebar_id}` so
|
|
138
|
+
* `aria-controls={sidebar_id}` so closing can return focus to it.
|
|
112
139
|
* `show_sidebar` is viewport-dependent: the server renders it as in wide
|
|
113
140
|
* mode, so rendering it as visible content changes on hydration at
|
|
114
141
|
* narrow widths — attributes are repaired, but key visible content on
|
|
@@ -120,7 +147,6 @@
|
|
|
120
147
|
} = $props();
|
|
121
148
|
|
|
122
149
|
const uid = $props.id();
|
|
123
|
-
const sidebar_id = `${uid}-sidebar`;
|
|
124
150
|
|
|
125
151
|
let fallback_sidebar_state: SidebarState | undefined;
|
|
126
152
|
const get_sidebar_state = sidebar_state_context.set(
|
|
@@ -128,14 +154,14 @@
|
|
|
128
154
|
);
|
|
129
155
|
const sidebar_state = $derived(get_sidebar_state());
|
|
130
156
|
|
|
157
|
+
const sidebar_id = $derived(sidebar_state.sidebar_id ?? `${uid}-sidebar`);
|
|
158
|
+
|
|
131
159
|
// viewport-free, so the server and hydration renders agree
|
|
132
160
|
const wide_open = $derived(sidebar_state.enabled && sidebar_state.show_sidebar_wide);
|
|
133
161
|
const narrow_open = $derived(sidebar_state.enabled && sidebar_state.show_sidebar_narrow);
|
|
134
162
|
// viewport-dependent, only for `inert` and input handling (always `false` in SSR)
|
|
135
163
|
const overlay_open = $derived(sidebar_state.narrow && narrow_open);
|
|
136
164
|
|
|
137
|
-
const toggle_selector = `[aria-controls="${sidebar_id}"]`;
|
|
138
|
-
|
|
139
165
|
const button_title = $derived(
|
|
140
166
|
(sidebar_state.show_sidebar ? 'hide sidebar' : 'show sidebar') +
|
|
141
167
|
(keyboard_shortcut ? ` [${keyboard_shortcut}]` : '')
|
|
@@ -170,26 +196,49 @@
|
|
|
170
196
|
if (should_return_focus) return_focus();
|
|
171
197
|
};
|
|
172
198
|
|
|
199
|
+
// focuses `el`, returning whether it took focus — disabled, hidden, and
|
|
200
|
+
// inert elements refuse it
|
|
201
|
+
const try_focus = (el: HTMLElement): boolean => {
|
|
202
|
+
el.focus();
|
|
203
|
+
return document.activeElement === el;
|
|
204
|
+
};
|
|
205
|
+
|
|
173
206
|
// the opener if it's still there and takes focus, else the first toggle that
|
|
174
207
|
// does — Safari doesn't focus a clicked button, so the opener can be `null`
|
|
175
208
|
const return_focus = (): void => {
|
|
176
|
-
if (opener?.isConnected)
|
|
177
|
-
|
|
178
|
-
|
|
209
|
+
if (opener?.isConnected && try_focus(opener)) return;
|
|
210
|
+
for (const toggle of document.querySelectorAll<HTMLElement>('[aria-controls]')) {
|
|
211
|
+
// `aria-controls` is an id list
|
|
212
|
+
const ids = toggle.getAttribute('aria-controls')!.split(/\s+/);
|
|
213
|
+
if (ids.includes(sidebar_id) && try_focus(toggle)) return;
|
|
179
214
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// the first control in the sidebar that takes focus, else the sidebar itself
|
|
218
|
+
// (`tabindex="-1"` while it's the overlay); `tabIndex` screens out what only
|
|
219
|
+
// script can focus
|
|
220
|
+
const focus_sidebar = (): void => {
|
|
221
|
+
if (!sidebar_el) return;
|
|
222
|
+
for (const el of sidebar_el.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR)) {
|
|
223
|
+
if (el.tabIndex >= 0 && try_focus(el)) return;
|
|
183
224
|
}
|
|
225
|
+
sidebar_el.focus();
|
|
184
226
|
};
|
|
185
227
|
|
|
186
|
-
// while the overlay is open, remember what had focus (to return it on
|
|
187
|
-
//
|
|
188
|
-
//
|
|
228
|
+
// while the overlay is open, remember what had focus (to return it on
|
|
229
|
+
// close), move focus into the sidebar — the content just went inert, which
|
|
230
|
+
// drops its focus — and listen for Escape, registered now, not at mount, so
|
|
231
|
+
// window listeners mounted earlier (a contextmenu's, say) see Escape first
|
|
232
|
+
// and can swallow it
|
|
189
233
|
$effect(() => {
|
|
190
234
|
if (!overlay_open) return;
|
|
191
235
|
const active = document.activeElement;
|
|
192
|
-
|
|
236
|
+
// never an element in the sidebar, which hides again on close
|
|
237
|
+
opener =
|
|
238
|
+
active instanceof HTMLElement && active !== document.body && !sidebar_el?.contains(active)
|
|
239
|
+
? active
|
|
240
|
+
: null;
|
|
241
|
+
focus_sidebar();
|
|
193
242
|
return on(window, 'keydown', on_escape);
|
|
194
243
|
});
|
|
195
244
|
</script>
|
|
@@ -267,11 +316,7 @@
|
|
|
267
316
|
{@render children()}
|
|
268
317
|
</div>
|
|
269
318
|
<!-- pointer-only dismissal; Escape is the keyboard path -->
|
|
270
|
-
<div
|
|
271
|
-
class="app-shell-scrim"
|
|
272
|
-
aria-hidden="true"
|
|
273
|
-
onclick={() => sidebar_state.close_narrow()}
|
|
274
|
-
></div>
|
|
319
|
+
<div class="app-shell-scrim" aria-hidden="true" onclick={close_overlay}></div>
|
|
275
320
|
{#if show_toggle && sidebar_state.enabled}
|
|
276
321
|
{#if toggle_button}
|
|
277
322
|
{@render toggle_button({
|
|
@@ -295,7 +340,20 @@
|
|
|
295
340
|
</button>
|
|
296
341
|
{/if}
|
|
297
342
|
{/if}
|
|
298
|
-
|
|
343
|
+
<!-- open, the overlay is a modal dialog whose `tabindex="-1"` makes it focus's
|
|
344
|
+
fallback target; the wide sidebar stays a plain element, so a click on its
|
|
345
|
+
background doesn't take focus. The a11y rule can't see the value is only
|
|
346
|
+
ever -1. -->
|
|
347
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
348
|
+
<div
|
|
349
|
+
class="app-shell-sidebar"
|
|
350
|
+
id={sidebar_id}
|
|
351
|
+
role={overlay_open ? 'dialog' : undefined}
|
|
352
|
+
aria-modal={overlay_open ? 'true' : undefined}
|
|
353
|
+
aria-label={overlay_open ? sidebar_label : undefined}
|
|
354
|
+
tabindex={overlay_open ? -1 : undefined}
|
|
355
|
+
bind:this={sidebar_el}
|
|
356
|
+
>
|
|
299
357
|
{@render sidebar()}
|
|
300
358
|
</div>
|
|
301
359
|
</div>
|
|
@@ -15,10 +15,26 @@
|
|
|
15
15
|
* default, so the browser's close requests stay intact; it does nothing
|
|
16
16
|
* while a modal (`:modal`) is open, or when a window `keydown` listener
|
|
17
17
|
* registered before the overlay opened swallows it (a contextmenu, say).
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
18
|
+
*
|
|
19
|
+
* The open overlay is a modal dialog: the sidebar takes `role="dialog"`,
|
|
20
|
+
* `aria-modal`, `tabindex="-1"`, and `sidebar_label` as its name, and
|
|
21
|
+
* focus moves into it, to its first control that takes focus, else to the
|
|
22
|
+
* sidebar itself — the content went inert and dropped it. The attributes
|
|
23
|
+
* key on the open overlay, which reads the viewport — that's what keeps
|
|
24
|
+
* the wide sidebar from ever being a dialog — and the server render
|
|
25
|
+
* matches hydration because the overlay always starts closed, as with the
|
|
26
|
+
* content's `inert`. `aria-modal` lets assistive tech hide everything
|
|
27
|
+
* outside the sidebar, the built-in toggle included, so a sidebar whose
|
|
28
|
+
* screen-reader users need a close control beyond Escape and navigation
|
|
29
|
+
* renders its own.
|
|
30
|
+
*
|
|
31
|
+
* Closing with Escape, the keyboard shortcut, or a scrim click returns
|
|
32
|
+
* focus to what opened the overlay, or else to the first element whose
|
|
33
|
+
* `aria-controls` names the sidebar's id and that takes it — when focus
|
|
34
|
+
* was inside the sidebar or had fallen to the body. The id is the state's
|
|
35
|
+
* `sidebar_id` when set, so a toggle outside the shell can carry
|
|
36
|
+
* `aria-controls` too; Safari doesn't focus a clicked button, so without
|
|
37
|
+
* it a mouse-opened overlay has no opener to return to.
|
|
22
38
|
*
|
|
23
39
|
* The shell's rules sit in two cascade layers nested under `fuz_app`.
|
|
24
40
|
* `fuz_app.app_shell_base`, in the scoped style, holds the base and state
|
|
@@ -70,6 +86,12 @@ type $$ComponentProps = SvelteHTMLElements['div'] & {
|
|
|
70
86
|
* @default 180
|
|
71
87
|
*/
|
|
72
88
|
sidebar_width?: number;
|
|
89
|
+
/**
|
|
90
|
+
* Accessible name for the sidebar while it's the narrow-mode overlay,
|
|
91
|
+
* announced as a modal dialog.
|
|
92
|
+
* @default 'sidebar'
|
|
93
|
+
*/
|
|
94
|
+
sidebar_label?: string;
|
|
73
95
|
/** Optional pre-built `SidebarState` for sharing visibility across shells. */
|
|
74
96
|
sidebar_state?: SidebarState;
|
|
75
97
|
/**
|
|
@@ -91,7 +113,7 @@ type $$ComponentProps = SvelteHTMLElements['div'] & {
|
|
|
91
113
|
* `class="app-shell-toggle"` so it stacks above the sidebar and scrim
|
|
92
114
|
* (it renders as a direct child of the root) — the z-index applies only
|
|
93
115
|
* to a positioned button, so position it (e.g. `position: fixed`) — and
|
|
94
|
-
* `aria-controls={sidebar_id}` so
|
|
116
|
+
* `aria-controls={sidebar_id}` so closing can return focus to it.
|
|
95
117
|
* `show_sidebar` is viewport-dependent: the server renders it as in wide
|
|
96
118
|
* mode, so rendering it as visible content changes on hydration at
|
|
97
119
|
* narrow widths — attributes are repaired, but key visible content on
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AppShell.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/AppShell.svelte"],"names":[],"mappings":"AAGA
|
|
1
|
+
{"version":3,"file":"AppShell.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/AppShell.svelte"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4EI;AACJ,OAAO,EAAa,KAAK,OAAO,EAAE,MAAM,QAAQ,CAAC;AACjD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAK1D,OAAO,EAAE,YAAY,EAAyB,MAAM,2BAA2B,CAAC;AAE/E,KAAK,gBAAgB,GAAI,kBAAkB,CAAC,KAAK,CAAC,GAAG;IACpD,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,8EAA8E;IAC9E,aAAa,CAAC,EAAE,YAAY,CAAC;IAC7B;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IACnC;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;;;;;;;;;OAWG;IACH,aAAa,CAAC,EAAE,OAAO,CACtB;QAAC;YAAE,KAAK,EAAE,MAAM,CAAC;YAAC,YAAY,EAAE,OAAO,CAAC;YAAC,MAAM,EAAE,MAAM,IAAI,CAAC;YAAC,UAAU,EAAE,MAAM,CAAA;SAAE;KAAC,CAClF,CAAC;CACF,CAAC;AAsLH,QAAA,MAAM,QAAQ,sDAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
|
package/dist/ui/CLAUDE.md
CHANGED
|
@@ -76,7 +76,8 @@ pattern rather than reintroducing prop-drilling.
|
|
|
76
76
|
## Shell + layout
|
|
77
77
|
|
|
78
78
|
- `AppShell.svelte` — sidebar-and-main shell. Props: `children`,
|
|
79
|
-
`sidebar` (Snippet), `sidebar_width = 180`, `
|
|
79
|
+
`sidebar` (Snippet), `sidebar_width = 180`, `sidebar_label = 'sidebar'`
|
|
80
|
+
(the open overlay's accessible name), `sidebar_state?`,
|
|
80
81
|
`keyboard_shortcut?`, `show_toggle?`, `toggle_button?` (receives
|
|
81
82
|
`{title, show_sidebar, toggle, sidebar_id}`). Provisions
|
|
82
83
|
`sidebar_state_context` internally (creates one `SidebarState` if the
|
|
@@ -84,7 +85,8 @@ pattern rather than reintroducing prop-drilling.
|
|
|
84
85
|
(`afterNavigate` from `$app/navigation`). The toggle renders only while
|
|
85
86
|
`show_toggle && sidebar_state.enabled`, before the sidebar in the DOM;
|
|
86
87
|
the built-in one is a disclosure (`aria-expanded` + `aria-controls`
|
|
87
|
-
pointing at the sidebar's
|
|
88
|
+
pointing at the sidebar's id — `SidebarState.sidebar_id` when set, else
|
|
89
|
+
one from `$props.id()`) with the static name
|
|
88
90
|
`aria-label="sidebar"`, a dynamic `title`, `aria-keyshortcuts` when
|
|
89
91
|
`keyboard_shortcut` is set, and a `←` glyph CSS mirrors when closed.
|
|
90
92
|
The shortcut acts only while the sidebar is enabled, on the current
|
|
@@ -100,6 +102,12 @@ pattern rather than reintroducing prop-drilling.
|
|
|
100
102
|
attributes are repaired on hydration, but visible content keyed on it
|
|
101
103
|
changes at narrow widths, so key visible content on CSS or the
|
|
102
104
|
viewport-free fields.
|
|
105
|
+
- **Toggles outside the shell** (a top nav's hamburger) set
|
|
106
|
+
`SidebarState`'s `sidebar_id` option and carry
|
|
107
|
+
`aria-controls={sidebar_state.sidebar_id}`, so the focus fallback finds
|
|
108
|
+
them too. `aria-controls` is read as an id list. A set id assumes one
|
|
109
|
+
shell per state — shells sharing it at the same time would render the
|
|
110
|
+
same id.
|
|
103
111
|
- **Two modes, split by `SidebarState.narrow_query`.** Wide: the sidebar
|
|
104
112
|
sits beside the content (padded by `--sidebar_offset`) and follows
|
|
105
113
|
`show_sidebar_wide`. Narrow: it starts closed and opens as an overlay
|
|
@@ -107,8 +115,24 @@ pattern rather than reintroducing prop-drilling.
|
|
|
107
115
|
content is `inert` while open. The overlay closes on navigation
|
|
108
116
|
(`afterNavigate` inside `AppShell` — consumers write nothing), Escape,
|
|
109
117
|
a scrim click, and widening out of narrow mode (a crossing `$effect`).
|
|
110
|
-
|
|
111
|
-
|
|
118
|
+
- **The open overlay is a modal dialog.** While it's open the sidebar
|
|
119
|
+
carries `role="dialog"`, `aria-modal="true"`, and `aria-label` from
|
|
120
|
+
`sidebar_label`. They key on the open overlay (`overlay_open`), which
|
|
121
|
+
reads the viewport — that's what keeps the persistent wide sidebar (the
|
|
122
|
+
same element) from ever being a dialog — and the server render matches
|
|
123
|
+
hydration because the overlay always starts closed, as with `inert`.
|
|
124
|
+
Opening moves
|
|
125
|
+
focus into it — the content went `inert`, which dropped focus — to the
|
|
126
|
+
first control that takes focus (`tabIndex >= 0`, checked after each
|
|
127
|
+
`focus()`), else to the sidebar itself, `tabindex="-1"` while it's the
|
|
128
|
+
overlay (script-focusable, skipped by Tab; the wide sidebar has none, so
|
|
129
|
+
a click on its background leaves focus where it was). No focus trap:
|
|
130
|
+
`inert` already keeps Tab out of the content, and the built-in toggle
|
|
131
|
+
stays Tab-reachable as a close control. `aria-modal` lets assistive
|
|
132
|
+
tech hide everything outside the sidebar, the toggle included
|
|
133
|
+
(Chromium keeps it exposed; WebKit may not), so a sidebar whose
|
|
134
|
+
screen-reader users need a close control beyond Escape and navigation
|
|
135
|
+
renders its own.
|
|
112
136
|
- **Escape** is a window `keydown` listener registered by an `$effect`
|
|
113
137
|
only while the overlay is open, so listeners registered earlier (a
|
|
114
138
|
contextmenu's, mounted with the page) see Escape first and can swallow
|
|
@@ -120,12 +144,12 @@ pattern rather than reintroducing prop-drilling.
|
|
|
120
144
|
focus returns. When focus was inside the sidebar or had fallen to `body`
|
|
121
145
|
(opening from the content makes it `inert`, which drops focus there), it
|
|
122
146
|
goes back to the opener if still connected and focusable, else to the
|
|
123
|
-
first `[aria-controls
|
|
124
|
-
focus a clicked button, so the opener can be `null`). A
|
|
125
|
-
close from `keyboard_shortcut`
|
|
126
|
-
path; wide mode just toggles. The shortcut
|
|
127
|
-
so pressing it inside a dialog opened from
|
|
128
|
-
overlay behind it.
|
|
147
|
+
first `[aria-controls]` naming the sidebar's id that takes focus (Safari
|
|
148
|
+
doesn't focus a clicked button, so the opener can be `null`). A
|
|
149
|
+
narrow-mode close from `keyboard_shortcut` or a scrim click goes through
|
|
150
|
+
the same close-and-return path; wide mode just toggles. The shortcut
|
|
151
|
+
bails on an open modal too, so pressing it inside a dialog opened from
|
|
152
|
+
the overlay doesn't close the overlay behind it.
|
|
129
153
|
- **No hydration flash.** The mode rules sit in two cascade layers
|
|
130
154
|
nested under `fuz_app`. `@layer fuz_app.app_shell_base` in the scoped
|
|
131
155
|
`<style>` holds the base and state rules, which apply in both modes;
|
|
@@ -208,7 +232,9 @@ pattern rather than reintroducing prop-drilling.
|
|
|
208
232
|
`enabled?` (reactive getter overriding the internal field) and
|
|
209
233
|
`narrow_query?` (`null` disables narrow mode; must contain parentheses
|
|
210
234
|
since it feeds both CSS `media` and `MediaQuery`, which would auto-wrap a
|
|
211
|
-
paren-less query while CSS wouldn't — throws in DEV)
|
|
235
|
+
paren-less query while CSS wouldn't — throws in DEV), and `sidebar_id?`
|
|
236
|
+
(the sidebar element's id, read back as the readonly `sidebar_id`, `null`
|
|
237
|
+
when unset — for toggles outside `AppShell`). Visibility is two
|
|
212
238
|
viewport-free fields, `show_sidebar_wide` (default `true`, the desktop
|
|
213
239
|
preference narrow interactions never touch) and `show_sidebar_narrow`
|
|
214
240
|
(default `false`). `narrow` is the viewport match (`false` in SSR);
|
|
@@ -509,7 +535,8 @@ format_scope, global_label)` helper — `global_label = null` renders no
|
|
|
509
535
|
`format_scope?: FormatScope` prop — same shape as the context, prop
|
|
510
536
|
wins when supplied.
|
|
511
537
|
- `sidebar_state_context` — `() => SidebarState`. Provisioned by
|
|
512
|
-
`AppShell`.
|
|
538
|
+
`AppShell`. A toggle reading it for `aria-controls` needs the state's
|
|
539
|
+
`sidebar_id` option set (see Shell + layout).
|
|
513
540
|
|
|
514
541
|
## Popovers
|
|
515
542
|
|
|
@@ -42,11 +42,22 @@ export interface SidebarStateOptions {
|
|
|
42
42
|
* @default SIDEBAR_NARROW_QUERY_DEFAULT
|
|
43
43
|
*/
|
|
44
44
|
narrow_query?: string | null;
|
|
45
|
+
/**
|
|
46
|
+
* The sidebar element's `id`. Set it when a toggle outside `AppShell` needs
|
|
47
|
+
* `aria-controls` — the relationship closing the overlay also follows to
|
|
48
|
+
* return focus when the opener didn't take it (Safari doesn't focus a
|
|
49
|
+
* clicked button). Omitted, `AppShell` generates one, which only its own
|
|
50
|
+
* toggle knows. Set, it assumes one shell per state: shells mounted at
|
|
51
|
+
* the same time with this state would render the same id.
|
|
52
|
+
*/
|
|
53
|
+
sidebar_id?: string;
|
|
45
54
|
}
|
|
46
55
|
export declare class SidebarState {
|
|
47
56
|
#private;
|
|
48
57
|
/** The media query for narrow mode, or `null` when narrow mode is disabled. */
|
|
49
58
|
readonly narrow_query: string | null;
|
|
59
|
+
/** The sidebar element's `id`, or `null` to let `AppShell` generate one. */
|
|
60
|
+
readonly sidebar_id: string | null;
|
|
50
61
|
/**
|
|
51
62
|
* Whether the sidebar shows beside the content outside narrow mode — the
|
|
52
63
|
* desktop preference. Narrow-mode interactions never touch it.
|
|
@@ -57,7 +68,7 @@ export declare class SidebarState {
|
|
|
57
68
|
/**
|
|
58
69
|
* Creates the state, subscribing to `narrow_query` unless it is `null`.
|
|
59
70
|
*
|
|
60
|
-
* @param options - enabled getter
|
|
71
|
+
* @param options - enabled getter, narrow-mode query, and sidebar id
|
|
61
72
|
* @throws Error in DEV when `narrow_query` has no parentheses — `MediaQuery`
|
|
62
73
|
* would auto-wrap it but CSS `media` would not, so they'd disagree
|
|
63
74
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sidebar_state.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/sidebar_state.svelte.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAMH;;;GAGG;AACH,eAAO,MAAM,4BAA4B,uBAAuB,CAAC;AAEjE;;;;GAIG;AACH,eAAO,MAAM,qBAAqB;2CAAwB,YAAY;4BAAZ,YAAY;uBAAZ,YAAY,WAAZ,YAAY;CAAG,CAAC;AAE1E,MAAM,WAAW,mBAAmB;IACnC;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,OAAO,CAAC;IACxB;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"sidebar_state.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/sidebar_state.svelte.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAMH;;;GAGG;AACH,eAAO,MAAM,4BAA4B,uBAAuB,CAAC;AAEjE;;;;GAIG;AACH,eAAO,MAAM,qBAAqB;2CAAwB,YAAY;4BAAZ,YAAY;uBAAZ,YAAY,WAAZ,YAAY;CAAG,CAAC;AAE1E,MAAM,WAAW,mBAAmB;IACnC;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,OAAO,CAAC;IACxB;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,YAAY;;IACxB,+EAA+E;IAC/E,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAErC,4EAA4E;IAC5E,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAEnC;;;OAGG;IACH,iBAAiB,EAAE,OAAO,CAAoB;IAE9C,+CAA+C;IAC/C,mBAAmB,EAAE,OAAO,CAAqB;IAMjD;;;;;;OAMG;gBACS,OAAO,CAAC,EAAE,mBAAmB;IAczC,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,IAAI,OAAO,CAAC,KAAK,EAAE,OAAO,EAEzB;IAED;;;;;OAKG;IACH,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED;;;;OAIG;IACH,IAAI,YAAY,IAAI,OAAO,CAG1B;IAED,IAAI,YAAY,CAAC,KAAK,EAAE,OAAO,EAM9B;IAED;;;;OAIG;IACH,cAAc,CAAC,KAAK,GAAE,OAA4B,GAAG,IAAI;IAIzD;;;;OAIG;IACH,YAAY,IAAI,IAAI;IAIpB;;;;;;OAMG;IACH,QAAQ,IAAI,MAAM,IAAI;CAStB"}
|
|
@@ -28,6 +28,8 @@ export const sidebar_state_context = create_context();
|
|
|
28
28
|
export class SidebarState {
|
|
29
29
|
/** The media query for narrow mode, or `null` when narrow mode is disabled. */
|
|
30
30
|
narrow_query;
|
|
31
|
+
/** The sidebar element's `id`, or `null` to let `AppShell` generate one. */
|
|
32
|
+
sidebar_id;
|
|
31
33
|
/**
|
|
32
34
|
* Whether the sidebar shows beside the content outside narrow mode — the
|
|
33
35
|
* desktop preference. Narrow-mode interactions never touch it.
|
|
@@ -41,7 +43,7 @@ export class SidebarState {
|
|
|
41
43
|
/**
|
|
42
44
|
* Creates the state, subscribing to `narrow_query` unless it is `null`.
|
|
43
45
|
*
|
|
44
|
-
* @param options - enabled getter
|
|
46
|
+
* @param options - enabled getter, narrow-mode query, and sidebar id
|
|
45
47
|
* @throws Error in DEV when `narrow_query` has no parentheses — `MediaQuery`
|
|
46
48
|
* would auto-wrap it but CSS `media` would not, so they'd disagree
|
|
47
49
|
*/
|
|
@@ -52,6 +54,7 @@ export class SidebarState {
|
|
|
52
54
|
throw new Error(`SidebarState narrow_query must include parentheses, got '${narrow_query}' — try '(${narrow_query})'`);
|
|
53
55
|
}
|
|
54
56
|
this.narrow_query = narrow_query;
|
|
57
|
+
this.sidebar_id = options?.sidebar_id ?? null;
|
|
55
58
|
this.#narrow_media = narrow_query === null ? null : new MediaQuery(narrow_query, false);
|
|
56
59
|
}
|
|
57
60
|
get enabled() {
|