You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/docs/process/CONTINUE-direction.md

625 lines
33 KiB

# CONTINUE — el eje de dirección (RTL/LTR)
> **Kickoff**: _"Lee `docs/process/CONTINUE-direction.md` y empieza por §2.1."_
> **Fecha**: 2026-08-02 · Rama `alpha-0.1-dir-prefs` (sale de `alpha-0.1-sec-dom`).
> **§1 es lo que ya está cerrado — no lo rehagas.** Lo abierto empieza en §2.
>
> 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 |
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
| **sin commitear** | **§2.1 cerrada**: el guard RTL-1 + los 23 hallazgos + los 12 `[dir='rtl']` + el pin del `<main>` (§6) |
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:**
- **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) |
⚠️ **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 |
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** |
⚠️ **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
```
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í:
- 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
```
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).

Powered by TurnKey Linux.