Skip to content

Tailwind v4#842

Draft
georgewrmarshall wants to merge 43 commits intomainfrom
tailwind-v4
Draft

Tailwind v4#842
georgewrmarshall wants to merge 43 commits intomainfrom
tailwind-v4

Conversation

@georgewrmarshall
Copy link
Copy Markdown
Contributor

@georgewrmarshall georgewrmarshall commented Oct 20, 2025

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):

    • Supports both Tailwind v3 and v4
    • v3 support via design-system-tailwind-preset (existing configuration)
    • v4 support via new theme.css in design-tokens package
    • Web Storybook migrated to v4 using CSS-first approach
  • React Native (design-system-react-native):

    • Tailwind v3 only (via TWRNC preset)
    • TWRNC library not yet compatible with v4
    • Maintains existing v3 workflow unchanged

Key Changes:

  1. New Tailwind v4 Theme System:

    • Created packages/design-tokens/src/tailwind/theme.css with design token integration
    • CSS-first configuration using @import 'tailwindcss', @source, and @theme directives
    • Automated parity check script validates v3/v4 class coverage
  2. Storybook Configuration:

    • Web Storybook (apps/storybook-react) migrated to v4
    • Uses @tailwindcss/vite and @tailwindcss/postcss plugins
    • New CSS entry point: apps/storybook-react/tailwind.css
  3. Typography Updates:

    • Simplified class names: text-<variant> (e.g., text-heading-lg)
    • Updated twMerge configuration for proper class merging
    • Comprehensive test updates for new class patterns
  4. ESLint Configuration:

    • Web packages: eslint-plugin-better-tailwindcss for v4 compatibility
    • React Native packages: eslint-plugin-tailwindcss with relaxed no-custom-classname
    • Platform-specific VSCode settings for appropriate Tailwind IntelliSense
  5. Monorepo Tooling:

    • Yarn hoisting limits set to workspaces level
    • Depcheck configuration allows Tailwind version inconsistencies across workspaces
    • React Native packages pinned to Tailwind v3 via resolutions
  6. Build & Test Infrastructure:

    • Added @types/react dependencies to resolve TypeScript compilation issues
    • Fixed design-system-twrnc-preset tsconfig (jsx: react instead of react-native)
    • Jest setup mocks for React Native platform constants
    • Disabled Watchman in package-level Jest configs

Related issues

Fixes: (Add issue number if applicable)

Manual testing steps

  1. Verify Web Storybook (Tailwind v4):

    yarn storybook
    • Verify all components render correctly
    • Check responsive utilities work
    • Verify design token classes apply properly
  2. Verify React Native Storybook (Tailwind v3):

    yarn storybook:ios
    # or
    yarn storybook:android
    • Verify mobile components render correctly
    • Check TWRNC styling works as expected
  3. Run Parity Check:

    yarn check:tailwind-theme-parity
    • Should show v3/v4 class coverage comparison
    • Verify no critical class losses
  4. Build All Packages:

    yarn build
    • Should complete without TypeScript errors
  5. Run Linting:

    yarn lint
    • Should pass with platform-specific Tailwind rules

Screenshots/Recordings

N/A - This is a build tooling and configuration change with no visual differences.

Pre-merge author checklist

  • I've followed MetaMask Contributor Docs
  • I've completed the PR template to the best of my ability
  • I've included tests if applicable
  • I've documented my code using JSDoc format if applicable
  • I've applied the right labels on the PR (see labeling guidelines). Not required for external contributors.

Pre-merge reviewer checklist

  • I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed).
  • I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.

Technical Details

Package Changes

design-tokens:

  • New theme.css with Tailwind v4 theme configuration
  • Exports CSS file for consumption by web packages

design-system-react:

  • Updated to support both Tailwind v3 (preset) and v4 (theme.css)
  • Typography class simplification
  • ESLint plugin: better-tailwindcss for v4

design-system-react-native:

  • Remains on Tailwind v3 exclusively
  • ESLint plugin: tailwindcss with relaxed custom classes
  • Jest setup mocks for React Native platform

