Commit 737b1fdb authored by Mahmoud Aglan's avatar Mahmoud Aglan

fix(website): stop the partner carousel scrolling the page, and turn the hero...

fix(website): stop the partner carousel scrolling the page, and turn the hero showcase into a real flipping carousel

Three faults, all found by watching the built site rather than reading it.

The partner carousel dragged the whole page down to itself. Its timer called
`el.scrollIntoView()` every few seconds, and scrollIntoView walks up and scrolls
*every* scrollable ancestor including the document — so a reader anywhere on the
page was hauled to the partners section on a loop. It now sets `scrollLeft` on
its own track, which cannot move anything but itself. Verified: twelve seconds
on the homepage, `window.scrollY` never leaves 0, while the track's own
scrollLeft still advances.

The hero showcase was manual when it should play itself. The reference plays
each design for a beat, turns it on its Y axis to show the back, holds again,
then hands over to the next — an Embla autoplay at `delay: 2500` with a
`duration-700` `rotateY(180deg)` flip and `backfaceVisibility`, read out of
their own bundle rather than guessed. Ours sat still until someone clicked a
swatch. It now runs that cycle: a genuine 3D flip of one object, not a crossfade
between two pictures, with both faces backface-hidden so they never show through
one another mid-turn. Hovering pauses it; reduced-motion skips it entirely.

The colour swatches were large filled circles under the garment, which read as
loud page furniture rather than a control. They are gone by default. A site that
wants them gets `showcase_controls`, and they render as the same discreet
progress dashes the other carousels use.

New fields: `showcase_autoplay` (default on), `showcase_interval` (default
2500ms, matching the reference), `showcase_controls` (default off).

