@carlonicora/nextjs-jsonapi 1.134.0 → 1.135.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{BlockNoteEditor-3IHMNBZ2.mjs → BlockNoteEditor-FAPCNKXZ.mjs} +2 -2
- package/dist/{BlockNoteEditor-2WOKHBLM.js → BlockNoteEditor-PYRHKBF3.js} +9 -9
- package/dist/{BlockNoteEditor-2WOKHBLM.js.map → BlockNoteEditor-PYRHKBF3.js.map} +1 -1
- package/dist/billing/index.js +310 -310
- package/dist/billing/index.js.map +1 -1
- package/dist/billing/index.mjs +2 -2
- package/dist/billing/index.mjs.map +1 -1
- package/dist/{chunk-LBXHYRZH.js → chunk-DNWYHP4V.js} +283 -193
- package/dist/chunk-DNWYHP4V.js.map +1 -0
- package/dist/{chunk-DV5YYI3G.mjs → chunk-EF2HVXOV.mjs} +1698 -1608
- package/dist/chunk-EF2HVXOV.mjs.map +1 -0
- package/dist/client/index.js +2 -2
- package/dist/client/index.mjs +1 -1
- package/dist/components/index.d.mts +12 -1
- package/dist/components/index.d.ts +12 -1
- 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.d.mts +44 -2
- package/dist/contexts/index.d.ts +44 -2
- package/dist/contexts/index.js +6 -2
- package/dist/contexts/index.js.map +1 -1
- package/dist/contexts/index.mjs +5 -1
- package/dist/features/help/index.js +31 -31
- package/dist/features/help/index.mjs +1 -1
- package/package.json +1 -1
- package/src/components/containers/RoundPageContainer.tsx +15 -5
- package/src/components/navigations/MobileNavigationBar.tsx +99 -0
- package/src/components/navigations/__tests__/MobileNavigationBar.spec.tsx +131 -0
- package/src/components/navigations/index.ts +1 -0
- package/src/contexts/MobileNavigationContext.tsx +66 -0
- package/src/contexts/index.ts +1 -0
- package/src/features/assistant/components/parts/AssistantEmptyState.tsx +1 -1
- package/src/features/billing/stripe-customer/components/forms/PaymentMethodForm.tsx +1 -1
- package/src/shadcnui/ui/__tests__/cursor-controls.test.tsx +109 -0
- package/src/shadcnui/ui/__tests__/cursor-menu-items.test.tsx +89 -0
- package/src/shadcnui/ui/button.tsx +1 -1
- package/src/shadcnui/ui/checkbox.tsx +1 -1
- package/src/shadcnui/ui/combobox.tsx +1 -1
- package/src/shadcnui/ui/command.tsx +1 -1
- package/src/shadcnui/ui/context-menu.tsx +4 -4
- package/src/shadcnui/ui/dropdown-menu.tsx +4 -4
- package/src/shadcnui/ui/label.tsx +1 -1
- package/src/shadcnui/ui/radio-group.tsx +1 -1
- package/src/shadcnui/ui/select.tsx +3 -3
- package/src/shadcnui/ui/slider.tsx +3 -2
- package/src/shadcnui/ui/switch.tsx +1 -1
- package/dist/chunk-DV5YYI3G.mjs.map +0 -1
- package/dist/chunk-LBXHYRZH.js.map +0 -1
- /package/dist/{BlockNoteEditor-3IHMNBZ2.mjs.map → BlockNoteEditor-FAPCNKXZ.mjs.map} +0 -0
|
@@ -22,7 +22,7 @@ var _chunkYBZVWLPHjs = require('../../chunk-YBZVWLPH.js');
|
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
|
|
25
|
-
var
|
|
25
|
+
var _chunkDNWYHP4Vjs = require('../../chunk-DNWYHP4V.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,
|
|
78
|
-
|
|
77
|
+
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkDNWYHP4Vjs.Sheet, { open, onOpenChange, children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
78
|
+
_chunkDNWYHP4Vjs.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,
|
|
82
|
+
children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkDNWYHP4Vjs.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 =
|
|
89
|
+
const ctx = _chunkDNWYHP4Vjs.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,
|
|
94
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
95
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
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") })
|
|
96
96
|
] }),
|
|
97
|
-
!showThread ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
97
|
+
!showThread ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkDNWYHP4Vjs.AssistantEmptyState, { onSend: send }) : /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _jsxruntime.Fragment, { children: [
|
|
98
98
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
99
|
-
|
|
99
|
+
_chunkDNWYHP4Vjs.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,
|
|
109
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkDNWYHP4Vjs.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 } =
|
|
119
|
+
const { currentUser } = _chunkDNWYHP4Vjs.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,
|
|
124
|
-
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
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: [
|
|
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,
|
|
128
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkDNWYHP4Vjs.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,
|
|
132
|
+
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkDNWYHP4Vjs.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 } =
|
|
145
|
+
const { currentUser } = _chunkDNWYHP4Vjs.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,
|
|
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") })
|
|
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 =
|
|
203
|
+
const generateUrl = _chunkDNWYHP4Vjs.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 =
|
|
284
|
+
const generateUrl = _chunkDNWYHP4Vjs.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,
|
|
341
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkDNWYHP4Vjs.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 =
|
|
362
|
+
const generateUrl = _chunkDNWYHP4Vjs.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
|
-
|
|
380
|
+
_chunkDNWYHP4Vjs.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,
|
|
389
|
-
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
390
|
-
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
391
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
392
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
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]), () => ( "")) })
|
|
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 =
|
|
424
|
+
const generateUrl = _chunkDNWYHP4Vjs.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
|
-
|
|
435
|
+
_chunkDNWYHP4Vjs.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,
|
|
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,
|
|
445
445
|
_link2.default,
|
|
446
446
|
{
|
|
447
447
|
href: generateUrl({ page: `/help/${article.howToType}/${article.slug}` }),
|
package/package.json
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
} from "@/components";
|
|
20
20
|
import { partitionTabs, Tab } from "@/components/containers";
|
|
21
21
|
import { RoundPageContainerTitle } from "@/components/containers/RoundPageContainerTitle";
|
|
22
|
-
import { Header } from "@/components/navigations";
|
|
22
|
+
import { Header, MobileNavigationBar } from "@/components/navigations";
|
|
23
23
|
import { useHeaderChildren, useHeaderLeftContent, useHeaderLogo, useHeaderMobileChildren } from "@/contexts";
|
|
24
24
|
import { useUrlRewriter } from "@/hooks";
|
|
25
25
|
import { cn, useIsMobile } from "@/index";
|
|
@@ -163,10 +163,15 @@ export function RoundPageContainer({
|
|
|
163
163
|
>
|
|
164
164
|
{headerChildren}
|
|
165
165
|
</Header>
|
|
166
|
-
<div className={cn("flex h-[calc(100vh-3rem)] w-full flex-col", isMobile ? "" : "p-2 pt-0 pl-0")}>
|
|
167
|
-
|
|
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")}>
|
|
167
|
+
{/* `min-h-0 flex-1`, NOT `h-full`: the bar below is an in-flow sibling in
|
|
168
|
+
this fixed-height flex column. `h-full` would resolve to 100% of the
|
|
169
|
+
wrapper, leaving no room and pushing the bar below the fold (visible
|
|
170
|
+
only after scrolling to the end of the page). */}
|
|
171
|
+
<div className={cn("bg-background flex min-h-0 w-full flex-1", isMobile ? "" : "rounded-lg border p-0")}>
|
|
168
172
|
<div className="flex w-full flex-col" />
|
|
169
173
|
</div>
|
|
174
|
+
<MobileNavigationBar />
|
|
170
175
|
</div>
|
|
171
176
|
</>
|
|
172
177
|
);
|
|
@@ -182,8 +187,12 @@ export function RoundPageContainer({
|
|
|
182
187
|
>
|
|
183
188
|
{headerChildren}
|
|
184
189
|
</Header>
|
|
185
|
-
<div className={cn(`flex h-[calc(100vh-3rem)] w-full flex-col`, isMobile ? "p-1 pt-0" : "p-2 pt-0 pl-0")}>
|
|
186
|
-
|
|
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")}>
|
|
191
|
+
{/* `min-h-0 flex-1`, NOT `h-full`: MobileNavigationBar below is an in-flow
|
|
192
|
+
sibling in this fixed-height flex column. `h-full` resolves to 100% of
|
|
193
|
+
the wrapper, leaving the bar no room and pushing it below the fold —
|
|
194
|
+
it then appears only after scrolling to the very end of the page. */}
|
|
195
|
+
<div className="bg-background flex min-h-0 w-full flex-1 rounded-lg border p-0">
|
|
187
196
|
<div className="flex w-full flex-col">
|
|
188
197
|
{(!fullWidth || forceHeader) && (
|
|
189
198
|
<RoundPageContainerTitle
|
|
@@ -409,6 +418,7 @@ export function RoundPageContainer({
|
|
|
409
418
|
</div>
|
|
410
419
|
))}
|
|
411
420
|
</div>
|
|
421
|
+
<MobileNavigationBar />
|
|
412
422
|
</div>
|
|
413
423
|
</>
|
|
414
424
|
);
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { usePathname } from "next/navigation";
|
|
4
|
+
import { useMobileNavigationItems, type MobileNavigationItem } from "../../contexts/MobileNavigationContext";
|
|
5
|
+
import { Link } from "../../shadcnui";
|
|
6
|
+
import { cn } from "../../utils/cn";
|
|
7
|
+
import { useIsMobile } from "../../utils/use-mobile";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Active only for navigational slots.
|
|
11
|
+
*
|
|
12
|
+
* Exact-or-prefix, NOT a bare `startsWith`: the prefix arm keeps a detail route
|
|
13
|
+
* (/accounts/123) highlighting its section (/accounts).
|
|
14
|
+
*
|
|
15
|
+
* "/" is special-cased to EXACT match. Without that, its prefix arm degenerates
|
|
16
|
+
* to `pathname.startsWith("/")`, which is true for every route, and the Home
|
|
17
|
+
* slot highlights everywhere. This is pinned by a unit test in Task C.
|
|
18
|
+
*/
|
|
19
|
+
function isActiveHref(pathname: string, href: string): boolean {
|
|
20
|
+
if (href === "/") return pathname === "/";
|
|
21
|
+
return pathname === href || pathname.startsWith(`${href}/`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const slotClass = cn(
|
|
25
|
+
// min-h-12/min-w-12 = 48px touch target (Material/HIG guidance).
|
|
26
|
+
"relative flex min-h-12 min-w-12 flex-1 items-center justify-center",
|
|
27
|
+
"transition-colors",
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
function SlotContent({ item }: { item: MobileNavigationItem }) {
|
|
31
|
+
if (item.render) return <>{item.render}</>;
|
|
32
|
+
const Icon = item.icon;
|
|
33
|
+
return Icon ? <Icon className="size-6" /> : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The mobile bottom navigation bar.
|
|
38
|
+
*
|
|
39
|
+
* Renders nothing on desktop, and nothing when the application supplied no
|
|
40
|
+
* items — so page containers can render it unconditionally.
|
|
41
|
+
*
|
|
42
|
+
* Rendered as an in-flow flex sibling of the page content (NOT position:fixed),
|
|
43
|
+
* so the content column shrinks to fit and can never be hidden behind the bar.
|
|
44
|
+
*/
|
|
45
|
+
export function MobileNavigationBar() {
|
|
46
|
+
const items = useMobileNavigationItems();
|
|
47
|
+
const isMobile = useIsMobile();
|
|
48
|
+
const pathname = usePathname();
|
|
49
|
+
|
|
50
|
+
if (!isMobile || items.length === 0) return null;
|
|
51
|
+
|
|
52
|
+
// A detached surface sitting BELOW the page card with a gap, so it carries a
|
|
53
|
+
// rounded border rather than a `border-t`, which would read as if the bar were
|
|
54
|
+
// welded to the card's bottom edge. `bg-sidebar` deliberately matches the
|
|
55
|
+
// Header's own background so the two chrome edges of the viewport agree.
|
|
56
|
+
return (
|
|
57
|
+
<nav
|
|
58
|
+
aria-label="Primary"
|
|
59
|
+
data-testid="mobile-navigation-bar"
|
|
60
|
+
className="bg-sidebar flex w-full shrink-0 flex-row items-stretch rounded-lg border pb-[env(safe-area-inset-bottom)]"
|
|
61
|
+
>
|
|
62
|
+
{items.map((item) => {
|
|
63
|
+
const active = item.href ? isActiveHref(pathname, item.href) : false;
|
|
64
|
+
const tone = active ? "text-primary" : "text-muted-foreground";
|
|
65
|
+
|
|
66
|
+
const body = (
|
|
67
|
+
<>
|
|
68
|
+
{active && <span aria-hidden className="bg-primary absolute inset-x-0 top-0 h-0.5" />}
|
|
69
|
+
<SlotContent item={item} />
|
|
70
|
+
</>
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
return item.href ? (
|
|
74
|
+
<Link
|
|
75
|
+
key={item.key}
|
|
76
|
+
href={item.href}
|
|
77
|
+
aria-label={item.label}
|
|
78
|
+
aria-current={active ? "page" : undefined}
|
|
79
|
+
data-testid={`mobile-nav-${item.key}`}
|
|
80
|
+
className={cn(slotClass, tone)}
|
|
81
|
+
>
|
|
82
|
+
{body}
|
|
83
|
+
</Link>
|
|
84
|
+
) : (
|
|
85
|
+
<button
|
|
86
|
+
key={item.key}
|
|
87
|
+
type="button"
|
|
88
|
+
onClick={item.onClick}
|
|
89
|
+
aria-label={item.label}
|
|
90
|
+
data-testid={`mobile-nav-${item.key}`}
|
|
91
|
+
className={cn(slotClass, tone)}
|
|
92
|
+
>
|
|
93
|
+
{body}
|
|
94
|
+
</button>
|
|
95
|
+
);
|
|
96
|
+
})}
|
|
97
|
+
</nav>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
import { HomeIcon, SearchIcon } from "lucide-react";
|
|
4
|
+
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { MobileNavigationProvider, type MobileNavigationItem } from "../../../contexts/MobileNavigationContext";
|
|
6
|
+
import { ViewportProvider } from "../../../contexts/ViewportContext";
|
|
7
|
+
import { configureI18n } from "../../../i18n";
|
|
8
|
+
import { MobileNavigationBar } from "../MobileNavigationBar";
|
|
9
|
+
|
|
10
|
+
// usePathname is globally mocked to "/" in vitest.setup.ts. Override it here so
|
|
11
|
+
// the active-state rule can be exercised against a real route. A spec-level
|
|
12
|
+
// vi.mock takes precedence over the setup-file mock for this module.
|
|
13
|
+
const pathnameRef = { current: "/" };
|
|
14
|
+
vi.mock("next/navigation", () => ({
|
|
15
|
+
usePathname: () => pathnameRef.current,
|
|
16
|
+
useRouter: () => ({
|
|
17
|
+
push: vi.fn(),
|
|
18
|
+
replace: vi.fn(),
|
|
19
|
+
prefetch: vi.fn(),
|
|
20
|
+
back: vi.fn(),
|
|
21
|
+
forward: vi.fn(),
|
|
22
|
+
refresh: vi.fn(),
|
|
23
|
+
}),
|
|
24
|
+
useSearchParams: () => new URLSearchParams(),
|
|
25
|
+
useParams: () => ({}),
|
|
26
|
+
redirect: vi.fn(),
|
|
27
|
+
notFound: vi.fn(),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
beforeAll(() => {
|
|
31
|
+
// The package Link resolves its inner component at runtime and throws if i18n
|
|
32
|
+
// was never configured (src/i18n/config.ts:53-58).
|
|
33
|
+
configureI18n({
|
|
34
|
+
useRouter: () => ({
|
|
35
|
+
push: vi.fn(),
|
|
36
|
+
replace: vi.fn(),
|
|
37
|
+
back: vi.fn(),
|
|
38
|
+
forward: vi.fn(),
|
|
39
|
+
refresh: vi.fn(),
|
|
40
|
+
prefetch: vi.fn(),
|
|
41
|
+
}),
|
|
42
|
+
useTranslations: () => (key: string) => key,
|
|
43
|
+
usePathname: () => pathnameRef.current,
|
|
44
|
+
Link: ({ href, children, ...rest }: { href: string; children: React.ReactNode; [key: string]: any }) => (
|
|
45
|
+
<a href={href} {...rest}>
|
|
46
|
+
{children}
|
|
47
|
+
</a>
|
|
48
|
+
),
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
pathnameRef.current = "/";
|
|
54
|
+
// jsdom defaults to 1024; ViewportProvider's effect measures immediately and
|
|
55
|
+
// would otherwise overwrite the mobile seed with "desktop".
|
|
56
|
+
Object.defineProperty(window, "innerWidth", { writable: true, configurable: true, value: 375 });
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
function renderBar(items: MobileNavigationItem[], { mobile = true }: { mobile?: boolean } = {}) {
|
|
60
|
+
if (!mobile) {
|
|
61
|
+
Object.defineProperty(window, "innerWidth", { writable: true, configurable: true, value: 1024 });
|
|
62
|
+
}
|
|
63
|
+
return render(
|
|
64
|
+
<ViewportProvider initialIsMobile={mobile}>
|
|
65
|
+
<MobileNavigationProvider items={items}>
|
|
66
|
+
<MobileNavigationBar />
|
|
67
|
+
</MobileNavigationProvider>
|
|
68
|
+
</ViewportProvider>,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const searchItem: MobileNavigationItem = { key: "search", label: "Cerca", icon: SearchIcon, onClick: vi.fn() };
|
|
73
|
+
const homeItem: MobileNavigationItem = { key: "home", label: "Dashboard", icon: HomeIcon, href: "/" };
|
|
74
|
+
|
|
75
|
+
describe("MobileNavigationBar", () => {
|
|
76
|
+
it("renders nothing when the application supplied no items", () => {
|
|
77
|
+
renderBar([]);
|
|
78
|
+
expect(screen.queryByTestId("mobile-navigation-bar")).not.toBeInTheDocument();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("renders nothing on desktop even when items exist", () => {
|
|
82
|
+
renderBar([searchItem], { mobile: false });
|
|
83
|
+
expect(screen.queryByTestId("mobile-navigation-bar")).not.toBeInTheDocument();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("renders one slot per item, labelled by its accessible name", () => {
|
|
87
|
+
renderBar([searchItem, homeItem]);
|
|
88
|
+
expect(screen.getByTestId("mobile-navigation-bar")).toBeInTheDocument();
|
|
89
|
+
expect(screen.getByRole("button", { name: "Cerca" })).toBeInTheDocument();
|
|
90
|
+
expect(screen.getByRole("link", { name: "Dashboard" })).toBeInTheDocument();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("does not render the label as visible text (icons-only bar)", () => {
|
|
94
|
+
renderBar([searchItem]);
|
|
95
|
+
expect(screen.queryByText("Cerca")).not.toBeInTheDocument();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("fires onClick for action slots", async () => {
|
|
99
|
+
const onClick = vi.fn();
|
|
100
|
+
renderBar([{ ...searchItem, onClick }]);
|
|
101
|
+
await userEvent.click(screen.getByRole("button", { name: "Cerca" }));
|
|
102
|
+
expect(onClick).toHaveBeenCalledOnce();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("renders the `render` escape hatch instead of an icon", () => {
|
|
106
|
+
renderBar([
|
|
107
|
+
{ key: "profile", label: "Impostazioni", render: <span data-testid="avatar-stub" />, href: "/settings" },
|
|
108
|
+
]);
|
|
109
|
+
expect(screen.getByTestId("avatar-stub")).toBeInTheDocument();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// The load-bearing case: a bare startsWith would make href="/" match everything.
|
|
113
|
+
it("marks only the exactly-matching slot active when one href is '/'", () => {
|
|
114
|
+
pathnameRef.current = "/conversations";
|
|
115
|
+
renderBar([homeItem, { key: "ai", label: "Chiedi all'AI", icon: SearchIcon, href: "/conversations" }]);
|
|
116
|
+
expect(screen.getByRole("link", { name: "Dashboard" })).not.toHaveAttribute("aria-current");
|
|
117
|
+
expect(screen.getByRole("link", { name: "Chiedi all'AI" })).toHaveAttribute("aria-current", "page");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("keeps a section active on its detail route", () => {
|
|
121
|
+
pathnameRef.current = "/conversations/abc-123";
|
|
122
|
+
renderBar([{ key: "ai", label: "Chiedi all'AI", icon: SearchIcon, href: "/conversations" }]);
|
|
123
|
+
expect(screen.getByRole("link", { name: "Chiedi all'AI" })).toHaveAttribute("aria-current", "page");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("never marks an action slot active", () => {
|
|
127
|
+
pathnameRef.current = "/";
|
|
128
|
+
renderBar([searchItem]);
|
|
129
|
+
expect(screen.getByRole("button", { name: "Cerca" })).not.toHaveAttribute("aria-current");
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import type { ReactNode } from "react";
|
|
5
|
+
import type { LucideIcon } from "lucide-react";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* One slot in the mobile bottom navigation bar.
|
|
9
|
+
*
|
|
10
|
+
* Supply exactly one of `icon` / `render`, and exactly one of `href` / `onClick`.
|
|
11
|
+
* This is a review-time contract, not a runtime-validated one — the mistake is
|
|
12
|
+
* immediately visible in dev, and validation code would ship to every consumer.
|
|
13
|
+
*
|
|
14
|
+
* `render` content MUST NOT contain an interactive element: an `href` slot is
|
|
15
|
+
* already wrapped in a <Link>, so a nested <a>/<button> is invalid HTML and
|
|
16
|
+
* triggers a hydration error.
|
|
17
|
+
*/
|
|
18
|
+
export type MobileNavigationItem = {
|
|
19
|
+
/** Stable unique key. Used as the React key and the `data-testid` suffix. */
|
|
20
|
+
key: string;
|
|
21
|
+
/** Accessible name. The bar is icons-only, so this is never rendered as text. */
|
|
22
|
+
label: string;
|
|
23
|
+
/** Rendered at 24px. Mutually exclusive with `render`. */
|
|
24
|
+
icon?: LucideIcon;
|
|
25
|
+
/** Escape hatch for slots an icon cannot express (logo mark, user avatar). */
|
|
26
|
+
render?: ReactNode;
|
|
27
|
+
/** Navigational slot — receives the active highlight. */
|
|
28
|
+
href?: string;
|
|
29
|
+
/** Action slot — never highlighted. */
|
|
30
|
+
onClick?: () => void;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A plain module-scope context, mirroring the sibling `HeaderChildrenContext`.
|
|
35
|
+
*
|
|
36
|
+
* The `globalThis`-Symbol lazy-creation dance in `utils/use-mobile.tsx` is NOT
|
|
37
|
+
* needed here: that exists for the `core` entry, which is deliberately excluded
|
|
38
|
+
* from tsup's `clientEntries` and is reachable from server code. Provider (the
|
|
39
|
+
* `contexts` entry) and consumer (the `components` entry) share one context
|
|
40
|
+
* object, exactly as `HeaderChildrenContext` already does in production.
|
|
41
|
+
*/
|
|
42
|
+
const MobileNavigationContext = React.createContext<MobileNavigationItem[]>([]);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Supplies the items rendered by `MobileNavigationBar`.
|
|
46
|
+
*
|
|
47
|
+
* Unlike `useSharedContext`, the hook below does NOT throw without a provider —
|
|
48
|
+
* it returns `[]` and the bar renders nothing. Apps that never install this
|
|
49
|
+
* provider are completely unaffected.
|
|
50
|
+
*
|
|
51
|
+
* MUST be rendered inside `ViewportProvider` so the bar's `useIsMobile()` reads
|
|
52
|
+
* the server-seeded viewport rather than flashing.
|
|
53
|
+
*/
|
|
54
|
+
export function MobileNavigationProvider({ children, items }: { children: ReactNode; items: MobileNavigationItem[] }) {
|
|
55
|
+
// Items are rebuilt on every render of the app-side provider (it depends on
|
|
56
|
+
// hooks); memoising on the array identity keeps consumers from re-rendering
|
|
57
|
+
// more than the parent already does.
|
|
58
|
+
const value = React.useMemo(() => items, [items]);
|
|
59
|
+
|
|
60
|
+
return <MobileNavigationContext.Provider value={value}>{children}</MobileNavigationContext.Provider>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Returns the application-supplied bottom-navigation items, or `[]` when none. */
|
|
64
|
+
export function useMobileNavigationItems(): MobileNavigationItem[] {
|
|
65
|
+
return React.useContext(MobileNavigationContext);
|
|
66
|
+
}
|
package/src/contexts/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ export * from "./CommonContext";
|
|
|
10
10
|
export * from "./HeaderChildrenContext";
|
|
11
11
|
export * from "./HeaderLeftContentContext";
|
|
12
12
|
export * from "./HeaderLogoContext";
|
|
13
|
+
export * from "./MobileNavigationContext";
|
|
13
14
|
export * from "./SharedContext";
|
|
14
15
|
export * from "./SocketContext";
|
|
15
16
|
export * from "./ViewportContext";
|
|
@@ -43,7 +43,7 @@ export function AssistantEmptyState({ onSend, operatorMode = false, onOperatorMo
|
|
|
43
43
|
checked={operatorMode}
|
|
44
44
|
onCheckedChange={(checked) => onOperatorModeChange(checked === true)}
|
|
45
45
|
/>
|
|
46
|
-
<Label htmlFor="assistant-operator-mode" className="text-muted-foreground text-xs">
|
|
46
|
+
<Label htmlFor="assistant-operator-mode" className="text-muted-foreground cursor-pointer text-xs">
|
|
47
47
|
{t("features.assistant.operator_mode")}
|
|
48
48
|
</Label>
|
|
49
49
|
</div>
|
|
@@ -132,7 +132,7 @@ export function PaymentMethodForm({ onSuccess, onCancel, isLoading = false }: Pa
|
|
|
132
132
|
{/* Set as Default Checkbox */}
|
|
133
133
|
<div className="flex items-center gap-x-2">
|
|
134
134
|
<Checkbox id="setAsDefault" checked={setAsDefault} onCheckedChange={(checked) => setSetAsDefault(!!checked)} />
|
|
135
|
-
<Label htmlFor="setAsDefault" className="font-normal">
|
|
135
|
+
<Label htmlFor="setAsDefault" className="cursor-pointer font-normal">
|
|
136
136
|
Set as default payment method
|
|
137
137
|
</Label>
|
|
138
138
|
</div>
|