# 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 | 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 `` 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 que no existe (PRIMERO: pequeño y evita la clase entera) **Nada cruza `transform: translate*` con `inset-inline*`.** Hay **39 transforms** en el CSS de eidos y ningún script los mira. `eidos-lint` sólo clasifica *selectores* `[data-*]`, no declaraciones, y el guard de la fila **RTL** del contrato de construcción (`docs/guides/component-guide.md:37`) es literalmente «rule (LIVE)» — o sea, **revisión humana**. Por eso el bug llegó a producción y lo cazó el ojo del usuario, no el CI. Regla a implementar: en una misma regla CSS, `transform: translateX/translate(` + cualquier `inset-inline*` / `margin-inline*` / `padding-inline*` = error. El eje de BLOQUE (`inset-block-start` + `translateY`) es correcto y **no** debe marcarse. ### 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. ### 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. ### 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 `
` 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"`, `` 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.