refactor(theming)!: el indicador de navigation-menu es CANAL - y el consumidor estiliza la PARTE

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 <noreply@anthropic.com>
alpha-0.1-background
dev 1 month ago
parent b9339633f0
commit de2e136a0f

@ -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

@ -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.

@ -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 `<Slider>` 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

@ -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 `<a>`
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

@ -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;

@ -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

@ -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);

@ -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)',

@ -193,25 +193,28 @@ const PENDING_PRIVATE_RENAME: Readonly<Record<string, string>> = {
// 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

@ -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 |

@ -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);

@ -334,7 +334,7 @@
<div data-uix-subsection-head>Item · Trigger · Content · Link · Indicator</div>
<p data-uix-section-desc style="margin-top: 0.25rem;">
<code>Item</code> is the wrapper around each trigger / content pair (gets <code>value</code> for routing in the bar's <code>value</code> binding). <code>Trigger</code> renders a native <code>&lt;button&gt;</code> with chevron affordance; <code>Link</code> renders <code>&lt;a&gt;</code> with <code>aria-current</code> support — both styled by the recipe with the same flat in-bar chrome, NOT routed through <code>&lt;Button&gt;</code>. <code>Indicator</code> is a 2px underline that slides between active triggers via CSS vars (<code>{`--navigation-menu-indicator-{x,y,w,h}`}</code>) written by soma.
<code>Item</code> is the wrapper around each trigger / content pair (gets <code>value</code> for routing in the bar's <code>value</code> binding). <code>Trigger</code> renders a native <code>&lt;button&gt;</code> with chevron affordance; <code>Link</code> renders <code>&lt;a&gt;</code> with <code>aria-current</code> support — both styled by the recipe with the same flat in-bar chrome, NOT routed through <code>&lt;Button&gt;</code>. <code>Indicator</code> is a 2px underline that slides between active triggers via private value channels (<code>{`--_navigation-menu-indicator-{x,y,w,h}`}</code>) written by soma.
</p>
</section>
{/if}
@ -466,7 +466,7 @@
<tr><td class="name">[data-navigation-menu-link][aria-current='page']</td><td><span data-uix-tag data-kind="soma">morfo</span></td><td>Active-page accent.</td></tr>
<tr><td class="name">[data-navigation-menu-content]</td><td><span data-uix-tag data-kind="soma">morfo</span></td><td>Mega-menu panel chrome + entrance animation.</td></tr>
<tr><td class="name">[data-navigation-menu-content][data-motion]</td><td><span data-uix-tag data-kind="soma">morfo</span></td><td>Directional slide on trigger swap (from-start/end + to-start/end).</td></tr>
<tr><td class="name">[data-navigation-menu-indicator]</td><td><span data-uix-tag data-kind="soma">morfo</span></td><td>2px underline that slides via CSS vars (<code>{`--navigation-menu-indicator-{x,y,w,h}`}</code>).</td></tr>
<tr><td class="name">[data-navigation-menu-indicator]</td><td><span data-uix-tag data-kind="soma">morfo</span></td><td>2px underline that slides via private value channels (<code>{`--_navigation-menu-indicator-{x,y,w,h}`}</code>).</td></tr>
</tbody>
</table>
</div>

Loading…
Cancel
Save

Powered by TurnKey Linux.