Files
rocketry/src/components/VariableCard.jsx
2026-03-03 20:30:29 +00:00

196 lines
6.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useRef, useState } from 'react'
import { useDraggable } from '@dnd-kit/core'
import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { VARIABLES } from '../engine/variables.js'
import { getUnitsForFamily } from '../engine/units.js'
import { formatValue } from '../engine/format.js'
// Card shown in the left palette — draggable
export function PaletteCard({ varId, isInWorkspace }) {
const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
id: `palette-${varId}`,
data: { varId, source: 'palette' },
})
const v = VARIABLES[varId]
if (!v) return null
return (
<div
ref={setNodeRef}
{...listeners}
{...attributes}
className={`
flex items-center gap-2 px-3 py-2 rounded-lg border text-sm cursor-grab select-none transition-all
${isDragging ? 'opacity-40' : ''}
${isInWorkspace
? 'border-slate-600 bg-slate-800 text-slate-500'
: 'border-slate-600 bg-slate-700 text-slate-200 hover:border-blue-400 hover:bg-slate-600 active:cursor-grabbing'}
`}
>
<span className="font-mono font-bold text-blue-300 w-10 shrink-0 text-center">{v.symbol}</span>
<div className="min-w-0">
<div className="truncate">{v.name}</div>
<div className="text-xs text-slate-400">{v.units}</div>
</div>
{isInWorkspace && (
<span className="ml-auto text-green-500 text-xs shrink-0"></span>
)}
</div>
)
}
// Card shown in the workspace
export function WorkspaceCard({
varId,
userValue,
solvedInfo,
onRemove,
onValueChange,
getUnit,
onUnitChange,
sciNotation,
areaRatioBranch,
onToggleBranch,
}) {
const inputRef = useRef(null)
const [inputError, setInputError] = useState(false)
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: varId })
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
}
const v = VARIABLES[varId]
if (!v) return null
const unit = getUnit(varId)
const availableUnits = getUnitsForFamily(v.unitFamily)
const isUserSet = userValue !== undefined && userValue !== ''
const isSolved = !!solvedInfo
const handleInputChange = (rawDisplayValue) => {
if (rawDisplayValue === '') {
setInputError(false)
onValueChange(rawDisplayValue)
return
}
const parsed = parseFloat(rawDisplayValue)
if (isNaN(parsed)) return
// Validate positive constraint
if (v.positive && parsed < 0) {
setInputError(true)
return
}
setInputError(false)
onValueChange(rawDisplayValue)
}
// State: user-entered (blue), solved (green), unknown (grey), error (red)
const borderColor = inputError ? 'border-red-500' : isUserSet ? 'border-blue-500' : isSolved ? 'border-green-500' : 'border-slate-600'
const symbolColor = inputError ? 'text-red-300' : isUserSet ? 'text-blue-300' : isSolved ? 'text-green-300' : 'text-slate-400'
const valueBg = inputError ? 'bg-red-950' : isUserSet ? 'bg-blue-950' : isSolved ? 'bg-green-950' : 'bg-slate-800'
// Display value converted from stored SI to current display unit
const inputDisplayValue = isUserSet
? parseFloat(unit.fromSI(userValue).toPrecision(10))
: ''
const siUnit = availableUnits[0]
const isNonSI = unit.id !== siUnit.id
return (
<div
ref={setNodeRef}
style={style}
className={`rounded-xl border-2 ${borderColor} bg-slate-800 p-3 flex flex-col gap-1 relative group ${isDragging ? 'opacity-50' : ''}`}
{...attributes}
{...listeners}
>
<button
onClick={onRemove}
className="absolute top-1.5 right-1.5 w-5 h-5 rounded-full bg-slate-700 text-slate-400 hover:bg-red-700 hover:text-white opacity-0 group-hover:opacity-100 transition-all text-xs flex items-center justify-center leading-none"
title="Remove from workspace"
>
×
</button>
<div className="flex items-baseline gap-2">
<span className={`font-mono font-bold text-lg ${symbolColor}`}>{v.symbol}</span>
{availableUnits.length > 1 ? (
<select
value={unit.id}
onChange={e => onUnitChange(e.target.value)}
className={`text-xs rounded px-1.5 py-0.5 border outline-none cursor-pointer transition-colors ${
isNonSI
? 'bg-violet-900/40 text-violet-200 border-violet-600 hover:border-violet-400'
: 'bg-transparent text-slate-400 border-transparent hover:border-slate-500 hover:text-slate-300'
}`}
>
{availableUnits.map(u => (
<option key={u.id} value={u.id} className="bg-slate-800 text-slate-200">{u.label}</option>
))}
</select>
) : (
<span className="text-slate-400 text-xs">{unit.label}</span>
)}
{isSolved && varId === 'Me' && solvedInfo?.equationName === 'Isentropic Area Ratio' && onToggleBranch && (
<button
onClick={onToggleBranch}
title={`Switch to ${areaRatioBranch === 'supersonic' ? 'subsonic' : 'supersonic'}`}
className="ml-auto text-xs px-1.5 py-0.5 rounded border transition-colors bg-amber-900/40 text-amber-300 border-amber-600 hover:border-amber-400 hover:bg-amber-900/60"
>
{areaRatioBranch === 'supersonic' ? 'SS' : 'Sub'}
</button>
)}
</div>
<div className="text-slate-300 text-xs truncate">{v.name}</div>
<div className={`mt-1 rounded-md ${valueBg} px-2 py-1`}>
{isSolved && !isUserSet ? (
<div>
<div className="font-mono text-green-300 text-sm font-semibold">
{formatValue(unit.fromSI(solvedInfo.value), sciNotation)}
</div>
{isNonSI && (
<div className="text-green-700 text-[10px] mt-0.5 font-mono">
= {formatValue(solvedInfo.value, sciNotation)} {siUnit.label}
</div>
)}
<div className="text-green-600 text-xs mt-0.5">via {solvedInfo.equationName}</div>
</div>
) : (
<div>
<input
ref={inputRef}
type="number"
value={inputDisplayValue}
onChange={e => handleInputChange(e.target.value)}
placeholder="Enter value…"
className={`w-full bg-transparent font-mono text-sm ${inputError ? 'text-red-200' : 'text-blue-200'} placeholder-slate-600 outline-none`}
/>
{inputError && (
<p className="text-xs text-red-400 mt-0.5">Must be positive</p>
)}
{isUserSet && isNonSI && !inputError && (
<div className="text-blue-800 text-[10px] mt-0.5 font-mono">
= {formatValue(userValue, sciNotation)} {siUnit.label}
</div>
)}
</div>
)}
</div>
</div>
)
}