@tomehq/theme 0.3.1 → 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 (57) hide show
  1. package/dist/{chunk-YXKONM3A.js → chunk-2AXAEADQ.js} +471 -138
  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 +173 -4
  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-GR2WCRGK.js +0 -2182
  29. package/dist/chunk-HNLKDQ64.js +0 -2139
  30. package/dist/chunk-INUMUXN5.js +0 -2095
  31. package/dist/chunk-IW3NHNOQ.js +0 -2187
  32. package/dist/chunk-JA4PMX6M.js +0 -1500
  33. package/dist/chunk-JSPFS7G5.js +0 -2102
  34. package/dist/chunk-JZRT4WNC.js +0 -1441
  35. package/dist/chunk-KQBY2JDB.js +0 -2112
  36. package/dist/chunk-LIMYFTPC.js +0 -1468
  37. package/dist/chunk-MEP7P6A7.js +0 -1500
  38. package/dist/chunk-NOZBIES7.js +0 -1948
  39. package/dist/chunk-O4GH3KYX.js +0 -1712
  40. package/dist/chunk-OEXM3BEC.js +0 -1702
  41. package/dist/chunk-Q7PYTVW3.js +0 -1771
  42. package/dist/chunk-QCWZYABW.js +0 -1468
  43. package/dist/chunk-RDF25WB2.js +0 -2085
  44. package/dist/chunk-RKTT3ZEX.js +0 -1500
  45. package/dist/chunk-S47BRMNQ.js +0 -1715
  46. package/dist/chunk-S4ZH5F56.js +0 -1949
  47. package/dist/chunk-SRD7NJHS.js +0 -1949
  48. package/dist/chunk-SWFYJO5H.js +0 -2187
  49. package/dist/chunk-TQDWPSTO.js +0 -2087
  50. package/dist/chunk-TTRXRPP6.js +0 -1941
  51. package/dist/chunk-UKYFJSUA.js +0 -509
  52. package/dist/chunk-VKEQHP2E.js +0 -2133
  53. package/dist/chunk-VUT2FMSI.js +0 -1937
  54. package/dist/chunk-VVCC5JHK.js +0 -1949
  55. package/dist/chunk-W732TVBK.js +0 -1944
  56. package/dist/chunk-X4VQYPKO.js +0 -1768
  57. package/dist/chunk-YZ3P3TNS.js +0 -1760
package/src/Shell.tsx CHANGED
@@ -212,6 +212,27 @@ const GlobeIcon = () => <Icon d="M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18ZM3.6 9h16.
212
212
  // ── TOP NAV EXTERNAL LINK ICON ────────────────────────────
213
213
  const ExtLinkIcon = () => <Icon d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6M15 3h6v6M10 14L21 3" size={11} />;
214
214
 
