feat: add QR code generator tool

Add a sixth tool with live SVG preview, customizable foreground/background
colors, error correction level, margin control, and export as PNG (256–2048px)
or SVG. URL params enable shareable state. All processing runs client-side
via the qrcode package.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 00:58:57 +01:00
parent 695ba434e2
commit f917891a31
13 changed files with 776 additions and 16 deletions

View File

@@ -0,0 +1,137 @@
'use client';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Slider } from '@/components/ui/slider';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
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;
}
const EC_OPTIONS: { value: ErrorCorrectionLevel; label: string }[] = [
{ value: 'L', label: 'Low (7%)' },
{ value: 'M', label: 'Medium (15%)' },
{ value: 'Q', label: 'Quartile (25%)' },
{ value: 'H', label: 'High (30%)' },
];
export function QROptions({
errorCorrection,
foregroundColor,
backgroundColor,
margin,
onErrorCorrectionChange,
onForegroundColorChange,
onBackgroundColorChange,
onMarginChange,
}: QROptionsProps) {
const isTransparent = backgroundColor === '#00000000';
return (
<Card>
<CardHeader>
<CardTitle>Options</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{/* Error Correction */}
<div className="space-y-1.5">
<Label className="text-xs">Error Correction</Label>
<Select value={errorCorrection} onValueChange={(v) => onErrorCorrectionChange(v as ErrorCorrectionLevel)}>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
{EC_OPTIONS.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Colors */}
<div className="space-y-3">
<div className="space-y-1.5">
<Label className="text-xs">Foreground</Label>
<div className="flex gap-2">
<Input
type="color"
className="w-9 p-1 h-9 shrink-0"
value={foregroundColor}
onChange={(e) => onForegroundColorChange(e.target.value)}
/>
<Input
className="font-mono text-xs"
value={foregroundColor}
onChange={(e) => onForegroundColorChange(e.target.value)}
/>
</div>
</div>
<div className="space-y-1.5">
<div className="flex items-center justify-between">
<Label className="text-xs">Background</Label>
<Button
variant={isTransparent ? 'default' : 'outline'}
size="xs"
className="h-5 text-[10px] px-1.5"
onClick={() =>
onBackgroundColorChange(isTransparent ? '#ffffff' : '#00000000')
}
>
Transparent
</Button>
</div>
<div className="flex gap-2">
<Input
type="color"
className="w-9 p-1 h-9 shrink-0"
disabled={isTransparent}
value={backgroundColor}
onChange={(e) => onBackgroundColorChange(e.target.value)}
/>
<Input
className="font-mono text-xs"
disabled={isTransparent}
value={backgroundColor}
onChange={(e) => onBackgroundColorChange(e.target.value)}
/>
</div>
</div>
</div>
{/* Margin */}
<div className="space-y-1.5">
<div className="flex items-center justify-between">
<Label className="text-xs">Margin</Label>
<span className="text-xs text-muted-foreground">{margin}</span>
</div>
<Slider
value={[margin]}
onValueChange={([v]) => onMarginChange(v)}
min={0}
max={8}
step={1}
/>
</div>
</CardContent>
</Card>
);
}