|
|
# CONTINUE — el eje de dirección (RTL/LTR)
|
|
|
|
|
|
> **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 |
|
|
|
| `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
|
|
|
|
|
|
### 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.
|
|
|
|
|
|
⚠️ **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.
|
|
|
|
|
|
✅ **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.
|
|
|
|
|
|
---
|
|
|
|
|
|
## 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.
|
|
|
|
|
|
**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.
|
|
|
|
|
|
### 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.
|
|
|
|
|
|
### 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`.
|
|
|
|
|
|
### 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.
|
|
|
|
|
|
### 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.
|
|
|
|
|
|
**`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.
|
|
|
|
|
|
### 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.
|
|
|
|
|
|
### 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.
|
|
|
|
|
|
### 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).
|
|
|
|
|
|
---
|
|
|
|
|
|
## 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 |
|
|
|
|
|
|
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.
|
|
|
|
|
|
---
|
|
|
|
|
|
## 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.
|
|
|
|
|
|
Dos señales, ninguna verificada:
|
|
|
- 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)
|
|
|
|
|
|
- **`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
|
|
|
|
|
|
- ~~`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`.
|
|
|
|
|
|
### 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
|
|
|
`--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.
|
|
|
|
|
|
### 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 |
|
|
|
|
|
|
⚠️ **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).
|
|
|
|
|
|
### 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) |
|
|
|
| --- | --- | --- |
|
|
|
| `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.
|