'use client' import React, { useCallback, useEffect, useRef } from 'react' import { useStore } from 'zustand' import { Tooltip, TooltipContent, TooltipTrigger } from '@djangocfg/ui-core/components' import { useHotkey } from '@djangocfg/ui-core/hooks' import { cn } from '@djangocfg/ui-core/lib' import { Bug } from 'lucide-react' import { isDevelopment } from '../internal' import { devtoolsStore } from '../store' import { useDebugMode } from '../useDebugMode' import { DevtoolsPanel } from './DevtoolsPanel' import type { DevtoolsPanelProps } from './DevtoolsPanel' export interface DevtoolsButtonProps { className?: string /** Props forwarded to the panel */ panel?: DevtoolsPanelProps /** * Explicitly disable. Default: undefined (auto — visible in dev, hidden in * prod, `?debug=1` always unlocks). */ enabled?: boolean } /** * Floating debug button. * * Visibility: * 1. `enabled === false` and no `?debug=1` → renders nothing * 2. dev or `?debug=1` → visible button + ⌘D shortcut * 3. prod without `?debug=1` → invisible click trap (5 clicks in 2s unlock) */ export function DevtoolsButton({ className, panel = {}, enabled }: DevtoolsButtonProps) { const isOpen = useStore(devtoolsStore, (s) => s.isOpen) const togglePanel = useStore(devtoolsStore, (s) => s.togglePanel) const errorCount = useStore(devtoolsStore, (s) => s.entries.reduce((n, e) => (e.level === 'error' ? n + 1 : n), 0), ) const isDebugMode = useDebugMode() const unlocked = isDevelopment || isDebugMode useHotkey( 'meta+d', (e) => { e.preventDefault() if (unlocked) togglePanel() }, { preventDefault: true }, ) // Hide the Next.js dev indicator while the panel is open — it overlaps. useEffect(() => { if (typeof document === 'undefined') return const apply = () => { document.querySelectorAll('nextjs-portal').forEach((el) => { el.style.display = isOpen ? 'none' : '' }) } apply() const observer = new MutationObserver(apply) observer.observe(document.body, { childList: true }) return () => observer.disconnect() }, [isOpen]) // Easter egg: 5 clicks in 2s on the invisible trap opens the panel. const clicks = useRef([]) const handleTrapClick = useCallback(() => { const now = Date.now() clicks.current = [...clicks.current.filter((t) => now - t < 2000), now] if (clicks.current.length >= 5) { clicks.current = [] devtoolsStore.getState().openPanel() } }, []) if (enabled === false && !isDebugMode) return null if (!unlocked) { return (
) } return ( <> {!isOpen && ( // Inline position, not Tailwind: this package's utility classes aren't // always in the consumer app's content scan. `left: 64px` in dev // clears the Next.js dev indicator.
Devtools ⌘D
)} ) }