@broberg/app-shell 0.10.1 → 0.11.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/README.md +24 -1
- package/css/app-shell.css +21 -0
- package/dist/preact.cjs +61 -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 +61 -3
- package/dist/preact.js.map +1 -1
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -46,13 +46,36 @@ 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.0 — the user menu's Appearance, tidied (F001.23)
|
|
57
|
+
|
|
58
|
+
Christian 10/10, via appkit: «Neuroner … er reelt set kun til trail og cardmem» and «Paletten er ikke pæn».
|
|
59
|
+
|
|
60
|
+
- **Backdrop (Neurons · Plain) is opt-in.** It only means something in an app
|
|
61
|
+
that draws the neuron canvas (`mountConstellation` from `@broberg/theme`).
|
|
62
|
+
**trail and cardmem: add one line to keep it:**
|
|
63
|
+
```tsx
|
|
64
|
+
<AppShell userMenu={{ appearance: { backdrop: true } }} … />
|
|
65
|
+
```
|
|
66
|
+
`appearance: false` still hides the whole block; `true`/omitted shows Theme ·
|
|
67
|
+
Palette · Surfaces.
|
|
68
|
+
- **Palette is a dropdown**, not four wrapping buttons. Custom, not a native
|
|
69
|
+
`<select>` (D-4cd764): arrows open and move, Enter/Space choose, Escape closes
|
|
70
|
+
the list without choosing and leaves the menu open; a second Escape closes the
|
|
71
|
+
menu. It opens in flow under its row, so the menu never clips it.
|
|
72
|
+
testids: `user-menu-palette` (the button), `user-menu-palette-list`,
|
|
73
|
+
`user-menu-palette-<id>` (each option). **A Lens flow that clicked
|
|
74
|
+
`user-menu-palette-<id>` directly must click `user-menu-palette` first.**
|
|
75
|
+
- New dependency: `@broberg/ui-controls-core` (its keyboard reducer — pure, no DOM).
|
|
76
|
+
- Pairs with `@broberg/theme` 0.13.0, where Layered is the default. Works with
|
|
77
|
+
≥ 0.11.0 too; then the old default (Flat) applies.
|
|
78
|
+
|
|
56
79
|
## 0.9.2 — ConfirmDialog listens from the first frame (F092.14)
|
|
57
80
|
|
|
58
81
|
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 — the palette dropdown. The list opens IN FLOW under its row (the
|
|
302
|
+
menu clips its overflow, so an absolute list near the bottom would be cut);
|
|
303
|
+
display:contents lets button and list wrap inside .bas-pref. */
|
|
304
|
+
.bas-pref:has(> .bas-dd) { flex-wrap: wrap; }
|
|
305
|
+
.bas-dd { display: contents; }
|
|
306
|
+
.bas-dd__btn {
|
|
307
|
+
display: inline-flex; align-items: center; gap: 8px; min-width: 120px; justify-content: space-between;
|
|
308
|
+
font-family: var(--font-mono, ui-monospace, monospace); font-size: 11px; font-weight: 500; padding: 5px 10px;
|
|
309
|
+
border-radius: var(--bas-radius); background: var(--bg-sunk); border: 1px solid var(--border); color: var(--fg); cursor: pointer;
|
|
310
|
+
}
|
|
311
|
+
.bas-dd__btn:hover { border-color: var(--fg-faint); }
|
|
312
|
+
.bas-dd__btn:active { background: var(--active); }
|
|
313
|
+
.bas-dd__btn[aria-expanded="true"] .bas-dd__caret { transform: rotate(180deg); }
|
|
314
|
+
.bas-dd__caret { color: var(--fg-muted); transition: transform .12s; }
|
|
315
|
+
.bas-dd__list {
|
|
316
|
+
flex-basis: 100%; list-style: none; margin: 4px 0 0; padding: 4px;
|
|
317
|
+
border-radius: var(--bas-radius); background: var(--bg-sunk); border: 1px solid var(--border);
|
|
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,63 @@ 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 current = Math.max(0, props.options.findIndex(([v]) => v === props.value));
|
|
851
|
+
const choose = (i) => {
|
|
852
|
+
setState({ open: false, highlighted: i });
|
|
853
|
+
props.onChange(props.options[i][0]);
|
|
854
|
+
};
|
|
855
|
+
const onKeyDown = (e) => {
|
|
856
|
+
if (e.key === "Tab") return setState((st) => ({ ...st, open: false }));
|
|
857
|
+
const from = state.open ? state : { open: false, highlighted: current };
|
|
858
|
+
const { state: next, intent } = uiControlsCore.selectKeyReducer(from, e.key, props.options.length);
|
|
859
|
+
if (intent.type === "none" && next === from) return;
|
|
860
|
+
e.preventDefault();
|
|
861
|
+
if (state.open || intent.type !== "close") e.stopPropagation();
|
|
862
|
+
if (intent.type === "select") return choose(intent.index);
|
|
863
|
+
setState(next);
|
|
864
|
+
};
|
|
865
|
+
const optId = (v) => `${props.testid}-opt-${v}`;
|
|
866
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "bas-dd", onFocusOut: (e) => !e.currentTarget.contains(e.relatedTarget) && setState((st) => ({ ...st, open: false })), children: [
|
|
867
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
868
|
+
"button",
|
|
869
|
+
{
|
|
870
|
+
type: "button",
|
|
871
|
+
class: "bas-dd__btn",
|
|
872
|
+
"data-testid": props.testid,
|
|
873
|
+
"aria-haspopup": "listbox",
|
|
874
|
+
"aria-expanded": state.open,
|
|
875
|
+
"aria-label": props.label,
|
|
876
|
+
"aria-activedescendant": state.open && state.highlighted >= 0 ? optId(props.options[state.highlighted][0]) : void 0,
|
|
877
|
+
onClick: () => setState((st) => ({ open: !st.open, highlighted: current })),
|
|
878
|
+
onKeyDown,
|
|
879
|
+
children: [
|
|
880
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: props.options[current]?.[1] }),
|
|
881
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { class: "bas-dd__caret", "aria-hidden": "true", children: "\u25BE" })
|
|
882
|
+
]
|
|
883
|
+
}
|
|
884
|
+
),
|
|
885
|
+
state.open ? /* @__PURE__ */ jsxRuntime.jsx("ul", { class: "bas-dd__list", role: "listbox", "aria-label": props.label, "data-testid": `${props.testid}-list`, children: props.options.map(([v, label], i) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
886
|
+
"li",
|
|
887
|
+
{
|
|
888
|
+
id: optId(v),
|
|
889
|
+
role: "option",
|
|
890
|
+
"aria-selected": props.value === v,
|
|
891
|
+
class: "bas-dd__opt" + (i === state.highlighted ? " is-active" : ""),
|
|
892
|
+
"data-testid": `${props.testid}-${v}`,
|
|
893
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
894
|
+
onMouseEnter: () => setState((st) => ({ ...st, highlighted: i })),
|
|
895
|
+
onClick: () => choose(i),
|
|
896
|
+
children: [
|
|
897
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { class: "bas-mi__check", "aria-hidden": "true", children: props.value === v ? "\u2713" : "" }),
|
|
898
|
+
label
|
|
899
|
+
]
|
|
900
|
+
},
|
|
901
|
+
v
|
|
902
|
+
)) }) : null
|
|
903
|
+
] });
|
|
904
|
+
}
|
|
847
905
|
function UserMenu(p) {
|
|
848
906
|
const t = TEXT[p.lang];
|
|
849
907
|
const [open, setOpen] = hooks.useState(false);
|
|
@@ -1004,16 +1062,16 @@ function UserMenu(p) {
|
|
|
1004
1062
|
] }),
|
|
1005
1063
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "bas-pref", children: [
|
|
1006
1064
|
/* @__PURE__ */ jsxRuntime.jsx("span", { children: t.palette }),
|
|
1007
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1065
|
+
/* @__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
1066
|
] }),
|
|
1009
1067
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "bas-pref", children: [
|
|
1010
1068
|
/* @__PURE__ */ jsxRuntime.jsx("span", { children: t.surfaces }),
|
|
1011
1069
|
/* @__PURE__ */ jsxRuntime.jsx(Segmented, { testid: "user-menu-surfaces", value: surfaces, options: [["flat", t.flat], ["layered", t.layered]], onChange: theme.setSurfaces })
|
|
1012
1070
|
] }),
|
|
1013
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "bas-pref", children: [
|
|
1071
|
+
typeof p.appearance === "object" && p.appearance.backdrop ? /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "bas-pref", children: [
|
|
1014
1072
|
/* @__PURE__ */ jsxRuntime.jsx("span", { children: t.backdrop }),
|
|
1015
1073
|
/* @__PURE__ */ jsxRuntime.jsx(Segmented, { testid: "user-menu-backdrop", value: backdrop, options: [["neurons", t.neurons], ["plain", t.plain]], onChange: theme.setBackdrop })
|
|
1016
|
-
] })
|
|
1074
|
+
] }) : null
|
|
1017
1075
|
] })
|
|
1018
1076
|
] }) : null,
|
|
1019
1077
|
p.language ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|