diff --git a/.agents/skills/design-system-code-review/SKILL.md b/.agents/skills/design-system-code-review/SKILL.md index f9f9dcc05abf41..719369ede0cd71 100644 --- a/.agents/skills/design-system-code-review/SKILL.md +++ b/.agents/skills/design-system-code-review/SKILL.md @@ -18,6 +18,6 @@ Read the affected package's `AGENTS.md` and [Working with WordPress Design Syste 5. **Interaction or accessibility regressions.** Check the semantics, accessible names, keyboard paths, focus, and disabled states the change can affect. Use the guide's [verification guidance](../../../docs/contributors/design/design-system-packages.md#verify-the-affected-behavior) for standards and browser evidence. Passing types or class assertions do not prove interaction or visual parity. 6. **Public documentation or generated output left behind.** Check affected prop documentation, stories, component status and recommendation metadata, generated token assets, and required changelogs. Do not request new stories or documentation for an unchanged capability. -Apply [package-runtime-compatibility](../package-runtime-compatibility/SKILL.md) only when the changed contract crosses independently updated package and WordPress versions. For an Emotion migration, use the [migration guide](../../../packages/components/emotion-to-scss-modules.md) to check cascade and consumer compatibility. +Apply [package-runtime-compatibility](../package-runtime-compatibility/SKILL.md) only when the changed contract crosses independently updated package and WordPress versions. For stylesheet changes, use the [styling conventions](../../../packages/components/CONTRIBUTING.md#styling) to check cascade and consumer compatibility. Keep findings tied to the change, including downstream effects. Missing tests alone are not a finding. Reuse existing coverage and request a new check only for a concrete, meaningful risk introduced by our code or integration. Combine concerns that have one coherent fix. diff --git a/.agents/skills/design-system-contribution/SKILL.md b/.agents/skills/design-system-contribution/SKILL.md index b4f2bb6d27c09d..4eeb1d9e5f943a 100644 --- a/.agents/skills/design-system-contribution/SKILL.md +++ b/.agents/skills/design-system-contribution/SKILL.md @@ -21,7 +21,7 @@ Keep product-specific behavior in the consuming package and use [design-system-u - Follow the package's component, prop/ref, state, and styling conventions. Reuse existing composition and helpers instead of rebuilding behavior already provided by a dependency. - For removals, replacements, or behavior changes, compare the affected old and new contracts using the [compatibility guidance](../../../docs/contributors/design/design-system-packages.md#compare-contracts-before-replacing-an-api). Include plugin and npm consumers, even when all Gutenberg callers have been migrated. -- For token changes, choose by semantic purpose and affected modes, then follow the [token source guide](../../../packages/theme/tokens/README.md) and [build procedure](../../../packages/theme/README.md#building). For Emotion migrations, use [emotion-to-scss-modules](../emotion-to-scss-modules/SKILL.md). +- For token changes, choose by semantic purpose and affected modes, then follow the [token source guide](../../../packages/theme/tokens/README.md) and [build procedure](../../../packages/theme/README.md#building). - Use [package-runtime-compatibility](../package-runtime-compatibility/SKILL.md) only when changing a contract between independently updated package and WordPress versions. ## Verify what changed diff --git a/.agents/skills/emotion-to-scss-modules/SKILL.md b/.agents/skills/emotion-to-scss-modules/SKILL.md deleted file mode 100644 index d11b1faccab1e5..00000000000000 --- a/.agents/skills/emotion-to-scss-modules/SKILL.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -name: emotion-to-scss-modules -description: Use when planning or implementing an Emotion-to-SCSS-Modules migration in @wordpress/components, including styled wrappers, css/useCx composition, and migration-related cascade fixes. ---- - -# Migrate Emotion styles to SCSS Modules - -In a Gutenberg checkout, read the relative links below from the checked-out revision. Outside a checkout, resolve them from `.agents/skills/emotion-to-scss-modules/` in [Gutenberg on GitHub](https://github.com/WordPress/gutenberg/tree/trunk). - -Read the [migration guide](../../../packages/components/emotion-to-scss-modules.md) and the package's [styling conventions](../../../packages/components/CONTRIBUTING.md#styling) before editing. - -## Preserve behavior and consumer overrides - -1. Identify the requested component and baseline. Search for consumers of its wrappers, style fragments, and public classes, including outside `packages/components`. Use the guide's [contract audit](../../../packages/components/emotion-to-scss-modules.md#establish-the-component-and-consumer-contract) to select affected states and compositions for comparison. -2. Capture the baseline for those cases. For a reported regression, reproduce it first. Apply the [translation rules](../../../packages/components/emotion-to-scss-modules.md#translate-the-styles), distinguishing static, conditional, and genuinely dynamic values. Preserve the wrapper's element, props, and ref behavior. -3. Check which declarations win after removing the wrapper. Follow the guide's [cascade checks](../../../packages/components/emotion-to-scss-modules.md#preserve-the-winning-declarations) for consumer overrides and remaining Emotion composition. Justify a specificity change with the actual conflicting declaration. - -Keep intentional API, interaction, or visual changes within the agreed scope. Consult a [merged example](../../../packages/components/emotion-to-scss-modules.md#reference-migrations) only when it answers a migration question; an older exception is not a rule for this component. - -## Verify the migration - -Reuse existing tests and stories. A behavior-preserving migration may need no new tests. Add a focused test only for an uncovered risk introduced by the migration; do not duplicate shared helper or dependency suites. Follow the guide's [verification choices](../../../packages/components/emotion-to-scss-modules.md#verify-css-delivery-and-behavior) and the [testing overview](../../../docs/contributors/code/testing-overview.md#folder-structure). - -Compare actual styles for the affected states and consumers, including RTL or another document when relevant. Class assertions and mocked styles cannot prove cascade or visual parity. Recheck affected consumers after subsequent selector changes or a rebase that changes relevant styles. - -Follow the [completion checks](../../../packages/components/emotion-to-scss-modules.md#complete-the-migration) for scoped cleanup, affected downstream snapshots, generated files, required checks, and the migration changelog. Report any unverified styling case with reproducible manual steps. diff --git a/packages/components/AGENTS.md b/packages/components/AGENTS.md index be4fef880564ff..b30f8bd7836d85 100644 --- a/packages/components/AGENTS.md +++ b/packages/components/AGENTS.md @@ -4,6 +4,5 @@ Use [design-system-contribution](../../.agents/skills/design-system-contribution - [CONTRIBUTING.md](./CONTRIBUTING.md) for component APIs, compatibility, styling, stories, and documentation. Existing components can have compatibility constraints that differ from the conventions for new components. - The affected component's README, types, stories, and consumers before changing its contract. -- [emotion-to-scss-modules](../../.agents/skills/emotion-to-scss-modules/SKILL.md) for an Emotion migration. For consumer component selection, follow [Choose a recommended component](../../docs/contributors/design/design-system-packages.md#choose-a-recommended-component). This package remains supported; do not migrate existing consumers to `@wordpress/ui` just because it is newer. diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 08a99c0d8ed2cd..1d417afcbdbbb6 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -71,6 +71,7 @@ ### Documentation +- Retire completed Emotion migration guidance and keep permanent styling advice in the contribution guide ([#85023](https://github.com/WordPress/gutenberg/pull/85023)). - Update styling guidance and the `ToolsPanel` example after the Emotion migration ([#85020](https://github.com/WordPress/gutenberg/pull/85020)). ## 42.0.0 (2026-10-07) diff --git a/packages/components/CONTRIBUTING.md b/packages/components/CONTRIBUTING.md index 46cdd96a0c634a..1941a69cae7876 100644 --- a/packages/components/CONTRIBUTING.md +++ b/packages/components/CONTRIBUTING.md @@ -424,7 +424,7 @@ Use `--_wp-components-*` for new private custom properties. These are implementa Components no longer use Emotion. Do not add new Emotion imports. -The [Emotion-to-SCSS migration guide](./emotion-to-scss-modules.md) records compatibility checks, cascade and custom-property pitfalls, verification, and examples from the completed source migrations. +For styling compatibility and verification, follow [Change a package safely](/docs/contributors/design/design-system-packages.md#change-a-package-safely) and the [cross-document setup guidance](/packages/theme/README.md#across-documents-iframes-and-other-portals). ### Deprecating styles diff --git a/packages/components/emotion-to-scss-modules.md b/packages/components/emotion-to-scss-modules.md deleted file mode 100644 index e24d614b6da655..00000000000000 --- a/packages/components/emotion-to-scss-modules.md +++ /dev/null @@ -1,146 +0,0 @@ -# Migrating components from Emotion to SCSS Modules - -An Emotion migration should preserve the component's public API, rendered elements, interactions, and resulting styles. Use the existing SCSS Modules and `@wordpress/style-runtime` pipeline. A component migration does not require a new CSS extraction pipeline. - -The component source migration is complete. This guide records the [migration guidelines in #66806](https://github.com/WordPress/gutenberg/issues/66806) and lessons from its merged PRs. The [reference migrations](#reference-migrations) identify useful examples and their limitations. Read the current implementation before copying an older diff. Some merged PRs include explicitly agreed visual or behavior changes that are not part of a mechanical migration. - -## Establish the component and consumer contract - -Before editing, read the component's types, README, stories, style hooks, and composed primitives. Search for consumers of its exported wrappers, style fragments, and public classes, including outside `packages/components`. A shared style file can serve several components, as with `ColorPalette` and `GradientPicker`. - -Record the behavior that needs comparison: - -- Default rendered element, supported `as` targets, refs, prop forwarding, and intrinsic HTML/SVG prop filtering. -- Existing public classes and which element adds each one. `useContextSystem` may already supply the root class. Keep legacy spellings, including singular `component-*` names. -- Consumer `className` and `style` handling, including spread order and conditional forwarding. -- Defaults, variants, nested instances, focus and disabled states, directional styles, and animation state changes. -- Selectors that override the component and selectors the component uses to override its children. -- Portal and iframe paths, including the document that receives styles. - -Capture a focused baseline in Storybook or the editor for the states and consumers at risk. If a regression motivates the migration, reproduce it before changing the implementation. An unchanged isolated story cannot establish compatibility for a consumer that adds its own styles. - -### Replace wrappers without changing their role - -For `styled( ExistingComponent )`, apply the module class to that same component when it already supplies the needed behavior. Do not replace `Flex`, `Heading`, `Text`, or an input control with a native element just because its wrapper only contained CSS. - -For a wrapper that relied directly on Emotion's `as` support, inspect and reuse [`PolymorphicElement`](/packages/components/src/utils/polymorphic-element.tsx). Preserve the original default tag and the component's existing ref and prop contract. `View` already delegates to this utility. A fixed internal element with no exposed polymorphic path can remain a plain element. - -Check the current [`WordPressComponent` types](/packages/components/src/context/wordpress-component.ts) instead of introducing another polymorphic type system. [#80705](https://github.com/WordPress/gutenberg/pull/80705) deliberately reduced intrinsic-prop type complexity to control compilation cost. Removing Emotion does not remove the compatibility obligation for `as`. - -Keep shared wrappers' private module classes separate from public classes added by callers. In [#80001](https://github.com/WordPress/gutenberg/pull/80001), adding BaseControl label/help classes inside shared wrappers made unrelated consumers match BaseControl selectors; adding classes on both sides also produced duplicates. Test a real `as="legend"` consumer when migrating shared labels, including its visible and visually hidden branches. - -`View`'s legacy `css` prop is an explicit exception established by [#79443](https://github.com/WordPress/gutenberg/pull/79443). It remains accepted as a no-op and is consumed before rendering. Do not restore its styling behavior, forward it to the DOM, or treat that decision as permission to remove other props. - -## Translate the styles - -Follow the package's [styling conventions](/packages/components/CONTRIBUTING.md#styling). Put static declarations in a colocated `style.module.scss`, use kebab-case classes, and compose variants with `clsx`. Use existing semantic attributes and pseudo-classes where appropriate; do not add data attributes solely to select styles. - -Name modifier classes for their state and make their owning element clear. When scoping a modifier under a base class, account for the extra specificity. A standalone `.is-active` and `.root.is-active` do not have equal specificity. - -Keep fixed values and calculations in Sass when JavaScript does not need them. `ProgressBar` moved its constant indeterminate indicator width into Sass and shared it with the keyframes. CSS Module keyframes can have descriptive local names without component prefixes. - -### Preserve dynamic values and overrides - -Prefer inline custom properties for genuinely dynamic values, with the actual property declared in the stylesheet. Emotion adds units to many numeric CSS values; custom properties do not. Preserve the original conversion, such as `space( value )` or an explicit `px` suffix, and keep zero distinct from an absent value. - -Check these differences before choosing the representation: - -- **Cascade.** Moving `z-index` from a CSS rule to `style.zIndex` prevents ordinary consumer CSS from overriding it. [ZStack's review](https://github.com/WordPress/gutenberg/pull/80514#discussion_r3845479235) led to passing the number through a custom property and keeping `z-index` in the module rule. -- **Precedence.** Preserve consumer `style` values and the component's existing override rules. Do not copy a universal spread order from another migration. `Spacer` lets consumer custom properties win; `Flex` supplies its internal variables after the consumer style object. Ordinary inline CSS properties still override stylesheet declarations. -- **Inheritance.** Internal custom properties inherit unless reset. Reset them on each instance when nested components must not inherit a parent's values, as in `Spacer`. Do not reset intentionally inherited theme variables. -- **Shorthands.** A later longhand with an absent custom property can undo the shorthand result. Preserve the fallback and precedence of all-sides, axis, and individual-side values. Check shorthand-only, longhand-only, and combined inputs, including supported CSS strings. The `Spacer` review caught missing longhand fallbacks. -- **Accepted CSS values.** CSS-wide keywords apply to a custom property itself. `--style: inherit; border-style: var(--style, solid)` does not mean `border-style: inherit`. In [#80437](https://github.com/WordPress/gutenberg/pull/80437#discussion_r3608442254), the indicator retained direct `borderStyle` and `borderColor` values and the original `color || fallback` behavior. Check empty strings and CSS-wide values when the API accepts arbitrary CSS. Direct inline properties can be appropriate, but assess their effect on consumer overrides explicitly. - -### Preserve direction, tokens, and state changes - -Use logical properties where they preserve the intended behavior. Compare LTR and RTL, and relevant writing modes, before changing a physical property. `Surface` exposes physical border props; `TextareaControl` retains `resize: vertical`; `DropdownContentWrapper` offsets the Popover's physical padding. These are reasons for narrow lint suppressions, not a blanket ban on logical properties. `padding: 0` also resets more than `padding-inline: 0`. - -Preserve necessary browser declarations and pseudo-elements. Examples include Truncate's `-webkit-box-orient: vertical`, SearchControl's WebKit search decorations, and TextareaControl's placeholder selectors and global dark-theme ancestor. - -Prefer equivalent WPDS tokens and existing Sass variables or mixins. Check what they expand to, including fallback values and validated-control accent overrides. A similarly named token can change the color or geometry. Keep intentional visual changes explicit, as in `Surface` and `ToggleGroupControl`, instead of presenting them as style parity. - -Compare transitions between states as well as static states. [ProgressBar's review](https://github.com/WordPress/gutenberg/pull/80512#discussion_r3830324726) caught a width transition newly applying when switching to indeterminate mode. Preserve the original animation direction, duration, transition conditions, reduced-motion rules, and forced-colors treatment where present. - -## Preserve the winning declarations - -CSS Module scoping prevents class-name collisions. It does not control specificity or stylesheet insertion order. The order of class names passed to `clsx` does not control the cascade either. - -Compare matched declarations on the component and its consumers: - -- Keep low-specificity defaults overridable. The Divider migration added attribute-selector specificity and broke `CardDivider` and complementary-area border overrides. [#79534](https://github.com/WordPress/gutenberg/pull/79534) wrapped orientation attributes in `:where()` to remove that extra specificity. -- Preserve deliberate Emotion `&&` or `&&&` increases. Sass can express these as `.wrapper.wrapper` or `.label.label.label`; raw `&&&` is not valid Dart Sass. ConfirmDialog's doubled selector keeps its overlay above the parent Popover. -- Preserve declarations that had to override primitives during the staged migration. ColorPalette initially protected `font-size` and margin but missed `line-height`; Heading's later Emotion rule won in the root document. ToolsPanel had a similar heading conflict. Check current consumers before reducing the resulting specificity. -- Keep selector relationships and scope. Nesting `.layered > .child` under `.z-stack` adds specificity. Replacing Emotion component interpolation requires checking which real elements the new selector matches. -- Mark existing external classes with `:global(...)` inside a CSS Module, for example `:global(.components-panel__row) &`. Do not accidentally hash a legacy class, broaden its matching elements, or add a public class solely to replace an internal selector. - -Use the smallest selector change that preserves the required winner. Do not put every rule in `:where()`, increase every selector, or add `!important` to hide an unexplained conflict. Record why a deliberate specificity adjustment is needed. - -### Preserve external Emotion composition - -Earlier migration stages used `useCx` to combine component and consumer classes. That hook has been removed, and the context system now composes class names with `clsx`. Consumers that still author Emotion styles must combine source-order-dependent fragments in one `css()` call before passing the resulting class name to a component: - -```js -import { css } from '@emotion/css'; - -const className = css( baseStyles, condition && overrideStyles ); -``` - -This is compatibility guidance for external Emotion consumers. New component styles use SCSS Modules. Do not mechanically combine unrelated fragments. Audit downstream callers when changing composition: [#79443](https://github.com/WordPress/gutenberg/pull/79443) updated several consumers but missed the BorderBoxControl gutter regression fixed in [#79967](https://github.com/WordPress/gutenberg/pull/79967). - -Test mixed Emotion/Module composition in both the root and iframe documents when used there. [Scrollable's review](https://github.com/WordPress/gutenberg/pull/80694#discussion_r3845389532) found that `CardBody isScrollable` kept `height: 100%` in an iframe but resolved to `auto` in the root document because insertion order differed. - -## Verify CSS delivery and behavior - -Reuse existing tests and stories first. Add or extend a test only for a concrete migration risk that existing coverage does not protect. Choose the smallest check that observes the behavior and would fail if it regressed. A behavior-preserving migration may need no new tests. When automated tests cannot observe the real styling path, document a focused before/after browser check instead of adding a test that cannot detect the regression. - -[`StyleProvider`](/packages/components/src/style-provider/index.tsx) registers a document with `@wordpress/style-runtime`. Keep this registration path for CSS Module delivery, including its `useInsertionEffect` timing and cleanup. The provider no longer supplies an Emotion cache. Consumers using `@emotion/react` or `@emotion/styled` in another document must provide their own `CacheProvider` with a cache whose `container` is that document's `
`. Consumers using `@emotion/css` must create an instance with `@emotion/css/create-instance`, set its `container` to that document's ``, and use the instance's styling functions. See [Emotion's package summary](https://emotion.sh/docs/package-summary) and [custom-instance guidance](https://emotion.sh/docs/@emotion/css#custom-instances). - -For SlotFill or portals, use an attached iframe with a real `contentDocument` and `defaultView`. Assert a computed style in the destination document. A class on the element or a style tag in the parent document does not prove delivery. [#80384](https://github.com/WordPress/gutenberg/pull/80384) pairs a registration test with a Storybook example that renders migrated Spacer styles in an iframe. - -Match the evidence to the behavior at risk: - -| What needs proof | Suitable evidence | -| --- | --- | -| DOM, public classes, refs, props, variants, and interactions | Existing component tests; extend them only for an uncovered integration risk. Assert the actual styled node, including the outer Grid child when an inner wrapper has the same class. | -| Real SCSS declarations, specificity, custom-property resolution, dimensions, and animation | Storybook or editor comparison using production styles, computed styles, and relevant interactions. Include a consumer override and the affected document/RTL states. | -| Cross-document registration or a particular mixed-style composition mechanism | Reuse shared registration coverage. Add a component-specific test only for an uncovered registration or composition risk. A small fixture stylesheet can test that mechanism; pair it with a real-style check. | - -The Node and jsdom projects mock stylesheet imports; Browser Mode loads the real styles. Public-class assertions can protect a compatibility contract; private module-class assertions establish wiring only. Neither proves that the real CSS loads or wins. Do not add class assertions or snapshots solely to mirror the implementation. Do not inject the desired production selector into a test and claim it protects that selector: the test would still pass if the SCSS rule were deleted. This distinction led to removing tests in [#81792](https://github.com/WordPress/gutenberg/pull/81792#discussion_r3811746653). - -Keep new tests focused on the migrated component's integration. A wrapper test can protect its `as`, ref, or prop forwarding even when the shared helper is already tested. Do not copy PolymorphicElement's entire filtering suite into every wrapper, duplicate a full editor setup for a small assertion, or introduce a new Storybook testing convention just for the migration. Check the relevant states and combinations from the contract audit; do not generate every prop combination or supported tag. When a real-style assertion is unavailable, state the gap and give reproducible manual steps. - -## Complete the migration - -Remove obsolete Emotion imports, files, lint/type suppressions, and utilities that become unused because of this migration. Search all consumers before deleting shared constants. Preserve useful comments explaining layout calculations or compatibility workarounds. The compiler transforms, `useCx`, and Emotion `rtl` utility have been removed. Audit direct and transitive dependencies across workspaces before removing remaining package declarations or lockfile entries. Leave unrelated cleanup outside the migration. - -Follow [worktree setup](/docs/contributors/code/getting-started-with-code-contribution.md#set-up-each-worktree) and read the [testing overview](/docs/contributors/code/testing-overview.md) before adding tests. All unit tests use Vitest. Follow the [filename-based environment conventions](/docs/contributors/code/testing-overview.md#folder-structure). From the repository root, use the appropriate focused command below, then run lint and the repository gates: - -```sh -npm run test:unit --