@guildofgleks/ui 21.8.0 → 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/AGENTS.md CHANGED
@@ -5,8 +5,8 @@ an app that **consumes** the published `@guildofgleks/ui` npm package. It is not
5
5
  authoring the library — if you are working inside the `gleks_web_ui` monorepo itself, read
6
6
  `.github/instructions/*.md` instead.
7
7
 
8
- Everything below reflects the library's actual source as of **`21.8.0`** (in progress — the
9
- released version is 21.7.2; see `CHANGELOG.md` for what 21.8.0 adds). 21.7.0 removed the three
8
+ Everything below reflects the library's actual source as of **`21.9.0`** (in progress — the
9
+ released version is 21.8.0; see `CHANGELOG.md` for what 21.9.0 adds). 21.7.0 removed the three
10
10
  abbreviated token prefixes and 21.5.0 removed a batch of deprecated API — see **Removed in 21.7.0**
11
11
  and **Removed in 21.5.0** near the end of this file, which exist so code written against an older
12
12
  version can be migrated — and `CHANGELOG.md` has the rest. `README.md` covers the same ground at a
@@ -181,13 +181,30 @@ Full model is in `README.md`'s Theming section; short version:
181
181
  `--gog-text-transform`/`--gog-letter-spacing` (emphasis casing/tracking). Component tokens in
182
182
  the categories these cover derive from them by default; setting one in a `[data-theme]` block
183
183
  restyles every component that reads it, with nothing to re-list per component.
184
+ - **The type scale is `--gog-text-xs | sm | md | lg | slg | xl | 2xl | 3xl`.** `slg` (1.25rem)
185
+ fills the gap between `lg` and `xl` and is named for the control size that needed it. Every
186
+ component font size that is one of these reads the token, so retuning the scale retunes the
187
+ library; the handful that do not are off-scale on purpose (an 11px chip, the accordion
188
+ chevron's px ramp, the toggle's own micro-ramp).
189
+
190
+ - **Weight is `--gog-font-weight-medium | semibold | bold | heavy`** (500/600/700/900). Every
191
+ component weight reads one of them, so a lighter or heavier house style is four declarations.
192
+
193
+ - **`--gog-z-base` moves the whole stacking order.** Badge `+1`, toast `+100`, dropdowns, dialogs
194
+ and menus `+300`, tooltip `+400`, the blocking spinner overlay `+8000`. Set the base to lift
195
+ the library above your own chrome without disturbing its internal order.
196
+
184
197
  - **`--gog-density` is the character layer for spacing** (since 21.7.0, `docs/themes.md`
185
198
  iteration 6). It multiplies the fourteen-step scale `--gog-space-2` … `--gog-space-48`, named
186
199
  for their pixel value at density 1, and every padding and gap in the library derives from a
187
200
  step. `--gog-density: 0.9` in a `[data-theme]` block makes the whole library tighter; nothing
188
201
  else needs to be named. `--gog-space-xs|sm|md|lg|2xl` are aliases for steps 4/8/16/24/48 and
189
- still work. Icon offsets follow density; the glyph box, focus-ring offset and float-label
190
- reserve deliberately do not they are legibility, not spacing.
202
+ still work. Icon offsets, dropdown panel gaps, error-line offsets and the badge's overhang
203
+ follow density; the glyph box, the focus-ring offset, the float-label reserve and the
204
+ scrollbar/toggle thumb insets deliberately do not — those are legibility or geometry fitted to
205
+ a fixed-width track, not spacing. Since 21.9.0 the split is enforced rather than trusted:
206
+ `check-tokens` rule H fails the build on a length token that restates a scale step's value as
207
+ a bare literal, with the three exceptions named in the script.
191
208
  - **Component prefixes are spelled out** since 21.5.0: `--gog-button-*`, `--gog-multiselect-*`,
192
209
  `--gog-confirmation-dialog-*`. The abbreviated `--gog-btn-*`, `--gog-ms-*` and `--gog-confirm-*`
193
210
  were removed in 21.7.0 — if you're reading a codebase or an example that still uses one, rename
@@ -271,7 +288,8 @@ parent's config**, one level deep per key — it does not replace it.
271
288
  | `datepicker` | `locale`, `firstDayOfWeek`, `format` | `gog-datepicker`, `gog-calendar`. |
272
289
  | `autocomplete` | `searchDebounce`, `minLength`, `openOnFocus` | `gog-autocomplete`. |
273
290
  | `tooltip` | `position`, `showDelay`, `hideDelay` | the `gogTooltip` directive. |
