diff --git a/packages/docs/content/docs/llm-resources.mdx b/packages/docs/content/docs/llm-resources.mdx index ff9539ed5..de9d03295 100644 --- a/packages/docs/content/docs/llm-resources.mdx +++ b/packages/docs/content/docs/llm-resources.mdx @@ -14,9 +14,18 @@ Steps to install or integrate StyleX across Vite, Next.js, Webpack, Rspack, and - ## Style authoring guide Context on defining and using styles, StyleX APIs, and common antipatterns. + +## Migration guides + +Guides for LLM agents to migrate codebases from existing styling systems to +StyleX while preserving behavior and clearly reporting anything that cannot be +converted. + +### CSS Modules → StyleX + + diff --git a/packages/docs/src/components/mdx/LLMFiles.tsx b/packages/docs/src/components/mdx/LLMFiles.tsx index e620071e3..5ae4400d0 100644 --- a/packages/docs/src/components/mdx/LLMFiles.tsx +++ b/packages/docs/src/components/mdx/LLMFiles.tsx @@ -35,3 +35,14 @@ export function LLMStylingFile() { /> ); } + +export function LLMMigrationFile() { + const content = getFileContent('stylex-migration-css-modules.md'); + return ( + + ); +} diff --git a/packages/docs/src/components/mdx/index.tsx b/packages/docs/src/components/mdx/index.tsx index 7ebd9ff87..c76f755d1 100644 --- a/packages/docs/src/components/mdx/index.tsx +++ b/packages/docs/src/components/mdx/index.tsx @@ -25,7 +25,11 @@ import { import { Li, Ol, P, Ul } from './core'; import Image from './Image'; import { CodeBlock, Pre } from './CodeBlock'; -import { LLMInstallationFile, LLMStylingFile } from './LLMFiles'; +import { + LLMInstallationFile, + LLMMigrationFile, + LLMStylingFile, +} from './LLMFiles'; import { preMarker } from './mdx.stylex'; import { vars } from '@/theming/vars.stylex'; @@ -92,6 +96,7 @@ export const mdxComponents = { ), CodeBlock, LLMInstallationFile, + LLMMigrationFile, LLMStylingFile, }; diff --git a/packages/docs/static/llm/stylex-migration-css-modules.md b/packages/docs/static/llm/stylex-migration-css-modules.md new file mode 100644 index 000000000..50a12ce30 --- /dev/null +++ b/packages/docs/static/llm/stylex-migration-css-modules.md @@ -0,0 +1,1243 @@ +# Migrating CSS Modules to StyleX + +This self-contained playbook guides an AI agent migrating CSS Modules to StyleX. + +Convert what is provably safe; label and report everything else accurately. + +**A labeled refusal is a success. A silent visual change is a failure.** + +--- + +## 0. How to use this document + +1. Read this entire document before editing any file. +2. Work in two passes: inventory first (no edits), then convert. +3. Convert one component at a time. Never batch-edit across components. +4. Produce the report described in section 10 even if you convert nothing. + +This document assumes StyleX v0.18+. For older versions, verify every API used +here against the installed version. + +--- + +## 1. The contract + +These rules are mandatory. + +- **R1. Never invent a value.** Copy or derive every value from repository + evidence. If you cannot see it, do not write it. +- **R2. Refuse rather than approximate.** If a CSS construct has no exact StyleX + equivalent, do not emit the "closest thing". Label it (section 7). +- **R3. Styling-only diffs.** Do not refactor: no renaming, reordering, + extraction, prop additions/removals/renames, component splits, unrelated + cleanup (including no-op class lookups), or formatting sweeps. Only R16 may + remove a proven-unreachable private-app styling passthrough. +- **R4. Never delete a source rule you did not convert.** Deleting unconverted + CSS silently removes styling. +- **R5. Conserve declarations.** Every source declaration must be converted, + refused, or explicitly dropped with a reason; counts must balance (section 8). + Freeze a parser-backed source total before editing, never from the result or + report prose. +- **R6. Do not combine arbitrary `className` or `style` values with + `stylex.props()` on one element.** They do not merge predictably. R17 permits + only a proven behavior-only class token, never another styling class or an + arbitrary caller value. +- **R7. Preserve effective directionality.** Inspect the CSS pipeline for + transforms such as `rtlcss`. Normally keep physical properties physical + (`marginLeft` stays `marginLeft`). If the exact pinned transform makes authored + physical CSS flow-relative, use the matching logical StyleX property and + record evidence. If CSS Module output is transformed but the transform is + unavailable, retain the contagious rule as `UNRESOLVED_VALUE` until equivalent + StyleX post-processing or RTL output is proven. A finite direction value + already in JavaScript may select an RTL namespace for non-property behavior + such as icon rotation; never add or mutate a document marker merely to observe + global direction. +- **R8. Do not convert a component whose class names cross a file boundary** + until its consumers are handled. See section 6.9. +- **R9. Preserve cascade outcomes, not cascade mechanics.** Determine which + declaration wins in the original CSS, then order StyleX arguments so the same + one wins (section 6.3). +- **R10. Report only what you verified.** State the verification level you + actually reached (section 9). Never write "verified" for a step you did not + run. +- **R11. One component per change set.** Each converted component should be + reviewable and revertable on its own. +- **R12. When two readings of the source are possible, refuse.** Ambiguity is a + blocker, not a judgment call. +- **R13. Verify version-matched third-party prop forwarding before converting.** + Evidence is exact-version source/types you actually opened, with its path and + symbol recorded. A lockfile, declared entrypoint, task statement, search result, + typecheck, or `className`-only example is locator evidence, not forwarding + proof. Both `className` and `style` from `stylex.props()` must reach the same DOM + element. Read the manifest and test every declared `exports`, `main`, `module`, + and `types` path directly; a bounded listing cannot prove absence. Refuse when + forwarding is disproven or remains unverified. +- **R14. Verify property support against pinned StyleX rules.** Before + conversion, check every property, exact value, and conditional branch with the + installed version's full ESLint/type reference. Finding the property key is + insufficient: validate its accepted value union. Check standard and vendor + spellings separately; compiler acceptance is insufficient. Refuse the whole + contagious rule when exact value support remains uncertain. +- **R15. Honor user-supplied facts.** Treat a stated StyleX version, compiler + capability, or module-resolution mode as pre-flight evidence and label it + user-supplied. A supplied dependency-evidence path is only a location to open; + it does not prove that the path exists or what it contains. Do not reject stated + facts because visible config is omitted, and never invent missing evidence. +- **R16. Remove a private optional styling contract only with closed-world + proof.** The component must be private application code: not package-exported, + published, copied, generated, dynamically loaded, or reached through an + unresolved alias. Resolve its full call graph; prove every live caller omits + optional `className` and `style` and no unknown spread can supply them. Only + then remove the dead passthrough. Public/incomplete graphs remain + `EXTERNAL_CONSUMER`. +- **R17. Preserve behavior-only class tokens explicitly.** A literal token may + coexist with StyleX only when repository-wide and exact-version dependency + evidence proves it is used solely by JavaScript behavior—not CSS, a stylesheet, + a styling library, or caller styling. Destructure one `stylex.props()` result, + join only its `className` with the literal token, and pass its `style` unchanged + to the same DOM element. Record the proving searches and consumer. This never + covers a CSS class, arbitrary expression, or prop value. + +--- + +## 2. Workflow + +### Pass 1: inventory (no edits) + +Produce a plan before touching anything. + +1. Find every `*.module.css`, `*.module.scss`, `*.module.less` file. +2. Find each file's consumers. Resolve imports using the repository's real + language/bundler order. If `../theme` resolves to `../theme.js`, it does not + import nearby `theme.module.css`; basename similarity proves nothing. +3. For dependency-owned consumers, resolve the exact lockfile version, inspect + version-matched types/source per R13, and report file, version, and evidence. +4. Freeze the parser-backed authored-declaration total for every scoped file. +5. Classify each file as **convertible**, **partial**, or **blocked** using the + ledger in section 5. +6. Order the convertible files leaf-first (section 6.10). +7. Write the report skeleton (section 10) with every file listed and no + conversions yet. + +If asked only for a plan, stop and present it. Also stop when a scope of **10+ +stylesheets** is over 20% blocked; it likely needs token bootstrap first +(section 4). Ignore this threshold for smaller scopes: when asked to migrate, +continue even if every file will be refused, so required markers are written. +An inventory-only plan neither edits files nor completes the migration report. + +### Pass 2: convert + +For each file in plan order: + +1. Read the stylesheet and **all** of its consumers in full. +2. Convert (sections 4 to 6). +3. Run the verification ladder (section 9). +4. Update the report row for that file with real counts. +5. Continue only after the prior file verifies; otherwise fix or revert it. + +--- + +## 3. Pre-flight + +Before editing, establish and report these facts; never guess. + +|Question|Evidence| +|---|---| +|Is StyleX installed and compiling?|`package.json` for `@stylexjs/stylex` plus StyleX build-plugin config| +|Version?|`package.json` / lockfile| +|JSX style prop?|Read `sxPropName`: default `sx`, configurable (for example `css`), or disabled with `false`; if unsure, use `stylex.props()`| +|Framework?|Non-React uses `stylex.attrs()`, not `stylex.props()`| +|Preprocessor?|`.module.scss` / `.module.less` invokes section 6.8| +|Token source?|`@value`, `:export`, custom properties, theme files| +|Layers/import order?|`@layer`, plugin `useCSSLayers`, global imports| +|Existing `.stylex.ts` tokens?|Reuse them; never duplicate the same tokens| +|Theming configured?|`defineVars`, `createTheme`, and markers require compiler module resolution; inspect its config| +|Supported properties/values?|Run all StyleX ESLint rules or inspect the installed ESLint/type reference; CSS standardization or compiler acceptance is not proof| + +User-stated facts override missing local config. Record them as user-supplied; +do not refuse a safe rule merely because its manifest/plugin config is hidden. + +--- + +## 4. Token bootstrap (do this first) + +Bootstrap tokens in their own change set before components, avoiding duplicated +literals and later rework. + +Map token sources as follows. + +**CSS custom properties on `:root`** + +```css +:root { + --color-primary: #0055ff; + --space-medium: 16px; +} + +[data-theme='dark'] { + --color-primary: #88aaff; +} +``` + +Token bootstrap is the sole exception to treating `:root` as global. First +inventory **all** definitions, overrides, and uses. Convert a `:root` block only +if it contains custom-property definitions exclusively and every runtime +override maps to a visible theme application site or the system-color path +below; otherwise refuse the whole block. + +In a published library, theme, plugin, scaffold, or copied/swizzled tree, an +authored named `:root` property is a downstream CSS API unless explicitly +private. A hashed `defineVars` name breaks readers/overrides. Retain it as +`GLOBAL_SELECTOR`/`UNRESOLVED_VALUE` unless a versioned API migration handles +consumers; no local overrides does not permit renaming. + +Put variable definitions in a `.stylex.ts` token file: + +```ts +// tokens.stylex.ts +import * as stylex from '@stylexjs/stylex'; + +export const tokens = stylex.defineVars({ + colorPrimary: '#0055ff', + spaceMedium: '16px', +}); +``` + +Put `createTheme` in an **ordinary module**, never in the token file: + +```ts +// themes.ts +import * as stylex from '@stylexjs/stylex'; +import { tokens } from './tokens.stylex'; + +export const darkTheme = stylex.createTheme(tokens, { + colorPrimary: '#88aaff', + spaceMedium: '16px', +}); +``` + +Apply the theme to the prior theme container. Keep its attribute/class when +behavior reads it. Use `defineVars`/`createTheme` only with compiler theming +module resolution. + +**System color-scheme overrides** + +`@media (prefers-color-scheme: light)` or +`@media (prefers-color-scheme: dark)` overrides of `:root` defaults may become +conditional `defineVars`; unlike attribute/class themes, the condition lives in +each variable value. + +```css +:root { + --text-primary: black; + --background: white; +} + +@media (prefers-color-scheme: dark) { + :root { + --text-primary: white; + --background: black; + } +} +``` + +```ts +// colors.stylex.ts +import * as stylex from '@stylexjs/stylex'; + +const DARK = '@media (prefers-color-scheme: dark)'; + +export const colors = stylex.defineVars({ + textPrimary: { default: 'black', [DARK]: 'white' }, + background: { default: 'white', [DARK]: 'black' }, +}); +``` + +Use this only for custom-property-only blocks after inventorying every default, +media override, and use. Preserve each query exactly; never invent a default. +Retain the family if the pinned version cannot represent its default, or if you +cannot prove original cascade/precedence against explicit themes, inline values, +or nested scopes. Use `GLOBAL_SELECTOR` or `UNRESOLVED_VALUE`. Media selection +does not waive the public CSS API rule above. + +**`@value` declarations** + +```css +@value primary: #0055ff; +@value spacingMedium: 16px; +``` + +If the value never changes at runtime, use `defineConsts`. If it is themed or +overridden, use `defineVars`. Follow the same file-placement rules above. + +**`:export` blocks** become `defineConsts` or `defineVars` according to theming; +update JS imports to the token file. + +**Media query breakpoints** become `defineConsts` entries so every component +references the same string. + +**IMPORTANT constraints on token files:** + +- Must be named `*.stylex.ts` or `*.stylex.js` +- Named exports only, no default export +- May export only values returned by `defineVars`, `defineConsts`, or + `defineMarker` (subject to the project's `enforce-extension` options) +- `createTheme`, `stylex.create`, and `stylex.keyframes` belong in ordinary + modules, not token files +- Import them directly. Re-exporting through a barrel/index file breaks + compile-time resolution + +Report token path/count and each `@value` or property not classifiable as const +vs var; retain and label it `UNRESOLVED_VALUE`. + +**Component-scoped custom properties** + +For custom properties defined on a local class and read only by owned +descendants: put defaults in token-file `defineVars`, authored local values in +ordinary-module `createTheme`, apply the theme to the same container, and replace +`var(--name)` reads with its token. This preserves inheritance without raw +unsupported `'--name'` keys. Require complete definition/override/consumer proof; +otherwise retain the family as `UNRESOLVED_VALUE`. + +**Private-app global-state value bridge** + +An app owning its loaded global stylesheet may bridge a finite global-state +selector by copying authored values into named custom properties on that +selector for StyleX. Another system may write the state, but exact-version source +must prove every value, state, placement, and scope. + +This bridge is not a new truth: copy values exactly and override every finite +state at every possible writer scope. If `data-theme` may occur on arbitrary +subtrees, light/dark requires `:root` defaults plus **both** +`[data-theme='light']` and `[data-theme='dark']` rules, so light resets inside +dark ancestors. Keep the global CSS loaded and limit changes to the private app. +Never use this for a library, theme, plugin, scaffold, copied/swizzled output, +or downstream CSS API. Incomplete source, states, placement, or scope means +`GLOBAL_SELECTOR`/`UNRESOLVED_VALUE`. + +--- + +## 5. Expressibility ledger + +Classify every rule in a stylesheet against this table before converting it. + +### Direct equivalents + +|CSS Modules|StyleX| +|---|---| +|`.foo { color: red }`|`stylex.create()` namespace| +|Multiple declarations|Multiple namespace properties| +|`.foo:hover`, `:focus`, `:active`, `:disabled`, `:focus-visible`|Condition nested in property value| +|`.foo::before`, `::after`, `::placeholder`, `::selection`|Top-level namespace key| +|`@media`, `@supports`, `@container`|Condition nested in property value| +|`@keyframes` + `animation-name`|`stylex.keyframes()` as `animationName`| +|Custom-property definitions|Token-file `defineVars()`| +|`@value`|`defineConsts()` or `defineVars()`| +|Multiple fallback values|`stylex.firstThatWorks()`| + +### Expressible with restructuring (allowed, with preconditions) + +|CSS Modules|StyleX|Precondition| +|---|---|---| +|`.card h2 { ... }`|Style every matching `

