diff --git a/.eslintrc.js b/.eslintrc.js index 402c16ec5c80fb..720e04e814752d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -14,6 +14,8 @@ const developmentFiles = [ '**/@(__mocks__|__tests__|test)/**/*.[tj]s?(x)', '**/@(storybook|stories)/**/*.[tj]s?(x)', 'packages/babel-preset-default/bin/**/*.js', + 'packages/theme/bin/**/*.[tj]s?(x)', + 'packages/theme/terrazzo.config.ts', ]; // All files from packages that have types provided with TypeScript. @@ -430,7 +432,12 @@ module.exports = { }, }, { - files: [ 'bin/**/*.js', 'bin/**/*.mjs', 'packages/env/**' ], + files: [ + 'bin/**/*.js', + 'bin/**/*.mjs', + 'packages/env/**', + 'packages/theme/bin/**/*.[tj]s?(x)', + ], rules: { 'no-console': 'off', }, diff --git a/bin/packages/build.mjs b/bin/packages/build.mjs index 181a02943c8302..e92cd5fda4b07c 100644 --- a/bin/packages/build.mjs +++ b/bin/packages/build.mjs @@ -16,6 +16,7 @@ import chokidar from 'chokidar'; import browserslistToEsbuild from 'browserslist-to-esbuild'; import { sassPlugin } from 'esbuild-sass-plugin'; import postcss from 'postcss'; +import postcssModulesPlugin from 'postcss-modules'; import autoprefixer from 'autoprefixer'; import rtlcss from 'rtlcss'; import cssnano from 'cssnano'; @@ -870,8 +871,10 @@ async function transpilePackage( packageName ) { /** * Compile styles for a single package. - * Discovers and compiles SCSS entry points based on package configuration. - * Supports wpStyleEntryPoints in package.json for custom entry point patterns. + * + * Discovers and compiles SCSS entry points based on package configuration + * (supporting wpStyleEntryPoints in package.json for custom entry point patterns), + * and all .module.css files in src/ directory. * * @param {string} packageName Package name. * @return {Promise} Build time in milliseconds, or null if no styles. @@ -881,18 +884,25 @@ async function compileStyles( packageName ) { const packageJsonPath = path.join( packageDir, 'package.json' ); const packageJson = JSON.parse( await readFile( packageJsonPath, 'utf8' ) ); - // Get entry point patterns from package.json, default to root-level only - const entryPointPatterns = packageJson.wpStyleEntryPoints || [ + // Get SCSS entry point patterns from package.json, default to root-level only + const scssEntryPointPatterns = packageJson.wpStyleEntryPoints || [ 'src/*.scss', ]; - const styleEntries = await glob( - entryPointPatterns.map( ( pattern ) => + // Find all matching SCSS files + const scssEntries = await glob( + scssEntryPointPatterns.map( ( pattern ) => normalizePath( path.join( packageDir, pattern ) ) ) ); - if ( styleEntries.length === 0 ) { + // Get CSS modules from anywhere in src/ + const cssModuleEntries = await glob( + normalizePath( path.join( packageDir, 'src/**/*.module.css' ) ), + { ignore: IGNORE_PATTERNS } + ); + + if ( scssEntries.length === 0 && cssModuleEntries.length === 0 ) { return null; } @@ -900,8 +910,67 @@ async function compileStyles( packageName ) { const buildStyleDir = path.join( packageDir, 'build-style' ); const srcDir = path.join( packageDir, 'src' ); + // Process .module.css files and generate JS modules + const cssResults = await Promise.all( + cssModuleEntries.map( async ( styleEntryPath ) => { + const buildDir = path.join( packageDir, 'build' ); + const buildModuleDir = path.join( packageDir, 'build-module' ); + + const cssContent = await readFile( styleEntryPath, 'utf8' ); + const relativePath = path.relative( srcDir, styleEntryPath ); + + let mappings = {}; + const result = await postcss( [ + postcssModulesPlugin( { + getJSON: ( _, json ) => ( mappings = json ), + } ), + ] ).process( cssContent, { from: styleEntryPath } ); + + // Write processed CSS to build-style (preserving directory structure) + const cssOutPath = path.join( + buildStyleDir, + relativePath.replace( '.module.css', '.css' ) + ); + await mkdir( path.dirname( cssOutPath ), { recursive: true } ); + await writeFile( cssOutPath, result.css ); + + // Generate JS modules with class name mappings (preserving directory structure) + const jsExport = JSON.stringify( mappings ); + const jsPath = `${ relativePath }.js`; + await Promise.all( [ + mkdir( path.dirname( path.join( buildDir, jsPath ) ), { + recursive: true, + } ), + mkdir( path.dirname( path.join( buildModuleDir, jsPath ) ), { + recursive: true, + } ), + ] ); + await Promise.all( [ + writeFile( + path.join( buildDir, jsPath ), + `"use strict";\nmodule.exports = ${ jsExport };\n` + ), + writeFile( + path.join( buildModuleDir, jsPath ), + `export default ${ jsExport };\n` + ), + ] ); + + // Return the processed CSS for combining + return result.css; + } ) + ); + + // Generate combined stylesheet from all CSS modules + if ( cssResults.length > 0 ) { + const combinedCss = cssResults.join( '\n' ); + await mkdir( buildStyleDir, { recursive: true } ); + await writeFile( path.join( buildStyleDir, 'style.css' ), combinedCss ); + } + + // Process SCSS files await Promise.all( - styleEntries.map( async ( styleEntryPath ) => { + scssEntries.map( async ( styleEntryPath ) => { // Calculate relative path from src/ to preserve directory structure const relativePath = path.relative( srcDir, styleEntryPath ); const relativeDir = path.dirname( relativePath ); diff --git a/docs/manifest.json b/docs/manifest.json index 0616aeb2a8e7f8..05d27bd3b36863 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1985,6 +1985,12 @@ "markdown_source": "../packages/sync/README.md", "parent": "packages" }, + { + "title": "@wordpress/theme", + "slug": "packages-theme", + "markdown_source": "../packages/theme/README.md", + "parent": "packages" + }, { "title": "@wordpress/token-list", "slug": "packages-token-list", diff --git a/package-lock.json b/package-lock.json index 026d4cdf1d6d8c..31ad06f8b64530 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,6 +49,9 @@ "@storybook/test": "8.4.7", "@storybook/theming": "8.4.7", "@storybook/types": "8.4.7", + "@terrazzo/cli": "^0.10.2", + "@terrazzo/plugin-css": "^0.10.2", + "@terrazzo/token-tools": "^0.10.2", "@testing-library/jest-dom": "5.16.5", "@testing-library/react": "14.3.0", "@testing-library/react-native": "12.4.3", @@ -132,6 +135,7 @@ "postcss": "8.4.38", "postcss-loader": "6.2.1", "postcss-local-keyframes": "^0.0.2", + "postcss-modules": "6.0.1", "prettier": "npm:wp-prettier@3.0.3", "progress": "2.0.3", "puppeteer-core": "23.10.1", @@ -4242,6 +4246,29 @@ "react": "*" } }, + "node_modules/@clack/core": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-0.4.1.tgz", + "integrity": "sha512-Pxhij4UXg8KSr7rPek6Zowm+5M22rbd2g1nfojHJkxp5YkFqiZ2+YLEM/XGVIzvGOcM0nqjIFxrpDwWRZYWYjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "sisteransi": "^1.0.5" + } + }, + "node_modules/@clack/prompts": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.9.1.tgz", + "integrity": "sha512-JIpyaboYZeWYlyP0H+OoPPxd6nqueG/CmN6ixBiNFsIDHREevjIf0n0Ohh5gr5C8pEDknzgvz+pIJ8dMhzWIeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@clack/core": "0.4.1", + "picocolors": "^1.0.0", + "sisteransi": "^1.0.5" + } + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -5375,6 +5402,19 @@ "@hapi/hoek": "^9.0.0" } }, + "node_modules/@hono/node-server": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.5.tgz", + "integrity": "sha512-iBuhh+uaaggeAuf+TftcjZyWh2GEgZcVGXkNtskLVoWaXhnJtC5HLHrU8W1KHDoucqO1MswwglmkWLFyiDn4WQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.6.0.tgz", @@ -5388,6 +5428,16 @@ "node": ">=10.10.0" } }, + "node_modules/@humanwhocodes/momoa": { + "version": "3.3.9", + "resolved": "https://registry.npmjs.org/@humanwhocodes/momoa/-/momoa-3.3.9.tgz", + "integrity": "sha512-LHw6Op4bJb3/3KZgOgwflJx5zY9XOy0NU1NuyUFKGdTwHYmP+PbnQGCYQJ8NVNlulLfQish34b0VuUlLYP3AXA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, "node_modules/@humanwhocodes/object-schema": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", @@ -14153,6 +14203,158 @@ "integrity": "sha512-zH2b4ptpfW4mEzt++2nKwTVgBNvfZEH1DgWAlE9uCxZceyH9uUET1Oqvz0LFxaC633WTOHxNxceA0ViJy8X9EA==", "license": "MIT" }, + "node_modules/@terrazzo/cli": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@terrazzo/cli/-/cli-0.10.3.tgz", + "integrity": "sha512-HuLGQiKW+k2aH8508odMBj29NFOF1OGdxJcgCFz0gkDQKsfSWd1V0FO2X2kOqQ1rPEKWwfaVZEAKKNAYy/lP7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@clack/prompts": "^0.9.1", + "@hono/node-server": "^1.14.3", + "@humanwhocodes/momoa": "^3.3.8", + "@terrazzo/parser": "^0.10.3", + "@terrazzo/token-tools": "^0.10.3", + "@types/escodegen": "^0.0.10", + "chokidar": "^3.6.0", + "detect-package-manager": "^3.0.2", + "dotenv": "^16.5.0", + "escodegen": "^2.1.0", + "merge-anything": "^5.1.7", + "meriyah": "^6.0.6", + "mime": "^4.0.7", + "picocolors": "^1.1.1", + "yaml-to-momoa": "^0.0.6" + }, + "bin": { + "terrazzo": "bin/cli.js", + "tz": "bin/cli.js" + } + }, + "node_modules/@terrazzo/cli/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/@terrazzo/cli/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/@terrazzo/cli/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@terrazzo/cli/node_modules/mime": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-4.1.0.tgz", + "integrity": "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa" + ], + "license": "MIT", + "bin": { + "mime": "bin/cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@terrazzo/cli/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/@terrazzo/parser": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@terrazzo/parser/-/parser-0.10.4.tgz", + "integrity": "sha512-5OzoKpn2a2c0Yz/TIB7sSDclhGuSI5/F81OIg74ZrMauRr1qJvUnDnHKvZjkWt7Q3ACNrk5ZgXSXM8Dazu5L/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@humanwhocodes/momoa": "^3.3.9", + "@terrazzo/token-tools": "^0.10.3", + "@types/babel__code-frame": "^7.0.6", + "@types/culori": "^4.0.1", + "culori": "^4.0.2", + "merge-anything": "^5.1.7", + "picocolors": "^1.1.1", + "scule": "^1.3.0", + "wildcard-match": "^5.1.4" + } + }, + "node_modules/@terrazzo/plugin-css": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@terrazzo/plugin-css/-/plugin-css-0.10.4.tgz", + "integrity": "sha512-SnxRZBkwLMQPbXB2xcrDi9O7VEU8DIus9HfCOjv4hXPjOfNaRJsZdM2cK9OKb6vzIyxLzzv3klsCeVKe3EMFgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@terrazzo/token-tools": "^0.10.3", + "wildcard-match": "^5.1.4" + }, + "peerDependencies": { + "@terrazzo/cli": "^0.10.3" + } + }, + "node_modules/@terrazzo/token-tools": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@terrazzo/token-tools/-/token-tools-0.10.3.tgz", + "integrity": "sha512-yeHGe+Bdyrz9H5XX/aN8cyn/rdlB8NWdf/DhI4YqZM1D05MlNcXGX3k8vB3I5U12y4dGsC/ekiAEPqcQKry98w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@humanwhocodes/momoa": "^3.3.8", + "culori": "^4.0.2", + "wildcard-match": "^5.1.4" + } + }, "node_modules/@testing-library/dom": { "version": "9.3.4", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz", @@ -14476,6 +14678,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/babel__code-frame": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/@types/babel__code-frame/-/babel__code-frame-7.0.6.tgz", + "integrity": "sha512-Anitqkl3+KrzcW2k77lRlg/GfLZLWXBuNgbEcIOU6M92yw42vsd3xV/Z/yAHEj8m+KUjL6bWOVOFqX8PFPJ4LA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.0", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.0.tgz", @@ -14576,12 +14785,26 @@ "@types/node": "*" } }, + "node_modules/@types/culori": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/culori/-/culori-4.0.1.tgz", + "integrity": "sha512-43M51r/22CjhbOXyGT361GZ9vncSVQ39u62x5eJdBQFviI8zWp2X5jzqg7k4M6PVgDQAClpy2bUe2dtwEgEDVQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/doctrine": { "version": "0.0.9", "resolved": "https://registry.npmjs.org/@types/doctrine/-/doctrine-0.0.9.tgz", "integrity": "sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==", "dev": true }, + "node_modules/@types/escodegen": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@types/escodegen/-/escodegen-0.0.10.tgz", + "integrity": "sha512-IVvcNLEFbiL17qiGRGzyfx/u9K6lA5w6wcQSIgv2h4JG3ZAFIY1Be9ITTSPuARIxRpzW54s8OvcF6PdonBbDzg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/eslint": { "version": "8.56.9", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.9.tgz", @@ -17035,6 +17258,10 @@ "resolved": "packages/sync", "link": true }, + "node_modules/@wordpress/theme": { + "resolved": "packages/theme", + "link": true + }, "node_modules/@wordpress/token-list": { "resolved": "packages/token-list", "link": true @@ -20809,7 +21036,6 @@ "version": "0.5.2", "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz", "integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==", - "dev": true, "license": "MIT" }, "node_modules/colorspace": { @@ -22672,6 +22898,16 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" }, + "node_modules/culori": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/culori/-/culori-4.0.2.tgz", + "integrity": "sha512-1+BhOB8ahCn4O0cep0Sh2l9KCOfOdY+BXJnKMHFFzDEouSr/el18QwXEMRlOj9UY5nCeA8UN3a/82rUWRBeyBw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/cwd": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/cwd/-/cwd-0.10.0.tgz", @@ -23396,6 +23632,128 @@ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" }, + "node_modules/detect-package-manager": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/detect-package-manager/-/detect-package-manager-3.0.2.tgz", + "integrity": "sha512-8JFjJHutStYrfWwzfretQoyNGoZVW1Fsrp4JO9spa7h/fBfwgTMEIy4/LBzRDGsxwVPHU0q+T9YvwLDJoOApLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/detect-package-manager/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/detect-package-manager/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/detect-package-manager/node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/detect-package-manager/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/detect-package-manager/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-package-manager/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-package-manager/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/detect-package-manager/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/devtools-protocol": { "version": "0.0.1367902", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1367902.tgz", @@ -27074,6 +27432,26 @@ "node": "^16.13.0 || >=18.0.0" } }, + "node_modules/generic-names": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/generic-names/-/generic-names-4.0.0.tgz", + "integrity": "sha512-ySFolZQfw9FoDb3ed9d80Cm9f0+r7qj+HJkWjeD9RBfpxEVTlVhol+gvaQB/78WbwYfbnNh8nWHHBSlg072y6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "loader-utils": "^3.2.0" + } + }, + "node_modules/generic-names/node_modules/loader-utils": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz", + "integrity": "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -29491,6 +29869,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/is-whitespace-character": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz", @@ -33164,6 +33555,13 @@ "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", "license": "MIT" }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", @@ -34422,6 +34820,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge-anything": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/merge-anything/-/merge-anything-5.1.7.tgz", + "integrity": "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/merge-deep": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/merge-deep/-/merge-deep-3.0.3.tgz", @@ -34524,6 +34938,16 @@ "node": ">= 8" } }, + "node_modules/meriyah": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/meriyah/-/meriyah-6.1.4.tgz", + "integrity": "sha512-Sz8FzjzI0kN13GK/6MVEsVzMZEPvOhnmmI1lU5+/1cGOiK3QUahntrNNtdVeihrO7t9JpoH75iMNXg6R6uWflQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/metaviewport-parser": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/metaviewport-parser/-/metaviewport-parser-0.3.0.tgz", @@ -39207,6 +39631,26 @@ "postcss": "^8.2.15" } }, + "node_modules/postcss-modules": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-modules/-/postcss-modules-6.0.1.tgz", + "integrity": "sha512-zyo2sAkVvuZFFy0gc2+4O+xar5dYlaVy/ebO24KT0ftk/iJevSNyPyQellsBLlnccwh7f6V6Y4GvuKRYToNgpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "generic-names": "^4.0.0", + "icss-utils": "^5.1.0", + "lodash.camelcase": "^4.3.0", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "string-hash": "^1.1.3" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, "node_modules/postcss-modules-extract-imports": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", @@ -43132,6 +43576,13 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/scule": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz", + "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==", + "dev": true, + "license": "MIT" + }, "node_modules/seedrandom": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", @@ -44772,6 +45223,13 @@ "node": ">=0.6.19" } }, + "node_modules/string-hash": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz", + "integrity": "sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -49726,6 +50184,13 @@ "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==" }, + "node_modules/wildcard-match": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/wildcard-match/-/wildcard-match-5.1.4.tgz", + "integrity": "sha512-wldeCaczs8XXq7hj+5d/F38JE2r7EXgb6WQDM84RVwxy81T/sxB5e9+uZLK9Q9oNz1mlvjut+QtvgaOQFPVq/g==", + "dev": true, + "license": "ISC" + }, "node_modules/windows-release": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-3.2.0.tgz", @@ -50204,6 +50669,30 @@ "node": ">= 6" } }, + "node_modules/yaml-to-momoa": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/yaml-to-momoa/-/yaml-to-momoa-0.0.6.tgz", + "integrity": "sha512-H6OxymcTIB5fTCQV7/+ETO9Wh6v1vpBDpmW94XnscKE+jQl6x58us/G6r6z5IiPCi5wuSvJ+0wipUsp0s2/Yyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@humanwhocodes/momoa": "^3.3.8", + "yaml": "^2.7.1" + } + }, + "node_modules/yaml-to-momoa/node_modules/yaml": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -53837,6 +54326,24 @@ "npm": ">=8.19.2" } }, + "packages/theme": { + "name": "@wordpress/theme", + "version": "0.0.1", + "license": "GPL-2.0-or-later", + "dependencies": { + "@wordpress/element": "file:../element", + "@wordpress/private-apis": "file:../private-apis", + "colorjs.io": "^0.5.2" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "packages/token-list": { "name": "@wordpress/token-list", "version": "3.33.0", diff --git a/package.json b/package.json index d18417276e0f77..6b04a153e1e061 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,9 @@ "@storybook/test": "8.4.7", "@storybook/theming": "8.4.7", "@storybook/types": "8.4.7", + "@terrazzo/cli": "^0.10.2", + "@terrazzo/plugin-css": "^0.10.2", + "@terrazzo/token-tools": "^0.10.2", "@testing-library/jest-dom": "5.16.5", "@testing-library/react": "14.3.0", "@testing-library/react-native": "12.4.3", @@ -141,6 +144,7 @@ "postcss": "8.4.38", "postcss-loader": "6.2.1", "postcss-local-keyframes": "^0.0.2", + "postcss-modules": "6.0.1", "prettier": "npm:wp-prettier@3.0.3", "progress": "2.0.3", "puppeteer-core": "23.10.1", diff --git a/packages/private-apis/src/implementation.ts b/packages/private-apis/src/implementation.ts index b716c2ef5c577e..5b6649bb1d04bd 100644 --- a/packages/private-apis/src/implementation.ts +++ b/packages/private-apis/src/implementation.ts @@ -30,6 +30,7 @@ const CORE_MODULES_USING_PRIVATE_APIS = [ '@wordpress/reusable-blocks', '@wordpress/router', '@wordpress/sync', + '@wordpress/theme', '@wordpress/dataviews', '@wordpress/fields', '@wordpress/media-utils', diff --git a/packages/theme/README.md b/packages/theme/README.md new file mode 100644 index 00000000000000..0d83086ce52962 --- /dev/null +++ b/packages/theme/README.md @@ -0,0 +1,67 @@ +# (Experimental) Theme + +A theming package that's part of the WordPress Design System. It has two parts: + +- **Design Tokens**: A comprehensive system of design tokens for colors, spacing, typography, and more +- **Theme System**: A flexible theming provider for consistent theming across applications + +## Design Tokens + +In the **[Design Tokens Reference](docs/ds-tokens.md)** document there is a complete reference of all available design tokens including colors, spacing, typography, and more. + +## Theme Provider + +The `ThemeProvider` is a React component that should wrap your application to provide design tokens and theme context to the child UI components. + +```tsx +import { ThemeProvider } from '@wordpress/theme'; + +function App() { + return ( + + { /* Your app content */ } + + ); +} +``` + +The provider can be used recursively to override or modify the theme for a specific subtree. + +```tsx + + { /* system-themed UI components */ } + + { /* dark-themed UI components */ } + + { /* light-themed UI components */ } + + { /* dark-themed UI components */ } + + { /* system-themed UI components */ } + +``` + +The `ThemeProvider` redefines some of the design system tokens. Components consuming semantic design system tokens will automatically follow the chosen theme. Note that the tokens are defined and inherited using the CSS cascade, and therefore the DOM tree, not the React tree. This is very important when using React portals. + +### Building + +This package is built in two steps. When `npm run build` is run at the root of the repo, it will first run the "prebuild" step of this package, which is defined in the `build` script of this package's package.json. + +This step will: + +1. Generate primitive tokens. +2. Build CSS and JavaScript token files. +3. Update the design tokens documentation. +4. Format all generated files. + +The files generated in this step will all be committed to the repo. + +After the prebuild step, the package will be built into its final form via the repo's standard package build script. + +## Contributing to this package + +This is an individual package that's part of the Gutenberg project. The project is organized as a monorepo. It's made up of multiple self-contained software packages, each with a specific purpose. The packages in this monorepo are published to [npm](https://www.npmjs.com/) and used by [WordPress](https://make.wordpress.org/core/) as well as other software projects. + +To find out more about contributing to this package or Gutenberg as a whole, please read the project's main [contributor guide](https://github.com/WordPress/gutenberg/tree/HEAD/CONTRIBUTING.md). + +

Code is Poetry.

