feat(stats): match theme icons to figma (#29893)

This commit is contained in:
Adam 2026-05-29 08:04:34 -05:00 committed by GitHub
parent 46836beec9
commit 76dbe10ad7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 336 additions and 14 deletions

1
packages/stats/AGENTS.md Normal file
View file

@ -0,0 +1 @@
To start the stats site locally, run `bun dev:stats` from the repo root.

View file

@ -1,6 +1,17 @@
// @refresh reload
import { createHandler, StartServer } from "@solidjs/start/server"
const statsThemePreloadScript = `;(function () {
var preference = "system"
try {
var stored = localStorage.getItem("opencode:stats-theme")
if (stored === "dark" || stored === "light" || stored === "system") preference = stored
} catch (_) {}
document.documentElement.dataset.statsTheme = preference
if (preference === "system") document.documentElement.style.removeProperty("color-scheme")
else document.documentElement.style.setProperty("color-scheme", preference)
})()`
export default createHandler(
() => (
<StartServer
@ -9,6 +20,7 @@ export default createHandler(
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script id="stats-theme-preload-script">{statsThemePreloadScript}</script>
{assets}
</head>
<body>

View file

@ -43,6 +43,7 @@
}
[data-page="stats"] {
color-scheme: light;
--color-background: #ffffff;
--color-background-weak: #fafafa;
--color-background-weak-hover: #eeeeee;
@ -61,6 +62,7 @@
--stats-text: #161616;
--stats-muted: #5c5c5c;
--stats-faint: #808080;
--stats-theme-icon-active: #3a3a3a;
--stats-accent: #3b5cf6;
--stats-accent-text: #6c7dff;
--stats-bar-idle: #d4d4d4;
@ -440,6 +442,7 @@
}
[data-page="stats"] [data-slot="subscribe-button"]:focus-visible,
[data-page="stats"] [data-slot="theme-option"]:focus-visible,
[data-page="stats"] [data-component="subscribe-modal"] button:focus-visible,
[data-page="stats"] [data-component="subscribe-modal"] input:focus-visible {
outline: 2px solid var(--stats-accent);
@ -640,6 +643,54 @@
gap: 24px;
}
[data-page="stats"] [data-slot="theme-toggle"] {
display: flex;
align-items: center;
gap: 1px;
padding: 1px;
background: var(--stats-layer-2);
}
[data-page="stats"] [data-slot="theme-option"] {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 20px;
margin: 0;
padding: 0;
border: 0;
border-radius: 0;
appearance: none;
background: transparent;
color: var(--stats-faint);
font: inherit;
font-size: 11px;
line-height: 1;
cursor: pointer;
}
[data-page="stats"] [data-slot="theme-icon"] {
display: block;
width: 16px;
height: 16px;
pointer-events: none;
}
[data-page="stats"] [data-slot="theme-option"]:hover {
color: var(--stats-text);
}
[data-page="stats"] [data-slot="theme-option"][aria-pressed="true"] {
background: var(--stats-bg);
color: var(--stats-theme-icon-active);
box-shadow:
0 0 0 0 #0000,
0 0 0 0.5px #0000001f,
0 1px 2px -1px #00000014,
0 2px 4px 0 #0000000a;
}
[data-page="stats"] [data-slot="status"] {
display: flex;
align-items: center;
@ -2114,8 +2165,123 @@
width: fit-content;
}
[data-page="stats"][data-theme="dark"],
:root[data-stats-theme="dark"] [data-page="stats"]:not([data-theme="light"]) {
color-scheme: dark;
--color-background: #161616;
--color-background-weak: #242424;
--color-background-weak-hover: #303030;
--color-background-strong: #ffffff;
--color-background-strong-hover: #eeeeee;
--color-text: #d4d4d4;
--color-text-weak: #808080;
--color-text-strong: #ffffff;
--color-text-inverted: #161616;
--color-border-weak: #ffffff1a;
--stats-bg: #161616;
--stats-layer: #242424;
--stats-layer-2: #303030;
--stats-line: #ffffff1a;
--stats-line-strong: #ffffff33;
--stats-text: #ffffff;
--stats-muted: #d4d4d4;
--stats-faint: #808080;
--stats-theme-icon-active: #fafafa;
--stats-bar-idle: #303030;
--stats-dot: #303030;
--stats-hero-muted: #808080;
--stats-hero-pattern: #303030;
--stats-logo-bg: #f1ecec;
--stats-logo-fill: #b7b1b1;
--stats-logo-stroke: #211e1e;
}
[data-page="stats"][data-theme="dark"] [data-component="chart-tooltip"],
:root[data-stats-theme="dark"] [data-page="stats"]:not([data-theme="light"]) [data-component="chart-tooltip"] {
background: #242424f2;
}
[data-page="stats"][data-theme="dark"] [data-section="top-models"] [data-component="chart-tooltip"],
:root[data-stats-theme="dark"]
[data-page="stats"]:not([data-theme="light"])
[data-section="top-models"]
[data-component="chart-tooltip"] {
background: #242424f2;
box-shadow:
0 0 0 0.5px #ffffff24,
0 8px 16px #0000003d,
0 4px 8px #00000052;
}
[data-page="stats"][data-theme="dark"] [data-section="top-models"] [data-component="chart-tooltip"] > span,
:root[data-stats-theme="dark"]
[data-page="stats"]:not([data-theme="light"])
[data-section="top-models"]
[data-component="chart-tooltip"]
> span {
color: var(--stats-faint);
}
[data-page="stats"][data-theme="dark"] [data-component="leader-card"][data-size="compact"],
:root[data-stats-theme="dark"]
[data-page="stats"]:not([data-theme="light"])
[data-component="leader-card"][data-size="compact"] {
box-shadow:
0 0 0 0.5px #ffffff1f,
0 1px 2px -1px #00000052,
0 2px 4px #0000003d;
}
[data-page="stats"][data-theme="dark"] [data-component="leader-card"]:hover,
[data-page="stats"][data-theme="dark"] [data-component="leader-card"]:focus-visible,
:root[data-stats-theme="dark"] [data-page="stats"]:not([data-theme="light"]) [data-component="leader-card"]:hover,
:root[data-stats-theme="dark"]
[data-page="stats"]:not([data-theme="light"])
[data-component="leader-card"]:focus-visible {
background: #303030;
box-shadow:
0 0 0 0.5px #ffffff24,
0 6px 16px #0000003d,
0 2px 6px #00000052;
}
[data-page="stats"][data-theme="dark"] [data-slot="header-button"][data-variant="neutral"],
[data-page="stats"][data-theme="dark"] [data-slot="menu-button"],
:root[data-stats-theme="dark"]
[data-page="stats"]:not([data-theme="light"])
[data-slot="header-button"][data-variant="neutral"],
:root[data-stats-theme="dark"] [data-page="stats"]:not([data-theme="light"]) [data-slot="menu-button"] {
color: #fafafa;
background: #ffffff0f;
box-shadow:
0 -0.5px 0 0 #ffffff33,
0 0 0 0.5px #ffffff33,
0 1px 2px 0 #00000066;
}
[data-page="stats"][data-theme="dark"] [data-slot="header-button"][data-variant="neutral"] span,
:root[data-stats-theme="dark"]
[data-page="stats"]:not([data-theme="light"])
[data-slot="header-button"][data-variant="neutral"]
span {
color: #aeaeae;
}
[data-page="stats"][data-theme="dark"] [data-slot="header-button"][data-variant="contrast"],
:root[data-stats-theme="dark"]
[data-page="stats"]:not([data-theme="light"])
[data-slot="header-button"][data-variant="contrast"] {
color: #ffffff;
background: #5c5c5c;
box-shadow:
0 -0.5px 0 0 #ffffff4d,
0 0 0 0.5px #ffffff66,
0 1px 2px 0 #00000066;
}
@media (prefers-color-scheme: dark) {
[data-page="stats"] {
:root:not([data-stats-theme="light"]) [data-page="stats"]:not([data-theme="light"]) {
color-scheme: dark;
--color-background: #161616;
--color-background-weak: #242424;
--color-background-weak-hover: #303030;
@ -2134,6 +2300,7 @@
--stats-text: #ffffff;
--stats-muted: #d4d4d4;
--stats-faint: #808080;
--stats-theme-icon-active: #fafafa;
--stats-bar-idle: #303030;
--stats-dot: #303030;
--stats-hero-muted: #808080;
@ -2143,11 +2310,14 @@
--stats-logo-stroke: #211e1e;
}
[data-page="stats"] [data-component="chart-tooltip"] {
:root:not([data-stats-theme="light"]) [data-page="stats"]:not([data-theme="light"]) [data-component="chart-tooltip"] {
background: #242424f2;
}
[data-page="stats"] [data-section="top-models"] [data-component="chart-tooltip"] {
:root:not([data-stats-theme="light"])
[data-page="stats"]:not([data-theme="light"])
[data-section="top-models"]
[data-component="chart-tooltip"] {
background: #242424f2;
box-shadow:
0 0 0 0.5px #ffffff24,
@ -2155,19 +2325,29 @@
0 4px 8px #00000052;
}
[data-page="stats"] [data-section="top-models"] [data-component="chart-tooltip"] > span {
:root:not([data-stats-theme="light"])
[data-page="stats"]:not([data-theme="light"])
[data-section="top-models"]
[data-component="chart-tooltip"]
> span {
color: var(--stats-faint);
}
[data-page="stats"] [data-component="leader-card"][data-size="compact"] {
:root:not([data-stats-theme="light"])
[data-page="stats"]:not([data-theme="light"])
[data-component="leader-card"][data-size="compact"] {
box-shadow:
0 0 0 0.5px #ffffff1f,
0 1px 2px -1px #00000052,
0 2px 4px #0000003d;
}
[data-page="stats"] [data-component="leader-card"]:hover,
[data-page="stats"] [data-component="leader-card"]:focus-visible {
:root:not([data-stats-theme="light"])
[data-page="stats"]:not([data-theme="light"])
[data-component="leader-card"]:hover,
:root:not([data-stats-theme="light"])
[data-page="stats"]:not([data-theme="light"])
[data-component="leader-card"]:focus-visible {
background: #303030;
box-shadow:
0 0 0 0.5px #ffffff24,
@ -2175,8 +2355,10 @@
0 2px 6px #00000052;
}
[data-page="stats"] [data-slot="header-button"][data-variant="neutral"],
[data-page="stats"] [data-slot="menu-button"] {
:root:not([data-stats-theme="light"])
[data-page="stats"]:not([data-theme="light"])
[data-slot="header-button"][data-variant="neutral"],
:root:not([data-stats-theme="light"]) [data-page="stats"]:not([data-theme="light"]) [data-slot="menu-button"] {
color: #fafafa;
background: #ffffff0f;
box-shadow:
@ -2185,11 +2367,16 @@
0 1px 2px 0 #00000066;
}
[data-page="stats"] [data-slot="header-button"][data-variant="neutral"] span {
:root:not([data-stats-theme="light"])
[data-page="stats"]:not([data-theme="light"])
[data-slot="header-button"][data-variant="neutral"]
span {
color: #aeaeae;
}
[data-page="stats"] [data-slot="header-button"][data-variant="contrast"] {
:root:not([data-stats-theme="light"])
[data-page="stats"]:not([data-theme="light"])
[data-slot="header-button"][data-variant="contrast"] {
color: #ffffff;
background: #5c5c5c;
box-shadow:

View file

@ -67,10 +67,18 @@ const usageColors = [
"#ff6467",
]
const marketColors = ["#ed6aff", "#a684ff", "#7c86ff", "#51a2ff", "#00d3f2", "#00d5be", "#00bc7d", "#9ae600", "#ffb900"]
const themePreferences = ["dark", "light", "system"] as const
const themePreferenceLabels = {
dark: "Dark",
light: "Light",
system: "System",
} as const
const themeStorageKey = "opencode:stats-theme"
type UsageProduct = (typeof products)[number]
type TokenProduct = (typeof tokenProducts)[number]
type UsageRange = (typeof ranges)[number]
type ThemePreference = (typeof themePreferences)[number]
const getData = query(async () => {
"use server"
@ -104,9 +112,24 @@ export default function StatsHome() {
const statsUnfurlUrl = new URL(statsUnfurlRankings, statsHomeUrl).toString()
const data = createAsync(() => getData())
const githubStars = createAsync(() => getGitHubStars())
const [themePreference, setThemePreference] = createSignal<ThemePreference>("system")
const updateThemePreference = (preference: ThemePreference) => {
applyThemePreference(preference)
setThemePreference(preference)
if (typeof window === "undefined") return
window.localStorage.setItem(themeStorageKey, preference)
}
onMount(() => {
if (typeof window === "undefined") return
const preference = window.localStorage.getItem(themeStorageKey)
const nextPreference = isThemePreference(preference) ? preference : "system"
applyThemePreference(nextPreference)
setThemePreference(nextPreference)
})
return (
<main data-page="stats">
<main data-page="stats" data-theme={themePreference()}>
<Title>{statsHomeTitle}</Title>
<Meta name="description" content={statsHomeDescription} />
<Link rel="canonical" href={statsHomeUrl} />
@ -145,12 +168,26 @@ export default function StatsHome() {
)}
</Show>
</div>
<Footer />
<Footer themePreference={themePreference()} onThemePreferenceChange={updateThemePreference} />
</div>
</main>
)
}
function isThemePreference(value: string | null): value is ThemePreference {
return value === "dark" || value === "light" || value === "system"
}
function applyThemePreference(preference: ThemePreference) {
if (typeof document === "undefined") return
document.documentElement.dataset.statsTheme = preference
if (preference === "system") {
document.documentElement.style.removeProperty("color-scheme")
return
}
document.documentElement.style.setProperty("color-scheme", preference)
}
function Hero(props: { updatedAt: string | null }) {
const [timeZone, setTimeZone] = createSignal("UTC")
const [previousTimeZone, setPreviousTimeZone] = createSignal("UTC")
@ -1421,7 +1458,10 @@ function OpenCodeMark() {
)
}
function Footer() {
function Footer(props: {
themePreference: ThemePreference
onThemePreferenceChange: (preference: ThemePreference) => void
}) {
const [subscribeOpen, setSubscribeOpen] = createSignal(false)
const modelStats = [
{ href: "#top-models", label: "Top Models" },
@ -1466,6 +1506,22 @@ function Footer() {
<span>© 2026 Anomaly Innovations Inc.</span>
<span data-slot="status">All systems Operational</span>
</div>
<div data-slot="theme-toggle" role="group" aria-label="Theme">
<For each={themePreferences}>
{(preference) => (
<button
data-slot="theme-option"
type="button"
aria-label={themePreferenceLabels[preference]}
aria-pressed={props.themePreference === preference ? "true" : "false"}
title={themePreferenceLabels[preference]}
onClick={() => props.onThemePreferenceChange(preference)}
>
<ThemePreferenceIcon preference={preference} />
</button>
)}
</For>
</div>
</div>
<Show when={subscribeOpen()}>
<SubscribeModal onClose={() => setSubscribeOpen(false)} />
@ -1474,6 +1530,72 @@ function Footer() {
)
}
function ThemePreferenceIcon(props: { preference: ThemePreference }) {
return (
<svg data-slot="theme-icon" width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<Show
when={props.preference === "dark"}
fallback={
<Show
when={props.preference === "light"}
fallback={
<>
<rect x="1.5552" y="2.4448" width="12.8896" height="8.8888" fill="currentColor" opacity="0.3" />
<svg
x="1.0552"
y="1.9446"
width="13.8889"
height="12.5325"
viewBox="0 0 13.8889 12.5325"
preserveAspectRatio="none"
overflow="visible"
>
<path
d="M4.05559 12.0555C4.72936 11.8431 5.72492 11.6111 6.94448 11.6111M6.94448 11.6111C7.65114 11.6111 8.66981 11.6893 9.83336 12.0555M6.94448 11.6111L6.94448 9.38888M13.3889 0.5H0.500102C0.500102 0.5 0.500017 1.29594 0.500017 2.27778V7.61112C0.500017 8.59298 0.500007 9.38889 0.500007 9.38889H13.3889C13.3889 9.38889 13.3889 8.59298 13.3889 7.61112V2.27778C13.3889 1.29594 13.3889 0.5 13.3889 0.5Z"
stroke="currentColor"
/>
</svg>
</>
}
>
<svg
x="0.6102"
y="0.6102"
width="14.7778"
height="14.7778"
viewBox="0 0 14.7778 14.7778"
preserveAspectRatio="none"
overflow="visible"
>
<path
d="M7.38889 0.5V1.38889M12.26 2.51782L11.6315 3.14627M14.2778 7.38892H13.3889M12.26 12.26L11.6315 11.6316M7.38889 14.2778V13.3889M2.51778 12.26L3.14622 11.6316M0.5 7.38892H1.38889M2.51778 2.51782L3.14622 3.14627M7.38888 11.1666C9.47528 11.1666 11.1667 9.47526 11.1667 7.38886C11.1667 5.30245 9.47528 3.61108 7.38888 3.61108C5.30247 3.61108 3.6111 5.30245 3.6111 7.38886C3.6111 9.47526 5.30247 11.1666 7.38888 11.1666Z"
stroke="currentColor"
stroke-linecap="square"
/>
</svg>
</Show>
}
>
<svg
x="2.0549"
y="1.742"
width="12.3867"
height="12.3971"
viewBox="0 0 12.3867 12.3971"
preserveAspectRatio="none"
overflow="visible"
>
<path
d="M9.05556 8.39711C6.37067 8.39711 4.19444 6.22089 4.19444 3.536C4.19444 2.48445 4.53122 1.51456 5.09822 0.71889C2.48178 1.20733 0.5 3.49944 0.5 6.25822C0.5 9.37244 3.02467 11.8971 6.13889 11.8971C8.76156 11.8971 10.9596 10.1036 11.5903 7.67844C10.8514 8.13189 9.98578 8.39711 9.05556 8.39711Z"
stroke="currentColor"
stroke-linecap="round"
/>
</svg>
</Show>
</svg>
)
}
function SubscribeModal(props: { onClose: () => void }) {
const [status, setStatus] = createSignal<"idle" | "pending" | "success" | "error">("idle")
const [message, setMessage] = createSignal("")