@tomehq/theme 0.3.1 → 0.3.3

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 (67) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/{chunk-YXKONM3A.js → chunk-MSXVVBDW.js} +493 -143
  3. package/dist/entry.js +1 -1
  4. package/dist/index.d.ts +37 -1
  5. package/dist/index.js +1 -1
  6. package/package.json +5 -5
  7. package/src/Shell.test.tsx +405 -0
  8. package/src/Shell.tsx +248 -24
  9. package/src/__virtual_stubs/config.ts +2 -0
  10. package/src/__virtual_stubs/doc-context.ts +2 -0
  11. package/src/__virtual_stubs/overrides.ts +2 -0
  12. package/src/__virtual_stubs/page-loader.ts +4 -0
  13. package/src/__virtual_stubs/routes.ts +5 -0
  14. package/src/entry-helpers.test.ts +76 -0
  15. package/src/entry-helpers.ts +18 -1
  16. package/src/entry.test.tsx +695 -0
  17. package/src/entry.tsx +179 -4
  18. package/src/global.d.ts +11 -0
  19. package/vitest.config.ts +31 -1
  20. package/dist/chunk-2APCPR2Y.js +0 -2110
  21. package/dist/chunk-37JI6XGT.js +0 -1720
  22. package/dist/chunk-3A2LPGUL.js +0 -1991
  23. package/dist/chunk-3I2QTWTW.js +0 -1948
  24. package/dist/chunk-45M5UIAB.js +0 -2110
  25. package/dist/chunk-462AGU3S.js +0 -1959
  26. package/dist/chunk-7MUTU5D4.js +0 -1720
  27. package/dist/chunk-ABNPB6BB.js +0 -2133
  28. package/dist/chunk-BZGWSKT2.js +0 -573
  29. package/dist/chunk-CMQCNCSY.js +0 -2127
  30. package/dist/chunk-CTPOZMMK.js +0 -1703
  31. package/dist/chunk-DO544M3G.js +0 -1702
  32. package/dist/chunk-DPKZBFQP.js +0 -1777
  33. package/dist/chunk-EK7PZUEB.js +0 -2147
  34. package/dist/chunk-FMOLIHQF.js +0 -2182
  35. package/dist/chunk-FWBTK5TL.js +0 -1444
  36. package/dist/chunk-GDQIBNX5.js +0 -1962
  37. package/dist/chunk-GHQ2MODM.js +0 -2127
  38. package/dist/chunk-GR2WCRGK.js +0 -2182
  39. package/dist/chunk-HNLKDQ64.js +0 -2139
  40. package/dist/chunk-INUMUXN5.js +0 -2095
  41. package/dist/chunk-IW3NHNOQ.js +0 -2187
  42. package/dist/chunk-JA4PMX6M.js +0 -1500
  43. package/dist/chunk-JSPFS7G5.js +0 -2102
  44. package/dist/chunk-JZRT4WNC.js +0 -1441
  45. package/dist/chunk-KQBY2JDB.js +0 -2112
  46. package/dist/chunk-LIMYFTPC.js +0 -1468
  47. package/dist/chunk-MEP7P6A7.js +0 -1500
  48. package/dist/chunk-NOZBIES7.js +0 -1948
  49. package/dist/chunk-O4GH3KYX.js +0 -1712
  50. package/dist/chunk-OEXM3BEC.js +0 -1702
  51. package/dist/chunk-Q7PYTVW3.js +0 -1771
  52. package/dist/chunk-QCWZYABW.js +0 -1468
  53. package/dist/chunk-RDF25WB2.js +0 -2085
  54. package/dist/chunk-RKTT3ZEX.js +0 -1500
  55. package/dist/chunk-S47BRMNQ.js +0 -1715
  56. package/dist/chunk-S4ZH5F56.js +0 -1949
  57. package/dist/chunk-SRD7NJHS.js +0 -1949
  58. package/dist/chunk-SWFYJO5H.js +0 -2187
  59. package/dist/chunk-TQDWPSTO.js +0 -2087
  60. package/dist/chunk-TTRXRPP6.js +0 -1941
  61. package/dist/chunk-UKYFJSUA.js +0 -509
  62. package/dist/chunk-VKEQHP2E.js +0 -2133
  63. package/dist/chunk-VUT2FMSI.js +0 -1937
  64. package/dist/chunk-VVCC5JHK.js +0 -1949
  65. package/dist/chunk-W732TVBK.js +0 -1944
  66. package/dist/chunk-X4VQYPKO.js +0 -1768
  67. package/dist/chunk-YZ3P3TNS.js +0 -1760
