refactor(theming)!: MeasuredIndicator escribe en el namespace del HOST - la capa es la pluma, no la dueña

Firma SS14 (2026-08-26, nombre poseido tras rechazar el nombre generico de
capa: habria repetido el defecto un piso abajo). La capa compartida
publicaba el rect medido como --indicator-{x,y,w,h} - forma PUBLICA sin
dueño, invisible para la ley del espacio cerrado (que audita --{c}-*) y
para todos los guards. Tercera aplicacion de la doctrina del canal
(navigation-menu fue la segunda, tabs la habia firmado por escrito citando
a navmenu como precedente).

- MeasuredIndicatorOpts gana `component: string`; los DOS hosts (tabs,
  radio-group - barrido completo, no hay mas) pasan xxxMorfo.kebab, NUNCA
  un literal: el nombre del canal queda atado a la declaracion que lo posee.
- tabs.css lee el poseido directo; el css COMPARTIDO (sliding-indicator)
  lee el alias local que cada receta declara (--_indicator-x:
  var(--_radio-group-indicator-x)) - el cableado del puente THM-2, contrato
  en el artefacto. --indicator-{duration,ease} INTACTOS a proposito: son
  ranuras de FORMA, no canal - el _ separa las dos especies en el mismo
  fichero; su falta de dueño queda flagged como especie pendiente.
- El censo aprende el CANAL TRANSITIVO (molde de derivesFromPublic,
  memoizado, a prueba de ciclos, con la mutacion que impide que un alias
  sobre un privado declarado clasifique canal). Efecto colateral MEDIDO con
  acta: tooltip.css:27 pasa de exception a channel - su comentario firmado
  ya decia "per-instance VALUE CHANNEL"; faltaba el mecanismo, no el
  criterio.
- Ledger: la clave tabs RETIRADA entera (2 entradas global) con acta que
  hace la lectura honesta - no se tokenizo nada ni se perdono deuda: se
  corrigio una mala clasificacion en su fuente. 1154 -> 1152, 0/0.
- Numeros: tabs 97% -> 100% (global 2->0, channel 0->2, ni una clave
  acunada) - atHundred 65 -> 66 - reach global 73% y public 3072 intactos.
- Dinamica verbatim en los dos hosts (radio-group manifiesta en segmented):
  el canal poseido LLEVA el rect del item activo (x 784=784, w 71=71 /
  x 793=793, w 102=102), el indicador desliza, los nombres viejos computan
  vacio, control negativo quieto y positivo probando la CADENA del alias.
  Re-verificado DESPUES de aterrizar el codemod del peer. Sonda estandar de
  tabs 2528 valores x 10 estados a 0 diffs - dicho en voz alta: esa sonda
  es CIEGA al canal; su 0 prueba lo circundante.
- Guards: 154 tests verdes - centinela tabs 70/79 identico (ninguna
  adjudicacion citaba las medidas) - audit 162 PASS - docs:check 0/0 -
  src/ sigue a 0 errores.

ATRIBUCION DECLARADA (acordada por canal entre sesiones): este commit CARGA
ademas la retirada de 5+5 `syncAttrs: true` en tabs-provider y
radio-group-provider - trabajo de "P0 fase C2c, audit 2026-08-26 - retirada
de syncAttrs, sesion de la auditoria" (vicen-63), que aterrizo en disco
durante esta construccion y cuya restauracion habria sido churn sin valor.
Su commit de C2c anota el viaje desde el otro lado.

BREAKING: --indicator-{x,y,w,h} ya no existen; el canal es
--_{c}-indicator-* del host y el css compartido lee el alias por receta.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
alpha-0.1-background
dev 1 month ago
parent 243122393d
commit 8ac23047f1

@ -71,6 +71,23 @@ instancia (veredictos §5 de `card`, `tags-input`, `avatar`).
## 3. Privados de la receta — ¿de dónde sale su valor?
<!-- mano:start -->
> **Nota 2026-08-26 (firma §14).** La receta declara ahora cuatro alias más sobre
> `[data-radio-group-selection-indicator]`:
> `--_indicator-{x,y,w,h}: var(--_radio-group-indicator-{x,y,w,h})`. Son el
> cableado del **canal de valor** que `MeasuredIndicator` publica: la capa
> compartida escribe el rect en el espacio de ESTE componente (el rect es un
> hecho de esta instancia), y `lib/sliding-indicator.css` —que no puede deletrear
> el nombre del anfitrión— lo lee por el alias, igual que
> `calendar-surface.css` lee `--_calendar-surface-accent`. **Ninguna cifra de
> esta hoja se mueve**: alcance 98 %, 59 knobs, el reparto idéntico. Las cuatro
> `--_radio-group-indicator-*` sí se sumarán a la lista de «consumidos y no
> declarados» de abajo al regenerar, que es justo lo que son. No se acuñan: un
> tema que fije una de las cuatro congela la píldora.
<!-- mano:end -->
| privado | declaraciones | valor(es) | origen | ¿deriva de un público? |
| --- | ---: | --- | --- | :-: |
| `--_radio-group-palette-text` | 4 | `var(--radio-group-dot-fg)`, `var(--_radio-group-palette-solid)` | private, public | no |