215
+ // ── SOCIAL LINK ICONS ──────────────────────────────────
216
+ const SOCIAL_ICON_PATHS: Record<string, string> = {
217
+ github: "M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z",
218
+ twitter: "M12.6.75h2.454l-5.36 6.142L16 15.25h-4.937l-3.867-5.07-4.425 5.07H.316l5.733-6.57L0 .75h5.063l3.495 4.633L12.601.75Zm-.86 13.028h1.36L4.323 2.145H2.865l8.875 11.633Z",
219
+ discord: "M13.545 2.907a13.227 13.227 0 00-3.257-1.011.05.05 0 00-.052.025c-.141.25-.297.577-.406.833a12.19 12.19 0 00-3.658 0 8.258 8.258 0 00-.412-.833.051.051 0 00-.052-.025c-1.125.194-2.22.534-3.257 1.011a.041.041 0 00-.021.018C.356 6.024-.213 9.047.066 12.032c.001.014.01.028.021.037a13.276 13.276 0 003.995 2.02.05.05 0 00.056-.019c.308-.42.582-.863.818-1.329a.05.05 0 00-.028-.07 8.735 8.735 0 01-1.248-.595.05.05 0 01-.005-.083c.084-.063.168-.129.248-.195a.05.05 0 01.051-.007c2.619 1.196 5.454 1.196 8.041 0a.052.052 0 01.053.007c.08.066.164.132.248.195a.051.051 0 01-.004.085c-.399.232-.813.431-1.249.594a.05.05 0 00-.03.07c.24.465.515.909.817 1.329a.05.05 0 00.056.019 13.235 13.235 0 004.001-2.02.049.049 0 00.021-.037c.334-3.451-.559-6.449-2.366-9.106a.034.034 0 00-.02-.019z",
220
+ linkedin: "M0 1.146C0 .513.526 0 1.175 0h13.65C15.474 0 16 .513 16 1.146v13.708c0 .633-.526 1.146-1.175 1.146H1.175C.526 16 0 15.487 0 14.854V1.146zm4.943 12.248V6.169H2.542v7.225h2.401zm-1.2-8.212c.837 0 1.358-.554 1.358-1.248-.015-.709-.52-1.248-1.342-1.248-.822 0-1.359.54-1.359 1.248 0 .694.521 1.248 1.327 1.248h.016zm4.908 8.212V9.359c0-.216.016-.432.08-.586.173-.431.568-.878 1.232-.878.869 0 1.216.662 1.216 1.634v3.865h2.401V9.25c0-2.22-1.184-3.252-2.764-3.252-1.274 0-1.845.7-2.165 1.193v.025h-.016a5.54 5.54 0 01.016-.025V6.169h-2.4c.03.678 0 7.225 0 7.225h2.4z",
221
+ youtube: "M8.051 1.999h.089c.822.003 4.987.033 6.11.335a2.01 2.01 0 011.415 1.42c.101.38.172.883.22 1.402l.01.104.022.26.008.104c.065.914.073 1.77.074 1.957v.075c-.001.194-.01 1.108-.082 2.06l-.008.105-.009.104c-.05.572-.124 1.14-.235 1.558a2.007 2.007 0 01-1.415 1.42c-1.16.312-5.569.334-6.18.335h-.142c-.309 0-1.587-.006-2.927-.052l-.17-.006-.087-.004-.171-.007-.171-.007c-1.11-.049-2.167-.128-2.654-.26a2.007 2.007 0 01-1.415-1.419c-.111-.417-.185-.986-.235-1.558L.09 9.82l-.008-.104A31.4 31.4 0 010 7.68v-.123c.002-.215.01-.958.064-1.778l.007-.103.003-.052.008-.104.022-.26.01-.104c.048-.519.119-1.023.22-1.402a2.007 2.007 0 011.415-1.42c.487-.13 1.544-.21 2.654-.26l.17-.007.172-.006.086-.003.171-.007A99.788 99.788 0 017.858 2h.193zM6.4 5.209v4.818l4.157-2.408L6.4 5.209z",
222
+ mastodon: "M11.19 12.195c2.016-.24 3.77-1.475 3.99-2.603.348-1.778.32-4.339.32-4.339 0-3.47-2.286-4.488-2.286-4.488C12.062.238 10.083.017 8.027 0h-.05C5.92.017 3.942.238 2.79.765 2.79.765.504 1.783.504 5.253c-.005.995-.01 2.19.013 3.44.075 4.21.56 8.354 3.383 9.386 1.302.476 2.418.576 3.317.507 1.628-.125 2.541-.8 2.541-.8l-.054-1.182s-1.163.366-2.47.322c-1.293-.044-2.658-.138-2.867-1.716a3.23 3.23 0 01-.028-.465s1.27.31 2.879.384c.984.045 1.905-.058 2.842-.17zM13 8.59V5.319c0-.67-.17-1.2-.507-1.592-.348-.4-.806-.605-1.373-.605-.656 0-1.154.252-1.486.756L9.2 4.595l-.434-.717c-.332-.504-.83-.756-1.486-.756-.567 0-1.025.204-1.373.605-.338.392-.507.923-.507 1.592V8.59h1.69V5.468c0-.67.285-1.012.855-1.012.63 0 .946.404.946 1.204V7.11h1.682V5.66c0-.8.316-1.204.946-1.204.57 0 .855.342.855 1.012V8.59H13z",
223
+ bluesky: "M3.468 1.948C5.303 3.325 7.276 6.118 8 7.616c.724-1.498 2.697-4.29 4.532-5.668C13.855 1.013 16 .638 16 3.14c0 .5-.286 4.2-.454 4.8-.585 2.093-2.716 2.628-4.544 2.305 3.195.564 4.007 2.433 2.25 4.302-3.337 3.548-4.8-1.244-5.252-2.547 0 0-.116-.334-.166-.334h.332C8.166 11.666 8.05 12 8.05 12c-.452 1.303-1.916 6.095-5.252 2.547-1.756-1.869-.946-3.738 2.25-4.302-1.829.323-3.96-.212-4.544-2.305C.336 7.34.05 3.64.05 3.14.05.638 2.195 1.013 3.468 1.948z",
224
+ };
225
+
226
+ const SocialIcon = ({ platform, customIcon }: { platform: string; customIcon?: string }) => {
227
+ const d = platform === "custom" && customIcon ? customIcon : SOCIAL_ICON_PATHS[platform];
228
+ if (!d) return null;
229
+ return (
230
+ <svg width={16} height={16} viewBox="0 0 16 16" fill="currentColor">
231
+ <path d={d} />
232
+ </svg>
233
+ );
234
+ };
235
+
215
236
  // ── SHELL COMPONENT ──────────────────────────────────────
