Skip to content

chore(Badge): migrate to CSS Modules with visual regression baseline#1054

Open
DreaminDani wants to merge 6 commits into
mainfrom
chore/migrate-badge-css-modules
Open

chore(Badge): migrate to CSS Modules with visual regression baseline#1054
DreaminDani wants to merge 6 commits into
mainfrom
chore/migrate-badge-css-modules

Conversation

@DreaminDani
Copy link
Copy Markdown
Contributor

@DreaminDani DreaminDani commented May 27, 2026

Commits

  • test(Badge): add visual regression baseline before CSS Modules migration — adds stories covering each type × state combination, both size variants, icon start/end, dismissible (with and without an icon), and ellipsisContent={false}; adds a Playwright spec under tests/display/badge.spec.ts with 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 ...props spread already forwards className and other HTML attributes to the underlying Container). Required by the Badge migration so the wrapping IconWrapper can receive its scoped class. Pure type change, no runtime behavior change.
  • chore(Badge): migrate styling from styled-components to CSS Modules — replaces the Wrapper, Content, BadgeContent (styled(IconWrapper)), and SvgContainer (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 .badgecontent are doubled (specificity 0,2,1) to preserve the source-order win the original styled-components emit had over Icon's own runtime-injected & svg { width: ... } rule; explained inline.

Verification

  • yarn test:visual tests/display/badge.spec.ts passes with zero snapshot regenerations
  • yarn lint:css, yarn lint:code, yarn build all green
  • grep -r 'styled-components' src/components/Badge/ empty

Closes 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 on state, 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> so className is typed when Badge passes module classes. Badge also merges an optional root className.

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.

@DreaminDani DreaminDani self-assigned this May 27, 2026
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 27, 2026

🦋 Changeset detected

Latest commit: ef38a1a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@clickhouse/click-ui Patch

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

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 IconWrapperProps to accept standard div HTML attributes such as className.
  • 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;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread tests/display/badge.spec.ts Outdated
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.
Comment thread src/components/Badge/Badge.tsx Outdated
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.
Comment thread src/components/Badge/Badge.module.css
Comment thread src/components/Badge/Badge.tsx Outdated
…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.
@workflow-authentication-public
Copy link
Copy Markdown
Contributor

Storybook Preview Deployed

✅ Preview URL: https://click-noiiz5qe7-clickhouse.vercel.app

Built from commit: 945f8e3e0b32d5c7ad16269dc79f7a1463286073

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ 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';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ef38a1a. Configure here.

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.

2 participants