@carlonicora/nextjs-jsonapi 1.109.0 → 1.110.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 (146) hide show
  1. package/dist/{AssistantMessageInterface-Cs1yb-gF.d.ts → AssistantMessageInterface-BNzf1etU.d.ts} +14 -1
  2. package/dist/{AssistantMessageInterface-DQ3mH5L8.d.mts → AssistantMessageInterface-DrgHDjZ5.d.mts} +14 -1
  3. package/dist/{BlockNoteEditor-3XYBZLWO.js → BlockNoteEditor-7OKW6HHN.js} +19 -20
  4. package/dist/BlockNoteEditor-7OKW6HHN.js.map +1 -0
  5. package/dist/{BlockNoteEditor-EBFZG7AL.mjs → BlockNoteEditor-K7IIWM3S.mjs} +4 -5
  6. package/dist/{BlockNoteEditor-EBFZG7AL.mjs.map → BlockNoteEditor-K7IIWM3S.mjs.map} +1 -1
  7. package/dist/BlockNoteViewer-OPSCTHKJ.mjs +31 -0
  8. package/dist/BlockNoteViewer-OPSCTHKJ.mjs.map +1 -0
  9. package/dist/BlockNoteViewer-UW5VZ5UF.js +31 -0
  10. package/dist/BlockNoteViewer-UW5VZ5UF.js.map +1 -0
  11. package/dist/billing/index.d.mts +41 -41
  12. package/dist/billing/index.d.ts +41 -41
  13. package/dist/billing/index.js +346 -347
  14. package/dist/billing/index.js.map +1 -1
  15. package/dist/billing/index.mjs +3 -4
  16. package/dist/billing/index.mjs.map +1 -1
  17. package/dist/{chunk-7E3O52U5.js → chunk-6XWW67F2.js} +25 -12
  18. package/dist/chunk-6XWW67F2.js.map +1 -0
  19. package/dist/{chunk-UHO3KUUH.js → chunk-AKXUNE3A.js} +977 -850
  20. package/dist/chunk-AKXUNE3A.js.map +1 -0
  21. package/dist/{chunk-UOYIWJEJ.mjs → chunk-CWVYU4YZ.mjs} +18 -5
  22. package/dist/{chunk-UOYIWJEJ.mjs.map → chunk-CWVYU4YZ.mjs.map} +1 -1
  23. package/dist/{chunk-CRTVAQEK.mjs → chunk-TXVVZLUQ.mjs} +1388 -1261
  24. package/dist/chunk-TXVVZLUQ.mjs.map +1 -0
  25. package/dist/{chunk-CFI4WZ5R.js → chunk-U2HR57MY.js} +111 -5
  26. package/dist/chunk-U2HR57MY.js.map +1 -0
  27. package/dist/chunk-WPAQRB5Q.mjs +7 -0
  28. package/dist/chunk-WPAQRB5Q.mjs.map +1 -0
  29. package/dist/chunk-YBZVWLPH.js +7 -0
  30. package/dist/chunk-YBZVWLPH.js.map +1 -0
  31. package/dist/{chunk-MSNNAHDB.mjs → chunk-ZYPNVEHB.mjs} +112 -6
  32. package/dist/{chunk-MSNNAHDB.mjs.map → chunk-ZYPNVEHB.mjs.map} +1 -1
  33. package/dist/client/index.d.mts +3 -4
  34. package/dist/client/index.d.ts +3 -4
  35. package/dist/client/index.js +4 -5
  36. package/dist/client/index.js.map +1 -1
  37. package/dist/client/index.mjs +3 -4
  38. package/dist/components/index.d.mts +418 -415
  39. package/dist/components/index.d.ts +418 -415
  40. package/dist/components/index.js +6 -5
  41. package/dist/components/index.js.map +1 -1
  42. package/dist/components/index.mjs +5 -4
  43. package/dist/{config-CN23v3eJ.d.ts → config-BrkU_Ynz.d.ts} +2 -2
  44. package/dist/{config-gh88Qn4h.d.mts → config-snd2ToEB.d.mts} +2 -2
  45. package/dist/contexts/index.d.mts +15 -15
  46. package/dist/contexts/index.d.ts +15 -15
  47. package/dist/contexts/index.js +4 -5
  48. package/dist/contexts/index.js.map +1 -1
  49. package/dist/contexts/index.mjs +3 -4
  50. package/dist/core/index.d.mts +35 -4
  51. package/dist/core/index.d.ts +35 -4
  52. package/dist/core/index.js +2 -2
  53. package/dist/core/index.mjs +1 -1
  54. package/dist/features/help/index.d.mts +25 -66
  55. package/dist/features/help/index.d.ts +25 -66
  56. package/dist/features/help/index.js +114 -124
  57. package/dist/features/help/index.js.map +1 -1
  58. package/dist/features/help/index.mjs +95 -105
  59. package/dist/features/help/index.mjs.map +1 -1
  60. package/dist/features/help/server.d.mts +27 -65
  61. package/dist/features/help/server.d.ts +27 -65
  62. package/dist/features/help/server.js +4 -119
  63. package/dist/features/help/server.js.map +1 -1
  64. package/dist/features/help/server.mjs +4 -119
  65. package/dist/features/help/server.mjs.map +1 -1
  66. package/dist/help-content-config.interface-C5ESIod9.d.mts +13 -0
  67. package/dist/help-content-config.interface-C5ESIod9.d.ts +13 -0
  68. package/dist/index.d.mts +3 -3
  69. package/dist/index.d.ts +3 -3
  70. package/dist/index.js +3 -4
  71. package/dist/index.js.map +1 -1
  72. package/dist/index.mjs +2 -3
  73. package/dist/server/index.js +3 -3
  74. package/dist/server/index.mjs +1 -1
  75. package/dist/testing/index.d.mts +1 -2
  76. package/dist/testing/index.d.ts +1 -2
  77. package/package.json +1 -6
  78. package/src/client/config.ts +1 -1
  79. package/src/components/index.ts +1 -0
  80. package/src/components/viewers/BlockNoteViewer.tsx +27 -0
  81. package/src/components/viewers/BlockNoteViewerContainer.tsx +10 -0
  82. package/src/core/registry/helpStore.ts +2 -19
  83. package/src/features/assistant-message/components/parts/MessageSourcesContainer.tsx +4 -0
  84. package/src/features/assistant-message/components/parts/tabs/ContentsTab.tsx +5 -5
  85. package/src/features/assistant-message/components/parts/tabs/ReferencesTab.tsx +3 -2
  86. package/src/features/help/components/HelpArticleBody.tsx +27 -19
  87. package/src/features/help/components/HelpHint.tsx +20 -11
  88. package/src/features/help/components/HelpSearchResultRow.tsx +16 -6
  89. package/src/features/help/components/HelpSideNav.tsx +54 -19
  90. package/src/features/help/components/__tests__/HelpAskAi.spec.tsx +1 -1
  91. package/src/features/help/components/__tests__/HelpHint.spec.tsx +17 -17
  92. package/src/features/help/components/__tests__/HelpSearchResultRow.spec.tsx +18 -19
  93. package/src/features/help/components/__tests__/HelpSideNav.spec.tsx +20 -22
  94. package/src/features/help/contexts/HelpContext.spec.tsx +1 -3
  95. package/src/features/help/index.ts +3 -6
  96. package/src/features/help/interfaces/help-content-config.interface.ts +0 -5
  97. package/src/features/help/server-entry.ts +6 -15
  98. package/src/features/help/types/help-article.types.ts +2 -4
  99. package/src/features/how-to/HowToModule.ts +14 -1
  100. package/src/features/how-to/components/forms/HowToEditor.tsx +108 -4
  101. package/src/features/how-to/data/HowTo.spec.ts +43 -0
  102. package/src/features/how-to/data/HowTo.ts +47 -4
  103. package/src/features/how-to/data/HowToInterface.ts +14 -1
  104. package/src/features/how-to/data/HowToService.ts +61 -0
  105. package/dist/BlockNoteEditor-3XYBZLWO.js.map +0 -1
  106. package/dist/chunk-3J7RQBF3.js +0 -123
  107. package/dist/chunk-3J7RQBF3.js.map +0 -1
  108. package/dist/chunk-7E3O52U5.js.map +0 -1
  109. package/dist/chunk-CFI4WZ5R.js.map +0 -1
  110. package/dist/chunk-CQID6RCF.mjs +0 -38
  111. package/dist/chunk-CQID6RCF.mjs.map +0 -1
  112. package/dist/chunk-CRTVAQEK.mjs.map +0 -1
  113. package/dist/chunk-MZTKPPET.mjs +0 -123
  114. package/dist/chunk-MZTKPPET.mjs.map +0 -1
  115. package/dist/chunk-UHO3KUUH.js.map +0 -1
  116. package/dist/chunk-YQQHAFBS.js +0 -38
  117. package/dist/chunk-YQQHAFBS.js.map +0 -1
  118. package/dist/features/help/server/createHelpAssetRouteHandler.d.mts +0 -11
  119. package/dist/features/help/server/createHelpAssetRouteHandler.d.ts +0 -11
  120. package/dist/features/help/server/createHelpAssetRouteHandler.js +0 -43
  121. package/dist/features/help/server/createHelpAssetRouteHandler.js.map +0 -1
  122. package/dist/features/help/server/createHelpAssetRouteHandler.mjs +0 -43
  123. package/dist/features/help/server/createHelpAssetRouteHandler.mjs.map +0 -1
  124. package/dist/help-content-config.interface-B9L02u9i.d.mts +0 -50
  125. package/dist/help-content-config.interface-B9L02u9i.d.ts +0 -50
  126. package/src/features/help/components/mdx/Callout.tsx +0 -21
  127. package/src/features/help/components/mdx/EntityRef.tsx +0 -18
  128. package/src/features/help/components/mdx/KeyBinding.tsx +0 -6
  129. package/src/features/help/components/mdx/Related.tsx +0 -33
  130. package/src/features/help/components/mdx/Screenshot.tsx +0 -9
  131. package/src/features/help/components/mdx/Steps.tsx +0 -21
  132. package/src/features/help/components/mdx/Video.tsx +0 -8
  133. package/src/features/help/components/mdx/mdx-server-components.ts +0 -23
  134. package/src/features/help/components/mdx/mdxComponents.ts +0 -9
  135. package/src/features/help/hooks/useHelpArticle.ts +0 -7
  136. package/src/features/help/hooks/useHelpManifest.ts +0 -5
  137. package/src/features/help/server/__tests__/createHelpAssetRouteHandler.spec.ts +0 -43
  138. package/src/features/help/server/createHelpAssetRouteHandler.ts +0 -35
  139. package/src/features/help/server/generateHelpArticleMetadata.ts +0 -18
  140. package/src/features/help/server/generateHelpArticleStaticParams.ts +0 -7
  141. package/src/features/help/server/generateHelpModeStaticParams.ts +0 -5
  142. package/src/features/help/server/getHelpContent.ts +0 -17
  143. package/src/features/help/server/index.ts +0 -8
  144. package/src/features/help/server/serializeHelpArticle.tsx +0 -46
  145. package/src/features/help/utils/__tests__/helpNavigation.spec.ts +0 -70
  146. package/src/features/help/utils/helpNavigation.ts +0 -29
