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

230 lines
12 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 |
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 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 `<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.

Powered by TurnKey Linux.