This commit is contained in:
2026-03-03 16:43:30 +00:00
commit 03452517b5
58 changed files with 13181 additions and 0 deletions

View 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>
)
}