فهرست منبع

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

Adam 2 ماه پیش
والد
کامیت
76dbe10ad7
4فایلهای تغییر یافته به همراه336 افزوده شده و 14 حذف شده
  1. 1 0
      packages/stats/AGENTS.md
  2. 12 0
      packages/stats/app/src/entry-server.tsx
  3. 198 11
      packages/stats/app/src/routes/index.css
  4. 125 3
      packages/stats/app/src/routes/index.tsx

+ 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
 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>

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

@@ -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:

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

@@ -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("")