@carlonicora/nextjs-jsonapi 6.26.0 → 6.28.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 (41) hide show
  1. package/dist/{BlockNoteEditor-RVDSB6TF.mjs → BlockNoteEditor-2LO34HXR.mjs} +2 -2
  2. package/dist/{BlockNoteEditor-JHXDOCS5.js → BlockNoteEditor-SWDRMNLT.js} +9 -9
  3. package/dist/{BlockNoteEditor-JHXDOCS5.js.map → BlockNoteEditor-SWDRMNLT.js.map} +1 -1
  4. package/dist/billing/index.js +378 -378
  5. package/dist/billing/index.mjs +1 -1
  6. package/dist/{chunk-JE6QWCNE.js → chunk-6RWEYSYE.js} +51 -7
  7. package/dist/chunk-6RWEYSYE.js.map +1 -0
  8. package/dist/{chunk-64REWDSK.mjs → chunk-NV3NNNS6.mjs} +51 -7
  9. package/dist/chunk-NV3NNNS6.mjs.map +1 -0
  10. package/dist/client/index.js +2 -2
  11. package/dist/client/index.mjs +1 -1
  12. package/dist/components/containers/RoundPageContainer.d.ts +3 -1
  13. package/dist/components/containers/RoundPageContainer.d.ts.map +1 -1
  14. package/dist/components/containers/detailsFitsByDefault.d.ts +19 -0
  15. package/dist/components/containers/detailsFitsByDefault.d.ts.map +1 -0
  16. package/dist/components/index.js +2 -2
  17. package/dist/components/index.mjs +1 -1
  18. package/dist/components/typography/DetailField.d.ts +3 -1
  19. package/dist/components/typography/DetailField.d.ts.map +1 -1
  20. package/dist/contexts/index.js +2 -2
  21. package/dist/contexts/index.mjs +1 -1
  22. package/dist/email-automations/index.js +78 -78
  23. package/dist/email-automations/index.mjs +1 -1
  24. package/dist/features/analytics/index.js +107 -107
  25. package/dist/features/analytics/index.mjs +1 -1
  26. package/dist/features/blog/index.js +93 -93
  27. package/dist/features/blog/index.mjs +1 -1
  28. package/dist/features/help/index.js +31 -31
  29. package/dist/features/help/index.mjs +1 -1
  30. package/dist/features/tokenusage/index.js +102 -102
  31. package/dist/features/tokenusage/index.mjs +1 -1
  32. package/package.json +1 -1
  33. package/src/components/containers/RoundPageContainer.tsx +44 -4
  34. package/src/components/containers/__tests__/RoundPageContainer.detailsSurface.spec.tsx +82 -0
  35. package/src/components/containers/__tests__/detailsFitsByDefault.spec.ts +25 -0
  36. package/src/components/containers/detailsFitsByDefault.ts +30 -0
  37. package/src/components/typography/DetailField.tsx +4 -1
  38. package/src/components/typography/__tests__/DetailField.spec.tsx +13 -0
  39. package/dist/chunk-64REWDSK.mjs.map +0 -1
  40. package/dist/chunk-JE6QWCNE.js.map +0 -1
  41. /package/dist/{BlockNoteEditor-RVDSB6TF.mjs.map → BlockNoteEditor-2LO34HXR.mjs.map} +0 -0
@@ -18,6 +18,7 @@ import {
18
18
  TabsTrigger,
19
19
  } from "@/components";
20
20
  import { partitionTabs, Tab } from "@/components/containers";
21
+ import { detailsFitsByDefault } from "@/components/containers/detailsFitsByDefault";
21
22
  import { HEADER_ROW_MIN_H, RoundPageContainerTitle } from "@/components/containers/RoundPageContainerTitle";
22
23
  import { Header, MobileNavigationBar } from "@/components/navigations";
23
24
  import { MicroLabel } from "@/components/typography";
