├── .eslintcache ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── applescript.sh ├── manifest.json ├── modd.conf ├── package.json ├── src ├── app.tsx ├── constants.ts ├── figma.d.ts ├── index.html ├── index.tsx └── sandbox.ts ├── tsconfig.json ├── webpack.config.js └── yarn.lock /.eslintcache: -------------------------------------------------------------------------------- 1 | [{"/Users/yusngadiman/projects/react-figma-plugin/webpack.config.js":"1"},{"size":1547,"mtime":1567712736649,"results":"2","hashOfConfig":"3"},{"filePath":"4","messages":"5","errorCount":1,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},"7aktqu","/Users/yusngadiman/projects/react-figma-plugin/webpack.config.js",["6"],{"ruleId":null,"fatal":true,"severity":2,"message":"7"},"Parsing error: \"parserOptions.project\" has been set for @typescript-eslint/parser.\nThe file does not match your project config: /Users/yusngadiman/projects/react-figma-plugin/webpack.config.js.\nThe file must be included in at least one of the projects provided."] -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | lib 2 | dist 3 | node_modules 4 | src/figma.d.ts 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | sourceType: 'module', 5 | ecmaFeatures: { 6 | jsx: true, 7 | }, 8 | useJSXTextNode: true, 9 | project: './tsconfig.json', 10 | }, 11 | plugins: ['@typescript-eslint', 'prettier', 'react', 'jsx-a11y', 'lodash', 'import', 'react-hooks'], 12 | extends: ['plugin:@typescript-eslint/recommended', 'prettier'], 13 | settings: { 14 | react: { 15 | version: '16.9', 16 | }, 17 | 'import/resolver': { 18 | node: { 19 | extensions: ['.js', '.jsx', '.ts', '.tsx'], 20 | }, 21 | }, 22 | }, 23 | env: { 24 | browser: true, 25 | node: true, 26 | es6: true, 27 | }, 28 | globals: { 29 | __html__: true, 30 | figma: true, 31 | React: true, 32 | describe: true, 33 | it: true, 34 | expect: true, 35 | jest: true, 36 | beforeEach: true, 37 | afterEach: true, 38 | beforeAll: true, 39 | afterAll: true, 40 | test: true, 41 | jsdom: true, 42 | __DEV__: true, 43 | __TEST__: true, 44 | before: true, 45 | after: true, 46 | context: true, 47 | assert: true, 48 | }, 49 | rules: { 50 | '@typescript-eslint/member-delimiter-style': 0, 51 | '@typescript-eslint/explicit-function-return-type': 0, 52 | 'import/no-unresolved': 'error', 53 | 'import/named': 'error', 54 | 'import/default': 'error', 55 | 'import/export': 'error', 56 | 'import/no-self-import': 'error', 57 | 'import/order': [ 58 | 'error', 59 | { 60 | groups: ['builtin', 'external', ['internal', 'parent', 'sibling'], 'index'], 61 | 'newlines-between': 'ignore', 62 | }, 63 | ], 64 | 'jsx-a11y/accessible-emoji': 'error', 65 | 'jsx-a11y/alt-text': 'error', 66 | 'jsx-a11y/anchor-has-content': 'error', 67 | 'jsx-a11y/anchor-is-valid': 'error', 68 | 'jsx-a11y/aria-activedescendant-has-tabindex': 'error', 69 | 'jsx-a11y/aria-props': 'error', 70 | 'jsx-a11y/aria-proptypes': 'error', 71 | 'jsx-a11y/aria-role': 'error', 72 | 'jsx-a11y/aria-unsupported-elements': 'error', 73 | 'jsx-a11y/click-events-have-key-events': 'error', 74 | 'jsx-a11y/heading-has-content': 'error', 75 | 'jsx-a11y/html-has-lang': 'error', 76 | 'jsx-a11y/iframe-has-title': 'error', 77 | 'jsx-a11y/img-redundant-alt': 'error', 78 | 'jsx-a11y/interactive-supports-focus': [ 79 | 'error', 80 | { 81 | tabbable: [ 82 | 'button', 83 | 'checkbox', 84 | 'link', 85 | 'progressbar', 86 | 'searchbox', 87 | 'slider', 88 | 'spinbutton', 89 | 'switch', 90 | 'textbox', 91 | ], 92 | }, 93 | ], 94 | 'jsx-a11y/label-has-for': [ 95 | 'error', 96 | { 97 | required: { 98 | every: ['id'], 99 | }, 100 | }, 101 | ], 102 | 'jsx-a11y/label-has-associated-control': 'error', 103 | 'jsx-a11y/media-has-caption': 'error', 104 | 'jsx-a11y/mouse-events-have-key-events': 'error', 105 | 'jsx-a11y/no-access-key': 'error', 106 | 'jsx-a11y/no-autofocus': [ 107 | 'error', 108 | { 109 | ignoreNonDOM: true, 110 | }, 111 | ], 112 | 'jsx-a11y/no-distracting-elements': 'error', 113 | 'jsx-a11y/no-interactive-element-to-noninteractive-role': 'error', 114 | 'jsx-a11y/no-noninteractive-element-interactions': [ 115 | 'error', 116 | { 117 | body: ['onError', 'onLoad'], 118 | iframe: ['onError', 'onLoad'], 119 | img: ['onError', 'onLoad'], 120 | }, 121 | ], 122 | 'jsx-a11y/no-noninteractive-element-to-interactive-role': 'error', 123 | 'jsx-a11y/no-noninteractive-tabindex': 'error', 124 | 'jsx-a11y/no-redundant-roles': 'error', 125 | 'jsx-a11y/no-static-element-interactions': 'error', 126 | 'jsx-a11y/role-has-required-aria-props': 'error', 127 | 'jsx-a11y/role-supports-aria-props': 'error', 128 | 'jsx-a11y/scope': 'error', 129 | 'jsx-a11y/tabindex-no-positive': 'error', 130 | 131 | 'prettier/prettier': ['error'], 132 | 'no-duplicate-imports': 'error', 133 | 'array-callback-return': 'error', 134 | curly: ['error', 'all'], 135 | 'no-eval': 'error', 136 | 'no-implied-eval': 'error', 137 | 'no-param-reassign': ['error', { props: true }], 138 | 'no-return-assign': 'error', 139 | 'no-self-compare': 'error', 140 | radix: 'error', 141 | 'no-array-constructor': 'error', 142 | 'no-new-wrappers': 'error', 143 | 'no-cond-assign': 'error', 144 | 'no-use-before-define': ['error', { functions: false }], 145 | 'no-undef': 'error', 146 | eqeqeq: 'error', 147 | camelcase: 'error', 148 | 'no-new-object': 'error', 149 | 'no-nested-ternary': 'error', 150 | 'no-unused-vars': ['error', { ignoreRestSiblings: true }], 151 | 'no-var': 'error', 152 | 'prefer-const': 'error', 153 | 'prefer-arrow-callback': 'error', 154 | 'arrow-body-style': ['error', 'as-needed'], 155 | 'new-cap': [ 156 | 'error', 157 | { 158 | capIsNewExceptions: [ 159 | 'Collection', 160 | 'DropTarget', 161 | 'DragDropContext', 162 | 'DragSource', 163 | 'Map', 164 | 'List', 165 | 'OrderedMap', 166 | 'OrderedSet', 167 | 'Record', 168 | 'Seq', 169 | 'Set', 170 | ], 171 | }, 172 | ], 173 | 'object-shorthand': ['error', 'properties'], 174 | 'no-useless-computed-key': 'error', 175 | 'require-await': 'error', 176 | 'react/jsx-no-undef': 'error', 177 | 'react/no-direct-mutation-state': 'error', 178 | 'react/react-in-jsx-scope': 'error', 179 | 'react/jsx-boolean-value': 'error', 180 | 'react/jsx-no-duplicate-props': 'error', 181 | 'react/jsx-key': 'error', 182 | 'react/jsx-uses-react': 'error', 183 | 'react/jsx-uses-vars': 'error', 184 | 'react/no-unknown-property': 'error', 185 | 'react/jsx-no-target-blank': 'error', 186 | 'react/no-string-refs': 'error', 187 | 'react/boolean-prop-naming': 'error', 188 | 'react/forbid-foreign-prop-types': 'error', 189 | 'react/no-is-mounted': 'error', 190 | 'react/no-render-return-value': 'error', 191 | 'react/void-dom-elements-no-children': 'error', 192 | 'react/no-typos': 'error', 193 | 'react/no-access-state-in-setstate': 'error', 194 | 'react/no-unescaped-entities': ['error', { forbid: ['>', '}'] }], 195 | 'react/no-unused-prop-types': 'error', 196 | 'react/no-find-dom-node': 'error', 197 | 'react/prefer-es6-class': 'error', 198 | 'react/no-deprecated': 'error', 199 | 'lodash/prefer-noop': 'error', 200 | 'react-hooks/rules-of-hooks': 'error', 201 | }, 202 | } 203 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | lib/ 4 | .idea 5 | .eslintcache 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 120, 5 | "semi": false 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Yusinto Ngadiman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-figma-plugin 2 | A template for developing figma plugins with react and graphql and hot reload 3 | -------------------------------------------------------------------------------- /applescript.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | osascript <<'EOF' 4 | tell application "Figma" to activate 5 | tell application "System Events" to tell process "Figma" 6 | keystroke "p" using {command down, option down} 7 | end tell 8 | EOF -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-graphql-figma-plugin", 3 | "id": "738168449509241862", 4 | "api": "1.0.0", 5 | "main": "dist/sandbox.js", 6 | "ui": "dist/index.html" 7 | } 8 | -------------------------------------------------------------------------------- /modd.conf: -------------------------------------------------------------------------------- 1 | 2 | ** !dist/** !lib/** !node_modules/** { 3 | prep: yarn build 4 | prep: ./applescript.sh 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-figma-plugin", 3 | "version": "1.0.0", 4 | "description": "A template for developing figma plugins with react and graphql", 5 | "main": "sandbox.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "lint": "eslint --ext .js,.ts,.tsx ./src", 9 | "prettier": "prettier --write 'src/*.@(js|ts|tsx|json|css)'", 10 | "clean": "rimraf dist && rimraf lib", 11 | "build": "webpack", 12 | "dev": "modd" 13 | }, 14 | "author": "Yusinto Ngadiman", 15 | "license": "MIT", 16 | "devDependencies": { 17 | "@types/graphql": "^14.5.0", 18 | "@types/jest": "^24.0.18", 19 | "@types/react": "^16.9.2", 20 | "@types/react-dom": "^16.9.0", 21 | "@typescript-eslint/eslint-plugin": "^2.1.0", 22 | "@typescript-eslint/parser": "^2.1.0", 23 | "css-loader": "^3.2.0", 24 | "eslint": "^6.3.0", 25 | "eslint-config-prettier": "^6.2.0", 26 | "eslint-import-resolver-webpack": "^0.11.1", 27 | "eslint-plugin-import": "^2.18.2", 28 | "eslint-plugin-jsx-a11y": "^6.2.3", 29 | "eslint-plugin-lodash": "^6.0.0", 30 | "eslint-plugin-prettier": "^3.1.0", 31 | "eslint-plugin-react": "^7.14.3", 32 | "eslint-plugin-react-hooks": "^2.0.1", 33 | "eslint-watch": "^6.0.0", 34 | "html-webpack-inline-source-plugin": "^0.0.10", 35 | "html-webpack-plugin": "^3.2.0", 36 | "prettier": "^1.18.2", 37 | "rimraf": "^3.0.0", 38 | "style-loader": "^1.0.0", 39 | "ts-loader": "^6.0.4", 40 | "typescript": "^3.6.2", 41 | "url-loader": "^2.1.0", 42 | "webpack": "^4.39.2", 43 | "webpack-cli": "^3.3.7" 44 | }, 45 | "dependencies": { 46 | "graphql": "^14.5.4", 47 | "react": "^16.9.0", 48 | "react-dom": "^16.9.0", 49 | "urql": "^1.4.1" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/app.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useQuery } from 'urql' 3 | import { Artist, query } from './constants' 4 | 5 | export default () => { 6 | const [result] = useQuery({ query }) 7 | const { fetching, data, error } = result 8 | 9 | if (fetching) { 10 | return <>Loading... 11 | } else if (error) { 12 | return <>{error.message} 13 | } 14 | 15 | const { queryArtists: artists } = data 16 | 17 | return artists.map(({ id, name, albums }: Artist) => ( 18 |
19 |

