@tomehq/theme 0.2.7 → 0.2.8
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/CHANGELOG.md +15 -0
- package/dist/chunk-3A2LPGUL.js +1991 -0
- package/dist/chunk-3I2QTWTW.js +1948 -0
- package/dist/chunk-45M5UIAB.js +2110 -0
- package/dist/chunk-462AGU3S.js +1959 -0
- package/dist/chunk-DPKZBFQP.js +1777 -0
- package/dist/chunk-GDQIBNX5.js +1962 -0
- package/dist/chunk-INUMUXN5.js +2095 -0
- package/dist/chunk-NOZBIES7.js +1948 -0
- package/dist/chunk-Q7PYTVW3.js +1771 -0
- package/dist/chunk-RDF25WB2.js +2085 -0
- package/dist/chunk-S4ZH5F56.js +1949 -0
- package/dist/chunk-SRD7NJHS.js +1949 -0
- package/dist/chunk-TQDWPSTO.js +2087 -0
- package/dist/chunk-TTRXRPP6.js +1941 -0
- package/dist/chunk-VUT2FMSI.js +1937 -0
- package/dist/chunk-VVCC5JHK.js +1949 -0
- package/dist/chunk-W732TVBK.js +1944 -0
- package/dist/entry.js +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +1 -1
- package/package.json +3 -3
- package/src/Shell.test.tsx +72 -0
- package/src/Shell.tsx +204 -16
- package/src/entry-helpers.test.ts +316 -0
- package/src/entry-helpers.ts +103 -0
- package/src/entry.tsx +152 -69
- package/src/routing.test.ts +124 -0
- package/src/routing.ts +45 -0
package/dist/entry.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -44,6 +44,11 @@ interface ShellProps {
|
|
|
44
44
|
label: string;
|
|
45
45
|
href: string;
|
|
46
46
|
}>;
|
|
47
|
+
banner?: {
|
|
48
|
+
text: string;
|
|
49
|
+
link?: string;
|
|
50
|
+
dismissible?: boolean;
|
|
51
|
+
};
|
|
47
52
|
[key: string]: unknown;
|
|
48
53
|
};
|
|
49
54
|
navigation: Array<{
|
|
@@ -95,8 +100,9 @@ interface ShellProps {
|
|
|
95
100
|
title: string;
|
|
96
101
|
content: string;
|
|
97
102
|
}>;
|
|
103
|
+
basePath?: string;
|
|
98
104
|
}
|
|
99
|
-
declare function Shell({ config, navigation, currentPageId, pageHtml, pageComponent, mdxComponents, pageTitle, pageDescription, headings, tocEnabled, editUrl, lastUpdated, changelogEntries, onNavigate, allPages, versioning, currentVersion, i18n, currentLocale, docContext, }: ShellProps): react_jsx_runtime.JSX.Element;
|
|
105
|
+
declare function Shell({ config, navigation, currentPageId, pageHtml, pageComponent, mdxComponents, pageTitle, pageDescription, headings, tocEnabled, editUrl, lastUpdated, changelogEntries, onNavigate, allPages, versioning, currentVersion, i18n, currentLocale, docContext, basePath, }: ShellProps): react_jsx_runtime.JSX.Element;
|
|
100
106
|
|
|
101
107
|
interface AiChatProps {
|
|
102
108
|
provider: "openai" | "anthropic" | "custom";
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tomehq/theme",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
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.2.
|
|
13
|
-
"@tomehq/core": "0.2.
|
|
12
|
+
"@tomehq/components": "0.2.8",
|
|
13
|
+
"@tomehq/core": "0.2.8"
|
|
14
14
|
},
|
|
15
15
|
"peerDependencies": {
|
|
16
16
|
"react": "^18.0.0 || ^19.0.0",
|
package/src/Shell.test.tsx
CHANGED
|
@@ -797,3 +797,75 @@ describe("Shell AI chat integration", () => {
|
|
|
797
797
|
expect(screen.getByTestId("ai-chat-button")).toBeInTheDocument();
|
|
798
798
|
});
|
|
799
799
|
});
|
|
800
|
+
|
|
801
|
+
// ── Banner (Shell banner) ────────────────────────────────
|
|
802
|
+
|
|
803
|
+
describe("Shell banner", () => {
|
|
804
|
+
beforeEach(() => {
|
|
805
|
+
localStorage.clear();
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
it("renders banner text when banner config is provided", () => {
|
|
809
|
+
renderShell({ config: { ...baseConfig, banner: { text: "New version available!" } } });
|
|
810
|
+
expect(screen.getByText("New version available!")).toBeInTheDocument();
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
it("renders banner as a link when link is provided", () => {
|
|
814
|
+
renderShell({
|
|
815
|
+
config: { ...baseConfig, banner: { text: "Click here", link: "https://example.com" } },
|
|
816
|
+
});
|
|
817
|
+
const link = screen.getByText("Click here");
|
|
818
|
+
expect(link.tagName).toBe("A");
|
|
819
|
+
expect(link).toHaveAttribute("href", "https://example.com");
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
it("does not render banner when banner config is not provided", () => {
|
|
823
|
+
renderShell();
|
|
824
|
+
expect(screen.queryByText("New version available!")).not.toBeInTheDocument();
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
it("dismisses banner when X button is clicked", () => {
|
|
828
|
+
renderShell({ config: { ...baseConfig, banner: { text: "Dismiss me" } } });
|
|
829
|
+
expect(screen.getByText("Dismiss me")).toBeInTheDocument();
|
|
830
|
+
const dismissBtn = screen.getByLabelText("Dismiss banner");
|
|
831
|
+
fireEvent.click(dismissBtn);
|
|
832
|
+
expect(screen.queryByText("Dismiss me")).not.toBeInTheDocument();
|
|
833
|
+
});
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
// ── Feedback widget ──────────────────────────────────────
|
|
837
|
+
|
|
838
|
+
describe("Shell feedback widget", () => {
|
|
839
|
+
it("renders 'Was this helpful?' text", () => {
|
|
840
|
+
renderShell();
|
|
841
|
+
expect(screen.getByText("Was this helpful?")).toBeInTheDocument();
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
it("renders thumbs up and thumbs down buttons", () => {
|
|
845
|
+
renderShell();
|
|
846
|
+
expect(screen.getByText("\uD83D\uDC4D")).toBeInTheDocument();
|
|
847
|
+
expect(screen.getByText("\uD83D\uDC4E")).toBeInTheDocument();
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
it("shows 'Thanks for your feedback!' after clicking thumbs up", () => {
|
|
851
|
+
renderShell();
|
|
852
|
+
fireEvent.click(screen.getByText("\uD83D\uDC4D"));
|
|
853
|
+
expect(screen.getByText("Thanks for your feedback!")).toBeInTheDocument();
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
it("shows 'Thanks for your feedback!' after clicking thumbs down", () => {
|
|
857
|
+
renderShell();
|
|
858
|
+
fireEvent.click(screen.getByText("\uD83D\uDC4E"));
|
|
859
|
+
expect(screen.getByText("Thanks for your feedback!")).toBeInTheDocument();
|
|
860
|
+
});
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
// ── Image zoom ───────────────────────────────────────────
|
|
864
|
+
|
|
865
|
+
describe("Shell image zoom", () => {
|
|
866
|
+
it("does not show zoom overlay initially", () => {
|
|
867
|
+
const { container } = renderShell();
|
|
868
|
+
const zoomOverlay = container.querySelector('[style*="cursor: zoom-out"]');
|
|
869
|
+
expect(zoomOverlay).toBeNull();
|
|
870
|
+
});
|
|
871
|
+
});
|
package/src/Shell.tsx
CHANGED
|
@@ -96,12 +96,14 @@ function AlgoliaSearchModal({
|
|
|
96
96
|
indexName,
|
|
97
97
|
onNavigate,
|
|
98
98
|
onClose,
|
|
99
|
+
basePath = "",
|
|
99
100
|
}: {
|
|
100
101
|
appId: string;
|
|
101
102
|
apiKey: string;
|
|
102
103
|
indexName: string;
|
|
103
104
|
onNavigate: (id: string) => void;
|
|
104
105
|
onClose: () => void;
|
|
106
|
+
basePath?: string;
|
|
105
107
|
}) {
|
|
106
108
|
const [DocSearchComponent, setDocSearchComponent] = useState<React.ComponentType<any> | null>(null);
|
|
107
109
|
const [loadFailed, setLoadFailed] = useState(false);
|
|
@@ -122,7 +124,12 @@ function AlgoliaSearchModal({
|
|
|
122
124
|
const extractPageId = useCallback((url: string): string => {
|
|
123
125
|
try {
|
|
124
126
|
const parsed = new URL(url, "http://localhost");
|
|
125
|
-
|
|
127
|
+
let pathname = parsed.pathname;
|
|
128
|
+
// Strip basePath prefix
|
|
129
|
+
if (basePath) {
|
|
130
|
+
const bp = basePath.replace(/\/$/, "");
|
|
131
|
+
if (pathname.startsWith(bp)) pathname = pathname.slice(bp.length);
|
|
132
|
+
}
|
|
126
133
|
return pathname
|
|
127
134
|
.replace(/^\//, "")
|
|
128
135
|
.replace(/\/index\.html$/, "")
|
|
@@ -131,7 +138,7 @@ function AlgoliaSearchModal({
|
|
|
131
138
|
} catch {
|
|
132
139
|
return "index";
|
|
133
140
|
}
|
|
134
|
-
}, []);
|
|
141
|
+
}, [basePath]);
|
|
135
142
|
|
|
136
143
|
if (loadFailed) {
|
|
137
144
|
return (
|
|
@@ -291,6 +298,7 @@ interface ShellProps {
|
|
|
291
298
|
ai?: { enabled?: boolean; provider?: "openai" | "anthropic" | "custom"; model?: string; apiKeyEnv?: string };
|
|
292
299
|
toc?: { enabled?: boolean; depth?: number };
|
|
293
300
|
topNav?: Array<{ label: string; href: string }>;
|
|
301
|
+
banner?: { text: string; link?: string; dismissible?: boolean };
|
|
294
302
|
[key: string]: unknown;
|
|
295
303
|
};
|
|
296
304
|
navigation: Array<{
|
|
@@ -315,12 +323,13 @@ interface ShellProps {
|
|
|
315
323
|
i18n?: I18nInfo;
|
|
316
324
|
currentLocale?: string;
|
|
317
325
|
docContext?: Array<{ id: string; title: string; content: string }>;
|
|
326
|
+
basePath?: string;
|
|
318
327
|
}
|
|
319
328
|
|
|
320
329
|
export function Shell({
|
|
321
330
|
config, navigation, currentPageId, pageHtml, pageComponent, mdxComponents,
|
|
322
331
|
pageTitle, pageDescription, headings, tocEnabled = true, editUrl, lastUpdated, changelogEntries, onNavigate, allPages,
|
|
323
|
-
versioning, currentVersion, i18n, currentLocale, docContext,
|
|
332
|
+
versioning, currentVersion, i18n, currentLocale, docContext, basePath = "",
|
|
324
333
|
}: ShellProps) {
|
|
325
334
|
const themeMode = config.theme?.mode || "auto";
|
|
326
335
|
|
|
@@ -336,6 +345,15 @@ export function Shell({
|
|
|
336
345
|
const [searchOpen, setSearch] = useState(false);
|
|
337
346
|
const [versionDropdownOpen, setVersionDropdown] = useState(false);
|
|
338
347
|
const [localeDropdownOpen, setLocaleDropdown] = useState(false);
|
|
348
|
+
const [zoomSrc, setZoomSrc] = useState<string | null>(null);
|
|
349
|
+
const [feedbackGiven, setFeedbackGiven] = useState<Record<string, boolean>>({});
|
|
350
|
+
const [bannerDismissed, setBannerDismissed] = useState(() => {
|
|
351
|
+
if (!config.banner?.text) return true;
|
|
352
|
+
try {
|
|
353
|
+
const hash = Array.from(config.banner.text).reduce((h, c) => ((h << 5) - h + c.charCodeAt(0)) | 0, 0).toString(36);
|
|
354
|
+
return localStorage.getItem("tome-banner-dismissed") === hash;
|
|
355
|
+
} catch { return false; }
|
|
356
|
+
});
|
|
339
357
|
|
|
340
358
|
// TOM-30: Determine if viewing an old version
|
|
341
359
|
const isOldVersion = versioning && currentVersion && currentVersion !== versioning.current;
|
|
@@ -398,6 +416,62 @@ export function Shell({
|
|
|
398
416
|
|
|
399
417
|
useEffect(() => { contentRef.current?.scrollTo(0, 0); }, [currentPageId]);
|
|
400
418
|
|
|
419
|
+
// ── Image zoom: delegate click on .tome-content img ──
|
|
420
|
+
useEffect(() => {
|
|
421
|
+
const el = contentRef.current;
|
|
422
|
+
if (!el) return;
|
|
423
|
+
const handler = (e: MouseEvent) => {
|
|
424
|
+
const target = e.target as HTMLElement;
|
|
425
|
+
if (target.tagName === "IMG" && target.closest(".tome-content")) {
|
|
426
|
+
setZoomSrc((target as HTMLImageElement).src);
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
el.addEventListener("click", handler);
|
|
430
|
+
return () => el.removeEventListener("click", handler);
|
|
431
|
+
}, []);
|
|
432
|
+
|
|
433
|
+
// ── Content link interception: delegate click on .tome-content a ──
|
|
434
|
+
// Intercepts internal links in rendered markdown so they navigate via pushState
|
|
435
|
+
// instead of triggering a full page reload.
|
|
436
|
+
useEffect(() => {
|
|
437
|
+
const el = contentRef.current;
|
|
438
|
+
if (!el) return;
|
|
439
|
+
const handler = (e: MouseEvent) => {
|
|
440
|
+
const anchor = (e.target as HTMLElement).closest("a");
|
|
441
|
+
if (!anchor) return;
|
|
442
|
+
const href = anchor.getAttribute("href");
|
|
443
|
+
if (!href) return;
|
|
444
|
+
// Skip external links, mailto, tel, pure anchors
|
|
445
|
+
if (href.startsWith("http://") || href.startsWith("https://") || href.startsWith("mailto:") || href.startsWith("tel:") || href.startsWith("//")) return;
|
|
446
|
+
// Pure heading anchor — let default scrolling work
|
|
447
|
+
if (href.startsWith("#")) return;
|
|
448
|
+
// Internal link — resolve page ID by stripping basePath
|
|
449
|
+
e.preventDefault();
|
|
450
|
+
let pageId = href.replace(/^\.\//, "").replace(/^\//, "").replace(/\.mdx?$/, "").replace(/\/$/, "");
|
|
451
|
+
// Strip basePath prefix (e.g. "docs/" from "/docs/quickstart")
|
|
452
|
+
if (basePath) {
|
|
453
|
+
const normalized = basePath.replace(/^\//, "").replace(/\/$/, "");
|
|
454
|
+
if (normalized && pageId.startsWith(normalized + "/")) {
|
|
455
|
+
pageId = pageId.slice(normalized.length + 1);
|
|
456
|
+
} else if (normalized && pageId === normalized) {
|
|
457
|
+
pageId = "index";
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
if (!pageId) pageId = "index";
|
|
461
|
+
onNavigate(pageId);
|
|
462
|
+
};
|
|
463
|
+
el.addEventListener("click", handler);
|
|
464
|
+
return () => el.removeEventListener("click", handler);
|
|
465
|
+
}, [onNavigate, basePath]);
|
|
466
|
+
|
|
467
|
+
// ── Image zoom: Escape to dismiss ──
|
|
468
|
+
useEffect(() => {
|
|
469
|
+
if (!zoomSrc) return;
|
|
470
|
+
const handler = (e: KeyboardEvent) => { if (e.key === "Escape") setZoomSrc(null); };
|
|
471
|
+
window.addEventListener("keydown", handler);
|
|
472
|
+
return () => window.removeEventListener("keydown", handler);
|
|
473
|
+
}, [zoomSrc]);
|
|
474
|
+
|
|
401
475
|
// ── TOC: Config-based depth filtering + frontmatter opt-out ──
|
|
402
476
|
const tocConfig = config.toc;
|
|
403
477
|
const tocDepth = tocConfig?.depth ?? 3;
|
|
@@ -498,8 +572,55 @@ export function Shell({
|
|
|
498
572
|
|
|
499
573
|
const PageComponent = pageComponent;
|
|
500
574
|
|
|
575
|
+
// Compute banner link properties
|
|
576
|
+
const bannerLink = config.banner?.link;
|
|
577
|
+
const bannerIsInternal = bannerLink ? (bannerLink.startsWith("#") || (basePath && bannerLink.startsWith(basePath + "/"))) : false;
|
|
578
|
+
|
|
501
579
|
return (
|
|
502
|
-
<div className="tome-grain" style={{ ...cssVars as React.CSSProperties, color: "var(--tx)", background: "var(--bg)", fontFamily: "var(--font-body)", minHeight: "100vh" }}>
|
|
580
|
+
<div className="tome-grain" style={{ ...cssVars as React.CSSProperties, color: "var(--tx)", background: "var(--bg)", fontFamily: "var(--font-body)", minHeight: "100vh", overflow: "hidden" }}>
|
|
581
|
+
{/* Banner */}
|
|
582
|
+
{config.banner?.text && !bannerDismissed && (
|
|
583
|
+
<div style={{
|
|
584
|
+
display: "flex", alignItems: "center", justifyContent: "center", gap: 12,
|
|
585
|
+
background: "var(--ac)", color: "#fff", padding: "8px 16px",
|
|
586
|
+
fontSize: 13, fontFamily: "var(--font-body)", fontWeight: 500, textAlign: "center",
|
|
587
|
+
width: "100%", boxSizing: "border-box",
|
|
588
|
+
}}>
|
|
589
|
+
{config.banner.link ? (
|
|
590
|
+
<a
|
|
591
|
+
href={bannerIsInternal && bannerLink!.startsWith("#") ? (basePath + "/" + bannerLink!.slice(1)) : bannerLink!}
|
|
592
|
+
{...(bannerIsInternal ? {} : { target: "_blank", rel: "noopener noreferrer" })}
|
|
593
|
+
style={{ color: "#fff", textDecoration: "underline" }}
|
|
594
|
+
onClick={bannerIsInternal ? (e: React.MouseEvent) => {
|
|
595
|
+
e.preventDefault();
|
|
596
|
+
const bp = basePath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
597
|
+
const pageId = bannerLink!.startsWith("#") ? bannerLink!.slice(1) : bannerLink!.replace(new RegExp("^" + bp + "/?"), "");
|
|
598
|
+
onNavigate(pageId || "index");
|
|
599
|
+
} : undefined}
|
|
600
|
+
>
|
|
601
|
+
{config.banner.text}
|
|
602
|
+
</a>
|
|
603
|
+
) : (
|
|
604
|
+
<span>{config.banner.text}</span>
|
|
605
|
+
)}
|
|
606
|
+
{config.banner.dismissible !== false && (
|
|
607
|
+
<button
|
|
608
|
+
onClick={() => {
|
|
609
|
+
setBannerDismissed(true);
|
|
610
|
+
try {
|
|
611
|
+
const hash = Array.from(config.banner!.text).reduce((h, c) => ((h << 5) - h + c.charCodeAt(0)) | 0, 0).toString(36);
|
|
612
|
+
localStorage.setItem("tome-banner-dismissed", hash);
|
|
613
|
+
} catch {}
|
|
614
|
+
}}
|
|
615
|
+
aria-label="Dismiss banner"
|
|
616
|
+
style={{ background: "none", border: "none", color: "#fff", cursor: "pointer", fontSize: 16, lineHeight: 1, padding: 0, opacity: 0.8 }}
|
|
617
|
+
>
|
|
618
|
+
×
|
|
619
|
+
</button>
|
|
620
|
+
)}
|
|
621
|
+
</div>
|
|
622
|
+
)}
|
|
623
|
+
|
|
503
624
|
{/* Search Modal (TOM-16: branch on provider) */}
|
|
504
625
|
{searchOpen && config.search?.provider === "algolia" && config.search.appId && config.search.apiKey && config.search.indexName ? (
|
|
505
626
|
<AlgoliaSearchModal
|
|
@@ -508,6 +629,7 @@ export function Shell({
|
|
|
508
629
|
indexName={config.search.indexName}
|
|
509
630
|
onNavigate={(id) => { onNavigate(id); setSearch(false); }}
|
|
510
631
|
onClose={() => setSearch(false)}
|
|
632
|
+
basePath={basePath}
|
|
511
633
|
/>
|
|
512
634
|
) : searchOpen ? (
|
|
513
635
|
<SearchModal
|
|
@@ -518,7 +640,7 @@ export function Shell({
|
|
|
518
640
|
/>
|
|
519
641
|
) : null}
|
|
520
642
|
|
|
521
|
-
<div style={{ display: "flex", height: "100vh" }}>
|
|
643
|
+
<div style={{ display: "flex", flex: 1, height: config.banner?.text && !bannerDismissed ? "calc(100vh - 32px)" : "100vh" }}>
|
|
522
644
|
{/* Mobile sidebar backdrop */}
|
|
523
645
|
{mobile && sbOpen && (
|
|
524
646
|
<div onClick={() => setSb(false)} style={{
|
|
@@ -587,6 +709,31 @@ export function Shell({
|
|
|
587
709
|
))}
|
|
588
710
|
</nav>
|
|
589
711
|
|
|
712
|
+
{/* Mobile version switcher in sidebar footer */}
|
|
713
|
+
{versioning && mobile && (
|
|
714
|
+
<div style={{ padding: "8px 16px", borderTop: "1px solid var(--bd)", display: "flex", gap: 6 }}>
|
|
715
|
+
{versioning.versions.map(v => (
|
|
716
|
+
<button
|
|
717
|
+
key={v}
|
|
718
|
+
onClick={() => {
|
|
719
|
+
// Navigate to the version's index page via hash routing
|
|
720
|
+
const targetId = v === versioning.current ? "index" : `${v}/index`;
|
|
721
|
+
onNavigate(targetId);
|
|
722
|
+
}}
|
|
723
|
+
style={{
|
|
724
|
+
flex: 1, padding: "6px 0", textAlign: "center",
|
|
725
|
+
background: v === (currentVersion || versioning.current) ? "var(--acD)" : "var(--sf)",
|
|
726
|
+
border: "1px solid var(--bd)", borderRadius: 2, cursor: "pointer",
|
|
727
|
+
color: v === (currentVersion || versioning.current) ? "var(--ac)" : "var(--tx2)",
|
|
728
|
+
fontSize: 12, fontFamily: "var(--font-code)",
|
|
729
|
+
fontWeight: v === versioning.current ? 600 : 400,
|
|
730
|
+
}}
|
|
731
|
+
>
|
|
732
|
+
{v}{v === versioning.current ? " (latest)" : ""}
|
|
733
|
+
</button>
|
|
734
|
+
))}
|
|
735
|
+
</div>
|
|
736
|
+
)}
|
|
590
737
|
<div style={{ padding: "12px 16px", borderTop: "1px solid var(--bd)", display: "flex", alignItems: "center", justifyContent: "space-between" }}>
|
|
591
738
|
{/* TOM-12: Only show toggle when mode is "auto" */}
|
|
592
739
|
{themeMode === "auto" ? (
|
|
@@ -605,6 +752,7 @@ export function Shell({
|
|
|
605
752
|
<header style={{
|
|
606
753
|
display: "flex", alignItems: "center", gap: mobile ? 8 : 12, padding: mobile ? "8px 12px" : "10px 24px",
|
|
607
754
|
borderBottom: "1px solid var(--bd)", background: "var(--hdBg)", backdropFilter: "blur(12px)",
|
|
755
|
+
maxWidth: "100vw", overflow: "visible", position: "relative", zIndex: 200,
|
|
608
756
|
}}>
|
|
609
757
|
<button aria-label={sbOpen ? "Close sidebar" : "Open sidebar"} onClick={() => setSb(!sbOpen)} style={{ background: "none", border: "none", color: "var(--txM)", cursor: "pointer", display: "flex" }}>
|
|
610
758
|
{sbOpen ? <XIcon /> : <MenuIcon />}
|
|
@@ -629,12 +777,18 @@ export function Shell({
|
|
|
629
777
|
{config.topNav && config.topNav.length > 0 && !mobile && (
|
|
630
778
|
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
|
|
631
779
|
{config.topNav.map((link) => {
|
|
632
|
-
const
|
|
780
|
+
const isInternal = link.href.startsWith("#") || (basePath && link.href.startsWith(basePath + "/"));
|
|
781
|
+
const isExternal = !isInternal;
|
|
633
782
|
return (
|
|
634
783
|
<a
|
|
635
784
|
key={link.label}
|
|
636
|
-
href={link.href}
|
|
785
|
+
href={isInternal && link.href.startsWith("#") ? (basePath + "/" + link.href.slice(1)) : link.href}
|
|
637
786
|
{...(isExternal ? { target: "_blank", rel: "noopener noreferrer" } : {})}
|
|
787
|
+
onClick={isInternal ? (e: React.MouseEvent) => {
|
|
788
|
+
e.preventDefault();
|
|
789
|
+
const pageId = link.href.startsWith("#") ? link.href.slice(1) : link.href.replace(new RegExp("^" + basePath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "/?"), "");
|
|
790
|
+
onNavigate(pageId);
|
|
791
|
+
} : undefined}
|
|
638
792
|
style={{
|
|
639
793
|
display: "flex", alignItems: "center", gap: 4,
|
|
640
794
|
color: "var(--txM)", textDecoration: "none", fontSize: 12,
|
|
@@ -653,8 +807,15 @@ export function Shell({
|
|
|
653
807
|
</div>
|
|
654
808
|
)}
|
|
655
809
|
|
|
656
|
-
{/*
|
|
657
|
-
{
|
|
810
|
+
{/* Theme toggle in header on mobile */}
|
|
811
|
+
{mobile && themeMode === "auto" && (
|
|
812
|
+
<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 }}>
|
|
813
|
+
{isDark ? <SunIcon /> : <MoonIcon />}
|
|
814
|
+
</button>
|
|
815
|
+
)}
|
|
816
|
+
|
|
817
|
+
{/* TOM-30: Version Switcher — hidden on mobile */}
|
|
818
|
+
{versioning && !mobile && (
|
|
658
819
|
<div style={{ position: "relative" }}>
|
|
659
820
|
<button
|
|
660
821
|
data-testid="version-switcher"
|
|
@@ -685,9 +846,9 @@ export function Shell({
|
|
|
685
846
|
key={v}
|
|
686
847
|
onClick={() => {
|
|
687
848
|
setVersionDropdown(false);
|
|
688
|
-
// Navigate to the version's
|
|
689
|
-
const
|
|
690
|
-
|
|
849
|
+
// Navigate to the version's index page via hash routing
|
|
850
|
+
const targetId = v === versioning.current ? "index" : `${v}/index`;
|
|
851
|
+
onNavigate(targetId);
|
|
691
852
|
}}
|
|
692
853
|
style={{
|
|
693
854
|
display: "block", width: "100%", textAlign: "left",
|
|
@@ -706,8 +867,8 @@ export function Shell({
|
|
|
706
867
|
</div>
|
|
707
868
|
)}
|
|
708
869
|
|
|
709
|
-
{/* TOM-34: Language Switcher */}
|
|
710
|
-
{i18n && i18n.locales.length > 1 && (
|
|
870
|
+
{/* TOM-34: Language Switcher — hidden on mobile */}
|
|
871
|
+
{i18n && i18n.locales.length > 1 && !mobile && (
|
|
711
872
|
<div style={{ position: "relative" }}>
|
|
712
873
|
<button
|
|
713
874
|
data-testid="language-switcher"
|
|
@@ -786,7 +947,7 @@ export function Shell({
|
|
|
786
947
|
>
|
|
787
948
|
<span>You're viewing docs for {currentVersion}.</span>
|
|
788
949
|
<button
|
|
789
|
-
onClick={() => {
|
|
950
|
+
onClick={() => { onNavigate("index"); }}
|
|
790
951
|
style={{
|
|
791
952
|
background: "none", border: "none", color: "var(--ac)",
|
|
792
953
|
cursor: "pointer", fontWeight: 600, fontSize: 13,
|
|
@@ -850,8 +1011,25 @@ export function Shell({
|
|
|
850
1011
|
</div>
|
|
851
1012
|
)}
|
|
852
1013
|
|
|
1014
|
+
{/* Feedback widget */}
|
|
1015
|
+
<div style={{ display: "flex", alignItems: "center", gap: 12, marginTop: 24, padding: "12px 0" }}>
|
|
1016
|
+
{feedbackGiven[currentPageId] ? (
|
|
1017
|
+
<span style={{ fontSize: 13, color: "var(--txM)", fontFamily: "var(--font-body)" }}>Thanks for your feedback!</span>
|
|
1018
|
+
) : (
|
|
1019
|
+
<>
|
|
1020
|
+
<span style={{ fontSize: 13, color: "var(--txM)", fontFamily: "var(--font-body)" }}>Was this helpful?</span>
|
|
1021
|
+
<button onClick={() => { setFeedbackGiven(prev => ({ ...prev, [currentPageId]: true })); try { localStorage.setItem(`tome-feedback-${currentPageId}`, "up"); } catch {} }} style={{
|
|
1022
|
+
background: "none", border: "1px solid var(--bd)", borderRadius: 2, padding: "4px 10px", cursor: "pointer", fontSize: 13, color: "var(--txM)", transition: "border-color .15s",
|
|
1023
|
+
}}>👍</button>
|
|
1024
|
+
<button onClick={() => { setFeedbackGiven(prev => ({ ...prev, [currentPageId]: true })); try { localStorage.setItem(`tome-feedback-${currentPageId}`, "down"); } catch {} }} style={{
|
|
1025
|
+
background: "none", border: "1px solid var(--bd)", borderRadius: 2, padding: "4px 10px", cursor: "pointer", fontSize: 13, color: "var(--txM)", transition: "border-color .15s",
|
|
1026
|
+
}}>👎</button>
|
|
1027
|
+
</>
|
|
1028
|
+
)}
|
|
1029
|
+
</div>
|
|
1030
|
+
|
|
853
1031
|
{/* Prev / Next */}
|
|
854
|
-
<div style={{ display: "flex", flexDirection: mobile ? "column" : "row", justifyContent: "space-between", marginTop:
|
|
1032
|
+
<div style={{ display: "flex", flexDirection: mobile ? "column" : "row", justifyContent: "space-between", marginTop: 16, paddingTop: 24, borderTop: "1px solid var(--bd)", gap: mobile ? 12 : 16 }}>
|
|
855
1033
|
{prev ? (
|
|
856
1034
|
<button onClick={() => onNavigate(prev.id)} style={{
|
|
857
1035
|
display: "flex", alignItems: "center", gap: 8, background: "none",
|
|
@@ -915,6 +1093,16 @@ export function Shell({
|
|
|
915
1093
|
context={docContext?.map((d) => `## ${d.title}\n${d.content}`).join("\n\n") ?? allPages.map((p) => `- ${p.title}${p.description ? ": " + p.description : ""}`).join("\n")}
|
|
916
1094
|
/>
|
|
917
1095
|
)}
|
|
1096
|
+
|
|
1097
|
+
{/* Image zoom overlay */}
|
|
1098
|
+
{zoomSrc && (
|
|
1099
|
+
<div onClick={() => setZoomSrc(null)} style={{
|
|
1100
|
+
position: "fixed", inset: 0, zIndex: 9999, display: "flex", alignItems: "center", justifyContent: "center",
|
|
1101
|
+
background: "rgba(0,0,0,0.7)", backdropFilter: "blur(8px)", cursor: "zoom-out",
|
|
1102
|
+
}}>
|
|
1103
|
+
<img src={zoomSrc} alt="" style={{ maxWidth: "90vw", maxHeight: "90vh", objectFit: "contain", borderRadius: 4, boxShadow: "0 16px 64px rgba(0,0,0,0.4)" }} />
|
|
1104
|
+
</div>
|
|
1105
|
+
)}
|
|
918
1106
|
</div>
|
|
919
1107
|
);
|
|
920
1108
|
}
|