@broberg/app-shell 0.13.1 → 0.15.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
@@ -16,8 +16,11 @@ interface NavItem<Icon = unknown> {
16
16
  * it is on the current path.
17
17
  */
18
18
  children?: NavItem<Icon>[];
19
- /** F092.20 — a status marker before the label; "working" = a run in progress. */
20
- marker?: "working";
19
+ /**
20
+ * F092.20 — a status marker before the label; "working" = a run in progress.
21
+ * F092.23 — "idle" = the same ring in grey (finished), so every row lines up.
22
+ */
23
+ marker?: "working" | "idle";
21
24
  /** F092.20 — shows a pin icon after the label. */
22
25
  pinned?: boolean;
23
26
  /** F092.21 — row actions: a «…» button opens a menu with these. */
@@ -42,6 +45,10 @@ interface NavAction {
42
45
  placeholder?: string;
43
46
  saveLabel?: string;
44
47
  };
48
+ /** F092.22 — ask twice: the first pick shows `label` («Tryk igen for at slette») and keeps the menu open; the second runs it. */
49
+ confirm?: {
50
+ label: string;
51
+ };
45
52
  }
46
53
  interface NavSection<Icon = unknown> {
47
54
  /** A sub-heading inside one group, e.g. "I dag" / "I går". */
@@ -55,6 +62,12 @@ interface NavGroup<Icon = unknown> {
55
62
  variant?: "default" | "plain";
56
63
  /** F092.20 — sub-headed lists inside this group, shown after `items`. */
57
64
  sections?: NavSection<Icon>[];
65
+ /**
66
+ * F092.23 — no heading row: the items show directly, always open (it cannot be
67
+ * folded, so a stored fold for this label is ignored). `label` stays required —
68
+ * it is the group's key and its accessible name.
69
+ */
70
+ hideLabel?: boolean;
58
71
  }
59
72
  /**
60
73
  * Is this item the current page? Exact match for "/", otherwise the path is the
package/dist/preact.d.ts CHANGED
@@ -16,8 +16,11 @@ interface NavItem<Icon = unknown> {
16
16
  * it is on the current path.
17
17
  */
18
18
  children?: NavItem<Icon>[];
19
- /** F092.20 — a status marker before the label; "working" = a run in progress. */
20
- marker?: "working";
19
+ /**
20
+ * F092.20 — a status marker before the label; "working" = a run in progress.
21
+ * F092.23 — "idle" = the same ring in grey (finished), so every row lines up.
22
+ */
23
+ marker?: "working" | "idle";
21
24
  /** F092.20 — shows a pin icon after the label. */
22
25
  pinned?: boolean;
23
26
  /** F092.21 — row actions: a «…» button opens a menu with these. */
@@ -42,6 +45,10 @@ interface NavAction {
42
45
  placeholder?: string;
43
46
  saveLabel?: string;
44
47
  };
48
+ /** F092.22 — ask twice: the first pick shows `label` («Tryk igen for at slette») and keeps the menu open; the second runs it. */
49
+ confirm?: {
50
+ label: string;
51
+ };
45
52
  }
