@broberg/app-shell 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -4
- package/css/app-shell.css +55 -24
- package/dist/index.cjs +18 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -1
- package/dist/index.d.ts +17 -1
- package/dist/index.js +18 -1
- package/dist/index.js.map +1 -1
- package/dist/preact.cjs +176 -19
- package/dist/preact.cjs.map +1 -1
- package/dist/preact.d.cts +54 -5
- package/dist/preact.d.ts +54 -5
- package/dist/preact.js +176 -20
- package/dist/preact.js.map +1 -1
- package/package.json +1 -1
package/dist/preact.d.ts
CHANGED
|
@@ -10,6 +10,12 @@ interface NavItem<Icon = unknown> {
|
|
|
10
10
|
icon?: Icon;
|
|
11
11
|
/** A count shown on the item (99+ above 99). 0/undefined shows nothing. */
|
|
12
12
|
badge?: number;
|
|
13
|
+
/**
|
|
14
|
+
* Sub-items, ONE level (F092.5). An item with children is a toggle that opens
|
|
15
|
+
* and closes them, not a link; its own `href` is used only to decide whether
|
|
16
|
+
* it is on the current path.
|
|
17
|
+
*/
|
|
18
|
+
children?: NavItem<Icon>[];
|
|
13
19
|
}
|
|
14
20
|
interface NavGroup<Icon = unknown> {
|
|
15
21
|
label: string;
|
|
@@ -26,6 +32,12 @@ declare function isActivePath(href: string, currentPath: string): boolean;
|
|
|
26
32
|
* http(s) URL is followed; anything else returns null and the row just closes.
|
|
27
33
|
*/
|
|
28
34
|
declare function safeNavigate(target: string | null | undefined): string | null;
|
|
35
|
+
/**
|
|
36
|
+
* The label of the nav item the user is on, for the content header (F092.4).
|
|
37
|
+
* Sub-items count, and the most specific match wins: on "/models/genesis" a
|
|
38
|
+
* "Genesis" child beats its "Models" parent.
|
|
39
|
+
*/
|
|
40
|
+
declare function activeNavLabel<Icon>(groups: NavGroup<Icon>[], footer: NavItem<Icon>[] | undefined, currentPath: string): string | undefined;
|
|
29
41
|
/** "99+" above 99, nothing for 0 or less. */
|
|
30
42
|
declare function badgeLabel(n: number | undefined): string | null;
|
|
31
43
|
/** Read a boolean flag from localStorage; storage errors read as `fallback`. */
|
|
@@ -86,6 +98,8 @@ declare function relativeTime(ts: string | number, lang: "da" | "en", now?: numb
|
|
|
86
98
|
declare const TEXT: {
|
|
87
99
|
readonly da: {
|
|
88
100
|
readonly openMenu: "Åbn menu";
|
|
101
|
+
readonly closeMenu: "Luk menu";
|
|
102
|
+
readonly home: "Forside";
|
|
89
103
|
readonly collapse: "Skjul sidemenu";
|
|
90
104
|
readonly expand: "Vis sidemenu";
|
|
91
105
|
readonly notifications: "Notifikationer";
|
|
@@ -110,6 +124,8 @@ declare const TEXT: {
|
|
|
110
124
|
};
|
|
111
125
|
readonly en: {
|
|
112
126
|
readonly openMenu: "Open menu";
|
|
127
|
+
readonly closeMenu: "Close menu";
|
|
128
|
+
readonly home: "Home";
|
|
113
129
|
readonly collapse: "Hide sidebar";
|
|
114
130
|
readonly expand: "Show sidebar";
|
|
115
131
|
readonly notifications: "Notifications";
|
|
@@ -149,9 +165,20 @@ interface SidebarProps {
|
|
|
149
165
|
footer?: PreactNavItem[];
|
|
150
166
|
currentPath: string;
|
|
151
167
|
onNavigate?: Navigate;
|
|
152
|
-
/** Desktop:
|
|
168
|
+
/** Desktop: is the sidebar folded (to nothing, or to the icon rail — see `collapse`). */
|
|
153
169
|
collapsed?: boolean;
|
|
154
|
-
|
|
170
|
+
/**
|
|
171
|
+
* F092.6 — what "folded" means on desktop, as shadcn's `collapsible`:
|
|
172
|
+
* "offcanvas" hides the sidebar, "icon" keeps a 48px rail of item icons with
|
|
173
|
+
* `brandMark` on top. Default: "icon" when `mobile` is "rail", else "offcanvas".
|
|
174
|
+
*/
|
|
175
|
+
collapse?: "offcanvas" | "icon";
|
|
176
|
+
/** The app's logo icon, shown at the top of the icon rail. */
|
|
177
|
+
brandMark?: ComponentChildren;
|
|
178
|
+
/** F092.7 — the app's start page: `header` and `brandMark` become links to it. */
|
|
179
|
+
homeHref?: string;
|
|
180
|
+
/** Accessible name of the brandMark link (it has no visible text). Default "Forside"/"Home". */
|
|
181
|
+
homeLabel?: string;
|
|
155
182
|
/** Phone, drawer mode: is the drawer open. */
|
|
156
183
|
mobileOpen?: boolean;
|
|
157
184
|
onClose?: () => void;
|
|
@@ -160,6 +187,8 @@ interface SidebarProps {
|
|
|
160
187
|
header?: ComponentChildren;
|
|
161
188
|
/** localStorage key for which groups are folded. */
|
|
162
189
|
groupsKey?: string;
|
|
190
|
+
/** localStorage key for which items with children are open (F092.5). */
|
|
191
|
+
itemsKey?: string;
|
|
163
192
|
lang: Lang;
|
|
164
193
|
}
|
|
165
194
|
declare function Sidebar(p: SidebarProps): JSX.Element;
|
|
@@ -201,6 +230,10 @@ declare function Avatar({ user, size }: {
|
|
|
201
230
|
}): JSX.Element;
|
|
202
231
|
declare function UserMenu(p: UserMenuProps): JSX.Element;
|
|
203
232
|
interface TopBarProps {
|
|
233
|
+
/** F092.7 — when set, the brand is a link to the app's start page. */
|
|
234
|
+
homeHref?: string;
|
|
235
|
+
/** Called after the brand link was followed (AppShell closes the drawer). */
|
|
236
|
+
onHome?: () => void;
|
|
204
237
|
brand?: ComponentChildren;
|
|
205
238
|
/** Anything app-specific (project switcher, search) between brand and bell. */
|
|
206
239
|
actions?: ComponentChildren;
|
|
@@ -247,9 +280,25 @@ interface AppShellProps {
|
|
|
247
280
|
notifications?: NotificationSource;
|
|
248
281
|
user?: ShellUser;
|
|
249
282
|
userMenu?: Omit<UserMenuProps, "user" | "lang" | "onNavigate">;
|
|
250
|
-
/** Phone behaviour: a slide-in drawer
|
|
283
|
+
/** Phone behaviour: a slide-in drawer, or a 60px icon rail. */
|
|
251
284
|
mobile?: "drawer" | "rail";
|
|
252
|
-
/**
|
|
285
|
+
/**
|
|
286
|
+
* F092.6 — desktop fold: "offcanvas" hides the sidebar, "icon" keeps a 48px
|
|
287
|
+
* icon rail with `brandMark` on top. Default: "icon" when `mobile` is "rail".
|
|
288
|
+
*/
|
|
289
|
+
collapse?: "offcanvas" | "icon";
|
|
290
|
+
/** The app's logo icon for the top of the icon rail. */
|
|
291
|
+
brandMark?: ComponentChildren;
|
|
292
|
+
/** F092.7 — the app's start page. Brand and brandMark become links to it; on a phone the click closes the drawer. */
|
|
293
|
+
homeHref?: string;
|
|
294
|
+
/** Accessible name for the brandMark link. Default "Forside"/"Home". */
|
|
295
|
+
homeLabel?: string;
|
|
296
|
+
/**
|
|
297
|
+
* F092.4 — the page title in the content header, after the sidebar button.
|
|
298
|
+
* Absent: the label of the active nav item (a sub-item wins over its parent).
|
|
299
|
+
*/
|
|
300
|
+
title?: ComponentChildren;
|
|
301
|
+
/** localStorage key prefix for the folded sidebar, folded groups and open items. */
|
|
253
302
|
storageKey?: string;
|
|
254
303
|
/** Required: the shell ships no default language (see @broberg/notifications/shell). */
|
|
255
304
|
lang: Lang;
|
|
@@ -257,4 +306,4 @@ interface AppShellProps {
|
|
|
257
306
|
}
|
|
258
307
|
declare function AppShell(p: AppShellProps): JSX.Element;
|
|
259
308
|
|
|
260
|
-
export { AppShell, type AppShellProps, Avatar, type Lang, type NavGroup, type NavItem, type Navigate, NotificationBell, type NotificationBellProps, type NotificationSource, PageHeader, type PageTab, PageTabs, type PreactNavGroup, type PreactNavItem, type ShellUser, Sidebar, type SidebarProps, TEXT, TopBar, type TopBarProps, UserMenu, type UserMenuItem, type UserMenuProps, badgeLabel, createFetchNotificationSource, createMemoryNotificationSource, initials, isActivePath, readFlag, readSet, relativeTime, safeNavigate, useIsMobile, writeFlag, writeSet };
|
|
309
|
+
export { AppShell, type AppShellProps, Avatar, type Lang, type NavGroup, type NavItem, type Navigate, NotificationBell, type NotificationBellProps, type NotificationSource, PageHeader, type PageTab, PageTabs, type PreactNavGroup, type PreactNavItem, type ShellUser, Sidebar, type SidebarProps, TEXT, TopBar, type TopBarProps, UserMenu, type UserMenuItem, type UserMenuProps, activeNavLabel, badgeLabel, createFetchNotificationSource, createMemoryNotificationSource, initials, isActivePath, readFlag, readSet, relativeTime, safeNavigate, useIsMobile, writeFlag, writeSet };
|
package/dist/preact.js
CHANGED
|
@@ -23,6 +23,19 @@ function safeNavigate(target) {
|
|
|
23
23
|
return null;
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
+
function activeNavLabel(groups, footer, currentPath) {
|
|
27
|
+
let best;
|
|
28
|
+
const visit = (it) => {
|
|
29
|
+
if (it.href && isActivePath(it.href, currentPath)) {
|
|
30
|
+
const len = it.href.length;
|
|
31
|
+
if (!best || len > best.len) best = { label: it.label, len };
|
|
32
|
+
}
|
|
33
|
+
it.children?.forEach(visit);
|
|
34
|
+
};
|
|
35
|
+
for (const g of groups) g.items.forEach(visit);
|
|
36
|
+
footer?.forEach(visit);
|
|
37
|
+
return best?.label;
|
|
38
|
+
}
|
|
26
39
|
function badgeLabel(n) {
|
|
27
40
|
if (!n || n <= 0) return null;
|
|
28
41
|
return n > 99 ? "99+" : String(n);
|
|
@@ -157,6 +170,8 @@ function relativeTime(ts, lang, now = Date.now()) {
|
|
|
157
170
|
var TEXT = {
|
|
158
171
|
da: {
|
|
159
172
|
openMenu: "\xC5bn menu",
|
|
173
|
+
closeMenu: "Luk menu",
|
|
174
|
+
home: "Forside",
|
|
160
175
|
collapse: "Skjul sidemenu",
|
|
161
176
|
expand: "Vis sidemenu",
|
|
162
177
|
notifications: "Notifikationer",
|
|
@@ -181,6 +196,8 @@ var TEXT = {
|
|
|
181
196
|
},
|
|
182
197
|
en: {
|
|
183
198
|
openMenu: "Open menu",
|
|
199
|
+
closeMenu: "Close menu",
|
|
200
|
+
home: "Home",
|
|
184
201
|
collapse: "Hide sidebar",
|
|
185
202
|
expand: "Show sidebar",
|
|
186
203
|
notifications: "Notifications",
|
|
@@ -238,8 +255,10 @@ function NavLink(props) {
|
|
|
238
255
|
var svg = (d, size = 18) => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", width: size, height: size, fill: "none", stroke: "currentColor", "stroke-width": "1.9", "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true", children: d });
|
|
239
256
|
var IconMenu = () => svg(/* @__PURE__ */ jsx("path", { d: "M4 6h16M4 12h16M4 18h16" }), 20);
|
|
240
257
|
var IconChevron = ({ open }) => /* @__PURE__ */ jsx("span", { class: "bas-chev" + (open ? " is-open" : ""), children: svg(/* @__PURE__ */ jsx("path", { d: "m9 18 6-6-6-6" }), 12) });
|
|
241
|
-
var
|
|
242
|
-
|
|
258
|
+
var IconPanelLeft = () => svg(/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
259
|
+
/* @__PURE__ */ jsx("rect", { width: "18", height: "18", x: "3", y: "3", rx: "2" }),
|
|
260
|
+
/* @__PURE__ */ jsx("path", { d: "M9 3v18" })
|
|
261
|
+
] }), 16);
|
|
243
262
|
var IconBell = () => svg(/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
244
263
|
/* @__PURE__ */ jsx("path", { d: "M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9" }),
|
|
245
264
|
/* @__PURE__ */ jsx("path", { d: "M10.3 21a1.94 1.94 0 0 0 3.4 0" })
|
|
@@ -254,8 +273,12 @@ var IconUser = () => svg(/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
|
254
273
|
/* @__PURE__ */ jsx("path", { d: "M4 21a8 8 0 0 1 16 0" })
|
|
255
274
|
] }), 14);
|
|
256
275
|
function Sidebar(p) {
|
|
257
|
-
const t = TEXT[p.lang];
|
|
258
276
|
const key = p.groupsKey ?? "broberg-app-shell.groups";
|
|
277
|
+
const itemsKey = p.itemsKey ?? "broberg-app-shell.items";
|
|
278
|
+
const mode = p.mobile ?? "drawer";
|
|
279
|
+
const collapseMode = p.collapse ?? (mode === "rail" ? "icon" : "offcanvas");
|
|
280
|
+
const isMobile = useIsMobile();
|
|
281
|
+
const railed = isMobile ? mode === "rail" : !!p.collapsed && collapseMode === "icon";
|
|
259
282
|
const [folded, setFolded] = useState(() => readSet(key));
|
|
260
283
|
const toggleGroup = (label) => {
|
|
261
284
|
const next = new Set(folded);
|
|
@@ -263,9 +286,47 @@ function Sidebar(p) {
|
|
|
263
286
|
setFolded(next);
|
|
264
287
|
writeSet(key, next);
|
|
265
288
|
};
|
|
266
|
-
const
|
|
267
|
-
const
|
|
268
|
-
const
|
|
289
|
+
const all = [...p.groups.flatMap((g) => g.items), ...p.footer ?? []];
|
|
290
|
+
const childActive = (it) => !!it.children?.some((c) => isActivePath(c.href, p.currentPath));
|
|
291
|
+
const [openItems, setOpenItems] = useState(() => {
|
|
292
|
+
const s = readSet(itemsKey);
|
|
293
|
+
for (const it of all) if (childActive(it)) s.add(it.id);
|
|
294
|
+
return s;
|
|
295
|
+
});
|
|
296
|
+
const toggleItem = (id) => {
|
|
297
|
+
const next = new Set(openItems);
|
|
298
|
+
next.has(id) ? next.delete(id) : next.add(id);
|
|
299
|
+
setOpenItems(next);
|
|
300
|
+
writeSet(itemsKey, next);
|
|
301
|
+
};
|
|
302
|
+
const [flyout, setFlyout] = useState(null);
|
|
303
|
+
const flyoutRef = useRef(null);
|
|
304
|
+
useEffect(() => {
|
|
305
|
+
if (!flyout) return;
|
|
306
|
+
const onKey = (e) => {
|
|
307
|
+
if (e.key === "Escape") setFlyout(null);
|
|
308
|
+
};
|
|
309
|
+
const onDown = (e) => {
|
|
310
|
+
const t = e.target;
|
|
311
|
+
if (flyoutRef.current && t && flyoutRef.current.contains(t)) return;
|
|
312
|
+
if (t instanceof Element && t.closest(`[data-flyout-for="${flyout.id}"]`)) return;
|
|
313
|
+
setFlyout(null);
|
|
314
|
+
};
|
|
315
|
+
document.addEventListener("keydown", onKey);
|
|
316
|
+
document.addEventListener("mousedown", onDown);
|
|
317
|
+
return () => {
|
|
318
|
+
document.removeEventListener("keydown", onKey);
|
|
319
|
+
document.removeEventListener("mousedown", onDown);
|
|
320
|
+
};
|
|
321
|
+
}, [flyout]);
|
|
322
|
+
useEffect(() => {
|
|
323
|
+
if (!railed) setFlyout(null);
|
|
324
|
+
}, [railed]);
|
|
325
|
+
const close = () => {
|
|
326
|
+
setFlyout(null);
|
|
327
|
+
p.onClose?.();
|
|
328
|
+
};
|
|
329
|
+
const leaf = (it, sub = false) => {
|
|
269
330
|
const active = isActivePath(it.href, p.currentPath);
|
|
270
331
|
const badge = badgeLabel(it.badge);
|
|
271
332
|
return /* @__PURE__ */ jsxs(
|
|
@@ -274,12 +335,13 @@ function Sidebar(p) {
|
|
|
274
335
|
href: it.href,
|
|
275
336
|
onNavigate: p.onNavigate,
|
|
276
337
|
onAfter: close,
|
|
277
|
-
class: "bas-item" + (active ? " is-active" : ""),
|
|
338
|
+
class: "bas-item" + (sub ? " bas-item--sub" : "") + (active ? " is-active" : ""),
|
|
278
339
|
"data-testid": `sidebar-item-${it.id}`,
|
|
279
340
|
"aria-current": active ? "page" : void 0,
|
|
341
|
+
"aria-label": railed && !sub ? it.label : void 0,
|
|
280
342
|
title: it.label,
|
|
281
343
|
children: [
|
|
282
|
-
active ? /* @__PURE__ */ jsx("span", { class: "bas-item__bar" }) : null,
|
|
344
|
+
active && !sub ? /* @__PURE__ */ jsx("span", { class: "bas-item__bar" }) : null,
|
|
283
345
|
it.icon ? /* @__PURE__ */ jsx("span", { class: "bas-item__icon", children: it.icon }) : null,
|
|
284
346
|
/* @__PURE__ */ jsx("span", { class: "bas-item__label", children: it.label }),
|
|
285
347
|
badge ? /* @__PURE__ */ jsx("span", { class: "bas-item__count", "data-testid": `sidebar-badge-${it.id}`, children: badge }) : null
|
|
@@ -288,22 +350,84 @@ function Sidebar(p) {
|
|
|
288
350
|
it.id
|
|
289
351
|
);
|
|
290
352
|
};
|
|
353
|
+
const parent = (it) => {
|
|
354
|
+
const children = it.children;
|
|
355
|
+
const onPage = childActive(it) || isActivePath(it.href, p.currentPath);
|
|
356
|
+
const open = railed ? flyout?.id === it.id : openItems.has(it.id);
|
|
357
|
+
const onClick = (e) => {
|
|
358
|
+
if (!railed) return toggleItem(it.id);
|
|
359
|
+
if (flyout?.id === it.id) return setFlyout(null);
|
|
360
|
+
const r = e.currentTarget.getBoundingClientRect();
|
|
361
|
+
setFlyout({ id: it.id, top: r.top, left: r.right + 6 });
|
|
362
|
+
};
|
|
363
|
+
return /* @__PURE__ */ jsxs("div", { class: "bas-itemgroup", children: [
|
|
364
|
+
/* @__PURE__ */ jsxs(
|
|
365
|
+
"button",
|
|
366
|
+
{
|
|
367
|
+
type: "button",
|
|
368
|
+
class: "bas-item bas-item--parent" + (onPage ? " is-parent-active" : ""),
|
|
369
|
+
"data-testid": `sidebar-item-${it.id}-toggle`,
|
|
370
|
+
"data-flyout-for": it.id,
|
|
371
|
+
"aria-expanded": open,
|
|
372
|
+
"aria-label": railed ? it.label : void 0,
|
|
373
|
+
title: it.label,
|
|
374
|
+
onClick,
|
|
375
|
+
children: [
|
|
376
|
+
onPage && railed ? /* @__PURE__ */ jsx("span", { class: "bas-item__bar" }) : null,
|
|
377
|
+
it.icon ? /* @__PURE__ */ jsx("span", { class: "bas-item__icon", children: it.icon }) : null,
|
|
378
|
+
/* @__PURE__ */ jsx("span", { class: "bas-item__label", children: it.label }),
|
|
379
|
+
/* @__PURE__ */ jsx("span", { class: "bas-item__chev", children: /* @__PURE__ */ jsx(IconChevron, { open }) })
|
|
380
|
+
]
|
|
381
|
+
}
|
|
382
|
+
),
|
|
383
|
+
!railed && open ? /* @__PURE__ */ jsx("div", { class: "bas-sub", children: children.map((c) => leaf(c, true)) }) : null,
|
|
384
|
+
railed && flyout?.id === it.id ? /* @__PURE__ */ jsxs(
|
|
385
|
+
"div",
|
|
386
|
+
{
|
|
387
|
+
ref: flyoutRef,
|
|
388
|
+
class: "bas-flyout",
|
|
389
|
+
"data-testid": `sidebar-flyout-${it.id}`,
|
|
390
|
+
role: "menu",
|
|
391
|
+
"aria-label": it.label,
|
|
392
|
+
style: { top: `${flyout.top}px`, left: `${flyout.left}px` },
|
|
393
|
+
children: [
|
|
394
|
+
/* @__PURE__ */ jsx("div", { class: "bas-flyout__title", children: it.label }),
|
|
395
|
+
children.map((c) => leaf(c, true))
|
|
396
|
+
]
|
|
397
|
+
}
|
|
398
|
+
) : null
|
|
399
|
+
] }, it.id);
|
|
400
|
+
};
|
|
401
|
+
const item = (it) => it.children?.length ? parent(it) : leaf(it);
|
|
291
402
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
292
403
|
mode === "drawer" ? /* @__PURE__ */ jsx("div", { class: "bas-backdrop" + (p.mobileOpen ? " is-open" : ""), "data-testid": "sidebar-backdrop", onClick: close }) : null,
|
|
293
404
|
/* @__PURE__ */ jsxs(
|
|
294
405
|
"aside",
|
|
295
406
|
{
|
|
296
|
-
class: `bas-sidebar is-mobile-${mode}` + (p.mobileOpen ? " is-open" : "") + (p.collapsed ? " is-collapsed" : ""),
|
|
407
|
+
class: `bas-sidebar is-mobile-${mode}` + (p.mobileOpen ? " is-open" : "") + (p.collapsed ? " is-collapsed" : "") + (railed ? " is-railed" : ""),
|
|
297
408
|
"data-testid": "sidebar-root",
|
|
298
409
|
"data-mobile": mode,
|
|
410
|
+
"data-collapse": collapseMode,
|
|
299
411
|
"aria-label": "Navigation",
|
|
300
412
|
children: [
|
|
301
|
-
/* @__PURE__ */ jsxs("div", { class: "bas-sidebar__head", children: [
|
|
302
|
-
p.header ? /* @__PURE__ */ jsx("div", { class: "bas-sidebar__brand", children: p.
|
|
303
|
-
p.
|
|
304
|
-
|
|
413
|
+
p.header || p.brandMark ? /* @__PURE__ */ jsxs("div", { class: "bas-sidebar__head", children: [
|
|
414
|
+
p.header ? /* @__PURE__ */ jsx("div", { class: "bas-sidebar__brand", children: p.homeHref ? /* @__PURE__ */ jsx(NavLink, { href: p.homeHref, onNavigate: p.onNavigate, onAfter: close, class: "bas-home", "data-testid": "sidebar-brand-home", children: p.header }) : p.header }) : null,
|
|
415
|
+
p.brandMark ? /* @__PURE__ */ jsx("div", { class: "bas-sidebar__mark", "data-testid": "sidebar-brand-mark", children: p.homeHref ? /* @__PURE__ */ jsx(
|
|
416
|
+
NavLink,
|
|
417
|
+
{
|
|
418
|
+
href: p.homeHref,
|
|
419
|
+
onNavigate: p.onNavigate,
|
|
420
|
+
onAfter: close,
|
|
421
|
+
class: "bas-home",
|
|
422
|
+
"data-testid": "brand-mark-home",
|
|
423
|
+
"aria-label": p.homeLabel ?? TEXT[p.lang].home,
|
|
424
|
+
title: p.homeLabel ?? TEXT[p.lang].home,
|
|
425
|
+
children: p.brandMark
|
|
426
|
+
}
|
|
427
|
+
) : p.brandMark }) : null
|
|
428
|
+
] }) : null,
|
|
305
429
|
/* @__PURE__ */ jsx("nav", { class: "bas-sidebar__nav", children: p.groups.map((g) => {
|
|
306
|
-
const open = !folded.has(g.label);
|
|
430
|
+
const open = railed || !folded.has(g.label);
|
|
307
431
|
const slug = g.label.toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
308
432
|
return /* @__PURE__ */ jsxs("div", { class: "bas-group", children: [
|
|
309
433
|
/* @__PURE__ */ jsxs("button", { type: "button", class: "bas-group__label", "data-testid": `sidebar-group-${slug}`, "aria-expanded": open, onClick: () => toggleGroup(g.label), children: [
|
|
@@ -591,7 +715,7 @@ function TopBar(p) {
|
|
|
591
715
|
const t = TEXT[p.lang];
|
|
592
716
|
return /* @__PURE__ */ jsx("header", { class: "bas-topbar", "data-testid": "topbar-root", children: /* @__PURE__ */ jsxs("div", { class: "bas-topbar__inner", children: [
|
|
593
717
|
p.onOpenMenu ? /* @__PURE__ */ jsx("button", { type: "button", class: "bas-iconbtn bas-hamburger", "data-testid": "sidebar-toggle", "aria-label": t.openMenu, onClick: p.onOpenMenu, children: /* @__PURE__ */ jsx(IconMenu, {}) }) : null,
|
|
594
|
-
p.brand ? /* @__PURE__ */ jsx("div", { class: "bas-topbar__brand", children: p.brand }) : null,
|
|
718
|
+
p.brand ? /* @__PURE__ */ jsx("div", { class: "bas-topbar__brand", children: p.homeHref ? /* @__PURE__ */ jsx(NavLink, { href: p.homeHref, onNavigate: p.onNavigate, onAfter: p.onHome, class: "bas-home", "data-testid": "brand-home", children: p.brand }) : p.brand }) : null,
|
|
595
719
|
p.actions ? /* @__PURE__ */ jsx("div", { class: "bas-topbar__actions", children: p.actions }) : null,
|
|
596
720
|
/* @__PURE__ */ jsx("div", { class: "bas-spacer" }),
|
|
597
721
|
/* @__PURE__ */ jsx(NotificationBell, { source: p.notifications, onNavigate: p.onNavigate, lang: p.lang }),
|
|
@@ -626,21 +750,29 @@ function PageTabs(p) {
|
|
|
626
750
|
function AppShell(p) {
|
|
627
751
|
const prefix = p.storageKey ?? "broberg-app-shell";
|
|
628
752
|
const mode = p.mobile ?? "drawer";
|
|
753
|
+
const collapse = p.collapse ?? (mode === "rail" ? "icon" : "offcanvas");
|
|
629
754
|
const [collapsed, setCollapsed] = useState(() => readFlag(`${prefix}.collapsed`, false));
|
|
630
755
|
const [drawer, setDrawer] = useState(false);
|
|
756
|
+
const isMobile = useIsMobile();
|
|
631
757
|
const t = TEXT[p.lang];
|
|
632
758
|
const toggle = () => {
|
|
633
759
|
const next = !collapsed;
|
|
634
760
|
setCollapsed(next);
|
|
635
761
|
writeFlag(`${prefix}.collapsed`, next);
|
|
636
762
|
};
|
|
763
|
+
const drawerMode = isMobile && mode === "drawer";
|
|
764
|
+
const onTrigger = () => drawerMode ? setDrawer(!drawer) : toggle();
|
|
765
|
+
const expanded = drawerMode ? drawer : !collapsed;
|
|
766
|
+
const triggerLabel = drawerMode ? drawer ? t.closeMenu : t.openMenu : collapsed ? t.expand : t.collapse;
|
|
767
|
+
const heading = p.title ?? activeNavLabel(p.groups, p.footer, p.currentPath);
|
|
637
768
|
return /* @__PURE__ */ jsxs("div", { class: "bas-root", "data-testid": "app-shell", "data-mobile": mode, children: [
|
|
638
769
|
/* @__PURE__ */ jsx(
|
|
639
770
|
TopBar,
|
|
640
771
|
{
|
|
641
772
|
brand: p.brand,
|
|
773
|
+
homeHref: p.homeHref,
|
|
774
|
+
onHome: () => setDrawer(false),
|
|
642
775
|
actions: p.actions,
|
|
643
|
-
onOpenMenu: mode === "drawer" ? () => setDrawer(true) : void 0,
|
|
644
776
|
notifications: p.notifications,
|
|
645
777
|
user: p.user,
|
|
646
778
|
userMenu: p.userMenu,
|
|
@@ -657,21 +789,45 @@ function AppShell(p) {
|
|
|
657
789
|
currentPath: p.currentPath,
|
|
658
790
|
onNavigate: p.onNavigate,
|
|
659
791
|
collapsed,
|
|
660
|
-
|
|
792
|
+
collapse,
|
|
793
|
+
brandMark: p.brandMark,
|
|
794
|
+
homeHref: p.homeHref,
|
|
795
|
+
homeLabel: p.homeLabel,
|
|
661
796
|
mobileOpen: drawer,
|
|
662
797
|
onClose: () => setDrawer(false),
|
|
663
798
|
mobile: mode,
|
|
664
799
|
header: mode === "drawer" ? p.brand : void 0,
|
|
665
800
|
groupsKey: `${prefix}.groups`,
|
|
801
|
+
itemsKey: `${prefix}.items`,
|
|
666
802
|
lang: p.lang
|
|
667
803
|
}
|
|
668
804
|
),
|
|
669
|
-
|
|
670
|
-
|
|
805
|
+
/* @__PURE__ */ jsxs("main", { class: "bas-main", "data-testid": "app-content", children: [
|
|
806
|
+
/* @__PURE__ */ jsxs("div", { class: "bas-contenthead", "data-testid": "content-header", children: [
|
|
807
|
+
/* @__PURE__ */ jsx(
|
|
808
|
+
"button",
|
|
809
|
+
{
|
|
810
|
+
type: "button",
|
|
811
|
+
class: "bas-iconbtn bas-trigger",
|
|
812
|
+
"data-testid": "sidebar-trigger",
|
|
813
|
+
"aria-label": triggerLabel,
|
|
814
|
+
"aria-expanded": expanded,
|
|
815
|
+
title: triggerLabel,
|
|
816
|
+
onClick: onTrigger,
|
|
817
|
+
children: /* @__PURE__ */ jsx(IconPanelLeft, {})
|
|
818
|
+
}
|
|
819
|
+
),
|
|
820
|
+
heading ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
821
|
+
/* @__PURE__ */ jsx("span", { class: "bas-contenthead__sep", "data-testid": "content-header-separator", "aria-hidden": "true" }),
|
|
822
|
+
/* @__PURE__ */ jsx("h1", { class: "bas-contenthead__title", "data-testid": "content-header-title", children: heading })
|
|
823
|
+
] }) : null
|
|
824
|
+
] }),
|
|
825
|
+
p.children
|
|
826
|
+
] })
|
|
671
827
|
] })
|
|
672
828
|
] });
|
|
673
829
|
}
|
|
674
830
|
|
|
675
|
-
export { AppShell, Avatar, NotificationBell, PageHeader, PageTabs, Sidebar, TEXT, TopBar, UserMenu, badgeLabel, createFetchNotificationSource, createMemoryNotificationSource, initials, isActivePath, readFlag, readSet, relativeTime, safeNavigate, useIsMobile, writeFlag, writeSet };
|
|
831
|
+
export { AppShell, Avatar, NotificationBell, PageHeader, PageTabs, Sidebar, TEXT, TopBar, UserMenu, activeNavLabel, badgeLabel, createFetchNotificationSource, createMemoryNotificationSource, initials, isActivePath, readFlag, readSet, relativeTime, safeNavigate, useIsMobile, writeFlag, writeSet };
|
|
676
832
|
//# sourceMappingURL=preact.js.map
|
|
677
833
|
//# sourceMappingURL=preact.js.map
|