test(uix): record native Container audit baseline

astra
dev 1 week ago
parent 7e5e47a134
commit 6cfe429336

@ -142,16 +142,29 @@ errores/avisos. Gate propia exit 0: 584 archivos/7006 tests, docs 971/
arts 25 limpios, `check:gate` 89 históricos/cero nuevos src/scripts,
apps/base check/build y smoke foundation 1/1 + boot 27/27.
Después, [Container](./audit-uix-2026-09-19/container.md) reviewed/censo 146:
17 canarios positivos y tres sondas `AssertionError` Q31, cero
pendientes, 20 nombres. Huella positiva original/final exacta, 18 PNG
originales inspeccionados/SHA (uno a 350px y tres de sondas). Las
sondas midieron `maxWidth={320}` absorbido por `size="sm"` (480px),
`width={200}` absorbido por `size="full"` (1000px) y `marginLeft={0}`
absorbido por `align="center"` (284px frente a 24px). La decisión de
precedencia/tipo queda diferida. Morfo/wrapper/tipos/CSS/receta/API
intactos. Guard PASS 1/cero errores/avisos. Gate propia exit 0: 585
archivos/7023 tests, docs 971/arts 25 limpios, `check:gate` 89
históricos/cero nuevos src/scripts, apps/base check/build y smoke
foundation 1/1 + boot 27/27.
Esta continuación documenta las fichas de Heading,
Text, Display, Link, Prose, Label, Box, Stack y Group, plan/registro/handoffs, README de Text,
Text, Display, Link, Prose, Label, Box, Stack, Group y Container, plan/registro/handoffs, README de Text,
Display, Prose y Group, comentario de tipo Prose y tests/helpers nuevos.
`.claude/settings.local.json` y `.vitest-attachments/` son del usuario
y siguen excluidos.
## Continuación segura
Container es la siguiente ficha pendiente; su
[ficha previa](./audit-uix-2026-09-19/container.md) registra Q31. Releer antes las cuatro guías
Section es la siguiente ficha pendiente; su
[ficha previa](./audit-uix-2026-09-19/section.md) registra Q32/Q33. Releer antes las cuatro guías
obligatorias de `AGENTS.md`. Mantener el alcance neutral autorizado:
baseline nativa, huella exacta, capturas originales inspeccionadas y
registro separado de defectos. No editar manualmente el árbol congelado
@ -170,7 +183,8 @@ También están ignoradas `tmp/heading-audit-evidence-2026-09-30/`,
`tmp/label-audit-evidence-2026-09-30/` y
`tmp/box-audit-evidence-2026-09-30/` y
`tmp/stack-audit-evidence-2026-09-30/` y
`tmp/group-audit-evidence-2026-09-30/`; sus archivos de imágenes y
`tmp/group-audit-evidence-2026-09-30/` y
`tmp/container-audit-evidence-2026-09-30/`; sus archivos de imágenes y
promociones tampoco deben repetirse.
Para gate/Vitest, apuntar `TEMP` y `TMP` a `node_modules/.cache`, capturar
`LASTEXITCODE` inmediatamente y no mutar archivos durante la gate.