@@ -118,6 +119,8 @@ type RoundPageContainerProps = {
118
119
  * navigation — an "info" icon actively misdescribes a conversation list.
119
120
  */
120
121
  detailsIcon?: ReactNode;
122
+ /** Paints the details panel on the recessed surface so cards inside it read as cards. Default "default" (unchanged). */
123
+ detailsSurface?: "default" | "muted";
121
124
  /**
122
125
  * Which element scrolls the page.
123
126
  * - `"document"` (default) — the shell's height only FLOORS at the viewport
@@ -186,6 +189,7 @@ export function RoundPageContainer({
186
189
  defaultDetailsOpen = false,
187
190
  detailsTitle,
188
191
  detailsIcon,
192
+ detailsSurface = "default",
189
193
  scroll = "fixed",
190
194
  }: RoundPageContainerProps) {
191
195
  const t = useTranslations();
@@ -207,8 +211,13 @@ export function RoundPageContainer({
207
211
  // without one (the customer portal) keeps the padding on both sides.
208
212
  const hasSidebar = useOptionalSidebar() !== null;
209
213
  const [mounted, setMounted] = useState(false);
214
+ // The page card; its inner width decides the details panel's default.
215
+ const cardRef = useRef<HTMLDivElement>(null);
210
216
 
217
+ // Runs once, on the first render after `mounted`: before that the container
218
+ // renders its placeholder and the card (measured below) does not exist yet.
211
219
  useEffect(() => {
220
+ if (!mounted) return;
212
221
  const match = document.cookie.split("; ").find((row) => row.startsWith(`${detailsCookieName}=`));
213
222
  const stored = match?.split("=")[1];
214
223
  // Apply the stored preference in BOTH directions. Previously this only ever
@@ -217,7 +226,22 @@ export function RoundPageContainer({
217
226
  // who explicitly collapsed the panel must stay collapsed on the next load.
218
227
  if (stored === "true") setShowDetailsState(true);
219
228
  else if (stored === "false") setShowDetailsState(false);
220
- }, []);
229
+ // No stored choice yet: open when the page card fits the panel beside the
230
+ // centred content column. The card is measured, so the left sidebar's real
231
+ // state (open, minimised, absent) counts. Decided once, on load — resizing
232
+ // does not move it.
233
+ else if (
234
+ !defaultDetailsOpen &&
235
+ cardRef.current &&
236
+ detailsFitsByDefault({
237
+ cardInnerPx: cardRef.current.clientWidth,
238
+ remPx: parseFloat(getComputedStyle(document.documentElement).fontSize) || 16,
239
+ hasRail: layout === "rail" && !!tabs,
240
+ hasCentredColumn: !fullWidth && (!activeFillHeight || activeConstrainWidth),
241
+ })
242
+ )
243
+ setShowDetailsState(true);
244
+ }, [mounted]);
221
245
 
222
246
  useEffect(() => {
223
247
  setMounted(true);
@@ -433,7 +457,7 @@ export function RoundPageContainer({
433
457
  sibling in this fixed-height flex column. `h-full` resolves to 100% of
434
458
  the wrapper, leaving the bar no room and pushing it below the fold —
435
459
  it then appears only after scrolling to the very end of the page. */}
436
- <div className="bg-background flex min-h-0 w-full flex-1 rounded-lg border p-0">
460
+ <div ref={cardRef} className="bg-background flex min-h-0 w-full flex-1 rounded-lg border p-0">
437
461
  {/* pb-16 under mobileFlow: room for the floating Help button, which
438
462
  otherwise sits over the last lines of the page. */}
439
463
  <div className={cn("flex w-full flex-col", mobileFlow && "min-h-0 overflow-y-auto pb-16")}>
@@ -681,7 +705,15 @@ export function RoundPageContainer({
681
705
  <SheetHeader>
682
706
  <SheetTitle>{detailsTitle ?? t("ui.labels.details")}</SheetTitle>
683
707
  </SheetHeader>
684
- <div className="overflow-y-auto p-6 pt-0">{details}</div>
708
+ <div
709
+ data-slot="details-body"
710
+ className={cn(
711
+ "overflow-y-auto",
712
+ detailsSurface === "muted" ? "bg-muted dark:bg-background p-3" : "p-6 pt-0",
713
+ )}
714
+ >
715
+ {details}
716
+ </div>
685
717
  </SheetContent>
686
718
  </Sheet>
687
719
  ) : (
@@ -718,7 +750,15 @@ export function RoundPageContainer({
718
750
  </div>
719
751
  </div>
720
752
  )}
721
- <div className="min-h-0 flex-1 overflow-y-auto p-4">{details}</div>
753
+ <div
754
+ data-slot="details-body"
755
+ className={cn(
756
+ "min-h-0 flex-1 overflow-y-auto",
757
+ detailsSurface === "muted" ? "bg-muted dark:bg-background p-3" : "p-4",
758
+ )}
759
+ >
760
+ {details}
761
+ </div>
722
762
  </div>
723
763
  ))}
