/** * The small parts every link widget is built from: the icon for what a link * stands for, a status as its shape, progress as a ring, the conversation * count with its unread dot, and the pickers. LinkChip, LinkRow and LinkCard * place them; an app may place them too. */ import { IconCircle, IconCircleCheck, IconCircleDashed, IconCircleHalf2, IconCircleX, IconFileText, IconMessage2 } from '@tabler/icons-react' import { Avatar } from './Avatar' import { cn } from './cn' import { KindIcon } from './KindIcon' import { Link } from './Link' import { chosenOf, type LinkedConversation, type LinkedKind, type LinkedPicker, type LinkedStatus, type StatusColour } from './linked' import { Select } from './Select' import { WithTooltip } from './Tooltip' import { UnreadDot } from './UnreadDot' const STATUS_ICON: Record = { neutral: { Icon: IconCircleDashed, tone: 'text-text-secondary' }, blue: { Icon: IconCircleHalf2, tone: 'text-info-default' }, green: { Icon: IconCircleCheck, tone: 'text-success-default' }, muted: { Icon: IconCircleX, tone: 'text-text-muted' }, } /** A status's shape alone, beside words that already name it. */ function StatusIcon({ colour }: { colour: StatusColour }) { const { Icon, tone } = STATUS_ICON[colour] return } /** A status as its shape, which reads without colour; its words on hover and to a screen reader. */ export function StatusMark({ status }: { status: LinkedStatus }) { return ( {status.label} ) } /** The icon for what a link stands for. A page of text has its own. */ export function LinkedIcon({ kind, size = 16, tone = 'secondary' }: { kind: LinkedKind; size?: 14 | 16; tone?: 'secondary' | 'inherit' }) { if (kind === 'page') return return } /** A key, the way people say it ("ABC-2"): before the title, quieter than it. */ export function LinkKey({ children }: { children: string }) { return {children} } /** Progress as a small ring, with the count beside it. */ export function ProgressRing({ done, total }: { done: number; total: number }) { const size = 16 const stroke = 2 const r = (size - stroke) / 2 const c = 2 * Math.PI * r const share = total > 0 ? Math.min(done, total) / total : 0 return ( {done}/{total} ) } /** * The count of what was said, a link to where it is read. With something * unread there the words are brighter and a dot follows. It sits above a * row's covering link, so the row opens the thing and the count its * conversation. */ export function CommentCount({ conversation, noun = 'comments' }: { conversation: LinkedConversation; noun?: string }) { const { count, href, unread = false } = conversation return ( {count} {unread ? `${noun}, unread — open the conversation` : `${noun} — open the conversation`} {unread && } ) } export interface LinkPickersProps { status?: LinkedPicker owner?: LinkedPicker /** The owner's word: "Assignee", "Lead". */ ownerLabel?: string /** Draws the values as words even where they could be changed: the hover preview. */ readOnly?: boolean } /** * A status and an owner, where the reader can change them from the card. A * picker whose app passed no `onChange` reads as words in the same place: the * reader may not change it. The hover preview is always words. */ export function LinkPickers({ status, owner, ownerLabel = 'Owner', readOnly = false }: LinkPickersProps) { const statusNow = chosenOf(status) const ownerNow = chosenOf(owner) const statusWords = status && (readOnly || !status.onChange) const ownerWords = owner && (readOnly || !owner.onChange) return ( {status && (statusWords ? ( {statusNow?.colour && } {statusNow?.label ?? 'No status'} ) : ( owner.onChange?.(value)} ariaLabel={ownerLabel} options={owner.options.map((option) => ({ value: option.value, label: option.label, leading: }))} /> ))} ) }