Tailwind v4#842
Conversation
📖 Storybook Preview |
|
Caution MetaMask internal reviewing guidelines:
Ignoring alerts on:
|
698afa6 to
55c6251
Compare
📖 Storybook Preview |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: New typography classes have no CSS utility definitions
- Added @Utility definitions for all new text-* variants in theme.css with responsive metrics so font-size, line-height, and letter-spacing apply.
- ✅ Fixed: Test expects wrong text-color conflict winner
- Corrected the test to expect text-muted as the conflict winner per tailwind-merge’s last-wins behavior.
Or push these changes by commenting:
@cursor push 0a19cee029
Preview (0a19cee029)
diff --git a/packages/design-system-react/src/utils/tw-merge.test.ts b/packages/design-system-react/src/utils/tw-merge.test.ts
--- a/packages/design-system-react/src/utils/tw-merge.test.ts
+++ b/packages/design-system-react/src/utils/tw-merge.test.ts
@@ -91,7 +91,7 @@
const result = twMerge(
'text-body-md text-heading-lg text-default text-muted',
);
- expect(result).toBe('text-heading-lg text-default');
+ expect(result).toBe('text-heading-lg text-muted');
});
});
});
diff --git a/packages/design-tokens/src/tailwind/theme.css b/packages/design-tokens/src/tailwind/theme.css
--- a/packages/design-tokens/src/tailwind/theme.css
+++ b/packages/design-tokens/src/tailwind/theme.css
@@ -502,3 +502,187 @@
@utility shadow-default { --shadow-color: var(--color-shadow-default) !important; }
@utility shadow-primary { --shadow-color: var(--color-shadow-primary) !important; }
@utility shadow-error { --shadow-color: var(--color-shadow-error) !important; }
+
+/* New typography shortcut utilities (unprefixed) */
+@utility text-display-lg {
+ font-size: var(--typography-s-display-lg-font-size);
+ line-height: var(--typography-s-display-lg-line-height);
+ letter-spacing: var(--typography-s-display-lg-letter-spacing);
+}
+@media (min-width: 48rem) {
+ .text-display-lg {
+ font-size: var(--typography-l-display-lg-font-size);
+ line-height: var(--typography-l-display-lg-line-height);
+ letter-spacing: var(--typography-l-display-lg-letter-spacing);
+ }
+}
+
+@utility text-display-md {
+ font-size: var(--typography-s-display-md-font-size);
+ line-height: var(--typography-s-display-md-line-height);
+ letter-spacing: var(--typography-s-display-md-letter-spacing);
+}
+@media (min-width: 48rem) {
+ .text-display-md {
+ font-size: var(--typography-l-display-md-font-size);
+ line-height: var(--typography-l-display-md-line-height);
+ letter-spacing: var(--typography-l-display-md-letter-spacing);
+ }
+}
+
+@utility text-heading-lg {
+ font-size: var(--typography-s-heading-lg-font-size);
+ line-height: var(--typography-s-heading-lg-line-height);
+ letter-spacing: var(--typography-s-heading-lg-letter-spacing);
+}
+@media (min-width: 48rem) {
+ .text-heading-lg {
+ font-size: var(--typography-l-heading-lg-font-size);
+ line-height: var(--typography-l-heading-lg-line-height);
+ letter-spacing: var(--typography-l-heading-lg-letter-spacing);
+ }
+}
+
+@utility text-heading-md {
+ font-size: var(--typography-s-heading-md-font-size);
+ line-height: var(--typography-s-heading-md-line-height);
+ letter-spacing: var(--typography-s-heading-md-letter-spacing);
+}
+@media (min-width: 48rem) {
+ .text-heading-md {
+ font-size: var(--typography-l-heading-md-font-size);
+ line-height: var(--typography-l-heading-md-line-height);
+ letter-spacing: var(--typography-l-heading-md-letter-spacing);
+ }
+}
+
+@utility text-heading-sm {
+ font-size: var(--typography-s-heading-sm-font-size);
+ line-height: var(--typography-s-heading-sm-line-height);
+ letter-spacing: var(--typography-s-heading-sm-letter-spacing);
+}
+@media (min-width: 48rem) {
+ .text-heading-sm {
+ font-size: var(--typography-l-heading-sm-font-size);
+ line-height: var(--typography-l-heading-sm-line-height);
+ letter-spacing: var(--typography-l-heading-sm-letter-spacing);
+ }
+}
+
+@utility text-body-lg {
+ /* Use medium metrics to align with default BodyLg weight */
+ font-size: var(--typography-s-body-lg-medium-font-size);
+ line-height: var(--typography-s-body-lg-medium-line-height);
+ letter-spacing: var(--typography-s-body-lg-medium-letter-spacing);
+}
+@media (min-width: 48rem) {
+ .text-body-lg {
+ font-size: var(--typography-l-body-lg-medium-font-size);
+ line-height: var(--typography-l-body-lg-medium-line-height);
+ letter-spacing: var(--typography-l-body-lg-medium-letter-spacing);
+ }
+}
+
+@utility text-body-md {
+ font-size: var(--typography-s-body-md-font-size);
+ line-height: var(--typography-s-body-md-line-height);
+ letter-spacing: var(--typography-s-body-md-letter-spacing);
+}
+@media (min-width: 48rem) {
+ .text-body-md {
+ font-size: var(--typography-l-body-md-font-size);
+ line-height: var(--typography-l-body-md-line-height);
+ letter-spacing: var(--typography-l-body-md-letter-spacing);
+ }
+}
+
+@utility text-body-sm {
+ font-size: var(--typography-s-body-sm-font-size);
+ line-height: var(--typography-s-body-sm-line-height);
+ letter-spacing: var(--typography-s-body-sm-letter-spacing);
+}
+@media (min-width: 48rem) {
+ .text-body-sm {
+ font-size: var(--typography-l-body-sm-font-size);
+ line-height: var(--typography-l-body-sm-line-height);
+ letter-spacing: var(--typography-l-body-sm-letter-spacing);
+ }
+}
+
+@utility text-body-xs {
+ font-size: var(--typography-s-body-xs-font-size);
+ line-height: var(--typography-s-body-xs-line-height);
+ letter-spacing: var(--typography-s-body-xs-letter-spacing);
+}
+@media (min-width: 48rem) {
+ .text-body-xs {
+ font-size: var(--typography-l-body-xs-font-size);
+ line-height: var(--typography-l-body-xs-line-height);
+ letter-spacing: var(--typography-l-body-xs-letter-spacing);
+ }
+}
+
+@utility text-page-heading {
+ font-size: var(--typography-s-page-heading-font-size);
+ line-height: var(--typography-s-page-heading-line-height);
+ letter-spacing: var(--typography-s-page-heading-letter-spacing);
+}
+@media (min-width: 48rem) {
+ .text-page-heading {
+ font-size: var(--typography-l-page-heading-font-size);
+ line-height: var(--typography-l-page-heading-line-height);
+ letter-spacing: var(--typography-l-page-heading-letter-spacing);
+ }
+}
+
+@utility text-section-heading {
+ font-size: var(--typography-s-section-heading-font-size);
+ line-height: var(--typography-s-section-heading-line-height);
+ letter-spacing: var(--typography-s-section-heading-letter-spacing);
+}
+@media (min-width: 48rem) {
+ .text-section-heading {
+ font-size: var(--typography-l-section-heading-font-size);
+ line-height: var(--typography-l-section-heading-line-height);
+ letter-spacing: var(--typography-l-section-heading-letter-spacing);
+ }
+}
+
+@utility text-button-label-md {
+ font-size: var(--typography-s-button-label-md-font-size);
+ line-height: var(--typography-s-button-label-md-line-height);
+ letter-spacing: var(--typography-s-button-label-md-letter-spacing);
+}
+@media (min-width: 48rem) {
+ .text-button-label-md {
+ font-size: var(--typography-l-button-label-md-font-size);
+ line-height: var(--typography-l-button-label-md-line-height);
+ letter-spacing: var(--typography-l-button-label-md-letter-spacing);
+ }
+}
+
+@utility text-button-label-lg {
+ font-size: var(--typography-s-button-label-lg-font-size);
+ line-height: var(--typography-s-button-label-lg-line-height);
+ letter-spacing: var(--typography-s-button-label-lg-letter-spacing);
+}
+@media (min-width: 48rem) {
+ .text-button-label-lg {
+ font-size: var(--typography-l-button-label-lg-font-size);
+ line-height: var(--typography-l-button-label-lg-line-height);
+ letter-spacing: var(--typography-l-button-label-lg-letter-spacing);
+ }
+}
+
+@utility text-amount-display-lg {
+ font-size: var(--typography-s-amount-display-lg-font-size);
+ line-height: var(--typography-s-amount-display-lg-line-height);
+ letter-spacing: var(--typography-s-amount-display-lg-letter-spacing);
+}
+@media (min-width: 48rem) {
+ .text-amount-display-lg {
+ font-size: var(--typography-l-amount-display-lg-font-size);
+ line-height: var(--typography-l-amount-display-lg-line-height);
+ letter-spacing: var(--typography-l-amount-display-lg-letter-spacing);
+ }
+}b93e3c8 to
d5ccea6
Compare
georgewrmarshall
left a comment
There was a problem hiding this comment.
Left some comments. I'm not sure we need to change anything in the design-system-tailwind-preset package
📖 Storybook Preview |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: cleancss corrupts Tailwind v4 directives in published theme
- Replaced clean-css minification with a direct copy for theme.css to preserve Tailwind v4 at-rules.
- ✅ Fixed: Web ESLint drops correctness rules for Tailwind classes
- Enabled the better-tailwindcss correctness preset in the web ESLint config to restore class validation rules.
Or push these changes by commenting:
@cursor push 7d3b2ced3f
Preview (7d3b2ced3f)
diff --git a/eslint.config.mjs b/eslint.config.mjs
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -233,6 +233,7 @@
plugins: {
'better-tailwindcss': betterTailwind,
},
+ extends: [betterTailwind.configs.correctness],
rules: {
'better-tailwindcss/sort-classes': 'error',
},
diff --git a/packages/design-tokens/package.json b/packages/design-tokens/package.json
--- a/packages/design-tokens/package.json
+++ b/packages/design-tokens/package.json
@@ -39,7 +39,7 @@
"scripts": {
"build": "ts-bridge --project tsconfig.build.json --verbose --clean --no-references && yarn build:css",
"build:css": "cleancss -o dist/styles.css src/css/index.css && yarn build:css:tailwind",
- "build:css:tailwind": "mkdir -p dist/tailwind && cleancss -o dist/tailwind/theme.css src/tailwind/theme.css",
+ "build:css:tailwind": "mkdir -p dist/tailwind && cp src/tailwind/theme.css dist/tailwind/theme.css",
"check:tailwind-theme-parity": "tsx scripts/check-tailwind-theme-parity.ts",
"changelog:update": "../../scripts/update-changelog.sh @metamask/design-tokens",
"changelog:validate": "../../scripts/validate-changelog.sh @metamask/design-tokens",
📖 Storybook Preview |
📖 Storybook Preview |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Conflicting classes lint rule downgraded from error to warn
- Updated eslint rule to 'error' for better-tailwindcss/no-conflicting-classes to match previous enforcement.
- ✅ Fixed: Default Tailwind font sizes and weights not disabled
- Added --font-size-: initial and --font-weight-: initial in @theme to clear defaults.
Or push these changes by commenting:
@cursor push 1e04a9d74c
Preview (1e04a9d74c)
diff --git a/eslint.config.mjs b/eslint.config.mjs
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -235,7 +235,7 @@
},
rules: {
'better-tailwindcss/sort-classes': 'error',
- 'better-tailwindcss/no-conflicting-classes': 'warn',
+ 'better-tailwindcss/no-conflicting-classes': 'error',
'better-tailwindcss/no-unregistered-classes': 'error',
},
},
diff --git a/packages/design-tokens/src/tailwind/theme.css b/packages/design-tokens/src/tailwind/theme.css
--- a/packages/design-tokens/src/tailwind/theme.css
+++ b/packages/design-tokens/src/tailwind/theme.css
@@ -3,6 +3,9 @@
@import '../css/shadow.css';
@theme {
+ /* Disable default Tailwind font sizes and weights */
+ --font-size-*: initial;
+ --font-weight-*: initial;
/* Essential Tailwind colors required for basic utilities */
--color-inherit: inherit;
--color-current: currentColor;
📖 Storybook Preview |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 3 total unresolved issues (including 2 from previous reviews).
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Missing
.lightCSS selector breaks theme switching- Added explicit
[data-theme='light'], .lightblock intheme.cssreapplying light theme variables so.lightcan override.darkin nested contexts.
- Added explicit
Or push these changes by commenting:
@cursor push bcd1e57523
Preview (bcd1e57523)
diff --git a/packages/design-tokens/src/tailwind/theme.css b/packages/design-tokens/src/tailwind/theme.css
--- a/packages/design-tokens/src/tailwind/theme.css
+++ b/packages/design-tokens/src/tailwind/theme.css
@@ -344,6 +344,173 @@
--color-shadow-error: #ff758433;
}
+/**
+ * Light Theme Colors
+ * Explicitly re-apply light values so `.light` (or [data-theme='light'])
+ * can override a surrounding `.dark` context.
+ */
+[data-theme='light'],
+.light {
+ /* Background default should be the darkest shade, 0 elevation.
+ Section is +1 elevation, subsection is +2 elevation.
+ Alternative should be deprecated. */
+ --color-background-default: var(--brand-colors-grey-grey000);
+ --color-background-section: var(--brand-colors-grey-grey050);
+ --color-background-subsection: var(--brand-colors-grey-grey000);
+ --color-background-alternative: var(--brand-colors-grey-grey050);
+
+ /* Applied to interactive elements, such as buttons.
+ For light mode, we use 8% increments of opacity to offer
+ sufficient affordance for usability. */
+ --color-background-muted: #b4b4b528;
+ --color-background-muted-hover: #b4b4b53d;
+ --color-background-muted-pressed: #b4b4b552;
+
+ /* Ensures visual consistency with section and subsection. */
+ --color-background-default-hover: var(--brand-colors-grey-grey050);
+ --color-background-default-pressed: var(--brand-colors-grey-grey100);
+
+ /* These colors should be deprecated eventually for simplicity */
+ --color-background-alternative-hover: #ebedf1;
+ --color-background-alternative-pressed: #e1e4ea;
+
+ /* These have opacities of pure white for general usage.
+ Visually, they align with section and subsection.*/
+ --color-background-hover: #b4b4b528;
+ --color-background-pressed: #b4b4b53d;
+
+ /* These are our content colors.
+ Contrast ratio of alternative: 5.7 on default, 5.1 on section.
+ Contrast ratio of muted: 1.9 on default, 1.7 on section.*/
+ --color-text-default: var(--brand-colors-grey-grey900);
+ --color-text-alternative: var(--brand-colors-grey-grey500);
+ --color-text-muted: var(--brand-colors-grey-grey200);
+
+ --color-icon-default: var(--brand-colors-grey-grey900);
+ --color-icon-default-hover: #2a2b2c;
+ --color-icon-default-pressed: #414243;
+
+ --color-icon-alternative: var(--brand-colors-grey-grey500);
+ --color-icon-muted: var(--brand-colors-grey-grey200);
+ --color-icon-inverse: var(--brand-colors-grey-grey000);
+
+ /* Border default has a 3:3 ratio when applied on bg-default
+ and 3.0 on section. We use opacity for border-muted so it
+ maintains sufficient contrast on bg-default and bg-section.*/
+ --color-border-default: var(--brand-colors-grey-grey400);
+ --color-border-muted: #b4b4b566;
+
+ /* Derived from the background hue, 264.5, for consistency.
+ Opacity for default is 36%, alternative is 57%. Default is meant
+ to be the inverse of dark mode so the layering feels consistent
+ across themes. Alternative is relatively darker in light mode for
+ better contrast.*/
+ --color-overlay-default: #0a0d135c;
+ --color-overlay-alternative: #0a0d1392;
+ --color-overlay-inverse: var(--brand-colors-grey-grey000);
+
+ /* For primary semantic elements: interactive, active, selected (#4459ff) */
+ --color-primary-default: var(--brand-colors-blue-blue500);
+ /* Stronger color for primary semantic elements (#2c3dc5) */
+ --color-primary-alternative: var(--brand-colors-blue-blue600);
+ /* Muted color for primary semantic elements (#4459ff1a) */
+ --color-primary-muted: #4459ff1a;
+ /* For elements placed on top of primary/default (#ffffff) */
+ --color-primary-inverse: var(--brand-colors-grey-grey000);
+ /* Hover state surface for primary/default (#384df5) */
+ --color-primary-default-hover: #384df5;
+ /* Pressed state surface for primary/default (#2b3eda) */
+ --color-primary-default-pressed: #2b3eda;
+ /* Hover state surface for primary/muted (#4459ff26) */
+ --color-primary-muted-hover: #4459ff26;
+ /* Pressed state surface for primary/muted (#4459ff33) */
+ --color-primary-muted-pressed: #4459ff33;
+ /* For danger semantic elements: error, critical, destructive (#ca3542) */
+ --color-error-default: var(--brand-colors-red-red500);
+ /* Stronger color for error semantic (#952731) */
+ --color-error-alternative: var(--brand-colors-red-red600);
+ /* Muted color for error semantic (#ca35421a) */
+ --color-error-muted: #ca35421a;
+ /* For elements placed on top of error/default (#ffffff) */
+ --color-error-inverse: var(--brand-colors-grey-grey000);
+ /* Hover state surface for error/default (#ba313d) */
+ --color-error-default-hover: #ba313d;
+ /* Pressed state surface for error/default (#9a2832) */
+ --color-error-default-pressed: #9a2832;
+ /* Hover state surface for error/muted (#ca354226) */
+ --color-error-muted-hover: #ca354226;
+ /* Pressed state surface for error/muted (#ca354233) */
+ --color-error-muted-pressed: #ca354233;
+ /* For warning semantic elements: caution, attention, precaution (#9a6300) */
+ --color-warning-default: var(--brand-colors-yellow-yellow500);
+ /* Muted color option for warning semantic (#9a63001a) */
+ --color-warning-muted: #9a63001a;
+ /* For elements placed on top of warning/default (#ffffff) */
+ --color-warning-inverse: var(--brand-colors-grey-grey000);
+ /* Hover state surface for warning/default (#855500) */
+ --color-warning-default-hover: #855500;
+ /* Pressed state surface for warning/default (#5c3b00) */
+ --color-warning-default-pressed: #5c3b00;
+ /* Hover state surface for warning/muted (#9a630026) */
+ --color-warning-muted-hover: #9a630026;
+ /* Pressed state surface for warning/muted (#9a630033) */
+ --color-warning-muted-pressed: #9a630033;
+ /* For positive semantic elements: success, confirm, complete, safe (#457A39) */
+ --color-success-default: var(--brand-colors-lime-lime500);
+ /* Muted color for positive semantic (#457a391a) */
+ --color-success-muted: #457a391a;
+ /* For elements placed on top of success/default (#ffffff) */
+ --color-success-inverse: var(--brand-colors-grey-grey000);
+ /* Hover state surface for success/default (#3d6c32) */
+ --color-success-default-hover: #3d6c32;
+ /* Pressed state surface for success/default (#2d5025) */
+ --color-success-default-pressed: #2d5025;
+ /* Hover state surface for success/muted (#457a3926) */
+ --color-success-muted-hover: #457a3926;
+ /* Pressed state surface for success/muted (#457a3933) */
+ --color-success-muted-pressed: #457a3933;
+ /* For informational read-only elements: info, reminder, hint (#4459ff) */
+ --color-info-default: var(--brand-colors-blue-blue500);
+ /* Muted color for informational semantic (#4459ff1a) */
+ --color-info-muted: #4459ff1a;
+ /* For elements placed on top of info/default (#ffffff) */
+ --color-info-inverse: var(--brand-colors-grey-grey000);
+ /* Expressive color in light orange (#ffa680) */
+ --color-accent01-light: var(--brand-colors-orange-orange200);
+ /* Expressive color in orange (#ff5c16) */
+ --color-accent01-normal: var(--brand-colors-orange-orange400);
+ /* Expressive color in dark orange (#661800) */
+ --color-accent01-dark: var(--brand-colors-orange-orange700);
+ /* Expressive color in light purple (#eac2ff) */
+ --color-accent02-light: var(--brand-colors-purple-purple100);
+ /* Expressive color in purple (#d075ff) */
+ --color-accent02-normal: var(--brand-colors-purple-purple300);
+ /* Expressive color in dark purple (#3d065f) */
+ --color-accent02-dark: var(--brand-colors-purple-purple800);
+ /* Expressive color in light lime (#e5ffc3) */
+ --color-accent03-light: var(--brand-colors-lime-lime050);
+ /* Expressive color in lime (#baf24a) */
+ --color-accent03-normal: var(--brand-colors-lime-lime100);
+ /* Expressive color in dark lime (#013330) */
+ --color-accent03-dark: var(--brand-colors-lime-lime700);
+ /* Expressive color in light indigo (#) */
+ --color-accent04-light: var(--brand-colors-indigo-indigo100);
+ /* Expressive color in indigo (#) */
+ --color-accent04-normal: var(--brand-colors-indigo-indigo200);
+ /* Expressive color in dark indigo (#) */
+ --color-accent04-dark: var(--brand-colors-indigo-indigo800);
+ /* For Flask primary accent color (#8f44e4) */
+ --color-flask-default: var(--brand-colors-purple-purple500);
+ /* For elements placed on top of flask/default (#ffffff) */
+ --color-flask-inverse: var(--brand-colors-grey-grey000);
+ /* For neutral drop shadow color (black-10% | black-40%) */
+ --color-shadow-default: #0000001a;
+ /* For primary drop shadow color (blue500-20% | blue300-20%) */
+ --color-shadow-primary: #4459ff33;
+ /* For critical/danger drop shadow color (red50-20% | red300-20%) */
+ --color-shadow-error: #ca354266;
+}
+
/* Color Shortcut Utilities - Enable shorter class names */
/* Text shortcuts: text-default instead of text-text-default */
@utility text-default {
📖 Storybook Preview |
📖 Storybook Preview |
📖 Storybook Preview |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Duplicated color tokens risk silent v3/v4 divergence
- Replaced the duplicated .light/.dark variable blocks in theme.css with imports of the source light/dark theme CSS files so values come from a single source of truth.
Or push these changes by commenting:
@cursor push 151c5fac02
Preview (151c5fac02)
diff --git a/packages/design-tokens/src/tailwind/theme.css b/packages/design-tokens/src/tailwind/theme.css
--- a/packages/design-tokens/src/tailwind/theme.css
+++ b/packages/design-tokens/src/tailwind/theme.css
@@ -1,6 +1,8 @@
@import '../css/brand-colors.css';
@import '../css/typography.css';
@import '../css/shadow.css';
+@import '../css/light-theme-colors.css';
+@import '../css/dark-theme-colors.css';
@theme {
--font-size-*: initial;
@@ -183,257 +185,6 @@
var(--shadow-color, var(--color-shadow-default));
}
-/**
- * Light Theme Colors
- * Explicitly scoped so .light resets variables when nested inside .dark.
- * The :root values come from @theme above — this block only needs
- * the class/attribute selectors for runtime theme switching.
- */
-[data-theme='light'],
-.light {
- --color-background-default: var(--brand-colors-grey-grey000);
- --color-background-section: var(--brand-colors-grey-grey050);
- --color-background-subsection: var(--brand-colors-grey-grey000);
- --color-background-alternative: var(--brand-colors-grey-grey050);
- --color-background-muted: #b4b4b528;
- --color-background-muted-hover: #b4b4b53d;
- --color-background-muted-pressed: #b4b4b552;
- --color-background-default-hover: var(--brand-colors-grey-grey050);
- --color-background-default-pressed: var(--brand-colors-grey-grey100);
- --color-background-alternative-hover: #ebedf1;
- --color-background-alternative-pressed: #e1e4ea;
- --color-background-hover: #b4b4b528;
- --color-background-pressed: #b4b4b53d;
- --color-text-default: var(--brand-colors-grey-grey900);
- --color-text-alternative: var(--brand-colors-grey-grey500);
- --color-text-muted: var(--brand-colors-grey-grey200);
- --color-icon-default: var(--brand-colors-grey-grey900);
- --color-icon-default-hover: #2a2b2c;
- --color-icon-default-pressed: #414243;
- --color-icon-alternative: var(--brand-colors-grey-grey500);
- --color-icon-muted: var(--brand-colors-grey-grey200);
- --color-icon-inverse: var(--brand-colors-grey-grey000);
- --color-border-default: var(--brand-colors-grey-grey400);
- --color-border-muted: #b4b4b566;
- --color-overlay-default: #0a0d135c;
- --color-overlay-alternative: #0a0d1392;
- --color-overlay-inverse: var(--brand-colors-grey-grey000);
- --color-primary-default: var(--brand-colors-blue-blue500);
- --color-primary-alternative: var(--brand-colors-blue-blue600);
- --color-primary-muted: #4459ff1a;
- --color-primary-inverse: var(--brand-colors-grey-grey000);
- --color-primary-default-hover: #384df5;
- --color-primary-default-pressed: #2b3eda;
- --color-primary-muted-hover: #4459ff26;
- --color-primary-muted-pressed: #4459ff33;
- --color-error-default: var(--brand-colors-red-red500);
- --color-error-alternative: var(--brand-colors-red-red600);
- --color-error-muted: #ca35421a;
- --color-error-inverse: var(--brand-colors-grey-grey000);
- --color-error-default-hover: #ba313d;
- --color-error-default-pressed: #9a2832;
- --color-error-muted-hover: #ca354226;
- --color-error-muted-pressed: #ca354233;
- --color-warning-default: var(--brand-colors-yellow-yellow500);
- --color-warning-muted: #9a63001a;
- --color-warning-inverse: var(--brand-colors-grey-grey000);
- --color-warning-default-hover: #855500;
- --color-warning-default-pressed: #5c3b00;
- --color-warning-muted-hover: #9a630026;
- --color-warning-muted-pressed: #9a630033;
- --color-success-default: var(--brand-colors-lime-lime500);
- --color-success-muted: #457a391a;
- --color-success-inverse: var(--brand-colors-grey-grey000);
- --color-success-default-hover: #3d6c32;
- --color-success-default-pressed: #2d5025;
- --color-success-muted-hover: #457a3926;
- --color-success-muted-pressed: #457a3933;
- --color-info-default: var(--brand-colors-blue-blue500);
- --color-info-muted: #4459ff1a;
- --color-info-inverse: var(--brand-colors-grey-grey000);
- --color-accent01-light: var(--brand-colors-orange-orange200);
- --color-accent01-normal: var(--brand-colors-orange-orange400);
- --color-accent01-dark: var(--brand-colors-orange-orange700);
- --color-accent02-light: var(--brand-colors-purple-purple100);
- --color-accent02-normal: var(--brand-colors-purple-purple300);
- --color-accent02-dark: var(--brand-colors-purple-purple800);
- --color-accent03-light: var(--brand-colors-lime-lime050);
- --color-accent03-normal: var(--brand-colors-lime-lime100);
- --color-accent03-dark: var(--brand-colors-lime-lime700);
- --color-accent04-light: var(--brand-colors-indigo-indigo100);
- --color-accent04-normal: var(--brand-colors-indigo-indigo200);
- --color-accent04-dark: var(--brand-colors-indigo-indigo800);
- --color-flask-default: var(--brand-colors-purple-purple500);
- --color-flask-inverse: var(--brand-colors-grey-grey000);
- --color-shadow-default: #0000001a;
- --color-shadow-primary: #4459ff33;
- --color-shadow-error: #ca354266;
-}
-
-/**
- * Dark Theme Colors
- */
-[data-theme='dark'],
-.dark {
- /* Background default should be the darkest shade, 0 elevation.
- Section is +1 elevation, subsection is +2 elevation.
- Alternative should be deprecated. */
- --color-background-default: var(--brand-colors-grey-grey900);
- --color-background-section: var(--brand-colors-grey-grey800);
- --color-background-subsection: var(--brand-colors-grey-grey700);
- --color-background-alternative: var(--brand-colors-grey-grey1000);
-
- /* Applied to interactive elements, such as buttons.
- For dark mode, we apply pure white with 4% opacity so these
- tokens inherit the background hue of 264.5. */
- --color-background-muted: #ffffff0a;
- --color-background-muted-hover: #ffffff14;
- --color-background-muted-pressed: #ffffff1f;
-
- /* Ensures visual consistency with section and subsection. */
- --color-background-default-hover: var(--brand-colors-grey-grey800);
- --color-background-default-pressed: var(--brand-colors-grey-grey700);
-
- /* Hover state surface for background/alternative (#0d0d0e) */
- --color-background-alternative-hover: #0d0d0e;
- --color-background-alternative-pressed: #161617;
-
- /* These have opacities of pure white for general usage.
- We set 8% for hover and 12% for pressed so these tokens pick up
- background hues and are consistent with +1 and +2 elevations.*/
- --color-background-hover: #ffffff0a;
- --color-background-pressed: #ffffff1f;
-
- /* These are our content colors.
- Contrast ratio of alternative: 7.4 on default, 8.5 on section.
- Contrast ratio of muted: 2.0 on default, 1.8 on section.*/
- --color-text-default: var(--brand-colors-grey-grey000);
- --color-text-alternative: var(--brand-colors-grey-grey300);
- --color-text-muted: var(--brand-colors-grey-grey600);
-
- --color-icon-default: var(--brand-colors-grey-grey000);
- --color-icon-default-hover: #f0f0f0;
- --color-icon-default-pressed: #d0d0d0;
-
- --color-icon-alternative: var(--brand-colors-grey-grey300);
- --color-icon-muted: var(--brand-colors-grey-grey600);
- --color-icon-inverse: var(--brand-colors-grey-grey900);
-
- /* Contrast of border-default: 3:3 on bg-default, 3.0 on section.
- We use 8% opacify of pure white for border-muted so it maintains
- sufficient contrast on bg-default and bg-section.*/
- --color-border-default: var(--brand-colors-grey-grey500);
- --color-border-muted: #ffffff14;
-
- /* Derived from the same hue as bg-default, 264.5, for visual
- consistency. Ensures we don't have too much "red".
- Opacities are 72% and 84% for default and alternative. */
- --color-overlay-default: #030304b8;
- --color-overlay-alternative: #030304d6;
- --color-overlay-inverse: var(--brand-colors-grey-grey000);
-
- /* For primary semantic elements: interactive, active, selected (#8b99ff) */
- --color-primary-default: var(--brand-colors-blue-blue300);
- /* Stronger color for primary semantic elements (#adb6fe) */
- --color-primary-alternative: var(--brand-colors-blue-blue200);
- /* Muted color for primary semantic elements (#8b99ff26) */
- --color-primary-muted: #8b99ff26;
- /* For elements placed on top of primary/default (#121314) */
- --color-primary-inverse: var(--brand-colors-grey-grey900);
- /* Hover state surface for primary/default (#9eaaff) */
- --color-primary-default-hover: #9eaaff;
- /* Pressed state surface for primary/default (#c7ceff) */
- --color-primary-default-pressed: #c7ceff;
- /* Hover state surface for primary/muted (#8b99ff33) */
- --color-primary-muted-hover: #8b99ff33;
- /* Pressed state surface for primary/muted (#8b99ff40) */
- --color-primary-muted-pressed: #8b99ff40;
- /* For danger semantic elements: error, critical, destructive (#ff7584) */
- --color-error-default: var(--brand-colors-red-red300);
- /* Stronger color for error semantic (#ffa1aa) */
- --color-error-alternative: var(--brand-colors-red-red200);
- /* Muted color for error semantic (#ff758426) */
- --color-error-muted: #ff758426;
- /* For elements placed on top of error/default (#121314) */
- --color-error-inverse: var(--brand-colors-grey-grey900);
- /* Hover state surface for error/default (#ff8a96) */
- --color-error-default-hover: #ff8a96;
- /* Pressed state surface for error/default (#ffb2bb) */
- --color-error-default-pressed: #ffb2bb;
- /* Hover state surface for error/muted (#ff758433) */
- --color-error-muted-hover: #ff758433;
- /* Pressed state surface for error/muted (#ff758440) */
- --color-error-muted-pressed: #ff758440;
- /* For warning semantic elements: caution, attention, precaution (#f0b034) */
- --color-warning-default: var(--brand-colors-yellow-yellow200);
- /* Muted color option for warning semantic (#f0b03426) */
- --color-warning-muted: #f0b03426;
- /* For elements placed on top of warning/default (#121314) */
- --color-warning-inverse: var(--brand-colors-grey-grey900);
- /* Hover state surface for warning/default (#f3be59) */
- --color-warning-default-hover: #f3be59;
- /* Pressed state surface for warning/default (#f6cd7f) */
- --color-warning-default-pressed: #f6cd7f;
- /* Hover state surface for warning/muted (#f0b03433) */
- --color-warning-muted-hover: #f0b03433;
- /* Pressed state surface for warning/muted (#f0b03440) */
- --color-warning-muted-pressed: #f0b03440;
- /* For positive semantic elements: success, confirm, complete, safe (#baf24a) */
- --color-success-default: var(--brand-colors-lime-lime100);
- /* Muted color for positive semantic (#baf24a26) */
- --color-success-muted: #baf24a26;
- /* For elements placed on top of success/default (#121314) */
- --color-success-inverse: var(--brand-colors-grey-grey900);
- /* Hover state surface for success/default (#c9f570) */
- --color-success-default-hover: #c9f570;
- /* Pressed state surface for success/default (#d7f796) */
- --color-success-default-pressed: #d7f796;
- /* Hover state surface for success/muted (#baf24a33) */
- --color-success-muted-hover: #baf24a33;
- /* Pressed state surface for success/muted (#baf24a40) */
- --color-success-muted-pressed: #baf24a40;
- /* For informational read-only elements: info, reminder, hint (#8b99ff) */
- --color-info-default: var(--brand-colors-blue-blue300);
- /* Muted color for informational semantic (#8b99ff26) */
- --color-info-muted: #8b99ff26;
- /* For elements placed on top of info/default (#121314) */
- --color-info-inverse: var(--brand-colors-grey-grey900);
- /* Expressive color in light orange (#ffa680) */
- --color-accent01-light: var(--brand-colors-orange-orange200);
- /* Expressive color in orange (#ff5c16) */
- --color-accent01-normal: var(--brand-colors-orange-orange400);
- /* Expressive color in dark orange (#661800) */
- --color-accent01-dark: var(--brand-colors-orange-orange700);
- /* Expressive color in light purple (#eac2ff) */
- --color-accent02-light: var(--brand-colors-purple-purple100);
- /* Expressive color in purple (#d075ff) */
- --color-accent02-normal: var(--brand-colors-purple-purple300);
- /* Expressive color in dark purple (#3d065f) */
- --color-accent02-dark: var(--brand-colors-purple-purple800);
- /* Expressive color in light lime (#e5ffc3) */
- --color-accent03-light: var(--brand-colors-lime-lime050);
- /* Expressive color in lime (#baf24a) */
- --color-accent03-normal: var(--brand-colors-lime-lime100);
- /* Expressive color in dark lime (#013330) */
- --color-accent03-dark: var(--brand-colors-lime-lime700);
- /* Expressive color in light indigo (#cce7ff) */
- --color-accent04-light: var(--brand-colors-indigo-indigo100);
- /* Expressive color in indigo (#89b0ff) */
- --color-accent04-normal: var(--brand-colors-indigo-indigo200);
- /* Expressive color in dark indigo (#190066) */
- --color-accent04-dark: var(--brand-colors-indigo-indigo800);
- /* For Flask primary accent color (#d27dff) */
- --color-flask-default: var(--brand-colors-purple-purple300);
- /* For elements placed on top of flask/default (#121314) */
- --color-flask-inverse: var(--brand-colors-grey-grey900);
- /* For neutral drop shadow color (black-40%) */
- --color-shadow-default: #00000066;
- /* For primary drop shadow color (#8b99ff33) */
- --color-shadow-primary: #8b99ff33;
- /* For critical/danger drop shadow color (#ff758433) */
- --color-shadow-error: #ff758433;
-}
-
/* Color Shortcut Utilities - Enable shorter class names */
/* Text shortcuts: text-default instead of text-text-default */
@utility text-default {
📖 Storybook Preview |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Removing
outlineclass breaks focus visibility for v3 consumers- Re-added
focus-visible:outlineto Primary, Secondary, and Tertiary buttons to restore focus outline style in Tailwind v3 while remaining harmless in v4.
- Re-added
Or push these changes by commenting:
@cursor push f7c20b63fa
Preview (f7c20b63fa)
diff --git a/packages/design-system-react/src/components/Button/variants/ButtonPrimary/ButtonPrimary.tsx b/packages/design-system-react/src/components/Button/variants/ButtonPrimary/ButtonPrimary.tsx
--- a/packages/design-system-react/src/components/Button/variants/ButtonPrimary/ButtonPrimary.tsx
+++ b/packages/design-system-react/src/components/Button/variants/ButtonPrimary/ButtonPrimary.tsx
@@ -59,6 +59,7 @@
isDanger && ['hover:bg-default-hover', 'active:bg-default-pressed'],
],
'focus-visible:ring-0',
+ 'focus-visible:outline',
isInverse
? 'focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-background-default'
: 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-default',
diff --git a/packages/design-system-react/src/components/Button/variants/ButtonSecondary/ButtonSecondary.tsx b/packages/design-system-react/src/components/Button/variants/ButtonSecondary/ButtonSecondary.tsx
--- a/packages/design-system-react/src/components/Button/variants/ButtonSecondary/ButtonSecondary.tsx
+++ b/packages/design-system-react/src/components/Button/variants/ButtonSecondary/ButtonSecondary.tsx
@@ -55,6 +55,7 @@
isDanger && ['hover:bg-default-hover', 'active:bg-default-pressed'],
],
'focus-visible:ring-0',
+ 'focus-visible:outline',
isInverse
? 'focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-background-default'
: 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-default',
diff --git a/packages/design-system-react/src/components/Button/variants/ButtonTertiary/ButtonTertiary.tsx b/packages/design-system-react/src/components/Button/variants/ButtonTertiary/ButtonTertiary.tsx
--- a/packages/design-system-react/src/components/Button/variants/ButtonTertiary/ButtonTertiary.tsx
+++ b/packages/design-system-react/src/components/Button/variants/ButtonTertiary/ButtonTertiary.tsx
@@ -57,6 +57,7 @@
],
],
'focus-visible:ring-0',
+ 'focus-visible:outline',
isInverse
? 'focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-background-default'
: 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-default',
📖 Storybook Preview |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: tailwind-merge v3 breaks Tailwind v3 consumer compatibility
- Reverted packages/design-system-react to use tailwind-merge ^2.x and updated yarn.lock to restore Tailwind v3 compatibility.
Or push these changes by commenting:
@cursor push 52feed80ab
Preview (52feed80ab)
diff --git a/packages/design-system-react/package.json b/packages/design-system-react/package.json
--- a/packages/design-system-react/package.json
+++ b/packages/design-system-react/package.json
@@ -54,7 +54,7 @@
"@metamask/jazzicon": "^2.0.0",
"@radix-ui/react-slot": "^1.1.0",
"blo": "^2.0.0",
- "tailwind-merge": "^3.0.0"
+ "tailwind-merge": "^2.0.0"
},
"devDependencies": {
"@figma/code-connect": "^1.0.0",
diff --git a/yarn.lock b/yarn.lock
--- a/yarn.lock
+++ b/yarn.lock
@@ -3488,7 +3488,7 @@
jest: "npm:^29.7.0"
jest-environment-jsdom: "npm:^29.7.0"
rimraf: "npm:^5.0.5"
- tailwind-merge: "npm:^3.0.0"
+ tailwind-merge: "npm:^2.0.0"
ts-jest: "npm:^29.2.5"
tsx: "npm:^4.20.6"
typescript: "npm:~5.2.2"
@@ -20673,10 +20673,10 @@
languageName: node
linkType: hard
-"tailwind-merge@npm:^3.0.0":
- version: 3.5.0
- resolution: "tailwind-merge@npm:3.5.0"
- checksum: 10/888861e2fc685dc282e6c0d735a2ca7656c3f13da6947896401f50aea924e481ab6dc7629b6f6a28125505f1b06ee97381ad792849204f34ee1ede375b119e0c
+"tailwind-merge@npm:^2.0.0":
+ version: 2.6.1
+ resolution: "tailwind-merge@npm:2.6.1"
+ checksum: 10/b68e9e63f0d8e4f8e8b9801b978c2d6c78136a20e407586b3bf4c905759ccbfa4ba9d0b6af06b0241816578866cb3dce660078fc29847a8a1dfabb87d315b80b
languageName: node
linkType: hard|
@metamaskbot publish-preview |
|
Preview builds have been published. See these instructions for more information about preview builds. Expand for full list of packages and versions. |
|
@SocketSecurity ignore npm/@tailwindcss/oxide@4.1.14 @SocketSecurity ignore npm/@emnapi/core@1.5.0 @SocketSecurity ignore npm/postcss-import@16.1.1 Tailwind v4 core packages — first-party WASM runtime dependencies — transitive deps of ESLint plugin dependencies — transitive deps of |
📖 Storybook Preview |
📖 Storybook Preview |
- Make design-system-tailwind-preset an optional peer dep in design-system-react via peerDependenciesMeta, signaling that v4 consumers can use design-tokens/tailwind/theme.css instead - Add tailwindcss v3 as devDep to tailwind-preset for build isolation from hoisted v4 Co-authored-by: George Marshall <georgewrmarshall@users.noreply.github.com>
…able overrides Tailwind v4's @theme inline inlines color values directly into utility classes, making them ignore .dark scope CSS variable overrides. Switching to @theme generates proper CSS custom properties on :root that the .dark block can override, fixing color contrast violations in dark mode for Button and TextButton components. Also removes the redundant styles.css import from storybook preview since theme.css is self-contained. Co-authored-by: George Marshall <georgewrmarshall@users.noreply.github.com>
…ctness rules - Replace cleancss with cp for theme.css build to prevent corruption of @theme and @Utility directives that cleancss doesn't understand - Enable better-tailwindcss correctness preset (no-conflicting-classes, no-unregistered-classes) for web packages to restore class validation lost during plugin migration Co-authored-by: George Marshall <georgewrmarshall@users.noreply.github.com>
- Load eslint-plugin-tailwindcss via nativeRequire from storybook-react-native context where tailwindcss v3 is available, fixing 'resolveConfig' not found error from v4 - Remove redundant root eslint-plugin-tailwindcss dep since it needs v3 and root has v4 - Add TODO comments for focus-visible:outline redundancy when dropping v3 support Co-authored-by: George Marshall <georgewrmarshall@users.noreply.github.com>
CI only built the v3 tailwind-preset before lint, but the RN eslint config also needs the twrnc-preset built to resolve custom class names via tailwind-intellisense.config.js. Also restores eslint-plugin-tailwindcss loading via nativeRequire from storybook-react-native context where tailwindcss v3 is available. Co-authored-by: George Marshall <georgewrmarshall@users.noreply.github.com>
… selector - Add --font-size-*: initial and --font-weight-*: initial to @theme block to disable default Tailwind utilities (text-xs, font-thin, etc.), enforcing design system typography - Add explicit [data-theme='light'], .light selector block so light theme variables reset correctly when nested inside .dark ancestors for runtime theme switching Note: TextButton 'As Child' story has a pre-existing dark mode a11y contrast issue (#8b99ff link vs #ffffff surrounding text at 2.59:1) now surfaced by the @theme dark mode fix. Co-authored-by: George Marshall <georgewrmarshall@users.noreply.github.com>
The twrnc-preset imports types from design-tokens, so the full build chain for lint is: design-tokens → tailwind-preset + twrnc-preset → lint. Co-authored-by: George Marshall <georgewrmarshall@users.noreply.github.com>
…me.css Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Bump tailwind-merge from ^2.0.0 to ^3.0.0 for Tailwind CSS v4 support. Remove focus-visible:outline-none and focus-visible:outline from button variants — in v4, outline-2 implies outline-style: solid, making both redundant. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Consumers now use @metamask/design-tokens/tailwind/theme.css for Tailwind v4 instead of the v3 preset. Also removes unused devDep from storybook-react. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Adds migration documentation to design-tokens (full setup guide for theme.css) and design-system-react (breaking changes for consumers: preset peer dep removal, tailwind-merge v3, focus outline v4 syntax). Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
theme.css uses relative @import paths to brand-colors.css, typography.css, and shadow.css. The build now copies these to dist/css/ alongside dist/tailwind/theme.css so consumers installing from npm can resolve them. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Replaces the copy-based approach with postcss-import to inline brand-colors.css, typography.css, and shadow.css into dist/tailwind/theme.css. Consumers get a single self-contained file with zero relative @import dependencies. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
import.meta.dirname is Node 20.11+ only, but engines allows ^18.18. On Node 18 it's undefined, causing path.resolve(undefined, ...) to throw and break all linting. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Tailwind v4 minifies hex values (#ffffff → #fff). The YIQ contrast function only handled 6/8-char hex, producing NaN for 3/4-char — white text was rendered on white backgrounds. Now expands shorthand hex before parsing. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Tailwind v4's shadow parser cannot process var() references in shadow values, causing custom @theme shadow definitions to be silently ignored. Bake the design token color (#0000001a) directly into shadow values so Tailwind generates correct utilities with --tw-shadow-color composition. - Replace var(--shadow-color, ...) with #0000001a in @theme shadow values - Remove unused @Utility shadow-default/primary/error (0 usage in design-system-react and extension, confirmed by codebase audit) - Remove !important that was on the dead @Utility directives - Remove unused Color story and shadow-primary/error story examples - Update tests to match new shadow value format
In Tailwind v4, the CSS-first @theme block needs --font-sans set explicitly for preflight to apply the correct font-family to <html>. Without this, elements not wrapped in a <Text> component (e.g. asChild ButtonBase) fall back to Tailwind's default system font stack instead of Geist.
- Reinstate focus-visible:outline-none, outline, and outline-2 strings on Button variants and ButtonHero (pre-a72c63ac parity) - Assert merged focus classes in tests (tailwind-merge v3 omits redundant outline) - Disable better-tailwindcss/no-conflicting-classes with scoped comments - Drop premature MIGRATION.md note about removing outline utilities Made-with: Cursor
- Restore outline-none before ring-0 with sort-classes disable (prior order, limit release diff) - Reword no-conflicting-classes disables: release diff, not Chromatic Made-with: Cursor
- Allowlist omitted v3-only shadow-default/primary/error utilities in parity script - Document shadow tokens in MIGRATION.md (--shadow-xs–lg, --color-shadow-*) - Add tsx devDependency for check:tailwind-theme-parity script Made-with: Cursor
- Restore Color story with TextColor on inverse backgrounds - Replace removed shadow-primary/error utilities with shadow-[var(--shadow-size-*)_var(--color-shadow-*)] Made-with: Cursor
ec53cd8 to
4b50dc2
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 3 total unresolved issues (including 2 from previous reviews).
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: twMerge config missing new typography class names
- Updated twMerge’s variantClassGroups to include the new simplified typography tokens (e.g., display-lg, body-md) so text-* classes deduplicate correctly while keeping legacy tokens.
Or push these changes by commenting:
@cursor push 389f68f56b
Preview (389f68f56b)
diff --git a/packages/design-system-react/src/utils/tw-merge.ts b/packages/design-system-react/src/utils/tw-merge.ts
--- a/packages/design-system-react/src/utils/tw-merge.ts
+++ b/packages/design-system-react/src/utils/tw-merge.ts
@@ -1,6 +1,9 @@
import { extendTailwindMerge } from 'tailwind-merge';
+// Register both legacy (s-*/l-*) and new simplified typography tokens so
+// twMerge can properly resolve conflicts regardless of which format is used.
const variantClassGroups = [
+ // Legacy tokens (kept for backward compatibility)
's-display-lg',
's-display-md',
's-heading-lg',
@@ -29,6 +32,21 @@
'l-button-label-md',
'l-button-label-lg',
'l-amount-display-lg',
+ // New simplified tokens (current Text variant class names)
+ 'display-lg',
+ 'display-md',
+ 'heading-lg',
+ 'heading-md',
+ 'heading-sm',
+ 'body-lg',
+ 'body-md',
+ 'body-sm',
+ 'body-xs',
+ 'page-heading',
+ 'section-heading',
+ 'button-label-md',
+ 'button-label-lg',
+ 'amount-display-lg',
];
/**You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit 4b50dc2. Configure here.
| [TextVariant.SectionHeading]: 'text-section-heading', | ||
| [TextVariant.ButtonLabelMd]: 'text-button-label-md', | ||
| [TextVariant.ButtonLabelLg]: 'text-button-label-lg', | ||
| [TextVariant.AmountDisplayLg]: 'text-amount-display-lg', |
There was a problem hiding this comment.
twMerge config missing new typography class names
High Severity
TWCLASSMAP_TEXT_VARIANT_FONTSTYLE was updated to emit simplified class names like text-display-lg, text-body-md, etc., but variantClassGroups in tw-merge.ts still only contains the old prefixed names (s-display-lg, l-display-lg, etc.). twMerge therefore cannot recognize the new typography classes as belonging to the font-size group, so conflicting typography classes won't be deduplicated when consumers override via className.
Reviewed by Cursor Bugbot for commit 4b50dc2. Configure here.



Description
This PR migrates the design system to support Tailwind CSS v4 while maintaining backwards compatibility with v3. The migration adopts a dual-version strategy to accommodate different platform requirements:
Reason for Change
Tailwind CSS v4 introduces a new architecture with improved performance, better CSS-first configuration, and modern tooling. However, React Native packages (TWRNC) are not yet compatible with v4, requiring a dual-version approach.
Solution
Platform-Specific Tailwind Strategy:
React Web (
design-system-react):design-system-tailwind-preset(existing configuration)theme.cssindesign-tokenspackageReact Native (
design-system-react-native):Key Changes:
New Tailwind v4 Theme System:
packages/design-tokens/src/tailwind/theme.csswith design token integration@import 'tailwindcss',@source, and@themedirectivesStorybook Configuration:
apps/storybook-react) migrated to v4@tailwindcss/viteand@tailwindcss/postcsspluginsapps/storybook-react/tailwind.cssTypography Updates:
text-<variant>(e.g.,text-heading-lg)twMergeconfiguration for proper class mergingESLint Configuration:
eslint-plugin-better-tailwindcssfor v4 compatibilityeslint-plugin-tailwindcsswith relaxedno-custom-classnameMonorepo Tooling:
workspaceslevelBuild & Test Infrastructure:
@types/reactdependencies to resolve TypeScript compilation issuesdesign-system-twrnc-presettsconfig (jsx:reactinstead ofreact-native)Related issues
Fixes: (Add issue number if applicable)
Manual testing steps
Verify Web Storybook (Tailwind v4):
Verify React Native Storybook (Tailwind v3):
yarn storybook:ios # or yarn storybook:androidRun Parity Check:
Build All Packages:
Run Linting:
Screenshots/Recordings
N/A - This is a build tooling and configuration change with no visual differences.
Pre-merge author checklist
Pre-merge reviewer checklist
Technical Details
Package Changes
design-tokens:
theme.csswith Tailwind v4 theme configurationdesign-system-react:
better-tailwindcssfor v4design-system-react-native:
tailwindcsswith relaxed custom classesdesign-system-tailwind-preset:
design-system-twrnc-preset:
storybook-react:
Dependency Management
@types/reactto multiple packages for TypeScript compilationtwrnc/tailwindcssto enforce v3Known Limitations
Note
Medium Risk
Medium risk due to broad tooling and dependency updates (Tailwind v4, new lint rules, build pipeline changes) that can affect styling output and CI across multiple packages, while also maintaining a dual v3/v4 setup for web vs React Native.
Overview
Adds Tailwind v4 support for web consumers by introducing
design-tokens/src/tailwind/theme.css(built intodist/tailwind/theme.css) and wiringapps/storybook-reactto use the CSS-first v4 flow via@tailwindcss/viteand@tailwindcss/postcss.Adds safety rails for the migration: a new
check:tailwind-theme-parityscript indesign-tokens(also exposed at repo root/Storybook) to compare v4theme.cssoutput against the v3 preset’s expected custom utilities, plus documentation (docs/tailwind-v4-pr-split-plan.md) outlining how to split the migration.Updates repo tooling for dual Tailwind versions: switches web Tailwind linting to
eslint-plugin-better-tailwindcss(entrypointapps/storybook-react/tailwind.css) while keeping RN oneslint-plugin-tailwindcsswith an absolute config path; CI lint setup now buildsdesign-tokens, the v3 preset, and thetwrncpreset; Yarn constraints allow inconsistenttailwindcssranges and adds atwrnc/tailwindcssv3 resolution.Adjusts component code/docs for new class semantics: bumps
tailwind-mergeto v3 and updates button focus-outline class expectations/order, simplifiesTextvariant class mappings totext-<variant>, updates shadow stories to use CSS variable-based shadows, and refreshes a few Storybook/examples/docs to match the new styling contracts.Reviewed by Cursor Bugbot for commit 4b50dc2. Bugbot is set up for automated code reviews on this repo. Configure here.