126 lines · 3.8 KB · TypeScript
RawDownload
import { CheckIcon, DownloadIcon, RefreshCwIcon, RotateCwIcon } from "lucide-react";import type { AnimationEventHandler } from "react"; import { cn } from "../../lib/utils"; const DOWNLOAD_PROGRESS_RADIUS = 14;const DOWNLOAD_PROGRESS_CIRCUMFERENCE = 2 * Math.PI * DOWNLOAD_PROGRESS_RADIUS; export type DesktopUpdateStatusIconState =  | "idle"  | "checking"  | "available"  | "downloading"  | "downloaded"; function normalizeDesktopUpdateDownloadPercent(percent: number | null): number {  if (percent === null || !Number.isFinite(percent)) return 0;  return Math.min(100, Math.max(0, percent));} export function shouldShowDesktopUpdateCheckIcon({  isAnimationLatched,  isChecking,  prefersReducedMotion,}: {  readonly isAnimationLatched: boolean;  readonly isChecking: boolean;  readonly prefersReducedMotion: boolean;}): boolean {  return isChecking || (isAnimationLatched && !prefersReducedMotion);} export function shouldContinueDesktopUpdateCheckAnimation({  isChecking,  prefersReducedMotion,}: {  readonly isChecking: boolean;  readonly prefersReducedMotion: boolean;}): boolean {  return isChecking && !prefersReducedMotion;} function DesktopUpdateAvailableIcon() {  return (    <span className="relative grid size-4 place-items-center">      <DownloadIcon className="size-4" />      <span        aria-hidden="true"        className="absolute -top-0.5 -right-0.5 size-1.5 rounded-full bg-current ring-2 ring-sidebar-control-surface"      />    </span>  );} function DesktopUpdateDownloadingIcon({ percent }: { readonly percent: number | null }) {  const normalizedPercent = normalizeDesktopUpdateDownloadPercent(percent);  const progressOffset = DOWNLOAD_PROGRESS_CIRCUMFERENCE * (1 - normalizedPercent / 100);   return (    <span className="relative grid size-8 place-items-center">      <svg        aria-hidden="true"        className="pointer-events-none absolute inset-0 size-full -rotate-90"        viewBox="0 0 32 32"      >        <circle          cx="16"          cy="16"          r={DOWNLOAD_PROGRESS_RADIUS}          fill="none"          stroke="color-mix(in srgb, currentColor 22%, transparent)"          strokeWidth="1.5"        />        <circle          cx="16"          cy="16"          r={DOWNLOAD_PROGRESS_RADIUS}          fill="none"          stroke="currentColor"          strokeDasharray={DOWNLOAD_PROGRESS_CIRCUMFERENCE}          strokeDashoffset={progressOffset}          strokeLinecap="round"          strokeWidth="1.5"          className="transition-[stroke-dashoffset] duration-300 ease-out motion-reduce:transition-none"        />      </svg>      <DownloadIcon className="size-4" />    </span>  );} function DesktopUpdateDownloadedIcon() {  return (    <span className="relative grid size-4 place-items-center">      <RotateCwIcon className="size-4" />      <span className="absolute -right-1 -bottom-1 grid size-2.5 place-items-center rounded-full bg-foreground text-background ring-2 ring-background">        <CheckIcon className="size-2" strokeWidth={3} />      </span>    </span>  );} export function DesktopUpdateStatusIcon({  downloadPercent,  isCheckAnimating,  onCheckAnimationIteration,  status,}: {  readonly downloadPercent?: number | null;  readonly isCheckAnimating?: boolean;  readonly onCheckAnimationIteration?: AnimationEventHandler<SVGSVGElement>;  readonly status: DesktopUpdateStatusIconState;}) {  if (status === "available") return <DesktopUpdateAvailableIcon />;  if (status === "downloading") {    return <DesktopUpdateDownloadingIcon percent={downloadPercent ?? null} />;  }  if (status === "downloaded") return <DesktopUpdateDownloadedIcon />;   return (    <RefreshCwIcon      className={cn("size-4", status === "checking" && isCheckAnimating && "animate-spin")}      onAnimationIteration={onCheckAnimationIteration}    />  );}