Datavision / frontend /src /components /simulator /VariableControl.tsx
DataVision CI/CD Bot
release: clean production build for HuggingFace Space
09801ca
Raw
History Blame Contribute Delete
7.85 kB
import React from 'react';
import { useUserStore } from '@/store/userStore';
import { Lock, Pin, Info } from 'lucide-react';
interface VariableControlProps {
name: string;
displayName: string;
controlType: string;
value: any;
onChange: (value: any) => void;
minValue?: number;
maxValue?: number;
step?: number;
unit?: string;
options?: string[];
locked?: boolean;
pinned?: boolean;
description?: string;
onLock?: () => void;
onPin?: () => void;
}
const VariableControl: React.FC<VariableControlProps> = ({
name, displayName, controlType, value, onChange,
minValue = 0, maxValue = 100, step = 1, unit = '',
options = [], locked = false, pinned = false, description,
onLock, onPin,
}) => {
const { isDark } = useUserStore();
const formatValue = (v: any) => {
if (typeof v !== 'number') return String(v);
if (unit === '₹') {
if (v >= 10000000) return `₹${(v / 10000000).toFixed(2)} Cr`;
if (v >= 100000) return `₹${(v / 100000).toFixed(1)} L`;
return `₹${v.toLocaleString()}`;
}
if (unit === '%') return `${v.toLocaleString()}%`;
return v.toLocaleString();
};
// Calculate slider fill percentage
const fillPct = controlType === 'slider' && typeof value === 'number'
? ((value - (minValue || 0)) / ((maxValue || 100) - (minValue || 0))) * 100
: 0;
return (
<div className={`space-y-2 ${locked ? 'opacity-50 pointer-events-none' : ''}`}>
{/* Label row */}
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-1.5 min-w-0">
<label
className="text-xs font-medium truncate"
style={{ color: isDark ? '#e2e8f0' : '#334155' }}
title={displayName}
>
{displayName}
</label>
{description && (
<div className="group relative">
<Info className="w-3 h-3 flex-shrink-0" style={{ color: isDark ? '#475569' : '#94a3b8' }} />
<div className="absolute z-50 bottom-full left-0 mb-1 hidden group-hover:block">
<div className="px-2 py-1 rounded-lg text-[10px] whitespace-nowrap shadow-lg border"
style={{
background: isDark ? '#1e293b' : '#fff',
borderColor: isDark ? '#334155' : '#e2e8f0',
color: isDark ? '#cbd5e1' : '#475569',
}}
>
{description}
</div>
</div>
</div>
)}
</div>
<div className="flex items-center gap-1">
{onPin && (
<button onClick={onPin} className="p-0.5 rounded hover:bg-white/5 transition-colors" title="Pin">
<Pin className={`w-3 h-3 ${pinned ? 'text-indigo-400' : ''}`} style={{ color: pinned ? undefined : (isDark ? '#475569' : '#94a3b8') }} />
</button>
)}
{onLock && (
<button onClick={onLock} className="p-0.5 rounded hover:bg-white/5 transition-colors" title="Lock">
<Lock className={`w-3 h-3 ${locked ? 'text-amber-400' : ''}`} style={{ color: locked ? undefined : (isDark ? '#475569' : '#94a3b8') }} />
</button>
)}
<span
className="text-xs font-bold px-2 py-0.5 rounded-md ml-1"
style={{
background: isDark ? 'rgba(99,102,241,0.1)' : 'rgba(99,102,241,0.08)',
color: '#6366f1',
}}
>
{formatValue(value)}
</span>
</div>
</div>
{/* Control */}
{controlType === 'slider' && (
<div className="space-y-1">
<div className="relative w-full h-1.5">
<div
className="absolute inset-0 rounded-full"
style={{ background: isDark ? '#1e293b' : '#e2e8f0' }}
/>
<div
className="absolute top-0 left-0 h-full rounded-full transition-all duration-75"
style={{
width: `${fillPct}%`,
background: 'linear-gradient(90deg, #6366f1, #818cf8)',
}}
/>
<input
type="range"
min={minValue}
max={maxValue}
step={step}
value={value}
onChange={(e) => onChange(parseFloat(e.target.value))}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
style={{ margin: 0 }}
/>
<div
className="absolute top-1/2 -translate-y-1/2 w-3.5 h-3.5 rounded-full border-2 shadow-md transition-all duration-75 pointer-events-none"
style={{
left: `calc(${fillPct}% - 7px)`,
borderColor: '#6366f1',
background: isDark ? '#0f172a' : '#ffffff',
boxShadow: '0 0 6px rgba(99,102,241,0.4)',
}}
/>
</div>
<div className="flex justify-between text-[10px]" style={{ color: isDark ? '#475569' : '#94a3b8' }}>
<span>{formatValue(minValue)}</span>
<span>{formatValue(maxValue)}</span>
</div>
</div>
)}
{controlType === 'number' && (
<input
type="number"
value={value}
min={minValue}
max={maxValue}
step={step}
onChange={(e) => onChange(parseFloat(e.target.value))}
className="w-full px-3 py-2 rounded-xl text-sm border outline-none focus:ring-2 focus:ring-indigo-500/30 transition-all"
style={{
background: isDark ? 'rgba(255,255,255,0.03)' : '#f8fafc',
borderColor: isDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.08)',
color: isDark ? '#f8fafc' : '#0f172a',
}}
/>
)}
{controlType === 'dropdown' && (
<select
value={value}
onChange={(e) => onChange(e.target.value)}
className="w-full px-3 py-2 rounded-xl text-sm border outline-none focus:ring-2 focus:ring-indigo-500/30 transition-all cursor-pointer"
style={{
background: isDark ? 'rgba(255,255,255,0.03)' : '#f8fafc',
borderColor: isDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.08)',
color: isDark ? '#f8fafc' : '#0f172a',
}}
>
{options.map((opt) => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
)}
{controlType === 'categorical' && (
<div className="flex flex-wrap gap-1.5">
{options.map((opt) => (
<button
key={opt}
onClick={() => onChange(opt)}
className={`px-2.5 py-1 rounded-lg text-[11px] font-medium transition-all border ${
value === opt
? 'bg-indigo-500/10 border-indigo-500/30 text-indigo-500'
: ''
}`}
style={value !== opt ? {
borderColor: isDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.08)',
color: isDark ? '#94a3b8' : '#64748b',
} : undefined}
>
{opt}
</button>
))}
</div>
)}
{controlType === 'boolean' && (
<button
onClick={() => onChange(!value)}
className={`relative w-11 h-6 rounded-full transition-all duration-200 ${
value ? 'bg-indigo-500' : isDark ? 'bg-gray-700' : 'bg-gray-300'
}`}
>
<div
className={`absolute top-0.5 w-5 h-5 rounded-full bg-white shadow-md transition-transform duration-200 ${
value ? 'translate-x-[22px]' : 'translate-x-0.5'
}`}
/>
</button>
)}
</div>
);
};
export default VariableControl;