refactor: align QR code tool with Calculate/Media blueprint

- QRCodeGenerator: lg:grid-cols-5 layout (2/5 options, 3/5 preview);
  full viewport height; mobile Configure|Preview glass pill tabs
- QRInput: remove shadcn Textarea/Card; native <textarea> in glass panel
  section; character counter in monospace
- QROptions: remove shadcn Card/Label/Input/Button/Select; EC level as
  4 pill buttons with recovery % label; native color inputs + pickers;
  transparent toggle as small pill; keep shadcn Slider for margin
- QRPreview: remove shadcn Card/Button/Skeleton/ToggleGroup/Tooltip/Empty;
  glass card fills full height; PNG button with inline size pill group
  (256/512/1k/2k); empty state and pulse skeleton match other tools

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-01 08:37:39 +01:00
parent 7da20c37c1
commit 50cf5823f9
4 changed files with 275 additions and 267 deletions

View File

@@ -1,22 +1,7 @@
'use client';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Skeleton } from '@/components/ui/skeleton';
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
import {
Empty,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from '@/components/ui/empty';
import { Copy, Share2, Image as ImageIcon, FileCode, QrCode } from 'lucide-react';
import { cn } from '@/lib/utils/cn';
import type { ExportSize } from '@/types/qrcode';
interface QRPreviewProps {
@@ -30,6 +15,16 @@ interface QRPreviewProps {
onDownloadSvg: () => void;
}
const EXPORT_SIZES: { value: ExportSize; label: string }[] = [
{ value: 256, label: '256' },
{ value: 512, label: '512' },
{ value: 1024, label: '1k' },
{ value: 2048, label: '2k' },
];
const actionBtn =
'flex items-center gap-1 px-2.5 py-1 text-xs glass rounded-md border border-border/30 text-muted-foreground hover:text-primary hover:border-primary/30 hover:bg-primary/10 transition-all disabled:opacity-40 disabled:cursor-not-allowed';
export function QRPreview({
svgString,
isGenerating,
@@ -41,92 +36,81 @@ export function QRPreview({
onDownloadSvg,
}: QRPreviewProps) {
return (
<Card className="h-full flex flex-col">
<CardHeader className="flex flex-row items-center justify-between flex-wrap gap-2">
<CardTitle>Preview</CardTitle>
<div className="flex items-center gap-1.5 flex-wrap">
<Tooltip>
<TooltipTrigger asChild>
<Button variant="outline" size="xs" onClick={onCopyImage} disabled={!svgString}>
<Copy className="h-3 w-3 mr-1" />
Copy
</Button>
</TooltipTrigger>
<TooltipContent>Copy image to clipboard</TooltipContent>
</Tooltip>
<div className="glass rounded-xl p-4 flex flex-col flex-1 min-h-0 overflow-hidden">
<Tooltip>
<TooltipTrigger asChild>
<Button variant="outline" size="xs" onClick={onShare} disabled={!svgString}>
<Share2 className="h-3 w-3 mr-1" />
Share
</Button>
</TooltipTrigger>
<TooltipContent>Copy shareable URL</TooltipContent>
</Tooltip>
{/* Action bar */}
<div className="flex items-center gap-1.5 mb-4 shrink-0 flex-wrap">
<span className="text-[10px] font-semibold text-muted-foreground uppercase tracking-widest mr-auto">
Preview
</span>
<div className="flex items-center gap-1">
<Tooltip>
<TooltipTrigger asChild>
<Button variant="outline" size="xs" onClick={onDownloadPng} disabled={!svgString}>
<ImageIcon className="h-3 w-3 mr-1" />
PNG
</Button>
</TooltipTrigger>
<TooltipContent>Download as PNG</TooltipContent>
</Tooltip>
<ToggleGroup
type="single"
value={String(exportSize)}
onValueChange={(v) => v && onExportSizeChange(Number(v) as ExportSize)}
variant="outline"
size="sm"
>
<ToggleGroupItem value="256" className="h-6 px-1.5 min-w-0 text-[10px]">256</ToggleGroupItem>
<ToggleGroupItem value="512" className="h-6 px-1.5 min-w-0 text-[10px]">512</ToggleGroupItem>
<ToggleGroupItem value="1024" className="h-6 px-1.5 min-w-0 text-[10px]">1k</ToggleGroupItem>
<ToggleGroupItem value="2048" className="h-6 px-1.5 min-w-0 text-[10px]">2k</ToggleGroupItem>
</ToggleGroup>
<button onClick={onCopyImage} disabled={!svgString} className={actionBtn}>
<Copy className="w-3 h-3" />Copy
</button>
<button onClick={onShare} disabled={!svgString} className={actionBtn}>
<Share2 className="w-3 h-3" />Share
</button>
{/* PNG + inline size selector */}
<div className="flex items-center glass rounded-md border border-border/30">
<button
onClick={onDownloadPng}
disabled={!svgString}
className="flex items-center gap-1 pl-2.5 pr-1.5 py-1 text-xs text-muted-foreground hover:text-primary transition-all disabled:opacity-40 disabled:cursor-not-allowed border-r border-border/20"
>
<ImageIcon className="w-3 h-3" />PNG
</button>
<div className="flex items-center px-1 gap-0.5">
{EXPORT_SIZES.map(({ value, label }) => (
<button
key={value}
onClick={() => onExportSizeChange(value)}
className={cn(
'text-[9px] font-mono px-1.5 py-0.5 rounded transition-all',
exportSize === value
? 'text-primary bg-primary/10'
: 'text-muted-foreground/40 hover:text-muted-foreground'
)}
>
{label}
</button>
))}
</div>
</div>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="outline" size="xs" onClick={onDownloadSvg} disabled={!svgString}>
<FileCode className="h-3 w-3 mr-1" />
SVG
</Button>
</TooltipTrigger>
<TooltipContent>Download as SVG</TooltipContent>
</Tooltip>
</div>
</CardHeader>
<CardContent className="flex-1 flex flex-col">
<div className="flex-1 min-h-[200px] rounded-lg p-4 flex items-center justify-center"
style={{
backgroundImage: 'repeating-conic-gradient(hsl(var(--muted)) 0% 25%, transparent 0% 50%)',
backgroundSize: '16px 16px',
}}
>
{isGenerating ? (
<Skeleton className="h-[200px] w-[200px]" />
) : svgString ? (
<div
className="w-full max-w-[400px] aspect-square [&>svg]:w-full [&>svg]:h-full"
dangerouslySetInnerHTML={{ __html: svgString }}
/>
) : (
<Empty>
<EmptyHeader>
<EmptyMedia variant="icon">
<QrCode />
</EmptyMedia>
<EmptyTitle>Enter text to generate a QR code</EmptyTitle>
<EmptyDescription>Type text or a URL in the input field above</EmptyDescription>
</EmptyHeader>
</Empty>
)}
</div>
</CardContent>
</Card>
<button onClick={onDownloadSvg} disabled={!svgString} className={actionBtn}>
<FileCode className="w-3 h-3" />SVG
</button>
</div>
{/* QR canvas */}
<div
className="flex-1 min-h-0 rounded-xl flex items-center justify-center"
style={{
backgroundImage: 'repeating-conic-gradient(rgba(255,255,255,0.025) 0% 25%, transparent 0% 50%)',
backgroundSize: '16px 16px',
}}
>
{isGenerating ? (
<div className="w-56 h-56 rounded-xl bg-white/5 animate-pulse" />
) : svgString ? (
<div
className="w-full max-w-sm aspect-square [&>svg]:w-full [&>svg]:h-full p-6"
dangerouslySetInnerHTML={{ __html: svgString }}
/>
) : (
<div className="flex flex-col items-center gap-3 text-center">
<div className="w-14 h-14 rounded-full bg-primary/10 flex items-center justify-center">
<QrCode className="w-6 h-6 text-primary/40" />
</div>
<div>
<p className="text-sm font-medium text-foreground/40">No QR code yet</p>
<p className="text-[10px] text-muted-foreground/30 font-mono mt-1">Enter text or a URL to generate</p>
</div>
</div>
)}
</div>
</div>
);
}