|
|
|
|
@ -0,0 +1,229 @@
|
|
|
|
|
# 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.
|