diff --git a/src/App.tsx b/src/App.tsx index 86e7a323..49ee7e44 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -306,10 +306,16 @@ function App() { triggerIncrementalIndex() }, [vault, triggerIncrementalIndex]) + const { notifyThemeSaved } = themeManager + const onNotePersisted = useCallback((path: string, content: string) => { + vault.clearUnsaved(path) + notifyThemeSaved(path, content) + }, [vault, notifyThemeSaved]) + const { handleSave: handleSaveRaw, handleContentChange, savePendingForPath, savePending } = useEditorSaveWithLinks({ updateEntry: vault.updateEntry, setTabs: notes.setTabs, setToastMessage, onAfterSave, - onNotePersisted: vault.clearUnsaved, + onNotePersisted, }) useEffect(() => { contentChangeRef.current = handleContentChange }, [handleContentChange]) diff --git a/src/hooks/useEditorSave.ts b/src/hooks/useEditorSave.ts index 066781ea..51ea173c 100644 --- a/src/hooks/useEditorSave.ts +++ b/src/hooks/useEditorSave.ts @@ -13,8 +13,8 @@ interface EditorSaveConfig { setTabs: (fn: SetStateAction) => void setToastMessage: (msg: string | null) => void onAfterSave?: () => void - /** Called after content is persisted — used to clear unsaved state. */ - onNotePersisted?: (path: string) => void + /** Called after content is persisted — used to clear unsaved state and live-reload themes. */ + onNotePersisted?: (path: string, content: string) => void } /** @@ -41,8 +41,9 @@ export function useEditorSave({ updateVaultContent, setTabs, setToastMessage, on if (!pending) return false if (pathFilter && pending.path !== pathFilter) return false await saveNote(pending.path, pending.content) + const savedContent = pending.content pendingContentRef.current = null - onNotePersisted?.(pending.path) + onNotePersisted?.(pending.path, savedContent) return true }, [saveNote, onNotePersisted]) @@ -53,7 +54,7 @@ export function useEditorSave({ updateVaultContent, setTabs, setToastMessage, on const saved = await flushPending() if (!saved && unsavedFallback) { await saveNote(unsavedFallback.path, unsavedFallback.content) - onNotePersisted?.(unsavedFallback.path) + onNotePersisted?.(unsavedFallback.path, unsavedFallback.content) setToastMessage('Saved') onAfterSave() return diff --git a/src/hooks/useEditorSaveWithLinks.ts b/src/hooks/useEditorSaveWithLinks.ts index dc388996..29d70ab6 100644 --- a/src/hooks/useEditorSaveWithLinks.ts +++ b/src/hooks/useEditorSaveWithLinks.ts @@ -8,7 +8,7 @@ export function useEditorSaveWithLinks(config: { setTabs: Parameters[0]['setTabs'] setToastMessage: (msg: string | null) => void onAfterSave: () => void - onNotePersisted?: (path: string) => void + onNotePersisted?: (path: string, content: string) => void }) { const { updateEntry } = config const saveContent = useCallback((path: string, content: string) => { diff --git a/src/hooks/useThemeManager.test.ts b/src/hooks/useThemeManager.test.ts index 0658a1b9..15aca1f2 100644 --- a/src/hooks/useThemeManager.test.ts +++ b/src/hooks/useThemeManager.test.ts @@ -441,6 +441,55 @@ describe('useThemeManager', () => { expect(result.current.isDark).toBe(false) }) + it('notifyThemeSaved updates CSS vars when active theme is saved', async () => { + const { result } = renderHook(() => + useThemeManager('/vault', entries, allContent) + ) + await waitFor(() => { + expect(result.current.activeThemeId).toBe(THEME_PATH_DEFAULT) + }) + expect(document.documentElement.style.getPropertyValue('--background')).toBe('#FFFFFF') + + const updatedContent = `--- +type: Theme +Description: Light theme +background: "#1a1a2e" +foreground: "#e0e0e0" +primary: "#155DFF" +sidebar: "#2a2a3e" +text-primary: "#e0e0e0" +--- + +# Default Theme +` + act(() => { + result.current.notifyThemeSaved(THEME_PATH_DEFAULT, updatedContent) + }) + + await waitFor(() => { + expect(document.documentElement.style.getPropertyValue('--background')).toBe('#1a1a2e') + }) + expect(document.documentElement.style.getPropertyValue('--sidebar')).toBe('#2a2a3e') + }) + + it('notifyThemeSaved is a no-op for non-active theme path', async () => { + const { result } = renderHook(() => + useThemeManager('/vault', entries, allContent) + ) + await waitFor(() => { + expect(result.current.activeThemeId).toBe(THEME_PATH_DEFAULT) + }) + expect(document.documentElement.style.getPropertyValue('--background')).toBe('#FFFFFF') + + act(() => { + result.current.notifyThemeSaved(THEME_PATH_DARK, DARK_THEME_CONTENT) + }) + + // Background should still be the default theme's white, not dark + await new Promise(r => setTimeout(r, 50)) + expect(document.documentElement.style.getPropertyValue('--background')).toBe('#FFFFFF') + }) + it('calls ensure_vault_themes on mount with vaultPath', async () => { renderHook(() => useThemeManager('/vault', entries, allContent)) await waitFor(() => { diff --git a/src/hooks/useThemeManager.ts b/src/hooks/useThemeManager.ts index e67cfc64..a3ce7ef8 100644 --- a/src/hooks/useThemeManager.ts +++ b/src/hooks/useThemeManager.ts @@ -124,6 +124,8 @@ export interface ThemeManager { reloadThemes: () => Promise /** Update a single frontmatter property on the active theme note. */ updateThemeProperty: (key: string, value: string) => Promise + /** Notify that the active theme note was saved with new content (live-reload on Cmd+S). */ + notifyThemeSaved: (path: string, content: string) => void } /** Manages loading and persisting the active theme path from vault settings. */ @@ -275,6 +277,10 @@ export function useThemeManager( const reloadThemes = useCallback(async () => { await reload() }, [reload]) + const notifyThemeSaved = useCallback((path: string, content: string) => { + if (path === activeThemeId) setCachedThemeContent(content) + }, [activeThemeId]) + const updateThemeProperty = useCallback(async (key: string, value: string) => { if (!activeThemeId) return try { @@ -290,6 +296,6 @@ export function useThemeManager( return { themes, activeThemeId, activeTheme, activeThemeContent: cachedThemeContent, - isDark, switchTheme, createTheme, reloadThemes, updateThemeProperty, + isDark, switchTheme, createTheme, reloadThemes, updateThemeProperty, notifyThemeSaved, } }