@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.
@@ -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
+ }
@@ -5,3 +5,5 @@ export { EmojiConfetti } from './EmojiConfetti'
5
5
  export { HomePage } from './HomePage'
6
6
  export { Markdown } from './Markdown'
7
7
  export { Sidebar } from './Sidebar'
8
+ export { ThemeProvider, useTheme, themeInitScript } from './ThemeProvider'
9
+ export { ThemeToggle } from './ThemeToggle'
package/src/index.ts CHANGED
@@ -8,6 +8,10 @@ export {
8
8
  InlineCode,
9
9
  Markdown,
10
10
  Sidebar,
11
+ ThemeProvider,
12
+ ThemeToggle,
13
+ useTheme,
14
+ themeInitScript,
11
15
  } from './components'
12
16
 
13
17
  // HomePage sub-components (for compound component pattern)
@@ -40,6 +44,8 @@ export type {
40
44
  SidebarProps,
41
45
  } from './types'
42
46
 
47
+ export type { Theme, ResolvedTheme } from './components/ThemeProvider'
48
+
43
49
  export type {
44
50
  HomePageProps,
45
51
  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 }) => <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
@@ -56,6 +56,31 @@
56
56
  --shiki-token-string-expression: #86efac;
57
57
  --shiki-token-punctuation: #94a3b8;
58
58
  --shiki-token-link: #38bdf8;
59
+
60
+ /* Surface colors for light mode */
61
+ --surface-primary: #ffffff;
62
+ --surface-secondary: #f9fafb;
63
+ --surface-tertiary: #f3f4f6;
64
+ --border-primary: #e5e7eb;
65
+ --border-secondary: #d1d5db;
66
+ --text-primary: #111827;
67
+ --text-secondary: #4b5563;
68
+ --text-tertiary: #6b7280;
69
+ }
70
+
71
+ .dark {
72
+ /* Surface colors for dark mode */
73
+ --surface-primary: #0f0f0f;
74
+ --surface-secondary: #171717;
75
+ --surface-tertiary: #262626;
76
+ --border-primary: #262626;
77
+ --border-secondary: #404040;
78
+ --text-primary: #fafafa;
79
+ --text-secondary: #a3a3a3;
80
+ --text-tertiary: #737373;
81
+
82
+ /* Slightly lighter code blocks in dark mode for contrast */
83
+ --shiki-color-background: #1a1a1a;
59
84
  }
60
85
 
61
86
  html {
@@ -71,6 +96,14 @@
71
96
  @apply antialiased bg-white text-gray-800;
72
97
  font-family: var(--font-sans);
73
98
  font-weight: 400;
99
+ transition: background-color 0.2s ease, color 0.2s ease;
100
+ }
101
+
102
+ /* Dark mode body - using direct selector for cross-package compatibility */
103
+ .dark body,
104
+ :root.dark body {
105
+ background-color: #0f0f0f;
106
+ color: #f3f4f6;
74
107
  }
75
108
 
76
109
  /* Headings use heading font */
@@ -129,6 +162,7 @@
129
162
  /* Inline code styling */
130
163
  .prose :not(pre) > code {
131
164
  @apply bg-gray-100 px-1.5 py-0.5 rounded text-sm font-medium text-gray-800;
165
+ @apply dark:bg-gray-800 dark:text-gray-200;
132
166
  }
133
167
 
134
168
  .prose :not(pre) > code::before,
@@ -140,6 +174,24 @@
140
174
  .prose-invert :not(pre) > code {
141
175
  @apply bg-gray-800;
142
176
  }
177
+
178
+ /* Dark mode prose text colors */
179
+ .dark .prose {
180
+ --tw-prose-body: theme(colors.gray.300);
181
+ --tw-prose-headings: theme(colors.white);
182
+ --tw-prose-lead: theme(colors.gray.400);
183
+ --tw-prose-bold: theme(colors.white);
184
+ --tw-prose-counters: theme(colors.gray.400);
185
+ --tw-prose-bullets: theme(colors.gray.600);
186
+ --tw-prose-hr: theme(colors.gray.700);
187
+ --tw-prose-quotes: theme(colors.gray.100);
188
+ --tw-prose-quote-borders: theme(colors.gray.700);
189
+ --tw-prose-captions: theme(colors.gray.400);
190
+ --tw-prose-kbd: theme(colors.white);
191
+ --tw-prose-kbd-shadows: 0 0 0 theme(colors.gray.100);
192
+ --tw-prose-th-borders: theme(colors.gray.600);
193
+ --tw-prose-td-borders: theme(colors.gray.700);
194
+ }
143
195
  }
144
196
 
145
197
  /* Syntax highlighting - rounded corners like Starlight */
@@ -170,6 +222,15 @@
170
222
  @apply bg-gray-400;
171
223
  }
172
224
 
225
+ /* Dark mode scrollbars */
226
+ .dark ::-webkit-scrollbar-thumb {
227
+ @apply bg-gray-700;
228
+ }
229
+
230
+ .dark ::-webkit-scrollbar-thumb:hover {
231
+ @apply bg-gray-600;
232
+ }
233
+
173
234
  /* Emoji confetti animation */
174
235
  @keyframes emojiConfettiBurst {
175
236
  0% {
package/src/types.ts CHANGED
@@ -26,6 +26,8 @@ export interface SharedProps {
26
26
  logoInvertedUrl?: string
27
27
  /** Footer logo image URL (from Python backend) */
28
28
  footerLogoUrl?: string
29
+ /** Footer logo image URL for dark mode (from Python backend) */
30
+ footerLogoInvertedUrl?: string
29
31
  /** GitHub repository URL (from Python backend) */
30
32
  githubUrl?: string
31
33
  /** Additional navigation links (from Python backend) */