Ablative config

This commit is contained in:
2026-03-03 20:30:29 +00:00
parent 386f6fe928
commit 585e66ceb4
16 changed files with 713 additions and 71 deletions

View File

@@ -1,5 +1,7 @@
import { useRef } from 'react'
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'
@@ -49,8 +51,26 @@ export function WorkspaceCard({
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
@@ -60,10 +80,28 @@ export function WorkspaceCard({
const isUserSet = userValue !== undefined && userValue !== ''
const isSolved = !!solvedInfo
// State: user-entered (blue), solved (green), unknown (grey)
const borderColor = isUserSet ? 'border-blue-500' : isSolved ? 'border-green-500' : 'border-slate-600'
const symbolColor = isUserSet ? 'text-blue-300' : isSolved ? 'text-green-300' : 'text-slate-400'
const valueBg = isUserSet ? 'bg-blue-950' : isSolved ? 'bg-green-950' : 'bg-slate-800'
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
@@ -74,7 +112,13 @@ export function WorkspaceCard({
const isNonSI = unit.id !== siUnit.id
return (
<div className={`rounded-xl border-2 ${borderColor} bg-slate-800 p-3 flex flex-col gap-1 relative group`}>
<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"
@@ -101,6 +145,15 @@ export function WorkspaceCard({
) : (
<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`}>
@@ -122,11 +175,14 @@ export function WorkspaceCard({
ref={inputRef}
type="number"
value={inputDisplayValue}
onChange={e => onValueChange(e.target.value)}
onChange={e => handleInputChange(e.target.value)}
placeholder="Enter value…"
className="w-full bg-transparent font-mono text-sm text-blue-200 placeholder-slate-600 outline-none"
className={`w-full bg-transparent font-mono text-sm ${inputError ? 'text-red-200' : 'text-blue-200'} placeholder-slate-600 outline-none`}
/>
{isUserSet && isNonSI && (
{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>