diff --git a/next.config.ts b/next.config.ts index f8f7d841..4fa1c77e 100644 --- a/next.config.ts +++ b/next.config.ts @@ -166,6 +166,7 @@ if (cloudMode) { /** @type {import('next').NextConfig} */ export default { reactStrictMode: false, + devIndicators: false, env: { basePath, cloudMode, diff --git a/packages/dials/src/components/DialsOverlay.tsx b/packages/dials/src/components/DialsOverlay.tsx index a93a3da7..8e0f77ce 100644 --- a/packages/dials/src/components/DialsOverlay.tsx +++ b/packages/dials/src/components/DialsOverlay.tsx @@ -12,6 +12,8 @@ import { BooleanControl } from '../controls/BooleanControl'; import { NumberControl } from '../controls/NumberControl'; import type { DialRegistration } from '../types'; +type OverlayState = 'hidden' | 'collapsed' | 'expanded'; + export interface DialsOverlayProps { /** Initial visibility state */ defaultVisible?: boolean; @@ -35,47 +37,27 @@ export function DialsOverlay({ defaultVisible = true, position = 'bottom-left', }: DialsOverlayProps) { - // Load visibility state from localStorage (avoiding hydration mismatch) - const [isVisible, setIsVisible] = useState(defaultVisible); + // Three states: hidden (nothing), collapsed (button only), expanded (full panel) + const [overlayState, setOverlayState] = useState( + defaultVisible ? 'expanded' : 'collapsed', + ); // Load from localStorage after mount to avoid hydration issues useEffect(() => { - const stored = localStorage.getItem('niteshift-dials-visible'); - if (stored !== null) { - setIsVisible(stored === 'true'); + const stored = localStorage.getItem('niteshift-dials-state'); + if (stored !== null && ['hidden', 'collapsed', 'expanded'].includes(stored)) { + setOverlayState(stored as OverlayState); } }, []); const [searchTerm, setSearchTerm] = useState(''); const [dials, setDials] = useState([]); - const [hasNextOverlay, setHasNextOverlay] = useState(false); const [isMacLike, setIsMacLike] = useState(false); - const [shortcutLabel, setShortcutLabel] = useState('Ctrl+D (macOS) / Ctrl+Alt+D (Win/Linux)'); const registry = getDialRegistry(); - // Persist visibility state to localStorage + // Persist state to localStorage useEffect(() => { - localStorage.setItem('niteshift-dials-visible', String(isVisible)); - }, [isVisible]); - - // Detect Next.js error overlay - useEffect(() => { - const checkNextOverlay = () => { - // Next.js error overlay has specific identifiers - const nextjsOverlay = - document.querySelector('nextjs-portal') || - document.querySelector('[data-nextjs-dialog-overlay]') || - document.querySelector('[data-nextjs-toast]'); - setHasNextOverlay(!!nextjsOverlay); - }; - - // Check on mount and set up observer - checkNextOverlay(); - - const observer = new MutationObserver(checkNextOverlay); - observer.observe(document.body, { childList: true, subtree: true }); - - return () => observer.disconnect(); - }, []); + localStorage.setItem('niteshift-dials-state', overlayState); + }, [overlayState]); // Subscribe to registry changes useEffect(() => { @@ -99,22 +81,28 @@ export function DialsOverlay({ setIsMacLike(isMac); }, []); - useEffect(() => { - setShortcutLabel(isMacLike ? 'Ctrl+D (macOS)' : 'Ctrl+Alt+D (Windows/Linux)'); - }, [isMacLike]); - // Keyboard shortcut to toggle visibility useEffect(() => { const handleKeyPress = (e: KeyboardEvent) => { + // Guard against undefined or null key (can happen with some special key events) + if (!e.key) return; + const key = e.key.toLowerCase(); if (key !== 'd') return; const macCombo = isMacLike && e.ctrlKey && !e.metaKey && !e.shiftKey && !e.altKey; const otherCombo = !isMacLike && e.ctrlKey && e.altKey && !e.metaKey && !e.shiftKey; + // Ctrl+Shift+D to hide entirely on any platform + const hideCombo = e.ctrlKey && e.shiftKey && !e.metaKey && !e.altKey; - if (macCombo || otherCombo) { + if (hideCombo) { + // Ctrl+Shift+D: toggle between hidden and collapsed e.preventDefault(); - setIsVisible(prev => !prev); + setOverlayState(prev => (prev === 'hidden' ? 'collapsed' : 'hidden')); + } else if (macCombo || otherCombo) { + // Ctrl+D / Ctrl+Alt+D: toggle between collapsed and expanded + e.preventDefault(); + setOverlayState(prev => (prev === 'expanded' ? 'collapsed' : 'expanded')); } }; @@ -163,20 +151,21 @@ export function DialsOverlay({ } }; - // Calculate bottom position based on Next.js overlay presence - const bottomPosition = hasNextOverlay ? '140px' : '20px'; + // Hidden state: render nothing + if (overlayState === 'hidden') { + return null; + } - if (!isVisible) { + // Collapsed state: render button only + if (overlayState === 'collapsed') { return (