` directly|All matches and render branches are visible in edited files; none comes through `children`, render props, or third-party DOM| +|`.card > .icon { ... }`|Style child directly|Same| +|`.card:hover .icon { ... }`|Icon: `stylex.when.ancestor(':hover')`; card: `stylex.defaultMarker()`|Observed ancestor and styled element are controlled| +|`.input:focus + .label { ... }`|`stylex.when.siblingBefore(':focus')` + marker|Same| +|`.row[data-open='true'] .cell { ... }`|`stylex.when.ancestor('[data-open="true"]')` + marker|Same| +|`.title, .subtitle { ... }`|Shared/equivalent namespace at every use|Every branch/use is convertible; count each authored declaration once| + +Structural-descendant locality: + +- Refuse targets from `children`, render props, or third-party output. +- A target behind a visible local conditional is convertible only when every + matching branch is visible and styled. +- A local `.map()` target is convertible when its element and all matching + branches are visible; style that element directly. +- Style every local match; refuse unbounded/non-enumerable sets. + +Directly applying `.card h2` preserves current rendering but not automatic +styling of future `

` children. Report each structural rewrite so future +matches receive the namespace; this note alone does not require refusal. + +For same-element attributes, nest the selector in the value: +`.trigger[data-state='open']` becomes `[data-state="open"]` plus `default`; +handle `aria-*` likewise. For ancestor attributes, use +`stylex.when.ancestor(...)` on the descendant and a marker in the ancestor's +`stylex.props()`. Remove a CSS class used only to identify that ancestor after +the marker replaces it. + +`stylex.when.*` observes **state** (pseudo/attribute) on a marked element, not +pure structure. `.card h2` must be styled on the child or refused. + +`stylex.when.descendant`, `anySibling`, and `siblingAfter` compile to `:has()`. +For projects supporting browsers without it, refuse and note why. + +### Refuse + +|CSS Modules|Reason| +|---|---| +|`:global(...)` / `:global { ... }`|`GLOBAL_SELECTOR`| +|`html`, `body`, `:root`, `*`, or bare tags (except section 4 bootstrap)|`GLOBAL_SELECTOR`| +|Third-party-rendered DOM target|`GLOBAL_SELECTOR`| +|Non-local structural target|`STRUCTURAL_SELECTOR`| +|`+`, `~`, `.stack > * + *` spacing|`STRUCTURAL_SELECTOR`| +|Structural `:nth-child`, `:first-child`, `:last-child`, `:not(...)`|`STRUCTURAL_SELECTOR`| +|`!important`|`IMPORTANT`| +|`composes` from `global` or unreadable file|`COMPOSES_UNRESOLVED`| +|Preprocessor mixin/function/`@extend`/loop|`UNRESOLVED_VALUE`| +|`@font-face`, `@page`, `@property`, `@import`|`UNSUPPORTED_AT_RULE`| +|`@layer` without StyleX layer config/global order|`UNSUPPORTED_AT_RULE` / `CASCADE_ORDER`| +|Vendor-only property without standard equivalent|`UNSUPPORTED_PROPERTY`| +|Dynamic access (`styles[name]`)|`DYNAMIC_CLASS`| +|Class on unowned component or crossing file boundary|`EXTERNAL_CONSUMER`| +|Conflicts without determinable static order|`CASCADE_ORDER`| + +A refusal is contagious: any class named in a refused rule's selector must keep +being applied, so it cannot be converted either. See 6.2.1. + +--- + +## 6. Conversion rules + +### 6.1 Declarations + +- Expand multi-value shorthands. `padding: 8px 16px` becomes + `paddingTop: 8`, `paddingRight: 16`, `paddingBottom: 8`, `paddingLeft: 16`. + `border: 1px solid red` becomes `borderWidth`, `borderStyle`, `borderColor`. + Single-value shorthands (`padding: 8px`) may stay as `padding: 8`. +- Before expanding shorthand containing fallback-free `var(--name)`, prove it + resolves in every applicable state. An unresolved variable invalidates the + **whole shorthand**; splitting may wrongly leave longhands active. If its + definition or every runtime assignment is unproven, retain the contagious + family as `UNRESOLVED_VALUE`. +- Convert property names to camelCase. +- Check every camel-cased property/value against the pinned version's complete + `valid-styles` and type reference. One unsupported declaration refuses its + whole rule and contagious family. Check standard/vendor spellings separately: + `WebkitLineClamp`/`WebkitBoxOrient` support proves nothing about `lineClamp`, + or vice versa. Name the failing property in `UNSUPPORTED_PROPERTY`. +- Inspect the final validator, not one table entry: CSS-wide keywords (`inherit`, + `initial`, `unset`, `revert`) may be added after table construction. A local + `showError(...)` does not prove `font: inherit` is rejected; run the rule or + follow final composition. +- Sort each namespace with configured `@stylexjs/sort-keys`, else its default. + At L1, autofix then rerun. If unavailable, do not guess priorities: mark order + unverified and not release-ready until the rule fixes and rechecks it. +- Preserve string values verbatim except: bare `px` may become a number + (`16px` → `16`), and authored unitless numbers must be numeric when the pinned + checker requires it (`line-height: 1.2` → `lineHeight: 1.2`). Never change units. +- `content` values keep their quotes: `content: ''` becomes `content: '""'`. +- Keep colors, `calc()`, gradients, and `transform` strings exactly as authored. +- Drop only with recorded unreachability proof. A rule fully overridden later + in-file qualifies. An unused rule qualifies only in a private app after + repository-wide searches exclude static/dynamic use, exports, generated or + template copies, package output, and runtime loaders. In libraries, published + packages, themes, plugins, scaffolds, or copied/swizzled trees, absence of a + local import is insufficient: retain/refuse. Apply consistently and count each declaration + _dropped_ with evidence. Decide reachability before support: dead private-app + rules are dropped even with unsupported properties. In monorepos classify the + target subtree, tracing its entrypoints, exports, file globs, copy steps, and + loaders; an unrelated public package does not make a private app public. + +### 6.2 Selector classification + +Parse CSS Modules scope before classification. Treat `:local(.foo)` as `.foo` +while preserving its pseudo-classes/elements, lists, and relationships. Never +blind-replace or unwrap `:global()`. + +Examples: + +- `:local(.button):hover` is classified like `.button:hover`. +- `:local(.title), :local(.subtitle)` is a selector list whose branches must + each be independently convertible. +- `:local(.card .title)` remains a structural descendant selector. +- `:local(.card) :global(.tooltip)` still contains a global target and is + refused as `GLOBAL_SELECTOR`. + +For each rule, stop at the first match: + +1. Selector contains `:global` or targets `html`/`body`/`:root`/`*`/a bare tag → + refuse `GLOBAL_SELECTOR`, except for a fully classified section 4 + custom-property bootstrap or private-app global-state value bridge. +2. Declaration block contains `!important` → refuse `IMPORTANT`. +3. Selector is a single class (`.foo`), optionally with a supported pseudo-class + or pseudo-element → **convert**. +4. Selector is a class plus a state on the same element (`.foo[data-x='y']`, + `.foo:hover`) → **convert** as a nested condition. +5. Relationship with **state on the left** and a local right side → **convert** + with `stylex.when.*` + marker. +6. Pure structure with all targets local → **convert** on each target. If a + boolean/finite variant for a class-keyed ancestor is already in JavaScript, + select a target namespace (for example `as === 'h1' && styles.titleH1`), not + `stylex.when.*`. +7. Global direction/finite state whose exact value is already in component JS → + **convert** by selecting a namespace; do not add markers to unowned document + nodes such as `html`/`body`. +8. Selector is a comma-separated list and every branch is independently + convertible → **convert** every branch without multiplying ledger counts. +9. Anything else → refuse `STRUCTURAL_SELECTOR`. + +If any selector-list branch is refused, refuse the whole rule; splitting may +change cascade/specificity. A preparatory source split requires prior proof of +identical selector and cascade outcomes. + +### 6.2.1 Dependency check (run before converting any class) + +A refused rule works only while its selector classes remain applied. Therefore: + +**Before converting `.x`, find every rule mentioning it. If any is refused, +`.x` cannot be converted.** + +```css +.card { + padding: 16px; +} /* looks convertible */ +.card :global(.tooltip) { + z-index: 10; +} /* refused: GLOBAL_SELECTOR */ +``` + +Converting `.card` would silently disable the tooltip rule. Keep both rules and +the class; do not migrate that element. Label it with the blocking code. + +Migrate per element: siblings may use different systems; R6 forbids mixing only +on the _same_ element. + +**Refuse whole rules, not declarations.** One blocked declaration retains and +counts the whole rule; extracting safe declarations would mix CSS Modules and +`stylex.props()` on one element. Apply contagion transitively to every named class. + +### 6.3 Multiple classes on one element + +In CSS Modules, `cx(styles.a, styles.b)` does **not** pick the winner; specificity +then source order do. In StyleX, the last argument wins. + +Procedure: + +1. List the properties set by more than one applied class. +2. For each, determine the CSS winner (higher specificity first, then later + source position). +3. Order the `stylex.props()` arguments so the winner is last. +4. If the classes are applied conditionally such that no single static order + reproduces the original for all combinations, refuse `CASCADE_ORDER`. + +```css +/* Button.module.css — .primary appears later, so it wins */ +.base { + background: gray; + padding: 8px; +} +.primary { + background: blue; +} +``` + +```tsx +// Correct: primary last +