@carlonicora/nextjs-jsonapi 1.117.1 → 1.118.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.
- package/dist/{BlockNoteEditor-CEWZCORH.js → BlockNoteEditor-5FFVS7KW.js} +9 -9
- package/dist/{BlockNoteEditor-CEWZCORH.js.map → BlockNoteEditor-5FFVS7KW.js.map} +1 -1
- package/dist/{BlockNoteEditor-K4T6QOJO.mjs → BlockNoteEditor-6GRZW6BL.mjs} +2 -2
- package/dist/billing/index.js +299 -299
- package/dist/billing/index.mjs +1 -1
- package/dist/{chunk-6D4GJHLK.mjs → chunk-KXHEOLPZ.mjs} +273 -150
- package/dist/chunk-KXHEOLPZ.mjs.map +1 -0
- package/dist/{chunk-56QAXZKI.js → chunk-M2ODDWY3.js} +238 -115
- package/dist/chunk-M2ODDWY3.js.map +1 -0
- package/dist/client/index.js +2 -2
- package/dist/client/index.mjs +1 -1
- package/dist/components/index.d.mts +72 -30
- package/dist/components/index.d.ts +72 -30
- package/dist/components/index.js +4 -2
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +3 -1
- package/dist/contexts/index.js +2 -2
- package/dist/contexts/index.mjs +1 -1
- package/dist/features/help/index.js +30 -30
- package/dist/features/help/index.mjs +1 -1
- package/package.json +1 -1
- package/src/components/containers/RoundPageContainer.tsx +197 -76
- package/src/components/containers/TabsContainer.tsx +14 -0
- package/src/components/containers/__tests__/partitionTabs.spec.ts +51 -0
- package/src/components/containers/index.ts +1 -0
- package/src/components/containers/partitionTabs.ts +43 -0
- package/src/components/forms/EditorSheet.tsx +7 -2
- package/dist/chunk-56QAXZKI.js.map +0 -1
- package/dist/chunk-6D4GJHLK.mjs.map +0 -1
- /package/dist/{BlockNoteEditor-K4T6QOJO.mjs.map → BlockNoteEditor-6GRZW6BL.mjs.map} +0 -0
|
@@ -21,7 +21,7 @@ var _chunkYBZVWLPHjs = require('../../chunk-YBZVWLPH.js');
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
var
|
|
24
|
+
var _chunkM2ODDWY3js = require('../../chunk-M2ODDWY3.js');
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
var _chunk6KLR3WJQjs = require('../../chunk-6KLR3WJQ.js');
|
|
@@ -73,29 +73,29 @@ var _lucidereact = require('lucide-react');
|
|
|
73
73
|
|
|
74
74
|
|
|
75
75
|
function HelpAssistantSheet({ open, onOpenChange }) {
|
|
76
|
-
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
77
|
-
|
|
76
|
+
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkM2ODDWY3js.Sheet, { open, onOpenChange, children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
77
|
+
_chunkM2ODDWY3js.SheetContent,
|
|
78
78
|
{
|
|
79
79
|
side: "right",
|
|
80
80
|
className: "flex w-full flex-col data-[side=right]:sm:max-w-2xl data-[side=right]:lg:max-w-3xl",
|
|
81
|
-
children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
81
|
+
children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkM2ODDWY3js.AssistantProvider, { manageUrl: false, children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, HelpAssistantSheetBody, {}) })
|
|
82
82
|
}
|
|
83
83
|
) });
|
|
84
84
|
}
|
|
85
85
|
_chunk7QVYU63Ejs.__name.call(void 0, HelpAssistantSheet, "HelpAssistantSheet");
|
|
86
86
|
function HelpAssistantSheetBody() {
|
|
87
87
|
const t = _nextintl.useTranslations.call(void 0, );
|
|
88
|
-
const ctx =
|
|
88
|
+
const ctx = _chunkM2ODDWY3js.useAssistantContext.call(void 0, );
|
|
89
89
|
const showThread = !!ctx.assistant || ctx.sending || ctx.messages.length > 0;
|
|
90
90
|
const send = /* @__PURE__ */ _chunk7QVYU63Ejs.__name.call(void 0, (content) => ctx.sendMessage(content, { howToMode: true }), "send");
|
|
91
91
|
return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _jsxruntime.Fragment, { children: [
|
|
92
|
-
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
93
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
94
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
92
|
+
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkM2ODDWY3js.SheetHeader, { children: [
|
|
93
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkM2ODDWY3js.SheetTitle, { children: t("help.askAi.sheet.title") }),
|
|
94
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkM2ODDWY3js.SheetDescription, { children: t("help.askAi.sheet.subtitle") })
|
|
95
95
|
] }),
|
|
96
|
-
!showThread ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
96
|
+
!showThread ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkM2ODDWY3js.AssistantEmptyState, { onSend: send }) : /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _jsxruntime.Fragment, { children: [
|
|
97
97
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
98
|
-
|
|
98
|
+
_chunkM2ODDWY3js.AssistantThread,
|
|
99
99
|
{
|
|
100
100
|
messages: ctx.messages,
|
|
101
101
|
sending: ctx.sending,
|
|
@@ -105,7 +105,7 @@ function HelpAssistantSheetBody() {
|
|
|
105
105
|
onRetry: ctx.retrySend
|
|
106
106
|
}
|
|
107
107
|
),
|
|
108
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
108
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkM2ODDWY3js.AssistantComposer, { onSend: send, disabled: ctx.sending })
|
|
109
109
|
] })
|
|
110
110
|
] });
|
|
111
111
|
}
|
|
@@ -115,20 +115,20 @@ _chunk7QVYU63Ejs.__name.call(void 0, HelpAssistantSheetBody, "HelpAssistantSheet
|
|
|
115
115
|
|
|
116
116
|
function HelpAskAi() {
|
|
117
117
|
const t = _nextintl.useTranslations.call(void 0, );
|
|
118
|
-
const { currentUser } =
|
|
118
|
+
const { currentUser } = _chunkM2ODDWY3js.useCurrentUserContext.call(void 0, );
|
|
119
119
|
const [open, setOpen] = _react.useState.call(void 0, false);
|
|
120
120
|
const disabled = !currentUser;
|
|
121
121
|
if (disabled) {
|
|
122
|
-
return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
123
|
-
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
122
|
+
return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkM2ODDWY3js.Tooltip, { children: [
|
|
123
|
+
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkM2ODDWY3js.TooltipTrigger, { render: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkM2ODDWY3js.Button, { variant: "outline", size: "sm", disabled: true }), children: [
|
|
124
124
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _lucidereact.SparklesIcon, { className: "h-4 w-4" }),
|
|
125
125
|
t("help.askAi.button")
|
|
126
126
|
] }),
|
|
127
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
127
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkM2ODDWY3js.TooltipContent, { children: t("help.askAi.loginTooltip") })
|
|
128
128
|
] });
|
|
129
129
|
}
|
|
130
130
|
return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _jsxruntime.Fragment, { children: [
|
|
131
|
-
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
131
|
+
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkM2ODDWY3js.Button, { variant: "outline", size: "sm", onClick: () => setOpen(true), children: [
|
|
132
132
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _lucidereact.SparklesIcon, { className: "h-4 w-4" }),
|
|
133
133
|
t("help.askAi.button")
|
|
134
134
|
] }),
|
|
@@ -141,7 +141,7 @@ _chunk7QVYU63Ejs.__name.call(void 0, HelpAskAi, "HelpAskAi");
|
|
|
141
141
|
|
|
142
142
|
function HelpHeader() {
|
|
143
143
|
const t = _nextintl.useTranslations.call(void 0, );
|
|
144
|
-
const { currentUser } =
|
|
144
|
+
const { currentUser } = _chunkM2ODDWY3js.useCurrentUserContext.call(void 0, );
|
|
145
145
|
const { brand } = useHelp();
|
|
146
146
|
const logo = _optionalChain([brand, 'optionalAccess', _ => _.logo]);
|
|
147
147
|
const label = _nullishCoalesce(_optionalChain([brand, 'optionalAccess', _2 => _2.label]), () => ( "Help"));
|
|
@@ -157,7 +157,7 @@ function HelpHeader() {
|
|
|
157
157
|
] }),
|
|
158
158
|
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "flex items-center gap-2", children: [
|
|
159
159
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, HelpAskAi, {}),
|
|
160
|
-
currentUser ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
160
|
+
currentUser ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkM2ODDWY3js.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, _chunkM2ODDWY3js.Button, { render: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _link2.default, { href: "/login" }), nativeButton: false, variant: "outline", size: "sm", children: t("help.header.login") })
|
|
161
161
|
] })
|
|
162
162
|
] });
|
|
163
163
|
}
|
|
@@ -199,7 +199,7 @@ _chunk7QVYU63Ejs.__name.call(void 0, useHelpFilter, "useHelpFilter");
|
|
|
199
199
|
|
|
200
200
|
function HelpSideNav() {
|
|
201
201
|
const t = _nextintl.useTranslations.call(void 0, );
|
|
202
|
-
const generateUrl =
|
|
202
|
+
const generateUrl = _chunkM2ODDWY3js.usePageUrlGenerator.call(void 0, );
|
|
203
203
|
const pathname = _navigation.usePathname.call(void 0, );
|
|
204
204
|
const [articles, setArticles] = _react.useState.call(void 0, []);
|
|
205
205
|
_react.useEffect.call(void 0, () => {
|
|
@@ -280,7 +280,7 @@ _chunk7QVYU63Ejs.__name.call(void 0, HelpSideNav, "HelpSideNav");
|
|
|
280
280
|
function HelpArticleBody(props) {
|
|
281
281
|
const t = _nextintl.useTranslations.call(void 0, );
|
|
282
282
|
const format = _nextintl.useFormatter.call(void 0, );
|
|
283
|
-
const generateUrl =
|
|
283
|
+
const generateUrl = _chunkM2ODDWY3js.usePageUrlGenerator.call(void 0, );
|
|
284
284
|
const { howToType, title, summary, updatedAt, prev, next, children } = props;
|
|
285
285
|
return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "article", { className: "prose dark:prose-invert max-w-none", children: [
|
|
286
286
|
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "nav", { className: "text-muted-foreground mb-2 text-xs", children: [
|
|
@@ -358,7 +358,7 @@ _chunk7QVYU63Ejs.__name.call(void 0, HelpTOC, "HelpTOC");
|
|
|
358
358
|
|
|
359
359
|
function HelpHint({ contextKey }) {
|
|
360
360
|
const t = _nextintl.useTranslations.call(void 0, );
|
|
361
|
-
const generateUrl =
|
|
361
|
+
const generateUrl = _chunkM2ODDWY3js.usePageUrlGenerator.call(void 0, );
|
|
362
362
|
const [open, setOpen] = _react.useState.call(void 0, false);
|
|
363
363
|
const [picked, setPicked] = _react.useState.call(void 0, null);
|
|
364
364
|
const [articles, setArticles] = _react.useState.call(void 0, []);
|
|
@@ -376,7 +376,7 @@ function HelpHint({ contextKey }) {
|
|
|
376
376
|
if (matches.length === 0) return null;
|
|
377
377
|
const active = _nullishCoalesce(picked, () => ( (matches.length === 1 ? matches[0] : null)));
|
|
378
378
|
return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
379
|
-
|
|
379
|
+
_chunkM2ODDWY3js.Sheet,
|
|
380
380
|
{
|
|
381
381
|
open,
|
|
382
382
|
onOpenChange: (o) => {
|
|
@@ -384,11 +384,11 @@ function HelpHint({ contextKey }) {
|
|
|
384
384
|
if (!o) setPicked(null);
|
|
385
385
|
},
|
|
386
386
|
children: [
|
|
387
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
388
|
-
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
389
|
-
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
390
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
391
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
387
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkM2ODDWY3js.SheetTrigger, { render: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkM2ODDWY3js.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" }) }),
|
|
388
|
+
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkM2ODDWY3js.SheetContent, { side: "right", className: "w-full max-w-md sm:max-w-lg", children: [
|
|
389
|
+
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkM2ODDWY3js.SheetHeader, { children: [
|
|
390
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkM2ODDWY3js.SheetTitle, { children: active ? active.name : t("help.hint.pickArticle") }),
|
|
391
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkM2ODDWY3js.SheetDescription, { children: _nullishCoalesce(_optionalChain([active, 'optionalAccess', _4 => _4.summary]), () => ( "")) })
|
|
392
392
|
] }),
|
|
393
393
|
active ? /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "mt-4 space-y-3", children: [
|
|
394
394
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { className: "text-muted-foreground text-sm", children: active.summary }),
|
|
@@ -420,7 +420,7 @@ _chunk7QVYU63Ejs.__name.call(void 0, HelpHint, "HelpHint");
|
|
|
420
420
|
|
|
421
421
|
function HelpSearchResultRow({ result, onSelect }) {
|
|
422
422
|
const t = _nextintl.useTranslations.call(void 0, );
|
|
423
|
-
const generateUrl =
|
|
423
|
+
const generateUrl = _chunkM2ODDWY3js.usePageUrlGenerator.call(void 0, );
|
|
424
424
|
const [article, setArticle] = _react.useState.call(void 0, null);
|
|
425
425
|
_react.useEffect.call(void 0, () => {
|
|
426
426
|
let active = true;
|
|
@@ -431,7 +431,7 @@ function HelpSearchResultRow({ result, onSelect }) {
|
|
|
431
431
|
}, [result.id]);
|
|
432
432
|
if (!article) {
|
|
433
433
|
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
434
|
-
|
|
434
|
+
_chunkM2ODDWY3js.CommandItem,
|
|
435
435
|
{
|
|
436
436
|
value: result.id,
|
|
437
437
|
disabled: true,
|
|
@@ -440,7 +440,7 @@ function HelpSearchResultRow({ result, onSelect }) {
|
|
|
440
440
|
}
|
|
441
441
|
);
|
|
442
442
|
}
|
|
443
|
-
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
443
|
+
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkM2ODDWY3js.CommandItem, { value: result.id, onSelect, className: "cursor-pointer p-0", children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
444
444
|
_link2.default,
|
|
445
445
|
{
|
|
446
446
|
href: generateUrl({ page: `/help/${article.howToType}/${article.slug}` }),
|
package/package.json
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
import {
|
|
4
4
|
Select,
|
|
5
5
|
SelectContent,
|
|
6
|
+
SelectGroup,
|
|
6
7
|
SelectItem,
|
|
8
|
+
SelectLabel,
|
|
7
9
|
SelectTrigger,
|
|
8
10
|
SelectValue,
|
|
9
11
|
Sheet,
|
|
@@ -15,7 +17,7 @@ import {
|
|
|
15
17
|
TabsList,
|
|
16
18
|
TabsTrigger,
|
|
17
19
|
} from "@/components";
|
|
18
|
-
import { Tab } from "@/components/containers";
|
|
20
|
+
import { partitionTabs, Tab } from "@/components/containers";
|
|
19
21
|
import { RoundPageContainerTitle } from "@/components/containers/RoundPageContainerTitle";
|
|
20
22
|
import { Header } from "@/components/navigations";
|
|
21
23
|
import { useHeaderChildren, useHeaderLeftContent } from "@/contexts";
|
|
@@ -23,7 +25,7 @@ import { useUrlRewriter } from "@/hooks";
|
|
|
23
25
|
import { cn, useIsMobile } from "@/index";
|
|
24
26
|
import { ModuleWithPermissions } from "@/permissions";
|
|
25
27
|
import { useSearchParams } from "next/navigation";
|
|
26
|
-
import { ReactNode, useCallback, useEffect, useState } from "react";
|
|
28
|
+
import { Fragment, ReactNode, useCallback, useEffect, useMemo, useState } from "react";
|
|
27
29
|
|
|
28
30
|
const DETAILS_COOKIE_NAME = "round_page_details_state";
|
|
29
31
|
const DETAILS_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
|
|
@@ -37,8 +39,31 @@ type RoundPageContainerProps = {
|
|
|
37
39
|
fullWidth?: boolean;
|
|
38
40
|
forceHeader?: boolean;
|
|
39
41
|
header?: ReactNode;
|
|
42
|
+
/**
|
|
43
|
+
* Section-navigation layout for `tabs`.
|
|
44
|
+
* - `"tabs"` (default) — horizontal `TabsList` (desktop) / `Select` (mobile).
|
|
45
|
+
* Unchanged from prior behaviour; existing callers need no edits.
|
|
46
|
+
* - `"rail"` — vertical 220px left rail grouped by each tab's `group`, with a
|
|
47
|
+
* `<md` grouped `Select` fallback. Mirrors the retired
|
|
48
|
+
* `PageContainerContentDetails` rail.
|
|
49
|
+
*/
|
|
50
|
+
layout?: "tabs" | "rail";
|
|
40
51
|
};
|
|
41
52
|
|
|
53
|
+
// Rail trigger class: override the horizontal TabsTrigger defaults for a
|
|
54
|
+
// vertical, left-aligned, dark-filled active state. tailwind-merge inside cn()
|
|
55
|
+
// resolves the conflicts with the base classes.
|
|
56
|
+
const railTriggerClass = cn(
|
|
57
|
+
"flex w-full items-center justify-start rounded-md px-3 py-1.5 text-left text-sm leading-tight whitespace-normal",
|
|
58
|
+
"text-muted-foreground",
|
|
59
|
+
"hover:bg-muted hover:text-foreground",
|
|
60
|
+
"data-[state=active]:bg-foreground data-[state=active]:text-background",
|
|
61
|
+
"data-[state=active]:font-semibold data-[state=active]:shadow-none",
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
/** Stable value for the URL `?section=` and active-tab matching. */
|
|
65
|
+
const tabValue = (tab: Tab): string => tab.sectionKey ?? tab.key?.name ?? tab.label;
|
|
66
|
+
|
|
42
67
|
export function RoundPageContainer({
|
|
43
68
|
module,
|
|
44
69
|
id,
|
|
@@ -48,6 +73,7 @@ export function RoundPageContainer({
|
|
|
48
73
|
fullWidth,
|
|
49
74
|
forceHeader,
|
|
50
75
|
header,
|
|
76
|
+
layout = "tabs",
|
|
51
77
|
}: RoundPageContainerProps) {
|
|
52
78
|
const headerChildren = useHeaderChildren();
|
|
53
79
|
const headerLeftContent = useHeaderLeftContent();
|
|
@@ -75,15 +101,14 @@ export function RoundPageContainer({
|
|
|
75
101
|
const rewriteUrl = useUrlRewriter();
|
|
76
102
|
|
|
77
103
|
const initialValue = tabs
|
|
78
|
-
? (section && tabs.find((i) => (i
|
|
79
|
-
(tabs[0].key?.name ?? tabs[0].label)
|
|
104
|
+
? (section && tabs.find((i) => tabValue(i) === section) ? section : null) || tabValue(tabs[0])
|
|
80
105
|
: undefined;
|
|
81
106
|
|
|
82
107
|
const [activeTab, setActiveTab] = useState(initialValue);
|
|
83
108
|
|
|
84
109
|
useEffect(() => {
|
|
85
110
|
if (tabs && section) {
|
|
86
|
-
const tab = tabs.find((i) => (i
|
|
111
|
+
const tab = tabs.find((i) => tabValue(i) === section);
|
|
87
112
|
if (tab) {
|
|
88
113
|
setActiveTab(section);
|
|
89
114
|
}
|
|
@@ -98,7 +123,10 @@ export function RoundPageContainer({
|
|
|
98
123
|
[module, id, rewriteUrl],
|
|
99
124
|
);
|
|
100
125
|
|
|
101
|
-
const activeFillHeight = tabs?.find((t) => (t
|
|
126
|
+
const activeFillHeight = tabs?.find((t) => tabValue(t) === activeTab)?.fillHeight === true;
|
|
127
|
+
|
|
128
|
+
// Rail partition — only consumed by `layout="rail"` but cheap to compute.
|
|
129
|
+
const { ungrouped, groups } = useMemo(() => partitionTabs(tabs ?? []), [tabs]);
|
|
102
130
|
|
|
103
131
|
const isReady = mounted && isMobile !== undefined;
|
|
104
132
|
|
|
@@ -135,85 +163,178 @@ export function RoundPageContainer({
|
|
|
135
163
|
/>
|
|
136
164
|
)}
|
|
137
165
|
<div className="flex h-full w-full overflow-hidden">
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
166
|
+
{layout === "rail" && tabs ? (
|
|
167
|
+
// Rail layout: the vertical-tab navigation is a flush-left
|
|
168
|
+
// sidebar of the card and the content fills the full remaining
|
|
169
|
+
// width (like `fullWidth`) — NOT the centred max-w-6xl column.
|
|
170
|
+
<Tabs
|
|
171
|
+
value={activeTab}
|
|
172
|
+
onValueChange={handleTabChange}
|
|
173
|
+
orientation="vertical"
|
|
174
|
+
className="flex h-full min-w-0 grow overflow-hidden"
|
|
175
|
+
>
|
|
176
|
+
{/* Flush-left section rail — md and up */}
|
|
177
|
+
<aside
|
|
178
|
+
data-testid="round-page-rail"
|
|
179
|
+
className="hidden shrink-0 border-r p-4 md:flex md:w-56 md:flex-col md:overflow-y-auto"
|
|
180
|
+
>
|
|
181
|
+
<TabsList className="flex h-auto flex-col items-stretch gap-0.5 bg-transparent p-0">
|
|
182
|
+
{ungrouped.map((tab) => (
|
|
183
|
+
<TabsTrigger key={tab.label} value={tabValue(tab)} className={railTriggerClass}>
|
|
184
|
+
{tab.contentLabel ?? tab.label}
|
|
185
|
+
</TabsTrigger>
|
|
186
|
+
))}
|
|
187
|
+
{groups.map((group) => (
|
|
188
|
+
<Fragment key={group.label}>
|
|
189
|
+
<div
|
|
190
|
+
role="presentation"
|
|
191
|
+
className="text-muted-foreground px-3 pt-3 pb-1 text-[10px] font-bold tracking-wider uppercase"
|
|
192
|
+
>
|
|
193
|
+
{group.label}
|
|
194
|
+
</div>
|
|
195
|
+
{group.items.map((tab) => (
|
|
196
|
+
<TabsTrigger key={tab.label} value={tabValue(tab)} className={railTriggerClass}>
|
|
197
|
+
{tab.contentLabel ?? tab.label}
|
|
198
|
+
</TabsTrigger>
|
|
199
|
+
))}
|
|
200
|
+
</Fragment>
|
|
201
|
+
))}
|
|
202
|
+
</TabsList>
|
|
203
|
+
</aside>
|
|
204
|
+
|
|
205
|
+
{/* Content — full width, fills the remaining space */}
|
|
206
|
+
<div className="flex min-w-0 grow flex-col overflow-hidden">
|
|
207
|
+
{/* Section Select — below md */}
|
|
208
|
+
<div data-testid="round-page-rail-select" className="p-2 md:hidden">
|
|
209
|
+
<Select
|
|
210
|
+
value={activeTab}
|
|
211
|
+
onValueChange={(value) => {
|
|
212
|
+
if (value) handleTabChange(value);
|
|
213
|
+
}}
|
|
214
|
+
>
|
|
215
|
+
<SelectTrigger className="w-full">
|
|
216
|
+
<SelectValue />
|
|
217
|
+
</SelectTrigger>
|
|
218
|
+
<SelectContent>
|
|
219
|
+
{ungrouped.map((tab) => (
|
|
220
|
+
<SelectItem key={tab.label} value={tabValue(tab)}>
|
|
221
|
+
{tab.contentLabel ?? tab.label}
|
|
222
|
+
</SelectItem>
|
|
223
|
+
))}
|
|
224
|
+
{groups.map((group) => (
|
|
225
|
+
<SelectGroup key={group.label}>
|
|
226
|
+
<SelectLabel>{group.label}</SelectLabel>
|
|
227
|
+
{group.items.map((tab) => (
|
|
228
|
+
<SelectItem key={tab.label} value={tabValue(tab)}>
|
|
229
|
+
{tab.contentLabel ?? tab.label}
|
|
230
|
+
</SelectItem>
|
|
231
|
+
))}
|
|
232
|
+
</SelectGroup>
|
|
233
|
+
))}
|
|
234
|
+
</SelectContent>
|
|
235
|
+
</Select>
|
|
236
|
+
</div>
|
|
237
|
+
<div
|
|
238
|
+
className={cn(
|
|
239
|
+
`min-w-0 grow`,
|
|
240
|
+
activeFillHeight ? `flex flex-col overflow-hidden` : `overflow-y-auto`,
|
|
241
|
+
)}
|
|
242
|
+
>
|
|
243
|
+
{header}
|
|
244
|
+
{tabs.map((tab) => (
|
|
245
|
+
<TabsContent
|
|
246
|
+
key={tab.label}
|
|
247
|
+
value={tabValue(tab)}
|
|
248
|
+
className={tab.fillHeight ? `flex min-h-0 w-full flex-1 flex-col` : ``}
|
|
249
|
+
>
|
|
250
|
+
{tab.content}
|
|
251
|
+
</TabsContent>
|
|
252
|
+
))}
|
|
253
|
+
{children && <div className="flex">{children}</div>}
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
</Tabs>
|
|
257
|
+
) : (
|
|
146
258
|
<div
|
|
147
259
|
className={cn(
|
|
148
|
-
`
|
|
149
|
-
|
|
150
|
-
|
|
260
|
+
`grow`,
|
|
261
|
+
isMobile ? `p-2` : `p-4`,
|
|
262
|
+
activeFillHeight ? `flex flex-col overflow-hidden` : `overflow-y-auto`,
|
|
263
|
+
fullWidth && `p-0`,
|
|
151
264
|
)}
|
|
152
265
|
>
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
266
|
+
<div
|
|
267
|
+
className={cn(
|
|
268
|
+
`mx-auto max-w-6xl space-y-8`,
|
|
269
|
+
activeFillHeight && `flex w-full flex-1 min-h-0 flex-col space-y-0`,
|
|
270
|
+
fullWidth && `max-w-full w-full p-0 h-full`,
|
|
271
|
+
)}
|
|
272
|
+
>
|
|
273
|
+
{header}
|
|
274
|
+
{tabs ? (
|
|
275
|
+
<>
|
|
276
|
+
<Tabs
|
|
277
|
+
value={activeTab}
|
|
278
|
+
className={cn(`w-full`, activeFillHeight && `flex flex-1 min-h-0 flex-col`)}
|
|
279
|
+
onValueChange={handleTabChange}
|
|
280
|
+
>
|
|
281
|
+
{isMobile ? (
|
|
282
|
+
<div className="p-0">
|
|
283
|
+
<Select
|
|
284
|
+
value={activeTab}
|
|
285
|
+
onValueChange={(value) => {
|
|
286
|
+
if (value) handleTabChange(value);
|
|
287
|
+
}}
|
|
288
|
+
>
|
|
289
|
+
<SelectTrigger className="w-full">
|
|
290
|
+
<SelectValue />
|
|
291
|
+
</SelectTrigger>
|
|
292
|
+
<SelectContent>
|
|
293
|
+
{tabs.map((tab) => (
|
|
294
|
+
<SelectItem key={tab.label} value={tabValue(tab)}>
|
|
295
|
+
{tab.contentLabel ?? tab.label}
|
|
296
|
+
</SelectItem>
|
|
297
|
+
))}
|
|
298
|
+
</SelectContent>
|
|
299
|
+
</Select>
|
|
300
|
+
</div>
|
|
301
|
+
) : (
|
|
302
|
+
<div className="p-4">
|
|
303
|
+
<TabsList>
|
|
173
304
|
{tabs.map((tab) => (
|
|
174
|
-
<
|
|
305
|
+
<TabsTrigger key={tab.label} value={tabValue(tab)} className="px-4">
|
|
175
306
|
{tab.contentLabel ?? tab.label}
|
|
176
|
-
</
|
|
307
|
+
</TabsTrigger>
|
|
177
308
|
))}
|
|
178
|
-
</
|
|
179
|
-
</
|
|
180
|
-
</div>
|
|
181
|
-
) : (
|
|
182
|
-
<div className="p-4">
|
|
183
|
-
<TabsList>
|
|
184
|
-
{tabs.map((tab) => (
|
|
185
|
-
<TabsTrigger key={tab.label} value={tab.key?.name ?? tab.label} className="px-4">
|
|
186
|
-
{tab.contentLabel ?? tab.label}
|
|
187
|
-
</TabsTrigger>
|
|
188
|
-
))}
|
|
189
|
-
</TabsList>
|
|
190
|
-
</div>
|
|
191
|
-
)}
|
|
192
|
-
<div
|
|
193
|
-
className={cn(
|
|
194
|
-
`flex w-full `,
|
|
195
|
-
isMobile ? `` : `px-4`,
|
|
196
|
-
activeFillHeight ? `flex-1 min-h-0` : `overflow-y-auto`,
|
|
309
|
+
</TabsList>
|
|
310
|
+
</div>
|
|
197
311
|
)}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
312
|
+
<div
|
|
313
|
+
className={cn(
|
|
314
|
+
`flex w-full `,
|
|
315
|
+
isMobile ? `` : `px-4`,
|
|
316
|
+
activeFillHeight ? `flex-1 min-h-0` : `overflow-y-auto`,
|
|
317
|
+
)}
|
|
318
|
+
>
|
|
319
|
+
{tabs.map((tab) => (
|
|
320
|
+
<TabsContent
|
|
321
|
+
key={tab.label}
|
|
322
|
+
value={tabValue(tab)}
|
|
323
|
+
className={tab.fillHeight ? `flex flex-1 min-h-0 w-full flex-col` : `pb-20`}
|
|
324
|
+
>
|
|
325
|
+
{tab.content}
|
|
326
|
+
</TabsContent>
|
|
327
|
+
))}
|
|
328
|
+
</div>
|
|
329
|
+
</Tabs>
|
|
330
|
+
{children && <div className={cn(`flex`, isMobile ? `px-2` : `px-4`)}>{children}</div>}
|
|
331
|
+
</>
|
|
332
|
+
) : (
|
|
333
|
+
children
|
|
334
|
+
)}
|
|
335
|
+
</div>
|
|
215
336
|
</div>
|
|
216
|
-
|
|
337
|
+
)}
|
|
217
338
|
{details &&
|
|
218
339
|
(isMobile ? (
|
|
219
340
|
<Sheet open={showDetails} onOpenChange={setShowDetails}>
|
|
@@ -13,6 +13,20 @@ export type Tab = {
|
|
|
13
13
|
content: React.ReactNode;
|
|
14
14
|
modules?: ModuleWithPermissions[];
|
|
15
15
|
action?: Action;
|
|
16
|
+
/**
|
|
17
|
+
* Section header this tab clusters under in the `RoundPageContainer`
|
|
18
|
+
* `layout="rail"` navigation. Tabs without a `group` render at the top of the
|
|
19
|
+
* rail (the "pinned" stratum) in declared order; groups render in first-seen
|
|
20
|
+
* order. Ignored by the default `layout="tabs"` (flat) rendering.
|
|
21
|
+
*/
|
|
22
|
+
group?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Stable identifier used for the URL `?section=` value and active-tab
|
|
25
|
+
* matching. Defaults to `key?.name ?? label`. Provide this for tabs that have
|
|
26
|
+
* no `key` (no backing module) but still need a stable, locale-independent
|
|
27
|
+
* deep link — e.g. `"dashboard"`, `"analysis"`, `"timeline"`.
|
|
28
|
+
*/
|
|
29
|
+
sectionKey?: string;
|
|
16
30
|
/**
|
|
17
31
|
* When true, the tab content fills the available viewport height inside its
|
|
18
32
|
* container. The tab's wrapper chain switches to a flex-column layout with
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { partitionTabs } from "../partitionTabs";
|
|
3
|
+
import type { Tab } from "../TabsContainer";
|
|
4
|
+
|
|
5
|
+
const tab = (label: string, group?: string): Tab => ({
|
|
6
|
+
label,
|
|
7
|
+
content: null,
|
|
8
|
+
group,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe("partitionTabs", () => {
|
|
12
|
+
it("returns empty partition for empty input", () => {
|
|
13
|
+
expect(partitionTabs([])).toEqual({ ungrouped: [], groups: [] });
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("keeps tabs without a group in the ungrouped stratum, in declared order", () => {
|
|
17
|
+
const result = partitionTabs([tab("a"), tab("b"), tab("c")]);
|
|
18
|
+
expect(result.ungrouped.map((t) => t.label)).toEqual(["a", "b", "c"]);
|
|
19
|
+
expect(result.groups).toEqual([]);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("clusters tabs by group label", () => {
|
|
23
|
+
const result = partitionTabs([tab("a", "Casebook"), tab("b", "Casebook"), tab("c", "Work")]);
|
|
24
|
+
expect(result.ungrouped).toEqual([]);
|
|
25
|
+
expect(result.groups).toEqual([
|
|
26
|
+
{ label: "Casebook", items: [expect.objectContaining({ label: "a" }), expect.objectContaining({ label: "b" })] },
|
|
27
|
+
{ label: "Work", items: [expect.objectContaining({ label: "c" })] },
|
|
28
|
+
]);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("renders groups in the order they first appear", () => {
|
|
32
|
+
const result = partitionTabs([tab("a", "Work"), tab("b", "Casebook"), tab("c", "Work"), tab("d", "Casebook")]);
|
|
33
|
+
expect(result.groups.map((g) => g.label)).toEqual(["Work", "Casebook"]);
|
|
34
|
+
expect(result.groups[0].items.map((t) => t.label)).toEqual(["a", "c"]);
|
|
35
|
+
expect(result.groups[1].items.map((t) => t.label)).toEqual(["b", "d"]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("preserves ungrouped tabs above grouped tabs regardless of array order", () => {
|
|
39
|
+
const result = partitionTabs([tab("one", "Work"), tab("two"), tab("three", "Work"), tab("four")]);
|
|
40
|
+
expect(result.ungrouped.map((t) => t.label)).toEqual(["two", "four"]);
|
|
41
|
+
expect(result.groups).toHaveLength(1);
|
|
42
|
+
expect(result.groups[0].items.map((t) => t.label)).toEqual(["one", "three"]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("treats an empty-string group as 'no group'", () => {
|
|
46
|
+
const result = partitionTabs([tab("a", ""), tab("b", "Work")]);
|
|
47
|
+
expect(result.ungrouped.map((t) => t.label)).toEqual(["a"]);
|
|
48
|
+
expect(result.groups).toHaveLength(1);
|
|
49
|
+
expect(result.groups[0].label).toBe("Work");
|
|
50
|
+
});
|
|
51
|
+
});
|