test(uix): retain neutral Box audit baseline

astra
dev 1 week ago
parent ea6e1aef12
commit cbb75903fe

@ -105,16 +105,29 @@ lógica extraída. Guard PASS 1/cero errores/avisos. Gate propia exit 0:
históricos/cero nuevos src/scripts, apps/base check/build y smoke
foundation 1/1 + boot 27/27.
Después, [Box](./audit-uix-2026-09-19/box.md) reviewed/censo 143:
15 canarios positivos y una sonda `AssertionError` Q28, cero pendientes,
16 nombres. Huella positiva original/final exacta, 14 PNG originales
inspeccionados/SHA (uno a 350px y uno de la sonda RTL), tests y dos
helpers fuera del wrapper. `left={10}` se colocó a 498px del borde
izquierdo del escenario RTL frente a 10px en LTR; la adjudicación
física/lógica queda diferida. Morfo/tipos/CSS/receta/API intactos;
wrapper sólo corrigió un comentario obsoleto, sin lógica extraída.
Guard PASS 1/cero errores/avisos. Gate propia exit 0: 582 archivos/
6976 tests, docs 971/arts 25 limpios, `check:gate` 89 históricos/
cero nuevos src/scripts, apps/base check/build y smoke foundation 1/1 y
boot 27/27.
Esta continuación documenta las fichas de Heading,
Text, Display, Link, Prose y Label, plan/registro/handoffs, README de Text,
Text, Display, Link, Prose, Label y Box, plan/registro/handoffs, README de Text,
Display y Prose, comentario de tipo Prose y tests/helpers nuevos.
`.claude/settings.local.json` y `.vitest-attachments/` son del usuario
y siguen excluidos.
## Continuación segura
Box es la siguiente ficha pendiente; su
[ficha previa](./audit-uix-2026-09-19/box.md) registra Q28. Releer antes las cuatro guías
Stack es la siguiente ficha pendiente; su
[ficha previa](./audit-uix-2026-09-19/stack.md) registra Q29. 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
@ -130,7 +143,8 @@ También están ignoradas `tmp/heading-audit-evidence-2026-09-30/`,
`tmp/display-audit-evidence-2026-09-30/`,
`tmp/link-audit-evidence-2026-09-30/` y
`tmp/prose-audit-evidence-2026-09-30/` y
`tmp/label-audit-evidence-2026-09-30/`; sus archivos de imágenes y
`tmp/label-audit-evidence-2026-09-30/` y
`tmp/box-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.

@ -27,17 +27,19 @@ pendiente.
| [Link](./audit-uix-2026-09-19/link-native.md) | reviewed/140 | 12 positivos, 2 `AssertionError`, 0 pendientes | 10 PNG/SHA, 1 a 350px | Q25: foco sin `href`; Q26: `target`/`rel` en span | 579/6936 |
| [Prose](./audit-uix-2026-09-19/prose-native.md) | reviewed/141 | 12 positivos, 1 `AssertionError`, 0 pendientes | 11 PNG/SHA, 1 a 350px | Q24: `::marker` atraviesa `data-prose-ignore` | 580/6948 |
| [Label](./audit-uix-2026-09-19/label.md) | reviewed/142 | 13 positivos, 1 `AssertionError`, 0 pendientes | 10 PNG/SHA, 1 a 350px | Q27: `style` del consumidor perdido | 581/6961 |
| [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 |
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, wrappers, CSS, recetas y APIs ejecutables quedaron intactos.
Morfo, CSS, recetas y APIs ejecutables quedaron intactos. El wrapper
de Box sólo corrigió un comentario obsoleto.
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). Los defectos Q22–Q27
predeterminado (19,55px medidos a 1100px). Los defectos Q22–Q28
seguían en el registro y se extendieron con sondas, sin reparación.
La última gate completa, de Label, salió con exit 0: 581 archivos/6961
La última gate completa, de Box, salió con exit 0: 582 archivos/6976
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
@ -47,8 +49,8 @@ paridad externa.
## Continuación segura
[Box](./audit-uix-2026-09-19/box.md) es el siguiente candidato;
la ficha previa registra Q28 (posición en RTL). Antes de
[Stack](./audit-uix-2026-09-19/stack.md) es el siguiente candidato;
la ficha previa registra Q29 (`alignContent` sin `wrap`). 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
@ -59,7 +61,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}-audit-evidence-2026-09-30/`;
ignorada está en `tmp/{heading,text,display,link,prose,label,box}-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

@ -3705,3 +3705,25 @@ exit 0: 581 archivos/6961 pruebas, docs 971/arts 25 limpios,
boot 27/27. Fase 6 abierta, fases 0–5 sin cerrar y fase 7 pendiente.
Box/Q28 es el siguiente candidato pendiente; no se inició su baseline
en este lote.
### Box — cierre neutral global (2026-09-30)
Box se auditó con baseline nativa propia: 15 canarios positivos, una
sonda `AssertionError` Q28, cero pendientes y 16 nombres únicos. Los
positivos promovidos conservan nombres y estados exactos; dos helpers
y los tests viven fuera del wrapper. Catorce PNG originales inspeccionados
y archivados/SHA, uno a 350px y otro de la sonda RTL. La sonda confirma
`left={10}` a 498px del borde izquierdo en RTL frente a 10px en LTR
para el escenario medido; la adjudicación física/lógica se difiere.
Morfo, tipos, CSS, receta, barrel y API intactos; wrapper sólo corrige
un comentario obsoleto sobre `as` y Morfo, sin cambio ejecutable ni
extracción de lógica.
[Ficha](./audit-uix-2026-09-19/box.md) reviewed/censo 143.
`component:audit --only box` PASS 1/cero errores/avisos. Gate propia
exit 0: 582 archivos/6976 pruebas, docs 971/arts 25 limpios,
`check:gate` 89 incidencias históricas del ledger y cero nuevas en
`src/` o `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.
Stack/Q29 es el siguiente candidato; no se inició su baseline en este
lote.

