@carlonicora/nextjs-jsonapi 3.13.0 → 3.14.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.
Files changed (33) hide show
  1. package/dist/{BlockNoteEditor-SERY337S.mjs → BlockNoteEditor-QG5OWH5K.mjs} +2 -2
  2. package/dist/{BlockNoteEditor-CFVSHS46.js → BlockNoteEditor-WP4K423H.js} +9 -9
  3. package/dist/{BlockNoteEditor-CFVSHS46.js.map → BlockNoteEditor-WP4K423H.js.map} +1 -1
  4. package/dist/billing/index.js +335 -335
  5. package/dist/billing/index.mjs +1 -1
  6. package/dist/{chunk-UQXE3SME.mjs → chunk-46BRB36R.mjs} +1543 -1453
  7. package/dist/chunk-46BRB36R.mjs.map +1 -0
  8. package/dist/{chunk-LVE3BJ6K.js → chunk-OH6GJY4B.js} +259 -169
  9. package/dist/chunk-OH6GJY4B.js.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 +27 -15
  13. package/dist/components/index.d.ts +27 -15
  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/dist/features/tokenusage/index.js +75 -75
  22. package/dist/features/tokenusage/index.mjs +1 -1
  23. package/package.json +2 -1
  24. package/src/components/containers/MermaidDiagram.tsx +117 -0
  25. package/src/components/containers/ReactMarkdownContainer.tsx +41 -8
  26. package/src/components/containers/index.ts +1 -0
  27. package/src/features/__tests__/i18n-keys.contract.spec.ts +131 -0
  28. package/src/features/billing/i18n-keys.ts +15 -0
  29. package/src/features/handbook/components/details/HandbookPageDetails.tsx +26 -1
  30. package/src/features/handbook/i18n-keys.ts +1 -0
  31. package/dist/chunk-LVE3BJ6K.js.map +0 -1
  32. package/dist/chunk-UQXE3SME.mjs.map +0 -1
  33. /package/dist/{BlockNoteEditor-SERY337S.mjs.map → BlockNoteEditor-QG5OWH5K.mjs.map} +0 -0
