fix(stats): refine hero layout for responsive Figma match

This commit is contained in:
Adam
2026-05-28 12:43:16 -05:00
parent fff7781fa8
commit 3ce9b4be0d
2 changed files with 46 additions and 19 deletions
+30 -11
View File
@@ -89,7 +89,7 @@
[data-page="stats"] [data-component="container"] { [data-page="stats"] [data-component="container"] {
box-sizing: border-box; box-sizing: border-box;
width: 100%; width: 100%;
max-width: 80rem; max-width: calc(80rem + 2px);
margin: 0 auto; margin: 0 auto;
border-left: 1px solid var(--stats-line); border-left: 1px solid var(--stats-line);
border-right: 1px solid var(--stats-line); border-right: 1px solid var(--stats-line);
@@ -461,6 +461,7 @@
} }
[data-page="stats"] [data-section="hero"] h1 { [data-page="stats"] [data-section="hero"] h1 {
order: 1;
color: var(--stats-text); color: var(--stats-text);
font-size: 64px; font-size: 64px;
font-weight: 500; font-weight: 500;
@@ -469,20 +470,30 @@
} }
[data-page="stats"] [data-slot="hero-copy"] { [data-page="stats"] [data-slot="hero-copy"] {
order: 3;
color: var(--stats-hero-muted); color: var(--stats-hero-muted);
font-size: 16px; font-size: 16px;
font-weight: 400; font-weight: 400;
line-height: 1.5; line-height: 1.5;
} }
[data-page="stats"] [data-slot="hero-copy-break"] {
display: none;
}
[data-page="stats"] [data-slot="hero-pattern"] { [data-page="stats"] [data-slot="hero-pattern"] {
order: 2;
flex: 0 0 auto; flex: 0 0 auto;
width: 100%; width: 100%;
height: 16px; height: 16px;
overflow: hidden; overflow: hidden;
background-image: radial-gradient(circle, var(--stats-hero-pattern) 1px, transparent 1.1px); background: var(--stats-hero-pattern);
background-position: 0 0; mask-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 6 6' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0H2V2H0V0Z' fill='black'/%3E%3C/svg%3E");
background-size: 6px 6px; mask-repeat: repeat;
mask-size: 6px 6px;
-webkit-mask-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 6 6' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0H2V2H0V0Z' fill='black'/%3E%3C/svg%3E");
-webkit-mask-repeat: repeat;
-webkit-mask-size: 6px 6px;
} }
[data-page="stats"] [data-slot="hero-meta"] { [data-page="stats"] [data-slot="hero-meta"] {
@@ -496,7 +507,7 @@
background: var(--stats-layer-2); background: var(--stats-layer-2);
color: var(--stats-hero-muted); color: var(--stats-hero-muted);
font-size: 13px; font-size: 13px;
font-weight: 600; font-weight: 500;
line-height: 1.1; line-height: 1.1;
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
@@ -514,10 +525,10 @@
text-overflow: ellipsis; text-overflow: ellipsis;
} }
@media (min-width: 58rem) { @media (min-width: 48rem) {
[data-page="stats"] [data-section="hero"] { [data-page="stats"] [data-section="hero"] {
gap: 16px; gap: 16px;
padding: 128px 40px 24px; padding: 128px 32px 32px;
} }
[data-page="stats"] [data-slot="hero-canvas"] { [data-page="stats"] [data-slot="hero-canvas"] {
@@ -553,11 +564,21 @@
right: 0; right: 0;
bottom: 0; bottom: 0;
z-index: 1; z-index: 1;
width: min(36rem, 100%); width: min(563px, 100%);
padding: 12px 0 0 16px; padding: 12px 0 0 16px;
background: var(--stats-bg); background: var(--stats-bg);
text-align: right; text-align: right;
} }
[data-page="stats"] [data-slot="hero-copy-break"] {
display: block;
}
}
@media (min-width: 75rem) {
[data-page="stats"] [data-section="hero"] {
padding: 128px 40px 24px;
}
} }
@media (min-width: 90rem) { @media (min-width: 90rem) {
@@ -1494,7 +1515,7 @@
} }
} }
@media (max-width: 74rem) { @media (max-width: 80rem) {
[data-page="stats"] [data-component="container"] { [data-page="stats"] [data-component="container"] {
border: 0; border: 0;
} }
@@ -1540,14 +1561,12 @@
--stats-section-padding: 4rem; --stats-section-padding: 4rem;
} }
[data-page="stats"] [data-section="hero"],
[data-page="stats"] [data-section="chart"], [data-page="stats"] [data-section="chart"],
[data-page="stats"] [data-section="newsletter"] { [data-page="stats"] [data-section="newsletter"] {
padding-left: 24px; padding-left: 24px;
padding-right: 24px; padding-right: 24px;
} }
[data-page="stats"] [data-section="hero"],
[data-page="stats"] [data-section="newsletter"] { [data-page="stats"] [data-section="newsletter"] {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
+16 -8
View File
@@ -18,7 +18,7 @@ import {
import { runtime } from "@opencode-ai/stats-core/runtime" import { runtime } from "@opencode-ai/stats-core/runtime"
import { createAsync, query } from "@solidjs/router" import { createAsync, query } from "@solidjs/router"
import { scaleBand, scaleLinear } from "d3-scale" import { scaleBand, scaleLinear } from "d3-scale"
import { createEffect, createMemo, createSignal, For, onCleanup, Show, type JSX } from "solid-js" import { createEffect, createMemo, createSignal, For, onCleanup, onMount, Show, type JSX } from "solid-js"
import { getRequestEvent } from "solid-js/web" import { getRequestEvent } from "solid-js/web"
const products = ["All Users", "Zen", "Go", "Enterprise"] as const const products = ["All Users", "Zen", "Go", "Enterprise"] as const
@@ -98,21 +98,29 @@ export default function StatsHome() {
} }
function Hero(props: { updatedAt: string | null }) { function Hero(props: { updatedAt: string | null }) {
const [timeZone, setTimeZone] = createSignal("UTC")
onMount(() => setTimeZone(Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC"))
return ( return (
<section data-section="hero"> <section data-section="hero">
<p data-slot="hero-meta"> <p data-slot="hero-meta">
<svg aria-hidden="true" width="16" height="16" viewBox="0 0 16 16"> <svg aria-hidden="true" width="16" height="16" viewBox="0 0 16 16">
<rect x="3" y="3" width="10" height="10" fill="currentColor" /> <path
<rect x="7" y="6.5" width="2" height="4.5" fill="var(--stats-layer-2)" /> fill-rule="evenodd"
<rect x="7" y="5" width="2" height="1" fill="var(--stats-layer-2)" /> clip-rule="evenodd"
d="M13 13H3V3H13V13ZM6.46777 6.81641V7.81641H7.5791V11.3721H8.5791V6.81641H6.46777ZM7.30078 4.62891V5.62891H8.85645V4.62891H7.30078Z"
fill="currentColor"
/>
</svg> </svg>
<span>{props.updatedAt ? `Updated ${formatUpdatedAt(props.updatedAt)}` : "No rows yet"}</span> <span>{props.updatedAt ? `Updated ${formatUpdatedAt(props.updatedAt, timeZone())}` : "No rows yet"}</span>
</p> </p>
<div data-slot="hero-canvas"> <div data-slot="hero-canvas">
<div data-slot="hero-pattern" aria-hidden="true" /> <div data-slot="hero-pattern" aria-hidden="true" />
<h1>Model Stats</h1> <h1>Model Stats</h1>
<p data-slot="hero-copy"> <p data-slot="hero-copy">
See which models are winning real usage, how the mix shifts over time, and where momentum is moving each week. See which models are winning real usage, how the mix{" "}
<br data-slot="hero-copy-break" />
shifts over time, and where momentum is moving each week.
</p> </p>
</div> </div>
</section> </section>
@@ -154,7 +162,7 @@ function EmptyState(props: { title: string; description: string }) {
) )
} }
function formatUpdatedAt(value: string) { function formatUpdatedAt(value: string, timeZone: string) {
const date = new Date(value) const date = new Date(value)
if (Number.isNaN(date.getTime())) return "just now" if (Number.isNaN(date.getTime())) return "just now"
return new Intl.DateTimeFormat("en", { return new Intl.DateTimeFormat("en", {
@@ -162,7 +170,7 @@ function formatUpdatedAt(value: string) {
day: "numeric", day: "numeric",
hour: "numeric", hour: "numeric",
minute: "2-digit", minute: "2-digit",
timeZone: "UTC", timeZone,
timeZoneName: "short", timeZoneName: "short",
}).format(date) }).format(date)
} }