@particle-academy/react-fancy 5.35.1 → 5.36.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 +51 -0
- package/dist/chunk-6KFCJLJN.cjs +630 -0
- package/dist/chunk-6KFCJLJN.cjs.map +1 -0
- package/dist/chunk-PK2FHI5R.js +626 -0
- package/dist/chunk-PK2FHI5R.js.map +1 -0
- package/dist/index.cjs +30 -17
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -3
- package/dist/inputs.cjs +5 -5
- package/dist/inputs.js +1 -1
- package/dist/qr-code.cjs +21 -0
- package/dist/qr-code.cjs.map +1 -0
- package/dist/qr-code.d.cts +201 -0
- package/dist/qr-code.d.ts +201 -0
- package/dist/qr-code.js +4 -0
- package/dist/qr-code.js.map +1 -0
- package/docs/QrCode.md +132 -0
- package/package.json +11 -1
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { SVGAttributes } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ISO/IEC 18004 table data for the QR encoder. **Data, not logic** — kept in
|
|
6
|
+
* its own module so the algorithm beside it can be read without scrolling past
|
|
7
|
+
* 160 rows, and so a correction here is a diff that cannot touch behaviour.
|
|
8
|
+
*
|
|
9
|
+
* There is no formula for these: the block structure per version and error
|
|
10
|
+
* correction level is a table in the standard. They were taken from the
|
|
11
|
+
* published error-correction table at
|
|
12
|
+
* <https://www.thonky.com/qr-code-tutorial/error-correction-table> rather than
|
|
13
|
+
* recalled, and every row is checked by `qr-code.test.tsx` against three
|
|
14
|
+
* equations that must all hold simultaneously:
|
|
15
|
+
*
|
|
16
|
+
* 1. `dataCodewords === g1Blocks * g1Size + g2Blocks * g2Size`
|
|
17
|
+
* 2. `totalCodewords === dataCodewords + ecPerBlock * (g1Blocks + g2Blocks)`
|
|
18
|
+
* 3. `totalCodewords === rawDataModules(version) / 8`, where
|
|
19
|
+
* `rawDataModules` is counted from the matrix THIS code builds.
|
|
20
|
+
*
|
|
21
|
+
* The third is the one worth having: it ties a transcribed table to an
|
|
22
|
+
* independently constructed matrix, so a mistyped row, a wrong alignment
|
|
23
|
+
* pattern position and a wrong function-pattern layout cannot all agree.
|
|
24
|
+
*/
|
|
25
|
+
/** Error correction level. Higher levels survive more damage and hold less. */
|
|
26
|
+
type QrErrorCorrection = 'L' | 'M' | 'Q' | 'H';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A QR encoder, dependency-free.
|
|
30
|
+
*
|
|
31
|
+
* ISO/IEC 18004, versions 1–40, error correction L/M/Q/H, with numeric,
|
|
32
|
+
* alphanumeric and byte modes and automatic selection between them. No
|
|
33
|
+
* Kanji mode and no structured append — neither has a caller here, and an
|
|
34
|
+
* untested branch in an encoder is worse than an absent one because the
|
|
35
|
+
* failure is a code a phone silently refuses to read.
|
|
36
|
+
*
|
|
37
|
+
* Table data and its provenance live in `./qr-tables`.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/** Which encoding mode a value is carried in. */
|
|
41
|
+
type QrMode = 'numeric' | 'alphanumeric' | 'byte';
|
|
42
|
+
interface QrSymbol {
|
|
43
|
+
/** `size * size` modules, row-major. `true` is dark. */
|
|
44
|
+
modules: boolean[][];
|
|
45
|
+
/** Modules per side, `version * 4 + 17`. */
|
|
46
|
+
size: number;
|
|
47
|
+
version: number;
|
|
48
|
+
errorCorrection: QrErrorCorrection;
|
|
49
|
+
mode: QrMode;
|
|
50
|
+
/** The mask pattern chosen by penalty scoring, 0–7. */
|
|
51
|
+
mask: number;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The narrowest mode that can carry `value`.
|
|
55
|
+
*
|
|
56
|
+
* Narrower is denser: numeric packs 3 digits into 10 bits where byte mode
|
|
57
|
+
* would spend 24. A six-digit pairing PIN is a third of the size as numeric,
|
|
58
|
+
* which is the difference between a code a phone reads across a desk and one
|
|
59
|
+
* it does not.
|
|
60
|
+
*/
|
|
61
|
+
declare function selectMode(value: string): QrMode;
|
|
62
|
+
interface EncodeOptions {
|
|
63
|
+
errorCorrection?: QrErrorCorrection;
|
|
64
|
+
/** Force a mode instead of selecting the narrowest that fits. */
|
|
65
|
+
mode?: QrMode;
|
|
66
|
+
/** Force a version. Throws if the value does not fit. */
|
|
67
|
+
version?: number;
|
|
68
|
+
/** Force a mask instead of scoring all eight. For tests and golden vectors. */
|
|
69
|
+
mask?: number;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Encode a string into a QR symbol.
|
|
73
|
+
*
|
|
74
|
+
* @throws RangeError when the value cannot fit, or when a forced version is
|
|
75
|
+
* too small for it. **Throwing is deliberate**: the alternative is returning
|
|
76
|
+
* a truncated symbol, which scans cleanly and yields the wrong string — a
|
|
77
|
+
* silent corruption on the one surface whose entire job is to carry a value
|
|
78
|
+
* accurately.
|
|
79
|
+
*/
|
|
80
|
+
declare function encodeQr(value: string, options?: EncodeOptions): QrSymbol;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* `viewBox` is omitted because the component computes it from the symbol's
|
|
84
|
+
* module count — overriding it would rescale the modules and break the
|
|
85
|
+
* crispness the vector output exists for.
|
|
86
|
+
*
|
|
87
|
+
* `version` is omitted because SVG has an attribute of that name and ours
|
|
88
|
+
* means the QR symbol version, a number. SVG's is a no-op in SVG 2 and was
|
|
89
|
+
* only ever a document-conformance hint, so nothing is lost by shadowing it,
|
|
90
|
+
* where renaming our prop would cost the one word every QR reference uses.
|
|
91
|
+
*/
|
|
92
|
+
interface QrCodeProps extends Omit<SVGAttributes<SVGSVGElement>, "children" | "viewBox" | "version"> {
|
|
93
|
+
/**
|
|
94
|
+
* What the code carries. Any string — a URL, a PIN, a token.
|
|
95
|
+
*
|
|
96
|
+
* An empty string renders a valid (empty) symbol rather than throwing, so a
|
|
97
|
+
* controlled surface can render before its value arrives.
|
|
98
|
+
*/
|
|
99
|
+
value: string;
|
|
100
|
+
/** Rendered width and height in pixels. Default 192. */
|
|
101
|
+
size?: number;
|
|
102
|
+
/**
|
|
103
|
+
* Error correction level. Default `"M"` — about 15% of the symbol can be
|
|
104
|
+
* damaged and still read, which is the usual choice for a screen.
|
|
105
|
+
*
|
|
106
|
+
* Higher is not better by default: `"H"` survives ~30% damage and holds a
|
|
107
|
+
* third less, so the same value needs a bigger symbol with smaller modules,
|
|
108
|
+
* and on a phone screen that can read WORSE than `"M"` at the same pixel
|
|
109
|
+
* size. Raise it for print, for a label that will be scuffed, or when a logo
|
|
110
|
+
* covers part of the symbol.
|
|
111
|
+
*/
|
|
112
|
+
errorCorrection?: QrErrorCorrection;
|
|
113
|
+
/**
|
|
114
|
+
* Accessible name. **Required**, and it must describe the DESTINATION rather
|
|
115
|
+
* than the artefact: "Pair this device" tells a screen-reader user what the
|
|
116
|
+
* code is for, where "QR code" tells them only that an image they cannot use
|
|
117
|
+
* is present.
|
|
118
|
+
*
|
|
119
|
+
* It is not a substitute for the value being reachable another way. A QR code
|
|
120
|
+
* is unusable by anyone who cannot point a second device at this screen, so
|
|
121
|
+
* the value should also appear as text or a link nearby — the `aria-label`
|
|
122
|
+
* cannot carry a URL a user is expected to type.
|
|
123
|
+
*/
|
|
124
|
+
label: string;
|
|
125
|
+
/**
|
|
126
|
+
* Modules of quiet zone around the symbol. Default 4, which is the
|
|
127
|
+
* standard's minimum.
|
|
128
|
+
*
|
|
129
|
+
* **Do not set this to 0 to save space.** The quiet zone is not padding; it
|
|
130
|
+
* is how a scanner finds the symbol's edge. A code flush against
|
|
131
|
+
* surrounding content is the single most common reason a QR code that looks
|
|
132
|
+
* perfect will not scan.
|
|
133
|
+
*/
|
|
134
|
+
quietZone?: number;
|
|
135
|
+
/** Module colour. Default `currentColor`, so it inherits text colour. */
|
|
136
|
+
foreground?: string;
|
|
137
|
+
/**
|
|
138
|
+
* Background colour. Default `"transparent"`.
|
|
139
|
+
*
|
|
140
|
+
* **A transparent background inherits whatever is behind it**, so on a dark
|
|
141
|
+
* surface the symbol renders dark-on-dark and most scanners will not read
|
|
142
|
+
* it. Set an explicit light background, or set `foreground` and background
|
|
143
|
+
* together, when the surface is not light. Contrast is what gets scanned,
|
|
144
|
+
* not the dark modules on their own.
|
|
145
|
+
*/
|
|
146
|
+
background?: string;
|
|
147
|
+
/**
|
|
148
|
+
* Force a mode instead of picking the narrowest that fits. Rarely useful;
|
|
149
|
+
* the default packs digits three to ten bits where byte mode spends 24.
|
|
150
|
+
*/
|
|
151
|
+
mode?: QrMode;
|
|
152
|
+
/**
|
|
153
|
+
* Force a symbol version (1–40), fixing the module count regardless of how
|
|
154
|
+
* little the value needs. Useful when several codes must render at the same
|
|
155
|
+
* module scale. Throws if the value does not fit.
|
|
156
|
+
*/
|
|
157
|
+
version?: number;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* A QR code, as an SVG, with no dependencies.
|
|
162
|
+
*
|
|
163
|
+
* ## Why SVG and one path
|
|
164
|
+
*
|
|
165
|
+
* The symbol is drawn as a **single `<path>`** of one-unit squares on a
|
|
166
|
+
* `viewBox` sized in modules, not as a grid of `<rect>` elements. A version 10
|
|
167
|
+
* symbol is 57x57 — over three thousand modules — and a rect each would be
|
|
168
|
+
* three thousand DOM nodes for something that never changes. One path is one
|
|
169
|
+
* node, and because the viewBox is in module units the symbol is resolution
|
|
170
|
+
* independent: `size` scales it with no resampling, so it stays crisp at any
|
|
171
|
+
* size and prints at the printer's resolution rather than the screen's.
|
|
172
|
+
*
|
|
173
|
+
* This matters more than it sounds for scanning. A raster QR code scaled to a
|
|
174
|
+
* non-integer multiple of its module size gets blurred module edges, and a
|
|
175
|
+
* scanner reading a blurred edge guesses. Vector output cannot have that
|
|
176
|
+
* problem.
|
|
177
|
+
*
|
|
178
|
+
* ## It is an image of a value, not the value
|
|
179
|
+
*
|
|
180
|
+
* A QR code is unreadable to anyone without a second device pointed at the
|
|
181
|
+
* screen — which includes every screen-reader user and anyone whose phone is
|
|
182
|
+
* the device showing it. So `label` is required, and the value should ALSO be
|
|
183
|
+
* reachable as text or a link beside it. A pairing panel that shows only a
|
|
184
|
+
* code is a panel some people cannot use at all.
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* ```tsx
|
|
188
|
+
* <QrCode
|
|
189
|
+
* value="https://alphar.tail1234.ts.net:7421/pair#482913"
|
|
190
|
+
* size={192}
|
|
191
|
+
* label="Pair this device"
|
|
192
|
+
* />
|
|
193
|
+
*
|
|
194
|
+
* // On a dark surface, set both colours: a transparent background renders
|
|
195
|
+
* // dark-on-dark and will not scan.
|
|
196
|
+
* <QrCode value={url} label="Pair" foreground="#0b0b0c" background="#ffffff" />
|
|
197
|
+
* ```
|
|
198
|
+
*/
|
|
199
|
+
declare const QrCode: react.ForwardRefExoticComponent<QrCodeProps & react.RefAttributes<SVGSVGElement>>;
|
|
200
|
+
|
|
201
|
+
export { type EncodeOptions, QrCode, type QrCodeProps, type QrErrorCorrection, type QrMode, type QrSymbol, encodeQr, selectMode };
|
package/dist/qr-code.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"qr-code.js"}
|
package/docs/QrCode.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# QrCode
|
|
2
|
+
|
|
3
|
+
A QR code rendered as an SVG, with **no dependencies** — the encoder is part of
|
|
4
|
+
the component.
|
|
5
|
+
|
|
6
|
+
ISO/IEC 18004, versions 1–40, error correction L/M/Q/H, with numeric,
|
|
7
|
+
alphanumeric and byte modes selected automatically.
|
|
8
|
+
|
|
9
|
+
## Import
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
import { QrCode } from "@particle-academy/react-fancy";
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Basic Usage
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
<QrCode
|
|
19
|
+
value="https://alphar.tail1234.ts.net:7421/pair#482913"
|
|
20
|
+
size={192}
|
|
21
|
+
label="Pair this device"
|
|
22
|
+
/>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Props
|
|
26
|
+
|
|
27
|
+
| Prop | Type | Default | Description |
|
|
28
|
+
|------|------|---------|-------------|
|
|
29
|
+
| value | `string` | — | What the code carries. An empty string renders a valid empty symbol rather than throwing. |
|
|
30
|
+
| label | `string` | — | **Required.** Accessible name. Describe the destination ("Pair this device"), not the artefact ("QR code"). |
|
|
31
|
+
| size | `number` | `192` | Rendered width and height in pixels. |
|
|
32
|
+
| errorCorrection | `"L" \| "M" \| "Q" \| "H"` | `"M"` | Damage tolerance. See the note below — higher is not automatically better. |
|
|
33
|
+
| quietZone | `number` | `4` | Modules of clear margin. The standard's minimum is 4. |
|
|
34
|
+
| foreground | `string` | `"currentColor"` | Module colour. |
|
|
35
|
+
| background | `string` | `"transparent"` | Background colour. See the note below. |
|
|
36
|
+
| mode | `"numeric" \| "alphanumeric" \| "byte"` | *auto* | Force an encoding mode. Rarely useful. |
|
|
37
|
+
| version | `number` | *auto* | Force a symbol version 1–40, fixing the module count. Throws if the value does not fit. |
|
|
38
|
+
|
|
39
|
+
Every other SVG attribute passes through. `viewBox` and SVG's own `version` are
|
|
40
|
+
not accepted — the component owns the first, and the second collides with the QR
|
|
41
|
+
symbol version.
|
|
42
|
+
|
|
43
|
+
## Three ways a perfect-looking code fails to scan
|
|
44
|
+
|
|
45
|
+
None of these are visible in a screenshot review, which is why they are here
|
|
46
|
+
rather than in a footnote.
|
|
47
|
+
|
|
48
|
+
**1. No quiet zone.** It is not padding — it is how a scanner finds the
|
|
49
|
+
symbol's edge. A code flush against surrounding content is the single most
|
|
50
|
+
common cause of "the QR code doesn't work". `quietZone={0}` is allowed, because
|
|
51
|
+
a consumer compositing their own margin has a legitimate reason, but it is not
|
|
52
|
+
a space saving.
|
|
53
|
+
|
|
54
|
+
**2. A transparent background on a dark surface.** The default inherits
|
|
55
|
+
whatever is behind it, so on a dark panel the symbol renders dark-on-dark and
|
|
56
|
+
most scanners refuse it. Contrast is what gets scanned, not the dark modules on
|
|
57
|
+
their own:
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
<QrCode value={url} label="Pair" foreground="#0b0b0c" background="#ffffff" />
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**3. Reaching for `"H"` because it sounds safer.** `"H"` survives about 30%
|
|
64
|
+
damage against `"M"`'s 15%, but it holds a third less — so the same value needs
|
|
65
|
+
a larger symbol, and at the same pixel width that means *smaller modules*. On a
|
|
66
|
+
phone screen `"H"` can read worse than `"M"`. Raise it for print, for a label
|
|
67
|
+
that will be scuffed, or when a logo covers part of the symbol.
|
|
68
|
+
|
|
69
|
+
## It is an image of a value, not the value
|
|
70
|
+
|
|
71
|
+
A QR code is unusable to anyone who cannot point a second device at the screen —
|
|
72
|
+
every screen-reader user, and anyone whose phone is the device displaying it. So
|
|
73
|
+
`label` is required, **and the value should also be reachable as text or a
|
|
74
|
+
link** beside it. An `aria-label` cannot carry a URL someone is expected to
|
|
75
|
+
type.
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
<div className="flex flex-col items-center gap-3">
|
|
79
|
+
<QrCode value={pairUrl} label="Pair this device" background="#ffffff" />
|
|
80
|
+
<code className="text-sm">{pairUrl}</code>
|
|
81
|
+
</div>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## A PIN is denser than a URL
|
|
85
|
+
|
|
86
|
+
Mode selection is automatic and it matters. Numeric mode packs three digits into
|
|
87
|
+
ten bits where byte mode spends twenty-four, so a six-digit PIN is a version 1
|
|
88
|
+
symbol — the largest modules and the easiest read:
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
<QrCode value="482913" label="Pairing PIN" /> {/* 21 modules + quiet zone */}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Why SVG, and one path
|
|
95
|
+
|
|
96
|
+
The symbol is one `<path>` of unit squares on a module-unit `viewBox`, not a
|
|
97
|
+
`<rect>` per module. A version 10 symbol has over three thousand modules, so a
|
|
98
|
+
rect each would be three thousand DOM nodes for something that never changes
|
|
99
|
+
after render. Horizontal runs are packed into single rectangles.
|
|
100
|
+
|
|
101
|
+
The module-unit viewBox is what makes `size` free: the symbol is resolution
|
|
102
|
+
independent, so it stays crisp at any size and prints at the printer's
|
|
103
|
+
resolution. That is not only cosmetic — a raster code scaled to a non-integer
|
|
104
|
+
multiple of its module size has blurred module edges, and a scanner reading a
|
|
105
|
+
blurred edge guesses.
|
|
106
|
+
|
|
107
|
+
## The encoder on its own
|
|
108
|
+
|
|
109
|
+
Exported alongside the component, for a consumer that needs modules rather than
|
|
110
|
+
an SVG — a canvas, a PDF, a thermal printer, a terminal:
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
import { encodeQr, selectMode } from "@particle-academy/react-fancy";
|
|
114
|
+
|
|
115
|
+
const symbol = encodeQr("https://example.com", { errorCorrection: "Q" });
|
|
116
|
+
symbol.modules; // boolean[][], true is dark
|
|
117
|
+
symbol.size; // version * 4 + 17
|
|
118
|
+
symbol.version; // 1-40
|
|
119
|
+
symbol.mask; // the mask chosen by penalty scoring
|
|
120
|
+
selectMode("482913"); // "numeric"
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`encodeQr` throws a `RangeError` rather than truncating a value that does not
|
|
124
|
+
fit. That is deliberate: a truncated symbol scans cleanly and yields the wrong
|
|
125
|
+
string, which is silent corruption on a surface whose entire job is to carry a
|
|
126
|
+
value accurately.
|
|
127
|
+
|
|
128
|
+
## Not supported
|
|
129
|
+
|
|
130
|
+
Kanji mode and structured append. Neither has a caller here, and an untested
|
|
131
|
+
branch in an encoder is worse than an absent one — the failure is a code a
|
|
132
|
+
phone quietly refuses to read.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@particle-academy/react-fancy",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.36.0",
|
|
4
4
|
"description": "React UI component library for Human+ UX — controlled, agent-bridgeable primitives",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -713,6 +713,16 @@
|
|
|
713
713
|
"default": "./dist/pull-quote.cjs"
|
|
714
714
|
}
|
|
715
715
|
},
|
|
716
|
+
"./qr-code": {
|
|
717
|
+
"import": {
|
|
718
|
+
"types": "./dist/qr-code.d.ts",
|
|
719
|
+
"default": "./dist/qr-code.js"
|
|
720
|
+
},
|
|
721
|
+
"require": {
|
|
722
|
+
"types": "./dist/qr-code.d.cts",
|
|
723
|
+
"default": "./dist/qr-code.cjs"
|
|
724
|
+
}
|
|
725
|
+
},
|
|
716
726
|
"./radio-group": {
|
|
717
727
|
"import": {
|
|
718
728
|
"types": "./dist/radio-group.d.ts",
|