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

19
app/(app)/qrcode/page.tsx Normal file
View File

@@ -0,0 +1,19 @@
import type { Metadata } from 'next';
import { QRCodeGenerator } from '@/components/qrcode/QRCodeGenerator';
import { AppPage } from '@/components/layout/AppPage';
import { getToolByHref } from '@/lib/tools';
const tool = getToolByHref('/qrcode')!;
export const metadata: Metadata = { title: tool.title };
export default function QRCodePage() {
return (
<AppPage
title={tool.title}
description={tool.description}
>
<QRCodeGenerator />
</AppPage>
);
}

View File

@@ -36,3 +36,15 @@ export const FaviconIcon = (props: React.SVGProps<SVGSVGElement>) => (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
</svg> </svg>
); );
export const QRCodeIcon = (props: React.SVGProps<SVGSVGElement>) => (
<svg {...props} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<rect x="3" y="3" width="7" height="7" rx="1" strokeWidth={2} />
<rect x="14" y="3" width="7" height="7" rx="1" strokeWidth={2} />
<rect x="3" y="14" width="7" height="7" rx="1" strokeWidth={2} />
<rect x="14" y="14" width="3" height="3" strokeWidth={2} />
<rect x="18" y="18" width="3" height="3" strokeWidth={2} />
<line x1="14" y1="18" x2="17" y2="18" strokeWidth={2} />
<line x1="18" y1="14" x2="18" y2="17" strokeWidth={2} />
</svg>
);

View File

