/** * Mobile results tray — replaces the side panel on small screens. * Collapsed: shows last result + drag handle (48px). * Expanded: scrollable list of all results (40vh). */ import { useState, useCallback, useRef } from 'react' import type { EngineLineResult } from '../engine/types.ts' import '../styles/mobile-results-tray.css' const DISPLAYABLE_TYPES = new Set([ 'number', 'unitValue', 'currencyValue', 'dateTime', 'timeDelta', 'boolean', ]) interface MobileResultsTrayProps { results: EngineLineResult[] docLines: string[] } export function MobileResultsTray({ results, docLines }: MobileResultsTrayProps) { const [expanded, setExpanded] = useState(false) const [copiedIdx, setCopiedIdx] = useState(null) const startY = useRef(null) // Find last displayable result let lastResult = '' for (let i = results.length - 1; i >= 0; i--) { if (DISPLAYABLE_TYPES.has(results[i].type) && results[i].display) { lastResult = results[i].display break } } const handleCopy = useCallback((idx: number, rawValue: number | null, display: string) => { const text = rawValue != null ? String(rawValue) : display navigator.clipboard.writeText(text).then(() => { setCopiedIdx(idx) setTimeout(() => setCopiedIdx(null), 1200) }).catch(() => {}) }, []) // Touch swipe handling const handleTouchStart = useCallback((e: React.TouchEvent) => { startY.current = e.touches[0].clientY }, []) const handleTouchEnd = useCallback((e: React.TouchEvent) => { if (startY.current === null) return const deltaY = startY.current - e.changedTouches[0].clientY startY.current = null if (deltaY > 40) setExpanded(true) // swipe up if (deltaY < -40) setExpanded(false) // swipe down }, []) // Build result items for expanded view const resultItems = results.map((r, i) => { if (!DISPLAYABLE_TYPES.has(r.type) || !r.display) return null const expr = docLines[i]?.trim() ?? '' const isCopied = copiedIdx === i return (
handleCopy(i, r.rawValue, r.display)} > Ln {i + 1} {expr} {isCopied ? 'Copied!' : r.display}
) }).filter(Boolean) return (
{/* Drag handle + collapsed view */}
setExpanded(prev => !prev)} >
{!expanded && ( {lastResult ? `Last: ${lastResult}` : 'No results'} )} {expanded && ( {resultItems.length} results )}
{/* Expanded content */} {expanded && (
{resultItems.length > 0 ? resultItems : (
No results yet
)}
)}
) }