package/dist/entry.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  entry_default
3
- } from "./chunk-YXKONM3A.js";
3
+ } from "./chunk-MSXVVBDW.js";
4
4
  export {
5
5
  entry_default as default
6
6
  };
package/dist/index.d.ts CHANGED
@@ -10,6 +10,7 @@ interface I18nInfo {
10
10
  defaultLocale: string;
11
11
  locales: string[];
12
12
  localeNames?: Record<string, string>;
13
+ localeDirs?: Record<string, "ltr" | "rtl">;
13
14
  }
14
15
  interface ShellProps {
15
16
  config: {
@@ -49,6 +50,12 @@ interface ShellProps {
49
50
  link?: string;
50
51
  dismissible?: boolean;
51
52
  };
53
+ socialLinks?: Array<{
54
+ platform: string;
55
+ url: string;
56
+ label?: string;
57
+ icon?: string;
58
+ }>;
52
59
  [key: string]: unknown;
53
60
  };
54
61
  navigation: Array<{
@@ -58,6 +65,10 @@ interface ShellProps {
58
65
  id: string;
59
66
  urlPath: string;
60
67
  icon?: string;
68
+ badge?: {
69
+ text: string;
70
+ variant: string;
71
+ };
61
72
  }>;
62
73
  }>;
63
74
  currentPageId: string;
@@ -85,6 +96,22 @@ interface ShellProps {
85
96
  items: string[];
86
97
  }>;
87
98
  }>;
99
+ apiManifest?: any;
100
+ apiBaseUrl?: string;
101
+ apiPlayground?: boolean;
102
+ apiAuth?: {
103
+ type: "bearer" | "apiKey";
104
+ header?: string;
105
+ };
106
+ ApiReferenceComponent?: React.ComponentType<{
107
+ manifest: any;
108
+ baseUrl?: string;
109
+ showPlayground?: boolean;
110
+ playgroundAuth?: {
111
+ type: "bearer" | "apiKey";
112
+ header?: string;
113
+ };
114
+ }>;
88
115
  onNavigate: (id: string) => void;
89
116
  allPages: Array<{
90
117
  id: string;
@@ -101,8 +128,17 @@ interface ShellProps {
101
128
  content: string;
102
129
  }>;
103
130
  basePath?: string;
131
+ isDraft?: boolean;
132
+ dir?: "ltr" | "rtl";
133
+ overrides?: {
134
+ Header?: React.ComponentType<any>;
135
+ Footer?: React.ComponentType<any>;
136
+ Sidebar?: React.ComponentType<any>;
137
+ Toc?: React.ComponentType<any>;
138
+ PageFooter?: React.ComponentType<any>;
139
+ };
104
140
  }
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;
141
+ declare function Shell({ config, navigation, currentPageId, pageHtml, pageComponent, mdxComponents, pageTitle, pageDescription, headings, tocEnabled, editUrl, lastUpdated, changelogEntries, apiManifest, apiBaseUrl, apiPlayground, apiAuth, ApiReferenceComponent, onNavigate, allPages, versioning, currentVersion, i18n, currentLocale, docContext, basePath, isDraft, dir: dirProp, overrides, }: ShellProps): react_jsx_runtime.JSX.Element;
106
142
 
