@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.
- package/dist/{BlockNoteEditor-SERY337S.mjs → BlockNoteEditor-QG5OWH5K.mjs} +2 -2
- package/dist/{BlockNoteEditor-CFVSHS46.js → BlockNoteEditor-WP4K423H.js} +9 -9
- package/dist/{BlockNoteEditor-CFVSHS46.js.map → BlockNoteEditor-WP4K423H.js.map} +1 -1
- package/dist/billing/index.js +335 -335
- package/dist/billing/index.mjs +1 -1
- package/dist/{chunk-UQXE3SME.mjs → chunk-46BRB36R.mjs} +1543 -1453
- package/dist/chunk-46BRB36R.mjs.map +1 -0
- package/dist/{chunk-LVE3BJ6K.js → chunk-OH6GJY4B.js} +259 -169
- package/dist/chunk-OH6GJY4B.js.map +1 -0
- package/dist/client/index.js +2 -2
- package/dist/client/index.mjs +1 -1
- package/dist/components/index.d.mts +27 -15
- package/dist/components/index.d.ts +27 -15
- 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 +31 -31
- package/dist/features/help/index.mjs +1 -1
- package/dist/features/tokenusage/index.js +75 -75
- package/dist/features/tokenusage/index.mjs +1 -1
- package/package.json +2 -1
- package/src/components/containers/MermaidDiagram.tsx +117 -0
- package/src/components/containers/ReactMarkdownContainer.tsx +41 -8
- package/src/components/containers/index.ts +1 -0
- package/src/features/__tests__/i18n-keys.contract.spec.ts +131 -0
- package/src/features/billing/i18n-keys.ts +15 -0
- package/src/features/handbook/components/details/HandbookPageDetails.tsx +26 -1
- package/src/features/handbook/i18n-keys.ts +1 -0
- package/dist/chunk-LVE3BJ6K.js.map +0 -1
- package/dist/chunk-UQXE3SME.mjs.map +0 -1
- /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
|
|
104
|
-
//
|
|
105
|
-
// whole
|
|
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
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
|
@@ -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
|
|
47
|
+
return (
|
|
48
|
+
<div className={READING_SCALE}>
|
|
49
|
+
<ReactMarkdownContainer content={handbookPageContent(handbookPage)} />
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
27
52
|
}
|