State label component
components specs/components/state-label.kmd
The status pill for a code-hosting entity (issue or pull request) whose value is drawn from a per-entity state machine, not a free tag. Distinct from a notification badge (count/dot) and a chip (user-editable filter): a state label is read-only, single-valued, and semantically colored by state. Modeled after GitHub Primer StateLabel. Used by Koder Flow issue lists, PR headers, dashboards, and any surface that shows VCS entity status.
When this spec applies
Primary triggers
- Display issue or pull-request status in a code-hosting surface
All triggers
- Show whether an issue/PR is open, closed, merged, or draft
- Render a status pill whose value comes from a fixed state machine
Specification body
Component — State label
Status: v0.1.0 — Draft. Promoted from the GitHub Primer parity scan (meta/docs/stack #092). Live URL once rendered:
kds.koder.dev/<locale>/components/components-state-label.html. Source: https://primer.style/components/state-label
R1 — What it is (and is not)
A state label is a read-only pill bound to ONE value from a fixed, entity-specific state machine (R2). It is NOT:
- a badge (
specs/components/badges.kmd) — that signals a count/dot; - a chip (
specs/components/chips.kmd) — that is user-editable / a filter input; - a free-text tag/label (issue labels are a separate, multi-valued, user-defined concept).
There is exactly one state label per entity instance.
R2 — State machines (closed sets)
The valid values and their semantics depend on the entity:
Issue
| State | Meaning | Semantic color | Icon |
|---|---|---|---|
Open | Active issue | success (open-green) | issue-opened |
Closed — completed | Resolved as done | done (purple) | issue-closed |
Closed — not planned | Closed without action | neutral (grey) | skip/circle-slash |
Pull request
| State | Meaning | Semantic color | Icon |
|---|---|---|---|
Draft | Not yet ready for review | neutral (grey) | git-pull-request-draft |
Open | Ready / in review | success (open-green) | git-pull-request |
Merged | Merged into target | done (purple) | git-merge |
Closed | Closed unmerged | danger (red) | git-pull-request-closed |
No other values exist. The state is derived from entity data, never set by hand in the view.
R3 — Anatomy
- Pill container with a leading icon + a text label.
- Sized in two scales:
small(inline, in lists/tables) anddefault(entity header). Both keep the icon+label pair. - Fill uses the state's semantic color at the established surface contrast; the label text meets WCAG AA on that fill.
R4 — Color is never the only signal
Each state is distinguished by icon + text label, not color alone (WCAG 1.4.1). The icon shape per row in R2 is mandatory; a state label without its icon is non-conformant.
R5 — Tokens
All fills, text, and icon colors come from Verge semantic tokens
(specs/themes/verge.kmd) — --kds-color-open, --kds-color-done,
--kds-color-danger, --kds-color-neutral (or their canonical names).
No raw hex. Light/dark are handled by the token layer, not by the
component.
R6 — Accessibility
- The label renders its state in words; the icon is decorative
(
aria-hidden) since the text carries meaning. - When used as the accessible name of an entity row, the state is part of that row's label ("Issue #214, Open").
- Non-interactive: a state label is not a button/link. If a surface makes the surrounding row clickable, the click target is the row, not the pill.
R7 — i18n
The visible state label is translated per specs/i18n/contract.kmd
("Open"→"Aberto", "Merged"→"Mesclado", etc.). The underlying state KEY
(used in code/filters) stays stable and language-invariant.
Não-escopo
- User-defined issue labels (multi-valued, colored tags) — separate.
- The state-transition rules themselves (owned by Koder Flow backend).
- Review-decision states (approved / changes-requested) — those belong to
the PR review surface (
specs/components/review spec, #138).
Requirements (testable)
Requirement: Read-only single value bound to one entity {#req-state-label-read-only-single-value}
A state label SHALL be read-only, single-valued, and bound to exactly one value derived from entity data; it SHALL NOT be user-editable, accept free text, or render more than one state per entity instance.
Scenario: Renders exactly one state per entity
- GIVEN an issue entity whose derived state is
Open - WHEN the state label is rendered for that entity
- THEN exactly one state label element is present
- AND its value is
Open
Scenario: State comes from entity data, not manual input
- GIVEN a state label bound to a pull-request entity
- WHEN the view is inspected for editable controls
- THEN the state label exposes no text input, editable field, or user-settable value
- AND the displayed state equals the value derived from the entity data
Requirement: Value drawn from a closed per-entity state machine {#req-state-label-closed-state-machine}
A state label SHALL only display a value from the fixed set defined for its entity type — Issue (Open, Closed — completed, Closed — not planned) or Pull request (Draft, Open, Merged, Closed) — each mapped to its established semantic color, and SHALL NOT render any value outside that set.
Scenario: Issue closed-not-planned maps to neutral
- GIVEN an issue entity whose derived state is
Closed — not planned - WHEN its state label is rendered
- THEN the visible label reads
Closed — not planned - AND its fill uses the neutral semantic color
Scenario: Pull request merged maps to done
- GIVEN a pull-request entity whose derived state is
Merged - WHEN its state label is rendered
- THEN the visible label reads
Merged - AND its fill uses the done semantic color
Scenario: Out-of-set value is rejected
- GIVEN a state label targeting an issue entity
- WHEN a value not in the issue state set (e.g.
Merged) is supplied - THEN the component does not render that value as a valid issue state
Requirement: State distinguished by icon plus text, never color alone {#req-state-label-icon-plus-text}
Each state SHALL be distinguished by both its mandatory per-state icon and its text label so meaning survives without color (WCAG 1.4.1); a state label rendered without its icon SHALL be treated as non-conformant.
Scenario: Icon and text both present
- GIVEN a pull-request state label with state
Open - WHEN it is rendered
- THEN a leading icon element is present
- AND a text label reading
Openis present
Scenario: Missing icon is non-conformant
- GIVEN a state label whose text label is present but whose leading icon is absent
- WHEN conformance is evaluated
- THEN the state label is flagged as non-conformant
Requirement: Accessible non-interactive state exposure {#req-state-label-accessibility}
The state label SHALL render its state in words with the icon marked decorative (aria-hidden) so the text carries the accessible meaning, and SHALL be non-interactive — it SHALL NOT be a button or link, and any surrounding clickable target belongs to the row, not the pill.
Scenario: Icon is decorative, text carries meaning
- GIVEN a state label with state
Closed - WHEN its accessibility tree is inspected
- THEN the icon is marked
aria-hidden - AND the accessible name conveys the word
Closed
Scenario: Pill is not an interactive target
- GIVEN a state label inside a clickable entity row
- WHEN the state label element is examined
- THEN it exposes no button or link role
- AND the click target is the surrounding row rather than the pill
Requirement: Translated visible label with stable state key {#req-state-label-i18n}
The visible state text SHALL be translated per the i18n contract while the underlying state KEY used in code and filters SHALL remain stable and language-invariant.
Scenario: Visible label translated, key unchanged
- GIVEN a state label whose state key is
Open - WHEN it is rendered in the
pt-BRlocale - THEN the visible text reads
Aberto - AND the state key exposed to code and filters remains
Open
References
specs/components/badges.kmdspecs/components/chips.kmdspecs/themes/verge.kmdspecs/i18n/contract.kmd