2026-02-28 00:58:57 +01:00
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
import { Slider } from '@/components/ui/slider';
|
2026-03-01 08:37:39 +01:00
|
|
|
import { cn } from '@/lib/utils/cn';
|
2026-02-28 00:58:57 +01:00
|
|
|
import type { ErrorCorrectionLevel } from '@/types/qrcode';
|
|
|
|
|
|
|
|
|
|
interface QROptionsProps {
|
|
|
|
|
errorCorrection: ErrorCorrectionLevel;
|
|
|
|
|
foregroundColor: string;
|
|
|
|
|
backgroundColor: string;
|
|
|
|
|
margin: number;
|
|
|
|
|
onErrorCorrectionChange: (ec: ErrorCorrectionLevel) => void;
|
|
|
|
|
onForegroundColorChange: (color: string) => void;
|
|
|
|
|
onBackgroundColorChange: (color: string) => void;
|
|
|
|
|
onMarginChange: (margin: number) => void;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-01 08:37:39 +01:00
|
|
|
const EC_OPTIONS: { value: ErrorCorrectionLevel; label: string; desc: string }[] = [
|
|
|
|
|
{ value: 'L', label: 'L', desc: '7%' },
|
|
|
|
|
{ value: 'M', label: 'M', desc: '15%' },
|
|
|
|
|
{ value: 'Q', label: 'Q', desc: '25%' },
|
|
|
|
|
{ value: 'H', label: 'H', desc: '30%' },
|
2026-02-28 00:58:57 +01:00
|
|
|
];
|
|
|
|
|
|
2026-03-01 08:37:39 +01:00
|
|
|
const inputCls =
|
|
|
|
|
'w-full bg-transparent border border-border/40 rounded-lg px-3 py-1.5 text-xs font-mono outline-none focus:border-primary/50 transition-colors text-foreground/80 placeholder:text-muted-foreground/30';
|
|
|
|
|
|
2026-02-28 00:58:57 +01:00
|
|
|
export function QROptions({
|
|
|
|
|
errorCorrection,
|
|
|
|
|
foregroundColor,
|
|
|
|
|
backgroundColor,
|
|
|
|
|
margin,
|
|
|
|
|
onErrorCorrectionChange,
|
|
|
|
|
onForegroundColorChange,
|
|
|
|
|
onBackgroundColorChange,
|
|
|
|
|
onMarginChange,
|
|
|
|
|
}: QROptionsProps) {
|
|
|
|
|
const isTransparent = backgroundColor === '#00000000';
|
|
|
|
|
|
|
|
|
|
return (
|
2026-03-01 08:37:39 +01:00
|
|
|
<div className="space-y-5">
|
|
|
|
|
|
|
|
|
|
{/* Error Correction */}
|
|
|
|
|
<div>
|
|
|
|
|
<span className="text-[10px] font-semibold text-muted-foreground uppercase tracking-widest block mb-2">
|
|
|
|
|
Error Correction
|
|
|
|
|
</span>
|
|
|
|
|
<div className="flex gap-1.5">
|
|
|
|
|
{EC_OPTIONS.map((opt) => (
|
|
|
|
|
<button
|
|
|
|
|
key={opt.value}
|
|
|
|
|
onClick={() => onErrorCorrectionChange(opt.value)}
|
|
|
|
|
className={cn(
|
|
|
|
|
'flex-1 flex flex-col items-center py-2 rounded-lg border text-xs font-mono transition-all',
|
|
|
|
|
errorCorrection === opt.value
|
|
|
|
|
? 'bg-primary/10 border-primary/40 text-primary'
|
|
|
|
|
: 'border-border/30 text-muted-foreground hover:border-primary/30 hover:text-foreground'
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<span className="font-semibold">{opt.label}</span>
|
|
|
|
|
<span className="text-[9px] opacity-50 mt-0.5">{opt.desc}</span>
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
2026-02-28 00:58:57 +01:00
|
|
|
</div>
|
2026-03-01 08:37:39 +01:00
|
|
|
</div>
|
2026-02-28 00:58:57 +01:00
|
|
|
|
2026-03-01 08:37:39 +01:00
|
|
|
{/* Colors */}
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
<span className="text-[10px] font-semibold text-muted-foreground uppercase tracking-widest block">
|
|
|
|
|
Colors
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
|
|
{/* Foreground */}
|
|
|
|
|
<div>
|
|
|
|
|
<label className="text-[9px] text-muted-foreground/50 font-mono block mb-1.5">Foreground</label>
|
|
|
|
|
<div className="flex gap-1.5">
|
|
|
|
|
<input
|
|
|
|
|
type="color"
|
|
|
|
|
value={foregroundColor}
|
|
|
|
|
onChange={(e) => onForegroundColorChange(e.target.value)}
|
|
|
|
|
className="w-8 h-8 rounded-lg cursor-pointer border border-border/40 bg-transparent shrink-0 p-0.5"
|
|
|
|
|
/>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={foregroundColor}
|
|
|
|
|
onChange={(e) => onForegroundColorChange(e.target.value)}
|
|
|
|
|
className={inputCls}
|
|
|
|
|
/>
|
2026-02-28 00:58:57 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-03-01 08:37:39 +01:00
|
|
|
{/* Background */}
|
|
|
|
|
<div>
|
|
|
|
|
<div className="flex items-center justify-between mb-1.5">
|
|
|
|
|
<label className="text-[9px] text-muted-foreground/50 font-mono">Background</label>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => onBackgroundColorChange(isTransparent ? '#ffffff' : '#00000000')}
|
|
|
|
|
className={cn(
|
|
|
|
|
'text-[9px] font-mono px-1.5 py-0.5 rounded border transition-all',
|
|
|
|
|
isTransparent
|
|
|
|
|
? 'border-primary/40 text-primary bg-primary/10'
|
|
|
|
|
: 'border-border/30 text-muted-foreground/50 hover:border-primary/30 hover:text-primary'
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
Transparent
|
|
|
|
|
</button>
|
2026-02-28 00:58:57 +01:00
|
|
|
</div>
|
2026-03-01 08:37:39 +01:00
|
|
|
<div className="flex gap-1.5">
|
|
|
|
|
<input
|
|
|
|
|
type="color"
|
|
|
|
|
disabled={isTransparent}
|
|
|
|
|
value={isTransparent ? '#ffffff' : backgroundColor}
|
|
|
|
|
onChange={(e) => onBackgroundColorChange(e.target.value)}
|
|
|
|
|
className={cn(
|
|
|
|
|
'w-8 h-8 rounded-lg cursor-pointer border border-border/40 bg-transparent shrink-0 p-0.5 transition-opacity',
|
|
|
|
|
isTransparent && 'opacity-30 cursor-not-allowed'
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
disabled={isTransparent}
|
|
|
|
|
value={isTransparent ? 'transparent' : backgroundColor}
|
|
|
|
|
onChange={(e) => onBackgroundColorChange(e.target.value)}
|
|
|
|
|
className={cn(inputCls, isTransparent && 'opacity-30')}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Margin */}
|
|
|
|
|
<div>
|
|
|
|
|
<div className="flex items-center justify-between mb-2">
|
|
|
|
|
<span className="text-[10px] font-semibold text-muted-foreground uppercase tracking-widest">
|
|
|
|
|
Margin
|
|
|
|
|
</span>
|
|
|
|
|
<span className="text-[10px] text-muted-foreground/40 font-mono tabular-nums">{margin}</span>
|
2026-02-28 00:58:57 +01:00
|
|
|
</div>
|
2026-03-01 08:37:39 +01:00
|
|
|
<Slider
|
|
|
|
|
value={[margin]}
|
|
|
|
|
onValueChange={([v]) => onMarginChange(v)}
|
|
|
|
|
min={0}
|
|
|
|
|
max={8}
|
|
|
|
|
step={1}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-02-28 00:58:57 +01:00
|
|
|
);
|
|
|
|
|
}
|