feat(filters): show a tag's own colour on its active filter chip (#185)

A tag carries a colour, and every place a tag appears shows it — a product card, the filter drawer's control, the admin taxonomy screen — except the removable chips beside the Filters button, which rendered every filter as a default grey. Picking `vintage` from a control that showed it in red produced a grey chip of the same name right next to it.

Only tags get a colour, because only tags have one. Category, price, favorites and status keep the default, and that asymmetry is the point: in a row mixing four kinds of filter, colour now means "this is a tag". Nothing depends on it — every chip still carries its label — so this reads the same to anyone who cannot distinguish the colours.

The close control inherits the tag's text colour, so a coloured chip gets a matching cross rather than a grey one on a coloured ground. A tag not yet in the loaded options has no colour to use and keeps the default, which is the same window the existing `Tag {id}` label fallback covers.

The test asserts the chip's colour equals the same tag's colour on a product card, rather than asserting it is red. The colour is derived from the tag's name and free to change; what must hold is that a tag looks like itself wherever it appears, and comparing the two places says that directly. Confirmed to fail without the change — the old grey chip sets no colour class at all.

Verified visually as well as by assertion: three tags selected together render in the drawer, in the chip row and on the card in the same colours.

Closes #185
This commit is contained in:
2026-08-25 14:05:34 -05:00
parent 3cffcf772c
commit d6e0942487
3 changed files with 60 additions and 1 deletions
+15 -1
View File
@@ -25,7 +25,11 @@ export default function ActiveFilterChips({
}: Props) {
if (!hasActiveFilters(filters)) return null;
const chips: { key: string; label: string; onRemove: () => void }[] = [];
// `color` only ever set for tags, which are the only filter with one. That
// makes colour in this row mean "this is a tag", which is a useful thing for
// a row mixing four kinds of filter to say — and nothing depends on it, since
// every chip still carries its label.
const chips: { key: string; label: string; color?: string; onRemove: () => void }[] = [];
// Listed first so it matches the drawer's ordering, and because it is the
// chip most worth noticing when a customer wonders why the grid looks short.
@@ -59,6 +63,11 @@ export default function ActiveFilterChips({
chips.push({
key: `tag-${tagId}`,
label: tag?.name ?? `Tag ${tagId}`,
// The same colour the drawer's control and the product cards show, so a
// tag looks like itself wherever it appears. Undefined while
// /api/filters is still loading, which is the case the label fallback
// above already covers — an uncoloured chip beats a missing one.
color: tag?.color,
onRemove: () => onChange({ ...filters, tagIds: filters.tagIds.filter((id) => id !== tagId) })
});
}
@@ -93,6 +102,11 @@ export default function ActiveFilterChips({
{chips.map((chip) => (
<Tag
key={chip.key}
// Undefined for every filter that has no colour of its own, which is
// antd's default rendering — the same as before this distinguished
// tags. The custom close icon below inherits the tag's text colour,
// so a coloured chip gets a matching cross rather than a grey one.
color={chip.color}
closable
onClose={(event) => {
event.preventDefault();