@ -12,6 +12,22 @@
## 1. Knobs fuera de alcance
<!-- mano:start -->
> ⚠ **Todo lo medido de esta hoja es del 2026-08-25 y quedó SUPERADO el
> 2026-08-26** por la firma §14 («MeasuredIndicator con nombre poseído»). Los
> dos knobs de §1.1 y las dos filas propuestas en §4.1 hablan de
> `var(--indicator-w, 0px)` / `var(--indicator-h, 0px)`: ese nombre ya no
> existe. La capa compartida escribe ahora en el espacio del ANFITRIÓN,
> `--_tabs-indicator-{x,y,w,h}`, y el censo los lee como lo que son —**canal**,
> fuera del ratio—. Cifras de hoy: **alcance 100 %**, global 2 → 0, canal
> 0 → 2, 77 knobs iguales; las dos entradas de `tabs` en el ledger de deuda
> RETIRADAS (acta en la cabecera de `scripts/theming-census-debt.ts`). Ninguna
> clave se acuñó: subió el alcance porque el nombre dejó de mentir. El detalle,
> en el veredicto §5.
<!-- mano:end -->
### 1.1 Directo a primitivo global (2)
| # | fichero:línea | selector | propiedad | valor |
@ -135,4 +151,28 @@ muerta que ceder.
**Su techo es 97 % y no es deuda**: lo que queda son las dos lecturas del canal
de valor, que ningún tema debe alcanzar.
## Acta 2026-08-26 — el techo era 100 %, y el 3 % era el NOMBRE
El veredicto de arriba acertó en la doctrina y se quedó corto en la aritmética.
«Las dos lecturas del canal de valor» eran, en efecto, canal — pero el censo no
podía verlo, y no por un defecto del censo: las cuatro vars se llamaban
`--indicator-{x,y,w,h}`, **sin prefijo de componente**. Un nombre así es público
de FORMA y de nadie de HECHO: la ley del espacio cerrado
(`recipe-css-contract.test.ts`) sólo audita `--{c}-*`, así que le pasaba por
debajo; y la regla de canal del censo exige el prefijo PROPIO
(`--_{c}-…`), así que sólo le quedaba puntuarlas `global` — deuda. `tabs` era
invisible justamente por nombrar PEOR que `navigation-menu`, que fue visible por
llevar prefijo. El guard premiaba al que nombraba mejor.
La firma §14 lo cerró por el escritor: `MeasuredIndicator` recibe el kebab del
anfitrión (`tabsMorfo.kebab`, no un literal) y publica
`--_tabs-indicator-{x,y,w,h}`. La capa es la PLUMA, no la dueña — el rect es un
hecho de cada instancia del anfitrión.
**Medido**: alcance **97 % → 100 %**, global 2 → 0, canal 0 → 2, knobs 77
intactos, contrato 79 intacto (no se acuñó ni se retiró una clave). En el
catálogo, `atHundred` 65 → 66 y la deuda 1154 → 1152 claves. **§4.1 sigue siendo
incorrecta** y ahora también es inejecutable: el nombre que proponía tokenizar
ya no existe.
<!-- veredicto:end -->

@ -50,6 +50,35 @@ Un tema los alcanza **a nivel de sistema**, por diseño (recipe-contract §2).
## 2-quater. Canal de valor (3) — fuera del ratio
<!-- mano:start -->
> **Acta 2026-08-26 — un knob cruzó de «excepción firmada» a «canal», por
> MEDIDA.** `tooltip.css:27` (`inline-size: var(--_tooltip-width)`) aparece
> arriba en §1.4 porque hasta hoy sólo lo salvaba la firma a mano de la
> declaración; la firma §14 extendió la regla de canal del censo al caso
> TRANSITIVO —un privado declarado cuyas fuentes resuelven TODAS a canales ES
> canal— y `--_tooltip-width` es exactamente eso:
> `var(--_tooltip-content-width-override, var(--_tooltip-match-anchor-width,
> auto))`, dos nombres que nadie declara y que se escriben por instancia. El
> propio comentario de la declaración ya lo decía con esas palabras («per-instance
> VALUE CHANNEL»); lo que faltaba era el mecanismo, no el criterio. Por eso NO es
> una reclasificación a la baja: es el cuarto paso del censo funcionando como
> está escrito («una firma sobre un knob que la máquina ya adjudicó es un acta sin
> nada que firmar»). Al regenerar, esta hoja leerá canal 4 · excepción 1 · knobs
> 21 (el `exception` no entra en el total de knobs y el `channel` sí; el ALCANCE
> no se mueve: 94 %, los dos están fuera del ratio).
>
> Lo que la extensión **no** se lleva por delante, y es la mutación de rigor que
> la delimita: `--_tooltip-max-width` lee
> `var(--_tooltip-content-max-width-override, var(--tooltip-max-width))` y su
> knob sigue siendo **público**, porque el segundo paso del censo (deriva de un
> público) corre ANTES que el tercero. Ese orden es carga estructural de la
> extensión, no casualidad. Un alias sobre un privado que SÍ está declarado
> tampoco cuela: resuelve al valor de esa declaración, cuyas refs son vacías o no
> son canal.
<!-- mano:end -->
La receta lee un privado que **nadie declara** — ni su CSS ni el generador: lo
escribe soma o el envoltorio **por instancia** (un %, un rect medido, la talla
que pide una prop). Un tema no debe alcanzarlo: fijarlo rompe el

