@guildofgleks/ui 21.7.2 → 21.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,413 @@ All notable changes to `@guildofgleks/ui` are documented here. Format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.0.0/); this project has not yet
5
5
  reached 1.0, so breaking changes may land in minor versions.
6
6
 
7
+ ## [21.9.0] - 04.09.2026
8
+
9
+ ### Added
10
+
11
+ - **`gog-button` and `[gogButton]` gain `severity`.** `severity` says what the action means;
12
+ `variant` says how loudly it is drawn. The two are orthogonal, which is the whole design: this
13
+ is not a fifth variant but a re-pointing of the colours all four are built from, so
14
+ `variant="ghost" severity="danger"` is a quiet delete and `variant="primary" severity="danger"`
15
+ a loud one. `'accent'` is the default and the absence of a claim — an existing button emits no
16
+ new class and renders byte-for-byte as before. The `GogSeverity` union is shared with
17
+ `gog-progressbar`, whose `GogProgressbarVariant` is now an alias of it.
18
+
19
+ Two colour rules came out of measuring rather than choosing. A **filled** severity button's
20
+ label is `--gog-<status>-text-color`, and hover and press deepen the fill *away* from it via
21
+ `--gog-<status>-shade`, so every state makes the label easier to read rather than harder — the
22
+ first attempt deepened toward the page's ink unconditionally and cost `primeng`'s info button
23
+ 6.44:1 down to 4.23:1 on press, because there the label *is* the ink. A **transparent** one's
24
+ label is `--gog-button-<status>-ink`, the status hue mixed halfway toward the ink, because the
25
+ raw hue is legible body text in only five of the eleven shipped themes; the 50% is the binding
26
+ case (`material`'s amber) rather than a round number. All four severities, four variants and
27
+ every state are gated by `check:contrast` — 1155 pairs across 11 themes, all passing.
28
+
29
+ - **A status colour is three tokens now, not one.** `--gog-<status>-color` is the fill;
30
+ `--gog-<status>-text-color` is the label that reads on it, the way `--gog-accent-text-color`
31
+ already did for the accent; and `--gog-<status>-shade` is the direction hover and press deepen
32
+ in. Both new ones default to the accent's answer, so a theme states them only where its own hue
33
+ disagrees — and the defaults are what nine of the eleven shipped presets still use. If you write
34
+ a theme, this is the contract to know: setting a status colour alone is how a bright amber ends
35
+ up under white text, which is exactly the defect in the Fixed section below.
36
+
37
+ - **`gog-scroll` gains `horizontalWheel`, so a vertical wheel can scroll a horizontal row.**
38
+ Hover a horizontal-only region, turn the wheel, and the page moves instead — the browser's own
39
+ behaviour, and the thing consumers report about this component most. The component draws an
40
+ overlay thumb over native scrolling and had deliberately never touched wheel handling; this is
41
+ the narrow exception, opt-in.
42
+
43
+ It acts only when all of this holds: the viewport cannot scroll vertically (checked against
44
+ live geometry, not the `axis` input, so `axis="both"` still scrolls down while there is down to
45
+ go), the event carries no horizontal delta of its own (a trackpad swipe and `Shift`+wheel
46
+ already work and translating on top would double them), `ctrlKey` is clear (pinch-zoom), and
47
+ there is room left in the direction of the turn. **The last one is most of the feature:** at the
48
+ content's end the event is left alone, so the page picks it up exactly as before. A region that
49
+ swallowed the wheel at its own end would leave the page feeling stuck, which is a worse bug than
50
+ the one being fixed. `overscrollBehavior: 'contain'` still contains — that boundary is the
51
+ browser's and this never reaches past it.
52
+
53
+ Off by default, because it changes what an existing instance does with a gesture it currently
54
+ passes on; `GOG_CONFIG.scroll.horizontalWheel` turns it on app-wide. A line-mode delta (what
55
+ Firefox sends) is scaled rather than applied raw, which would have moved the content three
56
+ pixels per notch.
57
+
58
+ - **`gog-chip` gains `selected` — the filter chip.** A row of chips you toggle on and off could
59
+ not be built from this component: it had `clickable` and `removable`, so a chip could be pressed
60
+ or dismissed, but nothing said "this one is on". `[(selected)]` is that, and it is tri-state
61
+ because the alternative would have changed every chip that already ships. `null`, the default,
62
+ is not a toggle at all — no `aria-pressed`, no selected look, activation emits `gogClick` and
63
+ nothing else. `false` is a toggle that is off, and states it: a chip with no `aria-pressed` is
64
+ not a toggle to a screen reader, so "off" has to be said rather than left absent. `true` draws
65
+ an inset ring, `--gog-chip-selected-shadow`.
66
+
67
+ The look and the semantics ship together on purpose, which is the whole reason this waited:
68
+ forwarding `aria-pressed` alone would have let a chip announce itself as on while looking
69
+ identical to an off one — WCAG 1.4.1 from the other side, and the exact trap `gog-button` was
70
+ in between 21.8.0 and 21.9.0. A ring rather than a fill for the same reason as the button's:
71
+ `:hover` and `:active` already own this surface's background, so a selected chip painted with a
72
+ fill would lose the one thing saying it is on the moment the pointer arrived.
73
+
74
+ It is a `model`, so the chip flips it on click, Enter and Space and a filter row needs no click
75
+ handler. `gogClick` still fires, after the flip, so a handler reading `selected()` sees the new
76
+ value. A `disabled` chip keeps the ring and drops `aria-pressed`, which needs the `role="button"`
77
+ a disabled chip does not carry — "on, and currently unavailable" is a real state, and hiding it
78
+ would leave it announced and invisible. `check:contrast` gained the ring against both the hover
79
+ and the press background, at 3:1 (WCAG 1.4.11, a boundary rather than text); all 11 themes pass.
80
+
81
+ - **`GOG_CONFIG.spinner.component` — one line replaces every loading indicator in the library.**
82
+ Passing a component, not a value: it is rendered through `NgComponentOutlet` in place of the
83
+ built-in look, wherever the library draws a spinner. That includes the two places a consumer
84
+ could not reach at all — `gog-button` and `gog-autocomplete` render `<gog-spinner>` from their
85
+ own templates and expose no input for it, so a house loader had no way in. It sits inside the
86
+ same size wrapper as the built-ins, keeping the sizing, the overlay behaviour, `role="status"`
87
+ and the accessible name; only the visual changes.
88
+
89
+ Precedence is the library's usual instance-then-config-then-default, and the wrinkle is stated
90
+ because it is the one people will ask about: an instance's own `variant` outranks both config
91
+ keys, so `<gog-spinner variant="ring">` stays a ring in an app that has set a component.
92
+ `spinner.variant` is there for an app that only wants to switch between the two built-ins.
93
+
94
+ `variant` is therefore `GogSpinnerVariant | undefined` now, resolving through the same
95
+ `resolveConfigured` chain as every other configurable input. `variant="custom"` with projected
96
+ content is unaffected.
97
+
98
+ ### Fixed
99
+
100
+ - **Leading is a character axis too: `--gog-line-height-none|tight|snug|normal|relaxed|loose`.**
101
+ Twenty component tokens held a bare number — seven of them the same `1.4` — so roomier text
102
+ meant finding and re-listing every one. Same values, nothing moves. `gog-panel`'s heading keeps
103
+ its own `1.25`, off the scale on purpose the way an 11px chip is off the type scale, and rule G
104
+ covers the family so the next bare number fails the build.
105
+
106
+ - **`--gog-control-clear-icon-ratio`: one number, not five.** The clear (×) button's glyph is the
107
+ same part in `gog-autocomplete`, `gog-datepicker`, `gog-inputfield`, `gog-multiselect` and
108
+ `gog-select`, and each carried its own `0.7`. `gog-textarea` keeps its own `1`: its clear
109
+ button sits in a corner rather than in the field's icon row, which is a different problem and
110
+ a different number.
111
+
112
+ - **Weight is a character axis now: `--gog-font-weight-medium|semibold|bold|heavy`.** Fifteen
113
+ component tokens held a bare `500`/`600`/`700`/`900`, so a house style that wanted lighter
114
+ chrome had to find and re-list every one — 21.7.0's character layer unified casing and tracking
115
+ and stopped short of this. Same values, nothing moves; what changes is that four tokens now
116
+ reach all fifteen. Four steps because four are used: no `normal`, since nothing in the library
117
+ paints 400 and an unused token is API nobody asked for. `check-tokens` rule G covers the family,
118
+ so the next bare number fails the build.
119
+
120
+ - **`--gog-z-base`: the whole stacking order moves as one.** `badge`, `toast`, `dropdown`
121
+ (which dialogs and menus read), `tooltip` and the blocking `spinner-overlay` were five
122
+ unrelated literals — 1, 100, 300, 400, 8000 — so an app that had to lift the library above its
123
+ own chrome edited five tokens and hoped it had found them all. Each is `calc(var(--gog-z-base) +
124
+ N)` now: the numbers are unchanged at `--gog-z-base: 0`, and setting it to 10000 gives
125
+ 10001/10100/10300/10400/18000 — the same order, one number. The gaps are deliberate, so an app
126
+ can still slot its own element between two library layers.
127
+
128
+ - **One disabled level, not four.** `--gog-disabled-opacity` (0.4) is read by nine components,
129
+ while `gog-accordion`, the `gog-select` and `gog-multiselect` option rows (0.5) and `gog-chip`
130
+ (0.55) each carried their own. One state should not have four opinions, and the three
131
+ stragglers were invisible to rule G by construction — it flags a literal only when the value
132
+ *matches* the token's. **This is a visual change**: those four fade slightly further now.
133
+
134
+ - **The type scale gained the step it was missing: `--gog-text-slg` (1.25rem).** `gog-button` and
135
+ the field controls both needed a size for their `slg` variant, the scale went straight from
136
+ 1.125rem to 1.5rem, and both wrote `1.25rem` as a literal — the same value chosen twice,
137
+ independently, which is a missing rung rather than two opinions. Both read the token now, and
138
+ rule G lists it, so the next component that wants 20px type cannot re-invent it. Named for the
139
+ control size it serves rather than continuing the t-shirt run, because that is what asked for
140
+ it.
141
+
142
+ - **Sixteen component font sizes now read the type scale, and rule G covers it.** A theme that
143
+ retuned `--gog-text-*` moved most of the library and left `gog-button`, `gog-chip`, `gog-tag`,
144
+ the dialog's close button, the toast's action and close, and the toggle's `lg` state label
145
+ behind — each held a literal that was byte-for-byte a scale step (`--gog-button-md-font-size:
146
+ 1rem` sitting beside `--gog-text-md: 1rem`). Same values, so nothing moves in any theme; the
147
+ difference is that retuning the scale now reaches them.
148
+
149
+ `check-tokens` rule G covered radii, strokes, casing and tracking but **not** font size, which
150
+ is how sixteen of them accumulated. It does now, and the thirteen remaining literals are right
151
+ to be literals: five are the accordion chevron's px ramp, and eight are off the scale on
152
+ purpose — `slg` is 1.25rem in two components because the scale has no step between 1.125 and
153
+ 1.5, an 11px chip sits deliberately below `xs`, and the toggle's state label has its own
154
+ four-step micro-ramp. Rule G flags only an exact match, which is precisely what keeps those
155
+ out of it.
156
+
157
+ - **Three clear buttons ignored the theme's corner radius.** `--gog-input-clear-radius`,
158
+ `--gog-select-clear-radius` and `--gog-multiselect-clear-radius` were a flat `2px`, so a theme
159
+ that set `--gog-radius` moved every corner in the library except these. They are
160
+ `calc(var(--gog-radius) / 4)` now: identical at the default 8px, so nothing moves in `light` or
161
+ `dark` — but `bevel`, whose whole identity is square corners (`--gog-radius: 0`), had three
162
+ quietly rounded controls and now does not.
163
+
164
+ Found by counting rather than by looking: of 47 radius tokens, 30 already derived from
165
+ `--gog-radius`, 8 are pills or circles (a shape, not a corner size), 5 are deliberately flat,
166
+ and these 3 were the remainder. `check-tokens` rule G could not have caught them — it flags a
167
+ literal only when its value *equals* a character token's, which is what keeps a pill's `999px`
168
+ from being called drift, and is exactly why a small arbitrary number is the shape of drift it
169
+ cannot see. `gog-toast` and `gog-accordion` keep their flat corners, on the user's call: those
170
+ are a chosen shape.
171
+
172
+ - **Error text was below WCAG AA in four themes.** `--gog-danger-color` is what every
173
+ `--gog-<block>-error-color` resolves to, so it is the colour a validation message is printed in
174
+ — text, needing 4.5:1, not the 3:1 a status accent gets away with. It cleared neither ground in
175
+ `one-light` (3.51:1 / 3.67:1), `primeng` (3.60 / 3.76), `one-dark` (4.38) or `slate` (4.46).
176
+ Each preset's red moves the smallest distance that clears 4.6:1 on both the page and a card:
177
+ `#e45649`→`#c2493e`, `#ef4444`→`#ce3a3a`, `#e06c75`→`#e2737c`, `#dc2626`→`#d82525`.
178
+ `check:contrast` gained the pair in the same change — it had none for `danger` at all, which is
179
+ why a field error nobody could read was invisible to it. Found by pointing the new
180
+ `check:app-contrast` at `ui-showcase`, which renders the library from source.
181
+
182
+ - **A toggle button now looks toggled.** 21.8.0 taught `gog-button` to forward `aria-pressed`,
183
+ and nothing in the library styled it — so a toggle could announce itself as on to a screen
184
+ reader while looking identical to an off one, which is WCAG 1.4.1 from the other side and
185
+ exactly the reason `docs/backlog.md` gives for *not* forwarding `aria-pressed` to `gog-chip`.
186
+ `aria-pressed="true"` (and `"mixed"`) now draws an inset ring: new
187
+ `--gog-button-<variant>-toggled-shadow`, with `--gog-button-toggled-shadow` as the per-instance
188
+ override. A ring rather than a fill because `:hover` and `:active` already own the background —
189
+ a toggled button that lost its state the moment the pointer arrived would be the same bug one
190
+ layer down. It keys off the attribute, not an input, so `[gogButton]` on your own element gets
191
+ it from the `aria-pressed` you already wrote there.
192
+
193
+ - **A button pressed with animations off now shows that it was pressed.** `:active` was a
194
+ `transform: scale()` and nothing else, and the `prefers-reduced-motion: reduce` block switched
195
+ that transform off — so the reader most likely to need the feedback got none at all. The ripple
196
+ did not cover it either: it is off by default, and it is deliberately suppressed under reduced
197
+ motion because it genuinely is decoration. `:active` now also deepens the button's background,
198
+ one step past its own hover so a press is distinguishable while hovering, and reduced motion
199
+ drops only the movement. New per-variant `--gog-button-<variant>-press-bg`/`-press-color`
200
+ tokens and the usual `--gog-button-press-bg`/`-press-color` instance overrides. **Ghost presses
201
+ to a filled `--gog-accent-dim`, like outline, rather than to a wash** — checked across all 11
202
+ themes, and a wash cannot work there: ghost's own *label* is the accent, so tinting its ground
203
+ with the accent walks the two together, and a 24% wash put the label under 4.5:1 in seven
204
+ themes. A filled press moves the label to `--gog-accent-text-color`, the pair `check:contrast`
205
+ already gates, so no future theme can quietly break it. Reduced motion must remove the animation, not the
206
+ information — the same rule the toast countdown was fixed under in 21.7.1.
207
+
208
+ The family is spelled `press`, not `active`, because `active` already means two different
209
+ things in this library — `--gog-tabs-active-color` is the *selected* tab, while
210
+ `--gog-scroll-thumb-active-bg` is the thumb being dragged. One name, one meaning. The shipped
211
+ `--gog-button-active-scale` keeps its spelling: renaming a token consumers already override
212
+ needs a deprecation cycle, and this is a patch.
213
+
214
+ - **`check:contrast` now measures composited washes, and found 24 more failures.** The script
215
+ compared palette hexes, so any pair where one side was a `color-mix()` wash was invisible to it
216
+ — which is both of the button failures above, found by hand instead. New `scripts/token-color.mjs`
217
+ resolves a component token the way a browser does (the theme's own block, then the `@supports`
218
+ mixed layer, then the derived layer, then the literals), composites it over the ground that
219
+ component actually sits on, and the check measures the label against that. 385 pairs across the
220
+ 11 themes, up from 143.
221
+
222
+ It then grew a second half that needs no table at all: `collectStatePairs` reads the compiled
223
+ stylesheets and checks every label/ground pair the rules themselves state, so a component added
224
+ later is covered without anyone remembering to list it. **627 pairs** in total now. That sweep
225
+ found one further real failure across the whole library — `gog-autocomplete`'s **selected
226
+ option** label, `--gog-accent-color` on its own tint, 4.12:1 in light (and `--gog-accent-dim` is
227
+ worse again, 3.77:1 in one-dark). It is `--gog-text-color` now; the tint and `aria-selected`
228
+ still mark the row, and `gog-select` keeps its accent label because its selected option has no
229
+ tint behind it. Icons are held to 3:1 rather than 4.5:1, which is what keeps a spin-button glyph
230
+ and a panel chevron — 4.35:1 and 4.40:1, both correct — from being "fixed" into near-black.
231
+
232
+ Its first run failed 24 of them, all real, and all fixed here:
233
+
234
+ - **A pressed tab's label leaves the muted tone.** A resting tab is deliberately
235
+ `--gog-muted-text-color`, which has no headroom to spend on a tinted ground: 3.24:1 in
236
+ one-dark and under 4.5 in six more. New `--gog-tabs-press-color`, the full text colour — which
237
+ is what pressing a tab is about to make it anyway.
238
+ - **An accordion header's label stops turning accent while hovered or held.** It took
239
+ `--gog-accordion-accent-color` on an accent-tinted strip: 3.61:1 in light, 3.79:1 in one-light,
240
+ 4.09:1 in ledger, and worse once the press deepened the tint. New `--gog-accordion-hover-color`,
241
+ defaulting to the header's own text colour; the lift is carried by the background alone. The
242
+ same trade as the ghost button's hover, for the same reason.
243
+ - **The press wash is 20%, not 22%.** Measured rather than chosen: at 21% one-dark's mid-grey
244
+ text falls under 4.5:1 on three of the surfaces. 20% is the strongest wash that clears AA in
245
+ every theme, and it is still a clear step past the 10-12% hover.
246
+
247
+ - **A ghost button's label was under WCAG AA on its own hover, in three themes.** `light`
248
+ 3.94:1, `primeng` 4.18:1, `one-light` 4.22:1. The variant's resting label *is*
249
+ `--gog-accent-color` and its hover tints the ground with the same accent, so the two walked
250
+ toward each other. **No background fixes it**, which is why this took a sweep rather than a
251
+ nudge: a half-strength wash (4.28), a neutral `--gog-hover-color` (4.15), a text scrim (3.91)
252
+ and an accent-dim wash (3.96) were all measured across the 11 themes, and `light` fails every
253
+ one — `--gog-accent-color` as *text* on that theme's background is 4.60:1 to begin with, so
254
+ there is no headroom to spend on any ground at all. The label now becomes `--gog-text-color`
255
+ while hovered, which clears 5.29:1 at worst (one-dark) and leaves the wash untouched, so the
256
+ hover stays the subtle one this variant is documented to have. Ghost's three states now read
257
+ transparent → tinted → filled, with a label chosen for each ground rather than one label
258
+ hoping to survive three.
259
+
260
+ - **`bevel` had no accent ramp, so its buttons could not show a press.** That preset declared
261
+ `--gog-accent-dim: #000080`, byte-identical to its `--gog-accent-color`. Harmless while `dim`
262
+ was only a field border; once it became the pressed fill, a pressed button in `bevel` painted
263
+ itself the colour it already was. Nothing failed — the token existed, resolved and passed every
264
+ contrast pair. It is now `#00005c`, and **`check-tokens` rule I was widened from the surface
265
+ tiers to any ramp**, so a theme whose rest/hover/press tones collapse into each other fails the
266
+ build instead of shipping a state nobody can see. Found by sweeping all 11 themes rather than by
267
+ the check that now catches it.
268
+
269
+ - **The other nine pressable surfaces had no press feedback either — now eight of them do.**
270
+ `.gog-btn:active` turned out to be the *only* `:active` rule in the library. `gogMenuItem`,
271
+ `gog-chip`, `gog-tabs` headers, `gog-accordion` headers, `gog-button-toggle-group` options and
272
+ the `gog-select` / `gog-multiselect` / `gog-autocomplete` option rows all acknowledged a press
273
+ through the ripple alone — which is off by default and suppressed under reduced motion — so a
274
+ default-configured app confirmed a press nowhere at all. Each now paints
275
+ `--gog-<block>-press-bg`: a wash roughly double the 10-12% one its own hover uses, in the same
276
+ ingredient, with a flat `--gog-border-color` for browsers without `color-mix()`. Two of them
277
+ are not that shape and say so in place: a `gog-tabs` header paints no background in any other
278
+ state (its hover moves the label colour only), and a *selected* button-toggle option is already
279
+ filled, so it deepens to `--gog-accent-dim` the way the filled button variants do.
280
+
281
+ **`gogCollapsibleTrigger` is deliberately not in that list.** It is the consumer's own element
282
+ and the library paints nothing on it — no background at rest, none on hover, only a cursor and
283
+ a disabled state. A press colour there would be the library inventing a look for markup it does
284
+ not own, and inventing it for one state out of three. The same reasoning `docs/ripple.md`
285
+ records for `gogCardLink` and `gog-table` rows.
286
+
287
+ The three option rows' press rules are guarded against the disabled modifier; their **hover**
288
+ rules are not, and were left alone — a disabled option lighting up under the pointer is a
289
+ separate decision from this one, and it is filed rather than folded in.
290
+
291
+ - **A disabled option no longer lights up under the pointer, and no longer ripples.** Two
292
+ separate holes in the same place. `.gog-select__option:hover` and `.gog-ms__option:hover`
293
+ carried no disabled guard, so a row that cannot be chosen took the same hover background as one
294
+ that can — only its opacity and its cursor disagreed, and neither is what a hover is read for.
295
+ `gog-autocomplete` already had this right (`:not([aria-disabled='true'])`), which is where the
296
+ fix was copied from. And all **three** wired `gogRipple` with `[rippleDisabled]="!rippleEnabled()"`,
297
+ taking no account of the option's own state: with the ripple switched on app-wide, a disabled
298
+ row answered a press with a wave. The options are rendered `aria-disabled`, not `disabled`, so
299
+ nothing else was stopping it. Same argument the library already applied to a non-interactive
300
+ `gog-chip`: a wave is a promise, and these rows cannot keep it.
301
+
302
+ - **The outline button's label was unreadable while hovered, in every shipped theme.**
303
+ `--gog-button-outline-hover-color` resolved to `--gog-primary-color`, the colour of text on the
304
+ *page*, while the hover fill is the accent — pale parchment on bright amber in `dark` (1.41:1),
305
+ light grey on blue in `one-dark` (1.11:1), and failing WCAG AA in all 11 themes, the best of
306
+ them `light` at 3.65:1. It now resolves to `--gog-accent-text-color`, the token that means "text
307
+ on an accent fill" and the one both filled variants already used. Found while adding the state
308
+ above, which would have copied the same mistake into `:active`.
309
+
310
+ - **`check:contrast` gained the pair that hid both.** The script had no pair for a label on the
311
+ accent *fill* other than `accentText/accent`, so neither the outline label nor the new held
312
+ state was covered. `accentText/accentDim(active)` is now checked at 4.5:1, and it immediately
313
+ failed one theme: `one-dark`'s `--gog-accent-dim` moved from `#4b8fca` to `#5399d6` (4.05:1 →
314
+ 4.59:1). That token had only ever been a field border, which is gated at 3:1; making it a fill
315
+ under a label is what raised the bar. The nudge moves it toward this palette's own `#61afef`,
316
+ so unlike 21.7.0's two comment-colour corrections it costs no fidelity.
317
+
318
+ - **Ten pieces of geometry ignored `--gog-density`, and nine of the eleven themes set it.**
319
+ `terminal` runs at 0.85, `one-dark`/`one-light`/`bevel`/`ledger` at 0.9, `primeng` at 0.95,
320
+ `slate` at 1.05, `material`/`parchment` at 1.1 — so in every one of them these ten stayed at
321
+ the pixel they were typed as while everything around them moved. The clearest case is the gap
322
+ between a dropdown and its panel: `gog-autocomplete` and `gog-datepicker` read `--gog-space-2`,
323
+ while `gog-select` and `gog-multiselect` restated `2px`, so in `terminal` two of the four
324
+ panels drew closer to their field and two did not. Also converted: `--gog-menu-offset` (which
325
+ sat between `--gog-menu-padding` and `--gog-menu-gap`, both already reading the scale),
326
+ `--gog-input-clear-inset` (the textarea's clear button — the other four clearable fields place
327
+ their chrome from `--gog-control-icon-offset`), the two error-line offsets, the toggle's
328
+ in-track wording clearance, the badge's overhang, and the calendar's header and time-row
329
+ margins. Each now reads its scale step, so all ten render exactly as before at the default
330
+ density and follow the theme everywhere else.
331
+
332
+ - **Four focus rings did not follow `--gog-focus-ring-offset`.** `gog-button-toggle` restated
333
+ its `2px` on the line directly after reading `--gog-focus-ring-width` from the foundation;
334
+ `gog-button` used `3px`; `gog-calendar` and `gog-tabs` used `-2px` to sit the ring inside a
335
+ cell, because both pack their cells edge to edge and an outward ring lands on the neighbour.
336
+ All four now derive from the foundation token — `var(…)`, `calc(… + 1px)` and `calc(… * -1)` —
337
+ so a theme that moves the ring moves them with it, and the two negative values now say in the
338
+ file why they are negative. No rendered value changes.
339
+
340
+ - **`check-tokens` rule H only covered two of the six families that hold a spacing number.**
341
+ The density check was written for `-padding` and `-gap`, the two the density work had just
342
+ converted, and `-offset`, `-inset` and `-margin` went unwatched for three releases — which is
343
+ where all ten literals above were hiding. The name filter now covers all six, and exempts the
344
+ lengths that match a scale step by coincidence rather than by meaning: every
345
+ `*-focus-ring-offset` (an accessibility affordance, which a compact theme must not shrink, and
346
+ which a component should answer by reading the foundation ring rather than a spacing step),
347
+ plus the scrollbar and toggle thumb insets, both fitted to a track whose own width is a fixed
348
+ pixel value. The script's header now also lists rules H and I, which had been in it unlisted
349
+ since they were added.
350
+
351
+ - **`gogBadge`'s status variants failed WCAG AA in four themes, and had since they shipped.** A
352
+ status badge paints `--gog-<status>-color` and labelled it `--gog-accent-text-color`, which on
353
+ a light theme is white: `material`'s amber measured **1.97:1**, `primeng`'s green 2.28:1, and
354
+ `slate` and `one-light` failed on all three of success/warning/info — 11 pairs in total.
355
+ `danger` passed everywhere, which is the tell: a danger pair was added to `check:contrast` on
356
+ 2026-09-03 and the palettes were tuned to it, while the other three statuses had never been
357
+ measured against anything.
358
+
359
+ Half the fix costs no fidelity: `material` puts near-black on its amber (8.69:1) and `primeng`
360
+ does the same on all three of Aura's bright hues (6.44–8.31:1), which is what both design
361
+ systems do themselves. The other half had no label to pick — neither white nor the theme's own
362
+ ink reached AA — so the hue moved instead, one step down each theme's own ramp: `slate`'s three
363
+ to Tailwind's 700s, `material`'s blue to Light Blue 800, and `one-light`'s three darkened, the
364
+ same trade that theme already made on its accent and muted tokens in 21.7.0.
365
+
366
+ **The reason no check caught it generalises past the badge.** `check:contrast`'s automatic
367
+ sweep pairs a rule that sets `color` with the `background-color` beside it; a variant class
368
+ sets neither, only `--gog-badge-variant-bg`/`-color` for the base rule to read. Every variant
369
+ of every component is written that way, so the sweep had been reporting ~180 passing states
370
+ without looking at one of them. Four explicit badge pairs close it for this component; the
371
+ general fix is filed in `docs/backlog.md`.
372
+
373
+ - **Two fields put their error line 2px lower than the other six.** Every control that renders an
374
+ error — `gog-inputfield`, `gog-textarea`, `gog-select`, `gog-multiselect`, `gog-autocomplete`,
375
+ `gog-datepicker`, `gog-radio-group`, `gog-slider` — is a flex column whose own gap already
376
+ separates the error from the field, the same gap that separates the label from it. Two of them
377
+ then added `margin-top: 2px` on top: measured in a browser, `gog-inputfield` and
378
+ `gog-multiselect` sat at 6px while `gog-select`, `gog-autocomplete` and `gog-datepicker` sat at
379
+ 4px from the identical gap. In one form, side by side, the six did not line up.
380
+ `--gog-input-error-offset` and `--gog-multiselect-error-offset` now default to `0`, so one
381
+ mechanism spaces the error line everywhere; both tokens stay, and setting one still pushes that
382
+ field's error further than the rest.
383
+
384
+ ## [21.8.0] - 03.09.2026
385
+
386
+ ### Added
387
+
388
+ - **`gog-button` forwards ARIA state and relationships: `ariaPressed`, `ariaExpanded`,
389
+ `ariaControls`, `ariaHasPopup`.** The component hides the real `<button>`, so
390
+ `<gog-button [attr.aria-pressed]="on()">` puts the attribute on the custom element host —
391
+ which has no role — and assistive tech never sees it. It compiles, throws nothing, looks
392
+ right, and silently ships a toggle button that is not a toggle button; `ariaLabel` already
393
+ existed for exactly this reason and was the only one. `null` (the default) omits the
394
+ attribute, while `false` is forwarded as `aria-pressed="false"`/`aria-expanded="false"`,
395
+ which is what an off toggle or a closed disclosure must say. New exported type
396
+ `GogAriaHasPopup`. The `[gogButton]` directive needs none of this — it styles an element you
397
+ own, so write the attributes there directly.
398
+
399
+ ### Fixed
400
+
401
+ - **`GOG_CONFIG`'s own documentation now names every component that reads each key.** The
402
+ `GogGlobalConfig` JSDoc — the closest thing the library has to a source of truth for which
403
+ component honours which setting, and what your editor shows you on hover — under-reported four
404
+ keys. `control.size` did not mention `gog-button-toggle-group`, `gog-toggle`, `gog-autocomplete`
405
+ or `gog-datepicker`; `control.errorDisplay`, `control.clearable` and `floatLabel` did not
406
+ mention `gog-autocomplete` or `gog-datepicker`; and `dropdown` claimed to apply to
407
+ `gog-select`/`gog-multiselect` as a block, when `appendToBody`/`direction` also reach
408
+ `gog-autocomplete` and `gog-datepicker` while `filter`/`filterPosition` genuinely do not.
409
+ `AGENTS.md`'s config table was closer but stated outright that `gog-toggle` does **not** take
410
+ `control.size`, which it does. No behaviour change — every one of these components already
411
+ honoured the setting; only the documentation was wrong, in the direction of telling you a
412
+ setting would not work when it does.
413
+
7
414
  ## [21.7.2] - 30.08.2026
