├── .github ├── FUNDING.yml └── workflows │ └── test.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── LICENSE ├── README.md ├── _resources ├── DS-Typography.jpg ├── DS-loves-motion.jpg ├── Design Tokens Plugin Cover.png ├── Design Tokens Plugin Icon.png ├── Design Tokens.svg ├── Dev-Pipeline-to-github.jpg ├── File-Export-Settings.png ├── Plugin Description.md ├── Plugin Icon.png ├── Typography-v6.1.0.jpg ├── Url-Export-Settings.png ├── Variants.jpg ├── _archive │ ├── Design Tokens Plugin Cover + Github.png │ ├── Design Tokens Plugin Cover.png │ ├── __old_Design Tokens Plugin Cover + Github.png │ └── settings.png ├── design-tokens-v5.jpg ├── example-breakpoints-tokens.png ├── example-sizes-tokens.png ├── example-spacing-tokens.png ├── promo-v5.jpg ├── promo-v6.jpg └── settings.png ├── dist ├── plugin.js ├── ui.html ├── ui.js └── ui.js.LICENSE.txt ├── examples ├── Readme.md ├── build.js ├── build │ ├── android │ │ ├── font │ │ │ └── font_family.xml │ │ └── values │ │ │ ├── dimens.xml │ │ │ └── font_styles.xml │ ├── css │ │ └── variables.css │ ├── ios │ │ ├── DesignToken.xcassets │ │ │ ├── Background.colorset │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── MultipleFills_0.colorset │ │ │ │ └── Contents.json │ │ │ ├── MultipleFills_1.colorset │ │ │ │ └── Contents.json │ │ │ ├── SingleBlue.colorset │ │ │ │ └── Contents.json │ │ │ ├── SpecialCharacters.colorset │ │ │ │ └── Contents.json │ │ │ └── SpecialCharactersNderung.colorset │ │ │ │ └── Contents.json │ │ ├── Size.swift │ │ └── StyleDictionary+Generated.swift │ └── scss │ │ └── variables.scss ├── filesToCopy │ └── font_family.xml ├── input │ └── standard-tokens.json └── libs │ ├── android │ ├── colorName.js │ ├── fontSizeToSp.js │ ├── formatFontStyle.js │ ├── formatResourcesSorted.js │ ├── index.js │ └── pxToDp.js │ ├── common │ ├── camelCaseHelper.js │ ├── colorToHex8.js │ ├── colorToRgbaString.js │ └── copyFileOrFolder.js │ ├── ios │ ├── colorsets.js │ ├── fontStyleTemplate.js │ ├── fontStyles.js │ └── index.js │ └── web │ ├── createPropertyFormatter.js │ ├── fileHeader.js │ ├── filterWeb.js │ ├── formatCss.js │ ├── formattedVariables.js │ ├── index.js │ ├── sizePx.js │ ├── sortByReference.js │ ├── webFont.js │ ├── webGradient.js │ ├── webPadding.js │ ├── webRadius.js │ └── webShadows.js ├── faq.md ├── manifest.json ├── package-lock.json ├── package.json ├── src ├── config │ ├── commands.ts │ ├── config.ts │ ├── defaultSettings.ts │ └── tokenTypes.ts ├── extractor │ ├── extractBorders.ts │ ├── extractBreakpoints.ts │ ├── extractColors.ts │ ├── extractEffects.ts │ ├── extractFonts.ts │ ├── extractGrids.ts │ ├── extractMotion.ts │ ├── extractOpacities.ts │ ├── extractRadii.ts │ ├── extractSizes.ts │ ├── extractSpacing.ts │ └── extractUtilities.ts ├── index.ts ├── transformer │ ├── originalFormatTransformer.ts │ ├── standardTransformer.ts │ └── tokenExtensions.ts ├── ui │ ├── components │ │ ├── Button.tsx │ │ ├── CancelButton.tsx │ │ ├── Checkbox.tsx │ │ ├── FileExportSettings.tsx │ │ ├── Footer.tsx │ │ ├── GeneralSettings.tsx │ │ ├── Info.tsx │ │ ├── Input.tsx │ │ ├── Label.tsx │ │ ├── Row.tsx │ │ ├── Select.tsx │ │ ├── Separator.tsx │ │ ├── Text.tsx │ │ ├── Title.tsx │ │ ├── UrlExportSettings.tsx │ │ ├── VersionNotice.tsx │ │ └── WebLink.tsx │ ├── context.tsx │ ├── css │ │ ├── ui.css │ │ └── variables.css │ ├── modules │ │ ├── downloadJson.ts │ │ ├── gitlabRepository.ts │ │ ├── handleKeyboardInput.ts │ │ └── urlExport.ts │ ├── ui.html │ └── ui.tsx └── utilities │ ├── accessToken.ts │ ├── base64.ts │ ├── buildFigmaData.ts │ ├── changeNotation.ts │ ├── convertColor.ts │ ├── deepMerge.ts │ ├── extractTokenNodeValues.ts │ ├── filterByNameProperty.ts │ ├── getEffectStyles.ts │ ├── getFileId.ts │ ├── getGridStyles.ts │ ├── getPaintStyles.ts │ ├── getTextStyles.ts │ ├── getTokenJson.ts │ ├── getTokenNodes.ts │ ├── getVariableTypeByValue.ts │ ├── getVariables.ts │ ├── getVersionDifference.ts │ ├── groupByName.ts │ ├── handleVariableAlias.ts │ ├── isTokenNode.ts │ ├── prefixTokenName.ts │ ├── prepareExport.ts │ ├── processAliasModes.ts │ ├── roundWithDecimals.ts │ ├── semVerDifference.ts │ ├── settings.ts │ ├── stringifyJson.ts │ ├── transformName.ts │ └── version.ts ├── tests ├── data │ └── variables.css ├── files │ ├── original-tokens.json │ └── standard-tokens.json ├── integration │ ├── cssOutput.test.ts │ ├── data │ │ ├── cssOutput.data.ts │ │ ├── cssStandardOutput.data.ts │ │ ├── jsonOriginalFormat.data.ts │ │ ├── original.variables.css │ │ └── standard.variables.css │ ├── jsonOutput.test.ts │ ├── libs │ │ └── standard │ │ │ └── web │ │ │ ├── colorToRgbaString.js │ │ │ ├── formatWeb.js │ │ │ ├── sizePx.js │ │ │ ├── webFont.js │ │ │ ├── webGradient.js │ │ │ ├── webPadding.js │ │ │ ├── webRadius.js │ │ │ └── webShadows.js │ ├── original.config.json │ └── standard.build.js └── unit │ ├── accessToken.test.ts │ ├── base64.test.ts │ ├── buildFigmaData.test.ts │ ├── convertColor.test.ts │ ├── data │ ├── color.data.ts │ ├── customTokenNode.data.ts │ ├── effectStyleObjects.data.ts │ ├── extractedFigmaTokens.data.ts │ ├── figmaData.data.ts │ ├── gridStyleObjects.data.ts │ ├── paintStyleObjects.data.ts │ ├── textStyleObjects.data.ts │ ├── transformedOriginalFormatTokens.data.ts │ └── transformedStandardTokens.data.ts │ ├── deepMerge.test.ts │ ├── exportRawTokenArray.test.ts │ ├── extractBorders.test.ts │ ├── extractBreakpoints.test.ts │ ├── extractColors.test.ts │ ├── extractEffects.test.ts │ ├── extractFonts.test.ts │ ├── extractGrids.test.ts │ ├── extractMotion.test.ts │ ├── extractOpacitites.test.ts │ ├── extractRadii.test.ts │ ├── extractSizes.test.ts │ ├── extractSpacing.test.ts │ ├── extractUtilities.test.ts │ ├── filterByNameProperty.test.ts │ ├── getEffectStyles.test.ts │ ├── getFileId.test.ts │ ├── getGridStyles.test.ts │ ├── getPaintStyles.test.ts │ ├── getTextStyles.test.ts │ ├── getTokenNames.test.ts │ ├── getVariableTypeByValue.test.ts │ ├── getVariables.test.ts │ ├── getVersionDifference.test.ts │ ├── groupByName.test.ts │ ├── handleVariableAlias.test.ts │ ├── prefixTokenName.test.ts │ ├── prepareExport.test.ts │ ├── processAliasModes.test.ts │ ├── roundRgba.test.ts │ ├── roundWithDecimals.test.ts │ ├── semVerDifference.test.ts │ ├── settings.test.ts │ ├── transformName.test.ts │ ├── transformer.originalFormatTransformer.test.ts │ ├── transformer.standardTransformer.test.ts │ └── urlExport.test.ts ├── tsconfig.json ├── types ├── extractedData.d.ts ├── extractorInterface.d.ts ├── figmaDataType.d.ts ├── originalFormatProperties.d.ts ├── pluginEvent.ts ├── propertyCategory.d.ts ├── propertyObject.d.ts ├── settings.ts ├── standardToken.d.ts ├── styles.d.ts ├── tokenCategory.d.ts ├── tokenExportKey.d.ts ├── tokenNodeTypes.d.ts ├── urlExportData.d.ts └── valueTypes.d.ts └── webpack.config.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [lukasoppermann] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Testing and linting 2 | 3 | on: 4 | pull_request: 5 | paths-ignore: 6 | - '**.md' 7 | push: 8 | branches: 9 | - main 10 | paths-ignore: 11 | - '**.md' 12 | 13 | jobs: 14 | test: 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [14.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - run: npm install 28 | - run: npm test 29 | - name: Coveralls 30 | uses: coverallsapp/github-action@v1.1.2 31 | with: 32 | path-to-lcov: "./tests/unit/coverage/lcov.info" 33 | github-token: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | tests/amazon-style-dictionary/build 4 | tests/unit/coverage 5 | _resources/_notSynced.md 6 | .idea -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | legacy-peer-deps=true -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v18.20.4 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Lukas Oppermann 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 | -------------------------------------------------------------------------------- /_resources/DS-Typography.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukasoppermann/design-tokens/c8c1337e4cd0b3a3a7097e7aa2579c88514d4250/_resources/DS-Typography.jpg -------------------------------------------------------------------------------- /_resources/DS-loves-motion.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukasoppermann/design-tokens/c8c1337e4cd0b3a3a7097e7aa2579c88514d4250/_resources/DS-loves-motion.jpg -------------------------------------------------------------------------------- /_resources/Design Tokens Plugin Cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukasoppermann/design-tokens/c8c1337e4cd0b3a3a7097e7aa2579c88514d4250/_resources/Design Tokens Plugin Cover.png -------------------------------------------------------------------------------- /_resources/Design Tokens Plugin Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukasoppermann/design-tokens/c8c1337e4cd0b3a3a7097e7aa2579c88514d4250/_resources/Design Tokens Plugin Icon.png -------------------------------------------------------------------------------- /_resources/Dev-Pipeline-to-github.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukasoppermann/design-tokens/c8c1337e4cd0b3a3a7097e7aa2579c88514d4250/_resources/Dev-Pipeline-to-github.jpg -------------------------------------------------------------------------------- /_resources/File-Export-Settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukasoppermann/design-tokens/c8c1337e4cd0b3a3a7097e7aa2579c88514d4250/_resources/File-Export-Settings.png -------------------------------------------------------------------------------- /_resources/Plugin Description.md: -------------------------------------------------------------------------------- 1 | Allows you to export fimga styles and elements to a json file, that is Amazon Style Dictionary compatible. 2 | 3 | Design Tokens 4 | You can create design tokens for the following properties: 5 | - colors 6 | - typograhy 7 | - grids 8 | - effects 9 | - sizes / spaces 10 | - radii 11 | - borders 12 | 13 | Benefits 14 | ✅ Actively developed 15 | ✅ Compatible with Amazon Style Dictionary 16 | ✅ Sync tokens directly with github repositories 17 | ✅ Support for Figma Styles 18 | ✅ Support for custom tokens 19 | 20 | Usage & transformation 21 | Find out how to use the plugin to create custom tokens from the documentation. https://github.com/lukasoppermann/design-tokens 22 | 23 | You can also view the example Figma file https://www.figma.com/file/2MQ759R5kJtzQn4qSHuqR7/Design-Tokens-for-Figma?node-id=231%3A2 24 | 25 | Get up to speed transforming your tokens using style directory with the transformer package. https://github.com/lukasoppermann/design-token-transformer 26 | 27 | 28 | Feature request & bugs 29 | Please create an issues in the repository. https://github.com/lukasoppermann/design-tokens/issues/new 30 | 31 | ------- 32 | 33 | Tags: 34 | amazon style dictionary, designsystems, design systems, design tokens, design-tokens, tokens, design properties, export, json, styleguides, styles, handoff -------------------------------------------------------------------------------- /_resources/Plugin Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukasoppermann/design-tokens/c8c1337e4cd0b3a3a7097e7aa2579c88514d4250/_resources/Plugin Icon.png -------------------------------------------------------------------------------- /_resources/Typography-v6.1.0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukasoppermann/design-tokens/c8c1337e4cd0b3a3a7097e7aa2579c88514d4250/_resources/Typography-v6.1.0.jpg -------------------------------------------------------------------------------- /_resources/Url-Export-Settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukasoppermann/design-tokens/c8c1337e4cd0b3a3a7097e7aa2579c88514d4250/_resources/Url-Export-Settings.png -------------------------------------------------------------------------------- /_resources/Variants.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukasoppermann/design-tokens/c8c1337e4cd0b3a3a7097e7aa2579c88514d4250/_resources/Variants.jpg -------------------------------------------------------------------------------- /_resources/_archive/Design Tokens Plugin Cover + Github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukasoppermann/design-tokens/c8c1337e4cd0b3a3a7097e7aa2579c88514d4250/_resources/_archive/Design Tokens Plugin Cover + Github.png -------------------------------------------------------------------------------- /_resources/_archive/Design Tokens Plugin Cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukasoppermann/design-tokens/c8c1337e4cd0b3a3a7097e7aa2579c88514d4250/_resources/_archive/Design Tokens Plugin Cover.png -------------------------------------------------------------------------------- /_resources/_archive/__old_Design Tokens Plugin Cover + Github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukasoppermann/design-tokens/c8c1337e4cd0b3a3a7097e7aa2579c88514d4250/_resources/_archive/__old_Design Tokens Plugin Cover + Github.png -------------------------------------------------------------------------------- /_resources/_archive/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukasoppermann/design-tokens/c8c1337e4cd0b3a3a7097e7aa2579c88514d4250/_resources/_archive/settings.png -------------------------------------------------------------------------------- /_resources/design-tokens-v5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukasoppermann/design-tokens/c8c1337e4cd0b3a3a7097e7aa2579c88514d4250/_resources/design-tokens-v5.jpg -------------------------------------------------------------------------------- /_resources/example-breakpoints-tokens.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukasoppermann/design-tokens/c8c1337e4cd0b3a3a7097e7aa2579c88514d4250/_resources/example-breakpoints-tokens.png -------------------------------------------------------------------------------- /_resources/example-sizes-tokens.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukasoppermann/design-tokens/c8c1337e4cd0b3a3a7097e7aa2579c88514d4250/_resources/example-sizes-tokens.png -------------------------------------------------------------------------------- /_resources/example-spacing-tokens.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukasoppermann/design-tokens/c8c1337e4cd0b3a3a7097e7aa2579c88514d4250/_resources/example-spacing-tokens.png -------------------------------------------------------------------------------- /_resources/promo-v5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukasoppermann/design-tokens/c8c1337e4cd0b3a3a7097e7aa2579c88514d4250/_resources/promo-v5.jpg -------------------------------------------------------------------------------- /_resources/promo-v6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukasoppermann/design-tokens/c8c1337e4cd0b3a3a7097e7aa2579c88514d4250/_resources/promo-v6.jpg -------------------------------------------------------------------------------- /_resources/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukasoppermann/design-tokens/c8c1337e4cd0b3a3a7097e7aa2579c88514d4250/_resources/settings.png -------------------------------------------------------------------------------- /dist/ui.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /** 2 | * @license React 3 | * react-dom.production.min.js 4 | * 5 | * Copyright (c) Facebook, Inc. and its affiliates. 6 | * 7 | * This source code is licensed under the MIT license found in the 8 | * LICENSE file in the root directory of this source tree. 9 | */ 10 | 11 | /** 12 | * @license React 13 | * react.production.min.js 14 | * 15 | * Copyright (c) Facebook, Inc. and its affiliates. 16 | * 17 | * This source code is licensed under the MIT license found in the 18 | * LICENSE file in the root directory of this source tree. 19 | */ 20 | 21 | /** 22 | * @license React 23 | * scheduler.production.min.js 24 | * 25 | * Copyright (c) Facebook, Inc. and its affiliates. 26 | * 27 | * This source code is licensed under the MIT license found in the 28 | * LICENSE file in the root directory of this source tree. 29 | */ 30 | -------------------------------------------------------------------------------- /examples/Readme.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | Some example custom amazon style dictionary transfomers to use together with the standard token format. 4 | 5 | - `input/*.json` is the token file exported from figma 6 | - `build` is the directory with all the output files for iOS, Android and web 7 | - `build.js` is running amazon style dictionary and has all the configuration. You can run it with the command `node ./examples/build.js`. 8 | - `libs` has all modules that transform the json for the specific platforms 9 | - `filesToCopy` is holding all files that are not generated but should just be copied to the build directory -------------------------------------------------------------------------------- /examples/build/android/font/font_family.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 11 | 15 | 19 | 23 | 27 | -------------------------------------------------------------------------------- /examples/build/android/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 32.72dp 9 | 40dp 10 | 60dp 11 | 80dp 12 | 200dp 13 | 200dp 14 | 200dp 15 | 60dp 16 | 90dp 17 | 120dp 18 | 32dp 19 | 32dp 20 | 32dp 21 | 1280dp 22 | 768dp 23 | 1024dp 24 | 20sp 25 | 16sp 26 | 12sp 27 | 12sp 28 | 20sp 29 | 22sp 30 | 31 | 32 | -------------------------------------------------------------------------------- /examples/build/android/values/font_styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 16 | 17 | 26 | 27 | 35 | 36 | 44 | 45 | 53 | 54 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /examples/build/css/variables.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Do not edit directly 3 | * Generated on Mon Sep 20 2021 16:18:44 GMT+0200 (Central European Summer Time) 4 | */ 5 | 6 | :root { 7 | --sizes-32: 32.72px; 8 | --sizes-40: 40px; 9 | --sizes-60: 60px; 10 | --sizes-80: 80px; 11 | --sizes-plain-token: 200px; 12 | --sizes-token-in-frame: 200px; 13 | --sizes-token-in-group: 200px; 14 | --sizes-in-variant-60: 60px; 15 | --sizes-in-variant-90: 90px; 16 | --sizes-in-variant-120: 120px; 17 | --sizes-frame: 32px; 18 | --sizes-rect: 32px; 19 | --sizes-shape-in-component: 32px; 20 | --breakpoints-lg: 1280px; 21 | --breakpoints-sm: 768px; 22 | --breakpoints-md: 1024px; 23 | --radius-5: 5px; 24 | --radii-smoothing: 10px; 25 | --radii-mixed: 5.5px 10px 20px 15px; 26 | --gradient-gradient-single-with-multiple-color-stops: radial-gradient(rgb(255, 184, 0) 0%, rgb(255, 138, 0) 34%, rgb(255, 46, 0) 65%, rgb(255, 0, 0) 100%); 27 | --gradient-gradient-multiple-0: linear-gradient(180deg, rgb(255, 184, 0) 0%, rgb(255, 184, 0) 100%); 28 | --gradient-gradient-multiple-1: radial-gradient(rgb(255, 255, 255) 0%, rgb(255, 255, 255) 100%); 29 | --gradient-gradient-multiple-2: undefined; 30 | --gradient-gradient-multiple-3: undefined; 31 | --color-colors-multiple-fills-0: rgb(64, 255, 186); 32 | --color-colors-multiple-fills-1: rgba(0, 0, 0, 0.1); 33 | --color-colors-single-blue: rgb(4, 74, 255); 34 | --color-colors-special-characters: rgb(64, 223, 80); 35 | --color-colors-special-characters-nderung: rgb(52, 86, 175); 36 | --font-body-h3: condensed 700 20/32 "Akzidenz-Grotesk Pro", sans-serif; 37 | --font-body-h4-strike-through: italic 500 16/19.2 Roboto; 38 | --font-body-italic: italic 400 12/14 Roboto; 39 | --font-body-extra-bold-condensed-italic: condensed italic 800 12/14.4 "Akzidenz-Grotesk Pro", sans-serif; 40 | --font-body-medium-extended-italic: expanded italic 500 20/24 "Akzidenz-Grotesk Pro", sans-serif; 41 | --font-body-super: 900 22/26.4 "Akzidenz-Grotesk Pro", sans-serif; 42 | --effect-drop-shadow-single: 0px 4px 4px 0px rgba(0, 0, 0, 0.25); 43 | --effect-inner-shadow-multiple-0: inset 0px 4px 4px 0px rgba(0, 0, 0, 0.25); 44 | --effect-inner-shadow-multiple-1: inset 10px 100px 1px 0.5px rgb(0, 0, 0); 45 | --effect-inner-shadow-multiple-2: inset -4px 2px 3px 11px rgba(0, 0, 0, 0.25); 46 | } 47 | 48 | @media (prefers-color-scheme: light) { 49 | :root { 50 | --color-light-background: rgb(255, 255, 255); 51 | } 52 | } 53 | 54 | @media (prefers-color-scheme: dark) { 55 | :root { 56 | --color-dark-background: rgb(0, 0, 0); 57 | } 58 | } 59 | 60 | html[data-theme="light"] { 61 | --color-light-background: rgb(255, 255, 255); 62 | } 63 | 64 | html[data-theme="dark"] { 65 | --color-dark-background: rgb(0, 0, 0); 66 | } 67 | -------------------------------------------------------------------------------- /examples/build/ios/DesignToken.xcassets/Background.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "author": "xcode", 4 | "version": 1 5 | }, 6 | "colors": [ 7 | { 8 | "idiom": "universal", 9 | "color": { 10 | "color-space": "srgb", 11 | "components": { 12 | "red": "1.000", 13 | "green": "1.000", 14 | "blue": "1.000", 15 | "alpha": "1.000" 16 | } 17 | } 18 | }, 19 | { 20 | "idiom": "universal", 21 | "color": { 22 | "color-space": "srgb", 23 | "components": { 24 | "red": "0.000", 25 | "green": "0.000", 26 | "blue": "0.000", 27 | "alpha": "1.000" 28 | } 29 | } 30 | }, 31 | { 32 | "idiom": "universal", 33 | "color": { 34 | "color-space": "srgb", 35 | "components": { 36 | "red": "1.000", 37 | "green": "1.000", 38 | "blue": "1.000", 39 | "alpha": "1.000" 40 | } 41 | } 42 | }, 43 | { 44 | "idiom": "universal", 45 | "color": { 46 | "color-space": "srgb", 47 | "components": { 48 | "red": "0.000", 49 | "green": "0.000", 50 | "blue": "0.000", 51 | "alpha": "1.000" 52 | } 53 | } 54 | } 55 | ] 56 | } -------------------------------------------------------------------------------- /examples/build/ios/DesignToken.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "author": "xcode", 4 | "version": 1 5 | } 6 | } -------------------------------------------------------------------------------- /examples/build/ios/DesignToken.xcassets/MultipleFills_0.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "author": "xcode", 4 | "version": 1 5 | }, 6 | "colors": [ 7 | { 8 | "idiom": "universal", 9 | "color": { 10 | "color-space": "srgb", 11 | "components": { 12 | "red": "0.250", 13 | "green": "1.000", 14 | "blue": "0.730", 15 | "alpha": "1.000" 16 | } 17 | } 18 | }, 19 | { 20 | "idiom": "universal", 21 | "color": { 22 | "color-space": "srgb", 23 | "components": { 24 | "red": "0.250", 25 | "green": "1.000", 26 | "blue": "0.730", 27 | "alpha": "1.000" 28 | } 29 | } 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /examples/build/ios/DesignToken.xcassets/MultipleFills_1.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "author": "xcode", 4 | "version": 1 5 | }, 6 | "colors": [ 7 | { 8 | "idiom": "universal", 9 | "color": { 10 | "color-space": "srgb", 11 | "components": { 12 | "red": "0.000", 13 | "green": "0.000", 14 | "blue": "0.000", 15 | "alpha": "0.102" 16 | } 17 | } 18 | }, 19 | { 20 | "idiom": "universal", 21 | "color": { 22 | "color-space": "srgb", 23 | "components": { 24 | "red": "0.000", 25 | "green": "0.000", 26 | "blue": "0.000", 27 | "alpha": "0.102" 28 | } 29 | } 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /examples/build/ios/DesignToken.xcassets/SingleBlue.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "author": "xcode", 4 | "version": 1 5 | }, 6 | "colors": [ 7 | { 8 | "idiom": "universal", 9 | "color": { 10 | "color-space": "srgb", 11 | "components": { 12 | "red": "0.020", 13 | "green": "0.290", 14 | "blue": "1.000", 15 | "alpha": "1.000" 16 | } 17 | } 18 | }, 19 | { 20 | "idiom": "universal", 21 | "color": { 22 | "color-space": "srgb", 23 | "components": { 24 | "red": "0.020", 25 | "green": "0.290", 26 | "blue": "1.000", 27 | "alpha": "1.000" 28 | } 29 | } 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /examples/build/ios/DesignToken.xcassets/SpecialCharacters.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "author": "xcode", 4 | "version": 1 5 | }, 6 | "colors": [ 7 | { 8 | "idiom": "universal", 9 | "color": { 10 | "color-space": "srgb", 11 | "components": { 12 | "red": "0.250", 13 | "green": "0.870", 14 | "blue": "0.310", 15 | "alpha": "1.000" 16 | } 17 | } 18 | }, 19 | { 20 | "idiom": "universal", 21 | "color": { 22 | "color-space": "srgb", 23 | "components": { 24 | "red": "0.250", 25 | "green": "0.870", 26 | "blue": "0.310", 27 | "alpha": "1.000" 28 | } 29 | } 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /examples/build/ios/DesignToken.xcassets/SpecialCharactersNderung.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "author": "xcode", 4 | "version": 1 5 | }, 6 | "colors": [ 7 | { 8 | "idiom": "universal", 9 | "color": { 10 | "color-space": "srgb", 11 | "components": { 12 | "red": "0.200", 13 | "green": "0.340", 14 | "blue": "0.690", 15 | "alpha": "1.000" 16 | } 17 | } 18 | }, 19 | { 20 | "idiom": "universal", 21 | "color": { 22 | "color-space": "srgb", 23 | "components": { 24 | "red": "0.200", 25 | "green": "0.340", 26 | "blue": "0.690", 27 | "alpha": "1.000" 28 | } 29 | } 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /examples/build/ios/Size.swift: -------------------------------------------------------------------------------- 1 | 2 | // 3 | // Size.swift 4 | // 5 | 6 | // Do not edit directly 7 | // Generated on Mon, 20 Sep 2021 14:18:44 GMT 8 | 9 | 10 | import UIKit 11 | 12 | public class Size { 13 | public static let breakpointsLg = 1280 14 | public static let breakpointsMd = 1024 15 | public static let breakpointsSm = 768 16 | public static let sizes32 = 32.72 17 | public static let sizes40 = 40 18 | public static let sizes60 = 60 19 | public static let sizes80 = 80 20 | public static let sizesFrame = 32 21 | public static let sizesInVariant120 = 120 22 | public static let sizesInVariant60 = 60 23 | public static let sizesInVariant90 = 90 24 | public static let sizesPlainToken = 200 25 | public static let sizesRect = 32 26 | public static let sizesShapeInComponent = 32 27 | public static let sizesTokenInFrame = 200 28 | public static let sizesTokenInGroup = 200 29 | } 30 | -------------------------------------------------------------------------------- /examples/build/ios/StyleDictionary+Generated.swift: -------------------------------------------------------------------------------- 1 | 2 | // 3 | // StyleDictionary+Generated.swift 4 | // 5 | // Created by Design Token Generator on 20.09.2021. 6 | // 7 | 8 | import UIKit 9 | 10 | public class StyleDictionary { 11 | 12 | enum Fonts { 13 | public static let bodyH3 = UIFontMetrics.default.scaledFont(for: UIFont(name: "AkzidenzGroteskPro_Bold", size: 20)!) 14 | public static let bodyH4StrikeThrough = UIFontMetrics.default.scaledFont(for: UIFont(name: "Roboto", size: 16)!) 15 | public static let bodyItalic = UIFontMetrics.default.scaledFont(for: UIFont(name: "Roboto", size: 12)!) 16 | public static let bodyExtraBoldCondensedItalic = UIFontMetrics.default.scaledFont(for: UIFont(name: "Akzidenz-Grotesk Pro", size: 12)!) 17 | public static let bodyMediumExtendedItalic = UIFontMetrics.default.scaledFont(for: UIFont(name: "Akzidenz-Grotesk Pro", size: 20)!) 18 | public static let bodySuper = UIFontMetrics.default.scaledFont(for: UIFont(name: "AkzidenzGroteskPro_Black", size: 22)!) 19 | } 20 | 21 | enum LineHeight { 22 | public static let bodyH3 = 1.60 23 | public static let bodyH4StrikeThrough = 1.20 24 | public static let bodyItalic = 1.17 25 | public static let bodyExtraBoldCondensedItalic = 1.20 26 | public static let bodyMediumExtendedItalic = 1.20 27 | public static let bodySuper = 1.20 28 | } 29 | 30 | enum Leading { 31 | public static let bodyH3 = 1.02 32 | public static let bodyH4StrikeThrough = 1.00 33 | public static let bodyItalic = 1.00 34 | public static let bodyExtraBoldCondensedItalic = 1.00 35 | public static let bodyMediumExtendedItalic = 1.00 36 | public static let bodySuper = 1.00 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /examples/build/scss/variables.scss: -------------------------------------------------------------------------------- 1 | 2 | // Do not edit directly 3 | // Generated on Mon, 20 Sep 2021 14:18:44 GMT 4 | 5 | $sizes-32: 32.72px; 6 | $sizes-40: 40px; 7 | $sizes-60: 60px; 8 | $sizes-80: 80px; 9 | $sizes-plain-token: 200px; 10 | $sizes-token-in-frame: 200px; 11 | $sizes-token-in-group: 200px; 12 | $sizes-in-variant-60: 60px; 13 | $sizes-in-variant-90: 90px; 14 | $sizes-in-variant-120: 120px; 15 | $sizes-frame: 32px; 16 | $sizes-rect: 32px; 17 | $sizes-shape-in-component: 32px; 18 | $breakpoints-lg: 1280px; 19 | $breakpoints-sm: 768px; 20 | $breakpoints-md: 1024px; 21 | $radius-5: 5px; 22 | $radii-smoothing: 10px; 23 | $radii-mixed: 5.5px 10px 20px 15px; 24 | $gradient-gradient-single-with-multiple-color-stops: radial-gradient(rgb(255, 184, 0) 0%, rgb(255, 138, 0) 34%, rgb(255, 46, 0) 65%, rgb(255, 0, 0) 100%); 25 | $gradient-gradient-multiple-0: linear-gradient(180deg, rgb(255, 184, 0) 0%, rgb(255, 184, 0) 100%); 26 | $gradient-gradient-multiple-1: radial-gradient(rgb(255, 255, 255) 0%, rgb(255, 255, 255) 100%); 27 | $gradient-gradient-multiple-2: undefined; 28 | $gradient-gradient-multiple-3: undefined; 29 | $color-colors-multiple-fills-0: rgb(64, 255, 186); 30 | $color-colors-multiple-fills-1: rgba(0, 0, 0, 0.1); 31 | $color-colors-single-blue: rgb(4, 74, 255); 32 | $color-colors-special-characters: rgb(64, 223, 80); 33 | $color-colors-special-characters-nderung: rgb(52, 86, 175); 34 | $color-light-background: rgb(255, 255, 255); 35 | $color-dark-background: rgb(0, 0, 0); 36 | $font-body-h3: condensed 700 20/32 Akzidenz-Grotesk Pro; 37 | $font-body-h4-strike-through: italic 500 16/19.2 Roboto; 38 | $font-body-italic: italic 400 12/14 Roboto; 39 | $font-body-extra-bold-condensed-italic: condensed italic 800 12/14.4 Akzidenz-Grotesk Pro; 40 | $font-body-medium-extended-italic: expanded italic 500 20/24 Akzidenz-Grotesk Pro; 41 | $font-body-super: 900 22/26.4 Akzidenz-Grotesk Pro; 42 | $effect-drop-shadow-single: 0px 4px 4px 0px rgba(0, 0, 0, 0.25); 43 | $effect-inner-shadow-multiple-0: inset 0px 4px 4px 0px rgba(0, 0, 0, 0.25); 44 | $effect-inner-shadow-multiple-1: inset 10px 100px 1px 0.5px rgb(0, 0, 0); 45 | $effect-inner-shadow-multiple-2: inset -4px 2px 3px 11px rgba(0, 0, 0, 0.25); -------------------------------------------------------------------------------- /examples/filesToCopy/font_family.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 11 | -------------------------------------------------------------------------------- /examples/libs/android/colorName.js: -------------------------------------------------------------------------------- 1 | const camelCase = require('../common/camelCaseHelper') 2 | 3 | module.exports = { 4 | type: 'name', 5 | matcher: function (token) { 6 | return token.type === 'color' 7 | }, 8 | transformer: function (token) { 9 | return camelCase(token.path.slice(2).join(' ')) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/libs/android/fontSizeToSp.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | type: 'value', 3 | matcher: function (token) { 4 | return token.type === 'custom-fontStyle' 5 | }, 6 | transformer: function (token) { 7 | return `${token.value.fontSize}sp` 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/libs/android/formatFontStyle.js: -------------------------------------------------------------------------------- 1 | const { fileHeader } = require('style-dictionary').formatHelpers 2 | const camelCase = require('../common/camelCaseHelper') 3 | 4 | const letterSpacingToFloat = (letterSpacing, fontSize) => 1 + (letterSpacing / fontSize) 5 | 6 | const printDescription = description => (description && description !== '' && description !== null ? ` \n` : '') 7 | 8 | module.exports = ({ dictionary, platform, options = {}, file }) => { 9 | const fontStyles = dictionary.allTokens 10 | // remove underlined 11 | .filter(compositeToken => compositeToken.original.value.textDecoration !== 'underline') 12 | // create style 13 | .map(compositeToken => { 14 | return ` \n' 23 | }) 24 | return ( 25 | '\n' + 26 | fileHeader({ file, commentStyle: 'xml' }) + 27 | '\n\n' + 28 | fontStyles.join('\n') + 29 | '\n\n' 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /examples/libs/android/formatResourcesSorted.js: -------------------------------------------------------------------------------- 1 | const { fileHeader } = require('style-dictionary').formatHelpers 2 | 3 | const printDescription = description => (description && description !== '' && description !== null ? ` ` : '') 4 | 5 | module.exports = ({ dictionary, platform, options = {}, file }) => { 6 | const tokens = dictionary.allTokens 7 | .sort() 8 | // create style 9 | .map(token => ` <${file.resourceType} name="${token.name}">${token.value}${printDescription(token.description)};`) 10 | 11 | return ( 12 | '\n' + 13 | fileHeader({ file, commentStyle: 'xml' }) + 14 | '\n\n' + 15 | tokens.join('\n') + 16 | '\n\n' 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /examples/libs/android/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | 'android/pxToDp': require('./pxToDp'), 4 | 'android/color': require('../common/colorToHex8'), 5 | 'android/fontSize': require('./fontSizeToSp'), 6 | 'android/colorName': require('./colorName') 7 | }, 8 | action: { 9 | copy_fileOrFolder: require('../common/copyFileOrFolder') 10 | }, 11 | format: { 12 | 'android/resourcesSorted': require('./formatResourcesSorted'), 13 | 'android/fontStyle': require('./formatFontStyle') 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/libs/android/pxToDp.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | type: 'value', 3 | matcher: function (token) { 4 | return token.type === 'dimension' 5 | }, 6 | transformer: function (token) { 7 | return `${token.value}dp` 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/libs/common/camelCaseHelper.js: -------------------------------------------------------------------------------- 1 | const changeCase = require('change-case') 2 | 3 | module.exports = name => changeCase.camelCase(name, { transform: changeCase.camelCaseTransformMerge }) 4 | -------------------------------------------------------------------------------- /examples/libs/common/colorToHex8.js: -------------------------------------------------------------------------------- 1 | const TinyColor = require('@ctrl/tinycolor') 2 | 3 | module.exports = { 4 | type: 'value', 5 | matcher: function (token) { 6 | return token.type === 'color' 7 | }, 8 | transformer: function ({ value }) { 9 | return `${new TinyColor.TinyColor(value).toHex8String()}` 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/libs/common/colorToRgbaString.js: -------------------------------------------------------------------------------- 1 | const TinyColor = require('@ctrl/tinycolor') 2 | 3 | module.exports = { 4 | type: 'value', 5 | matcher: function (token) { 6 | return token.type === 'color' 7 | }, 8 | transformer: function ({ value }) { 9 | return `${new TinyColor.TinyColor(value).toRgbString()}` 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/libs/common/copyFileOrFolder.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra') 2 | 3 | module.exports = { 4 | do: function (dictionary, config) { 5 | config.options.copyFilesAction.forEach(({ destination, origin }) => { 6 | console.log(`Copying ${origin} to ${destination}`) 7 | fs.copySync(origin, destination) 8 | }) 9 | }, 10 | undo: function (dictionary, config) { 11 | config.options.copyFilesAction.forEach(({ destination, origin }) => { 12 | console.log(`Cleaning ${destination}`) 13 | fs.removeSync(destination) 14 | }) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/libs/ios/colorsets.js: -------------------------------------------------------------------------------- 1 | const TinyColor = require('@ctrl/tinycolor') 2 | const fs = require('fs-extra') 3 | const changeCase = require('change-case') 4 | 5 | const contents = { 6 | info: { 7 | author: 'xcode', 8 | version: 1 9 | } 10 | } 11 | 12 | const percentageToFloat = percentageString => { 13 | return parseInt(percentageString.substring(0, percentageString.length - 1)) / 100 14 | } 15 | 16 | const ratioRgb = color => { 17 | const colorObj = new TinyColor.TinyColor(color) 18 | const percentages = colorObj.toPercentageRgb() 19 | 20 | return { 21 | red: `${percentageToFloat(percentages.r).toFixed(3)}`, 22 | green: `${percentageToFloat(percentages.g).toFixed(3)}`, 23 | blue: `${percentageToFloat(percentages.b).toFixed(3)}`, 24 | alpha: `${percentages.a.toFixed(3)}` 25 | } 26 | } 27 | 28 | /** 29 | * This action will iterate over all the colors in the Style Dictionary 30 | * and for each one write a colorset with light and (optional) dark 31 | * mode versions. 32 | */ 33 | module.exports = { 34 | // This is going to run once per theme. 35 | do: (dictionary, platform) => { 36 | const assetPath = `${platform.buildPath}/DesignToken.xcassets` 37 | fs.emptyDirSync(assetPath) 38 | fs.writeFileSync(`${assetPath}/Contents.json`, JSON.stringify(contents, null, 2)) 39 | 40 | dictionary.allTokens 41 | .filter(token => token.type === 'color') 42 | .forEach(token => { 43 | const colorsetPath = `${assetPath}/${changeCase.pascalCase(token.path.slice(2).join(' '))}.colorset` 44 | fs.ensureDirSync(colorsetPath) 45 | 46 | // The colorset might already exist because Style Dictionary is run multiple 47 | // times with different configurations. If the colorset already exists we want 48 | // to modify it rather than writing over it. 49 | const colorset = fs.existsSync(`${colorsetPath}/Contents.json`) 50 | ? fs.readJsonSync(`${colorsetPath}/Contents.json`) 51 | : { ...contents, colors: [] } 52 | 53 | const color = { 54 | idiom: 'universal', 55 | color: { 56 | 'color-space': 'srgb', 57 | components: ratioRgb(token.value) 58 | } 59 | } 60 | 61 | if (token.path[0] === 'dark') { 62 | color.appearances = [{ 63 | appearance: 'luminosity', 64 | value: 'dark' 65 | }] 66 | } 67 | 68 | colorset.colors.push(color) 69 | 70 | fs.writeFileSync(`${colorsetPath}/Contents.json`, JSON.stringify(colorset, null, 2)) 71 | }) 72 | }, 73 | undo: function (dictionary, platform) { 74 | // no undo 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /examples/libs/ios/fontStyleTemplate.js: -------------------------------------------------------------------------------- 1 | const dateNow = () => { 2 | return new Date().toLocaleDateString('de-DE', { year: 'numeric', month: '2-digit', day: '2-digit' }) 3 | } 4 | 5 | module.exports = (style) => ` 6 | // 7 | // ${style.filename} 8 | // 9 | // Created by Design Token Generator on ${dateNow()}. 10 | // 11 | 12 | import UIKit 13 | 14 | public class ${style.class} { 15 | 16 | enum Fonts { 17 | ${style.font.join('\n ')} 18 | } 19 | 20 | enum LineHeight { 21 | ${style.lineheight.join('\n ')} 22 | } 23 | 24 | enum Leading { 25 | ${style.leading.join('\n ')} 26 | } 27 | 28 | } 29 | ` 30 | -------------------------------------------------------------------------------- /examples/libs/ios/fontStyles.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra') 2 | const camelCase = require('../common/camelCaseHelper') 3 | const fontStyleTemplate = require('./fontStyleTemplate') 4 | 5 | const fontFile = ({ fontFamily, fontWeight }, fontOpts) => { 6 | return fontOpts && fontOpts[`${fontFamily}.${fontWeight}`] ? fontOpts[`${fontFamily}.${fontWeight}`] : fontFamily 7 | } 8 | 9 | /** 10 | * This action will iterate over all the colors in the Style Dictionary 11 | * and for each one write a colorset with light and (optional) dark 12 | * mode versions. 13 | */ 14 | module.exports = { 15 | // This is going to run once per theme. 16 | do: (dictionary, platform) => { 17 | const assetPath = `${platform.buildPath}` 18 | fs.ensureDirSync(assetPath) 19 | 20 | const fontStyles = { 21 | filename: 'StyleDictionary+Generated.swift', 22 | class: 'StyleDictionary', 23 | font: [], 24 | lineheight: [], 25 | leading: [] 26 | } 27 | // cycle through all tokens 28 | dictionary.allTokens 29 | // filter out custom styles 30 | .filter(token => token.type === 'custom-fontStyle') 31 | // remove all underline styles (they can not be used like this in iOS) 32 | .filter(token => token.original.value.textDecoration !== 'underline') 33 | // split int 2 parts: font & fontSize, lineheight, leading 34 | .forEach(({ original: { value }, path }) => { 35 | // chaning name & removeing "font" from name for better DX 36 | const name = camelCase(path.slice(1).join(' ')) 37 | // lineheight 38 | fontStyles.lineheight.push(`public static let ${name} = ${(value.lineHeight / value.fontSize).toFixed(2)}`) 39 | // leading 40 | fontStyles.leading.push(`public static let ${name} = ${(1 + value.letterSpacing / value.fontSize).toFixed(2)}`) 41 | // font style 42 | fontStyles.font.push(`public static let ${name} = UIFontMetrics.default.scaledFont(for: UIFont(name: "${fontFile(value, platform.options.fontFamilies)}", size: ${value.fontSize})!)`) 43 | }) 44 | // write .swift file with definitions for defaults 45 | fs.writeFileSync(`${assetPath}/${fontStyles.filename}`, fontStyleTemplate(fontStyles)) 46 | }, 47 | undo: function (dictionary, platform) { 48 | // no undo 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /examples/libs/ios/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: {}, 3 | format: {}, 4 | action: { 5 | 'ios/colorSets': require('./colorsets'), 6 | 'ios/fontStyles': require('./fontStyles') 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/libs/web/fileHeader.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | * the License. A copy of the License is located at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | * and limitations under the License. 12 | */ 13 | 14 | // no-op default 15 | const defaultFileHeader = (arr) => arr 16 | 17 | const lineSeparator = '\n' 18 | const defaultFormatting = { 19 | lineSeparator, 20 | prefix: ' * ', 21 | header: `/**${lineSeparator}`, 22 | footer: `${lineSeparator} */${lineSeparator}${lineSeparator}` 23 | } 24 | 25 | /** 26 | * 27 | * This is for creating the comment at the top of generated files with the generated at date. 28 | * It will use the custom file header if defined on the configuration, or use the 29 | * default file header. 30 | * @memberof module:formatHelpers 31 | * @param {Object} options 32 | * @param {File} options.file - The file object that is passed to the formatter. 33 | * @param {String} options.commentStyle - The only options are 'short' and 'xml', which will use the // or \ style comments respectively. Anything else will use \/\* style comments. 34 | * @param {Object} options.formatting - Custom formatting properties that define parts of a comment in code. The configurable strings are: prefix, lineSeparator, header, and footer. 35 | * @returns {String} 36 | * @example 37 | * ```js 38 | * StyleDictionary.registerFormat({ 39 | * name: 'myCustomFormat', 40 | * formatter: function({ dictionary, file }) { 41 | * return fileHeader({file, 'short') + 42 | * dictionary.allTokens.map(token => `${token.name} = ${token.value}`) 43 | * .join('\n'); 44 | * } 45 | * }); 46 | * ``` 47 | */ 48 | function fileHeader ({ file = {}, commentStyle, formatting = {} }) { 49 | // showFileHeader is true by default 50 | let showFileHeader = true 51 | if (file.options && typeof file.options.showFileHeader !== 'undefined') { 52 | showFileHeader = file.options.showFileHeader 53 | } 54 | 55 | // Return empty string if the showFileHeader is false 56 | if (!showFileHeader) return '' 57 | 58 | let fn = defaultFileHeader 59 | if (file.options && typeof file.options.fileHeader === 'function') { 60 | fn = file.options.fileHeader 61 | } 62 | 63 | // default header 64 | const defaultHeader = [ 65 | 'Do not edit directly', 66 | `Generated on ${new Date().toString()}` 67 | ] 68 | 69 | let { prefix, lineSeparator, header, footer } = Object.assign({}, defaultFormatting, formatting) 70 | 71 | if (commentStyle === 'short') { 72 | prefix = '// ' 73 | header = `${lineSeparator}` 74 | footer = `${lineSeparator}${lineSeparator}` 75 | } else if (commentStyle === 'xml') { 76 | prefix = ' ' 77 | header = `` 79 | } 80 | 81 | return `${header}${fn(defaultHeader) 82 | .map(line => `${prefix}${line}`) 83 | .join(lineSeparator)}${footer}` 84 | } 85 | 86 | module.exports = fileHeader 87 | -------------------------------------------------------------------------------- /examples/libs/web/filterWeb.js: -------------------------------------------------------------------------------- 1 | const acceptedTypes = ['color', 'dimension', 'font', 'custom-radius', 'custom-fontStyle', 'custom-shadow', 'custom-gradient'] 2 | 3 | module.exports = (token) => acceptedTypes.includes(token.type) 4 | -------------------------------------------------------------------------------- /examples/libs/web/formatCss.js: -------------------------------------------------------------------------------- 1 | const formattedVariables = require('./formattedVariables') 2 | const fileHeader = require('./fileHeader') 3 | 4 | const filteredTokens = (dictionary, filterFn) => { 5 | const filtered = dictionary.allTokens.filter(token => filterFn(token)) 6 | return { 7 | ...dictionary, 8 | ...{ 9 | allProperties: filtered, 10 | allTokens: filtered 11 | } 12 | } 13 | } 14 | 15 | module.exports = ({ dictionary, options, file }) => { 16 | const opts = options ?? {} 17 | const { outputReferences } = opts 18 | const groupedTokens = { 19 | // if you export the prefixes use token.path[0] instead of [1] 20 | light: filteredTokens(dictionary, (token) => token.path[1].toLowerCase() === 'light'), 21 | dark: filteredTokens(dictionary, (token) => token.path[1].toLowerCase() === 'dark'), 22 | rest: filteredTokens(dictionary, (token) => !['light', 'dark'].includes(token.path[1].toLowerCase())) 23 | } 24 | 25 | return ( 26 | fileHeader({ file }) + 27 | ':root {\n' + 28 | formattedVariables({ format: 'css', dictionary: groupedTokens.rest, outputReferences }) + 29 | '\n}\n\n' + 30 | '@media (prefers-color-scheme: light) {\n' + 31 | ' :root {\n' + 32 | formattedVariables({ format: 'css', dictionary: groupedTokens.light, outputReferences }) + 33 | '\n }\n}\n\n' + 34 | '@media (prefers-color-scheme: dark) {\n' + 35 | ' :root {\n' + 36 | formattedVariables({ format: 'css', dictionary: groupedTokens.dark, outputReferences }) + 37 | '\n }\n}\n\n' + 38 | 'html[data-theme="light"] {\n' + 39 | formattedVariables({ format: 'css', dictionary: groupedTokens.light, outputReferences }) + 40 | '\n}\n\n' + 41 | 'html[data-theme="dark"] {\n' + 42 | formattedVariables({ format: 'css', dictionary: groupedTokens.dark, outputReferences }) + 43 | '\n}\n' 44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /examples/libs/web/formattedVariables.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | * the License. A copy of the License is located at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | * and limitations under the License. 12 | */ 13 | 14 | const createPropertyFormatter = require('./createPropertyFormatter') 15 | const sortByReference = require('./sortByReference') 16 | 17 | const defaultFormatting = { 18 | lineSeparator: '\n' 19 | } 20 | 21 | /** 22 | * 23 | * This is used to create lists of variables like Sass variables or CSS custom properties 24 | * @memberof module:formatHelpers 25 | * @param {Object} options 26 | * @param {String} options.format - What type of variables to output. Options are: css, sass, less, and stylus 27 | * @param {Object} options.dictionary - The dictionary object that gets passed to the formatter method. 28 | * @param {Boolean} options.outputReferences - Whether or not to output references 29 | * @param {Object} options.formatting - Custom formatting properties that define parts of a declaration line in code. This will get passed to `formatHelpers.createPropertyFormatter` and used for the `lineSeparator` between lines of code. 30 | * @returns {String} 31 | * @example 32 | * ```js 33 | * StyleDictionary.registerFormat({ 34 | * name: 'myCustomFormat', 35 | * formatter: function({ dictionary, options }) { 36 | * return formattedVariables('less', dictionary, options.outputReferences); 37 | * } 38 | * }); 39 | * ``` 40 | */ 41 | function formattedVariables ({ format, dictionary, outputReferences = false, formatting = {} }) { 42 | let { allTokens } = dictionary 43 | 44 | const { lineSeparator } = Object.assign({}, defaultFormatting, formatting) 45 | 46 | // Some languages are imperative, meaning a variable has to be defined 47 | // before it is used. If `outputReferences` is true, check if the token 48 | // has a reference, and if it does send it to the end of the array. 49 | // We also need to account for nested references, a -> b -> c. They 50 | // need to be defined in reverse order: c, b, a so that the reference always 51 | // comes after the definition 52 | if (outputReferences) { 53 | // note: using the spread operator here so we get a new array rather than 54 | // mutating the original 55 | allTokens = [...allTokens].sort(sortByReference(dictionary)) 56 | } 57 | 58 | return allTokens 59 | .map(createPropertyFormatter({ outputReferences, dictionary, format, formatting })) 60 | .filter(function (strVal) { return !!strVal }) 61 | .join(lineSeparator) 62 | } 63 | 64 | module.exports = formattedVariables 65 | -------------------------------------------------------------------------------- /examples/libs/web/index.js: -------------------------------------------------------------------------------- 1 | const StyleDictionary = require('style-dictionary') 2 | 3 | module.exports = { 4 | transform: { 5 | 'size/px': require('./sizePx'), 6 | 'web/shadow': require('./webShadows'), 7 | 'web/radius': require('./webRadius'), 8 | 'web/padding': require('./webPadding'), 9 | 'web/font': require('./webFont'), 10 | 'web/gradient': require('./webGradient'), 11 | 'color/hex8ToRgba': require('../common/colorToRgbaString') 12 | }, 13 | transformGroup: { 14 | 'custom/css': StyleDictionary.transformGroup.css.concat([ 15 | 'size/px', 16 | 'web/shadow', 17 | 'web/radius', 18 | 'web/padding', 19 | 'web/font', 20 | 'web/gradient', 21 | 'color/hex8ToRgba' 22 | ]) 23 | }, 24 | format: { 25 | 'custom/css': require('./formatCss') 26 | }, 27 | action: {} 28 | } 29 | -------------------------------------------------------------------------------- /examples/libs/web/sizePx.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | type: 'value', 3 | matcher: function (token) { 4 | return token.type === 'dimension' && token.value !== 0 5 | }, 6 | transformer: function (token) { 7 | return `${token.value}px` 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/libs/web/sortByReference.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | * the License. A copy of the License is located at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | * and limitations under the License. 12 | */ 13 | 14 | /** 15 | * A function that returns a sorting function to be used with Array.sort that 16 | * will sort the allTokens array based on references. This is to make sure 17 | * if you use output references that you never use a reference before it is 18 | * defined. 19 | * @memberof module:formatHelpers 20 | * @example 21 | * ```javascript 22 | * dictionary.allTokens.sort(sortByReference(dictionary)) 23 | * ``` 24 | * @param {Dictionary} dictionary 25 | * @returns {Function} 26 | */ 27 | function sortByReference(dictionary) { 28 | // The sorter function is recursive to account for multiple levels of nesting 29 | function sorter(a, b) { 30 | const aComesFirst = -1; 31 | const bComesFirst = 1; 32 | 33 | // If token a uses a reference and token b doesn't, b might come before a 34 | // read on.. 35 | if (a.original && dictionary.usesReference(a.original.value)) { 36 | // Both a and b have references, we need to see if the reference each other 37 | if (b.original && dictionary.usesReference(b.original.value)) { 38 | const aRefs = dictionary.getReferences(a.original.value); 39 | const bRefs = dictionary.getReferences(b.original.value); 40 | 41 | aRefs.forEach(aRef => { 42 | // a references b, we want b to come first 43 | if (aRef.name === b.name) { 44 | return bComesFirst; 45 | } 46 | }); 47 | 48 | bRefs.forEach(bRef => { 49 | // ditto but opposite 50 | if (bRef.name === a.name) { 51 | return aComesFirst; 52 | } 53 | }); 54 | 55 | // both a and b have references and don't reference each other 56 | // we go further down the rabbit hole (reference chain) 57 | return sorter(aRefs[0], bRefs[0]); 58 | // a has a reference and b does not: 59 | } else { 60 | return bComesFirst; 61 | } 62 | // a does not have a reference it should come first regardless if b has one 63 | } else { 64 | return aComesFirst; 65 | } 66 | } 67 | 68 | return sorter; 69 | } 70 | 71 | module.exports = sortByReference; -------------------------------------------------------------------------------- /examples/libs/web/webFont.js: -------------------------------------------------------------------------------- 1 | const notDefault = (value, defaultValue) => (value !== defaultValue) ? value : '' 2 | 3 | const fontFamily = ({ fontFamily }, { fontFamilies } = {}) => fontFamilies && fontFamilies[fontFamily] ? fontFamilies[fontFamily] : fontFamily 4 | 5 | module.exports = { 6 | type: 'value', 7 | matcher: function (token) { 8 | return token.type === 'custom-fontStyle' 9 | }, 10 | transformer: function ({ value: font }, { options }) { 11 | // font: font-style font-variant font-weight font-size/line-height font-family; 12 | return `${notDefault(font.fontStretch, 'normal')} ${notDefault(font.fontStyle, 'normal')} ${font.fontWeight} ${font.fontSize}/${font.lineHeight} ${fontFamily(font, options)}`.trim() 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/libs/web/webGradient.js: -------------------------------------------------------------------------------- 1 | const TinyColor = require('@ctrl/tinycolor') 2 | 3 | module.exports = { 4 | type: 'value', 5 | matcher: function (token) { 6 | return token.type === 'custom-gradient' 7 | }, 8 | transformer: function ({ value }) { 9 | const stopsString = value.stops.map(stop => { 10 | return `${new TinyColor.TinyColor(stop.color).toRgbString()} ${stop.position * 100}%` 11 | }).join(', ') 12 | if (value.gradientType === 'linear') { 13 | return `linear-gradient(${value.rotation}deg, ${stopsString})` 14 | } 15 | if (value.gradientType === 'radial') { 16 | return `radial-gradient(${stopsString})` 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/libs/web/webPadding.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | type: 'value', 3 | matcher: function (token) { 4 | return token.type === 'custom-spacing' 5 | }, 6 | transformer: ({ value: { top, left, bottom, right } }) => { 7 | if ([bottom, left, right].every(v => v === top)) { 8 | return `${top}px` 9 | } 10 | return `${top}px ${right}px ${bottom}px ${left}px` 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/libs/web/webRadius.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | type: 'value', 3 | matcher: function (token) { 4 | return token.type === 'custom-radius' 5 | }, 6 | transformer: function ({ value }) { 7 | if ([value.topRight, value.bottomLeft, value.bottomRight].every(v => v === value.topLeft)) { 8 | return `${value.topLeft}px` 9 | } 10 | return `${value.topLeft}px ${value.topRight}px ${value.bottomLeft}px ${value.bottomRight}px` 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/libs/web/webShadows.js: -------------------------------------------------------------------------------- 1 | const TinyColor = require('@ctrl/tinycolor') 2 | 3 | module.exports = { 4 | type: 'value', 5 | matcher: function (token) { 6 | return token.type === 'custom-shadow' && token.value !== 0 7 | }, 8 | transformer: function ({ value }) { 9 | return `${value.shadowType === 'innerShadow' ? 'inset ' : ''}${value.offsetX}px ${value.offsetY}px ${value.radius}px ${value.spread}px ${new TinyColor.TinyColor(value.color).toRgbString()}` 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /faq.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | ## Why does style dictionary output [object Object]? 4 | If you are using the [`standard` token format](https://github.com/lukasoppermann/design-tokens#standard-w3c-draft) you are expected to write your own transformers ([See examples](https://github.com/lukasoppermann/design-tokens/tree/main/examples)). 5 | While this is a bit more work at first, you can get much better results like splitting dark and light mode, or getting correct conversions for iOS and Android. 6 | 7 | To get simple conversion out of the box you need without any custom code, you need to change to the [`original` token format](https://github.com/lukasoppermann/design-tokens#original-deprecated). 8 | 9 | ## Why is the original format marked as deprecated? 10 | This is the format that was originally shipped with the plugin. It is still in here for compatibility reasons and will stay for a long time. 11 | 12 | It is marked as deprecated to push users towards the new `standard` format and to show that it will not receive any new feature updates. 13 | 14 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Design Tokens", 3 | "id": "888356646278934516", 4 | "api": "1.0.0", 5 | "main": "dist/plugin.js", 6 | "ui": "dist/ui.html", 7 | "editorType": [ 8 | "figma" 9 | ], 10 | "networkAccess": { 11 | "allowedDomains": [ 12 | "*" 13 | ], 14 | "reasoning": "Wildcard is needed to use the plugin with companies' self-hosted CI instance" 15 | }, 16 | "menu": [ 17 | { 18 | "name": "Export Design Token File", 19 | "command": "export" 20 | }, 21 | { 22 | "name": "Send Design Tokens to Url", 23 | "command": "urlExport" 24 | }, 25 | { 26 | "separator": true 27 | }, 28 | { 29 | "name": "Settings", 30 | "command": "generalSettings" 31 | }, 32 | { 33 | "separator": true 34 | }, 35 | { 36 | "name": "Help", 37 | "command": "help" 38 | }, 39 | { 40 | "name": "Demo file", 41 | "command": "demo" 42 | }, 43 | { 44 | "separator": true 45 | }, 46 | { 47 | "name": "Reset Settings", 48 | "command": "reset" 49 | } 50 | ], 51 | "documentAccess": "dynamic-page" 52 | } -------------------------------------------------------------------------------- /src/config/commands.ts: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | export type PluginCommands = 'generalSettings' | 3 | 'export' | 4 | 'sendSettings' | 5 | 'saveSettings' | 6 | 'help' | 7 | 'demo' | 8 | 'openUrl' | 9 | 'reset' | 10 | 'urlExport' | 11 | 'closePlugin' 12 | 13 | export const commands = { 14 | generalSettings: 'generalSettings' as PluginCommands, 15 | export: 'export' as PluginCommands, 16 | sendSettings: 'sendSettings' as PluginCommands, 17 | urlExport: 'urlExport' as PluginCommands, 18 | help: 'help' as PluginCommands, 19 | demo: 'demo' as PluginCommands, 20 | openUrl: 'openUrl' as PluginCommands, 21 | reset: 'reset' as PluginCommands, 22 | saveSettings: 'saveSettings' as PluginCommands, 23 | closePlugin: 'closePlugin' as PluginCommands 24 | } 25 | -------------------------------------------------------------------------------- /src/config/config.ts: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | export default { 3 | ui: { 4 | generalSettings: { 5 | width: 550, 6 | height: 836 7 | }, 8 | export: { 9 | width: 550, 10 | height: 356 11 | }, 12 | urlExport: { 13 | width: 550, 14 | height: 650 15 | } 16 | }, 17 | key: { 18 | lastVersionSettingsOpened: 'lastVersionSettingsOpened', 19 | fileId: 'fileId', 20 | settings: 'settings', 21 | extensionPluginData: 'org.lukasoppermann.figmaDesignTokens', 22 | extensionFigmaStyleId: 'styleId', 23 | extensionVariableStyleId: 'variableId', 24 | extensionAlias: 'alias', 25 | authType: { 26 | token: 'token', 27 | gitlabToken: 'gitlab_token', 28 | gitlabCommit: 'gitlab_commit', 29 | basic: 'Basic', 30 | bearer: 'Bearer' 31 | } 32 | }, 33 | exclusionPrefixDefault: ['_', '.'], 34 | fileExtensions: [ 35 | { 36 | label: '.tokens.json', 37 | value: '.tokens.json' 38 | }, 39 | { 40 | label: '.tokens', 41 | value: '.tokens' 42 | }, 43 | { 44 | label: '.json', 45 | value: '.json' 46 | } 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /src/config/defaultSettings.ts: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | import { Settings } from '@typings/settings' 3 | 4 | export const defaultSettings: Settings = { 5 | filename: 'design-tokens', 6 | extension: '.tokens.json', 7 | nameConversion: 'default', 8 | tokenFormat: 'standard', 9 | compression: false, 10 | urlJsonCompression: true, 11 | serverUrl: undefined, 12 | eventType: 'update-tokens', 13 | accessToken: undefined, 14 | acceptHeader: 'application/vnd.github.everest-preview+json', 15 | contentType: 'text/plain;charset=UTF-8', 16 | authType: 'token', 17 | reference: 'main', 18 | exclusionPrefix: '', 19 | excludeExtensionProp: false, 20 | alias: 'alias, ref, reference', 21 | keyInName: false, 22 | prefixInName: true, 23 | modeInTokenValue: false, 24 | modeInTokenName: false, 25 | resolveSameCollectionOrModeReference: false, 26 | prefix: { 27 | color: 'color', 28 | gradient: 'gradient', 29 | typography: 'typography', 30 | font: 'font', 31 | effect: 'effect', 32 | grid: 'grid', 33 | border: 'border, borders', 34 | breakpoint: 'breakpoint, breakpoints', 35 | radius: 'radius, radii', 36 | size: 'size, sizes', 37 | spacing: 'spacing', 38 | motion: 'motion', 39 | opacity: 'opacity, opacities' 40 | }, 41 | exports: { 42 | color: true, 43 | gradient: true, 44 | font: true, 45 | typography: true, 46 | effect: true, 47 | grid: true, 48 | border: true, 49 | breakpoint: true, 50 | radius: true, 51 | size: true, 52 | spacing: true, 53 | motion: true, 54 | opacity: true, 55 | variables: true 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/config/tokenTypes.ts: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | export const tokenTypes: Record = { 7 | color: { 8 | label: 'Colors', 9 | key: 'color' 10 | }, 11 | gradient: { 12 | label: 'Gradients', 13 | key: 'gradient' 14 | }, 15 | font: { 16 | label: 'Font Styles', 17 | key: 'font' 18 | }, 19 | typography: { 20 | label: 'Typography', 21 | key: 'typography', 22 | exclude: ['original'] 23 | }, 24 | effect: { 25 | label: 'Effects', 26 | key: 'effect' 27 | }, 28 | grid: { 29 | label: 'Grids', 30 | key: 'grid' 31 | }, 32 | border: { 33 | label: 'Borders', 34 | key: 'border' 35 | }, 36 | breakpoint: { 37 | label: 'Breakpoints', 38 | key: 'breakpoint' 39 | }, 40 | radius: { 41 | label: 'Radii', 42 | key: 'radius' 43 | }, 44 | size: { 45 | label: 'Sizes', 46 | key: 'size' 47 | }, 48 | spacing: { 49 | label: 'Spacing', 50 | key: 'spacing' 51 | }, 52 | motion: { 53 | label: 'Motion', 54 | key: 'motion' 55 | }, 56 | opacity: { 57 | label: 'Opacity', 58 | key: 'opacity' 59 | }, 60 | variables: { 61 | label: 'Figma Variables (BETA)', 62 | key: 'variables', 63 | exclude: ['original'] 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/extractor/extractBorders.ts: -------------------------------------------------------------------------------- 1 | import extractorInterface from '@typings/extractorInterface' 2 | import { borderPropertyInterface } from '@typings/propertyObject' 3 | import { StrokeCap, StrokeAlign, PropertyType } from '@typings/valueTypes' 4 | import { customTokenNode } from '@typings/tokenNodeTypes' 5 | import roundWithDecimals from '@utils/roundWithDecimals' 6 | import { tokenTypes } from '@config/tokenTypes' 7 | import { filterByPrefix } from './extractUtilities' 8 | import { tokenCategoryType } from '@typings/tokenCategory' 9 | import { tokenExportKeyType } from '@typings/tokenExportKey' 10 | import config from '@config/config' 11 | 12 | const strokeJoins = { 13 | MITER: 'miter', 14 | BEVEL: 'bevel', 15 | ROUND: 'round' 16 | } 17 | 18 | const strokeAligns = { 19 | CENTER: 'center', 20 | INSIDE: 'inside', 21 | OUTSIDE: 'outside' 22 | } 23 | 24 | const extractBorders: extractorInterface = (tokenNodes: customTokenNode[], prefixArray: string[]): borderPropertyInterface[] => { 25 | // return as object 26 | return tokenNodes.filter(filterByPrefix(prefixArray)) 27 | // remove nodes with no border property 28 | .filter(node => node.strokes.length > 0) 29 | // convert borders 30 | .map(node => ({ 31 | name: node.name, 32 | category: 'border' as tokenCategoryType, 33 | exportKey: tokenTypes.border.key as tokenExportKeyType, 34 | description: node.description || null, 35 | values: { 36 | strokeAlign: { 37 | value: strokeAligns[node.strokeAlign] as StrokeAlign, 38 | type: 'string' as PropertyType 39 | }, 40 | dashPattern: { 41 | value: [...(node.dashPattern !== undefined && node.dashPattern.length > 0 ? node.dashPattern : [0, 0])], 42 | type: 'string' as PropertyType 43 | }, 44 | strokeCap: { 45 | value: ((typeof node.strokeCap === 'string') ? node.strokeCap.toLowerCase() : 'mixed') as StrokeCap, 46 | type: 'string' as PropertyType 47 | }, 48 | strokeJoin: { 49 | value: strokeJoins[node.strokeJoin], 50 | type: 'string' as PropertyType 51 | }, 52 | strokeMiterLimit: { 53 | value: roundWithDecimals(node.strokeMiterLimit), 54 | unit: 'degree', 55 | type: 'number' as PropertyType 56 | }, 57 | // strokeStyleId: { 58 | // value: node.strokeStyleId 59 | // }, 60 | strokeWeight: { 61 | value: node.strokeWeight, 62 | unit: 'pixel', 63 | type: 'number' as PropertyType 64 | }, 65 | stroke: { 66 | value: node.strokes[0], 67 | type: 'color' as PropertyType 68 | } 69 | }, 70 | extensions: { 71 | [config.key.extensionPluginData]: { 72 | exportKey: tokenTypes.border.key as tokenExportKeyType 73 | } 74 | } 75 | })) 76 | } 77 | 78 | export default extractBorders 79 | -------------------------------------------------------------------------------- /src/extractor/extractBreakpoints.ts: -------------------------------------------------------------------------------- 1 | import extractorInterface from '@typings/extractorInterface' 2 | import { breakpointPropertyInterface } from '@typings/propertyObject' 3 | import { customTokenNode } from '@typings/tokenNodeTypes' 4 | import { UnitTypePixel, PropertyType } from '@typings/valueTypes' 5 | import { tokenTypes } from '@config/tokenTypes' 6 | import roundWithDecimals from '@utils/roundWithDecimals' 7 | import { filterByPrefix } from './extractUtilities' 8 | import { tokenCategoryType } from '@typings/tokenCategory' 9 | import { tokenExportKeyType } from '@typings/tokenExportKey' 10 | import config from '@config/config' 11 | 12 | const extractBreakpoints: extractorInterface = (tokenNodes: customTokenNode[], prefixArray: string[]): breakpointPropertyInterface[] => { 13 | // return as object 14 | return tokenNodes.filter(filterByPrefix(prefixArray)).map(node => ({ 15 | name: node.name, 16 | category: 'breakpoint' as tokenCategoryType, 17 | exportKey: tokenTypes.breakpoint.key as tokenExportKeyType, 18 | description: node.description || null, 19 | values: { 20 | width: { 21 | value: roundWithDecimals(node.width, 2), 22 | unit: 'pixel' as UnitTypePixel, 23 | type: 'number' as PropertyType 24 | }, 25 | height: { 26 | value: roundWithDecimals(node.height, 2), 27 | unit: 'pixel' as UnitTypePixel, 28 | type: 'number' as PropertyType 29 | } 30 | }, 31 | extensions: { 32 | [config.key.extensionPluginData]: { 33 | exportKey: tokenTypes.breakpoint.key as tokenExportKeyType 34 | } 35 | } 36 | })) 37 | } 38 | 39 | export default extractBreakpoints 40 | -------------------------------------------------------------------------------- /src/extractor/extractEffects.ts: -------------------------------------------------------------------------------- 1 | import extractorInterface from '@typings/extractorInterface' 2 | import { effectPropertyInterface } from '@typings/propertyObject' 3 | import { EffectType, UnitTypePixel, PropertyType } from '@typings/valueTypes' 4 | import { tokenTypes } from '@config/tokenTypes' 5 | import { roundRgba } from '@utils/convertColor' 6 | import { tokenCategoryType } from '@typings/tokenCategory' 7 | import { tokenExportKeyType } from '@typings/tokenExportKey' 8 | import config from '@config/config' 9 | 10 | const effectType = { 11 | LAYER_BLUR: 'layerBlur', 12 | BACKGROUND_BLUR: 'backgroundBlur', 13 | DROP_SHADOW: 'dropShadow', 14 | INNER_SHADOW: 'innerShadow' 15 | } 16 | 17 | const blurValues = (effect) => ({ 18 | effectType: { 19 | value: effectType[effect.type] as EffectType, 20 | type: 'string' as PropertyType 21 | }, 22 | radius: { 23 | value: effect.radius, 24 | unit: 'pixel' as UnitTypePixel, 25 | type: 'number' as PropertyType 26 | } 27 | }) 28 | 29 | const shadowValues = effect => ({ 30 | effectType: { 31 | value: effectType[effect.type] as EffectType, 32 | type: 'string' as PropertyType 33 | }, 34 | radius: { 35 | value: effect.radius, 36 | unit: 'pixel' as UnitTypePixel, 37 | type: 'number' as PropertyType 38 | }, 39 | color: { 40 | value: roundRgba(effect.color), 41 | type: 'color' as PropertyType 42 | }, 43 | offset: { 44 | x: { 45 | value: effect.offset.x, 46 | unit: 'pixel' as UnitTypePixel, 47 | type: 'number' as PropertyType 48 | }, 49 | y: { 50 | value: effect.offset.y, 51 | unit: 'pixel' as UnitTypePixel, 52 | type: 'number' as PropertyType 53 | } 54 | }, 55 | spread: { 56 | value: effect.spread, 57 | unit: 'pixel' as UnitTypePixel, 58 | type: 'number' as PropertyType 59 | } 60 | }) 61 | 62 | const extractEffects: extractorInterface = (tokenNodes: EffectStyle[], prefixArray: string[]): effectPropertyInterface[] => { 63 | // get effect styles 64 | return tokenNodes 65 | // remove tokens with no grid 66 | .filter(node => node.effects.length > 0) 67 | // build 68 | .map(node => ({ 69 | name: `${prefixArray[0]}/${node.name}`, 70 | category: 'effect' as tokenCategoryType, 71 | exportKey: tokenTypes.effect.key as tokenExportKeyType, 72 | description: node.description || null, 73 | values: node.effects.map( 74 | (effect: Effect) => 75 | effect.type === 'LAYER_BLUR' || effect.type === 'BACKGROUND_BLUR' 76 | ? blurValues(effect) 77 | : shadowValues(effect) 78 | ), 79 | extensions: { 80 | [config.key.extensionPluginData]: { 81 | [config.key.extensionFigmaStyleId]: node.id, 82 | exportKey: tokenTypes.effect.key as tokenExportKeyType 83 | } 84 | } 85 | })) 86 | } 87 | 88 | export default extractEffects 89 | -------------------------------------------------------------------------------- /src/extractor/extractGrids.ts: -------------------------------------------------------------------------------- 1 | import extractorInterface from '@typings/extractorInterface' 2 | import { gridPropertyInterface } from '@typings/propertyObject' 3 | import { GridAlignment, GridPattern, PropertyType, UnitTypePixel } from '@typings/valueTypes' 4 | import { tokenTypes } from '@config/tokenTypes' 5 | import { tokenCategoryType } from '@typings/tokenCategory' 6 | import { tokenExportKeyType } from '@typings/tokenExportKey' 7 | import config from '@config/config' 8 | 9 | const gridValues = (grid: GridLayoutGrid) => ({ 10 | pattern: { 11 | value: grid.pattern.toLowerCase() as GridPattern, 12 | type: 'string' as PropertyType 13 | }, 14 | sectionSize: { 15 | value: grid.sectionSize, 16 | unit: 'pixel' as UnitTypePixel, 17 | type: 'number' as PropertyType 18 | } 19 | }) 20 | 21 | const getCount = count => { 22 | if (count === Infinity) { 23 | return { 24 | value: 'auto', 25 | type: 'string' as PropertyType 26 | } 27 | } 28 | return { 29 | value: count, 30 | type: 'number' as PropertyType 31 | } 32 | } 33 | 34 | const rowColumnValues = (grid: RowsColsLayoutGrid) => ({ 35 | pattern: { 36 | value: grid.pattern.toLowerCase() as GridPattern, 37 | type: 'string' as PropertyType 38 | }, 39 | // undefined when alignment stretch 40 | ...(grid.sectionSize !== undefined && { 41 | sectionSize: { 42 | value: grid.sectionSize, 43 | unit: 'pixel' as UnitTypePixel, 44 | type: 'number' as PropertyType 45 | } 46 | }), 47 | gutterSize: { 48 | value: grid.gutterSize, 49 | unit: 'pixel' as UnitTypePixel, 50 | type: 'number' as PropertyType 51 | }, 52 | alignment: { 53 | value: grid.alignment.toLowerCase() as GridAlignment, 54 | type: 'string' as PropertyType 55 | }, 56 | count: getCount(grid.count), 57 | // undefined when alignment centred 58 | ...(grid.offset !== undefined && { 59 | offset: { 60 | value: grid.offset, 61 | unit: 'pixel' as UnitTypePixel, 62 | type: 'number' as PropertyType 63 | } 64 | }) 65 | }) 66 | 67 | const extractGrids: extractorInterface = (tokenNodes: GridStyle[], prefixArray: string[]): gridPropertyInterface[] => { 68 | // get grid styles 69 | return tokenNodes 70 | // remove tokens with no grid 71 | .filter(node => node.layoutGrids.length > 0) 72 | // build 73 | .map(node => ({ 74 | name: `${prefixArray[0]}/${node.name}`, 75 | category: 'grid' as tokenCategoryType, 76 | exportKey: tokenTypes.grid.key as tokenExportKeyType, 77 | description: node.description || null, 78 | values: node.layoutGrids.map((grid: LayoutGrid) => grid.pattern === 'GRID' ? gridValues(grid) : rowColumnValues(grid)), 79 | extensions: { 80 | [config.key.extensionPluginData]: { 81 | [config.key.extensionFigmaStyleId]: node.id, 82 | exportKey: tokenTypes.grid.key as tokenExportKeyType 83 | } 84 | } 85 | })) 86 | } 87 | 88 | export default extractGrids 89 | -------------------------------------------------------------------------------- /src/extractor/extractOpacities.ts: -------------------------------------------------------------------------------- 1 | import extractorInterface from '@typings/extractorInterface' 2 | import { opacityPropertyInterface } from '@typings/propertyObject' 3 | import { customTokenNode } from '@typings/tokenNodeTypes' 4 | import { PropertyType } from '@typings/valueTypes' 5 | import { tokenTypes } from '@config/tokenTypes' 6 | import roundWithDecimals from '@utils/roundWithDecimals' 7 | import { filterByPrefix } from './extractUtilities' 8 | import { tokenExportKeyType } from '@typings/tokenExportKey' 9 | import { tokenCategoryType } from '@typings/tokenCategory' 10 | import config from '@config/config' 11 | 12 | const extractOpacities: extractorInterface = (tokenNodes: customTokenNode[], prefixArray: string[]): opacityPropertyInterface[] => { 13 | // return as object 14 | return tokenNodes.filter(filterByPrefix(prefixArray)).map(node => ({ 15 | name: node.name, 16 | category: 'opacity' as tokenCategoryType, 17 | exportKey: tokenTypes.opacity.key as tokenExportKeyType, 18 | description: node.description || null, 19 | values: { 20 | opacity: { 21 | value: roundWithDecimals(node.opacity, 2), 22 | type: 'number' as PropertyType 23 | } 24 | }, 25 | extensions: { 26 | [config.key.extensionPluginData]: { 27 | exportKey: tokenTypes.opacity.key as tokenExportKeyType 28 | } 29 | } 30 | })) 31 | } 32 | 33 | export default extractOpacities 34 | -------------------------------------------------------------------------------- /src/extractor/extractRadii.ts: -------------------------------------------------------------------------------- 1 | import extractorInterface from '@typings/extractorInterface' 2 | import { radiusPropertyInterface } from '@typings/propertyObject' 3 | import { customTokenNode } from '@typings/tokenNodeTypes' 4 | import { UnitTypePixel, PropertyType } from '@typings/valueTypes' 5 | import { tokenTypes } from '@config/tokenTypes' 6 | import roundWithDecimals from '@utils/roundWithDecimals' 7 | import { filterByPrefix } from './extractUtilities' 8 | import { tokenCategoryType } from '@typings/tokenCategory' 9 | import { tokenExportKeyType } from '@typings/tokenExportKey' 10 | import config from '@config/config' 11 | 12 | const extractRadii: extractorInterface = (tokenNodes: customTokenNode[], prefixArray: string[]): radiusPropertyInterface[] => { 13 | // get the type of the corner radius 14 | const getRadiusType = radius => { 15 | if (typeof radius === 'number') { 16 | return 'single' 17 | } 18 | return 'mixed' 19 | } 20 | // get the individual radii 21 | const getRadii = (node) => ({ 22 | topLeft: { 23 | value: node.topLeftRadius || 0, 24 | unit: 'pixel' as UnitTypePixel, 25 | type: 'number' as PropertyType 26 | }, 27 | topRight: { 28 | value: node.topRightRadius || 0, 29 | unit: 'pixel' as UnitTypePixel, 30 | type: 'number' as PropertyType 31 | }, 32 | bottomRight: { 33 | value: node.bottomRightRadius || 0, 34 | unit: 'pixel' as UnitTypePixel, 35 | type: 'number' as PropertyType 36 | }, 37 | bottomLeft: { 38 | value: node.bottomLeftRadius || 0, 39 | unit: 'pixel' as UnitTypePixel, 40 | type: 'number' as PropertyType 41 | } 42 | }) 43 | // return as object 44 | return tokenNodes.filter(filterByPrefix(prefixArray)) 45 | .map(node => ({ 46 | name: node.name, 47 | category: 'radius' as tokenCategoryType, 48 | exportKey: tokenTypes.radius.key as tokenExportKeyType, 49 | description: node.description || null, 50 | values: { 51 | ...(typeof node.cornerRadius === 'number' && { 52 | radius: { 53 | value: node.cornerRadius, 54 | unit: 'pixel' as UnitTypePixel, 55 | type: 'number' as PropertyType 56 | } 57 | }), 58 | radiusType: { 59 | value: getRadiusType(node.cornerRadius), 60 | type: 'string' as PropertyType 61 | }, 62 | radii: getRadii(node), 63 | smoothing: { 64 | value: roundWithDecimals(node.cornerSmoothing, 2), 65 | comment: 'Percent as decimal from 0.0 - 1.0', 66 | type: 'number' as PropertyType 67 | } 68 | }, 69 | extensions: { 70 | [config.key.extensionPluginData]: { 71 | exportKey: tokenTypes.radius.key as tokenExportKeyType 72 | } 73 | } 74 | })) 75 | } 76 | 77 | export default extractRadii 78 | -------------------------------------------------------------------------------- /src/extractor/extractSizes.ts: -------------------------------------------------------------------------------- 1 | import extractorInterface from '@typings/extractorInterface' 2 | import { sizePropertyInterface } from '@typings/propertyObject' 3 | import { customTokenNode } from '@typings/tokenNodeTypes' 4 | import { UnitTypePixel, PropertyType } from '@typings/valueTypes' 5 | import { tokenTypes } from '@config/tokenTypes' 6 | import roundWithDecimals from '@utils/roundWithDecimals' 7 | import { filterByPrefix } from './extractUtilities' 8 | import { tokenExportKeyType } from '@typings/tokenExportKey' 9 | import { tokenCategoryType } from '@typings/tokenCategory' 10 | import config from '@config/config' 11 | 12 | const extractSizes: extractorInterface = (tokenNodes: customTokenNode[], prefixArray: string[]): sizePropertyInterface[] => { 13 | // return as object 14 | return tokenNodes.filter(filterByPrefix(prefixArray)).map(node => ({ 15 | name: node.name, 16 | category: 'size' as tokenCategoryType, 17 | exportKey: tokenTypes.size.key as tokenExportKeyType, 18 | description: node.description || null, 19 | values: { 20 | width: { 21 | value: roundWithDecimals(node.width, 2), 22 | unit: 'pixel' as UnitTypePixel, 23 | type: 'number' as PropertyType 24 | }, 25 | height: { 26 | value: roundWithDecimals(node.height, 2), 27 | unit: 'pixel' as UnitTypePixel, 28 | type: 'number' as PropertyType 29 | } 30 | }, 31 | extensions: { 32 | [config.key.extensionPluginData]: { 33 | exportKey: tokenTypes.size.key as tokenExportKeyType 34 | } 35 | } 36 | })) 37 | } 38 | 39 | export default extractSizes 40 | -------------------------------------------------------------------------------- /src/extractor/extractSpacing.ts: -------------------------------------------------------------------------------- 1 | import extractorInterface from '@typings/extractorInterface' 2 | import { spacingPropertyInterface } from '@typings/propertyObject' 3 | import { customTokenNode } from '@typings/tokenNodeTypes' 4 | import { UnitTypePixel, PropertyType } from '@typings/valueTypes' 5 | import { tokenTypes } from '@config/tokenTypes' 6 | import roundWithDecimals from '@utils/roundWithDecimals' 7 | import { filterByPrefix } from './extractUtilities' 8 | import { tokenCategoryType } from '@typings/tokenCategory' 9 | import { tokenExportKeyType } from '@typings/tokenExportKey' 10 | import config from '@config/config' 11 | 12 | const extractSpacing: extractorInterface = (tokenNodes: customTokenNode[], prefixArray: string[]): spacingPropertyInterface[] => { 13 | // return as object 14 | return tokenNodes.filter(filterByPrefix(prefixArray)) 15 | .map(node => ({ 16 | name: node.name, 17 | category: 'spacing' as tokenCategoryType, 18 | exportKey: tokenTypes.spacing.key as tokenExportKeyType, 19 | description: node.description || null, 20 | values: { 21 | top: { 22 | value: roundWithDecimals(node.paddingTop, 2), 23 | unit: 'pixel' as UnitTypePixel, 24 | type: 'number' as PropertyType 25 | }, 26 | right: { 27 | value: roundWithDecimals(node.paddingRight, 2), 28 | unit: 'pixel' as UnitTypePixel, 29 | type: 'number' as PropertyType 30 | }, 31 | bottom: { 32 | value: roundWithDecimals(node.paddingBottom, 2), 33 | unit: 'pixel' as UnitTypePixel, 34 | type: 'number' as PropertyType 35 | }, 36 | left: { 37 | value: roundWithDecimals(node.paddingLeft, 2), 38 | unit: 'pixel' as UnitTypePixel, 39 | type: 'number' as PropertyType 40 | } 41 | }, 42 | extensions: { 43 | [config.key.extensionPluginData]: { 44 | exportKey: tokenTypes.spacing.key as tokenExportKeyType 45 | } 46 | } 47 | }) 48 | ) 49 | } 50 | 51 | export default extractSpacing 52 | -------------------------------------------------------------------------------- /src/extractor/extractUtilities.ts: -------------------------------------------------------------------------------- 1 | export const filterByPrefix = (prefixArray: string[]) => node => { 2 | // abort if wrong argument 3 | if (!Array.isArray(prefixArray)) return 4 | // extract prefix from node name 5 | const nodePrefix = node.name.substr(0, node.name.indexOf('/')).replace(/\s+/g, '') 6 | // abort if no prefix 7 | if (nodePrefix.length === 0) return 8 | // return array 9 | return prefixArray.includes(nodePrefix) 10 | } 11 | -------------------------------------------------------------------------------- /src/transformer/tokenExtensions.ts: -------------------------------------------------------------------------------- 1 | import { internalTokenInterface } from '@typings/propertyObject' 2 | import { StandardTokenExtensionsInterface } from '@typings/standardToken' 3 | 4 | export const tokenExtensions = (token: internalTokenInterface, { excludeExtensionProp }): { extensions: StandardTokenExtensionsInterface; } => { 5 | if (excludeExtensionProp !== true) { 6 | return { 7 | extensions: { 8 | ...token.extensions 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/ui/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/css' 2 | import * as React from 'react' 3 | 4 | const style = css` 5 | display: flex; 6 | align-items: center; 7 | border-radius: var(--border-radius-large); 8 | flex-shrink: 0; 9 | font-family: var(--font-stack); 10 | font-size: var(--font-size-xsmall); 11 | font-weight: var(--font-weight-medium); 12 | letter-spacing: var(--font-letter-spacing-neg-small); 13 | line-height: var(--font-line-height); 14 | height: var(--size-medium); 15 | padding: 0 var(--size-xsmall) 0 var(--size-xsmall); 16 | text-decoration: none; 17 | outline: none; 18 | border: 2px solid transparent; 19 | user-select: none; 20 | 21 | // primary 22 | &.button--primary { 23 | color: var(--figma-color-text-onbrand); 24 | background-color: var(--figma-color-bg-brand); 25 | 26 | &:enabled:active, &:enabled:focus { 27 | border: 2px solid var(--figma-color-border-brand); 28 | } 29 | &:disabled { 30 | border: 1px solid var(--figma-color-border); 31 | } 32 | } 33 | 34 | // secondary 35 | &.button--secondary { 36 | background-color: var(--figma-color-bg); 37 | border: 1px solid var(--figma-color-border-strong); 38 | color: var(--figma-color-text); 39 | padding: 0 calc(var(--size-xsmall) + 1px) 0 calc(var(--size-xsmall) + 1px); 40 | letter-spacing: var(--font-letter-spacing-pos-small); 41 | 42 | &:enabled:active, &:enabled:focus { 43 | border: 2px solid var(--figma-color-border-brand); 44 | padding: 0 var(--size-xsmall) 0 var(--size-xsmall); 45 | } 46 | &:disabled { 47 | border: 1px solid var(--figma-color-border); 48 | color: var(--figma-color-text-secondary); 49 | } 50 | } 51 | ` 52 | 53 | type buttonProps = { 54 | children: any; 55 | className?: string; 56 | autofocus?: boolean; 57 | isSecondary?: boolean; 58 | isDisabled?: boolean; 59 | type?: 'submit' | 'button' | 'reset'; 60 | onClick?: (event: React.MouseEvent) => void; 61 | } 62 | 63 | export const Button = ({ 64 | children, 65 | className, 66 | isSecondary, 67 | isDisabled, 68 | onClick, 69 | type, 70 | autofocus 71 | }: buttonProps) => { 72 | return ( 73 | 82 | ) 83 | } 84 | -------------------------------------------------------------------------------- /src/ui/components/CancelButton.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { useContext } from 'react' 3 | import { Button } from '@components/Button' 4 | import { commands } from '@config/commands' 5 | import { FigmaContext } from '@ui/context' 6 | 7 | const CancelButton = () => { 8 | const { figmaUIApi } = useContext(FigmaContext) 9 | 10 | const closePlugin = () => { 11 | // close the plugin 12 | figmaUIApi.postMessage({ 13 | pluginMessage: { 14 | command: commands.closePlugin 15 | } 16 | // @ts-ignore 17 | }, '*') 18 | } 19 | 20 | return 21 | } 22 | export { CancelButton } 23 | -------------------------------------------------------------------------------- /src/ui/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/css' 2 | import * as React from 'react' 3 | 4 | const style = css` 5 | padding: 8px 0; 6 | display: flex; 7 | justify-content: flex-start; 8 | margin-top: 16px; 9 | border-top: 1px solid var(--figma-color-border); 10 | & > * { 11 | align-self: center; 12 | } 13 | button { 14 | margin-right: 4px; 15 | } 16 | :last-child{ 17 | margin-right: 0; 18 | } 19 | & [data-align="start"] { 20 | margin-right: auto; 21 | } 22 | ` 23 | 24 | type FooterProps = { 25 | children: any 26 | } 27 | 28 | export const Footer = ({ children }: FooterProps) => { 29 | return ( 30 |
31 | {children} 32 |
33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /src/ui/components/Info.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/css' 2 | import * as React from 'react' 3 | 4 | type InfoProps = { 5 | label: string, 6 | width: number, 7 | position?: 'center' | 'left' 8 | } 9 | 10 | const style = css` 11 | position: relative; 12 | cursor: help; 13 | display: inline-block; 14 | svg path { 15 | fill: var(--figma-color-icon-secondary); 16 | } 17 | .tooltip { 18 | pointer-events: none; 19 | background: var(--ui-contrast); 20 | font-size: var(--font-size-xsmall); 21 | font-weight: normal; 22 | color: var(--on--ui-contrast); 23 | padding: 4px 6px; 24 | border-radius: 3px; 25 | opacity: 0; 26 | display: inline-block; 27 | position: absolute; 28 | white-space: pre-line; 29 | top: 0; 30 | transform: translate(-50%, -90%); 31 | transition: opacity .35s ease; 32 | backdrop-filter: blur(5px); 33 | left: 50%; 34 | ::before { 35 | content: ""; 36 | display: block; 37 | position: absolute; 38 | border: 4px solid transparent; 39 | border-top-color: var(--ui-contrast); 40 | bottom: -8px; 41 | left: 50%; 42 | transform: translateX(-50%); 43 | } 44 | } 45 | &:hover { 46 | svg path { 47 | fill: var(--figma-color-icon-hover); 48 | } 49 | .tooltip { 50 | transform: translate(-50%, -96%); 51 | opacity: .85; 52 | } 53 | } 54 | &.position-left:hover { 55 | .tooltip { 56 | transform: translate(-90%, -96%); 57 | ::before { 58 | left:90%; 59 | } 60 | } 61 | } 62 | @media (prefers-color-scheme: dark) { 63 | .tooltip { 64 | color: var(--ui-contrast); 65 | background: var(--on--ui-contrast); 66 | } 67 | } 68 | ` 69 | 70 | export const Info = ({ label, width, position = 'center' }: InfoProps) => { 71 | return ( 72 |
73 |
{label}
74 | 75 | 76 | 77 | 78 |
79 | ) 80 | } 81 | -------------------------------------------------------------------------------- /src/ui/components/Label.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/css' 2 | import * as React from 'react' 3 | 4 | const style = css` 5 | font-weight: var(--font-weight-normal); 6 | font-size: var(--font-size-small); 7 | letter-spacing: var(--font-letter-spacing-pos-small); 8 | line-height: var(--line-height); 9 | color: var(--figma-color-text-secondary); 10 | height: var(--size-medium); 11 | width: 100%; 12 | display: flex; 13 | align-items: center; 14 | cursor: default; 15 | user-select: none; 16 | padding: 0 var(--size-xxxsmall) 0 var(--size-xxsmall); 17 | ` 18 | 19 | type props = { 20 | children: any; 21 | className?: string; 22 | } 23 | 24 | export const Label = ({ 25 | children, 26 | className 27 | }: props) => { 28 | return ( 29 |
30 | {children} 31 |
32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /src/ui/components/Row.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/css' 2 | import * as React from 'react' 3 | 4 | const style = css` 5 | display: flex; 6 | &.fill > * { 7 | flex-grow: 1; 8 | } 9 | &.fill > .flex-grow--none { 10 | flex-grow: 0; 11 | } 12 | ` 13 | 14 | type RowProps = { 15 | children: any, 16 | fill?: boolean 17 | } 18 | 19 | export const Row = ({ children, fill }: RowProps) => { 20 | return ( 21 |
{children}
22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /src/ui/components/Separator.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/css' 2 | import * as React from 'react' 3 | 4 | const style = css` 5 | margin-top: 8px; 6 | height: 1px; 7 | border: 0px; 8 | width: 100%; 9 | background: var(--figma-color-border); 10 | ` 11 | 12 | export const Separator = () => { 13 | return ( 14 |
15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/ui/components/Text.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/css' 2 | import * as React from 'react' 3 | 4 | const style = css` 5 | font-family: var(--font-stack); 6 | font-size: var(--font-size-xsmall); 7 | font-weight: var(--font-weight-normal); 8 | line-height: var(--font-line-height); 9 | letter-spacing: var(--font-letter-spacing-pos-xsmall); 10 | 11 | /* sizes */ 12 | &.type--small { 13 | font-size: var(--font-size-small); 14 | letter-spacing: var(--font-letter-spacing-pos-small); 15 | } 16 | &.type--large { 17 | font-size: var(--font-size-large); 18 | line-height: var(--font-line-height-large); 19 | letter-spacing: var(--font-letter-spacing-pos-large); 20 | } 21 | &.type--xlarge { 22 | font-size: var(--font-size-xlarge); 23 | line-height: var(--font-line-height-large); 24 | letter-spacing: var(--font-letter-spacing-pos-xlarge); 25 | } 26 | ` 27 | 28 | type props = { 29 | children: any; 30 | className?: string; 31 | size?: 'small' | 'large' | 'xlarge'; 32 | } 33 | 34 | export const Text = ({ 35 | children, 36 | className, 37 | size 38 | }: props) => { 39 | return ( 40 |