@ -2559,7 +2559,12 @@ 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
**✅ EJECUTADA 2026-08-26** — firma «MeasuredIndicator con nombre poseído», la
**tercera** aplicación de la doctrina del canal (`navigation-menu` fue la
segunda). El acta, con los números medidos, cierra esta sección; lo redactado
abajo se conserva porque el diagnóstico fue correcto entero.
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
@ -2606,3 +2611,102 @@ en estas partes, así que una lectura inmediata devuelve el valor VIEJO — hay
congelar transiciones o el probe informa «no se movió» de todo.
**Deps**: ninguna. Ejecutable cuando se quiera cerrar la clase.
### 14-bis. Acta de ejecución — 2026-08-26
**Lo decidido en el punto 1**: `--_{c}-indicator-{x,y,w,h}`, **no**
`--_indicator-*` de capa. Porque el rect es un hecho de CADA INSTANCIA del
anfitrión: la capa sostiene la pluma, no es la dueña. Un solo nombre de capa
para dos anfitriones habría vuelto a poner una medida por instancia en un
espacio que nadie posee — el mismo defecto, un piso más abajo.
**El paso del kebab, mínimo**: `MeasuredIndicatorOpts` gana **un** campo,
`component: string`, y los dos hosts pasan `tabsMorfo.kebab` /
`radioGroupMorfo.kebab` — **nunca un literal**. El nombre del canal queda atado
a la declaración de morfo que lo posee, así que un renombrado de kebab viaja
solo. Nada más se movió en la capa.
**El cableado del CSS COMPARTIDO** (`lib/sliding-indicator.css`, que no puede
deletrear el nombre del anfitrión): cada receta declara su alias local explícito
—`--_indicator-x: var(--_radio-group-indicator-x)`— y el fichero compartido lee
el alias. Es la forma del puente THM-2 y la que `calendar-surface.css` ya usa
para `--_calendar-surface-accent`: contrato EN el artefacto, greppable en un
paso. `tabs` no consume la capa (tiene su bloque propio), así que lee el nombre
poseído directo.
**Hosts, barridos**: exactamente DOS (`git grep "new MeasuredIndicator"`), los
dos ya redactados arriba. El barrido de retirada confirmó la trampa de las
llaves otra vez: el grep del nombre LITERAL veía 8 menciones, y el de la forma
`--indicator-{…}` más el genérico `--indicator-` destapó **tres comentarios de
código vivo más** (`radio-group.svelte:21`, `morfo/components/radio-group.ts:140`,
`tabs-provider.svelte.ts:423`). Consumos vivos de los cuatro nombres viejos tras
el flip: **0**.
**El punto 2 se respetó**: `--indicator-duration` / `--indicator-ease` siguen
bare y sin tocar — son ranuras de FORMA de la capa (un tema las mueve), no
canal. El `_` es lo que separa las dos especies dentro del mismo fichero, y así
queda escrito en su cabecera. Su puerta sigue siendo la del contrato.
**El punto 3, medido**: el suelo **sube**. `tabs` 97 % → **100 %** (global
2 → 0, canal 0 → 2, 77 knobs y 79 claves de contrato intactos: no se acuñó ni se
retiró una sola). Catálogo: `atHundred` **65 → 66**, global 763 → **761**, canal
38 → **41**, reach global **73 % intacto**, `--names` **4566 · 0 desviadas**
(intacto: `base.ts` no se tocó). Ledger de deuda: las **2 entradas de `tabs`
RETIRADAS** —la clave entera desaparece— con acta en la cabecera de
`scripts/theming-census-debt.ts`; **1154 → 1152 · 0 NEW · 0 STALE**.
**La trampa de clasificación, prevista y resuelta**: la regla de canal del censo
mira el privado que el knob lee DIRECTAMENTE con cero declaraciones; una
línea-alias es un privado DECLARADO cuya única fuente es el canal, así que la
esquiva y cae a residuo `private` no-derivado. El tercer paso de `scanComponent`
resuelve ahora el canal **TRANSITIVAMENTE** (molde de `derivesFromPublic`:
memoizado, a prueba de ciclos, `every` en todo el camino). La **mutación de
rigor** que lo delimita: un alias hacia un privado que SÍ está declarado NO
clasifica canal — resuelve al valor de esa declaración, cuyas refs son vacías (un
literal) o no son canal; `refs.length > 0` es la línea que lo impone. Y el orden
de pasos es carga estructural: el segundo (deriva de un público) corre ANTES, así
que `--_tooltip-max-width`, que lee `var(--_tooltip-content-max-width-override,
var(--tooltip-max-width))`, sigue siendo **público**.
**Un efecto colateral, medido y adjudicado con acta**: la extensión reclasificó
**un** knob ajeno, `tooltip.css:27` (`inline-size: var(--_tooltip-width)`), de
`exception` a `channel`. **No es regresión, es validación independiente**: el
comentario firmado de esa declaración ya decía «per-instance VALUE CHANNEL» con
esas palabras, y `--_tooltip-width` resuelve a dos nombres que nadie declara.
Faltaba el mecanismo, no el criterio — que es exactamente lo que el cuarto paso
del censo dice de sí mismo («una firma sobre un knob que la máquina ya adjudicó
es un acta sin nada que firmar»). `tooltip` no mueve su alcance (94 %: las dos
clases están fuera del ratio). Acta en `docs/audit/theming/tooltip.md` §2-quater.
**Verificación**. Dinámica por CDP con transiciones CONGELADAS (el aviso de esta
misma sección: `inline-size` va transicionada y una lectura inmediata devuelve el
valor VIEJO), en los **dos** hosts. `tabs`: el estilo inline lleva sólo los
nombres poseídos, los cuatro viejos computan **cadena vacía**, y al cambiar de
pestaña `translate 0px → 183.719px` e `inline-size 99.906px → 71.359px` — el
indicador cae EXACTO sobre el trigger activo (x 784 vs 784, w 71 vs 71). Control
negativo: escribir los cuatro nombres viejos en `:root` no mueve **nada**.
Control positivo: `--_tabs-indicator-w` en el elemento → 333px. `radio-group`
manifiesta su indicador **sólo en la variante `segmented`** (la píldora
`[data-radio-group-selection-indicator]`, que el envoltorio auto-monta ahí y en
ningún otro sitio): el alias computa los mismos cuatro valores que el nombre
poseído, la píldora desliza (`translate 4px 32px → 4px 60px`) y cubre exacto el
item marcado (x 793 vs 793, w 102 vs 102); mismo control negativo a cero y el
positivo probando la CADENA del alias (101.703px → 222px a través del CSS
compartido).
Estática: sonda estándar de `tabs` antes/después, **2528 valores computados en 10
estados, 0 diffs y 0 nodos perdidos**. ⚠ Y hay que decirlo: **esa sonda es CIEGA
al canal** — lee valores computados, no nombres de var, así que un flip de nombre
correcto le sale idéntico por construcción. Su 0 no prueba el flip; prueba lo
CIRCUNDANTE, que es para lo que se corre. Quien prueba el flip es la dinámica.
Verde lo demás: `recipe-css-contract` + `rtl-lint` + `theming-reach-floor` +
`component-visual-attrs` + `generated-css` + las suites de los tocados (10
ficheros, 154 tests), centinela de `tabs` **70/79** idéntico, `component:audit`
**162 PASS / 4 NEEDS-WORK / 0 BROKEN** idéntico, `docs:check` **0/0** en 816
docs, `npm run check` sin errores atribuibles.
**Lo que queda flagged, sin acuñar**: `--indicator-duration` / `--indicator-ease`
siguen siendo dos públicos de FORMA sin prefijo de dueño y sin entrada de
contrato — la misma fuga de espacio que esta firma cerró para las medidas, una
especie más allá. El punto 2 los mandó a la puerta del contrato y ahí siguen.

@ -342,6 +342,12 @@ temprano**, para que la dependencia se registre en todas las pasadas. El consumi
(`radio-group`) le pasa `resolvedDir`. Getter y no lectura del DOM: la dirección
viene de la cadena prop/prefs, y el atributo es proyección, no fuente.
> **Nota 2026-08-26** — las cuatro medidas que aquí se llaman `--indicator-*` se
> renombraron al namespace del anfitrión (`--_{c}-indicator-{x,y,w,h}`) por la
> firma §14 del eje theming. Nada de lo medido en esta sección cambia: sólo el
> nombre. Detalle en `CONTINUE-theming.md`, «§14, MeasuredIndicator con nombre
> POSEÍDO».
⚠️ **TRAMPA DE MÉTODO, me costó una pasada entera**: el primer probe volteaba con
`el.setAttribute('dir', 'rtl')` y daba «no arreglado» — pero eso **no cambia
`resolvedDir`**, que sale de la prop/prefs, así que la dependencia no podía

