@broberg/app-shell 0.2.2 → 0.3.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 +31 -0
- package/css/app-shell.css +13 -3
- package/dist/index.cjs +17 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.js +17 -1
- package/dist/index.js.map +1 -1
- package/dist/preact.cjs +97 -10
- package/dist/preact.cjs.map +1 -1
- package/dist/preact.d.cts +31 -1
- package/dist/preact.d.ts +31 -1
- package/dist/preact.js +97 -11
- package/dist/preact.js.map +1 -1
- package/package.json +1 -1
package/dist/preact.cjs
CHANGED
|
@@ -25,6 +25,18 @@ function safeNavigate(target) {
|
|
|
25
25
|
return null;
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
+
function isExternal(href, origin = typeof location !== "undefined" ? location.origin : void 0) {
|
|
29
|
+
if (typeof href !== "string") return false;
|
|
30
|
+
const h = href.trim();
|
|
31
|
+
if (!origin) return /^(https?:)?\/\//i.test(h);
|
|
32
|
+
let u;
|
|
33
|
+
try {
|
|
34
|
+
u = new URL(h, origin);
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
return (u.protocol === "http:" || u.protocol === "https:") && u.origin !== origin;
|
|
39
|
+
}
|
|
28
40
|
function activeNavLabel(groups, footer, currentPath) {
|
|
29
41
|
let best;
|
|
30
42
|
const visit = (it) => {
|
|
@@ -174,6 +186,8 @@ var TEXT = {
|
|
|
174
186
|
openMenu: "\xC5bn menu",
|
|
175
187
|
closeMenu: "Luk menu",
|
|
176
188
|
home: "Forside",
|
|
189
|
+
language: "Sprog",
|
|
190
|
+
couldNotMarkAll: "Kunne ikke markere alle som l\xE6st. Pr\xF8v igen.",
|
|
177
191
|
collapse: "Skjul sidemenu",
|
|
178
192
|
expand: "Vis sidemenu",
|
|
179
193
|
notifications: "Notifikationer",
|
|
@@ -200,6 +214,8 @@ var TEXT = {
|
|
|
200
214
|
openMenu: "Open menu",
|
|
201
215
|
closeMenu: "Close menu",
|
|
202
216
|
home: "Home",
|
|
217
|
+
language: "Language",
|
|
218
|
+
couldNotMarkAll: "Could not mark all as read. Try again.",
|
|
203
219
|
collapse: "Hide sidebar",
|
|
204
220
|
expand: "Show sidebar",
|
|
205
221
|
notifications: "Notifications",
|
|
@@ -238,6 +254,9 @@ function useIsMobile() {
|
|
|
238
254
|
}
|
|
239
255
|
function NavLink(props) {
|
|
240
256
|
const { href, onNavigate, onAfter, children, ...rest } = props;
|
|
257
|
+
if (isExternal(href)) {
|
|
258
|
+
return /* @__PURE__ */ jsxRuntime.jsx("a", { ...rest, href, target: "_blank", rel: "noopener noreferrer", onClick: () => onAfter?.(), children });
|
|
259
|
+
}
|
|
241
260
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
242
261
|
"a",
|
|
243
262
|
{
|
|
@@ -413,7 +432,7 @@ function Sidebar(p) {
|
|
|
413
432
|
"aria-label": "Navigation",
|
|
414
433
|
children: [
|
|
415
434
|
p.header || p.brandMark ? /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "bas-sidebar__head", children: [
|
|
416
|
-
p.header ? /* @__PURE__ */ jsxRuntime.jsx("div", { class: "bas-sidebar__brand", children: p.homeHref ? /* @__PURE__ */ jsxRuntime.jsx(NavLink, { href: p.homeHref, onNavigate: p.onNavigate, onAfter: close, class: "bas-home", "data-testid": "sidebar-brand-home", children: p.header }) : p.header }) : null,
|
|
435
|
+
p.header ? /* @__PURE__ */ jsxRuntime.jsx("div", { class: "bas-sidebar__brand", children: p.homeHref ? /* @__PURE__ */ jsxRuntime.jsx(NavLink, { href: p.homeHref, onNavigate: p.onNavigate, onAfter: close, class: "bas-home", "data-testid": p.brandTestId ?? "sidebar-brand-home", children: p.header }) : p.header }) : null,
|
|
417
436
|
p.brandMark ? /* @__PURE__ */ jsxRuntime.jsx("div", { class: "bas-sidebar__mark", "data-testid": "sidebar-brand-mark", children: p.homeHref ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
418
437
|
NavLink,
|
|
419
438
|
{
|
|
@@ -486,14 +505,42 @@ function NotificationBell({ source, onNavigate, lang }) {
|
|
|
486
505
|
else void shell$1.refresh();
|
|
487
506
|
});
|
|
488
507
|
}, [shell$1, source]);
|
|
508
|
+
const bellRef = hooks.useRef(null);
|
|
509
|
+
const panelRef = hooks.useRef(null);
|
|
489
510
|
hooks.useEffect(() => {
|
|
490
511
|
if (!state.open) return;
|
|
491
512
|
const onKey = (e) => {
|
|
492
|
-
if (e.key === "Escape" && shell$1.escape() === "close")
|
|
513
|
+
if (e.key === "Escape" && shell$1.escape() === "close") {
|
|
514
|
+
e.preventDefault();
|
|
515
|
+
bellRef.current?.focus();
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
const onDown = (e) => {
|
|
519
|
+
const t2 = e.target;
|
|
520
|
+
if (t2 && (panelRef.current?.contains(t2) || bellRef.current?.contains(t2))) return;
|
|
521
|
+
shell$1.close();
|
|
493
522
|
};
|
|
494
523
|
document.addEventListener("keydown", onKey);
|
|
495
|
-
|
|
524
|
+
document.addEventListener("mousedown", onDown);
|
|
525
|
+
return () => {
|
|
526
|
+
document.removeEventListener("keydown", onKey);
|
|
527
|
+
document.removeEventListener("mousedown", onDown);
|
|
528
|
+
};
|
|
496
529
|
}, [state.open, shell$1]);
|
|
530
|
+
const [markAllFailed, setMarkAllFailed] = hooks.useState(false);
|
|
531
|
+
hooks.useEffect(() => {
|
|
532
|
+
if (!state.open) setMarkAllFailed(false);
|
|
533
|
+
}, [state.open]);
|
|
534
|
+
const onMarkAll = async () => {
|
|
535
|
+
setMarkAllFailed(false);
|
|
536
|
+
try {
|
|
537
|
+
await shell$1.markAllSeen();
|
|
538
|
+
} catch {
|
|
539
|
+
setMarkAllFailed(true);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
shell$1.close();
|
|
543
|
+
};
|
|
497
544
|
const label = badgeLabel(state.count);
|
|
498
545
|
const onRow = async (n) => {
|
|
499
546
|
shell$1.close();
|
|
@@ -502,7 +549,9 @@ function NotificationBell({ source, onNavigate, lang }) {
|
|
|
502
549
|
await shell$1.refresh();
|
|
503
550
|
}
|
|
504
551
|
const target = safeNavigate(n.navigate);
|
|
505
|
-
if (target) {
|
|
552
|
+
if (target && isExternal(target)) {
|
|
553
|
+
if (typeof window !== "undefined") window.open(target, "_blank", "noopener,noreferrer");
|
|
554
|
+
} else if (target) {
|
|
506
555
|
if (onNavigate) onNavigate(target);
|
|
507
556
|
else if (typeof window !== "undefined") window.location.assign(target);
|
|
508
557
|
}
|
|
@@ -513,6 +562,7 @@ function NotificationBell({ source, onNavigate, lang }) {
|
|
|
513
562
|
"button",
|
|
514
563
|
{
|
|
515
564
|
type: "button",
|
|
565
|
+
ref: bellRef,
|
|
516
566
|
class: "bas-iconbtn",
|
|
517
567
|
"data-testid": "topbar-notifications",
|
|
518
568
|
"aria-label": t.notifications,
|
|
@@ -532,6 +582,7 @@ function NotificationBell({ source, onNavigate, lang }) {
|
|
|
532
582
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
533
583
|
"div",
|
|
534
584
|
{
|
|
585
|
+
ref: panelRef,
|
|
535
586
|
class: "bas-panel " + (mobile ? "bas-panel--sheet" : "bas-panel--drop"),
|
|
536
587
|
role: "dialog",
|
|
537
588
|
"aria-label": shell$1.labels.panel,
|
|
@@ -539,8 +590,9 @@ function NotificationBell({ source, onNavigate, lang }) {
|
|
|
539
590
|
children: [
|
|
540
591
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "bas-panel__head", children: [
|
|
541
592
|
/* @__PURE__ */ jsxRuntime.jsx("h3", { children: t.notifications }),
|
|
542
|
-
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", class: "bas-link", "data-testid": "notifications-mark-all", onClick: () => void
|
|
593
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", class: "bas-link", "data-testid": "notifications-mark-all", onClick: () => void onMarkAll(), disabled: !state.count, children: shell$1.labels.markAll })
|
|
543
594
|
] }),
|
|
595
|
+
markAllFailed ? /* @__PURE__ */ jsxRuntime.jsx("p", { class: "bas-panel__note is-error", role: "alert", "data-testid": "notifications-mark-all-error", children: t.couldNotMarkAll }) : null,
|
|
544
596
|
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "bas-panel__list", "data-testid": "notifications-list", children: error && state.rows.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { class: "bas-panel__note is-error", "data-testid": "notifications-error", children: t.couldNotLoad }) : state.rows.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { class: "bas-panel__note", "data-testid": "notifications-empty", children: shell$1.labels.empty }) : state.rows.map((n) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
545
597
|
"button",
|
|
546
598
|
{
|
|
@@ -575,7 +627,16 @@ function Avatar({ user, size = 28 }) {
|
|
|
575
627
|
return /* @__PURE__ */ jsxRuntime.jsx("span", { class: "bas-avatar bas-avatar--initials", style, "data-testid": "avatar-initials", "aria-hidden": "true", children: initials(user.name, user.email) });
|
|
576
628
|
}
|
|
577
629
|
function Segmented(props) {
|
|
578
|
-
|
|
630
|
+
const onKeyDown = (e) => {
|
|
631
|
+
if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
|
|
632
|
+
const buttons = [...e.currentTarget.querySelectorAll("button")];
|
|
633
|
+
const i = buttons.indexOf(document.activeElement);
|
|
634
|
+
if (i < 0) return;
|
|
635
|
+
e.preventDefault();
|
|
636
|
+
const next = (i + (e.key === "ArrowRight" ? 1 : -1) + buttons.length) % buttons.length;
|
|
637
|
+
buttons[next].focus();
|
|
638
|
+
};
|
|
639
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { class: "bas-seg", "data-testid": props.testid, role: "group", "aria-label": props.label, onKeyDown, children: props.options.map(([v, label]) => /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "aria-pressed": props.value === v, "data-testid": `${props.testid}-${v}`, onClick: () => props.onChange(v), children: label }, v)) });
|
|
579
640
|
}
|
|
580
641
|
function UserMenu(p) {
|
|
581
642
|
const t = TEXT[p.lang];
|
|
@@ -644,7 +705,7 @@ function UserMenu(p) {
|
|
|
644
705
|
] })
|
|
645
706
|
] }),
|
|
646
707
|
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "bas-sep" }),
|
|
647
|
-
p.accountHref ? /* @__PURE__ */ jsxRuntime.jsxs(
|
|
708
|
+
p.accountHref ? /* @__PURE__ */ jsxRuntime.jsxs(NavLink, { href: p.accountHref, onNavigate: p.onNavigate, onAfter: () => setOpen(false), class: "bas-mi", "data-testid": "user-menu-account", role: "menuitem", children: [
|
|
648
709
|
/* @__PURE__ */ jsxRuntime.jsx(IconUser, {}),
|
|
649
710
|
/* @__PURE__ */ jsxRuntime.jsx("span", { children: t.account })
|
|
650
711
|
] }) : null,
|
|
@@ -703,6 +764,22 @@ function UserMenu(p) {
|
|
|
703
764
|
] })
|
|
704
765
|
] })
|
|
705
766
|
] }) : null,
|
|
767
|
+
p.language ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
768
|
+
p.appearance === false ? /* @__PURE__ */ jsxRuntime.jsx("div", { class: "bas-sep" }) : null,
|
|
769
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "bas-prefs", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "bas-pref", children: [
|
|
770
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: t.language }),
|
|
771
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
772
|
+
Segmented,
|
|
773
|
+
{
|
|
774
|
+
testid: "user-menu-language",
|
|
775
|
+
label: t.language,
|
|
776
|
+
value: p.language.value,
|
|
777
|
+
options: p.language.options.map((o) => [o.id, o.label]),
|
|
778
|
+
onChange: p.language.onChange
|
|
779
|
+
}
|
|
780
|
+
)
|
|
781
|
+
] }) })
|
|
782
|
+
] }) : null,
|
|
706
783
|
p.onSignOut ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
707
784
|
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "bas-sep" }),
|
|
708
785
|
/* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", class: "bas-mi is-danger", role: "menuitem", "data-testid": "topbar-sign-out", disabled: busy, onClick: () => void signOut(), children: [
|
|
@@ -752,6 +829,8 @@ function PageTabs(p) {
|
|
|
752
829
|
function AppShell(p) {
|
|
753
830
|
const prefix = p.storageKey ?? "broberg-app-shell";
|
|
754
831
|
const mode = p.mobile ?? "drawer";
|
|
832
|
+
const layout = p.layout ?? "inset";
|
|
833
|
+
const inset = layout === "inset";
|
|
755
834
|
const collapse = p.collapse ?? (mode === "rail" ? "icon" : "offcanvas");
|
|
756
835
|
const [collapsed, setCollapsed] = hooks.useState(() => readFlag(`${prefix}.collapsed`, false));
|
|
757
836
|
const [drawer, setDrawer] = hooks.useState(false);
|
|
@@ -767,8 +846,8 @@ function AppShell(p) {
|
|
|
767
846
|
const expanded = drawerMode ? drawer : !collapsed;
|
|
768
847
|
const triggerLabel = drawerMode ? drawer ? t.closeMenu : t.openMenu : collapsed ? t.expand : t.collapse;
|
|
769
848
|
const heading = p.title ?? activeNavLabel(p.groups, p.footer, p.currentPath);
|
|
770
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "bas-root", "data-testid": "app-shell", "data-mobile": mode, children: [
|
|
771
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
849
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "bas-root", "data-testid": "app-shell", "data-mobile": mode, "data-layout": layout, children: [
|
|
850
|
+
inset ? null : /* @__PURE__ */ jsxRuntime.jsx(
|
|
772
851
|
TopBar,
|
|
773
852
|
{
|
|
774
853
|
brand: p.brand,
|
|
@@ -798,7 +877,8 @@ function AppShell(p) {
|
|
|
798
877
|
mobileOpen: drawer,
|
|
799
878
|
onClose: () => setDrawer(false),
|
|
800
879
|
mobile: mode,
|
|
801
|
-
header: mode === "drawer" ? p.brand : void 0,
|
|
880
|
+
header: inset || mode === "drawer" ? p.brand : void 0,
|
|
881
|
+
brandTestId: inset ? "brand-home" : void 0,
|
|
802
882
|
groupsKey: `${prefix}.groups`,
|
|
803
883
|
itemsKey: `${prefix}.items`,
|
|
804
884
|
lang: p.lang
|
|
@@ -822,6 +902,12 @@ function AppShell(p) {
|
|
|
822
902
|
heading ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
823
903
|
/* @__PURE__ */ jsxRuntime.jsx("span", { class: "bas-contenthead__sep", "data-testid": "content-header-separator", "aria-hidden": "true" }),
|
|
824
904
|
/* @__PURE__ */ jsxRuntime.jsx("h1", { class: "bas-contenthead__title", "data-testid": "content-header-title", children: heading })
|
|
905
|
+
] }) : null,
|
|
906
|
+
inset ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
907
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "bas-spacer" }),
|
|
908
|
+
p.actions ? /* @__PURE__ */ jsxRuntime.jsx("div", { class: "bas-contenthead__actions", children: p.actions }) : null,
|
|
909
|
+
/* @__PURE__ */ jsxRuntime.jsx(NotificationBell, { source: p.notifications, onNavigate: p.onNavigate, lang: p.lang }),
|
|
910
|
+
p.user ? /* @__PURE__ */ jsxRuntime.jsx(UserMenu, { user: p.user, ...p.userMenu, onNavigate: p.onNavigate, lang: p.lang }) : null
|
|
825
911
|
] }) : null
|
|
826
912
|
] }),
|
|
827
913
|
p.children
|
|
@@ -845,6 +931,7 @@ exports.createFetchNotificationSource = createFetchNotificationSource;
|
|
|
845
931
|
exports.createMemoryNotificationSource = createMemoryNotificationSource;
|
|
846
932
|
exports.initials = initials;
|
|
847
933
|
exports.isActivePath = isActivePath;
|
|
934
|
+
exports.isExternal = isExternal;
|
|
848
935
|
exports.readFlag = readFlag;
|
|
849
936
|
exports.readSet = readSet;
|
|
850
937
|
exports.relativeTime = relativeTime;
|