/** * Hoover — components/qr.js * * QR code SVG renderer with optional logo overlay. * Uses qrcode-svg library for generation. */ import { h } from '../vdom.js?v=9'; import { esc } from '../helpers.js?v=9'; import QRCode from '../../../vendor/qrcode-svg-1.1.0.js'; /** * Generate a QR code SVG string from text content. * * @param {object} props * @param {string} props.text - Text to encode * @param {number} [props.size=200] - QR code size in px * @param {number} [props.margin=2] - Quiet zone margin * @param {string} [props.ecLevel='Q'] - Error correction level (L/M/Q/H) * @param {string} [props.logo] - Base64 data URL for center logo * @param {number} [props.logoSize=40] - Logo size in px (when overlaying) * @param {string} [props.color] - Foreground color (default: #000000) * @param {string} [props.background] - Background color (default: #ffffff) * @returns {string} SVG markup string */ export function qrSVG(props = {}) { const { text, size = 200, margin = 2, ecLevel = 'Q', logo, logoSize = 40, color = '#000000', background = '#ffffff', } = props; if (!text) return ''; const qr = new QRCode({ content: text, container: 'svg', margin: margin, padding: 0, width: size, height: size, color: color, background: background, ecl: ecLevel, creambo: false, prettyprint: false, }); let svg = qr.svg(); // Add center logo overlay if provided if (logo) { const halfSize = size / 2; const halfLogo = logoSize / 2; const ns = 'http://www.w3.org/2000/svg'; const parser = new DOMParser(); const doc = parser.parseFromString(svg, 'image/svg+xml'); const rootSvg = doc.documentElement; const viewBox = rootSvg.getAttribute('viewBox') || `0 0 ${size} ${size}`; const newSvg = doc.createElementNS(ns, 'svg'); newSvg.setAttribute('xmlns', ns); newSvg.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink'); newSvg.setAttribute('width', size); newSvg.setAttribute('height', size); newSvg.setAttribute('viewBox', viewBox); // Clone original content const clone = rootSvg.cloneNode(true); while (clone.firstChild) { newSvg.appendChild(clone.firstChild); } // White background behind logo const bgRect = doc.createElementNS(ns, 'rect'); bgRect.setAttribute('x', halfSize - halfLogo - 4); bgRect.setAttribute('y', halfSize - halfLogo - 4); bgRect.setAttribute('width', logoSize + 8); bgRect.setAttribute('height', logoSize + 8); bgRect.setAttribute('fill', background); newSvg.appendChild(bgRect); // Logo image overlay const img = doc.createElementNS(ns, 'image'); img.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', logo); img.setAttribute('x', halfSize - halfLogo); img.setAttribute('y', halfSize - halfLogo); img.setAttribute('width', logoSize); img.setAttribute('height', logoSize); newSvg.appendChild(img); const serializer = new XMLSerializer(); svg = serializer.serializeToString(newSvg); } return svg; } /** * Render a QR code as a VNode with innerHTML for the SVG. * * @param {object} props * @param {string} props.text - Text to encode * @param {number} [props.size=200] - QR code size * @param {string} [props.logo] - Base64 data URL for logo * @param {number} [props.logoSize=40] - Logo overlay size * @returns {object} VNode */ export function QRCodeVNode(props = {}) { const svgString = qrSVG(props); if (!svgString) { return h('div', {}, h('span', { class: 'text-muted' }, 'No content')); } return h('div', { class: 'qr-code-container text-center', innerHTML: svgString, }); } /** * Logo upload widget — file input that produces base64 data URL. * * @param {object} props * @param {string} props.id - Input element ID * @param {function} props.onChange - Callback(logoBase64) on file select */ export function LogoUpload(props = {}) { const inputId = props.id || 'qr-logo-input'; return h('div', { class: 'mb-2' }, [ h('label', { class: 'form-label' }, 'Logo (optional)'), h('input', { type: 'file', id: inputId, accept: 'image/*', class: 'form-control', onChange: function (e) { const file = e.target.files?.[0]; if (!file || !props.onChange) return; const reader = new FileReader(); reader.onload = function (ev) { props.onChange(ev.target.result); }; reader.readAsDataURL(file); }, }), ]); }