41 | {children} 42 |

43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /src/ui/components/Title.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/css' 2 | import * as React from 'react' 3 | 4 | const style = css` 5 | font-size: var(--font-size-xsmall); 6 | font-weight: var(--font-weight-bold); 7 | letter-spacing: var( --font-letter-spacing-pos-xsmall); 8 | line-height: var(--line-height); 9 | height: var(--size-medium); 10 | width: 100%; 11 | display: flex; 12 | align-items: center; 13 | cursor: default; 14 | user-select: none; 15 | padding: 0 calc(var(--size-xxsmall) / 2) 0 var(--size-xxsmall); 16 | /* sizes */ 17 | &.title--small { 18 | font-size: var(--font-size-small); 19 | letter-spacing: var(--font-letter-spacing-pos-small); 20 | } 21 | &.title--large { 22 | font-size: var(--font-size-large); 23 | line-height: var(--font-line-height-large); 24 | letter-spacing: var(--font-letter-spacing-pos-large); 25 | } 26 | &.title--xlarge { 27 | font-size: var(--font-size-xlarge); 28 | line-height: var(--font-line-height-large); 29 | letter-spacing: var(--font-letter-spacing-pos-xlarge); 30 | } 31 | &.title--medium { 32 | font-weight: var(--font-weight-medium); 33 | } 34 | &.title--bold { 35 | font-weight: var(--font-weight-bold); 36 | } 37 | ` 38 | 39 | type props = { 40 | children: any; 41 | className?: string; 42 | size?: 'small' | 'large' | 'xlarge'; 43 | weight?: 'medium' | 'bold'; 44 | } 45 | 46 | export const Title = ({ 47 | children, 48 | className, 49 | size, 50 | weight 51 | }: props) => { 52 | return ( 53 |

