@tomehq/theme 0.2.9 → 0.3.1
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/chunk-ABNPB6BB.js +2133 -0
- package/dist/chunk-CMQCNCSY.js +2127 -0
- package/dist/chunk-EK7PZUEB.js +2147 -0
- package/dist/chunk-FMOLIHQF.js +2182 -0
- package/dist/chunk-GHQ2MODM.js +2127 -0
- package/dist/chunk-GR2WCRGK.js +2182 -0
- package/dist/chunk-HNLKDQ64.js +2139 -0
- package/dist/chunk-IW3NHNOQ.js +2187 -0
- package/dist/chunk-JSPFS7G5.js +2102 -0
- package/dist/chunk-KQBY2JDB.js +2112 -0
- package/dist/chunk-SWFYJO5H.js +2187 -0
- package/dist/chunk-VKEQHP2E.js +2133 -0
- package/dist/chunk-YXKONM3A.js +2192 -0
- package/dist/entry.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +3 -3
- package/src/Shell.tsx +15 -2
- package/src/entry.tsx +116 -33
package/dist/entry.js
CHANGED
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tomehq/theme",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
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.
|
|
13
|
-
"@tomehq/core": "0.
|
|
12
|
+
"@tomehq/components": "0.3.1",
|
|
13
|
+
"@tomehq/core": "0.3.1"
|
|
14
14
|
},
|
|
15
15
|
"peerDependencies": {
|
|
16
16
|
"react": "^18.0.0 || ^19.0.0",
|
package/src/Shell.tsx
CHANGED
|
@@ -359,6 +359,8 @@ export function Shell({
|
|
|
359
359
|
const isOldVersion = versioning && currentVersion && currentVersion !== versioning.current;
|
|
360
360
|
const [expanded, setExpanded] = useState<string[]>(navigation.map(n => n.section));
|
|
361
361
|
const contentRef = useRef<HTMLDivElement>(null);
|
|
362
|
+
const htmlContentRef = useRef<HTMLDivElement>(null);
|
|
363
|
+
const lastHtmlRef = useRef<string>("");
|
|
362
364
|
const [wide, setWide] = useState(() => typeof window !== "undefined" && window.innerWidth > 1100);
|
|
363
365
|
|
|
364
366
|
const preset = (config.theme?.preset || "amber") as PresetName;
|
|
@@ -531,6 +533,17 @@ export function Shell({
|
|
|
531
533
|
// Reset active heading when page changes
|
|
532
534
|
useEffect(() => { setActiveHeadingId(""); }, [currentPageId]);
|
|
533
535
|
|
|
536
|
+
// Set HTML content via ref so React doesn't re-set innerHTML on re-renders
|
|
537
|
+
// (scroll-spy state changes would otherwise destroy client-side mermaid SVGs)
|
|
538
|
+
useEffect(() => {
|
|
539
|
+
if (!htmlContentRef.current || !pageHtml) return;
|
|
540
|
+
const stripped = pageHtml.replace(/<h1[^>]*>[\s\S]*?<\/h1>\s*/, "");
|
|
541
|
+
if (stripped !== lastHtmlRef.current) {
|
|
542
|
+
htmlContentRef.current.innerHTML = stripped;
|
|
543
|
+
lastHtmlRef.current = stripped;
|
|
544
|
+
}
|
|
545
|
+
}, [pageHtml]);
|
|
546
|
+
|
|
534
547
|
// Smooth scroll handler for TOC links
|
|
535
548
|
const scrollToHeading = useCallback((e: React.MouseEvent<HTMLAnchorElement>, id: string) => {
|
|
536
549
|
e.preventDefault();
|
|
@@ -584,7 +597,7 @@ export function Shell({
|
|
|
584
597
|
display: "flex", alignItems: "center", justifyContent: "center", gap: 12,
|
|
585
598
|
background: "var(--ac)", color: "#fff", padding: "8px 16px",
|
|
586
599
|
fontSize: 13, fontFamily: "var(--font-body)", fontWeight: 500, textAlign: "center",
|
|
587
|
-
width: "
|
|
600
|
+
width: "100vw", boxSizing: "border-box",
|
|
588
601
|
}}>
|
|
589
602
|
{config.banner.link ? (
|
|
590
603
|
<a
|
|
@@ -977,7 +990,7 @@ export function Shell({
|
|
|
977
990
|
) : (
|
|
978
991
|
<div
|
|
979
992
|
className="tome-content"
|
|
980
|
-
|
|
993
|
+
ref={htmlContentRef}
|
|
981
994
|
/>
|
|
982
995
|
)}
|
|
983
996
|
</div>
|
package/src/entry.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState, useEffect, useCallback } from "react";
|
|
1
|
+
import React, { useState, useEffect, useCallback, useRef } from "react";
|
|
2
2
|
import { createRoot } from "react-dom/client";
|
|
3
3
|
import { Shell } from "./Shell.js";
|
|
4
4
|
import { pathnameToPageId as _pathnameToPageId, pageIdToPath as _pageIdToPath } from "./routing.js";
|
|
@@ -75,6 +75,27 @@ const contentStyles = `
|
|
|
75
75
|
.tome-mermaid { margin: 1.2em 0; text-align: center; overflow-x: auto; }
|
|
76
76
|
.tome-mermaid svg { max-width: 100%; height: auto; overflow: visible; }
|
|
77
77
|
.tome-mermaid svg .nodeLabel { overflow: visible; white-space: nowrap; }
|
|
78
|
+
/* Ensure mermaid text meets WCAG AA contrast in light mode */
|
|
79
|
+
/* Mermaid v11 uses foreignObject with inline-styled spans — !important needed */
|
|
80
|
+
html:not(.dark) .tome-mermaid svg .nodeLabel,
|
|
81
|
+
html:not(.dark) .tome-mermaid svg .nodeLabel span,
|
|
82
|
+
html:not(.dark) .tome-mermaid svg .nodeLabel div,
|
|
83
|
+
html:not(.dark) .tome-mermaid svg foreignObject div,
|
|
84
|
+
html:not(.dark) .tome-mermaid svg foreignObject span { color: #1a1a1a !important; }
|
|
85
|
+
html:not(.dark) .tome-mermaid svg .edgeLabel,
|
|
86
|
+
html:not(.dark) .tome-mermaid svg .edgeLabel span { color: #333 !important; }
|
|
87
|
+
html:not(.dark) .tome-mermaid svg text { fill: #1a1a1a !important; }
|
|
88
|
+
html:not(.dark) .tome-mermaid svg .node rect,
|
|
89
|
+
html:not(.dark) .tome-mermaid svg .node polygon { stroke: #555 !important; }
|
|
90
|
+
/* Dark mode: force bright text in mermaid nodes for readability */
|
|
91
|
+
html.dark .tome-mermaid svg .nodeLabel,
|
|
92
|
+
html.dark .tome-mermaid svg .nodeLabel span,
|
|
93
|
+
html.dark .tome-mermaid svg .nodeLabel div,
|
|
94
|
+
html.dark .tome-mermaid svg foreignObject div,
|
|
95
|
+
html.dark .tome-mermaid svg foreignObject span { color: #f0f0f0 !important; }
|
|
96
|
+
html.dark .tome-mermaid svg .edgeLabel,
|
|
97
|
+
html.dark .tome-mermaid svg .edgeLabel span { color: #ddd !important; }
|
|
98
|
+
html.dark .tome-mermaid svg text { fill: #f0f0f0 !important; }
|
|
78
99
|
|
|
79
100
|
/* Mobile responsive content */
|
|
80
101
|
@media (max-width: 767px) {
|
|
@@ -135,17 +156,22 @@ function pageIdToPath(id: string): string {
|
|
|
135
156
|
return _pageIdToPath(id, basePath, routes);
|
|
136
157
|
}
|
|
137
158
|
|
|
159
|
+
// ── EAGER INITIAL PAGE LOAD ──────────────────────────────
|
|
160
|
+
// Start loading the initial page at module scope — before React mounts —
|
|
161
|
+
// so the data is ready (or nearly ready) by the time App first renders.
|
|
162
|
+
// This eliminates the "Loading..." flash on production.
|
|
163
|
+
const _initialPageId = resolveInitialPageId(
|
|
164
|
+
window.location.pathname,
|
|
165
|
+
window.location.hash,
|
|
166
|
+
routes,
|
|
167
|
+
basePath,
|
|
168
|
+
_pathnameToPageId,
|
|
169
|
+
);
|
|
170
|
+
const _initialPagePromise = loadPage(_initialPageId, routes, loadPageModule);
|
|
171
|
+
|
|
138
172
|
// ── APP ──────────────────────────────────────────────────
|
|
139
173
|
function App() {
|
|
140
|
-
const [currentPageId, setCurrentPageId] = useState(
|
|
141
|
-
resolveInitialPageId(
|
|
142
|
-
window.location.pathname,
|
|
143
|
-
window.location.hash,
|
|
144
|
-
routes,
|
|
145
|
-
basePath,
|
|
146
|
-
_pathnameToPageId,
|
|
147
|
-
)
|
|
148
|
-
);
|
|
174
|
+
const [currentPageId, setCurrentPageId] = useState(_initialPageId);
|
|
149
175
|
|
|
150
176
|
const [pageData, setPageData] = useState<LoadedPage | null>(null);
|
|
151
177
|
const [loading, setLoading] = useState(true);
|
|
@@ -176,7 +202,7 @@ function App() {
|
|
|
176
202
|
}
|
|
177
203
|
}, []);
|
|
178
204
|
|
|
179
|
-
// Initial page load
|
|
205
|
+
// Initial page load — use the eagerly-started promise from module scope
|
|
180
206
|
useEffect(() => {
|
|
181
207
|
// If user landed on a legacy hash URL, redirect to clean path
|
|
182
208
|
const hash = window.location.hash.slice(1);
|
|
@@ -185,7 +211,13 @@ function App() {
|
|
|
185
211
|
window.history.replaceState(null, "", fullPath);
|
|
186
212
|
navigateTo(hash, { replace: true });
|
|
187
213
|
} else {
|
|
188
|
-
|
|
214
|
+
// Use the pre-fetched promise instead of starting a new load
|
|
215
|
+
const fullPath = pageIdToPath(currentPageId);
|
|
216
|
+
window.history.replaceState(null, "", fullPath);
|
|
217
|
+
_initialPagePromise.then((data) => {
|
|
218
|
+
setPageData(data);
|
|
219
|
+
setLoading(false);
|
|
220
|
+
});
|
|
189
221
|
}
|
|
190
222
|
}, []);
|
|
191
223
|
|
|
@@ -201,7 +233,30 @@ function App() {
|
|
|
201
233
|
return () => window.removeEventListener("popstate", onPopState);
|
|
202
234
|
}, [currentPageId, navigateTo]);
|
|
203
235
|
|
|
204
|
-
// Mermaid diagram rendering: load from CDN and render .tome-mermaid elements
|
|
236
|
+
// Mermaid diagram rendering: load from CDN and render .tome-mermaid elements.
|
|
237
|
+
// Also re-renders when theme changes (dark ↔ light) so colors stay correct.
|
|
238
|
+
const mermaidModuleRef = useRef<any>(null);
|
|
239
|
+
const [mermaidTheme, setMermaidTheme] = useState(() => {
|
|
240
|
+
if (typeof document === "undefined") return "light";
|
|
241
|
+
// Check the class first (already set), then fall back to config + system preference
|
|
242
|
+
// to avoid a white flash before Shell syncs the dark class onto <html>
|
|
243
|
+
if (document.documentElement.classList.contains("dark")) return "dark";
|
|
244
|
+
const mode = config.theme?.mode || "auto";
|
|
245
|
+
if (mode === "dark") return "dark";
|
|
246
|
+
if (mode === "light") return "light";
|
|
247
|
+
return window.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// Watch for dark class changes on <html> to trigger mermaid re-render
|
|
251
|
+
useEffect(() => {
|
|
252
|
+
const observer = new MutationObserver(() => {
|
|
253
|
+
const isDark = document.documentElement.classList.contains("dark");
|
|
254
|
+
setMermaidTheme(isDark ? "dark" : "light");
|
|
255
|
+
});
|
|
256
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] });
|
|
257
|
+
return () => observer.disconnect();
|
|
258
|
+
}, []);
|
|
259
|
+
|
|
205
260
|
useEffect(() => {
|
|
206
261
|
const els = document.querySelectorAll(".tome-mermaid[data-mermaid]");
|
|
207
262
|
if (els.length === 0) return;
|
|
@@ -211,11 +266,12 @@ function App() {
|
|
|
211
266
|
|
|
212
267
|
(async () => {
|
|
213
268
|
try {
|
|
214
|
-
|
|
215
|
-
|
|
269
|
+
if (!mermaidModuleRef.current) {
|
|
270
|
+
mermaidModuleRef.current = (await import(/* @vite-ignore */ MERMAID_CDN)).default;
|
|
271
|
+
}
|
|
272
|
+
const mermaid = mermaidModuleRef.current;
|
|
216
273
|
if (cancelled) return;
|
|
217
|
-
const isDark =
|
|
218
|
-
// Resolve CSS variable to concrete font name — mermaid can't resolve CSS vars for text measurement
|
|
274
|
+
const isDark = mermaidTheme === "dark";
|
|
219
275
|
const resolvedFont = getComputedStyle(document.documentElement).getPropertyValue("--font-body").trim() || "sans-serif";
|
|
220
276
|
mermaid.initialize({
|
|
221
277
|
startOnLoad: false,
|
|
@@ -226,22 +282,14 @@ function App() {
|
|
|
226
282
|
|
|
227
283
|
for (let i = 0; i < els.length; i++) {
|
|
228
284
|
const el = els[i] as HTMLElement;
|
|
229
|
-
if (el.querySelector("svg")) continue; // already rendered
|
|
230
285
|
const encoded = el.getAttribute("data-mermaid");
|
|
231
286
|
if (!encoded) continue;
|
|
232
287
|
try {
|
|
233
288
|
const code = atob(encoded);
|
|
289
|
+
// Render new SVG off-screen first, then swap — avoids white flash on theme change
|
|
234
290
|
const { svg } = await mermaid.render(`tome-mermaid-${i}-${Date.now()}`, code);
|
|
235
291
|
if (!cancelled) {
|
|
236
|
-
|
|
237
|
-
try {
|
|
238
|
-
// @ts-ignore — CDN dynamic import for browser-only sanitization
|
|
239
|
-
const DOMPurify = (await import(/* @vite-ignore */ "https://cdn.jsdelivr.net/npm/dompurify@3/dist/purify.es.mjs")).default;
|
|
240
|
-
el.innerHTML = DOMPurify.sanitize(svg, { USE_PROFILES: { html: true, svg: true, svgFilters: true } });
|
|
241
|
-
} catch {
|
|
242
|
-
// DOMPurify unavailable — render without sanitization (acceptable for trusted content)
|
|
243
|
-
el.innerHTML = svg;
|
|
244
|
-
}
|
|
292
|
+
el.innerHTML = svg;
|
|
245
293
|
}
|
|
246
294
|
} catch (err) {
|
|
247
295
|
console.warn("[tome] Mermaid render failed:", err);
|
|
@@ -259,7 +307,7 @@ function App() {
|
|
|
259
307
|
})();
|
|
260
308
|
|
|
261
309
|
return () => { cancelled = true; };
|
|
262
|
-
}, [pageData, loading]);
|
|
310
|
+
}, [pageData, loading, mermaidTheme]);
|
|
263
311
|
|
|
264
312
|
const allPages = routes.map((r: any) => ({
|
|
265
313
|
id: r.id,
|
|
@@ -272,9 +320,10 @@ function App() {
|
|
|
272
320
|
const currentVersion = detectCurrentVersion(currentRoute, versions);
|
|
273
321
|
const editUrl = computeEditUrl(config.editLink, currentRoute?.filePath);
|
|
274
322
|
|
|
275
|
-
// KaTeX CSS: inject stylesheet when math is enabled
|
|
323
|
+
// KaTeX CSS: inject stylesheet when math is enabled or math placeholders exist
|
|
276
324
|
useEffect(() => {
|
|
277
|
-
|
|
325
|
+
const hasMathPlaceholders = document.querySelectorAll(".tome-math[data-math]").length > 0;
|
|
326
|
+
if (!(config as any).math && !hasMathPlaceholders) return;
|
|
278
327
|
const id = "tome-katex-css";
|
|
279
328
|
if (document.getElementById(id)) return;
|
|
280
329
|
const link = document.createElement("link");
|
|
@@ -283,7 +332,41 @@ function App() {
|
|
|
283
332
|
link.href = "https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css";
|
|
284
333
|
link.crossOrigin = "anonymous";
|
|
285
334
|
document.head.appendChild(link);
|
|
286
|
-
}, []);
|
|
335
|
+
}, [pageData, loading]);
|
|
336
|
+
|
|
337
|
+
// Client-side KaTeX rendering for MDX math placeholders (.tome-math[data-math])
|
|
338
|
+
useEffect(() => {
|
|
339
|
+
const els = document.querySelectorAll(".tome-math[data-math]");
|
|
340
|
+
if (els.length === 0) return;
|
|
341
|
+
let cancelled = false;
|
|
342
|
+
|
|
343
|
+
const KATEX_CDN = "https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.mjs";
|
|
344
|
+
|
|
345
|
+
(async () => {
|
|
346
|
+
try {
|
|
347
|
+
const katex = (await import(/* @vite-ignore */ KATEX_CDN)).default;
|
|
348
|
+
if (cancelled) return;
|
|
349
|
+
for (const el of els) {
|
|
350
|
+
const encoded = el.getAttribute("data-math");
|
|
351
|
+
if (!encoded) continue;
|
|
352
|
+
try {
|
|
353
|
+
const tex = atob(encoded);
|
|
354
|
+
const isBlock = el.classList.contains("tome-math-block");
|
|
355
|
+
katex.render(tex, el as HTMLElement, {
|
|
356
|
+
displayMode: isBlock,
|
|
357
|
+
throwOnError: false,
|
|
358
|
+
});
|
|
359
|
+
} catch (err) {
|
|
360
|
+
console.warn("[tome] KaTeX render failed:", err);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
} catch (err) {
|
|
364
|
+
console.warn("[tome] Failed to load KaTeX from CDN:", err);
|
|
365
|
+
}
|
|
366
|
+
})();
|
|
367
|
+
|
|
368
|
+
return () => { cancelled = true; };
|
|
369
|
+
}, [pageData, loading]);
|
|
287
370
|
|
|
288
371
|
return (
|
|
289
372
|
<>
|
|
@@ -292,10 +375,10 @@ function App() {
|
|
|
292
375
|
config={config}
|
|
293
376
|
navigation={navigation}
|
|
294
377
|
currentPageId={currentPageId}
|
|
295
|
-
pageHtml={!pageData?.isMdx ? (loading ? "
|
|
378
|
+
pageHtml={!pageData?.isMdx ? (loading ? "" : pageData?.html || "<p>Page not found</p>") : undefined}
|
|
296
379
|
pageComponent={pageData?.isMdx ? pageData.component : undefined}
|
|
297
380
|
mdxComponents={MDX_COMPONENTS}
|
|
298
|
-
pageTitle={pageData?.frontmatter.title || (loading ? "
|
|
381
|
+
pageTitle={pageData?.frontmatter.title || (loading ? "" : "Not Found")}
|
|
299
382
|
pageDescription={pageData?.frontmatter.description}
|
|
300
383
|
headings={pageData?.headings || []}
|
|
301
384
|
tocEnabled={pageData?.frontmatter.toc !== false}
|