"use client"; import { Led } from "@unlocalhosted/metalui"; import { CalendarIcon, PersonIcon, TagIcon, XIcon } from "@/components/icons"; import { Avatar } from "@/components/ui"; import { ESTIMATES, PRIORITIES, STATUSES, formatDay, priorityLabel, statusLabel, type Status } from "@/lib/issue-meta"; import { LabelChip, LabelDot, PriorityIcon, StatusIcon } from "./issue-icons"; import { Picker } from "./picker"; import { useTracker } from "./tracker-context"; /** * How a property trigger looks: `icon` is a quiet ghost key for dense rows, `chip` a compact * raised cap (composer), `row` a full-width sidebar field that lifts like a list row. */ export type PropertyVariant = "icon" | "chip" | "row"; const TRIGGER: Record = { icon: "mu-icon-trigger relative z-10 inline-flex h-6 min-w-6 cursor-pointer items-center justify-center rounded-pill px-1 text-ink2 transition-[background-color,color] enabled:hover:recipe-icon-button-ghost-hover enabled:hover:text-ink aria-expanded:recipe-icon-button-ghost-hover focus-visible:focus-ring-flush", chip: "mu-icon-trigger inline-flex h-[26px] max-w-52 cursor-pointer items-center gap-1.5 rounded-pill px-2.5 type-ui text-ink recipe-button-compact transition-button-compact active:translate-y-button-travel active:recipe-button-compact-pressed aria-expanded:recipe-button-compact-pressed focus-visible:focus-ring", row: "mu-icon-trigger flex h-8 w-full min-w-0 items-center gap-2 rounded-row px-2 text-left type-ui text-ink transition-[background,box-shadow] enabled:cursor-pointer enabled:hover:recipe-row-list-hover aria-expanded:recipe-row-list-hover focus-visible:focus-ring-flush", }; type Control = { value: T; onChange: (value: T) => void; variant?: PropertyVariant; disabled?: boolean; open?: boolean; onOpenChange?: (open: boolean) => void; }; export function StatusPicker({ value, onChange, variant = "chip", ...rest }: Control) { return ( ({ value: s.id, label: s.label, icon: }))} value={value} onSelect={(v) => onChange(v as Status)} triggerClassName={TRIGGER[variant]} {...rest} > {variant !== "icon" && {statusLabel(value)}} ); } export function PriorityPicker({ value, onChange, variant = "chip", ...rest }: Control) { return ( ({ value: String(p.id), label: p.label, icon: }))} value={String(value)} onSelect={(v) => onChange(Number(v))} triggerClassName={TRIGGER[variant]} {...rest} > {variant !== "icon" && ( {value === 0 && variant === "chip" ? "Priority" : priorityLabel(value)} )} ); } /** Nobody: an empty sunk disc with the person glyph. */ function Unassigned() { return ( ); } export function AssigneePicker({ value, onChange, variant = "chip", ...rest }: Control) { const { members, viewerId } = useTracker(); const assignee = members.find((m) => m.id === value); // The viewer first, like Linear's "assign to me". const sorted = [...members].sort((a, b) => Number(b.id === viewerId) - Number(a.id === viewerId)); return ( }, ...sorted.map((m) => ({ value: m.id, label: m.id === viewerId ? `${m.username} (you)` : m.username, icon: , })), ]} value={value ?? ""} onSelect={(v) => onChange(v || null)} triggerClassName={TRIGGER[variant]} {...rest} > {assignee ? : } {variant !== "icon" && ( {assignee ? assignee.username : variant === "chip" ? "Assignee" : "Unassigned"} )} ); } export function LabelsPicker({ value, onChange, variant = "chip", ...rest }: Control) { const { labels, createLabel, canTriage } = useTracker(); const chosen = labels.filter((l) => value.includes(l.id)); return ( ({ value: l.id, label: l.name, icon: }))} value={value} onSelect={(v) => onChange(v)} onCreate={ canTriage ? async (name) => { const label = await createLabel(name); if (label) onChange([...value, label.id]); } : undefined } createLabel={(name) => `Create Label “${name}”`} triggerClassName={`${TRIGGER[variant]} ${variant === "row" ? "h-auto min-h-8 flex-wrap gap-1 py-1.5" : ""}`} {...rest} > {chosen.length === 0 ? ( <> {variant !== "icon" && ( {variant === "row" ? "Add label" : "Labels"} )} ) : variant === "row" ? ( chosen.map((l) => ) ) : ( <> {chosen.slice(0, 3).map((l) => ( ))} {chosen.length === 1 ? chosen[0].name : `${chosen.length} labels`} )} ); } export function EstimatePicker({ value, onChange, variant = "chip", ...rest }: Control) { return ( ({ value: String(e), label: `${e} point${e === 1 ? "" : "s"}` })), ]} value={value === null ? "" : String(value)} onSelect={(v) => onChange(v ? Number(v) : null)} triggerClassName={TRIGGER[variant]} {...rest} > {value === null ? (variant === "row" ? "No estimate" : "Estimate") : `${value} point${value === 1 ? "" : "s"}`} ); } /** Points: a diamond on the 24 grid at MetalUI's 1.7 stroke. */ export function EstimateIcon({ size = 14 }: { size?: number }) { return ( ); } /** A native date input dressed as a property; an overdue date gets the red LED beside it. */ export function DueDateField({ value, onChange, variant = "chip", disabled }: Omit, "open" | "onOpenChange">) { const { today } = useTracker(); const overdue = !!value && value < today; const hover = disabled ? "" : variant === "row" ? "cursor-pointer hover:recipe-row-list-hover" : "cursor-pointer"; return ( {value ? formatDay(value, variant === "row") : variant === "row" ? "No due date" : "Due date"} {overdue && ( Overdue )} {!disabled && ( onChange(e.target.value || null)} onClick={(e) => e.currentTarget.showPicker?.()} className="absolute inset-0 cursor-pointer opacity-0" /> )} {!disabled && value && variant === "row" && ( )} ); }