+ )
+}
+
+/** A provider's mark in the text colour; named only where no text beside it says the name. */
+function Mark(props: { logo: string; name?: string }) {
+ return (
+
+ )
+}
diff --git a/services/site/src/component/site/carousel-arrows.css b/services/site/src/component/site/carousel-arrows.css
new file mode 100644
index 00000000000..64f95c693a5
--- /dev/null
+++ b/services/site/src/component/site/carousel-arrows.css
@@ -0,0 +1,39 @@
+/* A carousel's previous and next buttons (carousel-arrows.tsx): 24px targets side by side, the
+ chevrons in the muted text colour, the ink on hover or press, and fainter at an end. */
+[data-site] [data-component="carousel-arrows"] {
+ display: flex;
+
+ button {
+ display: grid;
+ place-items: center;
+ width: 24px;
+ height: 24px;
+ padding: 0;
+ color: var(--text-weak);
+ transition: color 150ms ease;
+
+ &:focus-visible {
+ outline-offset: -1px;
+ }
+
+ &[aria-disabled="true"] {
+ color: var(--text-weaker);
+ cursor: default;
+ }
+
+ @media (hover: hover) and (pointer: fine) {
+ &:not([aria-disabled="true"]):hover {
+ color: var(--text-strong);
+ }
+ }
+
+ &:not([aria-disabled="true"]):active {
+ color: var(--text-strong);
+ }
+ }
+}
+
+/* The chevrons point the way the page reads. */
+[dir="rtl"] [data-site] [data-component="carousel-arrows"] svg {
+ scale: -1 1;
+}
diff --git a/services/site/src/component/site/carousel-arrows.tsx b/services/site/src/component/site/carousel-arrows.tsx
new file mode 100644
index 00000000000..86d184cffb3
--- /dev/null
+++ b/services/site/src/component/site/carousel-arrows.tsx
@@ -0,0 +1,46 @@
+import "./carousel-arrows.css"
+
+/**
+ * Previous and next buttons for a carousel (the changelog's media, the product cards): 24px targets
+ * with 12px chevrons in the muted text colour. An end the carousel has reached is aria-disabled, so
+ * the button keeps focus there; the chevrons point the way the page reads.
+ */
+export function CarouselArrows(props: {
+ previous: string
+ next: string
+ onPrevious: () => void
+ onNext: () => void
+ atStart?: boolean
+ atEnd?: boolean
+ controls?: string
+ "data-slot"?: string
+}) {
+ return (
+
+
+
+
+ )
+}
diff --git a/services/site/src/component/site/changelog-entry.css b/services/site/src/component/site/changelog-entry.css
index 0016f26cf44..5575ab6f16b 100644
--- a/services/site/src/component/site/changelog-entry.css
+++ b/services/site/src/component/site/changelog-entry.css
@@ -275,20 +275,12 @@
[data-slot="bars"] button:hover > [data-slot="bar"] {
background: var(--link);
}
-
- [data-slot="arrows"] button:hover {
- color: var(--text-strong);
- }
}
[data-slot="bars"] button:active > [data-slot="bar"] {
background: var(--link);
}
- [data-slot="arrows"] button:active {
- color: var(--text-strong);
- }
-
/* bars + count: "1/3" at the right end of the row. */
[data-slot="count"] {
margin-inline-start: auto;
@@ -297,7 +289,8 @@
color: var(--text-weak);
}
- /* bars + arrows: previous and next at the right end, their icons lined up with the media edge. */
+ /* bars + arrows: previous and next (carousel-arrows.tsx) at the right end, their icons lined up
+ with the media edge. */
[data-slot="arrows"] {
display: flex;
margin-inline-start: auto;
diff --git a/services/site/src/component/site/changelog-entry.tsx b/services/site/src/component/site/changelog-entry.tsx
index 64a00457d65..715fbb8f0f0 100644
--- a/services/site/src/component/site/changelog-entry.tsx
+++ b/services/site/src/component/site/changelog-entry.tsx
@@ -16,6 +16,7 @@ import {
type ChangelogNote,
type ChangelogView,
} from "~/lib/changelog-notes"
+import { CarouselArrows } from "./carousel-arrows"
import { Chevron, ExternalArrow, NewTab } from "./icons"
import { Inline, ReleaseItem } from "./release"
import { prefersReducedMotion } from "./products"
@@ -572,18 +573,13 @@ function EntryCarousel(props: { slides: Slide[]; title: string }) {
-
-
-
-
+ go(current() - 1)}
+ onNext={() => go(current() + 1)}
+ />
diff --git a/services/site/src/component/site/products-arrows.css b/services/site/src/component/site/products-arrows.css
new file mode 100644
index 00000000000..a4a05fb8825
--- /dev/null
+++ b/services/site/src/component/site/products-arrows.css
@@ -0,0 +1,19 @@
+/* The products carousel's previous and next (products-arrows.tsx): under the cards at the content's
+ end edge, the chevron lined up with it, in the space before the next section, so they take no room
+ and move nothing when they appear. Fine pointers only; touch screens swipe. */
+[data-site] [data-component="products"] {
+ position: relative;
+
+ > [data-slot="carousel-nav"] {
+ display: none;
+ position: absolute;
+ top: calc(100% + 1rem);
+ inset-inline-end: calc(var(--edge) - 6px);
+ }
+
+ @media (pointer: fine) {
+ > [data-slot="carousel-nav"] {
+ display: flex;
+ }
+ }
+}
diff --git a/services/site/src/component/site/products-arrows.tsx b/services/site/src/component/site/products-arrows.tsx
new file mode 100644
index 00000000000..ea5914c2bfe
--- /dev/null
+++ b/services/site/src/component/site/products-arrows.tsx
@@ -0,0 +1,62 @@
+import "./products-arrows.css"
+import { createSignal, onCleanup, onMount, Show } from "solid-js"
+import { useI18n } from "~/context/i18n"
+import { CarouselArrows } from "./carousel-arrows"
+import { prefersReducedMotion } from "./products"
+
+/**
+ * The products carousel's previous and next, for a mouse, whose wheel can't scroll the cards
+ * sideways. ProductCarousel loads this only on a fine pointer once the cards overflow, so touch
+ * screens and screens wide enough for every card never download it. Shown while the cards overflow,
+ * each end disabled once the cards reach it.
+ */
+export default function ProductsArrows(props: { track: HTMLElement }) {
+ const i18n = useI18n()
+ const [scrolled, setScrolled] = createSignal<{ start: boolean; end: boolean }>()
+ const track = props.track
+ track.id ||= "products-track"
+ const measure = () => {
+ const max = track.scrollWidth - track.clientWidth
+ const at = Math.abs(track.scrollLeft)
+ setScrolled(max > 1 ? { start: at < 2, end: at > max - 2 } : undefined)
+ }
+ // By as many whole cards as fit (at least one); the snap lines them up.
+ const page = (direction: 1 | -1) => {
+ const card = track.firstElementChild
+ if (!(card instanceof HTMLElement)) return
+ const style = getComputedStyle(track)
+ const step = card.offsetWidth + (parseFloat(style.columnGap) || 0)
+ const cards = Math.max(1, Math.floor(track.clientWidth / step))
+ track.scrollBy({
+ left: direction * (style.direction === "rtl" ? -1 : 1) * step * cards,
+ behavior: prefersReducedMotion() ? "instant" : "smooth",
+ })
+ }
+ onMount(() => {
+ measure()
+ const resize = new ResizeObserver(measure)
+ resize.observe(track)
+ track.addEventListener("scroll", measure, { passive: true })
+ onCleanup(() => {
+ resize.disconnect()
+ track.removeEventListener("scroll", measure)
+ })
+ })
+ return (
+
+ {(edges) => (
+
+ page(-1)}
+ onNext={() => page(1)}
+ />
+
+ )}
+
+ )
+}
diff --git a/services/site/src/component/site/products.tsx b/services/site/src/component/site/products.tsx
index cd05e2bb165..65df5ee407d 100644
--- a/services/site/src/component/site/products.tsx
+++ b/services/site/src/component/site/products.tsx
@@ -6,10 +6,12 @@ import {
createUniqueId,
For,
Match,
+ lazy,
on,
onCleanup,
onMount,
Show,
+ Suspense,
Switch,
type JSX,
} from "solid-js"
@@ -59,6 +61,9 @@ import { inlineKey } from "~/lib/direction"
export { installs, products, sdkSnippet, type ProductSlug } from "./product-data"
+// Loaded only for a fine pointer once the carousel's cards overflow (ProductCarousel).
+const ProductsArrows = lazy(() => import("./products-arrows"))
+
/**
* Loads a video again once it is on the page. A client-rendered video gets its src while it still
* belongs to Solid's template document, and while the router holds a new page off screen for its
@@ -245,8 +250,22 @@ export function ProductCarousel(props: { exclude?: ProductSlug; header?: JSX.Ele
const productCopy = useArea("products", productsSource)
const navigate = useNavigate()
let track: HTMLDivElement | undefined
+ // A mouse wheel can't scroll the cards sideways, so a fine pointer gets arrows, loaded once the
+ // cards overflow (products-arrows.tsx). Touch screens swipe and never load them.
+ const [arrows, setArrows] = createSignal(false)
- onMount(() => trackScroll("/", track))
+ onMount(() => {
+ trackScroll("/", track)
+ const check = () => {
+ if (!track || track.scrollWidth - track.clientWidth < 2 || !matchMedia("(pointer: fine)").matches) return
+ setArrows(true)
+ window.removeEventListener("resize", check)
+ }
+ check()
+ if (arrows()) return
+ window.addEventListener("resize", check)
+ onCleanup(() => window.removeEventListener("resize", check))
+ })
// The visitor's order (Mobile first on a phone or tablet), fixed at the first render.
const order = deviceOrder(products)
@@ -375,6 +394,13 @@ export function ProductCarousel(props: { exclude?: ProductSlug; header?: JSX.Ele
+
+ {(element) => (
+
+
+
+ )}
+
)
}
diff --git a/services/site/src/component/site/reveal.ts b/services/site/src/component/site/reveal.ts
index a3ea0bfc0d4..9cd06d9a8d9 100644
--- a/services/site/src/component/site/reveal.ts
+++ b/services/site/src/component/site/reveal.ts
@@ -15,7 +15,7 @@ import type { logoAnimations, SiteSettings } from "./settings"
/**
* The entrance wash for a grid: the first time `element` scrolls into view, `start` becomes that
* moment (performance.now()), and each item arrives `washStep` staggers later. Armed on the client
- * only, never with reduced motion, and only while `enabled`. `columns` follows the grid's tracks.
+ * only, never with reduced motion, and only while `enabled`. `columns` follows the grid's tracks (or --wash-columns).
* Give the container `data-entrance` and `data-mounted={mounted()}` so it waits from first paint.
*/
export function createWash(
@@ -44,8 +44,18 @@ export function createWash(
setMounted(true)
const target = element()
if (!target) return
- const measure = () =>
- setColumns(Math.max(1, getComputedStyle(target).gridTemplateColumns.split(" ").filter(Boolean).length))
+ // A grid whose cells span several tracks (the logo tiles, index.css) gives its cells across in
+ // --wash-columns.
+ const measure = () => {
+ const style = getComputedStyle(target)
+ setColumns(
+ Math.max(
+ 1,
+ Number(style.getPropertyValue("--wash-columns")) ||
+ style.gridTemplateColumns.split(" ").filter(Boolean).length,
+ ),
+ )
+ }
measure()
const resize = new ResizeObserver(measure)
resize.observe(target)
diff --git a/services/site/src/component/site/settings.ts b/services/site/src/component/site/settings.ts
index 65ec56795c4..12b60366540 100644
--- a/services/site/src/component/site/settings.ts
+++ b/services/site/src/component/site/settings.ts
@@ -495,8 +495,10 @@ export const bannerLabels = ["badge", "plain"] as const
// a sentence, a dense grid with the count, or a slow marquee.
export const providerLogoStyles = ["none", "row", "inline", "grid", "marquee"] as const
// "Bring your own subs": the providers' grid then the plans, your subscriptions beside the plans,
-// or tabs between the two.
-export const subsStyles = ["grid", "split", "tabs"] as const
+// or tabs between the two. A, B and C (byos-options.tsx) explore showing that OpenCode works with
+// nearly every subscription, provider and model: a ruled row per way in, a wall of every provider's
+// mark, or the names set as one run of text.
+export const subsStyles = ["grid", "split", "tabs", "A rows", "B wall", "C names"] as const
// "Get started": a numbered path, a price ladder, one large sentence, a terminal transcript, or two
// cells in the tools grid's borders.
export const getStartedStyles = ["steps", "ladder", "sentence", "terminal", "cards"] as const
diff --git a/services/site/src/i18n/ar.ts b/services/site/src/i18n/ar.ts
index cd06011de4f..5bd0d30ca62 100644
--- a/services/site/src/i18n/ar.ts
+++ b/services/site/src/i18n/ar.ts
@@ -282,6 +282,8 @@ export const dict = {
"site.newTab": "{{label}} (يفتح في علامة تبويب جديدة)",
"site.products.label": "المنتجات",
"site.products.gridLabel": "منتجات OpenCode",
+ "site.products.previous": "السابق",
+ "site.products.next": "التالي",
"site.action.copy": "نسخ",
"site.action.copied": "نُسخ",
"site.action.desktopDownloads": "تنزيلات Desktop",
diff --git a/services/site/src/i18n/br.ts b/services/site/src/i18n/br.ts
index ce074d8809c..6c371942293 100644
--- a/services/site/src/i18n/br.ts
+++ b/services/site/src/i18n/br.ts
@@ -284,6 +284,8 @@ export const dict = {
"site.newTab": "{{label}} (abre em uma nova aba)",
"site.products.label": "Produtos",
"site.products.gridLabel": "Produtos OpenCode",
+ "site.products.previous": "Anterior",
+ "site.products.next": "Próximo",
"site.action.forOs": "Baixar[ para {{os}}](os)",
"site.action.copy": "Copiar",
"site.action.copied": "Copiado",
diff --git a/services/site/src/i18n/da.ts b/services/site/src/i18n/da.ts
index 0e035c1230c..b0e94a074f2 100644
--- a/services/site/src/i18n/da.ts
+++ b/services/site/src/i18n/da.ts
@@ -285,6 +285,8 @@ export const dict = {
"site.newTab": "{{label}} (åbner i en ny fane)",
"site.products.label": "Produkter",
"site.products.gridLabel": "OpenCode-produkter",
+ "site.products.previous": "Forrige",
+ "site.products.next": "Næste",
"site.action.forOs": "Download[ til {{os}}](os)",
"site.action.copy": "Kopier",
"site.action.copied": "Kopieret",
diff --git a/services/site/src/i18n/de.ts b/services/site/src/i18n/de.ts
index 00b3320056f..ac5e77a187b 100644
--- a/services/site/src/i18n/de.ts
+++ b/services/site/src/i18n/de.ts
@@ -284,6 +284,8 @@ export const dict = {
"site.newTab": "{{label}} (öffnet in neuem Tab)",
"site.products.label": "Produkte",
"site.products.gridLabel": "OpenCode-Produkte",
+ "site.products.previous": "Zurück",
+ "site.products.next": "Weiter",
"site.action.forOs": "Download[ für {{os}}](os)",
"site.action.copy": "Kopieren",
"site.action.copied": "Kopiert",
diff --git a/services/site/src/i18n/en.ts b/services/site/src/i18n/en.ts
index 6b21dece9f5..b55305ca5c3 100644
--- a/services/site/src/i18n/en.ts
+++ b/services/site/src/i18n/en.ts
@@ -284,6 +284,8 @@ export const dict = {
"site.newTab": "{{label}} (opens in a new tab)",
"site.products.label": "Products",
"site.products.gridLabel": "OpenCode products",
+ "site.products.previous": "Previous",
+ "site.products.next": "Next",
"site.action.forOs": "Download[ for {{os}}](os)",
"site.action.copyCommand": "Copy[ {{method}} command](sr)",
"site.action.copiedCommand": "Copied[ {{method}} command](sr)",
diff --git a/services/site/src/i18n/es.ts b/services/site/src/i18n/es.ts
index b363f85b7b9..2e4898a6210 100644
--- a/services/site/src/i18n/es.ts
+++ b/services/site/src/i18n/es.ts
@@ -285,6 +285,8 @@ export const dict = {
"site.newTab": "{{label}} (se abre en una pestaña nueva)",
"site.products.label": "Productos",
"site.products.gridLabel": "Productos de OpenCode",
+ "site.products.previous": "Anterior",
+ "site.products.next": "Siguiente",
"site.action.forOs": "Descargar[ para {{os}}](os)",
"site.action.copy": "Copiar",
"site.action.copied": "Copiado",
diff --git a/services/site/src/i18n/fr.ts b/services/site/src/i18n/fr.ts
index cc71ba5a583..2024d4e2b74 100644
--- a/services/site/src/i18n/fr.ts
+++ b/services/site/src/i18n/fr.ts
@@ -284,6 +284,8 @@ export const dict = {
"site.newTab": "{{label}} (s'ouvre dans un nouvel onglet)",
"site.products.label": "Produits",
"site.products.gridLabel": "Produits OpenCode",
+ "site.products.previous": "Précédent",
+ "site.products.next": "Suivant",
"site.action.forOs": "Télécharger[ pour {{os}}](os)",
"site.action.copy": "Copier",
"site.action.copied": "Copié",
diff --git a/services/site/src/i18n/it.ts b/services/site/src/i18n/it.ts
index 1330d249f78..520a8f2b204 100644
--- a/services/site/src/i18n/it.ts
+++ b/services/site/src/i18n/it.ts
@@ -284,6 +284,8 @@ export const dict = {
"site.newTab": "{{label}} (si apre in una nuova scheda)",
"site.products.label": "Prodotti",
"site.products.gridLabel": "Prodotti OpenCode",
+ "site.products.previous": "Precedente",
+ "site.products.next": "Successivo",
"site.action.forOs": "Scarica[ per {{os}}](os)",
"site.action.copy": "Copia",
"site.action.copied": "Copiato",
diff --git a/services/site/src/i18n/ja.ts b/services/site/src/i18n/ja.ts
index d4df27694de..e1f197ce1b0 100644
--- a/services/site/src/i18n/ja.ts
+++ b/services/site/src/i18n/ja.ts
@@ -284,6 +284,8 @@ export const dict = {
"site.newTab": "{{label}}(新しいタブで開きます)",
"site.products.label": "製品",
"site.products.gridLabel": "OpenCodeの製品",
+ "site.products.previous": "前へ",
+ "site.products.next": "次へ",
"site.action.forOs": "ダウンロード[({{os}}版)](os)",
"site.action.copy": "コピー",
"site.action.copied": "コピー済み",
diff --git a/services/site/src/i18n/ko.ts b/services/site/src/i18n/ko.ts
index 8d2f3a92450..9739289db7f 100644
--- a/services/site/src/i18n/ko.ts
+++ b/services/site/src/i18n/ko.ts
@@ -285,6 +285,8 @@ export const dict = {
"site.newTab": "{{label}} (새 탭에서 열림)",
"site.products.label": "제품",
"site.products.gridLabel": "OpenCode 제품",
+ "site.products.previous": "이전",
+ "site.products.next": "다음",
"site.action.forOs": "[{{os}}용 ](os)다운로드",
"site.action.copy": "복사",
"site.action.copied": "복사됨",
diff --git a/services/site/src/i18n/no.ts b/services/site/src/i18n/no.ts
index 77befc68190..1d02f9e2576 100644
--- a/services/site/src/i18n/no.ts
+++ b/services/site/src/i18n/no.ts
@@ -285,6 +285,8 @@ export const dict = {
"site.newTab": "{{label}} (åpnes i ny fane)",
"site.products.label": "Produkter",
"site.products.gridLabel": "OpenCode-produkter",
+ "site.products.previous": "Forrige",
+ "site.products.next": "Neste",
"site.action.forOs": "Last ned[ for {{os}}](os)",
"site.action.copy": "Kopier",
"site.action.copied": "Kopiert",
diff --git a/services/site/src/i18n/pl.ts b/services/site/src/i18n/pl.ts
index c74751d1605..47cf6a13230 100644
--- a/services/site/src/i18n/pl.ts
+++ b/services/site/src/i18n/pl.ts
@@ -285,6 +285,8 @@ export const dict = {
"site.newTab": "{{label}} (otwiera się w nowej karcie)",
"site.products.label": "Produkty",
"site.products.gridLabel": "Produkty OpenCode",
+ "site.products.previous": "Wstecz",
+ "site.products.next": "Dalej",
"site.action.forOs": "Pobierz[ na {{os}}](os)",
"site.action.copy": "Kopiuj",
"site.action.copied": "Skopiowano",
diff --git a/services/site/src/i18n/ru.ts b/services/site/src/i18n/ru.ts
index cf566120318..3764200204a 100644
--- a/services/site/src/i18n/ru.ts
+++ b/services/site/src/i18n/ru.ts
@@ -281,6 +281,8 @@ export const dict = {
"site.newTab": "{{label}} (откроется в новой вкладке)",
"site.products.label": "Продукты",
"site.products.gridLabel": "Продукты OpenCode",
+ "site.products.previous": "Назад",
+ "site.products.next": "Вперёд",
"site.action.copy": "Копировать",
"site.action.copied": "Скопировано",
"site.action.desktopDownloads": "Загрузки Desktop",
diff --git a/services/site/src/i18n/th.ts b/services/site/src/i18n/th.ts
index 2e61c1dfe79..df4a1805673 100644
--- a/services/site/src/i18n/th.ts
+++ b/services/site/src/i18n/th.ts
@@ -285,6 +285,8 @@ export const dict = {
"site.newTab": "{{label}} (เปิดในแท็บใหม่)",
"site.products.label": "ผลิตภัณฑ์",
"site.products.gridLabel": "ผลิตภัณฑ์ OpenCode",
+ "site.products.previous": "ก่อนหน้า",
+ "site.products.next": "ถัดไป",
"site.action.forOs": "ดาวน์โหลด[สำหรับ {{os}}](os)",
"site.action.copy": "คัดลอก",
"site.action.copied": "คัดลอกแล้ว",
diff --git a/services/site/src/i18n/tr.ts b/services/site/src/i18n/tr.ts
index 93a75f787ec..271cf0d32ae 100644
--- a/services/site/src/i18n/tr.ts
+++ b/services/site/src/i18n/tr.ts
@@ -285,6 +285,8 @@ export const dict = {
"site.newTab": "{{label}} (yeni sekmede açılır)",
"site.products.label": "Ürünler",
"site.products.gridLabel": "OpenCode ürünleri",
+ "site.products.previous": "Önceki",
+ "site.products.next": "Sonraki",
"site.action.forOs": "[{{os}} için ](os)İndir",
"site.action.copy": "Kopyala",
"site.action.copied": "Kopyalandı",
diff --git a/services/site/src/i18n/uk.ts b/services/site/src/i18n/uk.ts
index 0c949d5257a..5b234decb31 100644
--- a/services/site/src/i18n/uk.ts
+++ b/services/site/src/i18n/uk.ts
@@ -280,6 +280,8 @@ export const dict = {
"site.newTab": "{{label}} (відкриється в новій вкладці)",
"site.products.label": "Продукти",
"site.products.gridLabel": "Продукти OpenCode",
+ "site.products.previous": "Назад",
+ "site.products.next": "Далі",
"site.action.copy": "Копіювати",
"site.action.copied": "Скопійовано",
"site.action.desktopDownloads": "Завантаження Desktop",
diff --git a/services/site/src/i18n/zh.ts b/services/site/src/i18n/zh.ts
index 0f913ee79dc..783529e3986 100644
--- a/services/site/src/i18n/zh.ts
+++ b/services/site/src/i18n/zh.ts
@@ -284,6 +284,8 @@ export const dict = {
"site.newTab": "{{label}}(在新标签页中打开)",
"site.products.label": "产品",
"site.products.gridLabel": "OpenCode 产品",
+ "site.products.previous": "上一个",
+ "site.products.next": "下一个",
"site.action.forOs": "下载[ {{os}} 版](os)",
"site.action.copy": "复制",
"site.action.copied": "已复制",
diff --git a/services/site/src/i18n/zht.ts b/services/site/src/i18n/zht.ts
index fe60535c32a..9e4c59a68a7 100644
--- a/services/site/src/i18n/zht.ts
+++ b/services/site/src/i18n/zht.ts
@@ -285,6 +285,8 @@ export const dict = {
"site.newTab": "{{label}}(在新分頁中開啟)",
"site.products.label": "產品",
"site.products.gridLabel": "OpenCode 產品",
+ "site.products.previous": "上一個",
+ "site.products.next": "下一個",
"site.action.forOs": "下載[ {{os}} 版](os)",
"site.action.copy": "複製",
"site.action.copied": "已複製",
diff --git a/services/site/src/lib/byos-options.ts b/services/site/src/lib/byos-options.ts
new file mode 100644
index 00000000000..f7cf49d05f5
--- /dev/null
+++ b/services/site/src/lib/byos-options.ts
@@ -0,0 +1,57 @@
+import { docsUrl } from "~/lib/docs"
+import { providerLogo } from "~/lib/providers"
+
+/**
+ * Settings › Subscriptions style A, B and C (component/site/byos-options.tsx): explorations of showing
+ * that OpenCode works with nearly every subscription, provider and model. English only while they
+ * are explorations; the chosen one's copy moves into lib/byos.ts (and its translations). A module of
+ * its own, so production, which folds the options out, never loads it. Checked on 2026-11-06 against
+ * this repo:
+ * - Log-ins (packages/core/src/plugin/provider): ChatGPT Plus/Pro (chatgpt.ts, "Sign in with
+ * ChatGPT"), GitHub Copilot (github-copilot.ts), SuperGrok (xai.ts, "SuperGrok Subscription") and
+ * GitLab Duo (gitlab.ts; GitLab Premium or Ultimate, experimental per the provider docs).
+ * - Plans that take the plan's API key, each a provider in the bundled models.dev catalog
+ * (packages/core/src/models-dev/snapshot.txt): zai-coding-plan, kimi-code-plan-global,
+ * minimax-coding-plan (named "MiniMax Token Plan") and ollama-cloud.
+ * - Local models: the Ollama, LM Studio and vLLM provider plugins (ollama.ts, lmstudio.ts, vllm.ts).
+ * - "200+ providers": the snapshot lists 230 (models.dev's live API 235), written as 200+ like the
+ * rest of the site.
+ * No Anthropic mark or name anywhere in these.
+ */
+export const byosOptions = {
+ leads: {
+ rows: "Use the subscriptions, API keys and local models you already have, or an OpenCode subscription.",
+ wall: "Log in with ChatGPT Plus or Pro, GitHub Copilot, SuperGrok or GitLab Duo, or connect any of 200+ providers with an API key. Or use an OpenCode subscription.",
+ names:
+ "Use the subscriptions and API keys you already have, or run models on your own machine. Or use an OpenCode subscription.",
+ },
+ rows: {
+ subscriptions: "Subscriptions",
+ providers: "Providers",
+ local: "Local",
+ opencode: "OpenCode",
+ providersMore: "200+ through models.dev",
+ },
+ all: "All 200+ providers",
+ subscriptions: [
+ { name: "ChatGPT Plus/Pro", id: "openai" },
+ { name: "GitHub Copilot", id: "github-copilot" },
+ { name: "SuperGrok", id: "xai" },
+ { name: "GitLab Duo", id: "gitlab" },
+ { name: "Z.ai Coding Plan", id: "zai" },
+ { name: "Kimi For Coding", id: "moonshotai" },
+ { name: "MiniMax Token Plan", id: "minimax" },
+ { name: "Ollama Cloud", id: "ollama" },
+ ].map((item) => ({ name: item.name, logo: providerLogo(item.id) })),
+ local: [
+ { name: "Ollama", logo: providerLogo("ollama") },
+ { name: "LM Studio", logo: providerLogo("lmstudio") },
+ { name: "vLLM" },
+ ],
+ // The same pages as Get started and the main nav link (lib/get-started.ts, links.ts).
+ opencode: [
+ { name: "Free models", href: docsUrl("/console/models/#free-models") },
+ { name: "OpenCode Go", href: "/console/go" },
+ { name: "OpenCode Models", href: "/console/models" },
+ ],
+} as const
diff --git a/services/site/src/routes/index.css b/services/site/src/routes/index.css
index 4d6934e1bae..f25b4cad84b 100644
--- a/services/site/src/routes/index.css
+++ b/services/site/src/routes/index.css
@@ -1831,16 +1831,38 @@
/* Logo-only tiles ("Bring your own subs"): the mark centred, twice the icon's size. The
models.dev marks share one square box with their own margins, so one size balances them; it
- steps down at narrow widths and stays on whole pixels. */
+ steps down at narrow widths and stays on whole pixels.
+ Four across (three on phones) on 12 tracks, so a short last row can share the full width
+ equally and no cell is left empty, whatever the number of logos. --wash-columns tells the
+ entrance wash (reveal.ts) the number of cells across. */
&[data-tiles="icon"] {
--icon-size: 40px;
+ --wash-columns: 4;
+ grid-template-columns: repeat(12, minmax(0, 1fr));
li {
+ grid-column: span 3;
justify-content: center;
height: 4rem;
padding-block: 0;
}
+ @media (width > 40rem) {
+ li:nth-child(4n + 1):nth-last-child(3),
+ li:nth-child(4n + 1):nth-last-child(3) ~ li {
+ grid-column: span 4;
+ }
+
+ li:nth-child(4n + 1):nth-last-child(2),
+ li:nth-child(4n + 1):nth-last-child(2) ~ li {
+ grid-column: span 6;
+ }
+
+ li:nth-child(4n + 1):last-child {
+ grid-column: span 12;
+ }
+ }
+
[data-slot="icon"] {
width: var(--icon-size);
height: var(--icon-size);
@@ -1850,10 +1872,23 @@
--icon-size: 36px;
}
- /* Phones: three across (the wordmark tiles keep two), so twelve logos make four full rows. */
+ /* Phones: three across (the wordmark tiles keep two). */
@media (max-width: 40rem) {
--icon-size: 32px;
- grid-template-columns: repeat(3, minmax(0, 1fr));
+ --wash-columns: 3;
+
+ li {
+ grid-column: span 4;
+ }
+
+ li:nth-child(3n + 1):nth-last-child(2),
+ li:nth-child(3n + 1):nth-last-child(2) ~ li {
+ grid-column: span 6;
+ }
+
+ li:nth-child(3n + 1):last-child {
+ grid-column: span 12;
+ }
}
}
diff --git a/services/site/src/routes/index.tsx b/services/site/src/routes/index.tsx
index 6beb062278c..8a434eb79eb 100644
--- a/services/site/src/routes/index.tsx
+++ b/services/site/src/routes/index.tsx
@@ -41,6 +41,7 @@ import { byos } from "~/lib/byos"
import { localize, useArea } from "~/i18n/content"
import { announcementsSource } from "~/i18n/content/areas/announcements"
import "~/component/site/byos.css"
+import { ByosOptionLeads, ByosOptions } from "~/component/site/byos-options"
import { getStarted } from "~/lib/get-started"
import "~/component/site/get-started.css"
import { installs } from "~/component/site/product-data"
@@ -1254,6 +1255,17 @@ function BringYourOwnSubs() {
)}
/>
+ {/* Spelled out so the production build folds it (script/fold-settings.ts). */}
+
+
+
{/* "grid": the providers' grid. */}
@@ -1308,6 +1320,18 @@ function BringYourOwnSubs() {
+
+ {/* A, B and C: showing that OpenCode works with nearly every subscription, provider and model. */}
+
+
+
)