Ablative config
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user