Explorar o código

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

Adam hai 3 meses
pai
achega
76dbe10ad7

+ 1 - 0
packages/stats/AGENTS.md

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

+ 12 - 0
packages/stats/app/src/entry-server.tsx

@@ -1,6 +1,17 @@
 // @refresh reload
 // @refresh reload
 import { createHandler, StartServer } from "@solidjs/start/server"
 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(
 export default createHandler(
   () => (
   () => (
     <StartServer
     <StartServer
@@ -9,6 +20,7 @@ export default createHandler(
           <head>
           <head>
             <meta charset="utf-8" />
             <meta charset="utf-8" />
             <meta name="viewport" content="width=device-width, initial-scale=1" />
             <meta name="viewport" content="width=device-width, initial-scale=1" />
+            <script id="stats-theme-preload-script">{statsThemePreloadScript}</script>
             {assets}
             {assets}
           </head>
           </head>
           <body>
           <body>

+ 198 - 11
packages/stats/app/src/routes/index.css

@@ -43,6 +43,7 @@
 }
 }
 
 
 [data-page="stats"] {
 [data-page="stats"] {
+  color-scheme: light;
   --color-background: #ffffff;
   --color-background: #ffffff;
   --color-background-weak: #fafafa;
   --color-background-weak: #fafafa;
   --color-background-weak-hover: #eeeeee;
   --color-background-weak-hover: #eeeeee;
@@ -61,6 +62,7 @@
   --stats-text: #161616;
   --stats-text: #161616;
   --stats-muted: #5c5c5c;
   --stats-muted: #5c5c5c;
   --stats-faint: #808080;
   --stats-faint: #808080;
+  --stats-theme-icon-active: #3a3a3a;
   --stats-accent: #3b5cf6;
   --stats-accent: #3b5cf6;
   --stats-accent-text: #6c7dff;
   --stats-accent-text: #6c7dff;
   --stats-bar-idle: #d4d4d4;
   --stats-bar-idle: #d4d4d4;
@@ -440,6 +442,7 @@
 }
 }
 
 
 [data-page="stats"] [data-slot="subscribe-button"]:focus-visible,
 [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"] button:focus-visible,
 [data-page="stats"] [data-component="subscribe-modal"] input:focus-visible {
 [data-page="stats"] [data-component="subscribe-modal"] input:focus-visible {
   outline: 2px solid var(--stats-accent);
   outline: 2px solid var(--stats-accent);
@@ -640,6 +643,54 @@
   gap: 24px;
   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"] {
 [data-page="stats"] [data-slot="status"] {
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
@@ -2114,8 +2165,123 @@
   width: fit-content;
   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) {
 @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: #161616;
     --color-background-weak: #242424;
     --color-background-weak: #242424;
     --color-background-weak-hover: #303030;
     --color-background-weak-hover: #303030;
@@ -2134,6 +2300,7 @@
     --stats-text: #ffffff;
     --stats-text: #ffffff;
     --stats-muted: #d4d4d4;
     --stats-muted: #d4d4d4;
     --stats-faint: #808080;
     --stats-faint: #808080;
+    --stats-theme-icon-active: #fafafa;
     --stats-bar-idle: #303030;
     --stats-bar-idle: #303030;
     --stats-dot: #303030;
     --stats-dot: #303030;
     --stats-hero-muted: #808080;
     --stats-hero-muted: #808080;
@@ -2143,11 +2310,14 @@
     --stats-logo-stroke: #211e1e;
     --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;
     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;
     background: #242424f2;
     box-shadow:
     box-shadow:
       0 0 0 0.5px #ffffff24,
       0 0 0 0.5px #ffffff24,
@@ -2155,19 +2325,29 @@
       0 4px 8px #00000052;
       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);
     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:
     box-shadow:
       0 0 0 0.5px #ffffff1f,
       0 0 0 0.5px #ffffff1f,
       0 1px 2px -1px #00000052,
       0 1px 2px -1px #00000052,
       0 2px 4px #0000003d;
       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;
     background: #303030;
     box-shadow:
     box-shadow:
       0 0 0 0.5px #ffffff24,
       0 0 0 0.5px #ffffff24,
@@ -2175,8 +2355,10 @@
       0 2px 6px #00000052;
       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;
     color: #fafafa;
     background: #ffffff0f;
     background: #ffffff0f;
     box-shadow:
     box-shadow:
@@ -2185,11 +2367,16 @@
       0 1px 2px 0 #00000066;
       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;
     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;
     color: #ffffff;
     background: #5c5c5c;
     background: #5c5c5c;
     box-shadow:
     box-shadow:

+ 125 - 3
packages/stats/app/src/routes/index.tsx

@@ -67,10 +67,18 @@ const usageColors = [
   "#ff6467",
   "#ff6467",
 ]
 ]
 const marketColors = ["#ed6aff", "#a684ff", "#7c86ff", "#51a2ff", "#00d3f2", "#00d5be", "#00bc7d", "#9ae600", "#ffb900"]
 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 UsageProduct = (typeof products)[number]
 type TokenProduct = (typeof tokenProducts)[number]
 type TokenProduct = (typeof tokenProducts)[number]
 type UsageRange = (typeof ranges)[number]
 type UsageRange = (typeof ranges)[number]
+type ThemePreference = (typeof themePreferences)[number]
 
 
 const getData = query(async () => {
 const getData = query(async () => {
   "use server"
   "use server"
@@ -104,9 +112,24 @@ export default function StatsHome() {
   const statsUnfurlUrl = new URL(statsUnfurlRankings, statsHomeUrl).toString()
   const statsUnfurlUrl = new URL(statsUnfurlRankings, statsHomeUrl).toString()
   const data = createAsync(() => getData())
   const data = createAsync(() => getData())
   const githubStars = createAsync(() => getGitHubStars())
   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 (
   return (
-    <main data-page="stats">
+    <main data-page="stats" data-theme={themePreference()}>
       <Title>{statsHomeTitle}</Title>
       <Title>{statsHomeTitle}</Title>
       <Meta name="description" content={statsHomeDescription} />
       <Meta name="description" content={statsHomeDescription} />
       <Link rel="canonical" href={statsHomeUrl} />
       <Link rel="canonical" href={statsHomeUrl} />
@@ -145,12 +168,26 @@ export default function StatsHome() {
             )}
             )}
           </Show>
           </Show>
         </div>
         </div>
-        <Footer />
+        <Footer themePreference={themePreference()} onThemePreferenceChange={updateThemePreference} />
       </div>
       </div>
     </main>
     </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 }) {
 function Hero(props: { updatedAt: string | null }) {
   const [timeZone, setTimeZone] = createSignal("UTC")
   const [timeZone, setTimeZone] = createSignal("UTC")
   const [previousTimeZone, setPreviousTimeZone] = 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 [subscribeOpen, setSubscribeOpen] = createSignal(false)
   const modelStats = [
   const modelStats = [
     { href: "#top-models", label: "Top Models" },
     { href: "#top-models", label: "Top Models" },
@@ -1466,6 +1506,22 @@ function Footer() {
           <span>© 2026 Anomaly Innovations Inc.</span>
           <span>© 2026 Anomaly Innovations Inc.</span>
           <span data-slot="status">All systems Operational</span>
           <span data-slot="status">All systems Operational</span>
         </div>
         </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>
       </div>
       <Show when={subscribeOpen()}>
       <Show when={subscribeOpen()}>
         <SubscribeModal onClose={() => setSubscribeOpen(false)} />
         <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 }) {
 function SubscribeModal(props: { onClose: () => void }) {
   const [status, setStatus] = createSignal<"idle" | "pending" | "success" | "error">("idle")
   const [status, setStatus] = createSignal<"idle" | "pending" | "success" | "error">("idle")
   const [message, setMessage] = createSignal("")
   const [message, setMessage] = createSignal("")