107
143
  interface AiChatProps {
108
144
  provider: "openai" | "anthropic" | "custom";
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  Shell,
4
4
  THEME_PRESETS,
5
5
  entry_default
6
- } from "./chunk-YXKONM3A.js";
6
+ } from "./chunk-MSXVVBDW.js";
7
7
  export {
8
8
  AiChat,
9
9
  entry_default as App,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tomehq/theme",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
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.3.1",
13
- "@tomehq/core": "0.3.1"
12
+ "@tomehq/components": "0.3.3",
13
+ "@tomehq/core": "0.3.3"
14
14
  },
15
15
  "peerDependencies": {
16
16
  "react": "^18.0.0 || ^19.0.0",
@@ -41,11 +41,11 @@
41
41
  "license": "MIT",
42
42
  "repository": {
43
43
  "type": "git",
44
- "url": "https://github.com/vxcozy/tome.git",
44
+ "url": "https://github.com/tomehq/tome.git",
45
45
  "directory": "packages/theme"
46
46
  },
47
47
  "scripts": {
48
- "build": "tsup src/index.tsx src/entry.tsx --format esm --dts --external react --external react-dom --external 'virtual:tome/config' --external 'virtual:tome/routes' --external 'virtual:tome/page-loader' --external 'virtual:tome/doc-context' --external '@tomehq/components'",
48
+ "build": "tsup src/index.tsx src/entry.tsx --format esm --dts --external react --external react-dom --external 'virtual:tome/config' --external 'virtual:tome/routes' --external 'virtual:tome/page-loader' --external 'virtual:tome/doc-context' --external 'virtual:tome/overrides' --external '@tomehq/components'",
49
49
  "dev": "tsup src/index.tsx src/entry.tsx --format esm --dts --external react --external react-dom --external 'virtual:tome/config' --external 'virtual:tome/routes' --external 'virtual:tome/page-loader' --external 'virtual:tome/doc-context' --external '@tomehq/components' --watch",
50
50
  "clean": "rm -rf dist"
51
51
  }
@@ -860,6 +860,115 @@ describe("Shell feedback widget", () => {
860
860
  });
861
861
  });
862
862
 
