From de2e136a0fea1dfc46b350909f2d4381202c77ea Mon Sep 17 00:00:00 2001 From: dev Date: Wed, 26 Aug 2026 03:58:57 +0200 Subject: [PATCH] refactor(theming)!: el indicador de navigation-menu es CANAL - y el consumidor estiliza la PARTE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Firma "(a) completada" (2026-08-26), cerrada tras rechazar la especie `output` por abrir excepciones (clase barata - frontera de prosa - patron ya rechazado dos veces): las cuatro hermanas --navigation-menu-indicator- {x,y,w,h} pasan a --_ (la adjudicacion que tabs ya firmo citando a ESTE componente como precedente), indicator-w se RETIRA del contrato, y el README de soma deja de enseñar a saltarse la composicion. - Flip atomico escritor+lectores. Trampa del barrido, medida: el grep del nombre LITERAL es ciego a la forma con llaves {x,y,w,h} - escondia tres menciones. Barrido final: 0 consumos vivos de los nombres viejos. - indicator-w retirada con acta MEDIDA (y una auto-correccion: la primera acta decia "con la barra cerrada" y era falso - el indicador se monta condicional y hay 0 nodos; lo real, con transiciones CONGELADAS: solo una regla !important de autor le llegaba, y la unica ventana donde asomaba era el indicador montado sin rect - un subrayado de ancho cero). Contrato 44 -> 43, generated -1 linea, ningun ledger rancio. - README de soma: las medidas siguen tabuladas como canales privados "not consumer API"; la seccion nueva enseña la PARTE ([data-navigation-menu- indicator] + data-state + los tres pomos thickness/bg/radius) con ejemplos VERIFICADOS POR MEDICION (el ejemplo 1 es literalmente el control positivo: block-size 2 -> 9px). Hueco FLAGGED sin acuñar: el ejemplo viejo de pildora no tiene traduccion - falta un eje de forma en la parte; en SS13. - Registro PENDING_PRIVATE_RENAME -> UNA entrada (motion). SS14 NUEVO en next-features: unificar tabs (--indicator-* SIN prefijo, MeasuredIndicator :80-83, 2 claves global en el ledger) - con el hallazgo de que ningun guard los caza HOY porque no llevan prefijo: el espacio cerrado premio al que nombraba mejor. - Numeros: public 3073->3072, channel 37->38, reach 73% y atHundred 65 intactos (ningun redondeo cayo), --names 4566 con 0 desviadas, centinela nav-menu 42/43 con la misma unica adjudicacion, --debt identico. - Dinamica verbatim: el indicador DESLIZA (left 0 -> 111px, ancho 107 -> 108.56 = rect del trigger, canal w/h == triggerRect), los nombres viejos computan vacio en el elemento (control negativo 777/333: nada se mueve). Guards: 78 tests verdes + soma 9/9 + mutacion por las dos caras + audit 162 PASS + docs:check 0/0. BREAKING: los cuatro nombres publicos del indicador ya no existen y --navigation-menu-indicator-w sale del contrato; el indicador se estiliza por su parte y sus pomos. Co-Authored-By: Claude Fable 5 --- docs/canon/recipe-contract.md | 21 +++ docs/next-features.md | 123 +++++++++++++++--- docs/process/CONTINUE-theming.md | 87 +++++++++++-- .../components/navigation-menu/README.md | 25 +++- .../navigation-menu/navigation-menu.css | 10 +- .../navigation-menu/navigation-menu.svelte | 2 +- src/uix/eidos/generated/base.css | 1 - src/uix/eidos/lib/recipes/base.ts | 33 +++-- src/uix/eidos/recipe-css-contract.test.ts | 39 +++--- .../soma/components/navigation-menu/README.md | 58 +++++---- .../navigation-menu-provider.svelte.ts | 11 +- .../components/navigation-menu/+page.svelte | 4 +- 12 files changed, 317 insertions(+), 97 deletions(-) diff --git a/docs/canon/recipe-contract.md b/docs/canon/recipe-contract.md index 6b2a0de3b..8ecc4a1a2 100644 --- a/docs/canon/recipe-contract.md +++ b/docs/canon/recipe-contract.md @@ -296,6 +296,27 @@ one of four splits a documented public API and leaves a declared sibling contradicting it. Whether the family goes private, goes public, or stays split is a SIGNATURE, not a codemod. +> **Annotation 2026-08-26 (later the same day) — the signature landed, and the +> registry is down to ONE.** The paragraph above records where the codemod stood +> that morning; it stays because its measurement was right for its day. What was +> signed is option **(a)**: the FOUR siblings are private +> (`--_navigation-menu-indicator-{x,y,w,h}`, writer and every reader flipped +> atomically), `indicator-w` leaves the contract (`navigation-menu` 44 → 43 +> rows), and the soma README's `## CSS Variables` section stops teaching the +> measurements as API — it teaches the PART and its three shape knobs. Option +> (b), declaring `-x/-y/-h` to match the README, was REJECTED: it would bless +> four per-instance measurements as theming surface, the exact thing the +> `channel` class exists to deny. A new species `output` — a third class between +> public and private — was proposed and rejected too, for opening exceptions on +> a cheap class with a prose border. `-w` was a token that LIES, measured: soma +> writes it inline on the element, so with the underline drawn neither a write on +> the component root (what the sentinel does) nor one on `:root` reaches it; it +> scored `reach` only in the window where the indicator is mounted with no rect +> yet and the underline is 0px wide and invisible. Measured: `public` +> 3073 → 3072, `channel` 37 → 38, reach **73 %** and `atHundred` **65** +> untouched, ledger **1154 · 0 new · 0 stale**; the `navigation-menu` sentinel +> 43/44 → **42/43**, same single adjudication. **ONE** entry survives, motion's. + Mutation-proved three ways the day it landed: an invented `var(--mockup-inventado, red)` — own prefix, WITH a fallback — turns the guard red naming the key; the same injection with a FOREIGN borrowed name and a diff --git a/docs/next-features.md b/docs/next-features.md index 5ed88ba0e..cdd6316e4 100644 --- a/docs/next-features.md +++ b/docs/next-features.md @@ -552,6 +552,23 @@ 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. +- **HUECO DE POMO — `navigation-menu`: el indicador no sabe hacer de píldora** + (2026-08-26, al reescribir la sección `## CSS Variables` del README de soma). + Los dos ejemplos viejos enseñaban a leer las medidas privadas; el segundo era + un *full-trigger highlight* (píldora / fondo detrás del trigger). Reescritos + al camino de la casa —estilar la PARTE y sus pomos—, el primero se traduce sin + pérdida (la receta YA pinta ese subrayado; el consumidor sólo mueve + `indicator-{thickness,bg,radius}`), **pero el segundo NO tiene traducción**: + la receta clava `block-size: var(--navigation-menu-indicator-thickness)` y + desplaza el subrayado por debajo del trigger con `translateY`, así que ningún + pomo del contrato hace que la decoración RELLENE la caja del trigger. Haría + falta un eje de forma/variante en la parte. **NO se acuñó** —«compose + existing; flag gaps»—: el README lo dice y para. Decisión pendiente: o el + indicador gana ese eje (y entonces es diseño de componente, no de theming), o + se documenta que el `navigation-menu` subraya y quien quiera píldora usa + `tabs`, que es la parte que ya hace exactamente eso con + `lib/sliding-indicator.css`. + - **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 @@ -1086,23 +1103,45 @@ Lo que la cola pequeña añadió (2026-08-22): `--menubar-panel-min-width` como fallback), así que el censo no las contaba como knobs de apariencia públicos. **El suelo NO se toca.** - **⚠ PARADO — `--navigation-menu-indicator-h`, y necesita TU FIRMA.** No es - un canal suelto: el provider escribe **CUATRO hermanas** en un mismo - literal (`--navigation-menu-indicator-{x,y,w,h}`, - `navigation-menu-provider.svelte.ts:832-835`), el README de soma las - enseña **las cuatro** bajo un epígrafe `## CSS Variables` como forma - **controlada por el consumidor** («here are the trigger's measurements — - style the decoration however you want») con **dos ejemplos de CSS de app - que leen `-h` directamente**, y **`-w` es clave DECLARADA del contrato** - (`base.ts`, `indicator-w: 0px`) — que es la única razón por la que `-h` es - la única miembro de la familia que el guard ve (a `-x`/`-y` no las lee - ninguna receta). Privatizar una de cuatro parte una API pública - documentada y deja una hermana declarada contradiciéndola. Las opciones, - sin elegir: (a) las cuatro privadas + retirar `indicator-w` del contrato y - reescribir la sección del README como interna; (b) las cuatro públicas + - declarar `-x`/`-y`/`-h` en `base.ts` (reconoce la API que el README ya - promete); (c) queda partido y se DOCUMENTA por qué. Su entrada del - registro se queda, con la razón escrita. + **✅ RESUELTO 2026-08-26 (tarde) — `--navigation-menu-indicator-h`, con la + opción (a) FIRMADA y EJECUTADA. El registro queda en UNA entrada.** + Lo que estuvo PARADO: el provider escribe **CUATRO hermanas** en un mismo + literal (`navigation-menu-provider.svelte.ts:832-835`), el README de soma + las enseñaba **las cuatro** bajo `## CSS Variables` como forma controlada + por el consumidor con dos ejemplos de CSS de app leyendo `-h`, y `-w` era + clave DECLARADA del contrato — la única razón por la que el guard veía a + `-h` sola. **Lo firmado, (a)**: las cuatro pasan a + `--_navigation-menu-indicator-{x,y,w,h}` (flip atómico escritor+lectores, + barrido `git ls-files` por nombre), `indicator-w` se RETIRA del contrato + (44 → 43 filas) y la sección del README se reescribe al camino de la casa + — el consumidor estiliza **la PARTE** (`[data-navigation-menu-indicator]`, + con su `data-state`) y sus tres pomos de forma + (`indicator-{thickness,bg,radius}`), no las medidas. + **(b) —las cuatro públicas, declarando `-x`/`-y`/`-h`— se RECHAZÓ**: + bendecir como superficie de tema cuatro medidas por instancia es + exactamente lo que la clase `channel` de la firma de los 318 existe para + negar. **Y se rechazó una especie nueva, `output`** (una tercera clase + entre público y privado): abre excepciones sobre una clase barata con + frontera de PROSA — el patrón que ya se había rechazado dos veces. + **`-w` era un token-que-miente, y está MEDIDO** (CDP, transiciones + congeladas porque `inline-size` se transiciona en esta parte): con el + subrayado pintado, escribirlo en la raíz del componente —lo que hace el + centinela— deja `inline-size` en 107px, y en `:root` igual; sólo una regla + `!important` de autor llega. Puntuaba `reach` en la única ventana donde la + declaración asoma: el indicador montado SIN rect todavía, donde el + subrayado mide 0px y no se ve. + **Números**: `public` 3073 → **3072**, `channel` 37 → **38**, denominador + 4227 → 4226, reach **73 %** y `atHundred` **65** intactos, `--names` + 4567 → **4566**, ledger **1154 · 0 new · 0 stale**. Centinela de + `navigation-menu` **43/44 → 42/43**, misma única adjudicación + (`disabled-trigger-fg`): el total baja porque la clave sale del contrato, + no porque nada deje de llegar. **Dinámica** (la sonda estándar es ciega a + estos canales): las privadas llevan la geometría del trigger activo + (Products w=107px h=36px · Solutions w=108.56px h=36px), `inline-size` y + `left` las siguen (0px → 111px al deslizar) y el `translateY(36px)` se + mantiene; **los nombres viejos computan VACÍO** y fijarlos a 777px no mueve + nada. Los tres pomos del contrato siguen llegando (`block-size` 2 → 9px, + `background` → `rgb(1,2,3)`). **Docs: se ANOTARON, no se reescribieron.** Las fichas medidas (`audit/theming/{knob,color-swatch,text-gradient}.md`), el README de @@ -2517,3 +2556,53 @@ tiene que llevar el atributo — el contexto sólo le deja heredar el valor. **Deps**: ninguna. Son mejoras del instrumental del eje, ejecutables cuando estorben. + +## 14. Unificar `tabs` bajo la adjudicación del canal de valor — 2026-08-26 + +**PENDIENTE — redactado, NO ejecutado.** Lo abre la firma «(a) completada» de +`navigation-menu` (§13 arriba y el punto 1 del bloque del registro): un +indicador de `tablist` es **canal de valor, no superficie de tema**, y `tabs` +citó a `navigation-menu` como precedente cuando firmó eso por escrito. Ahora el +precedente está ejecutado y `tabs` es el que queda fuera de su propia doctrina. + +**El hecho.** La capa COMPARTIDA `MeasuredIndicator` +(`src/uix/soma/layers/measured-indicator.svelte.ts:80-83`) escribe cuatro +medidas por instancia con nombres **SIN prefijo de componente**: +`--indicator-{x,y,w,h}`. Sus consumidores son `tabs-provider.svelte.ts:454` y +`radio-group-provider.svelte.ts:337`. Los lectores son +`src/uix/eidos/components/tabs/tabs.css:312-314` y la capa +`src/uix/eidos/lib/sliding-indicator.css:32-34`. + +**Por qué NO lo caza ningún guard hoy, y es lo interesante.** La ley del espacio +cerrado (`recipe-css-contract.test.ts`) sólo mira el prefijo PROPIO del +componente (`--{c}-*`); un nombre sin prefijo es vocabulario ajeno y se le +escapa. El censo, por la misma razón, los puntúa **`global`**: hoy son 2 +entradas del ledger de deuda (`global · tabs.css · [data-tabs-indicator] · +block-size` y `· inline-size`, `scripts/theming-census-debt.ts:1242-1243`). O +sea: `navigation-menu` fue visible porque llevaba prefijo, y `tabs` es invisible +porque NO lo lleva — el guard premió al que nombraba mejor. + +**Lo que habría que decidir** (la firma, no el codemod): + +1. ¿A qué nombre van? Un canal de una capa compartida no puede ser + `--_{c}-*` sin más: lo escribe la capa, y lo leen DOS componentes más la + propia capa. Candidatos: `--_indicator-*` (privado de capa, un solo nombre + para los tres) o `--_{c}-indicator-*` por componente, que obligaría a la capa + a saber su anfitrión. +2. Qué pasa con `--indicator-duration` / `--indicator-ease` + (`sliding-indicator.css:36-39`): **NO son canal** — nadie los escribe en + runtime, son pomos de forma sin declarar. Van por la puerta del contrato, no + por ésta. +3. Las 2 entradas `global` del ledger salen o cambian de clase con el + renombrado; hay que medir si el suelo se mueve (en `navigation-menu` no se + movió: 1 knob de `public` a `channel`, reach y `atHundred` intactos). + +**Dependencias / trampas ya medidas.** El barrido `git ls-files` por nombre es +obligatorio y **no basta con el literal**: en `navigation-menu` la forma +`--…-indicator-{x,y,w,h}` (llaves) escondía tres menciones más que el grep del +nombre suelto no veía. `rtl-lint.test.ts:81-92` lleva estos nombres en un +fixture. Y la verificación que vale es DINÁMICA: `inline-size` va transicionada +en estas partes, así que una lectura inmediata devuelve el valor VIEJO — hay que +congelar transiciones o el probe informa «no se movió» de todo. + +**Deps**: ninguna. Ejecutable cuando se quiera cerrar la clase. diff --git a/docs/process/CONTINUE-theming.md b/docs/process/CONTINUE-theming.md index 8d8d74b8a..9c47fa846 100644 --- a/docs/process/CONTINUE-theming.md +++ b/docs/process/CONTINUE-theming.md @@ -16,8 +16,9 @@ EJECUTADA — la ley del espacio cerrado»). Heredada de la segunda queda la re-clasificación de los 117 nombres `--palabras-*` que el censo puntúa `public` siendo canal de VALOR, más DOS deudas nuevas de eje propio que ella misma nombró: el codemod de los 14 canales públicos a `--_{c}-*` — -**EJECUTADO 2026-08-26 (13 de 14; el 14.º PARADO, ver §«FIRMA EJECUTADA — el -codemod de los 14 canales»)** — y el cableado `EidosConfig.motion` → CSS +**EJECUTADO 2026-08-26 (14 de 14: 13 en el codemod + el 14.º por firma propia, +ver §«FIRMA EJECUTADA — el indicador de navigation-menu»)** — y el cableado +`EidosConfig.motion` → CSS (las dos, con su lista, en `next-features.md`). **Lo que sigue pendiente en este carril: el cableado de motion · la re-clasificación de `palabras` · la `timeline` §5 pre-B′.** @@ -65,15 +66,13 @@ Nada de grepear los propios patrones: el barrido destapó lectores que ninguna lista traía (el comentario del morfo de `knob`, las dos páginas de demo, nueve menciones en `PLAN-background.md`). -**⚠ PARADO — `--navigation-menu-indicator-h`.** No es un canal suelto: el -provider escribe **CUATRO hermanas** en un mismo literal -(`--navigation-menu-indicator-{x,y,w,h}`), el README de soma las enseña **las -cuatro** bajo `## CSS Variables` como forma **controlada por el consumidor**, -con dos ejemplos de CSS de app que leen `-h`, y **`-w` es clave DECLARADA del -contrato** — que es la única razón por la que `-h` es la única de la familia que -el guard ve. Privatizar una de cuatro parte una API documentada y deja una -hermana declarada contradiciéndola: **es firma, no codemod**. Opciones -redactadas sin elegir en `next-features.md`. +**✅ RESUELTO — `--navigation-menu-indicator-h`, con (a) FIRMADA Y EJECUTADA +(2026-08-26, tarde).** Estuvo PARADO porque no era un canal suelto: el provider +escribe **CUATRO hermanas** en un mismo literal, el README de soma las enseñaba +las cuatro bajo `## CSS Variables` como forma controlada por el consumidor (con +dos ejemplos leyendo `-h`), y **`-w` era clave DECLARADA del contrato** — la +única razón por la que el guard veía a `-h` sola. Ver §«FIRMA EJECUTADA — el +indicador de navigation-menu» abajo. **El registro queda en UNA** (motion). **⚠ LA TRAMPA DEL CENSO NO MORDIÓ, y la predicción estaba MAL por un orden de magnitud.** Se esperaba que ~15 knobs pasaran de `public` a `channel` y que el @@ -127,6 +126,72 @@ VIVO (READMEs de componente y las dos demos). Queda anotada la fila GENERADA es un `` DESNUDO y `slider.svelte` renderiza sólo su provider — un `div[data-slider]` **vacío**, sin raíl ni pulgar. No se puede accionar. +### FIRMA EJECUTADA (2026-08-26) — el indicador de navigation-menu + +**Qué se firmó**: la opción **(a)** del PARADO. Las CUATRO medidas del trigger +activo pasan a privadas — `--_navigation-menu-indicator-{x,y,w,h}`, flip +ATÓMICO escritor + todos los lectores, con barrido `git ls-files` por nombre —, +**`indicator-w` se RETIRA del contrato** (44 → 43 filas) y la sección +`## CSS Variables` del README de soma se reescribe al camino de la casa: el +consumidor estiliza **la PARTE** (`[data-navigation-menu-indicator]` y su +`data-state`) con los tres pomos de forma, no las medidas. + +**Lo que se RECHAZÓ, y por qué importa**: (b) declarar `-x/-y/-h` para casar con +el README bendeciría como superficie de tema cuatro medidas por instancia — +justo lo que la clase `channel` de la firma de los 318 existe para negar. Y se +rechazó una especie nueva, **`output`** (tercera clase entre público y privado): +abre excepciones sobre una clase barata con frontera de PROSA, patrón ya +rechazado dos veces. + +**`-w` era un token-que-miente, MEDIDO** (CDP, transiciones CONGELADAS — en esta +parte `inline-size` va transicionada y una lectura inmediata devuelve el valor +VIEJO; el probe informaba «no se movió» de TODO hasta congelarlas): con el +subrayado pintado, escribirlo en la raíz del componente —lo que hace el +centinela— deja `inline-size` en **107px**, y en `:root` igual; **sólo una regla +`!important` de autor llega** (1234px), porque soma escribe INLINE en el +elemento. La única ventana donde la declaración asomaba es el indicador montado +**SIN rect todavía** (`isOpen` true, `triggerRect` aún null; o `forceMount`): +ahí `inline-size` lee el `0px` que daba esta clave y un write en el host lo +mueve a 1234px. Es decir, llegaba **sólo donde no pintaba nada**. ⚠ El +indicador está montado CONDICIONALMENTE (`{#if state.isOpen || forceMount}`): +con la barra cerrada hay **0 nodos** en la página. + +**Números**: `public` 3073 → **3072**, `channel` 37 → **38**, denominador +4227 → 4226, reach **73 %** y `atHundred` **65** intactos, `--names` +4567 → **4566** (0 desviadas), ledger **1154 · 0 new · 0 stale**. Centinela +`navigation-menu` **43/44 → 42/43**, misma única adjudicación +(`disabled-trigger-fg`): el total baja porque la clave sale del contrato, no +porque nada deje de llegar — **el fichero de excepciones no se toca**. +`component:audit` **162 PASS / 4 NEEDS-WORK** idéntico. + +**Dinámica, que es la que vale aquí** (la sonda estándar es ciega a estos +canales): Products `w=107px h=36px` = rect del trigger, Solutions +`w=108.5625px h=36px`, `inline-size` las sigue (107 → 108.562) y `left` +**0px → 111px** — el subrayado DESLIZA — con `translateY(36px)` estable. +**Control negativo**: los nombres viejos computan **VACÍO** en el indicador, y +fijarlos en la raíz a 777px/333px no mueve nada. **Control positivo**: los tres +pomos del contrato siguen llegando (`block-size` 2 → 9px, `background` → +`rgb(1,2,3)`). + +**Guard**: `PENDING_PRIVATE_RENAME` queda en **UNA** entrada (motion), con acta +en el propio registro. Mutado por sus dos caras: reintroducir la entrada +retirada → **STALE nombrándola**; devolver `--navigation-menu-indicator-w` a la +receta → **`missing` nombrándolo**. + +**⚠ Trampa del barrido**: el grep del nombre LITERAL es CIEGO a la forma con +llaves `--…-indicator-{x,y,w,h}`, que escondía tres menciones más (el comentario +de `navigation-menu.svelte` de eidos y dos filas de la demo). Hay que barrer +también la familia. + +**Hueco FLAGGED, no acuñado**: el ejemplo viejo de *píldora / fondo de trigger* +no tiene traducción a pomos — la receta clava el `block-size` al grosor. Queda +en §13 de `next-features.md`, sin inventar un eje de forma. + +**Abre expediente**: §14 de `next-features.md` — unificar `tabs` bajo la misma +adjudicación (`--indicator-{x,y,w,h}` SIN prefijo, escritos por la capa +compartida `MeasuredIndicator`, 2 entradas `global` en el ledger). Redactado, +**NO ejecutado**. + ### FIRMA EJECUTADA (2026-08-26) — la ley del espacio cerrado **Qué se firmó**: el espacio de nombres público es **CERRADO sobre el diff --git a/src/uix/eidos/components/navigation-menu/README.md b/src/uix/eidos/components/navigation-menu/README.md index ece53737b..62bae421b 100644 --- a/src/uix/eidos/components/navigation-menu/README.md +++ b/src/uix/eidos/components/navigation-menu/README.md @@ -98,10 +98,22 @@ pasan a la escala de control. Medido 2026-08-19, antes → después: altura barra y el `Link` de barra viste el cromo del trigger; las filas son los `` del consumidor DENTRO del panel, así que su prefijo los nombra por donde viven. Su tinta sigue leyendo `trigger-fg`, como venía. -- **`--navigation-menu-indicator-w` es canal de valor, no superficie de tema**: - soma le escribe el rect del trigger activo en cada movimiento. Su gemelo `-x` - estuvo declarado sin que NADIE lo leyera (el seguimiento horizontal es el - `left` inline de soma) y se retiró — 0 diffs, que es la prueba. +- **Las medidas del trigger activo son canal de valor, no superficie de tema**: + soma escribe las cuatro (`--_navigation-menu-indicator-{x,y,w,h}`) en el + `style` inline del indicador en cada movimiento, así que ninguna hoja de tema + puede ganarles. Por eso son privadas (firma 2026-08-26) y ninguna se declara. + `-x` estuvo declarada sin que NADIE la leyera (el seguimiento horizontal es el + `left` inline de soma) y se retiró el 23-08 — 0 diffs, que es la prueba; `-w` + siguió el mismo camino el 26-08 (44 → 43 filas de contrato). Medido por CDP + con las transiciones CONGELADAS (`inline-size` se transiciona en esta parte y + una lectura inmediata devuelve el valor VIEJO): con el subrayado pintado, + escribir `--navigation-menu-indicator-w` en la raíz del componente —que es lo + que hace el centinela— deja `inline-size` en 107px, y en `:root` igual; sólo + una regla `!important` de autor llega. La única ventana en la que la + declaración asomaba es el indicador MONTADO SIN rect todavía (`forceMount`, o + el hueco entre montar y medir): ahí `inline-size` lee el `0px` que esta clave + daba. Un subrayado de ancho cero e invisible — llegaba sólo donde no pintaba + NADA. Un tema decide la forma en una línea —`--navigation-menu-trigger-radius: var(--radius-full)` da píldoras— sin mover el radio del sistema. Hasta @@ -158,8 +170,9 @@ Referencias: [Radix Navigation Menu](https://www.radix-ui.com/primitives/docs/co aquí desde las notas de sesión; el porqué vive también como comentario en `navigation-menu.css`): - Soma inline-estila `position:absolute` + `left`/`top` al corner TOP-LEFT - del trigger activo y publica el rect en las vars - `--navigation-menu-indicator-{x,y,w,h}`. + del trigger activo y escribe el rect en los canales de valor PRIVADOS + `--_navigation-menu-indicator-{x,y,w,h}` (firma 2026-08-26; ninguno se + declara — los escribe la instancia y un tema no debe alcanzarlos). - La receta SOLO da forma: `inline-size: var(--…-w)`, subrayado de 2px, `transform: translateY(var(--…-h))` para sentarse BAJO el trigger. **NUNCA re-posiciona** (añadir `translateX(var(--…-x))` DUPLICA el diff --git a/src/uix/eidos/components/navigation-menu/navigation-menu.css b/src/uix/eidos/components/navigation-menu/navigation-menu.css index 2b1d95c73..c3b9f189e 100644 --- a/src/uix/eidos/components/navigation-menu/navigation-menu.css +++ b/src/uix/eidos/components/navigation-menu/navigation-menu.css @@ -317,8 +317,10 @@ /* ── Indicator (underline under active Trigger) ─ * - * Soma writes the active-trigger rect into CSS vars on the indicator: - * --navigation-menu-indicator-x / -y / -w / -h + * Soma writes the active-trigger rect into PRIVATE value channels on the + * indicator (`--_{c}-*`, firma 2026-08-26 — a theme must not reach a + * per-instance measurement): + * --_navigation-menu-indicator-x / -y / -w / -h * The recipe consumes them to position a 2px underline that slides * between triggers. The indicator itself stays inside the navigation * bar; it's NOT in the floating layer. @@ -340,9 +342,9 @@ position: absolute; z-index: 1; pointer-events: none; - inline-size: var(--navigation-menu-indicator-w, 0px); + inline-size: var(--_navigation-menu-indicator-w, 0px); block-size: var(--navigation-menu-indicator-thickness); - transform: translateY(var(--navigation-menu-indicator-h, 0px)); + transform: translateY(var(--_navigation-menu-indicator-h, 0px)); background: var(--navigation-menu-indicator-bg); border-radius: var(--navigation-menu-indicator-radius); opacity: 0; diff --git a/src/uix/eidos/components/navigation-menu/navigation-menu.svelte b/src/uix/eidos/components/navigation-menu/navigation-menu.svelte index 4761c86e3..df6d19719 100644 --- a/src/uix/eidos/components/navigation-menu/navigation-menu.svelte +++ b/src/uix/eidos/components/navigation-menu/navigation-menu.svelte @@ -21,7 +21,7 @@ * Soma owns active-item state, hover open/close timing * (`openDelay` / `skipDelayDuration` / `closeDelay`), `hoverEnabled` * gate, loop arrow nav, `data-motion` direction marker, the Indicator - * rect CSS vars (`--navigation-menu-indicator-{x,y,w,h}`), `forceMount` + * rect value channels (`--_navigation-menu-indicator-{x,y,w,h}`), `forceMount` * + dismissal. Eidos paints the bar + trigger states + Content panel * chrome — fresh recipe (does not reuse dropdown-menu or menubar * since NavigationMenu lives inline in the page, not in a floating diff --git a/src/uix/eidos/generated/base.css b/src/uix/eidos/generated/base.css index afe42a9f5..23e91d130 100644 --- a/src/uix/eidos/generated/base.css +++ b/src/uix/eidos/generated/base.css @@ -4753,7 +4753,6 @@ --drag-drop-preview-font-size: var(--size-sm-font-size); --drag-drop-preview-shadow: var(--shadow-raised); --navigation-menu-content-z: var(--z-index-overlay-inline); - --navigation-menu-indicator-w: 0px; --navigation-menu-indicator-thickness: var(--border-width-medium); --navigation-menu-indicator-bg: var(--color-primary-solid); --navigation-menu-indicator-radius: var(--radius-full); diff --git a/src/uix/eidos/lib/recipes/base.ts b/src/uix/eidos/lib/recipes/base.ts index 1410e0572..a49095348 100644 --- a/src/uix/eidos/lib/recipes/base.ts +++ b/src/uix/eidos/lib/recipes/base.ts @@ -8168,17 +8168,32 @@ export const THEME_BASE_RECIPE_TOKENS = defineRecipes({ 'navigation-menu': { // Inline (non-portaled) dropdown panel: stacks above normal page flow. 'content-z': 'var(--z-index-overlay-inline)', - // `-w` is soma's VALUE CHANNEL, not theming surface: the provider writes - // the active trigger's rect into it on every move and the recipe reads it - // with a `0px` fallback. `-x` sat here too and NOBODY read it — soma - // stamps the horizontal tracking as an inline `left` (its own README says - // so, and the recipe's comment records that adding `translateX(var(--…-x))` - // DOUBLED the offset), so the declaration moved nothing anywhere: - // retired 2026-08-23, 0 diffs, which is the proof it was dead. `-y` / `-h` - // were never declared, which is the same reading from the other side. + // ACTA 2026-08-26 — `indicator-w` RETIRED (44 → 43 contract rows). The + // note below already said it out loud: `-w` is soma's VALUE CHANNEL, the + // provider writes the active trigger's rect into it on every move. A + // declared key it can only LIE about, and the lie is MEASURED (CDP, demo + // at :5180, transitions frozen — `inline-size` is transitioned here and an + // immediate read returns the old value): with the underline drawn, writing + // `--navigation-menu-indicator-w` on the component root — exactly what the + // sentinel does — leaves `inline-size` at 107px, and so does writing it on + // `:root`. Only an author `!important` rule reaches (1234px), because + // soma's write is INLINE on the element and nothing a theme does outranks + // that. The one window where the declaration DID show through is the + // indicator mounted with no rect yet (`isOpen` true, `triggerRect` still + // null; `forceMount` before the effect measures): there `inline-size` + // reads the `0px` this key supplied, and a host write moves it to 1234px. + // That is a zero-width invisible underline — the key reached only where it + // painted NOTHING, which is what a token-that-lies always scores on. + // The four siblings now spell what they are — + // `--_navigation-menu-indicator-{x,y,w,h}`. + // `-x` sat here too and NOBODY read it — soma stamps the horizontal + // tracking as an inline `left` (its own README says so, and the recipe's + // comment records that adding `translateX(var(--…-x))` DOUBLED the offset), + // so the declaration moved nothing anywhere: retired 2026-08-23, 0 diffs, + // which is the proof it was dead. `-y` / `-h` were never declared, which is + // the same reading from the other side. // The underline's own FORM — thickness, fill, radius — is what belongs to // a theme, and those are the three below. - 'indicator-w': '0px', 'indicator-thickness': 'var(--border-width-medium)', 'indicator-bg': 'var(--color-primary-solid)', 'indicator-radius': 'var(--radius-full)', diff --git a/src/uix/eidos/recipe-css-contract.test.ts b/src/uix/eidos/recipe-css-contract.test.ts index e51e91156..bf30b11fb 100644 --- a/src/uix/eidos/recipe-css-contract.test.ts +++ b/src/uix/eidos/recipe-css-contract.test.ts @@ -193,25 +193,28 @@ const PENDING_PRIVATE_RENAME: Readonly> = { // CONTRACT on 2026-08-23 is untouched: it is still a value channel and it // still does not come back: only the spelling changed. // - // TWO entries survive, each for its own reason, and both are the registry - // working as designed — it counts, it does not bless. + // ── ACTA 2026-08-26 (later the same day) — the fourteenth entry RETIRED ── + // `--navigation-menu-indicator-h` was the one the codemod STOPPED on, and it + // went out by SIGNATURE, option (a): the FOUR siblings are private + // (`--_navigation-menu-indicator-{x,y,w,h}`, writer + every reader flipped + // atomically), `indicator-w` is RETIRED from the contract (44 → 43 rows) and + // the soma README's `## CSS Variables` section no longer teaches the + // measurements as consumer API — it teaches the PART and its three shape + // knobs. `-w` was a token-that-lies of the manual, MEASURED: the provider + // writes it INLINE on the element, so with the underline drawn a write on the + // component root (what the sentinel does) or on `:root` moves nothing — only + // an author `!important` rule reaches. It scored `reach` on the one window + // where the declaration shows through, the indicator mounted with no rect + // yet, where the underline is 0px wide and invisible. + // Option (b) — declaring `-x/-y/-h` to match the README — was REJECTED: it + // would bless four per-instance measurements as theming surface, the exact + // thing the 318's `channel` class exists to deny. The species `output` + // (a third class between public and private) was proposed and rejected too, + // for opening exceptions on a cheap class with a prose border. + // + // ONE entry survives, and it is the registry working as designed — it counts, + // it does not bless. // - // `--navigation-menu-indicator-h` — the codemod STOPPED here and reported - // it rather than improvising. It is not a lone channel: the provider writes - // FOUR siblings in one object literal - // (`--navigation-menu-indicator-{x,y,w,h}`), the soma README documents all - // four under a `## CSS Variables` heading as CONSUMER-controlled shape - // («here are the trigger's measurements — style the decoration however you - // want») with two worked CSS examples that read `-h` directly, and `-w` is - // a DECLARED contract key (`base.ts`, `indicator-w: 0px`) — which is the - // only reason `-h` is the single member of the family the guard can see. - // Privatising one of four splits a documented public API and leaves a - // declared sibling contradicting it; deciding whether the family goes - // private, goes public, or stays split is a signature, not a codemod. - '--navigation-menu-indicator-h': - 'STOPPED 2026-08-26 — one of four documented sibling measurements (`-x/-y/-w/-h`), ' + - '`-w` is a declared contract key and the soma README teaches all four as consumer API; ' + - 'the family needs a signature, not a rename', // NOT a value channel: nobody writes it at runtime. Its home is the motion // channel — the default belongs to `EidosConfig.motion`, not to a recipe // contract, and declaring it in base.ts would contradict `motion`'s signed diff --git a/src/uix/soma/components/navigation-menu/README.md b/src/uix/soma/components/navigation-menu/README.md index bbff41668..82d16aef9 100644 --- a/src/uix/soma/components/navigation-menu/README.md +++ b/src/uix/soma/components/navigation-menu/README.md @@ -170,43 +170,53 @@ In the anatomy file (`anima/components/navigation-menu.anatomy.ts`) the `content ## CSS Variables -The `Indicator` is inline-styled with `position: absolute` + `left` / `top` anchored to the active trigger's top-left corner (relative to the Provider root). Its **shape** — width, height, offset — is consumer-controlled via CSS using these variables: +The `Indicator` is inline-styled with `position: absolute` + `left` / `top`, anchored to the active trigger's top-left corner (relative to the Provider root), and soma writes the trigger's four measurements into **private value channels** on that same inline `style`: -| Variable | Meaning (px, unit-less number) | -| ------------------------------------------- | ---------------------------------------------- | -| `--navigation-menu-indicator-x` | Left offset of the active trigger | -| `--navigation-menu-indicator-y` | Top offset | -| `--navigation-menu-indicator-w` | Width of the active trigger | -| `--navigation-menu-indicator-h` | Height of the active trigger | +| Channel | Meaning | +| ------------------------------------ | ------------------------------ | +| `--_navigation-menu-indicator-x` | Left offset of the active trigger | +| `--_navigation-menu-indicator-y` | Top offset | +| `--_navigation-menu-indicator-w` | Width of the active trigger | +| `--_navigation-menu-indicator-h` | Height of the active trigger | -This split matters: if the provider forced `width`/`height` on the indicator element, a 2px underline would be stretched to cover the full trigger and hide its text. Instead, the component tells you "here are the trigger's measurements — style the decoration however you want". +**These are not consumer API.** The leading `_` is the house mark of a value channel: a per-instance measurement soma writes and the eidos recipe reads. They are written INLINE on the element, so a stylesheet — yours or a theme's — can never win against them; a rule that tried would be a rule that lies. They are listed here to describe the mechanism, not to invite a `var()`. -### Example — underline at the bottom of the trigger +The reason the provider does not simply force `width` / `height` on the element still holds: a 2px underline stretched to cover the full trigger would hide its text. Soma measures; the recipe shapes. + +### What you style: the part + +You style the `Indicator` **part** — `[data-navigation-menu-indicator]`, which carries `data-state="open" | "closed"` — and you re-shape it through the recipe's contract knobs, not through the measurements: + +| Token | Default | Shapes | +| ---------------------------------------- | --------------------------- | ------------------ | +| `--navigation-menu-indicator-thickness` | `var(--border-width-medium)` | The underline's thickness | +| `--navigation-menu-indicator-bg` | `var(--color-primary-solid)` | Its fill | +| `--navigation-menu-indicator-radius` | `var(--radius-full)` | Its corners | + +The recipe already tracks the active trigger for you: it takes the trigger's width, sits under it, slides between triggers and fades in on `data-state="open"`. + +### Example — a thicker underline in another tone ```css -[data-navigation-menu-indicator] { - width: var(--navigation-menu-indicator-w); - height: 2px; - background: currentColor; - /* `top` is the trigger's top — slide down by its height to sit underneath */ - transform: translateY(var(--navigation-menu-indicator-h)); - transition: left 200ms ease, width 200ms ease, transform 200ms ease; +[data-navigation-menu] { + --navigation-menu-indicator-thickness: var(--border-width-thick); + --navigation-menu-indicator-bg: var(--color-affirm-solid); + --navigation-menu-indicator-radius: var(--radius-sm); } ``` -### Example — full-trigger highlight (pill/backdrop) +### Example — reacting to the open state + +`data-state` is on the part, so any decoration of your own hangs off it: ```css -[data-navigation-menu-indicator] { - width: var(--navigation-menu-indicator-w); - height: var(--navigation-menu-indicator-h); - background: rgba(29, 78, 216, 0.12); - border-radius: 6px; - transition: left 200ms ease, top 200ms ease, width 200ms ease, height 200ms ease; - z-index: -1; +[data-navigation-menu-indicator][data-state='open'] { + box-shadow: 0 0 var(--space-2) var(--navigation-menu-indicator-bg); } ``` +> **Not reachable today: the full-trigger highlight (pill / backdrop).** The recipe pins the indicator's `block-size` to `--navigation-menu-indicator-thickness` and offsets it below the trigger, so there is no contract knob that makes the decoration fill the trigger's box instead of underlining it. That would need a knob the part does not have (a shape/variant axis), and inventing one here — or reading the private measurements — is the thing this section stopped teaching. Flagged as a gap: `docs/next-features.md` §13. + ## Keyboard | Key | Action | diff --git a/src/uix/soma/components/navigation-menu/navigation-menu-provider.svelte.ts b/src/uix/soma/components/navigation-menu/navigation-menu-provider.svelte.ts index afe41d130..9ea36a111 100644 --- a/src/uix/soma/components/navigation-menu/navigation-menu-provider.svelte.ts +++ b/src/uix/soma/components/navigation-menu/navigation-menu-provider.svelte.ts @@ -829,10 +829,13 @@ export class NavigationMenuIndicatorProvider { left: `${rect.left}px`, top: `${rect.top}px`, 'pointer-events': 'none', - '--navigation-menu-indicator-x': `${rect.left}px`, - '--navigation-menu-indicator-y': `${rect.top}px`, - '--navigation-menu-indicator-w': `${rect.width}px`, - '--navigation-menu-indicator-h': `${rect.height}px` + // VALUE CHANNELS, not theming surface: the four measurements of + // the active trigger, written per instance on every move. A theme + // must not reach them, so they are `--_{c}-*` (firma 2026-08-26). + '--_navigation-menu-indicator-x': `${rect.left}px`, + '--_navigation-menu-indicator-y': `${rect.top}px`, + '--_navigation-menu-indicator-w': `${rect.width}px`, + '--_navigation-menu-indicator-h': `${rect.height}px` } : { position: 'absolute', 'pointer-events': 'none' } } as const); diff --git a/web/routes/uix/components/navigation-menu/+page.svelte b/web/routes/uix/components/navigation-menu/+page.svelte index ae949818e..13126a813 100644 --- a/web/routes/uix/components/navigation-menu/+page.svelte +++ b/web/routes/uix/components/navigation-menu/+page.svelte @@ -334,7 +334,7 @@
Item · Trigger · Content · Link · Indicator

- Item is the wrapper around each trigger / content pair (gets value for routing in the bar's value binding). Trigger renders a native <button> with chevron affordance; Link renders <a> with aria-current support — both styled by the recipe with the same flat in-bar chrome, NOT routed through <Button>. Indicator is a 2px underline that slides between active triggers via CSS vars ({`--navigation-menu-indicator-{x,y,w,h}`}) written by soma. + Item is the wrapper around each trigger / content pair (gets value for routing in the bar's value binding). Trigger renders a native <button> with chevron affordance; Link renders <a> with aria-current support — both styled by the recipe with the same flat in-bar chrome, NOT routed through <Button>. Indicator is a 2px underline that slides between active triggers via private value channels ({`--_navigation-menu-indicator-{x,y,w,h}`}) written by soma.

{/if} @@ -466,7 +466,7 @@ [data-navigation-menu-link][aria-current='page']morfoActive-page accent. [data-navigation-menu-content]morfoMega-menu panel chrome + entrance animation. [data-navigation-menu-content][data-motion]morfoDirectional slide on trigger swap (from-start/end + to-start/end). - [data-navigation-menu-indicator]morfo2px underline that slides via CSS vars ({`--navigation-menu-indicator-{x,y,w,h}`}). + [data-navigation-menu-indicator]morfo2px underline that slides via private value channels ({`--_navigation-menu-indicator-{x,y,w,h}`}).