274
- | `scroll` | `autoHide`, `hideDelay`, `size`, `overscrollBehavior`, `showTrack` | `gog-scroll` (and every component that uses one internally). |
291
+ | `spinner` | `component`, `variant` | every spinner the library draws — `gog-spinner`, `gog-spinner-overlay`, and the ones inside `gog-button` and `gog-autocomplete`, which have no input of their own. `component` takes **your** component and renders it in place of the built-in look. |
292
+ | `scroll` | `autoHide`, `hideDelay`, `size`, `overscrollBehavior`, `showTrack`, `horizontalWheel` | `gog-scroll` (and every component that uses one internally). |
275
293
  | `button` | `debounce` | `gog-button`. |
276
294
  | `ripple` | `enabled` | the press ripple on `gog-button`, `[gogButton]`, `gog-button-toggle-group`, `gog-chip`, `gog-tabs`, `gog-accordion`, `gogCollapsibleTrigger`, `gogMenuItem` and the `gog-select`/`gog-multiselect`/`gog-autocomplete` options. **Off by default.** Each of those takes a `ripple` input that wins over it. Not the `gogRipple` directive — writing that attribute is already the per-element decision. |
277
295
  | `inputfield` | `showSpinButtons` | `gog-inputfield`. |
@@ -468,6 +486,7 @@ Every component below is exported from `@guildofgleks/ui`'s root — `import { X
468
486
  | Input | Type | Default | Notes |
469
487
  | ----------- | --------------------------------- | ----------- | ----------------------------------------------------- |
470
488
  | `variant` | `GogVariant` | `'primary'` | |
489
+ | `severity` | `GogSeverity` | `'accent'` | what the action means; orthogonal to `variant` — see below |
471
490
  | `size` | `GogSize \| undefined` | `'md'` | via `GOG_CONFIG.control.size` |
472
491
  | `disabled` | `boolean` | `false` | |
473
492
  | `fullWidth` | `boolean` | `false` | |
@@ -483,6 +502,29 @@ Every component below is exported from `@guildofgleks/ui`'s root — `import { X
483
502
 
484
503
  Outputs: `gogClick: MouseEvent`.
485
504
 
505
+ **`severity` says what the action means; `variant` says how loudly it is drawn** (21.9.0). The
506
+ two are orthogonal, so this is not a fifth variant — it re-points the colours all four are built
507
+ from, and every combination is real: `variant="ghost" severity="danger"` is a quiet delete,
508
+ `variant="primary" severity="danger"` a loud one. `'accent'` is the default and the absence of a
509
+ claim, so nothing has to opt out of a severity it does not have. `GogSeverity` is shared with
510
+ `gog-progressbar`, whose `GogProgressbarVariant` is now an alias of it.
511
+
512
+ ```html
513
+ <gog-button severity="danger" (gogClick)="deleteAccount()">Delete account</gog-button>
514
+ <gog-button variant="outline" severity="warning">Discard draft</gog-button>
515
+ <a gogButton severity="success" routerLink="/done">Finish</a>
516
+ ```
517
+
518
+ Two colour rules are worth knowing before you override anything. A **filled** severity button's
519
+ label is `--gog-<status>-text-color`, which each theme states for its own hue — `material` and
520
+ `primeng` put near-black on their bright ones, the rest white — and hover and press deepen the
521
+ fill *away* from that label (`--gog-<status>-shade`), so a state always makes the label easier to
522
+ read rather than harder. A **transparent** one's label is `--gog-button-<status>-ink`: the status
523
+ hue mixed halfway toward the page's ink, because the raw hue is legible body text in only five of
524
+ the eleven shipped themes. Override `--gog-button-<status>-ink` if your own theme wants more
525
+ colour there, and check it: all four severities across all four variants and all their states are
526
+ gated by `npm run check:contrast`.
527
+
486
528
  **Every ARIA attribute this button needs has an input, and a raw attribute is not a
487
529
  substitute.** `<gog-button [attr.aria-pressed]="on()">` compiles, throws nothing, and does
488
530
  nothing: the attribute lands on the `<gog-button>` custom element, which has no role, while the
