@guildofgleks/ui 21.9.1 → 21.11.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
@@ -195,11 +195,16 @@ Full model is in `README.md`'s Theming section; short version:
195
195
  the library above your own chrome without disturbing its internal order.
196
196
 
197
197
  - **`--gog-density` is the character layer for spacing** (since 21.7.0, `docs/themes.md`
198
- iteration 6). It multiplies the fourteen-step scale `--gog-space-2` … `--gog-space-48`, named
198
+ iteration 6). It multiplies the ten-step scale `--gog-space-4` … `--gog-space-48`, named
199
199
  for their pixel value at density 1, and every padding and gap in the library derives from a
200
200
  step. `--gog-density: 0.9` in a `[data-theme]` block makes the whole library tighter; nothing
201
201
  else needs to be named. `--gog-space-xs|sm|md|lg|2xl` are aliases for steps 4/8/16/24/48 and
202
- still work. Icon offsets, dropdown panel gaps, error-line offsets and the badge's overhang
202
+ still work. **Every step is a multiple of 4** (since 21.11.0): the five 2px-granular steps came
203
+ out once the last of their 102 readers moved, so "on the grid" is a fact about the scale rather
204
+ than a habit. Three lengths stay off it on purpose and say so in their own comments — a toggle
205
+ thumb's inset, a scrollbar thumb's, and the resize grip's hairline gap — because a length inside
206
+ a single painted mark defines that mark's shape rather than spacing two things apart.
207
+ Icon offsets, dropdown panel gaps, error-line offsets and the badge's overhang
203
208
  follow density; the glyph box, the focus-ring offset, the float-label reserve and the
204
209
  scrollbar/toggle thumb insets deliberately do not — those are legibility or geometry fitted to
205
210
  a fixed-width track, not spacing. Since 21.9.0 the split is enforced rather than trusted:
@@ -288,7 +293,7 @@ parent's config**, one level deep per key — it does not replace it.
288
293
  | `datepicker` | `locale`, `firstDayOfWeek`, `format` | `gog-datepicker`, `gog-calendar`. |
289
294
  | `autocomplete` | `searchDebounce`, `minLength`, `openOnFocus` | `gog-autocomplete`. |
290
295
  | `tooltip` | `position`, `showDelay`, `hideDelay` | the `gogTooltip` directive. |
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. |
296
+ | `spinner` | `component`, `variant` | every spinner the library draws — `gog-spinner`, `gog-spinner-overlay`, and the ones inside `gog-button`, `gog-autocomplete` and `gog-table`, which have no input of their own. `component` takes **your** component and renders it in place of the built-in look. The overlay honoured neither key until 21.10.0, and `gog-table` was simply never listed. |
292
297
  | `scroll` | `autoHide`, `hideDelay`, `size`, `overscrollBehavior`, `showTrack`, `horizontalWheel` | `gog-scroll` (and every component that uses one internally). |
293
298
  | `button` | `debounce` | `gog-button`. |
294
299
  | `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. |
@@ -543,6 +548,12 @@ background: the state has to survive both, and until 21.9.0 it did not exist at
543
548
  could announce itself as on to a screen reader and look identical to an off one. `[gogButton]`
544
549
  gets the same look from the attribute you write on your own element.
545
550
 
551
+ **A `disabled` toggle keeps the ring** (21.10.0), dimmed by `--gog-button-disabled-opacity` like
552
+ the rest of the button. `disabled` on a real `<button>` does not remove `aria-pressed`, so "on,
553
+ and unavailable" is announced either way and has to be visible; the rule had excluded
554
+ `:disabled` until then, copied from the hover and press rules where the guard belongs.
555
+ `gog-chip`'s `selected` ring has always behaved this way, and the two are now the same.
556
+
546
557
  **`[gogButton]` needs none of these inputs.** It styles an element you own, so write the ARIA
547
558
  attributes on your own `<button>`/`<a>` directly. Same for `[gogMenuTrigger]`, which sets
548
559
  `aria-haspopup`/`aria-expanded`/`aria-controls` on its host — put it on your own `<button
