@broberg/app-shell 0.1.0 → 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 +45 -4
- package/css/app-shell.css +55 -24
- package/dist/index.cjs +30 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +23 -1
- package/dist/index.d.ts +23 -1
- package/dist/index.js +29 -1
- package/dist/index.js.map +1 -1
- package/dist/preact.cjs +192 -22
- package/dist/preact.cjs.map +1 -1
- package/dist/preact.d.cts +60 -5
- package/dist/preact.d.ts +60 -5
- package/dist/preact.js +191 -23
- package/dist/preact.js.map +1 -1
- package/package.json +5 -5
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;
|
|
@@ -20,6 +26,18 @@ interface NavGroup<Icon = unknown> {
|
|
|
20
26
|
* item or below it ("/board/123" is on "/board", "/boardroom" is not).
|
|
21
27
|
*/
|
|
22
28
|
declare function isActivePath(href: string, currentPath: string): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* A notification's `navigate` is SERVER DATA, and it ends in `location.assign`.
|
|
31
|
+
* `javascript:` there runs code in the app, so only a same-site path or an
|
|
32
|
+
* http(s) URL is followed; anything else returns null and the row just closes.
|
|
33
|
+
*/
|
|
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;
|
|
23
41
|
/** "99+" above 99, nothing for 0 or less. */
|
|
24
42
|
declare function badgeLabel(n: number | undefined): string | null;
|
|
25
43
|
/** Read a boolean flag from localStorage; storage errors read as `fallback`. */
|
|
@@ -80,6 +98,8 @@ declare function relativeTime(ts: string | number, lang: "da" | "en", now?: numb
|
|
|
80
98
|
declare const TEXT: {
|
|
81
99
|
readonly da: {
|
|
82
100
|
readonly openMenu: "Åbn menu";
|
|
101
|
+
readonly closeMenu: "Luk menu";
|
|
102
|
+
readonly home: "Forside";
|
|
83
103
|
readonly collapse: "Skjul sidemenu";
|
|
84
104
|
readonly expand: "Vis sidemenu";
|
|
85
105
|
readonly notifications: "Notifikationer";
|
|
@@ -104,6 +124,8 @@ declare const TEXT: {
|
|
|
104
124
|
};
|
|
105
125
|
readonly en: {
|
|
106
126
|
readonly openMenu: "Open menu";
|
|
127
|
+
readonly closeMenu: "Close menu";
|
|
128
|
+
readonly home: "Home";
|
|
107
129
|
readonly collapse: "Hide sidebar";
|
|
108
130
|
readonly expand: "Show sidebar";
|
|
109
131
|
readonly notifications: "Notifications";
|
|
@@ -143,9 +165,20 @@ interface SidebarProps {
|
|
|
143
165
|
footer?: PreactNavItem[];
|
|
144
166
|
currentPath: string;
|
|
145
167
|
onNavigate?: Navigate;
|
|
146
|
-
/** Desktop:
|
|
168
|
+
/** Desktop: is the sidebar folded (to nothing, or to the icon rail — see `collapse`). */
|
|
147
169
|
collapsed?: boolean;
|
|
148
|
-
|
|
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;
|
|
149
182
|
/** Phone, drawer mode: is the drawer open. */
|
|
150
183
|
mobileOpen?: boolean;
|
|
151
184
|
onClose?: () => void;
|
|
@@ -154,6 +187,8 @@ interface SidebarProps {
|
|
|
154
187
|
header?: ComponentChildren;
|
|
155
188
|
/** localStorage key for which groups are folded. */
|
|
156
189
|
groupsKey?: string;
|
|
190
|
+
/** localStorage key for which items with children are open (F092.5). */
|
|
191
|
+
itemsKey?: string;
|
|
157
192
|
lang: Lang;
|
|
158
193
|
}
|
|
159
194
|
declare function Sidebar(p: SidebarProps): JSX.Element;
|
|
@@ -195,6 +230,10 @@ declare function Avatar({ user, size }: {
|
|
|
195
230
|
}): JSX.Element;
|
|
196
231
|
declare function UserMenu(p: UserMenuProps): JSX.Element;
|
|
197
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;
|
|
198
237
|
brand?: ComponentChildren;
|
|
199
238
|
/** Anything app-specific (project switcher, search) between brand and bell. */
|
|
200
239
|
actions?: ComponentChildren;
|
|
@@ -241,9 +280,25 @@ interface AppShellProps {
|
|
|
241
280
|
notifications?: NotificationSource;
|
|
242
281
|
user?: ShellUser;
|
|
243
282
|
userMenu?: Omit<UserMenuProps, "user" | "lang" | "onNavigate">;
|
|
244
|
-
/** Phone behaviour: a slide-in drawer
|
|
283
|
+
/** Phone behaviour: a slide-in drawer, or a 60px icon rail. */
|
|
245
284
|
mobile?: "drawer" | "rail";
|
|
246
|
-
/**
|
|
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. */
|
|
247
302
|
storageKey?: string;
|
|
248
303
|
/** Required: the shell ships no default language (see @broberg/notifications/shell). */
|
|
249
304
|
lang: Lang;
|
|
@@ -251,4 +306,4 @@ interface AppShellProps {
|
|
|
251
306
|
}
|
|
252
307
|
declare function AppShell(p: AppShellProps): JSX.Element;
|
|
253
308
|
|
|
254
|
-
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, 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
|
@@ -12,6 +12,30 @@ function isActivePath(href, currentPath) {
|
|
|
12
12
|
if (target === "/") return path === "/";
|
|
13
13
|
return path === target || path.startsWith(target + "/");
|
|
14
14
|
}
|
|
15
|
+
function safeNavigate(target) {
|
|
16
|
+
if (typeof target !== "string") return null;
|
|
17
|
+
const t = target.trim();
|
|
18
|
+
if (t.startsWith("/") && !t.startsWith("//") && !t.startsWith("/\\")) return t;
|
|
19
|
+
try {
|
|
20
|
+
const u = new URL(t);
|
|
21
|
+
return u.protocol === "https:" || u.protocol === "http:" ? u.href : null;
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
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
|
+
}
|
|
15
39
|
function badgeLabel(n) {
|
|
16
40
|
if (!n || n <= 0) return null;
|
|
17
41
|
return n > 99 ? "99+" : String(n);
|
|
@@ -146,6 +170,8 @@ function relativeTime(ts, lang, now = Date.now()) {
|
|
|
146
170
|
var TEXT = {
|
|
147
171
|
da: {
|
|
148
172
|
openMenu: "\xC5bn menu",
|
|
173
|
+
closeMenu: "Luk menu",
|
|
174
|
+
home: "Forside",
|
|
149
175
|
collapse: "Skjul sidemenu",
|
|
150
176
|
expand: "Vis sidemenu",
|
|
151
177
|
notifications: "Notifikationer",
|
|
@@ -170,6 +196,8 @@ var TEXT = {
|
|
|
170
196
|
},
|
|
171
197
|
en: {
|
|
172
198
|
openMenu: "Open menu",
|
|
199
|
+
closeMenu: "Close menu",
|
|
200
|
+
home: "Home",
|
|
173
201
|
collapse: "Hide sidebar",
|
|
174
202
|
expand: "Show sidebar",
|
|
175
203
|
notifications: "Notifications",
|
|
@@ -227,8 +255,10 @@ function NavLink(props) {
|
|
|
227
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 });
|
|
228
256
|
var IconMenu = () => svg(/* @__PURE__ */ jsx("path", { d: "M4 6h16M4 12h16M4 18h16" }), 20);
|
|
229
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) });
|
|
230
|
-
var
|
|
231
|
-
|
|
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);
|
|
232
262
|
var IconBell = () => svg(/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
233
263
|
/* @__PURE__ */ jsx("path", { d: "M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9" }),
|
|
234
264
|
/* @__PURE__ */ jsx("path", { d: "M10.3 21a1.94 1.94 0 0 0 3.4 0" })
|
|
@@ -243,8 +273,12 @@ var IconUser = () => svg(/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
|
243
273
|
/* @__PURE__ */ jsx("path", { d: "M4 21a8 8 0 0 1 16 0" })
|
|
244
274
|
] }), 14);
|
|
245
275
|
function Sidebar(p) {
|
|
246
|
-
const t = TEXT[p.lang];
|
|
247
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";
|
|
248
282
|
const [folded, setFolded] = useState(() => readSet(key));
|
|
249
283
|
const toggleGroup = (label) => {
|
|
250
284
|
const next = new Set(folded);
|
|
@@ -252,9 +286,47 @@ function Sidebar(p) {
|
|
|
252
286
|
setFolded(next);
|
|
253
287
|
writeSet(key, next);
|
|
254
288
|
};
|
|
255
|
-
const
|
|
256
|
-
const
|
|
257
|
-
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) => {
|
|
258
330
|
const active = isActivePath(it.href, p.currentPath);
|
|
259
331
|
const badge = badgeLabel(it.badge);
|
|
260
332
|
return /* @__PURE__ */ jsxs(
|
|
@@ -263,12 +335,13 @@ function Sidebar(p) {
|
|
|
263
335
|
href: it.href,
|
|
264
336
|
onNavigate: p.onNavigate,
|
|
265
337
|
onAfter: close,
|
|
266
|
-
class: "bas-item" + (active ? " is-active" : ""),
|
|
338
|
+
class: "bas-item" + (sub ? " bas-item--sub" : "") + (active ? " is-active" : ""),
|
|
267
339
|
"data-testid": `sidebar-item-${it.id}`,
|
|
268
340
|
"aria-current": active ? "page" : void 0,
|
|
341
|
+
"aria-label": railed && !sub ? it.label : void 0,
|
|
269
342
|
title: it.label,
|
|
270
343
|
children: [
|
|
271
|
-
active ? /* @__PURE__ */ jsx("span", { class: "bas-item__bar" }) : null,
|
|
344
|
+
active && !sub ? /* @__PURE__ */ jsx("span", { class: "bas-item__bar" }) : null,
|
|
272
345
|
it.icon ? /* @__PURE__ */ jsx("span", { class: "bas-item__icon", children: it.icon }) : null,
|
|
273
346
|
/* @__PURE__ */ jsx("span", { class: "bas-item__label", children: it.label }),
|
|
274
347
|
badge ? /* @__PURE__ */ jsx("span", { class: "bas-item__count", "data-testid": `sidebar-badge-${it.id}`, children: badge }) : null
|
|
@@ -277,22 +350,84 @@ function Sidebar(p) {
|
|
|
277
350
|
it.id
|
|
278
351
|
);
|
|
279
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);
|
|
280
402
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
281
403
|
mode === "drawer" ? /* @__PURE__ */ jsx("div", { class: "bas-backdrop" + (p.mobileOpen ? " is-open" : ""), "data-testid": "sidebar-backdrop", onClick: close }) : null,
|
|
282
404
|
/* @__PURE__ */ jsxs(
|
|
283
405
|
"aside",
|
|
284
406
|
{
|
|
285
|
-
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" : ""),
|
|
286
408
|
"data-testid": "sidebar-root",
|
|
287
409
|
"data-mobile": mode,
|
|
410
|
+
"data-collapse": collapseMode,
|
|
288
411
|
"aria-label": "Navigation",
|
|
289
412
|
children: [
|
|
290
|
-
/* @__PURE__ */ jsxs("div", { class: "bas-sidebar__head", children: [
|
|
291
|
-
p.header ? /* @__PURE__ */ jsx("div", { class: "bas-sidebar__brand", children: p.
|
|
292
|
-
p.
|
|
293
|
-
|
|
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,
|
|
294
429
|
/* @__PURE__ */ jsx("nav", { class: "bas-sidebar__nav", children: p.groups.map((g) => {
|
|
295
|
-
const open = !folded.has(g.label);
|
|
430
|
+
const open = railed || !folded.has(g.label);
|
|
296
431
|
const slug = g.label.toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
297
432
|
return /* @__PURE__ */ jsxs("div", { class: "bas-group", children: [
|
|
298
433
|
/* @__PURE__ */ jsxs("button", { type: "button", class: "bas-group__label", "data-testid": `sidebar-group-${slug}`, "aria-expanded": open, onClick: () => toggleGroup(g.label), children: [
|
|
@@ -364,9 +499,10 @@ function NotificationBell({ source, onNavigate, lang }) {
|
|
|
364
499
|
await source.markSeen([n.id]);
|
|
365
500
|
await shell.refresh();
|
|
366
501
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
502
|
+
const target = safeNavigate(n.navigate);
|
|
503
|
+
if (target) {
|
|
504
|
+
if (onNavigate) onNavigate(target);
|
|
505
|
+
else if (typeof window !== "undefined") window.location.assign(target);
|
|
370
506
|
}
|
|
371
507
|
};
|
|
372
508
|
const seen = (n) => n.seenAt != null || state.clearedIds.includes(n.id);
|
|
@@ -579,7 +715,7 @@ function TopBar(p) {
|
|
|
579
715
|
const t = TEXT[p.lang];
|
|
580
716
|
return /* @__PURE__ */ jsx("header", { class: "bas-topbar", "data-testid": "topbar-root", children: /* @__PURE__ */ jsxs("div", { class: "bas-topbar__inner", children: [
|
|
581
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,
|
|
582
|
-
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,
|
|
583
719
|
p.actions ? /* @__PURE__ */ jsx("div", { class: "bas-topbar__actions", children: p.actions }) : null,
|
|
584
720
|
/* @__PURE__ */ jsx("div", { class: "bas-spacer" }),
|
|
585
721
|
/* @__PURE__ */ jsx(NotificationBell, { source: p.notifications, onNavigate: p.onNavigate, lang: p.lang }),
|
|
@@ -614,21 +750,29 @@ function PageTabs(p) {
|
|
|
614
750
|
function AppShell(p) {
|
|
615
751
|
const prefix = p.storageKey ?? "broberg-app-shell";
|
|
616
752
|
const mode = p.mobile ?? "drawer";
|
|
753
|
+
const collapse = p.collapse ?? (mode === "rail" ? "icon" : "offcanvas");
|
|
617
754
|
const [collapsed, setCollapsed] = useState(() => readFlag(`${prefix}.collapsed`, false));
|
|
618
755
|
const [drawer, setDrawer] = useState(false);
|
|
756
|
+
const isMobile = useIsMobile();
|
|
619
757
|
const t = TEXT[p.lang];
|
|
620
758
|
const toggle = () => {
|
|
621
759
|
const next = !collapsed;
|
|
622
760
|
setCollapsed(next);
|
|
623
761
|
writeFlag(`${prefix}.collapsed`, next);
|
|
624
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);
|
|
625
768
|
return /* @__PURE__ */ jsxs("div", { class: "bas-root", "data-testid": "app-shell", "data-mobile": mode, children: [
|
|
626
769
|
/* @__PURE__ */ jsx(
|
|
627
770
|
TopBar,
|
|
628
771
|
{
|
|
629
772
|
brand: p.brand,
|
|
773
|
+
homeHref: p.homeHref,
|
|
774
|
+
onHome: () => setDrawer(false),
|
|
630
775
|
actions: p.actions,
|
|
631
|
-
onOpenMenu: mode === "drawer" ? () => setDrawer(true) : void 0,
|
|
632
776
|
notifications: p.notifications,
|
|
633
777
|
user: p.user,
|
|
634
778
|
userMenu: p.userMenu,
|
|
@@ -645,21 +789,45 @@ function AppShell(p) {
|
|
|
645
789
|
currentPath: p.currentPath,
|
|
646
790
|
onNavigate: p.onNavigate,
|
|
647
791
|
collapsed,
|
|
648
|
-
|
|
792
|
+
collapse,
|
|
793
|
+
brandMark: p.brandMark,
|
|
794
|
+
homeHref: p.homeHref,
|
|
795
|
+
homeLabel: p.homeLabel,
|
|
649
796
|
mobileOpen: drawer,
|
|
650
797
|
onClose: () => setDrawer(false),
|
|
651
798
|
mobile: mode,
|
|
652
799
|
header: mode === "drawer" ? p.brand : void 0,
|
|
653
800
|
groupsKey: `${prefix}.groups`,
|
|
801
|
+
itemsKey: `${prefix}.items`,
|
|
654
802
|
lang: p.lang
|
|
655
803
|
}
|
|
656
804
|
),
|
|
657
|
-
|
|
658
|
-
|
|
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
|
+
] })
|
|
659
827
|
] })
|
|
660
828
|
] });
|
|
661
829
|
}
|
|
662
830
|
|
|
663
|
-
export { AppShell, Avatar, NotificationBell, PageHeader, PageTabs, Sidebar, TEXT, TopBar, UserMenu, badgeLabel, createFetchNotificationSource, createMemoryNotificationSource, initials, isActivePath, readFlag, readSet, relativeTime, 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 };
|
|
664
832
|
//# sourceMappingURL=preact.js.map
|
|
665
833
|
//# sourceMappingURL=preact.js.map
|