@ -11,7 +11,7 @@ status: reviewed
Workspace `G:\dev\svelte\vicen`, rama `astra`, remoto `gita/astra`.
El [handoff del 30](./HANDOFF-uix-quality-2026-09-30.md) conserva el
contexto de Highlight y del commit previo `5045216d6`. Este handoff
recoge el tramo tipográfico y la continuación hasta Group. Al retomar,
recoge el tramo tipográfico y la continuación hasta Container. Al retomar,
comprobar `HEAD`, `gita/astra` y el estado local antes de seguir; no
depender de un hash escrito dentro de este documento. La fase 6 sigue
abierta; fases 0–5 no cerradas y fase 7
@ -30,20 +30,21 @@ pendiente.
| [Box](./audit-uix-2026-09-19/box.md) | reviewed/143 | 15 positivos, 1 `AssertionError`, 0 pendientes | 14 PNG/SHA, 1 a 350px | Q28: `left` en RTL cambia de lado | 582/6976 |
| [Stack](./audit-uix-2026-09-19/stack.md) | reviewed/144 | 13 positivos, 1 `AssertionError`, 0 pendientes | 12 PNG/SHA, 1 a 350px | Q29: `alignContent` sin efecto con `nowrap` | 583/6989 |
| [Group](./audit-uix-2026-09-19/group.md) | reviewed/145 | 17 positivos, 1 `AssertionError`, 0 pendientes | 17 PNG/SHA, 1 a 350px | Q30: `grow` en columna no iguala anchos | 584/7006 |
| [Container](./audit-uix-2026-09-19/container.md) | reviewed/146 | 17 positivos, 3 `AssertionError`, 0 pendientes | 18 PNG/SHA, 1 a 350px | Q31: precedencia de props de Box | 585/7023 |
Los positivos promovidos conservaron nombres/estados exactos frente a
cada baseline. Los tests y dos helpers por componente viven fuera de
los wrappers; cero bolsas, conexiones o líneas de lógica extraídas.
Morfo, CSS, recetas y APIs ejecutables quedaron intactos. El wrapper
de Box sólo corrigió un comentario obsoleto.
Stack y Group dejaron intactos wrapper, tipos, CSS, receta y API.
Stack, Group y Container dejaron intactos wrapper, tipos, CSS, receta y API.
README de Text/Display sólo corrigió comparativas; README y comentario
de tipo de Prose aclaran que `lg` es fluido entre 18 y 20px con scaling
predeterminado (19,55px medidos a 1100px). Group actualizó la referencia
de Chakra HStack a Chakra Group en README y ficha. Los defectos Q22–Q30
de Chakra HStack a Chakra Group en README y ficha. Los defectos Q22–Q31
seguían en el registro y se extendieron con sondas, sin reparación.
La última gate completa, de Group, salió con exit 0: 584 archivos/7006
La última gate completa, de Container, salió con exit 0: 585 archivos/7023
tests, docs 971/arts 25 limpios, `check:gate` 89 errores históricos
dentro del ledger y cero nuevos src/scripts, apps/base check/build y
smoke foundation 1/1 + boot 27/27. Tras el cierre documental, repetir
@ -53,8 +54,8 @@ paridad externa.
## Continuación segura
[Container](./audit-uix-2026-09-19/container.md) es el siguiente candidato;
la ficha previa registra Q31 (precedencia de props de Box). Antes de
[Section](./audit-uix-2026-09-19/section.md) es el siguiente candidato;
la ficha previa registra Q32/Q33. Antes de
crear o modificar el componente, releer las cuatro guías obligatorias
de `AGENTS.md`. Mantener auditoría neutral: preflight, baseline nativa,
huella exacta, capturas originales inspeccionadas y hallazgos diferidos
@ -65,7 +66,7 @@ el inventario y las decisiones.
No editar manualmente `web/` ni usar reset/stash/checkout. Los cambios
locales `.claude/settings.local.json` y `.vitest-attachments/` son del
usuario y se excluyeron de los commits de este lote. La evidencia
ignorada está en `tmp/{heading,text,display,link,prose,label,box,stack,group}-audit-evidence-2026-09-30/`;
ignorada está en `tmp/{heading,text,display,link,prose,label,box,stack,group,container}-audit-evidence-2026-09-30/`;
archivo de capturas y promoción ya se ejecutaron, así que no repetir
esas mutaciones. Para gate/Vitest, apuntar `TEMP` y `TMP` a
`node_modules/.cache`, capturar `$LASTEXITCODE` inmediatamente y no

@ -3769,3 +3769,25 @@ exit 0: 584 archivos/7006 pruebas, docs 971/arts 25 limpios,
check/build y smoke foundation 1/1 + boot 27/27. Fase 6 abierta,
fases 0–5 sin cerrar y fase 7 pendiente. Container/Q31 es el siguiente
candidato; no se inició su baseline en este lote.
### Container — cierre neutral global (2026-09-30)
Container se auditó con baseline nativa propia: 17 canarios positivos,
tres sondas `AssertionError` Q31, cero pendientes y 20 nombres únicos.
Los positivos promovidos conservan nombres y estados exactos; dos
helpers y los tests viven fuera del wrapper. Dieciocho PNG originales
inspeccionados y archivados/SHA, uno a 350px y tres de Q31. Las sondas
confirmaron `maxWidth={320}` absorbido por `size="sm"` (480px),
`width={200}` absorbido por `size="full"` (1000px) y `marginLeft={0}`
absorbido por `align="center"` (284px desde el borde del escenario
frente a 24px). La decisión de precedencia/tipo queda diferida.
Morfo, wrapper, tipos, CSS, receta, barrel, README y API intactos;
cero extracción de lógica.
[Ficha](./audit-uix-2026-09-19/container.md) reviewed/censo 146.
`component:audit --only container` PASS 1/cero errores/avisos. Gate
propia exit 0: 585 archivos/7023 pruebas, docs 971/arts 25 limpios,
`check:gate` 89 históricos/cero nuevos src/scripts, apps/base
check/build y smoke foundation 1/1 + boot 27/27. Fase 6 abierta,
fases 0–5 sin cerrar y fase 7 pendiente. Section/Q32/Q33 es el
siguiente candidato; no se inició su baseline en este lote.

