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/AUDIT-blocks-2026-08-01.md

71 KiB

AUDIT — tier blocks (2026-08-01)

Congelado desde el workflow wf_31fd1de3-ed6 (50 agentes). El journal del workflow es de SESIÓN y desaparece: esta es la única copia.

⚠️ SU CLASIFICACIÓN NO VALE — 2026-08-05. Los campos «Veredicto» de este documento están desemparejados de su hallazgo (el de cta/Container lleva uno sobre Card/BoxProps; el del contraste de hero, uno sobre el drawer de site-header; la primera justificación de la lista de refutados refuta el hallazgo de feature-grid que la sección de confirmados declara CONFIRMADO). Con el veredicto viajó la etiqueta CONFIRMADO/REFUTADO, así que ni los 29 «confirmados» ni los 11 «refutados» son evidencia de nada y la instrucción anterior —«no re-reportar lo refutado»— queda retirada. Lo que SÍ es válido y por eso este fichero se conserva íntegro: la reclamación, la evidencia y la sugerencia de cada entrada, que sí casan entre sí. El estado vivo de cada hallazgo vive ahora en AUDIT-blocks-ledger.md, con id estable por fila.

Método

Dos dimensiones en paralelo sobre los 15 blocks — doctrina (contrato B + regla de coordinación, por lectura de fuente/README/demo) y percepción (sema en vivo en navegador, interacción por interacción) — y después un verificador ESCÉPTICO por hallazgo, con el encargo de refutarlo y la lista de falsos positivos conocidos del repo (medir antes de hidratar, pestaña en segundo plano, transforms de entradas sin correr, confundir un hueco congelado F15–F22).

La dimensión CRUZADA (los 15 en una página compuesta) no se llegó a ejecutar.

Hallazgos en bruto 90
Únicos 90
Verificados 40
Confirmados 29
Refutados 11 (28%)
Sin verificar 50

⚠️ El tope de verificación (40) lo puso el script, no el trabajo: 50 hallazgos quedaron sin refutar, 18 de ellos marcados ALTA. No son menos graves: están sin comprobar, y la tasa de falsos positivos medida es del 28%.


Confirmados (29)

[ALTA] cta — doctrina

La disposición «Panel a sangre → app-land, la app pone container="full"» es falsa: Container siempre aplica padding inline y el block no ofrece escotilla, así que el panel nunca llega al borde.

  • Fichero: src/uix/blocks/cta/README.md
  • Evidencia: cta/README.md:81 (y la pestaña Gaps de la demo, web/routes/blocks/cta/+page.svelte:191-193) prometen sangre con container="full". Pero src/uix/eidos/components/container/container.svelte:49 hace paddingX ?? 'var(--container-padding-inline)' = var(--space-4) = 16px (src/uix/eidos/generated/base.css:145 y :87); full solo quita el cap (--container-width-full: none). cta.svelte:104 monta <Container size={container}> sin reenviar paddingX, y ...rest va al <section> (cta.svelte:102), no al Container. Medido en Chrome (/blocks/cta/preview, viewport 1280, body 1254): el [data-surface] pasa de 992@x139 a 1222@x24 al emular full — sigue metido 16px por lado (y con `…
  • Veredicto: CONFIRMADO, y el mecanismo es exactamente el descrito. src/uix/eidos/components/card/types.ts:66 declara CardProps = Omit<BoxProps,'display'> y el bloque doc de :58-65 promete «Composes through so consumers can apply layout primitives», pero card.svelte NO importa Box: destructura solo variant/size/color/rounded/shape/interactive/selected/disabled/noEmerge/motion/onSelect/style/children y vuelca el resto con {...rest} sobre <svelte:element this={tag} …> (card.svelte:108-134). Por…
  • Sugerencia del auditor: O reenviar paddingX (o un bleed) al Container, o reescribir la disposición del gap: container="full" da ancho completo del contenido, NO sangre — la sangre real hoy no existe.

[ALTA] hero — percepcion

En layout="background" la CTA secundaria mide 1.78:1 contra su fondo — muy por debajo de AA — y el token que la demo escribe a mano para arreglarlo no hace absolutamente nada.

  • Fichero: web/routes/blocks/hero/HeroSite.svelte
  • Evidencia: HeroSite.svelte:99-106 — <Link variant="subtle" color={layout === 'background' ? 'var(--color-content-on-solid)' : undefined}>. Medido en /blocks/hero/preview?layout=background: el <a data-link> computa color: oklch(0.2435 0 0) (rgb 32,32,32) y un clon sondeado SIN el override computa EXACTAMENTE lo mismo — el valor crudo entra por --color-custom (lib/component-color.ts:57) y variant="subtle" hereda la tinta de página igual. Fondo compuesto bajo el enlace = gradiente oklch(0.459 0.151 305.86) + scrim rgb(28 25 23 / 0.42) a opacity .45 → rgb(94,53,128). Contraste enlace/fondo 1.78:1; el título, que el block SÍ pasa a on-solid, mide 9.14:1 en el mismo punto.
  • Veredicto: CONFIRMADO, y el punto débil de la evidencia original (su propio pageScrollable:true) queda resuelto: window.scrollTo() sigue moviendo el documento porque overflow:hidden no bloquea el scroll programático, pero SÍ bloquea el del usuario. Medido con rueda real y con swipe táctil real: la página no se mueve. Mecanismo verificado en el código: site-header.svelte:56-66 envuelve el <Drawer> COMPLETO (Trigger + Overlay + Content, sin Drawer.Portal — que existe: `eidos/components/drawer/ind…
  • Sugerencia del auditor: Que el block ponga la tinta on-solid en el CONTENEDOR de la copia bajo background (no elemento a elemento en Display/Text), de forma que lo que el app suelte en actions / eyebrow / children la herede; y retirar de la demo el color="var(--color-content-on-solid)", que es un token a mano y además inerte.

[ALTA] feature-grid — doctrina

El README no declara landmark ni cómo ajustar la jerarquía de encabezados, que es obligación explícita de B-8 y de la plantilla; sus dos hermanos sí lo hacen.

  • Fichero: src/uix/blocks/feature-grid/README.md
  • Evidencia: feature-grid/README.md no contiene ninguna línea de landmark (la «Composition map» acaba en la fila .ItemText). Contraste: site-header/README.md:21-25 «Landmark + headings: emits a single <header> and NO heading…» y hero/README.md:28-30 «Landmark + heading: one <section> named by its title…». La plantilla lo exige en src/uix/blocks/README.md:78-79 y B-8 en docs/architecture/blocks.md:126. El hecho existe pero solo en la demo (feature-grid/+page.svelte:173-177: «Sección sin nombre por ahora»), mientras el block emite <section {...rest}> sin nombre accesible en feature-grid.svelte:25.
  • Veredicto: SE SOSTIENE (con dos correcciones al diagnostico del auditor, ninguna lo refuta). 1) El override ES inerte, y por una razon MAS FUERTE que la que dio el auditor. La causa no es que el valor crudo entre por --color-custom: es src/uix/eidos/components/link/link.css:34-37, [data-link][data-variant='subtle'],[data-link][data-variant='plain'] { color: inherit }, que gana por especificidad (0,2,0) a [data-link] { color: var(--_link-palette-text) } (0,1,0) de link.css:21. Sondas en vivo: el clo…
  • Sugerencia del auditor: Añadir el párrafo al README: emite un <section> sin nombre (por tanto no es landmark expuesto), la cabecera del app es h2, .ItemTitle es h3 ajustable por su prop level, y aria-label/aria-labelledby pasan por ...rest.

[ALTA] contact — doctrina

CONTACT_ACTION («la acción nombra lo que está pasando», state.ts:86) solo se cumple a la vista: el nombre accesible del botón es siempre «Enviar», porque el canon Form.Submit impone su propio aria-label y gana en el merge. En sending y sent el nombre accesible contradice al visible, y en reposo «Enviar» ni siquiera contiene la etiqueta visible «Enviar mensaje» (falla WCAG 2.5.3 Label in Name).

  • Fichero: G:/dev/svelte/vicen/src/uix/blocks/contact/state.ts
  • Evidencia: MEDIDO en /blocks/contact/preview: texto «Enviar mensaje» / aria-label="Enviar"; tras enviar, texto «Enviado» y aria-label="Enviar". Causa: src/uix/soma/components/form/form-provider.svelte.ts:198 'aria-label': this.resolvedLabel + src/uix/soma/props/props.ts:131 (b !== undefined ? b : a, y state.props va de segundo).
  • Veredicto: CONFIRMADO, y no es ninguno de los falsos positivos habituales. Cadena causal cerrada en el código: src/uix/blocks/banner/banner.svelte:59 pasa variant="ghost" y NINGÚN color; src/uix/eidos/components/banner/banner-close.svelte:30-37 reenvía ese color undefined al IconButton, que cae al default canónico → el nodo sale con data-color="primary" (leído del DOM real, no inferido). En button.css:208-214 el slice ghost fija --_button-fg: var(--button-palette-text), o sea la tinta P…
  • Sugerencia del auditor: El aria-label del canon debería ceder ante el contenido cuando hay children (o ante un aria-label explícito del consumidor). Como el block no puede pisarlo, es un hilo de canon a registrar; mientras tanto la afirmación de state.ts:86 debería acotarse a «a la vista».

