feat(eidos): `Barcode` — el código de barras 1D, con encoder propio y cero dependencias
Ocho simbologías desde su estándar ISO publicado (Code 128 con auto-switching
A/B/C, EAN-13/8, UPC-A/E, Code 39, ITF/ITF-14) en `$libs/barcode`: tablas
transcritas de la especificación o derivadas de su regla de construcción, sin
una sola dependencia npm añadida. JsBarcode y bwip-js son referencia de
corrección, jamás import — la misma doctrina con la que `$libs/qr` usó a Nayuki.
Ningún headless del mercado trae barcode: el terreno lo ocupan encoders
imperativos sin anatomía ni a11y.
- **La física manda sobre la ergonomía visual.** La dimensión X (`moduleWidth`)
es el ancla de escaneabilidad y la altura de barra un eje libre — un escáner
lee una franja horizontal. Por eso la geometría es px absolutos en el viewBox
(precedente: la familia `chart`) y no el token `size` cuadrado del QR:
colapsar los dos ejes en uno haría que la X dependiera de la altura, que es
justo lo que no puede flotar. La zona muda va en módulos, con el mínimo de
cada estándar.
- **Lo que hace reconocible a la familia.** Las guardas EAN/UPC bajan hasta la
línea base y los dígitos se imprimen dígito-bajo-dígito sobre su celda de 7
módulos, en mono (la HRI del estándar es OCR-B, de ancho fijo). ITF-14 lleva
su barra portadora por fuera de la zona muda, que no puede comerse.
- **Un valor no codificable es un ESTADO, no una excepción.** `encode` lanza un
`BarcodeError` tipado; el wrapper lo captura, marca `data-invalid`, avisa por
`onInvalid` (deduplicado por razón: `encoded` es un objeto nuevo por
pulsación) y deja un marco placeholder decorativo. Un EAN a medio teclear es
un paso normal de edición; dejarlo reventar dentro de un `$derived` tumbaría
la página.
- **`'text'` entra en `MorfoElement`** — y también en el schema de sium, que
tiene su propia lista literal. Añadir solo la unión de TypeScript compila y
falla en runtime; lo cazó `morfo:check`, no `npm run check`.
Verificación: 48 tests de encoder en tres capas (invariantes estructurales de
cada tabla · vectores publicados transcritos aparte · decoders independientes
que releen la retícula), la lectura real confirmada con escáner por el usuario,
y la retícula decodificada desde el SVG ya pintado en navegador. `component:audit`
PASS con 0 errores; los 2 warnings (D-1.5 / D-4.3) son los mismos que da
`button`, la demo canaria: sus regex buscan la forma v1 inline del
MutationObserver y del `emit`, que la v2 movió al harness compartido.
Demo v2 canónica de 9 pestañas. El alcance diferido (Codabar/MSI/Pharmacode,
GS1-128, add-ons EAN-2/5, y las 2D como componentes aparte) queda registrado en
`next-features.md` §10 — el hueco de numeración se cierra cuando el track de
blocks commitee sus §8/§9, hoy sin commitear.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
# Barcode
Themeable, accessible **1D barcode** — own zero-dependency encoder, framework
4-layer rigor, the anatomy no reference library ships.
> **Design proposal approved 2026-07-28, shipped 2026-07-29.** This README is
> both the design record and the component doc. It is the sibling of
> [`qr-code/README.md`](../qr-code/README.md): same architecture, same doctrine,
> different physics.
## Baseline
No Air baseline — new component (2026-07-28). Nothing in the legacy layers
rendered linear barcodes; the encoder is also new (`$libs/barcode`, zero-dep).
The **internal** baseline is `QrCode` (2026-06-12): eidos-native display
primitive, `scope: ['eidos']` , own encoder in `$libs` , `DownloadTrigger` that
composes `<Button>` . Every divergence from it is argued in `## Decisiones` .
## Comparativa — why a new one (vs the references)
No headless library ships a barcode: ark-ui, bits-ui, Radix and React Aria have
a QR code (or nothing) and stop there. The field belongs to imperative encoders.
| Capability | JsBarcode | bwip-js | react-barcode | ark-ui / bits-ui / Radix | **Barcode (ours)** |
| -------------------------------------- | ------------------------ | ----------------- | --------------- | ------------------------ | ---------------------------------------- |
| Parts / anatomy | ❌ imperative | ❌ imperative | ❌ single comp | ❌ **no barcode at all** | ✅ Provider·Pattern·Text·DownloadTrigger |
| Encoder | own dep (~40 kB) | own dep (~500 kB) | wraps JsBarcode | — | **own, zero-dep (`$libs/barcode`)** |
| Retail symbologies (EAN/UPC) | ✅ | ✅ | ✅ | — | ✅ EAN-13/8 · UPC-A/E |
| Logistics (Code128 A/B/C auto, ITF-14) | ✅ | ✅ | ✅ | — | ✅ + bearer bars |
| Check digit computed | ✅ | ✅ | ✅ | — | ✅ (returned, not hidden) |
| Quiet zone per spec | ⚠️ single `margin` in px | ✅ | ⚠️ px | — | ✅ **in modules, per symbology** |
| Guard bars extend under HRI | ✅ EAN only | ✅ | ✅ | — | ✅ (the `guards` mask) |
| Themeable colour (tokens) | ⚠️ free strings | ⚠️ free strings | ⚠️ props | — | ✅ **via tokens + `color`** |
| Invalid input is a state, not a throw | ❌ throws / logs | ❌ throws | ❌ throws | — | ✅ `data-invalid` + `onInvalid` |
| `role=img` + label a11y | ❌ | ❌ | ⚠️ partial | — | ✅ |
| Download PNG/SVG | ❌ manual | ❌ manual | ❌ manual | — | ✅ **DownloadTrigger = `<Button>`** |
| Framework morfo/recipe rigor | n/a | n/a | n/a | n/a | ✅ |
**Takeaway:** JsBarcode has the right symbology coverage and the wrong shape
(imperative, mutates a DOM node you hand it, no a11y, no anatomy); bwip-js is
encyclopaedic and enormous; react-barcode is a thin wrapper that inherits both
problems. Ours keeps QrCode's clean anatomy + API, owns the encoder (every
symbology here is a published ISO standard — no npm dep), and adds themeable
colour, real a11y, a first-class invalid state and PNG/SVG export.
## Encoder — `$libs/barcode` (own, zero-dep)
```ts
encode(value, { symbology?, quietZone? }) → BarcodeResult
```
A from-scratch linear-barcode encoder living in `$libs` alongside the other pure
helpers. Each symbology is a **published open standard** — ISO/IEC 15417
(Code 128), ISO/IEC 15420 (EAN/UPC), ISO/IEC 16388 (Code 39), ISO/IEC 16390
(ITF) — so the tables are transcribed from the specification and the layout is
written from scratch. JsBarcode / bwip-js are **correctness references only,
never imported** — the same doctrine `$libs/qr` used with Nayuki's QR reference.
**Zero dependencies added to `package.json` **, runtime or dev.
```ts
export type Symbology = 'code128' | 'ean13' | 'ean8' | 'upca' | 'upce' | 'code39' | 'itf' | 'itf14';
export interface BarcodeResult {
/** Module lattice, left→right. `true` = dark (bar). Excludes the quiet zone. */
modules: boolean[];
/** Symbol width in modules (= `modules.length` ). */
size: number;
/** Quiet zone in modules per side — the symbology's spec minimum. */
quietZone: { start: number; end: number };
/**
* Module ranges whose bars extend below the baseline into the text band
* (EAN/UPC guard patterns). The renderer must honour them: they are what
* lets a scanner find the symbol's edges.
*/
guards: { start: number; end: number }[];
/**
* HRI groups anchored in module space (`start` may be < 0 — the quiet zone ).
* EAN/UPC emit ONE GROUP PER DIGIT, each over the 7 modules that encode it;
* the other symbologies emit a single group over the whole symbol.
*/
text: { text: string; start: number; end: number }[];
/** The value as encoded (input + computed check digit). */
value: string;
symbology: Symbology;
/** The check digit computed, when the symbology carries one. */
checkDigit?: string;
/** ITF-14 only: the symbol must be framed by a bearer bar. */
bearerBar?: boolean;
}
export type BarcodeErrorReason =
| 'empty'
| 'invalid-characters'
| 'invalid-length'
| 'invalid-check-digit';
export class BarcodeError extends Error {
readonly reason: BarcodeErrorReason;
}
```
`guards` is the **exact analogue of `reserved` in `$libs/qr`** : the function
modules the renderer must treat differently. There they stay square so the
scanner locks on; here they extend below the baseline for the same reason.
### v1 symbologies (Retail + Logistics)
| Symbology | Standard | Input | Check digit | Notes |
| --------- | ------------- | ------------------------------ | -------------------------------------- | ------------------------------------------------------------------- |
| `code128` | ISO/IEC 15417 | ASCII 0– 127 | mod-103, **internal** (not in the HRI) | auto A/B/C subset switching; subset C for digit runs ≥ 4 |
| `ean13` | ISO/IEC 15420 | 12 digits | mod-10, appended | L/G parity per first digit; first digit sits **outside** the symbol |
| `ean8` | ISO/IEC 15420 | 7 digits | mod-10, appended | no parity table |
| `upca` | ISO/IEC 15420 | 11 digits | mod-10, appended | EAN-13 with a leading 0; outer digits outside the symbol |
| `upce` | ISO/IEC 15420 | 6 digits (+ number system 0/1) | mod-10 over the expanded UPC-A | parity table selected by the check digit |
| `code39` | ISO/IEC 16388 | 43-char alphabet | mod-43 **optional** | `*` start/stop; self-checking |
| `itf` | ISO/IEC 16390 | digits, **even count** | mod-10 optional | interleaved pairs |
| `itf14` | ISO/IEC 16390 | 13 digits | mod-10 **mandatory** | + bearer bars |
feat(barcode): el ISBN es un PERFIL de entrada sobre EAN-13, no una simbología
Un código de barras de ISBN ES un EAN-13: ISO 2108 mete los libros en el mismo
espacio GTIN-13 que todo lo demás bajo los prefijos Bookland 978/979 — por eso
el mismo lector lee una novela y una lata de garbanzos. Así que `isbn` NO entra
como noveno valor de `symbology`: declararlo haría que `data-symbology`
afirmara un estándar que no existe. Entra como perfil de entrada sobre `ean13`,
que es la forma que adoptan las implementaciones serias.
- **Separadores tolerados** en las simbologías NUMÉRICAS: un ISBN viaja con
guiones y un GTIN con espacios. Code 128 y Code 39 quedan fuera a propósito —
ahí un guión es dato codificable y quitarlo cambiaría la cadena en silencio.
- **ISBN-10 → Bookland**: un valor de 10 caracteres en `ean13` se lee como
ISBN-10, se le valida SU dígito de control (pesos 10…1, mod 11, con `X`
valiendo 10) y se convierte con prefijo 978 y mod-10 recalculado. La
validación previa es la parte que importa: sin ella, un ISBN-10 mal tecleado
se convierte en un EAN-13 perfectamente válido que apunta a OTRO libro — el
modo de fallo más caro del dominio. Falla con `invalid-check-digit`.
- **La hyphenación NO es nuestra** y queda documentada como tal: las posiciones
de los guiones no se calculan desde el número, dependen de las tablas de
rangos de la International ISBN Agency, que son dato versionado con fecha de
caducidad. Meterlas aquí pondría un reloj de mantenimiento dentro de una
librería de cero dependencias. Si algún día se pinta la línea `ISBN 978-…`
sobre las barras, el string con guiones lo trae el consumidor.
Y un defecto que el ISBN destapó, anterior a él: **el nombre accesible mentía**.
`aria-label` anunciaba el valor CRUDO mientras el símbolo codificaba otro —
pasaba ya con cualquier EAN al que se le añade el dígito de control, y con el
ITF-14, y con Code 39 al mayusculizar. Ahora anuncia lo que el símbolo lleva
de verdad.
- Vectores publicados en los tests: `0-306-40615-2` → `978-0-306-40615-7`, y los
dos casos de control `X` (`0-8044-2957-X`, `0-9752298-0-X`).
- `D-1.2` estaba ROTO en el commit anterior y no lo vi: prettier parte la unión
`type Tab` a 102 caracteres y el audit exige la forma de UNA línea que
documenta la guía de demos. Restaurada con `prettier-ignore`; el audit vuelve
a PASS con 0 errores.
- `F-1.4` se rompió al escribir la sección ISBN: mencionaba `## Gaps` en línea y
el regex del audit engancha esa primera aparición. Reescrito.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
## ISBN — a profile, not a symbology
**An ISBN barcode IS an EAN-13.** ISO 2108 assigns books the Bookland prefixes
`978` / `979` inside the same GTIN-13 space as everything else, which is why one
scanner reads a novel and a can of beans. So ISBN is not a ninth `symbology`
value: declaring one would make `data-symbology` claim a standard that does not
exist. It is an **input profile** over `ean13` — the shape every serious
implementation ships.
```svelte
< Barcode symbology = "ean13" value = "978-84-339-2042-3" / >
<!-- ISBN - 13, hyphenated -->
< Barcode symbology = "ean13" value = "0306406152" / >
<!-- ISBN - 10 → 9780306406157 -->
< Barcode symbology = "ean13" value = "080442957X" / >
<!-- base - 11 X check character -->
```
What the profile does:
- **Tolerates separators** (`-`, whitespace) in every NUMERIC symbology — an
ISBN travels hyphenated, a GTIN travels spaced. Code 128 and Code 39 are
deliberately excluded: there a hyphen is encodable data, and stripping it
would silently encode a different string.
- **Reads a 10-character `ean13` value as an ISBN-10** and converts it: prefix
`978` , keep the 9 significant digits, recompute the EAN mod-10 check.
- **Validates the ISBN-10's own check digit first** (weights 10…1, mod 11, where
`X` = 10). This is the part that matters: without it a mistyped ISBN-10 becomes
a _perfectly valid_ EAN-13 pointing at a different book — the costliest failure
mode in this domain. A bad check digit fails with `invalid-check-digit` .
What it deliberately does NOT do:
- **Hyphenate.** The hyphen positions are not computable from the number: they
depend on the International ISBN Agency's prefix-range tables, which are
versioned data with an expiry date. Vendoring them would put a maintenance
clock inside a zero-dependency library. If the `ISBN 978-84-339-2042-3` line
above the bars is ever added, the hyphenated string comes from the consumer's
catalogue — where it is already stored that way.
- **Print the upper ISBN line** (the book-cover convention) or the **EAN-5 price
add-on** — both registered as deferred gaps below.
feat(eidos): `Barcode` — el código de barras 1D, con encoder propio y cero dependencias
Ocho simbologías desde su estándar ISO publicado (Code 128 con auto-switching
A/B/C, EAN-13/8, UPC-A/E, Code 39, ITF/ITF-14) en `$libs/barcode`: tablas
transcritas de la especificación o derivadas de su regla de construcción, sin
una sola dependencia npm añadida. JsBarcode y bwip-js son referencia de
corrección, jamás import — la misma doctrina con la que `$libs/qr` usó a Nayuki.
Ningún headless del mercado trae barcode: el terreno lo ocupan encoders
imperativos sin anatomía ni a11y.
- **La física manda sobre la ergonomía visual.** La dimensión X (`moduleWidth`)
es el ancla de escaneabilidad y la altura de barra un eje libre — un escáner
lee una franja horizontal. Por eso la geometría es px absolutos en el viewBox
(precedente: la familia `chart`) y no el token `size` cuadrado del QR:
colapsar los dos ejes en uno haría que la X dependiera de la altura, que es
justo lo que no puede flotar. La zona muda va en módulos, con el mínimo de
cada estándar.
- **Lo que hace reconocible a la familia.** Las guardas EAN/UPC bajan hasta la
línea base y los dígitos se imprimen dígito-bajo-dígito sobre su celda de 7
módulos, en mono (la HRI del estándar es OCR-B, de ancho fijo). ITF-14 lleva
su barra portadora por fuera de la zona muda, que no puede comerse.
- **Un valor no codificable es un ESTADO, no una excepción.** `encode` lanza un
`BarcodeError` tipado; el wrapper lo captura, marca `data-invalid`, avisa por
`onInvalid` (deduplicado por razón: `encoded` es un objeto nuevo por
pulsación) y deja un marco placeholder decorativo. Un EAN a medio teclear es
un paso normal de edición; dejarlo reventar dentro de un `$derived` tumbaría
la página.
- **`'text'` entra en `MorfoElement`** — y también en el schema de sium, que
tiene su propia lista literal. Añadir solo la unión de TypeScript compila y
falla en runtime; lo cazó `morfo:check`, no `npm run check`.
Verificación: 48 tests de encoder en tres capas (invariantes estructurales de
cada tabla · vectores publicados transcritos aparte · decoders independientes
que releen la retícula), la lectura real confirmada con escáner por el usuario,
y la retícula decodificada desde el SVG ya pintado en navegador. `component:audit`
PASS con 0 errores; los 2 warnings (D-1.5 / D-4.3) son los mismos que da
`button`, la demo canaria: sus regex buscan la forma v1 inline del
MutationObserver y del `emit`, que la v2 movió al harness compartido.
Demo v2 canónica de 9 pestañas. El alcance diferido (Codabar/MSI/Pharmacode,
GS1-128, add-ons EAN-2/5, y las 2D como componentes aparte) queda registrado en
`next-features.md` §10 — el hueco de numeración se cierra cuando el track de
blocks commitee sus §8/§9, hoy sin commitear.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
## Scannability (the doctrine)
The QR's scannability rules restated for a linear symbol — same principle, own
physics:
- **The X-dimension is the anchor.** `moduleWidth` is the rendered width of the
narrowest bar, in px. It is what governs whether a scanner can resolve the
symbol (GS1 nominal X = 0.33 mm; retail range 0.264– 0.66 mm). Everything else
derives from it. Rendering below the nominal X degrades decoding — the `<svg>`
carries `max-width: 100%` so it never overflows a layout, with that caveat
documented rather than silently accepted.
- **The bar height is free.** A scanner reads a horizontal slice; vertical size
is redundancy, not information. That is why `height` is an independent prop
and not a second dimension of a single `size` token.
- **Quiet zone in modules, not pixels — and per symbology.** EAN-13 wants 11 X
left / 7 X right; Code 128 and ITF want 10 X. The encoder returns the spec
value; `quietZone` overrides it. There is deliberately no pixel margin prop:
for fixed spacing in a layout, pad the container in CSS using the barcode's
`background` .
- **Guard bars stay long.** EAN/UPC guards extend through the HRI band down to
the digits' baseline (the `guards` mask), so the digit groups read _between_
them. Flattening them for looks is the linear-barcode equivalent of rounding a
QR's finder patterns — it breaks edge detection.
- **The HRI aligns digit-under-digit.** Every EAN/UPC digit is centred over the
7 modules that encode it (the encoder emits one text group per cell), and the
face is the system's mono anchor — the standard's HRI type is OCR-B, a
fixed-width design. A proportional face with a compact centred string
misaligns the digits from their bars and loses the family's silhouette.
- **ITF-14 carries its bearer bar.** The frame sits OUTSIDE the quiet zone (it
must never eat into it), so the box grows by its thickness on every side.
- **Fixed-tone default colours** (dark-on-light). A barcode cannot 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).
- **Export bakes resolved styles.** A serialized SVG loses the document's CSS
custom properties **and** its inherited font. `DownloadTrigger` resolves the
fills and the HRI's `font-family` / `fill` into the clone before writing
PNG / SVG — the same reason QrCode bakes its fills.
- **The HRI travels in both exports.** The text is a real SVG `<text>` , not a
`<foreignObject>` — so unlike QrCode's logo (which PNG export drops, a
Chromium rasterization limit) it survives PNG too.
## Anatomy (parts)
```
< Barcode value = "5901234123457" symbology = "ean13" > ← Provider (svg, role="img", aria-label)
└ Pattern ← the < path > : bars + guards (decorative)
└ Text ← the HRI < text > (decorative, optional)
< Barcode.DownloadTrigger > ← < Button > → PNG / SVG (optional)
```
morfo `scope: ['eidos']` . **No soma** — a barcode is pure display, like
Avatar / Image / QrCode: `value` → modules in a `$derived` , rendered to SVG.
Download is imperative through the eidos context.
## API (root)
| Prop | Type | Default | |
| ------------- | -------------------------------------- | ---------------------------- | ---------------------------------------- |
| `value` | `string` | `''` | data to encode |
| `symbology` | `Symbology` | `'code128'` | which standard |
| `height` | `number` | `60` | bar height in px |
| `moduleWidth` | `number` | `2` | px per narrow module — the X-dimension |
| `quietZone` | `number` | the symbology's spec minimum | in **modules** , never px |
| `color` | `string` | fixed dark token | bar colour (you own the contrast) |
| `background` | `string` | fixed light token | quiet-zone + symbol background |
| `showText` | `boolean` | `true` | render the human-readable interpretation |
| `text` | `string` | the encoded value | override the HRI |
| `alt` | `string` | `value` | accessible name for the Provider |
| `onInvalid` | `(reason: BarcodeErrorReason) => void` | — | fires once per distinct reason |
Two behaviours worth knowing: `onInvalid` is **deduped by reason** (`encoded` is
a fresh object per keystroke, so an un-deduped effect would spam the handler
while a value is typed), and with nothing encoded the placeholder frame carries
`aria-hidden` — a `role="img"` with an empty name would announce an unlabelled
image.
`DownloadTrigger` props: `format: 'png' | 'svg'` (default `'png'` ), `filename`
(default `'barcode'` ) + all `<Button>` props (`variant`, `size` , `color` , …).
It renders the framework `<Button>` and calls the root's style-baking export via
context.
Compound: `<Barcode>` + `.DownloadTrigger` . The morfo's Pattern / Text parts are
render internals, not subcomponents.
### Geometry
```
mw = moduleWidth // px per module (the X-dimension)
barH = height // px
hri = showText ? max(8, round(barH * 0.2)) : 0 // text band
desc = guards.length ? round(hri * 0.55) : 0 // guard descender into the band
W = (quiet.start + size + quiet.end) * mw
H = barH + hri
viewBox = "0 0 W H" // px, uniform scale → no glyph distortion
```
The `<text>` 's `font-size` travels as a **computed SVG attribute** (proportional
to the band), not as a CSS literal — the same pattern, and the same reason, as
QrCode's inline overlay padding: the viewBox owns geometry, the recipe owns
`font-family` / `letter-spacing` / `fill` .
## Decisiones
- **Own zero-dep encoder** (`$libs/barcode`) — every v1 symbology is a published
ISO standard; JsBarcode / bwip-js are correctness references only. No npm
dependency, runtime or dev.
- **No soma** — pure display (like Avatar / Image / QrCode): `value` → modules
in a `$derived` ; download is imperative through the eidos context.
- **Absolute px geometry instead of QrCode's `size` token.** A QR is square, so
its box collapses into one token. A barcode has two physically independent
axes: the X-dimension governs scannability, the bar height is free redundancy.
Collapsing them into one t-shirt token would make the X-dimension a function
of the height — exactly the property that must not float. The precedent is the
`chart` family (`chart/gauge.svelte`: numeric `size` , px viewBox, `<text>`
styled by the recipe), which is the catalog's home for SVG-drawn primitives.
- **HRI inside the SVG** (`< text > `, not an HTML sibling) — a sibling would be
lost on export, and it is what lets guard bars interleave with the digits.
- **Invalid input is a state, not an exception.** Unlike a QR (any UTF-8 string
encodes), EAN-13 needs 12/13 digits, ITF an even count, Code 39 a 43-char
alphabet — a half-typed value is a _normal_ state. `encode` throws a typed
`BarcodeError` ; the wrapper catches it, stamps `data-invalid` and calls
`onInvalid(reason)` (the framework's canonical validation vocabulary).
Letting it throw inside a `$derived` would take the page down.
- **`quietZone` in modules with a per-symbology default** (not QrCode's single
`4` ) — each standard fixes its own; pixel spacing belongs to the container.
- **The `*` delimiters stay out of Code 39's value.** They are structure, not
data: ISO/IEC 16388 keeps the start/stop out of the HRI, and every scanner
returns the bare string. `result.value` is the (uppercased) input.
- **The HRI is one text group per cell**, emitted by the encoder in module
space. Placement belongs to whoever knows the cell geometry — the renderer
only centres each group in its range, so it needs no per-symbology branch.
- **Fixed-tone default colours** — a barcode can't invert in dark mode and stay
scannable; theming is opt-in via `color` / `background` .
- **Exports bake resolved styles** (`getComputedStyle` into the clone) — a
serialized SVG loses both the document's custom properties and its font.
- **One `<path>` for the whole pattern** — bars and extended guards are subpaths
of different heights, mirroring QrCode's single-path module matrix.
## v1 scope (proposed)
8 symbologies (Code 128 A/B/C auto · EAN-13 · EAN-8 · UPC-A · UPC-E · Code 39 ·
ITF · ITF-14) · computed check digits · spec quiet zones · guard bars · HRI with
override · themeable colour · invalid state · download PNG + SVG · `role=img` +
label.
## Gaps
- **Codabar · MSI · Pharmacode** — the niche tail of JsBarcode's set —
disposition: **diferir** (v2).
- **GS1-128 (FNC1 + Application Identifier parsing)** — shipped by bwip-js —
disposition: **diferir** (v2); it is a Code 128 profile, not a new encoder.
feat(barcode): el ISBN es un PERFIL de entrada sobre EAN-13, no una simbología
Un código de barras de ISBN ES un EAN-13: ISO 2108 mete los libros en el mismo
espacio GTIN-13 que todo lo demás bajo los prefijos Bookland 978/979 — por eso
el mismo lector lee una novela y una lata de garbanzos. Así que `isbn` NO entra
como noveno valor de `symbology`: declararlo haría que `data-symbology`
afirmara un estándar que no existe. Entra como perfil de entrada sobre `ean13`,
que es la forma que adoptan las implementaciones serias.
- **Separadores tolerados** en las simbologías NUMÉRICAS: un ISBN viaja con
guiones y un GTIN con espacios. Code 128 y Code 39 quedan fuera a propósito —
ahí un guión es dato codificable y quitarlo cambiaría la cadena en silencio.
- **ISBN-10 → Bookland**: un valor de 10 caracteres en `ean13` se lee como
ISBN-10, se le valida SU dígito de control (pesos 10…1, mod 11, con `X`
valiendo 10) y se convierte con prefijo 978 y mod-10 recalculado. La
validación previa es la parte que importa: sin ella, un ISBN-10 mal tecleado
se convierte en un EAN-13 perfectamente válido que apunta a OTRO libro — el
modo de fallo más caro del dominio. Falla con `invalid-check-digit`.
- **La hyphenación NO es nuestra** y queda documentada como tal: las posiciones
de los guiones no se calculan desde el número, dependen de las tablas de
rangos de la International ISBN Agency, que son dato versionado con fecha de
caducidad. Meterlas aquí pondría un reloj de mantenimiento dentro de una
librería de cero dependencias. Si algún día se pinta la línea `ISBN 978-…`
sobre las barras, el string con guiones lo trae el consumidor.
Y un defecto que el ISBN destapó, anterior a él: **el nombre accesible mentía**.
`aria-label` anunciaba el valor CRUDO mientras el símbolo codificaba otro —
pasaba ya con cualquier EAN al que se le añade el dígito de control, y con el
ITF-14, y con Code 39 al mayusculizar. Ahora anuncia lo que el símbolo lleva
de verdad.
- Vectores publicados en los tests: `0-306-40615-2` → `978-0-306-40615-7`, y los
dos casos de control `X` (`0-8044-2957-X`, `0-9752298-0-X`).
- `D-1.2` estaba ROTO en el commit anterior y no lo vi: prettier parte la unión
`type Tab` a 102 caracteres y el audit exige la forma de UNA línea que
documenta la guía de demos. Restaurada con `prettier-ignore`; el audit vuelve
a PASS con 0 errores.
- `F-1.4` se rompió al escribir la sección ISBN: mencionaba `## Gaps` en línea y
el regex del audit engancha esa primera aparición. Reescrito.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **EAN-2 / EAN-5 add-ons** — shipped by JsBarcode; the EAN-5 is the price
supplement printed beside a book's ISBN — disposition: **diferir** (v2).
- **The `ISBN 978-…` line above the bars** — the book-cover convention of the
industry guidelines. The band is easy; the hyphenation is not ours (see
`## ISBN` ), so the consumer would pass the already-hyphenated string —
disposition: **diferir** (v2), pairs with the EAN-5 add-on.
feat(eidos): `Barcode` — el código de barras 1D, con encoder propio y cero dependencias
Ocho simbologías desde su estándar ISO publicado (Code 128 con auto-switching
A/B/C, EAN-13/8, UPC-A/E, Code 39, ITF/ITF-14) en `$libs/barcode`: tablas
transcritas de la especificación o derivadas de su regla de construcción, sin
una sola dependencia npm añadida. JsBarcode y bwip-js son referencia de
corrección, jamás import — la misma doctrina con la que `$libs/qr` usó a Nayuki.
Ningún headless del mercado trae barcode: el terreno lo ocupan encoders
imperativos sin anatomía ni a11y.
- **La física manda sobre la ergonomía visual.** La dimensión X (`moduleWidth`)
es el ancla de escaneabilidad y la altura de barra un eje libre — un escáner
lee una franja horizontal. Por eso la geometría es px absolutos en el viewBox
(precedente: la familia `chart`) y no el token `size` cuadrado del QR:
colapsar los dos ejes en uno haría que la X dependiera de la altura, que es
justo lo que no puede flotar. La zona muda va en módulos, con el mínimo de
cada estándar.
- **Lo que hace reconocible a la familia.** Las guardas EAN/UPC bajan hasta la
línea base y los dígitos se imprimen dígito-bajo-dígito sobre su celda de 7
módulos, en mono (la HRI del estándar es OCR-B, de ancho fijo). ITF-14 lleva
su barra portadora por fuera de la zona muda, que no puede comerse.
- **Un valor no codificable es un ESTADO, no una excepción.** `encode` lanza un
`BarcodeError` tipado; el wrapper lo captura, marca `data-invalid`, avisa por
`onInvalid` (deduplicado por razón: `encoded` es un objeto nuevo por
pulsación) y deja un marco placeholder decorativo. Un EAN a medio teclear es
un paso normal de edición; dejarlo reventar dentro de un `$derived` tumbaría
la página.
- **`'text'` entra en `MorfoElement`** — y también en el schema de sium, que
tiene su propia lista literal. Añadir solo la unión de TypeScript compila y
falla en runtime; lo cazó `morfo:check`, no `npm run check`.
Verificación: 48 tests de encoder en tres capas (invariantes estructurales de
cada tabla · vectores publicados transcritos aparte · decoders independientes
que releen la retícula), la lectura real confirmada con escáner por el usuario,
y la retícula decodificada desde el SVG ya pintado en navegador. `component:audit`
PASS con 0 errores; los 2 warnings (D-1.5 / D-4.3) son los mismos que da
`button`, la demo canaria: sus regex buscan la forma v1 inline del
MutationObserver y del `emit`, que la v2 movió al harness compartido.
Demo v2 canónica de 9 pestañas. El alcance diferido (Codabar/MSI/Pharmacode,
GS1-128, add-ons EAN-2/5, y las 2D como componentes aparte) queda registrado en
`next-features.md` §10 — el hueco de numeración se cierra cuando el track de
blocks commitee sus §8/§9, hoy sin commitear.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **2D symbologies (DataMatrix, PDF417, Aztec)** — shipped by bwip-js —
disposition: **diferir** ; each is a whole different encoder (own
Reed– Solomon, matrix layout), so they belong to their own component, not to
this one's scope.
- **`textPosition: 'top'`** — shipped by JsBarcode — disposition: **diferir** (v2).
- **Custom webfont in PNG export** — a rasterized SVG does not load webfonts —
disposition: **descartar** as a defect; the HRI token resolves to a generic
(`ui-monospace, monospace`) family precisely so PNG export stays faithful.
Documented, not worked around.
## Passive justification
Zero events by design: a barcode 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` ), the same
recorded exception path QrCode declares.
## Layers
- **`$libs/barcode`** — own encoder + tests.
- **morfo** `barcode.ts` — Provider / Pattern / Text / DownloadTrigger contract.
- **eidos** `barcode/` — wrapper + recipe (`--barcode-*` tokens) + SVG render
from the module lattice.
- **langs** `langs/components/barcode.ts` — the `download` catalog entry.
- **demo** `web/routes/uix/components/barcode/` + nav entry (Media group).
## Recipe tokens (proposed)
| Token | Role |
| -------------------------------------------------------- | ------------------------------------------------------------------ |
| `--barcode-fg` / `--barcode-bg` | bar colour / quiet-zone (fixed-tone, annotated `/* literal: … */` ) |
| `--barcode-radius` | frame radius |
| `--barcode-text-font-family` | HRI family — anchored to `var(--style-label-font-family)` |
refactor(eidos): el catálogo habla un idioma — 269 claves al vocabulario firmado
D-TH.6, ejecutada. El slot de tinta pasa a `fg` y el modificador interactivo
se pone delante, que es lo que theming §6.7 r7 documentaba sin guard desde
que se escribió. Value-preserving: renombra la clave en `recipes/base.ts` y
sus 793 referencias en 101 ficheros; no toca un solo valor.
claves renombradas 269 en 66 componentes
referencias 793 en 101 ficheros
censo antes/después 162 recetas · 5.203 knobs · 1.841 públicos (37 %) · 56
sin contrato — IDÉNTICO, como debe ser un rename
--names antes/después 269 desviadas → 0
Qué NO entra, y por qué:
47 `{rol}-{slot-de-rol}` canónicas: COLOR_ROLE_SLOTS pone el modificador
detrás POR CONSTRUCCIÓN (`primary-solid-hover`)
13 exentas firmadas `--focus-ring-*` es familia del sistema; el color
de `aura` es un sustantivo; `stop-color` ES una
parte de gradient-builder
47 hovers neutros por VALOR: migran a la capa de estado
(§38 + R-4.3), no se renombran — firma 3
6 ocurrencias en historia changelog, errores-toxico, PLAN-affix/background:
reescribir un registro fechado lo vuelve mentira
El clasificador vive en el censo (`--names`), no en un script suelto, para que
el guard R-5.3 consuma la MISMA gramática que el codemod. Su muta-prueba
(`__names-mutatest.ts`, 27 casos) es lo que hizo el trabajo: cazó que yo
promovía al frente CUALQUIER valor declarado por el morfo, y así
`--sidebar-width-icon` (la anchura del raíl colapsado) se convertía en
`--sidebar-icon-width` (la anchura de un icono), que es otra cosa. La firma
dice «delante lo interactivo, detrás lo dimensional y contextual»: ahora sólo
promociona el vocabulario interactivo cerrado, y lo contextual —`below`,
`loaded`, `vertical`, `icon`— se queda donde estaba. Los cinco casos de
regresión están en la muta-prueba.
También cazó que `dropdown-menu.item-bg-hover` lee `var(--color-primary-element)`:
es un hover CON VALENCIA (el palette swap de recipe-contract §2), no el hover
bespoke que §38 deprecó. Clasificar por el nombre lo habría metido en una
migración que no le toca; se clasifica por el VALOR.
Verificación (§7.4, artefacto por paso):
diff de generated/ 269 renombres 1:1 · 0 cambios de valor · 22 privados
reapuntados a los nombres nuevos (`__names-verify-diff`)
computed tag-group · field · tabs: 6.467 valores en 23 estados,
0 diffs — y comprobado en el navegador que sirve el CSS
nuevo, para que ese 0 no sea el de una copia cacheada
huérfanos 0 en código
suite eidos 434 pasan · 1 rojo, el conocido (`skin-media-player`).
`recipe-css-contract` verde: es el guard que caza el
`var()` sin fallback a un nombre que ya no se emite,
o sea el fallo exacto de un rename a medias
component:audit 163 PASS · 3 NEEDS-WORK, los tres SIN TOCAR por esto
check 0 errores en ficheros tocados (los 73 globales son de
otras sesiones de la rama; se atribuye por fichero)
rtl:check 0 · docs:check 0
formato el renombrado no alarga ninguna línea: ningún fichero
tiene más líneas de 100 chars que antes, así que no se
pasa prettier — hacerlo reformateaba 300 ficheros de
deriva ajena
Entra aquí la corrección del repaso de los 7 ya hechos:
`gradient-builder.checker-color` → `checker-fg` (el damero de transparencia;
ahí `color` era slot). Sus `stop-color-*` no se tocan.
Queda para el paso siguiente: R-5.3 no puede graduar a `error` directo
mientras los 47 hovers neutros sigan hablando el idioma viejo — o migran
antes (firma 3, ya firmada), o el guard necesita una exención greppable.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| `--barcode-text-letter-spacing` / `--barcode-text-fg` | HRI treatment |
feat(eidos): `Barcode` — el código de barras 1D, con encoder propio y cero dependencias
Ocho simbologías desde su estándar ISO publicado (Code 128 con auto-switching
A/B/C, EAN-13/8, UPC-A/E, Code 39, ITF/ITF-14) en `$libs/barcode`: tablas
transcritas de la especificación o derivadas de su regla de construcción, sin
una sola dependencia npm añadida. JsBarcode y bwip-js son referencia de
corrección, jamás import — la misma doctrina con la que `$libs/qr` usó a Nayuki.
Ningún headless del mercado trae barcode: el terreno lo ocupan encoders
imperativos sin anatomía ni a11y.
- **La física manda sobre la ergonomía visual.** La dimensión X (`moduleWidth`)
es el ancla de escaneabilidad y la altura de barra un eje libre — un escáner
lee una franja horizontal. Por eso la geometría es px absolutos en el viewBox
(precedente: la familia `chart`) y no el token `size` cuadrado del QR:
colapsar los dos ejes en uno haría que la X dependiera de la altura, que es
justo lo que no puede flotar. La zona muda va en módulos, con el mínimo de
cada estándar.
- **Lo que hace reconocible a la familia.** Las guardas EAN/UPC bajan hasta la
línea base y los dígitos se imprimen dígito-bajo-dígito sobre su celda de 7
módulos, en mono (la HRI del estándar es OCR-B, de ancho fijo). ITF-14 lleva
su barra portadora por fuera de la zona muda, que no puede comerse.
- **Un valor no codificable es un ESTADO, no una excepción.** `encode` lanza un
`BarcodeError` tipado; el wrapper lo captura, marca `data-invalid`, avisa por
`onInvalid` (deduplicado por razón: `encoded` es un objeto nuevo por
pulsación) y deja un marco placeholder decorativo. Un EAN a medio teclear es
un paso normal de edición; dejarlo reventar dentro de un `$derived` tumbaría
la página.
- **`'text'` entra en `MorfoElement`** — y también en el schema de sium, que
tiene su propia lista literal. Añadir solo la unión de TypeScript compila y
falla en runtime; lo cazó `morfo:check`, no `npm run check`.
Verificación: 48 tests de encoder en tres capas (invariantes estructurales de
cada tabla · vectores publicados transcritos aparte · decoders independientes
que releen la retícula), la lectura real confirmada con escáner por el usuario,
y la retícula decodificada desde el SVG ya pintado en navegador. `component:audit`
PASS con 0 errores; los 2 warnings (D-1.5 / D-4.3) son los mismos que da
`button`, la demo canaria: sus regex buscan la forma v1 inline del
MutationObserver y del `emit`, que la v2 movió al harness compartido.
Demo v2 canónica de 9 pestañas. El alcance diferido (Codabar/MSI/Pharmacode,
GS1-128, add-ons EAN-2/5, y las 2D como componentes aparte) queda registrado en
`next-features.md` §10 — el hueco de numeración se cierra cuando el track de
blocks commitee sus §8/§9, hoy sin commitear.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| `--barcode-invalid-border` / `--barcode-invalid-color` | the invalid placeholder |
No geometry tokens: geometry is computed inline (see Geometry), and a public
token with no real consumer fails `recipe-css-contract.test.ts` .
## Build plan
The route of [`docs/building-a-component.md` ](../../../../../docs/building-a-component.md ):
| Phase | Deliverable | Guard |
| -------------- | ------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------- |
| 1 · Encoder | `src/libs/barcode/{barcode,index,barcode.test}.ts` | `npx vitest run src/libs/barcode` |
| 2 · Morfo | `morfo/components/barcode.ts` + `'text'` added to `MorfoElement` + `langs/components/barcode.ts` registered in the index | `npm run check` · `npm run translations:check` |
| 3 · Sema | none — 0 events (see `## Passive justification` ) | `npm run morfo:vocabulary` |
| 4 · Wrapper | root + download-trigger + context + types + index | `component-api-contract.test.ts` |
| 5 · Recipe | `barcode` block in `lib/recipes/base.ts` + `barcode.css` | `npm run generate:eidos-css` · `recipe-css-contract.test.ts` |
| 6 · Demo | `web/routes/uix/components/barcode/+page.svelte` — **v2 9-tab template** + nav entry | audit `D-*` · `npm run smoke` |
| 7 · README | this file, minus the "pending implementation" note | audit `F-*` |
| 8 · Acceptance | it passes | `component:audit --only barcode` · `morfo:check` · `eidos-lint` |
### Morfo skeleton
```
Provider svg role='img' aria-label ← propRef('alt')
data-symbology (enum of 8) · data-invalid (optional)
Pattern path archetype 'indicator' aria-hidden (bars + guards)
Text text archetype 'indicator' aria-hidden optional (HRI)
DownloadTrigger button archetype 'trigger' type=button + aria-label optional
texts: { download: '#?components.barcode.download|Download barcode' }
```
`data-symbology` documents the closed set in the contract the same way
`data-cell-shape` does for QrCode (which likewise carries no CSS rule —
`architecture/eidos.md` §"the unused column" classifies that as legitimate).
`data-invalid` **does** get a recipe rule (R-1.4).
### Verification
1. `npx vitest run src/libs/barcode` — published vectors per symbology (EAN-13
`5901234123457` , UPC-A `036000291452` , Code 39 `*ABC*` , …), check digits,
module counts, guard positions, quiet zones, **plus an independently written
test decoder** (tables transcribed a second time) that re-derives the input
from `modules` — it catches layout / interleaving errors the shared tables
would hide.
2. `npm run check` — 0 errors in the new files.
3. `npm run generate:eidos-css` + `npx vitest run src/uix/eidos` — TSC + recipe
contract.
4. `node --import tsx/esm scripts/eidos-lint.ts barcode` — 0 `invalid` .
5. `npm run translations:check` .
6. With `npm run dev` up: `npm run morfo:check` ·
`SMOKE_SCOPE=/uix/components/barcode npm run smoke` .
7. `npm run component:audit -- --only barcode` — 0 errors; every remaining
warning justified under `## Audit exceptions` .
8. Browser: open `/uix/components/barcode` , look at it, exercise light/dark, RTL
and density from the System tab, and check both exports.
**Open item — real-world scanning.** Chrome on Windows does not expose
`BarcodeDetector` (the Shape Detection API ships only on Android / ChromeOS /
macOS), and adding a decoder package would break the zero-dependency premise.
Two ways out, user's call: ** (a)** scan the demo with a phone — the real proof;
**(b)** a one-off cross-check with `npx @zxing/library` in a throwaway script
outside the repo (`npx` never touches `package.json` ), with exactly the status
Nayuki had for the QR: correctness reference, never an import. Without one of
the two, scannability rests on the standards' vectors alone.
## Audit exceptions
- `R-1.5 exception:` the only interactive part (`DownloadTrigger`) composes the
canonical `<Button>` — the focus ring lives in `button.css` ; `Pattern` and
`Text` are display indicators.
- `D-1.5` / `D-4.3` (warnings, shared with the canary): the audit greps the demo
source for an inlined `new MutationObserver` and a literal `uix.events.emit` .
The v2 migration moved both into the shared harness — the observer lives in
`DemoTrace` , the ▶ play in `SemaPanel` — so ** `button` , the canonical
reference demo, reports the same two warnings**. Verified by running the audit
on it. The regexes lag the harness; this demo uses both correctly.
- The Sema ▶ play surface is empty by construction: 0 events (see
`## Passive justification` ), so `SemaPanel` renders its justified empty state.