@ -3,7 +3,7 @@ title: UIX — Container, medida de contenido
type: notes
audience: human + agent
authority: process — auditoría de componente, no contrato nuevo
status: current
status: reviewed
---
# Container
@ -56,3 +56,36 @@ duplica el defecto.
`component:audit --only container`: PASS (1/1);
`docs:check`: 0 errores y 0 avisos; gate integral: 481/481 archivos,
5510/5510 tests y build/smoke de `apps/base` pasados. `web/` intacto.
## Continuación neutral — 2026-09-30
Se releyeron las cuatro guías obligatorias y se contrastó la matriz con
Radix Themes, Chakra, Mantine y MUI. La baseline nativa propia dio 17
canarios positivos y tres sondas `AssertionError` Q31, cero pendientes
y 20 nombres únicos. Los 17 positivos promovidos conservaron
exactamente nombres y estados; dos helpers y los tests viven fuera del
wrapper. Morfo, wrapper, tipos, CSS, receta, barrel, README y API
quedaron intactos.
Los positivos cubren el único root Box/Container, tamaño predeterminado
`xl`, topes `sm`/`md` y `full`, padding lateral predeterminado y su
anulación, `as="section"`, alineación izquierda/centro/derecha,
`container-type: inline-size`, estilo externo, props de Box absorbidas,
anidación, atributos, reactividad y 350px. Las tres sondas Q31
confirmaron que `size="sm"` conservó `max-width: 480px` frente al
`maxWidth={320}` pedido; `size="full"` conservó 1000px frente a
`width={200}`; y `align="center"` dejó el root a 284px del borde del
escenario frente a los 24px esperados con `marginLeft={0}`. Se difiere
la precedencia o reducción del tipo hasta caracterizar consumidores.
Se inspeccionaron y archivaron con SHA 18 PNG originales: catorce
positivos de 1100×900, uno a 350×900 y tres sondas Q31 de 1100×900.
Las capturas no certifican AT, touch, tema oscuro, contraste, otros
motores ni paridad externa. Cero lógica extraída y cero cambio de
producción.
`component:audit --only container`: PASS 1/cero errores/avisos. Gate
propia exit 0: 585 archivos/7023 pruebas, docs 971/arts 25 limpios,
`check:gate` 89 errores históricos dentro del ledger y cero nuevos en
`src/` o `scripts/`, apps/base check/build y smoke foundation 1/1 +
boot 27/27. Container queda reviewed/censo 146; fase 6 abierta.

