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));
}