mirror of
https://github.com/backnotprop/plannotator.git
synced 2026-09-14 14:17:26 +08:00
92d4f73b66
A React-based annotation tool that intercepts Claude Code's ExitPlanMode to provide visual plan review with collaborative features. Features: - Markdown plan rendering with syntax highlighting - Text selection annotations (delete, replace, insert, comment) - Redline mode for quick deletion marking - URL-based sharing via compressed hash (no server storage) - Tater identity system for anonymous collaboration - Export to diff format - Dark/light theme support - Tater sprite easter egg 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
88 lines
2.1 KiB
TypeScript
88 lines
2.1 KiB
TypeScript
import { createContext, useContext, useEffect, useState } from 'react';
|
|
|
|
type Theme = 'dark' | 'light' | 'system';
|
|
|
|
type ThemeProviderState = {
|
|
theme: Theme;
|
|
setTheme: (theme: Theme) => void;
|
|
};
|
|
|
|
const ThemeProviderContext = createContext<ThemeProviderState>({
|
|
theme: 'dark',
|
|
setTheme: () => null,
|
|
});
|
|
|
|
interface ThemeProviderProps {
|
|
children: React.ReactNode;
|
|
defaultTheme?: Theme;
|
|
storageKey?: string;
|
|
}
|
|
|
|
export function ThemeProvider({
|
|
children,
|
|
defaultTheme = 'dark',
|
|
storageKey = 'plannotator-theme',
|
|
}: ThemeProviderProps) {
|
|
const [theme, setTheme] = useState<Theme>(
|
|
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
|
|
);
|
|
|
|
useEffect(() => {
|
|
const root = window.document.documentElement;
|
|
|
|
// Remove light class (dark is default, no class needed)
|
|
root.classList.remove('light');
|
|
|
|
let effectiveTheme = theme;
|
|
if (theme === 'system') {
|
|
effectiveTheme = window.matchMedia('(prefers-color-scheme: light)').matches
|
|
? 'light'
|
|
: 'dark';
|
|
}
|
|
|
|
// Only add 'light' class when in light mode
|
|
if (effectiveTheme === 'light') {
|
|
root.classList.add('light');
|
|
}
|
|
}, [theme]);
|
|
|
|
// Listen for system theme changes
|
|
useEffect(() => {
|
|
if (theme !== 'system') return;
|
|
|
|
const mediaQuery = window.matchMedia('(prefers-color-scheme: light)');
|
|
const handleChange = () => {
|
|
const root = window.document.documentElement;
|
|
root.classList.remove('light');
|
|
if (mediaQuery.matches) {
|
|
root.classList.add('light');
|
|
}
|
|
};
|
|
|
|
mediaQuery.addEventListener('change', handleChange);
|
|
return () => mediaQuery.removeEventListener('change', handleChange);
|
|
}, [theme]);
|
|
|
|
const value = {
|
|
theme,
|
|
setTheme: (newTheme: Theme) => {
|
|
localStorage.setItem(storageKey, newTheme);
|
|
setTheme(newTheme);
|
|
},
|
|
};
|
|
|
|
return (
|
|
<ThemeProviderContext.Provider value={value}>
|
|
{children}
|
|
</ThemeProviderContext.Provider>
|
|
);
|
|
}
|
|
|
|
export const useTheme = () => {
|
|
const context = useContext(ThemeProviderContext);
|
|
if (context === undefined) {
|
|
throw new Error('useTheme must be used within a ThemeProvider');
|
|
}
|
|
return context;
|
|
};
|