@carlonicora/nextjs-jsonapi 1.108.0 → 1.109.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/{AssistantInterface-BYgI5z1-.d.mts → AssistantInterface-B1c8FhGA.d.mts} +2 -0
- package/dist/{AssistantInterface-DfDcz0gJ.d.ts → AssistantInterface-BBUHxOCd.d.ts} +2 -0
- package/dist/{AssistantMessageInterface-BpEhx2pC.d.ts → AssistantMessageInterface-Cs1yb-gF.d.ts} +3 -1
- package/dist/{AssistantMessageInterface-DJ3Me16Y.d.mts → AssistantMessageInterface-DQ3mH5L8.d.mts} +3 -1
- package/dist/{AuthComponent-B6DIk8Vf.d.ts → AuthComponent-Cd7lcYif.d.ts} +1 -1
- package/dist/{AuthComponent-BKI0ZbtD.d.mts → AuthComponent-DdxCFgUZ.d.mts} +1 -1
- package/dist/{BlockNoteEditor-2AXSTGGG.js → BlockNoteEditor-3XYBZLWO.js} +20 -19
- package/dist/BlockNoteEditor-3XYBZLWO.js.map +1 -0
- package/dist/{BlockNoteEditor-XVIBGXHF.mjs → BlockNoteEditor-EBFZG7AL.mjs} +5 -4
- package/dist/{BlockNoteEditor-XVIBGXHF.mjs.map → BlockNoteEditor-EBFZG7AL.mjs.map} +1 -1
- package/dist/{auth.interface-BBUgMZzs.d.ts → auth.interface-8b601idJ.d.ts} +1 -1
- package/dist/{auth.interface-XYEREOD6.d.mts → auth.interface-CXBF8Mhi.d.mts} +1 -1
- package/dist/billing/index.js +347 -346
- package/dist/billing/index.js.map +1 -1
- package/dist/billing/index.mjs +4 -3
- package/dist/billing/index.mjs.map +1 -1
- package/dist/chunk-3J7RQBF3.js +123 -0
- package/dist/chunk-3J7RQBF3.js.map +1 -0
- package/dist/{chunk-VLDLERJN.js → chunk-7E3O52U5.js} +15 -8
- package/dist/chunk-7E3O52U5.js.map +1 -0
- package/dist/{chunk-RXXZGPC3.js → chunk-CFI4WZ5R.js} +159 -113
- package/dist/chunk-CFI4WZ5R.js.map +1 -0
- package/dist/chunk-CQID6RCF.mjs +38 -0
- package/dist/chunk-CQID6RCF.mjs.map +1 -0
- package/dist/{chunk-56XBGQGU.mjs → chunk-CRTVAQEK.mjs} +42 -27
- package/dist/chunk-CRTVAQEK.mjs.map +1 -0
- package/dist/{chunk-N3NVIPSU.mjs → chunk-MSNNAHDB.mjs} +129 -83
- package/dist/{chunk-N3NVIPSU.mjs.map → chunk-MSNNAHDB.mjs.map} +1 -1
- package/dist/chunk-MZTKPPET.mjs +123 -0
- package/dist/chunk-MZTKPPET.mjs.map +1 -0
- package/dist/{chunk-HC3JFN3C.js → chunk-UHO3KUUH.js} +838 -823
- package/dist/chunk-UHO3KUUH.js.map +1 -0
- package/dist/{chunk-CFECWLHH.mjs → chunk-UOYIWJEJ.mjs} +10 -3
- package/dist/chunk-UOYIWJEJ.mjs.map +1 -0
- package/dist/chunk-YQQHAFBS.js +38 -0
- package/dist/chunk-YQQHAFBS.js.map +1 -0
- package/dist/client/index.d.mts +8 -16
- package/dist/client/index.d.ts +8 -16
- package/dist/client/index.js +5 -4
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +4 -3
- package/dist/components/index.d.mts +6 -5
- package/dist/components/index.d.ts +6 -5
- package/dist/components/index.js +5 -4
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +4 -3
- package/dist/{config-CLQynoaa.d.ts → config-CN23v3eJ.d.ts} +4 -1
- package/dist/{config-k61pe_o2.d.mts → config-gh88Qn4h.d.mts} +4 -1
- package/dist/contexts/index.d.mts +18 -7
- package/dist/contexts/index.d.ts +18 -7
- package/dist/contexts/index.js +5 -4
- package/dist/contexts/index.js.map +1 -1
- package/dist/contexts/index.mjs +4 -3
- package/dist/core/index.d.mts +44 -11
- package/dist/core/index.d.ts +44 -11
- package/dist/core/index.js +2 -2
- package/dist/core/index.mjs +1 -1
- package/dist/features/help/index.css +29 -0
- package/dist/features/help/index.css.map +1 -0
- package/dist/features/help/index.d.mts +115 -0
- package/dist/features/help/index.d.ts +115 -0
- package/dist/features/help/index.js +532 -0
- package/dist/features/help/index.js.map +1 -0
- package/dist/features/help/index.mjs +532 -0
- package/dist/features/help/index.mjs.map +1 -0
- package/dist/features/help/server/createHelpAssetRouteHandler.d.mts +11 -0
- package/dist/features/help/server/createHelpAssetRouteHandler.d.ts +11 -0
- package/dist/features/help/server/createHelpAssetRouteHandler.js +43 -0
- package/dist/features/help/server/createHelpAssetRouteHandler.js.map +1 -0
- package/dist/features/help/server/createHelpAssetRouteHandler.mjs +43 -0
- package/dist/features/help/server/createHelpAssetRouteHandler.mjs.map +1 -0
- package/dist/features/help/server.d.mts +71 -0
- package/dist/features/help/server.d.ts +71 -0
- package/dist/features/help/server.js +123 -0
- package/dist/features/help/server.js.map +1 -0
- package/dist/features/help/server.mjs +123 -0
- package/dist/features/help/server.mjs.map +1 -0
- package/dist/help-content-config.interface-B9L02u9i.d.mts +50 -0
- package/dist/help-content-config.interface-B9L02u9i.d.ts +50 -0
- package/dist/index.d.mts +10 -8
- package/dist/index.d.ts +10 -8
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3 -2
- package/dist/{notification.interface-aLEJbA_g.d.ts → notification.interface-C1T1C2ee.d.ts} +1 -100
- package/dist/{notification.interface-DLZGtV7Z.d.mts → notification.interface-DIxR23eS.d.mts} +1 -100
- package/dist/{s3.service-CVgLWaDc.d.mts → s3.service-0BTClOYO.d.mts} +2 -2
- package/dist/{s3.service-SLlX0Zbz.d.ts → s3.service-CT27Fm1s.d.ts} +2 -2
- package/dist/server/index.d.mts +4 -3
- package/dist/server/index.d.ts +4 -3
- package/dist/server/index.js +3 -3
- package/dist/server/index.mjs +1 -1
- package/dist/types-CQSjy7et.d.mts +101 -0
- package/dist/types-DHOxe8rc.d.ts +101 -0
- package/dist/usePageUrlGenerator-tjq2mlDV.d.ts +14 -0
- package/dist/usePageUrlGenerator-uOnyJ6j2.d.mts +14 -0
- package/dist/{useSocket-BkxHHujj.d.mts → useSocket-B1fMIr17.d.mts} +1 -1
- package/dist/{useSocket-CMDjWFYm.d.ts → useSocket-BdJTBXKv.d.ts} +1 -1
- package/package.json +20 -1
- package/src/client/config.ts +9 -1
- package/src/core/registry/helpStore.ts +45 -0
- package/src/features/assistant/contexts/AssistantContext.tsx +35 -19
- package/src/features/assistant/contexts/__tests__/AssistantContext.spec.tsx +66 -6
- package/src/features/assistant/data/Assistant.ts +2 -0
- package/src/features/assistant/data/AssistantInterface.ts +2 -0
- package/src/features/assistant/data/AssistantService.ts +18 -8
- package/src/features/assistant-message/components/parts/MessageSourcesPanel.tsx +6 -4
- package/src/features/assistant-message/components/parts/tabs/ContentsTab.tsx +5 -1
- package/src/features/assistant-message/components/parts/tabs/ReferencesTab.tsx +2 -1
- package/src/features/assistant-message/data/AssistantMessage.ts +27 -1
- package/src/features/assistant-message/data/AssistantMessageInterface.ts +1 -0
- package/src/features/assistant-message/data/__tests__/AssistantMessage.spec.ts +7 -3
- package/src/features/help/components/HelpArticleBody.tsx +54 -0
- package/src/features/help/components/HelpAskAi.tsx +36 -0
- package/src/features/help/components/HelpAssistantSheet.tsx +53 -0
- package/src/features/help/components/HelpHeader.tsx +40 -0
- package/src/features/help/components/HelpHint.tsx +77 -0
- package/src/features/help/components/HelpSearchResultRow.tsx +51 -0
- package/src/features/help/components/HelpSideNav.tsx +84 -0
- package/src/features/help/components/HelpTOC.tsx +49 -0
- package/src/features/help/components/__tests__/HelpAskAi.spec.tsx +68 -0
- package/src/features/help/components/__tests__/HelpAssistantSheet.spec.tsx +36 -0
- package/src/features/help/components/__tests__/HelpHint.spec.tsx +50 -0
- package/src/features/help/components/__tests__/HelpSearchResultRow.spec.tsx +59 -0
- package/src/features/help/components/__tests__/HelpSideNav.spec.tsx +52 -0
- package/src/features/help/components/mdx/Callout.tsx +21 -0
- package/src/features/help/components/mdx/EntityRef.tsx +18 -0
- package/src/features/help/components/mdx/KeyBinding.tsx +6 -0
- package/src/features/help/components/mdx/Related.tsx +33 -0
- package/src/features/help/components/mdx/Screenshot.tsx +9 -0
- package/src/features/help/components/mdx/Steps.tsx +21 -0
- package/src/features/help/components/mdx/Video.tsx +8 -0
- package/src/features/help/components/mdx/mdx-server-components.ts +23 -0
- package/src/features/help/components/mdx/mdxComponents.ts +9 -0
- package/src/features/help/contexts/HelpContext.spec.tsx +28 -0
- package/src/features/help/contexts/HelpContext.tsx +24 -0
- package/src/features/help/hooks/useHelp.ts +1 -0
- package/src/features/help/hooks/useHelpArticle.ts +7 -0
- package/src/features/help/hooks/useHelpFilter.ts +27 -0
- package/src/features/help/hooks/useHelpManifest.ts +5 -0
- package/src/features/help/i18n-keys.ts +34 -0
- package/src/features/help/index.ts +27 -0
- package/src/features/help/interfaces/help-content-config.interface.ts +17 -0
- package/src/features/help/server/__tests__/createHelpAssetRouteHandler.spec.ts +43 -0
- package/src/features/help/server/createHelpAssetRouteHandler.ts +35 -0
- package/src/features/help/server/generateHelpArticleMetadata.ts +18 -0
- package/src/features/help/server/generateHelpArticleStaticParams.ts +7 -0
- package/src/features/help/server/generateHelpModeStaticParams.ts +5 -0
- package/src/features/help/server/getHelpContent.ts +17 -0
- package/src/features/help/server/index.ts +8 -0
- package/src/features/help/server/serializeHelpArticle.tsx +46 -0
- package/src/features/help/server-entry.ts +20 -0
- package/src/features/help/types/help-article.types.ts +37 -0
- package/src/features/help/utils/__tests__/helpNavigation.spec.ts +70 -0
- package/src/features/help/utils/articleUrl.ts +13 -0
- package/src/features/help/utils/helpNavigation.ts +29 -0
- package/src/features/how-to/HowToModule.ts +1 -1
- package/src/features/how-to/data/HowTo.ts +21 -3
- package/src/features/how-to/data/HowToInterface.ts +1 -0
- package/src/index.ts +4 -0
- package/dist/BlockNoteEditor-2AXSTGGG.js.map +0 -1
- package/dist/chunk-56XBGQGU.mjs.map +0 -1
- package/dist/chunk-CFECWLHH.mjs.map +0 -1
- package/dist/chunk-HC3JFN3C.js.map +0 -1
- package/dist/chunk-RXXZGPC3.js.map +0 -1
- package/dist/chunk-VLDLERJN.js.map +0 -1
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { configureJsonApi } from "../../../../client/config";
|
|
4
|
+
import { HelpProvider } from "../../contexts/HelpContext";
|
|
5
|
+
import { HelpHint } from "../HelpHint";
|
|
6
|
+
|
|
7
|
+
const article = {
|
|
8
|
+
id: "1",
|
|
9
|
+
slug: "x",
|
|
10
|
+
mode: "how-to",
|
|
11
|
+
title: "X",
|
|
12
|
+
summary: "S",
|
|
13
|
+
order: 1,
|
|
14
|
+
tags: [],
|
|
15
|
+
contextualKeys: ["npc.editor"],
|
|
16
|
+
aiIndexed: true,
|
|
17
|
+
draft: false,
|
|
18
|
+
contentHash: "h",
|
|
19
|
+
path: "how-to/x.mdx",
|
|
20
|
+
headings: [],
|
|
21
|
+
relatedSlugs: [],
|
|
22
|
+
lastUpdated: "2026-01-01T00:00:00Z",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
configureJsonApi({
|
|
27
|
+
apiUrl: "http://localhost",
|
|
28
|
+
helpContent: { manifest: [article], namespaceUuid: "00000000-0000-5000-8000-000000000000" },
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("HelpHint", () => {
|
|
33
|
+
it("renders null when no article matches the contextKey", () => {
|
|
34
|
+
const { container } = render(
|
|
35
|
+
<HelpProvider>
|
|
36
|
+
<HelpHint contextKey="nope.absent" />
|
|
37
|
+
</HelpProvider>,
|
|
38
|
+
);
|
|
39
|
+
expect(container.querySelector("button")).toBeNull();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("renders the trigger when at least one article matches", () => {
|
|
43
|
+
render(
|
|
44
|
+
<HelpProvider>
|
|
45
|
+
<HelpHint contextKey="npc.editor" />
|
|
46
|
+
</HelpProvider>,
|
|
47
|
+
);
|
|
48
|
+
expect(screen.getByLabelText(/help on this|hint/i)).toBeTruthy();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { Command, CommandList } from "../../../../shadcnui";
|
|
4
|
+
import { configureJsonApi } from "../../../../client/config";
|
|
5
|
+
import { HelpProvider } from "../../contexts/HelpContext";
|
|
6
|
+
import { HelpSearchResultRow } from "../HelpSearchResultRow";
|
|
7
|
+
|
|
8
|
+
const article = {
|
|
9
|
+
id: "abc",
|
|
10
|
+
slug: "x",
|
|
11
|
+
mode: "how-to" as const,
|
|
12
|
+
title: "X title",
|
|
13
|
+
summary: "S",
|
|
14
|
+
order: 1,
|
|
15
|
+
tags: [],
|
|
16
|
+
contextualKeys: [],
|
|
17
|
+
aiIndexed: true,
|
|
18
|
+
draft: false,
|
|
19
|
+
contentHash: "h",
|
|
20
|
+
path: "how-to/x.mdx",
|
|
21
|
+
headings: [],
|
|
22
|
+
relatedSlugs: [],
|
|
23
|
+
lastUpdated: "2026-01-01T00:00:00Z",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
configureJsonApi({
|
|
28
|
+
apiUrl: "http://localhost",
|
|
29
|
+
helpContent: { manifest: [article], namespaceUuid: "00000000-0000-5000-8000-000000000000" },
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("HelpSearchResultRow", () => {
|
|
34
|
+
it("renders the article title when manifest contains the id", () => {
|
|
35
|
+
render(
|
|
36
|
+
<HelpProvider>
|
|
37
|
+
<Command>
|
|
38
|
+
<CommandList>
|
|
39
|
+
<HelpSearchResultRow result={{ id: "abc", name: "ignored", entityType: "howtos" }} />
|
|
40
|
+
</CommandList>
|
|
41
|
+
</Command>
|
|
42
|
+
</HelpProvider>,
|
|
43
|
+
);
|
|
44
|
+
expect(screen.getByText("X title")).toBeTruthy();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("renders disabled removed-state row when id is missing", () => {
|
|
48
|
+
render(
|
|
49
|
+
<HelpProvider>
|
|
50
|
+
<Command>
|
|
51
|
+
<CommandList>
|
|
52
|
+
<HelpSearchResultRow result={{ id: "ghost", name: "ignored", entityType: "howtos" }} />
|
|
53
|
+
</CommandList>
|
|
54
|
+
</Command>
|
|
55
|
+
</HelpProvider>,
|
|
56
|
+
);
|
|
57
|
+
expect(screen.queryByText("X title")).toBeNull();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { render, screen, fireEvent } from "@testing-library/react";
|
|
3
|
+
import { configureJsonApi } from "../../../../client/config";
|
|
4
|
+
import { HelpProvider } from "../../contexts/HelpContext";
|
|
5
|
+
import { HelpSideNav } from "../HelpSideNav";
|
|
6
|
+
|
|
7
|
+
const base = {
|
|
8
|
+
tags: [],
|
|
9
|
+
contextualKeys: [],
|
|
10
|
+
aiIndexed: true,
|
|
11
|
+
draft: false,
|
|
12
|
+
contentHash: "h",
|
|
13
|
+
headings: [],
|
|
14
|
+
relatedSlugs: [],
|
|
15
|
+
lastUpdated: "2026-01-01T00:00:00Z",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
configureJsonApi({
|
|
20
|
+
apiUrl: "http://localhost",
|
|
21
|
+
helpContent: {
|
|
22
|
+
manifest: [
|
|
23
|
+
{ ...base, id: "a", slug: "a", mode: "how-to", title: "Alpha", summary: "s", order: 1, path: "how-to/a.mdx" },
|
|
24
|
+
{ ...base, id: "b", slug: "b", mode: "how-to", title: "Beta", summary: "s", order: 2, path: "how-to/b.mdx" },
|
|
25
|
+
],
|
|
26
|
+
namespaceUuid: "00000000-0000-5000-8000-000000000000",
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("HelpSideNav", () => {
|
|
32
|
+
it("lists every non-draft article", () => {
|
|
33
|
+
render(
|
|
34
|
+
<HelpProvider>
|
|
35
|
+
<HelpSideNav />
|
|
36
|
+
</HelpProvider>,
|
|
37
|
+
);
|
|
38
|
+
expect(screen.getByText("Alpha")).toBeTruthy();
|
|
39
|
+
expect(screen.getByText("Beta")).toBeTruthy();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("filters by query", () => {
|
|
43
|
+
render(
|
|
44
|
+
<HelpProvider>
|
|
45
|
+
<HelpSideNav />
|
|
46
|
+
</HelpProvider>,
|
|
47
|
+
);
|
|
48
|
+
fireEvent.change(screen.getByPlaceholderText(/filter/i), { target: { value: "Bet" } });
|
|
49
|
+
expect(screen.queryByText("Alpha")).toBeNull();
|
|
50
|
+
expect(screen.getByText("Beta")).toBeTruthy();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { InfoIcon, AlertTriangleIcon, LightbulbIcon, XCircleIcon, LucideIcon } from "lucide-react";
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
type CalloutType = "info" | "warning" | "tip" | "danger";
|
|
5
|
+
|
|
6
|
+
const STYLES: Record<CalloutType, { icon: LucideIcon; ring: string }> = {
|
|
7
|
+
info: { icon: InfoIcon, ring: "ring-blue-500/30 bg-blue-500/5" },
|
|
8
|
+
warning: { icon: AlertTriangleIcon, ring: "ring-amber-500/30 bg-amber-500/5" },
|
|
9
|
+
tip: { icon: LightbulbIcon, ring: "ring-emerald-500/30 bg-emerald-500/5" },
|
|
10
|
+
danger: { icon: XCircleIcon, ring: "ring-rose-500/30 bg-rose-500/5" },
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function Callout({ type = "info", children }: { type?: CalloutType; children: ReactNode }) {
|
|
14
|
+
const { icon: Icon, ring } = STYLES[type];
|
|
15
|
+
return (
|
|
16
|
+
<div className={`my-4 flex gap-3 rounded-md ring-1 p-3 ${ring}`}>
|
|
17
|
+
<Icon className="mt-0.5 h-4 w-4 shrink-0" />
|
|
18
|
+
<div className="text-sm leading-relaxed">{children}</div>
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Modules } from "../../../../core";
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
export function EntityRef({ type, children }: { type: string; children: ReactNode }) {
|
|
5
|
+
let Icon: React.ComponentType<{ className?: string }> | null = null;
|
|
6
|
+
try {
|
|
7
|
+
const m = Modules.findByName(type);
|
|
8
|
+
Icon = m.icon ?? null;
|
|
9
|
+
} catch {
|
|
10
|
+
Icon = null;
|
|
11
|
+
}
|
|
12
|
+
return (
|
|
13
|
+
<span className="bg-muted text-foreground/90 inline-flex items-center gap-1 rounded px-1.5 py-0.5 align-middle text-xs">
|
|
14
|
+
{Icon ? <Icon className="h-3 w-3" /> : null}
|
|
15
|
+
{children}
|
|
16
|
+
</span>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import Link from "next/link";
|
|
3
|
+
import { useTranslations } from "next-intl";
|
|
4
|
+
import { useHelp } from "../../contexts/HelpContext";
|
|
5
|
+
import { usePageUrlGenerator } from "../../../../client";
|
|
6
|
+
import { articleUrl } from "../../utils/articleUrl";
|
|
7
|
+
|
|
8
|
+
export function Related({ slugs }: { slugs: string[] }) {
|
|
9
|
+
const t = useTranslations();
|
|
10
|
+
const generateUrl = usePageUrlGenerator();
|
|
11
|
+
const { manifest } = useHelp();
|
|
12
|
+
const items = slugs
|
|
13
|
+
.map((s) => manifest.find((a) => `${a.mode}/${a.slug}` === s))
|
|
14
|
+
.filter((a): a is NonNullable<typeof a> => Boolean(a));
|
|
15
|
+
if (items.length === 0) return null;
|
|
16
|
+
return (
|
|
17
|
+
<aside className="my-6">
|
|
18
|
+
<h3 className="text-muted-foreground mb-2 text-xs font-semibold tracking-wider uppercase">
|
|
19
|
+
{t("help.article.related")}
|
|
20
|
+
</h3>
|
|
21
|
+
<ul className="grid gap-2 sm:grid-cols-2">
|
|
22
|
+
{items.map((a) => (
|
|
23
|
+
<li key={a.id}>
|
|
24
|
+
<Link href={articleUrl(generateUrl, a)} className="hover:bg-muted block rounded border p-3">
|
|
25
|
+
<div className="font-medium">{a.title}</div>
|
|
26
|
+
<div className="text-muted-foreground text-xs">{a.summary}</div>
|
|
27
|
+
</Link>
|
|
28
|
+
</li>
|
|
29
|
+
))}
|
|
30
|
+
</ul>
|
|
31
|
+
</aside>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function Screenshot({ src, alt, caption }: { src: string; alt?: string; caption?: string }) {
|
|
2
|
+
const url = src.startsWith("/") ? src : `/help-assets/${src}`;
|
|
3
|
+
return (
|
|
4
|
+
<figure className="my-4">
|
|
5
|
+
<img src={url} alt={alt ?? caption ?? ""} className="rounded-md ring-1 ring-border" />
|
|
6
|
+
{caption ? <figcaption className="text-muted-foreground mt-1 text-xs">{caption}</figcaption> : null}
|
|
7
|
+
</figure>
|
|
8
|
+
);
|
|
9
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ReactNode, Children } from "react";
|
|
2
|
+
|
|
3
|
+
export function Steps({ children }: { children: ReactNode }) {
|
|
4
|
+
const items = Children.toArray(children);
|
|
5
|
+
return (
|
|
6
|
+
<ol className="my-4 list-none space-y-3 pl-0">
|
|
7
|
+
{items.map((child, i) => (
|
|
8
|
+
<li key={i} className="flex gap-3">
|
|
9
|
+
<span className="bg-muted text-muted-foreground flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-xs">
|
|
10
|
+
{i + 1}
|
|
11
|
+
</span>
|
|
12
|
+
<div className="flex-1 [&>p:first-child]:mt-0">{child}</div>
|
|
13
|
+
</li>
|
|
14
|
+
))}
|
|
15
|
+
</ol>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function Step({ children }: { children: ReactNode }) {
|
|
20
|
+
return <>{children}</>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function Video({ src, poster }: { src: string; poster?: string }) {
|
|
2
|
+
const url = src.startsWith("/") ? src : `/help-assets/${src}`;
|
|
3
|
+
return (
|
|
4
|
+
<video controls preload="metadata" poster={poster} className="my-4 w-full rounded-md ring-1 ring-border">
|
|
5
|
+
<source src={url} />
|
|
6
|
+
</video>
|
|
7
|
+
);
|
|
8
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Server-safe subset of MDX_COMPONENTS used by `renderHelpArticle`.
|
|
2
|
+
// Mirrors `mdxComponents.ts` but EXCLUDES `Related` (which is a client
|
|
3
|
+
// component that uses `useHelp` + `usePageUrlGenerator`). Server-side rendering
|
|
4
|
+
// of `<Related>` would require passing the manifest by prop, which the standard
|
|
5
|
+
// MDX components map cannot do — articles that need related links should be
|
|
6
|
+
// extended via a server-side render hook rather than the MDX shortcode.
|
|
7
|
+
|
|
8
|
+
import { Callout } from "./Callout";
|
|
9
|
+
import { Steps, Step } from "./Steps";
|
|
10
|
+
import { Screenshot } from "./Screenshot";
|
|
11
|
+
import { EntityRef } from "./EntityRef";
|
|
12
|
+
import { KeyBinding } from "./KeyBinding";
|
|
13
|
+
import { Video } from "./Video";
|
|
14
|
+
|
|
15
|
+
export const MDX_SERVER_COMPONENTS = {
|
|
16
|
+
Callout,
|
|
17
|
+
Steps,
|
|
18
|
+
Step,
|
|
19
|
+
Screenshot,
|
|
20
|
+
EntityRef,
|
|
21
|
+
KeyBinding,
|
|
22
|
+
Video,
|
|
23
|
+
} as const;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Callout } from "./Callout";
|
|
2
|
+
import { Steps, Step } from "./Steps";
|
|
3
|
+
import { Screenshot } from "./Screenshot";
|
|
4
|
+
import { EntityRef } from "./EntityRef";
|
|
5
|
+
import { KeyBinding } from "./KeyBinding";
|
|
6
|
+
import { Video } from "./Video";
|
|
7
|
+
import { Related } from "./Related";
|
|
8
|
+
|
|
9
|
+
export const MDX_COMPONENTS = { Callout, Steps, Step, Screenshot, EntityRef, KeyBinding, Video, Related };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { renderHook } from "@testing-library/react";
|
|
3
|
+
import { ReactNode } from "react";
|
|
4
|
+
import { configureJsonApi } from "../../../client/config";
|
|
5
|
+
import { HelpProvider, useHelp } from "./HelpContext";
|
|
6
|
+
|
|
7
|
+
describe("HelpProvider / useHelp", () => {
|
|
8
|
+
const cfg = {
|
|
9
|
+
manifest: [],
|
|
10
|
+
namespaceUuid: "00000000-0000-5000-8000-000000000000",
|
|
11
|
+
brand: { logo: "/logo.png", label: "Test", appHref: "/" },
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
configureJsonApi({ apiUrl: "http://localhost", helpContent: cfg });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("exposes the configured helpContent via useHelp()", () => {
|
|
19
|
+
const wrapper = ({ children }: { children: ReactNode }) => <HelpProvider>{children}</HelpProvider>;
|
|
20
|
+
const { result } = renderHook(() => useHelp(), { wrapper });
|
|
21
|
+
expect(result.current.manifest).toBe(cfg.manifest);
|
|
22
|
+
expect(result.current.brand?.label).toBe("Test");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("throws when useHelp() is called outside HelpProvider", () => {
|
|
26
|
+
expect(() => renderHook(() => useHelp())).toThrow(/outside <HelpProvider>/);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { createContext, useContext, ReactNode } from "react";
|
|
3
|
+
import { _getStaticHelpContent } from "../../../core/registry/helpStore";
|
|
4
|
+
import type { HelpContentConfig } from "../interfaces/help-content-config.interface";
|
|
5
|
+
|
|
6
|
+
const HelpContext = createContext<HelpContentConfig | null>(null);
|
|
7
|
+
|
|
8
|
+
export function HelpProvider({ children }: { children: ReactNode }) {
|
|
9
|
+
const cfg = _getStaticHelpContent<HelpContentConfig>();
|
|
10
|
+
if (!cfg) {
|
|
11
|
+
throw new Error(
|
|
12
|
+
"Help content not configured — call configureJsonApi({ helpContent: {...} }) before importing help components.",
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
return <HelpContext.Provider value={cfg}>{children}</HelpContext.Provider>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function useHelp(): HelpContentConfig {
|
|
19
|
+
const ctx = useContext(HelpContext);
|
|
20
|
+
if (!ctx) {
|
|
21
|
+
throw new Error("useHelp() called outside <HelpProvider>. Wrap your help route group with <HelpProvider>.");
|
|
22
|
+
}
|
|
23
|
+
return ctx;
|
|
24
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useHelp } from "../contexts/HelpContext";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useMemo, useState } from "react";
|
|
3
|
+
import Fuse from "fuse.js";
|
|
4
|
+
import type { HelpArticle } from "../types/help-article.types";
|
|
5
|
+
|
|
6
|
+
export function useHelpFilter(articles: readonly HelpArticle[]) {
|
|
7
|
+
const [query, setQuery] = useState("");
|
|
8
|
+
const fuse = useMemo(
|
|
9
|
+
() =>
|
|
10
|
+
new Fuse([...articles], {
|
|
11
|
+
keys: [
|
|
12
|
+
{ name: "title", weight: 0.5 },
|
|
13
|
+
{ name: "summary", weight: 0.3 },
|
|
14
|
+
{ name: "tags", weight: 0.1 },
|
|
15
|
+
{ name: "headings.text", weight: 0.1 },
|
|
16
|
+
],
|
|
17
|
+
threshold: 0.4,
|
|
18
|
+
ignoreLocation: true,
|
|
19
|
+
}),
|
|
20
|
+
[articles],
|
|
21
|
+
);
|
|
22
|
+
const filtered = useMemo(() => {
|
|
23
|
+
if (!query.trim()) return [...articles];
|
|
24
|
+
return fuse.search(query).map((r) => r.item);
|
|
25
|
+
}, [query, articles, fuse]);
|
|
26
|
+
return { query, setQuery, filtered };
|
|
27
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The fixed namespace of i18n keys this feature reads via useTranslations/getTranslations.
|
|
3
|
+
* Consuming apps must define each entry in their messages/<locale>.json.
|
|
4
|
+
*/
|
|
5
|
+
export const HELP_I18N_KEYS = [
|
|
6
|
+
"help.modes.tutorial",
|
|
7
|
+
"help.modes.how-to",
|
|
8
|
+
"help.modes.reference",
|
|
9
|
+
"help.modes.explanation",
|
|
10
|
+
"help.sideNav.filterPlaceholder",
|
|
11
|
+
"help.sideNav.noMatches",
|
|
12
|
+
"help.toc.title",
|
|
13
|
+
"help.article.lastUpdated",
|
|
14
|
+
"help.article.previous",
|
|
15
|
+
"help.article.next",
|
|
16
|
+
"help.article.related",
|
|
17
|
+
"help.askAi.button",
|
|
18
|
+
"help.askAi.loginTooltip",
|
|
19
|
+
"help.askAi.sheet.title",
|
|
20
|
+
"help.askAi.sheet.subtitle",
|
|
21
|
+
"help.header.search",
|
|
22
|
+
"help.header.login",
|
|
23
|
+
"help.header.openApp",
|
|
24
|
+
"help.hint.trigger",
|
|
25
|
+
"help.hint.viewArticle",
|
|
26
|
+
"help.hint.pickArticle",
|
|
27
|
+
"help.landing.heading",
|
|
28
|
+
"help.landing.subheading",
|
|
29
|
+
"help.landing.featuredTutorials",
|
|
30
|
+
"help.landing.browseByMode",
|
|
31
|
+
"help.modeIndex.empty",
|
|
32
|
+
"help.search.removed",
|
|
33
|
+
"help.footerLink",
|
|
34
|
+
] as const;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Client-only public barrel for the help feature.
|
|
2
|
+
// Consumed via `@carlonicora/nextjs-jsonapi/help`. Tsup adds a top-level
|
|
3
|
+
// "use client" directive to the bundled output via clientEntries.
|
|
4
|
+
//
|
|
5
|
+
// Pair this with `@carlonicora/nextjs-jsonapi/help/server` (server) for the
|
|
6
|
+
// pure server utilities (serializeHelpArticle, generate*StaticParams, etc.).
|
|
7
|
+
// Consumers compose page shells themselves: import the client primitives below
|
|
8
|
+
// (HelpProvider, HelpHeader, HelpSideNav, HelpArticleBody, HelpTOC, etc.) and
|
|
9
|
+
// the server utilities from /help/server, then assemble pages in app routes.
|
|
10
|
+
|
|
11
|
+
export { HelpProvider, useHelp } from "./contexts/HelpContext";
|
|
12
|
+
export { HelpHeader } from "./components/HelpHeader";
|
|
13
|
+
export { HelpSideNav } from "./components/HelpSideNav";
|
|
14
|
+
export { HelpArticleBody } from "./components/HelpArticleBody";
|
|
15
|
+
export { HelpTOC } from "./components/HelpTOC";
|
|
16
|
+
export { HelpHint } from "./components/HelpHint";
|
|
17
|
+
export { HelpAskAi } from "./components/HelpAskAi";
|
|
18
|
+
export { HelpSearchResultRow } from "./components/HelpSearchResultRow";
|
|
19
|
+
export { MDX_COMPONENTS } from "./components/mdx/mdxComponents";
|
|
20
|
+
export { useHelpManifest } from "./hooks/useHelpManifest";
|
|
21
|
+
export { useHelpArticle } from "./hooks/useHelpArticle";
|
|
22
|
+
export { useHelpFilter } from "./hooks/useHelpFilter";
|
|
23
|
+
export { articleUrl, modeUrl } from "./utils/articleUrl";
|
|
24
|
+
export { HELP_I18N_KEYS } from "./i18n-keys";
|
|
25
|
+
export type { HelpContentConfig, HelpBrandConfig } from "./interfaces/help-content-config.interface";
|
|
26
|
+
export { HELP_MODES } from "./types/help-article.types";
|
|
27
|
+
export type { HelpArticle, HelpHeading, HelpMode, HelpRedirect } from "./types/help-article.types";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { HelpArticle, HelpRedirect } from "../types/help-article.types";
|
|
2
|
+
|
|
3
|
+
export interface HelpBrandConfig {
|
|
4
|
+
/** URL of the brand logo image (served by the consuming app). */
|
|
5
|
+
logo?: string;
|
|
6
|
+
/** Brand label shown in the help header. */
|
|
7
|
+
label?: string;
|
|
8
|
+
/** Where the "Open app" button navigates for logged-in users. Defaults to "/". */
|
|
9
|
+
appHref?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface HelpContentConfig {
|
|
13
|
+
manifest: readonly HelpArticle[];
|
|
14
|
+
namespaceUuid: string;
|
|
15
|
+
redirects?: readonly HelpRedirect[];
|
|
16
|
+
brand?: HelpBrandConfig;
|
|
17
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
+
import { mkdtemp, writeFile, mkdir, rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { createHelpAssetRouteHandler } from "../createHelpAssetRouteHandler";
|
|
6
|
+
|
|
7
|
+
describe("createHelpAssetRouteHandler", () => {
|
|
8
|
+
let srcRoot: string;
|
|
9
|
+
|
|
10
|
+
beforeAll(async () => {
|
|
11
|
+
srcRoot = await mkdtemp(join(tmpdir(), "help-assets-"));
|
|
12
|
+
await mkdir(srcRoot, { recursive: true });
|
|
13
|
+
await writeFile(join(srcRoot, "ok.png"), Buffer.from([0x89, 0x50, 0x4e, 0x47]));
|
|
14
|
+
});
|
|
15
|
+
afterAll(async () => {
|
|
16
|
+
await rm(srcRoot, { recursive: true, force: true });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("returns 200 with correct MIME for an existing PNG", async () => {
|
|
20
|
+
const GET = createHelpAssetRouteHandler({ srcRoot });
|
|
21
|
+
const res = await GET(new Request("http://localhost/help-assets/ok.png"), {
|
|
22
|
+
params: Promise.resolve({ path: ["ok.png"] }),
|
|
23
|
+
});
|
|
24
|
+
expect(res.status).toBe(200);
|
|
25
|
+
expect(res.headers.get("Content-Type")).toBe("image/png");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("returns 404 for missing files", async () => {
|
|
29
|
+
const GET = createHelpAssetRouteHandler({ srcRoot });
|
|
30
|
+
const res = await GET(new Request("http://localhost/help-assets/missing.png"), {
|
|
31
|
+
params: Promise.resolve({ path: ["missing.png"] }),
|
|
32
|
+
});
|
|
33
|
+
expect(res.status).toBe(404);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("sanitizes path traversal attempts", async () => {
|
|
37
|
+
const GET = createHelpAssetRouteHandler({ srcRoot });
|
|
38
|
+
const res = await GET(new Request("http://localhost/help-assets/etc/passwd"), {
|
|
39
|
+
params: Promise.resolve({ path: ["..", "..", "etc", "passwd"] }),
|
|
40
|
+
});
|
|
41
|
+
expect(res.status).toBe(404);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { readFile, stat } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
const MIME: Record<string, string> = {
|
|
6
|
+
".png": "image/png",
|
|
7
|
+
".jpg": "image/jpeg",
|
|
8
|
+
".jpeg": "image/jpeg",
|
|
9
|
+
".webp": "image/webp",
|
|
10
|
+
".gif": "image/gif",
|
|
11
|
+
".svg": "image/svg+xml",
|
|
12
|
+
".mp4": "video/mp4",
|
|
13
|
+
".webm": "video/webm",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function createHelpAssetRouteHandler({ srcRoot }: { srcRoot: string }) {
|
|
17
|
+
return async function GET(_req: Request, { params }: { params: Promise<{ path: string[] }> }) {
|
|
18
|
+
const { path } = await params;
|
|
19
|
+
const safe = path.filter((p) => p !== ".." && !p.includes("/")).join("/");
|
|
20
|
+
const filePath = join(srcRoot, safe);
|
|
21
|
+
try {
|
|
22
|
+
const s = await stat(filePath);
|
|
23
|
+
if (!s.isFile()) return new NextResponse("Not found", { status: 404 });
|
|
24
|
+
const ext = filePath.toLowerCase().slice(filePath.lastIndexOf("."));
|
|
25
|
+
const mime = MIME[ext] ?? "application/octet-stream";
|
|
26
|
+
const data = await readFile(filePath);
|
|
27
|
+
return new NextResponse(data, {
|
|
28
|
+
status: 200,
|
|
29
|
+
headers: { "Content-Type": mime, "Cache-Control": "public, max-age=3600" },
|
|
30
|
+
});
|
|
31
|
+
} catch {
|
|
32
|
+
return new NextResponse("Not found", { status: 404 });
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { findHelpArticle } from "../utils/helpNavigation";
|
|
3
|
+
import { getHelpContent } from "./getHelpContent";
|
|
4
|
+
|
|
5
|
+
export async function generateHelpArticleMetadata({
|
|
6
|
+
params,
|
|
7
|
+
}: {
|
|
8
|
+
params: Promise<{ mode: string; slug: string }>;
|
|
9
|
+
}): Promise<Metadata> {
|
|
10
|
+
const { mode, slug } = await params;
|
|
11
|
+
const article = findHelpArticle(getHelpContent().manifest, mode, slug);
|
|
12
|
+
if (!article) return {};
|
|
13
|
+
return {
|
|
14
|
+
title: article.title,
|
|
15
|
+
description: article.summary,
|
|
16
|
+
openGraph: { title: article.title, description: article.summary },
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { _getStaticHelpContent } from "../../../core/registry/helpStore";
|
|
2
|
+
import type { HelpContentConfig } from "../interfaces/help-content-config.interface";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Server-side accessor for the help-content config set via configureJsonApi().
|
|
6
|
+
* Returns the same value <HelpProvider> exposes via React Context to client components.
|
|
7
|
+
* Throws with a setup error if helpContent was never configured.
|
|
8
|
+
*/
|
|
9
|
+
export function getHelpContent(): HelpContentConfig {
|
|
10
|
+
const cfg = _getStaticHelpContent<HelpContentConfig>();
|
|
11
|
+
if (!cfg) {
|
|
12
|
+
throw new Error(
|
|
13
|
+
"Help content not configured — call configureJsonApi({ helpContent: {...} }) before importing help pages.",
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
return cfg;
|
|
17
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { getHelpContent } from "./getHelpContent";
|
|
2
|
+
export { renderHelpArticle, serializeHelpArticle } from "./serializeHelpArticle";
|
|
3
|
+
export { generateHelpArticleStaticParams } from "./generateHelpArticleStaticParams";
|
|
4
|
+
export { generateHelpArticleMetadata } from "./generateHelpArticleMetadata";
|
|
5
|
+
export { generateHelpModeStaticParams } from "./generateHelpModeStaticParams";
|
|
6
|
+
// NOTE: `createHelpAssetRouteHandler` is intentionally NOT re-exported here.
|
|
7
|
+
// It uses `node:fs/promises` and must only be reachable via the dedicated
|
|
8
|
+
// `@carlonicora/nextjs-jsonapi/help-asset-route` subpath, never the main bundle.
|