diff --git a/packages/theme/bin/build-tokens.js b/packages/theme/bin/build-tokens.js new file mode 100755 index 00000000000000..0503c197039772 --- /dev/null +++ b/packages/theme/bin/build-tokens.js @@ -0,0 +1,83 @@ +/** + * External dependencies + */ +const { execSync } = require( 'child_process' ); +const fs = require( 'fs' ); +const path = require( 'path' ); + +/** + * Build script for design tokens. + * + * This script: + * 1. Compiles and runs the primitive token generator + * 2. Compiles the terrazzo config and runs the Terrazzo build + * 3. Cleans up temporary files + */ + +const TEMP_FILES = [ + 'bin/generate-primitive-tokens/index.mjs', + 'terrazzo.config.mjs', +]; + +function cleanup() { + TEMP_FILES.forEach( ( file ) => { + const filePath = path.join( process.cwd(), file ); + if ( fs.existsSync( filePath ) ) { + fs.unlinkSync( filePath ); + } + } ); +} + +// Ensure cleanup happens even if the process exits unexpectedly +process.on( 'exit', cleanup ); +process.on( 'SIGINT', () => { + cleanup(); + process.exit( 130 ); +} ); +process.on( 'SIGTERM', () => { + cleanup(); + process.exit( 143 ); +} ); + +try { + // Step 1: Compile the primitive token generator + console.log( '🔨 Compiling primitive token generator...' ); + execSync( + 'npx esbuild bin/generate-primitive-tokens/index.ts --bundle --platform=node --format=esm --packages=external --outfile=bin/generate-primitive-tokens/index.mjs', + { + stdio: 'inherit', + cwd: process.cwd(), + } + ); + + // Step 2: Run the primitive token generator + console.log( '🎨 Generating primitive tokens...' ); + execSync( 'node bin/generate-primitive-tokens/index.mjs', { + stdio: 'inherit', + cwd: process.cwd(), + } ); + + // Step 3: Compile the terrazzo config + console.log( '🔨 Compiling terrazzo config...' ); + execSync( + 'npx esbuild terrazzo.config.ts --bundle --platform=node --format=esm --packages=external --outfile=terrazzo.config.mjs', + { + stdio: 'inherit', + cwd: process.cwd(), + } + ); + + // Step 4: Run terrazzo build + console.log( '🏗️ Running terrazzo build...' ); + execSync( 'npx tz build --config terrazzo.config.mjs', { + stdio: 'inherit', + cwd: process.cwd(), + } ); + + console.log( '✅ Token build complete!' ); +} catch ( error ) { + console.error( '❌ Build failed:', error.message ); + process.exit( 1 ); +} finally { + cleanup(); +} diff --git a/packages/theme/bin/generate-primitive-tokens/index.ts b/packages/theme/bin/generate-primitive-tokens/index.ts new file mode 100644 index 00000000000000..dd2ce18011ff74 --- /dev/null +++ b/packages/theme/bin/generate-primitive-tokens/index.ts @@ -0,0 +1,115 @@ +/** + * External dependencies + */ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import Color from 'colorjs.io'; + +/** + * Internal dependencies + */ +import { + DEFAULT_SEED_COLORS, + buildBgRamp, + buildAccentRamp, +} from '../../src/color-ramps/index'; + +const __filename = fileURLToPath( import.meta.url ); +const __dirname = path.dirname( __filename ); + +// Path to the color.json file +const colorJsonPath = path.join( __dirname, '../../tokens/color.json' ); + +const transformColorStringToDTCGValue = ( color: string ) => { + if ( /oklch|p3/.test( color ) ) { + let parsed: Color; + try { + parsed = new Color( color ).to( 'oklch' ); + } catch { + return color; + } + + const coords = parsed.coords; + return { + colorSpace: 'oklch', + components: [ + Math.floor( 10000 * coords[ 0 ] ) / 10000, // l + coords[ 1 ], // c + isNaN( coords[ 2 ] ) ? 0 : coords[ 2 ], // h + ], + ...( parsed.alpha < 1 ? { alpha: parsed.alpha } : undefined ), + hex: parsed.to( 'srgb' ).toString( { format: 'hex' } ), + }; + } + + return color; +}; + +// Main function +function generatePrimitiveColorTokens() { + const startTime = performance.now(); + console.log( '🎨 Starting primitive color tokens generation...' ); + + try { + // Read the color.json file + const colorJson = JSON.parse( + fs.readFileSync( colorJsonPath, 'utf8' ) + ); + + // Build the ramps + const bgRamp = buildBgRamp( { seed: DEFAULT_SEED_COLORS.bg } ); + const accentRamps = [ ...Object.entries( DEFAULT_SEED_COLORS ) ] + .filter( ( [ scaleName ] ) => scaleName !== 'bg' ) + .map( ( [ scaleName, seed ] ) => ( { + scaleName, + ramp: buildAccentRamp( { + seed, + bgRamp, + } ), + } ) ); + + // Convert the ramp values in a DTCG compatible format + [ + { + scaleName: 'bg', + ramp: bgRamp, + }, + ...accentRamps, + ].forEach( ( { scaleName, ramp } ) => { + colorJson.color.primitive[ scaleName ] = {}; + for ( const [ tokenName, tokenValue ] of Object.entries( + ramp.ramp + ) ) { + colorJson.color.primitive[ scaleName ][ tokenName ] = { + $value: transformColorStringToDTCGValue( tokenValue.color ), + }; + } + } ); + + // Write the updated JSON back to the file with proper formatting + fs.writeFileSync( + colorJsonPath, + JSON.stringify( colorJson, null, '\t' ) + ); + + const endTime = performance.now(); + const duration = endTime - startTime; + console.log( + `✅ Successfully updated color.json (${ duration.toFixed( 2 ) }ms)` + ); + } catch ( error ) { + const endTime = performance.now(); + const duration = endTime - startTime; + console.error( + `❌ Error updating color tokens after ${ duration.toFixed( + 2 + ) }ms:`, + error + ); + process.exit( 1 ); + } +} + +// Run the script +generatePrimitiveColorTokens(); diff --git a/packages/theme/bin/terrazzo-plugin-ds-tokens-docs/index.ts b/packages/theme/bin/terrazzo-plugin-ds-tokens-docs/index.ts new file mode 100644 index 00000000000000..04982e3f12a3eb --- /dev/null +++ b/packages/theme/bin/terrazzo-plugin-ds-tokens-docs/index.ts @@ -0,0 +1,103 @@ +/** + * External dependencies + */ +import { FORMAT_ID } from '@terrazzo/plugin-css'; +import type { Plugin } from '@terrazzo/parser'; + +function isPrivateToken( token: string ) { + return /-private-/i.test( token ); +} + +function titleCase( str: string ) { + return str[ 0 ].toUpperCase() + str.slice( 1 ); +} + +type TokensMap = Record< string, Record< string, string > >; + +export default function pluginDsTokenDocs( { + filename = 'design-tokens.md', +} = {} ): Plugin { + return { + name: '@terrazzo/terrazzo-plugin-ds-tokens-docs', + async build( { getTransforms, outputFile } ) { + if ( ! filename ) { + return; + } + + const primitiveTokens: TokensMap = {}; + const semanticTokens: TokensMap = {}; + // Re-use transformed tokens from the CSS plugin + for ( const token of getTransforms( { + format: FORMAT_ID, + id: '*', + mode: '.', + } ) ) { + if ( token.localID === undefined ) { + console.warn( + 'Unexpected — Missing local ID when building token list for eslint plugin' + ); + continue; + } + + // Use the tokens filename (without .json) as the group name + const group = + token.token.source.loc + ?.split( '/' ) + .at( -1 ) + ?.split( '.json' )[ 0 ] ?? 'Miscellaneous'; + + // Organize tokens in semantic/private, and group by category. + const tokensObject = isPrivateToken( token.localID ) + ? primitiveTokens + : semanticTokens; + tokensObject[ group ] ??= {}; + tokensObject[ group ][ token.localID ] = + token.token.$description ?? 'N/A'; + } + + function tokensToMdTable( + tokens: TokensMap, + isPrivate: boolean = false + ) { + return Object.entries( tokens ) + .map( ( [ group, tokensInGroup ] ) => [ + `### ${ titleCase( group ) }${ + isPrivate ? ' (private)' : '' + }`, + '', + '| Variable name | Description |', + '|---|---|', + ...Object.entries( tokensInGroup ).map( + ( [ name, description ] ) => + `| \`${ name }\` | ${ description } |` + ), + '', + ] ) + .flat( 2 ); + } + + outputFile( + filename, + [ + '', + '', + '# DS Tokens reference', + '', + '## Semantic tokens', + '', + ...tokensToMdTable( semanticTokens ), + '', + '## Primitive tokens', + '', + '**🚨 Note: These tokens are only private implementation details of the Theme, and should never be referenced / consumed directly in the code.**', + '', + ...tokensToMdTable( primitiveTokens, true ), + '', // final empty line + ].join( '\n' ) + ); + }, + }; +} diff --git a/packages/theme/bin/terrazzo-plugin-figma-ds-token-manager/index.ts b/packages/theme/bin/terrazzo-plugin-figma-ds-token-manager/index.ts new file mode 100644 index 00000000000000..0a181b7b776b35 --- /dev/null +++ b/packages/theme/bin/terrazzo-plugin-figma-ds-token-manager/index.ts @@ -0,0 +1,210 @@ +/** + * External dependencies + */ +import type { Plugin, TokenNormalized } from '@terrazzo/parser'; +import { transformCSSValue } from '@terrazzo/token-tools/css'; +import Color from 'colorjs.io'; + +/** + * Internal dependencies + */ +import { FORMAT_JSON_ID } from './lib'; + +function titleCase( str: string ) { + return str[ 0 ].toUpperCase() + str.slice( 1 ); +} + +function kebabToCamel( str: string ) { + return str.replace( /-([a-z])/g, ( _, letter ) => letter.toUpperCase() ); +} + +function transformTokenName( { id }: { id: string } ) { + return ( + id + // Capitalize first segment + .replace( /^(\w+)\./g, ( _, g1 ) => `${ titleCase( g1 ) }/` ) + // Capitalize + .replace( /primitive\./g, '_Primitives/' ) + .replace( /semantic\./g, 'Semantic/' ) + .replace( + /(color\/_Primitives)\/(\w+)\.(.*)/gi, + ( _, prefix, tone, rampStep ) => { + return `${ prefix }/${ titleCase( tone ) }/${ rampStep }`; + } + ) + // Color-specific transformation for semantic tokens: + // - add extra folder (Background, Foreground, Stroke) + // - swap "tone" folder order, capitalize + // - limit bg-* to 6 characters + // - keep last part of the token name with dots (eg no folders) + .replace( + /(color\/Semantic)\/([\w,\-]+)\.(\w+)\.(.*)/gi, + ( _, prefix, element, tone, emphasisAndState ) => { + let extraFolder = ''; + let elementName = element; + if ( /bg/.test( element ) ) { + extraFolder = 'Background/'; + elementName = element.slice( 0, 6 ); + } else if ( /fg/.test( element ) ) { + extraFolder = 'Foreground/'; + elementName = element.slice( 0, 6 ); + } else if ( /stroke/.test( element ) ) { + extraFolder = 'Stroke/'; + elementName = element.slice( 0, 10 ); + } + return `${ prefix }/${ extraFolder }${ titleCase( + tone + ) }/${ kebabToCamel( + elementName + ) }.${ tone }.${ emphasisAndState }`; + } + ) + // Remove default emphasis and state variants from variable name + .replace( /normal\./g, '' ) + .replace( /resting/g, '' ) + // Remove double dots + .replace( /\.{2,}/g, '.' ) + // Remove trailing dot + .replace( /\.$/g, '' ) + // Replace remaining dots with dashes + .replace( /\./g, '-' ) + ); +} + +function transformColorToken( + token: TokenNormalized, + mode: string, + tokens: Record< string, TokenNormalized > +) { + if ( + token.mode[ mode ]?.aliasChain && + token.mode[ mode ].aliasChain.length > 0 + ) { + // Keep aliases + return `{${ transformTokenName( { + id: token.mode[ mode ].aliasChain[ 0 ], + } ) }}`; + } + // Start by letting terrazzo do the heavy lifting. + const baselineCSSValue = transformCSSValue( token, { + mode, + tokensSet: tokens, + transformAlias: transformTokenName, + } ); + + if ( baselineCSSValue === undefined ) { + console.warn( 'Unexpected: could not tranform color token value' ); + return; + } + + let cssColorValue: string; + + if ( typeof baselineCSSValue === 'object' ) { + if ( 'srgb' in baselineCSSValue ) { + // Pick SRGB gamut (safer compared to p3 or rec2020) + cssColorValue = baselineCSSValue.srgb; + } else { + console.log( 'UNSUPPORTED USE CASE' ); + return; + } + } else { + cssColorValue = baselineCSSValue; + } + + // Always convert to hex + // (easier to convert to Figma RGB, and includes clamping) + let convertedColor: Color; + try { + convertedColor = new Color( cssColorValue ); + } catch { + console.warn( 'Unexpected: could not convert token value to Color' ); + return; + } + + return convertedColor.to( 'srgb' ).toString( { format: 'hex' } ); +} + +export default function pluginFigmaDsTokenManager( { + filename = 'figma-ds-tokens.json', +} = {} ): Plugin { + return { + name: '@terrazzo/plugin-figma-ds-token-manager', + async transform( { tokens, getTransforms, setTransform } ) { + // skip work if another .json plugin has already run + const jsonTokens = getTransforms( { + format: FORMAT_JSON_ID, + id: '*', + mode: '.', + } ); + if ( jsonTokens.length ) { + return; + } + + for ( const [ id, token ] of Object.entries( tokens ) ) { + for ( const mode of Object.keys( token.mode ) ) { + const localID = transformTokenName( token ); + + let transformedValue; + + if ( token.$type === 'color' ) { + transformedValue = transformColorToken( + token, + mode, + tokens + ); + } else if ( + token.mode[ mode ]?.aliasChain && + token.mode[ mode ].aliasChain.length > 0 + ) { + // Keep aliases + transformedValue = `{${ transformTokenName( { + id: token.mode[ mode ].aliasChain[ 0 ], + } ) }}`; + } else { + // Fallback to terrazzo + transformedValue = transformCSSValue( token, { + mode, + tokensSet: tokens, + transformAlias: transformTokenName, + } ); + } + + if ( transformedValue !== undefined ) { + setTransform( id, { + format: FORMAT_JSON_ID, + localID, + value: transformedValue, + mode, + } ); + } + } + } + }, + async build( { getTransforms, outputFile } ) { + const tokenVals: Record< + string, + { + value: Record< string, string | Record< string, string > >; + description?: string; + } + > = {}; + + for ( const token of getTransforms( { + format: FORMAT_JSON_ID, + id: '*', + } ) ) { + if ( ! token.localID ) { + continue; + } + + tokenVals[ token.localID ] ??= { value: {}, description: '' }; + + tokenVals[ token.localID ].value[ token.mode ] = token.value; + tokenVals[ token.localID ].description = + token.token.$description; + } + + outputFile( filename, JSON.stringify( tokenVals, null, 2 ) ); + }, + }; +} diff --git a/packages/theme/bin/terrazzo-plugin-figma-ds-token-manager/lib.ts b/packages/theme/bin/terrazzo-plugin-figma-ds-token-manager/lib.ts new file mode 100644 index 00000000000000..95a893f289b3ea --- /dev/null +++ b/packages/theme/bin/terrazzo-plugin-figma-ds-token-manager/lib.ts @@ -0,0 +1 @@ +export const FORMAT_JSON_ID = 'json'; diff --git a/packages/theme/bin/terrazzo-plugin-known-wpds-css-variables/index.ts b/packages/theme/bin/terrazzo-plugin-known-wpds-css-variables/index.ts new file mode 100644 index 00000000000000..00095bae096d05 --- /dev/null +++ b/packages/theme/bin/terrazzo-plugin-known-wpds-css-variables/index.ts @@ -0,0 +1,72 @@ +/** + * External dependencies + */ +import { FORMAT_ID } from '@terrazzo/plugin-css'; +import type { Plugin } from '@terrazzo/parser'; + +function isPrivateToken( token: string ) { + return /-private-/i.test( token ); +} + +export default function pluginKnownWpdsCssVariables( { + exports = [ { filename: 'design-tokens.js', modes: false } ], +} = {} ): Plugin { + return { + name: '@terrazzo/plugin-known-wpds-css-variables', + async build( { getTransforms, outputFile } ) { + // Either a string (modes=false) or an object (modes=true) + const tokensToExport: Record< + string, + Record< string, string | Record< string, string > > + > = {}; + + for ( const token of getTransforms( { + format: FORMAT_ID, + id: '*', + } ) ) { + if ( ! token.localID ) { + console.warn( + 'Unexpected — Missing local ID when building token list for eslint plugin' + ); + continue; + } + + if ( ! isPrivateToken( token.localID ) ) { + tokensToExport[ token.localID ] ??= {}; + + tokensToExport[ token.localID ][ token.mode ] = token.value; + } + } + + const exportsAsArray = ! Array.isArray( exports ) + ? [ exports ] + : exports; + for ( const { filename, modes } of exportsAsArray ) { + outputFile( + filename, + [ + '/**', + ' * This file is generated by the @terrazzo/plugin-known-wpds-css-variables plugin.', + ' * Do not edit this file directly.', + ' */', + '', + `export default ${ JSON.stringify( + modes === false + ? Array.from( + new Set( Object.keys( tokensToExport ) ) + ) + : tokensToExport, + null, + 2 + ) }${ + filename.endsWith( '.ts' ) && modes + ? ' as Record< string, Record< string, string > >' + : '' + }`, + '', // final empty line + ].join( '\n' ) + ); + } + }, + }; +} diff --git a/packages/theme/docs/ds-tokens.md b/packages/theme/docs/ds-tokens.md new file mode 100644 index 00000000000000..94270bcf123787 --- /dev/null +++ b/packages/theme/docs/ds-tokens.md @@ -0,0 +1,283 @@ + + +# DS Tokens reference + +## Semantic tokens + +### Border + +| Variable name | Description | +| ------------------------------ | --------------------------- | +| `--wpds-border-radius-x-small` | Extra small radius | +| `--wpds-border-radius-small` | Small radius | +| `--wpds-border-radius-medium` | Medium radius | +| `--wpds-border-radius-large` | Large radius | +| `--wpds-border-width-focus` | Border width for focus ring | + +### Color + +| Variable name | Description | +| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | +| `--wpds-color-bg-surface-neutral` | Background color for surfaces with normal emphasis. | +| `--wpds-color-bg-surface-neutral-strong` | Background color for surfaces with strong emphasis. | +| `--wpds-color-bg-surface-neutral-weak` | Background color for surfaces with weak emphasis. | +| `--wpds-color-bg-surface-brand` | Background color for surfaces with brand tone and normal emphasis. | +| `--wpds-color-bg-surface-success` | Background color for surfaces with success tone and normal emphasis. | +| `--wpds-color-bg-surface-success-weak` | Background color for surfaces with success tone and weak emphasis. | +| `--wpds-color-bg-surface-info` | Background color for surfaces with info tone and normal emphasis. | +| `--wpds-color-bg-surface-info-weak` | Background color for surfaces with info tone and weak emphasis. | +| `--wpds-color-bg-surface-warning` | Background color for surfaces with warning tone and normal emphasis. | +| `--wpds-color-bg-surface-warning-weak` | Background color for surfaces with warning tone and weak emphasis. | +| `--wpds-color-bg-surface-error` | Background color for surfaces with error tone and normal emphasis. | +| `--wpds-color-bg-surface-error-weak` | Background color for surfaces with error tone and weak emphasis. | +| `--wpds-color-bg-interactive-neutral` | Background color for interactive elements with neutral tone and normal emphasis. | +| `--wpds-color-bg-interactive-neutral-active` | Background color for interactive elements with neutral tone and normal emphasis that are hovered, focused, or active. | +| `--wpds-color-bg-interactive-neutral-disabled` | Background color for interactive elements with neutral tone and normal emphasis, in their disabled state. | +| `--wpds-color-bg-interactive-neutral-strong` | Background color for interactive elements with neutral tone and strong emphasis. | +| `--wpds-color-bg-interactive-neutral-strong-active` | Background color for interactive elements with neutral tone and strong emphasis that are hovered, focused, or active. | +| `--wpds-color-bg-interactive-neutral-strong-disabled` | Background color for interactive elements with neutral tone and strong emphasis, in their disabled state. | +| `--wpds-color-bg-interactive-neutral-weak` | Background color for interactive elements with neutral tone and weak emphasis. | +| `--wpds-color-bg-interactive-neutral-weak-active` | Background color for interactive elements with neutral tone and weak emphasis that are hovered, focused, or active. | +| `--wpds-color-bg-interactive-neutral-weak-disabled` | Background color for interactive elements with neutral tone and weak emphasis, in their disabled state. | +| `--wpds-color-bg-interactive-brand` | Background color for interactive elements with brand tone and normal emphasis. | +| `--wpds-color-bg-interactive-brand-active` | Background color for interactive elements with brand tone and normal emphasis that are hovered, focused, or active. | +| `--wpds-color-bg-interactive-brand-disabled` | Background color for interactive elements with brand tone and normal emphasis, in their disabled state. | +| `--wpds-color-bg-interactive-brand-strong` | Background color for interactive elements with brand tone and strong emphasis. | +| `--wpds-color-bg-interactive-brand-strong-active` | Background color for interactive elements with brand tone and strong emphasis that are hovered, focused, or active. | +| `--wpds-color-bg-interactive-brand-strong-disabled` | Background color for interactive elements with brand tone and strong emphasis, in their disabled state. | +| `--wpds-color-bg-interactive-brand-weak` | Background color for interactive elements with brand tone and weak emphasis. | +| `--wpds-color-bg-interactive-brand-weak-active` | Background color for interactive elements with brand tone and weak emphasis that are hovered, focused, or active. | +| `--wpds-color-bg-interactive-brand-weak-disabled` | Background color for interactive elements with brand tone and weak emphasis, in their disabled state. | +| `--wpds-color-bg-track-neutral-weak` | Background color for tracks with a neutral tone and weak emphasis (eg. scrollbar track). | +| `--wpds-color-bg-track-neutral` | Background color for tracks with a neutral tone and normal emphasis (eg. slider or progressbar track). | +| `--wpds-color-bg-thumb-neutral-weak` | Background color for thumbs with a neutral tone and weak emphasis (eg. scrollbar thumb). | +| `--wpds-color-bg-thumb-neutral-weak-active` | Background color for thumbs with a neutral tone and weak emphasis (eg. scrollbar thumb) that are hovered, focused, or active. | +| `--wpds-color-bg-thumb-brand` | Background color for thumbs with a brand tone and normal emphasis (eg. slider thumb and filled track). | +| `--wpds-color-bg-thumb-brand-active` | Background color for thumbs with a brand tone and normal emphasis (eg. slider thumb and filled track) that are hovered, focused, or active. | +| `--wpds-color-bg-thumb-brand-disabled` | Background color for thumbs with a brand tone and normal emphasis (eg. slider thumb and filled track), in their disabled state. | +| `--wpds-color-fg-content-neutral` | Foreground color for content like text with normal emphasis. | +| `--wpds-color-fg-content-neutral-weak` | Foreground color for content like text with weak emphasis. | +| `--wpds-color-fg-interactive-neutral` | Foreground color for interactive elements with neutral tone and normal emphasis. | +| `--wpds-color-fg-interactive-neutral-active` | Foreground color for interactive elements with neutral tone and normal emphasis that are hovered, focused, or active. | +| `--wpds-color-fg-interactive-neutral-disabled` | Foreground color for interactive elements with neutral tone and normal emphasis, in their disabled state. | +| `--wpds-color-fg-interactive-neutral-strong` | Foreground color for interactive elements with neutral tone and strong emphasis. | +| `--wpds-color-fg-interactive-neutral-strong-active` | Foreground color for interactive elements with neutral tone and strong emphasis that are hovered, focused, or active. | +| `--wpds-color-fg-interactive-neutral-strong-disabled` | Foreground color for interactive elements with neutral tone and strong emphasis, in their disabled state. | +| `--wpds-color-fg-interactive-neutral-weak` | Foreground color for interactive elements with neutral tone and weak emphasis. | +| `--wpds-color-fg-interactive-neutral-weak-disabled` | Foreground color for interactive elements with neutral tone and weak emphasis, in their disabled state. | +| `--wpds-color-fg-interactive-brand` | Foreground color for interactive elements with brand tone and normal emphasis. | +| `--wpds-color-fg-interactive-brand-active` | Foreground color for interactive elements with brand tone and normal emphasis that are hovered, focused, or active. | +| `--wpds-color-fg-interactive-brand-disabled` | Foreground color for interactive elements with brand tone and normal emphasis, in their disabled state. | +| `--wpds-color-fg-interactive-brand-strong` | Foreground color for interactive elements with brand tone and strong emphasis. | +| `--wpds-color-fg-interactive-brand-strong-active` | Foreground color for interactive elements with brand tone and strong emphasis that are hovered, focused, or active. | +| `--wpds-color-fg-interactive-brand-strong-disabled` | Foreground color for interactive elements with brand tone and strong emphasis, in their disabled state. | +| `--wpds-color-stroke-surface-neutral` | Decorative stroke color used to define neutrally-toned surface boundaries with normal emphasis. | +| `--wpds-color-stroke-surface-neutral-weak` | Decorative stroke color used to define neutrally-toned surface boundaries with weak emphasis. | +| `--wpds-color-stroke-surface-neutral-strong` | Decorative stroke color used to define neutrally-toned surface boundaries with strong emphasis. | +| `--wpds-color-stroke-surface-brand` | Decorative stroke color used to define brand-toned surface boundaries with normal emphasis. | +| `--wpds-color-stroke-surface-brand-strong` | Decorative stroke color used to define neutrally-toned surface boundaries with strong emphasis. | +| `--wpds-color-stroke-surface-success` | Decorative stroke color used to define success-toned surface boundaries with normal emphasis. | +| `--wpds-color-stroke-surface-success-strong` | Decorative stroke color used to define success-toned surface boundaries with strong emphasis. | +| `--wpds-color-stroke-surface-info` | Decorative stroke color used to define info-toned surface boundaries with normal emphasis. | +| `--wpds-color-stroke-surface-info-strong` | Decorative stroke color used to define info-toned surface boundaries with strong emphasis. | +| `--wpds-color-stroke-surface-warning` | Decorative stroke color used to define warning-toned surface boundaries with normal emphasis. | +| `--wpds-color-stroke-surface-warning-strong` | Decorative stroke color used to define warning-toned surface boundaries with strong emphasis. | +| `--wpds-color-stroke-surface-error` | Decorative stroke color used to define error-toned surface boundaries with normal emphasis. | +| `--wpds-color-stroke-surface-error-strong` | Decorative stroke color used to define error-toned surface boundaries with strong emphasis. | +| `--wpds-color-stroke-interactive-neutral` | Accessible stroke color used for interactive neutrally-toned elements with normal emphasis. | +| `--wpds-color-stroke-interactive-neutral-active` | Accessible stroke color used for interactive neutrally-toned elements with normal emphasis that are hovered, focused, or active. | +| `--wpds-color-stroke-interactive-neutral-disabled` | Accessible stroke color used for interactive neutrally-toned elements with normal emphasis, in their disabled state. | +| `--wpds-color-stroke-interactive-neutral-strong` | Accessible stroke color used for interactive neutrally-toned elements with strong emphasis. | +| `--wpds-color-stroke-interactive-brand` | Accessible stroke color used for interactive brand-toned elements with normal emphasis. | +| `--wpds-color-stroke-interactive-brand-active` | Accessible stroke color used for interactive brand-toned elements with normal emphasis that are hovered, focused, or active. | +| `--wpds-color-stroke-interactive-brand-disabled` | Accessible stroke color used for interactive brand-toned elements with normal emphasis, in their disabled state. | +| `--wpds-color-stroke-interactive-error-strong` | Accessible stroke color used for interactive error-toned elements with strong emphasis. | +| `--wpds-color-stroke-focus-brand` | Accessible stroke color applied to focus rings. | + +### Elevation + +| Variable name | Description | +| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| `--wpds-elevation-x-small` | For sections and containers that group related content and controls, which may overlap other content. Example: Preview Frame. | +| `--wpds-elevation-small` | For components that provide contextual feedback without being intrusive. Generally non-interruptive. Example: Tooltips, Snackbar. | +| `--wpds-elevation-medium` | For components that offer additional actions. Example: Menus, Command Palette | +| `--wpds-elevation-large` | For components that confirm decisions or handle necessary interruptions. Example: Modals. | + +### Spacing + +| Variable name | Description | +| ------------------- | ------------------- | +| `--wpds-spacing-05` | Extra small spacing | +| `--wpds-spacing-10` | Small spacing | +| `--wpds-spacing-15` | Medium spacing | +| `--wpds-spacing-20` | Large spacing | +| `--wpds-spacing-30` | Extra large spacing | +| `--wpds-spacing-40` | 2X large spacing | +| `--wpds-spacing-50` | 3X large spacing | +| `--wpds-spacing-60` | 4X large spacing | +| `--wpds-spacing-70` | 5X large spacing | +| `--wpds-spacing-80` | 6X large spacing | + +### Typography + +| Variable name | Description | +| ---------------------------------- | ----------------------- | +| `--wpds-font-family-heading` | Headings font family | +| `--wpds-font-family-body` | Body font family | +| `--wpds-font-family-mono` | Monospace font family | +| `--wpds-font-size-x-small` | Extra small font size | +| `--wpds-font-size-small` | Small font size | +| `--wpds-font-size-medium` | Medium font size | +| `--wpds-font-size-large` | Large font size | +| `--wpds-font-size-x-large` | Extra large font size | +| `--wpds-font-size-2x-large` | 2X large font size | +| `--wpds-font-line-height-x-small` | Extra small line height | +| `--wpds-font-line-height-small` | Small line height | +| `--wpds-font-line-height-medium` | Medium line height | +| `--wpds-font-line-height-large` | Large line height | +| `--wpds-font-line-height-x-large` | Extra large line height | +| `--wpds-font-line-height-2x-large` | 2X large line height | + +## Primitive tokens + +**🚨 Note: These tokens are only private implementation details of the Theme, and should never be referenced / consumed directly in the code.** + +### Color (private) + +| Variable name | Description | +| ------------------------------------------------ | ----------- | +| `--wpds-color-private-primary-bg-fill1` | N/A | +| `--wpds-color-private-primary-fg-fill` | N/A | +| `--wpds-color-private-primary-bg-fill2` | N/A | +| `--wpds-color-private-primary-surface2` | N/A | +| `--wpds-color-private-primary-surface6` | N/A | +| `--wpds-color-private-primary-surface5` | N/A | +| `--wpds-color-private-primary-surface4` | N/A | +| `--wpds-color-private-primary-surface3` | N/A | +| `--wpds-color-private-primary-fg-surface4` | N/A | +| `--wpds-color-private-primary-fg-surface3` | N/A | +| `--wpds-color-private-primary-fg-surface2` | N/A | +| `--wpds-color-private-primary-fg-surface1` | N/A | +| `--wpds-color-private-primary-stroke3` | N/A | +| `--wpds-color-private-primary-stroke4` | N/A | +| `--wpds-color-private-primary-stroke2` | N/A | +| `--wpds-color-private-primary-stroke1` | N/A | +| `--wpds-color-private-primary-bg-fill-dark` | N/A | +| `--wpds-color-private-primary-fg-fill-dark` | N/A | +| `--wpds-color-private-primary-bg-fill-inverted2` | N/A | +| `--wpds-color-private-primary-bg-fill-inverted1` | N/A | +| `--wpds-color-private-primary-fg-fill-inverted` | N/A | +| `--wpds-color-private-primary-surface1` | N/A | +| `--wpds-color-private-info-bg-fill1` | N/A | +| `--wpds-color-private-info-fg-fill` | N/A | +| `--wpds-color-private-info-bg-fill2` | N/A | +| `--wpds-color-private-info-surface2` | N/A | +| `--wpds-color-private-info-surface6` | N/A | +| `--wpds-color-private-info-surface5` | N/A | +| `--wpds-color-private-info-surface4` | N/A | +| `--wpds-color-private-info-surface3` | N/A | +| `--wpds-color-private-info-fg-surface4` | N/A | +| `--wpds-color-private-info-fg-surface3` | N/A | +| `--wpds-color-private-info-fg-surface2` | N/A | +| `--wpds-color-private-info-fg-surface1` | N/A | +| `--wpds-color-private-info-stroke3` | N/A | +| `--wpds-color-private-info-stroke4` | N/A | +| `--wpds-color-private-info-stroke2` | N/A | +| `--wpds-color-private-info-stroke1` | N/A | +| `--wpds-color-private-info-bg-fill-dark` | N/A | +| `--wpds-color-private-info-fg-fill-dark` | N/A | +| `--wpds-color-private-info-bg-fill-inverted2` | N/A | +| `--wpds-color-private-info-bg-fill-inverted1` | N/A | +| `--wpds-color-private-info-fg-fill-inverted` | N/A | +| `--wpds-color-private-info-surface1` | N/A | +| `--wpds-color-private-success-bg-fill1` | N/A | +| `--wpds-color-private-success-fg-fill` | N/A | +| `--wpds-color-private-success-bg-fill2` | N/A | +| `--wpds-color-private-success-surface2` | N/A | +| `--wpds-color-private-success-surface6` | N/A | +| `--wpds-color-private-success-surface5` | N/A | +| `--wpds-color-private-success-surface4` | N/A | +| `--wpds-color-private-success-surface3` | N/A | +| `--wpds-color-private-success-fg-surface4` | N/A | +| `--wpds-color-private-success-fg-surface3` | N/A | +| `--wpds-color-private-success-fg-surface2` | N/A | +| `--wpds-color-private-success-fg-surface1` | N/A | +| `--wpds-color-private-success-stroke3` | N/A | +| `--wpds-color-private-success-stroke4` | N/A | +| `--wpds-color-private-success-stroke2` | N/A | +| `--wpds-color-private-success-stroke1` | N/A | +| `--wpds-color-private-success-bg-fill-dark` | N/A | +| `--wpds-color-private-success-fg-fill-dark` | N/A | +| `--wpds-color-private-success-bg-fill-inverted2` | N/A | +| `--wpds-color-private-success-bg-fill-inverted1` | N/A | +| `--wpds-color-private-success-fg-fill-inverted` | N/A | +| `--wpds-color-private-success-surface1` | N/A | +| `--wpds-color-private-warning-bg-fill1` | N/A | +| `--wpds-color-private-warning-fg-fill` | N/A | +| `--wpds-color-private-warning-bg-fill2` | N/A | +| `--wpds-color-private-warning-surface2` | N/A | +| `--wpds-color-private-warning-surface6` | N/A | +| `--wpds-color-private-warning-surface5` | N/A | +| `--wpds-color-private-warning-surface4` | N/A | +| `--wpds-color-private-warning-surface3` | N/A | +| `--wpds-color-private-warning-fg-surface4` | N/A | +| `--wpds-color-private-warning-fg-surface3` | N/A | +| `--wpds-color-private-warning-fg-surface2` | N/A | +| `--wpds-color-private-warning-fg-surface1` | N/A | +| `--wpds-color-private-warning-stroke3` | N/A | +| `--wpds-color-private-warning-stroke4` | N/A | +| `--wpds-color-private-warning-stroke2` | N/A | +| `--wpds-color-private-warning-stroke1` | N/A | +| `--wpds-color-private-warning-bg-fill-dark` | N/A | +| `--wpds-color-private-warning-fg-fill-dark` | N/A | +| `--wpds-color-private-warning-bg-fill-inverted2` | N/A | +| `--wpds-color-private-warning-bg-fill-inverted1` | N/A | +| `--wpds-color-private-warning-fg-fill-inverted` | N/A | +| `--wpds-color-private-warning-surface1` | N/A | +| `--wpds-color-private-error-bg-fill1` | N/A | +| `--wpds-color-private-error-fg-fill` | N/A | +| `--wpds-color-private-error-bg-fill2` | N/A | +| `--wpds-color-private-error-surface2` | N/A | +| `--wpds-color-private-error-surface6` | N/A | +| `--wpds-color-private-error-surface5` | N/A | +| `--wpds-color-private-error-surface4` | N/A | +| `--wpds-color-private-error-surface3` | N/A | +| `--wpds-color-private-error-fg-surface4` | N/A | +| `--wpds-color-private-error-fg-surface3` | N/A | +| `--wpds-color-private-error-fg-surface2` | N/A | +| `--wpds-color-private-error-fg-surface1` | N/A | +| `--wpds-color-private-error-stroke3` | N/A | +| `--wpds-color-private-error-stroke4` | N/A | +| `--wpds-color-private-error-stroke2` | N/A | +| `--wpds-color-private-error-stroke1` | N/A | +| `--wpds-color-private-error-bg-fill-dark` | N/A | +| `--wpds-color-private-error-fg-fill-dark` | N/A | +| `--wpds-color-private-error-bg-fill-inverted2` | N/A | +| `--wpds-color-private-error-bg-fill-inverted1` | N/A | +| `--wpds-color-private-error-fg-fill-inverted` | N/A | +| `--wpds-color-private-error-surface1` | N/A | +| `--wpds-color-private-bg-surface2` | N/A | +| `--wpds-color-private-bg-bg-fill1` | N/A | +| `--wpds-color-private-bg-fg-fill` | N/A | +| `--wpds-color-private-bg-bg-fill2` | N/A | +| `--wpds-color-private-bg-surface6` | N/A | +| `--wpds-color-private-bg-surface5` | N/A | +| `--wpds-color-private-bg-surface4` | N/A | +| `--wpds-color-private-bg-surface3` | N/A | +| `--wpds-color-private-bg-fg-surface4` | N/A | +| `--wpds-color-private-bg-fg-surface3` | N/A | +| `--wpds-color-private-bg-fg-surface2` | N/A | +| `--wpds-color-private-bg-fg-surface1` | N/A | +| `--wpds-color-private-bg-stroke3` | N/A | +| `--wpds-color-private-bg-stroke4` | N/A | +| `--wpds-color-private-bg-stroke2` | N/A | +| `--wpds-color-private-bg-stroke1` | N/A | +| `--wpds-color-private-bg-bg-fill-dark` | N/A | +| `--wpds-color-private-bg-fg-fill-dark` | N/A | +| `--wpds-color-private-bg-bg-fill-inverted2` | N/A | +| `--wpds-color-private-bg-bg-fill-inverted1` | N/A | +| `--wpds-color-private-bg-fg-fill-inverted` | N/A | +| `--wpds-color-private-bg-surface1` | N/A | diff --git a/packages/theme/package.json b/packages/theme/package.json new file mode 100644 index 00000000000000..9ea59abf15e36e --- /dev/null +++ b/packages/theme/package.json @@ -0,0 +1,57 @@ +{ + "name": "@wordpress/theme", + "version": "0.0.1", + "description": "Theme and context provider for the WordPress Design System.", + "author": "The WordPress Contributors", + "license": "GPL-2.0-or-later", + "keywords": [ + "wordpress", + "gutenberg", + "components" + ], + "homepage": "https://github.com/WordPress/gutenberg/tree/HEAD/packages/theme/README.md", + "repository": { + "type": "git", + "url": "git+https://github.com/WordPress/gutenberg.git", + "directory": "packages/theme" + }, + "bugs": { + "url": "https://github.com/WordPress/gutenberg/issues" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + }, + "main": "build/index.js", + "module": "build-module/index.js", + "exports": { + ".": { + "types": "./build-types/index.d.ts", + "import": "./build-module/index.js", + "default": "./build/index.js" + }, + "./design-tokens.css": "./src/prebuilt/css/design-tokens.css", + "./design-tokens.js": "./src/prebuilt/js/design-tokens.js", + "./package.json": "./package.json", + "./build-style/": "./build-style/" + }, + "react-native": "src/index", + "wpScript": true, + "types": "build-types", + "sideEffects": false, + "dependencies": { + "@wordpress/element": "file:../element", + "@wordpress/private-apis": "file:../private-apis", + "colorjs.io": "^0.5.2" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "publishConfig": { + "access": "public" + }, + "scripts": { + "build": "rimraf src/prebuilt docs && node bin/build-tokens.js && prettier --write tokens/color.json src/prebuilt/* docs/*" + } +} diff --git a/packages/theme/src/color-ramps/index.ts b/packages/theme/src/color-ramps/index.ts new file mode 100644 index 00000000000000..9aa48c8be7ef00 --- /dev/null +++ b/packages/theme/src/color-ramps/index.ts @@ -0,0 +1,155 @@ +/** + * External dependencies + */ +import Color from 'colorjs.io'; + +/** + * Internal dependencies + */ +import { buildRamp } from './lib/index'; +import { clampAccentScaleReferenceLightness } from './lib/utils'; +import { BG_RAMP_CONFIG, ACCENT_RAMP_CONFIG } from './lib/ramp-configs'; +import type { + RampResult as InternalRampResult, + RampDirection, + Ramp, +} from './lib/types'; +import { getCachedContrast } from './lib/cache-utils'; +import { CONTRAST_COMBINATIONS } from './lib/constants'; +export { DEFAULT_SEED_COLORS } from './lib/constants'; + +/** + * Creates a background ramp. + * @param params + * @param params.seed + * @param params.debug + */ +export function buildBgRamp( { + seed, + debug, +}: { + seed: string; + debug?: boolean; +} ): InternalRampResult { + if ( typeof seed !== 'string' || seed.trim() === '' ) { + throw new Error( 'Seed color must be a non-empty string' ); + } + + return buildRamp( seed, BG_RAMP_CONFIG, { debug } ); +} + +const STEP_TO_PIN = 'surface2'; +function getBgRampInfo( ramp: InternalRampResult ): { + mainDirection: RampDirection; + pinLightness: { + stepName: keyof Ramp; + value: number; + }; +} { + return { + mainDirection: ramp.direction, + pinLightness: { + stepName: STEP_TO_PIN, + value: clampAccentScaleReferenceLightness( + new Color( ramp.ramp[ STEP_TO_PIN ].color ).oklch.l, + ramp.direction + ), + }, + }; +} + +/** + * Creates an accent ramp (ie used by primary, success, info, warning and error + * ramps). + * @param params + * @param params.seed + * @param params.bgRamp + * @param params.debug + */ +export function buildAccentRamp( { + seed, + bgRamp, + debug, +}: { + seed: string; + bgRamp?: InternalRampResult; + debug?: boolean; +} ): InternalRampResult { + if ( typeof seed !== 'string' || seed.trim() === '' ) { + throw new Error( 'Seed color must be a non-empty string' ); + } + + const bgRampInfo = bgRamp ? getBgRampInfo( bgRamp ) : undefined; + return buildRamp( seed, ACCENT_RAMP_CONFIG, { + ...bgRampInfo, + debug, + } ); +} + +/** + * Checks that all bg/fg combinations generated by the ramps meet contrast + * targets. + * @param params + * @param params.bgRamp + * @param params.accentRamps + */ +export function checkAccessibleCombinations( { + bgRamp, + accentRamps = [], +}: { + bgRamp: InternalRampResult; + accentRamps?: InternalRampResult[]; +} ) { + const unmetTargets: { + bgName: keyof Ramp; + bgColor: string; + fgName: keyof Ramp; + fgColor: string; + unmetContrast: number; + }[] = []; + + // Assess combinations within each ramp + [ bgRamp, ...accentRamps ].forEach( ( ramp ) => { + CONTRAST_COMBINATIONS.forEach( ( { bgs, fgs, target } ) => { + for ( const bg of bgs ) { + for ( const fg of fgs ) { + const bgColor = new Color( ramp.ramp[ bg ].color ); + const fgColor = new Color( ramp.ramp[ fg ].color ); + if ( getCachedContrast( bgColor, fgColor ) < target ) { + unmetTargets.push( { + bgName: bg, + bgColor: bgColor.toString(), + fgName: fg, + fgColor: fgColor.toString(), + unmetContrast: target, + } ); + } + } + } + } ); + } ); + // Assess each accent ramp's fg color against bg ramp + accentRamps.forEach( ( ramp ) => { + CONTRAST_COMBINATIONS.forEach( ( { bgs, fgs, target } ) => { + for ( const bg of bgs ) { + for ( const fg of fgs ) { + const bgColor = new Color( bgRamp.ramp[ bg ].color ); + const fgColor = new Color( ramp.ramp[ fg ].color ); + if ( getCachedContrast( bgColor, fgColor ) < target ) { + unmetTargets.push( { + bgName: bg, + bgColor: bgColor.toString(), + fgName: fg, + fgColor: fgColor.toString(), + unmetContrast: target, + } ); + } + } + } + } ); + } ); + + return unmetTargets; +} + +export type RampResult = InternalRampResult; diff --git a/packages/theme/src/color-ramps/lib/cache-utils.ts b/packages/theme/src/color-ramps/lib/cache-utils.ts new file mode 100644 index 00000000000000..d0d0e5f6d8a234 --- /dev/null +++ b/packages/theme/src/color-ramps/lib/cache-utils.ts @@ -0,0 +1,56 @@ +/** + * External dependencies + */ +import type Color from 'colorjs.io'; + +/** + * Cache for WCAG contrast calculations + */ +const contrastCache = new Map< string, number >(); + +/** + * Cache for color string representations + */ +const colorStringCache = new Map< Color, string >(); + +/** + * Get cached string representation of a color + * @param color - Color object to stringify + * @return Cached string representation + */ +export function getColorString( color: Color ): string { + let str = colorStringCache.get( color ); + if ( str === undefined ) { + str = color.to( 'srgb' ).toString( { format: 'hex', inGamut: true } ); + colorStringCache.set( color, str ); + } + return str; +} + +/** + * Get cached contrast calculation between two colors + * @param colorA - First color + * @param colorB - Second color + * @return WCAG 2.1 contrast ratio + */ +export function getCachedContrast( colorA: Color, colorB: Color ): number { + const keyA = getColorString( colorA ); + const keyB = getColorString( colorB ); + const cacheKey = + keyA < keyB ? `${ keyA }|${ keyB }` : `${ keyB }|${ keyA }`; + + let contrast = contrastCache.get( cacheKey ); + if ( contrast === undefined ) { + contrast = colorA.contrastWCAG21( colorB ); + contrastCache.set( cacheKey, contrast ); + } + return contrast; +} + +/** + * Clear all caches - useful for memory management or testing + */ +export function clearCaches(): void { + contrastCache.clear(); + colorStringCache.clear(); +} diff --git a/packages/theme/src/color-ramps/lib/constants.ts b/packages/theme/src/color-ramps/lib/constants.ts new file mode 100644 index 00000000000000..9d9903c0f3a31f --- /dev/null +++ b/packages/theme/src/color-ramps/lib/constants.ts @@ -0,0 +1,85 @@ +/** + * External dependencies + */ +import Color from 'colorjs.io'; + +/** + * Internal dependencies + */ +import type { Ramp } from './types'; + +export const WHITE = new Color( '#fff' ).to( 'oklch' ); +export const BLACK = new Color( '#000' ).to( 'oklch' ); + +// Margin added to target contrasts to counter for algorithm approximations +// and rounding errors. +export const UNIVERSAL_CONTRAST_TOPUP = 0.05; + +// When enabling "lighter direction" bias, this is the amount by which +// black text contrast needs to be greater than white text contrast. +// The higher the value, the stronger the preference for white text. +// The current value has been determined empirically as the highest value +// that won't cause the algo not to be able to correctly solve all contrasts. +export const WHITE_TEXT_CONTRAST_MARGIN = 3.1; + +// These values are used as thresholds when trying to match the background +// ramp's lightness while calculating an accent ramp. They prevent the accent +// scale from being pinned to lightness values in the middle of the range, +// which would cause the algorithm to struggle to satisfy the accent scale +// constraints and therefore produce unexpected results. +export const ACCENT_SCALE_BASE_LIGHTNESS_THRESHOLDS = { + lighter: { min: 0.2, max: 0.4 }, + darker: { min: 0.75, max: 0.98 }, +} as const; + +// Minimum lightness difference noticed by the algorithm. +export const LIGHTNESS_EPSILON = 1e-3; + +export const MAX_BISECTION_ITERATIONS = 25; + +export const CONTRAST_COMBINATIONS: { + bgs: ( keyof Ramp )[]; + fgs: ( keyof Ramp )[]; + target: number; +}[] = [ + { + bgs: [ 'surface1', 'surface2', 'surface3' ], + fgs: [ 'fgSurface3', 'fgSurface4' ], + target: 4.5, + }, + { + bgs: [ 'surface4', 'surface5' ], + fgs: [ 'fgSurface4' ], + target: 4.5, + }, + { + bgs: [ 'bgFill1' ], + fgs: [ 'fgFill' ], + target: 4.5, + }, + { + bgs: [ 'bgFillInverted1' ], + fgs: [ 'fgFillInverted' ], + target: 4.5, + }, + { + bgs: [ 'bgFillInverted1' ], + fgs: [ 'fgFillInverted' ], + target: 4.5, + }, + { + bgs: [ 'surface1', 'surface2', 'surface3' ], + fgs: [ 'stroke3' ], + target: 3, + }, +]; + +// Used when generating the DTCG tokens and the static color ramps. +export const DEFAULT_SEED_COLORS = { + bg: '#f8f8f8', + primary: '#3858e9', + info: '#0090ff', + success: '#4ab866', + warning: '#f0b849', + error: '#cc1818', +}; diff --git a/packages/theme/src/color-ramps/lib/find-color-with-constraints.ts b/packages/theme/src/color-ramps/lib/find-color-with-constraints.ts new file mode 100644 index 00000000000000..2e8d9d668ccef6 --- /dev/null +++ b/packages/theme/src/color-ramps/lib/find-color-with-constraints.ts @@ -0,0 +1,190 @@ +/** + * External dependencies + */ +import Color from 'colorjs.io'; + +/** + * Internal dependencies + */ +import { clampToGamut } from './utils'; +import { + WHITE, + BLACK, + LIGHTNESS_EPSILON, + MAX_BISECTION_ITERATIONS, +} from './constants'; +import { getCachedContrast } from './cache-utils'; +import { type TaperChromaOptions, taperChroma } from './taper-chroma'; + +/** + * Solve for L such that: + * - the L applied to the seed meets the contrast target against the reference + * - the search is performed in one direction (ie lighter / darker) + * - more constraints can be applied around lightness + * - chroma could be tapered + * @param reference + * @param seed + * @param target + * @param direction + * @param options + * @param options.strict + * @param options.debug + * @param options.lightnessConstraint + * @param options.lightnessConstraint.type + * @param options.lightnessConstraint.value + * @param options.taperChromaOptions + */ +export function findColorMeetingRequirements( + reference: Color, + seed: Color, + target: number, + direction: 'lighter' | 'darker', + { + lightnessConstraint, + taperChromaOptions, + strict = true, + debug = false, + }: { + lightnessConstraint?: { + type: 'force' | 'onlyIfSucceeds'; + value: number; + }; + taperChromaOptions?: TaperChromaOptions; + strict?: boolean; + debug?: boolean; + } = {} +): { color: Color; reached: boolean; achieved: number } { + // A target of 1 means same color. + // A target lower than 1 doesn't make sense. + if ( target <= 1 ) { + return { color: seed.clone(), reached: true, achieved: 1 }; + } + + if ( lightnessConstraint ) { + // Apply a specific L value. + // Useful when pinning a step to a specific lightness, of to specify + // min/max L values. + let newL = lightnessConstraint.value; + let newC = seed.oklch.c; + + if ( taperChromaOptions ) { + ( { l: newL, c: newC } = taperChroma( + seed, + newL, + taperChromaOptions + ) ); + } + + const colorWithExactL = clampToGamut( + new Color( 'oklch', [ newL, newC, seed.oklch.h ] ) + ); + const exactLContrast = getCachedContrast( reference, colorWithExactL ); + + if ( debug ) { + // eslint-disable-next-line no-console + console.log( + `Succeeded with ${ lightnessConstraint.type } lightness`, + lightnessConstraint.value, + colorWithExactL.oklch.l + ); + } + + // If the L constraint is of "force" type, apply it even when it doesn't + // meet the contrast target. + if ( + lightnessConstraint.type === 'force' || + exactLContrast >= target + ) { + return { + color: colorWithExactL, + reached: exactLContrast >= target, + achieved: exactLContrast, + }; + } + } + + // Set the boundary based on the direction. + const mostContrastingL = direction === 'lighter' ? 1 : 0; + const mostContrastingColor = direction === 'lighter' ? WHITE : BLACK; + const highestPossibleContrast = getCachedContrast( + reference, + mostContrastingColor + ); + + // If even the most contrasting color can't reach the target, + // the target is unreachable. + if ( highestPossibleContrast < target ) { + if ( strict ) { + throw new Error( + `Contrast target ${ target.toFixed( + 2 + ) }:1 unreachable in ${ direction } direction against ${ mostContrastingColor.toString() }` + + `(boundary achieves ${ highestPossibleContrast.toFixed( + 3 + ) }:1).` + ); + } + + if ( debug ) { + // eslint-disable-next-line no-console + console.log( + 'Did not succeeded because it reached the limit', + mostContrastingL + ); + } + return { + color: mostContrastingColor, + reached: false, + achieved: highestPossibleContrast, + }; + } + + // Bracket: low fails, high meets. + // Originally this was seed.oklch.l — although it's an assumption that works + // only when we know for sure the direction of the search. + // TODO: can we bring this back to seed.oklch.l ? + let worseL = reference.oklch.l; + let betterL = mostContrastingL; + + let bestContrastFound = highestPossibleContrast; + let resultingColor = mostContrastingColor; + + for ( + let i = 0; + i < MAX_BISECTION_ITERATIONS && + Math.abs( betterL - worseL ) > LIGHTNESS_EPSILON; + i++ + ) { + let newL = ( worseL + betterL ) / 2; + let newC = seed.oklch.c; + + if ( taperChromaOptions ) { + ( { l: newL, c: newC } = taperChroma( + seed, + newL, + taperChromaOptions + ) ); + } + + const newColor = clampToGamut( + new Color( 'oklch', [ newL, newC, seed.oklch.h ] ) + ); + const newContrast = getCachedContrast( reference, newColor ); + + if ( newContrast >= target ) { + betterL = newL; + // Only update the resulting color when the target is met, this ensuring + // at the end of the search the target is always met. + bestContrastFound = newContrast; + resultingColor = newColor; + } else { + worseL = newL; + } + } + + return { + color: resultingColor, + reached: true, + achieved: bestContrastFound, + }; +} diff --git a/packages/theme/src/color-ramps/lib/index.ts b/packages/theme/src/color-ramps/lib/index.ts new file mode 100644 index 00000000000000..8cdd183cb86ae0 --- /dev/null +++ b/packages/theme/src/color-ramps/lib/index.ts @@ -0,0 +1,369 @@ +/** + * External dependencies + */ +import Color from 'colorjs.io'; + +/** + * Internal dependencies + */ +import { getCachedContrast, getColorString } from './cache-utils'; +import { findColorMeetingRequirements } from './find-color-with-constraints'; +import { + clampToGamut, + sortByDependency, + computeBetterFgColorDirection, + adjustContrastTarget, +} from './utils'; + +import type { + FollowDirection, + Ramp, + RampDirection, + RampConfig, + RampResult, +} from './types'; +import { LIGHTNESS_EPSILON, MAX_BISECTION_ITERATIONS } from './constants'; + +/** + * Calculate a complete color ramp based on the provided configuration. + * + * @param params - The calculation parameters + * @param params.seed - The base color to build the ramp from + * @param params.sortedSteps - Steps sorted in dependency order + * @param params.config - Ramp configuration defining contrast requirements + * @param params.mainDir - Primary direction for the ramp (lighter/darker) + * @param params.oppDir - Opposite direction from mainDir + * @param params.pinLightness - Optional lightness override for a given step + * @param params.pinLightness.stepName + * @param params.pinLightness.value + * @param params.debug + * @return Object containing ramp results and satisfaction status + */ +function calculateRamp( { + seed, + sortedSteps, + config, + mainDir, + oppDir, + pinLightness, + debug = false, +}: { + seed: Color; + sortedSteps: ( keyof Ramp )[]; + config: RampConfig; + mainDir: RampDirection; + oppDir: RampDirection; + pinLightness?: { + stepName: keyof Ramp; + value: number; + }; + debug?: boolean; +} ) { + const rampResults = {} as Record< + keyof Ramp, + { color: string; warning: boolean } + >; + let SATISFIED_ALL_CONTRAST_REQUIREMENTS = true; + let UNSATISFIED_DIRECTION: RampDirection = 'lighter'; + let MAX_WEIGHTED_DEFICIT = 0; + + // Keep track of the calculated colors, as they are going to be useful + // when other colors reference them. + const calculatedColors = new Map< keyof Ramp | 'seed', Color >(); + calculatedColors.set( 'seed', seed ); + + for ( const stepName of sortedSteps ) { + const { + contrast, + lightness: stepLightnessConstraint, + taperChromaOptions, + sameAsIfPossible, + } = config[ stepName ]; + const referenceColor = calculatedColors.get( contrast.reference ); + + if ( ! referenceColor ) { + throw new Error( + `Reference color for step ${ stepName } not found: ${ contrast.reference }` + ); + } + + // Check if we can reuse color from the `sameAsIfPossible` config option + if ( sameAsIfPossible ) { + const candidateColor = calculatedColors.get( sameAsIfPossible ); + if ( candidateColor ) { + const candidateContrast = getCachedContrast( + referenceColor, + candidateColor + ); + const adjustedTarget = adjustContrastTarget( contrast.target ); + // If the candidate meets the contrast requirement, use it + if ( candidateContrast >= adjustedTarget ) { + // Store the reused color + calculatedColors.set( stepName, candidateColor ); + rampResults[ stepName ] = { + color: getColorString( candidateColor ), + warning: false, + }; + + continue; // Skip to next step + } + } + } + + function computeDirection( + color: Color, + followDirection: FollowDirection + ): RampDirection { + if ( followDirection === 'main' ) { + return mainDir; + } + + if ( followDirection === 'opposite' ) { + return oppDir; + } + + if ( followDirection === 'best' ) { + return computeBetterFgColorDirection( + color, + contrast.preferLighter + ).better; + } + + return followDirection; + } + + const computedDir = computeDirection( + referenceColor, + contrast.followDirection + ); + + const adjustedTarget = adjustContrastTarget( contrast.target ); + + // Define the lightness constraint, if needed. + let lightnessConstraint; + if ( pinLightness?.stepName === stepName ) { + lightnessConstraint = { + value: pinLightness.value, + type: 'force', + } as const; + } else if ( stepLightnessConstraint ) { + lightnessConstraint = { + value: stepLightnessConstraint( computedDir ), + type: 'onlyIfSucceeds', + } as const; + } + + // Calculate the color meeting the requirements + const searchResults = findColorMeetingRequirements( + referenceColor, + seed, + adjustedTarget, + computedDir, + { + strict: false, + lightnessConstraint, + taperChromaOptions, + debug, + } + ); + + // When the target contrast is not met, take note of it and use + // that information to guide the ramp calculation bisection. + if ( ! searchResults.reached && ! contrast.ignoreWhenAdjustingSeed ) { + SATISFIED_ALL_CONTRAST_REQUIREMENTS = false; + + // Calculate constraint failure severity for seed optimization + // Use the relative deficit size, weighted by how changing the seed would impact this constraint + const deficitVsTarget = adjustedTarget - searchResults.achieved; + + // Weight the deficit by how much seed adjustment would help this constraint + // If seed has low contrast vs reference, adjusting seed has high impact + // If seed has high contrast vs reference, adjusting seed has low impact + const impactWeight = 1 / getCachedContrast( seed, referenceColor ); + const weightedDeficit = deficitVsTarget * impactWeight; + + // Track the most impactful failure for seed optimization + if ( weightedDeficit > MAX_WEIGHTED_DEFICIT ) { + MAX_WEIGHTED_DEFICIT = weightedDeficit; + UNSATISFIED_DIRECTION = computedDir; + } + } + + // Store calculated color for future dependencies + calculatedColors.set( stepName, searchResults.color ); + + // Add to results + rampResults[ stepName ] = { + color: getColorString( searchResults.color ), + warning: + ! contrast.ignoreWhenAdjustingSeed && ! searchResults.reached, + }; + } + + return { + rampResults, + SATISFIED_ALL_CONTRAST_REQUIREMENTS, + UNSATISFIED_DIRECTION, + }; +} + +export function buildRamp( + seedArg: string, + config: RampConfig, + { + mainDirection, + pinLightness, + debug = false, + rescaleToFitContrastTargets = true, + }: { + mainDirection?: RampDirection; + pinLightness?: { + stepName: keyof Ramp; + value: number; + }; + rescaleToFitContrastTargets?: boolean; + debug?: boolean; + } = {} +): RampResult { + let seed: Color; + try { + seed = clampToGamut( new Color( seedArg ) ); + } catch ( error ) { + throw new Error( + `Invalid seed color "${ seedArg }": ${ + error instanceof Error ? error.message : 'Unknown error' + }` + ); + } + + let mainDir: RampDirection = 'lighter'; + let oppDir: RampDirection = 'darker'; + + if ( mainDirection ) { + mainDir = mainDirection; + oppDir = mainDirection === 'darker' ? 'lighter' : 'darker'; + } else { + const { better, worse } = computeBetterFgColorDirection( seed ); + mainDir = better; + oppDir = worse; + } + + // Get the correct calculation order based on dependencies + const sortedSteps = sortByDependency( config ); + + // Calculate the ramp with the initial seed. + const { + rampResults, + SATISFIED_ALL_CONTRAST_REQUIREMENTS, + UNSATISFIED_DIRECTION, + } = calculateRamp( { + seed, + sortedSteps, + config, + mainDir, + oppDir, + pinLightness, + debug, + } ); + const toReturn = { + ramp: rampResults, + direction: mainDir, + } as RampResult; + + if ( debug ) { + // eslint-disable-next-line no-console + console.log( `First run`, { + SATISFIED_ALL_CONTRAST_REQUIREMENTS, + UNSATISFIED_DIRECTION, + seed: seed.toString(), + sortedSteps, + config, + mainDir, + oppDir, + pinLightness, + } ); + } + + if ( + ! SATISFIED_ALL_CONTRAST_REQUIREMENTS && + rescaleToFitContrastTargets + ) { + let worseSeedL = seed.oklch.l; + // For a scale with the "lighter" direction, the contrast can be improved + // by darkening the seed. For "darker" direction, by lightening the seed. + let betterSeedL = UNSATISFIED_DIRECTION === 'lighter' ? 0 : 1; + + // Binary search: try a new seed and recompute the whole ramp + // (TODO: try a smarter approach?) + for ( + let i = 0; + i < MAX_BISECTION_ITERATIONS && + Math.abs( betterSeedL - worseSeedL ) > LIGHTNESS_EPSILON; + i++ + ) { + const newSeed = clampToGamut( + seed.clone().set( { + l: ( worseSeedL + betterSeedL ) / 2, + } ) + ); + + if ( debug ) { + // eslint-disable-next-line no-console + console.log( `Iteration ${ i }`, { + worseSeedL, + newSeedL: ( worseSeedL + betterSeedL ) / 2, + betterSeedL, + } ); + } + + const iterationResults = calculateRamp( { + seed: newSeed, + sortedSteps, + config, + mainDir, + oppDir, + pinLightness, + debug, + } ); + + if ( iterationResults.SATISFIED_ALL_CONTRAST_REQUIREMENTS ) { + betterSeedL = newSeed.oklch.l; + // Only update toReturn when the ramp satisfies all constraints. + toReturn.ramp = iterationResults.rampResults; + } else if ( UNSATISFIED_DIRECTION !== mainDir ) { + // Failing constraint is in opposite direction to main ramp direction + // We've moved too far in mainDir, constrain the search + betterSeedL = newSeed.oklch.l; + } else { + // Failing constraint is in same direction as main ramp direction + // We haven't moved far enough in mainDir, continue searching + worseSeedL = newSeed.oklch.l; + } + + if ( debug ) { + // eslint-disable-next-line no-console + console.log( `Retry #${ i }`, { + SATISFIED_ALL_CONTRAST_REQUIREMENTS, + UNSATISFIED_DIRECTION, + seed: newSeed.toString(), + sortedSteps, + config, + mainDir, + oppDir, + pinLightness, + } ); + } + } + } + + // Swap surface1 and surface3 for darker ramps to maintain visual elevation hierarchy. + // This ensures surface1 appears "behind" surface2, and surface3 appears "in front", + // regardless of the ramp's main direction. + if ( mainDir === 'darker' ) { + const tmpSurface1 = toReturn.ramp.surface1; + toReturn.ramp.surface1 = toReturn.ramp.surface3; + toReturn.ramp.surface3 = tmpSurface1; + } + + return toReturn; +} diff --git a/packages/theme/src/color-ramps/lib/ramp-configs.ts b/packages/theme/src/color-ramps/lib/ramp-configs.ts new file mode 100644 index 00000000000000..b46760a2e6e7ad --- /dev/null +++ b/packages/theme/src/color-ramps/lib/ramp-configs.ts @@ -0,0 +1,309 @@ +/** + * Internal dependencies + */ +import type { RampStepConfig, RampConfig, RampDirection } from './types'; +import type { TaperChromaOptions } from './taper-chroma'; + +const lightnessConstraintForegroundHighContrast = ( + direction: RampDirection +) => + direction === 'lighter' + ? 0.9551 // lightness of #f0f0f0 (ie $gray-100) + : 0.235; // lightness of #1e1e1e (ie $gray-900) +const lightnessConstraintForegroundMediumContrast = ( + direction: RampDirection +) => + direction === 'lighter' + ? 0.77 // lightness of #b4b4b4 + : 0.56; // lightness of #747474 +const lightnessConstraintBgFill = ( direction: RampDirection ) => + direction === 'lighter' + ? 0.67 // lightness of #969696 (7:1 vs black) + : 0.45; // lightness of #555555 (7:1 vs white) + +const BG_SURFACE_TAPER_CHROMA: TaperChromaOptions = { + alpha: 0.7, +}; +const FG_TAPER_CHROMA: TaperChromaOptions = { + alpha: 0.6, + kLight: 0.2, + kDark: 0.2, +}; +const STROKE_TAPER_CHROMA: TaperChromaOptions = { + alpha: 0.6, + radiusDark: 0.01, + radiusLight: 0.01, + kLight: 0.8, + kDark: 0.8, +}; +const ACCENT_SURFACE_TAPER_CHROMA: TaperChromaOptions = { + alpha: 0.75, + radiusDark: 0.01, + radiusLight: 0.01, +}; + +const fgSurface4Config: RampStepConfig = { + contrast: { + reference: 'surface3', + followDirection: 'main', + target: 7, + preferLighter: true, + }, + lightness: lightnessConstraintForegroundHighContrast, + taperChromaOptions: FG_TAPER_CHROMA, +}; + +export const BG_RAMP_CONFIG: RampConfig = { + // Surface + surface1: { + contrast: { + reference: 'surface2', + followDirection: 'opposite', + target: 1.02, + ignoreWhenAdjustingSeed: true, + }, + taperChromaOptions: BG_SURFACE_TAPER_CHROMA, + }, + surface2: { + contrast: { + reference: 'seed', + followDirection: 'main', + target: 1, + }, + }, + surface3: { + contrast: { + reference: 'surface2', + followDirection: 'main', + target: 1.02, + }, + taperChromaOptions: BG_SURFACE_TAPER_CHROMA, + }, + surface4: { + contrast: { + reference: 'surface2', + followDirection: 'main', + target: 1.08, + }, + taperChromaOptions: BG_SURFACE_TAPER_CHROMA, + }, + surface5: { + contrast: { + reference: 'surface2', + followDirection: 'main', + target: 1.2, + }, + taperChromaOptions: BG_SURFACE_TAPER_CHROMA, + }, + surface6: { + contrast: { + reference: 'surface2', + followDirection: 'main', + target: 1.4, + }, + taperChromaOptions: BG_SURFACE_TAPER_CHROMA, + }, + // Bg fill + bgFill1: { + contrast: { + reference: 'surface2', + followDirection: 'main', + target: 4, + }, + lightness: lightnessConstraintBgFill, + }, + bgFill2: { + contrast: { + reference: 'bgFill1', + followDirection: 'main', + target: 1.2, + }, + }, + bgFillInverted1: { + contrast: { + reference: 'bgFillInverted2', + followDirection: 'opposite', + target: 1.2, + }, + }, + bgFillInverted2: fgSurface4Config, + bgFillDark: { + contrast: { + reference: 'surface3', + followDirection: 'darker', // This is what causes the token to be always dark + target: 7, + ignoreWhenAdjustingSeed: true, + }, + lightness: lightnessConstraintForegroundHighContrast, + taperChromaOptions: FG_TAPER_CHROMA, + }, + // Stroke + stroke1: { + contrast: { + reference: 'stroke3', + followDirection: 'opposite', + target: 2.2, + }, + taperChromaOptions: STROKE_TAPER_CHROMA, + }, + stroke2: { + contrast: { + reference: 'stroke3', + followDirection: 'opposite', + target: 1.5, + }, + taperChromaOptions: STROKE_TAPER_CHROMA, + }, + stroke3: { + contrast: { + reference: 'surface3', + followDirection: 'main', + target: 3, + }, + taperChromaOptions: STROKE_TAPER_CHROMA, + }, + stroke4: { + contrast: { + reference: 'stroke3', + followDirection: 'main', + target: 1.5, + }, + taperChromaOptions: STROKE_TAPER_CHROMA, + }, + // fgSurface + fgSurface1: { + contrast: { + reference: 'surface3', + followDirection: 'main', + target: 2, + preferLighter: true, + }, + taperChromaOptions: FG_TAPER_CHROMA, + }, + fgSurface2: { + contrast: { + reference: 'surface3', + followDirection: 'main', + target: 3, + preferLighter: true, + }, + taperChromaOptions: FG_TAPER_CHROMA, + }, + fgSurface3: { + contrast: { + reference: 'surface3', + followDirection: 'main', + target: 4.5, + preferLighter: true, + }, + lightness: lightnessConstraintForegroundMediumContrast, + taperChromaOptions: FG_TAPER_CHROMA, + }, + fgSurface4: fgSurface4Config, + // fgFill + fgFill: { + contrast: { + reference: 'bgFill1', + followDirection: 'best', + target: 4.5, + preferLighter: true, + }, + lightness: lightnessConstraintForegroundHighContrast, + taperChromaOptions: FG_TAPER_CHROMA, + }, + fgFillInverted: { + contrast: { + reference: 'bgFillInverted1', + followDirection: 'best', + target: 4.5, + preferLighter: true, + }, + lightness: lightnessConstraintForegroundHighContrast, + taperChromaOptions: FG_TAPER_CHROMA, + }, + fgFillDark: { + contrast: { + reference: 'bgFillDark', + followDirection: 'best', + target: 4.5, + preferLighter: true, + }, + lightness: lightnessConstraintForegroundHighContrast, + taperChromaOptions: FG_TAPER_CHROMA, + }, +}; + +// BG_RAMP: seed => surface2 => {bgFill, surface3 => all other tokens} +// ACCENT_RAMP: seed => bgFill1 => surface2 => surface3 => all other tokens +export const ACCENT_RAMP_CONFIG: RampConfig = { + ...BG_RAMP_CONFIG, + surface1: { + ...BG_RAMP_CONFIG.surface1, + taperChromaOptions: ACCENT_SURFACE_TAPER_CHROMA, + }, + surface2: { + contrast: { + reference: 'bgFill1', + followDirection: 'opposite', + target: BG_RAMP_CONFIG.bgFill1.contrast.target, + ignoreWhenAdjustingSeed: true, + }, + taperChromaOptions: ACCENT_SURFACE_TAPER_CHROMA, + }, + surface3: { + ...BG_RAMP_CONFIG.surface3, + taperChromaOptions: ACCENT_SURFACE_TAPER_CHROMA, + }, + surface4: { + ...BG_RAMP_CONFIG.surface4, + taperChromaOptions: ACCENT_SURFACE_TAPER_CHROMA, + }, + surface5: { + ...BG_RAMP_CONFIG.surface5, + taperChromaOptions: ACCENT_SURFACE_TAPER_CHROMA, + }, + surface6: { + ...BG_RAMP_CONFIG.surface6, + taperChromaOptions: ACCENT_SURFACE_TAPER_CHROMA, + }, + bgFill1: { + contrast: { + reference: 'seed', + followDirection: 'main', + target: 1, + }, + }, + stroke1: { + ...BG_RAMP_CONFIG.stroke1, + }, + stroke2: { + ...BG_RAMP_CONFIG.stroke2, + }, + stroke3: { + ...BG_RAMP_CONFIG.stroke3, + sameAsIfPossible: 'fgSurface3', + taperChromaOptions: undefined, + }, + stroke4: { + ...BG_RAMP_CONFIG.stroke4, + taperChromaOptions: undefined, + }, + // fgSurface: do not de-saturate + fgSurface1: { + ...BG_RAMP_CONFIG.fgSurface1, + taperChromaOptions: undefined, + }, + fgSurface2: { + ...BG_RAMP_CONFIG.fgSurface2, + taperChromaOptions: undefined, + }, + fgSurface3: { + ...BG_RAMP_CONFIG.fgSurface3, + taperChromaOptions: undefined, + sameAsIfPossible: 'bgFill1', + }, + fgSurface4: { + ...BG_RAMP_CONFIG.fgSurface4, + taperChromaOptions: undefined, + }, +}; diff --git a/packages/theme/src/color-ramps/lib/taper-chroma.ts b/packages/theme/src/color-ramps/lib/taper-chroma.ts new file mode 100644 index 00000000000000..deb00acec672b8 --- /dev/null +++ b/packages/theme/src/color-ramps/lib/taper-chroma.ts @@ -0,0 +1,226 @@ +// npm i colorjs.io +/** + * External dependencies + */ +import Color from 'colorjs.io'; + +export interface TaperChromaOptions { + gamut?: 'p3' | 'srgb'; // target gamut (default "p3") + alpha?: number; // base fraction of Cmax at target (default 0.62) + carry?: number; // seed vividness carry exponent β in [0..1] (default 0.5) + cUpperBound?: number; // hard search cap for C (default 0.45) + // Continuous taper around the seed (desaturate both sides slightly) + radiusLight?: number; // distance in L where kLight is reached (default 0.20) + radiusDark?: number; // distance in L where kDark is reached (default 0.20) + kLight?: number; // floor multiplier near lighter side (default 0.85) + kDark?: number; // floor multiplier near darker side (default 0.85) + // Achromatic handling + hueFallback?: number; // degrees: if seed is achromatic and you still want color + achromaEpsilon?: number; // ≤ this chroma → treat as achromatic (default 0.005) +} + +/** + * Given the seed and the target lightness, tapers the chroma smoothly. + * - C_intended = Cmax(Lt,H0) * alpha * (seedRelative^carry) + * - Continuous taper vs |Lt - Ls| to softly reduce chroma for neighbors + * - Downward-only clamp on C (preserve L & H) + * @param seed + * @param lTarget + * @param options + */ +export function taperChroma( + seed: Color, // already OKLCH + lTarget: number, // [0..1] + options: TaperChromaOptions = {} +): { l: number; c: number } { + const gamut = options.gamut ?? 'p3'; + const alpha = options.alpha ?? 0.65; // 0.7-0.8 works well for accent surface + const carry = options.carry ?? 0.5; + const cUpperBound = options.cUpperBound ?? 0.45; + const radiusLight = options.radiusLight ?? 0.2; + const radiusDark = options.radiusDark ?? 0.2; + const kLight = options.kLight ?? 0.85; + const kDark = options.kDark ?? 0.85; + const achromaEpsilon = options.achromaEpsilon ?? 0.005; + + const cSeed = Math.max( 0, seed.oklch.c ); + let hSeed = Number( seed.oklch.h ); + + const chromaIsTiny = cSeed < achromaEpsilon; + const hueIsInvalid = ! Number.isFinite( hSeed ); + + if ( chromaIsTiny || hueIsInvalid ) { + if ( typeof options.hueFallback === 'number' ) { + hSeed = normalizeHue( options.hueFallback ); + } else { + // Respect achromatic intent: grayscale at target L + return new Color( 'oklch', [ clamp01( lTarget ), 0, 0 ] ); + } + } + + // Capacity at seed and target + const lSeed = clamp01( seed.oklch.l ); + const cmaxSeed = getCachedMaxChromaAtLH( lSeed, hSeed, gamut, cUpperBound ); + const cmaxTarget = getCachedMaxChromaAtLH( + clamp01( lTarget ), + hSeed, + gamut, + cUpperBound + ); + + // Seed vividness ratio (hue-fair normalization) + let seedRelative = 0; + const denom = cmaxSeed > 0 ? cmaxSeed : 1e-6; + seedRelative = clamp01( cSeed / denom ); + + // Intended chroma from local capacity, tempered by seed vividness + const cIntendedBase = alpha * cmaxTarget; + const cWithCarry = + cIntendedBase * Math.pow( seedRelative, clamp01( carry ) ); + + // Gentle, symmetric desaturation vs distance in L + const t = continuousTaper( lSeed, lTarget, { + radiusLight, + radiusDark, + kLight, + kDark, + } ); + let cPlanned = cWithCarry * t; + + // Downward-only clamp (preserve L & H) + const lOut = clamp01( lTarget ); + const candidate = new Color( 'oklch', [ lOut, cPlanned, hSeed ] ); + if ( ! candidate.inGamut( gamut ) ) { + const cap = Math.min( cPlanned, cUpperBound ); + cPlanned = getCachedMaxChromaAtLH( lOut, hSeed, gamut, cap ); + } + + cPlanned = Math.min( cPlanned, cSeed ); + + return { l: lOut, c: cPlanned }; +} + +/* ---------------- helpers & caches ---------------- */ + +function clamp01( x: number ): number { + if ( x < 0 ) { + return 0; + } + if ( x > 1 ) { + return 1; + } + return x; +} +function normalizeHue( h: number ): number { + let hue = h % 360; + if ( hue < 0 ) { + hue += 360; + } + return hue; +} +function raisedCosine( u: number ): number { + const x = clamp01( u ); + return 0.5 - 0.5 * Math.cos( Math.PI * x ); +} + +/** + * smooth, distance-from-seed chroma taper (raised-cosine per side) + * @param seedL + * @param targetL + * @param opts + * @param opts.radiusLight + * @param opts.radiusDark + * @param opts.kLight + * @param opts.kDark + */ +function continuousTaper( + seedL: number, + targetL: number, + opts: { + radiusLight: number; + radiusDark: number; + kLight: number; + kDark: number; + } +): number { + const d = targetL - seedL; + if ( d >= 0 ) { + const u = opts.radiusLight > 0 ? Math.abs( d ) / opts.radiusLight : 1; + const w = raisedCosine( u > 1 ? 1 : u ); + return 1 - ( 1 - opts.kLight ) * w; + } + const u = opts.radiusDark > 0 ? Math.abs( d ) / opts.radiusDark : 1; + const w = raisedCosine( u > 1 ? 1 : u ); + return 1 - ( 1 - opts.kDark ) * w; +} + +/* ---- chroma-capacity queries with small caches ---- */ + +const maxChromaCache = new Map< string, number >(); + +function keyMax( + l: number, + h: number, + gamut: 'p3' | 'srgb', + cap: number +): string { + // Quantize to keep cache compact + const lq = quantize( l, 1e-3 ); + const hq = quantize( normalizeHue( h ), 1e-1 ); + const cq = quantize( cap, 1e-3 ); + return `${ gamut }|L:${ lq }|H:${ hq }|cap:${ cq }`; +} +function quantize( x: number, step: number ): number { + const k = Math.round( x / step ); + return k * step; +} + +function getCachedMaxChromaAtLH( + l: number, + h: number, + gamut: 'p3' | 'srgb', + cap: number +): number { + const key = keyMax( l, h, gamut, cap ); + const hit = maxChromaCache.get( key ); + if ( typeof hit === 'number' ) { + return hit; + } + + const computed = maxInGamutChromaAtLH( l, h, gamut, cap ); + maxChromaCache.set( key, computed ); + return computed; +} + +/** + * Binary-search the max in-gamut chroma at fixed (L,H) in the target gamut + * @param l + * @param h + * @param gamut + * @param cap + */ +function maxInGamutChromaAtLH( + l: number, + h: number, + gamut: 'p3' | 'srgb', + cap: number +): number { + let lo = 0; + let hi = cap; + let ok = 0; + + const lFixed = clamp01( l ); + const hFixed = normalizeHue( h ); + + for ( let i = 0; i < 18; i++ ) { + const mid = ( lo + hi ) / 2; + const probe = new Color( 'oklch', [ lFixed, mid, hFixed ] ); + if ( probe.inGamut( gamut ) ) { + ok = mid; + lo = mid; + } else { + hi = mid; + } + } + return ok; +} diff --git a/packages/theme/src/color-ramps/lib/types.ts b/packages/theme/src/color-ramps/lib/types.ts new file mode 100644 index 00000000000000..83dfa79f2337ee --- /dev/null +++ b/packages/theme/src/color-ramps/lib/types.ts @@ -0,0 +1,90 @@ +/** + * Internal dependencies + */ +import type { TaperChromaOptions } from './taper-chroma'; + +export type Ramp = { + // Backgrounds for surfaces (nuanced, slight variations compared to bg) + surface1: string; + surface2: string; + surface3: string; + surface4: string; + surface5: string; + surface6: string; + // Strokes + stroke1: string; + stroke2: string; + stroke3: string; + stroke4: string; + // Stronger backgrounds for primary UI elements + bgFill1: string; + bgFill2: string; + bgFillInverted1: string; + bgFillInverted2: string; + bgFillDark: string; + // Foreground (text, icon) colors + fgSurface1: string; + fgSurface2: string; + fgSurface3: string; + fgSurface4: string; + // Foreground (text, icon) colors on top of bgFill + fgFill: string; + fgFillInverted: string; + fgFillDark: string; +}; + +export type RampDirection = 'lighter' | 'darker'; +export type FollowDirection = 'main' | 'opposite' | 'best' | RampDirection; +export type ContrastRequirement = { + /** The reference color against which to calculate the contrast */ + reference: keyof Ramp | 'seed'; + /** + * Which direction should the algorithm search a matching color in: + * - main: follow the same direction as the ramp's main direction + * - opposite: follow the opposite direction of the ramp + * - best: pick the direction that has the most contrast headroom + * - hardcoded ramp direction (useful for generating colors that always + * light/dark regardless of the ramp direction) + */ + followDirection: FollowDirection; + /** + * Prefer "lighter" direction when searching for a contrasting color. + * Especially useful for foreground color to counter the poor results that the + * WCAG algo gives when contrasting white text over mid-lightness backgrounds. + */ + preferLighter?: boolean; + /** + * The contrast target to meet. + */ + target: number; + /** + * When true, the algorithm won't count a failure in meeting the contrast + * target as a reason to recalculate the ramp. + */ + ignoreWhenAdjustingSeed?: boolean; +}; + +export type RampStepConfig = { + contrast: ContrastRequirement; + lightness?: ( direction: RampDirection ) => number; + taperChromaOptions?: TaperChromaOptions; + /** + * If specified, try to reuse the color from this step if it meets + * the contrast requirements. This reduces the number of unique colors + * in the ramp and improves consistency. + */ + sameAsIfPossible?: keyof Ramp; +}; + +export type RampConfig = Record< keyof Ramp, RampStepConfig >; + +export type RampResult = { + ramp: Record< + keyof Ramp, + { + color: string; + warning: boolean; + } + >; + direction: RampDirection; +}; diff --git a/packages/theme/src/color-ramps/lib/utils.ts b/packages/theme/src/color-ramps/lib/utils.ts new file mode 100644 index 00000000000000..a0f25dfbc10470 --- /dev/null +++ b/packages/theme/src/color-ramps/lib/utils.ts @@ -0,0 +1,161 @@ +/** + * External dependencies + */ +import type Color from 'colorjs.io'; + +/** + * Internal dependencies + */ +import { + WHITE, + BLACK, + UNIVERSAL_CONTRAST_TOPUP, + WHITE_TEXT_CONTRAST_MARGIN, + ACCENT_SCALE_BASE_LIGHTNESS_THRESHOLDS, +} from './constants'; +import type { Ramp, RampStepConfig, RampDirection } from './types'; +import { getCachedContrast } from './cache-utils'; + +/** + * Make sure that a color is valid in the p3 gamut, and converts it to oklch. + * @param c + */ +export const clampToGamut = ( c: Color ) => + c + .toGamut( { space: 'p3', method: 'css' } ) // map into Display-P3 using CSS OKLCH method + .to( 'oklch' ); + +/** + * Build a dependency graph from the steps configuration + * @param config - The steps configuration object + */ +function buildDependencyGraph( config: Record< keyof Ramp, RampStepConfig > ): { + dependencies: Map< keyof Ramp, ( keyof Ramp | 'seed' )[] >; + dependents: Map< keyof Ramp | 'seed', ( keyof Ramp )[] >; +} { + const dependencies = new Map< keyof Ramp, ( keyof Ramp | 'seed' )[] >(); + const dependents = new Map< keyof Ramp | 'seed', ( keyof Ramp )[] >(); + + // Initialize maps + Object.keys( config ).forEach( ( step ) => { + dependencies.set( step as keyof Ramp, [] ); + } ); + dependents.set( 'seed', [] ); + Object.keys( config ).forEach( ( step ) => { + dependents.set( step as keyof Ramp, [] ); + } ); + + // Build the graph + Object.entries( config ).forEach( ( [ stepName, stepConfig ] ) => { + const step = stepName as keyof Ramp; + const reference = stepConfig.contrast.reference; + + dependencies.get( step )!.push( reference ); + dependents.get( reference )!.push( step ); + + // Add dependency for sameAsIfPossible + if ( stepConfig.sameAsIfPossible ) { + dependencies.get( step )!.push( stepConfig.sameAsIfPossible ); + dependents.get( stepConfig.sameAsIfPossible )!.push( step ); + } + } ); + + return { dependencies, dependents }; +} + +/** + * Topologically sort steps based on their dependencies + * @param config - The steps configuration object + */ +export function sortByDependency( + config: Record< keyof Ramp, RampStepConfig > +): ( keyof Ramp )[] { + const { dependents } = buildDependencyGraph( config ); + const result: ( keyof Ramp )[] = []; + const visited = new Set< keyof Ramp | 'seed' >(); + const visiting = new Set< keyof Ramp | 'seed' >(); + + function visit( node: keyof Ramp | 'seed' ): void { + if ( visiting.has( node ) ) { + throw new Error( + `Circular dependency detected involving step: ${ String( + node + ) }` + ); + } + if ( visited.has( node ) ) { + return; + } + + visiting.add( node ); + + // Visit all dependents (steps that depend on this node) + const nodeDependents = dependents.get( node ) || []; + nodeDependents.forEach( ( dependent ) => { + visit( dependent ); + } ); + + visiting.delete( node ); + visited.add( node ); + + // Add to result only if it's a step (not 'seed') + if ( node !== 'seed' ) { + result.unshift( node ); // Add to front for correct topological order + } + } + + // Start with seed - this will recursively visit all reachable nodes + visit( 'seed' ); + + return result; +} + +/** + * Finds out whether a lighter or a darker foreground color achieves a better + * contrast against the seed + * @param seed + * @param preferLighter Whether the check should favor white foreground color + * @return An object with "better" and "worse" properties, each holding a + * ramp direction value. + */ +export function computeBetterFgColorDirection( + seed: Color, + preferLighter?: boolean +): { + better: RampDirection; + worse: RampDirection; +} { + const contrastAgainstBlack = getCachedContrast( seed, BLACK ); + const contrastAgainstWhite = getCachedContrast( seed, WHITE ); + + return contrastAgainstBlack > + contrastAgainstWhite + + ( preferLighter ? WHITE_TEXT_CONTRAST_MARGIN : 0 ) + ? { better: 'darker', worse: 'lighter' } + : { better: 'lighter', worse: 'darker' }; +} + +export function adjustContrastTarget( target: number ) { + if ( target === 1 ) { + return 1; + } + + // Add a little top up to take into account any rounding error and algo imprecisions. + return target + UNIVERSAL_CONTRAST_TOPUP; +} + +/** + * Prevent the accent scale from referencing a lightness value that + * would prevent the algorithm from complying with the requirements + * and cause it to generate unexpected results. + * @param rawLightness + * @param direction + * @return The clamped lightness value + */ +export function clampAccentScaleReferenceLightness( + rawLightness: number, + direction: RampDirection +) { + const thresholds = ACCENT_SCALE_BASE_LIGHTNESS_THRESHOLDS[ direction ]; + return Math.max( thresholds.min, Math.min( thresholds.max, rawLightness ) ); +} diff --git a/packages/theme/src/color-ramps/test/__snapshots__/index.test.ts.snap b/packages/theme/src/color-ramps/test/__snapshots__/index.test.ts.snap new file mode 100644 index 00000000000000..8a2dffd2135f0b --- /dev/null +++ b/packages/theme/src/color-ramps/test/__snapshots__/index.test.ts.snap @@ -0,0 +1,1280 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`buildRamps accent ramp snapshots 1`] = ` +[ + [ + { + "input": { + "bgInfo": { + "mainDirection": "lighter", + "pinLightness": { + "stepName": "surface2", + "value": 0, + }, + }, + "seedComputed": "#3858e9", + "seedOriginal": "#3858e9", + "seedUnchanged": true, + }, + "output": { + "direction": "lighter", + "ramp": { + "bgFill1": { + "color": "#3858e9", + "warning": false, + }, + "bgFill2": { + "color": "#456afc", + "warning": false, + }, + "bgFillDark": { + "color": "#000", + "warning": false, + }, + "bgFillInverted1": { + "color": "#c3d9ff", + "warning": false, + }, + "bgFillInverted2": { + "color": "#eff0f2", + "warning": false, + }, + "fgFill": { + "color": "#eff0f2", + "warning": false, + }, + "fgFillDark": { + "color": "#eff0f2", + "warning": false, + }, + "fgFillInverted": { + "color": "#1b1e26", + "warning": false, + }, + "fgSurface1": { + "color": "#1f2dbf", + "warning": false, + }, + "fgSurface2": { + "color": "#314ede", + "warning": false, + }, + "fgSurface3": { + "color": "#8cafff", + "warning": false, + }, + "fgSurface4": { + "color": "#e3f0ff", + "warning": false, + }, + "stroke1": { + "color": "#576fb5", + "warning": false, + }, + "stroke2": { + "color": "#768bc3", + "warning": false, + }, + "stroke3": { + "color": "#8cafff", + "warning": false, + }, + "stroke4": { + "color": "#c7dcff", + "warning": false, + }, + "surface1": { + "color": "#000", + "warning": false, + }, + "surface2": { + "color": "#000", + "warning": false, + }, + "surface3": { + "color": "#040925", + "warning": false, + }, + "surface4": { + "color": "#071034", + "warning": false, + }, + "surface5": { + "color": "#0d1949", + "warning": false, + }, + "surface6": { + "color": "#152462", + "warning": false, + }, + }, + }, + }, + { + "input": { + "bgInfo": { + "mainDirection": "lighter", + "pinLightness": { + "stepName": "surface2", + "value": 0.5, + }, + }, + "seedComputed": "#3858e9", + "seedOriginal": "#3858e9", + "seedUnchanged": true, + }, + "output": { + "direction": "lighter", + "ramp": { + "bgFill1": { + "color": "#3858e9", + "warning": false, + }, + "bgFill2": { + "color": "#456afc", + "warning": false, + }, + "bgFillDark": { + "color": "#000", + "warning": false, + }, + "bgFillInverted1": { + "color": "#d6e7ff", + "warning": false, + }, + "bgFillInverted2": { + "color": "#fff", + "warning": true, + }, + "fgFill": { + "color": "#eff0f2", + "warning": false, + }, + "fgFillDark": { + "color": "#eff0f2", + "warning": false, + }, + "fgFillInverted": { + "color": "#1b1e26", + "warning": false, + }, + "fgSurface1": { + "color": "#6c94ff", + "warning": false, + }, + "fgSurface2": { + "color": "#9bbbff", + "warning": false, + }, + "fgSurface3": { + "color": "#d4e5ff", + "warning": false, + }, + "fgSurface4": { + "color": "#fff", + "warning": true, + }, + "stroke1": { + "color": "#8498c9", + "warning": false, + }, + "stroke2": { + "color": "#aab8da", + "warning": false, + }, + "stroke3": { + "color": "#d4e5ff", + "warning": false, + }, + "stroke4": { + "color": "#fff", + "warning": true, + }, + "surface1": { + "color": "#3552c1", + "warning": false, + }, + "surface2": { + "color": "#3b58c3", + "warning": false, + }, + "surface3": { + "color": "#405ec5", + "warning": false, + }, + "surface4": { + "color": "#3f5fd6", + "warning": false, + }, + "surface5": { + "color": "#4c6ac9", + "warning": false, + }, + "surface6": { + "color": "#5976cd", + "warning": false, + }, + }, + }, + }, + { + "input": { + "bgInfo": { + "mainDirection": "darker", + "pinLightness": { + "stepName": "surface2", + "value": 1, + }, + }, + "seedComputed": "#3858e9", + "seedOriginal": "#3858e9", + "seedUnchanged": true, + }, + "output": { + "direction": "darker", + "ramp": { + "bgFill1": { + "color": "#3858e9", + "warning": false, + }, + "bgFill2": { + "color": "#2c47d7", + "warning": false, + }, + "bgFillDark": { + "color": "#1b1e26", + "warning": false, + }, + "bgFillInverted1": { + "color": "#1401a5", + "warning": false, + }, + "bgFillInverted2": { + "color": "#1b1e26", + "warning": false, + }, + "fgFill": { + "color": "#eff0f2", + "warning": false, + }, + "fgFillDark": { + "color": "#eff0f2", + "warning": false, + }, + "fgFillInverted": { + "color": "#eff0f2", + "warning": false, + }, + "fgSurface1": { + "color": "#88acff", + "warning": false, + }, + "fgSurface2": { + "color": "#5d86ff", + "warning": false, + }, + "fgSurface3": { + "color": "#3858e9", + "warning": false, + }, + "fgSurface4": { + "color": "#080071", + "warning": false, + }, + "stroke1": { + "color": "#93a4d0", + "warning": false, + }, + "stroke2": { + "color": "#7085c0", + "warning": false, + }, + "stroke3": { + "color": "#3858e9", + "warning": false, + }, + "stroke4": { + "color": "#2236c7", + "warning": false, + }, + "surface1": { + "color": "#f5f7fc", + "warning": false, + }, + "surface2": { + "color": "#fff", + "warning": false, + }, + "surface3": { + "color": "#fff", + "warning": false, + }, + "surface4": { + "color": "#edf1fa", + "warning": false, + }, + "surface5": { + "color": "#e0e6f6", + "warning": false, + }, + "surface6": { + "color": "#ccd6f0", + "warning": false, + }, + }, + }, + }, + ], + [ + { + "input": { + "bgInfo": { + "mainDirection": "lighter", + "pinLightness": { + "stepName": "surface2", + "value": 0, + }, + }, + "seedComputed": "#cc1818", + "seedOriginal": "#cc1818", + "seedUnchanged": true, + }, + "output": { + "direction": "lighter", + "ramp": { + "bgFill1": { + "color": "#cc1818", + "warning": false, + }, + "bgFill2": { + "color": "#df332b", + "warning": false, + }, + "bgFillDark": { + "color": "#000", + "warning": false, + }, + "bgFillInverted1": { + "color": "#ffc9bd", + "warning": false, + }, + "bgFillInverted2": { + "color": "#f2efef", + "warning": false, + }, + "fgFill": { + "color": "#f2efef", + "warning": false, + }, + "fgFillDark": { + "color": "#f2efef", + "warning": false, + }, + "fgFillInverted": { + "color": "#231c1b", + "warning": false, + }, + "fgSurface1": { + "color": "#9a0000", + "warning": false, + }, + "fgSurface2": { + "color": "#c2000a", + "warning": false, + }, + "fgSurface3": { + "color": "#ff7f6f", + "warning": false, + }, + "fgSurface4": { + "color": "#ffe4dd", + "warning": false, + }, + "stroke1": { + "color": "#ae4d43", + "warning": false, + }, + "stroke2": { + "color": "#cc685d", + "warning": false, + }, + "stroke3": { + "color": "#ff7f6f", + "warning": false, + }, + "stroke4": { + "color": "#ffbfb3", + "warning": false, + }, + "surface1": { + "color": "#000", + "warning": false, + }, + "surface2": { + "color": "#000", + "warning": false, + }, + "surface3": { + "color": "#1c0504", + "warning": false, + }, + "surface4": { + "color": "#280907", + "warning": false, + }, + "surface5": { + "color": "#38100d", + "warning": false, + }, + "surface6": { + "color": "#4c1914", + "warning": false, + }, + }, + }, + }, + { + "input": { + "bgInfo": { + "mainDirection": "lighter", + "pinLightness": { + "stepName": "surface2", + "value": 0.5, + }, + }, + "seedComputed": "#cc1818", + "seedOriginal": "#cc1818", + "seedUnchanged": true, + }, + "output": { + "direction": "lighter", + "ramp": { + "bgFill1": { + "color": "#cc1818", + "warning": false, + }, + "bgFill2": { + "color": "#df332b", + "warning": false, + }, + "bgFillDark": { + "color": "#000", + "warning": false, + }, + "bgFillInverted1": { + "color": "#ffddd4", + "warning": false, + }, + "bgFillInverted2": { + "color": "#fff", + "warning": true, + }, + "fgFill": { + "color": "#f2efef", + "warning": false, + }, + "fgFillDark": { + "color": "#f2efef", + "warning": false, + }, + "fgFillInverted": { + "color": "#231c1b", + "warning": false, + }, + "fgSurface1": { + "color": "#ff5c4f", + "warning": false, + }, + "fgSurface2": { + "color": "#ff9b8c", + "warning": false, + }, + "fgSurface3": { + "color": "#ffd6cd", + "warning": false, + }, + "fgSurface4": { + "color": "#fff", + "warning": true, + }, + "stroke1": { + "color": "#d77e72", + "warning": false, + }, + "stroke2": { + "color": "#e3a89f", + "warning": false, + }, + "stroke3": { + "color": "#ffd6cd", + "warning": false, + }, + "stroke4": { + "color": "#fff", + "warning": true, + }, + "surface1": { + "color": "#9a3b32", + "warning": false, + }, + "surface2": { + "color": "#a23e35", + "warning": false, + }, + "surface3": { + "color": "#aa4138", + "warning": false, + }, + "surface4": { + "color": "#b04339", + "warning": false, + }, + "surface5": { + "color": "#ba483e", + "warning": false, + }, + "surface6": { + "color": "#cb4f44", + "warning": false, + }, + }, + }, + }, + { + "input": { + "bgInfo": { + "mainDirection": "darker", + "pinLightness": { + "stepName": "surface2", + "value": 1, + }, + }, + "seedComputed": "#cc1818", + "seedOriginal": "#cc1818", + "seedUnchanged": true, + }, + "output": { + "direction": "darker", + "ramp": { + "bgFill1": { + "color": "#cc1818", + "warning": false, + }, + "bgFill2": { + "color": "#b90000", + "warning": false, + }, + "bgFillDark": { + "color": "#231c1b", + "warning": false, + }, + "bgFillInverted1": { + "color": "#6d0000", + "warning": false, + }, + "bgFillInverted2": { + "color": "#231c1b", + "warning": false, + }, + "fgFill": { + "color": "#f2efef", + "warning": false, + }, + "fgFillDark": { + "color": "#f2efef", + "warning": false, + }, + "fgFillInverted": { + "color": "#f2efef", + "warning": false, + }, + "fgSurface1": { + "color": "#ff8979", + "warning": false, + }, + "fgSurface2": { + "color": "#fc5145", + "warning": false, + }, + "fgSurface3": { + "color": "#cc1818", + "warning": false, + }, + "fgSurface4": { + "color": "#4a0000", + "warning": false, + }, + "stroke1": { + "color": "#dc9085", + "warning": false, + }, + "stroke2": { + "color": "#cd695d", + "warning": false, + }, + "stroke3": { + "color": "#cc1818", + "warning": false, + }, + "stroke4": { + "color": "#a30000", + "warning": false, + }, + "surface1": { + "color": "#fdf5f4", + "warning": false, + }, + "surface2": { + "color": "#fff", + "warning": false, + }, + "surface3": { + "color": "#fff", + "warning": false, + }, + "surface4": { + "color": "#fceeec", + "warning": false, + }, + "surface5": { + "color": "#f9e0dc", + "warning": false, + }, + "surface6": { + "color": "#f6cdc6", + "warning": false, + }, + }, + }, + }, + ], + [ + { + "input": { + "bgInfo": { + "mainDirection": "lighter", + "pinLightness": { + "stepName": "surface2", + "value": 0, + }, + }, + "seedComputed": "#c7a589", + "seedOriginal": "#c7a589", + "seedUnchanged": true, + }, + "output": { + "direction": "lighter", + "ramp": { + "bgFill1": { + "color": "#c7a589", + "warning": false, + }, + "bgFill2": { + "color": "#dcba9d", + "warning": false, + }, + "bgFillDark": { + "color": "#000", + "warning": false, + }, + "bgFillInverted1": { + "color": "#f5d2b4", + "warning": false, + }, + "bgFillInverted2": { + "color": "#f1f0ee", + "warning": false, + }, + "fgFill": { + "color": "#1f1e1c", + "warning": false, + }, + "fgFillDark": { + "color": "#f1f0ee", + "warning": false, + }, + "fgFillInverted": { + "color": "#1f1e1c", + "warning": false, + }, + "fgSurface1": { + "color": "#5c3f26", + "warning": false, + }, + "fgSurface2": { + "color": "#77593f", + "warning": false, + }, + "fgSurface3": { + "color": "#c7a589", + "warning": false, + }, + "fgSurface4": { + "color": "#ffe9cb", + "warning": false, + }, + "stroke1": { + "color": "#7e6959", + "warning": false, + }, + "stroke2": { + "color": "#9c836f", + "warning": false, + }, + "stroke3": { + "color": "#c7a589", + "warning": false, + }, + "stroke4": { + "color": "#f2ceb1", + "warning": false, + }, + "surface1": { + "color": "#000", + "warning": false, + }, + "surface2": { + "color": "#000", + "warning": false, + }, + "surface3": { + "color": "#100b06", + "warning": false, + }, + "surface4": { + "color": "#1a120c", + "warning": false, + }, + "surface5": { + "color": "#251c14", + "warning": false, + }, + "surface6": { + "color": "#34271d", + "warning": false, + }, + }, + }, + }, + { + "input": { + "bgInfo": { + "mainDirection": "lighter", + "pinLightness": { + "stepName": "surface2", + "value": 0.5, + }, + }, + "seedComputed": "#c7a589", + "seedOriginal": "#c7a589", + "seedUnchanged": true, + }, + "output": { + "direction": "lighter", + "ramp": { + "bgFill1": { + "color": "#c7a589", + "warning": false, + }, + "bgFill2": { + "color": "#dcba9d", + "warning": false, + }, + "bgFillDark": { + "color": "#000", + "warning": false, + }, + "bgFillInverted1": { + "color": "#ffdfc1", + "warning": false, + }, + "bgFillInverted2": { + "color": "#fff", + "warning": true, + }, + "fgFill": { + "color": "#1f1e1c", + "warning": false, + }, + "fgFillDark": { + "color": "#f1f0ee", + "warning": false, + }, + "fgFillInverted": { + "color": "#1f1e1c", + "warning": false, + }, + "fgSurface1": { + "color": "#b7967a", + "warning": false, + }, + "fgSurface2": { + "color": "#dbb89b", + "warning": false, + }, + "fgSurface3": { + "color": "#ffe0c3", + "warning": false, + }, + "fgSurface4": { + "color": "#fff", + "warning": true, + }, + "stroke1": { + "color": "#b1957e", + "warning": false, + }, + "stroke2": { + "color": "#d3b59d", + "warning": false, + }, + "stroke3": { + "color": "#ffe0c3", + "warning": false, + }, + "stroke4": { + "color": "#fff", + "warning": true, + }, + "surface1": { + "color": "#715945", + "warning": false, + }, + "surface2": { + "color": "#765e49", + "warning": false, + }, + "surface3": { + "color": "#7c624d", + "warning": false, + }, + "surface4": { + "color": "#806650", + "warning": false, + }, + "surface5": { + "color": "#896d55", + "warning": false, + }, + "surface6": { + "color": "#95765d", + "warning": false, + }, + }, + }, + }, + { + "input": { + "bgInfo": { + "mainDirection": "darker", + "pinLightness": { + "stepName": "surface2", + "value": 1, + }, + }, + "seedComputed": "#c7a589", + "seedOriginal": "#c7a589", + "seedUnchanged": true, + }, + "output": { + "direction": "darker", + "ramp": { + "bgFill1": { + "color": "#c7a589", + "warning": false, + }, + "bgFill2": { + "color": "#b49277", + "warning": false, + }, + "bgFillDark": { + "color": "#1f1e1c", + "warning": false, + }, + "bgFillInverted1": { + "color": "#452910", + "warning": false, + }, + "bgFillInverted2": { + "color": "#1f1e1c", + "warning": false, + }, + "fgFill": { + "color": "#1f1e1c", + "warning": false, + }, + "fgFillDark": { + "color": "#f1f0ee", + "warning": false, + }, + "fgFillInverted": { + "color": "#f1f0ee", + "warning": false, + }, + "fgSurface1": { + "color": "#cba88c", + "warning": false, + }, + "fgSurface2": { + "color": "#a9886d", + "warning": false, + }, + "fgSurface3": { + "color": "#8a6b51", + "warning": false, + }, + "fgSurface4": { + "color": "#301700", + "warning": false, + }, + "stroke1": { + "color": "#caaa91", + "warning": false, + }, + "stroke2": { + "color": "#a78d77", + "warning": false, + }, + "stroke3": { + "color": "#8a6b51", + "warning": false, + }, + "stroke4": { + "color": "#6c4e34", + "warning": false, + }, + "surface1": { + "color": "#fbf6f3", + "warning": false, + }, + "surface2": { + "color": "#fff", + "warning": false, + }, + "surface3": { + "color": "#fff", + "warning": false, + }, + "surface4": { + "color": "#f7efe9", + "warning": false, + }, + "surface5": { + "color": "#f1e3d8", + "warning": false, + }, + "surface6": { + "color": "#e9d2c0", + "warning": false, + }, + }, + }, + }, + ], +] +`; + +exports[`buildRamps background ramp snapshots 1`] = ` +[ + { + "input": { + "seedComputed": "#4d4c4d", + "seedOriginal": "#4d4d4d", + "seedUnchanged": false, + }, + "output": { + "direction": "lighter", + "ramp": { + "bgFill1": { + "color": "#b3b3b3", + "warning": false, + }, + "bgFill2": { + "color": "#c8c8c8", + "warning": false, + }, + "bgFillDark": { + "color": "#000", + "warning": false, + }, + "bgFillInverted1": { + "color": "#d8d8d8", + "warning": false, + }, + "bgFillInverted2": { + "color": "#f1f1f1", + "warning": false, + }, + "fgFill": { + "color": "#1e1e1e", + "warning": false, + }, + "fgFillDark": { + "color": "#f0f0f0", + "warning": false, + }, + "fgFillInverted": { + "color": "#1e1e1e", + "warning": false, + }, + "fgSurface1": { + "color": "#818181", + "warning": false, + }, + "fgSurface2": { + "color": "#a0a0a0", + "warning": false, + }, + "fgSurface3": { + "color": "#c3c3c3", + "warning": false, + }, + "fgSurface4": { + "color": "#f1f1f1", + "warning": false, + }, + "stroke1": { + "color": "#636363", + "warning": false, + }, + "stroke2": { + "color": "#7d7d7d", + "warning": false, + }, + "stroke3": { + "color": "#a0a0a0", + "warning": false, + }, + "stroke4": { + "color": "#c7c7c7", + "warning": false, + }, + "surface1": { + "color": "#484848", + "warning": false, + }, + "surface2": { + "color": "#4d4c4d", + "warning": false, + }, + "surface3": { + "color": "#515151", + "warning": false, + }, + "surface4": { + "color": "#555", + "warning": false, + }, + "surface5": { + "color": "#5b5b5b", + "warning": false, + }, + "surface6": { + "color": "#656565", + "warning": false, + }, + }, + }, + }, + { + "input": { + "seedComputed": "#6c6", + "seedOriginal": "#6c6", + "seedUnchanged": true, + }, + "output": { + "direction": "darker", + "ramp": { + "bgFill1": { + "color": "#005e00", + "warning": false, + }, + "bgFill2": { + "color": "#004e00", + "warning": false, + }, + "bgFillDark": { + "color": "#1c1f1c", + "warning": false, + }, + "bgFillInverted1": { + "color": "#003b00", + "warning": false, + }, + "bgFillInverted2": { + "color": "#1c1f1c", + "warning": false, + }, + "fgFill": { + "color": "#edf2ed", + "warning": false, + }, + "fgFillDark": { + "color": "#edf2ed", + "warning": false, + }, + "fgFillInverted": { + "color": "#edf2ed", + "warning": false, + }, + "fgSurface1": { + "color": "#707a6f", + "warning": false, + }, + "fgSurface2": { + "color": "#575f56", + "warning": false, + }, + "fgSurface3": { + "color": "#3e453e", + "warning": false, + }, + "fgSurface4": { + "color": "#1c1f1c", + "warning": false, + }, + "stroke1": { + "color": "#6fa46e", + "warning": false, + }, + "stroke2": { + "color": "#5a8558", + "warning": false, + }, + "stroke3": { + "color": "#436542", + "warning": false, + }, + "stroke4": { + "color": "#2e472d", + "warning": false, + }, + "surface1": { + "color": "#67c467", + "warning": false, + }, + "surface2": { + "color": "#6c6", + "warning": false, + }, + "surface3": { + "color": "#6fd26f", + "warning": false, + }, + "surface4": { + "color": "#65be65", + "warning": false, + }, + "surface5": { + "color": "#61b561", + "warning": false, + }, + "surface6": { + "color": "#5da75d", + "warning": false, + }, + }, + }, + }, + { + "input": { + "seedComputed": "#009", + "seedOriginal": "#009", + "seedUnchanged": true, + }, + "output": { + "direction": "lighter", + "ramp": { + "bgFill1": { + "color": "#508bff", + "warning": false, + }, + "bgFill2": { + "color": "#6ca1ff", + "warning": false, + }, + "bgFillDark": { + "color": "#000", + "warning": false, + }, + "bgFillInverted1": { + "color": "#bedaff", + "warning": false, + }, + "bgFillInverted2": { + "color": "#eff0f2", + "warning": false, + }, + "fgFill": { + "color": "#0b1b41", + "warning": false, + }, + "fgFillDark": { + "color": "#eff0f2", + "warning": false, + }, + "fgFillInverted": { + "color": "#0b1b41", + "warning": false, + }, + "fgSurface1": { + "color": "#515e78", + "warning": false, + }, + "fgSurface2": { + "color": "#70798b", + "warning": false, + }, + "fgSurface3": { + "color": "#afb4be", + "warning": false, + }, + "fgSurface4": { + "color": "#eff0f2", + "warning": false, + }, + "stroke1": { + "color": "#1f3e88", + "warning": false, + }, + "stroke2": { + "color": "#2e55b1", + "warning": false, + }, + "stroke3": { + "color": "#5577c0", + "warning": false, + }, + "stroke4": { + "color": "#7f9ad0", + "warning": false, + }, + "surface1": { + "color": "#041c65", + "warning": false, + }, + "surface2": { + "color": "#009", + "warning": false, + }, + "surface3": { + "color": "#06257d", + "warning": false, + }, + "surface4": { + "color": "#072885", + "warning": false, + }, + "surface5": { + "color": "#0a2e93", + "warning": false, + }, + "surface6": { + "color": "#1038a4", + "warning": false, + }, + }, + }, + }, +] +`; diff --git a/packages/theme/src/color-ramps/test/index.test.ts b/packages/theme/src/color-ramps/test/index.test.ts new file mode 100644 index 00000000000000..d716eaa639474a --- /dev/null +++ b/packages/theme/src/color-ramps/test/index.test.ts @@ -0,0 +1,94 @@ +/** + * External dependencies + */ +// Not sure why ESLint is erroring. +// eslint-disable-next-line import/no-extraneous-dependencies +import Color from 'colorjs.io'; + +/** + * Internal dependencies + */ +import { buildRamp } from '../lib'; +import { BG_RAMP_CONFIG, ACCENT_RAMP_CONFIG } from '../lib/ramp-configs'; +import { DEFAULT_SEED_COLORS } from '../lib/constants'; + +describe( 'buildRamps', () => { + it( 'background ramp snapshots', () => { + // Representative sample covering edge cases and various hue/saturation/lightness combinations + const allBgColors = [ + 'hsl(0deg 0% 30%)', // Dark gray (desaturated) + 'hsl(120deg 50% 60%)', // Mid-range green + 'hsl(240deg 100% 30%)', // Dark saturated blue + ]; + + expect( + allBgColors.map( ( bg ) => { + const ramp = buildRamp( bg, BG_RAMP_CONFIG ); + const seedOriginal = new Color( bg ) + .to( 'srgb' ) + .toString( { format: 'hex', inGamut: true } ); + const seedComputed = new Color( ramp.ramp.surface2.color ) + .to( 'srgb' ) + .toString( { format: 'hex', inGamut: true } ); + + return { + input: { + seedOriginal, + seedComputed, + seedUnchanged: seedOriginal === seedComputed, + }, + output: ramp, + }; + } ) + ).toMatchSnapshot(); + } ); + + it( 'accent ramp snapshots', () => { + // Representative sample covering key option combinations + const options = [ + { + pinLightness: { stepName: 'surface2', value: 0 }, + mainDirection: 'lighter', + }, + { + pinLightness: { stepName: 'surface2', value: 0.5 }, + mainDirection: 'lighter', + }, + { + pinLightness: { stepName: 'surface2', value: 1 }, + mainDirection: 'darker', + }, + ] as const; + + // Representative sample covering different hue ranges and saturation levels + const allPrimaryColors = [ + DEFAULT_SEED_COLORS.primary, // WP blue (mid saturation) + DEFAULT_SEED_COLORS.error, // WP error red (saturated) + '#c7a589', // WP Admin "coffee" theme accent (desaturated/beige) + ]; + + expect( + allPrimaryColors.map( ( primary ) => + options.map( ( o ) => { + const ramp = buildRamp( primary, ACCENT_RAMP_CONFIG, o ); + const seedOriginal = new Color( primary ) + .to( 'srgb' ) + .toString( { format: 'hex', inGamut: true } ); + const seedComputed = new Color( ramp.ramp.bgFill1.color ) + .to( 'srgb' ) + .toString( { format: 'hex', inGamut: true } ); + + return { + input: { + seedOriginal, + seedComputed, + seedUnchanged: seedOriginal === seedComputed, + bgInfo: o, + }, + output: ramp, + }; + } ) + ) + ).toMatchSnapshot(); + } ); +} ); diff --git a/packages/theme/src/context.ts b/packages/theme/src/context.ts new file mode 100644 index 00000000000000..b8728b26eaf6e5 --- /dev/null +++ b/packages/theme/src/context.ts @@ -0,0 +1,19 @@ +/** + * WordPress dependencies + */ +import { createContext } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import type { ThemeProviderSettings } from './types'; + +interface ThemeContextType { + resolvedSettings: ThemeProviderSettings; +} + +export const ThemeContext = createContext< ThemeContextType >( { + resolvedSettings: { + color: {}, + }, +} ); diff --git a/packages/theme/src/index.ts b/packages/theme/src/index.ts new file mode 100644 index 00000000000000..0de667033b338c --- /dev/null +++ b/packages/theme/src/index.ts @@ -0,0 +1,2 @@ +// Private APIs. +export { privateApis } from './private-apis'; diff --git a/packages/theme/src/lock-unlock.ts b/packages/theme/src/lock-unlock.ts new file mode 100644 index 00000000000000..b1a535e3859773 --- /dev/null +++ b/packages/theme/src/lock-unlock.ts @@ -0,0 +1,10 @@ +/** + * WordPress dependencies + */ +import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/private-apis'; + +export const { lock, unlock } = + __dangerousOptInToUnstableAPIsOnlyForCoreModules( + 'I acknowledge private features are not for use in themes or plugins and doing so will break in the next version of WordPress.', + '@wordpress/theme' + ); diff --git a/packages/theme/src/prebuilt/css/design-tokens.css b/packages/theme/src/prebuilt/css/design-tokens.css new file mode 100644 index 00000000000000..d1ca95bc588d7e --- /dev/null +++ b/packages/theme/src/prebuilt/css/design-tokens.css @@ -0,0 +1,401 @@ +/* ------------------------------------------- + * Autogenerated by ⛋ Terrazzo. DO NOT EDIT! + * ------------------------------------------- */ + +:root { + --wpds-border-radius-large: 8px; /* Large radius */ + --wpds-border-radius-medium: 4px; /* Medium radius */ + --wpds-border-radius-small: 2px; /* Small radius */ + --wpds-border-radius-x-small: 1px; /* Extra small radius */ + --wpds-border-width-focus: 2px; /* Border width for focus ring */ + --wpds-color-bg-interactive-brand: #00000000; /* Background color for interactive elements with brand tone and normal emphasis. */ + --wpds-color-bg-interactive-brand-active: var( + --wpds-color-private-primary-surface2 + ); /* Background color for interactive elements with brand tone and normal emphasis that are hovered, focused, or active. */ + --wpds-color-bg-interactive-brand-disabled: var( + --wpds-color-private-bg-surface5 + ); /* Background color for interactive elements with brand tone and normal emphasis, in their disabled state. */ + --wpds-color-bg-interactive-brand-strong: var( + --wpds-color-private-primary-bg-fill1 + ); /* Background color for interactive elements with brand tone and strong emphasis. */ + --wpds-color-bg-interactive-brand-strong-active: var( + --wpds-color-private-primary-bg-fill2 + ); /* Background color for interactive elements with brand tone and strong emphasis that are hovered, focused, or active. */ + --wpds-color-bg-interactive-brand-strong-disabled: var( + --wpds-color-private-bg-surface6 + ); /* Background color for interactive elements with brand tone and strong emphasis, in their disabled state. */ + --wpds-color-bg-interactive-brand-weak: #00000000; /* Background color for interactive elements with brand tone and weak emphasis. */ + --wpds-color-bg-interactive-brand-weak-active: var( + --wpds-color-private-primary-surface4 + ); /* Background color for interactive elements with brand tone and weak emphasis that are hovered, focused, or active. */ + --wpds-color-bg-interactive-brand-weak-disabled: var( + --wpds-color-private-bg-surface5 + ); /* Background color for interactive elements with brand tone and weak emphasis, in their disabled state. */ + --wpds-color-bg-interactive-neutral: #00000000; /* Background color for interactive elements with neutral tone and normal emphasis. */ + --wpds-color-bg-interactive-neutral-active: var( + --wpds-color-private-bg-surface4 + ); /* Background color for interactive elements with neutral tone and normal emphasis that are hovered, focused, or active. */ + --wpds-color-bg-interactive-neutral-disabled: var( + --wpds-color-private-bg-surface5 + ); /* Background color for interactive elements with neutral tone and normal emphasis, in their disabled state. */ + --wpds-color-bg-interactive-neutral-strong: var( + --wpds-color-private-bg-bg-fill-inverted1 + ); /* Background color for interactive elements with neutral tone and strong emphasis. */ + --wpds-color-bg-interactive-neutral-strong-active: var( + --wpds-color-private-bg-bg-fill-inverted2 + ); /* Background color for interactive elements with neutral tone and strong emphasis that are hovered, focused, or active. */ + --wpds-color-bg-interactive-neutral-strong-disabled: var( + --wpds-color-private-bg-surface6 + ); /* Background color for interactive elements with neutral tone and strong emphasis, in their disabled state. */ + --wpds-color-bg-interactive-neutral-weak: #00000000; /* Background color for interactive elements with neutral tone and weak emphasis. */ + --wpds-color-bg-interactive-neutral-weak-active: var( + --wpds-color-private-bg-surface4 + ); /* Background color for interactive elements with neutral tone and weak emphasis that are hovered, focused, or active. */ + --wpds-color-bg-interactive-neutral-weak-disabled: var( + --wpds-color-private-bg-surface5 + ); /* Background color for interactive elements with neutral tone and weak emphasis, in their disabled state. */ + --wpds-color-bg-surface-brand: var( + --wpds-color-private-primary-surface1 + ); /* Background color for surfaces with brand tone and normal emphasis. */ + --wpds-color-bg-surface-error: var( + --wpds-color-private-error-surface4 + ); /* Background color for surfaces with error tone and normal emphasis. */ + --wpds-color-bg-surface-error-weak: var( + --wpds-color-private-error-surface2 + ); /* Background color for surfaces with error tone and weak emphasis. */ + --wpds-color-bg-surface-info: var( + --wpds-color-private-info-surface4 + ); /* Background color for surfaces with info tone and normal emphasis. */ + --wpds-color-bg-surface-info-weak: var( + --wpds-color-private-info-surface2 + ); /* Background color for surfaces with info tone and weak emphasis. */ + --wpds-color-bg-surface-neutral: var( + --wpds-color-private-bg-surface2 + ); /* Background color for surfaces with normal emphasis. */ + --wpds-color-bg-surface-neutral-strong: var( + --wpds-color-private-bg-surface3 + ); /* Background color for surfaces with strong emphasis. */ + --wpds-color-bg-surface-neutral-weak: var( + --wpds-color-private-bg-surface1 + ); /* Background color for surfaces with weak emphasis. */ + --wpds-color-bg-surface-success: var( + --wpds-color-private-success-surface4 + ); /* Background color for surfaces with success tone and normal emphasis. */ + --wpds-color-bg-surface-success-weak: var( + --wpds-color-private-success-surface2 + ); /* Background color for surfaces with success tone and weak emphasis. */ + --wpds-color-bg-surface-warning: var( + --wpds-color-private-warning-surface4 + ); /* Background color for surfaces with warning tone and normal emphasis. */ + --wpds-color-bg-surface-warning-weak: var( + --wpds-color-private-warning-surface2 + ); /* Background color for surfaces with warning tone and weak emphasis. */ + --wpds-color-bg-thumb-brand: var( + --wpds-color-private-primary-stroke3 + ); /* Background color for thumbs with a brand tone and normal emphasis (eg. slider thumb and filled track). */ + --wpds-color-bg-thumb-brand-active: var( + --wpds-color-private-primary-stroke3 + ); /* Background color for thumbs with a brand tone and normal emphasis (eg. slider thumb and filled track) that are hovered, focused, or active. */ + --wpds-color-bg-thumb-brand-disabled: var( + --wpds-color-private-bg-stroke2 + ); /* Background color for thumbs with a brand tone and normal emphasis (eg. slider thumb and filled track), in their disabled state. */ + --wpds-color-bg-thumb-neutral-weak: var( + --wpds-color-private-bg-stroke3 + ); /* Background color for thumbs with a neutral tone and weak emphasis (eg. scrollbar thumb). */ + --wpds-color-bg-thumb-neutral-weak-active: var( + --wpds-color-private-bg-stroke4 + ); /* Background color for thumbs with a neutral tone and weak emphasis (eg. scrollbar thumb) that are hovered, focused, or active. */ + --wpds-color-bg-track-neutral: var( + --wpds-color-private-bg-stroke2 + ); /* Background color for tracks with a neutral tone and normal emphasis (eg. slider or progressbar track). */ + --wpds-color-bg-track-neutral-weak: var( + --wpds-color-private-bg-stroke1 + ); /* Background color for tracks with a neutral tone and weak emphasis (eg. scrollbar track). */ + --wpds-color-fg-content-neutral: var( + --wpds-color-private-bg-fg-surface4 + ); /* Foreground color for content like text with normal emphasis. */ + --wpds-color-fg-content-neutral-weak: var( + --wpds-color-private-bg-fg-surface3 + ); /* Foreground color for content like text with weak emphasis. */ + --wpds-color-fg-interactive-brand: var( + --wpds-color-private-primary-fg-surface3 + ); /* Foreground color for interactive elements with brand tone and normal emphasis. */ + --wpds-color-fg-interactive-brand-active: var( + --wpds-color-private-primary-fg-surface3 + ); /* Foreground color for interactive elements with brand tone and normal emphasis that are hovered, focused, or active. */ + --wpds-color-fg-interactive-brand-disabled: var( + --wpds-color-private-bg-fg-surface2 + ); /* Foreground color for interactive elements with brand tone and normal emphasis, in their disabled state. */ + --wpds-color-fg-interactive-brand-strong: var( + --wpds-color-private-primary-fg-fill + ); /* Foreground color for interactive elements with brand tone and strong emphasis. */ + --wpds-color-fg-interactive-brand-strong-active: var( + --wpds-color-private-primary-fg-fill + ); /* Foreground color for interactive elements with brand tone and strong emphasis that are hovered, focused, or active. */ + --wpds-color-fg-interactive-brand-strong-disabled: var( + --wpds-color-private-bg-fg-surface3 + ); /* Foreground color for interactive elements with brand tone and strong emphasis, in their disabled state. */ + --wpds-color-fg-interactive-neutral: var( + --wpds-color-private-bg-fg-surface4 + ); /* Foreground color for interactive elements with neutral tone and normal emphasis. */ + --wpds-color-fg-interactive-neutral-active: var( + --wpds-color-private-bg-fg-surface4 + ); /* Foreground color for interactive elements with neutral tone and normal emphasis that are hovered, focused, or active. */ + --wpds-color-fg-interactive-neutral-disabled: var( + --wpds-color-private-bg-fg-surface2 + ); /* Foreground color for interactive elements with neutral tone and normal emphasis, in their disabled state. */ + --wpds-color-fg-interactive-neutral-strong: var( + --wpds-color-private-bg-fg-fill-inverted + ); /* Foreground color for interactive elements with neutral tone and strong emphasis. */ + --wpds-color-fg-interactive-neutral-strong-active: var( + --wpds-color-private-bg-fg-fill-inverted + ); /* Foreground color for interactive elements with neutral tone and strong emphasis that are hovered, focused, or active. */ + --wpds-color-fg-interactive-neutral-strong-disabled: var( + --wpds-color-private-bg-fg-surface3 + ); /* Foreground color for interactive elements with neutral tone and strong emphasis, in their disabled state. */ + --wpds-color-fg-interactive-neutral-weak: var( + --wpds-color-private-bg-fg-surface3 + ); /* Foreground color for interactive elements with neutral tone and weak emphasis. */ + --wpds-color-fg-interactive-neutral-weak-disabled: var( + --wpds-color-private-bg-fg-surface2 + ); /* Foreground color for interactive elements with neutral tone and weak emphasis, in their disabled state. */ + --wpds-color-private-bg-bg-fill-dark: #1e1e1e; + --wpds-color-private-bg-bg-fill-inverted1: #2f2f2f; + --wpds-color-private-bg-bg-fill-inverted2: #1e1e1e; + --wpds-color-private-bg-bg-fill1: #555555; + --wpds-color-private-bg-bg-fill2: #474747; + --wpds-color-private-bg-fg-fill: #f0f0f0; + --wpds-color-private-bg-fg-fill-dark: #f0f0f0; + --wpds-color-private-bg-fg-fill-inverted: #f0f0f0; + --wpds-color-private-bg-fg-surface1: #a9a9a9; + --wpds-color-private-bg-fg-surface2: #898989; + --wpds-color-private-bg-fg-surface3: #6c6c6c; + --wpds-color-private-bg-fg-surface4: #1e1e1e; + --wpds-color-private-bg-stroke1: #d0d0d0; + --wpds-color-private-bg-stroke2: #aeaeae; + --wpds-color-private-bg-stroke3: #898989; + --wpds-color-private-bg-stroke4: #6a6a6a; + --wpds-color-private-bg-surface1: #efefef; + --wpds-color-private-bg-surface2: #f8f8f8; + --wpds-color-private-bg-surface3: #ffffff; + --wpds-color-private-bg-surface4: #eaeaea; + --wpds-color-private-bg-surface5: #dfdfdf; + --wpds-color-private-bg-surface6: #d0d0d0; + --wpds-color-private-error-bg-fill-dark: #231c1b; + --wpds-color-private-error-bg-fill-inverted1: #6d0000; + --wpds-color-private-error-bg-fill-inverted2: #231c1b; + --wpds-color-private-error-bg-fill1: #cc1818; + --wpds-color-private-error-bg-fill2: #b90000; + --wpds-color-private-error-fg-fill: #f2efef; + --wpds-color-private-error-fg-fill-dark: #f2efef; + --wpds-color-private-error-fg-fill-inverted: #f2efef; + --wpds-color-private-error-fg-surface1: #ff8070; + --wpds-color-private-error-fg-surface2: #f64b40; + --wpds-color-private-error-fg-surface3: #cc1818; + --wpds-color-private-error-fg-surface4: #4a0000; + --wpds-color-private-error-stroke1: #dc9085; + --wpds-color-private-error-stroke2: #cd695d; + --wpds-color-private-error-stroke3: #cc1818; + --wpds-color-private-error-stroke4: #a30000; + --wpds-color-private-error-surface1: #fcedea; + --wpds-color-private-error-surface2: #fdf6f5; + --wpds-color-private-error-surface3: #ffffff; + --wpds-color-private-error-surface4: #fae5e2; + --wpds-color-private-error-surface5: #f8d8d3; + --wpds-color-private-error-surface6: #f5c5bd; + --wpds-color-private-info-bg-fill-dark: #1b1e23; + --wpds-color-private-info-bg-fill-inverted1: #00297b; + --wpds-color-private-info-bg-fill-inverted2: #1b1e23; + --wpds-color-private-info-bg-fill1: #0090ff; + --wpds-color-private-info-bg-fill2: #007eec; + --wpds-color-private-info-fg-fill: #1b1e23; + --wpds-color-private-info-fg-fill-dark: #eff0f2; + --wpds-color-private-info-fg-fill-inverted: #eff0f2; + --wpds-color-private-info-fg-surface1: #4dafff; + --wpds-color-private-info-fg-surface2: #008bf9; + --wpds-color-private-info-fg-surface3: #006cd8; + --wpds-color-private-info-fg-surface4: #001758; + --wpds-color-private-info-stroke1: #8baed6; + --wpds-color-private-info-stroke2: #5c90c9; + --wpds-color-private-info-stroke3: #006cd8; + --wpds-color-private-info-stroke4: #004bb5; + --wpds-color-private-info-surface1: #e9f1fa; + --wpds-color-private-info-surface2: #f5f9fd; + --wpds-color-private-info-surface3: #ffffff; + --wpds-color-private-info-surface4: #e0ecf8; + --wpds-color-private-info-surface5: #d0e1f5; + --wpds-color-private-info-surface6: #b9d3f0; + --wpds-color-private-primary-bg-fill-dark: #1b1e26; + --wpds-color-private-primary-bg-fill-inverted1: #1401a5; + --wpds-color-private-primary-bg-fill-inverted2: #1b1e26; + --wpds-color-private-primary-bg-fill1: #3858e9; + --wpds-color-private-primary-bg-fill2: #2c47d7; + --wpds-color-private-primary-fg-fill: #eff0f2; + --wpds-color-private-primary-fg-fill-dark: #eff0f2; + --wpds-color-private-primary-fg-fill-inverted: #eff0f2; + --wpds-color-private-primary-fg-surface1: #81a6ff; + --wpds-color-private-primary-fg-surface2: #577fff; + --wpds-color-private-primary-fg-surface3: #3858e9; + --wpds-color-private-primary-fg-surface4: #080071; + --wpds-color-private-primary-stroke1: #93a4d0; + --wpds-color-private-primary-stroke2: #7085c0; + --wpds-color-private-primary-stroke3: #3858e9; + --wpds-color-private-primary-stroke4: #2236c7; + --wpds-color-private-primary-surface1: #ecf0f9; + --wpds-color-private-primary-surface2: #f6f8fc; + --wpds-color-private-primary-surface3: #ffffff; + --wpds-color-private-primary-surface4: #e5eaf7; + --wpds-color-private-primary-surface5: #d7dff3; + --wpds-color-private-primary-surface6: #c4d0ee; + --wpds-color-private-success-bg-fill-dark: #1b1f1c; + --wpds-color-private-success-bg-fill-inverted1: #003b00; + --wpds-color-private-success-bg-fill-inverted2: #1b1f1c; + --wpds-color-private-success-bg-fill1: #4ab866; + --wpds-color-private-success-bg-fill2: #34a554; + --wpds-color-private-success-fg-fill: #1b1f1c; + --wpds-color-private-success-fg-fill-dark: #edf2ed; + --wpds-color-private-success-fg-fill-inverted: #edf2ed; + --wpds-color-private-success-fg-surface1: #52bf6d; + --wpds-color-private-success-fg-surface2: #2a9e4d; + --wpds-color-private-success-fg-surface3: #008030; + --wpds-color-private-success-fg-surface4: #002b00; + --wpds-color-private-success-stroke1: #78bb84; + --wpds-color-private-success-stroke2: #629a6c; + --wpds-color-private-success-stroke3: #008030; + --wpds-color-private-success-stroke4: #006013; + --wpds-color-private-success-surface1: #ddf8e1; + --wpds-color-private-success-surface2: #f0fcf2; + --wpds-color-private-success-surface3: #ffffff; + --wpds-color-private-success-surface4: #cdf5d2; + --wpds-color-private-success-surface5: #abf0b7; + --wpds-color-private-success-surface6: #7be792; + --wpds-color-private-warning-bg-fill-dark: #1f1e1b; + --wpds-color-private-warning-bg-fill-inverted1: #472900; + --wpds-color-private-warning-bg-fill-inverted2: #1f1e1b; + --wpds-color-private-warning-bg-fill1: #f0b849; + --wpds-color-private-warning-bg-fill2: #dba32f; + --wpds-color-private-warning-fg-fill: #1f1e1b; + --wpds-color-private-warning-fg-fill-dark: #f3f0e9; + --wpds-color-private-warning-fg-fill-inverted: #f3f0e9; + --wpds-color-private-warning-fg-surface1: #d7a02b; + --wpds-color-private-warning-fg-surface2: #b58000; + --wpds-color-private-warning-fg-surface3: #966200; + --wpds-color-private-warning-fg-surface4: #2f1800; + --wpds-color-private-warning-stroke1: #c3a777; + --wpds-color-private-warning-stroke2: #a18a61; + --wpds-color-private-warning-stroke3: #966200; + --wpds-color-private-warning-stroke4: #724700; + --wpds-color-private-warning-surface1: #faefdc; + --wpds-color-private-warning-surface2: #fdf7ee; + --wpds-color-private-warning-surface3: #ffffff; + --wpds-color-private-warning-surface4: #f8e8cd; + --wpds-color-private-warning-surface5: #f5dcb2; + --wpds-color-private-warning-surface6: #f0cb89; + --wpds-color-stroke-focus-brand: var( + --wpds-color-private-primary-stroke3 + ); /* Accessible stroke color applied to focus rings. */ + --wpds-color-stroke-interactive-brand: var( + --wpds-color-private-primary-stroke3 + ); /* Accessible stroke color used for interactive brand-toned elements with normal emphasis. */ + --wpds-color-stroke-interactive-brand-active: var( + --wpds-color-private-primary-stroke4 + ); /* Accessible stroke color used for interactive brand-toned elements with normal emphasis that are hovered, focused, or active. */ + --wpds-color-stroke-interactive-brand-disabled: var( + --wpds-color-private-bg-stroke2 + ); /* Accessible stroke color used for interactive brand-toned elements with normal emphasis, in their disabled state. */ + --wpds-color-stroke-interactive-error-strong: var( + --wpds-color-private-error-stroke3 + ); /* Accessible stroke color used for interactive error-toned elements with strong emphasis. */ + --wpds-color-stroke-interactive-neutral: var( + --wpds-color-private-bg-stroke3 + ); /* Accessible stroke color used for interactive neutrally-toned elements with normal emphasis. */ + --wpds-color-stroke-interactive-neutral-active: var( + --wpds-color-private-bg-stroke4 + ); /* Accessible stroke color used for interactive neutrally-toned elements with normal emphasis that are hovered, focused, or active. */ + --wpds-color-stroke-interactive-neutral-disabled: var( + --wpds-color-private-bg-stroke2 + ); /* Accessible stroke color used for interactive neutrally-toned elements with normal emphasis, in their disabled state. */ + --wpds-color-stroke-interactive-neutral-strong: var( + --wpds-color-private-bg-stroke4 + ); /* Accessible stroke color used for interactive neutrally-toned elements with strong emphasis. */ + --wpds-color-stroke-surface-brand: var( + --wpds-color-private-primary-stroke1 + ); /* Decorative stroke color used to define brand-toned surface boundaries with normal emphasis. */ + --wpds-color-stroke-surface-brand-strong: var( + --wpds-color-private-primary-stroke3 + ); /* Decorative stroke color used to define neutrally-toned surface boundaries with strong emphasis. */ + --wpds-color-stroke-surface-error: var( + --wpds-color-private-error-stroke1 + ); /* Decorative stroke color used to define error-toned surface boundaries with normal emphasis. */ + --wpds-color-stroke-surface-error-strong: var( + --wpds-color-private-error-stroke3 + ); /* Decorative stroke color used to define error-toned surface boundaries with strong emphasis. */ + --wpds-color-stroke-surface-info: var( + --wpds-color-private-info-stroke1 + ); /* Decorative stroke color used to define info-toned surface boundaries with normal emphasis. */ + --wpds-color-stroke-surface-info-strong: var( + --wpds-color-private-info-stroke3 + ); /* Decorative stroke color used to define info-toned surface boundaries with strong emphasis. */ + --wpds-color-stroke-surface-neutral: var( + --wpds-color-private-bg-stroke2 + ); /* Decorative stroke color used to define neutrally-toned surface boundaries with normal emphasis. */ + --wpds-color-stroke-surface-neutral-strong: var( + --wpds-color-private-bg-stroke3 + ); /* Decorative stroke color used to define neutrally-toned surface boundaries with strong emphasis. */ + --wpds-color-stroke-surface-neutral-weak: var( + --wpds-color-private-bg-stroke1 + ); /* Decorative stroke color used to define neutrally-toned surface boundaries with weak emphasis. */ + --wpds-color-stroke-surface-success: var( + --wpds-color-private-success-stroke1 + ); /* Decorative stroke color used to define success-toned surface boundaries with normal emphasis. */ + --wpds-color-stroke-surface-success-strong: var( + --wpds-color-private-success-stroke3 + ); /* Decorative stroke color used to define success-toned surface boundaries with strong emphasis. */ + --wpds-color-stroke-surface-warning: var( + --wpds-color-private-warning-stroke1 + ); /* Decorative stroke color used to define warning-toned surface boundaries with normal emphasis. */ + --wpds-color-stroke-surface-warning-strong: var( + --wpds-color-private-warning-stroke3 + ); /* Decorative stroke color used to define warning-toned surface boundaries with strong emphasis. */ + --wpds-elevation-large: 0 5px 15px 0 #00000014, 0 15px 27px 0 #00000012, + 0 30px 36px 0 #0000000a, 0 50px 43px 0 #00000005; /* For components that confirm decisions or handle necessary interruptions. Example: Modals. */ + --wpds-elevation-medium: 0 2px 3px 0 #0000000d, 0 4px 5px 0 #0000000a, + 0 12px 12px 0 #00000008, 0 16px 16px 0 #00000005; /* For components that offer additional actions. Example: Menus, Command Palette */ + --wpds-elevation-small: 0 1px 2px 0 #0000000d, 0 2px 3px 0 #0000000a, + 0 6px 6px 0 #00000008, 0 8px 8px 0 #00000005; /* For components that provide contextual feedback without being intrusive. Generally non-interruptive. Example: Tooltips, Snackbar. */ + --wpds-elevation-x-small: 0 1px 1px 0 #00000008, 0 1px 2px 0 #00000005, + 0 3px 3px 0 #00000005, 0 4px 4px 0 #00000003; /* For sections and containers that group related content and controls, which may overlap other content. Example: Preview Frame. */ + --wpds-font-family-body: -apple-system, system-ui, 'Segoe UI', 'Roboto', + 'Oxygen-Sans', 'Ubuntu', 'Cantarell', 'Helvetica Neue', sans-serif; /* Body font family */ + --wpds-font-family-heading: -apple-system, system-ui, 'Segoe UI', 'Roboto', + 'Oxygen-Sans', 'Ubuntu', 'Cantarell', 'Helvetica Neue', sans-serif; /* Headings font family */ + --wpds-font-family-mono: 'Menlo', 'Consolas', monaco, monospace; /* Monospace font family */ + --wpds-font-line-height-2x-large: 40px; /* 2X large line height */ + --wpds-font-line-height-large: 28px; /* Large line height */ + --wpds-font-line-height-medium: 24px; /* Medium line height */ + --wpds-font-line-height-small: 20px; /* Small line height */ + --wpds-font-line-height-x-large: 32px; /* Extra large line height */ + --wpds-font-line-height-x-small: 16px; /* Extra small line height */ + --wpds-font-size-2x-large: 32px; /* 2X large font size */ + --wpds-font-size-large: 15px; /* Large font size */ + --wpds-font-size-medium: 13px; /* Medium font size */ + --wpds-font-size-small: 12px; /* Small font size */ + --wpds-font-size-x-large: 20px; /* Extra large font size */ + --wpds-font-size-x-small: 11px; /* Extra small font size */ + --wpds-spacing-05: 4px; /* Extra small spacing */ + --wpds-spacing-10: 8px; /* Small spacing */ + --wpds-spacing-15: 12px; /* Medium spacing */ + --wpds-spacing-20: 16px; /* Large spacing */ + --wpds-spacing-30: 24px; /* Extra large spacing */ + --wpds-spacing-40: 32px; /* 2X large spacing */ + --wpds-spacing-50: 40px; /* 3X large spacing */ + --wpds-spacing-60: 48px; /* 4X large spacing */ + --wpds-spacing-70: 56px; /* 5X large spacing */ + --wpds-spacing-80: 64px; /* 6X large spacing */ +} + +@media ( -webkit-min-device-pixel-ratio: 2 ), ( min-resolution: 192dpi ) { + :root { + --wpds-border-width-focus: 1.5px; /* Border width for focus ring */ + } +} diff --git a/packages/theme/src/prebuilt/js/design-tokens.js b/packages/theme/src/prebuilt/js/design-tokens.js new file mode 100644 index 00000000000000..2233c2b52bc887 --- /dev/null +++ b/packages/theme/src/prebuilt/js/design-tokens.js @@ -0,0 +1,116 @@ +/** + * This file is generated by the @terrazzo/plugin-known-wpds-css-variables plugin. + * Do not edit this file directly. + */ + +export default [ + '--wpds-border-radius-x-small', + '--wpds-border-radius-small', + '--wpds-border-radius-medium', + '--wpds-border-radius-large', + '--wpds-border-width-focus', + '--wpds-color-bg-surface-neutral', + '--wpds-color-bg-surface-neutral-strong', + '--wpds-color-bg-surface-neutral-weak', + '--wpds-color-bg-surface-brand', + '--wpds-color-bg-surface-success', + '--wpds-color-bg-surface-success-weak', + '--wpds-color-bg-surface-info', + '--wpds-color-bg-surface-info-weak', + '--wpds-color-bg-surface-warning', + '--wpds-color-bg-surface-warning-weak', + '--wpds-color-bg-surface-error', + '--wpds-color-bg-surface-error-weak', + '--wpds-color-bg-interactive-neutral', + '--wpds-color-bg-interactive-neutral-active', + '--wpds-color-bg-interactive-neutral-disabled', + '--wpds-color-bg-interactive-neutral-strong', + '--wpds-color-bg-interactive-neutral-strong-active', + '--wpds-color-bg-interactive-neutral-strong-disabled', + '--wpds-color-bg-interactive-neutral-weak', + '--wpds-color-bg-interactive-neutral-weak-active', + '--wpds-color-bg-interactive-neutral-weak-disabled', + '--wpds-color-bg-interactive-brand', + '--wpds-color-bg-interactive-brand-active', + '--wpds-color-bg-interactive-brand-disabled', + '--wpds-color-bg-interactive-brand-strong', + '--wpds-color-bg-interactive-brand-strong-active', + '--wpds-color-bg-interactive-brand-strong-disabled', + '--wpds-color-bg-interactive-brand-weak', + '--wpds-color-bg-interactive-brand-weak-active', + '--wpds-color-bg-interactive-brand-weak-disabled', + '--wpds-color-bg-track-neutral-weak', + '--wpds-color-bg-track-neutral', + '--wpds-color-bg-thumb-neutral-weak', + '--wpds-color-bg-thumb-neutral-weak-active', + '--wpds-color-bg-thumb-brand', + '--wpds-color-bg-thumb-brand-active', + '--wpds-color-bg-thumb-brand-disabled', + '--wpds-color-fg-content-neutral', + '--wpds-color-fg-content-neutral-weak', + '--wpds-color-fg-interactive-neutral', + '--wpds-color-fg-interactive-neutral-active', + '--wpds-color-fg-interactive-neutral-disabled', + '--wpds-color-fg-interactive-neutral-strong', + '--wpds-color-fg-interactive-neutral-strong-active', + '--wpds-color-fg-interactive-neutral-strong-disabled', + '--wpds-color-fg-interactive-neutral-weak', + '--wpds-color-fg-interactive-neutral-weak-disabled', + '--wpds-color-fg-interactive-brand', + '--wpds-color-fg-interactive-brand-active', + '--wpds-color-fg-interactive-brand-disabled', + '--wpds-color-fg-interactive-brand-strong', + '--wpds-color-fg-interactive-brand-strong-active', + '--wpds-color-fg-interactive-brand-strong-disabled', + '--wpds-color-stroke-surface-neutral', + '--wpds-color-stroke-surface-neutral-weak', + '--wpds-color-stroke-surface-neutral-strong', + '--wpds-color-stroke-surface-brand', + '--wpds-color-stroke-surface-brand-strong', + '--wpds-color-stroke-surface-success', + '--wpds-color-stroke-surface-success-strong', + '--wpds-color-stroke-surface-info', + '--wpds-color-stroke-surface-info-strong', + '--wpds-color-stroke-surface-warning', + '--wpds-color-stroke-surface-warning-strong', + '--wpds-color-stroke-surface-error', + '--wpds-color-stroke-surface-error-strong', + '--wpds-color-stroke-interactive-neutral', + '--wpds-color-stroke-interactive-neutral-active', + '--wpds-color-stroke-interactive-neutral-disabled', + '--wpds-color-stroke-interactive-neutral-strong', + '--wpds-color-stroke-interactive-brand', + '--wpds-color-stroke-interactive-brand-active', + '--wpds-color-stroke-interactive-brand-disabled', + '--wpds-color-stroke-interactive-error-strong', + '--wpds-color-stroke-focus-brand', + '--wpds-elevation-x-small', + '--wpds-elevation-small', + '--wpds-elevation-medium', + '--wpds-elevation-large', + '--wpds-spacing-05', + '--wpds-spacing-10', + '--wpds-spacing-15', + '--wpds-spacing-20', + '--wpds-spacing-30', + '--wpds-spacing-40', + '--wpds-spacing-50', + '--wpds-spacing-60', + '--wpds-spacing-70', + '--wpds-spacing-80', + '--wpds-font-family-heading', + '--wpds-font-family-body', + '--wpds-font-family-mono', + '--wpds-font-size-x-small', + '--wpds-font-size-small', + '--wpds-font-size-medium', + '--wpds-font-size-large', + '--wpds-font-size-x-large', + '--wpds-font-size-2x-large', + '--wpds-font-line-height-x-small', + '--wpds-font-line-height-small', + '--wpds-font-line-height-medium', + '--wpds-font-line-height-large', + '--wpds-font-line-height-x-large', + '--wpds-font-line-height-2x-large', +]; diff --git a/packages/theme/src/prebuilt/json/figma.json b/packages/theme/src/prebuilt/json/figma.json new file mode 100644 index 00000000000000..4b8d01f7edada8 --- /dev/null +++ b/packages/theme/src/prebuilt/json/figma.json @@ -0,0 +1,1317 @@ +{ + "Border/radius-x-small": { + "value": { + ".": "1px" + }, + "description": "Extra small radius" + }, + "Border/radius-small": { + "value": { + ".": "2px" + }, + "description": "Small radius" + }, + "Border/radius-medium": { + "value": { + ".": "4px" + }, + "description": "Medium radius" + }, + "Border/radius-large": { + "value": { + ".": "8px" + }, + "description": "Large radius" + }, + "Border/width-focus": { + "value": { + ".": "2px", + "high-dpi": "1.5px" + }, + "description": "Border width for focus ring" + }, + "Color/_Primitives/Primary/bgFill1": { + "value": { + ".": "#3858e9" + } + }, + "Color/_Primitives/Primary/fgFill": { + "value": { + ".": "#eff0f2" + } + }, + "Color/_Primitives/Primary/bgFill2": { + "value": { + ".": "#2c47d7" + } + }, + "Color/_Primitives/Primary/surface2": { + "value": { + ".": "#f6f8fc" + } + }, + "Color/_Primitives/Primary/surface6": { + "value": { + ".": "#c4d0ee" + } + }, + "Color/_Primitives/Primary/surface5": { + "value": { + ".": "#d7dff3" + } + }, + "Color/_Primitives/Primary/surface4": { + "value": { + ".": "#e5eaf7" + } + }, + "Color/_Primitives/Primary/surface3": { + "value": { + ".": "#fff" + } + }, + "Color/_Primitives/Primary/fgSurface4": { + "value": { + ".": "#080071" + } + }, + "Color/_Primitives/Primary/fgSurface3": { + "value": { + ".": "#3858e9" + } + }, + "Color/_Primitives/Primary/fgSurface2": { + "value": { + ".": "#577fff" + } + }, + "Color/_Primitives/Primary/fgSurface1": { + "value": { + ".": "#81a6ff" + } + }, + "Color/_Primitives/Primary/stroke3": { + "value": { + ".": "#3858e9" + } + }, + "Color/_Primitives/Primary/stroke4": { + "value": { + ".": "#2236c7" + } + }, + "Color/_Primitives/Primary/stroke2": { + "value": { + ".": "#7085c0" + } + }, + "Color/_Primitives/Primary/stroke1": { + "value": { + ".": "#93a4d0" + } + }, + "Color/_Primitives/Primary/bgFillDark": { + "value": { + ".": "#1b1e26" + } + }, + "Color/_Primitives/Primary/fgFillDark": { + "value": { + ".": "#eff0f2" + } + }, + "Color/_Primitives/Primary/bgFillInverted2": { + "value": { + ".": "#1b1e26" + } + }, + "Color/_Primitives/Primary/bgFillInverted1": { + "value": { + ".": "#1401a5" + } + }, + "Color/_Primitives/Primary/fgFillInverted": { + "value": { + ".": "#eff0f2" + } + }, + "Color/_Primitives/Primary/surface1": { + "value": { + ".": "#ecf0f9" + } + }, + "Color/_Primitives/Info/bgFill1": { + "value": { + ".": "#0090ff" + } + }, + "Color/_Primitives/Info/fgFill": { + "value": { + ".": "#1b1e23" + } + }, + "Color/_Primitives/Info/bgFill2": { + "value": { + ".": "#007eec" + } + }, + "Color/_Primitives/Info/surface2": { + "value": { + ".": "#f5f9fd" + } + }, + "Color/_Primitives/Info/surface6": { + "value": { + ".": "#b9d3f0" + } + }, + "Color/_Primitives/Info/surface5": { + "value": { + ".": "#d0e1f5" + } + }, + "Color/_Primitives/Info/surface4": { + "value": { + ".": "#e0ecf8" + } + }, + "Color/_Primitives/Info/surface3": { + "value": { + ".": "#fff" + } + }, + "Color/_Primitives/Info/fgSurface4": { + "value": { + ".": "#001758" + } + }, + "Color/_Primitives/Info/fgSurface3": { + "value": { + ".": "#006cd8" + } + }, + "Color/_Primitives/Info/fgSurface2": { + "value": { + ".": "#008bf9" + } + }, + "Color/_Primitives/Info/fgSurface1": { + "value": { + ".": "#4dafff" + } + }, + "Color/_Primitives/Info/stroke3": { + "value": { + ".": "#006cd8" + } + }, + "Color/_Primitives/Info/stroke4": { + "value": { + ".": "#004bb5" + } + }, + "Color/_Primitives/Info/stroke2": { + "value": { + ".": "#5c90c9" + } + }, + "Color/_Primitives/Info/stroke1": { + "value": { + ".": "#8baed6" + } + }, + "Color/_Primitives/Info/bgFillDark": { + "value": { + ".": "#1b1e23" + } + }, + "Color/_Primitives/Info/fgFillDark": { + "value": { + ".": "#eff0f2" + } + }, + "Color/_Primitives/Info/bgFillInverted2": { + "value": { + ".": "#1b1e23" + } + }, + "Color/_Primitives/Info/bgFillInverted1": { + "value": { + ".": "#00297b" + } + }, + "Color/_Primitives/Info/fgFillInverted": { + "value": { + ".": "#eff0f2" + } + }, + "Color/_Primitives/Info/surface1": { + "value": { + ".": "#e9f1fa" + } + }, + "Color/_Primitives/Success/bgFill1": { + "value": { + ".": "#4ab866" + } + }, + "Color/_Primitives/Success/fgFill": { + "value": { + ".": "#1b1f1c" + } + }, + "Color/_Primitives/Success/bgFill2": { + "value": { + ".": "#34a554" + } + }, + "Color/_Primitives/Success/surface2": { + "value": { + ".": "#f0fcf2" + } + }, + "Color/_Primitives/Success/surface6": { + "value": { + ".": "#7be792" + } + }, + "Color/_Primitives/Success/surface5": { + "value": { + ".": "#abf0b7" + } + }, + "Color/_Primitives/Success/surface4": { + "value": { + ".": "#cdf5d2" + } + }, + "Color/_Primitives/Success/surface3": { + "value": { + ".": "#fff" + } + }, + "Color/_Primitives/Success/fgSurface4": { + "value": { + ".": "#002b00" + } + }, + "Color/_Primitives/Success/fgSurface3": { + "value": { + ".": "#008030" + } + }, + "Color/_Primitives/Success/fgSurface2": { + "value": { + ".": "#2a9e4d" + } + }, + "Color/_Primitives/Success/fgSurface1": { + "value": { + ".": "#52bf6d" + } + }, + "Color/_Primitives/Success/stroke3": { + "value": { + ".": "#008030" + } + }, + "Color/_Primitives/Success/stroke4": { + "value": { + ".": "#006013" + } + }, + "Color/_Primitives/Success/stroke2": { + "value": { + ".": "#629a6c" + } + }, + "Color/_Primitives/Success/stroke1": { + "value": { + ".": "#78bb84" + } + }, + "Color/_Primitives/Success/bgFillDark": { + "value": { + ".": "#1b1f1c" + } + }, + "Color/_Primitives/Success/fgFillDark": { + "value": { + ".": "#edf2ed" + } + }, + "Color/_Primitives/Success/bgFillInverted2": { + "value": { + ".": "#1b1f1c" + } + }, + "Color/_Primitives/Success/bgFillInverted1": { + "value": { + ".": "#003b00" + } + }, + "Color/_Primitives/Success/fgFillInverted": { + "value": { + ".": "#edf2ed" + } + }, + "Color/_Primitives/Success/surface1": { + "value": { + ".": "#ddf8e1" + } + }, + "Color/_Primitives/Warning/bgFill1": { + "value": { + ".": "#f0b849" + } + }, + "Color/_Primitives/Warning/fgFill": { + "value": { + ".": "#1f1e1b" + } + }, + "Color/_Primitives/Warning/bgFill2": { + "value": { + ".": "#dba32f" + } + }, + "Color/_Primitives/Warning/surface2": { + "value": { + ".": "#fdf7ee" + } + }, + "Color/_Primitives/Warning/surface6": { + "value": { + ".": "#f0cb89" + } + }, + "Color/_Primitives/Warning/surface5": { + "value": { + ".": "#f5dcb2" + } + }, + "Color/_Primitives/Warning/surface4": { + "value": { + ".": "#f8e8cd" + } + }, + "Color/_Primitives/Warning/surface3": { + "value": { + ".": "#fff" + } + }, + "Color/_Primitives/Warning/fgSurface4": { + "value": { + ".": "#2f1800" + } + }, + "Color/_Primitives/Warning/fgSurface3": { + "value": { + ".": "#966200" + } + }, + "Color/_Primitives/Warning/fgSurface2": { + "value": { + ".": "#b58000" + } + }, + "Color/_Primitives/Warning/fgSurface1": { + "value": { + ".": "#d7a02b" + } + }, + "Color/_Primitives/Warning/stroke3": { + "value": { + ".": "#966200" + } + }, + "Color/_Primitives/Warning/stroke4": { + "value": { + ".": "#724700" + } + }, + "Color/_Primitives/Warning/stroke2": { + "value": { + ".": "#a18a61" + } + }, + "Color/_Primitives/Warning/stroke1": { + "value": { + ".": "#c3a777" + } + }, + "Color/_Primitives/Warning/bgFillDark": { + "value": { + ".": "#1f1e1b" + } + }, + "Color/_Primitives/Warning/fgFillDark": { + "value": { + ".": "#f3f0e9" + } + }, + "Color/_Primitives/Warning/bgFillInverted2": { + "value": { + ".": "#1f1e1b" + } + }, + "Color/_Primitives/Warning/bgFillInverted1": { + "value": { + ".": "#472900" + } + }, + "Color/_Primitives/Warning/fgFillInverted": { + "value": { + ".": "#f3f0e9" + } + }, + "Color/_Primitives/Warning/surface1": { + "value": { + ".": "#faefdc" + } + }, + "Color/_Primitives/Error/bgFill1": { + "value": { + ".": "#cc1818" + } + }, + "Color/_Primitives/Error/fgFill": { + "value": { + ".": "#f2efef" + } + }, + "Color/_Primitives/Error/bgFill2": { + "value": { + ".": "#b90000" + } + }, + "Color/_Primitives/Error/surface2": { + "value": { + ".": "#fdf6f5" + } + }, + "Color/_Primitives/Error/surface6": { + "value": { + ".": "#f5c5bd" + } + }, + "Color/_Primitives/Error/surface5": { + "value": { + ".": "#f8d8d3" + } + }, + "Color/_Primitives/Error/surface4": { + "value": { + ".": "#fae5e2" + } + }, + "Color/_Primitives/Error/surface3": { + "value": { + ".": "#fff" + } + }, + "Color/_Primitives/Error/fgSurface4": { + "value": { + ".": "#4a0000" + } + }, + "Color/_Primitives/Error/fgSurface3": { + "value": { + ".": "#cc1818" + } + }, + "Color/_Primitives/Error/fgSurface2": { + "value": { + ".": "#f64b40" + } + }, + "Color/_Primitives/Error/fgSurface1": { + "value": { + ".": "#ff8070" + } + }, + "Color/_Primitives/Error/stroke3": { + "value": { + ".": "#cc1818" + } + }, + "Color/_Primitives/Error/stroke4": { + "value": { + ".": "#a30000" + } + }, + "Color/_Primitives/Error/stroke2": { + "value": { + ".": "#cd695d" + } + }, + "Color/_Primitives/Error/stroke1": { + "value": { + ".": "#dc9085" + } + }, + "Color/_Primitives/Error/bgFillDark": { + "value": { + ".": "#231c1b" + } + }, + "Color/_Primitives/Error/fgFillDark": { + "value": { + ".": "#f2efef" + } + }, + "Color/_Primitives/Error/bgFillInverted2": { + "value": { + ".": "#231c1b" + } + }, + "Color/_Primitives/Error/bgFillInverted1": { + "value": { + ".": "#6d0000" + } + }, + "Color/_Primitives/Error/fgFillInverted": { + "value": { + ".": "#f2efef" + } + }, + "Color/_Primitives/Error/surface1": { + "value": { + ".": "#fcedea" + } + }, + "Color/_Primitives/Bg/surface2": { + "value": { + ".": "#f8f8f8" + } + }, + "Color/_Primitives/Bg/bgFill1": { + "value": { + ".": "#555" + } + }, + "Color/_Primitives/Bg/fgFill": { + "value": { + ".": "#f0f0f0" + } + }, + "Color/_Primitives/Bg/bgFill2": { + "value": { + ".": "#474747" + } + }, + "Color/_Primitives/Bg/surface6": { + "value": { + ".": "#d0d0d0" + } + }, + "Color/_Primitives/Bg/surface5": { + "value": { + ".": "#dfdfdf" + } + }, + "Color/_Primitives/Bg/surface4": { + "value": { + ".": "#eaeaea" + } + }, + "Color/_Primitives/Bg/surface3": { + "value": { + ".": "#fff" + } + }, + "Color/_Primitives/Bg/fgSurface4": { + "value": { + ".": "#1e1e1e" + } + }, + "Color/_Primitives/Bg/fgSurface3": { + "value": { + ".": "#6c6c6c" + } + }, + "Color/_Primitives/Bg/fgSurface2": { + "value": { + ".": "#898989" + } + }, + "Color/_Primitives/Bg/fgSurface1": { + "value": { + ".": "#a9a9a9" + } + }, + "Color/_Primitives/Bg/stroke3": { + "value": { + ".": "#898989" + } + }, + "Color/_Primitives/Bg/stroke4": { + "value": { + ".": "#6a6a6a" + } + }, + "Color/_Primitives/Bg/stroke2": { + "value": { + ".": "#aeaeae" + } + }, + "Color/_Primitives/Bg/stroke1": { + "value": { + ".": "#d0d0d0" + } + }, + "Color/_Primitives/Bg/bgFillDark": { + "value": { + ".": "#1e1e1e" + } + }, + "Color/_Primitives/Bg/fgFillDark": { + "value": { + ".": "#f0f0f0" + } + }, + "Color/_Primitives/Bg/bgFillInverted2": { + "value": { + ".": "#1e1e1e" + } + }, + "Color/_Primitives/Bg/bgFillInverted1": { + "value": { + ".": "#2f2f2f" + } + }, + "Color/_Primitives/Bg/fgFillInverted": { + "value": { + ".": "#f0f0f0" + } + }, + "Color/_Primitives/Bg/surface1": { + "value": { + ".": "#efefef" + } + }, + "Color/Semantic/Background/Neutral/bgSur-neutral": { + "value": { + ".": "{Color/_Primitives/Bg/surface2}" + }, + "description": "Background color for surfaces with normal emphasis." + }, + "Color/Semantic/Background/Neutral/bgSur-neutral-strong": { + "value": { + ".": "{Color/_Primitives/Bg/surface3}" + }, + "description": "Background color for surfaces with strong emphasis." + }, + "Color/Semantic/Background/Neutral/bgSur-neutral-weak": { + "value": { + ".": "{Color/_Primitives/Bg/surface1}" + }, + "description": "Background color for surfaces with weak emphasis." + }, + "Color/Semantic/Background/Brand/bgSur-brand": { + "value": { + ".": "{Color/_Primitives/Primary/surface1}" + }, + "description": "Background color for surfaces with brand tone and normal emphasis." + }, + "Color/Semantic/Background/Success/bgSur-success": { + "value": { + ".": "{Color/_Primitives/Success/surface4}" + }, + "description": "Background color for surfaces with success tone and normal emphasis." + }, + "Color/Semantic/Background/Success/bgSur-success-weak": { + "value": { + ".": "{Color/_Primitives/Success/surface2}" + }, + "description": "Background color for surfaces with success tone and weak emphasis." + }, + "Color/Semantic/Background/Info/bgSur-info": { + "value": { + ".": "{Color/_Primitives/Info/surface4}" + }, + "description": "Background color for surfaces with info tone and normal emphasis." + }, + "Color/Semantic/Background/Info/bgSur-info-weak": { + "value": { + ".": "{Color/_Primitives/Info/surface2}" + }, + "description": "Background color for surfaces with info tone and weak emphasis." + }, + "Color/Semantic/Background/Warning/bgSur-warning": { + "value": { + ".": "{Color/_Primitives/Warning/surface4}" + }, + "description": "Background color for surfaces with warning tone and normal emphasis." + }, + "Color/Semantic/Background/Warning/bgSur-warning-weak": { + "value": { + ".": "{Color/_Primitives/Warning/surface2}" + }, + "description": "Background color for surfaces with warning tone and weak emphasis." + }, + "Color/Semantic/Background/Error/bgSur-error": { + "value": { + ".": "{Color/_Primitives/Error/surface4}" + }, + "description": "Background color for surfaces with error tone and normal emphasis." + }, + "Color/Semantic/Background/Error/bgSur-error-weak": { + "value": { + ".": "{Color/_Primitives/Error/surface2}" + }, + "description": "Background color for surfaces with error tone and weak emphasis." + }, + "Color/Semantic/Background/Neutral/bgInt-neutral": { + "value": { + ".": "#0000" + }, + "description": "Background color for interactive elements with neutral tone and normal emphasis." + }, + "Color/Semantic/Background/Neutral/bgInt-neutral-active": { + "value": { + ".": "{Color/_Primitives/Bg/surface4}" + }, + "description": "Background color for interactive elements with neutral tone and normal emphasis that are hovered, focused, or active." + }, + "Color/Semantic/Background/Neutral/bgInt-neutral-disabled": { + "value": { + ".": "{Color/_Primitives/Bg/surface5}" + }, + "description": "Background color for interactive elements with neutral tone and normal emphasis, in their disabled state." + }, + "Color/Semantic/Background/Neutral/bgInt-neutral-strong": { + "value": { + ".": "{Color/_Primitives/Bg/bgFillInverted1}" + }, + "description": "Background color for interactive elements with neutral tone and strong emphasis." + }, + "Color/Semantic/Background/Neutral/bgInt-neutral-strong-active": { + "value": { + ".": "{Color/_Primitives/Bg/bgFillInverted2}" + }, + "description": "Background color for interactive elements with neutral tone and strong emphasis that are hovered, focused, or active." + }, + "Color/Semantic/Background/Neutral/bgInt-neutral-strong-disabled": { + "value": { + ".": "{Color/_Primitives/Bg/surface6}" + }, + "description": "Background color for interactive elements with neutral tone and strong emphasis, in their disabled state." + }, + "Color/Semantic/Background/Neutral/bgInt-neutral-weak": { + "value": { + ".": "#0000" + }, + "description": "Background color for interactive elements with neutral tone and weak emphasis." + }, + "Color/Semantic/Background/Neutral/bgInt-neutral-weak-active": { + "value": { + ".": "{Color/_Primitives/Bg/surface4}" + }, + "description": "Background color for interactive elements with neutral tone and weak emphasis that are hovered, focused, or active." + }, + "Color/Semantic/Background/Neutral/bgInt-neutral-weak-disabled": { + "value": { + ".": "{Color/_Primitives/Bg/surface5}" + }, + "description": "Background color for interactive elements with neutral tone and weak emphasis, in their disabled state." + }, + "Color/Semantic/Background/Brand/bgInt-brand": { + "value": { + ".": "#0000" + }, + "description": "Background color for interactive elements with brand tone and normal emphasis." + }, + "Color/Semantic/Background/Brand/bgInt-brand-active": { + "value": { + ".": "{Color/_Primitives/Primary/surface2}" + }, + "description": "Background color for interactive elements with brand tone and normal emphasis that are hovered, focused, or active." + }, + "Color/Semantic/Background/Brand/bgInt-brand-disabled": { + "value": { + ".": "{Color/_Primitives/Bg/surface5}" + }, + "description": "Background color for interactive elements with brand tone and normal emphasis, in their disabled state." + }, + "Color/Semantic/Background/Brand/bgInt-brand-strong": { + "value": { + ".": "{Color/_Primitives/Primary/bgFill1}" + }, + "description": "Background color for interactive elements with brand tone and strong emphasis." + }, + "Color/Semantic/Background/Brand/bgInt-brand-strong-active": { + "value": { + ".": "{Color/_Primitives/Primary/bgFill2}" + }, + "description": "Background color for interactive elements with brand tone and strong emphasis that are hovered, focused, or active." + }, + "Color/Semantic/Background/Brand/bgInt-brand-strong-disabled": { + "value": { + ".": "{Color/_Primitives/Bg/surface6}" + }, + "description": "Background color for interactive elements with brand tone and strong emphasis, in their disabled state." + }, + "Color/Semantic/Background/Brand/bgInt-brand-weak": { + "value": { + ".": "#0000" + }, + "description": "Background color for interactive elements with brand tone and weak emphasis." + }, + "Color/Semantic/Background/Brand/bgInt-brand-weak-active": { + "value": { + ".": "{Color/_Primitives/Primary/surface4}" + }, + "description": "Background color for interactive elements with brand tone and weak emphasis that are hovered, focused, or active." + }, + "Color/Semantic/Background/Brand/bgInt-brand-weak-disabled": { + "value": { + ".": "{Color/_Primitives/Bg/surface5}" + }, + "description": "Background color for interactive elements with brand tone and weak emphasis, in their disabled state." + }, + "Color/Semantic/Background/Neutral/bgTra-neutral-weak": { + "value": { + ".": "{Color/_Primitives/Bg/stroke1}" + }, + "description": "Background color for tracks with a neutral tone and weak emphasis (eg. scrollbar track)." + }, + "Color/Semantic/Background/Neutral/bgTra-neutral": { + "value": { + ".": "{Color/_Primitives/Bg/stroke2}" + }, + "description": "Background color for tracks with a neutral tone and normal emphasis (eg. slider or progressbar track)." + }, + "Color/Semantic/Background/Neutral/bgThu-neutral-weak": { + "value": { + ".": "{Color/_Primitives/Bg/stroke3}" + }, + "description": "Background color for thumbs with a neutral tone and weak emphasis (eg. scrollbar thumb)." + }, + "Color/Semantic/Background/Neutral/bgThu-neutral-weak-active": { + "value": { + ".": "{Color/_Primitives/Bg/stroke4}" + }, + "description": "Background color for thumbs with a neutral tone and weak emphasis (eg. scrollbar thumb) that are hovered, focused, or active." + }, + "Color/Semantic/Background/Brand/bgThu-brand": { + "value": { + ".": "{Color/_Primitives/Primary/stroke3}" + }, + "description": "Background color for thumbs with a brand tone and normal emphasis (eg. slider thumb and filled track)." + }, + "Color/Semantic/Background/Brand/bgThu-brand-active": { + "value": { + ".": "{Color/_Primitives/Primary/stroke3}" + }, + "description": "Background color for thumbs with a brand tone and normal emphasis (eg. slider thumb and filled track) that are hovered, focused, or active." + }, + "Color/Semantic/Background/Brand/bgThu-brand-disabled": { + "value": { + ".": "{Color/_Primitives/Bg/stroke2}" + }, + "description": "Background color for thumbs with a brand tone and normal emphasis (eg. slider thumb and filled track), in their disabled state." + }, + "Color/Semantic/Foreground/Neutral/fgCon-neutral": { + "value": { + ".": "{Color/_Primitives/Bg/fgSurface4}" + }, + "description": "Foreground color for content like text with normal emphasis." + }, + "Color/Semantic/Foreground/Neutral/fgCon-neutral-weak": { + "value": { + ".": "{Color/_Primitives/Bg/fgSurface3}" + }, + "description": "Foreground color for content like text with weak emphasis." + }, + "Color/Semantic/Foreground/Neutral/fgInt-neutral": { + "value": { + ".": "{Color/_Primitives/Bg/fgSurface4}" + }, + "description": "Foreground color for interactive elements with neutral tone and normal emphasis." + }, + "Color/Semantic/Foreground/Neutral/fgInt-neutral-active": { + "value": { + ".": "{Color/_Primitives/Bg/fgSurface4}" + }, + "description": "Foreground color for interactive elements with neutral tone and normal emphasis that are hovered, focused, or active." + }, + "Color/Semantic/Foreground/Neutral/fgInt-neutral-disabled": { + "value": { + ".": "{Color/_Primitives/Bg/fgSurface2}" + }, + "description": "Foreground color for interactive elements with neutral tone and normal emphasis, in their disabled state." + }, + "Color/Semantic/Foreground/Neutral/fgInt-neutral-strong": { + "value": { + ".": "{Color/_Primitives/Bg/fgFillInverted}" + }, + "description": "Foreground color for interactive elements with neutral tone and strong emphasis." + }, + "Color/Semantic/Foreground/Neutral/fgInt-neutral-strong-active": { + "value": { + ".": "{Color/_Primitives/Bg/fgFillInverted}" + }, + "description": "Foreground color for interactive elements with neutral tone and strong emphasis that are hovered, focused, or active." + }, + "Color/Semantic/Foreground/Neutral/fgInt-neutral-strong-disabled": { + "value": { + ".": "{Color/_Primitives/Bg/fgSurface3}" + }, + "description": "Foreground color for interactive elements with neutral tone and strong emphasis, in their disabled state." + }, + "Color/Semantic/Foreground/Neutral/fgInt-neutral-weak": { + "value": { + ".": "{Color/_Primitives/Bg/fgSurface3}" + }, + "description": "Foreground color for interactive elements with neutral tone and weak emphasis." + }, + "Color/Semantic/Foreground/Neutral/fgInt-neutral-weak-disabled": { + "value": { + ".": "{Color/_Primitives/Bg/fgSurface2}" + }, + "description": "Foreground color for interactive elements with neutral tone and weak emphasis, in their disabled state." + }, + "Color/Semantic/Foreground/Brand/fgInt-brand": { + "value": { + ".": "{Color/_Primitives/Primary/fgSurface3}" + }, + "description": "Foreground color for interactive elements with brand tone and normal emphasis." + }, + "Color/Semantic/Foreground/Brand/fgInt-brand-active": { + "value": { + ".": "{Color/_Primitives/Primary/fgSurface3}" + }, + "description": "Foreground color for interactive elements with brand tone and normal emphasis that are hovered, focused, or active." + }, + "Color/Semantic/Foreground/Brand/fgInt-brand-disabled": { + "value": { + ".": "{Color/_Primitives/Bg/fgSurface2}" + }, + "description": "Foreground color for interactive elements with brand tone and normal emphasis, in their disabled state." + }, + "Color/Semantic/Foreground/Brand/fgInt-brand-strong": { + "value": { + ".": "{Color/_Primitives/Primary/fgFill}" + }, + "description": "Foreground color for interactive elements with brand tone and strong emphasis." + }, + "Color/Semantic/Foreground/Brand/fgInt-brand-strong-active": { + "value": { + ".": "{Color/_Primitives/Primary/fgFill}" + }, + "description": "Foreground color for interactive elements with brand tone and strong emphasis that are hovered, focused, or active." + }, + "Color/Semantic/Foreground/Brand/fgInt-brand-strong-disabled": { + "value": { + ".": "{Color/_Primitives/Bg/fgSurface3}" + }, + "description": "Foreground color for interactive elements with brand tone and strong emphasis, in their disabled state." + }, + "Color/Semantic/Stroke/Neutral/strokeSur-neutral": { + "value": { + ".": "{Color/_Primitives/Bg/stroke2}" + }, + "description": "Decorative stroke color used to define neutrally-toned surface boundaries with normal emphasis." + }, + "Color/Semantic/Stroke/Neutral/strokeSur-neutral-weak": { + "value": { + ".": "{Color/_Primitives/Bg/stroke1}" + }, + "description": "Decorative stroke color used to define neutrally-toned surface boundaries with weak emphasis." + }, + "Color/Semantic/Stroke/Neutral/strokeSur-neutral-strong": { + "value": { + ".": "{Color/_Primitives/Bg/stroke3}" + }, + "description": "Decorative stroke color used to define neutrally-toned surface boundaries with strong emphasis." + }, + "Color/Semantic/Stroke/Brand/strokeSur-brand": { + "value": { + ".": "{Color/_Primitives/Primary/stroke1}" + }, + "description": "Decorative stroke color used to define brand-toned surface boundaries with normal emphasis." + }, + "Color/Semantic/Stroke/Brand/strokeSur-brand-strong": { + "value": { + ".": "{Color/_Primitives/Primary/stroke3}" + }, + "description": "Decorative stroke color used to define neutrally-toned surface boundaries with strong emphasis." + }, + "Color/Semantic/Stroke/Success/strokeSur-success": { + "value": { + ".": "{Color/_Primitives/Success/stroke1}" + }, + "description": "Decorative stroke color used to define success-toned surface boundaries with normal emphasis." + }, + "Color/Semantic/Stroke/Success/strokeSur-success-strong": { + "value": { + ".": "{Color/_Primitives/Success/stroke3}" + }, + "description": "Decorative stroke color used to define success-toned surface boundaries with strong emphasis." + }, + "Color/Semantic/Stroke/Info/strokeSur-info": { + "value": { + ".": "{Color/_Primitives/Info/stroke1}" + }, + "description": "Decorative stroke color used to define info-toned surface boundaries with normal emphasis." + }, + "Color/Semantic/Stroke/Info/strokeSur-info-strong": { + "value": { + ".": "{Color/_Primitives/Info/stroke3}" + }, + "description": "Decorative stroke color used to define info-toned surface boundaries with strong emphasis." + }, + "Color/Semantic/Stroke/Warning/strokeSur-warning": { + "value": { + ".": "{Color/_Primitives/Warning/stroke1}" + }, + "description": "Decorative stroke color used to define warning-toned surface boundaries with normal emphasis." + }, + "Color/Semantic/Stroke/Warning/strokeSur-warning-strong": { + "value": { + ".": "{Color/_Primitives/Warning/stroke3}" + }, + "description": "Decorative stroke color used to define warning-toned surface boundaries with strong emphasis." + }, + "Color/Semantic/Stroke/Error/strokeSur-error": { + "value": { + ".": "{Color/_Primitives/Error/stroke1}" + }, + "description": "Decorative stroke color used to define error-toned surface boundaries with normal emphasis." + }, + "Color/Semantic/Stroke/Error/strokeSur-error-strong": { + "value": { + ".": "{Color/_Primitives/Error/stroke3}" + }, + "description": "Decorative stroke color used to define error-toned surface boundaries with strong emphasis." + }, + "Color/Semantic/Stroke/Neutral/strokeInt-neutral": { + "value": { + ".": "{Color/_Primitives/Bg/stroke3}" + }, + "description": "Accessible stroke color used for interactive neutrally-toned elements with normal emphasis." + }, + "Color/Semantic/Stroke/Neutral/strokeInt-neutral-active": { + "value": { + ".": "{Color/_Primitives/Bg/stroke4}" + }, + "description": "Accessible stroke color used for interactive neutrally-toned elements with normal emphasis that are hovered, focused, or active." + }, + "Color/Semantic/Stroke/Neutral/strokeInt-neutral-disabled": { + "value": { + ".": "{Color/_Primitives/Bg/stroke2}" + }, + "description": "Accessible stroke color used for interactive neutrally-toned elements with normal emphasis, in their disabled state." + }, + "Color/Semantic/Stroke/Neutral/strokeInt-neutral-strong": { + "value": { + ".": "{Color/_Primitives/Bg/stroke4}" + }, + "description": "Accessible stroke color used for interactive neutrally-toned elements with strong emphasis." + }, + "Color/Semantic/Stroke/Brand/strokeInt-brand": { + "value": { + ".": "{Color/_Primitives/Primary/stroke3}" + }, + "description": "Accessible stroke color used for interactive brand-toned elements with normal emphasis." + }, + "Color/Semantic/Stroke/Brand/strokeInt-brand-active": { + "value": { + ".": "{Color/_Primitives/Primary/stroke4}" + }, + "description": "Accessible stroke color used for interactive brand-toned elements with normal emphasis that are hovered, focused, or active." + }, + "Color/Semantic/Stroke/Brand/strokeInt-brand-disabled": { + "value": { + ".": "{Color/_Primitives/Bg/stroke2}" + }, + "description": "Accessible stroke color used for interactive brand-toned elements with normal emphasis, in their disabled state." + }, + "Color/Semantic/Stroke/Error/strokeInt-error-strong": { + "value": { + ".": "{Color/_Primitives/Error/stroke3}" + }, + "description": "Accessible stroke color used for interactive error-toned elements with strong emphasis." + }, + "Color/Semantic/Stroke/Brand/strokeFoc-brand": { + "value": { + ".": "{Color/_Primitives/Primary/stroke3}" + }, + "description": "Accessible stroke color applied to focus rings." + }, + "Elevation/x-small": { + "value": { + ".": "0 1px 1px 0 color(srgb 0 0 0 / 0.03), 0 1px 2px 0 color(srgb 0 0 0 / 0.02), 0 3px 3px 0 color(srgb 0 0 0 / 0.02), 0 4px 4px 0 color(srgb 0 0 0 / 0.01)" + }, + "description": "For sections and containers that group related content and controls, which may overlap other content. Example: Preview Frame." + }, + "Elevation/small": { + "value": { + ".": "0 1px 2px 0 color(srgb 0 0 0 / 0.05), 0 2px 3px 0 color(srgb 0 0 0 / 0.04), 0 6px 6px 0 color(srgb 0 0 0 / 0.03), 0 8px 8px 0 color(srgb 0 0 0 / 0.02)" + }, + "description": "For components that provide contextual feedback without being intrusive. Generally non-interruptive. Example: Tooltips, Snackbar." + }, + "Elevation/medium": { + "value": { + ".": "0 2px 3px 0 color(srgb 0 0 0 / 0.05), 0 4px 5px 0 color(srgb 0 0 0 / 0.04), 0 12px 12px 0 color(srgb 0 0 0 / 0.03), 0 16px 16px 0 color(srgb 0 0 0 / 0.02)" + }, + "description": "For components that offer additional actions. Example: Menus, Command Palette" + }, + "Elevation/large": { + "value": { + ".": "0 5px 15px 0 color(srgb 0 0 0 / 0.08), 0 15px 27px 0 color(srgb 0 0 0 / 0.07), 0 30px 36px 0 color(srgb 0 0 0 / 0.04), 0 50px 43px 0 color(srgb 0 0 0 / 0.02)" + }, + "description": "For components that confirm decisions or handle necessary interruptions. Example: Modals." + }, + "Spacing/05": { + "value": { + ".": "4px" + }, + "description": "Extra small spacing" + }, + "Spacing/10": { + "value": { + ".": "8px" + }, + "description": "Small spacing" + }, + "Spacing/15": { + "value": { + ".": "12px" + }, + "description": "Medium spacing" + }, + "Spacing/20": { + "value": { + ".": "16px" + }, + "description": "Large spacing" + }, + "Spacing/30": { + "value": { + ".": "24px" + }, + "description": "Extra large spacing" + }, + "Spacing/40": { + "value": { + ".": "32px" + }, + "description": "2X large spacing" + }, + "Spacing/50": { + "value": { + ".": "40px" + }, + "description": "3X large spacing" + }, + "Spacing/60": { + "value": { + ".": "48px" + }, + "description": "4X large spacing" + }, + "Spacing/70": { + "value": { + ".": "56px" + }, + "description": "5X large spacing" + }, + "Spacing/80": { + "value": { + ".": "64px" + }, + "description": "6X large spacing" + }, + "Font/family-heading": { + "value": { + ".": "-apple-system, system-ui, \"Segoe UI\", \"Roboto\", \"Oxygen-Sans\", \"Ubuntu\", \"Cantarell\", \"Helvetica Neue\", sans-serif" + }, + "description": "Headings font family" + }, + "Font/family-body": { + "value": { + ".": "-apple-system, system-ui, \"Segoe UI\", \"Roboto\", \"Oxygen-Sans\", \"Ubuntu\", \"Cantarell\", \"Helvetica Neue\", sans-serif" + }, + "description": "Body font family" + }, + "Font/family-mono": { + "value": { + ".": "\"Menlo\", \"Consolas\", monaco, monospace" + }, + "description": "Monospace font family" + }, + "Font/size-x-small": { + "value": { + ".": "11px" + }, + "description": "Extra small font size" + }, + "Font/size-small": { + "value": { + ".": "12px" + }, + "description": "Small font size" + }, + "Font/size-medium": { + "value": { + ".": "13px" + }, + "description": "Medium font size" + }, + "Font/size-large": { + "value": { + ".": "15px" + }, + "description": "Large font size" + }, + "Font/size-x-large": { + "value": { + ".": "20px" + }, + "description": "Extra large font size" + }, + "Font/size-2x-large": { + "value": { + ".": "32px" + }, + "description": "2X large font size" + }, + "Font/lineHeight-x-small": { + "value": { + ".": "16px" + }, + "description": "Extra small line height" + }, + "Font/lineHeight-small": { + "value": { + ".": "20px" + }, + "description": "Small line height" + }, + "Font/lineHeight-medium": { + "value": { + ".": "24px" + }, + "description": "Medium line height" + }, + "Font/lineHeight-large": { + "value": { + ".": "28px" + }, + "description": "Large line height" + }, + "Font/lineHeight-x-large": { + "value": { + ".": "32px" + }, + "description": "Extra large line height" + }, + "Font/lineHeight-2x-large": { + "value": { + ".": "40px" + }, + "description": "2X large line height" + } +} diff --git a/packages/theme/src/prebuilt/ts/design-tokens.ts b/packages/theme/src/prebuilt/ts/design-tokens.ts new file mode 100644 index 00000000000000..ab40c5a4041c42 --- /dev/null +++ b/packages/theme/src/prebuilt/ts/design-tokens.ts @@ -0,0 +1,335 @@ +/** + * This file is generated by the @terrazzo/plugin-known-wpds-css-variables plugin. + * Do not edit this file directly. + */ + +export default { + '--wpds-border-radius-x-small': { + '.': '1px', + }, + '--wpds-border-radius-small': { + '.': '2px', + }, + '--wpds-border-radius-medium': { + '.': '4px', + }, + '--wpds-border-radius-large': { + '.': '8px', + }, + '--wpds-border-width-focus': { + '.': '2px', + 'high-dpi': '1.5px', + }, + '--wpds-color-bg-surface-neutral': { + '.': 'var(--wpds-color-private-bg-surface2)', + }, + '--wpds-color-bg-surface-neutral-strong': { + '.': 'var(--wpds-color-private-bg-surface3)', + }, + '--wpds-color-bg-surface-neutral-weak': { + '.': 'var(--wpds-color-private-bg-surface1)', + }, + '--wpds-color-bg-surface-brand': { + '.': 'var(--wpds-color-private-primary-surface1)', + }, + '--wpds-color-bg-surface-success': { + '.': 'var(--wpds-color-private-success-surface4)', + }, + '--wpds-color-bg-surface-success-weak': { + '.': 'var(--wpds-color-private-success-surface2)', + }, + '--wpds-color-bg-surface-info': { + '.': 'var(--wpds-color-private-info-surface4)', + }, + '--wpds-color-bg-surface-info-weak': { + '.': 'var(--wpds-color-private-info-surface2)', + }, + '--wpds-color-bg-surface-warning': { + '.': 'var(--wpds-color-private-warning-surface4)', + }, + '--wpds-color-bg-surface-warning-weak': { + '.': 'var(--wpds-color-private-warning-surface2)', + }, + '--wpds-color-bg-surface-error': { + '.': 'var(--wpds-color-private-error-surface4)', + }, + '--wpds-color-bg-surface-error-weak': { + '.': 'var(--wpds-color-private-error-surface2)', + }, + '--wpds-color-bg-interactive-neutral': { + '.': '#00000000', + }, + '--wpds-color-bg-interactive-neutral-active': { + '.': 'var(--wpds-color-private-bg-surface4)', + }, + '--wpds-color-bg-interactive-neutral-disabled': { + '.': 'var(--wpds-color-private-bg-surface5)', + }, + '--wpds-color-bg-interactive-neutral-strong': { + '.': 'var(--wpds-color-private-bg-bg-fill-inverted1)', + }, + '--wpds-color-bg-interactive-neutral-strong-active': { + '.': 'var(--wpds-color-private-bg-bg-fill-inverted2)', + }, + '--wpds-color-bg-interactive-neutral-strong-disabled': { + '.': 'var(--wpds-color-private-bg-surface6)', + }, + '--wpds-color-bg-interactive-neutral-weak': { + '.': '#00000000', + }, + '--wpds-color-bg-interactive-neutral-weak-active': { + '.': 'var(--wpds-color-private-bg-surface4)', + }, + '--wpds-color-bg-interactive-neutral-weak-disabled': { + '.': 'var(--wpds-color-private-bg-surface5)', + }, + '--wpds-color-bg-interactive-brand': { + '.': '#00000000', + }, + '--wpds-color-bg-interactive-brand-active': { + '.': 'var(--wpds-color-private-primary-surface2)', + }, + '--wpds-color-bg-interactive-brand-disabled': { + '.': 'var(--wpds-color-private-bg-surface5)', + }, + '--wpds-color-bg-interactive-brand-strong': { + '.': 'var(--wpds-color-private-primary-bg-fill1)', + }, + '--wpds-color-bg-interactive-brand-strong-active': { + '.': 'var(--wpds-color-private-primary-bg-fill2)', + }, + '--wpds-color-bg-interactive-brand-strong-disabled': { + '.': 'var(--wpds-color-private-bg-surface6)', + }, + '--wpds-color-bg-interactive-brand-weak': { + '.': '#00000000', + }, + '--wpds-color-bg-interactive-brand-weak-active': { + '.': 'var(--wpds-color-private-primary-surface4)', + }, + '--wpds-color-bg-interactive-brand-weak-disabled': { + '.': 'var(--wpds-color-private-bg-surface5)', + }, + '--wpds-color-bg-track-neutral-weak': { + '.': 'var(--wpds-color-private-bg-stroke1)', + }, + '--wpds-color-bg-track-neutral': { + '.': 'var(--wpds-color-private-bg-stroke2)', + }, + '--wpds-color-bg-thumb-neutral-weak': { + '.': 'var(--wpds-color-private-bg-stroke3)', + }, + '--wpds-color-bg-thumb-neutral-weak-active': { + '.': 'var(--wpds-color-private-bg-stroke4)', + }, + '--wpds-color-bg-thumb-brand': { + '.': 'var(--wpds-color-private-primary-stroke3)', + }, + '--wpds-color-bg-thumb-brand-active': { + '.': 'var(--wpds-color-private-primary-stroke3)', + }, + '--wpds-color-bg-thumb-brand-disabled': { + '.': 'var(--wpds-color-private-bg-stroke2)', + }, + '--wpds-color-fg-content-neutral': { + '.': 'var(--wpds-color-private-bg-fg-surface4)', + }, + '--wpds-color-fg-content-neutral-weak': { + '.': 'var(--wpds-color-private-bg-fg-surface3)', + }, + '--wpds-color-fg-interactive-neutral': { + '.': 'var(--wpds-color-private-bg-fg-surface4)', + }, + '--wpds-color-fg-interactive-neutral-active': { + '.': 'var(--wpds-color-private-bg-fg-surface4)', + }, + '--wpds-color-fg-interactive-neutral-disabled': { + '.': 'var(--wpds-color-private-bg-fg-surface2)', + }, + '--wpds-color-fg-interactive-neutral-strong': { + '.': 'var(--wpds-color-private-bg-fg-fill-inverted)', + }, + '--wpds-color-fg-interactive-neutral-strong-active': { + '.': 'var(--wpds-color-private-bg-fg-fill-inverted)', + }, + '--wpds-color-fg-interactive-neutral-strong-disabled': { + '.': 'var(--wpds-color-private-bg-fg-surface3)', + }, + '--wpds-color-fg-interactive-neutral-weak': { + '.': 'var(--wpds-color-private-bg-fg-surface3)', + }, + '--wpds-color-fg-interactive-neutral-weak-disabled': { + '.': 'var(--wpds-color-private-bg-fg-surface2)', + }, + '--wpds-color-fg-interactive-brand': { + '.': 'var(--wpds-color-private-primary-fg-surface3)', + }, + '--wpds-color-fg-interactive-brand-active': { + '.': 'var(--wpds-color-private-primary-fg-surface3)', + }, + '--wpds-color-fg-interactive-brand-disabled': { + '.': 'var(--wpds-color-private-bg-fg-surface2)', + }, + '--wpds-color-fg-interactive-brand-strong': { + '.': 'var(--wpds-color-private-primary-fg-fill)', + }, + '--wpds-color-fg-interactive-brand-strong-active': { + '.': 'var(--wpds-color-private-primary-fg-fill)', + }, + '--wpds-color-fg-interactive-brand-strong-disabled': { + '.': 'var(--wpds-color-private-bg-fg-surface3)', + }, + '--wpds-color-stroke-surface-neutral': { + '.': 'var(--wpds-color-private-bg-stroke2)', + }, + '--wpds-color-stroke-surface-neutral-weak': { + '.': 'var(--wpds-color-private-bg-stroke1)', + }, + '--wpds-color-stroke-surface-neutral-strong': { + '.': 'var(--wpds-color-private-bg-stroke3)', + }, + '--wpds-color-stroke-surface-brand': { + '.': 'var(--wpds-color-private-primary-stroke1)', + }, + '--wpds-color-stroke-surface-brand-strong': { + '.': 'var(--wpds-color-private-primary-stroke3)', + }, + '--wpds-color-stroke-surface-success': { + '.': 'var(--wpds-color-private-success-stroke1)', + }, + '--wpds-color-stroke-surface-success-strong': { + '.': 'var(--wpds-color-private-success-stroke3)', + }, + '--wpds-color-stroke-surface-info': { + '.': 'var(--wpds-color-private-info-stroke1)', + }, + '--wpds-color-stroke-surface-info-strong': { + '.': 'var(--wpds-color-private-info-stroke3)', + }, + '--wpds-color-stroke-surface-warning': { + '.': 'var(--wpds-color-private-warning-stroke1)', + }, + '--wpds-color-stroke-surface-warning-strong': { + '.': 'var(--wpds-color-private-warning-stroke3)', + }, + '--wpds-color-stroke-surface-error': { + '.': 'var(--wpds-color-private-error-stroke1)', + }, + '--wpds-color-stroke-surface-error-strong': { + '.': 'var(--wpds-color-private-error-stroke3)', + }, + '--wpds-color-stroke-interactive-neutral': { + '.': 'var(--wpds-color-private-bg-stroke3)', + }, + '--wpds-color-stroke-interactive-neutral-active': { + '.': 'var(--wpds-color-private-bg-stroke4)', + }, + '--wpds-color-stroke-interactive-neutral-disabled': { + '.': 'var(--wpds-color-private-bg-stroke2)', + }, + '--wpds-color-stroke-interactive-neutral-strong': { + '.': 'var(--wpds-color-private-bg-stroke4)', + }, + '--wpds-color-stroke-interactive-brand': { + '.': 'var(--wpds-color-private-primary-stroke3)', + }, + '--wpds-color-stroke-interactive-brand-active': { + '.': 'var(--wpds-color-private-primary-stroke4)', + }, + '--wpds-color-stroke-interactive-brand-disabled': { + '.': 'var(--wpds-color-private-bg-stroke2)', + }, + '--wpds-color-stroke-interactive-error-strong': { + '.': 'var(--wpds-color-private-error-stroke3)', + }, + '--wpds-color-stroke-focus-brand': { + '.': 'var(--wpds-color-private-primary-stroke3)', + }, + '--wpds-elevation-x-small': { + '.': '0 1px 1px 0 #00000008, 0 1px 2px 0 #00000005, 0 3px 3px 0 #00000005, 0 4px 4px 0 #00000003', + }, + '--wpds-elevation-small': { + '.': '0 1px 2px 0 #0000000d, 0 2px 3px 0 #0000000a, 0 6px 6px 0 #00000008, 0 8px 8px 0 #00000005', + }, + '--wpds-elevation-medium': { + '.': '0 2px 3px 0 #0000000d, 0 4px 5px 0 #0000000a, 0 12px 12px 0 #00000008, 0 16px 16px 0 #00000005', + }, + '--wpds-elevation-large': { + '.': '0 5px 15px 0 #00000014, 0 15px 27px 0 #00000012, 0 30px 36px 0 #0000000a, 0 50px 43px 0 #00000005', + }, + '--wpds-spacing-05': { + '.': '4px', + }, + '--wpds-spacing-10': { + '.': '8px', + }, + '--wpds-spacing-15': { + '.': '12px', + }, + '--wpds-spacing-20': { + '.': '16px', + }, + '--wpds-spacing-30': { + '.': '24px', + }, + '--wpds-spacing-40': { + '.': '32px', + }, + '--wpds-spacing-50': { + '.': '40px', + }, + '--wpds-spacing-60': { + '.': '48px', + }, + '--wpds-spacing-70': { + '.': '56px', + }, + '--wpds-spacing-80': { + '.': '64px', + }, + '--wpds-font-family-heading': { + '.': '-apple-system, system-ui, "Segoe UI", "Roboto", "Oxygen-Sans", "Ubuntu", "Cantarell", "Helvetica Neue", sans-serif', + }, + '--wpds-font-family-body': { + '.': '-apple-system, system-ui, "Segoe UI", "Roboto", "Oxygen-Sans", "Ubuntu", "Cantarell", "Helvetica Neue", sans-serif', + }, + '--wpds-font-family-mono': { + '.': '"Menlo", "Consolas", monaco, monospace', + }, + '--wpds-font-size-x-small': { + '.': '11px', + }, + '--wpds-font-size-small': { + '.': '12px', + }, + '--wpds-font-size-medium': { + '.': '13px', + }, + '--wpds-font-size-large': { + '.': '15px', + }, + '--wpds-font-size-x-large': { + '.': '20px', + }, + '--wpds-font-size-2x-large': { + '.': '32px', + }, + '--wpds-font-line-height-x-small': { + '.': '16px', + }, + '--wpds-font-line-height-small': { + '.': '20px', + }, + '--wpds-font-line-height-medium': { + '.': '24px', + }, + '--wpds-font-line-height-large': { + '.': '28px', + }, + '--wpds-font-line-height-x-large': { + '.': '32px', + }, + '--wpds-font-line-height-2x-large': { + '.': '40px', + }, +} as Record< string, Record< string, string > >; diff --git a/packages/theme/src/private-apis.ts b/packages/theme/src/private-apis.ts new file mode 100644 index 00000000000000..5c5678317ab7d3 --- /dev/null +++ b/packages/theme/src/private-apis.ts @@ -0,0 +1,12 @@ +/** + * Internal dependencies + */ +import { lock } from './lock-unlock'; +import { ThemeProvider } from './theme-provider'; +import { useThemeProviderStyles } from './use-theme-provider-styles'; + +export const privateApis = {}; +lock( privateApis, { + ThemeProvider, + useThemeProviderStyles, +} ); diff --git a/packages/theme/src/style.module.css b/packages/theme/src/style.module.css new file mode 100644 index 00000000000000..9efb21f7a28d93 --- /dev/null +++ b/packages/theme/src/style.module.css @@ -0,0 +1,3 @@ +.root { + display: contents; +} diff --git a/packages/theme/src/theme-provider.tsx b/packages/theme/src/theme-provider.tsx new file mode 100644 index 00000000000000..b63a6c7561f185 --- /dev/null +++ b/packages/theme/src/theme-provider.tsx @@ -0,0 +1,87 @@ +/** + * External dependencies + */ +import type { CSSProperties } from 'react'; + +/** + * WordPress dependencies + */ +import { useMemo, useId } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { ThemeContext } from './context'; +import { useThemeProviderStyles } from './use-theme-provider-styles'; +import { type ThemeProviderProps } from './types'; +import styles from './style.module.css'; + +function cssObjectToText( values: CSSProperties ) { + return Object.entries( values ) + .map( ( [ key, value ] ) => `${ key }: ${ value };` ) + .join( '' ); +} + +function generateCSSSelector( { + instanceId, + isRoot, +}: { + instanceId: string; + isRoot: boolean; +} ) { + const rootSel = `[data-wpds-root-provider="true"]`; + const instanceIdSel = `[data-wpds-theme-provider-id="${ instanceId }"]`; + + const selectors = []; + + if ( isRoot ) { + selectors.push( + `:root:has(.${ styles.root }${ rootSel }${ instanceIdSel })` + ); + } + + selectors.push( `.${ styles.root }.${ styles.root }${ instanceIdSel }` ); + + return selectors.join( ',' ); +} + +export const ThemeProvider = ( { + children, + color = {}, + isRoot = false, +}: ThemeProviderProps ) => { + const instanceId = useId(); + + const { themeProviderStyles, resolvedSettings } = useThemeProviderStyles( { + color, + } ); + + const contextValue = useMemo( + () => ( { + resolvedSettings, + } ), + [ resolvedSettings ] + ); + + return ( + <> + { themeProviderStyles ? ( + + ) : null } +
+ + { children } + +
+ + ); +}; diff --git a/packages/theme/src/types.ts b/packages/theme/src/types.ts new file mode 100644 index 00000000000000..f5f20e07a64ef2 --- /dev/null +++ b/packages/theme/src/types.ts @@ -0,0 +1,44 @@ +/** + * External dependencies + */ +import { type ReactNode } from 'react'; + +export interface ThemeProviderSettings { + /** + * The set of color options to apply to the theme. + */ + color?: { + /** + * The primary seed color to use for the theme. + * + * By default, it inherits from parent `ThemeProvider`, + * and fallbacks to statically built CSS. + */ + primary?: string; + /** + * The background seed color to use for the theme. + * + * By default, it inherits from parent `ThemeProvider`, + * and fallbacks to statically built CSS. + */ + bg?: string; + }; +} + +export interface ThemeProviderProps extends ThemeProviderSettings { + /** + * The children to render. + */ + children?: ReactNode; + + /** + * When a ThemeProvider is the root provider, it will apply its theming + * settings also to the root document element (e.g. the html element). + * This is useful, for example, to make sure that the `html` element can + * consume the right background color, or that overlays rendered inside a + * portal can inherit the correct color scheme. + * + * @default false + */ + isRoot?: boolean; +} diff --git a/packages/theme/src/types/css-modules.d.ts b/packages/theme/src/types/css-modules.d.ts new file mode 100644 index 00000000000000..22fb5a58615ca8 --- /dev/null +++ b/packages/theme/src/types/css-modules.d.ts @@ -0,0 +1,4 @@ +declare module '*.module.css' { + const classes: { [ key: string ]: string }; + export default classes; +} diff --git a/packages/theme/src/use-theme-provider-styles.ts b/packages/theme/src/use-theme-provider-styles.ts new file mode 100644 index 00000000000000..feb5029721dabc --- /dev/null +++ b/packages/theme/src/use-theme-provider-styles.ts @@ -0,0 +1,247 @@ +/** + * External dependencies + */ +import type { CSSProperties } from 'react'; +import Color from 'colorjs.io'; + +/** + * WordPress dependencies + */ +import { useMemo, useContext } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { ThemeContext } from './context'; +import semanticVariables from './prebuilt/ts/design-tokens'; +import { + buildBgRamp, + buildAccentRamp, + DEFAULT_SEED_COLORS, + type RampResult, +} from './color-ramps'; +import type { ThemeProviderProps } from './types'; + +type Entry = [ string, string ]; + +const legacyWpComponentsOverridesCSS: Entry[] = [ + [ '--wp-components-color-accent', 'var(--wp-admin-theme-color)' ], + [ + '--wp-components-color-accent-darker-10', + 'var(--wp-admin-theme-color-darker-10)', + ], + [ + '--wp-components-color-accent-darker-20', + 'var(--wp-admin-theme-color-darker-20)', + ], + [ + '--wp-components-color-accent-inverted', + 'var(--wpds-color-fg-interactive-brand-strong)', + ], + [ + '--wp-components-color-background', + 'var(--wpds-color-bg-surface-neutral-strong)', + ], + [ + '--wp-components-color-foreground', + 'var(--wpds-color-fg-content-neutral)', + ], + [ + '--wp-components-color-foreground-inverted', + 'var(--wpds-color-bg-surface-neutral)', + ], + [ + '--wp-components-color-gray-100', + 'var(--wpds-color-bg-surface-neutral)', + ], + [ + '--wp-components-color-gray-200', + 'var(--wpds-color-stroke-surface-neutral)', + ], + [ + '--wp-components-color-gray-300', + 'var(--wpds-color-stroke-surface-neutral)', + ], + [ + '--wp-components-color-gray-400', + 'var(--wpds-color-stroke-interactive-neutral)', + ], + [ + '--wp-components-color-gray-600', + 'var(--wpds-color-stroke-interactive-neutral)', + ], + [ + '--wp-components-color-gray-700', + 'var(--wpds-color-fg-content-neutral-weak)', + ], + [ + '--wp-components-color-gray-800', + 'var(--wpds-color-fg-content-neutral)', + ], +]; + +function customRgbFormat( color: Color ) { + const rgb = color.to( 'srgb' ); + return [ rgb.r, rgb.g, rgb.b ] + .map( ( n ) => Math.round( n * 255 ) ) + .join( ', ' ); +} + +function legacyWpAdminThemeOverridesCSS( accent: string ): Entry[] { + const parsedAccent = new Color( accent ).to( 'hsl' ); + + const hsl = parsedAccent.coords; + const darker10 = new Color( 'hsl', [ + hsl[ 0 ], // h + hsl[ 1 ], // s + Math.max( 0, Math.min( 100, hsl[ 2 ] - 5 ) ), // l (reduced by 5%) + ] ).to( 'srgb' ); + const darker20 = new Color( 'hsl', [ + hsl[ 0 ], // h + hsl[ 1 ], // s + Math.max( 0, Math.min( 100, hsl[ 2 ] - 10 ) ), // l (reduced by 10%) + ] ).to( 'srgb' ); + + return [ + [ + '--wp-admin-theme-color', + parsedAccent.to( 'srgb' ).toString( { format: 'hex' } ), + ], + [ '--wp-admin-theme-color--rgb', customRgbFormat( parsedAccent ) ], + [ + '--wp-admin-theme-color-darker-10', + darker10.toString( { format: 'hex' } ), + ], + [ + '--wp-admin-theme-color-darker-10--rgb', + customRgbFormat( darker10 ), + ], + [ + '--wp-admin-theme-color-darker-20', + darker20.toString( { format: 'hex' } ), + ], + [ + '--wp-admin-theme-color-darker-20--rgb', + customRgbFormat( darker20 ), + ], + ]; +} + +function semanticTokensCSS( + filterFn: ( entry: [ string, Record< string, string > ] ) => boolean = () => + true +): Entry[] { + return Object.entries( semanticVariables ) + .filter( filterFn ) + .map( ( [ variableName, modesAndValues ] ) => [ + variableName, + modesAndValues[ '.' ], + ] ); +} + +const toKebabCase = ( str: string ) => + str.replace( + /[A-Z]+(?![a-z])|[A-Z]/g, + ( $, ofs ) => ( ofs ? '-' : '' ) + $.toLowerCase() + ); + +function colorRampCSS( ramp: RampResult, prefix: string ): Entry[] { + return [ ...Object.entries( ramp.ramp ) ].map( + ( [ tokenName, tokenValue ] ) => [ + `${ prefix }${ toKebabCase( tokenName ) }`, + tokenValue.color, + ] + ); +} + +function generateStyles( { + primary, + computedColorRamps, +}: { + primary: string; + computedColorRamps: Map< string, RampResult >; +} ): CSSProperties { + return Object.fromEntries( + [ + // Primitive tokens + Array.from( computedColorRamps ) + .map( ( [ rampName, computedColorRamp ] ) => [ + colorRampCSS( + computedColorRamp, + `--wpds-color-private-${ rampName }-` + ), + ] ) + .flat( 2 ), + // Semantic color tokens (other semantic tokens for now are static) + semanticTokensCSS( ( [ key ] ) => /color/.test( key ) ), + // Legacy overrides + legacyWpAdminThemeOverridesCSS( primary ), + legacyWpComponentsOverridesCSS, + ].flat() + ); +} + +export function useThemeProviderStyles( { + color = {}, +}: { + color?: ThemeProviderProps[ 'color' ]; +} = {} ) { + const { resolvedSettings: inheritedSettings } = useContext( ThemeContext ); + + // Compute settings: + // - used provided prop value; + // - otherwise, use inherited value from parent instance; + // - otherwise, use fallback value (where applicable). + const primary = + color.primary ?? + inheritedSettings.color?.primary ?? + DEFAULT_SEED_COLORS.bg; + const bg = + color.bg ?? inheritedSettings.color?.bg ?? DEFAULT_SEED_COLORS.primary; + + const resolvedSettings = useMemo( + () => ( { + color: { + primary, + bg, + }, + } ), + [ primary, bg ] + ); + + const themeProviderStyles = useMemo( () => { + // Determine which seeds are needed for generating ramps. + const seeds = { + ...DEFAULT_SEED_COLORS, + bg, + primary, + }; + + // Generate ramps. + const computedColorRamps = new Map< string, RampResult >(); + const bgRamp = buildBgRamp( { seed: seeds.bg } ); + Object.entries( seeds ).forEach( ( [ rampName, seed ] ) => { + if ( rampName === 'bg' ) { + computedColorRamps.set( rampName, bgRamp ); + } else { + computedColorRamps.set( + rampName, + buildAccentRamp( { + seed, + bgRamp, + } ) + ); + } + } ); + + return generateStyles( { + primary: seeds.primary, + computedColorRamps, + } ); + }, [ primary, bg ] ); + + return { + resolvedSettings, + themeProviderStyles, + }; +} diff --git a/packages/theme/terrazzo.config.ts b/packages/theme/terrazzo.config.ts new file mode 100644 index 00000000000000..f2b6759ea2fcb9 --- /dev/null +++ b/packages/theme/terrazzo.config.ts @@ -0,0 +1,102 @@ +/** + * External dependencies + */ +import { defineConfig } from '@terrazzo/cli'; +import pluginCSS from '@terrazzo/plugin-css'; +import { makeCSSVar } from '@terrazzo/token-tools/css'; +/** + * Internal dependencies + */ +import pluginFigmaDsTokenManager from './bin/terrazzo-plugin-figma-ds-token-manager/index'; +import pluginKnownWpdsCssVariables from './bin/terrazzo-plugin-known-wpds-css-variables/index'; +import pluginDsTokenDocs from './bin/terrazzo-plugin-ds-tokens-docs/index'; + +export default defineConfig( { + tokens: [ + './tokens/border.json', + './tokens/color.json', + './tokens/elevation.json', + './tokens/spacing.json', + './tokens/typography.json', + ], + outDir: './src/prebuilt', + + plugins: [ + pluginCSS( { + filename: 'css/design-tokens.css', + variableName: ( token ) => + makeCSSVar( + `wpds.${ token.id + .replace( /normal/g, '' ) + .replace( /resting/g, '' ) + .replace( /primitive/g, 'private' ) + .replace( /semantic/g, '' ) }` + ), + baseSelector: ':root', + modeSelectors: [ + { + mode: 'high-dpi', + selectors: [ + '@media ( -webkit-min-device-pixel-ratio: 2 ), ( min-resolution: 192dpi )', + ], + }, + ], + legacyHex: true, + } ), + pluginFigmaDsTokenManager( { + filename: 'json/figma.json', + } ), + pluginKnownWpdsCssVariables( { + exports: [ + { filename: 'js/design-tokens.js', modes: false }, + { filename: 'ts/design-tokens.ts', modes: true }, + ], + } ), + pluginDsTokenDocs( { + filename: '../../docs/ds-tokens.md', + } ), + ], + + // Linter rules current error when multiple entry files are used + // See https://github.com/terrazzoapp/terrazzo/issues/505 + // lint: { + // rules: { + // 'a11y/min-contrast': [ + // 'error', + // { + // level: 'AA', + // pairs: [ + // // Standard BG / FG pairs + // ...[ + // 'color.primitive.neutral.1', + // 'color.primitive.neutral.2', + // 'color.primitive.neutral.3', + // 'color.primitive.primary.1', + // 'color.primitive.primary.2', + // 'color.primitive.primary.3', + // ].flatMap( ( bgToken ) => + // [ + // 'color.primitive.neutral.11', + // 'color.primitive.neutral.12', + // 'color.primitive.primary.11', + // 'color.primitive.primary.12', + // ].map( ( fgToken ) => ( { + // foreground: fgToken, + // background: bgToken, + // } ) ) + // ), + // // Action pairs (ie. using step 9 as background) + // { + // foreground: 'color.primitive.primary.contrast', + // background: 'color.primitive.primary.9', + // }, + // { + // foreground: 'color.primitive.primary.1', + // background: 'color.primitive.primary.9', + // }, + // ], + // }, + // ], + // }, + // }, +} ); diff --git a/packages/theme/tokens/border.json b/packages/theme/tokens/border.json new file mode 100644 index 00000000000000..cfe210f08ae204 --- /dev/null +++ b/packages/theme/tokens/border.json @@ -0,0 +1,34 @@ +{ + "border": { + "$type": "dimension", + "radius": { + "x-small": { + "$value": { "value": 1, "unit": "px" }, + "$description": "Extra small radius" + }, + "small": { + "$value": { "value": 2, "unit": "px" }, + "$description": "Small radius" + }, + "medium": { + "$value": { "value": 4, "unit": "px" }, + "$description": "Medium radius" + }, + "large": { + "$value": { "value": 8, "unit": "px" }, + "$description": "Large radius" + } + }, + "width": { + "focus": { + "$value": { "value": 2, "unit": "px" }, + "$description": "Border width for focus ring", + "$extensions": { + "mode": { + "high-dpi": { "value": 1.5, "unit": "px" } + } + } + } + } + } +} diff --git a/packages/theme/tokens/color.json b/packages/theme/tokens/color.json new file mode 100644 index 00000000000000..97a2072a18aad2 --- /dev/null +++ b/packages/theme/tokens/color.json @@ -0,0 +1,877 @@ +{ + "color": { + "$type": "color", + "primitive": { + "primary": { + "bgFill1": { + "$value": "#3858e9" + }, + "fgFill": { + "$value": "#eff0f2" + }, + "bgFill2": { + "$value": "#2c47d7" + }, + "surface2": { + "$value": "#f6f8fc" + }, + "surface6": { + "$value": "#c4d0ee" + }, + "surface5": { + "$value": "#d7dff3" + }, + "surface4": { + "$value": "#e5eaf7" + }, + "surface3": { + "$value": "#fff" + }, + "fgSurface4": { + "$value": "#080071" + }, + "fgSurface3": { + "$value": "#3858e9" + }, + "fgSurface2": { + "$value": "#577fff" + }, + "fgSurface1": { + "$value": "#81a6ff" + }, + "stroke3": { + "$value": "#3858e9" + }, + "stroke4": { + "$value": "#2236c7" + }, + "stroke2": { + "$value": "#7085c0" + }, + "stroke1": { + "$value": "#93a4d0" + }, + "bgFillDark": { + "$value": "#1b1e26" + }, + "fgFillDark": { + "$value": "#eff0f2" + }, + "bgFillInverted2": { + "$value": "#1b1e26" + }, + "bgFillInverted1": { + "$value": "#1401a5" + }, + "fgFillInverted": { + "$value": "#eff0f2" + }, + "surface1": { + "$value": "#ecf0f9" + } + }, + "info": { + "bgFill1": { + "$value": "#0090ff" + }, + "fgFill": { + "$value": "#1b1e23" + }, + "bgFill2": { + "$value": "#007eec" + }, + "surface2": { + "$value": "#f5f9fd" + }, + "surface6": { + "$value": "#b9d3f0" + }, + "surface5": { + "$value": "#d0e1f5" + }, + "surface4": { + "$value": "#e0ecf8" + }, + "surface3": { + "$value": "#fff" + }, + "fgSurface4": { + "$value": "#001758" + }, + "fgSurface3": { + "$value": "#006cd8" + }, + "fgSurface2": { + "$value": "#008bf9" + }, + "fgSurface1": { + "$value": "#4dafff" + }, + "stroke3": { + "$value": "#006cd8" + }, + "stroke4": { + "$value": "#004bb5" + }, + "stroke2": { + "$value": "#5c90c9" + }, + "stroke1": { + "$value": "#8baed6" + }, + "bgFillDark": { + "$value": "#1b1e23" + }, + "fgFillDark": { + "$value": "#eff0f2" + }, + "bgFillInverted2": { + "$value": "#1b1e23" + }, + "bgFillInverted1": { + "$value": "#00297b" + }, + "fgFillInverted": { + "$value": "#eff0f2" + }, + "surface1": { + "$value": "#e9f1fa" + } + }, + "success": { + "bgFill1": { + "$value": "#4ab866" + }, + "fgFill": { + "$value": "#1b1f1c" + }, + "bgFill2": { + "$value": "#34a554" + }, + "surface2": { + "$value": "#f0fcf2" + }, + "surface6": { + "$value": "#7be792" + }, + "surface5": { + "$value": "#abf0b7" + }, + "surface4": { + "$value": "#cdf5d2" + }, + "surface3": { + "$value": "#fff" + }, + "fgSurface4": { + "$value": "#002b00" + }, + "fgSurface3": { + "$value": "#008030" + }, + "fgSurface2": { + "$value": "#2a9e4d" + }, + "fgSurface1": { + "$value": "#52bf6d" + }, + "stroke3": { + "$value": "#008030" + }, + "stroke4": { + "$value": "#006013" + }, + "stroke2": { + "$value": "#629a6c" + }, + "stroke1": { + "$value": "#78bb84" + }, + "bgFillDark": { + "$value": "#1b1f1c" + }, + "fgFillDark": { + "$value": "#edf2ed" + }, + "bgFillInverted2": { + "$value": "#1b1f1c" + }, + "bgFillInverted1": { + "$value": "#003b00" + }, + "fgFillInverted": { + "$value": "#edf2ed" + }, + "surface1": { + "$value": "#ddf8e1" + } + }, + "warning": { + "bgFill1": { + "$value": "#f0b849" + }, + "fgFill": { + "$value": "#1f1e1b" + }, + "bgFill2": { + "$value": "#dba32f" + }, + "surface2": { + "$value": "#fdf7ee" + }, + "surface6": { + "$value": "#f0cb89" + }, + "surface5": { + "$value": "#f5dcb2" + }, + "surface4": { + "$value": "#f8e8cd" + }, + "surface3": { + "$value": "#fff" + }, + "fgSurface4": { + "$value": "#2f1800" + }, + "fgSurface3": { + "$value": "#966200" + }, + "fgSurface2": { + "$value": "#b58000" + }, + "fgSurface1": { + "$value": "#d7a02b" + }, + "stroke3": { + "$value": "#966200" + }, + "stroke4": { + "$value": "#724700" + }, + "stroke2": { + "$value": "#a18a61" + }, + "stroke1": { + "$value": "#c3a777" + }, + "bgFillDark": { + "$value": "#1f1e1b" + }, + "fgFillDark": { + "$value": "#f3f0e9" + }, + "bgFillInverted2": { + "$value": "#1f1e1b" + }, + "bgFillInverted1": { + "$value": "#472900" + }, + "fgFillInverted": { + "$value": "#f3f0e9" + }, + "surface1": { + "$value": "#faefdc" + } + }, + "error": { + "bgFill1": { + "$value": "#cc1818" + }, + "fgFill": { + "$value": "#f2efef" + }, + "bgFill2": { + "$value": "#b90000" + }, + "surface2": { + "$value": "#fdf6f5" + }, + "surface6": { + "$value": "#f5c5bd" + }, + "surface5": { + "$value": "#f8d8d3" + }, + "surface4": { + "$value": "#fae5e2" + }, + "surface3": { + "$value": "#fff" + }, + "fgSurface4": { + "$value": "#4a0000" + }, + "fgSurface3": { + "$value": "#cc1818" + }, + "fgSurface2": { + "$value": "#f64b40" + }, + "fgSurface1": { + "$value": "#ff8070" + }, + "stroke3": { + "$value": "#cc1818" + }, + "stroke4": { + "$value": "#a30000" + }, + "stroke2": { + "$value": "#cd695d" + }, + "stroke1": { + "$value": "#dc9085" + }, + "bgFillDark": { + "$value": "#231c1b" + }, + "fgFillDark": { + "$value": "#f2efef" + }, + "bgFillInverted2": { + "$value": "#231c1b" + }, + "bgFillInverted1": { + "$value": "#6d0000" + }, + "fgFillInverted": { + "$value": "#f2efef" + }, + "surface1": { + "$value": "#fcedea" + } + }, + "bg": { + "surface2": { + "$value": "#f8f8f8" + }, + "bgFill1": { + "$value": "#555" + }, + "fgFill": { + "$value": "#f0f0f0" + }, + "bgFill2": { + "$value": "#474747" + }, + "surface6": { + "$value": "#d0d0d0" + }, + "surface5": { + "$value": "#dfdfdf" + }, + "surface4": { + "$value": "#eaeaea" + }, + "surface3": { + "$value": "#fff" + }, + "fgSurface4": { + "$value": "#1e1e1e" + }, + "fgSurface3": { + "$value": "#6c6c6c" + }, + "fgSurface2": { + "$value": "#898989" + }, + "fgSurface1": { + "$value": "#a9a9a9" + }, + "stroke3": { + "$value": "#898989" + }, + "stroke4": { + "$value": "#6a6a6a" + }, + "stroke2": { + "$value": "#aeaeae" + }, + "stroke1": { + "$value": "#d0d0d0" + }, + "bgFillDark": { + "$value": "#1e1e1e" + }, + "fgFillDark": { + "$value": "#f0f0f0" + }, + "bgFillInverted2": { + "$value": "#1e1e1e" + }, + "bgFillInverted1": { + "$value": "#2f2f2f" + }, + "fgFillInverted": { + "$value": "#f0f0f0" + }, + "surface1": { + "$value": "#efefef" + } + } + }, + "semantic": { + "bg-surface": { + "neutral": { + "normal": { + "resting": { + "$value": "{color.primitive.bg.surface2}", + "$description": "Background color for surfaces with normal emphasis." + } + }, + "strong": { + "resting": { + "$value": "{color.primitive.bg.surface3}", + "$description": "Background color for surfaces with strong emphasis." + } + }, + "weak": { + "resting": { + "$value": "{color.primitive.bg.surface1}", + "$description": "Background color for surfaces with weak emphasis." + } + } + }, + "brand": { + "normal": { + "resting": { + "$value": "{color.primitive.primary.surface1}", + "$description": "Background color for surfaces with brand tone and normal emphasis." + } + } + }, + "success": { + "normal": { + "resting": { + "$value": "{color.primitive.success.surface4}", + "$description": "Background color for surfaces with success tone and normal emphasis." + } + }, + "weak": { + "resting": { + "$value": "{color.primitive.success.surface2}", + "$description": "Background color for surfaces with success tone and weak emphasis." + } + } + }, + "info": { + "normal": { + "resting": { + "$value": "{color.primitive.info.surface4}", + "$description": "Background color for surfaces with info tone and normal emphasis." + } + }, + "weak": { + "resting": { + "$value": "{color.primitive.info.surface2}", + "$description": "Background color for surfaces with info tone and weak emphasis." + } + } + }, + "warning": { + "normal": { + "resting": { + "$value": "{color.primitive.warning.surface4}", + "$description": "Background color for surfaces with warning tone and normal emphasis." + } + }, + "weak": { + "resting": { + "$value": "{color.primitive.warning.surface2}", + "$description": "Background color for surfaces with warning tone and weak emphasis." + } + } + }, + "error": { + "normal": { + "resting": { + "$value": "{color.primitive.error.surface4}", + "$description": "Background color for surfaces with error tone and normal emphasis." + } + }, + "weak": { + "resting": { + "$value": "{color.primitive.error.surface2}", + "$description": "Background color for surfaces with error tone and weak emphasis." + } + } + } + }, + "bg-interactive": { + "neutral": { + "normal": { + "resting": { + "$value": "transparent", + "$description": "Background color for interactive elements with neutral tone and normal emphasis." + }, + "active": { + "$value": "{color.primitive.bg.surface4}", + "$description": "Background color for interactive elements with neutral tone and normal emphasis that are hovered, focused, or active." + }, + "disabled": { + "$value": "{color.primitive.bg.surface5}", + "$description": "Background color for interactive elements with neutral tone and normal emphasis, in their disabled state." + } + }, + "strong": { + "resting": { + "$value": "{color.primitive.bg.bgFillInverted1}", + "$description": "Background color for interactive elements with neutral tone and strong emphasis." + }, + "active": { + "$value": "{color.primitive.bg.bgFillInverted2}", + "$description": "Background color for interactive elements with neutral tone and strong emphasis that are hovered, focused, or active." + }, + "disabled": { + "$value": "{color.primitive.bg.surface6}", + "$description": "Background color for interactive elements with neutral tone and strong emphasis, in their disabled state." + } + }, + "weak": { + "resting": { + "$value": "transparent", + "$description": "Background color for interactive elements with neutral tone and weak emphasis." + }, + "active": { + "$value": "{color.primitive.bg.surface4}", + "$description": "Background color for interactive elements with neutral tone and weak emphasis that are hovered, focused, or active." + }, + "disabled": { + "$value": "{color.primitive.bg.surface5}", + "$description": "Background color for interactive elements with neutral tone and weak emphasis, in their disabled state." + } + } + }, + "brand": { + "normal": { + "resting": { + "$value": "transparent", + "$description": "Background color for interactive elements with brand tone and normal emphasis." + }, + "active": { + "$value": "{color.primitive.primary.surface2}", + "$description": "Background color for interactive elements with brand tone and normal emphasis that are hovered, focused, or active." + }, + "disabled": { + "$value": "{color.primitive.bg.surface5}", + "$description": "Background color for interactive elements with brand tone and normal emphasis, in their disabled state." + } + }, + "strong": { + "resting": { + "$value": "{color.primitive.primary.bgFill1}", + "$description": "Background color for interactive elements with brand tone and strong emphasis." + }, + "active": { + "$value": "{color.primitive.primary.bgFill2}", + "$description": "Background color for interactive elements with brand tone and strong emphasis that are hovered, focused, or active." + }, + "disabled": { + "$value": "{color.primitive.bg.surface6}", + "$description": "Background color for interactive elements with brand tone and strong emphasis, in their disabled state." + } + }, + "weak": { + "resting": { + "$value": "transparent", + "$description": "Background color for interactive elements with brand tone and weak emphasis." + }, + "active": { + "$value": "{color.primitive.primary.surface4}", + "$description": "Background color for interactive elements with brand tone and weak emphasis that are hovered, focused, or active." + }, + "disabled": { + "$value": "{color.primitive.bg.surface5}", + "$description": "Background color for interactive elements with brand tone and weak emphasis, in their disabled state." + } + } + } + }, + "bg-track": { + "neutral": { + "weak": { + "resting": { + "$value": "{color.primitive.bg.stroke1}", + "$description": "Background color for tracks with a neutral tone and weak emphasis (eg. scrollbar track)." + } + }, + "normal": { + "resting": { + "$value": "{color.primitive.bg.stroke2}", + "$description": "Background color for tracks with a neutral tone and normal emphasis (eg. slider or progressbar track)." + } + } + } + }, + "bg-thumb": { + "neutral": { + "weak": { + "resting": { + "$value": "{color.primitive.bg.stroke3}", + "$description": "Background color for thumbs with a neutral tone and weak emphasis (eg. scrollbar thumb)." + }, + "active": { + "$value": "{color.primitive.bg.stroke4}", + "$description": "Background color for thumbs with a neutral tone and weak emphasis (eg. scrollbar thumb) that are hovered, focused, or active." + } + } + }, + "brand": { + "normal": { + "resting": { + "$value": "{color.primitive.primary.stroke3}", + "$description": "Background color for thumbs with a brand tone and normal emphasis (eg. slider thumb and filled track)." + }, + "active": { + "$value": "{color.primitive.primary.stroke3}", + "$description": "Background color for thumbs with a brand tone and normal emphasis (eg. slider thumb and filled track) that are hovered, focused, or active." + }, + "disabled": { + "$value": "{color.primitive.bg.stroke2}", + "$description": "Background color for thumbs with a brand tone and normal emphasis (eg. slider thumb and filled track), in their disabled state." + } + } + } + }, + "fg-content": { + "neutral": { + "normal": { + "resting": { + "$value": "{color.primitive.bg.fgSurface4}", + "$description": "Foreground color for content like text with normal emphasis." + } + }, + "weak": { + "resting": { + "$value": "{color.primitive.bg.fgSurface3}", + "$description": "Foreground color for content like text with weak emphasis." + } + } + } + }, + "fg-interactive": { + "neutral": { + "normal": { + "resting": { + "$value": "{color.primitive.bg.fgSurface4}", + "$description": "Foreground color for interactive elements with neutral tone and normal emphasis." + }, + "active": { + "$value": "{color.primitive.bg.fgSurface4}", + "$description": "Foreground color for interactive elements with neutral tone and normal emphasis that are hovered, focused, or active." + }, + "disabled": { + "$value": "{color.primitive.bg.fgSurface2}", + "$description": "Foreground color for interactive elements with neutral tone and normal emphasis, in their disabled state." + } + }, + "strong": { + "resting": { + "$value": "{color.primitive.bg.fgFillInverted}", + "$description": "Foreground color for interactive elements with neutral tone and strong emphasis." + }, + "active": { + "$value": "{color.primitive.bg.fgFillInverted}", + "$description": "Foreground color for interactive elements with neutral tone and strong emphasis that are hovered, focused, or active." + }, + "disabled": { + "$value": "{color.primitive.bg.fgSurface3}", + "$description": "Foreground color for interactive elements with neutral tone and strong emphasis, in their disabled state." + } + }, + "weak": { + "resting": { + "$value": "{color.primitive.bg.fgSurface3}", + "$description": "Foreground color for interactive elements with neutral tone and weak emphasis." + }, + "disabled": { + "$value": "{color.primitive.bg.fgSurface2}", + "$description": "Foreground color for interactive elements with neutral tone and weak emphasis, in their disabled state." + } + } + }, + "brand": { + "normal": { + "resting": { + "$value": "{color.primitive.primary.fgSurface3}", + "$description": "Foreground color for interactive elements with brand tone and normal emphasis." + }, + "active": { + "$value": "{color.primitive.primary.fgSurface3}", + "$description": "Foreground color for interactive elements with brand tone and normal emphasis that are hovered, focused, or active." + }, + "disabled": { + "$value": "{color.primitive.bg.fgSurface2}", + "$description": "Foreground color for interactive elements with brand tone and normal emphasis, in their disabled state." + } + }, + "strong": { + "resting": { + "$value": "{color.primitive.primary.fgFill}", + "$description": "Foreground color for interactive elements with brand tone and strong emphasis." + }, + "active": { + "$value": "{color.primitive.primary.fgFill}", + "$description": "Foreground color for interactive elements with brand tone and strong emphasis that are hovered, focused, or active." + }, + "disabled": { + "$value": "{color.primitive.bg.fgSurface3}", + "$description": "Foreground color for interactive elements with brand tone and strong emphasis, in their disabled state." + } + } + } + }, + "stroke-surface": { + "neutral": { + "normal": { + "resting": { + "$value": "{color.primitive.bg.stroke2}", + "$description": "Decorative stroke color used to define neutrally-toned surface boundaries with normal emphasis." + } + }, + "weak": { + "resting": { + "$value": "{color.primitive.bg.stroke1}", + "$description": "Decorative stroke color used to define neutrally-toned surface boundaries with weak emphasis." + } + }, + "strong": { + "resting": { + "$value": "{color.primitive.bg.stroke3}", + "$description": "Decorative stroke color used to define neutrally-toned surface boundaries with strong emphasis." + } + } + }, + "brand": { + "normal": { + "resting": { + "$value": "{color.primitive.primary.stroke1}", + "$description": "Decorative stroke color used to define brand-toned surface boundaries with normal emphasis." + } + }, + "strong": { + "resting": { + "$value": "{color.primitive.primary.stroke3}", + "$description": "Decorative stroke color used to define neutrally-toned surface boundaries with strong emphasis." + } + } + }, + "success": { + "normal": { + "resting": { + "$value": "{color.primitive.success.stroke1}", + "$description": "Decorative stroke color used to define success-toned surface boundaries with normal emphasis." + } + }, + "strong": { + "resting": { + "$value": "{color.primitive.success.stroke3}", + "$description": "Decorative stroke color used to define success-toned surface boundaries with strong emphasis." + } + } + }, + "info": { + "normal": { + "resting": { + "$value": "{color.primitive.info.stroke1}", + "$description": "Decorative stroke color used to define info-toned surface boundaries with normal emphasis." + } + }, + "strong": { + "resting": { + "$value": "{color.primitive.info.stroke3}", + "$description": "Decorative stroke color used to define info-toned surface boundaries with strong emphasis." + } + } + }, + "warning": { + "normal": { + "resting": { + "$value": "{color.primitive.warning.stroke1}", + "$description": "Decorative stroke color used to define warning-toned surface boundaries with normal emphasis." + } + }, + "strong": { + "resting": { + "$value": "{color.primitive.warning.stroke3}", + "$description": "Decorative stroke color used to define warning-toned surface boundaries with strong emphasis." + } + } + }, + "error": { + "normal": { + "resting": { + "$value": "{color.primitive.error.stroke1}", + "$description": "Decorative stroke color used to define error-toned surface boundaries with normal emphasis." + } + }, + "strong": { + "resting": { + "$value": "{color.primitive.error.stroke3}", + "$description": "Decorative stroke color used to define error-toned surface boundaries with strong emphasis." + } + } + } + }, + "stroke-interactive": { + "neutral": { + "normal": { + "resting": { + "$value": "{color.primitive.bg.stroke3}", + "$description": "Accessible stroke color used for interactive neutrally-toned elements with normal emphasis." + }, + "active": { + "$value": "{color.primitive.bg.stroke4}", + "$description": "Accessible stroke color used for interactive neutrally-toned elements with normal emphasis that are hovered, focused, or active." + }, + "disabled": { + "$value": "{color.primitive.bg.stroke2}", + "$description": "Accessible stroke color used for interactive neutrally-toned elements with normal emphasis, in their disabled state." + } + }, + "strong": { + "resting": { + "$value": "{color.primitive.bg.stroke4}", + "$description": "Accessible stroke color used for interactive neutrally-toned elements with strong emphasis." + } + } + }, + "brand": { + "normal": { + "resting": { + "$value": "{color.primitive.primary.stroke3}", + "$description": "Accessible stroke color used for interactive brand-toned elements with normal emphasis." + }, + "active": { + "$value": "{color.primitive.primary.stroke4}", + "$description": "Accessible stroke color used for interactive brand-toned elements with normal emphasis that are hovered, focused, or active." + }, + "disabled": { + "$value": "{color.primitive.bg.stroke2}", + "$description": "Accessible stroke color used for interactive brand-toned elements with normal emphasis, in their disabled state." + } + } + }, + "error": { + "strong": { + "resting": { + "$value": "{color.primitive.error.stroke3}", + "$description": "Accessible stroke color used for interactive error-toned elements with strong emphasis." + } + } + } + }, + "stroke-focus": { + "brand": { + "normal": { + "resting": { + "$value": "{color.primitive.primary.stroke3}", + "$description": "Accessible stroke color applied to focus rings." + } + } + } + } + } + } +} diff --git a/packages/theme/tokens/elevation.json b/packages/theme/tokens/elevation.json new file mode 100644 index 00000000000000..2711ee22e8fe14 --- /dev/null +++ b/packages/theme/tokens/elevation.json @@ -0,0 +1,201 @@ +{ + "elevation": { + "$type": "shadow", + "x-small": { + "$value": [ + { + "color": { + "colorSpace": "srgb", + "components": [ 0, 0, 0 ], + "alpha": 0.03 + }, + "offsetX": { "value": 0, "unit": "px" }, + "offsetY": { "value": 1, "unit": "px" }, + "blur": { "value": 1, "unit": "px" }, + "spread": { "value": 0, "unit": "px" } + }, + { + "color": { + "colorSpace": "srgb", + "components": [ 0, 0, 0 ], + "alpha": 0.02 + }, + "offsetX": { "value": 0, "unit": "px" }, + "offsetY": { "value": 1, "unit": "px" }, + "blur": { "value": 2, "unit": "px" }, + "spread": { "value": 0, "unit": "px" } + }, + { + "color": { + "colorSpace": "srgb", + "components": [ 0, 0, 0 ], + "alpha": 0.02 + }, + "offsetX": { "value": 0, "unit": "px" }, + "offsetY": { "value": 3, "unit": "px" }, + "blur": { "value": 3, "unit": "px" }, + "spread": { "value": 0, "unit": "px" } + }, + { + "color": { + "colorSpace": "srgb", + "components": [ 0, 0, 0 ], + "alpha": 0.01 + }, + "offsetX": { "value": 0, "unit": "px" }, + "offsetY": { "value": 4, "unit": "px" }, + "blur": { "value": 4, "unit": "px" }, + "spread": { "value": 0, "unit": "px" } + } + ], + "$description": "For sections and containers that group related content and controls, which may overlap other content. Example: Preview Frame." + }, + "small": { + "$value": [ + { + "color": { + "colorSpace": "srgb", + "components": [ 0, 0, 0 ], + "alpha": 0.05 + }, + "offsetX": { "value": 0, "unit": "px" }, + "offsetY": { "value": 1, "unit": "px" }, + "blur": { "value": 2, "unit": "px" }, + "spread": { "value": 0, "unit": "px" } + }, + { + "color": { + "colorSpace": "srgb", + "components": [ 0, 0, 0 ], + "alpha": 0.04 + }, + "offsetX": { "value": 0, "unit": "px" }, + "offsetY": { "value": 2, "unit": "px" }, + "blur": { "value": 3, "unit": "px" }, + "spread": { "value": 0, "unit": "px" } + }, + { + "color": { + "colorSpace": "srgb", + "components": [ 0, 0, 0 ], + "alpha": 0.03 + }, + "offsetX": { "value": 0, "unit": "px" }, + "offsetY": { "value": 6, "unit": "px" }, + "blur": { "value": 6, "unit": "px" }, + "spread": { "value": 0, "unit": "px" } + }, + { + "color": { + "colorSpace": "srgb", + "components": [ 0, 0, 0 ], + "alpha": 0.02 + }, + "offsetX": { "value": 0, "unit": "px" }, + "offsetY": { "value": 8, "unit": "px" }, + "blur": { "value": 8, "unit": "px" }, + "spread": { "value": 0, "unit": "px" } + } + ], + "$description": "For components that provide contextual feedback without being intrusive. Generally non-interruptive. Example: Tooltips, Snackbar." + }, + "medium": { + "$value": [ + { + "color": { + "colorSpace": "srgb", + "components": [ 0, 0, 0 ], + "alpha": 0.05 + }, + "offsetX": { "value": 0, "unit": "px" }, + "offsetY": { "value": 2, "unit": "px" }, + "blur": { "value": 3, "unit": "px" }, + "spread": { "value": 0, "unit": "px" } + }, + { + "color": { + "colorSpace": "srgb", + "components": [ 0, 0, 0 ], + "alpha": 0.04 + }, + "offsetX": { "value": 0, "unit": "px" }, + "offsetY": { "value": 4, "unit": "px" }, + "blur": { "value": 5, "unit": "px" }, + "spread": { "value": 0, "unit": "px" } + }, + { + "color": { + "colorSpace": "srgb", + "components": [ 0, 0, 0 ], + "alpha": 0.03 + }, + "offsetX": { "value": 0, "unit": "px" }, + "offsetY": { "value": 12, "unit": "px" }, + "blur": { "value": 12, "unit": "px" }, + "spread": { "value": 0, "unit": "px" } + }, + { + "color": { + "colorSpace": "srgb", + "components": [ 0, 0, 0 ], + "alpha": 0.02 + }, + "offsetX": { "value": 0, "unit": "px" }, + "offsetY": { "value": 16, "unit": "px" }, + "blur": { "value": 16, "unit": "px" }, + "spread": { "value": 0, "unit": "px" } + } + ], + "$description": "For components that offer additional actions. Example: Menus, Command Palette" + }, + "large": { + "$value": [ + { + "color": { + "colorSpace": "srgb", + "components": [ 0, 0, 0 ], + "alpha": 0.08 + }, + "offsetX": { "value": 0, "unit": "px" }, + "offsetY": { "value": 5, "unit": "px" }, + "blur": { "value": 15, "unit": "px" }, + "spread": { "value": 0, "unit": "px" } + }, + { + "color": { + "colorSpace": "srgb", + "components": [ 0, 0, 0 ], + "alpha": 0.07 + }, + "offsetX": { "value": 0, "unit": "px" }, + "offsetY": { "value": 15, "unit": "px" }, + "blur": { "value": 27, "unit": "px" }, + "spread": { "value": 0, "unit": "px" } + }, + { + "color": { + "colorSpace": "srgb", + "components": [ 0, 0, 0 ], + "alpha": 0.04 + }, + "offsetX": { "value": 0, "unit": "px" }, + "offsetY": { "value": 30, "unit": "px" }, + "blur": { "value": 36, "unit": "px" }, + "spread": { "value": 0, "unit": "px" } + }, + { + "color": { + "colorSpace": "srgb", + "components": [ 0, 0, 0 ], + "alpha": 0.02 + }, + "offsetX": { "value": 0, "unit": "px" }, + "offsetY": { "value": 50, "unit": "px" }, + "blur": { "value": 43, "unit": "px" }, + "spread": { "value": 0, "unit": "px" } + } + ], + "$description": "For components that confirm decisions or handle necessary interruptions. Example: Modals." + } + } +} diff --git a/packages/theme/tokens/spacing.json b/packages/theme/tokens/spacing.json new file mode 100644 index 00000000000000..55198fe2257385 --- /dev/null +++ b/packages/theme/tokens/spacing.json @@ -0,0 +1,45 @@ +{ + "spacing": { + "$type": "dimension", + "05": { + "$value": { "value": 4, "unit": "px" }, + "$description": "Extra small spacing" + }, + "10": { + "$value": { "value": 8, "unit": "px" }, + "$description": "Small spacing" + }, + "15": { + "$value": { "value": 12, "unit": "px" }, + "$description": "Medium spacing" + }, + "20": { + "$value": { "value": 16, "unit": "px" }, + "$description": "Large spacing" + }, + "30": { + "$value": { "value": 24, "unit": "px" }, + "$description": "Extra large spacing" + }, + "40": { + "$value": { "value": 32, "unit": "px" }, + "$description": "2X large spacing" + }, + "50": { + "$value": { "value": 40, "unit": "px" }, + "$description": "3X large spacing" + }, + "60": { + "$value": { "value": 48, "unit": "px" }, + "$description": "4X large spacing" + }, + "70": { + "$value": { "value": 56, "unit": "px" }, + "$description": "5X large spacing" + }, + "80": { + "$value": { "value": 64, "unit": "px" }, + "$description": "6X large spacing" + } + } +} diff --git a/packages/theme/tokens/typography.json b/packages/theme/tokens/typography.json new file mode 100644 index 00000000000000..8ffa6d6537bf1d --- /dev/null +++ b/packages/theme/tokens/typography.json @@ -0,0 +1,93 @@ +{ + "font": { + "family": { + "$type": "fontFamily", + "heading": { + "$value": [ + "-apple-system", + "system-ui", + "Segoe UI", + "Roboto", + "Oxygen-Sans", + "Ubuntu", + "Cantarell", + "Helvetica Neue", + "sans-serif" + ], + "$description": "Headings font family" + }, + "body": { + "$value": [ + "-apple-system", + "system-ui", + "Segoe UI", + "Roboto", + "Oxygen-Sans", + "Ubuntu", + "Cantarell", + "Helvetica Neue", + "sans-serif" + ], + "$description": "Body font family" + }, + "mono": { + "$value": [ "Menlo", "Consolas", "monaco", "monospace" ], + "$description": "Monospace font family" + } + }, + "size": { + "$type": "dimension", + "x-small": { + "$value": { "value": 11, "unit": "px" }, + "$description": "Extra small font size" + }, + "small": { + "$value": { "value": 12, "unit": "px" }, + "$description": "Small font size" + }, + "medium": { + "$value": { "value": 13, "unit": "px" }, + "$description": "Medium font size" + }, + "large": { + "$value": { "value": 15, "unit": "px" }, + "$description": "Large font size" + }, + "x-large": { + "$value": { "value": 20, "unit": "px" }, + "$description": "Extra large font size" + }, + "2x-large": { + "$value": { "value": 32, "unit": "px" }, + "$description": "2X large font size" + } + }, + "lineHeight": { + "$type": "dimension", + "x-small": { + "$value": { "value": 16, "unit": "px" }, + "$description": "Extra small line height" + }, + "small": { + "$value": { "value": 20, "unit": "px" }, + "$description": "Small line height" + }, + "medium": { + "$value": { "value": 24, "unit": "px" }, + "$description": "Medium line height" + }, + "large": { + "$value": { "value": 28, "unit": "px" }, + "$description": "Large line height" + }, + "x-large": { + "$value": { "value": 32, "unit": "px" }, + "$description": "Extra large line height" + }, + "2x-large": { + "$value": { "value": 40, "unit": "px" }, + "$description": "2X large line height" + } + } + } +} diff --git a/packages/theme/tsconfig.json b/packages/theme/tsconfig.json new file mode 100644 index 00000000000000..2238d7767fe70e --- /dev/null +++ b/packages/theme/tsconfig.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig.json", + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "moduleResolution": "bundler", + "types": [ "jest", "@testing-library/jest-dom" ] + }, + "references": [ { "path": "../element" }, { "path": "../private-apis" } ] +} diff --git a/tsconfig.json b/tsconfig.json index cf5608b93c69d8..96e1ba6cf6efd4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -56,6 +56,7 @@ { "path": "packages/shortcode" }, { "path": "packages/style-engine" }, { "path": "packages/sync" }, + { "path": "packages/theme" }, { "path": "packages/token-list" }, { "path": "packages/undo-manager" }, { "path": "packages/upload-media" },