@fuzdev/fuz_app 0.116.0 → 0.117.1
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/AccountSessions.svelte +1 -1
- package/dist/ui/AdminAccounts.svelte +1 -1
- package/dist/ui/AdminAuditLog.svelte +1 -1
- package/dist/ui/AdminInvites.svelte +5 -4
- package/dist/ui/AdminInvites.svelte.d.ts.map +1 -1
- package/dist/ui/AdminOverview.svelte +6 -6
- package/dist/ui/AdminRoleGrantHistory.svelte +2 -2
- package/dist/ui/AdminSessions.svelte +1 -1
- package/dist/ui/AdminSettings.svelte +3 -3
- package/dist/ui/AdminSurface.svelte +1 -1
- 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/RoleGrantOfferHistory.svelte +1 -1
- package/dist/ui/RoleGrantOfferInbox.svelte +1 -1
- package/dist/ui/SurfaceExplorer.svelte +7 -7
- 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
|
@@ -53,10 +53,11 @@
|
|
|
53
53
|
</script>
|
|
54
54
|
|
|
55
55
|
<section>
|
|
56
|
-
<h1>
|
|
57
|
-
<
|
|
56
|
+
<h1>Invites</h1>
|
|
57
|
+
<fieldset>
|
|
58
|
+
<legend>Signup</legend>
|
|
58
59
|
<OpenSignupToggle />
|
|
59
|
-
</
|
|
60
|
+
</fieldset>
|
|
60
61
|
{#if admin_invites.invite_count > 0}
|
|
61
62
|
<p>
|
|
62
63
|
<span class="chip color_a">
|
|
@@ -73,7 +74,7 @@
|
|
|
73
74
|
}}
|
|
74
75
|
>
|
|
75
76
|
<fieldset class="row gap_sm">
|
|
76
|
-
<legend>
|
|
77
|
+
<legend>Invite Target</legend>
|
|
77
78
|
<label class="grow">
|
|
78
79
|
<div class="title">email</div>
|
|
79
80
|
<input
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AdminInvites.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/AdminInvites.svelte"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"AdminInvites.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/AdminInvites.svelte"],"names":[],"mappings":"AAwJA,QAAA,MAAM,YAAY,2DAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
<div class="overview">
|
|
85
85
|
<section>
|
|
86
86
|
<div class="panel-header">
|
|
87
|
-
<h3>
|
|
87
|
+
<h3>Accounts</h3>
|
|
88
88
|
<a href={resolve('/admin/accounts' as any)} class="text_50 font_size_sm">view all →</a>
|
|
89
89
|
</div>
|
|
90
90
|
{#if accounts.list.loading}
|
|
@@ -126,7 +126,7 @@
|
|
|
126
126
|
|
|
127
127
|
<section>
|
|
128
128
|
<div class="panel-header">
|
|
129
|
-
<h3>
|
|
129
|
+
<h3>Sessions</h3>
|
|
130
130
|
<a href={resolve('/admin/sessions' as any)} class="text_50 font_size_sm">view all →</a>
|
|
131
131
|
</div>
|
|
132
132
|
{#if sessions.list.loading}
|
|
@@ -156,7 +156,7 @@
|
|
|
156
156
|
|
|
157
157
|
<section>
|
|
158
158
|
<div class="panel-header">
|
|
159
|
-
<h3>
|
|
159
|
+
<h3>Invites</h3>
|
|
160
160
|
<a href={resolve('/admin/invites' as any)} class="text_50 font_size_sm">view all →</a>
|
|
161
161
|
</div>
|
|
162
162
|
{#if invites.list.loading}
|
|
@@ -201,7 +201,7 @@
|
|
|
201
201
|
|
|
202
202
|
<section>
|
|
203
203
|
<div class="panel-header">
|
|
204
|
-
<h3>
|
|
204
|
+
<h3>Recent Activity</h3>
|
|
205
205
|
<a href={resolve('/admin/audit-log' as any)} class="text_50 font_size_sm">view all →</a>
|
|
206
206
|
</div>
|
|
207
207
|
{#if audit_log.list.loading}
|
|
@@ -229,7 +229,7 @@
|
|
|
229
229
|
|
|
230
230
|
<section>
|
|
231
231
|
<div class="panel-header">
|
|
232
|
-
<h3>
|
|
232
|
+
<h3>Security</h3>
|
|
233
233
|
<a href={resolve('/admin/audit-log' as any)} class="text_50 font_size_sm">audit log →</a>
|
|
234
234
|
</div>
|
|
235
235
|
{#if audit_log.list.loading}
|
|
@@ -267,7 +267,7 @@
|
|
|
267
267
|
|
|
268
268
|
<section>
|
|
269
269
|
<div class="panel-header">
|
|
270
|
-
<h3>
|
|
270
|
+
<h3>System</h3>
|
|
271
271
|
</div>
|
|
272
272
|
{#if app_settings.list.loading}
|
|
273
273
|
<p class="text_50">loading...</p>
|
|
@@ -38,10 +38,10 @@
|
|
|
38
38
|
</script>
|
|
39
39
|
|
|
40
40
|
<section>
|
|
41
|
-
<h1>
|
|
41
|
+
<h1>Role-Grant History</h1>
|
|
42
42
|
|
|
43
43
|
{#if audit_log.role_grant_history.loading}
|
|
44
|
-
<p class="text_50">loading
|
|
44
|
+
<p class="text_50">loading role-grant history...</p>
|
|
45
45
|
{:else if audit_log.role_grant_history.error}
|
|
46
46
|
<p class="color_c_50">{audit_log.role_grant_history.error}</p>
|
|
47
47
|
{:else}
|
|
@@ -16,10 +16,10 @@
|
|
|
16
16
|
</script>
|
|
17
17
|
|
|
18
18
|
<section>
|
|
19
|
-
<h1>
|
|
20
|
-
<h2>
|
|
19
|
+
<h1>Settings</h1>
|
|
20
|
+
<h2>Signup</h2>
|
|
21
21
|
<OpenSignupToggle />
|
|
22
|
-
<h2>
|
|
22
|
+
<h2>Authentication</h2>
|
|
23
23
|
{#if auth_state.account}
|
|
24
24
|
<p>Logged in as <strong>{auth_state.account.username}</strong>.</p>
|
|
25
25
|
{:else}
|
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
|
|
|
@@ -133,7 +133,7 @@
|
|
|
133
133
|
{/if}
|
|
134
134
|
</div>
|
|
135
135
|
|
|
136
|
-
<h3>
|
|
136
|
+
<h3>Routes</h3>
|
|
137
137
|
<div class="mb_sm">
|
|
138
138
|
<label>
|
|
139
139
|
<div class="title">auth filter</div>
|
|
@@ -218,7 +218,7 @@
|
|
|
218
218
|
</div>
|
|
219
219
|
{/if}
|
|
220
220
|
|
|
221
|
-
<h3>
|
|
221
|
+
<h3>Middleware</h3>
|
|
222
222
|
{#if surface.middleware.length === 0}
|
|
223
223
|
<p class="text_50">no middleware</p>
|
|
224
224
|
{:else}
|
|
@@ -247,7 +247,7 @@
|
|
|
247
247
|
{/if}
|
|
248
248
|
|
|
249
249
|
{#if surface.env.length}
|
|
250
|
-
<h3>
|
|
250
|
+
<h3>Environment</h3>
|
|
251
251
|
<div style:overflow-x="auto">
|
|
252
252
|
<table>
|
|
253
253
|
<thead>
|
|
@@ -275,7 +275,7 @@
|
|
|
275
275
|
{/if}
|
|
276
276
|
|
|
277
277
|
{#if surface.events.length}
|
|
278
|
-
<h3>
|
|
278
|
+
<h3>Events</h3>
|
|
279
279
|
<div style:overflow-x="auto">
|
|
280
280
|
<table>
|
|
281
281
|
<thead>
|
|
@@ -319,7 +319,7 @@
|
|
|
319
319
|
{/if}
|
|
320
320
|
|
|
321
321
|
{#if surface.rpc_endpoints.length}
|
|
322
|
-
<h3>
|
|
322
|
+
<h3>RPC Endpoints</h3>
|
|
323
323
|
{#each surface.rpc_endpoints as endpoint (endpoint.path)}
|
|
324
324
|
<div class="row" style:gap="var(--space_sm)" style:align-items="center">
|
|
325
325
|
<code>{endpoint.path}</code>
|
|
@@ -380,7 +380,7 @@
|
|
|
380
380
|
{/if}
|
|
381
381
|
|
|
382
382
|
{#if surface.ws_endpoints.length}
|
|
383
|
-
<h3>
|
|
383
|
+
<h3>WebSocket Endpoints</h3>
|
|
384
384
|
{#each surface.ws_endpoints as endpoint (endpoint.path)}
|
|
385
385
|
<div
|
|
386
386
|
class="row"
|
|
@@ -458,7 +458,7 @@
|
|
|
458
458
|
{/if}
|
|
459
459
|
|
|
460
460
|
{#if surface.diagnostics.length}
|
|
461
|
-
<h3>
|
|
461
|
+
<h3>Diagnostics</h3>
|
|
462
462
|
<div style:overflow-x="auto">
|
|
463
463
|
<table>
|
|
464
464
|
<thead>
|
|
@@ -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() {
|