Skip to content
Open
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
7 changes: 7 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Bash(pnpm lint *)"
]
}
}
6 changes: 3 additions & 3 deletions src/components/layout/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ export function Header({ onOpenSidePanel }: HeaderProps) {
const { theme, cycleTheme } = useTheme();
const reduxUsername = useSelector((state: RootState) => state.auth.username);

// Check localStorage for remembered device user (client-only, SSR-safe).
// Mirrors wallet-legacy's autopost2 restore in Main.js — the avatar should
// be visible whenever the device remembers a username, even before full auth.
// getServerSnapshot returns null so server and hydration renders both produce
// null (isLoggedIn = false). React detects the post-hydration snapshot diff
// and schedules a sync re-render to show the avatar without a mismatch.
const rememberedUsername = useSyncExternalStore(
() => () => {},
() => getRememberedDeviceUsername(),
Expand Down
56 changes: 34 additions & 22 deletions src/lib/theme.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import { useLayoutEffect, useState } from 'react';
import { useCallback, useLayoutEffect, useSyncExternalStore } from 'react';

export type LegacyTheme = 'original' | 'light' | 'dark';

Expand All @@ -10,46 +10,58 @@ const validThemes: LegacyTheme[] = ['original', 'light', 'dark'];
/** Default day theme — wallet-legacy uses `theme-light` only (see App.jsx), not `theme-original`. */
const DEFAULT_THEME: LegacyTheme = 'light';

// Module-level store so useSyncExternalStore can subscribe to mutations.
// Initialized from localStorage at module load time on the client — the
// server always returns DEFAULT_THEME via getServerSnapshot, so hydration
// produces the same tree the server sent before React switches to the real value.
let _theme: LegacyTheme = DEFAULT_THEME;
const _listeners = new Set<() => void>();

if (typeof window !== 'undefined') {
const stored = localStorage.getItem(THEME_KEY) as LegacyTheme;
if (stored && validThemes.includes(stored)) _theme = stored;
}

function _subscribeTheme(cb: () => void) {
_listeners.add(cb);
return () => { _listeners.delete(cb); };
}

function _getThemeSnapshot(): LegacyTheme { return _theme; }
function _getThemeServerSnapshot(): LegacyTheme { return DEFAULT_THEME; }

/**
* Hook for managing legacy wallet themes (original, light, dark)
*/
export function useTheme() {
const [theme, setThemeState] = useState<LegacyTheme>(() => {
if (typeof window === 'undefined') return DEFAULT_THEME;
const stored = localStorage.getItem(THEME_KEY) as LegacyTheme;
return stored && validThemes.includes(stored) ? stored : DEFAULT_THEME;
});

const applyTheme = (newTheme: LegacyTheme) => {
// Remove all theme classes
document.documentElement.classList.remove('theme-original', 'theme-light', 'theme-dark');

// Add new theme class
document.documentElement.classList.add(`theme-${newTheme}`);
};
const theme = useSyncExternalStore(_subscribeTheme, _getThemeSnapshot, _getThemeServerSnapshot);

// Apply theme class before paint to avoid FOUC.
useLayoutEffect(() => {
applyTheme(theme);
document.documentElement.classList.remove('theme-original', 'theme-light', 'theme-dark');
document.documentElement.classList.add(`theme-${theme}`);
}, [theme]);

const changeTheme = (newTheme: LegacyTheme) => {
const changeTheme = useCallback((newTheme: LegacyTheme) => {
if (!validThemes.includes(newTheme)) {
console.warn(`Invalid theme: ${newTheme}. Valid themes are: ${validThemes.join(', ')}`);
return;
}

setThemeState(newTheme);
_theme = newTheme;
localStorage.setItem(THEME_KEY, newTheme);
applyTheme(newTheme);
};
// Apply immediately so the class change is synchronous (avoids FOUC between
// the store mutation and the useLayoutEffect running after React re-renders).
document.documentElement.classList.remove('theme-original', 'theme-light', 'theme-dark');
document.documentElement.classList.add(`theme-${newTheme}`);
_listeners.forEach((l) => l());
}, []);

const cycleTheme = () => {
const cycleTheme = useCallback(() => {
const currentIndex = validThemes.indexOf(theme);
const nextIndex = currentIndex >= 0 ? (currentIndex + 1) % validThemes.length : 0;
const nextTheme = validThemes[nextIndex];
if (nextTheme) changeTheme(nextTheme);
};
}, [theme, changeTheme]);

return {
theme,
Expand Down
Loading