@broberg/app-shell 0.11.0 → 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 CHANGED
@@ -53,6 +53,16 @@ the user menu reads and writes theme, palette, surfaces and backdrop through it.
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
+
56
66
  ## 0.11.0 — the user menu's Appearance, tidied (F001.23)
57
67
 
58
68
  Christian 10/10, via appkit: «Neuroner … er reelt set kun til trail og cardmem» and «Paletten er ikke pæn».
@@ -68,7 +78,7 @@ Christian 10/10, via appkit: «Neuroner … er reelt set kun til trail og cardme
68
78
  - **Palette is a dropdown**, not four wrapping buttons. Custom, not a native
69
79
  `<select>` (D-4cd764): arrows open and move, Enter/Space choose, Escape closes
70
80
  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.
81
+ menu. (0.11.1: it floats over the menu as a popover.)
72
82
  testids: `user-menu-palette` (the button), `user-menu-palette-list`,
73
83
  `user-menu-palette-<id>` (each option). **A Lens flow that clicked
74
84
  `user-menu-palette-<id>` directly must click `user-menu-palette` first.**
package/css/app-shell.css CHANGED
@@ -298,11 +298,10 @@
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; }
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; }
306
305
  .bas-dd__btn {
307
306
  display: inline-flex; align-items: center; gap: 8px; min-width: 120px; justify-content: space-between;
308
307
  font-family: var(--font-mono, ui-monospace, monospace); font-size: 11px; font-weight: 500; padding: 5px 10px;
@@ -313,8 +312,9 @@
313
312
  .bas-dd__btn[aria-expanded="true"] .bas-dd__caret { transform: rotate(180deg); }
314
313
  .bas-dd__caret { color: var(--fg-muted); transition: transform .12s; }
315
314
  .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);
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
318
  }
319
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
320
  .bas-dd__opt.is-active { background: var(--hover); }
package/dist/preact.cjs CHANGED
@@ -847,6 +847,33 @@ function Segmented(props) {
847
847
  }
848
848
  function Dropdown(props) {
849
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]);
850
877
  const current = Math.max(0, props.options.findIndex(([v]) => v === props.value));
851
878
  const choose = (i) => {
852
879
  setState({ open: false, highlighted: i });
@@ -867,6 +894,7 @@ function Dropdown(props) {
867
894
  /* @__PURE__ */ jsxRuntime.jsxs(
868
895
  "button",
869
896
  {
897
+ ref: btnRef,
870
898
  type: "button",
871
899
  class: "bas-dd__btn",
872
900
  "data-testid": props.testid,
@@ -882,7 +910,7 @@ function Dropdown(props) {
882
910
  ]
883
911
  }
884
912
  ),
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(
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(
886
914
  "li",
887
915
  {
888
916
  id: optId(v),