fix(eidos): el aviso al pie se lee donde se ve, y la fuente unica deja de mentir sobre si misma

Cierra los dos hallazgos que quedaban de la auditoria.

ORDEN DE LECTURA (a11y). `position: fixed` saca la tira del flujo VISUAL pero no
del DOM: se lee donde esta en el fuente. Con `affix="bottom"` el aviso se ve al
pie y la mini-pagina lo renderizaba primero, asi que un lector de pantalla lo
anunciaba ANTES que la cabecera — medido en la preview del propio block, era el
primer nodo del documento.

Quien decide eso es el APP, no el block: el block coloca, el documento ordena.
Asi que la correccion no es un prop, es que la demo deje de modelar el desajuste
— el aviso se renderiza donde se ve — mas la advertencia en el typedoc de `affix`
y una fila en los Gaps. Verificado en los dos bordes: con `bottom` el aviso va
DESPUES de la cabecera en el DOM y se ve al pie; con `top` va antes y se ve
arriba. En ambos, z 150 y pegado a su borde.

AFIRMACIONES OBSOLETAS. Cinco sitios seguian diciendo que `Fab` "ships a private
copy today" y que su migracion estaba pendiente — falso desde 1fc0a7a96, que es
mi propio commit. El fichero que declaro fuente unica mentia sobre su estado.
Corregidos `affix.svelte`, `index.ts`, la tabla de attrs del README y las dos
menciones de la demo. Lo que queda dicho es lo cierto: `Fab` la lee, `MenuDial`
sigue con copia propia.

check 69 = base intacta · blocks 0 · layer:check 0 violaciones · contrato de capa
10/10 · smoke 311/311.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
alpha-0.1-dir-prefs
dev 2 months ago
parent acb7f0b416
commit 4b39b718c6

@ -100,6 +100,7 @@ surfaces: it owns the `dismissed` state, as the canon requires.
| **`role="banner"` cannot be overridden** | **canon** — the component stamps it AFTER its rest props, so a page that also has a header gets a second banner landmark. Measured in the preview: two. Mitigated only by naming both with `aria-label` |
| **~~Sticky / bottom notice~~ — RESUELTO 2026-08-14** | **shipped** — `affix="top" \| "bottom"` composes the canon `Affix`. The old disposition («app-land, compose `Sticky`») named a component that CANNOT do it: sticky unpins when its container ends, and a banner lives at the start of the document |
| **The affixed strip covers content** | **app-land** — `fixed` reserves no space, exactly like a fixed header. The block cannot pad a page it does not own; the app adds the matching `padding-block` when it affixes |
| **Reading order of an `affix="bottom"` strip** | **app-land** — `fixed` takes the strip out of the VISUAL flow, never out of the DOM: it is read where it sits in the source. A bottom notice rendered at the top of the page is announced BEFORE the header — measured in this block's own preview, where it was the first node of the document. The block places; the document orders, and the demo now renders the notice where it is SEEN |
| **An affixed strip inside a transformed ancestor stops being fixed** | **canon** — any ancestor with `transform`/`filter`/`contain` becomes the containing block for a fixed descendant, silently. Registered in `Affix`'s own Gaps (its portal escape is deliberately out of v1); a page-level banner is rarely inside one |
| **Dismissal persistence** (cookie / storage) | **app-land** — the component's README discarded it explicitly |
| **Animated entrance / exit** | **deferred** — the component deferred it until ≥2 cases; here it would shift the page on load |

