├── .gitignore ├── .prettierrc.yml ├── LICENSE.txt ├── README.md ├── manifest.json ├── package.json ├── screenshot.png ├── src ├── app │ ├── common │ │ ├── CustomSelect.tsx │ │ ├── FileUploadDropzone.tsx │ │ ├── Global.styles.ts │ │ ├── RadioButtons.tsx │ │ ├── Section.tsx │ │ └── Spinner.tsx │ ├── components │ │ ├── About │ │ │ └── About.tsx │ │ ├── App.tsx │ │ ├── Axes │ │ │ ├── Axes.tsx │ │ │ └── Axis.tsx │ │ ├── Font │ │ │ ├── Font.tsx │ │ │ └── FontUpload.tsx │ │ ├── Glyphs │ │ │ └── Glyphs.tsx │ │ ├── Info │ │ │ └── Info.tsx │ │ ├── Instances │ │ │ └── Instances.tsx │ │ ├── Layout │ │ │ ├── Footer.tsx │ │ │ ├── Header.tsx │ │ │ └── Layout.tsx │ │ ├── Preview │ │ │ └── Preview.tsx │ │ ├── Style │ │ │ ├── AlignmentSection.tsx │ │ │ ├── ColorSection.tsx │ │ │ └── Style.tsx │ │ ├── Token │ │ │ └── Token.tsx │ │ └── routes.tsx │ ├── consts.ts │ ├── context │ │ └── stateContext.tsx │ ├── declarations.d.ts │ ├── hooks │ │ ├── useFetchFigmaMessages.ts │ │ ├── useGetFontList.ts │ │ ├── useGetHbInstance.ts │ │ └── useGetToken.ts │ ├── index.html │ ├── index.tsx │ ├── store │ │ ├── activeTextSlice.ts │ │ ├── configStore.ts │ │ └── rootReducer.ts │ ├── types.ts │ └── utils │ │ ├── convertHtmlToText.ts │ │ ├── convertTextToSvg.ts │ │ ├── fontUtils.ts │ │ ├── getFontData.ts │ │ ├── googleVariableFontList.ts │ │ └── updateOnFigma.ts ├── libraries │ ├── hb.wasm │ ├── hbjs.js │ └── samsa.js └── plugin │ ├── constants.ts │ ├── controller.ts │ ├── events.ts │ ├── glyphs.ts │ ├── init.ts │ └── utils.ts ├── tsconfig.json ├── variable-fonts-lambda ├── .gitignore ├── .jshintrc ├── .travis.yml ├── Dockerfile ├── LICENSE ├── README.md ├── configs │ ├── dev.yml │ ├── local.yml │ └── prod.yml ├── deploy-policy.json ├── docker-compose.yml ├── handlers │ ├── _not-found.yml │ └── fonts-endpoints.yml ├── modules │ ├── fonts │ │ └── endpoints │ │ │ ├── callback.js │ │ │ ├── delete.js │ │ │ ├── read.js │ │ │ ├── token.js │ │ │ ├── update.js │ │ │ └── upload.js │ └── redirects.js ├── package-lock.json ├── package.json ├── serverless-dynamic.js ├── serverless.yml └── shared │ └── lib │ ├── dynamo.js │ ├── kinesis.js │ ├── lambda.js │ ├── parsers.js │ ├── response.js │ └── uuid.js ├── webpack.config.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | settings.json 4 | yarn-error.log 5 | *.DS_Store 6 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | trailingComma: es5 2 | singleQuote: true 3 | printWidth: 120 4 | tabWidth: 4 5 | bracketSpacing: true 6 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Figma Variable Fonts 2 | 3 | ![Figma Variable Fonts plugin v0.1.0 screenshot](screenshot.png) 4 | 5 | "If you can play [DOOM](https://twitter.com/possan/status/1193164022885081089) in Figma, why not use variable fonts?" - [Lenny](https://twitter.com/rememberlenny) 6 | 7 | A plugin to provide basic variable fonts support through [samsa.js](https://github.com/Lorp/samsa). 8 | 9 | This plugin enables you to: 10 | 11 | - Render variable fonts **as graphics** on the Figma canvas 12 | - Preview, create, and edit/update these graphics 13 | - Choose from all the axes in a variable font 14 | - Choose from all the variable fonts available from Google Fonts 15 | 16 | Near term roadmap features include: 17 | 18 | - Choose from all the named instances in a variable font's fvar and STAT table 19 | 20 | Longer term aims: 21 | 22 | - Create static font instances to download and install, to create 'real' figma Text objects (for now, use [Slice](https://github.com/source-foundry/Slice) instead and do it manually) 23 | - Support OpenType Shaping for Arabic, Indic, South East Asian fonts ([#7](https://github.com/Tgemayel/variable-fonts-figma/issues/7)) 24 | 25 | ## Install 26 | 27 | **1. Obtain a built copy of the plugin.** 28 | 29 | A simple way is to download an release asset zip from this repo's [releases page](https://github.com/Tgemayel/variable-fonts-figma/releases) (`variable-font-plugin-public.zip`) and unzip it. 30 | 31 | (If you download/clone this source code repo (via git), tldr run `yarn`, and see the Development section below for full details!) 32 | 33 | **2. Install the plugin within Figma's desktop client.** 34 | 35 | - Go to Menu → Plugins → Development → New Plugin 36 | - This will bring up the "Create a plugin" modal dialog 37 | - In the lower 2nd section to "Link existing plugin", click to choose a manifest.json file 38 | - Select the `manifest.json` file in the folder you downloaded this plugin to 39 | 40 | ## Usage 41 | 42 | Run the Figma desktop client, and open or create a new file. 43 | 44 | Go to Menu → Plugins → Development → Variable Fonts 45 | 46 | You should see the Variable Fonts palette appear, similar to the screenshot above. 47 | 48 | - Pick one of the dozens of variable fonts available from [Google Fonts](https://fonts.google.com/?vfonly=true). 49 | 50 | - In the Preview section, enter some custom text. 51 | 52 | - Set the color and axes values you want. 53 | 54 | - Then click the **Add** button at the top of the Preview section. 55 | 56 | This will add a Vector graphic object to your Figma canvas. 57 | 58 | That result is a normal figma Vector object that you can do all the normal things you can typically do with such objects. 59 | 60 | It is **NOT** a Text object! 61 | 62 | You can only edit a graphic's text within the plugin window, because the plugin saves some metadata of what the text input was inside that Vector object. 63 | 64 | It can only load variable fonts as TTF files from URLs, not fonts you have installed. 65 | 66 | An "upload any font" feature is planned soon. 67 | 68 | ## Development 69 | 70 | As of April 2021, this plugin is still in an early phase of development - but it is a "MVP," and basically works. 71 | 72 | Please contribute ideas and suggestions to the Github issue tracker: 73 | 74 | [github.com/tgemayel/variable-fonts-figma/issues](http://github.com/tgemayel/variable-fonts-figma/issues) 75 | 76 | #### Contributing Code 77 | 78 | If you are a developer, you may prefer to download the source code from this repo and built it yourself, using [yarn](https://yarnpkg.com), instead of using a pre-built release. 79 | 80 | The dependencies can be conveniently installed on macOS with [Homebrew](https://brew.sh) and on Windows with [Chocolatey](https://chocolatey.org). 81 | If you use Windows, replace the following `brew` command with `choco`. 82 | 83 | To install Homebrew, open Terminal and run these 2 commands: 84 | 85 | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"; 86 | brew doctor; 87 | 88 | You will be offered to install the Command Line Developer Tools from Apple. 89 | Confirm by clicking Install. 90 | After the installation finishes, continue installing Homebrew by hitting Return again. 91 | 92 | Then install git, yarn, and the Figma desktop client, download this repo, and build it: 93 | 94 | brew install git yarn figma; 95 | git clone https://github.com/Tgemayel/variable-fonts-figma.git; 96 | cd variable-fonts-figma; 97 | yarn; 98 | yarn build:watch; 99 | 100 | The final command means that if you update the source code, it will rebuild automatically on save. 101 | Just close the plugin and reopen it to see how your changes work. 102 | 103 | Pull requests are welcome! 104 | 105 | ## License 106 | 107 | Apache License 2.0 108 | 109 | ## Thanks 110 | 111 | Thanks to [Dave Crossland](https://twitter.com/davelab6) for organizing this project, [Lenny Bogonoff](https://twitter.com/rememberlenny) for initial prototyping, and the Google Fonts team for generously supporting development. 112 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Variable Fonts", 3 | "id": "00000000", 4 | "api": "1.0.0", 5 | "main": "dist/code.js", 6 | "ui": "dist/ui.html", 7 | "build": "yarn" 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "figma-variable-fonts", 3 | "version": "1.1.0", 4 | "description": "This plugin allows you to add/edit variable fonts on the Figma canvas.", 5 | "license": "ISC", 6 | "scripts": { 7 | "build": "webpack --mode=production", 8 | "build:watch": "webpack --mode=development --watch", 9 | "prettier:format": "prettier --write 'src/**/*.{js,jsx,ts,tsx,css,json}' " 10 | }, 11 | "keywords": [ 12 | "variable-fonts", 13 | "figma", 14 | "figma-plugins" 15 | ], 16 | "dependencies": { 17 | "dotenv-webpack": "^7.0.3", 18 | "opentype.js": "^1.3.3", 19 | "rc-slider": "^9.7.1", 20 | "react": "^17.0.1", 21 | "react-color": "^2.19.3", 22 | "react-contenteditable": "^3.3.5", 23 | "react-dom": "^17.0.1", 24 | "react-figma-plugin-ds": "^2.0.5", 25 | "react-outside-click-handler": "^1.3.0", 26 | "react-router-config": "^5.1.1", 27 | "react-router-dom": "^5.2.0", 28 | "striptags": "^3.1.1", 29 | "styled-components": "^5.2.1" 30 | }, 31 | "devDependencies": { 32 | "@figma/plugin-typings": "^1.19.0", 33 | "@reduxjs/toolkit": "^1.5.0", 34 | "@types/node": "^14.14.35", 35 | "@types/react": "^17.0.0", 36 | "@types/react-dom": "^17.0.0", 37 | "@types/react-redux": "^7.1.16", 38 | "css-loader": "^5.0.1", 39 | "file-loader": "^6.2.0", 40 | "html-webpack-inline-source-plugin": "^0.0.10", 41 | "html-webpack-plugin": "^3.2.0", 42 | "husky": "^4.3.0", 43 | "lint-staged": "^10.5.1", 44 | "prettier": "^2.2.0", 45 | "react-redux": "^7.2.2", 46 | "redux": "^4.0.5", 47 | "style-loader": "^2.0.0", 48 | "ts-loader": "^8.0.11", 49 | "typescript": "^4.1.2", 50 | "url-loader": "^4.1.1", 51 | "webassembly-loader": "^1.1.0", 52 | "webpack": "^4.41.4", 53 | "webpack-cli": "^3.3.6" 54 | }, 55 | "husky": { 56 | "hooks": { 57 | "pre-commit": "lint-staged" 58 | } 59 | }, 60 | "lint-staged": { 61 | "src/**/*.{js,jsx,ts,tsx,css,json}": [ 62 | "prettier --write", 63 | "git add" 64 | ] 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tgemayel/variable-fonts-figma/46c2d000e8b0a03d2dd39364bb39fb1f94a4dca1/screenshot.png -------------------------------------------------------------------------------- /src/app/common/CustomSelect.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Icon } from 'react-figma-plugin-ds'; 3 | import OutsideClickHandler from 'react-outside-click-handler'; 4 | import styled from 'styled-components'; 5 | 6 | export interface BasicProps { 7 | className?: string; 8 | } 9 | 10 | export interface SelectOption { 11 | divider?: string | boolean; 12 | value: string; 13 | label: string; 14 | } 15 | 16 | export interface SelectProps extends BasicProps { 17 | options: SelectOption[]; 18 | isDisabled?: boolean; 19 | defaultValue?: string | number | boolean; 20 | icon: string; 21 | onExpand?: (state: boolean) => void; 22 | onChange?: (option: SelectOption) => void; 23 | } 24 | 25 | const CustomSelect: React.FunctionComponent = ({ 26 | className = '', 27 | options, 28 | isDisabled, 29 | defaultValue, 30 | icon, 31 | onExpand, 32 | onChange, 33 | }) => { 34 | const [isExpanded, onExpandedStateChange] = React.useState(false); 35 | const [selectedOption, onSelectOption] = React.useState(options.find(({ value }) => defaultValue === value)); 36 | React.useEffect(() => { 37 | onExpand && onExpand(isExpanded); 38 | }, [isExpanded]); 39 | React.useEffect(() => { 40 | onChange && selectedOption && onChange(selectedOption); 41 | }, [selectedOption]); 42 | React.useEffect(() => { 43 | const newSelectedOption = options.find(({ value }) => defaultValue === value); 44 | onSelectOption(newSelectedOption); 45 | }, [defaultValue]); 46 | 47 | const handleExpandClick = () => onExpandedStateChange(!isExpanded); 48 | 49 | const handleOutsideClick = () => onExpandedStateChange(false); 50 | 51 | const handleSelectClick = (value: any) => { 52 | const newOption = options.find(({ value: v }) => v === value); 53 | onExpandedStateChange(false); 54 | onSelectOption(newOption); 55 | }; 56 | 57 | const expanListClass = isExpanded ? 'select-menu__menu--active' : ''; 58 | const disabledColorClass = isDisabled ? 'icon--black-3' : ''; 59 | 60 | return ( 61 | 62 |
63 | 64 | 65 | 66 |
    70 | {options && 71 | options.map(({ value, label, divider }, i) => 72 | divider ? ( 73 | 74 | {divider !== true && {divider}} 75 |
    76 | 77 | ) : ( 78 |
  • handleSelectClick(value)} 85 | key={`select-option--${i}`} 86 | > 87 | {label} 88 |
  • 89 | ) 90 | )} 91 |