8
415
 
9
416
  ### Fixed
package/README.md CHANGED
@@ -168,6 +168,16 @@ variant and size classes:
168
168
  > "what a raised surface looks like here". Change one and every raised surface follows, which is
169
169
  > the intent; the rest of `--gog-panel-*` belongs to the component alone.
170
170
 
171
+ **A status colour is three tokens, not one.** `--gog-danger-color` and its three siblings are
172
+ fills, and a fill needs a label that reads on it and a direction to deepen in — so each also has
173
+ `--gog-<status>-text-color` (the label; defaults to the accent's, state it only when your hue
174
+ disagrees) and `--gog-<status>-shade` (which way hover and press move; defaults to the page's ink,
175
+ and should be the opposite when your label *is* the ink). Setting a status colour alone and
176
+ leaving those at their defaults is how a bright amber ends up under white text: it measured 1.97:1
177
+ in one of this package's own presets before 21.9.0. `gogBadge` and `gog-button`'s `severity` read
178
+ the label; the button also reads the shade. `gog-tag` derives its own pair by mixing and
179
+ `gog-progressbar` paints no label on its bar, so neither needs them.
180
+
171
181
  Every group and token name is in **[`TOKENS.md`](./TOKENS.md)**, generated from `theme.css` so it
172
182
  cannot drift, and available at runtime as `GOG_TOKEN_GROUPS`.
