@broberg/app-shell 0.19.0 → 0.20.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/README.md +16 -0
- package/css/app-shell.css +5 -2
- package/dist/preact.cjs +115 -86
- package/dist/preact.cjs.map +1 -1
- package/dist/preact.d.cts +29 -1
- package/dist/preact.d.ts +29 -1
- package/dist/preact.js +117 -89
- package/dist/preact.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -160,6 +160,22 @@ with a building icon (`user-menu-org`); the sidebar foot keeps `subtitle`.
|
|
|
160
160
|
at the right of the heading row (`sidebar-group-<slug>-action`), also with `collapsible: false`; clicking it
|
|
161
161
|
never folds the group. The app opens its own menu from the anchor.
|
|
162
162
|
|
|
163
|
+
**0.20.0** — `ItemActionsMenu` (F092.29): the sidebar row menu on its own, so a title «⌄» opens THE SAME
|
|
164
|
+
menu (same keys, confirm, input, submenu). The sidebar's «…» uses this component — one implementation.
|
|
165
|
+
|
|
166
|
+
```tsx
|
|
167
|
+
const anchor = useRef<HTMLButtonElement>(null);
|
|
168
|
+
const [open, setOpen] = useState(false);
|
|
169
|
+
<button ref={anchor} data-testid="title-trigger" onClick={() => setOpen(!open)}>{title} ⌄</button>
|
|
170
|
+
{open && anchor.current ? (
|
|
171
|
+
<ItemActionsMenu actions={[...rowActions, { id: "share", label: "Del chat" }]} anchor={anchor.current}
|
|
172
|
+
lang="da" label={title} placement="down" testidPrefix="title"
|
|
173
|
+
onAction={(id, value) => …} onClose={() => setOpen(false)} />
|
|
174
|
+
) : null}
|
|
175
|
+
```
|
|
176
|
+
testids: `<prefix>-menu`, `<prefix>-action-<id>`, `<prefix>-back`, `<prefix>-input`, `<prefix>-save` — so the
|
|
177
|
+
anchor's own testid must not be `<prefix>-menu`.
|
|
178
|
+
|
|
163
179
|
## 0.11.1 — the palette list floats over the menu (F001.25)
|
|
164
180
|
|
|
165
181
|
Christian 10/10: «Kunne denne ikke være en pop-over ting så dialogen ikke bliver
|
package/css/app-shell.css
CHANGED
|
@@ -70,8 +70,11 @@
|
|
|
70
70
|
.bas-sidebar {
|
|
71
71
|
/* sticky makes its own stacking context, so the rail's flyout can only rise
|
|
72
72
|
* as high as the sidebar does; without this <main> paints over it and takes
|
|
73
|
-
* its clicks (appkit, Lens 839394b1). Below the top bar (50).
|
|
74
|
-
|
|
73
|
+
* its clicks (appkit, Lens 839394b1). Below the top bar (50).
|
|
74
|
+
* F092.30 — and ABOVE the inset content header (30): every popover that opens
|
|
75
|
+
* from the sidebar (user menu, row menu, flyout) lives in this context, so at
|
|
76
|
+
* 20 the header's line and blur ran over the user menu (scout #2779). */
|
|
77
|
+
z-index: 40;
|
|
75
78
|
flex: 0 0 240px; width: 240px;
|
|
76
79
|
position: sticky; top: var(--bas-top); align-self: flex-start;
|
|
77
80
|
height: calc(100dvh - var(--bas-top));
|
package/dist/preact.cjs
CHANGED
|
@@ -516,12 +516,17 @@ var IconBuilding = () => svg(/* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment
|
|
|
516
516
|
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M9 22v-4h6v4" }),
|
|
517
517
|
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M8 6h.01M16 6h.01M12 6h.01M12 10h.01M12 14h.01M16 10h.01M16 14h.01M8 10h.01M8 14h.01" })
|
|
518
518
|
] }), 15);
|
|
519
|
-
function
|
|
519
|
+
function ItemActionsMenu(props) {
|
|
520
520
|
const t = TEXT[props.lang];
|
|
521
|
-
const
|
|
522
|
-
const btn = hooks.useRef(null);
|
|
521
|
+
const pre = props.testidPrefix ?? "item-actions";
|
|
523
522
|
const menu = hooks.useRef(null);
|
|
524
|
-
const [
|
|
523
|
+
const first = [{ kind: "list", actions: props.actions }];
|
|
524
|
+
const [stack, setStack] = hooks.useState(first);
|
|
525
|
+
const stackRef = hooks.useRef(first);
|
|
526
|
+
const setStackNow = (next) => {
|
|
527
|
+
stackRef.current = next;
|
|
528
|
+
setStack(next);
|
|
529
|
+
};
|
|
525
530
|
const [hi, setHiState] = hooks.useState(0);
|
|
526
531
|
const hiRef = hooks.useRef(0);
|
|
527
532
|
const setHi = (n) => {
|
|
@@ -540,16 +545,12 @@ function RowActions(props) {
|
|
|
540
545
|
const [value, setValue] = hooks.useState("");
|
|
541
546
|
const [pos, setPos] = hooks.useState(void 0);
|
|
542
547
|
const view = stack[stack.length - 1];
|
|
543
|
-
const
|
|
544
|
-
const stackRef = hooks.useRef([]);
|
|
545
|
-
const setStackNow = (next) => {
|
|
546
|
-
stackRef.current = next;
|
|
547
|
-
setStack(next);
|
|
548
|
-
};
|
|
548
|
+
const closed = hooks.useRef(false);
|
|
549
549
|
const close = (refocus = true) => {
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
if (refocus)
|
|
550
|
+
if (closed.current) return;
|
|
551
|
+
closed.current = true;
|
|
552
|
+
if (refocus) props.anchor.focus();
|
|
553
|
+
props.onClose();
|
|
553
554
|
};
|
|
554
555
|
const show = (v) => {
|
|
555
556
|
setStackNow([...stackRef.current, v]);
|
|
@@ -574,25 +575,24 @@ function RowActions(props) {
|
|
|
574
575
|
props.onAction(a.id, value);
|
|
575
576
|
};
|
|
576
577
|
hooks.useLayoutEffect(() => {
|
|
577
|
-
|
|
578
|
-
const r = btn.current.getBoundingClientRect();
|
|
578
|
+
const r = props.anchor.getBoundingClientRect();
|
|
579
579
|
const vw = window.innerWidth, vh = window.innerHeight, m = 8;
|
|
580
580
|
const w = Math.min(240, vw - 2 * m);
|
|
581
581
|
const h = menu.current?.offsetHeight ?? 0;
|
|
582
582
|
const left = Math.max(m, Math.min(r.right - w, vw - w - m));
|
|
583
|
-
const
|
|
583
|
+
const fitsBelow = r.bottom + 4 + h <= vh - m;
|
|
584
|
+
const top = props.placement === "down" || fitsBelow || r.top - 4 - h < m ? Math.min(r.bottom + 4, vh - m - h) : r.top - 4 - h;
|
|
584
585
|
setPos({ position: "fixed", left: `${left}px`, top: `${Math.max(m, top)}px`, width: `${w}px` });
|
|
585
|
-
}, [
|
|
586
|
+
}, [stack.length]);
|
|
586
587
|
hooks.useLayoutEffect(() => {
|
|
587
|
-
if (!
|
|
588
|
-
if (view
|
|
588
|
+
if (!pos) return;
|
|
589
|
+
if (view.kind === "input") menu.current?.querySelector("input")?.focus();
|
|
589
590
|
else items.current[hiRef.current]?.focus();
|
|
590
|
-
}, [
|
|
591
|
+
}, [pos, hi, stack.length]);
|
|
591
592
|
hooks.useLayoutEffect(() => {
|
|
592
|
-
if (!open) return;
|
|
593
593
|
const outside = (e) => {
|
|
594
594
|
const n = e.target instanceof Node ? e.target : null;
|
|
595
|
-
if (n && (menu.current?.contains(n) ||
|
|
595
|
+
if (n && (menu.current?.contains(n) || props.anchor.contains(n))) return;
|
|
596
596
|
close(false);
|
|
597
597
|
};
|
|
598
598
|
const early = (e) => {
|
|
@@ -611,8 +611,9 @@ function RowActions(props) {
|
|
|
611
611
|
window.removeEventListener("scroll", outside, true);
|
|
612
612
|
window.removeEventListener("resize", outside);
|
|
613
613
|
};
|
|
614
|
-
}, [
|
|
614
|
+
}, []);
|
|
615
615
|
const onKeyDown = (e) => {
|
|
616
|
+
if (closed.current) return;
|
|
616
617
|
const stack2 = stackRef.current;
|
|
617
618
|
const view2 = stack2[stack2.length - 1];
|
|
618
619
|
if (!view2) return;
|
|
@@ -655,6 +656,82 @@ function RowActions(props) {
|
|
|
655
656
|
}
|
|
656
657
|
};
|
|
657
658
|
onKeyDownRef.current = onKeyDown;
|
|
659
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
660
|
+
"div",
|
|
661
|
+
{
|
|
662
|
+
ref: menu,
|
|
663
|
+
class: "bas-rowmenu",
|
|
664
|
+
role: "menu",
|
|
665
|
+
tabIndex: -1,
|
|
666
|
+
"aria-label": view.kind === "list" && view.title ? view.title : props.label,
|
|
667
|
+
"data-testid": `${pre}-menu`,
|
|
668
|
+
style: pos ?? { position: "fixed", visibility: "hidden" },
|
|
669
|
+
onKeyDown,
|
|
670
|
+
children: [
|
|
671
|
+
stack.length > 1 ? /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", class: "bas-rowmenu__back", "data-testid": `${pre}-back`, "aria-label": t.back, onClick: back, children: [
|
|
672
|
+
/* @__PURE__ */ jsxRuntime.jsx(IconChevronLeft, {}),
|
|
673
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: view.kind === "list" ? view.title : view.action.label })
|
|
674
|
+
] }) : null,
|
|
675
|
+
view.kind === "list" ? view.actions.map((a, i) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
676
|
+
"button",
|
|
677
|
+
{
|
|
678
|
+
type: "button",
|
|
679
|
+
role: "menuitem",
|
|
680
|
+
ref: (el) => {
|
|
681
|
+
items.current[i] = el;
|
|
682
|
+
},
|
|
683
|
+
tabIndex: i === hi ? 0 : -1,
|
|
684
|
+
class: "bas-rowmenu__item" + (i === hi ? " is-active" : "") + (a.danger ? " is-danger" : "") + (armed === a.id ? " is-armed" : ""),
|
|
685
|
+
"data-testid": `${pre}-action-${a.id}`,
|
|
686
|
+
onMouseEnter: () => setHi(i),
|
|
687
|
+
onFocus: () => setHi(i),
|
|
688
|
+
onClick: () => pick(a),
|
|
689
|
+
children: [
|
|
690
|
+
a.icon ? /* @__PURE__ */ jsxRuntime.jsx("span", { class: "bas-rowmenu__icon", children: a.icon }) : null,
|
|
691
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { class: "bas-rowmenu__label", children: armed === a.id && a.confirm ? a.confirm.label : a.label }),
|
|
692
|
+
a.submenu?.length ? /* @__PURE__ */ jsxRuntime.jsx(IconChevronRight, {}) : a.kbd ? /* @__PURE__ */ jsxRuntime.jsx("kbd", { class: "bas-rowmenu__kbd", children: a.kbd.toUpperCase() }) : null
|
|
693
|
+
]
|
|
694
|
+
},
|
|
695
|
+
a.id
|
|
696
|
+
)) : /* @__PURE__ */ jsxRuntime.jsxs(
|
|
697
|
+
"form",
|
|
698
|
+
{
|
|
699
|
+
class: "bas-rowmenu__form",
|
|
700
|
+
"data-testid": `${pre}-form`,
|
|
701
|
+
onSubmit: (e) => {
|
|
702
|
+
e.preventDefault();
|
|
703
|
+
save(view.action);
|
|
704
|
+
},
|
|
705
|
+
children: [
|
|
706
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
707
|
+
"input",
|
|
708
|
+
{
|
|
709
|
+
class: "bas-rowmenu__input",
|
|
710
|
+
"data-testid": `${pre}-input`,
|
|
711
|
+
"aria-label": view.action.label,
|
|
712
|
+
value,
|
|
713
|
+
placeholder: view.action.input.placeholder,
|
|
714
|
+
onInput: (e) => setValue(e.currentTarget.value)
|
|
715
|
+
}
|
|
716
|
+
),
|
|
717
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "submit", class: "bas-rowmenu__save", "data-testid": `${pre}-save`, children: view.action.input.saveLabel ?? t.save })
|
|
718
|
+
]
|
|
719
|
+
}
|
|
720
|
+
)
|
|
721
|
+
]
|
|
722
|
+
}
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
function RowActions(props) {
|
|
726
|
+
const t = TEXT[props.lang];
|
|
727
|
+
const id = props.item.id;
|
|
728
|
+
const btn = hooks.useRef(null);
|
|
729
|
+
const [open, setOpenState] = hooks.useState(false);
|
|
730
|
+
const openRef = hooks.useRef(false);
|
|
731
|
+
const setOpen = (v) => {
|
|
732
|
+
openRef.current = v;
|
|
733
|
+
setOpenState(v);
|
|
734
|
+
};
|
|
658
735
|
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
659
736
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
660
737
|
"button",
|
|
@@ -666,73 +743,24 @@ function RowActions(props) {
|
|
|
666
743
|
"aria-label": `${t.moreFor} ${props.item.label}`,
|
|
667
744
|
"aria-haspopup": "menu",
|
|
668
745
|
"aria-expanded": open,
|
|
669
|
-
onClick: () =>
|
|
746
|
+
onClick: () => {
|
|
747
|
+
if (!openRef.current) return setOpen(true);
|
|
748
|
+
setOpen(false);
|
|
749
|
+
btn.current?.focus();
|
|
750
|
+
},
|
|
670
751
|
children: /* @__PURE__ */ jsxRuntime.jsx(IconMore, {})
|
|
671
752
|
}
|
|
672
753
|
),
|
|
673
|
-
|
|
674
|
-
|
|
754
|
+
open && btn.current ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
755
|
+
ItemActionsMenu,
|
|
675
756
|
{
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
onKeyDown,
|
|
684
|
-
children: [
|
|
685
|
-
stack.length > 1 ? /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", class: "bas-rowmenu__back", "data-testid": `sidebar-item-${id}-back`, "aria-label": t.back, onClick: back, children: [
|
|
686
|
-
/* @__PURE__ */ jsxRuntime.jsx(IconChevronLeft, {}),
|
|
687
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { children: view.kind === "list" ? view.title : view.action.label })
|
|
688
|
-
] }) : null,
|
|
689
|
-
view.kind === "list" ? view.actions.map((a, i) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
690
|
-
"button",
|
|
691
|
-
{
|
|
692
|
-
type: "button",
|
|
693
|
-
role: "menuitem",
|
|
694
|
-
ref: (el) => {
|
|
695
|
-
items.current[i] = el;
|
|
696
|
-
},
|
|
697
|
-
tabIndex: i === hi ? 0 : -1,
|
|
698
|
-
class: "bas-rowmenu__item" + (i === hi ? " is-active" : "") + (a.danger ? " is-danger" : "") + (armed === a.id ? " is-armed" : ""),
|
|
699
|
-
"data-testid": `sidebar-item-${id}-action-${a.id}`,
|
|
700
|
-
onMouseEnter: () => setHi(i),
|
|
701
|
-
onFocus: () => setHi(i),
|
|
702
|
-
onClick: () => pick(a),
|
|
703
|
-
children: [
|
|
704
|
-
a.icon ? /* @__PURE__ */ jsxRuntime.jsx("span", { class: "bas-rowmenu__icon", children: a.icon }) : null,
|
|
705
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { class: "bas-rowmenu__label", children: armed === a.id && a.confirm ? a.confirm.label : a.label }),
|
|
706
|
-
a.submenu?.length ? /* @__PURE__ */ jsxRuntime.jsx(IconChevronRight, {}) : a.kbd ? /* @__PURE__ */ jsxRuntime.jsx("kbd", { class: "bas-rowmenu__kbd", children: a.kbd.toUpperCase() }) : null
|
|
707
|
-
]
|
|
708
|
-
},
|
|
709
|
-
a.id
|
|
710
|
-
)) : /* @__PURE__ */ jsxRuntime.jsxs(
|
|
711
|
-
"form",
|
|
712
|
-
{
|
|
713
|
-
class: "bas-rowmenu__form",
|
|
714
|
-
"data-testid": `sidebar-item-${id}-form`,
|
|
715
|
-
onSubmit: (e) => {
|
|
716
|
-
e.preventDefault();
|
|
717
|
-
save(view.action);
|
|
718
|
-
},
|
|
719
|
-
children: [
|
|
720
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
721
|
-
"input",
|
|
722
|
-
{
|
|
723
|
-
class: "bas-rowmenu__input",
|
|
724
|
-
"data-testid": `sidebar-item-${id}-input`,
|
|
725
|
-
"aria-label": view.action.label,
|
|
726
|
-
value,
|
|
727
|
-
placeholder: view.action.input.placeholder,
|
|
728
|
-
onInput: (e) => setValue(e.currentTarget.value)
|
|
729
|
-
}
|
|
730
|
-
),
|
|
731
|
-
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "submit", class: "bas-rowmenu__save", "data-testid": `sidebar-item-${id}-save`, children: view.action.input.saveLabel ?? t.save })
|
|
732
|
-
]
|
|
733
|
-
}
|
|
734
|
-
)
|
|
735
|
-
]
|
|
757
|
+
actions: props.item.actions,
|
|
758
|
+
anchor: btn.current,
|
|
759
|
+
lang: props.lang,
|
|
760
|
+
label: props.item.label,
|
|
761
|
+
testidPrefix: `sidebar-item-${id}`,
|
|
762
|
+
onAction: props.onAction,
|
|
763
|
+
onClose: () => setOpen(false)
|
|
736
764
|
}
|
|
737
765
|
) : null
|
|
738
766
|
] });
|
|
@@ -2037,6 +2065,7 @@ exports.AnalyticsPage = AnalyticsPage;
|
|
|
2037
2065
|
exports.AppShell = AppShell;
|
|
2038
2066
|
exports.Avatar = Avatar;
|
|
2039
2067
|
exports.ConfirmDialog = ConfirmDialog;
|
|
2068
|
+
exports.ItemActionsMenu = ItemActionsMenu;
|
|
2040
2069
|
exports.NotificationBell = NotificationBell;
|
|
2041
2070
|
exports.PageHeader = PageHeader;
|
|
2042
2071
|
exports.PageTabs = PageTabs;
|