Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/components/StellarHistory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export function StellarHistory() {
tx.status === 'confirmed'
? 'bg-secondary'
: tx.status === 'pending'
? 'bg-tertiary animate-pulse'
? 'bg-warning animate-pulse'
: 'bg-error'
}`}
/>
Expand Down
48 changes: 35 additions & 13 deletions src/context/ThemeContext.tsx
Original file line number Diff line number Diff line change
@@ -1,51 +1,73 @@
import { createContext, useContext, useEffect, useState, type ReactNode } from 'react';

type Theme = 'light' | 'dark';
export type ThemePreference = Theme | 'system';

interface ThemeContextType {
theme: Theme;
preference: ThemePreference;
setThemePreference: (preference: ThemePreference) => void;
toggleTheme: () => void;
}

const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

const THEME_STORAGE_KEY = 'wraith-theme';

function getInitialTheme(): Theme {
if (typeof window === 'undefined') return 'light';
function getInitialPreference(): ThemePreference {
if (typeof window === 'undefined') return 'system';

const stored = localStorage.getItem(THEME_STORAGE_KEY);
if (stored === 'light' || stored === 'dark') return stored;
return stored === 'light' || stored === 'dark' ? stored : 'system';
}

return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
function getSystemTheme(): Theme {
return typeof window !== 'undefined' && window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
}

export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<Theme>(getInitialTheme);
const [isMounted, setIsMounted] = useState(false);
const [preference, setPreference] = useState<ThemePreference>(getInitialPreference);
const [systemTheme, setSystemTheme] = useState<Theme>(getSystemTheme);
const theme = preference === 'system' ? systemTheme : preference;

useEffect(() => {
setIsMounted(true);
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = (event: MediaQueryListEvent) => {
setSystemTheme(event.matches ? 'dark' : 'light');
};
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
}, []);

useEffect(() => {
if (!isMounted) return;

const root = document.documentElement;
if (theme === 'dark') {
root.classList.add('dark');
} else {
root.classList.remove('dark');
}

localStorage.setItem(THEME_STORAGE_KEY, theme);
}, [theme, isMounted]);
root.style.colorScheme = theme;
if (preference === 'system') {
localStorage.removeItem(THEME_STORAGE_KEY);
} else {
localStorage.setItem(THEME_STORAGE_KEY, preference);
}
}, [theme, preference]);

const toggleTheme = () => {
setTheme((prev) => (prev === 'light' ? 'dark' : 'light'));
setPreference(theme === 'light' ? 'dark' : 'light');
};

return <ThemeContext.Provider value={{ theme, toggleTheme }}>{children}</ThemeContext.Provider>;
return (
<ThemeContext.Provider
value={{ theme, preference, setThemePreference: setPreference, toggleTheme }}
>
{children}
</ThemeContext.Provider>
);
}

export function useTheme() {
Expand Down
14 changes: 10 additions & 4 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,20 @@
/* Light theme (default) */
--color-surface: #ffffff;
--color-surface-container: #f5f5f5;
--color-surface-container-high: #eeeeee;
--color-surface-bright: #fafafa;
--color-primary: #1a1a1a;
--color-on-surface: #1a1a1a;
--color-on-surface-variant: #4a4a4a;
--color-outline: #6b6b6b;
--color-outline-variant: #e0e0e0;
--color-error: #d32f2f;
--color-warning: #8a5a00;
--color-secondary: #2563eb;
--color-tertiary: #2e7d32;
--color-selection-bg: #1a1a1a;
--color-selection-text: #ffffff;
--color-placeholder: #6b6b6b;
--color-placeholder: #5f5f5f;
--color-scrollbar-track: #ffffff;
--color-scrollbar-thumb: #c0c0c0;
}
Expand All @@ -25,17 +28,20 @@
/* Dark theme */
--color-surface: #0e0e0e;
--color-surface-container: #141414;
--color-surface-container-high: #202020;
--color-surface-bright: #1a1a1a;
--color-primary: #c6c6c7;
--color-on-surface: #e6e1e5;
--color-on-surface-variant: #c4c7c5;
--color-outline: #767575;
--color-outline: #a3a3a3;
--color-outline-variant: #444444;
--color-error: #ee7d77;
--color-tertiary: #22c55e;
--color-warning: #facc15;
--color-secondary: #60a5fa;
--color-tertiary: #4ade80;
--color-selection-bg: #c6c6c7;
--color-selection-text: #0e0e0e;
--color-placeholder: #767575;
--color-placeholder: #a3a3a3;
--color-scrollbar-track: #0e0e0e;
--color-scrollbar-thumb: #444444;
}
Expand Down
8 changes: 5 additions & 3 deletions src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,11 @@ function Providers({ children }: { children: React.ReactNode }) {
createRoot(document.getElementById('root')!).render(
<StrictMode>
<BrowserRouter>
<Providers>
<App />
</Providers>
<ThemeProvider>
<Providers>
<App />
</Providers>
</ThemeProvider>
</BrowserRouter>
</StrictMode>,
);
61 changes: 61 additions & 0 deletions src/pages/Settings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { useTheme, type ThemePreference } from '@/context/ThemeContext';

const preferences: Array<{ value: ThemePreference; label: string; description: string }> = [
{
value: 'system',
label: 'System default',
description: 'Follow your operating system preference.',
},
{ value: 'light', label: 'Light', description: 'Always use the light theme.' },
{ value: 'dark', label: 'Dark', description: 'Always use the dark theme.' },
];

export default function Settings() {
const { preference, setThemePreference } = useTheme();

return (
<section className="flex flex-col gap-8">
<div className="flex flex-col gap-2">
<span className="font-mono text-[10px] uppercase tracking-widest text-outline">
Preferences
</span>
<h1 className="font-heading text-[28px] font-bold uppercase tracking-tight text-on-surface">
Settings
</h1>
<p className="font-body text-sm leading-relaxed text-on-surface-variant">
Choose how Wraith should display colors. System default updates immediately when your OS
theme changes.
</p>
</div>

<fieldset className="flex flex-col gap-3 border border-outline-variant bg-surface-container p-5">
<legend className="font-mono text-[10px] uppercase tracking-widest text-on-surface">
Appearance
</legend>
{preferences.map((option) => (
<label
key={option.value}
className="flex cursor-pointer items-start gap-3 border border-outline-variant p-3 transition-colors hover:bg-surface-bright"
>
<input
type="radio"
name="theme"
value={option.value}
checked={preference === option.value}
onChange={() => setThemePreference(option.value)}
className="mt-1 accent-[var(--color-tertiary)]"
/>
<span className="flex flex-col gap-1">
<span className="font-heading text-sm font-semibold text-on-surface">
{option.label}
</span>
<span className="font-body text-xs text-on-surface-variant">
{option.description}
</span>
</span>
</label>
))}
</fieldset>
</section>
);
}
3 changes: 3 additions & 0 deletions tailwind.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const config: Config = {
surface: {
DEFAULT: 'var(--color-surface)',
container: 'var(--color-surface-container)',
'container-high': 'var(--color-surface-container-high)',
bright: 'var(--color-surface-bright)',
},
primary: 'var(--color-primary)',
Expand All @@ -19,6 +20,8 @@ const config: Config = {
variant: 'var(--color-outline-variant)',
},
error: 'var(--color-error)',
warning: 'var(--color-warning)',
secondary: 'var(--color-secondary)',
tertiary: 'var(--color-tertiary)',
},
fontFamily: {
Expand Down
Loading