@@ -719,6 +730,14 @@ CVA: yes, both. Slots (shared): `<ng-template gogDropdownChevron>` (custom chevr
719
730
  `<ng-template gogDropdownOption let-opt let-selected="selected" let-label="label">` (custom
720
731
  option row). Multiselect adds `<ng-template gogMultiselectClearIcon>`.
721
732
 
733
+ **Turn `filter` on past about seven options — or order them instead.** Choice time grows with the
734
+ log of the count (`T = b · log₂(n + 1)`), so beyond roughly seven a panel stops being scanned and
735
+ starts being read. The escape is not always the filter box: the law governs _unordered_ choices,
736
+ and a list the reader can predict — alphabetical countries, ascending amounts, a familiar fixed
737
+ sequence — is one they search rather than choose from, so ordering it well is worth as much as
738
+ filtering it. Both, for a long list of neither. `GOG_CONFIG.dropdown.filter` sets this once for
739
+ the app rather than per dropdown, which is usually the right place for it.
740
+
722
741
  ```html
723
742
  <gog-select
724
743
  label="Region"
@@ -1127,7 +1146,7 @@ Slot: `<ng-template gogTagIcon>` for custom icon markup.
1127
1146
  | Input | Type | Default |
1128
1147
  | -------------------------------- | ------------------------------------------------- | ------------------------------------------- |
1129
1148
  | `size` | `GogSize` | `'md'` |
1130
- | `variant` | `GogSpinnerVariant` (`'runic'\|'ring'\|'custom'`) | `'runic'` |
1149
+ | `variant` | `GogSpinnerVariant` (`'runic'\|'ring'\|'custom'`) | unset — see below |
1131
1150
  | `ariaLabel` | `string` | `'Loading'` |
1132
1151
  | `overlay` (spinner only) | `boolean` | `false` |
1133
1152
  | `loading` (spinner-overlay only) | `boolean` | `false` — toggles the overlay + `aria-busy` |
@@ -1136,8 +1155,8 @@ Slot: `<ng-template gogTagIcon>` for custom icon markup.
1136
1155
  `--gog-spinner-color` theming.
1137
1156
 
1138
1157
  **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:
1158
+ three places you cannot reach with an input: `gog-button`'s and `gog-autocomplete`'s loading
1159
+ states, and the spinner `gog-table` draws in place of its rows.
1141
1160
 
1142
1161
  ```ts
1143
1162
  provideGogConfig({ spinner: { component: HouseLoaderComponent } });
@@ -1148,6 +1167,13 @@ behaviour, `role="status"` and the accessible name — only the visual is yours.
1148
1167
  `variant` still wins over it, so `<gog-spinner variant="ring">` is a ring in an app that has set
1149
1168
  a component: a default does not overrule something asked for explicitly.
1150
1169
 
1170
+ **Neither component's `variant` has a default value**, and on `gog-spinner-overlay` that is the
1171
+ whole of the 21.10.0 fix: the overlay forwards its `variant` to the spinner it wraps, so a default
1172
+ there would have been an instance overruling the config on every overlay ever rendered — which is
1173
+ exactly what happened before, leaving the one spinner that covers a whole region on the built-in
1174
+ look while every other spinner in the app was the house one. `size` and `ariaLabel` keep their
1175
+ defaults: neither has a config key to fall through to.
1176
+
1151
1177
  ```html
1152
1178
  <gog-spinner-overlay [loading]="isLoading()">
1153
1179
  <app-content-that-loads />
@@ -1186,6 +1212,14 @@ a component: a default does not overrule something asked for explicitly.
1186
1212
  <gog-progressbar mode="buffer" [value]="42" [buffer]="70" />
1187
1213
  ```
1188
1214
 
1215
+ **The fill's end is marked by two hairlines** (21.10.0), `--gog-progressbar-edge-color` over
1216
+ `--gog-progressbar-edge-backing-color`, each `--gog-progressbar-edge-width` wide. That boundary is
1217
+ the value — `showValue` is off by default — and the fill and the track cannot carry it themselves:
1218
+ in every shipped theme the five fills straddle mid-luminance, so no one track colour clears WCAG
1219
+ 1.4.11's 3:1 against all of them. Two tones always do, and `check:contrast` gates the pair. Retint
1220
+ them per theme if you like; keep them a *pair* whose tones sit on opposite sides of the middle, or
1221
+ the marker disappears on whichever fill it happens to match.
1222
+
1189
1223
  #### `gog-divider`
1190
1224
 
1191
1225
  | Input | Type | Default |
package/CHANGELOG.md CHANGED
@@ -4,6 +4,473 @@ 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.11.0] - 09.09.2026
8
+
9
+ ### Added
10
+
11
+ - **`--gog-space-40`**, the one step the spacing scale was missing. Added because the optical ratio
12
+ requires it rather than because a design asked for it: horizontal padding is exactly twice
13
+ vertical, `slg` controls carry 20px of vertical padding, and the scale went 32 and then jumped to
14
+ 48. It multiplies `--gog-density` like every other step.
15
+
16
+ - **`--gog-text-2xs` (0.6875rem, 11px)**, the step below `xs`. `gog-chip` and `gog-tag` had both
17
+ written that value as a literal at `xsm` — the same number chosen twice, independently, which is
18
+ a missing rung rather than two opinions. Nothing renders differently; a theme moving the scale
19
+ now moves both with it.
20
+
21
+ - **A leading token for every block that sets a font size** — around forty-five of them, and the
22
+ reason is law 4 rather than tidiness: a block that changes its size while inheriting its leading
23
+ changes the ratio silently. A `--gog-text-xs` numeric table cell inside a `--gog-text-md` page
24
+ inherited the page's leading and landed on a ratio nobody chose. Where the role is shared the
25
+ token is shared: `--gog-field-line-height`, `--gog-field-label-line-height` and
26
+ `--gog-field-error-line-height` are declared once and aliased by every field, the way the padding
27
+ tier already works. Six roles, one leading each — the full table is in
28
+ `docs/component-geometry.md`.
29
+
30
+ ### Removed
31
+
32
+ - **`--gog-space-2`, `-6`, `-10`, `-14` and `-18` are gone from the spacing scale**, which is now
33
+ ten steps and every one of them a multiple of 4. This is the end of the sweep rather than the
34
+ start of it: the five could only come out once the last of their 102 readers had moved, and each
35
+ of those moves is a component entry above.
36
+
37
+ **Three lengths stay off the grid on purpose**, each with the reason in its own comment: a toggle
38
+ thumb's 2px inset, a scrollbar thumb's, and the 2px gap between the resize grip's three
39
+ hairlines. A length *inside* a single painted mark defines that mark's shape rather than spacing
40
+ two things apart — at 4px the grip's stripes are a 20px block in the corner of every textarea
41
+ instead of a 9px hint. `--gog-focus-ring-offset` was never part of the scale and is unaffected.
42
+
43
+ If your app reads one of the five, replace it with the neighbouring step: the library rounded
44
+ **up** everywhere, because it had seventeen pointer targets under 24×24 and none over.
45
+
46
+ ### Changed
47
+
48
+ - **`gog-tag`'s padding lands on the 4px grid, at a ratio of exactly 2.0.** Horizontal padding is
49
+ twice vertical at every size, which is the library's optical-ratio law (`styling.instructions.md`);
50
+ the tag had been running 3.00 at `xsm` down to 1.60 at `slg`, five different opinions about one
51
+ shape. New values, per size: `4/8`, `4/8`, `8/16`, `8/16`, `12/24` (block/inline), with the gap
52
+ following the block padding as before.
53
+
54
+ **Two adjacent sizes now share a padding**, and that is the finding this first component
55
+ produced rather than an oversight. Five *distinct* vertical paddings on a 4px grid would have to
56
+ run 4, 8, 12, 16, 20, which doubles `slg` and makes a tag the size of a button. So the size step
57
+ is carried by the type scale — 11, 12, 14, 16, 18px — and the padding repeats between adjacent
58
+ steps rather than the geometry inflating to keep five distinct numbers. Every sized component in
59
+ this release follows the same rule.
60
+
61
+ - **The shared control tier is on the grid, at a ratio of 2.0.** `--gog-control-padding-y`/`-x`
62
+ go from `10/14` (a ratio of 1.40) to `12/24`, and `--gog-field-md-padding-*` derives from them —
63
+ so an `md` input, select, multiselect, autocomplete and datepicker are all padded by this one
64
+ pair. `--gog-control-icon-offset` follows from 10px to 12px, which also moves the four
65
+ `*-actions-inset` tokens that read it.
66
+
67
+ - **`--gog-control-checkbox-padding` is 8px, not 6px**, which is the padding a checkable control's
68
+ `<label>` carries around its box. It is part of the pointer target rather than decoration: a
69
+ 12px `xsm` box inside 8px of padding is a 28px target, and that is how the smallest checkbox,
70
+ radio and toggle clear WCAG 2.5.8 without the painted box growing at all. The literal fallback
71
+ in `checkable-control.config.ts` moves with it — a fallback that disagrees with the token is a
72
+ second default nobody can find.
73
+
74
+ - **`gog-panel`'s four off-grid lengths move to the grid** — `xsm` vertical padding 10 to 12,
75
+ `sm` 14 to 16 with its gap 10 to 12, `md` 18 to 20. A surface like the card, and outside the
76
+ optical ratio for the same reason.
77
+
78
+ - **`gog-scroll`'s thumb hit padding is 8px, not 6px** — the invisible band around the thumb that
79
+ makes it easier to grab, so the grid moves it in the helpful direction. **The thumb's 2px inset
80
+ stays**, for the same reason the toggle's does: a length inside a single painted mark defines
81
+ that mark's shape, and 4px on a thin scrollbar would leave the thumb barely wider than the
82
+ hairline it rides in.
83
+
84
+ - **`gog-slider`'s thumb has a 24×24 pointer target and still paints at 16px**, and its gap goes
85
+ from 6px to 8px. This is the one control in the library a pointer *drags*, and a 24px dot would
86
+ cover the value it points at. It stops at 24 rather than 2.5.5's 44 on purpose: the track is
87
+ clickable along its whole length, so the coarse-pointer story is "tap the track", not "hit the
88
+ thumb". The AAA gap is written down in the stylesheet rather than left implicit.
89
+
90
+ - **`gog-datepicker` and `gog-calendar` are on the grid, at a ratio of 2.0.** The calendar's
91
+ footer buttons go from 4/10 (a ratio of 2.50, the only block in the library padded *too* wide
92
+ for its height) to 4/8; the time row's gap 6 to 8 and its input 4/6 to 4/8; the datepicker's
93
+ actions gap 6 to 8 and its panel gap 2 to 4.
94
+
95
+ - **`gog-autocomplete`'s option row is 8/16 at a ratio of 2.0**, from 8/10, with its gap 10 to 12,
96
+ the actions gap 6 to 8 and the panel gap 2 to 4. An option row is a control — it is what a
97
+ pointer picks — so it takes the ratio while the panel holding it does not.
98
+
99
+ - **`gog-multiselect`'s panel chrome is on the grid** — the controls row 6/10 to 8/12, the
100
+ actions gap 6 to 8, the filter row 6 to 8 with its input at 8/16 (a ratio of 2.0, from 1.33),
101
+ the empty state 10 to 12, the `+N` overflow chip's gap 6 to 8, and the panel offset 2 to 4.
102
+
103
+ - **`gog-select`'s panel chrome is on the grid** — the filter row's padding 6 to 8, its input
104
+ 8/16 at a ratio of 2.0 (from 1.33), the empty-state padding 10 to 12, the clear button's gap 6
105
+ to 8, the option row's gap 10 to 12, and the panel's offset from the field 2px to 4px.
106
+
107
+ - **`gog-dialog`'s header padding is on the grid** — its bottom side goes from 14px to 16px, so
108
+ the header is padded 16/20/16 rather than 16/20/14.
109
+
110
+ - **`gog-menu`'s item padding is 8/16 at a ratio of 2.0**, from 8/12, and the panel's own gap
111
+ goes from 2px to 4px. A menu item is a control — it is the row a pointer activates — so unlike
112
+ the panel around it, the ratio applies.
113
+
114
+ - **`gog-tooltip`'s padding is 8/12, not 6/10** — on the grid. A bubble around text rather than a
115
+ control, so it stays outside the optical ratio.
116
+
117
+ - **`gog-toast` is on the grid** — the stack gap 10 to 12, its own padding 12/14 to 12/16, the
118
+ content gap 10 to 12, the action row's gap 6 to 8 and the close button's padding 2/4 to 4/8. A
119
+ surface, so outside the optical ratio; its two buttons are controls and are checked as targets.
120
+
121
+ - **`gog-card`'s four off-grid lengths move to the grid** — `xsm` side padding 10 to 12 and its
122
+ gap 6 to 8, `sm` side padding 14 to 16, `lg` gap 14 to 16. The card is **outside** the
123
+ optical-ratio law and stays at roughly 1.2: it frames projected content rather than balancing a
124
+ label, and 2.0 would give it twice as much padding at its sides as above.
125
+
126
+ - **`gog-table`'s cell padding is on the grid** — vertical `4/4/8/12/16` per size and horizontal
127
+ 16px, from `2/2/6/10/14` and 14. A row is 2 to 4px taller at every density. The table is
128
+ deliberately **outside** the optical-ratio law: a cell's padding is row density and a column
129
+ rhythm, not a label balanced inside a control, and 2.0 would put 32px between every column.
130
+ The sortable header cell is still checked as a pointer target, which is where the law that
131
+ applies to it lives.
132
+
133
+ - **`gog-tabs` is on the grid at a ratio of 2.0** — `8/16`, `8/16`, `12/24`, `16/32`, `20/40`
134
+ (vertical/horizontal), from 1.67, 1.75, 1.50, 1.50, 1.40. `xsm` and `sm` now share a padding and
135
+ are told apart by their type, as everywhere else in this release.
136
+
137
+ - **`gog-accordion`'s header is on the grid at a ratio of 2.0**, and it had the widest drift in
138
+ the library: 2.00, 1.67, 1.40, 1.14, 1.00 — by `slg` the header was padded equally on all four
139
+ sides, which is a box rather than a row. Now `4/8`, `8/16`, `12/24`, `16/32`, `20/40`. The body's
140
+ two off-grid paddings and the three 2px content gaps move with it.
141
+
142
+ **An `xsm` header is 24px tall**, up from 20. It grows rather than inflating a hit area because
143
+ headers stack directly on one another: an invisible target reaching past the row would overlap
144
+ the neighbouring header's, and two targets claiming the same pixels is worse than one small
145
+ target.
146
+
147
+ - **`gog-chip` is on the grid at a ratio of 2.0, and it is the one component whose paint had to
148
+ grow.** Padding goes to `4/8`, `4/8`, `8/16`, `12/24`, `12/24` (block/inline) and the gaps to 4,
149
+ 8, 8, 12, 12.
150
+
151
+ **A chip is 24px tall at minimum now**, which adds 0.8px at `xsm` and nothing at any other size —
152
+ `sm` measures 24.4px on its own. Everywhere else in this release an undersized target grew an invisible hit area and
153
+ the paint stayed put; a chip cannot do that, because `.gog-chip__surface` clips — the ripple and
154
+ the pill shape both need `overflow: hidden` — so a hit area larger than the surface is cut off at
155
+ the surface's edge. The same line is what gives the remove button room: a 24px target inside a
156
+ 21px box is impossible for exactly the same reason.
157
+
158
+ **The remove button's target is 24×24 while its glyph stays between 13px and 22px.** An X drawn
159
+ at 24px inside a chip would be the loudest thing in it.
160
+
161
+ - **`gog-toggle`'s state chrome is on the grid** — the offset of the on/off state labels and the
162
+ gap between a state label and the thumb both go from 6px to 8px. **The thumb's inset stays at
163
+ 2px**, deliberately and now with the reason written next to it: at 2px the thumb is 83% of a
164
+ 24px track, which is what makes a switch read as a switch, and 4px would take it to 67%. A
165
+ length inside a single painted mark is not spacing between two things.
166
+
167
+ - **`gog-radio-group`'s two gaps are 12px, not 10px** — between a radio and its label, and
168
+ between the options in a group.
169
+
170
+ - **`gog-checkbox`'s gap between box and label is 12px, not 10px** — its one length off the 4px
171
+ grid.
172
+
173
+ - **The shared field tier is on the grid, at a ratio of 2.0.** Every text-bearing control that
174
+ reads it — `gog-inputfield`, `gog-textarea`, `gog-select`, `gog-multiselect`, `gog-autocomplete`
175
+ and `gog-datepicker` — is padded `4/8`, `8/16`, `12/24`, `16/32`, `20/40` (vertical/horizontal).
176
+ It had run 2.00, 1.67, 1.40, 1.29, 1.11 across the five sizes, drifting further from the ratio at
177
+ every step up; `slg` gains the most, from 20px of side padding to 40px. The icon offsets follow
178
+ the same grid — `xsm` 6 to 8, `lg` 14 to 16, `slg` 18 to 20 — which moves where the clear button
179
+ and the chevron sit inside a field.
180
+
181
+ - **`gog-button`'s padding is on the grid, at a ratio of exactly 2.0.** Horizontal padding is twice
182
+ vertical at every size step: `4/8`, `8/16`, `12/24`, `16/32`, `20/40` (vertical/horizontal). It
183
+ had run 2.00, 1.75, 1.67, 1.50, 1.40 across the five — monotonic drift, which is what per-size
184
+ eyeballing looks like from the outside. **`xsm` does not move and every other size gets wider**;
185
+ `slg` gains the most, from 28px of side padding to 40px.
186
+
187
+ - **Every `gog-button` carries a 24×24 pointer target, whatever its padding says.** A transparent
188
+ `::before` centred on the button supplies the size when the button is short, and no painted edge
189
+ moves. It is inert whenever the button is already big enough — which, it turns out, includes
190
+ `xsm` at its own padding: 4 + 12 + 4 is 20, but the button draws a 2px border on each side and
191
+ `box-sizing` is `border-box`, so it measures 24 in a browser. What the rule is actually for is a
192
+ button whose padding has been overridden, like `gog-toast`'s own two, which land at 20px at any
193
+ size class.
194
+
195
+ - **`gogBadge` hangs 8px outside its host's corner, not 6px.** The badge's one off-grid length, and
196
+ the commit that had to settle the tie-break behind all of them: every off-grid step this library
197
+ used sat exactly halfway between two grid steps, so snapping to 4px is a *direction* rather than
198
+ a rounding rule. It rounds up. Here that is also right on its own terms — a 20px badge clears the
199
+ host's corner radius at `--gog-radius: 8px` at 8px of overhang, and did not quite at 6px.
200
+
201
+ - **`gog-autocomplete`'s option row is concentric with its panel at every density.** The radius was
202
+ `calc(var(--gog-radius) - 4px)`, which is the right number only where `--gog-density` is 1: the
203
+ 4px it subtracts is the list's own padding, and that padding scales with density while a restated
204
+ literal does not. It now reads the two tokens it is derived from, so at `--gog-density: 0.85` the
205
+ corner is 4.6px rather than 4px and the gap to the panel stays constant. Nothing moves at the
206
+ default density.
207
+
208
+ - **`gog-menu`'s item corner is 12px, not 8px.** The menu panel paints `--gog-panel-radius`, which
209
+ is the base radius plus 8, and insets its items by `--gog-menu-padding` (4px) — so a concentric
210
+ item corner is 16 − 4 = 12px, and the item had been repeating the base `--gog-radius` instead.
211
+ At the default theme the first and last items were visibly squarer than the corner they sit in.
212
+ `--gog-menu-item-radius` now derives from the panel radius and the padding rather than restating
213
+ a value, so a theme that changes either carries the item with it.
214
+
215
+ - **`gog-multiselect`'s panel interior is concentric with the panel — two corners, both derived.**
216
+ The option row went from 6px to 4px (8px panel, 4px of list padding), and **the filter input's
217
+ corner is now square**. The second one looks like a value clamped to zero and is not: the filter
218
+ is inset by 8px, which is exactly the panel's radius, and where the inset equals the radius the
219
+ inner box's corner point lands on the centre of the outer arc — so a right angle is equidistant
220
+ from the whole curve, and it is the only shape that keeps the gap constant. Both now read the
221
+ panel radius and their own padding rather than restating a number, which is also what makes them
222
+ hold at a density other than 1.
223
+
224
+ - **`gog-select`'s filter input has a square corner**, for the same reason the multiselect's does:
225
+ it is inset by exactly the panel's radius, and at that distance a right angle is the only corner
226
+ that stays an equal distance from the panel's curve. `--gog-select-filter-input-radius` derives
227
+ from the panel radius and the filter padding rather than repeating `--gog-radius`.
228
+
229
+ - **`gog-scroll`'s thumb derives its corner from the track**, `--gog-scroll-track-radius` minus
230
+ `--gog-scroll-thumb-inset`, where it had repeated `--gog-radius`. **No pixel changes today**: at
231
+ both track widths (10px and 6px, less 2px of inset per side) either radius already exceeds half
232
+ the thumb's width and renders as a full pill. It matters to a theme that raises `--gog-radius`
233
+ on a wide scroller, where the thumb would otherwise carry the same corner as the track it sits
234
+ inside.
235
+
236
+ - **`gog-accordion`'s chevron is a ratio of its header instead of a px ladder, and it fits its box
237
+ now.** Ten literals become two: `1em` for the em basis and `1.4em` for the surrounding box.
238
+ Writing it as a ratio surfaced two bugs the ladder had hidden. It was **px beside a label in
239
+ `rem`**, so raising the browser's text size grew the label and left the chevron where it was —
240
+ and its box/label ratio drifted 0.92 to 1.29 across the five sizes. And **the glyph overflowed
241
+ its box at every size**: the font-size token is the em basis for the `gog-icon` inside, which
242
+ renders at 1.2em of it, so a 21.6px mark sat in a 20px box. The chevrons at `xsm`, `sm` and `md`
243
+ are identical now, because those three sizes all label with `--gog-text-xs` and the chevron
244
+ ladder was the only thing distinguishing them.
245
+
246
+ - **`--gog-panel-heading-line-height` is `snug` (1.3), not 1.25** — the one leading in the library
247
+ outside `--gog-line-height-*`, which runs 1.2 then 1.3 with nothing between.
248
+
249
+ - **Eight literal `line-height` declarations in component stylesheets now read tokens**, so a theme
250
+ can reach them at all: the checkbox and input icons, the three clear buttons, the toggle's state
251
+ label and the textarea's own field, which is the one place the library renders a paragraph the
252
+ consumer typed. The dialog's was dead code — its close button already read the token two lines
253
+ below.
254
+
255
+ - **`gog-tooltip`, `gog-menu` and `gog-toast` cap themselves against the viewport.**
256
+ `--gog-tooltip-max-width`, `--gog-menu-max-width` and `--gog-toast-max-width` each become
257
+ `min(<cap>, calc(100vw - <margin> * 2))` — an overlay positioned against the screen itself rather
258
+ than a container, where "no wider than the screen" is what the component is for. The margin reads
259
+ the component's own edge-inset token where one exists (toast's `--gog-toast-stack-padding`)
260
+ rather than a new one; menu and tooltip, which have none, read `--gog-space-16` directly.
261
+
262
+ **`gog-confirmation-dialog` deliberately does not get one**, though an earlier draft of this
263
+ release gave it one: it renders inside `.gog-dialog__panel`, which already defaults to `90vw`,
264
+ and the dialog body pads 20px a side inside that — so the width available to it is
265
+ `0.9 * 100vw - 40px`, tighter than any `100vw - margin` clamp above an 80px viewport. The clamp
266
+ could never bind, and an inert declaration is worse than an absent one. Its cap is the measure
267
+ alone. The three dropdown panel widths (`autocomplete`/`select`/`multiselect`, 420px) are
268
+ unchanged for a related reason — each already tracks its trigger field's own width via
269
+ `min-width: 100%`.
270
+
271
+ **A `min-width` outranks a `max-width`, so each overlay's own floor still decides below a
272
+ certain width**, unchanged by this release: menu at 212px of viewport, toast at 312px — both
273
+ narrower than any device in use.
274
+
275
+ - **Three caps move from `px` to `ch`**: tooltip `43ch`, toast `53ch`, the
276
+ confirmation dialog `51ch` — each the nearest whole character to what the cap already rendered,
277
+ so a consumer raising the relevant font-size token now widens the bubble with it instead of the
278
+ text silently dropping from 47 characters a line to 30. Menu stays in `px`: its items do not
279
+ wrap, so a character measure would be measuring nothing. `docs/component-geometry.md`, "D7 —
280
+ taken", has the survey and the reasoning, including a live-caught bug in the fix itself: `ch`
281
+ resolves against the font of the element `max-width` is declared on, not a descendant's, which
282
+ toast's own message/container split got wrong on the first pass.
283
+
284
+ ### Fixed
285
+
286
+ - **`gog-dialog`'s title now states its own size.** `.gog-dialog__title` (the library's only
287
+ `<h2>`) declared neither `font-size` nor `line-height`, so it rendered at the browser's default
288
+ 24px bold rather than a token. `--gog-dialog-title-font-size` reads `--gog-text-xl` — exactly
289
+ today's rendered size, so nothing changes visibly — and `--gog-dialog-title-line-height` reads
290
+ `--gog-line-height-snug`, law 4's `heading` role. A theme raising `--gog-text-xl` now resizes the
291
+ dialog title with it, which it previously could not.
292
+
293
+ - **`gog-confirmation-dialog`'s title and description now read the type scale.** Both carried a
294
+ dead utility class (`heading-md`, `body-sm`) with no CSS rule anywhere in the library, so the
295
+ title rendered at the browser's default `<h3>` size (18.72px) and the description at the
296
+ inherited 16px, neither following `--gog-text-*`. The classes are removed; the title now reads
297
+ `--gog-text-lg` (18px, the nearest step to what already rendered — a dead class named `md` is
298
+ not evidence it was meant to be one) and the description reads `--gog-text-sm` (14px, a visible
299
+ 2px shrink, the size every other muted secondary line in the library already uses). A theme
300
+ moving the scale now moves both with it. `--gog-confirmation-dialog-max-width` (51ch) is
301
+ unaffected — a `ch` cap resolves against the font of the element `max-width` is declared on
302
+ (`.confirm-dialog`), not the description's, so the cap's pixel width does not change; only the
303
+ measure it produces against the now-smaller description does, and it stays in L9's 45–75 band.
304
+
305
+ ### Documentation
306
+
307
+ - **Every class a library template applies to its own markup is checked for the `gog-` prefix**
308
+ (`npm run check:class-names`, a CI step). The confirmation-dialog defect above is exactly what an
309
+ unprefixed class can hide: two utility classes with no CSS rule anywhere in the library. The
310
+ check does not require every class to have a rule — roughly 50 of the library's own state and
311
+ behaviour hooks (`gog-scroll--dragging`, `gog-select--floated`, …) legitimately have none — only
312
+ that it be named as this library's own rather than a name that looks borrowed or invented in
313
+ passing. Two legacy unprefixed names on shipped components (`confirm-dialog*`, `slide-left`) are
314
+ named exceptions rather than renamed, since a rename there is a consumer-visible class change.
315
+
316
+ - **The icon set is verified centred, and the audit reversed the rule it was written for.** All 41
317
+ built-in glyphs now pass a check (`npm run check:geometry`, second half): a glyph centres its ink
318
+ in its own viewBox, so that centring the box centres the mark. Nothing moved — every glyph
319
+ already passed, to within a hundredth of a unit horizontally.
320
+
321
+ The audit is the interesting half. The rule had been planned against the ink's **centre of
322
+ mass**, and by that measure the set looks broken: `arrow-right`'s mass sits 2.05 units right of
323
+ centre and `download`'s 3.47 units low, on a 24 grid. Neither is a defect. A directional glyph is
324
+ *supposed* to carry its mass toward its head, and re-centring one would pull its tail off the
325
+ edge of the box. What the eye reads in a uniform-weight set is the extent, so the extent is what
326
+ is checked — with one branch for a **filled** mark, where a solid triangle's centroid genuinely
327
+ sits a sixth of its width off the box centre. The registry has one filled glyph (`star-filled`,
328
+ 0.51 low) and that branch is what would catch a play triangle the day one is added.
329
+
330
+ - **`README.md` carries the recipe for making the library fluid**, which it had never stated and a
331
+ consumer had to derive: the two-point linear interpolation, worked, applied to the root font size
332
+ (the type scale is in `rem`, so one `clamp()` moves all of it) — with the reason the intercept
333
+ belongs in `rem` rather than `px`, which is WCAG 1.4.4. It also says plainly why the library
334
+ itself ships zero `clamp()`, zero `vw` and zero breakpoints, and how to carry the spacing scale
335
+ along if you want gaps to grow with the type.
336
+
337
+ - **`AGENTS.md`'s dropdown section says when to turn `filter` on**, and the narrower half of the
338
+ rule that most quotations of it drop: choice time grows with the log of the option count, so a
339
+ panel past roughly seven options wants a filter — *unless* the list is one the reader can
340
+ predict, in which case they are searching rather than choosing and ordering it well is worth as
341
+ much. `GOG_CONFIG.dropdown.filter` sets it once for an app.
342
+
343
+ - **`README.md`'s fluid-sizing section is corrected, and it was wrong before this release as well
344
+ as after it.** It said the library ships zero `clamp()`, zero `vw` and zero breakpoints. The
345
+ overlay caps above made the first half false; the second half had never been true — a dialog
346
+ panel has defaulted to `90vw` and `--gog-dialog-max-height` to `90vh` for as long as both have
347
+ existed, `gog-menu` falls back to `100vh`, and `gog-table`'s `maxHeight` takes `'60vh'` as its own
348
+ documented example. The section now scopes the claim to *component sizing*, lists every viewport
349
+ unit the library actually contains, and says why none of them is the recipe beside it: the recipe
350
+ grows a size with the viewport, these only ever cap one against it.
351
+
352
+ Worth keeping for the shape of the mistake rather than the fact: the claim was checked by
353
+ grepping `.css` and `.scss`, and every counterexample but one lives in exactly those files. The
354
+ one that does not — the dialog panel's `90vw` — is an inline `[style.max-width]` binding in a
355
+ template, and it is the one that mattered, because it is what makes a clamp on the confirmation
356
+ dialog unable to bind. `npm run survey:measure` now inventories viewport units across CSS, SCSS
357
+ **and** templates, so the next such claim is measured rather than recalled.
358
+
359
+ ## [21.10.0] - 05.09.2026
360
+
361
+ ### Added
362
+
363
+ - **`gog-progressbar` marks where its fill ends**, with two hairlines rather than a colour
364
+ difference — `--gog-progressbar-edge-color` (the theme's ink, outermost, against the track),
365
+ `--gog-progressbar-edge-backing-color` (the surface colour, inside the fill) and
366
+ `--gog-progressbar-edge-width`, 1px each. Both are inset shadows on the fill, so they cost no
367
+ layout and cannot push a 100% bar past its own track, and they flip end under `dir="rtl"`
368
+ through `--gog-direction-sign`.
369
+
370
+ **The boundary is the value.** `showValue` is `false` by default, so nothing else states it, and
371
+ WCAG 1.4.11 asks 3:1 of the part of a graphic that carries its meaning. The fill and track could
372
+ not carry it: 51 of the 55 shipped fill/track combinations were under 3:1, and that is not a
373
+ palette bug — the five fills straddle mid-luminance in every theme, so a track dark enough for
374
+ the amber warning lands on the blue info. Sweeping the entire `--gog-text-color`-to-
375
+ `--gog-border-color` axis, the best achievable worst-fill ratio is 1.58 to 3.45 per theme, and
376
+ only `primeng` clears 3:1 at all.
377
+
378
+ One marker tone does not work either (`--gog-surface-color` is 1.79:1 on `material`'s warning
379
+ fill, `--gog-text-color` 1.06:1 on `one-dark`'s success). Two do, and `check:contrast` gates them
380
+ as a pair: every neighbour — the track and all five fills — must clear 3:1 against the better of
381
+ the two tones. Worst case across the eleven themes is 3.25:1.
382
+
383
+ Found by rendering the component in greyscale, which is what a reader with achromatopsia sees:
384
+ on `primeng`, success, warning and info had no visible boundary at all. The showcase's
385
+ progressbar page carries that comparison as a toggle.
386
+
387
+ ### Fixed
388
+
389
+ - **`GOG_CONFIG.spinner.component` now reaches `gog-spinner-overlay`.** The overlay forwards its
390
+ own `variant` to the spinner it wraps, and that input defaulted to `'runic'` rather than to
391
+ nothing — so the spinner inside read it as an instance asking for the built-in look, and the
392
+ config, whose rule is that an explicit `variant` outranks it, correctly stepped aside. An app
393
+ that had set a house spinner got it everywhere except on the one component it reaches for to
394
+ cover a whole region while it loads. `variant` is now `GogSpinnerVariant | undefined` and
395
+ defaults to unset, so "asked for nothing" survives being passed down; `size` and `ariaLabel`
396
+ keep their defaults, having no config key to fall through to.
397
+
398
+ Nothing changes for an app that configures no spinner: an overlay with no `variant` still
399
+ renders `runic`. The type widens rather than narrows, so a binding that passes a
400
+ `GogSpinnerVariant` still compiles; only code that *reads* `overlay.variant()` now has to
401
+ account for `undefined`.
402
+
403
+ Found from the documentation side rather than from a report — the same way 21.8.0's four
404
+ under-reported `GOG_CONFIG` keys were. `spinner-config.spec.ts` had mounted only `gog-spinner`
405
+ and `gog-button`, which is why every test passed over a key that missed a third of its targets;
406
+ it now mounts an overlay too, in four cases.
407
+
408
+ - **A disabled toggle button keeps its ring.** `.gog-btn[aria-pressed='true']` carried a
409
+ `:not(:disabled)` guard, copied from the hover and press rules above it where the guard is
410
+ correct. It is not correct here: `disabled` on a real `<button>` does not remove `aria-pressed`,
411
+ so a disabled toggle announced itself as on and looked exactly like the off one beside it —
412
+ which is the WCAG 1.4.1 failure the ring was added to prevent, in the one state nobody looks at.
413
+ `gog-chip`'s `selected` ring never had the guard, and `docs/backlog.md` had the asymmetry filed
414
+ with the chip named as the correct side; this settles it that way. The disabled opacity dims the
415
+ ring with the rest of the button, which is the right amount of "unavailable".
416
+
417
+ The guard was also paying for something invisible: it made the rule (0,3,0), level with
418
+ `:hover:not(:disabled)`, which is what lets the ring survive a pointer by source order. The
419
+ selector is now `.gog-btn.gog-btn[aria-pressed='…']` — the same doubled class `:focus-visible`
420
+ and `:disabled` already use, which restores that and protects the state from a consumer's
421
+ single-class rule.
422
+
423
+ **`check:state-specificity` did not catch the weakened rule when it was tested against it**, and
424
+ that turned out to be two bugs in the check: `[aria-pressed` was not in its list of states, and
425
+ its specificity arithmetic read a quoted attribute value as an element name, floating
426
+ `.gog-btn[aria-pressed='true']` from (0,2,0) to (0,2,1) — over the consumer floor it is there to
427
+ enforce. Both fixed, `:not()` no longer counted as a class of its own, and the function now
428
+ self-tests against eight selectors on every run, because a specificity check that miscounts does
429
+ not fail loudly: it passes a rule it should have caught.
430
+
431
+ - **Five WCAG AA failures on shipped variants**, none of which any check had ever looked at.
432
+ `check:contrast` took its pairs from rules that set `color` and `background-color`, and a variant
433
+ class in this library sets neither — `.gog-tag--danger` re-points `--gog-tag-variant-color`,
434
+ `.gog-btn--outline` re-points `--gog-button-variant-*`, and one painting rule reads that layer
435
+ through a `var()` chain. The script now resolves the indirection (and a rest state with no
436
+ modifier at all, which nothing measured either), taking it from 1155 pairs to 2187. What it
437
+ found:
438
+
439
+ - **`gog-tag`'s label was mixed toward literal `black`, at 82% of the status hue.** That is a
440
+ light-theme assumption twice over: on a dark theme it darkened a label already sitting on a
441
+ dark ground, and on a light one 18% of black cannot bring a bright hue down to text contrast.
442
+ Eleven of the 55 shipped combinations were under AA — `one-dark`'s danger at 3.11:1,
443
+ `material`'s warning at 2.49:1, `primeng`'s success at 3.06:1. `--gog-tag-color-base` is now
444
+ `var(--gog-text-color)` so the mix follows the theme, and `--gog-tag-color-mix` is 50%: the
445
+ same ratio, and the same argument, as `--gog-button-<status>-ink`. Worst pair is now 4.67:1.
446
+ **This changes how every status tag looks** — the label is a softer, inkier version of its
447
+ hue, and the background is untouched.
448
+ - **The table's header labels** were the raw accent on a tinted strip: 4.15:1 in `light`, 4.32:1
449
+ in `one-light`. `--gog-table-accent-color` now mixes 20% of the page's ink into the accent
450
+ inside the `@supports` block, which lifts every theme (worst 5.09:1) and keeps the accent
451
+ identity the header is drawn in. The flat fallback stays the raw accent.
452
+ - **`slate`'s secondary button** filled with Tailwind sky-500 under a white label: 2.77:1.
453
+ sky-600 does not clear either (4.10:1), so `--gog-secondary-color` moves two steps to sky-700
454
+ — the same move the theme's three status colours made in 21.9.0.
455
+
456
+ A false positive is recorded rather than silenced: `.gog-checkbox__box` states the tick's colour
457
+ and the *unchecked* box's background in one rule, and those never render together, so the sweep
458
+ read 1.00:1 in `ledger`. It is the one entry in the script's `REST_PAIRS_NOT_RENDERED` list, and
459
+ the pair that does render — the tick on the checked background — is measured and passes.
460
+
461
+ `token-color.mjs` learned two things it needed for this: a `color-mix()` percentage that is
462
+ itself a token (`--gog-tag-color-mix: 82%`), and the named colours `black`/`white`. Without them
463
+ `gog-tag` resolved to nothing at all and its nine variant pairs were skipped in every theme —
464
+ the checker failing open, which is the failure mode this change exists to remove.
465
+
466
+ ### Documentation
467
+
468
+ - **`gog-table` is named among the spinners `GOG_CONFIG.spinner` reaches**, in `AGENTS.md` and in
469
+ `GogGlobalConfig`'s own JSDoc. It renders a bare `gog-spinner` in place of its rows and has
470
+ always honoured both keys; it appeared in no "Applies to" sentence because it reads no config
471
+ itself, so a grep for readers never finds it. Documentation only — the behaviour was already
472
+ correct, which is exactly what makes this class of defect quiet.
473
+
7
474
  ## [21.9.1] - 04.09.2026
8
475
 
9
476
  ### Fixed