You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/docs/process/CONTINUE-blocks.md

27 KiB

CONTINUE — tier blocks (handoff, act. 2026-08-06)

Estado: F1 CERRADA (8/8) · F2 CERRADA (15/15) · saneamiento de la auditoría CERRADO · fase 4 del saneamiento (newsletter coordina) HECHA. Los 15: site-header · hero · feature-grid · feature-split · pricing · testimonials · faq · stats-band · cta · newsletter · site-footer · banner · team · contact · content-section.

⚠️ LO PRIMERO: la fuente viva es el LEDGER, no el documento del 2026-08-01

docs/process/AUDIT-blocks-ledger.md — 90 filas con id estable A-01…A-90. El documento del 2026-08-01 se conserva (sus reclamaciones y evidencias son válidas y el ledger las indexa) pero su clasificación NO vale: tenía los veredictos desemparejados de sus hallazgos por un join por POSICIÓN, y el journal del workflow era de sesión. Se reprodujo en vivo al re-verificar — el journal devuelve los resultados en un orden distinto al de entrada.

Estado del ledger: 41 ARREGLADO · 34 CONFIRMADO · 12 REFUTADO · 3 DATO. Cero pendientes: los 90 están medidos. La tasa real de refutación fue del 13%, no del 28%.

La regla que hay que respetar al escribir en él: unión por id, SIEMPRE. Ningún proceso vuelve a casar dos listas por posición. Un verificador que no devuelve su id no escribe fila.

El hallazgo grande de la re-verificación

De las 12 ALTA de percepción, 8 tenían la causa raíz en el CANON, no en los blocks. Los blocks componen bien; lo que estaba roto era el arco perceptivo del canon. Todos arreglados ya (ver abajo), pero la lección se queda: cuando una auditoría de un tier acusa al tier, comprueba en qué capa vive el defecto.

Lo hecho en la sesión del 2026-08-06 (commit 866089a40)

Canon — Link (la prop color era inerte en subtle/plain, el hover clavaba primary, el :active quedaba tapado) · Card (prometía BoxProps sin aplicarlos: height="100%" era un atributo inerte; ahora el tipo dice la verdad y el eje de tamaño va por buildSizeStyle, helper compartido) · Form.Submit y Form.Reset (componen el Button del canon, así que la acción principal de un formulario recupera el contact-activate en el gesto; y el aria-label genérico deja de pisar el texto propio — WCAG 2.5.3 para todo consumidor, verificado con el AX tree de Chrome) · Field (dejaba de duplicar commit-submit dentro de un Form) · NavigationMenu (el commit-select se mueve al Link, el despliegue habla como emerge-open/close en vez de fingir una selección por hover, y el pack casa por fin con la parte que recibe el estampado — antes sonaba a la ganancia base, 10× lo diseñado) · Badge (el ✕ compone IconButton; la altura del chip pasa a ser la del control) · la fundación: [data-on] arrastra la propiedad color, no solo las variables.

Blocks — hero 1.61:1 → 6.61:1 · contact separa incomplete de invalid (el camino de error por campo era inalcanzable por construcción) y su frase llega a la AT · site-header ya no congela la página al cruzar el breakpoint · site-footer emite su commit · pricing no se vacía · cta llega a sangre de verdad · newsletter COORDINA.

Doctrina — la frontera dura 1 nombra $libs/forms; el contrato B admite un segundo servicio, el anunciador (uix.announce): un block que posee las palabras de sus estados tiene que poder decirlas.

Qué queda, por orden

  1. Fase 5 del saneamiento — endurecer blocks:check (lo único del plan sin empezar): lista blanca de importaciones, README completo con declaración de landmark, ficha en catalog.ts. Cada regla con su fixture negativo en el selfTest(), que el guard ya corre en cada ejecución.
  2. Los 34 CONFIRMADOS sin arreglar (4 ALTA · 15 MEDIA · 9 BAJA · resto): sobre todo stats-band (4), pricing (4), faq, site-header, site-footer (3 cada uno). Cada fila lleva mecanismo y evidencia en el ledger.
  3. Tres cabos concretos:
    • Un desbordamiento de texto en las cards del card-group que el usuario ve y que NO se reprodujo a 1280 en claro ni en oscuro. Falta el ancho de ventana.
    • Los hermanos del Field (password-field, search-field, textarea) declaran su propio commit-submit copiando el patrón: pueden duplicar igual dentro de un Form. Se comprueba en un comando con la sonda (abajo).
    • La página compuesta (dimensión CRUZADA), que sigue siendo la condición de cierre de F2 en el plan y que el usuario dejó fuera de alcance.
  4. F3 del plan original (10 blocks de aplicación).

