Skip to content

feat(forms): add the form-control-group primitive and move every field component onto it - #675

Merged
mrholek merged 26 commits into
v6-devfrom
feat/form-control-group-v6
Aug 4, 2026
Merged

feat(forms): add the form-control-group primitive and move every field component onto it#675
mrholek merged 26 commits into
v6-devfrom
feat/form-control-group-v6

Conversation

@mrholek

@mrholek mrholek commented Aug 3, 2026

Copy link
Copy Markdown
Member

Implements the frame layer of the v6 styling doctrine (architecture/v6-form-controls-styling.md in the workspace repo), designed with @mrholek on 2026-08-03.

The problem

Six components draw a frame that has to read as a single control — autocomplete, chip input, multi select, password input and the four picker shells — and each carried its own copy of the styling. Per family, 26–34 Sass variables literally aliased $input-*. That duplication produced three v2 bugs already: a double icon, a double frame that broke sizing, and one picker reading another family's properties.

The primitive

<div class="form-control-group">
  <input type="password" class="form-control">
  <button type="button" class="form-control-action">
    <span class="form-control-icon"></span>
  </button>
</div>
  • .form-control-group is the frame. It shares one canonical definition with .form-control through the new form-control-frame() mixin, lays children out as a flex row, and answers focus on :focus-within (a div never fires :focus). form-control-group-focus() lets a component replay that treatment while its popup holds the focus.
  • The editable keeps the class it already had. .form-control-group > .form-control gives up its own frame — one deliberate rule replacing the per-family copies, and it outranks a theme's .form-control border so a second frame can't reappear inside a group. No "ghost" class, unlike upstream's form-adorn/form-ghost.
  • .form-control-icon is an icon and nothing more — passive, usable on its own or inside an action. .form-control-action is an interactive adornment (a real button) that may hold one; .form-control-cleaner is the value-clearing action. Upstream's adornments are passive only; every real adornment in our components is a button, which is where the consolidation actually pays.
  • Sizing rides the standard .form-control-sm/-lg on the frame, so per-family sizing classes are gone. The group answers those classes itself, because it declares the control tokens and would otherwise win the cascade against them.

Adoption — all six families

Family What changed
date / time / date-range / date-time picker frame, indicator and the range separator move to the shared classes; sizing classes and indicator tokens gone
password input overlay model dropped: the toggle was an absolutely positioned button over the input, with padding carved out and its size recalculated per size class. As a flex child none of that is needed; the icon paints in currentcolor so it follows the action through hover and validation
chip input keeps wrapping, chip spacing and chip disabling; frame/focus/disabled come from the group
autocomplete, multi select frame moves; validation, open and disabled states are written onto the frame instead of inherited from the root, which retires their !important overrides. Their frames carry no padding — both pad the parts inside instead, because a wrapped tag has to reach its edge — and their parts stretch rather than centre. Adornments keep family styling for now: those icons are CSS masks, and converting them to the inline-SVG convention belongs with their Popup pass

Validation generalizes to one .form-control-group selector, replacing the per-family pair added in #673.

Result

Compiled CSS drops by 1892 lines (17569 → 15677). fusv is green with zero unused variables after removing everything the migration orphaned.

Verification

Full gate: eslint, tsc, stylelint, 44 SCSS tests, 2939 unit tests, DefaultType check, dist, bundlewatch PASS, and the docs site builds (139 pages). Beyond that, every family was checked in a real browser — frame, focus, disabled, invalid/valid and the sm/lg variants — because none of that is covered by unit tests: picker heights come back at exactly the pre-migration 31/38/48 px, password renders correctly in all four states (including the red eye icon on invalid, which is the currentcolor payoff), chips render and wrap in all three sizes, and autocomplete/multi-select keep their frames in every state.

Docs

The primitive gets its own page — parts, sizing, validation, disabled, and the boundary against .input-group (separate controls side by side vs one control made of parts). The migration guide carries the class map for markup users write by hand (the password wrapper and chip input).


Follow-up in this branch: the pickers get their cleaner back

