Encargo del autor: anotar todas las incidencias de esta clase para resolverlas después. Van al registro canónico (`next-features.md`), no a un sitio nuevo, y con su MEDICIÓN — no como impresión. **§12 — El contrato de cascada del velo de estado.** Seis incidencias que resultaron ser la misma: nadie fijó nunca cómo compone el velo de `archetypes.css` con las reglas de una receta. Y la raíz explica por qué se manifiestan de formas tan distintas: el velo se pinta con DOS pesos según el arquetipo — `:where()` (0,0,0) para `trigger`, especificidad plena (0,5,0) para `item`/`option`. 131 declaraciones-atajo en 45 componentes cancelan el velo (66 BASE · 32 ACENTO · 23 HOVER · 10 OTRA; cero valores con gradiente, así que el paso a longhand no tiene riesgo técnico; 32 nodos velados que hoy no reaccionan al ratón pasarían a hacerlo) · la prop `hoverable` de table no suprime nada · en tree-grid el empate (0,5,0) lo decide el ORDEN DE CARGA y varía entre recargas · la banda de tree-grid mata el hover en las filas pares · un token de tinta no puede ganar al arquetipo (`selected-row-fg` se retiró por eso) · el thumb de scroll-area no tiene velo al que migrar. Esto explica de paso la adopción 21/135 que midió la auditoría del 2026-07-01: el velo estaba escrito y estructuralmente derrotado. **§13 — Huecos de instrumento y de demo.** Lo que hizo que una medición mintiera o no existiera, que importa porque el eje entero se apoya en ellas: falsos negativos del centinela sobre propiedades transicionadas, pseudo-elementos y componentes compuestos que no ve, la sonda que no pasa el ratón por un `<tr>`, y la lección general —**una sonda sobre una parte que la demo no monta compara CERO valores y pasa**—, con la lista de partes condicionales afectadas. El handoff gana la regla para las sesiones del eje: si una incidencia mueve píxel o toca morfo, se mide, se anota ahí y se sigue; no se arregla dentro del commit del componente. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>alpha-0.1-background
parent
9748733adc
commit
5596fc8707
@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Análisis del hallazgo sistémico (firma 3): las declaraciones `background:`
|
||||
* (atajo) sobre nodos velados, clasificadas por la CLASE de regla — porque la
|
||||
* clase decide el trabajo:
|
||||
*
|
||||
* BASE la regla sin estado: el fix es mecánico (→ background-color) y
|
||||
* NO mueve píxel en reposo; destapa el velo en hover.
|
||||
* HOVER un hover propio: es la migración de la firma 3 (retirar).
|
||||
* ACENTO selected/checked/active/current con valor de rol: §38 manda
|
||||
* background-color para que el velo COMPONGA encima.
|
||||
* OTRA dragging, disabled, variantes…: caso a caso.
|
||||
*
|
||||
* Y por el VALOR: un token que resuelva a un GRADIENTE no puede pasar a
|
||||
* background-color — romperlo sería peor que el velo ausente.
|
||||
*/
|
||||
import { readFileSync, existsSync, readdirSync } from 'node:fs';
|
||||
import { join, resolve } from 'node:path';
|
||||
import { compileMorfo } from '../src/uix/morfo/compile';
|
||||
import type { Morfo } from '../src/uix/morfo/types';
|
||||
|
||||
const ROOT = resolve('src/uix/eidos/components');
|
||||
const VEILED = new Set(['trigger', 'item', 'option']);
|
||||
const CONTRACT = readFileSync(resolve('src/uix/eidos/lib/recipes/base.ts'), 'utf8').replace(/\r\n/g, '\n');
|
||||
|
||||
// ¿el valor (o el token que referencia, un salto) contiene un gradiente?
|
||||
function hasGradient(value: string): boolean {
|
||||
if (/gradient\(/.test(value)) return true;
|
||||
for (const m of value.matchAll(/var\((--[a-z0-9-]+)/g)) {
|
||||
const name = m[1];
|
||||
// buscar la declaración del token en el contrato (clave → valor)
|
||||
const key = name.replace(/^--[a-z0-9]+(-[a-z0-9]+)*?-/, ''); // aproximación
|
||||
const re = new RegExp("'" + name.slice(2).split('-').slice(1).join('-') + "'\s*:\s*'([^']*)'");
|
||||
const hit = CONTRACT.match(re);
|
||||
if (hit && /gradient\(/.test(hit[1])) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
type Klass = 'BASE' | 'HOVER' | 'ACENTO' | 'OTRA';
|
||||
const stats = new Map<Klass, number>();
|
||||
const perComp = new Map<string, Map<Klass, number>>();
|
||||
const gradients: string[] = [];
|
||||
const accentSamples: string[] = [];
|
||||
const otherSamples: string[] = [];
|
||||
let total = 0;
|
||||
let baseNoHover = 0; // nodos con base-atajo y SIN hover propio: el velo aparecería como hover NUEVO
|
||||
|
||||
for (const dir of readdirSync(ROOT)) {
|
||||
const css = join(ROOT, dir, `${dir}.css`);
|
||||
if (!existsSync(css)) continue;
|
||||
if (!existsSync(resolve(`src/uix/morfo/components/${dir}.ts`))) continue;
|
||||
let compiled;
|
||||
try {
|
||||
const mod = (await import(`../src/uix/morfo/components/${dir}`)) as Record<string, unknown>;
|
||||
const key = `${dir.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase())}Morfo`;
|
||||
if (!mod[key]) continue;
|
||||
compiled = compileMorfo(mod[key] as Morfo);
|
||||
} catch { continue; }
|
||||
const veiledAttrs: string[] = [];
|
||||
for (const kebab of compiled.parts.order) {
|
||||
const part = compiled.parts.byKebab.get(kebab);
|
||||
if (part && VEILED.has(String(part.archetype))) veiledAttrs.push(compiled.parts.attrs[kebab]);
|
||||
}
|
||||
if (!veiledAttrs.length) continue;
|
||||
|
||||
const lines = readFileSync(css, 'utf8').replace(/\r\n/g, '\n').split('\n');
|
||||
const seenBase = new Set<string>();
|
||||
const seenHover = new Set<string>();
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (!/^\s*background\s*:/.test(lines[i])) continue;
|
||||
let sel = '';
|
||||
for (let j = i; j >= 0; j--)
|
||||
if (lines[j].includes('{')) { sel = lines[j].replace('{', '').trim(); break; }
|
||||
const last = sel.split(/\s+(?![^[]*\])/).pop() ?? '';
|
||||
const attr = veiledAttrs.find((a) => last.includes(`[${a}]`));
|
||||
if (!attr || /::/.test(last)) continue;
|
||||
total++;
|
||||
const value = lines[i].replace(/^\s*background\s*:\s*/, '').replace(/;.*/, '');
|
||||
let k: Klass;
|
||||
if (/:hover/.test(last) || /data-state='hover'/.test(last)) { k = 'HOVER'; seenHover.add(attr); }
|
||||
else if (/data-(selected|state='(checked|on|open|active|indeterminate)'|current|highlighted|in-range|range-(start|end))|aria-selected/.test(last)) {
|
||||
k = 'ACENTO';
|
||||
if (accentSamples.length < 8) accentSamples.push(`${dir} :: ${last.slice(0, 55)} → ${value.slice(0, 38)}`);
|
||||
}
|
||||
else if (/:active|:focus|data-(disabled|dragging|readonly|invalid|variant|mode|outside-month)/.test(last)) {
|
||||
k = 'OTRA';
|
||||
if (otherSamples.length < 8) otherSamples.push(`${dir} :: ${last.slice(0, 55)} → ${value.slice(0, 38)}`);
|
||||
}
|
||||
else { k = 'BASE'; seenBase.add(attr); }
|
||||
stats.set(k, (stats.get(k) ?? 0) + 1);
|
||||
const pc = perComp.get(dir) ?? new Map();
|
||||
pc.set(k, (pc.get(k) ?? 0) + 1);
|
||||
perComp.set(dir, pc);
|
||||
if (hasGradient(value)) gradients.push(`${dir} :: ${last.slice(0, 50)} → ${value.slice(0, 45)}`);
|
||||
}
|
||||
for (const a of seenBase) if (!seenHover.has(a)) baseNoHover++;
|
||||
}
|
||||
|
||||
console.log('declaraciones-atajo sobre nodo velado:', total, 'en', perComp.size, 'componentes\n');
|
||||
console.log('por clase de regla:');
|
||||
for (const [k, n] of [...stats].sort((a, b) => b[1] - a[1])) console.log(` ${k.padEnd(7)} ${n}`);
|
||||
console.log('\nnodos con base-atajo y SIN hover propio (el velo sería un hover NUEVO):', baseNoHover);
|
||||
console.log('\nvalores con GRADIENTE (no pueden pasar a background-color):', gradients.length);
|
||||
for (const g of gradients) console.log(' ' + g);
|
||||
console.log('\nmuestras ACENTO:'); for (const s of accentSamples) console.log(' ' + s);
|
||||
console.log('\nmuestras OTRA:'); for (const s of otherSamples) console.log(' ' + s);
|
||||
console.log('\npor componente (BASE/HOVER/ACENTO/OTRA):');
|
||||
const rows = [...perComp].map(([c, m]) => [c, (m.get('BASE')??0)+(m.get('HOVER')??0)+(m.get('ACENTO')??0)+(m.get('OTRA')??0), m] as const).sort((a,b)=>b[1]-a[1]);
|
||||
for (const [c, n, m] of rows) console.log(` ${c.padEnd(20)} ${String(n).padStart(3)} ${m.get('BASE')??0}/${m.get('HOVER')??0}/${m.get('ACENTO')??0}/${m.get('OTRA')??0}`);
|
||||
@ -0,0 +1,34 @@
|
||||
// ¿La fila coge el velo del sistema al pasar el ratón? Y si la tabla NO es
|
||||
// hoverable, ¿lo coge igual? (el velo de `item` no está gated por la prop)
|
||||
import { chromium } from 'playwright';
|
||||
|
||||
const url = process.argv[2] ?? 'http://localhost:5180/uix/components/table';
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
|
||||
await page.goto(url, { waitUntil: 'networkidle' });
|
||||
await page.waitForTimeout(500);
|
||||
await page.addStyleTag({ content: '*, *::before, *::after { transition: none !important; }' });
|
||||
|
||||
const read = async (label: string) => {
|
||||
const row = page.locator('[data-table-body] [data-table-row]').first();
|
||||
const before = await row.evaluate((n) => {
|
||||
const cs = getComputedStyle(n);
|
||||
return { img: cs.backgroundImage, bg: cs.backgroundColor };
|
||||
});
|
||||
await row.hover();
|
||||
await page.waitForTimeout(120);
|
||||
const after = await row.evaluate((n) => {
|
||||
const cs = getComputedStyle(n);
|
||||
return { img: cs.backgroundImage, bg: cs.backgroundColor };
|
||||
});
|
||||
console.log(`${label}\n reposo img: ${before.img}\n hover img: ${after.img}\n hover bg : ${after.bg}`);
|
||||
await page.mouse.move(0, 0);
|
||||
await page.waitForTimeout(120);
|
||||
};
|
||||
|
||||
await read('CON data-hoverable (como la demo)');
|
||||
await page.evaluate(() => {
|
||||
for (const r of document.querySelectorAll('[data-table-root]')) r.removeAttribute('data-hoverable');
|
||||
});
|
||||
await read('SIN data-hoverable (la prop apagada)');
|
||||
await browser.close();
|
||||
@ -0,0 +1,38 @@
|
||||
import { chromium } from 'playwright';
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
|
||||
await page.goto('http://localhost:5180/uix/components/table', { waitUntil: 'networkidle' });
|
||||
await page.waitForTimeout(400);
|
||||
const out = await page.evaluate(() => {
|
||||
const root = document.querySelector('[data-table-root]') as HTMLElement;
|
||||
const row = root.querySelector('[data-table-body] [data-table-row]') as HTMLElement;
|
||||
row.setAttribute('data-selected', '');
|
||||
const res: Record<string, unknown> = { archetype: row.getAttribute('data-archetype') };
|
||||
// ¿quién gana el `color` de la fila seleccionada?
|
||||
const winners: string[] = [];
|
||||
const walk = (rules: CSSRuleList | undefined) => {
|
||||
for (const r of rules ?? []) {
|
||||
const any = r as CSSStyleRule & { cssRules?: CSSRuleList };
|
||||
if (any.cssRules) walk(any.cssRules);
|
||||
if (!any.selectorText || !any.style?.color) continue;
|
||||
try {
|
||||
if (row.matches(any.selectorText)) winners.push(`${any.selectorText.slice(0, 70)} => ${any.style.color}`);
|
||||
} catch { /* selector no evaluable */ }
|
||||
}
|
||||
};
|
||||
for (const sh of document.styleSheets) { try { walk(sh.cssRules) } catch { res.corsSheets = (res.corsSheets as number ?? 0) + 1 } }
|
||||
res.reglasColor = winners;
|
||||
res.computed = getComputedStyle(row).color;
|
||||
// filas pares con striped
|
||||
root.setAttribute('data-striped', '');
|
||||
const even = root.querySelector('[data-table-body] [data-table-row]:nth-of-type(even)') as HTMLElement;
|
||||
res.filaPar = !!even;
|
||||
if (even) {
|
||||
const b = getComputedStyle(even).backgroundColor;
|
||||
root.style.setProperty('--table-striped-row-bg', 'rgb(1, 2, 3)');
|
||||
res.stripedAlcanza = getComputedStyle(even).backgroundColor !== b;
|
||||
}
|
||||
return res;
|
||||
});
|
||||
console.log(JSON.stringify(out, null, 1));
|
||||
await browser.close();
|
||||
@ -0,0 +1,25 @@
|
||||
import { chromium } from 'playwright';
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
|
||||
await page.goto('http://localhost:5180/uix/components/tree-grid', { waitUntil: 'networkidle' });
|
||||
await page.waitForTimeout(500);
|
||||
await page.addStyleTag({ content: '*, *::before, *::after { transition: none !important; }' });
|
||||
|
||||
const boxes = await page.evaluate(() =>
|
||||
[...document.querySelectorAll('[data-tree-grid-row]')].slice(0, 4).map((n, i) => {
|
||||
const r = (n as HTMLElement).getBoundingClientRect();
|
||||
const cs = getComputedStyle(n);
|
||||
return { i, x: r.x + r.width / 2, y: r.y + r.height / 2, restBg: cs.backgroundColor, restImg: cs.backgroundImage.slice(0, 30) };
|
||||
})
|
||||
);
|
||||
for (const b of boxes) {
|
||||
await page.mouse.move(b.x, b.y);
|
||||
await page.waitForTimeout(140);
|
||||
const h = await page.evaluate((i) => {
|
||||
const n = [...document.querySelectorAll('[data-tree-grid-row]')][i] as HTMLElement;
|
||||
const cs = getComputedStyle(n);
|
||||
return { bg: cs.backgroundColor, img: cs.backgroundImage.slice(0, 46), hov: n.matches(':hover') };
|
||||
}, b.i);
|
||||
console.log(`fila ${b.i}: reposo ${b.restBg} → hover ${h.bg} | velo ${h.img} | :hover ${h.hov}`);
|
||||
}
|
||||
await browser.close();
|
||||
Loading…
Reference in new issue