From c6833e6b07926c3255585bacefcb40b71bee2e32 Mon Sep 17 00:00:00 2001 From: Ben Schellenberger Date: Thu, 16 Apr 2026 01:27:13 -0400 Subject: [PATCH] Replace install platform select with button radiogroup Use segmented platform buttons with shared session state, radiogroup semantics, and arrow-key navigation. Scope copy-button styles to actions so platform controls keep their own look. Made-with: Cursor --- src/components/InstallPicker.astro | 40 ++++++++++-- src/scripts/install-pickers.ts | 100 +++++++++++++++++++++++++---- src/styles/global.css | 54 ++++++++++++---- 3 files changed, 163 insertions(+), 31 deletions(-) diff --git a/src/components/InstallPicker.astro b/src/components/InstallPicker.astro index 6e5304d..3f9f38f 100644 --- a/src/components/InstallPicker.astro +++ b/src/components/InstallPicker.astro @@ -14,6 +14,7 @@ const releasesUrl = : 'https://github.com/git-fire/git-rain/releases'; const labelId = `platform-label-${product}`; +const platformGroupId = `platform-group-${product}`; ---
{!embedded &&

{title}

} - - +

{embedded ? 'Install' : 'Platform'}

+
+ + + + +
diff --git a/src/scripts/install-pickers.ts b/src/scripts/install-pickers.ts index c09d0c3..3ff3ae1 100644 --- a/src/scripts/install-pickers.ts +++ b/src/scripts/install-pickers.ts @@ -3,6 +3,13 @@ type OsFamily = 'macos' | 'windows' | 'linux' | 'go'; const STORAGE_KEY = 'git-fire-site-install-os'; +const PLATFORM_ORDER: OsFamily[] = ['macos', 'windows', 'linux', 'go']; + +function coerceOs(value: string | undefined): OsFamily | null { + if (value === 'macos' || value === 'windows' || value === 'linux' || value === 'go') return value; + return null; +} + function detectOs(): OsFamily { if (typeof navigator === 'undefined') return 'go'; const uaData = navigator.userAgentData; @@ -92,16 +99,32 @@ function commandBlock(product: ProductId, os: OsFamily): { command: string; note } } +function selectedOs(root: HTMLElement): OsFamily { + const active = root.querySelector( + '[data-install-platform][aria-checked="true"]', + ); + return coerceOs(active?.dataset.installPlatform) ?? PLATFORM_ORDER[0]; +} + +function syncPlatformButtons(root: HTMLElement, os: OsFamily) { + root.querySelectorAll('[data-install-platform]').forEach((btn) => { + const v = coerceOs(btn.dataset.installPlatform); + const isSelected = v === os; + btn.setAttribute('aria-checked', isSelected ? 'true' : 'false'); + btn.tabIndex = isSelected ? 0 : -1; + btn.classList.toggle('install-picker__platform-btn--active', isSelected); + }); +} + function renderPicker(root: HTMLElement) { const product = root.dataset.product as ProductId; if (product !== 'git-fire' && product !== 'git-rain') return; - const select = root.querySelector('[data-install-select]'); const pre = root.querySelector('[data-install-command]'); const noteEl = root.querySelector('[data-install-note]'); - if (!select || !pre || !noteEl) return; + if (!pre || !noteEl) return; - const os = select.value as OsFamily; + const os = selectedOs(root); const { command, note } = commandBlock(product, os); pre.textContent = command; noteEl.textContent = note; @@ -110,28 +133,83 @@ function renderPicker(root: HTMLElement) { function setPlatformEverywhere(os: OsFamily) { persistOs(os); document.querySelectorAll('[data-install-picker]').forEach((root) => { - const select = root.querySelector('[data-install-select]'); - if (select) select.value = os; + syncPlatformButtons(root, os); renderPicker(root); }); } +function bindPlatformRadiogroup(root: HTMLElement) { + const group = root.querySelector('.install-picker__platforms'); + const buttons = root.querySelectorAll('[data-install-platform]'); + if (!group || buttons.length === 0) return; + + group.addEventListener('keydown', (event) => { + if (!(event.target instanceof HTMLButtonElement)) return; + if (!event.target.matches('[data-install-platform]')) return; + + const current = coerceOs(event.target.dataset.installPlatform); + if (!current) return; + + let next: OsFamily | null = null; + + switch (event.key) { + case 'ArrowRight': + case 'ArrowDown': { + event.preventDefault(); + const i = PLATFORM_ORDER.indexOf(current); + next = PLATFORM_ORDER[Math.min(i + 1, PLATFORM_ORDER.length - 1)]; + break; + } + case 'ArrowLeft': + case 'ArrowUp': { + event.preventDefault(); + const i = PLATFORM_ORDER.indexOf(current); + next = PLATFORM_ORDER[Math.max(i - 1, 0)]; + break; + } + case 'Home': { + event.preventDefault(); + next = PLATFORM_ORDER[0]; + break; + } + case 'End': { + event.preventDefault(); + next = PLATFORM_ORDER[PLATFORM_ORDER.length - 1]; + break; + } + default: + return; + } + + if (next) { + setPlatformEverywhere(next); + queueMicrotask(() => { + root.querySelector(`[data-install-platform="${next}"]`)?.focus(); + }); + } + }); + + buttons.forEach((btn) => { + btn.addEventListener('click', () => { + const os = coerceOs(btn.dataset.installPlatform); + if (os) setPlatformEverywhere(os); + }); + }); +} + function bindPicker(root: HTMLElement) { const product = root.dataset.product as ProductId; if (product !== 'git-fire' && product !== 'git-rain') return; - const select = root.querySelector('[data-install-select]'); const copyBtn = root.querySelector('[data-install-copy]'); - if (!select || !copyBtn) return; + if (!copyBtn) return; const initial = storedOs() ?? detectOs(); - select.value = initial; + syncPlatformButtons(root, initial); renderPicker(root); - select.addEventListener('change', () => { - setPlatformEverywhere(select.value as OsFamily); - }); + bindPlatformRadiogroup(root); copyBtn.addEventListener('click', async () => { const pre = root.querySelector('[data-install-command]'); diff --git a/src/styles/global.css b/src/styles/global.css index e80686f..71810a9 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -445,7 +445,7 @@ a:hover { border-color: color-mix(in srgb, var(--color-border) 80%, var(--color-rain)); } -.install-picker--embedded label { +.install-picker--embedded .install-picker__label { margin-bottom: var(--space-xs); } @@ -470,25 +470,53 @@ a:hover { font-size: 0.8125rem; } -.install-picker label { +.install-picker__label { display: block; font-size: 0.8125rem; font-weight: 600; color: var(--color-muted); - margin-bottom: var(--space-sm); + margin: 0 0 var(--space-sm); } -.install-picker select { - width: 100%; +.install-picker__platforms { + display: flex; + flex-wrap: wrap; + gap: var(--space-xs); + margin-bottom: var(--space-md); max-width: 22rem; - padding: var(--space-sm) var(--space-md); +} + +.install-picker__platform-btn { font: inherit; - font-size: 0.9375rem; - color: var(--color-text); - background: var(--color-bg); - border: 1px solid color-mix(in srgb, var(--color-border) 80%, var(--color-text)); + font-size: 0.8125rem; + font-weight: 600; + padding: var(--space-xs) var(--space-sm); border-radius: var(--radius-sm); - margin-bottom: var(--space-md); + border: 1px solid color-mix(in srgb, var(--color-border) 85%, var(--color-text)); + background: var(--color-bg); + color: var(--color-muted); + cursor: pointer; + transition: + border-color 0.15s ease, + background 0.15s ease, + color 0.15s ease; +} + +.install-picker__platform-btn:hover { + color: var(--color-text); + border-color: color-mix(in srgb, var(--color-border) 55%, var(--color-text)); +} + +.install-picker[data-product='git-fire'] .install-picker__platform-btn--active { + color: var(--color-fire); + border-color: color-mix(in srgb, var(--color-fire) 45%, var(--color-border)); + background: color-mix(in srgb, var(--color-fire) 12%, var(--color-bg)); +} + +.install-picker[data-product='git-rain'] .install-picker__platform-btn--active { + color: var(--color-rain); + border-color: color-mix(in srgb, var(--color-rain) 45%, var(--color-border)); + background: color-mix(in srgb, var(--color-rain) 12%, var(--color-bg)); } .install-picker pre { @@ -513,7 +541,7 @@ a:hover { margin-bottom: var(--space-md); } -.install-picker button { +.install-picker__actions button { font: inherit; font-size: 0.875rem; font-weight: 600; @@ -526,7 +554,7 @@ a:hover { transition: border-color 0.15s ease, background 0.15s ease; } -.install-picker button:hover { +.install-picker__actions button:hover { border-color: var(--color-rain-dim); background: color-mix(in srgb, var(--color-rain) 12%, var(--color-surface)); }