@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.
@@ -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
- * Closing with Escape or the keyboard shortcut returns focus to what
20
- * opened the overlay, or else to the toggle, when focus was inside the
21
- * sidebar or had fallen to the body (as it does when the content goes
22
- * inert).
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 Escape can return focus to it.
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
- opener.focus();
178
- if (document.activeElement === opener) return;
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
- for (const toggle of document.querySelectorAll<HTMLElement>(toggle_selector)) {
181
- toggle.focus();
182
- if (document.activeElement === toggle) return;
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 Escape)
187
- // and listen for Escape — registered now, not at mount, so window listeners
188
- // mounted earlier (a contextmenu's, say) see Escape first and can swallow it
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
- opener = active instanceof HTMLElement && active !== document.body ? active : null;
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
- <div class="app-shell-sidebar" id={sidebar_id} bind:this={sidebar_el}>
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
- * Closing with Escape or the keyboard shortcut returns focus to what
19
- * opened the overlay, or else to the toggle, when focus was inside the
20
- * sidebar or had fallen to the body (as it does when the content goes
21
- * inert).
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 Escape can return focus to it.
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4DI;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,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;AAyJH,QAAA,MAAM,QAAQ,sDAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
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`, `sidebar_state?`,
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 `$props.id()` id) with the static name
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
- Not `role="dialog"` — the same element is the persistent sidebar at
111
- wide, so no focus trap.
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="<sidebar_id>"]` that takes focus (Safari doesn't
124
- focus a clicked button, so the opener can be `null`). A narrow-mode
125
- close from `keyboard_shortcut` goes through the same close-and-return
126
- path; wide mode just toggles. The shortcut bails on an open modal too,
127
- so pressing it inside a dialog opened from the overlay doesn't close the
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). Visibility is two
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 and narrow-mode query
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;CAC7B;AAED,qBAAa,YAAY;;IACxB,+EAA+E;IAC/E,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAErC;;;OAGG;IACH,iBAAiB,EAAE,OAAO,CAAoB;IAE9C,+CAA+C;IAC/C,mBAAmB,EAAE,OAAO,CAAqB;IAMjD;;;;;;OAMG;gBACS,OAAO,CAAC,EAAE,mBAAmB;IAazC,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"}
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 and narrow-mode query
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() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzdev/fuz_app",
3
- "version": "0.116.0",
3
+ "version": "0.117.0",
4
4
  "description": "fullstack app library",
5
5
  "glyph": "🗝",
6
6
  "logo": "logo.svg",