feat(website): refine responsive product mockups

Keep desktop and mobile scenes visually consistent while improving phone depth, diff realism, alignment, and interaction polish.
This commit is contained in:
Mohamed Boudra
2026-08-30 23:48:54 +02:00
parent 375e9b3e53
commit fa53ba8f93
6 changed files with 161 additions and 75 deletions
@@ -1238,7 +1238,7 @@ function PhoneShowcase() {
role="img"
aria-label="Paseo workspace drawer"
>
<PhoneFrame time="18:54">
<PhoneFrame time="18:54" depth="right">
<MobileSidebar />
</PhoneFrame>
</motion.div>
@@ -1264,7 +1264,7 @@ function PhoneShowcase() {
role="img"
aria-label="Paseo diff view"
>
<PhoneFrame time="18:55">
<PhoneFrame time="18:55" depth="left">
<MobileDiff />
</PhoneFrame>
</motion.div>
@@ -128,9 +128,11 @@ function Composer() {
<span className="block text-[16px] text-mock-fg-xmuted">Message, @files, /commands</span>
<div className="flex items-center gap-[13px] pt-[26px]">
<Plus size={20} className="shrink-0 text-mock-fg-muted" strokeWidth={1.9} />
<CodexIcon size={19} className="shrink-0 text-mock-fg" />
<span className="shrink-0 whitespace-nowrap text-[16px] text-mock-fg">GPT-5.6-Sol</span>
<span className="shrink-0 whitespace-nowrap text-[16px] text-mock-fg-muted">Medium</span>
<span className="flex shrink-0 items-center gap-[5px]">
<CodexIcon size={19} className="shrink-0 text-mock-fg" />
<span className="whitespace-nowrap text-[16px] text-mock-fg">GPT-5.6-Sol</span>
<span className="whitespace-nowrap text-[16px] text-mock-fg-muted">Medium</span>
</span>
<span className="flex-1" />
<LoaderCircle size={19} className="shrink-0 text-mock-fg-muted" strokeWidth={1.8} />
<Mic size={19} className="shrink-0 text-mock-fg-muted" strokeWidth={1.8} />
@@ -4,57 +4,89 @@
// an addition, so the whole gutter runs green). Reuses the shared `Code`
// highlighter and the `--color-mock-diff-*` tokens.
import { ChevronDown, GitBranch, MoreHorizontal, RefreshCw, SquarePlus, X } from "lucide-react";
import {
ChevronDown,
FileDiff,
Files,
GitBranch,
GitPullRequest,
MoreHorizontal,
RefreshCw,
SquareDot,
X,
} from "lucide-react";
import { Code } from "../atoms";
import { ReactFileIcon } from "../icons";
// Every visible line of the new atoms.tsx — all additions. Long lines are
// clipped by the phone width exactly as they are in the app.
const ADDED_LINES: { n: number; text: string }[] = [
{ n: 1, text: "// Shared bits of the Paseo UI, drawn small. Every" },
{ n: 2, text: "// no state, no interactivity. Colors come from th" },
{ n: 3, text: "// styles.css, which are copied from the app's def" },
{ n: 4, text: "" },
{ n: 5, text: 'import type * as React from "react";' },
{ n: 6, text: "" },
{ n: 7, text: "/**" },
{ n: 8, text: " * The `+12 −18` footnote. A diff stat is a *statu" },
{ n: 9, text: " * uses statusSuccess/statusDanger — see the comme" },
{ n: 10, text: " * packages/app/src/styles/theme.ts." },
{ n: 11, text: " */" },
{ n: 12, text: "export function DiffStat({ add, remove }: { add: st" },
{ n: 13, text: " return (" },
{ n: 14, text: ' <span className="flex shrink-0 items-center ga' },
{ n: 15, text: ' <span className="text-mock-success">+{add}</s' },
{ n: 16, text: ' <span className="text-mock-danger">-{remove}<' },
{ n: 17, text: " </span>" },
{ n: 18, text: " );" },
{ n: 19, text: "}" },
{ n: 20, text: "" },
{ n: 21, text: 'export type DotTone = "success" | "danger" | "warni' },
{ n: 22, text: "" },
{ n: 23, text: "const DOT_TONE: Record<DotTone, string> = {" },
{ n: 24, text: ' success: "bg-mock-dot-success",' },
{ n: 25, text: ' danger: "bg-mock-dot-danger",' },
{ n: 26, text: ' warning: "bg-mock-dot-warning",' },
{ n: 27, text: ' running: "bg-mock-dot-running",' },
{ n: 28, text: ' idle: "bg-mock-surface3",' },
{ n: 29, text: "};" },
{ n: 30, text: "" },
{ n: 31, text: "/** The 6pt disc on a sidebar row that carries the" },
{ n: 32, text: "export function StatusDot({ tone }: { tone: DotTone" },
{ n: 33, text: " return <span className={`size-[7px] shrink-0 rou" },
type MobileDiffMark = "add" | "remove" | "context";
interface MobileDiffLine {
n: number;
text: string;
mark: MobileDiffMark;
}
const ROW_COLOR: Record<MobileDiffMark, string> = {
add: "bg-mock-diff-add",
remove: "bg-mock-diff-remove",
context: "",
};
const NUMBER_COLOR: Record<MobileDiffMark, string> = {
add: "text-mock-success",
remove: "text-mock-danger",
context: "text-mock-fg-muted",
};
const DIFF_LINES: MobileDiffLine[] = [
{ n: 7, text: 'type DotTone = "success" | "danger" | "idle";', mark: "remove" },
{ n: 7, text: "export type DotTone =", mark: "add" },
{ n: 8, text: ' | "success"', mark: "add" },
{ n: 9, text: ' | "danger"', mark: "add" },
{ n: 10, text: ' | "warning"', mark: "add" },
{ n: 11, text: ' | "running"', mark: "add" },
{ n: 12, text: ' | "idle";', mark: "add" },
{ n: 13, text: "", mark: "context" },
{ n: 14, text: "interface DiffStatProps {", mark: "context" },
{ n: 15, text: " add: string;", mark: "context" },
{ n: 16, text: " remove: string;", mark: "context" },
{ n: 17, text: "}", mark: "context" },
{ n: 18, text: "", mark: "context" },
{ n: 19, text: "/** Compact diff summary. */", mark: "add" },
{ n: 20, text: "export function DiffStat({", mark: "add" },
{ n: 21, text: " add,", mark: "add" },
{ n: 22, text: " remove,", mark: "add" },
{ n: 23, text: "}: DiffStatProps) {", mark: "add" },
{ n: 24, text: " return (", mark: "context" },
{ n: 25, text: ' <span className="flex gap-1">', mark: "remove" },
{ n: 25, text: ' <span className="flex gap-[4px]">', mark: "add" },
{ n: 26, text: ' <span className="text-mock-success">', mark: "context" },
{ n: 27, text: " +{add}", mark: "context" },
{ n: 28, text: " </span>", mark: "context" },
{ n: 29, text: ' <span className="text-mock-danger">', mark: "context" },
{ n: 30, text: " -{remove}", mark: "context" },
{ n: 31, text: " </span>", mark: "context" },
{ n: 32, text: " </span>", mark: "context" },
{ n: 33, text: " )", mark: "remove" },
{ n: 33, text: " );", mark: "add" },
{ n: 34, text: "}", mark: "context" },
];
function Tabs() {
return (
<div className="flex shrink-0 items-center gap-[16px] px-[18px] pt-[4px] pb-[14px]">
<div className="flex items-center gap-[6px] rounded-[11px] bg-mock-surface2 p-[3px]">
<span className="rounded-[8px] bg-mock-fg px-[15px] py-[6px] text-[16px] font-semibold text-mock-surface0">
Changes
</span>
<span className="px-[13px] py-[6px] text-[16px] text-mock-fg-muted">Files</span>
</div>
<div className="flex shrink-0 items-center gap-[5px] px-[18px] pt-[4px] pb-[14px]">
<span className="flex items-center gap-[7px] rounded-[8px] px-[10px] py-[6px] text-[16px] text-mock-fg-muted">
<Files size={17} strokeWidth={1.8} />
Files
</span>
<span className="flex items-center gap-[7px] rounded-[8px] bg-mock-surface2 px-[10px] py-[6px] text-[16px] text-mock-fg">
<FileDiff size={17} strokeWidth={1.8} />
Changes
</span>
<span className="flex items-center gap-[7px] rounded-[8px] px-[10px] py-[6px] text-[16px] text-mock-fg-muted tabular-nums">
<GitPullRequest size={17} strokeWidth={1.8} />
3981
</span>
<span className="flex-1" />
<X size={22} className="text-mock-fg-muted" strokeWidth={1.9} />
</div>
@@ -106,25 +138,26 @@ function FileHeader() {
packages/website/src/componen…
</span>
<span className="flex shrink-0 items-center gap-[3px] text-[14px] tabular-nums">
<span className="text-mock-success">+175</span>
<span className="text-mock-fg-xmuted">-0</span>
<span className="text-mock-success">+56</span>
<span className="text-mock-danger">-18</span>
</span>
<SquarePlus size={17} className="shrink-0 text-mock-fg-muted" strokeWidth={1.7} />
<SquareDot size={17} className="shrink-0 text-mock-warning" strokeWidth={1.7} />
</div>
);
}
function DiffLine({ n, text }: { n: number; text: string }) {
function DiffLine({ n, text, mark }: MobileDiffLine) {
const comment = text.trimStart().startsWith("/") || text.trimStart().startsWith("*");
return (
<div className="flex items-center bg-mock-diff-add" style={ROW_STYLE}>
<div className={`flex items-center ${ROW_COLOR[mark]}`} style={ROW_STYLE}>
<span
className="shrink-0 pr-[10px] text-right font-mono text-[13px] tabular-nums text-mock-success"
className={`shrink-0 pr-[10px] text-right font-mono text-[13px] tabular-nums ${NUMBER_COLOR[mark]}`}
style={GUTTER_STYLE}
>
{n}
</span>
<span className="min-w-0 flex-1 overflow-hidden whitespace-pre pl-[12px] font-mono text-[14px] leading-[26px]">
<Code line={text} />
{comment ? <span className="text-mock-fg-muted">{text}</span> : <Code line={text} />}
</span>
</div>
);
@@ -151,11 +184,11 @@ export function MobileDiff() {
<div className="flex items-center bg-mock-surface-diff-empty" style={ROW_STYLE}>
<span className="shrink-0" style={GUTTER_STYLE} />
<span className="pl-[12px] font-mono text-[13px] text-mock-fg-muted">
@@ -0,0 +1,175 @@
@@ -7,20 +7,28 @@
</span>
</div>
{ADDED_LINES.map((line) => (
<DiffLine key={line.n} n={line.n} text={line.text} />
{DIFF_LINES.map((line) => (
<DiffLine key={`${line.n}-${line.mark}-${line.text}`} {...line} />
))}
</div>
<div className="flex h-[52px] shrink-0 items-center border-t border-mock-border px-[18px] pb-[16px] text-[16px] text-mock-fg">
@@ -15,6 +15,21 @@ const SCREEN_SCALE = {
height: PHONE_H,
transform: `scale(tan(atan2(100cqw, ${PHONE_W}px)))`,
};
type PhoneDepth = "front" | "left" | "right";
const SLAB_STYLE: Record<Exclude<PhoneDepth, "front">, React.CSSProperties> = {
left: { transform: "translate3d(-5px, 0, -8px)" },
right: { transform: "translate3d(5px, 0, -8px)" },
};
const CONTACT_SHADOW_STYLE: Record<PhoneDepth, React.CSSProperties> = {
front: { transform: "translate3d(0, 4px, -12px)" },
left: { transform: "translate3d(-2px, 3px, -12px)" },
right: { transform: "translate3d(2px, 3px, -12px)" },
};
const GLASS_STYLE = {
background:
"linear-gradient(122deg, rgba(255,255,255,0.048) 0%, rgba(255,255,255,0.016) 25%, transparent 43%), radial-gradient(circle at 78% 10%, rgba(255,255,255,0.026), transparent 31%)",
};
/** iOS status bar: time, dynamic island, and the signal/wifi/battery cluster. */
function StatusBar({ time }: { time: string }) {
@@ -71,20 +86,54 @@ function HomeIndicator() {
);
}
export function PhoneFrame({ time, children }: { time: string; children: React.ReactNode }) {
function FrontButtons() {
return (
<div
className="relative w-full overflow-hidden rounded-[13.5%/6.2%] border-[3px] border-black bg-black shadow-2xl outline outline-[3px] outline-white/20"
style={FRAME_ASPECT}
>
<div className="absolute -inset-[3px] [container-type:inline-size]">
<div className="absolute top-0 left-0 origin-top-left" style={SCREEN_SCALE}>
<div className="relative flex h-[874px] w-[402px] flex-col overflow-hidden bg-mock-surface0 text-mock-fg antialiased">
<StatusBar time={time} />
<div className="relative flex min-h-0 flex-1 flex-col overflow-hidden">{children}</div>
<HomeIndicator />
<>
<span className="pointer-events-none absolute top-[18%] -left-[2px] h-[8%] w-[3px] rounded-l-[2px] bg-[#1b1e1d]" />
<span className="pointer-events-none absolute top-[28%] -left-[2px] h-[5%] w-[3px] rounded-l-[2px] bg-[#1b1e1d]" />
<span className="pointer-events-none absolute top-[25%] -right-[2px] h-[13%] w-[3px] rounded-r-[2px] bg-[#151716]" />
</>
);
}
export function PhoneFrame({
time,
children,
depth = "front",
}: {
time: string;
children: React.ReactNode;
depth?: PhoneDepth;
}) {
return (
<div className="relative w-full select-none [transform-style:preserve-3d]" style={FRAME_ASPECT}>
<div
className="pointer-events-none absolute right-[8%] -bottom-[0.8%] left-[8%] h-[3.5%] rounded-full bg-black/25 blur-[10px]"
style={CONTACT_SHADOW_STYLE[depth]}
/>
{depth === "front" ? (
<FrontButtons />
) : (
<div
className="pointer-events-none absolute inset-0 rounded-[13.5%/6.2%] border border-white/7 bg-[linear-gradient(180deg,#303432_0%,#1b1e1d_42%,#0b0d0c_100%)] shadow-[0_8px_22px_rgba(0,0,0,0.2)]"
style={SLAB_STYLE[depth]}
/>
)}
<div className="absolute inset-0 overflow-hidden rounded-[13.5%/6.2%] border-[3px] border-black bg-black shadow-[inset_0_0_0_1px_rgba(255,255,255,0.14)] outline outline-[1px] outline-white/25">
<div className="absolute -inset-[3px] [container-type:inline-size]">
<div className="absolute top-0 left-0 origin-top-left" style={SCREEN_SCALE}>
<div className="relative flex h-[874px] w-[402px] flex-col overflow-hidden bg-mock-surface0 text-mock-fg antialiased">
<StatusBar time={time} />
<div className="relative flex min-h-0 flex-1 flex-col overflow-hidden">
{children}
</div>
<HomeIndicator />
</div>
</div>
</div>
<div className="pointer-events-none absolute inset-0 z-20" style={GLASS_STYLE} />
</div>
</div>
);
@@ -267,13 +267,15 @@ export function Sidebar({ state }: { state: MockupStateId }) {
return (
<div className="flex shrink-0 flex-col overflow-hidden bg-mock-sidebar" style={SIDEBAR_STYLE}>
{/* The sidebar is one full-height column with no header border of its own. */}
<div className="flex shrink-0 items-center gap-[10px] px-[13px]" style={TITLEBAR_STYLE}>
<div className="flex items-center gap-[7px]">
<div className="flex shrink-0 items-center gap-[8px] px-[18px]" style={TITLEBAR_STYLE}>
<div className="flex h-[24px] items-center gap-[7px]">
<i className="block size-[10px] rounded-full bg-[#ff5f57]" />
<i className="block size-[10px] rounded-full bg-[#febc2e]" />
<i className="block size-[10px] rounded-full bg-[#28c840]" />
</div>
<PanelLeftGlyph />
<span className="flex size-[24px] shrink-0 items-center justify-center">
<PanelLeftGlyph />
</span>
</div>
<div className="px-[8px] pt-[7px]">
@@ -334,5 +336,5 @@ export function Sidebar({ state }: { state: MockupStateId }) {
}
function PanelLeftGlyph() {
return <PanelLeft size={15} strokeWidth={1.8} className="text-mock-fg-muted" />;
return <PanelLeft size={15} strokeWidth={1.8} className="translate-y-px text-mock-fg-muted" />;
}
@@ -101,7 +101,7 @@ export function MockupWindow({ state }: { state: MockupStateId }) {
return (
<MotionConfig transition={reduceMotion ? INSTANT_TRANSITION : MOCKUP_TRANSITION}>
<div
className="relative flex overflow-hidden bg-mock-surface0 text-mock-fg antialiased"
className="relative flex select-none overflow-hidden bg-mock-surface0 text-mock-fg antialiased"
style={WINDOW_STYLE}
>
{/* Three full-height columns, each owning its own header. The middle