{children}

54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /src/ui/components/VersionNotice.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/css' 2 | import * as React from 'react' 3 | import { versionDifference } from '@utils/semVerDifference' 4 | 5 | const style = css` 6 | font-family: var(--font-stack); 7 | font-size: var(--font-size-xsmall); 8 | font-weight: var(--font-weight-normal); 9 | letter-spacing: var(--font-letter-spacing-pos-xsmall); 10 | line-height: var(--font-line-height); 11 | padding: var(--size-xxsmall); 12 | border-radius: 3px; 13 | display: flex; 14 | align-items: center; 15 | position: relative; 16 | background: var(--figma-color-bg-brand-tertiary); 17 | color: var(--figma-color-text); 18 | &.is-hidden { 19 | display: none; 20 | } 21 | a { 22 | color: var(--figma-color-text-brand); 23 | &:hover { 24 | text-decoration: none; 25 | } 26 | &.subtle { 27 | text-decoration: none; 28 | &:hover { 29 | text-decoration: underline; 30 | } 31 | } 32 | } 33 | .icon { 34 | margin-right: var(--size-xxsmall); 35 | margin-left: var(--size-xxxsmall); 36 | } 37 | ` 38 | 39 | interface VersionNoticeProps { 40 | versionDifference?: versionDifference; 41 | } 42 | 43 | export const VersionNotice = ({ versionDifference }: VersionNoticeProps) => { 44 | if (versionDifference !== 'major' && versionDifference !== 'minor') { 45 | return <> 46 | } 47 | return ( 48 |
49 |
🎉
50 |
51 | The{' '} 52 | 58 | Design Token plugin 59 | {' '} 60 | was updated. 61 |
62 | Find out about changes & new features in the{' '} 63 | 68 | release notes → 69 | 70 |
71 |
72 | ) 73 | } 74 | -------------------------------------------------------------------------------- /src/ui/components/WebLink.tsx: -------------------------------------------------------------------------------- 1 | import { commands } from '@config/commands' 2 | import { css } from '@emotion/css' 3 | import * as React from 'react' 4 | 5 | type WebLinkProps = { 6 | children: any, 7 | href: string 8 | align?: string 9 | } 10 | 11 | const style = css` 12 | display: inline-block; 13 | text-decoration: underline; 14 | font-size: var(--font-size-xsmall); 15 | font-weight: var(--font-weight-medium); 16 | padding: 3px 5px; 17 | &:hover { 18 | cursor: pointer; 19 | text-decoration: none; 20 | color: var(--figma-color-text-brand); 21 | } 22 | ` 23 | const clickWebLink = (href) => { 24 | window.open(href) 25 | parent.postMessage({ 26 | pluginMessage: { 27 | command: commands.closePlugin 28 | } 29 | }, '*') 30 | } 31 | 32 | export const WebLink = ({ children, href, align }: WebLinkProps) => { 33 | return
clickWebLink(href)}>{children}
34 | } 35 | -------------------------------------------------------------------------------- /src/ui/context.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | export const FigmaContext = React.createContext(null) 4 | export const SettingsContext = React.createContext(null) 5 | export const TokenContext = React.createContext(null) 6 | -------------------------------------------------------------------------------- /src/ui/css/ui.css: -------------------------------------------------------------------------------- 1 | /* FONTS */ 2 | @font-face { 3 | font-family: 'Inter'; 4 | font-weight: 400; 5 | font-style: normal; 6 | src: url('https://rsms.me/inter/font-files/Inter-Regular.woff2?v=3.7') 7 | format('woff2'), 8 | url('https://rsms.me/inter/font-files/Inter-Regular.woff?v=3.7') 9 | format('woff'); 10 | } 11 | 12 | @font-face { 13 | font-family: 'Inter'; 14 | font-weight: 500; 15 | font-style: normal; 16 | src: url('https://rsms.me/inter/font-files/Inter-Medium.woff2?v=3.7') 17 | format('woff2'), 18 | url('https://rsms.me/inter/font-files/Inter-Medium.woff2?v=3.7') 19 | format('woff'); 20 | } 21 | @font-face { 22 | font-family: 'Inter'; 23 | font-weight: 600; 24 | font-style: normal; 25 | src: url('https://rsms.me/inter/font-files/Inter-SemiBold.woff2?v=3.7') 26 | format('woff2'), 27 | url('https://rsms.me/inter/font-files/Inter-SemiBold.woff2?v=3.7') 28 | format('woff'); 29 | } 30 | 31 | /* figma plugin ds overwrite */ 32 | .switch__label { 33 | padding-right: 0 !important; 34 | } 35 | .switch__toggle:focus-visible + .switch__label:before { 36 | box-shadow: 0 0 0 2px var(--figma-color-border-brand); 37 | } 38 | 39 | /* Normal css */ 40 | * { 41 | box-sizing: border-box; 42 | } 43 | h3 { 44 | padding: 0 var(--size-xxsmall); 45 | margin: var(--size-xxsmall) 0 0; 46 | font-size: var(--font-size-small); 47 | letter-spacing: var(--font-letter-spacing-pos-small); 48 | line-height: var(--line-height); 49 | height: var(--size-medium); 50 | width: 100%; 51 | display: flex; 52 | align-items: center; 53 | } 54 | h3:first-child { 55 | margin-top: 8px; 56 | } 57 | .message-box { 58 | padding: 0 var(--size-xxxsmall) var(--size-xxsmall) var(--size-xxsmall); 59 | } 60 | .message-box .message { 61 | font-family: var(--font-stack); 62 | font-size: var(--font-size-xsmall); 63 | font-weight: var(--font-weight-normal); 64 | letter-spacing: var(--font-letter-spacing-pos-xsmall); 65 | line-height: var(--font-line-height); 66 | } 67 | .flex-horizontal { 68 | display: flex; 69 | } 70 | .flex-half { 71 | flex: 0; 72 | flex-basis: 50%; 73 | } 74 | .flex-horizontal ~ .flex-horizontal { 75 | margin-top: var(--size-xxsmall); 76 | } 77 | .flex-horizontal .label { 78 | width: auto; 79 | flex-shrink: 0; 80 | align-items: flex-start; 81 | padding-top: var(--size-xxsmall); 82 | } 83 | .label.label--info { 84 | color: var(--figma-color-text-secondary); 85 | flex-shrink: 1; 86 | height: auto; 87 | } 88 | .flex-horizontal input[type="text"] { 89 | min-width: 50px; 90 | } 91 | .inside-label-behind--sm { 92 | position: absolute; 93 | right: var(--size-xxsmall); 94 | } 95 | .with-inside-label-behind-sm { 96 | padding-right: 50px; 97 | } 98 | :not(h3) + .section-title { 99 | padding-top: var(--size-xxsmall); 100 | margin-top: var(--size-xsmall); 101 | border-top: 1px solid var(--figma-color-border); 102 | } 103 | /* CSS */ 104 | body { 105 | position: relative; 106 | box-sizing: border-box; 107 | font-family: 'Inter', sans-serif; 108 | margin: 0; 109 | padding: 0; 110 | background-color: var(--figma-color-bg); 111 | color: var(--figma-color-text); 112 | } 113 | -------------------------------------------------------------------------------- /src/ui/css/variables.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --ui-contrast: #222222; 3 | --on--ui-contrast: #ffffff; 4 | --on--ui-contrast--subtle: rgba(255, 255, 255, 0.4); 5 | /* Font stack */ 6 | --font-stack: 'Inter', sans-serif; 7 | 8 | /* Font sizes */ 9 | --font-size-xsmall: 11px; 10 | --font-size-small: 12px; 11 | --font-size-large: 13px; 12 | --font-size-xlarge: 14px; 13 | /* Font weights */ 14 | --font-weight-normal: 400; 15 | --font-weight-medium: 500; 16 | --font-weight-bold: 600; 17 | /* Lineheight */ 18 | --font-line-height: 16px; /* Use For xsmall, small font sizes */ 19 | --font-line-height-large: 24px; /* Use For large, xlarge font sizes */ 20 | /* Letterspacing */ 21 | --font-letter-spacing-pos-xsmall: 0.005em; 22 | --font-letter-spacing-neg-xsmall: 0.01em; 23 | --font-letter-spacing-pos-small: 0; 24 | --font-letter-spacing-neg-small: 0.005em; 25 | --font-letter-spacing-pos-large: -0.0025em; 26 | --font-letter-spacing-pos-xlarge: -0.001em; 27 | 28 | /* BORDER RADIUS */ 29 | --border-radius-small: 2px; 30 | --border-radius-large: 6px; 31 | /* SHADOWS */ 32 | --shadow-hud: 0 5px 17px rgba(0, 0, 0, 0.2), 0 2px 7px rgba(0, 0, 0, 0.15); 33 | /* SPACING + SIZING */ 34 | --size-xxxsmall: 4px; 35 | --size-xxsmall: 8px; 36 | --size-xsmall: 16px; 37 | --size-small: 24px; 38 | --size-medium: 32px; 39 | --size-large: 40px; 40 | --size-xlarge: 48px; 41 | --size-xxlarge: 64px; 42 | --size-xxxlarge: 80px; 43 | 44 | } -------------------------------------------------------------------------------- /src/ui/modules/downloadJson.ts: -------------------------------------------------------------------------------- 1 | import { commands } from '@config/commands' 2 | import { PluginMessage } from '@typings/pluginEvent' 3 | 4 | export const downloadJson = (parent, link: HTMLLinkElement, json: string) => { 5 | // if no tokens are present 6 | if (json === '[]') { 7 | parent.postMessage({ 8 | pluginMessage: { 9 | // command: commands.closePlugin, 10 | payload: { 11 | notification: '⛔️ No design token detected!' 12 | } 13 | } as PluginMessage 14 | }, '*') 15 | // abort 16 | return 17 | } 18 | // try to export tokens 19 | try { 20 | const blob = new Blob([json], { type: 'application/json' }) 21 | const url = URL.createObjectURL(blob) 22 | link.href = url 23 | // Programmatically trigger a click on the anchor element 24 | link.click() 25 | URL.revokeObjectURL(url) 26 | // send success message 27 | parent.postMessage({ 28 | pluginMessage: { 29 | command: commands.closePlugin, 30 | payload: { 31 | notification: '🎉 Design token export successful!' 32 | } 33 | } as PluginMessage 34 | }, '*') 35 | } catch (error) { 36 | // send success message 37 | parent.postMessage({ 38 | pluginMessage: { 39 | // command: commands.closePlugin, 40 | payload: { 41 | notification: '⛔️ Design token failed!' 42 | } 43 | } as PluginMessage 44 | }, '*') 45 | // log error 46 | console.error('Export error: ', error) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/ui/modules/handleKeyboardInput.ts: -------------------------------------------------------------------------------- 1 | import { commands } from '@config/commands' 2 | import { KeyboardEvent } from 'react' 3 | 4 | const getKeyboardFocusableElements = (element = document): HTMLElement[] => { 5 | // @ts-ignore 6 | return [...element.querySelectorAll( 7 | 'a, button, input, textarea, select, details,[tabindex]:not([tabindex="-1"])' 8 | )] 9 | .filter(el => !el.hasAttribute('disabled')) 10 | } 11 | 12 | const trapFocus = (event) => { 13 | const focusableElements = getKeyboardFocusableElements() 14 | const lastFocusableEl = focusableElements[focusableElements.length - 1] 15 | /* shift + tab */ 16 | if (event.shiftKey && document.activeElement === focusableElements[0]) { 17 | lastFocusableEl.focus() 18 | event.preventDefault() 19 | } 20 | /* tab */ 21 | if (!event.shiftKey && document.activeElement === lastFocusableEl) { 22 | focusableElements[0].focus() 23 | event.preventDefault() 24 | } 25 | } 26 | 27 | export const handleKeyboardInput = (event: KeyboardEvent, figmaUIApi) => { 28 | // close dialog on escape 29 | if (event.code === 'Escape') { 30 | // abort on select 31 | if (document.activeElement.classList.contains('select-menu__button--active')) { 32 | return 33 | } 34 | // close window 35 | figmaUIApi.postMessage({ 36 | pluginMessage: { 37 | command: commands.closePlugin 38 | } 39 | // @ts-ignore 40 | }, '*') 41 | } 42 | // capture focus 43 | if (event.code === 'Tab') { 44 | trapFocus(event) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/ui/ui.html: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /src/ui/ui.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import 'figma-plugin-ds/dist/figma-plugin-ds.css' 4 | import './css/variables.css' 5 | import './css/ui.css' 6 | import { GeneralSettings } from '@components/GeneralSettings' 7 | import { useState } from 'react' 8 | import { FigmaContext, SettingsContext, TokenContext } from '@ui/context' 9 | import { useImmer } from 'use-immer' 10 | import { VersionNotice } from '@components/VersionNotice' 11 | import { css } from '@emotion/css' 12 | import { defaultSettings } from '@config/defaultSettings' 13 | import { handleKeyboardInput } from './modules/handleKeyboardInput' 14 | import { commands, PluginCommands } from '@config/commands' 15 | import { PluginEvent } from '@typings/pluginEvent' 16 | import { FileExportSettings } from '@components/FileExportSettings' 17 | import { UrlExportSettings } from '@components/UrlExportSettings' 18 | // --------------------------------- 19 | // @ts-ignore 20 | const figmaUIApi: UIAPI = parent as UIAPI 21 | 22 | const style = css` 23 | padding: 8px var(--size-xxsmall) 0; 24 | margin-bottom: 0; 25 | form { 26 | margin-bottom: 0; 27 | } 28 | ` 29 | 30 | const PluginUi = () => { 31 | const [versionDifference, setVersionDifference] = useState(null) 32 | const [activePage, setActivePage] = useState(null) 33 | const [tokens, setTokens] = useState(null) 34 | const [figmaMetaData, setFigmaMetaData] = useState(null) 35 | const [settings, updateSettings] = useImmer(defaultSettings) 36 | 37 | // listen to messages 38 | // eslint-disable-next-line 39 | onmessage = (event: PluginEvent) => { 40 | // capture message 41 | const { command, payload } = event.data.pluginMessage || {} as {command: PluginCommands, payload: any} 42 | // set settings 43 | if ([commands.urlExport, commands.export, commands.generalSettings].includes(command)) { 44 | updateSettings({ 45 | ...payload.settings, 46 | filename: payload.settings.filename || payload.metadata.filename 47 | }) 48 | setVersionDifference(payload.versionDifference) 49 | setFigmaMetaData(payload.metadata) 50 | setTokens(payload.data) 51 | // activate page 52 | setActivePage(command) 53 | } 54 | // open url 55 | if ([commands.help, commands.demo, commands.openUrl].includes(command)) { 56 | window.open(payload.url) 57 | parent.postMessage({ 58 | pluginMessage: { 59 | command: commands.closePlugin 60 | } 61 | }, '*') 62 | } 63 | } 64 | 65 | return ( 66 | 67 | 68 | 69 |
handleKeyboardInput(e, figmaUIApi)}> 70 | 71 | {activePage === commands.generalSettings && } 72 | {activePage === commands.export && } 73 | {activePage === commands.urlExport && } 74 |
75 |
76 |
77 |
78 | ) 79 | } 80 | 81 | const root = createRoot(document.getElementById('pluginUI')) // createRoot(container!) if you use TypeScript 82 | root.render() 83 | -------------------------------------------------------------------------------- /src/utilities/accessToken.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @name getAccessToken 3 | * @description returns the access token for the current file or undefined 4 | * @param fileId {string} — ID of the current file 5 | */ 6 | const getAccessToken = async (fileId: string): Promise => { 7 | // get all access tokens 8 | const accessTokens = await figma.clientStorage.getAsync('accessTokens') 9 | // if access tokens object is present 10 | if (accessTokens !== undefined && accessTokens instanceof Object) { 11 | // retrieve the access token from the cache 12 | const accessToken = accessTokens[fileId] 13 | // return the access token or an empty string 14 | return accessToken || '' 15 | } 16 | // return empty string if no token is stored 17 | return '' 18 | } 19 | /** 20 | * @name setAccessToken 21 | * @description store the access token for the current given file in the user clientStorage 22 | * @param fileId {string} — ID of the current file 23 | * @param fileId {string} — access token 24 | */ 25 | /* istanbul ignore next */ 26 | const setAccessToken = async (fileId: string, accessToken: string) => { 27 | // get the access token object 28 | const accessTokens = (await figma.clientStorage.getAsync('accessTokens')) || {} 29 | // merge tokens 30 | const mergedTokens = { 31 | ...accessTokens, 32 | ...{ [fileId]: accessToken } 33 | } 34 | // merge the new token into the object 35 | return await figma.clientStorage.setAsync('accessTokens', mergedTokens) 36 | } 37 | 38 | export { getAccessToken, setAccessToken } 39 | -------------------------------------------------------------------------------- /src/utilities/base64.ts: -------------------------------------------------------------------------------- 1 | const utf8ToBase64 = (text: string): string => { 2 | const utf8EncodedBytes = new TextEncoder().encode(text) 3 | const binString = Array.from(utf8EncodedBytes, (byte) => 4 | String.fromCodePoint(byte) 5 | ).join('') 6 | return btoa(binString) 7 | } 8 | 9 | export { utf8ToBase64 } 10 | -------------------------------------------------------------------------------- /src/utilities/buildFigmaData.ts: -------------------------------------------------------------------------------- 1 | import { figmaDataType } from '@typings/figmaDataType' 2 | import filterByPropertyName from '@utils/filterByNameProperty' 3 | import getPaintStyles from '@utils/getPaintStyles' 4 | import getGridStyles from '@utils/getGridStyles' 5 | import getTokenNodes from '@utils/getTokenNodes' 6 | import getTextStyles from '@utils/getTextStyles' 7 | import getEffectStyles from '@utils/getEffectStyles' 8 | import { Settings } from '@typings/settings' 9 | 10 | /** 11 | * @function buildFigmaData – return an object with all styles & frame to use for export 12 | * @param {PluginAPI} figma — the figma PluginAPI object 13 | * @param options – options object 14 | */ 15 | const buildFigmaData = async ( 16 | figma: PluginAPI, 17 | settings: Settings 18 | ): Promise => { 19 | // use spread operator because the original is readOnly 20 | const tokenFrames = await getTokenNodes([...figma.root.children]) 21 | // get user exclusion prefixes 22 | const userExclusionPrefixes = settings.exclusionPrefix 23 | .split(',') 24 | .map((item) => item.replace(/\s+/g, '')) 25 | // get data from figma 26 | return { 27 | tokenFrames: tokenFrames, 28 | paintStyles: getPaintStyles(await figma.getLocalPaintStylesAsync()).filter( 29 | (item) => filterByPropertyName(item, userExclusionPrefixes) 30 | ), 31 | gridStyles: getGridStyles(await figma.getLocalGridStylesAsync()).filter( 32 | (item) => filterByPropertyName(item, userExclusionPrefixes) 33 | ), 34 | textStyles: getTextStyles(await figma.getLocalTextStylesAsync()).filter( 35 | (item) => filterByPropertyName(item, userExclusionPrefixes) 36 | ), 37 | effectStyles: getEffectStyles( 38 | await figma.getLocalEffectStylesAsync() 39 | ).filter((item) => filterByPropertyName(item, userExclusionPrefixes)) 40 | } 41 | } 42 | 43 | export default buildFigmaData 44 | -------------------------------------------------------------------------------- /src/utilities/changeNotation.ts: -------------------------------------------------------------------------------- 1 | export const changeNotation = (name, currentDelimiter = '/', desiredDelimiter = '.') => { 2 | return name.split(currentDelimiter).join(desiredDelimiter).toLowerCase() 3 | } 4 | -------------------------------------------------------------------------------- /src/utilities/convertColor.ts: -------------------------------------------------------------------------------- 1 | import { ColorRgba } from '@typings/valueTypes' 2 | import roundWithDecimals from './roundWithDecimals' 3 | import { tinycolor } from '@ctrl/tinycolor' 4 | 5 | export const roundRgba = (rgba: { 6 | r: number, 7 | g: number, 8 | b: number, 9 | a?: number, 10 | }, opacity?: number): ColorRgba => ({ 11 | r: roundWithDecimals(rgba.r * 255, 0), 12 | g: roundWithDecimals(rgba.g * 255, 0), 13 | b: roundWithDecimals(rgba.b * 255, 0), 14 | a: roundWithDecimals(opacity ?? rgba.a ?? 1) 15 | }) 16 | 17 | export const convertPaintToRgba = (paint): ColorRgba => { 18 | if (paint.type === 'SOLID' && paint.visible === true) { 19 | return roundRgba(paint.color, paint.opacity) 20 | } 21 | return null 22 | } 23 | 24 | export const convertRgbaObjectToString = (rgbaObject: ColorRgba): string => `rgba(${rgbaObject.r}, ${rgbaObject.g}, ${rgbaObject.b}, ${rgbaObject.a})` 25 | 26 | export const rgbaObjectToHex8 = (rgbaObject: ColorRgba): string => { 27 | // return value 28 | return tinycolor(convertRgbaObjectToString(rgbaObject)).toHex8String() 29 | } 30 | -------------------------------------------------------------------------------- /src/utilities/deepMerge.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Performs a deep merge of `source` into `target`. 3 | * Mutates `target` only but not its objects and arrays. 4 | * 5 | * @author inspired by [jhildenbiddle](https://stackoverflow.com/a/48218209). 6 | */ 7 | const deepMerge = (target, source) => { 8 | // function to test if a variable is an object 9 | const isObject = (obj) => obj && typeof obj === 'object' 10 | // make sure both the target and the source are objects 11 | // otherwise return source 12 | if (!isObject(target) || !isObject(source)) { 13 | return source 14 | } 15 | // iterate over source 16 | Object.keys(source).forEach(key => { 17 | // get values from both target and source for the given key 18 | const targetValue = target[key] 19 | const sourceValue = source[key] 20 | // merge both values 21 | if (Array.isArray(targetValue) && Array.isArray(sourceValue)) { 22 | target[key] = [...new Set(targetValue.concat(sourceValue))] 23 | } else if (isObject(targetValue) && isObject(sourceValue)) { 24 | target[key] = deepMerge(Object.assign({}, targetValue), sourceValue) 25 | } else { 26 | target[key] = sourceValue 27 | } 28 | }) 29 | // return merge object 30 | return target 31 | } 32 | 33 | export default deepMerge 34 | -------------------------------------------------------------------------------- /src/utilities/extractTokenNodeValues.ts: -------------------------------------------------------------------------------- 1 | import { customTokenNode } from '@typings/tokenNodeTypes' 2 | import { ColorRgba } from '@typings/valueTypes' 3 | import { convertPaintToRgba } from './convertColor' 4 | /** 5 | * Return an array of solid stroke colors 6 | */ 7 | const getSolidStrokes = (paints: readonly Paint[]): ColorRgba[] => { 8 | // clone without reference 9 | return [...paints] 10 | .map(paint => convertPaintToRgba(paint)) 11 | .filter(paint => paint != null) 12 | } 13 | /** 14 | * extractTokenNodeValues 15 | * @param node: SceneNode 16 | * @returns node object 17 | */ 18 | const extractTokenNodeValues = (node: ComponentNode | RectangleNode | FrameNode): customTokenNode => ({ 19 | name: node.name, 20 | // @ts-ignore 21 | description: node.description || undefined, 22 | bottomLeftRadius: node.bottomLeftRadius, 23 | bottomRightRadius: node.bottomRightRadius, 24 | topLeftRadius: node.topLeftRadius, 25 | topRightRadius: node.topRightRadius, 26 | cornerRadius: node.cornerRadius || undefined, 27 | cornerSmoothing: node.cornerSmoothing, 28 | strokes: getSolidStrokes(node.strokes), 29 | strokeWeight: node.strokeWeight as number, 30 | strokeStyleId: node.strokeStyleId, 31 | strokeMiterLimit: node.strokeMiterLimit, 32 | strokeJoin: node.strokeJoin, 33 | strokeCap: node.strokeCap, 34 | dashPattern: node.dashPattern, 35 | strokeAlign: node.strokeAlign, 36 | width: node.width, 37 | height: node.height, 38 | reactions: node.reactions || undefined, 39 | // @ts-ignore 40 | paddingTop: node.paddingTop || 0, 41 | // @ts-ignore 42 | paddingRight: node.paddingRight || 0, 43 | // @ts-ignore 44 | paddingBottom: node.paddingBottom || 0, 45 | // @ts-ignore 46 | paddingLeft: node.paddingLeft || 0, 47 | opacity: node.opacity ?? 1 48 | }) 49 | 50 | export default extractTokenNodeValues 51 | -------------------------------------------------------------------------------- /src/utilities/filterByNameProperty.ts: -------------------------------------------------------------------------------- 1 | import config from '@config/config' 2 | 3 | type objectWithNameProperty = { 4 | name: string, 5 | [key: string]: any 6 | } 7 | 8 | const exclusionPrefix = (exclusionPrefixStrings: string[]): string[] => { 9 | return [ 10 | ...config.exclusionPrefixDefault, 11 | ...exclusionPrefixStrings 12 | ] 13 | } 14 | 15 | const filterByPropertyName = (object: objectWithNameProperty, exclusionPrefixStrings: string[]) => !exclusionPrefix(exclusionPrefixStrings).includes(object.name.trim().substr(0, 1)) 16 | 17 | export default filterByPropertyName 18 | -------------------------------------------------------------------------------- /src/utilities/getEffectStyles.ts: -------------------------------------------------------------------------------- 1 | import { EffectStyleObject } from '@typings/styles' 2 | 3 | /** 4 | * @function getEffectStyles 5 | * @param {Array} styles – the effectStyle from the figma file 6 | */ 7 | const getEffectStyles = (styles: EffectStyle[]): EffectStyleObject[] => { 8 | // init styleArray 9 | const styleArray = [] 10 | // loop through Figma styles and add to array 11 | styles.forEach(style => { 12 | styleArray.push({ 13 | name: style.name, 14 | id: style.id, 15 | description: style.description, 16 | effects: style.effects 17 | }) 18 | }) 19 | // return array 20 | return styleArray 21 | } 22 | 23 | export default getEffectStyles 24 | -------------------------------------------------------------------------------- /src/utilities/getFileId.ts: -------------------------------------------------------------------------------- 1 | import config from '@config/config' 2 | 3 | const getFileId = (figma: PluginAPI): string => { 4 | let fileId = figma.root.getPluginData(config.key.fileId) 5 | // set plugin id if it does not exist 6 | if (fileId === undefined || fileId === '') { 7 | figma.root.setPluginData(config.key.fileId, figma.root.name + ' ' + Math.floor(Math.random() * 1000000000)) 8 | // grab file ID 9 | fileId = figma.root.getPluginData(config.key.fileId) 10 | } 11 | return fileId 12 | } 13 | 14 | export default getFileId 15 | -------------------------------------------------------------------------------- /src/utilities/getGridStyles.ts: -------------------------------------------------------------------------------- 1 | import { GridStyleObject } from '@typings/styles' 2 | 3 | /** 4 | * @function getGridStyles 5 | * @param {Array} gridStyles – the gridStyles from the figma file 6 | */ 7 | const getGridStyles = (styles: GridStyle[]): GridStyleObject[] => { 8 | // init styleArray 9 | const styleArray = [] 10 | // loop through Figma styles and add to array 11 | styles.forEach(style => { 12 | styleArray.push({ 13 | name: style.name, 14 | id: style.id, 15 | description: style.description, 16 | layoutGrids: style.layoutGrids 17 | }) 18 | }) 19 | // return array 20 | return styleArray 21 | } 22 | 23 | export default getGridStyles 24 | -------------------------------------------------------------------------------- /src/utilities/getPaintStyles.ts: -------------------------------------------------------------------------------- 1 | import { PaintStyleObject } from '@typings/styles' 2 | 3 | /** 4 | * @function getPaintStyles 5 | * @param {Array} paintStyles – the paintStyles from the figma file (somehow still connected) 6 | */ 7 | const getPaintStyles = (styles: PaintStyle[]): PaintStyleObject[] => { 8 | // init styleArray 9 | const styleArray = [] 10 | // loop through Figma styles and add to array 11 | styles.forEach(style => { 12 | styleArray.push({ 13 | name: style.name, 14 | id: style.id, 15 | description: style.description, 16 | paints: style.paints 17 | }) 18 | }) 19 | // return array 20 | return styleArray 21 | } 22 | 23 | export default getPaintStyles 24 | -------------------------------------------------------------------------------- /src/utilities/getTextStyles.ts: -------------------------------------------------------------------------------- 1 | import { TextStyleObject } from '@typings/styles' 2 | 3 | /** 4 | * @function getTextStyles 5 | * @param {Array} styles – the paintStyles from the figma file (somehow still connected) 6 | */ 7 | const getTextStyles = (styles: TextStyle[]): TextStyleObject[] => { 8 | // init styleArray 9 | const styleArray = [] 10 | // loop through Figma styles and add to array 11 | styles.forEach(style => { 12 | styleArray.push({ 13 | name: style.name, 14 | id: style.id, 15 | description: style.description, 16 | fontSize: style.fontSize, 17 | textDecoration: style.textDecoration, 18 | fontName: style.fontName, 19 | letterSpacing: style.letterSpacing, 20 | lineHeight: style.lineHeight, 21 | paragraphIndent: style.paragraphIndent, 22 | paragraphSpacing: style.paragraphSpacing, 23 | textCase: style.textCase 24 | }) 25 | }) 26 | // return array 27 | return styleArray 28 | } 29 | 30 | export default getTextStyles 31 | -------------------------------------------------------------------------------- /src/utilities/getTokenJson.ts: -------------------------------------------------------------------------------- 1 | import extractColors from '../extractor/extractColors' 2 | import extractGrids from '../extractor/extractGrids' 3 | import extractFonts from '../extractor/extractFonts' 4 | import extractEffects from '../extractor/extractEffects' 5 | import extractMotion from '../extractor/extractMotion' 6 | import extractSizes from '../extractor/extractSizes' 7 | import extractSpacing from '../extractor/extractSpacing' 8 | import extractBorders from '../extractor/extractBorders' 9 | import extractRadii from '../extractor/extractRadii' 10 | import extractBreakpoints from '../extractor/extractBreakpoints' 11 | import extractOpacities from '../extractor/extractOpacities' 12 | import { figmaDataType } from '@typings/figmaDataType' 13 | import buildFigmaData from './buildFigmaData' 14 | import { Settings } from '@typings/settings' 15 | import { getVariables } from './getVariables' 16 | 17 | const getPrefixArray = (prefixString = '') => prefixString.split(',').map(item => item.replace(/\s+/g, '')) 18 | 19 | export const exportRawTokenArray = async (figma: PluginAPI, settings: Settings) => { 20 | const figmaData: figmaDataType = await buildFigmaData(figma, settings) 21 | // get tokens 22 | return [ 23 | ...extractSizes(figmaData.tokenFrames, getPrefixArray(settings.prefix.size)), 24 | ...extractBreakpoints(figmaData.tokenFrames, getPrefixArray(settings.prefix.breakpoint)), 25 | ...extractSpacing(figmaData.tokenFrames, getPrefixArray(settings.prefix.spacing)), 26 | ...extractBorders(figmaData.tokenFrames, getPrefixArray(settings.prefix.border)), 27 | ...extractRadii(figmaData.tokenFrames, getPrefixArray(settings.prefix.radius)), 28 | ...extractMotion(figmaData.tokenFrames, getPrefixArray(settings.prefix.motion)), 29 | ...extractOpacities(figmaData.tokenFrames, getPrefixArray(settings.prefix.opacity)), 30 | ...extractColors(figmaData.paintStyles, { color: getPrefixArray(settings.prefix.color), gradient: getPrefixArray(settings.prefix.gradient), alias: getPrefixArray(settings.alias) }), 31 | ...extractGrids(figmaData.gridStyles, getPrefixArray(settings.prefix.grid)), 32 | ...extractFonts(figmaData.textStyles, getPrefixArray(settings.prefix.font)), 33 | ...extractEffects(figmaData.effectStyles, getPrefixArray(settings.prefix.effect)), 34 | ...(await getVariables(figma, settings)) 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /src/utilities/getTokenNodes.ts: -------------------------------------------------------------------------------- 1 | import { customTokenNode } from '@typings/tokenNodeTypes' 2 | import extractTokenNodeValues from '@utils/extractTokenNodeValues' 3 | import isTokenNode from '@utils/isTokenNode' 4 | 5 | // the name that token frames have 6 | const tokenFrameName = '_tokens' 7 | 8 | // check if a frame is a _token frame 9 | const isTokenFrame = (node): boolean => node.type === 'FRAME' && node.name.trim().toLowerCase().substr(0, tokenFrameName.length) === tokenFrameName 10 | 11 | // return only nodes that are frames 12 | const getFrameNodes = (nodes): FrameNode[] => [...nodes.map(page => page.findChildren(node => isTokenFrame(node))).reduce((flatten, arr) => [...flatten, ...arr])] 13 | /** 14 | * getVariantName 15 | * creates the variant name of the parent and child name 16 | */ 17 | const getVariantName = (parentName: string, childName: string): string => { 18 | // split into array 19 | childName = childName.split(',') 20 | // remove hidden names 21 | .filter(part => !['_', '.'].includes(part.trim().substr(0, 1))) 22 | // cleanup names, only return value part 23 | .map(part => part.split('=')[1]) 24 | // combine 25 | .join('/') 26 | 27 | // return full name 28 | return `${parentName}/${childName}` 29 | } 30 | /** 31 | * Returns all frames from the file that have a name that starts with _tokens or the user defined token specifier 32 | * 33 | * @param pages PageNodes 34 | */ 35 | const getTokenNodes = async (pages: PageNode[]): Promise => { 36 | await figma.loadAllPagesAsync() 37 | // get token frames 38 | const tokenFrames = getFrameNodes(pages) 39 | // get all children of token frames 40 | return tokenFrames.map(frame => frame 41 | // check if children are of valid types 42 | .findAll( 43 | /* istanbul ignore next */ 44 | node => isTokenNode(node) 45 | )) 46 | // merges all children into one array 47 | .reduce((flatten, arr) => [...flatten, ...arr], []) 48 | // unpack variants & warn about deprecated types 49 | .map((item): customTokenNode[] => { 50 | if (item.type === 'RECTANGLE' || item.type === 'FRAME') { 51 | console.warn('Please use only main components and variants, other types may be deprecated as tokens in the future') 52 | } 53 | // unpack variants 54 | if (item.type === 'COMPONENT_SET') { 55 | // TODO: Name is overwriting real object in figma 56 | // -> create clone and move to new array to return 57 | return item.children.map((child: ComponentNode) => ({ 58 | ...extractTokenNodeValues(child), 59 | ...{ name: getVariantName(item.name, child.name) } 60 | })) 61 | } 62 | // return normal item as array to unpack later 63 | // @ts-ignore 64 | return [extractTokenNodeValues(item)] 65 | }) 66 | // merges the variant children into one array 67 | .reduce((flatten, arr) => [...flatten, ...arr], []) 68 | } 69 | 70 | export default getTokenNodes 71 | export const __testing = { 72 | isTokenNode: isTokenNode, 73 | isTokenFrame: isTokenFrame 74 | } 75 | -------------------------------------------------------------------------------- /src/utilities/getVariableTypeByValue.ts: -------------------------------------------------------------------------------- 1 | export const getVariableTypeByValue = (value: string | number | boolean | object) => { 2 | if (typeof value === 'boolean') return 'string' 3 | if (typeof value === 'number') return 'dimension' 4 | if (typeof value === 'object') return 'color' 5 | if (typeof value === 'string') return 'string' 6 | } 7 | -------------------------------------------------------------------------------- /src/utilities/getVersionDifference.ts: -------------------------------------------------------------------------------- 1 | import semVerDifference, { versionDifference } from './semVerDifference' 2 | import currentVersion from './version' 3 | import config from '@config/config' 4 | 5 | const getVersionDifference = async (figma: PluginAPI): Promise => { 6 | // get version & version difference 7 | const lastVersionSettingsOpened = await figma.clientStorage.getAsync(config.key.lastVersionSettingsOpened) 8 | const versionDifference = semVerDifference(currentVersion, lastVersionSettingsOpened) 9 | // update version 10 | if (!lastVersionSettingsOpened || lastVersionSettingsOpened !== currentVersion) { 11 | await figma.clientStorage.setAsync(config.key.lastVersionSettingsOpened, currentVersion) 12 | } 13 | // return version Difference 14 | return versionDifference 15 | } 16 | 17 | export default getVersionDifference 18 | -------------------------------------------------------------------------------- /src/utilities/groupByName.ts: -------------------------------------------------------------------------------- 1 | import deepMerge from './deepMerge' 2 | import transformName from '@utils/transformName' 3 | import { Settings } from '@typings/settings' 4 | import { OriginalFormatTokenInterface } from '@typings/originalFormatProperties' 5 | import { StandardTokenInterface } from '@typings/standardToken' 6 | // create a nested object structure from the array (['style','colors','main','red']) 7 | const nestedObjectFromArray = (array: string[], value: any) => { 8 | // reducer 9 | const reducer = (val, key) => ({ [key]: val }) 10 | // return reduced array 11 | return array.reduceRight(reducer, value) 12 | } 13 | 14 | export const groupByKeyAndName = (tokenArray: OriginalFormatTokenInterface[] | StandardTokenInterface[], userSettings: Settings) => { 15 | const removeName = true 16 | // guard 17 | if (tokenArray.length <= 0) return [] 18 | // nest tokens into object with hierarchy defined by name using / 19 | const groupedTokens = tokenArray.map(token => { 20 | // split token name into array 21 | // remove leading and following whitespace for every item 22 | // transform items to lowerCase 23 | const groupsFromName = token.name.split('/').map(group => transformName(group, userSettings.nameConversion)) 24 | // remove name if not otherwise specified 25 | if (removeName === true) { 26 | delete token.name 27 | } 28 | // return 29 | return nestedObjectFromArray(groupsFromName, token) 30 | }) 31 | // return merged object of tokens grouped by name hierarchy 32 | return groupedTokens.reduce((accumulator = {}, currentValue) => deepMerge(accumulator, currentValue)) 33 | } 34 | -------------------------------------------------------------------------------- /src/utilities/handleVariableAlias.ts: -------------------------------------------------------------------------------- 1 | import { tokenExportKeyType } from '@typings/tokenExportKey' 2 | import { tokenTypes } from '@config/tokenTypes' 3 | 4 | import { getVariableTypeByValue } from '@utils/getVariableTypeByValue' 5 | import { changeNotation } from '@utils/changeNotation' 6 | 7 | async function handleVariableAlias ( 8 | variable: Variable & { aliasSameMode?: boolean }, 9 | value: { id: string }, 10 | mode: { modeId: string; name: string }, 11 | aliasSameMode = false 12 | ) { 13 | const resolvedAlias = await figma.variables.getVariableByIdAsync(value.id) 14 | const collection = await figma.variables.getVariableCollectionByIdAsync( 15 | resolvedAlias.variableCollectionId 16 | ) 17 | return { 18 | description: variable.description || '', 19 | exportKey: tokenTypes.variables.key as tokenExportKeyType, 20 | category: getVariableTypeByValue( 21 | Object.values(resolvedAlias.valuesByMode)[0] 22 | ), 23 | values: `{${collection.name.toLowerCase()}.${changeNotation( 24 | resolvedAlias.name, 25 | '/', 26 | '.' 27 | )}}`, 28 | 29 | // this is being stored so we can properly update the design tokens later to account for all 30 | // modes when using aliases 31 | aliasCollectionName: collection.name.toLowerCase(), 32 | aliasMode: mode, 33 | aliasSameMode: variable.aliasSameMode || aliasSameMode 34 | } 35 | } 36 | 37 | export default handleVariableAlias 38 | -------------------------------------------------------------------------------- /src/utilities/isTokenNode.ts: -------------------------------------------------------------------------------- 1 | // the node types that can be used for tokens 2 | const tokenNodeTypes = [ 3 | 'COMPONENT', 4 | 'COMPONENT_SET', // => variant 5 | 'RECTANGLE', 6 | 'FRAME' 7 | ] 8 | /** 9 | * check if a node is a valid token node type 10 | * Currently: 'COMPONENT', 'FRAME or 'RECTANGLE' 11 | * @param SceneNode node 12 | */ 13 | const isTokenNode = (node: SceneNode): boolean => { 14 | return node.parent.type !== 'COMPONENT_SET' && tokenNodeTypes.includes(node.type) && node.name.length > 0 15 | } 16 | 17 | export default isTokenNode 18 | -------------------------------------------------------------------------------- /src/utilities/prefixTokenName.ts: -------------------------------------------------------------------------------- 1 | import config from '@config/config' 2 | import { OriginalFormatTokenInterface } from '@typings/originalFormatProperties' 3 | import { Settings } from '@typings/settings' 4 | import { StandardTokenInterface } from '@typings/standardToken' 5 | 6 | const getExportKey = (token: OriginalFormatTokenInterface | StandardTokenInterface) => { 7 | // standard token 8 | if (token.extensions?.[config.key.extensionPluginData]?.exportKey !== undefined) { 9 | return token.extensions[config.key.extensionPluginData].exportKey 10 | } 11 | return 'missingExportKey' 12 | } 13 | 14 | export const prefixTokenName = (tokenArray: OriginalFormatTokenInterface[] | StandardTokenInterface[], userSettings: Settings) => { 15 | // guard 16 | if (tokenArray.length <= 0) return [] 17 | // nest tokens into object with hierarchy defined by name using / 18 | return tokenArray.map(token => { 19 | // remove top level prefix from name if desired 20 | if (userSettings.prefixInName === false && token.exportKey !== 'variables') { 21 | token.name = token.name.substr(token.name.indexOf('/') + 1).trim().trimLeft() 22 | } else { 23 | if (token.extensions?.[config.key.extensionPluginData]?.alias !== undefined) { 24 | const prefix = token.name.substr(0, token.name.indexOf('/')).trim().trimLeft() 25 | token.extensions[config.key.extensionPluginData].alias = `${prefix}.${token.extensions[config.key.extensionPluginData].alias}` 26 | } 27 | } 28 | // add key to name if desired 29 | if (userSettings.keyInName) { 30 | token.name = `${getExportKey(token)}/${token.name}` 31 | // add exportKey to token 32 | if (token.extensions?.[config.key.extensionPluginData]?.alias !== undefined) { 33 | token.extensions[config.key.extensionPluginData].alias = `${getExportKey(token)}.${token.extensions[config.key.extensionPluginData].alias 34 | }` 35 | } 36 | } 37 | 38 | return token 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /src/utilities/prepareExport.ts: -------------------------------------------------------------------------------- 1 | import { internalTokenInterface } from '@typings/propertyObject' 2 | import { Settings } from '@typings/settings' 3 | import { transformer as originalFormatTransformer } from '@src/transformer/originalFormatTransformer' 4 | import { transformer as standardTransformer } from '@src/transformer/standardTransformer' 5 | import { groupByKeyAndName } from '@utils/groupByName' 6 | import { tokenTypes } from '@config/tokenTypes' 7 | import { tokenCategoryType } from '@typings/tokenCategory' 8 | import { tokenExportKeyType } from '@typings/tokenExportKey' 9 | import config from '@config/config' 10 | import { prefixTokenName } from '@utils/prefixTokenName' 11 | 12 | const tokenTransformer = { 13 | original: originalFormatTransformer, 14 | standard: standardTransformer, 15 | standardDeprecated: standardTransformer 16 | } 17 | 18 | const createTypographyTokens = (tokens: internalTokenInterface[], settings) => { 19 | if (settings.tokenFormat === 'standard') { 20 | return JSON.parse(JSON.stringify(tokens.filter(item => item.category === tokenTypes.font.key))) 21 | .map(item => { 22 | item.name = 'typography/' + item.name.substr(item.name.indexOf('/') + 1).trim().trimStart() 23 | item.category = tokenTypes.typography.key as tokenCategoryType 24 | item.exportKey = tokenTypes.typography.key as tokenExportKeyType 25 | if (settings.excludeExtensionProp !== true) { 26 | item.extensions[config.key.extensionPluginData] = { 27 | ...item.extensions[config.key.extensionPluginData], 28 | exportKey: tokenTypes.typography.key as tokenCategoryType 29 | } 30 | } 31 | return item 32 | }) 33 | } 34 | return [] 35 | } 36 | 37 | export const prepareExport = (tokens: string, settings: Settings) => { 38 | if (tokens.length === 0) tokens = '[]' 39 | // parse json string 40 | let tokenArray: internalTokenInterface[] = JSON.parse(tokens) 41 | // duplicate font if typography is true && format = standard 42 | tokenArray = [...tokenArray, ...createTypographyTokens(tokenArray, settings)] 43 | // filter by user setting for export keys 44 | const tokensFiltered: internalTokenInterface[] = tokenArray.filter(({ exportKey }) => settings.exports[exportKey]) 45 | // add to name 46 | const prefixedTokens = prefixTokenName(tokensFiltered, settings) 47 | // converted values 48 | const tokensConverted = prefixedTokens.map(token => tokenTransformer[settings.tokenFormat]?.(token, settings)).filter(Boolean) 49 | // group items by their names 50 | // @ts-ignore 51 | const tokensGroupedByName = groupByKeyAndName(tokensConverted, settings) 52 | // return tokens 53 | return tokensGroupedByName 54 | } 55 | -------------------------------------------------------------------------------- /src/utilities/processAliasModes.ts: -------------------------------------------------------------------------------- 1 | const processAliasModes = (variables) => { 2 | return variables.reduce((collector, variable) => { 3 | // only one mode will be passed in if any 4 | if (!variable.aliasMode) { 5 | collector.push(variable) 6 | 7 | return collector 8 | } 9 | 10 | // alias mode singular because only one is shown 11 | const { aliasMode, aliasCollectionName } = variable 12 | 13 | collector.push({ 14 | ...variable, 15 | values: variable.values.replace( 16 | `{${aliasCollectionName}.`, 17 | `{${aliasCollectionName}.${aliasMode.name.toLowerCase()}.` 18 | ) 19 | }) 20 | 21 | return collector 22 | }, []) 23 | } 24 | 25 | export default processAliasModes 26 | -------------------------------------------------------------------------------- /src/utilities/roundWithDecimals.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * If the provided value is a number 3 | * it is rounded to 3 decimal positions 4 | * otherwise it is returned as is 5 | * @param value number 6 | * @param decimalPlaces int 7 | */ 8 | const roundWithDecimals = (value?: number, decimalPlaces = 2) => { 9 | // exit if value is undefined 10 | if (value === undefined) { 11 | return 12 | } 13 | // check for correct inputs 14 | if (typeof value !== 'number' || typeof decimalPlaces !== 'number') { 15 | throw new Error(`Invalid parameters, both value "${value}" (${typeof value}) and decimalPlaces "${decimalPlaces}" (${typeof decimalPlaces}) must be of type number`) 16 | } 17 | // set decimal places 18 | const factorOfTen = Math.pow(10, decimalPlaces) 19 | // round result and return 20 | return Math.round(value * factorOfTen) / factorOfTen 21 | } 22 | 23 | export default roundWithDecimals 24 | -------------------------------------------------------------------------------- /src/utilities/semVerDifference.ts: -------------------------------------------------------------------------------- 1 | export type versionDifference = 'major' | 'minor' | 'patch' 2 | 3 | export default (currentSemVer, prevSemVers = '1.0.0'): versionDifference | undefined => { 4 | const [pMajor, pMinor, pPatch] = prevSemVers.split('.') 5 | const [cMajor, cMinor, cPatch] = currentSemVer.split('.') 6 | 7 | if (pMajor < cMajor) { 8 | return 'major' 9 | } 10 | if (pMinor < cMinor) { 11 | return 'minor' 12 | } 13 | if (pPatch < cPatch) { 14 | return 'patch' 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/utilities/settings.ts: -------------------------------------------------------------------------------- 1 | import { defaultSettings } from '@config/defaultSettings' 2 | import { Settings } from '@typings/settings' 3 | import config from '@config/config' 4 | import { stringifyJson } from './stringifyJson' 5 | 6 | const fixMissing = (defaults, current) => Object.fromEntries(Object.entries(defaults).map(([key, value]) => { 7 | if (value !== undefined && typeof current[key] !== typeof value) { 8 | return [key, defaults[key]] 9 | } 10 | return [key, current[key]] 11 | })) 12 | 13 | /** 14 | * get the current users settings 15 | * for settings that are not set, the defaults will be used 16 | * @return object 17 | */ 18 | const getSettings = (): Settings => { 19 | let storedSettings: string = figma.root.getPluginData(config.key.settings) 20 | // return defaults if no settings are present 21 | if (storedSettings === '') { 22 | return defaultSettings 23 | } 24 | // parse stored settings 25 | storedSettings = JSON.parse(storedSettings) 26 | // fix issues on first level 27 | const fixedSettings = fixMissing(defaultSettings, storedSettings) 28 | fixedSettings.prefix = fixMissing(defaultSettings.prefix, fixedSettings.prefix) 29 | fixedSettings.exports = fixMissing(defaultSettings.exports, fixedSettings.exports) 30 | // return settings 31 | return fixedSettings 32 | } 33 | 34 | /** 35 | * @name saveSettings 36 | * @description save the user settings to the "cache" 37 | * @param {UserSettings} settings 38 | */ 39 | const setSettings = (settings: Settings) => { 40 | settings = { 41 | ...defaultSettings, 42 | ...settings 43 | } 44 | // store public settings that should be shared across org 45 | figma.root.setPluginData(config.key.settings, stringifyJson(settings)) 46 | } 47 | /** 48 | * @name resetSettings 49 | * @description resetSettings the user settings to the "cache" 50 | */ 51 | const resetSettings = () => figma.root.setPluginData(config.key.settings, stringifyJson(defaultSettings)) 52 | 53 | // exports 54 | export { getSettings, setSettings, resetSettings } 55 | -------------------------------------------------------------------------------- /src/utilities/stringifyJson.ts: -------------------------------------------------------------------------------- 1 | export const stringifyJson = (object, compression = true): string => { 2 | if (compression === true) { 3 | return JSON.stringify(object) 4 | } 5 | // return uncompressed json 6 | return JSON.stringify(object, null, 2) 7 | } 8 | -------------------------------------------------------------------------------- /src/utilities/transformName.ts: -------------------------------------------------------------------------------- 1 | const returnOrThrow = (convertedString: string, originalString: string, stringCase: string): string => { 2 | // return converted string if successful 3 | if (typeof convertedString === 'string' && convertedString !== '') { 4 | return convertedString 5 | } 6 | // throw error 7 | throw new Error(`converting "${originalString}" to ${stringCase}, resulting in "${convertedString}"`) 8 | } 9 | 10 | const toCamelCase = (string: string): string => { 11 | const convertedString: string = string.toLowerCase() 12 | .replace(/['"]/g, '') 13 | .replace(/([-_ ]){1,}/g, ' ') 14 | .replace(/\W+/g, ' ') 15 | .trim() 16 | .replace(/ (.)/g, function ($1) { return $1.toUpperCase() }) 17 | .replace(/ /g, '') 18 | // return or throw 19 | return returnOrThrow(convertedString, string, 'camelCase') 20 | } 21 | 22 | const toKebabCase = (string: string): string => { 23 | const convertedString: string = string.toLowerCase() 24 | .replace(/['"]/g, '') 25 | .replace(/([-_ ]){1,}/g, ' ') 26 | .replace(/\W+/g, ' ') 27 | .trim() 28 | .replace(/ /g, '-') 29 | // return or throw 30 | return returnOrThrow(convertedString, string, 'kebabCase') 31 | } 32 | 33 | const transformName = (name: string, nameConversion = 'default'): string => { 34 | // if camelCase 35 | if (nameConversion === 'camelCase') { 36 | return toCamelCase(name) 37 | } 38 | // if kebabCase 39 | if (nameConversion === 'kebabCase') { 40 | return toKebabCase(name) 41 | } 42 | return name.trim().toLowerCase() 43 | } 44 | 45 | export default transformName 46 | export const __testing = { 47 | toCamelCase: toCamelCase, 48 | toKebabCase: toKebabCase 49 | } 50 | -------------------------------------------------------------------------------- /src/utilities/version.ts: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | const version = '6.11.3' 3 | export default version 4 | -------------------------------------------------------------------------------- /tests/data/variables.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Do not edit directly 3 | * Generated on Fri, 02 Oct 2020 06:55:39 GMT 4 | */ 5 | 6 | :root { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /tests/integration/cssOutput.test.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import cssOutputData from './data/cssOutput.data' 3 | import cssStandardOutputData from './data/cssStandardOutput.data' 4 | 5 | describe('Compare css converterd file to data set', () => { 6 | test('original css data', () => { 7 | // read files 8 | let originalCss = fs.readFileSync('./tests/integration/data/original.variables.css', 'utf8').replace(/^\s+|\s+$/g, '') 9 | // remove starting comment 10 | const lines = originalCss.split('\n') 11 | // remove comment from start 12 | lines.splice(0, lines.findIndex(line => line === ':root {')) 13 | // join the array back into a single string 14 | originalCss = lines.join('\n') 15 | // compare to data 16 | expect(originalCss).toStrictEqual(cssOutputData) 17 | }) 18 | 19 | test('standard css data', () => { 20 | // read files 21 | const css = fs.readFileSync('./tests/integration/data/standard.variables.css', 'utf8').replace(/^\s+|\s+$/g, '') 22 | // remove starting comment 23 | const lines = css.split('\n') 24 | // remove comment from start 25 | lines.splice(0, lines.findIndex(line => line === ':root {')) 26 | // join the array back into a single string 27 | const convertedCss = lines.join('\n') 28 | // compare to data 29 | expect(convertedCss).toStrictEqual(cssStandardOutputData) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /tests/integration/jsonOutput.test.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import jsonExpectedOutput from './data/jsonOriginalFormat.data' 3 | 4 | describe('Verify json output for style dictionary', () => { 5 | // read files 6 | const json = JSON.parse(fs.readFileSync('./tests/files/original-tokens.json', 'utf8')) 7 | 8 | test('Top level names available', () => { 9 | expect('color' in json).toBeTruthy() 10 | expect('gradient' in json).toBeTruthy() 11 | expect('sizes' in json).toBeTruthy() 12 | expect('breakpoints' in json).toBeTruthy() 13 | expect('spacing' in json).toBeTruthy() 14 | expect('motion' in json).toBeTruthy() 15 | expect('grid' in json).toBeTruthy() 16 | expect('font' in json).toBeTruthy() 17 | expect('effect' in json).toBeTruthy() 18 | expect('borders' in json).toBeTruthy() 19 | expect('radius' in json).toBeTruthy() 20 | expect('invalid' in json).toBeFalsy() 21 | }) 22 | 23 | test('Compare color', () => { 24 | expect(json.color).toStrictEqual(jsonExpectedOutput.color) 25 | }) 26 | 27 | test('Compare gradient', () => { 28 | expect(json.gradient).toStrictEqual(jsonExpectedOutput.gradient) 29 | }) 30 | 31 | test('Compare sizes', () => { 32 | expect(json.sizes).toStrictEqual(jsonExpectedOutput.sizes) 33 | }) 34 | 35 | test('Compare breakpoints', () => { 36 | expect(json.breakpoints).toStrictEqual(jsonExpectedOutput.breakpoints) 37 | }) 38 | 39 | test('Compare spacing', () => { 40 | expect(json.spacing).toStrictEqual(jsonExpectedOutput.spacing) 41 | }) 42 | 43 | test('Compare motion', () => { 44 | expect(json.motion).toStrictEqual(jsonExpectedOutput.motion) 45 | }) 46 | 47 | test('Compare grid', () => { 48 | expect(json.grid).toStrictEqual(jsonExpectedOutput.grid) 49 | }) 50 | 51 | test('Compare effect', () => { 52 | expect(json.effect).toStrictEqual(jsonExpectedOutput.effect) 53 | }) 54 | 55 | test('Compare borders', () => { 56 | expect(json.borders).toStrictEqual(jsonExpectedOutput.borders) 57 | }) 58 | 59 | test('Compare fonts', () => { 60 | expect(json.font).toStrictEqual(jsonExpectedOutput.font) 61 | }) 62 | 63 | test('Compare radius', () => { 64 | expect(json.radii).toStrictEqual(jsonExpectedOutput.radii) 65 | expect(json.radius).toStrictEqual(jsonExpectedOutput.radius) 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /tests/integration/libs/standard/web/colorToRgbaString.js: -------------------------------------------------------------------------------- 1 | const TinyColor = require('@ctrl/tinycolor') 2 | 3 | module.exports = { 4 | type: 'value', 5 | matcher: function (token) { 6 | return token.type === 'color' 7 | }, 8 | transformer: function ({ value }) { 9 | return `${new TinyColor.TinyColor(value).toRgbString()}` 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/integration/libs/standard/web/formatWeb.js: -------------------------------------------------------------------------------- 1 | const printDescription = description => description && description.length > 0 ? ` /* ${description} */` : '' 2 | 3 | const formatTokens = (dictionary) => 4 | dictionary.allTokens.map(({ name, value, description }) => { 5 | return ` --${name}: ${value};${printDescription(description)}` 6 | }).join('\n') 7 | 8 | module.exports = ({ dictionary, options, file }) => { 9 | return ( 10 | ':root {\n' + 11 | formatTokens(dictionary) + 12 | '\n}\n' 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /tests/integration/libs/standard/web/sizePx.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | type: 'value', 3 | matcher: function (token) { 4 | return token.type === 'dimension' && token.value !== 0 5 | }, 6 | transformer: function (token) { 7 | return `${token.value}px` 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/integration/libs/standard/web/webFont.js: -------------------------------------------------------------------------------- 1 | const notDefault = (value, defaultValue) => (value !== defaultValue) ? value : '' 2 | 3 | const fontFamily = ({ fontFamily }, { fontFamilies } = {}) => fontFamilies && fontFamilies[fontFamily] ? fontFamilies[fontFamily] : fontFamily 4 | 5 | module.exports = { 6 | type: 'value', 7 | matcher: function (token) { 8 | return token.type === 'custom-fontStyle' 9 | }, 10 | transformer: function ({ value: font }, { options }) { 11 | // font: font-style font-variant font-weight font-size/line-height font-family; 12 | return `${notDefault(font.fontStretch, 'normal')} ${notDefault(font.fontStyle, 'normal')} ${font.fontWeight} ${font.fontSize}/${font.lineHeight} ${fontFamily(font, options)}`.trim() 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/integration/libs/standard/web/webGradient.js: -------------------------------------------------------------------------------- 1 | const TinyColor = require('@ctrl/tinycolor') 2 | 3 | module.exports = { 4 | type: 'value', 5 | matcher: function (token) { 6 | return token.type === 'custom-gradient' 7 | }, 8 | transformer: function ({ value }) { 9 | const stopsString = value.stops.map(stop => { 10 | return `${new TinyColor.TinyColor(stop.color).toRgbString()} ${stop.position * 100}%` 11 | }).join(', ') 12 | if (value.gradientType === 'linear') { 13 | return `linear-gradient(${value.rotation}deg, ${stopsString})` 14 | } 15 | if (value.gradientType === 'radial') { 16 | return `radial-gradient(${stopsString})` 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/integration/libs/standard/web/webPadding.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | type: 'value', 3 | matcher: function (token) { 4 | return token.type === 'custom-spacing' 5 | }, 6 | transformer: ({ value: { top, left, bottom, right } }) => { 7 | if ([bottom, left, right].every(v => v === top)) { 8 | return `${top}px` 9 | } 10 | return `${top}px ${right}px ${bottom}px ${left}px` 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/integration/libs/standard/web/webRadius.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | type: 'value', 3 | matcher: function (token) { 4 | return token.type === 'custom-radius' 5 | }, 6 | transformer: function ({ value }) { 7 | if ([value.topRight, value.bottomLeft, value.bottomRight].every(v => v === value.topLeft)) { 8 | return `${value.topLeft}px` 9 | } 10 | return `${value.topLeft}px ${value.topRight}px ${value.bottomLeft}px ${value.bottomRight}px` 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/integration/libs/standard/web/webShadows.js: -------------------------------------------------------------------------------- 1 | const TinyColor = require('@ctrl/tinycolor') 2 | 3 | module.exports = { 4 | type: 'value', 5 | matcher: function (token) { 6 | return token.type === 'custom-shadow' && token.value !== 0 7 | }, 8 | transformer: function ({ value }) { 9 | return `${value.shadowType === 'innerShadow' ? 'inset ' : ''}${value.offsetX}px ${value.offsetY}px ${value.radius}px ${value.spread}px ${new TinyColor.TinyColor(value.color).toRgbString()}` 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/integration/original.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": ["./tests/files/original-tokens.json"], 3 | "platforms": { 4 | "css": { 5 | "transforms": ["attribute/cti", "name/cti/kebab", "time/seconds", "content/icon", "size/px", "color/css"], 6 | "buildPath": "./tests/integration/data/", 7 | "files": [ 8 | { 9 | "destination": "original.variables.css", 10 | "format": "css/variables", 11 | "options" : { 12 | "showFileHeader": false 13 | } 14 | } 15 | ] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/integration/standard.build.js: -------------------------------------------------------------------------------- 1 | const StyleDictionary = require('style-dictionary') 2 | 3 | const StyleDictionaryExtended = StyleDictionary.extend({ 4 | source: ['./tests/files/standard-tokens.json'], 5 | transform: { 6 | 'size/px': require('./libs/standard/web/sizePx'), 7 | 'web/shadow': require('./libs/standard/web/webShadows'), 8 | 'web/radius': require('./libs/standard/web/webRadius'), 9 | 'web/padding': require('./libs/standard/web/webPadding'), 10 | 'web/font': require('./libs/standard/web/webFont'), 11 | 'web/gradient': require('./libs/standard/web/webGradient'), 12 | 'color/hex8ToRgba': require('./libs/standard/web/colorToRgbaString') 13 | }, 14 | format: { 15 | css: require('./libs/standard/web/formatWeb') 16 | }, 17 | platforms: { 18 | css: { 19 | transforms: StyleDictionary.transformGroup.css.concat([ 20 | 'size/px', 21 | 'web/shadow', 22 | 'web/radius', 23 | 'web/padding', 24 | 'web/font', 25 | 'web/gradient', 26 | 'color/hex8ToRgba' 27 | ]), 28 | buildPath: './tests/integration/data/', 29 | files: [ 30 | { 31 | destination: 'standard.variables.css', 32 | format: 'css', 33 | options: { 34 | showFileHeader: false 35 | } 36 | } 37 | ] 38 | } 39 | } 40 | }) 41 | 42 | StyleDictionaryExtended.buildAllPlatforms() 43 | -------------------------------------------------------------------------------- /tests/unit/accessToken.test.ts: -------------------------------------------------------------------------------- 1 | import { getAccessToken } from '@utils/accessToken' 2 | 3 | beforeAll(() => { 4 | // @ts-ignore 5 | global.figma = { 6 | clientStorage: { 7 | getAsync: jest.fn() 8 | } 9 | } 10 | }) 11 | 12 | describe('Testing getAccessToken', () => { 13 | test('return correct token object', () => { 14 | // @ts-ignore 15 | global.figma.clientStorage.getAsync.mockReturnValue(Promise.resolve({ 16 | '125454sdaf': 'test' 17 | })) 18 | 19 | return getAccessToken('125454sdaf').then(data => { 20 | expect(data).toBe('test') 21 | }) 22 | }) 23 | 24 | test('return string instead of token object', () => { 25 | // @ts-ignore 26 | global.figma.clientStorage.getAsync.mockReturnValue(Promise.resolve('wrong')) 27 | 28 | return getAccessToken('125454sdaf').then(data => { 29 | expect(data).toBe('') 30 | }) 31 | }) 32 | 33 | test('token does not exist', () => { 34 | // @ts-ignore 35 | global.figma.clientStorage.getAsync.mockReturnValue(Promise.resolve({ 36 | '125454sdaf': 'test' 37 | })) 38 | 39 | return getAccessToken('N25454sdaf').then(data => { 40 | expect(data).toBe('') 41 | }) 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /tests/unit/base64.test.ts: -------------------------------------------------------------------------------- 1 | import { utf8ToBase64 } from '@utils/base64' 2 | 3 | describe('utf8ToBase64', () => { 4 | it('should convert a simple string to base64', () => { 5 | const input = 'hello' 6 | const expectedOutput = 'aGVsbG8=' 7 | expect(utf8ToBase64(input)).toBe(expectedOutput) 8 | }) 9 | 10 | it('should convert a string with special characters to base64', () => { 11 | const input = 'hello world!' 12 | const expectedOutput = 'aGVsbG8gd29ybGQh' 13 | expect(utf8ToBase64(input)).toBe(expectedOutput) 14 | }) 15 | 16 | it('should convert an empty string to base64', () => { 17 | const input = '' 18 | const expectedOutput = '' 19 | expect(utf8ToBase64(input)).toBe(expectedOutput) 20 | }) 21 | 22 | it('should convert a string with unicode characters to base64', () => { 23 | const input = 'こんにちは' 24 | const expectedOutput = '44GT44KT44Gr44Gh44Gv' 25 | expect(utf8ToBase64(input)).toBe(expectedOutput) 26 | }) 27 | }) -------------------------------------------------------------------------------- /tests/unit/buildFigmaData.test.ts: -------------------------------------------------------------------------------- 1 | import { defaultSettings } from '@config/defaultSettings' 2 | import buildFigmaData from '@utils/buildFigmaData' 3 | import getTokenNodes from '@utils/getTokenNodes' 4 | jest.mock('@utils/getTokenNodes', () => jest.fn()) 5 | 6 | const defaultOutput = { 7 | effectStyles: [{ 8 | name: 'EffectStyle', 9 | id: undefined, 10 | description: undefined, 11 | effects: undefined 12 | }], 13 | textStyles: [{ 14 | name: 'TextStyle', 15 | id: undefined, 16 | description: undefined, 17 | fontName: undefined, 18 | fontSize: undefined, 19 | letterSpacing: undefined, 20 | lineHeight: undefined, 21 | paragraphIndent: undefined, 22 | paragraphSpacing: undefined, 23 | textCase: undefined, 24 | textDecoration: undefined 25 | }], 26 | gridStyles: [{ 27 | name: 'GridStyle', 28 | id: undefined, 29 | description: undefined, 30 | layoutGrids: undefined 31 | }], 32 | paintStyles: [{ 33 | name: 'PaintStyle', 34 | id: undefined, 35 | description: undefined, 36 | paints: undefined 37 | }], 38 | tokenFrames: [ 39 | 'token' 40 | ] 41 | } 42 | 43 | beforeAll(() => { 44 | // @ts-ignore 45 | global.figma = { 46 | root: { 47 | children: [{ 48 | findChildren: jest.fn() 49 | }] 50 | }, 51 | getLocalPaintStylesAsync: jest.fn(), 52 | getLocalGridStylesAsync: jest.fn(), 53 | getLocalTextStylesAsync: jest.fn(), 54 | getLocalEffectStylesAsync: jest.fn() 55 | } 56 | 57 | // @ts-ignore 58 | global.figma.getLocalPaintStylesAsync.mockReturnValue([{ 59 | name: 'PaintStyle' 60 | }, 61 | { 62 | name: '_HiddenPaintStyle' 63 | }]) 64 | // @ts-ignore 65 | global.figma.getLocalGridStylesAsync.mockReturnValue([{ 66 | name: 'GridStyle' 67 | }, 68 | { 69 | name: '_HiddenGridStyle' 70 | }]) 71 | // @ts-ignore 72 | global.figma.getLocalTextStylesAsync.mockReturnValue([{ 73 | name: 'TextStyle' 74 | }, 75 | { 76 | name: '_HiddenTextStyle' 77 | }]) 78 | // @ts-ignore 79 | global.figma.getLocalEffectStylesAsync.mockReturnValue([{ 80 | name: 'EffectStyle' 81 | }, 82 | { 83 | name: '_HiddenEffectStyle' 84 | }]) 85 | // @ts-ignore 86 | getTokenNodes.mockImplementation(() => ['token']) 87 | }) 88 | 89 | describe('Testing buildFigmaData', () => { 90 | test('without options', async () => { 91 | // assert 92 | // @ts-ignore 93 | expect(await buildFigmaData(global.figma, defaultSettings)).toStrictEqual(defaultOutput) 94 | }) 95 | }) 96 | -------------------------------------------------------------------------------- /tests/unit/data/color.data.ts: -------------------------------------------------------------------------------- 1 | export const rgb = { 2 | raw: { 3 | r: 0.523874682, 4 | g: 1, 5 | b: 0 6 | }, 7 | converted: { 8 | r: 134, 9 | g: 255, 10 | b: 0 11 | } 12 | } 13 | export const opacity = 0.75 14 | export const rgba = { 15 | raw: { 16 | r: 0.52456, 17 | g: 1, 18 | b: 0, 19 | a: 0.65 20 | }, 21 | converted: { 22 | r: 134, 23 | g: 255, 24 | b: 0, 25 | a: 0.65 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/unit/data/customTokenNode.data.ts: -------------------------------------------------------------------------------- 1 | export const customTokenNode = { 2 | name: 'notRecognizedName/customTokenNode', 3 | description: 'a description text', 4 | bottomLeftRadius: 3, 5 | bottomRightRadius: 4, 6 | topLeftRadius: 5, 7 | topRightRadius: 0, 8 | cornerRadius: 'mixed', 9 | cornerSmoothing: 0.35, 10 | strokes: [{ r: 255, g: 230, b: 0, a: 1 }], 11 | strokeWeight: 2, 12 | // strokeStyleId: node.strokeStyleId, 13 | strokeMiterLimit: 25, 14 | strokeJoin: 'MITER', 15 | strokeCap: 'ROUND', 16 | dashPattern: [2, 5], 17 | strokeAlign: 'center', 18 | width: 10, 19 | height: 20, 20 | reactions: [{ 21 | action: { 22 | type: 'NODE', 23 | transition: { 24 | type: 'MOVE_IN', 25 | duration: 0.32124124, 26 | direction: 'LEFT', 27 | easing: { 28 | type: 'LINEAR' 29 | } 30 | } 31 | } 32 | }], 33 | paddingTop: 0, 34 | paddingRight: 0, 35 | paddingBottom: 0, 36 | paddingLeft: 0 37 | } 38 | -------------------------------------------------------------------------------- /tests/unit/data/effectStyleObjects.data.ts: -------------------------------------------------------------------------------- 1 | export const effectStyles = [ 2 | { 3 | name: 'shadow', 4 | id: 30, 5 | description: 'an effect style', 6 | effects: [ 7 | { 8 | color: { 9 | r: 0.1, 10 | g: 0.2, 11 | b: 0.3, 12 | a: 0.4 13 | }, 14 | offset: { 15 | x: 5, 16 | y: 3 17 | }, 18 | blendMode: 'NORMAL', 19 | type: 'DROP_SHADOW', 20 | radius: 30, 21 | visible: true 22 | }, 23 | { 24 | type: 'BACKGROUND_BLUR', // | "BACKGROUND_BLUR" 25 | radius: 2, 26 | visible: true 27 | } 28 | ], 29 | // @ts-ignore 30 | type: 'EFFECT' 31 | }, 32 | { 33 | name: 'blur no description', 34 | id: 31, 35 | effects: [{ 36 | type: 'LAYER_BLUR', // | "BACKGROUND_BLUR" 37 | radius: 7, 38 | visible: true 39 | }], 40 | // @ts-ignore 41 | type: 'EFFECT' 42 | } 43 | ] 44 | export const effectStyleObjects = [ 45 | { 46 | name: 'shadow', 47 | id: 30, 48 | description: 'an effect style', 49 | effects: [{ 50 | type: 'DROP_SHADOW', // | "INNER_SHADOW" 51 | color: { 52 | r: 0.1, 53 | g: 0.2, 54 | b: 0.3, 55 | a: 0.4 56 | }, 57 | offset: { 58 | x: 5, 59 | y: 3 60 | }, 61 | radius: 30, 62 | // readonly spread?: number 63 | visible: true, 64 | blendMode: 'NORMAL' 65 | }, 66 | { 67 | type: 'BACKGROUND_BLUR', // | "BACKGROUND_BLUR" 68 | radius: 2, 69 | visible: true 70 | }] 71 | }, 72 | { 73 | name: 'blur no description', 74 | id: 31, 75 | description: undefined, 76 | effects: [{ 77 | type: 'LAYER_BLUR', // | "BACKGROUND_BLUR" 78 | radius: 7, 79 | visible: true 80 | }] 81 | } 82 | ] 83 | -------------------------------------------------------------------------------- /tests/unit/data/gridStyleObjects.data.ts: -------------------------------------------------------------------------------- 1 | export const gridStyles = [ 2 | { 3 | name: 'rows & columns', 4 | id: 20, 5 | description: 'a description grid', 6 | layoutGrids: [ 7 | { 8 | // readonly pattern: "ROWS" | "COLUMNS" 9 | pattern: 'ROWS', 10 | // readonly alignment: "MIN" | "MAX" | "STRETCH" | "CENTER" 11 | alignment: 'CENTER', 12 | gutterSize: 7, 13 | count: 8, // Infinity when "Auto" is set in the UI 14 | sectionSize: 10, // Not set for alignment: "STRETCH" 15 | // offset?: number // Not set for alignment: "CENTER" 16 | visible: true, 17 | color: { 18 | r: 0.1, 19 | g: 0.2, 20 | b: 0.3, 21 | a: 0.4 22 | } 23 | }, 24 | { 25 | // readonly pattern: "ROWS" | "COLUMNS" 26 | pattern: 'COLUMNS', 27 | // readonly alignment: "MIN" | "MAX" | "STRETCH" | "CENTER" 28 | alignment: 'MAX', 29 | gutterSize: 7, 30 | count: Infinity, // Infinity when "Auto" is set in the UI 31 | sectionSize: 10, // Not set for alignment: "STRETCH" 32 | offset: 9, // Not set for alignment: "CENTER" 33 | visible: true, 34 | color: { 35 | r: 0.1, 36 | g: 0.2, 37 | b: 0.3, 38 | a: 0.4 39 | } 40 | } 41 | ] 42 | }, 43 | { 44 | name: 'grid no description', 45 | id: 21, 46 | layoutGrids: [ 47 | { 48 | pattern: 'GRID', 49 | sectionSize: 5, 50 | visible: true, 51 | color: { 52 | r: 0.1, 53 | g: 0.2, 54 | b: 0.3, 55 | a: 0.4 56 | } 57 | } 58 | ] 59 | } 60 | ] 61 | export const gridStyleObjects = [ 62 | { 63 | name: 'rows & columns', 64 | id: 20, 65 | description: 'a description grid', 66 | layoutGrids: [ 67 | { 68 | // readonly pattern: "ROWS" | "COLUMNS" 69 | pattern: 'ROWS', 70 | // readonly alignment: "MIN" | "MAX" | "STRETCH" | "CENTER" 71 | alignment: 'CENTER', 72 | gutterSize: 7, 73 | count: 8, // Infinity when "Auto" is set in the UI 74 | sectionSize: 10, // Not set for alignment: "STRETCH" 75 | // offset?: number // Not set for alignment: "CENTER" 76 | visible: true, 77 | color: { 78 | r: 0.1, 79 | g: 0.2, 80 | b: 0.3, 81 | a: 0.4 82 | } 83 | }, 84 | { 85 | // readonly pattern: "ROWS" | "COLUMNS" 86 | pattern: 'COLUMNS', 87 | // readonly alignment: "MIN" | "MAX" | "STRETCH" | "CENTER" 88 | alignment: 'MAX', 89 | gutterSize: 7, 90 | count: Infinity, // Infinity when "Auto" is set in the UI 91 | sectionSize: 10, // Not set for alignment: "STRETCH" 92 | offset: 9, // Not set for alignment: "CENTER" 93 | visible: true, 94 | color: { 95 | r: 0.1, 96 | g: 0.2, 97 | b: 0.3, 98 | a: 0.4 99 | } 100 | } 101 | ] 102 | }, 103 | { 104 | name: 'grid no description', 105 | id: 21, 106 | description: undefined, 107 | layoutGrids: [ 108 | { 109 | pattern: 'GRID', 110 | sectionSize: 5, 111 | visible: true, 112 | color: { 113 | r: 0.1, 114 | g: 0.2, 115 | b: 0.3, 116 | a: 0.4 117 | } 118 | } 119 | ] 120 | } 121 | ] 122 | -------------------------------------------------------------------------------- /tests/unit/deepMerge.test.ts: -------------------------------------------------------------------------------- 1 | // deepMerge.test.ts 2 | import deepMerge from '@utils/deepMerge' 3 | 4 | describe('deepMerge', () => { 5 | it('should merge two objects', () => { 6 | const target = { a: 1, b: 2 } 7 | const source = { b: 3, c: 4 } 8 | const result = deepMerge(target, source) 9 | expect(result).toEqual({ a: 1, b: 3, c: 4 }) 10 | }) 11 | 12 | it('should merge nested objects', () => { 13 | const target = { a: { b: 1 } } 14 | const source = { a: { c: 2 } } 15 | const result = deepMerge(target, source) 16 | expect(result).toEqual({ a: { b: 1, c: 2 } }) 17 | }) 18 | 19 | it('should merge arrays without duplicates', () => { 20 | const target = { a: [1, 2] } 21 | const source = { a: [2, 3] } 22 | const result = deepMerge(target, source) 23 | expect(result).toEqual({ a: [1, 2, 3] }) 24 | }) 25 | 26 | it('should overwrite non-object values', () => { 27 | const target = { a: 1 } 28 | const source = { a: 2 } 29 | const result = deepMerge(target, source) 30 | expect(result).toEqual({ a: 2 }) 31 | }) 32 | 33 | it('should return source if target is not an object', () => { 34 | const target = null 35 | const source = { a: 1 } 36 | const result = deepMerge(target, source) 37 | expect(result).toEqual({ a: 1 }) 38 | }) 39 | 40 | it('should return source if source is not an object', () => { 41 | const target = { a: 1 } 42 | const source = null 43 | const result = deepMerge(target, source) 44 | expect(result).toEqual(null) 45 | }) 46 | 47 | it('should handle empty objects', () => { 48 | const target = {} 49 | const source = {} 50 | const result = deepMerge(target, source) 51 | expect(result).toEqual({}) 52 | }) 53 | 54 | it('should handle empty arrays', () => { 55 | const target = { a: [] } 56 | const source = { a: [] } 57 | const result = deepMerge(target, source) 58 | expect(result).toEqual({ a: [] }) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /tests/unit/extractBreakpoints.test.ts: -------------------------------------------------------------------------------- 1 | import extractBreakpoints from '@src/extractor/extractBreakpoints' 2 | import { customTokenNode } from './data/customTokenNode.data' 3 | 4 | describe('extracting breakpoints', () => { 5 | const nodeArray = [ 6 | { 7 | ...customTokenNode, 8 | ...{ 9 | name: 'breakpoints/desktop', 10 | description: 'the width will be set as a max-width for desktop', 11 | width: 1440 12 | } 13 | } 14 | ] 15 | 16 | test('extracting only the token with correct name from customTokenNodesArray', () => { 17 | expect(extractBreakpoints(nodeArray, ['breakpoints'])).toStrictEqual([{ 18 | category: 'breakpoint', 19 | description: 'the width will be set as a max-width for desktop', 20 | exportKey: 'breakpoint', 21 | name: 'breakpoints/desktop', 22 | values: { 23 | height: { 24 | type: 'number', 25 | unit: 'pixel', 26 | value: 20 27 | }, 28 | width: { 29 | type: 'number', 30 | unit: 'pixel', 31 | value: 1440 32 | } 33 | }, 34 | extensions: { 35 | 'org.lukasoppermann.figmaDesignTokens': { 36 | exportKey: 'breakpoint' 37 | } 38 | } 39 | } 40 | ]) 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /tests/unit/extractEffects.test.ts: -------------------------------------------------------------------------------- 1 | import extractEffects from '@src/extractor/extractEffects' 2 | import { effectStyleObjects } from './data/effectStyleObjects.data' 3 | 4 | describe('extracting effects', () => { 5 | test('extract only valid effects', () => { 6 | expect(extractEffects(effectStyleObjects, ['effect'])).toStrictEqual([{ 7 | category: 'effect', 8 | exportKey: 'effect', 9 | description: 'an effect style', 10 | extensions: { 11 | 'org.lukasoppermann.figmaDesignTokens': { 12 | exportKey: 'effect', 13 | styleId: 30 14 | } 15 | }, 16 | name: 'effect/shadow', 17 | values: [{ 18 | color: { 19 | type: 'color', 20 | value: { 21 | a: 0.4, 22 | b: 77, 23 | g: 51, 24 | r: 26 25 | } 26 | }, 27 | radius: { 28 | value: 30, 29 | type: 'number', 30 | unit: 'pixel' 31 | }, 32 | offset: { 33 | x: { 34 | value: 5, 35 | type: 'number', 36 | unit: 'pixel' 37 | }, 38 | y: { 39 | value: 3, 40 | type: 'number', 41 | unit: 'pixel' 42 | } 43 | }, 44 | spread: { 45 | value: undefined, 46 | type: 'number', 47 | unit: 'pixel' 48 | }, 49 | effectType: { 50 | value: 'dropShadow', 51 | type: 'string' 52 | } 53 | }, 54 | { 55 | radius: { 56 | value: 2, 57 | type: 'number', 58 | unit: 'pixel' 59 | }, 60 | effectType: { 61 | value: 'backgroundBlur', 62 | type: 'string' 63 | } 64 | }] 65 | }, 66 | { 67 | category: 'effect', 68 | exportKey: 'effect', 69 | description: null, 70 | extensions: { 71 | 'org.lukasoppermann.figmaDesignTokens': { 72 | exportKey: 'effect', 73 | styleId: 31 74 | } 75 | }, 76 | name: 'effect/blur no description', 77 | values: [{ 78 | radius: { 79 | value: 7, 80 | type: 'number', 81 | unit: 'pixel' 82 | }, 83 | effectType: { 84 | value: 'layerBlur', 85 | type: 'string' 86 | } 87 | }] 88 | } 89 | ]) 90 | }) 91 | }) 92 | -------------------------------------------------------------------------------- /tests/unit/extractFonts.test.ts: -------------------------------------------------------------------------------- 1 | import extractFonts from '@src/extractor/extractFonts' 2 | import { textStyleObjects, extractFontsOutput } from './data/textStyleObjects.data' 3 | 4 | describe('extracting fonts', () => { 5 | test('extract only valid fonts', () => { 6 | expect(extractFonts(textStyleObjects, ['font'])).toStrictEqual(extractFontsOutput) 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /tests/unit/extractGrids.test.ts: -------------------------------------------------------------------------------- 1 | import extractGrids from '@src/extractor/extractGrids' 2 | import { gridStyleObjects } from './data/gridStyleObjects.data' 3 | 4 | describe('extracting grids', () => { 5 | test('extracting only the token with correct name from customTokenNodesArray', () => { 6 | expect(extractGrids(gridStyleObjects, ['grid'])).toStrictEqual([{ 7 | category: 'grid', 8 | exportKey: 'grid', 9 | description: 'a description grid', 10 | name: 'grid/rows & columns', 11 | extensions: { 12 | 'org.lukasoppermann.figmaDesignTokens': { 13 | styleId: 20, 14 | exportKey: 'grid' 15 | } 16 | }, 17 | values: [{ 18 | pattern: { 19 | type: 'string', 20 | value: 'rows' 21 | }, 22 | alignment: { 23 | type: 'string', 24 | value: 'center' 25 | }, 26 | sectionSize: { 27 | type: 'number', 28 | unit: 'pixel', 29 | value: 10 30 | }, 31 | gutterSize: { 32 | type: 'number', 33 | unit: 'pixel', 34 | value: 7 35 | }, 36 | count: { 37 | type: 'number', 38 | value: 8 39 | } 40 | }, 41 | { 42 | pattern: { 43 | type: 'string', 44 | value: 'columns' 45 | }, 46 | alignment: { 47 | type: 'string', 48 | value: 'max' 49 | }, 50 | sectionSize: { 51 | type: 'number', 52 | unit: 'pixel', 53 | value: 10 54 | }, 55 | offset: { 56 | type: 'number', 57 | unit: 'pixel', 58 | value: 9 59 | }, 60 | gutterSize: { 61 | type: 'number', 62 | unit: 'pixel', 63 | value: 7 64 | }, 65 | count: { 66 | type: 'string', 67 | value: 'auto' 68 | } 69 | }] 70 | }, 71 | { 72 | category: 'grid', 73 | exportKey: 'grid', 74 | description: null, 75 | name: 'grid/grid no description', 76 | extensions: { 77 | 'org.lukasoppermann.figmaDesignTokens': { 78 | styleId: 21, 79 | exportKey: 'grid' 80 | } 81 | }, 82 | values: [{ 83 | pattern: { 84 | type: 'string', 85 | value: 'grid' 86 | }, 87 | sectionSize: { 88 | type: 'number', 89 | unit: 'pixel', 90 | value: 5 91 | } 92 | }] 93 | } 94 | ]) 95 | }) 96 | }) 97 | -------------------------------------------------------------------------------- /tests/unit/extractOpacitites.test.ts: -------------------------------------------------------------------------------- 1 | import extractOpacities from '@src/extractor/extractOpacities' 2 | import { customTokenNode } from './data/customTokenNode.data' 3 | 4 | describe('extracting opacities', () => { 5 | const nodeArray = [ 6 | { 7 | ...customTokenNode, 8 | ...{ 9 | name: 'opacities/button-disabled', 10 | description: 'the opacity of disabled buttons', 11 | opacity: 0.3 12 | } 13 | } 14 | ] 15 | 16 | test('extracting only the token with correct name from customTokenNodesArray', () => { 17 | expect(extractOpacities(nodeArray, ['opacities'])).toStrictEqual([{ 18 | category: 'opacity', 19 | description: 'the opacity of disabled buttons', 20 | exportKey: 'opacity', 21 | name: 'opacities/button-disabled', 22 | values: { 23 | opacity: { 24 | type: 'number', 25 | value: 0.3 26 | } 27 | }, 28 | extensions: { 29 | 'org.lukasoppermann.figmaDesignTokens': { 30 | exportKey: 'opacity' 31 | } 32 | } 33 | } 34 | ]) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /tests/unit/extractSizes.test.ts: -------------------------------------------------------------------------------- 1 | import extractSizes from '@src/extractor/extractSizes' 2 | import { customTokenNode } from './data/customTokenNode.data' 3 | 4 | describe('extracting sizes', () => { 5 | const nodeArray = [ 6 | customTokenNode, 7 | { 8 | ...customTokenNode, 9 | ...{ name: 'sizes/10' } 10 | }, 11 | { 12 | ...customTokenNode, 13 | ...{ 14 | name: 'sizes/10 no desc', 15 | description: null, 16 | width: 10.2345, 17 | height: 0.567 18 | } 19 | } 20 | ] 21 | 22 | test('extracting only the token with correct name from customTokenNodesArray', () => { 23 | expect(extractSizes(nodeArray, ['sizes'])).toStrictEqual([{ 24 | category: 'size', 25 | exportKey: 'size', 26 | description: 'a description text', 27 | name: 'sizes/10', 28 | extensions: { 29 | 'org.lukasoppermann.figmaDesignTokens': { 30 | exportKey: 'size' 31 | } 32 | }, 33 | values: { 34 | height: { 35 | type: 'number', 36 | unit: 'pixel', 37 | value: 20 38 | }, 39 | width: { 40 | type: 'number', 41 | unit: 'pixel', 42 | value: 10 43 | } 44 | } 45 | }, 46 | { 47 | category: 'size', 48 | exportKey: 'size', 49 | description: null, 50 | name: 'sizes/10 no desc', 51 | extensions: { 52 | 'org.lukasoppermann.figmaDesignTokens': { 53 | exportKey: 'size' 54 | } 55 | }, 56 | values: { 57 | height: { 58 | type: 'number', 59 | unit: 'pixel', 60 | value: 0.57 61 | }, 62 | width: { 63 | type: 'number', 64 | unit: 'pixel', 65 | value: 10.23 66 | } 67 | } 68 | } 69 | ]) 70 | }) 71 | }) 72 | -------------------------------------------------------------------------------- /tests/unit/extractUtilities.test.ts: -------------------------------------------------------------------------------- 1 | import { filterByPrefix } from '@src/extractor/extractUtilities' 2 | 3 | describe('filterByPrefix', () => { 4 | const nodeList = [{ 5 | name: 'rect 19' 6 | }, 7 | { 8 | name: 'color/red' 9 | }] 10 | test('invalid: undefined', () => { 11 | // @ts-ignore 12 | expect(nodeList.filter(filterByPrefix(undefined))).toStrictEqual([]) 13 | }) 14 | 15 | test('invalid: string', () => { 16 | // @ts-ignore 17 | expect(nodeList.filter(filterByPrefix('color'))).toStrictEqual([]) 18 | }) 19 | 20 | test('valid: array with multiple strings', () => { 21 | expect(nodeList.filter(filterByPrefix(['color', 'colors']))).toStrictEqual([{ 22 | name: 'color/red' 23 | }]) 24 | }) 25 | 26 | test('valid: array with string', () => { 27 | expect(nodeList.filter(filterByPrefix(['color']))).toStrictEqual([{ 28 | name: 'color/red' 29 | }]) 30 | }) 31 | 32 | test('valid: array with empty string', () => { 33 | expect(nodeList.filter(filterByPrefix(['']))).toStrictEqual([]) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /tests/unit/filterByNameProperty.test.ts: -------------------------------------------------------------------------------- 1 | import filterByNameProperty from '@utils/filterByNameProperty' 2 | 3 | describe('getTokenStyles', () => { 4 | test('exclude _ . and * prefix', () => { 5 | expect([ 6 | { 7 | id: 'valid', 8 | type: 'PAINT', 9 | name: 'valid', 10 | description: '' 11 | }, 12 | { 13 | id: 'invalid', 14 | type: 'PAINT', 15 | name: '_invalid', 16 | description: '' 17 | }, 18 | { 19 | id: 'invalid', 20 | type: 'PAINT', 21 | name: '.invalid', 22 | description: '' 23 | }, 24 | { 25 | id: 'invalid', 26 | type: 'PAINT', 27 | name: '*invalid', 28 | description: '' 29 | } 30 | ].filter(item => filterByNameProperty(item, ['*']))).toStrictEqual([ 31 | { 32 | id: 'valid', 33 | type: 'PAINT', 34 | name: 'valid', 35 | description: '' 36 | } 37 | ]) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /tests/unit/getEffectStyles.test.ts: -------------------------------------------------------------------------------- 1 | import getEffectStyles from '@utils/getEffectStyles' 2 | import { effectStyles, effectStyleObjects } from './data/effectStyleObjects.data' 3 | 4 | describe('Testing getEffectStyles', () => { 5 | test('Testing function', () => { 6 | // @ts-ignore 7 | expect(getEffectStyles(effectStyles)).toStrictEqual(effectStyleObjects) 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /tests/unit/getFileId.test.ts: -------------------------------------------------------------------------------- 1 | import getFileId from '@utils/getFileId' 2 | import config from '@config/config' 3 | 4 | describe('getFileId', () => { 5 | const figmaMock = { 6 | root: { 7 | name: 'testFile', 8 | getPluginData: jest.fn(), 9 | setPluginData: jest.fn() 10 | } 11 | } 12 | 13 | beforeEach(() => { 14 | jest.resetAllMocks() 15 | }) 16 | 17 | test('no file id set', () => { 18 | figmaMock.root.getPluginData.mockReturnValue(undefined) 19 | // run module 20 | // @ts-ignore 21 | getFileId(figmaMock) 22 | // assert 23 | expect(figmaMock.root.getPluginData).toBeCalledTimes(2) 24 | expect(figmaMock.root.setPluginData.mock.calls[0][0]).toStrictEqual(config.key.fileId) 25 | expect(figmaMock.root.setPluginData.mock.calls[0][1].substr(0, 8)).toStrictEqual('testFile') 26 | }) 27 | 28 | test('file id already set', () => { 29 | figmaMock.root.getPluginData.mockReturnValue('alreadySet 2345') 30 | // run module 31 | // @ts-ignore 32 | const value = getFileId(figmaMock) 33 | // assert 34 | expect(figmaMock.root.getPluginData).toBeCalledTimes(1) 35 | expect(figmaMock.root.setPluginData).not.toBeCalled() 36 | expect(value).toStrictEqual('alreadySet 2345') 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /tests/unit/getGridStyles.test.ts: -------------------------------------------------------------------------------- 1 | import getGridStyles from '@utils/getGridStyles' 2 | import { gridStyles, gridStyleObjects } from './data/gridStyleObjects.data' 3 | 4 | describe('Testing getGridStyles', () => { 5 | test('Testing function', () => { 6 | // @ts-ignore 7 | expect(getGridStyles(gridStyles)).toStrictEqual(gridStyleObjects) 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /tests/unit/getPaintStyles.test.ts: -------------------------------------------------------------------------------- 1 | import getPaintStyles from '@utils/getPaintStyles' 2 | import { paintStyles, paintStyleObjects } from './data/paintStyleObjects.data' 3 | 4 | describe('Testing getPaintStyles', () => { 5 | test('Testing function', () => { 6 | // @ts-ignore 7 | expect(getPaintStyles(paintStyles)).toStrictEqual(paintStyleObjects) 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /tests/unit/getTextStyles.test.ts: -------------------------------------------------------------------------------- 1 | import getTextStyles from '@utils/getTextStyles' 2 | import { textStyles, textStyleObjects } from './data/textStyleObjects.data' 3 | 4 | describe('Testing getTextStyles', () => { 5 | test('Testing function', () => { 6 | // @ts-ignore 7 | expect(getTextStyles(textStyles)).toStrictEqual(textStyleObjects) 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /tests/unit/getVariableTypeByValue.test.ts: -------------------------------------------------------------------------------- 1 | import { getVariableTypeByValue } from '@utils/getVariableTypeByValue' 2 | 3 | describe('getVariableTypeByValue', () => { 4 | it('should return "string" for boolean values', () => { 5 | expect(getVariableTypeByValue(true)).toBe('string') 6 | expect(getVariableTypeByValue(false)).toBe('string') 7 | }) 8 | 9 | it('should return "dimension" for number values', () => { 10 | expect(getVariableTypeByValue(42)).toBe('dimension') 11 | expect(getVariableTypeByValue(3.14)).toBe('dimension') 12 | }) 13 | 14 | it('should return "color" for object values', () => { 15 | expect(getVariableTypeByValue({})).toBe('color') 16 | expect(getVariableTypeByValue({ key: 'value' })).toBe('color') 17 | }) 18 | 19 | it('should return "string" for string values', () => { 20 | expect(getVariableTypeByValue('hello')).toBe('string') 21 | expect(getVariableTypeByValue('')).toBe('string') 22 | }) 23 | }) -------------------------------------------------------------------------------- /tests/unit/getVersionDifference.test.ts: -------------------------------------------------------------------------------- 1 | import getVersionDifference from '@utils/getVersionDifference' 2 | jest.mock('@utils/version', () => '3.1.2') 3 | 4 | describe('getVersionDifference', () => { 5 | const figmaMock = { 6 | clientStorage: { 7 | getAsync: jest.fn(), 8 | setAsync: jest.fn() 9 | } 10 | } 11 | 12 | test('same version', async () => { 13 | figmaMock.clientStorage.getAsync.mockReturnValue('3.1.2') 14 | // @ts-ignore 15 | const versionDifference = await getVersionDifference(figmaMock) 16 | // expect outcome 17 | expect(versionDifference).toStrictEqual(undefined) 18 | expect(figmaMock.clientStorage.setAsync).not.toBeCalledWith('3.1.2') 19 | }) 20 | 21 | test('major version', async () => { 22 | figmaMock.clientStorage.getAsync.mockReturnValue('2.0.0') 23 | // @ts-ignore 24 | const versionDifference = await getVersionDifference(figmaMock) 25 | // expect outcome 26 | expect(versionDifference).toStrictEqual('major') 27 | expect(figmaMock.clientStorage.setAsync).toBeCalledWith('lastVersionSettingsOpened', '3.1.2') 28 | }) 29 | 30 | test('minor version', async () => { 31 | figmaMock.clientStorage.getAsync.mockReturnValue('3.0.0') 32 | // @ts-ignore 33 | const versionDifference = await getVersionDifference(figmaMock) 34 | // expect outcome 35 | expect(versionDifference).toStrictEqual('minor') 36 | expect(figmaMock.clientStorage.setAsync).toBeCalledWith('lastVersionSettingsOpened', '3.1.2') 37 | }) 38 | 39 | test('patch version', async () => { 40 | figmaMock.clientStorage.getAsync.mockReturnValue('3.1.1') 41 | // @ts-ignore 42 | const versionDifference = await getVersionDifference(figmaMock) 43 | // expect outcome 44 | expect(versionDifference).toStrictEqual('patch') 45 | expect(figmaMock.clientStorage.setAsync).toBeCalledWith('lastVersionSettingsOpened', '3.1.2') 46 | }) 47 | 48 | test.skip('invers version difference', async () => { 49 | figmaMock.clientStorage.getAsync.mockReturnValue('4.1.1') 50 | // @ts-ignore 51 | const versionDifference = await getVersionDifference(figmaMock) 52 | // expect outcome 53 | expect(versionDifference).toStrictEqual('') 54 | expect(figmaMock.clientStorage.setAsync).not.toBeCalledWith('3.1.2') 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /tests/unit/handleVariableAlias.test.ts: -------------------------------------------------------------------------------- 1 | import handleVariableAlias from '@utils/handleVariableAlias' 2 | 3 | import { tokenExportKeyType } from '@typings/tokenExportKey' 4 | import { tokenTypes } from '@config/tokenTypes' 5 | 6 | import { getVariableTypeByValue } from '@utils/getVariableTypeByValue' 7 | import { changeNotation } from '@utils/changeNotation' 8 | 9 | jest.mock('@utils/getVariableTypeByValue', () => ({ 10 | getVariableTypeByValue: jest.fn() 11 | })) 12 | 13 | jest.mock('@utils/changeNotation', () => ({ 14 | changeNotation: jest.fn() 15 | })) 16 | 17 | describe('handleVariableAlias', () => { 18 | beforeEach(() => { 19 | jest.clearAllMocks() 20 | }) 21 | 22 | beforeAll(() => { 23 | // @ts-ignore 24 | global.figma = { 25 | variables: { 26 | getVariableByIdAsync: jest.fn(), 27 | getVariableCollectionByIdAsync: jest.fn() 28 | } 29 | } 30 | }) 31 | 32 | it('should return the correct object', async () => { 33 | const variable = { description: 'test description' } as any 34 | const value = { id: 'test id' } 35 | const resolvedAlias = { 36 | variableCollectionId: 'test collection id', 37 | name: 'test name', 38 | valuesByMode: { mode1: 'value1' } 39 | } 40 | const collection = { 41 | name: 'test collection name', 42 | modes: 'test modes' 43 | } 44 | 45 | // @ts-ignore 46 | await global.figma.variables.getVariableByIdAsync.mockReturnValue(resolvedAlias) 47 | 48 | // @ts-ignore 49 | getVariableTypeByValue.mockImplementation(() => 'test category') 50 | 51 | // @ts-ignore 52 | changeNotation.mockImplementation(() => 'test notation') 53 | 54 | // @ts-ignore 55 | global.figma.variables.getVariableCollectionByIdAsync.mockReturnValue( 56 | collection 57 | ) 58 | 59 | const result = await handleVariableAlias(variable, value, { modeId: 'passedInModeId', name: 'passedInMode' }) 60 | 61 | expect(result).toEqual({ 62 | description: 'test description', 63 | exportKey: tokenTypes.variables.key as tokenExportKeyType, 64 | category: 'test category', 65 | values: '{test collection name.test notation}', 66 | aliasCollectionName: 'test collection name', 67 | aliasMode: { modeId: 'passedInModeId', name: 'passedInMode' }, 68 | aliasSameMode: false 69 | }) 70 | }) 71 | }) 72 | -------------------------------------------------------------------------------- /tests/unit/prefixTokenName.test.ts: -------------------------------------------------------------------------------- 1 | import { defaultSettings } from '@config/defaultSettings' 2 | import { prefixTokenName } from '@utils/prefixTokenName' 3 | 4 | describe('prefixTokenName', () => { 5 | test('token with alias', () => { 6 | expect(prefixTokenName([ 7 | // @ts-ignore 8 | { 9 | name: 'token/withAlias/red', 10 | category: 'color', 11 | values: '#000000', 12 | extensions: { 13 | 'org.lukasoppermann.figmaDesignTokens': { 14 | exportKey: 'color', 15 | styleId: 31, 16 | alias: 'colors.red' 17 | } 18 | } 19 | } 20 | ], { 21 | ...defaultSettings, 22 | ...{ keyInName: true } 23 | })).toStrictEqual([{ 24 | name: 'color/token/withAlias/red', 25 | category: 'color', 26 | values: '#000000', 27 | extensions: { 28 | 'org.lukasoppermann.figmaDesignTokens': { 29 | exportKey: 'color', 30 | styleId: 31, 31 | alias: 'color.token.colors.red' 32 | } 33 | } 34 | }]) 35 | }) 36 | 37 | test('token no prefix', () => { 38 | expect(prefixTokenName([ 39 | // @ts-ignore 40 | { 41 | name: 'token/full', 42 | category: 'color', 43 | values: '#000000' 44 | } 45 | ], { 46 | ...defaultSettings, 47 | ...{ prefixInName: false } 48 | })).toStrictEqual([{ 49 | name: 'full', 50 | category: 'color', 51 | values: '#000000' 52 | }]) 53 | }) 54 | 55 | test('no tokens', () => { 56 | expect(prefixTokenName([], defaultSettings)).toStrictEqual([]) 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /tests/unit/processAliasModes.test.ts: -------------------------------------------------------------------------------- 1 | import processAliasModes from '@utils/processAliasModes' 2 | 3 | describe('processAliasModes', () => { 4 | it('should return the same variables if they have no alias modes', () => { 5 | const variables = [ 6 | { values: '{color.black}' }, 7 | ] 8 | const result = processAliasModes(variables) 9 | expect(result).toEqual(variables) 10 | }) 11 | 12 | it('should match aliasCollectionName case-insensitively and return the alias collection name', () => { 13 | const variables = [ 14 | { 15 | values: '{CollEctIon.}', 16 | aliasMode: { name: 'mode1' }, 17 | aliasCollectionName: 'collection', 18 | }, 19 | ] 20 | const result = processAliasModes(variables) 21 | expect(result).toMatchInlineSnapshot(` 22 | Array [ 23 | Object { 24 | "aliasCollectionName": "collection", 25 | "aliasMode": Object { 26 | "name": "mode1", 27 | }, 28 | "values": "{CollEctIon.}", 29 | }, 30 | ] 31 | `) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /tests/unit/roundRgba.test.ts: -------------------------------------------------------------------------------- 1 | import { roundRgba } from '@utils/convertColor' 2 | import { rgb, rgba } from './data/color.data' 3 | 4 | describe('Testing roundRgba function', () => { 5 | test('RGB and no opacity', () => { 6 | expect(roundRgba(rgb.raw)).toStrictEqual({ 7 | ...rgb.converted, 8 | ...{ a: 1 } 9 | }) 10 | }) 11 | 12 | test('RGBA', () => { 13 | expect(roundRgba(rgba.raw)).toStrictEqual(rgba.converted) 14 | }) 15 | 16 | test('RGB with opacity', () => { 17 | expect(roundRgba(rgb.raw, .34)).toStrictEqual({ 18 | ...rgb.converted, 19 | ...{ a: .34 } 20 | }) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /tests/unit/roundWithDecimals.test.ts: -------------------------------------------------------------------------------- 1 | import roundWithDecimals from '@utils/roundWithDecimals' 2 | 3 | describe('roundWithDecimals', () => { 4 | test('1.234 to 1 (0 decimal places)', () => { 5 | expect(roundWithDecimals(1.234, 0)).toStrictEqual(1) 6 | }) 7 | 8 | test('1.734 to 2 (0 decimal places)', () => { 9 | expect(roundWithDecimals(1.734, 0)).toStrictEqual(2) 10 | }) 11 | 12 | test('1.234 to 1.234 (4 decimal places)', () => { 13 | expect(roundWithDecimals(1.234, 4)).toStrictEqual(1.234) 14 | }) 15 | 16 | test('1.234 to 1.23 (2 decimal places)', () => { 17 | expect(roundWithDecimals(1.234, 2)).toStrictEqual(1.23) 18 | }) 19 | 20 | test('1.234 to 1.23 (undefined = 2 decimal places)', () => { 21 | expect(roundWithDecimals(1.234, undefined)).toStrictEqual(1.23) 22 | }) 23 | 24 | test('undefined as 1st arg', () => { 25 | // @ts-ignore 26 | expect(roundWithDecimals(undefined, undefined)).toStrictEqual() 27 | }) 28 | 29 | 30 | test('null for 2ng arg decimal placess', () => { 31 | const catchError = () => roundWithDecimals(1.234, null) 32 | expect(catchError).toThrowError(`Invalid parameters, both value "${1.234}" (${typeof 1.234}) and decimalPlaces "${null}" (${typeof null}) must be of type number`) 33 | }) 34 | 35 | test('string as 1st arg', () => { 36 | // @ts-ignore 37 | const catchError = () => roundWithDecimals('string', 2) 38 | expect(catchError).toThrowError(`Invalid parameters, both value "string" (${typeof 'string'}) and decimalPlaces "${2}" (${typeof 2}) must be of type number`) 39 | }) 40 | 41 | test('null as 1st arg', () => { 42 | // @ts-ignore 43 | const catchError = () => roundWithDecimals(null, 2) 44 | expect(catchError).toThrowError(`Invalid parameters, both value "null" (${typeof null}) and decimalPlaces "${2}" (${typeof 2}) must be of type number`) 45 | }) 46 | 47 | test('string for 2ng arg decimal places', () => { 48 | // @ts-ignore 49 | const catchError = () => roundWithDecimals(1.234, 'string') 50 | expect(catchError).toThrowError(`Invalid parameters, both value "1.234" (${typeof 1.234}) and decimalPlaces "string" (${typeof 'string'}) must be of type number`) 51 | }) 52 | 53 | 54 | }) 55 | -------------------------------------------------------------------------------- /tests/unit/semVerDifference.test.ts: -------------------------------------------------------------------------------- 1 | import semVerDifference from '@utils/semVerDifference' 2 | 3 | describe('Testing semVerDifference', () => { 4 | test('Wrong order of version number', () => { 5 | expect(semVerDifference('0.4.0', '0.5.0')).toStrictEqual(undefined) 6 | }) 7 | test('Same version number', () => { 8 | expect(semVerDifference('0.5.0', '0.5.0')).toStrictEqual(undefined) 9 | }) 10 | test('No prev version defined', () => { 11 | expect(semVerDifference('1.0.1')).toStrictEqual('patch') 12 | }) 13 | test('major version increase', () => { 14 | expect(semVerDifference('1.0.0', '0.5.0')).toStrictEqual('major') 15 | }) 16 | test('minor version increase', () => { 17 | expect(semVerDifference('0.6.1', '0.5.0')).toStrictEqual('minor') 18 | }) 19 | test('patch version increase', () => { 20 | expect(semVerDifference('0.5.1', '0.5.0')).toStrictEqual('patch') 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /tests/unit/transformName.test.ts: -------------------------------------------------------------------------------- 1 | import transformName from '@utils/transformName' 2 | 3 | const strings: string[] = [ 4 | ' Foo Bar', 5 | '--foo-bar--', 6 | '__FOO_BAR__-', 7 | 'foo123Bar', 8 | 'foo_Bar', 9 | 'foo.Bar:foo,bar;foo+bar*foo—bar', 10 | 'EquipmentClass name', 11 | 'Equipment className', 12 | 'equipment class name', 13 | ' Equipment Class Name ' 14 | ] 15 | 16 | describe('transformName', () => { 17 | // default case only transforms to lowercase and trims 18 | test('default case', () => { 19 | const transformed = strings.map(string => transformName(string)) 20 | expect(transformed).toStrictEqual([ 21 | 'foo bar', 22 | '--foo-bar--', 23 | '__foo_bar__-', 24 | 'foo123bar', 25 | 'foo_bar', 26 | 'foo.bar:foo,bar;foo+bar*foo—bar', 27 | 'equipmentclass name', 28 | 'equipment classname', 29 | 'equipment class name', 30 | 'equipment class name' 31 | ]) 32 | }) 33 | 34 | // transform to camelCase and trim 35 | test('camelCase', () => { 36 | const transformed = strings.map(string => transformName(string, 'camelCase')) 37 | expect(transformed).toStrictEqual([ 38 | 'fooBar', 39 | 'fooBar', 40 | 'fooBar', 41 | 'foo123bar', 42 | 'fooBar', 43 | 'fooBarFooBarFooBarFooBar', 44 | 'equipmentclassName', 45 | 'equipmentClassname', 46 | 'equipmentClassName', 47 | 'equipmentClassName' 48 | ]) 49 | }) 50 | 51 | // transform to kebab-case and trim 52 | test('kebab-case', () => { 53 | const transformed = strings.map(string => transformName(string, 'kebabCase')) 54 | expect(transformed).toStrictEqual([ 55 | 'foo-bar', 56 | 'foo-bar', 57 | 'foo-bar', 58 | 'foo123bar', 59 | 'foo-bar', 60 | 'foo-bar-foo-bar-foo-bar-foo-bar', 61 | 'equipmentclass-name', 62 | 'equipment-classname', 63 | 'equipment-class-name', 64 | 'equipment-class-name' 65 | ]) 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /tests/unit/transformer.originalFormatTransformer.test.ts: -------------------------------------------------------------------------------- 1 | import { transformer } from '@src/transformer/originalFormatTransformer' 2 | import { extractedFigmaTokens } from './data/extractedFigmaTokens.data' 3 | import { transformedOriginalTokens } from './data/transformedOriginalFormatTokens.data' 4 | 5 | describe('originalFormatTransfomer', () => { 6 | test('size token', () => expect(transformer(extractedFigmaTokens.size, {})).toStrictEqual(transformedOriginalTokens.size)) 7 | test('breakpoint token', () => expect(transformer(extractedFigmaTokens.breakpoint, {})).toStrictEqual(transformedOriginalTokens.breakpoint)) 8 | test('spacing token', () => expect(transformer(extractedFigmaTokens.spacing, {})).toStrictEqual(transformedOriginalTokens.spacing)) 9 | test('radius mixed token', () => expect(transformer(extractedFigmaTokens.radiusMixed, {})).toStrictEqual(transformedOriginalTokens.radiusMixed)) 10 | test('radius single token', () => expect(transformer(extractedFigmaTokens.radiusSingle, {})).toStrictEqual(transformedOriginalTokens.radiusSingle)) 11 | test('grid token', () => expect(transformer(extractedFigmaTokens.grid, {})).toStrictEqual(transformedOriginalTokens.grid)) 12 | test('multi grid token', () => expect(transformer(extractedFigmaTokens.multiGrid, {})).toStrictEqual(transformedOriginalTokens.multiGrid)) 13 | test('font token', () => expect(transformer(extractedFigmaTokens.font, {})).toStrictEqual(transformedOriginalTokens.font)) 14 | test('border token', () => expect(transformer(extractedFigmaTokens.border, {})).toStrictEqual(transformedOriginalTokens.border)) 15 | test('color token', () => expect(transformer(extractedFigmaTokens.color, {})).toStrictEqual(transformedOriginalTokens.color)) 16 | test('multi color token', () => expect(transformer(extractedFigmaTokens.multiColor, {})).toStrictEqual(transformedOriginalTokens.multiColor)) 17 | test('color and gradient', () => expect(transformer(extractedFigmaTokens.colorAndGradient, {})).toStrictEqual(transformedOriginalTokens.colorAndGradient)) 18 | test('gradient and color', () => expect(transformer(extractedFigmaTokens.gradientAndColor, {})).toStrictEqual(transformedOriginalTokens.gradientAndColor)) 19 | test('gradient token', () => expect(transformer(extractedFigmaTokens.gradient, {})).toStrictEqual(transformedOriginalTokens.gradient)) 20 | test('effect token', () => expect(transformer(extractedFigmaTokens.effect, {})).toStrictEqual(transformedOriginalTokens.effect)) 21 | test('multi effect token', () => expect(transformer(extractedFigmaTokens.multiEffect, {})).toStrictEqual(transformedOriginalTokens.multiEffect)) 22 | test('motion token', () => expect(transformer(extractedFigmaTokens.motion, {})).toStrictEqual(transformedOriginalTokens.motion)) 23 | test('opacity token', () => expect(transformer(extractedFigmaTokens.opacity, {})).toStrictEqual(transformedOriginalTokens.opacity)) 24 | }) 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "lib": ["dom", "es2020", "dom.iterable"], 5 | "target": "es6", 6 | "moduleResolution": "node", 7 | "typeRoots": ["./node_modules/@types", "./node_modules/@figma", "./types"], 8 | "esModuleInterop": true, 9 | "baseUrl": ".", 10 | "paths": { 11 | "@components/*": ["src/ui/components/*"], 12 | "@config/*": ["src/config/*"], 13 | "@src/*": ["src/*"], 14 | "@ui/*": ["src/ui/*"], 15 | "@utils/*": ["src/utilities/*"], 16 | "@typings/*": ["types/*"] 17 | } 18 | }, 19 | "include": ["src/**/*", "tests/**/*"], 20 | "exclude": ["src/**/_*"], 21 | "files": ["src/index.ts"] 22 | } 23 | -------------------------------------------------------------------------------- /types/extractedData.d.ts: -------------------------------------------------------------------------------- 1 | import { GridAlignment, GridPattern } from './valueTypes' 2 | 3 | export type extractedGridValues = { 4 | pattern: { 5 | value: GridPattern 6 | }, 7 | sectionSize?: { 8 | value: number, 9 | unit: string 10 | }, 11 | gutterSize?: { 12 | value: number, 13 | unit: string 14 | }, 15 | alignment?: { 16 | value: GridAlignment 17 | }, 18 | count?: { 19 | value: number 20 | }, 21 | offset?: { 22 | value: number, 23 | unit: string 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /types/extractorInterface.d.ts: -------------------------------------------------------------------------------- 1 | import { propertyObject } from './propertyObject' 2 | 3 | type extractorInterface = (tokenNodes, prefixArray?: string[] | { [key: string]: string[] }) => propertyObject[]; 4 | 5 | export default extractorInterface 6 | -------------------------------------------------------------------------------- /types/figmaDataType.d.ts: -------------------------------------------------------------------------------- 1 | import { EffectStyleObject, GridStyleObject, PaintStyleObject, TextStyleObject } from './styles' 2 | import { customTokenNode } from './tokenNodeTypes' 3 | 4 | export type figmaDataType = { 5 | tokenFrames: customTokenNode[], 6 | paintStyles: PaintStyleObject[], 7 | gridStyles: GridStyleObject[], 8 | textStyles: TextStyleObject[], 9 | effectStyles: EffectStyleObject[] 10 | } 11 | -------------------------------------------------------------------------------- /types/originalFormatProperties.d.ts: -------------------------------------------------------------------------------- 1 | import { PropertyType } from './valueTypes' 2 | 3 | export type OriginalFormatPropertyObject = { 4 | value: string | number, 5 | type: PropertyType, 6 | unit?: string 7 | comment?: string, 8 | } 9 | 10 | export type OriginalFormatPropertyGroup = { 11 | name: string, 12 | exportKey: string, 13 | comment?: string, 14 | } & { 15 | [key: string]: OriginalFormatPropertyObject | any 16 | } 17 | 18 | export type OriginalFormatTokenInterface = { 19 | name: string, 20 | exportKey: string, 21 | category: string, 22 | comment?: string, 23 | } & { 24 | [key: string]: OriginalFormatPropertyObject | any 25 | } 26 | -------------------------------------------------------------------------------- /types/pluginEvent.ts: -------------------------------------------------------------------------------- 1 | import { PluginCommands } from '@config/commands' 2 | 3 | export type PluginMessage = { 4 | command: PluginCommands 5 | payload?: any 6 | } 7 | 8 | export type PluginEvent = { 9 | data: { 10 | pluginMessage: PluginMessage 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /types/propertyCategory.d.ts: -------------------------------------------------------------------------------- 1 | export type propertyCategory = 'font' | 'border' | 'size' | 'grid' | 'effect' | 'radius' | 'radius'| 'breakpoint' 2 | -------------------------------------------------------------------------------- /types/settings.ts: -------------------------------------------------------------------------------- 1 | export type nameConversionType = 2 | 'default' | 3 | 'camelCase' | 4 | 'kebabCase' 5 | 6 | export type tokenFormatType = 7 | 'standard' | 8 | 'original' 9 | 10 | export type Settings = { 11 | filename: string, 12 | extension: string, 13 | nameConversion: nameConversionType, 14 | tokenFormat: tokenFormatType, 15 | compression: boolean, 16 | urlJsonCompression: boolean, 17 | serverUrl?: string, 18 | eventType: string, 19 | accessToken?: string, 20 | acceptHeader?: string, 21 | contentType?: string, 22 | exclusionPrefix: string, 23 | excludeExtensionProp: boolean, 24 | alias: string, 25 | authType: string, 26 | reference: string, 27 | keyInName: boolean, 28 | prefixInName: boolean, 29 | modeInTokenValue: boolean, 30 | modeInTokenName: boolean, 31 | resolveSameCollectionOrModeReference: boolean, 32 | prefix: { 33 | color: string, 34 | gradient: string, 35 | font: string, 36 | typography: string, 37 | effect: string, 38 | grid: string, 39 | border: string, 40 | breakpoint: string, 41 | radius: string, 42 | size: string, 43 | spacing: string, 44 | motion: string, 45 | opacity: string 46 | } 47 | exports: { 48 | color: boolean, 49 | gradient: boolean, 50 | font: boolean, 51 | typography: boolean, 52 | effect: boolean, 53 | grid: boolean, 54 | border: boolean, 55 | breakpoint: boolean, 56 | radius: boolean, 57 | size: boolean, 58 | spacing: boolean, 59 | motion: boolean, 60 | opacity: boolean, 61 | variables: boolean 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /types/standardToken.d.ts: -------------------------------------------------------------------------------- 1 | import type { BlendType } from './valueTypes' 2 | 3 | export type customTokenTypes = 'custom-spacing' | 4 | 'custom-radius' | 5 | 'custom-fontStyle' | 6 | 'custom-shadow' | 7 | 'custom-transition' | 8 | 'custom-stroke' | 9 | 'custom-grid' | 10 | 'custom-gradient' | 11 | 'custom-opacity' 12 | 13 | export type StandardTokenTypes = 'string' | 14 | 'number' | 15 | 'object' | 16 | 'array' | 17 | 'boolean' | 18 | 'null' | 19 | 'color' | 20 | 'dimension' | 21 | 'font' | 22 | customTokenTypes 23 | 24 | export type StandardTokenValueType = string | number | Array | Object | Boolean | null 25 | 26 | export type StandardCompositeTokenValueType = { 27 | [key: string]: StandardTokenValueType, 28 | } 29 | 30 | export type StandardTokenGroup = { 31 | [name: string]: { 32 | description?: string 33 | [name: string | number]: StandardTokenDataInterface | string 34 | } 35 | } 36 | 37 | export type pluginExtensionKey = 'org.lukasoppermann.figmaDesignTokens' 38 | 39 | export type StandardTokenExtensionsInterface = { 40 | [key: string | pluginExtensionKey]: any | { 41 | styleId?: string, 42 | exportKey?: string, 43 | category?: string, 44 | group?: string, 45 | unit?: string, 46 | } 47 | } 48 | 49 | export type StandardTokenDataInterface = { 50 | description?: string, 51 | value: StandardTokenValueType | StandardCompositeTokenValueType, 52 | type: StandardTokenTypes, 53 | blendMode?: BlendType, 54 | extensions?: StandardTokenExtensionsInterface 55 | } 56 | 57 | export type StandardTokenInterface = { 58 | name: string 59 | } & StandardTokenDataInterface 60 | -------------------------------------------------------------------------------- /types/styles.d.ts: -------------------------------------------------------------------------------- 1 | export type BaseStyle = { 2 | readonly id: string, 3 | readonly type: StyleType, 4 | name: string, 5 | description: string 6 | } 7 | 8 | type GenericStyleObject = { 9 | name: string, 10 | description: string 11 | } 12 | 13 | export type PaintStyleObject = GenericStyleObject & { 14 | id: string, 15 | paints: any[] 16 | } 17 | 18 | type GridType = 'GRID' | 'ROWS' | 'COLUMNS' 19 | type layoutGrid = { 20 | pattern: GridType, 21 | sectionSize?: number, 22 | gutterSize?: number, 23 | alignment?: string, 24 | count?: any, 25 | offset?: number 26 | } 27 | 28 | export type GridStyleObject = GenericStyleObject & { 29 | layoutGrids: layoutGrid[] 30 | } 31 | 32 | export type TextStyleObject = GenericStyleObject & { 33 | fontSize: number, 34 | textDecoration: TextDecoration, 35 | fontName: FontName, 36 | letterSpacing: LetterSpacing, 37 | lineHeight: LineHeight, 38 | paragraphIndent: number, 39 | paragraphSpacing: number, 40 | textCase: TextCase 41 | } 42 | 43 | export type EffectStyleObject = GenericStyleObject & { 44 | effects: Effect[] 45 | } 46 | -------------------------------------------------------------------------------- /types/tokenCategory.d.ts: -------------------------------------------------------------------------------- 1 | export type tokenCategoryType = 2 | 'color' | 3 | 'gradient' | 4 | 'font' | 5 | 'typography' | 6 | 'effect' | 7 | 'grid' | 8 | 'border' | 9 | 'breakpoint' | 10 | 'radius' | 11 | 'size' | 12 | 'spacing' | 13 | 'motion' | 14 | 'opacity' | 15 | 'string' | 16 | 'boolean' | 17 | 'alias' | 18 | 'dimension' 19 | -------------------------------------------------------------------------------- /types/tokenExportKey.d.ts: -------------------------------------------------------------------------------- 1 | export type tokenExportKeyType = 2 | 'color' | 3 | 'gradient' | 4 | 'font' | 5 | 'typography' | 6 | 'effect' | 7 | 'grid' | 8 | 'border' | 9 | 'breakpoint' | 10 | 'radius' | 11 | 'size' | 12 | 'spacing' | 13 | 'motion' | 14 | 'opacity' | 15 | 'variables' 16 | -------------------------------------------------------------------------------- /types/tokenNodeTypes.d.ts: -------------------------------------------------------------------------------- 1 | import { ColorRgba } from './valueTypes' 2 | 3 | export type customTokenNode = { 4 | name: string, 5 | description?: string, 6 | bottomLeftRadius?: number, 7 | bottomRightRadius?: number, 8 | topLeftRadius?: number, 9 | topRightRadius?: number, 10 | cornerRadius?: number | PluginAPI['mixed'], 11 | cornerSmoothing?: number, 12 | strokes: ColorRgba[], 13 | strokeWeight: number, 14 | strokeStyleId: string, 15 | strokeMiterLimit: number, 16 | strokeJoin: StrokeJoin | PluginAPI['mixed'], 17 | strokeCap: StrokeCap | PluginAPI['mixed'], 18 | dashPattern?: readonly number[], 19 | strokeAlign: 'CENTER' | 'INSIDE' | 'OUTSIDE', 20 | width: number, 21 | height: number, 22 | reactions?: readonly Reaction[], 23 | paddingTop?: number, 24 | paddingRight?: number, 25 | paddingBottom?: number, 26 | paddingLeft?: number, 27 | opacity?: number 28 | } 29 | 30 | export type nodeWithNodeTransition = customTokenNode & { 31 | reactions: readonly { 32 | action: { 33 | readonly type: 'NODE' 34 | readonly destinationId: string | null 35 | readonly navigation: Navigation 36 | readonly transition: Transition | null 37 | readonly preserveScrollPosition: boolean 38 | // Only present if navigation == "OVERLAY" and the destination uses 39 | // overlay position type "RELATIVE" 40 | readonly overlayRelativePosition?: Vector 41 | }, 42 | trigger: Trigger 43 | }[] 44 | } 45 | -------------------------------------------------------------------------------- /types/urlExportData.d.ts: -------------------------------------------------------------------------------- 1 | export type urlExportSettings = { 2 | url: string, 3 | accessToken: string, 4 | acceptHeader: string, 5 | authType: string, 6 | contentType: string, 7 | reference: string 8 | } 9 | 10 | export type urlExportRequestBody = { 11 | 'event_type': string, 12 | 'client_payload': { 13 | tokens: string, 14 | filename: string, 15 | commitMessage: string 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /types/valueTypes.d.ts: -------------------------------------------------------------------------------- 1 | export type ColorRgba = { 2 | r: number, 3 | g: number, 4 | b: number, 5 | a: number 6 | } 7 | 8 | export type BlendType = 'normal' | 'darken' | 'multiply' | 'color_burn' | 'lighten' | 'screen' | 'color_dodge' | 'overlay' | 'soft_light' | 'hard_light' | 'difference' | 'exclusion' | 'hue' | 'saturation' | 'color' | 'luminosity' 9 | 10 | export type GradientType = 'linear' | 'radial' | 'angular' | 'diamond' 11 | 12 | export type UnitTypeDegree = 'degree' 13 | export type UnitTypePixel = 'pixel' 14 | export type UnitTypePercent = 'percent' 15 | export type UnitTypeSeconds = 'ms' 16 | export type NumericUnitTypes = UnitTypeDegree | UnitTypePixel | UnitTypePercent 17 | 18 | export type TextCase = 'none' | 'uppercase' | 'lowercase' | 'capitalize' 19 | export type TextDecoration = 'none' | 'underline' | 'line-through' 20 | export type FontStyle = 'normal' | 'italic' | 'oblique' 21 | export type FontStretch = 'normal' | 'condensed' | 'expanded' 22 | 23 | export type StrokeAlign = 'center' | 'inside' | 'outside' 24 | export type StrokeCap = 'none' | 'round' | 'square' | 'arrow_lines' | 'arrow_equilateral' | 'mixed' 25 | export type StrokeJoin = 'miter' | 'bevel' | 'round' 26 | 27 | export type GridPattern = 'rows' | 'columns' | 'grid' 28 | export type GridAlignment = 'stretch' | 'center' | 'min' | 'max' 29 | 30 | export type EffectType = 'dropShadow' | 'innerShadow' | 'layerBlur' | 'backgroundBlur' 31 | 32 | export type PropertyType = 'number' | 'color' | 'string' 33 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const InlineChunkHtmlPlugin = require('react-dev-utils/InlineChunkHtmlPlugin') 2 | const HtmlWebpackPlugin = require('html-webpack-plugin') 3 | const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin') 4 | const path = require('path') 5 | const webpack = require('webpack') 6 | 7 | module.exports = (env, argv) => ({ 8 | mode: argv.mode === 'production' ? 'production' : 'development', 9 | 10 | // This is necessary because Figma's 'eval' works differently than normal eval 11 | devtool: argv.mode === 'production' ? false : 'inline-source-map', 12 | 13 | entry: { 14 | ui: './src/ui/ui.tsx', // The entry point for your UI code 15 | plugin: './src/index.ts' // The entry point for your plugin code 16 | }, 17 | 18 | module: { 19 | rules: [ 20 | // Converts TypeScript code to JavaScript 21 | { 22 | test: /\.tsx?$/, 23 | use: 'ts-loader', 24 | exclude: /node_modules/ 25 | }, 26 | 27 | // Enables including CSS by doing "import './file.css'" in your TypeScript code 28 | { 29 | test: /\.css$/, 30 | use: ['style-loader', 'css-loader'] 31 | }, 32 | 33 | // Allows you to use "<%= require('./file.svg') %>" in your HTML code to get a data URI 34 | { 35 | test: /\.(png|jpg|gif|webp|svg|zip)$/, 36 | use: 'url-loader' 37 | } 38 | ] 39 | }, 40 | 41 | // Webpack tries these extensions for you if you omit the extension like "import './file'" 42 | resolve: { 43 | extensions: ['.tsx', '.ts', '.jsx', '.js'], 44 | plugins: [new TsconfigPathsPlugin({/* options: see below */})] 45 | }, 46 | 47 | output: { 48 | filename: '[name].js', 49 | path: path.resolve(__dirname, 'dist') // Compile into a folder called "dist" 50 | }, 51 | 52 | // Tells Webpack to generate "ui.html" and to inline "ui.ts" into it 53 | plugins: [ 54 | new webpack.DefinePlugin({ 55 | global: {} // Fix missing symbol error when running in developer VM 56 | }), 57 | new HtmlWebpackPlugin({ 58 | inject: 'body', 59 | template: './src/ui/ui.html', 60 | filename: 'ui.html', 61 | chunks: ['ui'] 62 | }), 63 | new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/ui/]) 64 | ] 65 | }) 66 | --------------------------------------------------------------------------------