import { CircleNotchIcon, DownloadSimpleIcon, IconProps, } from '@phosphor-icons/react' import classNames from 'classnames' import React, { useCallback, useState } from 'react' import { triggerDownload } from './triggerDownload' import { VIEWER_ACTION_CLASS } from './viewerChromeClasses' export type DownloadActionVariant = /** Solid pill button used inside compact / file rows. */ | 'pill' /** * Compact round icon button sized for the trailing slot of a * `CompactDocumentRow` (PDF / File rows). Adopts the row's tone so * it sits inline next to the filename without competing with it. */ | 'inline' /** * Round icon button sized to match the close action in the * `ViewerShell` chrome — used by `ImageViewer` for in-viewer * downloads. Shares `VIEWER_ACTION_CLASS` with the close button. */ | 'viewer' export interface DownloadActionProps { url: string filename?: string variant?: DownloadActionVariant /** * Override the visible label on `pill` variants. Defaults to * `'Download'`. On `inline` / `viewer` variants the label is * hidden visually and surfaced as the button's `aria-label`. */ label?: string /** Hide the label, keeping just the icon. Defaults to `true` for non-pill variants. */ iconOnly?: boolean /** Tone of the surface. Used by the `pill` variant. */ tone?: 'dark' | 'light' /** * Triggered after the download starts so consumers can fire analytics * or close a viewer. Errors during download don't suppress the call. */ onTriggered?: () => void } const DownloadAction: React.FC = ({ url, filename, variant = 'pill', label = 'Download', iconOnly, tone = 'dark', onTriggered, }) => { const [busy, setBusy] = useState(false) const handleClick = useCallback( (e: React.MouseEvent) => { e.stopPropagation() if (busy) return setBusy(true) triggerDownload(url, filename) .catch(() => { /* swallowed — fallback path inside `triggerDownload` */ }) .finally(() => { setBusy(false) onTriggered?.() }) }, [busy, url, filename, onTriggered] ) const showIconOnly = iconOnly ?? variant !== 'pill' const iconClass = classNames( variant === 'pill' ? 'size-4' : 'size-5', 'shrink-0' ) const iconProps: IconProps = { className: iconClass, weight: 'bold' } if (variant === 'inline') { // Sized to match the existing trailing slot used by `DismissButton` // and the decorative download span in `FileAttachment`. Tone keys // off the surrounding `Bubble` variant so a sender (dark) bubble // gets a lighter icon and a receiver (light) bubble gets a darker // one — same approach `CompactDocumentRow` already uses for the // type icon. const inlineToneClasses: Record<'dark' | 'light', string> = { dark: 'text-white/70 hover:bg-white/[0.08] hover:text-white', light: 'text-black/70 hover:bg-black/[0.08] hover:text-black', } return ( ) } if (variant === 'viewer') { // Sits next to the close action inside `ViewerShell`'s top-right chrome // and shares its circular-button styling via `VIEWER_ACTION_CLASS`. return ( ) } // pill — only remaining variant return ( ) } export default DownloadAction