724
764
  </div>
@@ -0,0 +1,82 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { beforeAll, describe, expect, it, vi } from "vitest";
3
+ import { SharedProvider } from "../../../contexts/SharedContext";
4
+ import { configureI18n } from "../../../i18n";
5
+ import { RoundPageContainer } from "../../containers/RoundPageContainer";
6
+
7
+ vi.mock("next/navigation", () => ({
8
+ usePathname: () => "/",
9
+ useRouter: () => ({
10
+ push: vi.fn(),
11
+ replace: vi.fn(),
12
+ prefetch: vi.fn(),
13
+ back: vi.fn(),
14
+ forward: vi.fn(),
15
+ refresh: vi.fn(),
16
+ }),
17
+ useSearchParams: () => new URLSearchParams(),
18
+ useParams: () => ({}),
19
+ redirect: vi.fn(),
20
+ notFound: vi.fn(),
21
+ }));
22
+
23
+ beforeAll(() => {
24
+ configureI18n({
25
+ useRouter: () => ({
26
+ push: vi.fn(),
27
+ replace: vi.fn(),
28
+ back: vi.fn(),
29
+ forward: vi.fn(),
30
+ refresh: vi.fn(),
31
+ prefetch: vi.fn(),
32
+ }),
33
+ useTranslations: () => (key: string) => key,
34
+ usePathname: () => "/",
35
+ Link: ({ href, children, ...rest }: { href: string; children: React.ReactNode; [key: string]: any }) => (
36
+ <a href={href} {...rest}>
37
+ {children}
38
+ </a>
39
+ ),
40
+ });
41
+ });
42
+
43
+ function withShared(node: React.ReactNode) {
44
+ return <SharedProvider value={{ breadcrumbs: [], title: { type: "" } }}>{node}</SharedProvider>;
45
+ }
46
+
47
+ async function detailsBody(): Promise<HTMLElement> {
48
+ const content = await screen.findByText("d");
49
+ const body = content.closest<HTMLElement>('[data-slot="details-body"]');
50
+ if (!body) throw new Error("details body not found");
51
+ return body;
52
+ }
53
+
54
+ describe("RoundPageContainer details surface", () => {
55
+ it("default surface keeps the details body unpainted with p-4", async () => {
56
+ render(
57
+ withShared(
58
+ <RoundPageContainer testId="shell" details={<div>d</div>} defaultDetailsOpen>
59
+ content
60
+ </RoundPageContainer>,
61
+ ),
62
+ );
63
+ const body = await detailsBody();
64
+ expect(body.classList.contains("p-4")).toBe(true);
65
+ expect(body.classList.contains("bg-muted")).toBe(false);
66
+ });
67
+
68
+ it("muted surface paints the details body", async () => {
69
+ render(
70
+ withShared(
71
+ <RoundPageContainer testId="shell" details={<div>d</div>} defaultDetailsOpen detailsSurface="muted">
72
+ content
73
+ </RoundPageContainer>,
74
+ ),
75
+ );
76
+ const body = await detailsBody();
77
+ expect(body.classList.contains("bg-muted")).toBe(true);
78
+ expect(body.classList.contains("dark:bg-background")).toBe(true);
79
+ expect(body.classList.contains("p-3")).toBe(true);
80
+ expect(body.classList.contains("p-4")).toBe(false);
81
+ });
82
+ });
@@ -0,0 +1,25 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { detailsFitsByDefault } from "../detailsFitsByDefault";
3
+
4
+ const base = { remPx: 16, hasRail: false, hasCentredColumn: true };
5
+
6
+ describe("detailsFitsByDefault", () => {
7
+ it("needs column + padding + panel inside the card (1569px)", () => {
8
+ expect(detailsFitsByDefault({ ...base, cardInnerPx: 1569 })).toBe(true);
9
+ expect(detailsFitsByDefault({ ...base, cardInnerPx: 1568 })).toBe(false);
10
+ });
11
+
12
+ it("adds the section rail when the page has one (1793px)", () => {
13
+ expect(detailsFitsByDefault({ ...base, hasRail: true, cardInnerPx: 1793 })).toBe(true);
14
+ expect(detailsFitsByDefault({ ...base, hasRail: true, cardInnerPx: 1792 })).toBe(false);
15
+ });
16
+
17
+ it("scales with the root font size", () => {
18
+ expect(detailsFitsByDefault({ ...base, remPx: 20, cardInnerPx: 1961 })).toBe(true);
19
+ expect(detailsFitsByDefault({ ...base, remPx: 20, cardInnerPx: 1960 })).toBe(false);
20
+ });
21
+
22
+ it("stays closed on pages without a centred column", () => {
23
+ expect(detailsFitsByDefault({ ...base, hasCentredColumn: false, cardInnerPx: 4000 })).toBe(false);
24
+ });
25
+ });
@@ -0,0 +1,30 @@
1
+ // Widths, in rem, of what has to fit INSIDE the page card next to each other.
2
+ // Each value mirrors a class in RoundPageContainer — change them together.
3
+ const RAIL_REM = 14; // section rail `md:w-56`, only on `layout="rail"` pages
4
+ const CONTENT_PADDING_REM = 2; // `p-4` on both sides of the content column
5
+ const CONTENT_COLUMN_REM = 72; // `max-w-6xl`
6
+ const DETAILS_REM = 24; // details panel `w-96`
7
+ const DETAILS_BORDER_PX = 1; // details panel `border-s`
8
+
9
+ /**
10
+ * Whether the details panel fits beside the page's centred content column
11
+ * without squeezing it. Used only as the default when the user has never
12
+ * toggled the panel on this module.
13
+ *
14
+ * `cardInnerPx` is the measured inner width of the page card, so whatever sits
15
+ * outside it — the left sidebar open, minimised to icons or absent, the shell
16
+ * margins — is already accounted for.
17
+ *
18
+ * Pages without a centred column (`fullWidth`, or a full-bleed fill-height tab)
19
+ * have nothing to protect, so they return `false` and keep the closed default.
20
+ */
21
+ export function detailsFitsByDefault(params: {
22
+ cardInnerPx: number;
23
+ remPx: number;
24
+ hasRail: boolean;
25
+ hasCentredColumn: boolean;
26
+ }): boolean {
27
+ if (!params.hasCentredColumn) return false;
28
+ const rem = (params.hasRail ? RAIL_REM : 0) + CONTENT_PADDING_REM + CONTENT_COLUMN_REM + DETAILS_REM;
29
+ return params.cardInnerPx >= rem * params.remPx + DETAILS_BORDER_PX;
30
+ }
@@ -6,6 +6,8 @@ type DetailFieldProps = React.ComponentProps<"div"> & {
6
6
  value?: React.ReactNode;
7
7
  horizontal?: boolean;
8
8
  labelWidth?: string;
9
+ /** Extra classes for the value element (e.g. "min-w-0 flex-1 break-words" so long values wrap in a horizontal row). */
10
+ valueClassName?: string;
9
11
  };