@@ -11,7 +11,7 @@ import { addRecentFont } from '@/lib/storage/favorites';
import { decodeFromUrl, updateUrl, getShareableUrl } from '@/lib/utils/urlSharing'; import { decodeFromUrl, updateUrl, getShareableUrl } from '@/lib/utils/urlSharing';
import { toast } from 'sonner'; import { toast } from 'sonner';
import type { ASCIIFont } from '@/types/ascii'; import type { ASCIIFont } from '@/types/ascii';
import { Card, CardContent } from '../ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
export function ASCIIConverter() { export function ASCIIConverter() {
const [text, setText] = React.useState('ASCII'); const [text, setText] = React.useState('ASCII');
@@ -121,7 +121,11 @@ export function ASCIIConverter() {
{/* Left Column - Input and Preview */} {/* Left Column - Input and Preview */}
<div className="lg:col-span-2 space-y-6 overflow-y-auto custom-scrollbar"> <div className="lg:col-span-2 space-y-6 overflow-y-auto custom-scrollbar">
<Card> <Card>
<CardHeader>
<CardTitle>Text</CardTitle>
</CardHeader>
<CardContent> <CardContent>
<TextInput <TextInput
value={text} value={text}
onChange={setText} onChange={setText}

View File

@@ -0,0 +1,145 @@
'use client';
import * as React from 'react';
import { QRInput } from './QRInput';
import { QRPreview } from './QRPreview';
import { QROptions } from './QROptions';
import { generateSvg, generateDataUrl } from '@/lib/qrcode/qrcodeService';
import { decodeQRFromUrl, updateQRUrl, getQRShareableUrl } from '@/lib/qrcode/urlSharing';
import { downloadBlob } from '@/lib/media/utils/fileUtils';
import { debounce } from '@/lib/utils/debounce';
import { toast } from 'sonner';
import type { ErrorCorrectionLevel, ExportSize } from '@/types/qrcode';
export function QRCodeGenerator() {
const [text, setText] = React.useState('https://kit.pivoine.art');
const [errorCorrection, setErrorCorrection] = React.useState<ErrorCorrectionLevel>('M');
const [foregroundColor, setForegroundColor] = React.useState('#000000');
const [backgroundColor, setBackgroundColor] = React.useState('#ffffff');
const [margin, setMargin] = React.useState(4);
const [exportSize, setExportSize] = React.useState<ExportSize>(512);
const [svgString, setSvgString] = React.useState('');
const [isGenerating, setIsGenerating] = React.useState(false);
// Load state from URL on mount
React.useEffect(() => {
const urlState = decodeQRFromUrl();
if (urlState) {
if (urlState.text !== undefined) setText(urlState.text);
if (urlState.errorCorrection) setErrorCorrection(urlState.errorCorrection);
if (urlState.foregroundColor) setForegroundColor(urlState.foregroundColor);
if (urlState.backgroundColor) setBackgroundColor(urlState.backgroundColor);
if (urlState.margin !== undefined) setMargin(urlState.margin);
}
}, []);
// Debounced generation
const generate = React.useMemo(
() =>
debounce(async (t: string, ec: ErrorCorrectionLevel, fg: string, bg: string, m: number) => {
if (!t) {
setSvgString('');
setIsGenerating(false);
return;
}
setIsGenerating(true);
try {
const svg = await generateSvg(t, ec, fg, bg, m);
setSvgString(svg);
} catch (error) {
console.error('QR generation error:', error);
setSvgString('');
toast.error('Failed to generate QR code. Text may be too long.');
} finally {
setIsGenerating(false);
}
}, 200),
[],
);
// Regenerate on changes
React.useEffect(() => {
generate(text, errorCorrection, foregroundColor, backgroundColor, margin);
updateQRUrl(text, errorCorrection, foregroundColor, backgroundColor, margin);
}, [text, errorCorrection, foregroundColor, backgroundColor, margin, generate]);
// Export: PNG download
const handleDownloadPng = async () => {
if (!text) return;
try {
const dataUrl = await generateDataUrl(text, errorCorrection, foregroundColor, backgroundColor, margin, exportSize);
const res = await fetch(dataUrl);
const blob = await res.blob();
downloadBlob(blob, `qrcode-${Date.now()}.png`);
} catch {
toast.error('Failed to export PNG');
}
};
// Export: SVG download
const handleDownloadSvg = () => {
if (!svgString) return;
const blob = new Blob([svgString], { type: 'image/svg+xml' });
downloadBlob(blob, `qrcode-${Date.now()}.svg`);
};
// Copy image to clipboard
const handleCopyImage = async () => {
if (!text) return;
try {
const dataUrl = await generateDataUrl(text, errorCorrection, foregroundColor, backgroundColor, margin, exportSize);
const res = await fetch(dataUrl);
const blob = await res.blob();
await navigator.clipboard.write([
new ClipboardItem({ 'image/png': blob }),
]);
toast.success('Image copied to clipboard!');
} catch {
toast.error('Failed to copy image');
}
};
// Share URL
const handleShare = async () => {
const shareUrl = getQRShareableUrl(text, errorCorrection, foregroundColor, backgroundColor, margin);
try {
await navigator.clipboard.writeText(shareUrl);
toast.success('Shareable URL copied!');
} catch {
toast.error('Failed to copy URL');
}
};
return (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 items-stretch lg:max-h-[800px]">
{/* Left Column - Input and Options */}
<div className="lg:col-span-1 space-y-6 overflow-y-auto custom-scrollbar">
<QRInput value={text} onChange={setText} />
<QROptions
errorCorrection={errorCorrection}
foregroundColor={foregroundColor}
backgroundColor={backgroundColor}
margin={margin}
onErrorCorrectionChange={setErrorCorrection}
onForegroundColorChange={setForegroundColor}
onBackgroundColorChange={setBackgroundColor}
onMarginChange={setMargin}
/>
</div>
{/* Right Column - Preview */}
<div className="lg:col-span-2 h-full">
<QRPreview
svgString={svgString}
isGenerating={isGenerating}
exportSize={exportSize}
onExportSizeChange={setExportSize}
onCopyImage={handleCopyImage}
onShare={handleShare}
onDownloadPng={handleDownloadPng}
onDownloadSvg={handleDownloadSvg}
/>
</div>
</div>
);
}

View File

@@ -0,0 +1,34 @@
'use client';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Textarea } from '@/components/ui/textarea';
interface QRInputProps {
value: string;
onChange: (value: string) => void;
}
const MAX_LENGTH = 2048;
export function QRInput({ value, onChange }: QRInputProps) {
return (
<Card>
<CardHeader>
<CardTitle>Text</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<Textarea
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder="Enter text or URL..."
maxLength={MAX_LENGTH}
rows={3}
className="resize-none font-mono text-sm"
/>
<div className="text-[10px] text-muted-foreground text-right">
{value.length} / {MAX_LENGTH}
</div>
</CardContent>
</Card>
);
}

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>
);
}

View File

