From 3c3d8e1ac81512b2f45fe995d4b23c7a30ad64e5 Mon Sep 17 00:00:00 2001 From: dev Date: Mon, 3 Aug 2026 14:59:47 +0200 Subject: [PATCH] fix(motion): las barras horizontales crecian desde su punta en RTL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `grow-x` clavaba `transform-origin: 0% 50%` — el borde fisico izquierdo. En RTL la barra se dispone contra el borde DERECHO, asi que la entrada la hacia crecer desde su punta de vuelta a su base, despegandose de su ancla. Medido en los dos consumidores, con la direccion movida por el toggle del topbar: bar-segment borde izq. clavado en 296, el derecho avanza 108 -> 0 bar-list borde izq. clavado en 239, y el de ANCLAJE se aleja 65 -> 3 `transform-origin` no tiene forma logica en CSS, asi que el preset lee ahora `--motion-origin-inline-start`, que `render-css.ts` emite por `:dir()` — el mismo patron que `scale-fade` ya usa con `--floating-transform-origin`. La var HEREDA (al reves que el indice de stagger, que es `inherits: false`): es lo que permite que un `:dir()` de un ancestro alcance al nodo animado. Se declaran las DOS direcciones para que un subarbol que redeclare la suya tambien acierte — la misma razon por la que `[dir='rtl']` esta prohibido. Tras el arreglo el borde de anclaje queda CLAVADO en 0 y la punta avanza, en ambas direcciones. LTR intacto (`origin: 0px`, ancla en 0). Verificado a ojo congelando la animacion al 45%: las cinco barras del bar-list nacen pegadas a su ancla —derecha en RTL, izquierda en LTR— con el stagger escalonado. RTL-1 no podia verlo: es `transform-origin` en un preset de TypeScript, ni siquiera CSS estatico. `metrics-progress` NO usa `grow-x`; los afectados eran solo esos dos. Co-Authored-By: Claude Opus 5 --- src/uix/eidos/generated/base.css | 12 +++++++++-- src/uix/eidos/lib/motion/presets/css.ts | 28 +++++++++++++++++-------- src/uix/eidos/lib/render-css.ts | 13 ++++++++++++ 3 files changed, 42 insertions(+), 11 deletions(-) diff --git a/src/uix/eidos/generated/base.css b/src/uix/eidos/generated/base.css index 0f2150a66..cc82f4859 100644 --- a/src/uix/eidos/generated/base.css +++ b/src/uix/eidos/generated/base.css @@ -5712,6 +5712,14 @@ inherits: false; } +:dir(ltr) { + --motion-origin-inline-start: 0%; +} + +:dir(rtl) { + --motion-origin-inline-start: 100%; +} + @property --motion-stagger-index { syntax: ''; inherits: false; @@ -6902,14 +6910,14 @@ [data-animation-style='grow-x'][data-motion-state='open'] { animation: grow-right var(--motion-duration-enter, var(--duration-moderate)) var(--motion-ease-enter, var(--ease-out)) backwards; animation-delay: calc(var(--motion-stagger-index, 0) * var(--motion-stagger-each, 0ms)); - transform-origin: 0% 50%; + transform-origin: var(--motion-origin-inline-start, 0%) 50%; } [data-animation-style='grow-x'][data-state='closed'], [data-stagger][data-state='closed'] > [data-animation-style='grow-x'] { animation: shrink-left var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards; animation-delay: calc(var(--motion-stagger-index-rev, 0) * var(--motion-stagger-each, 0ms)); - transform-origin: 0% 50%; + transform-origin: var(--motion-origin-inline-start, 0%) 50%; } [data-motion='reduce'] [data-animation-style='grow-x'][data-state='open'], diff --git a/src/uix/eidos/lib/motion/presets/css.ts b/src/uix/eidos/lib/motion/presets/css.ts index 591ef0afa..cb0da6906 100644 --- a/src/uix/eidos/lib/motion/presets/css.ts +++ b/src/uix/eidos/lib/motion/presets/css.ts @@ -50,10 +50,12 @@ export const BUILTIN_KEYFRAMES: Readonly> = { to: { 'stroke-dasharray': '1', 'stroke-dashoffset': '1' } }, - // Grow from the left edge (horizontal bars · progress fills). Horizontal-only - // `scale` (`0 1` → `1 1`); pair with `transform-origin: 0% 50%` (the `grow-x` - // preset sets it). HTML default `transform-box: border-box` already scopes the - // origin to the element — no `fill-box` needed for a `
`. + // Grow from the inline-start edge (horizontal bars · progress fills). + // Horizontal-only `scale` (`0 1` → `1 1`); pair with a `transform-origin` on + // that edge (the `grow-x` preset sets it, direction-aware). HTML default + // `transform-box: border-box` already scopes the origin to the element — no + // `fill-box` needed for a `
`. The names are the LTR reading of the + // effect; in RTL the same keyframes grow towards the left. 'grow-right': { from: { scale: '0 1' }, to: { scale: '1 1' } }, 'shrink-left': { from: { scale: '1 1' }, to: { scale: '0 1' } }, @@ -533,18 +535,26 @@ export const BUILTIN_CSS_PRESETS: Readonly> = { reduce: 'instant' }, - // Grow from the left edge — horizontal bars (bar-list / bar-segment), progress - // fills. Scales on `scale` (X only) with the origin at the left; under reduce it - // snaps to full width. + // Grow from the inline-start edge — horizontal bars (bar-list / bar-segment), + // progress fills. Scales on `scale` (X only) with the origin on the edge the + // bar is anchored to; under reduce it snaps to full width. The origin comes + // from `--motion-origin-inline-start` (rendered per `:dir()`, render-css.ts) + // because `transform-origin` has no logical form: hard-coding `0%` made every + // RTL bar grow from its tip back to its base. 'grow-x': { driver: 'css', enter: { keyframes: 'grow-right', duration: 'moderate', ease: 'out', - transformOrigin: '0% 50%' + transformOrigin: 'var(--motion-origin-inline-start, 0%) 50%' + }, + exit: { + keyframes: 'shrink-left', + duration: 'fast', + ease: 'in', + transformOrigin: 'var(--motion-origin-inline-start, 0%) 50%' }, - exit: { keyframes: 'shrink-left', duration: 'fast', ease: 'in', transformOrigin: '0% 50%' }, reduce: 'instant' }, diff --git a/src/uix/eidos/lib/render-css.ts b/src/uix/eidos/lib/render-css.ts index 801b7391d..70a1c4b20 100644 --- a/src/uix/eidos/lib/render-css.ts +++ b/src/uix/eidos/lib/render-css.ts @@ -896,6 +896,19 @@ function renderMotionBlocks(motion: MotionConfig): string { blocks.push(`@property --motion-duration-${phase} {\n\tsyntax: '*';\n\tinherits: false;\n}`) blocks.push(`@property --motion-ease-${phase} {\n\tsyntax: '*';\n\tinherits: false;\n}`) } + // `transform-origin` has NO logical form in CSS, so a preset that anchors to + // the inline-start edge (`grow-x` — horizontal bars, progress fills) grows + // from the physical left in both directions: in RTL the bar is laid out + // against the right edge, so it would grow from its TIP back to its base. + // This var carries the inline-start edge as a percentage; presets read it + // instead of hard-coding `0%`. It INHERITS (unlike the stagger index) — + // that is how a `:dir()` match on an ancestor reaches the animated node. + // Both directions are declared so a subtree that re-declares its own + // direction gets the right value, which is exactly why `[dir='rtl']` is + // forbidden catalogue-wide. + blocks.push(':dir(ltr) {\n\t--motion-origin-inline-start: 0%;\n}') + blocks.push(':dir(rtl) {\n\t--motion-origin-inline-start: 100%;\n}') + // Stagger index is per-item — `inherits: false` stops it leaking into nested // groups; the rhythm (`--motion-stagger-each`) inherits from the container. blocks.push(