@tomehq/theme 0.3.0 → 0.3.2

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.
Files changed (54) hide show
  1. package/dist/{chunk-GR2WCRGK.js → chunk-2AXAEADQ.js} +496 -153
  2. package/dist/entry.js +1 -1
  3. package/dist/index.d.ts +21 -1
  4. package/dist/index.js +1 -1
  5. package/package.json +5 -5
  6. package/src/Shell.test.tsx +183 -0
  7. package/src/Shell.tsx +231 -21
  8. package/src/entry.tsx +207 -20
  9. package/src/global.d.ts +11 -0
  10. package/dist/chunk-2APCPR2Y.js +0 -2110
  11. package/dist/chunk-37JI6XGT.js +0 -1720
  12. package/dist/chunk-3A2LPGUL.js +0 -1991
  13. package/dist/chunk-3I2QTWTW.js +0 -1948
  14. package/dist/chunk-45M5UIAB.js +0 -2110
  15. package/dist/chunk-462AGU3S.js +0 -1959
  16. package/dist/chunk-7MUTU5D4.js +0 -1720
  17. package/dist/chunk-ABNPB6BB.js +0 -2133
  18. package/dist/chunk-BZGWSKT2.js +0 -573
  19. package/dist/chunk-CMQCNCSY.js +0 -2127
  20. package/dist/chunk-CTPOZMMK.js +0 -1703
  21. package/dist/chunk-DO544M3G.js +0 -1702
  22. package/dist/chunk-DPKZBFQP.js +0 -1777
  23. package/dist/chunk-EK7PZUEB.js +0 -2147
  24. package/dist/chunk-FMOLIHQF.js +0 -2182
  25. package/dist/chunk-FWBTK5TL.js +0 -1444
  26. package/dist/chunk-GDQIBNX5.js +0 -1962
  27. package/dist/chunk-GHQ2MODM.js +0 -2127
  28. package/dist/chunk-HNLKDQ64.js +0 -2139
  29. package/dist/chunk-INUMUXN5.js +0 -2095
  30. package/dist/chunk-JA4PMX6M.js +0 -1500
  31. package/dist/chunk-JSPFS7G5.js +0 -2102
  32. package/dist/chunk-JZRT4WNC.js +0 -1441
  33. package/dist/chunk-KQBY2JDB.js +0 -2112
  34. package/dist/chunk-LIMYFTPC.js +0 -1468
  35. package/dist/chunk-MEP7P6A7.js +0 -1500
  36. package/dist/chunk-NOZBIES7.js +0 -1948
  37. package/dist/chunk-O4GH3KYX.js +0 -1712
  38. package/dist/chunk-OEXM3BEC.js +0 -1702
  39. package/dist/chunk-Q7PYTVW3.js +0 -1771
  40. package/dist/chunk-QCWZYABW.js +0 -1468
  41. package/dist/chunk-RDF25WB2.js +0 -2085
  42. package/dist/chunk-RKTT3ZEX.js +0 -1500
  43. package/dist/chunk-S47BRMNQ.js +0 -1715
  44. package/dist/chunk-S4ZH5F56.js +0 -1949
  45. package/dist/chunk-SRD7NJHS.js +0 -1949
  46. package/dist/chunk-TQDWPSTO.js +0 -2087
  47. package/dist/chunk-TTRXRPP6.js +0 -1941
  48. package/dist/chunk-UKYFJSUA.js +0 -509
  49. package/dist/chunk-VKEQHP2E.js +0 -2133
  50. package/dist/chunk-VUT2FMSI.js +0 -1937
  51. package/dist/chunk-VVCC5JHK.js +0 -1949
  52. package/dist/chunk-W732TVBK.js +0 -1944
  53. package/dist/chunk-X4VQYPKO.js +0 -1768
  54. package/dist/chunk-YZ3P3TNS.js +0 -1760
