@rangojs/router 0.0.0-experimental.0f44aca1

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 (305) hide show
  1. package/AGENTS.md +5 -0
  2. package/README.md +899 -0
  3. package/dist/bin/rango.js +1601 -0
  4. package/dist/vite/index.js +5214 -0
  5. package/package.json +176 -0
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +262 -0
  8. package/skills/caching/SKILL.md +220 -0
  9. package/skills/composability/SKILL.md +172 -0
  10. package/skills/debug-manifest/SKILL.md +112 -0
  11. package/skills/document-cache/SKILL.md +182 -0
  12. package/skills/fonts/SKILL.md +167 -0
  13. package/skills/hooks/SKILL.md +704 -0
  14. package/skills/host-router/SKILL.md +218 -0
  15. package/skills/intercept/SKILL.md +313 -0
  16. package/skills/layout/SKILL.md +310 -0
  17. package/skills/links/SKILL.md +239 -0
  18. package/skills/loader/SKILL.md +596 -0
  19. package/skills/middleware/SKILL.md +339 -0
  20. package/skills/mime-routes/SKILL.md +128 -0
  21. package/skills/parallel/SKILL.md +305 -0
  22. package/skills/prerender/SKILL.md +643 -0
  23. package/skills/rango/SKILL.md +118 -0
  24. package/skills/response-routes/SKILL.md +411 -0
  25. package/skills/route/SKILL.md +385 -0
  26. package/skills/router-setup/SKILL.md +439 -0
  27. package/skills/tailwind/SKILL.md +129 -0
  28. package/skills/theme/SKILL.md +79 -0
  29. package/skills/typesafety/SKILL.md +623 -0
  30. package/skills/use-cache/SKILL.md +324 -0
  31. package/src/__internal.ts +273 -0
  32. package/src/bin/rango.ts +321 -0
  33. package/src/browser/action-coordinator.ts +97 -0
  34. package/src/browser/action-response-classifier.ts +99 -0
  35. package/src/browser/event-controller.ts +899 -0
  36. package/src/browser/history-state.ts +80 -0
  37. package/src/browser/index.ts +18 -0
  38. package/src/browser/intercept-utils.ts +52 -0
  39. package/src/browser/link-interceptor.ts +141 -0
  40. package/src/browser/logging.ts +55 -0
  41. package/src/browser/merge-segment-loaders.ts +134 -0
  42. package/src/browser/navigation-bridge.ts +645 -0
  43. package/src/browser/navigation-client.ts +215 -0
  44. package/src/browser/navigation-store.ts +806 -0
  45. package/src/browser/navigation-transaction.ts +295 -0
  46. package/src/browser/network-error-handler.ts +61 -0
  47. package/src/browser/partial-update.ts +550 -0
  48. package/src/browser/prefetch/cache.ts +146 -0
  49. package/src/browser/prefetch/fetch.ts +135 -0
  50. package/src/browser/prefetch/observer.ts +65 -0
  51. package/src/browser/prefetch/policy.ts +42 -0
  52. package/src/browser/prefetch/queue.ts +88 -0
  53. package/src/browser/rango-state.ts +112 -0
  54. package/src/browser/react/Link.tsx +360 -0
  55. package/src/browser/react/NavigationProvider.tsx +386 -0
  56. package/src/browser/react/ScrollRestoration.tsx +94 -0
  57. package/src/browser/react/context.ts +59 -0
  58. package/src/browser/react/filter-segment-order.ts +11 -0
  59. package/src/browser/react/index.ts +52 -0
  60. package/src/browser/react/location-state-shared.ts +162 -0
  61. package/src/browser/react/location-state.ts +107 -0
  62. package/src/browser/react/mount-context.ts +37 -0
  63. package/src/browser/react/nonce-context.ts +23 -0
  64. package/src/browser/react/shallow-equal.ts +27 -0
  65. package/src/browser/react/use-action.ts +218 -0
  66. package/src/browser/react/use-client-cache.ts +58 -0
  67. package/src/browser/react/use-handle.ts +162 -0
  68. package/src/browser/react/use-href.tsx +40 -0
  69. package/src/browser/react/use-link-status.ts +135 -0
  70. package/src/browser/react/use-mount.ts +31 -0
  71. package/src/browser/react/use-navigation.ts +99 -0
  72. package/src/browser/react/use-params.ts +65 -0
  73. package/src/browser/react/use-pathname.ts +47 -0
  74. package/src/browser/react/use-router.ts +63 -0
  75. package/src/browser/react/use-search-params.ts +56 -0
  76. package/src/browser/react/use-segments.ts +171 -0
  77. package/src/browser/response-adapter.ts +73 -0
  78. package/src/browser/rsc-router.tsx +431 -0
  79. package/src/browser/scroll-restoration.ts +400 -0
  80. package/src/browser/segment-reconciler.ts +216 -0
  81. package/src/browser/segment-structure-assert.ts +83 -0
  82. package/src/browser/server-action-bridge.ts +667 -0
  83. package/src/browser/shallow.ts +40 -0
  84. package/src/browser/types.ts +538 -0
  85. package/src/browser/validate-redirect-origin.ts +29 -0
  86. package/src/build/generate-manifest.ts +438 -0
  87. package/src/build/generate-route-types.ts +36 -0
  88. package/src/build/index.ts +35 -0
  89. package/src/build/route-trie.ts +265 -0
  90. package/src/build/route-types/ast-helpers.ts +25 -0
  91. package/src/build/route-types/ast-route-extraction.ts +98 -0
  92. package/src/build/route-types/codegen.ts +102 -0
  93. package/src/build/route-types/include-resolution.ts +411 -0
  94. package/src/build/route-types/param-extraction.ts +48 -0
  95. package/src/build/route-types/per-module-writer.ts +128 -0
  96. package/src/build/route-types/router-processing.ts +469 -0
  97. package/src/build/route-types/scan-filter.ts +78 -0
  98. package/src/build/runtime-discovery.ts +231 -0
  99. package/src/cache/background-task.ts +34 -0
  100. package/src/cache/cache-key-utils.ts +44 -0
  101. package/src/cache/cache-policy.ts +125 -0
  102. package/src/cache/cache-runtime.ts +338 -0
  103. package/src/cache/cache-scope.ts +382 -0
  104. package/src/cache/cf/cf-cache-store.ts +540 -0
  105. package/src/cache/cf/index.ts +25 -0
  106. package/src/cache/document-cache.ts +369 -0
  107. package/src/cache/handle-capture.ts +81 -0
  108. package/src/cache/handle-snapshot.ts +41 -0
  109. package/src/cache/index.ts +43 -0
  110. package/src/cache/memory-segment-store.ts +328 -0
  111. package/src/cache/profile-registry.ts +73 -0
  112. package/src/cache/read-through-swr.ts +134 -0
  113. package/src/cache/segment-codec.ts +256 -0
  114. package/src/cache/taint.ts +98 -0
  115. package/src/cache/types.ts +342 -0
  116. package/src/client.rsc.tsx +85 -0
  117. package/src/client.tsx +601 -0
  118. package/src/component-utils.ts +76 -0
  119. package/src/components/DefaultDocument.tsx +27 -0
  120. package/src/context-var.ts +86 -0
  121. package/src/debug.ts +243 -0
  122. package/src/default-error-boundary.tsx +88 -0
  123. package/src/deps/browser.ts +8 -0
  124. package/src/deps/html-stream-client.ts +2 -0
  125. package/src/deps/html-stream-server.ts +2 -0
  126. package/src/deps/rsc.ts +10 -0
  127. package/src/deps/ssr.ts +2 -0
  128. package/src/errors.ts +365 -0
  129. package/src/handle.ts +135 -0
  130. package/src/handles/MetaTags.tsx +246 -0
  131. package/src/handles/breadcrumbs.ts +66 -0
  132. package/src/handles/index.ts +7 -0
  133. package/src/handles/meta.ts +264 -0
  134. package/src/host/cookie-handler.ts +165 -0
  135. package/src/host/errors.ts +97 -0
  136. package/src/host/index.ts +53 -0
  137. package/src/host/pattern-matcher.ts +214 -0
  138. package/src/host/router.ts +352 -0
  139. package/src/host/testing.ts +79 -0
  140. package/src/host/types.ts +146 -0
  141. package/src/host/utils.ts +25 -0
  142. package/src/href-client.ts +222 -0
  143. package/src/index.rsc.ts +233 -0
  144. package/src/index.ts +277 -0
  145. package/src/internal-debug.ts +11 -0
  146. package/src/loader.rsc.ts +89 -0
  147. package/src/loader.ts +64 -0
  148. package/src/network-error-thrower.tsx +23 -0
  149. package/src/outlet-context.ts +15 -0
  150. package/src/outlet-provider.tsx +45 -0
  151. package/src/prerender/param-hash.ts +37 -0
  152. package/src/prerender/store.ts +185 -0
  153. package/src/prerender.ts +463 -0
  154. package/src/reverse.ts +330 -0
  155. package/src/root-error-boundary.tsx +289 -0
  156. package/src/route-content-wrapper.tsx +196 -0
  157. package/src/route-definition/dsl-helpers.ts +934 -0
  158. package/src/route-definition/helper-factories.ts +200 -0
  159. package/src/route-definition/helpers-types.ts +430 -0
  160. package/src/route-definition/index.ts +52 -0
  161. package/src/route-definition/redirect.ts +93 -0
  162. package/src/route-definition.ts +1 -0
  163. package/src/route-map-builder.ts +275 -0
  164. package/src/route-name.ts +53 -0
  165. package/src/route-types.ts +259 -0
  166. package/src/router/content-negotiation.ts +116 -0
  167. package/src/router/debug-manifest.ts +72 -0
  168. package/src/router/error-handling.ts +287 -0
  169. package/src/router/find-match.ts +158 -0
  170. package/src/router/handler-context.ts +451 -0
  171. package/src/router/intercept-resolution.ts +395 -0
  172. package/src/router/lazy-includes.ts +234 -0
  173. package/src/router/loader-resolution.ts +420 -0
  174. package/src/router/logging.ts +248 -0
  175. package/src/router/manifest.ts +267 -0
  176. package/src/router/match-api.ts +620 -0
  177. package/src/router/match-context.ts +266 -0
  178. package/src/router/match-handlers.ts +440 -0
  179. package/src/router/match-middleware/background-revalidation.ts +223 -0
  180. package/src/router/match-middleware/cache-lookup.ts +634 -0
  181. package/src/router/match-middleware/cache-store.ts +295 -0
  182. package/src/router/match-middleware/index.ts +81 -0
  183. package/src/router/match-middleware/intercept-resolution.ts +306 -0
  184. package/src/router/match-middleware/segment-resolution.ts +192 -0
  185. package/src/router/match-pipelines.ts +179 -0
  186. package/src/router/match-result.ts +219 -0
  187. package/src/router/metrics.ts +282 -0
  188. package/src/router/middleware-cookies.ts +55 -0
  189. package/src/router/middleware-types.ts +222 -0
  190. package/src/router/middleware.ts +748 -0
  191. package/src/router/pattern-matching.ts +563 -0
  192. package/src/router/prerender-match.ts +402 -0
  193. package/src/router/preview-match.ts +170 -0
  194. package/src/router/revalidation.ts +289 -0
  195. package/src/router/router-context.ts +316 -0
  196. package/src/router/router-interfaces.ts +452 -0
  197. package/src/router/router-options.ts +592 -0
  198. package/src/router/router-registry.ts +24 -0
  199. package/src/router/segment-resolution/fresh.ts +570 -0
  200. package/src/router/segment-resolution/helpers.ts +263 -0
  201. package/src/router/segment-resolution/loader-cache.ts +198 -0
  202. package/src/router/segment-resolution/revalidation.ts +1239 -0
  203. package/src/router/segment-resolution/static-store.ts +67 -0
  204. package/src/router/segment-resolution.ts +21 -0
  205. package/src/router/segment-wrappers.ts +289 -0
  206. package/src/router/telemetry-otel.ts +299 -0
  207. package/src/router/telemetry.ts +300 -0
  208. package/src/router/timeout.ts +148 -0
  209. package/src/router/trie-matching.ts +239 -0
  210. package/src/router/types.ts +170 -0
  211. package/src/router.ts +1002 -0
  212. package/src/rsc/handler-context.ts +45 -0
  213. package/src/rsc/handler.ts +1089 -0
  214. package/src/rsc/helpers.ts +198 -0
  215. package/src/rsc/index.ts +36 -0
  216. package/src/rsc/loader-fetch.ts +209 -0
  217. package/src/rsc/manifest-init.ts +86 -0
  218. package/src/rsc/nonce.ts +32 -0
  219. package/src/rsc/origin-guard.ts +141 -0
  220. package/src/rsc/progressive-enhancement.ts +379 -0
  221. package/src/rsc/response-error.ts +37 -0
  222. package/src/rsc/response-route-handler.ts +347 -0
  223. package/src/rsc/rsc-rendering.ts +235 -0
  224. package/src/rsc/runtime-warnings.ts +42 -0
  225. package/src/rsc/server-action.ts +348 -0
  226. package/src/rsc/ssr-setup.ts +128 -0
  227. package/src/rsc/types.ts +263 -0
  228. package/src/search-params.ts +230 -0
  229. package/src/segment-system.tsx +454 -0
  230. package/src/server/context.ts +591 -0
  231. package/src/server/cookie-store.ts +190 -0
  232. package/src/server/fetchable-loader-store.ts +37 -0
  233. package/src/server/handle-store.ts +308 -0
  234. package/src/server/loader-registry.ts +133 -0
  235. package/src/server/request-context.ts +914 -0
  236. package/src/server/root-layout.tsx +10 -0
  237. package/src/server/tsconfig.json +14 -0
  238. package/src/server.ts +51 -0
  239. package/src/ssr/index.tsx +365 -0
  240. package/src/static-handler.ts +114 -0
  241. package/src/theme/ThemeProvider.tsx +297 -0
  242. package/src/theme/ThemeScript.tsx +61 -0
  243. package/src/theme/constants.ts +62 -0
  244. package/src/theme/index.ts +48 -0
  245. package/src/theme/theme-context.ts +44 -0
  246. package/src/theme/theme-script.ts +155 -0
  247. package/src/theme/types.ts +182 -0
  248. package/src/theme/use-theme.ts +44 -0
  249. package/src/types/boundaries.ts +158 -0
  250. package/src/types/cache-types.ts +198 -0
  251. package/src/types/error-types.ts +192 -0
  252. package/src/types/global-namespace.ts +100 -0
  253. package/src/types/handler-context.ts +687 -0
  254. package/src/types/index.ts +88 -0
  255. package/src/types/loader-types.ts +183 -0
  256. package/src/types/route-config.ts +170 -0
  257. package/src/types/route-entry.ts +102 -0
  258. package/src/types/segments.ts +148 -0
  259. package/src/types.ts +1 -0
  260. package/src/urls/include-helper.ts +197 -0
  261. package/src/urls/index.ts +53 -0
  262. package/src/urls/path-helper-types.ts +339 -0
  263. package/src/urls/path-helper.ts +329 -0
  264. package/src/urls/pattern-types.ts +95 -0
  265. package/src/urls/response-types.ts +106 -0
  266. package/src/urls/type-extraction.ts +372 -0
  267. package/src/urls/urls-function.ts +98 -0
  268. package/src/urls.ts +1 -0
  269. package/src/use-loader.tsx +354 -0
  270. package/src/vite/discovery/bundle-postprocess.ts +184 -0
  271. package/src/vite/discovery/discover-routers.ts +344 -0
  272. package/src/vite/discovery/prerender-collection.ts +385 -0
  273. package/src/vite/discovery/route-types-writer.ts +258 -0
  274. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  275. package/src/vite/discovery/state.ts +110 -0
  276. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  277. package/src/vite/index.ts +16 -0
  278. package/src/vite/plugin-types.ts +131 -0
  279. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  280. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  281. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  282. package/src/vite/plugins/expose-action-id.ts +365 -0
  283. package/src/vite/plugins/expose-id-utils.ts +287 -0
  284. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  285. package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
  286. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  287. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  288. package/src/vite/plugins/expose-ids/types.ts +45 -0
  289. package/src/vite/plugins/expose-internal-ids.ts +569 -0
  290. package/src/vite/plugins/refresh-cmd.ts +65 -0
  291. package/src/vite/plugins/use-cache-transform.ts +323 -0
  292. package/src/vite/plugins/version-injector.ts +83 -0
  293. package/src/vite/plugins/version-plugin.ts +254 -0
  294. package/src/vite/plugins/version.d.ts +12 -0
  295. package/src/vite/plugins/virtual-entries.ts +123 -0
  296. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  297. package/src/vite/rango.ts +510 -0
  298. package/src/vite/router-discovery.ts +785 -0
  299. package/src/vite/utils/ast-handler-extract.ts +517 -0
  300. package/src/vite/utils/banner.ts +36 -0
  301. package/src/vite/utils/bundle-analysis.ts +137 -0
  302. package/src/vite/utils/manifest-utils.ts +70 -0
  303. package/src/vite/utils/package-resolution.ts +121 -0
  304. package/src/vite/utils/prerender-utils.ts +189 -0
  305. package/src/vite/utils/shared-utils.ts +169 -0
