@tomehq/theme 0.1.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.
@@ -0,0 +1,200 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import React from 'react';
3
+ export { default as App } from './entry.js';
4
+
5
+ interface VersioningInfo {
6
+ current: string;
7
+ versions: string[];
8
+ }
9
+ interface I18nInfo {
10
+ defaultLocale: string;
11
+ locales: string[];
12
+ localeNames?: Record<string, string>;
13
+ }
14
+ interface ShellProps {
15
+ config: {
16
+ name: string;
17
+ theme?: {
18
+ preset?: string;
19
+ mode?: string;
20
+ accent?: string;
21
+ fonts?: {
22
+ heading?: string;
23
+ body?: string;
24
+ code?: string;
25
+ };
26
+ };
27
+ search?: {
28
+ provider?: string;
29
+ appId?: string;
30
+ apiKey?: string;
31
+ indexName?: string;
32
+ };
33
+ ai?: {
34
+ enabled?: boolean;
35
+ provider?: "openai" | "anthropic" | "custom";
36
+ model?: string;
37
+ apiKeyEnv?: string;
38
+ };
39
+ topNav?: Array<{
40
+ label: string;
41
+ href: string;
42
+ }>;
43
+ [key: string]: unknown;
44
+ };
45
+ navigation: Array<{
46
+ section: string;
47
+ pages: Array<{
48
+ title: string;
49
+ id: string;
50
+ urlPath: string;
51
+ icon?: string;
52
+ }>;
53
+ }>;
54
+ currentPageId: string;
55
+ pageHtml?: string;
56
+ pageComponent?: React.ComponentType<{
57
+ components?: Record<string, React.ComponentType>;
58
+ }>;
59
+ mdxComponents?: Record<string, React.ComponentType>;
60
+ pageTitle: string;
61
+ pageDescription?: string;
62
+ headings: Array<{
63
+ depth: number;
64
+ text: string;
65
+ id: string;
66
+ }>;
67
+ onNavigate: (id: string) => void;
68
+ allPages: Array<{
69
+ id: string;
70
+ title: string;
71
+ description?: string;
72
+ }>;
73
+ versioning?: VersioningInfo;
74
+ currentVersion?: string;
75
+ i18n?: I18nInfo;
76
+ currentLocale?: string;
77
+ docContext?: Array<{
78
+ id: string;
79
+ title: string;
80
+ content: string;
81
+ }>;
82
+ }
83
+ declare function Shell({ config, navigation, currentPageId, pageHtml, pageComponent, mdxComponents, pageTitle, pageDescription, headings, onNavigate, allPages, versioning, currentVersion, i18n, currentLocale, docContext, }: ShellProps): react_jsx_runtime.JSX.Element;
84
+
85
+ interface AiChatProps {
86
+ provider: "openai" | "anthropic" | "custom";
87
+ model?: string;
88
+ apiKey?: string;
89
+ context?: string;
90
+ }
91
+ declare function AiChat({ provider, model, apiKey, context }: AiChatProps): react_jsx_runtime.JSX.Element;
92
+
93
+ interface ThemeTokens {
94
+ bg: string;
95
+ sf: string;
96
+ sfH: string;
97
+ bd: string;
98
+ tx: string;
99
+ tx2: string;
100
+ txM: string;
101
+ ac: string;
102
+ acD: string;
103
+ acT: string;
104
+ cdBg: string;
105
+ cdTx: string;
106
+ sbBg: string;
107
+ hdBg: string;
108
+ }
109
+ interface ThemePreset {
110
+ dark: ThemeTokens;
111
+ light: ThemeTokens;
112
+ fonts: {
113
+ heading: string;
114
+ body: string;
115
+ code: string;
116
+ };
117
+ }
118
+ declare const THEME_PRESETS: {
119
+ readonly amber: {
120
+ readonly dark: {
121
+ readonly bg: "#09090b";
122
+ readonly sf: "#111114";
123
+ readonly sfH: "#18181c";
124
+ readonly bd: "#1e1e24";
125
+ readonly tx: "#e4e4e7";
126
+ readonly tx2: "#a1a1aa";
127
+ readonly txM: "#919199";
128
+ readonly ac: "#e8a845";
129
+ readonly acD: "rgba(232,168,69,0.12)";
130
+ readonly acT: "#fbbf24";
131
+ readonly cdBg: "#0c0c0f";
132
+ readonly cdTx: "#c4c4cc";
133
+ readonly sbBg: "#0c0c0e";
134
+ readonly hdBg: "rgba(9,9,11,0.85)";
135
+ };
136
+ readonly light: {
137
+ readonly bg: "#fafaf9";
138
+ readonly sf: "#ffffff";
139
+ readonly sfH: "#f5f5f4";
140
+ readonly bd: "#e7e5e4";
141
+ readonly tx: "#1c1917";
142
+ readonly tx2: "#57534e";
143
+ readonly txM: "#706b66";
144
+ readonly ac: "#96640a";
145
+ readonly acD: "rgba(150,100,10,0.08)";
146
+ readonly acT: "#7a5208";
147
+ readonly cdBg: "#f5f3f0";
148
+ readonly cdTx: "#2c2520";
149
+ readonly sbBg: "#f5f5f3";
150
+ readonly hdBg: "rgba(250,250,249,0.85)";
151
+ };
152
+ readonly fonts: {
153
+ readonly heading: "Instrument Serif";
154
+ readonly body: "DM Sans";
155
+ readonly code: "JetBrains Mono";
156
+ };
157
+ };
158
+ readonly editorial: {
159
+ readonly dark: {
160
+ readonly bg: "#080c1f";
161
+ readonly sf: "#0e1333";
162
+ readonly sfH: "#141940";
163
+ readonly bd: "#1a2050";
164
+ readonly tx: "#e8e6f0";
165
+ readonly tx2: "#b5b1c8";
166
+ readonly txM: "#9490ae";
167
+ readonly ac: "#ff6b4a";
168
+ readonly acD: "rgba(255,107,74,0.1)";
169
+ readonly acT: "#ff8a70";
170
+ readonly cdBg: "#0a0e27";
171
+ readonly cdTx: "#b8b4cc";
172
+ readonly sbBg: "#0a0e27";
173
+ readonly hdBg: "rgba(8,12,31,0.9)";
174
+ };
175
+ readonly light: {
176
+ readonly bg: "#f6f4f0";
177
+ readonly sf: "#ffffff";
178
+ readonly sfH: "#eeece6";
179
+ readonly bd: "#ddd9d0";
180
+ readonly tx: "#1a1716";
181
+ readonly tx2: "#4a443e";
182
+ readonly txM: "#706960";
183
+ readonly ac: "#b83d22";
184
+ readonly acD: "rgba(184,61,34,0.07)";
185
+ readonly acT: "#9c3019";
186
+ readonly cdBg: "#edeae4";
187
+ readonly cdTx: "#3a3530";
188
+ readonly sbBg: "#f0ede8";
189
+ readonly hdBg: "rgba(246,244,240,0.92)";
190
+ };
191
+ readonly fonts: {
192
+ readonly heading: "Cormorant Garamond";
193
+ readonly body: "Bricolage Grotesque";
194
+ readonly code: "Fira Code";
195
+ };
196
+ };
197
+ };
198
+ type PresetName = keyof typeof THEME_PRESETS;
199
+
200
+ export { AiChat, type AiChatProps, type PresetName, Shell, THEME_PRESETS, type ThemePreset, type ThemeTokens };
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ import {
2
+ AiChat,
3
+ Shell,
4
+ THEME_PRESETS,
5
+ entry_default
6
+ } from "./chunk-MEP7P6A7.js";
7
+ export {
8
+ AiChat,
9
+ entry_default as App,
10
+ Shell,
11
+ THEME_PRESETS
12
+ };
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@tomehq/theme",
3
+ "version": "0.1.0",
4
+ "description": "Tome default theme and React app shell",
5
+ "type": "module",
6
+ "main": "./src/index.tsx",
7
+ "exports": {
8
+ ".": "./src/index.tsx",
9
+ "./entry": "./src/entry.tsx"
10
+ },
11
+ "dependencies": {
12
+ "@tomehq/components": "0.1.0",
13
+ "@tomehq/core": "0.1.0"
14
+ },
15
+ "peerDependencies": {
16
+ "react": "^18.0.0 || ^19.0.0",
17
+ "react-dom": "^18.0.0 || ^19.0.0",
18
+ "@docsearch/react": "^3.0.0",
19
+ "@docsearch/css": "^3.0.0"
20
+ },
21
+ "peerDependenciesMeta": {
22
+ "@docsearch/react": {
23
+ "optional": true
24
+ },
25
+ "@docsearch/css": {
26
+ "optional": true
27
+ }
28
+ },
29
+ "devDependencies": {
30
+ "@testing-library/jest-dom": "^6.9.1",
31
+ "@testing-library/react": "^16.3.2",
32
+ "@testing-library/user-event": "^14.6.1",
33
+ "@types/react": "^19.0.0",
34
+ "@types/react-dom": "^19.0.0",
35
+ "jsdom": "^28.1.0",
36
+ "react": "^19.0.0",
37
+ "react-dom": "^19.0.0",
38
+ "tsup": "^8.0.0",
39
+ "typescript": "^5.5.0"
40
+ },
41
+ "license": "MIT",
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "https://github.com/vxcozy/tome.git",
45
+ "directory": "packages/theme"
46
+ },
47
+ "scripts": {
48
+ "build": "tsup src/index.tsx src/entry.tsx --format esm --dts --external react --external react-dom --external 'virtual:tome/config' --external 'virtual:tome/routes' --external 'virtual:tome/page-loader' --external 'virtual:tome/doc-context' --external '@tomehq/components'",
49
+ "dev": "tsup src/index.tsx src/entry.tsx --format esm --dts --external react --external react-dom --external 'virtual:tome/config' --external 'virtual:tome/routes' --external 'virtual:tome/page-loader' --external 'virtual:tome/doc-context' --external '@tomehq/components' --watch",
50
+ "clean": "rm -rf dist"
51
+ }
52
+ }
@@ -0,0 +1,308 @@
1
+ import React from "react";
2
+ import { describe, it, expect, vi, beforeAll, beforeEach, afterEach } from "vitest";
3
+ import { render, screen, fireEvent, act } from "@testing-library/react";
4
+ import { AiChat } from "./AiChat.js";
5
+
6
+ // ── jsdom matchMedia mock ─────────────────────────────────
7
+ beforeAll(() => {
8
+ Object.defineProperty(window, "matchMedia", {
9
+ writable: true,
10
+ value: vi.fn().mockImplementation((query: string) => ({
11
+ matches: false,
12
+ media: query,
13
+ addEventListener: vi.fn(),
14
+ removeEventListener: vi.fn(),
15
+ })),
16
+ });
17
+ });
18
+
19
+ // ── Shared helpers ───────────────────────────────────────
20
+ const defaultProps = {
21
+ provider: "openai" as const,
22
+ model: "gpt-4o-mini",
23
+ };
24
+
25
+ function renderChat(overrides: Partial<React.ComponentProps<typeof AiChat>> = {}) {
26
+ return render(<AiChat {...defaultProps} {...overrides} />);
27
+ }
28
+
29
+ // ── Rendering ────────────────────────────────────────────
30
+
31
+ describe("AiChat rendering", () => {
32
+ it("renders floating button when closed", () => {
33
+ renderChat();
34
+ expect(screen.getByTestId("ai-chat-button")).toBeInTheDocument();
35
+ expect(screen.queryByTestId("ai-chat-panel")).not.toBeInTheDocument();
36
+ });
37
+
38
+ it("opens chat panel on button click", () => {
39
+ renderChat();
40
+ fireEvent.click(screen.getByTestId("ai-chat-button"));
41
+ expect(screen.getByTestId("ai-chat-panel")).toBeInTheDocument();
42
+ expect(screen.queryByTestId("ai-chat-button")).not.toBeInTheDocument();
43
+ });
44
+
45
+ it("shows input field when open", () => {
46
+ renderChat();
47
+ fireEvent.click(screen.getByTestId("ai-chat-button"));
48
+ expect(screen.getByTestId("ai-chat-input")).toBeInTheDocument();
49
+ });
50
+
51
+ it("closes panel on close button click", () => {
52
+ renderChat();
53
+ fireEvent.click(screen.getByTestId("ai-chat-button"));
54
+ expect(screen.getByTestId("ai-chat-panel")).toBeInTheDocument();
55
+ fireEvent.click(screen.getByTestId("ai-chat-close"));
56
+ expect(screen.queryByTestId("ai-chat-panel")).not.toBeInTheDocument();
57
+ expect(screen.getByTestId("ai-chat-button")).toBeInTheDocument();
58
+ });
59
+
60
+ it("shows 'Ask AI' branding in the header", () => {
61
+ renderChat();
62
+ fireEvent.click(screen.getByTestId("ai-chat-button"));
63
+ expect(screen.getByText("Ask AI")).toBeInTheDocument();
64
+ });
65
+ });
66
+
67
+ // ── API Key ──────────────────────────────────────────────
68
+
69
+ describe("AiChat API key handling", () => {
70
+ it("shows 'AI not configured' message when no key provided", () => {
71
+ renderChat({ apiKey: undefined });
72
+ fireEvent.click(screen.getByTestId("ai-chat-button"));
73
+ expect(screen.getByTestId("ai-chat-no-key")).toBeInTheDocument();
74
+ expect(screen.getByText("AI not configured")).toBeInTheDocument();
75
+ });
76
+
77
+ it("does not show 'Set API key' message when key is provided", () => {
78
+ renderChat({ apiKey: "test-key-123" });
79
+ fireEvent.click(screen.getByTestId("ai-chat-button"));
80
+ expect(screen.queryByTestId("ai-chat-no-key")).not.toBeInTheDocument();
81
+ });
82
+
83
+ it("reads API key from window.__TOME_AI_KEY__ when prop is not set", () => {
84
+ (window as any).__TOME_AI_KEY__ = "window-key-456";
85
+ renderChat({ apiKey: undefined });
86
+ fireEvent.click(screen.getByTestId("ai-chat-button"));
87
+ expect(screen.queryByTestId("ai-chat-no-key")).not.toBeInTheDocument();
88
+ delete (window as any).__TOME_AI_KEY__;
89
+ });
90
+
91
+ it("disables input when no API key is available", () => {
92
+ renderChat({ apiKey: undefined });
93
+ fireEvent.click(screen.getByTestId("ai-chat-button"));
94
+ const input = screen.getByTestId("ai-chat-input") as HTMLInputElement;
95
+ expect(input.disabled).toBe(true);
96
+ });
97
+ });
98
+
99
+ // ── User input ───────────────────────────────────────────
100
+
101
+ describe("AiChat user messages", () => {
102
+ beforeEach(() => {
103
+ vi.useFakeTimers();
104
+ global.fetch = vi.fn().mockResolvedValue({
105
+ ok: true,
106
+ json: () => Promise.resolve({
107
+ choices: [{ message: { content: "AI response here" } }],
108
+ }),
109
+ }) as any;
110
+ });
111
+
112
+ afterEach(() => {
113
+ vi.useRealTimers();
114
+ vi.restoreAllMocks();
115
+ });
116
+
117
+ it("renders user message when user submits text", async () => {
118
+ renderChat({ apiKey: "test-key" });
119
+ fireEvent.click(screen.getByTestId("ai-chat-button"));
120
+
121
+ const input = screen.getByTestId("ai-chat-input");
122
+ fireEvent.change(input, { target: { value: "Hello AI" } });
123
+ fireEvent.click(screen.getByTestId("ai-chat-send"));
124
+
125
+ expect(screen.getByText("Hello AI")).toBeInTheDocument();
126
+ expect(screen.getByTestId("ai-chat-message-user")).toBeInTheDocument();
127
+ });
128
+
129
+ it("shows loading indicator while waiting for response", async () => {
130
+ // Use a promise that we can control to keep loading state active
131
+ let resolveResponse!: (value: any) => void;
132
+ global.fetch = vi.fn().mockReturnValue(
133
+ new Promise((resolve) => { resolveResponse = resolve; }),
134
+ ) as any;
135
+
136
+ renderChat({ apiKey: "test-key" });
137
+ fireEvent.click(screen.getByTestId("ai-chat-button"));
138
+
139
+ const input = screen.getByTestId("ai-chat-input");
140
+ fireEvent.change(input, { target: { value: "Hello AI" } });
141
+ fireEvent.click(screen.getByTestId("ai-chat-send"));
142
+
143
+ expect(screen.getByTestId("ai-chat-loading")).toBeInTheDocument();
144
+ expect(screen.getByText("Thinking...")).toBeInTheDocument();
145
+
146
+ // Clean up
147
+ await act(async () => {
148
+ resolveResponse({
149
+ ok: true,
150
+ json: () => Promise.resolve({ choices: [{ message: { content: "Done" } }] }),
151
+ });
152
+ });
153
+ });
154
+
155
+ it("renders assistant response after API call", async () => {
156
+ renderChat({ apiKey: "test-key" });
157
+ fireEvent.click(screen.getByTestId("ai-chat-button"));
158
+
159
+ const input = screen.getByTestId("ai-chat-input");
160
+ fireEvent.change(input, { target: { value: "Hello AI" } });
161
+
162
+ await act(async () => {
163
+ fireEvent.click(screen.getByTestId("ai-chat-send"));
164
+ });
165
+
166
+ // Wait for the response to render
167
+ await act(async () => {
168
+ await vi.advanceTimersByTimeAsync(10);
169
+ });
170
+
171
+ expect(screen.getByText("AI response here")).toBeInTheDocument();
172
+ expect(screen.getByTestId("ai-chat-message-assistant")).toBeInTheDocument();
173
+ });
174
+
175
+ it("submits on Enter key press", async () => {
176
+ renderChat({ apiKey: "test-key" });
177
+ fireEvent.click(screen.getByTestId("ai-chat-button"));
178
+
179
+ const input = screen.getByTestId("ai-chat-input");
180
+ fireEvent.change(input, { target: { value: "Question" } });
181
+ fireEvent.keyDown(input, { key: "Enter" });
182
+
183
+ expect(screen.getByText("Question")).toBeInTheDocument();
184
+ });
185
+
186
+ it("clears input after submission", async () => {
187
+ renderChat({ apiKey: "test-key" });
188
+ fireEvent.click(screen.getByTestId("ai-chat-button"));
189
+
190
+ const input = screen.getByTestId("ai-chat-input") as HTMLInputElement;
191
+ fireEvent.change(input, { target: { value: "Hello" } });
192
+ fireEvent.click(screen.getByTestId("ai-chat-send"));
193
+
194
+ expect(input.value).toBe("");
195
+ });
196
+
197
+ it("does not submit empty messages", () => {
198
+ renderChat({ apiKey: "test-key" });
199
+ fireEvent.click(screen.getByTestId("ai-chat-button"));
200
+
201
+ fireEvent.click(screen.getByTestId("ai-chat-send"));
202
+ expect(screen.queryByTestId("ai-chat-message-user")).not.toBeInTheDocument();
203
+ });
204
+
205
+ it("shows error when API call fails", async () => {
206
+ global.fetch = vi.fn().mockResolvedValue({
207
+ ok: false,
208
+ status: 401,
209
+ text: () => Promise.resolve("Unauthorized"),
210
+ }) as any;
211
+
212
+ renderChat({ apiKey: "bad-key" });
213
+ fireEvent.click(screen.getByTestId("ai-chat-button"));
214
+
215
+ const input = screen.getByTestId("ai-chat-input");
216
+ fireEvent.change(input, { target: { value: "Test" } });
217
+
218
+ await act(async () => {
219
+ fireEvent.click(screen.getByTestId("ai-chat-send"));
220
+ });
221
+
222
+ await act(async () => {
223
+ await vi.advanceTimersByTimeAsync(10);
224
+ });
225
+
226
+ expect(screen.getByTestId("ai-chat-error")).toBeInTheDocument();
227
+ });
228
+ });
229
+
230
+ // ── Provider handling ────────────────────────────────────
231
+
232
+ describe("AiChat provider handling", () => {
233
+ beforeEach(() => {
234
+ vi.useFakeTimers();
235
+ });
236
+
237
+ afterEach(() => {
238
+ vi.useRealTimers();
239
+ vi.restoreAllMocks();
240
+ });
241
+
242
+ it("calls OpenAI API for openai provider", async () => {
243
+ global.fetch = vi.fn().mockResolvedValue({
244
+ ok: true,
245
+ json: () => Promise.resolve({
246
+ choices: [{ message: { content: "OpenAI response" } }],
247
+ }),
248
+ }) as any;
249
+
250
+ renderChat({ provider: "openai", apiKey: "test-key" });
251
+ fireEvent.click(screen.getByTestId("ai-chat-button"));
252
+
253
+ const input = screen.getByTestId("ai-chat-input");
254
+ fireEvent.change(input, { target: { value: "Hello" } });
255
+
256
+ await act(async () => {
257
+ fireEvent.click(screen.getByTestId("ai-chat-send"));
258
+ });
259
+
260
+ await act(async () => {
261
+ await vi.advanceTimersByTimeAsync(10);
262
+ });
263
+
264
+ expect(global.fetch).toHaveBeenCalledWith(
265
+ "https://api.openai.com/v1/chat/completions",
266
+ expect.objectContaining({
267
+ method: "POST",
268
+ headers: expect.objectContaining({
269
+ "Authorization": "Bearer test-key",
270
+ }),
271
+ }),
272
+ );
273
+ });
274
+
275
+ it("calls Anthropic API for anthropic provider", async () => {
276
+ global.fetch = vi.fn().mockResolvedValue({
277
+ ok: true,
278
+ json: () => Promise.resolve({
279
+ content: [{ text: "Anthropic response" }],
280
+ }),
281
+ }) as any;
282
+
283
+ renderChat({ provider: "anthropic", apiKey: "test-key" });
284
+ fireEvent.click(screen.getByTestId("ai-chat-button"));
285
+
286
+ const input = screen.getByTestId("ai-chat-input");
287
+ fireEvent.change(input, { target: { value: "Hello" } });
288
+
289
+ await act(async () => {
290
+ fireEvent.click(screen.getByTestId("ai-chat-send"));
291
+ });
292
+
293
+ await act(async () => {
294
+ await vi.advanceTimersByTimeAsync(10);
295
+ });
296
+
297
+ expect(global.fetch).toHaveBeenCalledWith(
298
+ "https://api.anthropic.com/v1/messages",
299
+ expect.objectContaining({
300
+ method: "POST",
301
+ headers: expect.objectContaining({
302
+ "x-api-key": "test-key",
303
+ "anthropic-version": "2023-06-01",
304
+ }),
305
+ }),
306
+ );
307
+ });
308
+ });