@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/dist/preact.d.cts
CHANGED
|
@@ -465,8 +465,15 @@ interface UserMenuProps {
|
|
|
465
465
|
items?: UserMenuItem[];
|
|
466
466
|
onSignOut?: () => void | Promise<void>;
|
|
467
467
|
onNavigate?: Navigate;
|
|
468
|
-
/**
|
|
469
|
-
|
|
468
|
+
/**
|
|
469
|
+
* Show Theme · Palette · Surfaces. Default true; false hides the block.
|
|
470
|
+
* F001.23 — the Backdrop row (Neurons · Plain) is opt-in: pass
|
|
471
|
+
* `{ backdrop: true }`. Only apps that draw the neuron canvas
|
|
472
|
+
* (mountConstellation — trail, cardmem) have anything for it to switch.
|
|
473
|
+
*/
|
|
474
|
+
appearance?: boolean | {
|
|
475
|
+
backdrop?: boolean;
|
|
476
|
+
};
|
|
470
477
|
/**
|
|
471
478
|
* F092.11 — a «Sprog»/«Language» row with a segmented control, like the
|
|
472
479
|
* appearance rows. The app owns the language; the shell shows and reports it.
|
package/dist/preact.d.ts
CHANGED
|
@@ -465,8 +465,15 @@ interface UserMenuProps {
|
|
|
465
465
|
items?: UserMenuItem[];
|
|
466
466
|
onSignOut?: () => void | Promise<void>;
|
|
467
467
|
onNavigate?: Navigate;
|
|
468
|
-
/**
|
|
469
|
-
|
|
468
|
+
/**
|
|
469
|
+
* Show Theme · Palette · Surfaces. Default true; false hides the block.
|
|
470
|
+
* F001.23 — the Backdrop row (Neurons · Plain) is opt-in: pass
|
|
471
|
+
* `{ backdrop: true }`. Only apps that draw the neuron canvas
|
|
472
|
+
* (mountConstellation — trail, cardmem) have anything for it to switch.
|
|
473
|
+
*/
|
|
474
|
+
appearance?: boolean | {
|
|
475
|
+
backdrop?: boolean;
|
|
476
|
+
};
|
|
470
477
|
/**
|
|
471
478
|
* F092.11 — a «Sprog»/«Language» row with a segmented control, like the
|
|
472
479
|
* appearance rows. The app owns the language; the shell shows and reports it.
|
package/dist/preact.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useState, useEffect, useRef, useMemo, useLayoutEffect } from 'preact/hooks';
|
|
2
2
|
import { createBellShell } from '@broberg/notifications/shell';
|
|
3
|
+
import { selectKeyReducer } from '@broberg/ui-controls-core';
|
|
3
4
|
import { getPreference, getPalette, getSurfaces, getBackdrop, onThemeChange, onPaletteChange, onSurfacesChange, onBackdropChange, setPreference, setPalette, PALETTES, PALETTE_LABELS, setSurfaces, setBackdrop } from '@broberg/theme';
|
|
4
5
|
import { jsxs, Fragment, jsx } from 'preact/jsx-runtime';
|
|
5
6
|
|
|
@@ -842,6 +843,91 @@ function Segmented(props) {
|
|
|
842
843
|
};
|
|
843
844
|
return /* @__PURE__ */ jsx("div", { class: "bas-seg", "data-testid": props.testid, role: "group", "aria-label": props.label, onKeyDown, children: props.options.map(([v, label]) => /* @__PURE__ */ jsx("button", { type: "button", "aria-pressed": props.value === v, "data-testid": `${props.testid}-${v}`, onClick: () => props.onChange(v), children: label }, v)) });
|
|
844
845
|
}
|
|
846
|
+
function Dropdown(props) {
|
|
847
|
+
const [state, setState] = useState({ open: false, highlighted: -1 });
|
|
848
|
+
const btnRef = useRef(null);
|
|
849
|
+
const listRef = useRef(null);
|
|
850
|
+
const [pos, setPos] = useState(void 0);
|
|
851
|
+
useLayoutEffect(() => {
|
|
852
|
+
if (!state.open || !btnRef.current) return setPos(void 0);
|
|
853
|
+
const r = btnRef.current.getBoundingClientRect();
|
|
854
|
+
const vw = window.innerWidth, vh = window.innerHeight, m = 8;
|
|
855
|
+
const h = listRef.current?.offsetHeight ?? 0;
|
|
856
|
+
const width = Math.min(Math.max(r.width, 160), vw - 2 * m);
|
|
857
|
+
const left = Math.max(m, Math.min(r.right - width, vw - width - m));
|
|
858
|
+
const below = r.bottom + 4;
|
|
859
|
+
const top = below + h <= vh - m || r.top - 4 - h < m ? Math.min(below, vh - m - h) : r.top - 4 - h;
|
|
860
|
+
setPos({ position: "fixed", left: `${left}px`, top: `${Math.max(m, top)}px`, width: `${width}px` });
|
|
861
|
+
}, [state.open]);
|
|
862
|
+
useEffect(() => {
|
|
863
|
+
if (!state.open) return;
|
|
864
|
+
const close = (e) => {
|
|
865
|
+
if (e.target instanceof Node && listRef.current?.contains(e.target)) return;
|
|
866
|
+
setState((st) => ({ ...st, open: false }));
|
|
867
|
+
};
|
|
868
|
+
window.addEventListener("scroll", close, true);
|
|
869
|
+
window.addEventListener("resize", close);
|
|
870
|
+
return () => {
|
|
871
|
+
window.removeEventListener("scroll", close, true);
|
|
872
|
+
window.removeEventListener("resize", close);
|
|
873
|
+
};
|
|
874
|
+
}, [state.open]);
|
|
875
|
+
const current = Math.max(0, props.options.findIndex(([v]) => v === props.value));
|
|
876
|
+
const choose = (i) => {
|
|
877
|
+
setState({ open: false, highlighted: i });
|
|
878
|
+
props.onChange(props.options[i][0]);
|
|
879
|
+
};
|
|
880
|
+
const onKeyDown = (e) => {
|
|
881
|
+
if (e.key === "Tab") return setState((st) => ({ ...st, open: false }));
|
|
882
|
+
const from = state.open ? state : { open: false, highlighted: current };
|
|
883
|
+
const { state: next, intent } = selectKeyReducer(from, e.key, props.options.length);
|
|
884
|
+
if (intent.type === "none" && next === from) return;
|
|
885
|
+
e.preventDefault();
|
|
886
|
+
if (state.open || intent.type !== "close") e.stopPropagation();
|
|
887
|
+
if (intent.type === "select") return choose(intent.index);
|
|
888
|
+
setState(next);
|
|
889
|
+
};
|
|
890
|
+
const optId = (v) => `${props.testid}-opt-${v}`;
|
|
891
|
+
return /* @__PURE__ */ jsxs("div", { class: "bas-dd", onFocusOut: (e) => !e.currentTarget.contains(e.relatedTarget) && setState((st) => ({ ...st, open: false })), children: [
|
|
892
|
+
/* @__PURE__ */ jsxs(
|
|
893
|
+
"button",
|
|
894
|
+
{
|
|
895
|
+
ref: btnRef,
|
|
896
|
+
type: "button",
|
|
897
|
+
class: "bas-dd__btn",
|
|
898
|
+
"data-testid": props.testid,
|
|
899
|
+
"aria-haspopup": "listbox",
|
|
900
|
+
"aria-expanded": state.open,
|
|
901
|
+
"aria-label": props.label,
|
|
902
|
+
"aria-activedescendant": state.open && state.highlighted >= 0 ? optId(props.options[state.highlighted][0]) : void 0,
|
|
903
|
+
onClick: () => setState((st) => ({ open: !st.open, highlighted: current })),
|
|
904
|
+
onKeyDown,
|
|
905
|
+
children: [
|
|
906
|
+
/* @__PURE__ */ jsx("span", { children: props.options[current]?.[1] }),
|
|
907
|
+
/* @__PURE__ */ jsx("span", { class: "bas-dd__caret", "aria-hidden": "true", children: "\u25BE" })
|
|
908
|
+
]
|
|
909
|
+
}
|
|
910
|
+
),
|
|
911
|
+
state.open ? /* @__PURE__ */ 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__ */ jsxs(
|
|
912
|
+
"li",
|
|
913
|
+
{
|
|
914
|
+
id: optId(v),
|
|
915
|
+
role: "option",
|
|
916
|
+
"aria-selected": props.value === v,
|
|
917
|
+
class: "bas-dd__opt" + (i === state.highlighted ? " is-active" : ""),
|
|
918
|
+
"data-testid": `${props.testid}-${v}`,
|
|
919
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
920
|
+
onMouseEnter: () => setState((st) => ({ ...st, highlighted: i })),
|
|
921
|
+
onClick: () => choose(i),
|
|
922
|
+
children: [
|
|
923
|
+
/* @__PURE__ */ jsx("span", { class: "bas-mi__check", "aria-hidden": "true", children: props.value === v ? "\u2713" : "" }),
|
|
924
|
+
label
|
|
925
|
+
]
|
|
926
|
+
},
|
|
927
|
+
v
|
|
928
|
+
)) }) : null
|
|
929
|
+
] });
|
|
930
|
+
}
|
|
845
931
|
function UserMenu(p) {
|
|
846
932
|
const t = TEXT[p.lang];
|
|
847
933
|
const [open, setOpen] = useState(false);
|
|
@@ -1002,16 +1088,16 @@ function UserMenu(p) {
|
|
|
1002
1088
|
] }),
|
|
1003
1089
|
/* @__PURE__ */ jsxs("div", { class: "bas-pref", children: [
|
|
1004
1090
|
/* @__PURE__ */ jsx("span", { children: t.palette }),
|
|
1005
|
-
/* @__PURE__ */ jsx(
|
|
1091
|
+
/* @__PURE__ */ jsx(Dropdown, { testid: "user-menu-palette", label: t.palette, value: palette, options: PALETTES.map((x) => [x, PALETTE_LABELS[x]]), onChange: setPalette })
|
|
1006
1092
|
] }),
|
|
1007
1093
|
/* @__PURE__ */ jsxs("div", { class: "bas-pref", children: [
|
|
1008
1094
|
/* @__PURE__ */ jsx("span", { children: t.surfaces }),
|
|
1009
1095
|
/* @__PURE__ */ jsx(Segmented, { testid: "user-menu-surfaces", value: surfaces, options: [["flat", t.flat], ["layered", t.layered]], onChange: setSurfaces })
|
|
1010
1096
|
] }),
|
|
1011
|
-
/* @__PURE__ */ jsxs("div", { class: "bas-pref", children: [
|
|
1097
|
+
typeof p.appearance === "object" && p.appearance.backdrop ? /* @__PURE__ */ jsxs("div", { class: "bas-pref", children: [
|
|
1012
1098
|
/* @__PURE__ */ jsx("span", { children: t.backdrop }),
|
|
1013
1099
|
/* @__PURE__ */ jsx(Segmented, { testid: "user-menu-backdrop", value: backdrop, options: [["neurons", t.neurons], ["plain", t.plain]], onChange: setBackdrop })
|
|
1014
|
-
] })
|
|
1100
|
+
] }) : null
|
|
1015
1101
|
] })
|
|
1016
1102
|
] }) : null,
|
|
1017
1103
|
p.language ? /* @__PURE__ */ jsxs(Fragment, { children: [
|