@ -3,7 +3,7 @@ title: UIX — Box, base de layout
type: notes
audience: human + agent
authority: process — auditoría de componente, no contrato nuevo
status: current
status: reviewed
---
# Box
@ -50,3 +50,35 @@ ejecutable ni en `web/`.
y 0 avisos sobre 855 documentos; pruebas dirigidas de helpers y contratos
Eidos: 14/14; gate completo: 481 archivos y 5510 pruebas, con smoke de
`apps/base` 27/27. Reducción de producción: **0 líneas**.
## Continuación neutral — 2026-09-30
Se releyeron las cuatro guías obligatorias, y la matriz previa se
contrastó otra vez con Radix Themes, Chakra, Mantine y MUI. Morfo,
wrapper, tipos, barrel, CSS, helpers, README y ficha previa se
revisaron; `web/` permanece congelado. La baseline nativa propia tiene
15 canarios positivos, una sonda `AssertionError` Q28, cero pendientes
y 16 nombres únicos. Los 15 positivos promovidos conservan los nombres
y estados exactos de la baseline. Tests y dos helpers viven fuera del
wrapper; cero bolsas, conexiones o líneas de lógica extraídas.
Los positivos cubren tag predeterminado y semántico, medidas y límites,
espaciado, display/overflow, posición LTR, flex, grid item, estilo del
consumidor, no herencia de padding entre Box anidados, atributos nativos,
reactividad y 350px. La sonda Q28 ubica `left={10}` a 498px del borde
izquierdo de un escenario RTL de 528px de ancho exterior; en LTR queda
a 10px. La caja se coloca a 10px del borde derecho en RTL. La decisión
entre semántica física o lógica sigue diferida; ni CSS ni API cambiaron.
Se inspeccionaron y archivaron con SHA 14 PNG originales: 12 positivos
de 1100×900, un positivo de 350×900 y la sonda RTL de 1100×900. El
comentario de cabecera del wrapper se corrigió para describir el
`as` ya existente y el Morfo pasivo; la plantilla y el código ejecutable
quedaron intactos. Las capturas no certifican AT, touch, tema oscuro,
contraste, otros motores ni paridad externa.
`component:audit --only box`: PASS 1/cero errores/avisos. Gate completa
propia exit 0: 582 archivos/6976 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. Box queda reviewed/censo 143; fase 6 abierta.

@ -1,15 +1,16 @@
<script lang="ts">
/**
* Eidos `<Box>` — universal box-model utility. Visual-only primitive:
* every prop maps to a `--box-*` custom property on a single `<div
* data-box>` root, and the recipe in `box.css` consumes those vars
* every prop maps to a `--box-*` custom property on a single
* `[data-box]` root (`div` by default, or the chosen `as` tag), and
* the recipe in `box.css` consumes those vars
* with `revert-layer` fallbacks so unset props inherit normal cascade.
*
* <Box padding={4} maxWidth={600} display="flex" gap={3}>…</Box>
*
* `Box` is the foundation that Flex, Grid, Stack, Group, Wrap,
* Container and Section all build on. It owns no behavior and no
* morfo — it just translates props to CSS variables.
* semantic events — it just translates props to CSS variables.
*/
import { ActiveEidos } from '$uix/eidos';
import type { BoxProps } from './types';