216
237
  export interface VersioningInfo {
217
238
  current: string;
@@ -222,6 +243,7 @@ export interface I18nInfo {
222
243
  defaultLocale: string;
223
244
  locales: string[];
224
245
  localeNames?: Record<string, string>;
246
+ localeDirs?: Record<string, "ltr" | "rtl">;
225
247
  }
226
248
 
227
249
  // ── CHANGELOG VIEW (TOM-49) ─────────────────────────────
@@ -290,6 +312,34 @@ function ChangelogView({ entries }: { entries: ChangelogViewEntry[] }) {
290
312
  );
291
313
  }
292
314
 
315
+ // ── BREADCRUMBS ─────────────────────────────────────────
316
+ type BreadcrumbItem = { label: string; href: string | null };
317
+
318
+ function getBreadcrumbs(
319
+ navigation: Array<{ section: string; pages: Array<{ title: string; id: string; urlPath: string }> }>,
320
+ currentPageId: string,
321
+ pageTitle: string,
322
+ ): BreadcrumbItem[] {
323
+ if (currentPageId === "index") return [];
324
+
325
+ for (const section of navigation) {
326
+ const found = section.pages.find(p => p.id === currentPageId);
327
+ if (found) {
328
+ const crumbs: BreadcrumbItem[] = [];
329
+ // Section label — link to first page in section
330
+ const firstPage = section.pages[0];
331
+ crumbs.push({
332
+ label: section.section,
333
+ href: firstPage ? firstPage.urlPath : null,
334
+ });
335
+ // Current page (last crumb, not a link)
336
+ crumbs.push({ label: pageTitle, href: null });
337
+ return crumbs;
338
+ }
339
+ }
340
+ return [];
341
+ }
342
+
293
343
  interface ShellProps {
294
344
  config: {
295
345
  name: string;
@@ -299,11 +349,12 @@ interface ShellProps {
299
349
  toc?: { enabled?: boolean; depth?: number };
300
350
  topNav?: Array<{ label: string; href: string }>;
301
351
  banner?: { text: string; link?: string; dismissible?: boolean };
352
+ socialLinks?: Array<{ platform: string; url: string; label?: string; icon?: string }>;
302
353
  [key: string]: unknown;
303
354
  };
304
355
  navigation: Array<{
305
356
  section: string;
306
- pages: Array<{ title: string; id: string; urlPath: string; icon?: string }>;
357
+ pages: Array<{ title: string; id: string; urlPath: string; icon?: string; badge?: { text: string; variant: string } }>;
307
358
  }>;
308
359
  currentPageId: string;
309
360
  pageHtml?: string;
@@ -324,13 +375,27 @@ interface ShellProps {
324
375
  currentLocale?: string;
325
376
  docContext?: Array<{ id: string; title: string; content: string }>;
326
377
  basePath?: string;
378
+ isDraft?: boolean;
379
+ dir?: "ltr" | "rtl";
380
+ overrides?: {
381
+ Header?: React.ComponentType<any>;
382
+ Footer?: React.ComponentType<any>;
383
+ Sidebar?: React.ComponentType<any>;
384
+ Toc?: React.ComponentType<any>;
385
+ PageFooter?: React.ComponentType<any>;
386
+ };
327
387
  }
328
388
 
329
389
  export function Shell({
330
390
  config, navigation, currentPageId, pageHtml, pageComponent, mdxComponents,
331
391
  pageTitle, pageDescription, headings, tocEnabled = true, editUrl, lastUpdated, changelogEntries, onNavigate, allPages,
332
- versioning, currentVersion, i18n, currentLocale, docContext, basePath = "",
392
+ versioning, currentVersion, i18n, currentLocale, docContext, basePath = "", isDraft, dir: dirProp, overrides,
333
393
  }: ShellProps) {
394
+ // RTL support: resolve text direction from prop, i18n.localeDirs, or default to "ltr"
395
+ const resolvedLocale = currentLocale || i18n?.defaultLocale || "en";
396
+ const dir: "ltr" | "rtl" = dirProp || i18n?.localeDirs?.[resolvedLocale] || "ltr";
397
+ const isRtl = dir === "rtl";
398
+
334
399
  const themeMode = config.theme?.mode || "auto";
335
400
 
336
401
  // TOM-12: Initialize dark mode from config.theme.mode + system preference
@@ -571,6 +636,9 @@ export function Shell({
571
636
  const prev = idx > 0 ? allNavPages[idx - 1] : null;
572
637
  const next = idx < allNavPages.length - 1 ? allNavPages[idx + 1] : null;
573
638
 
639
+ // Breadcrumbs
640
+ const breadcrumbs = getBreadcrumbs(navigation, currentPageId, pageTitle);
641
+
574
642
  const togSec = (s: string) => setExpanded(p => p.includes(s) ? p.filter(x => x !== s) : [...p, s]);
575
643
 
576
644
  const cssVars: Record<string, string> = {
@@ -590,7 +658,7 @@ export function Shell({
590
658
  const bannerIsInternal = bannerLink ? (bannerLink.startsWith("#") || (basePath && bannerLink.startsWith(basePath + "/"))) : false;
591
659
 
592
660
  return (
593
- <div className="tome-grain" style={{ ...cssVars as React.CSSProperties, color: "var(--tx)", background: "var(--bg)", fontFamily: "var(--font-body)", minHeight: "100vh", overflow: "hidden" }}>
661
+ <div dir={dir} className="tome-grain" style={{ ...cssVars as React.CSSProperties, color: "var(--tx)", background: "var(--bg)", fontFamily: "var(--font-body)", minHeight: "100vh", overflow: "hidden" }}>
594
662
  {/* Banner */}
595
663
  {config.banner?.text && !bannerDismissed && (
596
664
  <div style={{
@@ -653,7 +721,7 @@ export function Shell({
653
721
  />
654
722
  ) : null}
655
723
 
656
- <div style={{ display: "flex", flex: 1, height: config.banner?.text && !bannerDismissed ? "calc(100vh - 32px)" : "100vh" }}>
724
+ <div style={{ display: "flex", flexDirection: isRtl ? "row-reverse" : "row", flex: 1, height: config.banner?.text && !bannerDismissed ? "calc(100vh - 32px)" : "100vh" }}>
657
725
  {/* Mobile sidebar backdrop */}
658
726
  {mobile && sbOpen && (
659
727
  <div onClick={() => setSb(false)} style={{
@@ -662,12 +730,25 @@ export function Shell({
662
730
  }} />
663
731
  )}
664
732
  {/* Sidebar */}
733
+ {overrides?.Sidebar ? (
734
+ <overrides.Sidebar
735
+ config={config}
736
+ navigation={navigation}
737
+ currentPageId={currentPageId}
738
+ onNavigate={onNavigate}
739
+ mobile={mobile}
740
+ sbOpen={sbOpen}
741
+ setSbOpen={setSb}
742
+ versioning={versioning}
743
+ currentVersion={currentVersion}
744
+ />
745
+ ) : (
665
746
  <aside style={{
666
747
  width: sbOpen ? 270 : 0, minWidth: sbOpen ? 270 : 0,
667
- background: "var(--sbBg)", borderRight: "1px solid var(--bd)",
748
+ background: "var(--sbBg)", [isRtl ? "borderLeft" : "borderRight"]: "1px solid var(--bd)",
668
749
  display: "flex", flexDirection: "column",
669
750
  transition: "width .2s, min-width .2s", overflow: "hidden",
670
- ...(mobile ? { position: "fixed" as const, top: 0, left: 0, bottom: 0, zIndex: 201 } : {}),
751
+ ...(mobile ? { position: "fixed" as const, top: 0, [isRtl ? "right" : "left"]: 0, bottom: 0, zIndex: 201 } : {}),
671
752
  }}>
672
753
  <a href="/" style={{ padding: "18px 20px", display: "flex", alignItems: "baseline", gap: 6, borderBottom: "1px solid var(--bd)", textDecoration: "none", color: "inherit" }}>
673
754
  <span style={{ fontFamily: "var(--font-heading)", fontSize: 22, fontWeight: 700, fontStyle: "italic" }}>
@@ -683,7 +764,7 @@ export function Shell({
683
764
  padding: "8px 12px", cursor: "pointer", color: "var(--txM)", fontSize: 12.5,
684
765
  fontFamily: "var(--font-body)",
685
766
  }}>
686
- <SearchIcon /><span style={{ flex: 1, textAlign: "left" }}>Search...</span>
767
+ <SearchIcon /><span style={{ flex: 1, textAlign: isRtl ? "right" : "left" }}>Search...</span>
687
768
  <kbd style={{ fontFamily: "var(--font-code)", fontSize: 9, background: "var(--sf)", border: "1px solid var(--bd)", borderRadius: 2, padding: "2px 6px" }}>{"\u2318K"}</kbd>
688
769
  </button>
689
770
  </div>
@@ -699,21 +780,38 @@ export function Shell({
699
780
  }}>
700
781
  {expanded.includes(sec.section) ? <ChevDown /> : <ChevRight />}{sec.section}
701
782
  </button>
702
- {expanded.includes(sec.section) && <div style={{ marginLeft: 8, borderLeft: "1px solid var(--bd)", paddingLeft: 0 }}>
783
+ {expanded.includes(sec.section) && <div style={{ [isRtl ? "marginRight" : "marginLeft"]: 8, [isRtl ? "borderRight" : "borderLeft"]: "1px solid var(--bd)", [isRtl ? "paddingRight" : "paddingLeft"]: 0 }}>
703
784
  {sec.pages.map(p => {
704
785
  const active = currentPageId === p.id;
705
786
  return (
706
787
  <button key={p.id} onClick={() => { onNavigate(p.id); if (mobile) setSb(false); }} style={{
707
788
  display: "flex", alignItems: "center", gap: 10, width: "100%",
708
- textAlign: "left", background: "none",
789
+ textAlign: isRtl ? "right" : "left", background: "none",
709
790
  border: "none", borderRadius: 0,
710
- borderLeft: active ? "2px solid var(--ac)" : "2px solid transparent",
791
+ [isRtl ? "borderRight" : "borderLeft"]: active ? "2px solid var(--ac)" : "2px solid transparent",
711
792
  padding: "7px 14px", cursor: "pointer",
712
793
  color: active ? "var(--ac)" : "var(--tx2)", fontSize: 13,
713
794
  fontWeight: active ? 500 : 400, fontFamily: "var(--font-body)",
714
795
  transition: "all .12s",
715
796
  }}>
716
797
  {p.title}
798
+ {p.badge && (() => {
799
+ const badgeColors: Record<string, { bg: string; text: string }> = {
800
+ default: { bg: "var(--sf)", text: "var(--tx2)" },
801
+ info: { bg: "rgba(59,130,246,0.15)", text: "rgb(59,130,246)" },
802
+ success: { bg: "rgba(34,197,94,0.15)", text: "rgb(34,197,94)" },
803
+ warning: { bg: "rgba(234,179,8,0.15)", text: "rgb(202,138,4)" },
804
+ danger: { bg: "rgba(239,68,68,0.15)", text: "rgb(239,68,68)" },
805
+ };
806
+ const bc = badgeColors[p.badge!.variant || "default"] || badgeColors.default;
807
+ return (
808
+ <span style={{
809
+ fontSize: 10, fontWeight: 600, padding: "2px 6px",
810
+ borderRadius: 4, marginLeft: 6, whiteSpace: "nowrap",
811
+ background: bc.bg, color: bc.text,
812
+ }}>{p.badge!.text}</span>
813
+ );
814
+ })()}
717
815
  </button>
718
816
  );
719
817
  })}
@@ -734,11 +832,11 @@ export function Shell({
734
832
  onNavigate(targetId);
735
833
  }}
736
834
  style={{
737
- flex: 1, padding: "6px 0", textAlign: "center",
835
+ flex: 1, padding: "3px 0", textAlign: "center",
738
836
  background: v === (currentVersion || versioning.current) ? "var(--acD)" : "var(--sf)",
739
837
  border: "1px solid var(--bd)", borderRadius: 2, cursor: "pointer",
740
838
  color: v === (currentVersion || versioning.current) ? "var(--ac)" : "var(--tx2)",
741
- fontSize: 12, fontFamily: "var(--font-code)",
839
+ fontSize: 11, fontFamily: "var(--font-code)",
742
840
  fontWeight: v === versioning.current ? 600 : 400,
743
841
  }}
744
842
  >
@@ -758,10 +856,30 @@ export function Shell({
758
856
  <span style={{ fontFamily: "var(--font-code)", fontSize: 10, color: "var(--txM)" }}>{typeof __TOME_VERSION__ !== "undefined" && __TOME_VERSION__ ? `v${__TOME_VERSION__}` : "v0.1.0"}</span>
759
857
  </div>
760
858
  </aside>
859
+ )}
761
860
 
762
861
  {/* Main area */}
763
862
  <div style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }}>
764
863
  {/* Header */}
864
+ {overrides?.Header ? (
865
+ <overrides.Header
866
+ config={config}
867
+ navigation={navigation}
868
+ currentPageId={currentPageId}
869
+ onNavigate={onNavigate}
870
+ mobile={mobile}
871
+ sbOpen={sbOpen}
872
+ setSbOpen={setSb}
873
+ isDark={isDark}
874
+ setDark={setDark}
875
+ versioning={versioning}
876
+ currentVersion={currentVersion}
877
+ i18n={i18n}
878
+ currentLocale={currentLocale}
879
+ onSearchOpen={() => setSearch(true)}
880
+ basePath={basePath}
881
+ />
882
+ ) : (
765
883
  <header style={{
766
884
  display: "flex", alignItems: "center", gap: mobile ? 8 : 12, padding: mobile ? "8px 12px" : "10px 24px",
767
885
  borderBottom: "1px solid var(--bd)", background: "var(--hdBg)", backdropFilter: "blur(12px)",
@@ -820,6 +938,30 @@ export function Shell({
820
938
  </div>
821
939
  )}
822
940
 
941
+ {/* Social Links */}
942
+ {config.socialLinks && config.socialLinks.length > 0 && !mobile && (
943
+ <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
944
+ {config.socialLinks.map((link) => (
945
+ <a
946
+ key={link.url}
947
+ href={link.url}
948
+ target="_blank"
949
+ rel="noopener noreferrer"
950
+ aria-label={link.label || link.platform}
951
+ data-testid={`social-link-${link.platform}`}
952
+ style={{
953
+ display: "flex", alignItems: "center", justifyContent: "center",
954
+ color: "var(--tx2)", cursor: "pointer", transition: "color .15s",
955
+ }}
956
+ onMouseOver={(e) => (e.currentTarget.style.color = "var(--tx)")}
957
+ onMouseOut={(e) => (e.currentTarget.style.color = "var(--tx2)")}
958
+ >
959
+ <SocialIcon platform={link.platform} customIcon={link.icon} />
960
+ </a>
961
+ ))}
962
+ </div>
963
+ )}
964
+
823
965
  {/* Theme toggle in header on mobile */}
824
966
  {mobile && themeMode === "auto" && (
825
967
  <button aria-label={isDark ? "Switch to light mode" : "Switch to dark mode"} onClick={() => setDark(d => !d)} style={{ background: "none", border: "none", color: "var(--txM)", cursor: "pointer", display: "flex", flexShrink: 0 }}>
@@ -947,6 +1089,7 @@ export function Shell({
947
1089
  </div>
948
1090
  )}
949
1091
  </header>
1092
+ )}
950
1093
 
951
1094
  {/* TOM-30: Old version banner */}
952
1095
  {isOldVersion && (
@@ -975,9 +1118,42 @@ export function Shell({
975
1118
  {/* Content + TOC */}
976
1119
  <div ref={contentRef} style={{ flex: 1, overflow: "auto", display: "flex" }}>
977
1120
  <main style={{ flex: 1, maxWidth: mobile ? "100%" : 760, padding: mobile ? "24px 16px 60px" : "40px 48px 80px", margin: "0 auto", minWidth: 0 }}>
1121
+ {breadcrumbs.length > 0 && (
1122
+ <nav aria-label="Breadcrumbs" data-testid="breadcrumbs" style={{
1123
+ display: "flex", alignItems: "center", gap: 6,
1124
+ fontSize: 13, color: "var(--tx2)", marginBottom: 8,
1125
+ }}>
1126
+ {breadcrumbs.map((crumb, i) => (
1127
+ <React.Fragment key={i}>
1128
+ {i > 0 && <span style={{ color: "var(--tx2)", opacity: 0.5 }}>{"\u203A"}</span>}
1129
+ {i < breadcrumbs.length - 1 && crumb.href !== null ? (
1130
+ <a
1131
+ href={crumb.href}
1132
+ onClick={(e: React.MouseEvent) => {
1133
+ e.preventDefault();
1134
+ // Find the page id for this href
1135
+ const page = navigation.flatMap(s => s.pages).find(p => p.urlPath === crumb.href);
1136
+ if (page) onNavigate(page.id);
1137
+ }}
1138
+ style={{ color: "var(--tx2)", textDecoration: "none", cursor: "pointer" }}
1139
+ >
1140
+ {crumb.label}
1141
+ </a>
1142
+ ) : (
1143
+ <span style={i === breadcrumbs.length - 1 ? { color: "var(--tx)" } : undefined}>{crumb.label}</span>
1144
+ )}
1145
+ </React.Fragment>
1146
+ ))}
1147
+ </nav>
1148
+ )}
978
1149
  <h1 style={{ fontFamily: "var(--font-heading)", fontSize: mobile ? 26 : 38, fontWeight: 400, fontStyle: "italic", lineHeight: 1.15, marginBottom: 8 }}>
979
1150
  {pageTitle}
980
1151
  </h1>
1152
+ {isDraft && (
1153
+ <div data-testid="draft-banner" style={{ background: "#fef3c7", color: "#92400e", padding: "8px 16px", borderRadius: 6, fontSize: 13, marginBottom: 16 }}>
1154
+ Draft — This page is only visible in development
1155
+ </div>
1156
+ )}
981
1157
  {pageDescription && <p style={{ fontSize: 16, color: "var(--tx2)", lineHeight: 1.6, marginBottom: 32 }}>{pageDescription}</p>}
982
1158
  <div style={{ borderTop: "1px solid var(--bd)", paddingTop: 28 }}>
983
1159
  {/* TOM-49: Changelog page type */}
@@ -995,7 +1171,19 @@ export function Shell({
995
1171
  )}
996
1172
  </div>
997
1173
 
998
- {/* TOM-48: Edit this page link + TOM-54: Last updated */}
1174
+ {/* TOM-48: Edit this page link + TOM-54: Last updated + Feedback + Prev/Next */}
1175
+ {overrides?.PageFooter ? (
1176
+ <overrides.PageFooter
1177
+ editUrl={editUrl}
1178
+ lastUpdated={lastUpdated}
1179
+ currentPageId={currentPageId}
1180
+ prev={prev}
1181
+ next={next}
1182
+ onNavigate={onNavigate}
1183
+ mobile={mobile}
1184
+ />
1185
+ ) : (
1186
+ <>
999
1187
  {(editUrl || lastUpdated) && (
1000
1188
  <div style={{ marginTop: 40, display: "flex", flexDirection: mobile ? "column" : "row", alignItems: mobile ? "flex-start" : "center", justifyContent: "space-between", gap: mobile ? 8 : 16 }}>
1001
1189
  {editUrl && (
@@ -1049,7 +1237,7 @@ export function Shell({
1049
1237
  border: "1px solid var(--bd)", borderRadius: 2, padding: "10px 16px",
1050
1238
  cursor: "pointer", color: "var(--tx2)", fontSize: 13, fontFamily: "var(--font-body)",
1051
1239
  transition: "border-color .15s, color .15s",
1052
- }}><ArrowLeft /> {prev.title}</button>
1240
+ }}>{isRtl ? <ArrowRight /> : <ArrowLeft />} {prev.title}</button>
1053
1241
  ) : <div />}
1054
1242
  {next ? (
1055
1243
  <button onClick={() => onNavigate(next.id)} style={{
@@ -1057,16 +1245,27 @@ export function Shell({
1057
1245
  border: "1px solid var(--bd)", borderRadius: 2, padding: "10px 16px",
1058
1246
  cursor: "pointer", color: "var(--tx2)", fontSize: 13, fontFamily: "var(--font-body)",
1059
1247
  transition: "border-color .15s, color .15s",
1060
- }}>{next.title} <ArrowRight /></button>
1248
+ }}>{next.title} {isRtl ? <ArrowLeft /> : <ArrowRight />}</button>
1061
1249
  ) : <div />}
1062
1250
  </div>
1251
+ </>
1252
+ )}
1063
1253
  </main>
1064
1254
 
1065
1255
  {/* TOC (TOM-52) */}
1066
- {showToc && filteredHeadings.length >= 2 && wide && (
1067
- <aside data-testid="toc-sidebar" style={{ width: 200, padding: "40px 16px 40px 0", position: "sticky", top: 0, alignSelf: "flex-start", flexShrink: 0 }}>
1256
+ {overrides?.Toc ? (
1257
+ showToc && filteredHeadings.length >= 2 && wide && (
1258
+ <overrides.Toc
1259
+ headings={filteredHeadings}
1260
+ activeHeadingId={activeHeadingId}
1261
+ onScrollToHeading={scrollToHeading}
1262
+ />
1263
+ )
1264
+ ) : (
1265
+ showToc && filteredHeadings.length >= 2 && wide && (
1266
+ <aside data-testid="toc-sidebar" style={{ width: 200, padding: isRtl ? "40px 0 40px 16px" : "40px 16px 40px 0", position: "sticky", top: 0, alignSelf: "flex-start", flexShrink: 0 }}>
1068
1267
  <div style={{ fontSize: 10, fontWeight: 600, textTransform: "uppercase", letterSpacing: ".1em", color: "var(--txM)", marginBottom: 12, fontFamily: "var(--font-code)" }}>On this page</div>
1069
- <nav aria-label="Table of contents" style={{ borderLeft: "1px solid var(--bd)", paddingLeft: 0 }}>
1268
+ <nav aria-label="Table of contents" style={{ [isRtl ? "borderRight" : "borderLeft"]: "1px solid var(--bd)", [isRtl ? "paddingRight" : "paddingLeft"]: 0 }}>
1070
1269
  {filteredHeadings.map((h, i) => {
1071
1270
  const isActive = activeHeadingId === h.id;
1072
1271
  return (
@@ -1081,22 +1280,33 @@ export function Shell({
1081
1280
  fontWeight: isActive ? 500 : 400,
1082
1281
  textDecoration: "none",
1083
1282
  padding: "4px 12px",
1084
- paddingLeft: 12 + (h.depth - 2) * 12,
1283
+ [isRtl ? "paddingRight" : "paddingLeft"]: 12 + (h.depth - 2) * 12,
1085
1284
  lineHeight: 1.4,
1086
1285
  transition: "color .15s, font-weight .15s",
1087
- borderLeft: isActive ? "2px solid var(--ac)" : "2px solid transparent",
1088
- marginLeft: -1,
1286
+ [isRtl ? "borderRight" : "borderLeft"]: isActive ? "2px solid var(--ac)" : "2px solid transparent",
1287
+ [isRtl ? "marginRight" : "marginLeft"]: -1,
1089
1288
  }}
1090
1289
  >{h.text}</a>
1091
1290
  );
1092
1291
  })}
1093
1292
  </nav>
1094
1293
  </aside>
1294
+ )
1095
1295
  )}
1096
1296
  </div>
1097
1297
  </div>
1098
1298
  </div>
1099
1299
 
1300
+ {/* Footer override */}
1301
+ {overrides?.Footer && (
1302
+ <overrides.Footer
1303
+ config={config}
1304
+ navigation={navigation}
1305
+ currentPageId={currentPageId}
1306
+ onNavigate={onNavigate}
1307
+ />
1308
+ )}
1309
+
1100
1310
  {/* TOM-32: AI Chat Widget (BYOK) */}
1101
1311
  {config.ai?.enabled && (
1102
1312
  <AiChat