@guildofgleks/ui 21.8.0 → 21.9.1

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