@tomehq/theme 0.6.3 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai-api-6KVITVN6.js +14 -0
- package/dist/{chunk-26CZLWVW.js → chunk-6AD5TJ6F.js} +426 -87
- package/dist/chunk-LNWYFDZ4.js +73 -0
- package/dist/entry.js +2 -1
- package/dist/index.d.ts +250 -4
- package/dist/index.js +2 -1
- package/package.json +3 -3
- package/src/AiChat.tsx +11 -74
- package/src/Shell.tsx +168 -17
- package/src/__tests__/ai-api.test.ts +42 -0
- package/src/__tests__/ai-search.test.tsx +205 -0
- package/src/__tests__/feedback-widget.test.tsx +132 -0
- package/src/ai-api.ts +110 -0
- package/src/entry-helpers.ts +8 -3
- package/src/entry.test.tsx +6 -22
- package/src/entry.tsx +3 -1
- package/src/presets.ts +93 -3
- package/src/routing.test.ts +2 -3
- package/src/routing.ts +12 -4
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, beforeAll } from "vitest";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { render, screen, fireEvent } from "@testing-library/react";
|
|
4
|
+
import Shell from "../Shell.js";
|
|
5
|
+
|
|
6
|
+
// jsdom doesn't provide matchMedia
|
|
7
|
+
beforeAll(() => {
|
|
8
|
+
Object.defineProperty(window, "matchMedia", {
|
|
9
|
+
writable: true,
|
|
10
|
+
value: vi.fn().mockImplementation((query: string) => ({
|
|
11
|
+
matches: query.includes("dark"),
|
|
12
|
+
media: query,
|
|
13
|
+
onchange: null,
|
|
14
|
+
addListener: vi.fn(),
|
|
15
|
+
removeListener: vi.fn(),
|
|
16
|
+
addEventListener: vi.fn(),
|
|
17
|
+
removeEventListener: vi.fn(),
|
|
18
|
+
dispatchEvent: vi.fn(),
|
|
19
|
+
})),
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// ── Minimal Shell props for feedback widget testing ──────
|
|
24
|
+
|
|
25
|
+
const baseConfig = {
|
|
26
|
+
name: "Test Docs",
|
|
27
|
+
theme: { preset: "amber", mode: "auto" },
|
|
28
|
+
toc: { enabled: false },
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const navigation = [{
|
|
32
|
+
section: "Docs",
|
|
33
|
+
pages: [{ id: "test", title: "Test Page", urlPath: "/test" }],
|
|
34
|
+
}];
|
|
35
|
+
|
|
36
|
+
const allPages = [{ id: "test", title: "Test Page", description: "A test page" }];
|
|
37
|
+
|
|
38
|
+
function renderShell(configOverrides = {}) {
|
|
39
|
+
return render(
|
|
40
|
+
<Shell
|
|
41
|
+
config={{ ...baseConfig, ...configOverrides }}
|
|
42
|
+
navigation={navigation}
|
|
43
|
+
currentPageId="test"
|
|
44
|
+
pageHtml="<h1>Test Page</h1><p>Content here.</p>"
|
|
45
|
+
pageTitle="Test Page"
|
|
46
|
+
headings={[]}
|
|
47
|
+
allPages={allPages}
|
|
48
|
+
onNavigate={() => {}}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── Setup ────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
localStorage.clear();
|
|
57
|
+
(window as any).__tome = {
|
|
58
|
+
trackFeedback: vi.fn(),
|
|
59
|
+
trackSearch: vi.fn(),
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// ── Tests ────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
describe("FeedbackWidget", () => {
|
|
66
|
+
it("renders thumbs up and down buttons by default", () => {
|
|
67
|
+
renderShell();
|
|
68
|
+
expect(screen.getByTestId("feedback-up")).toBeInTheDocument();
|
|
69
|
+
expect(screen.getByTestId("feedback-down")).toBeInTheDocument();
|
|
70
|
+
expect(screen.getByText("Was this helpful?")).toBeInTheDocument();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("hides feedback widget when feedback.enabled is false", () => {
|
|
74
|
+
renderShell({ feedback: { enabled: false } });
|
|
75
|
+
expect(screen.queryByTestId("feedback-up")).not.toBeInTheDocument();
|
|
76
|
+
expect(screen.queryByText("Was this helpful?")).not.toBeInTheDocument();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("shows thanks message after thumbs up without textInput", () => {
|
|
80
|
+
renderShell();
|
|
81
|
+
fireEvent.click(screen.getByTestId("feedback-up"));
|
|
82
|
+
expect(screen.getByText("Thanks for your feedback!")).toBeInTheDocument();
|
|
83
|
+
expect(screen.queryByTestId("feedback-up")).not.toBeInTheDocument();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("fires trackFeedback on thumbs up without textInput", () => {
|
|
87
|
+
renderShell();
|
|
88
|
+
fireEvent.click(screen.getByTestId("feedback-up"));
|
|
89
|
+
expect((window as any).__tome.trackFeedback).toHaveBeenCalledWith("test", "up");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("fires trackFeedback on thumbs down without textInput", () => {
|
|
93
|
+
renderShell();
|
|
94
|
+
fireEvent.click(screen.getByTestId("feedback-down"));
|
|
95
|
+
expect((window as any).__tome.trackFeedback).toHaveBeenCalledWith("test", "down");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("stores feedback in localStorage", () => {
|
|
99
|
+
renderShell();
|
|
100
|
+
fireEvent.click(screen.getByTestId("feedback-up"));
|
|
101
|
+
expect(localStorage.getItem("tome-feedback-test")).toBe("up");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("shows text input after thumbs up when textInput is enabled", () => {
|
|
105
|
+
renderShell({ feedback: { enabled: true, textInput: true } });
|
|
106
|
+
fireEvent.click(screen.getByTestId("feedback-up"));
|
|
107
|
+
expect(screen.getByTestId("feedback-text-input")).toBeInTheDocument();
|
|
108
|
+
expect(screen.getByTestId("feedback-submit")).toBeInTheDocument();
|
|
109
|
+
expect(screen.getByText("Any additional feedback? (optional)")).toBeInTheDocument();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("submits text feedback and shows thanks", () => {
|
|
113
|
+
renderShell({ feedback: { enabled: true, textInput: true } });
|
|
114
|
+
fireEvent.click(screen.getByTestId("feedback-up"));
|
|
115
|
+
|
|
116
|
+
const input = screen.getByTestId("feedback-text-input");
|
|
117
|
+
fireEvent.change(input, { target: { value: "Great docs!" } });
|
|
118
|
+
fireEvent.click(screen.getByTestId("feedback-submit"));
|
|
119
|
+
|
|
120
|
+
expect((window as any).__tome.trackFeedback).toHaveBeenCalledWith("test", "up", "Great docs!");
|
|
121
|
+
expect(screen.getByText("Thanks for your feedback!")).toBeInTheDocument();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("skip button submits without comment", () => {
|
|
125
|
+
renderShell({ feedback: { enabled: true, textInput: true } });
|
|
126
|
+
fireEvent.click(screen.getByTestId("feedback-down"));
|
|
127
|
+
fireEvent.click(screen.getByText("Skip"));
|
|
128
|
+
|
|
129
|
+
expect((window as any).__tome.trackFeedback).toHaveBeenCalledWith("test", "down");
|
|
130
|
+
expect(screen.getByText("Thanks for your feedback!")).toBeInTheDocument();
|
|
131
|
+
});
|
|
132
|
+
});
|
package/src/ai-api.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared AI API utilities for Tome's AI features (chat + search).
|
|
3
|
+
* Supports OpenAI and Anthropic providers with BYOK (Bring Your Own Key).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type AiProvider = "openai" | "anthropic" | "custom";
|
|
7
|
+
|
|
8
|
+
export interface AiMessage {
|
|
9
|
+
role: "user" | "assistant";
|
|
10
|
+
content: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Build a system prompt with optional documentation context.
|
|
15
|
+
*/
|
|
16
|
+
export function buildSystemPrompt(context?: string, instruction?: string): string {
|
|
17
|
+
const base = instruction || "You are a helpful documentation assistant. Answer questions accurately based on the documentation provided below. If the answer isn't in the documentation, say so clearly. Keep answers concise and reference specific sections when possible.";
|
|
18
|
+
if (!context) return base;
|
|
19
|
+
// Truncate context to stay within token limits (~100K chars ≈ 25K tokens)
|
|
20
|
+
const trimmed = context.length > 100000 ? context.slice(0, 100000) + "\n\n[Documentation truncated...]" : context;
|
|
21
|
+
return `${base}\n\nDocumentation:\n${trimmed}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Call OpenAI's chat completions API.
|
|
26
|
+
*/
|
|
27
|
+
export async function callOpenAI(
|
|
28
|
+
messages: AiMessage[],
|
|
29
|
+
apiKey: string,
|
|
30
|
+
model: string,
|
|
31
|
+
systemPrompt: string,
|
|
32
|
+
): Promise<string> {
|
|
33
|
+
const res = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
34
|
+
method: "POST",
|
|
35
|
+
headers: {
|
|
36
|
+
"Content-Type": "application/json",
|
|
37
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
38
|
+
},
|
|
39
|
+
body: JSON.stringify({
|
|
40
|
+
model,
|
|
41
|
+
messages: [
|
|
42
|
+
{ role: "system", content: systemPrompt },
|
|
43
|
+
...messages.map((m) => ({ role: m.role, content: m.content })),
|
|
44
|
+
],
|
|
45
|
+
}),
|
|
46
|
+
});
|
|
47
|
+
if (!res.ok) {
|
|
48
|
+
const err = await res.text();
|
|
49
|
+
throw new Error(`OpenAI API error (${res.status}): ${err}`);
|
|
50
|
+
}
|
|
51
|
+
const data = await res.json();
|
|
52
|
+
return data.choices?.[0]?.message?.content || "No response.";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Call Anthropic's messages API.
|
|
57
|
+
*/
|
|
58
|
+
export async function callAnthropic(
|
|
59
|
+
messages: AiMessage[],
|
|
60
|
+
apiKey: string,
|
|
61
|
+
model: string,
|
|
62
|
+
systemPrompt: string,
|
|
63
|
+
): Promise<string> {
|
|
64
|
+
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
65
|
+
method: "POST",
|
|
66
|
+
headers: {
|
|
67
|
+
"Content-Type": "application/json",
|
|
68
|
+
"x-api-key": apiKey,
|
|
69
|
+
"anthropic-version": "2023-06-01",
|
|
70
|
+
"anthropic-dangerous-direct-browser-access": "true",
|
|
71
|
+
},
|
|
72
|
+
body: JSON.stringify({
|
|
73
|
+
model,
|
|
74
|
+
max_tokens: 1024,
|
|
75
|
+
system: systemPrompt,
|
|
76
|
+
messages: messages.map((m) => ({ role: m.role, content: m.content })),
|
|
77
|
+
}),
|
|
78
|
+
});
|
|
79
|
+
if (!res.ok) {
|
|
80
|
+
const err = await res.text();
|
|
81
|
+
throw new Error(`Anthropic API error (${res.status}): ${err}`);
|
|
82
|
+
}
|
|
83
|
+
const data = await res.json();
|
|
84
|
+
return data.content?.[0]?.text || "No response.";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get the default model for a provider.
|
|
89
|
+
*/
|
|
90
|
+
export function getDefaultModel(provider: AiProvider): string {
|
|
91
|
+
if (provider === "openai") return "gpt-4o-mini";
|
|
92
|
+
return "claude-sonnet-4-20250514";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Call the appropriate AI provider with a query and context.
|
|
97
|
+
* Returns the AI's response text.
|
|
98
|
+
*/
|
|
99
|
+
export async function callAiProvider(
|
|
100
|
+
provider: AiProvider,
|
|
101
|
+
messages: AiMessage[],
|
|
102
|
+
apiKey: string,
|
|
103
|
+
model: string,
|
|
104
|
+
systemPrompt: string,
|
|
105
|
+
): Promise<string> {
|
|
106
|
+
if (provider === "openai") {
|
|
107
|
+
return callOpenAI(messages, apiKey, model, systemPrompt);
|
|
108
|
+
}
|
|
109
|
+
return callAnthropic(messages, apiKey, model, systemPrompt);
|
|
110
|
+
}
|
package/src/entry-helpers.ts
CHANGED
|
@@ -52,6 +52,7 @@ export interface ApiReferencePage {
|
|
|
52
52
|
headings: Array<{ depth: number; text: string; id: string }>;
|
|
53
53
|
changelogEntries?: undefined;
|
|
54
54
|
apiManifest: any;
|
|
55
|
+
asyncApiManifest?: any;
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
export type LoadedPage = HtmlPage | MdxPage | ApiReferencePage;
|
|
@@ -124,9 +125,13 @@ export async function loadPage(
|
|
|
124
125
|
// Regular .md page — mod.default is { html, frontmatter, headings }
|
|
125
126
|
if (!mod.default) throw new PageNotFoundError(id);
|
|
126
127
|
|
|
127
|
-
// API reference page (synthetic route from OpenAPI spec)
|
|
128
|
-
if (mod.isApiReference && mod.apiManifest) {
|
|
129
|
-
return {
|
|
128
|
+
// API reference page (synthetic route from OpenAPI/AsyncAPI spec)
|
|
129
|
+
if (mod.isApiReference && (mod.apiManifest || mod.asyncApiManifest)) {
|
|
130
|
+
return {
|
|
131
|
+
isMdx: false, isApiReference: true, ...mod.default,
|
|
132
|
+
apiManifest: mod.apiManifest,
|
|
133
|
+
asyncApiManifest: mod.asyncApiManifest,
|
|
134
|
+
};
|
|
130
135
|
}
|
|
131
136
|
|
|
132
137
|
// Changelog page type
|
package/src/entry.test.tsx
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import React from "react";
|
|
2
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
3
2
|
import { render, act } from "@testing-library/react";
|
|
4
3
|
|
|
@@ -31,27 +30,6 @@ vi.mock("virtual:tome/config", () => ({
|
|
|
31
30
|
},
|
|
32
31
|
}));
|
|
33
32
|
|
|
34
|
-
const mockRoutes = [
|
|
35
|
-
{
|
|
36
|
-
id: "index",
|
|
37
|
-
urlPath: "/",
|
|
38
|
-
filePath: "pages/index.md",
|
|
39
|
-
frontmatter: { title: "Home", description: "Welcome" },
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
id: "quickstart",
|
|
43
|
-
urlPath: "/quickstart",
|
|
44
|
-
filePath: "pages/quickstart.md",
|
|
45
|
-
frontmatter: { title: "Quick Start", description: "Get started" },
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
id: "api-reference",
|
|
49
|
-
urlPath: "/api",
|
|
50
|
-
filePath: "__api-reference__",
|
|
51
|
-
frontmatter: { title: "API Reference", description: "API docs" },
|
|
52
|
-
},
|
|
53
|
-
];
|
|
54
|
-
|
|
55
33
|
const mockNavigation = [
|
|
56
34
|
{ section: "Guide", pages: [{ id: "index", title: "Home", urlPath: "/" }] },
|
|
57
35
|
];
|
|
@@ -173,6 +151,12 @@ vi.mock("@tomehq/components", () => ({
|
|
|
173
151
|
LinkCard: () => <div />,
|
|
174
152
|
CardGrid: () => <div />,
|
|
175
153
|
ApiReference: MockApiReference,
|
|
154
|
+
AsyncApiReference: (p: any) => <div data-testid="async-api-ref" />,
|
|
155
|
+
ProtocolBadge: () => <div />,
|
|
156
|
+
DirectionBadge: () => <div />,
|
|
157
|
+
ChannelCard: () => <div />,
|
|
158
|
+
MessageBlock: () => <div />,
|
|
159
|
+
AsyncParameterTable: () => <div />,
|
|
176
160
|
}));
|
|
177
161
|
|
|
178
162
|
// ── Global stubs ─────────────────────────────────────────
|
package/src/entry.tsx
CHANGED
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
computeEditUrl,
|
|
8
8
|
resolveInitialPageId,
|
|
9
9
|
detectCurrentVersion,
|
|
10
|
-
NavigationCancelledError,
|
|
11
10
|
type LoadedPage,
|
|
12
11
|
} from "./entry-helpers.js";
|
|
13
12
|
|
|
@@ -39,6 +38,7 @@ import {
|
|
|
39
38
|
LinkCard,
|
|
40
39
|
CardGrid,
|
|
41
40
|
ApiReference,
|
|
41
|
+
AsyncApiReference,
|
|
42
42
|
} from "@tomehq/components";
|
|
43
43
|
|
|
44
44
|
const MDX_COMPONENTS: Record<string, React.ComponentType<any>> = {
|
|
@@ -576,10 +576,12 @@ function App() {
|
|
|
576
576
|
lastUpdated={currentRoute?.lastUpdated}
|
|
577
577
|
changelogEntries={!pageData?.isMdx ? pageData?.changelogEntries : undefined}
|
|
578
578
|
apiManifest={(!pageData?.isMdx && pageData?.isApiReference) ? pageData.apiManifest : undefined}
|
|
579
|
+
asyncApiManifest={(!pageData?.isMdx && pageData?.isApiReference) ? (pageData as any).asyncApiManifest : undefined}
|
|
579
580
|
apiBaseUrl={config.api?.baseUrl}
|
|
580
581
|
apiPlayground={config.api?.playground}
|
|
581
582
|
apiAuth={config.api?.auth}
|
|
582
583
|
ApiReferenceComponent={ApiReference}
|
|
584
|
+
AsyncApiReferenceComponent={AsyncApiReference}
|
|
583
585
|
onNavigate={navigateTo}
|
|
584
586
|
allPages={allPages}
|
|
585
587
|
docContext={docContext}
|
package/src/presets.ts
CHANGED
|
@@ -49,13 +49,13 @@ export const THEME_PRESETS = {
|
|
|
49
49
|
cipher: {
|
|
50
50
|
dark: {
|
|
51
51
|
bg:"#050508",sf:"#0c0c12",sfH:"#12121a",bd:"#1a1a25",
|
|
52
|
-
tx:"#d4ff00",tx2:"#8a90a0",txM:"#
|
|
52
|
+
tx:"#d4ff00",tx2:"#8a90a0",txM:"#7e8494",
|
|
53
53
|
ac:"#6666ff",acD:"rgba(102,102,255,0.10)",acT:"#8080ff",
|
|
54
54
|
cdBg:"#08080e",cdTx:"#b0c870",sbBg:"#08080d",hdBg:"rgba(5,5,8,0.88)",
|
|
55
55
|
},
|
|
56
56
|
light: {
|
|
57
57
|
bg:"#f0f2f5",sf:"#ffffff",sfH:"#e8eaef",bd:"#d0d4db",
|
|
58
|
-
tx:"#0f1219",tx2:"#4a5060",txM:"#
|
|
58
|
+
tx:"#0f1219",tx2:"#4a5060",txM:"#5c6272",
|
|
59
59
|
ac:"#2020cc",acD:"rgba(32,32,204,0.08)",acT:"#1a1aa8",
|
|
60
60
|
cdBg:"#e6e9ef",cdTx:"#2a3520",sbBg:"#ebedf2",hdBg:"rgba(240,242,245,0.90)",
|
|
61
61
|
},
|
|
@@ -64,7 +64,7 @@ export const THEME_PRESETS = {
|
|
|
64
64
|
mint: {
|
|
65
65
|
dark: {
|
|
66
66
|
bg:"#0d1117",sf:"#161b22",sfH:"#1c2129",bd:"#21262d",
|
|
67
|
-
tx:"#e6edf3",tx2:"#8b949e",txM:"#
|
|
67
|
+
tx:"#e6edf3",tx2:"#8b949e",txM:"#7d858f",
|
|
68
68
|
ac:"#0ea371",acD:"rgba(14,163,113,0.10)",acT:"#2dd4a0",
|
|
69
69
|
cdBg:"#0a0e14",cdTx:"#adbac7",sbBg:"#0d1117",hdBg:"rgba(13,17,23,0.88)",
|
|
70
70
|
},
|
|
@@ -76,6 +76,96 @@ export const THEME_PRESETS = {
|
|
|
76
76
|
},
|
|
77
77
|
fonts: { heading: "Inter", body: "Inter", code: "Fira Code" },
|
|
78
78
|
},
|
|
79
|
+
ocean: {
|
|
80
|
+
dark: {
|
|
81
|
+
bg:"#0a1628",sf:"#0f1d33",sfH:"#142540",bd:"#1a2e50",
|
|
82
|
+
tx:"#e0e8f0",tx2:"#8ca0b8",txM:"#7b92aa",
|
|
83
|
+
ac:"#0ea5e9",acD:"rgba(14,165,233,0.10)",acT:"#38bdf8",
|
|
84
|
+
cdBg:"#081320",cdTx:"#94b4d0",sbBg:"#0a1628",hdBg:"rgba(10,22,40,0.90)",
|
|
85
|
+
},
|
|
86
|
+
light: {
|
|
87
|
+
bg:"#f0f7ff",sf:"#ffffff",sfH:"#e8f0fa",bd:"#ccdcef",
|
|
88
|
+
tx:"#0c1929",tx2:"#3d5a78",txM:"#4a6a88",
|
|
89
|
+
ac:"#0369a1",acD:"rgba(3,105,161,0.07)",acT:"#025e8f",
|
|
90
|
+
cdBg:"#e4edf7",cdTx:"#1a3050",sbBg:"#eef4fc",hdBg:"rgba(240,247,255,0.92)",
|
|
91
|
+
},
|
|
92
|
+
fonts: { heading: "Outfit", body: "Inter", code: "JetBrains Mono" },
|
|
93
|
+
},
|
|
94
|
+
rose: {
|
|
95
|
+
dark: {
|
|
96
|
+
bg:"#0f0a10",sf:"#171118",sfH:"#1e1620",bd:"#2a1f2c",
|
|
97
|
+
tx:"#f0e4f0",tx2:"#b09ab0",txM:"#8a7890",
|
|
98
|
+
ac:"#f43f5e",acD:"rgba(244,63,94,0.10)",acT:"#fb7185",
|
|
99
|
+
cdBg:"#0d080e",cdTx:"#c8aec8",sbBg:"#0f0a10",hdBg:"rgba(15,10,16,0.88)",
|
|
100
|
+
},
|
|
101
|
+
light: {
|
|
102
|
+
bg:"#fef7f7",sf:"#ffffff",sfH:"#fceef0",bd:"#f0d4d8",
|
|
103
|
+
tx:"#1a0f12",tx2:"#6b4048",txM:"#8a5a64",
|
|
104
|
+
ac:"#c81e3e",acD:"rgba(200,30,62,0.06)",acT:"#a81835",
|
|
105
|
+
cdBg:"#f8eaec",cdTx:"#3a1a22",sbBg:"#fdf2f4",hdBg:"rgba(254,247,247,0.92)",
|
|
106
|
+
},
|
|
107
|
+
fonts: { heading: "Playfair Display", body: "Source Sans 3", code: "Fira Code" },
|
|
108
|
+
},
|
|
109
|
+
forest: {
|
|
110
|
+
dark: {
|
|
111
|
+
bg:"#091209",sf:"#0f1a0f",sfH:"#152215",bd:"#1e2e1e",
|
|
112
|
+
tx:"#e0f0e0",tx2:"#8aaa8a",txM:"#6a8a6a",
|
|
113
|
+
ac:"#22c55e",acD:"rgba(34,197,94,0.10)",acT:"#4ade80",
|
|
114
|
+
cdBg:"#070e07",cdTx:"#a0c4a0",sbBg:"#091209",hdBg:"rgba(9,18,9,0.90)",
|
|
115
|
+
},
|
|
116
|
+
light: {
|
|
117
|
+
bg:"#f4faf4",sf:"#ffffff",sfH:"#e8f4e8",bd:"#c8e0c8",
|
|
118
|
+
tx:"#0a1a0a",tx2:"#3a5a3a",txM:"#5a7a5a",
|
|
119
|
+
ac:"#15803d",acD:"rgba(21,128,61,0.07)",acT:"#116d34",
|
|
120
|
+
cdBg:"#e4f2e4",cdTx:"#1a3a1a",sbBg:"#eef6ee",hdBg:"rgba(244,250,244,0.92)",
|
|
121
|
+
},
|
|
122
|
+
fonts: { heading: "Merriweather", body: "Nunito Sans", code: "Source Code Pro" },
|
|
123
|
+
},
|
|
124
|
+
slate: {
|
|
125
|
+
dark: {
|
|
126
|
+
bg:"#0f1115",sf:"#16181e",sfH:"#1c1f26",bd:"#24272e",
|
|
127
|
+
tx:"#e2e4e8",tx2:"#9498a0",txM:"#808690",
|
|
128
|
+
ac:"#94a3b8",acD:"rgba(148,163,184,0.10)",acT:"#b0bec8",
|
|
129
|
+
cdBg:"#0c0e12",cdTx:"#a8acb4",sbBg:"#0f1115",hdBg:"rgba(15,17,21,0.88)",
|
|
130
|
+
},
|
|
131
|
+
light: {
|
|
132
|
+
bg:"#f8fafc",sf:"#ffffff",sfH:"#f1f5f9",bd:"#d8dfe7",
|
|
133
|
+
tx:"#0f172a",tx2:"#475569",txM:"#64748b",
|
|
134
|
+
ac:"#475569",acD:"rgba(71,85,105,0.07)",acT:"#3a4a5c",
|
|
135
|
+
cdBg:"#f0f4f8",cdTx:"#1e293b",sbBg:"#f5f7fa",hdBg:"rgba(248,250,252,0.92)",
|
|
136
|
+
},
|
|
137
|
+
fonts: { heading: "Inter", body: "Inter", code: "JetBrains Mono" },
|
|
138
|
+
},
|
|
139
|
+
sunset: {
|
|
140
|
+
dark: {
|
|
141
|
+
bg:"#120c06",sf:"#1a1208",sfH:"#22180c",bd:"#2e2010",
|
|
142
|
+
tx:"#f0e4d4",tx2:"#b0986a",txM:"#907850",
|
|
143
|
+
ac:"#f97316",acD:"rgba(249,115,22,0.10)",acT:"#fb923c",
|
|
144
|
+
cdBg:"#0e0a05",cdTx:"#c8aa78",sbBg:"#120c06",hdBg:"rgba(18,12,6,0.90)",
|
|
145
|
+
},
|
|
146
|
+
light: {
|
|
147
|
+
bg:"#fffbf5",sf:"#ffffff",sfH:"#fef3e6",bd:"#f0d8b8",
|
|
148
|
+
tx:"#1a1008",tx2:"#6a5030",txM:"#8a6840",
|
|
149
|
+
ac:"#c2410c",acD:"rgba(194,65,12,0.06)",acT:"#a63a0a",
|
|
150
|
+
cdBg:"#faf0e2",cdTx:"#3a2810",sbBg:"#fdf6ec",hdBg:"rgba(255,251,245,0.92)",
|
|
151
|
+
},
|
|
152
|
+
fonts: { heading: "Sora", body: "DM Sans", code: "Fira Code" },
|
|
153
|
+
},
|
|
154
|
+
carbon: {
|
|
155
|
+
dark: {
|
|
156
|
+
bg:"#080808",sf:"#101010",sfH:"#171717",bd:"#1f1f1f",
|
|
157
|
+
tx:"#d4d4d4",tx2:"#888888",txM:"#787878",
|
|
158
|
+
ac:"#e4e4e4",acD:"rgba(228,228,228,0.08)",acT:"#f0f0f0",
|
|
159
|
+
cdBg:"#0a0a0a",cdTx:"#a0a0a0",sbBg:"#080808",hdBg:"rgba(8,8,8,0.90)",
|
|
160
|
+
},
|
|
161
|
+
light: {
|
|
162
|
+
bg:"#f5f5f5",sf:"#ffffff",sfH:"#ebebeb",bd:"#d4d4d4",
|
|
163
|
+
tx:"#171717",tx2:"#525252",txM:"#6b6b6b",
|
|
164
|
+
ac:"#262626",acD:"rgba(38,38,38,0.06)",acT:"#1a1a1a",
|
|
165
|
+
cdBg:"#eaeaea",cdTx:"#1a1a1a",sbBg:"#f0f0f0",hdBg:"rgba(245,245,245,0.92)",
|
|
166
|
+
},
|
|
167
|
+
fonts: { heading: "Geist", body: "Geist", code: "Geist Mono" },
|
|
168
|
+
},
|
|
79
169
|
} as const satisfies Record<string, ThemePreset>;
|
|
80
170
|
|
|
81
171
|
export type PresetName = keyof typeof THEME_PRESETS;
|
package/src/routing.test.ts
CHANGED
|
@@ -61,9 +61,8 @@ describe("pathnameToPageId", () => {
|
|
|
61
61
|
|
|
62
62
|
it("resolves versioned index", () => {
|
|
63
63
|
// /docs/v1/ → strip basePath → /v1/ → strip leading / → v1/ → strip trailing / → v1
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
expect(pathnameToPageId("/docs/v1", basePath, routes)).toBeNull();
|
|
64
|
+
// Route v1/index has urlPath "/v1/" — matches via URL-based resolution
|
|
65
|
+
expect(pathnameToPageId("/docs/v1", basePath, routes)).toBe("v1/index");
|
|
67
66
|
});
|
|
68
67
|
|
|
69
68
|
describe("with empty basePath", () => {
|
package/src/routing.ts
CHANGED
|
@@ -18,8 +18,10 @@ export function pathnameToPageId(
|
|
|
18
18
|
routes: MinimalRoute[],
|
|
19
19
|
): string | null {
|
|
20
20
|
let relative = pathname;
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
// Handle basePath stripping — treat "/" basePath as empty to avoid stripping all leading slashes
|
|
22
|
+
const normalizedBase = basePath === "/" ? "" : basePath.replace(/\/+$/, "");
|
|
23
|
+
if (normalizedBase && relative.startsWith(normalizedBase)) {
|
|
24
|
+
relative = relative.slice(normalizedBase.length);
|
|
23
25
|
}
|
|
24
26
|
const id =
|
|
25
27
|
relative
|
|
@@ -27,8 +29,14 @@ export function pathnameToPageId(
|
|
|
27
29
|
.replace(/\/index\.html$/, "")
|
|
28
30
|
.replace(/\.html$/, "")
|
|
29
31
|
.replace(/\/$/, "") || "index";
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
// Match by ID first (most common: urlPath matches ID)
|
|
33
|
+
const routeById = routes.find((r) => r.id === id);
|
|
34
|
+
if (routeById) return id;
|
|
35
|
+
// Match by urlPath (for synthetic routes where ID differs from URL, e.g. api-reference at /events-api)
|
|
36
|
+
const urlPath = "/" + id;
|
|
37
|
+
const urlPathWithSlash = urlPath + "/";
|
|
38
|
+
const routeByUrl = routes.find((r) => r.urlPath === urlPath || r.urlPath === urlPathWithSlash);
|
|
39
|
+
return routeByUrl ? routeByUrl.id : null;
|
|
32
40
|
}
|
|
33
41
|
|
|
34
42
|
/**
|