@usecross/docs 0.5.0 → 0.6.0

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.
@@ -1,13 +1,15 @@
1
1
  import { Head, Link, usePage } from '@inertiajs/react'
2
2
  import { useState } from 'react'
3
3
  import { Sidebar } from './Sidebar'
4
+ import { ThemeToggle } from './ThemeToggle'
5
+ import { useTheme } from './ThemeProvider'
4
6
  import type { DocsLayoutProps, SharedProps } from '../types'
5
7
 
6
8
  function MobileMenuButton({ onClick, isOpen }: { onClick: () => void; isOpen: boolean }) {
7
9
  return (
8
10
  <button
9
11
  onClick={onClick}
10
- className="inline-flex items-center justify-center p-2 -ml-2 text-black hover:text-primary-500 lg:hidden"
12
+ className="inline-flex items-center justify-center p-2 -ml-2 text-gray-700 hover:text-primary-500 dark:text-gray-300 dark:hover:text-primary-400 lg:hidden transition-colors"
11
13
  aria-expanded={isOpen}
12
14
  >
13
15
  <span className="sr-only">{isOpen ? 'Close menu' : 'Open menu'}</span>
@@ -54,6 +56,7 @@ export function DocsLayout({
54
56
  const sharedProps = usePage<{ props: SharedProps }>().props as unknown as SharedProps
55
57
  const { nav, currentPath } = sharedProps
56
58
  const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
59
+ const { resolvedTheme } = useTheme()
57
60
 
58
61
  // Merge props - component props take precedence over shared props from Python
59
62
  const logoUrl = propLogoUrl ?? sharedProps.logoUrl
@@ -68,18 +71,20 @@ export function DocsLayout({
68
71
  <img src={logoUrl} alt="Logo" className="h-8" />
69
72
  ) : null)
70
73
 
71
- // Determine which logo to display in footer (prefer footer-specific logo)
74
+ // Determine which logo to display in footer (theme-aware)
72
75
  const footerLogoUrl = sharedProps.footerLogoUrl || logoUrl
73
- const footerLogo = logo || (footerLogoUrl ? (
74
- <img src={footerLogoUrl} alt="Logo" className="h-6" />
76
+ const footerLogoInvertedUrl = sharedProps.footerLogoInvertedUrl || logoInvertedUrl
77
+ const currentFooterLogoUrl = resolvedTheme === 'dark' ? (footerLogoInvertedUrl || footerLogoUrl) : footerLogoUrl
78
+ const footerLogo = logo || (currentFooterLogoUrl ? (
79
+ <img src={currentFooterLogoUrl} alt="Logo" className="h-6" />
75
80
  ) : null)
76
81
 
77
82
  return (
78
- <div className="min-h-screen bg-white flex flex-col">
83
+ <div className="min-h-screen bg-white dark:bg-[#0f0f0f] flex flex-col transition-colors duration-200">
79
84
  <Head title={title} />
80
85
 
81
86
  {/* Fixed navigation */}
82
- <nav className="fixed w-full z-50 bg-white border-b border-gray-200">
87
+ <nav className="fixed w-full z-50 bg-white/95 dark:bg-[#0f0f0f]/95 backdrop-blur-sm border-b border-gray-200 dark:border-gray-800 transition-colors">
83
88
  <div className="px-4 lg:px-10">
84
89
  <div className="flex justify-between h-16 items-center">
85
90
  <div className="flex items-center gap-2">
@@ -89,17 +94,21 @@ export function DocsLayout({
89
94
  {headerLogo}
90
95
  </Link>
91
96
  ) : (
92
- <Link href="/" className="font-bold text-lg">
97
+ <Link href="/" className="font-bold text-lg text-gray-900 dark:text-white">
93
98
  Docs
94
99
  </Link>
95
100
  )}
96
101
  </div>
97
- <div className="flex items-center space-x-8">
102
+ <div className="flex items-center gap-6">
103
+ <div className="-mr-2">
104
+ <ThemeToggle size="sm" />
105
+ </div>
106
+
98
107
  {navLinks.map((link) => (
99
108
  <Link
100
109
  key={link.href}
101
110
  href={link.href}
102
- className="text-black font-medium hover:text-primary-500 transition-colors"
111
+ className="hidden sm:block text-gray-700 dark:text-gray-300 font-medium hover:text-primary-600 dark:hover:text-primary-400 transition-colors"
103
112
  >
104
113
  {link.label}
105
114
  </Link>
@@ -109,7 +118,7 @@ export function DocsLayout({
109
118
  href={githubUrl}
110
119
  target="_blank"
111
120
  rel="noopener noreferrer"
112
- className="text-black hover:text-primary-500 transition-colors"
121
+ className="text-gray-700 dark:text-gray-300 hover:text-primary-600 dark:hover:text-primary-400 transition-colors"
113
122
  >
114
123
  <GitHubIcon />
115
124
  </a>
@@ -122,18 +131,18 @@ export function DocsLayout({
122
131
  {/* Mobile sidebar */}
123
132
  {mobileMenuOpen && (
124
133
  <div className="fixed inset-0 z-40 lg:hidden">
125
- <div className="fixed inset-0 bg-black/50" onClick={() => setMobileMenuOpen(false)} />
126
- <div className="fixed inset-y-0 left-0 w-72 overflow-y-auto bg-white px-4 lg:px-10 py-6 pt-20 border-r border-gray-200">
134
+ <div className="fixed inset-0 bg-black/50 dark:bg-black/70" onClick={() => setMobileMenuOpen(false)} />
135
+ <div className="fixed inset-y-0 left-0 w-72 overflow-y-auto bg-white dark:bg-[#0f0f0f] px-4 lg:px-10 py-6 pt-20 border-r border-gray-200 dark:border-gray-800 transition-colors">
127
136
  <Sidebar nav={nav} currentPath={currentPath} />
128
137
  </div>
129
138
  </div>
130
139
  )}
131
140
 
132
141
  {/* Main content area */}
133
- <div className="bg-white pt-16 w-full flex-1">
142
+ <div className="bg-white dark:bg-[#0f0f0f] pt-16 w-full flex-1 transition-colors">
134
143
  <div className="grid grid-cols-12">
135
144
  {/* Desktop sidebar */}
136
- <aside className="hidden lg:block lg:col-span-3 xl:col-span-2 border-r border-gray-200 min-h-[calc(100vh-4rem)]">
145
+ <aside className="hidden lg:block lg:col-span-3 xl:col-span-2 border-r border-gray-200 dark:border-gray-800 min-h-[calc(100vh-4rem)] transition-colors">
137
146
  <nav className="sticky top-16 px-4 lg:px-10 py-6 max-h-[calc(100vh-4rem)] overflow-y-auto">
138
147
  <Sidebar nav={nav} currentPath={currentPath} />
139
148
  </nav>
@@ -141,7 +150,7 @@ export function DocsLayout({
141
150
 
142
151
  {/* Main content */}
143
152
  <main className="col-span-12 lg:col-span-9 xl:col-span-10 p-4 lg:px-10 lg:py-6">
144
- <article className="prose prose-lg max-w-3xl prose-headings:font-bold prose-headings:tracking-tight prose-h1:text-3xl prose-h1:mb-4 prose-h2:text-2xl prose-h2:mt-10 first:prose-h2:mt-0 prose-h3:text-xl prose-a:text-primary-600 prose-a:no-underline hover:prose-a:underline prose-code:bg-gray-100 prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded prose-code:before:content-none prose-code:after:content-none">
153
+ <article className="prose prose-lg max-w-3xl prose-headings:font-bold prose-headings:tracking-tight prose-h1:text-3xl prose-h1:mb-4 prose-h2:text-2xl prose-h2:mt-10 first:prose-h2:mt-0 prose-h3:text-xl prose-a:text-primary-600 dark:prose-a:text-primary-400 prose-a:no-underline hover:prose-a:underline prose-code:bg-gray-100 dark:prose-code:bg-gray-800 prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded prose-code:before:content-none prose-code:after:content-none dark:prose-headings:text-white dark:prose-strong:text-white dark:text-gray-300">
145
154
  {children}
146
155
  </article>
147
156
  </main>
@@ -150,12 +159,12 @@ export function DocsLayout({
150
159
 
151
160
  {/* Footer */}
152
161
  {footer || (
153
- <footer className="border-t border-gray-200 py-8">
162
+ <footer className="border-t border-gray-200 dark:border-gray-800 py-8 bg-white dark:bg-[#0f0f0f] transition-colors">
154
163
  <div className="px-4 lg:px-10 flex flex-col md:flex-row justify-between items-center gap-6">
155
164
  {footerLogo && <Link href="/">{footerLogo}</Link>}
156
- <div className="flex gap-8 text-sm text-gray-600">
165
+ <div className="flex gap-8 text-sm text-gray-600 dark:text-gray-400">
157
166
  {navLinks.map((link) => (
158
- <Link key={link.href} href={link.href} className="hover:text-black transition-colors">
167
+ <Link key={link.href} href={link.href} className="hover:text-black dark:hover:text-white transition-colors">
159
168
  {link.label}
160
169
  </Link>
161
170
  ))}
@@ -164,7 +173,7 @@ export function DocsLayout({
164
173
  href={githubUrl}
165
174
  target="_blank"
166
175
  rel="noopener noreferrer"
167
- className="hover:text-black transition-colors"
176
+ className="hover:text-black dark:hover:text-white transition-colors"
168
177
  >
169
178
  GitHub
170
179
  </a>
@@ -1,5 +1,7 @@
1
1
  import { Head, Link } from '@inertiajs/react'
2
2
  import { createContext, useContext, useState, type ReactNode } from 'react'
3
+ import { ThemeToggle } from './ThemeToggle'
4
+ import { useTheme } from './ThemeProvider'
3
5
 
4
6
  // ============================================================================
5
7
  // Types
@@ -21,6 +23,7 @@ export interface HomePageContextValue {
21
23
  logoUrl?: string
22
24
  heroLogoUrl?: string
23
25
  footerLogoUrl?: string
26
+ footerLogoInvertedUrl?: string
24
27
  githubUrl?: string
25
28
  navLinks: Array<{ label: string; href: string }>
26
29
  }
@@ -78,9 +81,9 @@ function InstallCommand({ command }: { command: string }) {
78
81
  return (
79
82
  <button
80
83
  onClick={copyToClipboard}
81
- className="group relative flex items-center bg-black border border-black px-4 h-14 font-mono text-sm text-white hover:bg-white hover:text-black transition-colors cursor-pointer"
84
+ className="group relative flex items-center bg-gray-900 dark:bg-white border border-gray-900 dark:border-white px-4 h-14 font-mono text-sm text-white dark:text-gray-900 hover:bg-white dark:hover:bg-gray-900 hover:text-gray-900 dark:hover:text-white transition-colors cursor-pointer"
82
85
  >
83
- <span className="text-primary-500 mr-2">$</span>
86
+ <span className="text-primary-500 dark:text-primary-600 mr-2">$</span>
84
87
  <span>{command}</span>
85
88
  <svg
86
89
  className={`ml-4 w-4 h-4 transition ${copied ? 'text-green-400' : 'opacity-50 group-hover:opacity-100'}`}
@@ -96,7 +99,7 @@ function InstallCommand({ command }: { command: string }) {
96
99
  />
97
100
  </svg>
98
101
  <span
99
- className={`absolute -top-8 left-1/2 -translate-x-1/2 bg-black text-white text-xs py-1 px-2 rounded transition-opacity duration-300 whitespace-nowrap ${
102
+ className={`absolute -top-8 left-1/2 -translate-x-1/2 bg-gray-900 dark:bg-white text-white dark:text-gray-900 text-xs py-1 px-2 rounded transition-opacity duration-300 whitespace-nowrap ${
100
103
  copied ? 'opacity-100' : 'opacity-0'
101
104
  }`}
102
105
  >
@@ -137,7 +140,7 @@ function DefaultLogo() {
137
140
  }
138
141
 
139
142
  return (
140
- <Link href="/" className="font-bold text-lg">
143
+ <Link href="/" className="font-bold text-lg text-gray-900 dark:text-white">
141
144
  {title}
142
145
  </Link>
143
146
  )
@@ -151,16 +154,20 @@ export function HomeHeader({ renderLogo }: HomeHeaderProps = {}) {
151
154
  const { navLinks, githubUrl } = useHomePage()
152
155
 
153
156
  return (
154
- <nav className="fixed w-full z-50 bg-white border-b border-gray-200">
157
+ <nav className="fixed w-full z-50 bg-white/95 dark:bg-[#0f0f0f]/95 backdrop-blur-sm border-b border-gray-200 dark:border-gray-800 transition-colors">
155
158
  <div className="px-4 lg:px-10">
156
159
  <div className="flex justify-between h-16 items-center">
157
160
  {renderLogo ? renderLogo() : <DefaultLogo />}
158
- <div className="flex items-center space-x-8">
161
+ <div className="flex items-center gap-6">
162
+ <div className="-mr-2">
163
+ <ThemeToggle size="sm" />
164
+ </div>
165
+
159
166
  {navLinks.map((link) => (
160
167
  <Link
161
168
  key={link.href}
162
169
  href={link.href}
163
- className="text-black font-medium hover:text-primary-500 transition-colors"
170
+ className="hidden sm:block text-gray-700 dark:text-gray-300 font-medium hover:text-primary-600 dark:hover:text-primary-400 transition-colors"
164
171
  >
165
172
  {link.label}
166
173
  </Link>
@@ -170,7 +177,7 @@ export function HomeHeader({ renderLogo }: HomeHeaderProps = {}) {
170
177
  href={githubUrl}
171
178
  target="_blank"
172
179
  rel="noopener noreferrer"
173
- className="text-black hover:text-primary-500 transition-colors"
180
+ className="text-gray-700 dark:text-gray-300 hover:text-primary-600 dark:hover:text-primary-400 transition-colors"
174
181
  >
175
182
  <GitHubIcon />
176
183
  </a>
@@ -193,7 +200,7 @@ export function HomeHero() {
193
200
  <section className="pt-16">
194
201
  <div className="px-4 lg:px-10 py-16 lg:py-24">
195
202
  <div className="max-w-4xl">
196
- <div className="mb-4 text-sm font-mono uppercase tracking-widest text-gray-500">
203
+ <div className="mb-4 text-sm font-mono uppercase tracking-widest text-gray-500 dark:text-gray-400">
197
204
  {tagline}
198
205
  </div>
199
206
  {heroLogoUrl ? (
@@ -205,18 +212,18 @@ export function HomeHero() {
205
212
  />
206
213
  </h1>
207
214
  ) : (
208
- <h1 className="text-5xl lg:text-7xl font-bold tracking-tight mb-6">
215
+ <h1 className="text-5xl lg:text-7xl font-bold tracking-tight mb-6 text-gray-900 dark:text-white">
209
216
  {title}
210
217
  </h1>
211
218
  )}
212
- <p className="text-xl lg:text-2xl text-gray-700 max-w-2xl leading-relaxed mb-8">
219
+ <p className="text-xl lg:text-2xl text-gray-600 dark:text-gray-300 max-w-2xl leading-relaxed mb-8">
213
220
  {description}
214
221
  </p>
215
222
 
216
223
  <div className="flex flex-col sm:flex-row gap-3">
217
224
  <Link
218
225
  href={ctaHref}
219
- className="inline-flex items-center justify-center px-8 h-14 bg-black text-white font-bold text-lg hover:bg-primary-500 transition-colors border border-black"
226
+ className="inline-flex items-center justify-center px-8 h-14 bg-gray-900 dark:bg-white text-white dark:text-gray-900 font-bold text-lg hover:bg-primary-500 dark:hover:bg-primary-500 dark:hover:text-white transition-colors border border-gray-900 dark:border-white hover:border-primary-500 dark:hover:border-primary-500"
220
227
  >
221
228
  {ctaText}
222
229
  </Link>
@@ -234,17 +241,17 @@ export function HomeHero() {
234
241
  export function HomeFeatureItem({ feature, index, totalFeatures }: HomeFeatureItemProps) {
235
242
  return (
236
243
  <div
237
- className={`p-4 lg:p-10 border-b sm:border-b border-gray-200 ${
244
+ className={`p-4 lg:p-10 border-b sm:border-b border-gray-200 dark:border-gray-800 ${
238
245
  index % 2 === 0 ? 'sm:border-r' : ''
239
246
  } ${index >= totalFeatures - 2 ? 'sm:border-b-0' : ''} ${
240
247
  index === totalFeatures - 1 && totalFeatures % 2 === 1 ? 'border-b-0' : ''
241
- }`}
248
+ } transition-colors`}
242
249
  >
243
- <div className="text-5xl font-bold text-primary-500 mb-4">
250
+ <div className="text-5xl font-bold text-primary-500 dark:text-primary-400 mb-4">
244
251
  {String(index + 1).padStart(2, '0')}
245
252
  </div>
246
- <h3 className="text-xl font-bold mb-2">{feature.title}</h3>
247
- <p className="text-gray-600">{feature.description}</p>
253
+ <h3 className="text-xl font-bold mb-2 text-gray-900 dark:text-white">{feature.title}</h3>
254
+ <p className="text-gray-600 dark:text-gray-300">{feature.description}</p>
248
255
  </div>
249
256
  )
250
257
  }
@@ -260,13 +267,13 @@ export function HomeFeatures({ renderFeature }: HomeFeaturesProps = {}) {
260
267
  }
261
268
 
262
269
  return (
263
- <section className="border-t border-gray-200">
270
+ <section className="border-t border-gray-200 dark:border-gray-800 transition-colors">
264
271
  <div className="grid grid-cols-12">
265
- <div className="col-span-12 lg:col-span-4 p-4 lg:p-10 border-b lg:border-b-0 lg:border-r border-gray-200">
266
- <div className="text-sm font-mono uppercase tracking-widest text-gray-500 mb-4">
272
+ <div className="col-span-12 lg:col-span-4 p-4 lg:p-10 border-b lg:border-b-0 lg:border-r border-gray-200 dark:border-gray-800 transition-colors">
273
+ <div className="text-sm font-mono uppercase tracking-widest text-gray-500 dark:text-gray-400 mb-4">
267
274
  Features
268
275
  </div>
269
- <h2 className="text-4xl lg:text-5xl font-bold tracking-tight">
276
+ <h2 className="text-4xl lg:text-5xl font-bold tracking-tight text-gray-900 dark:text-white">
270
277
  Why {title}?
271
278
  </h2>
272
279
  </div>
@@ -299,27 +306,27 @@ export function HomeCTA() {
299
306
  const { ctaHref } = useHomePage()
300
307
 
301
308
  return (
302
- <section className="border-t border-gray-200">
309
+ <section className="border-t border-gray-200 dark:border-gray-800 transition-colors">
303
310
  <div className="grid grid-cols-12 items-center">
304
311
  <div className="col-span-12 lg:col-span-8 p-4 lg:p-10">
305
- <h2 className="text-4xl lg:text-6xl font-bold tracking-tight mb-4">
312
+ <h2 className="text-4xl lg:text-6xl font-bold tracking-tight mb-4 text-gray-900 dark:text-white">
306
313
  Ready to start?
307
314
  </h2>
308
- <p className="text-xl text-gray-600 mb-8 max-w-2xl">
315
+ <p className="text-xl text-gray-600 dark:text-gray-300 mb-8 max-w-2xl">
309
316
  Get up and running in minutes. Check out our documentation to learn more.
310
317
  </p>
311
318
  <Link
312
319
  href={ctaHref}
313
- className="inline-flex items-center justify-center px-8 py-4 bg-primary-500 text-white font-bold text-lg hover:bg-black transition-colors border border-primary-500 hover:border-black"
320
+ className="inline-flex items-center justify-center px-8 py-4 bg-primary-500 text-white font-bold text-lg hover:bg-gray-900 dark:hover:bg-white dark:hover:text-gray-900 transition-colors border border-primary-500 hover:border-gray-900 dark:hover:border-white"
314
321
  >
315
322
  Read the Docs
316
323
  </Link>
317
324
  </div>
318
325
  <Link
319
326
  href={ctaHref}
320
- className="col-span-12 lg:col-span-4 h-full bg-primary-500 hidden lg:flex items-center justify-center p-4 lg:p-10 hover:bg-black transition-colors min-h-[200px]"
327
+ className="col-span-12 lg:col-span-4 h-full bg-primary-500 hidden lg:flex items-center justify-center p-4 lg:p-10 hover:bg-gray-900 dark:hover:bg-white transition-colors min-h-[200px] group"
321
328
  >
322
- <div className="text-white text-8xl font-bold">&rarr;</div>
329
+ <div className="text-white group-hover:text-white dark:group-hover:text-gray-900 text-8xl font-bold transition-colors">&rarr;</div>
323
330
  </Link>
324
331
  </div>
325
332
  </section>
@@ -330,19 +337,25 @@ export function HomeCTA() {
330
337
  * Footer section.
331
338
  */
332
339
  export function HomeFooter() {
333
- const { title, logoUrl, footerLogoUrl, navLinks, githubUrl } = useHomePage()
340
+ const { title, logoUrl, footerLogoUrl, footerLogoInvertedUrl, navLinks, githubUrl } = useHomePage()
341
+ const { resolvedTheme } = useTheme()
342
+
343
+ // Select appropriate logo based on theme
344
+ const currentLogoUrl = resolvedTheme === 'dark'
345
+ ? (footerLogoInvertedUrl || footerLogoUrl || logoUrl)
346
+ : (footerLogoUrl || logoUrl)
334
347
 
335
348
  return (
336
- <footer className="border-t border-gray-200 py-8">
349
+ <footer className="border-t border-gray-200 dark:border-gray-800 py-8 bg-white dark:bg-[#0f0f0f] transition-colors">
337
350
  <div className="px-4 lg:px-10 flex flex-col md:flex-row justify-between items-center gap-6">
338
- {(footerLogoUrl || logoUrl) && (
351
+ {currentLogoUrl && (
339
352
  <Link href="/">
340
- <img src={footerLogoUrl || logoUrl} alt={title} className="h-6" />
353
+ <img src={currentLogoUrl} alt={title} className="h-6" />
341
354
  </Link>
342
355
  )}
343
- <div className="flex gap-8 text-sm text-gray-600">
356
+ <div className="flex gap-8 text-sm text-gray-600 dark:text-gray-300">
344
357
  {navLinks.map((link) => (
345
- <Link key={link.href} href={link.href} className="hover:text-black transition-colors">
358
+ <Link key={link.href} href={link.href} className="hover:text-black dark:hover:text-white transition-colors">
346
359
  {link.label}
347
360
  </Link>
348
361
  ))}
@@ -351,7 +364,7 @@ export function HomeFooter() {
351
364
  href={githubUrl}
352
365
  target="_blank"
353
366
  rel="noopener noreferrer"
354
- className="hover:text-black transition-colors"
367
+ className="hover:text-black dark:hover:text-white transition-colors"
355
368
  >
356
369
  GitHub
357
370
  </a>
@@ -369,12 +382,21 @@ export function HomeFooter() {
369
382
  function HomeSpacer() {
370
383
  return (
371
384
  <div
372
- className="grow"
385
+ className="grow dark:hidden"
373
386
  style={{ backgroundImage: 'linear-gradient(rgb(229, 231, 235) 1px, transparent 1px)' }}
374
387
  />
375
388
  )
376
389
  }
377
390
 
391
+ function HomeSpacerDark() {
392
+ return (
393
+ <div
394
+ className="grow hidden dark:block"
395
+ style={{ backgroundImage: 'linear-gradient(rgb(38, 38, 38) 1px, transparent 1px)' }}
396
+ />
397
+ )
398
+ }
399
+
378
400
  function DefaultHomeLayout() {
379
401
  return (
380
402
  <>
@@ -383,6 +405,7 @@ function DefaultHomeLayout() {
383
405
  <HomeFeatures />
384
406
  <HomeCTA />
385
407
  <HomeSpacer />
408
+ <HomeSpacerDark />
386
409
  <HomeFooter />
387
410
  </>
388
411
  )
@@ -428,7 +451,7 @@ export function HomePage({
428
451
 
429
452
  return (
430
453
  <HomePageContext.Provider value={contextValue}>
431
- <div className="min-h-screen bg-white flex flex-col">
454
+ <div className="min-h-screen bg-white dark:bg-[#0f0f0f] flex flex-col transition-colors duration-200">
432
455
  <Head title={props.title} />
433
456
  {children || <DefaultHomeLayout />}
434
457
  </div>
@@ -10,10 +10,10 @@ export function Sidebar({ nav, currentPath, className }: SidebarProps) {
10
10
  <nav className={cn('space-y-8', className)}>
11
11
  {nav.map((section) => (
12
12
  <div key={section.title}>
13
- <h3 className="mb-3 text-xs font-mono uppercase tracking-widest text-gray-500">
13
+ <h3 className="mb-3 text-xs font-mono uppercase tracking-widest text-gray-500 dark:text-gray-400">
14
14
  {section.title}
15
15
  </h3>
16
- <ul className="space-y-1 border-l-2 border-gray-200">
16
+ <ul className="space-y-1 border-l-2 border-gray-200 dark:border-gray-700">
17
17
  {section.items.map((item) => (
18
18
  <li key={item.href}>
19
19
  <Link
@@ -21,8 +21,8 @@ export function Sidebar({ nav, currentPath, className }: SidebarProps) {
21
21
  className={cn(
22
22
  'block border-l-2 py-1.5 pl-4 text-sm transition-colors -ml-0.5',
23
23
  currentPath === item.href
24
- ? 'border-primary-500 text-black font-bold'
25
- : 'border-transparent text-gray-600 hover:border-black hover:text-black'
24
+ ? 'border-primary-500 text-gray-900 dark:text-white font-bold'
25
+ : 'border-transparent text-gray-600 dark:text-gray-300 hover:border-gray-900 dark:hover:border-white hover:text-gray-900 dark:hover:text-white'
26
26
  )}
27
27
  >
28
28
  {item.title}
@@ -0,0 +1,125 @@
1
+ import { createContext, useContext, useEffect, useState, type ReactNode } from 'react'
2
+
3
+ export type Theme = 'light' | 'dark' | 'system'
4
+ export type ResolvedTheme = 'light' | 'dark'
5
+
6
+ interface ThemeContextValue {
7
+ theme: Theme
8
+ resolvedTheme: ResolvedTheme
9
+ setTheme: (theme: Theme) => void
10
+ }
11
+
12
+ const ThemeContext = createContext<ThemeContextValue | null>(null)
13
+
14
+ const STORAGE_KEY = 'cross-docs-theme'
15
+
16
+ function getSystemTheme(): ResolvedTheme {
17
+ if (typeof window === 'undefined') return 'light'
18
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
19
+ }
20
+
21
+ function getStoredTheme(): Theme | null {
22
+ if (typeof window === 'undefined') return null
23
+ const stored = localStorage.getItem(STORAGE_KEY)
24
+ if (stored === 'light' || stored === 'dark' || stored === 'system') {
25
+ return stored
26
+ }
27
+ return null
28
+ }
29
+
30
+ interface ThemeProviderProps {
31
+ children: ReactNode
32
+ /** Default theme if no preference is stored. Defaults to 'system'. */
33
+ defaultTheme?: Theme
34
+ /** Force a specific theme, ignoring user preference */
35
+ forcedTheme?: ResolvedTheme
36
+ }
37
+
38
+ export function ThemeProvider({
39
+ children,
40
+ defaultTheme = 'system',
41
+ forcedTheme,
42
+ }: ThemeProviderProps) {
43
+ const [theme, setThemeState] = useState<Theme>(() => {
44
+ // During SSR, use defaultTheme
45
+ if (typeof window === 'undefined') return defaultTheme
46
+ return getStoredTheme() ?? defaultTheme
47
+ })
48
+
49
+ const [resolvedTheme, setResolvedTheme] = useState<ResolvedTheme>(() => {
50
+ if (forcedTheme) return forcedTheme
51
+ if (typeof window === 'undefined') return 'light'
52
+ if (theme === 'system') return getSystemTheme()
53
+ return theme
54
+ })
55
+
56
+ // Update resolved theme when theme changes or system preference changes
57
+ useEffect(() => {
58
+ if (forcedTheme) {
59
+ setResolvedTheme(forcedTheme)
60
+ return
61
+ }
62
+
63
+ const updateResolvedTheme = () => {
64
+ if (theme === 'system') {
65
+ setResolvedTheme(getSystemTheme())
66
+ } else {
67
+ setResolvedTheme(theme)
68
+ }
69
+ }
70
+
71
+ updateResolvedTheme()
72
+
73
+ // Listen for system preference changes
74
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
75
+ const handleChange = () => {
76
+ if (theme === 'system') {
77
+ setResolvedTheme(getSystemTheme())
78
+ }
79
+ }
80
+
81
+ mediaQuery.addEventListener('change', handleChange)
82
+ return () => mediaQuery.removeEventListener('change', handleChange)
83
+ }, [theme, forcedTheme])
84
+
85
+ // Apply theme class to document
86
+ useEffect(() => {
87
+ const root = document.documentElement
88
+ root.classList.remove('light', 'dark')
89
+ root.classList.add(resolvedTheme)
90
+ }, [resolvedTheme])
91
+
92
+ const setTheme = (newTheme: Theme) => {
93
+ setThemeState(newTheme)
94
+ localStorage.setItem(STORAGE_KEY, newTheme)
95
+ }
96
+
97
+ return (
98
+ <ThemeContext.Provider value={{ theme, resolvedTheme, setTheme }}>
99
+ {children}
100
+ </ThemeContext.Provider>
101
+ )
102
+ }
103
+
104
+ export function useTheme(): ThemeContextValue {
105
+ const context = useContext(ThemeContext)
106
+ if (!context) {
107
+ throw new Error('useTheme must be used within a ThemeProvider')
108
+ }
109
+ return context
110
+ }
111
+
112
+ /**
113
+ * Script to prevent flash of unstyled content (FOUC) during page load.
114
+ * Include this in your HTML <head> before any stylesheets.
115
+ */
116
+ export const themeInitScript = `
117
+ (function() {
118
+ try {
119
+ var stored = localStorage.getItem('${STORAGE_KEY}');
120
+ var theme = stored === 'light' || stored === 'dark' ? stored :
121
+ (stored === 'system' || !stored) && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
122
+ document.documentElement.classList.add(theme);
123
+ } catch (e) {}
124
+ })();
125
+ `.trim()