@ -189,8 +189,67 @@ 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**.
compartida `MeasuredIndicator`, 2 entradas `global` en el ledger). **EJECUTADA
el mismo día**; ver la firma inmediatamente debajo.
### FIRMA EJECUTADA (2026-08-26) — §14, MeasuredIndicator con nombre POSEÍDO
**Qué se firmó**: la geometría que una capa COMPARTIDA publica es **canal** y
aterriza en el **namespace del ANFITRIÓN** —`--_{c}-indicator-{x,y,w,h}`—, no en
uno de capa. Tercera aplicación de la doctrina (`navigation-menu` fue la
segunda). **La capa es la PLUMA, no la dueña**: el rect es un hecho de cada
instancia del host. Un solo nombre de capa habría repetido el defecto un piso
más abajo — una medida por instancia en un espacio que nadie posee.
**Cómo llega el nombre**: `MeasuredIndicatorOpts` gana **un** campo,
`component: string`, y los dos hosts —los únicos, `git grep "new
MeasuredIndicator"`— pasan `tabsMorfo.kebab` / `radioGroupMorfo.kebab`, **nunca
un literal**: el canal queda atado a la declaración de morfo que lo posee.
**Cableado del CSS compartido** (`lib/sliding-indicator.css`, que no puede
deletrear el nombre del host): cada receta declara su alias local explícito
—`--_indicator-x: var(--_radio-group-indicator-x)`— y el compartido lee el
alias. Forma del puente THM-2, la misma que `calendar-surface.css` usa para
`--_calendar-surface-accent`: contrato EN el artefacto. `tabs` no consume la capa
(tiene bloque propio) y lee el nombre poseído directo.
**Números**: `tabs` **97 % → 100 %** (global 2 → 0, canal 0 → 2; 77 knobs y 79
claves de contrato INTACTOS — ni una acuñada ni retirada). Catálogo `atHundred`
**65 → 66**, global 763 → 761, canal 38 → 41, reach global **73 % intacto**,
`--names` 4566 · 0 desviadas. Ledger: las **2 entradas de `tabs` retiradas**
(la clave entera desaparece), **1154 → 1152 · 0 NEW · 0 STALE**, con acta en la
cabecera de `theming-census-debt.ts`. `radio-group` no mueve una cifra.
**⚠ La trampa de clasificación, prevista y resuelta**: la regla de canal miraba
el privado que el knob lee DIRECTAMENTE con cero declaraciones — una línea-alias
es un privado DECLARADO cuya única fuente es el canal, así que la esquivaba y
caía a residuo `private` no-derivado, `newDebt` ROJO con la razón equivocada. El
tercer paso del censo resuelve ahora el canal **TRANSITIVAMENTE** (molde de
`derivesFromPublic`). **Mutación de rigor**: un alias hacia un privado que SÍ
está declarado NO clasifica canal — `refs.length > 0` es la línea que lo impone.
Y el ORDEN de pasos es carga estructural: el segundo (deriva de un público) corre
antes, y por eso `--_tooltip-max-width` sigue siendo público.
**⚠ Un knob AJENO se reclasificó, con acta**: `tooltip.css:27` pasó de
`exception` a `channel`. **No es regresión**: el comentario firmado de esa
declaración ya decía «per-instance VALUE CHANNEL» con esas palabras — faltaba el
mecanismo, no el criterio, que es literalmente lo que el cuarto paso del censo
dice de sí mismo. `tooltip` no mueve su alcance. Acta en su ficha, §2-quater.
**⚠ La sonda estándar es CIEGA a esta clase de firma**: lee valores computados,
no nombres de var, así que un flip de nombre correcto le sale idéntico por
construcción. Sus **0 diffs en 2528 valores × 10 estados** prueban lo
CIRCUNDANTE, no el flip. Quien prueba el flip es la dinámica, y hay que
CONGELAR transiciones o `inline-size` devuelve el valor viejo: `tabs` desliza
(`translate 0 → 183.719px`, `inline-size 99.906 → 71.359px`, indicador exacto
sobre el trigger activo), `radio-group` sólo manifiesta su indicador en la
variante **`segmented`** y desliza igual (`4px 32px → 4px 60px`). Los cuatro
nombres viejos computan **cadena vacía** en ambos; control negativo en `:root` a
cero; control positivo probando además la CADENA del alias.
**Hueco FLAGGED, no acuñado**: `--indicator-duration` / `--indicator-ease` siguen
bare, sin dueño y sin entrada de contrato — la misma fuga de espacio, una especie
más allá (son ranuras de FORMA, no canal). Su puerta es el contrato, no ésta.
### FIRMA EJECUTADA (2026-08-26) — la ley del espacio cerrado
@ -982,9 +1041,12 @@ eso ES el resultado.
- **`tabs`** — la mitad de un contrato puede vivir en las VARIANTES (40/79 →
70/79 barriendo `data-variant` y usando las dos URL que el propio demo
ofrece), y su indicador es **canal de valor**: `--indicator-{x,y,w,h}` los
escribe `MeasuredIndicator` en cada movimiento, así que un público encima
ofrece), y su indicador es **canal de valor**: `--_tabs-indicator-{x,y,w,h}`
los escribe `MeasuredIndicator` en cada movimiento, así que un público encima
sería un knob que un tema puede fijar — y fijarlo rompe el deslizamiento.
⚠ **Actualizado 2026-08-26 (§14)**: entonces se llamaban `--indicator-*`, sin
prefijo, y por eso el censo sólo podía puntuarlos `global` — el techo de
`tabs` NO era 97 %, era **100 %**, y el 3 % que faltaba era el NOMBRE.
- **`toast`** — **§12.9 en una tarjeta PORTALADA se lleva la TARJETA ENTERA**,
21 claves, y con coste de PRODUCTO: un toast `risk` y uno `fulfill` visten la
misma tarjeta gris, y la franja de acento no se ve nunca en reposo (el plano

@ -842,7 +842,13 @@ excepción abre la mano para MEDIR, no para contabilizar.
escribe el rect del trigger activo en cada movimiento, así que un público
encima sería un knob que un tema puede FIJAR, y fijarlo rompe el
deslizamiento. Es la adjudicación que `navigation-menu` ya lleva escrita para
su propio `indicator-w` (y la razón por la que su `-x` se retiró, muerto). El
su propio `indicator-w` (y la razón por la que su `-x` se retiró, muerto).
⚠ **Anotado 2026-08-26 (firma §14)**: la adjudicación era correcta y el
NÚMERO se quedó corto. Aquellas vars se llamaban `--indicator-*` **sin prefijo
de componente** —públicas de forma, de nadie de hecho—, así que el censo sólo
podía puntuarlas `global` y la ley del espacio cerrado no las veía. Movidas al
namespace del anfitrión (`--_tabs-indicator-*`) leen como el canal que son:
`tabs` **97 % → 100 %**, sin acuñar una sola clave. El
tercero, el `width: 100%` de `fitted`, se FIRMA como identidad. **Lo que
enseñó**: (1) **la mitad de un contrato puede vivir en las VARIANTES** — el
escenario arranca en `line` y el guard leía 40/79; barriendo `data-variant` y

@ -109,6 +109,24 @@
* design: `system`, `structural`, `bridge`, `channel` and `exception` — four
* measured classes and one written act, each documented where it is decided.
*
* ── ACTA 2026-08-26 — the whole `tabs` key RETIRED, two entries ─────────────
* `global · tabs.css · [data-tabs-indicator] · {block-size, inline-size}` were
* the last two, and they left by exit 1 (TOKENIZED) read strictly, by exit 4
* (SYSTEM) read honestly: the declarations did not change shape, the NAME they
* read did. Firma §14 («MeasuredIndicator con nombre poseído»): the shared soma
* layer used to publish the measured rect as a bare `--indicator-{x,y,w,h}` —
* PUBLIC in shape and owned by nobody, which is why the census could only score
* it `global` and why the closed-namespace law never saw it (that law audits
* `--{c}-*`, and a name with no component prefix is foreign vocabulary). The
* layer now writes into the HOST's namespace, `--_tabs-indicator-*`, so the two
* knobs read as what they always were: the value CHANNEL, out of the ratio.
* Measured: `tabs` global 2 → 0, channel 0 → 2, reach 97 % → 100 %, knobs 77
* unchanged; catalogue `atHundred` 65 → 66. No key was tokenized away and no
* debt was forgiven — a misclassification was corrected at its source, which is
* exactly the reading the header's «renaming a selector reads as one STALE + one
* new key» paragraph anticipates, except here the new key belongs to a class
* this ledger does not register.
*
* Generated: `node --import tsx/esm scripts/theming-census.ts --debt --write`
* Checked: `node --import tsx/esm scripts/theming-census.ts --debt`
* Guarded: `src/uix/eidos/theming-reach-floor.test.ts` (catalogue) +
@ -1238,10 +1256,6 @@ export const CENSUS_DEBT: Record<string, string[]> = {
'literal · table.css · [data-table-sort-trigger][data-sorted] [data-table-sort-indicator] · opacity',
'literal · table.css · [data-table] · inline-size'
],
tabs: [
'global · tabs.css · [data-tabs-indicator] · block-size',
'global · tabs.css · [data-tabs-indicator] · inline-size'
],
'tag-group': ['literal · tag-group.css · [data-tag-group-remove-button] · line-height'],
'text-scramble': [
'literal · text-scramble.css · [data-text-scramble-sr] · height',

@ -652,13 +652,47 @@ export function scanComponent(dir: string): Scan | null {
// the same mechanism wherever it is emitted — but never a channel: this
// component's CSS is not where it would be declared.
const own = '--_' + dir + '-';
const isChannel = (r: string) =>
r.startsWith(own) && !declBySource.has(r) && !GENERATED_PRIVATES.has(r);
// TRANSITIVE, and this is the shape that forced it (firma §14, 2026-08-26): a
// SHARED layer's CSS cannot spell its host's name, so the consuming recipe
// declares a local ALIAS over the channel and the layer reads the alias — the
// palette bridge's wiring, one class over. That alias is a DECLARED private
// whose only source is an UNDECLARED one, so the direct test above misses it
// by construction and the knob falls to residue as a non-derived private:
// `newDebt` red with the wrong reason. Same mould as `derivesFromPublic`
// (cycle-safe via `seen`, memoized), and `every` throughout, so a knob mixing
// a channel with anything else stays residue.
//
// The rigour that keeps this from swallowing the class: an alias over a
// private that IS declared here resolves to THAT declaration's value, whose
// refs are either empty (a literal) or point somewhere that is not a channel
// — `refs.length > 0` is what makes the literal case return false. Only a
// chain that bottoms out in a name NOBODY declares reads as channel.
const channelCache = new Map<string, boolean>();
const resolvesToChannel = (name: string, seen = new Set<string>()): boolean => {
if (isChannel(name)) return true;
if (channelCache.has(name)) return channelCache.get(name)!;
if (seen.has(name)) return false;
seen.add(name);
const ok = (privValues.get(name) ?? []).every((v) => {
const refs = [...v.matchAll(/var\(\s*(--_[a-z0-9-]+)/g)].map((m) => m[1]);
return refs.length > 0 && refs.every((r) => resolvesToChannel(r, seen));
});
// `every` on an empty list is `true` — a name this component never declares
// is not an alias, it is a foreign private, and the guard above already
// refused it as a channel.
const declared = (privValues.get(name) ?? []).length > 0;
channelCache.set(name, ok && declared);
return ok && declared;
};
for (const k of knobs) {
if (k.klass !== 'private') continue;
const refs = [...k.value.matchAll(/var\(\s*(--_[a-z0-9-]+)/g)].map((m) => m[1]);
if (refs.length === 0) continue;
const klass: KnobClass | null = refs.every((r) => PALETTE_BRIDGE.has(r))
? 'bridge'
: refs.every((r) => r.startsWith(own) && !declBySource.has(r) && !GENERATED_PRIVATES.has(r))
: refs.every((r) => resolvesToChannel(r))
? 'channel'
: null;
if (klass === null) continue;

@ -219,8 +219,9 @@
* The options sit as flush text SEGMENTS in a recessed track; a neutral pill
* SLIDES to the checked one (iOS UISegmentedControl / Radix SegmentedControl /
* Ark SegmentGroup). Soma measures the checked item (MeasuredIndicator) and
* writes `--indicator-*` on the pill; `lib/sliding-indicator.css` positions it,
* this recipe SHAPES it. The per-item dot is dropped; role stays `radiogroup`.
* writes `--_radio-group-indicator-*` on the pill; `lib/sliding-indicator.css`
* positions it through the `--_indicator-*` alias declared below, and this
* recipe SHAPES it. The per-item dot is dropped; role stays `radiogroup`.
* In this variant the item BEARS its text (children) — unlike the circle
* variants, where the label sits external. */
@ -294,7 +295,7 @@
}
/* The sliding pill — SHAPE only; `lib/sliding-indicator.css` (opted in via
* `data-sliding-indicator`) owns position + size + slide from `--indicator-*`.
* `data-sliding-indicator`) owns position + size + slide from `--_indicator-*`.
* Its timing overrides feed the shared transition. */
/* The pill IS the selection marker → it takes the component's `color` (like the
* dot in the other variants), via the shared palette private. A coloured pill on
@ -305,6 +306,17 @@
background: var(--_radio-group-palette-solid);
border-radius: var(--radio-group-segmented-pill-radius);
box-shadow: var(--radio-group-segmented-pill-shadow);
/* The shared layer's ALIAS over soma's value channel (firma §14, 2026-08-26).
* `MeasuredIndicator` writes the rect in THIS component's namespace, because
* the rect is a fact of this instance; a shared file cannot spell that name,
* so it reads `--_indicator-*` and the consumer wires it here — the palette
* bridge's shape, contract in the artifact. NOT a theming surface: pinning
* one of the four from a theme freezes the pill. */
--_indicator-x: var(--_radio-group-indicator-x);
--_indicator-y: var(--_radio-group-indicator-y);
--_indicator-w: var(--_radio-group-indicator-w);
--_indicator-h: var(--_radio-group-indicator-h);
/* Shape slots of the layer, a different species — themeable, so bare. */
--indicator-duration: var(--radio-group-segmented-transition-duration);
--indicator-ease: var(--radio-group-segmented-transition-ease);
}

@ -18,7 +18,8 @@
import { composeInlineStyle } from '$uix/eidos/lib/style';
import { resolveComponentColor } from '$uix/eidos/lib/component-color';
// Shared sliding-indicator positioning — consumed by the `segmented` variant's
// pill (soma measures + writes `--indicator-*`; this file shapes it).
// pill (soma measures + writes `--_radio-group-indicator-*`, which the recipe
// aliases to the layer's `--_indicator-*`; this file shapes it).
import '$uix/eidos/lib/sliding-indicator.css';
import * as RadioGroup from '$soma/components/radio-group';
import type { RadioGroupProps } from './types';

@ -69,7 +69,8 @@ Sources: [Radix](https://www.radix-ui.com/primitives/docs/components/tabs),
relationships.
- Soma owns value, keyboard navigation, activation mode, trigger/content ID
registries, disabled handling, presence and the indicator's MEASUREMENT
(`MeasuredIndicator`, exposed as `--indicator-{x,y,w,h}`).
(`MeasuredIndicator`, exposed as `--_tabs-indicator-{x,y,w,h}` — the layer is
shared, the NAME is this component's; firma §14, 2026-08-26).
- Eidos owns size/variant/collapse/scroll/motion presentation, and positions +
shapes the indicator from those vars — the "soma positions, eidos shapes"
contract. The measurement used to live in the eidos wrapper; it moved to soma
@ -95,11 +96,20 @@ demo las resuelve en vivo sobre el escenario. El reparto, por dónde pintan:
Lo que el eje dejó **fuera** el 2026-08-24 (96 % → 97 %), medido y escrito:
- **`--indicator-{x,y,w,h}` NO se acuñan.** Son el canal de VALOR de soma: la
capa `MeasuredIndicator` escribe el rect del trigger activo en cada
- **`--_tabs-indicator-{x,y,w,h}` NO se acuñan.** Son el canal de VALOR de soma:
la capa `MeasuredIndicator` escribe el rect del trigger activo en cada
movimiento. Un público encima sería un knob que un tema podría fijar, y
fijarlo rompe el deslizamiento — la misma adjudicación que `navigation-menu`
ya lleva para su propio `indicator-w`.
Hasta el **2026-08-26** las cuatro se llamaban `--indicator-{x,y,w,h}`, **sin
prefijo de componente**: públicas de FORMA y de nadie. Por eso el censo sólo
podía puntuarlas `global` (2 entradas del ledger de deuda) y la ley del
espacio cerrado no las veía —esa ley audita `--{c}-*`, y un nombre sin prefijo
es vocabulario ajeno—. La firma §14 hizo que la capa compartida escriba en el
espacio del ANFITRIÓN: `tabs` pasó de **97 % a 100 %** (global 2 → 0, canal
0 → 2, 77 knobs iguales) sin acuñar una sola clave. El alcance no subió porque
se tokenizara nada: subió porque el nombre dejó de mentir.
- **`width: 100%` de `fitted` es identidad**, no knob: `fitted` ES «la lista
llena el eje inline». Firmado con su anotación `literal:`.

@ -4,7 +4,8 @@
* slides between active triggers.
*
* Shape only. Soma measures the active trigger and hands down
* `--indicator-{x,y,w,h}` (the shared `MeasuredIndicator` layer); the recipe
* `--_tabs-indicator-{x,y,w,h}` (the shared `MeasuredIndicator` layer, which
* writes into the HOST's namespace — firma §14, 2026-08-26); the recipe
* positions and sizes from those vars and owns the slide transition. This
* wrapper used to do the measuring itself with its own rAF + observers —
* that duplicated the layer and, having no access to the resolved direction,

@ -283,10 +283,10 @@
* active trigger's offset rect. The recipe handles the slide animation,
* z-index, and per-variant appearance. */
/* Position + size come from soma's `--indicator-*` (the "soma positions, eidos
shapes" contract); the variants below own the look and may pin a cross-axis
dimension on top. Uses the independent `translate` property, not `transform`,
so a per-variant `transform` never wipes the slide.
/* Position + size come from soma's `--_tabs-indicator-*` (the "soma positions,
eidos shapes" contract); the variants below own the look and may pin a
cross-axis dimension on top. Uses the independent `translate` property, not
`transform`, so a per-variant `transform` never wipes the slide.
Those four vars are soma's VALUE CHANNEL, not a theming surface: the
`MeasuredIndicator` layer writes the active trigger's offset rect onto this
@ -297,6 +297,12 @@
thickness, fill, radius, shadow, the slide's duration and ease — and those
are the keys below.
They are PRIVATE and they carry the host's name (firma §14, 2026-08-26).
Until then the layer wrote a bare `--indicator-*`: public in SHAPE, owned by
nobody, and therefore invisible to the closed-namespace law, which only
audits `--{c}-*`. Spelling them `--_tabs-*` is what makes the census read
them as the channel they are.
`left` is PHYSICAL on purpose: soma measures `triggerRect.left - listRect.left`,
an offset that grows rightwards whatever the direction. A logical
`inset-inline-start` anchored the indicator to the RIGHT edge in RTL while the
@ -309,9 +315,9 @@
left: 0;
pointer-events: none;
z-index: 0;
translate: var(--indicator-x, 0px) var(--indicator-y, 0px);
inline-size: var(--indicator-w, 0px);
block-size: var(--indicator-h, 0px);
translate: var(--_tabs-indicator-x, 0px) var(--_tabs-indicator-y, 0px);
inline-size: var(--_tabs-indicator-w, 0px);
block-size: var(--_tabs-indicator-h, 0px);
transition:
translate var(--tabs-indicator-transition-duration) var(--tabs-indicator-transition-ease),
inline-size var(--tabs-indicator-transition-duration) var(--tabs-indicator-transition-ease),

@ -4,9 +4,27 @@
*
* The soma `MeasuredIndicator` layer (`soma/layers/measured-indicator`) writes
* the active item's offset rect onto the indicator element as
* `--indicator-{x,y,w,h}`. This file POSITIONS + sizes the indicator from those
* vars and animates the slide; each component's recipe SHAPES it on top
* (pill background, underline thickness, border-radius, box-shadow).
* `--_{component}-indicator-{x,y,w,h}` — the HOST's namespace, because the rect
* is a fact of the host's instance (firma §14, 2026-08-26; the bare
* `--indicator-*` it wrote before was public in SHAPE and owned by nobody).
*
* A SHARED file cannot spell the host's name, so it reads a local ALIAS and each
* consuming recipe declares it — the palette bridge's wiring, and the same shape
* `calendar-surface.css` uses for `--_calendar-surface-accent`:
*
* [data-radio-group-selection-indicator] {
* --_indicator-x: var(--_radio-group-indicator-x);
* }
*
* The contract lives IN the artifact and greps in one step. This file POSITIONS
* + sizes the indicator from those vars and animates the slide; each component's
* recipe SHAPES it on top (pill background, underline thickness, border-radius,
* box-shadow).
*
* ⚠ `--indicator-duration` / `--indicator-ease` below are a DIFFERENT species
* and keep their bare spelling on purpose: they are the layer's shape slots — a
* theming surface a recipe declares — not a per-instance channel. The `_` is
* what separates the two.
*
* The consumer's eidos wrapper STAMPS `data-sliding-indicator` on the indicator
* element — a VISUAL-LAYER identity (NOT a morfo attr), the same pattern as
@ -29,9 +47,9 @@
the two are identical, so this changes nothing there. */
left: 0;
pointer-events: none;
translate: var(--indicator-x, 0px) var(--indicator-y, 0px);
inline-size: var(--indicator-w, 0px);
block-size: var(--indicator-h, 0px);
translate: var(--_indicator-x, 0px) var(--_indicator-y, 0px);
inline-size: var(--_indicator-w, 0px);
block-size: var(--_indicator-h, 0px);
transition:
translate var(--indicator-duration, var(--duration-fast))
var(--indicator-ease, var(--ease-default)),

@ -78,18 +78,20 @@ describe('lintRtlGeometry — RTL-1', () => {
});
it('treats a var() X as displacing — the sign is unknown at author time', () => {
// `sliding-indicator.css`: soma writes `--indicator-x` from an offset rect.
// `sliding-indicator.css`: soma writes the offset rect into the HOST's
// namespace and the shared file reads it through the `--_indicator-*`
// alias each consuming recipe declares (firma §14, 2026-08-26).
const css = `
[data-sliding-indicator] {
inset-inline-start: 0;
translate: var(--indicator-x, 0px) var(--indicator-y, 0px);
translate: var(--_indicator-x, 0px) var(--_indicator-y, 0px);
}
`;
const findings = lintRtlGeometry(css);
expect(findings).toHaveLength(1);
// The comma inside var() must not split the X component off.
expect(findings[0].translate).toBe(
'translate: var(--indicator-x, 0px) var(--indicator-y, 0px)'
'translate: var(--_indicator-x, 0px) var(--_indicator-y, 0px)'
);
});

@ -137,7 +137,7 @@ export const radioGroupMorfo = {
// a pill that slides to the checked item (the Ark `SegmentGroup`
// indicator; distinct from the per-item `Indicator` dot above).
// Decorative (aria-hidden); soma OWNS its position (measures the
// checked item, writes `--indicator-*`), eidos SHAPES it. Only
// checked item, writes `--_radio-group-indicator-*`), eidos SHAPES it. Only
// meaningful under `[data-variant='segmented']`; the eidos wrapper
// auto-renders it there, so the consumer never composes it by hand.
name: 'SelectionIndicator',

@ -92,8 +92,7 @@ export class RadioGroupProvider {
id: opts.id,
ref: opts.ref,
owner: this,
context: RadioGroupProvider.ctx,
syncAttrs: true
context: RadioGroupProvider.ctx
});
}
@ -218,8 +217,7 @@ export class RadioItemProvider {
orientation: () => this.provider.opts.orientation.current
},
owner: this,
context: RadioItemProvider.ctx,
syncAttrs: true
context: RadioItemProvider.ctx
});
}
@ -306,8 +304,7 @@ export class RadioGroupIndicatorProvider {
states: {
checked: () => this.item.isChecked
},
owner: this,
syncAttrs: true
owner: this
});
}
@ -342,16 +339,17 @@ export class RadioGroupSelectionIndicatorProvider {
this.runtimePart = this.provider.runtime.part('selection-indicator', {
id: opts.id,
ref: opts.ref,
owner: this,
syncAttrs: true
owner: this
});
// Soma owns the pill's position (the "soma positions, eidos shapes"
// contract): it tracks the checked item and exposes its offset rect as
// `--indicator-*`. The recipe positions + shapes the pill from those
// vars via the shared `lib/sliding-indicator.css`.
// `--_radio-group-indicator-*`. The recipe positions + shapes the pill
// from those vars via the shared `lib/sliding-indicator.css`, which reads
// them through the `--_indicator-*` alias the recipe declares.
this.indicator = new MeasuredIndicator({
dom: this.provider.soma.dom,
component: radioGroupMorfo.kebab,
root: () => this.provider.opts.ref.current ?? null,
active: () => {
const value = this.provider.opts.value.current;
@ -389,8 +387,7 @@ export class RadioGroupHiddenInputProvider {
this.runtimePart = this.item.provider.runtime.part('hidden-input', {
id: opts.id,
ref: opts.ref,
owner: this,
syncAttrs: true
owner: this
});
}

@ -95,8 +95,7 @@ export class TabsProvider {
id: opts.id,
ref: opts.ref,
owner: this,
context: TabsProvider.ctx,
syncAttrs: true
context: TabsProvider.ctx
});
}
@ -204,8 +203,7 @@ export class TabsListProvider {
ariaLabel: () => this.opts.ariaLabel.current,
ariaLabelledby: () => this.opts.ariaLabelledby.current
},
owner: this,
syncAttrs: true
owner: this
});
}
@ -251,8 +249,7 @@ export class TabsTriggerProvider {
parts: {
content: () => this.provider.contentIds[this.opts.value.current] || undefined
},
owner: this,
syncAttrs: true
owner: this
});
this.provider.triggerIds[opts.value.current] = opts.id.current;
@ -376,8 +373,7 @@ export class TabsContentProvider {
parts: {
trigger: () => this.provider.triggerIds[this.opts.value.current] || undefined
},
owner: this,
syncAttrs: true
owner: this
});
this.provider.contentIds[opts.value.current] = opts.id.current;
@ -420,7 +416,7 @@ interface TabsIndicatorOpts extends WithRefOpts {}
* Indicator part — an absolutely-positioned element that slides between
* active triggers, on the shared "soma positions, eidos shapes" contract:
* soma measures the active trigger and exposes its offset rect as
* `--indicator-*`; the recipe positions + shapes from those vars.
* `--_tabs-indicator-*`; the recipe positions + shapes from those vars.
*
* The measurement used to live in the eidos wrapper with its own rAF and
* observers. It moved here because that duplicated `MeasuredIndicator` — the
@ -447,12 +443,12 @@ export class TabsIndicatorProvider {
props: {
orientation: () => this.provider.opts.orientation.current
},
owner: this,
syncAttrs: true
owner: this
});
this.indicator = new MeasuredIndicator({
dom: this.provider.soma.dom,
component: tabsMorfo.kebab,
// The indicator parents itself to the List — that is the contract the
// recipe already relies on for its absolute positioning.
root: () => this.opts.ref.current?.parentElement ?? null,

@ -17,6 +17,15 @@ export interface MeasuredIndicatorRect {
export interface MeasuredIndicatorOpts {
/** The scope's ActiveDom (`soma.dom`). */
dom: ActiveDom;
/**
* The HOST component's kebab — pass `xxxMorfo.kebab`, never a literal, so the
* channel's spelling follows the morfo declaration that owns it.
*
* The layer is the PEN, not the owner: the rect it publishes is a fact of the
* host's instance, so the custom properties land in the HOST's namespace
* (`--_{kebab}-indicator-*`). See {@link MeasuredIndicator} for why.
*/
component: string;
/** The container the indicator positions within (list / group root). */
root: Getter<HTMLElement | null>;
/**
@ -45,11 +54,22 @@ export interface MeasuredIndicatorOpts {
* A sliding indicator's measurement engine — soma's half of the
* "soma positions, eidos shapes" contract (navigation-menu README §Indicator).
*
* Given the container and the active item, it exposes the item's offset rect
* as `--indicator-{x,y,w,h}` custom properties. The recipe POSITIONS + SHAPES
* from those vars (`translate: var(--indicator-x) var(--indicator-y)`, and
* `width`/`height` or a per-variant override) — the layer never inline-styles a
* base position, so there is no double-offset footgun.
* Given the container and the active item, it exposes the item's offset rect as
* `--_{component}-indicator-{x,y,w,h}` custom properties — the HOST's namespace,
* not the layer's. The rect is a fact of the host's instance; the layer only
* holds the pen. A bare `--indicator-*` (what this wrote until 2026-08-26) is a
* PUBLIC-shaped name owned by nobody: the closed-namespace law only audits
* `--{c}-*`, so it escaped every guard while reading as theming surface. It is
* not one — it is a VALUE CHANNEL (the census class of the same name), and a
* theme that pinned it would break the slide.
*
* The recipe POSITIONS + SHAPES from those vars (`translate:
* var(--_tabs-indicator-x) var(--_tabs-indicator-y)`, and `width`/`height` or a
* per-variant override) — the layer never inline-styles a base position, so
* there is no double-offset footgun. A SHARED recipe (`lib/sliding-indicator.css`)
* cannot spell the host's name, so it reads a local alias each consumer declares
* (`--_indicator-x: var(--_radio-group-indicator-x)`) — the palette bridge's
* wiring, contract in the artifact and greppable.
*
* Layout reads run through `dom.measure` (the coalesced post-layout read queue),
* so a measure never forces a synchronous reflow mid-turn. This is the shared
@ -62,6 +82,9 @@ export interface MeasuredIndicatorOpts {
* a menu is OPEN — opening IS the measure trigger, so it cannot go stale.
*/
export class MeasuredIndicator {
/** The host component's kebab — the namespace the channel is written into. */
private readonly component: string;
/** The active item's offset rect, or `null` before the first measure. */
rect = $state<MeasuredIndicatorRect | null>(null);
@ -69,23 +92,25 @@ export class MeasuredIndicator {
readonly ready = $derived(this.rect !== null);
/**
* The `--indicator-*` custom-property map to spread onto the indicator
* element's style. `undefined` before the first measure (the recipe keeps
* the element at `opacity: 0`).
* The `--_{component}-indicator-*` custom-property map to spread onto the
* indicator element's style. `undefined` before the first measure (the recipe
* keeps the element at `opacity: 0`).
*/
readonly vars = $derived.by<Record<string, string> | undefined>(() => {
const r = this.rect;
if (!r) return undefined;
const c = `--_${this.component}-indicator-`;
return {
'--indicator-x': `${r.x}px`,
'--indicator-y': `${r.y}px`,
'--indicator-w': `${r.w}px`,
'--indicator-h': `${r.h}px`
[`${c}x`]: `${r.x}px`,
[`${c}y`]: `${r.y}px`,
[`${c}w`]: `${r.w}px`,
[`${c}h`]: `${r.h}px`
};
});
constructor(opts: MeasuredIndicatorOpts) {
const { dom, root, active, dir } = opts;
const { dom, component, root, active, dir } = opts;
this.component = component;
$effect(() => {
const rootEl = root();

Loading…
Cancel
Save

Powered by TurnKey Linux.