feat(CheckboxGroup/RadioGroup): support icon in items - #464
Merged
Conversation
Ports nuxt/ui `07f3fe8d` (#6726). Upstream files 21 changes under one title,
doing three separable things; two are taken, one is declined by decision.
**The feature.** An item may carry an `icon`. On a checkbox it replaces the check
mark; when `indicator` is `hidden` — which makes the control `sr-only` — it
renders above the label instead. A radio has no icon inside its indicator, so
there the icon only ever appears in the hidden case.
Adapting it was larger here than the upstream diff suggests, because this fork's
`Checkbox` had **no `icon` prop at all**: the check and minus glyphs came
straight from `dictionary/icons`, fixed. So this adds public API rather than
extending it — `CheckboxProps.icon`, `CheckboxGroupItem.icon`,
`RadioGroupItem.icon`, a group-level `icon` on `CheckboxGroupProps` with the
item winning, and RadioGroup's `icon` slot, which did not exist.
`indeterminateIcon` is deliberately not added: the minus glyph stays governed by
the icon dictionary, which is this fork's override mechanism for it.
**The a11y fix the feature exposes.** With `indicator: hidden` the control
carries `sr-only`, computing to `clip-path: inset(50%)` — and the focus ring
lived on that same element, so tabbing to the checkbox produced a ring on
something clipped out of existence. Upstream's `focusCard()` helper is written in
their colour tokens, but the mechanism — `:has(:focus-visible)` on the wrapper —
transfers, and lands as one class list per component reusing the variables the
control already used. Scoped to `indicator: hidden`: upstream also moved focus
for `variant: card`, where our control is still visible and its own ring works.
**Declined:** upstream's palette refresh — `has-data-[state=checked]:bg-${color}/10`,
hover borders, `border-${color}/50`, the `highlight: false` branch and the
`focusControl` rewrite. This fork colours these components with `style-filled*`
and air tokens and already has its own checked/hover treatment for `card` and
`table`; translating them would be a design change wearing a port's clothes.
Their icon sizes for the *visible* indicator are not taken either — ours keeps
`size-full` inside the box, so nothing about the existing checkbox moves.
Docs take the item-type bullet, the `icon` entry in RadioGroup's item `b24ui`
list, and the note under **Indicator** on all three pages — without upstream's
`:badge{label="Soon"}`, which tracks their release train and which `aa5f4af0`,
two commits later in this queue, deletes. Per-item icons cannot go through
`::component-code` here, so both group pages get a `.vue` example; `checkbox.md`
keeps the inline form and gains an `indicator` selector so `start`/`end` stay
reachable. Both playgrounds get an icon row, per the §6 checklist.
Verified by measurement rather than assertion. Driving the built docs page in
Chromium and focusing the `sr-only` control, the item's computed outline is
`none 0px` with the new classes removed — the state before this port — and
`solid 2px rgb(0, 117, 255)` as shipped; the control itself computes
`clip-path: inset(50%)`, which is why its own ring was never a substitute. All
four generated rules are in the stylesheet with real declarations. Across the
three prerendered pages the icon renders inside the wrapper, the wrapper is the
centred column and `container` carries `h-auto`.
30 new tests, 12 new render cases. Gate with `CI=true`: `dev:prepare` · `lint` ·
`typecheck` · `test` (6900 passed, 6 skipped) · `build` · `docs:generate`
(1261 routes, up from 1259 for the two new examples).
Ledger: cursor → `07f3fe8d`, parity snapshot refreshed (one line, zero package
differences).
IgorShevchik
force-pushed
the
sync/nuxt-07f3fe8d
branch
from
August 22, 2026 07:45
1915491 to
e401152
Compare
This was referenced Aug 22, 2026
IgorShevchik
added a commit
that referenced
this pull request
Aug 22, 2026
`d6c3802a`, `f62aa143` and `aa5f4af0` now point at #466 and its squash SHA `4e42a221`. `07f3fe8d` is reconciled here too, with #464 and `a2d9083d`. That last one is a process slip worth recording: §6 step 4 has each port reconcile the **previous** entry, so #466 should have closed out `07f3fe8d` while it was closing out its own run. It did not, and nothing failed — the ledger simply carried a `pending-merge` on a commit that had shipped two hours earlier. Caught by re-reading the file rather than by a gate; the check is one line and now runs as part of this step. No entry is left unreconciled, and the cursor `aa5f4af0` is upstream HEAD. Bookkeeping only — no source, test or docs change. Co-authored-by: Shevchik Igor <noreply@anthropic.com>
IgorShevchik
pushed a commit
that referenced
this pull request
Aug 23, 2026
Only the subject reaches `CHANGELOG.md` — the body is not rendered, breaking
notes aside — so it is the one place a reader of the release notes can be
handed a way back to what was actually ported. Without it a port is
indistinguishable from local work in the only artefact most consumers read.
The subject stays ours. Copying upstream's own first line was the request as
originally put, and it is not what this does: their `Slider` is this fork's
`Range`, and §1 of PORTING.md makes that class of rename mandatory, so their
wording would put a component this library does not ship into our changelog.
The reference points at the commit; the sentence describes what changed here.
fix(Range): forward aria attributes to the thumb (nuxt/ui@d6c3802)
The trigger is a **new key in `processed`**, not the ledger being edited. That
distinction is the whole design: a reconciliation commit — §6 step 4 requires
one whenever a run's last entry has no follower — touches the same file and
ports nothing, and demanding a reference there would be wrong. Checked against
real history rather than fixtures: #467 (bookkeeping) passes, #470 (local work)
passes, #466 and #464 (real ports) are flagged.
Two mistakes worth recording, both of the shape this repository keeps hitting.
The first draft returned an empty array and tested it for truthiness, so every
commit was reported as an unnamed port; running it once was enough. The second
was quieter: without `HEAD^` the check returns nothing and passed silently, and
`actions/checkout` defaults to depth 1 — the same fail-open as the PyYAML
fallback removed in #468. It now warns when it cannot see the previous
revision, and ci.yml fetches depth 2.
`--stdin` skips it: a bare title cannot say what a commit touches, so the
PR-title job checks the parse and the type only. Mutation testing caught that
the case asserting this was vacuous — run from the repository root it passed
whether the guard respected `--stdin` or not, because HEAD there ports nothing.
It now stands on a port revision where the HEAD path does fire.
Refs #437
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012MsMuj8Fic9tjWVjyEyrxc
9 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Linked issue
Sync with
nuxt/ui@v4—07f3fe8d(nuxt/ui#6726). Three more commits remain in the queue after this one.Type of change
Description
Upstream files 21 changes under one title, doing three separable things. Two are taken, one is declined by maintainer decision.
The feature
An item may carry an
icon. On a checkbox it replaces the check mark; whenindicatorishidden— which makes the controlsr-only— it renders above the label instead. A radio has no icon inside its indicator, so there the icon only ever appears in the hidden case.Adapting it was larger here than the upstream diff suggests, because this fork's
Checkboxhad noiconprop at all: the check and minus glyphs came straight fromdictionary/icons, fixed. So this adds public API rather than extending it:CheckboxProps.icon?: IconComponent, defaulting toicons.checkCheckboxGroupItem.iconandRadioGroupItem.icon, plus a group-levelicononCheckboxGroupProps— the item's own icon wins (item.icon ?? props.icon)iconslot, which did not exist, and the matching entry in the item'sb24uipickindeterminateIconis deliberately not added: the minus glyph stays governed by the icon dictionary, which is this fork's override mechanism for it.The a11y fix the feature exposes
With
indicator: hiddenthe control carriessr-only, which computes toclip-path: inset(50%)— and the focus ring lived on that same element. Tabbing to the checkbox produced a ring on something clipped out of existence: no visible focus indication at all.Upstream's
focusCard()helper is written in their colour tokens, but the mechanism —:has(:focus-visible)on the wrapper — transfers, and lands as one class list per component reusing the variables the control already used (--b24ui-border-coloron Checkbox'sroot,--b24ui-backgroundon RadioGroup'sitem, which the colour variant defines onrootand inheritance carries down).Scoped to
indicator: hidden. Upstream also moved focus forvariant: cardgenerally; there our control is still visible and its own ring still works, so that is a design call rather than a defect.Declined
Upstream's palette refresh —
has-data-[state=checked]:bg-${color}/10,hover:not-has-disabled:…borders,border-${color}/50, thehighlight: falsebranch and thefocusControlrewrite. This fork colours these components withstyle-filled*classes and air tokens, and already has its own checked/hover treatment forcardandtable; translating them would be a design change wearing a port's clothes. Consequently upstream's two'with highlight indicator hidden'render cases are not ported, and their icon sizes for the visible indicator are not taken either — ours keepssize-fullinside the box, so nothing about the existing checkbox moves.Docs and playgrounds
Taken: the item-type bullet (
icon?: IconComponent, linked to#indicator), theiconentry in RadioGroup's itemb24uilist, and the note under Indicator on all three pages — without upstream's:badge{label="Soon"}, which tracks their release train and whichaa5f4af0, two commits later in this same queue, deletes. Upstream's editorial rewrites of example copy are unrelated to the feature and not taken.Per-item icons cannot be demonstrated through
::component-codehere — that renderer casts a single named icon viacast:, and this fork's idiom for items carrying components is a.vueexample. So both group pages get one (System/Light/Dark,indicator: hidden,variant: table), whilecheckbox.mdkeeps the inline form and gains anindicatorselector sostart/endstay reachable from the same block.MonitorIcondoes not exist in@bitrix24/b24icons-vue;ThemeIconis the outline set's equivalent.Both playgrounds get an icon row on
checkbox-groupandradio-group, per the §6 new-component checklist.Verification
The focus fix is measured, not asserted. Driving the built docs page in Chromium, focusing the
sr-onlycontrol and reading computed styles off the item:none 0pxsolid 2px rgb(0, 117, 255)and the control itself computes
clip-path: inset(50%), which is why its own ring was never a substitute. All four generated rules are in the stylesheet with real declarations (outline-width,outline-offset, both colour variants).Across the three prerendered pages the icon renders inside the wrapper (3 + 3 + 1), the wrapper is the centred column, and
containercarriesh-auto. Snapshot churn onTableis one<!--v-if-->placeholder per selection checkbox, from the newv-ifin the wrapper.30 new tests, 12 new render cases. Gate with
CI=true:dev:prepare·lint·typecheck·test(6900 passed, 6 skipped) ·build·docs:generate(1261 routes, up from 1259 for the two new examples).Ledger: cursor →
07f3fe8d, parity snapshot refreshed (one line, zero package differences).Checklist
Generated by Claude Code