@broberg/app-shell 0.10.1 → 0.11.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 +34 -1
- package/css/app-shell.css +21 -0
- package/dist/preact.cjs +89 -3
- package/dist/preact.cjs.map +1 -1
- package/dist/preact.d.cts +9 -2
- package/dist/preact.d.ts +9 -2
- package/dist/preact.js +89 -3
- package/dist/preact.js.map +1 -1
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -46,13 +46,46 @@ the user menu reads and writes theme, palette, surfaces and backdrop through it.
|
|
|
46
46
|
| `Sidebar` | Groups (foldable, remembered) + footer, from data. Items can have `children` (one level) that open and close. Active item from `currentPath`. Badges, 99+. |
|
|
47
47
|
| `TopBar` | Brand left, `actions` slot, then **the bell and the user menu, always last, in that order**. |
|
|
48
48
|
| `NotificationBell` | Unread count, panel (dropdown on desktop, bottom sheet on a phone), mark one / all read. |
|
|
49
|
-
| `UserMenu` | Avatar, name + email, **Konto** → Broberg ID, your items, Theme · Palette · Surfaces · Backdrop, sign out. |
|
|
49
|
+
| `UserMenu` | Avatar, name + email, **Konto** → Broberg ID, your items, Theme · Palette · Surfaces (· Backdrop, opt-in), sign out. |
|
|
50
50
|
| `PageHeader`, `PageTabs` | Eyebrow/title/description/actions; tabs as routes (`?tab=` or one path per tab). |
|
|
51
51
|
| `Avatar` | `picture` when given, otherwise initials. |
|
|
52
52
|
| `AccountPage` | The user's own name and picture, edited inside the app (0.4.0). |
|
|
53
53
|
| `TenantSwitcher` | The user's own organisations, the active one marked, switching through the app's route (0.7.1). |
|
|
54
54
|
| `AnalyticsPage` | The app's own visitors, page views, page views per day and most visited pages, from Upmetrics (0.6.0). |
|
|
55
55
|
|
|
56
|
+
## 0.11.1 — the palette list floats over the menu (F001.25)
|
|
57
|
+
|
|
58
|
+
Christian 10/10: «Kunne denne ikke være en pop-over ting så dialogen ikke bliver
|
|
59
|
+
forstørret hver gang man klikker på listen?» The list is now a popover
|
|
60
|
+
(`position: fixed`, placed from the button): the menu keeps its height and
|
|
61
|
+
nothing below moves. It opens below the button, upward when there is no room,
|
|
62
|
+
stays inside the viewport, and closes when the menu scrolls or the window
|
|
63
|
+
resizes. testids and keys unchanged. Rendering moved: the open list no longer
|
|
64
|
+
pushes «Flader» and the rest down.
|
|
65
|
+
|
|
66
|
+
## 0.11.0 — the user menu's Appearance, tidied (F001.23)
|
|
67
|
+
|
|
68
|
+
Christian 10/10, via appkit: «Neuroner … er reelt set kun til trail og cardmem» and «Paletten er ikke pæn».
|
|
69
|
+
|
|
70
|
+
- **Backdrop (Neurons · Plain) is opt-in.** It only means something in an app
|
|
71
|
+
that draws the neuron canvas (`mountConstellation` from `@broberg/theme`).
|
|
72
|
+
**trail and cardmem: add one line to keep it:**
|
|
73
|
+
```tsx
|
|
74
|
+
<AppShell userMenu={{ appearance: { backdrop: true } }} … />
|
|
75
|
+
```
|
|
76
|
+
`appearance: false` still hides the whole block; `true`/omitted shows Theme ·
|
|
77
|
+
Palette · Surfaces.
|
|
78
|
+
- **Palette is a dropdown**, not four wrapping buttons. Custom, not a native
|
|
79
|
+
`<select>` (D-4cd764): arrows open and move, Enter/Space choose, Escape closes
|
|
80
|
+
the list without choosing and leaves the menu open; a second Escape closes the
|
|
81
|
+
menu. (0.11.1: it floats over the menu as a popover.)
|
|
82
|
+
testids: `user-menu-palette` (the button), `user-menu-palette-list`,
|
|
83
|
+
`user-menu-palette-<id>` (each option). **A Lens flow that clicked
|
|
84
|
+
`user-menu-palette-<id>` directly must click `user-menu-palette` first.**
|
|
85
|
+
- New dependency: `@broberg/ui-controls-core` (its keyboard reducer — pure, no DOM).
|
|
86
|
+
- Pairs with `@broberg/theme` 0.13.0, where Layered is the default. Works with
|
|
87
|
+
≥ 0.11.0 too; then the old default (Flat) applies.
|
|
88
|
+
|
|
56
89
|
## 0.9.2 — ConfirmDialog listens from the first frame (F092.14)
|
|
57
90
|
|
|
58
91
|
0.9.1 carried the same change but never reached npm: its publish run was stopped by a flaky test in this package (happy-dom followed an external link to the live site; now switched off). 0.9.2 is the first published version with the fix.
|
package/css/app-shell.css
CHANGED
|
@@ -298,6 +298,27 @@
|
|
|
298
298
|
}
|
|
299
299
|
.bas-seg > button[aria-pressed="true"] { background: var(--olive-btn); color: var(--paper, var(--bg-card)); }
|
|
300
300
|
.bas-seg > button:not([aria-pressed="true"]):hover { color: var(--fg); }
|
|
301
|
+
/* F001.23/F001.25 — the palette dropdown. The list is a POPOVER (position
|
|
302
|
+
fixed, placed from the button in JS), so opening it neither grows the menu
|
|
303
|
+
nor gets clipped by its overflow. */
|
|
304
|
+
.bas-dd { position: relative; display: inline-flex; }
|
|
305
|
+
.bas-dd__btn {
|
|
306
|
+
display: inline-flex; align-items: center; gap: 8px; min-width: 120px; justify-content: space-between;
|
|
307
|
+
font-family: var(--font-mono, ui-monospace, monospace); font-size: 11px; font-weight: 500; padding: 5px 10px;
|
|
308
|
+
border-radius: var(--bas-radius); background: var(--bg-sunk); border: 1px solid var(--border); color: var(--fg); cursor: pointer;
|
|
309
|
+
}
|
|
310
|
+
.bas-dd__btn:hover { border-color: var(--fg-faint); }
|
|
311
|
+
.bas-dd__btn:active { background: var(--active); }
|
|
312
|
+
.bas-dd__btn[aria-expanded="true"] .bas-dd__caret { transform: rotate(180deg); }
|
|
313
|
+
.bas-dd__caret { color: var(--fg-muted); transition: transform .12s; }
|
|
314
|
+
.bas-dd__list {
|
|
315
|
+
z-index: 70; list-style: none; margin: 0; padding: 4px; max-height: min(60vh, 320px); overflow-y: auto;
|
|
316
|
+
border-radius: var(--bas-radius); background: var(--bg-card); border: 1px solid var(--border);
|
|
317
|
+
box-shadow: 0 12px 32px rgba(0, 0, 0, .22);
|
|
318
|
+
}
|
|
319
|
+
.bas-dd__opt { display: flex; align-items: center; gap: 8px; padding: 6px 8px; border-radius: calc(var(--bas-radius) - 2px); font-size: 12.5px; color: var(--fg); cursor: pointer; }
|
|
320
|
+
.bas-dd__opt.is-active { background: var(--hover); }
|
|
321
|
+
.bas-dd__opt:active { background: var(--active); }
|
|
301
322
|
|
|
302
323
|
/* ── page header + tabs ── */
|
|
303
324
|
.bas-pagehead { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; padding: 24px 24px 12px; }
|
package/dist/preact.cjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
var hooks = require('preact/hooks');
|
|
4
4
|
var shell = require('@broberg/notifications/shell');
|
|
5
|
+
var uiControlsCore = require('@broberg/ui-controls-core');
|
|
5
6
|
var theme = require('@broberg/theme');
|
|
6
7
|
var jsxRuntime = require('preact/jsx-runtime');
|
|
7
8
|
|
|
@@ -844,6 +845,91 @@ function Segmented(props) {
|
|
|
844
845
|
};
|
|
845
846
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { class: "bas-seg", "data-testid": props.testid, role: "group", "aria-label": props.label, onKeyDown, children: props.options.map(([v, label]) => /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "aria-pressed": props.value === v, "data-testid": `${props.testid}-${v}`, onClick: () => props.onChange(v), children: label }, v)) });
|
|
846
847
|
}
|
|
848
|
+
function Dropdown(props) {
|
|
849
|
+
const [state, setState] = hooks.useState({ open: false, highlighted: -1 });
|
|
850
|
+
const btnRef = hooks.useRef(null);
|
|
851
|
+
const listRef = hooks.useRef(null);
|
|
852
|
+
const [pos, setPos] = hooks.useState(void 0);
|
|
853
|
+
hooks.useLayoutEffect(() => {
|
|
854
|
+
if (!state.open || !btnRef.current) return setPos(void 0);
|
|
855
|
+
const r = btnRef.current.getBoundingClientRect();
|
|
856
|
+
const vw = window.innerWidth, vh = window.innerHeight, m = 8;
|
|
857
|
+
const h = listRef.current?.offsetHeight ?? 0;
|
|
858
|
+
const width = Math.min(Math.max(r.width, 160), vw - 2 * m);
|
|
859
|
+
const left = Math.max(m, Math.min(r.right - width, vw - width - m));
|
|
860
|
+
const below = r.bottom + 4;
|
|
861
|
+
const top = below + h <= vh - m || r.top - 4 - h < m ? Math.min(below, vh - m - h) : r.top - 4 - h;
|
|
862
|
+
setPos({ position: "fixed", left: `${left}px`, top: `${Math.max(m, top)}px`, width: `${width}px` });
|
|
863
|
+
}, [state.open]);
|
|
864
|
+
hooks.useEffect(() => {
|
|
865
|
+
if (!state.open) return;
|
|
866
|
+
const close = (e) => {
|
|
867
|
+
if (e.target instanceof Node && listRef.current?.contains(e.target)) return;
|
|
868
|
+
setState((st) => ({ ...st, open: false }));
|
|
869
|
+
};
|
|
870
|
+
window.addEventListener("scroll", close, true);
|
|
871
|
+
window.addEventListener("resize", close);
|
|
872
|
+
return () => {
|
|
873
|
+
window.removeEventListener("scroll", close, true);
|
|
874
|
+
window.removeEventListener("resize", close);
|
|
875
|
+
};
|
|
876
|
+
}, [state.open]);
|
|
877
|
+
const current = Math.max(0, props.options.findIndex(([v]) => v === props.value));
|
|
878
|
+
const choose = (i) => {
|
|
879
|
+
setState({ open: false, highlighted: i });
|
|
880
|
+
props.onChange(props.options[i][0]);
|
|
881
|
+
};
|
|
882
|
+
const onKeyDown = (e) => {
|
|
883
|
+
if (e.key === "Tab") return setState((st) => ({ ...st, open: false }));
|
|
884
|
+
const from = state.open ? state : { open: false, highlighted: current };
|
|
885
|
+
const { state: next, intent } = uiControlsCore.selectKeyReducer(from, e.key, props.options.length);
|
|
886
|
+
if (intent.type === "none" && next === from) return;
|
|
887
|
+
e.preventDefault();
|
|
888
|
+
if (state.open || intent.type !== "close") e.stopPropagation();
|
|
889
|
+
if (intent.type === "select") return choose(intent.index);
|
|
890
|
+
setState(next);
|
|
891
|
+
};
|
|
892
|
+
const optId = (v) => `${props.testid}-opt-${v}`;
|
|
893
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "bas-dd", onFocusOut: (e) => !e.currentTarget.contains(e.relatedTarget) && setState((st) => ({ ...st, open: false })), children: [
|
|
894
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
895
|
+
"button",
|
|
896
|
+
{
|
|
897
|
+
ref: btnRef,
|
|
898
|
+
type: "button",
|
|
899
|
+
class: "bas-dd__btn",
|
|
900
|
+
"data-testid": props.testid,
|
|
901
|
+
"aria-haspopup": "listbox",
|
|
902
|
+
"aria-expanded": state.open,
|
|
903
|
+
"aria-label": props.label,
|
|
904
|
+
"aria-activedescendant": state.open && state.highlighted >= 0 ? optId(props.options[state.highlighted][0]) : void 0,
|
|
905
|
+
onClick: () => setState((st) => ({ open: !st.open, highlighted: current })),
|
|
906
|
+
onKeyDown,
|
|
907
|
+
children: [
|
|
908
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: props.options[current]?.[1] }),
|
|
909
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { class: "bas-dd__caret", "aria-hidden": "true", children: "\u25BE" })
|
|
910
|
+
]
|
|
911
|
+
}
|
|
912
|
+
),
|
|
913
|
+
state.open ? /* @__PURE__ */ jsxRuntime.jsx("ul", { ref: listRef, class: "bas-dd__list", style: pos ?? { position: "fixed", visibility: "hidden" }, role: "listbox", "aria-label": props.label, "data-testid": `${props.testid}-list`, children: props.options.map(([v, label], i) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
914
|
+
"li",
|
|
915
|
+
{
|
|
916
|
+
id: optId(v),
|
|
917
|
+
role: "option",
|
|
918
|
+
"aria-selected": props.value === v,
|
|
919
|
+
class: "bas-dd__opt" + (i === state.highlighted ? " is-active" : ""),
|
|
920
|
+
"data-testid": `${props.testid}-${v}`,
|
|
921
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
922
|
+
onMouseEnter: () => setState((st) => ({ ...st, highlighted: i })),
|
|
923
|
+
onClick: () => choose(i),
|
|
924
|
+
children: [
|
|
925
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { class: "bas-mi__check", "aria-hidden": "true", children: props.value === v ? "\u2713" : "" }),
|
|
926
|
+
label
|
|
927
|
+
]
|
|
928
|
+
},
|
|
929
|
+
v
|
|
930
|
+
)) }) : null
|
|
931
|
+
] });
|
|
932
|
+
}
|
|
847
933
|
function UserMenu(p) {
|
|
848
934
|
const t = TEXT[p.lang];
|
|
849
935
|
const [open, setOpen] = hooks.useState(false);
|
|
@@ -1004,16 +1090,16 @@ function UserMenu(p) {
|
|
|
1004
1090
|
] }),
|
|
1005
1091
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "bas-pref", children: [
|
|
1006
1092
|
/* @__PURE__ */ jsxRuntime.jsx("span", { children: t.palette }),
|
|
1007
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1093
|
+
/* @__PURE__ */ jsxRuntime.jsx(Dropdown, { testid: "user-menu-palette", label: t.palette, value: palette, options: theme.PALETTES.map((x) => [x, theme.PALETTE_LABELS[x]]), onChange: theme.setPalette })
|
|
1008
1094
|
] }),
|
|
1009
1095
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "bas-pref", children: [
|
|
1010
1096
|
/* @__PURE__ */ jsxRuntime.jsx("span", { children: t.surfaces }),
|
|
1011
1097
|
/* @__PURE__ */ jsxRuntime.jsx(Segmented, { testid: "user-menu-surfaces", value: surfaces, options: [["flat", t.flat], ["layered", t.layered]], onChange: theme.setSurfaces })
|
|
1012
1098
|
] }),
|
|
1013
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "bas-pref", children: [
|
|
1099
|
+
typeof p.appearance === "object" && p.appearance.backdrop ? /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "bas-pref", children: [
|
|
1014
1100
|
/* @__PURE__ */ jsxRuntime.jsx("span", { children: t.backdrop }),
|
|
1015
1101
|
/* @__PURE__ */ jsxRuntime.jsx(Segmented, { testid: "user-menu-backdrop", value: backdrop, options: [["neurons", t.neurons], ["plain", t.plain]], onChange: theme.setBackdrop })
|
|
1016
|
-
] })
|
|
1102
|
+
] }) : null
|
|
1017
1103
|
] })
|
|
1018
1104
|
] }) : null,
|
|
1019
1105
|
p.language ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|