@tomehq/theme 0.3.1 → 0.3.3
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 +17 -0
- package/dist/{chunk-YXKONM3A.js → chunk-MSXVVBDW.js} +493 -143
- package/dist/entry.js +1 -1
- package/dist/index.d.ts +37 -1
- package/dist/index.js +1 -1
- package/package.json +5 -5
- package/src/Shell.test.tsx +405 -0
- package/src/Shell.tsx +248 -24
- 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 +179 -4
- package/src/global.d.ts +11 -0
- package/vitest.config.ts +31 -1
- package/dist/chunk-2APCPR2Y.js +0 -2110
- package/dist/chunk-37JI6XGT.js +0 -1720
- package/dist/chunk-3A2LPGUL.js +0 -1991
- package/dist/chunk-3I2QTWTW.js +0 -1948
- package/dist/chunk-45M5UIAB.js +0 -2110
- package/dist/chunk-462AGU3S.js +0 -1959
- package/dist/chunk-7MUTU5D4.js +0 -1720
- package/dist/chunk-ABNPB6BB.js +0 -2133
- package/dist/chunk-BZGWSKT2.js +0 -573
- package/dist/chunk-CMQCNCSY.js +0 -2127
- package/dist/chunk-CTPOZMMK.js +0 -1703
- package/dist/chunk-DO544M3G.js +0 -1702
- package/dist/chunk-DPKZBFQP.js +0 -1777
- package/dist/chunk-EK7PZUEB.js +0 -2147
- package/dist/chunk-FMOLIHQF.js +0 -2182
- package/dist/chunk-FWBTK5TL.js +0 -1444
- package/dist/chunk-GDQIBNX5.js +0 -1962
- package/dist/chunk-GHQ2MODM.js +0 -2127
- package/dist/chunk-GR2WCRGK.js +0 -2182
- package/dist/chunk-HNLKDQ64.js +0 -2139
- package/dist/chunk-INUMUXN5.js +0 -2095
- package/dist/chunk-IW3NHNOQ.js +0 -2187
- package/dist/chunk-JA4PMX6M.js +0 -1500
- package/dist/chunk-JSPFS7G5.js +0 -2102
- package/dist/chunk-JZRT4WNC.js +0 -1441
- package/dist/chunk-KQBY2JDB.js +0 -2112
- package/dist/chunk-LIMYFTPC.js +0 -1468
- package/dist/chunk-MEP7P6A7.js +0 -1500
- package/dist/chunk-NOZBIES7.js +0 -1948
- package/dist/chunk-O4GH3KYX.js +0 -1712
- package/dist/chunk-OEXM3BEC.js +0 -1702
- package/dist/chunk-Q7PYTVW3.js +0 -1771
- package/dist/chunk-QCWZYABW.js +0 -1468
- package/dist/chunk-RDF25WB2.js +0 -2085
- package/dist/chunk-RKTT3ZEX.js +0 -1500
- package/dist/chunk-S47BRMNQ.js +0 -1715
- package/dist/chunk-S4ZH5F56.js +0 -1949
- package/dist/chunk-SRD7NJHS.js +0 -1949
- package/dist/chunk-SWFYJO5H.js +0 -2187
- package/dist/chunk-TQDWPSTO.js +0 -2087
- package/dist/chunk-TTRXRPP6.js +0 -1941
- package/dist/chunk-UKYFJSUA.js +0 -509
- package/dist/chunk-VKEQHP2E.js +0 -2133
- package/dist/chunk-VUT2FMSI.js +0 -1937
- package/dist/chunk-VVCC5JHK.js +0 -1949
- package/dist/chunk-W732TVBK.js +0 -1944
- package/dist/chunk-X4VQYPKO.js +0 -1768
- package/dist/chunk-YZ3P3TNS.js +0 -1760
package/src/Shell.tsx
CHANGED
|
@@ -212,6 +212,27 @@ const GlobeIcon = () => <Icon d="M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18ZM3.6 9h16.
|
|
|
212
212
|
// ── TOP NAV EXTERNAL LINK ICON ────────────────────────────
|
|
213
213
|
const ExtLinkIcon = () => <Icon d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6M15 3h6v6M10 14L21 3" size={11} />;
|
|
214
214
|
|
|
215
|
+
// ── SOCIAL LINK ICONS ──────────────────────────────────
|
|
216
|
+
const SOCIAL_ICON_PATHS: Record<string, string> = {
|
|
217
|
+
github: "M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z",
|
|
218
|
+
twitter: "M12.6.75h2.454l-5.36 6.142L16 15.25h-4.937l-3.867-5.07-4.425 5.07H.316l5.733-6.57L0 .75h5.063l3.495 4.633L12.601.75Zm-.86 13.028h1.36L4.323 2.145H2.865l8.875 11.633Z",
|
|
219
|
+
discord: "M13.545 2.907a13.227 13.227 0 00-3.257-1.011.05.05 0 00-.052.025c-.141.25-.297.577-.406.833a12.19 12.19 0 00-3.658 0 8.258 8.258 0 00-.412-.833.051.051 0 00-.052-.025c-1.125.194-2.22.534-3.257 1.011a.041.041 0 00-.021.018C.356 6.024-.213 9.047.066 12.032c.001.014.01.028.021.037a13.276 13.276 0 003.995 2.02.05.05 0 00.056-.019c.308-.42.582-.863.818-1.329a.05.05 0 00-.028-.07 8.735 8.735 0 01-1.248-.595.05.05 0 01-.005-.083c.084-.063.168-.129.248-.195a.05.05 0 01.051-.007c2.619 1.196 5.454 1.196 8.041 0a.052.052 0 01.053.007c.08.066.164.132.248.195a.051.051 0 01-.004.085c-.399.232-.813.431-1.249.594a.05.05 0 00-.03.07c.24.465.515.909.817 1.329a.05.05 0 00.056.019 13.235 13.235 0 004.001-2.02.049.049 0 00.021-.037c.334-3.451-.559-6.449-2.366-9.106a.034.034 0 00-.02-.019z",
|
|
220
|
+
linkedin: "M0 1.146C0 .513.526 0 1.175 0h13.65C15.474 0 16 .513 16 1.146v13.708c0 .633-.526 1.146-1.175 1.146H1.175C.526 16 0 15.487 0 14.854V1.146zm4.943 12.248V6.169H2.542v7.225h2.401zm-1.2-8.212c.837 0 1.358-.554 1.358-1.248-.015-.709-.52-1.248-1.342-1.248-.822 0-1.359.54-1.359 1.248 0 .694.521 1.248 1.327 1.248h.016zm4.908 8.212V9.359c0-.216.016-.432.08-.586.173-.431.568-.878 1.232-.878.869 0 1.216.662 1.216 1.634v3.865h2.401V9.25c0-2.22-1.184-3.252-2.764-3.252-1.274 0-1.845.7-2.165 1.193v.025h-.016a5.54 5.54 0 01.016-.025V6.169h-2.4c.03.678 0 7.225 0 7.225h2.4z",
|
|
221
|
+
youtube: "M8.051 1.999h.089c.822.003 4.987.033 6.11.335a2.01 2.01 0 011.415 1.42c.101.38.172.883.22 1.402l.01.104.022.26.008.104c.065.914.073 1.77.074 1.957v.075c-.001.194-.01 1.108-.082 2.06l-.008.105-.009.104c-.05.572-.124 1.14-.235 1.558a2.007 2.007 0 01-1.415 1.42c-1.16.312-5.569.334-6.18.335h-.142c-.309 0-1.587-.006-2.927-.052l-.17-.006-.087-.004-.171-.007-.171-.007c-1.11-.049-2.167-.128-2.654-.26a2.007 2.007 0 01-1.415-1.419c-.111-.417-.185-.986-.235-1.558L.09 9.82l-.008-.104A31.4 31.4 0 010 7.68v-.123c.002-.215.01-.958.064-1.778l.007-.103.003-.052.008-.104.022-.26.01-.104c.048-.519.119-1.023.22-1.402a2.007 2.007 0 011.415-1.42c.487-.13 1.544-.21 2.654-.26l.17-.007.172-.006.086-.003.171-.007A99.788 99.788 0 017.858 2h.193zM6.4 5.209v4.818l4.157-2.408L6.4 5.209z",
|
|
222
|
+
mastodon: "M11.19 12.195c2.016-.24 3.77-1.475 3.99-2.603.348-1.778.32-4.339.32-4.339 0-3.47-2.286-4.488-2.286-4.488C12.062.238 10.083.017 8.027 0h-.05C5.92.017 3.942.238 2.79.765 2.79.765.504 1.783.504 5.253c-.005.995-.01 2.19.013 3.44.075 4.21.56 8.354 3.383 9.386 1.302.476 2.418.576 3.317.507 1.628-.125 2.541-.8 2.541-.8l-.054-1.182s-1.163.366-2.47.322c-1.293-.044-2.658-.138-2.867-1.716a3.23 3.23 0 01-.028-.465s1.27.31 2.879.384c.984.045 1.905-.058 2.842-.17zM13 8.59V5.319c0-.67-.17-1.2-.507-1.592-.348-.4-.806-.605-1.373-.605-.656 0-1.154.252-1.486.756L9.2 4.595l-.434-.717c-.332-.504-.83-.756-1.486-.756-.567 0-1.025.204-1.373.605-.338.392-.507.923-.507 1.592V8.59h1.69V5.468c0-.67.285-1.012.855-1.012.63 0 .946.404.946 1.204V7.11h1.682V5.66c0-.8.316-1.204.946-1.204.57 0 .855.342.855 1.012V8.59H13z",
|
|
223
|
+
bluesky: "M3.468 1.948C5.303 3.325 7.276 6.118 8 7.616c.724-1.498 2.697-4.29 4.532-5.668C13.855 1.013 16 .638 16 3.14c0 .5-.286 4.2-.454 4.8-.585 2.093-2.716 2.628-4.544 2.305 3.195.564 4.007 2.433 2.25 4.302-3.337 3.548-4.8-1.244-5.252-2.547 0 0-.116-.334-.166-.334h.332C8.166 11.666 8.05 12 8.05 12c-.452 1.303-1.916 6.095-5.252 2.547-1.756-1.869-.946-3.738 2.25-4.302-1.829.323-3.96-.212-4.544-2.305C.336 7.34.05 3.64.05 3.14.05.638 2.195 1.013 3.468 1.948z",
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const SocialIcon = ({ platform, customIcon }: { platform: string; customIcon?: string }) => {
|
|
227
|
+
const d = platform === "custom" && customIcon ? customIcon : SOCIAL_ICON_PATHS[platform];
|
|
228
|
+
if (!d) return null;
|
|
229
|
+
return (
|
|
230
|
+
<svg width={16} height={16} viewBox="0 0 16 16" fill="currentColor">
|
|
231
|
+
<path d={d} />
|
|
232
|
+
</svg>
|
|
233
|
+
);
|
|
234
|
+
};
|
|
235
|
+
|
|
215
236
|
// ── SHELL COMPONENT ──────────────────────────────────────
|
|
216
237
|
export interface VersioningInfo {
|
|
217
238
|
current: string;
|
|
@@ -222,6 +243,7 @@ export interface I18nInfo {
|
|
|
222
243
|
defaultLocale: string;
|
|
223
244
|
locales: string[];
|
|
224
245
|
localeNames?: Record<string, string>;
|
|
246
|
+
localeDirs?: Record<string, "ltr" | "rtl">;
|
|
225
247
|
}
|
|
226
248
|
|
|
227
249
|
// ── CHANGELOG VIEW (TOM-49) ─────────────────────────────
|
|
@@ -290,6 +312,34 @@ function ChangelogView({ entries }: { entries: ChangelogViewEntry[] }) {
|
|
|
290
312
|
);
|
|
291
313
|
}
|
|
292
314
|
|
|
315
|
+
// ── BREADCRUMBS ─────────────────────────────────────────
|
|
316
|
+
type BreadcrumbItem = { label: string; href: string | null };
|
|
317
|
+
|
|
318
|
+
function getBreadcrumbs(
|
|
319
|
+
navigation: Array<{ section: string; pages: Array<{ title: string; id: string; urlPath: string }> }>,
|
|
320
|
+
currentPageId: string,
|
|
321
|
+
pageTitle: string,
|
|
322
|
+
): BreadcrumbItem[] {
|
|
323
|
+
if (currentPageId === "index") return [];
|
|
324
|
+
|
|
325
|
+
for (const section of navigation) {
|
|
326
|
+
const found = section.pages.find(p => p.id === currentPageId);
|
|
327
|
+
if (found) {
|
|
328
|
+
const crumbs: BreadcrumbItem[] = [];
|
|
329
|
+
// Section label — link to first page in section
|
|
330
|
+
const firstPage = section.pages[0];
|
|
331
|
+
crumbs.push({
|
|
332
|
+
label: section.section,
|
|
333
|
+
href: firstPage ? firstPage.urlPath : null,
|
|
334
|
+
});
|
|
335
|
+
// Current page (last crumb, not a link)
|
|
336
|
+
crumbs.push({ label: pageTitle, href: null });
|
|
337
|
+
return crumbs;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return [];
|
|
341
|
+
}
|
|
342
|
+
|
|
293
343
|
interface ShellProps {
|
|
294
344
|
config: {
|
|
295
345
|
name: string;
|
|
@@ -299,11 +349,12 @@ interface ShellProps {
|
|
|
299
349
|
toc?: { enabled?: boolean; depth?: number };
|
|
300
350
|
topNav?: Array<{ label: string; href: string }>;
|
|
301
351
|
banner?: { text: string; link?: string; dismissible?: boolean };
|
|
352
|
+
socialLinks?: Array<{ platform: string; url: string; label?: string; icon?: string }>;
|
|
302
353
|
[key: string]: unknown;
|
|
303
354
|
};
|
|
304
355
|
navigation: Array<{
|
|
305
356
|
section: string;
|
|
306
|
-
pages: Array<{ title: string; id: string; urlPath: string; icon?: string }>;
|
|
357
|
+
pages: Array<{ title: string; id: string; urlPath: string; icon?: string; badge?: { text: string; variant: string } }>;
|
|
307
358
|
}>;
|
|
308
359
|
currentPageId: string;
|
|
309
360
|
pageHtml?: string;
|
|
@@ -316,6 +367,16 @@ interface ShellProps {
|
|
|
316
367
|
editUrl?: string;
|
|
317
368
|
lastUpdated?: string;
|
|
318
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
|
+
}>;
|
|
319
380
|
onNavigate: (id: string) => void;
|
|
320
381
|
allPages: Array<{ id: string; title: string; description?: string }>;
|
|
321
382
|
versioning?: VersioningInfo;
|
|
@@ -324,13 +385,28 @@ interface ShellProps {
|
|
|
324
385
|
currentLocale?: string;
|
|
325
386
|
docContext?: Array<{ id: string; title: string; content: string }>;
|
|
326
387
|
basePath?: string;
|
|
388
|
+
isDraft?: boolean;
|
|
389
|
+
dir?: "ltr" | "rtl";
|
|
390
|
+
overrides?: {
|
|
391
|
+
Header?: React.ComponentType<any>;
|
|
392
|
+
Footer?: React.ComponentType<any>;
|
|
393
|
+
Sidebar?: React.ComponentType<any>;
|
|
394
|
+
Toc?: React.ComponentType<any>;
|
|
395
|
+
PageFooter?: React.ComponentType<any>;
|
|
396
|
+
};
|
|
327
397
|
}
|
|
328
398
|
|
|
329
399
|
export function Shell({
|
|
330
400
|
config, navigation, currentPageId, pageHtml, pageComponent, mdxComponents,
|
|
331
|
-
pageTitle, pageDescription, headings, tocEnabled = true, editUrl, lastUpdated, changelogEntries,
|
|
332
|
-
|
|
401
|
+
pageTitle, pageDescription, headings, tocEnabled = true, editUrl, lastUpdated, changelogEntries,
|
|
402
|
+
apiManifest, apiBaseUrl, apiPlayground, apiAuth, ApiReferenceComponent, onNavigate, allPages,
|
|
403
|
+
versioning, currentVersion, i18n, currentLocale, docContext, basePath = "", isDraft, dir: dirProp, overrides,
|
|
333
404
|
}: ShellProps) {
|
|
405
|
+
// RTL support: resolve text direction from prop, i18n.localeDirs, or default to "ltr"
|
|
406
|
+
const resolvedLocale = currentLocale || i18n?.defaultLocale || "en";
|
|
407
|
+
const dir: "ltr" | "rtl" = dirProp || i18n?.localeDirs?.[resolvedLocale] || "ltr";
|
|
408
|
+
const isRtl = dir === "rtl";
|
|
409
|
+
|
|
334
410
|
const themeMode = config.theme?.mode || "auto";
|
|
335
411
|
|
|
336
412
|
// TOM-12: Initialize dark mode from config.theme.mode + system preference
|
|
@@ -571,6 +647,9 @@ export function Shell({
|
|
|
571
647
|
const prev = idx > 0 ? allNavPages[idx - 1] : null;
|
|
572
648
|
const next = idx < allNavPages.length - 1 ? allNavPages[idx + 1] : null;
|
|
573
649
|
|
|
650
|
+
// Breadcrumbs
|
|
651
|
+
const breadcrumbs = getBreadcrumbs(navigation, currentPageId, pageTitle);
|
|
652
|
+
|
|
574
653
|
const togSec = (s: string) => setExpanded(p => p.includes(s) ? p.filter(x => x !== s) : [...p, s]);
|
|
575
654
|
|
|
576
655
|
const cssVars: Record<string, string> = {
|
|
@@ -590,7 +669,7 @@ export function Shell({
|
|
|
590
669
|
const bannerIsInternal = bannerLink ? (bannerLink.startsWith("#") || (basePath && bannerLink.startsWith(basePath + "/"))) : false;
|
|
591
670
|
|
|
592
671
|
return (
|
|
593
|
-
<div 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)", minHeight: "100vh", overflow: "hidden" }}>
|
|
594
673
|
{/* Banner */}
|
|
595
674
|
{config.banner?.text && !bannerDismissed && (
|
|
596
675
|
<div style={{
|
|
@@ -653,7 +732,7 @@ export function Shell({
|
|
|
653
732
|
/>
|
|
654
733
|
) : null}
|
|
655
734
|
|
|
656
|
-
<div style={{ display: "flex", flex: 1, height: config.banner?.text && !bannerDismissed ? "calc(100vh - 32px)" : "100vh" }}>
|
|
735
|
+
<div style={{ display: "flex", flexDirection: isRtl ? "row-reverse" : "row", flex: 1, height: config.banner?.text && !bannerDismissed ? "calc(100vh - 32px)" : "100vh" }}>
|
|
657
736
|
{/* Mobile sidebar backdrop */}
|
|
658
737
|
{mobile && sbOpen && (
|
|
659
738
|
<div onClick={() => setSb(false)} style={{
|
|
@@ -662,12 +741,25 @@ export function Shell({
|
|
|
662
741
|
}} />
|
|
663
742
|
)}
|
|
664
743
|
{/* Sidebar */}
|
|
744
|
+
{overrides?.Sidebar ? (
|
|
745
|
+
<overrides.Sidebar
|
|
746
|
+
config={config}
|
|
747
|
+
navigation={navigation}
|
|
748
|
+
currentPageId={currentPageId}
|
|
749
|
+
onNavigate={onNavigate}
|
|
750
|
+
mobile={mobile}
|
|
751
|
+
sbOpen={sbOpen}
|
|
752
|
+
setSbOpen={setSb}
|
|
753
|
+
versioning={versioning}
|
|
754
|
+
currentVersion={currentVersion}
|
|
755
|
+
/>
|
|
756
|
+
) : (
|
|
665
757
|
<aside style={{
|
|
666
758
|
width: sbOpen ? 270 : 0, minWidth: sbOpen ? 270 : 0,
|
|
667
|
-
background: "var(--sbBg)", borderRight: "1px solid var(--bd)",
|
|
759
|
+
background: "var(--sbBg)", [isRtl ? "borderLeft" : "borderRight"]: "1px solid var(--bd)",
|
|
668
760
|
display: "flex", flexDirection: "column",
|
|
669
761
|
transition: "width .2s, min-width .2s", overflow: "hidden",
|
|
670
|
-
...(mobile ? { position: "fixed" as const, top: 0, left: 0, bottom: 0, zIndex: 201 } : {}),
|
|
762
|
+
...(mobile ? { position: "fixed" as const, top: 0, [isRtl ? "right" : "left"]: 0, bottom: 0, zIndex: 201 } : {}),
|
|
671
763
|
}}>
|
|
672
764
|
<a href="/" style={{ padding: "18px 20px", display: "flex", alignItems: "baseline", gap: 6, borderBottom: "1px solid var(--bd)", textDecoration: "none", color: "inherit" }}>
|
|
673
765
|
<span style={{ fontFamily: "var(--font-heading)", fontSize: 22, fontWeight: 700, fontStyle: "italic" }}>
|
|
@@ -683,7 +775,7 @@ export function Shell({
|
|
|
683
775
|
padding: "8px 12px", cursor: "pointer", color: "var(--txM)", fontSize: 12.5,
|
|
684
776
|
fontFamily: "var(--font-body)",
|
|
685
777
|
}}>
|
|
686
|
-
<SearchIcon /><span style={{ flex: 1, textAlign: "left" }}>Search...</span>
|
|
778
|
+
<SearchIcon /><span style={{ flex: 1, textAlign: isRtl ? "right" : "left" }}>Search...</span>
|
|
687
779
|
<kbd style={{ fontFamily: "var(--font-code)", fontSize: 9, background: "var(--sf)", border: "1px solid var(--bd)", borderRadius: 2, padding: "2px 6px" }}>{"\u2318K"}</kbd>
|
|
688
780
|
</button>
|
|
689
781
|
</div>
|
|
@@ -699,21 +791,38 @@ export function Shell({
|
|
|
699
791
|
}}>
|
|
700
792
|
{expanded.includes(sec.section) ? <ChevDown /> : <ChevRight />}{sec.section}
|
|
701
793
|
</button>
|
|
702
|
-
{expanded.includes(sec.section) && <div style={{ marginLeft: 8, borderLeft: "1px solid var(--bd)", paddingLeft: 0 }}>
|
|
794
|
+
{expanded.includes(sec.section) && <div style={{ [isRtl ? "marginRight" : "marginLeft"]: 8, [isRtl ? "borderRight" : "borderLeft"]: "1px solid var(--bd)", [isRtl ? "paddingRight" : "paddingLeft"]: 0 }}>
|
|
703
795
|
{sec.pages.map(p => {
|
|
704
796
|
const active = currentPageId === p.id;
|
|
705
797
|
return (
|
|
706
798
|
<button key={p.id} onClick={() => { onNavigate(p.id); if (mobile) setSb(false); }} style={{
|
|
707
799
|
display: "flex", alignItems: "center", gap: 10, width: "100%",
|
|
708
|
-
textAlign: "left", background: "none",
|
|
800
|
+
textAlign: isRtl ? "right" : "left", background: "none",
|
|
709
801
|
border: "none", borderRadius: 0,
|
|
710
|
-
borderLeft: active ? "2px solid var(--ac)" : "2px solid transparent",
|
|
802
|
+
[isRtl ? "borderRight" : "borderLeft"]: active ? "2px solid var(--ac)" : "2px solid transparent",
|
|
711
803
|
padding: "7px 14px", cursor: "pointer",
|
|
712
804
|
color: active ? "var(--ac)" : "var(--tx2)", fontSize: 13,
|
|
713
805
|
fontWeight: active ? 500 : 400, fontFamily: "var(--font-body)",
|
|
714
806
|
transition: "all .12s",
|
|
715
807
|
}}>
|
|
716
808
|
{p.title}
|
|
809
|
+
{p.badge && (() => {
|
|
810
|
+
const badgeColors: Record<string, { bg: string; text: string }> = {
|
|
811
|
+
default: { bg: "var(--sf)", text: "var(--tx2)" },
|
|
812
|
+
info: { bg: "rgba(59,130,246,0.15)", text: "rgb(59,130,246)" },
|
|
813
|
+
success: { bg: "rgba(34,197,94,0.15)", text: "rgb(34,197,94)" },
|
|
814
|
+
warning: { bg: "rgba(234,179,8,0.15)", text: "rgb(202,138,4)" },
|
|
815
|
+
danger: { bg: "rgba(239,68,68,0.15)", text: "rgb(239,68,68)" },
|
|
816
|
+
};
|
|
817
|
+
const bc = badgeColors[p.badge!.variant || "default"] || badgeColors.default;
|
|
818
|
+
return (
|
|
819
|
+
<span style={{
|
|
820
|
+
fontSize: 10, fontWeight: 600, padding: "2px 6px",
|
|
821
|
+
borderRadius: 4, marginLeft: 6, whiteSpace: "nowrap",
|
|
822
|
+
background: bc.bg, color: bc.text,
|
|
823
|
+
}}>{p.badge!.text}</span>
|
|
824
|
+
);
|
|
825
|
+
})()}
|
|
717
826
|
</button>
|
|
718
827
|
);
|
|
719
828
|
})}
|
|
@@ -734,11 +843,11 @@ export function Shell({
|
|
|
734
843
|
onNavigate(targetId);
|
|
735
844
|
}}
|
|
736
845
|
style={{
|
|
737
|
-
flex: 1, padding: "
|
|
846
|
+
flex: 1, padding: "3px 0", textAlign: "center",
|
|
738
847
|
background: v === (currentVersion || versioning.current) ? "var(--acD)" : "var(--sf)",
|
|
739
848
|
border: "1px solid var(--bd)", borderRadius: 2, cursor: "pointer",
|
|
740
849
|
color: v === (currentVersion || versioning.current) ? "var(--ac)" : "var(--tx2)",
|
|
741
|
-
fontSize:
|
|
850
|
+
fontSize: 11, fontFamily: "var(--font-code)",
|
|
742
851
|
fontWeight: v === versioning.current ? 600 : 400,
|
|
743
852
|
}}
|
|
744
853
|
>
|
|
@@ -758,10 +867,30 @@ export function Shell({
|
|
|
758
867
|
<span style={{ fontFamily: "var(--font-code)", fontSize: 10, color: "var(--txM)" }}>{typeof __TOME_VERSION__ !== "undefined" && __TOME_VERSION__ ? `v${__TOME_VERSION__}` : "v0.1.0"}</span>
|
|
759
868
|
</div>
|
|
760
869
|
</aside>
|
|
870
|
+
)}
|
|
761
871
|
|
|
762
872
|
{/* Main area */}
|
|
763
873
|
<div style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }}>
|
|
764
874
|
{/* Header */}
|
|
875
|
+
{overrides?.Header ? (
|
|
876
|
+
<overrides.Header
|
|
877
|
+
config={config}
|
|
878
|
+
navigation={navigation}
|
|
879
|
+
currentPageId={currentPageId}
|
|
880
|
+
onNavigate={onNavigate}
|
|
881
|
+
mobile={mobile}
|
|
882
|
+
sbOpen={sbOpen}
|
|
883
|
+
setSbOpen={setSb}
|
|
884
|
+
isDark={isDark}
|
|
885
|
+
setDark={setDark}
|
|
886
|
+
versioning={versioning}
|
|
887
|
+
currentVersion={currentVersion}
|
|
888
|
+
i18n={i18n}
|
|
889
|
+
currentLocale={currentLocale}
|
|
890
|
+
onSearchOpen={() => setSearch(true)}
|
|
891
|
+
basePath={basePath}
|
|
892
|
+
/>
|
|
893
|
+
) : (
|
|
765
894
|
<header style={{
|
|
766
895
|
display: "flex", alignItems: "center", gap: mobile ? 8 : 12, padding: mobile ? "8px 12px" : "10px 24px",
|
|
767
896
|
borderBottom: "1px solid var(--bd)", background: "var(--hdBg)", backdropFilter: "blur(12px)",
|
|
@@ -820,6 +949,30 @@ export function Shell({
|
|
|
820
949
|
</div>
|
|
821
950
|
)}
|
|
822
951
|
|
|
952
|
+
{/* Social Links */}
|
|
953
|
+
{config.socialLinks && config.socialLinks.length > 0 && !mobile && (
|
|
954
|
+
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
|
955
|
+
{config.socialLinks.map((link) => (
|
|
956
|
+
<a
|
|
957
|
+
key={link.url}
|
|
958
|
+
href={link.url}
|
|
959
|
+
target="_blank"
|
|
960
|
+
rel="noopener noreferrer"
|
|
961
|
+
aria-label={link.label || link.platform}
|
|
962
|
+
data-testid={`social-link-${link.platform}`}
|
|
963
|
+
style={{
|
|
964
|
+
display: "flex", alignItems: "center", justifyContent: "center",
|
|
965
|
+
color: "var(--tx2)", cursor: "pointer", transition: "color .15s",
|
|
966
|
+
}}
|
|
967
|
+
onMouseOver={(e) => (e.currentTarget.style.color = "var(--tx)")}
|
|
968
|
+
onMouseOut={(e) => (e.currentTarget.style.color = "var(--tx2)")}
|
|
969
|
+
>
|
|
970
|
+
<SocialIcon platform={link.platform} customIcon={link.icon} />
|
|
971
|
+
</a>
|
|
972
|
+
))}
|
|
973
|
+
</div>
|
|
974
|
+
)}
|
|
975
|
+
|
|
823
976
|
{/* Theme toggle in header on mobile */}
|
|
824
977
|
{mobile && themeMode === "auto" && (
|
|
825
978
|
<button aria-label={isDark ? "Switch to light mode" : "Switch to dark mode"} onClick={() => setDark(d => !d)} style={{ background: "none", border: "none", color: "var(--txM)", cursor: "pointer", display: "flex", flexShrink: 0 }}>
|
|
@@ -947,6 +1100,7 @@ export function Shell({
|
|
|
947
1100
|
</div>
|
|
948
1101
|
)}
|
|
949
1102
|
</header>
|
|
1103
|
+
)}
|
|
950
1104
|
|
|
951
1105
|
{/* TOM-30: Old version banner */}
|
|
952
1106
|
{isOldVersion && (
|
|
@@ -975,13 +1129,49 @@ export function Shell({
|
|
|
975
1129
|
{/* Content + TOC */}
|
|
976
1130
|
<div ref={contentRef} style={{ flex: 1, overflow: "auto", display: "flex" }}>
|
|
977
1131
|
<main style={{ flex: 1, maxWidth: mobile ? "100%" : 760, padding: mobile ? "24px 16px 60px" : "40px 48px 80px", margin: "0 auto", minWidth: 0 }}>
|
|
1132
|
+
{breadcrumbs.length > 0 && (
|
|
1133
|
+
<nav aria-label="Breadcrumbs" data-testid="breadcrumbs" style={{
|
|
1134
|
+
display: "flex", alignItems: "center", gap: 6,
|
|
1135
|
+
fontSize: 13, color: "var(--tx2)", marginBottom: 8,
|
|
1136
|
+
}}>
|
|
1137
|
+
{breadcrumbs.map((crumb, i) => (
|
|
1138
|
+
<React.Fragment key={i}>
|
|
1139
|
+
{i > 0 && <span style={{ color: "var(--tx2)", opacity: 0.5 }}>{"\u203A"}</span>}
|
|
1140
|
+
{i < breadcrumbs.length - 1 && crumb.href !== null ? (
|
|
1141
|
+
<a
|
|
1142
|
+
href={crumb.href}
|
|
1143
|
+
onClick={(e: React.MouseEvent) => {
|
|
1144
|
+
e.preventDefault();
|
|
1145
|
+
// Find the page id for this href
|
|
1146
|
+
const page = navigation.flatMap(s => s.pages).find(p => p.urlPath === crumb.href);
|
|
1147
|
+
if (page) onNavigate(page.id);
|
|
1148
|
+
}}
|
|
1149
|
+
style={{ color: "var(--tx2)", textDecoration: "none", cursor: "pointer" }}
|
|
1150
|
+
>
|
|
1151
|
+
{crumb.label}
|
|
1152
|
+
</a>
|
|
1153
|
+
) : (
|
|
1154
|
+
<span style={i === breadcrumbs.length - 1 ? { color: "var(--tx)" } : undefined}>{crumb.label}</span>
|
|
1155
|
+
)}
|
|
1156
|
+
</React.Fragment>
|
|
1157
|
+
))}
|
|
1158
|
+
</nav>
|
|
1159
|
+
)}
|
|
978
1160
|
<h1 style={{ fontFamily: "var(--font-heading)", fontSize: mobile ? 26 : 38, fontWeight: 400, fontStyle: "italic", lineHeight: 1.15, marginBottom: 8 }}>
|
|
979
1161
|
{pageTitle}
|
|
980
1162
|
</h1>
|
|
1163
|
+
{isDraft && (
|
|
1164
|
+
<div data-testid="draft-banner" style={{ background: "#fef3c7", color: "#92400e", padding: "8px 16px", borderRadius: 6, fontSize: 13, marginBottom: 16 }}>
|
|
1165
|
+
Draft — This page is only visible in development
|
|
1166
|
+
</div>
|
|
1167
|
+
)}
|
|
981
1168
|
{pageDescription && <p style={{ fontSize: 16, color: "var(--tx2)", lineHeight: 1.6, marginBottom: 32 }}>{pageDescription}</p>}
|
|
982
1169
|
<div style={{ borderTop: "1px solid var(--bd)", paddingTop: 28 }}>
|
|
983
|
-
{/* TOM-
|
|
984
|
-
{
|
|
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 ? (
|
|
985
1175
|
<ChangelogView entries={changelogEntries} />
|
|
986
1176
|
) : PageComponent ? (
|
|
987
1177
|
<div className="tome-content">
|
|
@@ -995,7 +1185,19 @@ export function Shell({
|
|
|
995
1185
|
)}
|
|
996
1186
|
</div>
|
|
997
1187
|
|
|
998
|
-
{/* TOM-48: Edit this page link + TOM-54: Last updated */}
|
|
1188
|
+
{/* TOM-48: Edit this page link + TOM-54: Last updated + Feedback + Prev/Next */}
|
|
1189
|
+
{overrides?.PageFooter ? (
|
|
1190
|
+
<overrides.PageFooter
|
|
1191
|
+
editUrl={editUrl}
|
|
1192
|
+
lastUpdated={lastUpdated}
|
|
1193
|
+
currentPageId={currentPageId}
|
|
1194
|
+
prev={prev}
|
|
1195
|
+
next={next}
|
|
1196
|
+
onNavigate={onNavigate}
|
|
1197
|
+
mobile={mobile}
|
|
1198
|
+
/>
|
|
1199
|
+
) : (
|
|
1200
|
+
<>
|
|
999
1201
|
{(editUrl || lastUpdated) && (
|
|
1000
1202
|
<div style={{ marginTop: 40, display: "flex", flexDirection: mobile ? "column" : "row", alignItems: mobile ? "flex-start" : "center", justifyContent: "space-between", gap: mobile ? 8 : 16 }}>
|
|
1001
1203
|
{editUrl && (
|
|
@@ -1049,7 +1251,7 @@ export function Shell({
|
|
|
1049
1251
|
border: "1px solid var(--bd)", borderRadius: 2, padding: "10px 16px",
|
|
1050
1252
|
cursor: "pointer", color: "var(--tx2)", fontSize: 13, fontFamily: "var(--font-body)",
|
|
1051
1253
|
transition: "border-color .15s, color .15s",
|
|
1052
|
-
}}
|
|
1254
|
+
}}>{isRtl ? <ArrowRight /> : <ArrowLeft />} {prev.title}</button>
|
|
1053
1255
|
) : <div />}
|
|
1054
1256
|
{next ? (
|
|
1055
1257
|
<button onClick={() => onNavigate(next.id)} style={{
|
|
@@ -1057,16 +1259,27 @@ export function Shell({
|
|
|
1057
1259
|
border: "1px solid var(--bd)", borderRadius: 2, padding: "10px 16px",
|
|
1058
1260
|
cursor: "pointer", color: "var(--tx2)", fontSize: 13, fontFamily: "var(--font-body)",
|
|
1059
1261
|
transition: "border-color .15s, color .15s",
|
|
1060
|
-
}}>{next.title} <ArrowRight
|
|
1262
|
+
}}>{next.title} {isRtl ? <ArrowLeft /> : <ArrowRight />}</button>
|
|
1061
1263
|
) : <div />}
|
|
1062
1264
|
</div>
|
|
1265
|
+
</>
|
|
1266
|
+
)}
|
|
1063
1267
|
</main>
|
|
1064
1268
|
|
|
1065
1269
|
{/* TOC (TOM-52) */}
|
|
1066
|
-
{
|
|
1067
|
-
|
|
1270
|
+
{overrides?.Toc ? (
|
|
1271
|
+
showToc && filteredHeadings.length >= 2 && wide && (
|
|
1272
|
+
<overrides.Toc
|
|
1273
|
+
headings={filteredHeadings}
|
|
1274
|
+
activeHeadingId={activeHeadingId}
|
|
1275
|
+
onScrollToHeading={scrollToHeading}
|
|
1276
|
+
/>
|
|
1277
|
+
)
|
|
1278
|
+
) : (
|
|
1279
|
+
showToc && filteredHeadings.length >= 2 && wide && (
|
|
1280
|
+
<aside data-testid="toc-sidebar" style={{ width: 200, padding: isRtl ? "40px 0 40px 16px" : "40px 16px 40px 0", position: "sticky", top: 0, alignSelf: "flex-start", flexShrink: 0 }}>
|
|
1068
1281
|
<div style={{ fontSize: 10, fontWeight: 600, textTransform: "uppercase", letterSpacing: ".1em", color: "var(--txM)", marginBottom: 12, fontFamily: "var(--font-code)" }}>On this page</div>
|
|
1069
|
-
<nav aria-label="Table of contents" style={{ borderLeft: "1px solid var(--bd)", paddingLeft: 0 }}>
|
|
1282
|
+
<nav aria-label="Table of contents" style={{ [isRtl ? "borderRight" : "borderLeft"]: "1px solid var(--bd)", [isRtl ? "paddingRight" : "paddingLeft"]: 0 }}>
|
|
1070
1283
|
{filteredHeadings.map((h, i) => {
|
|
1071
1284
|
const isActive = activeHeadingId === h.id;
|
|
1072
1285
|
return (
|
|
@@ -1081,22 +1294,33 @@ export function Shell({
|
|
|
1081
1294
|
fontWeight: isActive ? 500 : 400,
|
|
1082
1295
|
textDecoration: "none",
|
|
1083
1296
|
padding: "4px 12px",
|
|
1084
|
-
paddingLeft: 12 + (h.depth - 2) * 12,
|
|
1297
|
+
[isRtl ? "paddingRight" : "paddingLeft"]: 12 + (h.depth - 2) * 12,
|
|
1085
1298
|
lineHeight: 1.4,
|
|
1086
1299
|
transition: "color .15s, font-weight .15s",
|
|
1087
|
-
borderLeft: isActive ? "2px solid var(--ac)" : "2px solid transparent",
|
|
1088
|
-
marginLeft: -1,
|
|
1300
|
+
[isRtl ? "borderRight" : "borderLeft"]: isActive ? "2px solid var(--ac)" : "2px solid transparent",
|
|
1301
|
+
[isRtl ? "marginRight" : "marginLeft"]: -1,
|
|
1089
1302
|
}}
|
|
1090
1303
|
>{h.text}</a>
|
|
1091
1304
|
);
|
|
1092
1305
|
})}
|
|
1093
1306
|
</nav>
|
|
1094
1307
|
</aside>
|
|
1308
|
+
)
|
|
1095
1309
|
)}
|
|
1096
1310
|
</div>
|
|
1097
1311
|
</div>
|
|
1098
1312
|
</div>
|
|
1099
1313
|
|
|
1314
|
+
{/* Footer override */}
|
|
1315
|
+
{overrides?.Footer && (
|
|
1316
|
+
<overrides.Footer
|
|
1317
|
+
config={config}
|
|
1318
|
+
navigation={navigation}
|
|
1319
|
+
currentPageId={currentPageId}
|
|
1320
|
+
onNavigate={onNavigate}
|
|
1321
|
+
/>
|
|
1322
|
+
)}
|
|
1323
|
+
|
|
1100
1324
|
{/* TOM-32: AI Chat Widget (BYOK) */}
|
|
1101
1325
|
{config.ai?.enabled && (
|
|
1102
1326
|
<AiChat
|
|
@@ -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 };
|