diff --git a/src/components/StoreBadge/index.tsx b/src/components/StoreBadge/index.tsx index e0099c4..544f185 100644 --- a/src/components/StoreBadge/index.tsx +++ b/src/components/StoreBadge/index.tsx @@ -286,25 +286,34 @@ export function DirectDownloadButton({ * labels its downloads, and the visitor this page is for wants to know that * clicking gets them the app, not what a .dmg is. */ +// What the block calls each architecture, where a platform offers more than +// one and the block has to say which build it is. +const ARCH_LABEL = { x64: 'x86_64', arm64: 'ARM64' } as const; + export function DirectDownloadBlock({ platform, + arch, size, variant = 'primary', }: { platform: DirectPlatform; + // Set only where the platform ships more than one build (Linux); the block + // then names the architecture in place of who the build covers. + arch?: 'x64' | 'arm64'; // Unknown until the release fetch returns; the block reads fine without it. size?: string; variant?: ButtonVariant; }): ReactNode { - const { mark, name, platform: releasePlatform, format, covers } = DIRECT_PLATFORM[platform]; - const path = useInstallerUrl()(releasePlatform); + const { mark, name, platform: releasePlatform, format, covers: platformCovers } = DIRECT_PLATFORM[platform]; + const path = useInstallerUrl()(releasePlatform, arch); + const covers = arch ? ARCH_LABEL[arch] : platformCovers; return ( diff --git a/src/components/StoreBadge/styles.module.css b/src/components/StoreBadge/styles.module.css index 700713d..50ebd95 100644 --- a/src/components/StoreBadge/styles.module.css +++ b/src/components/StoreBadge/styles.module.css @@ -107,9 +107,7 @@ } .directCta:hover, -.directCta:focus-visible, -.directBlock:hover, -.directBlock:focus-visible { +.directCta:focus-visible { --dl-plate: var(--sl-white); --dl-ink: var(--sl-ink); color: var(--dl-ink); @@ -118,8 +116,9 @@ } /* On the download page the outline is the card's own border colour, so the - button sits in its card as part of it; it turns solid only on hover. The - light value matches the light theme's override of the card border in + button sits in its card as part of it. It never fills: on hover the outline + turns the brand pink; the card itself stays still. The light + value matches the light theme's override of the card border in pages/index.module.css. */ .directBlock { --dl-edge: var(--sl-line); @@ -133,7 +132,9 @@ .directBlock:focus-visible, [data-theme='light'] .directBlock:hover, [data-theme='light'] .directBlock:focus-visible { - --dl-edge: var(--sl-white); + --dl-edge: var(--sl-accent); + text-decoration: none; + transform: translateY(-1px); } /* The home page hero's button is the one exception: the solid brand pink, for diff --git a/src/hooks/useLatestRelease.ts b/src/hooks/useLatestRelease.ts index 6ff89ff..2afe3b8 100644 --- a/src/hooks/useLatestRelease.ts +++ b/src/hooks/useLatestRelease.ts @@ -59,17 +59,22 @@ export default function useLatestRelease(): LatestRelease { : { status: 'unavailable', release: null }; } -/** First artifact matching a platform/kind pair, or null. */ +/** + * The artifact for a platform/kind pair, or null. With `arch`, only that + * build; without it, x64 leads where there is one, as the API's download + * redirect does. + */ export function findArtifact( release: Release | null, platform: ArtifactPlatform, kind: ArtifactKind, + arch?: ArtifactArch, ): ReleaseArtifact | null { if (!release) return null; - return ( - release.artifacts.find((a) => a.platform === platform && a.kind === kind) ?? - null + const matches = release.artifacts.filter( + (a) => a.platform === platform && a.kind === kind && (!arch || a.arch === arch), ); + return matches.find((a) => a.arch === 'x64') ?? matches[0] ?? null; } /** diff --git a/src/pages/download.tsx b/src/pages/download.tsx index 06c19c9..95b7cb7 100644 --- a/src/pages/download.tsx +++ b/src/pages/download.tsx @@ -1,4 +1,4 @@ -import { type ReactNode } from 'react'; +import { type ReactNode, useEffect, useState } from 'react'; import Head from '@docusaurus/Head'; import Header from '../components/Header'; import Footer from '../components/Footer'; @@ -7,6 +7,7 @@ import useLatestRelease, { formatSize, type ArtifactKind, type ArtifactPlatform, + type Release, } from '../hooks/useLatestRelease'; import { DirectDownloadBlock, @@ -54,6 +55,8 @@ type StudioCard = { // payloads and must never be offered as a download link. platform: ArtifactPlatform; kind: ArtifactKind; + // One download per architecture where the platform ships more than one. + arches?: ('x64' | 'arm64')[]; }; const studioCards: StudioCard[] = [ @@ -90,10 +93,10 @@ const studioCards: StudioCard[] = [ { key: 'linux', title: 'Linux', - meta: 'Linux · x86_64', + meta: 'Linux · x86_64 · ARM64', icon: , minimum: [ - 'Linux x86_64 — Debian, Ubuntu, or a derivative', + 'Linux x86_64 or ARM64 — Debian, Ubuntu, or a derivative', 'Vulkan-capable GPU — NVIDIA, AMD, or Intel, including integrated graphics', '16 GB system RAM', '10 GB free disk space', @@ -101,9 +104,130 @@ const studioCards: StudioCard[] = [ recommended: ['More system RAM is better — 24 GB or above', 'A discrete NVIDIA GPU (RTX 40-series or newer, 8 GB+ VRAM)'], platform: 'linux', kind: 'deb', + arches: ['x64', 'arm64'], }, ]; +type Arch = 'x64' | 'arm64'; + +const ARCH_NAME: Record = { x64: 'x86_64', arm64: 'ARM64' }; + +/** + * The machine's architecture, where the browser says. Chromium reports it + * through User-Agent Client Hints; other browsers sometimes name it in the + * user agent (aarch64 / arm64 on Linux). Null off Linux, or when there is no + * telling, and the switch stays on x86_64. + */ +async function detectArch(): Promise { + const nav = navigator as Navigator & { + userAgentData?: { + platform?: string; + getHighEntropyValues(hints: string[]): Promise<{ architecture?: string }>; + }; + }; + // Only a Linux machine's own architecture says which Linux build it wants; + // on a Mac or PC the download is for some other box, so leave the default. + const platform = nav.userAgentData?.platform ?? navigator.userAgent; + if (!/linux/i.test(platform) || /android/i.test(navigator.userAgent)) return null; + try { + const hints = await nav.userAgentData?.getHighEntropyValues(['architecture']); + if (hints?.architecture === 'arm') return 'arm64'; + if (hints?.architecture === 'x86') return 'x64'; + } catch { + // Hints refused; fall back to the user agent. + } + if (/aarch64|arm64|armv8/i.test(navigator.userAgent)) return 'arm64'; + if (/x86_64|amd64|x64/i.test(navigator.userAgent)) return 'x64'; + return null; +} + +/** + * The build a multi-architecture card offers. It starts on the visitor's own + * architecture when the browser reports it, so most people never touch the + * switch. + */ +function useArch(arches: Arch[] | undefined): [Arch | undefined, (a: Arch) => void] { + const [arch, setArch] = useState(arches?.[0]); + + useEffect(() => { + if (!arches) return; + let live = true; + detectArch().then((found) => { + if (live && found && arches.includes(found)) setArch(found); + }); + return () => { + live = false; + }; + }, []); + + return [arch, setArch]; +} + +/** + * A platform's card. Where the platform ships more than one architecture, the + * switch takes the place of the line under its name, and the one button at + * the foot downloads whichever build is picked. + */ +function DownloadCard({ card, release }: { card: StudioCard; release: Release | null }): ReactNode { + const [arch, setArch] = useArch(card.arches); + const artifact = findArtifact(release, card.platform, card.kind, arch); + + return ( + // The download link carries its own click, so the card itself is a plain + // container rather than one big link, and does not light up on hover. +
+
+
{card.icon}
+
+

{card.title}

+ {card.arches && arch ? ( +
+ {card.arches.map((a) => ( + + ))} +
+ ) : ( +

{card.meta}

+ )} +
+
+

Minimum

+
    + {card.minimum.map((req, i) => ( +
  • {req}
  • + ))} +
+

Recommended

+
    + {card.recommended.map((req, i) => ( +
  • {req}
  • + ))} +
+
+ {/* The block links to the API's tracked redirect, which resolves the + latest build itself; the release fetch only adds the size. Every + card gets the same plain button, so the visitor's own platform is + not singled out. */} + +
+
+ ); +} + export default function Download(): ReactNode { const { status, release } = useLatestRelease(); @@ -138,48 +262,9 @@ export default function Download(): ReactNode { {/* Studio downloads */}
- {studioCards.map((card) => { - const artifact = findArtifact(release, card.platform, card.kind); - // Every card gets the plain button; hover is what highlights - // one, so the visitor's own platform is not singled out. - const variant = 'secondary'; - - return ( - // The badge and the direct link each carry their own click, so - // the card itself is a plain container rather than one big link. -
-
-
{card.icon}
-
-

{card.title}

-

{card.meta}

-
-
-

Minimum

-
    - {card.minimum.map((req, i) => ( -
  • {req}
  • - ))} -
-

Recommended

-
    - {card.recommended.map((req, i) => ( -
  • {req}
  • - ))} -
-
- {/* The block links to the API's tracked redirect, - which resolves the latest build itself; the release - fetch only adds the size. */} - -
-
- ); - })} + {studioCards.map((card) => ( + + ))}
diff --git a/src/pages/index.module.css b/src/pages/index.module.css index 95e6625..f0992cc 100644 --- a/src/pages/index.module.css +++ b/src/pages/index.module.css @@ -746,13 +746,6 @@ details[open] .faqQuestion::after { flex-direction: column; gap: 14px; text-decoration: none; - transition: border-color 0.2s, background 0.2s; -} - -.downloadCard:hover { - border-color: var(--sl-accent); - background: var(--sl-panel); - text-decoration: none; } .downloadCardDisabled { @@ -1027,6 +1020,23 @@ details[open] .faqQuestion::after { color: var(--sl-accent); } +/* The architecture switch in the header of a card whose platform ships more + than one build, in place of the line under its name. Rounded like the + download button, and in the builds' own spelling. */ +.archSwitch { + margin-top: 4px; + padding: 2px; + border-radius: var(--r-sm, 10px); +} + +.archSwitch .downloadOsTab { + padding: 3px 10px; + border-radius: calc(var(--r-sm, 10px) - 2px); + text-transform: none; + letter-spacing: 0.02em; + font-family: var(--sl-mono); +} + /* ===================== Pricing ===================== */ .pricingGrid { display: grid; @@ -1375,10 +1385,6 @@ details[open] .faqQuestion::after { border-color: rgba(204, 204, 204, 0.5); } -[data-theme='light'] .downloadCard:hover { - background: #ededed; -} - [data-theme='light'] .downloadCardTitle { color: #0a0a0a; } @@ -1397,11 +1403,6 @@ details[open] .faqQuestion::after { border-color: rgba(204, 204, 204, 0.5); } -[data-theme='light'] .downloadCard:hover { - border-color: var(--sl-accent); - background: #ededed; -} - [data-theme='light'] .downloadCardAction { background: rgba(0, 0, 0, 0.04); color: #0a0a0a; @@ -2828,15 +2829,6 @@ details[open] .faqQuestion::after { box-shadow: var(--shadow-soft); } -.downloadCard:hover { - border-color: rgba(236, 72, 153, 0.35); - transform: translateY(-2px); -} - -.downloadCard { - transition: border-color 0.2s, transform 0.2s; -} - .planCardFeatured { border-color: rgba(236, 72, 153, 0.45); box-shadow: var(--shadow-soft), 0 0 60px -10px var(--sl-accent-wash); diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 4f4b918..a2854a8 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -518,10 +518,11 @@ export default function Home(): ReactNode {

Same agent. No window.

- Install docent to chat with your documents, search the web and connect MCP servers — in your scripts or over SSH, with the same models and projects as Studio. + An agent harness for your terminal: build products, write code and chat with your documents, with the same models and projects as Studio.

    -
  • One command. Sets itself up on first run.
  • +
  • Writes code. Changes land as diffs, checked by your build until it passes.
  • +
  • Runs the work. Commits, installs and tests, once you say so.
  • Your files. Drop a PDF on the terminal and ask.
  • Scriptable. Pipe answers with --plain.
diff --git a/src/utils/api.ts b/src/utils/api.ts index 1dd1f46..648e874 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -15,11 +15,14 @@ export function useApiBaseUrl(): string { /** * The tracked installer download for a platform. The API logs the hit, * rate-limits it per address, and 302s to the artifact — the bundle never - * carries a direct CDN link a scraper could loop on. + * carries a direct CDN link a scraper could loop on. `arch` picks the build + * where a platform ships more than one (Linux: x64 and arm64). */ -export function useInstallerUrl(): (platform: ReleasePlatform) => string { +export function useInstallerUrl(): (platform: ReleasePlatform, arch?: 'x64' | 'arm64') => string { const base = useApiBaseUrl(); - return (platform) => `${base}/v1/releases/download/${platform}`; + // Without arch the API serves x64 where a platform ships both. + return (platform, arch) => + `${base}/v1/releases/download/${platform}${arch ? `?arch=${arch}` : ''}`; } export type ContactRequest = {