@@ -1,48 +1,56 @@
1
1
  "use client";
2
+
2
3
  import { ReactNode } from "react";
3
4
  import Link from "next/link";
4
5
  import { useFormatter, useTranslations } from "next-intl";
5
- import type { HelpArticle } from "../types/help-article.types";
6
6
  import { usePageUrlGenerator } from "../../../client";
7
- import { useHelp } from "../contexts/HelpContext";
8
- import { articleUrl } from "../utils/articleUrl";
9
- import { prevNextWithinMode } from "../utils/helpNavigation";
10
7
 
11
- export function HelpArticleBody({ article, children }: { article: HelpArticle; children: ReactNode }) {
8
+ type Sibling = { howToType: string; slug: string; title: string };
9
+
10
+ export function HelpArticleBody(props: {
11
+ howToType: string;
12
+ title: string;
13
+ summary?: string;
14
+ updatedAt?: string;
15
+ prev?: Sibling | null;
16
+ next?: Sibling | null;
17
+ children: ReactNode;
18
+ }) {
12
19
  const t = useTranslations();
13
20
  const format = useFormatter();
14
21
  const generateUrl = usePageUrlGenerator();
15
- const { manifest } = useHelp();
16
- const { prev, next } = prevNextWithinMode(manifest, article);
22
+ const { howToType, title, summary, updatedAt, prev, next, children } = props;
17
23
 
18
24
  return (
19
25
  <article className="prose dark:prose-invert max-w-none">
20
26
  <nav className="text-muted-foreground mb-2 text-xs">
21
27
  <Link href={generateUrl({ page: "/help" })}>Help</Link>
22
28
  {" · "}
23
- <Link href={generateUrl({ page: `/help/${article.mode}` })}>{t(`help.modes.${article.mode}`)}</Link>
29
+ <Link href={generateUrl({ page: `/help/${howToType}` })}>{t(`help.modes.${howToType}`)}</Link>
24
30
  </nav>
25
- <h1>{article.title}</h1>
26
- <p className="text-muted-foreground !mt-0 text-base">{article.summary}</p>
31
+ <h1>{title}</h1>
32
+ {summary ? <p className="text-muted-foreground !mt-0 text-base">{summary}</p> : null}
27
33
  {children}
28
34
  <hr className="my-6" />
29
- <div className="text-muted-foreground text-xs">
30
- <span>
31
- {t("help.article.lastUpdated", {
32
- date: format.dateTime(new Date(article.lastUpdated), { dateStyle: "short" }),
33
- })}
34
- </span>
35
- </div>
35
+ {updatedAt ? (
36
+ <div className="text-muted-foreground text-xs">
37
+ <span>
38
+ {t("help.article.lastUpdated", {
39
+ date: format.dateTime(new Date(updatedAt), { dateStyle: "short" }),
40
+ })}
41
+ </span>
42
+ </div>
43
+ ) : null}
36
44
  <div className="mt-4 flex justify-between text-sm">
37
45
  {prev ? (
38
- <Link href={articleUrl(generateUrl, prev)} className="hover:underline">
46
+ <Link href={generateUrl({ page: `/help/${prev.howToType}/${prev.slug}` })} className="hover:underline">
39
47
  ← {t("help.article.previous")}: {prev.title}
40
48
  </Link>
41
49
  ) : (
42
50
  <span />
43
51
  )}
44
52
  {next ? (
45
- <Link href={articleUrl(generateUrl, next)} className="hover:underline">
53
+ <Link href={generateUrl({ page: `/help/${next.howToType}/${next.slug}` })} className="hover:underline">
46
54
  {t("help.article.next")}: {next.title} →
47
55
  </Link>
48
56
  ) : (
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { useMemo, useState } from "react";
2
+ import { useEffect, useMemo, useState } from "react";
3
3
  import { useTranslations } from "next-intl";
4
4
  import Link from "next/link";
5
5
  import { HelpCircleIcon } from "lucide-react";
@@ -13,20 +13,29 @@ import {
13
13
  SheetDescription,
14
14
  } from "../../../shadcnui";
15
15
  import { usePageUrlGenerator } from "../../../client";
16
- import { useHelp } from "../contexts/HelpContext";
17
- import { articleUrl } from "../utils/articleUrl";
18
- import type { HelpArticle } from "../types/help-article.types";
16
+ import { HowToService } from "../../how-to/data/HowToService";
17
+ import type { HowToInterface } from "../../how-to/data/HowToInterface";
19
18
 
20
19
  export function HelpHint({ contextKey }: { contextKey: string }) {
21
20
  const t = useTranslations();
22
21
  const generateUrl = usePageUrlGenerator();
23
- const { manifest } = useHelp();
24
22
  const [open, setOpen] = useState(false);
25
- const [picked, setPicked] = useState<HelpArticle | null>(null);
23
+ const [picked, setPicked] = useState<HowToInterface | null>(null);
24
+
25
+ const [articles, setArticles] = useState<HowToInterface[]>([]);
26
+ useEffect(() => {
27
+ let active = true;
28
+ HowToService.findPublished()
29
+ .then((list) => active && setArticles(list))
30
+ .catch(() => active && setArticles([]));
31
+ return () => {
32
+ active = false;
33
+ };
34
+ }, []);
26
35
 
27
36
  const matches = useMemo(
28
- () => manifest.filter((a) => a.contextualKeys.includes(contextKey) && !a.draft),
29
- [manifest, contextKey],
37
+ () => articles.filter((a) => a.contextualKeys.includes(contextKey) && !a.draft),
38
+ [articles, contextKey],
30
39
  );
31
40
 
32
41
  if (matches.length === 0) return null;
@@ -45,13 +54,13 @@ export function HelpHint({ contextKey }: { contextKey: string }) {
45
54
  </SheetTrigger>
46
55
  <SheetContent side="right" className="w-full max-w-md sm:max-w-lg">
47
56
  <SheetHeader>
48
- <SheetTitle>{active ? active.title : t("help.hint.pickArticle")}</SheetTitle>
57
+ <SheetTitle>{active ? active.name : t("help.hint.pickArticle")}</SheetTitle>
49
58
  <SheetDescription>{active?.summary ?? ""}</SheetDescription>
50
59
  </SheetHeader>
51
60
  {active ? (
52
61
  <div className="mt-4 space-y-3">
53
62
  <p className="text-muted-foreground text-sm">{active.summary}</p>
54
- <Link href={articleUrl(generateUrl, active)} className="text-sm">
63
+ <Link href={generateUrl({ page: `/help/${active.howToType}/${active.slug}` })} className="text-sm">
55
64
  {t("help.hint.viewArticle")}
56
65
  </Link>
57
66
  </div>
@@ -64,7 +73,7 @@ export function HelpHint({ contextKey }: { contextKey: string }) {
64
73
  onClick={() => setPicked(a)}
65
74
  className="hover:bg-muted block w-full rounded px-2 py-1 text-left text-sm"
66
75
  >
67
- <div className="font-medium">{a.title}</div>
76
+ <div className="font-medium">{a.name}</div>
68
77
  <div className="text-muted-foreground text-xs">{a.summary}</div>
69
78
  </button>
70
79
  </li>
@@ -1,10 +1,12 @@
1
1
  "use client";
2
2
  import Link from "next/link";
3
3
  import { useTranslations } from "next-intl";
4
+ import { useEffect, useState } from "react";
4
5
  import { LifeBuoyIcon, ArrowUpRightIcon } from "lucide-react";
5
6
  import { CommandItem } from "../../../shadcnui";
6
7
  import { usePageUrlGenerator } from "../../../client";
7
- import { useHelp } from "../contexts/HelpContext";
8
+ import { HowToService } from "../../how-to/data/HowToService";
9
+ import type { HowToInterface } from "../../how-to/data/HowToInterface";
8
10
 
9
11
  export interface HelpSearchResultRowProps {
10
12
  result: { id: string; name: string; entityType: string };
@@ -14,8 +16,16 @@ export interface HelpSearchResultRowProps {
14
16
  export function HelpSearchResultRow({ result, onSelect }: HelpSearchResultRowProps) {
15
17
  const t = useTranslations();
16
18
  const generateUrl = usePageUrlGenerator();
17
- const { manifest } = useHelp();
18
- const article = manifest.find((a) => a.id === result.id);
19
+ const [article, setArticle] = useState<HowToInterface | null>(null);
20
+ useEffect(() => {
21
+ let active = true;
22
+ HowToService.findOne({ id: result.id })
23
+ .then((a) => active && setArticle(a))
24
+ .catch(() => active && setArticle(null));
25
+ return () => {
26
+ active = false;
27
+ };
28
+ }, [result.id]);
19
29
 
20
30
  if (!article) {
21
31
  return (
@@ -32,16 +42,16 @@ export function HelpSearchResultRow({ result, onSelect }: HelpSearchResultRowPro
32
42
  return (
33
43
  <CommandItem value={result.id} onSelect={onSelect} className="cursor-pointer p-0">
34
44
  <Link
35
- href={generateUrl({ page: `/help/${article.mode}/${article.slug}` })}
45
+ href={generateUrl({ page: `/help/${article.howToType}/${article.slug}` })}
36
46
  className="hover:bg-muted flex w-full items-center gap-3 rounded px-3 py-2"
37
47
  >
38
48
  <div className="bg-muted text-muted-foreground flex h-10 w-10 shrink-0 items-center justify-center rounded-full">
39
49
  <LifeBuoyIcon className="h-5 w-5" />
40
50
  </div>
41
51
  <div className="flex min-w-0 flex-1 flex-col">
42
- <span className="text-foreground truncate text-sm font-semibold">{article.title}</span>
52
+ <span className="text-foreground truncate text-sm font-semibold">{article.name}</span>
43
53
  <span className="text-muted-foreground truncate text-xs">
44
- {t(`help.modes.${article.mode}`)} · {article.summary}
54
+ {t(`help.modes.${article.howToType}`)} · {article.summary}
45
55
  </span>
46
56
  </div>
47
57
  <ArrowUpRightIcon className="text-muted-foreground h-5 w-5 shrink-0" />
@@ -1,24 +1,60 @@
1
1
  "use client";
2
+
3
+ import { useEffect, useMemo, useState } from "react";
2
4
  import { useTranslations } from "next-intl";
3
5
  import Link from "next/link";
4
6
  import { usePathname } from "next/navigation";
5
7
  import { HELP_MODES } from "../types/help-article.types";
6
8
  import { usePageUrlGenerator } from "../../../client";
7
- import { useHelp } from "../contexts/HelpContext";
8
9
  import { useHelpFilter } from "../hooks/useHelpFilter";
9
- import { buildHelpNav } from "../utils/helpNavigation";
10
- import { articleUrl, modeUrl } from "../utils/articleUrl";
10
+ import { HowToService } from "../../how-to/data/HowToService";
11
+ import type { HowToInterface } from "../../how-to/data/HowToInterface";
11
12
 
12
13
  export function HelpSideNav() {
13
14
  const t = useTranslations();
14
15
  const generateUrl = usePageUrlGenerator();
15
16
  const pathname = usePathname();
16
- const { manifest } = useHelp();
17
- const groups = buildHelpNav(manifest);
18
- const allArticles = groups.flatMap((g) => g.articles);
19
- const { query, setQuery, filtered } = useHelpFilter(allArticles);
17
+ const [articles, setArticles] = useState<HowToInterface[]>([]);
18
+
19
+ useEffect(() => {
20
+ let active = true;
21
+ HowToService.findPublished()
22
+ .then((list) => active && setArticles(list))
23
+ .catch(() => active && setArticles([]));
24
+ return () => {
25
+ active = false;
26
+ };
27
+ }, []);
28
+
29
+ // useHelpFilter indexes by title/summary/tags — map HowTo getters into the shape it expects.
30
+ const indexable = useMemo(
31
+ () =>
32
+ articles.map((a) => ({
33
+ id: a.id,
34
+ title: a.name,
35
+ summary: a.summary ?? "",
36
+ tags: a.tags,
37
+ mode: a.howToType ?? "",
38
+ slug: a.slug ?? "",
39
+ })),
40
+ [articles],
41
+ );
42
+ const { query, setQuery, filtered } = useHelpFilter(indexable as any);
20
43
  const filtering = query.trim().length > 0;
21
44
 
45
+ const groups = useMemo(
46
+ () =>
47
+ HELP_MODES.map((mode) => ({
48
+ mode,
49
+ articles: articles
50
+ .filter((a) => a.howToType === mode && !a.draft)
51
+ .sort((x, y) => (x.order ?? 0) - (y.order ?? 0)),
52
+ })),
53
+ [articles],
54
+ );
55
+
56
+ const isActive = (mode: string, slug: string) => pathname.endsWith(`/help/${mode}/${slug}`);
57
+
22
58
  return (
23
59
  <nav aria-label="Help navigation" className="flex h-full flex-col gap-3 overflow-y-auto p-4">
24
60
  <input
@@ -29,17 +65,17 @@ export function HelpSideNav() {
29
65
  className="border-input bg-background w-full rounded-md border px-3 py-2 text-sm"
30
66
  />
31
67
  {filtering ? (
32
- filtered.length === 0 ? (
68
+ (filtered as any[]).length === 0 ? (
33
69
  <p className="text-muted-foreground text-sm">{t("help.sideNav.noMatches")}</p>
34
70
  ) : (
35
71
  <ul className="space-y-1">
36
- {filtered.map((a) => (
72
+ {(filtered as any[]).map((a) => (
37
73
  <li key={a.id}>
38
74
  <Link
39
- href={articleUrl(generateUrl, a)}
75
+ href={generateUrl({ page: `/help/${a.mode}/${a.slug}` })}
40
76
  className={
41
77
  "hover:bg-muted block rounded px-2 py-1 text-sm " +
42
- (pathname.endsWith(`/help/${a.mode}/${a.slug}`) ? "bg-muted font-medium" : "")
78
+ (isActive(a.mode, a.slug) ? "bg-muted font-medium" : "")
43
79
  }
44
80
  >
45
81
  {a.title}
@@ -49,28 +85,27 @@ export function HelpSideNav() {
49
85
  </ul>
50
86
  )
51
87
  ) : (
52
- HELP_MODES.map((mode) => {
53
- const group = groups.find((g) => g.mode === mode)!;
88
+ groups.map((group) => {
54
89
  if (group.articles.length === 0) return null;
55
90
  return (
56
- <div key={mode}>
91
+ <div key={group.mode}>
57
92
  <Link
58
- href={modeUrl(generateUrl, mode)}
93
+ href={generateUrl({ page: `/help/${group.mode}` })}
59
94
  className="text-muted-foreground hover:text-foreground mb-1 block text-xs font-semibold tracking-wider uppercase"
60
95
  >
61
- {t(`help.modes.${mode}`)}
96
+ {t(`help.modes.${group.mode}`)}
62
97
  </Link>
63
98
  <ul className="space-y-0.5">
64
99
  {group.articles.map((a) => (
65
100
  <li key={a.id}>
66
101
  <Link
67
- href={articleUrl(generateUrl, a)}
102
+ href={generateUrl({ page: `/help/${a.howToType}/${a.slug}` })}
68
103
  className={
69
104
  "hover:bg-muted block rounded px-2 py-1 text-sm " +
70
- (pathname.endsWith(`/help/${a.mode}/${a.slug}`) ? "bg-muted font-medium" : "")
105
+ (isActive(a.howToType ?? "", a.slug ?? "") ? "bg-muted font-medium" : "")
71
106
  }
72
107
  >
73
- {a.title}
108
+ {a.name}
74
109
  </Link>
75
110
  </li>
76
111
  ))}
@@ -17,7 +17,7 @@ vi.mock("../HelpAssistantSheet", () => ({
17
17
  function setupConfig() {
18
18
  configureJsonApi({
19
19
  apiUrl: "http://localhost",
20
- helpContent: { manifest: [], namespaceUuid: "00000000-0000-5000-8000-000000000000" },
20
+ helpContent: { brand: { label: "narr8" } },
21
21
  });
22
22
  }
23
23
 
@@ -1,50 +1,50 @@
1
- import { describe, it, expect, beforeEach } from "vitest";
2
- import { render, screen } from "@testing-library/react";
1
+ import { describe, it, expect, beforeEach, vi } from "vitest";
2
+ import { render, screen, waitFor } from "@testing-library/react";
3
3
  import { configureJsonApi } from "../../../../client/config";
4
4
  import { HelpProvider } from "../../contexts/HelpContext";
5
5
  import { HelpHint } from "../HelpHint";
6
+ import { HowToService } from "../../../how-to/data/HowToService";
7
+
8
+ vi.mock("../../../how-to/data/HowToService", () => ({
9
+ HowToService: { findPublished: vi.fn() },
10
+ }));
6
11
 
7
12
  const article = {
8
13
  id: "1",
9
14
  slug: "x",
10
- mode: "how-to",
11
- title: "X",
15
+ howToType: "how-to",
16
+ name: "X",
12
17
  summary: "S",
13
- order: 1,
14
- tags: [],
15
18
  contextualKeys: ["npc.editor"],
16
- aiIndexed: true,
17
19
  draft: false,
18
- contentHash: "h",
19
- path: "how-to/x.mdx",
20
- headings: [],
21
- relatedSlugs: [],
22
- lastUpdated: "2026-01-01T00:00:00Z",
23
- };
20
+ } as any;
24
21
 
25
22
  beforeEach(() => {
23
+ vi.clearAllMocks();
24
+ vi.mocked(HowToService.findPublished).mockResolvedValue([article]);
26
25
  configureJsonApi({
27
26
  apiUrl: "http://localhost",
28
- helpContent: { manifest: [article], namespaceUuid: "00000000-0000-5000-8000-000000000000" },
27
+ helpContent: { brand: { label: "narr8" } },
29
28
  });
30
29
  });
31
30
 
32
31
  describe("HelpHint", () => {
33
- it("renders null when no article matches the contextKey", () => {
32
+ it("renders null when no published article matches the contextKey", async () => {
34
33
  const { container } = render(
35
34
  <HelpProvider>
36
35
  <HelpHint contextKey="nope.absent" />
37
36
  </HelpProvider>,
38
37
  );
38
+ await waitFor(() => expect(HowToService.findPublished).toHaveBeenCalled());
39
39
  expect(container.querySelector("button")).toBeNull();
40
40
  });
41
41
 
42
- it("renders the trigger when at least one article matches", () => {
42
+ it("renders the trigger when at least one published article matches", async () => {
43
43
  render(
44
44
  <HelpProvider>
45
45
  <HelpHint contextKey="npc.editor" />
46
46
  </HelpProvider>,
47
47
  );
48
- expect(screen.getByLabelText(/help on this|hint/i)).toBeTruthy();
48
+ expect(await screen.findByLabelText(/help on this|hint/i)).toBeTruthy();
49
49
  });
50
50
  });
@@ -1,37 +1,34 @@
1
- import { describe, it, expect, beforeEach } from "vitest";
2
- import { render, screen } from "@testing-library/react";
1
+ import { describe, it, expect, beforeEach, vi } from "vitest";
2
+ import { render, screen, waitFor } from "@testing-library/react";
3
3
  import { Command, CommandList } from "../../../../shadcnui";
4
4
  import { configureJsonApi } from "../../../../client/config";
5
5
  import { HelpProvider } from "../../contexts/HelpContext";
6
6
  import { HelpSearchResultRow } from "../HelpSearchResultRow";
7
+ import { HowToService } from "../../../how-to/data/HowToService";
8
+
9
+ vi.mock("../../../how-to/data/HowToService", () => ({
10
+ HowToService: { findOne: vi.fn() },
11
+ }));
7
12
 
8
13
  const article = {
9
14
  id: "abc",
10
15
  slug: "x",
11
- mode: "how-to" as const,
12
- title: "X title",
16
+ howToType: "how-to",
17
+ name: "X title",
13
18
  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
- };
19
+ } as any;
25
20
 
26
21
  beforeEach(() => {
22
+ vi.clearAllMocks();
27
23
  configureJsonApi({
28
24
  apiUrl: "http://localhost",
29
- helpContent: { manifest: [article], namespaceUuid: "00000000-0000-5000-8000-000000000000" },
25
+ helpContent: { brand: { label: "narr8" } },
30
26
  });
31
27
  });
32
28
 
33
29
  describe("HelpSearchResultRow", () => {
34
- it("renders the article title when manifest contains the id", () => {
30
+ it("renders the article title when the service returns the how-to", async () => {
31
+ vi.mocked(HowToService.findOne).mockResolvedValue(article);
35
32
  render(
36
33
  <HelpProvider>
37
34
  <Command>
@@ -41,10 +38,11 @@ describe("HelpSearchResultRow", () => {
41
38
  </Command>
42
39
  </HelpProvider>,
43
40
  );
44
- expect(screen.getByText("X title")).toBeTruthy();
41
+ expect(await screen.findByText("X title")).toBeTruthy();
45
42
  });
46
43
 
47
- it("renders disabled removed-state row when id is missing", () => {
44
+ it("renders disabled removed-state row when the service has no how-to", async () => {
45
+ vi.mocked(HowToService.findOne).mockRejectedValue(new Error("not found"));
48
46
  render(
49
47
  <HelpProvider>
50
48
  <Command>
@@ -54,6 +52,7 @@ describe("HelpSearchResultRow", () => {
54
52
  </Command>
55
53
  </HelpProvider>,
56
54
  );
55
+ await waitFor(() => expect(HowToService.findOne).toHaveBeenCalledWith({ id: "ghost" }));
57
56
  expect(screen.queryByText("X title")).toBeNull();
58
57
  });
59
58
  });
@@ -1,50 +1,48 @@
1
- import { describe, it, expect, beforeEach } from "vitest";
1
+ import { describe, it, expect, beforeEach, vi } from "vitest";
2
2
  import { render, screen, fireEvent } from "@testing-library/react";
3
3
  import { configureJsonApi } from "../../../../client/config";
4
4
  import { HelpProvider } from "../../contexts/HelpContext";
5
5
  import { HelpSideNav } from "../HelpSideNav";
6
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
- };
7
+ // HelpSideNav now sources its list from the public API via
8
+ // HowToService.findPublished() (Plan 3b) instead of the help manifest, so the
9
+ // test mocks the service rather than seeding the manifest.
10
+ vi.mock("../../../how-to/data/HowToService", () => ({
11
+ HowToService: { findPublished: vi.fn() },
12
+ }));
13
+ import { HowToService } from "../../../how-to/data/HowToService";
14
+
15
+ const published = [
16
+ { id: "a", name: "Alpha", summary: "s", tags: [], howToType: "how-to", slug: "a", order: 1, draft: false },
17
+ { id: "b", name: "Beta", summary: "s", tags: [], howToType: "how-to", slug: "b", order: 2, draft: false },
18
+ ];
17
19
 
18
20
  beforeEach(() => {
19
21
  configureJsonApi({
20
22
  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
- },
23
+ helpContent: { brand: { label: "narr8" } },
28
24
  });
25
+ (HowToService.findPublished as ReturnType<typeof vi.fn>).mockResolvedValue(published as any);
29
26
  });
30
27
 
31
28
  describe("HelpSideNav", () => {
32
- it("lists every non-draft article", () => {
29
+ it("lists every non-draft article", async () => {
33
30
  render(
34
31
  <HelpProvider>
35
32
  <HelpSideNav />
36
33
  </HelpProvider>,
37
34
  );
38
- expect(screen.getByText("Alpha")).toBeTruthy();
39
- expect(screen.getByText("Beta")).toBeTruthy();
35
+ expect(await screen.findByText("Alpha")).toBeTruthy();
36
+ expect(await screen.findByText("Beta")).toBeTruthy();
40
37
  });
41
38
 
42
- it("filters by query", () => {
39
+ it("filters by query", async () => {
43
40
  render(
44
41
  <HelpProvider>
45
42
  <HelpSideNav />
46
43
  </HelpProvider>,
47
44
  );
45
+ await screen.findByText("Alpha");
48
46
  fireEvent.change(screen.getByPlaceholderText(/filter/i), { target: { value: "Bet" } });
49
47
  expect(screen.queryByText("Alpha")).toBeNull();
50
48
  expect(screen.getByText("Beta")).toBeTruthy();
@@ -6,8 +6,6 @@ import { HelpProvider, useHelp } from "./HelpContext";
6
6
 
7
7
  describe("HelpProvider / useHelp", () => {
8
8
  const cfg = {
9
- manifest: [],
10
- namespaceUuid: "00000000-0000-5000-8000-000000000000",
11
9
  brand: { logo: "/logo.png", label: "Test", appHref: "/" },
12
10
  };
13
11
 
@@ -18,8 +16,8 @@ describe("HelpProvider / useHelp", () => {
18
16
  it("exposes the configured helpContent via useHelp()", () => {
19
17
  const wrapper = ({ children }: { children: ReactNode }) => <HelpProvider>{children}</HelpProvider>;
20
18
  const { result } = renderHook(() => useHelp(), { wrapper });
21
- expect(result.current.manifest).toBe(cfg.manifest);
22
19
  expect(result.current.brand?.label).toBe("Test");
20
+ expect(result.current.brand?.appHref).toBe("/");
23
21
  });
24
22
 
25
23
  it("throws when useHelp() is called outside HelpProvider", () => {
@@ -2,11 +2,11 @@
2
2
  // Consumed via `@carlonicora/nextjs-jsonapi/help`. Tsup adds a top-level
3
3
  // "use client" directive to the bundled output via clientEntries.
4
4
  //
5
- // Pair this with `@carlonicora/nextjs-jsonapi/help/server` (server) for the
6
- // pure server utilities (serializeHelpArticle, generate*StaticParams, etc.).
5
+ // Pair this with `@carlonicora/nextjs-jsonapi/help/server` for the server-safe
6
+ // HELP_MODES constant + article types (imported by server-component pages).
7
7
  // Consumers compose page shells themselves: import the client primitives below
8
8
  // (HelpProvider, HelpHeader, HelpSideNav, HelpArticleBody, HelpTOC, etc.) and
9
- // the server utilities from /help/server, then assemble pages in app routes.
9
+ // assemble pages in app routes.
10
10
 
11
11
  export { HelpProvider, useHelp } from "./contexts/HelpContext";
12
12
  export { HelpHeader } from "./components/HelpHeader";
@@ -16,9 +16,6 @@ export { HelpTOC } from "./components/HelpTOC";
16
16
  export { HelpHint } from "./components/HelpHint";
17
17
  export { HelpAskAi } from "./components/HelpAskAi";
18
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
19
  export { useHelpFilter } from "./hooks/useHelpFilter";
23
20
  export { articleUrl, modeUrl } from "./utils/articleUrl";
24
21
  export { HELP_I18N_KEYS } from "./i18n-keys";
@@ -1,5 +1,3 @@
1
- import type { HelpArticle, HelpRedirect } from "../types/help-article.types";
2
-
3
1
  export interface HelpBrandConfig {
4
2
  /** URL of the brand logo image (served by the consuming app). */
5
3
  logo?: string;
@@ -10,8 +8,5 @@ export interface HelpBrandConfig {
10
8
  }
11
9
 
12
10
  export interface HelpContentConfig {
13
- manifest: readonly HelpArticle[];
14
- namespaceUuid: string;
15
- redirects?: readonly HelpRedirect[];
16
11
  brand?: HelpBrandConfig;
17
12
  }
@@ -1,20 +1,11 @@
1
- // Server-only public barrel for the help feature.
1
+ // Server-safe public barrel for the help feature.
2
2
  // Consumed via `@carlonicora/nextjs-jsonapi/help/server`. Must NOT receive a
3
- // "use client" directive.
3
+ // "use client" directive — server components (e.g. the /help index + mode pages)
4
+ // import HELP_MODES/HelpMode from here without pulling the client bundle.
4
5
  //
5
- // This barrel exposes only server-safe utilities (no React components). The
6
- // React Server Component / Client Component boundary is enforced by tsup-level
7
- // bundle splitting: server containers cannot directly import client components
8
- // without leaking client-only modules (react-dom/client, hook usage at module
9
- // init) into server scopes. Consumers therefore compose the page shells in
10
- // their own route files, importing client primitives from
11
- // `@carlonicora/nextjs-jsonapi/help` and server utilities from here.
6
+ // Now that help articles are served from the API (HowTo) rather than the MDX
7
+ // manifest, this barrel exposes only the shared mode constant + article types.
8
+ // Client primitives live in `@carlonicora/nextjs-jsonapi/help`.
12
9
 
13
- export { getHelpContent } from "./server/getHelpContent";
14
- export { renderHelpArticle, serializeHelpArticle } from "./server/serializeHelpArticle";
15
- export { generateHelpArticleStaticParams, generateHelpArticleMetadata, generateHelpModeStaticParams } from "./server";
16
- export { findHelpArticle, prevNextWithinMode, buildHelpNav } from "./utils/helpNavigation";
17
- export { setHelpReader } from "../../core/registry/helpStore";
18
- export type { HelpContentConfig, HelpBrandConfig } from "./interfaces/help-content-config.interface";
19
10
  export { HELP_MODES } from "./types/help-article.types";
20
11
  export type { HelpArticle, HelpHeading, HelpMode, HelpRedirect } from "./types/help-article.types";