velvet-qr 1.5.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/LICENSE +21 -0
- package/README.md +603 -0
- package/dist/index.d.mts +542 -0
- package/dist/index.d.ts +542 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +4 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +85 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pooja Gohel
|
|
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,603 @@
|
|
|
1
|
+
# Velvet QR
|
|
2
|
+
|
|
3
|
+
> Generate beautiful QR codes effortlessly.
|
|
4
|
+
|
|
5
|
+
Part of the **Velvet** ecosystem of React developer libraries β `velvet-qr` is the easiest, most customizable, developer-friendly QR code component for React. Gradients, dot & eye styles, logos, and PNG/SVG/JPEG export, all fully typed.
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/velvet-qr)
|
|
8
|
+
[](./LICENSE)
|
|
9
|
+
|
|
10
|
+
**[Live demo & docs β](https://poojagohel.github.io/velvet-qr/)**
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Features
|
|
15
|
+
|
|
16
|
+
- π¨ **Fully customizable** β colors, gradients, dot styles, eye styles, rounded corners, transparent backgrounds
|
|
17
|
+
- π§΅ **Connected dot rendering** β "rounded"/"dots"/"classy"/"classy-rounded"/"extra-rounded" styles merge adjacent modules into a smooth connected shape, not isolated blobs
|
|
18
|
+
- πΌοΈ **Logo embedding** β center a logo with configurable size, padding, and border radius, with automatic module excavation for scannability
|
|
19
|
+
- π§© **Presets** β `QR_PRESETS` + `QRPresetPicker` for ready-made styles (gradient "Velvet" look, dark-mode "Midnight", and more)
|
|
20
|
+
- ποΈ **Batch generation** β `useQRBatch` / `QRBatchGenerator` for generating and bulk-downloading many QR codes at once
|
|
21
|
+
- π·οΈ **Frame / label templates** β `frame` prop bakes a "SCAN ME"-style banner or bordered label directly into the exported SVG/PNG/JPEG, so it survives print/flat-image sharing
|
|
22
|
+
- π· **QR scanner** β `useQRScanner` / `QRScanner` decode QR codes from a live camera or a static image, using the browser-native `BarcodeDetector` API where available and transparently falling back to the bundled `jsQR` decoder everywhere else β works in Chrome, Edge, Firefox, and Safari alike
|
|
23
|
+
- π **Local QR history** β `useQRHistory` / `QRHistoryPanel` remember recently generated configs in `localStorage` for quick re-use
|
|
24
|
+
- π **Opt-in local analytics** β `useQRAnalytics` tracks generate/download/copy/in-session-scan events *you* observe in your own page β see the honesty note below, it cannot see real-world camera scans
|
|
25
|
+
- π **Dynamic QR helpers** β `buildDynamicQrValue` / `useDynamicQr` help you build a QR that points at a stable URL under your own redirect endpoint, so the destination can change after printing (requires your own backend β see below)
|
|
26
|
+
- π¦ **Built-in content builders** β URL, plain text, email, phone, SMS, WiFi, vCard, and Google Maps/geo location
|
|
27
|
+
- β¬οΈ **One-line exports** β download as PNG, SVG, or JPEG, or copy the image straight to the clipboard
|
|
28
|
+
- πͺ **Hooks-first API** β `useQRGenerator`, `useQRDownload`, and `useQRBatch` for full control when you don't want the batteries-included components
|
|
29
|
+
- π **Strongly typed** β complete TypeScript definitions, no `any`
|
|
30
|
+
- β‘ **Tiny & tree-shakeable** β two runtime dependencies (`qrcode` for matrix encoding, `jsqr` for the scanner's cross-browser fallback decoding), ESM + CJS builds, `"use client"` banner for Next.js App Router
|
|
31
|
+
- βΏ **Accessible** β proper `role="img"` / `aria-label` on generated SVGs, including an accessible placeholder if `value` is empty or generation fails
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install velvet-qr
|
|
37
|
+
# or
|
|
38
|
+
yarn add velvet-qr
|
|
39
|
+
# or
|
|
40
|
+
pnpm add velvet-qr
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Requires React 18 or 19.
|
|
44
|
+
|
|
45
|
+
## Quick Start
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
import { VelvetQR } from "velvet-qr";
|
|
49
|
+
|
|
50
|
+
export default function Example() {
|
|
51
|
+
return (
|
|
52
|
+
<VelvetQR
|
|
53
|
+
value="https://poojagohel.github.io/velvet-qr/"
|
|
54
|
+
size={250}
|
|
55
|
+
foregroundColor="#000"
|
|
56
|
+
backgroundColor="#fff"
|
|
57
|
+
/>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Downloading & copying
|
|
63
|
+
|
|
64
|
+
`VelvetQR` exposes an imperative handle via `ref` β no extra wiring required:
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
import { useRef } from "react";
|
|
68
|
+
import { VelvetQR, type VelvetQRHandle } from "velvet-qr";
|
|
69
|
+
|
|
70
|
+
function DownloadableQR() {
|
|
71
|
+
const qrRef = useRef<VelvetQRHandle>(null);
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<>
|
|
75
|
+
<VelvetQR ref={qrRef} value="https://poojagohel.github.io/velvet-qr/" size={250} />
|
|
76
|
+
<button onClick={() => qrRef.current?.downloadPNG()}>Download PNG</button>
|
|
77
|
+
<button onClick={() => qrRef.current?.downloadSVG()}>Download SVG</button>
|
|
78
|
+
<button onClick={() => qrRef.current?.downloadJPEG()}>Download JPEG</button>
|
|
79
|
+
<button onClick={() => qrRef.current?.copyToClipboard()}>Copy image</button>
|
|
80
|
+
</>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Or use the ready-made toolbar:
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
import { useRef } from "react";
|
|
89
|
+
import { VelvetQR, QRDownloader, type VelvetQRHandle } from "velvet-qr";
|
|
90
|
+
|
|
91
|
+
function Example() {
|
|
92
|
+
const qrRef = useRef<VelvetQRHandle>(null);
|
|
93
|
+
return (
|
|
94
|
+
<>
|
|
95
|
+
<VelvetQR ref={qrRef} value="https://poojagohel.github.io/velvet-qr/" />
|
|
96
|
+
<QRDownloader qrRef={qrRef} />
|
|
97
|
+
</>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Gradients, dot styles, and a logo
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
<VelvetQR
|
|
106
|
+
value="https://poojagohel.github.io/velvet-qr/"
|
|
107
|
+
size={280}
|
|
108
|
+
dotStyle="extra-rounded"
|
|
109
|
+
eyeStyle="leaf"
|
|
110
|
+
cornerRadius={24}
|
|
111
|
+
gradient={{ type: "linear", colors: ["#7c3aed", "#ec4899"], rotation: 45 }}
|
|
112
|
+
logoImage="/logo.png"
|
|
113
|
+
logoSize={0.22}
|
|
114
|
+
logoPadding={6}
|
|
115
|
+
logoBorderRadius={12}
|
|
116
|
+
/>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Content builders
|
|
120
|
+
|
|
121
|
+
Every common QR use case has a small, pure helper that returns the correctly formatted string for `value`:
|
|
122
|
+
|
|
123
|
+
```tsx
|
|
124
|
+
import {
|
|
125
|
+
VelvetQR,
|
|
126
|
+
buildWifiValue,
|
|
127
|
+
buildVCardValue,
|
|
128
|
+
buildEmailValue,
|
|
129
|
+
buildSmsValue,
|
|
130
|
+
buildPhoneValue,
|
|
131
|
+
buildLocationValue,
|
|
132
|
+
buildGoogleMapsValue,
|
|
133
|
+
} from "velvet-qr";
|
|
134
|
+
|
|
135
|
+
// WiFi
|
|
136
|
+
<VelvetQR value={buildWifiValue({ ssid: "Velvet Cafe", password: "s3cret!", encryption: "WPA" })} />
|
|
137
|
+
|
|
138
|
+
// vCard / business card
|
|
139
|
+
<VelvetQR
|
|
140
|
+
value={buildVCardValue({
|
|
141
|
+
firstName: "Pooja",
|
|
142
|
+
lastName: "Gohel",
|
|
143
|
+
org: "Velvet UI",
|
|
144
|
+
title: "Founder",
|
|
145
|
+
phone: "+1 555 0100",
|
|
146
|
+
email: "hello@velvetui.dev",
|
|
147
|
+
website: "https://poojagohel.github.io/velvet-qr/",
|
|
148
|
+
})}
|
|
149
|
+
/>
|
|
150
|
+
|
|
151
|
+
// Email, SMS, phone, location
|
|
152
|
+
<VelvetQR value={buildEmailValue({ to: "hello@velvetui.dev", subject: "Hi!" })} />
|
|
153
|
+
<VelvetQR value={buildSmsValue({ phone: "+15550100", message: "Hello!" })} />
|
|
154
|
+
<VelvetQR value={buildPhoneValue("+15550100")} />
|
|
155
|
+
<VelvetQR value={buildLocationValue({ latitude: 37.7749, longitude: -122.4194 })} />
|
|
156
|
+
<VelvetQR value={buildGoogleMapsValue({ query: "Golden Gate Bridge" })} />
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### The customization panel
|
|
160
|
+
|
|
161
|
+
```tsx
|
|
162
|
+
import { useState } from "react";
|
|
163
|
+
import { VelvetQR, QRCustomizer, type QRCustomizerConfig } from "velvet-qr";
|
|
164
|
+
|
|
165
|
+
function Playground() {
|
|
166
|
+
const [config, setConfig] = useState<QRCustomizerConfig>({ value: "https://poojagohel.github.io/velvet-qr/" });
|
|
167
|
+
return (
|
|
168
|
+
<>
|
|
169
|
+
<VelvetQR {...config} />
|
|
170
|
+
<QRCustomizer value={config} onChange={setConfig} />
|
|
171
|
+
</>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Logo uploader
|
|
177
|
+
|
|
178
|
+
```tsx
|
|
179
|
+
import { useState } from "react";
|
|
180
|
+
import { VelvetQR, QRLogoUploader } from "velvet-qr";
|
|
181
|
+
|
|
182
|
+
function WithLogo() {
|
|
183
|
+
const [logo, setLogo] = useState<string | null>(null);
|
|
184
|
+
return (
|
|
185
|
+
<>
|
|
186
|
+
<VelvetQR value="https://poojagohel.github.io/velvet-qr/" logoImage={logo ?? undefined} />
|
|
187
|
+
<QRLogoUploader onLogoChange={setLogo} />
|
|
188
|
+
</>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Presets
|
|
194
|
+
|
|
195
|
+
Skip the styling decisions entirely with a curated set of ready-made looks:
|
|
196
|
+
|
|
197
|
+
```tsx
|
|
198
|
+
import { useState } from "react";
|
|
199
|
+
import { VelvetQR, QRPresetPicker, type VelvetQRProps } from "velvet-qr";
|
|
200
|
+
|
|
201
|
+
function PresetDemo() {
|
|
202
|
+
const [config, setConfig] = useState<Partial<VelvetQRProps>>({});
|
|
203
|
+
return (
|
|
204
|
+
<>
|
|
205
|
+
<VelvetQR value="https://poojagohel.github.io/velvet-qr/" {...config} />
|
|
206
|
+
<QRPresetPicker onSelect={setConfig} />
|
|
207
|
+
</>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Or apply one directly without the picker UI:
|
|
213
|
+
|
|
214
|
+
```tsx
|
|
215
|
+
import { VelvetQR, QR_PRESETS } from "velvet-qr";
|
|
216
|
+
|
|
217
|
+
const velvet = QR_PRESETS.find((p) => p.id === "velvet")!;
|
|
218
|
+
<VelvetQR value="https://poojagohel.github.io/velvet-qr/" {...velvet.config} />;
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Batch generation
|
|
222
|
+
|
|
223
|
+
Generate and bulk-download many QR codes at once:
|
|
224
|
+
|
|
225
|
+
```tsx
|
|
226
|
+
import { QRBatchGenerator } from "velvet-qr";
|
|
227
|
+
|
|
228
|
+
function Batch() {
|
|
229
|
+
return (
|
|
230
|
+
<QRBatchGenerator
|
|
231
|
+
items={[
|
|
232
|
+
{ id: "table-1", value: "https://poojagohel.github.io/velvet-qr/menu?table=1" },
|
|
233
|
+
{ id: "table-2", value: "https://poojagohel.github.io/velvet-qr/menu?table=2" },
|
|
234
|
+
{ id: "table-3", value: "https://poojagohel.github.io/velvet-qr/menu?table=3" },
|
|
235
|
+
]}
|
|
236
|
+
sharedOptions={{ dotStyle: "rounded", eyeStyle: "circle" }}
|
|
237
|
+
/>
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Or drive it yourself with the underlying hook:
|
|
243
|
+
|
|
244
|
+
```tsx
|
|
245
|
+
import { useQRBatch } from "velvet-qr";
|
|
246
|
+
|
|
247
|
+
function CustomBatch() {
|
|
248
|
+
const { results, downloadAll } = useQRBatch(
|
|
249
|
+
[{ id: "a", value: "https://a.example" }, { id: "b", value: "https://b.example" }],
|
|
250
|
+
);
|
|
251
|
+
// render results.map(r => r.svgMarkup) however you like
|
|
252
|
+
// downloadAll('png' | 'svg' | 'jpeg') triggers one sequential download per item
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Hooks β full control
|
|
257
|
+
|
|
258
|
+
```tsx
|
|
259
|
+
import { useQRGenerator, useQRDownload } from "velvet-qr";
|
|
260
|
+
|
|
261
|
+
function Custom() {
|
|
262
|
+
const { svgMarkup, getDataUrl, isGenerating, error } = useQRGenerator({
|
|
263
|
+
value: "https://poojagohel.github.io/velvet-qr/",
|
|
264
|
+
size: 240,
|
|
265
|
+
});
|
|
266
|
+
// render svgMarkup however you like, or call getDataUrl('png' | 'jpeg' | 'svg')
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Frame / label templates
|
|
271
|
+
|
|
272
|
+
Bake a "SCAN ME" caption directly into the exported image β it's part of the generated SVG
|
|
273
|
+
(not a separate on-screen wrapper), so it's included in PNG/SVG/JPEG downloads too:
|
|
274
|
+
|
|
275
|
+
```tsx
|
|
276
|
+
<VelvetQR
|
|
277
|
+
value="https://poojagohel.github.io/velvet-qr/"
|
|
278
|
+
size={240}
|
|
279
|
+
frame={{ style: "banner-bottom", text: "SCAN ME" }}
|
|
280
|
+
/>
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
`frame.style` is one of `"none"` (default), `"banner-top"`, `"banner-bottom"`, or
|
|
284
|
+
`"border-label"`. `"banner-top"`/`"banner-bottom"` grow the rendered SVG to
|
|
285
|
+
`size` wide Γ `size + size * 0.2` tall (the caption band is ~20% of `size`); `"border-label"`
|
|
286
|
+
keeps the SVG at `size` Γ `size` and instead draws a border with the label in a small pill
|
|
287
|
+
straddling its bottom edge. Because banner styles change the overall height, always read the
|
|
288
|
+
actual `viewBox`/`width`/`height` off the generated markup (or the size `VelvetQR`'s own
|
|
289
|
+
`downloadPNG`/`downloadJPEG`/`getDataUrl` methods rasterize to) rather than assuming a square
|
|
290
|
+
`size x size` canvas when doing layout. Keep `frame.text` to roughly 20 characters or fewer
|
|
291
|
+
(14 or fewer for `border-label`) for reliably legible results β longer text is automatically
|
|
292
|
+
shrunk and, if needed, truncated with an ellipsis.
|
|
293
|
+
|
|
294
|
+
```tsx
|
|
295
|
+
<VelvetQR
|
|
296
|
+
value="https://poojagohel.github.io/velvet-qr/"
|
|
297
|
+
size={240}
|
|
298
|
+
frame={{
|
|
299
|
+
style: "border-label",
|
|
300
|
+
text: "SCAN ME",
|
|
301
|
+
backgroundColor: "#7c3aed",
|
|
302
|
+
textColor: "#ffffff",
|
|
303
|
+
cornerRadius: 16,
|
|
304
|
+
}}
|
|
305
|
+
/>
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### Animation
|
|
309
|
+
|
|
310
|
+
Add a purely decorative, on-screen-only reveal or "alive" effect for a landing-page hero β
|
|
311
|
+
it never affects scannability, and **exports (PNG/SVG/JPEG downloads, `getDataUrl`, and batch
|
|
312
|
+
`downloadAll`) are always the fully-settled, static QR regardless of `animation`**:
|
|
313
|
+
|
|
314
|
+
```tsx
|
|
315
|
+
<VelvetQR
|
|
316
|
+
value="https://poojagohel.github.io/velvet-qr/"
|
|
317
|
+
size={240}
|
|
318
|
+
animation={{ type: "fade-in" }} // "fade-in" | "draw-in" | "pulse" | "gradient-shift"
|
|
319
|
+
/>
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
- `"fade-in"` / `"draw-in"`: a one-shot staggered reveal (data modules fade in, or fade+scale
|
|
323
|
+
in) that plays once and settles at fully visible forever after.
|
|
324
|
+
- `"pulse"`: a continuous, gentle looping "breathing" effect for hero sections β never applied
|
|
325
|
+
to a `logoImage` if one is set, so the logo stays static/legible.
|
|
326
|
+
- `"gradient-shift"`: continuously rotates a `gradient` (no-ops back to `"none"` if `gradient`
|
|
327
|
+
isn't also set).
|
|
328
|
+
|
|
329
|
+
All animation types respect `prefers-reduced-motion: reduce`, falling back to the static,
|
|
330
|
+
fully-settled render. Tune timing with `duration` (seconds) and, for the reveal types,
|
|
331
|
+
`staggerDelay` (seconds of extra delay per module):
|
|
332
|
+
|
|
333
|
+
```tsx
|
|
334
|
+
<VelvetQR value="https://poojagohel.github.io/velvet-qr/" animation={{ type: "draw-in", duration: 1.5, staggerDelay: 0.004 }} />
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### Scanner
|
|
338
|
+
|
|
339
|
+
Decode QR codes from a live camera or a static image. Uses the browser-native
|
|
340
|
+
`BarcodeDetector` API where available (Chrome/Edge) and transparently falls back to the
|
|
341
|
+
bundled `jsQR` decoder everywhere else (Firefox, Safari, and any other browser), so scanning
|
|
342
|
+
works across all major browsers:
|
|
343
|
+
|
|
344
|
+
```tsx
|
|
345
|
+
import { QRScanner } from "velvet-qr";
|
|
346
|
+
|
|
347
|
+
function Scan() {
|
|
348
|
+
return (
|
|
349
|
+
<QRScanner
|
|
350
|
+
onDetected={(value) => console.log("Scanned:", value)}
|
|
351
|
+
continuous={false}
|
|
352
|
+
/>
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
Or drive it yourself with the underlying hook:
|
|
358
|
+
|
|
359
|
+
```tsx
|
|
360
|
+
import { useRef } from "react";
|
|
361
|
+
import { useQRScanner } from "velvet-qr";
|
|
362
|
+
|
|
363
|
+
function CustomScanner() {
|
|
364
|
+
const { isSupported, isScanning, result, error, startCamera, stopCamera, scanImage } = useQRScanner();
|
|
365
|
+
const videoRef = useRef<HTMLVideoElement>(null);
|
|
366
|
+
|
|
367
|
+
if (!isSupported) return <p>Camera scanning isn't supported in this browser.</p>;
|
|
368
|
+
|
|
369
|
+
return (
|
|
370
|
+
<>
|
|
371
|
+
<video ref={videoRef} muted playsInline />
|
|
372
|
+
<button onClick={() => videoRef.current && startCamera(videoRef.current)}>Start</button>
|
|
373
|
+
<button onClick={stopCamera}>Stop</button>
|
|
374
|
+
<input type="file" accept="image/*" onChange={(e) => e.target.files?.[0] && scanImage(e.target.files[0])} />
|
|
375
|
+
{result && <p>Scanned: {result}</p>}
|
|
376
|
+
{error && <p>Error: {error}</p>}
|
|
377
|
+
</>
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
`isSupported` is `false` (rather than throwing) only when neither `BarcodeDetector` nor the
|
|
383
|
+
`jsQR` canvas fallback can run at all β in practice that's just non-browser/SSR
|
|
384
|
+
environments. Every real browser reports `true`. Still always render a fallback (e.g. the
|
|
385
|
+
`QRScanner` component's built-in file-input path) for the rare environments where it isn't.
|
|
386
|
+
|
|
387
|
+
### History
|
|
388
|
+
|
|
389
|
+
`useQRHistory` keeps a small local record of recently generated/used QR configurations in
|
|
390
|
+
`localStorage` (SSR-safe, capped at `maxEntries`), and `QRHistoryPanel` renders it as a row of
|
|
391
|
+
live, clickable thumbnails:
|
|
392
|
+
|
|
393
|
+
```tsx
|
|
394
|
+
import { useState } from "react";
|
|
395
|
+
import { VelvetQR, useQRHistory, QRHistoryPanel, type VelvetQRProps } from "velvet-qr";
|
|
396
|
+
|
|
397
|
+
function HistoryDemo() {
|
|
398
|
+
const [config, setConfig] = useState<Partial<VelvetQRProps>>({});
|
|
399
|
+
const [value, setValue] = useState("https://poojagohel.github.io/velvet-qr/");
|
|
400
|
+
const history = useQRHistory({ maxEntries: 20 });
|
|
401
|
+
|
|
402
|
+
return (
|
|
403
|
+
<>
|
|
404
|
+
<VelvetQR value={value} {...config} />
|
|
405
|
+
<button onClick={() => history.add({ value, config })}>Save to history</button>
|
|
406
|
+
<QRHistoryPanel
|
|
407
|
+
history={history}
|
|
408
|
+
onSelect={(entry) => {
|
|
409
|
+
setValue(entry.value);
|
|
410
|
+
setConfig(entry.config);
|
|
411
|
+
}}
|
|
412
|
+
/>
|
|
413
|
+
</>
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
This is purely local bookkeeping of configs *you* explicitly `add()` β it has no relationship
|
|
419
|
+
to whether the resulting QR code has ever been scanned by anyone.
|
|
420
|
+
|
|
421
|
+
### Analytics β honestly scoped
|
|
422
|
+
|
|
423
|
+
> **This package is a client-side React component library with no backend and no hosting.**
|
|
424
|
+
> It is architecturally impossible for it to detect when someone scans a printed or shared QR
|
|
425
|
+
> code with their own phone camera β that scan happens entirely outside any page this
|
|
426
|
+
> library's JS ever runs in. `useQRAnalytics` only tracks events **observable within your own
|
|
427
|
+
> page session**: generation, exports (PNG/SVG/JPEG), clipboard copies, and in-session decodes
|
|
428
|
+
> if you also use this package's own `useQRScanner`/`QRScanner`. Real "was my printed QR code
|
|
429
|
+
> scanned" tracking requires server-side infrastructure outside this package's scope β e.g.
|
|
430
|
+
> routing the QR's destination through your own backend redirect endpoint and logging hits
|
|
431
|
+
> there (see [Dynamic QR](#dynamic-qr--honestly-scoped) below).
|
|
432
|
+
|
|
433
|
+
`useQRAnalytics` does not wrap or auto-instrument `VelvetQR` β you call `track()` explicitly,
|
|
434
|
+
wherever you already have the relevant information:
|
|
435
|
+
|
|
436
|
+
```tsx
|
|
437
|
+
import { useRef } from "react";
|
|
438
|
+
import { VelvetQR, useQRAnalytics, type VelvetQRHandle } from "velvet-qr";
|
|
439
|
+
|
|
440
|
+
function AnalyticsDemo() {
|
|
441
|
+
const qrRef = useRef<VelvetQRHandle>(null);
|
|
442
|
+
const { track, stats } = useQRAnalytics({
|
|
443
|
+
persist: true, // aggregate local counts into localStorage
|
|
444
|
+
onTrack: (event) => myAnalyticsBackend.send(event), // pipe to GA/PostHog/your own backend
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
return (
|
|
448
|
+
<>
|
|
449
|
+
<VelvetQR
|
|
450
|
+
ref={qrRef}
|
|
451
|
+
value="https://poojagohel.github.io/velvet-qr/"
|
|
452
|
+
onGenerated={() => track({ type: "generate", value: "https://poojagohel.github.io/velvet-qr/" })}
|
|
453
|
+
/>
|
|
454
|
+
<button
|
|
455
|
+
onClick={async () => {
|
|
456
|
+
await qrRef.current?.downloadPNG();
|
|
457
|
+
track({ type: "download", value: "https://poojagohel.github.io/velvet-qr/", format: "png" });
|
|
458
|
+
}}
|
|
459
|
+
>
|
|
460
|
+
Download PNG
|
|
461
|
+
</button>
|
|
462
|
+
<p>Generated locally {stats.byType.generate} times this session/device.</p>
|
|
463
|
+
</>
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
### Dynamic QR β honestly scoped
|
|
469
|
+
|
|
470
|
+
A "dynamic QR" (one whose destination can change after printing) needs the printed code to
|
|
471
|
+
always point at a **stable URL your own backend controls**, so the backend β not this
|
|
472
|
+
package β decides where to redirect at scan time. `buildDynamicQrValue` builds that stable
|
|
473
|
+
URL:
|
|
474
|
+
|
|
475
|
+
```tsx
|
|
476
|
+
import { VelvetQR, buildDynamicQrValue } from "velvet-qr";
|
|
477
|
+
|
|
478
|
+
<VelvetQR value={buildDynamicQrValue({ baseUrl: "https://yourapp.com/r", id: "table-12" })} />;
|
|
479
|
+
// -> encodes "https://yourapp.com/r/table-12" β your server's /r/table-12 route decides
|
|
480
|
+
// where to redirect, and can change that destination at any time without reprinting the code.
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
`useDynamicQr` is a small convenience wrapper around that, with a **localStorage-backed
|
|
484
|
+
resolver meant only for local prototyping/demos** of the "swap the destination" experience β
|
|
485
|
+
it does *not* make the physical/printed code dynamic for anyone else scanning it with a real
|
|
486
|
+
camera, since that requires a real server-side redirect endpoint:
|
|
487
|
+
|
|
488
|
+
```tsx
|
|
489
|
+
import { VelvetQR, useDynamicQr } from "velvet-qr";
|
|
490
|
+
|
|
491
|
+
function DynamicQrDemo() {
|
|
492
|
+
const { value, destination, isLoading, setDestination } = useDynamicQr({
|
|
493
|
+
id: "table-12",
|
|
494
|
+
baseUrl: "https://yourapp.com/r",
|
|
495
|
+
// resolver: (id) => fetch(`/api/dynamic-links/${id}`).then((r) => r.json()).then((d) => d.destination),
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
return (
|
|
499
|
+
<>
|
|
500
|
+
<VelvetQR value={value} />
|
|
501
|
+
<p>{isLoading ? "Loadingβ¦" : `Currently redirects to: ${destination ?? "(unset)"}`}</p>
|
|
502
|
+
<button onClick={() => setDestination("https://example.com/new-menu")}>
|
|
503
|
+
Update destination (localStorage demo only)
|
|
504
|
+
</button>
|
|
505
|
+
</>
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
Pass your own `resolver` (backed by a real API) for production use β `setDestination` is a
|
|
511
|
+
no-op (with a `console.warn`) when a custom `resolver` is supplied, since updating the real
|
|
512
|
+
destination is your backend's job, not this hook's.
|
|
513
|
+
|
|
514
|
+
## Props
|
|
515
|
+
|
|
516
|
+
### `VelvetQR`
|
|
517
|
+
|
|
518
|
+
| Prop | Type | Default | Description |
|
|
519
|
+
| --- | --- | --- | --- |
|
|
520
|
+
| `value` | `string` | β | **Required.** The content to encode. Use the builder helpers for structured data (WiFi, vCard, etc). |
|
|
521
|
+
| `size` | `number` | `240` | Rendered width/height in pixels. Scales down responsively (`max-width: 100%`). Clamped to `> 0`. |
|
|
522
|
+
| `margin` | `number` | `4` | Quiet-zone size, in QR modules. Clamped to `>= 0`. |
|
|
523
|
+
| `backgroundColor` | `string` | `"#ffffff"` | Background fill color. Ignored if `transparentBackground` is set. |
|
|
524
|
+
| `foregroundColor` | `string` | `"#000000"` | Data module color. Ignored if `gradient` is set. |
|
|
525
|
+
| `errorCorrectionLevel` | `"L" \| "M" \| "Q" \| "H"` | `"M"` | Higher levels tolerate more damage/logo overlap at the cost of density. Use `"H"` when embedding a logo. |
|
|
526
|
+
| `cornerRadius` | `number` | `0` | Corner radius (px) of the outer background rect. |
|
|
527
|
+
| `dotStyle` | `"square" \| "rounded" \| "dots" \| "classy" \| "classy-rounded" \| "extra-rounded"` | `"square"` | Shape of the data modules. |
|
|
528
|
+
| `eyeStyle` | `"square" \| "circle" \| "rounded" \| "leaf"` | `"square"` | Shape of the three finder-pattern "eyes". |
|
|
529
|
+
| `eyeColor` | `string` | `foregroundColor` | Override color for the eyes only. |
|
|
530
|
+
| `gradient` | `{ type: "linear" \| "radial"; colors: [string, string]; rotation?: number }` | β | Applies a gradient fill to data modules instead of `foregroundColor`. |
|
|
531
|
+
| `transparentBackground` | `boolean` | `false` | Omit the background rect entirely (transparent PNG/SVG exports). |
|
|
532
|
+
| `logoImage` | `string` | β | URL or data URL of a logo to embed at the center. |
|
|
533
|
+
| `logoSize` | `number` | `0.2` | Logo size as a fraction (0β1) of `size`. Clamped to `[0, 1]`. |
|
|
534
|
+
| `logoPadding` | `number` | `4` | Solid-color padding (px) around the logo. |
|
|
535
|
+
| `logoBorderRadius` | `number` | `8` | Corner radius (px) of the logo's padded backdrop. |
|
|
536
|
+
| `logoExcavate` | `boolean` | `true` | Skip rendering data modules directly under the logo so it stays legible. |
|
|
537
|
+
| `frame` | `{ style: "none" \| "banner-top" \| "banner-bottom" \| "border-label"; text?: string; backgroundColor?: string; textColor?: string; cornerRadius?: number }` | β (`"none"`) | Bakes a caption banner/border-label into the exported SVG/PNG/JPEG. See "Frame / label templates" above for the exact dimension contract. |
|
|
538
|
+
| `animation` | `{ type: "none" \| "fade-in" \| "draw-in" \| "pulse" \| "gradient-shift"; duration?: number; staggerDelay?: number }` | β (`"none"`) | Purely decorative, **on-screen-only** animation. See "Animation" above. **Exports (`downloadPNG`/`downloadSVG`/`downloadJPEG`/`getDataUrl`, and `useQRBatch`'s `downloadAll`) are always the fully-settled, static QR, regardless of this prop.** |
|
|
539
|
+
| `className` / `style` | β | β | Passed through to the root `<svg>`. |
|
|
540
|
+
| `ariaLabel` | `string` | `` `QR code for ${value}` `` | Accessible label on the root `<svg>` (`role="img"`). |
|
|
541
|
+
| `onGenerated` | `(svgMarkup: string) => void` | β | Called whenever the generated SVG markup changes. |
|
|
542
|
+
| `id` | `string` | auto-generated | Root element id; also used internally for gradient/clip-path def scoping. |
|
|
543
|
+
|
|
544
|
+
### `VelvetQRHandle` (ref)
|
|
545
|
+
|
|
546
|
+
| Method | Description |
|
|
547
|
+
| --- | --- |
|
|
548
|
+
| `downloadPNG(filename?)` | Rasterizes the current SVG and downloads it as a PNG. |
|
|
549
|
+
| `downloadSVG(filename?)` | Downloads the raw SVG markup. |
|
|
550
|
+
| `downloadJPEG(filename?, quality?)` | Rasterizes and downloads as JPEG (`quality` 0β1, default `0.92`). |
|
|
551
|
+
| `copyToClipboard()` | Copies the rendered PNG to the system clipboard via the Clipboard API. |
|
|
552
|
+
| `getDataUrl(format, quality?)` | Returns a data URL (`"png" \| "jpeg" \| "svg"`) without triggering a download. |
|
|
553
|
+
|
|
554
|
+
## Components
|
|
555
|
+
|
|
556
|
+
| Component | Purpose |
|
|
557
|
+
| --- | --- |
|
|
558
|
+
| `VelvetQR` | The core QR renderer. |
|
|
559
|
+
| `QRPreview` | Styled preview card wrapping `VelvetQR`. |
|
|
560
|
+
| `QRDownloader` | Ready-made download/copy toolbar bound to a `VelvetQR` ref. |
|
|
561
|
+
| `QRCustomizer` | Controlled customization panel (`value` + `onChange`) for building playgrounds/editors. |
|
|
562
|
+
| `QRLogoUploader` | Drag-and-drop / file-picker input that resolves an image file to a data URL. |
|
|
563
|
+
| `QRPresetPicker` | Row of live, clickable preset thumbnails calling `onSelect(config)`. |
|
|
564
|
+
| `QRBatchGenerator` | Grid of generated QR codes for a list of items, plus a "Download All" action. |
|
|
565
|
+
| `QRScanner` | Camera + static-image QR scanner UI built on `useQRScanner`, calling `onDetected(value)`. |
|
|
566
|
+
| `QRHistoryPanel` | Row of live thumbnails for `useQRHistory` entries, calling `onSelect(entry)`, with per-entry remove. |
|
|
567
|
+
|
|
568
|
+
## Hooks
|
|
569
|
+
|
|
570
|
+
| Hook | Purpose |
|
|
571
|
+
| --- | --- |
|
|
572
|
+
| `useQRGenerator(options)` | Core generation hook: matrix + SVG markup + `getDataUrl`, without any component/DOM ref. |
|
|
573
|
+
| `useQRDownload(qrRef)` | Wraps a `VelvetQR` ref with `downloadPNG`/`downloadSVG`/`downloadJPEG`/`copyToClipboard`. |
|
|
574
|
+
| `useQRBatch(items, sharedOptions?)` | Generates `{ id, value, svgMarkup, error }` for each item, plus `downloadAll(format)`. |
|
|
575
|
+
| `useQRScanner()` | Decodes QR codes from a camera stream (`startCamera`/`stopCamera`) or a static image (`scanImage`) via the native `BarcodeDetector` API. `isSupported` is `false` when unavailable. |
|
|
576
|
+
| `useQRHistory(options?)` | Local, `localStorage`-backed history of QR configs: `entries`, `add`, `remove`, `clear`, `restore`. |
|
|
577
|
+
| `useQRAnalytics(options?)` | Opt-in, explicitly-called event tracking (`track`) for generate/download/copy/scan-decode events observable in your own session β **not** real-world camera scans. See "Analytics β honestly scoped" above. |
|
|
578
|
+
| `useDynamicQr(options)` | Builds a stable dynamic-QR `value` plus a `destination`/`setDestination`/`refresh` API; default resolver is `localStorage`-backed for prototyping only. See "Dynamic QR β honestly scoped" above. |
|
|
579
|
+
|
|
580
|
+
## Presets
|
|
581
|
+
|
|
582
|
+
| Export | Purpose |
|
|
583
|
+
| --- | --- |
|
|
584
|
+
| `QR_PRESETS` | 8 curated `{ id, name, description, config }` presets β spread `.config` onto `<VelvetQR>`. |
|
|
585
|
+
| `QrPreset` | The preset type, for building your own preset list in the same shape. |
|
|
586
|
+
|
|
587
|
+
## FAQ
|
|
588
|
+
|
|
589
|
+
**SVG, PNG, or JPEG β which should I export?** SVG for print/scalable use (logos, packaging); PNG when you need a transparent background or a raster file for the web; JPEG only if file size matters more than a transparent background (JPEG has no alpha channel).
|
|
590
|
+
|
|
591
|
+
**Will a logo make my QR code unscannable?** Keep `logoSize` under ~0.25 and use `errorCorrectionLevel="H"` (or at least `"Q"`) when embedding a logo β the extra error-correction data is what keeps the code scannable despite the excavated center.
|
|
592
|
+
|
|
593
|
+
**Does this work with Next.js / SSR?** Yes β the package ships a `"use client"` banner, so import `VelvetQR` directly in a Client Component. See `examples/nextjs-app-router` in the repo for a full walkthrough of why your own page still needs its own `"use client"` directive too.
|
|
594
|
+
|
|
595
|
+
**How big is the bundle?** `velvet-qr` has two runtime dependencies: `qrcode` (used only to compute the encoding matrix β all rendering is a hand-written SVG engine) and `jsqr` (~15KB, used only as the scanner's cross-browser fallback decoder when the native `BarcodeDetector` API isn't available). The whole package tree-shakes per export.
|
|
596
|
+
|
|
597
|
+
**Can velvet-qr track when my QR code gets scanned?** No β not a real-world scan of a printed or shared code. `velvet-qr` is a client-side component library with no backend and no hosting, so it never runs any code on the device that scans your printed QR β there's nothing for it to observe. `useQRAnalytics` can track events that happen *inside your own page*, in your own visitor's session (QR generation, PNG/SVG/JPEG exports, clipboard copies, and in-session decodes if you also use `useQRScanner`), but that's a different thing from "someone scanned my flyer." Real scan tracking requires your own server: point the QR at a stable URL under your own redirect endpoint (`buildDynamicQrValue`/`useDynamicQr`) and log hits there.
|
|
598
|
+
|
|
599
|
+
For deeper coverage (batch generation, presets, logo scannability tuning, error-correction-level tradeoffs), see [`docs/FAQ.md`](../docs/FAQ.md) in the repo root, alongside [`docs/INSTALLATION.md`](../docs/INSTALLATION.md), [`docs/API-REFERENCE.md`](../docs/API-REFERENCE.md), and [`docs/MIGRATION.md`](../docs/MIGRATION.md).
|
|
600
|
+
|
|
601
|
+
## License
|
|
602
|
+
|
|
603
|
+
MIT Β© Pooja Gohel
|