├── .gitignore ├── LICENSE ├── README.md ├── manifest.json ├── package.json ├── src ├── code.ts ├── components │ ├── helpers.ts │ ├── icons.tsx │ ├── svgutil.ts │ └── transform.ts ├── lib │ ├── selection.ts │ └── svg-to-jsx │ │ ├── bin │ │ └── svg-to-jsx │ │ ├── index.js │ │ └── utils.js ├── preview.jpg ├── ui.css ├── ui.html ├── ui.tsx └── views │ └── home.tsx ├── tsconfig.json ├── webpack.config.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | *.css.d.ts 4 | build/ 5 | node_modules/ 6 | dist/ 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) vijay verma (realvjy) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![SVG-TO-CODE-figma](/src/preview.jpg)](https://www.figma.com/community/plugin/1348045528828166727/svg-to-code-react-component) 2 | 3 | # Instant SVG to Code 4 | This Figma (Dev) plugin allows you to generate react components from selected nodes instantly. 5 | 6 | 7 | ## How to use 8 | 1. Select elements you want to transform 9 | 10 | 2. Run "SVG to Code" plugin from Dev Mode 11 | 12 | 3. Copy/Save react components 13 | 14 | 4. Or check the thumbnail to use 15 | 16 | --- 17 | ## Figma Community 18 | 19 | Grab community file from 👉 here [Figma Community](https://www.figma.com/community/plugin/1348045528828166727/svg-to-code-react-component) 20 | 21 | 22 | ## Local development 23 | 24 | Plugin is opensource. You are welcome to contribute. 25 | 26 | 1. Clone the repository 27 | 28 | ```shell 29 | git clone 30 | cd svg-to-code-figma 31 | ``` 32 | 33 | 1. Install the dependencies 34 | 35 | ```shell 36 | yarn 37 | ``` 38 | 39 | 1. Build the plugin 40 | 41 | ``` 42 | yarn watch 43 | ``` 44 | 45 | 1. Open the [Figma desktop app](https://www.figma.com/downloads/) 46 | 47 | 1. Go to `Menu > Plugins > Development > Import Plugin from manifest...` 48 | 49 | 1. Choose `svg-to-code-figma/manifest.json` 50 | 51 | 1. Run the plugin by going to `Menu > Plugins > Development > SVG to Code` 52 | 53 | ## Support & Donate 54 | 55 | 56 | [Github sponsor](https://github.com/sponsors/realvjy) | [Buy me a coffee](https://buymeacoffee.com/realvjy) 57 | 58 | ## Say hi or Feedback 59 | 60 | Feel free to tag me or say hi on Twitter ([@realvjy](http://twitter.com/realvjy)). You are also welcome to share your feedback or bug reports 🙏 61 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SVG to Code", 3 | "id": "1348045528828166727", 4 | "api": "1.0.0", 5 | "main": "dist/code.js", 6 | "ui": "dist/ui.html", 7 | "enableProposedApi": false, 8 | "documentAccess": "dynamic-page", 9 | "editorType": [ 10 | "figma", 11 | "dev" 12 | ], 13 | "permissions": [], 14 | "capabilities": [ 15 | "inspect", 16 | "vscode", 17 | "codegen" 18 | ], 19 | "codegenLanguages": [ 20 | { 21 | "label": "React", 22 | "value": "react" 23 | } 24 | ], 25 | "networkAccess": { 26 | "allowedDomains": [ 27 | "none" 28 | ] 29 | } 30 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "project-wrap-figma", 3 | "version": "1.0.0", 4 | "description": "Wrap Shapes", 5 | "main": "code.js", 6 | "author": "realvjy", 7 | "license": "MIT", 8 | "dependencies": { 9 | "@types/react": "^18.2.21", 10 | "@types/react-dom": "^18.2.19", 11 | "@types/react-html-parser": "^2.0.2", 12 | "@types/styled-components": "^5.1.25", 13 | "compressorjs": "^1.1.1", 14 | "css-to-object": "^1.1.0", 15 | "fs": "^0.0.1-security", 16 | "lodash.camelcase": "^4.3.0", 17 | "lodash.isplainobject": "^4.0.6", 18 | "lodash.isstring": "^4.0.1", 19 | "lodash.template": "^4.5.0", 20 | "os-browserify": "^0.3.0", 21 | "path-browserify": "^1.0.1", 22 | "rc-color-picker": "^1.2.6", 23 | "react": "^18.1.0", 24 | "react-ace": "^10.1.0", 25 | "react-best-gradient-color-picker": "^2.2.23", 26 | "react-color": "^2.19.3", 27 | "react-dev-utils": "^12.0.1", 28 | "react-dom": "^18.2.0", 29 | "react-figma-plugin-ds": "^2.3.0", 30 | "react-image-compressor": "^1.0.1", 31 | "react-image-file-resizer": "^0.4.8", 32 | "react-onclickoutside": "^6.12.2", 33 | "react-router-dom": "^6.3.0", 34 | "react-svg": "^16.1.33", 35 | "styled-components": "^5.3.5", 36 | "svg-parser": "^2.0.4", 37 | "svg-to-jsx": "^1.0.4", 38 | "svg2png-wasm": "^1.3.4", 39 | "timers-browserify": "^2.0.12", 40 | "url": "^0.11.3", 41 | "xmlbuilder": "^15.1.1" 42 | }, 43 | "devDependencies": { 44 | "@figma/plugin-typings": "^1.47.0", 45 | "@types/node-fetch": "^2.6.1", 46 | "@types/react-router-dom": "^5.3.3", 47 | "copy-to-clipboard": "^3.3.3", 48 | "css-loader": "^6.7.1", 49 | "html-webpack-inline-source-plugin": "^1.0.0-beta.2", 50 | "html-webpack-plugin": "^5.5.0", 51 | "style-loader": "^3.3.1", 52 | "ts-loader": "^9.3.0", 53 | "typescript": "^4.6.4", 54 | "url-loader": "^4.1.1", 55 | "webpack": "^5.75.0", 56 | "webpack-cli": "^5.1.4" 57 | }, 58 | "scripts": { 59 | "test": "echo \"Error: no test specified\" && exit 1", 60 | "build": "webpack --mode=production", 61 | "watch": "webpack --mode=development --watch" 62 | }, 63 | "figma-plugin": { 64 | "editorType": [ 65 | "figma" 66 | ], 67 | "id": "wrap", 68 | "name": "wrap", 69 | "main": "src/main.ts" 70 | } 71 | } -------------------------------------------------------------------------------- /src/code.ts: -------------------------------------------------------------------------------- 1 | // @realvjy 2 | import { useEffect, useState, useRef } from "react"; 3 | import { convertToCamelCase, convertToSVG } from "./components/helpers"; 4 | import svgToJsx from "./lib/svg-to-jsx"; 5 | const template = require("lodash.template"); 6 | 7 | import * as selection from "./lib/selection"; 8 | 9 | /** 10 | * current selection stored so its accessible later 11 | */ 12 | let SelectionNodes: readonly SceneNode[] = []; 13 | 14 | // 3 March, 2024 15 | export type PluginSettings = { 16 | framework: "react"; 17 | jsx: boolean; 18 | }; 19 | 20 | async function getUserModePreference() { 21 | const storedMode = await figma.clientStorage.getAsync("preferredMode"); 22 | return storedMode || "light"; // Default to light mode if no preference exists 23 | } 24 | 25 | async function setUserModePreference(mode: "light" | "dark") { 26 | await figma.clientStorage.setAsync("preferredMode", mode); 27 | } 28 | 29 | export const standardMode = async (settings: PluginSettings) => { 30 | figma.showUI(__html__, { width: 400, height: 850, themeColors: true }); 31 | // Subscribe to the selectionchange event to run the code when the selection changes 32 | onSelectionChange(); 33 | figma.on("selectionchange", async () => { 34 | // Code that should run when the selection changes 35 | onSelectionChange(); 36 | }); 37 | 38 | async function onSelectionChange() { 39 | console.log("changed"); 40 | SelectionNodes = selection.onChange(); 41 | 42 | if (SelectionNodes.length === 0) { 43 | figma.ui.postMessage({ 44 | type: "empty", 45 | }); 46 | return; 47 | } 48 | 49 | let result = ""; 50 | let svgCode = null; 51 | const selectedNode = SelectionNodes[0]; 52 | switch (settings.framework) { 53 | case "react": 54 | result = "react"; 55 | svgCode = await convertToSVG(selectedNode); 56 | break; 57 | } 58 | 59 | figma.ui.postMessage({ 60 | type: "code", 61 | name: selectedNode.name, 62 | data: result, 63 | settings: svgCode, 64 | }); 65 | } 66 | }; 67 | 68 | function reactify(svg, { type = "functional", name }) { 69 | const data = { 70 | parentComponent: `React.Component`, 71 | componentName: `${name}`, 72 | }; 73 | 74 | const compile = template(TEMPLATES[type]); 75 | const component = compile({ 76 | ...data, 77 | svg, 78 | }); 79 | 80 | return component; 81 | } 82 | 83 | figma.ui.onmessage = (msg) => {}; 84 | const TEMPLATES = { 85 | functional: `// Generated from SVG to React Figma Plugin 86 | import React from "react"; 87 | 88 | export const <%= componentName %> = (props) => ( 89 | <%= svg %> 90 | ); 91 | `, 92 | }; 93 | let code = ""; 94 | const codegenMode = async (settings: PluginSettings) => { 95 | figma.codegen.on("generate", async (e) => { 96 | code = await onSelectionChange(e.node); 97 | return [ 98 | { 99 | title: "SVG to JSX", 100 | language: "JAVASCRIPT", 101 | code: code, 102 | }, 103 | ]; 104 | }); 105 | 106 | async function onSelectionChange(node) { 107 | if (!node) { 108 | figma.ui.postMessage({ 109 | type: "empty", 110 | }); 111 | return ""; 112 | } 113 | 114 | const svgCode = await convertToSVG(node); 115 | const jsxCode = await new Promise((resolve, reject) => { 116 | svgToJsx(svgCode, function (error, jsx) { 117 | if (error) { 118 | console.error(error); 119 | reject(error); 120 | } else { 121 | resolve(jsx); 122 | } 123 | }); 124 | }); 125 | 126 | const componentName = convertToCamelCase(node.name); 127 | return reactify(jsxCode, { 128 | type: "functional", 129 | name: componentName, 130 | }); 131 | } 132 | }; 133 | 134 | figma.ui.on("message", async (msg) => {}); 135 | 136 | switch (figma.mode) { 137 | case "default": 138 | case "inspect": 139 | console.log("Inspect mode"); 140 | SelectionNodes = selection.onChange(); 141 | 142 | standardMode({ framework: "react", jsx: true }); 143 | break; 144 | case "codegen": 145 | console.log("Codegen mode"); 146 | codegenMode({ framework: "react", jsx: true }); 147 | break; 148 | default: 149 | break; 150 | } 151 | 152 | // run timerwatch when plugin starts 153 | // selection.timerWatch(); 154 | 155 | figma.on("close", () => { 156 | console.log("closing"); 157 | 158 | selection.setPluginClose(true); 159 | }); 160 | -------------------------------------------------------------------------------- /src/components/helpers.ts: -------------------------------------------------------------------------------- 1 | // by @realvjy 2 | // 5 March, 2024 3 | 4 | export const handleDownloadPNG = (imgRef, canvasRef) => { 5 | const canvasS = canvasRef.current; 6 | }; 7 | 8 | // Get image and return image data to add on figma 9 | 10 | export const getImageData = (image, canvasRef) => { 11 | const canvas = canvasRef.current; 12 | 13 | canvas.width = image.width; 14 | canvas.height = image.height; 15 | const context = canvas.getContext("2d"); 16 | context.drawImage(image, 0, 0); 17 | return { 18 | imageData: context.getImageData(0, 0, image.width, image.height), 19 | canvas, 20 | context, 21 | }; 22 | }; 23 | 24 | // Load image from the view 25 | export const loadImage = async (src, imgRef) => 26 | new Promise((resolve, reject) => { 27 | const img = imgRef.current; 28 | 29 | img.crossOrigin = "anonymous"; 30 | img.onload = () => resolve(img); 31 | img.onerror = (...args) => reject(args); 32 | img.src = src; 33 | }); 34 | 35 | // Encode image to object to upload on figma 36 | export async function encodeFigma(canvas, ctx, imageData) { 37 | ctx.putImageData(imageData, 0, 0); 38 | 39 | return await new Promise((resolve, reject) => { 40 | canvas.toBlob((blob) => { 41 | const reader = new FileReader(); 42 | //@ts-ignore 43 | reader.onload = () => resolve(new Uint8Array(reader.result)); 44 | reader.onerror = () => reject(new Error("Could not read from blob")); 45 | reader.readAsArrayBuffer(blob); 46 | }); 47 | }); 48 | } 49 | 50 | // Set Image on Figma convas 51 | export const setBg = async (imageData) => { 52 | parent.postMessage( 53 | { 54 | pluginMessage: { 55 | type: "set-bg", 56 | data: { imageData }, 57 | }, 58 | }, 59 | "*" 60 | ); 61 | }; 62 | 63 | export const svgBase64 = (svg) => { 64 | var base64 = btoa(svg); 65 | return `data:image/svg+xml;base64,${base64}`; 66 | }; 67 | 68 | // Fix Node Type Issue 69 | // Group Node and Section not work properly with fill 70 | export const checkNode = (node) => { 71 | const type = node.type; 72 | 73 | if (type === "TEXT") { 74 | return false; 75 | } 76 | return true; 77 | }; 78 | 79 | export function getRandomNumberBetween(min, max) { 80 | return Math.floor(Math.random() * (max - min + 1) + min); 81 | } 82 | 83 | export function getRandomFloat(min, max) { 84 | return Math.random() * (min - max) + max; 85 | } 86 | 87 | export function rescaleFactor(dimension) { 88 | switch (dimension) { 89 | case 800: 90 | return 0.5; 91 | break; 92 | 93 | default: 94 | return 1; 95 | break; 96 | } 97 | } 98 | 99 | export function getRandomXYPoint() { 100 | return { 101 | x: getRandomNumberBetween(0, 1600 / 2), 102 | y: getRandomNumberBetween(0, 1600 / 2), 103 | }; 104 | } 105 | 106 | export function getRandomShapeDimension( 107 | MIN_SHAPE_SIZE = 0.5, 108 | MAX_SHAPE_SIZE = 0.8 109 | ) { 110 | return getRandomNumberBetween(1600 * MIN_SHAPE_SIZE, 1600 * MAX_SHAPE_SIZE); 111 | } 112 | 113 | // adjust noise 114 | export function adjustNoiseParameters(value) { 115 | const baseFrequencyRange = [0.1, 0.8]; // Range of baseFrequency values 116 | const numOctavesRange = [6, 18]; // Range of numOctaves values 117 | 118 | // Reverse the scaling logic for baseFrequency 119 | const baseFrequency = 120 | (baseFrequencyRange[1] - baseFrequencyRange[0]) * ((16 - value) / 16) + 121 | baseFrequencyRange[0]; 122 | 123 | // Reverse the scaling logic for numOctaves 124 | const numOctaves = Math.floor( 125 | (numOctavesRange[1] - numOctavesRange[0]) * ((16 - value) / 16) + 126 | numOctavesRange[0] 127 | ); 128 | 129 | // Generate a random seed value 130 | const seed = Math.floor(Math.random() * 1000); 131 | 132 | return { baseFrequency, numOctaves, seed }; 133 | } 134 | 135 | // 136 | export const calculateAspectRatioFit = ( 137 | srcWidth, 138 | srcHeight, 139 | maxWidth, 140 | maxHeight 141 | ) => { 142 | var ratio = Math.min(maxWidth / srcWidth, maxHeight / srcHeight); 143 | 144 | return { width: srcWidth * ratio, height: srcHeight * ratio, ratio: ratio }; 145 | }; 146 | 147 | export const convertToSVG = async (data) => { 148 | const vectorShape = data; 149 | 150 | console.log(vectorShape); 151 | 152 | // const selectedNode = selection[0]; 153 | const svg = await vectorShape.exportAsync({ format: "SVG_STRING" }); 154 | 155 | return svg; 156 | }; 157 | 158 | export const convertToCamelCase = (inputString) => { 159 | // Remove leading numbers, symbols, and hyphens 160 | if (!inputString) { 161 | return ""; 162 | }; 163 | let cleanedString = inputString 164 | .replace(/^[^a-zA-Z_]+/, "") 165 | .replace(/[^a-zA-Z0-9_]+/g, ""); 166 | 167 | // Split the string into words 168 | const words = cleanedString.split(/[\s-_]+/); 169 | 170 | // Capitalize the first letter of each word and join with underscores 171 | const camelCaseString = words 172 | .map((word, index) => { 173 | return index === 0 174 | ? word.charAt(0).toUpperCase() + word.slice(1).toLowerCase() 175 | : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); 176 | }) 177 | .join("_"); 178 | 179 | return camelCaseString; 180 | 181 | } 182 | -------------------------------------------------------------------------------- /src/components/icons.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | interface IconProps extends React.SVGProps { 4 | size?: number; 5 | } 6 | 7 | export const WrapIcon: React.FC = ({ size, ...props }) => ( 8 | 18 | 19 | 20 | ); 21 | 22 | export const SearchIcon = ({ 23 | height = "32px", 24 | width = "32px", 25 | color = "black", 26 | secondaryColor, 27 | ...props 28 | }: React.SVGProps & { secondaryColor?: string }) => ( 29 | 39 | 40 | 41 | ); 42 | -------------------------------------------------------------------------------- /src/components/svgutil.ts: -------------------------------------------------------------------------------- 1 | const isPlainObject = require("lodash.isplainobject"); 2 | const isString = require("lodash.isstring"); 3 | const camelCase = require("lodash.camelcase"); 4 | const nativeCSS = require("css-to-object"); 5 | const template = require("lodash.template"); 6 | 7 | /** 8 | * React component templates. 9 | * @readonly 10 | * @type {Map} 11 | */ 12 | const TEMPLATES = { 13 | class: ` 14 | import React from "react"; 15 | 16 | class Icon extends <%= parentComponent %> { 17 | render() { 18 | return <%= svg %>; 19 | } 20 | } 21 | 22 | export default Icon; 23 | `, 24 | functional: ` 25 | import React from "react"; 26 | 27 | function Icon() { 28 | return <%= svg %>; 29 | } 30 | 31 | export default <%= exportComponent %>; 32 | `, 33 | }; 34 | 35 | /** 36 | * Creates React component. 37 | * @param {string} svg Transformed SVG string. 38 | * @param {string="functional","class"} config.type Desired component type. 39 | * @return {string} 40 | */ 41 | function reactify(svg, { type = "functional", memo }) { 42 | const data = { 43 | parentComponent: memo ? `React.PureComponent` : `React.Component`, 44 | exportComponent: memo ? `React.memo(Icon)` : `Icon`, 45 | }; 46 | 47 | const compile = template(TEMPLATES[type]); 48 | const component = compile({ 49 | ...data, 50 | svg, 51 | }); 52 | 53 | return component; 54 | } 55 | 56 | /** 57 | * Stringify style. 58 | * @param {Object=} style Node style. 59 | * @returns {string} 60 | */ 61 | function stringifyStyle(style = {}) { 62 | const proprietyNames = Object.keys(style); 63 | 64 | return proprietyNames.reduce((accumulator, proprietyName) => { 65 | const propriety = style[proprietyName]; 66 | const isStringPropriety = isString(propriety); 67 | 68 | if (isStringPropriety) { 69 | return accumulator + `${proprietyName}: "${propriety}", `; 70 | } 71 | 72 | return accumulator + `${proprietyName}: ${propriety}, `; 73 | }, String()); 74 | } 75 | 76 | /** 77 | * Stringify attributes. 78 | * @param {Object=} attributes Node attributes. 79 | * @returns {string} 80 | */ 81 | function stringifyAttributes(attributes = {}) { 82 | const attributeNames = Object.keys(attributes); 83 | 84 | return attributeNames.reduce((accumulator, attributeName) => { 85 | const attribute = attributes[attributeName]; 86 | const isStyleAttribute = isPlainObject(attribute); 87 | 88 | if (isStyleAttribute) { 89 | return ( 90 | accumulator + ` ${attributeName}={{ ${stringifyStyle(attribute)} }}` 91 | ); 92 | } 93 | 94 | return accumulator + ` ${attributeName}="${attribute}"`; 95 | }, String()); 96 | } 97 | 98 | /** 99 | * Stringify SVG tree. 100 | * @param {Object} node Root node. 101 | * @returns {string} 102 | */ 103 | export function stringify(node) { 104 | if (isString(node)) { 105 | return node; 106 | } 107 | 108 | const attributes = stringifyAttributes(node.attributes); 109 | const buffer = `<${node.name}${attributes}>`; 110 | 111 | const childrensBuffer = node.children.reduce((accumulator, childrenNode) => { 112 | return accumulator + stringify(childrenNode); 113 | }, buffer); 114 | 115 | return childrensBuffer + ``; 116 | } 117 | -------------------------------------------------------------------------------- /src/components/transform.ts: -------------------------------------------------------------------------------- 1 | const isString = require("lodash.isstring"); 2 | import { parse } from "svg-parser"; 3 | const nativeCSS = require("css-to-object"); 4 | const camelCase = require("lodash.camelcase"); 5 | 6 | const CUSTOM_ATTRIBUTES = { 7 | class: "className", 8 | }; 9 | 10 | function transformStyle(style) { 11 | const transformed = nativeCSS(style, { 12 | numbers: true, 13 | camelCase: true, 14 | }); 15 | 16 | return transformed; 17 | } 18 | 19 | export function transform(node) { 20 | // console.log(node.children[0], 'node here'); 21 | 22 | // if (isString(node)) { 23 | // return node; 24 | // } 25 | if (!node || !node.children || node.children.length === 0) { 26 | console.error("Invalid SVG object structure."); 27 | return {}; 28 | } 29 | 30 | const svgElement = node.children.find((child) => child.tagName === "svg"); 31 | 32 | if (!svgElement) { 33 | console.error("SVG element not found in the SVG object."); 34 | return {}; 35 | } 36 | 37 | const attributeNames = Object.keys(svgElement.properties); 38 | console.log(attributeNames, "attributeNames here"); 39 | const attributes = attributeNames.reduce((accumulator, attributeName) => { 40 | const attribute = svgElement.properties[attributeName]; 41 | 42 | return { 43 | ...accumulator, 44 | [attributeName]: attribute, 45 | }; 46 | }, {}); 47 | 48 | console.log(attributes, "attributes here"); 49 | 50 | const children = node.children.map((child) => { 51 | console.log(child, "child here"); 52 | 53 | if (child.children) { 54 | return transform(child); // Recursively extract attributes for nested children 55 | } else { 56 | return child; // If there are no further children, return the child as is 57 | } 58 | }); 59 | 60 | return { 61 | ...node, 62 | children, 63 | attributes, 64 | }; 65 | 66 | // const attributeNames = Object.values(node.children[0].children[0].properties); 67 | // const attributes = attributeNames.reduce((accumulator, attributeName) => { 68 | // const attribute = svgElement.getAttribute(attributeName); 69 | // const isStyleAttribute = attributeName === 'style'; 70 | // const isDataAttribute = attributeName.startsWith('data-'); 71 | // console.log(attribute, 'attribute here'); 72 | // if (isDataAttribute) { 73 | // return { 74 | // ...accumulator, 75 | // [attributeName]: attribute, 76 | // }; 77 | // } 78 | 79 | // if (isStyleAttribute) { 80 | // return { 81 | // ...accumulator, 82 | // [attributeName]: transformStyle(attribute), 83 | // }; 84 | // } 85 | 86 | // return { 87 | // ...accumulator, 88 | // [camelCase(attributeName)]: attribute, 89 | // }; 90 | // }, {}); 91 | 92 | // const children = node.children.map(transform); 93 | // return { 94 | // ...node, 95 | // children, 96 | // attributes, 97 | // }; 98 | // const attributes = attributeNames.reduce((accumulator, attributeName) => { 99 | // const attribute = node.attributes[attributeName]; 100 | // const isStyleAttribute = attributeName === 'style'; 101 | // const isDataAttribute = attributeName.startsWith('data-'); 102 | // if (isDataAttribute) { 103 | // return { 104 | // ...accumulator, 105 | // [attributeName]: attribute, 106 | // }; 107 | // } 108 | // if (isStyleAttribute) { 109 | // return { 110 | // ...accumulator, 111 | // [attributeName]: transformStyle(attribute), 112 | // }; 113 | // } 114 | // if (CUSTOM_ATTRIBUTES[attributeName]) { 115 | // return { 116 | // ...accumulator, 117 | // [CUSTOM_ATTRIBUTES[attributeName]]: attribute, 118 | // }; 119 | // } 120 | // return { 121 | // ...accumulator, 122 | // [camelCase(attributeName)]: attribute, 123 | // }; 124 | // }, {}); 125 | // const children = node.children.map(transform); 126 | // return { 127 | // ...node, 128 | // children, 129 | // attributes, 130 | // }; 131 | } 132 | 133 | /** 134 | * Clean-up and transform SVG into valid JSX. 135 | * @param {string} svg SVG string 136 | * @param {Object} config Output component type and Prettier options. 137 | * @returns {string} 138 | */ 139 | export async function transformer(svg, config = {}) { 140 | console.log(svg, "svg here"); 141 | // Define optimization options 142 | const svgoOptions = { 143 | plugins: [ 144 | // Remove unused IDs 145 | ], 146 | }; 147 | 148 | const parsed = parse(svg); 149 | const transformed = transform(parsed); 150 | console.log(transformed, "transformed here"); 151 | 152 | return "formatted"; 153 | } 154 | -------------------------------------------------------------------------------- /src/lib/selection.ts: -------------------------------------------------------------------------------- 1 | // 8 March, 24 2 | // Selection Handle 3 | // Author: @realvjy 4 | 5 | /** 6 | * bool of whether or not currently selected is already a linked object 7 | */ 8 | export let isLinkedObject = false; 9 | 10 | /** 11 | * 12 | */ 13 | export let prevData: string = ""; 14 | 15 | /** 16 | * @param setData 17 | */ 18 | export const prevDataChange = (setData): string => { 19 | return (prevData = setData); 20 | }; 21 | 22 | /** 23 | * bool state of whether or not plugin is closed 24 | */ 25 | let pluginClose: boolean = false; 26 | 27 | export const setPluginClose = (state: boolean) => { 28 | console.log("closed"); 29 | 30 | pluginClose = state; 31 | }; 32 | 33 | /** 34 | * do things on a selection change 35 | */ 36 | export const onChange = () => { 37 | const selection = figma.currentPage.selection; 38 | return selection; 39 | }; 40 | 41 | /** 42 | * update ui only when selection is changed 43 | * @param value 44 | * @param selection 45 | * @param data 46 | */ 47 | export const send = (value: string, selection = null) => { 48 | console.log("send fuction", selection); 49 | if (selection != null) { 50 | // var svgdata = data.curve.vectorPaths[0].data; 51 | 52 | // if (data.curve.vectorPaths[0].data.match(/M/g).length > 1) 53 | // value = "vectornetwork"; 54 | // const width = 120; 55 | 56 | figma.ui.postMessage({ 57 | type: "svg", 58 | selection: selection, 59 | }); 60 | } else { 61 | figma.ui.postMessage({ type: "rest", value }); 62 | } 63 | }; 64 | 65 | /** 66 | * watch every set 300 milliseconds, if certain objects are selected, watch for changes 67 | */ 68 | export const timerWatch = () => { 69 | setTimeout(function () { 70 | if (!pluginClose) { 71 | let localselection = figma.currentPage.selection; 72 | 73 | send("selection", localselection); 74 | 75 | timerWatch(); 76 | } 77 | return; 78 | }, 300); 79 | }; 80 | -------------------------------------------------------------------------------- /src/lib/svg-to-jsx/bin/svg-to-jsx: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | var fs = require('fs'); 5 | var argv = require('yargs').argv; 6 | var svgtojsx = require('../index.js'); 7 | 8 | var help = argv.h || argv.help; 9 | if (help) { 10 | console.log('svg-to-jsx [options] [input filename]'); 11 | console.log(''); 12 | console.log('options:'); 13 | console.log('\t-h, --help\tDisplay this help screen.'); 14 | console.log('\t-o, --output\tOutput filename. if not provided, output will be spit out to stdout.'); 15 | console.log('\t-p, --pass\tPass this.props to the root JSX tag.'); 16 | console.log('\t-r, --root\tID of an SVG element to output.'); 17 | console.log('\t-c, --renderChildren\tRender this.props.children. Can also be a string value - ID of an element to render children into.'); 18 | console.log('\t --refs\t= pair(s) of key (SVG element ID) and value (JSX ref value), e.g. --refs root=myRef will result in element #root to have attribute ref="myRef".'); 19 | 20 | process.exit(0); 21 | } 22 | 23 | var inputFilename = argv._ && argv._[0]; 24 | var outputFilename = argv.o || argv.output; 25 | 26 | if (!inputFilename) throw new Error('Input file not specified'); 27 | if (!fs.existsSync(inputFilename)) throw new Error('Input file ' + inputFilename + ' does not exist'); 28 | 29 | var svg = fs.readFileSync(inputFilename); 30 | var options = { 31 | passProps: argv.p || argv.pass, 32 | renderChildren: argv.c || argv.renderChildren, 33 | root: argv.r || argv.root, 34 | refs: argv.refs && argv.refs.reduce(function(hash, refDefinition) { 35 | var keyValue = refDefinition.split('='); 36 | var id = keyValue[0]; 37 | var ref = keyValue[1]; 38 | 39 | hash[id] = ref; 40 | 41 | return hash; 42 | }, {}) 43 | }; 44 | 45 | svgtojsx(svg, options, function(error, jsx) { 46 | if (error) throw error; 47 | 48 | if (outputFilename) { 49 | fs.writeFileSync(outputFilename, jsx); 50 | } else { 51 | process.stdout.write(jsx + '\n'); 52 | } 53 | }); 54 | -------------------------------------------------------------------------------- /src/lib/svg-to-jsx/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var q = require('q'); 4 | var assign = require('object-assign'); 5 | var parseString = require('xml2js').parseString; 6 | var xmlbuilder = require('xmlbuilder'); 7 | var utils = require('./utils.js'); 8 | 9 | var defaults = { 10 | passProps: false, 11 | passChildren: false, 12 | root: null, 13 | refs: null 14 | }; 15 | 16 | function cleanupParsedSVGElement(xpath, previousSibling, element) { 17 | return { 18 | tagName: element['#name'], 19 | attributes: element.$ || {}, 20 | children: element.$$ || [], 21 | text: element._ 22 | }; 23 | } 24 | 25 | function parseSVG(svg, callback) { 26 | parseString(svg, { 27 | explicitArray: true, 28 | explicitChildren: true, 29 | explicitRoot: false, 30 | mergeAttrs: false, 31 | normalize: true, 32 | normalizeTags: false, 33 | preserveChildrenOrder: true, 34 | attrNameProcessors: [utils.processAttributeName], 35 | validator: cleanupParsedSVGElement 36 | }, callback); 37 | } 38 | 39 | function afterParseSVG(parsedSVG) { 40 | utils.forEach(parsedSVG, function (element) { 41 | // if (element.tagName === 'use') { 42 | // var referenceHref = element.attributes['xlink:href'] || ''; 43 | // var referenceID = referenceHref.slice(1); 44 | // var reference = utils.filter(parsedSVG, function (ch) { 45 | // return ch.attributes.id === referenceID; 46 | // }).shift(); 47 | 48 | // if (reference) { 49 | // element.attributes = assign({}, reference.attributes, element.attributes); 50 | // element.children = reference.children; 51 | // element.tagName = reference.tagName; 52 | // element.text = reference.text; 53 | 54 | // delete element.attributes.id; 55 | // delete element.attributes['xlink:href']; 56 | // } 57 | // } 58 | 59 | if (!utils.supportsAllAttributes(element)) { 60 | element.attributes = utils.sanitizeAttributes(element.attributes); 61 | } 62 | 63 | element.children = utils.sanitizeChildren(element.children); 64 | }); 65 | 66 | return parsedSVG; 67 | } 68 | 69 | function formatElementForXMLBuilder(element) { 70 | 71 | var attributes = element.attributes; 72 | var children = element.children && element.children.map(formatElementForXMLBuilder); 73 | 74 | var result = Object.keys(attributes).reduce(function (hash, name) { 75 | hash['@' + name] = attributes[name]; 76 | 77 | return hash; 78 | }, {}); 79 | 80 | if (element.text) result['#text'] = element.text; 81 | 82 | if (children && children.length) { 83 | children.forEach(function (child) { 84 | var tagName = Object.keys(child)[0]; 85 | var existingValue = result[tagName]; 86 | if (existingValue) { 87 | if (Array.isArray(existingValue)) { 88 | // existing element array, push new element 89 | existingValue.push(child[tagName]); 90 | } else { 91 | // create array with existing and new elements 92 | result[tagName] = [existingValue, child[tagName]]; 93 | } 94 | } else { 95 | // first child element with this tag name 96 | result[tagName] = child[tagName]; 97 | } 98 | }); 99 | } 100 | 101 | var wrapped = {}; 102 | wrapped[element.tagName] = result; 103 | 104 | return wrapped; 105 | } 106 | 107 | function beforeBuildSVG(options, parsed) { 108 | if (options.root) { 109 | var root = utils.findById(parsed, options.root); 110 | if (!root) throw new Error('Cannot find root element #' + options.root); 111 | 112 | parsed = root; 113 | } 114 | 115 | if (options.refs) { 116 | Object.keys(options.refs).forEach(function (id) { 117 | var ref = options.refs[id]; 118 | 119 | var element = utils.findById(parsed, id); 120 | if (!element) throw new Error('Cannot find element #' + id + ' for ref ' + ref); 121 | 122 | element.attributes.ref = ref; 123 | }); 124 | } 125 | 126 | if (options.passProps) { 127 | parsed.attributes.passProps = 1; 128 | } 129 | 130 | if (options.renderChildren) { 131 | var passChildrenToSpecificId = typeof (options.renderChildren) === 'string'; 132 | var passChildrenTo = passChildrenToSpecificId ? utils.findById(parsed, options.renderChildren) : parsed; 133 | 134 | if (!passChildrenTo) throw new Error('Cannot find element #' + options.renderChildren + ' to render children into'); 135 | 136 | passChildrenTo.text = [passChildrenTo.text || '', '{this.props.children}'].join('\n'); 137 | } 138 | 139 | return formatElementForXMLBuilder(parsed); 140 | } 141 | 142 | function afterBuildSVG(built) { 143 | return built 144 | .replace(/style="((?:[^"\\]|\\.)*)"/ig, function (matched, styleString) { 145 | var style = styleString.split(/\s*;\s*/g).filter(Boolean).reduce(function (hash, rule) { 146 | var keyValue = rule.split(/\s*\:\s*(.*)/); 147 | var property = utils.cssProperty(keyValue[0]); 148 | var value = keyValue[1]; 149 | 150 | hash[property] = value; 151 | 152 | return hash; 153 | }, {}); 154 | 155 | return 'style={' + JSON.stringify(style) + '}'; 156 | }) 157 | .replace(/passProps="1"/, '{...this.props}'); 158 | } 159 | 160 | function buildSVG(object) { 161 | return xmlbuilder 162 | .create(object, { headless: true }) 163 | .end({ pretty: true, indent: '\t', newline: '\n' }); 164 | } 165 | 166 | module.exports = function svgToJsx(svg, options, callback) { 167 | if (arguments.length === 2) { 168 | callback = options; 169 | options = {}; 170 | } 171 | 172 | options = assign({}, defaults, options); 173 | 174 | var promise = q 175 | .nfcall(parseSVG, svg) 176 | .then(afterParseSVG) 177 | .then(beforeBuildSVG.bind(null, options)) 178 | .then(buildSVG) 179 | .then(afterBuildSVG); 180 | 181 | if (callback) { 182 | promise.done(function (result) { 183 | callback(null, result); 184 | }, function (error) { 185 | callback(error, null); 186 | }); 187 | } 188 | 189 | return promise; 190 | }; 191 | -------------------------------------------------------------------------------- /src/lib/svg-to-jsx/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assign = require('object-assign'); 4 | 5 | var ALLOWED_HTML_ATTRIBUTES = 'accept acceptCharset accessKey action allowFullScreen allowTransparency alt async autoComplete autoFocus autoPlay cellPadding cellSpacing charSet checked classID className colSpan cols content contentEditable contextMenu controls coords crossOrigin data dateTime defer dir disabled download draggable encType form formAction formEncType formMethod formNoValidate formTarget frameBorder headers height hidden high href hrefLang htmlFor httpEquiv icon id label lang list loop low manifest marginHeight marginWidth max maxLength media mediaGroup method min multiple muted name noValidate open optimum pattern placeholder poster preload radioGroup readOnly rel required role rowSpan rows sandbox scope scoped scrolling seamless selected shape size sizes span spellCheck src srcDoc srcSet start step style tabIndex target title type useMap value width wmode'.split(' '); 6 | var ALLOWED_SVG_ATTRIBUTES = 'clipPath cx cy d dx dy fill fillOpacity fontFamily fillRule fontSize fx fy gradientTransform gradientUnits markerEnd markerMid markerStart offset opacity patternContentUnits patternUnits points preserveAspectRatio r rx ry spreadMethod stopColor stopOpacity stroke strokeDasharray strokeLinecap strokeLinejoin strokeMiterlimit strokeOpacity strokeWidth textAnchor transform vectorEffect version viewBox xmlns x1 x2 x y1 y2 y xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space mask maskUnits filter filterUnits filterRes result in in2 result stdDeviation x y colorInterpolationFilters floodOpacity primitiveUnits'.split(' '); 7 | 8 | var ALLOWED_ATTRIBUTES = ALLOWED_HTML_ATTRIBUTES.concat(ALLOWED_SVG_ATTRIBUTES); 9 | var ALLOWED_TAGS = 'circle clipPath defs ellipse g image line linearGradient mask path pattern polygon polyline radialGradient rect stop svg text use tspan title style filter feFlood feBlend feGaussianBlur'.split(' '); 10 | 11 | var DATA_ATTRIBUTE = /^data-/i; 12 | 13 | exports.cssProperty = function (string) { 14 | var unprefixed = string.replace(/^-ms/, 'ms'); 15 | 16 | return exports.camelCase(unprefixed); 17 | }; 18 | 19 | exports.camelCase = function (string) { 20 | if (string.indexOf('--') === 0) return string; 21 | return string.replace(/(?:-|_)([a-z])/g, function (g) { return g[1].toUpperCase(); }); 22 | }; 23 | 24 | exports.processAttributeName = function (name) { 25 | return DATA_ATTRIBUTE.test(name) ? name : exports.camelCase(name); 26 | }; 27 | 28 | exports.unnamespaceAttributeName = function (name) { 29 | return name.replace(/(\w+):(\w)/i, function (match, namespace, char) { 30 | return namespace + char.toUpperCase(); 31 | }); 32 | }; 33 | 34 | exports.sanitizeAttributes = function (attributes) { 35 | if (!attributes) return null; 36 | 37 | if (attributes.class) { 38 | attributes.className = attributes.class; 39 | delete attributes.class; 40 | } 41 | 42 | var allowed = ALLOWED_ATTRIBUTES.reduce(function (hash, name) { 43 | if (name in attributes) { 44 | var unnamespacedName = exports.unnamespaceAttributeName(name); 45 | 46 | hash[unnamespacedName] = attributes[name]; 47 | } 48 | 49 | return hash; 50 | }, {}); 51 | 52 | var custom = Object.keys(attributes).filter(function (name) { 53 | return DATA_ATTRIBUTE.test(name); 54 | }).reduce(function (data, name) { 55 | data[name] = attributes[name]; 56 | 57 | return data; 58 | }, {}); 59 | 60 | return assign(allowed, custom); 61 | }; 62 | 63 | exports.sanitizeChildren = function (children) { 64 | if (!children) return null; 65 | 66 | return children.filter(function isTagAllowed(child) { 67 | return ALLOWED_TAGS.indexOf(child.tagName) !== -1; 68 | }); 69 | }; 70 | 71 | exports.styleAttribute = function (string) { 72 | var object = string.split(/\s*;\s*/g).reduce(function (hash, keyValue) { 73 | var split = keyValue.split(/\s*\:\s*/); 74 | var key = exports.camelCase((split[0] || '').trim()); 75 | var value = (split[1] || '').trim(); 76 | 77 | hash[key] = value; 78 | 79 | return hash; 80 | }, {}); 81 | 82 | return JSON.stringify(object); 83 | }; 84 | 85 | exports.forEach = function (element, callback) { 86 | element.children && element.children.forEach(function (child) { 87 | exports.forEach(child, callback); 88 | }); 89 | 90 | callback(element); 91 | }; 92 | 93 | exports.filter = function (element, test) { 94 | var filtered = []; 95 | 96 | exports.forEach(element, function (child) { 97 | if (test(child)) filtered.push(child); 98 | }); 99 | 100 | return filtered; 101 | }; 102 | 103 | exports.findById = function (element, id) { 104 | return exports.filter(element, function (node) { 105 | return node.attributes.id === id; 106 | }).shift() || null; 107 | }; 108 | 109 | exports.supportsAllAttributes = function (element) { 110 | var hasHyphen = element.tagName.indexOf('-') !== -1; 111 | var hasIsAttribute = 'is' in element.attributes; 112 | 113 | return hasHyphen || hasIsAttribute; 114 | }; 115 | -------------------------------------------------------------------------------- /src/preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realvjy/svg-to-code-figma/62a5f94d611a25113601ad12381c259e6d26b61a/src/preview.jpg -------------------------------------------------------------------------------- /src/ui.css: -------------------------------------------------------------------------------- 1 | 2 | .figma-light body { 3 | --shadow-hud: 0 5px 17px rgba(0, 0, 0, 0.2), 0 2px 7px rgba(0, 0, 0, 0.15); 4 | --shadow-floating-window: 0 2px 14px rgba(0, 0, 0, 0.15); 5 | --size-xxxsmall: 4px; 6 | --size-xxsmall: 8px; 7 | --list-hover-bg: rgba(245, 245, 245, 0.5); 8 | --image-filter: none; 9 | --primary-button-bg: #000; 10 | --primary-button-text-color: #fff; 11 | --node-bg: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjIiIGhlaWdodD0iMTciIHZpZXdCb3g9IjAgMCAyMiAxNyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTcuNSA2LjI1SDEwLjc1VjE2Ljc1SDAuMjVWNi4yNUgzLjVIMy42MjAxNkwzLjY5NTIyIDYuMTU2MTdMNS41IDMuOTAwMkw3LjMwNDc4IDYuMTU2MTdMNy4zNzk4NCA2LjI1SDcuNVoiIHN0cm9rZT0iYmxhY2siIHN0cm9rZS13aWR0aD0iMC41Ii8+CjxwYXRoIGQ9Ik0xOC41IDYuMjVIMjEuNzVWMTYuNzVIMTEuMjVWNi4yNUgxNC41SDE0LjYyMDJMMTQuNjk1MiA2LjE1NjE3TDE2LjUgMy45MDAyTDE4LjMwNDggNi4xNTYxN0wxOC4zNzk4IDYuMjVIMTguNVoiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iMC41Ii8+CjxyZWN0IHg9IjEyLjkiIHk9IjcuOSIgd2lkdGg9IjcuMiIgaGVpZ2h0PSI3LjIiIHN0cm9rZT0iIzk2OTY5NiIgc3Ryb2tlLW9wYWNpdHk9IjAuNDIiIHN0cm9rZS13aWR0aD0iMC4yIi8+CjxyZWN0IHg9IjEuOSIgeT0iNy45IiB3aWR0aD0iNy4yIiBoZWlnaHQ9IjcuMiIgc3Ryb2tlPSIjOTY5Njk2IiBzdHJva2Utb3BhY2l0eT0iMC40MiIgc3Ryb2tlLXdpZHRoPSIwLjIiLz4KPC9zdmc+Cg==) left center 12 | } 13 | 14 | .figma-dark body { 15 | --list-hover-bg: rgba(245, 245, 245, 0.15); 16 | --svg-fill-color: rgba(255, 255, 255); 17 | --footer-svg-fill: #fff; 18 | --image-filter: invert(100%) grayscale(1); 19 | --primary-button-bg: #fff; 20 | --primary-button-text-color: #000; 21 | --node-bg: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjIiIGhlaWdodD0iMTciIHZpZXdCb3g9IjAgMCAyMiAxNyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTcuNSA2LjI1SDEwLjc1VjE2Ljc1SDAuMjVWNi4yNUgzLjVIMy42MjAxNkwzLjY5NTIyIDYuMTU2MTdMNS41IDMuOTAwMkw3LjMwNDc4IDYuMTU2MTdMNy4zNzk4NCA2LjI1SDcuNVoiIHN0cm9rZT0iYmxhY2siIHN0cm9rZS13aWR0aD0iMC41Ii8+CjxwYXRoIGQ9Ik0xOC41IDYuMjVIMjEuNzVWMTYuNzVIMTEuMjVWNi4yNUgxNC41SDE0LjYyMDJMMTQuNjk1MiA2LjE1NjE3TDE2LjUgMy45MDAyTDE4LjMwNDggNi4xNTYxN0wxOC4zNzk4IDYuMjVIMTguNVoiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iMC41Ii8+CjxyZWN0IHg9IjEyLjkiIHk9IjcuOSIgd2lkdGg9IjcuMiIgaGVpZ2h0PSI3LjIiIHN0cm9rZT0iIzk2OTY5NiIgc3Ryb2tlLW9wYWNpdHk9IjAuNDIiIHN0cm9rZS13aWR0aD0iMC4yIi8+CjxyZWN0IHg9IjEuOSIgeT0iNy45IiB3aWR0aD0iNy4yIiBoZWlnaHQ9IjcuMiIgc3Ryb2tlPSIjOTY5Njk2IiBzdHJva2Utb3BhY2l0eT0iMC40MiIgc3Ryb2tlLXdpZHRoPSIwLjIiLz4KPC9zdmc+Cg==) right center 22 | } 23 | 24 | * { 25 | 26 | --bg-primary: #2c2c2c; 27 | --bg-secondary: #1c1c1c; 28 | --text-disable: #5e5e5e; 29 | --grey-alpha01: rgba(45,45,45,75%); 30 | --grey-alpha02: rgba(80,80,80,75%); 31 | --grey-alpha05: rgba(128,128,128,80%); 32 | --grey-alpha06: rgba(88,88,88,80%); 33 | --text-primary: #ffffff; 34 | --text-secondary: #777777; 35 | --purple: #7B61FF; 36 | --blue: #0080F6; 37 | --blue-dark: #0067ee; 38 | --notify-bg: rgba(250, 55, 200, 80%); 39 | --font-stack: "Inter", sans-serif; 40 | --font-size-xsmall: 11px; 41 | --font-size-small: 12px; 42 | --font-size-large: 13px; 43 | --font-size-xlarge: 14px; 44 | --font-weight-normal: 400; 45 | --font-weight-medium: 500; 46 | --font-weight-bold: 600; 47 | --font-line-height: 16px; 48 | --font-line-height-large: 24px; 49 | --font-letter-spacing-pos-xsmall: 0.005em; 50 | --font-letter-spacing-neg-xsmall: 0.01em; 51 | --font-letter-spacing-pos-small: 0; 52 | --font-letter-spacing-neg-small: 0.005em; 53 | --font-letter-spacing-pos-large: -0.0025em; 54 | --font-letter-spacing-neg-large: 0.0025em; 55 | --font-letter-spacing-pos-xlarge: -0.001em; 56 | --font-letter-spacing-neg-xlarge: -0.001em; 57 | --border-radius-small: 2px; 58 | --border-radius-med: 5px; 59 | --border-radius-large: 6px; 60 | --just-white: #fff; 61 | 62 | --radius-m: 6px; 63 | --radius-l: 8px; 64 | 65 | --base-clr: #101010; 66 | --base-middle-clr: #222222; 67 | --base-light-clr: #323232; 68 | 69 | --secondary-clr: #7748fc; 70 | --primary-clr: #fff; 71 | 72 | --err-clr: #f45; 73 | 74 | --checker-clr: rgba(131, 131, 131, 0.2); 75 | 76 | --transition-m: 0.1s ease-out; 77 | --transition-l: 0.2s ease-out; 78 | --transition-xl: 0.3s ease-out; 79 | } 80 | 81 | body { 82 | margin: 0; 83 | font-family: "Inter", sans-serif; 84 | -webkit-font-smoothing: antialiased; 85 | background-color: var(--bg-primary); 86 | color: var(--text-primary); 87 | } 88 | 89 | /* ::-webkit-scrollbar { 90 | width: 6px; 91 | height: 0px; 92 | } 93 | 94 | ::-webkit-scrollbar-track { 95 | padding: 0 1px; 96 | background: var(--grey-alpha01); 97 | border-left: 1px solid var(--grey-alpha05); 98 | } 99 | 100 | ::-webkit-scrollbar-thumb { 101 | border-radius: 3px; 102 | background: red; 103 | } 104 | 105 | ::-webkit-scrollbar-thumb:hover { 106 | background: #555; 107 | } */ 108 | 109 | button { 110 | font-family: "Inter", sans-serif; 111 | border-radius: 5px; 112 | background: white; 113 | color: black; 114 | border: none; 115 | padding: 8px 15px; 116 | box-shadow: inset 0 0 0 1px black; 117 | outline: none; 118 | } 119 | 120 | #create { 121 | box-shadow: none; 122 | background: #0d99ff; 123 | color: white; 124 | } 125 | 126 | input { 127 | border: none; 128 | outline: none; 129 | padding: 8px; 130 | } 131 | 132 | input:hover { 133 | box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1); 134 | } 135 | 136 | button:focus { 137 | box-shadow: inset 0 0 0 1px transparent; 138 | } 139 | 140 | #create:focus { 141 | box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.3); 142 | } 143 | 144 | input:focus { 145 | box-shadow: inset 0 0 0 1px #0d99ff; 146 | } 147 | 148 | /* .figma-light body { 149 | background-color: white; 150 | color: blue; 151 | } 152 | 153 | .figma-dark body { 154 | background-color: black; 155 | color: red; 156 | } */ 157 | 158 | .searchicon { 159 | position: absolute; 160 | top: 0; 161 | left: 0; 162 | padding: 4px; 163 | } 164 | 165 | .left-icon { 166 | display: flex; 167 | align-items: center; 168 | } 169 | 170 | .left-icon span { 171 | color: var(--figma-color-icon-disabled); 172 | margin-left: 8px; 173 | font-size: var(--font-size-xsmall); 174 | font-weight: 600; 175 | } 176 | 177 | .logotype button { 178 | min-height: 80px; 179 | } 180 | 181 | .logotype button img { 182 | max-width: 62px; 183 | max-height: 62px; 184 | } 185 | 186 | .show { 187 | display: block; 188 | visibility: visible; 189 | font-size: 9px; 190 | color: var(--figma-color-text-secondary); 191 | text-align: center; 192 | display: -webkit-box; 193 | -webkit-line-clamp: 2; 194 | -webkit-box-orient: vertical; 195 | overflow: hidden; 196 | text-overflow: ellipsis; 197 | } 198 | 199 | .hide { 200 | display: none; 201 | visibility: hidden; 202 | } 203 | 204 | .range{ 205 | display: flex; 206 | gap: 8px; 207 | } 208 | 209 | 210 | 211 | /* React Color Graidnet Picker */ 212 | .controls-wrapper .ap { 213 | flex: none; 214 | box-sizing: border-box; 215 | background-color: #fff; 216 | border: 1px solid #4374ad; 217 | border-radius: 50%; 218 | display: inline-block; 219 | position: relative; 220 | cursor: pointer; 221 | margin-top: -2px; 222 | } 223 | .controls-wrapper .ap .apc { 224 | width: 6px; 225 | position: absolute; 226 | left: 0; 227 | right: 0; 228 | top: 0; 229 | bottom: 0; 230 | margin: auto; 231 | } 232 | .controls-wrapper .ap .aph { 233 | width: 6px; 234 | height: 6px; 235 | background-color: #4374ad; 236 | display: inline-block; 237 | border-radius: 50%; 238 | position: absolute; 239 | left: 0; 240 | right: 0; 241 | top: 4px; 242 | margin: auto; 243 | cursor: pointer; 244 | } 245 | .controls-wrapper .ai { 246 | background: #f2f2f2; 247 | display: flex; 248 | flex: 1; 249 | margin: 0 12px; 250 | justify-content: space-around; 251 | align-items: center; 252 | } 253 | .controls-wrapper .ai input { 254 | border: none; 255 | text-align: center; 256 | width: 48px; 257 | color: #0c0c09; 258 | background: inherit; 259 | } 260 | .controls-wrapper .ai span { 261 | padding: 5px; 262 | color: #000; 263 | cursor: pointer; 264 | user-select: none; 265 | font-size: 14px; 266 | } 267 | .cs { 268 | height: 17px; 269 | position: absolute; 270 | width: 11px; 271 | cursor: pointer; 272 | background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAARCAQAAABzuJQIAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QA/4ePzL8AAAB7SURBVCjP3c7BDQMhDETRP4gyUlb6SEphe9w+JgdArIMPm2uGizV6MpbpERgBzGb0tuZcLvTbDSoHrMUMQmFrY/IaaOeKB7yE+1yTz43irZNXbXcCFosuXqFt9g0cyZICT7LkrU+b62vGeVv4IX+Phc+tfABZK3xj5cgHC29ECUeufxkAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTctMDUtMzFUMTg6NDE6MzIrMDI6MDD31tLGAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE3LTA1LTMxVDE4OjQxOjMyKzAyOjAwhotqegAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAAASUVORK5CYII=) 273 | right center; 274 | } 275 | .cs div { 276 | height: 7px; 277 | left: 2px; 278 | width: 7px; 279 | position: absolute; 280 | top: 8px; 281 | } 282 | .cs .active { 283 | background-position: left center; 284 | } 285 | .cp div { 286 | box-sizing: border-box; 287 | cursor: pointer; 288 | display: inline-block; 289 | height: 16px; 290 | width: 16px; 291 | } 292 | .cp div:hover { 293 | border: 1px solid #fff; 294 | } 295 | .gp { 296 | display: flex; 297 | flex-direction: column; 298 | align-items: center; 299 | } 300 | .gp .gp-flat { 301 | margin: 0 auto; 302 | padding: 10px 0 0 !important; 303 | box-shadow: none !important; 304 | transform: none !important; 305 | } 306 | .type-picker-wrapper { 307 | display: inline-flex; 308 | margin-right: 16px; 309 | } 310 | .type-picker-wrapper .type-picker { 311 | height: 24px; 312 | width: 24px; 313 | border: 1px solid #eee; 314 | filter: grayscale(1); 315 | margin-right: 8px; 316 | cursor: pointer; 317 | transition: all 0.2s ease; 318 | } 319 | .type-picker-wrapper .type-picker.active { 320 | border: 1px solid #4374ad; 321 | filter: grayscale(0); 322 | } 323 | .type-picker-wrapper .type-picker.lg { 324 | background: linear-gradient(270deg, #ddd, #4374ad); 325 | } 326 | .type-picker-wrapper .type-picker.rg { 327 | background: radial-gradient(circle, #fff 0, #4374ad 100%); 328 | } 329 | .gpw { 330 | padding: 20px; 331 | } 332 | .gpw .trigger { 333 | padding: 5px; 334 | background: #fff; 335 | border-radius: 1px; 336 | box-shadow: rgba(0, 0, 0, 0.1) 0 0 0 1px; 337 | display: inline-block; 338 | cursor: pointer; 339 | } 340 | .gpw .trigger .inner { 341 | width: 36px; 342 | height: 14px; 343 | border-radius: 2px; 344 | } 345 | .gpw .popover { 346 | z-index: 2; 347 | margin-top: 6px; 348 | box-shadow: rgba(0, 0, 0, 0.15) 0 0 0 1px, rgba(0, 0, 0, 0.15) 0 8px 16px; 349 | background-color: #fff; 350 | padding: 12px; 351 | border-radius: 4px; 352 | position: absolute; 353 | } 354 | .gpw .overlay { 355 | position: fixed; 356 | top: 0; 357 | right: 0; 358 | bottom: 0; 359 | left: 0; 360 | } 361 | .gpw .controls-wrapper { 362 | margin: 0 -10px 10px; 363 | padding: 6px 0 0 10px; 364 | display: flex; 365 | flex-wrap: wrap; 366 | position: relative; 367 | } 368 | .gpw .controls-wrapper.with-angle { 369 | justify-content: space-around; 370 | align-items: center; 371 | } 372 | 373 | .cs { 374 | background: var(--node-bg) !important; 375 | transform: scale(1.5) translateY(1.4px)!important; 376 | } 377 | 378 | 379 | -------------------------------------------------------------------------------- /src/ui.html: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /src/ui.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ReactDOM from "react-dom"; 3 | 4 | import Home from "./views/home"; 5 | import "./ui.css"; 6 | 7 | const App = (props) => { 8 | const renderPage = () => { 9 | return ; 10 | }; 11 | 12 | return renderPage(); 13 | }; 14 | 15 | ReactDOM.render(, document.getElementById("react-page")); 16 | -------------------------------------------------------------------------------- /src/views/home.tsx: -------------------------------------------------------------------------------- 1 | // home.tsx 2 | // 3 March, 2024 3 | 4 | import * as React from "react"; 5 | import { useEffect, useState, useRef } from "react"; 6 | import svgToJsx from "../lib/svg-to-jsx"; 7 | import { SearchIcon, WrapIcon } from "../components/icons"; 8 | import styled from "styled-components"; 9 | import AceEditor from "react-ace"; 10 | const template = require("lodash.template"); 11 | import "ace-builds/src-noconflict/mode-jsx"; 12 | import "ace-builds/src-noconflict/theme-cloud9_night"; 13 | import "ace-builds/src-noconflict/ext-language_tools"; 14 | import ace from "ace-builds/src-noconflict/ace"; // Assuming you have Ace Editor installed 15 | import { convertToCamelCase } from "../components/helpers"; 16 | import * as copy from "copy-to-clipboard"; 17 | 18 | const rgbToRgba = (rgb, a = 1) => 19 | rgb.replace("rgb(", "rgba(").replace(")", `, ${a})`); 20 | 21 | const TEMPLATES = { 22 | functional: `// Generated from SVG to Code Figma Plugin 23 | import React from "react"; 24 | 25 | export const <%= componentName %> = (props) => ( 26 | <%= svg %> 27 | ); 28 | `, 29 | }; 30 | 31 | function reactify(svg, { type = "functional", name }) { 32 | const data = { 33 | parentComponent: `React.Component`, 34 | componentName: `${name}`, 35 | }; 36 | 37 | const compile = template(TEMPLATES[type]); 38 | const component = compile({ 39 | ...data, 40 | svg, 41 | }); 42 | 43 | return component; 44 | } 45 | 46 | const Home = (props) => { 47 | const canvasRef = useRef(null); 48 | const [state, setState] = useState({}); 49 | const [svg, setSvg] = useState(null); 50 | const [svgCode, setSvgCode] = useState(null); 51 | const [viewCode, setViewCode] = useState(null); 52 | const [svgName, setSvgName] = useState(null); 53 | const [wrap, setWrap] = useState(false); 54 | const [copied, setCopied] = useState(false); 55 | const editorRef = useRef(null); 56 | const [formattedCode, setFormattedCode] = useState(""); 57 | const [numberOfLines, setNumberOfLines] = useState(0); // State to store line count 58 | useEffect(() => { 59 | onmessage = async (event) => { 60 | const message = event.data.pluginMessage; 61 | if (message != null) { 62 | setSvg(message.settings); 63 | let componentName = convertToCamelCase(message.name); 64 | setSvgName(componentName); 65 | } 66 | }; 67 | }, []); 68 | 69 | useEffect(() => { 70 | setViewCode("// Generating code..."); 71 | if (svg == null) { 72 | setViewCode("// Nothing Selected. Select any object"); 73 | return; 74 | } 75 | 76 | // Perform the conversion using svgToJsx 77 | svgToJsx(svg, function (error, jsx) { 78 | let newCode = reactify(jsx, { type: "functional", name: svgName }); 79 | setViewCode(newCode); 80 | }); 81 | }, [svg, svgName]); 82 | 83 | function onChange(newValue) { 84 | console.log("change"); 85 | } 86 | 87 | function toggleWrap() { 88 | setWrap(!wrap); 89 | } 90 | 91 | useEffect(() => { 92 | if (editorRef.current) { 93 | const editor = editorRef.current.editor; // Access the editor instance 94 | const lines = editor.session.getLength(); 95 | setNumberOfLines(lines); 96 | } else { 97 | console.warn("Editor not yet initialized, ignoring line count update"); 98 | } 99 | }, [viewCode]); 100 | 101 | const handleCopyReact = () => { 102 | console.log("copying"); 103 | try { 104 | copy(viewCode); 105 | // This code block will be executed after copy(viewCode) completes 106 | setCopied(true); 107 | // setTimeout function is called after setCopied(true) has finished 108 | setTimeout(() => { 109 | setCopied(false); 110 | }, 1400); 111 | } catch (error) { 112 | // Handle any errors that may occur during the copy(viewCode) operation 113 | console.error("Copy failed:", error); 114 | } 115 | }; 116 | 117 | return ( 118 | 119 | 120 | 121 | 122 | React 123 | 124 | 125 | SwiftUI Soon 126 | 127 | 128 | 129 | 130 |

Line # {numberOfLines}

131 |
132 | 133 | 134 | 135 |
136 |
137 | 138 | 156 | 157 | React/JSX code copied 158 | 159 | 160 | 161 | Copy React Code 162 | 163 |

♥ made by

realvjy 164 |
165 |
166 |
167 | ); 168 | }; 169 | export default Home; 170 | 171 | const ViewWrapper = styled.div` 172 | overflow: hidden; 173 | position: relative; 174 | height: 100%; 175 | display: flex; 176 | flex-direction: column; 177 | `; 178 | const Selectors = styled.div` 179 | position: relative; 180 | display: flex; 181 | flex-direction: row; 182 | justify-content: space-between; 183 | align-items: center; 184 | `; 185 | 186 | const Editor = styled.div` 187 | /* Ace update */ 188 | background-color: var(--bg-secondary); 189 | position: relative; 190 | padding-top: 16px; 191 | overflow: hidden; 192 | flex-grow: 1; 193 | .ace_editor { 194 | font-family: "Roboto Mono", Monaco, "Courier New", monospace; 195 | background-color: var(--bg-secondary); 196 | .ace_gutter { 197 | background: transparent; 198 | border-right: 1px solid var(--grey-alpha01); 199 | color: var(--grey-alpha06); 200 | } 201 | } 202 | .ace_scroller { 203 | bottom: 6px !important; 204 | right: 6px !important; 205 | } 206 | .ace_scrollbar { 207 | &::-webkit-scrollbar { 208 | height: 6px; 209 | width: 6px; 210 | } 211 | 212 | &::-webkit-scrollbar-track { 213 | box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); 214 | background-color: transparent; /* Matches ace monokai */ 215 | border-radius: 0px; 216 | border-left: 1px solid var(--grey-alpha01); 217 | border-top: 1px solid var(--grey-alpha01); 218 | } 219 | 220 | &::-webkit-scrollbar-thumb { 221 | background-color: var(--grey-alpha06); 222 | border-radius: 10px; 223 | &:hover { 224 | background-color: var(--grey-alpha05); 225 | } 226 | } 227 | } 228 | `; 229 | 230 | const LeftSide = styled.div` 231 | position: relative; 232 | `; 233 | 234 | const RightSide = styled.div` 235 | display: flex; 236 | padding-right: 12px; 237 | align-items: center; 238 | gap: 12px; 239 | `; 240 | 241 | const Counter = styled.div` 242 | h4 { 243 | font-size: 14px; 244 | font-weight: 500; 245 | margin: 0; 246 | color: var(--grey-alpha06); 247 | } 248 | `; 249 | 250 | const Btn = styled.button` 251 | appearance: none; 252 | border: 0px; 253 | box-shadow: none; 254 | cursor: pointer; 255 | white-space: nowrap; 256 | text-align: center; 257 | user-select: none; 258 | flex-direction: row; 259 | align-items: center; 260 | flex-grow: 1; 261 | `; 262 | 263 | const SelectBtn = styled(Btn)` 264 | font-size: 13px; 265 | padding: 4px 16px; 266 | line-height: 16px; 267 | display: inline-flex; 268 | white-space: nowrap; 269 | user-select: none; 270 | justify-content: center; 271 | flex-direction: row; 272 | align-items: center; 273 | flex-grow: 1; 274 | min-height: 36px; 275 | background: transparent; 276 | color: var(--text-secondary); 277 | gap: 4px; 278 | border-radius: 0; 279 | &.active { 280 | background-color: var(--bg-secondary); 281 | color: var(--white); 282 | font-weight: 600; 283 | } 284 | &.disable { 285 | color: var(--text-disable); 286 | } 287 | `; 288 | 289 | const Badge = styled.div` 290 | background: var(--purple); 291 | color: var(--text-primary); 292 | text-transform: uppercase; 293 | font-weight: 500; 294 | font-size: 10px; 295 | line-height: 11px; 296 | border-radius: 3px; 297 | padding: 2px 6px; 298 | `; 299 | 300 | const ActionButtons = styled.div` 301 | position: relative; 302 | padding: 12px; 303 | display: flex; 304 | flex-direction: column; 305 | gap: 8px; 306 | `; 307 | 308 | const CopyBtn = styled(Btn)` 309 | border-radius: 0; 310 | width: 100%; 311 | appearance: none; 312 | border: 0px; 313 | box-shadow: none; 314 | cursor: pointer; 315 | font-weight: 500; 316 | font-size: 13px; 317 | padding: 4px 16px; 318 | line-height: 16px; 319 | display: inline-flex; 320 | white-space: nowrap; 321 | text-align: center; 322 | user-select: none; 323 | justify-content: center; 324 | border-radius: 4px; 325 | flex-direction: row; 326 | align-items: center; 327 | flex-grow: 1; 328 | min-height: 36px; 329 | background: var(--blue); 330 | color: var(--white); 331 | gap: 4px; 332 | &:hover { 333 | background: var(--blue-dark); 334 | } 335 | 336 | &.active { 337 | background-color: var(--bg-secondary); 338 | color: var(--white); 339 | font-weight: 600; 340 | } 341 | &.disable { 342 | color: var(--text-disable); 343 | } 344 | `; 345 | 346 | const Credit = styled.div` 347 | display: flex; 348 | flex-direction: row; 349 | align-items: center; 350 | justify-content: center; 351 | gap: 4px; 352 | h4 { 353 | margin: 0; 354 | color: var(--text-secondary); 355 | font-weight: 400; 356 | font-size: 12px; 357 | } 358 | a { 359 | font-weight: 600; 360 | font-size: 12px; 361 | text-decoration: none; 362 | margin: 0; 363 | color: var(--text-secondary); 364 | &:hover { 365 | color: var(--text-primary); 366 | } 367 | } 368 | `; 369 | 370 | const ToggleBtn = styled(Btn)` 371 | cursor: pointer; 372 | line-height: 20px; 373 | padding: 4px; 374 | display: inline-flex; 375 | white-space: nowrap; 376 | text-align: center; 377 | user-select: none; 378 | justify-content: center; 379 | flex-direction: row; 380 | align-items: center; 381 | flex-grow: 1; 382 | background: transparent; 383 | color: var(--text-secondary); 384 | border-radius: 2px; 385 | gap: 4px; 386 | &.active { 387 | background-color: var(--grey-alpha06); 388 | color: var(--white); 389 | font-weight: 600; 390 | } 391 | &:hover { 392 | background: var(--grey-alpha02); 393 | color: var(--white); 394 | } 395 | &.disable { 396 | color: var(--text-disable); 397 | } 398 | `; 399 | 400 | const Notify = styled.div` 401 | top: 50%; 402 | left: 50%; 403 | opacity: 0; 404 | transform: translate(-50%, -20%); 405 | display: flex; 406 | align-items: center; 407 | justify-content: center; 408 | font-size: 15px; 409 | padding: 8px 14px; 410 | border-radius: 4px; 411 | position: absolute; 412 | background: var(--notify-bg); 413 | color: var(--white); 414 | z-index: 99; 415 | backdrop-filter: blur(10px); 416 | transition: all 0.3s ease-in-out; 417 | &.show { 418 | top: 50%; 419 | opacity: 1; 420 | transform: translate(-50%, -50%); 421 | } 422 | `; 423 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ES2022", 4 | "target": "es2017", 5 | "outDir": "dist", 6 | "jsx": "react", 7 | "noUnusedLocals": false, 8 | "noUnusedParameters": false, 9 | "experimentalDecorators": true, 10 | "removeComments": true, 11 | "noImplicitAny": false, 12 | "moduleResolution": "node", 13 | "typeRoots": ["./node_modules/@types", "./node_modules/@figma"] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const InlineChunkHtmlPlugin = require('react-dev-utils/InlineChunkHtmlPlugin') 2 | const HtmlWebpackPlugin = require('html-webpack-plugin') 3 | const path = require('path') 4 | const webpack = require('webpack') 5 | 6 | module.exports = (env, argv) => ({ 7 | mode: argv.mode === 'production' ? 'production' : 'development', 8 | 9 | // This is necessary because Figma's 'eval' works differently than normal eval 10 | devtool: argv.mode === 'production' ? false : 'inline-source-map', 11 | 12 | entry: { 13 | ui: './src/ui.tsx', // The entry point for your UI code 14 | code: './src/code.ts', // The entry point for your plugin code 15 | }, 16 | 17 | module: { 18 | rules: [ 19 | // Converts TypeScript code to JavaScript 20 | { 21 | test: /\.tsx?$/, 22 | use: 'ts-loader', 23 | exclude: /node_modules/ 24 | }, 25 | // Enables including CSS by doing "import './file.css'" in your TypeScript code 26 | { 27 | test: /\.css$/, 28 | use: ["style-loader", "css-loader"], 29 | }, 30 | // Allows you to use "<%= require('./file.svg') %>" in your HTML code to get a data URI 31 | // { test: /\.(png|jpg|gif|webp|svg|zip)$/, loader: [{ loader: 'url-loader' }] } 32 | { 33 | test: /\.svg/, 34 | type: 'asset/inline' 35 | }, 36 | ] 37 | }, 38 | 39 | // Webpack tries these extensions for you if you omit the extension like "import './file'" 40 | resolve: { 41 | extensions: ['.tsx', '.ts', '.jsx', '.js'], 42 | fallback: { 43 | path: require.resolve("path-browserify"), 44 | url: require.resolve("url/"), 45 | os: require.resolve("os-browserify/browser"), 46 | fs: false, 47 | timers: require.resolve('timers-browserify'), 48 | "string_decoder": false, 49 | "stream": false 50 | } 51 | }, 52 | 53 | output: { 54 | filename: '[name].js', 55 | path: path.resolve(__dirname, 'dist'), // Compile into a folder called "dist" 56 | }, 57 | performance: { 58 | hints: false, 59 | maxEntrypointSize: 512000, 60 | maxAssetSize: 512000 61 | }, 62 | 63 | // Tells Webpack to generate "ui.html" and to inline "ui.ts" into it 64 | plugins: [ 65 | new webpack.DefinePlugin({ 66 | 'global': {} // Fix missing symbol error when running in developer VM 67 | }), 68 | new HtmlWebpackPlugin({ 69 | inject: "body", 70 | template: './src/ui.html', 71 | filename: 'ui.html', 72 | inlineSource: '.(js)$', 73 | chunks: ['ui'], 74 | }), 75 | new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/ui/]), 76 | ], 77 | }) --------------------------------------------------------------------------------