92 |
93 |
94 | ); 95 | }; 96 | 97 | const IconButton = styled.div` 98 | .icon { 99 | margin: 0; 100 | opacity: 1; 101 | } 102 | `; 103 | export default CustomSelect; 104 | -------------------------------------------------------------------------------- /src/app/common/FileUploadDropzone.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styled from 'styled-components'; 3 | import { OnSelectedFiles } from '../types'; 4 | 5 | export interface FileUploadDropzoneProps { 6 | acceptedFileTypes?: string[]; 7 | multiple?: boolean; 8 | onSelectedFiles: OnSelectedFiles; 9 | children?: React.ReactNode; 10 | } 11 | 12 | const FileUploadDropzone = ({ acceptedFileTypes, onSelectedFiles, multiple, children }: FileUploadDropzoneProps) => { 13 | const [isDropActive, setIsDropActive] = React.useState(false); 14 | 15 | const filterFiles = React.useCallback( 16 | function (files: FileList): Array { 17 | const result = Array.prototype.slice.call(files).sort(comparator); 18 | if (typeof acceptedFileTypes === 'undefined') { 19 | return result; 20 | } 21 | return result.filter(function (file) { 22 | return acceptedFileTypes.indexOf(file.name.split('.').pop()) !== -1; 23 | }); 24 | }, 25 | [acceptedFileTypes] 26 | ); 27 | 28 | const handleChange = React.useCallback( 29 | function (event: Event): void { 30 | const files = (event.target as HTMLInputElement).files; 31 | if (files === null) { 32 | return; 33 | } 34 | onSelectedFiles(filterFiles(files), event); 35 | }, 36 | [filterFiles, onSelectedFiles] 37 | ); 38 | const handleDragEnter = React.useCallback(function (event: DragEvent): void { 39 | event.preventDefault(); 40 | }, []); 41 | const handleDragOver = React.useCallback(function (event: DragEvent): void { 42 | event.preventDefault(); 43 | setIsDropActive(true); 44 | }, []); 45 | const handleDragEnd = React.useCallback(function (event: DragEvent): void { 46 | event.preventDefault(); 47 | setIsDropActive(false); 48 | }, []); 49 | const handleDrop = React.useCallback( 50 | function (event: DragEvent): void { 51 | event.preventDefault(); 52 | if (event.dataTransfer === null) { 53 | return; 54 | } 55 | const files = filterFiles(event.dataTransfer.files); 56 | onSelectedFiles(files, event); 57 | setIsDropActive(false); 58 | }, 59 | [filterFiles, onSelectedFiles] 60 | ); 61 | 62 | return ( 63 | 64 | 74 | 75 | {children} 76 | 77 | ); 78 | }; 79 | 80 | const DropzoneWrapper = styled.div` 81 | position: relative; 82 | width: 100%; 83 | padding: 32px 0; 84 | cursor: pointer; 85 | `; 86 | 87 | const Dropzone = styled.input` 88 | position: absolute; 89 | top: 0; 90 | right: 0; 91 | bottom: 0; 92 | left: 0; 93 | width: 100%; 94 | opacity: 0; 95 | `; 96 | 97 | const DashLine = styled.div` 98 | position: absolute; 99 | top: 0; 100 | right: 0; 101 | bottom: 0; 102 | left: 0; 103 | 104 | border: 1px dashed gray; 105 | pointer-events: none; 106 | `; 107 | const Child = styled.div` 108 | text-align: center; 109 | `; 110 | 111 | function comparator(a: File, b: File) { 112 | const aName = a.name.toLowerCase(); 113 | const bName = b.name.toLowerCase(); 114 | if (aName !== bName) { 115 | return aName.localeCompare(bName); 116 | } 117 | return a.lastModified - b.lastModified; 118 | } 119 | 120 | export default FileUploadDropzone; 121 | -------------------------------------------------------------------------------- /src/app/common/Global.styles.ts: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from 'styled-components'; 2 | 3 | export default createGlobalStyle` 4 | html, body, div, span, applet, object, iframe, 5 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, 6 | abbr, acronym, address, big, cite, code, del, dfn, em, 7 | font, img, ins, kbd, q, s, samp, small, strike, strong, 8 | sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, 9 | fieldset, form, label, legend, table, caption, tbody, tfoot, 10 | thead, tr, th, td, input, textarea, keygen, select, button { 11 | margin: 0; 12 | padding: 0; 13 | outline: 0; 14 | border: 0; 15 | text-rendering: optimizeLegibility; 16 | -webkit-font-smoothing: antialiased; 17 | -webkit-text-size-adjust: 100%; 18 | } 19 | 20 | button { 21 | cursor: pointer; 22 | } 23 | 24 | .disclosure__content { 25 | user-select: all; 26 | pointer-events: all; 27 | } 28 | `; 29 | -------------------------------------------------------------------------------- /src/app/common/RadioButtons.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Text } from 'react-figma-plugin-ds'; 3 | import styled from 'styled-components'; 4 | 5 | export interface RadioButtonsProps { 6 | disabled?: boolean; 7 | focused?: boolean; 8 | name: string; 9 | onChange: (value) => void; 10 | options: RadioButtonsOption[]; 11 | propagateEscapeKeyDown?: boolean; 12 | value: null | string; 13 | } 14 | 15 | interface RadioButtonsOption { 16 | disabled?: boolean; 17 | text: string; 18 | value: null | string; 19 | } 20 | 21 | const RadioButtons = ({ disabled, focused, name, onChange, options, value }: RadioButtonsProps) => { 22 | const handleChange = React.useCallback( 23 | (event: React.ChangeEvent) => { 24 | const index = (event.target as HTMLElement).getAttribute('data-index'); 25 | if (index === null) { 26 | return; 27 | } 28 | const newValue = options[parseInt(index)].value; 29 | onChange(newValue); 30 | }, 31 | [name, onChange, options] 32 | ); 33 | 34 | return ( 35 | 36 | {options.map(function (option, index) { 37 | const text = typeof option.text === 'undefined' ? option.value : option.text; 38 | const isOptionDisabled = disabled === true || option.disabled === true; 39 | return ( 40 | 41 | 52 | 53 | {text} 54 | 55 | 56 | ); 57 | })} 58 | 59 | ); 60 | }; 61 | 62 | export default RadioButtons; 63 | 64 | const RadioButtonsWrapper = styled.div` 65 | display: flex; 66 | `; 67 | 68 | const RadioButton = styled.div` 69 | flex: 1; 70 | display: flex; 71 | align-items: center; 72 | `; 73 | 74 | const RadioInput = styled.input` 75 | margin-right: 0.25rem; 76 | `; 77 | 78 | const RadioLabel = styled.div``; 79 | -------------------------------------------------------------------------------- /src/app/common/Section.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Text } from 'react-figma-plugin-ds'; 3 | import styled from 'styled-components'; 4 | 5 | interface Props { 6 | label: string; 7 | children?: React.ReactNode | React.ReactNode[]; 8 | } 9 | 10 | const Section = ({ label, children }: Props) => { 11 | return ( 12 | 13 | 14 | 15 | {label} 16 | 17 | 18 | {children} 19 | 20 | ); 21 | }; 22 | 23 | const SectionWrapper = styled.div` 24 | padding: 0.625rem; 25 | `; 26 | 27 | const LabelWrapper = styled.div` 28 | margin-bottom: 0.5rem; 29 | `; 30 | 31 | export default Section; 32 | -------------------------------------------------------------------------------- /src/app/common/Spinner.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const Spinner = () => { 5 | return ( 6 | 7 | 8 | 12 | 20 | 21 | 22 | 23 | ); 24 | }; 25 | 26 | const SpinnerWrapper = styled.div` 27 | width: 100%; 28 | height: 100%; 29 | display: flex; 30 | align-items: center; 31 | justify-content: center; 32 | `; 33 | 34 | export default Spinner; 35 | -------------------------------------------------------------------------------- /src/app/components/About/About.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styled from 'styled-components'; 3 | import { Text } from 'react-figma-plugin-ds'; 4 | import Section from '../../common/Section'; 5 | 6 | const About = () => { 7 | return ( 8 |
9 | 10 | 11 | Variable Fonts plugin is developed by Toni Gemayel. The 12 | variable fonts rendering engine is based on samsa.js by{' '} 13 | Laurence Penney. 14 | 15 | 16 |
17 | ); 18 | }; 19 | 20 | const Content = styled.div` 21 | padding-top: 1rem; 22 | padding-bottom: 1rem; 23 | `; 24 | 25 | export default About; 26 | -------------------------------------------------------------------------------- /src/app/components/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { HashRouter } from 'react-router-dom'; 3 | import { route } from './routes'; 4 | 5 | import Spinner from '../common/Spinner'; 6 | import useFetchFigmaMessages from '../hooks/useFetchFigmaMessages'; 7 | import useGetFontList from '../hooks/useGetFontList'; 8 | import useGetHbInstance from '../hooks/useGetHbInstance'; 9 | import GlobalStyles from '../common/Global.styles'; 10 | import useGetToken from '../hooks/useGetToken'; 11 | 12 | const App = ({}) => { 13 | useGetToken(); 14 | useGetHbInstance(); 15 | useFetchFigmaMessages(); 16 | 17 | const { loading } = useGetFontList(); 18 | 19 | return ( 20 | <> 21 | 22 | {loading ? : } 23 | 24 | ); 25 | }; 26 | 27 | export default App; 28 | -------------------------------------------------------------------------------- /src/app/components/Axes/Axes.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Button, Text } from 'react-figma-plugin-ds'; 3 | import { useSelector } from 'react-redux'; 4 | import styled from 'styled-components'; 5 | import { useAppState } from '../../context/stateContext'; 6 | import { RootState } from '../../store/rootReducer'; 7 | import { IAxis } from '../../types'; 8 | import Axis from './Axis'; 9 | 10 | const Axes = () => { 11 | const { fonts } = useAppState(); 12 | const { fontName, axes } = useSelector((state: RootState) => state.activeText); 13 | 14 | const handleAdd = React.useCallback(() => {}, []); 15 | 16 | if (!fontName) return <>; 17 | 18 | const totalAxes = fonts[fontName].axes; 19 | 20 | const displayAxes = () => { 21 | return totalAxes.map((axis: IAxis, index: number) => { 22 | const { min, max, tag, name } = axis; 23 | return ( 24 |
25 | 26 |
27 | ); 28 | }); 29 | }; 30 | 31 | return ( 32 | 33 | 34 | 35 | Axes 36 | 37 | 40 | 41 | {displayAxes()} 42 | 43 | ); 44 | }; 45 | 46 | const SectionWrapper = styled.div` 47 | padding: 0.625rem; 48 | `; 49 | 50 | const SectionTitle = styled.div` 51 | display: flex; 52 | align-items: center; 53 | justify-content: space-between; 54 | margin-bottom: 1rem; 55 | `; 56 | 57 | export default Axes; 58 | -------------------------------------------------------------------------------- /src/app/components/Axes/Axis.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Text } from 'react-figma-plugin-ds'; 3 | import styled from 'styled-components'; 4 | import Slider from 'rc-slider'; 5 | 6 | import 'rc-slider/assets/index.css'; 7 | import { useDispatch, useSelector } from 'react-redux'; 8 | import { RootState } from '../../store/rootReducer'; 9 | import { updateActiveFontAxis } from '../../store/activeTextSlice'; 10 | import { useAppState } from '../../context/stateContext'; 11 | import { IActiveAxes } from '../../types'; 12 | import asyncUpdateFigma from '../../utils/updateOnFigma'; 13 | 14 | interface Props { 15 | tag: string; 16 | name: string; 17 | value: number; 18 | min: number; 19 | max: number; 20 | } 21 | 22 | const Axis = ({ tag, name, value, min, max }: Props) => { 23 | const dispatch = useDispatch(); 24 | 25 | const { fontName, content } = useSelector((state: RootState) => state.activeText); 26 | const { hbInstance, fonts, activeColor, activeAxes, setActiveAxes } = useAppState(); 27 | 28 | const onChange = React.useCallback( 29 | (val) => { 30 | dispatch( 31 | updateActiveFontAxis({ 32 | tag, 33 | value: val, 34 | }) 35 | ); 36 | }, 37 | [updateActiveFontAxis] 38 | ); 39 | 40 | const onAfterChange = React.useCallback( 41 | (val) => { 42 | setActiveAxes((prev: IActiveAxes) => ({ 43 | ...prev, 44 | [tag]: val, 45 | })); 46 | asyncUpdateFigma( 47 | hbInstance, 48 | fonts[fontName].fontUrl, 49 | fontName, 50 | content, 51 | { ...activeAxes, [tag]: val }, 52 | activeColor 53 | ); 54 | }, 55 | [hbInstance, fonts, fontName, content, activeAxes, activeColor, setActiveAxes] 56 | ); 57 | 58 | return ( 59 | 60 | 61 | 62 | 63 | {tag} 64 | 65 | 66 | 67 | 68 | {name} 69 | 70 | 71 | 72 | {value} 73 | 74 | 75 | 76 | 84 | 85 | 86 | ); 87 | }; 88 | 89 | const AxesWrapper = styled.div` 90 | height: 64px; 91 | `; 92 | 93 | const AxesLabels = styled.div` 94 | display: flex; 95 | align-items: center; 96 | `; 97 | 98 | const LabelWrapper = styled.div` 99 | width: 60px; 100 | `; 101 | 102 | const TypeWrapper = styled.div` 103 | flex: 1; 104 | `; 105 | 106 | const SliderRow = styled.div` 107 | margin-top: 0.5rem; 108 | 109 | .rangeslider.rangeslider-horizontal, 110 | .rangeslider-horizontal .rangeslider__fill { 111 | background: rgba(0, 0, 0, 0.8); 112 | } 113 | .rangeslider, 114 | .rangeslider .rangeslider__fill { 115 | box-shadow: none; 116 | } 117 | .rangeslider-horizontal { 118 | height: 1px; 119 | border-radius: 10px; 120 | } 121 | .rangeslider { 122 | margin: 0px; 123 | } 124 | .rangeslider-horizontal .rangeslider__handle { 125 | width: 8px; 126 | height: 8px; 127 | border-radius: 8px; 128 | outline: 2px solid white; 129 | } 130 | .rangeslider .rangeslider__handle { 131 | background: #000; 132 | box-shadow: none; 133 | } 134 | .rangeslider__handle:focus { 135 | background: #18a0fb; 136 | } 137 | `; 138 | 139 | export default Axis; 140 | -------------------------------------------------------------------------------- /src/app/components/Font/Font.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styled from 'styled-components'; 3 | import { Select } from 'react-figma-plugin-ds'; 4 | import Section from '../../common/Section'; 5 | import { IFigmaSelectOption } from '../../types'; 6 | import { updateActiveFont } from '../../store/activeTextSlice'; 7 | import { useDispatch, useSelector } from 'react-redux'; 8 | import { useAppState } from '../../context/stateContext'; 9 | import { RootState } from '../../store/rootReducer'; 10 | import { DEFAULT_COLOR } from '../../consts'; 11 | import FontUpload from './FontUpload'; 12 | 13 | const Font = () => { 14 | const { fonts, setActiveAxes, setActiveColor, setActiveInstance } = useAppState(); 15 | const { fontName } = useSelector((state: RootState) => state.activeText); 16 | const dispatch = useDispatch(); 17 | 18 | const onChange = React.useCallback( 19 | (data: IFigmaSelectOption) => { 20 | const font = fonts[data.value]; 21 | 22 | if (data.value !== fontName) { 23 | const axes = {}; 24 | font.axes.forEach((axis) => { 25 | axes[axis.tag] = axis.default; 26 | }); 27 | dispatch( 28 | updateActiveFont({ 29 | fontName: font.fontName, 30 | axes, 31 | color: DEFAULT_COLOR, 32 | }) 33 | ); 34 | setActiveAxes(axes); 35 | setActiveColor(DEFAULT_COLOR); 36 | setActiveInstance(font.instances.find((instance) => instance.type === 'default')); 37 | } 38 | }, 39 | [fonts, fontName] 40 | ); 41 | 42 | const fontList = React.useMemo( 43 | () => 44 | Object.keys(fonts).map((fontName: string) => ({ 45 | label: fonts[fontName].fontFamilyName || fontName, 46 | value: fontName, 47 | })), 48 | [fonts] 49 | ); 50 | 51 | return ( 52 | <> 53 |
54 | 55 | 129 | 130 | ) : ( 131 | 132 | 133 | {files.length ? ( 134 | files.map((file) => {file.name}) 135 | ) : ( 136 | <> 137 | 138 | Drop a variable font here

or click to select a variable font from your 139 | computer 140 |
141 | 142 | Supported formats: TTF 143 | 144 | 145 | )} 146 |
147 |
148 | )} 149 | 150 | {error && ( 151 | 152 | 153 | * Only TTF files are supported for now. 154 | 155 | 156 | )} 157 | 158 | 159 | setOwnership(!ownership)} 162 | type="checkbox" 163 | /> 164 | 165 | 166 | 167 | 170 | 171 | 172 | {!accessToken.length && ( 173 | 174 | 175 | To enable this feature,
you should Request a token first. 176 |
177 |
178 | )} 179 | 180 | ); 181 | }; 182 | 183 | const Wrapper = styled.div` 184 | margin-top: 1rem; 185 | `; 186 | 187 | const ButtonWrapper = styled.div` 188 | margin-top: 1rem; 189 | 190 | .button { 191 | width: 100%; 192 | justify-content: center; 193 | } 194 | `; 195 | 196 | const ErrorWrapper = styled.div` 197 | margin-top: 0.25rem; 198 | color: red; 199 | text-align: center; 200 | `; 201 | 202 | export default FontUpload; 203 | -------------------------------------------------------------------------------- /src/app/components/Glyphs/Glyphs.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Text } from 'react-figma-plugin-ds'; 3 | import { useSelector } from 'react-redux'; 4 | import styled from 'styled-components'; 5 | import { FIGMA_EVENT_TYPES } from '../../../plugin/constants'; 6 | import Section from '../../common/Section'; 7 | import { useAppState } from '../../context/stateContext'; 8 | import { RootState } from '../../store/rootReducer'; 9 | import { convertGlyphToSvg } from '../../utils/convertTextToSvg'; 10 | 11 | const Glyphs = () => { 12 | const fontName = useSelector((state: RootState) => state.activeText.fontName); 13 | const { hbInstance, fonts, activeAxes, activeColor } = useAppState(); 14 | 15 | const font = React.useMemo(() => { 16 | if (fontName) { 17 | return fonts[fontName]; 18 | } else { 19 | return null; 20 | } 21 | }, [fontName]); 22 | 23 | const onClick = React.useCallback( 24 | (glyph) => { 25 | const asyncLoad = async function () { 26 | const svgPathData = await convertGlyphToSvg(hbInstance, font.fontUrl, glyph.id, { 27 | variations: activeAxes, 28 | }); 29 | const codePoints = [font.glyphsIndexMap[glyph.id].unicode]; 30 | window.parent.postMessage( 31 | { 32 | pluginMessage: { 33 | type: FIGMA_EVENT_TYPES.RENDER_SVG, 34 | payload: { 35 | paths: svgPathData, 36 | fontName: fontName, 37 | axes: activeAxes, 38 | codePoints, 39 | color: activeColor, 40 | }, 41 | }, 42 | }, 43 | '*' 44 | ); 45 | }; 46 | asyncLoad(); 47 | }, 48 | [font, hbInstance, activeAxes] 49 | ); 50 | 51 | if (!fontName) return <>; 52 | 53 | return ( 54 |
55 | 56 | {font && 57 | font.glyphs.map((glyph) => { 58 | const scale = 50 / font.unitsPerEm; 59 | 60 | let glyphName = glyph.name || 'undefined'; 61 | let iglyph; 62 | if (glyph.numContours < 0) iglyph = glyph.decompose(font.instances[0].tuple); 63 | else iglyph = glyph.instantiate(null, font.instances[0]); 64 | 65 | return ( 66 | onClick(glyph)}> 67 | 68 | {glyphName} 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | ); 77 | })} 78 | 79 |
80 | ); 81 | }; 82 | 83 | const GlyphWrapper = styled.div` 84 | display: flex; 85 | flex-wrap: wrap; 86 | justify-content: center; 87 | `; 88 | 89 | const Glyph = styled.div` 90 | display: inline-block; 91 | :hover { 92 | background-color: rgba(0, 0, 0, 0.1); 93 | } 94 | `; 95 | 96 | export default Glyphs; 97 | -------------------------------------------------------------------------------- /src/app/components/Info/Info.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Text } from 'react-figma-plugin-ds'; 3 | import Section from '../../common/Section'; 4 | 5 | const Info = () => { 6 | return ( 7 |
8 | 9 | Family name: Roboto Extremo 10 | 11 |
12 | ); 13 | }; 14 | 15 | export default Info; 16 | -------------------------------------------------------------------------------- /src/app/components/Instances/Instances.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styled from 'styled-components'; 3 | import { Input, Select, Text } from 'react-figma-plugin-ds'; 4 | import { useDispatch, useSelector } from 'react-redux'; 5 | import Section from '../../common/Section'; 6 | import { useAppState } from '../../context/stateContext'; 7 | import { RootState } from '../../store/rootReducer'; 8 | import { IFigmaSelectOption, IInstance } from '../../types'; 9 | import { updateActiveFontAxes } from '../../store/activeTextSlice'; 10 | import asyncUpdateFigma from '../../utils/updateOnFigma'; 11 | 12 | const Instances = () => { 13 | const dispatch = useDispatch(); 14 | const { fontName, content } = useSelector((state: RootState) => state.activeText); 15 | const { hbInstance, fonts, activeColor, activeInstance, setActiveInstance, setActiveAxes } = useAppState(); 16 | 17 | const namedInstances = fontName ? fonts[fontName].instances : []; 18 | const namedInstanceList = React.useMemo( 19 | () => namedInstances.map((instance: IInstance) => ({ label: instance.name, value: instance.id })), 20 | [namedInstances] 21 | ); 22 | 23 | const onChange = React.useCallback( 24 | (data: IFigmaSelectOption) => { 25 | const instance = namedInstances.find((instance) => instance.id === data.value); 26 | setActiveInstance(instance); 27 | setActiveAxes(instance.fvs); 28 | dispatch(updateActiveFontAxes(instance.fvs)); 29 | 30 | asyncUpdateFigma(hbInstance, fonts[fontName].fontUrl, fontName, content, instance.fvs, activeColor); 31 | }, 32 | [namedInstances, hbInstance, fonts, fontName, content, activeColor, setActiveInstance, setActiveAxes] 33 | ); 34 | 35 | const onChangeName = React.useCallback(() => {}, []); 36 | 37 | if (!fontName) return <>; 38 | 39 | return ( 40 |
41 | 42 | 57 | 58 | 59 | 60 | Axes: 61 | 62 | 63 | {Object.keys(activeInstance.fvs).map((axis) => { 64 | return ( 65 | 66 | 67 | {axis} 68 | 69 | 70 | {activeInstance.fvs[axis]} 71 | 72 | 73 | ); 74 | })} 75 | 76 | 77 | 78 | )} 79 |
80 | ); 81 | }; 82 | 83 | const InstanceListWrapper = styled.div` 84 | margin-top: 0.5rem; 85 | 86 | .instance-select .select-menu__button { 87 | justify-content: space-between; 88 | border-color: var(--black1); 89 | } 90 | .instance-select .select-menu__label { 91 | font-size: var(--font-size-large); 92 | } 93 | .instance-select .select-menu__menu { 94 | max-height: 350px; 95 | } 96 | `; 97 | 98 | const InstanceInfoWrapper = styled.div``; 99 | 100 | const InstanceName = styled.div` 101 | display: flex; 102 | align-items: center; 103 | margin-top: 0.5rem; 104 | margin-bottom: 0.5rem; 105 | 106 | .instance-name { 107 | width: 3rem; 108 | } 109 | `; 110 | 111 | const InstanceAxesWrapper = styled.div` 112 | display: flex; 113 | align-items: center; 114 | 115 | .instance-name { 116 | width: 3rem; 117 | } 118 | `; 119 | 120 | const InstanceAxes = styled.div` 121 | display: flex; 122 | align-items: center; 123 | flex-wrap: wrap; 124 | `; 125 | 126 | const InstanceAxis = styled.div` 127 | margin-left: 0.5rem; 128 | margin-right: 0.5rem; 129 | width: 40px; 130 | `; 131 | 132 | export default Instances; 133 | -------------------------------------------------------------------------------- /src/app/components/Layout/Footer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styled from 'styled-components'; 3 | import { Text } from 'react-figma-plugin-ds'; 4 | 5 | const Footer = () => { 6 | return ( 7 | 8 | 9 | @ 2021 Variable Fonts {process.env.REACT_APP_VERSION} 10 | 11 | 12 | ); 13 | }; 14 | 15 | const Wrapper = styled.div` 16 | border-top: 1px solid #e5e5e5; 17 | padding: 0.625rem; 18 | `; 19 | 20 | export default Footer; 21 | -------------------------------------------------------------------------------- /src/app/components/Layout/Header.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styled from 'styled-components'; 3 | import { useLocation, useHistory } from 'react-router-dom'; 4 | import { Text } from 'react-figma-plugin-ds'; 5 | import CustomSelect from '../../common/CustomSelect'; 6 | import { routeMap } from '../routes'; 7 | 8 | const Header = () => { 9 | const location = useLocation(); 10 | const history = useHistory(); 11 | 12 | const onClick = (path: string) => { 13 | if (location.pathname !== path) history.push(path); 14 | }; 15 | 16 | const tabs = routeMap.filter((route) => route.tab === true); 17 | const settings = routeMap 18 | .filter((route) => route.settings === true) 19 | .map((route) => ({ label: route.title, value: route.path })); 20 | 21 | return ( 22 | 23 | 24 | {tabs.map((item, index) => ( 25 | onClick(item.path)} 29 | > 30 | 31 | {item.title} 32 | 33 | 34 | ))} 35 | 36 | 37 | onClick(option.value)} options={settings} /> 38 | 39 | 40 | ); 41 | }; 42 | 43 | const Wrapper = styled.div` 44 | padding: 0.625rem; 45 | border-bottom: 1px solid #e5e5e5; 46 | display: flex; 47 | align-items: center; 48 | justify-content: start; 49 | `; 50 | 51 | const Tabs = styled.div` 52 | flex: 1; 53 | `; 54 | 55 | const Tab = styled.span` 56 | cursor: pointer; 57 | color: ${(props) => (props.isActive ? 'black' : 'gray')}; 58 | padding-right: 1rem; 59 | 60 | p { 61 | display: inline; 62 | } 63 | `; 64 | 65 | const Settings = styled.span` 66 | cursor: pointer; 67 | float: right; 68 | `; 69 | 70 | export default Header; 71 | -------------------------------------------------------------------------------- /src/app/components/Layout/Layout.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { renderRoutes } from 'react-router-config'; 3 | import { Redirect, useLocation } from 'react-router-dom'; 4 | import styled from 'styled-components'; 5 | import Preview from '../Preview/Preview'; 6 | 7 | import { routeMap, routes } from '../routes'; 8 | import Footer from './Footer'; 9 | import Header from './Header'; 10 | 11 | const Layout = () => { 12 | const location = useLocation(); 13 | const isPreviewEnabled = 14 | routeMap.findIndex((route) => route.path === location.pathname && route.tab === true) !== -1; 15 | 16 | return ( 17 | 18 |
19 | 20 | {isPreviewEnabled && } 21 | 22 | {renderRoutes(routes)} 23 | 24 |