diff options
| author | David Hill <[email protected]> | 2026-03-04 22:07:49 +0000 |
|---|---|---|
| committer | David Hill <[email protected]> | 2026-03-04 22:07:49 +0000 |
| commit | 0b825ca383f12fa6649d48165fb3f5e27cc4b049 (patch) | |
| tree | a5169779591e78529e6c58e00fd10367e8ad66f5 | |
| parent | 40fc406424c27a00f0841af46f6ec96572c973f7 (diff) | |
| download | opencode-0b825ca383f12fa6649d48165fb3f5e27cc4b049.tar.gz opencode-0b825ca383f12fa6649d48165fb3f5e27cc4b049.zip | |
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.
| -rw-r--r-- | packages/console/app/src/i18n/en.ts | 2 | ||||
| -rw-r--r-- | packages/console/app/src/routes/go/index.css | 254 | ||||
| -rw-r--r-- | packages/console/app/src/routes/go/index.tsx | 146 |
3 files changed, 242 insertions, 160 deletions
diff --git a/packages/console/app/src/i18n/en.ts b/packages/console/app/src/i18n/en.ts index 3f39fcd9d..9bf8ad43d 100644 --- a/packages/console/app/src/i18n/en.ts +++ b/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.", diff --git a/packages/console/app/src/routes/go/index.css b/packages/console/app/src/routes/go/index.css index 34be47f0d..99c61a70b 100644 --- a/packages/console/app/src/routes/go/index.css +++ b/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; } } } diff --git a/packages/console/app/src/routes/go/index.tsx b/packages/console/app/src/routes/go/index.tsx index 9e4f0eefd..6ae5e476c 100644 --- a/packages/console/app/src/routes/go/index.tsx +++ b/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"> |
