@wildmason/aegis 2.1.0 → 2.2.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/CHANGELOG.md CHANGED
@@ -18,6 +18,328 @@ that 404s.
18
18
 
19
19
  ## [Unreleased]
20
20
 
21
+ ## [2.2.1] - 2026-10-07
22
+
23
+ Patch: one fix and one theme colour. Nothing is added, renamed or removed.
24
+
25
+ ### Fixed
26
+
27
+ - `WmSlider`: the thumb sits on the value after the value and the range change
28
+ in one render. The native input bound its `value` before its `min`, `max`
29
+ and `step`, and the browser sanitizes a written value against the bounds the
30
+ input holds at that moment. A value that rose with `max` was clamped to the
31
+ old `max`, and the binding was not written again because the value did not
32
+ change. The thumb and the input's own value, which a screen reader reads as
33
+ the current value, sat a stop short, while the fill, the readout and
34
+ `aria-valuetext` named the new value. Scaffold hit it when an effort scale
35
+ gained a stop: the readout said "ultra" and the thumb sat on "max". The
36
+ bounds and the step are now bound first.
37
+
38
+ ### Changed
39
+
40
+ - `wildmason` theme: `--wm-text-muted` is `#949d94`, down from `#c0cac0`. The
41
+ old value was within 2/255 of `--wm-text-secondary` (`#c2ccc2`), so muted text
42
+ such as a timestamp read as body text. Every muted label on this theme dims.
43
+
44
+ ## [2.2.0] - 2026-10-02
45
+
46
+ Minor: new components, variants and inputs, and accessibility fixes. Nothing is
47
+ renamed or removed, so a consumer on `^2.0.0` picks this up on its next install
48
+ and nothing it already names moves. Two fixes change the ARIA that renders, so
49
+ a consumer test that reads it can see a difference: the `<wm-popover>` host no
50
+ longer carries the panel's role, and a `dialog` or `alertdialog` panel now has
51
+ `aria-modal="true"`.
52
+
53
+ ### Added
54
+
55
+ - `WmTabs` accepts `activeValue` `null` for "no tab selected". Use it when the
56
+ stage shows something that is not any tab's panel, such as a document opened
57
+ from a second tablist in the same row. Every tab reports
58
+ `aria-selected="false"` and none paints as active, and the tablist keeps one
59
+ tab stop (see Fixed). The input type widens from `string` to
60
+ `string | null`, so every existing template binding still compiles.
61
+ - `WmTabs` `ariaLabel` and `ariaLabelledby` inputs name the inner
62
+ `role="tablist"`; `ariaLabelledby` wins when both are set. The tablist had no
63
+ way to take a name, so two tablists in one row sounded identical to a screen
64
+ reader. An `aria-label` attribute on the `<wm-tabs>` element still names
65
+ nothing, because that element has no role.
66
+ - `WmButtonDirective` (`button[wmButton]`, `a[wmButton]`) puts the Aegis
67
+ button classes on a native element the consumer writes, from the same
68
+ `variant`, `size` and `iconOnly` inputs as `<wm-button>`. `<wm-button>`
69
+ renders its own `<button>` inside a host that has no role and cannot take
70
+ focus, and passes it only `disabled`, `type`, `ariaLabel` and `btnTitle`.
71
+ So `aria-pressed`, `aria-expanded`, `aria-controls`, `cdkFocusInitial`, an
72
+ `id`, a popover `trigger`, `[wmTooltip]`, `data-tooltip` and layout classes
73
+ all stayed on the wrapper. On the directive's element they reach the button
74
+ with no forwarding list. Both forms compute their classes in one function,
75
+ so they cannot drift apart. A `<button>` with no `type` gets
76
+ `type="button"`, the `<wm-button>` default, so moving from the component to
77
+ the directive does not create a submit button; a written or bound `type`
78
+ wins. The consumer's own classes, static or bound, are kept. `WmButton` is
79
+ unchanged.
80
+ - `.wm-btn--dashed`, and `variant="dashed"` on `<wm-button>` and
81
+ `[wmButton]`: the empty slot, for the place a new item goes at the end of
82
+ the list it adds to. It is transparent, with a 1px dashed edge and a
83
+ `--wm-text-secondary` label. On hover it fills with `--wm-bg-sunken`, the
84
+ edge turns solid and the label moves to `--wm-text-primary`. Focus uses the
85
+ pulse, as `ghost` does. `ButtonVariant` gains `'dashed'`. Helm hand-rolled
86
+ this twice (`workflow-chain-add`, `lfs-add-pattern`), and the two copies
87
+ disagreed on edge token, opacity and padding. At full strength, neither
88
+ copy's edge token reached 3:1 on every theme and surface: `--wm-border` is
89
+ 1.04:1 on warm-light, and `--wm-border-subtle` is 1.00:1 on arctic-night.
90
+ Their 80% and 78% mixes toward transparent fall below 3:1 on all 48 theme
91
+ and surface pairs. The shipped edge mixes `--wm-border-input` 70/30 toward
92
+ `--wm-text-primary`, which clears 3:1 everywhere (worst 3.34:1, wildmason
93
+ on `--wm-bg-float`).
94
+ - `scripts/check-dashed-contrast.mjs` (`npm run check:dashed-contrast`, part
95
+ of `build`) reads the variant's rules out of inputs.css and holds them on
96
+ every theme and on `--wm-bg-base`, `-raised`, `-float` and `-sunken`: a
97
+ 4.5:1 label at rest and on hover, APCA Lc 60 on hover, and a 3:1 edge at
98
+ rest and on hover, against the fill as well as the surface.
99
+ - `scripts/check-focus-pulse.mjs` (`npm run check:focus-pulse`, part of
100
+ `build`) fails when a selector animates `wm-focus-pulse` and has no
101
+ reduced-motion rule in the same file that stops the animation and sets a
102
+ static `box-shadow` ring.
103
+ - `WmToggleGroup` and `WmToggleGroupItem` (`<wm-toggle-group>`,
104
+ `<wm-toggle-group-item>`): a single choice among a few visible options,
105
+ drawn as joined `segmented` items in one sunken track or as separate
106
+ `chip`s that wrap, in `size` `sm` (24px) or `md` (28px). Aegis had no
107
+ single-select control of this kind, so each consumer hand-rolled one: Helm
108
+ has six, each a `role="group"` row of `aria-pressed` buttons in which every
109
+ button is its own Tab stop, the arrow keys do nothing, and nothing tells a
110
+ screen reader that the options exclude each other. The component is the
111
+ WAI-ARIA radio group: the group element is `role="radiogroup"`, each item
112
+ element is `role="radio"` with `aria-checked`, the group keeps one Tab stop
113
+ (the focused item while focus is inside, otherwise the checked item,
114
+ otherwise the first item that is not disabled), and the arrow keys move
115
+ focus and check, wrapping and stepping over disabled items, with Left and
116
+ Right mirrored in a right-to-left layout. `Home` and `End` go to the ends,
117
+ `Space` checks, and `Enter` does not. `(valueChange)` fires once per user
118
+ choice and never with `null`, so `[(value)]` type-checks against a signal
119
+ that cannot hold `null`. Values compare by `Object.is` and keep their type.
120
+ It is a ControlValueAccessor and marks the control touched when focus
121
+ leaves the group. The items are the radios, so `aria-label` on an
122
+ icon-only item, `data-testid` and `[wmTooltip]` land on the control. New
123
+ exported types: `WmToggleGroupAppearance`, `WmToggleGroupSize`.
124
+ - `scripts/check-toggle-group-contrast.mjs` (`npm run
125
+ check:toggle-group-contrast`, part of `build`) reads the toggle group's
126
+ rules out of its stylesheet and holds both appearances on every theme and
127
+ on `--wm-bg-base`, `-raised`, `-float` and `-sunken`: 4.5:1 labels
128
+ unchecked, hovered and checked, APCA Lc 60 on hover, and a 3:1 checked edge
129
+ and focus ring. It measured out the first choices: a bare
130
+ `--wm-color-accent` fill is 2.91:1 against `--wm-bg-float` on
131
+ posh-sandalwood, so the checked item carries a
132
+ `--wm-color-accent-readable` edge, and primary text on `--wm-bg-hover` is
133
+ 4.41:1 on vampires-kiss, so segmented hover fills with `--wm-bg-float`.
134
+
135
+ - `.wm-hatch` and `.wm-hatch--warning` in inputs.css: the stand-in texture,
136
+ for an elided node that stands for items the view does not draw and for a
137
+ value that nobody has filled in. The class paints 135deg bands, 4px of tone
138
+ then 4px clear, as `background-image` over the element's own fill. The
139
+ neutral tone is `--wm-text-primary` at 10% and the warning tone is
140
+ `--wm-color-warning` at 14%, both mixed toward `transparent`;
141
+ `--wm-hatch-tone` replaces the band colour. Aegis had no texture, so two
142
+ consumers hand-rolled the same geometry: Helm's Stacks overflow node
143
+ (`+N`) and Mortar's unset path variable. Helm's copy built its band from
144
+ `--wm-bg-hover` mixed into `--wm-bg-raised`, which measures 1.08:1 on
145
+ thistle-mocha, close to invisible, and 2.24:1 on vampires-kiss. Each
146
+ shipped share is the smallest that shows on every theme and surface,
147
+ because every point above it costs the text on top contrast.
148
+ - `scripts/check-hatch-contrast.mjs` (`npm run check:hatch-contrast`, part of
149
+ `build`) reads the hatch rules out of inputs.css and holds both tones on
150
+ every theme and on `--wm-bg-base`, `-raised`, `-float` and `-sunken`: a band
151
+ of at least 1.15:1 against the surface, `--wm-text-primary` on the band at
152
+ 4.5:1 and APCA Lc 60, every token the tone reads defined by the theme, the
153
+ 135deg 4px/8px geometry, and a translucent tone. One pair has no APCA bar:
154
+ on solarized-precision's `--wm-bg-float` the bare text is Lc 65.4, and no
155
+ share of either tone gives a visible band with Lc 60 text (the neutral band
156
+ leaves Lc 57.5 and 5.87:1). The guard proves that on every run and fails if
157
+ a theme change makes the pair reachable.
158
+
159
+ ### Fixed
160
+
161
+ - `.wm-btn--flat` and the `.wm-checkbox` class kept the animated focus pulse
162
+ under `prefers-reduced-motion: reduce`. The reduced-motion rule in
163
+ inputs.css lists each pulsing selector by hand, and these two were missing,
164
+ although the Accessibility page said every pulse becomes a static ring. Both
165
+ now get the static 3px ring. `check:focus-pulse` found them.
166
+
167
+ - `WmTabs` no longer drops out of the page's Tab order when no tab is
168
+ selected. It gave the tab stop to the selected tab only, so an `activeValue`
169
+ that matched no tab left every tab at `tabindex="-1"`, and a consumer could
170
+ not repair it because the binding re-applied `-1` on each change detection.
171
+ The stop is now the focused tab while focus is inside, otherwise the
172
+ selected tab, otherwise the first tab that is not `unavailable` (the first
173
+ tab when all are). Helm's Navigator hid the selected pill in CSS to avoid
174
+ this, which left `aria-selected="true"` on a tool that did not look
175
+ selected.
176
+ - `WmTabs` tracks the focused tab by value instead of by index. A tab added or
177
+ removed before the focused one shifted the index, so the stop moved to a
178
+ different tab or past the end of the list, where no tab had
179
+ `tabindex="0"`.
180
+ - `WmTabs` returns its tab stop to the selected tab when focus moves to a
181
+ second `<wm-tabs>`. Its blur check asked whether focus was inside any
182
+ `.wm-tabs` element, so focus in the other tablist counted as inside this
183
+ one and left the stop on the tab last focused.
184
+
185
+ - `WmPopover` sets `aria-modal="true"` on a `role="dialog"` or
186
+ `role="alertdialog"` panel. The panel always opens over a click-catching
187
+ backdrop and traps focus, so it is modal, and without the attribute a screen
188
+ reader kept reading the page behind it. Other roles get no `aria-modal`,
189
+ since the attribute is defined for those two only. A panel with
190
+ `[trapFocusAutoCapture]="false"` gets none either: focus stays on the trigger
191
+ outside the panel, and `aria-modal` would hide that focused trigger. Mortar's
192
+ combobox is that case (default `dialog` role, auto-capture off), and keeps
193
+ its current semantics. A modal panel whose content has nothing tabbable
194
+ (text only, or a lone disabled button) takes focus itself, through
195
+ `tabindex="-1"`, and Tab and Shift+Tab keep focus on it. The focus trap
196
+ finds nothing to focus there, so focus used to stay on the trigger, which
197
+ `aria-modal` hides, and a second Tab walked out to the page behind the
198
+ backdrop. A non-modal panel gets no `tabindex`, so a click on it cannot pull
199
+ focus off a typeahead input.
200
+ - The `<wm-popover>` host no longer carries the panel's role. A consumer
201
+ writes `role="dialog"` as a plain attribute, and Angular copies a static
202
+ attribute onto the host as well as into the input, so each popover written
203
+ that way wrapped its trigger in a second, unnamed `dialog` — or in a `menu`
204
+ that owned no `menuitem`. The role now lives on the panel alone.
205
+ - `WmModal` lays out its own backdrop. It used Tailwind utilities for this
206
+ (`fixed inset-0 flex items-center justify-center z-50`, and `items-start
207
+ pt-20` for `align="top"`), which Aegis does not ship. A consumer's Tailwind
208
+ build emits only the classes its own sources name, so the backdrop was fixed
209
+ and centred only where a consumer happened to use the same utilities; in an
210
+ app without Tailwind, the docs app included, it rendered in the page flow.
211
+ Helm's sources name every centred-layout utility but not `pt-20`, so
212
+ `align="top"` never got its offset there; Helm does not use `align="top"`.
213
+ The rendered values are unchanged: fixed, full viewport, z-index 50, and
214
+ `5rem` from the top for `align="top"`. The inline
215
+ `background: var(--wm-modal-backdrop)` stays, because consumers detect an
216
+ open modal by `[style*="--wm-modal-backdrop"]`.
217
+
218
+ ### Documentation
219
+
220
+ - The `popover` guide page, which the MCP serves, documented the docs app's
221
+ private copy of the popover rather than the shipped `WmPopover`: a required
222
+ `data-popover-trigger`, two-way `[(isOpen)]`, `align="center"` and CSS-string
223
+ widths, none of which the shipped component has. The page now runs its demos
224
+ on `WmPopover`, documents `(closed)` and `trapFocusAutoCapture`, and adds an
225
+ Accessibility section. The private copy is deleted.
226
+ - The `popover` page, DESIGN_LANGUAGE.md §7.13 and `WmPopover`'s doc comment
227
+ now say what a consumer following the old page got wrong. The `trigger`
228
+ attribute is the only thing the component reads; `data-popover-trigger` is
229
+ not. The component writes `aria-haspopup`, `aria-expanded` and
230
+ `aria-controls` to the trigger on every change to `isOpen` or `hasPopup`, so
231
+ the template does not bind them, and a static `aria-haspopup` is replaced by
232
+ `hasPopup`. A `<wm-button>` cannot be the trigger: the attribute stays on
233
+ its host, which then wraps a second button role around the real `<button>`,
234
+ so use a native `<button trigger>` with the `wm-btn` classes. The
235
+ Accessibility page's trigger row now lists `aria-controls` too. Helm carries
236
+ nine dead `data-popover-trigger` attributes, eight hand-written
237
+ `aria-expanded` bindings and two `<wm-button trigger>` sites from the old
238
+ page; Mortar carries five dead attributes and one `<mortar-button trigger>`.
239
+ - The `modal` guide page (the Dialog topic) never mentioned
240
+ `--wm-dialog-width`, so it read as though `size="lg"` (640px) were the widest
241
+ `wm-dialog` can go. The property overrides every preset, and Helm sets it on
242
+ ten dialogs, up to 960px. The page now documents it, with a demo, and ran its
243
+ demos on a private copy of the dialog that ignored it; the page now runs on
244
+ the shipped `WmDialog` and `WmButton`, and the dialog copy is deleted. The
245
+ page also documents `wm-modal` for the first time: when to choose it over
246
+ `wm-dialog`, its inputs, what it supplies (backdrop, dismissal, focus trap)
247
+ and what the consumer supplies (`role`, `aria-modal`, the accessible name, a
248
+ close control), with a working demo. `WmDialog`'s own doc comment wrongly
249
+ credited the browser with returning focus on close; `cdkTrapFocusAutoCapture`
250
+ does it.
251
+ - DESIGN_LANGUAGE.md lists `<wm-modal>` among the shipped components (§20 says
252
+ anything unlisted is not shipped), §8.6 names both components and
253
+ `--wm-dialog-width`, and §15.2 says the shipped overlays trap focus
254
+ themselves instead of telling consumers to implement a trap.
255
+ - The `tabs` guide page, which the MCP serves, ran its demos on a private
256
+ copy of the tabs in the docs app; it now runs on the shipped `WmTabs`, and
257
+ the copy is deleted. The page adds a "No Tab Selected" section with a
258
+ working demo of two named tablists that hand the stage to each other, names
259
+ every demo tablist, and documents `ariaLabel`, `ariaLabelledby`, the tab
260
+ stop rule, the `wm-tab-{value}`/`wm-panel-{value}` ids and `WmTab`'s
261
+ `unavailable`, `unavailableReason` and `separatorBefore` inputs, which its
262
+ API table left out. Its request and response demos both used the values
263
+ `body` and `headers`, so the page rendered `wm-tab-body` and
264
+ `wm-tab-headers` twice; the response demo now uses its own values.
265
+ - DESIGN_LANGUAGE.md §10 documents `<wm-tabs>` for the first time (§10.0).
266
+ The section taught only hand-rolled switchers, with no tablist semantics.
267
+ - The Quick Reference component table listed an `[options]` input on
268
+ `<wm-tabs>`, which does not exist, and gave `<wm-popover>` the export name
269
+ `Popover` and a two-way `[(isOpen)]`. The rows now name `WmTabs`'s real
270
+ inputs, and `WmPopover` with `[isOpen]` and `(closed)`.
271
+ - The `button` guide page, which the MCP serves, said `<wm-button>` and the
272
+ `.wm-btn` classes "are interchangeable" and "produce identical output".
273
+ They are not: attributes written on `<wm-button>` stay on its host. Helm
274
+ met this twice on 2026-08-09 (an `aria-pressed` that could not reach the
275
+ button, and a `cdkFocusInitial` that CDK reported as "not focusable") and
276
+ fell back to writing the classes by hand. The page now opens with a
277
+ "Component, Directive or Class"
278
+ section: use `<wm-button>` by default, `[wmButton]` whenever something has
279
+ to land on the focusable element, and the bare classes only outside
280
+ Angular, with a table of what does not reach the inner button and a working
281
+ `aria-pressed` toggle. `data-tooltip` is in that table: it opens on
282
+ `:focus-visible`, which the wrapper never matches, so an icon-only
283
+ `<wm-button>` never showed its tooltip to a keyboard user. DESIGN_LANGUAGE.md
284
+ gains the same rule as §6.5, and `WmButton` gains a doc comment that says
285
+ what it forwards.
286
+ - The `button` page ran on a private copy of `WmButton` in the docs app that
287
+ had drifted from the library (no `xs`, `iconOnly`, `ariaLabel` or
288
+ `btnTitle`). The page and the Composition, Form Patterns and Toast pages now
289
+ use the library component, and the copy is deleted. The page documents the
290
+ `xs` size, `iconOnly`, `ariaLabel` and `btnTitle`, which its API table left
291
+ out. Its own stylesheet set `.btn-icon-only` to 36 × 36px, so every
292
+ icon-only demo rendered at the default size whatever its size class, against
293
+ the 28px and 40px its own table promised; that rule is removed.
294
+ - DESIGN_LANGUAGE.md §8.4 (toolbar button) and §12.3 (notification dot) wrote
295
+ `class="btn btn-sm btn-ghost"`, which Aegis does not ship; the docs check
296
+ cannot see it because the names have no `wm-` prefix. Both now use
297
+ `[wmButton]`, and §8.4 tells a toolbar toggle to expose its state with
298
+ `aria-pressed` or `aria-expanded` rather than colour alone. §20 said
299
+ anything it did not list is not shipped, and it did not list `<wm-button>`,
300
+ `<wm-checkbox>`, `<wm-toggle>`, `<wm-ghost-field>` or `<wm-rich-tooltip>`;
301
+ it lists them now, with `[wmButton]`.
302
+ - The `popover` page, DESIGN_LANGUAGE.md §7.13 and `WmPopover`'s doc comment
303
+ now point an Aegis-styled trigger at `<button wmButton trigger>`, and the
304
+ page's demo triggers use it. The Quick Reference and Accessibility pages
305
+ list `[wmButton]` beside `<wm-button>`.
306
+ - The `button` page gains an Empty Slot section: when to use `dashed`, a demo
307
+ of both Helm shapes (a full-width slot and one at its own width), what each
308
+ state paints, and why the edge is mixed. It says that width is the
309
+ consumer's layout, so a full-width slot is `[wmButton]` plus a width class.
310
+ The Variants demo, table and both API tables list `dashed`. The Quick
311
+ Reference page lists `.wm-btn--flat`, `.wm-btn--dashed` and `.wm-btn--xs`;
312
+ it listed none of the three before. The Accessibility page's pulse card
313
+ lists every control that pulses. DESIGN_LANGUAGE.md §6.3 and §15.1 and the
314
+ inputs.css button header are updated.
315
+ - A `toggle-group` guide page (served by the MCP) documents
316
+ `<wm-toggle-group>` with working demos of both appearances, `size="sm"`,
317
+ icon-only items, a reactive form with numeric values, disabled items and
318
+ groups, the keyboard and ARIA contract, and how to replace a hand-rolled
319
+ `aria-pressed` row. DESIGN_LANGUAGE.md gains §10.4. §10's opening splits
320
+ tabs from toggle groups by what the control does (choose a panel, or set a
321
+ value), and §7.5, §10.2, §10.3 and §12.5 point at the component. §15.1
322
+ adds the outline focus treatment its items use, and §20 lists it.
323
+ - The Checkbox, Combobox, Select and Toggle pages sent exclusive choices to
324
+ "radio buttons", which Aegis does not ship, and the Slider page and
325
+ `WmSlider`'s doc comment sent them to "a pill tab group", which is for
326
+ switching panels. All now name `<wm-toggle-group>`. The Tabs page adds a
327
+ Don't: pill tabs do not set a value. The Quick Reference and Accessibility
328
+ pages list the component, its keys, its roles and its reduced-motion rule.
329
+ - DESIGN_LANGUAGE.md gains §2.8, the hatch: what it marks and what it must
330
+ not, the geometry, why the fill must be `background-color` (the
331
+ `background` shorthand resets `background-image`, and a component
332
+ stylesheet outranks `.wm-hatch`), and the rules. Text on a hatch is
333
+ `--wm-text-primary`: under the neutral band `--wm-text-secondary` falls to
334
+ 3.70:1 on vampires-kiss. The hatch never carries the meaning alone, because
335
+ forced-colors mode computes a gradient `background-image` and `box-shadow`
336
+ to `none`; a border survives. §6.3 and §18.3 point at it. The `color` guide
337
+ page gains a Hatch section with demos on all four surfaces, and Quick
338
+ Reference gains rows for both classes.
339
+ - DESIGN_LANGUAGE.md §2.7 said Aegis ships 10 themes and left `sage-linen`
340
+ and `wildmason` out of its table; it ships 12. §20 now lists the
341
+ `.wm-shadow` utilities, which it left out, and the hatch classes.
342
+
21
343
  ## [2.1.0] - 2026-09-20
22
344
 
23
345
  Minor: one new component and its inputs. Nothing existing changes, so a consumer
@@ -461,7 +783,9 @@ a `^1.x` range does not pick this release up by accident.
461
783
  Releases before 1.8.0 are in `git log`. See also `wiki/products/Aegis - Health.md`
462
784
  for the decisions behind these changes, including the ones decided against.
463
785
 
464
- [Unreleased]: https://github.com/wildmason/aegis/compare/v2.1.0...HEAD
786
+ [Unreleased]: https://github.com/wildmason/aegis/compare/v2.2.1...HEAD
787
+ [2.2.1]: https://github.com/wildmason/aegis/releases/tag/v2.2.1
788
+ [2.2.0]: https://github.com/wildmason/aegis/releases/tag/v2.2.0
465
789
  [2.1.0]: https://github.com/wildmason/aegis/releases/tag/v2.1.0
466
790
  [2.0.0]: https://github.com/wildmason/aegis/releases/tag/v2.0.0
467
791
  [1.18.0]: https://github.com/wildmason/aegis/releases/tag/v1.18.0