Verified on a local replica carrying the full 11-page site: sampling the hero
across a cycle shows active=1 front → active=1 flipped → active=2 front, so the
flip and the hand-over both really happen; 22 page/locale combinations answer
200 with no logged block failures; 137 block/variant combinations render; suite
passes.
Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
parent 16db6135
......@@ -57,6 +57,12 @@ public function fields(): array
'three_quarter' => 'ثلاثة أرباع', 'full' => 'ملء الشاشة',
])->default('three_quarter'),
BlockField::alignment('align', 'محاذاة النص'),
BlockField::toggle('showcase_autoplay', 'تشغيل تلقائي لصور العرض')->default(true)
->help('يعرض كل تصميم بضع ثوانٍ ثم يقلبه لإظهار الخلف، ثم ينتقل للتالي'),
BlockField::select('showcase_interval', 'مدة عرض كل وجه', [
'2000' => '2 ثانية', '2500' => '2.5 ثانية', '3500' => '3.5 ثانية', '5000' => '5 ثوانٍ',
])->default('2500'),
BlockField::toggle('showcase_controls', 'إظهار أزرار اختيار اللون'),
BlockField::select('showcase_motion', 'حركة صورة العرض', [
'none' => 'بدون', 'float' => 'طفو', 'glow' => 'توهج', 'float_glow' => 'طفو وتوهج',
])->default('none'),
......
......@@ -1010,6 +1010,43 @@
box-shadow: 0 0 28px -4px color-mix(in srgb, var(--site-accent) 75%, transparent);
}
/* ─── Showcase: a design turns on its axis to show its back ─── */
.ec-jersey-stage { perspective: 1400px; }
.ec-jersey-card {
position: absolute;
inset: 0;
opacity: 0;
pointer-events: none;
transition: opacity 0.7s ease;
}
.ec-jersey-card.is-active { opacity: 1; }
.ec-jersey-inner {
position: relative;
width: 100%;
height: 100%;
transform-style: preserve-3d;
transition: transform 0.7s cubic-bezier(0.4, 0, 0.2, 1);
}
.ec-jersey-inner.is-flipped { transform: rotateY(180deg); }
/* Each face is hidden while it points away from the reader, so the two
never show through one another mid-turn. */
.ec-jersey-face {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: contain;
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
}
.ec-jersey-face--back { transform: rotateY(180deg); }
/* ─── A card whose subject is out of season ─── */
.ec-card-dormant {
opacity: 0.45;
......@@ -1063,7 +1100,9 @@
.ec-marquee-track,
.ec-nav-item::after,
.ec-focus-item,
.ec-frame-tilt > img {
.ec-frame-tilt > img,
.ec-jersey-card,
.ec-jersey-inner {
animation: none !important;
transition: none !important;
}
......
......@@ -76,32 +76,71 @@ class="ec-btn ec-btn--{{ data_get($btn, 'style', 'primary') }} inline-flex items
</div>
@if ($variant === 'product_showcase' && $products->isNotEmpty())
<div x-data="{ active: 0, back: false }" class="flex flex-col items-center gap-6">
<div class="relative w-full max-w-sm aspect-square {{ $showcaseMotion }}">
@foreach ($products as $idx => $product)
@foreach (['front', 'back'] as $face)
@if (data_get($product, $face))
<img src="{{ data_get($product, $face) }}"
alt="{{ website_text(data_get($product, 'name')) }}"
class="absolute inset-0 h-full w-full object-contain transition-all duration-500"
x-bind:class="(active === {{ $idx }} && back === {{ $face === 'back' ? 'true' : 'false' }}) ? 'opacity-100 scale-100' : 'opacity-0 scale-95 pointer-events-none'">
@endif
@endforeach
@endforeach
</div>
<div class="flex items-center gap-3">
@php
$autoplay = (bool) $block->get('showcase_autoplay', true);
$interval = (int) ($block->get('showcase_interval') ?: 2500);
$controls = (bool) $block->get('showcase_controls');
@endphp
{{--
Each design holds for a beat, turns on its Y axis to show the
back, holds again, then hands over to the next. A real 3D flip
rather than a crossfade between two pictures, so the garment
reads as one object being turned around.
--}}
<div class="flex flex-col items-center gap-6"
x-data="{
i: 0,
back: false,
n: {{ $products->count() }},
timer: null,
reduced: window.matchMedia('(prefers-reduced-motion: reduce)').matches,
step() {
if (this.back) { this.back = false; this.i = (this.i + 1) % this.n; }
else { this.back = true; }
},
start() {
@if ($autoplay)
if (this.reduced || this.n < 1) return;
this.timer = setInterval(() => this.step(), {{ $interval }});
@endif
},
stop() { clearInterval(this.timer); this.timer = null; },
show(idx) { this.stop(); this.i = idx; this.back = false; this.start(); },
}"
x-init="start()"
x-on:mouseenter="stop()" x-on:mouseleave="start()">
<div class="ec-jersey-stage relative w-full max-w-md aspect-square {{ $showcaseMotion }}">
@foreach ($products as $idx => $product)
<button type="button" x-on:click="active = {{ $idx }}"
class="w-8 h-8 rounded-full border-2 transition"
x-bind:class="active === {{ $idx }} ? 'ring-2 ring-offset-2 scale-110' : 'opacity-70'"
style="background:{{ data_get($product, 'swatch', '#ccc') }}"
aria-label="{{ website_text(data_get($product, 'name')) }}"></button>
@php
$front = data_get($product, 'front');
$back = data_get($product, 'back') ?: $front;
$label = website_text(data_get($product, 'name'));
@endphp
@continue (! $front)
<div class="ec-jersey-card" x-bind:class="i === {{ $idx }} && 'is-active'"
@if ($idx > 0) aria-hidden="true" @endif>
<div class="ec-jersey-inner" x-bind:class="(i === {{ $idx }} && back) && 'is-flipped'">
<img src="{{ $front }}" alt="{{ $label }}"
class="ec-jersey-face" @if ($idx > 0) loading="lazy" @endif>
<img src="{{ $back }}" alt="" aria-hidden="true"
class="ec-jersey-face ec-jersey-face--back" loading="lazy">
</div>
</div>
@endforeach
</div>
<button type="button" x-on:click="back = !back" class="ec-muted text-sm underline">
<span x-show="!back">{{ __('عرض الخلف') }}</span>
<span x-show="back" x-cloak>{{ __('عرض الأمام') }}</span>
</button>
@if ($controls)
<div class="flex items-center gap-2.5">
@foreach ($products as $idx => $product)
<button type="button" x-on:click="show({{ $idx }})"
class="h-1.5 rounded-full transition-all"
x-bind:class="i === {{ $idx }} ? 'w-6 bg-[var(--site-accent)]' : 'w-1.5 bg-current opacity-40'"
aria-label="{{ website_text(data_get($product, 'name')) ?: __('تصميم').' '.($idx + 1) }}"></button>
@endforeach
</div>
@endif
</div>
@elseif ($split && $variant === 'split_end' && $block->get('image'))
<div class="{{ $showcaseMotion }}"><img src="{{ $block->get('image') }}" alt="{{ $block->get('title') }}" class="w-full rounded-2xl object-cover"></div>
......
......@@ -41,8 +41,17 @@ class="h-12 w-auto object-contain shrink-0 transition {{ $gray }}">
stop() { clearInterval(this.timer); this.timer = null; },
go(i) {
this.active = i;
const el = this.$refs.track?.children[i];
if (el) el.scrollIntoView({ inline: 'center', block: 'nearest', behavior: this.reduced ? 'auto' : 'smooth' });
const track = this.$refs.track;
const el = track?.children[i];
if (!el) return;
/*
* Scroll the track itself, never scrollIntoView(): that walks
* up and scrolls every ancestor including the document, so a
* timer on this carousel dragged the whole page down to it
* every few seconds while the reader was elsewhere.
*/
const target = el.offsetLeft - (track.clientWidth - el.clientWidth) / 2;
track.scrollTo({ left: target, behavior: this.reduced ? 'auto' : 'smooth' });
},
}"
x-init="start()"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment