diff --git a/app/(app)/qrcode/page.tsx b/app/(app)/qrcode/page.tsx new file mode 100644 index 0000000..7a9c6f5 --- /dev/null +++ b/app/(app)/qrcode/page.tsx @@ -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 ( + + + + ); +} diff --git a/components/AppIcons.tsx b/components/AppIcons.tsx index ccebc0a..acd7cb6 100644 --- a/components/AppIcons.tsx +++ b/components/AppIcons.tsx @@ -36,3 +36,15 @@ export const FaviconIcon = (props: React.SVGProps) => ( ); + +export const QRCodeIcon = (props: React.SVGProps) => ( + + + + + + + + + +); diff --git a/components/ascii/ASCIIConverter.tsx b/components/ascii/ASCIIConverter.tsx index 7f19ed3..d0ab233 100644 --- a/components/ascii/ASCIIConverter.tsx +++ b/components/ascii/ASCIIConverter.tsx @@ -11,7 +11,7 @@ import { addRecentFont } from '@/lib/storage/favorites'; import { decodeFromUrl, updateUrl, getShareableUrl } from '@/lib/utils/urlSharing'; import { toast } from 'sonner'; import type { ASCIIFont } from '@/types/ascii'; -import { Card, CardContent } from '../ui/card'; +import { Card, CardContent, CardHeader, CardTitle } from '../ui/card'; export function ASCIIConverter() { const [text, setText] = React.useState('ASCII'); @@ -121,7 +121,11 @@ export function ASCIIConverter() { {/* Left Column - Input and Preview */} + + Text + + ('M'); + const [foregroundColor, setForegroundColor] = React.useState('#000000'); + const [backgroundColor, setBackgroundColor] = React.useState('#ffffff'); + const [margin, setMargin] = React.useState(4); + const [exportSize, setExportSize] = React.useState(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 ( + + {/* Left Column - Input and Options */} + + + + + + {/* Right Column - Preview */} + + + + + ); +} diff --git a/components/qrcode/QRInput.tsx b/components/qrcode/QRInput.tsx new file mode 100644 index 0000000..fdf8edf --- /dev/null +++ b/components/qrcode/QRInput.tsx @@ -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 ( + + + Text + + + onChange(e.target.value)} + placeholder="Enter text or URL..." + maxLength={MAX_LENGTH} + rows={3} + className="resize-none font-mono text-sm" + /> + + {value.length} / {MAX_LENGTH} + + + + ); +} diff --git a/components/qrcode/QROptions.tsx b/components/qrcode/QROptions.tsx new file mode 100644 index 0000000..90ecd44 --- /dev/null +++ b/components/qrcode/QROptions.tsx @@ -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 ( + + + Options + + + {/* Error Correction */} + + Error Correction + onErrorCorrectionChange(v as ErrorCorrectionLevel)}> + + + + + {EC_OPTIONS.map((opt) => ( + + {opt.label} + + ))} + + + + + {/* Colors */} + + + Foreground + + onForegroundColorChange(e.target.value)} + /> + onForegroundColorChange(e.target.value)} + /> + + + + + Background + + onBackgroundColorChange(isTransparent ? '#ffffff' : '#00000000') + } + > + Transparent + + + + onBackgroundColorChange(e.target.value)} + /> + onBackgroundColorChange(e.target.value)} + /> + + + + + {/* Margin */} + + + Margin + {margin} + + onMarginChange(v)} + min={0} + max={8} + step={1} + /> + + + + ); +} diff --git a/components/qrcode/QRPreview.tsx b/components/qrcode/QRPreview.tsx new file mode 100644 index 0000000..29b5d66 --- /dev/null +++ b/components/qrcode/QRPreview.tsx @@ -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 ( + + + Preview + + + + + + Copy + + + Copy image to clipboard + + + + + + + Share + + + Copy shareable URL + + + + + + + + PNG + + + Download as PNG + + v && onExportSizeChange(Number(v) as ExportSize)} + variant="outline" + size="sm" + > + 256 + 512 + 1k + 2k + + + + + + + + SVG + + + Download as SVG + + + + + + {isGenerating ? ( + + ) : svgString ? ( + + ) : ( + + + + + + Enter text to generate a QR code + Type text or a URL in the input field above + + + )} + + + + ); +} diff --git a/lib/qrcode/qrcodeService.ts b/lib/qrcode/qrcodeService.ts new file mode 100644 index 0000000..584509b --- /dev/null +++ b/lib/qrcode/qrcodeService.ts @@ -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 { + 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 { + return QRCode.toDataURL(text, { + errorCorrectionLevel: errorCorrection, + color: { + dark: foregroundColor, + light: backgroundColor, + }, + margin, + width: size, + }); +} diff --git a/lib/qrcode/urlSharing.ts b/lib/qrcode/urlSharing.ts new file mode 100644 index 0000000..9c9fc1f --- /dev/null +++ b/lib/qrcode/urlSharing.ts @@ -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}` : ''}`; +} diff --git a/lib/tools.tsx b/lib/tools.tsx index 354bb68..a5a190e 100644 --- a/lib/tools.tsx +++ b/lib/tools.tsx @@ -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 { /** Short display name (e.g. "Color") */ @@ -15,10 +15,6 @@ export interface Tool { summary: string; /** Icon component */ 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 */ badges: string[]; } @@ -33,8 +29,6 @@ export const tools: Tool[] = [ summary: 'Modern color manipulation toolkit with palette generation, accessibility testing, and format conversion. Supports hex, RGB, HSL, Lab, and more.', icon: ColorIcon, - gradient: 'gradient-indigo-purple', - accentColor: '#a855f7', badges: ['Open Source', 'WCAG', 'Free'], }, { @@ -46,8 +40,6 @@ export const tools: Tool[] = [ summary: 'Smart unit converter with 187 units across 23 categories. Real-time bidirectional conversion with fuzzy search.', icon: UnitsIcon, - gradient: 'gradient-cyan-purple', - accentColor: '#2dd4bf', badges: ['Open Source', 'Real-time', 'Free'], }, { @@ -59,8 +51,6 @@ export const tools: Tool[] = [ summary: '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, - gradient: 'gradient-yellow-amber', - accentColor: '#eab308', badges: ['Open Source', 'ASCII Art', 'Free'], }, { @@ -72,8 +62,6 @@ export const tools: Tool[] = [ 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.', icon: MediaIcon, - gradient: 'gradient-green-teal', - accentColor: '#10b981', badges: ['Open Source', 'Converter', 'Free'], }, { @@ -85,8 +73,17 @@ export const tools: Tool[] = [ summary: '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, - gradient: 'gradient-blue-cyan', - accentColor: '#3b82f6', + badges: ['Open Source', 'Generator', 'Free'], + }, + { + 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'], }, ]; diff --git a/package.json b/package.json index 4324795..dab87bb 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "jszip": "^3.10.1", "lucide-react": "^0.575.0", "next": "^16.1.6", + "qrcode": "^1.5.4", "radix-ui": "^1.4.3", "react": "^19.2.4", "react-colorful": "^5.6.1", @@ -40,6 +41,7 @@ "@tailwindcss/postcss": "^4.2.0", "@types/figlet": "^1.7.0", "@types/node": "^25.3.0", + "@types/qrcode": "^1.5.6", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "eslint": "^9.21.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a9af904..d2d00df 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,6 +59,9 @@ importers: next: 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) + qrcode: + specifier: ^1.5.4 + version: 1.5.4 radix-ui: 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) @@ -93,6 +96,9 @@ importers: '@types/node': specifier: ^25.3.0 version: 25.3.0 + '@types/qrcode': + specifier: ^1.5.6 + version: 1.5.6 '@types/react': specifier: ^19.2.14 version: 19.2.14 @@ -1520,6 +1526,9 @@ packages: '@types/prismjs@1.26.6': resolution: {integrity: sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==} + '@types/qrcode@1.5.6': + resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==} + '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -1861,6 +1870,10 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + caniuse-lite@1.0.30001772: resolution: {integrity: sha512-mIwLZICj+ntVTw4BT2zfp+yu/AqV6GMKfJVJMx3MwPxs+uk/uj2GLl2dH8LQbjiLDX66amCga5nKFyDgRR43kg==} @@ -1890,6 +1903,9 @@ packages: client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -2015,6 +2031,10 @@ packages: supports-color: optional: true + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + dedent@1.7.1: resolution: {integrity: sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==} peerDependencies: @@ -2065,6 +2085,9 @@ packages: resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==} engines: {node: '>=0.3.1'} + dijkstrajs@1.0.3: + resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} + doctrine@2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} @@ -2366,6 +2389,10 @@ packages: resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} engines: {node: '>= 18.0.0'} + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -2933,6 +2960,10 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -3224,14 +3255,26 @@ packages: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + p-locate@5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + package-manager-detector@1.6.0: resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} @@ -3293,6 +3336,10 @@ packages: resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} 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: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -3344,6 +3391,11 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + qrcode@1.5.4: + resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==} + engines: {node: '>=10.13.0'} + hasBin: true + qs@6.15.0: resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} engines: {node: '>=0.6'} @@ -3443,6 +3495,9 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -3520,6 +3575,9 @@ packages: resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} engines: {node: '>= 18'} + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -3886,6 +3944,9 @@ packages: resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} engines: {node: '>= 0.4'} + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + which-typed-array@1.1.20: resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} engines: {node: '>= 0.4'} @@ -3919,6 +3980,9 @@ packages: resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==} engines: {node: '>=20'} + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -3926,10 +3990,18 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} + yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + yargs@17.7.2: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} @@ -5410,6 +5482,10 @@ snapshots: '@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)': dependencies: '@types/react': 19.2.14 @@ -5772,6 +5848,8 @@ snapshots: callsites@3.1.0: {} + camelcase@5.3.1: {} + caniuse-lite@1.0.30001772: {} chalk@4.1.2: @@ -5795,6 +5873,12 @@ snapshots: 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: dependencies: string-width: 4.2.3 @@ -5902,6 +5986,8 @@ snapshots: dependencies: ms: 2.1.3 + decamelize@1.2.0: {} + dedent@1.7.1: {} deep-is@0.1.4: {} @@ -5937,6 +6023,8 @@ snapshots: diff@8.0.3: {} + dijkstrajs@1.0.3: {} + doctrine@2.1.0: dependencies: esutils: 2.0.3 @@ -6423,6 +6511,11 @@ snapshots: transitivePeerDependencies: - supports-color + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -6914,6 +7007,10 @@ snapshots: lines-and-columns@1.2.4: {} + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -7255,14 +7352,24 @@ snapshots: object-keys: 1.1.1 safe-push-apply: 1.0.0 + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + p-locate@5.0.0: dependencies: p-limit: 3.1.0 + p-try@2.2.0: {} + package-manager-detector@1.6.0: {} pako@1.0.11: {} @@ -7304,6 +7411,8 @@ snapshots: pkce-challenge@5.0.1: {} + pngjs@5.0.0: {} + possible-typed-array-names@1.1.0: {} postcss-selector-parser@7.1.1: @@ -7357,6 +7466,12 @@ snapshots: 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: dependencies: side-channel: 1.1.0 @@ -7518,6 +7633,8 @@ snapshots: require-from-string@2.0.2: {} + require-main-filename@2.0.0: {} + resolve-from@4.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -7616,6 +7733,8 @@ snapshots: transitivePeerDependencies: - supports-color + set-blocking@2.0.0: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -8105,6 +8224,8 @@ snapshots: is-weakmap: 2.0.2 is-weakset: 2.0.4 + which-module@2.0.1: {} + which-typed-array@1.1.20: dependencies: available-typed-arrays: 1.0.7 @@ -8144,12 +8265,33 @@ snapshots: is-wsl: 3.1.1 powershell-utils: 0.1.0 + y18n@4.0.3: {} + y18n@5.0.8: {} yallist@3.1.1: {} + yargs-parser@18.1.3: + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + 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: dependencies: cliui: 8.0.1 diff --git a/types/qrcode.ts b/types/qrcode.ts new file mode 100644 index 0000000..3a35910 --- /dev/null +++ b/types/qrcode.ts @@ -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; +}