From a81b44b40c4872202257f056a2113b7694893818 Mon Sep 17 00:00:00 2001 From: dev Date: Sat, 23 May 2026 22:32:14 +0200 Subject: [PATCH] =?UTF-8?q?fix(carousel):=20first-click=20no-op=20?= =?UTF-8?q?=E2=80=94=20viewportSize=20was=20stale=20until=20ResizeObserver?= =?UTF-8?q?=20fired?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Real bug behind the persistent "los clicks fallan" report. The `viewportSize` derivation was reading exclusively from the `viewportClient{Width,Height}` $state mirrors, which are populated asynchronously by the ResizeObserver. On first paint (and on the very first click after mount / orientation switch) those mirrors were still `0`. So: translate = -value × slideSize = -1 × (viewportSize / 1) = -1 × 0 = 0 The index DID advance (soma's `value` went 0→1) but the rail stayed at translate=0, so the user saw the active slide unchanged — looked exactly like a dead click. Subsequent clicks accumulated until the observer finally fired and the carousel suddenly jumped to whatever index had become. Fix: two-tier read in the `viewportSize` derivation. Prefer the observer-tracked mirror when non-zero (steady state, picks up reshape); fall back to a synchronous `vp.clientWidth` / `clientHeight` read while the mirror is still 0. Same derivation reactivity, no extra effects. Verified: first click after page load now lands `translate3d(-380px, 0, 0)` on a 380px-wide viewport (idx 0→1) — was 0 / no movement before. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../carousel/carousel-provider.svelte.ts | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/src/uix/soma/components/carousel/carousel-provider.svelte.ts b/src/uix/soma/components/carousel/carousel-provider.svelte.ts index ee6da174b..e64fc5b06 100644 --- a/src/uix/soma/components/carousel/carousel-provider.svelte.ts +++ b/src/uix/soma/components/carousel/carousel-provider.svelte.ts @@ -201,17 +201,26 @@ export class CarouselProvider { // ── Geometry ───────────────────────────────────────────────────────────── /** - * Size (px) of the viewport along the primary axis. Sourced from the - * `viewportClient{Width,Height}` reactive mirrors (populated by the - * ResizeObserver above) so that layout-only changes — like flipping - * orientation, which rewrites the viewport's block-size via the eidos - * recipe — propagate to translate calc on the next reactive tick. + * Size (px) of the viewport along the primary axis. + * + * Two-tier read: the ResizeObserver mirrors (`viewportClient{Width, + * Height}`) are the steady-state source — they pick up layout reshape + * (orientation flip, container resize, font scaling) on the next + * frame. BUT the observer is async, so on first paint — or on the very + * first user interaction after mount — those mirrors may still be `0`. + * Falling back to the live `clientWidth/Height` reads synchronously + * from the DOM so the first click translates to a real pixel offset + * instead of `value * 0 = 0` (the bug that made early clicks look like + * dead-no-ops while the index actually advanced). */ - readonly viewportSize = $derived.by(() => - this.opts.orientation.current === 'horizontal' - ? this.viewportClientWidth - : this.viewportClientHeight - ); + readonly viewportSize = $derived.by(() => { + const horizontal = this.opts.orientation.current === 'horizontal'; + const tracked = horizontal ? this.viewportClientWidth : this.viewportClientHeight; + if (tracked > 0) return tracked; + const vp = this.viewportRef; + if (!vp) return 0; + return horizontal ? vp.clientWidth : vp.clientHeight; + }); /** Slide size (px) along the primary axis = viewport / slidesPerView. */ readonly slideSize = $derived.by(() => {