v1 offered a button inside the field that cleared the value; the v2 rewrite dropped it — which is how its styling tokens ended up dead and got removed earlier. The primitive already had a place for it, so it returns properly:

  • A real <button>. v1 used a bare <div>, so the control that cleared the field could not be reached from the keyboard at all. It is labelled, disabled together with the picker, and clears without opening the popup.
  • It appears only once the field holds a value. The section field already reports that with .form-date-time-filled, so the rule belongs to that family and one selector covers all four shells — no component tracks the state itself.
  • New options on all four shells: cleaner (on by default, as in v1), cleanerIcon, ariaCleanerLabel.
  • The cross icon was duplicated byte for byte as a data URI in autocomplete and multi select; it is now one inline SVG in util/icons, painted in currentColor so it follows the action through hover, validation and disabled.
  • Building an adornment moved to util/form-control-group — the JavaScript half of the primitive's markup contract, and the same consolidation the SCSS went through.

Bundle budgets

The restored feature adds JS, so coreui.js, coreui.esm.js and coreui.bundle.js move up by ~1 kB each. In the same commit the CSS budgets come down (56.5 → 55 kB, 47 → 45.5 kB) so the 1892 lines this branch removed stay removed rather than becoming slack.

Verified in the browser across all four pickers: visible with a value, hidden while empty, disabled with the picker, absent under cleaner: false, and clearing without opening the popup. 2946 unit tests green (7 new), docs site builds.

One cleaner across the library

The cross was drawn three different ways: a data URI duplicated byte for byte in autocomplete and multi select, painted through a CSS mask on a ::before, and a third copy about to appear in the pickers. All of them are .form-control-cleaner with the shared inline SVG now, so every cleaner is the same mark at the same size and follows its button's colour through hover, validation and disabled rather than carrying tokens of its own. 32 declarations go — both families' cleaner width, height, padding, icon, icon colours and icon size.

The primitive gives a cleaner its own icon size (.625rem — the value both families already used): the cross is drawn on a 16-unit grid, so it carries more weight than the 512-grid glyph icons and has to render smaller to read as their equal. That is also why the first attempt looked oversized next to the calendar.

Autocomplete and multi select finish the move

Their indicator was the last part still styled by the family, drawn as a CSS mask through a ::before. It is a .form-control-action with the shared inline SVG now, so every adornment in the library is the same mark at the same size and takes its colour from the button.

The chevron turned out to be the second icon duplicated byte for byte between those two (after the cross) — both live in util/icons now. Their indicators also became genuinely disabled buttons instead of ones merely dropped from the tab order with tabindex="-1", which is what a disabled control should offer.

What remains family-styled in those two is what they actually own: the options listbox, tags, search, and the caret turning when the popup opens.

CI guard worth knowing about

check-class-api blocks any public class disappearing without a declared removal. Twelve did across this branch — the per-family frames, indicators, separators, sizing classes and cleaners — each now registered in build/class-api-removals.json with the reason a user needs, and mapped in the migration guide.


Layer 2: the .popup primitive

Decided with @mrholek in review: the floating panel is its own primitive, and deliberately not named after the menu — the same surface will grow a fullscreen/modal presentation for mobile (the v1 pickers had one; the v2 rewrite lost it), so the name must not bake the anchored look in.

  • The generated panel carries .popup next to its structural class (date-picker-dropdown / time-picker-dropdown / combobox-popup), which keeps only its own shape, the parent-driven open state, and the family tokens for the teleported case.
  • 19 chrome declarations (three sets of bg/border/radius/shadow/z-index, two of them byte-for-byte copies) collapse into six $popup-* variables — theme the popup once and every field component's panel follows. Defaults match the previous values, so nothing changes visually; verified in the browser on an open picker, an open combobox and a picker teleported via container.
  • Deliberate upstream divergence: BS6's combobox puts literal .menu on its panel. Ours cannot — the modal mode is a real requirement here — and the popup borrows the menu's look, never its interaction semantics.

The doctrine's three layers are now: form-control-group (one frame) — implemented; .popup (one floating surface, two presentations) — anchored mode implemented, fullscreen mode designed, lands separately; component-owned bodies — untouched by design.

Running CSS total for the branch: 17569 → 15442 lines (−2127).

…d component onto it

