# 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 `