La sonda de sema — G:/tmp/sonda-sema.mjs

Herramienta nueva, y la forma de contestar «¿esto suena?» sin discutirlo:

node G:/tmp/sonda-sema.mjs <url> <enter|click|hover> [selector]

Dice, por gesto, cuántos nodos de audio se crean y qué eventos se estampan con su familia e intent. Con ella se midió el doble commit-submit del Field (2 eventos a 11 ms y 4 nodos de audio → 1 y 2) y el mudo del NavigationMenu.Link. ⚠️ Sesgo conocido: asocia los metadatos por ventana temporal de 30 ms, así que puede atribuir el intent de un evento vecino. El contact-activate NO lleva intent (morfo de Button, cap. 22 §11).

⚠️ CAMBIO DE DOCTRINA (decisión del usuario, 2026-07-31). Lee la sección «La doctrina cambió» antes de tocar nada: un block ya no es solo colocación. Ya está ESCRITA en el contrato (architecture/blocks.md §«Coordination»).

(El plan fijó 14; feature-split se añadió como block hermano con scope-approval, así que el tier tiene 15 y el denominador honesto es 15.)

Commiteado en alpha-0.1-dir-prefs (866089a40 + este handoff). SIN PUSH.

Dónde vive cada cosa:

  • Plan y bitácora por block: docs/process/PLAN-blocks.md (fichas §F2.x + registro cronológico al final, con el detalle de cada cierre).
  • Contrato del tier: docs/architecture/blocks.md (B-1..B-11, D-BLK).
  • Suelo de paridad: docs/process/RESEARCH-blocks-references.md (el dossier).
  • Deuda de canon congelada: docs/process/PLAN-blocks-quality.md §6.
  • Decisiones por block: el README de cada uno (src/uix/blocks/{kebab}/).

La doctrina cambió: un block coordina, tiene estado y trae datos

Decisión del usuario, textual: «si al final es una lista de componentes… el bloque es coordinación, estado, y data también». El motivo, con el contact delante: cuando el estado vive fuera, cada disabled es una expresión distinta montada en el punto de uso y aparecen huecos — un envío que no se puede hacer y nadie dice por qué.

