@broberg/app-shell 0.12.0 → 0.13.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
@@ -20,6 +20,28 @@ interface NavItem<Icon = unknown> {
20
20
  marker?: "working";
21
21
  /** F092.20 — shows a pin icon after the label. */
22
22
  pinned?: boolean;
23
+ /** F092.21 — row actions: a «…» button opens a menu with these. */
24
+ actions?: NavAction[];
25
+ }
26
+ /**
27
+ * F092.21 — one row action. `submenu` switches the menu to a sub-list (with a
28
+ * back arrow); `input` switches it to a field + save (e.g. rename). A plain
29
+ * action, a submenu leaf and a saved input all call `onItemAction`.
30
+ */
31
+ interface NavAction {
32
+ id: string;
33
+ label: string;
34
+ icon?: unknown;
35
+ /** A single letter that picks this action while the menu is open (e.g. "P"). */
36
+ kbd?: string;
37
+ /** Shown in the danger colour (delete). */
38
+ danger?: boolean;
39
+ submenu?: NavAction[];
40
+ input?: {
41
+ value: string;
42
+ placeholder?: string;
43
+ saveLabel?: string;
44
+ };
23
45
  }
24
46
  interface NavSection<Icon = unknown> {
25
47
  /** A sub-heading inside one group, e.g. "I dag" / "I går". */
@@ -250,6 +272,8 @@ declare const TEXT: {
250
272
  readonly closeMenu: "Luk menu";
251
273
  readonly home: "Forside";
252
274
  readonly working: "Arbejder";
275
+ readonly moreFor: "Mere for";
276
+ readonly back: "Tilbage";
253
277
  readonly pinned: "Fastgjort";
254
278
  readonly language: "Sprog";
255
279
  readonly zoom: "Zoom";
@@ -323,6 +347,8 @@ declare const TEXT: {
323
347
  readonly closeMenu: "Close menu";
324
348
  readonly home: "Home";
325
349
  readonly working: "Working";
350
+ readonly moreFor: "More for";
351
+ readonly back: "Back";
326
352
  readonly pinned: "Pinned";
327
353
  readonly language: "Language";
328
354
  readonly zoom: "Zoom";
@@ -437,6 +463,8 @@ interface SidebarProps {
437
463
  lang: Lang;
438
464
  /** F092.15 — rendered at the very bottom of the sidebar (the user menu in "sidebar-footer"). */
439
465
  bottom?: ComponentChildren;
466
+ /** F092.21 — a row action was chosen (value: the saved text of an `input` action). */
467
+ onItemAction?: (navItemId: string, actionId: string, value?: string) => void;
440
468
  }
441
469
  declare function Sidebar(p: SidebarProps): JSX.Element;
442
470
  interface NotificationBellProps {
@@ -569,6 +597,8 @@ declare function PageTabs(p: {
569
597
  onNavigate?: Navigate;
570
598
  }): JSX.Element;
571
599
  interface AppShellProps {
600
+ /** F092.21 — passed to the sidebar: a row action was chosen. */
601
+ onItemAction?: SidebarProps["onItemAction"];
572
602
  brand?: ComponentChildren;
573
603
  actions?: ComponentChildren;
574
604
  groups: PreactNavGroup[];
@@ -706,4 +736,4 @@ interface ConfirmDialogProps {
706
736
  }
707
737
  declare function ConfirmDialog(p: ConfirmDialogProps): JSX.Element | null;
708
738
 
709
- 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 NavSection, 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 };
739
+ 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 NavAction, type NavGroup, type NavItem, type NavSection, 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
@@ -20,6 +20,28 @@ interface NavItem<Icon = unknown> {
20
20
  marker?: "working";
21
21
  /** F092.20 — shows a pin icon after the label. */
22
22
  pinned?: boolean;
23
+ /** F092.21 — row actions: a «…» button opens a menu with these. */
24
+ actions?: NavAction[];
25
+ }
26
+ /**
27
+ * F092.21 — one row action. `submenu` switches the menu to a sub-list (with a
28
+ * back arrow); `input` switches it to a field + save (e.g. rename). A plain
29
+ * action, a submenu leaf and a saved input all call `onItemAction`.
30
+ */
31
+ interface NavAction {
32
+ id: string;
33
+ label: string;
34
+ icon?: unknown;
35
+ /** A single letter that picks this action while the menu is open (e.g. "P"). */
36
+ kbd?: string;
37
+ /** Shown in the danger colour (delete). */
38
+ danger?: boolean;
39
+ submenu?: NavAction[];
40
+ input?: {
41
+ value: string;
42
+ placeholder?: string;
43
+ saveLabel?: string;
44
+ };
23
45
  }
24
46
  interface NavSection<Icon = unknown> {
25
47
  /** A sub-heading inside one group, e.g. "I dag" / "I går". */
@@ -250,6 +272,8 @@ declare const TEXT: {
250
272
  readonly closeMenu: "Luk menu";
251
273
  readonly home: "Forside";
252
274
  readonly working: "Arbejder";
275
+ readonly moreFor: "Mere for";
276
+ readonly back: "Tilbage";
253
277
  readonly pinned: "Fastgjort";
254
278
  readonly language: "Sprog";
255
279
  readonly zoom: "Zoom";
@@ -323,6 +347,8 @@ declare const TEXT: {
323
347
  readonly closeMenu: "Close menu";
324
348
  readonly home: "Home";
325
349
  readonly working: "Working";
350
+ readonly moreFor: "More for";
351
+ readonly back: "Back";
326
352
  readonly pinned: "Pinned";
327
353
  readonly language: "Language";
328
354
  readonly zoom: "Zoom";
@@ -437,6 +463,8 @@ interface SidebarProps {
437
463
  lang: Lang;
438
464
  /** F092.15 — rendered at the very bottom of the sidebar (the user menu in "sidebar-footer"). */
439
465
  bottom?: ComponentChildren;
466
+ /** F092.21 — a row action was chosen (value: the saved text of an `input` action). */
467
+ onItemAction?: (navItemId: string, actionId: string, value?: string) => void;
440
468
  }
441
469
  declare function Sidebar(p: SidebarProps): JSX.Element;
442
470
  interface NotificationBellProps {
@@ -569,6 +597,8 @@ declare function PageTabs(p: {
569
597
  onNavigate?: Navigate;
570
598
  }): JSX.Element;
571
599
  interface AppShellProps {
600
+ /** F092.21 — passed to the sidebar: a row action was chosen. */
601
+ onItemAction?: SidebarProps["onItemAction"];
572
602
  brand?: ComponentChildren;
573
603
  actions?: ComponentChildren;
574
604
  groups: PreactNavGroup[];
@@ -706,4 +736,4 @@ interface ConfirmDialogProps {
706
736
  }
707
737
  declare function ConfirmDialog(p: ConfirmDialogProps): JSX.Element | null;
708
738
 
709
- 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 NavSection, 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 };
739
+ 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 NavAction, type NavGroup, type NavItem, type NavSection, 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
@@ -305,6 +305,8 @@ var TEXT = {
305
305
  closeMenu: "Luk menu",
306
306
  home: "Forside",
307
307
  working: "Arbejder",
308
+ moreFor: "Mere for",
309
+ back: "Tilbage",
308
310
  pinned: "Fastgjort",
309
311
  language: "Sprog",
310
312
  zoom: "Zoom",
@@ -378,6 +380,8 @@ var TEXT = {
378
380
  closeMenu: "Close menu",
379
381
  home: "Home",
380
382
  working: "Working",
383
+ moreFor: "More for",
384
+ back: "Back",
381
385
  pinned: "Pinned",
382
386
  language: "Language",
383
387
  zoom: "Zoom",
@@ -505,6 +509,186 @@ var IconUser = () => svg(/* @__PURE__ */ jsxs(Fragment, { children: [
505
509
  /* @__PURE__ */ jsx("circle", { cx: "12", cy: "8", r: "4" }),
506
510
  /* @__PURE__ */ jsx("path", { d: "M4 21a8 8 0 0 1 16 0" })
507
511
  ] }), 14);
512
+ function RowActions(props) {
513
+ const t = TEXT[props.lang];
514
+ const id = props.item.id;
515
+ const btn = useRef(null);
516
+ const menu = useRef(null);
517
+ const [stack, setStack] = useState([]);
518
+ const [hi, setHi] = useState(0);
519
+ const [value, setValue] = useState("");
520
+ const [pos, setPos] = useState(void 0);
521
+ const view = stack[stack.length - 1];
522
+ const open = !!view;
523
+ const close = (refocus = true) => {
524
+ setStack([]);
525
+ if (refocus) btn.current?.focus();
526
+ };
527
+ const show = (v) => {
528
+ setStack((s) => [...s, v]);
529
+ setHi(0);
530
+ if (v.kind === "input") setValue(v.action.input.value);
531
+ };
532
+ const back = () => {
533
+ setStack((s) => s.slice(0, -1));
534
+ setHi(0);
535
+ };
536
+ const pick = (a) => {
537
+ if (a.submenu?.length) return show({ kind: "list", actions: a.submenu, title: a.label });
538
+ if (a.input) return show({ kind: "input", action: a });
539
+ close();
540
+ props.onAction(a.id);
541
+ };
542
+ const save = (a) => {
543
+ close();
544
+ props.onAction(a.id, value);
545
+ };
546
+ useLayoutEffect(() => {
547
+ if (!open || !btn.current) return setPos(void 0);
548
+ const r = btn.current.getBoundingClientRect();
549
+ const vw = window.innerWidth, vh = window.innerHeight, m = 8;
550
+ const w = Math.min(240, vw - 2 * m);
551
+ const h = menu.current?.offsetHeight ?? 0;
552
+ const left = Math.max(m, Math.min(r.right - w, vw - w - m));
553
+ const top = r.bottom + 4 + h <= vh - m || r.top - 4 - h < m ? Math.min(r.bottom + 4, vh - m - h) : r.top - 4 - h;
554
+ setPos({ position: "fixed", left: `${left}px`, top: `${Math.max(m, top)}px`, width: `${w}px` });
555
+ if (view?.kind === "input") menu.current?.querySelector("input")?.focus();
556
+ else menu.current?.focus();
557
+ }, [open, stack.length]);
558
+ useEffect(() => {
559
+ if (!open) return;
560
+ const outside = (e) => {
561
+ const n = e.target instanceof Node ? e.target : null;
562
+ if (n && (menu.current?.contains(n) || btn.current?.contains(n))) return;
563
+ close(false);
564
+ };
565
+ document.addEventListener("mousedown", outside);
566
+ window.addEventListener("scroll", outside, true);
567
+ window.addEventListener("resize", outside);
568
+ return () => {
569
+ document.removeEventListener("mousedown", outside);
570
+ window.removeEventListener("scroll", outside, true);
571
+ window.removeEventListener("resize", outside);
572
+ };
573
+ }, [open]);
574
+ const onKeyDown = (e) => {
575
+ if (!view) return;
576
+ e.stopPropagation();
577
+ if (e.key === "Escape") {
578
+ e.preventDefault();
579
+ return close();
580
+ }
581
+ if (view.kind === "input") {
582
+ if (e.key === "Enter") {
583
+ e.preventDefault();
584
+ save(view.action);
585
+ }
586
+ return;
587
+ }
588
+ if ((e.key === "ArrowLeft" || e.key === "Backspace") && stack.length > 1) {
589
+ e.preventDefault();
590
+ return back();
591
+ }
592
+ const list = view.actions;
593
+ const { state, intent } = selectKeyReducer({ open: true, highlighted: hi }, e.key, list.length);
594
+ if (intent.type === "select") {
595
+ e.preventDefault();
596
+ return pick(list[intent.index]);
597
+ }
598
+ if (state.highlighted !== hi) {
599
+ e.preventDefault();
600
+ return setHi(state.highlighted);
601
+ }
602
+ const byKbd = e.key.length === 1 ? list.find((a) => a.kbd?.toLowerCase() === e.key.toLowerCase()) : void 0;
603
+ if (byKbd) {
604
+ e.preventDefault();
605
+ pick(byKbd);
606
+ }
607
+ };
608
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
609
+ /* @__PURE__ */ jsx(
610
+ "button",
611
+ {
612
+ ref: btn,
613
+ type: "button",
614
+ class: "bas-item__more" + (open ? " is-open" : ""),
615
+ "data-testid": `sidebar-item-${id}-more`,
616
+ "aria-label": `${t.moreFor} ${props.item.label}`,
617
+ "aria-haspopup": "menu",
618
+ "aria-expanded": open,
619
+ onClick: () => open ? close() : show({ kind: "list", actions: props.item.actions }),
620
+ children: /* @__PURE__ */ jsx(IconMore, {})
621
+ }
622
+ ),
623
+ view ? /* @__PURE__ */ jsxs(
624
+ "div",
625
+ {
626
+ ref: menu,
627
+ class: "bas-rowmenu",
628
+ role: "menu",
629
+ tabIndex: -1,
630
+ "aria-label": view.kind === "list" && view.title ? view.title : props.item.label,
631
+ "data-testid": `sidebar-item-${id}-menu`,
632
+ style: pos ?? { position: "fixed", visibility: "hidden" },
633
+ onKeyDown,
634
+ children: [
635
+ stack.length > 1 ? /* @__PURE__ */ jsxs("button", { type: "button", class: "bas-rowmenu__back", "data-testid": `sidebar-item-${id}-back`, "aria-label": t.back, onClick: back, children: [
636
+ /* @__PURE__ */ jsx(IconChevronLeft, {}),
637
+ /* @__PURE__ */ jsx("span", { children: view.kind === "list" ? view.title : view.action.label })
638
+ ] }) : null,
639
+ view.kind === "list" ? view.actions.map((a, i) => /* @__PURE__ */ jsxs(
640
+ "button",
641
+ {
642
+ type: "button",
643
+ role: "menuitem",
644
+ class: "bas-rowmenu__item" + (i === hi ? " is-active" : "") + (a.danger ? " is-danger" : ""),
645
+ "data-testid": `sidebar-item-${id}-action-${a.id}`,
646
+ onMouseEnter: () => setHi(i),
647
+ onClick: () => pick(a),
648
+ children: [
649
+ a.icon ? /* @__PURE__ */ jsx("span", { class: "bas-rowmenu__icon", children: a.icon }) : null,
650
+ /* @__PURE__ */ jsx("span", { class: "bas-rowmenu__label", children: a.label }),
651
+ a.submenu?.length ? /* @__PURE__ */ jsx(IconChevronRight, {}) : a.kbd ? /* @__PURE__ */ jsx("kbd", { class: "bas-rowmenu__kbd", children: a.kbd.toUpperCase() }) : null
652
+ ]
653
+ },
654
+ a.id
655
+ )) : /* @__PURE__ */ jsxs(
656
+ "form",
657
+ {
658
+ class: "bas-rowmenu__form",
659
+ "data-testid": `sidebar-item-${id}-form`,
660
+ onSubmit: (e) => {
661
+ e.preventDefault();
662
+ save(view.action);
663
+ },
664
+ children: [
665
+ /* @__PURE__ */ jsx(
666
+ "input",
667
+ {
668
+ class: "bas-rowmenu__input",
669
+ "data-testid": `sidebar-item-${id}-input`,
670
+ "aria-label": view.action.label,
671
+ value,
672
+ placeholder: view.action.input.placeholder,
673
+ onInput: (e) => setValue(e.currentTarget.value)
674
+ }
675
+ ),
676
+ /* @__PURE__ */ jsx("button", { type: "submit", class: "bas-rowmenu__save", "data-testid": `sidebar-item-${id}-save`, children: view.action.input.saveLabel ?? t.save })
677
+ ]
678
+ }
679
+ )
680
+ ]
681
+ }
682
+ ) : null
683
+ ] });
684
+ }
685
+ var IconMore = () => svg(/* @__PURE__ */ jsxs(Fragment, { children: [
686
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "1" }),
687
+ /* @__PURE__ */ jsx("circle", { cx: "19", cy: "12", r: "1" }),
688
+ /* @__PURE__ */ jsx("circle", { cx: "5", cy: "12", r: "1" })
689
+ ] }), 16);
690
+ var IconChevronLeft = () => svg(/* @__PURE__ */ jsx("path", { d: "m15 18-6-6 6-6" }), 14);
691
+ var IconChevronRight = () => svg(/* @__PURE__ */ jsx("path", { d: "m9 18 6-6-6-6" }), 14);
508
692
  function Sidebar(p) {
509
693
  const key = p.groupsKey ?? "broberg-app-shell.groups";
510
694
  const itemsKey = p.itemsKey ?? "broberg-app-shell.items";
@@ -562,7 +746,7 @@ function Sidebar(p) {
562
746
  const leaf = (it, sub = false) => {
563
747
  const active = isActivePath(it.href, p.currentPath);
564
748
  const badge = badgeLabel(it.badge);
565
- return /* @__PURE__ */ jsxs(
749
+ const link = /* @__PURE__ */ jsxs(
566
750
  NavLink,
567
751
  {
568
752
  href: it.href,
@@ -584,6 +768,11 @@ function Sidebar(p) {
584
768
  },
585
769
  it.id
586
770
  );
771
+ if (!it.actions?.length || railed) return link;
772
+ return /* @__PURE__ */ jsxs("div", { class: "bas-itemrow", children: [
773
+ link,
774
+ /* @__PURE__ */ jsx(RowActions, { item: it, lang: p.lang, onAction: (a, v) => v === void 0 ? p.onItemAction?.(it.id, a) : p.onItemAction?.(it.id, a, v) })
775
+ ] }, it.id);
587
776
  };
588
777
  const parent = (it) => {
589
778
  const children = it.children;
@@ -1255,6 +1444,7 @@ function AppShell(p) {
1255
1444
  {
1256
1445
  groups: p.groups,
1257
1446
  footer: p.footer,
1447
+ onItemAction: p.onItemAction,
1258
1448
  currentPath: p.currentPath,
1259
1449
  onNavigate: p.onNavigate,
1260
1450
  collapsed,