Size and dim plugin icons with a data-icon-root contract - #3580
Merged
Merged
Conversation
ymichael
force-pushed
the
bb/investigate-small-duck-icon-thr_7bb8mbabbq
branch
from
September 12, 2026 07:27
8bfa132 to
4f0c47e
Compare
Container styles reached into icon internals with [&>svg] and [&_svg], which assumed every icon renders a bare <svg>. Icon wraps plugin-registered custom icons in a <span>, and PluginCompactIconMask renders a <span> mask, so those selectors missed plugin icons entirely: the sidebar footer duck stayed 16px while its neighbours took the coarse-pointer bump to 20px, and kept opacity 1 against their 0.8. Every icon now marks exactly one root element with data-icon-root, and the 34 container selectors that style icons target that marker instead of svg. Dropping the svg arm is what makes the descendant form safe: the inner svg of a wrapped icon stays unmarked, so [&_[data-icon-root]] cannot match both the wrapper and its child and compound size and opacity. Raw SVG glyphs that these rules already styled are marked too, preserving their current rendering. Containers that style third-party SVG rather than icons keep the svg selectors and are allowlisted: mermaid diagrams, recharts legends, and react-day-picker chevrons. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ymichael
force-pushed
the
bb/investigate-small-duck-icon-thr_7bb8mbabbq
branch
from
September 14, 2026 02:35
4f0c47e to
6f19765
Compare
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.
Human comments
What was wrong
Container styles reached into icon internals with
[&>svg]and[&_svg], which assume every icon renders a bare<svg>. That assumption is false for plugin icons:Iconwraps plugin-registered custom icon components in a<span>, andPluginCompactIconMaskrenders a<span>with a CSS mask. Those selectors therefore skipped plugin icons entirely.The visible symptom was the
lame-duckplugin's icon in the sidebar footer.COARSE_POINTER_CHILD_ICON_BUTTON_CLASSbumps[&>svg]fromsize-4tosize-5on narrow coarse-pointer viewports, so on mobile the duck stayed 16px while Settings, Remote access, and Report a bug went to 20px. Separately,SIDEBAR_FOOTER_ACTION_CLASSdims its glyph with[&>svg]:opacity-80, so the duck rendered at opacity 1 against its neighbours' 0.8 — on every pointer type, not just coarse. This affects any plugin icon in any of these containers, not just the footer;PluginIconis rendered from 21 files.What changed
Each icon now marks exactly one root element with
data-icon-root:BuiltinIcon's svg,ExtendedIcon's svg,Icon's custom-icon wrapper span, andPluginCompactIconMask's span. The 34 container selectors that style icons target that marker —[&>[data-icon-root]]and[&_[data-icon-root]]— acrossshared-ui(button,toggle,command,context-menu,dropdown-menu,alert,breadcrumb,coarse-pointer-sizing),apps/web(button), and 11apps/appcomponents.Dropping the
svgarm is load-bearing rather than cosmetic. Keeping it (as:is(svg,[data-icon-root])) is safe under the child combinator but broken under the descendant combinator, which 26 of the 34 sites use: it matches the wrapper and the svg inside it, compounding the declaration. Measured in Chrome,opacity-80applied that way yields an effective 0.64 rather than 0.8. Marking only the outermost element makes both combinators resolve to exactly one node.To preserve current rendering, raw SVG glyphs that these rules already styled are marked as roots too: the
disclosurechevron,GithubFaviconIcon, both context-window chevrons, and the locally-renderedbreadcrumbandcommandlucide icons. Deliberately left unmarked, because they are self-sized graphics rather than icons: theSplitPaneMiniMapglyph, the context-window gauge, and theh-0 w-0svg defs.Four files keep the
svgselectors and are allowlisted, because they style third-party SVG rather than icons:markdown-mermaid-diagram(mermaid),chart(recharts),calendar(react-day-picker), and one story.No wire, CLI, or plugin SDK surface changes.
How you verified
New
apps/app/src/components/ui/icon-root.test.tsx:data-icon-root.host > [data-icon-root]and.host [data-icon-root]resolve to exactly one node per shape — zero would mean the rule missed the icon, two would mean it applies twice:is(svg,[data-icon-root])as a descendant matches 2 nodes where the marker matches 1apps/app/src,apps/web/src, andpackages/shared-ui/srcfor new[&_svg]:/[&>svg]:utilities outside the allowlist, and asserts the allowlist has no stale entriesThe guard was mutation-tested rather than only observed passing: unmarking the wrapper span and reintroducing one
[&_svg]selector each fail it (4 of 5 assertions).Commands, all clean:
@bb/app4496 tests / 524 files,@bb/web124 tests / 23 files, 0 lint errors. One pre-existing assertion inFilePreview.test.tsxpinned the literal old class string and was updated.Manual verification drove the real app (
pnpm start:worktree) with thelame-duckplugin installed, via CDP withpointer: coarseemulation confirmed in-page. Before and after are both real builds of this checkout, with the served CSS hash checked each pass to rule out a stale bundle.Full-frame pixel diffs confirm nothing else moved: the mobile frame differs only in one 54×50 region (the duck); the desktop frame differs only in the duck and the animated
bblogo, which was cropped and confirmed to be the same geometry at a different animation phase; the context-menu frame differs only in that logo. A sweep of all 26 icon roots rendered on the root view found no zero-sized or oversized icons.Not covered by manual checks: the dev instance had no threads, so timeline surfaces such as
MessageActionBarrest on unit tests rather than a screenshot.🤖 Generated with Claude Code