@ -0,0 +1,171 @@
import { expect, it } from 'vitest';
import { page } from 'vitest/browser';
import { visit } from '../../../test/box/box-audit-view';
it('native default box renders a div with the contract marker', () =>
visit(async (view) => {
const root = view.node();
expect(root.tagName).toBe('DIV');
expect(root.getAttribute('data-box')).toBe('');
expect(root.textContent?.trim()).toBe('Layout box');
await view.capture('default');
}));
it('native semantic element preserves the box recipe', () =>
visit(
async (view) => {
expect(view.node().tagName).toBe('SECTION');
expect(view.node().getAttribute('data-box')).toBe('');
await view.capture('section');
},
{ as: 'section' }
));
it('native numeric width and height become CSS lengths', () =>
visit(
async (view) => {
const root = view.node();
expect(root.style.getPropertyValue('--box-width').trim()).toBe('120px');
expect(root.style.getPropertyValue('--box-height').trim()).toBe('60px');
expect(getComputedStyle(root).width).toBe('120px');
await view.capture('lengths');
},
{ width: 120, height: 60 }
));
it('native min and max width constrain the rendered box', () =>
visit(
async (view) => {
const root = view.node();
expect(root.style.getPropertyValue('--box-min-width').trim()).toBe('80px');
expect(root.style.getPropertyValue('--box-max-width').trim()).toBe('160px');
expect(getComputedStyle(root).width).toBe('160px');
await view.capture('clamped');
},
{ width: 220, minWidth: 80, maxWidth: 160 }
));
it('native padding side overrides the shared spacing', () =>
visit(
async (view) => {
const style = view.node().style;
expect(style.getPropertyValue('--box-padding').trim()).toBe('var(--space-4)');
expect(style.getPropertyValue('--box-padding-top').trim()).toBe('var(--space-6)');
expect(getComputedStyle(view.node()).paddingTop).not.toBe(
getComputedStyle(view.node()).paddingBottom
);
await view.capture('padding');
},
{ padding: 4, paddingTop: 6, width: 180 }
));
it('native inline margin uses the spacing token path', () =>
visit(
async (view) => {
expect(view.node().style.getPropertyValue('--box-margin-inline').trim()).toBe(
'var(--space-3)'
);
await view.capture('margin');
},
{ marginX: 3, width: 180 }
));
it('native display and overflow axes reach the recipe', () =>
visit(
async (view) => {
expect(getComputedStyle(view.node()).display).toBe('flex');
expect(getComputedStyle(view.node()).overflowX).toBe('auto');
await view.capture('display');
},
{ display: 'flex', overflowX: 'auto', width: 180 }
));
it('native LTR left and top position from the physical edges', () =>
visit(
async (view) => {
const root = view.node().getBoundingClientRect();
const stage = view.stage().getBoundingClientRect();
expect(root.left - stage.left).toBe(10);
expect(root.top - stage.top).toBe(5);
await view.capture('position-ltr');
},
{ position: 'absolute', left: 10, top: 5, width: 20, height: 20, dir: 'ltr' }
));
it('native flex shorthand expands and explicit grow wins', () =>
visit(
async (view) => {
const style = view.node().style;
expect(style.getPropertyValue('--box-grow').trim()).toBe('3');
expect(style.getPropertyValue('--box-shrink').trim()).toBe('1');
expect(style.getPropertyValue('--box-basis').trim()).toBe('0%');
await view.capture('flex');
},
{ flex: 2, grow: 3 }
));
it('native grid area stamps its guard and grid item variables', () =>
visit(
async (view) => {
const root = view.node();
expect(root.getAttribute('data-box-area')).toBe('');
expect(root.style.getPropertyValue('--box-grid-area').trim()).toBe('1 / 2 / 3 / 4');
expect(root.style.getPropertyValue('--box-grid-column').trim()).toBe('2 / 4');
await view.capture('grid');
},
{ gridArea: '1 / 2 / 3 / 4', gridColumn: '2 / 4' }
));
it('native consumer style follows internal declarations', () =>
visit(
async (view) => {
const root = view.node();
expect(root.style.getPropertyValue('--box-width').trim()).toBe('80px');
expect(getComputedStyle(root).width).toBe('80px');
await view.capture('consumer-style');
},
{ width: 120, style: '--box-width:80px;color:rgb(20, 40, 60)' }
));
it('native nested Box does not inherit the parent padding axis', () =>
visit(
async (view) => {
const inner = view.inner()!;
expect(inner.getAttribute('data-box')).toBe('');
expect(inner.style.getPropertyValue('--box-padding')).toBe('');
expect(getComputedStyle(inner).paddingTop).toBe('0px');
await view.capture('nested');
},
{ nested: true, padding: 4, width: 220 }
));
it('native global attributes and class reach the chosen element', () =>
visit(async (view) => {
const root = view.node();
expect(root.classList.contains('audit-box')).toBe(true);
expect(root.getAttribute('data-probe')).toBe('box');
expect(root.getAttribute('title')).toBe('Audit Box');
}));
it('native reactive text and width update the same box', () =>
visit(async (view) => {
const original = view.node();
view.controls().text('Changed box');
view.controls().width(210);
await view.ready();
expect(view.node()).toBe(original);
expect(view.node().textContent?.trim()).toBe('Changed box');
expect(getComputedStyle(view.node()).width).toBe('210px');
}));
it('native 350px box remains inside its 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');
},
{ width: 180, stageWidth: 300 }
));

