From 466b24f34f7be9e0bf40d77d1997c416ae233e7f Mon Sep 17 00:00:00 2001 From: dev Date: Tue, 25 Aug 2026 02:24:14 +0200 Subject: [PATCH] feat(theming)!: el sobre de trigger de popover es un SUELO - tercera ley 55 El "baseline button envelope" que popover.css pinta a sus triggers vivia a (0,2,0) con hover a (0,5,0): le ganaba a las recetas de sus HUESPEDES (el chip de anadir reaccion de chat-message perdia 9/9 propiedades disputadas y salia cuadrado gris entre pildoras - SU receta lo pintaba en el mismo selector-lista y PERDIA), mataba la capa de estado del sistema en hover (chronos plain = caja gris) y dejaba el trigger de calendar en manos del orden de carga (14/16/14/14px medidos - la moneda al aire). El sobre baja a :where() - base y hover, con el :not(field-trigger) DENTRO (where anula especificidad, no matching). El suelo pinta lo que el huesped CALLA; la receta gana donde habla; la capa de estado compone otra vez. Tercera aplicacion de la ley firmada (12.9 planos - B'/53 paleta - y archetypes.css:9-21 ya lo decia para SU capa). Cambio de codigo: 2 lineas. Medido (gate de dos niveles, 76 instancias, 16 rutas, 4 estados): - No afectados: 3 field-trigger diff VACIO; los 66 desnudos ganan EXACTAMENTE los dos valores del SISTEMA que el sobre tapaba (velo de hover de archetypes + su transition). 167 nodos de referencia: 0. - Los 7 movers, valor a valor por CDP hacia lo que SU receta declara: el chip vuelve pildora (36->26px, r9999) - chronos recupera su plain y su hover - calendar mes/anio pasa a su ghost compuesto Y SE VUELVE DETERMINISTA (8 cargas en dos ordenes, mismo pixel; antes 2 de 6 caian del otro lado) - emoji/palabras/ntp a sus recetas (ntp revive 4 claves: 44->48/62). - Capturas antes/despues x8 revisadas por el supervisor (chip, chronos, calendar, emoji, gradient, popover generico, palabras, ntp). - Guarda 55 en active-eidos-config.test.ts validada por SEIS mutaciones (la sexta se anadio porque la primera pasada dejo pasar un verde falso; y el arnes mintio con -t sobre 0 tests - detector de cero-tests anadido). - vitest 35/36 (rojo ajeno skin-media-player) - check 0 atribuibles - docs-check 0/0 - eidos-lint popover/chat-message 0 invalid. Registrado en 13 y fichas: los parches-pin de chronos/calendar pasan a REVISABLES (ahora redundantes o semi) - el suelo comparte peldano (0,0,0) con archetypes.css (medido 16 cargas: archetypes cae despues y el resultado es identico, pero es un empate real sin verificar en produccion) - la dependencia sin registrar de palabras queda anotada junto a su regla (no tiene README de eidos). changelog 55 escrito (la guarda citaba una seccion que debia existir). CON ESTA, LA LISTA "Lo que espera TU FIRMA" DEL CONTINUE QUEDA VACIA. Co-Authored-By: Claude Fable 5 --- docs/audit/theming/chat-message.md | 9 ++ docs/audit/theming/popover.md | 13 +++ docs/next-features.md | 61 ++++++++++- docs/process/CONTINUE-theming.md | 29 ++++- docs/process/PLAN-theming.md | 76 +++++++++++++ docs/theming/changelog.md | 80 +++++++++++++- src/uix/eidos/active-eidos-config.test.ts | 103 ++++++++++++++++++ .../eidos/components/chat-message/README.md | 16 ++- .../components/palabras/palabras-chrome.css | 9 ++ src/uix/eidos/components/popover/README.md | 12 ++ src/uix/eidos/components/popover/popover.css | 35 +++++- 11 files changed, 433 insertions(+), 10 deletions(-) diff --git a/docs/audit/theming/chat-message.md b/docs/audit/theming/chat-message.md index 89c9ae9f0..22f42abff 100644 --- a/docs/audit/theming/chat-message.md +++ b/docs/audit/theming/chat-message.md @@ -177,6 +177,15 @@ de 45) / `emoji-picker` (3). Los `reaction-*` NO mienten —alcanzan sobre los chips normales—, así que no hay nada que retirar; lo que hay es una decisión de diseño pendiente. Registrado, no tocado (D-TH.5). +**D-TH.5 — CERRADO 2026-08-25 por la firma «el sobre de trigger de popover es el +SUELO»** (changelog §55). Se resolvió donde estaba el defecto, no aquí: el sobre +de `popover.css` bajó a `:where(...)` (0,0,0) —tercera aplicación de la ley de +§12.9 y §53— y el bloque de esta receta gana ya sus siete propiedades. El chip +mide **34×26, radio 9999px, fondo `oklch(0.9911 0 0)`, 12px de letra, 8px de +padding**: la misma píldora que sus hermanas. Cero claves retiradas y cero +añadidas — el número del componente no se mueve, el centinela sigue en 77/81 con +las mismas cuatro adjudicadas. + **Lo que queda fuera y por qué**: cuatro identidades firmadas (§1.4) y siete knobs de sistema transversal (§2, anillo de foco × 4, velo de estado × 2, `--opacity-muted` del `sending`). No hay deuda: el componente está CERRADO en su diff --git a/docs/audit/theming/popover.md b/docs/audit/theming/popover.md index a7cc6a706..7d20599e1 100644 --- a/docs/audit/theming/popover.md +++ b/docs/audit/theming/popover.md @@ -116,6 +116,19 @@ no distingue lo que debería — se marca `⚠`. **EJECUTADO 2026-08-22 — 46 % → 67 %**, 11 claves de costura para el DISPARADOR + una retirada. Lo que queda fuera es UNA cosa, y no es suya. +**NOTA 2026-08-25 — el sobre de disparador pasa a SUELO** (firma, changelog +§55). Las doce claves de disparador + estado descritas abajo no cambian de +nombre ni de número: cambia su ESPECIFICIDAD. Las dos reglas que las consumen +—el reposo y su `:hover`— se emiten desde `:where(...)`, (0,0,0), porque el +propio fichero llama a esta regla _«a baseline button envelope»_ y un baseline a +(0,2,0) le gana a toda receta de huésped a (0,1,0). Medido: **69 de 76** +instancias del catálogo no mueven un valor de receta —el disparador desnudo, que +es para quien se escribió, sigue vestido entero— y las **7** que mueven lo hacen +hacia lo que su propia receta declara. Tercera aplicación de la ley de §12.9 +(plano de profundidad) y §53 (escalera de paleta). `[data-state='open']` y +`:focus-visible` se quedan donde estaban: fuera de la firma, en +`next-features.md` §13. + **El disparador genérico es cromo propio**: el que no lleva el arquetipo `field-trigger` (que tiene su tratamiento aparte) montaba su alto, su padding, su borde, su radio, su superficie y su tinta sobre primitivos crudos. diff --git a/docs/next-features.md b/docs/next-features.md index ed184ab06..054119a24 100644 --- a/docs/next-features.md +++ b/docs/next-features.md @@ -531,6 +531,52 @@ instrumental para medirla ya existe (`__statelayer-analysis.ts` + **Origen**: la misma sesión. Cosas que hicieron que una medición mintiera o no existiera. Se anotan porque el eje entero se apoya en esas mediciones. +- **Los PARCHES-PIN que la firma §55 vuelve redundantes o semi-redundantes** + (2026-08-25, NO tocados — cada uno es una decisión aparte). Cuando el sobre de + trigger de `popover.css` estaba a (0,2,0), tres componentes se pincharon un + selector propio ENCIMA para recuperar su cromo. Con el sobre en el suelo, esos + pines ya no defienden de nada — pero siguen pesando, y ahora ganan a reglas que + quizá deberían ganarles: + - `chronos.css:417` — `[data-popover-trigger][data-chronos-more-link]:not(…)` + a **(0,3,0)**, con su comentario diciendo por qué existe. Medido hoy: su + `background: var(--_button-bg)` (shorthand) le gana al velo de hover de + `archetypes.css` (0,0,0), así que el `+N more` NO recibe la capa de estado + del sistema; su afordancia de hover es el `text-decoration: underline` de su + propia receta, que es correcta para un enlace pero NO es lo que el pin + pretendía. **Retirarlo es un cambio de píxel: firma aparte.** + - `calendar` — sus selectores de mes/año NO llevan pin propio; el que empataba + era `[data-calendar-month-select][data-button]` (0,2,0), que hoy gana solo. + Nada que revisar. + - `chat-message.css:462` — `[data-chat-message-actions] [data-chat-message-reaction-add]` + (0,2,0) **era una trampa armada** (empataba con el sobre y ninguna superficie + del repo monta ese anidamiento). Hoy gana de forma determinista: medido + montando el nodo, con el sobre viejo reinyectado PIERDE (borde 1px, fondo + `0.9821`, 36px) y con el suelo GANA (borde 0, fondo transparente, píldora de + 26px). **Ya no es deuda**; se anota porque documenta que la clase se cierra + entera con una sola decisión. +- **Las DOS reglas de estado de `popover.css` que la firma §55 dejó ARRIBA** + (2026-08-25): `[data-popover-trigger]:not(…)[data-state='open']` y + `…:focus-visible`, las dos a **(0,3,0)**. No estaban en la firma, así que no se + tocaron — pero el coste de dejarlas está medido: bajarlas también sube el + centinela de `natural-time-picker` de **48/62 a 50/62** (reviven `trigger-bg` + y `open-trigger-border`, que hoy pierden contra el repintado de popover cuando + el panel está abierto). El paralelo de §12.9 dice que sí deberían bajar — + ninguna de las dos es una petición explícita por elemento como el `frost`, y + `archetypes.css:19-21` ya declara que el anillo de foco «must lose to any + component rule». **Decisión pendiente, con su número.** +- **El suelo de `popover.css` COMPARTE peldaño con `archetypes.css`** + (2026-08-25, medido, sin defecto observado). Al bajar a (0,0,0), las dos reglas + del sobre empatan con `:where([data-archetype='trigger'])` y su gemela de + hover, que declaran `transition` y el velo de estado. El orden lo decide el + documento. **Medido 16 cargas limpias en 4 rutas: archetypes SIEMPRE cae + después** (los índices de hoja varían de `sheet0` a `sheet19` y el resultado no + cambia), así que hoy el sistema gana su `transition` y su velo — que es lo que + la firma quería. Pero es un empate de verdad, y **no se ha verificado en + producción** (bundle de Vite en vez de dev), donde el orden de emisión es otro. + Si alguna vez se invierte, un trigger desnudo perdería el velo de hover. + El arreglo limpio sería que `popover.css` dejara de declarar hover propio y se + apoyara en la capa del sistema — otra firma. + - ~~**El centinela da falsos negativos sobre una propiedad transicionada.**~~ **CAUSA ENCONTRADA Y ARREGLADA 2026-08-21** (revisión adversarial): no era la transición. El paso «abrir lo que se pueda abrir» hacía CLIC en @@ -1523,7 +1569,9 @@ Lo que `chart` destapó (2026-08-22): marcador tras el clic) en vez de dormir un número fijo. Mientras tanto, su entrada de ledger sale STALE en las corridas donde alcanza: está escrito en la propia razón para que nadie la borre por eso. -- **⚠⚠ UN COMPONENTE COMPUESTO GANA A LA RECETA QUE LO COMPONE, y ya van TRES** +- **✅ ~~⚠⚠ UN COMPONENTE COMPUESTO GANA A LA RECETA QUE LO COMPONE, y ya van + TRES~~ — RESPONDIDA 2026-08-25 para la mitad `data-popover-trigger`, por la + firma «el sobre de trigger de popover es el SUELO»** (changelog §55). (medido 2026-08-23). Cuando una receta estiliza un nodo que también lleva `data-button` / `data-popover-trigger`, los dos selectores casan a la MISMA especificidad (0,1,0) y decide el ORDEN DE CARGA — que gana el componente @@ -1534,6 +1582,17 @@ Lo que `chart` destapó (2026-08-22): o la receta que lo compone**. Si la respuesta es «la receta», el arreglo es subir la especificidad de esas reglas y no retirar nada; si es «el compuesto», hay ~62 claves públicas que retirar en tres componentes. + **La respuesta firmada es «LA RECETA»**, y no se ejecutó subiendo la + especificidad de cada víctima sino BAJANDO la del baseline a `:where()`, que es + la misma herramienta de §12.9 y §53. Nótese que el enunciado de arriba se + quedaba corto en un punto medido: el sobre de `popover.css` no estaba a + (0,1,0) sino a **(0,2,0)** —y su `:hover` a (0,5,0)—, así que no era un empate + por orden de carga, era una derrota limpia; el empate por orden sí existía + entre el sobre y `[data-calendar-*-select][data-button]`, los dos a (0,2,0) + (reproducido: 2 de 6 cargas limpias en la cara equivocada). **Lo que queda + abierto** es la otra mitad, `data-button` contra la receta que lo compone + (`form` 23 claves adjudicadas): esa firma no se ha hecho, y `button.css` NO se + tocó aquí. - **R-5.3 no guarda el vocabulario DIMENSIONAL** (medido 2026-08-23 en `badge`). Cubre la ranura de tinta (`fg` vs `color`) y la posición del modificador, pero no que una clave POR TALLA se llame `…-height-{k}` y no diff --git a/docs/process/CONTINUE-theming.md b/docs/process/CONTINUE-theming.md index 58b64a0d7..be22b5f64 100644 --- a/docs/process/CONTINUE-theming.md +++ b/docs/process/CONTINUE-theming.md @@ -180,8 +180,33 @@ guarda `active-eidos-config.test.ts` · ledger `scripts/theming-sentinel-excepti §13): la tinta de tono de la etiqueta activa, enmascarada por `[data-archetype='item'][data-highlighted]` (0,3,0) — **no** por `aria-selected`, atribución que esta firma corrige midiendo. - 6. **El chip de añadir reacción de `chat-message`** lo pinta `popover.css`: - los chips de reacción son píldoras redondas y el de añadir un cuadrado gris. + 6. ~~**El chip de añadir reacción de `chat-message`** lo pinta `popover.css`: + los chips de reacción son píldoras redondas y el de añadir un cuadrado + gris.~~ — **✅ FIRMADA Y EJECUTADA 2026-08-25** (opción b, «la ley del + suelo»): el arreglo NO fue de `chat-message` sino del sobre de + `popover.css`, que se autodescribe como _«a baseline button envelope»_ y + estaba a **(0,2,0)** con su `:hover` a **(0,5,0)**. Baja a + `:where(...)` — (0,0,0) — reposo y hover. **Tercera aplicación de la ley** + de §12.9 y de B′/§53, y la primera que cae en una receta escrita a mano. + **76 instancias en 16 rutas, cuatro estados**: 69 no mueven un valor de + receta (los 64 botones desnudos del sitio de docs, `gradient-picker`, la + demo `data-perm-step` y los tres field-trigger excluidos) y **7 mueven, las + 7 hacia lo que su propia receta declara** — el chip vuelve a ser **píldora** + (34×26, radio 9999, 12px), `chronos` recupera su `plain`, `calendar` pasa a + ghost y **deja de ser no determinista** (reproducido antes: 2 de 6 cargas en + 14px en vez de 16px; después: 8 cargas en dos órdenes, el mismo píxel), + `emoji-picker`, `palabras` y `natural-time-picker` recuperan lo suyo. **0 + movimientos** en 167 nodos de referencia. Centinela: `natural-time-picker` + **44/62 → 48/62**; ninguna entrada del ledger salió STALE (ninguna estaba + adjudicada por este sobre). Cierra de paso el empate latente de + `chat-message.css:462`. Guarda por mutación en + `active-eidos-config.test.ts` (§55, 6 mutaciones, 6 mordidas). Doctrina en + `theming/changelog.md` §55; lo que queda abierto —los dos estados a (0,3,0) + y los pines redundantes— en `next-features.md` §13. + + **CON ESTO LA LISTA QUEDA VACÍA: no espera NINGUNA firma tuya en este eje.** + Los seis puntos están ejecutados. Lo que siga tiene que abrirse como entrada + nueva, no heredarse de aquí. ### Lo último que se hizo (2026-08-24 noche): §12.9 FIRMADA diff --git a/docs/process/PLAN-theming.md b/docs/process/PLAN-theming.md index 84746ea93..30d485d67 100644 --- a/docs/process/PLAN-theming.md +++ b/docs/process/PLAN-theming.md @@ -521,6 +521,82 @@ excepción abre la mano para MEDIR, no para contabilizar. ## 8. Registro +- 2026-08-25 — **FIRMA: el sobre de trigger de `popover` es el SUELO, no el + techo** (opción b del expediente V4′; doctrina en + [`theming/changelog.md`](../theming/changelog.md) §55). **Tercera aplicación de + la misma ley** —§12.9 el plano de profundidad, §53 la escalera de paleta— y la + primera que cae en una receta ESCRITA A MANO en vez del CSS generado. + **Qué cambia**: `popover.css` se autodescribe como _«a baseline button + envelope»_ y emitía ese sobre a **(0,2,0)**, con el `:hover` a **(0,5,0)**. Un + baseline a (0,2,0) le gana a toda receta de huésped a (0,1,0) — eso no es un + baseline, es un techo. Las dos reglas pasan a + `:where([data-popover-trigger]:not([data-archetype='field-trigger']))` y su + gemela de `:hover`. El `:not()` se queda **DENTRO** del `:where()`: anula la + contribución de especificidad, nunca el emparejamiento, así que los cinco + triggers de picker siguen excluidos igual, ahora a (0,0,0); sacarlo fuera + dejaría la regla a (0,1,0), un empate nuevo con toda receta de huésped. + **Sonda §7.2/§7.4** (76 instancias de trigger en 16 rutas × reposo · hover · + `:focus-visible` · abierto, transiciones y animaciones congeladas, más 167 + nodos de referencia medidos en paralelo): + **69 instancias no mueven un solo valor de RECETA** — los 64 botones desnudos + del sitio de docs, `gradient-picker`, el `data-perm-step` de la demo y los tres + field-trigger excluidos (estos tres, con diff literalmente vacío). De esas 69, + **66 recuperan dos valores del SISTEMA** que el (0,2,0)/(0,5,0) les tapaba: el + velo de hover de `archetypes.css` y su `transition`. Es la firma trabajando + —«la capa de estado del sistema vuelve a componer en hover»—, no daño + colateral, y **la sonda de V4′ no podía verlo** porque no medía hover ni + `background-image`. + **7 identidades mueven, y las 7 hacia lo que su propia receta declara**, + verificado además por atribución CDP (`getMatchedStylesForNode`) propiedad a + propiedad: `chat-message` chip → **píldora** (36→26px, radio 6→9999px, fondo + `0.9821`→`0.9911`, padding 12→8, letra 14→12, ancho 42→34), `chronos` `+N more` + → tinta primaria y **sin caja gris al hover** (fondo `0.931`→transparente, + borde `0.7921`→transparente), `calendar` mes y año → **ghost** (fondo y borde + transparentes, tinta primaria, padding 12→16), `emoji-picker` → su icon-button + (fondo→transparente, borde 1→0, padding 12→0), `palabras` → radio 6→4, tinta + `content-subtle`, padding 12→8, letra 14→12, `natural-time-picker` → fondo + `0.9821`→`0.9911` y letra 14→16. + **`calendar` deja de ser una moneda al aire.** El empate era + `[data-calendar-month-select][data-button]` (0,2,0) contra el sobre (0,2,0), + las dos declarando `font-size`; **reproducido antes por CDP: 2 de 6 cargas + limpias cayeron en 14px y 4 en 16px**. Después: **8 cargas en dos órdenes de + carga distintos** (directa y navegando desde `/popover`) → el mismo píxel + siempre. Es la deuda que `next-features.md:1847` declaraba envenenando el + instrumento de medida de todo este eje. + **Centinela**: `natural-time-picker` **44/62 → 48/62** — reviven + `trigger-padding-inline`, `trigger-fg`, `trigger-radius` y `trigger-font-size`, + que su README ya registraba como inertes. `chat-message` 77/81, `calendar` + 68/74, `emoji-picker` 17/21, `gradient-picker` 9/25, `popover` 35/46 y + `chronos` 6/9 **sin cambio** (comparados antes/después reconstruyendo la + cascada vieja en el fichero, nunca por `git stash`). **Ninguna entrada del + ledger salió STALE, y ninguna estaba adjudicada por este sobre**: los + componentes que V4′/V1 citaban habían RETIRADO sus claves en vez de + adjudicarlas. Los 3 muertos sin adjudicar de `chronos` (`num-row`, + `timegrid-block-size`, `disabled-opacity`) son **previos y ajenos** —medidos + idénticos con la cascada vieja—, deuda de otro eje. + **Cierra la trampa latente de `chat-message.css:462`**: el bloque «los chips + dentro de la píldora sueltan su cromo» está a (0,2,0) y **empataba** con el + sobre. Medido montando el nodo que ninguna superficie del repo monta: con el + sobre viejo reinyectado a (0,2,0) el bloque PIERDE (borde 1px, fondo `0.9821`, + 36px); con el suelo GANA (borde 0, fondo transparente, la píldora de 26px). La + misma decisión resuelve las dos, sin tocar `chat-message.css`. + **Guarda por MUTACIÓN** en `active-eidos-config.test.ts` (§55): **6 mutaciones, + 6 mordidas** — reposo desenvuelto · hover desenvuelto · `:not()` izado fuera + del `:where()` · suelo vaciado (anti-vacío) · una QUINTA regla de trigger fuera + del suelo (cierre estructural) · y las dos reglas de estado bajadas en silencio + (la sexta se añadió PORQUE la primera pasada la dejó pasar verde). Censo de la + propia guarda: exactamente **4 reglas** de trigger en la hoja. + **Lo que NO se tocó, y por qué**: `[data-state='open']` (0,3,0) y + `:focus-visible` (0,3,0) no estaban en la firma — coste de dejarlos, medido: + el centinela de `natural-time-picker` se quedaría en 50/62 si bajaran. El pin + (0,3,0) de `chronos.css:417` sigue en pie (ahora semi-redundante: le gana al + velo del sistema). Los dos, en `next-features.md` §13. + **Registrado además**: `palabras` dependía del sobre **sin figurar en ningún + sitio** —y no puede figurar en el censo, porque **no tiene bloque de receta** + (`theming:sentinel -- palabras` muere con «no recipe block»)—, así que la + dependencia queda escrita como comentario en `palabras-chrome.css`, junto a la + regla, con los tres ejes que sigue heredando (`height`, `border`, + `background`). - 2026-08-25 — **FIRMA: las cuatro claves de `avatar` fuera de gramática dicen ya la verdad** (opción A-c). Las cuatro eran las **únicas 4 desviaciones de 4.558 claves públicas** del catálogo, y la medida demostró que **no son la diff --git a/docs/theming/changelog.md b/docs/theming/changelog.md index 5d9087b2c..b726e1d00 100644 --- a/docs/theming/changelog.md +++ b/docs/theming/changelog.md @@ -2235,6 +2235,80 @@ no es un contrato. --- -**Última revisión**: 2026-08-24 (§54 una palabra, un significado). Si algo -en este doc no coincide con el código, el código gana — pero abre un issue para -que actualicemos el doc. +## 55. El sobre de trigger de `popover` es el SUELO, no el techo (2026-08-25) + +**FIRMA — TERCERA aplicación de la misma ley.** §12.9 la firmó para el plano de +profundidad, §53 (B′) para la cascada de paleta, y ésta es la primera que cae en +una receta ESCRITA A MANO en vez del CSS generado. `popover.css` se +autodescribe como _«a baseline button envelope»_ y lo emitía a **(0,2,0)**, con +el `:hover` a **(0,5,0)**. Un baseline a (0,2,0) le gana a TODA receta de +huésped a (0,1,0): eso no es un baseline, es un techo. + +**Lo que pintaba de más, medido**: el chip de añadir reacción de `chat-message` +salía **cuadrado gris al lado de sus propias píldoras** (las siete propiedades +del sobre declaradas por su receta y las siete perdidas); los selectores de mes +y año de `calendar` **empataban a (0,2,0)** con su propia regla +(`[data-calendar-month-select][data-button]`) y leían **14 px o 16 px según qué +chunk cargara el último** — reproducido: **2 de 6 cargas limpias** cayeron en la +cara equivocada; y el `:hover` a (0,5,0) **mataba la capa de estado del sistema** +en todo huésped que pinta su propia superficie. + +**La regla baja a `:where(...)`** — reposo y hover. `archetypes.css:9-21` ya +había escrito esta ley para la capa transversal (_«These are DEFAULTS — a +component recipe must ALWAYS be able to override them»*) y nombró sus **dos +excepciones deliberadas**; este sobre hacía trabajo de default y no estaba en +ninguna de las dos. + +**El `:not([data-archetype='field-trigger'])` se queda DENTRO del `:where()`.** +`:where()` anula la CONTRIBUCIÓN de especificidad, nunca el emparejamiento: los +cinco triggers de picker siguen excluidos exactamente igual, ahora a (0,0,0). +Sacarlo fuera dejaría la regla a (0,1,0) — un empate nuevo con toda receta de +huésped, que es la moneda al aire que §12.9 y §53 se firmaron para prohibir. + +**Los números** (76 instancias de trigger en 16 rutas, reposo · hover · foco · +abierto, transiciones congeladas): + +- **69 no mueven un solo valor de receta** — los 64 botones desnudos del sitio de + docs, `gradient-picker`, el `data-perm-step` de la demo y los tres + field-triggers excluidos. El baseline sigue haciendo su trabajo justo donde se + escribió para hacerlo. De esas 69, **66 sí recuperan dos valores del SISTEMA**: + el velo de hover de `archetypes.css` y su `transition`, que el (0,5,0)/(0,2,0) + les tapaba. Es la firma trabajando, no un daño colateral. +- **7 identidades mueven, y las 7 hacia lo que su propia receta declara**: el + chip de `chat-message` vuelve a ser **píldora** (26 px, radio 9999, fondo de la + píldora, 12 px de letra — idéntico a sus hermanas), `chronos` recupera su + `plain` (tinta primaria, sin caja gris al hover), `calendar` pasa a **ghost** y + **deja de ser no determinista** (8 cargas en dos órdenes → el mismo píxel), + `emoji-picker` recupera su icon-button transparente, `palabras` su radio y su + tinta, `natural-time-picker` su fondo y su cuerpo de letra. +- **0 movimientos** en 167 nodos de referencia medidos en paralelo (`[data-button]` + que no son trigger, píldoras de reacción, celdas de calendario y de chronos). +- **Centinela**: `natural-time-picker` **44/62 → 48/62** — reviven + `trigger-padding-inline`, `trigger-fg`, `trigger-radius` y `trigger-font-size`, + que su README ya registraba como inertes. Ninguna entrada del ledger salió + STALE: **ninguna estaba adjudicada por este sobre** (`gradient-picker` y + `emoji-picker` habían RETIRADO sus claves en vez de adjudicarlas). + +**Cierra de paso la trampa latente de `chat-message.css:462`** — el bloque +«los chips dentro de la píldora sueltan su cromo» está a (0,2,0) y **empataba** +con el sobre; hoy no muerde porque ninguna superficie monta un `ReactionAdd` +dentro de `Actions`, pero estaba armada. Medido montando el nodo: con el sobre +viejo reinyectado a (0,2,0) el bloque PIERDE (borde 1 px, fondo `0.9821`, +36 px); con el suelo GANA (borde 0, fondo transparente, la píldora de 26 px). +La misma decisión resuelve las dos. + +**Lo que NO baja, y es decisión, no descuido**: `[data-state='open']` (0,3,0) y +`:focus-visible` (0,3,0). No estaban en la firma. Medido lo que cuesta dejarlos: +bajarlos también subiría `natural-time-picker` a **50/62** (reviven +`trigger-bg` y `open-trigger-border`). Registrado en `next-features.md` §13. + +Guarda por **mutación** en `active-eidos-config.test.ts` (§55, 6 mutaciones, +**6 mordidas**): reposo desenvuelto · hover desenvuelto · `:not()` izado fuera +del `:where()` · suelo vaciado · una QUINTA regla de trigger fuera del suelo · +y las dos reglas de estado bajadas en silencio. + +--- + +**Última revisión**: 2026-08-25 (§55 el sobre de trigger de popover es el +suelo). Si algo en este doc no coincide con el código, el código gana — pero +abre un issue para que actualicemos el doc. diff --git a/src/uix/eidos/active-eidos-config.test.ts b/src/uix/eidos/active-eidos-config.test.ts index 57192703d..8739b63d3 100644 --- a/src/uix/eidos/active-eidos-config.test.ts +++ b/src/uix/eidos/active-eidos-config.test.ts @@ -1,3 +1,5 @@ +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' import { describe, expect, it } from 'vitest' import { renderStaticCss, renderThemeCss } from './lib/render-css' import { computeLightSolidScales } from './lib/on-solid' @@ -569,6 +571,107 @@ describe('ActiveEidos config', () => { expect(css).toContain(":where([data-depth='sheet']) {"); }); + /** + * §55 — THE POPOVER TRIGGER ENVELOPE IS THE FLOOR (signed 2026-08-25). + * Third application of the law: §12.9 above made the depth PLANE a floor, + * §B′ below made the palette cascade a ladder, and this one lands on the + * only baseline that lived in a HAND-AUTHORED recipe — so it is read from + * the file, not from `renderStaticCss()`. + * + * `popover.css` calls its trigger rule "a baseline button envelope" and + * shipped it at (0,2,0), hover at (0,5,0). That beat every host recipe at + * (0,1,0): the chat-message add-chip rendered a grey square beside its own + * pills, the calendar month/year selects TIED at (0,2,0) with their own rule + * and read 14px or 16px depending on which chunk loaded last (measured: 2 of + * 6 fresh loads landed on the wrong face), and the (0,5,0) hover erased the + * system's hover layer on every host that paints its own surface. + * `archetypes.css` (lines 9-21) had already written this law for the + * transversal layer and named its TWO deliberate exceptions; this envelope + * did a default's work and was in neither. + */ + it('§55 — popover trigger envelope rides the floor: rest + hover in :where(), never bare', () => { + const css = readFileSync( + resolve('src/uix/eidos/components/popover/popover.css'), + 'utf8' + ).replace(/\/\*[\s\S]*?\*\//g, ''); + // popover.css has no nested at-rules (asserted below), so a flat split is + // exact — and it never credits a wrapper for what it holds. + const rules = [...css.matchAll(/([^{}]+)\{([^{}]*)\}/g)].map((m) => ({ + selector: m[1].replace(/\s+/g, ' ').trim(), + body: m[2] + })); + expect(css, 'nested at-rule in popover.css — the flat split stopped being exact').not.toMatch( + /@(media|supports|container)/ + ); + + const BASE = "[data-popover-trigger]:not([data-archetype='field-trigger'])"; + + // (a) the REST envelope rides the floor — and it is the block that carries + // the envelope. An empty floor proves nothing. + const rest = rules.find((rule) => rule.selector === `:where(${BASE})`); + expect(rest, `no \`:where(${BASE}) {\` block in popover.css`).toBeDefined(); + for (const token of [ + '--popover-trigger-bg', + '--popover-trigger-height', + '--popover-trigger-radius', + '--popover-trigger-padding-inline', + '--popover-trigger-font-size', + '--popover-trigger-border' + ]) + expect(rest!.body, `floor block declares no ${token}`).toContain(`var(${token})`); + + // (b) the HOVER rides the floor too. It was the (0,5,0) rung, the one that + // repainted the surface instead of letting the system's hover layer + // compose over whatever the host already painted. + const hover = rules.find( + (rule) => rule.selector.startsWith(':where(') && rule.selector.includes(`${BASE}:hover`) + ); + expect(hover, 'no `:where(… :hover …) {` block in popover.css').toBeDefined(); + expect(hover!.body).toContain('var(--popover-hover-trigger-bg)'); + expect(hover!.body).toContain('var(--popover-hover-trigger-border)'); + + // (c) …and NEITHER bare form is back. `:where()` zeroes the specificity + // CONTRIBUTION, never the matching, so the `:not()` stays INSIDE it: the + // five field-trigger pickers are excluded exactly as before, at (0,0,0). + // Hoisting it out — `:where([data-popover-trigger]):not(…)` — would read + // (0,1,0), a fresh tie with every host recipe, i.e. the coin flip §12.9 + // and §B′ were signed to forbid. + expect(css, 'the bare (0,2,0) envelope is back').not.toContain(`\n${BASE} {`); + expect(css, 'the bare (0,5,0) hover is back').not.toContain(`\n${BASE}:hover`); + expect( + css, + 'the `:not()` was hoisted out of the `:where()` — (0,1,0), a new tie' + ).not.toContain(':where([data-popover-trigger]):not('); + + // (d) the two state rules the signature deliberately left ABOVE the floor + // keep their specificity. Mirror of the frost assertion in §12.9 and of + // the tone rung in §B′: same wrapper, opposite intent. Demoting them is a + // separate decision with its own measurement, not a tidy-up — a `:where()` + // here would drop `[data-state='open']` onto the floor and re-open the + // question this signature answered for two rules only. + expect(css, "`[data-state='open']` was demoted — outside the signed scope").toContain( + `\n${BASE}[data-state='open'] {` + ); + expect(css, '`:focus-visible` was demoted — outside the signed scope').toContain( + `\n${BASE}:focus-visible {` + ); + + // (e) structural closer: EVERY rule aimed at the trigger is either on the + // floor or one of those two. A fifth one is a tie waiting to happen, + // whoever wrote it. + const triggerRules = rules.filter((rule) => rule.selector.includes('[data-popover-trigger]')); + for (const rule of triggerRules) { + if (rule.selector.startsWith(':where(')) continue; + expect( + [`${BASE}[data-state='open']`, `${BASE}:focus-visible`], + `popover.css: \`${rule.selector} {\` aims at the trigger off the floor` + ).toContain(rule.selector); + } + // A guard that inspected NOTHING passes green: the census is the other half + // of the assertion. Four trigger rules — two on the floor, two above it. + expect(triggerRules).toHaveLength(4); + }); + it('emits the Fase 4 frost atmosphere (opt-in [data-frost] + blur tokens)', () => { const css = createThemeBaseEidos().renderStaticCss(); // Blur is a canonical primitive (`--blur-*`); the planes consume it instead diff --git a/src/uix/eidos/components/chat-message/README.md b/src/uix/eidos/components/chat-message/README.md index 306b7102e..eb78f5301 100644 --- a/src/uix/eidos/components/chat-message/README.md +++ b/src/uix/eidos/components/chat-message/README.md @@ -107,13 +107,25 @@ compuesta DENTRO de un Feed, y su propia demo la monta suelta (medida en la de `chat-log`); y `quick-react-hover-scale` — `transform` no está en la lista de propiedades del guard y la celda es un botón portalado con regla `:hover`. -**Defecto conocido, NO tocado aquí**: el chip de añadir reacción lleva también +~~**Defecto conocido, NO tocado aquí**: el chip de añadir reacción lleva también `data-popover-trigger`, y `[data-popover-trigger]:not([data-archetype='field-trigger'])` pesa (0,2,0) contra los (0,1,0) de esta receta — así que su fondo, su borde, su radio y su tinta los pinta `popover.css`, no `reaction-*`. Se ve en la demo: los dos chips de reacción son píldoras redondas y el de añadir, un cuadrado gris. Es la clase `gradient-picker` / `emoji-picker`; arreglarlo mueve píxel, así que -queda registrado, no corregido. +queda registrado, no corregido.~~ + +**RESUELTO 2026-08-25 — firma «el sobre de trigger de popover es el SUELO»** +(changelog §55). El arreglo NO fue de este componente: el sobre de `popover.css` +bajó a `:where(...)` (0,0,0), así que el bloque `[data-chat-message-reaction], +[data-chat-message-reaction-add]` gana ya las siete propiedades que siempre +declaró. **El chip es una píldora**, idéntica a sus hermanas — medido: 34×26, +radio 9999px, fondo `oklch(0.9911 0 0)`, letra 12px, padding 8px. Y al hover +recupera la capa de estado del sistema (el velo sobre su propio fondo) en vez +del repintado gris que le metía el `:hover` (0,5,0) del sobre. De paso queda +resuelto el empate latente de `chat-message.css:462` — el bloque «los chips +dentro de la píldora sueltan su cromo» está a (0,2,0) y **empataba** con el +sobre; hoy gana de forma determinista. ## Tokens (`--chat-message-*`) diff --git a/src/uix/eidos/components/palabras/palabras-chrome.css b/src/uix/eidos/components/palabras/palabras-chrome.css index e29c54d69..cb571e313 100644 --- a/src/uix/eidos/components/palabras/palabras-chrome.css +++ b/src/uix/eidos/components/palabras/palabras-chrome.css @@ -360,6 +360,15 @@ font-size: var(--font-size-xs); color: var(--color-content-subtle); } +/* This button is a `Popover.Trigger`, so it also wears the baseline envelope of + * `popover.css` — which, since 2026-08-25 (changelog §55), is a FLOOR at + * (0,0,0): the rules below win everything they name. What they do NOT name, + * the floor still paints, and today that is three axes, measured: `height` + * (36px), `border` (1px + `--popover-trigger-border`) and `background` + * (`--popover-trigger-bg`). This is a REGISTRATION, not a defect: the chrome is + * inherited on purpose. It is written down because `palabras` has no recipe + * block in `lib/recipes/base.ts` — `npm run theming:sentinel -- palabras` dies + * with "no recipe block" — so no census and no sentinel would ever surface it. */ [data-palabras-outline-trigger] { display: inline-flex; align-items: center; diff --git a/src/uix/eidos/components/popover/README.md b/src/uix/eidos/components/popover/README.md index 73145d4af..06a8797d2 100644 --- a/src/uix/eidos/components/popover/README.md +++ b/src/uix/eidos/components/popover/README.md @@ -29,6 +29,18 @@ puede escribir, y el guard R-5.4 comprueba que ninguna miente. | `description` | `description-font-family` · `description-font-size` · `description-line-height` · `description-fg` | | `arrow` | `arrow-width` · `arrow-height` | +> **2026-08-25 — el sobre de trigger es un SUELO, no un techo** (firma, +> changelog §55). Las doce claves de `trigger` y `estado` de arriba se emiten +> ahora desde `:where([data-popover-trigger]:not([data-archetype='field-trigger']))` +> y su gemela de `:hover` — **especificidad (0,0,0)**. Siguen pintando entero al +> `Popover.Trigger` DESNUDO, que es para lo que se escribieron (medido: 69 de 76 +> instancias del catálogo no mueven un valor de receta), y dejan de pisar al +> huésped que declara lo suyo. `[data-state='open']` y `:focus-visible` +> conservan su especificidad — fuera de esta firma, registrado en +> `next-features.md` §13. El `:not()` va DENTRO del `:where()`: anula la +> contribución de especificidad, no el emparejamiento, así que los cinco +> triggers de picker siguen excluidos igual. + **Lo que el guard R-5.4 da por silencioso, con su razón medida** (`scripts/theming-sentinel-exceptions.ts`): diff --git a/src/uix/eidos/components/popover/popover.css b/src/uix/eidos/components/popover/popover.css index 032eab8e6..2b830bf93 100644 --- a/src/uix/eidos/components/popover/popover.css +++ b/src/uix/eidos/components/popover/popover.css @@ -11,8 +11,35 @@ * BOTH the inline field-icon triggers AND the standalone picker triggers (e.g. * the time-picker clock button) that lack that marker. This generic envelope must * NOT win over them (equal specificity → would otherwise depend on load order). + * + * ── THE ENVELOPE IS THE FLOOR, NOT THE CEILING (signed 2026-08-25, §55) ─── + * Third application of the law §12.9 signed for the depth plane and §53 (B′) + * for the palette ladder: a BASELINE is emitted at specificity ZERO, so the host + * recipe wins wherever it SPEAKS while the baseline keeps painting everything + * the host leaves UNSAID — which is what "baseline" means. `archetypes.css` + * (lines 9-21) already wrote this law for the transversal layer — "These are + * DEFAULTS — a component recipe must ALWAYS be able to override them" — and + * named its TWO deliberate exceptions; this envelope does a default's work and + * was in neither. At (0,2,0) it beat every host recipe at (0,1,0): the + * chat-message add-chip rendered a grey square beside its own pills, the + * calendar month/year selects TIED at (0,2,0) with their own rule and read + * 14px or 16px depending on which chunk Vite emitted last, and chronos' plain + * button needed a (0,3,0) pin to read plain. Measured cost of the demotion: + * of 76 trigger instances across 16 routes, 69 do not move a pixel — every + * bare trigger, which is 100 % of the reason this envelope exists. + * + * `:not(...)` stays INSIDE the `:where()`. `:where()` zeroes the specificity + * CONTRIBUTION, never the matching, so the five field-trigger pickers stay + * excluded exactly as before — now at (0,0,0). Hoisting it out would make the + * rule (0,1,0): a fresh tie with every host recipe, i.e. the coin flip §12.9 + * and §53 were signed to forbid. + * + * Signed scope is the REST envelope and its `:hover` — the latter was at + * (0,5,0) and killed the system's hover layer on every host that paints its + * own surface. `[data-state='open']` and `:focus-visible` keep their + * specificity: not part of this signature, registered in `next-features.md`. */ -[data-popover-trigger]:not([data-archetype='field-trigger']) { +:where([data-popover-trigger]:not([data-archetype='field-trigger'])) { display: inline-flex; align-items: center; justify-content: center; @@ -30,7 +57,11 @@ border-color var(--duration-fast) var(--ease-default); } -[data-popover-trigger]:not([data-archetype='field-trigger']):hover:not([disabled]):not([data-disabled]) { +:where( + [data-popover-trigger]:not([data-archetype='field-trigger']):hover:not([disabled]):not( + [data-disabled] + ) +) { background: var(--popover-hover-trigger-bg); border-color: var(--popover-hover-trigger-border); }