All files / src/components/projects ProjectFacets.tsx

100% Statements 5/5
100% Branches 8/8
100% Functions 3/3
100% Lines 5/5

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88                                                                                81x                           324x 324x 324x         6x                                                    
import { cn } from "../../lib/utils";
import { PROJECT_STATUS_FACETS, type ProjectStatus } from "./project-status";
 
/**
 * The projects list's status facets: `ALL 12  LIVE 8  DRAFT 3  ARCHIVED 1`.
 *
 * A facet is a `.label` CODE WITH A COUNT, not a pill and not a dropdown —
 * `.context/mockups/projects-list.html`, `.facet` in `_base.css`. The pressed
 * facet is marked by ink and ground, never by index red: selection is state,
 * and index red is never used for state.
 *
 * WHY NOT `FacetedFilter`
 * That primitive is a multi-select inside a popover with checkboxes and a
 * `bg-primary-500` count pill. The mockup prints the four codes flat on the
 * toolbar so the whole shape of the portfolio is readable without a click, and
 * makes them single-select — which is what made archived reachable at all:
 * the old default selection was `{draft, published}`, so an archived project
 * had no page where it appeared and its unarchive action was unreachable.
 */
 
export interface ProjectFacetsProps {
	/** `""` is the All facet. */
	value: "" | ProjectStatus;
	onChange: (value: "" | ProjectStatus) => void;
	/** How many projects each facet matches. `all` is the unfiltered total. */
	counts: {
		all: number;
		draft: number;
		published: number;
		archived: number;
	};
	className?: string;
}
 
export function ProjectFacets({
	value,
	onChange,
	counts,
	className,
}: ProjectFacetsProps) {
	return (
		// `fieldset` rather than `div role="group"`: native grouping semantics,
		// and Biome's useSemanticElements rule rightly rejects the ARIA stand-in
		// when an element exists. `min-w-0` defeats fieldset's min-content
		// sizing quirk inside a flex row; the border/padding/margin resets undo
		// the UA stylesheet.
		<fieldset
			className={cn(
				"m-0 flex min-w-0 flex-wrap items-center gap-0.5 border-0 p-0",
				className,
			)}
		>
			<legend className="sr-only">Filter by status</legend>
			{PROJECT_STATUS_FACETS.map((facet) => {
				const count = facet.value === "" ? counts.all : counts[facet.value];
				const isActive = facet.value === value;
				return (
					<button
						key={facet.value || "all"}
						type="button"
						aria-pressed={isActive}
						onClick={() => onChange(facet.value)}
						className={cn(
							// A 32px dense control: the toolbar tier.
							"inline-flex h-8 items-center gap-1.5 rounded-sm px-2.5",
							"text-xs font-semibold uppercase tracking-wider",
							"transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500",
							isActive
								? "bg-background-muted text-foreground-default"
								: "text-foreground-muted hover:bg-foreground-default/7 hover:text-foreground-default",
						)}
					>
						{facet.label}
						<span
							className={cn(
								"tabular-nums",
								isActive ? "text-foreground-default" : "text-foreground-subtle",
							)}
						>
							{count}
						</span>
					</button>
				);
			})}
		</fieldset>
	);
}