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
Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.
Binary file not shown.
@@ -1,6 +1,8 @@
import { Header } from "@/components/layout/header"; import { Header } from "@/components/layout/header";
import { MobileNav } from "@/components/layout/mobile-nav"; import { MobileNav } from "@/components/layout/mobile-nav";
import { DemosGallery } from "@/components/products/demos-gallery"; import { DemosGallery } from "@/components/products/demos-gallery";
import { ShowcaseVideo } from "@/components/showcase/showcase-video";
import { BrowserFrame } from "@/components/showcase/browser-frame";
import { Card } from "@repo/ui/components/card"; import { Card } from "@repo/ui/components/card";
import { Button } from "@repo/ui/components/button"; import { Button } from "@repo/ui/components/button";
import { JsonLd, breadcrumbLd, faqLd } from "@/components/seo/json-ld"; import { JsonLd, breadcrumbLd, faqLd } from "@/components/seo/json-ld";
@@ -110,6 +112,22 @@ export default function WebshopsPage() {
maintain it on revenue share, so we only win when you make sales. maintain it on revenue share, so we only win when you make sales.
</p> </p>
{/* Theming morph: the same platform themed as five real demo brands. */}
<figure className="mb-16">
<BrowserFrame url="your-brand.com">
<ShowcaseVideo
webm="/showcase/theming-morph.webm"
mp4="/showcase/theming-morph.mp4"
poster="/showcase/theming-morph-poster.jpg"
label="The same commerce platform themed as five distinct brands, dissolving one into the next: an artisan shop, a merch store, a crypto shop, a yoga studio, and a coaching site."
className="block aspect-video w-full"
/>
</BrowserFrame>
<figcaption className="mt-3 text-center text-sm text-muted-foreground">
One platform, five real storefronts. Every shop here runs the same engine.
</figcaption>
</figure>
{/* Theming spotlight, the headline story */} {/* Theming spotlight, the headline story */}
<section aria-labelledby="theming-heading" className="section-container border-l-4 border-emerald-500 mb-16"> <section aria-labelledby="theming-heading" className="section-container border-l-4 border-emerald-500 mb-16">
<h2 id="theming-heading" className="text-2xl md:text-3xl font-bold mb-4"> <h2 id="theming-heading" className="text-2xl md:text-3xl font-bold mb-4">
@@ -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>
);
}