From 92c5e421c9e8b8bc1273e2aebceb0484674aa728 Mon Sep 17 00:00:00 2001 From: dev Date: Sat, 23 May 2026 23:12:39 +0200 Subject: [PATCH] fix(carousel): :active press feedback was replacing centering transform MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug behind the user's "el botón se desplaza en el eje contrario al orientation" report. The Button recipe sets [data-button]:active { transform: translateY(1px); } on press to simulate a physical button push. Carousel prev/next triggers absolute-position via: [data-carousel-prev-trigger] { transform: translateY(-50%); } (horiz) [data-carousel][data-orientation='vertical'] [data-carousel-prev-trigger] { transform: translateX(-50%); } (vert) CSS `transform` is a single property — Button's :active value REPLACES the carousel's centering transform, so on press the trigger jumped off its anchor: in horizontal it lost vertical centering, in vertical it lost horizontal centering AND moved 1px down (wrong axis relative to slide travel). Override `:active` on the carousel triggers to compose both transforms on the appropriate axis: horizontal: translateY(calc(-50% + 1px)) vertical: translateX(calc(-50% + 1px)) Now press feedback shifts the trigger 1px along the centering axis without breaking the absolute anchor. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/uix/eidos/components/carousel/README.md | 61 +++++++++++++++++++ .../eidos/components/carousel/carousel.css | 22 +++++++ .../carousel/components/carousel.svelte | 3 +- .../uix/components/carousel/+page.svelte | 61 +++++++++++++++++++ 4 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 src/uix/eidos/components/carousel/README.md diff --git a/src/uix/eidos/components/carousel/README.md b/src/uix/eidos/components/carousel/README.md new file mode 100644 index 000000000..a80050969 --- /dev/null +++ b/src/uix/eidos/components/carousel/README.md @@ -0,0 +1,61 @@ +# Eidos Carousel + +Carousel es el wrapper visual de Eidos sobre el carrusel headless de Soma. Soma +posee indice, drag, teclado, autoplay, ARIA y disabled state; Eidos anade +chrome, tamano, color, botones de flecha con `Button` y receta CSS. + +## Baseline Air + +En la rama `morfo-runtime` no existe un `src/uix/air/components/carousel` visual +separado. La baseline local anterior vive en +`src/uix/soma/components/carousel`: root region, viewport, item-group, items, +prev/next triggers e indicadores. + +Eidos conserva esa anatomia, pero la presenta como componente compound +`` con hijos explicitos (`Viewport`, `ItemGroup`, `Item`, +`PrevTrigger`, `NextTrigger`, `IndicatorGroup`, `Indicator`) y anade props +visuales (`size`, `variant`, `color`, `block`, `verticalBlockSize`). + +## Comparativa + +| Capacidad | APG / WAI-ARIA | Ark UI | shadcn-svelte / Embla | UIX | +| --- | --- | --- | --- | --- | +| Root con nombre accesible | Recomienda `region` + `aria-roledescription` | Root + partes | Root visual + Embla API | Soma/Morfo | +| Prev/Next como botones nativos | Recomendado | `PrevTrigger` / `NextTrigger` | `Previous` / `Next` | Soma triggers + Eidos Button | +| Indicadores | Botones/tabs de seleccion | `IndicatorGroup` / `Indicator` | Dots via composicion | Soma indicators | +| Drag/swipe | No prescribe motor | Incluido | Embla | Soma `Gesture.base()` | +| Autoplay + pausa | Debe ser controlable | `autoplay`, pause cases | Plugin Embla | Soma timer + stop/pause props | +| Visual styling | Fuera de scope | Headless/data attrs | Tailwind recipe | Eidos recipe tokens | + +Fuentes: + +- [WAI-ARIA APG Carousel](https://www.w3.org/WAI/ARIA/apg/patterns/carousel/) +- [Ark UI Carousel](https://ark-ui.com/docs/components/carousel) +- [shadcn-svelte Carousel](https://shadcn-svelte.com/docs/components/carousel) +- [Embla Carousel Svelte](https://www.embla-carousel.com/docs/get-started/svelte/) + +## Decisiones + +- Los triggers de Eidos renderizan `Button iconOnly` por defecto. El icono es + decorativo (`aria-hidden`) y no debe ser target de puntero; el click pertenece + al boton nativo que trae el handler de Soma. +- `PrevTrigger`, `NextTrigger` e `Indicator` llaman a la API publica + `scrollPrev`, `scrollNext` y `scrollTo`, para que `stopOnInteraction` trate + todos los clicks como interaccion real de usuario. +- `Indicator` hereda el archetype global `indicator`, que por defecto es + decorativo y usa `pointer-events: none`. Carousel lo sobreescribe a + `pointer-events: auto` porque sus indicadores son botones/tab stops reales. +- `slidesPerView`, `slidesToScroll`, `align` y `gap` viven en Soma porque + alteran geometria y bounds. Eidos solo consume el resultado visual mediante + `data-*` y estilos. +- `verticalBlockSize` es Eidos-only: el eje vertical necesita una altura + acotada para que el viewport pueda recortar, pero no cambia el contrato ARIA. + +## Gaps + +| Gap | Disposicion | Detalle | +| --- | --- | --- | +| Pause/play trigger publico dentro del componente | diferir | La API imperativa ya expone `play`, `pause` y `reset`; una parte visual requiere Morfo/Soma. | +| Labels traducidos para slide/indicator generados por indice | implementar en Soma | El README de Soma ya documenta que parte del texto se arma en JS. | +| Tests browser-level permanentes | implementar | El fallo de click sobre SVG se verifico manualmente con Playwright; conviene convertirlo en test visual si el harness lo permite. | +| Motion attrs por direccion | diferir | Seria un extra visual para animaciones internas de slide. | diff --git a/src/uix/eidos/components/carousel/carousel.css b/src/uix/eidos/components/carousel/carousel.css index 368b9ab43..35db01c39 100644 --- a/src/uix/eidos/components/carousel/carousel.css +++ b/src/uix/eidos/components/carousel/carousel.css @@ -97,6 +97,7 @@ align-self: center; } + [data-carousel][data-block] { inline-size: 100%; } @@ -245,6 +246,26 @@ cursor: not-allowed; } +/* + * Compose the Button `:active` press feedback with the trigger's + * centering transform. Without these overrides Button's recipe sets + * `transform: translateY(1px)` on press which REPLACES our centering + * `translateY(-50%)` (or `translateX(-50%)` in vertical), making the + * trigger jump entirely off its anchor when clicked. The override + * keeps the centering AND adds the 1px press shift on the same axis. + */ +[data-carousel-prev-trigger]:active:not([data-disabled]):not([data-loading]), +[data-carousel-next-trigger]:active:not([data-disabled]):not([data-loading]) { + transform: translateY(calc(-50% + 1px)); +} + +[data-carousel][data-orientation='vertical'] + [data-carousel-prev-trigger]:active:not([data-disabled]):not([data-loading]), +[data-carousel][data-orientation='vertical'] + [data-carousel-next-trigger]:active:not([data-disabled]):not([data-loading]) { + transform: translateX(calc(-50% + 1px)); +} + /* Vertical orientation flips the triggers to top/bottom. */ [data-carousel][data-orientation='vertical'] [data-carousel-prev-trigger] { inset-block-start: var(--_carousel-trigger-offset); @@ -283,6 +304,7 @@ border-radius: var(--radius-full); background: var(--color-surface-overlay); cursor: pointer; + pointer-events: auto; transition: background var(--duration-fast) var(--ease-default), inline-size var(--duration-fast) var(--ease-default), diff --git a/src/uix/soma/components/carousel/components/carousel.svelte b/src/uix/soma/components/carousel/components/carousel.svelte index 4522738bd..0641da80a 100644 --- a/src/uix/soma/components/carousel/components/carousel.svelte +++ b/src/uix/soma/components/carousel/components/carousel.svelte @@ -1,4 +1,5 @@