[ALTA] contact — doctrina

El block importa de $libs/forms, que no está en la lista de importaciones permitidas de la frontera dura 1 («may import $uix, $adom and the public arts»). No es un ciclo ni una importación entre blocks —blocks:check pasa limpio— pero el contrato escrito y el código no coinciden.

  • Fichero: G:/dev/svelte/vicen/src/uix/blocks/contact/contact.svelte
  • Evidencia: contact.svelte:28 import { createForm } from '$libs/forms'; (también context.ts:2 y types.ts:3) vs docs/architecture/blocks.md:95-97. Nota: soma consume $libs/forms igual (src/uix/soma/components/form/form-provider.svelte.ts) y no hay puerta de canon que exporte createForm, así que probablemente lo que falta es enmendar la doctrina, no el código.
  • Veredicto: Intenté refutarlo por las cuatro vías habituales y ninguna aplica: (1) medí DESPUÉS de la puerta de hidratación real (style#uix-blocks-display), y además --_link-palette-text es CSS puro — no depende de runtime alguno; (2) la pestaña estaba activa y document.querySelectorAll('[data-animation-pending]').length === 0, opacity: 1 en el link y en sus 7 ancestros (el block declara explícitamente que NO trae motion de entrada); (3) no hay getBoundingClientRect en juego, son colores computado…
  • Sugerencia del auditor: Añadir $libs a la frontera dura 1 (o nombrar explícitamente $libs/forms como puerta sancionada), o exportar createForm desde el canon Form y consumirlo por ahí.

El éxito del alta en la banda de sign-up del footer es PERCEPTIVAMENTE NULO: no suena, no estampa y no emite — el único momento que merecía commit-submit + fulfill no ocurre en absoluto.

  • Fichero: web/routes/blocks/site-footer/SiteFooterSite.svelte:105
  • Evidencia: Medido en /blocks/site-footer con dos instrumentos independientes. (1) Contador WebAudio parcheado sobre AudioContext.prototype.createOscillator/createBufferSource: escribir vicen@example.com y pulsar «Suscribirme» → 0 nodos de audio; el MISMO gesto en /blocks/newsletter → 2 osciladores (1 earcon). (2) MutationObserver ligado AL NODO <form data-form> (sobrevive al desmontaje): traza completa = {dt:0 before-click, connected:true} → {dt:46 DETACHED, connected:false} y NINGUNA mutación data-event jamás, ni siquiera ya desprendido. Causa: SiteFooterSite.svelte:105 {#if subscribed} sustituye el <Form> entero por un <Text>; el commit-submit del morfo de Form es `sequence:…
  • Veredicto: CONFIRMADO, y en el caso peor es más grave de lo reportado. La cadena es toda del block: contact.svelte:61 elige validationBehaviour:'progressive', que en form-core.svelte.ts:337-344 + :410-411 solo escribe errores tras un submit fallido; y contact-submit.svelte:21 + state.ts:59,65-67 solo habilitan el botón cuando schema.validateSync(values).ok ya es cierto (contact.svelte:74-78). El único disparador de submit del sistema es el onsubmit del <form> (`form-provider.svelte.ts:1…
  • Sugerencia del auditor: Mantener el <Form> montado y añadir la confirmación como hermana —exactamente lo que hace NewsletterSite.svelte con su Callout—, o si el swap es deliberado, que el onValidSubmit emita antes de cambiar el estado. Vale la pena además que la doctrina lo escriba: un sequence:'post' no sobrevive al desmontaje de su propio provider.

[MEDIA] faq — doctrina

La sección «Coordination» declara UNA sola costura (value) pero el block también expone disabled: una pregunta que no se puede abrir, sin nada que diga por qué y sin que el block posea ese estado.

  • Fichero: src/uix/blocks/faq/types.ts
  • Evidencia: faq/types.ts:27-28 declara /** Disable this question. @default false */ disabled?: boolean; y faq-item.svelte:16 lo reenvía a Accordion.Item. El README §Coordination (faq/README.md:30-33) solo dice «value is a bindable SEAM… A seam is not ownership», sin mencionar disabled. .Item no hace spread de ...rest, o sea que las dos props expuestas son una ELECCIÓN del block, no un passthrough genérico. Es literalmente el patrón que el brief marca como grave (una interacción bloqueada sin frase), aunque aquí quien decide bloquear es la app.
  • Veredicto: Se sostiene, y es mas amplio de lo que reporto el auditor. Medido en navegador: la Card featured (Pro) NO tiene elevacion de ningun tipo — difiere de las otras SOLO en el color de borde. El unico eje de distincion es cromatico, exactamente como declaran el comentario de implementacion (pricing-plan.svelte:7-11) y la brecha del README (README.md:74). Ademas de la demo, la MISMA afirmacion falsa esta en la superficie publica de la API: types.ts:55 documenta featured como «(accent border, elevati…
  • Sugerencia del auditor: O nombrar disabled en §Coordination diciendo por qué una pregunta bloqueada no necesita frase (la app pone el motivo en el texto de la pregunta), o retirarlo. Que la sección enumere una costura y el código exponga dos es la deriva.

[MEDIA] cta — doctrina

La medida de la descripción se construye a mano con un valor fuera de escala (60ch) en vez de usar el vocabulario de medida que el canon ya expone en el propio Text que envuelve.

  • Fichero: src/uix/blocks/cta/cta.svelte
  • Evidencia: cta.svelte:69 <Box maxWidth="60ch"> envolviendo el <Text as="p" …> de cta.svelte:73-81. El canon tiene el eje: src/uix/eidos/components/text/types.ts:58-59 measure?: ResponsiveProp<TextMeasure> («needs a block as, e.g. p» — el bloque YA pone as="p"), con escala tokenizada --measure-narrow: 54ch · --measure-normal: 66ch · --measure-wide: 78ch (src/uix/eidos/generated/base.css:323-325). 60ch no es ningún escalón. Las propias demos del tier usan el eje canónico: web/routes/blocks/cta/CtaSite.svelte:36 y web/routes/blocks/stats-band/StatsBandSite.svelte:51 pasan measure="wide".
  • Veredicto: CONFIRMADO. No es ninguno de los falsos positivos listados: medí despues de la puerta de hidratacion (style#uix-blocks-display, con state:'attached' — el <style> nunca es "visible"), despues de correr las entradas de viewport (0 [data-animation-pending]), y sobre atributos que YA aplico la runtime del morfo (data-state + aria-pressed presentes en ambos items). No es F15–F22 ni es el contrato B-1: el defecto no es que el block carezca de morfo, es que su propio control escribe un valo…
  • Sugerencia del auditor: Sustituir el Box maxWidth="60ch" por measure="narrow" en el Text (o justificar el 60ch como literal: si la medida elegida es intencionalmente intermedia).

[MEDIA] stats-band — doctrina

En modo live el anuncio a lectores de pantalla puede llevar la cifra CRUDA mientras en pantalla se ve la formateada por locale: el block pasa value y children a la vez por dos caminos distintos.

  • Fichero: src/uix/blocks/stats-band/stats-band-value.svelte
  • Evidencia: stats-band-value.svelte:20 <Metrics.Value value={count} {...rest}> con <CountUp to={count}> como children (:22). En el canon, metrics-value.svelte dispara ctx?.notifyUpdate(el, String(next)) al cambiar value, y metrics.svelte:57-63 lo pasa como message de signal-notify-update (que metrics/context.svelte.ts:11-13 describe como «flash + sound/haptic + live-region announce»). Lo renderizado, en cambio, lo formatea CountUp vía uix.format.numbers (count-up.svelte:63-86). O sea: con count=12500 se vería «12.500» y se anunciaría «12500». La demo publicita live como parte del API de .Stat (web/routes/blocks/stats-band/+page.svelte:131-135). NO lo he reproducido en…
  • Veredicto: El hallazgo SE SOSTIENE, y además la causa que dio el auditor es la correcta (lo he medido, no deducido). Mecanismo verificado en el código: - src/uix/morfo/components/drawer.ts:180-184 declara en la parte Trigger un aria-label con value: v.translationRef('#?components.drawer.trigger|Open drawer') sin condition. Al no ser literal, el compilador lo mete en dynamicAttrs (src/uix/morfo/compile.ts:418-425). - src/uix/soma/components/drawer/drawer-provider.svelte.ts:415-423 regist…
  • Sugerencia del auditor: Decidir el camino único: o no pasar value cuando hay count (live no anuncia cifras contadas) o formatear el mensaje con uix.format antes de notificar. Verificar con live + un count que cambie.

[MEDIA] site-header — doctrina

Si el cajón está abierto y el viewport cruza el breakpoint, el propio block esconde cajón + overlay + trigger con display:none, pero el Drawer sigue open y el bloqueo de scroll del body sigue puesto: la página queda inmovilizada y no hay NADA en pantalla que lo explique.

  • Fichero: src/uix/blocks/site-header/site-header.svelte
  • Evidencia: site-header.svelte:56-66 — <Box display={{ base: 'contents', [breakpoint]: 'none' }}> envuelve el <Drawer> completo (Overlay + Content, sin Drawer.Portal, que sí existe: eidos/components/drawer/index.ts:58). Medido en http://localhost:5180/blocks/site-header/preview?breakpoint=md — abrir el cajón a 500px, redimensionar a 1200px y disparar resize: {drawerState:'open', drawerVisible:false, overlayVisible:false, triggerVisible:false, hiddenByAncestorStyle:'--box-display: none;', bodyStyle:'overflow: hidden;', pageScrollable:true}. Solo Escape recupera (verificado: state pasa a 'closed' y el style del body se vacía) — un usuario táctil que gira la tableta se queda sin salida visible. E…
  • Veredicto: CONFIRMADO, y la causa raíz es de canon, no del block. Card declara sus props como Omit<BoxProps,'display'> (src/uix/eidos/components/card/types.ts:66) — TypeScript acepta height — pero card.svelte NO compone Box: su destructuring (líneas 24-39) no recoge height, así que cae en ...rest y se derrama tal cual en el <svelte:element this={tag}> (línea 131 sobre la 108). Como interactive=false el tag es <div>, y height no es atributo presentacional de un <div>: es INERTE. Box …
  • Sugerencia del auditor: El block YA posee el breakpoint que provoca el corte: cerrar mobileOpen cuando la variante ancha toma el relevo, leyendo el estado reactivo del propio framework (uix.dom.isAtLeast(breakpoint) / currentBreakpoint, no un matchMedia propio — sigue siendo B-6). Alternativa mínima: envolver el Drawer en Drawer.Portal para que no cuelgue del Box que se apaga, aunque eso deja el cajón abierto…

[MEDIA] site-header — doctrina

Las demos de site-header y hero redeclaran a mano uniones que ya existen en el canon, y ya han derivado: el xxl de ContainerSize es inalcanzable desde el control vivo aunque el block lo acepte.

  • Fichero: web/routes/blocks/site-header/SiteHeaderSite.svelte
  • Evidencia: SiteHeaderSite.svelte:25-26 type ContainerSize = 'sm'|'md'|'lg'|'xl'|'full' y type Breakpoint = 'sm'|'md'|'lg'. Canon: eidos/components/container/types.ts:4 incluye 'xxl' (token real: generated/base.css:143 --container-width-xxl), y libs/dom/responsive.ts:1 declara seis valores ('base'|'sm'|'md'|'lg'|'xl'|'xxl'). Mismo calco en site-header/+page.svelte:24-25, site-header/preview/+page.svelte:13-14, hero/HeroSite.svelte:30-33 y hero/preview/+page.svelte:13-19 (este último recalca también BackdropPattern). El block hace lo correcto (site-header/types.ts:3-4 importa los tipos del canon); la demo no.
  • Veredicto: Se sostiene, y con una consecuencia MEDIBLE que el auditor no llegó a nombrar. Los tres datos que aportó son literales y correctos: hero.svelte:46 deriva onDark, se consume SOLO en :81 (Display) y :92 (Text); types.ts:43-64 declara eyebrow/actions/media/children como Snippet sin parámetros y el block no hace setContext (a diferencia de contact/pricing/team/content-section, que sí tienen context.ts); HeroSite.svelte:103 re-deriva layout === 'background' a mano; y README.md:36 dice…
  • Sugerencia del auditor: Importar ContainerSize, Breakpoint, SectionSize, HeroLayout y BackdropPattern desde su origen en las cuatro demos. Así un valor nuevo del canon rompe a nivel de tipo en vez de quedarse sin control vivo en silencio.

[MEDIA] feature-grid — doctrina

El tipo y la demo describen el chip del icono como Surface soft; el código lo pinta solid por defecto, y el README dice solid. Dos de las cuatro fuentes mienten.

  • Fichero: src/uix/blocks/feature-grid/types.ts
  • Evidencia: types.ts:40 «Wraps a soft Surface chip». web/routes/blocks/feature-grid/+page.svelte:134 «el chip (Surface soft)». Frente a feature-grid-item-icon.svelte:12 let { color = 'primary', variant = 'solid', … } y feature-grid/README.md:17 «Surface (solid, accent) … solid so it reads in light mode».
  • Veredicto: Se sostiene, con tres precisiones. CONFIRMADO: (a) el block emite encabezados propios con nivel FIJO 3 y sin escotilla por su parte — faq-item.svelte:17 usa <Accordion.Header> sin level, faq-item.svelte:10 destructura sin ...rest, y types.ts:24-33 cierra FaqItemProps a 4 props (value,disabled,question,children); el nivel viene del default level = 3 de soma/components/accordion/components/accordion-header.svelte:16, y el canon SÍ lo expone (eidos `accordion-header.svelte…
  • Sugerencia del auditor: Corregir el JSDoc de FeatureGridItemIconProps y el DocRow de la demo a solid (que es la decisión razonada del README), mencionando el variant="soft" como opción.

[MEDIA] testimonials — percepcion

Mismo defecto: height="100%" sobre Card es inerte, así que la tarjeta no rellena la altura de fila y las caras NO se alinean cuando las citas miden distinto.

  • Fichero: src/uix/blocks/testimonials/testimonials-item.svelte
  • Evidencia: Medido en Chrome (preview de testimonials, barrido de anchos): a 940px la celda 2 mide 196 px y su [data-card] 169 px; a 820px y 760px la celda 4 mide 196 px y su card 169 px (27 px de hueco muerto bajo la tarjeta). El atributo aparece crudo en el DOM: card.getAttribute('height') = "100%" sobre un <div>. Falsa: README.md:16 («fills the row height») y README.md:27-28 («.Author pins to the card's bottom (margin-block-start: auto) so a row of uneven quotes still lines its faces up»), más la demo web/routes/blocks/testimonials/+page.svelte:42-43 («tarjetas de igual alto»). A 1280px no se nota porque las cinco citas caen en el mismo número de líneas — la demo enmascara el fallo ju…
  • Veredicto: Se sostiene: es deriva real entre el typedoc público y el código, no un falso positivo. (1) src/uix/blocks/faq/types.ts:17 promete «Wraps a Box (measure-cap, centered)»; faq-header.svelte:12 es <Box maxWidth="48rem" width="100%"> y Box NO emite margen auto (box.css:242-251 solo escribe margin-* desde --box-margin-*, que aquí van sin poner → computed margin-left/right: 0px, medido); faq.svelte:23 apila con align="stretch" (computed align-items: stretch, medido). Con `align-i…
  • Sugerencia del auditor: Misma brecha de canon que en pricing (Card no consume BoxProps pese a declararlas). Registrar en Gaps y/o corregir el README hasta que el canon lo sostenga; añadir a la demo una cita larga y otra corta para que el desalineado se vea al ancho por defecto.

[MEDIA] feature-split · pricing · testimonials — doctrina

Ninguno de los tres README declara el landmark ni la jerarquía de encabezados, que es la segunda mitad literal de B-8 y el único apartado a11y propio del tier.

  • Fichero: src/uix/blocks/pricing/README.md
  • Evidencia: B-8 (docs/architecture/blocks.md:126): «Correct landmarks: sectioning element + aria-label/aria-labelledby where landmarks repeat; heading hierarchy coherent and documented in the block README (which level it emits, how to adjust)». La plantilla lo pide con ese mismo nombre en src/uix/blocks/README.md:80-81. grep -n -i "heading hierarchy|Landmark +" src/uix/blocks/*/README.md devuelve SOLO hero/README.md:28 y site-header/README.md:21. Los tres auditados emiten un <section> sin nombre accesible (feature-split.svelte:20, pricing.svelte:36, testimonials.svelte:19 — todos <section {...rest}>) mientras los hermanos con la misma forma sí lo nombran (cta.svelte:102, `h…
  • Veredicto: SE SOSTIENE, y es mas amplio de lo que dijo el auditor. Comprobado en las cuatro patas (tipo, compilado, runtime de Svelte y DOM vivo), y descartadas las causas de falso positivo: no es medicion pre-hidratacion (espere style#uix-blocks-display con state:'attached' — el <style> esta oculto, visible da timeout falso), no es pestana de fondo (0 [data-animation-pending] tras hacer scroll), no es transform de una entrada sin correr (no mido rects, leo getComputedStyle y el style inline)…
  • Sugerencia del auditor: Añadir a cada README el párrafo «Landmark + heading hierarchy» que ya escribe hero: qué elemento de seccionado emite, con qué nombre (o por qué va sin nombre — un <section> anónimo NO se expone como region, y decirlo es la mitad del valor), qué nivel de encabezado pone el block (.Title h2, .PlanName h3) y cómo lo ajusta la app. Decisión separada a elevar: pricing y testimonials renderi…

[MEDIA] contact — doctrina

La frase del estado no llega a la tecnología de apoyo: el submit va disabled nativo (no es alcanzable con teclado, así que un lector de pantalla nunca lo encuentra para saber que está bloqueado) y la razón es un <span> pelado, sin role="status"/aria-live y sin aria-describedby desde el botón. B-7 dice que el registro que bloquea la acción escribe la frase «so it cannot go silent» — en el canal de AT sí es muda.

  • Fichero: G:/dev/svelte/vicen/src/uix/blocks/contact/contact-reason.svelte
  • Evidencia: contact-reason.svelte:24-26 renderiza <Text> sin rol ni live; contact-submit.svelte:21 pasa disabled nativo. MEDIDO en /blocks/contact: botón disabled=true, aria-describedby=null; el único [aria-live] de la página es un div vacío ajeno. El cambio de estado incomplete→unverified cambió el texto del span («Completa todos los campos…» → «Resuelve la verificación de arriba.») sin ninguna región viva que lo anuncie. Doctrina: docs/architecture/blocks.md:125 (B-7 enmendado).
  • Veredicto: El hallazgo SE SOSTIENE, y medido en navegador es peor de lo que el auditor dijo. Intenté refutarlo por las cuatro vías habituales de falso positivo y ninguna aplica: (a) medí tras la puerta de hidratación real (style#uix-blocks-display), con scroll completo y esperando a que no quedara ningún [data-animation-pending], así que la entrada scale-fade ya había corrido; (b) los valores son getComputedStyle().color, que no lo tocan los transforms de la entrada; (c) no es ninguno de los huecos…
  • Sugerencia del auditor: Contact.Reason con role="status" (o aria-live="polite") y un id que Contact.Submit referencie por aria-describedby; si el canon Form.Submit no admite aria-describedby, ese es el hueco a promover.

[MEDIA] contact — doctrina

Deriva README↔código: el README afirma en negrita que el block «no valida y no pinta los errores de los campos», y Contact.Fields hace exactamente eso — lee form.errors y renderiza un Field.ErrorText por cada uno de los tres campos.

  • Fichero: G:/dev/svelte/vicen/src/uix/blocks/contact/README.md
  • Evidencia: README.md:50-53 («It does not validate and it does not render fields' errors.») vs contact-fields.svelte:37-40 (errorOf() leyendo form?.errors?.[field]?.[0]) y :53-55, :63-65, :76-78 (<Field.ErrorText>{errorOf(...)}</Field.ErrorText>).
  • Veredicto: El hallazgo SE SOSTIENE, y lo he probado por A/B sobre el elemento real, no por lectura de CSS. Cadena verificada de punta a punta: 1. link.svelte:40,55 — un color no canónico entra por resolveComponentColor() (eidos/lib/component-color.ts:57) y solo produce data-color-custom + el semilla inline --color-custom: …. NUNCA escribe color inline. 2. generated/base.css:4769 — [data-link][data-color-custom] deriva --_link-palette-text: var(--palette-text, …). La derivación SÍ corre.…
  • Sugerencia del auditor: Corregir la frase: el block SÍ pinta el error del campo (traducido, que es el motivo de F20). Y si se resuelve el hallazgo alta, esa rama por fin dejará de ser inalcanzable.

[MEDIA] content-section — doctrina

La pestaña API de la demo (superficie obligatoria por B-9) publica un default falso para ContentSection.Media: dice width «def. wide» cuando el código y toda la doctrina del block dicen measure — precisamente la decisión de cabecera («una figura que se ensancha sin que se lo pidas es una sorpresa»).

  • Fichero: G:/dev/svelte/vicen/web/routes/blocks/content-section/+page.svelte
  • Evidencia: web/routes/blocks/content-section/+page.svelte:193 «width (def. wide)» vs src/uix/blocks/content-section/content-section-media.svelte:26 let { width = 'measure', … } y README.md:86-90 + types.ts:88 (@default 'measure'). La propia demo se contradice: +page.svelte:22-24 comenta que el control arranca en el default del block y arranca en measure.
  • Veredicto: CONFIRMADO. El mecanismo es exactamente el descrito: [data-banner] es display:flex (banner.css:20), el Container del block lleva margin-left:auto + margin-right:auto (container.svelte:26-30, align='center' por defecto, estampados como marginLeft/marginRight en 57-58) y Banner.Close añade margin-inline-start:auto (banner.css:150-152). Tres márgenes automáticos en el eje principal → el algoritmo flex reparte el espacio libre en TRES partes iguales en vez de dos, y la columna de…
  • Sugerencia del auditor: Cambiar «def. wide» por «def. measure» en el DocRow de ContentSection.Media.

[MEDIA] team — doctrina

Comentario nuevo en castellano dentro de la fuente de un block, contra la regla del proyecto (comentarios en inglés; los castellanos que quedan son legado y no se añaden más). Es el único fichero de los tres blocks que lo tiene.

  • Fichero: G:/dev/svelte/vicen/src/uix/blocks/team/team-member-links.svelte
  • Evidencia: team-member-links.svelte:15-17: «// Pegada al fondo de la tarjeta: con biografías de distinto largo, las filas / de enlaces quedarían a alturas distintas y la retícula se leería descuadrada. / Sin biografías las tarjetas ya miden lo mismo y esto no cambia nada.» (CLAUDE.md §Code Style: «Comments in English… don't add new ones»).
  • Veredicto: Reproducido, y no cae en ninguna de las causas de falso positivo. Medí DESPUÉS de la puerta de hidratación (style#uix-blocks-display), tras recorrer la página entera y esperar a que no quedara ningún [data-animation-pending], y con getComputedStyle (no getBoundingClientRect, así que ningún transform de entrada contamina). Mismos números en /blocks/site-footer y en /blocks/site-footer/preview?columns=4, en claro y en oscuro. No es ninguno de los huecos congelados F15–F22 (verifiqué §6…
  • Sugerencia del auditor: Traducirlo — y de paso reescribirlo, porque tal como está describe un efecto que no ocurre (ver el hallazgo alta de este mismo fichero).

[BAJA] faq — percepcion

El typedoc de Faq.Header promete un Box «centered» que el propio root desmiente: la raíz apila con align="stretch", así que la cabecera queda pegada al inicio en cuanto el container es más ancho que 48rem.

  • Fichero: src/uix/blocks/faq/types.ts
  • Evidencia: faq/types.ts:17 dice «Wraps a Box (measure-cap, centered)». faq-header.svelte:12 es <Box {maxWidth} width="100%"> (Box no emite margen auto salvo marginX) y faq.svelte:23 es <Stack gap={8} align="stretch">. Los DOS bloques con el MISMO comentario sí centran: pricing.svelte:39 y testimonials.svelte:22 usan align="center" — faq copió la frase sin la alineación. Medido en Chrome (localhost:5180/blocks/faq/preview, viewport 1280): con el default (container='md') coinciden (container 768@x251, header 736@x267); emulando container="lg" (--box-max-width:1024px) el contenido va 139→1147 (centro 643) y la cabecera queda 768@x139 (centro 523) = 120px descentrada respecto al aco…
  • Veredicto: Se sostiene. Las cuatro fuentes dicen lo que el auditor afirma, y el navegador confirma que lo que se pinta es SOLID. types.ts:40 declara «Wraps a soft Surface chip» siguiendo el mismo patron con que sus lineas hermanas declaran defaults («Wraps a Stack. align defaults to start», «Wraps a muted Text»), asi que el adjetivo ES el default declarado, no prosa. La demo repite «el chip (Surface soft)» en +page.svelte:134. El codigo (feature-grid-item-icon.svelte:12) pone `variant = 'solid'…
  • Sugerencia del auditor: O centrar de verdad (marginX="auto" en el Box de faq-header.svelte, o align="center" en el Stack raíz como pricing/testimonials) o borrar «centered» del typedoc. Es una línea, pero hay que decidir cuál de las dos es la intención.

[BAJA] faq — doctrina

B-8 incumplido: el block emite un encabezado por pregunta con aria-level FIJO en 3, no ofrece forma de ajustarlo y el README no documenta ni el landmark ni la jerarquía de encabezados.

  • Fichero: src/uix/blocks/faq/README.md
  • Evidencia: faq-item.svelte:17 renderiza <Accordion.Header> desnudo; FaqItemProps (faq/types.ts:24-33) tiene 4 props y ninguna es level, y .Item no hace spread de ...rest (faq-item.svelte:10), así que no hay escotilla. El nivel sale del default del canon (src/uix/soma/components/accordion/components/accordion-header.svelte:16 → level = 3). Medido en Chrome (/blocks/faq/preview): 5 × DIV[role=heading][aria-level=3]. B-8 exige «heading hierarchy coherent and documented in the block README (which level it emits, how to adjust)» y el README de faq no tiene línea de landmark/heading — su mapa de composición (línea 13) solo dice «the app's section Heading + Text». El resto del tier s…
  • Veredicto: Se sostiene. El tipo publico documenta un default que el componente nunca usa: types.ts:26 dice «Wraps a Card (the quote card). variant defaults to soft.» mientras testimonials-item.svelte:13 destructura variant = 'outline' y lo reenvia con <Card {variant} …> (variant sale del rest, asi que ...rest no puede reponerlo). No es regresion: git show b59332132 (commit que crea el block) ya trae 'outline' en el .svelte y 'soft' en el tipo — el origen de la deriva es que la frase copia el …
  • Sugerencia del auditor: Añadir la línea «Landmark + heading» al README (section sin nombre accesible + role=heading aria-level=3 por pregunta) y exponer level en FaqItemProps reenviándolo a Accordion.Header, para una página que meta el FAQ bajo un h3.

[BAJA] stats-band — doctrina

El typedoc de countOptions anuncia una opción style que CountUp NO acepta — pasarla es error de tipos.

  • Fichero: src/uix/blocks/stats-band/types.ts
  • Evidencia: stats-band/types.ts:38-40: «Everything CountUp takes (from, duration, notation, style…)». Pero src/uix/eidos/components/count-up/types.ts:3 es Omit<HTMLAttributes<HTMLElement>, 'children' | 'style'> — style está EXCLUIDA a propósito; la prop de formato se llama formatStyle (count-up/types.ts:39, count-up.svelte:31).
  • Veredicto: Se sostiene la parte observable, no la causal. HECHOS VERIFICADOS: (1) el calco existe — SiteHeaderSite.svelte:25-26 declara ContainerSize = 'sm'|'md'|'lg'|'xl'|'full' y Breakpoint = 'sm'|'md'|'lg', contra canon container/types.ts:4 (6 valores, con 'xxl') y libs/dom/responsive.ts:1 (6 valores); mismo calco en site-header/+page.svelte:24-25, site-header/preview/+page.svelte:13-14, hero/HeroSite.svelte:30-31 y hero/preview/+page.svelte:13-14; el block SI importa del canon (blocks/site-header/t…
  • Sugerencia del auditor: Cambiar style por formatStyle en el comentario (que además es la que el ejemplo quiere: contar hacia una moneda o un porcentaje).

[BAJA] stats-band — doctrina

El README no documenta landmark ni jerarquía de encabezados (B-8); el dato solo vive en la pestaña A11y de la demo.

  • Fichero: src/uix/blocks/stats-band/README.md
  • Evidencia: grep -ci 'heading|landmark' src/uix/blocks/stats-band/README.md = 0 (el único README del tier con 0; hero=4, team=4, banner=4, site-footer=6). El block emite <section> desnudo sin nombre accesible (stats-band.svelte:26) y ningún encabezado. B-8 (docs/architecture/blocks.md:126) pide que la jerarquía esté documentada EN EL README del block. La frase existe, pero en la demo: web/routes/blocks/stats-band/+page.svelte:154-157 «Sin encabezados: una banda son datos…».
  • Veredicto: El hallazgo se sostiene. src/uix/blocks/pricing/index.ts:12 muestra <Pricing.Switch annualLabel="Anual (−20%)" /> mientras que src/uix/blocks/pricing/types.ts:34-41 declara monthlyLabel: string, annualLabel: string y 'aria-label': string las tres SIN ?, y src/uix/blocks/pricing/pricing-switch.svelte:15 las desestructura SIN defaults (con un comentario, lineas 11-14, que declara la ausencia deliberada bajo B-7). Intente refutarlo por tres vias y las tres fallan: (1) no es una elis…
  • Sugerencia del auditor: Subir esa misma frase al README (una línea bajo el mapa de composición): «section sin nombre accesible, sin encabezados; si la página necesita título, lo pone alrededor».

[BAJA] pricing — doctrina

Pulsar el segmento de periodo YA ACTIVO devuelve la sección entera al periodo contrario: el block posee el estado y su propio control lo mueve a un valor que el usuario no eligió, sin decir nada.

  • Fichero: src/uix/blocks/pricing/pricing-switch.svelte
  • Evidencia: Medido en Chrome (localhost:5180/blocks/pricing/preview, 1280x900), pulsando dos veces «Anual (−20 %)»: before {Mensual=on, precios 0/29/99 €} → click1 {Anual=on, 0/23/79 €} → click2 sobre el MISMO «Anual» {Mensual=on, 0/29/99 €}. Cadena exacta: pricing-switch.svelte:20-26 compone <ToggleGroup selectionMode="single"> SIN deselectable={false}, y el default de soma es deselectable: true (src/uix/soma/components/toggle-group/types.ts:17-23: «In single mode, whether re-pressing the active item deselects it … @default true»). La re-pulsación entrega onValueChange([]) y la línea 25 lo traga: ctx?.setPeriod((v[0] ?? 'monthly') as BillingPeriod). `grep -rn deselectable src/uix/blocks…
  • Veredicto: SE SOSTIENE LA PREMISA, SE REFUTA LA CONCLUSIÓN. Cierto y reproducible: container="full" NO produce sangre. SIZE_TO_WIDTH.full es var(--container-width-full) = none (G:/dev/svelte/vicen/src/uix/eidos/generated/base.css:144), es decir solo quita el TOPE de anchura; el padding inline es independiente y siempre se aplica — container.svelte:49 paddingX ?? 'var(--container-padding-inline)' = --space-4 = 16px (base.css:145 y :87). cta.svelte:104 monta <Container size={container}> sin…
  • Sugerencia del auditor: Un periodo de facturación es radio-like, no clearable: pasar deselectable={false} en pricing-switch.svelte:20 (o el canónico enabledSelections={[1,1]}) y quitar el ?? 'monthly' de la línea 25, que hoy convierte silenciosamente «selección vacía» en «mensual». Doctrina §Coordination: el estado que la sección posee no puede cambiar por un camino que la pantalla no explica.

[BAJA] pricing — percepcion

Las tarjetas de plan NO son de igual alto y los CTA NO se alinean: height="100%" sobre Card es un atributo HTML inerte, no una prop. El README y la demo afirman exactamente lo contrario.

  • Fichero: src/uix/blocks/pricing/pricing-plan.svelte
  • Evidencia: Medido en Chrome (preview de pricing, 1280x900, con el gate de Motion neutralizado para que la visibilidad no confunda la lectura): wrapper de rejilla 505/505/505 px, pero [data-card] 388/505/461 px, y los bottoms de los tres [data-button] en y=667 / 784 / 741 → 117 px de desfase. El DOM lo delata: <div data-card="" data-variant="outline" … height="100%"> — height quedó como atributo crudo sobre un <div> (getComputedStyle(card).height = 387.875px). Origen en canon: src/uix/eidos/components/card/types.ts:61,66 declara CardProps = Omit<BoxProps,'display'> y dice «Composes through <Box> so consumers can apply layout primitives (gridColumn, margin, gap, etc.)», pero `card.s…
  • Veredicto: SE SOSTIENE EL NÚCLEO MECÁNICO, PERO LA MITAD DE LA EVIDENCIA ES FALSA Y EL ENCUADRE «doctrina» ESTÁ INFLADO. 1) VERDADERO y probado: la autogeneración del block es duplicación inerte. El canon ya la resuelve por la MISMA ruta de composición: src/uix/soma/components/accordion/components/accordion-item.svelte:16 (value = useId('accordion-item-value')), y el wrapper eidos la deja pasar tal cual — src/uix/eidos/components/accordion/accordion-item.svelte:5,8 desestructura `{ children, ...rest …
  • Sugerencia del auditor: Es una brecha del canon a promover, no a parchear dentro del block (Card debe aceptar de verdad su superficie BoxProps o dejar de declararla). Mientras tanto, o el block deja de prometer alineación en README+demo, o el estirado se consigue con un componente de layout que sí lo cumpla. Nota: pricing-plan-action.svelte:14 (marginTop="auto") sí funciona — empuja el botón al borde de SU tarjet…

[BAJA] pricing — doctrina

La demo documenta una elevación que el código se niega explícitamente a hacer y que el propio README lista como brecha abierta del canon.

  • Fichero: web/routes/blocks/pricing/+page.svelte
  • Evidencia: web/routes/blocks/pricing/+page.svelte:53: «featured le da acento y elevación, y un badge encima». Contra src/uix/blocks/pricing/pricing-plan.svelte:8-11: «The featured tier is NOT elevated: Card exposes no elevation prop, and a block paints nothing of its own … Registered as a gap in the README instead of faked with an inline box-shadow». Y contra src/uix/blocks/pricing/README.md:74: «Elevation on the featured plan — canon candidate … hasta entonces featured es color only».
  • Veredicto: Se sostiene la MITAD de encabezados; refuto la de landmark. CIERTO: el README de stats-band no dice ni una palabra sobre jerarquía de encabezados, y es el único del tier con 0 coincidencias de 'heading|landmark'. La obligación no es solo B-8 (docs/architecture/blocks.md:126) — la plantilla del tier la exige literalmente en src/uix/blocks/README.md:78-79 ("Landmark + heading hierarchy: which sectioning element / heading level the block emits and how the app adjusts it", justo tras el Composition …
  • Sugerencia del auditor: Quitar «y elevación» de la línea 53 de la demo. La única superficie donde el visitante lee la doctrina del block no puede contradecir la decisión firmada.

[BAJA] pricing — doctrina

El block exige a la app tres cadenas obligatorias para nombrar el ÚNICO estado que coordina, y justifica esa exigencia citando una lectura de B-7 que la enmienda del 2026-07-31 sustituyó.

  • Fichero: src/uix/blocks/pricing/pricing-switch.svelte
  • Evidencia: pricing-switch.svelte:11-15 comenta «NO defaults for the three strings: a block ships zero words of its own (B-7)» y types.ts:29-41 hace monthlyLabel: string; annualLabel: string; 'aria-label': string REQUERIDAS. Pero docs/architecture/blocks.md:125 (B-7) dice: «Amended 2026-07-31: a block DOES own the words of the states it coordinates (and only those), as idlangrefs with an English fallback resolved through the translator (#?blocks.<block>.<key>|…)», y :141 sanciona el traductor como el único servicio permitido «to resolve the block's own state words». El periodo de facturación ES el estado que Pricing coordina (context.ts:12-17) y monthlyLabel/annualLabel son literal…
  • Veredicto: El ejemplo del front door del block anuncia una prop que la API no tiene y que el propio block documenta como CORTADA a propósito. src/uix/blocks/cta/index.ts:11 reza // <Cta layout="justified" variant="soft">…</Cta>, pero CtaProps (src/uix/blocks/cta/types.ts:15-61) no declara variant y su base es Omit<HTMLAttributes<HTMLElement>, 'children' | 'title'> — comprobado en node_modules/svelte/elements.d.ts:751-858, ese interface NO tiene índice [key: string]: any (solo [key: \data-…
  • Sugerencia del auditor: Volver las tres cadenas OPCIONALES con idlangref + fallback inglés ('#?blocks.pricing.period.monthly|Monthly', …annual|Annual, …switch.label|Billing period) resueltos con ActiveEidos.require().langs.t, como hace contact; la app las sigue sustituyendo pasando las suyas. Y actualizar el comentario, que hoy cita la versión derogada de B-7.

[BAJA] testimonials — doctrina

El tipo público documenta un default de variant que el componente no usa.

  • Fichero: src/uix/blocks/testimonials/types.ts
  • Evidencia: types.ts:26: «Wraps a Card (the quote card). variant defaults to soft.» Contra testimonials-item.svelte:13: variant = 'outline',. El README acierta (README.md:16: «wrapping a Card (outline)»), así que la deriva está solo en el tipo — el sitio donde la lee el editor.
  • Veredicto: Se sostiene. El typedoc de countOptions en src/uix/blocks/stats-band/types.ts:38 lista style como una de las props que «CountUp takes», pero CountUpProps la excluye explicitamente (src/uix/eidos/components/count-up/types.ts:3 = Omit<HTMLAttributes, 'children' | 'style'>) y la prop de formato real se llama formatStyle (count-up/types.ts:40, destructurada en count-up.svelte:31 y usada como style: formatStyle hacia Intl.NumberFormatOptions en formatValue). Pasar style dentro …
  • Sugerencia del auditor: Corregir a outline en types.ts:26.

[BAJA] testimonials — doctrina

.Items tipa toda la superficie de AutoGrid pero descarta en silencio parte de ella: el spread del consumidor va ANTES de props fijadas a mano.

  • Fichero: src/uix/blocks/testimonials/testimonials-items.svelte
  • Evidencia: testimonials-items.svelte:11-19: <AutoGrid {...rest} minChildWidth={…} {columns} {gap} align="stretch" width="100%" data-stagger>. Como TestimonialsItemsProps = AutoGridProps (types.ts:24) y AutoGridProps = Omit<GridProps,…> → GridProps → BoxProps, escribir <Testimonials.Items align="start" width="60ch"> tipa perfecto y no hace absolutamente nada. El block hermano hace lo contrario en el mismo directorio: testimonials-item.svelte:24 pone {...rest} AL FINAL, así que ahí el consumidor sí manda. pricing-plans.svelte evita el problema declarando un tipo cerrado sin rest (types.ts:44-52).
  • Veredicto: El ejemplo de la cabecera de src/uix/blocks/newsletter/index.ts:5 enseña <Newsletter {form} {schema} onValidSubmit={subscribe}>, y NewsletterProps NO tiene ese prop: comprobado con tsc, no razonado. NewsletterProps = Omit<HTMLAttributes<HTMLElement>,'children'|'title'> & {…} y HTMLAttributes de Svelte solo trae dos índices — [key: \data-${string}`]y[key: symbol](node_modules/svelte/elements.d.ts:853-857) — no hay comodínon*`, así que el nombre no entra por ningún hueco del…
  • Sugerencia del auditor: O cerrar el tipo a las props que el block realmente honra (como hace PricingPlansProps), o mover {...rest} al final y aceptar que align/width/data-stagger son sobreescribibles. Prometer y descartar es lo único que no vale.

[BAJA] pricing · testimonials — percepcion

Las vistas de 375/768 de estas dos demos no heredan NINGUNO de los ejes de la sección: siempre salen en claro, español y ltr — justo la rama estrecha que solo puede verse ahí.

  • Fichero: web/routes/blocks/pricing/+page.svelte
  • Evidencia: web/routes/blocks/pricing/+page.svelte:14 (const previewSrc = '/blocks/pricing/preview') y web/routes/blocks/testimonials/+page.svelte:12 pasan la ruta pelada, sin query. El +layout@.svelte del preview lee los ejes SOLO de la URL (page.url.searchParams.get('mode'|'dir'|'lang'), con default light/ltr/es), y _lib/BlockDemo.svelte:146 usa src={previewSrc} tal cual sin añadir nada. Todas las demás demos pasan al menos dir (banner:22, hero:28-29, contact:20, newsletter:22, site-footer:20, site-header:29, feature-split:20). Contradice src/uix/blocks/README.md:60-61: «The section's axes (colour mode, language, direction, density) live in the shell … so every demo inherits th…
  • Veredicto: Se sostiene el NUCLEO, pero la solucion que propone el auditor es medible y falsa, y hay que corregirla o el "arreglo" cambia el diseno. LO QUE HOLDS: (1) 60ch no es escalon de la escala (--measure-narrow:54ch · normal:66ch · wide:78ch, base.css:323-325); (2) el canon declara que ESTE es el vocabulario del tier: src/uix/eidos/lib/types.ts:127-128 dice literalmente que «the blocks tier sizes a reading column with TextMeasure»; (3) un block hermano YA lo obedece: `content-section/grid.ts:31-…
  • Sugerencia del auditor: Propagar mode/lang/dir del shell al previewSrc — mejor en _lib/BlockDemo.svelte, que es donde el problema es sistémico, para que ningún demo tenga que acordarse. Sin eso, la verificación visual pendiente de «light/dark × RTL a 375/768» no es ejecutable desde la galería.

Sin verificar (50) — NO son hallazgos hasta refutarlos

Severidad alta

  • newsletter (doctrina) — UNA sola pulsación de Enter dentro del campo de correo emite DOS commit-submit con intents contradictorios (neutral y fulfill) y suenan DOS earcons a 19 ms: la composición Form > Field duplica el mismo verbo. · src/uix/blocks/newsletter/newsletter.svelte:105
  • newsletter (doctrina) — signal-warn-invalid está declarado persistence:'untilFix' pero NUNCA se limpia al arreglar: corregido el correo, el form sigue estampado en risk indefinidamente mientras el texto de error y aria-invalid ya dicen que está resuelto. · src/uix/soma/components/form/form-provider.svelte.ts:129
  • site-header (percepcion) — El pack de sema de NavigationMenu NUNCA casa: su regla de cascada apunta a la parte item y el estampado cae siempre en el trigger, asi que el nav suena con la ganancia BASE de la familia commit (0.30) en vez de con el tuning form.commit.subtle (0.03) elegido para navegacion de alta frecuencia — 10x mas fuerte de lo disenado. · src/uix/sema/components/navigation-menu.ts
  • site-header (percepcion) — El nav del header emite commit-select + affirm — con earcon audible — por el simple HOVER del puntero, sin un solo clic: un barrido del raton sobre la barra dispara dos confirmaciones afirmativas de algo que el usuario no ha fijado. · src/uix/soma/components/navigation-menu/navigation-menu-provider.svelte.ts
  • site-header (percepcion) — Inversion de dominancia atencional: pasar el raton por encima del nav suena mas largo y mas fuerte que pulsar el CTA principal del block, y con timbre indistinguible (misma fundamental y mismo intervalo). · web/routes/blocks/site-header/SiteHeaderSite.svelte
  • site-header (percepcion) — MUDOS: 4 de los 7 elementos interactivos visibles del header no emiten NADA, y son precisamente los que navegan — el brand «Acme», los NavigationMenu.Link «Precios» y «Docs», y el Link «Entrar». En el drawer, los 5 enlaces de navegacion tambien. La navegacion real es silenciosa mientras el despliegue del menu canta. · web/routes/blocks/site-header/SiteHeaderSite.svelte
  • site-header (percepcion) — Carrera de estampado en el trigger del drawer: contact-activate vive 3 ms antes de que emerge.open lo pise en el MISMO nodo, asi que el feedback de pulsacion del boton hamburguesa se trunca 40x por debajo de su hold canonico. · src/uix/blocks/site-header/site-header.svelte
  • banner (doctrina) — El block cablea el descarte con variant="ghost" y sin tinta, así que la ✕ se pinta SIEMPRE con el ink primary del Button sobre el lienzo que el propio block eligió — invisible en la forma que su README dice haber adoptado. · src/uix/blocks/banner/banner.svelte
  • banner (percepcion) — La pestaña «Notas» afirma que la acción en línea HEREDA la tinta de contraste de la tira «en todos los roles y en los dos modos». La medición lo refuta: el camino de color custom OSCURECE la semilla un 30 %, y el hover ignora el color del link por completo. · web/routes/blocks/banner/+page.svelte
  • newsletter (doctrina) — El ejemplo de uso de la puerta de entrada del block anuncia onValidSubmit — exactamente el prop que el block decidió NO exponer porque es un no-op silencioso, y que su propio tipo rechaza. · src/uix/blocks/newsletter/index.ts
  • contact (percepcion) — El clic sobre «Enviar mensaje» —la accion principal de la seccion— no estampa NINGUN evento sema en el momento del gesto; la unica emision llega 915 ms despues, cuando el envio ya ha resuelto. · src/uix/blocks/contact/contact-submit.svelte
  • contact (percepcion) — Un solo Enter dentro de un campo estampa DOS commits con intents distintos; y cuando el envio esta bloqueado estampa un commit que no commitea nada. · src/uix/morfo/components/field.ts
  • contact (percepcion) — El nombre accesible del boton de envio queda congelado en «Enviar» en los cinco estados: las palabras de estado que el block posee por B-7 nunca llegan a la tecnologia asistiva. · src/uix/blocks/contact/contact-submit.svelte
  • contact (percepcion) — Contact.Reason —la frase que el block existe para que ningun estado bloqueado quede mudo— no es region viva ni esta enlazada al control: cambia sin que nada la anuncie. · src/uix/blocks/contact/contact-reason.svelte
  • contact (doctrina) — Con los tres campos rellenos y el correo mal formado, la seccion afirma «Completa todos los campos para enviar.» y no marca el campo: el camino de error del propio block es inalcanzable por construccion. · src/uix/blocks/contact/contact-fields.svelte
  • pricing (percepcion) — Un solo clic sobre el periodo YA activo VACIA el segmented control: la seccion queda sin ningun periodo marcado mientras los precios siguen siendo los mensuales — el unico control que coordina el block miente sobre el estado de su seccion. · src/uix/blocks/pricing/pricing-switch.svelte
  • cta (percepcion) — La accion secundaria del cluster («Hablar con ventas») es MUDA: no emite ningun evento sema, y en reposo es tipograficamente identica al parrafo de la descripcion — ni se siente ni se ve como accion. · web/routes/blocks/cta/CtaSite.svelte:72
  • cta (percepcion) — El anillo de foco es practicamente invisible sobre el panel solido: los DOS unicos elementos interactivos del block quedan sin indicador de foco por teclado. · src/uix/blocks/cta/cta.svelte:111

Severidad media

  • banner (percepcion) — El descarte —el ÚNICO gesto que el block cablea— sólo expresa contact-activate (una pulsación); la desaparición de la tira no la expresa nadie, y ese estampado además cae sobre un nodo ya fuera del documento, así que su canal visual no puede pintarse. · src/uix/blocks/banner/banner.svelte:59
  • banner (percepcion) — La única llamada a la acción del banner («Ver qué cambia →») es completamente muda: ni estampa ni suena, en clic, en Enter ni en hover. · web/routes/blocks/banner/BannerSite.svelte:56
  • site-footer (percepcion) — 18 de los 24 elementos interactivos del footer son Link y TODOS son mudos: el block cuya función entera es navegar no tiene canal perceptivo para su gesto principal, mientras sus tres iconos decorativos sí suenan. · src/uix/blocks/site-footer/site-footer.svelte:77
  • newsletter (doctrina) — La acción principal del block no tiene contact propio: pulsar «Suscribirme» no estampa NADA sobre el control — el único estampado es el commit del <form> —, así que se incumple la regla de composición «contact precede al resultado cuando hay acción directa». · src/uix/morfo/components/form.ts:90
  • site-header (percepcion) — La costura mobileOpen cierra el drawer en SILENCIO: el unico camino de cierre que el block documenta como suyo es el unico que no se anuncia, mientras Escape y overlay si emiten emerge.close. · src/uix/blocks/site-header/site-header.svelte
  • hero (percepcion) — Las dos acciones del hero viven en el mismo Group, con la misma jerarquia de par primario/secundario, y responden de forma opuesta: el CTA emite contact-activate y el enlace hermano es MUDO. · web/routes/blocks/hero/HeroSite.svelte
  • site-header (doctrina) — El morfo de NavigationMenu carga el intent evaluable en el MARCO: declara un unico evento, commit-select + affirm, para lo que solo despliega un panel, y no declara ninguno para su parte Link, que es el acto evaluable de verdad (navegar). · src/uix/morfo/components/navigation-menu.ts
  • banner (doctrina) — «full-bleed por fuera, en columna por dentro» deja de ser cierto en cuanto se pasa onDismiss: el margin-inline-start:auto del Close es un TERCER margen automático y el flex reparte el hueco libre en tres, descentrando la columna del mensaje. · src/uix/blocks/banner/banner.svelte
  • banner (doctrina) — B-9 exige el block A SANGRE en la página de demo, «nunca dentro de un marco con padding». En la galería el block se pinta dentro del raíl del catálogo: para banner, cuya función ES la tira de borde a borde sobre la cabecera, eso cambia lo que el block hace. · web/routes/blocks/_lib/BlockDemo.svelte
  • newsletter (percepcion) — Con panel={false} la nota de privacidad se pinta con MÁS fuerza que la línea de apoyo: la descripción cae a muted y la nota se queda con la tinta plena, invirtiendo la jerarquía. · src/uix/blocks/newsletter/newsletter.svelte
  • site-footer (doctrina) — La demo construye tintas a mano en app-land (color="var(--color-content-*)") y son INERTES: con variant="subtle" el recipe del Link fuerza color: inherit con más especificidad, así que todos los enlaces del pie salen a tinta plena y los legales acaban más oscuros que el copyright que tienen al lado. · web/routes/blocks/site-footer/SiteFooterSite.svelte
  • site-footer (percepcion) — El título del grupo se pinta MÁS PEQUEÑO que los enlaces que nombra y con la misma tinta: la única señal de jerarquía que queda es el peso. · src/uix/blocks/site-footer/site-footer-column-title.svelte
  • site-footer (percepcion) — «Tantas columnas como caben, el mismo código para 2 o para 5 grupos» describe el código, no el resultado: AutoGrid usa auto-fill, que crea las pistas vacías igualmente, así que con 2 grupos las columnas se apelotonan al inicio y casi dos tercios de la región quedan muertos. · src/uix/blocks/site-footer/site-footer.svelte
  • contact (percepcion) — El submit bloqueado sale del orden de tabulacion, asi que quien navega con teclado nunca llega al control ni se topa con la frase que lo explica. · src/uix/blocks/contact/contact-submit.svelte
  • team (percepcion) — Los 12 IconButton que el block pone en escena estampan contact-activate y no hacen absolutamente nada: el contacto inicia y nada resuelve, doce veces. · web/routes/blocks/team/TeamSite.svelte
  • contact (percepcion) — El Link «hola@vicen.dev» dentro de Contact.Detail es mudo: es el unico elemento interactivo de la columna de datos y no emite nada. · web/routes/blocks/contact/ContactSite.svelte
  • pricing (percepcion) — Ese mismo clic muerto emite commit-toggle + intent neutral: el sistema anuncia que algo quedo fijado cuando la seccion no cambio de estado (los precios no se mueven). · src/uix/blocks/pricing/pricing-switch.svelte
  • pricing (percepcion) — Un solo gesto sobre el CTA de un plan produce DOS eventos sema sobre el MISMO elemento a 1 ms: el commit-select de la app pisa el contact-activate del Button y la microcompresion del press (press-squeeze) no llega a arrancar nunca. · web/routes/blocks/pricing/PricingSite.svelte
  • pricing (percepcion) — El plan destacado se grada fulfill en lo visual pero su consecuencia suena affirm, exactamente igual que los otros dos planes: la lectura unificada se parte en el gesto mas importante de la seccion. · web/routes/blocks/pricing/PricingSite.svelte
  • faq (percepcion) — Un solo clic que cambia de pregunta dispara DOS earcons emerge simultaneos y sin atenuar (collapse descendente + expand ascendente): un gesto, dos sonidos superpuestos de peso casi igual. · src/uix/blocks/faq/faq-list.svelte:14
  • stats-band (percepcion) — La coreografia de la banda esta desincronizada: la ENTRADA escalona, pero los contadores arrancan todos en el mismo frame, asi que una cifra ya va por su 21 % cuando se hace visible. · src/uix/blocks/stats-band/stats-band-value.svelte:20
  • stats-band (percepcion) — Las cuatro cifras no aterrizan nunca juntas y duration no significa segundos: con el valor por defecto (2), la cifra grande tarda 7,9 s en asentarse y la pequena 3,6 s — la banda esta en movimiento ocho segundos. · src/uix/eidos/components/count-up/count-up.svelte:169

Severidad baja

  • feature-grid (percepcion) — DATO, NO DEFECTO: feature-grid no tiene ningun elemento interactivo — nada que emitir y nada que auditar perceptivamente en el. · src/uix/blocks/feature-grid/feature-grid.svelte
  • site-footer (doctrina) — La página de demo pinta el block ANTES de su propio h1, así que el documento arranca en h2/h3 y el h1 llega el último — jerarquía de encabezados incoherente en la superficie donde B-8/B-9 piden que sea coherente. · web/routes/blocks/_lib/BlockDemo.svelte
  • contact (percepcion) — La ventana de vuelo (900 ms) no tiene canal perceptivo propio: ningun sustain.*, ningun spinner — solo el cambio de etiqueta visible. · src/uix/blocks/contact/contact-submit.svelte
  • content-section (percepcion) — DATO, no defecto: el block no pone NINGUN elemento interactivo — cero en toda su region — y nada en el estampa sema. · web/routes/blocks/content-section/ContentSectionSite.svelte
  • feature-split (percepcion) — Los 3 unicos elementos interactivos del block emiten su contact correctamente, pero no llevan a ningun sitio: los href apuntan a fragmentos que no existen en el documento, asi que el arco perceptivo muere en «te he sentido» y no ocurre nada. · web/routes/blocks/feature-split/FeatureSplitSite.svelte
  • pricing (percepcion) — Volver a pulsar el CTA de un plan YA elegido re-emite un commit-select+affirm identico: no hay diferencia perceptiva entre «acabas de elegirlo» y «ya estaba elegido». · web/routes/blocks/pricing/PricingSite.svelte
  • testimonials (percepcion) — DATO, no defecto: el block no pone NINGUN elemento interactivo — no hay nada que pulsar y, por tanto, nada mudo; ademas sus Card no fingen afordancia. · src/uix/blocks/testimonials/testimonials.svelte
  • stats-band (percepcion) — DATO, no defecto: el block no pone NINGUN elemento interactivo — no hay nada que pulsar y por tanto nada que estampar. Toda su superficie perceptiva es la entrada (Motion viewport + data-stagger + CountUp); no se midio ni un solo data-event originado en el block. · src/uix/blocks/stats-band/stats-band.svelte
  • stats-band (doctrina) — El
    del block sale sin nombre accesible Y sin ningun encabezado dentro, asi que no es una region navegable ni por landmarks ni por titulares; B-8 pide landmark correcto y jerarquia documentada, y el README no dice nada de ninguna de las dos. · src/uix/blocks/stats-band/stats-band.svelte:26

Severidad sospecha

  • site-footer (percepcion) — Cambiar de idioma en el selector del footer cambia el régimen de toda la página (langs, formatos, dirección) pero suena y se estampa exactamente igual que elegir un ítem cualquiera de una lista: commit-select + affirm, sin ningún shift. · web/routes/blocks/site-footer/SiteFooterSite.svelte:164

Refutados (11) — NO volver a reportarlos

  • faq — El block reimplementa la autogeneración del value que el canon ya hace, y README+index la venden como «la única conveniencia» de .Item.
    • Por qué no se sostiene: Las dos premisas del hallazgo fallan al medirlas. (1) «El README no declara landmark» es FALSO. La fila root del Composition map lo declara: src/uix/blocks/feature-grid/README.md:13 → «root | bare <section> + Section + Container + Stack». Lo que no hay es una frase sobre el NOMBRE accesi…
  • cta — El ejemplo del index.ts anuncia variant="soft", una prop que se CORTÓ a propósito: copiarlo es un error de tipos.
    • Por qué no se sostiene: Los hechos citados son exactos (las tres props son requeridas y el comentario invoca B-7 en términos absolutos), pero la conclusión doctrinal es falsa: la enmienda del 2026-07-31 NO sustituyó la lectura que aplica a pricing, y el proyecto ya resolvió este caso concreto por escrito y con posteriori…
  • cta — La demo redeclara en app-land la unión que el block EXPORTA (CtaLayout), duplicando vocabulario.
    • Por qué no se sostiene: La afirmación, tal y como está redactada («ninguno de los tres README declara el landmark ni la jerarquía de encabezados»), es falsa en sus dos mitades, y la evidencia de apoyo contiene un error de hecho y una insinuación de código que se mide como no-problema. 1) EL LANDMARK SÍ ESTÁ DECLARADO en lo…
  • hero — El block deriva el estado «la copia va sobre sólido» y lo aplica solo a dos de sus piezas, sin publicarlo: cada consumidor lo re-deriva en el punto de uso — el patrón exacto que la doctrina de coordinación señala como generador de huecos.
    • Por qué no se sostiene: REFUTADO como hallazgo de pricing/testimonials. Dos de las tres piezas del hallazgo se caen: 1. El diferenciador es falso EN EFECTO. El auditor apoya la acusación en «todas las demás demos pasan al menos dir». Ese dir no es el eje de la sección: es un $state local de cada demo, con default…
  • feature-grid — align está aplicado a medias: el block declara la prop y solo mueve la caja de la cabecera; el propio tipo instruye al app a repetir la alineación en tres sitios más, y la demo lo repite tres veces.
    • Por qué no se sostiene: Los HECHOS mecánicos son ciertos, pero la acusación (doctrina: «aplicado a medias» + «el README lo niega») no se sostiene. (1) La cita del README es un error de alcance: src/uix/blocks/feature-grid/README.md:34-40 es la sección «Coordination», que remite explícitamente a `docs/architecture/blocks.…
  • site-header — El aria-label que la demo escribe para el trigger del cajón nunca llega al DOM: el lector de pantalla anuncia la etiqueta genérica del canon.
    • Por qué no se sostiene: No se sostiene como defecto de doctrina, por cuatro razones medidas. (1) La propia doctrina exime a faq POR SU NOMBRE: docs/architecture/blocks.md:82-86 lo lista entre los blocks de layout que «own nothing» y dice que inventarles estado «would be the opposite mistake»; la regla «fires where a se…
  • pricing — El ejemplo de uso de la superficie pública del block no compila: omite dos props obligatorias.
    • Por qué no se sostiene: El HECHO citado es cierto (las tres líneas existen literalmente), pero NO es un defecto del block cta ni sostiene su consecuencia. (1) La duplicación está SOLO en app-land: el block es fuente única — src/uix/blocks/cta/types.ts:10 declara CtaLayout, cta.svelte:35 consume CtaProps importado…
  • contact — El propio gating del block deja INALCANZABLE el error por campo: con progressive los errores solo se escriben tras un envío inválido, y el envío inválido no puede ocurrir porque el botón está deshabilitado salvo en ready. Resultado medido: con nombre «A» y correo «roto» no hay ni un ErrorText, ni un aria-invalid, y la única frase en pantalla es «Completa todos los campos para enviar.» — falsa, porque los tres campos están llenos.
    • Por qué no se sostiene: El fenomeno tecnico existe (el anuncio lleva String(value) y la pantalla el formato de locale), pero el hallazgo NO se sostiene como defecto del block ni como asunto de doctrina B en stats-band-value.svelte: 1. ATRIBUCION ERRONEA. Es comportamiento del canon Metrics, reproducible en su propia …
  • team — marginTop: 'auto' no ancla nada: el ítem de la retícula es el <div> del Motion (block, estirado), y el Stack de dentro mide su contenido, así que no hay espacio libre que el margen automático pueda comerse. Con biografías de distinto largo —el caso exacto para el que el README dice que existe la regla— las filas de enlaces quedan a alturas distintas y la retícula se lee descuadrada.
    • Por qué no se sostiene: Los hechos del DOM se confirman, pero el hallazgo NO se sostiene como defecto de site-footer ni como incumplimiento de B-8/B-9. 1) MAL ATRIBUIDO. La causa está en el shell compartido web/routes/blocks/_lib/BlockDemo.svelte y el propio auditor admite que afecta a las 15 demos. No es una propiedad…
  • contact — sent es un estado TERMINAL del que el block no sabe salir, y lo entra él solo: tras un envío correcto el formulario sigue editable pero la sección ya no puede volver a enviar nunca. El usuario puede reescribir el mensaje entero y el botón se queda en «Enviado», deshabilitado, con «Gracias. Te respondemos por correo.» debajo. La máquina no tiene arista de vuelta ante un cambio de valores.
    • Por qué no se sostiene: La MEDICIÓN es exacta y reproducible (auto-fill en auto-grid.svelte:26, 5 pistas, 443px libres con 2 grupos), pero las tres afirmaciones que la convierten en defecto son falsas: 1. «se apelotonan al inicio» es falso. Medido en las cuatro cuentas: la pista mide lo mismo siempre — 123,59px con…
  • contact — Vocabulario paralelo: ContactVerification re-declara palabra por palabra el ProofOfHumanStatus del canon, y lo hace señalando a ese mismo componente como la fuente. Si el canon renombra o añade un estado, el block no rompe a nivel de tipos — el valor nuevo cae en silencio en el cajón !== 'verified'.
    • Por qué no se sostiene: La tesis del hallazgo —«el raíl del catálogo mete el block en un marco y eso cambia lo que banner hace»— NO se sostiene. Tres razones medidas: (1) B-9 prohíbe una cosa concreta y estrecha: «never inside a padded frame or a scroll box» (docs/architecture/blocks.md:127). Medí la cadena de ancestros …

Powered by TurnKey Linux.