uix(box): ocultar por CSS descarga el vídeo igual, y evitarlo pedía no tener SSR
`visibleFrom` / `hiddenFrom` no ocultan: no renderizan. El subárbol no está en
el DOM, sus efectos no corren y su media no se pide — la mitad que
`display={{ base: 'none' }}` no puede dar. Los once contenedores que componen
Box (Section, Container, Flex, Grid, Stack, Group, Wrap, AutoGrid, Surface,
AspectRatio, Float) la heredan sin tocar sus ficheros. La puerta es un
`$derived` sobre `eidos.dom.isAtLeast(bp)` y un `{#if}`: sin atributos nuevos,
sin generador, sobre los breakpoints que configura el app.
Ninguna referencia lo hace así. Mantine (`hiddenFrom`/`visibleFrom`), Chakra y
Panda (`hideFrom`/`hideBelow`), Framer y Webflow ocultan por CSS y dejan el DOM
montado — Webflow lo documenta como coste de ancho de banda y Framer llega a
duplicar secciones enteras por breakpoint. Eligieron CSS porque con SSR una
puerta JS pinta la variante equivocada en el HTML (MUI devuelve un `matches` por
defecto en el primer montaje; Vuetify #17252 reporta los saltos de layout), y el
precio de evitarlo es adivinar el ancho por User-Agent o Client Hints.
Aquí ese precio no existe: el proyecto se despliega como SPA (`adapter-static` +
`fallback`, ninguna ruta con `prerender = true`), así que el primer render
ocurre en el navegador con el `innerWidth` real — el build es un `200.html` de
1,8 KB, shell puro. El que SÍ se paga está escrito en el tipo: cruzar un
breakpoint destruye el subárbol y su estado. Cuando el estado deba sobrevivir,
`display`. Sólo el dev server renderiza en servidor (ancho 0) y empieza a
descargar la media de la variante `base` antes de que la hidratación la pode:
queda como caveat de desarrollo en el README.
`base` sale del tipo en ambas props: `visibleFrom="base"` sería «siempre» y
`hiddenFrom="base"` «nunca renderiza», y ninguna de las dos es una cosa que una
prop deba poder decir.
Card, Banner, ScrollArea y Sticky rinden su propia raíz y NO heredan la puerta:
queda como gap diferido con la regla del `as` (≥2 casos reales). El puente de
SSR queda escrito con su disparador, no construido — hoy sería un atributo que
nadie lee.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
# CONTINUE — visibilidad por breakpoint (`visibleFrom` / `hiddenFrom`) (handoff, act. 2026-08-19)
**Estado: F1– F3 HECHAS, AUDITADAS, VERIFICADAS en Chrome real y COMMITEADAS**
(2026-08-19, `uix(box): ocultar por CSS descarga el vídeo igual…` ). La auditoría
de cierre encontró tres desviaciones menores, las tres corregidas antes del
commit (ver §Auditoría). Nada bloquea el uso.
**Por dónde entrar: [§Qué queda, por orden ](#qué-queda-por-orden ).** Lo único
pendiente es F4 (rollout al hero), y sólo si el autor lo pide.
## Qué es
Dos props en `Box` que **NO RENDERIZAN** el elemento fuera de su rango de
breakpoints — el subárbol no está en el DOM, sus efectos no corren y su media
no se pide. Es la mitad que `display={{ base: 'none' }}` no puede dar (oculta,
pero monta). Por herencia la ganan los once contenedores que componen `Box`
(Section, Container, Flex, Grid, Stack, Group, Wrap, AutoGrid, Surface,
AspectRatio, Float) sin tocar sus ficheros.
- `visibleFrom="md"` → no existe por debajo de `md` (desktop-only).
- `hiddenFrom="md"` → no existe desde `md` (mobile-only).
- Ambos → rango contiguo. `base` excluido del tipo a propósito.
La puerta es `eidos.dom.isAtLeast(bp)` —el predicado que `ActiveDom` ya tenía,
sobre los breakpoints que configura el app— en un `$derived` y un `{#if}`
alrededor de la raíz de `box.svelte` . Sin atributos nuevos, sin generador.
**Fuente viva de las decisiones**: `C:\Users\dev\.claude\plans\elegant-weaving-lark.md`
(D-V.1…4 firmadas 2026-08-18, con el estudio de cómo lo hacen Mantine, Chakra,
MUI, Vuetify, Framer y Webflow — todos por CSS o pagando flash/adivinando el
ancho). Su contenido de decisiones debería moverse a un lugar del repo si el eje
sigue creciendo (hoy vive en el README de Box y en `docs/architecture/eidos.md` ).
## Auditoría de cierre (2026-08-19) — hallazgos y correcciones
| # | Hallazgo | Corrección |
| --- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1 | El tipo aceptaba `base` : `visibleFrom="base"` = «siempre», `hiddenFrom="base"` = «nunca renderiza» — y la demo (correctamente) no lo ofrecía en los chips: paridad rota entre tipo y demo | `Exclude<Breakpoint, 'base'>` en ambas props, con el porqué en el JSDoc. El mismo `Exclude` bajó a la demo (`$state` + chips) el 2026-08-19: sin él `npm run check` daba 2 errores en `box/` . **HECHO** |
| 2 | README decía «los doce contenedores que componen Box» y listaba once (el doceavo es Box) | «once… — doce con Box». **HECHO** |
| 3 | El plan pedía en Gaps la exclusión de Card / Banner / ScrollArea / Sticky (raíz propia, no heredan) y faltaba | Fila añadida con la regla del `as` (≥2 casos reales). **HECHO** |
| 4 | Dos afirmaciones del informe no se habían LEÍDO en consola: «Svelte repara el `{#if}` sin aviso» y «sin `effect_update_depth_exceeded` al cruzar con capas que se registran» | Medido en Chrome con consola capturada desde la carga: **cero** avisos de hidratación, **cero** bucles en 3 ciclos con un `Background.Gradient animate` dentro; `Stack → Flex → Box` hereda. **CONFIRMADO** |
| 5 | La nota documental fue a `docs/architecture/eidos.md` y no a `theming/reference.md` como decía el plan | `theming/reference.md` no tiene sección responsive; el párrafo del «puente responsive» vive en `eidos.md` §tokens. Desvío justificado, no error |
Verificado (Chrome real, 2026-08-18/19): a 2133px conviven `visibleFrom="md"`
presente, `hiddenFrom="md"` ausente y rango `sm– lg` ausente · al cerrar la puerta
en vivo desaparecen sección, `<video>` e `<input>` del DOM; al reabrir el input
vuelve **vacío** (la renuncia D-V.4, medida) · el par cambia con un `resize` ·
`npm run build` → `200.html` de 1.815 bytes, shell puro: **en producción no hay
SSR** que pueda pintar mal · en DEV sí: el dev server sirve la variante `base` y
pide su media antes de que la hidratación la pode (documentado en el README de
Box como caveat de desarrollo).
## Qué queda, por orden
> **Commit hecho el 2026-08-19** con las seis rutas del eje
> (`box/{box.svelte,types.ts,README.md}`, la demo, `eidos.md`, este handoff) y
> `git add` por ruta, nunca `-A`: otra sesión editaba `app-shell` en paralelo.
> Gates en verde ese día: `check` sin nada en `box/`, `blocks:check` 0,
> `morfo:check` PASS box, `docs:check` 0, `npx vitest run src/uix/eidos` 434/435
> (el fallo es `skin-media-player`, ajeno), prettier OK — salvo `eidos.md`, que
> **ya fallaba en HEAD** en tablas ajenas al eje y por eso no se reformatea.
blocks(hero): la mitad de escritorio no se oculta en el móvil, no existe — y el clip no se pide
F4 del eje de visibilidad por breakpoint: el primer caso real, y la receta
escrita donde un agente la va a buscar — el README del block que la usa.
Medido sobre build de PRODUCCIÓN (`build` + `vite preview`, Chrome), que es el
único sitio donde la afirmación es comprobable: a 375, cero `<video>` en el DOM
y NINGUNA petición de `video.mp4`; a 1280, uno y una (`206 Partial Content`).
En dev es falsa por construcción — el servidor renderiza a ancho 0 y sirve la
variante `base` antes de que la hidratación la pode.
Dos criterios que la receta fija para el tier. La puerta va DENTRO del snippet,
nunca alrededor del `<Hero>`: envolver el block duplicaría título, copy,
formulario y acciones por breakpoint, que es lo que hacen las secciones
por-breakpoint de Framer y la razón de que deriven; la media es lo único que
difiere, así que es lo único escrito dos veces. Y `display="contents"`: una
puerta decide presencia, no añade geometría.
La mitad móvil no es un vídeo más ligero: es NINGÚN vídeo. El Surface con
aurora cuesta cero bytes y sigue la paleta del tema.
⚠️ Hallazgo del canon, medido de paso y NO arreglado aquí: la media dentro de
un `Background.Layer` desborda la capa — el mínimo automático del ítem de
rejilla gana a `block-size: 100%`. Hero a 1280: capa 567px, vídeo 714px. En la
demo del propio canon con `Background.Video`: capa 288px, vídeo 435px.
`min-block-size: 0` lo colapsa exacto en ambos. `overflow: clip` lo escondía,
así que `cover` lleva recortando descentrado sin que se viera. Es del eje
`background`, cerrado el 2026-08-18: queda escrito en §Found while composing.
`launch.json` gana `preview-prod`: la entrada llamada `preview` arranca
`npm run dev`, así que no servía el build y la medida no se podía hacer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
> **F4 HECHA el 2026-08-19.** La receta vive donde se usa — `hero/README.md`
> §«The responsive backdrop» — y el vehículo es el backdrop de `HeroSite`. La
> puerta va DENTRO del snippet, nunca alrededor del `<Hero>`: envolver el block
> duplicaría título, copy, formulario y acciones por breakpoint (lo que hace
> Framer). `display="contents"` en las dos Box: una puerta decide presencia, no
> añade geometría. **Medido sobre build de PRODUCCIón** (`npm run build` +
> `vite preview`, Chrome): a **375** → 0 `<video>` en el DOM y **ninguna**
> petición de `video.mp4`; a **1280** → 1 `<video>` y **una** peticion
> (`206 Partial Content`). La afirmación central del eje deja de estar medida
> sólo en dev, donde es falsa.
>
> ⚠️ **Hallazgo ajeno, medido de paso**: la media dentro de un
> `Background.Layer` DESBORDA la capa — el mínimo automático del ítem de rejilla
> gana a `block-size: 100%`. Hero a 1280: capa 567px, `<video>` **714px**; y en
> la demo del propio canon con `<Background.Video>`: capa 288px, vídeo **435px**.
> `min-block-size: 0` lo colapsa exacto en ambos. `overflow: clip` lo esconde, y
> por eso nunca se vio. Es del eje `background` (cerrado el 2026-08-18), escrito
> en `hero/README.md` §Found while composing — marcado, no arreglado.
1. **Si algún día hay SSR/prerender** — el puente C está escrito en el README de
uix(box): ocultar por CSS descarga el vídeo igual, y evitarlo pedía no tener SSR
`visibleFrom` / `hiddenFrom` no ocultan: no renderizan. El subárbol no está en
el DOM, sus efectos no corren y su media no se pide — la mitad que
`display={{ base: 'none' }}` no puede dar. Los once contenedores que componen
Box (Section, Container, Flex, Grid, Stack, Group, Wrap, AutoGrid, Surface,
AspectRatio, Float) la heredan sin tocar sus ficheros. La puerta es un
`$derived` sobre `eidos.dom.isAtLeast(bp)` y un `{#if}`: sin atributos nuevos,
sin generador, sobre los breakpoints que configura el app.
Ninguna referencia lo hace así. Mantine (`hiddenFrom`/`visibleFrom`), Chakra y
Panda (`hideFrom`/`hideBelow`), Framer y Webflow ocultan por CSS y dejan el DOM
montado — Webflow lo documenta como coste de ancho de banda y Framer llega a
duplicar secciones enteras por breakpoint. Eligieron CSS porque con SSR una
puerta JS pinta la variante equivocada en el HTML (MUI devuelve un `matches` por
defecto en el primer montaje; Vuetify #17252 reporta los saltos de layout), y el
precio de evitarlo es adivinar el ancho por User-Agent o Client Hints.
Aquí ese precio no existe: el proyecto se despliega como SPA (`adapter-static` +
`fallback`, ninguna ruta con `prerender = true`), así que el primer render
ocurre en el navegador con el `innerWidth` real — el build es un `200.html` de
1,8 KB, shell puro. El que SÍ se paga está escrito en el tipo: cruzar un
breakpoint destruye el subárbol y su estado. Cuando el estado deba sobrevivir,
`display`. Sólo el dev server renderiza en servidor (ancho 0) y empieza a
descargar la media de la variante `base` antes de que la hidratación la pode:
queda como caveat de desarrollo en el README.
`base` sale del tipo en ambas props: `visibleFrom="base"` sería «siempre» y
`hiddenFrom="base"` «nunca renderiza», y ninguna de las dos es una cosa que una
prop deba poder decir.
Card, Banner, ScrollArea y Sticky rinden su propia raíz y NO heredan la puerta:
queda como gap diferido con la regla del `as` (≥2 casos reales). El puente de
SSR queda escrito con su disparador, no construido — hoy sería un atributo que
nadie lee.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
Box §Gaps con su disparador: `data-visible-from` /`data-hidden-from`, dos
reglas `@media` por breakpoint en `render-css.ts` (junto a las de
tipografía), y la puerta `hidratado ? resuelto : true` . Hoy sería un atributo
que nadie lee — no construir sin el disparador.
blocks(hero): la mitad de escritorio no se oculta en el móvil, no existe — y el clip no se pide
F4 del eje de visibilidad por breakpoint: el primer caso real, y la receta
escrita donde un agente la va a buscar — el README del block que la usa.
Medido sobre build de PRODUCCIÓN (`build` + `vite preview`, Chrome), que es el
único sitio donde la afirmación es comprobable: a 375, cero `<video>` en el DOM
y NINGUNA petición de `video.mp4`; a 1280, uno y una (`206 Partial Content`).
En dev es falsa por construcción — el servidor renderiza a ancho 0 y sirve la
variante `base` antes de que la hidratación la pode.
Dos criterios que la receta fija para el tier. La puerta va DENTRO del snippet,
nunca alrededor del `<Hero>`: envolver el block duplicaría título, copy,
formulario y acciones por breakpoint, que es lo que hacen las secciones
por-breakpoint de Framer y la razón de que deriven; la media es lo único que
difiere, así que es lo único escrito dos veces. Y `display="contents"`: una
puerta decide presencia, no añade geometría.
La mitad móvil no es un vídeo más ligero: es NINGÚN vídeo. El Surface con
aurora cuesta cero bytes y sigue la paleta del tema.
⚠️ Hallazgo del canon, medido de paso y NO arreglado aquí: la media dentro de
un `Background.Layer` desborda la capa — el mínimo automático del ítem de
rejilla gana a `block-size: 100%`. Hero a 1280: capa 567px, vídeo 714px. En la
demo del propio canon con `Background.Video`: capa 288px, vídeo 435px.
`min-block-size: 0` lo colapsa exacto en ambos. `overflow: clip` lo escondía,
así que `cover` lleva recortando descentrado sin que se viera. Es del eje
`background`, cerrado el 2026-08-18: queda escrito en §Found while composing.
`launch.json` gana `preview-prod`: la entrada llamada `preview` arranca
`npm run dev`, así que no servía el build y la medida no se podía hacer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
2. **Card, Banner, ScrollArea, Sticky** — no heredan (raíz propia). Regla del
uix(box): ocultar por CSS descarga el vídeo igual, y evitarlo pedía no tener SSR
`visibleFrom` / `hiddenFrom` no ocultan: no renderizan. El subárbol no está en
el DOM, sus efectos no corren y su media no se pide — la mitad que
`display={{ base: 'none' }}` no puede dar. Los once contenedores que componen
Box (Section, Container, Flex, Grid, Stack, Group, Wrap, AutoGrid, Surface,
AspectRatio, Float) la heredan sin tocar sus ficheros. La puerta es un
`$derived` sobre `eidos.dom.isAtLeast(bp)` y un `{#if}`: sin atributos nuevos,
sin generador, sobre los breakpoints que configura el app.
Ninguna referencia lo hace así. Mantine (`hiddenFrom`/`visibleFrom`), Chakra y
Panda (`hideFrom`/`hideBelow`), Framer y Webflow ocultan por CSS y dejan el DOM
montado — Webflow lo documenta como coste de ancho de banda y Framer llega a
duplicar secciones enteras por breakpoint. Eligieron CSS porque con SSR una
puerta JS pinta la variante equivocada en el HTML (MUI devuelve un `matches` por
defecto en el primer montaje; Vuetify #17252 reporta los saltos de layout), y el
precio de evitarlo es adivinar el ancho por User-Agent o Client Hints.
Aquí ese precio no existe: el proyecto se despliega como SPA (`adapter-static` +
`fallback`, ninguna ruta con `prerender = true`), así que el primer render
ocurre en el navegador con el `innerWidth` real — el build es un `200.html` de
1,8 KB, shell puro. El que SÍ se paga está escrito en el tipo: cruzar un
breakpoint destruye el subárbol y su estado. Cuando el estado deba sobrevivir,
`display`. Sólo el dev server renderiza en servidor (ancho 0) y empieza a
descargar la media de la variante `base` antes de que la hidratación la pode:
queda como caveat de desarrollo en el README.
`base` sale del tipo en ambas props: `visibleFrom="base"` sería «siempre» y
`hiddenFrom="base"` «nunca renderiza», y ninguna de las dos es una cosa que una
prop deba poder decir.
Card, Banner, ScrollArea y Sticky rinden su propia raíz y NO heredan la puerta:
queda como gap diferido con la regla del `as` (≥2 casos reales). El puente de
SSR queda escrito con su disparador, no construido — hoy sería un atributo que
nadie lee.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
`as` : a mano cuando aparezcan ≥2 casos reales; mientras, envolver en `<Box>` .
## Lo que este eje enseñó
- **Pregunta «¿y si no tenemos SSR?» antes de diseñar el puente.** Iba a
construir la híbrida (CSS hasta hidratar + desmontar después) por «framework de
referencia»; el repo respondió: `adapter-static` + `fallback` sin ninguna ruta
`prerender = true` = SPA puro. La mitad cara del diseño era para un despliegue
que no existe.
- **El panel oculto miente para esto también**: `innerWidth` 0 ⇒ todo resuelve a
`base` . La verificación exige Chrome visible (`claude-in-chrome`), y aun ahí
una ventana maximizada no admite `resize` — cruza el umbral por el breakpoint
(`hiddenFrom` con un botón), que recorre el mismo `$derived` .
- **Una sonda que no puede cerrar la puerta no prueba nada**: a 2133px
`visibleFrom="xxl"` sigue abierta (1536 < 2133 ). Elegir el eje que s í cruza .
## Mapa de ficheros
| Qué | Dónde |
| ---------------------------------------- | --------------------------------------------------------------- |
| La puerta | `src/uix/eidos/components/box/box.svelte` (`visible` + `{#if}` ) |
| El tipo y su JSDoc | `src/uix/eidos/components/box/types.ts` |
| Decisiones, medidas, caveat de dev, gaps | `src/uix/eidos/components/box/README.md` |
| La demo (chips + snippet en paridad) | `web/routes/uix/components/box/+page.svelte` |
blocks(hero): la mitad de escritorio no se oculta en el móvil, no existe — y el clip no se pide
F4 del eje de visibilidad por breakpoint: el primer caso real, y la receta
escrita donde un agente la va a buscar — el README del block que la usa.
Medido sobre build de PRODUCCIÓN (`build` + `vite preview`, Chrome), que es el
único sitio donde la afirmación es comprobable: a 375, cero `<video>` en el DOM
y NINGUNA petición de `video.mp4`; a 1280, uno y una (`206 Partial Content`).
En dev es falsa por construcción — el servidor renderiza a ancho 0 y sirve la
variante `base` antes de que la hidratación la pode.
Dos criterios que la receta fija para el tier. La puerta va DENTRO del snippet,
nunca alrededor del `<Hero>`: envolver el block duplicaría título, copy,
formulario y acciones por breakpoint, que es lo que hacen las secciones
por-breakpoint de Framer y la razón de que deriven; la media es lo único que
difiere, así que es lo único escrito dos veces. Y `display="contents"`: una
puerta decide presencia, no añade geometría.
La mitad móvil no es un vídeo más ligero: es NINGÚN vídeo. El Surface con
aurora cuesta cero bytes y sigue la paleta del tema.
⚠️ Hallazgo del canon, medido de paso y NO arreglado aquí: la media dentro de
un `Background.Layer` desborda la capa — el mínimo automático del ítem de
rejilla gana a `block-size: 100%`. Hero a 1280: capa 567px, vídeo 714px. En la
demo del propio canon con `Background.Video`: capa 288px, vídeo 435px.
`min-block-size: 0` lo colapsa exacto en ambos. `overflow: clip` lo escondía,
así que `cover` lleva recortando descentrado sin que se viera. Es del eje
`background`, cerrado el 2026-08-18: queda escrito en §Found while composing.
`launch.json` gana `preview-prod`: la entrada llamada `preview` arranca
`npm run dev`, así que no servía el build y la medida no se podía hacer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| La receta (caso real, con cifras) | `src/uix/blocks/hero/README.md` §«The responsive backdrop» |
| El caso real | `web/routes/blocks/hero/HeroSite.svelte` (snippet `backdrop` ) |
uix(box): ocultar por CSS descarga el vídeo igual, y evitarlo pedía no tener SSR
`visibleFrom` / `hiddenFrom` no ocultan: no renderizan. El subárbol no está en
el DOM, sus efectos no corren y su media no se pide — la mitad que
`display={{ base: 'none' }}` no puede dar. Los once contenedores que componen
Box (Section, Container, Flex, Grid, Stack, Group, Wrap, AutoGrid, Surface,
AspectRatio, Float) la heredan sin tocar sus ficheros. La puerta es un
`$derived` sobre `eidos.dom.isAtLeast(bp)` y un `{#if}`: sin atributos nuevos,
sin generador, sobre los breakpoints que configura el app.
Ninguna referencia lo hace así. Mantine (`hiddenFrom`/`visibleFrom`), Chakra y
Panda (`hideFrom`/`hideBelow`), Framer y Webflow ocultan por CSS y dejan el DOM
montado — Webflow lo documenta como coste de ancho de banda y Framer llega a
duplicar secciones enteras por breakpoint. Eligieron CSS porque con SSR una
puerta JS pinta la variante equivocada en el HTML (MUI devuelve un `matches` por
defecto en el primer montaje; Vuetify #17252 reporta los saltos de layout), y el
precio de evitarlo es adivinar el ancho por User-Agent o Client Hints.
Aquí ese precio no existe: el proyecto se despliega como SPA (`adapter-static` +
`fallback`, ninguna ruta con `prerender = true`), así que el primer render
ocurre en el navegador con el `innerWidth` real — el build es un `200.html` de
1,8 KB, shell puro. El que SÍ se paga está escrito en el tipo: cruzar un
breakpoint destruye el subárbol y su estado. Cuando el estado deba sobrevivir,
`display`. Sólo el dev server renderiza en servidor (ancho 0) y empieza a
descargar la media de la variante `base` antes de que la hidratación la pode:
queda como caveat de desarrollo en el README.
`base` sale del tipo en ambas props: `visibleFrom="base"` sería «siempre» y
`hiddenFrom="base"` «nunca renderiza», y ninguna de las dos es una cosa que una
prop deba poder decir.
Card, Banner, ScrollArea y Sticky rinden su propia raíz y NO heredan la puerta:
queda como gap diferido con la regla del `as` (≥2 casos reales). El puente de
SSR queda escrito con su disparador, no construido — hoy sería un atributo que
nadie lee.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| El párrafo de arquitectura | `docs/architecture/eidos.md` §«responsive bridge» |
| El predicado que reutiliza | `src/arts/adom/active-dom.svelte.ts` `isAtLeast` |