863
+ // ── Breadcrumbs ──────────────────────────────────────────
864
+
865
+ describe("Shell breadcrumbs", () => {
866
+ const multiSectionNav = [
867
+ {
868
+ section: "Getting Started",
869
+ pages: [
870
+ { id: "intro", title: "Introduction", urlPath: "/intro" },
871
+ { id: "quickstart", title: "Quick Start", urlPath: "/quickstart" },
872
+ ],
873
+ },
874
+ {
875
+ section: "Guides",
876
+ pages: [
877
+ { id: "search", title: "Search", urlPath: "/search" },
878
+ { id: "deploy", title: "Deploy", urlPath: "/deploy" },
879
+ ],
880
+ },
881
+ ];
882
+
883
+ const multiSectionAllPages = [
884
+ { id: "intro", title: "Introduction" },
885
+ { id: "quickstart", title: "Quick Start" },
886
+ { id: "search", title: "Search" },
887
+ { id: "deploy", title: "Deploy" },
888
+ ];
889
+
890
+ it("renders breadcrumbs for a non-index page", () => {
891
+ renderShell({
892
+ navigation: multiSectionNav,
893
+ allPages: multiSectionAllPages,
894
+ currentPageId: "quickstart",
895
+ pageTitle: "Quick Start",
896
+ });
897
+ const breadcrumbs = screen.getByTestId("breadcrumbs");
898
+ expect(breadcrumbs).toBeInTheDocument();
899
+ expect(within(breadcrumbs).getByText("Getting Started")).toBeInTheDocument();
900
+ expect(within(breadcrumbs).getByText("Quick Start")).toBeInTheDocument();
901
+ });
902
+
903
+ it("does not render breadcrumbs on the index page", () => {
904
+ renderShell({
905
+ currentPageId: "index",
906
+ pageTitle: "Home",
907
+ });
908
+ expect(screen.queryByTestId("breadcrumbs")).not.toBeInTheDocument();
909
+ });
910
+
911
+ it("renders section name as a link", () => {
912
+ renderShell({
913
+ navigation: multiSectionNav,
914
+ allPages: multiSectionAllPages,
915
+ currentPageId: "deploy",
916
+ pageTitle: "Deploy",
917
+ });
918
+ const breadcrumbs = screen.getByTestId("breadcrumbs");
919
+ const sectionLink = within(breadcrumbs).getByText("Guides");
920
+ expect(sectionLink.tagName).toBe("A");
921
+ });
922
+
923
+ it("renders current page as plain text (not a link)", () => {
924
+ renderShell({
925
+ navigation: multiSectionNav,
926
+ allPages: multiSectionAllPages,
927
+ currentPageId: "deploy",
928
+ pageTitle: "Deploy",
929
+ });
930
+ const breadcrumbs = screen.getByTestId("breadcrumbs");
931
+ const currentPage = within(breadcrumbs).getByText("Deploy");
932
+ expect(currentPage.tagName).toBe("SPAN");
933
+ });
934
+
935
+ it("clicking section link calls onNavigate with first page id", () => {
936
+ const onNavigate = vi.fn();
937
+ renderShell({
938
+ navigation: multiSectionNav,
939
+ allPages: multiSectionAllPages,
940
+ currentPageId: "deploy",
941
+ pageTitle: "Deploy",
942
+ onNavigate,
943
+ });
944
+ const breadcrumbs = screen.getByTestId("breadcrumbs");
945
+ fireEvent.click(within(breadcrumbs).getByText("Guides"));
946
+ expect(onNavigate).toHaveBeenCalledWith("search");
947
+ });
948
+
949
+ it("renders separator between breadcrumb items", () => {
950
+ renderShell({
951
+ navigation: multiSectionNav,
952
+ allPages: multiSectionAllPages,
953
+ currentPageId: "quickstart",
954
+ pageTitle: "Quick Start",
955
+ });
956
+ const breadcrumbs = screen.getByTestId("breadcrumbs");
957
+ expect(breadcrumbs.textContent).toContain("\u203A");
958
+ });
959
+
960
+ it("has proper aria-label for accessibility", () => {
961
+ renderShell({
962
+ navigation: multiSectionNav,
963
+ allPages: multiSectionAllPages,
964
+ currentPageId: "quickstart",
965
+ pageTitle: "Quick Start",
966
+ });
967
+ const nav = screen.getByRole("navigation", { name: "Breadcrumbs" });
968
+ expect(nav).toBeInTheDocument();
969
+ });
970
+ });
971
+
863
972
  // ── Image zoom ───────────────────────────────────────────
864
973
 
865
974
  describe("Shell image zoom", () => {
@@ -869,3 +978,299 @@ describe("Shell image zoom", () => {
869
978
  expect(zoomOverlay).toBeNull();
870
979
  });
871
980
  });
