diff --git a/.github/bin/generate-docs/readme.mjs b/.github/bin/generate-docs/readme.mjs index 957e7a6b0..5c8cf232a 100644 --- a/.github/bin/generate-docs/readme.mjs +++ b/.github/bin/generate-docs/readme.mjs @@ -28,10 +28,14 @@ installDoc = installDoc.replace(` // Insert "Header" section installDoc = installDoc.replace('
', `# [PostCSS Logo][postcss] -[npm version][npm-url] -[CSS Standard Status][css-url] -[Build Status][cli-url] -[Discord][discord]`); +` + `[npm version][npm-url] ` + +`${ + packageJSONInfo.csstools?.cssdbId ? + `[CSS Standard Status][css-url] ` : + '' +}` + +`[Build Status][cli-url] ` + +`[Discord][discord]`); // Insert "Usage" section installDoc = installDoc.replace('', `## Usage @@ -62,7 +66,11 @@ instructions for: // Insert "Link List" section installDoc = installDoc.replace('', `[cli-url]: https://github.com/csstools/postcss-plugins/actions/workflows/test.yml?query=workflow/test -[css-url]: https://cssdb.org/# +${ + packageJSONInfo.csstools?.cssdbId ? + `[css-url]: https://cssdb.org/#` : + '' +} [discord]: https://discord.gg/bUadyRwkJS [npm-url]: https://www.npmjs.com/package/ diff --git a/package-lock.json b/package-lock.json index 6de3ab45a..72f75f66f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1775,6 +1775,10 @@ "resolved": "plugins/postcss-color-function", "link": true }, + "node_modules/@csstools/postcss-design-tokens": { + "resolved": "plugins/postcss-design-tokens", + "link": true + }, "node_modules/@csstools/postcss-font-format-keywords": { "resolved": "plugins/postcss-font-format-keywords", "link": true @@ -6181,6 +6185,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-dictionary-design-tokens-example": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/style-dictionary-design-tokens-example/-/style-dictionary-design-tokens-example-1.0.0.tgz", + "integrity": "sha512-dUjgpSbx4uOAj3qZ1Tja3Vur3w+/UbVlT7JW2aAH3U7w8Hqm7nKwE0dCVU8O94sSFCs0jsdfPlvcYsEIIzLtvA==", + "dev": true + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -6979,6 +6989,28 @@ "postcss": "^8.4" } }, + "plugins/postcss-design-tokens": { + "name": "@csstools/postcss-design-tokens", + "version": "1.0.0", + "license": "CC0-1.0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "devDependencies": { + "postcss-import": "^14.1.0", + "style-dictionary-design-tokens-example": "^1.0.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.3" + } + }, "plugins/postcss-dir-pseudo-class": { "version": "6.0.4", "license": "CC0-1.0", @@ -8610,6 +8642,14 @@ "postcss-value-parser": "^4.2.0" } }, + "@csstools/postcss-design-tokens": { + "version": "file:plugins/postcss-design-tokens", + "requires": { + "postcss-import": "^14.1.0", + "postcss-value-parser": "^4.2.0", + "style-dictionary-design-tokens-example": "^1.0.0" + } + }, "@csstools/postcss-font-format-keywords": { "version": "file:plugins/postcss-font-format-keywords", "requires": { @@ -11920,6 +11960,12 @@ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, + "style-dictionary-design-tokens-example": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/style-dictionary-design-tokens-example/-/style-dictionary-design-tokens-example-1.0.0.tgz", + "integrity": "sha512-dUjgpSbx4uOAj3qZ1Tja3Vur3w+/UbVlT7JW2aAH3U7w8Hqm7nKwE0dCVU8O94sSFCs0jsdfPlvcYsEIIzLtvA==", + "dev": true + }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", diff --git a/plugins/postcss-base-plugin/README.md b/plugins/postcss-base-plugin/README.md index 0d18299e7..9fa7056e9 100644 --- a/plugins/postcss-base-plugin/README.md +++ b/plugins/postcss-base-plugin/README.md @@ -1,11 +1,8 @@ # PostCSS Base Plugin [PostCSS Logo][postcss] -[npm version][npm-url] -[CSS Standard Status][css-url] -[Build Status][cli-url] -[Discord][discord] +[npm version][npm-url] [CSS Standard Status][css-url] [Build Status][cli-url] [Discord][discord] -[PostCSS Base Plugin] lets easily create new plugins following some [CSS Specification]. +[PostCSS Base Plugin] lets you easily create new plugins following some [CSS Specification]. ```pcss .foo { diff --git a/plugins/postcss-base-plugin/docs/README.md b/plugins/postcss-base-plugin/docs/README.md index 3bc455892..8680a65c8 100644 --- a/plugins/postcss-base-plugin/docs/README.md +++ b/plugins/postcss-base-plugin/docs/README.md @@ -14,7 +14,7 @@
-[] lets easily create new plugins following some [CSS Specification]. +[] lets you easily create new plugins following some [CSS Specification]. ```pcss diff --git a/plugins/postcss-cascade-layers/README.md b/plugins/postcss-cascade-layers/README.md index cfa0007c3..6f5b7954b 100644 --- a/plugins/postcss-cascade-layers/README.md +++ b/plugins/postcss-cascade-layers/README.md @@ -1,9 +1,6 @@ # PostCSS Cascade Layers [PostCSS Logo][postcss] -[npm version][npm-url] -[CSS Standard Status][css-url] -[Build Status][cli-url] -[Discord][discord] +[npm version][npm-url] [CSS Standard Status][css-url] [Build Status][cli-url] [Discord][discord] [PostCSS Cascade Layers] lets you use `@layer` following the [Cascade Layers Specification]. For more information on layers, checkout [A Complete Guide to CSS Cascade Layers] by Miriam Suzanne. diff --git a/plugins/postcss-color-function/README.md b/plugins/postcss-color-function/README.md index c19e804e1..62d6b294d 100644 --- a/plugins/postcss-color-function/README.md +++ b/plugins/postcss-color-function/README.md @@ -1,9 +1,6 @@ # PostCSS Color Function [PostCSS Logo][postcss] -[npm version][npm-url] -[CSS Standard Status][css-url] -[Build Status][cli-url] -[Discord][discord] +[npm version][npm-url] [CSS Standard Status][css-url] [Build Status][cli-url] [Discord][discord] [PostCSS Color Function] lets you use the `color` function in CSS, following the [CSS Color] specification. diff --git a/plugins/postcss-design-tokens/.gitignore b/plugins/postcss-design-tokens/.gitignore new file mode 100644 index 000000000..7172b04f1 --- /dev/null +++ b/plugins/postcss-design-tokens/.gitignore @@ -0,0 +1,6 @@ +node_modules +package-lock.json +yarn.lock +*.result.css +*.result.css.map +dist/* diff --git a/plugins/postcss-design-tokens/.nvmrc b/plugins/postcss-design-tokens/.nvmrc new file mode 100644 index 000000000..f0b10f153 --- /dev/null +++ b/plugins/postcss-design-tokens/.nvmrc @@ -0,0 +1 @@ +v16.13.1 diff --git a/plugins/postcss-design-tokens/.tape.mjs b/plugins/postcss-design-tokens/.tape.mjs new file mode 100644 index 000000000..075221bb4 --- /dev/null +++ b/plugins/postcss-design-tokens/.tape.mjs @@ -0,0 +1,104 @@ +import postcssTape from '../../packages/postcss-tape/dist/index.mjs'; +import plugin from '@csstools/postcss-design-tokens'; +import postcssImport from 'postcss-import'; + +postcssTape(plugin)({ + basic: { + message: "supports basic usage", + plugins: [ + postcssImport(), + plugin() + ] + }, + 'basic:rootFontSize-20': { + message: "supports basic usage with { unitsAndValues { rootFontSize: 20 } }", + plugins: [ + postcssImport(), + plugin({ + unitsAndValues: { + rootFontSize: 20 + } + }) + ] + }, + 'basic:rootFontSize-NaN': { + message: "supports basic usage with { unitsAndValues { rootFontSize: NaN } }", + plugins: [ + postcssImport(), + plugin({ + unitsAndValues: { + rootFontSize: NaN + } + }) + ] + }, + 'basic:rootFontSize-invalid': { + message: "supports basic usage with { unitsAndValues { rootFontSize: invalid } }", + plugins: [ + postcssImport(), + plugin({ + unitsAndValues: { + rootFontSize: 'invalid' + } + }) + ] + }, + 'errors': { + message: "handles issues correctly", + options: {}, + warnings: 4 + }, + 'is': { + message: "supports basic usage with { is ['dark', 'tablet', 'branded-green'] }", + options: { + is: ['dark', 'tablet', 'branded-green'] + } + }, + 'value-parsing-a': { + message: "supports value parsing (A)", + }, + 'value-parsing-b': { + message: "supports value parsing (B)", + }, + 'value-parsing-c': { + message: "supports value parsing (C)", + warnings: 2 + }, + 'value-parsing-d': { + message: "supports value parsing (D)", + warnings: 2 + }, + 'value-parsing-e': { + message: "supports value parsing (E)", + warnings: 2 + }, + 'value-parsing-f': { + message: "supports value parsing (F)", + }, + 'value-parsing-g': { + message: "supports value parsing (G)", + warnings: 2 + }, + 'examples/example': { + message: 'minimal example', + options: {}, + }, + 'examples/example-conditional': { + message: 'minimal example with conditional imports : default', + options: {}, + }, + 'examples/example-conditional:dark': { + message: 'minimal example with conditional imports : dark', + options: { + is: ['dark'] + }, + }, + 'examples/example:rootFontSize-20': { + message: "minimal example with { unitsAndValues { rootFontSize: 20 } }", + options: { + unitsAndValues: { + rootFontSize: 20 + } + } + }, +}); diff --git a/plugins/postcss-design-tokens/CHANGELOG.md b/plugins/postcss-design-tokens/CHANGELOG.md new file mode 100644 index 000000000..ea750358a --- /dev/null +++ b/plugins/postcss-design-tokens/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changes to PostCSS Design Tokens + +### 1.0.0 (Unreleased) + +- Initial version diff --git a/plugins/postcss-design-tokens/INSTALL.md b/plugins/postcss-design-tokens/INSTALL.md new file mode 100644 index 000000000..a6635e80b --- /dev/null +++ b/plugins/postcss-design-tokens/INSTALL.md @@ -0,0 +1,176 @@ +# Installing PostCSS Design Tokens + +[PostCSS Design Tokens] runs in all Node environments, with special instructions for: + +| [Node](#node) | [PostCSS CLI](#postcss-cli) | [Webpack](#webpack) | [Create React App](#create-react-app) | [Gulp](#gulp) | [Grunt](#grunt) | +| --- | --- | --- | --- | --- | --- | + +## Node + +Add [PostCSS Design Tokens] to your project: + +```bash +npm install postcss @csstools/postcss-design-tokens --save-dev +``` + +Use it as a [PostCSS] plugin: + +```js +const postcss = require('postcss'); +const postcssDesignTokens = require('@csstools/postcss-design-tokens'); + +postcss([ + postcssDesignTokens(/* pluginOptions */) +]).process(YOUR_CSS /*, processOptions */); +``` + +## PostCSS CLI + +Add [PostCSS CLI] to your project: + +```bash +npm install postcss-cli @csstools/postcss-design-tokens --save-dev +``` + +Use [PostCSS Design Tokens] in your `postcss.config.js` configuration file: + +```js +const postcssDesignTokens = require('@csstools/postcss-design-tokens'); + +module.exports = { + plugins: [ + postcssDesignTokens(/* pluginOptions */) + ] +} +``` + +## Webpack + +_Webpack version 5_ + +Add [PostCSS Loader] to your project: + +```bash +npm install postcss-loader @csstools/postcss-design-tokens --save-dev +``` + +Use [PostCSS Design Tokens] in your Webpack configuration: + +```js +module.exports = { + module: { + rules: [ + { + test: /\.css$/i, + use: [ + "style-loader", + { + loader: "css-loader", + options: { importLoaders: 1 }, + }, + { + loader: "postcss-loader", + options: { + postcssOptions: { + plugins: [ + [ + "@csstools/postcss-design-tokens", + { + // Options + }, + ], + ], + }, + }, + }, + ], + }, + ], + }, +}; +``` + +## Create React App + +Add [React App Rewired] and [React App Rewire PostCSS] to your project: + +```bash +npm install react-app-rewired react-app-rewire-postcss @csstools/postcss-design-tokens --save-dev +``` + +Use [React App Rewire PostCSS] and [PostCSS Design Tokens] in your +`config-overrides.js` file: + +```js +const reactAppRewirePostcss = require('react-app-rewire-postcss'); +const postcssDesignTokens = require('@csstools/postcss-design-tokens'); + +module.exports = config => reactAppRewirePostcss(config, { + plugins: () => [ + postcssDesignTokens(/* pluginOptions */) + ] +}); +``` + +## Gulp + +Add [Gulp PostCSS] to your project: + +```bash +npm install gulp-postcss @csstools/postcss-design-tokens --save-dev +``` + +Use [PostCSS Design Tokens] in your Gulpfile: + +```js +const postcss = require('gulp-postcss'); +const postcssDesignTokens = require('@csstools/postcss-design-tokens'); + +gulp.task('css', function () { + var plugins = [ + postcssDesignTokens(/* pluginOptions */) + ]; + + return gulp.src('./src/*.css') + .pipe(postcss(plugins)) + .pipe(gulp.dest('.')); +}); +``` + +## Grunt + +Add [Grunt PostCSS] to your project: + +```bash +npm install grunt-postcss @csstools/postcss-design-tokens --save-dev +``` + +Use [PostCSS Design Tokens] in your Gruntfile: + +```js +const postcssDesignTokens = require('@csstools/postcss-design-tokens'); + +grunt.loadNpmTasks('grunt-postcss'); + +grunt.initConfig({ + postcss: { + options: { + processors: [ + postcssDesignTokens(/* pluginOptions */) + ] + }, + dist: { + src: '*.css' + } + } +}); +``` + +[Gulp PostCSS]: https://github.com/postcss/gulp-postcss +[Grunt PostCSS]: https://github.com/nDmitry/grunt-postcss +[PostCSS]: https://github.com/postcss/postcss +[PostCSS CLI]: https://github.com/postcss/postcss-cli +[PostCSS Loader]: https://github.com/postcss/postcss-loader +[PostCSS Design Tokens]: https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-design-tokens +[React App Rewire PostCSS]: https://github.com/csstools/react-app-rewire-postcss +[React App Rewired]: https://github.com/timarney/react-app-rewired diff --git a/plugins/postcss-design-tokens/LICENSE.md b/plugins/postcss-design-tokens/LICENSE.md new file mode 100644 index 000000000..0bc1fa706 --- /dev/null +++ b/plugins/postcss-design-tokens/LICENSE.md @@ -0,0 +1,108 @@ +# CC0 1.0 Universal + +## Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator and +subsequent owner(s) (each and all, an “owner”) of an original work of +authorship and/or a database (each, a “Work”). + +Certain owners wish to permanently relinquish those rights to a Work for the +purpose of contributing to a commons of creative, cultural and scientific works +(“Commons”) that the public can reliably and without fear of later claims of +infringement build upon, modify, incorporate in other works, reuse and +redistribute as freely as possible in any form whatsoever and for any purposes, +including without limitation commercial purposes. These owners may contribute +to the Commons to promote the ideal of a free culture and the further +production of creative, cultural and scientific works, or to gain reputation or +greater distribution for their Work in part through the use and efforts of +others. + +For these and/or other purposes and motivations, and without any expectation of +additional consideration or compensation, the person associating CC0 with a +Work (the “Affirmer”), to the extent that he or she is an owner of Copyright +and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and +publicly distribute the Work under its terms, with knowledge of his or her +Copyright and Related Rights in the Work and the meaning and intended legal +effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be + protected by copyright and related or neighboring rights (“Copyright and + Related Rights”). Copyright and Related Rights include, but are not limited + to, the following: + 1. the right to reproduce, adapt, distribute, perform, display, communicate, + and translate a Work; + 2. moral rights retained by the original author(s) and/or performer(s); + 3. publicity and privacy rights pertaining to a person’s image or likeness + depicted in a Work; + 4. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(i), below; + 5. rights protecting the extraction, dissemination, use and reuse of data in + a Work; + 6. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation thereof, + including any amended or successor version of such directive); and + 7. other similar, equivalent or corresponding rights throughout the world + based on applicable law or treaty, and any national implementations + thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention of, + applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and + unconditionally waives, abandons, and surrenders all of Affirmer’s Copyright + and Related Rights and associated claims and causes of action, whether now + known or unknown (including existing as well as future claims and causes of + action), in the Work (i) in all territories worldwide, (ii) for the maximum + duration provided by applicable law or treaty (including future time + extensions), (iii) in any current or future medium and for any number of + copies, and (iv) for any purpose whatsoever, including without limitation + commercial, advertising or promotional purposes (the “Waiver”). Affirmer + makes the Waiver for the benefit of each member of the public at large and + to the detriment of Affirmer’s heirs and successors, fully intending that + such Waiver shall not be subject to revocation, rescission, cancellation, + termination, or any other legal or equitable action to disrupt the quiet + enjoyment of the Work by the public as contemplated by Affirmer’s express + Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason be + judged legally invalid or ineffective under applicable law, then the Waiver + shall be preserved to the maximum extent permitted taking into account + Affirmer’s express Statement of Purpose. In addition, to the extent the + Waiver is so judged Affirmer hereby grants to each affected person a + royalty-free, non transferable, non sublicensable, non exclusive, + irrevocable and unconditional license to exercise Affirmer’s Copyright and + Related Rights in the Work (i) in all territories worldwide, (ii) for the + maximum duration provided by applicable law or treaty (including future time + extensions), (iii) in any current or future medium and for any number of + copies, and (iv) for any purpose whatsoever, including without limitation + commercial, advertising or promotional purposes (the “License”). The License + shall be deemed effective as of the date CC0 was applied by Affirmer to the + Work. Should any part of the License for any reason be judged legally + invalid or ineffective under applicable law, such partial invalidity or + ineffectiveness shall not invalidate the remainder of the License, and in + such case Affirmer hereby affirms that he or she will not (i) exercise any + of his or her remaining Copyright and Related Rights in the Work or (ii) + assert any associated claims and causes of action with respect to the Work, + in either case contrary to Affirmer’s express Statement of Purpose. + +4. Limitations and Disclaimers. + 1. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + 2. Affirmer offers the Work as-is and makes no representations or warranties + of any kind concerning the Work, express, implied, statutory or + otherwise, including without limitation warranties of title, + merchantability, fitness for a particular purpose, non infringement, or + the absence of latent or other defects, accuracy, or the present or + absence of errors, whether or not discoverable, all to the greatest + extent permissible under applicable law. + 3. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person’s Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the Work. + 4. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to this + CC0 or use of the Work. + +For more information, please see +http://creativecommons.org/publicdomain/zero/1.0/. diff --git a/plugins/postcss-design-tokens/README.md b/plugins/postcss-design-tokens/README.md new file mode 100644 index 000000000..87614e8e1 --- /dev/null +++ b/plugins/postcss-design-tokens/README.md @@ -0,0 +1,287 @@ +# PostCSS Design Tokens [PostCSS Logo][postcss] + +[npm version][npm-url] [Build Status][cli-url] [Discord][discord] + +[PostCSS Design Tokens] lets you use design tokens in your CSS source files. + +```json +{ + "color": { + "background": { + "primary": { "value": "#fff" } + } + }, + "size": { + "spacing": { + "small": { "value": "16px" } + } + } +} +``` + +```pcss +@design-tokens url('./tokens.json') format('style-dictionary3'); + +.foo { + color: design-token('color.background.primary'); + padding-top: design-token('size.spacing.small'); + padding-left: design-token('size.spacing.small' to px); + padding-bottom: design-token('size.spacing.small' to rem); +} + +/* becomes */ + +.foo { + color: #fff; + padding-top: 16px; + padding-left: 16px; + padding-bottom: 1rem; +} +``` + +## Usage + +Add [PostCSS Design Tokens] to your project: + +```bash +npm install postcss @csstools/postcss-design-tokens --save-dev +``` + +Use it as a [PostCSS] plugin: + +```js +const postcss = require('postcss'); +const postcssDesignTokens = require('@csstools/postcss-design-tokens'); + +postcss([ + postcssDesignTokens(/* pluginOptions */) +]).process(YOUR_CSS /*, processOptions */); +``` + +[PostCSS Design Tokens] runs in all Node environments, with special +instructions for: + +| [Node](INSTALL.md#node) | [PostCSS CLI](INSTALL.md#postcss-cli) | [Webpack](INSTALL.md#webpack) | [Create React App](INSTALL.md#create-react-app) | [Gulp](INSTALL.md#gulp) | [Grunt](INSTALL.md#grunt) | +| --- | --- | --- | --- | --- | --- | + +## Formats + +At this time there is no standardized format for design tokens. +Although there is an ongoing effort to create this, we feel it is still too early to adopt this. + +For the moment we only support [Style Dictionary](https://amzn.github.io/style-dictionary/#/). +Use `style-dictionary3` in `@design-tokens` rules to pick this format. + +## Options + +### is + +The `is` option determines which design tokens are used. +This allows you to generate multiple themed stylesheets. + +By default only `@design-tokens` without any `when('foo')` conditions are used. + +#### Example usage + +**For these two token files :** + +```json +{ + "color": { + "background": { + "primary": { "value": "#fff" } + } + } +} +``` + +```json +{ + "color": { + "background": { + "primary": { "value": "#000" } + } + } +} +``` + +**And this CSS :** + +```pcss +@design-tokens url('./tokens-light.json') format('style-dictionary3'); +@design-tokens url('./tokens-dark.json') when('dark') format('style-dictionary3'); + +.foo { + color: design-token('color.background.primary'); +} +``` + +**You can configure :** + +##### No `is` option. + +```js +postcssDesignTokens() +``` + +```pcss +@design-tokens url('./tokens-light.json') format('style-dictionary3'); +@design-tokens url('./tokens-dark.json') when('dark') format('style-dictionary3'); + +.foo { + color: design-token('color.background.primary'); +} + +/* becomes */ + +.foo { + color: #fff; +} +``` + +##### `is` option set to 'dark'. + +```js +postcssDesignTokens({ is: ['dark'] }) +``` + +```pcss +@design-tokens url('./tokens-light.json') format('style-dictionary3'); +@design-tokens url('./tokens-dark.json') when('dark') format('style-dictionary3'); + +.foo { + color: design-token('color.background.primary'); +} + +/* becomes */ + +.foo { + color: #000; +} +``` + +### unitsAndValues + +The `unitsAndValues` option allows you to control some aspects of how design values are converted to CSS. +`rem` <-> `px` for example can only be calculated when we know the root font size. + +#### rootFontSize + +defaults to `16` + +```js +postcssDesignTokens({ + unitsAndValues: { + rootFontSize: 20, + }, +}) +``` + +```pcss +@design-tokens url('./tokens.json') format('style-dictionary3'); + +.foo { + color: design-token('color.background.primary'); + padding-top: design-token('size.spacing.small'); + padding-left: design-token('size.spacing.small' to px); + padding-bottom: design-token('size.spacing.small' to rem); +} + +/* becomes */ + +.foo { + color: #fff; + padding-top: 16px; + padding-left: 16px; + padding-bottom: 0.8rem; +} +``` + +## Syntax + +[PostCSS Design Tokens] is non-standard and is not part of any official CSS Specification. + +### Editor support + +This is all very new and we hope that one day design tokens will become first class citizens in editors and other tools. +Until then we will do our best to provide extensions. +These will have rough edges but should illustrate were we want to go. + +| editor | plugin | +| --- | --- | +| VSCode | [CSSTools Design Tokens](https://marketplace.visualstudio.com/items?itemName=RomainMenke.csstools-design-tokens) | + +### `@design-tokens` rule + +The `@design-tokens` rule is used to import design tokens from a JSON file into your CSS. + +```pcss +@design-tokens url('./tokens.json') format('style-dictionary3'); +``` + +```pcss +@design-tokens url('./tokens.json') format('style-dictionary3'); +@design-tokens url('./tokens-dark-mode.json') format('style-dictionary3') when('dark'); +``` + +``` +@design-tokens [ | ] + [ when(*) ]? + format(); + + = + + = [ 'style-dictionary3' ] +``` + +All `@design-tokens` rules in a document are evaluated in order of appearance. +If a token with the same path and name already exists it will be overridden. + +All `@design-tokens` rules are evaluated before any `design-token()` functions. + +`@design-tokens` rules can be conditional through `when` conditions. Multiple values can be specified in `when`.
+Multiple conditions always have an `AND` relationship. + +> ```css +> /* only evaluated when tooling receives 'blue' and 'muted' as arguments */ +> @design-tokens url('./tokens.json') format('style-dictionary3') when('blue' 'muted'); +> ``` + +`@design-tokens` rules can never be made conditional through `@supports`, `@media` or other conditional rules. + +> ```css +> @media (min-width: 500px) { +> @design-tokens url('./tokens.json') format('style-dictionary3'); /* always evaluated */ +> } +> ``` + +Any form of nesting is meaningless, `@design-tokens` will always be evaluated as if they were declared at the top level. + + +### `design-token()` function + +The `design-token()` function takes a token path and returns the token value. + +```pcss +.foo { + color: design-token('color.background.primary'); +} +``` + +``` +design-token() = design-token( [ to ]? ) + + = + = [ px | rem ] +``` + +[cli-url]: https://github.com/csstools/postcss-plugins/actions/workflows/test.yml?query=workflow/test + +[discord]: https://discord.gg/bUadyRwkJS +[npm-url]: https://www.npmjs.com/package/@csstools/postcss-design-tokens + +[Gulp PostCSS]: https://github.com/postcss/gulp-postcss +[Grunt PostCSS]: https://github.com/nDmitry/grunt-postcss +[PostCSS]: https://github.com/postcss/postcss +[PostCSS Loader]: https://github.com/postcss/postcss-loader +[PostCSS Design Tokens]: https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-design-tokens diff --git a/plugins/postcss-design-tokens/docs/README.md b/plugins/postcss-design-tokens/docs/README.md new file mode 100644 index 000000000..e105b158c --- /dev/null +++ b/plugins/postcss-design-tokens/docs/README.md @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + +
+ +[] lets you use design tokens in your CSS source files. + +```json + +``` + +```pcss + + +/* becomes */ + + +``` + + + + + +## Formats + +At this time there is no standardized format for design tokens. +Although there is an ongoing effort to create this, we feel it is still too early to adopt this. + +For the moment we only support [Style Dictionary](https://amzn.github.io/style-dictionary/#/). +Use `style-dictionary3` in `@design-tokens` rules to pick this format. + +## Options + +### is + +The `is` option determines which design tokens are used. +This allows you to generate multiple themed stylesheets. + +By default only `@design-tokens` without any `when('foo')` conditions are used. + +#### Example usage + +**For these two token files :** + +```json + +``` + +```json + +``` + +**And this CSS :** + +```pcss + +``` + +**You can configure :** + +##### No `is` option. + +```js +() +``` + +```pcss + + +/* becomes */ + + +``` + +##### `is` option set to 'dark'. + +```js +({ is: ['dark'] }) +``` + +```pcss + + +/* becomes */ + + +``` + +### unitsAndValues + +The `unitsAndValues` option allows you to control some aspects of how design values are converted to CSS. +`rem` <-> `px` for example can only be calculated when we know the root font size. + +#### rootFontSize + +defaults to `16` + +```js +({ + unitsAndValues: { + rootFontSize: 20, + }, +}) +``` + +```pcss + + +/* becomes */ + + +``` + +## Syntax + +[] is non-standard and is not part of any official CSS Specification. + +### Editor support + +This is all very new and we hope that one day design tokens will become first class citizens in editors and other tools. +Until then we will do our best to provide extensions. +These will have rough edges but should illustrate were we want to go. + +| editor | plugin | +| --- | --- | +| VSCode | [CSSTools Design Tokens](https://marketplace.visualstudio.com/items?itemName=RomainMenke.csstools-design-tokens) | + +### `@design-tokens` rule + +The `@design-tokens` rule is used to import design tokens from a JSON file into your CSS. + +```pcss +@design-tokens url('./tokens.json') format('style-dictionary3'); +``` + +```pcss +@design-tokens url('./tokens.json') format('style-dictionary3'); +@design-tokens url('./tokens-dark-mode.json') format('style-dictionary3') when('dark'); +``` + +``` +@design-tokens [ | ] + [ when(*) ]? + format(); + + = + + = [ 'style-dictionary3' ] +``` + +All `@design-tokens` rules in a document are evaluated in order of appearance. +If a token with the same path and name already exists it will be overridden. + +All `@design-tokens` rules are evaluated before any `design-token()` functions. + +`@design-tokens` rules can be conditional through `when` conditions. Multiple values can be specified in `when`.
+Multiple conditions always have an `AND` relationship. + +> ```css +> /* only evaluated when tooling receives 'blue' and 'muted' as arguments */ +> @design-tokens url('./tokens.json') format('style-dictionary3') when('blue' 'muted'); +> ``` + +`@design-tokens` rules can never be made conditional through `@supports`, `@media` or other conditional rules. + +> ```css +> @media (min-width: 500px) { +> @design-tokens url('./tokens.json') format('style-dictionary3'); /* always evaluated */ +> } +> ``` + +Any form of nesting is meaningless, `@design-tokens` will always be evaluated as if they were declared at the top level. + + +### `design-token()` function + +The `design-token()` function takes a token path and returns the token value. + +```pcss +.foo { + color: design-token('color.background.primary'); +} +``` + +``` +design-token() = design-token( [ to ]? ) + + = + = [ px | rem ] +``` + + diff --git a/plugins/postcss-design-tokens/package.json b/plugins/postcss-design-tokens/package.json new file mode 100644 index 000000000..e0cc6333a --- /dev/null +++ b/plugins/postcss-design-tokens/package.json @@ -0,0 +1,80 @@ +{ + "name": "@csstools/postcss-design-tokens", + "description": "Use design tokens in your CSS", + "version": "1.0.0", + "contributors": [ + { + "name": "Antonio Laguna", + "email": "antonio@laguna.es", + "url": "https://antonio.laguna.es" + }, + { + "name": "Romain Menke ", + "email": "romainmenke@gmail.com" + } + ], + "license": "CC0-1.0", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "main": "dist/index.cjs", + "module": "dist/index.mjs", + "types": "dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.mjs", + "require": "./dist/index.cjs", + "default": "./dist/index.mjs" + } + }, + "files": [ + "CHANGELOG.md", + "LICENSE.md", + "README.md", + "dist" + ], + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "peerDependencies": { + "postcss": "^8.3" + }, + "devDependencies": { + "postcss-import": "^14.1.0", + "style-dictionary-design-tokens-example": "^1.0.0" + }, + "scripts": { + "build": "rollup -c ../../rollup/default.js", + "clean": "node -e \"fs.rmSync('./dist', { recursive: true, force: true });\"", + "docs": "node ../../.github/bin/generate-docs/install.mjs && node ../../.github/bin/generate-docs/readme.mjs", + "lint": "npm run lint:eslint && npm run lint:package-json", + "lint:eslint": "eslint ./src --ext .js --ext .ts --ext .mjs --no-error-on-unmatched-pattern", + "lint:package-json": "node ../../.github/bin/format-package-json.mjs", + "prepublishOnly": "npm run clean && npm run build && npm run test", + "test": "node .tape.mjs && npm run test:exports", + "test:exports": "node ./test/_import.mjs && node ./test/_require.cjs", + "test:rewrite-expects": "REWRITE_EXPECTS=true node .tape.mjs" + }, + "homepage": "https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-design-tokens#readme", + "repository": { + "type": "git", + "url": "https://github.com/csstools/postcss-plugins.git", + "directory": "plugins/postcss-design-tokens" + }, + "bugs": "https://github.com/csstools/postcss-plugins/issues", + "keywords": [ + "design-tokens", + "postcss-plugin" + ], + "csstools": { + "exportName": "postcssDesignTokens", + "humanReadableName": "PostCSS Design Tokens" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/plugins/postcss-design-tokens/src/constants.ts b/plugins/postcss-design-tokens/src/constants.ts new file mode 100644 index 000000000..c77d745b5 --- /dev/null +++ b/plugins/postcss-design-tokens/src/constants.ts @@ -0,0 +1,2 @@ +// a random, but shared default condition +export const DEFAULT_CONDITION = '6b4e71e7-4787-42f7-a092-8684961895db'; diff --git a/plugins/postcss-design-tokens/src/data-formats/base/token.ts b/plugins/postcss-design-tokens/src/data-formats/base/token.ts new file mode 100644 index 000000000..5838f8695 --- /dev/null +++ b/plugins/postcss-design-tokens/src/data-formats/base/token.ts @@ -0,0 +1,10 @@ +export interface TokenTransformOptions { + pluginOptions: { + rootFontSize: number; + }; + toUnit?: string; +} + +export interface Token { + cssValue(opts?: TokenTransformOptions): string +} diff --git a/plugins/postcss-design-tokens/src/data-formats/parse-import.ts b/plugins/postcss-design-tokens/src/data-formats/parse-import.ts new file mode 100644 index 000000000..f3089c72c --- /dev/null +++ b/plugins/postcss-design-tokens/src/data-formats/parse-import.ts @@ -0,0 +1,68 @@ +import valueParser from 'postcss-value-parser'; +import { Token } from './base/token'; +import { extractStyleDictionaryTokens } from './style-dictionary/style-dictionary'; +import path from 'path'; +import { promises as fsp } from 'fs'; +import { DEFAULT_CONDITION } from '../constants'; + +function parseImport(statement: string): { filePath: string, format: string, conditions: Array } { + const importAST = valueParser(statement); + + const result = { + filePath: '', + format: 'standard', + conditions: [DEFAULT_CONDITION], + }; + + importAST.walk((node) => { + if (node.type === 'function' && node.value === 'url') { + result.filePath = node.nodes[0].value; + } + + if (node.type === 'function' && node.value === 'format') { + result.format = node.nodes[0].value; + } + + if (node.type === 'function' && node.value === 'when') { + result.conditions = node.nodes.filter((child) => { + return child.type === 'string'; + }).map((child) => child.value); + } + }); + + if (!result.conditions.length) { + result.conditions = [DEFAULT_CONDITION]; + } + + return result; +} + +export async function tokensFromImport(buildIs: Array, sourceFilePath: string, statement: string, alreadyImported: Set): Promise<{ filePath: string, tokens: Map }|false> { + const { filePath, format, conditions } = parseImport(statement); + if (!conditions.every((condition) => buildIs.includes(condition))) { + return false; + } + + const resolvedPath = path.resolve(path.dirname(sourceFilePath), filePath); + if (alreadyImported.has(resolvedPath)) { + return false; + } + + alreadyImported.add(resolvedPath); + + const fileContents = await fsp.readFile(resolvedPath, 'utf8'); + const tokenContents = JSON.parse(fileContents); + + switch (format) { + case 'style-dictionary3': + return { + filePath: path.resolve(filePath), + tokens: extractStyleDictionaryTokens('3', tokenContents, resolvedPath), + }; + + default: + break; + } + + throw new Error('Unsupported format: ' + format); +} diff --git a/plugins/postcss-design-tokens/src/data-formats/style-dictionary/style-dictionary.ts b/plugins/postcss-design-tokens/src/data-formats/style-dictionary/style-dictionary.ts new file mode 100644 index 000000000..a2b588294 --- /dev/null +++ b/plugins/postcss-design-tokens/src/data-formats/style-dictionary/style-dictionary.ts @@ -0,0 +1,13 @@ + +import { Token } from '../base/token'; +import { extractStyleDictionaryV3Tokens, StyleDictionaryV3TokenGroup } from './v3/group'; + +export const latestStyleDictionaryVersion = '3'; + +export function extractStyleDictionaryTokens(version: string, node: unknown, filePath: string): Map { + if (version === '3') { + return extractStyleDictionaryV3Tokens(node as StyleDictionaryV3TokenGroup, filePath); + } + + throw new Error('Unsupported version: ' + version); +} diff --git a/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/dereference.ts b/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/dereference.ts new file mode 100644 index 000000000..5ac37c07b --- /dev/null +++ b/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/dereference.ts @@ -0,0 +1,248 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { TokenTransformOptions } from '../../base/token'; +import { toposort } from '../../toposort/toposort'; +import { applyTransformsToValue, StyleDictionaryV3TokenValue } from './value'; + +export function dereferenceTokenValues(tokens: Map): Map { + const tainted = new Set(); + const referenceASTs = new Map>(); + + // Gather all references. + { + for (const [id, token] of tokens.entries()) { + const referenceAST = parseReferences(token.value); + if (!referenceAST.length) { + continue; + } + + tainted.add(id); + referenceASTs.set(id, referenceAST); + } + } + + // Fast dereference. + // Only handles references for which a value can found immediately. + { + for (const [id, referenceAST] of referenceASTs.entries()) { + for (let i = 0; i < referenceAST.length; i++) { + const reference = referenceAST[i]; + if (reference.type !== 'value-reference') { + continue; + } + + if (tainted.has(reference.raw)) { + continue; + } + + if (!tokens.has(reference.raw)) { + throw new Error('Alias "' + reference.raw + '" not found'); + } + + const sourceToken = tokens.get(reference.raw)!; + referenceAST[i] = { + type: 'value-resolved', + raw: reference.raw, + value: sourceToken.cssValue(), + }; + } + + const hasFurtherReferences = referenceAST.some(part => part.type === 'value-reference'); + if (hasFurtherReferences) { + continue; + } + + const value = (referenceAST as ValuePartsResolved).map(part => part.value).join(''); + const currentToken = tokens.get(id)!; + + currentToken.value = value; + currentToken.cssValue = (transformOptions: TokenTransformOptions) => { + return applyTransformsToValue(value, transformOptions); + }; + + tokens.set(id, currentToken); + tainted.delete(id); + referenceASTs.delete(id); + } + + if (tainted.size === 0) { + return tokens; + } + } + + // Topological dereference. + { + const nodes : Array = Array.from(tokens.keys()); + const edges: Array> = []; + + for (const [id, referenceAST] of referenceASTs.entries()) { + for (let i = 0; i < referenceAST.length; i++) { + const reference = referenceAST[i]; + if (reference.type !== 'value-reference') { + continue; + } + + edges.push([reference.raw, id]); + } + } + + const sorted = toposort(nodes, edges); + if (!sorted) { + throw new Error('Circular reference detected'); + } + + for (const id of sorted) { + if (!referenceASTs.has(id)) { + continue; + } + + const referenceAST = referenceASTs.get(id)!; + for (let i = 0; i < referenceAST.length; i++) { + const reference = referenceAST[i]; + if (reference.type !== 'value-reference') { + continue; + } + + if (tainted.has(reference.raw)) { + throw new Error('Alias "' + reference.raw + '" can not be resolved'); + } + + if (!tokens.has(reference.raw)) { + throw new Error('Alias "' + reference.raw + '" not found'); + } + + const sourceToken = tokens.get(reference.raw)!; + referenceAST[i] = { + type: 'value-resolved', + raw: reference.raw, + value: sourceToken.cssValue(), + }; + } + + const hasFurtherReferences = referenceAST.some(part => part.type === 'value-reference'); + if (hasFurtherReferences) { + throw new Error('Token "' + id + '" can not be fully resolved'); + } + + const value = (referenceAST as ValuePartsResolved).map(part => part.value).join(''); + const currentToken = tokens.get(id)!; + + currentToken.value = value; + currentToken.cssValue = (transformOptions: TokenTransformOptions) => { + return applyTransformsToValue(value, transformOptions); + }; + + tokens.set(id, currentToken); + tainted.delete(id); + referenceASTs.delete(id); + } + + if (tainted.size === 0) { + return tokens; + } + } + + return tokens; +} + +type ValuePartsResolved = Array; +type ValuePart = ValueReference | ValueNonReference | ValueResolved; + +type ValueReference = { + type: 'value-reference', + raw: string, +} + +type ValueResolved = { + type: 'value-resolved', + raw: string, + value: string, +} + +type ValueNonReference = { + type: 'value-non-reference', + value: string, +} + +function parseReferences(valueWithReferences: unknown): Array { + if (typeof valueWithReferences !== 'string') { + return []; + } + + if (valueWithReferences.indexOf('{') === -1) { + return []; + } + + const result: Array = []; + let hasReferences = false; + + let inReference = false; + let buf = ''; + + for (let index = 0; index < valueWithReferences.length; index++) { + const char = valueWithReferences[index]; + + switch (char) { + case '{': + if (inReference) { + throw new Error('Unexpected "{" in "' + valueWithReferences + '" at ' + index); + } + + if (buf.length > 0) { + result.push({ + type: 'value-non-reference', + value: buf, + }); + buf = ''; + } + + inReference = true; + break; + case '}': + if (!inReference) { + throw new Error('Unexpected "}" in "' + valueWithReferences + '" at ' + index); + } + + if (buf.length === 0) { + throw new Error('Empty alias "{}" in "' + valueWithReferences + '" at ' + index); + } + + { + let reference = buf.trim(); + if (reference.slice(-6) === '.value') { + reference = reference.slice(0, -6); + } + + result.push({ + type: 'value-reference', + raw: reference, + }); + buf = ''; + } + + hasReferences = true; + inReference = false; + break; + + default: + buf += char; + break; + } + } + + if (inReference) { + throw new Error('Unexpected end of alias in "' + valueWithReferences + '"'); + } + + if (buf.length > 0) { + result.push({ + type: 'value-non-reference', + value: buf, + }); + } + + if (!hasReferences) { + return []; + } + + return result; +} diff --git a/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/group.ts b/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/group.ts new file mode 100644 index 000000000..b21c3955b --- /dev/null +++ b/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/group.ts @@ -0,0 +1,38 @@ +import { Token } from '../../base/token'; +import { dereferenceTokenValues } from './dereference'; +import { extractStyleDictionaryV3Token, StyleDictionaryV3TokenValue } from './value'; + +export type StyleDictionaryV3TokenGroup = { + [key: string]: StyleDictionaryV3TokenGroup | StyleDictionaryV3TokenValue; +} + +function extractTokens(node: StyleDictionaryV3TokenGroup, path: Array, filePath: string): Map { + const result: Map = new Map(); + for (const key in node) { + if (Object.hasOwnProperty.call(node, key)) { + const child = Object(node[key]); + if (!child) { + throw new Error('Parsing error'); + } + + if (typeof child['value'] !== 'undefined') { + const token = extractStyleDictionaryV3Token(child, key, path, filePath); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + result.set(token.metadata!.path.join('.'), token ); + continue; + } + + for (const [tokenPath, token] of extractTokens(child, [...path, key], filePath).entries()) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + result.set(tokenPath, token); + continue; + } + } + } + + return result; +} + +export function extractStyleDictionaryV3Tokens(node: StyleDictionaryV3TokenGroup, filePath: string): Map { + return dereferenceTokenValues(extractTokens(node, [], filePath)); +} diff --git a/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/value.ts b/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/value.ts new file mode 100644 index 000000000..6a854e099 --- /dev/null +++ b/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/value.ts @@ -0,0 +1,115 @@ +import { TokenTransformOptions } from '../../base/token'; +import valueParser from 'postcss-value-parser'; + +export type StyleDictionaryV3TokenValue = { + cssValue(transformOptions?: TokenTransformOptions): string + // The value of the design token. This can be any type of data, a hex string, an integer, a file path to a file, even an object or array. + value: unknown + // Usually the name for a design token is generated with a name transform, but you can write your own if you choose. By default Style Dictionary will add a default name which is the key of the design token object. + name?: string + // The comment attribute will show up in a code comment in output files if the format supports it. + comment?: string + // This is used in formats that support override-able or themeable values like the !default flag in Sass. + themeable?: boolean + // Extra information about the design token you want to include. Attribute transforms will modify this object so be careful + attributes?: unknown + + // Set by the token parser + metadata?: { + // A default name of the design token that is set to the key of the design token. This is only added if you do not provide one. + name?: string + // The object path of the design token. + path: Array + // The file path of the file the token is defined in. This file path is derived from the source or include file path arrays defined in the configuration. + filePath: string + // If the token is from a file defined in the source array as opposed to include in the configuration. + isSource: boolean + } +} + +export function extractStyleDictionaryV3Token(node: Record, key: string, path: Array, filePath: string): StyleDictionaryV3TokenValue { + if (typeof node['value'] === 'undefined') { + throw new Error('Token value is undefined for "' + [...path, key].join('.') + '"'); + } + + switch (typeof node['value']) { + case 'string': + case 'number': + break; + + default: + throw new Error('Token value is not a string or a number for "' + [...path, key].join('.') + '"'); + } + + const value = String(node['value']); + + return { + value: value, + cssValue: (transformOptions?: TokenTransformOptions) => { + return applyTransformsToValue(value, transformOptions); + }, + name: String(node['name'] ?? '') || key, + comment: String(node['comment'] ?? '') || undefined, + metadata: { + name: String(node['name'] ?? '') ? key : undefined, + path: [...path, key], + filePath: filePath, + isSource: true, + }, + }; +} + +export function applyTransformsToValue(value: string|undefined|null, transformOptions?: TokenTransformOptions): string { + if (!value) { + return ''; + } + + if (!transformOptions) { + return value; + } + + if (transformOptions.toUnit) { + const dimension = valueParser.unit(value ?? ''); + if (!dimension || dimension.unit === transformOptions.toUnit) { + return `${value}`; + } + + if (dimension.unit === 'rem' && transformOptions.toUnit === 'px') { + return remToPx(parseFloat(dimension.number), transformOptions.pluginOptions?.rootFontSize ?? 16); + } + + if (dimension.unit === 'px' && transformOptions.toUnit === 'rem') { + return pxToRem(parseFloat(dimension.number), transformOptions.pluginOptions?.rootFontSize ?? 16); + } + } + + return value; +} + +function remToPx(value: number, rootFontSize: number): string { + return `${formatFloat(value * rootFontSize)}px`; +} + +function pxToRem(value: number, rootFontSize: number): string { + return `${formatFloat(value / rootFontSize)}rem`; +} + +function formatFloat(value: number): string { + if (Number.isInteger(value)) { + return value.toString(); + } + + let fixedPrecision = value.toFixed(5); + for (let i = fixedPrecision.length; i > 0; i--) { + if (fixedPrecision[i] === '.') { + break; + } + + if (fixedPrecision[i] !== '0') { + fixedPrecision = fixedPrecision.slice(0, i + 1); + continue; + } + } + + return fixedPrecision; +} diff --git a/plugins/postcss-design-tokens/src/data-formats/token.ts b/plugins/postcss-design-tokens/src/data-formats/token.ts new file mode 100644 index 000000000..704cb796c --- /dev/null +++ b/plugins/postcss-design-tokens/src/data-formats/token.ts @@ -0,0 +1,9 @@ +import { Token } from './base/token'; + +export function mergeTokens(a: Map, b: Map): Map { + const result = new Map(a); + for (const [key, value] of b) { + result.set(key, value); + } + return result; +} diff --git a/plugins/postcss-design-tokens/src/data-formats/toposort/toposort.ts b/plugins/postcss-design-tokens/src/data-formats/toposort/toposort.ts new file mode 100644 index 000000000..73b773d40 --- /dev/null +++ b/plugins/postcss-design-tokens/src/data-formats/toposort/toposort.ts @@ -0,0 +1,104 @@ +// Toposort - Topological sorting for node.js +// Copyright (c) 2012 by Marcel Klehr +// MIT LICENSE +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +export function toposort(nodes: Array, edges: Array>): Array { + let cursor = nodes.length; + const sorted: Array = new Array(cursor); + const visited: Record = {}; + let i = cursor; + // Better data structures make algorithm much faster. + const outgoingEdges = makeOutgoingEdges(edges); + const nodesHash = makeNodesHash(nodes); + + // check for unknown nodes + edges.forEach(function (edge) { + if (!nodesHash.has(edge[0]) || !nodesHash.has(edge[1])) { + throw new Error('Unknown token. Make sure to provide all tokens used in aliases.'); + } + }); + + while (i--) { + if (!visited[i]) { + visit(nodes[i], i, new Set()); + } + } + + return sorted; + + function visit(node: string, j: number, predecessors: Set) { + if (predecessors.has(node)) { + let nodeRep; + try { + nodeRep = ', token was: ' + JSON.stringify(node); + } catch (e) { + nodeRep = ''; + } + throw new Error('Cyclic dependency' + nodeRep); + } + + if (!nodesHash.has(node)) { + throw new Error('Found unknown token. Make sure to provided all involved tokens. Unknown token: ' + JSON.stringify(node)); + } + + if (visited[j]) { + return; + } + visited[j] = true; + + let outgoing = outgoingEdges.get(node) || new Set(); + outgoing = Array.from(outgoing); + + // eslint-disable-next-line no-cond-assign + if (j = outgoing.length) { + predecessors.add(node); + do { + const child = outgoing[--j]; + visit(child, nodesHash.get(child), predecessors); + } while (j); + predecessors.delete(node); + } + + sorted[--cursor] = node; + } +} + +function makeOutgoingEdges(arr: Array>) { + const edges = new Map(); + for (let i = 0, len = arr.length; i < len; i++) { + const edge = arr[i]; + if (!edges.has(edge[0])) { + edges.set(edge[0], new Set()); + } + if (!edges.has(edge[1])) { + edges.set(edge[1], new Set()); + } + edges.get(edge[0]).add(edge[1]); + } + return edges; +} + +function makeNodesHash(arr: Array) { + const res = new Map(); + for (let i = 0, len = arr.length; i < len; i++) { + res.set(arr[i], i); + } + return res; +} diff --git a/plugins/postcss-design-tokens/src/index.ts b/plugins/postcss-design-tokens/src/index.ts new file mode 100644 index 000000000..0cd1ecada --- /dev/null +++ b/plugins/postcss-design-tokens/src/index.ts @@ -0,0 +1,83 @@ +import type { Node, PluginCreator } from 'postcss'; +import { Token } from './data-formats/base/token'; +import { tokensFromImport } from './data-formats/parse-import'; +import { mergeTokens } from './data-formats/token'; +import { parsePluginOptions, pluginOptions } from './options'; +import { onCSSValue } from './values'; + + +const creator: PluginCreator = (opts?: pluginOptions) => { + const options = parsePluginOptions(opts); + + return { + postcssPlugin: 'postcss-design-tokens', + prepare() { + let tokens = new Map(); + let importedFiles = new Set(); + + return { + OnceExit() { + tokens = new Map(); + importedFiles = new Set(); + }, + Once: async (root, { result }) => { + const designTokenAtRules: Array<{filePath: string, params: string, node: Node}> = []; + root.walkAtRules('design-tokens', (atRule) => { + if (!atRule?.source?.input?.file) { + return; + } + + const filePath = atRule.source.input.file; + const params = atRule.params; + atRule.remove(); + + designTokenAtRules.push({ + filePath: filePath, + params: params, + node: atRule, + }); + }); + + for (const atRule of designTokenAtRules.values()) { + let importResult: { filePath: string, tokens: Map }|false; + try { + importResult = await tokensFromImport(options.is, atRule.filePath, atRule.params, importedFiles); + if (!importResult) { + continue; + } + } catch (e) { + atRule.node.warn(result, `Failed to import design tokens from "${atRule.params}" with error:\n\t` + (e as Error).message); + continue; + } + + result.messages.push({ + type: 'dependency', + plugin: 'postcss-design-tokens', + file: importResult.filePath, + parent: atRule.filePath, + }); + + tokens = mergeTokens(tokens, importResult.tokens); + } + }, + Declaration(decl, { result }) { + if (decl.value.indexOf('design-token') === -1) { + return; + } + + const modifiedValue = onCSSValue(tokens, result, decl, options); + if (modifiedValue === decl.value) { + return; + } + + decl.value = modifiedValue; + }, + }; + }, + }; +}; + +creator.postcss = true; + +export default creator; + diff --git a/plugins/postcss-design-tokens/src/options.ts b/plugins/postcss-design-tokens/src/options.ts new file mode 100644 index 000000000..adebc06b5 --- /dev/null +++ b/plugins/postcss-design-tokens/src/options.ts @@ -0,0 +1,50 @@ +import { DEFAULT_CONDITION } from './constants'; + +export type pluginOptions = { + is?: Array + unitsAndValues?: { + rootFontSize?: number + } +} + +export type parsedPluginOptions = { + is: Array + unitsAndValues: { + rootFontSize: number + } +} + +export function parsePluginOptions(opts?: pluginOptions): parsedPluginOptions { + const options: parsedPluginOptions = { + is: [DEFAULT_CONDITION], + unitsAndValues: { + rootFontSize: 16, + }, + }; + + if (!opts) { + return options; + } + + if (typeof opts !== 'object') { + return options; + } + + if (Array.isArray(opts.is)) { + options.is = opts.is.filter((x) => typeof x === 'string'); + } + + if (options.is.length === 0) { + options.is = [DEFAULT_CONDITION]; + } + + if (typeof opts.unitsAndValues === 'object' && typeof opts.unitsAndValues.rootFontSize === 'number' && isPositiveAndNotZero(opts.unitsAndValues.rootFontSize)) { + options.unitsAndValues.rootFontSize = opts.unitsAndValues.rootFontSize; + } + + return options; +} + +function isPositiveAndNotZero(x: number) { + return x > 0 && x !== Infinity; +} diff --git a/plugins/postcss-design-tokens/src/values.ts b/plugins/postcss-design-tokens/src/values.ts new file mode 100644 index 000000000..f3447bcf6 --- /dev/null +++ b/plugins/postcss-design-tokens/src/values.ts @@ -0,0 +1,61 @@ +import type { Declaration, Result } from 'postcss'; +import valueParser from 'postcss-value-parser'; +import { Token, TokenTransformOptions } from './data-formats/base/token'; +import { parsedPluginOptions } from './options'; + +export function onCSSValue(tokens: Map, result: Result, decl: Declaration, opts: parsedPluginOptions) { + const valueAST = valueParser(decl.value); + + valueAST.walk(node => { + if (node.type !== 'function' || node.value !== 'design-token') { + return; + } + + if (!node.nodes || node.nodes.length === 0) { + decl.warn(result, 'Expected at least a single string literal for the design-token function.'); + return; + } + + if (node.nodes[0].type !== 'string') { + decl.warn(result, 'Expected at least a single string literal for the design-token function.'); + return; + } + + const tokenName = node.nodes[0].value; + const replacement = tokens.get(tokenName); + if (!replacement) { + decl.warn(result, `design-token: "${tokenName}" is not configured.`); + return; + } + + const remainingNodes = node.nodes.slice(1).filter(x => x.type === 'word'); + if (!remainingNodes.length) { + node.value = replacement.cssValue(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (node as any).nodes = undefined; + return; + } + + const transformOptions: TokenTransformOptions = { + pluginOptions: opts.unitsAndValues, + }; + for (let i = 0; i < remainingNodes.length; i++) { + if ( + remainingNodes[i].type === 'word' && + remainingNodes[i].value === 'to' && + remainingNodes[i + 1] && + remainingNodes[i + 1].type === 'word' && + ['px', 'rem'].includes(remainingNodes[i + 1].value) + ) { + transformOptions.toUnit = remainingNodes[i + 1].value; + i++; + } + } + + node.value = replacement.cssValue(transformOptions); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (node as any).nodes = undefined; + }); + + return String(valueAST); +} diff --git a/plugins/postcss-design-tokens/test/_import.mjs b/plugins/postcss-design-tokens/test/_import.mjs new file mode 100644 index 000000000..97e23a010 --- /dev/null +++ b/plugins/postcss-design-tokens/test/_import.mjs @@ -0,0 +1,6 @@ +import assert from 'assert'; +import plugin from '@csstools/postcss-design-tokens'; +plugin(); + +assert.ok(plugin.postcss, 'should have "postcss flag"'); +assert.equal(typeof plugin, 'function', 'should return a function'); diff --git a/plugins/postcss-design-tokens/test/_require.cjs b/plugins/postcss-design-tokens/test/_require.cjs new file mode 100644 index 000000000..e48299ce9 --- /dev/null +++ b/plugins/postcss-design-tokens/test/_require.cjs @@ -0,0 +1,6 @@ +const assert = require('assert'); +const plugin = require('@csstools/postcss-design-tokens'); +plugin(); + +assert.ok(plugin.postcss, 'should have "postcss flag"'); +assert.equal(typeof plugin, 'function', 'should return a function'); diff --git a/plugins/postcss-design-tokens/test/basic.css b/plugins/postcss-design-tokens/test/basic.css new file mode 100644 index 000000000..9fc8643f7 --- /dev/null +++ b/plugins/postcss-design-tokens/test/basic.css @@ -0,0 +1,54 @@ +@import url('./imported.css'); +@design-tokens url('./tokens/basic.json') format('style-dictionary3'); + +.foo { + font-family: design-token('font.family.base'); + font-size: design-token('size.font.small'); + color: design-token('color.font.base'); +} + +.card { + background-color: design-token('card.background'); + color: design-token('card.foreground'); + color: design-token( 'card.foreground'); + color: design-token('card.foreground' ); + color: design-token( + /* a foreground color */ + 'card.foreground' + ); + color: design-token( + 'card.foreground' + /* a foreground color */ + ); +} + +.px-to-px { + padding-bottom: design-token('space.small' to px); + padding-bottom: design-token('space.default' to px); + padding-bottom: design-token('space.large' to px); +} + +.px-to-rem { + padding-bottom: design-token('space.small' to rem); + padding-bottom: design-token('space.default' to rem); + padding-bottom: design-token('space.large' to rem); +} + +.rem-to-rem { + padding-bottom: design-token('space.small-b' to rem); + padding-bottom: design-token('space.default-b' to rem); + padding-bottom: design-token('space.large-b' to rem); +} + +.rem-to-px { + padding-bottom: design-token('space.small-b' to px); + padding-bottom: design-token('space.default-b' to px); + padding-bottom: design-token('space.large-b' to px); +} + +.invalid-conversion { + color: design-token('card.foreground' to rem); + color: design-token('card.foreground' to px); + color: design-token('space.lh' to rem); + color: design-token('space.lh' to px); +} diff --git a/plugins/postcss-design-tokens/test/basic.expect.css b/plugins/postcss-design-tokens/test/basic.expect.css new file mode 100644 index 000000000..d4b93d305 --- /dev/null +++ b/plugins/postcss-design-tokens/test/basic.expect.css @@ -0,0 +1,39 @@ +.foo { + font-family: Helvetica sans; + font-size: 10; + color: #111111; +} +.card { + background-color: blue; + color: red; + color: red; + color: red; + color: red; + color: red; +} +.px-to-px { + padding-bottom: 8px; + padding-bottom: 18px; + padding-bottom: 32px; +} +.px-to-rem { + padding-bottom: 0.5rem; + padding-bottom: 1.1rem; + padding-bottom: 2rem; +} +.rem-to-rem { + padding-bottom: 0.5rem; + padding-bottom: 1.125rem; + padding-bottom: 2rem; +} +.rem-to-px { + padding-bottom: 8px; + padding-bottom: 18px; + padding-bottom: 32px; +} +.invalid-conversion { + color: red; + color: red; + color: 1lh; + color: 1lh; +} diff --git a/plugins/postcss-design-tokens/test/basic.rootFontSize-20.expect.css b/plugins/postcss-design-tokens/test/basic.rootFontSize-20.expect.css new file mode 100644 index 000000000..d1915ac36 --- /dev/null +++ b/plugins/postcss-design-tokens/test/basic.rootFontSize-20.expect.css @@ -0,0 +1,39 @@ +.foo { + font-family: Helvetica sans; + font-size: 10; + color: #111111; +} +.card { + background-color: blue; + color: red; + color: red; + color: red; + color: red; + color: red; +} +.px-to-px { + padding-bottom: 8px; + padding-bottom: 18px; + padding-bottom: 32px; +} +.px-to-rem { + padding-bottom: 0.4rem; + padding-bottom: 0.9rem; + padding-bottom: 1.6rem; +} +.rem-to-rem { + padding-bottom: 0.5rem; + padding-bottom: 1.125rem; + padding-bottom: 2rem; +} +.rem-to-px { + padding-bottom: 10px; + padding-bottom: 22.5px; + padding-bottom: 40px; +} +.invalid-conversion { + color: red; + color: red; + color: 1lh; + color: 1lh; +} diff --git a/plugins/postcss-design-tokens/test/basic.rootFontSize-NaN.expect.css b/plugins/postcss-design-tokens/test/basic.rootFontSize-NaN.expect.css new file mode 100644 index 000000000..d4b93d305 --- /dev/null +++ b/plugins/postcss-design-tokens/test/basic.rootFontSize-NaN.expect.css @@ -0,0 +1,39 @@ +.foo { + font-family: Helvetica sans; + font-size: 10; + color: #111111; +} +.card { + background-color: blue; + color: red; + color: red; + color: red; + color: red; + color: red; +} +.px-to-px { + padding-bottom: 8px; + padding-bottom: 18px; + padding-bottom: 32px; +} +.px-to-rem { + padding-bottom: 0.5rem; + padding-bottom: 1.1rem; + padding-bottom: 2rem; +} +.rem-to-rem { + padding-bottom: 0.5rem; + padding-bottom: 1.125rem; + padding-bottom: 2rem; +} +.rem-to-px { + padding-bottom: 8px; + padding-bottom: 18px; + padding-bottom: 32px; +} +.invalid-conversion { + color: red; + color: red; + color: 1lh; + color: 1lh; +} diff --git a/plugins/postcss-design-tokens/test/basic.rootFontSize-invalid.expect.css b/plugins/postcss-design-tokens/test/basic.rootFontSize-invalid.expect.css new file mode 100644 index 000000000..d4b93d305 --- /dev/null +++ b/plugins/postcss-design-tokens/test/basic.rootFontSize-invalid.expect.css @@ -0,0 +1,39 @@ +.foo { + font-family: Helvetica sans; + font-size: 10; + color: #111111; +} +.card { + background-color: blue; + color: red; + color: red; + color: red; + color: red; + color: red; +} +.px-to-px { + padding-bottom: 8px; + padding-bottom: 18px; + padding-bottom: 32px; +} +.px-to-rem { + padding-bottom: 0.5rem; + padding-bottom: 1.1rem; + padding-bottom: 2rem; +} +.rem-to-rem { + padding-bottom: 0.5rem; + padding-bottom: 1.125rem; + padding-bottom: 2rem; +} +.rem-to-px { + padding-bottom: 8px; + padding-bottom: 18px; + padding-bottom: 32px; +} +.invalid-conversion { + color: red; + color: red; + color: 1lh; + color: 1lh; +} diff --git a/plugins/postcss-design-tokens/test/errors.css b/plugins/postcss-design-tokens/test/errors.css new file mode 100644 index 000000000..92fb33bab --- /dev/null +++ b/plugins/postcss-design-tokens/test/errors.css @@ -0,0 +1,4 @@ +@design-tokens url('./tokens/does-not-exist.json') format('style-dictionary3'); +@design-tokens url('./tokens/basic.json') format('does not exist'); +@design-tokens url('./tokens/circular.json') format('style-dictionary3'); +@design-tokens url('./tokens/missing.json') format('style-dictionary3'); diff --git a/plugins/postcss-design-tokens/test/errors.expect.css b/plugins/postcss-design-tokens/test/errors.expect.css new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/plugins/postcss-design-tokens/test/errors.expect.css @@ -0,0 +1 @@ + diff --git a/plugins/postcss-design-tokens/test/examples/example-conditional.css b/plugins/postcss-design-tokens/test/examples/example-conditional.css new file mode 100644 index 000000000..8592a21da --- /dev/null +++ b/plugins/postcss-design-tokens/test/examples/example-conditional.css @@ -0,0 +1,6 @@ +@design-tokens url('./tokens-light.json') format('style-dictionary3'); +@design-tokens url('./tokens-dark.json') when('dark') format('style-dictionary3'); + +.foo { + color: design-token('color.background.primary'); +} diff --git a/plugins/postcss-design-tokens/test/examples/example-conditional.dark.expect.css b/plugins/postcss-design-tokens/test/examples/example-conditional.dark.expect.css new file mode 100644 index 000000000..cab059366 --- /dev/null +++ b/plugins/postcss-design-tokens/test/examples/example-conditional.dark.expect.css @@ -0,0 +1,3 @@ +.foo { + color: #000; +} diff --git a/plugins/postcss-design-tokens/test/examples/example-conditional.expect.css b/plugins/postcss-design-tokens/test/examples/example-conditional.expect.css new file mode 100644 index 000000000..33a418a59 --- /dev/null +++ b/plugins/postcss-design-tokens/test/examples/example-conditional.expect.css @@ -0,0 +1,3 @@ +.foo { + color: #fff; +} diff --git a/plugins/postcss-design-tokens/test/examples/example.css b/plugins/postcss-design-tokens/test/examples/example.css new file mode 100644 index 000000000..e4e3e2c99 --- /dev/null +++ b/plugins/postcss-design-tokens/test/examples/example.css @@ -0,0 +1,8 @@ +@design-tokens url('./tokens.json') format('style-dictionary3'); + +.foo { + color: design-token('color.background.primary'); + padding-top: design-token('size.spacing.small'); + padding-left: design-token('size.spacing.small' to px); + padding-bottom: design-token('size.spacing.small' to rem); +} diff --git a/plugins/postcss-design-tokens/test/examples/example.expect.css b/plugins/postcss-design-tokens/test/examples/example.expect.css new file mode 100644 index 000000000..5c5964adb --- /dev/null +++ b/plugins/postcss-design-tokens/test/examples/example.expect.css @@ -0,0 +1,6 @@ +.foo { + color: #fff; + padding-top: 16px; + padding-left: 16px; + padding-bottom: 1rem; +} diff --git a/plugins/postcss-design-tokens/test/examples/example.rootFontSize-20.expect.css b/plugins/postcss-design-tokens/test/examples/example.rootFontSize-20.expect.css new file mode 100644 index 000000000..6b28e7c0e --- /dev/null +++ b/plugins/postcss-design-tokens/test/examples/example.rootFontSize-20.expect.css @@ -0,0 +1,6 @@ +.foo { + color: #fff; + padding-top: 16px; + padding-left: 16px; + padding-bottom: 0.8rem; +} diff --git a/plugins/postcss-design-tokens/test/examples/tokens-dark.json b/plugins/postcss-design-tokens/test/examples/tokens-dark.json new file mode 100644 index 000000000..09de442b7 --- /dev/null +++ b/plugins/postcss-design-tokens/test/examples/tokens-dark.json @@ -0,0 +1,7 @@ +{ + "color": { + "background": { + "primary": { "value": "#000" } + } + } +} diff --git a/plugins/postcss-design-tokens/test/examples/tokens-light.json b/plugins/postcss-design-tokens/test/examples/tokens-light.json new file mode 100644 index 000000000..9776bbaed --- /dev/null +++ b/plugins/postcss-design-tokens/test/examples/tokens-light.json @@ -0,0 +1,7 @@ +{ + "color": { + "background": { + "primary": { "value": "#fff" } + } + } +} diff --git a/plugins/postcss-design-tokens/test/examples/tokens.json b/plugins/postcss-design-tokens/test/examples/tokens.json new file mode 100644 index 000000000..1ff537046 --- /dev/null +++ b/plugins/postcss-design-tokens/test/examples/tokens.json @@ -0,0 +1,12 @@ +{ + "color": { + "background": { + "primary": { "value": "#fff" } + } + }, + "size": { + "spacing": { + "small": { "value": "16px" } + } + } +} diff --git a/plugins/postcss-design-tokens/test/imported.css b/plugins/postcss-design-tokens/test/imported.css new file mode 100644 index 000000000..6c68c5991 --- /dev/null +++ b/plugins/postcss-design-tokens/test/imported.css @@ -0,0 +1 @@ +@design-tokens url('../../../node_modules/style-dictionary-design-tokens-example/style-dictionary.tokens.json') format('style-dictionary3'); diff --git a/plugins/postcss-design-tokens/test/is.css b/plugins/postcss-design-tokens/test/is.css new file mode 100644 index 000000000..78b783e71 --- /dev/null +++ b/plugins/postcss-design-tokens/test/is.css @@ -0,0 +1,12 @@ +@design-tokens url('./tokens/color_dark_branded-blue.tokens.json') when('dark' 'branded-blue') format('style-dictionary3'); +@design-tokens url('./tokens/color_light_branded-blue.tokens.json') when('light' 'branded-blue') format('style-dictionary3'); +@design-tokens url('./tokens/color_dark_branded-green.tokens.json') when('dark' 'branded-green') format('style-dictionary3'); +@design-tokens url('./tokens/color_light_branded-green.tokens.json') when('light' 'branded-green') format('style-dictionary3'); +@design-tokens url('./tokens/size_mobile.tokens.json') when('mobile') format('style-dictionary3'); +@design-tokens url('./tokens/size_tablet.tokens.json') when('tablet') format('style-dictionary3'); +@design-tokens url('./tokens/size_desktop.tokens.json') when('desktop') format('style-dictionary3'); + +.foo { + color: design-token('color'); + font-size: design-token('size'); +} diff --git a/plugins/postcss-design-tokens/test/is.expect.css b/plugins/postcss-design-tokens/test/is.expect.css new file mode 100644 index 000000000..6b837bc2b --- /dev/null +++ b/plugins/postcss-design-tokens/test/is.expect.css @@ -0,0 +1,4 @@ +.foo { + color: rgb(0, 120, 0); + font-size: 18px; +} diff --git a/plugins/postcss-design-tokens/test/tokens/basic.json b/plugins/postcss-design-tokens/test/tokens/basic.json new file mode 100644 index 000000000..1ec411322 --- /dev/null +++ b/plugins/postcss-design-tokens/test/tokens/basic.json @@ -0,0 +1,45 @@ +{ + "font": { + "family": { + "sans": { "value": "sans" }, + "helvetica": { "value": "Helvetica" }, + "serif": { "value": "serif" }, + "base": { "value": "{ font.family.helvetica.value} {font.family.sans.value}" } + } + }, + "card": { + "foreground": { "value": "{logical-color.foreground}" }, + "background": { "value": "{logical-color.background}" } + }, + "base-color": { + "red": { "value": "red" }, + "blue": { "value": "blue" } + }, + "logical-color": { + "foreground": { "value": "{base-color.red}" }, + "background": { "value": "{base-color.blue}" } + }, + "space": { + "small": { + "value": "8px" + }, + "default": { + "value": "18px" + }, + "large": { + "value": "32px" + }, + "small-b": { + "value": "0.5rem" + }, + "default-b": { + "value": "1.125rem" + }, + "large-b": { + "value": "2rem" + }, + "lh": { + "value": "1lh" + } + } +} diff --git a/plugins/postcss-design-tokens/test/tokens/circular.json b/plugins/postcss-design-tokens/test/tokens/circular.json new file mode 100644 index 000000000..805827672 --- /dev/null +++ b/plugins/postcss-design-tokens/test/tokens/circular.json @@ -0,0 +1,6 @@ +{ + "a": { + "1": { "value": "{a.2}" }, + "2": { "value": "{a.1}" } + } +} diff --git a/plugins/postcss-design-tokens/test/tokens/color_dark_branded-blue.tokens.json b/plugins/postcss-design-tokens/test/tokens/color_dark_branded-blue.tokens.json new file mode 100644 index 000000000..8554a449d --- /dev/null +++ b/plugins/postcss-design-tokens/test/tokens/color_dark_branded-blue.tokens.json @@ -0,0 +1,5 @@ +{ + "color": { + "value": "rgb(0, 0, 120)" + } +} diff --git a/plugins/postcss-design-tokens/test/tokens/color_dark_branded-green.tokens.json b/plugins/postcss-design-tokens/test/tokens/color_dark_branded-green.tokens.json new file mode 100644 index 000000000..aac06ff82 --- /dev/null +++ b/plugins/postcss-design-tokens/test/tokens/color_dark_branded-green.tokens.json @@ -0,0 +1,5 @@ +{ + "color": { + "value": "rgb(0, 120, 0)" + } +} diff --git a/plugins/postcss-design-tokens/test/tokens/color_light_branded-blue.tokens.json b/plugins/postcss-design-tokens/test/tokens/color_light_branded-blue.tokens.json new file mode 100644 index 000000000..8569a6d3a --- /dev/null +++ b/plugins/postcss-design-tokens/test/tokens/color_light_branded-blue.tokens.json @@ -0,0 +1,5 @@ +{ + "color": { + "value": "rgb(50, 50, 250)" + } +} diff --git a/plugins/postcss-design-tokens/test/tokens/color_light_branded-green.tokens.json b/plugins/postcss-design-tokens/test/tokens/color_light_branded-green.tokens.json new file mode 100644 index 000000000..3e9ac3d69 --- /dev/null +++ b/plugins/postcss-design-tokens/test/tokens/color_light_branded-green.tokens.json @@ -0,0 +1,5 @@ +{ + "color": { + "value": "rgb(50, 250, 50)" + } +} diff --git a/plugins/postcss-design-tokens/test/tokens/missing.json b/plugins/postcss-design-tokens/test/tokens/missing.json new file mode 100644 index 000000000..e08a0b0bc --- /dev/null +++ b/plugins/postcss-design-tokens/test/tokens/missing.json @@ -0,0 +1,5 @@ +{ + "a": { + "1": { "value": "{a.2}" } + } +} diff --git a/plugins/postcss-design-tokens/test/tokens/size_desktop.tokens.json b/plugins/postcss-design-tokens/test/tokens/size_desktop.tokens.json new file mode 100644 index 000000000..0e36ff29e --- /dev/null +++ b/plugins/postcss-design-tokens/test/tokens/size_desktop.tokens.json @@ -0,0 +1,5 @@ +{ + "size": { + "value": "20px" + } +} diff --git a/plugins/postcss-design-tokens/test/tokens/size_mobile.tokens.json b/plugins/postcss-design-tokens/test/tokens/size_mobile.tokens.json new file mode 100644 index 000000000..06069df89 --- /dev/null +++ b/plugins/postcss-design-tokens/test/tokens/size_mobile.tokens.json @@ -0,0 +1,5 @@ +{ + "size": { + "value": "16px" + } +} diff --git a/plugins/postcss-design-tokens/test/tokens/size_tablet.tokens.json b/plugins/postcss-design-tokens/test/tokens/size_tablet.tokens.json new file mode 100644 index 000000000..8102cac0c --- /dev/null +++ b/plugins/postcss-design-tokens/test/tokens/size_tablet.tokens.json @@ -0,0 +1,5 @@ +{ + "size": { + "value": "18px" + } +} diff --git a/plugins/postcss-design-tokens/test/tokens/value-parsing-a.json b/plugins/postcss-design-tokens/test/tokens/value-parsing-a.json new file mode 100644 index 000000000..f3970cd27 --- /dev/null +++ b/plugins/postcss-design-tokens/test/tokens/value-parsing-a.json @@ -0,0 +1,5 @@ +{ + "token": { + "value": 20 + } +} diff --git a/plugins/postcss-design-tokens/test/tokens/value-parsing-b.json b/plugins/postcss-design-tokens/test/tokens/value-parsing-b.json new file mode 100644 index 000000000..22b517934 --- /dev/null +++ b/plugins/postcss-design-tokens/test/tokens/value-parsing-b.json @@ -0,0 +1,5 @@ +{ + "token": { + "value": "foo" + } +} diff --git a/plugins/postcss-design-tokens/test/tokens/value-parsing-c.json b/plugins/postcss-design-tokens/test/tokens/value-parsing-c.json new file mode 100644 index 000000000..474f5a633 --- /dev/null +++ b/plugins/postcss-design-tokens/test/tokens/value-parsing-c.json @@ -0,0 +1,7 @@ +{ + "token": { + "value": { + "an-object": true + } + } +} diff --git a/plugins/postcss-design-tokens/test/tokens/value-parsing-d.json b/plugins/postcss-design-tokens/test/tokens/value-parsing-d.json new file mode 100644 index 000000000..728e997aa --- /dev/null +++ b/plugins/postcss-design-tokens/test/tokens/value-parsing-d.json @@ -0,0 +1,5 @@ +{ + "token": { + "value": false + } +} diff --git a/plugins/postcss-design-tokens/test/tokens/value-parsing-e.json b/plugins/postcss-design-tokens/test/tokens/value-parsing-e.json new file mode 100644 index 000000000..adac6202e --- /dev/null +++ b/plugins/postcss-design-tokens/test/tokens/value-parsing-e.json @@ -0,0 +1,5 @@ +{ + "token": { + "value": null + } +} diff --git a/plugins/postcss-design-tokens/test/tokens/value-parsing-f.json b/plugins/postcss-design-tokens/test/tokens/value-parsing-f.json new file mode 100644 index 000000000..b16797c95 --- /dev/null +++ b/plugins/postcss-design-tokens/test/tokens/value-parsing-f.json @@ -0,0 +1,5 @@ +{ + "token": { + "value": "\"foo\"" + } +} diff --git a/plugins/postcss-design-tokens/test/tokens/value-parsing-g.json b/plugins/postcss-design-tokens/test/tokens/value-parsing-g.json new file mode 100644 index 000000000..f94530ca5 --- /dev/null +++ b/plugins/postcss-design-tokens/test/tokens/value-parsing-g.json @@ -0,0 +1,5 @@ +{ + "token": { + "value": [20] + } +} diff --git a/plugins/postcss-design-tokens/test/value-parsing-a.css b/plugins/postcss-design-tokens/test/value-parsing-a.css new file mode 100644 index 000000000..7ee8b4fb8 --- /dev/null +++ b/plugins/postcss-design-tokens/test/value-parsing-a.css @@ -0,0 +1,5 @@ +@design-tokens url('./tokens/value-parsing-a.json') format('style-dictionary3'); + +.value-parsing { + order: design-token('token'); +} diff --git a/plugins/postcss-design-tokens/test/value-parsing-a.expect.css b/plugins/postcss-design-tokens/test/value-parsing-a.expect.css new file mode 100644 index 000000000..ed650bb67 --- /dev/null +++ b/plugins/postcss-design-tokens/test/value-parsing-a.expect.css @@ -0,0 +1,3 @@ +.value-parsing { + order: 20; +} diff --git a/plugins/postcss-design-tokens/test/value-parsing-b.css b/plugins/postcss-design-tokens/test/value-parsing-b.css new file mode 100644 index 000000000..d29163c4a --- /dev/null +++ b/plugins/postcss-design-tokens/test/value-parsing-b.css @@ -0,0 +1,5 @@ +@design-tokens url('./tokens/value-parsing-b.json') format('style-dictionary3'); + +.value-parsing { + order: design-token('token'); +} diff --git a/plugins/postcss-design-tokens/test/value-parsing-b.expect.css b/plugins/postcss-design-tokens/test/value-parsing-b.expect.css new file mode 100644 index 000000000..e98253051 --- /dev/null +++ b/plugins/postcss-design-tokens/test/value-parsing-b.expect.css @@ -0,0 +1,3 @@ +.value-parsing { + order: foo; +} diff --git a/plugins/postcss-design-tokens/test/value-parsing-c.css b/plugins/postcss-design-tokens/test/value-parsing-c.css new file mode 100644 index 000000000..57ad788c7 --- /dev/null +++ b/plugins/postcss-design-tokens/test/value-parsing-c.css @@ -0,0 +1,5 @@ +@design-tokens url('./tokens/value-parsing-c.json') format('style-dictionary3'); + +.value-parsing { + order: design-token('token'); +} diff --git a/plugins/postcss-design-tokens/test/value-parsing-c.expect.css b/plugins/postcss-design-tokens/test/value-parsing-c.expect.css new file mode 100644 index 000000000..4ffb47baf --- /dev/null +++ b/plugins/postcss-design-tokens/test/value-parsing-c.expect.css @@ -0,0 +1,3 @@ +.value-parsing { + order: design-token('token'); +} diff --git a/plugins/postcss-design-tokens/test/value-parsing-d.css b/plugins/postcss-design-tokens/test/value-parsing-d.css new file mode 100644 index 000000000..5bcc9dcba --- /dev/null +++ b/plugins/postcss-design-tokens/test/value-parsing-d.css @@ -0,0 +1,5 @@ +@design-tokens url('./tokens/value-parsing-d.json') format('style-dictionary3'); + +.value-parsing { + order: design-token('token'); +} diff --git a/plugins/postcss-design-tokens/test/value-parsing-d.expect.css b/plugins/postcss-design-tokens/test/value-parsing-d.expect.css new file mode 100644 index 000000000..4ffb47baf --- /dev/null +++ b/plugins/postcss-design-tokens/test/value-parsing-d.expect.css @@ -0,0 +1,3 @@ +.value-parsing { + order: design-token('token'); +} diff --git a/plugins/postcss-design-tokens/test/value-parsing-e.css b/plugins/postcss-design-tokens/test/value-parsing-e.css new file mode 100644 index 000000000..cabafcf23 --- /dev/null +++ b/plugins/postcss-design-tokens/test/value-parsing-e.css @@ -0,0 +1,5 @@ +@design-tokens url('./tokens/value-parsing-e.json') format('style-dictionary3'); + +.value-parsing { + order: design-token('token'); +} diff --git a/plugins/postcss-design-tokens/test/value-parsing-e.expect.css b/plugins/postcss-design-tokens/test/value-parsing-e.expect.css new file mode 100644 index 000000000..4ffb47baf --- /dev/null +++ b/plugins/postcss-design-tokens/test/value-parsing-e.expect.css @@ -0,0 +1,3 @@ +.value-parsing { + order: design-token('token'); +} diff --git a/plugins/postcss-design-tokens/test/value-parsing-f.css b/plugins/postcss-design-tokens/test/value-parsing-f.css new file mode 100644 index 000000000..34a46664b --- /dev/null +++ b/plugins/postcss-design-tokens/test/value-parsing-f.css @@ -0,0 +1,5 @@ +@design-tokens url('./tokens/value-parsing-f.json') format('style-dictionary3'); + +.value-parsing { + order: design-token('token'); +} diff --git a/plugins/postcss-design-tokens/test/value-parsing-f.expect.css b/plugins/postcss-design-tokens/test/value-parsing-f.expect.css new file mode 100644 index 000000000..4c99d32ef --- /dev/null +++ b/plugins/postcss-design-tokens/test/value-parsing-f.expect.css @@ -0,0 +1,3 @@ +.value-parsing { + order: "foo"; +} diff --git a/plugins/postcss-design-tokens/test/value-parsing-g.css b/plugins/postcss-design-tokens/test/value-parsing-g.css new file mode 100644 index 000000000..801cfb750 --- /dev/null +++ b/plugins/postcss-design-tokens/test/value-parsing-g.css @@ -0,0 +1,5 @@ +@design-tokens url('./tokens/value-parsing-g.json') format('style-dictionary3'); + +.value-parsing { + order: design-token('token'); +} diff --git a/plugins/postcss-design-tokens/test/value-parsing-g.expect.css b/plugins/postcss-design-tokens/test/value-parsing-g.expect.css new file mode 100644 index 000000000..4ffb47baf --- /dev/null +++ b/plugins/postcss-design-tokens/test/value-parsing-g.expect.css @@ -0,0 +1,3 @@ +.value-parsing { + order: design-token('token'); +} diff --git a/plugins/postcss-design-tokens/tsconfig.json b/plugins/postcss-design-tokens/tsconfig.json new file mode 100644 index 000000000..64a39afa7 --- /dev/null +++ b/plugins/postcss-design-tokens/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "declarationDir": ".", + "module": "es2020", + "strict": true, + }, + "include": ["./src/**/*"], + "exclude": ["dist"], +} diff --git a/plugins/postcss-gradients-interpolation-method/README.md b/plugins/postcss-gradients-interpolation-method/README.md index fbb284fd8..690e33012 100644 --- a/plugins/postcss-gradients-interpolation-method/README.md +++ b/plugins/postcss-gradients-interpolation-method/README.md @@ -1,9 +1,6 @@ # PostCSS Gradients Interpolation Method [PostCSS Logo][postcss] -[npm version][npm-url] -[CSS Standard Status][css-url] -[Build Status][cli-url] -[Discord][discord] +[npm version][npm-url] [CSS Standard Status][css-url] [Build Status][cli-url] [Discord][discord] [PostCSS Gradients Interpolation Method] lets you use different interpolation methods in CSS gradient functions following [CSS Specification]. diff --git a/plugins/postcss-stepped-value-functions/README.md b/plugins/postcss-stepped-value-functions/README.md index aaa9a4dbd..3cd235169 100644 --- a/plugins/postcss-stepped-value-functions/README.md +++ b/plugins/postcss-stepped-value-functions/README.md @@ -1,9 +1,6 @@ # PostCSS Stepped Value Functions [PostCSS Logo][postcss] -[npm version][npm-url] -[CSS Standard Status][css-url] -[Build Status][cli-url] -[Discord][discord] +[npm version][npm-url] [CSS Standard Status][css-url] [Build Status][cli-url] [Discord][discord] [PostCSS Stepped Value Functions] lets you use `round`, `rem` and `mod` stepped value functions, following the [CSS Values 4]. diff --git a/plugins/postcss-trigonometric-functions/README.md b/plugins/postcss-trigonometric-functions/README.md index 5b5973d2b..d52d95fc7 100644 --- a/plugins/postcss-trigonometric-functions/README.md +++ b/plugins/postcss-trigonometric-functions/README.md @@ -1,9 +1,6 @@ # PostCSS Trigonometric Functions [PostCSS Logo][postcss] -[npm version][npm-url] -[CSS Standard Status][css-url] -[Build Status][cli-url] -[Discord][discord] +[npm version][npm-url] [CSS Standard Status][css-url] [Build Status][cli-url] [Discord][discord] [PostCSS Trigonometric Functions] lets you use `sin`, `cos`, `tan`, `asin`, `acos`, `atan` and `atan2` to be able to compute trigonometric relationships following the [CSS Values 4] specification.