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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user