@carlonicora/nextjs-jsonapi 1.135.1 → 1.136.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.
@@ -22,7 +22,7 @@ var _chunkYBZVWLPHjs = require('../../chunk-YBZVWLPH.js');
22
22
 
23
23
 
24
24
 
25
- var _chunkDNWYHP4Vjs = require('../../chunk-DNWYHP4V.js');
25
+ var _chunk3NVJEXUTjs = require('../../chunk-3NVJEXUT.js');
26
26
 
27
27
 
28
28
  var _chunkC2LEIBUKjs = require('../../chunk-C2LEIBUK.js');
@@ -74,29 +74,29 @@ var _lucidereact = require('lucide-react');
74
74
 
75
75
 
76
76
  function HelpAssistantSheet({ open, onOpenChange }) {
77
- return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkDNWYHP4Vjs.Sheet, { open, onOpenChange, children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
78
- _chunkDNWYHP4Vjs.SheetContent,
77
+ return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunk3NVJEXUTjs.Sheet, { open, onOpenChange, children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
78
+ _chunk3NVJEXUTjs.SheetContent,
79
79
  {
80
80
  side: "right",
81
81
  className: "flex w-full flex-col data-[side=right]:sm:max-w-2xl data-[side=right]:lg:max-w-3xl",
82
- children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkDNWYHP4Vjs.AssistantProvider, { manageUrl: false, children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, HelpAssistantSheetBody, {}) })
82
+ children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunk3NVJEXUTjs.AssistantProvider, { manageUrl: false, children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, HelpAssistantSheetBody, {}) })
83
83
  }
84
84
  ) });
85
85
  }
86
86
  _chunk7QVYU63Ejs.__name.call(void 0, HelpAssistantSheet, "HelpAssistantSheet");
87
87
  function HelpAssistantSheetBody() {
88
88
  const t = _nextintl.useTranslations.call(void 0, );
89
- const ctx = _chunkDNWYHP4Vjs.useAssistantContext.call(void 0, );
89
+ const ctx = _chunk3NVJEXUTjs.useAssistantContext.call(void 0, );
90
90
  const showThread = !!ctx.assistant || ctx.sending || ctx.messages.length > 0;
91
91
  const send = /* @__PURE__ */ _chunk7QVYU63Ejs.__name.call(void 0, (content) => ctx.sendMessage(content, { howToMode: true }), "send");
92
92
  return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _jsxruntime.Fragment, { children: [
93
- /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkDNWYHP4Vjs.SheetHeader, { children: [
94
- /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkDNWYHP4Vjs.SheetTitle, { children: t("help.askAi.sheet.title") }),
95
- /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkDNWYHP4Vjs.SheetDescription, { children: t("help.askAi.sheet.subtitle") })
93
+ /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunk3NVJEXUTjs.SheetHeader, { children: [
94
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunk3NVJEXUTjs.SheetTitle, { children: t("help.askAi.sheet.title") }),
95
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunk3NVJEXUTjs.SheetDescription, { children: t("help.askAi.sheet.subtitle") })
96
96
  ] }),
97
- !showThread ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkDNWYHP4Vjs.AssistantEmptyState, { onSend: send }) : /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _jsxruntime.Fragment, { children: [
97
+ !showThread ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunk3NVJEXUTjs.AssistantEmptyState, { onSend: send }) : /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _jsxruntime.Fragment, { children: [
98
98
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
99
- _chunkDNWYHP4Vjs.AssistantThread,
99
+ _chunk3NVJEXUTjs.AssistantThread,
100
100
  {
101
101
  messages: ctx.messages,
102
102
  sending: ctx.sending,
@@ -106,7 +106,7 @@ function HelpAssistantSheetBody() {
106
106
  onRetry: ctx.retrySend
107
107
  }
108
108
  ),
109
- /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkDNWYHP4Vjs.AssistantComposer, { onSend: send, disabled: ctx.sending })
109
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunk3NVJEXUTjs.AssistantComposer, { onSend: send, disabled: ctx.sending })
110
110
  ] })
111
111
  ] });
112
112
  }
