@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/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
- /** Show Theme · Palette · Surfaces · Backdrop. Default true. */
469
- appearance?: boolean;
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
- /** Show Theme · Palette · Surfaces · Backdrop. Default true. */
469
- appearance?: boolean;
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(Segmented, { testid: "user-menu-palette", value: palette, options: PALETTES.map((x) => [x, PALETTE_LABELS[x]]), onChange: setPalette })
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: [