design-system-tailwind-preset:

  • Maintains v3 compatibility layer
  • Continues to work for consumers not yet migrated to v4

design-system-twrnc-preset:

  • Pinned to Tailwind v3 (TWRNC requirement)
  • TypeScript config fix (jsx: react)

storybook-react:

  • Migrated to Tailwind v4 with Vite/PostCSS plugins
  • New CSS entry point for theme

Dependency Management

  • Added @types/react to multiple packages for TypeScript compilation
  • Yarn resolution for twrnc/tailwindcss to enforce v3
  • Depcheck rules to allow intentional version mismatches

Known Limitations

  • React Native packages cannot upgrade to v4 until TWRNC supports it
  • Dual-version strategy adds slight complexity to dependency management
  • Some test failures in React Native are pre-existing (unrelated to this PR)

Note

Medium Risk
Medium risk because it upgrades Tailwind and related tooling across the monorepo (including ESLint, PostCSS/Vite, and class merging), which can cause widespread styling/build/lint regressions despite limited runtime logic changes.

Overview
Introduces a Tailwind v4, CSS-first theming surface by adding packages/design-tokens/src/tailwind/theme.css, exporting it as @metamask/design-tokens/tailwind/theme.css, and adding build + parity/test tooling (check-tailwind-theme-parity) to validate v3 preset vs v4 class coverage.

Migrates apps/storybook-react to Tailwind v4 by removing tailwind.config.js, switching tailwind.css to @import 'tailwindcss' + @source scanning + theme import, and updating Vite/PostCSS to @tailwindcss/vite and @tailwindcss/postcss (plus Chromatic config). Web Tailwind linting switches to eslint-plugin-better-tailwindcss with the new CSS entrypoint.

Keeps React Native on Tailwind v3 by adding explicit v3 tailwindcss deps/resolutions, ensuring CI builds the twrnc preset for class resolution, and adjusting repo tooling to allow mixed Tailwind versions (yarn.config.cjs, .depcheckrc.yml). Component/tests/stories are minimally tweaked for Tailwind v4/tailwind-merge v3 behavior (e.g., focus outline class expectations and shadow utilities).

Written by Cursor Bugbot for commit 22624d1. This will update automatically on new commits. Configure here.

@github-actions
Copy link
Copy Markdown
Contributor

📖 Storybook Preview

@socket-security
Copy link
Copy Markdown

socket-security Bot commented Oct 20, 2025

@socket-security
Copy link
Copy Markdown

socket-security Bot commented Oct 20, 2025

Warning

MetaMask internal reviewing guidelines:

  • Do not ignore-all
  • Each alert has instructions on how to review if you don't know what it means. If lost, ask your Security Liaison or the supply-chain group
  • Copy-paste ignore lines for specific packages or a group of one kind with a note on what research you did to deem it safe.
    @SocketSecurity ignore npm/PACKAGE@VERSION
Action Severity Alert  (click "▶" to expand/collapse)
Warn Low
Potential code anomaly (AI signal): npm yaml is 100.0% likely to have a medium risk anomaly

Notes: The analyzed code is a standard YAML stringify module with robust tag resolution, anchor handling, and formatting controls. It correctly delegates to appropriate stringify logic and handles edge cases like circular aliases and unresolved tags with explicit errors. Overall security posture is conservative and typical for a serialization library; no malicious activity detected.

Confidence: 1.00

Severity: 0.60

From: ?npm/@metamask/create-release-branch@4.1.4npm/react-native@0.72.17npm/postcss-cli@11.0.1npm/yaml@2.8.2

ℹ Read more on: This package | This alert | What is an AI-detected potential code anomaly?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: An AI system found a low-risk anomaly in this package. It may still be fine to use, but you should check that it is safe before proceeding.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/yaml@2.8.2. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Ignoring alerts on:

  • @eslint/css-tree@3.6.6
  • @tailwindcss/oxide@4.1.14
  • @tailwindcss/oxide-wasm32-wasi@4.1.14
  • @emnapi/core@1.5.0
  • @emnapi/runtime@1.5.0
  • @tybys/wasm-util@0.10.1
  • synckit@0.11.11
  • postcss-import@16.1.1
  • jiti@2.6.1