@ -22,6 +22,13 @@ export type SiteBannerProps = Omit<BannerProps, 'children'> & {
* Out of flow, so the page does not reserve space for it: pad the page by
* the strip's height if it must not cover content. Undefined = in flow.
*
* ⚠️ **Reading order is yours, not the block's.** `fixed` takes the strip out
* of the visual flow but NOT out of the DOM — it is read where it sits in the
* source. With `affix="bottom"` the notice is SEEN at the foot, so rendering
* the block at the top of the page makes a screen reader announce it before
* the header. Render it where it is seen; the block places, the document
* orders.
*
* For the OTHER behaviour — a strip that scrolls with the page and then pins
* — wrap the block in `Sticky` from the app; that stays composition, exactly
* as it was before this prop existed.

@ -116,11 +116,11 @@ undeclared `data-affix*`. If `Fab` and `MenuDial` stamped `data-affix` to get
the geometry, their own elements would be welded into this component's contract
(a `Fab placement="static"` would report a missing required attr). So:
| Attr | Stamped by | Selected by |
| ---------------------- | --------------------------------------------------- | ----------------------------------------------------------- |
| `data-affix` | `<Affix>` only | nothing — it is the identity, and morfo-check's entry point |
| `data-affix-placement` | `<Affix>` today; `Fab` / `MenuDial` after migration | every rule in the layer |
| `data-affix-stretch` | `<Affix>` | the one full-bleed rule (block edges only) |
| Attr | Stamped by | Selected by |
| ---------------------- | ----------------------------------------------- | ----------------------------------------------------------- |
| `data-affix` | `<Affix>` only | nothing — it is the identity, and morfo-check's entry point |
| `data-affix-placement` | `<Affix>` and `Fab`; `MenuDial` after migration | every rule in the layer |
| `data-affix-stretch` | `<Affix>` | the one full-bleed rule (block edges only) |
morfo-check skips any attr that does not start with the morfo's own kebab, so a
`Fab` carrying `data-affix-placement` is never judged against `affixMorfo`. This

@ -15,8 +15,9 @@
*
* scope:['eidos'] — no runtime. `affixMorfo` declares the `data-affix`
* identity and the two knobs; this wrapper stamps them. The geometry lives
* in `affix.css`, keyed on `data-affix-placement` so `Fab` / `MenuDial` can
* migrate onto the same rules without inheriting this morfo's contract.
* in `affix.css`, keyed on `data-affix-placement` so a consumer gets the same
* rules without inheriting this morfo's contract. `Fab` did on 2026-08-14;
* `MenuDial` is the one still carrying a private copy.
*/
import { ActiveEidos } from '$uix/eidos';
import { composeInlineStyle } from '$uix/eidos/lib/style';

@ -9,7 +9,8 @@
// The companion of <Sticky>, not a variant of it — sticky unpins when its
// container ends, fixed never does. It POSITIONS; the surface is the child's.
// The geometry is a shared layer keyed on `data-affix-placement` (affix.css),
// the single source `Fab` and `MenuDial` migrate onto. scope:['eidos'].
// the single source of that geometry: `Fab` reads it (2026-08-14), `MenuDial`
// still carries its own copy. scope:['eidos'].
import Affix from './affix.svelte';
export { Affix };

@ -49,7 +49,7 @@
const pagePad = $derived(!dismissed && affix ? `${padSide}: 3.5rem;` : '');
</script>
{#if !dismissed}
{#snippet notice()}
<!--
`aria-label` porque la página tiene DOS landmarks `banner`: este strip (que
fija `role="banner"` y no deja sobreescribirlo) y el `<header>` del
@ -72,6 +72,18 @@
<Link href="#novedades" size="sm" color="currentColor">Ver qué cambia →</Link>
{/snippet}
</SiteBanner>
{/snippet}
<!--
El aviso se renderiza donde SE VE, no siempre arriba. `position: fixed` saca la
tira del flujo visual pero NO del DOM: se lee donde está en el fuente. Con
`affix="bottom"` el aviso se ve al pie, así que ponerlo primero haría que un
lector de pantalla lo oyera antes que la cabecera — medido en esta misma
preview, era el primer nodo del documento. Quién decide esto es el APP, no el
block: el block coloca, el documento ordena.
-->
{#if !dismissed && affix !== 'bottom'}
{@render notice()}
{/if}
<div style={pagePad}>
@ -157,3 +169,7 @@
</Section>
{/each}
</div>
{#if !dismissed && affix === 'bottom'}
{@render notice()}
{/if}

@ -145,7 +145,7 @@
away with it; an element anchored to a viewport edge is <code>fixed</code>, and that is a
different component. It POSITIONS and nothing else: the surface is the child's. Its geometry
is a <strong>shared layer</strong> keyed on <code>data-affix-placement</code> — the single
source <code>Fab</code> and <code>MenuDial</code> migrate onto.
source <code>Fab</code> already reads, and <code>MenuDial</code> still has to.
</p>
<div data-uix-page-meta>
<span data-uix-meta-pill><span data-uix-meta-key>zones</span>{placements.length}</span>
@ -639,9 +639,11 @@
<p data-uix-section-desc>
<code>src/uix/eidos/components/affix/affix.css</code> is both this component's recipe AND
the shared viewport-placement layer. <code>Fab</code> (4 zones) and <code>MenuDial</code>
(9) ship private copies of these rules today, with divergent tokens; they migrate onto this file
by importing it and stamping <code>data-affix-placement</code>, the way the menu / listbox
recipes consume <code>lib/list-surface.css</code>.
(9) each shipped a private copy. <code>Fab</code> migrated on 2026-08-14;
<code>MenuDial</code>
is the one left, and it migrates onto this file by importing it and stamping
<code>data-affix-placement</code>, the way the menu / listbox recipes consume
<code>lib/list-surface.css</code>.
</p>
<div data-uix-table-wrap>
<table data-uix-table>

Loading…
Cancel
Save

Powered by TurnKey Linux.