@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/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
- function TenantSwitcher({ lang, activeSlug, adapter, onSwitch }) {
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