@tomehq/theme 0.2.9 → 0.3.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/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-JSPFS7G5.js +2102 -0
- package/dist/chunk-KQBY2JDB.js +2112 -0
- package/dist/chunk-VKEQHP2E.js +2133 -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 +85 -20
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.0",
|
|
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.0",
|
|
13
|
+
"@tomehq/core": "0.3.0"
|
|
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) {
|
|
@@ -201,7 +222,23 @@ function App() {
|
|
|
201
222
|
return () => window.removeEventListener("popstate", onPopState);
|
|
202
223
|
}, [currentPageId, navigateTo]);
|
|
203
224
|
|
|
204
|
-
// Mermaid diagram rendering: load from CDN and render .tome-mermaid elements
|
|
225
|
+
// Mermaid diagram rendering: load from CDN and render .tome-mermaid elements.
|
|
226
|
+
// Also re-renders when theme changes (dark ↔ light) so colors stay correct.
|
|
227
|
+
const mermaidModuleRef = useRef<any>(null);
|
|
228
|
+
const [mermaidTheme, setMermaidTheme] = useState(() =>
|
|
229
|
+
typeof document !== "undefined" && document.documentElement.classList.contains("dark") ? "dark" : "light"
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
// Watch for dark class changes on <html> to trigger mermaid re-render
|
|
233
|
+
useEffect(() => {
|
|
234
|
+
const observer = new MutationObserver(() => {
|
|
235
|
+
const isDark = document.documentElement.classList.contains("dark");
|
|
236
|
+
setMermaidTheme(isDark ? "dark" : "light");
|
|
237
|
+
});
|
|
238
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] });
|
|
239
|
+
return () => observer.disconnect();
|
|
240
|
+
}, []);
|
|
241
|
+
|
|
205
242
|
useEffect(() => {
|
|
206
243
|
const els = document.querySelectorAll(".tome-mermaid[data-mermaid]");
|
|
207
244
|
if (els.length === 0) return;
|
|
@@ -211,11 +248,12 @@ function App() {
|
|
|
211
248
|
|
|
212
249
|
(async () => {
|
|
213
250
|
try {
|
|
214
|
-
|
|
215
|
-
|
|
251
|
+
if (!mermaidModuleRef.current) {
|
|
252
|
+
mermaidModuleRef.current = (await import(/* @vite-ignore */ MERMAID_CDN)).default;
|
|
253
|
+
}
|
|
254
|
+
const mermaid = mermaidModuleRef.current;
|
|
216
255
|
if (cancelled) return;
|
|
217
|
-
const isDark =
|
|
218
|
-
// Resolve CSS variable to concrete font name — mermaid can't resolve CSS vars for text measurement
|
|
256
|
+
const isDark = mermaidTheme === "dark";
|
|
219
257
|
const resolvedFont = getComputedStyle(document.documentElement).getPropertyValue("--font-body").trim() || "sans-serif";
|
|
220
258
|
mermaid.initialize({
|
|
221
259
|
startOnLoad: false,
|
|
@@ -226,22 +264,14 @@ function App() {
|
|
|
226
264
|
|
|
227
265
|
for (let i = 0; i < els.length; i++) {
|
|
228
266
|
const el = els[i] as HTMLElement;
|
|
229
|
-
if (el.querySelector("svg")) continue; // already rendered
|
|
230
267
|
const encoded = el.getAttribute("data-mermaid");
|
|
231
268
|
if (!encoded) continue;
|
|
232
269
|
try {
|
|
233
270
|
const code = atob(encoded);
|
|
271
|
+
// Render new SVG off-screen first, then swap — avoids white flash on theme change
|
|
234
272
|
const { svg } = await mermaid.render(`tome-mermaid-${i}-${Date.now()}`, code);
|
|
235
273
|
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
|
-
}
|
|
274
|
+
el.innerHTML = svg;
|
|
245
275
|
}
|
|
246
276
|
} catch (err) {
|
|
247
277
|
console.warn("[tome] Mermaid render failed:", err);
|
|
@@ -259,7 +289,7 @@ function App() {
|
|
|
259
289
|
})();
|
|
260
290
|
|
|
261
291
|
return () => { cancelled = true; };
|
|
262
|
-
}, [pageData, loading]);
|
|
292
|
+
}, [pageData, loading, mermaidTheme]);
|
|
263
293
|
|
|
264
294
|
const allPages = routes.map((r: any) => ({
|
|
265
295
|
id: r.id,
|
|
@@ -272,9 +302,10 @@ function App() {
|
|
|
272
302
|
const currentVersion = detectCurrentVersion(currentRoute, versions);
|
|
273
303
|
const editUrl = computeEditUrl(config.editLink, currentRoute?.filePath);
|
|
274
304
|
|
|
275
|
-
// KaTeX CSS: inject stylesheet when math is enabled
|
|
305
|
+
// KaTeX CSS: inject stylesheet when math is enabled or math placeholders exist
|
|
276
306
|
useEffect(() => {
|
|
277
|
-
|
|
307
|
+
const hasMathPlaceholders = document.querySelectorAll(".tome-math[data-math]").length > 0;
|
|
308
|
+
if (!(config as any).math && !hasMathPlaceholders) return;
|
|
278
309
|
const id = "tome-katex-css";
|
|
279
310
|
if (document.getElementById(id)) return;
|
|
280
311
|
const link = document.createElement("link");
|
|
@@ -283,7 +314,41 @@ function App() {
|
|
|
283
314
|
link.href = "https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css";
|
|
284
315
|
link.crossOrigin = "anonymous";
|
|
285
316
|
document.head.appendChild(link);
|
|
286
|
-
}, []);
|
|
317
|
+
}, [pageData, loading]);
|
|
318
|
+
|
|
319
|
+
// Client-side KaTeX rendering for MDX math placeholders (.tome-math[data-math])
|
|
320
|
+
useEffect(() => {
|
|
321
|
+
const els = document.querySelectorAll(".tome-math[data-math]");
|
|
322
|
+
if (els.length === 0) return;
|
|
323
|
+
let cancelled = false;
|
|
324
|
+
|
|
325
|
+
const KATEX_CDN = "https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.mjs";
|
|
326
|
+
|
|
327
|
+
(async () => {
|
|
328
|
+
try {
|
|
329
|
+
const katex = (await import(/* @vite-ignore */ KATEX_CDN)).default;
|
|
330
|
+
if (cancelled) return;
|
|
331
|
+
for (const el of els) {
|
|
332
|
+
const encoded = el.getAttribute("data-math");
|
|
333
|
+
if (!encoded) continue;
|
|
334
|
+
try {
|
|
335
|
+
const tex = atob(encoded);
|
|
336
|
+
const isBlock = el.classList.contains("tome-math-block");
|
|
337
|
+
katex.render(tex, el as HTMLElement, {
|
|
338
|
+
displayMode: isBlock,
|
|
339
|
+
throwOnError: false,
|
|
340
|
+
});
|
|
341
|
+
} catch (err) {
|
|
342
|
+
console.warn("[tome] KaTeX render failed:", err);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
} catch (err) {
|
|
346
|
+
console.warn("[tome] Failed to load KaTeX from CDN:", err);
|
|
347
|
+
}
|
|
348
|
+
})();
|
|
349
|
+
|
|
350
|
+
return () => { cancelled = true; };
|
|
351
|
+
}, [pageData, loading]);
|
|
287
352
|
|
|
288
353
|
return (
|
|
289
354
|
<>
|