Six components draw a frame that has to read as a single control —
autocomplete, chip input, multi select, password input and the four
picker shells — and each carried its own copy of the styling. That
duplication is what produced three v2 bugs already (a double icon, a
double frame that broke sizing, one picker reading another's properties)
and it is what the styling doctrine set out to end.

The primitive
-------------
- `.form-control-group` is the frame. It shares one canonical definition
  with `.form-control` through the new `form-control-frame()` mixin, lays
  its children out as a flex row, and answers focus on `:focus-within`
  because a div never fires `:focus`. `form-control-group-focus()` lets a
  component replay that treatment while its popup holds the focus.
- `.form-control-group > .form-control`: the control inside gives up its
  own frame. One deliberate rule replaces the per-family copies, and it
  outranks a theme's `.form-control` border so a second frame can never
  reappear inside a group. There is no "ghost" class — the editable keeps
  the class it already had.
- `.form-control-icon` is an icon and nothing more: passive, and usable
  either on its own or inside an action. `.form-control-action` is an
  interactive adornment (a real button) that may hold one;
  `.form-control-cleaner` is the value-clearing action.
- Sizing rides the standard `.form-control-sm`/`-lg` on the frame, so the
  per-family sizing classes are gone. The group answers those classes
  itself, since it declares the control tokens and would otherwise win
  the cascade against them.

Adoption
--------
- Pickers: frame, indicator and the range separator move to the shared
  classes; the sizing classes and the indicator tokens disappear.
- Password input drops the overlay model entirely — the toggle was an
  absolutely positioned button over the input, with padding carved out
  and its size recalculated per size class. As a flex child none of that
  is needed. Its icon paints in `currentcolor`, so it follows the action
  through hover and validation instead of needing its own colour rules.
- Chip input keeps wrapping, chip spacing and chip disabling; the frame,
  focus and disabled treatment come from the group.
- Autocomplete and multi select take the frame. Their validation, open
  and disabled states are now written onto the frame rather than
  inherited from the component root, which also retires their
  `!important` overrides. Their adornments keep family styling for now:
  those icons are CSS masks, and converting them to the inline-SVG
  convention the primitive's actions expect belongs with their Popup pass.
- Validation generalizes to one `.form-control-group` selector, replacing
  the per-family pair added in #673.

Compiled CSS drops by 1892 lines. The primitive gets its own docs page
and the migration guide carries the class map for the markup users write
by hand. Design and rationale: architecture/v6-form-controls-styling.md
in the workspace repo.
@coveralls

coveralls commented Aug 3, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 30898775070

Coverage increased (+0.08%) to 93.105%

Details

  • Coverage increased (+0.08%) from the base build.
  • Patch coverage: 2 uncovered changes across 2 files (113 of 115 lines covered, 98.26%).
  • No coverage regressions found.

Uncovered Changes

File Changed Covered %
js/src/date-picker.ts 19 18 94.74%
js/src/date-range-picker.ts 20 19 95.0%
Total (10 files) 115 113 98.26%

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 8526
Covered Lines: 8147
Line Coverage: 95.55%
Relevant Branches: 4773
Covered Branches: 4235
Branch Coverage: 88.73%
Branches in Coverage %: Yes
Coverage Strength: 366.18 hits per line

💛 - Coveralls

mrholek added 14 commits August 3, 2026 11:06
Both families pad the parts inside the frame, not the frame itself — a
wrapped tag has to be able to reach its edge — so the padding the
primitive applies came on top of theirs and the control grew from 38px
to 50px. Their frames now carry none, and their parts stretch to the
full height as they did before.
…s disabled

A control that isn't a real input — the pickers' section field — carries
`.disabled` as a class, which `:disabled` never matches. So the frame
never learned it was disabled while the field painted the disabled
background on itself: a grey block inside a white frame. Both spellings
now reach the frame, and the control inside gives up its disabled
background the same way it gives up the rest of its chrome.

This restores what v1 did through `.date-picker.disabled`, without each
family having to wire its root state to the frame.
…is disabled

The indicator button was never actually disabled — the shells only had
the click handler return early — so a disabled picker still offered a
focusable button that silently did nothing, and it kept its enabled
colours next to a greyed-out frame. The shells set the attribute now, so
it drops out of the tab order and the primitive's :disabled styling
applies.

Adornments inside a group marked .disabled are muted too, for groups
assembled by hand where the author disabled the control but not every
button. Direct children only, so an icon inside an action doesn't fade
twice.
…rimitive

v1 offered a button inside the field that cleared the value; the v2
rewrite dropped it, which is how its styling tokens ended up dead. The
primitive already had a place for it, so it comes back properly:

- a real `<button>` — v1 used a bare div, so the control it operated
  could not be reached from the keyboard — labelled, disabled together
  with the picker, and clearing without opening the popup.
- it appears only once the field holds a value. The section field already
  reports that with `.form-date-time-filled`, so the rule belongs to that
  family and one selector covers all four shells; no component has to
  track the state itself.
- `cleaner` (on by default, as in v1), `cleanerIcon` and
  `ariaCleanerLabel` on all four shells.

The cross icon was duplicated byte for byte as a data URI in autocomplete
and multi select; it becomes one inline SVG in `util/icons`, painted in
currentColor so it follows the action through hover, validation and
disabled. Building an adornment moves to `util/form-control-group` — the
JavaScript half of the primitive's markup contract, and the same
consolidation the SCSS just went through.

Budgets: the JS ones move up for the restored feature, and the CSS ones
come down, so the 1892 lines this branch removed stay removed.
The cross came from a 16-unit grid, where the same shape carries roughly
twice the stroke weight of the 512-grid icons the indicators use — at a
shared 16px it read noticeably heavier than the calendar and clock next
to it.
The cross was drawn three different ways: a data URI duplicated byte for
byte in autocomplete and multi select, painted through a CSS mask on a
::before, and a third copy about to appear in the pickers. Both families
now use `.form-control-cleaner` with the shared inline SVG, so every
cleaner in the library is the same mark at the same size, and follows its
button's colour through hover, validation and disabled instead of needing
tokens of its own.

The primitive gives a cleaner its own icon size (.625rem, the value both
families already used): the cross is drawn on a 16-unit grid, so it
carries more weight than the 512-grid glyph icons and has to render
smaller to read as their equal.

32 declarations go — both families' cleaner width, height, padding, icon,
icon colours and icon size. Their indicator keeps its mask-based styling
until the Popup pass; merging what was left of the shared base rule into
it removes a duplicate selector.
The cleaner had its own smaller icon size, inherited from autocomplete,
because the cross it used was drawn on a 16-unit grid and carried about
twice the stroke weight of the icons beside it. That put a per-role
exception into a primitive whose whole point is that these are the same
kind of button.

The cross is cil-x from the CoreUI set now — drawn on the same 512 grid
as the icons it stands next to — so both roles share one rule and one
icon size, and the exception goes away with the token behind it.
cil-x carries 85 units of built-in margin where the calendar and clock
carry 16, so on the raw 512 box it painted a third less ink than they did
at the same rendered size — the cross looked inset rather than equal.

Cropping it to their exact coverage overshoots the other way: a cross
that reaches the corners reads larger than an orthogonal glyph of the
same box, its diagonal being 1.41x the side. The viewBox sits between the
two, which lets every adornment keep one icon size.
The guard exists so no public class disappears without a migration note.
Ten did: the per-family frames' indicators, separators and sizing
classes, and the three cleaners.
Their frames and cleaners already came across; the indicator was the last
part still styled by the family, drawn as a CSS mask through a ::before.
It is a `.form-control-action` now with the shared inline SVG, so every
adornment in the library is the same mark at the same size and takes its
colour from the button.

The chevron was the second icon duplicated byte for byte between these
two — it joins the cross in `util/icons`. Both indicators also become
genuinely disabled buttons rather than ones merely dropped from the tab
order with tabindex, which is what a disabled control should offer.

What is left of these two families' styling is what they actually own:
the options listbox, tags, search, and the caret turning when the popup
opens.
…he group's own

Two things were still hand-written inside the group. The editable carried
`.autocomplete-input`, a copy of the typography, colours, padding and
reset that `.form-control` plus the group's neutralization already give
it — it is a plain `.form-control` now. And both families wrapped their
adornments in a `-buttons` div that existed to give an unpadded frame its
height; the group lays its children out itself, so the buttons are its
children and the wrapper is gone.

With that, the frame pads both families like every other control instead
of delegating it inwards: the multi select tag row keeps only its own
spacing, and the autocomplete markup is now the same shape as a picker's
— control, cleaner, indicator.

The neutralization reaches nested editables too, since everything inside
a group is part of the control, and the picker's cleaner-visibility rule
is scoped to groups actually built around a section field. It was written
when their cleaner sat inside a wrapper; once every cleaner became a
direct child it would have hidden the ones the other components manage
themselves.
…ks for it

`selectionType: 'tags'` and `'chips'` were the same idea built twice, and
the default was the hand-rolled one: a div with a delete button, styled by
its own set of variables, without the roles a Chip carries since #658.

Both values render the Chip component now, so a selection looks and
behaves the same everywhere in the library and is themed through the
chip properties. `'tags'` stays accepted as a synonym.

That removes the second implementation entirely — its markup builder, its
update pass, its delete handler (chips route their own removal through
the selection model) and eighteen variables.
… panel

The pickers' dropdowns and the combobox listbox drew the same box three
times: three sets of background, border, radius, shadow and z-index
tokens, two of them byte-for-byte copies of each other. The panel now
carries `.popup`, the floating-surface primitive of the field components,
next to its structural class — which keeps its own shape (min-content
for pickers, toggler-width for the listbox) and the parent-driven open
state, and keeps carrying the family tokens for the teleported case.

Nineteen chrome declarations collapse into six `$popup-*` variables and
their custom properties: theme the popup once and every field
component's panel follows. Defaults match what the panels drew before,
so nothing changes visually.

The name is deliberately presentation-neutral — this is the anchored
mode; the fullscreen/modal mode for mobile (the v1 pickers had one, the
v2 rewrite lost it) is designed at this primitive and lands separately.
The popup borrows the look of a menu, never its interaction semantics:
the body inside owns those, and Menu itself stays the nav primitive.
… typing

The combobox family plan makes chip-input the selection surface of the
multi select: chips there come from the listbox selection, and typing
filters options rather than minting chips. `create: false` is that
mode: Enter, separators, paste-splitting and create-on-blur stop
producing chips, no hidden form input is rendered (the host owns the
value), while `add()` and the whole chip keyboard model keep working.

Groundwork for embedding: chip-input already adopts an existing <input>
instead of creating its own, so the host can hand it a field carrying
its own wiring.

it('should not render a hidden form input', () => {
fixtureEl.innerHTML = '<div id="ci"></div>'
const chipInput = new ChipInput(fixtureEl.querySelector('#ci'), { create: false }) // eslint-disable-line no-unused-vars
mrholek added 11 commits August 4, 2026 01:02
Every regression of the primitives cycle — the doubled padding, the white
disabled frame, the vanishing cleaner, the oversized cross — was caught by
eye, none by the 2950-test unit suite, because a shared rule reaches six
components and no unit test sees a pixel. This is the safety net for
exactly that class of change.

Vitest 4's browser mode ships toMatchScreenshot, so the suite is the same
Chromium-through-Playwright the unit specs run in — no new dependency. 21
screenshots cover the form-control-group and .popup surfaces: the pickers
(states, sizes, open popups, dark mode), autocomplete, multi select
(chips, dark), chip input and password input.

Determinism: fixed dates only, transitions and animations off, caret
hidden, fixed viewport, one screenshot per test (creating a missing
baseline aborts the test, so a second one would never get its baseline on
the first pass). Baselines are per platform — the matcher suffixes them
with the browser and OS — and macOS ones are committed here; the Visual
workflow generates the Linux set as an artifact on its first run and
gates once it is committed.

Runs via `npm run js-test-visual`; deliberately outside the local
pre-push gate, since macOS font rendering would fight the CI baselines.
…ation across chips

The chips were individually focusable but not navigable: no roving focus,
no keyboard model across the row. The selection area is a role-less
ChipSet now (the ChipInput precedent — the container mixes chips with the
search input), which brings arrow-key navigation, Home/End, keyboard
removal on a focused chip, and a role=status region announcing add and
remove — replacing the cruder aria-live on the container, which announced
every re-render. The selection model stays the component's own:
Chip.getOrCreateInstance makes both sides meet on one instance.
The frame declares the --cui-control-* properties on itself, so a family's
own copy of them - declared on the component root and shadowed on every
element inside the group - never had an effect. Each family keeps only
what is genuinely its own: placeholder colours, the pickers' footers, the
multi select's tag row. Gone with the layer: the frame-mirroring Sass
variables, the family-named sizing classes (.autocomplete-sm/-lg,
.form-multi-select-sm/-lg - sizing is .form-control-sm/-lg on the group),
the multi select's tag-delete tokens orphaned by the move to chips, and
the @use lines none of it needed. The visual suite pins the proof: all 21
screenshots unchanged.
…s in two chip input examples

Calendar and the date-time field partial each carried a box-shadow @use
nothing consumed. The chip input Sizes examples lost their
.form-control-group during the frame migration - the component does not
add the class itself, so the small and default variants rendered without
a frame.
…the chevron snapped

The shared $input-btn-transition moves color, background, border and
shadow; the indicator flip on an open combobox rotates. v5 gave the
indicator its own transform transition - the action token carries it now,
for every family at once.
Nothing in the harness ever loaded the stylesheet, so all 21 baselines
were pictures of bare markup: no frames, no chips, and the dark-mode
shots identical to the light ones. It would have passed any CSS
regression, which is the only thing it exists to catch. The spec imports
scss/coreui.scss now - the source, so a change shows up on the next run
with no build step - and every baseline is regenerated. Three cases join
it, one per way a validation state reaches the frame: the component root
(autocomplete, multi select) and native constraint validation on the
control inside the group.
… rule

Autocomplete and Multi Select carried their own copy of the frame's
validation state, and it had drifted: it set the border but not the
adornment colours, so an invalid multi select kept a grey chevron where
an invalid picker got a red one, and its focus ring was mixed from a
different source than every other control in the library. Both copies
are gone; the shared mixin now names their root classes alongside the
group's own state, and the four $*-invalid/valid-border-color variables
that shadowed --cui-form-*-border-color go with them.

Native constraint validation reaches the frame too, which it never did:
a required control inside a group under .was-validated only recoloured
itself, and the control inside a group draws no border. The
autocomplete's was-validated selector had been looking for a sibling
element the component never builds.

Locked by three visual cases - one per route the state takes to the
frame. Catching them needed the comparator to count antialiased pixels:
a 16px icon's stroke is almost all antialiasing, and pixelmatch discounts
those by default, so grey-to-red scored as no difference.
…lready does

Autocomplete is down to four rules and no variables of its own. What went:
a disabled state the group already reads off the disabled control inside
it (and reads more completely - it also sets the cursor and mutes the
adornments); a text colour the control inherits from the focused frame; a
placeholder colour identical to --cui-control-placeholder-color, which it
was shadowing rather than extending; and a selector feeding tokens to a
teleported .autocomplete-dropdown, a class the component stopped emitting
when the options menu became the shared combobox surface.

Multi Select loses the same placeholder mirror and dropdown selector, plus
a position: relative on the selection with nothing absolute inside it. Its
disabled state moves where the doctrine puts it - on the frame - so the
one shared rule covers it: the class lands on the group in JS now, since
the search input the group would otherwise read is absent when search is
off.

The visual suite is what makes this safe to do: the two states without a
screenshot got one first, so every removal had to prove itself against a
baseline taken before it.
…ilities API

The dark: variants were generated for the colour utilities only, never
documented, and duplicated every value under a [data-coreui-theme]
selector - 40 classes and ~10 kB of CSS to say what light-dark() already
says in the palette. The rtl key had nothing left to do: direction is
logical properties now, so rtl: false only emitted RTLCSS /* rtl:remove */
directives and rtl: true carried ltr/rtl value maps for a single utility.
Both branches doubled the generator, which is 44 lines lighter for it.

translate-middle follows Bootstrap v6-dev and stops flipping under
dir="rtl" - transform is not direction-aware, and the flag was the only
thing left asking the generator to care about direction.

Aligns the API with Bootstrap's v6-dev branch, which we track; their side
adds selector types and child-selector on top, which is the next step.
… rtl key

transform is not direction-aware: in an RTL document .start-50 resolves to
right: 50%, so an element centred on that line has to move the other way.
Dropping the generator's rtl key took the flip with it; it comes back as
the plain [dir="rtl"] rule it always should have been - one rule for the
one utility that needs it, instead of an option every utility carries.
The Sass suite covers the generator directly, and two specs still described
the old API: one asserted the RTLCSS removal directives that the dropped
rtl key used to emit, and the api spec caught the translate-middle
override leaking into a run whose $utilities map never asked for that
utility. The override is tied to the utility now, so disabling it takes
the override with it - which is what the spec was really reporting.
@mrholek
mrholek merged commit 15c0b54 into v6-dev Aug 4, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants