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 (
{v.symbol}
{isInWorkspace && (
✓
)}
)
}
// 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 (
{v.symbol}
{availableUnits.length > 1 ? (
) : (
{unit.label}
)}
{isSolved && varId === 'Me' && solvedInfo?.equationName === 'Isentropic Area Ratio' && onToggleBranch && (
)}
{v.name}
{isSolved && !isUserSet ? (
{formatValue(unit.fromSI(solvedInfo.value), sciNotation)}
{isNonSI && (
= {formatValue(solvedInfo.value, sciNotation)} {siUnit.label}
)}
via {solvedInfo.equationName}
) : (
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 && (
Must be positive
)}
{isUserSet && isNonSI && !inputError && (
= {formatValue(userValue, sciNotation)} {siUnit.label}
)}
)}
)
}