@broberg/app-shell 0.9.2 → 0.10.1

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
@@ -237,6 +237,9 @@ declare const TEXT: {
237
237
  readonly closeMenu: "Luk menu";
238
238
  readonly home: "Forside";
239
239
  readonly language: "Sprog";
240
+ readonly zoom: "Zoom";
241
+ readonly zoomOut: "Zoom ud";
242
+ readonly zoomIn: "Zoom ind";
240
243
  readonly couldNotMarkAll: "Kunne ikke markere alle som læst. Prøv igen.";
241
244
  readonly collapse: "Skjul sidemenu";
242
245
  readonly expand: "Vis sidemenu";
@@ -305,6 +308,9 @@ declare const TEXT: {
305
308
  readonly closeMenu: "Close menu";
306
309
  readonly home: "Home";
307
310
  readonly language: "Language";
311
+ readonly zoom: "Zoom";
312
+ readonly zoomOut: "Zoom out";
313
+ readonly zoomIn: "Zoom in";
308
314
  readonly couldNotMarkAll: "Could not mark all as read. Try again.";
309
315
  readonly collapse: "Hide sidebar";
310
316
  readonly expand: "Show sidebar";
@@ -412,6 +418,8 @@ interface SidebarProps {
412
418
  /** localStorage key for which items with children are open (F092.5). */
413
419
  itemsKey?: string;
414
420
  lang: Lang;
421
+ /** F092.15 — rendered at the very bottom of the sidebar (the user menu in "sidebar-footer"). */
422
+ bottom?: ComponentChildren;
415
423
  }
416
424
  declare function Sidebar(p: SidebarProps): JSX.Element;
417
425
  interface NotificationBellProps {
@@ -427,6 +435,8 @@ interface ShellUser {
427
435
  email?: string;
428
436
  /** Broberg ID OIDC `picture`. Without it: initials. Never Gravatar (D-1dc849). */
429
437
  picture?: string;
438
+ /** F092.15 — a line under the name, e.g. «Administrator · Enhed Nord». */
439
+ subtitle?: string;
430
440
  }
431
441
  interface UserMenuItem {
432
442
  id: string;
@@ -434,6 +444,19 @@ interface UserMenuItem {
434
444
  icon?: Icon;
435
445
  href?: string;
436
446
  onSelect?: () => void;
447
+ /** F092.15 — right-aligned hint, e.g. a shortcut «⌘K». The app owns the shortcut itself. */
448
+ hint?: string;
449
+ }
450
+ /** F092.15 — a radio group in the user menu (organisation, unit, area …). The app owns the choice. */
451
+ interface UserMenuSection {
452
+ id: string;
453
+ label: string;
454
+ items: {
455
+ id: string;
456
+ label: string;
457
+ hint?: string;
458
+ checked?: boolean;
459
+ }[];
437
460
  }
438
461
  interface UserMenuProps {
439
462
  user: ShellUser;
@@ -456,6 +479,19 @@ interface UserMenuProps {
456
479
  }[];
457
480
  onChange: (id: string) => void;
458
481
  };
482
+ /** F092.15 — radio groups shown after your items; a pick calls onSectionSelect and closes the menu. */
483
+ sections?: UserMenuSection[];
484
+ onSectionSelect?: (sectionId: string, itemId: string) => void;
485
+ /** F092.15 — a «Zoom» row: − / value % / +. The app owns the value (percent). */
486
+ zoom?: {
487
+ value: number;
488
+ onChange: (value: number) => void;
489
+ min?: number;
490
+ max?: number;
491
+ step?: number;
492
+ };
493
+ /** F092.15 — where the shell put the menu. "sidebar-footer" opens upward. Set by AppShell. */
494
+ placement?: "topbar" | "sidebar-footer";
459
495
  lang: Lang;
460
496
  }
461
497
  declare function Avatar({ user, size }: {
@@ -518,7 +554,12 @@ interface AppShellProps {
518
554
  /** The notification centre's source. The bell is shown either way. */
519
555
  notifications?: NotificationSource;
520
556
  user?: ShellUser;
521
- userMenu?: Omit<UserMenuProps, "user" | "lang" | "onNavigate">;
557
+ userMenu?: Omit<UserMenuProps, "user" | "lang" | "onNavigate" | "placement">;
558
+ /**
559
+ * F092.15 — where the user menu sits. "topbar" (default): the content's top row
560
+ * (or the full-width bar). "sidebar-footer": the bottom of the sidebar, opening upward.
561
+ */
562
+ userMenuPlacement?: "topbar" | "sidebar-footer";
522
563
  /** Phone behaviour: a slide-in drawer, or a 60px icon rail. */
523
564
  mobile?: "drawer" | "rail";
524
565
  /**
@@ -641,4 +682,4 @@ interface ConfirmDialogProps {
641
682
  }
642
683
  declare function ConfirmDialog(p: ConfirmDialogProps): JSX.Element | null;
643
684
 
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 };
685
+ 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, type UserMenuSection, 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
@@ -237,6 +237,9 @@ declare const TEXT: {
237
237
  readonly closeMenu: "Luk menu";
238
238
  readonly home: "Forside";
239
239
  readonly language: "Sprog";
240
+ readonly zoom: "Zoom";
241
+ readonly zoomOut: "Zoom ud";
242
+ readonly zoomIn: "Zoom ind";
240
243
  readonly couldNotMarkAll: "Kunne ikke markere alle som læst. Prøv igen.";
241
244
  readonly collapse: "Skjul sidemenu";
242
245
  readonly expand: "Vis sidemenu";
@@ -305,6 +308,9 @@ declare const TEXT: {
305
308
  readonly closeMenu: "Close menu";
306
309
  readonly home: "Home";
307
310
  readonly language: "Language";
311
+ readonly zoom: "Zoom";
312
+ readonly zoomOut: "Zoom out";
313
+ readonly zoomIn: "Zoom in";
308
314
  readonly couldNotMarkAll: "Could not mark all as read. Try again.";
309
315
  readonly collapse: "Hide sidebar";
310
316
  readonly expand: "Show sidebar";
@@ -412,6 +418,8 @@ interface SidebarProps {
412
418
  /** localStorage key for which items with children are open (F092.5). */
413
419
  itemsKey?: string;
414
420
  lang: Lang;
421
+ /** F092.15 — rendered at the very bottom of the sidebar (the user menu in "sidebar-footer"). */
422
+ bottom?: ComponentChildren;
415
423
  }
416
424
  declare function Sidebar(p: SidebarProps): JSX.Element;
417
425
  interface NotificationBellProps {
@@ -427,6 +435,8 @@ interface ShellUser {
427
435
  email?: string;
428
436
  /** Broberg ID OIDC `picture`. Without it: initials. Never Gravatar (D-1dc849). */
429
437
  picture?: string;
438
+ /** F092.15 — a line under the name, e.g. «Administrator · Enhed Nord». */
439
+ subtitle?: string;
430
440
  }
431
441
  interface UserMenuItem {
432
442
  id: string;
@@ -434,6 +444,19 @@ interface UserMenuItem {
434
444
  icon?: Icon;
435
445
  href?: string;
436
446
  onSelect?: () => void;
447
+ /** F092.15 — right-aligned hint, e.g. a shortcut «⌘K». The app owns the shortcut itself. */
448
+ hint?: string;
449
+ }
450
+ /** F092.15 — a radio group in the user menu (organisation, unit, area …). The app owns the choice. */
451
+ interface UserMenuSection {
452
+ id: string;
453
+ label: string;
454
+ items: {
455
+ id: string;
456
+ label: string;
457
+ hint?: string;
458
+ checked?: boolean;
459
+ }[];
437
460
  }
438
461
  interface UserMenuProps {
439
462
  user: ShellUser;
@@ -456,6 +479,19 @@ interface UserMenuProps {
456
479
  }[];
457
480
  onChange: (id: string) => void;
458
481
  };
482
+ /** F092.15 — radio groups shown after your items; a pick calls onSectionSelect and closes the menu. */
483
+ sections?: UserMenuSection[];
484
+ onSectionSelect?: (sectionId: string, itemId: string) => void;
485
+ /** F092.15 — a «Zoom» row: − / value % / +. The app owns the value (percent). */
486
+ zoom?: {
487
+ value: number;
488
+ onChange: (value: number) => void;
489
+ min?: number;
490
+ max?: number;
491
+ step?: number;
492
+ };
493
+ /** F092.15 — where the shell put the menu. "sidebar-footer" opens upward. Set by AppShell. */
494
+ placement?: "topbar" | "sidebar-footer";
459
495
  lang: Lang;
460
496
  }
461
497
  declare function Avatar({ user, size }: {
@@ -518,7 +554,12 @@ interface AppShellProps {
518
554
  /** The notification centre's source. The bell is shown either way. */
519
555
  notifications?: NotificationSource;
520
556
  user?: ShellUser;
521
- userMenu?: Omit<UserMenuProps, "user" | "lang" | "onNavigate">;
557
+ userMenu?: Omit<UserMenuProps, "user" | "lang" | "onNavigate" | "placement">;
558
+ /**
559
+ * F092.15 — where the user menu sits. "topbar" (default): the content's top row
560
+ * (or the full-width bar). "sidebar-footer": the bottom of the sidebar, opening upward.
561
+ */
562
+ userMenuPlacement?: "topbar" | "sidebar-footer";
522
563
  /** Phone behaviour: a slide-in drawer, or a 60px icon rail. */
523
564
  mobile?: "drawer" | "rail";
524
565
  /**
@@ -641,4 +682,4 @@ interface ConfirmDialogProps {
641
682
  }
642
683
  declare function ConfirmDialog(p: ConfirmDialogProps): JSX.Element | null;
643
684
 
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 };
685
+ 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, type UserMenuSection, 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
@@ -304,6 +304,9 @@ var TEXT = {
304
304
  closeMenu: "Luk menu",
305
305
  home: "Forside",
306
306
  language: "Sprog",
307
+ zoom: "Zoom",
308
+ zoomOut: "Zoom ud",
309
+ zoomIn: "Zoom ind",
307
310
  couldNotMarkAll: "Kunne ikke markere alle som l\xE6st. Pr\xF8v igen.",
308
311
  collapse: "Skjul sidemenu",
309
312
  expand: "Vis sidemenu",
@@ -372,6 +375,9 @@ var TEXT = {
372
375
  closeMenu: "Close menu",
373
376
  home: "Home",
374
377
  language: "Language",
378
+ zoom: "Zoom",
379
+ zoomOut: "Zoom out",
380
+ zoomIn: "Zoom in",
375
381
  couldNotMarkAll: "Could not mark all as read. Try again.",
376
382
  collapse: "Hide sidebar",
377
383
  expand: "Show sidebar",
@@ -629,7 +635,7 @@ function Sidebar(p) {
629
635
  "aria-label": "Navigation",
630
636
  children: [
631
637
  p.header || p.brandMark ? /* @__PURE__ */ jsxs("div", { class: "bas-sidebar__head", children: [
632
- 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,
638
+ 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", "aria-label": p.homeLabel, children: p.header }) : p.header }) : null,
633
639
  p.brandMark ? /* @__PURE__ */ jsx("div", { class: "bas-sidebar__mark", "data-testid": "sidebar-brand-mark", children: p.homeHref ? /* @__PURE__ */ jsx(
634
640
  NavLink,
635
641
  {
@@ -655,7 +661,8 @@ function Sidebar(p) {
655
661
  open ? g.items.map(item) : null
656
662
  ] }, g.label);
657
663
  }) }),
658
- p.footer?.length ? /* @__PURE__ */ jsx("div", { class: "bas-sidebar__foot", children: p.footer.map(item) }) : null
664
+ p.footer?.length ? /* @__PURE__ */ jsx("div", { class: "bas-sidebar__foot", children: p.footer.map(item) }) : null,
665
+ p.bottom ? /* @__PURE__ */ jsx("div", { class: "bas-sidebar__user", "data-testid": "sidebar-user", children: p.bottom }) : null
659
666
  ]
660
667
  }
661
668
  )
@@ -876,28 +883,48 @@ function UserMenu(p) {
876
883
  setOpen(false);
877
884
  }
878
885
  };
879
- return /* @__PURE__ */ jsxs("div", { class: "bas-usermenu", ref: root, children: [
886
+ const inSidebar = p.placement === "sidebar-footer";
887
+ const btn = useRef(null);
888
+ const [upPos, setUpPos] = useState(void 0);
889
+ useLayoutEffect(() => {
890
+ if (!open || !inSidebar || !btn.current) return;
891
+ const r = btn.current.getBoundingClientRect();
892
+ const vw = window.innerWidth, vh = window.innerHeight;
893
+ const railed = !!btn.current.closest(".is-railed");
894
+ const width = Math.min(320, vw - 16);
895
+ const left = railed ? r.right + 8 : Math.max(8, Math.min(r.left, vw - width - 8));
896
+ const bottom = railed ? Math.max(8, vh - r.bottom) : vh - r.top + 8;
897
+ setUpPos({ position: "fixed", left: `${left}px`, bottom: `${bottom}px`, width: `${width}px`, maxHeight: `${vh - bottom - 8}px` });
898
+ }, [open, inSidebar]);
899
+ const zoom = p.zoom;
900
+ const zMin = zoom?.min ?? 50, zMax = zoom?.max ?? 200, zStep = zoom?.step ?? 10;
901
+ return /* @__PURE__ */ jsxs("div", { class: "bas-usermenu" + (inSidebar ? " is-sidebar" : ""), ref: root, children: [
880
902
  /* @__PURE__ */ jsxs(
881
903
  "button",
882
904
  {
905
+ ref: btn,
883
906
  type: "button",
884
- class: "bas-userbtn" + (open ? " is-open" : ""),
885
- "data-testid": "topbar-user-menu",
907
+ class: "bas-userbtn" + (open ? " is-open" : "") + (inSidebar ? " is-sidebar" : ""),
908
+ "data-testid": inSidebar ? "sidebar-user-menu" : "topbar-user-menu",
886
909
  "aria-haspopup": "menu",
887
910
  "aria-expanded": open,
888
911
  "aria-label": p.user.email ?? p.user.name ?? t.account,
889
912
  onClick: () => setOpen((v) => !v),
890
913
  children: [
891
- /* @__PURE__ */ jsx(Avatar, { user: p.user }),
892
- /* @__PURE__ */ jsx("span", { class: "bas-userbtn__name", children: firstName })
914
+ /* @__PURE__ */ jsx(Avatar, { user: p.user, size: inSidebar ? 32 : 28 }),
915
+ inSidebar ? /* @__PURE__ */ jsxs("span", { class: "bas-userbtn__text", children: [
916
+ /* @__PURE__ */ jsx("span", { class: "bas-userbtn__name", children: p.user.name ?? firstName }),
917
+ p.user.subtitle ? /* @__PURE__ */ jsx("span", { class: "bas-userbtn__sub", "data-testid": "sidebar-user-subtitle", children: p.user.subtitle }) : null
918
+ ] }) : /* @__PURE__ */ jsx("span", { class: "bas-userbtn__name", children: firstName })
893
919
  ]
894
920
  }
895
921
  ),
896
- open ? /* @__PURE__ */ jsxs("div", { class: "bas-menu", role: "menu", "data-testid": "topbar-user-menu-dropdown", children: [
922
+ open ? /* @__PURE__ */ jsxs("div", { class: "bas-menu" + (inSidebar ? " is-up" : ""), style: inSidebar ? upPos : void 0, role: "menu", "data-testid": "topbar-user-menu-dropdown", children: [
897
923
  /* @__PURE__ */ jsxs("div", { class: "bas-menu__who", children: [
898
924
  /* @__PURE__ */ jsx(Avatar, { user: p.user, size: 36 }),
899
925
  /* @__PURE__ */ jsxs("div", { class: "bas-menu__whotext", children: [
900
926
  p.user.name ? /* @__PURE__ */ jsx("div", { class: "bas-menu__name", children: p.user.name }) : null,
927
+ p.user.subtitle ? /* @__PURE__ */ jsx("div", { class: "bas-menu__sub", children: p.user.subtitle }) : null,
901
928
  p.user.email ? /* @__PURE__ */ jsx("div", { class: "bas-menu__email", children: p.user.email }) : null
902
929
  ] })
903
930
  ] }),
@@ -909,7 +936,8 @@ function UserMenu(p) {
909
936
  (p.items ?? []).map(
910
937
  (it) => it.href ? /* @__PURE__ */ jsxs(NavLink, { href: it.href, onNavigate: p.onNavigate, onAfter: () => setOpen(false), class: "bas-mi", "data-testid": `user-menu-${it.id}`, children: [
911
938
  it.icon ?? null,
912
- /* @__PURE__ */ jsx("span", { children: it.label })
939
+ /* @__PURE__ */ jsx("span", { children: it.label }),
940
+ it.hint ? /* @__PURE__ */ jsx("span", { class: "bas-mi__hint", children: it.hint }) : null
913
941
  ] }, it.id) : /* @__PURE__ */ jsxs(
914
942
  "button",
915
943
  {
@@ -923,12 +951,37 @@ function UserMenu(p) {
923
951
  },
924
952
  children: [
925
953
  it.icon ?? null,
926
- /* @__PURE__ */ jsx("span", { children: it.label })
954
+ /* @__PURE__ */ jsx("span", { children: it.label }),
955
+ it.hint ? /* @__PURE__ */ jsx("span", { class: "bas-mi__hint", children: it.hint }) : null
927
956
  ]
928
957
  },
929
958
  it.id
930
959
  )
931
960
  ),
961
+ (p.sections ?? []).map((sec) => /* @__PURE__ */ jsxs("div", { class: "bas-menu__section", role: "group", "aria-label": sec.label, "data-testid": `user-menu-section-${sec.id}`, children: [
962
+ /* @__PURE__ */ jsx("div", { class: "bas-sep" }),
963
+ /* @__PURE__ */ jsx("div", { class: "bas-menu__label", children: sec.label }),
964
+ sec.items.map((it) => /* @__PURE__ */ jsxs(
965
+ "button",
966
+ {
967
+ type: "button",
968
+ class: "bas-mi" + (it.checked ? " is-checked" : ""),
969
+ role: "menuitemradio",
970
+ "aria-checked": it.checked ? "true" : "false",
971
+ "data-testid": `user-menu-section-${sec.id}-${it.id}`,
972
+ onClick: () => {
973
+ setOpen(false);
974
+ p.onSectionSelect?.(sec.id, it.id);
975
+ },
976
+ children: [
977
+ /* @__PURE__ */ jsx("span", { class: "bas-mi__check", "aria-hidden": "true", children: it.checked ? "\u2713" : "" }),
978
+ /* @__PURE__ */ jsx("span", { children: it.label }),
979
+ it.hint ? /* @__PURE__ */ jsx("span", { class: "bas-mi__hint", children: it.hint }) : null
980
+ ]
981
+ },
982
+ it.id
983
+ ))
984
+ ] }, sec.id)),
932
985
  p.appearance !== false ? /* @__PURE__ */ jsxs(Fragment, { children: [
933
986
  /* @__PURE__ */ jsx("div", { class: "bas-sep" }),
934
987
  /* @__PURE__ */ jsxs("div", { class: "bas-prefs", children: [
@@ -977,6 +1030,42 @@ function UserMenu(p) {
977
1030
  )
978
1031
  ] }) })
979
1032
  ] }) : null,
1033
+ zoom ? /* @__PURE__ */ jsxs(Fragment, { children: [
1034
+ p.appearance === false && !p.language ? /* @__PURE__ */ jsx("div", { class: "bas-sep" }) : null,
1035
+ /* @__PURE__ */ jsx("div", { class: "bas-prefs", children: /* @__PURE__ */ jsxs("div", { class: "bas-pref", children: [
1036
+ /* @__PURE__ */ jsx("span", { children: t.zoom }),
1037
+ /* @__PURE__ */ jsxs("div", { class: "bas-zoom", role: "group", "aria-label": t.zoom, children: [
1038
+ /* @__PURE__ */ jsx(
1039
+ "button",
1040
+ {
1041
+ type: "button",
1042
+ class: "bas-zoom__btn",
1043
+ "data-testid": "user-menu-zoom-out",
1044
+ "aria-label": t.zoomOut,
1045
+ disabled: zoom.value - zStep < zMin,
1046
+ onClick: () => zoom.onChange(Math.max(zMin, zoom.value - zStep)),
1047
+ children: "\u2212"
1048
+ }
1049
+ ),
1050
+ /* @__PURE__ */ jsxs("span", { class: "bas-zoom__value", "data-testid": "user-menu-zoom-value", children: [
1051
+ zoom.value,
1052
+ " %"
1053
+ ] }),
1054
+ /* @__PURE__ */ jsx(
1055
+ "button",
1056
+ {
1057
+ type: "button",
1058
+ class: "bas-zoom__btn",
1059
+ "data-testid": "user-menu-zoom-in",
1060
+ "aria-label": t.zoomIn,
1061
+ disabled: zoom.value + zStep > zMax,
1062
+ onClick: () => zoom.onChange(Math.min(zMax, zoom.value + zStep)),
1063
+ children: "+"
1064
+ }
1065
+ )
1066
+ ] })
1067
+ ] }) })
1068
+ ] }) : null,
980
1069
  p.onSignOut ? /* @__PURE__ */ jsxs(Fragment, { children: [
981
1070
  /* @__PURE__ */ jsx("div", { class: "bas-sep" }),
982
1071
  /* @__PURE__ */ jsxs("button", { type: "button", class: "bas-mi is-danger", role: "menuitem", "data-testid": "topbar-sign-out", disabled: busy, onClick: () => void signOut(), children: [
@@ -1043,6 +1132,8 @@ function AppShell(p) {
1043
1132
  const expanded = drawerMode ? drawer : !collapsed;
1044
1133
  const triggerLabel = drawerMode ? drawer ? t.closeMenu : t.openMenu : collapsed ? t.expand : t.collapse;
1045
1134
  const heading = p.title ?? activeNavLabel(p.groups, p.footer, p.currentPath);
1135
+ const userInSidebar = p.userMenuPlacement === "sidebar-footer";
1136
+ const sidebarUser = userInSidebar && p.user ? /* @__PURE__ */ jsx(UserMenu, { user: p.user, ...p.userMenu, placement: "sidebar-footer", onNavigate: p.onNavigate, lang: p.lang }) : null;
1046
1137
  return /* @__PURE__ */ jsxs("div", { class: "bas-root", "data-testid": "app-shell", "data-mobile": mode, "data-layout": layout, children: [
1047
1138
  inset ? null : /* @__PURE__ */ jsx(
1048
1139
  TopBar,
@@ -1052,7 +1143,7 @@ function AppShell(p) {
1052
1143
  onHome: () => setDrawer(false),
1053
1144
  actions: p.actions,
1054
1145
  notifications: p.notifications,
1055
- user: p.user,
1146
+ user: userInSidebar ? void 0 : p.user,
1056
1147
  userMenu: p.userMenu,
1057
1148
  onNavigate: p.onNavigate,
1058
1149
  lang: p.lang
@@ -1078,6 +1169,7 @@ function AppShell(p) {
1078
1169
  brandTestId: inset ? "brand-home" : void 0,
1079
1170
  groupsKey: `${prefix}.groups`,
1080
1171
  itemsKey: `${prefix}.items`,
1172
+ bottom: sidebarUser,
1081
1173
  lang: p.lang
1082
1174
  }
1083
1175
  ),
@@ -1104,7 +1196,7 @@ function AppShell(p) {
1104
1196
  /* @__PURE__ */ jsx("div", { class: "bas-spacer" }),
1105
1197
  p.actions ? /* @__PURE__ */ jsx("div", { class: "bas-contenthead__actions", children: p.actions }) : null,
1106
1198
  /* @__PURE__ */ jsx(NotificationBell, { source: p.notifications, onNavigate: p.onNavigate, lang: p.lang }),
1107
- p.user ? /* @__PURE__ */ jsx(UserMenu, { user: p.user, ...p.userMenu, onNavigate: p.onNavigate, lang: p.lang }) : null
1199
+ p.user && !userInSidebar ? /* @__PURE__ */ jsx(UserMenu, { user: p.user, ...p.userMenu, onNavigate: p.onNavigate, lang: p.lang }) : null
1108
1200
  ] }) : null
1109
1201
  ] }),
1110
1202
  p.children