@@ -494,6 +536,13 @@ simply not a toggle to a screen reader. Use `[ariaPressed]`, `[ariaExpanded]`, `
494
536
  `aria-pressed="false"` / `aria-expanded="false"`, which is what an off toggle or a closed
495
537
  disclosure has to say — a button with no `aria-pressed` at all is not a toggle button.
496
538
 
539
+ **A toggle button now looks toggled** (21.9.0). `aria-pressed="true"` (or `"mixed"`) draws an
540
+ inset ring — `--gog-button-<variant>-toggled-shadow`, overridable per instance with
541
+ `--gog-button-toggled-shadow`. A ring rather than a fill because hover and press already own the
542
+ background: the state has to survive both, and until 21.9.0 it did not exist at all, so a button
543
+ could announce itself as on to a screen reader and look identical to an off one. `[gogButton]`
544
+ gets the same look from the attribute you write on your own element.
545
+
497
546
  **`[gogButton]` needs none of these inputs.** It styles an element you own, so write the ARIA
498
547
  attributes on your own `<button>`/`<a>` directly. Same for `[gogMenuTrigger]`, which sets
499
548
  `aria-haspopup`/`aria-expanded`/`aria-controls` on its host — put it on your own `<button
@@ -506,6 +555,19 @@ gogButton>`, as its own example shows, not on a `<gog-button>`.
506
555
  (gogClick)="open.set(!open())">Filters</gog-button>
507
556
  ```
508
557
 
558
+ **The press is a colour, not only a movement.** `:active` deepens the button's background (and
559
+ the label where the fill demands it) as well as scaling it by `--gog-button-active-scale`. Under
560
+ `prefers-reduced-motion: reduce` the scale is dropped and the colour stays, so the press is still
561
+ visible to a reader who has switched animations off — before 21.9.0 that reader got no feedback at
562
+ all, since the ripple is off by default and is itself suppressed under reduced motion. Override
563
+ per instance with `--gog-button-press-bg` / `--gog-button-press-color`, or per theme with
564
+ `--gog-button-<variant>-active-bg`.
565
+
566
+ Every other pressable surface in the library does the same thing since 21.9.0 — menu items,
567
+ chips, tab and accordion headers, button-toggle options and the three dropdowns' option rows —
568
+ each through its own `--gog-<block>-press-bg`. `gogCollapsibleTrigger` is the exception: the
569
+ library paints nothing on that element in any state, because it is yours.
570
+
509
571
  **`debounce` is a spam guard, not a delay before the first click.** The first click in a window
510
572
  fires immediately (leading edge); further clicks within `debounce` ms are silently dropped.
511
573
 
@@ -956,6 +1018,7 @@ the element stays yours and the directive only gives it the look.
956
1018
  | Input | Type | Default |
957
1019
  | ----------- | ------------------------ | ---------------------------------------- |
958
1020
  | `variant` | `GogVariant` | `'primary'` |
1021
+ | `severity` | `GogSeverity` | `'accent'`; same as `gog-button` |
959
1022
  | `size` | `GogSize \| undefined` | `'md'`; via `GOG_CONFIG.control.size` |
960
1023
  | `fullWidth` | `boolean` (bare attr ok) | `false` |
961
1024
  | `ripple` | `boolean \| undefined` | `false`; via `GOG_CONFIG.ripple.enabled` |
@@ -1004,6 +1067,7 @@ are impossible by design.
1004
1067
  | `size` | `GogSize` | `'md'` |
1005
1068
  | `shape` | `GogTagShape` (`'rounded'\|'pill'`) | `'rounded'` |
1006
1069
  | `disabled`, `clickable` | `boolean` | `false`, `true` |
1070
+ | `selected` | `boolean \| null` (two-way) | `null` — see below |
1007
1071
  | `removable` | `boolean` | `false` |
1008
1072
  | `fullWidth` | `boolean` | `false` |
1009
1073
  | `ariaLabel`, `removeAriaLabel` | `string` | `''`, `'Remove chip'` |
@@ -1019,6 +1083,29 @@ Outputs: `gogClick: MouseEvent | KeyboardEvent`, `gogRemove: void`.
1019
1083
  >
