chore: improve tailwind intellisense and linting for react native#783
Conversation
| const expectedTextColor = tw`text-muted`.color; | ||
| const expectedFontSize = tw`text-body-sm`.fontSize; |
There was a problem hiding this comment.
Fixing invalid classname linting violation
| const tw = useTailwind(); | ||
| // Compute expected container style using an empty twClassName. | ||
| const computedExpectedContainer = tw`h-[16px] w-[16px] items-center justify-center rounded-full bg-icon-default`; | ||
| const computedExpectedContainer = tw`size-[16px] items-center justify-center rounded-full bg-icon-default`; |
| const box = getByTestId('box'); | ||
| const styles = flattenStyles(box.props.style); | ||
| expect(styles[0]).toStrictEqual(tw`bg-primary-default flex p-4`); | ||
| expect(styles[0]).toStrictEqual(tw`flex bg-primary-default p-4`); |
|
|
||
| return ( | ||
| <View style={[tw`${twContainerClassNames}`, style]} {...props}> | ||
| <View style={[twContainerClassNames, style]} {...props}> |
There was a problem hiding this comment.
We should try to avoid this way of defining tailwind classnames in react native because unless the classnames appear in tw tailwind intellisense and the eslint tailwind plugin won't work. We should likely refactor all of our react-native components to fix this
| const { result } = renderHook(() => useTailwind()); | ||
| const tw = result.current; | ||
| const expected = tw`items-center justify-center ${TWCLASSMAP_BUTTONICON_SIZE_DIMENSION[ButtonIconSize.Md]} text-primary-default rounded-sm bg-transparent opacity-100`; | ||
| const expected = tw`items-center justify-center ${TWCLASSMAP_BUTTONICON_SIZE_DIMENSION[ButtonIconSize.Md]} rounded-sm bg-transparent text-primary-default opacity-100`; |
| expect.arrayContaining([ | ||
| expect.objectContaining( | ||
| tw`flex h-[22px] w-[22px] items-center justify-center rounded border-2 border-error-default bg-default`, | ||
| tw`flex size-[22px] items-center justify-center rounded border-2 border-error-default bg-default`, |
| {...checkboxContainerProps} | ||
| style={[ | ||
| tw`${getCheckboxContainerStyle(pressed)} flex h-[22px] w-[22px] items-center justify-center rounded border-2`, | ||
| tw`${getCheckboxContainerStyle(pressed)} flex size-[22px] items-center justify-center rounded border-2`, |
|
|
||
| it('computes baselineOffset correctly for BodyMd', () => { | ||
| const variant = MAP_TEXTBUTTON_SIZE_TEXTVARIANT[TextButtonSize.BodyMd]; | ||
| // eslint-disable-next-line tailwindcss/no-custom-classname |
There was a problem hiding this comment.
Disabling no custom rule for this dynamic usage. We should try to avoid this if possible.
…ig for intellisense and eslint plugin
339d0f7 to
1fac47b
Compare
📖 Storybook Preview |
| @@ -1,23 +1,12 @@ | |||
| const designSystemPreset = require('@metamask/design-system-tailwind-preset'); | |||
There was a problem hiding this comment.
Refactors and streamlines the tailwind-intellisense.config.js used for IntelliSense and the ESLint Tailwind plugin in React Native Storybook stories. Instead of relying on the previous Storybook React Tailwind config, which was similar but not fully compatible, this change imports the canonical Tailwind config directly from @metamask/design-system-twrnc-preset. This ensures the config is accurate, up to date, and fully aligned with the design system used in React Native components, improving both developer experience and consistency across the project.
| @@ -26,6 +26,101 @@ or | |||
| npm install react@^18.2.0 react-native@0.72.15 twrnc@^4.5.1 | |||
| ``` | |||
|
|
|||
There was a problem hiding this comment.
Updating docs to provide instructions on setting up tailwind config for intellisense and eslint tailwind plugin
📖 Storybook Preview |
| }, | ||
| }, | ||
| // Tailwind ESLint | ||
| // Tailwind ESLint for React Web |
There was a problem hiding this comment.
Separating eslint tailwind plugin settings so we can point to each tailwind config separately
| }, | ||
| }, | ||
| }, | ||
| // Tailwind ESLint for React Native |
There was a problem hiding this comment.
Adding the same rules for react native but taking in to account twrnc and the new tailwind config
📖 Storybook Preview |
| export { useTailwind, useTheme } from './hooks'; | ||
|
|
||
| // Config generation | ||
| export { generateTailwindConfig } from './tailwind.config'; |
There was a problem hiding this comment.
We are exporting generateTailwindConfig so we can use it in the tailwind-intellisense.config.js file
📖 Storybook Preview |
📖 Storybook Preview |
📖 Storybook Preview |
| // Keep essential semantic colors, remove default palette colors. | ||
| // We want to rely on the colors provided by the design system preset |
There was a problem hiding this comment.
Small comment update
| black: '#000000', | ||
| white: '#ffffff', | ||
| black: brandColor.black, | ||
| white: brandColor.white, |
There was a problem hiding this comment.
Updating to use design token values instead of static hex values
| // Include all design system colors in base theme for IntelliSense | ||
| ...colors, | ||
| }, | ||
| // This removes all default Tailwind font sizes and weights. | ||
| // We want to rely on the design system font sizes and enforce use of the Text component | ||
| textColor: { | ||
| ...colors, | ||
| ...textColors, // e.g. text-default instead of text-text-default | ||
| }, | ||
| backgroundColor: { | ||
| ...colors, // Incorporate existing color utilities like bg-primary-default | ||
| ...backgroundColors, // e.g. bg-default instead of bg-background-default | ||
| }, | ||
| borderColor: { | ||
| ...colors, // Incorporate existing color utilities like border-primary-default | ||
| ...borderColors, // e.g. border-default instead of border-border-default | ||
| }, | ||
| fontSize: { | ||
| // Empty to remove default Tailwind font sizes (text-sm, text-lg, etc.) | ||
| // Design system font sizes added via extend | ||
| ...typographyTailwindConfig.fontSize, | ||
| }, | ||
| extend: { |
There was a problem hiding this comment.
Instead of extending the default Tailwind theme and configuration, we intentionally override it. This is because the only projects using this package. Specifically, the monorepo storybook and mobile, do not have an existing Tailwind configuration (unlike Portfolio). As a result, we can fully replace Tailwind’s default colors and typography with those provided by the design system, ensuring complete alignment with our design tokens and enforcing consistency across these projects.
| // Export Theme enum for consumers | ||
| export { Theme } from './Theme.types'; |
There was a problem hiding this comment.
Needed for taiwlind-intellisense.config.js setup
| @@ -13,7 +13,7 @@ describe('ButtonIcon', () => { | |||
| it('renders default state correctly', () => { | |||
There was a problem hiding this comment.
lint fixes
| "./tailwind.config": { | ||
| "import": { | ||
| "types": "./dist/tailwind.config.d.mts", | ||
| "default": "./dist/tailwind.config.mjs" | ||
| }, | ||
| "require": { | ||
| "types": "./dist/tailwind.config.d.cts", | ||
| "default": "./dist/tailwind.config.cjs" | ||
| } | ||
| }, |
There was a problem hiding this comment.
JSX vs Node.js: Why We Need the Separate Export
The Problem:
// Main export (packages/design-system-twrnc-preset/src/index.ts)
export { ThemeProvider } from './ThemeProvider'; // ← Contains JSX
export { useTailwind, useTheme } from './hooks'; // ← Contains JSX
export { generateTailwindConfig } from './tailwind.config'; // ← Pure JSWhen compiled, the JSX becomes:
// dist/ThemeProvider.cjs
return (<ThemeContext.Provider value={contextValue}> // ← Node.js can't parse this
{children}
</ThemeContext.Provider>);The Issue:
- React Apps: Can handle JSX syntax ✅
- Node.js environments: Cannot parse JSX syntax ❌
- ESLint running
tailwind-intellisense.config.js - Build tools loading config files
- Any server-side usage
- ESLint running
The Solution:
// package.json exports
{
".": "./dist/index.cjs", // ← Contains JSX (for React apps)
"./tailwind.config": "./dist/tailwind.config.cjs" // ← No JSX (for Node.js)
}Usage Pattern:
// ❌ This crashes in Node.js (ESLint, config files)
const { generateTailwindConfig } = require('@metamask/design-system-twrnc-preset');
// ✅ This works in Node.js
const { generateTailwindConfig } = require('@metamask/design-system-twrnc-preset/tailwind.config');
// ✅ This works in React apps
import { ThemeProvider } from '@metamask/design-system-twrnc-preset';Why This Matters:
ESLint needs to load tailwind-intellisense.config.js to validate classnames. Since ESLint runs in Node.js, it can't import from the main export that contains JSX components. The separate /tailwind.config export provides a Node.js-safe path to access only the config functions.
TL;DR: Main export has JSX for React apps, separate export has pure JS for Node.js tools like ESLint.
| fontFamily: { | ||
| ...typographyTailwindConfig.fontFamily, | ||
| }, | ||
| extend: { |
There was a problem hiding this comment.
Instead of extending the Tailwind theme and configuration, we actually want to override it. The only projects that will use this package are the monorepo Storybook and Mobile, which don't have existing Tailwind configurations like Portfolio does. So we can fully override the Tailwind colors and typography in favor of the design system–aligned values.
📖 Storybook Preview |
📖 Storybook Preview |
22561f1 to
7308553
Compare
📖 Storybook Preview |
| const expectedTextColor = tw`${TextColor.TextMuted}`.color; | ||
| const expectedTextColor = tw`text-muted`.color; | ||
| // eslint-disable-next-line tailwindcss/no-custom-classname | ||
| const expectedFontSize = tw`text-${TextVariant.BodySm}`.fontSize; |
There was a problem hiding this comment.
We should be able to change this to text-body-sm, but for some reason it passes linting locally and fails in CI. Leaving it as is for now.
That said, we should avoid constructing Tailwind classnames using partial strings or string literals. It's harder to read, harder to debug, and more error-prone. Wherever possible, we should define the full classname explicitly—this improves readability, makes debugging easier, and helps with linting and IntelliSense support.
There was a problem hiding this comment.
Pull Request Overview
Enhance React Native Tailwind tooling by introducing a static config generator, packaging updates, docs, and linting support, plus migrating existing components/tests to new shorthand classes.
- Extend
generateTailwindConfigwith essential colors and export it - Add package exports and usage docs for IntelliSense and ESLint integration
- Update ESLint config and Storybook setup for React Native, and migrate styles/tests to
size-[x]shorthand
Reviewed Changes
Copilot reviewed 15 out of 16 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| packages/design-system-twrnc-preset/src/tailwind.config.ts | Enhanced config generator by merging essential and design system colors |
| packages/design-system-twrnc-preset/src/index.ts | Exported generateTailwindConfig from main entry |
| packages/design-system-twrnc-preset/package.json | Added exports for tailwind.config for both ESM and CJS |
| packages/design-system-twrnc-preset/README.md | Added usage guide for ThemeProvider, useTailwind, and config gen |
| packages/design-system-react-native/src/components/TextButton/TextButton.tsx | Added ESLint disable for dynamic Tailwind class in code |
| packages/design-system-react-native/src/components/TextButton/TextButton.test.tsx | Added ESLint disable for dynamic Tailwind class in tests |
| packages/design-system-react-native/src/components/Checkbox/Checkbox.tsx | Switched from h-[22px] w-[22px] to size-[22px] shorthand |
| packages/design-system-react-native/src/components/Checkbox/Checkbox.test.tsx | Updated tests to reflect size-[22px] shorthand |
| packages/design-system-react-native/src/components/ButtonIcon/ButtonIcon.test.tsx | Adjusted expected class order in ButtonIcon test |
| packages/design-system-react-native/src/components/Box/Box.tsx | Simplified tw invocation and removed nested .trim() |
| packages/design-system-react-native/src/components/Box/Box.test.tsx | Updated expected class order in Box tests |
| packages/design-system-react-native/src/components/BadgeIcon/BadgeIcon.test.tsx | Migrated BadgeIcon test to use size-[16px] shorthand |
| packages/design-system-react-native/src/components/AvatarBase/AvatarBase.test.tsx | Simplified imports and updated class usage in AvatarBase test |
| eslint.config.mjs | Added dedicated Tailwind ESLint rules/settings for React Native |
| apps/storybook-react-native/tailwind-intellisense.config.js | Switched from preset to generateTailwindConfig for IntelliSense |
Comments suppressed due to low confidence (2)
packages/design-system-twrnc-preset/README.md:34
- The README references
ThemeProvider, but this package'sindex.tsdoes not exportThemeProvider. Either update the docs to match the actual exports or addThemeProviderto the package API.
import {
packages/design-system-twrnc-preset/src/tailwind.config.ts:47
- There are no direct unit tests for
generateTailwindConfig. Adding tests to validate the output structure and key properties for eachThemewould help prevent regressions.
export const generateTailwindConfig = (theme: Theme): TwConfig => {
Description
This PR significantly enhances the React Native developer experience by adding comprehensive Tailwind IntelliSense and ESLint support to the
@metamask/design-system-twrnc-presetpackage. This bridges the gap between TWRNC usage and standard Tailwind tooling, providing React Native developers with the same excellent developer experience as React Web developers.What is the reason for the change?
React Native developers using TWRNC were missing critical developer experience features:
What is the improvement/solution?
We've implemented a comprehensive Tailwind configuration system that:
tailwind.config.jsfiles compatible with standard Tailwind toolingeslint-plugin-tailwindcssfor React Native projectsred-500)tw\classnames`) and function calls (tw('classnames')`)Related issues
Fixes: #747
Manual testing steps
IntelliSense Testing:
packages/design-system-react-native/src/components/Button/Button.tsx)tw\bg-and verify IntelliSense shows design system colors likebg-default,bg-primary-default`tw\text-` and verify design system text colors appearbg-red-500appear in suggestionsESLint Validation Testing:
tw\bg-red-500` and verify ESLint flags it as invalidtw\p-4 flexsuggests reordering totw`flex p-4`)tw\h-4 w-4suggeststw`size-4`)Build and Test:
yarn workspace @metamask/design-system-react-native test- should passyarn lint- should pass with only expected template literal warningsyarn workspace @metamask/design-system-twrnc-preset build- should build successfullyScreenshots/Recordings
Before
.trim()After
twtemplate literal usage for better tooling supportintellisense720.mov
Mobile storybook and components still render as expected
after720.mov