173
183
 
@@ -263,9 +273,20 @@ provideGogConfig({
263
273
  ```
264
274
 
265
275
  Keys: `control`, `dropdown`, `floatLabel`, `datepicker`, `autocomplete`, `inputfield`, `textarea`,
266
- `tooltip`, `scroll`, `button`, `ripple`, `paginator`, `toast`, `theme`, `labels`. An instance's own
267
- input always wins, and providing the config again lower in the injector tree layers onto the
268
- parent rather than replacing it.
276
+ `tooltip`, `scroll`, `button`, `ripple`, `spinner`, `paginator`, `toast`, `theme`, `labels`. An
277
+ instance's own input always wins, and providing the config again lower in the injector tree layers
278
+ onto the parent rather than replacing it.
279
+
280
+ `spinner` is the one key that takes a **component** rather than a value:
281
+
282
+ ```ts
283
+ provideGogConfig({ spinner: { component: HouseLoader } });
284
+ ```
285
+
286
+ That renders your loader wherever the library draws a spinner — including the two places no input
287
+ could reach, since `gog-button` and `gog-autocomplete` render their own and expose nothing for it.
288
+ It keeps the library's sizing, overlay behaviour, `role="status"` and accessible name; only the
289
+ visual is yours.
269
290
 
270
291
  `ripple` is the one visual default that is not a token, and the exception is deliberate:
271
292
  `--gog-ripple-opacity: 0` would hide the wash but still pay for the DOM node, the listeners and