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 | 160x 630x 630x 3x | import { cn } from "../../lib/utils";
/**
* A row of single-select facets — `.facet` from `.context/mockups/_base.css`
* §Controls, in Tailwind.
*
* WHAT IT REPLACES
* The admin library pages filtered by category with a row of filled
* `rounded-full` chips. A full-radius filled chip is a pill, and the Job Book
* has no pills: the printed vocabulary is a 2px-radius `.label` in caps on a
* transparent ground, tinting on hover, and marking the selected one with a
* 2px inset underline in page ink. Index red is NOT used here — it marks the
* current item as a 3px inset bar and nothing else, and a filter is not the
* current item.
*
* `aria-pressed` carries the selection, which is what the mockup's
* `.facet[aria-pressed="true"]` selector keys off and what a screen reader
* needs to hear. A filled ground alone would have said it only in colour.
*
* The group is a real `<fieldset>` with an `sr-only` `<legend>` rather than a
* `div[role="group"]`: same semantics, the element that means it.
*
* Single-select and exclusive by design — `FacetedFilter` in `components/ui`
* is the multi-select dropdown and a different control.
*/
export interface FacetOption {
value: string;
label: string;
/** Right-aligned tabular count, when there is something to count. */
count?: number;
}
export interface FacetRowProps {
/** The group's accessible name, e.g. "Filter by category". */
label: string;
options: readonly FacetOption[];
value: string;
onChange: (value: string) => void;
className?: string;
}
export function FacetRow({
label,
options,
value,
onChange,
className,
}: FacetRowProps) {
return (
<fieldset
className={cn("flex flex-wrap items-center gap-1", className)}
>
<legend className="sr-only">{label}</legend>
{options.map((option) => {
const isActive = option.value === value;
return (
<button
key={option.value}
type="button"
aria-pressed={isActive}
onClick={() => onChange(option.value)}
className={cn(
"ctl-32 inline-flex min-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-1 focus-visible:ring-foreground-default",
isActive
? "bg-foreground-default/7 text-foreground-default shadow-[inset_0_-2px_0_var(--foreground-default)]"
: "text-foreground-muted hover:bg-foreground-default/7 hover:text-foreground-default",
)}
>
{option.label}
{option.count !== undefined && (
<span
className={cn(
"tabular-nums",
isActive ? "text-foreground-default" : "text-foreground-subtle",
)}
>
{option.count.toLocaleString("en-IN")}
</span>
)}
</button>
);
})}
</fieldset>
);
}
|