10
12
 
11
13
  function DetailField({
@@ -13,6 +15,7 @@ function DetailField({
13
15
  value,
14
16
  horizontal = false,
15
17
  labelWidth,
18
+ valueClassName,
16
19
  className,
17
20
  children,
18
21
  ...props
@@ -26,7 +29,7 @@ function DetailField({
26
29
  <span className={cn("text-muted-foreground text-xs", horizontal && labelWidth && cn("shrink-0", labelWidth))}>
27
30
  {label}
28
31
  </span>
29
- <div className="text-sm">{value ?? children}</div>
32
+ <div className={cn("text-sm", valueClassName)}>{value ?? children}</div>
30
33
  </div>
31
34
  );
32
35
  }
@@ -0,0 +1,13 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { describe, expect, it } from "vitest";
3
+ import { DetailField } from "../DetailField";
4
+
5
+ describe("DetailField", () => {
6
+ it("valueClassName lands on the value element", () => {
7
+ render(<DetailField horizontal label="L" value="V" valueClassName="min-w-0 flex-1" />);
8
+ const value = screen.getByText("V");
9
+ expect(value.classList.contains("text-sm")).toBe(true);
10
+ expect(value.classList.contains("min-w-0")).toBe(true);
11
+ expect(value.classList.contains("flex-1")).toBe(true);
12
+ });
13
+ });