@usecross/docs 0.5.0 → 0.7.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.
- package/dist/index.d.ts +51 -4
- package/dist/index.js +592 -145
- package/dist/index.js.map +1 -1
- package/dist/ssr.d.ts +1 -1
- package/dist/ssr.js +79 -1
- package/dist/ssr.js.map +1 -1
- package/dist/{types-CCdOzu28.d.ts → types-DlF8TX2Q.d.ts} +22 -1
- package/package.json +1 -1
- package/src/app.tsx +6 -3
- package/src/components/DocSetSelector.tsx +239 -0
- package/src/components/DocsLayout.tsx +35 -26
- package/src/components/HomePage.tsx +59 -36
- package/src/components/Sidebar.tsx +16 -6
- package/src/components/ThemeProvider.tsx +125 -0
- package/src/components/ThemeToggle.tsx +188 -0
- package/src/components/index.ts +3 -0
- package/src/index.ts +8 -0
- package/src/ssr.tsx +6 -1
- package/src/styles.css +64 -0
- package/src/types.ts +22 -0
|
@@ -1,19 +1,28 @@
|
|
|
1
1
|
import { Link } from '@inertiajs/react'
|
|
2
2
|
import { cn } from '../lib/utils'
|
|
3
|
+
import { DocSetSelector } from './DocSetSelector'
|
|
3
4
|
import type { SidebarProps } from '../types'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Documentation sidebar with section-based navigation.
|
|
8
|
+
* In multi-docs mode, includes a dropdown selector at the top.
|
|
7
9
|
*/
|
|
8
|
-
export function Sidebar({ nav, currentPath, className }: SidebarProps) {
|
|
10
|
+
export function Sidebar({ nav, currentPath, className, docSets, currentDocSet }: SidebarProps) {
|
|
9
11
|
return (
|
|
10
|
-
<nav className={cn('space-y-
|
|
12
|
+
<nav className={cn('space-y-6', className)}>
|
|
13
|
+
{/* Doc Set Selector - only shown in multi-docs mode */}
|
|
14
|
+
{docSets && docSets.length > 1 && (
|
|
15
|
+
<DocSetSelector docSets={docSets} currentDocSet={currentDocSet ?? ''} className="mb-6" />
|
|
16
|
+
)}
|
|
17
|
+
|
|
18
|
+
{/* Navigation Sections */}
|
|
19
|
+
<div className="space-y-8">
|
|
11
20
|
{nav.map((section) => (
|
|
12
21
|
<div key={section.title}>
|
|
13
|
-
<h3 className="mb-3 text-xs font-mono uppercase tracking-widest text-gray-500">
|
|
22
|
+
<h3 className="mb-3 text-xs font-mono uppercase tracking-widest text-gray-500 dark:text-gray-400">
|
|
14
23
|
{section.title}
|
|
15
24
|
</h3>
|
|
16
|
-
<ul className="space-y-1 border-l-2 border-gray-200">
|
|
25
|
+
<ul className="space-y-1 border-l-2 border-gray-200 dark:border-gray-700">
|
|
17
26
|
{section.items.map((item) => (
|
|
18
27
|
<li key={item.href}>
|
|
19
28
|
<Link
|
|
@@ -21,8 +30,8 @@ export function Sidebar({ nav, currentPath, className }: SidebarProps) {
|
|
|
21
30
|
className={cn(
|
|
22
31
|
'block border-l-2 py-1.5 pl-4 text-sm transition-colors -ml-0.5',
|
|
23
32
|
currentPath === item.href
|
|
24
|
-
? 'border-primary-500 text-
|
|
25
|
-
: 'border-transparent text-gray-600 hover:border-
|
|
33
|
+
? 'border-primary-500 text-gray-900 dark:text-white font-bold'
|
|
34
|
+
: '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
35
|
)}
|
|
27
36
|
>
|
|
28
37
|
{item.title}
|
|
@@ -32,6 +41,7 @@ export function Sidebar({ nav, currentPath, className }: SidebarProps) {
|
|
|
32
41
|
</ul>
|
|
33
42
|
</div>
|
|
34
43
|
))}
|
|
44
|
+
</div>
|
|
35
45
|
</nav>
|
|
36
46
|
)
|
|
37
47
|
}
|
|
@@ -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()
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { useState, useRef, useEffect } from 'react'
|
|
2
|
+
import { useTheme, type Theme } from './ThemeProvider'
|
|
3
|
+
import { cn } from '../lib/utils'
|
|
4
|
+
|
|
5
|
+
interface ThemeToggleProps {
|
|
6
|
+
/** Additional CSS classes */
|
|
7
|
+
className?: string
|
|
8
|
+
/** Size variant */
|
|
9
|
+
size?: 'sm' | 'md' | 'lg'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Refined sun icon with balanced proportions
|
|
13
|
+
const SunIcon = ({ className }: { className?: string }) => (
|
|
14
|
+
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
15
|
+
<circle cx="12" cy="12" r="4" stroke="currentColor" strokeWidth="1.5" />
|
|
16
|
+
<path d="M12 5V3M12 21v-2M5 12H3m18 0h-2M7.05 7.05 5.636 5.636m12.728 12.728L16.95 16.95M7.05 16.95l-1.414 1.414M18.364 5.636 16.95 7.05" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
|
17
|
+
</svg>
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
// Refined moon icon - elegant crescent
|
|
21
|
+
const MoonIcon = ({ className }: { className?: string }) => (
|
|
22
|
+
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
23
|
+
<path d="M21.752 15.002A9.718 9.718 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
24
|
+
</svg>
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
// Refined monitor icon - clean display shape
|
|
28
|
+
const MonitorIcon = ({ className }: { className?: string }) => (
|
|
29
|
+
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
30
|
+
<rect x="2" y="3" width="20" height="14" rx="2" stroke="currentColor" strokeWidth="1.5" />
|
|
31
|
+
<path d="M8 21h8m-4-4v4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
|
32
|
+
</svg>
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
const themeOptions: { value: Theme; label: string; icon: typeof SunIcon }[] = [
|
|
36
|
+
{ value: 'light', label: 'Light', icon: SunIcon },
|
|
37
|
+
{ value: 'dark', label: 'Dark', icon: MoonIcon },
|
|
38
|
+
{ value: 'system', label: 'System', icon: MonitorIcon },
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Theme toggle dropdown with Light, Dark, and System options.
|
|
43
|
+
* Refined design with smooth animations and premium feel.
|
|
44
|
+
*/
|
|
45
|
+
export function ThemeToggle({ className, size = 'md' }: ThemeToggleProps) {
|
|
46
|
+
const { theme, resolvedTheme, setTheme } = useTheme()
|
|
47
|
+
const [isOpen, setIsOpen] = useState(false)
|
|
48
|
+
const dropdownRef = useRef<HTMLDivElement>(null)
|
|
49
|
+
|
|
50
|
+
// Close dropdown when clicking outside
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
53
|
+
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
|
54
|
+
setIsOpen(false)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (isOpen) {
|
|
59
|
+
document.addEventListener('mousedown', handleClickOutside)
|
|
60
|
+
return () => document.removeEventListener('mousedown', handleClickOutside)
|
|
61
|
+
}
|
|
62
|
+
}, [isOpen])
|
|
63
|
+
|
|
64
|
+
// Close on escape key
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
const handleEscape = (event: KeyboardEvent) => {
|
|
67
|
+
if (event.key === 'Escape') setIsOpen(false)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (isOpen) {
|
|
71
|
+
document.addEventListener('keydown', handleEscape)
|
|
72
|
+
return () => document.removeEventListener('keydown', handleEscape)
|
|
73
|
+
}
|
|
74
|
+
}, [isOpen])
|
|
75
|
+
|
|
76
|
+
const iconSizes = {
|
|
77
|
+
sm: 'w-[18px] h-[18px]',
|
|
78
|
+
md: 'w-5 h-5',
|
|
79
|
+
lg: 'w-[22px] h-[22px]',
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<div className="relative" ref={dropdownRef}>
|
|
84
|
+
{/* Toggle Button */}
|
|
85
|
+
<button
|
|
86
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
87
|
+
className={cn(
|
|
88
|
+
'relative inline-flex items-center justify-center',
|
|
89
|
+
'rounded-full p-4',
|
|
90
|
+
'text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white',
|
|
91
|
+
'hover:bg-gray-100 dark:hover:bg-white/10',
|
|
92
|
+
'transition-all duration-200 ease-out',
|
|
93
|
+
'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/50 focus-visible:ring-offset-2 dark:focus-visible:ring-offset-[#0f0f0f]',
|
|
94
|
+
iconSizes[size],
|
|
95
|
+
className
|
|
96
|
+
)}
|
|
97
|
+
aria-label="Toggle theme"
|
|
98
|
+
aria-expanded={isOpen}
|
|
99
|
+
aria-haspopup="listbox"
|
|
100
|
+
>
|
|
101
|
+
{/* Sun icon - visible in light mode */}
|
|
102
|
+
<SunIcon
|
|
103
|
+
className={cn(
|
|
104
|
+
iconSizes[size],
|
|
105
|
+
'absolute inset-0 m-auto transition-all duration-300 ease-out',
|
|
106
|
+
resolvedTheme === 'light'
|
|
107
|
+
? 'rotate-0 scale-100 opacity-100'
|
|
108
|
+
: 'rotate-90 scale-75 opacity-0'
|
|
109
|
+
)}
|
|
110
|
+
/>
|
|
111
|
+
|
|
112
|
+
{/* Moon icon - visible in dark mode */}
|
|
113
|
+
<MoonIcon
|
|
114
|
+
className={cn(
|
|
115
|
+
iconSizes[size],
|
|
116
|
+
'absolute inset-0 m-auto transition-all duration-300 ease-out',
|
|
117
|
+
resolvedTheme === 'dark'
|
|
118
|
+
? 'rotate-0 scale-100 opacity-100'
|
|
119
|
+
: '-rotate-90 scale-75 opacity-0'
|
|
120
|
+
)}
|
|
121
|
+
/>
|
|
122
|
+
</button>
|
|
123
|
+
|
|
124
|
+
{/* Dropdown Menu */}
|
|
125
|
+
<div
|
|
126
|
+
className={cn(
|
|
127
|
+
'absolute right-0 mt-2 min-w-[140px]',
|
|
128
|
+
'p-1',
|
|
129
|
+
'bg-white dark:bg-[#171717]',
|
|
130
|
+
'border border-gray-200 dark:border-[#262626]',
|
|
131
|
+
'rounded-xl',
|
|
132
|
+
'shadow-lg shadow-black/5 dark:shadow-black/40',
|
|
133
|
+
'z-50',
|
|
134
|
+
'transition-all duration-200 ease-out origin-top-right',
|
|
135
|
+
isOpen
|
|
136
|
+
? 'opacity-100 scale-100 translate-y-0'
|
|
137
|
+
: 'opacity-0 scale-95 -translate-y-1 pointer-events-none'
|
|
138
|
+
)}
|
|
139
|
+
role="listbox"
|
|
140
|
+
aria-label="Select theme"
|
|
141
|
+
>
|
|
142
|
+
{themeOptions.map((option, index) => {
|
|
143
|
+
const Icon = option.icon
|
|
144
|
+
const isSelected = theme === option.value
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<button
|
|
148
|
+
key={option.value}
|
|
149
|
+
onClick={() => {
|
|
150
|
+
setTheme(option.value)
|
|
151
|
+
setIsOpen(false)
|
|
152
|
+
}}
|
|
153
|
+
className={cn(
|
|
154
|
+
'w-full flex items-center gap-2.5 px-3 py-2',
|
|
155
|
+
'rounded-lg',
|
|
156
|
+
'text-[13px] font-medium',
|
|
157
|
+
'transition-all duration-150 ease-out',
|
|
158
|
+
'focus:outline-none',
|
|
159
|
+
isSelected
|
|
160
|
+
? 'text-gray-900 dark:text-white bg-gray-100 dark:bg-[#262626]'
|
|
161
|
+
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-50 dark:hover:bg-[#1f1f1f]'
|
|
162
|
+
)}
|
|
163
|
+
role="option"
|
|
164
|
+
aria-selected={isSelected}
|
|
165
|
+
style={{
|
|
166
|
+
animationDelay: isOpen ? `${index * 25}ms` : '0ms'
|
|
167
|
+
}}
|
|
168
|
+
>
|
|
169
|
+
<Icon className={cn(
|
|
170
|
+
'w-4 h-4 flex-shrink-0',
|
|
171
|
+
'transition-transform duration-150',
|
|
172
|
+
isSelected ? 'scale-110' : 'scale-100'
|
|
173
|
+
)} />
|
|
174
|
+
<span className="flex-1 text-left">{option.label}</span>
|
|
175
|
+
<div className={cn(
|
|
176
|
+
'w-1.5 h-1.5 rounded-full',
|
|
177
|
+
'transition-all duration-200',
|
|
178
|
+
isSelected
|
|
179
|
+
? 'bg-primary-500 scale-100 opacity-100'
|
|
180
|
+
: 'bg-transparent scale-0 opacity-0'
|
|
181
|
+
)} />
|
|
182
|
+
</button>
|
|
183
|
+
)
|
|
184
|
+
})}
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
)
|
|
188
|
+
}
|
package/src/components/index.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
export { CodeBlock, InlineCode } from './CodeBlock'
|
|
2
|
+
export { DocSetSelector } from './DocSetSelector'
|
|
2
3
|
export { DocsLayout } from './DocsLayout'
|
|
3
4
|
export { DocsPage } from './DocsPage'
|
|
4
5
|
export { EmojiConfetti } from './EmojiConfetti'
|
|
5
6
|
export { HomePage } from './HomePage'
|
|
6
7
|
export { Markdown } from './Markdown'
|
|
7
8
|
export { Sidebar } from './Sidebar'
|
|
9
|
+
export { ThemeProvider, useTheme, themeInitScript } from './ThemeProvider'
|
|
10
|
+
export { ThemeToggle } from './ThemeToggle'
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Components
|
|
2
2
|
export {
|
|
3
3
|
CodeBlock,
|
|
4
|
+
DocSetSelector,
|
|
4
5
|
DocsLayout,
|
|
5
6
|
DocsPage,
|
|
6
7
|
EmojiConfetti,
|
|
@@ -8,6 +9,10 @@ export {
|
|
|
8
9
|
InlineCode,
|
|
9
10
|
Markdown,
|
|
10
11
|
Sidebar,
|
|
12
|
+
ThemeProvider,
|
|
13
|
+
ThemeToggle,
|
|
14
|
+
useTheme,
|
|
15
|
+
themeInitScript,
|
|
11
16
|
} from './components'
|
|
12
17
|
|
|
13
18
|
// HomePage sub-components (for compound component pattern)
|
|
@@ -33,6 +38,7 @@ export type {
|
|
|
33
38
|
DocContent,
|
|
34
39
|
DocsAppConfig,
|
|
35
40
|
DocsLayoutProps,
|
|
41
|
+
DocSetMeta,
|
|
36
42
|
MarkdownProps,
|
|
37
43
|
NavItem,
|
|
38
44
|
NavSection,
|
|
@@ -40,6 +46,8 @@ export type {
|
|
|
40
46
|
SidebarProps,
|
|
41
47
|
} from './types'
|
|
42
48
|
|
|
49
|
+
export type { Theme, ResolvedTheme } from './components/ThemeProvider'
|
|
50
|
+
|
|
43
51
|
export type {
|
|
44
52
|
HomePageProps,
|
|
45
53
|
HomePageContextValue,
|
package/src/ssr.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { createInertiaApp } from '@inertiajs/react'
|
|
|
2
2
|
import createServer from '@inertiajs/react/server'
|
|
3
3
|
import ReactDOMServer from 'react-dom/server'
|
|
4
4
|
import type { DocsAppConfig } from './types'
|
|
5
|
+
import { ThemeProvider } from './components/ThemeProvider'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Create an SSR server for documentation.
|
|
@@ -33,7 +34,11 @@ export function createDocsServer(config: DocsAppConfig): void {
|
|
|
33
34
|
}
|
|
34
35
|
return pageComponent
|
|
35
36
|
},
|
|
36
|
-
setup: ({ App, props }) =>
|
|
37
|
+
setup: ({ App, props }) => (
|
|
38
|
+
<ThemeProvider>
|
|
39
|
+
<App {...props} />
|
|
40
|
+
</ThemeProvider>
|
|
41
|
+
),
|
|
37
42
|
})
|
|
38
43
|
)
|
|
39
44
|
}
|
package/src/styles.css
CHANGED
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
* Note: The Google Fonts import must come before other imports to avoid CSS ordering warnings.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
/* Enable class-based dark mode (using .dark class on root element) */
|
|
17
|
+
@custom-variant dark (&:where(.dark, .dark *));
|
|
18
|
+
|
|
16
19
|
/* Theme customizations for Tailwind v4 */
|
|
17
20
|
@theme {
|
|
18
21
|
/* Max width */
|
|
@@ -56,6 +59,31 @@
|
|
|
56
59
|
--shiki-token-string-expression: #86efac;
|
|
57
60
|
--shiki-token-punctuation: #94a3b8;
|
|
58
61
|
--shiki-token-link: #38bdf8;
|
|
62
|
+
|
|
63
|
+
/* Surface colors for light mode */
|
|
64
|
+
--surface-primary: #ffffff;
|
|
65
|
+
--surface-secondary: #f9fafb;
|
|
66
|
+
--surface-tertiary: #f3f4f6;
|
|
67
|
+
--border-primary: #e5e7eb;
|
|
68
|
+
--border-secondary: #d1d5db;
|
|
69
|
+
--text-primary: #111827;
|
|
70
|
+
--text-secondary: #4b5563;
|
|
71
|
+
--text-tertiary: #6b7280;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.dark {
|
|
75
|
+
/* Surface colors for dark mode */
|
|
76
|
+
--surface-primary: #0f0f0f;
|
|
77
|
+
--surface-secondary: #171717;
|
|
78
|
+
--surface-tertiary: #262626;
|
|
79
|
+
--border-primary: #262626;
|
|
80
|
+
--border-secondary: #404040;
|
|
81
|
+
--text-primary: #fafafa;
|
|
82
|
+
--text-secondary: #a3a3a3;
|
|
83
|
+
--text-tertiary: #737373;
|
|
84
|
+
|
|
85
|
+
/* Slightly lighter code blocks in dark mode for contrast */
|
|
86
|
+
--shiki-color-background: #1a1a1a;
|
|
59
87
|
}
|
|
60
88
|
|
|
61
89
|
html {
|
|
@@ -71,6 +99,14 @@
|
|
|
71
99
|
@apply antialiased bg-white text-gray-800;
|
|
72
100
|
font-family: var(--font-sans);
|
|
73
101
|
font-weight: 400;
|
|
102
|
+
transition: background-color 0.2s ease, color 0.2s ease;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* Dark mode body - using direct selector for cross-package compatibility */
|
|
106
|
+
.dark body,
|
|
107
|
+
:root.dark body {
|
|
108
|
+
background-color: #0f0f0f;
|
|
109
|
+
color: #f3f4f6;
|
|
74
110
|
}
|
|
75
111
|
|
|
76
112
|
/* Headings use heading font */
|
|
@@ -129,6 +165,7 @@
|
|
|
129
165
|
/* Inline code styling */
|
|
130
166
|
.prose :not(pre) > code {
|
|
131
167
|
@apply bg-gray-100 px-1.5 py-0.5 rounded text-sm font-medium text-gray-800;
|
|
168
|
+
@apply dark:bg-gray-800 dark:text-gray-200;
|
|
132
169
|
}
|
|
133
170
|
|
|
134
171
|
.prose :not(pre) > code::before,
|
|
@@ -140,6 +177,24 @@
|
|
|
140
177
|
.prose-invert :not(pre) > code {
|
|
141
178
|
@apply bg-gray-800;
|
|
142
179
|
}
|
|
180
|
+
|
|
181
|
+
/* Dark mode prose text colors */
|
|
182
|
+
.dark .prose {
|
|
183
|
+
--tw-prose-body: theme(colors.gray.300);
|
|
184
|
+
--tw-prose-headings: theme(colors.white);
|
|
185
|
+
--tw-prose-lead: theme(colors.gray.400);
|
|
186
|
+
--tw-prose-bold: theme(colors.white);
|
|
187
|
+
--tw-prose-counters: theme(colors.gray.400);
|
|
188
|
+
--tw-prose-bullets: theme(colors.gray.600);
|
|
189
|
+
--tw-prose-hr: theme(colors.gray.700);
|
|
190
|
+
--tw-prose-quotes: theme(colors.gray.100);
|
|
191
|
+
--tw-prose-quote-borders: theme(colors.gray.700);
|
|
192
|
+
--tw-prose-captions: theme(colors.gray.400);
|
|
193
|
+
--tw-prose-kbd: theme(colors.white);
|
|
194
|
+
--tw-prose-kbd-shadows: 0 0 0 theme(colors.gray.100);
|
|
195
|
+
--tw-prose-th-borders: theme(colors.gray.600);
|
|
196
|
+
--tw-prose-td-borders: theme(colors.gray.700);
|
|
197
|
+
}
|
|
143
198
|
}
|
|
144
199
|
|
|
145
200
|
/* Syntax highlighting - rounded corners like Starlight */
|
|
@@ -170,6 +225,15 @@
|
|
|
170
225
|
@apply bg-gray-400;
|
|
171
226
|
}
|
|
172
227
|
|
|
228
|
+
/* Dark mode scrollbars */
|
|
229
|
+
.dark ::-webkit-scrollbar-thumb {
|
|
230
|
+
@apply bg-gray-700;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.dark ::-webkit-scrollbar-thumb:hover {
|
|
234
|
+
@apply bg-gray-600;
|
|
235
|
+
}
|
|
236
|
+
|
|
173
237
|
/* Emoji confetti animation */
|
|
174
238
|
@keyframes emojiConfettiBurst {
|
|
175
239
|
0% {
|
package/src/types.ts
CHANGED
|
@@ -16,6 +16,18 @@ export interface NavSection {
|
|
|
16
16
|
items: NavItem[]
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/** Documentation set metadata (for multi-docs mode) */
|
|
20
|
+
export interface DocSetMeta {
|
|
21
|
+
name: string
|
|
22
|
+
slug: string
|
|
23
|
+
description: string
|
|
24
|
+
/** Emoji or short text icon (e.g., "🍓") */
|
|
25
|
+
icon?: string
|
|
26
|
+
/** URL to icon image */
|
|
27
|
+
iconUrl?: string
|
|
28
|
+
prefix: string
|
|
29
|
+
}
|
|
30
|
+
|
|
19
31
|
/** Shared props passed to all pages via Inertia */
|
|
20
32
|
export interface SharedProps {
|
|
21
33
|
nav: NavSection[]
|
|
@@ -26,10 +38,16 @@ export interface SharedProps {
|
|
|
26
38
|
logoInvertedUrl?: string
|
|
27
39
|
/** Footer logo image URL (from Python backend) */
|
|
28
40
|
footerLogoUrl?: string
|
|
41
|
+
/** Footer logo image URL for dark mode (from Python backend) */
|
|
42
|
+
footerLogoInvertedUrl?: string
|
|
29
43
|
/** GitHub repository URL (from Python backend) */
|
|
30
44
|
githubUrl?: string
|
|
31
45
|
/** Additional navigation links (from Python backend) */
|
|
32
46
|
navLinks?: Array<{ label: string; href: string }>
|
|
47
|
+
/** Available documentation sets (multi-docs mode) */
|
|
48
|
+
docSets?: DocSetMeta[]
|
|
49
|
+
/** Current documentation set slug (multi-docs mode) */
|
|
50
|
+
currentDocSet?: string
|
|
33
51
|
}
|
|
34
52
|
|
|
35
53
|
/** Document content structure */
|
|
@@ -65,6 +83,10 @@ export interface SidebarProps {
|
|
|
65
83
|
nav: NavSection[]
|
|
66
84
|
currentPath: string
|
|
67
85
|
className?: string
|
|
86
|
+
/** Available documentation sets (multi-docs mode) */
|
|
87
|
+
docSets?: DocSetMeta[]
|
|
88
|
+
/** Current documentation set slug (multi-docs mode) */
|
|
89
|
+
currentDocSet?: string
|
|
68
90
|
}
|
|
69
91
|
|
|
70
92
|
/** Props for Markdown component */
|