Files
kit-ui/components/units/MainConverter.tsx
Sebastian Krüger 56c0d6403c refactor: go fully native — remove all remaining shadcn component usage
Replace shadcn Select → native <select>:
- ascii/FontPreview.tsx: comment-style picker → glass pill wrapper
  with MessageSquareCode icon + native select
- color/ExportMenu.tsx: format + color-space pickers → native select
  with shared selectCls
- units/MainConverter.tsx: from/to unit pickers → native select

Delete dead code:
- components/media/FormatSelector.tsx (not imported anywhere,
  used shadcn Input + Label + Card)
- components/ui/select.tsx  — now unused
- components/ui/input.tsx   — now unused
- components/ui/label.tsx   — now unused
- components/ui/card.tsx    — now unused

Remaining components/ui/:
  slider.tsx, tooltip.tsx (TooltipProvider in Providers.tsx),
  slider-row.tsx, color-input.tsx

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 13:25:02 +01:00

322 lines
13 KiB
TypeScript

'use client';
import { useState, useEffect, useCallback } from 'react';
import { ArrowLeftRight, BarChart3, Grid3X3 } from 'lucide-react';
import SearchUnits from './SearchUnits';
import VisualComparison from './VisualComparison';
import {
getAllMeasures,
getUnitsForMeasure,
convertToAll,
convertUnit,
formatMeasureName,
type Measure,
type ConversionResult,
} from '@/lib/units/units';
import { parseNumberInput, formatNumber, cn } from '@/lib/utils';
type Tab = 'category' | 'convert';
const CATEGORY_ICONS: Partial<Record<Measure, string>> = {
length: '📏', mass: '⚖️', temperature: '🌡️', speed: '⚡', time: '⏱️',
area: '⬛', volume: '🧊', digital: '💾', energy: '⚡', pressure: '🔵',
power: '🔆', frequency: '〰️', angle: '📐', current: '⚡', voltage: '🔌',
};
export default function MainConverter() {
const [selectedMeasure, setSelectedMeasure] = useState<Measure>('length');
const [selectedUnit, setSelectedUnit] = useState<string>('m');
const [targetUnit, setTargetUnit] = useState<string>('ft');
const [inputValue, setInputValue] = useState<string>('1');
const [conversions, setConversions] = useState<ConversionResult[]>([]);
const [showChart, setShowChart] = useState(false);
const [tab, setTab] = useState<Tab>('category');
const measures = getAllMeasures();
const units = getUnitsForMeasure(selectedMeasure);
useEffect(() => {
const numValue = parseNumberInput(inputValue);
if (numValue !== null && selectedUnit) {
setConversions(convertToAll(numValue, selectedUnit));
} else {
setConversions([]);
}
}, [inputValue, selectedUnit]);
useEffect(() => {
const availableUnits = getUnitsForMeasure(selectedMeasure);
if (availableUnits.length > 0) {
setSelectedUnit(availableUnits[0]);
setTargetUnit(availableUnits[1] ?? availableUnits[0]);
}
}, [selectedMeasure]);
const handleSwapUnits = useCallback(() => {
const numValue = parseNumberInput(inputValue);
if (numValue !== null) {
setInputValue(convertUnit(numValue, selectedUnit, targetUnit).toString());
}
setSelectedUnit(targetUnit);
setTargetUnit(selectedUnit);
}, [selectedUnit, targetUnit, inputValue]);
const handleSearchSelect = useCallback((unit: string, measure: Measure) => {
setSelectedMeasure(measure);
setSelectedUnit(unit);
setTab('convert');
}, []);
const handleCategorySelect = useCallback((measure: Measure) => {
setSelectedMeasure(measure);
setTab('convert');
}, []);
const handleValueChange = useCallback(
(value: number, unit: string, _dragging: boolean) => {
setInputValue(convertUnit(value, unit, selectedUnit).toString());
},
[selectedUnit]
);
const resultValue = (() => {
const n = parseNumberInput(inputValue);
return n !== null ? convertUnit(n, selectedUnit, targetUnit) : null;
})();
return (
<div className="flex flex-col gap-4">
{/* ── Mobile tab switcher ────────────────────────────────── */}
<div className="flex lg:hidden glass rounded-xl p-1 gap-1">
{(['category', 'convert'] as Tab[]).map((t) => (
<button
key={t}
onClick={() => setTab(t)}
className={cn(
'flex-1 py-2.5 rounded-lg text-sm font-medium capitalize transition-all',
tab === t
? 'bg-primary text-primary-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground'
)}
>
{t === 'category' ? 'Category' : 'Convert'}
</button>
))}
</div>
{/* ── Main layout ────────────────────────────────────────── */}
<div
className="grid grid-cols-1 lg:grid-cols-5 gap-4"
style={{ height: 'calc(100svh - 180px)' }}
>
{/* Left panel: search + categories */}
<div
className={cn(
'lg:col-span-2 flex flex-col gap-3 overflow-hidden',
tab !== 'category' && 'hidden lg:flex'
)}
>
{/* Search */}
<div className="glass rounded-xl p-4 shrink-0">
<span className="text-[10px] font-semibold text-muted-foreground uppercase tracking-widest block mb-2">
Search
</span>
<SearchUnits onSelectUnit={handleSearchSelect} />
</div>
{/* Category list */}
<div className="glass rounded-xl p-3 flex flex-col flex-1 min-h-0 overflow-hidden">
<div className="flex items-center justify-between mb-3 shrink-0">
<span className="text-[10px] font-semibold text-muted-foreground uppercase tracking-widest">
Categories
</span>
<span className="text-[10px] text-muted-foreground/35 font-mono tabular-nums">
{measures.length}
</span>
</div>
<div className="flex-1 min-h-0 overflow-y-auto scrollbar-thin scrollbar-thumb-primary/20 scrollbar-track-transparent space-y-0.5 pr-0.5">
{measures.map((measure) => {
const isSelected = selectedMeasure === measure;
const unitCount = getUnitsForMeasure(measure).length;
return (
<button
key={measure}
onClick={() => handleCategorySelect(measure)}
className={cn(
'w-full flex items-center gap-2 px-2 py-1.5 rounded-lg transition-all text-left',
'border-l-2',
isSelected
? 'bg-primary/10 border-primary text-primary'
: 'border-transparent text-foreground/65 hover:bg-primary/8 hover:text-foreground'
)}
>
<span className="text-xs leading-none shrink-0 opacity-70">
{CATEGORY_ICONS[measure] ?? '📦'}
</span>
<span className="flex-1 text-xs font-mono truncate">{formatMeasureName(measure)}</span>
<span
className={cn(
'text-[10px] font-mono tabular-nums shrink-0 px-1.5 py-0.5 rounded',
isSelected
? 'bg-primary/20 text-primary'
: 'bg-muted/40 text-muted-foreground/40'
)}
>
{unitCount}
</span>
</button>
);
})}
</div>
</div>
</div>
{/* Right panel: converter + results */}
<div
className={cn(
'lg:col-span-3 flex flex-col gap-3 overflow-hidden',
tab !== 'convert' && 'hidden lg:flex'
)}
>
{/* Converter card */}
<div className="glass rounded-xl p-4 shrink-0">
<span className="text-[10px] font-semibold text-muted-foreground uppercase tracking-widest block mb-3">
Convert {formatMeasureName(selectedMeasure)}
</span>
{/* Input row */}
<div className="flex items-center gap-2">
{/* Value input */}
<input
type="text"
inputMode="decimal"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="0"
className="flex-1 min-w-0 bg-transparent border border-border/40 rounded-lg px-3 py-2 text-sm font-mono outline-none focus:border-primary/50 transition-colors placeholder:text-muted-foreground/30 tabular-nums"
/>
{/* From unit */}
<select
value={selectedUnit}
onChange={(e) => setSelectedUnit(e.target.value)}
className="w-28 shrink-0 bg-transparent border border-border/40 rounded-lg px-2.5 py-2 text-xs font-mono outline-none focus:border-primary/50 transition-colors text-foreground/80 cursor-pointer"
>
{units.map((unit) => (
<option key={unit} value={unit}>{unit}</option>
))}
</select>
{/* Swap */}
<button
onClick={handleSwapUnits}
title="Swap units"
className="shrink-0 w-8 h-8 flex items-center justify-center glass rounded-lg border border-border/30 text-muted-foreground hover:text-primary hover:border-primary/30 hover:bg-primary/10 transition-all"
>
<ArrowLeftRight className="w-3.5 h-3.5" />
</button>
{/* To unit */}
<select
value={targetUnit}
onChange={(e) => setTargetUnit(e.target.value)}
className="w-28 shrink-0 bg-transparent border border-border/40 rounded-lg px-2.5 py-2 text-xs font-mono outline-none focus:border-primary/50 transition-colors text-foreground/80 cursor-pointer"
>
{units.map((unit) => (
<option key={unit} value={unit}>{unit}</option>
))}
</select>
</div>
{/* Result display */}
{resultValue !== null && (
<div className="mt-3 px-3 py-2.5 rounded-lg bg-primary/5 border border-primary/15">
<div className="text-[10px] text-muted-foreground/50 font-mono mb-0.5">Result</div>
<div className="flex items-baseline gap-2">
<span className="text-xl font-bold tabular-nums font-mono bg-gradient-to-r from-primary to-pink-400 bg-clip-text text-transparent">
{formatNumber(resultValue)}
</span>
<span className="text-sm text-muted-foreground/60 font-mono">{targetUnit}</span>
</div>
</div>
)}
</div>
{/* All conversions */}
<div className="glass rounded-xl p-3 flex flex-col flex-1 min-h-0 overflow-hidden">
<div className="flex items-center justify-between mb-3 shrink-0">
<span className="text-[10px] font-semibold text-muted-foreground uppercase tracking-widest">
All Conversions
</span>
{/* Grid / Chart toggle */}
<div className="flex glass rounded-lg p-0.5 gap-0.5">
<button
onClick={() => setShowChart(false)}
title="Grid view"
className={cn(
'flex items-center gap-1 px-2 py-1 rounded-md text-xs transition-all',
!showChart
? 'bg-primary text-primary-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground'
)}
>
<Grid3X3 className="w-3 h-3" />
</button>
<button
onClick={() => setShowChart(true)}
title="Chart view"
className={cn(
'flex items-center gap-1 px-2 py-1 rounded-md text-xs transition-all',
showChart
? 'bg-primary text-primary-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground'
)}
>
<BarChart3 className="w-3 h-3" />
</button>
</div>
</div>
<div className="flex-1 min-h-0 overflow-y-auto scrollbar-thin scrollbar-thumb-primary/20 scrollbar-track-transparent pr-0.5">
{showChart ? (
<VisualComparison conversions={conversions} onValueChange={handleValueChange} />
) : (
<div className="grid grid-cols-2 lg:grid-cols-3 gap-2">
{conversions.map((conversion) => {
const isTarget = targetUnit === conversion.unit;
return (
<button
key={conversion.unit}
onClick={() => setTargetUnit(conversion.unit)}
className={cn(
'p-2.5 rounded-lg border text-left transition-all',
isTarget
? 'border-primary/50 bg-primary/10 text-primary'
: 'border-border/30 hover:border-primary/30 hover:bg-primary/6 text-foreground/75'
)}
>
<div className="text-[10px] text-muted-foreground/50 font-mono truncate mb-0.5">
{conversion.unitInfo.plural}
</div>
<div className="text-sm font-bold tabular-nums font-mono leading-none">
{formatNumber(conversion.value)}
</div>
<div className="text-[10px] text-muted-foreground/40 font-mono mt-0.5">
{conversion.unit}
</div>
</button>
);
})}
</div>
)}
</div>
</div>
</div>
</div>
</div>
);
}