@@ -0,0 +1,132 @@
'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 type { ExportSize } from '@/types/qrcode';
interface QRPreviewProps {
svgString: string;
isGenerating: boolean;
exportSize: ExportSize;
onExportSizeChange: (size: ExportSize) => void;
onCopyImage: () => void;
onShare: () => void;
onDownloadPng: () => void;
onDownloadSvg: () => void;
}
export function QRPreview({
svgString,
isGenerating,
exportSize,
onExportSizeChange,
onCopyImage,
onShare,
onDownloadPng,
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>
<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>
<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>
</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>
);
}

View File

@@ -0,0 +1,39 @@
import QRCode from 'qrcode';
import type { ErrorCorrectionLevel } from '@/types/qrcode';
export async function generateSvg(
text: string,
errorCorrection: ErrorCorrectionLevel,
foregroundColor: string,
backgroundColor: string,
margin: number,
): Promise<string> {
return QRCode.toString(text, {
type: 'svg',
errorCorrectionLevel: errorCorrection,
color: {
dark: foregroundColor,
light: backgroundColor,
},
margin,
});
}
export async function generateDataUrl(
text: string,
errorCorrection: ErrorCorrectionLevel,
foregroundColor: string,
backgroundColor: string,
margin: number,
size: number,
): Promise<string> {
return QRCode.toDataURL(text, {
errorCorrectionLevel: errorCorrection,
color: {
dark: foregroundColor,
light: backgroundColor,
},
margin,
width: size,
});
}

85
lib/qrcode/urlSharing.ts Normal file
View File

@@ -0,0 +1,85 @@
'use client';
import type { ErrorCorrectionLevel } from '@/types/qrcode';
export interface QRShareableState {
text?: string;
errorCorrection?: ErrorCorrectionLevel;
foregroundColor?: string;
backgroundColor?: string;
margin?: number;
}
const DEFAULTS = {
errorCorrection: 'M' as ErrorCorrectionLevel,
foregroundColor: '#000000',
backgroundColor: '#ffffff',
margin: 4,
};
export function decodeQRFromUrl(): QRShareableState | null {
if (typeof window === 'undefined') return null;
const params = new URLSearchParams(window.location.search);
const text = params.get('text');
const ec = params.get('ec') as ErrorCorrectionLevel | null;
const fg = params.get('fg');
const bg = params.get('bg');
const margin = params.get('margin');
if (!text && !ec && !fg && !bg && !margin) return null;
return {
text: text || undefined,
errorCorrection: ec || undefined,
foregroundColor: fg ? `#${fg}` : undefined,
backgroundColor: bg ? `#${bg}` : undefined,
margin: margin ? parseInt(margin, 10) : undefined,
};
}
export function updateQRUrl(
text: string,
errorCorrection: ErrorCorrectionLevel,
foregroundColor: string,
backgroundColor: string,
margin: number,
): void {
if (typeof window === 'undefined') return;
const params = new URLSearchParams();
if (text) params.set('text', text);
if (errorCorrection !== DEFAULTS.errorCorrection) params.set('ec', errorCorrection);
if (foregroundColor !== DEFAULTS.foregroundColor) params.set('fg', foregroundColor.replace('#', ''));
if (backgroundColor !== DEFAULTS.backgroundColor) params.set('bg', backgroundColor.replace('#', ''));
if (margin !== DEFAULTS.margin) params.set('margin', String(margin));
const query = params.toString();
const newUrl = query
? `${window.location.pathname}?${query}`
: window.location.pathname;
window.history.replaceState({}, '', newUrl);
}
export function getQRShareableUrl(
text: string,
errorCorrection: ErrorCorrectionLevel,
foregroundColor: string,
backgroundColor: string,
margin: number,
): string {
if (typeof window === 'undefined') return '';
const params = new URLSearchParams();
if (text) params.set('text', text);
if (errorCorrection !== DEFAULTS.errorCorrection) params.set('ec', errorCorrection);
if (foregroundColor !== DEFAULTS.foregroundColor) params.set('fg', foregroundColor.replace('#', ''));
if (backgroundColor !== DEFAULTS.backgroundColor) params.set('bg', backgroundColor.replace('#', ''));
if (margin !== DEFAULTS.margin) params.set('margin', String(margin));
const query = params.toString();
return `${window.location.origin}${window.location.pathname}${query ? `?${query}` : ''}`;
}

View File