981
+
982
+ // ── RTL support ──────────────────────────────────────────
983
+
984
+ describe("Shell RTL support", () => {
985
+ it("sets dir='rtl' on root element when dir prop is 'rtl'", () => {
986
+ const { container } = renderShell({ dir: "rtl" });
987
+ const root = container.querySelector("[dir='rtl']");
988
+ expect(root).not.toBeNull();
989
+ });
990
+
991
+ it("sets dir='ltr' on root element by default", () => {
992
+ const { container } = renderShell();
993
+ const root = container.querySelector("[dir='ltr']");
994
+ expect(root).not.toBeNull();
995
+ });
996
+
997
+ it("sets dir='ltr' when dir prop is explicitly 'ltr'", () => {
998
+ const { container } = renderShell({ dir: "ltr" });
999
+ const root = container.querySelector("[dir='ltr']");
1000
+ expect(root).not.toBeNull();
1001
+ expect(container.querySelector("[dir='rtl']")).toBeNull();
1002
+ });
1003
+
1004
+ it("resolves dir from i18n.localeDirs when dir prop is not set", () => {
1005
+ const { container } = renderShell({
1006
+ i18n: {
1007
+ defaultLocale: "en",
1008
+ locales: ["en", "ar"],
1009
+ localeDirs: { ar: "rtl" },
1010
+ },
1011
+ currentLocale: "ar",
1012
+ });
1013
+ const root = container.querySelector("[dir='rtl']");
1014
+ expect(root).not.toBeNull();
1015
+ });
1016
+
1017
+ it("defaults to ltr when locale has no entry in localeDirs", () => {
1018
+ const { container } = renderShell({
1019
+ i18n: {
1020
+ defaultLocale: "en",
1021
+ locales: ["en", "ar"],
1022
+ localeDirs: { ar: "rtl" },
1023
+ },
1024
+ currentLocale: "en",
1025
+ });
1026
+ const root = container.querySelector("[dir='ltr']");
1027
+ expect(root).not.toBeNull();
1028
+ });
1029
+
1030
+ it("reverses main layout flex direction for RTL", () => {
1031
+ const { container } = renderShell({ dir: "rtl" });
1032
+ const flexRow = container.querySelector('[style*="flex-direction: row-reverse"]');
1033
+ expect(flexRow).not.toBeNull();
1034
+ });
1035
+
1036
+ it("uses normal row direction for LTR", () => {
1037
+ const { container } = renderShell({ dir: "ltr" });
1038
+ const flexRowReverse = container.querySelector('[style*="flex-direction: row-reverse"]');
1039
+ expect(flexRowReverse).toBeNull();
1040
+ });
1041
+
1042
+ it("mirrors sidebar border to borderLeft for RTL", () => {
1043
+ const { container } = renderShell({ dir: "rtl" });
1044
+ const aside = container.querySelector("aside");
1045
+ expect(aside).not.toBeNull();
1046
+ expect(aside!.style.borderLeft).toContain("1px solid");
1047
+ });
1048
+
1049
+ it("renders without errors in RTL mode", () => {
1050
+ expect(() =>
1051
+ renderShell({ dir: "rtl" })
1052
+ ).not.toThrow();
1053
+ });
1054
+ });
1055
+
1056
+ // ── API Reference (TOM-19) ──────────────────────────────
1057
+
1058
+ describe("Shell API reference rendering", () => {
1059
+ const mockManifest = {
1060
+ title: "Test API",
1061
+ version: "1.0.0",
1062
+ servers: [{ url: "https://api.example.com" }],
1063
+ tags: [{ name: "Users", description: "User management" }],
1064
+ endpoints: [
1065
+ {
1066
+ method: "GET",
1067
+ path: "/users",
1068
+ summary: "List all users",
1069
+ tags: ["Users"],
1070
+ parameters: [],
1071
+ responses: [{ status: "200", description: "OK" }],
1072
+ },
1073
+ ],
1074
+ };
1075
+
1076
+ function MockApiRef({ manifest, baseUrl, showPlayground, playgroundAuth }: { manifest: any; baseUrl?: string; showPlayground?: boolean; playgroundAuth?: { type: string; header?: string } }) {
1077
+ return (
1078
+ <div data-testid="api-reference">
1079
+ <span data-testid="api-title">{manifest.title}</span>
1080
+ {baseUrl && <span data-testid="api-base-url">{baseUrl}</span>}
1081
+ {showPlayground && <span data-testid="api-playground">playground-enabled</span>}
1082
+ {playgroundAuth && <span data-testid="api-auth">{playgroundAuth.type}</span>}
1083
+ </div>
1084
+ );
1085
+ }
1086
+
1087
+ it("renders ApiReferenceComponent when both apiManifest and component are provided", () => {
1088
+ renderShell({
1089
+ apiManifest: mockManifest,
1090
+ ApiReferenceComponent: MockApiRef,
1091
+ pageHtml: undefined,
1092
+ });
1093
+ expect(screen.getByTestId("api-reference")).toBeInTheDocument();
1094
+ expect(screen.getByTestId("api-title")).toHaveTextContent("Test API");
1095
+ });
1096
+
1097
+ it("passes baseUrl to ApiReferenceComponent", () => {
1098
+ renderShell({
1099
+ apiManifest: mockManifest,
1100
+ ApiReferenceComponent: MockApiRef,
1101
+ apiBaseUrl: "https://api.example.com",
1102
+ pageHtml: undefined,
1103
+ });
1104
+ expect(screen.getByTestId("api-base-url")).toHaveTextContent("https://api.example.com");
1105
+ });
1106
+
1107
+ it("does not render API reference when only apiManifest is provided (no component)", () => {
1108
+ renderShell({
1109
+ apiManifest: mockManifest,
1110
+ pageHtml: "<p>Fallback content</p>",
1111
+ });
1112
+ expect(screen.queryByTestId("api-reference")).not.toBeInTheDocument();
1113
+ });
1114
+
1115
+ it("does not render API reference when only component is provided (no manifest)", () => {
1116
+ renderShell({
1117
+ ApiReferenceComponent: MockApiRef,
1118
+ pageHtml: "<p>Fallback content</p>",
1119
+ });
1120
+ expect(screen.queryByTestId("api-reference")).not.toBeInTheDocument();
1121
+ });
1122
+
1123
+ it("falls back to pageHtml when no apiManifest", () => {
1124
+ const { container } = renderShell({
1125
+ pageHtml: "<p>Regular page content</p>",
1126
+ });
1127
+ expect(container.querySelector(".tome-content")).toBeInTheDocument();
1128
+ expect(screen.queryByTestId("api-reference")).not.toBeInTheDocument();
1129
+ });
1130
+
1131
+ it("API reference takes precedence over changelog entries", () => {
1132
+ const changelogEntries = [
1133
+ { version: "1.0.0", sections: [{ type: "Added", items: ["Feature"] }] },
1134
+ ];
1135
+ renderShell({
1136
+ apiManifest: mockManifest,
1137
+ ApiReferenceComponent: MockApiRef,
1138
+ changelogEntries,
1139
+ pageHtml: undefined,
1140
+ });
1141
+ // API reference should render, not changelog
1142
+ expect(screen.getByTestId("api-reference")).toBeInTheDocument();
1143
+ expect(screen.queryByTestId("changelog-timeline")).not.toBeInTheDocument();
1144
+ });
1145
+
1146
+ it("API reference takes precedence over pageComponent", () => {
1147
+ const PageComp = () => <div data-testid="page-comp">MDX content</div>;
1148
+ renderShell({
1149
+ apiManifest: mockManifest,
1150
+ ApiReferenceComponent: MockApiRef,
1151
+ pageComponent: PageComp,
1152
+ pageHtml: undefined,
1153
+ });
1154
+ expect(screen.getByTestId("api-reference")).toBeInTheDocument();
1155
+ expect(screen.queryByTestId("page-comp")).not.toBeInTheDocument();
1156
+ });
1157
+
1158
+ it("passes showPlayground prop to ApiReferenceComponent", () => {
1159
+ renderShell({
1160
+ apiManifest: mockManifest,
1161
+ ApiReferenceComponent: MockApiRef,
1162
+ apiPlayground: true,
1163
+ pageHtml: undefined,
1164
+ });
1165
+ expect(screen.getByTestId("api-playground")).toHaveTextContent("playground-enabled");
1166
+ });
1167
+
1168
+ it("passes playgroundAuth prop to ApiReferenceComponent", () => {
1169
+ renderShell({
1170
+ apiManifest: mockManifest,
1171
+ ApiReferenceComponent: MockApiRef,
1172
+ apiPlayground: true,
1173
+ apiAuth: { type: "bearer" },
1174
+ pageHtml: undefined,
1175
+ });
1176
+ expect(screen.getByTestId("api-auth")).toHaveTextContent("bearer");
1177
+ });
1178
+
1179
+ it("does not pass playground props when not provided", () => {
1180
+ renderShell({
1181
+ apiManifest: mockManifest,
1182
+ ApiReferenceComponent: MockApiRef,
1183
+ pageHtml: undefined,
1184
+ });
1185
+ expect(screen.getByTestId("api-reference")).toBeInTheDocument();
1186
+ expect(screen.queryByTestId("api-playground")).not.toBeInTheDocument();
1187
+ expect(screen.queryByTestId("api-auth")).not.toBeInTheDocument();
1188
+ });
1189
+ });
1190
+
1191
+ // ── Content link interception ───────────────────────────────
1192
+
1193
+ describe("Content link interception", () => {
1194
+ it("intercepts internal bare page ID links and calls onNavigate", () => {
1195
+ const onNavigate = vi.fn();
1196
+ renderShell({
1197
+ onNavigate,
1198
+ pageHtml: '<p><a href="quickstart">Go to quickstart</a></p>',
1199
+ });
1200
+ const link = screen.getByText("Go to quickstart");
1201
+ fireEvent.click(link);
1202
+ expect(onNavigate).toHaveBeenCalledWith("quickstart");
1203
+ });
1204
+
1205
+ it("intercepts ./relative links and strips the prefix", () => {
1206
+ const onNavigate = vi.fn();
1207
+ renderShell({
1208
+ onNavigate,
1209
+ pageHtml: '<p><a href="./installation">Install</a></p>',
1210
+ });
1211
+ fireEvent.click(screen.getByText("Install"));
1212
+ expect(onNavigate).toHaveBeenCalledWith("installation");
1213
+ });
1214
+
1215
+ it("does not intercept external http links", () => {
1216
+ const onNavigate = vi.fn();
1217
+ renderShell({
1218
+ onNavigate,
1219
+ pageHtml: '<p><a href="https://example.com">External</a></p>',
1220
+ });
1221
+ fireEvent.click(screen.getByText("External"));
1222
+ expect(onNavigate).not.toHaveBeenCalled();
1223
+ });
1224
+
1225
+ it("does not intercept mailto links", () => {
1226
+ const onNavigate = vi.fn();
1227
+ renderShell({
1228
+ onNavigate,
1229
+ pageHtml: '<p><a href="mailto:test@example.com">Email</a></p>',
1230
+ });
1231
+ fireEvent.click(screen.getByText("Email"));
1232
+ expect(onNavigate).not.toHaveBeenCalled();
1233
+ });
1234
+
1235
+ it("does not intercept pure anchor links", () => {
1236
+ const onNavigate = vi.fn();
1237
+ renderShell({
1238
+ onNavigate,
1239
+ pageHtml: '<p><a href="#section-1">Jump to section</a></p>',
1240
+ });
1241
+ fireEvent.click(screen.getByText("Jump to section"));
1242
+ expect(onNavigate).not.toHaveBeenCalled();
1243
+ });
1244
+
1245
+ it("strips basePath prefix from links", () => {
1246
+ const onNavigate = vi.fn();
1247
+ renderShell({
1248
+ onNavigate,
1249
+ basePath: "/docs/",
1250
+ pageHtml: '<p><a href="/docs/quickstart">Go</a></p>',
1251
+ });
1252
+ fireEvent.click(screen.getByText("Go"));
1253
+ expect(onNavigate).toHaveBeenCalledWith("quickstart");
1254
+ });
1255
+
1256
+ it("resolves basePath-only link to index", () => {
1257
+ const onNavigate = vi.fn();
1258
+ renderShell({
1259
+ onNavigate,
1260
+ basePath: "/docs/",
1261
+ pageHtml: '<p><a href="/docs/">Home</a></p>',
1262
+ });
1263
+ fireEvent.click(screen.getByText("Home"));
1264
+ expect(onNavigate).toHaveBeenCalledWith("index");
1265
+ });
1266
+
1267
+ it("resolves empty path to index", () => {
1268
+ const onNavigate = vi.fn();
1269
+ renderShell({
1270
+ onNavigate,
1271
+ pageHtml: '<p><a href="./">Home</a></p>',
1272
+ });
1273
+ fireEvent.click(screen.getByText("Home"));
1274
+ expect(onNavigate).toHaveBeenCalledWith("index");
1275
+ });
1276
+ });