feat(frontend): add platform icons and improve download button UX

Replace generic download icon with platform-specific icons (Windows,
macOS, Linux) using react-icons. Show detected platform name in the
main download button text and disable auto-download when platform
cannot be detected, requiring manual selection from dropdown instead.
This commit is contained in:
2025-12-11 17:40:04 -06:00
parent 65aa9d66d3
commit fd474767ae
3 changed files with 57 additions and 17 deletions

View File

@@ -22,6 +22,7 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-icons": "^5.5.0",
"react-tooltip": "^5.28.0", "react-tooltip": "^5.28.0",
"react-use-websocket": "^4.11.1", "react-use-websocket": "^4.11.1",
"tailwind-merge": "^2.5.5", "tailwind-merge": "^2.5.5",

View File

@@ -47,6 +47,9 @@ importers:
react-dom: react-dom:
specifier: ^19.0.0 specifier: ^19.0.0
version: 19.0.0(react@19.0.0) version: 19.0.0(react@19.0.0)
react-icons:
specifier: ^5.5.0
version: 5.5.0(react@19.0.0)
react-tooltip: react-tooltip:
specifier: ^5.28.0 specifier: ^5.28.0
version: 5.28.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) version: 5.28.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@@ -2086,6 +2089,11 @@ packages:
peerDependencies: peerDependencies:
react: ^19.0.0 react: ^19.0.0
react-icons@5.5.0:
resolution: {integrity: sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==}
peerDependencies:
react: '*'
react-refresh@0.14.2: react-refresh@0.14.2:
resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -5086,6 +5094,10 @@ snapshots:
react: 19.0.0 react: 19.0.0
scheduler: 0.25.0 scheduler: 0.25.0
react-icons@5.5.0(react@19.0.0):
dependencies:
react: 19.0.0
react-refresh@0.14.2: {} react-refresh@0.14.2: {}
react-tooltip@5.28.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0): react-tooltip@5.28.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0):

View File

@@ -9,10 +9,10 @@ import {
MenuSeparator, MenuSeparator,
} from "@headlessui/react"; } from "@headlessui/react";
import { import {
ArrowDownTrayIcon,
BeakerIcon, BeakerIcon,
ChevronDownIcon, ChevronDownIcon,
} from "@heroicons/react/16/solid"; } from "@heroicons/react/16/solid";
import { FaWindows, FaApple, FaLinux } from "react-icons/fa";
import { useRef } from "react"; import { useRef } from "react";
type DownloadButtonProps = { type DownloadButtonProps = {
@@ -36,6 +36,34 @@ function getSystemType(): SystemType | null {
} }
} }
function getPlatformIcon(id: string, className?: string) {
const platformId = id.toLowerCase();
switch (platformId) {
case "windows":
return <FaWindows className={className} />;
case "macos":
return <FaApple className={className} />;
case "linux":
return <FaLinux className={className} />;
default:
return null;
}
}
function getPlatformDisplayName(id: string): string {
const platformId = id.toLowerCase();
switch (platformId) {
case "windows":
return "Windows";
case "macos":
return "macOS";
case "linux":
return "Linux";
default:
return id;
}
}
export default function DownloadButton({ export default function DownloadButton({
disabled, disabled,
executables, executables,
@@ -47,6 +75,10 @@ export default function DownloadButton({
return executables?.find((e) => e.id.toLowerCase() === id.toLowerCase()); return executables?.find((e) => e.id.toLowerCase() === id.toLowerCase());
} }
const detectedPlatform = getSystemType();
const platformExecutable = detectedPlatform ? getExecutable(detectedPlatform) : null;
const canAutoDownload = platformExecutable != null;
async function handleDownload(id: string) { async function handleDownload(id: string) {
const executable = getExecutable(id); const executable = getExecutable(id);
if (executable == null) { if (executable == null) {
@@ -59,16 +91,8 @@ export default function DownloadButton({
} }
function handleDownloadAutomatic() { function handleDownloadAutomatic() {
const systemType = getSystemType(); if (canAutoDownload && detectedPlatform) {
handleDownload(detectedPlatform);
// If the system type is unknown/unavailable, open the menu for manual selection
if (systemType == null || getExecutable(systemType) == null) {
menuRef.current?.click();
}
// Otherwise, download the executable automatically
else {
handleDownload(systemType);
} }
} }
@@ -83,14 +107,17 @@ export default function DownloadButton({
)} )}
> >
<Button <Button
onClick={handleDownloadAutomatic} onClick={canAutoDownload ? handleDownloadAutomatic : undefined}
suppressHydrationWarning suppressHydrationWarning
disabled={disabled} disabled={disabled || !canAutoDownload}
className={cn("pl-3 font-semibold pr-2.5", { className={cn("pl-3 font-semibold pr-2.5", {
"hover:bg-white/5": !disabled, "hover:bg-white/5 cursor-pointer": !disabled && canAutoDownload,
"cursor-default": !canAutoDownload,
})} })}
> >
Download {canAutoDownload && detectedPlatform
? `Download for ${getPlatformDisplayName(detectedPlatform)}`
: "Download"}
</Button> </Button>
<Menu> <Menu>
<MenuButton <MenuButton
@@ -115,8 +142,8 @@ export default function DownloadButton({
onClick={() => handleDownload(executable.id)} onClick={() => handleDownload(executable.id)}
> >
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<ArrowDownTrayIcon className="size-4 fill-white/40" /> {getPlatformIcon(executable.id, "size-4 fill-white/40")}
{executable.id} {getPlatformDisplayName(executable.id)}
</div> </div>
<div className="text-xs text-zinc-500"> <div className="text-xs text-zinc-500">
{(executable.size / 1024 / 1024).toFixed(1)} MiB {(executable.size / 1024 / 1024).toFixed(1)} MiB