@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 +23 -0
- package/dist/{chunk-2AXAEADQ.js → chunk-QYINBNMJ.js} +28 -8
- package/dist/entry.js +1 -1
- package/dist/index.d.ts +17 -1
- package/dist/index.js +1 -1
- package/package.json +3 -3
- package/src/Shell.test.tsx +222 -0
- package/src/Shell.tsx +18 -4
- package/src/__virtual_stubs/config.ts +2 -0
- package/src/__virtual_stubs/doc-context.ts +2 -0
- package/src/__virtual_stubs/overrides.ts +2 -0
- package/src/__virtual_stubs/page-loader.ts +4 -0
- package/src/__virtual_stubs/routes.ts +5 -0
- package/src/entry-helpers.test.ts +76 -0
- package/src/entry-helpers.ts +18 -1
- package/src/entry.test.tsx +695 -0
- package/src/entry.tsx +9 -0
- package/vitest.config.ts +31 -1
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)",
|
|
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:
|
|
1617
|
-
|
|
1618
|
-
{
|
|
1619
|
-
|
|
1620
|
-
|
|
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
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tomehq/theme",
|
|
3
|
-
"version": "0.3.
|
|
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.
|
|
13
|
-
"@tomehq/core": "0.3.
|
|
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",
|
package/src/Shell.test.tsx
CHANGED
|
@@ -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,
|
|
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)",
|
|
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-
|
|
1160
|
-
{
|
|
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">
|
|
@@ -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({
|
package/src/entry-helpers.ts
CHANGED
|
@@ -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
|
|
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 };
|