├── .gitattributes ├── .github └── workflows │ ├── ci.yaml │ └── release.yml ├── .gitignore ├── .npmignore ├── .prettierrc ├── .prettierrc.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── declaration.d.ts ├── examples ├── cli-options.json └── tokens-examples │ ├── design-tokens.tokens.json │ ├── dtcg.json │ ├── simple-plugin-export │ ├── tokens-studio-2.json │ └── tokens-studio-example.json ├── global.d.ts ├── manifest.json ├── package.json ├── pnpm-lock.yaml ├── readme-assets ├── fig1.webp ├── fig10.webp ├── fig11.webp ├── fig12.webp ├── fig13.webp ├── fig2.webp ├── fig3.webp ├── fig4.webp ├── fig5.webp ├── fig6.webp ├── fig7.webp ├── fig8.webp ├── fig9.webp ├── preview.webp └── rename-styles.gif ├── src ├── app │ ├── api │ │ ├── downloadTokensFile.ts │ │ ├── pluginApiResolver.ts │ │ └── servers │ │ │ ├── githubPullRequest.ts │ │ │ ├── pushToCustomURL.ts │ │ │ ├── pushToGithub.ts │ │ │ ├── pushToGitlab.ts │ │ │ └── pushToJSONBin.ts │ ├── components │ │ ├── StatusPicture │ │ │ ├── index.tsx │ │ │ └── styles.module.scss │ │ └── Toast │ │ │ ├── index.tsx │ │ │ └── styles.module.scss │ ├── container.tsx │ ├── controller │ │ ├── changeUIFrameSize.ts │ │ ├── checkForVariables.ts │ │ ├── config.ts │ │ ├── getStorageConfig.ts │ │ └── index.ts │ ├── hooks │ │ └── useDidUpdate.ts │ ├── index.html │ ├── index.tsx │ ├── styles.module.scss │ └── views │ │ ├── CodePreviewView │ │ ├── index.tsx │ │ └── styles.module.scss │ │ ├── EmptyView │ │ ├── index.tsx │ │ └── styles.module.scss │ │ ├── LoadingView │ │ ├── index.tsx │ │ └── styles.module.scss │ │ ├── ServerSettingsView │ │ ├── index.tsx │ │ └── styles.module.scss │ │ └── SettingsView │ │ ├── index.tsx │ │ └── styles.module.scss ├── cli.ts ├── cli │ ├── index.ts │ └── restApiResolver.ts └── common │ ├── export.ts │ ├── resolver.ts │ └── transform │ ├── color │ ├── convertFigmaLinearGradient.ts │ ├── convertRGBA.ts │ └── normilizeRGBAColor.ts │ ├── countTokens.ts │ ├── findByVariableId.ts │ ├── getAliasVariableName.ts │ ├── getTokenKeyName.ts │ ├── getTokensStat.ts │ ├── groupObjectNamesIntoCategories.ts │ ├── mergeStylesIntoTokens.ts │ ├── normalizeValue.spec.ts │ ├── normalizeValue.ts │ ├── normilizeType.ts │ ├── removeDollarSign.ts │ ├── styles │ ├── effectStylesToTokens.ts │ ├── gridStylesToTokens.ts │ ├── stylesToTokens.ts │ └── textStylesToTokens.ts │ ├── text │ ├── getFontStyleAndWeight.spec.ts │ ├── getFontStyleAndWeight.ts │ ├── getLetterSpacing.ts │ └── getLineHeight.ts │ └── variablesToTokens.ts ├── tsconfig.base.json ├── tsconfig.cli.json ├── tsconfig.json ├── tsconfig.plugin.json ├── webpack.config.cli.js └── webpack.config.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | pull_request: 6 | types: [assigned, opened, synchronize, reopened, labeled] 7 | name: ci 8 | permissions: 9 | contents: read # to fetch code (actions/checkout) 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | 18 | - uses: pnpm/action-setup@v4 19 | name: Install pnpm 20 | with: 21 | run_install: false 22 | 23 | - name: Install Node.js 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: 'lts/*' 27 | cache: 'pnpm' 28 | 29 | - name: Install dependencies 30 | run: | 31 | pnpm install 32 | node --version 33 | - name: Build & Tests 34 | run: | 35 | pnpm run build 36 | pnpm run test 37 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | name: release-please 6 | permissions: {} 7 | jobs: 8 | release-please: 9 | permissions: 10 | contents: write # to create release commit (google-github-actions/release-please-action) 11 | pull-requests: write # to create release PR (google-github-actions/release-please-action) 12 | statuses: write 13 | issues: write 14 | packages: write # to create NPM package under github registry 15 | 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: googleapis/release-please-action@v4 19 | id: release 20 | with: 21 | token: ${{ secrets.GITHUB_TOKEN }} 22 | release-type: node 23 | - name: Checkout 24 | uses: actions/checkout@v2 25 | 26 | - name: Install pnpm 27 | uses: pnpm/action-setup@v4 28 | with: 29 | run_install: false 30 | 31 | - name: Install Node.js 32 | uses: actions/setup-node@v4 33 | with: 34 | node-version: 'lts/*' 35 | cache: 'pnpm' 36 | registry-url: 'https://npm.pkg.github.com' 37 | 38 | - name: Build package 39 | run: pnpm install && pnpm run build 40 | if: ${{ steps.release.outputs.release_created }} 41 | 42 | - name: Upload Release Artifact 43 | if: ${{ steps.release.outputs.release_created }} 44 | run: gh release upload ${{ steps.release.outputs.tag_name }} ./dist/figma-plugin.zip 45 | env: 46 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 47 | 48 | - name: Publish on Github registry 49 | run: pnpm publish 50 | env: 51 | NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} 52 | if: ${{ steps.release.outputs.release_created && github.repository == 'tokens-bruecke/figma-plugin'}} 53 | 54 | # - name: Publish on npm 55 | # run: pnpm publish 56 | # env: 57 | # NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 58 | # if: ${{ steps.release.outputs.release_created && github.repository == 'tokens-bruecke/figma-plugin'}} 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .DS_Store 4 | build.zip 5 | bin/ 6 | out/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | examples/ 3 | node_modules/ 4 | src/ 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | trailingComma: es5 2 | singleQuote: false 3 | printWidth: 80 4 | tabWidth: 2 5 | bracketSpacing: true 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [2.6.2](https://github.com/tokens-bruecke/figma-plugin/compare/v2.6.1...v2.6.2) (2025-06-01) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * prettier and file formatting ([43bbca5](https://github.com/tokens-bruecke/figma-plugin/commit/43bbca5a922a2d5653704c56b0c2d6e1060855d2)) 9 | 10 | ## [2.6.1](https://github.com/tokens-bruecke/figma-plugin/compare/v2.6.0...v2.6.1) (2025-04-17) 11 | 12 | ### Bug Fixes 13 | 14 | - handle font style value ([77db500](https://github.com/tokens-bruecke/figma-plugin/commit/77db5001285c9bbee3870f88fb0cb77502f3599a)) 15 | - handle font style value ([7b284d6](https://github.com/tokens-bruecke/figma-plugin/commit/7b284d6239bb7f0bf7f9283667e38020ecd523db)) 16 | - handle font weight value ([5ff1181](https://github.com/tokens-bruecke/figma-plugin/commit/5ff1181d088d01c9dc64338dd610d3ac745d8d90)) 17 | 18 | ## [2.6.0](https://github.com/tokens-bruecke/figma-plugin/compare/2.5.0...v2.6.0) (2025-04-11) 19 | 20 | ### Bug Fixes 21 | 22 | - fix warning about passing float to figma.ui.resize ([3c5702d](https://github.com/tokens-bruecke/figma-plugin/commit/3c5702d253d402f8c0e7b19ea26a4c44b5e59c75)) 23 | 24 | ### Miscellaneous Chores 25 | 26 | - release 2.6.0 ([cf525b1](https://github.com/tokens-bruecke/figma-plugin/commit/cf525b148f465bd109fa5f7c73c891a45acc1115)) 27 | 28 | ## 2.5.0 29 | 30 | - [Introduce CLI Based tool that uses Figma Rest API](https://github.com/tokens-bruecke/figma-plugin/pull/33) _by [Sylvain Marcadal](https://github.com/r1m)_ (not yet published to NPM) 31 | - [Fix: support font style strings like "Semi Bold"](https://github.com/tokens-bruecke/figma-plugin/pull/36) _by [Peter Lazar](peterlazar1993)_ 32 | - [Fix: precision errors for floats](https://github.com/tokens-bruecke/figma-plugin/pull/37) _by [Peter Lazar](peterlazar1993)_ 33 | 34 | ## 2.4.0 35 | 36 | - [Add font style support for italic](https://github.com/tokens-bruecke/figma-plugin/pull/35) _by [Sylvain Marcadal](https://github.com/r1m)_ 37 | - [Add support for selfhosted gitlab](https://github.com/tokens-bruecke/figma-plugin/pull/34) _by [Sylvain Marcadal](https://github.com/r1m)_ 38 | 39 | ## 2.3.1 40 | 41 | - [Fixes dimension type for vars with single scope](https://github.com/tokens-bruecke/figma-plugin/pull/31) 42 | 43 | ## 2.3.0 44 | 45 | - Added `codeSyntax` property to variables. See this PR — [include variable's "codeSyntax" property in exported token json](https://github.com/tokens-bruecke/figma-plugin/pull/28) 46 | 47 | ## 2.2.3 48 | 49 | - Convert `OPACITY` scope to valid value using this formula `value / 100`. 50 | 51 | ## 2.2.2 52 | 53 | - Do not convert the value to PX units if the variable scope is `FONT_WEIGHT` 54 | 55 | ## 2.2.1 56 | 57 | - Added `paragraphSpacing` and `paragraphIndent` to the typography styles 58 | 59 | ## 2.2.0 60 | 61 | - Added aliases handling for typography styles — [Related issue](https://github.com/tokens-bruecke/figma-plugin/issues/24) 62 | - Added aliases handling for effects 63 | 64 | ## 2.1.4 and 2.1.5 65 | 66 | - Fix wrong font weight output. Related PR — [Right the heuristic wrongs](https://github.com/tokens-bruecke/figma-plugin/pull/20). _by [@JeroenRoodIHS](https://github.com/JeroenRoodIHS)_ 67 | 68 | ## 2.1.3 69 | 70 | - Fixed font weights to be numbers. Related PR — [Font weights fix - output as numbers (DTCG format)](https://github.com/tokens-bruecke/figma-plugin/pull/22) 71 | 72 | ## 2.1.2 73 | 74 | - Updated the function to generate text styles. Related PR — [Update textStylesToTokens.ts ](https://github.com/tokens-bruecke/figma-plugin/pull/19) 75 | 76 | ## 2.1.1 77 | 78 | - `$meta` tag moved to `$extensions` object. See issue — [$meta is not valid DTCG](https://github.com/tokens-bruecke/figma-plugin/issues/13) 79 | 80 | ## 2.1.0 81 | 82 | - Multiple `Shadow` and `Blur` styles support added. [Link to the PR](https://github.com/tokens-bruecke/figma-plugin/issues/11) 83 | 84 | ## 2.0.0 85 | 86 | - tokens structure was changed. All modes now moved from variable names into `$extensions/modes` object. In order to make it work with [Cobalt](https://cobalt-ui.pages.dev/guides/modes#with-modes). For morre details see this issue — [Multiple collection and modes](https://github.com/tokens-bruecke/figma-plugin/issues/7). Previous implementation didn't work correctly with multiple modes and aliasees. 87 | 88 | ## 1.6.0 89 | 90 | - `value` string for aliases is now optional 91 | 92 | ## 1.5.0 93 | 94 | - Added `GitHub PR` option to the `Push to server` feature 95 | - `Connect server` renamed to `Push to server` 96 | - _Thanks for contribution to [@distolma](https://github.com/distolma)_ 97 | 98 | ## 1.4.0 99 | 100 | - Added `warning` type to the `Toast` component 101 | - structure refactoring 102 | - code refactoring 103 | - updated `Github` errors handling 104 | - added `value` to all aliases at the end of the path. Also support for `DTCG` keys format added 105 | - added storage versioning 106 | - updated DTCG format switching 107 | - added `Copy` button for the tokens preview 108 | 109 | ## 1.3.0 110 | 111 | - Functions names refactoring 112 | 113 | ## 1.2.0 114 | 115 | - Updated method to check `VARIABLE_ALIAS` in `normalizeValue` function 116 | - Handle aliases from another files 117 | - Removed the property `aliasPath` from `$extensions` object, since it's not needed anymore 118 | 119 | ## 1.1.1 120 | 121 | - Updated errors handling for GitHub server 122 | 123 | ## 1.1.0 124 | 125 | - `Update` button animation added 126 | - added token types as a separate package 127 | 128 | ## 1.0.9 129 | 130 | - Fixed `line-height` value conversion. It wasn't rounded to the nearest integer. 131 | 132 | ## 1.0.8 133 | 134 | - Fix for [Reference tokens auto-referencing themselves in the exported JSON](https://github.com/PavelLaptev/tokens-bruecke/issues/1) 135 | 136 | ## 1.0.7 137 | 138 | - Code cleanup 139 | 140 | ## 1.0.6 141 | 142 | - WIP [Reference tokens auto-referencing themselves in the exported JSON](https://github.com/PavelLaptev/tokens-bruecke/issues/1) 143 | 144 | ## 1.0.5 145 | 146 | - Allowed to use plugin in files without variables 147 | 148 | ## 1.0.4 149 | 150 | - Fix scopes conversion 151 | - `$meta` info adding order fixed 152 | 153 | ## 1.0.3 154 | 155 | - HEX color fixed 156 | - Alias variables fixed 157 | 158 | ## 1.0.2 159 | 160 | - Fixed RGBA to HEXA conversion 161 | - Added color styles support 162 | - Added basic support for linear and radial gradients 163 | 164 | ## 1.0.1 165 | 166 | - Fixed Aliases handling. Removed `mode` from the alias string if there is only one mode in the collection. 167 | 168 | ## 1.0.0 169 | 170 | - Initial release 171 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Pavel Laptev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TokensBrücke — Figma plugin 2 | 3 | 4 | preview 5 | 6 | 7 | ## What is this plugin for? 8 | 9 | The plugin converts Figma variables into design-tokens JSON that are compatible with the latest [Design Tokens specification](https://design-tokens.github.io/community-group/format/). 10 | 11 | --- 12 | 13 | ## Table of contents 14 | 15 | - [TokensBrücke — Figma plugin](#tokensbrücke--figma-plugin) 16 | - [What is this plugin for?](#what-is-this-plugin-for) 17 | - [Table of contents](#table-of-contents) 18 | - [New version 2.0.0](#new-version-200) 19 | - [How to use](#how-to-use) 20 | - [General settings](#general-settings) 21 | - [Color mode](#color-mode) 22 | - [Include styles](#include-styles) 23 | - [Add styles to](#add-styles-to) 24 | - [Include variable scopes](#include-variable-scopes) 25 | - [Use DTCG keys format](#use-dtcg-keys-format) 26 | - [Include `.value` string for aliases](#include-value-string-for-aliases) 27 | - [Include Figma metadata](#include-figma-metadata) 28 | - [Use as cli tool](#use-as-cli-tool) 29 | - [Installation](#installation) 30 | - [Usage](#usage) 31 | - [Options](#options) 32 | - [CLI Configuration File](#cli-configuration-file) 33 | - [Push to server](#push-to-server) 34 | - [JSONBin](#jsonbin) 35 | - [GitHub](#github) 36 | - [GitHub PR](#github-pr) 37 | - [GitLab](#gitlab) 38 | - [Custom server](#custom-server) 39 | - [Show output](#show-output) 40 | - [Config autosaving](#config-autosaving) 41 | - [Styles support](#styles-support) 42 | - [Typography](#typography) 43 | - [Grids](#grids) 44 | - [Shadows](#shadows) 45 | - [Blur](#blur) 46 | - [Multiple `Shadow` and `Blur` styles support](#multiple-shadow-and-blur-styles-support) 47 | - [Why there is no support for color styles?](#why-there-is-no-support-for-color-styles) 48 | - [Gradients support 🚧](#gradients-support-) 49 | - [Tokens structure](#tokens-structure) 50 | - [Aliases handling](#aliases-handling) 51 | - [Include `.value` string for aliases](#include-value-string-for-aliases-1) 52 | - [Handle variables from another file](#handle-variables-from-another-file) 53 | - [Handle modes](#handle-modes) 54 | - [Variables types conversion](#variables-types-conversion) 55 | - [Design tokens types](#design-tokens-types) 56 | - [Scopes lemitations](#scopes-lemitations) 57 | - [Style Dictionary support](#style-dictionary-support) 58 | - [Contribution 🚧](#contribution-) 59 | - [Feedback](#feedback) 60 | 61 | --- 62 | 63 | ## New version 2.0.0 64 | 65 | In the new version of the plugin all mode variables moved into the `$extensions` / `modes` object. 66 | This is how Terrazzo (formerly “Cobalt UI”) works with multiple modes. Check it here — [https://terrazzo.app/docs/](https://terrazzo.app/docs/) 67 | 68 | You can download and install previos version `1.6.1` here — [github.com/tokens-bruecke/figma-plugin/files/13536853/build.zip](https://github.com/tokens-bruecke/figma-plugin/files/13536853/build.zip) 69 | 70 | ## How to use 71 | 72 | 1. Install the plugin from the [Figma Community](https://www.figma.com/community/plugin/1254538877056388290). 73 | 2. Make sure you have variables in your Figma file. 74 | 3. Run the plugin. 75 | 4. Adjust the settings. 76 | 5. Then you can download the JSON file or push it to on of the [supported services](#link). 77 | 78 | --- 79 | 80 | ## General settings 81 | 82 | ### Color mode 83 | 84 | Allows you to choose the color mode for the generated JSON. Default value is `HEX`. The plugin supports the following color modes: 85 | 86 | - `HEX` — HEX color format. Could be converted into `HEXA` if the color has an alpha channel. 87 | - `RGBA CSS` — RGBA color format in CSS syntax, e.g. `rgba(0, 0, 0, 0.5)`. 88 | - `RGBA Object` — RGBA color format in object syntax, e.g. `{ r: 0, g: 0, b: 0, a: 0.5 }`. 89 | - `HSLA CSS` — HSLA color format in CSS syntax, e.g. `hsla(0, 0%, 0%, 0.5)`. 90 | - `HSLA Object` — HSLA color format in object syntax, e.g. `{ h: 0, s: 0, l: 0, a: 0.5 }`. 91 | 92 | ### Include styles 93 | 94 | Allows you to include styles into the generated JSON. See more about styles support in the [Styles support](#styles-support) section. 95 | 96 | There is an option to rename each style's group and give it a custom name for better organization. 97 | 98 | ![rename-styles](readme-assets/rename-styles.gif) 99 | 100 | ### Add styles to 101 | 102 | Allows you to choose where to put styles in the generated JSON. By default, the selected value is `Keep separate`. In this case styles will be added into the root of the JSON and will be treated as collections. There is also an option to add styles into the corresponding collection (fig.4). 103 | 104 | ![fig.4](readme-assets/fig4.webp) 105 | 106 | ### Include variable scopes 107 | 108 | Each Figma variable has a [scope property](https://www.figma.com/plugin-docs/api/VariableScope). The plugin allows you to include scopes into the generated JSON. It will be included as an array of strings without any transformations. 109 | 110 | ```json 111 | { 112 | "button": { 113 | "background": { 114 | "type": "color", 115 | "value": "#000000", 116 | "scopes": ["ALL_SCOPES"] 117 | } 118 | } 119 | } 120 | ``` 121 | 122 | ### Use DTCG keys format 123 | 124 | Is `off` by default. Currently many design tokens tools doesn't support [DTCG keys format](https://design-tokens.github.io/community-group/format/#character-restrictions). All DTCG keys are prefixed with `$` symbol. 125 | 126 | ```json 127 | // Without DTCG keys format 128 | { 129 | "button": { 130 | "background": { 131 | "type": "color", 132 | "value": "#000000" 133 | } 134 | } 135 | } 136 | 137 | // With DTCG keys format 138 | { 139 | "button": { 140 | "background": { 141 | "$type": "color", 142 | "$value": "#000000", 143 | } 144 | } 145 | } 146 | ``` 147 | 148 | ### Include `.value` string for aliases 149 | 150 | Is `off` by default. Allows you to include `.value` string to the end of the path for aliases. It will be added to the alias string. 151 | 152 | ```json 153 | { 154 | "button": { 155 | "background": { 156 | "type": "color", 157 | "value": "{colors.light.primary.10.value}" 158 | } 159 | } 160 | } 161 | ``` 162 | 163 | If the format is `DTCG`: 164 | 165 | ```json 166 | { 167 | "button": { 168 | "background": { 169 | "$type": "color", 170 | "$value": "{colors.light.primary.10.$value}" 171 | } 172 | } 173 | } 174 | ``` 175 | 176 | ![fig.13](readme-assets/fig13.webp) 177 | 178 | ### Include Figma metadata 179 | 180 | Is `off` by default. Allows you to include Figma metadata like `styleId`, `variableId`, etc. into the generated JSON. It will be added to the `$extensions` object. 181 | 182 | ```json 183 | "figma": { 184 | "codeSyntax": {}, 185 | "variableId": "VariableID:1:4", 186 | "collection": { 187 | "id": "VariableCollectionId:1:3", 188 | "name": "Primitives", 189 | "defaultModeId": "1:0" 190 | } 191 | } 192 | ``` 193 | 194 | --- 195 | 196 | ## Use as cli tool 197 | 198 | > [!WARNING] 199 | > ⚠️ You need a Figma Enterprise plan to use the Figma REST API for variables. 200 | 201 | ### Installation 202 | 203 | To install the CLI globally, run: 204 | 205 | ```bash 206 | pnpm add -g tokens-bruecke 207 | #or npm install -g tokens-bruecke 208 | ``` 209 | 210 | This will make the `tokens-bruecke` command available globally on your system. 211 | 212 | ### Usage 213 | 214 | After installation, you can run the CLI tool using: 215 | 216 | ```bash 217 | tokens-bruecke [options] 218 | ``` 219 | 220 | For example: 221 | 222 | ```bash 223 | tokens-bruecke --api-key $FIGMA_TOKEN --file-key $FIGMA_FILE --config config.json --output out/tokens.json 224 | ``` 225 | 226 | This will fetch figma variables and export them in `out/tokens.json` 227 | 228 | ### Options 229 | 230 | Same options than the plugin are available throught the usage of a json file. 231 | 232 | ### CLI Configuration File 233 | 234 | You can use a JSON configuration file to specify the export options for the CLI. 235 | 236 | ```json 237 | { 238 | "includedStyles": { 239 | "text": { "isIncluded": true, "customName": "typography" }, 240 | "effects": { "isIncluded": false, "customName": "effects" }, 241 | "grids": { "isIncluded": false, "customName": "grids" } 242 | }, 243 | "includeScopes": true, 244 | "useDTCGKeys": false, 245 | "includeValueStringKeyToAlias": true, 246 | "includeFigmaMetaData": false, // Include Figma metadata like styleId, variableId, etc. 247 | "colorMode": "hex", // "hex" | "rgba-object" | "rgba-css" | "hsla-object" | "hsla-css"; 248 | "storeStyleInCollection": "none" // Name of one of your collection or "none" to keep them separated 249 | } 250 | ``` 251 | 252 | Save this JSON file and pass it to the CLI using the `--config` option: 253 | 254 | --- 255 | 256 | ## Push to server 257 | 258 | With this feature you can connect a server and push the generated JSON directly to it. At the moment the plugin supports [JSONBin](https://jsonbin.io), [GitHub](https://github.com) and custom servers. 259 | 260 | ![fig.5](readme-assets/fig5.webp) 261 | 262 | If you connected multiple servers, the plugin will try to push the tokens to all of them one by one. 263 | In ordere to test if your credentials are valid you can make a test request by clicking the `Push to server` button (fig.6). 264 | 265 | ![fig.6](readme-assets/fig6.webp) 266 | 267 | ### [JSONBin](https://jsonbin.io) 268 | 269 | 1. Open [JSONBin](https://jsonbin.io) and create an account. 270 | 2. Generate a [new API key](https://jsonbin.io/api-reference/access-keys/create). 271 | 3. If you want to use an existing bin, copy its ID. Otherwise just leave the ID field empty in the plugin settings. 272 | 4. Add a name for the bin. 273 | 274 | ![fig.7](readme-assets/fig7.webp) 275 | 276 | ### [GitHub](https://github.com) 277 | 278 | 1. You need to create a [personal access token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) with `repo` scope. 279 | 2. In the plugin settings paste the token into the `Personal access token` field. 280 | 3. Add an owner name, repository name and a branch name. 281 | 4. In the file name field you can specify a path to the file. If the file doesn't exist, it will be created. If the file exists, it will be overwritten. File name should include the file extension, e.g. `tokens.json`. 282 | 5. You can also specify a commit message. 283 | 284 | ![fig.8](readme-assets/fig8.webp) 285 | 286 | ### [GitHub PR](https://github.com) 287 | 288 | All the steps are the same as for the [GitHub](#github) server, except the last two. 289 | 290 | - **PR title**. You can specify a title for the PR. If you leave it empty, the plugin will use `chore(tokens): update tokens` as a default title. 291 | - **PR body**. You can specify a body for the PR. If you leave it empty, the plugin won't add any body to the PR. 292 | 293 | ![fig.12](readme-assets/fig12.webp) 294 | 295 | ### [GitLab](https://gitlab.com) 296 | 297 | 1. You need to create a [project access token](https://docs.gitlab.com/ee/user/project/settings/project_access_tokens.html) with `api` scope. 298 | 2. In the plugin settings paste the token into the `Project access token` field. 299 | 3. Add an owner name, repository name and a branch name. 300 | 4. In the file name field you can specify a path to the file. If the file doesn't exist, it will be created. If the file exists, it will be overwritten. File name should include the file extension, e.g. `tokens.json`. 5. You can also specify a commit message. 301 | 302 | ![fig.11](readme-assets/fig11.webp) 303 | 304 | ### Custom server 305 | 306 | There is a possibilty to connect a custom server. In order to do that you need to specify a URL, a method (by default it's `POST`) and headers. 307 | 308 | ![fig.9](readme-assets/fig9.webp) 309 | 310 | --- 311 | 312 | ## Show output 313 | 314 | If you want to see the generated JSON, you can enable the `Show output` option. The plugin will show the JSON in the sidebar. The output doesn't update automatically, in order to optimize the performance. So, if you want to see the updated JSON, you need to click the `Update` button. 315 | 316 | ![fig.10](readme-assets/fig10.webp) 317 | 318 | --- 319 | 320 | ## Config autosaving 321 | 322 | The plugin saves the config automatically. So, you don't need to set it up every time you run the plugin. 323 | 324 | --- 325 | 326 | ## Styles support 327 | 328 | The plugin can support some styles and effects too. Until Figma will support all the styles and effects, the plugin will convert them into the corresponding design tokens types. But it's not a backward compatibility, it's a temporary solution until Figma will support all the styles and effects as variables. 329 | 330 | Supported styles: 331 | 332 | - Typography 333 | - Grids 334 | - Shadows (including `inset` shadows) 335 | - Blur (including `background` and `layer` blur) 336 | 337 | ### Typography 338 | 339 | ```json 340 | "extralight": { 341 | "type": "typography", 342 | "value": { 343 | "fontFamily": "Inter", 344 | "fontWeight": 400, 345 | "fontSize": "18px", 346 | "lineHeight": "28px", 347 | "letterSpacing": "0%" 348 | }, 349 | "description": "", 350 | "extensions": { 351 | "styleId": "S:0ffe98ad785a13839980113831d5fbaf21724594," 352 | } 353 | } 354 | ``` 355 | 356 | ### Grids 357 | 358 | In Figma you can add as many grids in the style as you want. But the plugin will take only first two grids and treat the first one as `column` grid and the second one as `row` grid. 359 | 360 | ```json 361 | // Column grid 362 | "1024": { 363 | "type": "grid", 364 | "value": { 365 | "columnCount": 12, 366 | "columnGap": "20px", 367 | "columnMargin": "40px" 368 | } 369 | } 370 | 371 | // Row grid 372 | "1024": { 373 | "type": "grid", 374 | "value": { 375 | "rowCount": 12, 376 | "rowGap": "20px", 377 | "rowMargin": "40px" 378 | } 379 | } 380 | 381 | // Both grids 382 | "1024": { 383 | "type": "grid", 384 | "value": { 385 | "columnCount": 12, 386 | "columnGap": "20px", 387 | "columnMargin": "40px", 388 | "rowCount": 12, 389 | "rowGap": "20px", 390 | "rowMargin": "40px" 391 | } 392 | } 393 | ``` 394 | 395 | ### Shadows 396 | 397 | The plugin supports `drop-shadow` and `inner-shadow` effects. If the effect is `inner-shadow`, the plugin will set the `inset` property to `true`. 398 | 399 | ```json 400 | "xl": { 401 | "type": "shadow", 402 | "value": { 403 | "inset": false, 404 | "color": "#0000000a", 405 | "offsetX": "0px", 406 | "offsetY": "10px", 407 | "blur": "10px", 408 | "spread": "-5px" 409 | } 410 | } 411 | ``` 412 | 413 | ### Blur 414 | 415 | The plugin supports `background` and `layer` blur effects. In order to distinguish between them, the plugin adds the `role` property to the generated JSON. 416 | 417 | ```json 418 | // Background blur 419 | "sm": { 420 | "type": "blur", 421 | "value": { 422 | "role": "background", 423 | "blur": "4px" 424 | } 425 | } 426 | 427 | // Layer blur 428 | "md": { 429 | "type": "blur", 430 | "value": { 431 | "role": "layer", 432 | "blur": "12px" 433 | } 434 | } 435 | ``` 436 | 437 | ### Multiple `Shadow` and `Blur` styles support 438 | 439 | If the style has multiple `Shadow` or `Blur` styles, the plugin will add them into the array. 440 | 441 | ```json 442 | "new-sh": { 443 | "$type": "shadow", 444 | "$value": [ 445 | { 446 | "inset": false, 447 | "color": "#e4505040", 448 | "offsetX": "0px", 449 | "offsetY": "4px", 450 | "blur": "54px", 451 | "spread": "0px" 452 | }, 453 | { 454 | "inset": false, 455 | "color": "#5b75ff40", 456 | "offsetX": "0px", 457 | "offsetY": "4px", 458 | "blur": "24px", 459 | "spread": "0px" 460 | }, 461 | { 462 | "inset": false, 463 | "color": "#00000040", 464 | "offsetX": "0px", 465 | "offsetY": "4px", 466 | "blur": "4px", 467 | "spread": "0px" 468 | } 469 | ] 470 | } 471 | ``` 472 | 473 | ### Why there is no support for color styles? 474 | 475 | Despite the fact that color styles could be important for backward compatibility — the main goal of the plugin is to convert Figma variables into design tokens. Since Figma already has a support for color in variables, there is no need to convert also color styles into design tokens. 476 | 477 | ### Gradients support 🚧 478 | 479 | Support for gradients is comming with the next major release. 480 | 481 | --- 482 | 483 | ## Tokens structure 484 | 485 | Plugin first takes the `collection` name, then the `group` and then the `variable` name (fig.1). 486 | Mode variables will be wrapped under the `$extensions` objects 487 | 488 | ![fig.1](readme-assets/fig1.webp) 489 | 490 | For example, if you have a collection named `clr-theme`, mode named `light` and variable named `dark`, the plugin will generate the following JSON: 491 | 492 | ```json 493 | "clr-theme": { 494 | "container-outline/mid": { 495 | "type": "color", 496 | "value": "{clr-core.ntrl.40}", 497 | "description": "", 498 | "$extensions": { 499 | "mode": { 500 | "light": "{clr-core.ntrl.40}", 501 | "dark": "{clr-core.ntrl.55}" 502 | } 503 | } 504 | } 505 | } 506 | , 507 | ``` 508 | 509 | ![fig.2](readme-assets/fig2.webp) 510 | 511 | Figma automatically merges groups and their names into a single name, e.g. `Base/Primary/10` (fig.2). In this case, the plugin will generate the following JSON: 512 | 513 | ```json 514 | { 515 | "base": { 516 | "primary": { 517 | "10": { 518 | "type": "color", 519 | "value": "#000000" 520 | } 521 | } 522 | } 523 | } 524 | ``` 525 | 526 | ## Aliases handling 527 | 528 | All aliases are converted into the alias string format from the [Design Tokens specification](https://design-tokens.github.io/community-group/format/#aliases-references). 529 | 530 | ```json 531 | { 532 | "button": { 533 | "background": { 534 | "type": "color", 535 | "value": "{colors.primary.10}" 536 | } 537 | } 538 | } 539 | ``` 540 | 541 | ### Include `.value` string for aliases 542 | 543 | You can switch on the `Include .value string for aliases` option in [the plugin settings](#include-value-string-for-aliases). 544 | 545 | --- 546 | 547 | ### Handle variables from another file 548 | 549 | Imagine you have a library from another file with "base" variables. And you use this variables in your current file. 550 | 551 | The plugin will generate the alias name anyway, but it will be a path to the variable as if it was in the current file. 552 | 553 | ```json 554 | { 555 | "button": { 556 | "background": { 557 | "type": "color", 558 | "value": "{colors.primary.10}" 559 | } 560 | } 561 | } 562 | ``` 563 | 564 | The plugin wouldn't include the variable into the generated JSON in order to avoid duplicates or conflicts with JSON files you can generate from another Figma files. 565 | 566 | So you will need to merge the file with the base variables from one file with another where you use them. Otherwise tools like Style Dictionary wouldn't be able to resolve the aliases. 567 | 568 | --- 569 | 570 | ### Handle modes 571 | 572 | If there is only one mode — the plugin wouldn't include it in a generated JSON. 573 | If there are multiple modes, the plugin will place them under the `$extensions` objects. 574 | 575 | It follows the same pattern as used by [Cobalt](https://cobalt-ui.pages.dev/guides/modes#with-modes) 576 | 577 | --- 578 | 579 | ## Variables types conversion 580 | 581 | Unlike design tokens, Figma variables now [support only 4 types](https://www.figma.com/plugin-docs/api/VariableResolvedDataType) — `COLOR`, `BOOLEAN`, `FLOAT` and `STRING`. So, the plugin converts them into the corresponding types from the [Design Tokens specification](https://design-tokens.github.io/community-group/format/#types). 582 | 583 | | Figma type | Design Tokens type | 584 | | ---------- | ----------------------------------------------------------------------------------- | 585 | | COLOR | [color](https://design-tokens.github.io/community-group/format/#color) | 586 | | BOOLEAN | _boolean_ \* | 587 | | FLOAT | [dimension](https://design-tokens.github.io/community-group/format/#dimension) \*\* | 588 | | STRING | _string_ \* | 589 | 590 | \* native JSON types. The specification doesn't restrict the type of the value, so it could be any JSON type. Also see [this issue](https://github.com/design-tokens/community-group/issues/120#issuecomment-1279527414). 591 | 592 | \*\* currently figma supports only the `FLOAT` type for dimensions, that could be used only for `px` values. So, the plugin converts `FLOAT` values into `dimension` type with `px` unit. 593 | 594 | --- 595 | 596 | ## Design tokens types 597 | 598 | In order to validate types, the plugin uses the [Design Tokens types](https://github.com/PavelLaptev/tokens-bruecke/blob/main/token-types.d.ts). 599 | 600 | --- 601 | 602 | ## Scopes lemitations 603 | 604 | In order to convert `FONT-WEIGHT` and `OPACITY` types into valid values you should specify thme as scopes in the Figma variables. The plugin will read the first scope and convert it into the valid value. If there are multiple scopes, the plugin will take the first one. 605 | 606 | - `FONT_WEIGHT` scope will be converted into `string` type. 607 | - `OPACITY` scope will be converted into `number` type. 608 | 609 | --- 610 | 611 | ## Style Dictionary support 612 | 613 | There is a set of utils for [Style Dictionary](https://github.com/tokens-bruecke/sd-utils). 614 | 615 | --- 616 | 617 | ## Contribution 🚧 618 | 619 | Comming soon. 620 | 621 | --- 622 | 623 | ## Feedback 624 | 625 | If you have any questions or suggestions, feel free to [create an issue](https://github.com/tokens-bruecke/figma-plugin/issues) 626 | -------------------------------------------------------------------------------- /declaration.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.scss' { 2 | const content: { [className: string]: string }; 3 | export = content; 4 | } 5 | 6 | declare module '*.png'; 7 | declare module '*.gif'; 8 | declare module '*.jpg'; 9 | declare module '*.jpeg'; 10 | declare module '*.webp'; 11 | declare module '*.svg'; 12 | 13 | type PluginFormatTypes = 'WEBP' | 'PNG' | 'JPEG'; 14 | -------------------------------------------------------------------------------- /examples/cli-options.json: -------------------------------------------------------------------------------- 1 | { 2 | "includedStyles": { 3 | "text": { 4 | "isIncluded": true, 5 | "customName": "typography" 6 | }, 7 | "effects": { 8 | "isIncluded": true, 9 | "customName": "effects" 10 | }, 11 | "grids": { 12 | "isIncluded": false, 13 | "customName": "grids" 14 | } 15 | }, 16 | "includeScopes": false, 17 | "useDTCGKeys": true, 18 | "includeValueStringKeyToAlias": true, 19 | "colorMode": "hsla-css", 20 | "storeStyleInCollection": "none" 21 | } 22 | -------------------------------------------------------------------------------- /examples/tokens-examples/design-tokens.tokens.json: -------------------------------------------------------------------------------- 1 | { 2 | "color": { 3 | "main": { 4 | "0": { 5 | "description": "", 6 | "type": "color", 7 | "value": "#ffffffff", 8 | "blendMode": "normal", 9 | "extensions": { 10 | "org.lukasoppermann.figmaDesignTokens": { 11 | "styleId": "S:b41eb3adf6e95bf8457721fa7d914f0ff277facd,", 12 | "exportKey": "color" 13 | } 14 | } 15 | }, 16 | "50": { 17 | "description": "", 18 | "type": "color", 19 | "value": "#f4f4f4ff", 20 | "blendMode": "normal", 21 | "extensions": { 22 | "org.lukasoppermann.figmaDesignTokens": { 23 | "styleId": "S:ac9972e62f812c92c907eb51637ae78aa04e257b,", 24 | "exportKey": "color" 25 | } 26 | } 27 | }, 28 | "100": { 29 | "description": "", 30 | "type": "color", 31 | "value": "#b9b9b9ff", 32 | "blendMode": "normal", 33 | "extensions": { 34 | "org.lukasoppermann.figmaDesignTokens": { 35 | "styleId": "S:1c97651a8f6671ac8ac6f0f7473393538805f5b7,", 36 | "exportKey": "color" 37 | } 38 | } 39 | }, 40 | "200": { 41 | "description": "", 42 | "type": "color", 43 | "value": "#8a8a8aff", 44 | "blendMode": "normal", 45 | "extensions": { 46 | "org.lukasoppermann.figmaDesignTokens": { 47 | "styleId": "S:f22ef9f32e162756104b8b5147e846ba9e88a7be,", 48 | "exportKey": "color" 49 | } 50 | } 51 | }, 52 | "400": { 53 | "description": "", 54 | "type": "color", 55 | "value": "#373737ff", 56 | "blendMode": "normal", 57 | "extensions": { 58 | "org.lukasoppermann.figmaDesignTokens": { 59 | "styleId": "S:104ce97bd9c286d15fb3ef2d556f8aaa6290e40f,", 60 | "exportKey": "color" 61 | } 62 | } 63 | }, 64 | "500": { 65 | "description": "", 66 | "type": "color", 67 | "value": "#141414ff", 68 | "blendMode": "normal", 69 | "extensions": { 70 | "org.lukasoppermann.figmaDesignTokens": { 71 | "styleId": "S:b66b7e320071568f08d9b3f81de00b5e30b2fb25,", 72 | "exportKey": "color" 73 | } 74 | } 75 | }, 76 | "transparent": { 77 | "50": { 78 | "description": "", 79 | "type": "color", 80 | "value": "#1414140a", 81 | "blendMode": "normal", 82 | "extensions": { 83 | "org.lukasoppermann.figmaDesignTokens": { 84 | "styleId": "S:b7c474509c8fa65fba4c25d5bd7ddce873a7915f,", 85 | "exportKey": "color" 86 | } 87 | } 88 | }, 89 | "80": { 90 | "description": "", 91 | "type": "color", 92 | "value": "#14141414", 93 | "blendMode": "normal", 94 | "extensions": { 95 | "org.lukasoppermann.figmaDesignTokens": { 96 | "styleId": "S:19c705f230b9eb65014c6241136ab23842bd7a36,", 97 | "exportKey": "color" 98 | } 99 | } 100 | }, 101 | "100": { 102 | "description": "", 103 | "type": "color", 104 | "value": "#1414144d", 105 | "blendMode": "normal", 106 | "extensions": { 107 | "org.lukasoppermann.figmaDesignTokens": { 108 | "styleId": "S:a1f48459244d4a077e3d45eb70ec31e9fdb06096,", 109 | "exportKey": "color" 110 | } 111 | } 112 | }, 113 | "200": { 114 | "description": "", 115 | "type": "color", 116 | "value": "#14141480", 117 | "blendMode": "normal", 118 | "extensions": { 119 | "org.lukasoppermann.figmaDesignTokens": { 120 | "styleId": "S:dbf3da3d29405117620dd75addb5b58d14dcbc84,", 121 | "exportKey": "color" 122 | } 123 | } 124 | }, 125 | "400": { 126 | "description": "", 127 | "type": "color", 128 | "value": "#141414d9", 129 | "blendMode": "normal", 130 | "extensions": { 131 | "org.lukasoppermann.figmaDesignTokens": { 132 | "styleId": "S:0ea4c3cc4790cafbca82c3880b3da88adde6d4ee,", 133 | "exportKey": "color" 134 | } 135 | } 136 | } 137 | } 138 | }, 139 | "accent": { 140 | "50": { 141 | "description": "", 142 | "type": "color", 143 | "value": "#f6f0f9ff", 144 | "blendMode": "normal", 145 | "extensions": { 146 | "org.lukasoppermann.figmaDesignTokens": { 147 | "styleId": "S:54cb084deba332495d4bb6883bc52a8fd98fc84e,", 148 | "exportKey": "color" 149 | } 150 | } 151 | }, 152 | "200": { 153 | "description": "", 154 | "type": "color", 155 | "value": "#e1c6f3ff", 156 | "blendMode": "normal", 157 | "extensions": { 158 | "org.lukasoppermann.figmaDesignTokens": { 159 | "styleId": "S:eb450fc105ec22f5f302df3824342210b52e10fc,", 160 | "exportKey": "color" 161 | } 162 | } 163 | }, 164 | "500": { 165 | "description": "", 166 | "type": "color", 167 | "value": "#b380d4ff", 168 | "blendMode": "normal", 169 | "extensions": { 170 | "org.lukasoppermann.figmaDesignTokens": { 171 | "styleId": "S:22cf0ac9aa5c5fc4816c29a62373e87adccd4ce9,", 172 | "exportKey": "color" 173 | } 174 | } 175 | }, 176 | "800": { 177 | "description": "", 178 | "type": "color", 179 | "value": "#9a63c3ff", 180 | "blendMode": "normal", 181 | "extensions": { 182 | "org.lukasoppermann.figmaDesignTokens": { 183 | "styleId": "S:4b369d595dc50fa8466fbd6f7219290e0e6b4813,", 184 | "exportKey": "color" 185 | } 186 | } 187 | }, 188 | "transparent": { 189 | "50": { 190 | "description": "", 191 | "type": "color", 192 | "value": "#b380d41f", 193 | "blendMode": "normal", 194 | "extensions": { 195 | "org.lukasoppermann.figmaDesignTokens": { 196 | "styleId": "S:fb9c65ca24931b6fbafcde82c8d7bae0a7aefb8b,", 197 | "exportKey": "color" 198 | } 199 | } 200 | }, 201 | "200": { 202 | "description": "", 203 | "type": "color", 204 | "value": "#b380d470", 205 | "blendMode": "normal", 206 | "extensions": { 207 | "org.lukasoppermann.figmaDesignTokens": { 208 | "styleId": "S:d7bb5148bf7d37124a68330f8671ab8429ce60b7,", 209 | "exportKey": "color" 210 | } 211 | } 212 | } 213 | } 214 | }, 215 | "peach": { 216 | "50": { 217 | "description": "", 218 | "type": "color", 219 | "value": "#fcf5edff", 220 | "blendMode": "normal", 221 | "extensions": { 222 | "org.lukasoppermann.figmaDesignTokens": { 223 | "styleId": "S:4fa322fd568bfe8c4fbf3d597990121b0b35d415,", 224 | "exportKey": "color" 225 | } 226 | } 227 | }, 228 | "200": { 229 | "description": "", 230 | "type": "color", 231 | "value": "#feeadfff", 232 | "blendMode": "normal", 233 | "extensions": { 234 | "org.lukasoppermann.figmaDesignTokens": { 235 | "styleId": "S:efcbdb1ba53d427b647d21d5112a57e443cd249a,", 236 | "exportKey": "color" 237 | } 238 | } 239 | }, 240 | "500": { 241 | "description": "", 242 | "type": "color", 243 | "value": "#fdcebeff", 244 | "blendMode": "normal", 245 | "extensions": { 246 | "org.lukasoppermann.figmaDesignTokens": { 247 | "styleId": "S:8e615d79a03ebcac50c24aec9b9c4c29f8695ce5,", 248 | "exportKey": "color" 249 | } 250 | } 251 | }, 252 | "900": { 253 | "description": "", 254 | "type": "color", 255 | "value": "#fcb099ff", 256 | "blendMode": "normal", 257 | "extensions": { 258 | "org.lukasoppermann.figmaDesignTokens": { 259 | "styleId": "S:3dd15adae1afcd049c98771652103ff5d760bbb9,", 260 | "exportKey": "color" 261 | } 262 | } 263 | }, 264 | "transparent": { 265 | "50": { 266 | "description": "", 267 | "type": "color", 268 | "value": "#fdd8be42", 269 | "blendMode": "normal", 270 | "extensions": { 271 | "org.lukasoppermann.figmaDesignTokens": { 272 | "styleId": "S:27913e5ae34e2610e98c7fda1238275629c9cee1,", 273 | "exportKey": "color" 274 | } 275 | } 276 | }, 277 | "200": { 278 | "description": "", 279 | "type": "color", 280 | "value": "#fdd5be70", 281 | "blendMode": "normal", 282 | "extensions": { 283 | "org.lukasoppermann.figmaDesignTokens": { 284 | "styleId": "S:1735fbe6c0bc69536aa573c598c80f9dd11ce008,", 285 | "exportKey": "color" 286 | } 287 | } 288 | } 289 | } 290 | }, 291 | "service": { 292 | "error": { 293 | "main": { 294 | "description": "", 295 | "type": "color", 296 | "value": "#f54e5dff", 297 | "blendMode": "normal", 298 | "extensions": { 299 | "org.lukasoppermann.figmaDesignTokens": { 300 | "styleId": "S:ae5ce40c124949ce8921dba1b29a0e67dea89d5e,", 301 | "exportKey": "color" 302 | } 303 | } 304 | }, 305 | "dark": { 306 | "description": "", 307 | "type": "color", 308 | "value": "#a8333bff", 309 | "blendMode": "normal", 310 | "extensions": { 311 | "org.lukasoppermann.figmaDesignTokens": { 312 | "styleId": "S:33f4f48c68b757aaf78235fbee9eeb9b603cea82,", 313 | "exportKey": "color" 314 | } 315 | } 316 | }, 317 | "light": { 318 | "description": "", 319 | "type": "color", 320 | "value": "#ffeef1ff", 321 | "blendMode": "normal", 322 | "extensions": { 323 | "org.lukasoppermann.figmaDesignTokens": { 324 | "styleId": "S:93ecb3fa393b632770cbff0d93aa79f23ae57041,", 325 | "exportKey": "color" 326 | } 327 | } 328 | } 329 | }, 330 | "warning": { 331 | "main": { 332 | "description": "", 333 | "type": "color", 334 | "value": "#ffca64ff", 335 | "blendMode": "normal", 336 | "extensions": { 337 | "org.lukasoppermann.figmaDesignTokens": { 338 | "styleId": "S:c9cfe3aa407cb8e146fd52a8b501a862bd5f7e0b,", 339 | "exportKey": "color" 340 | } 341 | } 342 | }, 343 | "dark": { 344 | "description": "", 345 | "type": "color", 346 | "value": "#d07408ff", 347 | "blendMode": "normal", 348 | "extensions": { 349 | "org.lukasoppermann.figmaDesignTokens": { 350 | "styleId": "S:27ec23f96bafde93ad392ccde3d3092bace07fea,", 351 | "exportKey": "color" 352 | } 353 | } 354 | }, 355 | "light": { 356 | "description": "", 357 | "type": "color", 358 | "value": "#fff6e4ff", 359 | "blendMode": "normal", 360 | "extensions": { 361 | "org.lukasoppermann.figmaDesignTokens": { 362 | "styleId": "S:0317b0c142291465167c5a1a05846a3b15c1d890,", 363 | "exportKey": "color" 364 | } 365 | } 366 | } 367 | }, 368 | "success": { 369 | "main": { 370 | "description": "", 371 | "type": "color", 372 | "value": "#d3fb60ff", 373 | "blendMode": "normal", 374 | "extensions": { 375 | "org.lukasoppermann.figmaDesignTokens": { 376 | "styleId": "S:f9757d7a98096b7c00c74d6ef9eed912b28e6671,", 377 | "exportKey": "color" 378 | } 379 | } 380 | }, 381 | "dark": { 382 | "description": "", 383 | "type": "color", 384 | "value": "#588909ff", 385 | "blendMode": "normal", 386 | "extensions": { 387 | "org.lukasoppermann.figmaDesignTokens": { 388 | "styleId": "S:fc06cf8c3a3c80df041a80ee0007058cdc9303b8,", 389 | "exportKey": "color" 390 | } 391 | } 392 | }, 393 | "light": { 394 | "description": "", 395 | "type": "color", 396 | "value": "#f5fcedff", 397 | "blendMode": "normal", 398 | "extensions": { 399 | "org.lukasoppermann.figmaDesignTokens": { 400 | "styleId": "S:abe4d874fceca38564df003490baf7b8f2d3f859,", 401 | "exportKey": "color" 402 | } 403 | } 404 | } 405 | } 406 | }, 407 | "complex gradients": { 408 | "apricot": { 409 | "0": { 410 | "type": "color", 411 | "value": "#feeadfff", 412 | "blendMode": "normal" 413 | }, 414 | "1": { 415 | "type": "custom-gradient", 416 | "value": { 417 | "gradientType": "radial", 418 | "rotation": 209.2483730745027, 419 | "stops": [ 420 | { 421 | "position": 0, 422 | "color": "#ff9a7866" 423 | }, 424 | { 425 | "position": 1, 426 | "color": "#fdd4c600" 427 | } 428 | ] 429 | } 430 | }, 431 | "2": { 432 | "type": "custom-gradient", 433 | "value": { 434 | "gradientType": "radial", 435 | "rotation": 242.87929516726268, 436 | "stops": [ 437 | { 438 | "position": 0, 439 | "color": "#d1d3ff66" 440 | }, 441 | { 442 | "position": 1, 443 | "color": "#d1d3ff00" 444 | } 445 | ] 446 | } 447 | }, 448 | "description": "", 449 | "extensions": { 450 | "org.lukasoppermann.figmaDesignTokens": { 451 | "styleId": "S:7ab6186cdc20f9368d28a719c10ae6ba3e3b3875,", 452 | "exportKey": "color" 453 | } 454 | } 455 | }, 456 | "azalea": { 457 | "0": { 458 | "type": "color", 459 | "value": "#f6f0f9ff", 460 | "blendMode": "normal" 461 | }, 462 | "1": { 463 | "type": "custom-gradient", 464 | "value": { 465 | "gradientType": "radial", 466 | "rotation": 152.94135576370823, 467 | "stops": [ 468 | { 469 | "position": 0, 470 | "color": "#ffcdeeb3" 471 | }, 472 | { 473 | "position": 1, 474 | "color": "#ffcdee00" 475 | } 476 | ] 477 | } 478 | }, 479 | "2": { 480 | "type": "custom-gradient", 481 | "value": { 482 | "gradientType": "radial", 483 | "rotation": 216.19057909691782, 484 | "stops": [ 485 | { 486 | "position": 0, 487 | "color": "#fac7bdb3" 488 | }, 489 | { 490 | "position": 1, 491 | "color": "#fac7bd00" 492 | } 493 | ] 494 | } 495 | }, 496 | "description": "", 497 | "extensions": { 498 | "org.lukasoppermann.figmaDesignTokens": { 499 | "styleId": "S:d3edf81779615f814b01a2e7b4a9fe14dfc18e9b,", 500 | "exportKey": "color" 501 | } 502 | } 503 | }, 504 | "cinderella": { 505 | "0": { 506 | "type": "color", 507 | "value": "#fcf5edff", 508 | "blendMode": "normal" 509 | }, 510 | "1": { 511 | "type": "custom-gradient", 512 | "value": { 513 | "gradientType": "radial", 514 | "rotation": 212.8227513496966, 515 | "stops": [ 516 | { 517 | "position": 0, 518 | "color": "#fac7bdb3" 519 | }, 520 | { 521 | "position": 1, 522 | "color": "#fac7bd00" 523 | } 524 | ] 525 | } 526 | }, 527 | "2": { 528 | "type": "custom-gradient", 529 | "value": { 530 | "gradientType": "radial", 531 | "rotation": 132.17632118978167, 532 | "stops": [ 533 | { 534 | "position": 0, 535 | "color": "#ffc4fcff" 536 | }, 537 | { 538 | "position": 1, 539 | "color": "#ffc4fc00" 540 | } 541 | ] 542 | } 543 | }, 544 | "description": "", 545 | "extensions": { 546 | "org.lukasoppermann.figmaDesignTokens": { 547 | "styleId": "S:42a9a58579ad4e45e933d4ca788203fc126dad00,", 548 | "exportKey": "color" 549 | } 550 | } 551 | } 552 | }, 553 | "skeleton": { 554 | "body": { 555 | "description": "", 556 | "type": "color", 557 | "value": "#00000005", 558 | "blendMode": "normal", 559 | "extensions": { 560 | "org.lukasoppermann.figmaDesignTokens": { 561 | "styleId": "S:49e0cd063607233dd883b460056a259acb9d6b00,", 562 | "exportKey": "color" 563 | } 564 | } 565 | } 566 | } 567 | }, 568 | "gradient": { 569 | "gradient": { 570 | "peach": { 571 | "description": "", 572 | "type": "custom-gradient", 573 | "value": { 574 | "gradientType": "radial", 575 | "rotation": 180, 576 | "stops": [ 577 | { 578 | "position": 0, 579 | "color": "#fdcebeff" 580 | }, 581 | { 582 | "position": 1, 583 | "color": "#fdcebe00" 584 | } 585 | ] 586 | }, 587 | "extensions": { 588 | "org.lukasoppermann.figmaDesignTokens": { 589 | "styleId": "S:bf481399553f24520faef59318c9db4199b31dbd,", 590 | "exportKey": "gradient" 591 | } 592 | } 593 | }, 594 | "velvet": { 595 | "description": "", 596 | "type": "custom-gradient", 597 | "value": { 598 | "gradientType": "radial", 599 | "rotation": 180, 600 | "stops": [ 601 | { 602 | "position": 0, 603 | "color": "#c4c7f2ff" 604 | }, 605 | { 606 | "position": 1, 607 | "color": "#c4c7f200" 608 | } 609 | ] 610 | }, 611 | "extensions": { 612 | "org.lukasoppermann.figmaDesignTokens": { 613 | "styleId": "S:3b451e5da0a46dd00ad2654bbcc0416abbeda399,", 614 | "exportKey": "gradient" 615 | } 616 | } 617 | }, 618 | "ice": { 619 | "description": "", 620 | "type": "custom-gradient", 621 | "value": { 622 | "gradientType": "radial", 623 | "rotation": 180, 624 | "stops": [ 625 | { 626 | "position": 0, 627 | "color": "#a4eff4ff" 628 | }, 629 | { 630 | "position": 1, 631 | "color": "#a4eff400" 632 | } 633 | ] 634 | }, 635 | "extensions": { 636 | "org.lukasoppermann.figmaDesignTokens": { 637 | "styleId": "S:6923a9c779681a19d7301a641dc188dc0fd0ebe0,", 638 | "exportKey": "gradient" 639 | } 640 | } 641 | }, 642 | "pink": { 643 | "description": "", 644 | "type": "custom-gradient", 645 | "value": { 646 | "gradientType": "radial", 647 | "rotation": 180, 648 | "stops": [ 649 | { 650 | "position": 0, 651 | "color": "#ffc2ffff" 652 | }, 653 | { 654 | "position": 1, 655 | "color": "#ffc2ff00" 656 | } 657 | ] 658 | }, 659 | "extensions": { 660 | "org.lukasoppermann.figmaDesignTokens": { 661 | "styleId": "S:9da04d114626f0c2184657724579fbb25bbe571a,", 662 | "exportKey": "gradient" 663 | } 664 | } 665 | }, 666 | "banana": { 667 | "description": "", 668 | "type": "custom-gradient", 669 | "value": { 670 | "gradientType": "radial", 671 | "rotation": 180, 672 | "stops": [ 673 | { 674 | "position": 0, 675 | "color": "#fde4beff" 676 | }, 677 | { 678 | "position": 1, 679 | "color": "#fde4be00" 680 | } 681 | ] 682 | }, 683 | "extensions": { 684 | "org.lukasoppermann.figmaDesignTokens": { 685 | "styleId": "S:569de94fdbf5d6c466b15f2ee5b5399088fe8b40,", 686 | "exportKey": "gradient" 687 | } 688 | } 689 | } 690 | }, 691 | "complex gradients": { 692 | "lace": { 693 | "description": "", 694 | "type": "custom-gradient", 695 | "value": { 696 | "gradientType": "radial", 697 | "rotation": 201.9467027950271, 698 | "stops": [ 699 | { 700 | "position": 0, 701 | "color": "#fbe5dbff" 702 | }, 703 | { 704 | "position": 1, 705 | "color": "#fcf5edff" 706 | } 707 | ] 708 | }, 709 | "extensions": { 710 | "org.lukasoppermann.figmaDesignTokens": { 711 | "styleId": "S:201586222cc03aa56670383131be5c09e6a5e48c,", 712 | "exportKey": "gradient" 713 | } 714 | } 715 | }, 716 | "remy": { 717 | "description": "", 718 | "type": "custom-gradient", 719 | "value": { 720 | "gradientType": "radial", 721 | "rotation": 227.99510789290616, 722 | "stops": [ 723 | { 724 | "position": 0, 725 | "color": "#ffe9f5ff" 726 | }, 727 | { 728 | "position": 1, 729 | "color": "#fdf6eeff" 730 | } 731 | ] 732 | }, 733 | "extensions": { 734 | "org.lukasoppermann.figmaDesignTokens": { 735 | "styleId": "S:61df58d030e3bda6ca1ce1b5a237c02cc59a3f84,", 736 | "exportKey": "gradient" 737 | } 738 | } 739 | }, 740 | "lilac": { 741 | "description": "", 742 | "type": "custom-gradient", 743 | "value": { 744 | "gradientType": "radial", 745 | "rotation": 190.87763528252307, 746 | "stops": [ 747 | { 748 | "position": 0, 749 | "color": "#dbd9faff" 750 | }, 751 | { 752 | "position": 1, 753 | "color": "#f5eff9ff" 754 | } 755 | ] 756 | }, 757 | "extensions": { 758 | "org.lukasoppermann.figmaDesignTokens": { 759 | "styleId": "S:0b80ee98165b04eee57ecf7bd990c1c3d2417f05,", 760 | "exportKey": "gradient" 761 | } 762 | } 763 | } 764 | } 765 | }, 766 | "grid": { 767 | "375": { 768 | "description": null, 769 | "type": "custom-grid", 770 | "value": { 771 | "pattern": "columns", 772 | "gutterSize": 20, 773 | "alignment": "stretch", 774 | "count": 6, 775 | "offset": 20 776 | }, 777 | "extensions": { 778 | "org.lukasoppermann.figmaDesignTokens": { 779 | "styleId": "S:aebb61791e3dcfcefa0097b51bb1c083a8c64d69,", 780 | "exportKey": "grid" 781 | } 782 | } 783 | }, 784 | "1024": { 785 | "description": null, 786 | "type": "custom-grid", 787 | "value": { 788 | "pattern": "columns", 789 | "gutterSize": 20, 790 | "alignment": "stretch", 791 | "count": 12, 792 | "offset": 40 793 | }, 794 | "extensions": { 795 | "org.lukasoppermann.figmaDesignTokens": { 796 | "styleId": "S:fc2ce074402569c53b99f0c0965540c23efc73ff,", 797 | "exportKey": "grid" 798 | } 799 | } 800 | }, 801 | "1440": { 802 | "description": null, 803 | "type": "custom-grid", 804 | "value": { 805 | "pattern": "columns", 806 | "gutterSize": 20, 807 | "alignment": "stretch", 808 | "count": 12, 809 | "offset": 50 810 | }, 811 | "extensions": { 812 | "org.lukasoppermann.figmaDesignTokens": { 813 | "styleId": "S:bafbb7e38711d50926a17e777258a7ce4c0c140f,", 814 | "exportKey": "grid" 815 | } 816 | } 817 | } 818 | } 819 | } 820 | -------------------------------------------------------------------------------- /examples/tokens-examples/dtcg.json: -------------------------------------------------------------------------------- 1 | { 2 | "Color": { 3 | "$type": "color", 4 | "background-body": { 5 | "$value": "#233042" 6 | }, 7 | "primary-body": { 8 | "$value": "#354156" 9 | }, 10 | "blue-light": { 11 | "$value": "#30aab3" 12 | }, 13 | "yellow": { 14 | "$value": "#ffca30" 15 | }, 16 | "red": { 17 | "$value": "#fa3757" 18 | }, 19 | "red-dark": { 20 | "$value": "#e02045" 21 | }, 22 | "green-dark": { 23 | "$value": "#4d967b" 24 | }, 25 | "background-primary": { 26 | "$value": "#fcfcf4" 27 | } 28 | }, 29 | "Spacing": { 30 | "$type": "dimension", 31 | "small": { 32 | "$value": "4px" 33 | }, 34 | "medium": { 35 | "$value": "8px" 36 | }, 37 | "large": { 38 | "$value": "12px" 39 | }, 40 | "x-large": { 41 | "$value": "20px" 42 | } 43 | }, 44 | "Duration": { 45 | "$type": "duration", 46 | "paused": { 47 | "$value": "paused" 48 | }, 49 | "slow": { 50 | "$value": "3s" 51 | }, 52 | "fast": { 53 | "$value": "500ms" 54 | } 55 | }, 56 | "Easing": { 57 | "$type": "easing", 58 | "easeInSine": { 59 | "$value": "cubic-bezier(0.12, 0, 0.39, 0)" 60 | }, 61 | "easeOutSine": { 62 | "$value": "cubic-bezier(0.61, 1, 0.88, 1)500ms" 63 | } 64 | }, 65 | "Radius": { 66 | "$type": "string", 67 | "circle": { 68 | "$value": "50%" 69 | }, 70 | "large": { 71 | "$value": "8px" 72 | }, 73 | "small": { 74 | "$value": "2px" 75 | } 76 | }, 77 | "Opacity": { 78 | "$type": "opacity", 79 | "opacity-25": { 80 | "$value": "0.25" 81 | }, 82 | "opacity-50": { 83 | "$value": "0.5" 84 | }, 85 | "opacity-75": { 86 | "$value": "0.75" 87 | } 88 | }, 89 | "Shadow": { 90 | "$type": "shadow", 91 | "level-1": { 92 | "$value": "0 1px 1px 0 rgba(0,0,0,0.14), 0 2px 1px -1px rgba(0,0,0,0.12), 0 1px 3px 0 rgba(0,0,0,0.20);" 93 | }, 94 | "level-2": { 95 | "$value": "0 3px 4px 0 rgba(0,0,0,0.14), 0 3px 3px -2px rgba(0,0,0,0.12), 0 1px 8px 0 rgba(0,0,0,0.20);" 96 | }, 97 | "level-3": { 98 | "$value": "0 6px 10px 0 rgba(0,0,0,0.14), 0 1px 18px 0 rgba(0,0,0,0.12), 0 3px 5px -1px rgba(0,0,0,0.20)" 99 | } 100 | }, 101 | "Media Query": { 102 | "$type": "dimension", 103 | "max-width-mobile": { 104 | "$value": "600px" 105 | }, 106 | "max-width-tablet": { 107 | "$value": "1024px" 108 | } 109 | }, 110 | "Font Family": { 111 | "$type": "fontFamily", 112 | "body": { 113 | "$value": "Arial, Helvetica, sans-serif" 114 | }, 115 | "headings": { 116 | "$value": "Palatino Linotype, serif" 117 | } 118 | }, 119 | "Font Size": { 120 | "$type": "dimension", 121 | "caption": { 122 | "$value": "12px" 123 | }, 124 | "body": { 125 | "$value": "16px" 126 | }, 127 | "headings": { 128 | "$value": "26px" 129 | } 130 | }, 131 | "Letter Spacing": { 132 | "$type": "dimension", 133 | "dense": { 134 | "$value": "-1px" 135 | }, 136 | "double": { 137 | "$value": "2px" 138 | } 139 | }, 140 | "Line Height": { 141 | "$type": "dimension", 142 | "heading": { 143 | "$value": "1.25" 144 | }, 145 | "reset": { 146 | "$value": "1" 147 | }, 148 | "text": { 149 | "$value": "1.5" 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /examples/tokens-examples/simple-plugin-export: -------------------------------------------------------------------------------- 1 | /* colors.light.tokens.json */ 2 | 3 | { 4 | "keys": { 5 | "on primary container": { 6 | "$type": "color", 7 | "$value": "{base.primary.10}" 8 | }, 9 | "primary container": { 10 | "$type": "color", 11 | "$value": "{base.primary.90}" 12 | }, 13 | "on primary": { 14 | "$type": "color", 15 | "$value": "{base.primary.10}" 16 | }, 17 | "pimary": { 18 | "$type": "color", 19 | "$value": "{base.primary.50}" 20 | } 21 | }, 22 | "base": { 23 | "neutral": { 24 | "10": { 25 | "$type": "color", 26 | "$value": "#1f1f1f" 27 | }, 28 | "20": { 29 | "$type": "color", 30 | "$value": "#373737" 31 | }, 32 | "25": { 33 | "$type": "color", 34 | "$value": "#505050" 35 | }, 36 | "35": { 37 | "$type": "color", 38 | "$value": "#686868" 39 | } 40 | }, 41 | "primary": { 42 | "10": { 43 | "$type": "color", 44 | "$value": "#07412b" 45 | }, 46 | "20": { 47 | "$type": "color", 48 | "$value": "#0c714c" 49 | }, 50 | "30": { 51 | "$type": "color", 52 | "$value": "#11a26d" 53 | }, 54 | "40": { 55 | "$type": "color", 56 | "$value": "#16d28d" 57 | }, 58 | "50": { 59 | "$type": "color", 60 | "$value": "#36eaa9" 61 | }, 62 | "60": { 63 | "$type": "color", 64 | "$value": "#5feeba" 65 | }, 66 | "70": { 67 | "$type": "color", 68 | "$value": "#89f3cc" 69 | }, 70 | "80": { 71 | "$type": "color", 72 | "$value": "#b4f7df" 73 | }, 74 | "90": { 75 | "$type": "color", 76 | "$value": "#dffcf1" 77 | } 78 | } 79 | } 80 | } 81 | 82 | 83 | /* colors.dark.tokens.json */ 84 | 85 | { 86 | "keys": { 87 | "on primary container": { 88 | "$type": "color", 89 | "$value": "{base.primary.10}" 90 | }, 91 | "primary container": { 92 | "$type": "color", 93 | "$value": "{base.primary.90}" 94 | }, 95 | "on primary": { 96 | "$type": "color", 97 | "$value": "{base.primary.10}" 98 | }, 99 | "pimary": { 100 | "$type": "color", 101 | "$value": "{base.primary.50}" 102 | } 103 | }, 104 | "base": { 105 | "neutral": { 106 | "10": { 107 | "$type": "color", 108 | "$value": "#949494" 109 | }, 110 | "20": { 111 | "$type": "color", 112 | "$value": "#7b7b7b" 113 | }, 114 | "25": { 115 | "$type": "color", 116 | "$value": "#6f6f6f" 117 | }, 118 | "35": { 119 | "$type": "color", 120 | "$value": "#545454" 121 | } 122 | }, 123 | "primary": { 124 | "10": { 125 | "$type": "color", 126 | "$value": "#092b1f" 127 | }, 128 | "20": { 129 | "$type": "color", 130 | "$value": "#104c36" 131 | }, 132 | "30": { 133 | "$type": "color", 134 | "$value": "#166c4d" 135 | }, 136 | "40": { 137 | "$type": "color", 138 | "$value": "#1d8d64" 139 | }, 140 | "50": { 141 | "$type": "color", 142 | "$value": "#24ae7b" 143 | }, 144 | "60": { 145 | "$type": "color", 146 | "$value": "#29c98e" 147 | }, 148 | "70": { 149 | "$type": "color", 150 | "$value": "#3cd79e" 151 | }, 152 | "80": { 153 | "$type": "color", 154 | "$value": "#57ddac" 155 | }, 156 | "90": { 157 | "$type": "color", 158 | "$value": "#72e2b9" 159 | } 160 | } 161 | } 162 | } 163 | 164 | 165 | /* spacers.Mode 1.tokens.json */ 166 | 167 | { 168 | "4": { 169 | "$type": "number", 170 | "$value": 4 171 | }, 172 | "8": { 173 | "$type": "number", 174 | "$value": 8 175 | }, 176 | "12": { 177 | "$type": "number", 178 | "$value": 12 179 | }, 180 | "16": { 181 | "$type": "number", 182 | "$value": 16 183 | }, 184 | "24": { 185 | "$type": "number", 186 | "$value": 24 187 | }, 188 | "32": { 189 | "$type": "number", 190 | "$value": 32 191 | }, 192 | "40": { 193 | "$type": "number", 194 | "$value": 40 195 | }, 196 | "52": { 197 | "$type": "number", 198 | "$value": 52 199 | }, 200 | "64": { 201 | "$type": "number", 202 | "$value": 64 203 | }, 204 | "76": { 205 | "$type": "number", 206 | "$value": 76 207 | } 208 | } -------------------------------------------------------------------------------- /examples/tokens-examples/tokens-studio-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "global": { 3 | "Main": { 4 | "0": { 5 | "value": "#ffffff", 6 | "type": "color" 7 | }, 8 | "50": { 9 | "value": "#f4f4f4", 10 | "type": "color" 11 | }, 12 | "100": { 13 | "value": "#b9b9b9", 14 | "type": "color" 15 | }, 16 | "200": { 17 | "value": "#8a8a8a", 18 | "type": "color" 19 | }, 20 | "400": { 21 | "value": "#373737", 22 | "type": "color" 23 | }, 24 | "500": { 25 | "value": "#141414", 26 | "type": "color" 27 | }, 28 | "Transparent": { 29 | "50": { 30 | "value": "#1414140a", 31 | "type": "color" 32 | }, 33 | "80": { 34 | "value": "#14141414", 35 | "type": "color" 36 | }, 37 | "200": { 38 | "value": "#14141480", 39 | "type": "color" 40 | }, 41 | "400": { 42 | "value": "#141414d9", 43 | "type": "color" 44 | } 45 | } 46 | }, 47 | "Accent": { 48 | "50": { 49 | "value": "#f6f0f9", 50 | "type": "color" 51 | }, 52 | "200": { 53 | "value": "#e1c6f3", 54 | "type": "color" 55 | }, 56 | "500": { 57 | "value": "#b380d4", 58 | "type": "color" 59 | }, 60 | "800": { 61 | "value": "#9a63c3", 62 | "type": "color" 63 | }, 64 | "Transparent": { 65 | "50": { 66 | "value": "#b380d41f", 67 | "type": "color" 68 | }, 69 | "200": { 70 | "value": "#b380d470", 71 | "type": "color" 72 | } 73 | } 74 | }, 75 | "Peach": { 76 | "50": { 77 | "value": "#fcf5ed", 78 | "type": "color" 79 | }, 80 | "200": { 81 | "value": "#feeadf", 82 | "type": "color" 83 | }, 84 | "500": { 85 | "value": "#fdcebe", 86 | "type": "color" 87 | }, 88 | "900": { 89 | "value": "#fcb099", 90 | "type": "color" 91 | }, 92 | "Transparent": { 93 | "50": { 94 | "value": "#fdd8be42", 95 | "type": "color" 96 | }, 97 | "200": { 98 | "value": "#fdd5be70", 99 | "type": "color" 100 | } 101 | } 102 | }, 103 | "Service": { 104 | "Error": { 105 | "Main": { 106 | "value": "#f54e5d", 107 | "type": "color" 108 | }, 109 | "Dark": { 110 | "value": "#a8333b", 111 | "type": "color" 112 | }, 113 | "Light": { 114 | "value": "#ffeef1", 115 | "type": "color" 116 | } 117 | }, 118 | "Warning": { 119 | "Main": { 120 | "value": "#ffca64", 121 | "type": "color" 122 | }, 123 | "Dark": { 124 | "value": "#d07408", 125 | "type": "color" 126 | }, 127 | "Light": { 128 | "value": "#fff6e4", 129 | "type": "color" 130 | } 131 | }, 132 | "Success": { 133 | "Main": { 134 | "value": "#d3fb60", 135 | "type": "color" 136 | }, 137 | "Dark": { 138 | "value": "#588909", 139 | "type": "color" 140 | }, 141 | "Light": { 142 | "value": "#f5fced", 143 | "type": "color" 144 | } 145 | } 146 | }, 147 | "Skeleton": { 148 | "Body": { 149 | "value": "#00000005", 150 | "type": "color" 151 | } 152 | }, 153 | "grad": { 154 | "value": "#d49999", 155 | "type": "color" 156 | }, 157 | "Gradient": { 158 | "value": "linear-gradient(45deg, #ffffff 0%, #000000 100%)", 159 | "type": "color", 160 | "description": "gradient" 161 | } 162 | }, 163 | "$themes": [], 164 | "$metadata": { 165 | "tokenSetOrder": ["global"] 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /examples/tokens-examples/tokens-studio-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "fg": { 3 | "default": { 4 | "value": "{colors.black}", 5 | "type": "color" 6 | }, 7 | "muted": { 8 | "value": "{colors.gray.700}", 9 | "type": "color" 10 | }, 11 | "subtle": { 12 | "value": "{colors.gray.500}", 13 | "type": "color" 14 | } 15 | }, 16 | "bg": { 17 | "default": { 18 | "value": "{colors.white}", 19 | "type": "color" 20 | }, 21 | "muted": { 22 | "value": "{colors.gray.100}", 23 | "type": "color" 24 | }, 25 | "subtle": { 26 | "value": "{colors.gray.200}", 27 | "type": "color" 28 | } 29 | }, 30 | "accent": { 31 | "default": { 32 | "value": "{colors.indigo.400}", 33 | "type": "color" 34 | }, 35 | "onAccent": { 36 | "value": "{colors.white}", 37 | "type": "color" 38 | }, 39 | "bg": { 40 | "value": "{colors.indigo.200}", 41 | "type": "color" 42 | } 43 | }, 44 | "shadows": { 45 | "default": { 46 | "value": "{colors.gray.900}", 47 | "type": "color" 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /global.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | type nameConventionType = 5 | | 'none' 6 | | 'PascalCase' 7 | | 'camelCase' 8 | | 'snake_case' 9 | | 'kebab-case' 10 | | 'UPPERCASE' 11 | | 'lowercase' 12 | | 'MACRO_CASE' 13 | | 'COBOL-CASE' 14 | | 'Cobol case' 15 | | 'Ada_Case' 16 | | 'dot.notation'; 17 | 18 | type colorModeType = 19 | | 'hex' 20 | | 'rgba-object' 21 | | 'rgba-css' 22 | | 'hsla-object' 23 | | 'hsla-css'; 24 | 25 | type stylesType = 'text' | 'colors' | 'effects' | 'grids'; 26 | 27 | type variableFeatureType = 'scope' | 'hidden'; 28 | 29 | type JSONSettingsStyleType = { 30 | isIncluded: boolean; 31 | customName: string; 32 | }; 33 | 34 | interface IncludedStylesI { 35 | text: JSONSettingsStyleType; 36 | effects: JSONSettingsStyleType; 37 | grids: JSONSettingsStyleType; 38 | } 39 | 40 | interface JsonbinCredentialsI { 41 | isEnabled: boolean; 42 | id?: string; 43 | name: string; 44 | secretKey: string; 45 | } 46 | 47 | interface GithubCredentialsI { 48 | isEnabled: boolean; 49 | token: string; 50 | repo: string; 51 | branch: string; 52 | fileName: string; 53 | owner: string; 54 | commitMessage?: string; 55 | } 56 | 57 | interface GithubPullRequestCredentialsI { 58 | isEnabled: boolean; 59 | token: string; 60 | repo: string; 61 | baseBranch: string; 62 | branch: string; 63 | fileName: string; 64 | owner: string; 65 | commitMessage?: string; 66 | pullRequestTitle?: string; 67 | pullRequestBody?: string; 68 | } 69 | 70 | interface GitlabCredentialsI { 71 | isEnabled: boolean; 72 | owner: string; 73 | host: string; 74 | repo: string; 75 | branch: string; 76 | fileName: string; 77 | token: string; 78 | commitMessage?: string; 79 | } 80 | 81 | interface CustomURLCredentialsI { 82 | isEnabled: boolean; 83 | url: string; 84 | method: 'POST' | 'PUT'; 85 | headers: string; 86 | } 87 | 88 | interface ExportSettingsI { 89 | includedStyles: IncludedStylesI; 90 | includeScopes: boolean; 91 | useDTCGKeys: boolean; 92 | includeValueStringKeyToAlias: boolean; 93 | colorMode: colorModeType; 94 | storeStyleInCollection: string; 95 | includeFigmaMetaData: boolean; 96 | } 97 | 98 | interface ServerSettingsI { 99 | jsonbin: JsonbinCredentialsI; 100 | github: GithubCredentialsI; 101 | githubPullRequest: GithubPullRequestCredentialsI; 102 | gitlab: GitlabCredentialsI; 103 | customURL: CustomURLCredentialsI; 104 | } 105 | type PluginStateI = { 106 | variableCollections: string[]; 107 | }; 108 | 109 | type JSONSettingsConfigI = ExportSettingsI & 110 | PluginStateI & { 111 | servers: ServerSettingsI; 112 | }; 113 | 114 | interface PluginTokenI { 115 | $value: string; 116 | $type: TokenType; 117 | $description: string; 118 | scopes?: VariableScope[]; 119 | $extensions: { 120 | mode: Object; 121 | figma?: { 122 | variableId: string; 123 | codeSyntax: { 124 | WEB?: string; 125 | iOS?: string; 126 | ANDROID?: string; 127 | }; 128 | collection: { 129 | id: string; 130 | name: string; 131 | defaultModeId: string; 132 | }; 133 | }; 134 | }; 135 | } 136 | 137 | type ServerType = 138 | | 'jsonbin' 139 | | 'github' 140 | | 'githubPullRequest' 141 | | 'gitlab' 142 | | 'bitbucket' 143 | | 'customURL' 144 | | 'none'; 145 | 146 | interface TokensMessageI { 147 | type: 'getTokens' | 'setTokens'; 148 | tokens: any; 149 | role: 'preview' | 'push' | 'download'; 150 | server: ServerType[]; 151 | } 152 | 153 | interface MetaPropsI { 154 | useDTCGKeys: boolean; 155 | colorMode: colorModeType; 156 | variableCollections: string[] | undefined; 157 | createdAt: string; 158 | } 159 | 160 | interface ToastIPropsI { 161 | title: string; 162 | message: string; 163 | options: { 164 | type?: 'success' | 'error' | 'warn' | 'info'; 165 | timeout?: number; 166 | onClose?: () => void; 167 | }; 168 | } 169 | 170 | // Extend Figmas PaintStyle interface 171 | interface PaintStyleExtended extends PaintStyle { 172 | readonly boundVariables?: { 173 | readonly paints: VariableAlias[]; 174 | }; 175 | } 176 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "TokensBrücke", 3 | "id": "1254538877056388290", 4 | "api": "1.0.0", 5 | "main": "code.js", 6 | "editorType": ["figma"], 7 | "ui": "ui.html", 8 | "networkAccess": { 9 | "allowedDomains": ["*"], 10 | "reasoning": "There is a push method that uses a custom URL. This URL can be any" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tokens-bruecke", 3 | "version": "2.6.2", 4 | "license": "MIT", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/tokens-bruecke/figma-plugin.git" 8 | }, 9 | "description": "Tokens Bruecke is a Figma cli that helps you to export varibales as design tokens.", 10 | "keywords": [ 11 | "figma", 12 | "plugin", 13 | "tokens", 14 | "design tokens", 15 | "design system" 16 | ], 17 | "author": { 18 | "name": "Pavel Laptev", 19 | "url": "https://pavellaptev.github.io" 20 | }, 21 | "scripts": { 22 | "build:cli": "webpack --mode=production --config webpack.config.cli.js", 23 | "build:plugin": "webpack --mode=production", 24 | "build": "npm run build:cli && npm run build:plugin", 25 | "dev": "webpack --mode=development --watch", 26 | "test": "vitest" 27 | }, 28 | "type": "commonjs", 29 | "main": "bin/cli.js", 30 | "bin": { 31 | "tokens-bruecke": "bin/cli.js" 32 | }, 33 | "dependencies": { 34 | "decimal.js": "^10.5.0", 35 | "figma-api": "2.0.1-beta", 36 | "yargs": "^17.7.2" 37 | }, 38 | "devDependencies": { 39 | "@figma/plugin-typings": "^1.100.0", 40 | "@figma/rest-api-spec": "^0.25.0", 41 | "@octokit/core": "^5.0.0", 42 | "@tokens-bruecke/token-types": "git+https://github.com/tokens-bruecke/token-types.git#1.4.0", 43 | "@types/node": "^22.7.4", 44 | "@types/react": "^18.0.33", 45 | "@types/react-dom": "^18.0.11", 46 | "@types/yargs": "^17.0.33", 47 | "buffer": "^6.0.3", 48 | "clipboard-copy": "^4.0.1", 49 | "copy-webpack-plugin": "^13.0.0", 50 | "css-loader": "^6.8.1", 51 | "html-inline-css-webpack-plugin": "^1.11.1", 52 | "html-inline-script-webpack-plugin": "^3.2.0", 53 | "html-webpack-plugin": "^5.5.0", 54 | "mini-css-extract-plugin": "^2.7.2", 55 | "pavelLaptev/react-figma-ui": "git+https://git@github.com/PavelLaptev/react-figma-ui.git#4c8d20dbd911c0008fd4d0b20280f14404f186e7", 56 | "prettier": "^2.7.1", 57 | "react": "^18.2.0", 58 | "react-dev-utils": "^12.0.1", 59 | "react-dom": "^18.2.0", 60 | "sass": "^1.57.1", 61 | "sass-loader": "^13.2.0", 62 | "style-loader": "^3.3.1", 63 | "ts-loader": "^9.3.1", 64 | "typescript": "^4.7.4", 65 | "url-loader": "^4.1.1", 66 | "vitest": "^3.1.1", 67 | "webpack": "^5.74.0", 68 | "webpack-cli": "^4.10.0", 69 | "zip-webpack-plugin": "^4.0.3" 70 | }, 71 | "packageManager": "pnpm@9.11.0+sha512.0a203ffaed5a3f63242cd064c8fb5892366c103e328079318f78062f24ea8c9d50bc6a47aa3567cabefd824d170e78fa2745ed1f16b132e16436146b7688f19b" 72 | } 73 | -------------------------------------------------------------------------------- /readme-assets/fig1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tokens-bruecke/figma-plugin/5b2bb6344eab85c68c6bbc9a5b6325c13b111705/readme-assets/fig1.webp -------------------------------------------------------------------------------- /readme-assets/fig10.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tokens-bruecke/figma-plugin/5b2bb6344eab85c68c6bbc9a5b6325c13b111705/readme-assets/fig10.webp -------------------------------------------------------------------------------- /readme-assets/fig11.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tokens-bruecke/figma-plugin/5b2bb6344eab85c68c6bbc9a5b6325c13b111705/readme-assets/fig11.webp -------------------------------------------------------------------------------- /readme-assets/fig12.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tokens-bruecke/figma-plugin/5b2bb6344eab85c68c6bbc9a5b6325c13b111705/readme-assets/fig12.webp -------------------------------------------------------------------------------- /readme-assets/fig13.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tokens-bruecke/figma-plugin/5b2bb6344eab85c68c6bbc9a5b6325c13b111705/readme-assets/fig13.webp -------------------------------------------------------------------------------- /readme-assets/fig2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tokens-bruecke/figma-plugin/5b2bb6344eab85c68c6bbc9a5b6325c13b111705/readme-assets/fig2.webp -------------------------------------------------------------------------------- /readme-assets/fig3.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tokens-bruecke/figma-plugin/5b2bb6344eab85c68c6bbc9a5b6325c13b111705/readme-assets/fig3.webp -------------------------------------------------------------------------------- /readme-assets/fig4.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tokens-bruecke/figma-plugin/5b2bb6344eab85c68c6bbc9a5b6325c13b111705/readme-assets/fig4.webp -------------------------------------------------------------------------------- /readme-assets/fig5.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tokens-bruecke/figma-plugin/5b2bb6344eab85c68c6bbc9a5b6325c13b111705/readme-assets/fig5.webp -------------------------------------------------------------------------------- /readme-assets/fig6.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tokens-bruecke/figma-plugin/5b2bb6344eab85c68c6bbc9a5b6325c13b111705/readme-assets/fig6.webp -------------------------------------------------------------------------------- /readme-assets/fig7.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tokens-bruecke/figma-plugin/5b2bb6344eab85c68c6bbc9a5b6325c13b111705/readme-assets/fig7.webp -------------------------------------------------------------------------------- /readme-assets/fig8.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tokens-bruecke/figma-plugin/5b2bb6344eab85c68c6bbc9a5b6325c13b111705/readme-assets/fig8.webp -------------------------------------------------------------------------------- /readme-assets/fig9.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tokens-bruecke/figma-plugin/5b2bb6344eab85c68c6bbc9a5b6325c13b111705/readme-assets/fig9.webp -------------------------------------------------------------------------------- /readme-assets/preview.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tokens-bruecke/figma-plugin/5b2bb6344eab85c68c6bbc9a5b6325c13b111705/readme-assets/preview.webp -------------------------------------------------------------------------------- /readme-assets/rename-styles.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tokens-bruecke/figma-plugin/5b2bb6344eab85c68c6bbc9a5b6325c13b111705/readme-assets/rename-styles.gif -------------------------------------------------------------------------------- /src/app/api/downloadTokensFile.ts: -------------------------------------------------------------------------------- 1 | // code to download a json object as a file 2 | // https://stackoverflow.com/questions/19721439/download-json-object-as-a-file-from-browser 3 | 4 | export const downloadTokensFile = (objectToSave) => { 5 | const fileName = 'design.tokens.json'; 6 | const json = JSON.stringify(objectToSave, null, 2); 7 | const blob = new Blob([json], { type: 'application/json' }); 8 | const url = URL.createObjectURL(blob); 9 | 10 | const link = document.createElement('a'); 11 | link.href = url; 12 | link.download = fileName; 13 | 14 | document.body.appendChild(link); 15 | link.click(); 16 | document.body.removeChild(link); 17 | 18 | URL.revokeObjectURL(url); 19 | }; 20 | -------------------------------------------------------------------------------- /src/app/api/pluginApiResolver.ts: -------------------------------------------------------------------------------- 1 | import { IResolver } from '../../common/resolver'; 2 | 3 | export class PluginAPIResolver implements IResolver { 4 | async getLocalEffectStyles(): Promise { 5 | return figma.getLocalEffectStyles(); 6 | } 7 | 8 | async getLocalVariableCollections(): Promise { 9 | return figma.variables.getLocalVariableCollectionsAsync(); 10 | } 11 | 12 | async getLocalVariables(): Promise { 13 | return figma.variables.getLocalVariablesAsync(); 14 | } 15 | 16 | async getLocalGridStyles(): Promise { 17 | return figma.getLocalGridStylesAsync(); 18 | } 19 | 20 | async getLocalTextStyles(): Promise { 21 | return figma.getLocalTextStylesAsync(); 22 | } 23 | 24 | getVariableById(variableId: string): Variable { 25 | return figma.variables.getVariableById(variableId); 26 | } 27 | 28 | getVariableCollectionById(id: string): VariableCollection { 29 | return figma.variables.getVariableCollectionById(id); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/app/api/servers/githubPullRequest.ts: -------------------------------------------------------------------------------- 1 | import { Octokit } from '@octokit/core'; 2 | 3 | export const githubPullRequest = async ( 4 | credentials: GithubPullRequestCredentialsI, 5 | tokens: any, 6 | toastCallback: (props: ToastIPropsI) => void 7 | ) => { 8 | const { token, owner, repo, baseBranch, fileName, pullRequestBody } = 9 | credentials; 10 | const branch = credentials.branch || 'tokens-bruecke/update-tokens'; 11 | const commitMessage = credentials.commitMessage || 'Update tokens'; 12 | const pullRequestTitle = 13 | credentials.pullRequestTitle || 'chore(tokens): update tokens'; 14 | const fileContent = JSON.stringify(tokens, null, 2); 15 | 16 | const octokit = new Octokit({ auth: token }); 17 | 18 | type Commit = Awaited>; 19 | type Ref = Awaited>; 20 | 21 | await create(); 22 | 23 | async function create() { 24 | try { 25 | console.log('start creating pull request'); 26 | const commit = await createCommit(); 27 | console.log('commit created'); 28 | await createOrUpdateBranch(commit); 29 | console.log('branch created or updated'); 30 | 31 | await createPullRequest(); 32 | console.log('pull request created'); 33 | toastCallback({ 34 | title: 'Github: Updated successfully', 35 | message: 'Github Pull Request has been updated successfully', 36 | options: { 37 | type: 'success', 38 | }, 39 | }); 40 | } catch (error) { 41 | console.log('error creating pull request', error); 42 | toastCallback({ 43 | title: 'Github: Error creating pull request', 44 | message: `Error creating pull request: ${error.message}.`, 45 | options: { 46 | type: 'error', 47 | }, 48 | }); 49 | } 50 | } 51 | 52 | function getBaseBranch() { 53 | return octokit.request('GET /repos/{owner}/{repo}/git/ref/{ref}', { 54 | owner, 55 | repo, 56 | ref: `heads/${baseBranch}`, 57 | }); 58 | } 59 | 60 | async function createCommit() { 61 | const baseBranch = await getBaseBranch(); 62 | console.log('base branch fetched'); 63 | const tree = await createTree(baseBranch); 64 | console.log('tree created'); 65 | 66 | return octokit.request('POST /repos/{owner}/{repo}/git/commits', { 67 | owner, 68 | repo, 69 | message: commitMessage, 70 | tree: tree.data.sha, 71 | parents: [baseBranch.data.object.sha], 72 | }); 73 | } 74 | 75 | async function createTree(baseRef: Ref) { 76 | return octokit.request('POST /repos/{owner}/{repo}/git/trees', { 77 | owner, 78 | repo, 79 | base_tree: baseRef.data.object.sha, 80 | tree: [ 81 | { 82 | path: fileName, 83 | // mode 100644 is regular file 84 | mode: '100644', 85 | type: 'blob', 86 | content: fileContent, 87 | }, 88 | ], 89 | }); 90 | } 91 | 92 | async function createOrUpdateBranch(commit: Commit) { 93 | if (await isBrunchExist(branch)) { 94 | console.log('update the branch'); 95 | updateBranch(commit); 96 | } else { 97 | console.log('create a branch'); 98 | createBranch(commit); 99 | } 100 | } 101 | 102 | function createBranch(commit: Commit) { 103 | return octokit.request('POST /repos/{owner}/{repo}/git/refs', { 104 | owner, 105 | repo, 106 | ref: `refs/heads/${branch}`, 107 | sha: commit.data.sha, 108 | }); 109 | } 110 | 111 | function updateBranch(commit: Commit) { 112 | return octokit.request('PATCH /repos/{owner}/{repo}/git/refs/{ref}', { 113 | owner, 114 | repo, 115 | ref: `heads/${branch}`, 116 | sha: commit.data.sha, 117 | force: true, 118 | }); 119 | } 120 | 121 | async function createPullRequest() { 122 | if (await isPullRequestExist(branch)) { 123 | console.log('pull request already exist'); 124 | return null; 125 | } 126 | 127 | return octokit.request('POST /repos/{owner}/{repo}/pulls', { 128 | owner, 129 | repo, 130 | title: pullRequestTitle, 131 | body: pullRequestBody, 132 | head: branch, 133 | base: baseBranch, 134 | }); 135 | } 136 | 137 | async function isBrunchExist(branch: string) { 138 | try { 139 | await octokit.request('GET /repos/{owner}/{repo}/git/refs/{ref}', { 140 | owner, 141 | repo, 142 | ref: `heads/${branch}`, 143 | }); 144 | return true; 145 | } catch (error) { 146 | if (error.status === 404) { 147 | return false; 148 | } 149 | throw error; 150 | } 151 | } 152 | 153 | async function isPullRequestExist(branch: string) { 154 | const pullRequest = await octokit.request( 155 | 'GET /repos/{owner}/{repo}/pulls', 156 | { 157 | owner, 158 | repo, 159 | head: branch, 160 | } 161 | ); 162 | 163 | return pullRequest.data.length > 0; 164 | } 165 | }; 166 | -------------------------------------------------------------------------------- /src/app/api/servers/pushToCustomURL.ts: -------------------------------------------------------------------------------- 1 | export const pushToCustomURL = async ( 2 | credentials: CustomURLCredentialsI, 3 | tokens: any 4 | ) => { 5 | // upload JSON to custom URL 6 | const url = credentials.url; 7 | const method = credentials.method; 8 | const headers = JSON.parse(credentials.headers); 9 | const body = JSON.stringify(tokens, null, 2); 10 | 11 | fetch(url, { 12 | method: method, 13 | headers: headers, 14 | body: body, 15 | }) 16 | .then((response) => response.json()) 17 | .then((data) => { 18 | console.log('Success:', data); 19 | }) 20 | .catch((error) => { 21 | console.error('Error:', error); 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /src/app/api/servers/pushToGithub.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'buffer'; 2 | import { Octokit } from '@octokit/core'; 3 | 4 | export const pushToGithub = async ( 5 | credentials: GithubCredentialsI, 6 | tokens: any, 7 | toastCallback: (props: ToastIPropsI) => void 8 | ) => { 9 | const ghToken = credentials.token; 10 | const ghUser = credentials.owner; 11 | const ghRepo = credentials.repo; 12 | const branch = credentials.branch; 13 | const fileName = credentials.fileName; 14 | const commitMessage = credentials.commitMessage || 'Update tokens'; 15 | const fileContent = Buffer.from(JSON.stringify(tokens, null, 2)).toString( 16 | 'base64' 17 | ); 18 | 19 | const octokit = new Octokit({ auth: ghToken }); 20 | 21 | const commonParams = { 22 | owner: ghUser, 23 | repo: ghRepo, 24 | path: fileName, 25 | }; 26 | 27 | const commonPushParams = { 28 | ...commonParams, 29 | message: commitMessage, 30 | content: fileContent, 31 | branch: branch, 32 | }; 33 | 34 | try { 35 | const { data: file } = await octokit.request( 36 | 'GET /repos/{owner}/{repo}/contents/{path}', 37 | { 38 | ...commonParams, 39 | ref: branch, 40 | } 41 | ); 42 | 43 | const response = await octokit.request( 44 | 'PUT /repos/{owner}/{repo}/contents/{path}', 45 | { 46 | ...commonPushParams, 47 | sha: file['sha'], // Use the existing sha when updating the file 48 | } 49 | ); 50 | 51 | // handle status response 52 | console.log('File updated successfully:', response); 53 | toastCallback({ 54 | title: 'Github: Updated successfully', 55 | message: 'Tokens on Github have been updated successfully', 56 | options: { 57 | type: 'success', 58 | }, 59 | }); 60 | } catch (error) { 61 | // handle status response 62 | console.error('Error upating file:', error); 63 | 64 | if (error.status === 404) { 65 | try { 66 | const response = await octokit.request( 67 | 'PUT /repos/{owner}/{repo}/contents/{path}', 68 | commonPushParams 69 | ); 70 | 71 | // handle status response 72 | console.log('File created successfully:', response); 73 | toastCallback({ 74 | title: 'Github: Created successfully', 75 | message: 'Tokens on Github have been created successfully', 76 | options: { 77 | type: 'success', 78 | }, 79 | }); 80 | } catch (error) { 81 | // handle status response 82 | console.error('Error creating file:', error); 83 | toastCallback({ 84 | title: 'Github: Error creating file', 85 | message: `Error creating file: ${error.message}.`, 86 | options: { 87 | type: 'error', 88 | }, 89 | }); 90 | } 91 | } else { 92 | toastCallback({ 93 | title: 'Github: An error occurred', 94 | message: error.message, 95 | options: { 96 | type: 'error', 97 | }, 98 | }); 99 | } 100 | } 101 | }; 102 | -------------------------------------------------------------------------------- /src/app/api/servers/pushToGitlab.ts: -------------------------------------------------------------------------------- 1 | export const pushToGitlab = async ( 2 | credentials: GitlabCredentialsI, 3 | tokens: any, 4 | toastCallback: (props: ToastIPropsI) => void 5 | ) => { 6 | const glToken = credentials.token; 7 | const glUser = credentials.owner; 8 | const glRepo = credentials.repo; 9 | const branch = credentials.branch; 10 | const glHost = credentials.host || 'gitlab.com'; 11 | const fileName = credentials.fileName; 12 | const commitMessage = credentials.commitMessage || 'Update tokens'; 13 | const fileContent = JSON.stringify(tokens, null, 2); 14 | 15 | const payload = { 16 | branch: branch, 17 | commit_message: commitMessage, 18 | content: fileContent, 19 | }; 20 | 21 | const fetchUrl = `https://${glHost}/api/v4/projects/${glUser}%2F${glRepo}/repository/files/${fileName}`; 22 | 23 | const gitlabRequest = async (method: 'POST' | 'PUT') => { 24 | try { 25 | const response = await fetch(fetchUrl, { 26 | method: method, 27 | headers: { 28 | 'Content-Type': 'application/json', 29 | 'PRIVATE-TOKEN': glToken, 30 | }, 31 | body: JSON.stringify(payload), 32 | }); 33 | 34 | return await response.json(); 35 | } catch (error) { 36 | console.error('Error:', error); 37 | toastCallback({ 38 | title: 'Gitlab: An error occured', 39 | message: `Error: ${error.message}`, 40 | options: { 41 | type: 'error', 42 | }, 43 | }); 44 | 45 | return null; 46 | } 47 | }; 48 | 49 | const data = await gitlabRequest('POST'); 50 | 51 | console.log('Gitlab response', data); 52 | 53 | if (data.message) { 54 | if (data.message === 'A file with this name already exists') { 55 | console.warn('File already exists, updating'); 56 | 57 | await gitlabRequest('PUT'); 58 | 59 | console.log('File updated successfully'); 60 | toastCallback({ 61 | title: 'Gitlab: Updated successfully', 62 | message: 'Tokens on Gitlab have been updated successfully', 63 | options: { 64 | type: 'success', 65 | }, 66 | }); 67 | 68 | return; 69 | } 70 | 71 | console.error('Error:', data.message); 72 | toastCallback({ 73 | title: 'Gitlab: An error occured', 74 | message: `Error: ${data.message}`, 75 | options: { 76 | type: 'error', 77 | }, 78 | }); 79 | } else { 80 | // handle status response 81 | // if file doesn't exist, create it 82 | console.log('File created successfully'); 83 | toastCallback({ 84 | title: 'Gitlab: Created successfully', 85 | message: 'Tokens on Gitlab have been created successfully', 86 | options: { 87 | type: 'success', 88 | }, 89 | }); 90 | } 91 | }; 92 | -------------------------------------------------------------------------------- /src/app/api/servers/pushToJSONBin.ts: -------------------------------------------------------------------------------- 1 | export const pushToJSONBin = async ( 2 | credentials: JsonbinCredentialsI, 3 | tokens: any, 4 | toastCallback: (props: ToastIPropsI) => void 5 | ) => { 6 | // console.log("JSONBin credentials", credentials); 7 | 8 | let response = null; 9 | 10 | // update existing bin 11 | if (credentials.id) { 12 | response = await fetch(`https://api.jsonbin.io/v3/b/${credentials.id}`, { 13 | method: 'PUT', 14 | headers: { 15 | 'Content-Type': 'application/json', 16 | 'X-Master-Key': credentials.secretKey, 17 | 'X-Bin-Name': credentials.name, 18 | }, 19 | body: JSON.stringify(tokens), 20 | }); 21 | } 22 | 23 | // create new bin 24 | if (!credentials.id) { 25 | response = await fetch(`https://api.jsonbin.io/v3/b`, { 26 | method: 'POST', 27 | headers: { 28 | 'Content-Type': 'application/json', 29 | 'X-Master-Key': credentials.secretKey, 30 | 'X-Bin-Name': credentials.name, 31 | }, 32 | body: JSON.stringify(tokens), 33 | }); 34 | } 35 | 36 | // handle response 37 | if (response.ok) { 38 | const json = await response.json(); 39 | 40 | console.log('JSONBin success', json); 41 | 42 | toastCallback({ 43 | title: 'JSONBin: Updated successfully', 44 | message: 'Tokens on JSONBin have been updated successfully', 45 | options: { 46 | type: 'success', 47 | }, 48 | }); 49 | 50 | return json; 51 | } 52 | 53 | // handle error 54 | console.log('JSONBin error', response); 55 | if (!response.ok) { 56 | toastCallback({ 57 | title: 'JSONBin: Error pushing tokens', 58 | message: `Error pushing tokens to JSONBin: ${response.statusText}`, 59 | options: { 60 | type: 'error', 61 | }, 62 | }); 63 | } 64 | }; 65 | -------------------------------------------------------------------------------- /src/app/components/StatusPicture/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import styles from './styles.module.scss'; 4 | 5 | interface StatusPictureProps { 6 | status: 'error' | 'tokens' | 'import'; 7 | } 8 | 9 | const errorSVG = ( 10 | 11 | 15 | 19 | 20 | ); 21 | 22 | const tokensSVG = ( 23 | 24 | 28 | 29 | 30 | ); 31 | 32 | const importSVG = ( 33 | 34 | 38 | 39 | 43 | 44 | ); 45 | 46 | export const StatusPicture = ({ status }: StatusPictureProps) => { 47 | return ( 48 |
49 | {status === 'error' && errorSVG} 50 | {status === 'tokens' && tokensSVG} 51 | {status === 'import' && importSVG} 52 |
53 | ); 54 | }; 55 | -------------------------------------------------------------------------------- /src/app/components/StatusPicture/styles.module.scss: -------------------------------------------------------------------------------- 1 | .statusPicture { 2 | width: 80px; 3 | height: 60px; 4 | 5 | svg { 6 | width: 100%; 7 | height: 100%; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/app/components/Toast/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef, useImperativeHandle, Ref, useEffect } from 'react'; 2 | 3 | import { Text } from 'pavelLaptev/react-figma-ui/ui'; 4 | import styles from './styles.module.scss'; 5 | 6 | interface ToastRefI { 7 | show: (params: ToastIPropsI) => void; 8 | } 9 | 10 | export const Toast = forwardRef((_, ref: Ref) => { 11 | const [toasts, setToasts] = React.useState([]); 12 | 13 | useImperativeHandle(ref as Ref, () => ({ 14 | show: (params) => { 15 | setToasts([ 16 | ...toasts, 17 | { 18 | title: params.title ?? 'Title', 19 | message: params.message, 20 | options: { 21 | type: params.options?.type ?? 'info', 22 | timeout: params.options?.timeout ?? 5000, 23 | onClose: params.options?.onClose, 24 | }, 25 | }, 26 | ]); 27 | }, 28 | })); 29 | 30 | const handleClose = (index: number) => { 31 | const toast = toasts[index]; 32 | toast.options?.onClose?.(); 33 | setToasts(toasts.filter((_, i) => i !== index)); 34 | }; 35 | 36 | useEffect(() => { 37 | if (toasts[0]?.options?.type === 'error') { 38 | return; 39 | } 40 | 41 | const timeout = setTimeout(() => { 42 | if (toasts.length) { 43 | handleClose(0); 44 | } 45 | }, toasts[0]?.options?.timeout || 3000); 46 | 47 | return () => clearTimeout(timeout); 48 | }, [toasts]); 49 | 50 | return ( 51 |
52 |
53 | {toasts.map((toast, index) => ( 54 |
handleClose(index)} 58 | > 59 | 60 | {toast.options.type === 'error' 61 | ? '⛔️ ' 62 | : toast.options.type === 'warn' 63 | ? '⚠️ ' 64 | : '🎉 '} 65 | {toast.title} 66 | 67 | 68 | {toast.message} 69 |
70 | ))} 71 |
72 |
73 | ); 74 | }); 75 | -------------------------------------------------------------------------------- /src/app/components/Toast/styles.module.scss: -------------------------------------------------------------------------------- 1 | .toastContainer { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | z-index: 9999; 6 | overflow: hidden; 7 | width: 300px; 8 | height: 100%; 9 | pointer-events: none; 10 | } 11 | 12 | .toastWrapper { 13 | position: absolute; 14 | bottom: 0; 15 | display: flex; 16 | flex-direction: column; 17 | padding: var(--space-extra-small); 18 | gap: var(--space-extra-small); 19 | pointer-events: all; 20 | overflow-y: auto; 21 | max-height: 100%; 22 | width: 100%; 23 | 24 | // hide the scrollbar 25 | &::-webkit-scrollbar { 26 | display: none; 27 | } 28 | } 29 | 30 | .toast { 31 | display: flex; 32 | flex-direction: column; 33 | gap: 8px; 34 | pointer-events: all; 35 | padding: 16px; 36 | user-select: text; 37 | border-radius: var(--border-radius-2); 38 | } 39 | 40 | .header { 41 | display: flex; 42 | justify-content: space-between; 43 | align-items: center; 44 | gap: 8px; 45 | } 46 | 47 | .close { 48 | display: flex; 49 | justify-content: center; 50 | align-items: center; 51 | width: 24px; 52 | height: 24px; 53 | } 54 | 55 | .message { 56 | word-break: break-word; 57 | hyphens: auto; 58 | user-select: text; 59 | pointer-events: all; 60 | } 61 | 62 | .success { 63 | background: var(--figma-color-bg-success-tertiary); 64 | } 65 | 66 | .error { 67 | background: var(--figma-color-bg-danger-tertiary); 68 | 69 | .message { 70 | color: var(--figma-color-text-danger-tertiary); 71 | } 72 | } 73 | 74 | .warn { 75 | background: var(--figma-color-bg-warning-tertiary); 76 | 77 | .message { 78 | color: var(--figma-color-text-warning-tertiary); 79 | } 80 | } 81 | 82 | .info { 83 | background: var(--figma-color-bg); 84 | } 85 | -------------------------------------------------------------------------------- /src/app/container.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | 3 | import { useDidUpdate } from './hooks/useDidUpdate'; 4 | 5 | import { LoadingView } from './views/LoadingView'; 6 | import { EmptyView } from './views/EmptyView'; 7 | import { SettingsView } from './views/SettingsView'; 8 | 9 | import { CodePreviewView } from './views/CodePreviewView'; 10 | 11 | import styles from './styles.module.scss'; 12 | 13 | const Container = () => { 14 | const wrapperRef = React.useRef(null); 15 | 16 | const [generatedTokens, setGeneratedTokens] = useState({}); 17 | 18 | const [isLoading, setIsLoading] = useState(true); 19 | 20 | const [frameHeight, setFrameHeight] = useState(0); 21 | const [isCodePreviewOpen, setIsCodePreviewOpen] = useState(false); 22 | 23 | const [currentView, setCurrentView] = useState('main'); 24 | const [fileHasVariables, setFileHasVariables] = useState(false); 25 | 26 | const [JSONsettingsConfig, setJSONsettingsConfig] = useState({ 27 | includedStyles: { 28 | text: { 29 | isIncluded: false, 30 | customName: 'Typography-styles', 31 | }, 32 | effects: { 33 | isIncluded: false, 34 | customName: 'Effect-styles', 35 | }, 36 | grids: { 37 | isIncluded: false, 38 | customName: 'Grid-styles', 39 | }, 40 | }, 41 | variableCollections: [], 42 | storeStyleInCollection: 'none', 43 | colorMode: 'hex', 44 | includeScopes: false, 45 | useDTCGKeys: false, 46 | includeValueStringKeyToAlias: false, 47 | includeFigmaMetaData: false, 48 | servers: { 49 | jsonbin: { 50 | isEnabled: false, 51 | id: '', 52 | name: '', 53 | secretKey: '', 54 | }, 55 | github: { 56 | isEnabled: false, 57 | token: '', 58 | repo: '', 59 | branch: '', 60 | fileName: '', 61 | owner: '', 62 | commitMessage: '', 63 | }, 64 | githubPullRequest: { 65 | isEnabled: false, 66 | token: '', 67 | repo: '', 68 | branch: '', 69 | baseBranch: '', 70 | fileName: '', 71 | owner: '', 72 | commitMessage: '', 73 | }, 74 | gitlab: { 75 | isEnabled: false, 76 | host: '', 77 | token: '', 78 | repo: '', 79 | branch: '', 80 | fileName: '', 81 | owner: '', 82 | commitMessage: '', 83 | }, 84 | customURL: { 85 | isEnabled: false, 86 | url: '', 87 | method: 'POST', 88 | headers: '', 89 | }, 90 | }, 91 | } as JSONSettingsConfigI); 92 | 93 | const commonProps = { 94 | JSONsettingsConfig, 95 | setJSONsettingsConfig, 96 | setCurrentView, 97 | }; 98 | 99 | ////////////////////// 100 | // HANDLE FUNCTIONS // 101 | ////////////////////// 102 | 103 | ///////////////// 104 | // USE EFFECTS // 105 | ///////////////// 106 | 107 | // Get all collections from Figma 108 | useEffect(() => { 109 | parent.postMessage({ pluginMessage: { type: 'checkForVariables' } }, '*'); 110 | 111 | window.onmessage = (event) => { 112 | const { type, hasVariables, variableCollections, storageConfig } = 113 | event.data.pluginMessage; 114 | 115 | // check if file has variables 116 | if (type === 'checkForVariables') { 117 | setFileHasVariables(hasVariables); 118 | setIsLoading(false); 119 | 120 | if (hasVariables) { 121 | setJSONsettingsConfig((prev) => ({ 122 | ...prev, 123 | variableCollections, 124 | })); 125 | } 126 | } 127 | 128 | // check storage on load 129 | if (type === 'storageConfig') { 130 | if (storageConfig) { 131 | setJSONsettingsConfig(storageConfig); 132 | } 133 | } 134 | }; 135 | }, []); 136 | 137 | // Check if the view was changed 138 | useEffect(() => { 139 | const resizeObserver = new ResizeObserver((entries) => { 140 | const { height } = entries[0].contentRect; 141 | setFrameHeight(height); 142 | 143 | if (isCodePreviewOpen) return; 144 | 145 | parent.postMessage( 146 | { 147 | pluginMessage: { 148 | type: 'resizeUIHeight', 149 | height, 150 | }, 151 | }, 152 | '*' 153 | ); 154 | }); 155 | 156 | if (wrapperRef.current) { 157 | resizeObserver.observe(wrapperRef.current); 158 | } 159 | 160 | return () => { 161 | resizeObserver.disconnect(); 162 | }; 163 | }, [isCodePreviewOpen]); 164 | 165 | // pass changed to figma controller 166 | useDidUpdate(() => { 167 | parent.postMessage( 168 | { 169 | pluginMessage: { 170 | type: 'JSONSettingsConfig', 171 | config: JSONsettingsConfig, 172 | }, 173 | }, 174 | '*' 175 | ); 176 | }, [JSONsettingsConfig]); 177 | 178 | // handle code preview 179 | useDidUpdate(() => { 180 | if (isCodePreviewOpen) { 181 | parent.postMessage( 182 | { 183 | pluginMessage: { 184 | type: 'openCodePreview', 185 | isCodePreviewOpen, 186 | height: frameHeight, 187 | }, 188 | }, 189 | '*' 190 | ); 191 | } 192 | }, [isCodePreviewOpen]); 193 | 194 | ///////////////////// 195 | // RENDER FUNCTION // 196 | ///////////////////// 197 | 198 | const renderView = () => { 199 | if (isLoading) { 200 | return ; 201 | } 202 | 203 | if (!fileHasVariables) { 204 | return ; 205 | } 206 | 207 | return ( 208 | 215 | ); 216 | }; 217 | 218 | return ( 219 |
220 | {renderView()} 221 | {isCodePreviewOpen && ( 222 | 223 | )} 224 |
225 | ); 226 | }; 227 | 228 | export default Container; 229 | -------------------------------------------------------------------------------- /src/app/controller/changeUIFrameSize.ts: -------------------------------------------------------------------------------- 1 | import { config } from './config'; 2 | 3 | export const changeUIFrameSize = (msg) => { 4 | if (msg.type === 'resizeUIHeight') { 5 | figma.ui.resize(config.frameWidth, msg.height); 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /src/app/controller/checkForVariables.ts: -------------------------------------------------------------------------------- 1 | export const checkForVariables = async (msgType) => { 2 | // get and set collections 3 | if (msgType === 'checkForVariables') { 4 | const variables = figma.variables.getLocalVariables() as Variable[]; 5 | const variableCollections = 6 | figma.variables.getLocalVariableCollections() as VariableCollection[]; 7 | const collectionNames = variableCollections.map((collection) => { 8 | return collection.name; 9 | }); 10 | 11 | console.log('available variables', variables.length); 12 | console.log('available variable collections', variableCollections.length); 13 | 14 | figma.ui.postMessage({ 15 | type: 'checkForVariables', 16 | hasVariables: variables.length > 0, 17 | variableCollections: collectionNames, 18 | }); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/app/controller/config.ts: -------------------------------------------------------------------------------- 1 | export const config = { 2 | frameWidth: 300, 3 | docsLink: 'https://github.com/PavelLaptev/figma-variables-export', 4 | changelogLink: 5 | 'https://github.com/PavelLaptev/tokens-bruecke/blob/main/CHANGELOG.md', 6 | }; 7 | -------------------------------------------------------------------------------- /src/app/controller/getStorageConfig.ts: -------------------------------------------------------------------------------- 1 | export const getStorageConfig = async (key) => { 2 | // clear storage for testing 3 | // figma.clientStorage.setAsync(key, null); 4 | 5 | const storageVersionKey = 'bruecke-storage'; 6 | const actualStorageVersion = 'v1'; 7 | 8 | const storageConfig = await figma.clientStorage.getAsync(storageVersionKey); 9 | 10 | // clear storage if storage version is different 11 | if (storageConfig && storageConfig !== actualStorageVersion) { 12 | figma.clientStorage.setAsync(key, null); 13 | await figma.clientStorage.setAsync(storageVersionKey, actualStorageVersion); 14 | } 15 | 16 | // get storage config 17 | figma.clientStorage.getAsync(key).then((storageConfig) => { 18 | try { 19 | console.log('storageConfig >>>>', JSON.parse(storageConfig)); 20 | 21 | figma.ui.postMessage({ 22 | type: 'storageConfig', 23 | storageConfig: JSON.parse(storageConfig), 24 | }); 25 | } catch (error) { 26 | figma.ui.postMessage({ 27 | type: 'storageConfig', 28 | storageConfig: null, 29 | }); 30 | } 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /src/app/controller/index.ts: -------------------------------------------------------------------------------- 1 | import { checkForVariables } from './checkForVariables'; 2 | import { getStorageConfig } from './getStorageConfig'; 3 | 4 | // import { removeDollarSign } from "../utils/removeDollarSign"; 5 | 6 | import { config } from './config'; 7 | import { getTokens } from '../../common/export'; 8 | import { PluginAPIResolver } from '../api/pluginApiResolver'; 9 | 10 | // clear console on reload 11 | console.clear(); 12 | 13 | //////////////////////// 14 | // EXPORT TOKENS /////// 15 | //////////////////////// 16 | 17 | const pluginConfigKey = 'tokenbrücke-config'; 18 | 19 | getStorageConfig(pluginConfigKey); 20 | 21 | // 22 | let isCodePreviewOpen = false; 23 | 24 | const frameWidthWithCodePreview = 800; 25 | const frameWidth = isCodePreviewOpen 26 | ? frameWidthWithCodePreview 27 | : config.frameWidth; 28 | 29 | figma.showUI(__html__, { 30 | width: frameWidth, 31 | height: 600, 32 | themeColors: true, 33 | }); 34 | 35 | let JSONSettingsConfig: JSONSettingsConfigI; 36 | 37 | // listen for messages from the UI 38 | figma.ui.onmessage = async (msg) => { 39 | await checkForVariables(msg.type); 40 | 41 | // get JSON settings config from UI and store it in a variable 42 | if (msg.type === 'JSONSettingsConfig') { 43 | // update JSONSettingsConfig 44 | JSONSettingsConfig = msg.config; 45 | 46 | // console.log("updated JSONSettingsConfig received", JSONSettingsConfig); 47 | 48 | // handle client storage 49 | await figma.clientStorage.setAsync( 50 | pluginConfigKey, 51 | JSON.stringify(JSONSettingsConfig) 52 | ); 53 | } 54 | 55 | // generate tokens and send them to the UI 56 | if (msg.type === 'getTokens') { 57 | await getTokens( 58 | new PluginAPIResolver(), 59 | JSONSettingsConfig, 60 | JSONSettingsConfig 61 | ).then((tokens) => { 62 | figma.ui.postMessage({ 63 | type: 'setTokens', 64 | tokens: tokens, 65 | role: msg.role, 66 | server: msg.server, 67 | } as TokensMessageI); 68 | }); 69 | } 70 | 71 | // change size of UI 72 | if (msg.type === 'resizeUIHeight') { 73 | figma.ui.resize(frameWidth, Math.round(msg.height)); 74 | } 75 | 76 | if (msg.type === 'openCodePreview') { 77 | console.log('openCodePreview', msg.isCodePreviewOpen); 78 | 79 | isCodePreviewOpen = msg.isCodePreviewOpen; 80 | figma.ui.resize(frameWidthWithCodePreview, Math.round(msg.height)); 81 | } 82 | }; 83 | -------------------------------------------------------------------------------- /src/app/hooks/useDidUpdate.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | export const useDidUpdate = (callback, deps) => { 4 | const hasMount = useRef(false); 5 | useEffect(() => { 6 | if (hasMount.current) { 7 | callback(); 8 | } else { 9 | hasMount.current = true; 10 | } 11 | }, deps); 12 | }; 13 | -------------------------------------------------------------------------------- /src/app/index.html: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /src/app/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | 4 | import Container from './container'; 5 | import 'pavelLaptev/react-figma-ui/ui/styles.css'; 6 | 7 | document.addEventListener('DOMContentLoaded', function () { 8 | const container = document.getElementById('react-page'); 9 | const root = createRoot(container); 10 | root.render(); 11 | }); 12 | -------------------------------------------------------------------------------- /src/app/styles.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | overflow: hidden; 4 | height: auto; 5 | 6 | // hide the scrollbar 7 | &::-webkit-scrollbar { 8 | display: none; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/app/views/CodePreviewView/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import styles from './styles.module.scss'; 4 | 5 | import { getTokensStat } from '../../../common/transform/getTokensStat'; 6 | 7 | import { Text, Icon } from 'pavelLaptev/react-figma-ui/ui'; 8 | 9 | interface CodePreviewViewProps { 10 | generatedTokens: any; 11 | } 12 | 13 | const copy = require('clipboard-copy'); 14 | 15 | export const CodePreviewView = ({ generatedTokens }: CodePreviewViewProps) => { 16 | const [isUpdateButtonAnimated, setIsUpdateButtonAnimated] = 17 | React.useState(false); 18 | const [tokensStat, setTokensStat] = React.useState(null); 19 | const [isCodeCopied, setIsCodeCopied] = React.useState(false); 20 | 21 | const getTokensPreview = () => { 22 | // send command to figma controller 23 | parent.postMessage( 24 | { 25 | pluginMessage: { 26 | type: 'getTokens', 27 | role: 'preview', 28 | } as TokensMessageI, 29 | }, 30 | '*' 31 | ); 32 | 33 | // start animation 34 | setIsUpdateButtonAnimated(true); 35 | 36 | // stop animation 37 | setTimeout(() => { 38 | setIsUpdateButtonAnimated(false); 39 | }, 500); 40 | }; 41 | 42 | const copyCode = () => { 43 | // copy code to clipboard 44 | copy(JSON.stringify(generatedTokens, null, 2)); 45 | setIsCodeCopied(true); 46 | 47 | // stop animation 48 | setTimeout(() => { 49 | setIsCodeCopied(false); 50 | }, 2000); 51 | }; 52 | 53 | React.useEffect(() => { 54 | if (generatedTokens) { 55 | setTokensStat(getTokensStat(generatedTokens)); 56 | } 57 | }, [generatedTokens]); 58 | 59 | if (!tokensStat || !generatedTokens) { 60 | return null; 61 | } 62 | 63 | return ( 64 |
65 |
66 | 75 | 76 | 82 | 83 |
84 | 85 | {tokensStat.tokensCount} tokens, {tokensStat.groupsCount} groups,{' '} 86 | {tokensStat.codeLines} lines 87 | 88 |
89 | 90 |
91 | {tokensStat.size / 1000} KB 92 |
93 |
94 | 95 |
 96 |         {JSON.stringify(generatedTokens, null, 2)}
 97 |       
98 |
99 | ); 100 | }; 101 | -------------------------------------------------------------------------------- /src/app/views/CodePreviewView/styles.module.scss: -------------------------------------------------------------------------------- 1 | .codePreview { 2 | overflow: hidden; 3 | position: relative; 4 | flex: 1; 5 | overflow: auto; 6 | height: 100vh; 7 | background-color: var(--figma-color-bg-secondary); 8 | border-left: 1px solid var(--figma-color-border); 9 | overflow-y: scroll; 10 | 11 | pre { 12 | overflow: auto; 13 | margin: 0; 14 | padding: 20px; 15 | margin-bottom: 30px; 16 | 17 | &::-webkit-scrollbar { 18 | width: 0; 19 | display: none; 20 | } 21 | } 22 | 23 | code { 24 | user-select: text; 25 | } 26 | 27 | // custom scrollbar 28 | &::-webkit-scrollbar { 29 | width: 13px; 30 | } 31 | 32 | &::-webkit-scrollbar-track { 33 | background: transparent; 34 | } 35 | 36 | &::-webkit-scrollbar-thumb { 37 | border: 4px solid rgba(0, 0, 0, 0); 38 | background-clip: padding-box; 39 | border-radius: 9999px; 40 | background-color: var(--figma-color-icon-secondary); 41 | } 42 | } 43 | 44 | .codePreviewOpen { 45 | height: 100vh; 46 | 47 | .settingView { 48 | overflow-y: auto; 49 | } 50 | } 51 | 52 | .previewToolbar { 53 | display: flex; 54 | align-items: center; 55 | justify-content: center; 56 | gap: 4px; 57 | position: fixed; 58 | width: 500px; 59 | bottom: 20px; 60 | right: 0; 61 | opacity: 0; 62 | transform: translateY(10px); 63 | 64 | animation: previewToolbarFadeIn 0.3s ease-in-out forwards; 65 | animation-delay: 0.1s; 66 | } 67 | 68 | @keyframes previewToolbarFadeIn { 69 | 0% { 70 | opacity: 0; 71 | transform: translateY(10px); 72 | } 73 | 100% { 74 | opacity: 1; 75 | transform: translateY(0); 76 | } 77 | } 78 | 79 | .toolbarItem { 80 | display: flex; 81 | align-items: center; 82 | justify-content: center; 83 | gap: 6px; 84 | height: 24px; 85 | padding: 4px 8px; 86 | border-radius: var(--border-radius-6); 87 | } 88 | 89 | .previewToolbarButton { 90 | background-color: var(--figma-color-bg-inverse); 91 | 92 | span { 93 | color: var(--figma-color-bg); 94 | } 95 | 96 | svg { 97 | fill: var(--figma-color-bg); 98 | } 99 | } 100 | 101 | .successUpdateAnimation { 102 | animation: successUpdateAnimation 0.5s 1; 103 | } 104 | 105 | @keyframes successUpdateAnimation { 106 | 0% { 107 | background-color: var(--figma-color-bg-success); 108 | transform: scale(0.96); 109 | } 110 | 100% { 111 | background-color: var(--figma-color-bg-inverse); 112 | transform: scale(1); 113 | } 114 | } 115 | 116 | .previewToolbarStat, 117 | .previewToolbarSecondButton { 118 | background-color: var(--figma-color-bg); 119 | } 120 | 121 | .previewToolbarSecondButton { 122 | margin-right: 10px; 123 | } 124 | -------------------------------------------------------------------------------- /src/app/views/EmptyView/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { config } from '../../controller/config'; 4 | 5 | import { Text, Button, Stack } from 'pavelLaptev/react-figma-ui/ui'; 6 | import { StatusPicture } from '../../components/StatusPicture'; 7 | 8 | import styles from './styles.module.scss'; 9 | 10 | interface EmptyViewProps { 11 | setFileHasVariables: (value: boolean) => void; 12 | } 13 | 14 | export const EmptyView = ({ setFileHasVariables }: EmptyViewProps) => { 15 | return ( 16 |
17 | 18 | 19 | No variables found in the file 20 | 21 | 22 | 23 |
42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /src/app/views/EmptyView/styles.module.scss: -------------------------------------------------------------------------------- 1 | .emptyView { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | justify-content: center; 6 | height: 240px; 7 | width: 100%; 8 | gap: 24px; 9 | padding: 20px 0 30px; 10 | 11 | transform: translateY(10px); 12 | 13 | animation: shaky 0.3s ease forwards; 14 | } 15 | 16 | .label { 17 | opacity: 0.6; 18 | } 19 | 20 | .group { 21 | align-items: center; 22 | width: 100%; 23 | max-width: 200px; 24 | } 25 | 26 | @keyframes shaky { 27 | 0% { 28 | transform: translateX(-6px); 29 | } 30 | 30% { 31 | transform: translateX(6px); 32 | } 33 | 60% { 34 | transform: translateX(-6px); 35 | } 36 | 100% { 37 | transform: translateX(0); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/app/views/LoadingView/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import styles from './styles.module.scss'; 4 | 5 | export const LoadingView = () => { 6 | return ( 7 |
8 | 16 | 21 | 22 |
23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/app/views/LoadingView/styles.module.scss: -------------------------------------------------------------------------------- 1 | .loadingView { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | justify-content: center; 6 | height: 240px; 7 | width: 100%; 8 | gap: 12px; 9 | padding: 20px 0 30px; 10 | 11 | opacity: 0; 12 | transform: translateY(10px); 13 | 14 | animation: appear 0.3s ease-in-out forwards; 15 | animation-delay: 0.1s; 16 | } 17 | 18 | .spinner { 19 | animation: spinnerRotate 1s linear infinite; 20 | } 21 | 22 | @keyframes spinnerRotate { 23 | 0% { 24 | transform: rotate(0deg); 25 | } 26 | 100% { 27 | transform: rotate(360deg); 28 | } 29 | } 30 | 31 | @keyframes appear { 32 | 0% { 33 | opacity: 0; 34 | transform: translateY(10px); 35 | } 36 | 100% { 37 | opacity: 1; 38 | transform: translateY(0); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/app/views/ServerSettingsView/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import styles from './styles.module.scss'; 3 | 4 | import { 5 | PanelHeader, 6 | Panel, 7 | Stack, 8 | Button, 9 | Input, 10 | Text, 11 | } from 'pavelLaptev/react-figma-ui/ui'; 12 | 13 | type ViewsConfigI = { 14 | [K in ServerType]: { 15 | title: string; 16 | description: React.ReactNode; 17 | isEnabled: boolean; 18 | fields: { 19 | readonly id: string; 20 | readonly type: 'input' | 'textarea' | 'select'; 21 | readonly required: boolean; 22 | readonly placeholder?: string; 23 | readonly options?: string[]; 24 | value: string; 25 | }[]; 26 | }; 27 | }; 28 | 29 | interface ViewProps { 30 | JSONsettingsConfig: JSONSettingsConfigI; 31 | setJSONsettingsConfig: React.Dispatch< 32 | React.SetStateAction 33 | >; 34 | setCurrentView: React.Dispatch>; 35 | server: ServerType; 36 | } 37 | 38 | const viewsConfig = { 39 | jsonbin: { 40 | title: 'JSONbin credentials', 41 | description: ( 42 | <> 43 | To use JSONbin you need to create{' '} 44 | 45 | an account 46 | {' '} 47 | and get your{' '} 48 | 53 | API key 54 | 55 | . 56 | 57 | ), 58 | isEnabled: false, 59 | fields: [ 60 | { 61 | id: 'name', 62 | placeholder: 'Bin name', 63 | type: 'input', 64 | value: 'design.tokens', 65 | required: true, 66 | }, 67 | { 68 | id: 'secretKey', 69 | placeholder: 'Access Key', 70 | type: 'input', 71 | value: '', 72 | required: true, 73 | }, 74 | { 75 | id: 'id', 76 | placeholder: 'Bin ID (for existing bin)', 77 | type: 'input', 78 | value: '', 79 | required: false, 80 | }, 81 | ], 82 | }, 83 | github: { 84 | title: 'Github credentials', 85 | description: ( 86 | <> 87 | In order to post on Github you need to have a{' '} 88 | 93 | personal access token 94 | 95 | . 96 | 97 | ), 98 | isEnabled: false, 99 | fields: [ 100 | { 101 | id: 'token', 102 | placeholder: 'Personal access token', 103 | type: 'input', 104 | value: '', 105 | required: true, 106 | }, 107 | { 108 | id: 'owner', 109 | placeholder: 'Owner', 110 | type: 'input', 111 | value: '', 112 | required: true, 113 | }, 114 | { 115 | id: 'repo', 116 | placeholder: 'Repo name', 117 | type: 'input', 118 | value: '', 119 | required: true, 120 | }, 121 | { 122 | id: 'branch', 123 | placeholder: 'Branch name', 124 | type: 'input', 125 | value: '', 126 | required: true, 127 | }, 128 | { 129 | id: 'fileName', 130 | placeholder: 'File name', 131 | type: 'input', 132 | value: 'design.tokens.json', 133 | required: true, 134 | }, 135 | { 136 | id: 'commitMessage', 137 | placeholder: 'Commit message (optional)', 138 | type: 'input', 139 | value: '', 140 | required: false, 141 | }, 142 | ], 143 | }, 144 | githubPullRequest: { 145 | title: 'Github credentials', 146 | description: ( 147 | <> 148 | In order to post on Github you need to have a{' '} 149 | 154 | personal access token 155 | 156 | . 157 | 158 | ), 159 | isEnabled: false, 160 | fields: [ 161 | { 162 | id: 'token', 163 | placeholder: 'Personal access token', 164 | type: 'input', 165 | value: '', 166 | required: true, 167 | }, 168 | { 169 | id: 'owner', 170 | placeholder: 'Owner', 171 | type: 'input', 172 | value: '', 173 | required: true, 174 | }, 175 | { 176 | id: 'repo', 177 | placeholder: 'Repo name', 178 | type: 'input', 179 | value: '', 180 | required: true, 181 | }, 182 | { 183 | id: 'baseBranch', 184 | placeholder: 'Base branch', 185 | type: 'input', 186 | value: '', 187 | required: true, 188 | }, 189 | { 190 | id: 'branch', 191 | placeholder: 'Branch name (optional)', 192 | type: 'input', 193 | value: '', 194 | required: false, 195 | }, 196 | { 197 | id: 'fileName', 198 | placeholder: 'File name', 199 | type: 'input', 200 | value: 'design.tokens.json', 201 | required: true, 202 | }, 203 | { 204 | id: 'commitMessage', 205 | placeholder: 'Commit message (optional)', 206 | type: 'input', 207 | value: '', 208 | required: false, 209 | }, 210 | { 211 | id: 'pullRequestTitle', 212 | placeholder: 'PR title (optional)', 213 | type: 'input', 214 | value: '', 215 | required: false, 216 | }, 217 | { 218 | id: 'pullRequestBody', 219 | placeholder: 'PR body (optional)', 220 | type: 'input', 221 | value: '', 222 | required: false, 223 | }, 224 | ], 225 | }, 226 | gitlab: { 227 | title: 'Gitlab credentials', 228 | description: ( 229 | <> 230 | In order to post on Gitlab you need to have a{' '} 231 | 236 | project access token 237 | 238 | . 239 | 240 | ), 241 | isEnabled: false, 242 | fields: [ 243 | { 244 | id: 'host', 245 | placeholder: 'Gitlab host for selfhosted (default: gitlab.com)', 246 | type: 'input', 247 | value: '', 248 | required: false, 249 | }, 250 | { 251 | id: 'token', 252 | placeholder: 'Project access token', 253 | type: 'input', 254 | value: '', 255 | required: true, 256 | }, 257 | { 258 | id: 'owner', 259 | placeholder: 'Owner', 260 | type: 'input', 261 | value: '', 262 | required: true, 263 | }, 264 | { 265 | id: 'repo', 266 | placeholder: 'Repo name', 267 | type: 'input', 268 | value: '', 269 | required: true, 270 | }, 271 | { 272 | id: 'branch', 273 | placeholder: 'Branch name', 274 | type: 'input', 275 | value: '', 276 | required: true, 277 | }, 278 | { 279 | id: 'fileName', 280 | placeholder: 'File name', 281 | type: 'input', 282 | value: 'design.tokens.json', 283 | required: true, 284 | }, 285 | { 286 | id: 'commitMessage', 287 | placeholder: 'Commit message (optional)', 288 | type: 'input', 289 | value: '', 290 | required: false, 291 | }, 292 | ], 293 | }, 294 | customURL: { 295 | title: 'Custom URL', 296 | description: ( 297 | <> 298 | To use custom URL you need to create a server that will accept POST or 299 | PUT requests with JSON body. 300 | 301 | ), 302 | isEnabled: false, 303 | fields: [ 304 | { 305 | id: 'url', 306 | placeholder: 'URL', 307 | type: 'input', 308 | value: '', 309 | required: true, 310 | }, 311 | { 312 | id: 'method', 313 | placeholder: 'Method (POST or PUT)', 314 | type: 'input', 315 | required: true, 316 | }, 317 | { 318 | id: 'headers', 319 | placeholder: 'Headers (optional)', 320 | type: 'input', 321 | value: '', 322 | required: false, 323 | }, 324 | ], 325 | }, 326 | } as ViewsConfigI; 327 | 328 | interface LocalConfigI { 329 | isEnabled: boolean; 330 | [key: string]: string | boolean; 331 | } 332 | 333 | export const ServerSettingsView = (props: ViewProps) => { 334 | const { JSONsettingsConfig, setJSONsettingsConfig, setCurrentView } = props; 335 | const [errorFields, setErrorFields] = useState([] as string[]); 336 | 337 | const [config, setConfig] = useState( 338 | viewsConfig[props.server].fields.reduce((acc, field) => { 339 | const serverSettings = JSONsettingsConfig.servers[props.server] || {}; // Ensure server settings exist 340 | 341 | return { 342 | ...acc, 343 | isEnabled: serverSettings.isEnabled || false, // Provide a default value (false in this case) 344 | [field.id]: serverSettings[field.id], // This may be undefined if the field does not exist in server settings 345 | }; 346 | }, {} as LocalConfigI) 347 | ); 348 | 349 | // console.log("config state", config); 350 | 351 | const isFormValid = viewsConfig[props.server].fields.every((field) => { 352 | return config[field.id] !== '' || !field.required; 353 | }); 354 | 355 | ///////////////// 356 | // MAIN RENDER // 357 | ///////////////// 358 | 359 | // console.log("props.server", props.server); 360 | // console.log("viewsConfig[props.server]", viewsConfig[props.server]); 361 | 362 | return ( 363 | 364 | 365 | { 370 | setCurrentView('main'); 371 | }} 372 | /> 373 | 374 | 375 | 376 | 377 | {viewsConfig[props.server].description} 378 | 379 | 380 | 381 | 382 | {viewsConfig[props.server].fields.map((field) => { 383 | // console.log("field", field); 384 | 385 | const handleErrorsOnBlur = (value: string) => { 386 | if (value === '' && field.required) { 387 | setErrorFields((prevState) => { 388 | return [...prevState, field.id]; 389 | }); 390 | } 391 | }; 392 | 393 | const clearErrorOnFocus = () => { 394 | setErrorFields((prevState) => { 395 | return prevState.filter((item) => item !== field.id); 396 | }); 397 | }; 398 | 399 | const handleChange = (value: string) => { 400 | setConfig((prevState) => { 401 | return { 402 | ...prevState, 403 | [field.id]: value, 404 | }; 405 | }); 406 | }; 407 | 408 | return ( 409 | 421 | ); 422 | })} 423 | 424 | 425 | 426 |