docs(direction): 9.19, el alias y los cuatro de solo-CSS — el eje queda cerrado

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
alpha-0.1-dir-prefs
dev 2 months ago
parent d490f53e95
commit 76c807d3c7

@ -14,13 +14,13 @@
## 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 |
| 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 |
| `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**:
@ -97,17 +97,17 @@ ahí pasaría hiciera lo que hiciera el código.
### 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 |
| 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*
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.
@ -151,7 +151,7 @@ señalado y no tocado por ser ajeno al eje.
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
.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.
@ -169,25 +169,25 @@ señalado y no tocado por ser ajeno al eje.
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ó** |
| | ¿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.
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.
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
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`
@ -248,6 +248,7 @@ 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.
@ -263,11 +264,11 @@ guard no puede ver esa compensación.
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) |
| 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
@ -293,10 +294,10 @@ llega a esa media query:
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 |
| 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`.
@ -391,13 +392,13 @@ indicador del `radio-group`, que tampoco lo lista.
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** |
| | 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
@ -459,7 +460,7 @@ resolvía `align: 'start'` a `r.left` SIEMPRE — una alineación LÓGICA clavad
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
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.
@ -467,6 +468,7 @@ 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).
@ -572,8 +574,8 @@ 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
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
@ -653,16 +655,16 @@ 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 |
| | 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
@ -704,6 +706,7 @@ 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
@ -782,14 +785,14 @@ que recorrer sus controles.
`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 |
| | 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).
@ -802,11 +805,11 @@ derecha y lo lleva **negativo** hacia el final de lectura, hasta
Safari 14.1+). Dos traducciones privadas en el provider, y cada consumidor toma
la suya:
| Sigue el eje de **lectura** | Se queda **físico** |
| --- | --- |
| 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 | |
| `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
@ -842,11 +845,11 @@ 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%`) |
| | 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
@ -855,11 +858,11 @@ 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 |
| 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.
@ -880,11 +883,11 @@ 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 |
| 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**.
@ -908,11 +911,11 @@ fichero y uno de los 74. Preexistente y ajeno al eje — señalado, no tocado.
Commits `3c3d8e1ac` (motion) + `b0f995a3b` (metrics). `check` = **74 = línea
base** · 32/32 en motion + metrics · `rtl:check` = 1, el de siempre.
| pieza | veredicto |
| --- | --- |
| 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** |
| `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
@ -977,13 +980,13 @@ eidos, y sólo había corrido los tests del ámbito. La pasada COMPLETA da
`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 |
| 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`,
@ -1015,8 +1018,8 @@ Commit del arreglo: ver `git log`. `check` = **74 = línea base** · `rtl:check`
**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) |
| --- | --- | --- |
| | LTR | RTL (antes) |
| ------ | ---------------------- | ------------------------------ |
| `prev` | lado izq, apunta izq ✓ | lado **der**, apunta **izq** ✗ |
| `next` | lado der, apunta der ✓ | lado **izq**, apunta **der** ✗ |
@ -1145,13 +1148,13 @@ un eje de lectura. Y react-aria distingue `ColorArea` (rectangular, espeja) de
Medido antes: pinchar el MISMO punto físico daba el mismo valor en las dos
direcciones, y el degradado no seguía al thumb.
| Pieza | Qué necesitó |
| --- | --- |
| área · puntero | la fracción física se espeja sobre el canal |
| área · thumb | ancla **FÍSICA** (`left`) alimentada ya espejada |
| área · flechas | `ArrowRight` mueve a la derecha ⇒ BAJA el canal |
| Pieza | Qué necesitó |
| ----------------- | ---------------------------------------------------- |
| área · puntero | la fracción física se espeja sobre el canal |
| área · thumb | ancla **FÍSICA** (`left`) alimentada ya espejada |
| área · flechas | `ArrowRight` mueve a la derecha ⇒ BAJA el canal |
| área · degradados | saturación + arcoíris de hue voltean con `:dir(rtl)` |
| sliders de canal | **sólo el degradado** |
| sliders de canal | **sólo el degradado** |
⚠️ El thumb conserva `left` FÍSICO a propósito: el recipe lo centra con
`translate(-50%,-50%)`, y ancla lógica + translate físico es justo la trampa
@ -1204,7 +1207,7 @@ IZQUIERDA. **Ahí es donde debe estar en RTL** (el recipe lo coloca con
`inset-inline-end`), pero la matemática no acompañaba:
```ts
if (edge.includes('e')) width = start.width + dx; // dx es FÍSICO
if (edge.includes('e')) width = start.width + dx; // dx es FÍSICO
```
Los handles se colocan con insets LÓGICOS, así que el marcado `e` es el borde
@ -1226,11 +1229,11 @@ sólo montan `e`, `s` y el grip `se`. Ahora se acota por el hueco disponible.
El grip además mentía en RTL: `cursor: nwse-resize` (↘↖) y los chevrones a
`-45deg / bottom right` son físicos y no tienen forma lógica → par `:dir(rtl)`.
| | resultado |
| --- | --- |
| RTL · arrastre hacia fuera | 300 → 409 de ancho, borde DERECHO fijo en 803 |
| RTL · arrastre largo | se detiene exacto en el borde del stage (`left 0`) |
| LTR | sin cambios: 300 → 436, borde izquierdo fijo en 24 |
| | resultado |
| -------------------------- | -------------------------------------------------- |
| RTL · arrastre hacia fuera | 300 → 409 de ancho, borde DERECHO fijo en 803 |
| RTL · arrastre largo | se detiene exacto en el borde del stage (`left 0`) |
| LTR | sin cambios: 300 → 436, borde izquierdo fijo en 24 |
⚠️ **LECCIÓN, y es la tercera vez que aparece en esta sesión**: §6.10 y §9.5
dieron el float-panel por revisado **midiendo sólo la POSICIÓN del panel**.
@ -1326,7 +1329,8 @@ un caso raro.
Pendiente de criterio. Si se hace, el patrón es mecánico y ya está probado en
§9.16: `dir?: Direction` en types + `activeDir(() => dir, soma)` en el wrapper
+ `?? 'ltr'` en el provider.
- `?? 'ltr'` en el provider.
### 9.18 · Auditoría de la canonización — 2 defectos propios, con el criterio corregido
@ -1364,3 +1368,55 @@ afirmaba apoyarse en `[dir='rtl']`, prohibido desde §6.3.
📌 **El censo final**: 45 componentes aceptan `dir`; de ellos sólo `waveform` no
lo resuelve, y con razón. `media-player` lo arregló la otra sesión.
### 9.19 · El alias, y los cuatro que pintaban con `:dir()` sin dejar afirmarla
Commits `188450072` y `d490f53e9`. Cierra las dos deudas que quedaban abiertas
al final de §9.18.
**El alias.** Ocho componentes escribían `dir?: 'ltr' | 'rtl'` a mano en vez de
`Direction`: calendar, command, emoji-picker, field-langs (props Y el spec de
lengua del proveedor), month-grid, range-calendar y year-grid. Compila igual,
pero deja el contrato fuera del alias — ensanchar o renombrar `Direction` no
los tocaría, y la deriva no rompería en tipos, que es justo lo que el alias
está para garantizar. Cambio de tipo puro.
**Los cuatro de sólo-CSS.** `feed`, `nav-tree`, `sidebar` y `switch` tienen
reglas `:dir(rtl)` en su receta y no aceptaban `dir`. Funcionaban —`:dir()` lee
la dirección HEREDADA y la proyección de prefs pone `dir` en el `<html>`— pero
sólo para la página entera: no había forma de voltear UNO. Ahora corren la
cadena canónica y **estampan el crudo**, por la regla de §9.18: si el recipe usa
`:dir()`, el atributo es parte del contrato.
`switch` no pudo ir por `OptsFromProps`: ese helper quita el `undefined` del
prop público y da `Active<'ltr' | 'rtl'>`, cuando «nadie afirmó nada» es
exactamente el valor que el estampado crudo necesita distinguir. Va aparte, con
`ActiveProps<{ dir: Direction | undefined }>`, y en el envoltorio fuera del
`bindProps` (que toma getters, no `Active`s).
`sidebar` tenía además un resolutor a pelo en el motor flotante
(`soma.prefs?.getDir() ?? 'ltr'`, línea 662) que se saltaba el prop: ahora
consume `resolvedDir`. Ése sí necesita valor concreto —el motor espeja la
colocación del flyout—, a diferencia de la receta, que lee `:dir()`.
📌 **Censo final del eje** (ya cerrado):
- **56** componentes aceptan `dir`; **61** ficheros de envoltorio corren
`activeDir` (la diferencia son compuestos con varios proveedores).
- **0** componentes usan `:dir()` en su receta sin aceptar `dir`.
- **0** resolutores a pelo vivos — sólo quedan menciones a `getDir()` en
comentarios de documentación.
- Aceptan `dir` sin correr la cadena: `waveform` (delega en el Slider, §9.18) y
`field-langs`, que **no es del eje** — su `dir` es un campo de `LangSpec`, la
dirección de cada idioma del contenido, no una prop del componente.
**Verificación**: `check` sin errores nuevos (77 = la base con los ficheros de
`media-player`, de otra sesión). Tests de feed y switch en verde. En SSR los
cuatro estampan `dir="ltr"`; en Chrome el switch pasa a `dir="rtl"` al girar la
preferencia y la regla dispara (`--_switch-thumb-offset`: `16px` →
`calc(-1 * 16px)`). ⚠️ La transición del pulgar **no se pudo medir**: el panel
del navegador estaba oculto y ahí las transiciones CSS quedan congeladas —
`getComputedStyle` devuelve la matriz identidad y parece un defecto que no lo es.
**Queda fuera** (deuda ajena, §8.4): 91 ficheros de test que falsean
`Soma.require()`, cero tests de charts, `smoke` y `perm:check` sin ejecutar.

Loading…
Cancel
Save

Powered by TurnKey Linux.