diff --git a/frontend/src/sidebar/Sidebar.tsx b/frontend/src/sidebar/Sidebar.tsx index 4bacf50..fae985d 100644 --- a/frontend/src/sidebar/Sidebar.tsx +++ b/frontend/src/sidebar/Sidebar.tsx @@ -308,9 +308,6 @@ const Sidebar: React.FC = ({ neutralChroma, setNeutralChroma, resetNeutralChroma, - neutralContrast, - setNeutralContrast, - resetNeutralContrast, themeMode, cycleThemeMode, themeModes, @@ -343,7 +340,6 @@ const Sidebar: React.FC = ({ }, [collapseSidebar]); const neutralHueInputId = useId(); const neutralChromaInputId = useId(); - const neutralContrastInputId = useId(); const sortedCorrespondents = useMemo(() => { if (!Array.isArray(correspondents)) { return []; @@ -446,8 +442,7 @@ const Sidebar: React.FC = ({ const handleThemeAdjustmentsReset = useCallback(() => { resetNeutralHue(); resetNeutralChroma(); - resetNeutralContrast(); - }, [resetNeutralHue, resetNeutralChroma, resetNeutralContrast]); + }, [resetNeutralHue, resetNeutralChroma]); const handleNeutralHueChange = useCallback( (value: string) => { @@ -479,21 +474,6 @@ const Sidebar: React.FC = ({ [resetNeutralChroma, setNeutralChroma], ); - const handleNeutralContrastChange = useCallback( - (value: string) => { - if (value === '') { - resetNeutralContrast(); - return; - } - const parsed = Number(value); - if (Number.isNaN(parsed)) { - return; - } - setNeutralContrast(parsed); - }, - [resetNeutralContrast, setNeutralContrast], - ); - const formatSliderValue = useCallback((value: number | string) => { const parsed = Number(value); if (Number.isNaN(parsed)) { @@ -574,20 +554,6 @@ const Sidebar: React.FC = ({ /> {formatSliderValue(neutralChroma)} - ) : null; diff --git a/frontend/src/sidebar/SidebarContext.tsx b/frontend/src/sidebar/SidebarContext.tsx index ff6caa5..f15953b 100644 --- a/frontend/src/sidebar/SidebarContext.tsx +++ b/frontend/src/sidebar/SidebarContext.tsx @@ -9,7 +9,6 @@ import React, { import { DARK_MODE_MEDIA_QUERY, DEFAULT_NEUTRAL_CHROMA, - DEFAULT_NEUTRAL_CONTRAST, DEFAULT_NEUTRAL_HUE, DEFAULT_THEME_MODE, SIDEBAR_COLLAPSE_STORAGE_KEY, @@ -26,9 +25,6 @@ interface SidebarContextValue { neutralChroma: number; setNeutralChroma: (value: number | string) => void; resetNeutralChroma: () => void; - neutralContrast: number; - setNeutralContrast: (value: number | string) => void; - resetNeutralContrast: () => void; themeMode: ThemeMode; setThemeMode: (mode: ThemeMode) => void; cycleThemeMode: () => void; @@ -43,7 +39,6 @@ const loadInitialThemeSettings = () => { const defaults = { neutralHue: DEFAULT_NEUTRAL_HUE, neutralChroma: DEFAULT_NEUTRAL_CHROMA, - neutralContrast: DEFAULT_NEUTRAL_CONTRAST, mode: DEFAULT_THEME_MODE, }; @@ -68,14 +63,12 @@ const loadInitialThemeSettings = () => { return { neutralHue: Number.isNaN(parsed) ? defaults.neutralHue : parsed, neutralChroma: readNumberVar('--neutral-chroma', defaults.neutralChroma), - neutralContrast: readNumberVar('--neutral-contrast', defaults.neutralContrast), }; }; const rootValues = loadFromRoot(); let neutralHueValue = rootValues.neutralHue; let neutralChromaValue = rootValues.neutralChroma; - let neutralContrastValue = rootValues.neutralContrast; let modeValue = defaults.mode; const composite = window.localStorage.getItem(THEME_STORAGE_KEY); @@ -90,10 +83,7 @@ const loadInitialThemeSettings = () => { if (!Number.isNaN(storedChroma)) { neutralChromaValue = Math.min(Math.max(storedChroma, 0), 1); } - const storedContrast = Number.parseFloat(parsed?.neutralContrast); - if (!Number.isNaN(storedContrast)) { - neutralContrastValue = Math.min(Math.max(storedContrast, 0), 1); - } + const storedMode = parsed?.mode; if (THEME_MODES.includes(storedMode)) { modeValue = storedMode; @@ -118,7 +108,6 @@ const loadInitialThemeSettings = () => { return { neutralHue: neutralHueValue, neutralChroma: neutralChromaValue, - neutralContrast: neutralContrastValue, mode: modeValue, }; }; @@ -143,7 +132,6 @@ export const SidebarProvider = ({ initialCollapsed = false, children }) => { const initialTheme = useMemo(() => loadInitialThemeSettings(), []); const [neutralHue, setNeutralHueState] = useState(initialTheme.neutralHue); const [neutralChroma, setNeutralChromaState] = useState(initialTheme.neutralChroma); - const [neutralContrast, setNeutralContrastState] = useState(initialTheme.neutralContrast); const [themeMode, setThemeModeState] = useState(initialTheme.mode); const [systemPrefersDark, setSystemPrefersDark] = useState(() => { if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') { @@ -160,10 +148,6 @@ export const SidebarProvider = ({ initialCollapsed = false, children }) => { document.documentElement.style.setProperty('--neutral-chroma', String(neutralChroma)); }, [neutralChroma]); - useEffect(() => { - document.documentElement.style.setProperty('--neutral-contrast', String(neutralContrast)); - }, [neutralContrast]); - useEffect(() => { if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') { return () => undefined; @@ -198,7 +182,6 @@ export const SidebarProvider = ({ initialCollapsed = false, children }) => { const payload = JSON.stringify({ neutralHue, neutralChroma, - neutralContrast, mode: themeMode, }); window.localStorage.setItem(THEME_STORAGE_KEY, payload); @@ -207,7 +190,7 @@ export const SidebarProvider = ({ initialCollapsed = false, children }) => { } catch (error) { console.warn('[theme] failed to persist theme settings', error); } - }, [neutralHue, neutralChroma, neutralContrast, themeMode]); + }, [neutralHue, neutralChroma, themeMode]); const setNeutralHue = useCallback((value) => { setNeutralHueState((prev) => { @@ -241,24 +224,6 @@ export const SidebarProvider = ({ initialCollapsed = false, children }) => { setNeutralChromaState(DEFAULT_NEUTRAL_CHROMA); }, []); - const setNeutralContrast = useCallback((value) => { - setNeutralContrastState((prev) => { - if (value === '' || value === null || value === undefined) { - return DEFAULT_NEUTRAL_CONTRAST; - } - const parsed = Number(value); - if (Number.isNaN(parsed)) { - return prev; - } - const clamped = Math.min(Math.max(parsed, 0), 1); - return Math.round(clamped * 1000) / 1000; - }); - }, []); - - const resetNeutralContrast = useCallback(() => { - setNeutralContrastState(DEFAULT_NEUTRAL_CONTRAST); - }, []); - const resetNeutralHue = useCallback(() => { setNeutralHueState(DEFAULT_NEUTRAL_HUE); }, []); @@ -296,18 +261,14 @@ export const SidebarProvider = ({ initialCollapsed = false, children }) => { setNeutralHue, neutralChroma, setNeutralChroma, - neutralContrast, - setNeutralContrast, resetNeutralHue, resetNeutralChroma, - resetNeutralContrast, themeMode, setThemeMode, cycleThemeMode, themeModes: THEME_MODES, defaultNeutralHue: DEFAULT_NEUTRAL_HUE, defaultNeutralChroma: DEFAULT_NEUTRAL_CHROMA, - defaultNeutralContrast: DEFAULT_NEUTRAL_CONTRAST, }), [ collapsed, @@ -316,11 +277,8 @@ export const SidebarProvider = ({ initialCollapsed = false, children }) => { setNeutralHue, neutralChroma, setNeutralChroma, - neutralContrast, - setNeutralContrast, resetNeutralHue, resetNeutralChroma, - resetNeutralContrast, themeMode, setThemeMode, cycleThemeMode, diff --git a/frontend/src/styles/base/theme.css b/frontend/src/styles/base/theme.css index 0d1b894..04dd369 100644 --- a/frontend/src/styles/base/theme.css +++ b/frontend/src/styles/base/theme.css @@ -11,14 +11,8 @@ --neutral-chroma-min: 0.01; --neutral-chroma-max: 0.20; --neutral-chroma-range: calc(var(--neutral-chroma-max) - var(--neutral-chroma-min)); - --neutral-chroma-effective: calc( - var(--neutral-chroma-min) - + clamp(0, var(--neutral-chroma), 1) * var(--neutral-chroma-range) - ); - --neutral-contrast: 0.56; - --neutral-contrast-amount: calc(0.85 + var(--neutral-contrast) * 0.55); - --neutral-contrast-baseline: 1.125; - --neutral-contrast-delta: calc(var(--neutral-contrast-amount) - var(--neutral-contrast-baseline)); + --neutral-chroma-effective: calc(var(--neutral-chroma-min) + clamp(0, var(--neutral-chroma), 1) * var(--neutral-chroma-range)); + --neutral-contrast-delta: 0.033; --neutral-chroma-scale: calc(clamp(0.0001, var(--neutral-chroma-effective), 1) / 0.1); --tag-chip-default-l: 0.9; --tag-chip-bg-lighten: 0.4; @@ -245,4 +239,4 @@ a:focus-visible { #app { height: 100%; -} +} \ No newline at end of file diff --git a/frontend/src/ui/useFloatingMenu.ts b/frontend/src/ui/useFloatingMenu.ts index 67c90ab..e359611 100644 --- a/frontend/src/ui/useFloatingMenu.ts +++ b/frontend/src/ui/useFloatingMenu.ts @@ -218,8 +218,8 @@ const useFloatingMenu = ({ const raf = window.requestAnimationFrame; const rafId = raf ? raf(() => { - ignoreFocusEvents = false; - }) + ignoreFocusEvents = false; + }) : null; if (!anchorRef?.current) { @@ -231,9 +231,18 @@ const useFloatingMenu = ({ if (event.type === 'focusin' && ignoreFocusEvents) { return; } + const target = event.target; + // If the target is no longer in the document, it means it was unmounted + // (e.g. due to a re-render caused by the open action). + // In this case, we should ignore the event. + if (target instanceof Node && !document.contains(target)) { + return; + } + const menu = menuRef.current; const anchor = anchorRef?.current; - if ((anchor && anchor.contains(event.target)) || (menu && menu.contains(event.target))) { + + if ((anchor && anchor.contains(target)) || (menu && menu.contains(target))) { return; } close(); @@ -245,11 +254,16 @@ const useFloatingMenu = ({ } }; - document.addEventListener('mousedown', handlePointer); - document.addEventListener('touchstart', handlePointer, { passive: true }); - document.addEventListener('focusin', handlePointer); - document.addEventListener('keydown', handleKeyDown); + // Delay adding listeners to avoid capturing the event that opened the menu + const timer = setTimeout(() => { + document.addEventListener('mousedown', handlePointer); + document.addEventListener('touchstart', handlePointer, { passive: true }); + document.addEventListener('focusin', handlePointer); + document.addEventListener('keydown', handleKeyDown); + }, 0); + return () => { + clearTimeout(timer); if (rafId != null) { window.cancelAnimationFrame(rafId); }