@broberg/app-shell 0.2.3 → 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 +24 -0
- package/css/app-shell.css +13 -3
- package/dist/index.cjs +17 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -3
- package/dist/index.d.ts +11 -3
- package/dist/index.js +17 -3
- package/dist/index.js.map +1 -1
- package/dist/preact.cjs +63 -9
- package/dist/preact.cjs.map +1 -1
- package/dist/preact.d.cts +31 -3
- package/dist/preact.d.ts +31 -3
- package/dist/preact.js +63 -10
- package/dist/preact.js.map +1 -1
- package/package.json +1 -1
package/dist/preact.d.ts
CHANGED
|
@@ -32,6 +32,12 @@ declare function isActivePath(href: string, currentPath: string): boolean;
|
|
|
32
32
|
* http(s) URL is followed; anything else returns null and the row just closes.
|
|
33
33
|
*/
|
|
34
34
|
declare function safeNavigate(target: string | null | undefined): string | null;
|
|
35
|
+
/**
|
|
36
|
+
* F092.10 — is this link leaving the app? An http(s) link to another origin.
|
|
37
|
+
* Paths, "#…", mailto:/tel: and the app's own origin are not. Owner decision
|
|
38
|
+
* D-d28547: every external link opens in a new tab.
|
|
39
|
+
*/
|
|
40
|
+
declare function isExternal(href: string | null | undefined, origin?: string | undefined): boolean;
|
|
35
41
|
/**
|
|
36
42
|
* The label of the nav item the user is on, for the content header (F092.4).
|
|
37
43
|
* Sub-items count, and the most specific match wins: on "/models/genesis" a
|
|
@@ -99,8 +105,9 @@ declare const TEXT: {
|
|
|
99
105
|
readonly da: {
|
|
100
106
|
readonly openMenu: "Åbn menu";
|
|
101
107
|
readonly closeMenu: "Luk menu";
|
|
102
|
-
readonly couldNotMarkAll: "Kunne ikke markere alle som læst. Prøv igen.";
|
|
103
108
|
readonly home: "Forside";
|
|
109
|
+
readonly language: "Sprog";
|
|
110
|
+
readonly couldNotMarkAll: "Kunne ikke markere alle som læst. Prøv igen.";
|
|
104
111
|
readonly collapse: "Skjul sidemenu";
|
|
105
112
|
readonly expand: "Vis sidemenu";
|
|
106
113
|
readonly notifications: "Notifikationer";
|
|
@@ -126,8 +133,9 @@ declare const TEXT: {
|
|
|
126
133
|
readonly en: {
|
|
127
134
|
readonly openMenu: "Open menu";
|
|
128
135
|
readonly closeMenu: "Close menu";
|
|
129
|
-
readonly couldNotMarkAll: "Could not mark all as read. Try again.";
|
|
130
136
|
readonly home: "Home";
|
|
137
|
+
readonly language: "Language";
|
|
138
|
+
readonly couldNotMarkAll: "Could not mark all as read. Try again.";
|
|
131
139
|
readonly collapse: "Hide sidebar";
|
|
132
140
|
readonly expand: "Show sidebar";
|
|
133
141
|
readonly notifications: "Notifications";
|
|
@@ -181,6 +189,8 @@ interface SidebarProps {
|
|
|
181
189
|
homeHref?: string;
|
|
182
190
|
/** Accessible name of the brandMark link (it has no visible text). Default "Forside"/"Home". */
|
|
183
191
|
homeLabel?: string;
|
|
192
|
+
/** data-testid of the brand link in the sidebar head. */
|
|
193
|
+
brandTestId?: string;
|
|
184
194
|
/** Phone, drawer mode: is the drawer open. */
|
|
185
195
|
mobileOpen?: boolean;
|
|
186
196
|
onClose?: () => void;
|
|
@@ -224,6 +234,18 @@ interface UserMenuProps {
|
|
|
224
234
|
onNavigate?: Navigate;
|
|
225
235
|
/** Show Theme · Palette · Surfaces · Backdrop. Default true. */
|
|
226
236
|
appearance?: boolean;
|
|
237
|
+
/**
|
|
238
|
+
* F092.11 — a «Sprog»/«Language» row with a segmented control, like the
|
|
239
|
+
* appearance rows. The app owns the language; the shell shows and reports it.
|
|
240
|
+
*/
|
|
241
|
+
language?: {
|
|
242
|
+
value: string;
|
|
243
|
+
options: {
|
|
244
|
+
id: string;
|
|
245
|
+
label: string;
|
|
246
|
+
}[];
|
|
247
|
+
onChange: (id: string) => void;
|
|
248
|
+
};
|
|
227
249
|
lang: Lang;
|
|
228
250
|
}
|
|
229
251
|
declare function Avatar({ user, size }: {
|
|
@@ -296,6 +318,12 @@ interface AppShellProps {
|
|
|
296
318
|
collapse?: "offcanvas" | "icon";
|
|
297
319
|
/** The app's logo icon for the top of the icon rail. */
|
|
298
320
|
brandMark?: ComponentChildren;
|
|
321
|
+
/**
|
|
322
|
+
* F092.8 — "inset" (default, shadcn dashboard-01): no full-width top bar; the
|
|
323
|
+
* brand heads the sidebar, and the content's top row is sidebar button ·
|
|
324
|
+
* title · actions · bell · user menu. "topbar": the 0.2.x full-width bar.
|
|
325
|
+
*/
|
|
326
|
+
layout?: "inset" | "topbar";
|
|
299
327
|
/** F092.7 — the app's start page. Brand and brandMark become links to it; on a phone the click closes the drawer. */
|
|
300
328
|
homeHref?: string;
|
|
301
329
|
/** Accessible name for the brandMark link. Default "Forside"/"Home". */
|
|
@@ -313,4 +341,4 @@ interface AppShellProps {
|
|
|
313
341
|
}
|
|
314
342
|
declare function AppShell(p: AppShellProps): JSX.Element;
|
|
315
343
|
|
|
316
|
-
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 };
|
|
344
|
+
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, isExternal, readFlag, readSet, relativeTime, safeNavigate, useIsMobile, writeFlag, writeSet };
|
package/dist/preact.js
CHANGED
|
@@ -23,6 +23,18 @@ function safeNavigate(target) {
|
|
|
23
23
|
return null;
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
+
function isExternal(href, origin = typeof location !== "undefined" ? location.origin : void 0) {
|
|
27
|
+
if (typeof href !== "string") return false;
|
|
28
|
+
const h = href.trim();
|
|
29
|
+
if (!origin) return /^(https?:)?\/\//i.test(h);
|
|
30
|
+
let u;
|
|
31
|
+
try {
|
|
32
|
+
u = new URL(h, origin);
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
return (u.protocol === "http:" || u.protocol === "https:") && u.origin !== origin;
|
|
37
|
+
}
|
|
26
38
|
function activeNavLabel(groups, footer, currentPath) {
|
|
27
39
|
let best;
|
|
28
40
|
const visit = (it) => {
|
|
@@ -171,8 +183,9 @@ var TEXT = {
|
|
|
171
183
|
da: {
|
|
172
184
|
openMenu: "\xC5bn menu",
|
|
173
185
|
closeMenu: "Luk menu",
|
|
174
|
-
couldNotMarkAll: "Kunne ikke markere alle som l\xE6st. Pr\xF8v igen.",
|
|
175
186
|
home: "Forside",
|
|
187
|
+
language: "Sprog",
|
|
188
|
+
couldNotMarkAll: "Kunne ikke markere alle som l\xE6st. Pr\xF8v igen.",
|
|
176
189
|
collapse: "Skjul sidemenu",
|
|
177
190
|
expand: "Vis sidemenu",
|
|
178
191
|
notifications: "Notifikationer",
|
|
@@ -198,8 +211,9 @@ var TEXT = {
|
|
|
198
211
|
en: {
|
|
199
212
|
openMenu: "Open menu",
|
|
200
213
|
closeMenu: "Close menu",
|
|
201
|
-
couldNotMarkAll: "Could not mark all as read. Try again.",
|
|
202
214
|
home: "Home",
|
|
215
|
+
language: "Language",
|
|
216
|
+
couldNotMarkAll: "Could not mark all as read. Try again.",
|
|
203
217
|
collapse: "Hide sidebar",
|
|
204
218
|
expand: "Show sidebar",
|
|
205
219
|
notifications: "Notifications",
|
|
@@ -238,6 +252,9 @@ function useIsMobile() {
|
|
|
238
252
|
}
|
|
239
253
|
function NavLink(props) {
|
|
240
254
|
const { href, onNavigate, onAfter, children, ...rest } = props;
|
|
255
|
+
if (isExternal(href)) {
|
|
256
|
+
return /* @__PURE__ */ jsx("a", { ...rest, href, target: "_blank", rel: "noopener noreferrer", onClick: () => onAfter?.(), children });
|
|
257
|
+
}
|
|
241
258
|
return /* @__PURE__ */ jsx(
|
|
242
259
|
"a",
|
|
243
260
|
{
|
|
@@ -413,7 +430,7 @@ function Sidebar(p) {
|
|
|
413
430
|
"aria-label": "Navigation",
|
|
414
431
|
children: [
|
|
415
432
|
p.header || p.brandMark ? /* @__PURE__ */ jsxs("div", { class: "bas-sidebar__head", children: [
|
|
416
|
-
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,
|
|
433
|
+
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": p.brandTestId ?? "sidebar-brand-home", children: p.header }) : p.header }) : null,
|
|
417
434
|
p.brandMark ? /* @__PURE__ */ jsx("div", { class: "bas-sidebar__mark", "data-testid": "sidebar-brand-mark", children: p.homeHref ? /* @__PURE__ */ jsx(
|
|
418
435
|
NavLink,
|
|
419
436
|
{
|
|
@@ -530,7 +547,9 @@ function NotificationBell({ source, onNavigate, lang }) {
|
|
|
530
547
|
await shell.refresh();
|
|
531
548
|
}
|
|
532
549
|
const target = safeNavigate(n.navigate);
|
|
533
|
-
if (target) {
|
|
550
|
+
if (target && isExternal(target)) {
|
|
551
|
+
if (typeof window !== "undefined") window.open(target, "_blank", "noopener,noreferrer");
|
|
552
|
+
} else if (target) {
|
|
534
553
|
if (onNavigate) onNavigate(target);
|
|
535
554
|
else if (typeof window !== "undefined") window.location.assign(target);
|
|
536
555
|
}
|
|
@@ -606,7 +625,16 @@ function Avatar({ user, size = 28 }) {
|
|
|
606
625
|
return /* @__PURE__ */ jsx("span", { class: "bas-avatar bas-avatar--initials", style, "data-testid": "avatar-initials", "aria-hidden": "true", children: initials(user.name, user.email) });
|
|
607
626
|
}
|
|
608
627
|
function Segmented(props) {
|
|
609
|
-
|
|
628
|
+
const onKeyDown = (e) => {
|
|
629
|
+
if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
|
|
630
|
+
const buttons = [...e.currentTarget.querySelectorAll("button")];
|
|
631
|
+
const i = buttons.indexOf(document.activeElement);
|
|
632
|
+
if (i < 0) return;
|
|
633
|
+
e.preventDefault();
|
|
634
|
+
const next = (i + (e.key === "ArrowRight" ? 1 : -1) + buttons.length) % buttons.length;
|
|
635
|
+
buttons[next].focus();
|
|
636
|
+
};
|
|
637
|
+
return /* @__PURE__ */ jsx("div", { class: "bas-seg", "data-testid": props.testid, role: "group", "aria-label": props.label, onKeyDown, children: props.options.map(([v, label]) => /* @__PURE__ */ jsx("button", { type: "button", "aria-pressed": props.value === v, "data-testid": `${props.testid}-${v}`, onClick: () => props.onChange(v), children: label }, v)) });
|
|
610
638
|
}
|
|
611
639
|
function UserMenu(p) {
|
|
612
640
|
const t = TEXT[p.lang];
|
|
@@ -675,7 +703,7 @@ function UserMenu(p) {
|
|
|
675
703
|
] })
|
|
676
704
|
] }),
|
|
677
705
|
/* @__PURE__ */ jsx("div", { class: "bas-sep" }),
|
|
678
|
-
p.accountHref ? /* @__PURE__ */ jsxs(
|
|
706
|
+
p.accountHref ? /* @__PURE__ */ jsxs(NavLink, { href: p.accountHref, onNavigate: p.onNavigate, onAfter: () => setOpen(false), class: "bas-mi", "data-testid": "user-menu-account", role: "menuitem", children: [
|
|
679
707
|
/* @__PURE__ */ jsx(IconUser, {}),
|
|
680
708
|
/* @__PURE__ */ jsx("span", { children: t.account })
|
|
681
709
|
] }) : null,
|
|
@@ -734,6 +762,22 @@ function UserMenu(p) {
|
|
|
734
762
|
] })
|
|
735
763
|
] })
|
|
736
764
|
] }) : null,
|
|
765
|
+
p.language ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
766
|
+
p.appearance === false ? /* @__PURE__ */ jsx("div", { class: "bas-sep" }) : null,
|
|
767
|
+
/* @__PURE__ */ jsx("div", { class: "bas-prefs", children: /* @__PURE__ */ jsxs("div", { class: "bas-pref", children: [
|
|
768
|
+
/* @__PURE__ */ jsx("span", { children: t.language }),
|
|
769
|
+
/* @__PURE__ */ jsx(
|
|
770
|
+
Segmented,
|
|
771
|
+
{
|
|
772
|
+
testid: "user-menu-language",
|
|
773
|
+
label: t.language,
|
|
774
|
+
value: p.language.value,
|
|
775
|
+
options: p.language.options.map((o) => [o.id, o.label]),
|
|
776
|
+
onChange: p.language.onChange
|
|
777
|
+
}
|
|
778
|
+
)
|
|
779
|
+
] }) })
|
|
780
|
+
] }) : null,
|
|
737
781
|
p.onSignOut ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
738
782
|
/* @__PURE__ */ jsx("div", { class: "bas-sep" }),
|
|
739
783
|
/* @__PURE__ */ jsxs("button", { type: "button", class: "bas-mi is-danger", role: "menuitem", "data-testid": "topbar-sign-out", disabled: busy, onClick: () => void signOut(), children: [
|
|
@@ -783,6 +827,8 @@ function PageTabs(p) {
|
|
|
783
827
|
function AppShell(p) {
|
|
784
828
|
const prefix = p.storageKey ?? "broberg-app-shell";
|
|
785
829
|
const mode = p.mobile ?? "drawer";
|
|
830
|
+
const layout = p.layout ?? "inset";
|
|
831
|
+
const inset = layout === "inset";
|
|
786
832
|
const collapse = p.collapse ?? (mode === "rail" ? "icon" : "offcanvas");
|
|
787
833
|
const [collapsed, setCollapsed] = useState(() => readFlag(`${prefix}.collapsed`, false));
|
|
788
834
|
const [drawer, setDrawer] = useState(false);
|
|
@@ -798,8 +844,8 @@ function AppShell(p) {
|
|
|
798
844
|
const expanded = drawerMode ? drawer : !collapsed;
|
|
799
845
|
const triggerLabel = drawerMode ? drawer ? t.closeMenu : t.openMenu : collapsed ? t.expand : t.collapse;
|
|
800
846
|
const heading = p.title ?? activeNavLabel(p.groups, p.footer, p.currentPath);
|
|
801
|
-
return /* @__PURE__ */ jsxs("div", { class: "bas-root", "data-testid": "app-shell", "data-mobile": mode, children: [
|
|
802
|
-
/* @__PURE__ */ jsx(
|
|
847
|
+
return /* @__PURE__ */ jsxs("div", { class: "bas-root", "data-testid": "app-shell", "data-mobile": mode, "data-layout": layout, children: [
|
|
848
|
+
inset ? null : /* @__PURE__ */ jsx(
|
|
803
849
|
TopBar,
|
|
804
850
|
{
|
|
805
851
|
brand: p.brand,
|
|
@@ -829,7 +875,8 @@ function AppShell(p) {
|
|
|
829
875
|
mobileOpen: drawer,
|
|
830
876
|
onClose: () => setDrawer(false),
|
|
831
877
|
mobile: mode,
|
|
832
|
-
header: mode === "drawer" ? p.brand : void 0,
|
|
878
|
+
header: inset || mode === "drawer" ? p.brand : void 0,
|
|
879
|
+
brandTestId: inset ? "brand-home" : void 0,
|
|
833
880
|
groupsKey: `${prefix}.groups`,
|
|
834
881
|
itemsKey: `${prefix}.items`,
|
|
835
882
|
lang: p.lang
|
|
@@ -853,6 +900,12 @@ function AppShell(p) {
|
|
|
853
900
|
heading ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
854
901
|
/* @__PURE__ */ jsx("span", { class: "bas-contenthead__sep", "data-testid": "content-header-separator", "aria-hidden": "true" }),
|
|
855
902
|
/* @__PURE__ */ jsx("h1", { class: "bas-contenthead__title", "data-testid": "content-header-title", children: heading })
|
|
903
|
+
] }) : null,
|
|
904
|
+
inset ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
905
|
+
/* @__PURE__ */ jsx("div", { class: "bas-spacer" }),
|
|
906
|
+
p.actions ? /* @__PURE__ */ jsx("div", { class: "bas-contenthead__actions", children: p.actions }) : null,
|
|
907
|
+
/* @__PURE__ */ jsx(NotificationBell, { source: p.notifications, onNavigate: p.onNavigate, lang: p.lang }),
|
|
908
|
+
p.user ? /* @__PURE__ */ jsx(UserMenu, { user: p.user, ...p.userMenu, onNavigate: p.onNavigate, lang: p.lang }) : null
|
|
856
909
|
] }) : null
|
|
857
910
|
] }),
|
|
858
911
|
p.children
|
|
@@ -861,6 +914,6 @@ function AppShell(p) {
|
|
|
861
914
|
] });
|
|
862
915
|
}
|
|
863
916
|
|
|
864
|
-
export { AppShell, Avatar, NotificationBell, PageHeader, PageTabs, Sidebar, TEXT, TopBar, UserMenu, activeNavLabel, badgeLabel, createFetchNotificationSource, createMemoryNotificationSource, initials, isActivePath, readFlag, readSet, relativeTime, safeNavigate, useIsMobile, writeFlag, writeSet };
|
|
917
|
+
export { AppShell, Avatar, NotificationBell, PageHeader, PageTabs, Sidebar, TEXT, TopBar, UserMenu, activeNavLabel, badgeLabel, createFetchNotificationSource, createMemoryNotificationSource, initials, isActivePath, isExternal, readFlag, readSet, relativeTime, safeNavigate, useIsMobile, writeFlag, writeSet };
|
|
865
918
|
//# sourceMappingURL=preact.js.map
|
|
866
919
|
//# sourceMappingURL=preact.js.map
|