Skip to content

Commit fea0892

Browse files
Merge pull request #10 from aviarytech/claude
dark mode
2 parents 1c96656 + ddf4709 commit fea0892

File tree

12 files changed

+265
-41
lines changed

12 files changed

+265
-41
lines changed

CLAUDE.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# AV1-C Development Guide
2+
3+
## Commands
4+
- Build: `bun run build` or `npm run build`
5+
- Watch mode: `bun run dev` or `npm run dev`
6+
- Docs development: `bun run docs:dev`
7+
- Docs build: `bun run docs:build`
8+
- Docs preview: `bun run docs:preview`
9+
10+
## Code Style
11+
- Components: Use functional components with forwardRef pattern
12+
- Types: Define explicit interfaces for props extending HTML elements
13+
- Naming: PascalCase for components/interfaces, camelCase for functions/variables
14+
- Styling: Use TailwindCSS with class-variance-authority (cva) for variants
15+
- Formatting: No explicit ESLint/Prettier config, but maintain consistent formatting
16+
- Error handling: Use ErrorState component with appropriate variants
17+
18+
## Project Structure
19+
- Components in `/src/components/[component-name]/Component.tsx`
20+
- Utils in `/src/utils/`
21+
- Types in `/src/types/`
22+
23+
## Git Workflow
24+
- Follow [Conventional Commits](https://www.conventionalcommits.org/) specification
25+
- Format: `type(scope): description` (e.g., `feat(button): add new variant`)

src/ThemeProvider.tsx

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import * as React from "react";
2+
3+
type Theme = 'light' | 'dark' | 'system';
4+
type ThemeContextType = {
5+
theme: Theme;
6+
resolvedTheme: 'light' | 'dark';
7+
setTheme: (theme: Theme) => void;
8+
};
9+
10+
const ThemeContext = React.createContext<ThemeContextType | undefined>(undefined);
11+
12+
export const ThemeProvider: React.FC<{children: React.ReactNode}> = ({ children }) => {
13+
const [theme, setThemeState] = React.useState<Theme>(() => {
14+
if (typeof window === 'undefined') return 'system';
15+
return (localStorage.getItem('theme') as Theme) || 'system';
16+
});
17+
18+
const [resolvedTheme, setResolvedTheme] = React.useState<'light' | 'dark'>(() => {
19+
if (typeof window === 'undefined') return 'light';
20+
if (theme === 'system') {
21+
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
22+
}
23+
return theme;
24+
});
25+
26+
// Handle system preference changes
27+
React.useEffect(() => {
28+
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
29+
30+
const handleChange = () => {
31+
if (theme === 'system') {
32+
const newTheme = mediaQuery.matches ? 'dark' : 'light';
33+
setResolvedTheme(newTheme);
34+
updateDocumentClass(newTheme);
35+
}
36+
};
37+
38+
mediaQuery.addEventListener('change', handleChange);
39+
return () => mediaQuery.removeEventListener('change', handleChange);
40+
}, [theme]);
41+
42+
// Update the DOM and resolved theme when theme changes
43+
React.useEffect(() => {
44+
if (theme === 'system') {
45+
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
46+
setResolvedTheme(systemTheme);
47+
updateDocumentClass(systemTheme);
48+
} else {
49+
setResolvedTheme(theme);
50+
updateDocumentClass(theme);
51+
}
52+
}, [theme]);
53+
54+
const updateDocumentClass = (resolvedTheme: string) => {
55+
if (resolvedTheme === 'dark') {
56+
document.documentElement.classList.add('dark');
57+
} else {
58+
document.documentElement.classList.remove('dark');
59+
}
60+
};
61+
62+
const setTheme = (newTheme: Theme) => {
63+
setThemeState(newTheme);
64+
if (newTheme === 'system') {
65+
localStorage.removeItem('theme');
66+
} else {
67+
localStorage.setItem('theme', newTheme);
68+
}
69+
};
70+
71+
return (
72+
<ThemeContext.Provider value={{ theme, resolvedTheme, setTheme }}>
73+
{children}
74+
</ThemeContext.Provider>
75+
);
76+
};
77+
78+
export const useTheme = () => {
79+
const context = React.useContext(ThemeContext);
80+
if (context === undefined) {
81+
throw new Error('useTheme must be used within a ThemeProvider');
82+
}
83+
return context;
84+
};

src/components/card/Card.tsx

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,35 @@ import { cn } from "../../utils/cn";
44
export interface CardProps extends React.HTMLAttributes<HTMLDivElement> {}
55

66
const Card = React.forwardRef<HTMLDivElement, CardProps>(
7-
({ className, ...props }, ref) => (
8-
<div
9-
ref={ref}
10-
className={cn(
11-
"rounded-lg border shadow-xl overflow-hidden",
12-
className
13-
)}
14-
{...props}
15-
/>
16-
)
7+
({ className, children, ...props }, ref) => {
8+
// Get all child elements
9+
const childArray = React.Children.toArray(children);
10+
11+
// Check if any child is a CardHeader or CardContent component
12+
const hasCardComponents = childArray.some(child =>
13+
React.isValidElement(child) &&
14+
(child.type === CardHeader ||
15+
child.type === CardContent ||
16+
child.type === CardFooter)
17+
);
18+
19+
// Add padding if there are no Card subcomponents (direct content)
20+
const needsPadding = !hasCardComponents;
21+
22+
return (
23+
<div
24+
ref={ref}
25+
className={cn(
26+
"rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 shadow-xl overflow-hidden",
27+
needsPadding && "p-6",
28+
className
29+
)}
30+
{...props}
31+
>
32+
{children}
33+
</div>
34+
);
35+
}
1736
);
1837
Card.displayName = "Card";
1938

@@ -49,7 +68,7 @@ export const CardDescription = React.forwardRef<
4968
>(({ className, ...props }, ref) => (
5069
<p
5170
ref={ref}
52-
className={cn("text-sm text-gray-400", className)}
71+
className={cn("text-sm text-gray-500 dark:text-gray-400", className)}
5372
{...props}
5473
/>
5574
));
@@ -67,7 +86,7 @@ export const CardFooter = React.forwardRef<HTMLDivElement, CardProps>(
6786
<div
6887
ref={ref}
6988
className={cn(
70-
"border-t border-gray-700 p-4 bg-gray-850 flex justify-between items-center",
89+
"border-t border-gray-200 dark:border-gray-700 p-4 bg-gray-50 dark:bg-gray-800 flex justify-between items-center",
7190
className
7291
)}
7392
{...props}

src/components/editor/CodeEditor.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import CodeMirror from '@uiw/react-codemirror';
44
import { oneDark } from '@codemirror/theme-one-dark';
55
import { javascript } from '@codemirror/lang-javascript';
66
import { json } from '@codemirror/lang-json';
7+
import { useTheme } from "../../ThemeProvider";
78

89
export interface CodeEditorProps {
910
value: string;
@@ -39,12 +40,16 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
3940
}
4041
};
4142

43+
// Get the current theme to determine if we should use the dark theme
44+
const { resolvedTheme } = useTheme();
45+
const isDark = resolvedTheme === 'dark';
46+
4247
return (
43-
<div className={cn("rounded-lg overflow-hidden border border-gray-700", className)}>
48+
<div className={cn("rounded-lg overflow-hidden border border-gray-200 dark:border-gray-700", className)}>
4449
<CodeMirror
4550
value={value}
4651
height={height}
47-
theme={oneDark}
52+
theme={isDark ? oneDark : undefined}
4853
extensions={[...getLanguageExtension(), ...extensions]}
4954
onChange={onChange}
5055
editable={!readOnly}

src/components/toast/Toast.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ export interface ToastProps extends React.HTMLAttributes<HTMLDivElement> {
1313
export const Toast = React.forwardRef<HTMLDivElement, ToastProps>(
1414
({ className, variant = "default", title, description, onClose, ...props }, ref) => {
1515
const variantStyles = {
16-
default: "bg-gray-800 border-gray-700",
17-
success: "bg-green-500/10 border-green-500/20 text-green-300",
18-
error: "bg-red-500/10 border-red-500/20 text-red-300",
19-
warning: "bg-yellow-500/10 border-yellow-500/20 text-yellow-300",
16+
default: "bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 text-gray-900 dark:text-gray-100",
17+
success: "bg-green-50 dark:bg-green-500/10 border-green-200 dark:border-green-500/20 text-green-700 dark:text-green-300",
18+
error: "bg-red-50 dark:bg-red-500/10 border-red-200 dark:border-red-500/20 text-red-700 dark:text-red-300",
19+
warning: "bg-yellow-50 dark:bg-yellow-500/10 border-yellow-200 dark:border-yellow-500/20 text-yellow-700 dark:text-yellow-300",
2020
};
2121

2222
return (

src/docs/App.tsx

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,11 @@ import { FeaturesPage } from "./pages/FeaturesPage";
3333
import { Toaster } from 'react-hot-toast';
3434
import { SchemaEditorPage } from "./pages/features/SchemaEditorPage";
3535
import { BadgePage } from "./pages/components/BadgePage";
36+
import { ThemeProvider, useTheme } from "../ThemeProvider";
3637

37-
export const App = () => {
38+
// Wrapper component that uses the theme context
39+
const AppContent = () => {
40+
const { resolvedTheme } = useTheme();
3841
const [currentPage, setCurrentPage] = React.useState(() => {
3942
// First try to get page from URL hash
4043
const hashPage = window.location.hash.slice(1);
@@ -132,8 +135,10 @@ export const App = () => {
132135
position="bottom-right"
133136
toastOptions={{
134137
style: {
135-
background: '#1f2937',
136-
color: '#fff',
138+
background: resolvedTheme === 'dark' ? '#1f2937' : '#ffffff',
139+
color: resolvedTheme === 'dark' ? '#fff' : '#000',
140+
border: '1px solid',
141+
borderColor: resolvedTheme === 'dark' ? '#374151' : '#e5e7eb',
137142
},
138143
success: {
139144
duration: 3000,
@@ -177,4 +182,13 @@ export const App = () => {
177182
</Container>
178183
</div>
179184
);
180-
};
185+
};
186+
187+
// Main App component with ThemeProvider
188+
export const App = () => {
189+
return (
190+
<ThemeProvider>
191+
<AppContent />
192+
</ThemeProvider>
193+
);
194+
};

src/docs/components/ThemeToggle.tsx

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,16 @@
11
import * as React from "react";
22
import { Button } from "../../components/button/Button";
3-
import { Sun, Moon } from "lucide-react";
3+
import { Sun, Moon, Monitor } from "lucide-react";
4+
import { useTheme } from "../../ThemeProvider";
45

56
export const ThemeToggle = () => {
6-
const [isDark, setIsDark] = React.useState(() => {
7-
if (typeof window === 'undefined') return false;
8-
return document.documentElement.classList.contains('dark');
9-
});
7+
const { theme, setTheme } = useTheme();
108

119
const toggleTheme = () => {
12-
const newTheme = !isDark;
13-
setIsDark(newTheme);
14-
15-
if (newTheme) {
16-
document.documentElement.classList.add('dark');
17-
localStorage.setItem('theme', 'dark');
18-
} else {
19-
document.documentElement.classList.remove('dark');
20-
localStorage.setItem('theme', 'light');
21-
}
10+
// Cycle through theme options: light -> dark -> system -> light
11+
if (theme === 'light') setTheme('dark');
12+
else if (theme === 'dark') setTheme('system');
13+
else setTheme('light');
2214
};
2315

2416
return (
@@ -27,11 +19,14 @@ export const ThemeToggle = () => {
2719
size="sm"
2820
onClick={toggleTheme}
2921
className="ml-auto"
22+
title={`Current theme: ${theme}`}
3023
>
31-
{isDark ? (
24+
{theme === 'light' ? (
3225
<Sun className="h-5 w-5" />
33-
) : (
26+
) : theme === 'dark' ? (
3427
<Moon className="h-5 w-5" />
28+
) : (
29+
<Monitor className="h-5 w-5" />
3530
)}
3631
</Button>
3732
);

src/docs/index.html

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,15 @@
99
// Immediately invoked function to set the theme before page render
1010
(function() {
1111
const stored = localStorage.getItem('theme');
12-
if (stored === 'dark' || (!stored && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
12+
13+
// If user has explicitly chosen a theme, use that
14+
if (stored === 'dark') {
15+
document.documentElement.classList.add('dark');
16+
} else if (stored === 'light') {
17+
document.documentElement.classList.remove('dark');
18+
}
19+
// Otherwise respect system preference
20+
else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
1321
document.documentElement.classList.add('dark');
1422
} else {
1523
document.documentElement.classList.remove('dark');

src/docs/pages/FeaturesPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export const FeaturesPage = () => {
3232
<li>Nested objects and arrays</li>
3333
<li>Field descriptions and validation</li>
3434
<li>JSON-LD context management</li>
35-
<li>Dark mode support</li>
35+
<li>Intelligent dark/light mode with system preference detection</li>
3636
<li>TypeScript integration</li>
3737
</ul>
3838
</div>

0 commit comments

Comments
 (0)