fix(dashboard): info popover toggle and click-outside dismiss

- Replace fixed overlay with useRef-based click-outside detection
- Add X close button in top-right corner of popover
- Click ? to toggle (open if closed, close if open)
- Click anywhere outside popover to dismiss
- Remove fixed inset-0 overlay that was blocking page interaction

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
K7ZVX 2026-05-13 16:04:36 +00:00
commit 23151f63ba
5 changed files with 2485 additions and 2463 deletions

View file

@ -1,4 +1,4 @@
import { useState, useEffect, useCallback } from 'react' import { useState, useEffect, useCallback, useRef } from 'react'
import NodePicker from '@/components/NodePicker' import NodePicker from '@/components/NodePicker'
import ChannelPicker from '@/components/ChannelPicker' import ChannelPicker from '@/components/ChannelPicker'
import { import {
@ -309,12 +309,28 @@ const US_STATES = [
{ value: 'US-WI', label: 'Wisconsin' }, { value: 'US-WY', label: 'Wyoming' }, { value: 'US-WI', label: 'Wisconsin' }, { value: 'US-WY', label: 'Wyoming' },
] ]
// InfoButton component // InfoButton component with click-outside dismiss and X close button
function InfoButton({ info, link, linkText = 'Learn more' }: { info: string; link?: string; linkText?: string }) { function InfoButton({ info, link, linkText = 'Learn more' }: { info: string; link?: string; linkText?: string }) {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const popoverRef = useRef<HTMLDivElement>(null)
// Close on click outside
useEffect(() => {
if (!open) return
function handleClickOutside(e: MouseEvent) {
if (popoverRef.current && !popoverRef.current.contains(e.target as Node)) {
setOpen(false)
}
}
const timer = setTimeout(() => document.addEventListener('mousedown', handleClickOutside), 0)
return () => {
clearTimeout(timer)
document.removeEventListener('mousedown', handleClickOutside)
}
}, [open])
return ( return (
<div className="relative inline-block"> <div className="relative inline-block" ref={popoverRef}>
<button <button
type="button" type="button"
onClick={(e) => { e.stopPropagation(); setOpen(!open) }} onClick={(e) => { e.stopPropagation(); setOpen(!open) }}
@ -324,10 +340,16 @@ function InfoButton({ info, link, linkText = 'Learn more' }: { info: string; lin
? ?
</button> </button>
{open && ( {open && (
<>
<div className="fixed inset-0 z-40" onClick={() => setOpen(false)} />
<div className="absolute left-0 top-6 z-50 w-72 p-3 bg-[#1a2332] border border-[#2a3a4a] rounded-lg shadow-xl text-xs text-slate-300 leading-relaxed"> <div className="absolute left-0 top-6 z-50 w-72 p-3 bg-[#1a2332] border border-[#2a3a4a] rounded-lg shadow-xl text-xs text-slate-300 leading-relaxed">
{info} <button
type="button"
onClick={() => setOpen(false)}
className="absolute top-1 right-1 w-5 h-5 rounded hover:bg-slate-700 text-slate-500 hover:text-slate-300 inline-flex items-center justify-center transition-colors"
aria-label="Close"
>
<X size={12} />
</button>
<div className="pr-4">{info}</div>
{link && ( {link && (
<a <a
href={link} href={link}
@ -340,7 +362,6 @@ function InfoButton({ info, link, linkText = 'Learn more' }: { info: string; lin
</a> </a>
)} )}
</div> </div>
</>
)} )}
</div> </div>
) )
@ -2139,6 +2160,7 @@ function EnvironmentalSection({ data, onChange }: { data: EnvironmentalConfig; o
</div> </div>
) )
} }
function DashboardSection({ data, onChange }: { data: DashboardConfig; onChange: (d: DashboardConfig) => void }) { function DashboardSection({ data, onChange }: { data: DashboardConfig; onChange: (d: DashboardConfig) => void }) {
return ( return (
<div className="space-y-4"> <div className="space-y-4">

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -8,8 +8,8 @@
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<script type="module" crossorigin src="/assets/index-DrKrP8CJ.js"></script> <script type="module" crossorigin src="/assets/index-DARDkZhk.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-E1oMzltW.css"> <link rel="stylesheet" crossorigin href="/assets/index-CYHGOAxN.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>