@@ -1,4 +1,4 @@
import { ColorIcon, UnitsIcon, ASCIIIcon, MediaIcon, FaviconIcon } from '@/components/AppIcons'; import { ColorIcon, UnitsIcon, ASCIIIcon, MediaIcon, FaviconIcon, QRCodeIcon } from '@/components/AppIcons';
export interface Tool { export interface Tool {
/** Short display name (e.g. "Color") */ /** Short display name (e.g. "Color") */
@@ -15,10 +15,6 @@ export interface Tool {
summary: string; summary: string;
/** Icon component */ /** Icon component */
icon: React.ElementType; icon: React.ElementType;
/** Tailwind gradient utility class for the landing card */
gradient: string;
/** Hex accent color for the landing card */
accentColor: string;
/** Badge labels for the landing card */ /** Badge labels for the landing card */
badges: string[]; badges: string[];
} }
@@ -33,8 +29,6 @@ export const tools: Tool[] = [
summary: summary:
'Modern color manipulation toolkit with palette generation, accessibility testing, and format conversion. Supports hex, RGB, HSL, Lab, and more.', 'Modern color manipulation toolkit with palette generation, accessibility testing, and format conversion. Supports hex, RGB, HSL, Lab, and more.',
icon: ColorIcon, icon: ColorIcon,
gradient: 'gradient-indigo-purple',
accentColor: '#a855f7',
badges: ['Open Source', 'WCAG', 'Free'], badges: ['Open Source', 'WCAG', 'Free'],
}, },
{ {
@@ -46,8 +40,6 @@ export const tools: Tool[] = [
summary: summary:
'Smart unit converter with 187 units across 23 categories. Real-time bidirectional conversion with fuzzy search.', 'Smart unit converter with 187 units across 23 categories. Real-time bidirectional conversion with fuzzy search.',
icon: UnitsIcon, icon: UnitsIcon,
gradient: 'gradient-cyan-purple',
accentColor: '#2dd4bf',
badges: ['Open Source', 'Real-time', 'Free'], badges: ['Open Source', 'Real-time', 'Free'],
}, },
{ {
@@ -59,8 +51,6 @@ export const tools: Tool[] = [
summary: summary:
'ASCII art text generator with 373 fonts. Create stunning text banners, terminal art, and retro designs with live preview and multiple export formats.', 'ASCII art text generator with 373 fonts. Create stunning text banners, terminal art, and retro designs with live preview and multiple export formats.',
icon: ASCIIIcon, icon: ASCIIIcon,
gradient: 'gradient-yellow-amber',
accentColor: '#eab308',
badges: ['Open Source', 'ASCII Art', 'Free'], badges: ['Open Source', 'ASCII Art', 'Free'],
}, },
{ {
@@ -72,8 +62,6 @@ export const tools: Tool[] = [
summary: summary:
'Modern browser-based file converter powered by WebAssembly. Convert videos, images, and audio locally without server uploads. Privacy-first with no file size limits.', 'Modern browser-based file converter powered by WebAssembly. Convert videos, images, and audio locally without server uploads. Privacy-first with no file size limits.',
icon: MediaIcon, icon: MediaIcon,
gradient: 'gradient-green-teal',
accentColor: '#10b981',
badges: ['Open Source', 'Converter', 'Free'], badges: ['Open Source', 'Converter', 'Free'],
}, },
{ {
@@ -85,8 +73,17 @@ export const tools: Tool[] = [
summary: summary:
'Generate a complete set of favicons for your website. Includes PWA manifest and HTML embed code. All processing happens locally in your browser.', 'Generate a complete set of favicons for your website. Includes PWA manifest and HTML embed code. All processing happens locally in your browser.',
icon: FaviconIcon, icon: FaviconIcon,
gradient: 'gradient-blue-cyan', badges: ['Open Source', 'Generator', 'Free'],
accentColor: '#3b82f6', },
{
shortTitle: 'QR Code',
title: 'QR Code Generator',
navTitle: 'QR Code Generator',
href: '/qrcode',
description: 'Generate QR codes with custom colors, error correction, and multi-format export.',
summary:
'Generate QR codes with live preview, customizable colors, error correction levels, and export as PNG or SVG. All processing happens locally in your browser.',
icon: QRCodeIcon,
badges: ['Open Source', 'Generator', 'Free'], badges: ['Open Source', 'Generator', 'Free'],
}, },
]; ];

View File

@@ -27,6 +27,7 @@
"jszip": "^3.10.1", "jszip": "^3.10.1",
"lucide-react": "^0.575.0", "lucide-react": "^0.575.0",
"next": "^16.1.6", "next": "^16.1.6",
"qrcode": "^1.5.4",
"radix-ui": "^1.4.3", "radix-ui": "^1.4.3",
"react": "^19.2.4", "react": "^19.2.4",
"react-colorful": "^5.6.1", "react-colorful": "^5.6.1",
@@ -40,6 +41,7 @@
"@tailwindcss/postcss": "^4.2.0", "@tailwindcss/postcss": "^4.2.0",
"@types/figlet": "^1.7.0", "@types/figlet": "^1.7.0",
"@types/node": "^25.3.0", "@types/node": "^25.3.0",
"@types/qrcode": "^1.5.6",
"@types/react": "^19.2.14", "@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"eslint": "^9.21.0", "eslint": "^9.21.0",

142
pnpm-lock.yaml generated
View File

@@ -59,6 +59,9 @@ importers:
next: next:
specifier: ^16.1.6 specifier: ^16.1.6
version: 16.1.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) version: 16.1.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
qrcode:
specifier: ^1.5.4
version: 1.5.4
radix-ui: radix-ui:
specifier: ^1.4.3 specifier: ^1.4.3
version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -93,6 +96,9 @@ importers:
'@types/node': '@types/node':
specifier: ^25.3.0 specifier: ^25.3.0
version: 25.3.0 version: 25.3.0
'@types/qrcode':
specifier: ^1.5.6
version: 1.5.6
'@types/react': '@types/react':
specifier: ^19.2.14 specifier: ^19.2.14
version: 19.2.14 version: 19.2.14
@@ -1520,6 +1526,9 @@ packages:
'@types/prismjs@1.26.6': '@types/prismjs@1.26.6':
resolution: {integrity: sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==} resolution: {integrity: sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==}
'@types/qrcode@1.5.6':
resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==}
'@types/react-dom@19.2.3': '@types/react-dom@19.2.3':
resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
peerDependencies: peerDependencies:
@@ -1861,6 +1870,10 @@ packages:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
camelcase@5.3.1:
resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
engines: {node: '>=6'}
caniuse-lite@1.0.30001772: caniuse-lite@1.0.30001772:
resolution: {integrity: sha512-mIwLZICj+ntVTw4BT2zfp+yu/AqV6GMKfJVJMx3MwPxs+uk/uj2GLl2dH8LQbjiLDX66amCga5nKFyDgRR43kg==} resolution: {integrity: sha512-mIwLZICj+ntVTw4BT2zfp+yu/AqV6GMKfJVJMx3MwPxs+uk/uj2GLl2dH8LQbjiLDX66amCga5nKFyDgRR43kg==}
@@ -1890,6 +1903,9 @@ packages:
client-only@0.0.1: client-only@0.0.1:
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
cliui@6.0.0:
resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
cliui@8.0.1: cliui@8.0.1:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
@@ -2015,6 +2031,10 @@ packages:
supports-color: supports-color:
optional: true optional: true
decamelize@1.2.0:
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
engines: {node: '>=0.10.0'}
dedent@1.7.1: dedent@1.7.1:
resolution: {integrity: sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==} resolution: {integrity: sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==}
peerDependencies: peerDependencies:
@@ -2065,6 +2085,9 @@ packages:
resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==} resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==}
engines: {node: '>=0.3.1'} engines: {node: '>=0.3.1'}
dijkstrajs@1.0.3:
resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
doctrine@2.1.0: doctrine@2.1.0:
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -2366,6 +2389,10 @@ packages:
resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==}
engines: {node: '>= 18.0.0'} engines: {node: '>= 18.0.0'}
find-up@4.1.0:
resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
engines: {node: '>=8'}
find-up@5.0.0: find-up@5.0.0:
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -2933,6 +2960,10 @@ packages:
lines-and-columns@1.2.4: lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
locate-path@5.0.0:
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
engines: {node: '>=8'}
locate-path@6.0.0: locate-path@6.0.0:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -3224,14 +3255,26 @@ packages:
resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
p-limit@2.3.0:
resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
engines: {node: '>=6'}
p-limit@3.1.0: p-limit@3.1.0:
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
p-locate@4.1.0:
resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
engines: {node: '>=8'}
p-locate@5.0.0: p-locate@5.0.0:
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
engines: {node: '>=10'} engines: {node: '>=10'}
p-try@2.2.0:
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
engines: {node: '>=6'}
package-manager-detector@1.6.0: package-manager-detector@1.6.0:
resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==}
@@ -3293,6 +3336,10 @@ packages:
resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==}
engines: {node: '>=16.20.0'} engines: {node: '>=16.20.0'}
pngjs@5.0.0:
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
engines: {node: '>=10.13.0'}
possible-typed-array-names@1.1.0: possible-typed-array-names@1.1.0:
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -3344,6 +3391,11 @@ packages:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'} engines: {node: '>=6'}
qrcode@1.5.4:
resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
engines: {node: '>=10.13.0'}
hasBin: true
qs@6.15.0: qs@6.15.0:
resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==}
engines: {node: '>=0.6'} engines: {node: '>=0.6'}
@@ -3443,6 +3495,9 @@ packages:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
require-main-filename@2.0.0:
resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
resolve-from@4.0.0: resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'} engines: {node: '>=4'}
@@ -3520,6 +3575,9 @@ packages:
resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==}
engines: {node: '>= 18'} engines: {node: '>= 18'}
set-blocking@2.0.0:
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
set-function-length@1.2.2: set-function-length@1.2.2:
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -3886,6 +3944,9 @@ packages:
resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
which-module@2.0.1:
resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
which-typed-array@1.1.20: which-typed-array@1.1.20:
resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -3919,6 +3980,9 @@ packages:
resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==} resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==}
engines: {node: '>=20'} engines: {node: '>=20'}
y18n@4.0.3:
resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
y18n@5.0.8: y18n@5.0.8:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -3926,10 +3990,18 @@ packages:
yallist@3.1.1: yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
yargs-parser@18.1.3:
resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
engines: {node: '>=6'}
yargs-parser@21.1.1: yargs-parser@21.1.1:
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
engines: {node: '>=12'} engines: {node: '>=12'}
yargs@15.4.1:
resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
engines: {node: '>=8'}
yargs@17.7.2: yargs@17.7.2:
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
engines: {node: '>=12'} engines: {node: '>=12'}
@@ -5410,6 +5482,10 @@ snapshots:
'@types/prismjs@1.26.6': {} '@types/prismjs@1.26.6': {}
'@types/qrcode@1.5.6':
dependencies:
'@types/node': 25.3.0
'@types/react-dom@19.2.3(@types/react@19.2.14)': '@types/react-dom@19.2.3(@types/react@19.2.14)':
dependencies: dependencies:
'@types/react': 19.2.14 '@types/react': 19.2.14
@@ -5772,6 +5848,8 @@ snapshots:
callsites@3.1.0: {} callsites@3.1.0: {}
camelcase@5.3.1: {}
caniuse-lite@1.0.30001772: {} caniuse-lite@1.0.30001772: {}
chalk@4.1.2: chalk@4.1.2:
@@ -5795,6 +5873,12 @@ snapshots:
client-only@0.0.1: {} client-only@0.0.1: {}
cliui@6.0.0:
dependencies:
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi: 6.2.0
cliui@8.0.1: cliui@8.0.1:
dependencies: dependencies:
string-width: 4.2.3 string-width: 4.2.3
@@ -5902,6 +5986,8 @@ snapshots:
dependencies: dependencies:
ms: 2.1.3 ms: 2.1.3
decamelize@1.2.0: {}
dedent@1.7.1: {} dedent@1.7.1: {}
deep-is@0.1.4: {} deep-is@0.1.4: {}
@@ -5937,6 +6023,8 @@ snapshots:
diff@8.0.3: {} diff@8.0.3: {}
dijkstrajs@1.0.3: {}
doctrine@2.1.0: doctrine@2.1.0:
dependencies: dependencies:
esutils: 2.0.3 esutils: 2.0.3
@@ -6423,6 +6511,11 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
find-up@4.1.0:
dependencies:
locate-path: 5.0.0
path-exists: 4.0.0
find-up@5.0.0: find-up@5.0.0:
dependencies: dependencies:
locate-path: 6.0.0 locate-path: 6.0.0
@@ -6914,6 +7007,10 @@ snapshots:
lines-and-columns@1.2.4: {} lines-and-columns@1.2.4: {}
locate-path@5.0.0:
dependencies:
p-locate: 4.1.0
locate-path@6.0.0: locate-path@6.0.0:
dependencies: dependencies:
p-locate: 5.0.0 p-locate: 5.0.0
@@ -7255,14 +7352,24 @@ snapshots:
object-keys: 1.1.1 object-keys: 1.1.1
safe-push-apply: 1.0.0 safe-push-apply: 1.0.0
p-limit@2.3.0:
dependencies:
p-try: 2.2.0
p-limit@3.1.0: p-limit@3.1.0:
dependencies: dependencies:
yocto-queue: 0.1.0 yocto-queue: 0.1.0
p-locate@4.1.0:
dependencies:
p-limit: 2.3.0
p-locate@5.0.0: p-locate@5.0.0:
dependencies: dependencies:
p-limit: 3.1.0 p-limit: 3.1.0
p-try@2.2.0: {}
package-manager-detector@1.6.0: {} package-manager-detector@1.6.0: {}
pako@1.0.11: {} pako@1.0.11: {}
@@ -7304,6 +7411,8 @@ snapshots:
pkce-challenge@5.0.1: {} pkce-challenge@5.0.1: {}
pngjs@5.0.0: {}
possible-typed-array-names@1.1.0: {} possible-typed-array-names@1.1.0: {}
postcss-selector-parser@7.1.1: postcss-selector-parser@7.1.1:
@@ -7357,6 +7466,12 @@ snapshots:
punycode@2.3.1: {} punycode@2.3.1: {}
qrcode@1.5.4:
dependencies:
dijkstrajs: 1.0.3
pngjs: 5.0.0
yargs: 15.4.1
qs@6.15.0: qs@6.15.0:
dependencies: dependencies:
side-channel: 1.1.0 side-channel: 1.1.0
@@ -7518,6 +7633,8 @@ snapshots:
require-from-string@2.0.2: {} require-from-string@2.0.2: {}
require-main-filename@2.0.0: {}
resolve-from@4.0.0: {} resolve-from@4.0.0: {}
resolve-pkg-maps@1.0.0: {} resolve-pkg-maps@1.0.0: {}
@@ -7616,6 +7733,8 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
set-blocking@2.0.0: {}
set-function-length@1.2.2: set-function-length@1.2.2:
dependencies: dependencies:
define-data-property: 1.1.4 define-data-property: 1.1.4
@@ -8105,6 +8224,8 @@ snapshots:
is-weakmap: 2.0.2 is-weakmap: 2.0.2
is-weakset: 2.0.4 is-weakset: 2.0.4
which-module@2.0.1: {}
which-typed-array@1.1.20: which-typed-array@1.1.20:
dependencies: dependencies:
available-typed-arrays: 1.0.7 available-typed-arrays: 1.0.7
@@ -8144,12 +8265,33 @@ snapshots:
is-wsl: 3.1.1 is-wsl: 3.1.1
powershell-utils: 0.1.0 powershell-utils: 0.1.0
y18n@4.0.3: {}
y18n@5.0.8: {} y18n@5.0.8: {}
yallist@3.1.1: {} yallist@3.1.1: {}
yargs-parser@18.1.3:
dependencies:
camelcase: 5.3.1
decamelize: 1.2.0
yargs-parser@21.1.1: {} yargs-parser@21.1.1: {}
yargs@15.4.1:
dependencies:
cliui: 6.0.0
decamelize: 1.2.0
find-up: 4.1.0
get-caller-file: 2.0.5
require-directory: 2.1.1
require-main-filename: 2.0.0
set-blocking: 2.0.0
string-width: 4.2.3
which-module: 2.0.1
y18n: 4.0.3
yargs-parser: 18.1.3
yargs@17.7.2: yargs@17.7.2:
dependencies: dependencies:
cliui: 8.0.1 cliui: 8.0.1

12
types/qrcode.ts Normal file
View File

@@ -0,0 +1,12 @@
export type ErrorCorrectionLevel = 'L' | 'M' | 'Q' | 'H';
export type ExportSize = 256 | 512 | 1024 | 2048;
export interface QRCodeOptions {
text: string;
errorCorrection: ErrorCorrectionLevel;
foregroundColor: string;
backgroundColor: string;
margin: number;
size: ExportSize;
}