@@ -0,0 +1,117 @@
1
+ "use client";
2
+
3
+ import { useTheme } from "next-themes";
4
+ import { useEffect, useId, useState } from "react";
5
+
6
+ type MermaidDiagramProps = {
7
+ /** The body of a ```mermaid fence, verbatim. */
8
+ chart: string;
9
+ };
10
+
11
+ /**
12
+ * One Mermaid diagram, rendered in the browser.
13
+ *
14
+ * Four decisions worth keeping:
15
+ *
16
+ * 1. **`mermaid` is imported dynamically, inside the effect.** The library is
17
+ * roughly half a megabyte gzipped. A static import at module scope would put
18
+ * it in the bundle of every page that renders any markdown at all; this way
19
+ * only a page that actually holds a diagram fetches it.
20
+ * 2. **The id comes from `useId`.** Mermaid keys its internal definitions by the
21
+ * id it is handed, and two diagrams sharing one silently blank the second.
22
+ * 3. **A parse failure falls back to the source.** Mermaid throws on malformed
23
+ * input and, worse, injects its own error graphic into the document when it
24
+ * does — `suppressErrorRendering` turns that off, and the caller renders the
25
+ * fenced block it would have shown anyway. A bad diagram must never cost the
26
+ * reader the rest of the page.
27
+ * 4. **The theme is read, not assumed.** `resolvedTheme` resolves "system" to a
28
+ * real value; re-running on a change is what keeps a diagram from staying
29
+ * black-on-black after the mode flips. Mirrors `TokenUsageTimelineChart`.
30
+ */
31
+ export function MermaidDiagram({ chart }: MermaidDiagramProps) {
32
+ const reactId = useId();
33
+ const { resolvedTheme } = useTheme();
34
+ const [svg, setSvg] = useState<string | undefined>(undefined);
35
+ const [failed, setFailed] = useState(false);
36
+
37
+ useEffect(() => {
38
+ let cancelled = false;
39
+
40
+ const draw = async () => {
41
+ try {
42
+ const mermaid = (await import("mermaid")).default;
43
+
44
+ mermaid.initialize({
45
+ startOnLoad: false,
46
+ securityLevel: "strict",
47
+ suppressErrorRendering: true,
48
+ theme: resolvedTheme === "dark" ? "dark" : "default",
49
+
50
+ // `useMaxWidth: false` on every diagram type this documentation uses.
51
+ //
52
+ // By default Mermaid emits `width="100%"` with a viewBox, so the
53
+ // drawing scales to whatever box it lands in — inside a reading
54
+ // column that shrinks a seven-node graph until its labels are
55
+ // unreadable, and no CSS `max-width` can undo it because the scaling
56
+ // comes from the viewBox, not the width limit. With it off the SVG
57
+ // carries its intrinsic width and the wrapper scrolls to it, the same
58
+ // bargain a wide table makes.
59
+ flowchart: { useMaxWidth: false },
60
+ sequence: { useMaxWidth: false },
61
+ class: { useMaxWidth: false },
62
+ state: { useMaxWidth: false },
63
+ er: { useMaxWidth: false },
64
+ journey: { useMaxWidth: false },
65
+ gantt: { useMaxWidth: false },
66
+ pie: { useMaxWidth: false },
67
+ });
68
+
69
+ // Colons and other punctuation React puts in a useId are not legal in
70
+ // the DOM id Mermaid derives from this one.
71
+ const id = `mermaid-${reactId.replace(/[^a-zA-Z0-9-]/g, "")}`;
72
+ const result = await mermaid.render(id, chart);
73
+
74
+ if (cancelled) return;
75
+ setSvg(result.svg);
76
+ setFailed(false);
77
+ } catch {
78
+ if (cancelled) return;
79
+ setFailed(true);
80
+ setSvg(undefined);
81
+ }
82
+ };
83
+
84
+ void draw();
85
+
86
+ return () => {
87
+ cancelled = true;
88
+ };
89
+ }, [chart, reactId, resolvedTheme]);
90
+
91
+ // Before the first render resolves, and after a failure, show the source. It
92
+ // is what the page displayed before diagrams were drawn at all, so the reader
93
+ // never ends up with less than they had.
94
+ if (failed || svg === undefined)
95
+ return (
96
+ // rtl-ok: deliberate LTR island (diagram source)
97
+ <pre dir="ltr" className="overflow-x-auto">
98
+ <code dir="ltr">{chart}</code>
99
+ </pre>
100
+ );
101
+
102
+ return (
103
+ <div
104
+ // Mermaid emits a complete <svg> document. It is generated from the
105
+ // repository's own documentation, and `securityLevel: "strict"` has
106
+ // already stripped script and foreign content from it.
107
+ // `!max-w-none`, not `max-w-full`. Mermaid writes an inline
108
+ // `max-width` on the SVG, and honouring it inside a reading column
109
+ // squeezes a wide graph until its labels are unreadable — the diagram is
110
+ // technically present and practically useless. An `!important` rule is
111
+ // what beats an inline style, so the graph keeps its natural size and the
112
+ // wrapper scrolls to it, exactly as a wide table does.
113
+ className="my-4 overflow-x-auto [&_svg]:mx-auto [&_svg]:h-auto [&_svg]:!max-w-none"
114
+ dangerouslySetInnerHTML={{ __html: svg }}
115
+ />
116
+ );
117
+ }
@@ -8,6 +8,29 @@ import rehypeSlug from "rehype-slug";
8
8
  import remarkGfm from "remark-gfm";
9
9
 
10
10
  import { cn } from "../../utils/cn";
