Reachy_Mini / src /pages /Download.jsx
tfrere's picture
tfrere HF Staff
feat(download): add Linux beta support
a540fdf
import { useState, useEffect } from 'react';
import {
Box,
Container,
Typography,
Button,
Grid,
Card,
CardContent,
Chip,
Stack,
CircularProgress,
IconButton,
Tooltip,
} from '@mui/material';
import DownloadIcon from '@mui/icons-material/Download';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import Layout from '../components/Layout';
// Platform configuration
const PLATFORMS = {
'darwin-aarch64': {
name: 'macOS',
subtitle: 'Apple Silicon',
arch: 'M1, M2, M3, M4',
format: '.dmg',
},
'darwin-x86_64': {
name: 'macOS',
subtitle: 'Intel',
arch: 'x86_64',
format: '.dmg',
},
'windows-x86_64': {
name: 'Windows',
subtitle: '64-bit',
arch: 'x86_64',
format: '.msi',
},
'linux-x86_64': {
name: 'Linux',
subtitle: 'Debian/Ubuntu',
arch: 'x86_64',
format: '.deb',
},
};
// URL to fetch latest release info (using GitHub API for CORS support)
const GITHUB_RELEASES_API = 'https://api.github.com/repos/pollen-robotics/reachy-mini-desktop-app/releases/latest';
const GITHUB_RELEASES_LIST_API = 'https://api.github.com/repos/pollen-robotics/reachy-mini-desktop-app/releases?per_page=10';
// Detect user's platform
function detectPlatform() {
const ua = navigator.userAgent;
const platform = navigator.platform || '';
if (/Mac/.test(platform) || /Mac/.test(ua)) {
return 'darwin-aarch64';
}
if (/Win/.test(platform) || /Windows/.test(ua)) {
return 'windows-x86_64';
}
if (/Linux/.test(platform) || /Linux/.test(ua)) {
return 'linux-x86_64';
}
return 'darwin-aarch64';
}
// Format date
function formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
});
}
// Parse release body and extract clean changes
function parseReleaseChanges(body) {
if (!body) return [];
const changes = [];
const lines = body.split('\n');
for (const line of lines) {
const trimmed = line.trim();
// Skip empty lines, headers, and meta content
if (!trimmed) continue;
if (trimmed.startsWith('##')) continue; // Skip all headers (## What's Changed, etc.)
if (trimmed.startsWith('**Full Changelog**')) continue;
if (trimmed.startsWith('**New Contributors**')) continue;
if (trimmed.includes('made their first contribution')) continue;
if (trimmed.startsWith('<!--') || trimmed.endsWith('-->')) continue;
if (trimmed === 'See the assets to download this version and install.') continue;
// Parse change lines (starting with * or -)
if (trimmed.startsWith('*') || trimmed.startsWith('-')) {
let change = trimmed.replace(/^[\*\-]\s*/, '');
// Extract the description from markdown links: "fix: description by @user in https://..."
// We want to keep: "fix: description"
const byMatch = change.match(/^(.+?)\s+by\s+@\w+/i);
if (byMatch) {
change = byMatch[1].trim();
}
// Remove trailing "in https://..." links
change = change.replace(/\s+in\s+https:\/\/[^\s]+$/i, '');
// Clean up any remaining markdown link syntax [text](url) -> text
change = change.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
// Skip if it's just a contributor line or empty after cleaning
if (change && change.length > 3 && !change.includes('first contribution')) {
changes.push(change);
}
}
}
return changes;
}
// Platform Icons - all use external SVG files with white fill
function AppleIcon() {
return (
<Box
component="img"
src="/assets/apple-logo.svg"
alt="Apple"
sx={{ width: 32, height: 32 }}
/>
);
}
function WindowsIcon() {
return (
<Box
component="img"
src="/assets/windows-logo.svg"
alt="Windows"
sx={{ width: 32, height: 32 }}
/>
);
}
function LinuxIcon() {
return (
<Box
component="img"
src="/assets/linux-logo.svg"
alt="Linux"
sx={{ width: 32, height: 32 }}
/>
);
}
// Get platform icon component
function getPlatformIcon(platformKey) {
if (platformKey.includes('darwin')) return <AppleIcon />;
if (platformKey.includes('windows')) return <WindowsIcon />;
if (platformKey.includes('linux')) return <LinuxIcon />;
return null;
}
// Parse release assets and map to platforms
// This is the single source of truth for platform → download URL mapping
function parseReleasePlatforms(assets) {
if (!assets) return {};
const platforms = {};
assets.forEach(asset => {
const name = asset.name.toLowerCase();
const url = asset.browser_download_url;
// macOS Apple Silicon - prefer .dmg
if (name.includes('arm64.dmg')) {
platforms['darwin-aarch64'] = { url };
} else if (name.includes('darwin-aarch64') && !platforms['darwin-aarch64']) {
platforms['darwin-aarch64'] = { url };
}
// macOS Intel - prefer .dmg
if (name.includes('x64.dmg') && !name.includes('arm64')) {
platforms['darwin-x86_64'] = { url };
} else if (name.includes('darwin-x86_64') && !platforms['darwin-x86_64']) {
platforms['darwin-x86_64'] = { url };
}
// Windows - .msi (exclude .sig signature files)
if (name.endsWith('.msi')) {
platforms['windows-x86_64'] = { url };
}
// Linux - .deb
if (name.includes('amd64.deb')) {
platforms['linux-x86_64'] = { url };
}
});
return platforms;
}
// Get download URL for a specific platform from release
function getDownloadUrlForPlatform(release, platform) {
if (!release?.assets || !platform) return release?.html_url;
const platforms = parseReleasePlatforms(release.assets);
return platforms[platform]?.url || release?.html_url;
}
// Platform Card component
function PlatformCard({ platformKey, url, isActive, onClick }) {
const platform = PLATFORMS[platformKey];
const isComingSoon = platformKey.includes('darwin-x86_64');
const isBeta = platformKey.includes('windows') || platformKey.includes('linux');
return (
<Card
onClick={onClick}
sx={{
cursor: 'pointer',
position: 'relative',
background: isActive
? 'linear-gradient(135deg, rgba(59, 130, 246, 0.15) 0%, rgba(139, 92, 246, 0.1) 100%)'
: 'rgba(255, 255, 255, 0.03)',
border: '1px solid',
borderColor: isActive ? 'rgba(59, 130, 246, 0.4)' : 'rgba(255, 255, 255, 0.08)',
backdropFilter: 'blur(10px)',
transition: 'all 0.25s ease',
opacity: isComingSoon ? 0.7 : 1,
'&:hover': {
borderColor: isActive ? 'rgba(59, 130, 246, 0.6)' : 'rgba(255, 255, 255, 0.2)',
transform: 'translateY(-4px)',
boxShadow: isActive
? '0 12px 40px rgba(59, 130, 246, 0.2)'
: '0 12px 40px rgba(0, 0, 0, 0.3)',
opacity: 1,
},
}}
>
{/* Coming soon tag */}
{isComingSoon && (
<Chip
label="Coming soon"
size="small"
sx={{
position: 'absolute',
top: 8,
right: 8,
backgroundColor: 'rgba(255, 149, 0, 0.2)',
color: '#FF9500',
fontSize: 10,
fontWeight: 700,
height: 20,
'& .MuiChip-label': { px: 1 },
}}
/>
)}
{/* Beta tag for Windows */}
{isBeta && !isComingSoon && (
<Chip
label="Beta"
size="small"
sx={{
position: 'absolute',
top: 8,
right: 8,
backgroundColor: 'rgba(59, 130, 246, 0.2)',
color: '#3b82f6',
fontSize: 10,
fontWeight: 700,
height: 20,
'& .MuiChip-label': { px: 1 },
}}
/>
)}
<CardContent
component="a"
href={isComingSoon ? undefined : url}
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: 1.5,
textDecoration: 'none',
color: 'inherit',
p: 3,
'&:last-child': { pb: 3 },
pointerEvents: isComingSoon ? 'none' : 'auto',
}}
>
<Box sx={{
width: 56,
height: 56,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 3,
backgroundColor: 'rgba(255, 255, 255, 0.05)',
}}>
{getPlatformIcon(platformKey)}
</Box>
<Box sx={{ textAlign: 'center' }}>
<Typography
variant="subtitle1"
sx={{ fontWeight: 600, color: 'white', lineHeight: 1.2 }}
>
{platform?.name}
</Typography>
<Typography
variant="caption"
sx={{ color: 'rgba(255,255,255,0.5)' }}
>
{platform?.subtitle}
</Typography>
</Box>
<Chip
label={platform?.format}
size="small"
sx={{
backgroundColor: 'rgba(255, 255, 255, 0.08)',
color: 'rgba(255,255,255,0.7)',
fontSize: 11,
fontWeight: 600,
height: 24,
}}
/>
</CardContent>
</Card>
);
}
export default function Download() {
const [releaseData, setReleaseData] = useState(null);
const [allReleases, setAllReleases] = useState([]);
const [detectedPlatform, setDetectedPlatform] = useState(null);
const [loading, setLoading] = useState(true);
const [showAllReleases, setShowAllReleases] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
setDetectedPlatform(detectPlatform());
// Fetch latest release info from GitHub API
async function fetchReleases() {
try {
// Fetch latest release for download buttons
const latestResponse = await fetch(GITHUB_RELEASES_API);
// Fetch all releases for changelog
const allResponse = await fetch(GITHUB_RELEASES_LIST_API);
if (latestResponse.ok) {
const data = await latestResponse.json();
// Transform GitHub API response to our format
const version = data.tag_name?.replace('v', '') || '';
const platforms = parseReleasePlatforms(data.assets);
setReleaseData({
version,
pub_date: data.published_at,
platforms,
});
} else {
setError('Failed to fetch release info');
}
// Set all releases for changelog
if (allResponse.ok) {
const releases = await allResponse.json();
setAllReleases(releases.filter(r => !r.draft));
}
} catch (err) {
console.error('Error fetching release:', err);
setError('Failed to fetch release info');
} finally {
setLoading(false);
}
}
fetchReleases();
}, []);
if (loading) {
return (
<Layout transparentHeader>
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh', bgcolor: '#000' }}>
<CircularProgress sx={{ color: 'white' }} />
</Box>
</Layout>
);
}
if (error || !releaseData) {
return (
<Layout transparentHeader>
<Box sx={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', minHeight: '100vh', bgcolor: '#000', color: 'white', gap: 3 }}>
<Typography variant="h5">Unable to load release info</Typography>
<Button
variant="outlined"
href="https://github.com/pollen-robotics/reachy-mini-desktop-app/releases"
target="_blank"
sx={{ color: 'white', borderColor: 'rgba(255,255,255,0.3)' }}
>
View releases on GitHub
</Button>
</Box>
</Layout>
);
}
const currentPlatform = PLATFORMS[detectedPlatform];
const currentUrl = releaseData?.platforms[detectedPlatform]?.url;
return (
<Layout transparentHeader>
<Box
sx={{
minHeight: '100vh',
background: 'linear-gradient(180deg, #000 0%, #0a0a12 50%, #0f0f1a 100%)',
color: 'white',
pt: 14,
pb: 12,
position: 'relative',
overflow: 'hidden',
}}
>
{/* Subtle gradient orbs - spread across the page */}
<Box
sx={{
position: 'absolute',
top: 100,
left: '-10%',
width: 600,
height: 600,
background: 'radial-gradient(circle, rgba(59, 130, 246, 0.1) 0%, transparent 70%)',
pointerEvents: 'none',
}}
/>
<Box
sx={{
position: 'absolute',
top: '40%',
right: '-15%',
width: 700,
height: 700,
background: 'radial-gradient(circle, rgba(139, 92, 246, 0.08) 0%, transparent 70%)',
pointerEvents: 'none',
}}
/>
<Box
sx={{
position: 'absolute',
bottom: 100,
left: '20%',
width: 500,
height: 500,
background: 'radial-gradient(circle, rgba(255, 149, 0, 0.05) 0%, transparent 70%)',
pointerEvents: 'none',
}}
/>
<Container maxWidth="md" sx={{ position: 'relative', zIndex: 1 }}>
{/* Hero Section */}
<Box sx={{ textAlign: 'center', mb: 8 }}>
{/* App icon */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
mb: 4,
}}
>
<Box
sx={{
width: 100,
height: 100,
borderRadius: '24px',
background: 'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)',
border: '1px solid rgba(255,255,255,0.1)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 8px 32px rgba(0,0,0,0.3)',
}}
>
<Box
component="img"
src="/assets/reachy-icon.svg"
alt="Reachy Mini Control"
sx={{ width: 64, height: 64 }}
/>
</Box>
</Box>
<Typography
variant="h2"
sx={{
mb: 2,
background: 'linear-gradient(135deg, #fff 0%, rgba(255,255,255,0.8) 100%)',
backgroundClip: 'text',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
}}
>
Reachy Mini Control
</Typography>
<Typography
variant="h6"
sx={{
color: 'rgba(255,255,255,0.6)',
fontWeight: 400,
mb: 3,
maxWidth: 450,
mx: 'auto',
}}
>
The official desktop app to control, program, and play with your Reachy Mini.
</Typography>
{/* Version info */}
<Stack
direction="row"
spacing={2}
justifyContent="center"
alignItems="center"
sx={{ mb: 5 }}
>
<Chip
icon={<Box sx={{ width: 8, height: 8, bgcolor: '#10b981', borderRadius: '50%', ml: 1 }} />}
label={`v${releaseData?.version}`}
sx={{
backgroundColor: 'rgba(16, 185, 129, 0.1)',
color: '#10b981',
fontWeight: 600,
border: '1px solid rgba(16, 185, 129, 0.2)',
}}
/>
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.4)' }}>
Released {formatDate(releaseData?.pub_date)}
</Typography>
</Stack>
{/* Primary download button - different for Windows/macOS Apple Silicon/Linux vs macOS Intel */}
{detectedPlatform?.startsWith('windows') || detectedPlatform === 'darwin-aarch64' || detectedPlatform?.includes('linux') ? (
<>
<Button
variant="contained"
size="large"
href={currentUrl}
startIcon={<DownloadIcon />}
sx={{
px: 6,
py: 2,
fontSize: 17,
fontWeight: 600,
borderRadius: 3,
background: 'linear-gradient(135deg, #FF9500 0%, #764ba2 100%)',
boxShadow: '0 8px 32px rgba(255, 149, 0, 0.35)',
transition: 'all 0.3s ease',
'&:hover': {
boxShadow: '0 12px 48px rgba(59, 130, 246, 0.5)',
transform: 'translateY(-2px)',
},
}}
>
Download for {currentPlatform?.name}
</Button>
<Typography
variant="body2"
sx={{
color: 'rgba(255,255,255,0.4)',
mt: 2,
fontSize: 13,
}}
>
{currentPlatform?.subtitle} • {currentPlatform?.format?.replace('.', '').toUpperCase()} package
</Typography>
{/* Beta Warning for Windows and Linux */}
{(detectedPlatform?.startsWith('windows') || detectedPlatform?.includes('linux')) && (
<Box
sx={{
mt: 3,
p: 2.5,
background: 'linear-gradient(135deg, rgba(59, 130, 246, 0.1) 0%, rgba(139, 92, 246, 0.08) 100%)',
border: '1px solid rgba(59, 130, 246, 0.3)',
borderRadius: 2,
maxWidth: 500,
mx: 'auto',
}}
>
<Typography
variant="body2"
sx={{
color: 'rgba(255,255,255,0.8)',
fontWeight: 500,
}}
>
{detectedPlatform?.startsWith('windows')
? <>⚠️ Windows version is currently in Beta — installation requires <strong style={{ color: 'rgba(255,255,255,0.9)' }}>administrator privileges</strong>.</>
: <>⚠️ Linux version is currently in Beta — please report any issues on <a href="https://github.com/pollen-robotics/reachy-mini-desktop-app/issues" target="_blank" rel="noopener noreferrer" style={{ color: '#3b82f6', textDecoration: 'underline' }}>GitHub</a> or <a href="https://discord.gg/HDrGY9eJHt" target="_blank" rel="noopener noreferrer" style={{ color: '#3b82f6', textDecoration: 'underline' }}>Discord</a>.</>
}
</Typography>
</Box>
)}
</>
) : (
<>
{/* macOS Intel - Coming soon message */}
<Box
sx={{
px: 5,
py: 2.5,
borderRadius: 3,
background: 'linear-gradient(135deg, rgba(255, 149, 0, 0.15) 0%, rgba(139, 92, 246, 0.1) 100%)',
border: '1px solid rgba(255, 149, 0, 0.3)',
display: 'inline-block',
}}
>
<Typography
variant="h6"
sx={{
color: '#FF9500',
fontWeight: 600,
fontSize: 17,
}}
>
🚧 {currentPlatform?.name} desktop app coming soon!
</Typography>
</Box>
<Typography
variant="body2"
sx={{
color: 'rgba(255,255,255,0.5)',
mt: 2,
fontSize: 14,
maxWidth: 500,
mx: 'auto',
}}
>
We're working hard to bring Reachy Mini Control to {currentPlatform?.name}.
In the meantime, macOS (Apple Silicon) is fully supported, and Windows & Linux are in beta!
</Typography>
{/* Alternative for Linux/advanced users - Python SDK */}
<Box
sx={{
mt: 3,
p: 3,
borderRadius: 2,
background: 'rgba(16, 185, 129, 0.08)',
border: '1px solid rgba(16, 185, 129, 0.25)',
maxWidth: 520,
mx: 'auto',
textAlign: 'left',
}}
>
<Typography
variant="body2"
sx={{
color: 'rgba(255,255,255,0.9)',
fontWeight: 600,
mb: 1,
fontSize: 14,
}}
>
🐍 Looking to use the Python SDK directly?
</Typography>
<Typography
variant="body2"
sx={{
color: 'rgba(255,255,255,0.6)',
fontSize: 13,
lineHeight: 1.6,
mb: 1.5,
}}
>
{detectedPlatform?.includes('linux')
? "Linux users can interact directly with their Reachy Mini using the Python SDK. Run the daemon locally and use the full API for custom applications."
: "Advanced users can interact directly with their Reachy Mini using the Python SDK and daemon."}
</Typography>
<Button
variant="outlined"
size="small"
href="https://huggingface.co/docs/reachy_mini/"
target="_blank"
endIcon={<OpenInNewIcon sx={{ fontSize: 14 }} />}
sx={{
color: '#10b981',
borderColor: 'rgba(16, 185, 129, 0.4)',
fontWeight: 600,
fontSize: 12,
textTransform: 'none',
'&:hover': {
borderColor: '#10b981',
bgcolor: 'rgba(16, 185, 129, 0.1)',
},
}}
>
View reachy_mini on GitHub
</Button>
</Box>
</>
)}
{/* App screenshot */}
<Box
component="img"
src="/assets/desktop-app-screenshot--white.png"
alt="Reachy Mini Control Dashboard"
sx={{
mt: 6,
width: '100%',
maxWidth: 700,
mx: 'auto',
display: 'block',
borderRadius: '12px',
}}
/>
</Box>
{/* All platforms */}
<Box sx={{ mb: 8 }}>
<Typography
variant="overline"
sx={{
color: 'rgba(255,255,255,0.4)',
display: 'block',
textAlign: 'center',
mb: 3,
letterSpacing: 2,
}}
>
Available for all platforms
</Typography>
<Grid container spacing={2}>
{['darwin-aarch64', 'darwin-x86_64', 'windows-x86_64', 'linux-x86_64'].map((key) => (
<Grid size={{ xs: 6, sm: 3 }} key={key}>
<PlatformCard
platformKey={key}
url={releaseData?.platforms[key]?.url}
isActive={key === detectedPlatform}
onClick={() => setDetectedPlatform(key)}
/>
</Grid>
))}
</Grid>
{/* Platform support notice - show on Windows, macOS Apple Silicon, and Linux */}
{(detectedPlatform?.startsWith('windows') || detectedPlatform === 'darwin-aarch64' || detectedPlatform?.includes('linux')) && (
<Box
sx={{
mt: 3,
p: 2,
background: 'rgba(255, 255, 255, 0.03)',
border: '1px solid rgba(255, 255, 255, 0.08)',
borderRadius: 2,
textAlign: 'center',
}}
>
<Typography
variant="body2"
sx={{ color: 'rgba(255,255,255,0.5)' }}
>
🚧 macOS Intel support coming soon
</Typography>
</Box>
)}
</Box>
{/* Features / What's included */}
<Box
sx={{
background: 'rgba(255, 255, 255, 0.02)',
border: '1px solid rgba(255, 255, 255, 0.06)',
borderRadius: 4,
p: 4,
mb: 6,
}}
>
<Typography
variant="h6"
sx={{ mb: 3, color: 'white', fontWeight: 600 }}
>
What's included
</Typography>
<Grid container spacing={2}>
{[
'3D visualization of your robot',
'Real-time motor control',
'App Store with 30+ apps',
'Camera & microphone access',
'Record & playback movements',
'Full SDK integration',
].map((feature, i) => (
<Grid size={{ xs: 12, sm: 6 }} key={i}>
<Stack direction="row" spacing={1.5} alignItems="center">
<CheckCircleIcon sx={{ color: '#10b981', fontSize: 20 }} />
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.7)' }}>
{feature}
</Typography>
</Stack>
</Grid>
))}
</Grid>
</Box>
{/* Requirements */}
<Box sx={{ textAlign: 'center', mb: 8 }}>
<Typography
variant="body2"
sx={{ color: 'rgba(255,255,255,0.4)', mb: 2 }}
>
Requires macOS 11+, Windows 10+, or Debian/Ubuntu Linux
</Typography>
{/* Privacy notice - subtle */}
<Typography
variant="caption"
sx={{
color: 'rgba(255,255,255,0.3)',
fontSize: 11,
display: 'block',
mb: 2,
}}
>
📊 Anonymous usage data is collected to improve the app.{' '}
<Box
component="a"
href="https://github.com/pollen-robotics/reachy-mini-desktop-app/blob/main/docs/TELEMETRY.md"
target="_blank"
rel="noopener noreferrer"
sx={{
color: 'rgba(255, 149, 0, 0.6)',
textDecoration: 'underline',
cursor: 'pointer',
'&:hover': { color: 'rgba(255, 149, 0, 0.8)' }
}}
>
Learn more
</Box>
</Typography>
<Button
variant="text"
size="small"
href="https://github.com/pollen-robotics/reachy-mini-desktop-app/releases"
target="_blank"
endIcon={<OpenInNewIcon sx={{ fontSize: 16 }} />}
sx={{
color: 'rgba(255,255,255,0.5)',
'&:hover': { color: 'white' },
}}
>
View all releases on GitHub
</Button>
</Box>
{/* Release Notes */}
{allReleases.length > 0 && (
<Box
id="release-notes"
sx={{
background: 'rgba(255, 255, 255, 0.02)',
border: '1px solid rgba(255, 255, 255, 0.06)',
borderRadius: 4,
p: 4,
scrollMarginTop: '100px', // Offset for fixed header
}}
>
<Typography
variant="h6"
sx={{ mb: 3, color: 'white', fontWeight: 600 }}
>
Release Notes
</Typography>
<Stack spacing={2.5}>
{(showAllReleases ? allReleases : allReleases.slice(0, 5))
.map((release) => {
const changes = parseReleaseChanges(release.body);
return (
<Box
key={release.id}
sx={{
borderLeft: '2px solid rgba(255, 149, 0, 0.4)',
pl: 3,
py: 0.5,
}}
>
<Stack direction="row" spacing={2} alignItems="center" flexWrap="wrap" sx={{ mb: changes.length > 0 ? 1 : 0 }}>
<Typography
variant="subtitle2"
sx={{ color: 'white', fontWeight: 600 }}
>
{release.tag_name}
</Typography>
<Chip
label={formatDate(release.published_at)}
size="small"
sx={{
backgroundColor: 'rgba(255, 255, 255, 0.05)',
color: 'rgba(255,255,255,0.5)',
fontSize: 10,
height: 20,
}}
/>
{release.prerelease && (
<Chip
label="Pre-release"
size="small"
sx={{
backgroundColor: 'rgba(255, 149, 0, 0.15)',
color: '#FF9500',
fontSize: 10,
height: 20,
}}
/>
)}
{/* Download icon - direct download for detected platform */}
<Tooltip title={`Download ${release.tag_name}`} arrow>
<IconButton
component="a"
href={getDownloadUrlForPlatform(release, detectedPlatform)}
target="_blank"
rel="noopener noreferrer"
size="small"
sx={{
color: 'rgba(255, 255, 255, 0.3)',
padding: 0.5,
ml: 'auto',
'&:hover': {
color: '#FF9500',
backgroundColor: 'rgba(255, 149, 0, 0.1)',
},
}}
>
<DownloadIcon sx={{ fontSize: 16 }} />
</IconButton>
</Tooltip>
</Stack>
{changes.length > 0 && (
<Box component="ul" sx={{ m: 0, pl: 2.5, listStyle: 'none' }}>
{changes.map((change, i) => (
<Box
component="li"
key={i}
sx={{
color: 'rgba(255,255,255,0.6)',
fontSize: 13,
lineHeight: 1.6,
position: 'relative',
'&::before': {
content: '"•"',
position: 'absolute',
left: -14,
color: 'rgba(255, 149, 0, 0.6)',
}
}}
>
{change}
</Box>
))}
</Box>
)}
</Box>
);
})}
</Stack>
{allReleases.length > 5 && (
<Button
variant="text"
onClick={() => setShowAllReleases(!showAllReleases)}
endIcon={showAllReleases ? <ExpandLessIcon /> : <ExpandMoreIcon />}
sx={{
mt: 2,
color: 'rgba(255,255,255,0.5)',
'&:hover': { color: 'white' },
}}
>
{showAllReleases ? 'Show less' : 'Show older releases'}
</Button>
)}
</Box>
)}
</Container>
</Box>
</Layout>
);
}