import type { IntentName } from "@charcuterie/tokens" import type { ReactNode } from "react" import { useState } from "react" import { Button } from "../Button/Button.tsx" import type { CardAccentEdge } from "../Card/cardAccentEdge.ts" import { getAccentEdgeClassName, getAccentEdgeStyle, } from "../Card/cardAccentEdge.ts" import { FOCUS_RING_CLASS, INTENT_SOLID_FILL_CLASS, } from "../intentStyles.ts" import type { MenuProps } from "../Menu/Menu.tsx" import { Menu } from "../Menu/Menu.tsx" import { TextLink } from "../TextLink/TextLink.tsx" import { toClassName } from "../toClassName.ts" import { VisuallyHidden } from "../VisuallyHidden/VisuallyHidden.tsx" type BoardItemFields = { /** What the bar means, in words. "Priority 0", "Movies and Shows". */ accentLabel?: string /** * Under the title and metadata, spanning the card: a live run * line, a gate explanation, a due date. A `ProgressBar` goes here. */ footer?: ReactNode /** * Visibly present but not actionable — Docket's *gated*: a task * whose phase has not opened yet. Paint only; it does not disable * the title, because being able to read a thing you cannot start * yet is the entire point of showing it. */ isDimmed?: boolean key: string /** * Small status marks rendered immediately before the title — * running, blocked, stale. Each one needs its own accessible name; * a bare coloured dot is the same WCAG 1.4.1 failure as the bar. */ marks?: ReactNode /** * Chips. Under the title when the lane is narrow, right-aligned * beside it when there is room. `Badge` is what goes here. */ meta?: ReactNode /** * The card's name, in **words**. * * Always a plain string, even when `titleContent` draws something * richer, because this is what the move handle and the drag * announcements are named after — and a control named after a * `ReactNode` is a control with no name at all. */ title: string /** The trailing cluster: an assignee avatar, an elapsed-time chip. */ trailing?: ReactNode /** * Extra actions in the card's existing menu, after the lane moves. * A separator is added when both sets have entries, so a consumer * can add a card action without creating a second trigger. */ menuItems?: MenuProps["items"] } /** * The leading colour bar, in one of two shapes — and the second one * exists because the first cannot follow a corner. * * `accentIntent` is an inset pill: a `w-1` span inside the card, * beside the content. It was built for priority — *"Priority is * styled as a leading colour bar, and **never** a coloured word * before the title."* An intent is a claim the design system makes * (`danger` says what happens if you press the thing), so this arm * is for a bar that means a STATE. * * `accentEdge` is the card's own edge — `Card`'s treatment, the same * pseudo-element, so it takes `border-radius: inherit` and wraps the * corner at `cq-lg` where this card grows one. It carries a * CATEGORICAL colour or a computed one, which is what an identity * needs: a project, a repo, a source. The owner asked for exactly * this on Docket's board — *"The Board has no colors. That could * use project colors on the sides at least."* — after reporting the * same treatment missing from the task rows. * * The two are mutually exclusive by type, not by convention. Both * paint a leading bar in the same place, and a card wearing two of * them is a card whose leading edge means two things. * * ⚠️ Either arm pairs with `accentLabel`. A bar is colour and * nothing else, so on its own it fails WCAG 1.4.1 outright and is * invisible to every screen reader; the label rides along in a * `VisuallyHidden` so the same fact reaches both channels. Docket's * first board painted its bars `--color-danger-9` — a Radix-style * scale this token set has never had — so every bar was transparent * while every "is it rendered" assertion passed. */ type BoardItemAccent = | { accentEdge?: never; accentIntent?: IntentName } | { accentEdge: CardAccentEdge; accentIntent?: never } /** * Who owns the card title's link — the board, or the caller. * * A union rather than three loose optionals, because the two halves * genuinely cannot coexist. `href` wraps the title in a `TextLink` * and `onSelect` wraps it in a ` ) : ( titleBlock )} {item.meta ? (
{item.meta}
) : null} {item.trailing || hasMenu ? (
{item.trailing} {hasMenu ? ( { setIsMenuVisible(false) }} placement="bottom-end" trigger={ } /> ) : null}
) : null} {item.footer ? (
{item.footer}
) : null} ) }