46
53
  interface NavSection<Icon = unknown> {
47
54
  /** A sub-heading inside one group, e.g. "I dag" / "I går". */
@@ -55,6 +62,12 @@ interface NavGroup<Icon = unknown> {
55
62
  variant?: "default" | "plain";
56
63
  /** F092.20 — sub-headed lists inside this group, shown after `items`. */
57
64
  sections?: NavSection<Icon>[];
65
+ /**
66
+ * F092.23 — no heading row: the items show directly, always open (it cannot be
67
+ * folded, so a stored fold for this label is ignored). `label` stays required —
68
+ * it is the group's key and its accessible name.
69
+ */
70
+ hideLabel?: boolean;
58
71
  }
59
72
  /**
60
73
  * Is this item the current page? Exact match for "/", otherwise the path is the
package/dist/preact.js CHANGED
@@ -515,27 +515,50 @@ function RowActions(props) {
515
515
  const btn = useRef(null);
516
516
  const menu = useRef(null);
517
517
  const [stack, setStack] = useState([]);
518
- const [hi, setHi] = useState(0);
518
+ const [hi, setHiState] = useState(0);
519
+ const hiRef = useRef(0);
520
+ const setHi = (n) => {
521
+ hiRef.current = n;
522
+ setHiState(n);
523
+ };
524
+ const [armed, setArmedState] = useState(null);
525
+ const armedRef = useRef(null);
526
+ const setArmed = (v) => {
527
+ armedRef.current = v;
528
+ setArmedState(v);
529
+ };
530
+ const items = useRef([]);
531
+ const onKeyDownRef = useRef(() => {
532
+ });
519
533
  const [value, setValue] = useState("");
520
534
  const [pos, setPos] = useState(void 0);
521
535
  const view = stack[stack.length - 1];
522
536
  const open = !!view;
537
+ const stackRef = useRef([]);
538
+ const setStackNow = (next) => {
539
+ stackRef.current = next;
540
+ setStack(next);
541
+ };
523
542
  const close = (refocus = true) => {
524
- setStack([]);
543
+ setStackNow([]);
544
+ setArmed(null);
525
545
  if (refocus) btn.current?.focus();
526
546
  };
527
547
  const show = (v) => {
528
- setStack((s) => [...s, v]);
548
+ setStackNow([...stackRef.current, v]);
529
549
  setHi(0);
550
+ setArmed(null);
530
551
  if (v.kind === "input") setValue(v.action.input.value);
531
552
  };
532
553
  const back = () => {
533
- setStack((s) => s.slice(0, -1));
554
+ setStackNow(stackRef.current.slice(0, -1));
534
555
  setHi(0);
556
+ setArmed(null);
535
557
  };
536
558
  const pick = (a) => {
537
559
  if (a.submenu?.length) return show({ kind: "list", actions: a.submenu, title: a.label });
538
560
  if (a.input) return show({ kind: "input", action: a });
561
+ if (a.confirm && armedRef.current !== a.id) return setArmed(a.id);
539
562
  close();
540
563
  props.onAction(a.id);
541
564
  };
@@ -552,50 +575,69 @@ function RowActions(props) {
552
575
  const left = Math.max(m, Math.min(r.right - w, vw - w - m));
553
576
  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
577
  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
578
  }, [open, stack.length]);
558
- useEffect(() => {
579
+ useLayoutEffect(() => {
580
+ if (!open || !pos) return;
581
+ if (view?.kind === "input") menu.current?.querySelector("input")?.focus();
582
+ else items.current[hiRef.current]?.focus();
583
+ }, [open, pos, hi, stack.length]);
584
+ useLayoutEffect(() => {
559
585
  if (!open) return;
560
586
  const outside = (e) => {
561
587
  const n = e.target instanceof Node ? e.target : null;
562
588
  if (n && (menu.current?.contains(n) || btn.current?.contains(n))) return;
563
589
  close(false);
564
590
  };
591
+ const early = (e) => {
592
+ const n = e.target instanceof Node ? e.target : null;
593
+ if (n && menu.current?.contains(n)) return;
594
+ if (e.key === "Enter" || e.key === " " || e.key === "Spacebar" || e.key === "Tab") return;
595
+ onKeyDownRef.current(e);
596
+ };
597
+ document.addEventListener("keydown", early, true);
565
598
  document.addEventListener("mousedown", outside);
566
599
  window.addEventListener("scroll", outside, true);
567
600
  window.addEventListener("resize", outside);
568
601
  return () => {
602
+ document.removeEventListener("keydown", early, true);
569
603
  document.removeEventListener("mousedown", outside);
570
604
  window.removeEventListener("scroll", outside, true);
571
605
  window.removeEventListener("resize", outside);
572
606
  };
573
607
  }, [open]);
574
608
  const onKeyDown = (e) => {
575
- if (!view) return;
609
+ const stack2 = stackRef.current;
610
+ const view2 = stack2[stack2.length - 1];
611
+ if (!view2) return;
576
612
  e.stopPropagation();
577
613
  if (e.key === "Escape") {
578
614
  e.preventDefault();
579
615
  return close();
580
616
  }
581
- if (view.kind === "input") {
617
+ if (view2.kind === "input") {
582
618
  if (e.key === "Enter") {
583
619
  e.preventDefault();
584
- save(view.action);
620
+ save(view2.action);
585
621
  }
586
622
  return;
587
623
  }
588
- if ((e.key === "ArrowLeft" || e.key === "Backspace") && stack.length > 1) {
624
+ if ((e.key === "ArrowLeft" || e.key === "Backspace") && stack2.length > 1) {
589
625
  e.preventDefault();
590
626
  return back();
591
627
  }
592
- const list = view.actions;
593
- const { state, intent } = selectKeyReducer({ open: true, highlighted: hi }, e.key, list.length);
628
+ const list = view2.actions;
629
+ if (e.key === "Enter" || e.key === " " || e.key === "Spacebar") {
630
+ e.preventDefault();
631
+ const at = items.current.findIndex((b) => b === document.activeElement);
632
+ const idx = at >= 0 ? at : hiRef.current;
633
+ return list[idx] ? pick(list[idx]) : void 0;
634
+ }
635
+ const { state, intent } = selectKeyReducer({ open: true, highlighted: hiRef.current }, e.key, list.length);
594
636
  if (intent.type === "select") {
595
637
  e.preventDefault();
596
638
  return pick(list[intent.index]);
597
639
  }
598
- if (state.highlighted !== hi) {
640
+ if (state.highlighted !== hiRef.current) {
599
641
  e.preventDefault();
600
642
  return setHi(state.highlighted);
601
643
  }
@@ -605,6 +647,7 @@ function RowActions(props) {
605
647
  pick(byKbd);
606
648
  }
607
649
  };
650
+ onKeyDownRef.current = onKeyDown;
608
651
  return /* @__PURE__ */ jsxs(Fragment, { children: [
609
652
  /* @__PURE__ */ jsx(
610
653
  "button",
@@ -616,7 +659,7 @@ function RowActions(props) {
616
659
  "aria-label": `${t.moreFor} ${props.item.label}`,
617
660
  "aria-haspopup": "menu",
618
661
  "aria-expanded": open,
619
- onClick: () => open ? close() : show({ kind: "list", actions: props.item.actions }),
662
+ onClick: () => stackRef.current.length ? close() : show({ kind: "list", actions: props.item.actions }),
620
663
  children: /* @__PURE__ */ jsx(IconMore, {})
621
664
  }
622
665
  ),
@@ -641,13 +684,18 @@ function RowActions(props) {
641
684
  {
642
685
  type: "button",
643
686
  role: "menuitem",
644
- class: "bas-rowmenu__item" + (i === hi ? " is-active" : "") + (a.danger ? " is-danger" : ""),
687
+ ref: (el) => {
688
+ items.current[i] = el;
689
+ },
690
+ tabIndex: i === hi ? 0 : -1,
691
+ class: "bas-rowmenu__item" + (i === hi ? " is-active" : "") + (a.danger ? " is-danger" : "") + (armed === a.id ? " is-armed" : ""),
645
692
  "data-testid": `sidebar-item-${id}-action-${a.id}`,
646
693
  onMouseEnter: () => setHi(i),
694
+ onFocus: () => setHi(i),
647
695
  onClick: () => pick(a),
648
696
  children: [
649
697
  a.icon ? /* @__PURE__ */ jsx("span", { class: "bas-rowmenu__icon", children: a.icon }) : null,
650
- /* @__PURE__ */ jsx("span", { class: "bas-rowmenu__label", children: a.label }),
698
+ /* @__PURE__ */ jsx("span", { class: "bas-rowmenu__label", children: armed === a.id && a.confirm ? a.confirm.label : a.label }),
651
699
  a.submenu?.length ? /* @__PURE__ */ jsx(IconChevronRight, {}) : a.kbd ? /* @__PURE__ */ jsx("kbd", { class: "bas-rowmenu__kbd", children: a.kbd.toUpperCase() }) : null
652
700
  ]
653
701
  },
@@ -760,7 +808,7 @@ function Sidebar(p) {
760
808
  children: [
761
809
  active && !sub ? /* @__PURE__ */ jsx("span", { class: "bas-item__bar" }) : null,
762
810
  it.icon ? /* @__PURE__ */ jsx("span", { class: "bas-item__icon", children: it.icon }) : null,
763
- it.marker === "working" ? /* @__PURE__ */ jsx("span", { class: "bas-item__marker", "data-testid": `sidebar-item-${it.id}-marker`, role: "img", "aria-label": TEXT[p.lang].working }) : null,
811
+ it.marker === "working" ? /* @__PURE__ */ jsx("span", { class: "bas-item__marker", "data-testid": `sidebar-item-${it.id}-marker`, role: "img", "aria-label": TEXT[p.lang].working }) : it.marker === "idle" ? /* @__PURE__ */ jsx("span", { class: "bas-item__marker bas-item__marker--idle", "data-testid": `sidebar-item-${it.id}-marker`, "aria-hidden": "true" }) : null,
764
812
  /* @__PURE__ */ jsx("span", { class: "bas-item__label", children: it.label }),
765
813
  it.pinned ? /* @__PURE__ */ jsx("span", { class: "bas-item__pin", "data-testid": `sidebar-item-${it.id}-pinned`, role: "img", "aria-label": TEXT[p.lang].pinned, children: /* @__PURE__ */ jsx(IconPin, {}) }) : null,
766
814
  badge ? /* @__PURE__ */ jsx("span", { class: "bas-item__count", "data-testid": `sidebar-badge-${it.id}`, children: badge }) : null
@@ -851,19 +899,29 @@ function Sidebar(p) {
851
899
  ) : p.brandMark }) : null
852
900
  ] }) : null,
853
901
  /* @__PURE__ */ jsx("nav", { class: "bas-sidebar__nav", children: p.groups.map((g) => {
854
- const open = railed || !folded.has(g.label);
902
+ const open = railed || g.hideLabel || !folded.has(g.label);
855
903
  const slug = g.label.toLowerCase().replace(/[^a-z0-9]+/g, "-");
856
- return /* @__PURE__ */ jsxs("div", { class: "bas-group" + (g.variant === "plain" ? " bas-group--plain" : ""), children: [
857
- /* @__PURE__ */ jsxs("button", { type: "button", class: "bas-group__label", "data-testid": `sidebar-group-${slug}`, "aria-expanded": open, onClick: () => toggleGroup(g.label), children: [
858
- /* @__PURE__ */ jsx(IconChevron, { open }),
859
- /* @__PURE__ */ jsx("span", { children: g.label })
860
- ] }),
861
- open ? g.items.map(item) : null,
862
- open ? g.sections?.map((sec) => /* @__PURE__ */ jsxs("div", { class: "bas-section", "data-testid": `sidebar-section-${slug}-${sec.label.toLowerCase().replace(/[^a-z0-9æøå]+/g, "-")}`, children: [
863
- /* @__PURE__ */ jsx("div", { class: "bas-section__label", children: sec.label }),
864
- sec.items.map(item)
865
- ] }, sec.label)) : null
866
- ] }, g.label);
904
+ return /* @__PURE__ */ jsxs(
905
+ "div",
906
+ {
907
+ class: "bas-group" + (g.variant === "plain" ? " bas-group--plain" : ""),
908
+ role: g.hideLabel ? "group" : void 0,
909
+ "aria-label": g.hideLabel ? g.label : void 0,
910
+ "data-testid": g.hideLabel ? `sidebar-group-${slug}-unlabelled` : void 0,
911
+ children: [
912
+ g.hideLabel ? null : /* @__PURE__ */ jsxs("button", { type: "button", class: "bas-group__label", "data-testid": `sidebar-group-${slug}`, "aria-expanded": open, onClick: () => toggleGroup(g.label), children: [
913
+ /* @__PURE__ */ jsx(IconChevron, { open }),
914
+ /* @__PURE__ */ jsx("span", { children: g.label })
915
+ ] }),
916
+ open ? g.items.map(item) : null,
917
+ open ? g.sections?.map((sec) => /* @__PURE__ */ jsxs("div", { class: "bas-section", "data-testid": `sidebar-section-${slug}-${sec.label.toLowerCase().replace(/[^a-z0-9æøå]+/g, "-")}`, children: [
918
+ /* @__PURE__ */ jsx("div", { class: "bas-section__label", children: sec.label }),
919
+ sec.items.map(item)
920
+ ] }, sec.label)) : null
921
+ ]
922
+ },
923
+ g.label
924
+ );
867
925
  }) }),
868
926
  p.footer?.length ? /* @__PURE__ */ jsx("div", { class: "bas-sidebar__foot", children: p.footer.map(item) }) : null,
869
927
  p.bottom ? /* @__PURE__ */ jsx("div", { class: "bas-sidebar__user", "data-testid": "sidebar-user", children: p.bottom }) : null