@@ -116,20 +116,20 @@ _chunk7QVYU63Ejs.__name.call(void 0, HelpAssistantSheetBody, "HelpAssistantSheet
116
116
 
117
117
  function HelpAskAi() {
118
118
  const t = _nextintl.useTranslations.call(void 0, );
119
- const { currentUser } = _chunkDNWYHP4Vjs.useCurrentUserContext.call(void 0, );
119
+ const { currentUser } = _chunk3NVJEXUTjs.useCurrentUserContext.call(void 0, );
120
120
  const [open, setOpen] = _react.useState.call(void 0, false);
121
121
  const disabled = !currentUser;
122
122
  if (disabled) {
123
- return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkDNWYHP4Vjs.Tooltip, { children: [
124
- /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkDNWYHP4Vjs.TooltipTrigger, { render: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkDNWYHP4Vjs.Button, { variant: "outline", size: "sm", disabled: true }), children: [
123
+ return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunk3NVJEXUTjs.Tooltip, { children: [
124
+ /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunk3NVJEXUTjs.TooltipTrigger, { render: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunk3NVJEXUTjs.Button, { variant: "outline", size: "sm", disabled: true }), children: [
125
125
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _lucidereact.SparklesIcon, { className: "h-4 w-4" }),
126
126
  t("help.askAi.button")
127
127
  ] }),
128
- /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkDNWYHP4Vjs.TooltipContent, { children: t("help.askAi.loginTooltip") })
128
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunk3NVJEXUTjs.TooltipContent, { children: t("help.askAi.loginTooltip") })
129
129
  ] });
130
130
  }
131
131
  return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _jsxruntime.Fragment, { children: [
132
- /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkDNWYHP4Vjs.Button, { variant: "outline", size: "sm", onClick: () => setOpen(true), children: [
132
+ /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunk3NVJEXUTjs.Button, { variant: "outline", size: "sm", onClick: () => setOpen(true), children: [
133
133
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _lucidereact.SparklesIcon, { className: "h-4 w-4" }),
134
134
  t("help.askAi.button")
135
135
  ] }),
@@ -142,7 +142,7 @@ _chunk7QVYU63Ejs.__name.call(void 0, HelpAskAi, "HelpAskAi");
142
142
 
143
143
  function HelpHeader() {
144
144
  const t = _nextintl.useTranslations.call(void 0, );
145
- const { currentUser } = _chunkDNWYHP4Vjs.useCurrentUserContext.call(void 0, );
145
+ const { currentUser } = _chunk3NVJEXUTjs.useCurrentUserContext.call(void 0, );
146
146
  const { brand } = useHelp();
147
147
  const logo = _optionalChain([brand, 'optionalAccess', _ => _.logo]);
148
148
  const label = _nullishCoalesce(_optionalChain([brand, 'optionalAccess', _2 => _2.label]), () => ( "Help"));
@@ -158,7 +158,7 @@ function HelpHeader() {
158
158
  ] }),
159
159
  /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "flex items-center gap-2", children: [
160
160
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0, HelpAskAi, {}),
161
- currentUser ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkDNWYHP4Vjs.Button, { render: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _link2.default, { href: appHref }), nativeButton: false, variant: "outline", size: "sm", children: t("help.header.openApp") }) : /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkDNWYHP4Vjs.Button, { render: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _link2.default, { href: "/login" }), nativeButton: false, variant: "outline", size: "sm", children: t("help.header.login") })
161
+ currentUser ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunk3NVJEXUTjs.Button, { render: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _link2.default, { href: appHref }), nativeButton: false, variant: "outline", size: "sm", children: t("help.header.openApp") }) : /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunk3NVJEXUTjs.Button, { render: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _link2.default, { href: "/login" }), nativeButton: false, variant: "outline", size: "sm", children: t("help.header.login") })
162
162
  ] })
163
163
  ] });
164
164
  }
@@ -200,7 +200,7 @@ _chunk7QVYU63Ejs.__name.call(void 0, useHelpFilter, "useHelpFilter");
200
200
 