@ -0,0 +1,108 @@
<script module lang="ts">
import type { BoxProps } from '$uix/eidos/components/box';
export type Config = {
text?: string;
as?: BoxProps['as'];
width?: BoxProps['width'];
minWidth?: BoxProps['minWidth'];
maxWidth?: BoxProps['maxWidth'];
height?: BoxProps['height'];
padding?: BoxProps['padding'];
paddingTop?: BoxProps['paddingTop'];
marginX?: BoxProps['marginX'];
display?: BoxProps['display'];
gap?: BoxProps['gap'];
position?: BoxProps['position'];
left?: BoxProps['left'];
right?: BoxProps['right'];
top?: BoxProps['top'];
overflowX?: BoxProps['overflowX'];
flex?: BoxProps['flex'];
grow?: BoxProps['grow'];
gridArea?: BoxProps['gridArea'];
gridColumn?: BoxProps['gridColumn'];
style?: BoxProps['style'];
nested?: boolean;
dir?: 'ltr' | 'rtl';
stageWidth?: number;
};
export type Controls = {
text: (next: string) => void;
width: (next: BoxProps['width']) => void;
};
</script>
<script lang="ts">
import { onDestroy, onMount } from 'svelte';
import { setActiveUix, type ActiveUix } from '$active-uix';
import { ActiveEidos } from '$uix/eidos';
import Box from '$uix/eidos/components/box';
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 text = $state(initial.text ?? 'Layout box');
// svelte-ignore state_referenced_locally
let width = $state(initial.width);
onMount(() => onControls({ text: (next) => (text = next), width: (next) => (width = next) }));
</script>
<div
id="audit-stage"
data-audit-stage
dir={initial.dir}
style={`position:relative;width:${initial.stageWidth ?? 480}px;min-height:160px;padding:24px;font-family:Arial,sans-serif`}
>
<Box
id="audit-box"
class="audit-box"
data-probe="box"
title="Audit Box"
as={initial.as}
{width}
minWidth={initial.minWidth}
maxWidth={initial.maxWidth}
height={initial.height}
padding={initial.padding}
paddingTop={initial.paddingTop}
marginX={initial.marginX}
display={initial.display}
gap={initial.gap}
position={initial.position}
left={initial.left}
right={initial.right}
top={initial.top}
overflowX={initial.overflowX}
flex={initial.flex}
grow={initial.grow}
gridArea={initial.gridArea}
gridColumn={initial.gridColumn}
style={initial.style}
>
{text}
{#if initial.nested}
<Box id="audit-inner" class="audit-inner">Inner box</Box>
{/if}
</Box>
</div>
<style>
:global(.audit-box) {
background: rgb(232, 240, 252);
outline: 1px solid rgb(66, 90, 128);
}
:global(.audit-inner) {
background: rgb(255, 250, 220);
outline: 1px solid rgb(160, 130, 70);
}
</style>

@ -0,0 +1,58 @@
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 './box-audit-harness.svelte';
export function renderBox(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-box')!;
const stage = () => target.querySelector<HTMLElement>('#audit-stage')!;
const inner = () => target.querySelector<HTMLElement>('#audit-inner');
const ready = async () =>
new Promise<void>((resolve) =>
requestAnimationFrame(() => requestAnimationFrame(() => resolve()))
);
return {
target,
node,
stage,
inner,
controls: () => controls,
ready,
async capture(name: string) {
if (import.meta.env.VITE_UIX_AUDIT_SCREENSHOTS === '1') {
await ready();
await page.screenshot({ path: 'tmp/box-' + name + '.png' });
}
},
async dispose() {
await unmount(component);
uix.dispose();
target.remove();
}
};
}
export type View = ReturnType<typeof renderBox>;
export async function visit(fn: (view: View) => Promise<void>, initial: Config = {}) {
await page.viewport(1100, 900);
const view = renderBox(initial);
try {
await view.ready();
await fn(view);
} finally {
await view.dispose();
}
}
Loading…
Cancel
Save

Powered by TurnKey Linux.