# CONTINUE — el eje de dirección (RTL/LTR)
docs(direction): cierre del barrido de graficas, la regla de RTL en SVG, y la cola para manana
LAS 11 GRAFICAS DEL CATALOGO, REVISADAS. Ninguna de las siete que quedaban
necesitaba cambios, y por razones distintas que conviene dejar escritas:
bar-list ya lo resuelve con CSS logico; sparkline y bubble-chart heredan el
frame de <Chart> arreglado en 55070c688; y gauge, pie-chart, polar-area y
smith-chart son RADIALES — sus posiciones son angulos, no tienen eje de lectura
y es correcto que no espejen.
LA REGLA, ahora en la fila "RTL · SVG" del contrato de construccion y
desarrollada en eidos/components/chart/README.md §Direction. Son DOS decisiones
y no son la misma:
1) ¿espeja la composicion? solo si hay EJE DE LECTURA. Se invierte el rango de
pixeles de la escala, o se refleja la x. Los valores de Y no se espejan
nunca — un grafico es una secuencia leida como lee el lector, no un espejo.
2) `text-anchor` es LOGICO: si la composicion espeja se deja tal cual (voltear
ademas lo cancela); si no espeja se fuerza el fisico.
TRAMPAS DE MEDICION que me engañaron durante toda la sesion, en §7.3: el bbox de
un path espejado es identico al del original —mi "fingerprint" declaro sparkline
sin espejar cuando si lo hacia—; un test de "se sale del SVG" no ve una invasion
hacia DENTRO; el hueco del eje Y hay que medirlo contra el TICK y no contra la
linea; verificar en LTR no revela NADA de un defecto de RTL; y la pagina
"heatmap" tiene dos modos tras un control, asi que capturar el primer svg solo
enseñaba uno.
HANDOFF. La cabecera apunta a la §8 nueva, que es la cola priorizada: scroll-area
(dos señales, sin medir, y le falta una demo con eje horizontal), los agujeros de
OBSERVABILIDAD que impiden verificar drawer y float-panel, las verificaciones
pendientes, y la deuda ajena por gravedad —encabezada por los 91 ficheros de test
que falsifican Soma.require()—. Y una §8.5 con lo que NO hay que rehacer, para
que nadie repita el trabajo medido.
check 74 = linea base exacta.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
> **Kickoff**: _"Lee `docs/process/CONTINUE-direction.md` y sigue por §8."_
> **Fecha**: 2026-08-03 · Rama `alpha-0.1-dir-prefs` (sale de `alpha-0.1-sec-dom`).
> **§1, §6 y §7 son lo cerrado — no lo rehagas.** Lo abierto está en §2 (lo que
> sobrevive), §3 (ajeno al eje) y **§8, que es la cola priorizada para hoy**.
>
> Hermano de este documento: [`CONTINUE-player-rtl.md`](./CONTINUE-player-rtl.md),
> cuyo §1 cerró este hilo. Sus §2– §5 (disposición de botones del player, iconos de
> seek, escenario mixto, `Captions`, waveform-como-scrubber) siguen abiertos y son
> de otro asunto.
---
## 1. Lo cerrado — 3 commits, verificados
| Commit | Qué |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `013ceac57` | La raíz: `getDir()` reactivo · estampado condicional · `activeDir()` único · 99 ficheros |
| `5469e05df` | 49 demos + arnés `SystemAxes` a `auto·ltr·rtl` · prosa declarada inglesa · 51 ficheros |
| `538f4932b` | El handoff anterior decía que esto seguía roto |
docs(direction): cierre del barrido de graficas, la regla de RTL en SVG, y la cola para manana
LAS 11 GRAFICAS DEL CATALOGO, REVISADAS. Ninguna de las siete que quedaban
necesitaba cambios, y por razones distintas que conviene dejar escritas:
bar-list ya lo resuelve con CSS logico; sparkline y bubble-chart heredan el
frame de <Chart> arreglado en 55070c688; y gauge, pie-chart, polar-area y
smith-chart son RADIALES — sus posiciones son angulos, no tienen eje de lectura
y es correcto que no espejen.
LA REGLA, ahora en la fila "RTL · SVG" del contrato de construccion y
desarrollada en eidos/components/chart/README.md §Direction. Son DOS decisiones
y no son la misma:
1) ¿espeja la composicion? solo si hay EJE DE LECTURA. Se invierte el rango de
pixeles de la escala, o se refleja la x. Los valores de Y no se espejan
nunca — un grafico es una secuencia leida como lee el lector, no un espejo.
2) `text-anchor` es LOGICO: si la composicion espeja se deja tal cual (voltear
ademas lo cancela); si no espeja se fuerza el fisico.
TRAMPAS DE MEDICION que me engañaron durante toda la sesion, en §7.3: el bbox de
un path espejado es identico al del original —mi "fingerprint" declaro sparkline
sin espejar cuando si lo hacia—; un test de "se sale del SVG" no ve una invasion
hacia DENTRO; el hueco del eje Y hay que medirlo contra el TICK y no contra la
linea; verificar en LTR no revela NADA de un defecto de RTL; y la pagina
"heatmap" tiene dos modos tras un control, asi que capturar el primer svg solo
enseñaba uno.
HANDOFF. La cabecera apunta a la §8 nueva, que es la cola priorizada: scroll-area
(dos señales, sin medir, y le falta una demo con eje horizontal), los agujeros de
OBSERVABILIDAD que impiden verificar drawer y float-panel, las verificaciones
pendientes, y la deuda ajena por gravedad —encabezada por los 91 ficheros de test
que falsifican Soma.require()—. Y una §8.5 con lo que NO hay que rehacer, para
que nadie repita el trabajo medido.
check 74 = linea base exacta.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| `ec533845d` …`714a1ddb5` | ** §2.1 + §2.2 cerradas** (§6): guard RTL-1 · 23 hallazgos · los 12 `[dir='rtl']` → `:dir()` · el pin del `<main>` · tabs · virtualizadores · float-panel · rating-group · splitter · carousel |
| `8086f21d6` …`01b891b64` | **Las gráficas** (§7): 6 defectos de RTL en el chart + funnel, calendar-heatmap, heatmap matriz, radar, y los bloques de código |
Empezó como «el slider no responde al RTL» y eran **dos defectos independientes** :
**El del slider** — `slider.css` emparejaba `inset-inline-start: 50%` (lógico) con
`translateX(-50%)` (**físico, no se voltea**) en las tres reglas verticales. Medido
en Chrome: raíz/pulgar/ticks en x=961, raíl y relleno en x=955. El `Tick` se libraba
porque ya usaba `margin-inline-start` — ése era el idioma correcto del propio
fichero. Mismo defecto en `accordion.css` (`text-align: left`), que era el ** único**
`text-align` físico de todo eidos.
**La causa raíz, que no estaba en ninguna hipótesis** — `makeDimension().get()`
(`arts/prefs/active-prefs.svelte.ts`) leía `engine.snapshot()` esquivando la celda
`$state` . Así que `soma.prefs.getDir()` era una **lectura sin tracking** y los ~64
componentes que resuelven su dirección desde prefs la **congelaban al montarse** .
La proyección DOM se salvaba porque usa `onChange` : misma dimensión, dos caminos de
lectura, uno solo reactivo.
**Y el estampado** — 33 de 37 componentes escribían `dir` con un valor SIEMPRE
concreto, así que una app que ponga `<html dir="rtl">` sin registrar la preferencia
se encontraba 33 islas del revés. Ahora el atributo se omite cuando nadie afirmó
nada. Es el modelo de **Zag** (`prop("dir")`), aplicado uniforme — Zag lo incumple
en 12 de sus 51 máquinas.
### El contrato, ratificado por el usuario
```
prop dir → soma.prefs.getDir() → 'ltr'
```
**SIN paso del padre.** El `dir` del DOM es **proyección, no fuente**
(`docs/architecture/active-architecture.md:407`). La herencia que se obtiene ahora
la hace el navegador por **ausencia** de atributo, no por ninguna lectura del DOM.
Fase posterior que el usuario dejó planteada, sin fecha: **qué propiedades podrían
heredarse implícitamente del padre** y si beneficia al framework.
### La forma única, para no volver a divergir
Había **diez** formas de resolver lo mismo en los wrappers y **cinco** de declarar
el tipo, con un cuarto escalón semántico —heredar del menú padre— enterrado en dos
expresiones sueltas. Ahora:
```ts
// wrapper — idéntico en los 36
dir: activeDir(() => dir, soma)
// el escalón extra se declara donde se ve
dir: activeDir(() => dir ?? parentMenu?.opts.dir.current, soma)
// provider — dos valores, nunca uno
readonly resolvedDir = $derived.by(() => this.opts.dir.current ?? 'ltr'); // la matemática
dir: this.opts.dir.current // el atributo, CRUDO
```
`src/uix/soma/direction.ts` documenta el porqué. **La regla que hay que defender:
el atributo NUNCA tiene valor por defecto.**
### Guards que nacieron en rojo
- `src/arts/prefs/test/active-prefs-reactivity.svelte.test.ts` — intent · derivación
desde el idioma · **granularidad por clave** (el negativo: un commit de `accent` NO
debe despertar al lector de `direction` ; con una celda global se pondría rojo).
- `src/uix/active-uix/test/prefs-view.svelte.test.ts` — `getDir()` distingue
«nadie afirmó» de «ltr».
⚠️ **Los dos DEBEN seguir siendo `*.svelte.test.ts`.** El proyecto `server` de vitest
compila el `$state` fuera (transform de servidor de Svelte), así que un `.test.ts`
ahí pasaría hiciera lo que hiciera el código.
---
## 2. Lo que queda — por orden de valor
feat(direction): un guard para el eje logico-fisico, y el <main> que congelaba las 51 demos
Cierra la §2.1 del handoff. El guard nacio en rojo con 24 hallazgos y al
arreglarlos aparecieron dos defectos mas grandes que el que buscaba.
1) EL GUARD RTL-1. Nada cruzaba un ancla LOGICA con un desplazamiento FISICO
en la misma regla CSS: eidos-lint clasifica SELECTORES, no declaraciones, y la
fila RTL del contrato de construccion decia "rule (LIVE)" — revision humana.
Por eso el defecto del slider llego a produccion y lo cazo el ojo del usuario.
Ahora `npm run rtl:check`, con la logica en src/uix/eidos/rtl-lint.ts (14
tests) para que sea testeable, calcando el par lint.ts / scripts/eidos-lint.ts.
El test ancla el caso historico: slider.css ANTES de 013ceac57 sale rojo y el
arreglo que se commiteo sale verde.
Dos desviaciones del enunciado, deliberadas. AMPLIA a la propiedad
independiente `translate:`, que se usa MAS que `transform: translate*` (87
frente a 36) y tiene el mismo defecto — ocho de los 24 hallazgos eran de esa
forma. RECORTA `padding-inline*`: no mueve la caja contra su ancla, asi que no
puede pelear con un translate; solo anadia ruido.
2) LA COSECHA, 24 -> 1. El unico que queda es palabras, auditado y no tocado.
Los 23 caen en tres familias: CENTRADO (10) donde el resultado deseado es
simetrico y basta dejar de descentrar; DIRECCIONAL (6) donde el signo debe
voltear; y FISICO (7) donde lo que sobraba era el ancla logica — handles de
brujula del cropper sobre una imagen que no se voltea, y el arco polar del
menu-dial. Medido en Chrome el switch: el thumb salia a +24px en un carril de
38px, colgando fuera. Verificados tambien carousel, avatar, cropper, menu-dial
y chat-log.
3) :dir() ES LA DOCTRINA, [dir='rtl'] QUEDA PROHIBIDO. Los 12 selectores que
habia en eidos estaban TODOS rotos, en los dos sentidos opuestos del mismo
error, y ambos medidos en Chrome. Los 5 descendientes se aplican DE MAS: el
sidebar tenia direction:ltr de verdad y aun asi matcheaba, porque el selector
capta cualquier ancestro RTL e ignora un dir mas cercano que redeclare. Los 7
de mismo-elemento NO se aplican NUNCA: el tree-view era rtl de verdad y no
matcheaba, porque desde 013ceac57 el atributo solo se estampa si alguien lo
afirmo. :dir() acierta los tres casos porque mira la direccion RESUELTA.
Baseline desde 2023.
4) EL PIN DEL <main>. 5469e05df hizo dos cosas que se anulan: puso las demos en
`auto` —sin prop dir, o sea el componente no estampa atributo y HEREDA del
DOM— y a la vez clavo <main data-uix-canvas dir="ltr"> sobre ellas. Ese <main>
era la herencia, asi que el toggle del topbar y el arabe llegaban a <html> y
morian ahi: las 51 demos congeladas en LTR. Su comentario afirmaba que "los
componentes siguen las prefs, este contenedor no les afecta" — cierto para la
LOGICA, falso para el CSS, que lee el direction COMPUTADO. Medido: html
dir="rtl" con el panel del sidebar todavia a la izquierda, y quitar ese
atributo y nada mas lo movia. Se queda el lang="en". Coste aceptado por el
usuario: vuelve la puntuacion desplazada en la prosa inglesa al elegir RTL, que
es cosmetico y solo en un modo de inspeccion; demos que no pueden demostrar,
no. Si hay que fijar la prosa otra vez, el pin va alrededor de la PROSA.
Verificado: check 74 = linea base exacta en las tres pasadas. rtl-lint 14/14.
Suite de eidos 375 pasan; el fallo de lint.test.ts por audio-player es
PREEXISTENTE, comprobado con stash, y es del hilo del sonido. Los 9 CSS tocados
ya fallaban prettier antes de tocarlos, asi que no se formatean.
SIN VERIFICAR EN NAVEGADOR, anotado en el handoff: archetypes y proof-of-human
(sus reglas viven en @media (pointer: coarse), invisible en escritorio) y
sliding-indicator (la demo del radio-group segmented no llegaba a medir).
QUEDA: hay wrappers estampando dir="" vacio en vez de omitir el atributo —
visto en sidebar y tree-view; no rompe porque un valor invalido hereda, pero
contradice la letra del contrato.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### 2.1 · El guard — CERRADA (ver §6)
### 2.2 · Geometría lógica en los cinco que calculan píxeles desde JS
| componente | sitios |
| --------------- | --------------------------------------------- |
| `slider` | 6 |
| `carousel` | 2 (signo del `translate3d` + signo del swipe) |
| `number-field` | 2 (signo del arrastre) |
| `css-field` | 2 (signo del arrastre) |
| `dropdown-menu` | colocación flotante |
**Funciona hoy** — no es un bug abierto. Pero es lo que convierte un `dir`
equivocado en catástrofe en vez de en detalle cosmético, y la fila RTL del contrato
manda lógicas para el flujo (`left`/`right` físicos sólo como API de _placement_
flotante, excepción EID-3). Migrar el PINTADO a `inset-inline-*` deja el JS con
`dir` sólo para el signo del gesto y las flechas.
**De uno en uno y con verificación en navegador.** Toca comportamiento ya probado.
feat(direction): un guard para el eje logico-fisico, y el <main> que congelaba las 51 demos
Cierra la §2.1 del handoff. El guard nacio en rojo con 24 hallazgos y al
arreglarlos aparecieron dos defectos mas grandes que el que buscaba.
1) EL GUARD RTL-1. Nada cruzaba un ancla LOGICA con un desplazamiento FISICO
en la misma regla CSS: eidos-lint clasifica SELECTORES, no declaraciones, y la
fila RTL del contrato de construccion decia "rule (LIVE)" — revision humana.
Por eso el defecto del slider llego a produccion y lo cazo el ojo del usuario.
Ahora `npm run rtl:check`, con la logica en src/uix/eidos/rtl-lint.ts (14
tests) para que sea testeable, calcando el par lint.ts / scripts/eidos-lint.ts.
El test ancla el caso historico: slider.css ANTES de 013ceac57 sale rojo y el
arreglo que se commiteo sale verde.
Dos desviaciones del enunciado, deliberadas. AMPLIA a la propiedad
independiente `translate:`, que se usa MAS que `transform: translate*` (87
frente a 36) y tiene el mismo defecto — ocho de los 24 hallazgos eran de esa
forma. RECORTA `padding-inline*`: no mueve la caja contra su ancla, asi que no
puede pelear con un translate; solo anadia ruido.
2) LA COSECHA, 24 -> 1. El unico que queda es palabras, auditado y no tocado.
Los 23 caen en tres familias: CENTRADO (10) donde el resultado deseado es
simetrico y basta dejar de descentrar; DIRECCIONAL (6) donde el signo debe
voltear; y FISICO (7) donde lo que sobraba era el ancla logica — handles de
brujula del cropper sobre una imagen que no se voltea, y el arco polar del
menu-dial. Medido en Chrome el switch: el thumb salia a +24px en un carril de
38px, colgando fuera. Verificados tambien carousel, avatar, cropper, menu-dial
y chat-log.
3) :dir() ES LA DOCTRINA, [dir='rtl'] QUEDA PROHIBIDO. Los 12 selectores que
habia en eidos estaban TODOS rotos, en los dos sentidos opuestos del mismo
error, y ambos medidos en Chrome. Los 5 descendientes se aplican DE MAS: el
sidebar tenia direction:ltr de verdad y aun asi matcheaba, porque el selector
capta cualquier ancestro RTL e ignora un dir mas cercano que redeclare. Los 7
de mismo-elemento NO se aplican NUNCA: el tree-view era rtl de verdad y no
matcheaba, porque desde 013ceac57 el atributo solo se estampa si alguien lo
afirmo. :dir() acierta los tres casos porque mira la direccion RESUELTA.
Baseline desde 2023.
4) EL PIN DEL <main>. 5469e05df hizo dos cosas que se anulan: puso las demos en
`auto` —sin prop dir, o sea el componente no estampa atributo y HEREDA del
DOM— y a la vez clavo <main data-uix-canvas dir="ltr"> sobre ellas. Ese <main>
era la herencia, asi que el toggle del topbar y el arabe llegaban a <html> y
morian ahi: las 51 demos congeladas en LTR. Su comentario afirmaba que "los
componentes siguen las prefs, este contenedor no les afecta" — cierto para la
LOGICA, falso para el CSS, que lee el direction COMPUTADO. Medido: html
dir="rtl" con el panel del sidebar todavia a la izquierda, y quitar ese
atributo y nada mas lo movia. Se queda el lang="en". Coste aceptado por el
usuario: vuelve la puntuacion desplazada en la prosa inglesa al elegir RTL, que
es cosmetico y solo en un modo de inspeccion; demos que no pueden demostrar,
no. Si hay que fijar la prosa otra vez, el pin va alrededor de la PROSA.
Verificado: check 74 = linea base exacta en las tres pasadas. rtl-lint 14/14.
Suite de eidos 375 pasan; el fallo de lint.test.ts por audio-player es
PREEXISTENTE, comprobado con stash, y es del hilo del sonido. Los 9 CSS tocados
ya fallaban prettier antes de tocarlos, asi que no se formatean.
SIN VERIFICAR EN NAVEGADOR, anotado en el handoff: archetypes y proof-of-human
(sus reglas viven en @media (pointer: coarse), invisible en escritorio) y
sliding-indicator (la demo del radio-group segmented no llegaba a medir).
QUEDA: hay wrappers estampando dir="" vacio en vez de omitir el atributo —
visto en sidebar y tree-view; no rompe porque un valor invalido hereda, pero
contradice la letra del contrato.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
⚠️ **Matiz que salió al cerrar §2.1** : el `sliding-indicator` era el mismo defecto y
se resolvió al revés — soma mide `itemRect.left - rootRect.left` , una coordenada
FÍSICA, así que el arreglo fue hacer FÍSICA el ancla (`left: 0`), no lógico el
pintado. Antes de migrar cada sitio, mira de qué lado está la medida: si el JS
mide en físico, lo coherente es el ancla física. Lo mismo vale para el arco polar
del `menu-dial` y los handles de brújula del `cropper` .
### 2.3 · El `secondary-range` vertical en RTL no lo ha visto nadie
Arreglado por simetría con el `range` (misma regla, mismo cambio), y el `range` sí
se midió. Pero **la combinación vertical + RTL + `secondaryValue` no es observable** :
la única demo que expone `secondaryValue` es la del **waveform** , y es horizontal.
O se cablea `secondaryValue` como control en la demo del slider, o se declara como
verificado-por-construcción y no por vista.
feat(direction): un guard para el eje logico-fisico, y el <main> que congelaba las 51 demos
Cierra la §2.1 del handoff. El guard nacio en rojo con 24 hallazgos y al
arreglarlos aparecieron dos defectos mas grandes que el que buscaba.
1) EL GUARD RTL-1. Nada cruzaba un ancla LOGICA con un desplazamiento FISICO
en la misma regla CSS: eidos-lint clasifica SELECTORES, no declaraciones, y la
fila RTL del contrato de construccion decia "rule (LIVE)" — revision humana.
Por eso el defecto del slider llego a produccion y lo cazo el ojo del usuario.
Ahora `npm run rtl:check`, con la logica en src/uix/eidos/rtl-lint.ts (14
tests) para que sea testeable, calcando el par lint.ts / scripts/eidos-lint.ts.
El test ancla el caso historico: slider.css ANTES de 013ceac57 sale rojo y el
arreglo que se commiteo sale verde.
Dos desviaciones del enunciado, deliberadas. AMPLIA a la propiedad
independiente `translate:`, que se usa MAS que `transform: translate*` (87
frente a 36) y tiene el mismo defecto — ocho de los 24 hallazgos eran de esa
forma. RECORTA `padding-inline*`: no mueve la caja contra su ancla, asi que no
puede pelear con un translate; solo anadia ruido.
2) LA COSECHA, 24 -> 1. El unico que queda es palabras, auditado y no tocado.
Los 23 caen en tres familias: CENTRADO (10) donde el resultado deseado es
simetrico y basta dejar de descentrar; DIRECCIONAL (6) donde el signo debe
voltear; y FISICO (7) donde lo que sobraba era el ancla logica — handles de
brujula del cropper sobre una imagen que no se voltea, y el arco polar del
menu-dial. Medido en Chrome el switch: el thumb salia a +24px en un carril de
38px, colgando fuera. Verificados tambien carousel, avatar, cropper, menu-dial
y chat-log.
3) :dir() ES LA DOCTRINA, [dir='rtl'] QUEDA PROHIBIDO. Los 12 selectores que
habia en eidos estaban TODOS rotos, en los dos sentidos opuestos del mismo
error, y ambos medidos en Chrome. Los 5 descendientes se aplican DE MAS: el
sidebar tenia direction:ltr de verdad y aun asi matcheaba, porque el selector
capta cualquier ancestro RTL e ignora un dir mas cercano que redeclare. Los 7
de mismo-elemento NO se aplican NUNCA: el tree-view era rtl de verdad y no
matcheaba, porque desde 013ceac57 el atributo solo se estampa si alguien lo
afirmo. :dir() acierta los tres casos porque mira la direccion RESUELTA.
Baseline desde 2023.
4) EL PIN DEL <main>. 5469e05df hizo dos cosas que se anulan: puso las demos en
`auto` —sin prop dir, o sea el componente no estampa atributo y HEREDA del
DOM— y a la vez clavo <main data-uix-canvas dir="ltr"> sobre ellas. Ese <main>
era la herencia, asi que el toggle del topbar y el arabe llegaban a <html> y
morian ahi: las 51 demos congeladas en LTR. Su comentario afirmaba que "los
componentes siguen las prefs, este contenedor no les afecta" — cierto para la
LOGICA, falso para el CSS, que lee el direction COMPUTADO. Medido: html
dir="rtl" con el panel del sidebar todavia a la izquierda, y quitar ese
atributo y nada mas lo movia. Se queda el lang="en". Coste aceptado por el
usuario: vuelve la puntuacion desplazada en la prosa inglesa al elegir RTL, que
es cosmetico y solo en un modo de inspeccion; demos que no pueden demostrar,
no. Si hay que fijar la prosa otra vez, el pin va alrededor de la PROSA.
Verificado: check 74 = linea base exacta en las tres pasadas. rtl-lint 14/14.
Suite de eidos 375 pasan; el fallo de lint.test.ts por audio-player es
PREEXISTENTE, comprobado con stash, y es del hilo del sonido. Los 9 CSS tocados
ya fallaban prettier antes de tocarlos, asi que no se formatean.
SIN VERIFICAR EN NAVEGADOR, anotado en el handoff: archetypes y proof-of-human
(sus reglas viven en @media (pointer: coarse), invisible en escritorio) y
sliding-indicator (la demo del radio-group segmented no llegaba a medir).
QUEDA: hay wrappers estampando dir="" vacio en vez de omitir el atributo —
visto en sidebar y tree-view; no rompe porque un valor invalido hereda, pero
contradice la letra del contrato.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
✅ **La mitad «no observable» ya no lo es** : el pin del `<main>` (§6.4) era lo que
impedía ver RTL en CUALQUIER demo. Ahora el toggle del topbar y el árabe llegan al
componente. Queda sólo cablear `secondaryValue` en la demo del slider.
### 2.4 · `parts[2]` en la demo del slider
`web/routes/uix/components/slider/+page.svelte:409` indexa `SecondaryRange` en vez
de `Thumb` — la tabla de teclado sale vacía y es el ** único** error de tipos del
fichero. Preexistente (venía de cuando `SecondaryRange` se insertó en el índice 2);
señalado y no tocado por ser ajeno al eje.
---
## 3. Encontrado por el camino, NO de este hilo
- **`html lang` no sigue al idioma.** El `dir` sí se proyecta; el `lang` se queda en
`en` con árabe seleccionado. Afecta a selección de fuentes, corte de palabras y
lectores de pantalla.
- **`perm:check` sin ejecutar.** El topbar ya lleva `data-perm-step="90"` , lo que
hace que **toda** ruta bajo el layout tenga paso (antes saltaba 158). La pasada se
vuelve mucho más larga, y como el runner usa **un solo contexto de navegador** , la
dirección persiste en `localStorage` y las rutas alternan ltr/rtl.
- **91 ficheros de test falsifican `Soma.require()` ** con `vi.spyOn(Soma, 'require')
.mockReturnValue({ prefs: { getDir: () => dir } } as unknown as Soma)`. Incumple
una Regla Crítica de CLAUDE.md y significa que esa parte de la batería **no
ejercita el código real**. Es otro proyecto entero, pero es el hallazgo más grave
de la lista.
- **El `?` desplazado dentro de un componente RTL no es arreglable desde el
framework.** Es el algoritmo bidi de Unicode sobre texto inglés en un párrafo RTL:
el `?` es neutro y al final de la tirada adopta la dirección del párrafo. El
componente hace bien en estar en `rtl` . Se cierra **traduciendo el contenido de
las demos** o marcándolo — que es exactamente la parte que las cinco librerías de
referencia dejan al consumidor.
---
## 4. Investigación de referencias — hecha, no la repitas
25 agentes, 13 hallazgos confirmados, **5 tumbados** por refutación con la fuente
delante.
| | ¿estampa `dir` ? | cómo resuelve |
| -------------- | ------------------------- | ------------------------------------------------------------------------------ |
| **MUI** | nunca | tema + contexto `useRtl` ; voltea CSS físico con stylis |
| **react-aria** | nunca (salvo portales) | del _locale_ por contexto; te manda escribir `<div lang dir>` en tu raíz |
| **bits-ui** | no verificado que escriba | prop con default duro `'ltr'` + `getComputedStyle().direction` en roving-focus |
| **Radix** | **siempre** | prop → contexto → `'ltr'` ; sin salida → su `discussions/1405` |
| **Zag/Ark** | **46/51 máquinas** | prop → contexto, **valor `prop("dir")`: ausente si nadie lo pidió** |
**Ninguna** de las cinco emite `dir="auto"` , `<bdi>` ni `unicode-bidi` .
Dos datos que costaron trabajo y conviene no volver a averiguar:
- **`unicode-bidi: isolate` NO arregla el `?` .** Aísla la tirada respecto a sus
hermanas, pero no cambia su dirección base, y el `?` está _dentro_ de la tirada.
- **`dir="ltr"` sí, y de paso aísla.** Verificado en la hoja de estilos de agente del
WHATWG: `… bdi, output, [dir=ltr i], [dir=rtl i], [dir=auto i] { unicode-bidi:
isolate; }`. Un atributo, dos efectos.
- **`dir="auto"` es la herramienta equivocada** cuando SÍ se sabe la dirección: mira
sólo el primer carácter fuerte, la spec llama a la heurística _"very crude"_ y el
W3C documenta que falla justo en esta clase de texto.
Excepción correcta que se queda: `chat-message.css:156` usa `unicode-bidi: plaintext`
porque el texto de un mensaje es de dirección **incognoscible al escribir** . Ése es
el discriminador de la doctrina: **auto-detectar sólo donde no se puede saber;
donde se sabe, declararlo en el sitio que lo sabe.**
---
## 5. Trampas de método — me costaron horas, léelas
- **Ensancha el TIPO primero.** Al pasar `dir: Direction` → `Direction | undefined`
en las opts, cada sitio de LÓGICA que asumía un valor concreto se volvió error de
compilación y el compilador enumeró el trabajo. **Pero no cubre la otra mitad** :
`dir: undefined` es un valor de atributo válido, así que los estampados compilan
pasara lo que pasara. Esa mitad se verifica **contando** (`grep -c` de crudo vs
resuelto por fichero), no confiando.
- **Los barridos con regex mintieron cuatro veces de cuatro**: `resolvedDir`
autorreferencial en 17 ficheros (el regex reescribió el cuerpo de la propia
declaración), duplicados en 3, campo insertado en 3 clases sin `dir` , y
`activeDir(valor)` en vez de `activeDir(() => valor)` en 10 wrappers.
- **Anclaje de clase que falla**: `^export class \w+Provider \{$` exige que la línea
TERMINE en `{` , así que se salta `class X implements Y {` y aterriza en la clase
siguiente. Usa `^export class \w+Provider\b[^\n]*\{$` .
- **Discriminante que sí generaliza** cuando un símbolo tiene dos roles: la forma
sintáctica, no el identificador. Aquí `dir: …` (clave de objeto) = estampado;
cualquier otro `.opts.dir.current` = lógica. Sobrevivió a las tres formas distintas
del catálogo.
- **NO lances `prettier --write` sobre un directorio entero.** Sobre las demos generó
**165 ficheros y +31.000 líneas** de ruido y **rompió dos atributos** normalizando
`data-perm-mode='type="X"'` a comillas dobles. Revertido y reaplicado sólo el
cambio: 49 ficheros, +357/− 161. Formatea únicamente los ficheros que tocas, y mira
el tamaño de la diff después.
- **El panel de navegador embebido no sirve** para nada visual (no compone frames).
Usar el Chrome real (`mcp__claude-in-chrome__*`).
- **`smoke` es inestable.** Con mis cambios falló en `/demos/motion` , `/temas` ,
`/uix/components/card` ; con los cambios stasheados falló en **dos rutas
distintas** (`/demos/cristal`, `/demos/heroscrolling` ). Conjuntos disjuntos ⇒ no
son regresiones. Compara siempre contra una pasada de referencia antes de culpar
a tu diff.
- ⚠️ **Rama compartida.** El usuario commiteó `42d58c394` mientras yo trabajaba.
Verifica `HEAD` antes de dar por sabido el estado.
feat(direction): un guard para el eje logico-fisico, y el <main> que congelaba las 51 demos
Cierra la §2.1 del handoff. El guard nacio en rojo con 24 hallazgos y al
arreglarlos aparecieron dos defectos mas grandes que el que buscaba.
1) EL GUARD RTL-1. Nada cruzaba un ancla LOGICA con un desplazamiento FISICO
en la misma regla CSS: eidos-lint clasifica SELECTORES, no declaraciones, y la
fila RTL del contrato de construccion decia "rule (LIVE)" — revision humana.
Por eso el defecto del slider llego a produccion y lo cazo el ojo del usuario.
Ahora `npm run rtl:check`, con la logica en src/uix/eidos/rtl-lint.ts (14
tests) para que sea testeable, calcando el par lint.ts / scripts/eidos-lint.ts.
El test ancla el caso historico: slider.css ANTES de 013ceac57 sale rojo y el
arreglo que se commiteo sale verde.
Dos desviaciones del enunciado, deliberadas. AMPLIA a la propiedad
independiente `translate:`, que se usa MAS que `transform: translate*` (87
frente a 36) y tiene el mismo defecto — ocho de los 24 hallazgos eran de esa
forma. RECORTA `padding-inline*`: no mueve la caja contra su ancla, asi que no
puede pelear con un translate; solo anadia ruido.
2) LA COSECHA, 24 -> 1. El unico que queda es palabras, auditado y no tocado.
Los 23 caen en tres familias: CENTRADO (10) donde el resultado deseado es
simetrico y basta dejar de descentrar; DIRECCIONAL (6) donde el signo debe
voltear; y FISICO (7) donde lo que sobraba era el ancla logica — handles de
brujula del cropper sobre una imagen que no se voltea, y el arco polar del
menu-dial. Medido en Chrome el switch: el thumb salia a +24px en un carril de
38px, colgando fuera. Verificados tambien carousel, avatar, cropper, menu-dial
y chat-log.
3) :dir() ES LA DOCTRINA, [dir='rtl'] QUEDA PROHIBIDO. Los 12 selectores que
habia en eidos estaban TODOS rotos, en los dos sentidos opuestos del mismo
error, y ambos medidos en Chrome. Los 5 descendientes se aplican DE MAS: el
sidebar tenia direction:ltr de verdad y aun asi matcheaba, porque el selector
capta cualquier ancestro RTL e ignora un dir mas cercano que redeclare. Los 7
de mismo-elemento NO se aplican NUNCA: el tree-view era rtl de verdad y no
matcheaba, porque desde 013ceac57 el atributo solo se estampa si alguien lo
afirmo. :dir() acierta los tres casos porque mira la direccion RESUELTA.
Baseline desde 2023.
4) EL PIN DEL <main>. 5469e05df hizo dos cosas que se anulan: puso las demos en
`auto` —sin prop dir, o sea el componente no estampa atributo y HEREDA del
DOM— y a la vez clavo <main data-uix-canvas dir="ltr"> sobre ellas. Ese <main>
era la herencia, asi que el toggle del topbar y el arabe llegaban a <html> y
morian ahi: las 51 demos congeladas en LTR. Su comentario afirmaba que "los
componentes siguen las prefs, este contenedor no les afecta" — cierto para la
LOGICA, falso para el CSS, que lee el direction COMPUTADO. Medido: html
dir="rtl" con el panel del sidebar todavia a la izquierda, y quitar ese
atributo y nada mas lo movia. Se queda el lang="en". Coste aceptado por el
usuario: vuelve la puntuacion desplazada en la prosa inglesa al elegir RTL, que
es cosmetico y solo en un modo de inspeccion; demos que no pueden demostrar,
no. Si hay que fijar la prosa otra vez, el pin va alrededor de la PROSA.
Verificado: check 74 = linea base exacta en las tres pasadas. rtl-lint 14/14.
Suite de eidos 375 pasan; el fallo de lint.test.ts por audio-player es
PREEXISTENTE, comprobado con stash, y es del hilo del sonido. Los 9 CSS tocados
ya fallaban prettier antes de tocarlos, asi que no se formatean.
SIN VERIFICAR EN NAVEGADOR, anotado en el handoff: archetypes y proof-of-human
(sus reglas viven en @media (pointer: coarse), invisible en escritorio) y
sliding-indicator (la demo del radio-group segmented no llegaba a medir).
QUEDA: hay wrappers estampando dir="" vacio en vez de omitir el atributo —
visto en sidebar y tree-view; no rompe porque un valor invalido hereda, pero
contradice la letra del contrato.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
---
## 6. Sesión 2026-08-02 — §2.1 cerrada, y lo que arrastró
Sin commitear. `check` = **74 = línea base exacta** en las tres pasadas.
### 6.1 · El guard RTL-1
`src/uix/eidos/rtl-lint.ts` (lógica) + `rtl-lint.test.ts` (**14/14**) +
`scripts/rtl-check.ts` + `npm run rtl:check` . La fila RTL de
`docs/guides/component-guide.md` deja de decir «rule (LIVE)».
El test ancla el caso histórico: `slider.css` ANTES de `013ceac57` sale rojo, el
arreglo que se commiteó sale verde, y el eje de bloque (`inset-block-start` +
`translateY` ) no se marca.
**Dos desviaciones del enunciado de §2.1, deliberadas:**
feat(direction): un guard para el eje logico-fisico, y el <main> que congelaba las 51 demos
Cierra la §2.1 del handoff. El guard nacio en rojo con 24 hallazgos y al
arreglarlos aparecieron dos defectos mas grandes que el que buscaba.
1) EL GUARD RTL-1. Nada cruzaba un ancla LOGICA con un desplazamiento FISICO
en la misma regla CSS: eidos-lint clasifica SELECTORES, no declaraciones, y la
fila RTL del contrato de construccion decia "rule (LIVE)" — revision humana.
Por eso el defecto del slider llego a produccion y lo cazo el ojo del usuario.
Ahora `npm run rtl:check`, con la logica en src/uix/eidos/rtl-lint.ts (14
tests) para que sea testeable, calcando el par lint.ts / scripts/eidos-lint.ts.
El test ancla el caso historico: slider.css ANTES de 013ceac57 sale rojo y el
arreglo que se commiteo sale verde.
Dos desviaciones del enunciado, deliberadas. AMPLIA a la propiedad
independiente `translate:`, que se usa MAS que `transform: translate*` (87
frente a 36) y tiene el mismo defecto — ocho de los 24 hallazgos eran de esa
forma. RECORTA `padding-inline*`: no mueve la caja contra su ancla, asi que no
puede pelear con un translate; solo anadia ruido.
2) LA COSECHA, 24 -> 1. El unico que queda es palabras, auditado y no tocado.
Los 23 caen en tres familias: CENTRADO (10) donde el resultado deseado es
simetrico y basta dejar de descentrar; DIRECCIONAL (6) donde el signo debe
voltear; y FISICO (7) donde lo que sobraba era el ancla logica — handles de
brujula del cropper sobre una imagen que no se voltea, y el arco polar del
menu-dial. Medido en Chrome el switch: el thumb salia a +24px en un carril de
38px, colgando fuera. Verificados tambien carousel, avatar, cropper, menu-dial
y chat-log.
3) :dir() ES LA DOCTRINA, [dir='rtl'] QUEDA PROHIBIDO. Los 12 selectores que
habia en eidos estaban TODOS rotos, en los dos sentidos opuestos del mismo
error, y ambos medidos en Chrome. Los 5 descendientes se aplican DE MAS: el
sidebar tenia direction:ltr de verdad y aun asi matcheaba, porque el selector
capta cualquier ancestro RTL e ignora un dir mas cercano que redeclare. Los 7
de mismo-elemento NO se aplican NUNCA: el tree-view era rtl de verdad y no
matcheaba, porque desde 013ceac57 el atributo solo se estampa si alguien lo
afirmo. :dir() acierta los tres casos porque mira la direccion RESUELTA.
Baseline desde 2023.
4) EL PIN DEL <main>. 5469e05df hizo dos cosas que se anulan: puso las demos en
`auto` —sin prop dir, o sea el componente no estampa atributo y HEREDA del
DOM— y a la vez clavo <main data-uix-canvas dir="ltr"> sobre ellas. Ese <main>
era la herencia, asi que el toggle del topbar y el arabe llegaban a <html> y
morian ahi: las 51 demos congeladas en LTR. Su comentario afirmaba que "los
componentes siguen las prefs, este contenedor no les afecta" — cierto para la
LOGICA, falso para el CSS, que lee el direction COMPUTADO. Medido: html
dir="rtl" con el panel del sidebar todavia a la izquierda, y quitar ese
atributo y nada mas lo movia. Se queda el lang="en". Coste aceptado por el
usuario: vuelve la puntuacion desplazada en la prosa inglesa al elegir RTL, que
es cosmetico y solo en un modo de inspeccion; demos que no pueden demostrar,
no. Si hay que fijar la prosa otra vez, el pin va alrededor de la PROSA.
Verificado: check 74 = linea base exacta en las tres pasadas. rtl-lint 14/14.
Suite de eidos 375 pasan; el fallo de lint.test.ts por audio-player es
PREEXISTENTE, comprobado con stash, y es del hilo del sonido. Los 9 CSS tocados
ya fallaban prettier antes de tocarlos, asi que no se formatean.
SIN VERIFICAR EN NAVEGADOR, anotado en el handoff: archetypes y proof-of-human
(sus reglas viven en @media (pointer: coarse), invisible en escritorio) y
sliding-indicator (la demo del radio-group segmented no llegaba a medir).
QUEDA: hay wrappers estampando dir="" vacio en vez de omitir el atributo —
visto en sidebar y tree-view; no rompe porque un valor invalido hereda, pero
contradice la letra del contrato.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **Amplía** a la propiedad independiente `translate:` , que se usa MÁS que
`transform: translate*` (87 apariciones frente a 36) y tiene el mismo defecto.
Ocho de los 24 hallazgos eran de esa forma.
- **Recorta** `padding-inline*` : no mueve la caja contra su ancla, así que no
puede pelear con un translate. Sólo añadía ruido.
Vía de escape `/* rtl-physical: <razón> */` en la línea del translate o la de
encima. Uso legítimo: el signo se invierte en otra regla bajo `:dir(rtl)` y el
guard no puede ver esa compensación.
### 6.2 · La cosecha: 24 → 1
El único que queda es `palabras-chrome.css:446` , **auditado y no tocado** por la
regla de no escribir en palabras. Los 23 restantes cayeron en tres familias:
| Familia | Cuántos | Arreglo |
| --------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Centrado** | 10 | `margin-inline-start: calc(<size> / -2)` si el tamaño se conoce (idioma del slider); `inset-inline: 0` + `margin-inline: auto` si lo fija el contenido |
| **Direccional** | 6 | El signo se invierte con `:dir(rtl)` ; el par queda marcado `rtl-physical` |
| **Físico** | 7 | `left` /`right` — geometría de brújula (`cropper`) y polar (`menu-dial` arco) |
feat(direction): un guard para el eje logico-fisico, y el <main> que congelaba las 51 demos
Cierra la §2.1 del handoff. El guard nacio en rojo con 24 hallazgos y al
arreglarlos aparecieron dos defectos mas grandes que el que buscaba.
1) EL GUARD RTL-1. Nada cruzaba un ancla LOGICA con un desplazamiento FISICO
en la misma regla CSS: eidos-lint clasifica SELECTORES, no declaraciones, y la
fila RTL del contrato de construccion decia "rule (LIVE)" — revision humana.
Por eso el defecto del slider llego a produccion y lo cazo el ojo del usuario.
Ahora `npm run rtl:check`, con la logica en src/uix/eidos/rtl-lint.ts (14
tests) para que sea testeable, calcando el par lint.ts / scripts/eidos-lint.ts.
El test ancla el caso historico: slider.css ANTES de 013ceac57 sale rojo y el
arreglo que se commiteo sale verde.
Dos desviaciones del enunciado, deliberadas. AMPLIA a la propiedad
independiente `translate:`, que se usa MAS que `transform: translate*` (87
frente a 36) y tiene el mismo defecto — ocho de los 24 hallazgos eran de esa
forma. RECORTA `padding-inline*`: no mueve la caja contra su ancla, asi que no
puede pelear con un translate; solo anadia ruido.
2) LA COSECHA, 24 -> 1. El unico que queda es palabras, auditado y no tocado.
Los 23 caen en tres familias: CENTRADO (10) donde el resultado deseado es
simetrico y basta dejar de descentrar; DIRECCIONAL (6) donde el signo debe
voltear; y FISICO (7) donde lo que sobraba era el ancla logica — handles de
brujula del cropper sobre una imagen que no se voltea, y el arco polar del
menu-dial. Medido en Chrome el switch: el thumb salia a +24px en un carril de
38px, colgando fuera. Verificados tambien carousel, avatar, cropper, menu-dial
y chat-log.
3) :dir() ES LA DOCTRINA, [dir='rtl'] QUEDA PROHIBIDO. Los 12 selectores que
habia en eidos estaban TODOS rotos, en los dos sentidos opuestos del mismo
error, y ambos medidos en Chrome. Los 5 descendientes se aplican DE MAS: el
sidebar tenia direction:ltr de verdad y aun asi matcheaba, porque el selector
capta cualquier ancestro RTL e ignora un dir mas cercano que redeclare. Los 7
de mismo-elemento NO se aplican NUNCA: el tree-view era rtl de verdad y no
matcheaba, porque desde 013ceac57 el atributo solo se estampa si alguien lo
afirmo. :dir() acierta los tres casos porque mira la direccion RESUELTA.
Baseline desde 2023.
4) EL PIN DEL <main>. 5469e05df hizo dos cosas que se anulan: puso las demos en
`auto` —sin prop dir, o sea el componente no estampa atributo y HEREDA del
DOM— y a la vez clavo <main data-uix-canvas dir="ltr"> sobre ellas. Ese <main>
era la herencia, asi que el toggle del topbar y el arabe llegaban a <html> y
morian ahi: las 51 demos congeladas en LTR. Su comentario afirmaba que "los
componentes siguen las prefs, este contenedor no les afecta" — cierto para la
LOGICA, falso para el CSS, que lee el direction COMPUTADO. Medido: html
dir="rtl" con el panel del sidebar todavia a la izquierda, y quitar ese
atributo y nada mas lo movia. Se queda el lang="en". Coste aceptado por el
usuario: vuelve la puntuacion desplazada en la prosa inglesa al elegir RTL, que
es cosmetico y solo en un modo de inspeccion; demos que no pueden demostrar,
no. Si hay que fijar la prosa otra vez, el pin va alrededor de la PROSA.
Verificado: check 74 = linea base exacta en las tres pasadas. rtl-lint 14/14.
Suite de eidos 375 pasan; el fallo de lint.test.ts por audio-player es
PREEXISTENTE, comprobado con stash, y es del hilo del sonido. Los 9 CSS tocados
ya fallaban prettier antes de tocarlos, asi que no se formatean.
SIN VERIFICAR EN NAVEGADOR, anotado en el handoff: archetypes y proof-of-human
(sus reglas viven en @media (pointer: coarse), invisible en escritorio) y
sliding-indicator (la demo del radio-group segmented no llegaba a medir).
QUEDA: hay wrappers estampando dir="" vacio en vez de omitir el atributo —
visto en sidebar y tree-view; no rompe porque un valor invalido hereda, pero
contradice la letra del contrato.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
⚠️ **NO añadas `inline-size: fit-content` a ciegas** en el centrado por márgenes
automáticos: al trigger del `carousel` le comió el ancho de 36px a 17px porque su
tamaño venía de fuera. Sólo hace falta cuando el elemento no tiene ancho
determinable, y hay que medirlo después.
**Verificado en Chrome**: switch (roto→arreglado, medido y visto), carousel,
avatar, cropper, menu-dial, chat-log.
fix(direction): el indicador deslizante no se remedia al voltear la direccion
Verificar los tres arreglos que quedaron sin ver en ec533845d destapo la otra
mitad del sliding-indicator.
MeasuredIndicator remide cuando cambian root o active, cuando alguno de los dos
redimensiona, en resize de ventana y en visibilitychange. Voltear la direccion
no redimensiona NADA —los items solo cambian de posicion— y active conserva su
identidad, asi que --indicator-x se quedaba con la coordenada de la disposicion
anterior y la pildora caia sobre otro item.
Arreglado con una opcion `dir` (un getter) que el $effect lee ANTES del return
temprano, para que la dependencia quede registrada en todas las pasadas y no
solo en las que llegan a medir. El consumidor —radio-group, el unico hoy— le
pasa resolvedDir. Getter y no lectura del DOM a proposito: la direccion viene de
la cadena prop/prefs y el atributo es proyeccion, no fuente.
A/B con git stash sobre los dos ficheros, moviendo la direccion por el CAMINO
REAL (el toggle del topbar): sin el arreglo el translate se queda en 177.156px y
el indicador cae 173px fuera del seleccionado; con el arreglo pasa a 4px y dx=0
en las tres posiciones del ciclo auto/ltr/rtl.
Los otros dos arreglos que faltaba ver quedan VERIFICADOS, con un probe de
Playwright y Emulation.setEmulatedMedia porque sus reglas viven en @media
(pointer: coarse) y el navegador embebido no llega ahi: archetypes (checkbox y
switch) da margin-inline-start -22px sobre un slop de 44px —la mitad exacta— con
translate sin componente X, y el barrido de hit-test alcanza 22/21 y 22/22 px,
simetrico en LTR y en RTL; proof-of-human da -23px sobre 46px con translateY
solo, y 23/23 en el barrido.
NOTA DE METODO, anotada en el handoff: el primer probe volteaba con
setAttribute('dir') sobre el elemento y daba "no arreglado". Eso NO cambia
resolvedDir, que sale de la prop/prefs, asi que la dependencia no podia
dispararse — la prueba era invalida, no el arreglo. Cualquier verificacion de
este eje tiene que mover la direccion por el camino real.
Verificado: check 74 = linea base exacta. 73/73 en soma/layers + radio-group.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
**Los tres que faltaban, verificados después** con un probe de Playwright y
`Emulation.setEmulatedMedia` (`pointer: coarse`), porque el navegador embebido no
llega a esa media query:
- `archetypes` (checkbox y switch): `margin-inline-start: -22px` con slop de
`44px` — la mitad exacta — y `translate: 0 -50%` sin componente X. Barrido de
hit-test: alcanza 22/21 y 22/22 px, **simétrico en LTR y en RTL** .
- `proof-of-human` : `-23px` sobre `46px` , `translateY(-22)` sin X, hit-test 23/23.
- `sliding-indicator` : el CSS es correcto — con medida fresca cae exacto en RTL.
Pero destapó §6.6.
feat(direction): un guard para el eje logico-fisico, y el <main> que congelaba las 51 demos
Cierra la §2.1 del handoff. El guard nacio en rojo con 24 hallazgos y al
arreglarlos aparecieron dos defectos mas grandes que el que buscaba.
1) EL GUARD RTL-1. Nada cruzaba un ancla LOGICA con un desplazamiento FISICO
en la misma regla CSS: eidos-lint clasifica SELECTORES, no declaraciones, y la
fila RTL del contrato de construccion decia "rule (LIVE)" — revision humana.
Por eso el defecto del slider llego a produccion y lo cazo el ojo del usuario.
Ahora `npm run rtl:check`, con la logica en src/uix/eidos/rtl-lint.ts (14
tests) para que sea testeable, calcando el par lint.ts / scripts/eidos-lint.ts.
El test ancla el caso historico: slider.css ANTES de 013ceac57 sale rojo y el
arreglo que se commiteo sale verde.
Dos desviaciones del enunciado, deliberadas. AMPLIA a la propiedad
independiente `translate:`, que se usa MAS que `transform: translate*` (87
frente a 36) y tiene el mismo defecto — ocho de los 24 hallazgos eran de esa
forma. RECORTA `padding-inline*`: no mueve la caja contra su ancla, asi que no
puede pelear con un translate; solo anadia ruido.
2) LA COSECHA, 24 -> 1. El unico que queda es palabras, auditado y no tocado.
Los 23 caen en tres familias: CENTRADO (10) donde el resultado deseado es
simetrico y basta dejar de descentrar; DIRECCIONAL (6) donde el signo debe
voltear; y FISICO (7) donde lo que sobraba era el ancla logica — handles de
brujula del cropper sobre una imagen que no se voltea, y el arco polar del
menu-dial. Medido en Chrome el switch: el thumb salia a +24px en un carril de
38px, colgando fuera. Verificados tambien carousel, avatar, cropper, menu-dial
y chat-log.
3) :dir() ES LA DOCTRINA, [dir='rtl'] QUEDA PROHIBIDO. Los 12 selectores que
habia en eidos estaban TODOS rotos, en los dos sentidos opuestos del mismo
error, y ambos medidos en Chrome. Los 5 descendientes se aplican DE MAS: el
sidebar tenia direction:ltr de verdad y aun asi matcheaba, porque el selector
capta cualquier ancestro RTL e ignora un dir mas cercano que redeclare. Los 7
de mismo-elemento NO se aplican NUNCA: el tree-view era rtl de verdad y no
matcheaba, porque desde 013ceac57 el atributo solo se estampa si alguien lo
afirmo. :dir() acierta los tres casos porque mira la direccion RESUELTA.
Baseline desde 2023.
4) EL PIN DEL <main>. 5469e05df hizo dos cosas que se anulan: puso las demos en
`auto` —sin prop dir, o sea el componente no estampa atributo y HEREDA del
DOM— y a la vez clavo <main data-uix-canvas dir="ltr"> sobre ellas. Ese <main>
era la herencia, asi que el toggle del topbar y el arabe llegaban a <html> y
morian ahi: las 51 demos congeladas en LTR. Su comentario afirmaba que "los
componentes siguen las prefs, este contenedor no les afecta" — cierto para la
LOGICA, falso para el CSS, que lee el direction COMPUTADO. Medido: html
dir="rtl" con el panel del sidebar todavia a la izquierda, y quitar ese
atributo y nada mas lo movia. Se queda el lang="en". Coste aceptado por el
usuario: vuelve la puntuacion desplazada en la prosa inglesa al elegir RTL, que
es cosmetico y solo en un modo de inspeccion; demos que no pueden demostrar,
no. Si hay que fijar la prosa otra vez, el pin va alrededor de la PROSA.
Verificado: check 74 = linea base exacta en las tres pasadas. rtl-lint 14/14.
Suite de eidos 375 pasan; el fallo de lint.test.ts por audio-player es
PREEXISTENTE, comprobado con stash, y es del hilo del sonido. Los 9 CSS tocados
ya fallaban prettier antes de tocarlos, asi que no se formatean.
SIN VERIFICAR EN NAVEGADOR, anotado en el handoff: archetypes y proof-of-human
(sus reglas viven en @media (pointer: coarse), invisible en escritorio) y
sliding-indicator (la demo del radio-group segmented no llegaba a medir).
QUEDA: hay wrappers estampando dir="" vacio en vez de omitir el atributo —
visto en sidebar y tree-view; no rompe porque un valor invalido hereda, pero
contradice la letra del contrato.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### 6.3 · `:dir()` es la doctrina; `[dir='rtl']` está PROHIBIDO
Los 12 selectores `[dir='rtl']` que había en eidos estaban **todos rotos** , en los
dos sentidos opuestos del mismo error. Medido en Chrome:
| Forma | Cuántos | Fallo | Medición |
| --------------------- | ------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------- |
| `[dir='rtl'] <desc>` | 5 | **Se aplica de más** — capta un ancestro RTL e ignora un `dir` más cercano que redeclare | `sidebar` : `direction: ltr` y aun así matchea |
| `[data-x][dir='rtl']` | 7 | **No se aplica nunca** — el atributo ya no se estampa por defecto (`013ceac57`) | `tree-view` : `direction: rtl` de verdad y NO matchea |
feat(direction): un guard para el eje logico-fisico, y el <main> que congelaba las 51 demos
Cierra la §2.1 del handoff. El guard nacio en rojo con 24 hallazgos y al
arreglarlos aparecieron dos defectos mas grandes que el que buscaba.
1) EL GUARD RTL-1. Nada cruzaba un ancla LOGICA con un desplazamiento FISICO
en la misma regla CSS: eidos-lint clasifica SELECTORES, no declaraciones, y la
fila RTL del contrato de construccion decia "rule (LIVE)" — revision humana.
Por eso el defecto del slider llego a produccion y lo cazo el ojo del usuario.
Ahora `npm run rtl:check`, con la logica en src/uix/eidos/rtl-lint.ts (14
tests) para que sea testeable, calcando el par lint.ts / scripts/eidos-lint.ts.
El test ancla el caso historico: slider.css ANTES de 013ceac57 sale rojo y el
arreglo que se commiteo sale verde.
Dos desviaciones del enunciado, deliberadas. AMPLIA a la propiedad
independiente `translate:`, que se usa MAS que `transform: translate*` (87
frente a 36) y tiene el mismo defecto — ocho de los 24 hallazgos eran de esa
forma. RECORTA `padding-inline*`: no mueve la caja contra su ancla, asi que no
puede pelear con un translate; solo anadia ruido.
2) LA COSECHA, 24 -> 1. El unico que queda es palabras, auditado y no tocado.
Los 23 caen en tres familias: CENTRADO (10) donde el resultado deseado es
simetrico y basta dejar de descentrar; DIRECCIONAL (6) donde el signo debe
voltear; y FISICO (7) donde lo que sobraba era el ancla logica — handles de
brujula del cropper sobre una imagen que no se voltea, y el arco polar del
menu-dial. Medido en Chrome el switch: el thumb salia a +24px en un carril de
38px, colgando fuera. Verificados tambien carousel, avatar, cropper, menu-dial
y chat-log.
3) :dir() ES LA DOCTRINA, [dir='rtl'] QUEDA PROHIBIDO. Los 12 selectores que
habia en eidos estaban TODOS rotos, en los dos sentidos opuestos del mismo
error, y ambos medidos en Chrome. Los 5 descendientes se aplican DE MAS: el
sidebar tenia direction:ltr de verdad y aun asi matcheaba, porque el selector
capta cualquier ancestro RTL e ignora un dir mas cercano que redeclare. Los 7
de mismo-elemento NO se aplican NUNCA: el tree-view era rtl de verdad y no
matcheaba, porque desde 013ceac57 el atributo solo se estampa si alguien lo
afirmo. :dir() acierta los tres casos porque mira la direccion RESUELTA.
Baseline desde 2023.
4) EL PIN DEL <main>. 5469e05df hizo dos cosas que se anulan: puso las demos en
`auto` —sin prop dir, o sea el componente no estampa atributo y HEREDA del
DOM— y a la vez clavo <main data-uix-canvas dir="ltr"> sobre ellas. Ese <main>
era la herencia, asi que el toggle del topbar y el arabe llegaban a <html> y
morian ahi: las 51 demos congeladas en LTR. Su comentario afirmaba que "los
componentes siguen las prefs, este contenedor no les afecta" — cierto para la
LOGICA, falso para el CSS, que lee el direction COMPUTADO. Medido: html
dir="rtl" con el panel del sidebar todavia a la izquierda, y quitar ese
atributo y nada mas lo movia. Se queda el lang="en". Coste aceptado por el
usuario: vuelve la puntuacion desplazada en la prosa inglesa al elegir RTL, que
es cosmetico y solo en un modo de inspeccion; demos que no pueden demostrar,
no. Si hay que fijar la prosa otra vez, el pin va alrededor de la PROSA.
Verificado: check 74 = linea base exacta en las tres pasadas. rtl-lint 14/14.
Suite de eidos 375 pasan; el fallo de lint.test.ts por audio-player es
PREEXISTENTE, comprobado con stash, y es del hilo del sonido. Los 9 CSS tocados
ya fallaban prettier antes de tocarlos, asi que no se formatean.
SIN VERIFICAR EN NAVEGADOR, anotado en el handoff: archetypes y proof-of-human
(sus reglas viven en @media (pointer: coarse), invisible en escritorio) y
sliding-indicator (la demo del radio-group segmented no llegaba a medir).
QUEDA: hay wrappers estampando dir="" vacio en vez de omitir el atributo —
visto en sidebar y tree-view; no rompe porque un valor invalido hereda, pero
contradice la letra del contrato.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
Migrados los 12 a `:dir()` , que acierta los tres casos (atributo propio, heredado,
y redeclarado por un ancestro intermedio). Verificado en `sidebar` y `tree-view` .
**No lo reintroduzcas.** Con el contrato «el atributo nunca tiene defecto»,
`[dir='rtl']` no puede expresar «la dirección efectiva de este elemento es RTL».
Baseline desde 2023 (Chrome 120, Safari 16.4, Firefox 49).
### 6.4 · El pin del `<main>` anulaba la otra mitad de `5469e05df`
`5469e05df` hizo dos cosas que **se cancelan** : puso las demos en `auto` (sin prop
`dir` , resolviendo por prefs → **no estampan atributo → heredan del DOM** ) y a la
vez clavó `<main data-uix-canvas dir="ltr">` sobre ellas. Resultado: el toggle del
topbar y el árabe llegaban a `<html>` y **morían en el `<main>`** . Las 51 demos
estaban congeladas en LTR.
El comentario que lo justificaba afirmaba que «los componentes siguen las prefs,
así que este contenedor no les afecta». Es cierto para la LÓGICA (teclado,
cálculos JS con `resolvedDir` ) y **falso para el CSS** , que depende del `direction`
computado, y éste viene del DOM.
Medido en el sidebar: `html dir="rtl"` con el panel todavía a la izquierda, y
quitar ESE atributo y nada más lo movía a la derecha.
Quitado el `dir` del `<main>` (el `lang="en"` se queda: es correcto y no toca la
dirección). **Coste aceptado por el usuario** : la prosa inglesa vuelve a mostrar la
puntuación desplazada en RTL. Es cosmético y sólo en un modo de inspección; demos
que no pueden demostrar, no. Si se quiere recuperar, **el pin va alrededor de la
PROSA**, nunca alrededor del canvas que también contiene los ejemplos vivos.
fix(direction): el indicador deslizante no se remedia al voltear la direccion
Verificar los tres arreglos que quedaron sin ver en ec533845d destapo la otra
mitad del sliding-indicator.
MeasuredIndicator remide cuando cambian root o active, cuando alguno de los dos
redimensiona, en resize de ventana y en visibilitychange. Voltear la direccion
no redimensiona NADA —los items solo cambian de posicion— y active conserva su
identidad, asi que --indicator-x se quedaba con la coordenada de la disposicion
anterior y la pildora caia sobre otro item.
Arreglado con una opcion `dir` (un getter) que el $effect lee ANTES del return
temprano, para que la dependencia quede registrada en todas las pasadas y no
solo en las que llegan a medir. El consumidor —radio-group, el unico hoy— le
pasa resolvedDir. Getter y no lectura del DOM a proposito: la direccion viene de
la cadena prop/prefs y el atributo es proyeccion, no fuente.
A/B con git stash sobre los dos ficheros, moviendo la direccion por el CAMINO
REAL (el toggle del topbar): sin el arreglo el translate se queda en 177.156px y
el indicador cae 173px fuera del seleccionado; con el arreglo pasa a 4px y dx=0
en las tres posiciones del ciclo auto/ltr/rtl.
Los otros dos arreglos que faltaba ver quedan VERIFICADOS, con un probe de
Playwright y Emulation.setEmulatedMedia porque sus reglas viven en @media
(pointer: coarse) y el navegador embebido no llega ahi: archetypes (checkbox y
switch) da margin-inline-start -22px sobre un slop de 44px —la mitad exacta— con
translate sin componente X, y el barrido de hit-test alcanza 22/21 y 22/22 px,
simetrico en LTR y en RTL; proof-of-human da -23px sobre 46px con translateY
solo, y 23/23 en el barrido.
NOTA DE METODO, anotada en el handoff: el primer probe volteaba con
setAttribute('dir') sobre el elemento y daba "no arreglado". Eso NO cambia
resolvedDir, que sale de la prop/prefs, asi que la dependencia no podia
dispararse — la prueba era invalida, no el arreglo. Cualquier verificacion de
este eje tiene que mover la direccion por el camino real.
Verificado: check 74 = linea base exacta. 73/73 en soma/layers + radio-group.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### 6.6 · El indicador deslizante no se re-medía al voltear
Verificar §6.2 destapó la otra mitad del `sliding-indicator` . `MeasuredIndicator`
(`src/uix/soma/layers/measured-indicator.svelte.ts`) re-mide cuando cambian `root`
o `active` , cuando alguno redimensiona, en `resize` de ventana y en
`visibilitychange` . **Voltear la dirección no redimensiona nada** —los ítems sólo
cambian de POSICIÓN— y `active` conserva su identidad, así que `--indicator-x`
se quedaba con la coordenada de la disposición anterior.
Arreglado con una opción `dir` (un getter) que el `$effect` lee **antes del return
temprano**, para que la dependencia se registre en todas las pasadas. El consumidor
(`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.
⚠️ **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
dispararse. Para probar cualquier cosa del eje hay que mover la dirección por el
CAMINO REAL (el toggle del topbar o el control de la demo), nunca tocando el DOM.
A/B con `git stash` sobre los dos ficheros, por la cadena real: sin el arreglo,
`translate` se queda en `177.156px` y el indicador cae 173px fuera del
seleccionado; con él, pasa a `4px` y `dx = 0` . `check` 74 = línea base, 73/73 en
`soma/layers` + `radio-group` .
**Único consumidor hoy**: `radio-group` . Si aparece otro, tiene que pasar `dir` .
refactor(tabs): el indicador pasa a MeasuredIndicator, y aparece un punto ciego del guard
Buscados los indicadores que comparten mecanismo con el de dc5e95665. Medidos
los dos, no supuestos.
NAVIGATION-MENU: CORRECTO, no se toca. Su indicador solo existe mientras un
menu esta ABIERTO, asi que abrirlo ES el disparador de la medida y nunca queda
rancia; ademas voltear cierra el menu, con lo que el escenario del rect viejo ni
se alcanza; y ancla y medida son ambas fisicas. Medido dx=0 en LTR, en RTL y al
volver. El usuario ya lo veia bien y tenia razon.
TABS: roto por partida doble. (a) No remedia al voltear —dx=518—, el mismo
agujero que el radio-group. (b) NI CON MEDIDA FRESCA acertaba en RTL —dx=400—
porque [data-tabs-indicator] anclaba con inset-inline-start LOGICO mientras el
JS le aplicaba un translate3d(x) FISICO. Es el defecto del slider, escondido
donde el guard no mira.
Migrado a MeasuredIndicator, que es la capa escrita para generalizar justo esto:
soma mide (heredando la dependencia `dir` de dc5e95665) y expone --indicator-*,
y el recipe posiciona con la propiedad translate desde esas vars y ancla en
left: 0 fisico, coherente con una medida que es fisica. El wrapper de eidos
pierde su rAF, sus observadores y su medicion: pasa a ser forma, que es su
sitio. Los cuatro escenarios a dx=0, verificado en las tres variantes con
indicador y a ojo en RTL.
PUNTO CIEGO DE RTL-1, documentado en el propio rtl-lint.ts y en el handoff: el
defecto (b) llevaba ahi desde siempre y el guard no podia verlo, porque el ancla
estaba en el CSS y el transform lo escribia el JS INLINE. RTL-1 cruza
declaraciones dentro de una regla CSS. "24 -> 1" significa 24 en el CSS
estatico, NO que el catalogo este limpio: lo que pinta desde JS sigue
necesitando el ojo.
Efecto lateral bueno: la variante `line` vuelve a mandar en su grosor de 2px. El
height inline del wrapper viejo lo pisaba — un comentario del propio tabs.css
daba por hecho que no pasaba.
Un guardian se activo y es CORRECTO que lo hiciera:
component-visual-attrs.test.ts exigia data-ready en el wrapper de eidos. El
atributo cambio de dueño a soma, asi que se retira la entrada, igual que el
indicador del radio-group, que tampoco lo lista.
Verificado: check 74 = linea base exacta. 446 tests pasan; el unico fallo es el
de lint.test.ts por audio-player, PREEXISTENTE y comprobado con stash.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### 6.7 · Los otros dos indicadores — uno bien, otro roto
Buscados los que comparten mecanismo con §6.6. **Medidos, no supuestos:**
- **`navigation-menu`: correcto, no se toca.** Su indicador sólo existe mientras
un menú está ABIERTO, así que abrirlo ES el disparador de la medida y nunca
queda rancia; voltear cierra el menú, con lo que el escenario ni se alcanza; y
ancla y medida son ambas físicas. Medido: `dx = 0` en LTR, en RTL y al volver.
- **`tabs`: roto por partida doble, y MIGRADO a `MeasuredIndicator` .** Medía en
el wrapper de eidos con su propio rAF y observadores. (a) No re-medía al
voltear —`dx = 518`— y (b) **ni con medida fresca acertaba en RTL** —`dx = 400`—
porque `[data-tabs-indicator]` anclaba con `inset-inline-start: 0` LÓGICO
mientras el JS le aplicaba un `translate3d(x)` FÍSICO. Ahora soma mide (con la
dependencia `dir` ) y el recipe posiciona desde `--indicator-*` con `left: 0` .
Los cuatro escenarios a `dx = 0` , verificado en las tres variantes con
indicador y en RTL a ojo.
⚠️ **PUNTO CIEGO DE RTL-1, y es importante** : el defecto (b) llevaba ahí desde
siempre y el guard **no podía verlo** , porque el ancla estaba en el CSS y el
`transform` lo escribía el JS INLINE. RTL-1 cruza declaraciones dentro de una
regla CSS. Así que «24 → 1» quiere decir 24 en el CSS estático, **no** que el
catálogo esté limpio: lo que pinta desde JS (§2.2) sigue necesitando el ojo.
Efecto lateral bueno: la variante `line` vuelve a mandar en su grosor (2px). El
`height` inline del wrapper viejo lo pisaba, cosa que un comentario del propio
`tabs.css` daba por hecha que no pasaba.
Un guardián se activó y **es correcto que lo hiciera** :
`component-visual-attrs.test.ts` exigía `data-ready` en el wrapper de eidos. El
atributo cambió de dueño a soma, así que la entrada se retira — igual que el
indicador del `radio-group` , que tampoco lo lista.
fix(float-panel): la semilla anclada usa la matematica de $ethereal en vez de una copia sin direccion
Verificados uno a uno los cinco componentes de la §2.2 del handoff, y despues
barrido el punto ciego del guard. Ningun cambio de comportamiento salvo el de
float-panel.
LOS CINCO DE §2.2 ESTAN SANOS. slider: click al 25% del ancho FISICO da 75 en
RTL, arrastrar a la derecha sube en LTR y baja en RTL, y ArrowRight igual.
number-field y css-field: el mismo arrastre fisico sube en LTR y baja en RTL.
dropdown-menu: el panel raiz se alinea al inline-start y el submenu abre a la
derecha en LTR y a la izquierda en RTL. carousel: el track voltea (Next mueve
-622px en LTR y +622px en RTL). Ninguno necesito arreglo.
EL SWIPE DEL CAROUSEL NO ESTA MEDIDO. Cinco intentos de simulacion de puntero y
no consegui disparar el gesto de forma reproducible ni en LTR: la capa tiene
umbral de distancia y de velocidad. El signo esta bien por LECTURA
(isRtlHorizontal ? -offset : offset). Son diez segundos con un raton de verdad.
UN MOTOR CUBRE NUEVE COMPONENTES. soma/layers/floating lo consumen combobox,
context-menu, dropdown-menu, link-preview, menubar, popover, select, sidebar y
tooltip. Verificado a traves de popover (align=end: derecha en LTR, izquierda en
RTL) y menubar (align=start, al reves). Los nueve quedan cubiertos.
FLOAT-PANEL NO USA ESE MOTOR, Y HACE BIEN: useFloating mantiene el panel PEGADO
a su ancla, y un float-panel se arrastra y redimensiona libremente. Pero su
SEMILLA anclada si era redundante: computeInitialPosition() re-derivaba
side + align contra el rect a mano, y esa copia resolvia align:'start' a r.left
SIEMPRE — una alineacion LOGICA clavada a un borde FISICO, que nunca voltea.
$ethereal ya exporta computeCoordsFromPlacement(rects, placement, rtl): pura,
sincrona, sin ciclo de vida, y es la misma que usa el motor compartido. Migrada
la semilla a ella. Se comparte la semilla, NO useFloating.
anchored-seed.test.ts fija la migracion, porque la demo NO ancla ningun panel y
el defecto no era observable ahi: en LTR el resultado es IDENTICO al algoritmo
anterior en las 12 combinaciones side x align (cero regresion); en RTL con side
vertical start y end se espejan; con side horizontal el align no cambia, que es
el eje de bloque; y center sigue centrado en ambas. La demo, medida antes y
despues, no se mueve.
NO TOCADO, hace falta criterio: Home/End en el eje X llevan el panel a
bounds.left / bounds.right. Para un panel que se arrastra libremente es
defendible que sean extremos fisicos y no principio/final de lectura.
Descartados como falsos positivos tras mirar el uso real: container (prop de
margenes), text-focus, text-scramble y path-trace (miden donde esta algo para un
efecto), drag-drop (sueltas donde ves), cropper (paneas una imagen que no se
voltea) y gradient-builder (arrastras la parada donde la ves). Y drawer, que no
aparecia en el primer cruce, esta BIEN y es el ejemplo a imitar: traduce
start/end a left/right una sola vez en resolveDirection() y desde ahi todo es
fisico y coherente — aunque su demo solo expone valores fisicos, asi que su
mitad logica no es observable.
Verificado: check 74 = linea base exacta. 12/12 en float-panel + 4/4 del test
nuevo.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### 6.8 · §2.2 verificada — los cinco están sanos, pero la LISTA no lo estaba
Pasados por navegador, con la dirección movida por el camino real (toggle del
topbar → prefs), nunca por `setAttribute` :
| | Resultado |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| **slider** | ✓ Click al 25% del ancho FÍSICO da 75 en RTL; arrastrar +120px a la derecha sube en LTR y baja en RTL; `ArrowRight` igual. Los seis sitios responden |
| **number-field** | ✓ El mismo arrastre físico sube en LTR y baja en RTL |
| **css-field** | ✓ Idéntico (`16px → 40px` en LTR, `40px → 16px` en RTL) |
| **dropdown-menu** | ✓ El panel raíz se alinea al inline-start (izquierda en LTR, derecha en RTL) y el submenú abre a la derecha en LTR, a la izquierda en RTL |
| **carousel** | Track ✓ (Next mueve − 622px en LTR y +622px en RTL). **Swipe SIN MEDIR** |
fix(float-panel): la semilla anclada usa la matematica de $ethereal en vez de una copia sin direccion
Verificados uno a uno los cinco componentes de la §2.2 del handoff, y despues
barrido el punto ciego del guard. Ningun cambio de comportamiento salvo el de
float-panel.
LOS CINCO DE §2.2 ESTAN SANOS. slider: click al 25% del ancho FISICO da 75 en
RTL, arrastrar a la derecha sube en LTR y baja en RTL, y ArrowRight igual.
number-field y css-field: el mismo arrastre fisico sube en LTR y baja en RTL.
dropdown-menu: el panel raiz se alinea al inline-start y el submenu abre a la
derecha en LTR y a la izquierda en RTL. carousel: el track voltea (Next mueve
-622px en LTR y +622px en RTL). Ninguno necesito arreglo.
EL SWIPE DEL CAROUSEL NO ESTA MEDIDO. Cinco intentos de simulacion de puntero y
no consegui disparar el gesto de forma reproducible ni en LTR: la capa tiene
umbral de distancia y de velocidad. El signo esta bien por LECTURA
(isRtlHorizontal ? -offset : offset). Son diez segundos con un raton de verdad.
UN MOTOR CUBRE NUEVE COMPONENTES. soma/layers/floating lo consumen combobox,
context-menu, dropdown-menu, link-preview, menubar, popover, select, sidebar y
tooltip. Verificado a traves de popover (align=end: derecha en LTR, izquierda en
RTL) y menubar (align=start, al reves). Los nueve quedan cubiertos.
FLOAT-PANEL NO USA ESE MOTOR, Y HACE BIEN: useFloating mantiene el panel PEGADO
a su ancla, y un float-panel se arrastra y redimensiona libremente. Pero su
SEMILLA anclada si era redundante: computeInitialPosition() re-derivaba
side + align contra el rect a mano, y esa copia resolvia align:'start' a r.left
SIEMPRE — una alineacion LOGICA clavada a un borde FISICO, que nunca voltea.
$ethereal ya exporta computeCoordsFromPlacement(rects, placement, rtl): pura,
sincrona, sin ciclo de vida, y es la misma que usa el motor compartido. Migrada
la semilla a ella. Se comparte la semilla, NO useFloating.
anchored-seed.test.ts fija la migracion, porque la demo NO ancla ningun panel y
el defecto no era observable ahi: en LTR el resultado es IDENTICO al algoritmo
anterior en las 12 combinaciones side x align (cero regresion); en RTL con side
vertical start y end se espejan; con side horizontal el align no cambia, que es
el eje de bloque; y center sigue centrado en ambas. La demo, medida antes y
despues, no se mueve.
NO TOCADO, hace falta criterio: Home/End en el eje X llevan el panel a
bounds.left / bounds.right. Para un panel que se arrastra libremente es
defendible que sean extremos fisicos y no principio/final de lectura.
Descartados como falsos positivos tras mirar el uso real: container (prop de
margenes), text-focus, text-scramble y path-trace (miden donde esta algo para un
efecto), drag-drop (sueltas donde ves), cropper (paneas una imagen que no se
voltea) y gradient-builder (arrastras la parada donde la ves). Y drawer, que no
aparecia en el primer cruce, esta BIEN y es el ejemplo a imitar: traduce
start/end a left/right una sola vez en resolveDirection() y desde ahi todo es
fisico y coherente — aunque su demo solo expone valores fisicos, asi que su
mitad logica no es observable.
Verificado: check 74 = linea base exacta. 12/12 en float-panel + 4/4 del test
nuevo.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
⚠️ **El swipe del carousel se me resistió a cinco intentos** de simulación de
puntero: la capa de gesto tiene umbral de distancia y de velocidad, y con
`page.mouse` no conseguí dispararlo de forma reproducible ni en LTR. El signo
está bien POR LECTURA (`isRtlHorizontal ? -offset : offset`, con los tres casos
comentados). Son diez segundos con un dedo o un ratón de verdad: probar que
arrastrar hacia la izquierda avanza en LTR y hacia la derecha avanza en RTL.
### 6.9 · Barrido del punto ciego — quién más pinta desde JS
Un primer cruce «matemática horizontal» × «menciona la dirección» dio trece
sospechosos. **Revisado uno a uno, esa lista estaba inflada Y le faltaban
piezas.** El cruce por sí solo no vale: `.left` capta cosas que no son
direccionales, y un componente puede manejar la dirección sin nombrar `rtl` .
**Un motor cubre nueve de golpe.** `src/uix/soma/layers/floating` lo consumen
`combobox` , `context-menu` , `dropdown-menu` , `link-preview` , `menubar` ,
`popover` , `select` , `sidebar` y `tooltip` . Verificado el motor a través de
`popover` (align=end: derecha en LTR, izquierda en RTL ✓) y `menubar`
(align=start: izquierda en LTR, derecha en RTL ✓), más `dropdown-menu` en §6.8.
**Los nueve quedan cubiertos.** ⚠️ `float-panel` NO usa este motor — tiene
colocación, arrastre, resize y teclado propios, y cero dirección.
**Falsos positivos, descartados por lectura del uso real:** `container` (es una
prop de márgenes), `text-focus` / `text-scramble` / `path-trace` (miden dónde
está un elemento o el puntero para un efecto: físico correcto), `drag-drop`
(sueltas donde ves), `cropper` (paneas una imagen que no se voltea, ya decidido
en §6.2), `gradient-builder` (arrastras la parada donde la ves).
**Falsos positivos de MI MÉTODO, que conviene no repetir:** medir «a qué borde
se pega el panel» falla cuando el panel tiene el ancho exacto del disparador —
el `select` daba `Δleft=0 Δright=0` y mi desempate lo marcaba en rojo sin que
pasara nada.
**Lo que el primer cruce NO vio** (escriben geometría inline desde JS sin usar
`clientX` ): `drawer` , `color-picker` , `rotate-align` , `spinner` ,
`text-circular` . De ellos:
- **`drawer` está bien y es el ejemplo a imitar**: traduce `start` /`end` a
`left` /`right` UNA vez en `resolveDirection()` y a partir de ahí toda su
matemática es física y coherente. Verificado que con un valor físico
(`right`) NO voltea, que es lo correcto. ⚠️ **Pero su demo sólo expone
`top` /`right`/`bottom`/`left`**, así que la mitad lógica —la única que
ejercita RTL— **no es observable** . Mismo agujero que §2.3.
- `color-picker` : el thumb del área 2D se queda en el mismo sitio al voltear.
Probablemente correcto por diseño (una rueda de color es física, como el
cropper), pero es una DECISIÓN, no un hecho verificado.
- `rotate-align` , `spinner` , `text-circular` : efectos rotatorios, sin eje.
### 6.10 · float-panel — era código redundante, y por eso estaba mal
`float-panel` no usa el motor compartido porque el suyo es otro problema:
`useFloating` mantiene el panel PEGADO a su ancla, y un float-panel se arrastra
y redimensiona libremente en cuanto se abre. Hasta ahí, correcto.
**Pero su semilla anclada sí era redundante.** `computeInitialPosition()`
re-derivaba `side` + `align` contra el rect del ancla a mano, y esa copia
resolvía `align: 'start'` a `r.left` SIEMPRE — una alineación LÓGICA clavada a
un borde FÍSICO, que nunca voltea. `$ethereal` ya exporta la matemática buena:
```ts
computeCoordsFromPlacement(rects, placement, rtl); // pura, síncrona, sin ciclo de vida
fix(float-panel): la semilla anclada usa la matematica de $ethereal en vez de una copia sin direccion
Verificados uno a uno los cinco componentes de la §2.2 del handoff, y despues
barrido el punto ciego del guard. Ningun cambio de comportamiento salvo el de
float-panel.
LOS CINCO DE §2.2 ESTAN SANOS. slider: click al 25% del ancho FISICO da 75 en
RTL, arrastrar a la derecha sube en LTR y baja en RTL, y ArrowRight igual.
number-field y css-field: el mismo arrastre fisico sube en LTR y baja en RTL.
dropdown-menu: el panel raiz se alinea al inline-start y el submenu abre a la
derecha en LTR y a la izquierda en RTL. carousel: el track voltea (Next mueve
-622px en LTR y +622px en RTL). Ninguno necesito arreglo.
EL SWIPE DEL CAROUSEL NO ESTA MEDIDO. Cinco intentos de simulacion de puntero y
no consegui disparar el gesto de forma reproducible ni en LTR: la capa tiene
umbral de distancia y de velocidad. El signo esta bien por LECTURA
(isRtlHorizontal ? -offset : offset). Son diez segundos con un raton de verdad.
UN MOTOR CUBRE NUEVE COMPONENTES. soma/layers/floating lo consumen combobox,
context-menu, dropdown-menu, link-preview, menubar, popover, select, sidebar y
tooltip. Verificado a traves de popover (align=end: derecha en LTR, izquierda en
RTL) y menubar (align=start, al reves). Los nueve quedan cubiertos.
FLOAT-PANEL NO USA ESE MOTOR, Y HACE BIEN: useFloating mantiene el panel PEGADO
a su ancla, y un float-panel se arrastra y redimensiona libremente. Pero su
SEMILLA anclada si era redundante: computeInitialPosition() re-derivaba
side + align contra el rect a mano, y esa copia resolvia align:'start' a r.left
SIEMPRE — una alineacion LOGICA clavada a un borde FISICO, que nunca voltea.
$ethereal ya exporta computeCoordsFromPlacement(rects, placement, rtl): pura,
sincrona, sin ciclo de vida, y es la misma que usa el motor compartido. Migrada
la semilla a ella. Se comparte la semilla, NO useFloating.
anchored-seed.test.ts fija la migracion, porque la demo NO ancla ningun panel y
el defecto no era observable ahi: en LTR el resultado es IDENTICO al algoritmo
anterior en las 12 combinaciones side x align (cero regresion); en RTL con side
vertical start y end se espejan; con side horizontal el align no cambia, que es
el eje de bloque; y center sigue centrado en ambas. La demo, medida antes y
despues, no se mueve.
NO TOCADO, hace falta criterio: Home/End en el eje X llevan el panel a
bounds.left / bounds.right. Para un panel que se arrastra libremente es
defendible que sean extremos fisicos y no principio/final de lectura.
Descartados como falsos positivos tras mirar el uso real: container (prop de
margenes), text-focus, text-scramble y path-trace (miden donde esta algo para un
efecto), drag-drop (sueltas donde ves), cropper (paneas una imagen que no se
voltea) y gradient-builder (arrastras la parada donde la ves). Y drawer, que no
aparecia en el primer cruce, esta BIEN y es el ejemplo a imitar: traduce
start/end a left/right una sola vez en resolveDirection() y desde ahi todo es
fisico y coherente — aunque su demo solo expone valores fisicos, asi que su
mitad logica no es observable.
Verificado: check 74 = linea base exacta. 12/12 en float-panel + 4/4 del test
nuevo.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
```
Es la misma que usa el motor compartido, y toma la dirección como argumento.
Migrado: se comparte la SEMILLA, no `useFloating` .
Verificado con `anchored-seed.test.ts` (4 casos), porque **la demo no ancla
ningún panel** y el defecto no era observable ahí:
fix(float-panel): la semilla anclada usa la matematica de $ethereal en vez de una copia sin direccion
Verificados uno a uno los cinco componentes de la §2.2 del handoff, y despues
barrido el punto ciego del guard. Ningun cambio de comportamiento salvo el de
float-panel.
LOS CINCO DE §2.2 ESTAN SANOS. slider: click al 25% del ancho FISICO da 75 en
RTL, arrastrar a la derecha sube en LTR y baja en RTL, y ArrowRight igual.
number-field y css-field: el mismo arrastre fisico sube en LTR y baja en RTL.
dropdown-menu: el panel raiz se alinea al inline-start y el submenu abre a la
derecha en LTR y a la izquierda en RTL. carousel: el track voltea (Next mueve
-622px en LTR y +622px en RTL). Ninguno necesito arreglo.
EL SWIPE DEL CAROUSEL NO ESTA MEDIDO. Cinco intentos de simulacion de puntero y
no consegui disparar el gesto de forma reproducible ni en LTR: la capa tiene
umbral de distancia y de velocidad. El signo esta bien por LECTURA
(isRtlHorizontal ? -offset : offset). Son diez segundos con un raton de verdad.
UN MOTOR CUBRE NUEVE COMPONENTES. soma/layers/floating lo consumen combobox,
context-menu, dropdown-menu, link-preview, menubar, popover, select, sidebar y
tooltip. Verificado a traves de popover (align=end: derecha en LTR, izquierda en
RTL) y menubar (align=start, al reves). Los nueve quedan cubiertos.
FLOAT-PANEL NO USA ESE MOTOR, Y HACE BIEN: useFloating mantiene el panel PEGADO
a su ancla, y un float-panel se arrastra y redimensiona libremente. Pero su
SEMILLA anclada si era redundante: computeInitialPosition() re-derivaba
side + align contra el rect a mano, y esa copia resolvia align:'start' a r.left
SIEMPRE — una alineacion LOGICA clavada a un borde FISICO, que nunca voltea.
$ethereal ya exporta computeCoordsFromPlacement(rects, placement, rtl): pura,
sincrona, sin ciclo de vida, y es la misma que usa el motor compartido. Migrada
la semilla a ella. Se comparte la semilla, NO useFloating.
anchored-seed.test.ts fija la migracion, porque la demo NO ancla ningun panel y
el defecto no era observable ahi: en LTR el resultado es IDENTICO al algoritmo
anterior en las 12 combinaciones side x align (cero regresion); en RTL con side
vertical start y end se espejan; con side horizontal el align no cambia, que es
el eje de bloque; y center sigue centrado en ambas. La demo, medida antes y
despues, no se mueve.
NO TOCADO, hace falta criterio: Home/End en el eje X llevan el panel a
bounds.left / bounds.right. Para un panel que se arrastra libremente es
defendible que sean extremos fisicos y no principio/final de lectura.
Descartados como falsos positivos tras mirar el uso real: container (prop de
margenes), text-focus, text-scramble y path-trace (miden donde esta algo para un
efecto), drag-drop (sueltas donde ves), cropper (paneas una imagen que no se
voltea) y gradient-builder (arrastras la parada donde la ves). Y drawer, que no
aparecia en el primer cruce, esta BIEN y es el ejemplo a imitar: traduce
start/end a left/right una sola vez en resolveDirection() y desde ahi todo es
fisico y coherente — aunque su demo solo expone valores fisicos, asi que su
mitad logica no es observable.
Verificado: check 74 = linea base exacta. 12/12 en float-panel + 4/4 del test
nuevo.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- LTR: idéntico al algoritmo anterior en las 12 combinaciones side × align —
cero regresión.
- RTL con side vertical: `start` y `end` se espejan (lo que el viejo no podía).
- RTL con side horizontal: el align NO cambia — es el eje de bloque.
- `center` sigue centrado en ambas.
La demo, medida antes y después, no se mueve (`left` 329 en LTR, 81 en RTL).
**No tocado, y es una decisión pendiente**: `Home` /`End` en el eje X llevan el
panel a `bounds.left` / `bounds.right` . Para un panel que se arrastra libremente
es defendible que sean extremos físicos y no principio/final de lectura. No es
un defecto claro; hace falta criterio.
fix(virtual-grid,virtual-list): en RTL el contenido desaparecia — el ancla de celda era fisica sobre un sizer logico
Reportado por el usuario: en la demo del virtual-grid, voltear la direccion deja
la rejilla EN BLANCO. Reproducido y medido en Chrome.
LTR : 108 celdas, 70 visibles, primera en x=381 (borde del viewport)
RTL : 108 celdas, 0 visibles, primera en x=-5101 (fuera de la pantalla)
No era scrollLeft —esta en 0 en ambos casos—, era el anclaje de la celda:
position:absolute con top:0, left:0 y translate3d(columnStart). Los dos terminos
son fisicos y coherentes ENTRE SI, pero el sizer interno es
`inline-size: 6000px`, o sea LOGICO: en RTL desborda hacia la izquierda y ocupa
[-5101, 899], asi que su borde fisico izquierdo es donde el contenido TERMINA.
`left: 0` mandaba las 108 celdas justo ahi.
Arreglado con ancla logica (inset-inline-start) y el signo en el offset, de modo
que el recorrido se queda en translate3d: un virtualizador recoloca miles de
celdas y conviene que siga en el compositor en vez de animar layout. Medido tras
el arreglo: 70 visibles en RTL, primera en x=779 = borde derecho del viewport
menos el ancho de celda, que es el inicio logico.
virtual-list tenia el MISMO patron (left:0 + translateX) y por tanto el mismo
defecto en modo horizontal. Mismo arreglo. El modo vertical no se ve afectado:
inset-inline-start:0 con width:100% es exactamente lo que left:0 era. Verificado:
12 items visibles en RTL, primero en x=755.
EL OTRO SINTOMA REPORTADO NO ES UN DEFECTO. «El cambio de idioma no le afecta»:
con el toggle en `auto`, elegir arabe voltea la lista correctamente —medido,
htmlDir y el direction computado del componente pasan a rtl—. Si el toggle esta
en ltr/rtl EXPLICITO el idioma no manda, porque el intent gana sobre la
derivacion; es lo ratificado en 013ceac57. Despista en la practica, pero es el
contrato y no lo toco.
Verificado: check 74 = linea base exacta. 10/10 en virtual-grid + virtual-list.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
**`toast`: descartado, es correcto.** Todo su vocabulario es FÍSICO y coherente
consigo mismo — `position` es `top-left` …`bottom-right` y `swipeDirection` es
`left` /`right`/`up`/`down` (por defecto `'right'` ). No hay un solo `start` /`end`
en su superficie, así que no promete seguir la dirección y no la incumple; misma
excepción legítima que el `side` físico del drawer. Radix hace igual. Aquí basta
la lectura porque lo que se comprueba es una AUSENCIA (no existe vocabulario
lógico), que es propiedad del código; no es el caso de `tabs` , donde había una
promesa lógica que el runtime incumplía.
### 6.11 · Los virtualizadores — el contenido DESAPARECÍA en RTL
Reportado por el usuario y reproducido: en `virtual-grid` , voltear la dirección
dejaba la rejilla en blanco. Medido en Chrome:
```
LTR : 108 celdas, 70 visibles, primera en x=381 (borde del viewport)
RTL : 108 celdas, 0 visibles, primera en x=-5101 (fuera de la pantalla)
```
No era `scrollLeft` —está en 0 en ambos—, era el anclaje de la celda:
```ts
position: absolute; top: 0; left: 0;
transform: translate3d(${columnStart}px, …)
```
El sizer interno es `inline-size: 6000px` , o sea **lógico** , así que en RTL
desborda hacia la izquierda y ocupa `[-5101, 899]` . Su borde físico izquierdo es
donde el contenido TERMINA, y `left: 0` mandaba todas las celdas allí.
Arreglado con ancla lógica (`inset-inline-start`) y el signo en el offset, para
que el recorrido siga en `translate3d` — un virtualizador recoloca miles de
celdas y conviene que siga en el compositor. Tras el arreglo: **70 visibles en
RTL**, primera en x=779 = borde derecho del viewport − ancho de celda.
**`virtual-list` tenía el mismo patrón** (`left: 0` + `translateX` ) y por tanto
el mismo defecto en modo horizontal. Mismo arreglo; el modo vertical no se ve
afectado (`inset-inline-start: 0` con `width: 100%` es lo que `left: 0` era).
Verificado: 12 items visibles en RTL, primero en x=755.
**El otro síntoma reportado —«el cambio de idioma no le afecta»— NO es un
defecto.** Con el toggle en `auto` el árabe voltea la lista correctamente
(medido: `htmlDir` y `direction` del componente pasan a `rtl` ). Si el toggle
está en `ltr` /`rtl` EXPLÍCITO, el idioma no manda: el intent gana sobre la
derivación, que es lo ratificado en `013ceac57` . Despista en la práctica, pero
es el contrato.
fix(rating-group,splitter): dos wrappers se quedaron fuera de 013ceac57, y el arrastre del splitter no volteaba
Verificados los tres componentes que "tenian codigo de direccion" pero nadie
habia comprobado que acertara. Dos estaban rotos.
RATING-GROUP, roto por DOS motivos encadenados.
1) calcFromPointer medía (clientX - rect.left) / rect.width, una fraccion desde
el borde FISICO izquierdo. En RTL los items van de derecha a izquierda, asi que
la mitad que EMPIEZA una estrella es la derecha: pinchar la primera mitad visual
daba la estrella entera. Volteado con el mismo `1 - pos` que ya usa el slider.
2) Y aun asi seguia sin funcionar, porque su WRAPPER se quedo fuera de la
normalizacion de 013ceac57: tenia `dir = 'ltr'` como default duro —que ademas
incumple "el atributo nunca tiene defecto"— y readableActive(() => dir) en vez
de activeDir(() => dir, soma). Nunca consultaba prefs, asi que resolvedDir valia
'ltr' pasara lo que pasara.
NATURAL-TIME-PICKER tenia el mismo agujero (dir ?? 'ltr'). Son los DOS unicos que
quedaron fuera; los otros 36 si usan activeDir. Medido tras arreglar ambos: en
RTL la mitad derecha da 2.5 y la izquierda la entera — espejo correcto.
SPLITTER: el arrastre estaba roto, el teclado no. resizePanels toma un delta
LOGICO —positivo agranda el panel de ANTES del handle— y recibia el delta fisico
del puntero sin voltear. En RTL arrastrar a la derecha agrandaba el panel que
debia encoger, y el arrastre CONTRADECIA a las flechas, que si pasan por
getDirectionalKeys. Tras el arreglo: LTR arrastre +118 / ArrowRight +6; RTL
arrastre -118 / ArrowRight -6 — espejo exacto y los dos de acuerdo.
NOTA DE METODO, anotada en el handoff: al ver `resolvedDir = opts.dir.current ??
'ltr'` en unos 30 providers di por hecho que a todos les faltaba el eslabon de
prefs. Es FALSO — la cadena vive en el WRAPPER (activeDir), y el provider recibe
la prop ya resuelta. El slider, que tiene ese mismo resolvedDir, voltea
perfectamente. Antes de "arreglar" 30 componentes, mirar el wrapper.
SIN VERIFICAR: scroll-area. Su resolvedDir aparece UNA sola vez —la
declaracion—, o sea es codigo muerto, y usa scrollLeft en cuatro sitios sin
tratar que en RTL los navegadores lo devuelven NEGATIVO (isAtLeft seria siempre
cierto, el ratio del thumb saldria negativo). No esta medido porque el
scroll-area de la demo no tiene eje horizontal.
Verificado: check 74 = linea base exacta. 20/20 en splitter + rating-group +
natural-time-picker.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### 6.12 · Los tres que «tenían código de dirección» — dos estaban rotos
**`rating-group`: roto, y por DOS motivos encadenados.**
1. `calcFromPointer` medía `(clientX - rect.left) / rect.width` , una fracción
desde el borde físico izquierdo. En RTL los items van de derecha a izquierda,
así que la mitad que EMPIEZA una estrella es la derecha: pinchar la primera
mitad visual daba la estrella entera. Volteado, igual que el slider.
2. Y aun así seguía sin funcionar, porque **su wrapper se quedó fuera de la
normalización de `013ceac57` **: tenía `dir = 'ltr'` como default duro y
`readableActive(() => dir)` en vez de `activeDir(() => dir, soma)` . O sea
nunca consultaba prefs y `resolvedDir` valía `'ltr'` siempre.
⚠️ ** `natural-time-picker` tenía el mismo agujero del wrapper** (`dir ?? 'ltr'`).
Son los DOS únicos que quedaron fuera; los otros 36 sí usan `activeDir` .
Medido tras arreglar ambos: en RTL la mitad derecha da 2.5 y la izquierda la
entera — espejo correcto.
**LECCIÓN DE MÉTODO**: al ver `resolvedDir = opts.dir.current ?? 'ltr'` en ~30
providers pensé que a todos les faltaba el eslabón de prefs. **Falso** : la cadena
vive en el WRAPPER (`activeDir(getter, soma)`), y el provider recibe la prop ya
resuelta. Antes de "arreglar" 30 componentes, mira el wrapper.
**`splitter`: el arrastre estaba roto, el teclado no.** `resizePanels` toma un
delta LÓGICO —positivo agranda el panel de ANTES del handle— y recibía el delta
físico del puntero sin voltear. Medido: en RTL arrastrar a la derecha agrandaba
el panel que debía encoger, y el arrastre **contradecía a las flechas** , que sí
pasan por `getDirectionalKeys` . Tras el arreglo: LTR arrastre +118 / `ArrowRight`
+6; RTL arrastre − 118 / `ArrowRight` − 6 — espejo exacto y ambos de acuerdo.
**`scroll-area`: SIN VERIFICAR, y con dos señales.** Su `resolvedDir` aparece
UNA sola vez —la declaración—, o sea es código muerto. Y usa `scrollLeft` en
cuatro sitios (`isAtLeft = scrollLeft < = 0`, el ratio del thumb, el salto por
click y el arrastre) sin tratar que en RTL los navegadores lo devuelven
**negativo**: `isAtLeft` sería siempre cierto y el ratio saldría negativo. No
está medido porque el scroll-area de la demo no tiene eje horizontal
(`maxScroll: 0`) — hace falta un ejemplo que sí lo tenga.
fix(carousel): en RTL el rail se movia AL CONTRARIO del dedo durante el arrastre
El usuario vio a mano lo que mi simulacion de puntero no consiguio disparar en
cinco intentos. El swipe estaba mal, y no donde yo habia mirado.
La decision de finishDrag SI voltea (isRtlHorizontal ? -offset : offset) y por
eso la daba por buena. El defecto estaba en el PINTADO durante el arrastre:
translate = base + dragOffset // base = indice, dragOffset = dedo
itemGroupTransform = translate * flip // el flip caia sobre LOS DOS
El recorrido por indice es LOGICO —N->N+1 mueve el rail a la izquierda en LTR y a
la derecha en RTL— y debe voltear. `dragOffset` es el desplazamiento FISICO del
puntero y no debe. Al voltear ambos, en RTL el rail se movia en sentido contrario
al dedo: arrastras a la derecha y el contenido huye a la izquierda. Separados:
solo baseTranslate lleva el signo.
A/B con git stash, midiendo el transform A MITAD del arrastre —que no exige
superar el umbral de gesto, y por eso ahora si es medible—: sin el arreglo, con
el dedo a +100px el rail da Δ-100 (huye); con el arreglo, Δ+100 (lo sigue). LTR
intacto en los dos casos.
NOTA DE METODO, anotada en el handoff: para un gesto con umbral de distancia y
velocidad, no intentes completar el swipe con page.mouse; mide el estado
INTERMEDIO con el puntero aun abajo. Es lo que me faltaba en §6.8.
DESCARTADOS CON MEDIDA, no se tocan:
scroll-area FUNCIONA. Confirmado por el usuario y medido: con el toggle en auto,
ES->ltr y AR->rtl, y el elemento pasa a direction:rtl. Su resolvedDir sigue
siendo codigo muerto y el uso de scrollLeft sin verificar en un eje horizontal
real, pero el componente responde a la direccion.
chart NO NECESITA ARREGLO y las referencias lo respaldan. Medido: chartDir,
legendDir y el texto de los ejes pasan de ltr a rtl; el eje numerico no se
voltea. Es el consenso: Chart.js implemento RTL SOLO para leyendas y tooltips y
mantiene abierto el issue de ejes/etiquetas (#11937, PR #6460), y el patron
documentado es eje numerico LTR con textos RTL. amCharts es de los pocos con RTL
integral.
Y el sintoma «la demo no reacciona al idioma», reportado tres veces
(virtual-list, carousel, scroll-area): medido en las tres, con el toggle en AUTO
las tres reaccionan. Si el toggle esta en ltr/rtl explicito el idioma no manda,
porque el intent gana sobre la derivacion. No es defecto, pero despista de forma
sistematica: parece un interruptor de dos posiciones y es un ciclo de tres donde
solo uno cede el mando al idioma, y el estado solo se lee en el title. Cerrarlo
seria trabajo de la shell, no del eje.
Verificado: check 74 = linea base exacta. 4/4 en carousel.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### 6.13 · El swipe del carousel SÍ estaba mal — el rail huía del dedo
Lo que §6.8 no consiguió disparar por simulación, el usuario lo vio a mano. El
defecto no estaba en la decisión de `finishDrag` (que sí voltea con
`isRtlHorizontal` ) sino en el pintado durante el arrastre:
```ts
translate = base + dragOffset; // base = índice, dragOffset = dedo
itemGroupTransform = translate * flip; // el flip caía sobre LOS DOS
fix(carousel): en RTL el rail se movia AL CONTRARIO del dedo durante el arrastre
El usuario vio a mano lo que mi simulacion de puntero no consiguio disparar en
cinco intentos. El swipe estaba mal, y no donde yo habia mirado.
La decision de finishDrag SI voltea (isRtlHorizontal ? -offset : offset) y por
eso la daba por buena. El defecto estaba en el PINTADO durante el arrastre:
translate = base + dragOffset // base = indice, dragOffset = dedo
itemGroupTransform = translate * flip // el flip caia sobre LOS DOS
El recorrido por indice es LOGICO —N->N+1 mueve el rail a la izquierda en LTR y a
la derecha en RTL— y debe voltear. `dragOffset` es el desplazamiento FISICO del
puntero y no debe. Al voltear ambos, en RTL el rail se movia en sentido contrario
al dedo: arrastras a la derecha y el contenido huye a la izquierda. Separados:
solo baseTranslate lleva el signo.
A/B con git stash, midiendo el transform A MITAD del arrastre —que no exige
superar el umbral de gesto, y por eso ahora si es medible—: sin el arreglo, con
el dedo a +100px el rail da Δ-100 (huye); con el arreglo, Δ+100 (lo sigue). LTR
intacto en los dos casos.
NOTA DE METODO, anotada en el handoff: para un gesto con umbral de distancia y
velocidad, no intentes completar el swipe con page.mouse; mide el estado
INTERMEDIO con el puntero aun abajo. Es lo que me faltaba en §6.8.
DESCARTADOS CON MEDIDA, no se tocan:
scroll-area FUNCIONA. Confirmado por el usuario y medido: con el toggle en auto,
ES->ltr y AR->rtl, y el elemento pasa a direction:rtl. Su resolvedDir sigue
siendo codigo muerto y el uso de scrollLeft sin verificar en un eje horizontal
real, pero el componente responde a la direccion.
chart NO NECESITA ARREGLO y las referencias lo respaldan. Medido: chartDir,
legendDir y el texto de los ejes pasan de ltr a rtl; el eje numerico no se
voltea. Es el consenso: Chart.js implemento RTL SOLO para leyendas y tooltips y
mantiene abierto el issue de ejes/etiquetas (#11937, PR #6460), y el patron
documentado es eje numerico LTR con textos RTL. amCharts es de los pocos con RTL
integral.
Y el sintoma «la demo no reacciona al idioma», reportado tres veces
(virtual-list, carousel, scroll-area): medido en las tres, con el toggle en AUTO
las tres reaccionan. Si el toggle esta en ltr/rtl explicito el idioma no manda,
porque el intent gana sobre la derivacion. No es defecto, pero despista de forma
sistematica: parece un interruptor de dos posiciones y es un ciclo de tres donde
solo uno cede el mando al idioma, y el estado solo se lee en el title. Cerrarlo
seria trabajo de la shell, no del eje.
Verificado: check 74 = linea base exacta. 4/4 en carousel.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
```
El recorrido por índice es LÓGICO y debe voltear; `dragOffset` es el
desplazamiento FÍSICO del puntero y no. Al voltear ambos, en RTL el rail se movía
en sentido contrario al dedo. Separados: sólo `baseTranslate` lleva el signo.
A/B con `git stash` , midiendo el transform A MITAD del arrastre (que no exige
superar el umbral de gesto, y por eso ahora sí es medible): sin el arreglo, RTL
da Δ− 100 con el dedo a +100 —huye—; con él, Δ+100 —lo sigue—. LTR intacto en
ambos casos.
**MÉTODO**: para un gesto con umbral de distancia/velocidad, no intentes
completar el swipe; mide el estado INTERMEDIO con el puntero aún abajo.
### 6.14 · Descartados con medida — no tocar
- **`scroll-area` funciona.** Confirmado por el usuario y medido: con el toggle
en `auto` , ES→ltr y AR→rtl, y el elemento pasa a `direction: rtl` . Su
`resolvedDir` sigue siendo código muerto (una sola aparición) y el uso de
`scrollLeft` sigue sin verificarse en un eje horizontal real — pero el
componente responde a la dirección.
- **`chart` no necesita arreglo, y las referencias lo respaldan.** Medido:
`chartDir` , `legendDir` y el texto de los ejes pasan de `ltr` a `rtl` ; el eje
numérico no se voltea. Es exactamente el consenso: Chart.js implementó RTL
SÓLO para leyendas y tooltips y mantiene abierto el issue de ejes/etiquetas
(chartjs/Chart.js#11937, PR #6460 ); el patrón documentado es eje numérico LTR
con textos RTL. amCharts es de los pocos con RTL integral.
### 6.15 · El síntoma «la demo no reacciona al idioma» — es el TOGGLE
Reportado tres veces (virtual-list, carousel, scroll-area) y medido en las tres:
**con el toggle en `auto` , las tres reaccionan al idioma**. Si el toggle está en
`ltr` /`rtl` EXPLÍCITO, el idioma no manda — el intent gana sobre la derivación,
que es lo ratificado en `013ceac57` .
No es un defecto, pero **despista de forma sistemática** : el control parece un
interruptor de dos posiciones y en realidad es un ciclo de tres donde sólo uno
cede el mando al idioma, y el estado sólo se lee en el `title` . Si se quiere
cerrar, es trabajo de la shell (hacer visible el estado), no del eje.
fix(float-panel): la semilla anclada usa la matematica de $ethereal en vez de una copia sin direccion
Verificados uno a uno los cinco componentes de la §2.2 del handoff, y despues
barrido el punto ciego del guard. Ningun cambio de comportamiento salvo el de
float-panel.
LOS CINCO DE §2.2 ESTAN SANOS. slider: click al 25% del ancho FISICO da 75 en
RTL, arrastrar a la derecha sube en LTR y baja en RTL, y ArrowRight igual.
number-field y css-field: el mismo arrastre fisico sube en LTR y baja en RTL.
dropdown-menu: el panel raiz se alinea al inline-start y el submenu abre a la
derecha en LTR y a la izquierda en RTL. carousel: el track voltea (Next mueve
-622px en LTR y +622px en RTL). Ninguno necesito arreglo.
EL SWIPE DEL CAROUSEL NO ESTA MEDIDO. Cinco intentos de simulacion de puntero y
no consegui disparar el gesto de forma reproducible ni en LTR: la capa tiene
umbral de distancia y de velocidad. El signo esta bien por LECTURA
(isRtlHorizontal ? -offset : offset). Son diez segundos con un raton de verdad.
UN MOTOR CUBRE NUEVE COMPONENTES. soma/layers/floating lo consumen combobox,
context-menu, dropdown-menu, link-preview, menubar, popover, select, sidebar y
tooltip. Verificado a traves de popover (align=end: derecha en LTR, izquierda en
RTL) y menubar (align=start, al reves). Los nueve quedan cubiertos.
FLOAT-PANEL NO USA ESE MOTOR, Y HACE BIEN: useFloating mantiene el panel PEGADO
a su ancla, y un float-panel se arrastra y redimensiona libremente. Pero su
SEMILLA anclada si era redundante: computeInitialPosition() re-derivaba
side + align contra el rect a mano, y esa copia resolvia align:'start' a r.left
SIEMPRE — una alineacion LOGICA clavada a un borde FISICO, que nunca voltea.
$ethereal ya exporta computeCoordsFromPlacement(rects, placement, rtl): pura,
sincrona, sin ciclo de vida, y es la misma que usa el motor compartido. Migrada
la semilla a ella. Se comparte la semilla, NO useFloating.
anchored-seed.test.ts fija la migracion, porque la demo NO ancla ningun panel y
el defecto no era observable ahi: en LTR el resultado es IDENTICO al algoritmo
anterior en las 12 combinaciones side x align (cero regresion); en RTL con side
vertical start y end se espejan; con side horizontal el align no cambia, que es
el eje de bloque; y center sigue centrado en ambas. La demo, medida antes y
despues, no se mueve.
NO TOCADO, hace falta criterio: Home/End en el eje X llevan el panel a
bounds.left / bounds.right. Para un panel que se arrastra libremente es
defendible que sean extremos fisicos y no principio/final de lectura.
Descartados como falsos positivos tras mirar el uso real: container (prop de
margenes), text-focus, text-scramble y path-trace (miden donde esta algo para un
efecto), drag-drop (sueltas donde ves), cropper (paneas una imagen que no se
voltea) y gradient-builder (arrastras la parada donde la ves). Y drawer, que no
aparecia en el primer cruce, esta BIEN y es el ejemplo a imitar: traduce
start/end a left/right una sola vez en resolveDirection() y desde ahi todo es
fisico y coherente — aunque su demo solo expone valores fisicos, asi que su
mitad logica no es observable.
Verificado: check 74 = linea base exacta. 12/12 en float-panel + 4/4 del test
nuevo.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
feat(direction): un guard para el eje logico-fisico, y el <main> que congelaba las 51 demos
Cierra la §2.1 del handoff. El guard nacio en rojo con 24 hallazgos y al
arreglarlos aparecieron dos defectos mas grandes que el que buscaba.
1) EL GUARD RTL-1. Nada cruzaba un ancla LOGICA con un desplazamiento FISICO
en la misma regla CSS: eidos-lint clasifica SELECTORES, no declaraciones, y la
fila RTL del contrato de construccion decia "rule (LIVE)" — revision humana.
Por eso el defecto del slider llego a produccion y lo cazo el ojo del usuario.
Ahora `npm run rtl:check`, con la logica en src/uix/eidos/rtl-lint.ts (14
tests) para que sea testeable, calcando el par lint.ts / scripts/eidos-lint.ts.
El test ancla el caso historico: slider.css ANTES de 013ceac57 sale rojo y el
arreglo que se commiteo sale verde.
Dos desviaciones del enunciado, deliberadas. AMPLIA a la propiedad
independiente `translate:`, que se usa MAS que `transform: translate*` (87
frente a 36) y tiene el mismo defecto — ocho de los 24 hallazgos eran de esa
forma. RECORTA `padding-inline*`: no mueve la caja contra su ancla, asi que no
puede pelear con un translate; solo anadia ruido.
2) LA COSECHA, 24 -> 1. El unico que queda es palabras, auditado y no tocado.
Los 23 caen en tres familias: CENTRADO (10) donde el resultado deseado es
simetrico y basta dejar de descentrar; DIRECCIONAL (6) donde el signo debe
voltear; y FISICO (7) donde lo que sobraba era el ancla logica — handles de
brujula del cropper sobre una imagen que no se voltea, y el arco polar del
menu-dial. Medido en Chrome el switch: el thumb salia a +24px en un carril de
38px, colgando fuera. Verificados tambien carousel, avatar, cropper, menu-dial
y chat-log.
3) :dir() ES LA DOCTRINA, [dir='rtl'] QUEDA PROHIBIDO. Los 12 selectores que
habia en eidos estaban TODOS rotos, en los dos sentidos opuestos del mismo
error, y ambos medidos en Chrome. Los 5 descendientes se aplican DE MAS: el
sidebar tenia direction:ltr de verdad y aun asi matcheaba, porque el selector
capta cualquier ancestro RTL e ignora un dir mas cercano que redeclare. Los 7
de mismo-elemento NO se aplican NUNCA: el tree-view era rtl de verdad y no
matcheaba, porque desde 013ceac57 el atributo solo se estampa si alguien lo
afirmo. :dir() acierta los tres casos porque mira la direccion RESUELTA.
Baseline desde 2023.
4) EL PIN DEL <main>. 5469e05df hizo dos cosas que se anulan: puso las demos en
`auto` —sin prop dir, o sea el componente no estampa atributo y HEREDA del
DOM— y a la vez clavo <main data-uix-canvas dir="ltr"> sobre ellas. Ese <main>
era la herencia, asi que el toggle del topbar y el arabe llegaban a <html> y
morian ahi: las 51 demos congeladas en LTR. Su comentario afirmaba que "los
componentes siguen las prefs, este contenedor no les afecta" — cierto para la
LOGICA, falso para el CSS, que lee el direction COMPUTADO. Medido: html
dir="rtl" con el panel del sidebar todavia a la izquierda, y quitar ese
atributo y nada mas lo movia. Se queda el lang="en". Coste aceptado por el
usuario: vuelve la puntuacion desplazada en la prosa inglesa al elegir RTL, que
es cosmetico y solo en un modo de inspeccion; demos que no pueden demostrar,
no. Si hay que fijar la prosa otra vez, el pin va alrededor de la PROSA.
Verificado: check 74 = linea base exacta en las tres pasadas. rtl-lint 14/14.
Suite de eidos 375 pasan; el fallo de lint.test.ts por audio-player es
PREEXISTENTE, comprobado con stash, y es del hilo del sonido. Los 9 CSS tocados
ya fallaban prettier antes de tocarlos, asi que no se formatean.
SIN VERIFICAR EN NAVEGADOR, anotado en el handoff: archetypes y proof-of-human
(sus reglas viven en @media (pointer: coarse), invisible en escritorio) y
sliding-indicator (la demo del radio-group segmented no llegaba a medir).
QUEDA: hay wrappers estampando dir="" vacio en vez de omitir el atributo —
visto en sidebar y tree-view; no rompe porque un valor invalido hereda, pero
contradice la letra del contrato.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### 6.5 · Suelto, para quien siga
- **Hay componentes estampando `dir=""` ** (cadena vacía) en vez de omitir el
atributo — visto en el wrapper del `sidebar` y en `tree-view` . No rompe nada
(valor inválido ⇒ hereda), pero contradice la letra del contrato y hace que
`[dir]` matchee. Sin investigar de dónde sale.
- `src/uix/eidos/lint.test.ts` falla por `audio-player` — **preexistente** ,
verificado con stash. Es del hilo del sonido, no de este eje.
- Los 9 CSS tocados ya fallaban `prettier --check` ANTES de tocarlos. No los
formatees: son ficheros enteros de ruido (§5).
docs(direction): cierre del barrido de graficas, la regla de RTL en SVG, y la cola para manana
LAS 11 GRAFICAS DEL CATALOGO, REVISADAS. Ninguna de las siete que quedaban
necesitaba cambios, y por razones distintas que conviene dejar escritas:
bar-list ya lo resuelve con CSS logico; sparkline y bubble-chart heredan el
frame de <Chart> arreglado en 55070c688; y gauge, pie-chart, polar-area y
smith-chart son RADIALES — sus posiciones son angulos, no tienen eje de lectura
y es correcto que no espejen.
LA REGLA, ahora en la fila "RTL · SVG" del contrato de construccion y
desarrollada en eidos/components/chart/README.md §Direction. Son DOS decisiones
y no son la misma:
1) ¿espeja la composicion? solo si hay EJE DE LECTURA. Se invierte el rango de
pixeles de la escala, o se refleja la x. Los valores de Y no se espejan
nunca — un grafico es una secuencia leida como lee el lector, no un espejo.
2) `text-anchor` es LOGICO: si la composicion espeja se deja tal cual (voltear
ademas lo cancela); si no espeja se fuerza el fisico.
TRAMPAS DE MEDICION que me engañaron durante toda la sesion, en §7.3: el bbox de
un path espejado es identico al del original —mi "fingerprint" declaro sparkline
sin espejar cuando si lo hacia—; un test de "se sale del SVG" no ve una invasion
hacia DENTRO; el hueco del eje Y hay que medirlo contra el TICK y no contra la
linea; verificar en LTR no revela NADA de un defecto de RTL; y la pagina
"heatmap" tiene dos modos tras un control, asi que capturar el primer svg solo
enseñaba uno.
HANDOFF. La cabecera apunta a la §8 nueva, que es la cola priorizada: scroll-area
(dos señales, sin medir, y le falta una demo con eje horizontal), los agujeros de
OBSERVABILIDAD que impiden verificar drawer y float-panel, las verificaciones
pendientes, y la deuda ajena por gravedad —encabezada por los 91 ficheros de test
que falsifican Soma.require()—. Y una §8.5 con lo que NO hay que rehacer, para
que nadie repita el trabajo medido.
check 74 = linea base exacta.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
---
## 7. Las gráficas — barrido completo del catálogo (2026-08-03)
El usuario abrió este frente con «los chart calculan mal» y una captura. **Me
costó cuatro avisos suyos dar con lo que señalaba**, y la causa de las tres
primeras equivocaciones fue siempre la misma: verificar en LTR, con números, y
sobre un recorte que yo elegía. La regla que sale de aquí está en la fila
**RTL · SVG** del contrato y desarrollada en
`src/uix/eidos/components/chart/README.md` §Direction.
### 7.1 · Las dos decisiones, que NO son la misma
1. ** ¿Espeja la composición?** Sólo si el gráfico tiene EJE DE LECTURA. Lo
tienen `chart` (escala x), `calendar-heatmap` , la matriz de `heatmap` , el
canalón del `funnel` y `bar-list` . NO lo tienen los radiales (`gauge`,
`pie-chart` , `polar-area` , `radar-chart` , `smith-chart` ): sus posiciones son
ángulos. Se espeja invirtiendo el RANGO de píxeles de la escala, o
reflejando la x (`mx()`). **Los valores de Y no se espejan nunca.**
2. ** `text-anchor` es LÓGICO.** Si la composición espeja, se deja lógico —
voltearlo además lo cancela. Si NO espeja (canalón del eje Y, radar), se
fuerza el físico con `rtl.anchor()` .
Confundirlas es silencioso y se ve como las etiquetas caminando sobre el
gráfico.
### 7.2 · Estado del catálogo — TODAS revisadas
| | Estado |
| ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `chart` + presets (line/area/bar/bubble/scatter) | Arreglado: eje X espeja · tooltip hereda `formatY` · canalón adaptativo · etiquetas a 9.4px del tick · anchor físico |
| `funnel` | Arreglado: composición espejada (embudo derecha, etiquetas izquierda) |
| `calendar-heatmap` | Arreglado: semanas y canalón de días espejados |
| `heatmap` (matriz) | Arreglado: columnas y canalón de filas espejados |
| `radar-chart` | Arreglado: anchor físico, sin espejar (es radial) |
| `bar-list` | Correcto sin tocar — CSS lógico |
| `sparkline` , `bubble-chart` | Correctos sin tocar — heredan el frame de `chart` |
| `gauge` , `pie-chart` , `polar-area` , `smith-chart` | Correctos sin tocar — radiales |
docs(direction): cierre del barrido de graficas, la regla de RTL en SVG, y la cola para manana
LAS 11 GRAFICAS DEL CATALOGO, REVISADAS. Ninguna de las siete que quedaban
necesitaba cambios, y por razones distintas que conviene dejar escritas:
bar-list ya lo resuelve con CSS logico; sparkline y bubble-chart heredan el
frame de <Chart> arreglado en 55070c688; y gauge, pie-chart, polar-area y
smith-chart son RADIALES — sus posiciones son angulos, no tienen eje de lectura
y es correcto que no espejen.
LA REGLA, ahora en la fila "RTL · SVG" del contrato de construccion y
desarrollada en eidos/components/chart/README.md §Direction. Son DOS decisiones
y no son la misma:
1) ¿espeja la composicion? solo si hay EJE DE LECTURA. Se invierte el rango de
pixeles de la escala, o se refleja la x. Los valores de Y no se espejan
nunca — un grafico es una secuencia leida como lee el lector, no un espejo.
2) `text-anchor` es LOGICO: si la composicion espeja se deja tal cual (voltear
ademas lo cancela); si no espeja se fuerza el fisico.
TRAMPAS DE MEDICION que me engañaron durante toda la sesion, en §7.3: el bbox de
un path espejado es identico al del original —mi "fingerprint" declaro sparkline
sin espejar cuando si lo hacia—; un test de "se sale del SVG" no ve una invasion
hacia DENTRO; el hueco del eje Y hay que medirlo contra el TICK y no contra la
linea; verificar en LTR no revela NADA de un defecto de RTL; y la pagina
"heatmap" tiene dos modos tras un control, asi que capturar el primer svg solo
enseñaba uno.
HANDOFF. La cabecera apunta a la §8 nueva, que es la cola priorizada: scroll-area
(dos señales, sin medir, y le falta una demo con eje horizontal), los agujeros de
OBSERVABILIDAD que impiden verificar drawer y float-panel, las verificaciones
pendientes, y la deuda ajena por gravedad —encabezada por los 91 ficheros de test
que falsifican Soma.require()—. Y una §8.5 con lo que NO hay que rehacer, para
que nadie repita el trabajo medido.
check 74 = linea base exacta.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
Y los bloques de código de las demos (`pre/code/kbd/samp`) se declaran `ltr` :
el bidi los volvía ilegibles (`< script lang = 'ts' > ` → `<'script lang='ts>` ). Es
la versión ESTRECHA del pin que llevaba `<main data-uix-canvas>` .
### 7.3 · Trampas de medición — me engañaron tres veces
- **El bbox de un path espejado es idéntico al del original.** Mi «fingerprint»
por `getBoundingClientRect` declaró `sparkline` sin espejar cuando sí lo
hacía. Para un espejado hay que mirar la PRIMERA coordenada (`M0,…` vs
`M186,…` ), no la caja.
- **Un desborde hacia DENTRO no sale en un test de «se sale del SVG».** El
barrido que escribí buscaba texto fuera del svg y dio 0 en todo mientras las
etiquetas invadían el área de trazado.
- **Medir contra el elemento equivocado.** El hueco del eje Y lo medí contra la
LÍNEA del eje (7px, aceptable) cuando lo que toca es medir contra el TICK, que
sale 4px hacia la etiqueta: quedaban 3.4px y se leía pegado.
- **Verificar en LTR un defecto de RTL.** Tres veces. El `text-anchor` lógico no
se manifiesta en LTR en absoluto.
- **La página completa, no el primer SVG.** La ruta `heatmap` tiene DOS modos
(`matrix` / `calendar` ) tras un control; capturando el primer svg sólo veía uno.
### 7.4 · QUEDA en las gráficas
- `radar-chart` : «Economy» empieza en **x=-6** , o sea se sale unos píxeles del
SVG **también en LTR** . Preexistente, no tocado.
- No hay NINGÚN test de chart en el repo. Todo lo de esta sesión está verificado
a ojo y con medidas puntuales, no fijado por tests.
- ~~Sin revisar en RTL: `metrics` , `bar-segment` , `chart-legend` .~~ **Revisadas
en §9.6** — y no eran inocentes.
docs(direction): cierre del barrido de graficas, la regla de RTL en SVG, y la cola para manana
LAS 11 GRAFICAS DEL CATALOGO, REVISADAS. Ninguna de las siete que quedaban
necesitaba cambios, y por razones distintas que conviene dejar escritas:
bar-list ya lo resuelve con CSS logico; sparkline y bubble-chart heredan el
frame de <Chart> arreglado en 55070c688; y gauge, pie-chart, polar-area y
smith-chart son RADIALES — sus posiciones son angulos, no tienen eje de lectura
y es correcto que no espejen.
LA REGLA, ahora en la fila "RTL · SVG" del contrato de construccion y
desarrollada en eidos/components/chart/README.md §Direction. Son DOS decisiones
y no son la misma:
1) ¿espeja la composicion? solo si hay EJE DE LECTURA. Se invierte el rango de
pixeles de la escala, o se refleja la x. Los valores de Y no se espejan
nunca — un grafico es una secuencia leida como lee el lector, no un espejo.
2) `text-anchor` es LOGICO: si la composicion espeja se deja tal cual (voltear
ademas lo cancela); si no espeja se fuerza el fisico.
TRAMPAS DE MEDICION que me engañaron durante toda la sesion, en §7.3: el bbox de
un path espejado es identico al del original —mi "fingerprint" declaro sparkline
sin espejar cuando si lo hacia—; un test de "se sale del SVG" no ve una invasion
hacia DENTRO; el hueco del eje Y hay que medirlo contra el TICK y no contra la
linea; verificar en LTR no revela NADA de un defecto de RTL; y la pagina
"heatmap" tiene dos modos tras un control, asi que capturar el primer svg solo
enseñaba uno.
HANDOFF. La cabecera apunta a la §8 nueva, que es la cola priorizada: scroll-area
(dos señales, sin medir, y le falta una demo con eje horizontal), los agujeros de
OBSERVABILIDAD que impiden verificar drawer y float-panel, las verificaciones
pendientes, y la deuda ajena por gravedad —encabezada por los 91 ficheros de test
que falsifican Soma.require()—. Y una §8.5 con lo que NO hay que rehacer, para
que nadie repita el trabajo medido.
check 74 = linea base exacta.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
---
## 8. La cola para mañana — por orden de valor
### 8.1 · `scroll-area` — **CERRADA** (ver §9)
Las dos señales eran **la misma cosa** y ambas estaban rotas. Lo de abajo es el
enunciado original, conservado; el cierre está en §9.
docs(direction): cierre del barrido de graficas, la regla de RTL en SVG, y la cola para manana
LAS 11 GRAFICAS DEL CATALOGO, REVISADAS. Ninguna de las siete que quedaban
necesitaba cambios, y por razones distintas que conviene dejar escritas:
bar-list ya lo resuelve con CSS logico; sparkline y bubble-chart heredan el
frame de <Chart> arreglado en 55070c688; y gauge, pie-chart, polar-area y
smith-chart son RADIALES — sus posiciones son angulos, no tienen eje de lectura
y es correcto que no espejen.
LA REGLA, ahora en la fila "RTL · SVG" del contrato de construccion y
desarrollada en eidos/components/chart/README.md §Direction. Son DOS decisiones
y no son la misma:
1) ¿espeja la composicion? solo si hay EJE DE LECTURA. Se invierte el rango de
pixeles de la escala, o se refleja la x. Los valores de Y no se espejan
nunca — un grafico es una secuencia leida como lee el lector, no un espejo.
2) `text-anchor` es LOGICO: si la composicion espeja se deja tal cual (voltear
ademas lo cancela); si no espeja se fuerza el fisico.
TRAMPAS DE MEDICION que me engañaron durante toda la sesion, en §7.3: el bbox de
un path espejado es identico al del original —mi "fingerprint" declaro sparkline
sin espejar cuando si lo hacia—; un test de "se sale del SVG" no ve una invasion
hacia DENTRO; el hueco del eje Y hay que medirlo contra el TICK y no contra la
linea; verificar en LTR no revela NADA de un defecto de RTL; y la pagina
"heatmap" tiene dos modos tras un control, asi que capturar el primer svg solo
enseñaba uno.
HANDOFF. La cabecera apunta a la §8 nueva, que es la cola priorizada: scroll-area
(dos señales, sin medir, y le falta una demo con eje horizontal), los agujeros de
OBSERVABILIDAD que impiden verificar drawer y float-panel, las verificaciones
pendientes, y la deuda ajena por gravedad —encabezada por los 91 ficheros de test
que falsifican Soma.require()—. Y una §8.5 con lo que NO hay que rehacer, para
que nadie repita el trabajo medido.
check 74 = linea base exacta.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
Dos señales, ninguna verificada:
docs(direction): cierre del barrido de graficas, la regla de RTL en SVG, y la cola para manana
LAS 11 GRAFICAS DEL CATALOGO, REVISADAS. Ninguna de las siete que quedaban
necesitaba cambios, y por razones distintas que conviene dejar escritas:
bar-list ya lo resuelve con CSS logico; sparkline y bubble-chart heredan el
frame de <Chart> arreglado en 55070c688; y gauge, pie-chart, polar-area y
smith-chart son RADIALES — sus posiciones son angulos, no tienen eje de lectura
y es correcto que no espejen.
LA REGLA, ahora en la fila "RTL · SVG" del contrato de construccion y
desarrollada en eidos/components/chart/README.md §Direction. Son DOS decisiones
y no son la misma:
1) ¿espeja la composicion? solo si hay EJE DE LECTURA. Se invierte el rango de
pixeles de la escala, o se refleja la x. Los valores de Y no se espejan
nunca — un grafico es una secuencia leida como lee el lector, no un espejo.
2) `text-anchor` es LOGICO: si la composicion espeja se deja tal cual (voltear
ademas lo cancela); si no espeja se fuerza el fisico.
TRAMPAS DE MEDICION que me engañaron durante toda la sesion, en §7.3: el bbox de
un path espejado es identico al del original —mi "fingerprint" declaro sparkline
sin espejar cuando si lo hacia—; un test de "se sale del SVG" no ve una invasion
hacia DENTRO; el hueco del eje Y hay que medirlo contra el TICK y no contra la
linea; verificar en LTR no revela NADA de un defecto de RTL; y la pagina
"heatmap" tiene dos modos tras un control, asi que capturar el primer svg solo
enseñaba uno.
HANDOFF. La cabecera apunta a la §8 nueva, que es la cola priorizada: scroll-area
(dos señales, sin medir, y le falta una demo con eje horizontal), los agujeros de
OBSERVABILIDAD que impiden verificar drawer y float-panel, las verificaciones
pendientes, y la deuda ajena por gravedad —encabezada por los 91 ficheros de test
que falsifican Soma.require()—. Y una §8.5 con lo que NO hay que rehacer, para
que nadie repita el trabajo medido.
check 74 = linea base exacta.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- Su `resolvedDir` aparece **una sola vez** en todo el componente — la
declaración. Es código muerto.
- Usa `scrollLeft` en cuatro sitios (`isAtLeft = scrollLeft < = 0`, el ratio del
thumb, el salto por click, el arrastre) sin tratar que **en RTL los
navegadores lo devuelven NEGATIVO**: `isAtLeft` sería siempre cierto y el
ratio del thumb saldría negativo.
⚠️ No está medido porque **el scroll-area de la demo no tiene eje horizontal**
(`maxScroll: 0`). Primero hace falta un ejemplo con scroll horizontal; sin eso
no es observable. El usuario ya confirmó que el componente «funciona» en lo
demás, y medido: con el toggle en `auto` , ES→ltr y AR→rtl llegan bien.
### 8.2 · Agujeros de OBSERVABILIDAD — **CERRADA** (ver §9.5)
docs(direction): cierre del barrido de graficas, la regla de RTL en SVG, y la cola para manana
LAS 11 GRAFICAS DEL CATALOGO, REVISADAS. Ninguna de las siete que quedaban
necesitaba cambios, y por razones distintas que conviene dejar escritas:
bar-list ya lo resuelve con CSS logico; sparkline y bubble-chart heredan el
frame de <Chart> arreglado en 55070c688; y gauge, pie-chart, polar-area y
smith-chart son RADIALES — sus posiciones son angulos, no tienen eje de lectura
y es correcto que no espejen.
LA REGLA, ahora en la fila "RTL · SVG" del contrato de construccion y
desarrollada en eidos/components/chart/README.md §Direction. Son DOS decisiones
y no son la misma:
1) ¿espeja la composicion? solo si hay EJE DE LECTURA. Se invierte el rango de
pixeles de la escala, o se refleja la x. Los valores de Y no se espejan
nunca — un grafico es una secuencia leida como lee el lector, no un espejo.
2) `text-anchor` es LOGICO: si la composicion espeja se deja tal cual (voltear
ademas lo cancela); si no espeja se fuerza el fisico.
TRAMPAS DE MEDICION que me engañaron durante toda la sesion, en §7.3: el bbox de
un path espejado es identico al del original —mi "fingerprint" declaro sparkline
sin espejar cuando si lo hacia—; un test de "se sale del SVG" no ve una invasion
hacia DENTRO; el hueco del eje Y hay que medirlo contra el TICK y no contra la
linea; verificar en LTR no revela NADA de un defecto de RTL; y la pagina
"heatmap" tiene dos modos tras un control, asi que capturar el primer svg solo
enseñaba uno.
HANDOFF. La cabecera apunta a la §8 nueva, que es la cola priorizada: scroll-area
(dos señales, sin medir, y le falta una demo con eje horizontal), los agujeros de
OBSERVABILIDAD que impiden verificar drawer y float-panel, las verificaciones
pendientes, y la deuda ajena por gravedad —encabezada por los 91 ficheros de test
que falsifican Soma.require()—. Y una §8.5 con lo que NO hay que rehacer, para
que nadie repita el trabajo medido.
check 74 = linea base exacta.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **`drawer`**: su demo sólo expone `top/right/bottom/left` . La mitad LÓGICA
(`start`/`end`), que es la única que ejercita RTL, no se puede ver. El
componente está bien por lectura (`resolveDirection()` es el ejemplo a imitar)
y verificado sólo en su mitad física.
- **`float-panel`**: su demo no ancla ningún panel, así que la semilla anclada
—lo que se arregló en `caf3cfd88` — no es observable. Cubierto por
`anchored-seed.test.ts` .
- **§2.3**: cablear `secondaryValue` en la demo del slider.
### 8.3 · Verificaciones que quedaron sin hacer
docs(direction): 8.3 cerrada — coarse eran 6 media queries, no 2, y el swipe con raton real
Seccion 9.9. Dos cierres y un hallazgo del usuario:
- `pointer: coarse`: el catalogo tiene SEIS media queries, no las dos que 8.3
daba por unicas. Las cuatro sin mirar (list-surface, chat-message,
radio-group, rotate-align) no tienen ninguna geometria de eje inline —
`--list-item-height`, `opacity`, `min-block-size` e `inset: 0` + `min-*`
logicos. Las dos que si la tenian son justo las que 6.2 midio con hit-test.
Cerrado por lectura, que aqui basta porque lo que se comprueba es una
AUSENCIA.
- El swipe del carousel con el raton REAL del navegador: las 4 combinaciones
dan espejo exacto. Cierra lo que 6.8 no consiguio disparar por simulacion.
MÉTODO: el raton real avanza 2 posiciones de golpe (momentum) y
`left_click_drag` no deja fijar la velocidad — lo que se comprueba es el
SIGNO, no la magnitud. Y verifica la direccion ANTES de cada gesto: mi
primera pasada midio creyendo estar en LTR con el toggle en RTL, y el
resultado parecia un defecto inexistente.
- Las flechas del carousel en RTL (arreglo en el commit anterior).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- ~~`archetypes` y `proof-of-human` : el resto de esa media query.~~ **CERRADO
(§9.9)**: hay **6** media queries `pointer: coarse` en el catálogo, no 2. Las
4 restantes (`list-surface`, `chat-message` , `radio-group` , `rotate-align` )
no tienen NINGUNA geometría de eje inline — `--list-item-height` , `opacity` ,
`min-block-size` e `inset: 0` + `min-*` lógicos. Las dos que sí la tenían son
justo las que §6.2 midió con hit-test.
- ~~**El swipe del `carousel` con un ratón de verdad.**~~ **CERRADO (§9.9)** —
con el ratón real del navegador, las 4 combinaciones dan espejo exacto.
- ~~`metrics`, `bar-segment` , `chart-legend` como piezas sueltas en RTL.~~
**CERRADO — ver §9.6.** Destapó un defecto de verdad en el preset `grow-x` .
docs(direction): cierre del barrido de graficas, la regla de RTL en SVG, y la cola para manana
LAS 11 GRAFICAS DEL CATALOGO, REVISADAS. Ninguna de las siete que quedaban
necesitaba cambios, y por razones distintas que conviene dejar escritas:
bar-list ya lo resuelve con CSS logico; sparkline y bubble-chart heredan el
frame de <Chart> arreglado en 55070c688; y gauge, pie-chart, polar-area y
smith-chart son RADIALES — sus posiciones son angulos, no tienen eje de lectura
y es correcto que no espejen.
LA REGLA, ahora en la fila "RTL · SVG" del contrato de construccion y
desarrollada en eidos/components/chart/README.md §Direction. Son DOS decisiones
y no son la misma:
1) ¿espeja la composicion? solo si hay EJE DE LECTURA. Se invierte el rango de
pixeles de la escala, o se refleja la x. Los valores de Y no se espejan
nunca — un grafico es una secuencia leida como lee el lector, no un espejo.
2) `text-anchor` es LOGICO: si la composicion espeja se deja tal cual (voltear
ademas lo cancela); si no espeja se fuerza el fisico.
TRAMPAS DE MEDICION que me engañaron durante toda la sesion, en §7.3: el bbox de
un path espejado es identico al del original —mi "fingerprint" declaro sparkline
sin espejar cuando si lo hacia—; un test de "se sale del SVG" no ve una invasion
hacia DENTRO; el hueco del eje Y hay que medirlo contra el TICK y no contra la
linea; verificar en LTR no revela NADA de un defecto de RTL; y la pagina
"heatmap" tiene dos modos tras un control, asi que capturar el primer svg solo
enseñaba uno.
HANDOFF. La cabecera apunta a la §8 nueva, que es la cola priorizada: scroll-area
(dos señales, sin medir, y le falta una demo con eje horizontal), los agujeros de
OBSERVABILIDAD que impiden verificar drawer y float-panel, las verificaciones
pendientes, y la deuda ajena por gravedad —encabezada por los 91 ficheros de test
que falsifican Soma.require()—. Y una §8.5 con lo que NO hay que rehacer, para
que nadie repita el trabajo medido.
check 74 = linea base exacta.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### 8.4 · Deuda ajena al eje, por gravedad
1. **91 ficheros de test falsifican `Soma.require()`** con
`vi.spyOn(Soma, 'require').mockReturnValue({ prefs: { getDir: () => dir } })` .
Incumple una Regla Crítica de CLAUDE.md: esa parte de la batería **no
ejercita el código real**. Es un proyecto entero y es lo más grave del
documento.
2. **No hay NINGÚN test de chart** en el repo. Todo lo de §7 está verificado a
ojo, no fijado.
3. `html lang` no sigue al idioma · componentes estampando `dir=""` vacío
(`sidebar`, `tree-view` ).
4. ** `smoke` y `perm:check` sin ejecutar** en toda la sesión.
### 8.5 · Lo que NO hay que rehacer
- Las 5 de §2.2 (slider, number-field, css-field, dropdown-menu, carousel) están
**medidas y sanas** ; el único defecto era el pintado del swipe, ya arreglado.
- El motor `soma/layers/floating` está verificado y cubre sus 9 consumidores.
- `toast` es correcto: su vocabulario es físico y coherente consigo mismo.
- Las 11 gráficas del catálogo están revisadas (§7.2).
---
## 9. Sesión 2026-08-03 (tarde) — §8.1 cerrada
Sin commitear. `check` = **74 = línea base exacta** · 4/4 en el componente ·
`rtl:check` = 1, el mismo `palabras-chrome.css:446` de siempre.
### 9.1 · El bloqueo no existía
«La demo no tiene eje horizontal (`maxScroll: 0`)» era cierto **sólo con el
control en su valor por defecto**. El chip `scrollbars` tiene `horizontal` y
`both` , y cualquiera de los dos le mete `inline-size: 48rem` al contenido
(`+page.svelte:168`). Con `both` sale `maxScroll: 258` y todo es observable.
**No hizo falta construir nada.** Antes de dar una demo por insuficiente, hay
que recorrer sus controles.
### 9.2 · Las dos señales eran una sola, y las dos estaban rotas
`resolvedDir` era código muerto **porque** nadie había traducido `scrollLeft` .
Medido en Chrome con `scrollbars="both"` y la dirección movida por el toggle:
| | antes | después |
| ---------------------- | ---------------------------------------- | -------------------------------------------- |
| `aria-valuenow` en RTL | `-50` , `-99` (con `aria-valuemin="0"` ) | 0 · 50 · 100 |
| thumb en RTL | `left: -167px` — **fuera del track** | dentro, espejado (gapR 0·84·168) |
| `data-at-left` | siempre puesto | sólo en el borde físico izquierdo |
| `data-at-right` | nunca | en reposo, que es donde el contenido se pega |
| click en el canalón | al 10% desde la izquierda → 0 (clampado) | → `-232` = el 90% lógico |
| arrastre del thumb | (tapado por el clamp) | dedo +60 → thumb +60, exacto |
LTR quedó **idéntico salvo una mejora** : el extremo derecho ahora sí enciende
`data-at-right` (ver 9.4).
### 9.3 · La forma: dos idiomas, uno por consumidor
En RTL el navegador **descansa `scrollLeft` en 0** con el contenido pegado a la
derecha y lo lleva **negativo** hacia el final de lectura, hasta
`-(scrollWidth - clientWidth)` (modelo negativo del CSSOM: Chrome 85+, Firefox,
Safari 14.1+). Dos traducciones privadas en el provider, y cada consumidor toma
la suya:
| Sigue el eje de **lectura** | Se queda **físico** |
| --------------------------------------------------------------------------------------------------------- | -------------------------------- |
| offset del thumb — ancla `inset-inline-start` , así descansa a la derecha en RTL, como un scrollbar nativo | `data-at-left` / `data-at-right` |
| `aria-valuenow` | |
| click en el canalón | |
**Por qué el thumb va al revés que `tabs` y el `sliding-indicator` ** (§6.7,
§6.6): allí el JS medía una coordenada FÍSICA y por eso el ancla tenía que ser
física. Aquí el JS no mide nada — calcula un PROGRESO de lectura, así que el
ancla lógica es la que le corresponde. La regla de §2.2 sigue siendo la misma:
**mira de qué lado está la medida.**
**Y por qué `data-at-*` se queda físico**: sus nombres lo son (misma familia que
`at-top` / `at-bottom` ) y su uso documentado —sombras de scroll— también: la
sombra va en el borde que aún tiene contenido detrás, se lea como se lea. Es el
criterio con el que §6.10 dejó `toast` intacto. Se arregló el CÁLCULO, no el
nombre; ninguna CSS del repo los consume, así que no rompe a nadie.
**El arrastre no necesitó nada**, y no por suerte: subir `scrollLeft` mueve el
viewport a la derecha en los dos marcos, así que un delta físico de dedo se
mapea igual. Verificado, no supuesto.
### 9.4 · El borde de 1px cambió de lado
`isAtRight` ya llevaba `- 1` de holgura y `isAtLeft` usaba `<= 0` pelado. Es
correcto en LTR porque **el extremo de reposo es un 0 exacto y el lejano es el
máximo fraccionario del navegador** — pero en RTL el lejano es el IZQUIERDO.
Sin holgura, `data-at-left` no se emitía nunca en RTL y el arreglo quedaba a
medias. Ahora los dos extremos llevan la misma tolerancia. Efecto lateral en
LTR: el extremo derecho, que tampoco se encendía, ahora sí.
### 9.5 · §8.2 cerrada — los tres agujeros de observabilidad
Los tres eran de DEMO, no de componente. `check` sigue en **74 = línea base** .
**`slider` (§2.3)** — cableado `secondaryValue` como un solo mando (porcentaje
del recorrido, para que sobreviva a editar min/max) y montada
`<Slider.SecondaryRange />` **siempre** : sin valor pinta a tamaño cero, así que
el mando basta y la parte no miente. Medido:
| | resultado |
| ------------------- | --------------------------------------------------- |
| horizontal LTR | `left: 0%; width: 60%` — 288/480 desde la izquierda |
| horizontal RTL | `right: 0%; width: 60%` — espejo exacto |
| vertical LTR vs RTL | **idénticos** (`bottom: 0%; height: 60%`) |
Lo vertical **debe** ser idéntico: `rangeStyle` sólo consulta `isRtl` en la rama
horizontal, porque el eje de bloque no se voltea. La combinación que §2.3 daba
por no observable ya está VISTA.
**`drawer`** — la demo listaba `['top','right','bottom','left']` y le faltaban
los dos lógicos. Añadidos. Medido, con la dirección movida por el toggle:
| direction | LTR | RTL |
| --------------- | -------------------------- | ------------------------------------------ |
| `start` | `data-side=left` , x 12– 452 | `data-side=right` , x 1681– 2121 |
| `end` | `right` | `left` |
| `left` (físico) | `left` | `left` — **no voltea** , que es lo correcto |
Confirma por vista lo que §6.9 sólo tenía por lectura: `resolveDirection()` es
el ejemplo a imitar.
**`float-panel` — el handoff acertaba, pero NO por lo que decía.** «Su demo no
ancla ningún panel» sugiere que falta el control, y el control **existía**
(`useAnchor`, con `side` y `align` ). La causa real: `posA` arranca en
`{x:24,y:24}` y `seedRect()` sólo siembra **si `position` está vacía** , así que
una posición ligada anula la semilla para siempre. El `align` era inerte.
Arreglado con `releasePosition()` (suelta `posA` ) llamado desde el toggle
`useAnchor` y desde los chips `side` / `align` — anclar y fijar posición son
excluyentes por diseño, y sin esto el hint «(re-open with useAnchor)» era
mentira.
⚠️ **Y aun así seguía sin verse, por un CLAMP** : el trigger empezaba exactamente
en el borde del stage (`triggerLeftInStage: 0`), así que `align=end` pedía
x=− 173 y `clampPosition` devolvía 0 — los tres aligns colapsaban en el mismo
sitio. Centrada la fila de disparadores sobre el escenario. Entonces sí:
| align | LTR | RTL |
| -------- | ------------ | -------------------- |
| `start` | dLeft **0** | dRight **0** |
| `end` | dRight **0** | dLeft **0** |
| `center` | − 86 / +87 | igual — no se espeja |
El arreglo de `caf3cfd88` pasa de «cubierto por `anchored-seed.test.ts` » a
**verificado en navegador**.
**MÉTODO, dos veces en la misma sesión**: una demo puede ser inobservable por
tres motivos distintos —falta el control (drawer), el control existe pero otro
estado lo anula (float-panel), o el control existe y basta con usarlo
(scroll-area, §9.1)—. **Antes de declarar el bloqueo, recorre los controles y
mira qué gana.**
📌 ** `float-panel` no expone prop `dir` **: lee `soma.prefs.getDir()` directo
(`float-panel-provider.svelte.ts:590`). Es la cadena documentada con el eslabón
de prop ausente, no un salto. Anotado, no tocado.
📌 ** §2.4 sigue viva**: `parts[2]` de la demo del slider indexa `SecondaryRange`
en vez de `Thumb` (ahora línea **430** , era 409). Es el único error de tipos del
fichero y uno de los 74. Preexistente y ajeno al eje — señalado, no tocado.
### 9.6 · Las tres piezas sueltas de las gráficas — y el defecto que escondían
Commits `3c3d8e1ac` (motion) + `b0f995a3b` (metrics). `check` = **74 = línea
base** · 32/32 en motion + metrics · `rtl:check` = 1, el de siempre.
| pieza | veredicto |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `chart-legend` | **Correcto.** No renderiza nada — es un config child; el marcado lo pinta el frame con flex lógico, y §6.14 ya midió que `legendDir` voltea |
| `metrics` | **CSS correcto** : `icon-start` va al borde derecho en RTL y la sparkline espeja (`M0,31` → `M304,31` ). Problema de VOCABULARIO |
| `bar-segment` | Layout, leyenda y tooltip correctos. **Defecto real en la animación de entrada** |
**El defecto no era de `bar-segment` , era del preset `grow-x` **, que clavaba
`transform-origin: 0% 50%` — el borde FÍSICO izquierdo. En RTL la barra se
dispone contra el borde derecho, así que crecía **desde su punta de vuelta a
su base**, despegándose de su ancla. Medido en los dos consumidores:
```
bar-segment borde izq. clavado en 296, el derecho avanza 108 → 0
bar-list borde izq. clavado en 239, y el de ANCLAJE se aleja 65 → 3
```
`transform-origin` **no tiene forma lógica en CSS** , así que el preset lee
ahora `--motion-origin-inline-start` , que `render-css.ts` emite por `:dir()` .
Es el patrón que el propio sistema ya usaba en `scale-fade` con
fix(motion,demos): dos desviaciones propias, encontradas auditando la sesion
1. La regla `:dir()` de `3c3d8e1ac` nacia SIN ACOTAR, o sea matcheaba cada
elemento del documento para declarar una custom property que solo lee el
nodo animado. Acotada a `[data-animation-style]:dir(...)`: `:dir()` se
resuelve contra ese nodo, asi que el caso del subarbol anidado sigue
cubierto y el coste deja de ser global. Medido igual que antes — origin
65.05px en RTL / 0px en LTR, con el borde de anclaje clavado en 0.
2. `releasePosition()` en la demo del float-panel (`1857c7854`) soltaba la
posicion SIEMPRE, tambien con `useAnchor` apagado: cambiar `side` en modo
libre re-centraba un panel colocado a mano. Condicionado al ancla, que es
lo unico con lo que la posicion fijada es excluyente. Medido: con ancla
off el panel se queda en (24,24); con ancla on sigue re-sembrando
(start x=234 · end x=61).
Y el hueco de metodo que las destapo: habia tocado `render-css.ts` —el
generador de CSS de TODO eidos— corriendo solo los tests del ambito. La
pasada COMPLETA da 8 fallos en 4 ficheros, y verifique que ninguno es mio
cruzando lo que señalan contra `git diff --name-only 40db0b981..HEAD`:
audio-player (sonido), aura + menubar (eje agentico), orca (hook timeout),
soma-attr-audit (pasa en solitario: flaky bajo carga).
Queda anotado en 9.7 un cabo suelto AJENO: 6.7 retiro la entrada de
`data-ready` de `component-visual-attrs.test.ts` al migrar tabs, pero
`contracts.test.ts` sigue marcando `data-ready` en radio-group y tabs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
`--floating-transform-origin` . La regla va **acotada a
`[data-animation-style]` **: el nodo que la lee es siempre el animado, así que
un `:dir()` pelado declararía una custom property heredada en CADA elemento
del documento para nada. `:dir()` se resuelve contra ese nodo, así que un
subárbol que redeclare su dirección sigue acertando — y se emiten las DOS
direcciones justamente para ese caso, que es por lo que `[dir='rtl']` está
prohibido (§6.3).
⚠️ **PUNTO CIEGO DE RTL-1, el tercero** (tras el `transform` inline del §6.7):
esto vive en un preset de TypeScript, ni siquiera en CSS estático. El guard
lee CSS; los presets y lo que pinta el JS siguen necesitando el ojo.
**`metrics`: renombrado `placement: 'right'` → `'end'` ** (BREAKING, sin shim).
El CSS siempre fue lógico, así que `right` ya ponía el chart a la izquierda en
RTL: el comportamiento era el bueno, mentía el nombre. Y era una **isla** —
`layout: 'icon-start'` , `align: 'start'` y el resto del vocabulario del
componente son lógicos. NO es el caso legítimo del `toast` (§6.10), que es
físico y coherente **consigo mismo** ; aquí la incoherencia estaba dentro.
**MÉTODO**: los tres se dieron por sanos en §7.2 mirando el CSS, y el CSS
**era** correcto en los tres. Lo que fallaba era la ANIMACIÓN, que no está en
el recipe sino en un preset compartido. Revisar una gráfica en RTL no es sólo
leer su recipe: hay que **disparar su entrada** y mirar de dónde crece.
📌 No tocado y señalado: el token `--metrics-chart-right-width` conserva el
nombre físico. Describe un ANCHO, no un lado, y es superficie pública de
theming — su renombrado es otra decisión.
fix(motion,demos): dos desviaciones propias, encontradas auditando la sesion
1. La regla `:dir()` de `3c3d8e1ac` nacia SIN ACOTAR, o sea matcheaba cada
elemento del documento para declarar una custom property que solo lee el
nodo animado. Acotada a `[data-animation-style]:dir(...)`: `:dir()` se
resuelve contra ese nodo, asi que el caso del subarbol anidado sigue
cubierto y el coste deja de ser global. Medido igual que antes — origin
65.05px en RTL / 0px en LTR, con el borde de anclaje clavado en 0.
2. `releasePosition()` en la demo del float-panel (`1857c7854`) soltaba la
posicion SIEMPRE, tambien con `useAnchor` apagado: cambiar `side` en modo
libre re-centraba un panel colocado a mano. Condicionado al ancla, que es
lo unico con lo que la posicion fijada es excluyente. Medido: con ancla
off el panel se queda en (24,24); con ancla on sigue re-sembrando
(start x=234 · end x=61).
Y el hueco de metodo que las destapo: habia tocado `render-css.ts` —el
generador de CSS de TODO eidos— corriendo solo los tests del ambito. La
pasada COMPLETA da 8 fallos en 4 ficheros, y verifique que ninguno es mio
cruzando lo que señalan contra `git diff --name-only 40db0b981..HEAD`:
audio-player (sonido), aura + menubar (eje agentico), orca (hook timeout),
soma-attr-audit (pasa en solitario: flaky bajo carga).
Queda anotado en 9.7 un cabo suelto AJENO: 6.7 retiro la entrada de
`data-ready` de `component-visual-attrs.test.ts` al migrar tabs, pero
`contracts.test.ts` sigue marcando `data-ready` en radio-group y tabs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### 9.7 · Auditoría de la propia sesión — 2 desviaciones mías, corregidas
Repaso crítico de todo lo de §9 a petición del usuario. **Dos cosas estaban
mal y eran mías:**
1. **La regla `:dir()` del §9.6 nacía sin acotar** (`:dir(ltr) { … }`), o sea
matcheaba **cada elemento del documento** para declarar una custom
property que sólo lee el nodo animado. Acotada a
`[data-animation-style]:dir(…)` . Mismo comportamiento medido (origin
`65.05px` en RTL / `0px` en LTR, ancla clavada), coste acotado.
2. ** `releasePosition()` en la demo del float-panel soltaba la posición
siempre**, también con `useAnchor` apagado — así que cambiar `side` en modo
libre re-centraba un panel colocado a mano. Condicionado a `useAnchor` .
Medido: con ancla off el panel se queda en (24,24) al cambiar `side` ; con
ancla on sigue re-sembrando (`start` x=234 · `end` x=61).
**Y un hueco de método**: toqué `render-css.ts` , que genera el CSS de TODO
eidos, y sólo había corrido los tests del ámbito. La pasada COMPLETA da
**8 fallos en 4 ficheros** — `contracts.test.ts` (5), `soma-attr-audit` ,
`eidos/lint` , `orca` . Verificado que **ninguno es mío** , cruzando los ficheros
que señalan contra `git diff --name-only 40db0b981..HEAD` :
| guard | señala | dueño |
| ----------------- | ---------------------------------------------------- | ---------------------------- |
| `eidos/lint` | `audio-player: no morfo` | hilo del sonido (ya en §6.5) |
| `contracts` × 5 | `aura/langs.ts` , `morfo/aura.ts` , `menubar-provider` | eje agéntico |
| `contracts` | `radio-group` + `tabs` : `data-ready` | ⚠️ **probablemente de §6.7** |
| `soma-attr-audit` | — pasa al correrlo solo: **flaky bajo carga** | — |
| `orca` | hook timeout a 10s | ajeno |
fix(motion,demos): dos desviaciones propias, encontradas auditando la sesion
1. La regla `:dir()` de `3c3d8e1ac` nacia SIN ACOTAR, o sea matcheaba cada
elemento del documento para declarar una custom property que solo lee el
nodo animado. Acotada a `[data-animation-style]:dir(...)`: `:dir()` se
resuelve contra ese nodo, asi que el caso del subarbol anidado sigue
cubierto y el coste deja de ser global. Medido igual que antes — origin
65.05px en RTL / 0px en LTR, con el borde de anclaje clavado en 0.
2. `releasePosition()` en la demo del float-panel (`1857c7854`) soltaba la
posicion SIEMPRE, tambien con `useAnchor` apagado: cambiar `side` en modo
libre re-centraba un panel colocado a mano. Condicionado al ancla, que es
lo unico con lo que la posicion fijada es excluyente. Medido: con ancla
off el panel se queda en (24,24); con ancla on sigue re-sembrando
(start x=234 · end x=61).
Y el hueco de metodo que las destapo: habia tocado `render-css.ts` —el
generador de CSS de TODO eidos— corriendo solo los tests del ambito. La
pasada COMPLETA da 8 fallos en 4 ficheros, y verifique que ninguno es mio
cruzando lo que señalan contra `git diff --name-only 40db0b981..HEAD`:
audio-player (sonido), aura + menubar (eje agentico), orca (hook timeout),
soma-attr-audit (pasa en solitario: flaky bajo carga).
Queda anotado en 9.7 un cabo suelto AJENO: 6.7 retiro la entrada de
`data-ready` de `component-visual-attrs.test.ts` al migrar tabs, pero
`contracts.test.ts` sigue marcando `data-ready` en radio-group y tabs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
⚠️ **Lo que merece mirarse** : §6.7 dice que retiró la entrada de `data-ready`
de `component-visual-attrs.test.ts` al migrar `tabs` a `MeasuredIndicator` ,
pero ** `contracts.test.ts` sigue marcando `data-ready` en `radio-group` y
`tabs` **. O el guard llevaba rojo de antes, o se retiró la entrada de UN guard
y hay OTRO que mira lo mismo. No es de este hilo, pero está sin cerrar.
Revisado y correcto, sin cambios: nadie lee `style.left` del thumb; el
renombrado de `placement` no dejó ningún consumidor (grep global); la
tolerancia de 1px es simétrica y no altera el caso sin overflow.
### 9.8 · Suelto
- **`-0`**: negar un `scrollLeft` en reposo da `-0` . Es inocuo en el DOM
(`String(-0)` es `"0"` , medido), pero un matcher estricto lo distingue — de
ahí el `expect.closeTo(0)` en el test.
- El test nuevo **ancla el caso histórico** : con el provider en HEAD sale rojo
(A/B con `git stash` ), con el arreglo verde.
- ⚠️ Se añadió al fichero de test existente, que es **uno de los 91 de §8.4.1**
(falsifica `Soma.require()` ). Migrarlo es ese proyecto, no éste.
- Los 3 ficheros tocados ya fallaban `prettier --check` en HEAD; no se
formatearon (§5).
docs(direction): 8.3 cerrada — coarse eran 6 media queries, no 2, y el swipe con raton real
Seccion 9.9. Dos cierres y un hallazgo del usuario:
- `pointer: coarse`: el catalogo tiene SEIS media queries, no las dos que 8.3
daba por unicas. Las cuatro sin mirar (list-surface, chat-message,
radio-group, rotate-align) no tienen ninguna geometria de eje inline —
`--list-item-height`, `opacity`, `min-block-size` e `inset: 0` + `min-*`
logicos. Las dos que si la tenian son justo las que 6.2 midio con hit-test.
Cerrado por lectura, que aqui basta porque lo que se comprueba es una
AUSENCIA.
- El swipe del carousel con el raton REAL del navegador: las 4 combinaciones
dan espejo exacto. Cierra lo que 6.8 no consiguio disparar por simulacion.
MÉTODO: el raton real avanza 2 posiciones de golpe (momentum) y
`left_click_drag` no deja fijar la velocidad — lo que se comprueba es el
SIGNO, no la magnitud. Y verifica la direccion ANTES de cada gesto: mi
primera pasada midio creyendo estar en LTR con el toggle en RTL, y el
resultado parecia un defecto inexistente.
- Las flechas del carousel en RTL (arreglo en el commit anterior).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### 9.9 · §8.3 cerrada — y las flechas del carousel, que nadie había mirado
Commit del arreglo: ver `git log` . `check` = **74 = línea base** · `rtl:check`
= 1 (el de palabras) · `eidos-lint carousel` : **invalid 0** .
**Las flechas del carousel apuntaban HACIA ADENTRO en RTL.** Lo vio el usuario
sobre la marcha. Medido, con el A/B completo:
| | LTR | RTL (antes) |
| ------ | ---------------------- | ------------------------------ |
docs(direction): 8.3 cerrada — coarse eran 6 media queries, no 2, y el swipe con raton real
Seccion 9.9. Dos cierres y un hallazgo del usuario:
- `pointer: coarse`: el catalogo tiene SEIS media queries, no las dos que 8.3
daba por unicas. Las cuatro sin mirar (list-surface, chat-message,
radio-group, rotate-align) no tienen ninguna geometria de eje inline —
`--list-item-height`, `opacity`, `min-block-size` e `inset: 0` + `min-*`
logicos. Las dos que si la tenian son justo las que 6.2 midio con hit-test.
Cerrado por lectura, que aqui basta porque lo que se comprueba es una
AUSENCIA.
- El swipe del carousel con el raton REAL del navegador: las 4 combinaciones
dan espejo exacto. Cierra lo que 6.8 no consiguio disparar por simulacion.
MÉTODO: el raton real avanza 2 posiciones de golpe (momentum) y
`left_click_drag` no deja fijar la velocidad — lo que se comprueba es el
SIGNO, no la magnitud. Y verifica la direccion ANTES de cada gesto: mi
primera pasada midio creyendo estar en LTR con el toggle en RTL, y el
resultado parecia un defecto inexistente.
- Las flechas del carousel en RTL (arreglo en el commit anterior).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| `prev` | lado izq, apunta izq ✓ | lado **der** , apunta **izq** ✗ |
| `next` | lado der, apunta der ✓ | lado **izq** , apunta **der** ✗ |
La POSICIÓN sí se espejaba —los insets del recipe son lógicos— pero el GLIFO
no: `chevronDir` en los triggers de eidos sólo mira `orientation` , nunca la
dirección (`carousel-prev-trigger.svelte:32`). Resultado: ambas flechas
apuntando al centro.
**Por qué no bastaba una regla `:dir()` normal**: `SvgChevron` escribe
`transform: rotate()` **inline** , así que ninguna regla lo re-apunta sin
`!important` . Se voltea con la propiedad independiente ** `rotate` **, que
COMPONE con ese transform en vez de reemplazarlo — exactamente el mismo motivo
por el que el centrado de los triggers usa `translate` y no `transform` , y que
el propio recipe ya documentaba dos reglas más arriba.
**Y por qué `[data-dir='rtl']` y no `:dir(rtl)` aquí**: `data-dir` es el
atributo PROPIO del componente, que soma estampa desde `resolvedDir` — no el
`dir` del DOM, así que no es el `[dir='rtl']` prohibido por §6.3. Leerlo
garantiza que el glifo coincida con la matemática del arrastre y del teclado,
que resuelven de esa misma fuente; con `:dir()` podrían discrepar si el DOM y
las prefs divergen. `eidos-lint` lo clasifica **morfo-backed** .
Verificado en las dos direcciones y a ojo. Es lo mismo que hace **shadcn**
(`rtl:rotate-180`); **nuxt/ui** tuvo este bug exacto
([#1354](https://github.com/nuxt/ui/issues/1354)) — allí fallaban además las
posiciones y el signo de la navegación, que aquí ya estaban bien (§6.13).
**El swipe con ratón REAL** (el del navegador, no eventos sintéticos) — las 4
combinaciones, espejo exacto. Cierra lo que §6.8 no consiguió disparar:
```
LTR arrastre izquierda 1 → 2 avanza LTR derecha 2 → 1 retrocede
RTL arrastre derecha 1 → 2 avanza RTL izquierda 2 → 1 retrocede
```
⚠️ **MÉTODO** : el ratón real avanza 2 posiciones de golpe (momentum), y el
`left_click_drag` no deja fijar la velocidad. Lo que se comprueba es el SIGNO,
no la magnitud. Y verifica la dirección ANTES de cada gesto: mi primera pasada
midió creyendo estar en LTR cuando el toggle estaba en RTL, y el resultado
parecía un defecto que no existía.
### 9.10 · El `dir=""` vacío — CERRADO, y era el shorthand de Svelte
Commit `b906dd407` . §6.5 lo había visto en `sidebar` y `tree-view` «sin
investigar de dónde sale». Sale del shorthand ** `{dir}` **: para `dir` Svelte
emite una asignación de PROPIEDAD además del atributo, y con `undefined` el
resultado es la cadena vacía en vez de la omisión.
Cazado con un trap sobre `setAttribute` + el descriptor de la propiedad: **dos
escrituras desde el mismo componente compilado**, la segunda por propiedad. Y
**no estaba en el SSR** — `dir=""` no aparece ni una vez en el HTML servido, lo
escribía el cliente al hidratar.
Un valor inválido HEREDA, así que nada pintaba mal; pero contradice la letra
del contrato y hace que `[dir]` matchee.
```svelte
{dir} → {...dir ? { dir } : {}}
```
- **`tree-view` (eidos)**: el wrapper LO NECESITA cuando el valor es concreto —
el recipe selecciona con `[data-tree-view-root]:dir(rtl)` y ese div es el
ANCESTRO del provider que estampa el attr. Ausente, hereda: correcto.
- **26 demos**: el arnés estándar (`data-uix-stage-area` / `-canvas-inner` ).
Verificado: en `auto` sólo quedan `<html>` (la proyección) y el provider con su
valor resuelto — cero `dir=""` ; en `rtl` los tres lo reciben concreto; al
volver a `auto` desaparece. Las 26 rutas sirven 200 y ninguna trae `dir=""` .
⚠️ **MÉTODO** : mi primer regex también matcheaba dentro de `dir={dir}` y dejó
`dir={...}` — dos ficheros con parse error, que `check` cazó (76 vs 74).
**Contar ficheros modificados NO basta; hay que mirar el diff.**
### 9.11 · `html lang` — diagnóstico completo, decisión PENDIENTE
Causa raíz: ** `src/app.html:2` tiene `<html lang="en">` hardcodeado y nadie lo
actualiza nunca.** El `dir` sí se actualiza porque `ActivePrefsDomProjection`
lo proyecta (`PREFS_DOM_ATTRS.DIR`); `lang` no está en esa lista.
Medido con árabe seleccionado: `<html dir="rtl">` ✓ · `<html lang="en">` ✗.
📌 **El `<main lang="en">` NO es el defecto y debe quedarse** — es el residuo
deliberado de §6.4 y es correcto: la prosa de las demos ESTÁ en inglés. Lo que
falla es el `lang` del DOCUMENTO, que debería seguir al idioma de la shell.
El arreglo sería simétrico y corto —la shell ya registra `language` en prefs
(`es·en·fr·de·ar`), que es justo de donde prefs deriva la dirección, así que
`slots.language` ya está disponible en la proyección— pero **cambia un
contrato declarado**: `contracts.ts` fija
`prefsDomProjection.ownsAttrs = ['dir','data-motion','data-sound','data-haptic']`
con su guard en `contracts.test.ts` , más los tests de `dom-projection` y el
README de prefs.
Y hay un argumento real EN CONTRA: en apps con i18n por routing el `lang` lo
fija el servidor, y una proyección de cliente lo pisaría. Es decisión de
arquitectura, no una corrección obvia — por eso §3 lo clasificó «NO de este
hilo».
**EJECUTADO** tras firmarlo el usuario (`41e1f2830`). `lang` viaja con `dir`
porque responden a la misma pregunta sobre el documento y el navegador lee LOS
DOS del DOM. El slot ya estaba disponible y su valor efectivo es un tag BCP-47,
justo lo que el atributo toma. Un esquema SIN la dimensión `language` no
produce slot y no proyecta nada, así que una app con i18n por routing queda
intacta — que era el argumento en contra, ahora cubierto por construcción.
`ownsAttrs` pasa a `['dir','lang','data-motion','data-sound','data-haptic']` .
Medido por el camino real: ar→`ar`/`rtl` · en→`en`/`ltr` · es→`es`/`ltr`; un
solo `language.set` mueve los dos, y eso queda fijado en el test.
### 9.12 · `color-picker` — el criterio lo zanjaron las referencias
Commit `46ffe1c74` . §6.9 lo había dejado como «probablemente correcto por
diseño (una rueda de color es física, como el cropper), pero es una DECISIÓN».
**Las referencias la zanjan, y en contra de esa suposición.**
react-aria / react-spectrum, sobre `ColorArea` :
> "In right-to-left languages, color areas should be mirrored. Orientation of
> the gradient background, positioning of the thumb, and dragging behavior is
> automatically mirrored by `ColorArea`."
La analogía con el `cropper` era mala y queda retirada: allí paneas una IMAGEN,
que no se voltea; aquí el eje X reparte un **canal** por la dimensión inline —
un eje de lectura. Y react-aria distingue `ColorArea` (rectangular, espeja) de
`ColorWheel` (radial, no), que es **el mismo corte que la regla RTL·SVG de §7.1** .
Medido antes: pinchar el MISMO punto físico daba el mismo valor en las dos
direcciones, y el degradado no seguía al thumb.
| Pieza | Qué necesitó |
| ----------------- | ---------------------------------------------------- |
| área · puntero | la fracción física se espeja sobre el canal |
| área · thumb | ancla **FÍSICA** (`left`) alimentada ya espejada |
| área · flechas | `ArrowRight` mueve a la derecha ⇒ BAJA el canal |
| área · degradados | saturación + arcoíris de hue voltean con `:dir(rtl)` |
| sliders de canal | **sólo el degradado** |
⚠️ El thumb conserva `left` FÍSICO a propósito: el recipe lo centra con
`translate(-50%,-50%)` , y ancla lógica + translate físico es justo la trampa
que el contrato prohíbe. Soma le pasa la fracción ya espejada
(`physicalXProgress`) — la misma resolución que el `sliding-indicator` (§6.2).
**Los sliders de canal casi no necesitaban nada**, y eso es mérito del refactor
de 2026-05-21: componen el `SliderProvider` genérico, que ya espeja thumb,
puntero y teclado. Pero su rampa estaba clavada a `to right` , así que **el
color BAJO el thumb dejaba de ser el valor** — medido con hue 210: thumb a
123px del borde derecho y el degradado contando aún desde la izquierda.
Verificado en las dos direcciones: al 10% del borde izquierdo físico, LTR da
`saturation 10` y RTL `90` ; el blanco del área y el rojo del hue quedan a la
derecha en RTL; `ArrowRight` mueve el thumb a la derecha en ambas. Dos tests
nuevos, ROJOS con el provider anterior.
`Home` /`End` se quedan en min/max del CANAL (un valor, no un borde físico),
igual que en un slider.
### 9.13 · `float-panel` (`Home`/`End`) — sigue PENDIENTE, y ahora se sabe por qué
Buscadas las referencias: **no existe patrón APG para paneles flotantes
movibles**. El análogo más cercano es el **Window Splitter** , y ahí el APG
define las teclas por VALOR, no por geometría:
> Home: "Moves splitter to the position that gives the primary pane its
> smallest allowed size." · End: "…its largest allowed size."
Y **no menciona RTL en ningún sitio** . Pero la analogía es imperfecta: un
splitter tiene un valor semántico unidimensional, así que «mínimo/máximo»
significa algo; un float-panel que se arrastra libre por un plano **no tiene
valor**, sólo posición. Lo único transferible es que react-aria sí voltea el
movimiento por teclado en RTL (`useMove`), lo que apunta a que las FLECHAS
deberían seguir la dirección.
Conclusión: extremos físicos son defendibles **porque no hay semántica que
respetar**, pero no hay precedente que lo respalde ni que lo contradiga.
**DECIDIDO** (el usuario dejó la elección): el movimiento por teclado del
panel —flechas y `Home` /`End`— **se queda FÍSICO** . Sin valor semántico que
respetar, el vocabulario físico es coherente CONSIGO MISMO (`bounds.left` /
`bounds.right` ya lo son), que es la misma excepción legítima con la que §6.10
dejó intacto al `toast` . No se toca nada.
### 9.14 · El resize del float-panel — el usuario lo vio, yo no lo había mirado
Commit `552f4f2c7` . Reportado con una captura del grip en la esquina inferior
IZQUIERDA. **Ahí es donde debe estar en RTL** (el recipe lo coloca con
`inset-inline-end` ), pero la matemática no acompañaba:
```ts
if (edge.includes('e')) width = start.width + dx; // dx es FÍSICO
```
Los handles se colocan con insets LÓGICOS, así que el marcado `e` es el borde
inline-END: físicamente derecho en LTR y físicamente IZQUIERDO en RTL.
Medido: **arrastrar el grip hacia FUERA encogía el panel** — 436 → 327 donde
debía crecer a 545. El eje de bloque sí estaba bien (292 → 346).
Resuelto convirtiendo el edge lógico a su lado FÍSICO una sola vez
(`physicalResizeEdge`), de modo que toda la geometría de abajo sigue en píxeles
físicos — **la forma de `drawer.resolveDirection()`** , que §6.9 señaló como el
ejemplo a imitar.
⚠️ **Y destapó un SEGUNDO defecto, latente** : crecer desde `w` /`n` mantiene el
lado opuesto fijo, así que el origen del panel viaja hacia fuera, y ese caso no
se acotaba contra los bounds — el panel se salía del contenedor (medido
`POS -85` con el stage empezando en 0). **En LTR la rama era inalcanzable** :
sólo montan `e` , `s` y el grip `se` . Ahora se acota por el hueco disponible.
El grip además mentía en RTL: `cursor: nwse-resize` (↘↖) y los chevrones a
`-45deg / bottom right` son físicos y no tienen forma lógica → par `:dir(rtl)` .
| | resultado |
| -------------------------- | -------------------------------------------------- |
| RTL · arrastre hacia fuera | 300 → 409 de ancho, borde DERECHO fijo en 803 |
| RTL · arrastre largo | se detiene exacto en el borde del stage (`left 0`) |
| LTR | sin cambios: 300 → 436, borde izquierdo fijo en 24 |
⚠️ **LECCIÓN, y es la tercera vez que aparece en esta sesión** : §6.10 y §9.5
dieron el float-panel por revisado **midiendo sólo la POSICIÓN del panel** .
Nadie tocó el resize. Verificar un componente con gestos no es medir su
geometría en reposo — hay que **ejercitar cada gesto que ofrece** .
### 9.15 · Censo de canonización — NO está todo normalizado
Pregunta del usuario: «¿todos los componentes han canonizado la forma de
obtener el `dir` ?». **No.** §6.12 dijo que `rating-group` y
`natural-time-picker` eran «los DOS únicos que quedaron fuera; los otros 36 sí
usan `activeDir` ». **Ese censo estaba incompleto** : contó los wrappers con
default duro, no los que resuelven en OTRO sitio.
Medido: **106 componentes en soma · 38 con el wrapper canónico**
(`dir: activeDir(() => dir, soma)`) · **11 fuera de la norma** , en dos grupos
de naturaleza distinta.
**Grupo A — exponen `dir` pero resuelven en el PROVIDER (4)**
`css-field` · `listbox` · `navigation-menu` · `number-field` . El wrapper pasa
la prop CRUDA (`readableActive(() => dir)`) y el provider hace
`opts.dir.current ?? prefs.getDir() ?? 'ltr'` — la cadena duplicada, al revés
del reparto que fijó `013ceac57` .
⚠️ Verificado que **hoy son equivalentes** : con el toggle en `auto` + árabe,
`number-field` estampa `dir="rtl"` y el canónico `scroll-area` también. La
sospecha de incumplimiento observable era FALSA y se retira.
Pero hay una divergencia **latente** : `listbox` y `navigation-menu` estampan
`assertedDir` (distinguen «afirmado» de «resuelto») mientras `css-field` y
`number-field` estampan el RESUELTO. En una app cuyo esquema de prefs no
declare dirección, `getDir()` devuelve `undefined` y ahí los primeros omiten el
atributo y los segundos estampan `'ltr'` — el defecto que `013ceac57` arregló
en 33 componentes, superviviente en 2.
**Grupo B — no exponen `dir` en absoluto (7)**
`drawer` · `float-panel` · `grid-list` · `tag-group` · `tree-grid` ·
`virtual-grid` · `virtual-list` . Leen `soma.prefs.getDir()` directamente. Es
coherente con el contrato **con el eslabón de prop ausente** , y por eso ninguno
está roto. Pero es una **asimetría de API** : 38 componentes dejan fijar la
dirección de su subárbol con una prop y estos 7 no.
Ninguno de los 11 es un defecto de comportamiento hoy. Son dos decisiones
pendientes: (a) mover la cadena al wrapper en los 4 del grupo A —refactor
mecánico, y de paso cierra la divergencia latente de 2—, y (b) si los 7 del
grupo B deben exponer `dir` , que es superficie pública.
⚠️ **Este censo se dejó fuera un GRUPO C, y ése sí estaba roto** (hallado el
2026-08-04, ver [`CONTINUE-player-rtl.md` ](./CONTINUE-player-rtl.md ) §2.1).
`media-player` expone `dir` y **ni lo resuelve ni lo estampa** : pasaba la prop
cruda y sólo la reenviaba a los Sliders compuestos. Con `dir="rtl"` por prop y
sin `dir` ambiental el cromo se disponía en LTR y sus dos sliders en RTL —
partido por la mitad. La búsqueda que hizo el censo mira wrappers y providers;
un componente que **delega la prop a un hijo** no cae en ninguno de los dos
patrones y pasa desapercibido. Al re-auditar, buscar también quién ACEPTA `dir`
sin escribirlo en ningún sitio.
### 9.16 · Grupo A canonizado — la cadena baja al wrapper (4/11)
Commit `b73d6baf8` . `css-field` · `listbox` · `navigation-menu` ·
`number-field` pasaban la prop CRUDA y duplicaban la cadena en el provider.
Dos de ellos lo documentaban como desviación consciente («Unlike the other
components…»), lo que confirma que fue deliberado y se quedó sin reconciliar.
**Funcionalmente eran equivalentes hoy** —verificado que `number-field` y el
canónico `scroll-area` estampan lo mismo con el toggle en `auto` — pero había
una divergencia **latente y real** en dos: `css-field` y `number-field`
estampaban el valor RESUELTO. Con un esquema de prefs sin dimensión de
dirección, `getDir()` devuelve `undefined` : los canónicos omiten el atributo y
esos dos estampaban `'ltr'` . Es **el defecto que `013ceac57` corrigió en 33
componentes, superviviente en 2**.
Tres tests se apoyaban en el mecanismo viejo (el harness devolvía dirección por
`prefs.getDir()` y esperaban verla en el provider). Actualizados al contrato:
sin afirmación el atributo se OMITE, con ella se estampa.
Verificado en Chrome en las tres posiciones del toggle. `check` 74, 25/25.
### 9.17 · Grupo B — NO es otro mecanismo, es otra decisión de API
`drawer` · `float-panel` · `grid-list` · `tag-group` · `tree-grid` ·
`virtual-grid` · `virtual-list` leen `soma.prefs.getDir()` directamente
**porque no exponen prop `dir` **. Conviene separarlo del grupo A: no usan un
mecanismo distinto, usan **el mismo con el primer eslabón ausente** , que es
exactamente lo que el contrato prescribe cuando no hay prop.
Así que la pregunta no es «¿cómo resuelven?» sino ** «¿deberían aceptar `dir` ?»**
— superficie pública, no implementación. A favor: 42 componentes ya la aceptan
y un consumidor no puede fijar la dirección de estos 7 subárboles. En contra:
añadir una prop a un `virtual-list` o un `tag-group` sólo tiene sentido si
alguien va a fijar la dirección de ESE subárbol contra la de la página, que es
un caso raro.
Pendiente de criterio. Si se hace, el patrón es mecánico y ya está probado en
§9.16: `dir?: Direction` en types + `activeDir(() => dir, soma)` en el wrapper
- `?? 'ltr'` en el provider.
### 9.18 · Auditoría de la canonización — 2 defectos propios, con el criterio corregido
Commit `3b4340d2a` . Repaso crítico de §9.16– §9.17 aplicando **el criterio que
la otra sesión demostró que faltaba**: buscar quién ACEPTA `dir` y cruzarlo con
quién lo resuelve, no sólo quién usa `activeDir` o consulta prefs.
**Defecto 1 — añadí la prop pero NO el estampado.** `tree-grid` y `float-panel`
usan `:dir()` en su recipe y no estampaban el atributo: un consumidor que
afirmase `dir` por prop movía la MATEMÁTICA y dejaba la PINTURA atrás, porque
`:dir()` lee la dirección EFECTIVA y sin atributo eso es la heredada. **Es el
mismo partido-por-la-mitad que la otra sesión halló en `media-player` ** — o
sea el grupo C tenía dos miembros más de los que yo creé sin darme cuenta.
Verificado: `tree-grid` estampa ltr/rtl/auto y el grip del `float-panel` pasa
de `nwse-resize` a `nesw-resize` .
Los otros 5 del grupo B no llevan `:dir()` en su recipe, así que no estampar no
rompe nada; se dejan como están para no ampliar el DOM sin motivo.
⚠️ **La regla que sale de aquí** : añadir la prop `dir` a un componente no basta.
Hay que preguntarse **quién LEE la dirección** — si el recipe usa `:dir()` , el
atributo es parte del contrato y hay que estamparlo; si sólo hay matemática JS,
la prop por `opts` es suficiente.
**Defecto 2 — artefacto de regex.** Cinco ficheros quedaron con un espacio antes
de la coma (`OnChangeFn , Direction`). Prettier no lo corrigió **porque ya
estaban sin formatear en HEAD**, así que nunca pasaron por su escritura — la
regla de «formatea sólo lo que ensuciaste» tiene ese punto ciego.
**Falso positivo, revisado y NO tocado**: `waveform` acepta `dir` y no lo
resuelve, pero **no está roto** — no tiene matemática direccional propia. El
recipe espeja con `:dir(rtl)` (medido: la onda pasa a `scaleX(-1)` ) y el Slider
que compone corre la cadena por su cuenta. Corregido sólo su COMENTARIO, que
afirmaba apoyarse en `[dir='rtl']` , prohibido desde §6.3.
📌 **El censo final** : 45 componentes aceptan `dir` ; de ellos sólo `waveform` no
lo resuelve, y con razón. `media-player` lo arregló la otra sesión.
### 9.19 · El alias, y los cuatro que pintaban con `:dir()` sin dejar afirmarla
Commits `188450072` y `d490f53e9` . Cierra las dos deudas que quedaban abiertas
al final de §9.18.
**El alias.** Ocho componentes escribían `dir?: 'ltr' | 'rtl'` a mano en vez de
`Direction` : calendar, command, emoji-picker, field-langs (props Y el spec de
lengua del proveedor), month-grid, range-calendar y year-grid. Compila igual,
pero deja el contrato fuera del alias — ensanchar o renombrar `Direction` no
los tocaría, y la deriva no rompería en tipos, que es justo lo que el alias
está para garantizar. Cambio de tipo puro.
**Los cuatro de sólo-CSS.** `feed` , `nav-tree` , `sidebar` y `switch` tienen
reglas `:dir(rtl)` en su receta y no aceptaban `dir` . Funcionaban —`:dir()` lee
la dirección HEREDADA y la proyección de prefs pone `dir` en el `<html>` — pero
sólo para la página entera: no había forma de voltear UNO. Ahora corren la
cadena canónica y **estampan el crudo** , por la regla de §9.18: si el recipe usa
`:dir()` , el atributo es parte del contrato.
`switch` no pudo ir por `OptsFromProps` : ese helper quita el `undefined` del
prop público y da `Active<'ltr' | 'rtl'>` , cuando «nadie afirmó nada» es
exactamente el valor que el estampado crudo necesita distinguir. Va aparte, con
`ActiveProps<{ dir: Direction | undefined }>` , y en el envoltorio fuera del
`bindProps` (que toma getters, no `Active` s).
`sidebar` tenía además un resolutor a pelo en el motor flotante
(`soma.prefs?.getDir() ?? 'ltr'`, línea 662) que se saltaba el prop: ahora
consume `resolvedDir` . Ése sí necesita valor concreto —el motor espeja la
colocación del flyout—, a diferencia de la receta, que lee `:dir()` .
📌 **Censo final del eje** (ya cerrado):
docs(direction): los 55 componentes que aceptan `dir` ya lo documentan
De los que aceptan la prop, la mayoria no la mencionaba en su README, y los que
si tenian seis redacciones distintas para el mismo hecho: «from Soma», «prefs»,
«soma config», «soma», «Override Soma defaults» y —la peor— «Inherited from Soma
when omitted», que se lee como herencia del PADRE, que es justo lo que la cadena
no hace.
Cada uno dice ahora QUE cambia la direccion EN ESE componente, leido de su propio
codigo (el JSDoc de la prop, los usos de `resolvedDir` en el provider, las reglas
`:dir(` de su receta), no una frase de plantilla. Y cada uno enlaza el contrato
una vez, en la primera mencion.
DOS AFIRMACIONES FALSAS, cazadas por el verificador adversarial:
- `feed` decia que la receta «mueve el rail del hilo al otro lado». No lo movia:
lo CANCELABA. Ese era el defecto arreglado en `dee2c9e3a`; el README describe
ahora lo que el codigo hace.
- `waveform` daba `prefs` como default. Falso: waveform es el caso DELEGANTE,
no llama a `activeDir`, y sin prop no hay atributo ni lectura de prefs —
hereda. Su Slider compuesto es quien corre la cadena.
Y dos que estaban mal COLOCADAS: `calendar` listaba `dir` bajo `## ARIA` (no es
un atributo ARIA) y `accordion` bajo `## HTML Attributes` describiendolo como
«la prop cruda», cuando lo estampado es prop O prefs.
CORRECCION AL PRIMER BARRIDO, por la ley de autoria: escribio la cadena literal
`prop → prefs → 'ltr'` en 54 ficheros. Eso es la forma restate-instead-of-link
que `docs/authoring.md` §1 prohibe citando este mismo fallo («'7 families'
sobrevivio en tres docs semanas despues de que el canon pasara a 8»). Retirada:
la cadena se enuncia UNA vez, en el contrato; el README conserva su efecto.
Tres READMEs de eidos ensenaban todavia el selector prohibido —y los tres se
equivocaban sobre su PROPIO CSS, que ya usa `:dir(rtl)`—: waveform, nav-tree y
toolbar (este ademas invertia la precedencia: «prefs o prop»; gana la prop).
⚠️ `number-field` no tiene tabla de props que ampliar; su `dir` sigue en su
seccion `### Direction resolution`, ahora enlazando el contrato. Fabricar una
tabla de una fila diria «`dir` es su unica prop».
Handoff §9.20 con el cierre, el doble volteo y como se colo.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **55** componentes aceptan la prop `dir` ; **61** ficheros de envoltorio corren
`activeDir` (la diferencia son compuestos con varios proveedores).
docs(direction): los 55 componentes que aceptan `dir` ya lo documentan
De los que aceptan la prop, la mayoria no la mencionaba en su README, y los que
si tenian seis redacciones distintas para el mismo hecho: «from Soma», «prefs»,
«soma config», «soma», «Override Soma defaults» y —la peor— «Inherited from Soma
when omitted», que se lee como herencia del PADRE, que es justo lo que la cadena
no hace.
Cada uno dice ahora QUE cambia la direccion EN ESE componente, leido de su propio
codigo (el JSDoc de la prop, los usos de `resolvedDir` en el provider, las reglas
`:dir(` de su receta), no una frase de plantilla. Y cada uno enlaza el contrato
una vez, en la primera mencion.
DOS AFIRMACIONES FALSAS, cazadas por el verificador adversarial:
- `feed` decia que la receta «mueve el rail del hilo al otro lado». No lo movia:
lo CANCELABA. Ese era el defecto arreglado en `dee2c9e3a`; el README describe
ahora lo que el codigo hace.
- `waveform` daba `prefs` como default. Falso: waveform es el caso DELEGANTE,
no llama a `activeDir`, y sin prop no hay atributo ni lectura de prefs —
hereda. Su Slider compuesto es quien corre la cadena.
Y dos que estaban mal COLOCADAS: `calendar` listaba `dir` bajo `## ARIA` (no es
un atributo ARIA) y `accordion` bajo `## HTML Attributes` describiendolo como
«la prop cruda», cuando lo estampado es prop O prefs.
CORRECCION AL PRIMER BARRIDO, por la ley de autoria: escribio la cadena literal
`prop → prefs → 'ltr'` en 54 ficheros. Eso es la forma restate-instead-of-link
que `docs/authoring.md` §1 prohibe citando este mismo fallo («'7 families'
sobrevivio en tres docs semanas despues de que el canon pasara a 8»). Retirada:
la cadena se enuncia UNA vez, en el contrato; el README conserva su efecto.
Tres READMEs de eidos ensenaban todavia el selector prohibido —y los tres se
equivocaban sobre su PROPIO CSS, que ya usa `:dir(rtl)`—: waveform, nav-tree y
toolbar (este ademas invertia la precedencia: «prefs o prop»; gana la prop).
⚠️ `number-field` no tiene tabla de props que ampliar; su `dir` sigue en su
seccion `### Direction resolution`, ahora enlazando el contrato. Fabricar una
tabla de una fila diria «`dir` es su unica prop».
Handoff §9.20 con el cierre, el doble volteo y como se colo.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
⚠️ El grep de `dir?: Direction` devuelve **56** ficheros: el 56.º es
`field-langs` , y ahí el `dir` es un campo de `LangSpec` —la dirección de cada
IDIOMA del contenido—, no una prop del componente. Su proveedor no declara
`dir` . **Contar el grep sin abrir el fichero da un censo falso.**
- **0** componentes usan `:dir()` en su receta sin aceptar `dir` .
- **0** resolutores a pelo vivos — sólo quedan menciones a `getDir()` en
comentarios de documentación.
docs(direction): los 55 componentes que aceptan `dir` ya lo documentan
De los que aceptan la prop, la mayoria no la mencionaba en su README, y los que
si tenian seis redacciones distintas para el mismo hecho: «from Soma», «prefs»,
«soma config», «soma», «Override Soma defaults» y —la peor— «Inherited from Soma
when omitted», que se lee como herencia del PADRE, que es justo lo que la cadena
no hace.
Cada uno dice ahora QUE cambia la direccion EN ESE componente, leido de su propio
codigo (el JSDoc de la prop, los usos de `resolvedDir` en el provider, las reglas
`:dir(` de su receta), no una frase de plantilla. Y cada uno enlaza el contrato
una vez, en la primera mencion.
DOS AFIRMACIONES FALSAS, cazadas por el verificador adversarial:
- `feed` decia que la receta «mueve el rail del hilo al otro lado». No lo movia:
lo CANCELABA. Ese era el defecto arreglado en `dee2c9e3a`; el README describe
ahora lo que el codigo hace.
- `waveform` daba `prefs` como default. Falso: waveform es el caso DELEGANTE,
no llama a `activeDir`, y sin prop no hay atributo ni lectura de prefs —
hereda. Su Slider compuesto es quien corre la cadena.
Y dos que estaban mal COLOCADAS: `calendar` listaba `dir` bajo `## ARIA` (no es
un atributo ARIA) y `accordion` bajo `## HTML Attributes` describiendolo como
«la prop cruda», cuando lo estampado es prop O prefs.
CORRECCION AL PRIMER BARRIDO, por la ley de autoria: escribio la cadena literal
`prop → prefs → 'ltr'` en 54 ficheros. Eso es la forma restate-instead-of-link
que `docs/authoring.md` §1 prohibe citando este mismo fallo («'7 families'
sobrevivio en tres docs semanas despues de que el canon pasara a 8»). Retirada:
la cadena se enuncia UNA vez, en el contrato; el README conserva su efecto.
Tres READMEs de eidos ensenaban todavia el selector prohibido —y los tres se
equivocaban sobre su PROPIO CSS, que ya usa `:dir(rtl)`—: waveform, nav-tree y
toolbar (este ademas invertia la precedencia: «prefs o prop»; gana la prop).
⚠️ `number-field` no tiene tabla de props que ampliar; su `dir` sigue en su
seccion `### Direction resolution`, ahora enlazando el contrato. Fabricar una
tabla de una fila diria «`dir` es su unica prop».
Handoff §9.20 con el cierre, el doble volteo y como se colo.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- Acepta `dir` sin correr la cadena: sólo `waveform` (delega en el Slider, §9.18).
- ⚠️ El union escrito a mano sobrevive en `palabras` (`types.ts:130` + su test),
que está **excluido de escritura** — la afirmación «0 inline unions» vale para
el catálogo, no para el árbol entero.
**Verificación**: `check` sin errores nuevos (77 = la base con los ficheros de
`media-player` , de otra sesión). Tests de feed y switch en verde. En SSR los
cuatro estampan `dir="ltr"` ; en Chrome el switch pasa a `dir="rtl"` al girar la
preferencia y la regla dispara (`--_switch-thumb-offset`: `16px` →
`calc(-1 * 16px)` ). ⚠️ La transición del pulgar **no se pudo medir** : el panel
del navegador estaba oculto y ahí las transiciones CSS quedan congeladas —
`getComputedStyle` devuelve la matriz identidad y parece un defecto que no lo es.
**Queda fuera** (deuda ajena, §8.4): 91 ficheros de test que falsean
`Soma.require()` , cero tests de charts, `smoke` y `perm:check` sin ejecutar.
docs(direction): los 55 componentes que aceptan `dir` ya lo documentan
De los que aceptan la prop, la mayoria no la mencionaba en su README, y los que
si tenian seis redacciones distintas para el mismo hecho: «from Soma», «prefs»,
«soma config», «soma», «Override Soma defaults» y —la peor— «Inherited from Soma
when omitted», que se lee como herencia del PADRE, que es justo lo que la cadena
no hace.
Cada uno dice ahora QUE cambia la direccion EN ESE componente, leido de su propio
codigo (el JSDoc de la prop, los usos de `resolvedDir` en el provider, las reglas
`:dir(` de su receta), no una frase de plantilla. Y cada uno enlaza el contrato
una vez, en la primera mencion.
DOS AFIRMACIONES FALSAS, cazadas por el verificador adversarial:
- `feed` decia que la receta «mueve el rail del hilo al otro lado». No lo movia:
lo CANCELABA. Ese era el defecto arreglado en `dee2c9e3a`; el README describe
ahora lo que el codigo hace.
- `waveform` daba `prefs` como default. Falso: waveform es el caso DELEGANTE,
no llama a `activeDir`, y sin prop no hay atributo ni lectura de prefs —
hereda. Su Slider compuesto es quien corre la cadena.
Y dos que estaban mal COLOCADAS: `calendar` listaba `dir` bajo `## ARIA` (no es
un atributo ARIA) y `accordion` bajo `## HTML Attributes` describiendolo como
«la prop cruda», cuando lo estampado es prop O prefs.
CORRECCION AL PRIMER BARRIDO, por la ley de autoria: escribio la cadena literal
`prop → prefs → 'ltr'` en 54 ficheros. Eso es la forma restate-instead-of-link
que `docs/authoring.md` §1 prohibe citando este mismo fallo («'7 families'
sobrevivio en tres docs semanas despues de que el canon pasara a 8»). Retirada:
la cadena se enuncia UNA vez, en el contrato; el README conserva su efecto.
Tres READMEs de eidos ensenaban todavia el selector prohibido —y los tres se
equivocaban sobre su PROPIO CSS, que ya usa `:dir(rtl)`—: waveform, nav-tree y
toolbar (este ademas invertia la precedencia: «prefs o prop»; gana la prop).
⚠️ `number-field` no tiene tabla de props que ampliar; su `dir` sigue en su
seccion `### Direction resolution`, ahora enlazando el contrato. Fabricar una
tabla de una fila diria «`dir` es su unica prop».
Handoff §9.20 con el cierre, el doble volteo y como se colo.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### 9.20 · El corpus, y los dos defectos que el propio barrido destapó
El eje estaba cerrado en CÓDIGO pero no en DOCUMENTACIÓN: el contrato vivía sólo
aquí, en un handoff que `docs/README.md` declara «never a source of truth».
**El contrato ahora es canon**: [`docs/canon/direction-contract.md` ](../canon/direction-contract.md )
(E2, con `enforcement:` como `recipe-contract.md` ). Cubre la cadena, las DOS
atributos (`dir` crudo vs `data-dir` resuelto), cuándo el estampado es
obligatorio, la doctrina de selector, las trampas que el guard no ve, qué espeja
y qué no, y la mitad global de prefs. Registrado en `docs/README.md` (tabla E2 +
«I want to…»), en `docs/decisions.md` y enlazado desde los 55 READMEs.
**Dos defectos de CÓDIGO, encontrados al documentar** — ambos el mismo patrón
nuevo, y ambos en el eje que yo había dado por cerrado:
> **El doble volteo**: una regla `:dir(rtl)` cuyo cuerpo sólo reasigna
> propiedades LÓGICAS. La propiedad ya se había espejado cuando la regla
> matchea, así que moverla de `inline-start` a `inline-end` la devuelve al
> punto de partida — y, como las declaraciones hermanas se quedan quietas,
> aterriza en el borde OPUESTO al de la cosa a la que pertenece.
- `feed.css` — el raíl del hilo: sangría a un lado, raíl al otro.
- `tree-view.css` — la guía de indentación: aparcada al otro lado de su subárbol.
Medido en Chrome tras el arreglo: `feed` da `padding-left:20px` +`border-left:2px`
en LTR y `padding-right:20px` +`border-right:2px` en RTL; la guía de `tree-view`
resuelve a `left:16px` en LTR y `right:16px` en RTL. ⚠️ **No pude ver los
píxeles**: el panel del navegador no estaba visible.
⚠️⚠️ **Cómo se colaron** : la migración §6.3 (`[dir='rtl']` → `:dir(rtl)` , commit
`ec533845d` ) cambió el SELECTOR de las 12 reglas sin preguntarse si el CUERPO
era correcto. Una migración mecánica hereda los defectos que traduce. **RTL-1 no
ve esta forma** — busca un desplazamiento FÍSICO. Extender el guard para cazar
«bloque `:dir()` cuyas declaraciones son todas lógicas» queda pendiente.
**Correcciones al propio barrido** (tres verificadores adversariales):
- La ley de autoría prohíbe copiar el canon, y el primer barrido escribió la
cadena literal en 54 READMEs. Retirada: cada README conserva SU efecto y
enlaza el contrato una vez.
- `RTL-1` estaba anunciado como guard de todo el capítulo. Sólo cubre §4.
- «nunca lee al padre» era absoluto en CLAUDE.md y en la arquitectura, y borraba
la composición sancionada en el punto de llamada (submenús).
- `component-guide.md` se contradecía a sí mismo: el estampado es CONDICIONAL.
- Las cuatro filas nuevas del checklist usaban una aplicabilidad (`directional`)
que ni la leyenda ni `component-audit.ts` conocen.
- `soma-architecture.md` §3.4 enseñaba el resolutor a pelo que el eje retiró.
- `html.ts` enseñaba en su JSDoc el union a mano que E-2.5 ahora prohíbe.
**Divergencia DECLARADA** (§7 del contrato): la familia `chart` no tiene prop ni
proveedor, y resuelve leyendo `getComputedStyle(node).direction` . Es el único
sitio del catálogo que hace lo que §1 prohíbe. Se registra en vez de esconderse;
el arreglo conocido es que los charts acepten `dir` .
**Guards**: `docs:check` 0/0 sobre 564 docs · `rtl:check` 1 error, `palabras`
(preexistente, excluido) · `eidos-lint feed` /`tree-view` invalid 0 · ningún test
afirma sobre las dos reglas retiradas.
docs(direction): §10, la cola para manana — RTL-2 ya viene especificada y medida
La unica deuda del eje es el guard del doble volteo, y es la forma que se me
escapo DOS veces (en la migracion §6.3 y en el cierre §9.19). El handoff no la
deja como idea: la deja lista para implementar.
LA FIRMA, mas estrecha que «bloque `:dir()` con puras logicas» —eso daria falsos
positivos sobre reglas legitimas que cambian un valor bajo RTL—: dentro de un
bloque con `:dir()`, la MISMA familia logica aparece como `-start` y como `-end`,
una con valor NEUTRO (`0`/`auto`/`none`) y la otra cargando el valor. Ese es el
gesto «apago un lado y lo repinto en el otro», que cancela un espejo en vez de
crearlo.
MEDIDA ANTES DE ESCRIBIRLA, que es lo que la hace accionable:
- contra `dee2c9e3a^` (antes del arreglo): 2/2 cazados
- contra HEAD: 0 hallazgos sobre 18 bloques `:dir()`
Nace verde y con recall demostrado sobre defectos REALES. Si al implementarla
sale distinto de 0, es un hallazgo nuevo, no un fallo de la regla.
Con las cuatro piezas a tocar (`rtl-lint.ts` y su docblock —que es doctrina—,
`scripts/rtl-check.ts`, y los casos de test, positivos Y negativos), tres
decisiones que NO dejo tomadas (`:dir(ltr)` tambien; escape hatch propio o
reutilizar `rtl-physical:`; regla aparte o ensanchar RTL-1), y el aviso de que
al aterrizar quedan DOS textos obsoletos el mismo dia — los escribi diciendo que
el guard no ve esta forma.
⚠️ NO copiar mi prototipo: usa una regex ingenua sin anidamiento ni comentarios.
`rtl-lint.ts` ya tiene `parseBlocks()`, que da exactamente lo que hace falta.
§10.2 lleva el resto por valor (los charts deberian aceptar `dir` — la
divergencia declarada en §7 del contrato; la tabla de props ausente de
number-field; la numeracion duplicada preexistente del checklist) y §10.3 el
estado del arbol.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
---
## §10 · Cola para mañana
feat(direction): RTL-2 — el guard del doble volteo, la forma que se me colo dos veces
Cierra §10.1, la ultima deuda del eje. `dee2c9e3a` arreglo los dos defectos pero
dejo la FORMA sin guard, y es la que sobrevivio a la migracion §6.3 y a que yo
diera el eje por cerrado en §9.19.
LA FIRMA — dentro de un bloque cuyo prelude tiene `:dir()`, la misma familia
logica (`border`/`padding`/`margin`/`inset`-`inline`) declarada en LAS DOS caras,
EXACTAMENTE UNA con valor neutro (`0`, `auto`, `none`, `initial`, `unset`,
`revert`). Ese desequilibrio ES el espejo cancelado: la propiedad ya se habia
volteado cuando la regla matchea, asi que reubicarla la devuelve al punto de
partida y la deja en el borde opuesto al de sus hermanas.
Es mas estrecha que «bloque `:dir()` con puras logicas» a proposito. Una regla
que CAMBIA un valor bajo RTL sin reubicarlo es legitima —asimetrico por diseño
existe—, y las dos caras con valor son un autor describiendo dos bordes reales.
Lo que delata el defecto es el PAR, una cara apagada. Hay un test negativo por
cada uno de esos casos, que son los que evitan que la regla se vuelva ruido.
LAS TRES DECISIONES que el handoff dejaba abiertas:
- `:dir(ltr)` tambien entra. Igual de sospechosa, y no anade ruido.
- Marcador PROPIO, `rtl-mirror: <reason>`. Reutilizar `rtl-physical:` mentiria:
aqui no hay nada fisico, y un marcador que miente es peor que ninguno. Mismo
mecanismo (`exemptLines` toma ahora el patron por parametro), dos vocabularios.
Un test comprueba que el marcador de RTL-1 NO exime a RTL-2.
- Regla aparte, `lintRtlMirror()` con su propio `RtlMirrorFinding`. Los 14 tests
de RTL-1 quedan intactos y el tipo lleva los campos que importan
(`family`/`neutral`/`payload`) en vez de forzar los de RTL-1.
VERIFICACION, en este orden:
- `rtl-lint.test.ts` 27/27 (14 de RTL-1 intactos + 13 nuevos).
- RECALL con el runner COMPLETO contra el arbol pre-arreglo: restaure los dos
ficheros de `dee2c9e3a^` sobre el arbol, corri `rtl:check` y los devolvi con
`git checkout` en el mismo bloque. **2/2 cazados** (`feed.css:147`,
`tree-view.css:211`). Probar la funcion no basta: el runner es lo que corre.
- `rtl:check` sobre HEAD: 1 error, el de `palabras`, preexistente y excluido.
- `docs:check` 0/0 sobre 564 docs. `check` 77 = linea base.
Un defecto de presentacion salio al hacerlo: el `calc()` multilinea de tree-view
partia el mensaje por el primer salto. Los campos que RTL-2 emite colapsan el
whitespace; con test.
Actualizados los textos que el handoff avisaba que quedarian obsoletos: el
`enforcement:` y §4 del contrato, la fila RTL del build contract, la fila X-1.6
del checklist, y los docblocks de `rtl-lint.ts` y `rtl-check.ts` — que son
doctrina, no adorno.
⚠️ Sigue siendo cierto lo que NINGUNA de las dos reglas ve: leen texto CSS. Un
`transform` inline escrito por JS, un preset de motion compartido y la geometria
SVG siguen necesitando el ojo en RTL.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### 10.1 · RTL-2 — el guard del doble volteo · **CERRADA** (ver §11)
docs(direction): §10, la cola para manana — RTL-2 ya viene especificada y medida
La unica deuda del eje es el guard del doble volteo, y es la forma que se me
escapo DOS veces (en la migracion §6.3 y en el cierre §9.19). El handoff no la
deja como idea: la deja lista para implementar.
LA FIRMA, mas estrecha que «bloque `:dir()` con puras logicas» —eso daria falsos
positivos sobre reglas legitimas que cambian un valor bajo RTL—: dentro de un
bloque con `:dir()`, la MISMA familia logica aparece como `-start` y como `-end`,
una con valor NEUTRO (`0`/`auto`/`none`) y la otra cargando el valor. Ese es el
gesto «apago un lado y lo repinto en el otro», que cancela un espejo en vez de
crearlo.
MEDIDA ANTES DE ESCRIBIRLA, que es lo que la hace accionable:
- contra `dee2c9e3a^` (antes del arreglo): 2/2 cazados
- contra HEAD: 0 hallazgos sobre 18 bloques `:dir()`
Nace verde y con recall demostrado sobre defectos REALES. Si al implementarla
sale distinto de 0, es un hallazgo nuevo, no un fallo de la regla.
Con las cuatro piezas a tocar (`rtl-lint.ts` y su docblock —que es doctrina—,
`scripts/rtl-check.ts`, y los casos de test, positivos Y negativos), tres
decisiones que NO dejo tomadas (`:dir(ltr)` tambien; escape hatch propio o
reutilizar `rtl-physical:`; regla aparte o ensanchar RTL-1), y el aviso de que
al aterrizar quedan DOS textos obsoletos el mismo dia — los escribi diciendo que
el guard no ve esta forma.
⚠️ NO copiar mi prototipo: usa una regex ingenua sin anidamiento ni comentarios.
`rtl-lint.ts` ya tiene `parseBlocks()`, que da exactamente lo que hace falta.
§10.2 lleva el resto por valor (los charts deberian aceptar `dir` — la
divergencia declarada en §7 del contrato; la tabla de props ausente de
number-field; la numeracion duplicada preexistente del checklist) y §10.3 el
estado del arbol.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
feat(direction): RTL-2 — el guard del doble volteo, la forma que se me colo dos veces
Cierra §10.1, la ultima deuda del eje. `dee2c9e3a` arreglo los dos defectos pero
dejo la FORMA sin guard, y es la que sobrevivio a la migracion §6.3 y a que yo
diera el eje por cerrado en §9.19.
LA FIRMA — dentro de un bloque cuyo prelude tiene `:dir()`, la misma familia
logica (`border`/`padding`/`margin`/`inset`-`inline`) declarada en LAS DOS caras,
EXACTAMENTE UNA con valor neutro (`0`, `auto`, `none`, `initial`, `unset`,
`revert`). Ese desequilibrio ES el espejo cancelado: la propiedad ya se habia
volteado cuando la regla matchea, asi que reubicarla la devuelve al punto de
partida y la deja en el borde opuesto al de sus hermanas.
Es mas estrecha que «bloque `:dir()` con puras logicas» a proposito. Una regla
que CAMBIA un valor bajo RTL sin reubicarlo es legitima —asimetrico por diseño
existe—, y las dos caras con valor son un autor describiendo dos bordes reales.
Lo que delata el defecto es el PAR, una cara apagada. Hay un test negativo por
cada uno de esos casos, que son los que evitan que la regla se vuelva ruido.
LAS TRES DECISIONES que el handoff dejaba abiertas:
- `:dir(ltr)` tambien entra. Igual de sospechosa, y no anade ruido.
- Marcador PROPIO, `rtl-mirror: <reason>`. Reutilizar `rtl-physical:` mentiria:
aqui no hay nada fisico, y un marcador que miente es peor que ninguno. Mismo
mecanismo (`exemptLines` toma ahora el patron por parametro), dos vocabularios.
Un test comprueba que el marcador de RTL-1 NO exime a RTL-2.
- Regla aparte, `lintRtlMirror()` con su propio `RtlMirrorFinding`. Los 14 tests
de RTL-1 quedan intactos y el tipo lleva los campos que importan
(`family`/`neutral`/`payload`) en vez de forzar los de RTL-1.
VERIFICACION, en este orden:
- `rtl-lint.test.ts` 27/27 (14 de RTL-1 intactos + 13 nuevos).
- RECALL con el runner COMPLETO contra el arbol pre-arreglo: restaure los dos
ficheros de `dee2c9e3a^` sobre el arbol, corri `rtl:check` y los devolvi con
`git checkout` en el mismo bloque. **2/2 cazados** (`feed.css:147`,
`tree-view.css:211`). Probar la funcion no basta: el runner es lo que corre.
- `rtl:check` sobre HEAD: 1 error, el de `palabras`, preexistente y excluido.
- `docs:check` 0/0 sobre 564 docs. `check` 77 = linea base.
Un defecto de presentacion salio al hacerlo: el `calc()` multilinea de tree-view
partia el mensaje por el primer salto. Los campos que RTL-2 emite colapsan el
whitespace; con test.
Actualizados los textos que el handoff avisaba que quedarian obsoletos: el
`enforcement:` y §4 del contrato, la fila RTL del build contract, la fila X-1.6
del checklist, y los docblocks de `rtl-lint.ts` y `rtl-check.ts` — que son
doctrina, no adorno.
⚠️ Sigue siendo cierto lo que NINGUNA de las dos reglas ve: leen texto CSS. Un
`transform` inline escrito por JS, un preset de motion compartido y la geometria
SVG siguen necesitando el ojo en RTL.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
Era la única deuda del eje. §9.20 dejó los dos defectos arreglados pero **la forma
docs(direction): §10, la cola para manana — RTL-2 ya viene especificada y medida
La unica deuda del eje es el guard del doble volteo, y es la forma que se me
escapo DOS veces (en la migracion §6.3 y en el cierre §9.19). El handoff no la
deja como idea: la deja lista para implementar.
LA FIRMA, mas estrecha que «bloque `:dir()` con puras logicas» —eso daria falsos
positivos sobre reglas legitimas que cambian un valor bajo RTL—: dentro de un
bloque con `:dir()`, la MISMA familia logica aparece como `-start` y como `-end`,
una con valor NEUTRO (`0`/`auto`/`none`) y la otra cargando el valor. Ese es el
gesto «apago un lado y lo repinto en el otro», que cancela un espejo en vez de
crearlo.
MEDIDA ANTES DE ESCRIBIRLA, que es lo que la hace accionable:
- contra `dee2c9e3a^` (antes del arreglo): 2/2 cazados
- contra HEAD: 0 hallazgos sobre 18 bloques `:dir()`
Nace verde y con recall demostrado sobre defectos REALES. Si al implementarla
sale distinto de 0, es un hallazgo nuevo, no un fallo de la regla.
Con las cuatro piezas a tocar (`rtl-lint.ts` y su docblock —que es doctrina—,
`scripts/rtl-check.ts`, y los casos de test, positivos Y negativos), tres
decisiones que NO dejo tomadas (`:dir(ltr)` tambien; escape hatch propio o
reutilizar `rtl-physical:`; regla aparte o ensanchar RTL-1), y el aviso de que
al aterrizar quedan DOS textos obsoletos el mismo dia — los escribi diciendo que
el guard no ve esta forma.
⚠️ NO copiar mi prototipo: usa una regex ingenua sin anidamiento ni comentarios.
`rtl-lint.ts` ya tiene `parseBlocks()`, que da exactamente lo que hace falta.
§10.2 lleva el resto por valor (los charts deberian aceptar `dir` — la
divergencia declarada en §7 del contrato; la tabla de props ausente de
number-field; la numeracion duplicada preexistente del checklist) y §10.3 el
estado del arbol.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
sigue sin guard**, y es la forma que se me escapó DOS veces: en la migración
§6.3 y en el cierre §9.19.
**Ya está especificada y probada.** No hay que investigar, hay que implementarla.
**La firma** — más estrecha que «bloque `:dir()` con puras lógicas», que daría
falsos positivos sobre reglas legítimas que cambian un valor bajo RTL:
> Dentro de un bloque cuyo prelude contiene `:dir()`, la MISMA familia lógica
> aparece como `-start` y como `-end`, **una de las dos con valor neutro**
> (`0`, `auto`, `none`, `initial`, `unset`) y la otra cargando el valor.
Familias: `border-inline-*` , `padding-inline-*` , `margin-inline-*` ,
`inset-inline-*` . Ése es exactamente el gesto «apago un lado y lo repinto en el
otro», que es cancelar un espejo, nunca crearlo.
**Recall y ruido, ya medidos** (prototipo en Python, contra el árbol real):
| | resultado |
| --------------------------------------- | ----------------------------------------------------- |
| Contra `dee2c9e3a^` (antes del arreglo) | **2/2 cazados** — `feed.css:145` , `tree-view.css:209` |
| Contra HEAD | **0 hallazgos** sobre **18** bloques `:dir()` |
O sea: la regla nace VERDE y con recall demostrado sobre defectos reales. Si al
implementarla sale algo distinto de 0, **es un hallazgo nuevo, no un bug de la
regla** — míralo antes de relajar el matcher.
**Dónde va**: `src/uix/eidos/rtl-lint.ts` . ⚠️ **NO copies mi prototipo** : usa una
regex ingenua que no maneja bloques anidados ni comentarios. El fichero ya tiene
`parseBlocks()` (línea ~175), que resuelve anidamiento, comillas, paréntesis y
comentarios, y ya te da `{ prelude, decls[] }` — que es justo lo que la regla
necesita. Reutilízalo.
Piezas a tocar:
1. `rtl-lint.ts` — nueva familia de constantes + la comprobación. `RtlFinding`
hoy tiene forma de RTL-1 (`translate` + `anchors` ); necesitarás un campo
discriminante (`rule: 'RTL-1' | 'RTL-2'`) o un segundo tipo.
2. El docblock del módulo — la lista «Deliberately NOT flagged» y el enunciado
de las reglas. Es doctrina, no adorno.
3. `scripts/rtl-check.ts` (~línea 44) formatea el mensaje de RTL-1 a mano;
necesita rama.
4. `src/uix/eidos/rtl-lint.test.ts` — casos POSITIVOS: las dos formas reales
(cópialas de `dee2c9e3a^` ). Casos NEGATIVOS, que son los que evitan que la
regla se vuelva ruido: un `:dir()` que cambia un valor sin relocalizarlo · un
bloque con sólo `-start` · uno con las dos caras y NINGUNA neutra · un bloque
sin `:dir()` en el prelude.
**Tres decisiones que son tuyas**, no las dejo tomadas:
- ¿`:dir(ltr)` también? Mi prototipo escanea ambos. Un `:dir(ltr)` con esta
firma es igual de sospechoso, pero es un gesto más raro.
- ¿Escape hatch propio o reutilizar `rtl-physical:` ? A favor de reutilizar: un
solo vocabulario. En contra: la razón que se declara es otra.
- ¿RTL-2 aparte o ensanchar RTL-1? Yo lo haría aparte — otra firma, otro
mensaje, y la fila `X-1.6` del checklist ya nombra RTL-1.
⚠️ **Cuando aterrice, DOS textos quedan obsoletos el mismo día** (los escribí
diciendo que el guard no ve esta forma, y dejarán de ser verdad):
- `docs/canon/direction-contract.md` §4, subsección «The double flip» — última
frase: «RTL-1 does not see this shape».
- `docs/process/CONTINUE-direction.md` §9.20 y la memoria
`reference_dir_rule_double_flip` .
**Verificación**: `npm run rtl:check` debe seguir dando **1 error** — el de
`palabras-chrome.css:446` , preexistente y excluido de escritura. Cualquier otro
número hay que explicarlo. Más `npx vitest run src/uix/eidos/rtl-lint.test.ts` .
### 10.2 · El resto de la cola, por valor
- **Los charts deberían aceptar `dir` .** Es la divergencia DECLARADA en §7 del
contrato: la familia no tiene prop ni provider y resuelve leyendo
`getComputedStyle(node).direction` (`chart/rtl.svelte.ts` + duplicado inline en
`chart.svelte:175-198` ). Mientras siga así, un chart se espeja con la página y
no se puede voltear para un subárbol. Consumidores: `calendar-heatmap` ,
`funnel` , `heatmap` , `radar-chart` .
- **`number-field/README.md` no tiene tabla de props** (ninguna, no es que le
falte la fila de `dir` ). Su `dir` vive en `### Direction resolution` . Fabricar
una tabla de una fila diría «`dir` es su única prop» — hace falta la tabla
entera o nada. **Decisión pendiente.**
- **Numeración duplicada** en el checklist de construcción de
`component-guide.md` : `38` y `39` aparecen dos veces, y el ítem 41 nuevo quedó
entre el 13 y el 14. Es deuda PREEXISTENTE; renumerar rompería las citas
(«los ítems 34– 35 son el mecanismo», A32). Requiere decisión, no barrido.
- **`palabras`** conserva el union a mano (`types.ts:130` + su test). Excluido
de escritura por decisión del usuario.
- **Deuda ajena (§8.4), intacta**: 91 ficheros de test que falsean
`Soma.require()` · cero tests de gráficas · `smoke` y `perm:check` sin correr.
### 10.3 · Estado del árbol
Commiteado hasta `961c1bf8b` , **sin pushear** — el remoto es `gita` , no hay
`origin` . Sin tocar en el árbol: `.claude/settings.local.json` ,
`src/arts/adom/__scratch-verify.ts` y `web/routes/alpha/` (este último **no se
commitea nunca**). `check` = 77, que es la línea base con los ficheros de
`media-player` de la otra sesión.
feat(direction): RTL-2 — el guard del doble volteo, la forma que se me colo dos veces
Cierra §10.1, la ultima deuda del eje. `dee2c9e3a` arreglo los dos defectos pero
dejo la FORMA sin guard, y es la que sobrevivio a la migracion §6.3 y a que yo
diera el eje por cerrado en §9.19.
LA FIRMA — dentro de un bloque cuyo prelude tiene `:dir()`, la misma familia
logica (`border`/`padding`/`margin`/`inset`-`inline`) declarada en LAS DOS caras,
EXACTAMENTE UNA con valor neutro (`0`, `auto`, `none`, `initial`, `unset`,
`revert`). Ese desequilibrio ES el espejo cancelado: la propiedad ya se habia
volteado cuando la regla matchea, asi que reubicarla la devuelve al punto de
partida y la deja en el borde opuesto al de sus hermanas.
Es mas estrecha que «bloque `:dir()` con puras logicas» a proposito. Una regla
que CAMBIA un valor bajo RTL sin reubicarlo es legitima —asimetrico por diseño
existe—, y las dos caras con valor son un autor describiendo dos bordes reales.
Lo que delata el defecto es el PAR, una cara apagada. Hay un test negativo por
cada uno de esos casos, que son los que evitan que la regla se vuelva ruido.
LAS TRES DECISIONES que el handoff dejaba abiertas:
- `:dir(ltr)` tambien entra. Igual de sospechosa, y no anade ruido.
- Marcador PROPIO, `rtl-mirror: <reason>`. Reutilizar `rtl-physical:` mentiria:
aqui no hay nada fisico, y un marcador que miente es peor que ninguno. Mismo
mecanismo (`exemptLines` toma ahora el patron por parametro), dos vocabularios.
Un test comprueba que el marcador de RTL-1 NO exime a RTL-2.
- Regla aparte, `lintRtlMirror()` con su propio `RtlMirrorFinding`. Los 14 tests
de RTL-1 quedan intactos y el tipo lleva los campos que importan
(`family`/`neutral`/`payload`) en vez de forzar los de RTL-1.
VERIFICACION, en este orden:
- `rtl-lint.test.ts` 27/27 (14 de RTL-1 intactos + 13 nuevos).
- RECALL con el runner COMPLETO contra el arbol pre-arreglo: restaure los dos
ficheros de `dee2c9e3a^` sobre el arbol, corri `rtl:check` y los devolvi con
`git checkout` en el mismo bloque. **2/2 cazados** (`feed.css:147`,
`tree-view.css:211`). Probar la funcion no basta: el runner es lo que corre.
- `rtl:check` sobre HEAD: 1 error, el de `palabras`, preexistente y excluido.
- `docs:check` 0/0 sobre 564 docs. `check` 77 = linea base.
Un defecto de presentacion salio al hacerlo: el `calc()` multilinea de tree-view
partia el mensaje por el primer salto. Los campos que RTL-2 emite colapsan el
whitespace; con test.
Actualizados los textos que el handoff avisaba que quedarian obsoletos: el
`enforcement:` y §4 del contrato, la fila RTL del build contract, la fila X-1.6
del checklist, y los docblocks de `rtl-lint.ts` y `rtl-check.ts` — que son
doctrina, no adorno.
⚠️ Sigue siendo cierto lo que NINGUNA de las dos reglas ve: leen texto CSS. Un
`transform` inline escrito por JS, un preset de motion compartido y la geometria
SVG siguen necesitando el ojo en RTL.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
---
## §11 · RTL-2 implementada — el guard del doble volteo
§10.1 cerrada. La regla existe, corre en `npm run rtl:check` junto a RTL-1, y
nace **verde con recall demostrado** .
**Las tres decisiones que §10.1 dejaba abiertas, tomadas:**
1. ** `:dir(ltr)` también entra.** La firma es igual de sospechosa y no cuesta
nada: 0 falsos positivos sobre el árbol.
2. **Marcador propio, `rtl-mirror: <reason>`.** Reutilizar `rtl-physical:`
mentiría — aquí no hay nada físico, y un marcador que miente es peor que
ninguno. Mismo mecanismo (`exemptLines` pasó a tomar el patrón por
parámetro), dos vocabularios precisos. Hay un test que comprueba que el
marcador de RTL-1 **no** exime a RTL-2.
3. **Regla aparte** : `lintRtlMirror()` con su propio `RtlMirrorFinding` . Los 14
tests de RTL-1 no se tocaron, y el tipo lleva los campos que importan
(`family` · `neutral` · `payload` ) en vez de forzar los de RTL-1.
**La firma implementada** — dentro de un bloque cuyo prelude tiene `:dir()` , la
misma familia lógica (`border`/`padding`/`margin`/`inset`-`inline`) declarada en
**las dos caras**, **exactamente una** con valor neutro (`0`, `auto` , `none` ,
`initial` , `unset` , `revert` ). El desequilibrio ES el espejo cancelado.
**Verificación, en este orden:**
| | resultado |
| ----------------------------------------------------- | ----------------------------------------------------- |
| `rtl-lint.test.ts` | **27/27** (14 de RTL-1 intactos + 13 nuevos) |
| Guard real contra el árbol pre-arreglo (`dee2c9e3a^`) | **2/2 cazados** — `feed.css:147` , `tree-view.css:211` |
| `npm run rtl:check` sobre HEAD | **1 error** , el de `palabras` , preexistente |
| `docs:check` | 0/0 sobre 564 docs |
| `check` | 77 = línea base |
La prueba de recall se hizo restaurando los dos ficheros del commit anterior
SOBRE el árbol, corriendo el guard completo y devolviéndolos con `git checkout`
en el mismo bloque — probar el runner entero, no sólo la función.
**Un defecto de presentación que salió al hacerlo**: el `calc()` multilínea de
`tree-view` partía el mensaje de error por el primer salto de línea. Los campos
que RTL-2 emite colapsan el whitespace; hay test.
**Textos actualizados** (los que §10.1 avisaba que quedarían obsoletos): el
`enforcement:` y §4 del contrato · la fila RTL del build contract en
`component-guide.md` · la fila `X-1.6` del checklist · los docblocks de
`rtl-lint.ts` y `rtl-check.ts` , que son doctrina.
⚠️ **Sigue siendo cierto lo que RTL-1 y RTL-2 NO ven** : leen texto CSS. Un
`transform` inline escrito por JS, un preset de motion compartido y la geometría
SVG siguen necesitando el ojo en RTL. El docblock lo dice; no lo borres al
tocarlo.
**Queda**: §10.2 sin cambios — los charts deberían aceptar `dir` (divergencia
declarada §7 del contrato) · `number-field` sin tabla de props · numeración
duplicada preexistente en `component-guide.md` · deuda ajena §8.4.