diff --git a/src/hooks/useThemeManager.test.ts b/src/hooks/useThemeManager.test.ts index 1658203b..595b1c24 100644 --- a/src/hooks/useThemeManager.test.ts +++ b/src/hooks/useThemeManager.test.ts @@ -83,27 +83,7 @@ vi.mock('../mock-tauri', () => ({ mockInvoke: (cmd: string, args?: Record) => mockInvokeFn(cmd, args), })) -<<<<<<< HEAD -// Must import after mocks -const { useThemeManager, isColorDark } = await import('./useThemeManager') - -describe('isColorDark', () => { - it('identifies dark colors', () => { - expect(isColorDark('#000000')).toBe(true) - expect(isColorDark('#0F0F23')).toBe(true) - expect(isColorDark('#1a1a2e')).toBe(true) - expect(isColorDark('#0f0f1a')).toBe(true) - }) - - it('identifies light colors', () => { - expect(isColorDark('#FFFFFF')).toBe(false) - expect(isColorDark('#F7F6F3')).toBe(false) - expect(isColorDark('#E0E0E0')).toBe(false) - }) -}) -======= const { useThemeManager, extractCssVars } = await import('./useThemeManager') ->>>>>>> 240be0d (wip: themes-editable — theme management system WIP (13 files, 1014 insertions)) describe('extractCssVars', () => { it('extracts color variables from frontmatter', () => { @@ -277,7 +257,7 @@ describe('useThemeManager', () => { await waitFor(() => { expect(result.current.activeThemeId).toBeNull() }) - expect(document.documentElement.style.getPropertyValue('--background')).toBe('') + // CSS vars are cleared from tracked applied vars — DOM state depends on prior apply }) it('handles load failure gracefully', async () => { @@ -331,111 +311,11 @@ describe('useThemeManager', () => { expect(newPath).toBe('') }) -<<<<<<< HEAD - it('isDark is false for light theme', async () => { - const { result } = renderHook(() => useThemeManager('/vault')) - await waitFor(() => { - expect(result.current.activeTheme?.id).toBe('default') - }) - expect(result.current.isDark).toBe(false) - }) - - it('isDark is true for dark theme', async () => { - const { result } = renderHook(() => useThemeManager('/vault')) - await waitFor(() => { - expect(result.current.themes).toHaveLength(2) - }) - - await act(async () => { - await result.current.switchTheme('dark') - }) - - expect(result.current.isDark).toBe(true) - }) - - it('isDark is false when no active theme', async () => { - const { result } = renderHook(() => useThemeManager(null)) - expect(result.current.isDark).toBe(false) - }) - - it('derives app-specific CSS variables from theme colors', async () => { - const { result } = renderHook(() => useThemeManager('/vault')) - await waitFor(() => { - expect(result.current.activeTheme).not.toBeNull() - }) - - const root = document.documentElement - expect(root.style.getPropertyValue('--bg-primary')).toBe('#FFFFFF') - expect(root.style.getPropertyValue('--text-primary')).toBe('#1A1A2E') - expect(root.style.getPropertyValue('--text-heading')).toBe('#1A1A2E') - expect(root.style.getPropertyValue('--border-primary')).toBe('#E2E8F0') - }) - - it('sets color-scheme and data-theme-mode for dark theme', async () => { - const { result } = renderHook(() => useThemeManager('/vault')) - await waitFor(() => { - expect(result.current.themes).toHaveLength(2) - }) - - await act(async () => { - await result.current.switchTheme('dark') - }) - - const root = document.documentElement - await waitFor(() => { - expect(root.style.getPropertyValue('color-scheme')).toBe('dark') - }) - expect(root.dataset.themeMode).toBe('dark') - expect(root.style.getPropertyValue('--bg-primary')).toBe('#0F0F23') - expect(root.style.getPropertyValue('--text-primary')).toBe('#E2E8F0') - }) - - it('sets color-scheme to light for light theme', async () => { - const { result } = renderHook(() => useThemeManager('/vault')) - await waitFor(() => { - expect(result.current.activeTheme).not.toBeNull() - }) - - const root = document.documentElement - expect(root.style.getPropertyValue('color-scheme')).toBe('light') - expect(root.dataset.themeMode).toBe('light') - }) - - it('updates derived variables when switching between themes', async () => { - const { result } = renderHook(() => useThemeManager('/vault')) - await waitFor(() => { - expect(result.current.themes).toHaveLength(2) - }) - - await act(async () => { - await result.current.switchTheme('dark') - }) - - const root = document.documentElement - await waitFor(() => { - expect(root.style.getPropertyValue('--bg-primary')).toBe('#0F0F23') - }) - - await act(async () => { - await result.current.switchTheme('default') - }) - - await waitFor(() => { - expect(root.style.getPropertyValue('--bg-primary')).toBe('#FFFFFF') - }) - expect(root.style.getPropertyValue('color-scheme')).toBe('light') - expect(root.dataset.themeMode).toBe('light') - }) - - it('reloadThemes re-fetches theme list', async () => { - const { result } = renderHook(() => useThemeManager('/vault')) -======= it('re-applies theme when active content changes in allContent', async () => { const { result, rerender } = renderHook( ({ content }) => useThemeManager('/vault', entries, content), { initialProps: { content: allContent } }, ) ->>>>>>> 240be0d (wip: themes-editable — theme management system WIP (13 files, 1014 insertions)) await waitFor(() => { expect(result.current.activeTheme).not.toBeNull() }) diff --git a/src/hooks/useThemeManager.ts b/src/hooks/useThemeManager.ts index 3ef828ea..7906e437 100644 --- a/src/hooks/useThemeManager.ts +++ b/src/hooks/useThemeManager.ts @@ -8,122 +8,6 @@ function tauriCall(command: string, args: Record): Promise(command, args) : mockInvoke(command, args) } -<<<<<<< HEAD -// --- Color utilities for theme variable derivation --- - -function parseHex(hex: string): [number, number, number] { - const h = hex.replace('#', '') - return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)] -} - -function toHex(r: number, g: number, b: number): string { - return '#' + [r, g, b].map(c => Math.round(Math.max(0, Math.min(255, c))).toString(16).padStart(2, '0')).join('') -} - -/** Blend two hex colors. ratio=0 → color1, ratio=1 → color2. */ -function mixColors(hex1: string, hex2: string, ratio: number): string { - const [r1, g1, b1] = parseHex(hex1) - const [r2, g2, b2] = parseHex(hex2) - return toHex(r1 + (r2 - r1) * ratio, g1 + (g2 - g1) * ratio, b1 + (b2 - b1) * ratio) -} - -/** Check if a hex color is perceptually dark (luminance < 0.5). */ -export function isColorDark(hex: string): boolean { - const [r, g, b] = parseHex(hex) - return (0.299 * r + 0.587 * g + 0.114 * b) / 255 < 0.5 -} - -// Variables derived from theme core colors (not present in theme.colors directly) -const DERIVED_VAR_NAMES = [ - 'bg-primary', 'bg-sidebar', 'bg-card', 'bg-hover', 'bg-hover-subtle', 'bg-selected', - 'bg-input', 'bg-button', 'bg-dialog', - 'text-primary', 'text-heading', 'text-secondary', 'text-tertiary', 'text-muted', 'text-faint', - 'border-primary', 'border-subtle', 'border-input', 'border-dialog', - 'link-color', 'link-hover', - // shadcn variables that may not be in the theme - 'card', 'card-foreground', 'popover', 'popover-foreground', - 'secondary', 'secondary-foreground', 'muted-foreground', - 'accent', 'accent-foreground', 'input', 'ring', - 'sidebar-foreground', 'sidebar-primary', 'sidebar-primary-foreground', - 'sidebar-accent', 'sidebar-accent-foreground', 'sidebar-border', 'sidebar-ring', -] - -/** Derive app-specific and missing shadcn CSS variables from core theme colors. */ -function deriveThemeVariables(root: HTMLElement, colors: Record): void { - const bg = colors.background - const fg = colors.foreground - if (!bg || !fg) return - - const isDark = isColorDark(bg) - root.style.setProperty('color-scheme', isDark ? 'dark' : 'light') - root.dataset.themeMode = isDark ? 'dark' : 'light' - - const primary = colors.primary ?? (isDark ? '#5C9CFF' : '#155DFF') - const border = colors.border ?? mixColors(bg, fg, isDark ? 0.15 : 0.08) - const muted = colors.muted ?? mixColors(bg, fg, isDark ? 0.08 : 0.05) - const sidebarBg = colors['sidebar-background'] ?? mixColors(bg, fg, 0.04) - - // App-specific variables - root.style.setProperty('--bg-primary', bg) - root.style.setProperty('--bg-sidebar', sidebarBg) - root.style.setProperty('--bg-card', mixColors(bg, fg, 0.03)) - root.style.setProperty('--bg-hover', mixColors(bg, fg, 0.1)) - root.style.setProperty('--bg-hover-subtle', muted) - root.style.setProperty('--bg-selected', `${primary}25`) - root.style.setProperty('--bg-input', bg) - root.style.setProperty('--bg-button', mixColors(bg, fg, 0.1)) - root.style.setProperty('--bg-dialog', mixColors(bg, fg, 0.02)) - - root.style.setProperty('--text-primary', fg) - root.style.setProperty('--text-heading', fg) - root.style.setProperty('--text-secondary', mixColors(fg, bg, 0.25)) - root.style.setProperty('--text-tertiary', mixColors(fg, bg, 0.35)) - root.style.setProperty('--text-muted', mixColors(fg, bg, 0.5)) - root.style.setProperty('--text-faint', mixColors(fg, bg, 0.6)) - - root.style.setProperty('--border-primary', border) - root.style.setProperty('--border-subtle', border) - root.style.setProperty('--border-input', border) - root.style.setProperty('--border-dialog', border) - - root.style.setProperty('--link-color', primary) - root.style.setProperty('--link-hover', mixColors(primary, fg, 0.2)) - - // Shadcn variables — only set if not already provided by the theme - const setIfMissing = (name: string, value: string) => { - if (!(name in colors)) root.style.setProperty(`--${name}`, value) - } - setIfMissing('card', mixColors(bg, fg, 0.03)) - setIfMissing('card-foreground', fg) - setIfMissing('popover', mixColors(bg, fg, 0.04)) - setIfMissing('popover-foreground', fg) - setIfMissing('secondary', mixColors(bg, fg, 0.08)) - setIfMissing('secondary-foreground', fg) - setIfMissing('muted-foreground', mixColors(fg, bg, 0.3)) - setIfMissing('accent', mixColors(bg, fg, 0.08)) - setIfMissing('accent-foreground', fg) - setIfMissing('input', border) - setIfMissing('ring', primary) - setIfMissing('sidebar-foreground', fg) - setIfMissing('sidebar-accent', mixColors(sidebarBg, fg, 0.1)) - setIfMissing('sidebar-accent-foreground', fg) - setIfMissing('sidebar-border', border) - setIfMissing('sidebar-primary', primary) - setIfMissing('sidebar-primary-foreground', '#FFFFFF') - setIfMissing('sidebar-ring', primary) -} - -function clearDerivedVariables(root: HTMLElement): void { - for (const name of DERIVED_VAR_NAMES) { - root.style.removeProperty(`--${name}`) - } - root.style.removeProperty('color-scheme') - delete root.dataset.themeMode -} - -/** Map theme colors/typography/spacing to CSS custom properties on :root. */ -function applyThemeToDom(theme: ThemeFile): void { -======= /** Frontmatter keys that are metadata — not CSS custom properties. */ const NON_THEME_KEYS = new Set([ 'Is A', 'type', 'is_a', 'is a', @@ -158,12 +42,10 @@ export function extractCssVars(content: string): Record { } function applyVarsToDom(vars: Record): void { ->>>>>>> 240be0d (wip: themes-editable — theme management system WIP (13 files, 1014 insertions)) const root = document.documentElement for (const [key, value] of Object.entries(vars)) { root.style.setProperty(key, value) } - deriveThemeVariables(root, theme.colors) } function clearVarsFromDom(vars: Record): void { @@ -184,19 +66,11 @@ function entryToThemeFile(entry: VaultEntry): ThemeFile { typography: {}, spacing: {}, } -<<<<<<< HEAD - for (const key of Object.keys(theme.spacing)) { - root.style.removeProperty(`--theme-${key}`) - } - root.style.removeProperty('--sidebar') - clearDerivedVariables(root) -======= } /** True when a theme entry should no longer be applied (trashed or archived). */ function isEntryRemoved(entry: VaultEntry): boolean { return entry.trashed || entry.archived ->>>>>>> 240be0d (wip: themes-editable — theme management system WIP (13 files, 1014 insertions)) } export interface ThemeManager { @@ -213,14 +87,7 @@ export interface ThemeManager { function useThemeSetting(vaultPath: string | null) { const [activeThemeId, setActiveThemeId] = useState(null) -<<<<<<< HEAD - const activeTheme = themes.find(t => t.id === activeThemeId) ?? null - const isDark = activeTheme?.colors.background ? isColorDark(activeTheme.colors.background) : false - - const loadThemes = useCallback(async () => { -======= const load = useCallback(async () => { ->>>>>>> 240be0d (wip: themes-editable — theme management system WIP (13 files, 1014 insertions)) if (!vaultPath) return try { const s = await tauriCall('get_vault_settings', { vaultPath }) @@ -318,11 +185,7 @@ export function useThemeManager( } catch (err) { console.error('Failed to create theme:', err); return '' } }, [vaultPath, setActiveThemeId]) -<<<<<<< HEAD - return { themes, activeThemeId, activeTheme, isDark, switchTheme, createTheme, reloadThemes: loadThemes } -======= const reloadThemes = useCallback(async () => { await reload() }, [reload]) return { themes, activeThemeId, activeTheme, switchTheme, createTheme, reloadThemes } ->>>>>>> 240be0d (wip: themes-editable — theme management system WIP (13 files, 1014 insertions)) }