11
+ import { MermaidDiagram } from "./MermaidDiagram";
12
+
13
+ /**
14
+ * The source of a ```mermaid fence, or `undefined` for any other `pre`.
15
+ *
16
+ * Reads react-markdown's hast node rather than the rendered children: the
17
+ * language lives in the inner `code` element's `language-*` class, and the
18
+ * source is its raw text. Pulling it off the React children would mean
19
+ * unpicking elements that the `code` override has already transformed.
20
+ */
21
+ function mermaidSourceOf(node: unknown): string | undefined {
22
+ const pre = node as { children?: { tagName?: string; properties?: { className?: unknown }; children?: unknown[] }[] };
23
+ const code = pre?.children?.[0];
24
+ if (code?.tagName !== "code") return undefined;
25
+
26
+ const classNames = Array.isArray(code.properties?.className) ? (code.properties.className as string[]) : [];
27
+ if (!classNames.includes("language-mermaid")) return undefined;
28
+
29
+ const text = code.children?.[0] as { type?: string; value?: string } | undefined;
30
+ if (text?.type !== "text" || typeof text.value !== "string") return undefined;
31
+
32
+ return text.value;
33
+ }
11
34
 
12
35
  type ReactMarkdownContainerProps = {
13
36
  content: string;
@@ -100,15 +123,25 @@ export function ReactMarkdownContainer({
100
123
  // left-to-right in every locale, so both the fenced block and the
101
124
  // inline span are deliberate LTR islands. `node` is react-markdown's
102
125
  // hast node — it must not reach the DOM element.
103
- // A fenced block scrolls on its own rather than widening the
104
- // page: a long command line is unbreakable, and without this the
105
- // whole layout is pushed sideways to accommodate it.
126
+ // A ```mermaid fence is a DIAGRAM, not code. It is caught here on
127
+ // the `pre` rather than on the `code` inside it, because the
128
+ // diagram replaces the whole block — catching it a level down
129
+ // would leave it wrapped in the `pre` chrome it is not.
130
+ //
131
+ // A fenced block otherwise scrolls on its own rather than
132
+ // widening the page: a long command line is unbreakable, and
133
+ // without this the whole layout is pushed sideways.
106
134
  // rtl-ok: deliberate LTR island (code)
107
- pre: ({ children, node: _node, ...props }) => (
108
- <pre dir="ltr" {...props} className="overflow-x-auto">
109
- {children}
110
- </pre>
111
- ),
135
+ pre: ({ children, node, ...props }) => {
136
+ const diagram = mermaidSourceOf(node);
137
+ if (diagram !== undefined) return <MermaidDiagram chart={diagram} />;
138
+
139
+ return (
140
+ <pre dir="ltr" {...props} className="overflow-x-auto">
141
+ {children}
142
+ </pre>
143
+ );
144
+ },
112
145
  // No wrap rule HERE. The same component renders inline spans, the
113
146
  // contents of fenced blocks and the contents of table cells; the
114
147
  // last two live in their own scrollers, and breaking a path
@@ -1,5 +1,6 @@
1
1
  export * from "./PageContainer";
2
2
  export * from "./partitionTabs";
3
+ export * from "./MermaidDiagram";
3
4
  export * from "./ReactMarkdownContainer";
4
5
  export * from "./RoundPageContainer";
5
6
  export * from "./RoundPageContainerTitle";
@@ -0,0 +1,131 @@
1
+ import { readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { dirname, join, relative } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { describe, expect, it } from "vitest";
5
+
6
+ /**
7
+ * Every `features/<feature>/i18n-keys.ts` declares the translation keys this
8
+ * package will ask a consuming app to define. Nothing imported those lists, so
9
+ * they were documentation that happened to compile: a component could add a
10
+ * `t("…")` call without touching the contract, and the omission only surfaced
11
+ * in whichever app rendered that branch first. `handbook.notFound` and the six
12
+ * `billing.tokens.*` keys reached several apps that way.
13
+ *
14
+ * These tests read the lists and the sources and compare them, in both
15
+ * directions, so the two cannot drift apart silently again.
16
+ */
17
+
18
+ const FEATURES_DIR = join(dirname(fileURLToPath(import.meta.url)), "..");
19
+ const SRC_DIR = join(FEATURES_DIR, "..");
20
+
21
+ /**
22
+ * Keys a contract promises that no component in this package reads — the app
23
+ * owns the surface that renders them (administration landing cards, the help
24
+ * landing page, handbook screens an app builds itself). They are legitimate
25
+ * contract entries, so they are listed rather than deleted, but the list is
26
+ * closed: a NEW unread key fails the second test until someone either wires it
27
+ * up, deletes it, or justifies it here.
28
+ */
29
+ const DECLARED_FOR_APP_RENDERED_SURFACES = new Set<string>([
30
+ "administration.billing.description",
31
+ "administration.companies.description",
32
+ "administration.group.platform",
33
+ "administration.rbac.description",
34
+ "administration.token_usage.description",
35
+ "administration.users.description",
36
+ "administration.ai_connections.description",
37
+ "handbook.chat.delete_confirm",
38
+ "handbook.chat.rename_placeholder",
39
+ "handbook.contents.title",
40
+ "handbook.notConfigured",
41
+ "handbook.subtitle",
42
+ "help.article.related",
43
+ "help.header.search",
44
+ "help.landing.browseByMode",
45
+ "help.landing.featuredTutorials",
46
+ "help.landing.heading",
47
+ "help.landing.subheading",
48
+ "help.modeIndex.empty",
49
+ "token_usage.series.other",
50
+ "token_usage.timeline.empty",
51
+ ]);
52
+
53
+ /** `t("key")`, `t('key')`, `t(`key`)` and their `t.rich(…)` equivalents. */
54
+ const STATIC_KEY = /\bt(?:\.rich)?\(\s*(?:"([^"${]+)"|'([^'${]+)'|`([^`${]+)`)/g;
55
+ /** ``t(`prefix.${expr}`)`` — only the literal head is knowable. */
56
+ const DYNAMIC_PREFIX = /\bt(?:\.rich)?\(\s*`([^`]*?)\$\{/g;
57
+
58
+ function sourceFiles(dir: string, found: string[] = []): string[] {
59
+ for (const entry of readdirSync(dir)) {
60
+ const path = join(dir, entry);
61
+ if (statSync(path).isDirectory()) {
62
+ if (entry !== "__tests__") sourceFiles(path, found);
63
+ } else if (/\.tsx?$/.test(entry) && !/\.(test|spec)\.tsx?$/.test(entry)) {
64
+ found.push(path);
65
+ }
66
+ }
67
+ return found;
68
+ }
69
+
70
+ /** Where each key is read, so a failure names the file instead of just the key. */
71
+ const readKeys = new Map<string, string>();
72
+ const dynamicPrefixes = new Set<string>();
73
+ for (const file of sourceFiles(SRC_DIR)) {
74
+ const source = readFileSync(file, "utf8");
75
+ for (const match of source.matchAll(STATIC_KEY)) {
76
+ const key = (match[1] ?? match[2] ?? match[3])!;
77
+ if (!readKeys.has(key)) readKeys.set(key, relative(SRC_DIR, file));
78
+ }
79
+ for (const match of source.matchAll(DYNAMIC_PREFIX)) {
80
+ if (match[1]) dynamicPrefixes.add(match[1]);
81
+ }
82
+ }
83
+
84
+ const contracts = new Map<string, string[]>();
85
+ for (const feature of readdirSync(FEATURES_DIR)) {
86
+ const path = join(FEATURES_DIR, feature, "i18n-keys.ts");
87
+ let source: string;
88
+ try {
89
+ source = readFileSync(path, "utf8");
90
+ } catch {
91
+ continue;
92
+ }
93
+ contracts.set(
94
+ feature,
95
+ [...source.matchAll(/"([^"]+)"/g)].map((match) => match[1]!),
96
+ );
97
+ }
98
+
99
+ /**
100
+ * Declared keys pooled across features: namespaces are shared (ai-connection
101
+ * reads `administration.*`), so ownership is per namespace, not per directory.
102
+ */
103
+ const declared = new Set([...contracts.values()].flat());
104
+ const declaredNamespaces = new Set([...declared].map((key) => key.split(".")[0]));
105
+
106
+ describe("i18n key contracts", () => {
107
+ it("finds a contract to check", () => {
108
+ expect(contracts.size).toBeGreaterThan(0);
109
+ expect(readKeys.size).toBeGreaterThan(0);
110
+ });
111
+
112
+ it("declares every key the package reads in a contracted namespace", () => {
113
+ const undeclared = [...readKeys.entries()]
114
+ .filter(([key]) => declaredNamespaces.has(key.split(".")[0]!) && !declared.has(key))
115
+ .map(([key, file]) => `${key} (read in ${file})`)
116
+ .sort();
117
+
118
+ expect(undeclared).toEqual([]);
119
+ });
120
+
121
+ it.each([...contracts.keys()])("has no unread keys in the %s contract", (feature) => {
122
+ const isRead = (key: string) => readKeys.has(key) || [...dynamicPrefixes].some((prefix) => key.startsWith(prefix));
123
+
124
+ const unread = contracts
125
+ .get(feature)!
126
+ .filter((key) => !isRead(key) && !DECLARED_FOR_APP_RENDERED_SURFACES.has(key))
127
+ .sort();
128
+
129
+ expect(unread).toEqual([]);
130
+ });
131
+ });
@@ -93,3 +93,18 @@ export const BILLING_ADMIN_I18N_KEYS = [
93
93
  "billing.admin.prices.restore.description",
94
94
  "billing.admin.prices.restore.confirm",
95
95
  ] as const;
96
+
97
+ /**
98
+ * The `billing.tokens.*` namespace, read by the token status indicator rather
99
+ * than by any administrative surface — which is why it is a second export
100
+ * instead of an addition to the list above, whose contract is scoped to the
101
+ * admin screens.
102
+ */
103
+ export const BILLING_TOKENS_I18N_KEYS = [
104
+ "billing.tokens.status",
105
+ "billing.tokens.monthly",
106
+ "billing.tokens.available",
107
+ "billing.tokens.available_extra",
108
+ "billing.tokens.purchase_extra",
109
+ "billing.tokens.upgrade_plan",
110
+ ] as const;
@@ -22,6 +22,31 @@ type HandbookPageDetailsProps = {
22
22
  * to the English file. This is the detail read, which is the only read that
23
23
  * carries `displayContent` at all.
24
24
  */
25
+ /**
26
+ * The reading column's type scale.
27
+ *
28
+ * `ReactMarkdownContainer`'s `size="normal"` puts NO size class on `p` or `li`,
29
+ * so they inherit — and the app's root is 14px while the browser default the
30
+ * container's own headings are sized against is 16px. The body therefore
31
+ * rendered a step larger than every other piece of body copy in the product,
32
+ * which role 7 of the typography system puts at `text-sm`.
33
+ *
34
+ * `text-sm` on the wrapper fixes the inherited elements by cascade. The heading
35
+ * ladder has explicit classes and cannot cascade, so it is restated one step
36
+ * down — a `text-3xl` H1 belongs to a page that has no header above it, and
37
+ * this one is already titled twice over by the reader's header and breadcrumb.
38
+ *
39
+ * Scoped HERE rather than in the shared container on purpose: 23 other surfaces
40
+ * render markdown through it — judgements, law articles, documents, legal
41
+ * research reports — and changing its default would restyle all of them
42
+ * silently. That alignment is worth making, but as its own change.
43
+ */
44
+ const READING_SCALE = "text-sm [&_h1]:text-2xl [&_h2]:text-xl [&_h3]:text-base [&_h4]:text-sm [&_table]:text-sm";
45
+
25
46
  export function HandbookPageDetails({ handbookPage }: HandbookPageDetailsProps) {
26
- return <ReactMarkdownContainer content={handbookPageContent(handbookPage)} />;
47
+ return (
48
+ <div className={READING_SCALE}>
49
+ <ReactMarkdownContainer content={handbookPageContent(handbookPage)} />
50
+ </div>
51
+ );
27
52
  }
@@ -17,6 +17,7 @@ export const handbookI18nKeys = [
17
17
  "handbook.syncFailed",
18
18
  "handbook.empty",
19
19
  "handbook.notConfigured",
20
+ "handbook.notFound",
20
21
  "handbook.chat.title",
21
22
  "handbook.chat.placeholder",
22
23
  "handbook.chat.ask",