feat(plugin page): 3-act narrative layout + final screenshots
Redesign 'See it in action' as a 3-act story with clearer flow: - Act 1: Get set up (3 cards: panel, pair, launch option) - Act 2: Capture performance (2 cards: recording, session-form) - Act 3: Share & compare (1 large featured card: entry-live) Gradient line connectors between acts imply flow; numbered badges moved from per-card to per-act so cards feel like supporting visuals. Add root smooth scrolling (scroll-smooth on <html>) for hash-link anchors. Add recording + session-form screenshots (in-game captures from Steam pause overlay), each rendered with the device-photo treatment (rounded + drop shadow) to match the other in-game-style shots. Flip all HAS_SCREENSHOT flags to true — the full gallery is now live.
This commit is contained in:
@@ -90,7 +90,7 @@ export default function RootLayout({
|
|||||||
return (
|
return (
|
||||||
<html
|
<html
|
||||||
lang='en'
|
lang='en'
|
||||||
className={`${font.variable} bg-background text-text antialiased overscroll-none`}
|
className={`${font.variable} bg-background text-text antialiased overscroll-none scroll-smooth`}
|
||||||
>
|
>
|
||||||
<head>
|
<head>
|
||||||
{/* DNS prefetch + preconnect for external image CDNs */}
|
{/* DNS prefetch + preconnect for external image CDNs */}
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ const SCREENSHOTS: Shot[] = [
|
|||||||
title: "Record while you play",
|
title: "Record while you play",
|
||||||
caption:
|
caption:
|
||||||
"Once in-game, open the panel and hit Start Recording. A live timer tracks your session. Stop when you're done benchmarking.",
|
"Once in-game, open the panel and hit Start Recording. A live timer tracks your session. Stop when you're done benchmarking.",
|
||||||
file: "recording.png",
|
file: "recording.jpg",
|
||||||
photo: true,
|
photo: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -69,7 +69,7 @@ const SCREENSHOTS: Shot[] = [
|
|||||||
title: "Review & submit",
|
title: "Review & submit",
|
||||||
caption:
|
caption:
|
||||||
"After stopping, review the captured FPS, 1% lows, and power draw. Add notes, then upload straight to DeckyVault — or export to a file.",
|
"After stopping, review the captured FPS, 1% lows, and power draw. Add notes, then upload straight to DeckyVault — or export to a file.",
|
||||||
file: "session-form.png",
|
file: "session-form.jpg",
|
||||||
photo: true,
|
photo: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -87,8 +87,8 @@ const HAS_SCREENSHOT: Record<string, boolean> = {
|
|||||||
"panel-overview": true,
|
"panel-overview": true,
|
||||||
"pair-qr": true,
|
"pair-qr": true,
|
||||||
"launch-option": true,
|
"launch-option": true,
|
||||||
recording: false,
|
recording: true,
|
||||||
"session-form": false,
|
"session-form": true,
|
||||||
"entry-live": true,
|
"entry-live": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,6 +184,40 @@ const STEPS = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// ── Screenshot acts ────────────────────────────────────
|
||||||
|
// The "See it in action" gallery is structured as a 3-act narrative so the
|
||||||
|
// story flows: Set up → Capture → Share. Each act has a heading + a list of
|
||||||
|
// shots. The last act's shot is rendered larger as a visual payoff.
|
||||||
|
const ACTS: {
|
||||||
|
number: string
|
||||||
|
title: string
|
||||||
|
subtitle: string
|
||||||
|
shotIds: string[]
|
||||||
|
layout: "grid-3" | "grid-2" | "feature"
|
||||||
|
}[] = [
|
||||||
|
{
|
||||||
|
number: "1",
|
||||||
|
title: "Get set up",
|
||||||
|
subtitle: "Three quick steps before your first benchmark.",
|
||||||
|
shotIds: ["panel-overview", "pair-qr", "launch-option"],
|
||||||
|
layout: "grid-3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
number: "2",
|
||||||
|
title: "Capture performance",
|
||||||
|
subtitle: "Hit record, play, then review your stats.",
|
||||||
|
shotIds: ["recording", "session-form"],
|
||||||
|
layout: "grid-2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
number: "3",
|
||||||
|
title: "Share & compare",
|
||||||
|
subtitle: "Your entry appears on the game's page for the community.",
|
||||||
|
shotIds: ["entry-live"],
|
||||||
|
layout: "feature",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
export function PluginPageClient() {
|
export function PluginPageClient() {
|
||||||
const [copied, setCopied] = useState(false)
|
const [copied, setCopied] = useState(false)
|
||||||
|
|
||||||
@@ -365,27 +399,74 @@ export function PluginPageClient() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* ── Screenshots gallery ──────────────────────────────── */}
|
{/* ── See it in action: 3-act narrative ──────────────────────── */}
|
||||||
<section className="px-4 md:px-[10svw] py-12 md:py-16 border-t border-border">
|
<section className="px-4 md:px-[10svw] py-12 md:py-20 border-t border-border">
|
||||||
<div className="max-w-6xl mx-auto">
|
<div className="max-w-6xl mx-auto">
|
||||||
<h2 className="text-2xl md:text-3xl font-bold text-center mb-2">
|
<div className="text-center mb-12 md:mb-16">
|
||||||
|
<h2 className="text-2xl md:text-3xl font-bold mb-2">
|
||||||
See it in action
|
See it in action
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-text/50 text-center mb-12 max-w-xl mx-auto">
|
<p className="text-text/50 max-w-xl mx-auto">
|
||||||
A walkthrough of the plugin, from pairing to publishing.
|
A walkthrough of the plugin, from pairing to publishing.
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="flex flex-col gap-10 md:gap-14">
|
||||||
{SCREENSHOTS.map((shot, i) => (
|
{ACTS.map((act, actIdx) => {
|
||||||
|
const shots = act.shotIds
|
||||||
|
.map((id) => SCREENSHOTS.find((s) => s.id === id))
|
||||||
|
.filter((s): s is (typeof SCREENSHOTS)[number] => Boolean(s))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={act.number} className="flex flex-col">
|
||||||
|
{/* Act heading */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, x: -12 }}
|
||||||
|
whileInView={{ opacity: 1, x: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ delay: actIdx * 0.05 }}
|
||||||
|
className="flex items-center gap-3 mb-5"
|
||||||
|
>
|
||||||
|
<div className="w-10 h-10 rounded-full bg-primary text-white flex items-center justify-center font-bold text-sm shrink-0">
|
||||||
|
{act.number}
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<h3 className="text-lg md:text-xl font-semibold leading-tight">
|
||||||
|
{act.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-text/50">
|
||||||
|
{act.subtitle}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Shot grid for this act */}
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
act.layout === "grid-3"
|
||||||
|
? "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"
|
||||||
|
: act.layout === "grid-2"
|
||||||
|
? "grid grid-cols-1 sm:grid-cols-2 gap-4"
|
||||||
|
: "grid grid-cols-1 gap-4"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{shots.map((shot) => {
|
||||||
|
const isFeature = act.layout === "feature"
|
||||||
|
return (
|
||||||
<motion.figure
|
<motion.figure
|
||||||
key={shot.id}
|
key={shot.id}
|
||||||
initial={{ opacity: 0, y: 16 }}
|
initial={{ opacity: 0, y: 16 }}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
transition={{ delay: i * 0.05 }}
|
className="rounded-xl border border-border bg-text/2 overflow-hidden flex flex-col hover:border-primary/30 transition-colors"
|
||||||
className="rounded-xl border border-border bg-text/2 overflow-hidden flex flex-col"
|
>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
isFeature
|
||||||
|
? "relative aspect-[16/9] bg-gradient-to-br from-text/5 to-text/10 flex items-center justify-center p-4"
|
||||||
|
: "relative aspect-[16/10] bg-gradient-to-br from-text/5 to-text/10 flex items-center justify-center p-3"
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div className="relative aspect-[16/10] bg-gradient-to-br from-text/5 to-text/10 flex items-center justify-center p-3">
|
|
||||||
{HAS_SCREENSHOT[shot.id] ? (
|
{HAS_SCREENSHOT[shot.id] ? (
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
<img
|
<img
|
||||||
@@ -393,33 +474,47 @@ export function PluginPageClient() {
|
|||||||
alt={shot.title}
|
alt={shot.title}
|
||||||
className={shot.photo
|
className={shot.photo
|
||||||
? "max-w-full max-h-full object-contain rounded-lg shadow-2xl shadow-black/40 ring-1 ring-black/20"
|
? "max-w-full max-h-full object-contain rounded-lg shadow-2xl shadow-black/40 ring-1 ring-black/20"
|
||||||
|
: isFeature
|
||||||
|
? "w-full h-full object-contain rounded-md"
|
||||||
: "w-full h-full object-contain"}
|
: "w-full h-full object-contain"}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center gap-2 text-text/30 p-6 text-center">
|
<div className="flex flex-col items-center gap-2 text-text/30 p-6 text-center">
|
||||||
<Gamepad2Icon className="h-10 w-10" />
|
<Gamepad2Icon className="h-8 w-8 opacity-50" />
|
||||||
<span className="text-xs font-mono">
|
|
||||||
/plugin/{shot.file}
|
|
||||||
</span>
|
|
||||||
<span className="text-xs">
|
<span className="text-xs">
|
||||||
{shot.photo ? "device photo" : "screenshot"} coming soon
|
{shot.photo ? "device photo" : "screenshot"} not yet captured
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<figcaption className="p-4 flex flex-col gap-1">
|
<figcaption className="p-4 flex flex-col gap-1">
|
||||||
<div className="text-sm font-semibold flex items-center gap-2">
|
<div className="text-sm font-semibold">
|
||||||
<span className="w-5 h-5 rounded-full bg-primary/15 text-primary text-[10px] font-bold flex items-center justify-center">
|
|
||||||
{i + 1}
|
|
||||||
</span>
|
|
||||||
{shot.title}
|
{shot.title}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-text/55 leading-relaxed pl-7">
|
<p className="text-xs text-text/55 leading-relaxed">
|
||||||
{shot.caption}
|
{shot.caption}
|
||||||
</p>
|
</p>
|
||||||
</figcaption>
|
</figcaption>
|
||||||
</motion.figure>
|
</motion.figure>
|
||||||
))}
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Flow arrow between acts */}
|
||||||
|
{actIdx < ACTS.length - 1 && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
whileInView={{ opacity: 1 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ delay: actIdx * 0.05 + 0.1 }}
|
||||||
|
className="flex justify-center mt-8 md:mt-10"
|
||||||
|
>
|
||||||
|
<div className="w-px h-8 md:h-10 bg-gradient-to-b from-primary/40 to-primary/10" />
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 59 KiB |
Reference in New Issue
Block a user