diff --git a/src/renderer/src/components/right-sidebar/use-plugin-panel-theme-revision.test.tsx b/src/renderer/src/components/right-sidebar/use-plugin-panel-theme-revision.test.tsx index 450a5f827d5..39a2915f637 100644 --- a/src/renderer/src/components/right-sidebar/use-plugin-panel-theme-revision.test.tsx +++ b/src/renderer/src/components/right-sidebar/use-plugin-panel-theme-revision.test.tsx @@ -67,4 +67,40 @@ describe('usePluginPanelThemeRevision', () => { expect(revisions.at(-1)).toBe(before) }) + + // The revision keys the panel iframe, so a bump here destroys in-panel state. + it('ignores root custom properties the panel shell does not bake in', async () => { + const { revisions } = renderProbe() + const before = revisions.at(-1) + + // Written every rAF while dragging the sidebar divider. + for (const width of [240, 241, 242, 243]) { + document.documentElement.style.setProperty('--workspace-sidebar-live-width', `${width}px`) + await flushObserver() + } + + expect(revisions.at(-1)).toBe(before) + }) + + it('ignores a class change that leaves the color scheme alone', async () => { + const { revisions } = renderProbe() + const before = revisions.at(-1) + + document.documentElement.classList.add('theme-transition-disabled') + await flushObserver() + + expect(revisions.at(-1)).toBe(before) + }) + + it('bumps once when a token settles on a new value', async () => { + const { revisions } = renderProbe() + const before = revisions.at(-1) + + document.documentElement.style.setProperty('--background', '#111') + await flushObserver() + document.documentElement.style.setProperty('--background', '#111') + await flushObserver() + + expect(revisions.at(-1)).toBe(before! + 1) + }) }) diff --git a/src/renderer/src/components/right-sidebar/use-plugin-panel-theme-revision.ts b/src/renderer/src/components/right-sidebar/use-plugin-panel-theme-revision.ts index 74b9eb70440..98894a9785e 100644 --- a/src/renderer/src/components/right-sidebar/use-plugin-panel-theme-revision.ts +++ b/src/renderer/src/components/right-sidebar/use-plugin-panel-theme-revision.ts @@ -1,10 +1,28 @@ -import { useEffect, useState } from 'react' +import { useEffect, useRef, useState } from 'react' +import { buildPanelDesignTokenCss, currentPanelColorScheme } from './plugin-panel-design-token-css' -/** Changes whenever root theme classes/tokens may have changed. */ +/** The exact shell inputs; anything else on the root must not rebuild the frame. */ +function readPanelThemeSnapshot(): string { + return `${currentPanelColorScheme()}|${buildPanelDesignTokenCss()}` +} + +/** Changes only when the values baked into the panel shell actually change. */ export function usePluginPanelThemeRevision(): number { const [revision, setRevision] = useState(0) + // Why: the revision keys the panel iframe, so remounting it destroys in-panel + // state. Root style writes fire at rAF cadence during a sidebar drag, so + // compare the baked values instead of counting mutations. + const snapshotRef = useRef(null) useEffect(() => { - const observer = new MutationObserver(() => setRevision((current) => current + 1)) + snapshotRef.current ??= readPanelThemeSnapshot() + const observer = new MutationObserver(() => { + const snapshot = readPanelThemeSnapshot() + if (snapshot === snapshotRef.current) { + return + } + snapshotRef.current = snapshot + setRevision((current) => current + 1) + }) observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'style']