Give every tag a colour and an icon, drawn wherever the tag is #285

Closed
opened 2026-09-19 10:07:28 +00:00 by tiagoagueda · 0 comments
Owner

What is wrong today

A tag has had a colour field since the beginning (core/models.py, Tag.colour, "A hint for the interface, such as “amber” or “sky”") and the tag form offers it, so a person types a colour, saves it, and nothing anywhere draws it: the detail page, the board card, the table row, the filters and the tags list all render every tag as the same grey pill (bg-ink-100 text-ink-600). The field is a promise the interface never kept, and free text besides, so "amber", "Amber", "orange-ish" and "#f59e0b" are all accepted and all mean nothing.

A tag is the one thing in the application a person invents for themselves — the model says so, deliberately free-form — and on a board of forty cards it is the fastest way to tell "remote" from "relocation" from "referral". Forty identical grey pills do not do that job; a person reads each one.

What to do

Every tag gets a colour and an icon, chosen from fixed sets, and both are drawn wherever the tag is.

  • Colour becomes a choice, not free text: the five names the stylesheet already paints for plugin categories (tag-grey, tag-blue, tag-amber, tag-violet, tag-teal, assets/css/app.css around line 376) plus a few more, each pair tuned to 3:1 against the card in both themes as those were. A migration maps what people typed to the nearest name where it is one, and to grey where it is not. grey stays the default for a tag made from the API or from a bulk action, so an unstyled tag looks as it does today.
  • Icon is a new field, a choice from a short curated list of lucide names — a dozen or two that mean something in a job search: home (remote), map-pin (relocation), users (referral), star, flag, clock, heart, briefcase, graduation-cap, banknote, globe, shield… — added to assets/icons.txt and synced like the rest. Blank means no icon, and blank stays the default.
  • Drawn as icon + word in one colour, through one component (<c-tag>, since #263 made that the shape) so the six places that render a tag today render it once: application_detail.html, partials/application_card.html, partials/application_row.html (twice), tag_list.html, and the chips in the filters and the intake form. The word is always there, so colour and icon are second signals and never the only one — the rule #274 set for every cue in the interface, and the same rule partials/plugin_tags.html already states for the plugin pills. Forced colours (#277) keep the border and the icon and drop the fill, which is fine because the word is what carries the meaning.
  • The form shows the choice as swatches and icons, not as two selects of names, and shows the tag as it will look while it is edited. The list of tags shows each one as it looks.
  • The API keeps taking and giving tag names (ApplicationDetailsIn.tags, ApplicationOut.tags), because that is what an extension or a script means by a tag; a tag made through the API is grey and iconless until somebody dresses it. Whether the tag list endpoint should carry colour and icon is a small separate question — probably yes, read-only, so a client can draw them the same way.
  • The export carries both fields, and the import restores them, since a tag is the person's own vocabulary and the archive is meant to be the whole of it.

What it is not

Not a colour picker. A free hex value would need a contrast check per tag per theme, would break under forced colours in a way a fixed palette does not, and would give a person a decision they did not ask for. Not emoji either: the record is printed on a CV's cover page nowhere, but the board is screen-read, and an emoji reads as its Unicode name where a lucide icon is aria-hidden beside the word it decorates.

Where it touches

core/models.py (Tag), a migration, applications/forms.py (TagForm), templates/cotton/tag.html (new) and the six templates above, assets/css/app.css (the tag palette), assets/icons.txt, core/export.py and core/importer.py, the tests around tags and the accessibility contrast test (tests/test_contrast.py) for the new pairs, plus the browser walk, which visits the tag form and list already.

Classification

Enhancement, interface. Depends on nothing; #263 made the component the natural shape and #274/#277 set the rules it has to obey.

## What is wrong today A tag has had a `colour` field since the beginning (`core/models.py`, `Tag.colour`, "A hint for the interface, such as “amber” or “sky”") and the tag form offers it, so a person types a colour, saves it, and **nothing anywhere draws it**: the detail page, the board card, the table row, the filters and the tags list all render every tag as the same grey pill (`bg-ink-100 text-ink-600`). The field is a promise the interface never kept, and free text besides, so "amber", "Amber", "orange-ish" and "#f59e0b" are all accepted and all mean nothing. A tag is the one thing in the application a person invents for themselves — the model says so, deliberately free-form — and on a board of forty cards it is the fastest way to tell "remote" from "relocation" from "referral". Forty identical grey pills do not do that job; a person reads each one. ## What to do **Every tag gets a colour and an icon, chosen from fixed sets, and both are drawn wherever the tag is.** - **Colour** becomes a choice, not free text: the five names the stylesheet already paints for plugin categories (`tag-grey`, `tag-blue`, `tag-amber`, `tag-violet`, `tag-teal`, `assets/css/app.css` around line 376) plus a few more, each pair tuned to 3:1 against the card in both themes as those were. A migration maps what people typed to the nearest name where it is one, and to grey where it is not. `grey` stays the default for a tag made from the API or from a bulk action, so an unstyled tag looks as it does today. - **Icon** is a new field, a choice from a short curated list of lucide names — a dozen or two that mean something in a job search: `home` (remote), `map-pin` (relocation), `users` (referral), `star`, `flag`, `clock`, `heart`, `briefcase`, `graduation-cap`, `banknote`, `globe`, `shield`… — added to `assets/icons.txt` and synced like the rest. Blank means no icon, and blank stays the default. - **Drawn as `icon + word` in one colour**, through one component (`<c-tag>`, since #263 made that the shape) so the six places that render a tag today render it once: `application_detail.html`, `partials/application_card.html`, `partials/application_row.html` (twice), `tag_list.html`, and the chips in the filters and the intake form. The word is always there, so **colour and icon are second signals and never the only one** — the rule #274 set for every cue in the interface, and the same rule `partials/plugin_tags.html` already states for the plugin pills. Forced colours (#277) keep the border and the icon and drop the fill, which is fine because the word is what carries the meaning. - **The form** shows the choice as swatches and icons, not as two selects of names, and shows the tag as it will look while it is edited. The list of tags shows each one as it looks. - **The API** keeps taking and giving tag *names* (`ApplicationDetailsIn.tags`, `ApplicationOut.tags`), because that is what an extension or a script means by a tag; a tag made through the API is grey and iconless until somebody dresses it. Whether the tag list endpoint should carry colour and icon is a small separate question — probably yes, read-only, so a client can draw them the same way. - **The export** carries both fields, and the import restores them, since a tag is the person's own vocabulary and the archive is meant to be the whole of it. ## What it is not Not a colour picker. A free hex value would need a contrast check per tag per theme, would break under forced colours in a way a fixed palette does not, and would give a person a decision they did not ask for. Not emoji either: the record is printed on a CV's cover page nowhere, but the board is screen-read, and an emoji reads as its Unicode name where a lucide icon is `aria-hidden` beside the word it decorates. ## Where it touches `core/models.py` (Tag), a migration, `applications/forms.py` (TagForm), `templates/cotton/tag.html` (new) and the six templates above, `assets/css/app.css` (the tag palette), `assets/icons.txt`, `core/export.py` and `core/importer.py`, the tests around tags and the accessibility contrast test (`tests/test_contrast.py`) for the new pairs, plus the browser walk, which visits the tag form and list already. ## Classification Enhancement, interface. Depends on nothing; #263 made the component the natural shape and #274/#277 set the rules it has to obey.
tiagoagueda added this to the 0.5.0 milestone 2026-09-19 10:07:28 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Postulo/postulo#285
No description provided.