summaryrefslogtreecommitdiffhomepage
path: root/packages/console
diff options
context:
space:
mode:
authorDavid Hill <[email protected]>2026-03-04 22:07:49 +0000
committerDavid Hill <[email protected]>2026-03-04 22:07:49 +0000
commit0b825ca383f12fa6649d48165fb3f5e27cc4b049 (patch)
treea5169779591e78529e6c58e00fd10367e8ad66f5 /packages/console
parent40fc406424c27a00f0841af46f6ec96572c973f7 (diff)
downloadopencode-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.
Diffstat (limited to 'packages/console')
-rw-r--r--packages/console/app/src/i18n/en.ts2
-rw-r--r--packages/console/app/src/routes/go/index.css254
-rw-r--r--packages/console/app/src/routes/go/index.tsx146
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">