Lo que cambia respecto de lo firmado en docs/architecture/blocks.md (B-5/B-7 y D-BLK: «todo el contenido por snippets, cero cadenas propias, sin servicios»):

  1. El block posee el estado de su sección. Una máquina única y EXHAUSTIVA, derivada, en contexto. Sus partes la leen; ninguna la recalcula ni inventa un disabled.
  2. El block trae la forma de sus datos (el esquema por defecto). El app lo sustituye pasando el suyo.
  3. El block habla, con idlangrefs por el traductor (#?blocks.<block>.<clave>|fallback), la misma puerta que usa el canon para sus texts:. Fallback en inglés; el app traduce registrando blocks.*.

ESCRITO (2026-07-31): architecture/blocks.md gana la sección «Coordination», B-5 admite que el block traiga la FORMA de sus datos, B-7 queda enmendado (posee las palabras de SUS estados, como idlangref con fallback inglés) y la convención de servicios se acota. La sección dice también a quién NO aplica.

Enmendado el 2026-08-06: los servicios sancionados son DOS — el traductor, que resuelve las palabras, y el anunciador (uix.announce), que las entrega a la tecnología asistiva. Entró por la misma razón que el primero: un block que posee las palabras de sus estados tiene que poder decirlas, y montar una región viva propia re-implementa Announce.

Alcance acordado: contact primero como prueba de la forma — HECHO. Revisión documental de los 15 HECHA (2026-08-01): cada README declara ahora su posición bajo la doctrina, y el mapa real salió así —

  • Coordinan: contact (máquina + esquema + palabras) · pricing (el periodo por contexto, pero nada se puede BLOQUEAR ahí, así que no necesita máquina ni palabras: coordinar no es siempre una máquina).
  • Comparten configuración, no estado: team (align) · content-section (la medida).
  • Costura bindable hacia el canon: faq (value → Accordion) · site-header (mobileOpen → Drawer). Reenviar no es poseer.
  • No poseen nada: banner · cta · feature-grid · feature-split · hero · site-footer · stats-band · testimonials.
  • ✅ newsletter COORDINA desde 2026-08-06 — era el candidato abierto de esta lista y ya está cerrado. Máquina de cinco estados (incomplete · invalid · ready · sending · sent), dos Record exhaustivos de razón y acción, y Newsletter.Submit / Newsletter.Reason como PARTES que leen el contexto: la regla de forma del tier aplicada, no una excepción — compound se gana cuando las partes repiten o coordinan. state.test.ts con 8 casos.

⚠️ Al revisarlos, el listón es el que dejó contact: el estado se deriva de una fuente y las partes lo leen; si un estado puede bloquear algo, tiene frase por Record exhaustivo. Y no inventes estado donde no lo hay — un block de layout que no coordina nada se queda como está.


F2.13 contact — CERRADO (2026-07-31)

Los seis puntos que quedaban están hechos: arco verificado en navegador · README rehecho · pestañas de doc al día · namespace blocks registrado en el arnés · ficha §F2.13 + bitácora en PLAN-blocks.md · contrato B actualizado.

Un defecto real, encontrado al verificar: la máquina leía form.isValid, que con progressive significa «aún no se ha encontrado nada mal» — un formulario vacío e intacto no tiene errores, así que se declaraba válido y incomplete era INALCANZABLE al cargar: la sección pedía resolver la verificación con los tres campos vacíos. Justo el hueco que la doctrina existe para cerrar. Se arregla preguntando al ESQUEMA (validateSync de SIUM: síncrono y sin escribir errores; form.validate() habría encendido los tres campos en rojo al cargar). Fijado con state.test.ts — 9 casos, primer test unitario del tier, porque state.ts es su primera lógica pura.

Añadido de paso el control vivo que faltaba para un prop público: verification con reto / sin reto. Omitir el prop NO es pasar idle: sin él la sección no tiene paso de verificación; con un estado que nunca llega a verified tienes una verificación que no pasa, que es otra cosa.

⚠️ Ojo con el dev server de otra sesión: el de :5173 sirvió un módulo VACÍO para blocks/contact/index.ts (500 «does not provide an export named Contact») porque tenía el grafo caducado tras crear ficheros nuevos. Arrancado uno propio en otro puerto, la página va perfecta. Si mañana ves ese 500, es eso: no persigas el código.

Dos hilos de canon abiertos (medidos, sin causa raíz)

  1. Form/SIUM: con progressive/onSubmit, un envío inválido bloquea y mueve el foco pero no expone mensajes (form.errors vacío). Con onChange sí aparecen —y traducidos—, pero saltan en los tres campos al escribir en uno; con onBlur aparecen desde la carga. La máquina de estado del block TAPA el agujero de cara al usuario (el botón dice qué falta), pero el hueco sigue ahí.
  2. ProofOfHuman: el provider de soma escribe status él mismo (writableActive), así que el veredicto que devuelve el app tras consultar a su servidor no se sostiene, y los slots stage* no renderizaron en ningún estado. El camino «rechazado» no es contable hoy desde el app. Consecuencia medida en contact: sus estados verifying y rejected quedan cubiertos por test unitario de la máquina, no por el reto en vivo.

La regla de forma del tier (no la re-decidas)

Compound solo si las partes:

  • SE REPITEN — el app mapea sobre N (feature-grid.Item, testimonials.Item, pricing.Plan, faq.Item, stats-band.Stat, site-footer.Column), o
  • COORDINAN — se hablan por contexto (pricing.Switch ↔ PlanPrice).

Si no hacen ninguna de las dos: slots de snippet en la raíz (hero, cta, newsletter, y la marca / alta / social / legal / extra de site-footer). El plan original dibujaba varias de esas como partes; la desviación está registrada en cada README.


Los 8 hallazgos de canon que dejó el tier (congelados)

Restricción del usuario en vigor: no tocar componentes ni librerías fuera de src/uix/blocks/ y web/routes/blocks/. Están todos medidos en navegador y listados en PLAN-blocks-quality.md §6 (F15–F22) — más los de motion (A1, A2, A4, C7, C8, C10, D12, E14) de la pasada de calidad.

# Qué
F15 Surface variant='soft' no acota un panel: su track queda a 0.002 de luminancia del fondo de página en claro, y Surface no tiene borde. Card outline sí acota pero no acepta gradient → «panel sosegado con borde» no tiene primitivo.
F16 La ranura contrast de la paleta es blanca en todo escalón sólido, así que un lienzo de luminancia media deja el cuerpo bajo AA: primary 5.18 · indigo 5.21 · plum 4.75 pasan; neutral 3.32 · secondary/slate 3.30 · teal 3.07 fallan en claro.
F17 Text align es inerte por defecto: renderiza un span y text-align no hace nada sobre caja inline. Hay que pedir as="p".
F18 La fundación de eidos no trae reset de modelo de caja y lo asume del app. Bajo content-box, [data-field-control] (inline-size:100% + padding) mide 30px más que su contenedor. Resuelto en app-land: web/routes/blocks/_lib/reset.css.
F19 onValidSubmit/onInvalidSubmit son no-op silenciosos si se pasa un form ya construido: el componente solo los reenvía al createForm que hace él mismo. El handler va SIEMPRE en createForm.
F20 Los mensajes de SIUM son idlangref (#?sium.errors.email|…): resolver con uix.langs.t(issue.message, issue.params). La demo de docs del Form los parte a mano y por eso siempre salen en inglés.
F21 Los primitivos de layout no pueden cambiar de elemento: Text/Heading aceptan as, pero Box —y Stack/Flex/Grid/Group/Wrap/Container/Section— es un <div> fijo. Una columna de enlaces no puede ser ul/li.
F22 Un Select controlado muestra el VALOR crudo hasta abrirse una vez: las etiquetas las registran los Select.Item al montarse y el Content portaleado está cerrado. Rodeo: el child de Select.Value.

Y dos huecos anteriores que siguen abiertos: Box/Surface flex/grow no hacen crecer a un hijo flex (se rodea con tracks 1fr de Grid) y Group no apila (usa Flex direction={{ base: 'column', sm: 'row' }}; hero todavía compone sus acciones con Group).


La plantilla ya existe — cópiala, no la reinventes

src/uix/blocks/{kebab}/
├── README.md            # Función · Mapa de composición · Decisiones · Gaps
├── index.ts             # export del compound + tipos
├── types.ts             # props (todo contenido entra por snippets, B-5/B-7)
└── {kebab}.svelte       # composición: solo componentes del canon, sin CSS

web/routes/blocks/{kebab}/
├── +page.svelte         # BlockDemo + controles vivos + pestañas de doc
├── {Name}Site.svelte    # el block dentro de contenido REAL de producto
└── preview/
    ├── +layout@.svelte  # `@` resetea el layout: la vista previa es su página
    └── +page.svelte     # sirve {Name}Site leyendo la URL

Y luego: marcar shipped: true en web/routes/blocks/_lib/catalog.ts (raíl y galería leen esa única fuente) y npm run blocks:check.


Reglas que ya costaron sangre (no las re-aprendas)

  • El block se enseña A SANGRE en la página. Nada entre el block y el borde: ni marco con relleno, ni caja con scroll, ni cromo pegajoso encima. Medido: un Card desplazaba 21px un header con offset: 0 (su recipe pinta con --card-padding-*, que padding={0} de la capa Box no alcanza), y un position: sticky dentro de un div con scroll es un comportamiento que nadie vive. Si un block se ancla a algo, se mide contra lo que se anclará en producción.
  • Verifica esperando la CONDICIÓN, nunca un timeout fijo. Con Form+Field+ SIUM el dev server tarda ~2,5s en hidratar y un screenshot temprano fotografía el panel todavía invisible (data-animation-pending puesto). Esperar a que ese atributo desaparezca es la diferencia entre verificar y reportar un bug que no existe.
  • Y la condición tiene que ser algo que escriba la RUNTIME del morfo, no el render. Los data-variant / data-size los pone el componente de eidos al renderizar, así que están desde el primer frame; type, aria-label y los handlers los aplica la runtime en un efecto POSTERIOR. Esperar a que exista el nodo (o a que se vea) no basta: a ~1s medí type ausente en TODOS los Button del árbol y ningún onclick disparando, y a ~6s los mismos botones tenían type="button", aria-label="Descartar" y el clic funcionando. Reporté tres «defectos del canon» que no existían.
  • MATIZ CARO (2026-07-31): que lo escriba la runtime NO basta — tiene que no existir en el SSR. Esperé button[type="submit"] con atributo type como puerta de hidratación en contact, y ese atributo viene ya en el HTML del servidor: la espera se cumplía sobre el documento estático y yo tecleaba antes de que Svelte tomara los inputs. Resultado: los valores entraban en el DOM, form.values seguía vacío, y pasé varias rondas persiguiendo un fallo del block que no existía (llegué a «arreglar» un $derived que estaba bien; lo revertí al medirlo). La puerta honesta: algo que solo pueda haber escrito el cliente — aquí <style id="uix-blocks-display">, que pone un $effect de BootUix — más un ida y vuelta reactivo real antes de dar por hidratado.
  • Si instrumentas con console.log y no lo ves en el navegador, míralo en el SERVIDOR. Un console.log dentro de un $derived sale por el stdout del dev server durante el SSR. Ver el log SOLO ahí es la prueba de que el componente no está corriendo en cliente — fue lo que delató el diagnóstico anterior.
  • Playwright headless SÍ sirve para foco y teclado. El pane suspendido no (congela rAF y pierde activeElement), pero un page.keyboard.type() real dispara :placeholder-shown y :focus-within: así se verificó que la etiqueta flotante del Field sube al borde. page.fill() usa el setter nativo y NO los dispara. Un script del scratchpad debe importar file:///G:/dev/svelte/vicen/node_modules/playwright/index.mjs (no resuelve playwright por ruta relativa).
  • Los anchos de dispositivo (375/768) van por la ruta preview en iframe, y es opt-in: en dev, dos documentos sin empaquetar a la vez agotan las conexiones del navegador (ERR_INSUFFICIENT_RESOURCES mata las DOS páginas).
  • Cada prop público, un control vivo en la demo; los ejes de sección (tema, idioma, dirección, densidad) ya los da el shell, no los repitas.
  • Un slot que se apaga se pasa como undefined, no se renderiza vacío. Declara el snippet arriba y pásalo por prop (signup={activo ? banda : undefined}): así el {#if} del block quita también su hueco. Y cuidado con el sombreado: {#snippet signup()} pisa un prop llamado signup dentro del componente.
  • Nada de backticks de markdown dentro de <Text>: se ven literales. Lo que es código va en <Code>.
  • Ojo con las variables de layout que heredan. justify de Group se hereda a los clusters anidados (pon justify explícito) y las de Box YA no heredan desde 2026-07-23 (docs/theming/changelog.md §46).
  • Un hueco del canon se registra, no se falsea. Si al componer falta algo, va a los Gaps del block como candidato a canon y la demo usa lo que hay.
  • gap en un Grid separa también las COLUMNAS, y una parte que las cruza se lleva esos huecos encima (content-section, medido): con gap={8} y cinco pistas, la parte que abarca 2/5 medía 1088 en vez de los 1024 del contenedor con el que debía alinearse, y la de 1/-1 llegaba a 532 dentro de una rejilla de 404 y hacía scrollear la página a 420px. Si las columnas son un instrumento de medida y no cosas puestas al lado: rowGap + columnGap={0}.
  • Y 100% dentro de una pista no es 100% de la rejilla. Una pista min(medida, 100%) se come sus propios canalones (a 420px la central se quedaba los 404 enteros y la rejilla se iba a 436), y algo anidado dentro de una parte ve el ancho de SU pista, no el de la rejilla — capar igual en los dos casos descuadra uno de los dos. Los dos se cazaron midiendo, no leyendo.
  • Los números de layout se miden. Tres defaults de site-footer (container, la razón de la rejilla, el gap de columnas) salieron mal a la primera y solo el navegador lo dijo. Si un default decide cuántas cosas caben en una fila, se comprueba con el contenido real.

Deuda del arnés de demos (mía, sin tocar)

  • La galería (web/routes/blocks/+layout@.svelte) arranca UIX en línea en vez de usar _lib/BootUix.svelte, que es lo que usan los previews: la misma cadena escrita dos veces, y el motivo de que reset.css haya que importarlo en los dos sitios.
  • En BlockDemo, a ~1400px, el conmutador de anchos de dispositivo se solapa con el párrafo que lo precede. Idéntico en los 11 blocks → es del arnés, no de un block.

Deuda a decisión tuya

Siguen sin decidir, cada una entra por el contrato B con fase 0 ligera (mirar ≥2 catálogos del dossier y anotar adoptado/descartado en el README):

  1. Tabla de comparación de features×planes en pricing (recurre en las refs; sería un hermano pricing-table).
  2. Cita en spotlight en testimonials (la variante modal de TODAS las refs; el grid es minoría, 2 de 8 en TW).
  3. Lista estática 2/3 columnas en faq (6 de 7 en TW no son acordeón).
  4. form-in-hero (slot de alta al boletín dentro del hero).

Resueltas ya: feature-split/alternante (F2.3b, con Mockup) · hero con fondo cover (layout background) · CTA que navega y parece botón (el child de Button entrega un snippet content; Button sigue sin href y Link sigue poseyendo la navegación — doctrina en el README de eidos Button §«CTA que navega»).


Estado de gates al parar (2026-08-06, commit 866089a40)

  • npm run blocks:check verde, 15 blocks / 114 ficheros.
  • npx vitest run src/uix/blocks/ — 20/20 (contact/state.test.ts 12 + newsletter/state.test.ts 8).
  • npx vitest run src/uix/eidos src/uix/blocks — 402/402.
  • npm run docs:check — 0 errores, 0 avisos (617 docs).
  • svelte-check: 75 errores / 54 avisos = LA LÍNEA BASE, ninguno en blocks/ ni en lo que toqué del canon. ⚠️ Esa cifra SE MUEVE: con otras sesiones vivas en el mismo árbol se midió 75, 76, 77 y 80 en el mismo día. Mídela justo antes y justo después de tu cambio, nunca contra un número recordado.
  • prettier: limpio en todo lo del commit.
  • Fallos AJENOS vivos al parar (no son del tier): 7 en contracts.test.ts + soma-attr-audit (barrel de waveform, MOR-4, escrituras DOM, data-attrs, namespaces camelCase de aura) y un __adv-verify-textarea-pack.test.ts sin trackear de otra sesión.

⚠️ El árbol está COMPARTIDO — cuidado al commitear

El commit 866089a40 incluye ficheros que ya venían modificados cuando empezó la sesión: había trabajo sin commitear de otras sesiones sobre blocks/ y eidos/, y al editar encima no hay forma de separarlo por fichero. Lo que sí se respetó: morfo/{calendar,pagination,rating-group,toolbar,tree-view,schema,types}, sema/components/textarea.ts y soma/tree-view quedaron FUERA por ser de otra sesión.

Antes de tocar sema o morfo, mira si hay otra sesión viva ahí. En este mismo día, una arregló el emparejamiento de las reglas (closest()) y el renombrado que mató el prefijo form. en las claves de sonido, y otra (ésta) cambió a qué parte apuntan los eventos. Se complementaron por suerte, no por diseño.


Sesión 2026-08-01 — lo que cambió

  • content-section HECHO → F2 cerrada 15/15. Rejilla de escape de 5 pistas (measure col 3 · wide col 2/5 · full col 1/-1), los tres ejes RESPONSIVE por eidos.resolve(), y .Media con default measure: la figura sigue al texto y salirse se PIDE, como en todas las referencias.
  • CANON tocado por orden del usuario: un componente no es librería de otro. Las cinco escalas tipográficas (TextTracking, TextLeading, TextWrap, TextNumeric, TextMeasure) salieron de components/text/types.ts a eidos/lib/types.ts. Heading ya las importaba de Text desde antes.
  • La galería EMITE: arrancaba sin packs de sema desde F0 — el motor estampaba data-event-* y nada era audible. Ahora events: { sound, haptic, components } con la lista DERIVADA del barrel (Object.values), no escrita a mano: la lista a mano del otro arnés se quedó atrás dos veces.
  • Los 15 READMEs declaran su posición bajo la doctrina de coordinación. El mapa no es uniforme y está en la sección de arriba.

Lecciones de verificación que costaron esta sesión

  1. La puerta de hidratación no puede ser un atributo que el SSR ya manda. Usé el type del submit y la espera se cumplía sobre el documento estático: tecleé antes de hidratar, los valores entraron en el DOM, form.values siguió vacío, y perseguí un fallo inexistente varias rondas. La puerta honesta es algo que solo escriba el cliente (style#uix-blocks-display).
  2. Medir el block contra SÍ MISMO no basta. El defecto de los 16px de content-section solo apareció al poner una sección normal debajo y comparar bordes. Los 15 blocks están verificados cada uno contra sí mismo.
  3. Inspeccionar en una pestaña de fondo miente: visibilityState: 'hidden' suspende el rAF y deja data-animation-pending con opacity: 0, que parece un fallo de visibilidad grave y no lo es.
  4. Screenshot Y MIRAR. Generé la captura en oscuro y nunca la abrí.

Powered by TurnKey Linux.