View full report

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 6, 2026

📖 Storybook Preview

Copy link
Copy Markdown
Contributor

@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 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.

Create PR

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);
+  }
+}

Comment thread packages/design-system-react/src/components/Text/Text.constants.ts Outdated
Comment thread packages/design-system-react/src/utils/tw-merge.test.ts
Copy link
Copy Markdown
Contributor Author

@georgewrmarshall georgewrmarshall left a comment

Choose a reason for hiding this comment

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

Left some comments. I'm not sure we need to change anything in the design-system-tailwind-preset package

Comment thread apps/storybook-react/scripts/check-tailwind-theme-parity.mjs Outdated
Comment thread apps/storybook-react/package.json Outdated
Comment thread packages/design-system-react-native/jest.setup.js Outdated
Comment thread packages/design-system-react/src/components/AvatarBase/AvatarBase.test.tsx Outdated
Comment thread packages/design-system-twrnc-preset/tsconfig.build.json Outdated
Comment thread .yarnrc.yml Outdated
Comment thread eslint.config.mjs Outdated
Comment thread jest.config.packages.js Outdated
Comment thread package.json Outdated
@github-actions
Copy link
Copy Markdown
Contributor

📖 Storybook Preview

Copy link
Copy Markdown
Contributor

@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 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.

Create PR

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",

Comment thread packages/design-tokens/package.json Outdated
Comment thread eslint.config.mjs
@github-actions
Copy link
Copy Markdown
Contributor

📖 Storybook Preview

@github-actions
Copy link
Copy Markdown
Contributor

📖 Storybook Preview

Copy link
Copy Markdown
Contributor

@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 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.

Create PR

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;

Comment thread eslint.config.mjs Outdated
Comment thread packages/design-tokens/src/tailwind/theme.css
@github-actions
Copy link
Copy Markdown
Contributor

📖 Storybook Preview

Copy link
Copy Markdown
Contributor

@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 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 .light CSS selector breaks theme switching
    • Added explicit [data-theme='light'], .light block in theme.css reapplying light theme variables so .light can override .dark in nested contexts.

