feat: Add Canvas dark theme styling and Setup page integration

Canvas Builder Visual Updates:
- Update all Canvas components to use Atomaster dark theme
- BaseNode: dark background (bg-dark-800), white text, primary selection glow
- NodePalette: dark sidebar with hover states
- NodeConfigPanel: dark inputs, labels, and panel background
- ValidationPanel: semi-transparent error/warning panels with backdrop blur
- ChatPanel: dark message area with themed welcome state
- ExecuteDialog: dark modal with primary button styling
- ConfigImporter: dark tabs, inputs, and file upload zone
- TemplateSelector: dark cards with category pills and hover effects

Setup Page Integration:
- Add Configuration/Canvas Builder tab switcher
- Canvas tab renders AtomizerCanvas full-height
- Tabs styled to match Atomaster theme

Build: Passes TypeScript and Vite build

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-14 21:30:30 -05:00
parent 92c82a5518
commit 9f3ac28000
10 changed files with 221 additions and 153 deletions

View File

@@ -14,6 +14,11 @@ interface NodeConfigPanelProps {
nodeId: string;
}
// Common input class for dark theme
const inputClass = "w-full px-3 py-2 bg-dark-800 border border-dark-600 text-white placeholder-dark-400 rounded-lg focus:border-primary-500 focus:outline-none transition-colors";
const selectClass = "w-full px-3 py-2 bg-dark-800 border border-dark-600 text-white rounded-lg focus:border-primary-500 focus:outline-none transition-colors";
const labelClass = "block text-sm font-medium text-dark-300 mb-1";
export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
const { nodes, updateNodeData, deleteSelected } = useCanvasStore();
const node = nodes.find((n) => n.id === nodeId);
@@ -27,12 +32,12 @@ export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
};
return (
<div className="w-80 bg-white border-l border-gray-200 p-4 overflow-y-auto">
<div className="w-80 bg-dark-850 border-l border-dark-700 p-4 overflow-y-auto">
<div className="flex justify-between items-center mb-4">
<h3 className="font-semibold text-gray-800">Configure {data.label}</h3>
<h3 className="font-semibold text-white">Configure {data.label}</h3>
<button
onClick={deleteSelected}
className="text-red-500 hover:text-red-700 text-sm"
className="text-red-400 hover:text-red-300 text-sm transition-colors"
>
Delete
</button>
@@ -41,14 +46,14 @@ export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
<div className="space-y-4">
{/* Common: Label */}
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">
<label className={labelClass}>
Label
</label>
<input
type="text"
value={data.label}
onChange={(e) => handleChange('label', e.target.value)}
className="w-full px-3 py-2 border rounded-lg"
className={inputClass}
/>
</div>
@@ -56,7 +61,7 @@ export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
{data.type === 'model' && (
<>
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">
<label className={labelClass}>
File Path
</label>
<input
@@ -64,17 +69,17 @@ export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
value={(data as ModelNodeData).filePath || ''}
onChange={(e) => handleChange('filePath', e.target.value)}
placeholder="path/to/model.prt"
className="w-full px-3 py-2 border rounded-lg font-mono text-sm"
className={`${inputClass} font-mono text-sm`}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">
<label className={labelClass}>
File Type
</label>
<select
value={(data as ModelNodeData).fileType || ''}
onChange={(e) => handleChange('fileType', e.target.value)}
className="w-full px-3 py-2 border rounded-lg"
className={selectClass}
>
<option value="">Select...</option>
<option value="prt">Part (.prt)</option>
@@ -87,13 +92,13 @@ export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
{data.type === 'solver' && (
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">
<label className={labelClass}>
Solution Type
</label>
<select
value={(data as SolverNodeData).solverType || ''}
onChange={(e) => handleChange('solverType', e.target.value)}
className="w-full px-3 py-2 border rounded-lg"
className={selectClass}
>
<option value="">Select...</option>
<option value="SOL101">SOL 101 - Linear Static</option>
@@ -109,7 +114,7 @@ export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
{data.type === 'designVar' && (
<>
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">
<label className={labelClass}>
Expression Name
</label>
<input
@@ -117,35 +122,35 @@ export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
value={(data as DesignVarNodeData).expressionName || ''}
onChange={(e) => handleChange('expressionName', e.target.value)}
placeholder="thickness"
className="w-full px-3 py-2 border rounded-lg font-mono"
className={`${inputClass} font-mono`}
/>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">
<label className={labelClass}>
Min
</label>
<input
type="number"
value={(data as DesignVarNodeData).minValue ?? ''}
onChange={(e) => handleChange('minValue', parseFloat(e.target.value))}
className="w-full px-3 py-2 border rounded-lg"
className={inputClass}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">
<label className={labelClass}>
Max
</label>
<input
type="number"
value={(data as DesignVarNodeData).maxValue ?? ''}
onChange={(e) => handleChange('maxValue', parseFloat(e.target.value))}
className="w-full px-3 py-2 border rounded-lg"
className={inputClass}
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">
<label className={labelClass}>
Unit
</label>
<input
@@ -153,7 +158,7 @@ export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
value={(data as DesignVarNodeData).unit || ''}
onChange={(e) => handleChange('unit', e.target.value)}
placeholder="mm"
className="w-full px-3 py-2 border rounded-lg"
className={inputClass}
/>
</div>
</>
@@ -162,7 +167,7 @@ export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
{data.type === 'extractor' && (
<>
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">
<label className={labelClass}>
Extractor ID
</label>
<select
@@ -182,7 +187,7 @@ export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
handleChange('extractorId', id);
handleChange('extractorName', names[id] || id);
}}
className="w-full px-3 py-2 border rounded-lg"
className={selectClass}
>
<option value="">Select...</option>
<option value="E1">E1 - Displacement</option>
@@ -201,13 +206,13 @@ export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
{data.type === 'algorithm' && (
<>
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">
<label className={labelClass}>
Method
</label>
<select
value={(data as AlgorithmNodeData).method || ''}
onChange={(e) => handleChange('method', e.target.value)}
className="w-full px-3 py-2 border rounded-lg"
className={selectClass}
>
<option value="">Select...</option>
<option value="TPE">TPE (Tree Parzen Estimator)</option>
@@ -218,7 +223,7 @@ export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">
<label className={labelClass}>
Max Trials
</label>
<input
@@ -226,7 +231,7 @@ export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
value={(data as AlgorithmNodeData).maxTrials ?? ''}
onChange={(e) => handleChange('maxTrials', parseInt(e.target.value))}
placeholder="100"
className="w-full px-3 py-2 border rounded-lg"
className={inputClass}
/>
</div>
</>
@@ -235,7 +240,7 @@ export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
{data.type === 'objective' && (
<>
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">
<label className={labelClass}>
Objective Name
</label>
<input
@@ -243,24 +248,24 @@ export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
value={(data as ObjectiveNodeData).name || ''}
onChange={(e) => handleChange('name', e.target.value)}
placeholder="mass"
className="w-full px-3 py-2 border rounded-lg"
className={inputClass}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">
<label className={labelClass}>
Direction
</label>
<select
value={(data as ObjectiveNodeData).direction || 'minimize'}
onChange={(e) => handleChange('direction', e.target.value)}
className="w-full px-3 py-2 border rounded-lg"
className={selectClass}
>
<option value="minimize">Minimize</option>
<option value="maximize">Maximize</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">
<label className={labelClass}>
Weight
</label>
<input
@@ -268,7 +273,7 @@ export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
step="0.1"
value={(data as ObjectiveNodeData).weight ?? 1}
onChange={(e) => handleChange('weight', parseFloat(e.target.value))}
className="w-full px-3 py-2 border rounded-lg"
className={inputClass}
/>
</div>
</>
@@ -277,7 +282,7 @@ export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
{data.type === 'constraint' && (
<>
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">
<label className={labelClass}>
Constraint Name
</label>
<input
@@ -285,18 +290,18 @@ export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
value={(data as ConstraintNodeData).name || ''}
onChange={(e) => handleChange('name', e.target.value)}
placeholder="max_stress"
className="w-full px-3 py-2 border rounded-lg"
className={inputClass}
/>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">
<label className={labelClass}>
Operator
</label>
<select
value={(data as ConstraintNodeData).operator || '<='}
onChange={(e) => handleChange('operator', e.target.value)}
className="w-full px-3 py-2 border rounded-lg"
className={selectClass}
>
<option value="<">&lt;</option>
<option value="<=">&lt;=</option>
@@ -306,14 +311,14 @@ export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">
<label className={labelClass}>
Value
</label>
<input
type="number"
value={(data as ConstraintNodeData).value ?? ''}
onChange={(e) => handleChange('value', parseFloat(e.target.value))}
className="w-full px-3 py-2 border rounded-lg"
className={inputClass}
/>
</div>
</div>
@@ -328,22 +333,22 @@ export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
id="surrogate-enabled"
checked={(data as SurrogateNodeData).enabled || false}
onChange={(e) => handleChange('enabled', e.target.checked)}
className="w-4 h-4"
className="w-4 h-4 rounded bg-dark-800 border-dark-600 text-primary-500 focus:ring-primary-500"
/>
<label htmlFor="surrogate-enabled" className="text-sm font-medium text-gray-600">
<label htmlFor="surrogate-enabled" className="text-sm font-medium text-dark-300">
Enable Neural Surrogate
</label>
</div>
{(data as SurrogateNodeData).enabled && (
<>
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">
<label className={labelClass}>
Model Type
</label>
<select
value={(data as SurrogateNodeData).modelType || ''}
onChange={(e) => handleChange('modelType', e.target.value)}
className="w-full px-3 py-2 border rounded-lg"
className={selectClass}
>
<option value="">Select...</option>
<option value="MLP">MLP (Multi-Layer Perceptron)</option>
@@ -352,14 +357,14 @@ export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">
<label className={labelClass}>
Min Trials Before Activation
</label>
<input
type="number"
value={(data as SurrogateNodeData).minTrials ?? 20}
onChange={(e) => handleChange('minTrials', parseInt(e.target.value))}
className="w-full px-3 py-2 border rounded-lg"
className={inputClass}
/>
</div>
</>