├── .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 |
21 | {albums.map(a => (
22 | - {a.name}
23 | ))}
24 |
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 |
--------------------------------------------------------------------------------