Create PR

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 {

Comment thread packages/design-tokens/src/tailwind/theme.css
@github-actions
Copy link
Copy Markdown
Contributor

📖 Storybook Preview

@github-actions
Copy link
Copy Markdown
Contributor

📖 Storybook Preview

Comment thread eslint.config.mjs
Comment thread apps/storybook-react/.storybook/preview.tsx
@github-actions
Copy link
Copy Markdown
Contributor

📖 Storybook Preview

Copy link
Copy Markdown
Contributor

@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 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.

Create PR

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 {

Comment thread packages/design-tokens/src/tailwind/theme.css
@github-actions
Copy link
Copy Markdown
Contributor

📖 Storybook Preview

Copy link
Copy Markdown
Contributor Author

@georgewrmarshall georgewrmarshall left a comment

Choose a reason for hiding this comment

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

Left some comments

Comment thread eslint.config.mjs Outdated
Comment thread packages/design-tokens/scripts/check-tailwind-theme-parity.ts
Comment thread packages/design-tokens/src/tailwind/theme.test.ts
Comment thread .github/workflows/lint-build-test.yml
Copy link
Copy Markdown
Contributor

@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 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 outline class breaks focus visibility for v3 consumers
    • Re-added focus-visible:outline to Primary, Secondary, and Tertiary buttons to restore focus outline style in Tailwind v3 while remaining harmless in v4.

Create PR

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',

@github-actions
Copy link
Copy Markdown
Contributor

📖 Storybook Preview

Copy link
Copy Markdown
Contributor

@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 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.

Create PR

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

Comment thread packages/design-system-react/package.json
@georgewrmarshall
Copy link
Copy Markdown
Contributor Author

@metamaskbot publish-preview

@github-actions
Copy link
Copy Markdown
Contributor

Preview builds have been published. See these instructions for more information about preview builds.

Expand for full list of packages and versions.
{
  "@metamask-previews/design-system-react": "0.11.0-preview.fa05289",
  "@metamask-previews/design-system-react-native": "0.11.0-preview.fa05289",
  "@metamask-previews/design-system-shared": "0.4.0-preview.fa05289",
  "@metamask-previews/design-system-tailwind-preset": "0.6.1-preview.fa05289",
  "@metamask-previews/design-system-twrnc-preset": "0.3.0-preview.fa05289",
  "@metamask-previews/design-tokens": "8.2.2-preview.fa05289"
}

@georgewrmarshall
Copy link
Copy Markdown
Contributor Author

georgewrmarshall commented Mar 17, 2026

@SocketSecurity ignore npm/@tailwindcss/oxide@4.1.14
@SocketSecurity ignore npm/@tailwindcss/oxide-wasm32-wasi@4.1.14

@SocketSecurity ignore npm/@emnapi/core@1.5.0
@SocketSecurity ignore npm/@emnapi/runtime@1.5.0
@SocketSecurity ignore npm/@tybys/wasm-util@0.10.1

@SocketSecurity ignore npm/postcss-import@16.1.1
@SocketSecurity ignore npm/@eslint/css-tree@3.6.6
@SocketSecurity ignore npm/jiti@2.6.1
@SocketSecurity ignore npm/synckit@0.11.11

Tailwind v4 core packages — first-party @tailwindcss/* packages. The oxide compiler uses shell access and network to download platform-specific native binaries at install time (standard pattern for Rust-based tools like SWC, esbuild, Lightning CSS). The WASM fallback variant uses fetch to load WASM modules when native binaries aren't available.

WASM runtime dependencies — transitive deps of @tailwindcss/oxide-wasm32-wasi providing the N-API/WASM bridge. The globalThis.fetch usage is for loading WASM modules at runtime, not external network calls. Socket's own AI analysis confirms "no evidence of malicious behavior" for both.

ESLint plugin dependencies — transitive deps of eslint-plugin-better-tailwindcss. postcss-import publisher change: romainmenke is a long-standing PostCSS org maintainer (postcss-plugins lead). jiti is the standard unjs TypeScript JIT loader. synckit is a worker thread sync utility. @eslint/css-tree is the official ESLint CSS parser. All widely used, Socket AI confirms no malicious behavior.

@github-actions
Copy link
Copy Markdown
Contributor

📖 Storybook Preview

@github-actions
Copy link
Copy Markdown
Contributor

📖 Storybook Preview

georgewrmarshall and others added 28 commits March 19, 2026 19:43
- Restore no-custom-classname: error for React Native ESLint config
- Move @tailwindcss/postcss and @tailwindcss/vite to devDependencies
- Relocate parity check script to design-tokens/scripts/ as TypeScript
- Add TODO for twrnc parity validation across v3, v4, and React Native

Co-authored-by: George Marshall <georgewrmarshall@users.noreply.github.com>
- 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>
Static analysis test that verifies theme.css produces the same utility classes and CSS variables as the v3 design-system-tailwind-preset. Covers typography, colors, shadows, and bidirectional completeness (349 test cases).

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>
Verifies .light and .dark blocks in theme.css match source light-theme-colors.css and dark-theme-colors.css — catches silent value divergence between duplicated color tokens.

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>
Add --color-*: initial and --box-shadow-*: initial to the @theme block. This clears Tailwind v4's entire default palette (slate, gray, blue, red, etc.) and shadow scale, enforcing design-token-only colors and shadows. Without this, utilities like bg-gray-500 or shadow-lg would resolve to Tailwind defaults instead of failing.

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 uses --shadow-* (not --box-shadow-*) for shadow theme variables. Also inlines shadow size values directly since @theme can't resolve var() references to non-theme (:root) variables at build time.

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
@github-actions
Copy link
Copy Markdown
Contributor

📖 Storybook Preview

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.

1 participant