@ -0,0 +1,195 @@
import { expect, it } from 'vitest';
import { page } from 'vitest/browser';
import { visit } from '../../../test/container/container-audit-view';
it('native default container is one Box root with xl measure and padding', () =>
visit(async (view) => {
const root = view.node();
expect(root.tagName).toBe('DIV');
expect(root.hasAttribute('data-box')).toBe(true);
expect(root.hasAttribute('data-container')).toBe(true);
expect(root.getAttribute('data-size')).toBe('xl');
expect(root.getAttribute('data-align')).toBe('center');
expect(getComputedStyle(root).width).toBe('1000px');
expect(getComputedStyle(root).maxWidth).toBe('1280px');
expect(getComputedStyle(root).paddingLeft).not.toBe('0px');
await view.capture('default');
}));
it('native sm size caps a wide parent at 480px', () =>
visit(
async (view) => {
const root = view.node();
expect(root.getAttribute('data-size')).toBe('sm');
expect(getComputedStyle(root).maxWidth).toBe('480px');
expect(getComputedStyle(root).width).toBe('480px');
await view.capture('sm');
},
{ size: 'sm' }
));
it('native md size caps a wide parent at 768px', () =>
visit(
async (view) => {
expect(getComputedStyle(view.node()).maxWidth).toBe('768px');
await view.capture('md');
},
{ size: 'md' }
));
it('native full size removes the cap but preserves default padding', () =>
visit(
async (view) => {
const root = view.node();
expect(getComputedStyle(root).maxWidth).toBe('none');
expect(getComputedStyle(root).width).toBe('1000px');
expect(getComputedStyle(root).paddingLeft).not.toBe('0px');
await view.capture('full');
},
{ size: 'full' }
));
it('native paddingX zero removes the default gutter', () =>
visit(
async (view) => {
expect(getComputedStyle(view.node()).paddingLeft).toBe('0px');
expect(getComputedStyle(view.node()).paddingRight).toBe('0px');
await view.capture('padding-zero');
},
{ paddingX: 0, size: 'sm' }
));
it('native semantic section retains Box and Container markers', () =>
visit(
async (view) => {
const root = view.node();
expect(root.tagName).toBe('SECTION');
expect(root.hasAttribute('data-box')).toBe(true);
expect(root.hasAttribute('data-container')).toBe(true);
await view.capture('section');
},
{ as: 'section', size: 'sm' }
));
it('native left alignment starts at the parent content edge', () =>
visit(
async (view) => {
const root = view.node().getBoundingClientRect();
const stage = view.stage().getBoundingClientRect();
expect(root.left - stage.left).toBe(24);
await view.capture('align-left');
},
{ size: 'sm', align: 'left' }
));
it('native center alignment balances the side margins', () =>
visit(
async (view) => {
const root = view.node().getBoundingClientRect();
const stage = view.stage().getBoundingClientRect();
expect(root.left - stage.left).toBe(284);
expect(stage.right - root.right).toBe(284);
await view.capture('align-center');
},
{ size: 'sm', align: 'center' }
));
it('native right alignment ends at the parent content edge', () =>
visit(
async (view) => {
const root = view.node().getBoundingClientRect();
const stage = view.stage().getBoundingClientRect();
expect(stage.right - root.right).toBe(24);
await view.capture('align-right');
},
{ size: 'sm', align: 'right' }
));
it('native Container is an inline-size query container', () =>
visit(async (view) => {
expect(getComputedStyle(view.node()).containerType).toBe('inline-size');
}));
it('native consumer style can override the Box max-width variable', () =>
visit(
async (view) => {
const root = view.node();
expect(getComputedStyle(root).maxWidth).toBe('320px');
expect(getComputedStyle(root).width).toBe('320px');
await view.capture('consumer-style');
},
{ size: 'sm', style: '--box-max-width:320px;color:rgb(20, 40, 60)' }
));
it('native inherited Box width and maxWidth props are shadowed by size', () =>
visit(
async (view) => {
const root = view.node();
expect(root.style.getPropertyValue('--box-width').trim()).toBe('100%');
expect(root.style.getPropertyValue('--box-max-width').trim()).toBe(
'var(--container-width-sm)'
);
expect(getComputedStyle(root).width).toBe('480px');
await view.capture('shadowed-width');
},
{ size: 'sm', width: 200, maxWidth: 320 }
));
it('native inherited Box margins are shadowed by alignment', () =>
visit(
async (view) => {
const root = view.node();
expect(root.style.getPropertyValue('--box-margin-left').trim()).toBe('auto');
expect(root.style.getPropertyValue('--box-margin-inline').trim()).toBe('var(--space-0)');
expect(root.getBoundingClientRect().left - view.stage().getBoundingClientRect().left).toBe(
284
);
await view.capture('shadowed-margin');
},
{ size: 'sm', align: 'center', marginLeft: 0, marginX: 0 }
));
it('native nested Container writes its own width cap', () =>
visit(
async (view) => {
const inner = view.inner()!;
expect(inner.hasAttribute('data-container')).toBe(true);
expect(inner.getAttribute('data-size')).toBe('sm');
expect(getComputedStyle(inner).maxWidth).toBe('480px');
await view.capture('nested');
},
{ size: 'md', nested: true }
));
it('native global attributes and class reach the root', () =>
visit(async (view) => {
const root = view.node();
expect(root.classList.contains('audit-container')).toBe(true);
expect(root.getAttribute('data-probe')).toBe('container');
expect(root.getAttribute('title')).toBe('Audit Container');
}));
it('native reactive size and align update the same root', () =>
visit(async (view) => {
const root = view.node();
view.controls().size('sm');
view.controls().align('left');
await view.ready();
expect(view.node()).toBe(root);
expect(root.getAttribute('data-size')).toBe('sm');
expect(root.getAttribute('data-align')).toBe('left');
expect(getComputedStyle(root).width).toBe('480px');
await view.capture('reactive');
}));
it('native 350px Container stays inside the viewport', () =>
visit(
async (view) => {
await page.viewport(350, 900);
await view.ready();
const rect = view.node().getBoundingClientRect();
expect(rect.left >= 0 && rect.right <= 350).toBe(true);
await view.capture('350');
},
{ stageWidth: 300 }
));

