@carlonicora/nextjs-jsonapi 1.135.1 → 1.137.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.
Files changed (29) hide show
  1. package/dist/{BlockNoteEditor-FAPCNKXZ.mjs → BlockNoteEditor-DFBJOA4C.mjs} +2 -2
  2. package/dist/{BlockNoteEditor-PYRHKBF3.js → BlockNoteEditor-WX2W67GH.js} +9 -9
  3. package/dist/{BlockNoteEditor-PYRHKBF3.js.map → BlockNoteEditor-WX2W67GH.js.map} +1 -1
  4. package/dist/billing/index.js +310 -310
  5. package/dist/billing/index.mjs +1 -1
  6. package/dist/{chunk-DNWYHP4V.js → chunk-CTKZM2YC.js} +381 -311
  7. package/dist/chunk-CTKZM2YC.js.map +1 -0
  8. package/dist/{chunk-EF2HVXOV.mjs → chunk-EWAOWYT5.mjs} +273 -203
  9. package/dist/chunk-EWAOWYT5.mjs.map +1 -0
  10. package/dist/client/index.js +2 -2
  11. package/dist/client/index.mjs +1 -1
  12. package/dist/components/index.d.mts +52 -3
  13. package/dist/components/index.d.ts +52 -3
  14. package/dist/components/index.js +4 -2
  15. package/dist/components/index.js.map +1 -1
  16. package/dist/components/index.mjs +3 -1
  17. package/dist/contexts/index.js +2 -2
  18. package/dist/contexts/index.mjs +1 -1
  19. package/dist/features/help/index.js +31 -31
  20. package/dist/features/help/index.mjs +1 -1
  21. package/package.json +1 -1
  22. package/src/components/containers/RoundPageContainer.tsx +99 -14
  23. package/src/components/containers/RoundPageContainerTitle.tsx +19 -9
  24. package/src/components/navigations/Header.tsx +15 -1
  25. package/src/components/pages/PageContentContainer.tsx +2 -2
  26. package/src/features/oauth/components/consent/OAuthConsentScreen.tsx +3 -3
  27. package/dist/chunk-DNWYHP4V.js.map +0 -1
  28. package/dist/chunk-EF2HVXOV.mjs.map +0 -1
  29. /package/dist/{BlockNoteEditor-FAPCNKXZ.mjs.map → BlockNoteEditor-DFBJOA4C.mjs.map} +0 -0
