feat(blocks): `newsletter` — el alta al boletín, y el reset que el framework daba por hecho
F2.9. Slots de snippet (eyebrow · title · description · field · submit · note ·
children), `center` y `justified`, y `panel` como interruptor real: el panel de
marca es la cara de las referencias, pero el control, la ayuda y el error están
calibrados para la superficie de página, así que apagarlo es un estado de primera
y no un fallback.
La fila es `Grid templateColumns={{ base: '1fr', sm: '1fr auto' }}`: el campo se
queda la pista libre y la acción abraza su contenido —eso es lo que hace que un
alta se lea como UN gesto— y bajo `sm` pasa a una columna, porque un botón al lado
de un campo de correo deja inservibles a los dos.
El block NO valida y NO emite sema. `Form` posee el runtime, el esquema, dirty/
touched, la agregación de errores y el foco al primer error; `Field` posee el
cableado ARIA; el morfo del `Form` ya declara `commit-submit` y `signal-invalid`.
El app posee esquema, valores y handler. No hay ni un `if` sobre un correo aquí.
## Superación del dossier: la etiqueta
El único hueco que el dossier nombraba era la nota de privacidad, y está. Pero la
diferencia de verdad es la etiqueta: las referencias shipean la fila escondiéndola
con `sr-only` o dejando solo un placeholder. El canon tiene `Field floatingLabel`
—arranca dentro del control y sube al borde al enfocar o rellenar—, así que la fila
queda alineada CON etiqueta real y asociada. Verificado con pulsaciones de teclado
de verdad: 10px dentro en reposo → −11px sobre el borde al enfocar y al rellenar.
## Tres hallazgos de canon más, medidos
- **La fundación de eidos no trae reset de modelo de caja y lo asume del app.**
`[data-field-control]` declara `inline-size: 100%` + padding, así que bajo
`content-box` el control mide 30px más que su contenedor: el campo se metía por
debajo del botón de envío. Campo 480 / control 510 en la galería frente a
502 / 502 en los docs de componentes, que sí resetean (igual que
`web/routes/active/styles.css`). Arreglado en app-land con
`web/routes/blocks/_lib/reset.css`, con A/B sobre los 10 previews y 5 páginas de
shell: cambia el newsletter y NADA más. Hay que importarlo dos veces porque la
galería arranca UIX en línea en vez de pasar por `BootUix` — deuda del arnés,
anotada en el handoff.
- **`onValidSubmit` es un no-op silencioso** cuando se pasa un `form` ya
construido: el componente solo lo reenvía al `createForm` que hace él mismo. El
envío validaba, limpiaba el error y no anunciaba nada. Por eso el block no
expone el prop: el handler va en el `createForm` del app.
- **Los mensajes de SIUM son idlangref.** La vía correcta es
`uix.langs.t(issue.message, issue.params)` —verificado, sale «Debe ser una
dirección de correo válida»—, pero la demo de docs del propio `Form` parte la
cadena a mano tras el `|`, así que el único ejemplo del repo enseña el patrón
equivocado y siempre muestra inglés.
Los tres quedan en los gaps del README y en `PLAN-blocks-quality.md` §6
(F18/F19/F20), sin tocar nada fuera del tier.
Verificado en navegador el arco completo: correo inválido → error traducido con
`role="alert"`, `aria-invalid`, `aria-describedby` y foco al primer error; correo
válido → confirmación del app (`Callout` afirmativo con la dirección) y error
limpio. Claro/oscuro/RTL, tres colores, `panel` sí/no, 420px. Cero errores de
página. Gates: `blocks:check` verde (10 blocks) · `svelte-check` sin errores
propios · prettier limpio.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
|
|
|
/*
|
|
|
|
|
* The one reset the blocks section needs, for BOTH of its surfaces (the gallery
|
|
|
|
|
* shell and every block's standalone `preview` page) — which is why it is
|
|
|
|
|
* imported from `BootUix`, the bootstrap they share.
|
|
|
|
|
*
|
|
|
|
|
* WHY: the eidos foundation ships no box-model reset; recipes assume the APP
|
|
|
|
|
* provides one. Every other surface in this repo does — `web/routes/uix/uix.css`
|
|
|
|
|
* under `[data-uix-docs]`, `web/routes/active/styles.css` for the app — and the
|
|
|
|
|
* blocks gallery was the one harness built without it (F0).
|
|
|
|
|
*
|
|
|
|
|
* The cost of that omission is not cosmetic. A recipe like `[data-field-control]`
|
|
|
|
|
* declares `inline-size: 100%` + `padding-inline`, so under `content-box` the
|
|
|
|
|
* control renders 30px WIDER than its container: in the newsletter's
|
|
|
|
|
* `1fr auto` row the email control grew past its track and slid under the submit
|
|
|
|
|
* button. Measured, not guessed — field 480 / control 510 here versus 502 / 502
|
|
|
|
|
* on the component docs, which have the reset.
|
|
|
|
|
*
|
|
|
|
|
* The canon-side question (should the foundation own this rather than assume it?)
|
|
|
|
|
* is registered in `docs/process/PLAN-blocks-quality.md` §6.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
*,
|
|
|
|
|
*::before,
|
|
|
|
|
*::after {
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
}
|
blocks(app-shell): la pieza que posee las alturas de la página, y por eso el aviso deja de tapar la cabecera
F3.1 del tier, con su fase 0 firmada delante. El block que faltaba: el que
POSEE las regiones de una página de aplicación — sus landmarks, sus enlaces de
salto y sus alturas.
**A-95 se cierra por construcción, no compensando.** Bajo `scroll="main"` no
hay nada fijo: el aviso es una fila, el cromo es una fila y la página es la fila
que se desplaza. Medido con la tira puesta: aviso [0,54], cabecera [54,85],
**solape 0**, y el hit-test en el centro de la cabecera cae en la cabecera
(antes: 49px de solape y el clic en la tira). El eje `scroll` es el que Mantine
expone como `mode` y Atlassian como `isFixed` por slot; `body` existe porque
`docs-shell` lo necesitará para anclas y TOC.
Compone `Sidebar` (sus tres colapsos, su drawer móvil, sus dos landmarks),
`SkipLink`, `Sticky`, `Grid`/`Flex`/`Box`. Sin `.css`. Coordina tres booleanos
(`navOpen · asideOpen · mobile`) por contexto, sin máquina y sin palabras: nada
en un shell puede bloquearse, así que no hay frase que decir.
Medido en navegador (Playwright; 1280 y 375, claro y oscuro, LTR y RTL):
- landmarks por construcción — 1 banner · 1 main · 2 navigation NOMBRADOS ·
2 complementary NOMBRADOS · 1 contentinfo; encabezados 1-2-2-2-2 sin saltos.
- `scroll="main"`: documento 720 = viewport, el main scrollea dentro y la
cabecera se queda en 0 tras desplazar 900px. `scroll="body"`: el documento
scrollea y la cabecera SE PEGA (top 0 a 900px de scroll).
- teclado: los tres enlaces de salto son las tres primeras paradas y Enter deja
el foco en el `<main>`.
- 375: el raíl es drawer, devuelve el foco al trigger con Escape, el aside se
pliega y el documento no crece. RTL: sin desbordamiento horizontal, raíl a la
derecha, aside a la izquierda.
Cuatro averías que sólo aparecieron midiendo, y las cuatro son doctrina ahora:
1. `minHeight` es un SUELO, no un techo: con `100dvh` ahí el documento crecía
con el contenido. Es `height`.
2. Un grid que sólo declara filas tiene UNA columna implícita `auto`, que se
encoge a su contenido: la página salía en una tira de 32px al lado del raíl.
3. `Box` declara `flex-grow/shrink/basis`, así que pisa el `flex: 1 1 auto` de
la receta del inset. Quien está más abajo en el árbol tiene que PEDIR crecer.
4. Un ancestro con `overflow` se lleva el `position: sticky` de dentro. El
`overflow: auto` de `Sidebar.Inset` dejaba la barra sin pegar bajo `body`
mientras `data-stuck` decía que sí — el atributo dando la razón a una página
que no la tenía. Por eso el shell pone su propia caja: gap de canon anotado
(el inset debería dejar elegir si es contenedor de scroll).
**Y una avería del ARNÉS que este block destapó**: los 19 previews del tier
llevaban los 8px de margen por defecto del navegador, así que ningún block se
enseñaba a sangre — contra la regla dura del propio tier. Medido:
`x: 8, y: 8, w: 1264` en 1280, la página compuesta incluida, cuyo README cita
cifras a sangre. Invisible porque todos scrollean; deja de serlo con un shell
que llena el viewport. Arreglado en el reset del arnés.
De paso, en el canon `skip-link`: el enlace del panel complementario decía «Ir
a la barra lateral», que es como todo el mundo llama al raíl de NAVEGACIÓN. En
una página que tiene los dos, nombraba al que no era. Ahora «Ir al panel
lateral».
Guards: `blocks:check` verde con 19 blocks · `docs:check` 0/0 (638) ·
`svelte-check` 74, ninguno aquí.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* The browser's default `body { margin: 8px }`, removed — and it is not a
|
|
|
|
|
* cosmetic call either.
|
|
|
|
|
*
|
|
|
|
|
* The tier's hard rule is that a block is shown A SANGRE: nothing between it
|
|
|
|
|
* and the edge, because a frame changes what the block DOES (a header pinned at
|
|
|
|
|
* `offset: 0` measured 21px off inside a Card). That rule was being broken by
|
|
|
|
|
* the document itself: measured 2026-08-19, every preview rendered at
|
|
|
|
|
* `x: 8, y: 8, w: 1264` inside a 1280 viewport — the composed landing page
|
|
|
|
|
* included, whose README quotes full-bleed numbers.
|
|
|
|
|
*
|
|
|
|
|
* It stayed invisible because every other block's page scrolls anyway, so 16px
|
|
|
|
|
* of extra document height reads as nothing. `app-shell` is where it stops
|
|
|
|
|
* being invisible: a shell that fills the viewport (`100dvh`) plus 16px of
|
|
|
|
|
* margin is a page with a scrollbar and no content to scroll.
|
|
|
|
|
*/
|
|
|
|
|
body {
|
|
|
|
|
margin: 0;
|
|
|
|
|
}
|
blocks(app-shell): la demo no usaba los componentes que SIGNIFICAN lo que enseñaba
Rehecha entera. Cada pieza es ahora el componente del canon que corresponde al
acto, no una caja con el aspecto adecuado:
- las tres cifras son `Metrics` (label · value · delta), con la dirección
desacoplada de la valencia: en «sin asignar», BAJAR es bueno (`goodTrend`),
que es justo lo que las referencias hacen mal;
- la bandeja y la actividad son `Feed` — `role="feed"`, cada conversación un
`role="article"` con `aria-posinset/setsize` y su título con `aria-level`,
teclado de feed incluido. Medido: 2 feeds, 11 artículos, 11 encabezados;
- la barra superior es `Toolbar` (`role="toolbar"`, roving tabindex: cinco
controles cuestan UNA parada de tabulación) más `Breadcrumb` con `current`;
- la tipografía sale de los estilos SEMÁNTICOS del tema — `Text
style="label|caption|body"`, `Heading level` + `style` — en vez de tamaños
elegidos a ojo, que es lo que había.
**Y la avería que esto destapó, que era del arnés y afectaba a los 19
previews**: la foundation declara los `@font-face` y emite los estilos
(`--style-body-*`…) pero no los aplica a ningún elemento — un `Text` ancla su
familia en su recipe y todo lo demás HEREDA del documento. El arnés de blocks no
anclaba nada, así que `html`, `body`, `[data-sidebar]` y cada fila del raíl
computaban **Times New Roman** al lado de los `Text` en Instrument Sans. El
shell de docs sí lo ancla (`uix.css`); éste no. Anclado en `_lib/reset.css` con
los tokens del tema, no con una familia escrita. De paso, el sangrado de 40px
que el navegador pone a `dd`.
Tres huecos del canon que salieron al componer, registrados en los Gaps:
1. **`Sidebar.MenuButton` es mudo y `NavigationMenu.Link` no** — el mismo acto
con dos respuestas. El README del `Sidebar` firma que «navegar una fila es
nativo y no suena», pero entonces el `NavigationMenu` contradice la firma; y
en un shell la fila casi nunca navega: SELECCIONA la sección. Es de sema, no
del tier.
2. **`Toolbar.Button` no acepta `variant`/`color`** — tipa contra el
`ButtonProps` de soma (`{id, disabled}`), así que un cluster no puede decir
cuál es su acción principal. Aquí va al lado, como en las referencias.
3. **No existe `description-list`** y el panel de detalle es exactamente el
«primer detail-view real» que su ficha F5 pone como disparador.
Guards: `blocks:check` verde · `docs:check` 0/0 · `svelte-check` sin errores
propios (dos props inexistentes corregidas de paso: `Icon.Filter` y el
`variant` del `Toolbar.Button`).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* The document's typographic anchor — the theme's own `body` style, and the
|
|
|
|
|
* second thing the foundation leaves to the app (the box model was the first).
|
|
|
|
|
*
|
|
|
|
|
* The foundation declares the `@font-face`s and emits the semantic styles
|
|
|
|
|
* (`--style-body-*`, `--style-label-*`, `--style-h1-*` …) but applies none of
|
|
|
|
|
* them to any element: a `Text` or a `Heading` anchors its own family in its
|
|
|
|
|
* recipe, and everything else INHERITS from the document. So any text the canon
|
|
|
|
|
* renders as plain text — the label `<span>` inside a `Sidebar.MenuButton`, a
|
|
|
|
|
* `Sidebar.GroupLabel` — falls to the browser default when the document says
|
|
|
|
|
* nothing. Measured 2026-08-19 on the app-shell preview: `html`, `body`,
|
|
|
|
|
* `[data-sidebar]` and every rail item computed `"Times New Roman"`, next to
|
|
|
|
|
* `Text` nodes in Instrument Sans. The component docs shell anchors its root
|
|
|
|
|
* (`uix.css`, `[data-uix-docs]`); this harness did not, on any of its 19
|
|
|
|
|
* previews.
|
|
|
|
|
*
|
|
|
|
|
* The values are the theme's `body` style, by token — never a family or a
|
|
|
|
|
* size written here — so a theme that redefines its body redefines this too.
|
|
|
|
|
*/
|
|
|
|
|
body {
|
|
|
|
|
font-family: var(--style-body-font-family);
|
|
|
|
|
font-size: var(--style-body-font-size);
|
|
|
|
|
line-height: var(--style-body-line-height);
|
|
|
|
|
letter-spacing: var(--style-body-letter-spacing);
|
|
|
|
|
-webkit-font-smoothing: antialiased;
|
|
|
|
|
-moz-osx-font-smoothing: grayscale;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* The description-list defaults, removed for the same reason as the body
|
|
|
|
|
* margin: the browser indents `dd` by 40px, which reads as an alignment choice
|
|
|
|
|
* nobody made. Measured on the app-shell preview's detail panel, where every
|
|
|
|
|
* value sat a step to the right of its term. Layout in the tier comes from the
|
|
|
|
|
* layout components' props, so a UA margin is noise, not a decision.
|
|
|
|
|
*/
|
|
|
|
|
dl,
|
|
|
|
|
dd {
|
|
|
|
|
margin: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* The description-list defaults, removed for the same reason as the body
|
|
|
|
|
* margin: the browser indents `dd` by 40px, which reads as an alignment choice
|
|
|
|
|
* nobody made. Measured on the app-shell preview's detail panel, where every
|
|
|
|
|
* value sat a step to the right of its term. Layout in the tier comes from the
|
|
|
|
|
* layout components' props, so a UA margin is noise, not a decision.
|
|
|
|
|
*/
|
|
|
|
|
dl,
|
|
|
|
|
dd {
|
|
|
|
|
margin: 0;
|
|
|
|
|
}
|