Tones
Four tones and the absence of one. A badge with no tone wears the skin's quiet surfaces; the four named ones are PctTone, the same list the toast and the button read, and every word over every box is a measured pair on both themes.
A short word wearing a tone: a status, a count, a label beside something that already has a name.
@pacit/components/badgepct-badgeNo APG pattern applies, and none is invented — the reasoning is under Accessibility ↓
import { Component } from '@angular/core';
import { PctBadge } from '@pacit/components/badge';
/** A word wearing a tone — and the tone is the library's four, never a list of its own. */
@Component({
selector: 'demo-badge',
imports: [PctBadge],
styles:
':host { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; }',
template: `
<pct-badge>Draft</pct-badge>
<pct-badge tone="success">Paid</pct-badge>
<pct-badge tone="danger">3 overdue</pct-badge>
`,
})
export class BadgeDemo {}badge.demo.ts — the same file runs here and is highlighted under Code
One import per component, and the shortest possible use — the package itself is installed once, on the start page.
import { PctBadge } from '@pacit/components/badge';| Exported | What it is |
|---|---|
PctBadgecomponentpct-badge | A badge: a word wearing a tone — Draft, Active, Overdue. |
<pct-badge>Draft</pct-badge>Each example is the component itself, not a screenshot. Open the code under any of them.
Four tones and the absence of one. A badge with no tone wears the skin's quiet surfaces; the four named ones are PctTone, the same list the toast and the button read, and every word over every box is a measured pair on both themes.
Where a tone earns its keep: one column, one word per row, and the colour repeating what the word already says. The word is what a screen reader reads — the tone is for the eye that scans the column and stops at the red one. The last row binds null, which is how a badge asks for no tone at all.
| Invoice | State |
|---|---|
| #1043 | |
| #1044 | |
| #1045 | |
| #1046 | |
| #1047 |
Read from the source at build time — the types, the defaults and the lines of documentation are the library's own.
A badge: a word wearing a tone — Draft, Active, Overdue.
| Name | Type | What it does |
|---|---|---|
tonedefault null | PctTone | null | The tone the word wears, or null for none — the default, and the quiet box the skin's own surfaces paint. It repeats the text, never replaces it. |
(click), (focus), the form's submit — are the platform's, and two-way models above carry the rest. The hooks a stylesheet or a test can hold. Stable by contract — the parts inventory gate fails the build when one disappears.
| Attribute | Bound to | Note |
|---|---|---|
class | pct-badge | |
data-pct-tone | tone() |
Two ways in, both stable. No class names to depend on.
In a test, PctBadgeHarness from @pacit/components/testing reads a part by the name below and a state by its attribute — the harness gate holds the names to the package.
No parts of its own — the element you write is the whole of it.
Every value the component reads, with the light and the dark default. Override a token at any scope and it cascades like any custom property.
| Token | Styles |
|---|---|
--pct-badge-bg{pct.surface-100}#f1f5f9 · #1e293b | Background of the badge. |
--pct-badge-bg-danger{pct.danger}#b91c1c · #f87171 | Background of the badge in the danger tone. |
--pct-badge-bg-info{pct.info}#2563eb · #60a5fa | Background of the badge in the info tone. |
--pct-badge-bg-success{pct.success}#15803d · #4ade80 | Background of the badge in the success tone. |
--pct-badge-bg-warning{pct.warning}#b45309 · #fbbf24 | Background of the badge in the warning tone. |
--pct-badge-border{pct.border}#e2e8f0 · #334155 | Edge colour of the badge. |
--pct-badge-border-danger{pct.danger}#b91c1c · #f87171 | Edge colour of the badge in the danger tone. |
--pct-badge-border-info{pct.info}#2563eb · #60a5fa | Edge colour of the badge in the info tone. |
--pct-badge-border-success{pct.success}#15803d · #4ade80 | Edge colour of the badge in the success tone. |
--pct-badge-border-warning{pct.warning}#b45309 · #fbbf24 | Edge colour of the badge in the warning tone. |
--pct-badge-fg{pct.text}#0f172a · #ffffff | Text colour of the badge. |
--pct-badge-fg-danger{pct.on-danger}#ffffff · #000000 | Text colour of the badge in the danger tone. |
--pct-badge-fg-info{pct.on-info}#ffffff · #0f172a | Text colour of the badge in the info tone. |
--pct-badge-fg-success{pct.on-success}#ffffff · #000000 | Text colour of the badge in the success tone. |
--pct-badge-fg-warning{pct.on-warning}#ffffff · #000000 | Text colour of the badge in the warning tone. |
--pct-badge-font-size{pct.font.size.sm}13px | Font size of the badge. |
--pct-badge-font-weight{pct.font.weight.medium}500 | Font weight of the badge. |
--pct-badge-gap{pct.space.2}4px | Gap between the badge's text and its icon. |
--pct-badge-padding-x{pct.space.2}4px | Horizontal padding of the badge. |
--pct-badge-radius{pct.radius.md}8px | Corner radius of the badge. |
A brand override in a few declarations. Scope it to a theme attribute, a page or one container — the component never notices.
[data-theme='brand'] {
--pct-badge-bg: #ccfbf1;
--pct-badge-fg: #134e4a;
--pct-badge-border: #99f6e4;
}Pattern: none — a badge is text and only text (0053): the word is projected, already part of the document's sentence, and the component adds a box around it and nothing audible to it. No role, no ARIA, no string of its own. Named in the class JSDoc.
| key | effect | test |
|---|
Empty by design: a badge is running text, holds no control, and takes no focus — the platform provides nothing here because there is nothing to provide (req-api-platform).
Every line is a test in three engines or a gate on every commit. The gaps are listed the same way, each with the requirement that will close it.
libs/components/badge/src/badge.tsaria-hidden-focus family has nothing to bite on a host that hides nothing and holds no controlapps/sandbox-e2e/src/a11y.spec.ts — /badge in SBX_ROUTESapps/sandbox-e2e/src/visual.spec.ts — badge-tones and badge-tones-rtlforced-colors: active — no state carried by colour alonelibs/components/badge/src/badge.scss — every tone drops to CanvasText on Canvas with the border standing, which is the component's own argument made visible. One :host rule inside the media block is enough, and the instrument for that is static: a tone rule here assigns --pct-badge-* and paints no property of its own, so check-styles point 7 finds nothing of a higher specificity for the block to lose to. No e2e case could say this — Chromium and Firefox substitute colours in the mode whichever rule won, and webkit is excluded from the suite (lesson-70). What apps/sandbox-e2e/src/forced-colors.spec.ts does read is the other half: every tone arrives at the palette's one word, and the edge is still drawnprefers-reduced-motion — duration from a tokennot applicable — no motion at all--pct-control-height-*deliberately not: a badge is not a control and stands on no control axis — apps/sandbox-e2e/src/badge.spec.ts measures the box UNDER the smallest control height, and that reading is the promisedir="rtl" screenshotlibs/components/badge/src/badge.scss — logical padding and a flex gap, nothing directional. The screenshot: badge-tones-rtlNG05xxapps/sandbox-e2e/src/hydration.spec.ts — /badge in SBX_ROUTEStools/check-parts.mjs counts zero and the README table carries the entrypointcontrast.policy.jsonlibs/tokens/src/contrast.policy.json — fourteen entries: every word over its box (AA error), every toned box and its edge against the page (UI error — the box IS the statement), the untoned edge (UI warn); libs/tokens/tokens.snapshot.mdPCT_TEXTSnot applicable — the component draws no text of its ownlibs/components/size.snapshot.md — ./badge reaches for no other entrypoint at all: PctTone comes in under import type, which the build erases, so the internal-deps column stays - and the row moved on its own bytes only. No PCT_TEXTS key either, so not one other row of the snapshot moveddocs/acr/at/ — three readers, whole sandbox, and /badge is the one view whose walk found no stop of its own at all: every stop in the three logs belongs to the sandbox scaffold. A badge takes no focus, which is the point of it, so a Tab walk cannot reach one and cannot confirm silence either. What would: a reader reading the page rather than walking itapps/sandbox/src/app/views/badge/ (the sandbox view). The published site: /components/badge — prerendered, the demo's own source is the code tablibs/components/badge/src/badge.spec.ts — 8 cases; libs/components/mutation.snapshot.md — badge.ts measured in the same full run as the avatar's rowRead at build time from the tracked snapshots, not typed here.
3 on a bell's corner is a different component — anchoring, a live region for the changing number, truncation — and it does not exist yet.tone="danger" beside the text OK is a page lying to its users in a way no component can see; the contract is prose, and the empty-badge warning is its enforceable corner.size input. A badge is sized by its own type token; one that wants a control's height is a button or a chip wanting to be quiet.