init
This commit is contained in:
139
src/components/VariableCard.jsx
Normal file
139
src/components/VariableCard.jsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import { useRef } from 'react'
|
||||
import { useDraggable } from '@dnd-kit/core'
|
||||
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,
|
||||
}) {
|
||||
const inputRef = useRef(null)
|
||||
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
|
||||
|
||||
// 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'
|
||||
|
||||
// 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 className={`rounded-xl border-2 ${borderColor} bg-slate-800 p-3 flex flex-col gap-1 relative group`}>
|
||||
<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>
|
||||
)}
|
||||
</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 => onValueChange(e.target.value)}
|
||||
placeholder="Enter value…"
|
||||
className="w-full bg-transparent font-mono text-sm text-blue-200 placeholder-slate-600 outline-none"
|
||||
/>
|
||||
{isUserSet && isNonSI && (
|
||||
<div className="text-blue-800 text-[10px] mt-0.5 font-mono">
|
||||
= {formatValue(userValue, sciNotation)} {siUnit.label}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user