summaryrefslogtreecommitdiffhomepage
path: root/packages/console/app/src
diff options
context:
space:
mode:
authorFrank <[email protected]>2026-01-12 17:10:50 -0500
committerFrank <[email protected]>2026-01-12 17:10:50 -0500
commite146083b73e0d8f2a33570e24672176d0811d663 (patch)
tree5fbaeb654847b8e8ac7b9c843074129c4075375d /packages/console/app/src
parentd7a1c268d9b75cb5aea9ad9686d7ac52a3053aa4 (diff)
downloadopencode-e146083b73e0d8f2a33570e24672176d0811d663.tar.gz
opencode-e146083b73e0d8f2a33570e24672176d0811d663.zip
wip: black
Diffstat (limited to 'packages/console/app/src')
-rw-r--r--packages/console/app/src/routes/black/index.css188
-rw-r--r--packages/console/app/src/routes/black/index.tsx118
2 files changed, 291 insertions, 15 deletions
diff --git a/packages/console/app/src/routes/black/index.css b/packages/console/app/src/routes/black/index.css
index eb0ec87d3..418598792 100644
--- a/packages/console/app/src/routes/black/index.css
+++ b/packages/console/app/src/routes/black/index.css
@@ -131,6 +131,188 @@
text-decoration: none;
}
+ [data-slot="pricing"] {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ width: 100%;
+ max-width: 540px;
+ padding: 0 20px;
+
+ @media (min-width: 768px) {
+ padding: 0;
+ }
+ }
+
+ [data-slot="pricing-card"] {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ padding: 20px;
+ border: 1px solid rgba(255, 255, 255, 0.17);
+ border-radius: 4px;
+ text-decoration: none;
+ transition: border-color 0.15s ease;
+ background: transparent;
+ cursor: pointer;
+ text-align: left;
+
+ &:hover {
+ border-color: rgba(255, 255, 255, 0.35);
+ }
+
+ [data-slot="icon"] {
+ color: rgba(255, 255, 255, 0.59);
+ }
+
+ [data-slot="price"] {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: baseline;
+ gap: 8px;
+ }
+
+ [data-slot="amount"] {
+ color: rgba(255, 255, 255, 0.92);
+ font-size: 24px;
+ font-weight: 500;
+ }
+
+ [data-slot="period"] {
+ color: rgba(255, 255, 255, 0.59);
+ font-size: 14px;
+ }
+
+ [data-slot="multiplier"] {
+ color: rgba(255, 255, 255, 0.39);
+ font-size: 14px;
+
+ &::before {
+ content: "·";
+ margin-right: 8px;
+ }
+ }
+ }
+
+ [data-slot="selected-plan"] {
+ display: flex;
+ flex-direction: column;
+ gap: 32px;
+ width: fit-content;
+ max-width: calc(100% - 40px);
+ margin: 0 auto;
+ }
+
+ [data-slot="selected-card"] {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ padding: 20px;
+ border: 1px solid rgba(255, 255, 255, 0.17);
+ border-radius: 4px;
+ width: fit-content;
+
+ [data-slot="icon"] {
+ color: rgba(255, 255, 255, 0.59);
+ }
+
+ [data-slot="price"] {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: baseline;
+ gap: 8px;
+ }
+
+ [data-slot="amount"] {
+ color: rgba(255, 255, 255, 0.92);
+ font-size: 24px;
+ font-weight: 500;
+ }
+
+ [data-slot="period"] {
+ color: rgba(255, 255, 255, 0.59);
+ font-size: 14px;
+ }
+
+ [data-slot="multiplier"] {
+ color: rgba(255, 255, 255, 0.39);
+ font-size: 14px;
+
+ &::before {
+ content: "·";
+ margin-right: 8px;
+ }
+ }
+
+ [data-slot="terms"] {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ text-align: left;
+
+ li {
+ color: rgba(255, 255, 255, 0.59);
+ font-size: 13px;
+ line-height: 1.5;
+ padding-left: 16px;
+ position: relative;
+ white-space: nowrap;
+
+ &::before {
+ content: "▪";
+ position: absolute;
+ left: 0;
+ color: rgba(255, 255, 255, 0.39);
+ }
+ }
+ }
+
+ [data-slot="actions"] {
+ display: flex;
+ gap: 16px;
+ margin-top: 8px;
+
+ button,
+ a {
+ flex: 1;
+ display: inline-flex;
+ height: 48px;
+ padding: 0 16px;
+ justify-content: center;
+ align-items: center;
+ border-radius: 4px;
+ font-family: var(--font-mono);
+ font-size: 16px;
+ font-weight: 400;
+ text-decoration: none;
+ cursor: pointer;
+ }
+
+ [data-slot="cancel"] {
+ background: transparent;
+ border: 1px solid rgba(255, 255, 255, 0.17);
+ color: rgba(255, 255, 255, 0.92);
+
+ &:hover {
+ border-color: rgba(255, 255, 255, 0.35);
+ }
+ }
+
+ [data-slot="continue"] {
+ background: rgba(255, 255, 255, 0.17);
+ border: 1px solid rgba(255, 255, 255, 0.17);
+ color: rgba(255, 255, 255, 0.59);
+
+ &:hover {
+ background: rgba(255, 255, 255, 0.25);
+ }
+ }
+ }
+ }
+
[data-slot="fine-print"] {
color: rgba(255, 255, 255, 0.39);
text-align: center;
@@ -138,6 +320,12 @@
font-style: normal;
font-weight: 400;
line-height: 160%; /* 20.8px */
+ font-style: italic;
+
+ a {
+ color: rgba(255, 255, 255, 0.39);
+ text-decoration: underline;
+ }
}
}
}
diff --git a/packages/console/app/src/routes/black/index.tsx b/packages/console/app/src/routes/black/index.tsx
index c83ccd251..f5a375adf 100644
--- a/packages/console/app/src/routes/black/index.tsx
+++ b/packages/console/app/src/routes/black/index.tsx
@@ -1,11 +1,54 @@
-import { A, createAsync } from "@solidjs/router"
+import { A, createAsync, useSearchParams } from "@solidjs/router"
import "./index.css"
import { Title } from "@solidjs/meta"
import { github } from "~/lib/github"
-import { createMemo, Match, Switch } from "solid-js"
+import { createMemo, createSignal, For, Match, Show, Switch } from "solid-js"
import { config } from "~/config"
+const plans = [
+ { id: "20", amount: 20, multiplier: null },
+ { id: "100", amount: 100, multiplier: "6x more usage than Black 20" },
+ { id: "200", amount: 200, multiplier: "21x more usage than Black 20" },
+] as const
+
+function PlanIcon(props: { plan: string }) {
+ return (
+ <Switch>
+ <Match when={props.plan === "20"}>
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <rect x="4" y="4" width="16" height="16" rx="2" stroke="currentColor" stroke-width="1.5" />
+ </svg>
+ </Match>
+ <Match when={props.plan === "100"}>
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <rect x="3" y="3" width="7" height="7" rx="1" stroke="currentColor" stroke-width="1.5" />
+ <rect x="14" y="3" width="7" height="7" rx="1" stroke="currentColor" stroke-width="1.5" />
+ <rect x="3" y="14" width="7" height="7" rx="1" stroke="currentColor" stroke-width="1.5" />
+ <rect x="14" y="14" width="7" height="7" rx="1" stroke="currentColor" stroke-width="1.5" />
+ </svg>
+ </Match>
+ <Match when={props.plan === "200"}>
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <rect x="2" y="2" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1" />
+ <rect x="10" y="2" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1" />
+ <rect x="18" y="2" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1" />
+ <rect x="2" y="10" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1" />
+ <rect x="10" y="10" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1" />
+ <rect x="18" y="10" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1" />
+ <rect x="2" y="18" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1" />
+ <rect x="10" y="18" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1" />
+ <rect x="18" y="18" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1" />
+ </svg>
+ </Match>
+ </Switch>
+ )
+}
+
export default function Black() {
+ const [params] = useSearchParams()
+ const [selected, setSelected] = createSignal<string | null>(params.plan as string | null)
+ const selectedPlan = createMemo(() => plans.find((p) => p.id === selected()))
+
const githubData = createAsync(() => github())
const starCount = createMemo(() =>
githubData()?.stars
@@ -16,9 +59,6 @@ export default function Black() {
: config.github.starsFormatted.compact,
)
- // TODO: Frank, toggle this based on availability
- const available = false
-
return (
<div data-page="black">
<Title>opencode</Title>
@@ -148,17 +188,65 @@ export default function Black() {
<p data-slot="subheading">Including Claude, GPT, Gemini, and more</p>
</div>
<Switch>
- <Match when={available}>
- <a href="/black/subscribe" data-slot="button">
- Subscribe $200/mo
- </a>
- <p data-slot="fine-print">Fair usage limits apply</p>
+ <Match when={!selected()}>
+ <div data-slot="pricing">
+ <For each={plans}>
+ {(plan) => (
+ <button type="button" onClick={() => setSelected(plan.id)} data-slot="pricing-card">
+ <div data-slot="icon">
+ <PlanIcon plan={plan.id} />
+ </div>
+ <p data-slot="price">
+ <span data-slot="amount">${plan.amount}</span> <span data-slot="period">per month</span>
+ <Show when={plan.multiplier}>
+ <span data-slot="multiplier">{plan.multiplier}</span>
+ </Show>
+ </p>
+ </button>
+ )}
+ </For>
+ </div>
+ <p data-slot="fine-print">
+ Prices shown don't include applicable tax · <A href="/legal/terms-of-service">Terms of Service</A>
+ </p>
</Match>
- <Match when={!available}>
- <p data-slot="back-soon">We’ll be back soon with more availability.</p>
- <a data-slot="follow-us" href="https://x.com/opencode" target="_blank">
- Follow @opencode
- </a>
+ <Match when={selectedPlan()}>
+ {(plan) => (
+ <div data-slot="selected-plan">
+ <div data-slot="selected-card">
+ <div data-slot="icon">
+ <PlanIcon plan={plan().id} />
+ </div>
+ <p data-slot="price">
+ <span data-slot="amount">${plan().amount}</span>{" "}
+ <span data-slot="period">per person billed monthly</span>
+ <Show when={plan().multiplier}>
+ <span data-slot="multiplier">{plan().multiplier}</span>
+ </Show>
+ </p>
+ <ul data-slot="terms">
+ <li>Your subscription will not start immediately</li>
+ <li>You will be added to the waitlist and activated soon</li>
+ <li>Your card will be only charged when your subscription is activated</li>
+ <li>Usage limits apply, heavily automated use may reach limits sooner</li>
+ <li>Subscriptions for individuals, contact Enterprise for teams</li>
+ <li>Limits may be adjusted and plans may be discontinued in the future</li>
+ <li>Cancel your subscription at anytime</li>
+ </ul>
+ <div data-slot="actions">
+ <button type="button" onClick={() => setSelected(null)} data-slot="cancel">
+ Cancel
+ </button>
+ <a href={`/black/subscribe?plan=${plan().id}`} data-slot="continue">
+ Continue
+ </a>
+ </div>
+ </div>
+ <p data-slot="fine-print">
+ Prices shown don't include applicable tax · <A href="/legal/terms-of-service">Terms of Service</A>
+ </p>
+ </div>
+ )}
</Match>
</Switch>
</section>