@ -0,0 +1,88 @@
<script module lang="ts">
import type { ContainerProps } from '$uix/eidos/components/container';
export type Config = {
as?: ContainerProps['as'];
size?: ContainerProps['size'];
align?: ContainerProps['align'];
paddingX?: ContainerProps['paddingX'];
width?: ContainerProps['width'];
maxWidth?: ContainerProps['maxWidth'];
marginLeft?: ContainerProps['marginLeft'];
marginX?: ContainerProps['marginX'];
style?: ContainerProps['style'];
nested?: boolean;
stageWidth?: number;
};
export type Controls = {
size: (next: ContainerProps['size']) => void;
align: (next: ContainerProps['align']) => void;
};
</script>
<script lang="ts">
import { onDestroy, onMount } from 'svelte';
import { setActiveUix, type ActiveUix } from '$active-uix';
import { ActiveEidos } from '$uix/eidos';
import Container from '$uix/eidos/components/container';
let {
uix,
initial = {},
onControls
}: {
uix: ActiveUix;
initial?: Config;
onControls: (controls: Controls) => void;
} = $props();
// svelte-ignore state_referenced_locally
setActiveUix(uix);
const eidos = ActiveEidos.create({ applyDom: false });
onDestroy(() => eidos.dispose());
// svelte-ignore state_referenced_locally
let size = $state(initial.size);
// svelte-ignore state_referenced_locally
let align = $state(initial.align);
onMount(() => onControls({ size: (next) => (size = next), align: (next) => (align = next) }));
</script>
<div
id="audit-stage"
data-audit-stage
style={`position:relative;width:${initial.stageWidth ?? 1000}px;min-height:160px;padding:24px;font-family:Arial,sans-serif`}
>
<Container
id="audit-container"
class="audit-container"
data-probe="container"
title="Audit Container"
as={initial.as}
{size}
{align}
paddingX={initial.paddingX}
width={initial.width}
maxWidth={initial.maxWidth}
marginLeft={initial.marginLeft}
marginX={initial.marginX}
style={initial.style}
>
<div class="audit-content">Content measure</div>
{#if initial.nested}
<Container id="audit-inner" class="audit-inner" size="sm">Inner measure</Container>
{/if}
</Container>
</div>
<style>
:global(.audit-container) {
background: rgb(232, 240, 252);
outline: 1px solid rgb(66, 90, 128);
}
:global(.audit-content) {
background: rgb(107, 156, 206);
color: white;
font-size: 13px;
}
:global(.audit-inner) {
background: rgb(255, 250, 220);
outline: 1px solid rgb(160, 130, 70);
}
</style>

@ -0,0 +1,60 @@
import { mount, unmount, flushSync } from 'svelte';
import { page } from 'vitest/browser';
import { createActiveUix } from '$active-uix';
import '$uix/eidos/index.css';
import Harness, { type Config, type Controls } from './container-audit-harness.svelte';
export function renderContainer(initial: Config = {}) {
const uix = createActiveUix({
langs: { schema: {}, defaultLocale: 'en' },
prefs: { storage: false },
projectPrefs: false
});
const target = document.createElement('div');
document.body.append(target);
let controls!: Controls;
const component = mount(Harness, {
target,
props: { uix, initial, onControls: (value) => (controls = value) }
});
flushSync();
const node = () => target.querySelector<HTMLElement>('#audit-container')!;
const stage = () => target.querySelector<HTMLElement>('#audit-stage')!;
const inner = () => target.querySelector<HTMLElement>('#audit-inner');
const content = () => target.querySelector<HTMLElement>('.audit-content')!;
const ready = async () =>
new Promise<void>((resolve) =>
requestAnimationFrame(() => requestAnimationFrame(() => resolve()))
);
return {
target,
node,
stage,
inner,
content,
controls: () => controls,
ready,
async capture(name: string) {
if (import.meta.env.VITE_UIX_AUDIT_SCREENSHOTS === '1') {
await ready();
await page.screenshot({ path: 'tmp/container-' + name + '.png' });
}
},
async dispose() {
await unmount(component);
uix.dispose();
target.remove();
}
};
}
export type View = ReturnType<typeof renderContainer>;
export async function visit(fn: (view: View) => Promise<void>, initial: Config = {}) {
await page.viewport(1100, 900);
const view = renderContainer(initial);
try {
await view.ready();
await fn(view);
} finally {
await view.dispose();
}
}
Loading…
Cancel
Save

Powered by TurnKey Linux.