@tomehq/theme 0.3.2 → 0.3.4

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/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # @tomehq/theme
2
2
 
3
+ ## 0.3.4
4
+
5
+ ### Patch Changes
6
+
7
+ - f03e6c2: Fix page scrolling beyond viewport bounds in all directions. Lock layout to 100vh with overflow hidden on html, body, and root container.
8
+
9
+ ## 0.3.3
10
+
11
+ ### Patch Changes
12
+
13
+ - 062349c: Fix MCP server stdout corruption, add dashboard mobile responsiveness, improve test coverage, and update docs.
14
+
15
+ - Fix: MCP CLI banner no longer writes to stdout, preventing JSON-RPC protocol corruption
16
+ - Fix: API Playground prop wiring and githubSource route crash
17
+ - Feat: MCP server `createMcpServer()` exported for programmatic use with graceful shutdown
18
+ - Feat: Dashboard mobile-responsive layout with media query breakpoints
19
+ - Feat: 13 MCP server integration tests using InMemoryTransport
20
+ - Docs: Add missing typedoc CLI command, fix social link examples, update package list
21
+
22
+ - Updated dependencies [062349c]
23
+ - @tomehq/core@0.3.3
24
+ - @tomehq/components@0.3.3
25
+
3
26
  ## 0.2.8
4
27
 
5
28
  ### Minor Changes
