Răsfoiți Sursa

docs: redesign Go pricing graph with horizontal bars and inline request labels

Improve visual clarity of request limits on the Go pricing page by replacing
dot-based chart with animated horizontal bars that directly show model names
and exact request counts. Add proper OpenGraph and Twitter Card meta tags for
better social sharing discovery.
David Hill 1 lună în urmă
părinte
comite
0b825ca383

+ 2 - 0
packages/console/app/src/i18n/en.ts

@@ -246,6 +246,8 @@ export const dict = {
   "zen.privacy.exceptionsLink": "following exceptions",
 
   "go.title": "OpenCode Go | Low cost coding models for everyone",
+  "go.meta.description":
+    "Go is a $10/month subscription with generous 5-hour request limits for GLM-5, Kimi K2.5, and MiniMax M2.5.",
   "go.hero.title": "Low cost coding models for everyone",
   "go.hero.body":
     "Go brings agentic coding to programmers around the world. Offering generous limits and reliable access to the most capable open-source models, so you can build with powerful agents without worrying about cost or availability.",

+ 143 - 111
packages/console/app/src/routes/go/index.css

@@ -21,6 +21,13 @@
   }
 }
 
+@keyframes go-graph-bar {
+  to {
+    opacity: 1;
+    transform: scaleX(1);
+  }
+}
+
 [data-page="go"] {
   --color-background: hsl(0, 20%, 99%);
   --color-background-weak: hsl(0, 8%, 97%);
@@ -424,13 +431,78 @@ body {
 
     [data-component="limit-graph"] {
       margin: 0 auto;
-      max-width: calc(100% - (var(--padding) * 2));
+      width: calc(100% - 120px);
+      max-width: calc(100% - 120px);
       border: none;
       background: transparent;
-      padding: 18px 18px 56px;
+      padding: 58px var(--padding) 56px;
+
+      @media (max-width: 48rem) {
+        width: 100%;
+        max-width: 100%;
+      }
 
       [data-slot="plot"] {
         position: relative;
+        overflow: visible;
+        width: 100%;
+        margin: 0 auto;
+        margin-left: -40px;
+      }
+
+      [data-slot="ylabels"] {
+        position: absolute;
+        inset: 0;
+        pointer-events: none;
+      }
+
+      [data-slot="ylabels"] [data-ylabel] {
+        position: absolute;
+        left: var(--x);
+        top: var(--y);
+        transform: translate(-100%, -50%);
+        color: var(--color-text-strong);
+        font-size: 16px;
+        font-weight: 700;
+        line-height: 1;
+        white-space: nowrap;
+      }
+
+      [data-slot="pills"] {
+        position: absolute;
+        inset: 0;
+        pointer-events: none;
+
+        [data-item] {
+          position: absolute;
+          left: var(--x);
+          top: var(--y);
+          transform: translate(12px, -50%);
+          display: inline-flex;
+          align-items: center;
+          gap: 8px;
+          border: none;
+          background: transparent;
+          height: 20px;
+          padding: 0 8px;
+          border-radius: 2px;
+          max-width: calc(100% - 12px);
+          font-size: 13px;
+          line-height: 20px;
+          box-sizing: border-box;
+          opacity: 0;
+        }
+
+        [data-name] {
+          color: var(--color-text);
+          white-space: nowrap;
+        }
+
+        [data-value] {
+          color: var(--color-text-strong);
+          font-weight: 600;
+          white-space: nowrap;
+        }
       }
 
       [data-slot="plot-labels"] {
@@ -451,8 +523,7 @@ body {
 
       svg {
         width: 100%;
-        height: auto;
-        aspect-ratio: 720 / 220;
+        height: 220px;
         display: block;
       }
 
@@ -479,13 +550,44 @@ body {
         font-weight: 600;
       }
 
+      [data-row],
+      [data-val] {
+        opacity: 0;
+      }
+
+      &[data-visible] [data-row],
+      &[data-visible] [data-val] {
+        opacity: 1;
+        transition: opacity 240ms ease;
+        transition-delay: var(--d, 0ms);
+      }
+
       [data-stub] {
         stroke: var(--color-border);
-        stroke-width: 2;
+        stroke-width: 1;
         stroke-linecap: round;
         opacity: 0.55;
       }
 
+      [data-bar] {
+        transform-box: fill-box;
+        transform-origin: left center;
+        opacity: 0;
+        transform: scaleX(0.02);
+        fill: var(--color-go-2);
+        stroke: none;
+      }
+
+      [data-bar][data-kind="free"] {
+        fill: var(--color-text-strong);
+      }
+
+      [data-val] {
+        fill: var(--color-text-strong);
+        font-size: 13px;
+        font-weight: 650;
+      }
+
       [data-range] {
         stroke: var(--color-text-strong);
         stroke-width: 2;
@@ -542,6 +644,17 @@ body {
         animation-delay: var(--d, 0ms);
       }
 
+      &[data-visible] [data-bar] {
+        animation: go-graph-bar 560ms cubic-bezier(0.2, 0.7, 0.2, 1) forwards;
+        animation-delay: var(--d, 0ms);
+      }
+
+      &[data-visible] [data-slot="pills"] [data-item] {
+        opacity: 1;
+        transition: opacity 240ms ease;
+        transition-delay: var(--d, 0ms);
+      }
+
       @media (prefers-reduced-motion: reduce) {
         [data-animate="line"] {
           stroke-dashoffset: 0;
@@ -552,34 +665,49 @@ body {
           transform: none;
           animation: none;
         }
+        [data-bar] {
+          opacity: 1;
+          transform: none;
+          animation: none;
+        }
+        [data-row],
+        [data-val] {
+          opacity: 1;
+          transition: none;
+        }
+
+        [data-slot="pills"] [data-item] {
+          opacity: 1;
+          transition: none;
+        }
       }
 
       figcaption {
         margin-top: 34px;
         display: flex;
-        flex-direction: column;
-        gap: 10px;
+        justify-content: center;
         font-size: 13px;
+        text-align: center;
       }
 
       [data-slot="caption-row"] {
         display: flex;
         width: 100%;
+        justify-content: center;
       }
 
       [data-slot="caption-left"] {
-        display: grid;
+        display: flex;
         width: 100%;
-        grid-template-columns: var(--start, 16.9%) minmax(0, 1fr);
-        grid-template-rows: auto auto;
-        align-items: center;
-        column-gap: 0;
-        row-gap: 0;
         min-width: 0;
+        justify-content: center;
       }
 
       [data-slot="caption-meta"] {
-        display: contents;
+        display: flex;
+        flex-direction: row;
+        gap: 16px;
+        align-items: baseline;
       }
 
       [data-slot="caption-label"] {
@@ -587,8 +715,6 @@ body {
         font-weight: 650;
         white-space: nowrap;
         line-height: 1;
-        grid-column: 1;
-        grid-row: 1;
       }
 
       [data-slot="caption-link"] {
@@ -596,73 +722,6 @@ body {
         text-decoration-thickness: 1px;
         width: fit-content;
         line-height: 1;
-        grid-column: 1;
-        grid-row: 2;
-        align-self: start;
-      }
-
-      [data-slot="legend"] {
-        display: flex;
-        width: 100%;
-        flex-wrap: nowrap;
-        gap: 10px;
-        min-width: 0;
-        overflow-x: auto;
-        -webkit-overflow-scrolling: touch;
-        padding-bottom: 8px;
-        margin-left: -12px;
-        grid-column: 2;
-        grid-row: 1;
-        align-self: center;
-
-        [data-item] {
-          display: inline-flex;
-          flex: 0 0 auto;
-          align-items: center;
-          gap: 8px;
-          border: 1px solid var(--color-border-weak);
-          background: var(--color-background);
-          padding: 6px 10px;
-          border-radius: 999px;
-          max-width: 100%;
-        }
-
-        [data-dot] {
-          width: 10px;
-          height: 10px;
-          border-radius: 999px;
-          display: inline-block;
-          border: 1px solid var(--color-text-strong);
-          background: var(--color-background);
-          flex: 0 0 auto;
-        }
-
-        [data-dot][data-kind="go"] {
-          background: var(--color-background-interactive);
-        }
-
-        [data-dot][data-kind="go"][data-model="glm"] {
-          background: var(--color-go-1);
-        }
-
-        [data-dot][data-kind="go"][data-model="kimi"] {
-          background: var(--color-go-2);
-        }
-
-        [data-dot][data-kind="go"][data-model="minimax"] {
-          background: var(--color-go-3);
-        }
-
-        [data-name] {
-          color: var(--color-text);
-          white-space: nowrap;
-        }
-
-        [data-value] {
-          color: var(--color-text-strong);
-          font-weight: 600;
-          white-space: nowrap;
-        }
       }
 
       [data-slot="caption-note"] {
@@ -671,35 +730,8 @@ body {
       }
 
       @media (max-width: 56.25rem) {
-        [data-slot="caption-left"] {
-          grid-template-columns: var(--start, 16.9%) minmax(0, 1fr);
-          grid-template-rows: auto auto;
-          align-items: start;
-        }
-
-        [data-slot="legend"] {
-          grid-column: 2;
-          grid-row: 1;
-        }
-
         [data-slot="caption-meta"] {
-          display: flex;
-          gap: 24px;
-          align-items: baseline;
-          grid-column: 2;
-          grid-row: 2;
-          margin-top: 12px;
-        }
-
-        [data-slot="caption-label"] {
-          grid-column: auto;
-          grid-row: auto;
-        }
-
-        [data-slot="caption-link"] {
-          grid-column: auto;
-          grid-row: auto;
-          align-self: baseline;
+          gap: 14px;
         }
       }
     }

+ 97 - 49
packages/console/app/src/routes/go/index.tsx

@@ -10,6 +10,7 @@ import { Faq } from "~/component/faq"
 import { Legal } from "~/component/legal"
 import { Footer } from "~/component/footer"
 import { Header } from "~/component/header"
+import { config } from "~/config"
 import { getLastSeenWorkspaceID } from "../workspace/common"
 import { IconMiniMax, IconZai } from "~/component/icon"
 import { useI18n } from "~/context/i18n"
@@ -49,24 +50,50 @@ function LimitsGraph(props: { href: string }) {
     { id: "kimi", name: "Kimi K2.5", req: 1850, d: "240ms" },
     { id: "minimax", name: "MiniMax M2.5", req: 20000, d: "360ms" },
   ]
-  const ratio = (n: number) => n / free
 
   const w = 720
   const h = 220
-  const left = 88
-  const right = 24
-  const top = 22
-  const bottom = 46
+  const left = 40
+  const right = 60
+  const top = 18
+  const bottom = 44
   const plot = w - left - right
+
+  const ratio = (n: number) => n / free
   const rmax = Math.max(1, ...models.map((m) => ratio(m.req)))
   const log = (n: number) => Math.log10(Math.max(n, 1))
-  const x = (r: number) => left + (log(r) / log(rmax)) * plot
+  const base = 24
+  const p = 2.2
+  const x = (r: number) => left + base + Math.pow(log(r) / log(rmax), p) * (plot - base)
   const start = (x(1) / w) * 100
 
-  const yFree = 74
-  const yGo = 134
   const ticks = [1, 2, 5, 10, 25, 50, 100].filter((t) => t <= rmax)
-  const y = (n: number) => `${(n / h) * 100}%`
+  const labels = (() => {
+    const set = new Set<number>()
+    let last = -Infinity
+    for (const t of ticks) {
+      if (t === 1) {
+        set.add(t)
+        last = x(t)
+        continue
+      }
+      const pos = x(t)
+      if (pos - last < 44) continue
+      set.add(t)
+      last = pos
+    }
+    return set
+  })()
+  const bh = 8
+  const gap = 16
+  const step = bh + gap
+  const sep = bh + 40
+  const fy = top + 22
+  const gy = (i: number) => fy + sep + step * i
+  const my = models.length < 2 ? gy(0) : (gy(0) + gy(models.length - 1)) / 2
+  const px = (n: number) => `${(n / w) * 100}%`
+  const py = (n: number) => `${(n / h) * 100}%`
+  const lx = px(left - 16)
 
   return (
     <figure
@@ -77,52 +104,81 @@ function LimitsGraph(props: { href: string }) {
       style={{ "--start": `${start}%` } as any}
     >
       <div data-slot="plot">
-        <svg viewBox={`0 0 ${w} ${h}`} role="img" aria-hidden="true">
+        <svg
+          viewBox={`0 0 ${w} ${h}`}
+          preserveAspectRatio="none"
+          role="img"
+          aria-hidden="true"
+          style={{ height: `${h}px` }}
+        >
           <g data-slot="grid">
             <For each={ticks}>
               {(t) => (
                 <g>
                   <line x1={x(t)} y1={top} x2={x(t)} y2={h - bottom} data-grid />
-                  <text x={x(t)} y={h - 18} text-anchor="middle" data-tick>
-                    {i18n.t("go.graph.tick", { n: t })}
-                  </text>
+                  {labels.has(t) ? (
+                    <text x={x(t)} y={h - 18} text-anchor="middle" data-tick>
+                      {i18n.t("go.graph.tick", { n: t })}
+                    </text>
+                  ) : null}
                 </g>
               )}
             </For>
           </g>
 
-          <g data-slot="free" style={{ "--d": "0ms" } as any}>
-            <circle cx={x(1)} cy={yFree} r={5.5} data-point data-kind="free" />
-          </g>
+          <line x1={left} y1={top} x2={left} y2={h - bottom} data-stub />
+
+          <g data-slot="bars">
+            <g style={{ "--d": "0ms" } as any}>
+              <rect x={left} y={fy - bh / 2} width={Math.max(0, x(1) - left)} height={bh} data-bar data-kind="free" />
+            </g>
 
-          <g data-slot="go">
-            <line x1={x(1)} y1={yGo} x2={x(ratio(models[0]!.req))} y2={yGo} data-range data-animate="line" />
-            <line
-              x1={x(ratio(models[0]!.req))}
-              y1={yGo}
-              x2={x(ratio(models[2]!.req))}
-              y2={yGo}
-              data-range
-              data-animate="line"
-            />
             <For each={models}>
-              {(m) => (
+              {(m, i) => (
                 <g style={{ "--d": m.d } as any}>
-                  <circle cx={x(ratio(m.req))} cy={yGo} r={5.5} data-point data-kind="go" data-model={m.id} />
+                  <rect
+                    x={left}
+                    y={gy(i()) - bh / 2}
+                    width={Math.max(0, x(ratio(m.req)) - left)}
+                    height={bh}
+                    data-bar
+                    data-kind="go"
+                    data-model={m.id}
+                  />
                 </g>
               )}
             </For>
           </g>
         </svg>
 
-        <div data-slot="plot-labels">
-          <span data-row-label style={{ "--y": y(yFree) } as any}>
+        <div data-slot="ylabels" aria-hidden="true">
+          <span data-ylabel style={{ "--x": lx, "--y": py(fy) } as any}>
             {i18n.t("go.graph.free")}
           </span>
-          <span data-row-label style={{ "--y": y(yGo) } as any}>
+          <span data-ylabel style={{ "--x": lx, "--y": py(my) } as any}>
             {i18n.t("go.graph.go")}
           </span>
         </div>
+
+        <div data-slot="pills" aria-hidden="true">
+          <span data-item data-kind="free" style={{ "--x": px(x(1)), "--y": py(fy), "--d": "0ms" } as any}>
+            <span data-name>{i18n.t("go.graph.free")}</span>
+            <span data-value>{free.toLocaleString()}</span>
+          </span>
+          <For each={models}>
+            {(m, i) => (
+              <span
+                data-item
+                data-kind="go"
+                data-model={m.id}
+                style={{ "--x": px(x(ratio(m.req))), "--y": py(gy(i())), "--d": m.d } as any}
+              >
+                <span data-name>{m.name}</span>
+                <span data-value>{m.req.toLocaleString()}</span>
+              </span>
+            )}
+          </For>
+        </div>
       </div>
 
       <figcaption>
@@ -134,22 +190,6 @@ function LimitsGraph(props: { href: string }) {
                 {i18n.t("go.graph.usageLimits")}
               </a>
             </div>
-            <div data-slot="legend">
-              <span data-item>
-                <i data-dot data-kind="free" />
-                <span data-name>{i18n.t("go.graph.free")}</span>
-                <span data-value>{free.toLocaleString()}</span>
-              </span>
-              <For each={models}>
-                {(m) => (
-                  <span data-item>
-                    <i data-dot data-kind="go" data-model={m.id} />
-                    <span data-name>{m.name}</span>
-                    <span data-value>{m.req.toLocaleString()}</span>
-                  </span>
-                )}
-              </For>
-            </div>
           </div>
         </div>
       </figcaption>
@@ -165,9 +205,17 @@ export default function Home() {
     <main data-page="go">
       {/*<HttpHeader name="Cache-Control" value="public, max-age=1, s-maxage=3600, stale-while-revalidate=86400" />*/}
       <Title>{i18n.t("go.title")}</Title>
+      <Meta name="description" content={i18n.t("go.meta.description")} />
       <LocaleLinks path="/go" />
-      <Meta property="og:image" content="/social-share-zen.png" />
-      <Meta name="twitter:image" content="/social-share-zen.png" />
+      <Meta property="og:type" content="website" />
+      <Meta property="og:url" content={`${config.baseUrl}${language.route("/go")}`} />
+      <Meta property="og:title" content={i18n.t("go.title")} />
+      <Meta property="og:description" content={i18n.t("go.meta.description")} />
+      <Meta property="og:image" content="/social-share-black.png" />
+      <Meta name="twitter:card" content="summary_large_image" />
+      <Meta name="twitter:title" content={i18n.t("go.title")} />
+      <Meta name="twitter:description" content={i18n.t("go.meta.description")} />
+      <Meta name="twitter:image" content="/social-share-black.png" />
       <Meta name="opencode:auth" content={loggedin() ? "true" : "false"} />
 
       <div data-component="container">