fix: live-reload CSS vars when saving active theme note in editor

When user edits a theme note directly in the editor and presses Cmd+S,
the app now immediately re-applies CSS variables — no manual reload
needed. Added notifyThemeSaved(path, content) to ThemeManager; wired
into onNotePersisted callback so saving the active theme updates
cachedThemeContent, triggering useThemeApplier.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
lucaronin
2026-03-11 16:31:03 +01:00
parent 484324ae3f
commit 517a2af730
5 changed files with 69 additions and 7 deletions

View File

@@ -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])

View File

@@ -13,8 +13,8 @@ interface EditorSaveConfig {
setTabs: (fn: SetStateAction<any[]>) => 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

View File

@@ -8,7 +8,7 @@ export function useEditorSaveWithLinks(config: {
setTabs: Parameters<typeof useEditorSave>[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) => {

View File

@@ -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(() => {

View File

@@ -124,6 +124,8 @@ export interface ThemeManager {
reloadThemes: () => Promise<void>
/** Update a single frontmatter property on the active theme note. */
updateThemeProperty: (key: string, value: string) => Promise<void>
/** 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,
}
}