diff --git a/ui/src/components/BridgeView.jsx b/ui/src/components/BridgeView.jsx index acb228d..3c5a89f 100644 --- a/ui/src/components/BridgeView.jsx +++ b/ui/src/components/BridgeView.jsx @@ -15,9 +15,15 @@ import { useState, useEffect, useRef, useCallback } from 'react' const UP = '#2a78d6' // increase const DOWN = '#e34948' // decrease const ANCHOR = '#6b7280' // baseline / current — neutral, 4.83:1 on white -const GRID = '#e5e7eb' -const INK = '#374151' -const INK_DIM = '#6b7280' +// Everything that is not a bar follows the theme. These were hex literals, so in +// dark mode the value labels stayed #374151 on a #2a2c2f panel and could not be +// read. Applied through `style` rather than the presentation attribute, where +// var() is not reliable across browsers. +const GRID = 'var(--border-color)' +const AXIS = 'var(--chart-axis)' +const INK = 'var(--text-secondary)' +const INK_DIM = 'var(--chart-ink-dim)' +const PANEL = 'var(--bg-secondary)' // the bars' separating stroke const fmt = (n, dp = 2) => n == null || !isFinite(n) ? '—' @@ -294,6 +300,10 @@ export default function BridgeView({ const [hover, setHover] = useState(null) const [width, setWidth] = useState(880) const boxRef = useRef(null) + // Where the panel has been dragged to, as an offset from centre. It opens + // centred each time rather than wherever it was last left. + const [pos, setPos] = useState({ x: 0, y: 0 }) + const dragRef = useRef(null) // Build the steps: baseline anchor, one floating step per tag, current anchor. const compute = useCallback(async () => { @@ -405,6 +415,36 @@ export default function BridgeView({ useEffect(() => { try { localStorage.setItem('pf_bridge_basis', basis) } catch {} }, [basis]) useEffect(() => { if (open) compute() }, [open, compute]) + useEffect(() => { if (open) setPos({ x: 0, y: 0 }) }, [open]) + + // Escape closes it. There is no backdrop to click any more -- see below. + useEffect(() => { + if (!open) return + const onKey = (e) => { if (e.key === 'Escape') onClose() } + window.addEventListener('keydown', onKey) + return () => window.removeEventListener('keydown', onKey) + }, [open, onClose]) + + // Drag by the header. Pointer capture keeps the drag alive when the cursor + // outruns the panel; the close button is excluded so clicking it still closes. + const startDrag = (e) => { + if (e.button !== 0 || e.target.closest('button')) return + e.currentTarget.setPointerCapture(e.pointerId) + dragRef.current = { px: e.clientX, py: e.clientY, x: pos.x, y: pos.y } + } + const moveDrag = (e) => { + const d = dragRef.current + if (!d) return + // keep the header on screen, or there is nothing left to drag it back by + const maxX = window.innerWidth / 2 - 40 + const maxY = window.innerHeight / 2 - 20 + setPos({ + x: Math.max(-maxX, Math.min(maxX, d.x + e.clientX - d.px)), + y: Math.max(-maxY, Math.min(maxY, d.y + e.clientY - d.py)), + }) + } + const endDrag = () => { dragRef.current = null } + useEffect(() => { if (!open || !boxRef.current) return const ro = new ResizeObserver(([e]) => setWidth(Math.max(420, e.contentRect.width))) @@ -421,11 +461,18 @@ export default function BridgeView({ const ticks = niceTicks(yMin, yMax, 5) return ( -