feat(eidos): Affix — la pieza que Sticky no puede ser, y la capa que Fab y MenuDial ya habian copiado dos veces
Un aviso al principio del documento se va con la pagina: `position: sticky` se
despega en cuanto termina su contenedor. Medido el 2026-08-10 sobre el block
`banner` — `Sticky edge="bottom"` acaba en `top: -8` sin `data-stuck`. No es un
cableado roto: es que un elemento anclado al viewport es `fixed`, y eso es otro
componente.
Pero no es un componente nuevo: la capacidad YA existia dos veces. `fab.css`
fija 4 esquinas, `menu-dial.css` nueve, y las dos ya divergian (`--fab-offset`
publico vs `--_menu-dial-offset` interno; `var(--fab-z)` vs
`var(--z-index-sticky, 1100)`, un fallback de 1100 sobre un token que vale 100).
Y antes de eso existio entera: `air/layout/float`, el primitivo de 9 zonas que
el refactor retiro sin sustituto — lo dicen los README de `float` y de `Fab`.
Asi que `affix.css` no es un tercer ejemplar: es la CAPA, enganchada en
`data-affix-placement` y no en `data-affix`. El split es portante —
`morfo-check` selecciona `[data-affix]` en toda la pagina y valida cada match
contra `affixMorfo`, asi que si `Fab` estampara la identidad quedaria soldado a
este contrato. Estampando solo el gancho de capa, no. Es el patron de
`lib/list-surface.css`: una fuente, cero nodos extra.
Lo que salio midiendo, no razonando:
- `--z-index-affix: 150`, peldano nuevo. Con la tira en `top` empataba con
`--sticky-z-index` (100 los dos) y el empate lo rompe el ORDEN DEL DOM: el
aviso va antes que la cabecera en el fuente, luego perdia. 30,7px de solape con
el cromo encima — el fallo original reproducido por su sustituto. Un peldano
toca DOS sitios: `STATIC_Z_INDEX` y la lista cerrada `Z_INDEX_KEYS`.
- NO compone `<Box>`, aunque sea el patron de los primitivos de layout:
`box.css` declara `position: var(--box-position, revert-layer)` a la misma
especificidad que el gancho, y eidos no usa `@layer`. En el orden de carga
equivocado, `position: static` — un `fixed` que no hace nada.
- `stretch` es solo de los bordes de bloque. El rail del eje inline se envio y se
retiro el mismo dia: `Affix` no dimensiona a su hijo, asi que era una caja
invisible de 380px con el hijo de 47px arriba, identica a `top-start` en
pantalla. El marco medido decia otra cosa; la pintura mandaba.
- `env(safe-area-inset-*)` es fisico y los anclajes logicos: remapeado bajo
`:dir(rtl)`. Emparejar `inset-inline-start` con `safe-area-inset-left` despeja
la muesca equivocada en RTL apaisado — que es lo que hacen hoy `fab` y
`menu-dial`, registrado.
`banner` recupera lo que su README daba por imposible: `affix="top" | "bottom"`.
La disposicion anterior decia «app-land, el canon ya trae Sticky» y nombraba un
componente que no puede hacerlo.
Verificado con recorrido real (342px en la demo, 1247px en la preview del
block): desplazamiento 0 en los dos bordes, nueve zonas exactas, RTL espejando,
el ancestro transformado derivando -342px como esta documentado, y
`elementFromPoint` sobre la tira devolviendo el aviso y no el cromo.
component:audit PASS 0/0 · morfo:check PASS · rtl 0/178 · smoke 311/311.
Queda: migrar `Fab` y `MenuDial` a la capa.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
|
|
|
|
# PLAN — `Affix`: fijar contenido a un borde del viewport (componente de canon)
|
|
|
|
|
|
|
|
|
|
|
|
> # ✅ CERRADO — 2026-08-14
|
|
|
|
|
|
>
|
|
|
|
|
|
> **`Affix` está construido, verificado y en el árbol.** Lo que sigue es el plan
|
|
|
|
|
|
> ORIGINAL, conservado íntegro porque su razonamiento sigue siendo la explicación
|
|
|
|
|
|
> de por qué existe el componente. Lo que se envió NO es lo que proponía §2: las
|
|
|
|
|
|
> cuatro decisiones se firmaron con cambios sustanciales, y el delta está en
|
|
|
|
|
|
> **[§6, al final](#6-lo-que-se-construyó-realmente--delta-vs-este-plan)**.
|
|
|
|
|
|
> Lee §6 antes que §2 o construirás otra cosa.
|
|
|
|
|
|
>
|
|
|
|
|
|
> **Fuente viva del componente**:
|
|
|
|
|
|
> [`src/uix/eidos/components/affix/README.md`](../../src/uix/eidos/components/affix/README.md).
|
|
|
|
|
|
>
|
|
|
|
|
|
> **Deuda abierta que dejó**: migrar `Fab` y `MenuDial` a la capa compartida
|
|
|
|
|
|
> (registrada en los Gaps del README del componente).
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
> **Kickoff para sesión nueva** _(histórico — el plan ya se ejecutó)_: _"Lee
|
|
|
|
|
|
> `docs/process/PLAN-affix.md` y empieza por la fase 0."_ Este plan es
|
|
|
|
|
|
> autosuficiente: cada fase dice QUÉ leer, QUÉ producir y CON QUÉ guard se
|
|
|
|
|
|
> verifica.
|
|
|
|
|
|
>
|
|
|
|
|
|
> **Antes de escribir una línea**: las CUATRO decisiones de §2 son propuestas
|
|
|
|
|
|
> razonadas, no decisiones tomadas. Se presentan al usuario y se firman. Sin eso
|
|
|
|
|
|
> no se abre la fase 1.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 0. PRECONDICIÓN — leer antes de concluir nada
|
|
|
|
|
|
|
|
|
|
|
|
> Regla férrea del usuario, dictada el 2026-08-10 tras una sesión entera de
|
|
|
|
|
|
> análisis erróneos. No es contexto opcional: es lo que se hace ANTES.
|
|
|
|
|
|
>
|
|
|
|
|
|
> 1. **Leer la documentación.** No ojearla, no «la parte relevante»: leerla.
|
|
|
|
|
|
> 2. **No sacar NINGUNA conclusión precipitada sin haber leído TODA la
|
|
|
|
|
|
> documentación** del eje que se toca.
|
|
|
|
|
|
> 3. **Contrastar TODAS las decisiones contra la documentación ANTES de emitir
|
|
|
|
|
|
> conclusiones** — no después, no cuando alguien lo pregunte.
|
|
|
|
|
|
|
|
|
|
|
|
**Leer a medias es no leer.** En la sesión que originó este plan se leyeron 120
|
|
|
|
|
|
de las 290 líneas de `theming/channels.md` y se afirmó que la doctrina «no
|
|
|
|
|
|
cubría» algo que estaba escrito en la línea 180.
|
|
|
|
|
|
|
|
|
|
|
|
**Y antes de PROPONER algo, leer lo que ya hay sobre ello.** Los dos errores más
|
|
|
|
|
|
caros de esa sesión fueron proponer soluciones que el repo ya tenía resueltas o
|
|
|
|
|
|
descartadas: la separación `Sticky`/`Affix` llevaba escrita en el dossier desde
|
|
|
|
|
|
julio, y una fila del ledger se «re-descubrió» peor de lo que su propia ficha ya
|
|
|
|
|
|
la tenía analizada. Antes de escribir una propuesta: el dossier, la ficha del
|
|
|
|
|
|
hallazgo, y el README del componente vecino.
|
|
|
|
|
|
|
|
|
|
|
|
### Lo que hay que haber leído para ESTE plan
|
|
|
|
|
|
|
|
|
|
|
|
| Documento | Por qué |
|
|
|
|
|
|
| ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
|
|
|
|
| [`docs/building-a-component.md`](../building-a-component.md) | la puerta única; enlaza el doc de cada fase |
|
|
|
|
|
|
| [`guides/component-guide.md`](../guides/component-guide.md) §4–5 | compose-first y la clasificación componente / capa / átomo pasivo |
|
|
|
|
|
|
| [`guides/completion-checklist.md`](../guides/completion-checklist.md) | la matriz de aceptación (A-\* · E-\* · R-\* · D-\* · F-\*) |
|
|
|
|
|
|
| [`src/uix/eidos/components/README.md`](../../src/uix/eidos/components/README.md) | las 7 reglas duras de la forma canónica |
|
|
|
|
|
|
| **`src/uix/eidos/components/sticky/`** (README + `sticky.css` + su morfo) | es el VECINO: este componente existe porque `Sticky` no cubre el caso, y su recipe explica por qué usa inset físico |
|
|
|
|
|
|
| [`process/RESEARCH-blocks-references.md`](./RESEARCH-blocks-references.md) §sticky (F1.1) | el dossier ya separa `Affix` de `Sticky` y lista las brechas del segundo |
|
|
|
|
|
|
| [`theming/motion.md`](../theming/motion.md) | **sólo si se anima la entrada/salida**; entonces, entera |
|
|
|
|
|
|
|
|
|
|
|
|
⚠️ **Un gate de tipos NO verifica comportamiento.** `blocks:check` y
|
|
|
|
|
|
`svelte-check` dieron verde sobre un `edge="bottom"` que no hacía absolutamente
|
|
|
|
|
|
nada. Lo que verifica comportamiento está en §4.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 1. Por qué existe — la necesidad está medida, no supuesta
|
|
|
|
|
|
|
|
|
|
|
|
El 2026-08-10, trabajando el block `banner`, el usuario pidió que el aviso
|
|
|
|
|
|
pudiera fijarse arriba **o abajo** de la página. El intento de resolverlo
|
|
|
|
|
|
componiendo el `Sticky` del canon falló, y la medición dice exactamente por qué:
|
|
|
|
|
|
|
|
|
|
|
|
| Intento | Resultado medido (`/blocks/banner/preview`, ventana 700px) |
|
|
|
|
|
|
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
|
|
|
|
| `Sticky edge="top"` | ✅ `position: sticky`, `top: 0`, `data-stuck` presente tras desplazar |
|
|
|
|
|
|
| `Sticky edge="bottom"` | ❌ la tira acaba en `top: -8` y `data-stuck` **ausente** — el prop no hace nada |
|
|
|
|
|
|
| `Box position="fixed"` en el block | ⚠️ se ancla al borde inferior, pero **queda por debajo del cromo de la página**: `Box` expone `position` e `inset` y NO `zIndex`, y `style=` está prohibido por B-3 |
|
|
|
|
|
|
|
|
|
|
|
|
**La causa de la segunda fila no es un cableado mal puesto**: `position: sticky`
|
|
|
|
|
|
se despega en cuanto termina su contenedor, y un aviso vive al PRINCIPIO del
|
|
|
|
|
|
documento, así que la página se lo lleva. Un elemento anclado a un borde del
|
|
|
|
|
|
viewport es `position: fixed`, y eso es otro componente.
|
|
|
|
|
|
|
|
|
|
|
|
El tercer intento se revirtió entero (`git checkout`) — está registrado aquí
|
|
|
|
|
|
porque **el apaño no debe volver**: un `fixed` sin z-index gestionado ni portal
|
|
|
|
|
|
es una versión ad-hoc de este componente dentro de un block, y la regla de
|
|
|
|
|
|
composición del proyecto dice que un primitivo que falta **se señala y se
|
|
|
|
|
|
construye**, nunca se inlinea ([`guides/component-guide.md`](../guides/component-guide.md) §4).
|
|
|
|
|
|
|
|
|
|
|
|
### La separación ya estaba documentada
|
|
|
|
|
|
|
|
|
|
|
|
El dossier de referencias lo dejó escrito en julio, en la ficha de `sticky`
|
|
|
|
|
|
(F1.1), y nadie lo leyó a tiempo:
|
|
|
|
|
|
|
|
|
|
|
|
> **Mantine Affix NO es comparable (es un slot fixed en Portal)**
|
|
|
|
|
|
|
|
|
|
|
|
y, entre las brechas del propio `sticky`:
|
|
|
|
|
|
|
|
|
|
|
|
> **borde bottom = centinela DISTINTO** (`threshold:[1]`, por ratio) — es un
|
|
|
|
|
|
> centinela por borde con configs diferentes
|
|
|
|
|
|
|
|
|
|
|
|
Es decir: la referencia separa las dos piezas a propósito. Nosotros tenemos
|
|
|
|
|
|
`Sticky` y no tenemos la otra.
|
|
|
|
|
|
|
|
|
|
|
|
### Por qué NO se resolvió metiendo un modo `fixed` en `Sticky`
|
|
|
|
|
|
|
|
|
|
|
|
Se analizó (2026-08-10) y el impacto de código era pequeño —sólo dos
|
|
|
|
|
|
consumidores (`site-header` y el layout de `/uix`), cero eventos en su morfo—
|
|
|
|
|
|
pero el doctrinal no:
|
|
|
|
|
|
|
|
|
|
|
|
1. **Rompe una promesa escrita en su propio recipe**: `data-stuck` + `data-edge`
|
|
|
|
|
|
espejan `@container scroll-state(stuck: top|bottom)` «para que los selectores
|
|
|
|
|
|
traduzcan mecánicamente a la query nativa cuando aterrice». Un modo `fixed`
|
|
|
|
|
|
no tiene equivalente en esa query.
|
|
|
|
|
|
2. **Fusiona lo que la referencia separa** (punto anterior).
|
|
|
|
|
|
3. **`data-stuck` y el centinela pierden significado**: en `fixed` el elemento
|
|
|
|
|
|
nunca se pega ni se despega.
|
|
|
|
|
|
4. Desaparece el hueco en el layout, el ancho colapsa sin insets inline, y el
|
|
|
|
|
|
`--sticky-z-index` está calibrado para un header pegado, no para algo que
|
|
|
|
|
|
flota sobre todo el contenido.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 2. Fase 0 — las cuatro decisiones a FIRMAR
|
|
|
|
|
|
|
|
|
|
|
|
### D-AFX.1 · Membresía: eidos-native, no soma
|
|
|
|
|
|
|
|
|
|
|
|
**Propuesta**: morfo con `scope: ['eidos']` y **0 eventos**, como `mockup`
|
|
|
|
|
|
(`src/uix/morfo/components/mockup.ts` es el precedente vivo), más la sección
|
|
|
|
|
|
`## Passive justification` en el README que la regla F-1.5 exige.
|
|
|
|
|
|
|
|
|
|
|
|
**Motivo**: no tiene estado ni observa nada — es colocación. `Sticky` es soma
|
|
|
|
|
|
porque detecta el cruce con un IntersectionObserver; aquí no hay nada que
|
|
|
|
|
|
detectar. Si algún día debe anunciar que apareció, entonces sube a soma.
|
|
|
|
|
|
|
|
|
|
|
|
### D-AFX.2 · La superficie v1
|
|
|
|
|
|
|
|
|
|
|
|
**Propuesta**:
|
|
|
|
|
|
|
|
|
|
|
|
| Prop | Valores | Default | Para qué |
|
|
|
|
|
|
| -------- | -------------------------------------- | ----------- | -------------------------------------------- |
|
|
|
|
|
|
| `edge` | `top` \| `bottom` | `bottom` | el borde al que se ancla |
|
|
|
|
|
|
| `offset` | px | `0` | separación desde ese borde |
|
|
|
|
|
|
| `inline` | `full` \| `start` \| `center` \| `end` | `full` | cómo ocupa el eje inline |
|
|
|
|
|
|
| `z` | clave de la escala `--z-index-*` | por decidir | apilamiento, **tokenizado, nunca un número** |
|
|
|
|
|
|
| `portal` | boolean | `false` | ver D-AFX.3 |
|
|
|
|
|
|
|
|
|
|
|
|
Con eso, `banner` pide `edge="bottom" inline="full"` y un FAB pediría
|
|
|
|
|
|
`edge="bottom" inline="end"`. Mantine usa insets sueltos
|
|
|
|
|
|
(`position={{ bottom: 20, right: 20 }}`); esta superficie es más cerrada y
|
|
|
|
|
|
tokenizada, que es la línea del proyecto.
|
|
|
|
|
|
|
|
|
|
|
|
⚠️ **A decidir con ella**: si v1 cubre sólo el eje de bloque (`top`/`bottom`) o
|
|
|
|
|
|
también `left`/`right`. La necesidad medida sólo pide el primero.
|
|
|
|
|
|
|
|
|
|
|
|
### D-AFX.3 · El portal, opt-in y por defecto apagado
|
|
|
|
|
|
|
|
|
|
|
|
**Propuesta**: `portal={false}` por defecto, componiendo el `Portal` que ya
|
|
|
|
|
|
existe (`$soma/components/internal`, exportado con `PortalProps` /
|
|
|
|
|
|
`PortalTarget`).
|
|
|
|
|
|
|
|
|
|
|
|
**Motivo**: sólo hace falta para escapar de un ancestro con
|
|
|
|
|
|
`transform` / `filter` / `contain`, que es raro — pero cuando pasa, mata el
|
|
|
|
|
|
`fixed` **en silencio**, y ésa es justo la clase de fallo que debe tener una
|
|
|
|
|
|
salida documentada.
|
|
|
|
|
|
|
|
|
|
|
|
### D-AFX.4 · El nombre
|
|
|
|
|
|
|
|
|
|
|
|
**Propuesta**: `Affix`, que es el de la referencia que el dossier ya cita.
|
|
|
|
|
|
Alternativas: `Pinned`, `Fixed`. ⚠️ Ojo: **AntD llama `Affix` a otra cosa**
|
|
|
|
|
|
(un sticky con `target`), así que la comparativa del README tiene que
|
|
|
|
|
|
desambiguarlo explícitamente o el nombre confunde.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 3. Fases 1–8 (la ruta canónica)
|
|
|
|
|
|
|
|
|
|
|
|
Leer primero [`docs/building-a-component.md`](../building-a-component.md) — es
|
|
|
|
|
|
la puerta única y enlaza el documento de cada fase.
|
|
|
|
|
|
|
|
|
|
|
|
| Fase | Produce | Guard |
|
|
|
|
|
|
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
|
|
|
|
|
|
| **1 · Morfo** | `morfo/components/affix.ts` — una parte (`Provider`), `data-affix` + `data-edge` + `data-inline`. Cero eventos, justificados en la cabecera | `validateMorfo` · `morfo:vocabulary` |
|
|
|
|
|
|
| **2 · Soma** | **no aplica** si se firma D-AFX.1 | — |
|
|
|
|
|
|
| **3 · Sema** | `expression: 'family-default'` explícito — no hay ocurrencia que expresar | `morfo:vocabulary` |
|
|
|
|
|
|
| **4 · Eidos** | `eidos/components/affix/` — `affix.svelte` + `types.ts` + `index.ts`; compone `Portal` cuando se pide | `component-api-contract` · audit `E-*` |
|
|
|
|
|
|
| **5 · Recipe** | `affix.css` — `position: fixed`, inset **lógico** en el eje inline (aquí no hay observer que obligue a lo físico, al revés que en `Sticky`, cuyo recipe explica por qué él sí) y `z-index` desde la escala tokenizada | `generate:eidos-css` · `recipe-css-contract` · audit `R-*` |
|
|
|
|
|
|
| **6 · Demo** | `web/routes/uix/components/affix/+page.svelte`, plantilla v2 de 9 pestañas. **Con recorrido de scroll real** — la preview del banner tenía 16px y por eso no se veía nada | audit `D-*` · `smoke` |
|
|
|
|
|
|
| **7 · README** | Baseline · Comparativa ≥3 refs · Decisiones · Gaps · `## Passive justification` · **y la sección que más falta: `Affix` vs `Sticky`, cuándo cada uno** | audit `F-*` |
|
|
|
|
|
|
| **8 · Aceptación** | nada nuevo: pasa | `component:audit --only affix` · `morfo:check` · `eidos-lint` · `rtl:check` |
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 4. Verificación específica (además de los guards)
|
|
|
|
|
|
|
|
|
|
|
|
Lo que falló en el intento del banner, para que no se repita:
|
|
|
|
|
|
|
|
|
|
|
|
1. **Scroll largo real** en los dos bordes, midiendo la posición en tres puntos.
|
|
|
|
|
|
Un gate de tipos NO verifica comportamiento: `blocks:check` y `svelte-check`
|
|
|
|
|
|
dieron verde sobre un `edge="bottom"` que no hacía nada.
|
|
|
|
|
|
2. **Z-index computado del elemento frente al del cromo de la página** — el
|
|
|
|
|
|
fallo exacto que el usuario vio en pantalla.
|
|
|
|
|
|
3. **Captura de los dos bordes, y MIRARLA** antes de decir que está.
|
|
|
|
|
|
4. **Ancestro con `transform`** en la demo: comprobar que sin `portal` falla y
|
|
|
|
|
|
con `portal` funciona, que es la razón de ser del prop.
|
|
|
|
|
|
5. Claro/oscuro y RTL (`inline="start"`/`"end"` deben espejar).
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 5. Qué desbloquea
|
|
|
|
|
|
|
|
|
|
|
|
- **`banner`** recupera `sticky` / `offset` / `edge` componiendo `Affix` para
|
|
|
|
|
|
`bottom` y `Sticky` para `top` — diez minutos cuando la pieza exista. La
|
|
|
|
|
|
necesidad y el precedente están en el ledger del tier
|
|
|
|
|
|
(`AUDIT-blocks-ledger.md`), donde `site-header` ya expone esa costura.
|
|
|
|
|
|
- **La categoría «cookie consent»** del dossier, que ≥2 referencias shippean y
|
|
|
|
|
|
el tier no puede hoy.
|
|
|
|
|
|
- El gap de `Box` sin `zIndex` deja de ser urgente: quien necesite fijar algo
|
|
|
|
|
|
compone `Affix` en vez de pelearse con el primitivo de layout.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 6. Lo que se construyó realmente — delta vs este plan
|
|
|
|
|
|
|
|
|
|
|
|
Ejecutado el 2026-08-14. Las cuatro decisiones se firmaron una a una; tres
|
|
|
|
|
|
cambiaron de forma durante la firma, y dos cosas más se descubrieron MIDIENDO,
|
|
|
|
|
|
no razonando. Esto es el registro, no un resumen.
|
|
|
|
|
|
|
|
|
|
|
|
### El hallazgo que reencuadró todo
|
|
|
|
|
|
|
|
|
|
|
|
**Este plan no cita `Fab` ni `MenuDial`, y las dos ya fijaban al viewport.**
|
|
|
|
|
|
`fab.css` tiene 4 esquinas + `static`; `menu-dial.css` tiene **9 zonas**, insets
|
|
|
|
|
|
lógicos, `env(safe-area-inset-*)` y centrado por márgenes auto. Y la pieza ya
|
|
|
|
|
|
había existido: el morfo de `float` dice _«NOT the legacy `air/Float` 9-zone
|
|
|
|
|
|
external placement primitive»_ y el README de `Fab` que _«the old
|
|
|
|
|
|
`air/layout/float` … was removed in the refactor»_. La capacidad no desapareció:
|
|
|
|
|
|
se re-inlineó dos veces, divergiendo (`--fab-offset` público vs
|
|
|
|
|
|
`--_menu-dial-offset` interno; `var(--fab-z)` vs `var(--z-index-sticky, 1100)`,
|
|
|
|
|
|
un fallback de 1100 sobre un token que vale 100).
|
|
|
|
|
|
|
|
|
|
|
|
Por eso `Affix` **no** es un tercer ejemplar: es una **capa compartida**.
|
|
|
|
|
|
|
|
|
|
|
|
### Las cuatro decisiones, como quedaron firmadas
|
|
|
|
|
|
|
|
|
|
|
|
| Decisión | Lo que proponía §2 | Lo firmado |
|
|
|
|
|
|
| ---------------------- | -------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
|
|
|
|
| **D-AFX.1** membresía | eidos-native, 0 eventos, precedente `mockup` | **igual**, con el precedente cambiado a `Fab` (mismo scope, 0 eventos, ya `fixed`) y añadiendo al README la sección `## Audit exceptions` (E-2.2 · D-3.1 · R-1.1), que el plan no preveía |
|
|
|
|
|
|
| **D-AFX.2** superficie | props `edge` · `offset` · `inline` · `z` · `portal`, con el componente como único consumidor | **rehecha**: capa compartida `affix.css` enganchada en `data-affix-placement` + componente público. Superficie final: `placement` (9 zonas) · `stretch` · `offset`. El mecanismo es el de `lib/list-surface.css`, NO anidar componentes — envolver `Fab`/`MenuDial` metería un nodo y separaría el `fixed` del elemento sobre el que están calibrados |
|
|
|
|
|
|
| **D-AFX.3** portal | `portal={false}` opt-in | **descartado en v1** (CLAUDE.md §2, sin caso real). Registrado en Gaps con su motivo; la demo exhibe el fallo en vivo con el control «transformed ancestor» |
|
|
|
|
|
|
| **D-AFX.4** nombre | `Affix` | **igual**. Verificado que `Float` y `Pin` están ocupados en el árbol, y que «affix» ya existe como palabra del framework — pero sólo en prosa y en UN token (`--field-affix-color`); el contrato DOM del campo es `data-field-prefix`/`-suffix`. Colisión documental, no mecánica |
|
|
|
|
|
|
|
|
|
|
|
|
### Lo que este plan NO podía prever, y salió midiendo
|
|
|
|
|
|
|
|
|
|
|
|
1. **La z: peldaño nuevo en el canon.** Con la tira en `top`, `--affix-z` empataba
|
|
|
|
|
|
con `--sticky-z-index` (ambos 100) y el empate lo rompe el ORDEN DEL DOM — un
|
|
|
|
|
|
aviso va antes que la cabecera en el fuente, así que perdía: **30,7px de
|
|
|
|
|
|
solape, cabecera encima**, o sea el fallo del §1 reproducido por su propio
|
|
|
|
|
|
sustituto. Firmado `--z-index-affix: 150` (cromo fijado al viewport ≠ cromo
|
|
|
|
|
|
sticky a su contenedor). ⚠️ Añadir un peldaño toca **dos** sitios:
|
|
|
|
|
|
`STATIC_Z_INDEX` y la lista cerrada `Z_INDEX_KEYS` — sólo el valor tira 34
|
|
|
|
|
|
tests con `unknown token key`.
|
|
|
|
|
|
2. **`Affix` NO compone `<Box>`**, aunque sea el patrón de todos los primitivos
|
|
|
|
|
|
de layout: `box.css` declara `position: var(--box-position, revert-layer)` a
|
|
|
|
|
|
la MISMA especificidad (0,1,0) que el gancho de la capa, y eidos no usa
|
|
|
|
|
|
`@layer`, así que `revert-layer` se comporta como `revert`. En un orden de
|
|
|
|
|
|
carga donde `box.css` llegue después, el affix computaría `position: static`
|
|
|
|
|
|
— un `fixed` que no hace nada, justo lo que este componente viene a terminar.
|
|
|
|
|
|
3. **`stretch` es sólo de los bordes de bloque.** La versión simétrica (raíl a
|
|
|
|
|
|
toda la altura en `left`/`right`) se envió y se retiró el mismo día: `Affix`
|
|
|
|
|
|
no dimensiona a su hijo, así que el raíl era una caja invisible de 380px con
|
|
|
|
|
|
un hijo de 47px arriba — **idéntico a `top-start` en pantalla**. El marco
|
|
|
|
|
|
medía bien; la pintura no. Registrado en Gaps.
|
|
|
|
|
|
4. **Un default de demo puede matar el control principal.** Con `stretch` ON,
|
|
|
|
|
|
`top-start`/`top-center`/`top-end` son la misma tira: seis de las nueve zonas
|
|
|
|
|
|
dejaban de cambiar nada (D-7.1). La demo arranca con `stretch` OFF y hijo
|
|
|
|
|
|
compacto.
|
|
|
|
|
|
|
|
|
|
|
|
### El §4 de este plan, ejecutado
|
|
|
|
|
|
|
|
|
|
|
|
| Verificación pedida | Resultado |
|
|
|
|
|
|
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ |
|
|
|
|
|
|
| Scroll largo real, tres puntos, dos bordes | ✅ 342px en la demo · `bottom-center` y `top-center` con desplazamiento **0** en los tres puntos, ancho = ancho del host |
|
|
|
|
|
|
| Z computada frente al cromo | ✅ affix 150 vs cabecera pegajosa 100 · `elementFromPoint` sobre la tira devuelve el aviso |
|
|
|
|
|
|
| Captura de los dos bordes, y MIRARLA | ⚠️ el agente no pudo (panel Browser sin componer frames); **comprobado por el autor** |
|
|
|
|
|
|
| Ancestro con `transform` | ✅ deriva −342px sobre 342px de scroll — se va del todo pese al `fixed`. En la demo como control |
|
|
|
|
|
|
| Claro/oscuro y RTL | ✅ `top-start` pasa de `left: 0` a `right: 0` en RTL; `env()` remapeado por `:dir(rtl)` a ranuras lógicas |
|
|
|
|
|
|
|
|
|
|
|
|
Y una trampa que el §4 no pedía: el primer harness de la demo puso `contain:
|
|
|
|
|
|
layout` y `overflow: auto` en el MISMO elemento, con lo que el bloque contenedor
|
|
|
|
|
|
del `fixed` ERA el scroller y la tira se iba con el contenido (0 → 171 → 342).
|
|
|
|
|
|
Separado en dos elementos.
|
|
|
|
|
|
|
|
|
|
|
|
### Lo que quedó abierto
|
|
|
|
|
|
|
|
|
|
|
|
- **Migrar `Fab` y `MenuDial`** a la capa (el autor lo acotó fuera: «adapta sólo
|
|
|
|
|
|
banner momentáneamente»). Ahí caen también su defecto RTL de safe-area inline
|
|
|
|
|
|
(emparejan `inset-inline-start` con `env(safe-area-inset-left)`, que despeja la
|
|
|
|
|
|
muesca equivocada en RTL apaisado) y el fallback `1100` de `menu-dial`.
|
|
|
|
|
|
- **`banner`**: envía `affix="top" | "bottom"` + `affixOffset`. Su README tenía
|
|
|
|
|
|
la disposición contraria («app-land, el canon ya trae `Sticky`») y se corrigió:
|
|
|
|
|
|
nombraba un componente que NO puede hacerlo.
|