@@ -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 _chunkCTKZM2YCjs = require('../../chunk-CTKZM2YC.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, _chunkCTKZM2YCjs.Sheet, { open, onOpenChange, children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
78
+ _chunkCTKZM2YCjs.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, _chunkCTKZM2YCjs.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 = _chunkCTKZM2YCjs.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, _chunkCTKZM2YCjs.SheetHeader, { children: [
94
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkCTKZM2YCjs.SheetTitle, { children: t("help.askAi.sheet.title") }),
95
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkCTKZM2YCjs.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, _chunkCTKZM2YCjs.AssistantEmptyState, { onSend: send }) : /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _jsxruntime.Fragment, { children: [
98
98
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
99
- _chunkDNWYHP4Vjs.AssistantThread,
99
+ _chunkCTKZM2YCjs.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, _chunkCTKZM2YCjs.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 } = _chunkCTKZM2YCjs.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, _chunkCTKZM2YCjs.Tooltip, { children: [
124
+ /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkCTKZM2YCjs.TooltipTrigger, { render: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkCTKZM2YCjs.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, _chunkCTKZM2YCjs.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, _chunkCTKZM2YCjs.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 } = _chunkCTKZM2YCjs.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, _chunkCTKZM2YCjs.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, _chunkCTKZM2YCjs.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 = _chunkCTKZM2YCjs.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 = _chunkCTKZM2YCjs.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, _chunkCTKZM2YCjs.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 = _chunkCTKZM2YCjs.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
+ _chunkCTKZM2YCjs.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, _chunkCTKZM2YCjs.SheetTrigger, { render: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkCTKZM2YCjs.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, _chunkCTKZM2YCjs.SheetContent, { side: "right", className: "w-full max-w-md sm:max-w-lg", children: [
390
+ /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkCTKZM2YCjs.SheetHeader, { children: [
391
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkCTKZM2YCjs.SheetTitle, { children: active ? active.name : t("help.hint.pickArticle") }),
392
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkCTKZM2YCjs.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 = _chunkCTKZM2YCjs.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
+ _chunkCTKZM2YCjs.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, _chunkCTKZM2YCjs.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-EWAOWYT5.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.137.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,13 @@ 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(
226
+ "flex h-[calc(100dvh-var(--app-header-h,3rem))] w-full flex-col",
227
+ isMobile ? "gap-1 p-1 pt-0" : "p-2 pt-0 pl-0",
228
+ )}
229
+ >
167
230
  {/* `min-h-0 flex-1`, NOT `h-full`: the bar below is an in-flow sibling in
168
231
  this fixed-height flex column. `h-full` would resolve to 100% of the
169
232
  wrapper, leaving no room and pushing the bar below the fold (visible
@@ -187,7 +250,13 @@ export function RoundPageContainer({
187
250
  >
188
251
  {headerChildren}
189
252
  </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")}>
253
+ <div
254
+ data-testid={testId}
255
+ className={cn(
256
+ `flex h-[calc(100dvh-var(--app-header-h,3rem))] w-full flex-col`,
257
+ isMobile ? "gap-1 p-1 pt-0" : "p-2 pt-0 pl-0",
258
+ )}
259
+ >
191
260
  {/* `min-h-0 flex-1`, NOT `h-full`: MobileNavigationBar below is an in-flow
192
261
  sibling in this fixed-height flex column. `h-full` resolves to 100% of
193
262
  the wrapper, leaving the bar no room and pushing it below the fold —
@@ -198,6 +267,8 @@ export function RoundPageContainer({
198
267
  <RoundPageContainerTitle
199
268
  module={module}
200
269
  details={details}
270
+ detailsTitle={detailsTitle}
271
+ detailsIcon={detailsIcon}
201
272
  showDetails={showDetails}
202
273
  setShowDetails={setShowDetails}
203
274
  fullWidth={fullWidth}
@@ -402,7 +473,7 @@ export function RoundPageContainer({
402
473
  <Sheet open={showDetails} onOpenChange={setShowDetails}>
403
474
  <SheetContent side="right">
404
475
  <SheetHeader>
405
- <SheetTitle>Details</SheetTitle>
476
+ <SheetTitle>{detailsTitle ?? "Details"}</SheetTitle>
406
477
  </SheetHeader>
407
478
  <div className="overflow-y-auto p-6 pt-0">{details}</div>
408
479
  </SheetContent>
@@ -410,11 +481,25 @@ export function RoundPageContainer({
410
481
  ) : (
411
482
  <div
412
483
  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",
484
+ // `flex-col` + a separate scroll body below: the header must stay
485
+ // put while the panel content scrolls. Scrolling on this element
486
+ // (as it did before) would carry the header away with the content.
487
+ "flex h-full shrink-0 flex-col overflow-hidden transition-all duration-300 ease-in-out",
488
+ showDetails ? "w-96 border-l opacity-100" : "ml-0 w-0 border-l-0 opacity-0",
415
489
  )}
416
490
  >
417
- {details}
491
+ {detailsTitle && (
492
+ // Mirrors RoundPageContainerTitle's structure exactly — border on an
493
+ // OUTER wrapper, height floor on the INNER row. Putting the border
494
+ // inside the measured row instead leaves this header 1px short of
495
+ // the page title bar, since `box-sizing: border-box` absorbs it.
496
+ <div className="shrink-0 border-b">
497
+ <div className={cn("flex items-center p-4", HEADER_ROW_MIN_H)}>
498
+ <span className="text-primary truncate text-sm font-semibold">{detailsTitle}</span>
499
+ </div>
500
+ </div>
501
+ )}
502
+ <div className="min-h-0 flex-1 overflow-y-auto p-4">{details}</div>
418
503
  </div>
419
504
  ))}
420
505
  </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>
@@ -22,7 +22,21 @@ export function Header({ children, mobileChildren, leftContent, logo, className
22
22
  const isMobile = useIsMobile();
23
23
 
24
24
  return (
25
- <header className={`sticky top-0 z-10 flex h-12 flex-col items-center justify-start gap-x-4 ${className ?? ""}`}>
25
+ // Outer element owns the safe-area inset; the inner row keeps h-12 so the
26
+ // content height is unchanged and only the notch padding is added above.
27
+ // bg-sidebar: the outer element has no background of its own, and a padded
28
+ // transparent strip would show scrolled content behind the status bar.
29
+ // The fallback is MANDATORY in this package: other consumers (neural-erp)
30
+ // never define --app-header-h, and a var() referencing an undefined
31
+ // property invalidates the whole declaration. The fallback MUST include the
32
+ // safe-area inset, because the padding below is unconditional: a consumer
33
+ // that never defines the variable but does set viewportFit: "cover" would
34
+ // otherwise get 3rem of height plus inset padding, squeezing the inner
35
+ // h-12 row. (Tailwind arbitrary values need _ for spaces; CSS calc needs
36
+ // whitespace around the +.)
37
+ <header
38
+ className={`bg-sidebar sticky top-0 z-10 flex h-[var(--app-header-h,calc(3rem_+_env(safe-area-inset-top)))] flex-col items-center justify-start gap-x-4 pt-[env(safe-area-inset-top)] ${className ?? ""}`}
39
+ >
26
40
  <div className="bg-sidebar flex h-12 w-full flex-row items-center justify-between pl-2 pr-4">
27
41
  {isMobile && logo && <div className="flex shrink-0 flex-row items-center pr-1">{logo}</div>}
28
42
  <SidebarTrigger aria-label="Toggle sidebar" id="sidebar-trigger" />
@@ -35,7 +35,7 @@ export function PageContentContainer({ header, details, footer, content, fullBle
35
35
  });
36
36
 
37
37
  if (!isReady) {
38
- return <div className="flex h-[calc(100vh-4rem)] w-full flex-col" />;
38
+ return <div className="flex h-[calc(100dvh-4rem)] w-full flex-col" />;
39
39
  }
40
40
 
41
41
  // v4 CRITICAL: Use STRING percentages for Panel defaultSize, not numbers!
@@ -46,7 +46,7 @@ export function PageContentContainer({ header, details, footer, content, fullBle
46
46
  const rightPanelDefaultSize = isMobile ? "90%" : "68%";
47
47
 
48
48
  return (
49
- <div className="flex h-[calc(100vh-4rem)] w-full flex-col transition-opacity duration-150 animate-in fade-in">
49
+ <div className="flex h-[calc(100dvh-4rem)] w-full flex-col transition-opacity duration-150 animate-in fade-in">
50
50
  {header && <div className="mb-4 flex w-full shrink-0 border-b pr-4">{header}</div>}
51
51
  <div className="min-h-0 flex-1">
52
52
  {details || footer ? (
@@ -49,7 +49,7 @@ export function OAuthConsentScreen({
49
49
  // Loading state
50
50
  if (isLoading) {
51
51
  return (
52
- <div className="min-h-screen flex items-center justify-center p-4">
52
+ <div className="min-h-dvh flex items-center justify-center p-4">
53
53
  <Card className="w-full max-w-md">
54
54
  <CardContent className="flex flex-col items-center justify-center py-12">
55
55
  <Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
@@ -63,7 +63,7 @@ export function OAuthConsentScreen({
63
63
  // Error state
64
64
  if (error || !clientInfo) {
65
65
  return (
66
- <div className="min-h-screen flex items-center justify-center p-4">
66
+ <div className="min-h-dvh flex items-center justify-center p-4">
67
67
  <Card className="w-full max-w-md">
68
68
  <CardContent className="py-8">
69
69
  <Alert variant="destructive">
@@ -81,7 +81,7 @@ export function OAuthConsentScreen({
81
81
  const { client, scopes } = clientInfo;
82
82
 
83
83
  return (
84
- <div className="min-h-screen flex items-center justify-center p-4 bg-muted/30">
84
+ <div className="min-h-dvh flex items-center justify-center p-4 bg-muted/30">
85
85
  <Card className="w-full max-w-md">
86
86
  <CardContent className="pt-6 space-y-6">
87
87
  {/* Header */}