@broberg/app-shell 0.7.1 → 0.9.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 +44 -0
- package/css/app-shell.css +38 -1
- package/dist/preact.cjs +108 -3
- package/dist/preact.cjs.map +1 -1
- package/dist/preact.d.cts +26 -2
- package/dist/preact.d.ts +26 -2
- package/dist/preact.js +109 -5
- package/dist/preact.js.map +1 -1
- package/package.json +1 -1
package/dist/preact.d.cts
CHANGED
|
@@ -609,12 +609,36 @@ interface TenantSwitcherProps {
|
|
|
609
609
|
* why; the switcher never pretends a switch happened.
|
|
610
610
|
*/
|
|
611
611
|
onSwitch: (slug: string) => Promise<void> | void;
|
|
612
|
+
/**
|
|
613
|
+
* The role in the user's own words («Medarbejder», «Administrator»). Roles
|
|
614
|
+
* are the app's vocabulary, so the switcher cannot translate them. Without
|
|
615
|
+
* it no role is shown — a raw «member» in a Danish UI is worse than nothing.
|
|
616
|
+
*/
|
|
617
|
+
roleLabel?: (role: string) => string;
|
|
612
618
|
}
|
|
613
619
|
/**
|
|
614
620
|
* The user's own organisations, the active one marked, and a switch. Only the
|
|
615
621
|
* memberships the backend returns are listed — never a typed or guessed slug.
|
|
616
622
|
* With one organisation it is a plain label, not a menu.
|
|
617
623
|
*/
|
|
618
|
-
declare function TenantSwitcher({ lang, activeSlug, adapter, onSwitch }: TenantSwitcherProps): JSX.Element;
|
|
624
|
+
declare function TenantSwitcher({ lang, activeSlug, adapter, onSwitch, roleLabel }: TenantSwitcherProps): JSX.Element;
|
|
625
|
+
interface ConfirmDialogProps {
|
|
626
|
+
open: boolean;
|
|
627
|
+
title: ComponentChildren;
|
|
628
|
+
body?: ComponentChildren;
|
|
629
|
+
confirmLabel?: string;
|
|
630
|
+
cancelLabel?: string;
|
|
631
|
+
/** Red confirm button, for an action that cannot be undone. */
|
|
632
|
+
destructive?: boolean;
|
|
633
|
+
/** The confirm action is running: buttons disabled, the window cannot be dismissed. */
|
|
634
|
+
busy?: boolean;
|
|
635
|
+
/** Shown inside the window; the window stays open so the user can retry or cancel. */
|
|
636
|
+
error?: ComponentChildren;
|
|
637
|
+
onConfirm: () => void;
|
|
638
|
+
onCancel: () => void;
|
|
639
|
+
/** Prefix for every data-testid. Default "confirm-dialog". */
|
|
640
|
+
testId?: string;
|
|
641
|
+
}
|
|
642
|
+
declare function ConfirmDialog(p: ConfirmDialogProps): JSX.Element | null;
|
|
619
643
|
|
|
620
|
-
export { AVATAR_MAX_BYTES, AVATAR_TYPES, type AccountAdapter, AccountError, AccountPage, type AccountPageProps, type AccountProfile, type AnalyticsAdapter, type AnalyticsData, type AnalyticsDay, AnalyticsError, AnalyticsPage, type AnalyticsPageProps, 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, type TenantAdapter, TenantLoadError, type TenantMembershipRow, TenantSwitcher, type TenantSwitcherProps, TopBar, type TopBarProps, UserMenu, type UserMenuItem, type UserMenuProps, accountErrorKind, activeNavLabel, analyticsErrorKind, badgeLabel, changePct, checkAvatarFile, copenhagenDay, createFetchAccountAdapter, createFetchAnalyticsAdapter, createFetchNotificationSource, createFetchTenantAdapter, createMemoryNotificationSource, defaultReauthHref, fillDays, initials, isActivePath, isExternal, readFlag, readSet, relativeTime, safeNavigate, useIsMobile, writeFlag, writeSet };
|
|
644
|
+
export { AVATAR_MAX_BYTES, AVATAR_TYPES, type AccountAdapter, AccountError, AccountPage, type AccountPageProps, type AccountProfile, type AnalyticsAdapter, type AnalyticsData, type AnalyticsDay, AnalyticsError, AnalyticsPage, type AnalyticsPageProps, AppShell, type AppShellProps, Avatar, ConfirmDialog, type ConfirmDialogProps, 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, type TenantAdapter, TenantLoadError, type TenantMembershipRow, TenantSwitcher, type TenantSwitcherProps, TopBar, type TopBarProps, UserMenu, type UserMenuItem, type UserMenuProps, accountErrorKind, activeNavLabel, analyticsErrorKind, badgeLabel, changePct, checkAvatarFile, copenhagenDay, createFetchAccountAdapter, createFetchAnalyticsAdapter, createFetchNotificationSource, createFetchTenantAdapter, createMemoryNotificationSource, defaultReauthHref, fillDays, initials, isActivePath, isExternal, readFlag, readSet, relativeTime, safeNavigate, useIsMobile, writeFlag, writeSet };
|
package/dist/preact.d.ts
CHANGED
|
@@ -609,12 +609,36 @@ interface TenantSwitcherProps {
|
|
|
609
609
|
* why; the switcher never pretends a switch happened.
|
|
610
610
|
*/
|
|
611
611
|
onSwitch: (slug: string) => Promise<void> | void;
|
|
612
|
+
/**
|
|
613
|
+
* The role in the user's own words («Medarbejder», «Administrator»). Roles
|
|
614
|
+
* are the app's vocabulary, so the switcher cannot translate them. Without
|
|
615
|
+
* it no role is shown — a raw «member» in a Danish UI is worse than nothing.
|
|
616
|
+
*/
|
|
617
|
+
roleLabel?: (role: string) => string;
|
|
612
618
|
}
|
|
613
619
|
/**
|
|
614
620
|
* The user's own organisations, the active one marked, and a switch. Only the
|
|
615
621
|
* memberships the backend returns are listed — never a typed or guessed slug.
|
|
616
622
|
* With one organisation it is a plain label, not a menu.
|
|
617
623
|
*/
|
|
618
|
-
declare function TenantSwitcher({ lang, activeSlug, adapter, onSwitch }: TenantSwitcherProps): JSX.Element;
|
|
624
|
+
declare function TenantSwitcher({ lang, activeSlug, adapter, onSwitch, roleLabel }: TenantSwitcherProps): JSX.Element;
|
|
625
|
+
interface ConfirmDialogProps {
|
|
626
|
+
open: boolean;
|
|
627
|
+
title: ComponentChildren;
|
|
628
|
+
body?: ComponentChildren;
|
|
629
|
+
confirmLabel?: string;
|
|
630
|
+
cancelLabel?: string;
|
|
631
|
+
/** Red confirm button, for an action that cannot be undone. */
|
|
632
|
+
destructive?: boolean;
|
|
633
|
+
/** The confirm action is running: buttons disabled, the window cannot be dismissed. */
|
|
634
|
+
busy?: boolean;
|
|
635
|
+
/** Shown inside the window; the window stays open so the user can retry or cancel. */
|
|
636
|
+
error?: ComponentChildren;
|
|
637
|
+
onConfirm: () => void;
|
|
638
|
+
onCancel: () => void;
|
|
639
|
+
/** Prefix for every data-testid. Default "confirm-dialog". */
|
|
640
|
+
testId?: string;
|
|
641
|
+
}
|
|
642
|
+
declare function ConfirmDialog(p: ConfirmDialogProps): JSX.Element | null;
|
|
619
643
|
|
|
620
|
-
export { AVATAR_MAX_BYTES, AVATAR_TYPES, type AccountAdapter, AccountError, AccountPage, type AccountPageProps, type AccountProfile, type AnalyticsAdapter, type AnalyticsData, type AnalyticsDay, AnalyticsError, AnalyticsPage, type AnalyticsPageProps, 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, type TenantAdapter, TenantLoadError, type TenantMembershipRow, TenantSwitcher, type TenantSwitcherProps, TopBar, type TopBarProps, UserMenu, type UserMenuItem, type UserMenuProps, accountErrorKind, activeNavLabel, analyticsErrorKind, badgeLabel, changePct, checkAvatarFile, copenhagenDay, createFetchAccountAdapter, createFetchAnalyticsAdapter, createFetchNotificationSource, createFetchTenantAdapter, createMemoryNotificationSource, defaultReauthHref, fillDays, initials, isActivePath, isExternal, readFlag, readSet, relativeTime, safeNavigate, useIsMobile, writeFlag, writeSet };
|
|
644
|
+
export { AVATAR_MAX_BYTES, AVATAR_TYPES, type AccountAdapter, AccountError, AccountPage, type AccountPageProps, type AccountProfile, type AnalyticsAdapter, type AnalyticsData, type AnalyticsDay, AnalyticsError, AnalyticsPage, type AnalyticsPageProps, AppShell, type AppShellProps, Avatar, ConfirmDialog, type ConfirmDialogProps, 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, type TenantAdapter, TenantLoadError, type TenantMembershipRow, TenantSwitcher, type TenantSwitcherProps, TopBar, type TopBarProps, UserMenu, type UserMenuItem, type UserMenuProps, accountErrorKind, activeNavLabel, analyticsErrorKind, badgeLabel, changePct, checkAvatarFile, copenhagenDay, createFetchAccountAdapter, createFetchAnalyticsAdapter, createFetchNotificationSource, createFetchTenantAdapter, createMemoryNotificationSource, defaultReauthHref, fillDays, initials, isActivePath, isExternal, readFlag, readSet, relativeTime, safeNavigate, useIsMobile, writeFlag, writeSet };
|
package/dist/preact.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useEffect, useRef, useMemo } from 'preact/hooks';
|
|
1
|
+
import { useState, useEffect, useRef, useMemo, useLayoutEffect } from 'preact/hooks';
|
|
2
2
|
import { createBellShell } from '@broberg/notifications/shell';
|
|
3
3
|
import { getPreference, getPalette, getSurfaces, getBackdrop, onThemeChange, onPaletteChange, onSurfacesChange, onBackdropChange, setPreference, setPalette, PALETTES, PALETTE_LABELS, setSurfaces, setBackdrop } from '@broberg/theme';
|
|
4
4
|
import { jsxs, Fragment, jsx } from 'preact/jsx-runtime';
|
|
@@ -1350,7 +1350,8 @@ function AnalyticsSeries({ data, t, locale, num }) {
|
|
|
1350
1350
|
] })
|
|
1351
1351
|
] });
|
|
1352
1352
|
}
|
|
1353
|
-
|
|
1353
|
+
var EDGE = 16;
|
|
1354
|
+
function TenantSwitcher({ lang, activeSlug, adapter, onSwitch, roleLabel }) {
|
|
1354
1355
|
const t = TEXT[lang];
|
|
1355
1356
|
const api = useMemo(() => adapter ?? createFetchTenantAdapter(), [adapter]);
|
|
1356
1357
|
const [rows, setRows] = useState(null);
|
|
@@ -1359,6 +1360,18 @@ function TenantSwitcher({ lang, activeSlug, adapter, onSwitch }) {
|
|
|
1359
1360
|
const [busy, setBusy] = useState(null);
|
|
1360
1361
|
const [switchFailed, setSwitchFailed] = useState(false);
|
|
1361
1362
|
const root = useRef(null);
|
|
1363
|
+
const menu = useRef(null);
|
|
1364
|
+
useLayoutEffect(() => {
|
|
1365
|
+
const el = menu.current;
|
|
1366
|
+
if (!open || !el) return;
|
|
1367
|
+
el.style.transform = "";
|
|
1368
|
+
const r = el.getBoundingClientRect();
|
|
1369
|
+
const vw = document.documentElement.clientWidth || window.innerWidth;
|
|
1370
|
+
let dx = 0;
|
|
1371
|
+
if (r.right > vw - EDGE) dx = vw - EDGE - r.right;
|
|
1372
|
+
if (r.left + dx < EDGE) dx = EDGE - r.left;
|
|
1373
|
+
if (dx !== 0) el.style.transform = `translateX(${Math.round(dx)}px)`;
|
|
1374
|
+
}, [open, rows, loadFailed, switchFailed]);
|
|
1362
1375
|
const load = () => {
|
|
1363
1376
|
setLoadFailed(false);
|
|
1364
1377
|
api.load().then(setRows, () => setLoadFailed(true));
|
|
@@ -1409,7 +1422,7 @@ function TenantSwitcher({ lang, activeSlug, adapter, onSwitch }) {
|
|
|
1409
1422
|
children: /* @__PURE__ */ jsx("span", { class: "bas-tenant__label", "data-testid": "tenant-switcher-label", children: label })
|
|
1410
1423
|
}
|
|
1411
1424
|
),
|
|
1412
|
-
open ? /* @__PURE__ */ jsxs("div", { class: "bas-menu bas-tenant__menu", role: "menu", "data-testid": "tenant-switcher-menu", children: [
|
|
1425
|
+
open ? /* @__PURE__ */ jsxs("div", { class: "bas-menu bas-tenant__menu", ref: menu, role: "menu", "data-testid": "tenant-switcher-menu", children: [
|
|
1413
1426
|
loadFailed ? /* @__PURE__ */ jsxs("div", { class: "bas-tenant__note", role: "alert", "data-testid": "tenant-switcher-load-error", children: [
|
|
1414
1427
|
/* @__PURE__ */ jsx("span", { children: t.tenantLoadFailed }),
|
|
1415
1428
|
/* @__PURE__ */ jsx("button", { type: "button", class: "bas-mi", onClick: load, "data-testid": "tenant-switcher-retry", children: t.tenantRetry })
|
|
@@ -1430,7 +1443,7 @@ function TenantSwitcher({ lang, activeSlug, adapter, onSwitch }) {
|
|
|
1430
1443
|
"data-testid": `tenant-switcher-item-${r.tenant.slug}`,
|
|
1431
1444
|
children: [
|
|
1432
1445
|
/* @__PURE__ */ jsx("span", { class: "bas-tenant__name", children: r.tenant.name }),
|
|
1433
|
-
/* @__PURE__ */ jsx("span", { class: "bas-tenant__role", children: suspended ? t.tenantSuspended : isActive ? t.tenantActive : r.role })
|
|
1446
|
+
suspended || isActive || roleLabel ? /* @__PURE__ */ jsx("span", { class: "bas-tenant__role", "data-testid": `tenant-switcher-role-${r.tenant.slug}`, children: suspended ? t.tenantSuspended : isActive ? t.tenantActive : roleLabel(r.role) }) : null
|
|
1434
1447
|
]
|
|
1435
1448
|
},
|
|
1436
1449
|
r.tenant.id
|
|
@@ -1439,7 +1452,98 @@ function TenantSwitcher({ lang, activeSlug, adapter, onSwitch }) {
|
|
|
1439
1452
|
] }) : null
|
|
1440
1453
|
] });
|
|
1441
1454
|
}
|
|
1455
|
+
var confirmDialogSeq = 0;
|
|
1456
|
+
function ConfirmDialog(p) {
|
|
1457
|
+
const t = p.testId ?? "confirm-dialog";
|
|
1458
|
+
const panel = useRef(null);
|
|
1459
|
+
const cancelBtn = useRef(null);
|
|
1460
|
+
const ids = useMemo(() => {
|
|
1461
|
+
const n = ++confirmDialogSeq;
|
|
1462
|
+
return { title: `bas-confirm-title-${n}`, body: `bas-confirm-body-${n}` };
|
|
1463
|
+
}, []);
|
|
1464
|
+
const live = useRef(p);
|
|
1465
|
+
live.current = p;
|
|
1466
|
+
useEffect(() => {
|
|
1467
|
+
if (!p.open) return;
|
|
1468
|
+
const before = document.activeElement;
|
|
1469
|
+
cancelBtn.current?.focus();
|
|
1470
|
+
const onKey = (e) => {
|
|
1471
|
+
if (e.key === "Escape") {
|
|
1472
|
+
e.preventDefault();
|
|
1473
|
+
if (!live.current.busy) live.current.onCancel();
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
if (e.key !== "Tab" || !panel.current) return;
|
|
1477
|
+
const focusable = [...panel.current.querySelectorAll("button:not([disabled])")];
|
|
1478
|
+
if (focusable.length === 0) {
|
|
1479
|
+
e.preventDefault();
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
const first = focusable[0];
|
|
1483
|
+
const last = focusable[focusable.length - 1];
|
|
1484
|
+
const active = document.activeElement;
|
|
1485
|
+
if (e.shiftKey && (active === first || !panel.current.contains(active))) {
|
|
1486
|
+
e.preventDefault();
|
|
1487
|
+
last.focus();
|
|
1488
|
+
} else if (!e.shiftKey && (active === last || !panel.current.contains(active))) {
|
|
1489
|
+
e.preventDefault();
|
|
1490
|
+
first.focus();
|
|
1491
|
+
}
|
|
1492
|
+
};
|
|
1493
|
+
document.addEventListener("keydown", onKey);
|
|
1494
|
+
return () => {
|
|
1495
|
+
document.removeEventListener("keydown", onKey);
|
|
1496
|
+
if (before && typeof before.focus === "function" && document.contains(before)) before.focus();
|
|
1497
|
+
};
|
|
1498
|
+
}, [p.open]);
|
|
1499
|
+
if (!p.open) return null;
|
|
1500
|
+
return /* @__PURE__ */ jsx(
|
|
1501
|
+
"div",
|
|
1502
|
+
{
|
|
1503
|
+
class: "bas-confirm-backdrop",
|
|
1504
|
+
"data-testid": `${t}-backdrop`,
|
|
1505
|
+
onMouseDown: (e) => {
|
|
1506
|
+
if (e.target === e.currentTarget && !p.busy) p.onCancel();
|
|
1507
|
+
},
|
|
1508
|
+
children: /* @__PURE__ */ jsxs(
|
|
1509
|
+
"div",
|
|
1510
|
+
{
|
|
1511
|
+
ref: panel,
|
|
1512
|
+
class: "bas-confirm",
|
|
1513
|
+
role: "alertdialog",
|
|
1514
|
+
"aria-modal": "true",
|
|
1515
|
+
"aria-labelledby": ids.title,
|
|
1516
|
+
"aria-describedby": p.body ? ids.body : void 0,
|
|
1517
|
+
"aria-busy": p.busy ? "true" : void 0,
|
|
1518
|
+
"data-testid": t,
|
|
1519
|
+
children: [
|
|
1520
|
+
/* @__PURE__ */ jsx("h2", { class: "bas-confirm__title", id: ids.title, "data-testid": `${t}-title`, children: p.title }),
|
|
1521
|
+
p.body ? /* @__PURE__ */ jsx("div", { class: "bas-confirm__body", id: ids.body, "data-testid": `${t}-body`, children: p.body }) : null,
|
|
1522
|
+
p.error ? /* @__PURE__ */ jsx("div", { class: "bas-confirm__error", role: "alert", "data-testid": `${t}-error`, children: p.error }) : null,
|
|
1523
|
+
/* @__PURE__ */ jsxs("div", { class: "bas-confirm__actions", children: [
|
|
1524
|
+
/* @__PURE__ */ jsx("button", { ref: cancelBtn, type: "button", class: "bas-confirm__btn", disabled: p.busy, "data-testid": `${t}-cancel`, onClick: () => p.onCancel(), children: p.cancelLabel ?? "Annuller" }),
|
|
1525
|
+
/* @__PURE__ */ jsxs(
|
|
1526
|
+
"button",
|
|
1527
|
+
{
|
|
1528
|
+
type: "button",
|
|
1529
|
+
class: `bas-confirm__btn is-primary${p.destructive ? " is-danger" : ""}${p.busy ? " is-busy" : ""}`,
|
|
1530
|
+
disabled: p.busy,
|
|
1531
|
+
"data-testid": `${t}-confirm`,
|
|
1532
|
+
onClick: () => p.onConfirm(),
|
|
1533
|
+
children: [
|
|
1534
|
+
p.busy ? /* @__PURE__ */ jsx("span", { class: "bas-confirm__spinner", "aria-hidden": "true" }) : null,
|
|
1535
|
+
p.confirmLabel ?? "Bekr\xE6ft"
|
|
1536
|
+
]
|
|
1537
|
+
}
|
|
1538
|
+
)
|
|
1539
|
+
] })
|
|
1540
|
+
]
|
|
1541
|
+
}
|
|
1542
|
+
)
|
|
1543
|
+
}
|
|
1544
|
+
);
|
|
1545
|
+
}
|
|
1442
1546
|
|
|
1443
|
-
export { AVATAR_MAX_BYTES, AVATAR_TYPES, AccountError, AccountPage, AnalyticsError, AnalyticsPage, AppShell, Avatar, NotificationBell, PageHeader, PageTabs, Sidebar, TEXT, TenantLoadError, TenantSwitcher, TopBar, UserMenu, accountErrorKind, activeNavLabel, analyticsErrorKind, badgeLabel, changePct, checkAvatarFile, copenhagenDay, createFetchAccountAdapter, createFetchAnalyticsAdapter, createFetchNotificationSource, createFetchTenantAdapter, createMemoryNotificationSource, defaultReauthHref, fillDays, initials, isActivePath, isExternal, readFlag, readSet, relativeTime, safeNavigate, useIsMobile, writeFlag, writeSet };
|
|
1547
|
+
export { AVATAR_MAX_BYTES, AVATAR_TYPES, AccountError, AccountPage, AnalyticsError, AnalyticsPage, AppShell, Avatar, ConfirmDialog, NotificationBell, PageHeader, PageTabs, Sidebar, TEXT, TenantLoadError, TenantSwitcher, TopBar, UserMenu, accountErrorKind, activeNavLabel, analyticsErrorKind, badgeLabel, changePct, checkAvatarFile, copenhagenDay, createFetchAccountAdapter, createFetchAnalyticsAdapter, createFetchNotificationSource, createFetchTenantAdapter, createMemoryNotificationSource, defaultReauthHref, fillDays, initials, isActivePath, isExternal, readFlag, readSet, relativeTime, safeNavigate, useIsMobile, writeFlag, writeSet };
|
|
1444
1548
|
//# sourceMappingURL=preact.js.map
|
|
1445
1549
|
//# sourceMappingURL=preact.js.map
|