qr-kit 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,90 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
+ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ---
9
+
10
+ ## [2.1.0] — Logo overlay, Web Worker, AbortSignal
11
+
12
+ ### Added
13
+
14
+ **Logo overlay** (`utils/logo.js`)
15
+ - `makeQrWithLogoSvg(model, logoDataUrl, opts)` — zero-DOM SVG with embedded logo. Works in Node, Deno, Cloudflare Workers, Web Workers, and browser. Function modules always rendered above the logo layer for scan reliability.
16
+ - `getLogoConstraints(model, size, margin, maxCoverage)` — returns `maxLogoSize`, `paddedSize`, and `coverageFraction` so you can pre-validate a logo before building the SVG.
17
+ - `loadLogoAsDataUrl(src, { signal })` — browser helper: URL → base64 data URL.
18
+ - `buildQrWithLogoSvgAsync(model, src, opts)` — browser convenience that loads logo and builds SVG in one call.
19
+ - `LOGO_MAX_COVERAGE_ECC_M` (0.11) and `LOGO_MAX_COVERAGE_ECC_L` (0.04) — exported constants for coverage budget.
20
+
21
+ **Web Worker** (`worker/qr.worker.js`)
22
+ - `qr.worker.js` — Worker entry point that runs `makeQr` off the main thread. Transfers `Uint8Array` buffers via `postMessage` to avoid memory copies.
23
+ - `useQrWorker(value, opts)` React hook — async QR computation in a shared background Worker. Use for v7–12 or real-time input animations to prevent frame drops.
24
+
25
+ **AbortSignal support**
26
+ - `buildQrCompositeBlob({ signal })` — cancellable poster composition.
27
+ - `buildPdfWithTemplateBytes({ signal })` — cancellable PDF generation.
28
+ - `loadLogoAsDataUrl(src, { signal })` — cancellable logo fetch.
29
+ - `loadImage(src, { signal })` (raster.js) — AbortSignal propagated to image loading.
30
+
31
+ ### Changed
32
+ - `package.json` bumped to v2.1.0.
33
+ - `scripts/size.js` updated to include new modules.
34
+ - `index.js` exports all new public APIs.
35
+
36
+ ---
37
+
38
+ ## [1.0.0] — Initial release
39
+
40
+ ### Added
41
+
42
+ **Core**
43
+ - `makeQr(text, opts)` — pure JS QR engine, Byte mode, versions 1–12, ECC L/M
44
+ - Reed-Solomon error correction with GF(256) exp/log tables
45
+ - All 8 mask patterns evaluated; lowest-penalty mask selected
46
+ - Correct remainder bits, dark module, version info (v7+), format info
47
+
48
+ **React component**
49
+ - `QRCodeGenerator` — renders an inline SVG via `React.forwardRef`
50
+ - `onEccFallback` prop — notifies caller when ECC M is silently downgraded to L
51
+ - Accessibility: `role="img"`, `aria-label`, `<title>`, `shapeRendering="crispEdges"`
52
+
53
+ **Layout**
54
+ - `computeLayout(moduleCount, size, margin)` — shared pixel geometry for SVG and canvas renderers; guarantees pixel-identical output
55
+
56
+ **Raster utilities** (`utils/raster.js`)
57
+ - `svgToPngDataURL(svgEl, scale)` — SVG → PNG data URL
58
+ - `pngDataURLtoJpegBytes(pngDataURL, quality)` — PNG data URL → JPEG bytes
59
+ - `dataURLToBytes(dataUrl)` — data URL → Uint8Array
60
+ - `dataURLToImage(dataUrl)` — data URL → HTMLImageElement
61
+ - `loadImage(src)` — URL → HTMLImageElement (crossOrigin: anonymous)
62
+ - `imageURLtoJpegBytes(src, quality)` — URL → JPEG bytes
63
+ - `downloadQrPng(svgEl, opts)` — downloads QR as PNG
64
+
65
+ **JPEG export** (`utils/jpegQr.js`)
66
+ - `downloadQrJpeg(opts)` — renders QR to canvas and downloads as JPEG
67
+ - `downloadBlob(blob, fileName)` — generic Blob download helper
68
+
69
+ **Poster / image composite** (`utils/poster.js`)
70
+ - `downloadQrComposite(opts)` — composites QR onto a background image and downloads
71
+
72
+ **PDF export** (`utils/pdf.js`)
73
+ - `downloadPdfWithTemplateImage(opts)` — full-page background + QR overlay PDF
74
+ - `downloadQrPdf(opts)` — simple A4 PDF with title, text, and QR
75
+ - Non-JPEG backgrounds (PNG, WebP, CMYK) auto-converted to RGB JPEG
76
+
77
+ **URL utilities** (`utils/url.js`)
78
+ - `sanitizeUrlForQR(input, opts)` — strips tracking params; optionally removes protocol
79
+ - `utf8ByteLen(s)` — UTF-8 byte length of a string
80
+
81
+ **Link builder** (`utils/link.js`)
82
+ - `buildQrLink(opts)` — encodes JSON payload as Base64 in a URL; binary-search trim to fit QR byte budget
83
+ - Strategies: `'trim'` | `'drop'` | `'error'`
84
+ - Guard for non-string `trimKey` values
85
+
86
+ **Tests** (zero dependencies, Node.js only)
87
+ - 47 tests across `qr-core`, `layout`, `url`, `link`
88
+
89
+ **TypeScript**
90
+ - Full declarations in `types/index.d.ts` for all public APIs
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yaroslav
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,323 @@
1
+ <div align="center">
2
+
3
+ # 🎯 qr-kit
4
+
5
+ **Complete QR code toolkit. Zero dependencies. 5.4 kB core.**
6
+
7
+ [![npm version](https://img.shields.io/npm/v/qr-kit?color=success)](https://www.npmjs.com/package/qr-kit)
8
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/qr-kit?label=gzip&color=success)](https://bundlephobia.com/package/qr-kit)
9
+ [![tests](https://img.shields.io/badge/tests-109%20passing-success)](tests/)
10
+ [![license](https://img.shields.io/npm/l/qr-kit)](LICENSE)
11
+
12
+ **[🛝 Live Playground](playground/index.html)** · **[📖 Docs](#installation)** · **[🎨 Examples](#quick-start)**
13
+
14
+ <img src="docs/hero-demo.gif" width="600" alt="QR code with logo overlay demo" />
15
+
16
+ *Generate QR codes with logos, export to PDF, optimize URLs — all in the browser, zero dependencies.*
17
+
18
+ </div>
19
+
20
+ ---
21
+
22
+ ## ✨ Why this library?
23
+
24
+ Every QR library on npm either:
25
+ - 📦 Pulls in 10+ dependencies
26
+ - 🐌 Bundles 150+ kB of minified code
27
+ - 🔨 Requires a build step (TypeScript, Babel, Webpack)
28
+ - 🌐 Works only in Node.js OR only in browser
29
+
30
+ **This library:**
31
+ - ✅ Zero dependencies — literally `"dependencies": {}`
32
+ - ✅ 5.4 kB gzipped core — smaller than most images on your page
33
+ - ✅ Ships as ES modules — use directly, no build step
34
+ - ✅ Works everywhere — Node, Deno, Cloudflare Workers, browser, Web Worker
35
+ - ✅ Logo overlay with ECC budget enforcement
36
+ - ✅ PDF export, poster generation, link optimization
37
+
38
+ ---
39
+
40
+ ## 🚀 Quick start
41
+
42
+ ### Install
43
+
44
+ ```bash
45
+ npm install qr-kit
46
+ ```
47
+
48
+ ### Basic QR in React (3 lines)
49
+
50
+ ```jsx
51
+ import QRCodeGenerator from 'qr-kit';
52
+
53
+ export default () => <QRCodeGenerator value="https://example.com" />;
54
+ ```
55
+
56
+ **Output:** Crisp SVG, single `<path>` element (not 500 `<rect>`), accessible, scales infinitely.
57
+
58
+ ---
59
+
60
+ ## 🎨 Logo overlay — the killer feature
61
+
62
+ ```jsx
63
+ import { makeQr } from 'qr-kit';
64
+ import { buildQrWithLogoSvgAsync } from 'qr-kit/utils/logo';
65
+
66
+ const model = makeQr('https://example.com', { eccLevel: 'M' });
67
+ const svg = await buildQrWithLogoSvgAsync(model, '/logo.svg', {
68
+ size: 400,
69
+ maxCoverage: 0.11, // 11% of QR area — safe for ECC M
70
+ });
71
+
72
+ document.body.innerHTML = svg; // self-contained SVG string
73
+ ```
74
+
75
+ **How it works:**
76
+ - QR rendered in 3 layers: data → logo → finder patterns (always on top)
77
+ - ECC M budget: 11% coverage leaves 4% safety margin
78
+ - Zero-DOM implementation — works in Node.js, Cloudflare Workers, anywhere
79
+
80
+ <div align="center">
81
+ <img src="docs/logo-overlay-example.png" width="300" alt="QR with company logo" />
82
+ </div>
83
+
84
+ ---
85
+
86
+ ## 📦 Tiny bundle, huge features
87
+
88
+ | Feature | Size (gzip) | Description |
89
+ |---------|-------------|-------------|
90
+ | **qr-core** | 4.7 kB | Pure QR engine — works everywhere |
91
+ | **React component** | +1.1 kB | SVG component with `forwardRef` |
92
+ | **Logo overlay** | +3.0 kB | Embed logos with ECC enforcement |
93
+ | **PDF export** | +4.4 kB | Generate PDFs in browser |
94
+ | **Link optimizer** | +2.1 kB | Fit URLs in QR byte budget |
95
+ | **Web Worker** | +0.8 kB | Off-thread for v10-12 |
96
+
97
+ **Total library:** 26.7 kB gzip
98
+ **Core only:** 5.4 kB gzip (qr-core + layout + url utilities)
99
+
100
+ Tree-shake what you don't need. Import only what you use.
101
+
102
+ ---
103
+
104
+ ## 🔥 More examples
105
+
106
+ ### Export to PNG/JPEG/PDF
107
+
108
+ ```js
109
+ import { downloadQrPng } from 'qr-kit/utils/raster';
110
+ import { downloadQrJpeg } from 'qr-kit/utils/jpegQr';
111
+ import { downloadQrPdf } from 'qr-kit/utils/pdf';
112
+
113
+ // PNG at 3× scale
114
+ await downloadQrPng(svgRef.current, { scale: 3 });
115
+
116
+ // JPEG (direct from canvas, no SVG round-trip)
117
+ await downloadQrJpeg({ value: 'https://example.com', size: 300 });
118
+
119
+ // PDF with title and metadata
120
+ await downloadQrPdf({
121
+ svgEl: svgRef.current,
122
+ title: 'Event QR Code',
123
+ org: 'Your Company',
124
+ url: 'https://example.com',
125
+ });
126
+ ```
127
+
128
+ ### Optimize URLs for QR (Link Builder)
129
+
130
+ ```js
131
+ import { buildQrLink } from 'qr-kit/utils/link';
132
+
133
+ const { qrUrl, fullUrl, trimmed } = buildQrLink({
134
+ baseUrl: 'https://example.com/app',
135
+ payload: { userId: 'abc123', campaign: 'summer2024promo' },
136
+ budget: 120, // target bytes
137
+ strategy: 'trim', // shorten campaign if needed
138
+ trimKey: 'campaign',
139
+ removeProtocol: true, // saves 8 bytes
140
+ });
141
+
142
+ // qrUrl: "example.com/app?d={...shortened...}"
143
+ // fullUrl: "https://example.com/app?d={...full payload...}"
144
+ ```
145
+
146
+ **Use case:** Generate short QR codes, redirect to full URLs on server.
147
+
148
+ ### Composite QR onto background image (Poster)
149
+
150
+ ```js
151
+ import { downloadQrComposite } from 'qr-kit/utils/poster';
152
+
153
+ await downloadQrComposite({
154
+ svgEl: qrRef.current,
155
+ templateSrc: '/event-poster.jpg',
156
+ qr: { x: 50, y: 400, size: 200 }, // position in px
157
+ scale: 2, // 2× for print
158
+ fileName: 'poster.jpg',
159
+ });
160
+ ```
161
+
162
+ <div align="center">
163
+ <img src="docs/poster-example.png" width="400" alt="QR on event poster" />
164
+ </div>
165
+
166
+ ### Branded QR (custom colors for finder patterns)
167
+
168
+ ```js
169
+ import { makeQrSvgString } from 'qr-kit/renderers/svg';
170
+
171
+ const svg = makeQrSvgString(model, {
172
+ fg: '#000', // data modules
173
+ fnColor: '#f97316', // finder patterns (3 corners)
174
+ bg: '#fff',
175
+ });
176
+ ```
177
+
178
+ ### Web Worker (no jank on v10-12)
179
+
180
+ ```jsx
181
+ import { useQrWorker } from 'qr-kit';
182
+
183
+ function MyQr({ url }) {
184
+ const { model, error, pending } = useQrWorker(url, { maxVersion: 10 });
185
+
186
+ if (pending) return <Spinner />;
187
+ return <canvas ref={useQrCanvas(model)} />;
188
+ }
189
+ ```
190
+
191
+ **When to use:**
192
+ - `useQrCode` (sync) — fast, zero overhead, v1-6
193
+ - `useQrWorker` (async) — prevents jank on v7-12 or real-time input
194
+
195
+ ---
196
+
197
+ ## 🎮 Interactive playground
198
+
199
+ Open [`playground/index.html`](playground/index.html) in your browser — no build, no server.
200
+
201
+ **Features:**
202
+ - 4 render modes: Basic, Rounded, Branded, Logo
203
+ - Link Builder with real-time byte counting
204
+ - PDF Export with metadata
205
+ - All export formats (SVG, PNG, JPEG)
206
+ - Live code examples
207
+
208
+ **Works offline.** Single HTML file, 53 kB.
209
+
210
+ ---
211
+
212
+ ## 🏗️ Architecture
213
+
214
+ Three layers, zero coupling:
215
+
216
+ **Layer 1 — Pure computation** (Node, Deno, Workers, browser)
217
+ ```js
218
+ import { makeQr } from 'qr-kit/qr/qr-core';
219
+ // → { modules: Uint8Array, functionMask: Uint8Array, version, size, eccLevel }
220
+ ```
221
+
222
+ **Layer 2 — Rendering adapters** (return data, no side effects)
223
+ ```js
224
+ import { makeQrSvgString } from 'qr-kit/renderers/svg';
225
+ import { renderQrToCanvas } from 'qr-kit/renderers/canvas';
226
+ ```
227
+
228
+ **Layer 3 — Browser actions** (downloads, side effects)
229
+ ```js
230
+ import { downloadQrPng } from 'qr-kit/utils/raster';
231
+ // Internally calls Layer 2 → triggers download
232
+ ```
233
+
234
+ **Design principle:** Functions return data, not perform actions.
235
+ Every `download*` function has a `build*Bytes` / `build*Blob` primitive.
236
+
237
+ ---
238
+
239
+ ## 📊 Bundle size comparison
240
+
241
+ | Library | Size (gzip) | Dependencies | Logo overlay |
242
+ |---------|-------------|--------------|--------------|
243
+ | **qr-kit** | **5.4 kB** | **0** | ✅ |
244
+ | qrcode | 29.8 kB | 4 | ❌ |
245
+ | qr-code-generator | 7.2 kB | 0 | ❌ |
246
+ | node-qrcode | 52.1 kB | 6 | ❌ |
247
+
248
+ *Core bundle size. Full library with all utilities: 26.7 kB gzip.*
249
+
250
+ ---
251
+
252
+ ## 🧪 Testing
253
+
254
+ ```bash
255
+ npm test # Run 109 tests
256
+ npm run size # Bundle size report
257
+ ```
258
+
259
+ **Test coverage:**
260
+ - ✅ 20 unit tests (qr-core, layout, svg, url, link, logo)
261
+ - ✅ 23 logo overlay tests (ECC budget, constraints, rendering)
262
+ - ✅ 8 property-based tests (500-10K random inputs)
263
+ - ✅ 8 golden file tests (regression against known outputs)
264
+
265
+ ---
266
+
267
+ ## 📚 API Reference
268
+
269
+ Full API docs: [`types/index.d.ts`](types/index.d.ts)
270
+
271
+ **Core:**
272
+ - `makeQr(text, opts)` — Generate QR model
273
+ - `getModule(model, x, y)` — Read module at position
274
+ - `isFunctionModule(model, x, y)` — Check if module is functional
275
+
276
+ **Renderers:**
277
+ - `makeQrPath(model, opts)` — SVG `<path>` d-attribute
278
+ - `makeQrPathSplit(model, opts)` — Separate data/function paths
279
+ - `makeQrSvgString(model, opts)` — Complete SVG markup
280
+ - `renderQrToCanvas(model, canvas, opts)` — Draw on canvas
281
+
282
+ **Logo overlay:**
283
+ - `makeQrWithLogoSvg(model, logoDataUrl, opts)` — SVG with logo
284
+ - `getLogoConstraints(model, size, margin)` — Max safe logo size
285
+ - `buildQrWithLogoSvgAsync(model, logoSrc, opts)` — Browser helper
286
+
287
+ **Exports:**
288
+ - `downloadQrPng(svgEl, opts)` — PNG export
289
+ - `downloadQrJpeg(opts)` — JPEG export (canvas-based)
290
+ - `downloadQrPdf(opts)` — PDF with QR + metadata
291
+ - `downloadQrComposite(opts)` — QR on background image
292
+
293
+ **Utilities:**
294
+ - `buildQrLink(opts)` — Optimize URL for QR byte budget
295
+ - `sanitizeUrlForQR(url, opts)` — Strip tracking params
296
+ - `utf8ByteLen(str)` — Count UTF-8 bytes
297
+
298
+ ---
299
+
300
+ ## 🤝 Contributing
301
+
302
+ See [`CONTRIBUTING.md`](CONTRIBUTING.md)
303
+
304
+ **Philosophy:**
305
+ - Zero dependencies, always
306
+ - No build step — ship source as ES modules
307
+ - Browser-only features are opt-in (deep imports)
308
+ - Every function is testable in Node.js
309
+
310
+ ---
311
+
312
+ ## 📝 License
313
+
314
+ MIT © [Your Name](https://github.com/your-org)
315
+
316
+ ---
317
+
318
+ ## 🌟 Show your support
319
+
320
+ If this library saved you time, give it a ⭐ on [GitHub](https://github.com/your-org/qr-code)!
321
+
322
+ **Built with ❤️ to prove that npm packages don't need dependencies to be powerful.**
323
+
@@ -0,0 +1,88 @@
1
+ // components/QRCodeGenerator.jsx
2
+ // React SVG component that renders a QR code using a single <path>.
3
+ // Uses React.forwardRef — pass ref={svgRef} to access the <svg> element
4
+ // for PNG/JPEG/PDF export via utils/raster or utils/poster.
5
+
6
+ import * as React from 'react';
7
+ import { useQrCode } from './useQrCode.js';
8
+ import { makeQrPath } from '../renderers/svg.js';
9
+ import { computeLayout } from '../utils/layout.js';
10
+
11
+ const DEFAULTS = { size: 256, margin: 16, fg: '#000', bg: '#fff', ecc: 'M', maxV: 6 };
12
+
13
+ /**
14
+ * Renders a QR code as an inline SVG.
15
+ *
16
+ * Uses a single <path> element instead of hundreds of <rect> elements:
17
+ * - Fewer DOM nodes, faster rendering
18
+ * - Single fill target — style with one CSS rule
19
+ * - Animatable
20
+ *
21
+ * @example
22
+ * const ref = useRef(null);
23
+ * <QRCodeGenerator ref={ref} value="https://example.com" rounded />
24
+ * await downloadQrPng(ref.current);
25
+ */
26
+ const QRCodeGenerator = React.forwardRef(function QRCodeGenerator(props, ref) {
27
+ const {
28
+ value,
29
+ size = DEFAULTS.size,
30
+ margin = DEFAULTS.margin,
31
+ title = 'QR Code',
32
+ ariaLabel = 'QR code',
33
+ fg = DEFAULTS.fg,
34
+ bg = DEFAULTS.bg,
35
+ eccLevel = DEFAULTS.ecc,
36
+ maxVersion = DEFAULTS.maxV,
37
+ rounded = false,
38
+ onEccFallback,
39
+ className,
40
+ } = props;
41
+
42
+ const { model, error } = useQrCode(value, {
43
+ eccLevel, maxVersion, fallbackToL: true, onEccFallback,
44
+ });
45
+
46
+ if (error) {
47
+ return (
48
+ <div
49
+ role="alert"
50
+ style={{
51
+ color: '#b91c1c', fontFamily: 'monospace',
52
+ border: '1px solid #fecaca', borderRadius: 8, padding: 12,
53
+ }}
54
+ >
55
+ QR error: {error.message}
56
+ </div>
57
+ );
58
+ }
59
+
60
+ if (!model) return null;
61
+
62
+ const { outer } = computeLayout(model.size, size, margin);
63
+ const d = makeQrPath(model, { size, margin, rounded });
64
+
65
+ return (
66
+ <svg
67
+ ref={ref}
68
+ className={className}
69
+ width={outer}
70
+ height={outer}
71
+ viewBox={`0 0 ${outer} ${outer}`}
72
+ role="img"
73
+ aria-label={ariaLabel}
74
+ xmlns="http://www.w3.org/2000/svg"
75
+ shapeRendering="crispEdges"
76
+ data-version={model.version}
77
+ data-modules={model.size}
78
+ data-ecc={model.eccLevel}
79
+ >
80
+ <title>{title}</title>
81
+ <rect width="100%" height="100%" fill={bg} />
82
+ <path fill={fg} d={d} />
83
+ </svg>
84
+ );
85
+ });
86
+
87
+ QRCodeGenerator.displayName = 'QRCodeGenerator';
88
+ export default QRCodeGenerator;
@@ -0,0 +1,59 @@
1
+ // components/useQrCode.js
2
+ // React hook for direct access to the QR model.
3
+ // Use this when you need to render QR with a custom renderer
4
+ // (WebGL, canvas, custom SVG, branded finder patterns, etc.)
5
+
6
+ import * as React from 'react';
7
+ import { makeQr, QrInputTooLongError } from '../qr/qr-core.js';
8
+
9
+ /**
10
+ * Computes a QR model for the given value and options.
11
+ * Returns the model directly — no rendering, no DOM.
12
+ *
13
+ * @param {string} value
14
+ * @param {object} [opts]
15
+ * @param {'L'|'M'} [opts.eccLevel='M']
16
+ * @param {number} [opts.maxVersion=6]
17
+ * @param {boolean} [opts.fallbackToL=true] - Auto-retry with ECC L if M is too long.
18
+ * @param {function} [opts.onEccFallback] - Called when ECC is downgraded.
19
+ * @returns {{ model: QRModel|null, error: Error|null, actualEccLevel: string|null }}
20
+ *
21
+ * @example
22
+ * function MyQr({ url }) {
23
+ * const { model, error } = useQrCode(url, { eccLevel: 'L' });
24
+ * if (error) return <p>Error: {error.message}</p>;
25
+ * if (!model) return null;
26
+ * // render model.modules however you want
27
+ * }
28
+ */
29
+ export function useQrCode(value, {
30
+ eccLevel = 'M',
31
+ maxVersion = 6,
32
+ fallbackToL = true,
33
+ onEccFallback,
34
+ } = {}) {
35
+ const onEccFallbackRef = React.useRef(onEccFallback);
36
+ React.useLayoutEffect(() => { onEccFallbackRef.current = onEccFallback; }, [onEccFallback]);
37
+
38
+ return React.useMemo(() => {
39
+ if (value == null || value === '') {
40
+ return { model: null, error: new Error('No value provided'), actualEccLevel: null };
41
+ }
42
+ try {
43
+ const model = makeQr(value, { eccLevel, maxVersion });
44
+ return { model, error: null, actualEccLevel: eccLevel };
45
+ } catch (e1) {
46
+ if (fallbackToL && eccLevel === 'M') {
47
+ try {
48
+ const model = makeQr(value, { eccLevel: 'L', maxVersion });
49
+ // Use setTimeout to avoid calling during render
50
+ setTimeout(() => onEccFallbackRef.current?.('M', 'L'), 0);
51
+ return { model, error: null, actualEccLevel: 'L' };
52
+ } catch (e2) {
53
+ return { model: null, error: e2, actualEccLevel: null };
54
+ }
55
+ }
56
+ return { model: null, error: e1, actualEccLevel: null };
57
+ }
58
+ }, [value, eccLevel, maxVersion, fallbackToL]);
59
+ }
@@ -0,0 +1,81 @@
1
+ // components/useQrWorker.js
2
+ // React hook that computes QR codes in a Web Worker to avoid main-thread jank.
3
+ //
4
+ // When to use vs useQrCode:
5
+ // - useQrCode: synchronous, zero overhead — use for v1-6 and non-animated UIs
6
+ // - useQrWorker: async, off-thread — use when animating input or targeting v7-12
7
+ // on low-end mobile where 10-15ms CPU time causes dropped frames
8
+ //
9
+ // @example
10
+ // function MyQr({ url }) {
11
+ // const { model, error, pending } = useQrWorker(url, { eccLevel: 'L', maxVersion: 10 });
12
+ // if (pending) return <Skeleton />;
13
+ // if (error) return <p>{error.message}</p>;
14
+ // return <canvas ref={canvasRef} />; // render model in useEffect
15
+ // }
16
+
17
+ import * as React from 'react';
18
+
19
+ let _worker = null;
20
+ let _callbacks = new Map();
21
+ let _nextId = 1;
22
+
23
+ /** Lazy-initialised shared worker instance. */
24
+ function getWorker() {
25
+ if (!_worker) {
26
+ _worker = new Worker(new URL('../worker/qr.worker.js', import.meta.url), { type: 'module' });
27
+ _worker.onmessage = ({ data }) => {
28
+ const cb = _callbacks.get(data.id);
29
+ if (cb) { _callbacks.delete(data.id); cb(data); }
30
+ };
31
+ _worker.onerror = (e) => {
32
+ // Broadcast error to all pending callbacks
33
+ for (const [id, cb] of _callbacks) { _callbacks.delete(id); cb({ id, error: String(e) }); }
34
+ };
35
+ }
36
+ return _worker;
37
+ }
38
+
39
+ /**
40
+ * Computes a QR model off the main thread via a shared Web Worker.
41
+ *
42
+ * @param {string} value
43
+ * @param {object} [opts]
44
+ * @param {'L'|'M'} [opts.eccLevel='M']
45
+ * @param {number} [opts.maxVersion=6]
46
+ * @returns {{ model: object|null, error: Error|null, pending: boolean }}
47
+ */
48
+ export function useQrWorker(value, { eccLevel = 'M', maxVersion = 6 } = {}) {
49
+ const [state, setState] = React.useState({ model: null, error: null, pending: false });
50
+
51
+ React.useEffect(() => {
52
+ if (!value) {
53
+ setState({ model: null, error: null, pending: false });
54
+ return;
55
+ }
56
+
57
+ let cancelled = false;
58
+ setState(s => ({ ...s, pending: true }));
59
+
60
+ const id = _nextId++;
61
+ const worker = getWorker();
62
+
63
+ _callbacks.set(id, ({ model, error }) => {
64
+ if (cancelled) return;
65
+ if (error) {
66
+ setState({ model: null, error: new Error(error), pending: false });
67
+ } else {
68
+ setState({ model, error: null, pending: false });
69
+ }
70
+ });
71
+
72
+ worker.postMessage({ id, value, eccLevel, maxVersion });
73
+
74
+ return () => {
75
+ cancelled = true;
76
+ _callbacks.delete(id);
77
+ };
78
+ }, [value, eccLevel, maxVersion]);
79
+
80
+ return state;
81
+ }