├── .gitignore ├── LICENSE ├── README.md ├── images └── img.png ├── index.html ├── package.json ├── public └── vite.svg ├── src ├── App.tsx ├── assets │ └── react.svg ├── index.css ├── lib │ ├── Figma.ts │ ├── components │ │ ├── Component.tsx │ │ ├── FigmaButton │ │ │ ├── FigmaButton.module.scss │ │ │ └── FigmaButton.tsx │ │ ├── FigmaLogin │ │ │ └── FigmaLogin.tsx │ │ ├── FigmaModal │ │ │ ├── FigmaModal.module.scss │ │ │ └── FigmaModal.tsx │ │ └── FigmaToReact │ │ │ └── FigmaToReact.tsx │ ├── helpers.ts │ └── hooks │ │ ├── useFigma.tsx │ │ └── useLocalStorage.ts ├── main.tsx ├── screens │ └── HomeScreen │ │ └── HomeScreen.tsx ├── services │ └── window.service.ts └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | yarn.lock 26 | .env -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Gihan Rangana 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 | # figma-to-react 2 | This is frontend tool to conver figma designs to react jsx elements using figma api 3 | 4 | # Demo 5 | 6 | [Demo](https://figma-to-react-zeta.vercel.app/) 7 | 8 | ## Install 9 | ``` 10 | npm install 11 | npm run dev 12 | ``` 13 | 14 | ``` 15 | yarn && yarn dev 16 | ``` 17 | 18 | ## Usage 19 | 20 | ```ts 21 | const figma = useFigma() 22 | 23 | return () 24 | ``` 25 | -------------------------------------------------------------------------------- /images/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gihanrangana/figma-to-react/1a14e3157e37a55407c00e229fb49de99a27a5d9/images/img.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Figma to React 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "figma-to-react", 3 | "private": true, 4 | "version": "1.0.4", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "axios": "^1.2.2", 13 | "react": "^18.2.0", 14 | "react-dom": "^18.2.0", 15 | "react-router-dom": "^6.6.1", 16 | "sass": "^1.57.1", 17 | "uuid": "^9.0.0" 18 | }, 19 | "devDependencies": { 20 | "@types/react": "^18.0.26", 21 | "@types/react-dom": "^18.0.9", 22 | "@types/uuid": "^9.0.0", 23 | "@vitejs/plugin-react": "^3.0.0", 24 | "typescript": "^4.9.3", 25 | "vite": "^4.0.0" 26 | }, 27 | "resolutions": { 28 | "@types/react": "^17.0.38" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { createBrowserRouter, RouterProvider } from "react-router-dom"; 2 | import HomeScreen from "./screens/HomeScreen/HomeScreen"; 3 | 4 | function App () { 5 | 6 | const router = createBrowserRouter([ 7 | { 8 | path: "/", 9 | element: 10 | } 11 | ]); 12 | 13 | return ( 14 | 15 | ) 16 | } 17 | 18 | 19 | export default App 20 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&family=Ubuntu:wght@300;400;500;700&display=swap'); 2 | 3 | :root { 4 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 5 | font-size: 16px; 6 | line-height: 24px; 7 | font-weight: 400; 8 | 9 | font-synthesis: none; 10 | text-rendering: optimizeLegibility; 11 | -webkit-font-smoothing: antialiased; 12 | -moz-osx-font-smoothing: grayscale; 13 | -webkit-text-size-adjust: 100%; 14 | } 15 | 16 | * { 17 | 18 | box-sizing: border-box; 19 | } 20 | 21 | a { 22 | font-weight: 500; 23 | color: #646cff; 24 | text-decoration: inherit; 25 | } 26 | 27 | a:hover { 28 | color: #535bf2; 29 | } 30 | 31 | body { 32 | margin: 0; 33 | display: flex; 34 | place-items: center; 35 | justify-content: center; 36 | min-width: 320px; 37 | min-height: 100vh; 38 | } 39 | 40 | h1 { 41 | font-size: 3.2em; 42 | line-height: 1.1; 43 | } 44 | 45 | button { 46 | border-radius: 8px; 47 | border: 1px solid transparent; 48 | padding: 0.6em 1.2em; 49 | font-size: 1em; 50 | font-weight: 500; 51 | font-family: inherit; 52 | background-color: #1a1a1a; 53 | cursor: pointer; 54 | transition: border-color 0.25s; 55 | color: #fff; 56 | } 57 | 58 | button:hover { 59 | border-color: #646cff; 60 | } 61 | 62 | button:focus, 63 | button:focus-visible { 64 | outline: 4px auto -webkit-focus-ring-color; 65 | } 66 | -------------------------------------------------------------------------------- /src/lib/Figma.ts: -------------------------------------------------------------------------------- 1 | import { backgroundSize, colorString, dropShadow, getPaint, imageURL, innerShadow, nodeSort, paintToLinearGradient, paintToRadialGradient } from "./helpers"; 2 | 3 | const GROUP_TYPES = ['GROUP', 'BOOLEAN_OPERATION']; 4 | const expandChildren = (node: any, parent: any, minChildren: any, maxChildren: any, centerChildren: any, offset: any) => { 5 | const children = node.children 6 | let added = offset 7 | 8 | if (children) { 9 | for (let i = 0; i < children.length; i++) { 10 | const child = children[i]; 11 | 12 | if (parent !== null && (node.type === 'COMPONENT' || node.type === 'INSTANCE')) { 13 | child.constraints = { vertical: "TOP_BOTTOM", horizontal: "LEFT_RIGHT" } 14 | } 15 | 16 | if (GROUP_TYPES.indexOf(child.type) >= 0) { 17 | added += expandChildren(child, parent, minChildren, maxChildren, centerChildren, added + i); 18 | continue; 19 | } 20 | 21 | child.order = i + added; 22 | 23 | if (child.constraints && child.constraints.vertical === 'BOTTOM') { 24 | maxChildren.push(child); 25 | } else if (child.constraints && child.constraints.vertical === 'TOP') { 26 | minChildren.push(child); 27 | } else { 28 | centerChildren.push(child); 29 | } 30 | } 31 | 32 | minChildren.sort(nodeSort); 33 | maxChildren.sort(nodeSort); 34 | 35 | return added + children.length - offset; 36 | } 37 | 38 | return added - offset; 39 | } 40 | 41 | export const createComponent = async (component: any, imgMap: any, componentMap: any, fileKey: any) => { 42 | const name = component.name.replace(/\W+/g, ''); 43 | const instance = name + component.id; 44 | let doc = ''; 45 | function print (msg: any, indent: any) { 46 | doc += `${indent}${msg}\n`; 47 | } 48 | 49 | const visitNode = async (node: any, parent: any, lastVertical: any, indent: any) => { 50 | let content = null; 51 | let img = null; 52 | const styles: any = {}; 53 | let minChildren: any = []; 54 | const maxChildren: any = []; 55 | const centerChildren: any = []; 56 | let bounds = null; 57 | let nodeBounds = null; 58 | 59 | if (parent != null) { 60 | nodeBounds = node.absoluteBoundingBox; 61 | const nx2 = nodeBounds.x + nodeBounds.width; 62 | const ny2 = nodeBounds.y + nodeBounds.height; 63 | const parentBounds = parent.absoluteBoundingBox; 64 | const px = parentBounds.x; 65 | const py = parentBounds.y; 66 | 67 | bounds = { 68 | left: nodeBounds.x - px, 69 | right: px + parentBounds.width - nx2, 70 | top: lastVertical == null ? nodeBounds.y - py : nodeBounds.y - lastVertical, 71 | bottom: py + parentBounds.height - ny2, 72 | width: nodeBounds.width, 73 | height: nodeBounds.height, 74 | } 75 | } 76 | 77 | expandChildren(node, parent, minChildren, maxChildren, centerChildren, 0); 78 | 79 | let outerClass = 'outerDiv'; 80 | let innerClass = 'innerDiv'; 81 | const cHorizontal = node.constraints && node.constraints.horizontal; 82 | const cVertical = node.constraints && node.constraints.vertical; 83 | const outerStyle: any = {}; 84 | 85 | if (node.order) { 86 | outerStyle.zIndex = node.order; 87 | } 88 | 89 | if (cHorizontal === 'LEFT_RIGHT') { 90 | if (bounds != null) { 91 | styles.marginLeft = bounds.left; 92 | styles.marginRight = bounds.right; 93 | styles.flexGrow = 1; 94 | } 95 | } else if (cHorizontal === 'RIGHT') { 96 | outerStyle.justifyContent = 'flex-end'; 97 | if (bounds != null) { 98 | styles.marginRight = bounds.right; 99 | styles.width = bounds.width; 100 | styles.minWidth = bounds.width; 101 | } 102 | } else if (cHorizontal === 'CENTER') { 103 | outerStyle.justifyContent = 'center'; 104 | if (bounds != null) { 105 | styles.width = bounds.width; 106 | styles.marginLeft = bounds.left && bounds.right ? bounds.left - bounds.right : null; 107 | } 108 | } else if (cHorizontal === 'SCALE') { 109 | if (bounds != null) { 110 | const parentWidth = bounds.left + bounds.width + bounds.right; 111 | styles.width = `${bounds.width * 100 / parentWidth}%`; 112 | styles.marginLeft = `${bounds.left * 100 / parentWidth}%`; 113 | } 114 | } else { 115 | if (bounds != null) { 116 | styles.marginLeft = bounds.left; 117 | styles.width = bounds.width; 118 | styles.minWidth = bounds.width; 119 | } 120 | } 121 | 122 | if (bounds && bounds.height && cVertical !== 'TOP_BOTTOM') styles.height = bounds.height; 123 | if (cVertical === 'TOP_BOTTOM') { 124 | outerClass += ' centerer'; 125 | if (bounds != null) { 126 | styles.marginTop = bounds.top; 127 | styles.marginBottom = bounds.bottom; 128 | } 129 | } else if (cVertical === 'CENTER') { 130 | outerClass += ' centerer'; 131 | outerStyle.alignItems = 'center'; 132 | if (bounds != null) { 133 | styles.marginTop = bounds.top - bounds.bottom; 134 | } 135 | } else if (cVertical === 'SCALE') { 136 | outerClass += ' centerer'; 137 | if (bounds != null) { 138 | const parentHeight = bounds.top + bounds.height + bounds.bottom; 139 | styles.height = `${bounds.height * 100 / parentHeight}%`; 140 | styles.top = `${bounds.top * 100 / parentHeight}%`; 141 | } 142 | } else { 143 | if (bounds != null) { 144 | styles.marginTop = bounds.top; 145 | styles.marginBottom = bounds.bottom; 146 | styles.minHeight = styles.height; 147 | styles.height = 'auto'; 148 | } 149 | } 150 | 151 | if (['FRAME', 'RECTANGLE', 'INSTANCE', 'COMPONENT'].indexOf(node.type) >= 0) { 152 | if (['FRAME', 'COMPONENT', 'INSTANCE'].indexOf(node.type) >= 0) { 153 | styles.backgroundColor = colorString(node.backgroundColor); 154 | if (node.clipsContent) styles.overflow = 'hidden'; 155 | } else if (node.type === 'RECTANGLE') { 156 | const lastFill = getPaint(node.fills); 157 | if (lastFill) { 158 | if (lastFill.type === 'SOLID') { 159 | styles.backgroundColor = colorString(lastFill.color); 160 | styles.opacity = lastFill.opacity; 161 | } else if (lastFill.type === 'IMAGE') { 162 | styles.backgroundImage = await imageURL(lastFill.imageRef, node.id, fileKey); 163 | styles.backgroundSize = backgroundSize(lastFill.scaleMode); 164 | } else if (lastFill.type === 'GRADIENT_LINEAR') { 165 | styles.background = paintToLinearGradient(lastFill); 166 | } else if (lastFill.type === 'GRADIENT_RADIAL') { 167 | styles.background = paintToRadialGradient(lastFill); 168 | } 169 | } 170 | 171 | if (node.effects) { 172 | for (let i = 0; i < node.effects.length; i++) { 173 | const effect = node.effects[i]; 174 | if (effect.type === 'DROP_SHADOW') { 175 | styles.boxShadow = dropShadow(effect); 176 | } else if (effect.type === 'INNER_SHADOW') { 177 | styles.boxShadow = innerShadow(effect); 178 | } else if (effect.type === 'LAYER_BLUR') { 179 | styles.filter = `blur(${effect.radius}px)`; 180 | } 181 | } 182 | } 183 | 184 | const lastStroke = getPaint(node.strokes); 185 | if (lastStroke) { 186 | if (lastStroke.type === 'SOLID') { 187 | const weight = node.strokeWeight || 1; 188 | styles.border = `${weight}px solid ${colorString(lastStroke.color)}`; 189 | } 190 | } 191 | 192 | const cornerRadii = node.rectangleCornerRadii; 193 | if (cornerRadii && cornerRadii.length === 4) { 194 | styles.borderTopLeftRadius = `${cornerRadii[0]}px` 195 | styles.borderTopRightRadius = `${cornerRadii[1]}px` 196 | styles.borderBottomRightRadius = `${cornerRadii[2]}px` 197 | styles.borderBottomLeftRadius = `${cornerRadii[3]}px` 198 | } 199 | 200 | if(node.cornerRadius){ 201 | styles.borderRadius = `${node.cornerRadius}px` 202 | } 203 | } 204 | } else if (node.type === 'TEXT') { 205 | const lastFill = getPaint(node.fills); 206 | if (lastFill) { 207 | styles.color = colorString(lastFill.color); 208 | } 209 | 210 | const lastStroke = getPaint(node.strokes); 211 | if (lastStroke) { 212 | const weight = node.strokeWeight || 1; 213 | styles.WebkitTextStroke = `${weight}px ${colorString(lastStroke.color)}`; 214 | } 215 | 216 | const fontStyle = node.style; 217 | 218 | const applyFontStyle = (_styles: any, fontStyle: any) => { 219 | if (fontStyle) { 220 | _styles.fontSize = fontStyle.fontSize; 221 | _styles.fontWeight = fontStyle.fontWeight; 222 | _styles.fontFamily = fontStyle.fontFamily; 223 | _styles.textAlign = fontStyle.textAlignHorizontal; 224 | _styles.fontStyle = fontStyle.italic ? 'italic' : 'normal'; 225 | _styles.lineHeight = `${fontStyle.lineHeightPercent * 1.25}%`; 226 | _styles.letterSpacing = `${fontStyle.letterSpacing}px`; 227 | } 228 | } 229 | applyFontStyle(styles, fontStyle); 230 | 231 | if (node.name.substring(0, 6) === 'input:') { 232 | content = [``]; 233 | } else if (node.characterStyleOverrides) { 234 | let para = ''; 235 | const ps = []; 236 | const styleCache: any = {}; 237 | let currStyle = 0; 238 | 239 | const commitParagraph = (key: any) => { 240 | if (para !== '') { 241 | if (styleCache[currStyle] == null && currStyle !== 0) { 242 | styleCache[currStyle] = {}; 243 | applyFontStyle(styleCache[currStyle], node.styleOverrideTable[currStyle]); 244 | } 245 | 246 | const styleOverride = styleCache[currStyle] ? JSON.stringify(styleCache[currStyle]) : '{}'; 247 | 248 | ps.push(`${para}`); 249 | para = ''; 250 | } 251 | } 252 | 253 | for (const i in node.characters) { 254 | let idx = node.characterStyleOverrides[i]; 255 | 256 | if (node.characters[i] === '\n') { 257 | commitParagraph(i); 258 | ps.push(`
`); 259 | continue; 260 | } 261 | 262 | if (idx == null) idx = 0; 263 | if (idx !== currStyle) { 264 | commitParagraph(i); 265 | currStyle = idx; 266 | } 267 | 268 | para += node.characters[i]; 269 | } 270 | commitParagraph('end'); 271 | 272 | content = ps; 273 | } else { 274 | content = node.characters.split("\n").map((line: any, idx: any) => `
${line}
`); 275 | } 276 | } 277 | 278 | function printDiv (styles: any, outerStyle: any, indent: any) { 279 | 280 | let styleString = JSON.stringify(styles) 281 | 282 | print(`
`, indent); 283 | print(` `, indent); 288 | } 289 | 290 | if (parent != null) { 291 | printDiv(styles, outerStyle, indent); 292 | } 293 | 294 | if (node.id !== component.id && node.name.charAt(0) === '#') { 295 | print(` `, indent); 296 | await createComponent(node, imgMap, componentMap, fileKey); 297 | } else if (node.type === 'VECTOR') { 298 | print(`
${imgMap[node.id]}
`, indent); 299 | } else { 300 | const newNodeBounds = node.absoluteBoundingBox; 301 | const newLastVertical = newNodeBounds && newNodeBounds.y + newNodeBounds.height; 302 | print(`
`, indent); 303 | let first = true; 304 | for (const child of minChildren) { 305 | await visitNode(child, node, first ? null : newLastVertical, indent + ' '); 306 | first = false; 307 | } 308 | for (const child of centerChildren) await visitNode(child, node, null, indent + ' '); 309 | if (maxChildren.length > 0) { 310 | outerClass += ' maxer'; 311 | styles.width = '100%'; 312 | styles.pointerEvents = 'none'; 313 | styles.backgroundColor = null; 314 | printDiv(styles, outerStyle, indent + ' '); 315 | first = true; 316 | for (const child of maxChildren) { 317 | await visitNode(child, node, first ? null : newLastVertical, indent + ' '); 318 | first = false; 319 | } 320 | print(`
`, indent); 321 | print(`
`, indent); 322 | } 323 | if (content != null) { 324 | if (node.name.charAt(0) === '$') { 325 | const varName = node.name.substring(1); 326 | print(` {this.props.${varName} && this.props.${varName}.split("\\n").map((line, idx) =>
{line}
)}`, indent); 327 | print(` {!this.props.${varName} && (
`, indent); 328 | for (const piece of content) { 329 | print(piece, indent + ' '); 330 | } 331 | print(`
)}`, indent); 332 | } else { 333 | for (const piece of content) { 334 | print(piece, indent + ' '); 335 | } 336 | } 337 | } 338 | print(` `, indent); 339 | } 340 | 341 | if (parent != null) { 342 | print(` `, indent); 343 | print(``, indent); 344 | } 345 | } 346 | 347 | await visitNode(component, null, null, ' '); 348 | 349 | componentMap[component.id] = { instance, name, doc }; 350 | return componentMap 351 | } 352 | 353 | // export const createComponent = async (component: any, imgMap: any, componentMap: any, fileKey?: string | null) => { 354 | // const name = 'C' + component.name.replace(/\W+/g, ''); 355 | // const instance = name + component.id.replace(';', 'S').replace(':', 'D'); 356 | // 357 | // let doc = ''; 358 | // 359 | // const print = (indent: any, msg: any) => { 360 | // doc += `${indent}${msg}\n`; 361 | // } 362 | // 363 | // const visitNode = async (node: any, parent: any, lastVertical: any, indent: any) => { 364 | // 365 | // let content: any = '' 366 | // const styles: any = {} 367 | // let minChildren: any = [] 368 | // const maxChildren: any = [] 369 | // const centerChildren: any = [] 370 | // let bounds = null 371 | // let nodeBounds = null 372 | // 373 | // if (parent !== null) { 374 | // nodeBounds = parent.absoluteBoundingBox 375 | // const nx2 = nodeBounds.x + nodeBounds.width; 376 | // const ny2 = nodeBounds.y + nodeBounds.height; 377 | // const parentBounds = parent.absoluteBoundingBox; 378 | // const px = parentBounds.x; 379 | // const py = parentBounds.y; 380 | // 381 | // bounds = { 382 | // left: nodeBounds.x - px, 383 | // right: px + parentBounds.width - nx2, 384 | // top: lastVertical == null ? nodeBounds.y - py : nodeBounds.y - lastVertical, 385 | // bottom: py + parentBounds.height - ny2, 386 | // width: nodeBounds.width, 387 | // height: nodeBounds.height, 388 | // } 389 | // } 390 | // 391 | // expandChildren(node, parent, minChildren, maxChildren, centerChildren, 0); 392 | // 393 | // let outerClass = 'outerDiv'; 394 | // let innerClass = 'innerDiv'; 395 | // const cHorizontal = node.constraints && node.constraints.horizontal; 396 | // const cVertical = node.constraints && node.constraints.vertical; 397 | // const outerStyle: any = {}; 398 | // 399 | // if (node.order) { 400 | // outerStyle.zIndex = node.order; 401 | // } 402 | // 403 | // if (cHorizontal === 'LEFT_RIGHT') { 404 | // if (bounds != null) { 405 | // styles.marginLeft = bounds.left; 406 | // styles.marginRight = bounds.right; 407 | // styles.flexGrow = 1; 408 | // } 409 | // } else if (cHorizontal === 'RIGHT') { 410 | // outerStyle.justifyContent = 'flex-end'; 411 | // if (bounds != null) { 412 | // styles.marginRight = bounds.right; 413 | // styles.width = bounds.width; 414 | // styles.minWidth = bounds.width; 415 | // } 416 | // } else if (cHorizontal === 'CENTER') { 417 | // outerStyle.justifyContent = 'center'; 418 | // if (bounds != null) { 419 | // styles.width = bounds.width; 420 | // styles.marginLeft = bounds.left && bounds.right ? bounds.left - bounds.right : null; 421 | // } 422 | // } else if (cHorizontal === 'SCALE') { 423 | // if (bounds != null) { 424 | // const parentWidth = bounds.left + bounds.width + bounds.right; 425 | // styles.width = `${bounds.width * 100 / parentWidth}%`; 426 | // styles.marginLeft = `${bounds.left * 100 / parentWidth}%`; 427 | // } 428 | // } else { 429 | // if (bounds != null) { 430 | // styles.marginLeft = bounds.left; 431 | // styles.width = bounds.width; 432 | // styles.minWidth = bounds.width; 433 | // } 434 | // } 435 | // 436 | // if (bounds && bounds.height && cVertical !== 'TOP_BOTTOM') styles.height = bounds.height; 437 | // if (cVertical === 'TOP_BOTTOM') { 438 | // outerClass += ' centerer'; 439 | // if (bounds != null) { 440 | // styles.marginTop = bounds.top; 441 | // styles.marginBottom = bounds.bottom; 442 | // } 443 | // } else if (cVertical === 'CENTER') { 444 | // outerClass += ' centerer'; 445 | // outerStyle.alignItems = 'center'; 446 | // if (bounds != null) { 447 | // styles.marginTop = bounds.top - bounds.bottom; 448 | // } 449 | // } else if (cVertical === 'SCALE') { 450 | // outerClass += ' centerer'; 451 | // if (bounds != null) { 452 | // const parentHeight = bounds.top + bounds.height + bounds.bottom; 453 | // styles.height = `${bounds.height * 100 / parentHeight}%`; 454 | // styles.top = `${bounds.top * 100 / parentHeight}%`; 455 | // } 456 | // } else { 457 | // if (bounds != null) { 458 | // styles.marginTop = bounds.top; 459 | // styles.marginBottom = bounds.bottom; 460 | // styles.minHeight = styles.height; 461 | // styles.height = null; 462 | // } 463 | // } 464 | // 465 | // if (['FRAME', 'RECTANGLE', 'INSTANCE', 'COMPONENT'].indexOf(node.type) >= 0) { 466 | // if (['FRAME', 'COMPONENT', 'INSTANCE'].indexOf(node.type) >= 0) { 467 | // styles.backgroundColor = colorString(node.backgroundColor); 468 | // if (node.clipsContent) styles.overflow = 'hidden'; 469 | // } else if (node.type === 'RECTANGLE') { 470 | // // const bgImage = await imageURL(node.fills.imageRef, node.id, fileKey); 471 | // const lastFill = getPaint(node.fills); 472 | // if (lastFill) { 473 | // if (lastFill.type === 'SOLID') { 474 | // styles.backgroundColor = colorString(lastFill.color); 475 | // styles.opacity = lastFill.opacity; 476 | // } else if (lastFill.type === 'IMAGE') { 477 | // styles.backgroundImage = `url('')`; 478 | // styles.backgroundSize = backgroundSize(lastFill.scaleMode); 479 | // } else if (lastFill.type === 'GRADIENT_LINEAR') { 480 | // styles.background = paintToLinearGradient(lastFill); 481 | // } else if (lastFill.type === 'GRADIENT_RADIAL') { 482 | // styles.background = paintToRadialGradient(lastFill); 483 | // } 484 | // } 485 | // 486 | // if (node.effects) { 487 | // for (let i = 0; i < node.effects.length; i++) { 488 | // const effect = node.effects[i]; 489 | // if (effect.type === 'DROP_SHADOW') { 490 | // styles.boxShadow = dropShadow(effect); 491 | // } else if (effect.type === 'INNER_SHADOW') { 492 | // styles.boxShadow = innerShadow(effect); 493 | // } else if (effect.type === 'LAYER_BLUR') { 494 | // styles.filter = `blur(${effect.radius}px)`; 495 | // } 496 | // } 497 | // } 498 | // 499 | // const lastStroke = getPaint(node.strokes); 500 | // if (lastStroke) { 501 | // if (lastStroke.type === 'SOLID') { 502 | // const weight = node.strokeWeight || 1; 503 | // styles.border = `${weight}px solid ${colorString(lastStroke.color)}`; 504 | // } 505 | // } 506 | // 507 | // const cornerRadii = node.rectangleCornerRadii; 508 | // if (cornerRadii && cornerRadii.length === 4 && cornerRadii[0] + cornerRadii[1] + cornerRadii[2] + cornerRadii[3] > 0) { 509 | // styles.borderRadius = `${cornerRadii[0]}px ${cornerRadii[1]}px ${cornerRadii[2]}px ${cornerRadii[3]}px`; 510 | // } 511 | // } 512 | // } else if (node.type === 'TEXT') { 513 | // const lastFill = getPaint(node.fills); 514 | // if (lastFill) { 515 | // styles.color = colorString(lastFill.color); 516 | // } 517 | // 518 | // const lastStroke = getPaint(node.strokes); 519 | // if (lastStroke) { 520 | // const weight = node.strokeWeight || 1; 521 | // styles.WebkitTextStroke = `${weight}px ${colorString(lastStroke.color)}`; 522 | // } 523 | // 524 | // const fontStyle = node.style; 525 | // 526 | // const applyFontStyle = (_styles: any, fontStyle: any) => { 527 | // if (fontStyle) { 528 | // _styles.fontSize = fontStyle.fontSize; 529 | // _styles.fontWeight = fontStyle.fontWeight; 530 | // _styles.fontFamily = fontStyle.fontFamily; 531 | // _styles.textAlign = fontStyle.textAlignHorizontal; 532 | // _styles.fontStyle = fontStyle.italic ? 'italic' : 'normal'; 533 | // _styles.lineHeight = `${fontStyle.lineHeightPercent * 1.25}%`; 534 | // _styles.letterSpacing = `${fontStyle.letterSpacing}px`; 535 | // } 536 | // } 537 | // applyFontStyle(styles, fontStyle); 538 | // 539 | // if (node.name.substring(0, 6) === 'input:') { 540 | // content = [``]; 541 | // } else if (node.characterStyleOverrides) { 542 | // let para = ''; 543 | // const ps = []; 544 | // const styleCache: any = {}; 545 | // let currStyle = 0; 546 | // 547 | // const commitParagraph = (key: any) => { 548 | // if (para !== '') { 549 | // if (styleCache[currStyle] == null && currStyle !== 0) { 550 | // styleCache[currStyle] = {}; 551 | // applyFontStyle(styleCache[currStyle], node.styleOverrideTable[currStyle]); 552 | // } 553 | // 554 | // const styleOverride = styleCache[currStyle] ? JSON.stringify(styleCache[currStyle]) : '{}'; 555 | // 556 | // ps.push(`${para}`); 557 | // para = ''; 558 | // } 559 | // } 560 | // 561 | // for (const i in node.characters) { 562 | // let idx = node.characterStyleOverrides[i]; 563 | // 564 | // if (node.characters[i] === '\n') { 565 | // commitParagraph(i); 566 | // ps.push(`
`); 567 | // continue; 568 | // } 569 | // 570 | // if (idx == null) idx = 0; 571 | // if (idx !== currStyle) { 572 | // commitParagraph(i); 573 | // currStyle = idx; 574 | // } 575 | // 576 | // para += node.characters[i]; 577 | // } 578 | // commitParagraph('end'); 579 | // 580 | // content = ps; 581 | // } else { 582 | // content = node.characters.split("\n").map((line: any, idx: any) => `
${line}
`); 583 | // } 584 | // } 585 | // function printDiv (styles: any, outerStyle: any, indent: any) { 586 | // print(`
`, indent); 587 | // print(` `, indent); 592 | // } 593 | // 594 | // if (parent != null) { 595 | // printDiv(styles, outerStyle, indent); 596 | // } 597 | // 598 | // if (node.id !== component.id && node.name.charAt(0) === '#') { 599 | // print(` `, indent); 600 | // await createComponent(node, imgMap, componentMap, fileKey); 601 | // } else if (node.type === 'VECTOR') { 602 | // print(`
`, indent); 603 | // } else { 604 | // const newNodeBounds = node.absoluteBoundingBox; 605 | // const newLastVertical = newNodeBounds && newNodeBounds.y + newNodeBounds.height; 606 | // print(`
`, indent); 607 | // let first = true; 608 | // for (const child of minChildren) { 609 | // await visitNode(child, node, first ? null : newLastVertical, indent + ' '); 610 | // first = false; 611 | // } 612 | // for (const child of centerChildren) await visitNode(child, node, null, indent + ' '); 613 | // if (maxChildren.length > 0) { 614 | // outerClass += ' maxer'; 615 | // styles.width = '100%'; 616 | // styles.pointerEvents = 'none'; 617 | // styles.backgroundColor = null; 618 | // printDiv(styles, outerStyle, indent + ' '); 619 | // first = true; 620 | // for (const child of maxChildren) { 621 | // await visitNode(child, node, first ? null : newLastVertical, indent + ' '); 622 | // first = false; 623 | // } 624 | // print(`
`, indent); 625 | // print(`
`, indent); 626 | // } 627 | // if (content != null) { 628 | // if (node.name.charAt(0) === '$') { 629 | // const varName = node.name.substring(1); 630 | // print(` {this.props.${varName} && this.props.${varName}.split("\\n").map((line, idx) =>
{line}
)}`, indent); 631 | // print(` {!this.props.${varName} && (
`, indent); 632 | // for (const piece of content) { 633 | // print(piece, indent + ' '); 634 | // } 635 | // print(`
)}`, indent); 636 | // } else { 637 | // for (const piece of content) { 638 | // print(piece, indent + ' '); 639 | // } 640 | // } 641 | // } 642 | // print(`
`, indent); 643 | // } 644 | // if (parent != null) { 645 | // print(` `, indent); 646 | // print(``, indent); 647 | // } 648 | // 649 | // return doc 650 | // } 651 | // 652 | // doc = await visitNode(component, null, null, ' '); 653 | // componentMap[component.id] = { instance, name, doc }; 654 | // } -------------------------------------------------------------------------------- /src/lib/components/Component.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Component = (props: any) => { 4 | 5 | return ( 6 |
7 | {React.Children.map(props.str, (child) => { 8 | return React.cloneElement(child); 9 | })} 10 | {/*{props.str}*/} 11 |
12 | ) 13 | } 14 | 15 | interface ComponentProps { 16 | [key: string]: any 17 | } 18 | 19 | export default Component -------------------------------------------------------------------------------- /src/lib/components/FigmaButton/FigmaButton.module.scss: -------------------------------------------------------------------------------- 1 | .userWrapper { 2 | height: 48px; 3 | display: flex; 4 | align-items: center; 5 | background-color: #000; 6 | border-radius: 8px; 7 | padding: 10px 15px; 8 | 9 | img { 10 | width: 32px; 11 | height: 32px; 12 | object-fit: cover; 13 | border-radius: 50%; 14 | margin-right: 10px; 15 | } 16 | 17 | span { 18 | color: #fff 19 | } 20 | } -------------------------------------------------------------------------------- /src/lib/components/FigmaButton/FigmaButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | 3 | import styles from './FigmaButton.module.scss' 4 | 5 | const FigmaButton: React.FC = (props) => { 6 | const { onClick, user } = props 7 | const styleObj = { 8 | cursor: "pointer" 9 | }; 10 | 11 | return ( 12 | <> 13 | {user && 14 |
15 | {user.handle}/ 16 | {user.handle} 17 |
18 | } 19 | 20 | {!user && 28 | 37 | 41 | 42 | 43 | 44 | 45 | 46 | } 47 | 48 | 49 | ) 50 | } 51 | 52 | interface FigmaButtonProps { 53 | [key: string]: any 54 | } 55 | 56 | export default FigmaButton -------------------------------------------------------------------------------- /src/lib/components/FigmaLogin/FigmaLogin.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import FigmaButton from "../FigmaButton/FigmaButton"; 3 | 4 | const FigmaLogin: React.FC = (props) => { 5 | 6 | const { user, redirectUrl, CLIENT_ID, REDIRECT_URL, SCOPE } = props 7 | const handleClick = () => { 8 | window.location.href = `https://www.figma.com/oauth?client_id=${CLIENT_ID}&redirect_uri=${redirectUrl || REDIRECT_URL}&scope=${SCOPE}&state=null&response_type=code` 9 | } 10 | 11 | return ( 12 | <> 13 | 17 | 18 | ) 19 | } 20 | 21 | interface FigmaLoginProps { 22 | CLIENT_ID: string, 23 | REDIRECT_URL: string, 24 | SCOPE: string 25 | 26 | [key: string]: any 27 | } 28 | 29 | export default FigmaLogin -------------------------------------------------------------------------------- /src/lib/components/FigmaModal/FigmaModal.module.scss: -------------------------------------------------------------------------------- 1 | .modalBackdrop { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | right: 0; 6 | bottom: 0; 7 | background: rgba(0, 0, 0, 0.5); 8 | display: flex; 9 | align-items: center; 10 | justify-content: center; 11 | animation: backdrop 0.3s alternate; 12 | } 13 | 14 | .modalContent { 15 | width: 550px; 16 | height: 600px; 17 | background: #fff; 18 | border-radius: 20px; 19 | padding: 20px; 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | flex-direction: column; 24 | gap: 20px; 25 | animation: content cubic-bezier(.17, .67, .83, .67) 0.5s alternate; 26 | 27 | input { 28 | width: 100%; 29 | padding: 8px 16px; 30 | outline: none; 31 | border: 1px solid hsla(0, 0, 0, 15%); 32 | border-radius: 8px; 33 | } 34 | } 35 | 36 | .framesList { 37 | margin: 0; 38 | padding: 0; 39 | width: 100%; 40 | list-style: none; 41 | 42 | .frameListItem { 43 | padding: 8px 16px; 44 | border: 1px solid hsla(0, 0, 0, 10%); 45 | border-radius: 8px; 46 | transition: all 0.3s ease; 47 | cursor: pointer; 48 | margin-bottom: 10px; 49 | 50 | &:hover { 51 | border-color: hsla(222, 75%, 59%, 100%) 52 | } 53 | } 54 | } 55 | 56 | @keyframes content { 57 | from { 58 | opacity: 0; 59 | transform: translate(0, -30%); 60 | } 61 | to { 62 | opacity: 1; 63 | transform: translate(0, 0); 64 | } 65 | } 66 | 67 | @keyframes backdrop { 68 | from { 69 | opacity: 0; 70 | } 71 | to { 72 | opacity: 1; 73 | } 74 | } -------------------------------------------------------------------------------- /src/lib/components/FigmaModal/FigmaModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useState } from 'react' 2 | import { createPortal } from "react-dom"; 3 | 4 | import styles from './FigmaModal.module.scss' 5 | 6 | const ModalContainer: React.FC = (props) => { 7 | 8 | const { status, handleSubmit, retrieveFiles, frames, setSelectedFrame } = props; 9 | 10 | const [value, setValue] = useState(null) 11 | const [step, setStep] = useState(0) 12 | 13 | const handleChange = (event: any) => { 14 | const val: any = { [event.target.name]: event.target.value } 15 | setValue((prev: any) => ({ ...prev, ...val })) 16 | } 17 | 18 | const setNextStep = useCallback(() => { 19 | retrieveFiles(value.fileKey).then(() => { 20 | setStep(prev => prev + 1) 21 | }) 22 | }, [step, value]) 23 | 24 | const handleSelectFrame = (id: string) => { 25 | setSelectedFrame(id) 26 | setStep(prev => prev + 1) 27 | } 28 | 29 | return ( 30 |
31 |
32 | 33 | {step === 0 && 34 | <> 35 | 42 | 43 | 44 | } 45 | 46 | {step === 1 && 47 | <> 48 |
49 |

Select Frame to generate the design

50 | 51 | {frames && 52 |
    53 | {frames.map((frame: any, i: number) => { 54 | return ( 55 |
  • 60 | {frame.name} 61 |
  • 62 | ) 63 | })} 64 |
65 | } 66 |
67 | 68 | } 69 | 70 | {step === 2 && 71 | 77 | } 78 | 79 |
80 |
81 | ) 82 | } 83 | 84 | const FigmaModal: React.FC = (props) => { 85 | return createPortal(, document.body) 86 | } 87 | 88 | interface FigmaModalProps { 89 | [key: string]: any 90 | } 91 | 92 | export default FigmaModal -------------------------------------------------------------------------------- /src/lib/components/FigmaToReact/FigmaToReact.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import { useSearchParams } from "react-router-dom"; 3 | import FigmaModal from "../FigmaModal/FigmaModal"; 4 | import Component from "../Component"; 5 | 6 | const FigmaToReact: React.FC = (props) => { 7 | 8 | const { figma, authToken, onHtmlReceived } = props 9 | 10 | const [searchParams, setSearchParams] = useSearchParams(); 11 | 12 | const handleClick = async (props: any) => { 13 | await figma.run(props) 14 | } 15 | 16 | useEffect(() => { 17 | 18 | if (!onHtmlReceived) return; 19 | onHtmlReceived(figma.html) 20 | 21 | }, [figma.html]) 22 | 23 | useEffect(() => { 24 | 25 | if (authToken) { 26 | // console.log('Auth token') 27 | figma.setAuthToken(authToken) 28 | return; 29 | } 30 | 31 | const code = searchParams.get("code"); 32 | if (!code) return 33 | 34 | (async () => { 35 | await figma.authenticate(code); 36 | })() 37 | 38 | }, [searchParams]) 39 | 40 | return ( 41 |
42 | 43 | {/* This is just only for show the errors, remove this on production */} 44 | {figma.error && 45 |
46 |                     {JSON.stringify(figma.error)}
47 |                 
48 | } 49 | 50 | {figma.status &&

{figma.status}

} 51 | 52 | {figma.renderLogin()} 53 | 54 | {figma.user && !figma.error && !figma.data && 55 | 62 | } 63 | 64 | {figma.data && 65 | figma.data.map((El: any, index: any) => { 66 | return ( 67 | 68 | ) 69 | }) 70 | } 71 |
72 | ) 73 | } 74 | 75 | interface FigmaToReactProps { 76 | figma: any, 77 | authToken?: {}, 78 | onHtmlReceived?: (html: any) => void 79 | } 80 | 81 | export default FigmaToReact -------------------------------------------------------------------------------- /src/lib/helpers.ts: -------------------------------------------------------------------------------- 1 | import { createElement } from "react"; 2 | import axios from "axios"; 3 | import { v4 as uuid } from 'uuid' 4 | 5 | function rgba2hex (orig: any) { 6 | let a: any, isPercent: any, 7 | rgb = orig.replace(/\s/g, '').match(/^rgba?\((\d+),(\d+),(\d+),?([^,\s)]+)?/i), 8 | alpha = (rgb && rgb[4] || "").trim(), 9 | hex = rgb ? 10 | (rgb[1] | 1 << 8).toString(16).slice(1) + 11 | (rgb[2] | 1 << 8).toString(16).slice(1) + 12 | (rgb[3] | 1 << 8).toString(16).slice(1) : orig; 13 | 14 | if (alpha !== "") { 15 | a = alpha; 16 | } else { 17 | a = 0o1; 18 | } 19 | // multiply before convert to HEX 20 | a = ((a * 255) | 1 << 8).toString(16).slice(1) 21 | hex = hex + a; 22 | 23 | return hex; 24 | } 25 | 26 | export const colorString = (color: any) => { 27 | const rgba = `rgba(${Math.round(color.r * 255)}, ${Math.round(color.g * 255)}, ${Math.round(color.b * 255)}, ${color.a})` 28 | return `#${rgba2hex(rgba)}`; 29 | } 30 | 31 | export const dropShadow = (effect: any) => { 32 | return `${effect.offset.x}px ${effect.offset.y}px ${effect.radius}px ${colorString(effect.color)}`; 33 | } 34 | 35 | export const innerShadow = (effect: any) => { 36 | return `inset ${effect.offset.x}px ${effect.offset.y}px ${effect.radius}px ${colorString(effect.color)}`; 37 | } 38 | 39 | export const imageURL = async (hash: any, id?: any, fileKey?: string | null) => { 40 | if (!fileKey) return; 41 | 42 | const authToken = JSON.parse(localStorage.getItem("figmaAuthToken") || '') 43 | 44 | let url = ''; 45 | const res = await axios.get(`https://api.figma.com/v1/images/${fileKey}?ids=${id}&format=png`, { 46 | headers: { 47 | Authorization: `Bearer ${authToken.access_token}` 48 | } 49 | }) 50 | url = res.data.images[id] 51 | return `url(${url})`; 52 | // const squash = hash.split('-').join(''); 53 | // return `url(https://s3-us-west-2.amazonaws.com/figma-alpha/img/${squash.substring(0, 4)}/${squash.substring(4, 8)}/${squash.substring(8)})`; 54 | } 55 | 56 | export const backgroundSize = (scaleMode: any) => { 57 | if (scaleMode === 'FILL') { 58 | return 'cover'; 59 | } 60 | } 61 | 62 | export const nodeSort = (a: any, b: any) => { 63 | if (a.absoluteBoundingBox.y < b.absoluteBoundingBox.y) return -1; 64 | else if (a.absoluteBoundingBox.y === b.absoluteBoundingBox.y) return 0; 65 | else return 1; 66 | } 67 | 68 | export const getPaint = (paintList: any) => { 69 | if (paintList && paintList.length > 0) { 70 | return paintList[paintList.length - 1]; 71 | } 72 | 73 | return null; 74 | } 75 | 76 | export const paintToLinearGradient = (paint: any) => { 77 | const handles = paint.gradientHandlePositions; 78 | const handle0 = handles[0]; 79 | const handle1 = handles[1]; 80 | 81 | const ydiff = handle1.y - handle0.y; 82 | const xdiff = handle0.x - handle1.x; 83 | 84 | const angle = Math.atan2(-xdiff, -ydiff); 85 | const stops = paint.gradientStops.map((stop: any) => { 86 | return `${colorString(stop.color)} ${Math.round(stop.position * 100)}%`; 87 | }).join(', '); 88 | return `linear-gradient(${angle}rad, ${stops})`; 89 | } 90 | 91 | export const paintToRadialGradient = (paint: any) => { 92 | const stops = paint.gradientStops.map((stop: any) => { 93 | return `${colorString(stop.color)} ${Math.round(stop.position * 60)}%`; 94 | }).join(', '); 95 | 96 | return `radial-gradient(${stops})`; 97 | } 98 | 99 | const getNodes = (str: string) => { 100 | const dom: any = new DOMParser().parseFromString(str, 'text/html').body; 101 | return Array.from(dom.querySelector('div:nth-child(1)').children) 102 | } 103 | 104 | export const camelCase = (str: string) => { 105 | return str.toLowerCase().replace(/[^a-zA-Z0-9]+(.)/g, (m, chr) => chr.toUpperCase()); 106 | } 107 | export const createJSX = (domStr: any, props: any) => { 108 | 109 | const nodes = typeof domStr === 'string' ? getNodes(domStr) : domStr; 110 | 111 | const JSXNodes: any = []; 112 | let attributesObj: any = {} 113 | for (const node of nodes) { 114 | let { attributes, localName: tag, childNodes, nodeValue, innerHtml }: any = node 115 | 116 | attributesObj.key = uuid().slice(0, 4) 117 | 118 | if (attributes) { 119 | Array.from(attributes).forEach((attr: any) => { 120 | 121 | let attrName = attr.name 122 | 123 | if (attrName.includes('-')) { 124 | attrName = camelCase(attrName) 125 | } 126 | 127 | switch (attrName) { 128 | case 'style': 129 | const style = JSON.parse(attr.nodeValue.substring(1, attr.nodeValue.length - 1)) 130 | attributesObj.style = style; 131 | break; 132 | case 'classname': 133 | attributesObj.className = attr.nodeValue 134 | break; 135 | default: 136 | attributesObj[attrName] = attr.nodeValue; 137 | break; 138 | } 139 | }) 140 | } 141 | 142 | if (tag) { 143 | const JSXElement = createElement(tag, attributesObj, childNodes ? createJSX(childNodes, props) : []) 144 | JSXNodes.push(JSXElement) 145 | } 146 | if (!tag) { 147 | 148 | const key = nodeValue.substring(nodeValue.indexOf('[') + 1, nodeValue.indexOf(']')) 149 | let value = nodeValue 150 | if (props[key]) { 151 | value = value.replace(`[${key}]`, props[key]) 152 | } 153 | JSXNodes.push(value) 154 | } 155 | } 156 | 157 | return JSXNodes; 158 | } -------------------------------------------------------------------------------- /src/lib/hooks/useFigma.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useMemo, useState } from "react"; 2 | import axios from "axios"; 3 | import { useSearchParams } from "react-router-dom"; 4 | import * as Figma from "../Figma"; 5 | 6 | import { createJSX } from "../helpers"; 7 | import useLocalStorage from "./useLocalStorage"; 8 | import FigmaLogin from "../components/FigmaLogin/FigmaLogin"; 9 | 10 | const vectorTypes: any = ['VECTOR', 'LINE', 'REGULAR_POLYGON', 'ELLIPSE', 'STAR']; 11 | 12 | const location = window.location.href.replace(window.location.search, '') 13 | const CLIENT_ID = 'Z70USUDZKrFDMF1DabBe3Y' 14 | const CLIENT_SECRET = 'AATunePpR13fV61pikgCxK0NReiUde' 15 | const REDIRECT_URL = encodeURIComponent(location.substring(0, location.length - 1)) 16 | const SCOPE = 'file_read' 17 | 18 | const useFigma = () => { 19 | const [data, setData] = useState(null) 20 | const [status, setStatus] = useState(null) 21 | const [error, setError]: any = useState(null) 22 | const [user, setUser] = useState(null) 23 | const [fileKey, setFileKey] = useState(null) 24 | const [canvasMap, setCanvasMap] = useState(null) 25 | const [frames, setFrames] = useState(null) 26 | const [selectedFrame, setSelectedFrame] = useState(null) 27 | const [html, setHtml] = useState(null) 28 | const [authToken, setAuthToken] = useLocalStorage('figmaAuthToken') 29 | 30 | const [searchParams, setSearchParams] = useSearchParams() 31 | 32 | let vectorMap: any = {}; 33 | const vectorList: any = []; 34 | const preprocessTree = (node: any) => { 35 | let vectorsOnly = node.name.charAt(0) !== '#'; 36 | let vectorVConstraint = null; 37 | let vectorHConstraint = null; 38 | 39 | function paintsRequireRender (paints: any) { 40 | if (!paints) return false; 41 | 42 | let numPaints = 0; 43 | for (const paint of paints) { 44 | if (!paint.visible) continue; 45 | 46 | numPaints++; 47 | if (paint.type === 'EMOJI') return true; 48 | } 49 | 50 | return numPaints > 1; 51 | } 52 | 53 | if (paintsRequireRender(node.fills) || paintsRequireRender(node.strokes) || (node.blendMode != null && ['PASS_THROUGH', 'NORMAL'].indexOf(node.blendMode) < 0)) { 54 | node.type = 'VECTOR'; 55 | } 56 | 57 | const children = node.children && node.children.filter((child: any) => child.visible !== false); 58 | if (children) { 59 | for (let j = 0; j < children.length; j++) { 60 | if (vectorTypes.indexOf(children[j].type) < 0) vectorsOnly = false; 61 | else { 62 | if (vectorVConstraint != null && children[j].constraints.vertical != vectorVConstraint) vectorsOnly = false; 63 | if (vectorHConstraint != null && children[j].constraints.horizontal != vectorHConstraint) vectorsOnly = false; 64 | vectorVConstraint = children[j].constraints.vertical; 65 | vectorHConstraint = children[j].constraints.horizontal; 66 | } 67 | } 68 | } 69 | node.children = children; 70 | 71 | if (children && children.length > 0 && vectorsOnly) { 72 | node.type = 'VECTOR'; 73 | node.constraints = { 74 | vertical: vectorVConstraint, 75 | horizontal: vectorHConstraint, 76 | }; 77 | } 78 | 79 | if (vectorTypes.indexOf(node.type) >= 0) { 80 | node.type = 'VECTOR'; 81 | vectorMap[node.id] = node; 82 | vectorList.push(node.id); 83 | node.children = []; 84 | } 85 | 86 | if (node.children) { 87 | for (const child of node.children) { 88 | preprocessTree(child); 89 | } 90 | } 91 | } 92 | 93 | const retrieveFiles = useCallback(async (fileKey: any) => { 94 | setFileKey(fileKey) 95 | try { 96 | setStatus('Fetching files...') 97 | const response: any = await axios.get(`https://api.figma.com/v1/files/${fileKey}`, { 98 | headers: { 99 | Authorization: `Bearer ${authToken.access_token}` 100 | } 101 | }) 102 | 103 | const doc = response.data.document; 104 | setCanvasMap(doc.children[0]) 105 | setFrames(doc.children[0].children.map((child: any) => ({ id: child.id, name: child.name }))) 106 | 107 | } catch (err: any) { 108 | setError({ message: err.message, code: err.code }); 109 | } 110 | setStatus(null) 111 | }, [fileKey, authToken]) 112 | 113 | const run = async (props: any) => { 114 | try { 115 | setStatus('Generating structure...') 116 | let canvas = canvasMap; 117 | 118 | if (selectedFrame) { 119 | canvas.children = canvasMap.children.filter((child: any) => child.id === selectedFrame) 120 | } 121 | 122 | for (let i = 0; i < canvas.children.length; i++) { 123 | const child = canvas.children[i] 124 | if (child.name.charAt(0) === '#' && child.visible !== false) { 125 | const child = canvas.children[i]; 126 | preprocessTree(child); 127 | } 128 | } 129 | 130 | let images: any; 131 | 132 | if (vectorList.length > 0) { 133 | let guids = vectorList.join(','); 134 | const imageJSON: any = await axios.get(`https://api.figma.com/v1/images/${fileKey}?ids=${guids}&format=svg`, { 135 | headers: { 136 | Authorization: `Bearer ${authToken.access_token}` 137 | } 138 | }); 139 | images = imageJSON.data.images || {}; 140 | } 141 | 142 | if (images) { 143 | let promises = []; 144 | let guids = []; 145 | for (const guid in images) { 146 | if (images[guid] == null) continue; 147 | guids.push(guid); 148 | promises.push(fetch(images[guid])); 149 | } 150 | 151 | let responses: any = await Promise.all(promises); 152 | promises = []; 153 | for (const resp of responses) { 154 | promises.push(resp.text()); 155 | } 156 | 157 | responses = await Promise.all(promises); 158 | console.log(responses) 159 | for (let i = 0; i < responses.length; i++) { 160 | images[guids[i]] = responses[i].replace(' { 187 | setStatus('Authenticating...') 188 | try { 189 | if (authToken?.accessToken) return; 190 | const url = `https://www.figma.com/api/oauth/token?` + 191 | `client_id=${CLIENT_ID}` + 192 | `&client_secret=${CLIENT_SECRET}` + 193 | `&redirect_uri=${REDIRECT_URL}` + 194 | `&scope=${SCOPE}` + 195 | `&code=${code}` + 196 | `&grant_type=authorization_code` 197 | 198 | const authResponse = await axios.post(url) 199 | setAuthToken(authResponse.data) 200 | 201 | searchParams.delete('code') 202 | searchParams.delete('state') 203 | setSearchParams(searchParams) 204 | 205 | } catch (err: any) { 206 | setError({ message: err.message, code: err.code }); 207 | } 208 | setStatus(null) 209 | } 210 | 211 | useEffect(() => { 212 | 213 | (async () => { 214 | setStatus('Fetching User...') 215 | setUser(null) 216 | try { 217 | if (!authToken?.access_token) { 218 | setStatus(null) 219 | return 220 | } 221 | const user = await axios.get('https://api.figma.com/v1/me', { 222 | headers: { 223 | Authorization: `Bearer ${authToken.access_token}` 224 | } 225 | }) 226 | setUser(user.data) 227 | 228 | } catch (err: any) { 229 | setAuthToken(null) 230 | setError({ message: err.message, code: err.code }) 231 | } 232 | setStatus(null) 233 | })() 234 | 235 | }, [authToken]) 236 | 237 | const renderLogin = useCallback(() => { 238 | return ( 239 | <> 240 | {!status && 241 | 247 | } 248 | 249 | ) 250 | }, [user, status]) 251 | 252 | const figma: any = useMemo(() => { 253 | return { 254 | data, 255 | error, 256 | status, 257 | user, 258 | html, 259 | fileKey, 260 | frames, 261 | selectedFrame, 262 | run, 263 | retrieveFiles, 264 | authenticate, 265 | setFileKey, 266 | setSelectedFrame, 267 | setAuthToken, 268 | renderLogin, 269 | get CLIENT_ID () { 270 | return CLIENT_ID 271 | }, 272 | get CLIENT_SECRET () { 273 | return CLIENT_SECRET; 274 | }, 275 | get REDIRECT_URL () { 276 | return REDIRECT_URL 277 | }, 278 | get SCOPE () { 279 | return SCOPE 280 | }, 281 | get authToken () { 282 | return authToken 283 | }, 284 | get canvas () { 285 | return canvasMap 286 | } 287 | } 288 | }, [data, error, status, user, fileKey, canvasMap, selectedFrame]) 289 | 290 | return figma; 291 | } 292 | 293 | 294 | export default useFigma; -------------------------------------------------------------------------------- /src/lib/hooks/useLocalStorage.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from "react"; 2 | 3 | const useLocalStorage = (key: string, initialState?: any) => { 4 | const serializedInitialState = JSON.stringify(initialState || {}); 5 | let storageValue = initialState; 6 | try { 7 | storageValue = JSON.parse(localStorage.getItem(key) || '') ?? initialState; 8 | } catch { 9 | localStorage.setItem(key, serializedInitialState); 10 | } 11 | const [value, setValue] = useState(storageValue); 12 | const updatedSetValue = useCallback((newValue: any) => { 13 | const serializedNewValue = JSON.stringify(newValue); 14 | if ( 15 | serializedNewValue === serializedInitialState || 16 | typeof newValue === 'undefined' 17 | ) { 18 | localStorage.removeItem(key); 19 | } else { 20 | localStorage.setItem(key, serializedNewValue); 21 | } 22 | setValue(newValue ?? initialState); 23 | }, 24 | [initialState, serializedInitialState, key] 25 | ); 26 | return [value, updatedSetValue]; 27 | } 28 | 29 | export default useLocalStorage; -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App' 4 | import './index.css' 5 | 6 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /src/screens/HomeScreen/HomeScreen.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import FigmaToReact from "../../lib/components/FigmaToReact/FigmaToReact"; 3 | import useFigma from "../../lib/hooks/useFigma"; 4 | 5 | const HomeScreen: React.FC = (props) => { 6 | 7 | const figma: any = useFigma() 8 | 9 | const onHtmlReceived = (html: any) => { 10 | console.log(html) 11 | } 12 | 13 | return ( 14 |
15 | 25 |
26 | ) 27 | } 28 | 29 | interface HomeScreenProps { 30 | [key: string]: any 31 | } 32 | 33 | export default HomeScreen -------------------------------------------------------------------------------- /src/services/window.service.ts: -------------------------------------------------------------------------------- 1 | export const openWindow = (url: string, name: string) => { 2 | const top = (window.innerHeight - 600) / 2 + window.screenY; 3 | const left = (window.innerWidth - 600) / 2 + window.screenX; 4 | 5 | return window.open(url, name, `dialog=yes,top=${top},left=${left},width=${550}px,height=${700}px`) 6 | } 7 | 8 | interface ObserveWindowParams { 9 | popup: Window, 10 | onClose: () => void, 11 | interval?: number 12 | } 13 | 14 | export const observeWindow = ({ popup, onClose, interval }: ObserveWindowParams) => { 15 | const intervalId = setInterval(() => { 16 | if (popup.closed) { 17 | clearInterval(intervalId); 18 | onClose(); 19 | } 20 | }, interval || 100) 21 | } -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | interface ImportMetaEnv { 3 | readonly VITE_FIGMA_TOKEN: string 4 | // more env variables... 5 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | server: { 7 | port: 3000, 8 | open: true 9 | }, 10 | plugins: [react()], 11 | }) 12 | --------------------------------------------------------------------------------