chore(Badge): migrate to CSS Modules with visual regression baseline#1054
chore(Badge): migrate to CSS Modules with visual regression baseline#1054DreaminDani wants to merge 6 commits into
Conversation
🦋 Changeset detectedLatest commit: ef38a1a The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
There was a problem hiding this comment.
Pull request overview
This PR migrates the Badge component from styled-components to CSS Modules with cva/cn, while adding visual regression coverage to preserve appearance across Badge variants.
Changes:
- Replaces Badge styled-components with CSS Module classes and variant helpers.
- Widens
IconWrapperPropsto accept standard div HTML attributes such asclassName. - Adds Badge Storybook variants, Playwright visual tests, and a patch changeset.
Reviewed changes
Copilot reviewed 6 out of 48 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
src/components/Badge/Badge.tsx |
Reworks Badge rendering to use plain elements, Icon, IconWrapper, and CSS Module variant classes. |
src/components/Badge/Badge.module.css |
Adds Badge CSS Module styles, state/type/size variants, icon specificity handling, and close icon styling. |
src/components/Badge/Badge.stories.tsx |
Adds Storybook stories covering Badge visual variants for regression tests. |
src/components/IconWrapper/IconWrapper.types.ts |
Extends IconWrapper props with HTMLAttributes<HTMLDivElement> for typed className/attribute passthrough. |
tests/display/badge.spec.ts |
Adds Playwright visual regression tests for Badge in light and dark themes. |
.changeset/migrate-badge-to-css-modules.md |
Records a patch release note for the Badge CSS Modules migration. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| }: BadgeProps) => { | ||
| const resolvedType = type ?? 'opaque'; | ||
| const resolvedSize = size ?? 'md'; | ||
| const typestate = `${resolvedType}-${state}` as BadgeTypeState; |
There was a problem hiding this comment.
Thanks — but state is already destructured with a default of 'default' (Badge.tsx line 110), so passing type="solid" without state gives typestate = 'solid-default', not 'solid-undefined'. This matches the styled-component's $state = 'default' default. The close-icon variant uses the same destructured state, so it gets the resolved value too.
The previous title implied the test verified onClose firing, but the story's handler is a no-op so the assertion only proves the close target accepts a click without throwing. Renaming to match what it actually does, per Copilot review on PR #1054.
The styled-components `BadgeContent` (`styled(IconWrapper)`) was instantiated without `$type` or `$size` — only `$state` was forwarded. Both defaulted to `opaque` and `md` inside the styled rule, so the descendant <svg> color was always driven by the opaque-text-* tokens and the SVG dimensions were always md, regardless of the badge's actual `type` or `size` props. The initial migration passed the real `typestate` and `resolvedSize`, which silently changed visual output for `solid`-type and `sm`-size badges with icons. The original test suite missed it because every icon story used opaque/md (caught by Cursor Bugbot on PR #1054). Reverts `badgeContentVariants` to key off `state` only and adds two stories (`IconSolid`, `IconSm`) plus snapshots that lock in the original quirk so a future fix will be visible.
…paque color Two more behavior changes flagged by Cursor Bugbot on PR #1054: 1. The original `Content` styled component was never passed `$size`, so its `gap` always defaulted to the md token regardless of the badge's actual `size`. The previous migration passed `resolvedSize` to a `contentVariants` cva, making sm dismissible badges have a smaller gap. Reverted to a single `.content` class with the md gap baked in; dropped the contentVariants entirely. 2. The original `SvgContainer` (`styled.svg as={Icon}`) only forwarded `$state`; `$type` defaulted to `opaque`, so the close icon color always came from the opaque-text-* token. CSS Modules bundle before runtime styled-components, so my single-class `.closeicon_state_*` selectors (0,1,0 specificity) would lose to Icon's internal SvgWrapper rule `color: currentColor` on a solid dismissible badge, causing the close icon to inherit the solid-text-* color from the wrapper. Bumped these selectors to doubled-class specificity (0,2,1) — same pattern as `badgecontent_state_*` for the same reason. Added `DismissibleSm` and `DismissibleSolid` stories plus light/dark snapshots to lock in both quirks.
Storybook Preview Deployed✅ Preview URL: https://click-noiiz5qe7-clickhouse.vercel.app Built from commit: |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit ef38a1a. Configure here.
| | 'solid-danger' | ||
| | 'solid-disabled' | ||
| | 'solid-warning' | ||
| | 'solid-info'; |
There was a problem hiding this comment.
Manually-maintained type duplicates derivable cross-product
Low Severity
BadgeTypeState manually enumerates all 14 members of the BadgeType × BadgeState cross-product, but TypeScript's template literal types can derive it automatically as `${BadgeType}-${BadgeState}`. The manual union risks drifting if a new state or type is added to Badge.types.ts, and the as BadgeTypeState cast on line 103 would silently mask any mismatch.
Reviewed by Cursor Bugbot for commit ef38a1a. Configure here.


Commits
test(Badge): add visual regression baseline before CSS Modules migration— adds stories covering eachtype×statecombination, bothsizevariants,iconstart/end,dismissible(with and without an icon), andellipsisContent={false}; adds a Playwright spec undertests/display/badge.spec.tswith light + dark snapshots and basic close-button accessibility assertions.chore(IconWrapper): widen props to accept HTMLAttributes for className passthrough— narrow type widening that reflects IconWrapper's existing runtime behavior (its...propsspread already forwardsclassNameand other HTML attributes to the underlying Container). Required by the Badge migration so the wrappingIconWrappercan receive its scoped class. Pure type change, no runtime behavior change.chore(Badge): migrate styling from styled-components to CSS Modules— replaces theWrapper,Content,BadgeContent(styled(IconWrapper)), andSvgContainer(styled.svg as={Icon}) styled components with.module.css+cva/cn. DOM-identical, byte-for-byte visual parity verified across 42 snapshots. The icon-size selectors on.badgecontentare doubled (specificity 0,2,1) to preserve the source-order win the original styled-components emit had overIcon's own runtime-injected& svg { width: ... }rule; explained inline.Verification
yarn test:visual tests/display/badge.spec.tspasses with zero snapshot regenerationsyarn lint:css,yarn lint:code,yarn buildall greengrep -r 'styled-components' src/components/Badge/emptyCloses CUI-9
Note
Medium Risk
Large styling rewrite with specificity workarounds against Icon’s styled-components, but scope is limited to Badge/IconWrapper typing and is backed by extensive visual regression tests.
Overview
Badge moves from styled-components to CSS Modules with
cva/cn, using--click-badge-*tokens for opaque/solid states, sizes, content layout, icon wrapper, and dismiss close icon. Styling intentionally keeps prior quirks (e.g. inner icon/close colors and sizes keyed only onstate, md content gap) and uses doubled-class selectors so module CSS still wins over Icon’s runtime styled-components rules.IconWrapper props now extend
HTMLAttributes<HTMLDivElement>soclassNameis typed when Badge passes module classes. Badge also merges an optional rootclassName.Storybook gains stories for each type/state, sizes, icons, dismissible cases, and ellipsis-off. A new Playwright suite snapshots those stories in light and dark (plus basic close
aria-label/click checks). A patch changeset documents no intended behavior change.Reviewed by Cursor Bugbot for commit ef38a1a. Bugbot is set up for automated code reviews on this repo. Configure here.