201
201
  function HelpSideNav() {
202
202
  const t = _nextintl.useTranslations.call(void 0, );
203
- const generateUrl = _chunkDNWYHP4Vjs.usePageUrlGenerator.call(void 0, );
203
+ const generateUrl = _chunk3NVJEXUTjs.usePageUrlGenerator.call(void 0, );
204
204
  const pathname = _navigation.usePathname.call(void 0, );
205
205
  const [articles, setArticles] = _react.useState.call(void 0, []);
206
206
  _react.useEffect.call(void 0, () => {
@@ -281,7 +281,7 @@ _chunk7QVYU63Ejs.__name.call(void 0, HelpSideNav, "HelpSideNav");
281
281
  function HelpArticleBody(props) {
282
282
  const t = _nextintl.useTranslations.call(void 0, );
283
283
  const format = _nextintl.useFormatter.call(void 0, );
284
- const generateUrl = _chunkDNWYHP4Vjs.usePageUrlGenerator.call(void 0, );
284
+ const generateUrl = _chunk3NVJEXUTjs.usePageUrlGenerator.call(void 0, );
285
285
  const { howToType, title, summary, updatedAt, prev, next, children } = props;
286
286
  return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "article", { className: "prose dark:prose-invert max-w-none", children: [
287
287
  /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "nav", { className: "text-muted-foreground mb-2 text-xs", children: [
@@ -338,7 +338,7 @@ function HelpTOC({ headings }) {
338
338
  }, [headings]);
339
339
  if (headings.length === 0) return null;
340
340
  return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "nav", { "aria-label": t("help.toc.title"), className: "hidden lg:block", children: [
341
- /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkDNWYHP4Vjs.MicroLabel, { className: "mb-2", children: t("help.toc.title") }),
341
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunk3NVJEXUTjs.MicroLabel, { className: "mb-2", children: t("help.toc.title") }),
342
342
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "ul", { className: "space-y-1 text-sm", children: headings.map((h) => /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "li", { style: { paddingLeft: `${(h.depth - 2) * 0.75}rem` }, children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
343
343
  "a",
344
344
  {
@@ -359,7 +359,7 @@ _chunk7QVYU63Ejs.__name.call(void 0, HelpTOC, "HelpTOC");
359
359
 
360
360
  function HelpHint({ contextKey }) {
361
361
  const t = _nextintl.useTranslations.call(void 0, );
362
- const generateUrl = _chunkDNWYHP4Vjs.usePageUrlGenerator.call(void 0, );
362
+ const generateUrl = _chunk3NVJEXUTjs.usePageUrlGenerator.call(void 0, );
363
363
  const [open, setOpen] = _react.useState.call(void 0, false);
364
364
  const [picked, setPicked] = _react.useState.call(void 0, null);
365
365
  const [articles, setArticles] = _react.useState.call(void 0, []);
@@ -377,7 +377,7 @@ function HelpHint({ contextKey }) {
377
377
  if (matches.length === 0) return null;
378
378
  const active = _nullishCoalesce(picked, () => ( (matches.length === 1 ? matches[0] : null)));
379
379
  return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
380
- _chunkDNWYHP4Vjs.Sheet,
380
+ _chunk3NVJEXUTjs.Sheet,
381
381
  {
382
382
  open,
383
383
  onOpenChange: (o) => {
@@ -385,11 +385,11 @@ function HelpHint({ contextKey }) {
385
385
  if (!o) setPicked(null);
386
386
  },
387
387
  children: [
388
- /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkDNWYHP4Vjs.SheetTrigger, { render: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkDNWYHP4Vjs.Button, { variant: "ghost", size: "icon-sm", "aria-label": t("help.hint.trigger") }), children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _lucidereact.HelpCircleIcon, { className: "h-4 w-4" }) }),
389
- /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkDNWYHP4Vjs.SheetContent, { side: "right", className: "w-full max-w-md sm:max-w-lg", children: [
390
- /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkDNWYHP4Vjs.SheetHeader, { children: [
391
- /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkDNWYHP4Vjs.SheetTitle, { children: active ? active.name : t("help.hint.pickArticle") }),
392
- /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkDNWYHP4Vjs.SheetDescription, { children: _nullishCoalesce(_optionalChain([active, 'optionalAccess', _4 => _4.summary]), () => ( "")) })
388
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunk3NVJEXUTjs.SheetTrigger, { render: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunk3NVJEXUTjs.Button, { variant: "ghost", size: "icon-sm", "aria-label": t("help.hint.trigger") }), children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _lucidereact.HelpCircleIcon, { className: "h-4 w-4" }) }),
389
+ /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunk3NVJEXUTjs.SheetContent, { side: "right", className: "w-full max-w-md sm:max-w-lg", children: [
390
+ /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunk3NVJEXUTjs.SheetHeader, { children: [
391
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunk3NVJEXUTjs.SheetTitle, { children: active ? active.name : t("help.hint.pickArticle") }),
392
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunk3NVJEXUTjs.SheetDescription, { children: _nullishCoalesce(_optionalChain([active, 'optionalAccess', _4 => _4.summary]), () => ( "")) })
393
393
  ] }),
394
394
  active ? /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "mt-4 space-y-3", children: [
395
395
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { className: "text-muted-foreground text-sm", children: active.summary }),
@@ -421,7 +421,7 @@ _chunk7QVYU63Ejs.__name.call(void 0, HelpHint, "HelpHint");
421
421
 
422
422
  function HelpSearchResultRow({ result, onSelect }) {
423
423
  const t = _nextintl.useTranslations.call(void 0, );
424
- const generateUrl = _chunkDNWYHP4Vjs.usePageUrlGenerator.call(void 0, );
424
+ const generateUrl = _chunk3NVJEXUTjs.usePageUrlGenerator.call(void 0, );
425
425
  const [article, setArticle] = _react.useState.call(void 0, null);
426
426
  _react.useEffect.call(void 0, () => {
427
427
  let active = true;
@@ -432,7 +432,7 @@ function HelpSearchResultRow({ result, onSelect }) {
432
432
  }, [result.id]);
433
433
  if (!article) {
434
434
  return /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
435
- _chunkDNWYHP4Vjs.CommandItem,
435
+ _chunk3NVJEXUTjs.CommandItem,
436
436
  {
437
437
  value: result.id,
438
438
  disabled: true,
@@ -441,7 +441,7 @@ function HelpSearchResultRow({ result, onSelect }) {
441
441
  }
442
442
  );
443
443
  }
444
- return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkDNWYHP4Vjs.CommandItem, { value: result.id, onSelect, className: "cursor-pointer p-0", children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
444
+ return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunk3NVJEXUTjs.CommandItem, { value: result.id, onSelect, className: "cursor-pointer p-0", children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
445
445
  _link2.default,
446
446
  {
447
447
  href: generateUrl({ page: `/help/${article.howToType}/${article.slug}` }),
@@ -22,7 +22,7 @@ import {
22
22
  useAssistantContext,
23
23
  useCurrentUserContext,
24
24
  usePageUrlGenerator
25
- } from "../../chunk-EF2HVXOV.mjs";
25
+ } from "../../chunk-2KXPQHZU.mjs";
26
26
  import {
27
27
  _getStaticHelpContent
28
28
  } from "../../chunk-M5IAN7IR.mjs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carlonicora/nextjs-jsonapi",
3
- "version": "1.135.1",
3
+ "version": "1.136.0",
4
4
  "description": "Next.js JSON:API client with server/client support and caching",
5
5
  "author": "Carlo Nicora",
6
6
  "license": "GPL-3.0-or-later",
@@ -18,7 +18,7 @@ import {
18
18
  TabsTrigger,
19
19
  } from "@/components";
20
20
  import { partitionTabs, Tab } from "@/components/containers";
21
- import { RoundPageContainerTitle } from "@/components/containers/RoundPageContainerTitle";
21
+ import { HEADER_ROW_MIN_H, RoundPageContainerTitle } from "@/components/containers/RoundPageContainerTitle";
22
22
  import { Header, MobileNavigationBar } from "@/components/navigations";
23
23
  import { useHeaderChildren, useHeaderLeftContent, useHeaderLogo, useHeaderMobileChildren } from "@/contexts";
24
24
  import { useUrlRewriter } from "@/hooks";
@@ -55,6 +55,44 @@ type RoundPageContainerProps = {
55
55
  * ancestors.
56
56
  */
57
57
  onSectionChange?: (section: string) => void;
58
+ /**
59
+ * `data-testid` for the page shell. Applied to the outermost content wrapper
60
+ * of BOTH return branches — the pre-hydration one and the main one — so an
61
+ * e2e suite gating page-readiness on the testid can attach before hydration
62
+ * completes rather than racing it.
63
+ */
64
+ testId?: string;
65
+ /**
66
+ * Initial state of the `details` panel before the persisted preference (the
67
+ * `round_page_details_state` cookie) is read. Defaults to `false` — the panel
68
+ * starts collapsed, which suits an informational aside.
69
+ *
70
+ * Pass `true` when `details` holds PRIMARY navigation rather than an aside
71
+ * (e.g. the chat / conversation list), where a collapsed-by-default panel
72
+ * would hide the page's main affordance behind a toggle. The stored
73
+ * preference still wins in both directions once the user sets one.
74
+ *
75
+ * Requires the title bar to be rendered (`!fullWidth || forceHeader`) —
76
+ * that is where the toggle lives, so a `fullWidth` caller passing `details`
77
+ * without `forceHeader` leaves the panel unreachable.
78
+ */
79
+ defaultDetailsOpen?: boolean;
80
+ /**
81
+ * Heading for the `details` panel. Rendered as a fixed header above the
82
+ * panel's scroll area, used as the mobile `Sheet` title (replacing the old
83
+ * hardcoded "Details"), and woven into the toggle's tooltip — so the control
84
+ * reads "Show conversations" rather than an unlabelled icon.
85
+ *
86
+ * Strongly recommended whenever `details` holds primary navigation: without
87
+ * it the panel is an unlabelled column and the toggle is unguessable.
88
+ */
89
+ detailsTitle?: ReactNode;
90
+ /**
91
+ * Icon for the `details` toggle. Defaults to an info glyph, which suits an
92
+ * informational aside. Pass a panel/list glyph when `details` holds primary
93
+ * navigation — an "info" icon actively misdescribes a conversation list.
94
+ */
95
+ detailsIcon?: ReactNode;
58
96
  };
59
97
 
60
98
  // Rail trigger class: override the horizontal TabsTrigger defaults for a
@@ -82,28 +120,47 @@ export function RoundPageContainer({
82
120
  header,
83
121
  layout = "tabs",
84
122
  onSectionChange,
123
+ testId,
124
+ defaultDetailsOpen = false,
125
+ detailsTitle,
126
+ detailsIcon,
85
127
  }: RoundPageContainerProps) {
86
128
  const headerChildren = useHeaderChildren();
87
129
  const headerLeftContent = useHeaderLeftContent();
88
130
  const headerLogo = useHeaderLogo();
89
131
  const headerMobileChildren = useHeaderMobileChildren();
90
- const [showDetails, setShowDetailsState] = useState(false);
132
+ const [showDetails, setShowDetailsState] = useState(defaultDetailsOpen);
91
133
  const isMobile = useIsMobile();
92
134
  const [mounted, setMounted] = useState(false);
93
135
 
94
136
  useEffect(() => {
95
- const match = document.cookie.split("; ").find((row) => row.startsWith(`${DETAILS_COOKIE_NAME}=`));
96
- if (match?.split("=")[1] === "true") setShowDetailsState(true);
137
+ const match = document.cookie.split("; ").find((row) => row.startsWith(`${detailsCookieName}=`));
138
+ const stored = match?.split("=")[1];
139
+ // Apply the stored preference in BOTH directions. Previously this only ever
140
+ // opened the panel, which was harmless while the initial state was always
141
+ // `false` — but with `defaultDetailsOpen` a caller can start open, and a user
142
+ // who explicitly collapsed the panel must stay collapsed on the next load.
143
+ if (stored === "true") setShowDetailsState(true);
144
+ else if (stored === "false") setShowDetailsState(false);
97
145
  }, []);
98
146
 
99
147
  useEffect(() => {
100
148
  setMounted(true);
101
149
  }, []);
102
150
 
103
- const setShowDetails = useCallback((value: boolean) => {
104
- setShowDetailsState(value);
105
- document.cookie = `${DETAILS_COOKIE_NAME}=${value}; path=/; max-age=${DETAILS_COOKIE_MAX_AGE}`;
106
- }, []);
151
+ // Scope the persisted preference PER MODULE. A single global cookie meant
152
+ // collapsing an informational aside on one page silently collapsed a primary
153
+ // navigation panel on another — so a page declaring `defaultDetailsOpen`
154
+ // could still load collapsed because of an unrelated page's cookie.
155
+ const detailsCookieName = module?.name ? `${DETAILS_COOKIE_NAME}_${module.name}` : DETAILS_COOKIE_NAME;
156
+
157
+ const setShowDetails = useCallback(
158
+ (value: boolean) => {
159
+ setShowDetailsState(value);
160
+ document.cookie = `${detailsCookieName}=${value}; path=/; max-age=${DETAILS_COOKIE_MAX_AGE}`;
161
+ },
162
+ [detailsCookieName],
163
+ );
107
164
 
108
165
  const searchParams = useSearchParams();
109
166
  const section = searchParams.get("section");
@@ -163,7 +220,10 @@ export function RoundPageContainer({
163
220
  >
164
221
  {headerChildren}
165
222
  </Header>
166
- <div className={cn("flex h-[calc(100vh-3rem)] w-full flex-col", isMobile ? "gap-1 p-1 pt-0" : "p-2 pt-0 pl-0")}>
223
+ <div
224
+ data-testid={testId}
225
+ className={cn("flex h-[calc(100vh-3rem)] w-full flex-col", isMobile ? "gap-1 p-1 pt-0" : "p-2 pt-0 pl-0")}
226
+ >
167
227
  {/* `min-h-0 flex-1`, NOT `h-full`: the bar below is an in-flow sibling in
168
228
  this fixed-height flex column. `h-full` would resolve to 100% of the
169
229
  wrapper, leaving no room and pushing the bar below the fold (visible
@@ -187,7 +247,10 @@ export function RoundPageContainer({
187
247
  >
188
248
  {headerChildren}
189
249
  </Header>
190
- <div className={cn(`flex h-[calc(100vh-3rem)] w-full flex-col`, isMobile ? "gap-1 p-1 pt-0" : "p-2 pt-0 pl-0")}>
250
+ <div
251
+ data-testid={testId}
252
+ className={cn(`flex h-[calc(100vh-3rem)] w-full flex-col`, isMobile ? "gap-1 p-1 pt-0" : "p-2 pt-0 pl-0")}
253
+ >
191
254
  {/* `min-h-0 flex-1`, NOT `h-full`: MobileNavigationBar below is an in-flow
192
255
  sibling in this fixed-height flex column. `h-full` resolves to 100% of
193
256
  the wrapper, leaving the bar no room and pushing it below the fold —
@@ -198,6 +261,8 @@ export function RoundPageContainer({
198
261
  <RoundPageContainerTitle
199
262
  module={module}
200
263
  details={details}
264
+ detailsTitle={detailsTitle}
265
+ detailsIcon={detailsIcon}
201
266
  showDetails={showDetails}
202
267
  setShowDetails={setShowDetails}
203
268
  fullWidth={fullWidth}
@@ -402,7 +467,7 @@ export function RoundPageContainer({
402
467
  <Sheet open={showDetails} onOpenChange={setShowDetails}>
403
468
  <SheetContent side="right">
404
469
  <SheetHeader>
405
- <SheetTitle>Details</SheetTitle>
470
+ <SheetTitle>{detailsTitle ?? "Details"}</SheetTitle>
406
471
  </SheetHeader>
407
472
  <div className="overflow-y-auto p-6 pt-0">{details}</div>
408
473
  </SheetContent>
@@ -410,11 +475,25 @@ export function RoundPageContainer({
410
475
  ) : (
411
476
  <div
412
477
  className={cn(
413
- "h-full shrink-0 overflow-hidden overflow-y-auto transition-all duration-300 ease-in-out",
414
- showDetails ? "w-96 border-l p-4 opacity-100" : "ml-0 w-0 border-l-0 p-0 opacity-0",
478
+ // `flex-col` + a separate scroll body below: the header must stay
479
+ // put while the panel content scrolls. Scrolling on this element
480
+ // (as it did before) would carry the header away with the content.
481
+ "flex h-full shrink-0 flex-col overflow-hidden transition-all duration-300 ease-in-out",
482
+ showDetails ? "w-96 border-l opacity-100" : "ml-0 w-0 border-l-0 opacity-0",
415
483
  )}
416
484
  >
417
- {details}
485
+ {detailsTitle && (
486
+ // Mirrors RoundPageContainerTitle's structure exactly — border on an
487
+ // OUTER wrapper, height floor on the INNER row. Putting the border
488
+ // inside the measured row instead leaves this header 1px short of
489
+ // the page title bar, since `box-sizing: border-box` absorbs it.
490
+ <div className="shrink-0 border-b">
491
+ <div className={cn("flex items-center p-4", HEADER_ROW_MIN_H)}>
492
+ <span className="text-primary truncate text-sm font-semibold">{detailsTitle}</span>
493
+ </div>
494
+ </div>
495
+ )}
496
+ <div className="min-h-0 flex-1 overflow-y-auto p-4">{details}</div>
418
497
  </div>
419
498
  ))}
420
499
  </div>
@@ -7,9 +7,21 @@ import { cn, useIsMobile } from "@/utils";
7
7
  import { InfoIcon } from "lucide-react";
8
8
  import { ReactNode } from "react";
9
9
 
10
+ /**
11
+ * Shared height floor for the card's top row. Applied BOTH here and to the
12
+ * `details` panel header in RoundPageContainer, so the two line up across the
13
+ * card's top edge. Expressed in px on purpose: the app scales its root
14
+ * font-size (14px), so a rem value would drift between the two rows.
15
+ */
16
+ export const HEADER_ROW_MIN_H = "min-h-[53px]";
17
+
10
18
  type RoundPageContainerTitleProps = {
11
19
  module?: ModuleWithPermissions;
12
20
  details?: ReactNode;
21
+ /** Names what the toggle reveals, so the control is not an unlabelled icon. */
22
+ detailsTitle?: ReactNode;
23
+ /** Overrides the default info glyph — see `RoundPageContainer.detailsIcon`. */
24
+ detailsIcon?: ReactNode;
13
25
  showDetails: boolean;
14
26
  setShowDetails: (show: boolean) => void;
15
27
  fullWidth?: boolean;
@@ -18,6 +30,8 @@ type RoundPageContainerTitleProps = {
18
30
  export function RoundPageContainerTitle({
19
31
  module,
20
32
  details,
33
+ detailsTitle,
34
+ detailsIcon,
21
35
  showDetails,
22
36
  setShowDetails,
23
37
  }: RoundPageContainerTitleProps) {
@@ -26,7 +40,7 @@ export function RoundPageContainerTitle({
26
40
 
27
41
  return (
28
42
  <div className="flex w-full flex-col border-b">
29
- <div className={cn(`flex w-full flex-row items-center gap-x-2 p-4 justify-between`)}>
43
+ <div className={cn(`flex w-full flex-row items-center gap-x-2 p-4 justify-between`, HEADER_ROW_MIN_H)}>
30
44
  {!isMobile ? (
31
45
  <div className="flex w-full gap-x-4">
32
46
  <div className={"text-muted-foreground flex items-center gap-x-2 text-lg font-light whitespace-nowrap"}>
@@ -56,16 +70,12 @@ export function RoundPageContainerTitle({
56
70
  onClick={() => setShowDetails(!showDetails)}
57
71
  className={cn(`cursor-pointer`)}
58
72
  >
59
- {/* <InfoIcon className={cn(``, showDetails ? `text-muted-foreground` : `text-accent`)} /> */}
60
- <InfoIcon />
61
- {/* {showDetails ? (
62
- <PanelRightCloseIcon className="text-muted-foreground" />
63
- ) : (
64
- <PanelRightOpenIcon className="text-accent" />
65
- )} */}
73
+ {detailsIcon ?? <InfoIcon />}
66
74
  </Button>
67
75
  </TooltipTrigger>
68
- <TooltipContent>{showDetails ? "Hide details" : "Show details"}</TooltipContent>
76
+ <TooltipContent>
77
+ {showDetails ? "Hide" : "Show"} {detailsTitle ?? "details"}
78
+ </TooltipContent>
69
79
  </Tooltip>
70
80
  )}
71
81
  </div>