feat(site): theming-morph showcase hero on /products/webshops

The 'render once, serve cheap pixels' showcase, live. A 7s reel of the same
platform themed as five real demo brands (artisan -> merch -> crypto -> yoga
-> coaching), inside a decorative browser frame, at the top of the webshops
page.

- ShowcaseVideo: poster-first, preload='none' (no download until played),
  plays only while in the viewport (IntersectionObserver), pauses offscreen,
  and prefers-reduced-motion -> poster only (video never loads). muted +
  playsInline for mobile autoplay-on-view.
- BrowserFrame: static decorative chrome so the reel reads as a real shop.
- 16:9 assets ~330-355KB (webm/mp4) + 50KB poster, all within the motion budget.
This commit is contained in:
jwinterm
2026-07-06 10:39:40 -04:00
parent 30f7e4f89f
commit 99ce876cdd
6 changed files with 125 additions and 0 deletions
@@ -0,0 +1,37 @@
import type { ReactNode } from 'react';
/**
* A lightweight, purely-decorative browser chrome (title bar + traffic lights +
* a URL pill) to wrap a storefront screenshot or the theming-morph clip, so it
* reads as "a real shop in a real browser." Static CSS, no JS.
*/
export function BrowserFrame({
url = 'your-brand.com',
children,
className,
}: {
url?: string;
children: ReactNode;
className?: string;
}) {
return (
<div
className={`overflow-hidden rounded-xl border border-white/10 bg-slate-900/60 shadow-2xl ${className ?? ''}`}
>
<div
className="flex items-center gap-2 border-b border-white/10 bg-slate-800/70 px-4 py-2.5"
aria-hidden="true"
>
<span className="flex gap-1.5">
<span className="h-3 w-3 rounded-full bg-red-400/70" />
<span className="h-3 w-3 rounded-full bg-amber-400/70" />
<span className="h-3 w-3 rounded-full bg-emerald-400/70" />
</span>
<span className="ml-2 flex-1 truncate rounded-md bg-slate-950/50 px-3 py-1 text-center text-xs text-slate-400">
{url}
</span>
</div>
{children}
</div>
);
}
@@ -0,0 +1,70 @@
'use client';
import { useEffect, useRef, useState } from 'react';
type ShowcaseVideoProps = {
webm: string;
mp4: string;
poster: string;
/** Accessible description of what the clip shows. */
label: string;
className?: string;
};
/**
* Perf-safe showcase video. The studio's motion budget, enforced in one place:
* - The poster paints instantly; the video is `preload="none"`, so it
* downloads NOTHING until it is actually played.
* - It only plays while in the viewport (IntersectionObserver) and pauses
* when scrolled away or the tab is hidden. No offscreen decoders.
* - `prefers-reduced-motion`: the video is never played, so with
* `preload="none"` it is never even downloaded. The poster is the whole
* experience for those visitors.
* - `muted` + `playsInline` so mobile autoplay-on-view is permitted (one
* video at a time; this component is for a single hero/section clip, not a
* grid — a grid would gate to one visible at a time).
*/
export function ShowcaseVideo({ webm, mp4, poster, label, className }: ShowcaseVideoProps) {
const ref = useRef<HTMLVideoElement>(null);
const [allowMotion, setAllowMotion] = useState(true);
useEffect(() => {
setAllowMotion(!window.matchMedia('(prefers-reduced-motion: reduce)').matches);
}, []);
useEffect(() => {
const v = ref.current;
if (!v || !allowMotion || typeof IntersectionObserver === 'undefined') return;
const io = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
// .play() on a preload="none" element triggers the deferred load,
// then plays once buffered. Ignore the rejection browsers throw when
// a play() is interrupted by a quick pause() on fast scroll.
v.play().catch(() => {});
} else {
v.pause();
}
},
{ threshold: 0.25 },
);
io.observe(v);
return () => io.disconnect();
}, [allowMotion]);
return (
<video
ref={ref}
className={className}
poster={poster}
muted
loop
playsInline
preload="none"
aria-label={label}
>
<source src={webm} type="video/webm" />
<source src={mp4} type="video/mp4" />
</video>
);
}