package/src/entry.tsx CHANGED
@@ -13,11 +13,13 @@ import {
13
13
  // @ts-ignore — resolved by vite-plugin-tome
14
14
  import config from "virtual:tome/config";
15
15
  // @ts-ignore — resolved by vite-plugin-tome
16
- import { routes, navigation, versions } from "virtual:tome/routes";
16
+ import { routes, navigation, versions, i18n } from "virtual:tome/routes";
17
17
  // @ts-ignore — resolved by vite-plugin-tome
18
18
  import loadPageModule from "virtual:tome/page-loader";
19
19
  // @ts-ignore — resolved by vite-plugin-tome
20
20
  import docContext from "virtual:tome/doc-context";
21
+ // @ts-ignore — resolved by vite-plugin-tome
22
+ import overrides from "virtual:tome/overrides";
21
23
 
22
24
  // TOM-8: Built-in MDX components from @tomehq/components
23
25
  // These are injected into every MDX page automatically
@@ -32,6 +34,9 @@ import {
32
34
  PackageManager,
33
35
  TypeTable,
34
36
  FileTree,
37
+ CodeSamples,
38
+ LinkCard,
39
+ CardGrid,
35
40
  } from "@tomehq/components";
36
41
 
37
42
  const MDX_COMPONENTS: Record<string, React.ComponentType<any>> = {
@@ -45,6 +50,9 @@ const MDX_COMPONENTS: Record<string, React.ComponentType<any>> = {
45
50
  PackageManager,
46
51
  TypeTable,
47
52
  FileTree, // Sub-components accessible as <FileTree.File /> and <FileTree.Folder /> in MDX
53
+ CodeSamples,
54
+ LinkCard,
55
+ CardGrid,
48
56
  };
49
57
 
50
58
  // ── CONTENT STYLES ───────────────────────────────────────
@@ -60,15 +68,15 @@ const contentStyles = `
60
68
  .tome-content a { color: var(--ac); text-decoration: none; }
61
69
  .tome-content a:hover { text-decoration: underline; }
62
70
  .tome-content .heading-anchor { display: none; }
63
- .tome-content ul, .tome-content ol { color: var(--tx2); padding-left: 1.5em; margin-bottom: 1em; }
71
+ .tome-content ul, .tome-content ol { color: var(--tx2); padding-inline-start: 1.5em; margin-bottom: 1em; }
64
72
  .tome-content li { margin-bottom: 0.3em; line-height: 1.7; }
65
73
  .tome-content code { font-family: var(--font-code); font-size: 0.88em; background: var(--cdBg); padding: 0.15em 0.4em; border-radius: 2px; color: var(--ac); }
66
74
  .tome-content pre { margin-bottom: 1.2em; border-radius: 2px; overflow-x: auto; border: 1px solid var(--bd); }
67
75
  .tome-content pre code { background: none; padding: 1em 1.2em; display: block; font-size: 12.5px; line-height: 1.7; color: var(--cdTx); }
68
- .tome-content blockquote { border-left: 3px solid var(--ac); padding: 0.5em 1em; margin: 1em 0; background: var(--acD); border-radius: 0 2px 2px 0; }
76
+ .tome-content blockquote { border-inline-start: 3px solid var(--ac); padding: 0.5em 1em; margin: 1em 0; background: var(--acD); border-radius: 0 2px 2px 0; }
69
77
  .tome-content blockquote p { color: var(--tx2); margin: 0; }
70
78
  .tome-content table { width: 100%; border-collapse: collapse; margin-bottom: 1em; }
71
- .tome-content th, .tome-content td { padding: 0.5em 0.8em; border: 1px solid var(--bd); text-align: left; font-size: 0.9em; }
79
+ .tome-content th, .tome-content td { padding: 0.5em 0.8em; border: 1px solid var(--bd); text-align: start; font-size: 0.9em; }
72
80
  .tome-content th { background: var(--sf); font-weight: 600; }
73
81
  .tome-content img { max-width: 100%; border-radius: 2px; cursor: zoom-in; }
74
82
  .tome-content hr { border: none; border-top: 1px solid var(--bd); margin: 2em 0; }
@@ -122,6 +130,74 @@ const contentStyles = `
122
130
  background-repeat: repeat; background-size: 256px;
123
131
  }
124
132
 
133
+ /* ── Expressive code blocks ───────────────────────────── */
134
+
135
+ /* Code block wrapper (for titled blocks) */
136
+ .tome-code-block-wrapper { position: relative; margin-bottom: 1.2em; border: 1px solid var(--bd); border-radius: 2px; overflow: hidden; }
137
+ .tome-code-block-wrapper pre { margin-bottom: 0; border: none; border-radius: 0; }
138
+ .tome-code-title {
139
+ font-family: var(--font-code); font-size: 12px; color: var(--tx2);
140
+ background: var(--sf); padding: 6px 12px; border-bottom: 1px solid var(--bd);
141
+ letter-spacing: 0.01em; font-weight: 500;
142
+ }
143
+
144
+ /* Line highlighting */
145
+ .tome-content pre .line.tome-line-highlight {
146
+ background: rgba(139, 148, 158, 0.1);
147
+ display: inline-block; width: 100%; margin: 0 -1.2em; padding: 0 1.2em;
148
+ }
149
+ html.dark .tome-content pre .line.tome-line-highlight {
150
+ background: rgba(200, 210, 220, 0.08);
151
+ }
152
+
153
+ /* Diff lines */
154
+ .tome-content pre .line.tome-line-added {
155
+ background: rgba(34, 197, 94, 0.12);
156
+ display: inline-block; width: 100%; margin: 0 -1.2em; padding: 0 1.2em;
157
+ }
158
+ .tome-content pre .line.tome-line-removed {
159
+ background: rgba(239, 68, 68, 0.12);
160
+ display: inline-block; width: 100%; margin: 0 -1.2em; padding: 0 1.2em;
161
+ }
162
+ html.dark .tome-content pre .line.tome-line-added { background: rgba(34, 197, 94, 0.15); }
163
+ html.dark .tome-content pre .line.tome-line-removed { background: rgba(239, 68, 68, 0.15); }
164
+
165
+ /* Line numbers (CSS counter) */
166
+ .tome-content pre[data-line-numbers] code {
167
+ counter-reset: line;
168
+ }
169
+ .tome-content pre[data-line-numbers] .line::before {
170
+ counter-increment: line;
171
+ content: counter(line);
172
+ display: inline-block; width: 2.5em; margin-inline-end: 1em;
173
+ text-align: end; color: var(--txM); opacity: 0.4;
174
+ font-size: 0.85em; user-select: none;
175
+ border-inline-end: 1px solid var(--bd); padding-inline-end: 0.8em; margin-inline-end: 0.8em;
176
+ }
177
+
178
+ /* Word highlighting */
179
+ .tome-word-highlight {
180
+ background: rgba(139, 148, 158, 0.2); border-radius: 2px;
181
+ padding: 1px 3px; margin: 0 -1px;
182
+ }
183
+ html.dark .tome-word-highlight {
184
+ background: rgba(200, 210, 220, 0.15);
185
+ }
186
+
187
+ /* Copy button */
188
+ .tome-content pre { position: relative; }
189
+ .tome-copy-btn {
190
+ position: absolute; top: 8px; inset-inline-end: 8px;
191
+ font-family: var(--font-code); font-size: 11px;
192
+ color: var(--tx2); background: var(--sf); border: 1px solid var(--bd);
193
+ padding: 3px 8px; border-radius: 2px; cursor: pointer;
194
+ opacity: 0; transition: opacity 0.15s;
195
+ z-index: 2; line-height: 1.4;
196
+ }
197
+ .tome-content pre:hover .tome-copy-btn,
198
+ .tome-copy-btn:focus { opacity: 1; }
199
+ .tome-copy-btn:hover { background: var(--sfH); }
200
+
125
201
  /* Shiki dual-theme support */
126
202
  .shiki { background: var(--cdBg) !important; }
127
203
 
@@ -143,6 +219,57 @@ const contentStyles = `
143
219
  html:not(.dark) .shiki span[style*="color:#22863A"] { color: #1a6e2e !important; }
144
220
  html:not(.dark) .shiki span[style*="color:#D73A49"] { color: #b62324 !important; }
145
221
  html:not(.dark) .shiki span[style*="color:#005CC5"] { color: #0349b4 !important; }
222
+
223
+ /* ── Twoslash type hover tooltips ───────────────────── */
224
+ .twoslash-hover {
225
+ position: relative;
226
+ border-bottom: 1px dotted var(--tx2);
227
+ cursor: help;
228
+ }
229
+ .twoslash-popup-container {
230
+ position: absolute;
231
+ opacity: 0;
232
+ display: none;
233
+ z-index: 10;
234
+ left: 0;
235
+ top: 100%;
236
+ margin-top: 4px;
237
+ padding: 6px 10px;
238
+ background: var(--sf);
239
+ border: 1px solid var(--bd);
240
+ border-radius: 6px;
241
+ font-size: 12px;
242
+ font-family: var(--font-code);
243
+ color: var(--tx);
244
+ white-space: pre-wrap;
245
+ max-width: 500px;
246
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
247
+ pointer-events: none;
248
+ }
249
+ .twoslash-hover:hover .twoslash-popup-container {
250
+ opacity: 1;
251
+ display: block;
252
+ }
253
+ /* Twoslash error/warning underlines */
254
+ .twoslash-error {
255
+ position: relative;
256
+ background: rgba(239, 68, 68, 0.1);
257
+ border-bottom: 2px wavy rgba(239, 68, 68, 0.6);
258
+ }
259
+ /* Twoslash highlighted identifiers */
260
+ .twoslash-highlighted {
261
+ background: rgba(139, 148, 158, 0.15);
262
+ border-radius: 2px;
263
+ padding: 1px 2px;
264
+ }
265
+ /* Twoslash type annotation line (^?) */
266
+ .twoslash-popup-code .shiki { background: transparent !important; padding: 0; margin: 0; }
267
+ .twoslash-popup-code .shiki code { padding: 0; font-size: 12px; }
268
+ html.dark .twoslash-popup-container {
269
+ background: var(--sf);
270
+ border-color: var(--bd);
271
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
272
+ }
146
273
  `;
147
274
 
148
275
  // ── ROUTING HELPERS ──────────────────────────────────────
@@ -156,17 +283,22 @@ function pageIdToPath(id: string): string {
156
283
  return _pageIdToPath(id, basePath, routes);
157
284
  }
158
285
 
286
+ // ── EAGER INITIAL PAGE LOAD ──────────────────────────────
287
+ // Start loading the initial page at module scope — before React mounts —
288
+ // so the data is ready (or nearly ready) by the time App first renders.
289
+ // This eliminates the "Loading..." flash on production.
290
+ const _initialPageId = resolveInitialPageId(
291
+ window.location.pathname,
292
+ window.location.hash,
293
+ routes,
294
+ basePath,
295
+ _pathnameToPageId,
296
+ );
297
+ const _initialPagePromise = loadPage(_initialPageId, routes, loadPageModule);
298
+
159
299
  // ── APP ──────────────────────────────────────────────────
160
300
  function App() {
161
- const [currentPageId, setCurrentPageId] = useState(() =>
162
- resolveInitialPageId(
163
- window.location.pathname,
164
- window.location.hash,
165
- routes,
166
- basePath,
167
- _pathnameToPageId,
168
- )
169
- );
301
+ const [currentPageId, setCurrentPageId] = useState(_initialPageId);
170
302
 
171
303
  const [pageData, setPageData] = useState<LoadedPage | null>(null);
172
304
  const [loading, setLoading] = useState(true);
@@ -197,7 +329,7 @@ function App() {
197
329
  }
198
330
  }, []);
199
331
 
200
- // Initial page load
332
+ // Initial page load — use the eagerly-started promise from module scope
201
333
  useEffect(() => {
202
334
  // If user landed on a legacy hash URL, redirect to clean path
203
335
  const hash = window.location.hash.slice(1);
@@ -206,7 +338,13 @@ function App() {
206
338
  window.history.replaceState(null, "", fullPath);
207
339
  navigateTo(hash, { replace: true });
208
340
  } else {
209
- navigateTo(currentPageId, { replace: true, skipScroll: true });
341
+ // Use the pre-fetched promise instead of starting a new load
342
+ const fullPath = pageIdToPath(currentPageId);
343
+ window.history.replaceState(null, "", fullPath);
344
+ _initialPagePromise.then((data) => {
345
+ setPageData(data);
346
+ setLoading(false);
347
+ });
210
348
  }
211
349
  }, []);
212
350
 
@@ -225,9 +363,16 @@ function App() {
225
363
  // Mermaid diagram rendering: load from CDN and render .tome-mermaid elements.
226
364
  // Also re-renders when theme changes (dark ↔ light) so colors stay correct.
227
365
  const mermaidModuleRef = useRef<any>(null);
228
- const [mermaidTheme, setMermaidTheme] = useState(() =>
229
- typeof document !== "undefined" && document.documentElement.classList.contains("dark") ? "dark" : "light"
230
- );
366
+ const [mermaidTheme, setMermaidTheme] = useState(() => {
367
+ if (typeof document === "undefined") return "light";
368
+ // Check the class first (already set), then fall back to config + system preference
369
+ // to avoid a white flash before Shell syncs the dark class onto <html>
370
+ if (document.documentElement.classList.contains("dark")) return "dark";
371
+ const mode = config.theme?.mode || "auto";
372
+ if (mode === "dark") return "dark";
373
+ if (mode === "light") return "light";
374
+ return window.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light";
375
+ });
231
376
 
232
377
  // Watch for dark class changes on <html> to trigger mermaid re-render
233
378
  useEffect(() => {
@@ -291,6 +436,39 @@ function App() {
291
436
  return () => { cancelled = true; };
292
437
  }, [pageData, loading, mermaidTheme]);
293
438
 
439
+ // Add copy buttons to all pre blocks (expressive code blocks)
440
+ useEffect(() => {
441
+ if (loading) return;
442
+ const preBlocks = document.querySelectorAll(".tome-content pre");
443
+ const buttons: HTMLButtonElement[] = [];
444
+ preBlocks.forEach((pre) => {
445
+ // Skip if already has a copy button
446
+ if (pre.querySelector(".tome-copy-btn")) return;
447
+ const btn = document.createElement("button");
448
+ btn.className = "tome-copy-btn";
449
+ btn.textContent = "Copy";
450
+ btn.addEventListener("click", async () => {
451
+ const code = pre.querySelector("code");
452
+ if (code) {
453
+ try {
454
+ await navigator.clipboard.writeText(code.textContent || "");
455
+ btn.textContent = "Copied!";
456
+ setTimeout(() => { btn.textContent = "Copy"; }, 2000);
457
+ } catch {
458
+ // Fallback for non-HTTPS contexts
459
+ btn.textContent = "Failed";
460
+ setTimeout(() => { btn.textContent = "Copy"; }, 2000);
461
+ }
462
+ }
463
+ });
464
+ pre.appendChild(btn);
465
+ buttons.push(btn);
466
+ });
467
+ return () => {
468
+ buttons.forEach((btn) => btn.remove());
469
+ };
470
+ }, [pageData, loading]);
471
+
294
472
  const allPages = routes.map((r: any) => ({
295
473
  id: r.id,
296
474
  title: r.frontmatter.title,
@@ -302,6 +480,10 @@ function App() {
302
480
  const currentVersion = detectCurrentVersion(currentRoute, versions);
303
481
  const editUrl = computeEditUrl(config.editLink, currentRoute?.filePath);
304
482
 
483
+ // RTL: detect current locale and compute text direction
484
+ const currentLocale = currentRoute?.locale || i18n?.defaultLocale || "en";
485
+ const dir: "ltr" | "rtl" = i18n?.localeDirs?.[currentLocale] || "ltr";
486
+
305
487
  // KaTeX CSS: inject stylesheet when math is enabled or math placeholders exist
306
488
  useEffect(() => {
307
489
  const hasMathPlaceholders = document.querySelectorAll(".tome-math[data-math]").length > 0;
@@ -357,10 +539,10 @@ function App() {
357
539
  config={config}
358
540
  navigation={navigation}
359
541
  currentPageId={currentPageId}
360
- pageHtml={!pageData?.isMdx ? (loading ? "<p>Loading...</p>" : pageData?.html || "<p>Page not found</p>") : undefined}
542
+ pageHtml={!pageData?.isMdx ? (loading ? "" : pageData?.html || "<p>Page not found</p>") : undefined}
361
543
  pageComponent={pageData?.isMdx ? pageData.component : undefined}
362
544
  mdxComponents={MDX_COMPONENTS}
363
- pageTitle={pageData?.frontmatter.title || (loading ? "Loading..." : "Not Found")}
545
+ pageTitle={pageData?.frontmatter.title || (loading ? "" : "Not Found")}
364
546
  pageDescription={pageData?.frontmatter.description}
365
547
  headings={pageData?.headings || []}
366
548
  tocEnabled={pageData?.frontmatter.toc !== false}
@@ -373,6 +555,11 @@ function App() {
373
555
  versioning={versions || undefined}
374
556
  currentVersion={currentVersion}
375
557
  basePath={basePath}
558
+ isDraft={currentRoute?.frontmatter?.draft === true}
559
+ dir={dir}
560
+ i18n={i18n || undefined}
561
+ currentLocale={currentLocale}
562
+ overrides={overrides}
376
563
  />
377
564
  </>
378
565
  );
package/src/global.d.ts CHANGED
@@ -21,3 +21,14 @@ declare module "virtual:tome/doc-context" {
21
21
  const docContext: Array<{ id: string; title: string; content: string }>;
22
22
  export default docContext;
23
23
  }
24
+
25
+ declare module "virtual:tome/overrides" {
26
+ const overrides: {
27
+ Header?: React.ComponentType<any>;
28
+ Footer?: React.ComponentType<any>;
29
+ Sidebar?: React.ComponentType<any>;
30
+ Toc?: React.ComponentType<any>;
31
+ PageFooter?: React.ComponentType<any>;
32
+ };
33
+ export default overrides;
34
+ }