@@ -818,6 +818,11 @@ function Shell({
818
818
  editUrl,
819
819
  lastUpdated,
820
820
  changelogEntries,
821
+ apiManifest,
822
+ apiBaseUrl,
823
+ apiPlayground,
824
+ apiAuth,
825
+ ApiReferenceComponent,
821
826
  onNavigate,
822
827
  allPages,
823
828
  versioning,
@@ -1049,7 +1054,7 @@ function Shell({
1049
1054
  const PageComponent = pageComponent;
1050
1055
  const bannerLink = config2.banner?.link;
1051
1056
  const bannerIsInternal = bannerLink ? bannerLink.startsWith("#") || basePath2 && bannerLink.startsWith(basePath2 + "/") : false;
1052
- return /* @__PURE__ */ jsxs2("div", { dir, className: "tome-grain", style: { ...cssVars, color: "var(--tx)", background: "var(--bg)", fontFamily: "var(--font-body)", minHeight: "100vh", overflow: "hidden" }, children: [
1057
+ return /* @__PURE__ */ jsxs2("div", { dir, className: "tome-grain", style: { ...cssVars, color: "var(--tx)", background: "var(--bg)", fontFamily: "var(--font-body)", height: "100vh", overflow: "hidden" }, children: [
1053
1058
  config2.banner?.text && !bannerDismissed && /* @__PURE__ */ jsxs2("div", { style: {
1054
1059
  display: "flex",
1055
1060
  alignItems: "center",
@@ -1613,12 +1618,15 @@ function Shell({
1613
1618
  /* @__PURE__ */ jsx2("h1", { style: { fontFamily: "var(--font-heading)", fontSize: mobile ? 26 : 38, fontWeight: 400, fontStyle: "italic", lineHeight: 1.15, marginBottom: 8 }, children: pageTitle }),
1614
1619
  isDraft && /* @__PURE__ */ jsx2("div", { "data-testid": "draft-banner", style: { background: "#fef3c7", color: "#92400e", padding: "8px 16px", borderRadius: 6, fontSize: 13, marginBottom: 16 }, children: "Draft \u2014 This page is only visible in development" }),
1615
1620
  pageDescription && /* @__PURE__ */ jsx2("p", { style: { fontSize: 16, color: "var(--tx2)", lineHeight: 1.6, marginBottom: 32 }, children: pageDescription }),
1616
- /* @__PURE__ */ jsx2("div", { style: { borderTop: "1px solid var(--bd)", paddingTop: 28 }, children: changelogEntries && changelogEntries.length > 0 ? /* @__PURE__ */ jsx2(ChangelogView, { entries: changelogEntries }) : PageComponent ? /* @__PURE__ */ jsx2("div", { className: "tome-content", children: /* @__PURE__ */ jsx2(PageComponent, { components: mdxComponents || {} }) }) : /* @__PURE__ */ jsx2(
1617
- "div",
1618
- {
1619
- className: "tome-content",
1620
- ref: htmlContentRef
1621
- }
1621
+ /* @__PURE__ */ jsx2("div", { style: { borderTop: "1px solid var(--bd)", paddingTop: 28 }, children: apiManifest && ApiReferenceComponent ? /* @__PURE__ */ jsx2(ApiReferenceComponent, { manifest: apiManifest, baseUrl: apiBaseUrl, showPlayground: apiPlayground, playgroundAuth: apiAuth }) : (
1622
+ /* TOM-49: Changelog page type */
1623
+ changelogEntries && changelogEntries.length > 0 ? /* @__PURE__ */ jsx2(ChangelogView, { entries: changelogEntries }) : PageComponent ? /* @__PURE__ */ jsx2("div", { className: "tome-content", children: /* @__PURE__ */ jsx2(PageComponent, { components: mdxComponents || {} }) }) : /* @__PURE__ */ jsx2(
1624
+ "div",
1625
+ {
1626
+ className: "tome-content",
1627
+ ref: htmlContentRef
1628
+ }
1629
+ )
1622
1630
  ) }),
1623
1631
  overrides2?.PageFooter ? /* @__PURE__ */ jsx2(
1624
1632
  overrides2.PageFooter,
@@ -1986,6 +1994,9 @@ async function loadPage(id, routes2, loadPageModule2) {
1986
1994
  };
1987
1995
  }
1988
1996
  if (!mod.default) return null;
1997
+ if (mod.isApiReference && mod.apiManifest) {
1998
+ return { isMdx: false, isApiReference: true, ...mod.default, apiManifest: mod.apiManifest };
1999
+ }
1989
2000
  if (mod.isChangelog && mod.changelogEntries) {
1990
2001
  return { isMdx: false, ...mod.default, changelogEntries: mod.changelogEntries };
1991
2002
  }
@@ -2018,7 +2029,8 @@ import {
2018
2029
  FileTree,
2019
2030
  CodeSamples,
2020
2031
  LinkCard,
2021
- CardGrid
2032
+ CardGrid,
2033
+ ApiReference
2022
2034
  } from "@tomehq/components";
2023
2035
  import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
2024
2036
  var MDX_COMPONENTS = {
@@ -2040,6 +2052,9 @@ var MDX_COMPONENTS = {
2040
2052
  var contentStyles = `
2041
2053
  @import url('https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:wght@300;400;500;600;700&family=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;0,700;1,300;1,400;1,700&family=Fira+Code:wght@400;500;600&display=swap');
2042
2054
 
2055
+ html, body { margin: 0; padding: 0; height: 100%; overflow: hidden; }
2056
+ #tome-root { height: 100%; overflow: hidden; }
2057
+
2043
2058
  .tome-content h1 { display: none; }
2044
2059
  .tome-content h2 { font-family: var(--font-body); font-size: 1.35em; font-weight: 600; margin-top: 2em; margin-bottom: 0.5em; display: flex; align-items: center; gap: 10px; letter-spacing: 0.01em; }
2045
2060
  .tome-content h2::before { content: "#"; font-family: var(--font-heading); font-size: 1.2em; font-weight: 300; font-style: italic; color: var(--ac); opacity: 0.5; }
@@ -2495,6 +2510,11 @@ function App() {
2495
2510
  editUrl,
2496
2511
  lastUpdated: currentRoute?.lastUpdated,
2497
2512
  changelogEntries: !pageData?.isMdx ? pageData?.changelogEntries : void 0,
2513
+ apiManifest: !pageData?.isMdx && pageData?.isApiReference ? pageData.apiManifest : void 0,
2514
+ apiBaseUrl: config.api?.baseUrl,
2515
+ apiPlayground: config.api?.playground,
2516
+ apiAuth: config.api?.auth,
2517
+ ApiReferenceComponent: ApiReference,
2498
2518
  onNavigate: navigateTo,
2499
2519
  allPages,
2500
2520
  docContext,
package/dist/entry.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  entry_default
3
- } from "./chunk-2AXAEADQ.js";
3
+ } from "./chunk-QYINBNMJ.js";
4
4
  export {
5
5
  entry_default as default
6
6
  };
package/dist/index.d.ts CHANGED
@@ -96,6 +96,22 @@ interface ShellProps {
96
96
  items: string[];
97
97
  }>;
98
98
  }>;
99
+ apiManifest?: any;
100
+ apiBaseUrl?: string;
101
+ apiPlayground?: boolean;
102
+ apiAuth?: {
103
+ type: "bearer" | "apiKey";
104
+ header?: string;
105
+ };
106
+ ApiReferenceComponent?: React.ComponentType<{
107
+ manifest: any;
108
+ baseUrl?: string;
109
+ showPlayground?: boolean;
110
+ playgroundAuth?: {
111
+ type: "bearer" | "apiKey";
112
+ header?: string;
113
+ };
114
+ }>;
99
115
  onNavigate: (id: string) => void;
100
116
  allPages: Array<{
101
117
  id: string;
@@ -122,7 +138,7 @@ interface ShellProps {
122
138
  PageFooter?: React.ComponentType<any>;
123
139
  };
124
140
  }
125
- declare function Shell({ config, navigation, currentPageId, pageHtml, pageComponent, mdxComponents, pageTitle, pageDescription, headings, tocEnabled, editUrl, lastUpdated, changelogEntries, onNavigate, allPages, versioning, currentVersion, i18n, currentLocale, docContext, basePath, isDraft, dir: dirProp, overrides, }: ShellProps): react_jsx_runtime.JSX.Element;
141
+ declare function Shell({ config, navigation, currentPageId, pageHtml, pageComponent, mdxComponents, pageTitle, pageDescription, headings, tocEnabled, editUrl, lastUpdated, changelogEntries, apiManifest, apiBaseUrl, apiPlayground, apiAuth, ApiReferenceComponent, onNavigate, allPages, versioning, currentVersion, i18n, currentLocale, docContext, basePath, isDraft, dir: dirProp, overrides, }: ShellProps): react_jsx_runtime.JSX.Element;
126
142
 
127
143
  interface AiChatProps {
128
144
  provider: "openai" | "anthropic" | "custom";
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  Shell,
4
4
  THEME_PRESETS,
5
5
  entry_default
6
- } from "./chunk-2AXAEADQ.js";
6
+ } from "./chunk-QYINBNMJ.js";
7
7
  export {
8
8
  AiChat,
9
9
  entry_default as App,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tomehq/theme",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "Tome default theme and React app shell",
5
5
  "type": "module",
6
6
  "main": "./src/index.tsx",
@@ -9,8 +9,8 @@
9
9
  "./entry": "./src/entry.tsx"
10
10
  },
11
11
  "dependencies": {
12
- "@tomehq/components": "0.3.2",
13
- "@tomehq/core": "0.3.2"
12
+ "@tomehq/components": "0.3.3",
13
+ "@tomehq/core": "0.3.3"
14
14
  },
15
15
  "peerDependencies": {
16
16
  "react": "^18.0.0 || ^19.0.0",
@@ -1052,3 +1052,225 @@ describe("Shell RTL support", () => {
1052
1052
  ).not.toThrow();
1053
1053
  });
1054
1054
  });
1055
+
1056
+ // ── API Reference (TOM-19) ──────────────────────────────
1057
+
1058
+ describe("Shell API reference rendering", () => {
1059
+ const mockManifest = {
1060
+ title: "Test API",
1061
+ version: "1.0.0",
1062
+ servers: [{ url: "https://api.example.com" }],
1063
+ tags: [{ name: "Users", description: "User management" }],
1064
+ endpoints: [
1065
+ {
1066
+ method: "GET",
1067
+ path: "/users",
1068
+ summary: "List all users",
1069
+ tags: ["Users"],
1070
+ parameters: [],
1071
+ responses: [{ status: "200", description: "OK" }],
1072
+ },
1073
+ ],
1074
+ };
1075
+
1076
+ function MockApiRef({ manifest, baseUrl, showPlayground, playgroundAuth }: { manifest: any; baseUrl?: string; showPlayground?: boolean; playgroundAuth?: { type: string; header?: string } }) {
1077
+ return (
1078
+ <div data-testid="api-reference">
1079
+ <span data-testid="api-title">{manifest.title}</span>
1080
+ {baseUrl && <span data-testid="api-base-url">{baseUrl}</span>}
1081
+ {showPlayground && <span data-testid="api-playground">playground-enabled</span>}
1082
+ {playgroundAuth && <span data-testid="api-auth">{playgroundAuth.type}</span>}
1083
+ </div>
1084
+ );
1085
+ }
1086
+
1087
+ it("renders ApiReferenceComponent when both apiManifest and component are provided", () => {
1088
+ renderShell({
1089
+ apiManifest: mockManifest,
1090
+ ApiReferenceComponent: MockApiRef,
1091
+ pageHtml: undefined,
1092
+ });
1093
+ expect(screen.getByTestId("api-reference")).toBeInTheDocument();
1094
+ expect(screen.getByTestId("api-title")).toHaveTextContent("Test API");
1095
+ });
1096
+
1097
+ it("passes baseUrl to ApiReferenceComponent", () => {
1098
+ renderShell({
1099
+ apiManifest: mockManifest,
1100
+ ApiReferenceComponent: MockApiRef,
1101
+ apiBaseUrl: "https://api.example.com",
1102
+ pageHtml: undefined,
1103
+ });
1104
+ expect(screen.getByTestId("api-base-url")).toHaveTextContent("https://api.example.com");
1105
+ });
1106
+
1107
+ it("does not render API reference when only apiManifest is provided (no component)", () => {
1108
+ renderShell({
1109
+ apiManifest: mockManifest,
1110
+ pageHtml: "<p>Fallback content</p>",
1111
+ });
1112
+ expect(screen.queryByTestId("api-reference")).not.toBeInTheDocument();
1113
+ });
1114
+
1115
+ it("does not render API reference when only component is provided (no manifest)", () => {
1116
+ renderShell({
1117
+ ApiReferenceComponent: MockApiRef,
1118
+ pageHtml: "<p>Fallback content</p>",
1119
+ });
1120
+ expect(screen.queryByTestId("api-reference")).not.toBeInTheDocument();
1121
+ });
1122
+
1123
+ it("falls back to pageHtml when no apiManifest", () => {
1124
+ const { container } = renderShell({
1125
+ pageHtml: "<p>Regular page content</p>",
1126
+ });
1127
+ expect(container.querySelector(".tome-content")).toBeInTheDocument();
1128
+ expect(screen.queryByTestId("api-reference")).not.toBeInTheDocument();
1129
+ });
1130
+
1131
+ it("API reference takes precedence over changelog entries", () => {
1132
+ const changelogEntries = [
1133
+ { version: "1.0.0", sections: [{ type: "Added", items: ["Feature"] }] },
1134
+ ];
1135
+ renderShell({
1136
+ apiManifest: mockManifest,
1137
+ ApiReferenceComponent: MockApiRef,
1138
+ changelogEntries,
1139
+ pageHtml: undefined,
1140
+ });
1141
+ // API reference should render, not changelog
1142
+ expect(screen.getByTestId("api-reference")).toBeInTheDocument();
1143
+ expect(screen.queryByTestId("changelog-timeline")).not.toBeInTheDocument();
1144
+ });
1145
+
1146
+ it("API reference takes precedence over pageComponent", () => {
1147
+ const PageComp = () => <div data-testid="page-comp">MDX content</div>;
1148
+ renderShell({
1149
+ apiManifest: mockManifest,
1150
+ ApiReferenceComponent: MockApiRef,
1151
+ pageComponent: PageComp,
1152
+ pageHtml: undefined,
1153
+ });
1154
+ expect(screen.getByTestId("api-reference")).toBeInTheDocument();
1155
+ expect(screen.queryByTestId("page-comp")).not.toBeInTheDocument();
1156
+ });
1157
+
1158
+ it("passes showPlayground prop to ApiReferenceComponent", () => {
1159
+ renderShell({
1160
+ apiManifest: mockManifest,
1161
+ ApiReferenceComponent: MockApiRef,
1162
+ apiPlayground: true,
1163
+ pageHtml: undefined,
1164
+ });
1165
+ expect(screen.getByTestId("api-playground")).toHaveTextContent("playground-enabled");
1166
+ });
1167
+
1168
+ it("passes playgroundAuth prop to ApiReferenceComponent", () => {
1169
+ renderShell({
1170
+ apiManifest: mockManifest,
1171
+ ApiReferenceComponent: MockApiRef,
1172
+ apiPlayground: true,
1173
+ apiAuth: { type: "bearer" },
1174
+ pageHtml: undefined,
1175
+ });
1176
+ expect(screen.getByTestId("api-auth")).toHaveTextContent("bearer");
1177
+ });
1178
+
1179
+ it("does not pass playground props when not provided", () => {
1180
+ renderShell({
1181
+ apiManifest: mockManifest,
1182
+ ApiReferenceComponent: MockApiRef,
1183
+ pageHtml: undefined,
1184
+ });
1185
+ expect(screen.getByTestId("api-reference")).toBeInTheDocument();
1186
+ expect(screen.queryByTestId("api-playground")).not.toBeInTheDocument();
1187
+ expect(screen.queryByTestId("api-auth")).not.toBeInTheDocument();
1188
+ });
1189
+ });
1190
+
1191
+ // ── Content link interception ───────────────────────────────
1192
+
1193
+ describe("Content link interception", () => {
1194
+ it("intercepts internal bare page ID links and calls onNavigate", () => {
1195
+ const onNavigate = vi.fn();
1196
+ renderShell({
1197
+ onNavigate,
1198
+ pageHtml: '<p><a href="quickstart">Go to quickstart</a></p>',
1199
+ });
1200
+ const link = screen.getByText("Go to quickstart");
1201
+ fireEvent.click(link);
1202
+ expect(onNavigate).toHaveBeenCalledWith("quickstart");
1203
+ });
1204
+
1205
+ it("intercepts ./relative links and strips the prefix", () => {
1206
+ const onNavigate = vi.fn();
1207
+ renderShell({
1208
+ onNavigate,
1209
+ pageHtml: '<p><a href="./installation">Install</a></p>',
1210
+ });
1211
+ fireEvent.click(screen.getByText("Install"));
1212
+ expect(onNavigate).toHaveBeenCalledWith("installation");
1213
+ });
1214
+
1215
+ it("does not intercept external http links", () => {
1216
+ const onNavigate = vi.fn();
1217
+ renderShell({
1218
+ onNavigate,
1219
+ pageHtml: '<p><a href="https://example.com">External</a></p>',
1220
+ });
1221
+ fireEvent.click(screen.getByText("External"));
1222
+ expect(onNavigate).not.toHaveBeenCalled();
1223
+ });
1224
+
1225
+ it("does not intercept mailto links", () => {
1226
+ const onNavigate = vi.fn();
1227
+ renderShell({
1228
+ onNavigate,
1229
+ pageHtml: '<p><a href="mailto:test@example.com">Email</a></p>',
1230
+ });
1231
+ fireEvent.click(screen.getByText("Email"));
1232
+ expect(onNavigate).not.toHaveBeenCalled();
1233
+ });
1234
+
1235
+ it("does not intercept pure anchor links", () => {
1236
+ const onNavigate = vi.fn();
1237
+ renderShell({
1238
+ onNavigate,
1239
+ pageHtml: '<p><a href="#section-1">Jump to section</a></p>',
1240
+ });
1241
+ fireEvent.click(screen.getByText("Jump to section"));
1242
+ expect(onNavigate).not.toHaveBeenCalled();
1243
+ });
1244
+
1245
+ it("strips basePath prefix from links", () => {
1246
+ const onNavigate = vi.fn();
1247
+ renderShell({
1248
+ onNavigate,
1249
+ basePath: "/docs/",
1250
+ pageHtml: '<p><a href="/docs/quickstart">Go</a></p>',
1251
+ });
1252
+ fireEvent.click(screen.getByText("Go"));
1253
+ expect(onNavigate).toHaveBeenCalledWith("quickstart");
1254
+ });
1255
+
1256
+ it("resolves basePath-only link to index", () => {
1257
+ const onNavigate = vi.fn();
1258
+ renderShell({
1259
+ onNavigate,
1260
+ basePath: "/docs/",
1261
+ pageHtml: '<p><a href="/docs/">Home</a></p>',
1262
+ });
1263
+ fireEvent.click(screen.getByText("Home"));
1264
+ expect(onNavigate).toHaveBeenCalledWith("index");
1265
+ });
1266
+
1267
+ it("resolves empty path to index", () => {
1268
+ const onNavigate = vi.fn();
1269
+ renderShell({
1270
+ onNavigate,
1271
+ pageHtml: '<p><a href="./">Home</a></p>',
1272
+ });
1273
+ fireEvent.click(screen.getByText("Home"));
1274
+ expect(onNavigate).toHaveBeenCalledWith("index");
1275
+ });
1276
+ });
package/src/Shell.tsx CHANGED
@@ -367,6 +367,16 @@ interface ShellProps {
367
367
  editUrl?: string;
368
368
  lastUpdated?: string;
369
369
  changelogEntries?: Array<{ version: string; date?: string; url?: string; sections: Array<{ type: string; items: string[] }> }>;
370
+ apiManifest?: any;
371
+ apiBaseUrl?: string;
372
+ apiPlayground?: boolean;
373
+ apiAuth?: { type: "bearer" | "apiKey"; header?: string };
374
+ ApiReferenceComponent?: React.ComponentType<{
375
+ manifest: any;
376
+ baseUrl?: string;
377
+ showPlayground?: boolean;
378
+ playgroundAuth?: { type: "bearer" | "apiKey"; header?: string };
379
+ }>;
370
380
  onNavigate: (id: string) => void;
371
381
  allPages: Array<{ id: string; title: string; description?: string }>;
372
382
  versioning?: VersioningInfo;
@@ -388,7 +398,8 @@ interface ShellProps {
388
398
 
389
399
  export function Shell({
390
400
  config, navigation, currentPageId, pageHtml, pageComponent, mdxComponents,
391
- pageTitle, pageDescription, headings, tocEnabled = true, editUrl, lastUpdated, changelogEntries, onNavigate, allPages,
401
+ pageTitle, pageDescription, headings, tocEnabled = true, editUrl, lastUpdated, changelogEntries,
402
+ apiManifest, apiBaseUrl, apiPlayground, apiAuth, ApiReferenceComponent, onNavigate, allPages,
392
403
  versioning, currentVersion, i18n, currentLocale, docContext, basePath = "", isDraft, dir: dirProp, overrides,
393
404
  }: ShellProps) {
394
405
  // RTL support: resolve text direction from prop, i18n.localeDirs, or default to "ltr"
@@ -658,7 +669,7 @@ export function Shell({
658
669
  const bannerIsInternal = bannerLink ? (bannerLink.startsWith("#") || (basePath && bannerLink.startsWith(basePath + "/"))) : false;
659
670
 
660
671
  return (
661
- <div dir={dir} className="tome-grain" style={{ ...cssVars as React.CSSProperties, color: "var(--tx)", background: "var(--bg)", fontFamily: "var(--font-body)", minHeight: "100vh", overflow: "hidden" }}>
672
+ <div dir={dir} className="tome-grain" style={{ ...cssVars as React.CSSProperties, color: "var(--tx)", background: "var(--bg)", fontFamily: "var(--font-body)", height: "100vh", overflow: "hidden" }}>
662
673
  {/* Banner */}
663
674
  {config.banner?.text && !bannerDismissed && (
664
675
  <div style={{
@@ -1156,8 +1167,11 @@ export function Shell({
1156
1167
  )}
1157
1168
  {pageDescription && <p style={{ fontSize: 16, color: "var(--tx2)", lineHeight: 1.6, marginBottom: 32 }}>{pageDescription}</p>}
1158
1169
  <div style={{ borderTop: "1px solid var(--bd)", paddingTop: 28 }}>
1159
- {/* TOM-49: Changelog page type */}
1160
- {changelogEntries && changelogEntries.length > 0 ? (
1170
+ {/* TOM-19: API Reference page */}
1171
+ {apiManifest && ApiReferenceComponent ? (
1172
+ <ApiReferenceComponent manifest={apiManifest} baseUrl={apiBaseUrl} showPlayground={apiPlayground} playgroundAuth={apiAuth} />
1173
+ ) : /* TOM-49: Changelog page type */
1174
+ changelogEntries && changelogEntries.length > 0 ? (
1161
1175
  <ChangelogView entries={changelogEntries} />
1162
1176
  ) : PageComponent ? (
1163
1177
  <div className="tome-content">
@@ -0,0 +1,2 @@
1
+ // Stub for virtual:tome/config — overridden by vi.mock in tests
2
+ export default {};
@@ -0,0 +1,2 @@
1
+ // Stub for virtual:tome/doc-context — overridden by vi.mock in tests
2
+ export default "";
@@ -0,0 +1,2 @@
1
+ // Stub for virtual:tome/overrides — overridden by vi.mock in tests
2
+ export default null;
@@ -0,0 +1,4 @@
1
+ // Stub for virtual:tome/page-loader — overridden by vi.mock in tests
2
+ export default async function loadPageModule(_id: string): Promise<any> {
3
+ return { default: null };
4
+ }
@@ -0,0 +1,5 @@
1
+ // Stub for virtual:tome/routes — overridden by vi.mock in tests
2
+ export const routes: any[] = [];
3
+ export const navigation: any[] = [];
4
+ export const versions: any = null;
5
+ export const i18n: any = null;
@@ -271,6 +271,82 @@ describe("loadPage", () => {
271
271
  expect(page!.isMdx).toBe(false);
272
272
  });
273
273
 
274
+ it("loads an API reference page with manifest", async () => {
275
+ const apiManifest = {
276
+ title: "My API",
277
+ servers: [{ url: "https://api.example.com" }],
278
+ tags: [{ name: "Users", description: "User endpoints" }],
279
+ endpoints: [],
280
+ };
281
+ const mockLoader = vi.fn().mockResolvedValue({
282
+ default: {
283
+ html: "",
284
+ frontmatter: { title: "API Reference" },
285
+ headings: [{ depth: 2, text: "Users", id: "users" }],
286
+ },
287
+ isApiReference: true,
288
+ apiManifest,
289
+ });
290
+ const apiRoutes = [...routesWithMeta, { id: "api-reference", urlPath: "/api", isMdx: false }];
291
+ const page = await loadPage("api-reference", apiRoutes, mockLoader);
292
+ expect(page).not.toBeNull();
293
+ expect(page!.isMdx).toBe(false);
294
+ expect((page as any).isApiReference).toBe(true);
295
+ expect((page as any).apiManifest).toEqual(apiManifest);
296
+ expect(page!.frontmatter.title).toBe("API Reference");
297
+ });
298
+
299
+ it("does not treat page as API reference when isApiReference is false", async () => {
300
+ const mockLoader = vi.fn().mockResolvedValue({
301
+ default: {
302
+ html: "<p>Regular</p>",
303
+ frontmatter: { title: "Regular Page" },
304
+ headings: [],
305
+ },
306
+ isApiReference: false,
307
+ });
308
+ const page = await loadPage("quickstart", routesWithMeta, mockLoader);
309
+ expect(page).not.toBeNull();
310
+ expect(page!.isMdx).toBe(false);
311
+ expect((page as any).isApiReference).toBeFalsy();
312
+ expect((page as any).apiManifest).toBeUndefined();
313
+ });
314
+
315
+ it("does not treat page as API reference when apiManifest is missing", async () => {
316
+ const mockLoader = vi.fn().mockResolvedValue({
317
+ default: {
318
+ html: "",
319
+ frontmatter: { title: "API Reference" },
320
+ headings: [],
321
+ },
322
+ isApiReference: true,
323
+ // apiManifest is undefined
324
+ });
325
+ const page = await loadPage("api-reference", routesWithMeta, mockLoader);
326
+ expect(page).not.toBeNull();
327
+ // Without apiManifest, falls through to regular page handling
328
+ expect((page as any).isApiReference).toBeFalsy();
329
+ });
330
+
331
+ it("API reference page preserves headings from module", async () => {
332
+ const headings = [
333
+ { depth: 2, text: "Users", id: "users" },
334
+ { depth: 2, text: "Posts", id: "posts" },
335
+ ];
336
+ const mockLoader = vi.fn().mockResolvedValue({
337
+ default: {
338
+ html: "",
339
+ frontmatter: { title: "API Reference" },
340
+ headings,
341
+ },
342
+ isApiReference: true,
343
+ apiManifest: { endpoints: [], tags: [], servers: [] },
344
+ });
345
+ const apiRoutes = [...routesWithMeta, { id: "api-reference", urlPath: "/api", isMdx: false }];
346
+ const page = await loadPage("api-reference", apiRoutes, mockLoader);
347
+ expect(page!.headings).toEqual(headings);
348
+ });
349
+
274
350
  it("does not treat non-MDX route as MDX even if mod.meta exists", async () => {
275
351
  // A markdown route shouldn't be treated as MDX even if the module has meta
276
352
  const mockLoader = vi.fn().mockResolvedValue({
@@ -7,6 +7,7 @@ export interface HtmlPage {
7
7
  frontmatter: { title: string; description?: string; toc?: boolean; type?: string };
8
8
  headings: Array<{ depth: number; text: string; id: string }>;
9
9
  changelogEntries?: Array<{ version: string; date?: string; url?: string; sections: Array<{ type: string; items: string[] }> }>;
10
+ isApiReference?: false;
10
11
  }
11
12
 
12
13
  export interface MdxPage {
@@ -14,9 +15,20 @@ export interface MdxPage {
14
15
  component: React.ComponentType<{ components?: Record<string, React.ComponentType> }>;
15
16
  frontmatter: { title: string; description?: string; toc?: boolean; type?: string };
16
17
  headings: Array<{ depth: number; text: string; id: string }>;
18
+ isApiReference?: false;
17
19
  }
18
20
 
19
- export type LoadedPage = HtmlPage | MdxPage;
21
+ export interface ApiReferencePage {
22
+ isMdx: false;
23
+ isApiReference: true;
24
+ html: string;
25
+ frontmatter: { title: string; description?: string; toc?: boolean; type?: string };
26
+ headings: Array<{ depth: number; text: string; id: string }>;
27
+ changelogEntries?: undefined;
28
+ apiManifest: any;
29
+ }
30
+
31
+ export type LoadedPage = HtmlPage | MdxPage | ApiReferencePage;
20
32
 
21
33
  // ── EDIT URL COMPUTATION ──────────────────────────────────
22
34
  export interface EditLinkConfig {
@@ -82,6 +94,11 @@ export async function loadPage(
82
94
  // Regular .md page — mod.default is { html, frontmatter, headings }
83
95
  if (!mod.default) return null;
84
96
 
97
+ // API reference page (synthetic route from OpenAPI spec)
98
+ if (mod.isApiReference && mod.apiManifest) {
99
+ return { isMdx: false, isApiReference: true, ...mod.default, apiManifest: mod.apiManifest };
100
+ }
101
+
85
102
  // Changelog page type
86
103
  if (mod.isChangelog && mod.changelogEntries) {
87
104
  return { isMdx: false, ...mod.default, changelogEntries: mod.changelogEntries };