1020
1084
  ```
1021
1085
 
1086
+ **`selected` makes it a filter chip** (21.9.0) — a chip you toggle on and off rather than press.
1087
+ It is tri-state, and `null` is the default so nothing about an existing chip changes: no
1088
+ `aria-pressed`, no selected look, activation only emits `gogClick`. Set it to `false` and the chip
1089
+ is a toggle that is off (`aria-pressed="false"` — a chip with no `aria-pressed` at all is not a
1090
+ toggle to a screen reader, so "off" has to be stated); `true` and it is on, which draws an inset
1091
+ ring from `--gog-chip-selected-shadow`. A ring rather than a fill because `:hover` and `:active`
1092
+ already own the chip's background and the selection has to survive both.
1093
+
1094
+ It is a two-way `model`, so the chip flips it on click, Enter and Space — a row of filters needs
1095
+ no click handler:
1096
+
1097
+ ```html
1098
+ @for (f of filters; track f.label) {
1099
+ <gog-chip [(selected)]="f.on">{{ f.label }}</gog-chip>
1100
+ }
1101
+ ```
1102
+
1103
+ `gogClick` still fires, **after** the flip, so a handler reading `selected()` sees the new value.
1104
+ Drive the state from that handler instead and you want a one-way `[selected]`, or the two writes
1105
+ cancel out. A `disabled` chip keeps the ring but drops `aria-pressed`, which needs the
1106
+ `role="button"` a disabled chip does not carry — "selected, and currently unavailable" is a real
1107
+ state and hiding it would leave it announced and invisible.
1108
+
1022
1109
  #### `gog-tag`
1023
1110
 
1024
1111
  | Input | Type | Default |
@@ -1048,6 +1135,19 @@ Slot: `<ng-template gogTagIcon>` for custom icon markup.
1048
1135
  `variant="custom"` renders your own projected markup, still inheriting the size wrapper and
1049
1136
  `--gog-spinner-color` theming.
1050
1137
 
1138
+ **To replace the spinner everywhere at once, pass a component to `GOG_CONFIG`** — including the
1139
+ two places you cannot reach with an input, `gog-button`'s and `gog-autocomplete`'s loading
1140
+ states:
1141
+
1142
+ ```ts
1143
+ provideGogConfig({ spinner: { component: HouseLoaderComponent } });
1144
+ ```
1145
+
1146
+ It renders inside the same size wrapper as the built-ins, so it keeps the sizing, the overlay
1147
+ behaviour, `role="status"` and the accessible name — only the visual is yours. An instance's own
1148
+ `variant` still wins over it, so `<gog-spinner variant="ring">` is a ring in an app that has set
1149
+ a component: a default does not overrule something asked for explicitly.
1150
+
1051
1151
  ```html
1052
1152
  <gog-spinner-overlay [loading]="isLoading()">
1053
1153
  <app-content-that-loads />
@@ -1588,6 +1688,21 @@ region — the library's official recommendation over a raw `overflow-x`/`overfl
1588
1688
  | `ariaLabel` | `string` | `''` |
1589
1689
  | `overscrollBehavior` | `GogScrollOverscrollBehavior \| undefined` (`'auto'\|'contain'\|'none'`) | `'auto'`; via `GOG_CONFIG.scroll.overscrollBehavior` |
1590
1690
  | `showTrack` | `boolean \| undefined` | `true`; via `GOG_CONFIG.scroll.showTrack` |
1691
+ | `horizontalWheel` | `boolean \| undefined` | `false`; via `GOG_CONFIG.scroll.horizontalWheel` |
1692
+
1693
+ **`horizontalWheel` turns a vertical wheel into horizontal scrolling** (21.9.0), for the case a
1694
+ consumer hits first: hover a horizontal-only row, turn the wheel, and the *page* moves. That is
1695
+ the browser's own behaviour and the component deliberately did nothing about it until now.
1696
+
1697
+ It is off by default because it changes what an existing instance does with a gesture it
1698
+ currently passes on; `provideGogConfig({ scroll: { horizontalWheel: true } })` turns it on
1699
+ app-wide. It only acts when the viewport cannot scroll vertically (checked against live
1700
+ geometry, so `axis="both"` scrolls down while there is down to go), the event carries no
1701
+ horizontal delta of its own (a trackpad swipe and `Shift`+wheel already work), `ctrlKey` is
1702
+ clear (pinch-zoom), and there is room left in the direction of the turn. **That last condition is
1703
+ the point:** at the content's end the event is left alone and the page picks it up, so the wheel
1704
+ never goes dead over a scrolled-to-the-end region. `overscrollBehavior: 'contain'` still
1705
+ contains — that boundary is the browser's and this never reaches past it.
1591
1706
 
1592
1707
  Outputs: `gogScroll: GogScrollMetrics`, `gogReachStart`/`gogReachEnd: 'vertical'|'horizontal'`.
1593
1708
  Methods (via template ref): `scrollTo(options)`, `scrollToTop()`, `scrollToBottom()`,
package/CHANGELOG.md CHANGED
@@ -4,6 +4,383 @@ 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
+
7
384
  ## [21.8.0] - 03.09.2026
8
385
 
9
386
  ### Added