Artist: {name}

20 | 25 |
26 | )) 27 | } 28 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export interface Album { 2 | id: string 3 | name: string 4 | image: string 5 | } 6 | 7 | export interface Artist { 8 | id: string 9 | name: string 10 | albums: Album[] 11 | } 12 | 13 | export const query = `{ 14 | queryArtists(byName: "yowis ben") { 15 | id 16 | name 17 | albums { 18 | id 19 | image 20 | name 21 | } 22 | } 23 | }` 24 | -------------------------------------------------------------------------------- /src/figma.d.ts: -------------------------------------------------------------------------------- 1 | // Global variable with Figma's plugin API. 2 | declare const figma: PluginAPI 3 | declare const __html__: string 4 | 5 | interface PluginAPI { 6 | readonly apiVersion: '1.0.0' 7 | readonly command: string 8 | readonly root: DocumentNode 9 | readonly viewport: ViewportAPI 10 | closePlugin(message?: string): void 11 | 12 | showUI(html: string, options?: ShowUIOptions): void 13 | readonly ui: UIAPI 14 | 15 | readonly clientStorage: ClientStorageAPI 16 | 17 | getNodeById(id: string): BaseNode | null 18 | getStyleById(id: string): BaseStyle | null 19 | 20 | currentPage: PageNode 21 | 22 | readonly mixed: symbol 23 | 24 | createRectangle(): RectangleNode 25 | createLine(): LineNode 26 | createEllipse(): EllipseNode 27 | createPolygon(): PolygonNode 28 | createStar(): StarNode 29 | createVector(): VectorNode 30 | createText(): TextNode 31 | createBooleanOperation(): BooleanOperationNode 32 | createFrame(): FrameNode 33 | createComponent(): ComponentNode 34 | createPage(): PageNode 35 | createSlice(): SliceNode 36 | 37 | createPaintStyle(): PaintStyle 38 | createTextStyle(): TextStyle 39 | createEffectStyle(): EffectStyle 40 | createGridStyle(): GridStyle 41 | 42 | importComponentByKeyAsync(key: string): Promise 43 | importStyleByKeyAsync(key: string): Promise 44 | 45 | listAvailableFontsAsync(): Promise 46 | loadFontAsync(fontName: FontName): Promise 47 | readonly hasMissingFont: boolean 48 | 49 | createNodeFromSvg(svg: string): FrameNode 50 | 51 | createImage(data: Uint8Array): Image 52 | getImageByHash(hash: string): Image 53 | 54 | group(nodes: ReadonlyArray, parent: BaseNode & ChildrenMixin, index?: number): FrameNode 55 | flatten(nodes: ReadonlyArray, parent?: BaseNode & ChildrenMixin, index?: number): VectorNode 56 | } 57 | 58 | interface ClientStorageAPI { 59 | getAsync(key: string): Promise 60 | setAsync(key: string, value: any): Promise 61 | } 62 | 63 | type ShowUIOptions = { 64 | visible?: boolean 65 | width?: number 66 | height?: number 67 | } 68 | 69 | type UIPostMessageOptions = { 70 | targetOrigin?: string 71 | } 72 | 73 | type OnMessageProperties = { 74 | sourceOrigin: string 75 | } 76 | 77 | interface UIAPI { 78 | show(): void 79 | hide(): void 80 | resize(width: number, height: number): void 81 | close(): void 82 | 83 | postMessage(pluginMessage: any, options?: UIPostMessageOptions): void 84 | onmessage: ((pluginMessage: any, props: OnMessageProperties) => void) | undefined 85 | } 86 | 87 | interface ViewportAPI { 88 | center: { x: number; y: number } 89 | zoom: number 90 | // @ts-ignore 91 | scrollAndZoomIntoView(nodes: ReadonlyArray) 92 | } 93 | 94 | //////////////////////////////////////////////////////////////////////////////// 95 | // Datatypes 96 | 97 | type Transform = [[number, number, number], [number, number, number]] 98 | 99 | interface Vector { 100 | readonly x: number 101 | readonly y: number 102 | } 103 | 104 | interface RGB { 105 | readonly r: number 106 | readonly g: number 107 | readonly b: number 108 | } 109 | 110 | interface RGBA { 111 | readonly r: number 112 | readonly g: number 113 | readonly b: number 114 | readonly a: number 115 | } 116 | 117 | interface FontName { 118 | readonly family: string 119 | readonly style: string 120 | } 121 | 122 | type TextCase = 'ORIGINAL' | 'UPPER' | 'LOWER' | 'TITLE' 123 | 124 | type TextDecoration = 'NONE' | 'UNDERLINE' | 'STRIKETHROUGH' 125 | 126 | interface ArcData { 127 | readonly startingAngle: number 128 | readonly endingAngle: number 129 | readonly innerRadius: number 130 | } 131 | 132 | interface ShadowEffect { 133 | readonly type: 'DROP_SHADOW' | 'INNER_SHADOW' 134 | readonly color: RGBA 135 | readonly offset: Vector 136 | readonly radius: number 137 | readonly visible: boolean 138 | readonly blendMode: BlendMode 139 | } 140 | 141 | interface BlurEffect { 142 | readonly type: 'LAYER_BLUR' | 'BACKGROUND_BLUR' 143 | readonly radius: number 144 | readonly visible: boolean 145 | } 146 | 147 | type Effect = ShadowEffect | BlurEffect 148 | 149 | type ConstraintType = 'MIN' | 'CENTER' | 'MAX' | 'STRETCH' | 'SCALE' 150 | 151 | interface Constraints { 152 | readonly horizontal: ConstraintType 153 | readonly vertical: ConstraintType 154 | } 155 | 156 | interface ColorStop { 157 | readonly position: number 158 | readonly color: RGBA 159 | } 160 | 161 | interface ImageFilters { 162 | exposure?: number 163 | contrast?: number 164 | saturation?: number 165 | temperature?: number 166 | tint?: number 167 | highlights?: number 168 | shadows?: number 169 | } 170 | 171 | interface SolidPaint { 172 | readonly type: 'SOLID' 173 | readonly color: RGB 174 | 175 | readonly visible?: boolean 176 | readonly opacity?: number 177 | readonly blendMode?: BlendMode 178 | } 179 | 180 | interface GradientPaint { 181 | readonly type: 'GRADIENT_LINEAR' | 'GRADIENT_RADIAL' | 'GRADIENT_ANGULAR' | 'GRADIENT_DIAMOND' 182 | readonly gradientTransform: Transform 183 | readonly gradientStops: ReadonlyArray 184 | 185 | readonly visible?: boolean 186 | readonly opacity?: number 187 | readonly blendMode?: BlendMode 188 | } 189 | 190 | interface ImagePaint { 191 | readonly type: 'IMAGE' 192 | readonly scaleMode: 'FILL' | 'FIT' | 'CROP' | 'TILE' 193 | readonly imageHash: string | null 194 | readonly imageTransform?: Transform // setting for "CROP" 195 | readonly scalingFactor?: number // setting for "TILE" 196 | readonly filters?: ImageFilters 197 | 198 | readonly visible?: boolean 199 | readonly opacity?: number 200 | readonly blendMode?: BlendMode 201 | } 202 | 203 | type Paint = SolidPaint | GradientPaint | ImagePaint 204 | 205 | interface Guide { 206 | readonly axis: 'X' | 'Y' 207 | readonly offset: number 208 | } 209 | 210 | interface RowsColsLayoutGrid { 211 | readonly pattern: 'ROWS' | 'COLUMNS' 212 | readonly alignment: 'MIN' | 'MAX' | 'STRETCH' | 'CENTER' 213 | readonly gutterSize: number 214 | 215 | readonly count: number // Infinity when "Auto" is set in the UI 216 | readonly sectionSize?: number // Not set for alignment: "STRETCH" 217 | readonly offset?: number // Not set for alignment: "CENTER" 218 | 219 | readonly visible?: boolean 220 | readonly color?: RGBA 221 | } 222 | 223 | interface GridLayoutGrid { 224 | readonly pattern: 'GRID' 225 | readonly sectionSize: number 226 | 227 | readonly visible?: boolean 228 | readonly color?: RGBA 229 | } 230 | 231 | type LayoutGrid = RowsColsLayoutGrid | GridLayoutGrid 232 | 233 | interface ExportSettingsConstraints { 234 | type: 'SCALE' | 'WIDTH' | 'HEIGHT' 235 | value: number 236 | } 237 | 238 | interface ExportSettingsImage { 239 | format: 'JPG' | 'PNG' 240 | contentsOnly?: boolean // defaults to true 241 | suffix?: string 242 | constraint?: ExportSettingsConstraints 243 | } 244 | 245 | interface ExportSettingsSVG { 246 | format: 'SVG' 247 | contentsOnly?: boolean // defaults to true 248 | suffix?: string 249 | svgOutlineText?: boolean // defaults to true 250 | svgIdAttribute?: boolean // defaults to false 251 | svgSimplifyStroke?: boolean // defaults to true 252 | } 253 | 254 | interface ExportSettingsPDF { 255 | format: 'PDF' 256 | contentsOnly?: boolean // defaults to true 257 | suffix?: string 258 | } 259 | 260 | type ExportSettings = ExportSettingsImage | ExportSettingsSVG | ExportSettingsPDF 261 | 262 | type WindingRule = 'NONZERO' | 'EVENODD' 263 | 264 | interface VectorVertex { 265 | readonly x: number 266 | readonly y: number 267 | readonly strokeCap?: StrokeCap 268 | readonly strokeJoin?: StrokeJoin 269 | readonly cornerRadius?: number 270 | readonly handleMirroring?: HandleMirroring 271 | } 272 | 273 | interface VectorSegment { 274 | readonly start: number 275 | readonly end: number 276 | readonly tangentStart?: Vector // Defaults to { x: 0, y: 0 } 277 | readonly tangentEnd?: Vector // Defaults to { x: 0, y: 0 } 278 | } 279 | 280 | interface VectorRegion { 281 | readonly windingRule: WindingRule 282 | readonly loops: ReadonlyArray> 283 | } 284 | 285 | interface VectorNetwork { 286 | readonly vertices: ReadonlyArray 287 | readonly segments: ReadonlyArray 288 | readonly regions?: ReadonlyArray // Defaults to [] 289 | } 290 | 291 | interface VectorPath { 292 | readonly windingRule: WindingRule | 'NONE' 293 | readonly data: string 294 | } 295 | 296 | type VectorPaths = ReadonlyArray 297 | 298 | type LetterSpacing = { 299 | readonly value: number 300 | readonly unit: 'PIXELS' | 'PERCENT' 301 | } 302 | 303 | type LineHeight = 304 | | { 305 | readonly value: number 306 | readonly unit: 'PIXELS' | 'PERCENT' 307 | } 308 | | { 309 | readonly unit: 'AUTO' 310 | } 311 | 312 | type BlendMode = 313 | | 'PASS_THROUGH' 314 | | 'NORMAL' 315 | | 'DARKEN' 316 | | 'MULTIPLY' 317 | | 'LINEAR_BURN' 318 | | 'COLOR_BURN' 319 | | 'LIGHTEN' 320 | | 'SCREEN' 321 | | 'LINEAR_DODGE' 322 | | 'COLOR_DODGE' 323 | | 'OVERLAY' 324 | | 'SOFT_LIGHT' 325 | | 'HARD_LIGHT' 326 | | 'DIFFERENCE' 327 | | 'EXCLUSION' 328 | | 'HUE' 329 | | 'SATURATION' 330 | | 'COLOR' 331 | | 'LUMINOSITY' 332 | 333 | interface Font { 334 | fontName: FontName 335 | } 336 | 337 | //////////////////////////////////////////////////////////////////////////////// 338 | // Mixins 339 | 340 | interface BaseNodeMixin { 341 | readonly id: string 342 | readonly parent: (BaseNode & ChildrenMixin) | null 343 | name: string // Note: setting this also sets \`autoRename\` to false on TextNodes 344 | readonly removed: boolean 345 | toString(): string 346 | remove(): void 347 | 348 | getPluginData(key: string): string 349 | setPluginData(key: string, value: string): void 350 | 351 | // Namespace is a string that must be at least 3 alphanumeric characters, and should 352 | // be a name related to your plugin. Other plugins will be able to read this data. 353 | getSharedPluginData(namespace: string, key: string): string 354 | setSharedPluginData(namespace: string, key: string, value: string): void 355 | } 356 | 357 | interface SceneNodeMixin { 358 | visible: boolean 359 | locked: boolean 360 | } 361 | 362 | interface ChildrenMixin { 363 | readonly children: ReadonlyArray 364 | 365 | appendChild(child: BaseNode): void 366 | insertChild(index: number, child: BaseNode): void 367 | 368 | findAll(callback?: (node: BaseNode) => boolean): ReadonlyArray 369 | findOne(callback: (node: BaseNode) => boolean): BaseNode | null 370 | } 371 | 372 | interface ConstraintMixin { 373 | constraints: Constraints 374 | } 375 | 376 | interface LayoutMixin { 377 | readonly absoluteTransform: Transform 378 | relativeTransform: Transform 379 | x: number 380 | y: number 381 | rotation: number // In degrees 382 | 383 | readonly width: number 384 | readonly height: number 385 | 386 | resize(width: number, height: number): void 387 | resizeWithoutConstraints(width: number, height: number): void 388 | } 389 | 390 | interface BlendMixin { 391 | opacity: number 392 | blendMode: BlendMode 393 | isMask: boolean 394 | effects: ReadonlyArray 395 | effectStyleId: string 396 | } 397 | 398 | interface FrameMixin { 399 | backgrounds: ReadonlyArray 400 | layoutGrids: ReadonlyArray 401 | clipsContent: boolean 402 | guides: ReadonlyArray 403 | gridStyleId: string 404 | backgroundStyleId: string 405 | } 406 | 407 | type StrokeCap = 'NONE' | 'ROUND' | 'SQUARE' | 'ARROW_LINES' | 'ARROW_EQUILATERAL' 408 | type StrokeJoin = 'MITER' | 'BEVEL' | 'ROUND' 409 | type HandleMirroring = 'NONE' | 'ANGLE' | 'ANGLE_AND_LENGTH' 410 | 411 | interface GeometryMixin { 412 | fills: ReadonlyArray | symbol 413 | strokes: ReadonlyArray 414 | strokeWeight: number 415 | strokeAlign: 'CENTER' | 'INSIDE' | 'OUTSIDE' 416 | strokeCap: StrokeCap | symbol 417 | strokeJoin: StrokeJoin | symbol 418 | dashPattern: ReadonlyArray 419 | fillStyleId: string | symbol 420 | strokeStyleId: string 421 | } 422 | 423 | interface CornerMixin { 424 | cornerRadius: number | symbol 425 | cornerSmoothing: number 426 | } 427 | 428 | interface ExportMixin { 429 | exportSettings: ExportSettings[] 430 | exportAsync(settings?: ExportSettings): Promise // Defaults to PNG format 431 | } 432 | 433 | interface DefaultShapeMixin 434 | extends BaseNodeMixin, 435 | SceneNodeMixin, 436 | BlendMixin, 437 | GeometryMixin, 438 | LayoutMixin, 439 | ExportMixin {} 440 | 441 | interface DefaultContainerMixin 442 | extends BaseNodeMixin, 443 | SceneNodeMixin, 444 | ChildrenMixin, 445 | FrameMixin, 446 | BlendMixin, 447 | ConstraintMixin, 448 | LayoutMixin, 449 | ExportMixin {} 450 | 451 | //////////////////////////////////////////////////////////////////////////////// 452 | // Nodes 453 | 454 | interface DocumentNode extends BaseNodeMixin, ChildrenMixin { 455 | readonly type: 'DOCUMENT' 456 | } 457 | 458 | interface PageNode extends BaseNodeMixin, ChildrenMixin, ExportMixin { 459 | readonly type: 'PAGE' 460 | clone(): PageNode 461 | 462 | guides: ReadonlyArray 463 | selection: ReadonlyArray 464 | } 465 | 466 | interface FrameNode extends DefaultContainerMixin { 467 | readonly type: 'FRAME' | 'GROUP' 468 | clone(): FrameNode 469 | } 470 | 471 | interface SliceNode extends BaseNodeMixin, SceneNodeMixin, LayoutMixin, ExportMixin { 472 | readonly type: 'SLICE' 473 | clone(): SliceNode 474 | } 475 | 476 | interface RectangleNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { 477 | readonly type: 'RECTANGLE' 478 | clone(): RectangleNode 479 | topLeftRadius: number 480 | topRightRadius: number 481 | bottomLeftRadius: number 482 | bottomRightRadius: number 483 | } 484 | 485 | interface LineNode extends DefaultShapeMixin, ConstraintMixin { 486 | readonly type: 'LINE' 487 | clone(): LineNode 488 | } 489 | 490 | interface EllipseNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { 491 | readonly type: 'ELLIPSE' 492 | clone(): EllipseNode 493 | arcData: ArcData 494 | } 495 | 496 | interface PolygonNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { 497 | readonly type: 'POLYGON' 498 | clone(): PolygonNode 499 | pointCount: number 500 | } 501 | 502 | interface StarNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { 503 | readonly type: 'STAR' 504 | clone(): StarNode 505 | pointCount: number 506 | innerRadius: number 507 | } 508 | 509 | interface VectorNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { 510 | readonly type: 'VECTOR' 511 | clone(): VectorNode 512 | vectorNetwork: VectorNetwork 513 | vectorPaths: VectorPaths 514 | handleMirroring: HandleMirroring | symbol 515 | } 516 | 517 | interface TextNode extends DefaultShapeMixin, ConstraintMixin { 518 | readonly type: 'TEXT' 519 | clone(): TextNode 520 | characters: string 521 | readonly hasMissingFont: boolean 522 | textAlignHorizontal: 'LEFT' | 'CENTER' | 'RIGHT' | 'JUSTIFIED' 523 | textAlignVertical: 'TOP' | 'CENTER' | 'BOTTOM' 524 | textAutoResize: 'NONE' | 'WIDTH_AND_HEIGHT' | 'HEIGHT' 525 | paragraphIndent: number 526 | paragraphSpacing: number 527 | autoRename: boolean 528 | 529 | textStyleId: string | symbol 530 | fontSize: number | symbol 531 | fontName: FontName | symbol 532 | textCase: TextCase | symbol 533 | textDecoration: TextDecoration | symbol 534 | letterSpacing: LetterSpacing | symbol 535 | lineHeight: LineHeight | symbol 536 | 537 | getRangeFontSize(start: number, end: number): number | symbol 538 | setRangeFontSize(start: number, end: number, value: number): void 539 | getRangeFontName(start: number, end: number): FontName | symbol 540 | setRangeFontName(start: number, end: number, value: FontName): void 541 | getRangeTextCase(start: number, end: number): TextCase | symbol 542 | setRangeTextCase(start: number, end: number, value: TextCase): void 543 | getRangeTextDecoration(start: number, end: number): TextDecoration | symbol 544 | setRangeTextDecoration(start: number, end: number, value: TextDecoration): void 545 | getRangeLetterSpacing(start: number, end: number): LetterSpacing | symbol 546 | setRangeLetterSpacing(start: number, end: number, value: LetterSpacing): void 547 | getRangeLineHeight(start: number, end: number): LineHeight | symbol 548 | setRangeLineHeight(start: number, end: number, value: LineHeight): void 549 | getRangeFills(start: number, end: number): Paint[] | symbol 550 | setRangeFills(start: number, end: number, value: Paint[]): void 551 | getRangeTextStyleId(start: number, end: number): string | symbol 552 | setRangeTextStyleId(start: number, end: number, value: string): void 553 | getRangeFillStyleId(start: number, end: number): string | symbol 554 | setRangeFillStyleId(start: number, end: number, value: string): void 555 | } 556 | 557 | interface ComponentNode extends DefaultContainerMixin { 558 | readonly type: 'COMPONENT' 559 | clone(): ComponentNode 560 | 561 | createInstance(): InstanceNode 562 | description: string 563 | readonly remote: boolean 564 | readonly key: string // The key to use with "importComponentByKeyAsync" 565 | } 566 | 567 | interface InstanceNode extends DefaultContainerMixin { 568 | readonly type: 'INSTANCE' 569 | clone(): InstanceNode 570 | masterComponent: ComponentNode 571 | } 572 | 573 | interface BooleanOperationNode extends DefaultShapeMixin, ChildrenMixin, CornerMixin { 574 | readonly type: 'BOOLEAN_OPERATION' 575 | clone(): BooleanOperationNode 576 | booleanOperation: 'UNION' | 'INTERSECT' | 'SUBTRACT' | 'EXCLUDE' 577 | } 578 | 579 | type BaseNode = DocumentNode | PageNode | SceneNode 580 | 581 | type SceneNode = 582 | | SliceNode 583 | | FrameNode 584 | | ComponentNode 585 | | InstanceNode 586 | | BooleanOperationNode 587 | | VectorNode 588 | | StarNode 589 | | LineNode 590 | | EllipseNode 591 | | PolygonNode 592 | | RectangleNode 593 | | TextNode 594 | 595 | type NodeType = 596 | | 'DOCUMENT' 597 | | 'PAGE' 598 | | 'SLICE' 599 | | 'FRAME' 600 | | 'GROUP' 601 | | 'COMPONENT' 602 | | 'INSTANCE' 603 | | 'BOOLEAN_OPERATION' 604 | | 'VECTOR' 605 | | 'STAR' 606 | | 'LINE' 607 | | 'ELLIPSE' 608 | | 'POLYGON' 609 | | 'RECTANGLE' 610 | | 'TEXT' 611 | 612 | //////////////////////////////////////////////////////////////////////////////// 613 | // Styles 614 | type StyleType = 'PAINT' | 'TEXT' | 'EFFECT' | 'GRID' 615 | 616 | interface BaseStyle { 617 | readonly id: string 618 | readonly type: StyleType 619 | name: string 620 | description: string 621 | remote: boolean 622 | readonly key: string // The key to use with "importStyleByKeyAsync" 623 | remove(): void 624 | } 625 | 626 | interface PaintStyle extends BaseStyle { 627 | type: 'PAINT' 628 | paints: ReadonlyArray 629 | } 630 | 631 | interface TextStyle extends BaseStyle { 632 | type: 'TEXT' 633 | fontSize: number 634 | textDecoration: TextDecoration 635 | fontName: FontName 636 | letterSpacing: LetterSpacing 637 | lineHeight: LineHeight 638 | paragraphIndent: number 639 | paragraphSpacing: number 640 | textCase: TextCase 641 | } 642 | 643 | interface EffectStyle extends BaseStyle { 644 | type: 'EFFECT' 645 | effects: ReadonlyArray 646 | } 647 | 648 | interface GridStyle extends BaseStyle { 649 | type: 'GRID' 650 | layoutGrids: ReadonlyArray 651 | } 652 | 653 | //////////////////////////////////////////////////////////////////////////////// 654 | // Other 655 | 656 | interface Image { 657 | readonly hash: string 658 | getBytesAsync(): Promise 659 | } 660 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import { Provider as GraphqlProvider, createClient } from 'urql' 4 | import App from './app' 5 | 6 | const graphqlClient = createClient({ 7 | url: 'https://spotify-graphql-server.herokuapp.com/graphql', 8 | }) 9 | 10 | render( 11 | 12 | 13 | , 14 | document.getElementById('reactDiv'), 15 | ) 16 | -------------------------------------------------------------------------------- /src/sandbox.ts: -------------------------------------------------------------------------------- 1 | figma.showUI(__html__) 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "lib", 4 | "target": "es5", 5 | "lib": ["dom","es6", "esnext"], 6 | "module": "commonjs", 7 | "declaration": true, 8 | "sourceMap": true, 9 | "strict": true, 10 | "noImplicitAny": true, 11 | "strictNullChecks": true, 12 | "jsx": "react", 13 | "esModuleInterop": true 14 | }, 15 | "include": [ 16 | "src" 17 | ], 18 | "exclude": [ 19 | "node_modules", 20 | "lib", 21 | "dist", 22 | "example", 23 | "**/__generated", 24 | ".idea" 25 | ] 26 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackInlineSourcePlugin = require('html-webpack-inline-source-plugin') 2 | const HtmlWebpackPlugin = require('html-webpack-plugin') 3 | const path = require('path') 4 | 5 | module.exports = (env, argv) => ({ 6 | mode: argv.mode === 'production' ? 'production' : 'development', 7 | 8 | // This is necessary because Figma's 'eval' works differently than normal eval 9 | devtool: argv.mode === 'production' ? false : 'inline-source-map', 10 | 11 | entry: { 12 | app: './src/index.tsx', // The entry point for your UI code 13 | sandbox: './src/sandbox.ts', // The entry point for your plugin code 14 | }, 15 | 16 | module: { 17 | rules: [ 18 | // Converts TypeScript code to JavaScript 19 | { test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/ }, 20 | 21 | // Allows you to use "<%= require('./file.svg') %>" in your HTML code to get a data URI 22 | { test: /\.(png|jpg|gif|webp|svg|zip)$/, loader: [{ loader: 'url-loader' }] }, 23 | 24 | { 25 | test: /\.mjs$/, 26 | include: /node_modules/, 27 | type: 'javascript/auto', 28 | }, 29 | ], 30 | }, 31 | 32 | // Webpack tries these extensions for you if you omit the extension like "import './file'" 33 | resolve: { extensions: ['.tsx', '.ts', '.jsx', '.js', '.mjs', '.gql', '.graphql'] }, 34 | 35 | output: { 36 | filename: '[name].js', 37 | path: path.resolve(__dirname, 'dist'), // Compile into a folder called "dist" 38 | }, 39 | 40 | plugins: [ 41 | new HtmlWebpackPlugin({ 42 | template: './src/index.html', 43 | filename: 'index.html', 44 | inlineSource: '.(js)$', 45 | chunks: ['app'], 46 | }), 47 | new HtmlWebpackInlineSourcePlugin(), 48 | ], 49 | }) 50 | --------------------------------------------------------------------------------