From 6ea920e9a70170eced025d77682dbe642f387e96 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Sun, 5 Oct 2025 11:29:17 +0100 Subject: [PATCH] Refactor legacy UI: performance, accessibility, and styling improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Performance: - Extract geometry calculation logic into useToolPanelGeometry hook with debounced resize handling (150ms) - Create reusable useLocalStorageState hook for cleaner state persistence - Optimize hook dependencies and memoization Code Organization: - Extract 80+ lines of geometry logic into dedicated hook - Create useFocusTrap hook for accessibility features - Add ToolPanelGeometry interface for type safety - Reduce ToolPanel component complexity Accessibility: - Add focus trap to legacy surface for keyboard navigation - Implement Tab/Shift+Tab cycling within modal - Respect prefers-reduced-motion for animations UX Improvements: - Add smooth exit animation for legacy mode (220ms slide-out) - Skip animations when reduced motion is preferred - Tool panel mode preference already persisted via ToolWorkflowContext Styling: - Add 27 CSS custom properties to centralize color-mix patterns - Replace pink accent colors with neutral tones matching UI - Remove gradient from legacy body background - Maintain tool item borders and hover effects with neutral colors 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../components/tools/LegacyToolSurface.tsx | 33 ++++- frontend/src/components/tools/ToolPanel.css | 121 +++++++++++++----- frontend/src/components/tools/ToolPanel.tsx | 82 ++---------- frontend/src/hooks/tools/useFocusTrap.ts | 74 +++++++++++ .../src/hooks/tools/useLocalStorageState.ts | 30 +++++ .../src/hooks/tools/useToolPanelGeometry.ts | 101 +++++++++++++++ 6 files changed, 330 insertions(+), 111 deletions(-) create mode 100644 frontend/src/hooks/tools/useFocusTrap.ts create mode 100644 frontend/src/hooks/tools/useLocalStorageState.ts create mode 100644 frontend/src/hooks/tools/useToolPanelGeometry.ts diff --git a/frontend/src/components/tools/LegacyToolSurface.tsx b/frontend/src/components/tools/LegacyToolSurface.tsx index 995879205..a621bc554 100644 --- a/frontend/src/components/tools/LegacyToolSurface.tsx +++ b/frontend/src/components/tools/LegacyToolSurface.tsx @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useEffect, useState, useRef } from 'react'; import { ActionIcon, Group, ScrollArea, Switch, Text, Tooltip } from '@mantine/core'; import ViewSidebarRoundedIcon from '@mui/icons-material/ViewSidebarRounded'; import { useTranslation } from 'react-i18next'; @@ -6,6 +6,7 @@ import ToolSearch from './toolPicker/ToolSearch'; import LegacyToolList from './LegacyToolList'; import { ToolRegistryEntry } from '../../data/toolsTaxonomy'; import { ToolId } from '../../types/toolId'; +import { useFocusTrap } from '../../hooks/tools/useFocusTrap'; import './ToolPanel.css'; interface LegacyToolSurfaceProps { @@ -43,17 +44,36 @@ const LegacyToolSurface = ({ geometry, }: LegacyToolSurfaceProps) => { const { t } = useTranslation(); + const [isExiting, setIsExiting] = useState(false); + const surfaceRef = useRef(null); + + // Enable focus trap when surface is active + useFocusTrap(surfaceRef, !isExiting); + + const handleExit = () => { + const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + + if (prefersReducedMotion) { + onExitLegacyMode(); + return; + } + + setIsExiting(true); + setTimeout(() => { + onExitLegacyMode(); + }, 220); // Match animation duration (0.22s) + }; useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') { - onExitLegacyMode(); + handleExit(); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); - }, [onExitLegacyMode]); + }, []); const style = geometry ? { @@ -71,7 +91,10 @@ const LegacyToolSurface = ({ role="region" aria-label={t('toolPanel.legacy.heading', 'All tools (legacy view)')} > -
+
@@ -86,7 +109,7 @@ const LegacyToolSurface = ({ variant="subtle" radius="xl" size="lg" - onClick={onExitLegacyMode} + onClick={handleExit} aria-label={toggleLabel} > diff --git a/frontend/src/components/tools/ToolPanel.css b/frontend/src/components/tools/ToolPanel.css index 27eabf7c7..19536c361 100644 --- a/frontend/src/components/tools/ToolPanel.css +++ b/frontend/src/components/tools/ToolPanel.css @@ -1,3 +1,32 @@ +/* CSS Custom Properties for Legacy Mode */ +.tool-panel__legacy-surface-inner { + --legacy-bg-surface-1: color-mix(in srgb, var(--bg-toolbar) 96%, transparent); + --legacy-bg-surface-2: color-mix(in srgb, var(--bg-background) 90%, transparent); + --legacy-bg-header: color-mix(in srgb, var(--bg-toolbar) 86%, transparent); + --legacy-bg-controls-1: color-mix(in srgb, var(--bg-toolbar) 84%, transparent); + --legacy-bg-controls-2: color-mix(in srgb, var(--bg-background) 72%, transparent); + --legacy-bg-body-1: color-mix(in srgb, var(--bg-background) 86%, transparent); + --legacy-bg-body-2: color-mix(in srgb, var(--bg-toolbar) 78%, transparent); + --legacy-bg-group: color-mix(in srgb, var(--bg-toolbar) 82%, transparent); + --legacy-bg-item: color-mix(in srgb, var(--bg-toolbar) 88%, transparent); + --legacy-bg-list-item: color-mix(in srgb, var(--bg-toolbar) 86%, transparent); + --legacy-bg-icon-detailed: color-mix(in srgb, var(--bg-muted) 75%, transparent); + --legacy-bg-icon-compact: color-mix(in srgb, var(--bg-muted) 70%, transparent); + --legacy-border-subtle-75: color-mix(in srgb, var(--border-subtle) 75%, transparent); + --legacy-border-subtle-70: color-mix(in srgb, var(--border-subtle) 70%, transparent); + --legacy-border-subtle-65: color-mix(in srgb, var(--border-subtle) 65%, transparent); + --legacy-shadow-primary: color-mix(in srgb, var(--shadow-color, rgba(15, 23, 42, 0.55)) 25%, transparent); + --legacy-shadow-secondary: color-mix(in srgb, var(--shadow-color, rgba(15, 23, 42, 0.35)) 30%, transparent); + --legacy-shadow-group: color-mix(in srgb, var(--shadow-color, rgba(15, 23, 42, 0.45)) 18%, transparent); + --legacy-accent-hover: color-mix(in srgb, var(--text-primary) 20%, var(--border-subtle)); + --legacy-accent-selected: color-mix(in srgb, var(--text-primary) 30%, var(--border-subtle)); + --legacy-accent-ring: color-mix(in srgb, var(--text-primary) 15%, transparent); + --legacy-accent-list-bg: color-mix(in srgb, var(--text-primary) 8%, var(--bg-toolbar)); + --legacy-accent-list-border: color-mix(in srgb, var(--text-primary) 20%, var(--border-subtle)); + --legacy-text-icon: color-mix(in srgb, var(--text-primary) 90%, var(--text-muted)); + --legacy-text-icon-compact: color-mix(in srgb, var(--text-primary) 88%, var(--text-muted)); +} + .tool-panel { position: relative; transition: width 0.3s ease, max-width 0.3s ease; @@ -62,17 +91,21 @@ background: linear-gradient( 140deg, - color-mix(in srgb, var(--bg-toolbar) 96%, transparent), - color-mix(in srgb, var(--bg-background) 90%, transparent) + var(--legacy-bg-surface-1), + var(--legacy-bg-surface-2) ) padding-box; - border: 1px solid color-mix(in srgb, var(--border-subtle) 75%, transparent); + border: 1px solid var(--legacy-border-subtle-75); box-shadow: - 0 24px 64px color-mix(in srgb, var(--shadow-color, rgba(15, 23, 42, 0.55)) 25%, transparent), - 0 6px 18px color-mix(in srgb, var(--shadow-color, rgba(15, 23, 42, 0.35)) 30%, transparent); + 0 24px 64px var(--legacy-shadow-primary), + 0 6px 18px var(--legacy-shadow-secondary); backdrop-filter: blur(18px); overflow: hidden; - animation: tool-panel-legacy-slide 0.28s ease forwards; + animation: tool-panel-legacy-slide-in 0.28s ease forwards; +} + +.tool-panel__legacy-surface-inner--exiting { + animation: tool-panel-legacy-slide-out 0.22s ease forwards; } .tool-panel__legacy-header { @@ -81,10 +114,10 @@ align-items: flex-start; gap: 1rem; padding: 1.25rem 1.75rem; - border-bottom: 1px solid color-mix(in srgb, var(--border-subtle) 70%, transparent); + border-bottom: 1px solid var(--legacy-border-subtle-70); background: linear-gradient( 180deg, - color-mix(in srgb, var(--bg-toolbar) 86%, transparent), + var(--legacy-bg-header), transparent 85% ); } @@ -100,11 +133,11 @@ align-items: center; gap: 1rem; padding: 1rem 1.75rem; - border-bottom: 1px solid color-mix(in srgb, var(--border-subtle) 70%, transparent); + border-bottom: 1px solid var(--legacy-border-subtle-70); background: linear-gradient( 180deg, - color-mix(in srgb, var(--bg-toolbar) 84%, transparent), - color-mix(in srgb, var(--bg-background) 72%, transparent) + var(--legacy-bg-controls-1), + var(--legacy-bg-controls-2) ); } @@ -115,11 +148,7 @@ .tool-panel__legacy-body { flex: 1; min-height: 0; - background: linear-gradient( - 180deg, - color-mix(in srgb, var(--bg-background) 86%, transparent), - color-mix(in srgb, var(--bg-toolbar) 78%, transparent) - ); + background: transparent; } .tool-panel__legacy-scroll { @@ -149,9 +178,9 @@ margin: 0 0 1.5rem; padding: 0.65rem 0.75rem 1rem; border-radius: 1rem; - background: color-mix(in srgb, var(--bg-toolbar) 82%, transparent); - border: 1px solid color-mix(in srgb, var(--border-subtle) 65%, transparent); - box-shadow: 0 14px 32px color-mix(in srgb, var(--shadow-color, rgba(15, 23, 42, 0.45)) 18%, transparent); + background: var(--legacy-bg-group); + border: 1px solid var(--legacy-border-subtle-65); + box-shadow: 0 14px 32px var(--legacy-shadow-group); break-inside: avoid; backdrop-filter: blur(10px); } @@ -179,9 +208,9 @@ gap: 0.75rem; align-items: flex-start; padding: 0.85rem 0.95rem; - border: 1px solid color-mix(in srgb, var(--border-subtle) 70%, transparent); + border: 1px solid var(--legacy-border-subtle-70); border-radius: 0.95rem; - background: color-mix(in srgb, var(--bg-toolbar) 88%, transparent); + background: var(--legacy-bg-item); backdrop-filter: blur(6px); cursor: pointer; transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease; @@ -190,7 +219,7 @@ } .tool-panel__legacy-item:focus-visible { - outline: 2px solid var(--accent-primary, var(--mantine-color-pink-6)); + outline: 2px solid var(--legacy-accent-selected); outline-offset: 3px; } @@ -202,13 +231,13 @@ .tool-panel__legacy-item:hover:not([aria-disabled="true"]):not(:disabled) { transform: translateY(-2px); - border-color: color-mix(in srgb, var(--accent-primary, var(--mantine-color-pink-6)) 38%, var(--border-subtle)); + border-color: var(--legacy-accent-hover); box-shadow: var(--shadow-xl, 0 18px 34px rgba(15, 23, 42, 0.14)); } .tool-panel__legacy-item--selected { - border-color: color-mix(in srgb, var(--accent-primary, var(--mantine-color-pink-6)) 52%, var(--border-subtle)); - box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent-primary, var(--mantine-color-pink-6)) 28%, transparent); + border-color: var(--legacy-accent-selected); + box-shadow: 0 0 0 2px var(--legacy-accent-ring); } .tool-panel__legacy-item--detailed { @@ -222,8 +251,8 @@ width: 2.75rem; height: 2.75rem; border-radius: 0.75rem; - background: color-mix(in srgb, var(--bg-muted) 75%, transparent); - color: color-mix(in srgb, var(--text-primary) 90%, var(--text-muted)); + background: var(--legacy-bg-icon-detailed); + color: var(--legacy-text-icon); flex-shrink: 0; } @@ -265,7 +294,7 @@ border-radius: 0.65rem; cursor: pointer; transition: background 0.2s ease, transform 0.2s ease; - background: color-mix(in srgb, var(--bg-toolbar) 86%, transparent); + background: var(--legacy-bg-list-item); border: 1px solid transparent; width: 100%; box-sizing: border-box; @@ -279,8 +308,8 @@ .tool-panel__legacy-list-item:hover:not([aria-disabled="true"]):not(:disabled), .tool-panel__legacy-list-item--selected { - background: color-mix(in srgb, var(--accent-primary, var(--mantine-color-pink-6)) 22%, var(--bg-toolbar)); - border-color: color-mix(in srgb, var(--accent-primary, var(--mantine-color-pink-6)) 30%, var(--border-subtle)); + background: var(--legacy-accent-list-bg); + border-color: var(--legacy-accent-list-border); } .tool-panel__legacy-list-item--selected { @@ -294,8 +323,8 @@ width: 2.1rem; height: 2.1rem; border-radius: 0.6rem; - background: color-mix(in srgb, var(--bg-muted) 70%, transparent); - color: color-mix(in srgb, var(--text-primary) 88%, var(--text-muted)); + background: var(--legacy-bg-icon-compact); + color: var(--legacy-text-icon-compact); flex-shrink: 0; } @@ -314,7 +343,7 @@ gap: 0.75rem; } -@keyframes tool-panel-legacy-slide { +@keyframes tool-panel-legacy-slide-in { from { transform: translateX(-6%) scaleX(0.85); opacity: 0; @@ -325,6 +354,32 @@ } } +@keyframes tool-panel-legacy-slide-out { + from { + transform: translateX(0) scaleX(1); + opacity: 1; + } + to { + transform: translateX(-6%) scaleX(0.85); + opacity: 0; + } +} + +@media (prefers-reduced-motion: reduce) { + .tool-panel__legacy-surface-inner { + animation: none !important; + } + + .tool-panel__mode-toggle { + transition: none; + } + + .tool-panel__legacy-item, + .tool-panel__legacy-list-item { + transition: none; + } +} + @media (max-width: 1440px) { .tool-panel__legacy-content { padding-inline: 1.5rem; diff --git a/frontend/src/components/tools/ToolPanel.tsx b/frontend/src/components/tools/ToolPanel.tsx index 4bc583db3..75c1462ef 100644 --- a/frontend/src/components/tools/ToolPanel.tsx +++ b/frontend/src/components/tools/ToolPanel.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState, useLayoutEffect } from 'react'; +import { useMemo } from 'react'; import { useRainbowThemeContext } from '../shared/RainbowThemeProvider'; import { useToolWorkflow } from '../../contexts/ToolWorkflowContext'; import ToolPicker from './ToolPicker'; @@ -14,6 +14,8 @@ import ViewSidebarRoundedIcon from '@mui/icons-material/ViewSidebarRounded'; import DashboardCustomizeRoundedIcon from '@mui/icons-material/DashboardCustomizeRounded'; import { useTranslation } from 'react-i18next'; import LegacyToolSurface from './LegacyToolSurface'; +import { useToolPanelGeometry } from '../../hooks/tools/useToolPanelGeometry'; +import { useLocalStorageState } from '../../hooks/tools/useLocalStorageState'; import './ToolPanel.css'; // No props needed - component uses context @@ -42,81 +44,15 @@ export default function ToolPanel() { const isLegacyMode = toolPanelMode === 'legacy'; const legacyExpanded = isLegacyMode && leftPanelView === 'toolPicker' && !isMobile; - const [legacyGeometry, setLegacyGeometry] = useState<{ left: number; top: number; width: number; height: number } | null>(null); - const LEGACY_DESCRIPTION_STORAGE_KEY = 'legacyToolDescriptions'; - const [showLegacyDescriptions, setShowLegacyDescriptions] = useState(() => { - if (typeof window === 'undefined') { - return true; - } - - const stored = window.localStorage.getItem(LEGACY_DESCRIPTION_STORAGE_KEY); - if (stored === null) { - return true; - } - return stored === 'true'; + // Use custom hooks for state management + const [showLegacyDescriptions, setShowLegacyDescriptions] = useLocalStorageState('legacyToolDescriptions', true); + const legacyGeometry = useToolPanelGeometry({ + enabled: legacyExpanded, + toolPanelRef, + quickAccessRef, }); - useEffect(() => { - if (typeof window === 'undefined') { - return; - } - - window.localStorage.setItem(LEGACY_DESCRIPTION_STORAGE_KEY, String(showLegacyDescriptions)); - }, [showLegacyDescriptions]); - - useLayoutEffect(() => { - if (!legacyExpanded) { - setLegacyGeometry(null); - return; - } - - const panelEl = toolPanelRef.current; - if (!panelEl) { - setLegacyGeometry(null); - return; - } - - const rightRailEl = () => document.querySelector('[data-sidebar="right-rail"]') as HTMLElement | null; - - const updateGeometry = () => { - const rect = panelEl.getBoundingClientRect(); - const rail = rightRailEl(); - const rightOffset = rail ? Math.max(0, window.innerWidth - rail.getBoundingClientRect().left) : 0; - const width = Math.max(360, window.innerWidth - rect.left - rightOffset); - const height = Math.max(rect.height, window.innerHeight - rect.top); - setLegacyGeometry({ - left: rect.left, - top: rect.top, - width, - height, - }); - }; - - updateGeometry(); - - const handleResize = () => updateGeometry(); - window.addEventListener('resize', handleResize); - - let resizeObserver: ResizeObserver | null = null; - if (typeof ResizeObserver !== 'undefined') { - resizeObserver = new ResizeObserver(() => updateGeometry()); - resizeObserver.observe(panelEl); - if (quickAccessRef.current) { - resizeObserver.observe(quickAccessRef.current); - } - const rail = rightRailEl(); - if (rail) { - resizeObserver.observe(rail); - } - } - - return () => { - window.removeEventListener('resize', handleResize); - resizeObserver?.disconnect(); - }; - }, [legacyExpanded, quickAccessRef, toolPanelRef]); - const toggleLabel = isLegacyMode ? t('toolPanel.toggle.sidebar', 'Switch to sidebar mode') : t('toolPanel.toggle.legacy', 'Switch to legacy mode'); diff --git a/frontend/src/hooks/tools/useFocusTrap.ts b/frontend/src/hooks/tools/useFocusTrap.ts new file mode 100644 index 000000000..7a66bd809 --- /dev/null +++ b/frontend/src/hooks/tools/useFocusTrap.ts @@ -0,0 +1,74 @@ +import { useEffect, RefObject } from 'react'; + +const FOCUSABLE_ELEMENTS = [ + 'a[href]', + 'button:not([disabled])', + 'input:not([disabled])', + 'select:not([disabled])', + 'textarea:not([disabled])', + '[tabindex]:not([tabindex="-1"])', +].join(', '); + +export function useFocusTrap(containerRef: RefObject, enabled: boolean = true) { + useEffect(() => { + if (!enabled || !containerRef.current) { + return; + } + + const container = containerRef.current; + const getFocusableElements = () => + Array.from(container.querySelectorAll(FOCUSABLE_ELEMENTS)); + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key !== 'Tab') { + return; + } + + const focusableElements = getFocusableElements(); + if (focusableElements.length === 0) { + return; + } + + const firstElement = focusableElements[0]; + const lastElement = focusableElements[focusableElements.length - 1]; + const activeElement = document.activeElement as HTMLElement; + + // Check if focus is within the container + if (!container.contains(activeElement)) { + event.preventDefault(); + firstElement.focus(); + return; + } + + // Shift + Tab (backwards) + if (event.shiftKey) { + if (activeElement === firstElement) { + event.preventDefault(); + lastElement.focus(); + } + } + // Tab (forwards) + else { + if (activeElement === lastElement) { + event.preventDefault(); + firstElement.focus(); + } + } + }; + + // Focus first element on mount + const focusableElements = getFocusableElements(); + if (focusableElements.length > 0) { + // Small delay to ensure the element is fully rendered + setTimeout(() => { + focusableElements[0]?.focus(); + }, 100); + } + + document.addEventListener('keydown', handleKeyDown); + + return () => { + document.removeEventListener('keydown', handleKeyDown); + }; + }, [containerRef, enabled]); +} diff --git a/frontend/src/hooks/tools/useLocalStorageState.ts b/frontend/src/hooks/tools/useLocalStorageState.ts new file mode 100644 index 000000000..ad7ed8438 --- /dev/null +++ b/frontend/src/hooks/tools/useLocalStorageState.ts @@ -0,0 +1,30 @@ +import { useState, useEffect } from 'react'; + +export function useLocalStorageState(key: string, defaultValue: T): [T, (value: T) => void] { + const [state, setState] = useState(() => { + if (typeof window === 'undefined') { + return defaultValue; + } + + const stored = window.localStorage.getItem(key); + if (stored === null) { + return defaultValue; + } + + try { + return JSON.parse(stored) as T; + } catch { + return defaultValue; + } + }); + + useEffect(() => { + if (typeof window === 'undefined') { + return; + } + + window.localStorage.setItem(key, JSON.stringify(state)); + }, [key, state]); + + return [state, setState]; +} diff --git a/frontend/src/hooks/tools/useToolPanelGeometry.ts b/frontend/src/hooks/tools/useToolPanelGeometry.ts new file mode 100644 index 000000000..bd198fdc7 --- /dev/null +++ b/frontend/src/hooks/tools/useToolPanelGeometry.ts @@ -0,0 +1,101 @@ +import { useLayoutEffect, useState, RefObject } from 'react'; + +export interface ToolPanelGeometry { + left: number; + top: number; + width: number; + height: number; +} + +interface UseToolPanelGeometryOptions { + enabled: boolean; + toolPanelRef: RefObject; + quickAccessRef: RefObject; +} + +export function useToolPanelGeometry({ + enabled, + toolPanelRef, + quickAccessRef, +}: UseToolPanelGeometryOptions) { + const [geometry, setGeometry] = useState(null); + + useLayoutEffect(() => { + if (!enabled) { + setGeometry(null); + return; + } + + const panelEl = toolPanelRef.current; + if (!panelEl) { + setGeometry(null); + return; + } + + const rightRailEl = () => document.querySelector('[data-sidebar="right-rail"]') as HTMLElement | null; + + let timeoutId: number | null = null; + + const updateGeometry = () => { + // Debounce: clear any pending update + if (timeoutId !== null) { + window.clearTimeout(timeoutId); + } + + // Schedule update after 150ms of inactivity + timeoutId = window.setTimeout(() => { + const rect = panelEl.getBoundingClientRect(); + const rail = rightRailEl(); + const rightOffset = rail ? Math.max(0, window.innerWidth - rail.getBoundingClientRect().left) : 0; + const width = Math.max(360, window.innerWidth - rect.left - rightOffset); + const height = Math.max(rect.height, window.innerHeight - rect.top); + setGeometry({ + left: rect.left, + top: rect.top, + width, + height, + }); + timeoutId = null; + }, 150); + }; + + // Initial geometry calculation (no debounce) + const rect = panelEl.getBoundingClientRect(); + const rail = rightRailEl(); + const rightOffset = rail ? Math.max(0, window.innerWidth - rail.getBoundingClientRect().left) : 0; + const width = Math.max(360, window.innerWidth - rect.left - rightOffset); + const height = Math.max(rect.height, window.innerHeight - rect.top); + setGeometry({ + left: rect.left, + top: rect.top, + width, + height, + }); + + const handleResize = () => updateGeometry(); + window.addEventListener('resize', handleResize); + + let resizeObserver: ResizeObserver | null = null; + if (typeof ResizeObserver !== 'undefined') { + resizeObserver = new ResizeObserver(() => updateGeometry()); + resizeObserver.observe(panelEl); + if (quickAccessRef.current) { + resizeObserver.observe(quickAccessRef.current); + } + const rail = rightRailEl(); + if (rail) { + resizeObserver.observe(rail); + } + } + + return () => { + if (timeoutId !== null) { + window.clearTimeout(timeoutId); + } + window.removeEventListener('resize', handleResize); + resizeObserver?.disconnect(); + }; + }, [enabled, quickAccessRef, toolPanelRef]); + + return geometry; +}