El plano `data-depth` pinta el bundle de elevación (superficie · borde · sombra
· tipografía on-surface) y hasta hoy lo hacía a (0,1,0), la misma especificidad
que la receta del componente. La Decisión 8 daba por bueno ese empate porque
«las recetas cargan después de la fundación y ganan por ORDEN DE CASCADA».
ESA PREMISA ERA FALSA
La fundación la inyecta ActiveEidos en runtime como <style> gestionado, y las
recetas llegan como chunks code-split de Vite: a igual peso ganaba quien
cargara el último, y se midió AL REVÉS en dev que en producción (§13). No era
una decisión de diseño, era una moneda al aire — y la moneda cayó del lado del
plano.
LO QUE COSTABA, MEDIDO
Un toast `risk` y uno `fulfill` vestían la MISMA TARJETA GRIS: los seis tonos
idénticos y la franja de acento —el rasgo que identifica la intención de un
vistazo— reducida a 0,67 px de gris neutro, porque el atajo `border` del plano
pisaba el longhand `border-inline-start` en los cuatro lados. Las TRES variantes
de tooltip computaban lo mismo: `outline` indistinguible de `solid`, `ghost` un
backdrop-filter invisible tras una superficie opaca. Y ~35 claves públicas
quedaban adjudicadas como mudas en cinco componentes, con tres recetas que
habían RETIRADO su tipografía por esto.
LA REGLA
La regla de apariencia del plano se emite envuelta en `:where(...)` —
especificidad CERO. La receta gana donde el componente HABLA, en cualquier
orden de carga; el plano sigue pintando todo lo que el componente CALLA, que es
exactamente lo que significa «baseline». Vale igual para un plano que añada un
tema (jaula abierta).
EL FROST NO BAJA, Y ES DELIBERADO
`[data-depth='{plane}'][data-frost]` conserva sus (0,2,0). El baseline es un
suelo que la receta puede pisar; el frost es una petición explícita por
elemento —alguien escribió `data-frost`— y honrarla significa ganarle al fondo
propio del componente. Mismo atributo, intención opuesta: queda comentado en el
emisor para que nadie los «armonice», y el guard lo fija por los dos lados.
QUÉ MUEVE DE PÍXEL, ENTERO
De 19 adoptantes, DIECISÉIS con diff CERO sobre ~49.000 valores computados. Los
tres que se mueven son la firma haciendo su trabajo:
- toast: 35 diffs (7 fondos + 7 bordes de tono + 21 anchuras). Cuatro tonos
medidos en la demo real: 4 de 4 DISTINTOS donde antes 3 de 3 eran idénticos,
y la franja de acento pasa de 0,67 px de gris a 3 px del color de la
intención.
- tooltip: sus tres variantes vuelven a distinguirse — solid opaco con sombra,
outline transparente con borde, ghost translúcido al 70 %.
- popover, tooltip y link-preview: `line-height` de 1.25 (el `--leading-ui` del
plano) al que cada receta pide. Es EXACTAMENTE la decisión que la receta de
link-preview tenía escrita desde el 2026-08-21: «se quedan declaradas A
PROPÓSITO; retirarlas arreglaría el empate a favor del plano — una decisión
de píxel que pertenece a la firma pendiente de §12.9».
`font-family` no se movió en ninguno: las recetas piden el mismo
`--style-label-font-family` que el plano pinta, así que la seguridad
tipográfica del portal (Decisión 8) queda intacta.
Refutada una hipótesis propia: temía que el atajo `background` del plano
estuviera matando el velo de hover del sistema y que bajarlo lo resucitara
moviendo píxel. `backgroundImage` no cambia en ninguno de los 19 — el velo vive
en los ítems, no en la superficie.
EL LEDGER
17 claves salieron STALE solas y se retiran: toast (3), tooltip (5),
float-panel (5), link-preview (2), popover (2). Los cinco vuelven verdes —
toast pasa de 51/85 a 72/85. El ledger sigue en 87 bloques, sin duplicados, y
parsea.
DOS VECES QUE EL INSTRUMENTO MINTIÓ
La sonda compartida mide UN nodo en dialog, drawer, popover, tooltip,
context-menu, dropdown-menu y link-preview: nunca abre la superficie, que es
justo donde el plano pinta, así que sus «0 diffs» no probaban nada. La medición
buena reproduce el «antes» EN RUNTIME, inyectando la regla vieja a (0,1,0) al
final del head en vez de revertir el fichero — entra como
`scripts/__plane-open.mjs`.
Y el primer barrido del ledger dio 87 de 87 rojos: «todos fallan igual» es la
firma del instrumento, y era la misma trampa del CR de Python en Windows que ya
está registrada. Re-corrido con la lista limpia.
EL BARRIDO DEL LEDGER ENTERO
87 de 87 componentes con entrada en el ledger, re-corridos tras el flip: cero
NO EFFECT sin adjudicar, cero STALE, cero errores. El cambio es global y se
verificó globalmente.
GUARDS
vitest src/uix/eidos 439/440 (el rojo conocido skin-media-player) ·
active-eidos-config 75/75 · npm run check COMPLETED con los 72 errores
preexistentes de la rama y ninguno en ficheros de este cambio · rtl:check 0 ·
docs:check 0 · eidos-lint 0 invalid en los cinco tocados · capturas de las tres
superficies que se mueven.
Un test hermano afirmaba el selector desnudo para un plano añadido por un tema:
ahora afirma `:where()`, porque un plano de tema es un suelo igual que los
cinco de fábrica.
Doctrina en docs/theming/changelog.md §29 (junto a la Decisión 8 que corrige),
cierre del expediente en next-features §13 y handoff al día.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
alpha-0.1-background
parent
d4a83cca68
commit
5d71d830e9
@ -0,0 +1,121 @@
|
||||
/**
|
||||
* §12.9 — measure the OPENED surface of every portaled adopter, before vs after.
|
||||
*
|
||||
* The shared probe measures 1 node on these components: it never opens them, so a
|
||||
* "0 diffs" there proves nothing about the plane, which paints exactly the surface
|
||||
* the probe cannot see (the 6.bis law: count the nodes before believing anything).
|
||||
*
|
||||
* The "before" is reproduced AT RUNTIME instead of reverting the file: inject the
|
||||
* plane's appearance rules at their OLD specificity (0,1,0), appended last so they
|
||||
* win by order — which is what they did in dev, measured. Whatever moves between
|
||||
* the two reads is exactly what the `:where()` flip changed on that surface.
|
||||
*/
|
||||
import { chromium } from 'playwright';
|
||||
|
||||
const OLD_PLANE = `
|
||||
[data-depth='raised'] { background: var(--depth-raised-surface); font-family: var(--style-label-font-family); line-height: var(--leading-ui); border: var(--border-width) solid var(--depth-raised-border); box-shadow: var(--depth-raised-shadow), var(--depth-raised-halo); }
|
||||
[data-depth='overlay'] { background: var(--depth-overlay-surface); font-family: var(--style-label-font-family); line-height: var(--leading-ui); border: var(--border-width) solid var(--depth-overlay-border); box-shadow: var(--depth-overlay-shadow), var(--depth-overlay-halo); }
|
||||
[data-depth='modal'] { background: var(--depth-modal-surface); font-family: var(--style-label-font-family); line-height: var(--leading-ui); border: var(--border-width) solid var(--depth-modal-border); box-shadow: var(--depth-modal-shadow), var(--depth-modal-halo); }
|
||||
`;
|
||||
|
||||
const PROPS = [
|
||||
'backgroundColor',
|
||||
'backgroundImage',
|
||||
'borderTopColor',
|
||||
'borderTopWidth',
|
||||
'borderInlineStartColor',
|
||||
'borderInlineStartWidth',
|
||||
'boxShadow',
|
||||
'fontFamily',
|
||||
'lineHeight',
|
||||
'color',
|
||||
'borderRadius'
|
||||
];
|
||||
|
||||
/** component → [how to open, surface selector]. `hover` for the ones that open on pointer. */
|
||||
const CASES = [
|
||||
['dialog', 'click', '[data-dialog-content]'],
|
||||
['drawer', 'click', '[data-drawer-content]'],
|
||||
['dropdown-menu', 'click', '[data-dropdown-menu-content]'],
|
||||
['popover', 'click', '[data-popover-content]'],
|
||||
['select', 'click', '[data-select-content]'],
|
||||
['combobox', 'click', '[data-combobox-content]'],
|
||||
['menubar', 'click', '[data-menubar-content]'],
|
||||
['tooltip', 'hover', '[data-tooltip-content]'],
|
||||
['link-preview', 'hover', '[data-link-preview-content]'],
|
||||
['navigation-menu', 'hover', '[data-navigation-menu-content]'],
|
||||
['context-menu', 'context', '[data-context-menu-content]']
|
||||
];
|
||||
|
||||
const browser = await chromium.launch();
|
||||
const report = [];
|
||||
|
||||
for (const [comp, how, surface] of CASES) {
|
||||
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
|
||||
const url = `http://localhost:5180/uix/components/${comp}`;
|
||||
try {
|
||||
try {
|
||||
await page.goto(url, { waitUntil: 'networkidle', timeout: 25000 });
|
||||
} catch {
|
||||
await page.goto(url, { waitUntil: 'load', timeout: 25000 });
|
||||
}
|
||||
await page.waitForTimeout(1500);
|
||||
await page.addStyleTag({
|
||||
content: '*, *::before, *::after { transition: none !important; animation: none !important; }'
|
||||
});
|
||||
|
||||
const trigger = page.locator(`[data-${comp}-trigger]`).first();
|
||||
if (!(await trigger.count())) {
|
||||
report.push({ comp, err: 'no trigger' });
|
||||
await page.close();
|
||||
continue;
|
||||
}
|
||||
if (how === 'click') await trigger.click({ timeout: 5000 }).catch(() => {});
|
||||
else if (how === 'hover') await trigger.hover({ timeout: 5000 }).catch(() => {});
|
||||
else await trigger.click({ button: 'right', timeout: 5000 }).catch(() => {});
|
||||
await page.waitForTimeout(900);
|
||||
|
||||
const res = await page.evaluate(
|
||||
({ surface, PROPS, OLD_PLANE }) => {
|
||||
const nodes = [...document.querySelectorAll(surface)];
|
||||
if (!nodes.length) return { err: 'surface not mounted' };
|
||||
const snap = () =>
|
||||
nodes.map((n) => {
|
||||
const cs = getComputedStyle(n);
|
||||
const o = {};
|
||||
for (const p of PROPS) o[p] = cs[p];
|
||||
return o;
|
||||
});
|
||||
const after = snap(); // current code = :where()
|
||||
const st = document.createElement('style');
|
||||
st.textContent = OLD_PLANE; // reproduce the old (0,1,0), appended last
|
||||
document.head.append(st);
|
||||
const before = snap();
|
||||
st.remove();
|
||||
const diffs = [];
|
||||
after.forEach((a, i) => {
|
||||
for (const p of PROPS)
|
||||
if (a[p] !== before[i][p]) diffs.push({ node: i, prop: p, antes: before[i][p], despues: a[p] });
|
||||
});
|
||||
return { nodes: nodes.length, depth: nodes[0].getAttribute('data-depth'), diffs };
|
||||
},
|
||||
{ surface, PROPS, OLD_PLANE }
|
||||
);
|
||||
report.push({ comp, ...res });
|
||||
} catch (e) {
|
||||
report.push({ comp, err: String(e).slice(0, 80) });
|
||||
}
|
||||
await page.close();
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
for (const r of report) {
|
||||
if (r.err) {
|
||||
console.log(`${r.comp.padEnd(18)} ⚠ ${r.err}`);
|
||||
continue;
|
||||
}
|
||||
const head = `${r.comp.padEnd(18)} nodos=${r.nodes} depth=${r.depth} diffs=${r.diffs.length}`;
|
||||
console.log(head);
|
||||
for (const d of r.diffs.slice(0, 8))
|
||||
console.log(` ${d.prop}: «${String(d.antes).slice(0, 46)}» → «${String(d.despues).slice(0, 46)}»`);
|
||||
}
|
||||
Loading…
Reference in new issue