From 153bb4d9a6b86666088b282f654d8e57c14bec1b Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Fri, 2 Oct 2026 20:32:48 +0200 Subject: [PATCH 1/6] Theme: Generate complete border token modes for Figma testing --- .gitignore | 1 + .../theme/bin/build-figma-tokens/generate.ts | 60 +++++++ .../theme/bin/build-figma-tokens/index.ts | 23 +++ .../build-figma-tokens/test/generate.test.ts | 161 ++++++++++++++++++ packages/theme/package.json | 1 + packages/theme/tokens/README.md | 48 +++++- 6 files changed, 291 insertions(+), 3 deletions(-) create mode 100644 packages/theme/bin/build-figma-tokens/generate.ts create mode 100644 packages/theme/bin/build-figma-tokens/index.ts create mode 100644 packages/theme/bin/build-figma-tokens/test/generate.test.ts diff --git a/.gitignore b/.gitignore index 140ec956b0cbbc..e3c51382df22b8 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ gutenberg.zip .content-entry.js coverage /.vitest-attachments/ +/packages/theme/figma/ /test-results/ **/__screenshots__/*.browser.test.*/ .phpunit.result.cache diff --git a/packages/theme/bin/build-figma-tokens/generate.ts b/packages/theme/bin/build-figma-tokens/generate.ts new file mode 100644 index 00000000000000..59df0b1be3c81a --- /dev/null +++ b/packages/theme/bin/build-figma-tokens/generate.ts @@ -0,0 +1,60 @@ +import type { Resolver } from '@terrazzo/parser'; + +/** + * Generate complete DTCG files for independent Figma border collections. + * + * @param resolver The parsed design token resolver. + * @return One import file per collection mode. + */ +export function generateFigmaFiles( resolver: Resolver ) { + const files: { filename: string; contents: string }[] = []; + + for ( const [ group, modifier ] of [ + [ 'radius', 'corner-radius' ], + [ 'width', 'pixel-density' ], + ] ) { + const contexts = resolver.source.modifiers?.[ modifier ]?.contexts; + if ( ! contexts ) { + throw new Error( + `Missing Figma collection modifier: ${ modifier }` + ); + } + + for ( const mode of Object.keys( contexts ) ) { + const tokens = resolver.apply( + { [ modifier ]: mode }, + { modifiers: [ modifier ], resolveAliases: false } + ); + const contents: Record< string, unknown > = {}; + + for ( const [ id, token ] of Object.entries( tokens ) ) { + if ( ! id.startsWith( `wpds-border.${ group }.` ) ) { + continue; + } + + // Keep the original hierarchy so Figma variable names still match. + const path = id.split( '.' ); + const name = path.pop()!; + let parent = contents; + for ( const segment of path ) { + parent[ segment ] ??= {}; + parent = parent[ segment ] as Record< string, unknown >; + } + parent[ name ] = { + $type: token.$type, + $value: token.$value, + $description: token.$description, + $extensions: token.$extensions, + $deprecated: token.$deprecated, + }; + } + + files.push( { + filename: `${ group }/${ mode }.json`, + contents: JSON.stringify( contents, null, '\t' ) + '\n', + } ); + } + } + + return files; +} diff --git a/packages/theme/bin/build-figma-tokens/index.ts b/packages/theme/bin/build-figma-tokens/index.ts new file mode 100644 index 00000000000000..94cf450f75338d --- /dev/null +++ b/packages/theme/bin/build-figma-tokens/index.ts @@ -0,0 +1,23 @@ +import { mkdir, readFile, writeFile } from 'node:fs/promises'; +import { dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { parse } from '@terrazzo/parser'; +import config from '../../terrazzo.config.ts'; +import { generateFigmaFiles } from './generate.ts'; + +const sources = await Promise.all( + config.tokens.map( async ( filename: URL ) => ( { + filename, + src: await readFile( filename, 'utf8' ), + } ) ) +); +const { resolver } = await parse( sources, { config } ); + +for ( const file of generateFigmaFiles( resolver ) ) { + const filename = new URL( + `../../figma/${ file.filename }`, + import.meta.url + ); + await mkdir( dirname( fileURLToPath( filename ) ), { recursive: true } ); + await writeFile( filename, file.contents ); +} diff --git a/packages/theme/bin/build-figma-tokens/test/generate.test.ts b/packages/theme/bin/build-figma-tokens/test/generate.test.ts new file mode 100644 index 00000000000000..ac6e5b8cfbdcf8 --- /dev/null +++ b/packages/theme/bin/build-figma-tokens/test/generate.test.ts @@ -0,0 +1,161 @@ +import { readFile } from 'node:fs/promises'; +import { beforeAll, describe, expect, it } from 'vitest'; +import { defineConfig, parse } from '@terrazzo/parser'; +import config from '../../../terrazzo.config.ts'; +import { generateFigmaFiles } from '../generate.ts'; + +describe( 'Figma border collection files', () => { + let files: ReturnType< typeof generateFigmaFiles >; + let border: Record< + string, + Record< + string, + { + $description?: string; + $extensions?: Record< string, unknown >; + } + > + >; + + beforeAll( async () => { + const sources = await Promise.all( + config.tokens.map( async ( filename: URL ) => ( { + filename, + src: await readFile( filename, 'utf8' ), + } ) ) + ); + const { resolver } = await parse( sources, { config } ); + files = generateFigmaFiles( resolver ); + border = JSON.parse( + await readFile( + new URL( '../../../tokens/border.json', import.meta.url ), + 'utf8' + ) + )[ 'wpds-border' ]; + } ); + + it( 'creates complete mode files with the original names, types, descriptions, and scopes', () => { + expect( files.map( ( file ) => file.filename ) ).toEqual( [ + 'radius/none.json', + 'radius/subtle.json', + 'radius/moderate.json', + 'radius/pronounced.json', + 'width/standard.json', + 'width/high-dpi.json', + ] ); + + for ( const file of files ) { + const group = file.filename.split( '/' )[ 0 ]; + const document = JSON.parse( file.contents ); + expect( Object.keys( document ) ).toEqual( [ 'wpds-border' ] ); + expect( Object.keys( document[ 'wpds-border' ] ) ).toEqual( [ + group, + ] ); + const tokens = document[ 'wpds-border' ][ group ]; + const names = Object.keys( border[ group ] ).filter( + ( name ) => ! name.startsWith( '$' ) + ); + expect( Object.keys( tokens ) ).toEqual( names ); + for ( const name of names ) { + expect( tokens[ name ] ).toEqual( { + $type: 'dimension', + $value: { value: expect.any( Number ), unit: 'px' }, + $description: border[ group ][ name ].$description, + $extensions: border[ group ][ name ].$extensions, + } ); + } + } + } ); + + it( 'keeps radius and width modes independent', () => { + const values = Object.fromEntries( + files.map( ( file ) => { + const group = file.filename.split( '/' )[ 0 ]; + const tokens: Record< string, { $value: { value: number } } > = + JSON.parse( file.contents )[ 'wpds-border' ][ group ]; + return [ + file.filename, + Object.values( tokens ).map( + ( token ) => token.$value.value + ), + ]; + } ) + ); + expect( values ).toEqual( { + 'radius/none.json': [ 0, 0, 0, 0, 0 ], + 'radius/subtle.json': [ 1, 2, 4, 8, 12 ], + 'radius/moderate.json': [ 6, 8, 12, 16, 20 ], + 'radius/pronounced.json': [ 18, 20, 22, 24, 26 ], + 'width/standard.json': [ 1, 2, 4, 8, 2 ], + 'width/high-dpi.json': [ 1, 2, 4, 8, 1.5 ], + } ); + } ); + + it( 'preserves token aliases within a collection', async () => { + const { resolver } = await parse( + [ + { + filename: new URL( + 'aliases.resolver.json', + import.meta.url + ), + src: { + version: '2025.10', + sets: { + base: { + sources: [ + { + 'wpds-border': { + $type: 'dimension', + radius: { + sm: { + $value: { + value: 2, + unit: 'px', + }, + }, + control: { + $value: '{wpds-border.radius.sm}', + }, + }, + }, + }, + ], + }, + }, + modifiers: { + 'corner-radius': { + default: 'subtle', + contexts: { subtle: [] }, + }, + 'pixel-density': { + default: 'standard', + contexts: { standard: [] }, + }, + }, + resolutionOrder: [ + { $ref: '#/sets/base' }, + { $ref: '#/modifiers/corner-radius' }, + { $ref: '#/modifiers/pixel-density' }, + ], + }, + }, + ], + { + config: defineConfig( + {}, + { cwd: new URL( './', import.meta.url ) } + ), + } + ); + const file = generateFigmaFiles( resolver ).find( + ( candidate ) => candidate.filename === 'radius/subtle.json' + )!; + expect( + JSON.parse( file.contents )[ 'wpds-border' ].radius.control + ).toEqual( { + $type: 'dimension', + $value: '{wpds-border.radius.sm}', + } ); + } ); +} ); diff --git a/packages/theme/package.json b/packages/theme/package.json index 0c119ebe46503c..997bd7472734f6 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -159,6 +159,7 @@ "scripts": { "build:default-ramps": "node bin/generate-default-ramps/index.ts", "build:tokens": "node bin/generate-primitive-tokens/index.ts && node bin/build-design-tokens/index.ts", + "build:figma": "node bin/build-figma-tokens/index.ts", "build": "npm run build:default-ramps && npm run build:tokens", "lint:package-contents": "wp-validate-package-contents . --disallow-path src", "postbuild": "prettier --write tokens/color.json tokens/modes prebuilt src/prebuilt src/color-ramps/lib/default-ramps.ts docs ../base-styles/internal/_wpds-token-fallbacks.scss" diff --git a/packages/theme/tokens/README.md b/packages/theme/tokens/README.md index 7a1cda332c9c48..11f2ebd372a229 100644 --- a/packages/theme/tokens/README.md +++ b/packages/theme/tokens/README.md @@ -72,8 +72,50 @@ The design tokens use [the `$extensions` feature](https://www.designtokens.org/t ### Figma Support -The tokens are implemented so that they can be imported directly into Figma variables, using [Figma's built-in support for importing design tokens](https://help.figma.com/hc/en-us/articles/15343816063383-Modes-for-variables#h_01KAGYPSFC984XDB4YWBCNRZJ7). +Figma can import supported DTCG token types as variables through its [built-in design token importer](https://help.figma.com/hc/en-us/articles/15343816063383-Modes-for-variables#h_01KAGYPSFC984XDB4YWBCNRZJ7). The resolver itself is not an import file. The files in `modes/` are partial resolver inputs, so they do not contain every variable needed to create a collection. -This also includes support for [variable modes](https://help.figma.com/hc/en-us/articles/15343816063383-Modes-for-variables). The contextual token values are stored under [the `modes/` directory](https://github.com/WordPress/gutenberg/blob/trunk/packages/theme/tokens/modes/) and composed by `wpds.resolver.json`. +#### Generate border collection samples -Token definitions will also include relevant Figma scopes, which are useful to ensure that token values are only shown in relevant fields in the Figma interface (e.g. border radius tokens only shown in the radius selection fields). These are implemented through the `$extensions['com.figma.scopes']` extension, and a full list of supported scopes is available in [Figma's `VariableScope` developer documentation](https://developers.figma.com/docs/plugins/api/VariableScope/). +From the repository root, run: + +```sh +npm run --workspace @wordpress/theme build:figma +``` + +This command applies `wpds.resolver.json` to generate six complete DTCG files under `packages/theme/figma/`. Radius and width use separate collections so designers can choose corner radius and pixel density independently. + +| Collection | Files relative to `packages/theme/figma/` | Default mode | Variables | +| ------------- | ------------------------------------------------------------------------------------------ | ------------ | --------------------------------------- | +| Border radius | `radius/none.json`, `radius/subtle.json`, `radius/moderate.json`, `radius/pronounced.json` | `subtle` | `wpds-border/radius/{xs,sm,md,lg,xl}` | +| Border width | `width/standard.json`, `width/high-dpi.json` | `standard` | `wpds-border/width/{xs,sm,md,lg,focus}` | + +Each file includes every token in its collection, including unchanged values, descriptions, types, and Figma scopes. The original hierarchy preserves variable names. Token aliases remain references instead of becoming literal values. The current border tokens contain no aliases; aliases within a collection have automated coverage, but Figma alias behavior still needs testing. + +These files are local, ignored samples, generated on demand rather than during the package build. Do not edit them as sources. `wpds.resolver.json`, its base sources, and the tracked files in `modes/` remain canonical. Keep the tracked mode files until the Figma tests below establish a compatible replacement and the artifact policy is agreed in [#82561](https://github.com/WordPress/gutenberg/issues/82561). + +#### Test in a Figma library branch + +This is a proposed workflow awaiting Figma validation. Record the results in #82561, including the library branch, tested revision, collection and mode names, and any importer differences. + +Expected pixel values: + +| Mode | `xs` | `sm` | `md` | `lg` | `xl` or `focus` | +| ------------------- | ---- | ---- | ---- | ---- | --------------- | +| Radius `none` | 0 | 0 | 0 | 0 | 0 | +| Radius `subtle` | 1 | 2 | 4 | 8 | 12 | +| Radius `moderate` | 6 | 8 | 12 | 16 | 20 | +| Radius `pronounced` | 18 | 20 | 22 | 24 | 26 | +| Width `standard` | 1 | 2 | 4 | 8 | 2 | +| Width `high-dpi` | 1 | 2 | 4 | 8 | 1.5 | + +1. Create a branch of the theme library. In the Variables view, create the Border radius collection and drag in all four `radius/` files together. Repeat with the two `width/` files in Border width. Check mode names against the filenames, rename them if needed, and set `subtle` and `standard` as the defaults. +2. Check that each collection has five Number variables with the names above. Radius variables must have the `CORNER_RADIUS` scope and width variables the `STROKE_FLOAT` scope. Check descriptions and the pixel values in the table. +3. Bind a sample component's corner radius and stroke width to the new variables. Switch both collections' modes independently on an object or page. High-DPI is an explicit preview choice in Figma; the generated CSS selects it automatically through a resolution media query. +4. Change one existing value in a local source, regenerate the samples, and right-click the corresponding Figma mode to select **Import mode**. Check that the matching variable updates and retains its bindings. Restore the source and import the original value again. +5. Add a temporary token to a local border source and regenerate. Confirm that importing into an existing mode does not create the new variable. Use the Figma API/plugin workflow to create it with the matching name, Number type, description, scope, and values for every mode, then verify later imports update it. Remove the temporary token after testing. This importer limitation was [reported by the theme library maintainer](https://github.com/WordPress/gutenberg/issues/82561#issuecomment-5799294192). +6. Add a temporary alias to another token within the same collection, regenerate, and test both creating a collection and updating a mode. Confirm that Figma preserves the alias relationship, then remove the temporary alias. Cross-collection aliases are outside these samples; they need Figma's `com.figma.aliasData` extension and a separate validation. +7. Inspect components bound to the existing combined Border collection. Record their variable IDs and bindings before testing the split. Verify a binding migration in the library branch, including component instances and existing mode selections. Matching names alone do not establish that bindings survive. Keep the existing collection until this migration is verified. + +Figma creates new variables only for supported tokens present with the same type in every imported file. Importing an existing mode updates variables with matching names and types. See [Figma's import documentation](https://help.figma.com/hc/en-us/articles/15343816063383-Modes-for-variables#h_01KAGYPSFC984XDB4YWBCNRZJ7) for those constraints and [the separate typography issue](https://github.com/WordPress/gutenberg/issues/74620) for unsupported typography types. The generated samples cover only border radius and width. + +Token definitions include Figma scopes to show variables in the relevant fields, such as corner radius. These use the `$extensions['com.figma.scopes']` extension. See [Figma's `VariableScope` developer documentation](https://developers.figma.com/docs/plugins/api/VariableScope/) for the supported scopes. From 0ca2ab2d27f2d941de2ade08a27712cffa69bc4d Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Fri, 2 Oct 2026 20:34:47 +0200 Subject: [PATCH 2/6] Theme: Add changelog entry for Figma import samples --- packages/theme/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/theme/CHANGELOG.md b/packages/theme/CHANGELOG.md index 21f350b5072b6c..d09df8d722bf2b 100644 --- a/packages/theme/CHANGELOG.md +++ b/packages/theme/CHANGELOG.md @@ -15,6 +15,7 @@ ### Internal +- Generate complete border radius and width token samples for Figma mode imports. ([#84035](https://github.com/WordPress/gutenberg/pull/84035)) - Update the Vite development dependency to 8.3.2 ([#83992](https://github.com/WordPress/gutenberg/pull/83992)). - Update the Vite development dependency to 8.3.1. ([#83569](https://github.com/WordPress/gutenberg/pull/83569)) - Restore color-scale Storybook accessibility checks with labeled color samples, readable seed labels, and keyboard scrolling ([#83358](https://github.com/WordPress/gutenberg/pull/83358)). From cc9d33d5a07e9eab615b04af985b017ebcdf1f47 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Tue, 6 Oct 2026 12:51:03 +0200 Subject: [PATCH 3/6] Theme: Commit validated Figma mode artifacts and build them by default --- .gitignore | 1 - packages/theme/CHANGELOG.md | 2 +- packages/theme/README.md | 3 +- .../theme/bin/build-figma-tokens/index.ts | 2 +- packages/theme/package.json | 3 +- .../theme/prebuilt/figma/radius/moderate.json | 61 +++++++++++++++++++ .../theme/prebuilt/figma/radius/none.json | 61 +++++++++++++++++++ .../prebuilt/figma/radius/pronounced.json | 61 +++++++++++++++++++ .../theme/prebuilt/figma/radius/subtle.json | 61 +++++++++++++++++++ .../theme/prebuilt/figma/width/high-dpi.json | 61 +++++++++++++++++++ .../theme/prebuilt/figma/width/standard.json | 61 +++++++++++++++++++ packages/theme/tokens/README.md | 24 ++++---- 12 files changed, 385 insertions(+), 16 deletions(-) create mode 100644 packages/theme/prebuilt/figma/radius/moderate.json create mode 100644 packages/theme/prebuilt/figma/radius/none.json create mode 100644 packages/theme/prebuilt/figma/radius/pronounced.json create mode 100644 packages/theme/prebuilt/figma/radius/subtle.json create mode 100644 packages/theme/prebuilt/figma/width/high-dpi.json create mode 100644 packages/theme/prebuilt/figma/width/standard.json diff --git a/.gitignore b/.gitignore index e3c51382df22b8..140ec956b0cbbc 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,6 @@ gutenberg.zip .content-entry.js coverage /.vitest-attachments/ -/packages/theme/figma/ /test-results/ **/__screenshots__/*.browser.test.*/ .phpunit.result.cache diff --git a/packages/theme/CHANGELOG.md b/packages/theme/CHANGELOG.md index d09df8d722bf2b..e8de6306ff5000 100644 --- a/packages/theme/CHANGELOG.md +++ b/packages/theme/CHANGELOG.md @@ -15,7 +15,7 @@ ### Internal -- Generate complete border radius and width token samples for Figma mode imports. ([#84035](https://github.com/WordPress/gutenberg/pull/84035)) +- Generate and commit complete border radius and width tokens for Figma mode imports during the theme build. ([#84035](https://github.com/WordPress/gutenberg/pull/84035)) - Update the Vite development dependency to 8.3.2 ([#83992](https://github.com/WordPress/gutenberg/pull/83992)). - Update the Vite development dependency to 8.3.1. ([#83569](https://github.com/WordPress/gutenberg/pull/83569)) - Restore color-scale Storybook accessibility checks with labeled color samples, readable seed labels, and keyboard scrolling ([#83358](https://github.com/WordPress/gutenberg/pull/83358)). diff --git a/packages/theme/README.md b/packages/theme/README.md index 7ec6902b1c8b66..6f88a13b9ac9fe 100644 --- a/packages/theme/README.md +++ b/packages/theme/README.md @@ -277,7 +277,8 @@ This step will: 2. Generate primitive tokens. 3. Build CSS and JavaScript token files. 4. Update the design tokens documentation. -5. Format all generated files. +5. Generate complete Figma border collection mode files in `prebuilt/figma`. +6. Format all generated files. The files generated in this step will all be committed to the repo. diff --git a/packages/theme/bin/build-figma-tokens/index.ts b/packages/theme/bin/build-figma-tokens/index.ts index 94cf450f75338d..5b3fd16c1183ad 100644 --- a/packages/theme/bin/build-figma-tokens/index.ts +++ b/packages/theme/bin/build-figma-tokens/index.ts @@ -15,7 +15,7 @@ const { resolver } = await parse( sources, { config } ); for ( const file of generateFigmaFiles( resolver ) ) { const filename = new URL( - `../../figma/${ file.filename }`, + `../../prebuilt/figma/${ file.filename }`, import.meta.url ); await mkdir( dirname( fileURLToPath( filename ) ), { recursive: true } ); diff --git a/packages/theme/package.json b/packages/theme/package.json index 997bd7472734f6..6565609711b7f2 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -160,7 +160,8 @@ "build:default-ramps": "node bin/generate-default-ramps/index.ts", "build:tokens": "node bin/generate-primitive-tokens/index.ts && node bin/build-design-tokens/index.ts", "build:figma": "node bin/build-figma-tokens/index.ts", - "build": "npm run build:default-ramps && npm run build:tokens", + "postbuild:figma": "prettier --write prebuilt/figma", + "build": "npm run build:default-ramps && npm run build:tokens && npm run build:figma", "lint:package-contents": "wp-validate-package-contents . --disallow-path src", "postbuild": "prettier --write tokens/color.json tokens/modes prebuilt src/prebuilt src/color-ramps/lib/default-ramps.ts docs ../base-styles/internal/_wpds-token-fallbacks.scss" } diff --git a/packages/theme/prebuilt/figma/radius/moderate.json b/packages/theme/prebuilt/figma/radius/moderate.json new file mode 100644 index 00000000000000..5a83d0f1c3c829 --- /dev/null +++ b/packages/theme/prebuilt/figma/radius/moderate.json @@ -0,0 +1,61 @@ +{ + "wpds-border": { + "radius": { + "xs": { + "$type": "dimension", + "$value": { + "value": 6, + "unit": "px" + }, + "$description": "Buttons and other elements nested inside controls.", + "$extensions": { + "com.figma.scopes": [ "CORNER_RADIUS" ] + } + }, + "sm": { + "$type": "dimension", + "$value": { + "value": 8, + "unit": "px" + }, + "$description": "Standalone buttons, inputs, and compact controls.", + "$extensions": { + "com.figma.scopes": [ "CORNER_RADIUS" ] + } + }, + "md": { + "$type": "dimension", + "$value": { + "value": 12, + "unit": "px" + }, + "$description": "Menus, popovers, and other small portaled overlays.", + "$extensions": { + "com.figma.scopes": [ "CORNER_RADIUS" ] + } + }, + "lg": { + "$type": "dimension", + "$value": { + "value": 16, + "unit": "px" + }, + "$description": "Cards, dialogs, notices, and other larger content containers.", + "$extensions": { + "com.figma.scopes": [ "CORNER_RADIUS" ] + } + }, + "xl": { + "$type": "dimension", + "$value": { + "value": 20, + "unit": "px" + }, + "$description": "Page and app shell surfaces.", + "$extensions": { + "com.figma.scopes": [ "CORNER_RADIUS" ] + } + } + } + } +} diff --git a/packages/theme/prebuilt/figma/radius/none.json b/packages/theme/prebuilt/figma/radius/none.json new file mode 100644 index 00000000000000..162e697101278e --- /dev/null +++ b/packages/theme/prebuilt/figma/radius/none.json @@ -0,0 +1,61 @@ +{ + "wpds-border": { + "radius": { + "xs": { + "$type": "dimension", + "$value": { + "value": 0, + "unit": "px" + }, + "$description": "Buttons and other elements nested inside controls.", + "$extensions": { + "com.figma.scopes": [ "CORNER_RADIUS" ] + } + }, + "sm": { + "$type": "dimension", + "$value": { + "value": 0, + "unit": "px" + }, + "$description": "Standalone buttons, inputs, and compact controls.", + "$extensions": { + "com.figma.scopes": [ "CORNER_RADIUS" ] + } + }, + "md": { + "$type": "dimension", + "$value": { + "value": 0, + "unit": "px" + }, + "$description": "Menus, popovers, and other small portaled overlays.", + "$extensions": { + "com.figma.scopes": [ "CORNER_RADIUS" ] + } + }, + "lg": { + "$type": "dimension", + "$value": { + "value": 0, + "unit": "px" + }, + "$description": "Cards, dialogs, notices, and other larger content containers.", + "$extensions": { + "com.figma.scopes": [ "CORNER_RADIUS" ] + } + }, + "xl": { + "$type": "dimension", + "$value": { + "value": 0, + "unit": "px" + }, + "$description": "Page and app shell surfaces.", + "$extensions": { + "com.figma.scopes": [ "CORNER_RADIUS" ] + } + } + } + } +} diff --git a/packages/theme/prebuilt/figma/radius/pronounced.json b/packages/theme/prebuilt/figma/radius/pronounced.json new file mode 100644 index 00000000000000..701c9698cb9052 --- /dev/null +++ b/packages/theme/prebuilt/figma/radius/pronounced.json @@ -0,0 +1,61 @@ +{ + "wpds-border": { + "radius": { + "xs": { + "$type": "dimension", + "$value": { + "value": 18, + "unit": "px" + }, + "$description": "Buttons and other elements nested inside controls.", + "$extensions": { + "com.figma.scopes": [ "CORNER_RADIUS" ] + } + }, + "sm": { + "$type": "dimension", + "$value": { + "value": 20, + "unit": "px" + }, + "$description": "Standalone buttons, inputs, and compact controls.", + "$extensions": { + "com.figma.scopes": [ "CORNER_RADIUS" ] + } + }, + "md": { + "$type": "dimension", + "$value": { + "value": 22, + "unit": "px" + }, + "$description": "Menus, popovers, and other small portaled overlays.", + "$extensions": { + "com.figma.scopes": [ "CORNER_RADIUS" ] + } + }, + "lg": { + "$type": "dimension", + "$value": { + "value": 24, + "unit": "px" + }, + "$description": "Cards, dialogs, notices, and other larger content containers.", + "$extensions": { + "com.figma.scopes": [ "CORNER_RADIUS" ] + } + }, + "xl": { + "$type": "dimension", + "$value": { + "value": 26, + "unit": "px" + }, + "$description": "Page and app shell surfaces.", + "$extensions": { + "com.figma.scopes": [ "CORNER_RADIUS" ] + } + } + } + } +} diff --git a/packages/theme/prebuilt/figma/radius/subtle.json b/packages/theme/prebuilt/figma/radius/subtle.json new file mode 100644 index 00000000000000..3d9efc4185f0aa --- /dev/null +++ b/packages/theme/prebuilt/figma/radius/subtle.json @@ -0,0 +1,61 @@ +{ + "wpds-border": { + "radius": { + "xs": { + "$type": "dimension", + "$value": { + "value": 1, + "unit": "px" + }, + "$description": "Buttons and other elements nested inside controls.", + "$extensions": { + "com.figma.scopes": [ "CORNER_RADIUS" ] + } + }, + "sm": { + "$type": "dimension", + "$value": { + "value": 2, + "unit": "px" + }, + "$description": "Standalone buttons, inputs, and compact controls.", + "$extensions": { + "com.figma.scopes": [ "CORNER_RADIUS" ] + } + }, + "md": { + "$type": "dimension", + "$value": { + "value": 4, + "unit": "px" + }, + "$description": "Menus, popovers, and other small portaled overlays.", + "$extensions": { + "com.figma.scopes": [ "CORNER_RADIUS" ] + } + }, + "lg": { + "$type": "dimension", + "$value": { + "value": 8, + "unit": "px" + }, + "$description": "Cards, dialogs, notices, and other larger content containers.", + "$extensions": { + "com.figma.scopes": [ "CORNER_RADIUS" ] + } + }, + "xl": { + "$type": "dimension", + "$value": { + "value": 12, + "unit": "px" + }, + "$description": "Page and app shell surfaces.", + "$extensions": { + "com.figma.scopes": [ "CORNER_RADIUS" ] + } + } + } + } +} diff --git a/packages/theme/prebuilt/figma/width/high-dpi.json b/packages/theme/prebuilt/figma/width/high-dpi.json new file mode 100644 index 00000000000000..06d2a898c11014 --- /dev/null +++ b/packages/theme/prebuilt/figma/width/high-dpi.json @@ -0,0 +1,61 @@ +{ + "wpds-border": { + "width": { + "xs": { + "$type": "dimension", + "$value": { + "value": 1, + "unit": "px" + }, + "$description": "Extra small width", + "$extensions": { + "com.figma.scopes": [ "STROKE_FLOAT" ] + } + }, + "sm": { + "$type": "dimension", + "$value": { + "value": 2, + "unit": "px" + }, + "$description": "Small width", + "$extensions": { + "com.figma.scopes": [ "STROKE_FLOAT" ] + } + }, + "md": { + "$type": "dimension", + "$value": { + "value": 4, + "unit": "px" + }, + "$description": "Medium width", + "$extensions": { + "com.figma.scopes": [ "STROKE_FLOAT" ] + } + }, + "lg": { + "$type": "dimension", + "$value": { + "value": 8, + "unit": "px" + }, + "$description": "Large width", + "$extensions": { + "com.figma.scopes": [ "STROKE_FLOAT" ] + } + }, + "focus": { + "$type": "dimension", + "$value": { + "value": 1.5, + "unit": "px" + }, + "$description": "Border width for focus ring", + "$extensions": { + "com.figma.scopes": [ "STROKE_FLOAT" ] + } + } + } + } +} diff --git a/packages/theme/prebuilt/figma/width/standard.json b/packages/theme/prebuilt/figma/width/standard.json new file mode 100644 index 00000000000000..8f41876e29518f --- /dev/null +++ b/packages/theme/prebuilt/figma/width/standard.json @@ -0,0 +1,61 @@ +{ + "wpds-border": { + "width": { + "xs": { + "$type": "dimension", + "$value": { + "value": 1, + "unit": "px" + }, + "$description": "Extra small width", + "$extensions": { + "com.figma.scopes": [ "STROKE_FLOAT" ] + } + }, + "sm": { + "$type": "dimension", + "$value": { + "value": 2, + "unit": "px" + }, + "$description": "Small width", + "$extensions": { + "com.figma.scopes": [ "STROKE_FLOAT" ] + } + }, + "md": { + "$type": "dimension", + "$value": { + "value": 4, + "unit": "px" + }, + "$description": "Medium width", + "$extensions": { + "com.figma.scopes": [ "STROKE_FLOAT" ] + } + }, + "lg": { + "$type": "dimension", + "$value": { + "value": 8, + "unit": "px" + }, + "$description": "Large width", + "$extensions": { + "com.figma.scopes": [ "STROKE_FLOAT" ] + } + }, + "focus": { + "$type": "dimension", + "$value": { + "value": 2, + "unit": "px" + }, + "$description": "Border width for focus ring", + "$extensions": { + "com.figma.scopes": [ "STROKE_FLOAT" ] + } + } + } + } +} diff --git a/packages/theme/tokens/README.md b/packages/theme/tokens/README.md index 11f2ebd372a229..8e5ee1f9ddfd3f 100644 --- a/packages/theme/tokens/README.md +++ b/packages/theme/tokens/README.md @@ -74,28 +74,30 @@ The design tokens use [the `$extensions` feature](https://www.designtokens.org/t Figma can import supported DTCG token types as variables through its [built-in design token importer](https://help.figma.com/hc/en-us/articles/15343816063383-Modes-for-variables#h_01KAGYPSFC984XDB4YWBCNRZJ7). The resolver itself is not an import file. The files in `modes/` are partial resolver inputs, so they do not contain every variable needed to create a collection. -#### Generate border collection samples +#### Generate border collection files -From the repository root, run: +The normal theme build generates and formats the committed Figma files. To regenerate only those files, run from the repository root: ```sh npm run --workspace @wordpress/theme build:figma ``` -This command applies `wpds.resolver.json` to generate six complete DTCG files under `packages/theme/figma/`. Radius and width use separate collections so designers can choose corner radius and pixel density independently. +This command applies `wpds.resolver.json` to generate six complete DTCG files under `packages/theme/prebuilt/figma/`. Radius and width use separate collections so designers can choose corner radius and pixel density independently. -| Collection | Files relative to `packages/theme/figma/` | Default mode | Variables | +| Collection | Files relative to `packages/theme/prebuilt/figma/` | Default mode | Variables | | ------------- | ------------------------------------------------------------------------------------------ | ------------ | --------------------------------------- | | Border radius | `radius/none.json`, `radius/subtle.json`, `radius/moderate.json`, `radius/pronounced.json` | `subtle` | `wpds-border/radius/{xs,sm,md,lg,xl}` | | Border width | `width/standard.json`, `width/high-dpi.json` | `standard` | `wpds-border/width/{xs,sm,md,lg,focus}` | -Each file includes every token in its collection, including unchanged values, descriptions, types, and Figma scopes. The original hierarchy preserves variable names. Token aliases remain references instead of becoming literal values. The current border tokens contain no aliases; aliases within a collection have automated coverage, but Figma alias behavior still needs testing. +Each file includes every token in its collection, including unchanged values, descriptions, types, and Figma scopes. The original hierarchy preserves variable names. Token aliases remain references instead of becoming literal values. The current border tokens contain no aliases. Aliases within a collection have automated coverage and were verified in Figma for both collection creation and mode updates. -These files are local, ignored samples, generated on demand rather than during the package build. Do not edit them as sources. `wpds.resolver.json`, its base sources, and the tracked files in `modes/` remain canonical. Keep the tracked mode files until the Figma tests below establish a compatible replacement and the artifact policy is agreed in [#82561](https://github.com/WordPress/gutenberg/issues/82561). +These generated files are committed under `prebuilt/figma` and included in the npm package. Do not edit them as sources. `wpds.resolver.json`, its base sources, and the tracked files in `modes/` remain canonical inputs for both CSS and Figma generation. #### Test in a Figma library branch -This is a proposed workflow awaiting Figma validation. Record the results in #82561, including the library branch, tested revision, collection and mode names, and any importer differences. +The theme library maintainer [tested this workflow in a Figma library branch](https://github.com/WordPress/gutenberg/pull/84035#issuecomment-6003936288) using the files from revision `0ca2ab2`. Collection creation preserved names, types, descriptions, scopes, values, and aliases. Radius and width modes switched independently. Existing-value updates preserved variable IDs and bindings. New variables required creation through the API/plugin before their values could be imported. + +Files imported together were ordered alphabetically, so set `subtle` and `standard` as the default modes explicitly. Rebinding a test component propagated to its instance, but the instance's existing `pronounced` selection remained attached to the old collection. Reapply mode selections from the new collections after rebinding. This test does not establish that the full library has been migrated. Use the following checks for future changes and record the results in [#82561](https://github.com/WordPress/gutenberg/issues/82561). Expected pixel values: @@ -111,11 +113,11 @@ Expected pixel values: 1. Create a branch of the theme library. In the Variables view, create the Border radius collection and drag in all four `radius/` files together. Repeat with the two `width/` files in Border width. Check mode names against the filenames, rename them if needed, and set `subtle` and `standard` as the defaults. 2. Check that each collection has five Number variables with the names above. Radius variables must have the `CORNER_RADIUS` scope and width variables the `STROKE_FLOAT` scope. Check descriptions and the pixel values in the table. 3. Bind a sample component's corner radius and stroke width to the new variables. Switch both collections' modes independently on an object or page. High-DPI is an explicit preview choice in Figma; the generated CSS selects it automatically through a resolution media query. -4. Change one existing value in a local source, regenerate the samples, and right-click the corresponding Figma mode to select **Import mode**. Check that the matching variable updates and retains its bindings. Restore the source and import the original value again. +4. Change one existing value in a local source, regenerate the files, and right-click the corresponding Figma mode to select **Import mode**. Check that the matching variable updates and retains its bindings. Restore the source and import the original value again. 5. Add a temporary token to a local border source and regenerate. Confirm that importing into an existing mode does not create the new variable. Use the Figma API/plugin workflow to create it with the matching name, Number type, description, scope, and values for every mode, then verify later imports update it. Remove the temporary token after testing. This importer limitation was [reported by the theme library maintainer](https://github.com/WordPress/gutenberg/issues/82561#issuecomment-5799294192). -6. Add a temporary alias to another token within the same collection, regenerate, and test both creating a collection and updating a mode. Confirm that Figma preserves the alias relationship, then remove the temporary alias. Cross-collection aliases are outside these samples; they need Figma's `com.figma.aliasData` extension and a separate validation. -7. Inspect components bound to the existing combined Border collection. Record their variable IDs and bindings before testing the split. Verify a binding migration in the library branch, including component instances and existing mode selections. Matching names alone do not establish that bindings survive. Keep the existing collection until this migration is verified. +6. Add a temporary alias to another token within the same collection, regenerate, and test both creating a collection and updating a mode. Confirm that Figma preserves the alias relationship, then remove the temporary alias. Cross-collection aliases are outside these files; they need Figma's `com.figma.aliasData` extension and a separate validation. +7. Inspect components bound to the existing combined Border collection. Record their variable IDs and bindings before testing the split. Rebind components in the library branch and check that their instances use the new variables. Reapply existing mode selections from the new collections on affected objects and pages, then verify the intended values. Matching names alone do not preserve bindings or mode selections. Keep the existing collection until all affected bindings and mode selections have been checked. -Figma creates new variables only for supported tokens present with the same type in every imported file. Importing an existing mode updates variables with matching names and types. See [Figma's import documentation](https://help.figma.com/hc/en-us/articles/15343816063383-Modes-for-variables#h_01KAGYPSFC984XDB4YWBCNRZJ7) for those constraints and [the separate typography issue](https://github.com/WordPress/gutenberg/issues/74620) for unsupported typography types. The generated samples cover only border radius and width. +Figma creates new variables only for supported tokens present with the same type in every imported file. Importing an existing mode updates variables with matching names and types. See [Figma's import documentation](https://help.figma.com/hc/en-us/articles/15343816063383-Modes-for-variables#h_01KAGYPSFC984XDB4YWBCNRZJ7) for those constraints and [the separate typography issue](https://github.com/WordPress/gutenberg/issues/74620) for unsupported typography types. The generated files cover only border radius and width. Token definitions include Figma scopes to show variables in the relevant fields, such as corner radius. These use the `$extensions['com.figma.scopes']` extension. See [Figma's `VariableScope` developer documentation](https://developers.figma.com/docs/plugins/api/VariableScope/) for the supported scopes. From d399f096ba9b8934ef97d0b9ee9d003244d47179 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Tue, 6 Oct 2026 14:31:06 +0200 Subject: [PATCH 4/6] Theme: Remove redundant Figma generator tests --- .../build-figma-tokens/test/generate.test.ts | 94 +------------------ packages/theme/tokens/README.md | 2 +- 2 files changed, 2 insertions(+), 94 deletions(-) diff --git a/packages/theme/bin/build-figma-tokens/test/generate.test.ts b/packages/theme/bin/build-figma-tokens/test/generate.test.ts index ac6e5b8cfbdcf8..7430f804d4a4e1 100644 --- a/packages/theme/bin/build-figma-tokens/test/generate.test.ts +++ b/packages/theme/bin/build-figma-tokens/test/generate.test.ts @@ -1,6 +1,6 @@ import { readFile } from 'node:fs/promises'; import { beforeAll, describe, expect, it } from 'vitest'; -import { defineConfig, parse } from '@terrazzo/parser'; +import { parse } from '@terrazzo/parser'; import config from '../../../terrazzo.config.ts'; import { generateFigmaFiles } from '../generate.ts'; @@ -66,96 +66,4 @@ describe( 'Figma border collection files', () => { } } } ); - - it( 'keeps radius and width modes independent', () => { - const values = Object.fromEntries( - files.map( ( file ) => { - const group = file.filename.split( '/' )[ 0 ]; - const tokens: Record< string, { $value: { value: number } } > = - JSON.parse( file.contents )[ 'wpds-border' ][ group ]; - return [ - file.filename, - Object.values( tokens ).map( - ( token ) => token.$value.value - ), - ]; - } ) - ); - expect( values ).toEqual( { - 'radius/none.json': [ 0, 0, 0, 0, 0 ], - 'radius/subtle.json': [ 1, 2, 4, 8, 12 ], - 'radius/moderate.json': [ 6, 8, 12, 16, 20 ], - 'radius/pronounced.json': [ 18, 20, 22, 24, 26 ], - 'width/standard.json': [ 1, 2, 4, 8, 2 ], - 'width/high-dpi.json': [ 1, 2, 4, 8, 1.5 ], - } ); - } ); - - it( 'preserves token aliases within a collection', async () => { - const { resolver } = await parse( - [ - { - filename: new URL( - 'aliases.resolver.json', - import.meta.url - ), - src: { - version: '2025.10', - sets: { - base: { - sources: [ - { - 'wpds-border': { - $type: 'dimension', - radius: { - sm: { - $value: { - value: 2, - unit: 'px', - }, - }, - control: { - $value: '{wpds-border.radius.sm}', - }, - }, - }, - }, - ], - }, - }, - modifiers: { - 'corner-radius': { - default: 'subtle', - contexts: { subtle: [] }, - }, - 'pixel-density': { - default: 'standard', - contexts: { standard: [] }, - }, - }, - resolutionOrder: [ - { $ref: '#/sets/base' }, - { $ref: '#/modifiers/corner-radius' }, - { $ref: '#/modifiers/pixel-density' }, - ], - }, - }, - ], - { - config: defineConfig( - {}, - { cwd: new URL( './', import.meta.url ) } - ), - } - ); - const file = generateFigmaFiles( resolver ).find( - ( candidate ) => candidate.filename === 'radius/subtle.json' - )!; - expect( - JSON.parse( file.contents )[ 'wpds-border' ].radius.control - ).toEqual( { - $type: 'dimension', - $value: '{wpds-border.radius.sm}', - } ); - } ); } ); diff --git a/packages/theme/tokens/README.md b/packages/theme/tokens/README.md index 8e5ee1f9ddfd3f..889110b83fd2c8 100644 --- a/packages/theme/tokens/README.md +++ b/packages/theme/tokens/README.md @@ -89,7 +89,7 @@ This command applies `wpds.resolver.json` to generate six complete DTCG files un | Border radius | `radius/none.json`, `radius/subtle.json`, `radius/moderate.json`, `radius/pronounced.json` | `subtle` | `wpds-border/radius/{xs,sm,md,lg,xl}` | | Border width | `width/standard.json`, `width/high-dpi.json` | `standard` | `wpds-border/width/{xs,sm,md,lg,focus}` | -Each file includes every token in its collection, including unchanged values, descriptions, types, and Figma scopes. The original hierarchy preserves variable names. Token aliases remain references instead of becoming literal values. The current border tokens contain no aliases. Aliases within a collection have automated coverage and were verified in Figma for both collection creation and mode updates. +Each file includes every token in its collection, including unchanged values, descriptions, types, and Figma scopes. The original hierarchy preserves variable names. Token aliases remain references instead of becoming literal values. The current border tokens contain no aliases. Aliases within a collection were verified in Figma for both collection creation and mode updates. These generated files are committed under `prebuilt/figma` and included in the npm package. Do not edit them as sources. `wpds.resolver.json`, its base sources, and the tracked files in `modes/` remain canonical inputs for both CSS and Figma generation. From 4f0d654ea5b765f4df9de50a3f65ad44b588520c Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Tue, 6 Oct 2026 14:40:12 +0200 Subject: [PATCH 5/6] Theme: Format Figma artifacts in the shared postbuild step --- packages/theme/package.json | 1 - packages/theme/tokens/README.md | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/theme/package.json b/packages/theme/package.json index 6565609711b7f2..991ecbaf7fbf59 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -160,7 +160,6 @@ "build:default-ramps": "node bin/generate-default-ramps/index.ts", "build:tokens": "node bin/generate-primitive-tokens/index.ts && node bin/build-design-tokens/index.ts", "build:figma": "node bin/build-figma-tokens/index.ts", - "postbuild:figma": "prettier --write prebuilt/figma", "build": "npm run build:default-ramps && npm run build:tokens && npm run build:figma", "lint:package-contents": "wp-validate-package-contents . --disallow-path src", "postbuild": "prettier --write tokens/color.json tokens/modes prebuilt src/prebuilt src/color-ramps/lib/default-ramps.ts docs ../base-styles/internal/_wpds-token-fallbacks.scss" diff --git a/packages/theme/tokens/README.md b/packages/theme/tokens/README.md index 889110b83fd2c8..ef0853cfba3a8a 100644 --- a/packages/theme/tokens/README.md +++ b/packages/theme/tokens/README.md @@ -76,13 +76,14 @@ Figma can import supported DTCG token types as variables through its [built-in d #### Generate border collection files -The normal theme build generates and formats the committed Figma files. To regenerate only those files, run from the repository root: +The normal theme build generates the committed Figma files and formats all generated artifacts in the shared `postbuild` step. For targeted Figma generation, run the generator followed by that formatting step from the repository root: ```sh npm run --workspace @wordpress/theme build:figma +npm run --workspace @wordpress/theme postbuild ``` -This command applies `wpds.resolver.json` to generate six complete DTCG files under `packages/theme/prebuilt/figma/`. Radius and width use separate collections so designers can choose corner radius and pixel density independently. +The generator applies `wpds.resolver.json` to produce six complete DTCG files under `packages/theme/prebuilt/figma/`. Radius and width use separate collections so designers can choose corner radius and pixel density independently. | Collection | Files relative to `packages/theme/prebuilt/figma/` | Default mode | Variables | | ------------- | ------------------------------------------------------------------------------------------ | ------------ | --------------------------------------- | From 206948c6cfa0f282736431921bb438f8bbcee70b Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Tue, 6 Oct 2026 14:46:04 +0200 Subject: [PATCH 6/6] Theme: Document the complete build for Figma artifacts --- packages/theme/tokens/README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/theme/tokens/README.md b/packages/theme/tokens/README.md index ef0853cfba3a8a..0b45b75fe3d3b0 100644 --- a/packages/theme/tokens/README.md +++ b/packages/theme/tokens/README.md @@ -76,11 +76,10 @@ Figma can import supported DTCG token types as variables through its [built-in d #### Generate border collection files -The normal theme build generates the committed Figma files and formats all generated artifacts in the shared `postbuild` step. For targeted Figma generation, run the generator followed by that formatting step from the repository root: +Run the theme build from the repository root to generate and format the committed artifacts, including the Figma files: ```sh -npm run --workspace @wordpress/theme build:figma -npm run --workspace @wordpress/theme postbuild +npm run --workspace @wordpress/theme build ``` The generator applies `wpds.resolver.json` to produce six complete DTCG files under `packages/theme/prebuilt/figma/`. Radius and width use separate collections so designers can choose corner radius and pixel density independently.