@guildofgleks/ui 21.12.0 → 21.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,618 @@ 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.13.0] - 13.09.2026
8
+
9
+ ### Added
10
+
11
+ - **`gog-table` windows its rows — `virtualize`, off by default, and it needed a second
12
+ primitive.** The dropdowns' `GogVirtualWindow` takes one row height, and a table cannot supply
13
+ one: **a table row's height cannot be pinned**, because `height` on a `<tr>` _and_ on a `<td>` is
14
+ a **minimum** in table layout. One cell taken from 40 to 600 characters measured 39px → 173.75px
15
+ under `table-layout: fixed`, with the column width unchanged, and neither `height` on the row nor
16
+ on its cells moved it.
17
+
18
+ So `GogVariableWindow` (internal, `lib/shared`): a height per row — measured where a row has
19
+ rendered, estimated everywhere else — with a prefix sum, so the range is a binary search rather
20
+ than a division. The three dropdowns keep the fixed-pitch one, which is exact where this is only
21
+ ever as right as the rows it has seen.
22
+
23
+ **It requires `maxHeight` and `fullWidth`, does nothing without either, and says which is missing
24
+ in a dev-mode warning.** Both are measured constraints rather than preferences: without
25
+ `maxHeight` the table never scrolls vertically on its own, so there is no viewport to window
26
+ against; and `fullWidth="false"` means `table-layout: auto`, where the browser sizes columns from
27
+ the rows that are _rendered_ — measured, rendering 2 of 24 rows moved columns by up to 7.8px, so
28
+ a windowed table would shift its own columns as you scroll.
29
+
30
+ The `<tbody>` spacers are `<tr>`s, since a table body takes rows and nothing else; a row honours
31
+ an explicit height exactly, including at 400 000px. **The ceiling is Chrome's, not the
32
+ library's:** an element clamps at 33 554 426px, about 745 000 rows at 45px — and a dev-mode
33
+ warning says so once when a table passes it, because past that the rows stay correct while the
34
+ scrollbar stops reaching the end of the data, which is not a symptom anyone traces back to a row
35
+ count.
36
+
37
+ **Three indices would have changed meaning silently, and all three are public promises**:
38
+ `gogRowClick`'s `index` (documented as the index within the page), the `showRowNumbers` column,
39
+ and `GogColumnBodyContext.index` in every consumer's cell template. Under a window `$index` is
40
+ the position in the rendered slice, so each keeps its name, its type and its documentation while
41
+ meaning something else. Specs fail without the fix with `expected +0 to be 196` and
42
+ `expected '1' to be '197'`. Checked rather than assumed on the other side: `toggleAllOnPage` and
43
+ the header checkbox still read the page, not the window — select-all would otherwise have
44
+ selected the twenty rows on screen while claiming a thousand.
45
+
46
+ **Two of the three things the plan called hard were not.** The sticky header lives in `<thead>`,
47
+ which a window over `<tbody>` never touches — pinned exactly at the viewport top across 200 000px
48
+ of scroll past a 400 000px spacer. The selection column is an ordinary `<td>` per row. That is
49
+ the third release running where the predicted hard part cost nothing.
50
+
51
+ **And one bug came out of the live pass that no spec would have suggested.** The effect that
52
+ clears cached heights on a new page or sort calls `reset()`, and `reset()` _reads_ the
53
+ measurement signal to decide whether it has anything to clear — so, called bare inside an effect,
54
+ that read became one of the effect's dependencies. Measuring wrote the signal, the effect re-ran,
55
+ and it cleared the measurements that had just been taken. **Nothing looked wrong**: the right
56
+ rows rendered at the right heights, and only the scroll height was quietly the estimate times the
57
+ row count, for ever. `untracked` is the fix and a regression spec now asserts it — it fails
58
+ without it on a spacer that is exactly `982 × 30`, a whole number of estimates.
59
+
60
+ The estimate is seeded from the **median** of the first rendered batch rather than its first row.
61
+ Taking row 0 was the first version, and the showcase's own demo is why it is wrong: it makes
62
+ every seventh row wrap, row 0 among them, so the estimate came out 65% high and all 10 000 rows
63
+ were sized from the one row that least resembles them.
64
+
65
+ - **`gog-select` windows its option list — `virtualize`, off by default.** Measured in Chrome on
66
+ one page holding two selects over the same 10 000 options: the eager panel takes **512ms** to
67
+ appear and builds **10 000 rows to show six**; the windowed one takes **21ms** and holds **10**.
68
+ Both scroll through an identical 450 008px of content, because spacers stand in for the rows
69
+ that are not there. A per-field input, with `GOG_CONFIG.dropdown.virtualize` as the app-wide
70
+ default.
71
+
72
+ **Never automatic above some row count**, which is the same call `GOG_CONFIG.ripple.enabled`
73
+ makes: a windowed list differs from a plain one under `Ctrl+F`, under a screen reader's "list
74
+ all items", and under consumer CSS targeting `:last-child`, so flipping it when enough rows
75
+ happen to arrive is behaviour that depends on how much data turned up — it works in development
76
+ and surprises in production.
77
+
78
+ Under it, `GogVirtualWindow` in `lib/shared` (added below, internal): the arithmetic and nothing
79
+ that touches the page, so the component keeps its own scroller. The four things
80
+ `docs/virtualization.md` said were easy to get wrong all needed work, and three of them landed
81
+ differently from the plan:
82
+
83
+ - **The count stays honest.** `aria-setsize` and `aria-posinset` carry the real list and the
84
+ real index, so a listbox holding twenty rows is announced as ten thousand items. Set only
85
+ while windowing — an unwindowed list has every row present and the browser's own count is
86
+ both right and free.
87
+ - **The keyboard moves an index, not an element.** ArrowUp from the trigger means option 10 000,
88
+ which is not in the DOM to be focused: the index moves first, the scroll follows, and the row
89
+ is focused after the render that stamps it. `nextRovingFocusIndex` already existed as the
90
+ index half of the roving-focus helper, so the navigation rules — wrapping, skipping disabled,
91
+ Home/End meaning first/last _reachable_ — did not have to be written twice.
92
+ - **The panel's height is read from the scroller, not from a token.** The plan asked for a
93
+ `ResizeObserver`; `gog-scroll` already runs one and already coalesces scroll and resize into
94
+ one rAF-batched emission carrying both `scrollTop` and `clientHeight`, so `(gogScroll)` is the
95
+ whole answer and a second observer would have measured the same element a frame later. A
96
+ reported height of **zero** is ignored rather than believed: the scroller's first emission can
97
+ land before the panel has a height, and taking it literally renders the whole list for a frame
98
+ — the one thing the seed exists to prevent.
99
+ - **Filtering resets the window and the scroller together** — and this one is narrower than the
100
+ plan claimed. `GogVirtualWindow` already clamps a scroll position past the end of its own
101
+ list, so filtering 10 000 options down to three cannot render rows 400–420 of a three-row
102
+ list; the plan's own example is handled for free. The real case is a filtered list still long
103
+ enough to scroll, where the old position clamps to a **valid** position in the new list and
104
+ the search shows the end of its results. The first spec written for this passed with the reset
105
+ removed, which is how that was found.
106
+
107
+ One thing the plan did not have at all. **A row scrolled out of the window is unmounted, and an
108
+ unmounted element holding focus drops it on `<body>`** — where an open panel has no keyboard:
109
+ Escape does not close it and the arrows scroll the page. So a mouse scroll that would take the
110
+ focused row away hands focus back to the trigger first, which Escape and ArrowDown both work
111
+ from. It is checked before the re-render rather than after, because once the row is gone there
112
+ is nothing left to ask whether it was the focused one. This is a cost a plain list does not pay
113
+ and part of why windowing is opt-in.
114
+
115
+ `gog-multiselect`, `gog-autocomplete` and `gog-table` do not window yet.
116
+
117
+ - **`npm run check:layering` — the library's own layering, as a build failure.** Two rules:
118
+ nothing in `lib/shared/` may import a component or a service, and no two units may import each
119
+ other. It exists because both were being broken and neither hurt anything: **a cycle is
120
+ invisible until something tries to cut along it.**
121
+
122
+ `shared/config.ts` imported `ToastPosition` from `services/toast-service`, which imports
123
+ `shared/config`. `shared/tooltip-overlay.ts` imported a component out of `components/tooltip/`,
124
+ which imports `shared/`. In one flat bundle, harmless. To a secondary entry point, fatal —
125
+ ng-packagr refuses a cycle between entry points outright — which is what makes this the
126
+ prerequisite for that work rather than tidying.
127
+
128
+ Both are fixed: `ToastPosition` moved to `shared/types.ts`, where
129
+ `gleks-ui-library.instructions.md` already said public types live, and is re-exported from the
130
+ service so the package's surface is unchanged; `tooltip-overlay.ts` moved into
131
+ `components/tooltip/`, which is its only consumer and the component it stamps. 36 units, no
132
+ cycles.
133
+
134
+ - **`npm run check:glyph-box` — the first check in this repo that measures a rendering.** Every
135
+ other one reads source; this rule cannot be read honestly from source, because a glyph is
136
+ `--gog-icon-size` (1.2em) of its element's resolved font-size and **an `em` attaches to the
137
+ element carrying the property, not the one the value was written for**. Guessing that produced a
138
+ "fix" 25% worse at `slg` in this very release. So it serves the prerendered `ui-showcase`, walks
139
+ all 46 routes in Playwright against the installed Chrome, and compares every `<gog-icon>`'s
140
+ `<svg>` to the element holding it. 580 icons.
141
+
142
+ The rule has **no exemption list**, which no other check here can say: a box roomier than its
143
+ mark is fine, smaller never is.
144
+
145
+ **Two things went wrong in the check before it was right, and both are the lesson.** It first
146
+ walked every route on one page, so findings depended on visit order — the showcase persists
147
+ theme and density, and a route measured after the themes page inherited whatever it had left
148
+ set. And it compared against the _content_ box, which reported `gog-checkbox`: a 12px tick
149
+ spanning its own 2px outline, which is what a checkbox is. **The border box is the box.**
150
+
151
+ - **`npm run check:tokens` rule K — a token `theme.css` declares that nothing reads.** The mirror
152
+ of rule F, which has always caught the opposite direction: a `var()` read with no declaration.
153
+ Nothing caught a declaration with no reader, so a component could ship a documented knob wired to
154
+ nothing and every gate this project has would pass. `--gog-menu-panel-gap` was exactly that for
155
+ the whole life of `gog-menu` (fixed below).
156
+
157
+ Reads are collected from component stylesheets, the global ones, `theme.css` itself **and
158
+ TypeScript**, because a token read only from script is still read. That last half took two
159
+ passes: TypeScript spells a token two ways — a bare name handed to `resolveLengthToken`, and a
160
+ whole declaration built as a string (`'var(--gog-control-checkbox-box-size-lg, 32px)'`, which a
161
+ host binding writes) — and the first version matched only the first shape, reporting **eight
162
+ live tokens as dead**. A check that cannot read half its inputs is worse than no check, because
163
+ its findings are what you act on.
164
+
165
+ Five exemptions, all steps of a complete public scale (`--gog-elevation-0`, two spacing aliases,
166
+ two type steps): a scale is offered whole or it is not a scale, and the consumer is the reader.
167
+ **Nothing belonging to a component may be exempted** — that is the defect the rule exists for.
168
+
169
+ It cannot see a dead _chain_: if A is read only by B's declaration and nothing reads B, both look
170
+ live. The leaf case is the one that has ever happened here.
171
+
172
+ - **`gog-multiselect` and `gog-autocomplete` window too — the same `virtualize`, same default.**
173
+ Verified live in Chrome on 10 000 options each. Multiselect: 8 rows in the DOM, a 490 004px
174
+ scroll height matching the unwindowed list to the pixel, and row 2 036 sitting at 99 768px,
175
+ which is `2036 × 49 + 4` exactly. Autocomplete: 12 rows, `End` reaching option 10 000 with
176
+ `aria-activedescendant` on it and the panel scrolled to match.
177
+
178
+ Two things were specific to these two rather than shared, and both were real:
179
+
180
+ - **A spacer in a list that declares a row `gap` takes that gap on both sides of itself**, while
181
+ the window's padding already stands in for every gap between the rows it replaces. So the
182
+ spacer's height is the padding less one gap. `gog-multiselect` is the only one of the three
183
+ lists with a row gap, and uncorrected the panel was two gaps too tall and every row sat one
184
+ gap below where its index said. **A constant error, not an accumulating one**, which is why it
185
+ would have survived a reading: at the shipped 4px nothing looks wrong, it is just 4px wrong
186
+ everywhere.
187
+ - **A combobox names a row by id, and the id has to be the index in the whole list.** Focus
188
+ never leaves `gog-autocomplete`'s input, so the highlight travels by `aria-activedescendant`.
189
+ With the id keyed to the rendered slice it restarts at zero on every scroll, so the input
190
+ points at option 0 while the highlight paints a row in the middle — the two agreeing only
191
+ while the window happens to be at the top. Its spec fails that way without the fix
192
+ (`expected 'gog-autocomplete-35-option-10' to contain '-option-999'`).
193
+
194
+ `gog-autocomplete`'s keyboard needed nothing else: its active row was already an index into the
195
+ full list rather than an element, because the combobox pattern had put it there years before
196
+ windowing existed. Only `scrollIntoView` had to go — it needs an element, and the window's whole
197
+ point is that most of them are not rendered; the arithmetic knows where the row would be.
198
+
199
+ **`virtualize` and `gogLoadMore` are different halves and compose.** One keeps the records the
200
+ server sends small, the other keeps the rows the browser builds small; a `gogLoadMore` list that
201
+ has loaded 10 000 records still stamps 10 000 rows without this. The spacers are also what keep
202
+ `gogReachEnd` meaning the end of the data rather than the end of the window.
203
+
204
+ `gog-table` still does not window.
205
+
206
+ - **`gog-alert` — a persistent, in-flow message**, and the thing `gog-toast` cannot be. No timer,
207
+ no queue, no overlay, no service: it renders where you write it and stays until your app removes
208
+ it. `severity` (the shared `GogSeverity`, defaulting to `'accent'`), an optional `heading`, a
209
+ projected body, `dismissible` with a `dismissed` output, `iconName` with a `gogAlertIcon` slot,
210
+ and `GOG_CONFIG.labels.closeAlert`.
211
+
212
+ **`dismissed` means pressed, not removed.** The alert stays in the DOM and the app decides. A
213
+ component that deleted itself would take the focused element with it and drop a keyboard reader
214
+ onto `<body>`.
215
+
216
+ **It announces through `live`** — `'assertive' | 'polite' | 'off'`, defaulting from the severity,
217
+ with `GogAlertLive` exported. The announcement is a copy of the message in a **separate
218
+ visually-hidden region**, empty until one render after the alert mounts, because a live region
219
+ filled in the same pass as its own creation announces nothing — the trap
220
+ `gog-toast-container`'s permanently-mounted regions exist to avoid.
221
+
222
+ **Whether the component could pick `live` itself was measured, and it cannot.** The question was
223
+ whether it can detect having been created during the application's first render, which is the
224
+ one case the severity-derived default gets wrong. `@angular/core` exposes no stability member on
225
+ `ApplicationRef` a component can read synchronously at construction, and `afterNextRender`
226
+ reports its _own_ first render — which every alert has, whenever it mounts. So `live` is the
227
+ consumer's call with a loud note: **set `'off'` for a message that is on the page when it
228
+ loads.**
229
+
230
+ One thing the plan called for and the code refused: **there is no `variant` input.** It was to be
231
+ `GogSurfaceVariant` defaulting to `'filled'`, but `filled` in this library means _a tint_ and
232
+ there is no per-status tint to draw it with — `--gog-accent-pale` exists, `--gog-success-pale`
233
+ does not. Inventing that family here would make the alert the twelfth place a theme restates its
234
+ red, so the severity is a leading edge and the icon over the ordinary surface, which is the shape
235
+ `gog-toast` already reached for the same reason.
236
+
237
+ - **`--gog-button-focus-ring-color`**, and it is a fix as much as an addition. `gog-button` had a
238
+ focus-ring width and a focus-ring offset but no colour, so `button.css` reached for
239
+ `--gog-button-variant-hover-bg` — the _hover_ fill doing focus duty, which is the same defect
240
+ eight other indicators had corrected in 21.12.0. It passed on `primary` and `secondary`, whose
241
+ hover fill is a saturated accent, and failed wherever that fill is a wash: **`ghost` at
242
+ 1.07–1.16:1** and the **severity `outline`** combinations at **1.79–2.65:1**, eleven failures
243
+ across `light`, `material`, `primeng` and `terminal`.
244
+
245
+ It defaults to `--gog-accent-color` — one colour, not one per variant. A focus ring's job is to
246
+ be visible against the page the button sits on, which does not change with the button's fill,
247
+ and this ring already sits a pixel further out than the foundation's so it reads as separate
248
+ even on a filled accent button. That is the answer the library's other fourteen rings give.
249
+
250
+ ### Changed
251
+
252
+ - **The package has a second entry point, `@guildofgleks/ui/shared`, and it is internal.** Shared
253
+ types, configuration and helpers moved out of the root bundle into their own, which the root now
254
+ imports by package path. Nothing an app imports changes: every symbol the root exported, it still
255
+ exports, now named one by one instead of re-exported wholesale.
256
+
257
+ This is phase 1 of `docs/entry-points.md` in the repository, and the reason for it is a
258
+ measurement: **the package tree-shakes but does not code-split.** On the real CLI a button costs
259
+ about 10 kB gzip over an empty app, but a lazy route holding `gog-table`, `gog-datepicker`,
260
+ `gog-calendar` and `gog-dialog` produced a 442-byte chunk and left all four in the initial bundle
261
+ — 40 kB heavier than it needs to be. Splitting the heavy components out needs `GOG_CONFIG` to live
262
+ in exactly one bundle, which is what this entry point is; the split itself comes in the next
263
+ minors.
264
+
265
+ `@guildofgleks/ui/shared` resolves, but it is plumbing the package's own entry points share.
266
+ Import from `@guildofgleks/ui`.
267
+
268
+ - **`public-api.ts` names what it exports instead of re-exporting two modules wholesale.** The
269
+ defect was never which symbols are public — it was that **adding one published it silently**. A
270
+ helper written into `date-utils.ts` or `option-accessor.ts` became part of the package's `.d.ts`
271
+ the moment it was saved, with nobody deciding that and no diff showing it.
272
+
273
+ **Nothing is dropped from `date-utils`.** All twenty of its helpers and `GogDateRange` are
274
+ listed by name, because `AGENTS.md` already advertises `formatDate`, `parseDate` "and a family
275
+ of date-math helpers" — the set is supported on purpose. The list changes nothing a consumer can
276
+ import; it changes who decides the next one, which is the whole point.
277
+
278
+ `option-accessor` is the module where the accident had consequences, and its three functions are
279
+ deprecated above.
280
+
281
+ - **A warning toast and an info toast were both the accent, and an info toast was pixel-identical
282
+ to a plain one.** `--gog-toast-warning-color` read `--gog-accent-bright` and
283
+ `--gog-toast-info-color` read `--gog-accent-color` — which is also what a _typeless_ toast
284
+ paints. That token is the whole signal: it draws the leading stripe, the icon and the countdown
285
+ bar. Three of the five toast states were the accent, in every theme, since the component
286
+ shipped. Both now read their own role.
287
+
288
+ **Surveyed rather than assumed, after the palette fix below raised the question.** All 45
289
+ severity-named colour tokens across every component were resolved against their own role in all
290
+ eleven themes; `gog-toast` is the only one that was wrong, and it was wrong twice. `gog-alert`,
291
+ `gog-button`, `gogBadge`, `gog-tag` and `gog-progressbar` all derive correctly — which is what
292
+ made this invisible, since nothing compared a token's _name_ against the root it reads.
293
+
294
+ - **`check:tokens` rule J — a token named for a severity resolves to that severity.** A text rule
295
+ rather than a sweep: the declaration either names its own role or it does not, and that needs no
296
+ colour maths. Scoped to the four suffixes whose job is to _be_ the role's colour (`-color`,
297
+ `-bg`, `-fill`, `-border`); `--gog-badge-warning-color` is the _label_ on the warning fill and is
298
+ in the exception list with that reason, beside `-wash`, `-ink` and `-buffer-bg`, which are
299
+ percentages of a role rather than the role. Verified by putting the old value back and watching
300
+ it fail.
301
+
302
+ - **`README.md` and `AGENTS.md` say where `gog-table` stops.** No column resizing or reordering by
303
+ the reader, no frozen columns, no expandable rows, no grouping — stated where someone evaluating
304
+ the table will read it rather than discovered halfway into a project. Documentation only;
305
+ nothing about the component changes.
306
+
307
+ **This list said "no virtualization" when it was written, and this release removed that line by
308
+ building the thing.** Both entries are in this changelog, days apart, and for a while the release
309
+ both added `virtualize` and advertised its absence. A limitations list is a claim with a shelf
310
+ life, and the shelf can be one release.
311
+
312
+ Each claim was verified against the code first, and one in `docs/backlog.md`'s filing was wrong:
313
+ it said "no sticky columns" while the table has shipped `stickyHeader` since 21.6.0. Those are
314
+ different axes — the header pins while rows scroll under it; the absent one is freezing a column
315
+ against _horizontal_ scroll — and both documents now draw that line, because a limitations list
316
+ that looks wrong on its first line teaches a reader to distrust the rest.
317
+
318
+ - **`check:oklch`'s R3 compares five severities, not four — and three themes were painting two of
319
+ them as one colour.** `GogSeverity` is `'accent' | 'success' | 'danger' | 'warning' | 'info'`
320
+ and the library paints all five as a set (`gog-button`'s `severity`, `gog-progressbar`'s
321
+ `variant`, `gogBadge`, and now `gog-alert`, whose entire signal is the colour of one edge). R3
322
+ was written against "the four status colours" and left the accent out, so four of the ten pairs
323
+ went unmeasured.
324
+
325
+ Two of the three findings are not close calls: the **dark** theme declared
326
+ `--gog-warning-color: #fbbf24`, the same hex as its accent, and **terminal** declared
327
+ `--gog-success-color: #3ddc5c`, the same hex as its. Nobody writes one value twice for two roles
328
+ on purpose — it is what a palette typed role-by-role produces when nothing compares them.
329
+ **parchment**'s danger sat 6.9° of hue and 0.052 of lightness from its oxblood accent, which is
330
+ not a distance a reader can use.
331
+
332
+ All three moved the **status**, never the accent: the accent is the theme's identity and the
333
+ status is the role that has to be read. `dark`'s warning is ember orange `#ffac4e` — moved by
334
+ hue rather than lightness, because darkening a status on a dark ground trades one defect for
335
+ another. `terminal`'s success is `#00b330`, moved along its own hue because that theme's other
336
+ three statuses already occupy yellow, red and cyan and the room left is in lightness.
337
+ `parchment`'s danger is `#a24439`. `check:contrast` stays green on all eleven themes.
338
+
339
+ - **A disabled control is exempt from `check:contrast`, consistently and on purpose.** WCAG carves
340
+ out "an inactive user interface component" in both SC 1.4.3 and 1.4.11 — a disabled control is
341
+ meant to look unavailable, and holding it to 4.5:1 makes "unavailable" impossible to draw. The
342
+ script had simply never had `:disabled` in one of its state regexes, with nothing saying why, so
343
+ the omission read as an oversight.
344
+
345
+ It also was not the exemption it looked like: **eight pairs reached the sweeps through compound
346
+ selectors** — `.gog-accordion__item--disabled .gog-accordion__header:hover` enters on its
347
+ `:hover` — and were gated. One predicate governs all three sweeps now, and such pairs are
348
+ printed rather than dropped. Nothing in the library changes; all eight already passed.
349
+
350
+ - **`npm run check:oklch` gates that a raised surface has an edge (R4).** `*-shadow` colours were
351
+ the last thing the palette gate did not read. `check:elevation` requires a theme to declare all
352
+ ten elevation knobs, but a theme may declare all ten at zero and pass it — rendering a dialog
353
+ with no boundary against the page behind it, with every check green.
354
+
355
+ The rule is a disjunction, because four different things can mark that edge and the eleven
356
+ shipped themes split on which: **six are carried by their shadow and five by their hairline
357
+ ring**, so gating any single carrier would have failed half the catalogue for a choice it made
358
+ deliberately. Whichever is strongest must clear ΔL ≥ 0.03 — the same threshold R1 already uses,
359
+ because it is the same question. Observed 0.0852 (`light`) to 0.3465 (`material`): the weakest
360
+ shipped theme clears it by 2.8x, and both figures are now printed per theme.
361
+
362
+ Nothing in the library changes. This one is a gate against a theme a consumer writes, which is
363
+ where the failure is silent.
364
+
365
+ - **`npm run check:contrast` resolves a boundary through the variant layer, and `.gog-btn` is
366
+ gated by it.** The button was the one control deliberately outside the boundary sweep, because
367
+ what identifies a button depends on its variant and the sweep resolved each painting rule once —
368
+ on `.gog-btn` that meant reading `--gog-button-primary-border`, `transparent` in the base theme.
369
+ It now resolves every boundary under each variant chain, the same machinery the variant sweep
370
+ already used for fills and labels, so `outline`'s border is measured as `outline`'s and
371
+ `ghost`'s transparent one is skipped. That is what found the focus-ring failures above.
372
+
373
+ Two smaller corrections came out of building it. **`boundaryBlock` matched the first gated
374
+ prefix rather than the longest**, and `.gog-ms` is a prefix of `.gog-ms__filter-input`: the
375
+ multiselect's filter input was being measured against the _page_ rather than the panel it sits
376
+ in, so the pair it reported was one nobody sees. And **the sweep now asserts that every block it
377
+ gates actually matched a declaration** — the same discipline as the pattern self-test added in
378
+ 21.12.0, and it is what surfaced the prefix bug. A gated list whose entries match nothing looks
379
+ exactly like a library with no defects.
380
+
381
+ ### Deprecated
382
+
383
+ - **`gog-table`, `gog-datepicker`/`gog-calendar` and `gog-dialog` move to their own entry points —
384
+ import them from `@guildofgleks/ui/table`, `/datepicker` and `/dialog`.** The root keeps
385
+ exporting all 25 symbols until **21.14.0** and stops then, when the code moves. The split is the
386
+ point: a lazy route using these components currently ships them in the initial bundle anyway,
387
+ and only an entry point the root does not re-export can change that — measured in
388
+ `docs/entry-points.md` in the repository.
389
+
390
+ - `@guildofgleks/ui/table` — `TableComponent`, `GogColumn`, `GogColumnBodyDirective`,
391
+ `GogColumnHeaderDirective`, `defaultCompare`, and the types `GogColumnBodyContext`,
392
+ `GogColumnHeaderContext`, `GogTableRowClickEvent`, `GogTableSelectionMode`,
393
+ `GogTableSortEvent`, `SortDirection`.
394
+ - `@guildofgleks/ui/datepicker` — `DatepickerComponent`, `CalendarComponent`, and the types
395
+ `GogCalendarDay`, `GogDatepickerValue`. The date helpers and `GogDateRange` stay in the root.
396
+ - `@guildofgleks/ui/dialog` — `DialogService`, `DialogComponent`,
397
+ `ConfirmationDialogComponent`, `DIALOG_DATA`, `DIALOG_REF`, and the types `DialogRef`,
398
+ `DialogConfig`, `DialogHandle`, `OpenDialog`, `ConfirmDialogData`.
399
+
400
+ **Your editor will not strike the old imports through**, and that is measured rather than
401
+ overlooked: ng-packagr bundles the root's types into a single export statement and drops the
402
+ deprecation tag on every re-export. A tag on the class itself would survive — and would strike
403
+ through the new subpath too, since it is the same class. This entry, `AGENTS.md` and
404
+ `GOG_DEPRECATIONS` are the notice.
405
+
406
+ Switching an import today changes nothing at runtime; both paths reach the same class.
407
+
408
+ - **`getByPath`, `readOption` and `isSameOptionValue` are deprecated, removed in 21.14.0.** They
409
+ are this library's own plumbing for reading a field off a consumer's object, and they became
410
+ public API because `public-api.ts` re-exported their module wholesale. Nothing in `README.md` or
411
+ `AGENTS.md` has ever mentioned them. `GogOptionAccessor` — the type every collection control's
412
+ `optionLabel` / `optionValue` / `optionDisabled` input is declared with — stays, and is the
413
+ reason the module was exported at all.
414
+
415
+ - **`--gog-slider-thumb-shadow` becomes `--gog-slider-thumb-glow-color`, removed in 21.14.0.**
416
+ The thumb composes it as `box-shadow: 0 0 var(--gog-slider-thumb-glow-size) <this>`, so the
417
+ token holds a **colour** and always has. Of the 31 `*-shadow` tokens the elevation audit
418
+ classified for 21.12.0, it was the only one whose name was simply wrong — and the wrongness is
419
+ not decorative: a consumer overriding it with a colour got what they expected, and one
420
+ overriding it with a shadow got a declaration the browser silently dropped.
421
+
422
+ `check-elevation.mjs` has carried it in `NOT_ELEVATION` with that reason since it was found;
423
+ that entry now explains why it is still listed (the sweep keys on the `-shadow`/`-glow`
424
+ families, not on what a token holds) rather than promising a rename.
425
+
426
+ - **`--gog-select-panel-offset` and `--gog-multiselect-panel-offset` become `*-panel-gap`,
427
+ removed in 21.14.0.** Five components place a panel with `calc(100% + <token>)` and split three
428
+ ways on what to call the value: `gog-autocomplete` and `gog-datepicker` said `-panel-gap`,
429
+ `gog-select` and `gog-multiselect` said `-panel-offset`, and `gog-menu` said `-offset`. A
430
+ consumer who learned one spelling guessed wrong on the next component.
431
+
432
+ `-gap` wins because it is the true one: an offset is a displacement from where a thing would
433
+ otherwise be, and this is the space between two things. `gog-menu`'s is renamed outright in the
434
+ same release with no window, because that one was never read (see Fixed) — a deprecation cycle
435
+ protects working consumer code, and there was none.
436
+
437
+ **Both old names keep resolving until 21.14.0.** `theme.css` declares each new token as
438
+ `var(<old name>, <value>)`, which is the mechanism the 21.7.0 prefix removals used: an override
439
+ on the old name still wins, one on the new name wins over it, and everyone else gets the value.
440
+ One minor rather than two, per `api-design.instructions.md` — the migration is a find-and-replace
441
+ in a theme.
442
+
443
+ **The ratchet had to grow a second half to hold this.** `DEPRECATED_NAMESPACES` can express
444
+ `--gog-btn-*` becoming `--gog-button-*`, because the prefix moves and the suffix is carried
445
+ through — it cannot express a rename, where the suffix itself moves and the replacement has to be
446
+ named. `DEPRECATED_TOKENS` is that map, `check:deprecations` fails on an overdue entry the same
447
+ way, and `GOG_DEPRECATIONS` now ships two entries where it shipped none. Verified by dating both
448
+ to the current version and watching the check fail.
449
+
450
+ ### Fixed
451
+
452
+ - **Six tokens were declared and read by nothing — one wired up, five removed.** Everything rule K
453
+ found on its first run, each given a verdict rather than a blanket fix:
454
+
455
+ **`gog-inputfield`'s clear mark was the wrong size**, and this is the one a consumer can see.
456
+ Six controls offer a clear button and all six declare `--gog-<block>-clear-icon-ratio` at `0.7`;
457
+ five read it and `gog-inputfield` did not, so its `×` rendered at the field's full type size —
458
+ about **43% larger** than the identical mark on a select, multiselect, autocomplete, datepicker
459
+ or textarea standing next to it. It now reads its own token, like its five siblings.
460
+
461
+ **Five leftovers removed**, none of which any stylesheet could reach:
462
+ `--gog-accordion-hover-ring` (the header's hover paints a colour and a background, never a ring),
463
+ `--gog-multiselect-checkbox-bg` and `--gog-multiselect-checkbox-checked-color` (the option's mark
464
+ is a glyph, so a background and a label colour have nothing to paint — the two tokens the mark
465
+ _does_ read are untouched), `--gog-panel-elevated-shadow` (21.12.0 deliberately pointed the
466
+ elevated variant at the foundation's own `--gog-panel-shadow`, which left this behind), and
467
+ `--gog-toast-gap` (the stack uses `--gog-toast-stack-expanded-gap` and the row
468
+ `--gog-toast-content-gap`).
469
+
470
+ **No deprecation cycle for any of the five**, on the same reasoning as the menu's rename: a
471
+ deprecation window protects working consumer code, and a token nothing reads has none to protect.
472
+ Overriding any of them has always done exactly nothing, and still does.
473
+
474
+ - **Two more marks were bigger than the boxes holding them**, found by the check above rather than
475
+ by eye — the same defect as the chip's remove mark, the select's chevron and the multiselect's
476
+ arrow earlier in this release, in two places that audit did not reach.
477
+
478
+ **`gog-table`'s sort icon was 9% wider than its slot**, and it is the _fourth_ time this library
479
+ has paid for "a relative unit resolves against the element carrying the property".
480
+ `--gog-table-sort-icon-width: 1.1em` reads as "a little wider than the mark" and is not: the
481
+ `em` resolves against the element's own font-size, which the line below it had already reduced
482
+ to `0.9em`, so the slot came out `0.99em` of the header while the mark is `1.2 × 0.9 = 1.08em`
483
+ of it. The slot now takes a floor of `--gog-icon-size`, which states the invariant in the CSS
484
+ rather than leaving it to a number someone has to get right. A consumer setting the token wider
485
+ still wins.
486
+
487
+ **`gog-textarea`'s clear mark was 20% wider than its button**, and this one is the largest of
488
+ the four because its ratio is deliberately `1` — `theme.css` says why, and it is right: 0.7
489
+ suits a dropdown's dense single-line trigger and reads as a speck on a multi-line box. So the
490
+ **box grew and the mark did not move**, which is the same resolution the chip's got. The clear
491
+ button's hit area is about 20% larger.
492
+
493
+ - **An `interactiveRows` table row answered a click and not a press.** It had a cursor, a hover
494
+ tint and a focus ring, and nothing at all under the finger — 21.9.0 gave nine other pressable
495
+ surfaces a `:active` colour and this one was not among them. `--gog-table-row-press-bg` now
496
+ fills that, guarded on `--interactive` because a plain row is not a control, and a selected row
497
+ keeps its own tint under the finger for the reason it already keeps it on hover.
498
+
499
+ **A colour rather than a ripple, and that is the verdict on `docs/ripple.md`'s deferred table
500
+ rows.** That plan left them out on two arguments and gated the revisit on the weaker one —
501
+ "no virtualization in this library yet" — which `virtualize` has now removed. The other never
502
+ depended on it: a wave whose radius is an 800–1200px row reads as a flash across the table
503
+ rather than as feedback where the finger landed. A colour also survives
504
+ `prefers-reduced-motion`, which was 21.9.0's other half.
505
+
506
+ - **`gog-menu`'s gap between trigger and panel was a token nothing read.** `--gog-menu-offset` was
507
+ declared in `theme.css`, listed in `TOKENS.md`, and documented on the site as "gap between the
508
+ trigger and the panel" — and the panel is placed in script, by a function that was called
509
+ without its `gap` argument and fell back to its own hard-coded `4`. Setting the token did
510
+ nothing, in every theme, since the component shipped.
511
+
512
+ What it cost beyond the dead knob: the four other components that place a panel this way take
513
+ their gap from CSS and follow `--gog-density`, so **a theme changing density moved four of the
514
+ five and left the menu's gap at 4px.** The component resolves the token now, through
515
+ `resolveLengthToken` — `parseFloat` would return `NaN` on the `calc(4px * var(--gog-density))`
516
+ the density scale produces, which is the trap that file exists for.
517
+
518
+ **Renamed to `--gog-menu-panel-gap` in the same change, with no deprecation cycle, on purpose.**
519
+ A deprecation window protects working consumer code and there is none to protect: nothing a
520
+ consumer wrote against the old name ever had an effect. Keeping an alias alive for two releases
521
+ would be complexity spent guarding a promise that was never kept. The new name is also the one
522
+ the other four already use — `--gog-<block>-panel-gap` — which is what the
523
+ `-gap`/`-offset` split in `docs/backlog.md` is about.
524
+
525
+ Found by sweeping `theme.css` for tokens nothing reads, after `--gog-menu-offset` turned up while
526
+ auditing that naming split. **The sweep found twelve candidates out of 1482**, and no check
527
+ covers the category: `check:tokens` rule F is the opposite direction — a `var()` read with no
528
+ declaration — so a declaration with no reader passes every gate the project has.
529
+
530
+ - **A dropdown could open downward into a panel that does not fit.** The three controls on
531
+ `GogDropdownBase` — `gog-select`, `gog-multiselect` and `gog-autocomplete` — size their panel
532
+ from `--gog-*-option-height` and choose up or down from the result. That token calls itself an estimate; measured against a rendered row it is wrong in
533
+ **all eleven shipped themes** — low in ten of them, by up to 8.38px a row — so the estimate
534
+ systematically under-reported and a short list could be judged to fit below when it needed more
535
+ room than there was.
536
+
537
+ It only ever misfired on lists short enough to sit under the panel's max-height cap (roughly
538
+ five rows), which is why it went unseen: above the cap the cap dominates and the error is
539
+ masked. **No static token can fix it** — the same `parchment` row is 48.38px at
540
+ `--gog-density: 1` and 42.38px at 0.85 — so the component now measures one real row a frame
541
+ after the panel renders and re-places if the token disagreed, caching it so every later open is
542
+ right from its first frame. The token remains as the seed for that first frame and its
543
+ documentation is corrected in all three.
544
+
545
+ Found by `docs/virtualization.md`'s iteration 0, which existed to check exactly this before
546
+ anything new depended on it.
547
+
548
+ - **Two of the three dropdowns sized their panel from a gap that is not between the rows.** The
549
+ height estimate that decides whether a panel opens up or down sums the rows _and the gap
550
+ between them_, and it took that gap from `--gog-<block>-option-gap` — which on `gog-select` and
551
+ `gog-autocomplete` is the gap **inside** a row, between the mark or icon and the label. Neither
552
+ options container declares a gap between rows at all, so every row added 12px of panel that is
553
+ not on the page: 48px on a five-row list, which is the length at which this decision is made at
554
+ all.
555
+
556
+ It is the same defect as the row-height one above and the opposite sign, which is why they were
557
+ invisible together — one estimate ran low per row and the other ran high per gap. Fixing the
558
+ first is what exposed the second, and that is the argument for measuring rather than deriving:
559
+ **a sum of two wrong terms can place a panel correctly and does not stay lucky.**
560
+
561
+ The gap is now measured from the rendered options container, in the same frame and from the same
562
+ element as the row — the row's own `parentElement`, so no subclass has to declare a second
563
+ selector. `gog-multiselect` is unaffected: its list is the one that really does declare a row
564
+ gap, and it is the only one that still seeds from a token.
565
+
566
+ **The token names are unchanged and so is what they paint.** `--gog-select-option-gap` still
567
+ sets the space between the check mark and the label, which is what it has always done; only
568
+ what read it for a different purpose has changed. All three now say in `theme.css` which gap
569
+ they are.
570
+
571
+ - **`gog-progressbar`'s buffer had no edge, and its edge is the whole of what it says.** The
572
+ buffer tier marks how much is loaded; where it _ends_ was under 3:1 against the track in **55 of
573
+ 55** shipped theme/variant combinations, worst **1.06:1**. That is a stronger result than the 51
574
+ of 55 that justified marking the fill's edge in 21.10.0, and it has the same cause: the two
575
+ tiers are the same hue by design — "the buffer tier is the same hue at low opacity, so it reads
576
+ as _ahead of the fill_ rather than as a second, competing colour" — which is the right choice
577
+ and exactly why a colour difference cannot carry the boundary.
578
+
579
+ The buffer now draws the same two hairlines the fill has drawn since 21.10.0. **No new token and
580
+ no palette change:** measured at the buffer's own edge across the same 55, the existing marker
581
+ clears 3:1 everywhere, worst 3.25:1, carried by the ink line against the bare track — the pair
582
+ `check:contrast` already gates. Visible wherever `mode="buffer"` is used.
583
+
584
+ - **Three icon buttons were smaller than the icon inside them** — `gog-chip`'s remove mark by 9%,
585
+ `gog-select`'s chevron and `gog-multiselect`'s arrow by 5%, at every size and in every theme.
586
+ Each set its box from one basis and let `<gog-icon>` draw the mark from another: the icon
587
+ renders its `<svg>` at `--gog-icon-size` (1.2em) of its own font-size, and none of the three
588
+ boxes was derived from that.
589
+
590
+ **Nothing painted the overflow, so nothing showed it** — the boxes carry no background at rest
591
+ or on hover. What it cost was the focus indicator. `:focus-visible` draws its outline on the
592
+ box, so on the chip the ring was drawn _inside_ the mark it indicates and cleared it only
593
+ because `--gog-chip-focus-ring-offset` happens to be 2px; measured at an offset of `0` — a
594
+ value any theme may choose, and one the theme generator offers as a slider — the ring landed
595
+ 0.79px inside the glyph at `md`.
596
+
597
+ All three now state the mark's font-size and its box on the same element, with the box reading
598
+ the same `--gog-icon-size` the icon reads. The two cannot drift again for any ratio, or for any
599
+ icon size a consumer sets.
600
+
601
+ - **`--gog-chip-remove-scale` now means what its name says, and its default is `1`.** It was
602
+ `1.1`, and it multiplied the button's _font-size_ rather than the mark — so it produced a box
603
+ 9% narrower than its own contents, and any value under `1.2` did nothing visible at all. It is
604
+ the ring around the mark now: `1` is the box the glyph occupies, and anything above it is
605
+ padding. **A theme that overrides this token gets a larger box than before at the same
606
+ number**, by a factor of 1.2.
607
+
608
+ `--gog-select-chevron-icon-ratio` and `--gog-multiselect-arrow-icon-ratio` were wrong in the
609
+ same way and are unchanged in value: both read `0.875`, which a reader takes to mean the
610
+ chevron is seven eighths of the field's type. It was 1.05 of it, because `--gog-icon-size`
611
+ multiplied on top. The mark does not move; the box around it grows 5% to contain it.
612
+
613
+ One visible consequence: **a removable chip with no avatar is ~9% of its remove box taller** —
614
+ 1.2px at `xsm` to 2.0px at `slg`, measured at `--gog-density: 1`. The mark itself does not
615
+ change size at any of the five. A chip with an avatar does not move at all, because the avatar
616
+ is the taller element. On `gog-select` and `gog-multiselect` nothing moves: the chevron's box
617
+ is not what sets a field's height.
618
+
7
619
  ## [21.12.0] - 10.09.2026
8
620
 
9
621
  ### Added