@@ -0,0 +1,297 @@
1
+ "use client";
2
+
3
+ /**
4
+ * ThemeProvider - Client component that provides theme state and management.
5
+ *
6
+ * Features:
7
+ * - Syncs theme to cookie/localStorage
8
+ * - Detects system preference changes
9
+ * - Cross-tab synchronization via storage events
10
+ * - Updates HTML element attribute when theme changes
11
+ * - Handles SSR hydration by deferring system theme detection
12
+ */
13
+
14
+ import React, {
15
+ useCallback,
16
+ useEffect,
17
+ useMemo,
18
+ useState,
19
+ useRef,
20
+ } from "react";
21
+ import { ThemeContext } from "./theme-context.js";
22
+ import type {
23
+ ResolvedTheme,
24
+ ResolvedThemeConfig,
25
+ Theme,
26
+ ThemeContextValue,
27
+ ThemeProviderProps,
28
+ } from "./types.js";
29
+ import { THEME_COOKIE } from "./constants.js";
30
+
31
+ /**
32
+ * Get system preference for color scheme
33
+ */
34
+ function getSystemTheme(): ResolvedTheme {
35
+ if (typeof window !== "undefined" && window.matchMedia) {
36
+ return window.matchMedia("(prefers-color-scheme: dark)").matches
37
+ ? "dark"
38
+ : "light";
39
+ }
40
+ return "light";
41
+ }
42
+
43
+ /**
44
+ * Read theme from cookie
45
+ */
46
+ function readThemeFromCookie(storageKey: string): string | null {
47
+ if (typeof document === "undefined") return null;
48
+
49
+ const cookies = document.cookie.split(";");
50
+ for (const cookie of cookies) {
51
+ const [name, ...rest] = cookie.trim().split("=");
52
+ if (name === storageKey) {
53
+ const raw = rest.join("=");
54
+ try {
55
+ return decodeURIComponent(raw);
56
+ } catch {
57
+ return raw;
58
+ }
59
+ }
60
+ }
61
+ return null;
62
+ }
63
+
64
+ /**
65
+ * Read theme from localStorage
66
+ */
67
+ function readThemeFromStorage(storageKey: string): string | null {
68
+ if (typeof localStorage === "undefined") return null;
69
+
70
+ try {
71
+ return localStorage.getItem(storageKey);
72
+ } catch {
73
+ return null;
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Write theme to cookie
79
+ */
80
+ function writeThemeToCookie(storageKey: string, theme: Theme): void {
81
+ if (typeof document === "undefined") return;
82
+
83
+ const value = encodeURIComponent(theme);
84
+ const cookie = `${storageKey}=${value}; Path=${THEME_COOKIE.path}; Max-Age=${THEME_COOKIE.maxAge}; SameSite=${THEME_COOKIE.sameSite}`;
85
+ document.cookie = cookie;
86
+ }
87
+
88
+ /**
89
+ * Write theme to localStorage
90
+ */
91
+ function writeThemeToStorage(storageKey: string, theme: Theme): void {
92
+ if (typeof localStorage === "undefined") return;
93
+
94
+ try {
95
+ localStorage.setItem(storageKey, theme);
96
+ } catch {
97
+ // localStorage might be disabled or full
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Apply theme to HTML element
103
+ */
104
+ function applyThemeToDocument(theme: Theme, config: ResolvedThemeConfig): void {
105
+ if (typeof document === "undefined") return;
106
+
107
+ const resolved =
108
+ theme === "system" && config.enableSystem
109
+ ? getSystemTheme()
110
+ : (theme as ResolvedTheme);
111
+
112
+ const value = config.value[resolved] || resolved;
113
+ const el = document.documentElement;
114
+
115
+ // Apply attribute
116
+ if (config.attribute === "class") {
117
+ // Remove all theme classes
118
+ for (const t of config.themes) {
119
+ const v = config.value[t] || t;
120
+ el.classList.remove(v);
121
+ }
122
+ // Add current theme class
123
+ el.classList.add(value);
124
+ } else {
125
+ el.setAttribute(config.attribute, value);
126
+ }
127
+
128
+ // Set color-scheme for native dark mode support
129
+ if (config.enableColorScheme) {
130
+ el.style.colorScheme = resolved;
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Get the resolved stored theme (validated against available themes)
136
+ */
137
+ function getStoredTheme(config: ResolvedThemeConfig): Theme {
138
+ const { storageKey, themes, defaultTheme, enableSystem } = config;
139
+
140
+ // Try cookie first (for SSR consistency)
141
+ let stored = readThemeFromCookie(storageKey);
142
+
143
+ // Fall back to localStorage
144
+ if (!stored) {
145
+ stored = readThemeFromStorage(storageKey);
146
+ }
147
+
148
+ // Validate stored value
149
+ if (stored) {
150
+ if (stored === "system" && enableSystem) {
151
+ return "system";
152
+ }
153
+ if (themes.includes(stored)) {
154
+ return stored as Theme;
155
+ }
156
+ }
157
+
158
+ return defaultTheme;
159
+ }
160
+
161
+ /**
162
+ * ThemeProvider component
163
+ *
164
+ * Provides theme state to the component tree via context.
165
+ * Handles theme persistence, system preference detection, and cross-tab sync.
166
+ */
167
+ export function ThemeProvider({
168
+ config,
169
+ initialTheme,
170
+ children,
171
+ }: ThemeProviderProps): React.ReactNode {
172
+ // Track mount state to avoid hydration mismatches
173
+ // During SSR and initial hydration, mounted is false
174
+ const [mounted, setMounted] = useState(false);
175
+
176
+ // Initialize theme from prop, storage, or default
177
+ const [theme, setThemeState] = useState<Theme>(() => {
178
+ if (initialTheme) return initialTheme;
179
+ if (typeof window === "undefined") return config.defaultTheme;
180
+ return getStoredTheme(config);
181
+ });
182
+
183
+ // Track system preference - use stable default during SSR
184
+ const [systemTheme, setSystemTheme] = useState<ResolvedTheme>("light");
185
+
186
+ // Set mounted after hydration and detect actual system theme
187
+ useEffect(() => {
188
+ setMounted(true);
189
+ setSystemTheme(getSystemTheme());
190
+ }, []);
191
+
192
+ // Set theme and persist to storage
193
+ const setTheme = useCallback(
194
+ (newTheme: Theme) => {
195
+ setThemeState(newTheme);
196
+ writeThemeToCookie(config.storageKey, newTheme);
197
+ writeThemeToStorage(config.storageKey, newTheme);
198
+ applyThemeToDocument(newTheme, config);
199
+ },
200
+ [config],
201
+ );
202
+
203
+ // Listen for system preference changes
204
+ useEffect(() => {
205
+ if (!config.enableSystem) return;
206
+ if (typeof window === "undefined" || !window.matchMedia) return;
207
+
208
+ const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
209
+
210
+ const handleChange = (e: MediaQueryListEvent) => {
211
+ const newSystemTheme = e.matches ? "dark" : "light";
212
+ setSystemTheme(newSystemTheme);
213
+
214
+ // If current theme is "system", re-apply to update document
215
+ if (theme === "system") {
216
+ applyThemeToDocument("system", config);
217
+ }
218
+ };
219
+
220
+ // Modern browsers
221
+ mediaQuery.addEventListener("change", handleChange);
222
+
223
+ return () => {
224
+ mediaQuery.removeEventListener("change", handleChange);
225
+ };
226
+ }, [config, theme]);
227
+
228
+ // Cross-tab synchronization via localStorage storage event
229
+ useEffect(() => {
230
+ if (typeof window === "undefined") return;
231
+
232
+ const handleStorageChange = (e: StorageEvent) => {
233
+ if (e.key !== config.storageKey) return;
234
+
235
+ const newTheme = e.newValue;
236
+ if (!newTheme) return;
237
+
238
+ // Validate and apply
239
+ if (newTheme === "system" || config.themes.includes(newTheme)) {
240
+ setThemeState(newTheme as Theme);
241
+ applyThemeToDocument(newTheme as Theme, config);
242
+ }
243
+ };
244
+
245
+ window.addEventListener("storage", handleStorageChange);
246
+
247
+ return () => {
248
+ window.removeEventListener("storage", handleStorageChange);
249
+ };
250
+ }, [config]);
251
+
252
+ // Compute resolved theme
253
+ // During SSR (not mounted), use the initial theme or default to avoid hydration mismatch
254
+ const resolvedTheme: ResolvedTheme = useMemo(() => {
255
+ if (!mounted) {
256
+ // During SSR, return the initial theme if it's not "system", otherwise "light"
257
+ // The inline script will apply the correct class before hydration
258
+ if (initialTheme && initialTheme !== "system") {
259
+ return initialTheme as ResolvedTheme;
260
+ }
261
+ return "light";
262
+ }
263
+ if (theme === "system" && config.enableSystem) {
264
+ return systemTheme;
265
+ }
266
+ return theme as ResolvedTheme;
267
+ }, [theme, systemTheme, config.enableSystem, mounted, initialTheme]);
268
+
269
+ // Build themes list (include "system" if enabled)
270
+ const themes = useMemo(() => {
271
+ if (config.enableSystem) {
272
+ return ["system", ...config.themes.filter((t) => t !== "system")];
273
+ }
274
+ return config.themes;
275
+ }, [config.themes, config.enableSystem]);
276
+
277
+ // Context value
278
+ // During SSR (not mounted), return stable values to avoid hydration mismatch
279
+ const contextValue: ThemeContextValue = useMemo(
280
+ () => ({
281
+ theme,
282
+ setTheme,
283
+ resolvedTheme,
284
+ // Return stable "light" for systemTheme during SSR - actual value updates after mount
285
+ systemTheme: mounted ? systemTheme : "light",
286
+ themes,
287
+ config,
288
+ }),
289
+ [theme, setTheme, resolvedTheme, systemTheme, themes, config, mounted],
290
+ );
291
+
292
+ return (
293
+ <ThemeContext.Provider value={contextValue}>
294
+ {children}
295
+ </ThemeContext.Provider>
296
+ );
297
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * ThemeScript - Server component that renders an inline script for FOUC prevention.
3
+ *
4
+ * This component renders a blocking inline script that:
5
+ * 1. Reads theme from cookie/localStorage before paint
6
+ * 2. Applies the theme to the HTML element immediately
7
+ * 3. Prevents flash of unstyled content (FOUC)
8
+ *
9
+ * Must be placed in the <head> element of your document, before any stylesheets.
10
+ *
11
+ * @example
12
+ * ```tsx
13
+ * // In your document component
14
+ * import { ThemeScript } from "@rangojs/router/theme";
15
+ *
16
+ * export function Document({ children }) {
17
+ * return (
18
+ * <html lang="en" suppressHydrationWarning>
19
+ * <head>
20
+ * <ThemeScript />
21
+ * <MetaTags />
22
+ * </head>
23
+ * <body>{children}</body>
24
+ * </html>
25
+ * );
26
+ * }
27
+ * ```
28
+ */
29
+
30
+ import React from "react";
31
+ import { generateThemeScript } from "./theme-script.js";
32
+ import type { ResolvedThemeConfig } from "./types.js";
33
+
34
+ export interface ThemeScriptProps {
35
+ /**
36
+ * Theme configuration - passed from router.themeConfig
37
+ */
38
+ config: ResolvedThemeConfig;
39
+
40
+ /**
41
+ * Optional nonce for CSP
42
+ */
43
+ nonce?: string;
44
+ }
45
+
46
+ /**
47
+ * Server component that renders the theme initialization script.
48
+ *
49
+ * This renders a synchronous inline script that applies the theme
50
+ * to the HTML element before React hydration, preventing FOUC.
51
+ */
52
+ export function ThemeScript({
53
+ config,
54
+ nonce,
55
+ }: ThemeScriptProps): React.ReactNode {
56
+ const scriptContent = generateThemeScript(config);
57
+
58
+ return (
59
+ <script nonce={nonce} dangerouslySetInnerHTML={{ __html: scriptContent }} />
60
+ );
61
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Default values for theme configuration
3
+ */
4
+
5
+ import type { ResolvedThemeConfig, ThemeConfig } from "./types.js";
6
+
7
+ /**
8
+ * Default theme configuration values
9
+ */
10
+ export const THEME_DEFAULTS = {
11
+ defaultTheme: "system",
12
+ themes: ["light", "dark"],
13
+ attribute: "class",
14
+ storageKey: "theme",
15
+ enableSystem: true,
16
+ enableColorScheme: true,
17
+ } as const;
18
+
19
+ /**
20
+ * Cookie configuration for theme persistence
21
+ */
22
+ export const THEME_COOKIE: {
23
+ readonly maxAge: number;
24
+ readonly path: string;
25
+ readonly sameSite: "lax";
26
+ } = {
27
+ maxAge: 60 * 60 * 24 * 365, // 1 year
28
+ path: "/",
29
+ sameSite: "lax",
30
+ };
31
+
32
+ /**
33
+ * Resolve theme config by applying defaults.
34
+ * Accepts `true` to enable with all defaults, or a config object.
35
+ */
36
+ export function resolveThemeConfig(
37
+ config: ThemeConfig | true,
38
+ ): ResolvedThemeConfig {
39
+ // Handle `theme: true` shorthand
40
+ if (config === true) {
41
+ config = {};
42
+ }
43
+
44
+ const themes = config.themes ?? [...THEME_DEFAULTS.themes];
45
+
46
+ // Build value mapping - default to identity mapping
47
+ const value: Record<string, string> = {};
48
+ for (const theme of themes) {
49
+ value[theme] = config.value?.[theme] ?? theme;
50
+ }
51
+
52
+ return {
53
+ defaultTheme: config.defaultTheme ?? THEME_DEFAULTS.defaultTheme,
54
+ themes,
55
+ attribute: config.attribute ?? THEME_DEFAULTS.attribute,
56
+ storageKey: config.storageKey ?? THEME_DEFAULTS.storageKey,
57
+ enableSystem: config.enableSystem ?? THEME_DEFAULTS.enableSystem,
58
+ enableColorScheme:
59
+ config.enableColorScheme ?? THEME_DEFAULTS.enableColorScheme,
60
+ value,
61
+ };
62
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Theme module exports for @rangojs/router/theme
3
+ *
4
+ * This module provides the public theme API:
5
+ * - useTheme: Hook for accessing theme state in client components
6
+ * - ThemeProvider: Component for manual theme provider setup (typically not needed)
7
+ * - ThemeScript: FOUC-prevention script component for document/head usage
8
+ * - Types for theme configuration
9
+ *
10
+ * @example
11
+ * ```tsx
12
+ * // In a client component
13
+ * import { useTheme } from "@rangojs/router/theme";
14
+ *
15
+ * function ThemeToggle() {
16
+ * const { theme, setTheme, themes } = useTheme();
17
+ * return (
18
+ * <select value={theme} onChange={e => setTheme(e.target.value)}>
19
+ * {themes.map(t => <option key={t}>{t}</option>)}
20
+ * </select>
21
+ * );
22
+ * }
23
+ * ```
24
+ */
25
+
26
+ // Main hook for accessing theme
27
+ export { useTheme } from "./use-theme.js";
28
+
29
+ // Provider (typically auto-included via NavigationProvider when theme is enabled)
30
+ export { ThemeProvider } from "./ThemeProvider.js";
31
+
32
+ // Script component for FOUC prevention (use in document head)
33
+ export { ThemeScript, type ThemeScriptProps } from "./ThemeScript.js";
34
+
35
+ // Types
36
+ export type {
37
+ Theme,
38
+ ResolvedTheme,
39
+ ThemeAttribute,
40
+ ThemeConfig,
41
+ ResolvedThemeConfig,
42
+ UseThemeReturn,
43
+ ThemeProviderProps,
44
+ ThemeContextValue,
45
+ } from "./types.js";
46
+
47
+ // Constants
48
+ export { THEME_DEFAULTS, THEME_COOKIE } from "./constants.js";
@@ -0,0 +1,44 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Theme context for sharing theme state and configuration.
5
+ *
6
+ * Used by:
7
+ * - ThemeProvider to provide theme state
8
+ * - useTheme hook to access theme state
9
+ * - MetaTags to check if theme is enabled and render script
10
+ */
11
+
12
+ import { createContext, useContext, type Context } from "react";
13
+ import type { ThemeContextValue } from "./types.js";
14
+
15
+ /**
16
+ * React context for theme state
17
+ * null when theme is not enabled in router config
18
+ */
19
+ export const ThemeContext: Context<ThemeContextValue | null> =
20
+ createContext<ThemeContextValue | null>(null);
21
+
22
+ /**
23
+ * Get theme context (internal use)
24
+ * Returns null if theme is not enabled
25
+ */
26
+ export function useThemeContext(): ThemeContextValue | null {
27
+ return useContext(ThemeContext);
28
+ }
29
+
30
+ /**
31
+ * Get theme context, throwing if not available
32
+ * Use this in useTheme hook
33
+ */
34
+ export function requireThemeContext(): ThemeContextValue {
35
+ const ctx = useContext(ThemeContext);
36
+ if (!ctx) {
37
+ throw new Error(
38
+ "useTheme must be used within a ThemeProvider. " +
39
+ "Make sure theme is enabled in your router config: " +
40
+ "createRouter({ theme: { ... } })",
41
+ );
42
+ }
43
+ return ctx;
44
+ }
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Generates an inline script for FOUC prevention.
3
+ *
4
+ * This script runs synchronously before page paint to:
5
+ * 1. Read theme from cookie or localStorage
6
+ * 2. Detect system preference if theme is "system"
7
+ * 3. Apply theme to HTML element via class or data attribute
8
+ * 4. Optionally set color-scheme CSS property
9
+ *
10
+ * The script is minified and inlined in <head> before any other content.
11
+ */
12
+
13
+ import type { ResolvedThemeConfig } from "./types.js";
14
+
15
+ /**
16
+ * Generate the inline script for theme initialization
17
+ *
18
+ * The script is designed to:
19
+ * - Run synchronously before paint (blocking)
20
+ * - Be as small as possible to minimize blocking time
21
+ * - Work without any external dependencies
22
+ * - Handle all edge cases (no localStorage, no cookie, etc.)
23
+ */
24
+ export function generateThemeScript(config: ResolvedThemeConfig): string {
25
+ // Build the script as a string, then minify
26
+ const script = `
27
+ (function() {
28
+ var storageKey = ${JSON.stringify(config.storageKey)};
29
+ var defaultTheme = ${JSON.stringify(config.defaultTheme)};
30
+ var attribute = ${JSON.stringify(config.attribute)};
31
+ var enableSystem = ${config.enableSystem};
32
+ var enableColorScheme = ${config.enableColorScheme};
33
+ var valueMap = ${JSON.stringify(config.value)};
34
+ var themes = ${JSON.stringify(config.themes)};
35
+
36
+ // Read theme from cookie or localStorage
37
+ function getStoredTheme() {
38
+ // Try cookie first (for SSR consistency)
39
+ var cookies = document.cookie.split(';');
40
+ for (var i = 0; i < cookies.length; i++) {
41
+ var cookie = cookies[i].trim();
42
+ if (cookie.indexOf(storageKey + '=') === 0) {
43
+ try { return decodeURIComponent(cookie.substring(storageKey.length + 1)); }
44
+ catch (e) { return cookie.substring(storageKey.length + 1); }
45
+ }
46
+ }
47
+ // Fall back to localStorage
48
+ try {
49
+ return localStorage.getItem(storageKey);
50
+ } catch (e) {
51
+ return null;
52
+ }
53
+ }
54
+
55
+ // Get system preference
56
+ function getSystemTheme() {
57
+ if (typeof window !== 'undefined' && window.matchMedia) {
58
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
59
+ }
60
+ return 'light';
61
+ }
62
+
63
+ // Resolve "system" to actual theme
64
+ function resolveTheme(theme) {
65
+ if (theme === 'system' && enableSystem) {
66
+ return getSystemTheme();
67
+ }
68
+ return theme;
69
+ }
70
+
71
+ // Apply theme to HTML element
72
+ function applyTheme(theme) {
73
+ var resolved = resolveTheme(theme);
74
+ var value = valueMap[resolved] || resolved;
75
+ var el = document.documentElement;
76
+
77
+ // Apply attribute
78
+ if (attribute === 'class') {
79
+ // Remove all theme classes, then add current
80
+ for (var i = 0; i < themes.length; i++) {
81
+ var v = valueMap[themes[i]] || themes[i];
82
+ el.classList.remove(v);
83
+ }
84
+ el.classList.add(value);
85
+ } else {
86
+ el.setAttribute(attribute, value);
87
+ }
88
+
89
+ // Set color-scheme for native dark mode support
90
+ if (enableColorScheme) {
91
+ el.style.colorScheme = resolved;
92
+ }
93
+ }
94
+
95
+ // Get stored theme or use default
96
+ var stored = getStoredTheme();
97
+ var theme = stored && (stored === 'system' || themes.indexOf(stored) !== -1)
98
+ ? stored
99
+ : defaultTheme;
100
+
101
+ // Apply immediately
102
+ applyTheme(theme);
103
+
104
+ // Listen for system preference changes (for "system" theme)
105
+ if (enableSystem && typeof window !== 'undefined' && window.matchMedia) {
106
+ try {
107
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function() {
108
+ var current = getStoredTheme() || defaultTheme;
109
+ if (current === 'system') {
110
+ applyTheme('system');
111
+ }
112
+ });
113
+ } catch (e) {
114
+ // Older browsers may not support addEventListener on MediaQueryList
115
+ }
116
+ }
117
+ })();
118
+ `;
119
+
120
+ // Minify by removing comments, extra whitespace, and newlines
121
+ return minifyScript(script);
122
+ }
123
+
124
+ /**
125
+ * Basic script minification
126
+ * Removes comments, extra whitespace, and unnecessary newlines
127
+ */
128
+ function minifyScript(script: string): string {
129
+ return (
130
+ script
131
+ // Remove single-line comments
132
+ .replace(/\/\/.*$/gm, "")
133
+ // Remove multi-line comments
134
+ .replace(/\/\*[\s\S]*?\*\//g, "")
135
+ // Remove leading/trailing whitespace from lines
136
+ .split("\n")
137
+ .map((line) => line.trim())
138
+ .filter((line) => line.length > 0)
139
+ .join("")
140
+ // Collapse multiple spaces to single space
141
+ .replace(/\s+/g, " ")
142
+ // Remove spaces around operators and punctuation
143
+ .replace(/\s*([{};,=!<>()[\]+\-*/&|?:])\s*/g, "$1")
144
+ // Add back necessary spaces (e.g., "var x")
145
+ .replace(/(var|function|return|if|for|try|catch|typeof|else)\(/g, "$1 (")
146
+ .replace(/\)([a-zA-Z])/g, ") $1")
147
+ );
148
+ }
149
+
150
+ /**
151
+ * Generate nonce attribute string if nonce is provided
152
+ */
153
+ export function getNonceAttribute(nonce?: string): string {
154
+ return nonce ? ` nonce="${nonce}"` : "";
155
+ }