|
|
2 months ago | |
|---|---|---|
| .. | ||
| README.md | 2 months ago | |
| context.ts | 4 months ago | |
| index.ts | 4 months ago | |
| qr-code-download-trigger.svelte | 4 months ago | |
| qr-code.css | 2 months ago | |
| qr-code.svelte | 4 months ago | |
| types.ts | 4 months ago | |
README.md
QrCode
Themeable, accessible QR code — own zero-dependency encoder, framework 4-layer rigor, richer than the references it learns from.
Design proposal (approved 2026-06-12). This README is both the design record and the component doc.
Baseline
No Air baseline — new component (2026-06-12). Nothing in the legacy layers
rendered QR codes; the encoder itself is also new ($libs/qr, zero-dep).
Talla y tema
10 clave(s) pública(s) en lib/recipes/base.ts (bloque qr-code), con sus pasos por talla y el nombre RESUELTO que consume la receta.
Es el contrato vivo: la pestaña Tokens de su demo lista estas mismas
claves y las resuelve sobre el escenario.
Token (--qr-code-…) |
Valor por defecto |
|---|---|
fg |
#18181b |
bg |
#ffffff |
size-xs |
96px |
size-sm |
128px |
size-md |
160px |
size-lg |
200px |
size-xl |
256px |
radius |
var(--radius-md) |
overlay-bg |
#ffffff |
overlay-radius |
var(--radius-sm) |
Lo que el guard R-5.4 da por silencioso, con su razón medida
(scripts/theming-sentinel-exceptions.ts):
size-xs— one step per instance: the wrapper writes var(--qr-code-size-{k}) inline from the size prop and the demo runs lg. Forced -> reaches (96px -> 123px)size-sm— same, one step per instancesize-md— same, one step per instancesize-xl— same, one step per instancefg— painted as the SVG fill of the module path, not as color on the root; read on the path -> reaches (rgb(24,24,27) -> rgb(1,2,3))
Comparativa — why a new one (vs the references)
| Capability | ark-ui (zag + uqr) |
qr-code-styling | react-qr-code | QrCode (ours) |
|---|---|---|---|---|
| Parts / anatomy | ✅ Root·Frame·Pattern·Overlay·Download | ❌ imperative | ❌ single comp | ✅ |
| Encoder | uqr dep |
qr-code-generator dep | qr.js dep | own, zero-dep ($libs/qr) |
| Error correction L/M/Q/H | ✅ | ✅ | ✅ | ✅ |
| Themeable colour (tokens) | ❌ currentColor only |
✅ free | fg/bg props | ✅ via tokens + color |
| Cell shapes (square/rounded/dots) | ❌ | ✅ | ❌ | ✅ |
| Logo overlay | ✅ | ✅ | partial | ✅ + auto quiet-zone + auto-boost ECC |
| Download PNG/SVG | ✅ | ✅ | manual | ✅ DownloadTrigger = <Button> |
role=img + label a11y |
partial | ❌ | ❌ | ✅ |
| Framework morfo/recipe rigor | n/a | n/a | n/a | ✅ |
Takeaway: ark has the best structure but is visually flat and pulls an encoder dependency; qr-code-styling is visually rich but imperative, dependency- heavy, no a11y. Ours keeps ark's clean anatomy + API, owns the encoder (the QR algorithm is the open ISO/IEC 18004 standard — no npm dep), and adds themeable colour, cell shapes, a scannable logo overlay and real a11y.
Encoder — $libs/qr (own, zero-dep)
encode(value, { errorCorrection, minVersion?, maxVersion? }) → { matrix: boolean[][], reserved: boolean[][], size, version, errorCorrection }. A
from-scratch QR encoder (segment analysis → Reed-Solomon ECC over GF(256) →
matrix layout → mask selection over all 8 masks by penalty), pure and
zero-dependency, living in $libs alongside the other pure helpers. The
algorithm follows the public QR standard; uqr/nayuki are correctness
references only, never imported. Unit-tested against known vectors + structural
invariants; the demo harness round-trips encode→decode through jsQR
(versions 1–24, all ECC, all modes, all four cell shapes).
reserved[y][x] marks the function modules (finders, timing, alignment,
format/version info) — the eidos layer keeps those square even when
cellShape is rounded/dots, so the scanner's grid-lock pattern stays
intact and every shape remains scannable. Only the data modules take the
decorative shape.
Scannability (verified)
- Function patterns stay square for all cell shapes (
reservedmask) — whatqr-code-stylingdoes, and the reason rounded/dots still decode. - Logo auto-boosts ECC to
Hand clears a central ~24% region so the lost modules fall within the error-correction budget. - Fixed-tone default colours (dark-on-light) — a QR can't invert in dark
mode and stay scannable, so the default fg/bg are theme-independent; theming
is opt-in via
color/background(you own the contrast). - Quiet zone is in modules, not pixels —
marginmeasures the quiet zone in QR modules (spec minimum 4 — ISO/IEC 18004), the canonical unit: it scales with the symbol so the clear ring stays proportional at any rendered size, which is what guarantees scannability. There is deliberately no pixel margin prop. For fixed pixel spacing in a layout, pad the container in CSS using the QR'sbackgroundcolour — the symbol's quiet zone stays canonical in modules. - Export bakes resolved colours — a serialized SVG loses the document's CSS
custom properties, so
DownloadTriggerresolvesgetComputedStylefills into the clone before writing PNG/SVG. - Logo in exports — SVG export is lossless (the logo is XML and travels with
it). PNG export omits the logo: browsers do not rasterize
<foreignObject>content when an SVG is drawn to a canvas via<img>(a Chromium limitation, not a taint — the QR itself rasterizes fine). The cleared centre region stays, but the overlay is dropped. Use SVG to keep the logo. (A future PNG path could composite a raster logo directly onto the canvas; deferred — thelogoslot is an arbitrary snippet, not a URL.)
Anatomy (parts)
<QrCode value="https://…"> ← Frame (svg, role="img", aria-label)
└ Pattern ← the <path> matrix (decorative)
└ Overlay ← centred logo slot (optional)
<QrCode.DownloadTrigger> ← <Button> → PNG / SVG (optional)
morfo scope: ['eidos'] (+ optional sema for a commit-download tap cue).
No soma — a QR is a pure display (like Avatar / Image): value → matrix in
a $derived, rendered to SVG. Download is imperative via $adom.
API (root)
| Prop | Type | Default | |
|---|---|---|---|
value |
string |
'' |
data to encode (bindable) |
size |
ResponsiveProp<number | Size> |
'md' |
rendered box size |
errorCorrection |
'L' | 'M' | 'Q' | 'H' |
'M' |
redundancy (auto-boosted to H when a logo is present) |
color |
AffirmativeColorRole | string |
currentColor/token |
module colour |
background |
string |
token / transparent | quiet-zone + module background |
cellShape |
'square' | 'rounded' | 'dots' |
'square' |
module shape |
margin |
number |
4 |
quiet zone in modules, not pixels (spec min 4 — see Scannability) |
logo |
Snippet |
— | centred overlay; clears the centre + boosts ECC to H |
alt |
string |
value |
accessible name for the Frame |
DownloadTrigger props: format: 'png' \| 'svg' (default 'png'), filename
(default 'qrcode') + all <Button> props (variant, size, color, …).
It renders the framework <Button> and calls the root's colour-baking export
via context.
Compound: <QrCode> + .DownloadTrigger. The logo is the logo snippet prop
(not a subcomponent); the morfo's Overlay/Pattern parts are render internals.
Decisiones
Each argued in place above; the compact register:
- Own zero-dep encoder (
$libs/qr) — the QR algorithm is the open ISO/IEC 18004 standard;uqr/nayuki are correctness references only. - Function modules stay square whatever
cellShape(thereservedmask) — scannability over decoration (see Scannability). - Fixed-tone default colours — a QR can't invert in dark mode and stay
scannable; theming is opt-in via
color/background. marginin modules, not pixels (spec min 4) — the quiet zone scales with the symbol; pixel spacing belongs to the container's CSS.- Exports bake resolved colours (
getComputedStyleinto the clone) — a serialized SVG loses the document's custom properties. - No soma — pure display (like Avatar/Image):
value→ matrix in a$derived; download is imperative via$adom.
v1 scope (approved)
square/rounded/dots cell shapes · themeable colour · logo Overlay with auto
quiet-zone + auto-ECC-boost · download PNG + SVG · role=img + label.
Gaps
- Gradient fills + per-corner (finder) styling — the marketing-grade flourishes of qr-code-styling — disposition: diferir (v2).
- PNG export drops the logo (
<foreignObject>isn't rasterized when SVG draws to canvas; see Scannability) — disposition: diferir (a raster-logo composite path); SVG export is lossless. - Demo page pending the v2 template migration — disposition: implementar (demo/docs web phase).
Passive justification
Zero events by design: a QR is pure display — value in, SVG out; no
interaction surface of its own. The one interactive part, DownloadTrigger,
IS a composed <Button> (its contact firma and states live there). The
morfo may later add an optional commit-download cue (scope would gain
sema), which is the recorded exception path.
Layers
$libs/qr— own encoder + tests.- morfo
qr-code.ts— Frame / Pattern / Overlay / DownloadTrigger contract. - eidos
qr-code/— wrapper + recipe (--qr-code-*tokens) + SVG render from the matrix. - demo
web/routes/uix/components/qr-code/+ nav entry.
Audit exceptions
R-1.5 exception:the only interactive part (DownloadTrigger) composes the canonical<Button>— the focus ring lives inbutton.css;PatternandOverlayare display indicators.