├── .gitignore ├── README.md ├── assets ├── demo.gif └── icon.png ├── editor ├── package.json ├── public │ └── index.html ├── src │ ├── components │ │ └── editor.tsx │ ├── index.css │ ├── index.tsx │ ├── react-app-env.d.ts │ └── setupTests.js └── tsconfig.json ├── package.json ├── types.ts ├── widget ├── manifest.json ├── package.json ├── scripts │ ├── build.js │ └── start.js ├── src │ ├── icons.ts │ ├── utils.ts │ └── widget.tsx └── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # testing 9 | coverage 10 | 11 | # production 12 | build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FigJam Live Code Block Widget 2 | 3 | Turn FigJam into a collaborative JavaScript canvas 4 | 5 | ![demo](assets/demo.gif) 6 | 7 | Could be used for: 8 | 9 | - Exploring APIs 10 | - Teaching 11 | - Debugging 12 | - Pair programming 13 | - Code review 14 | - Technical interviews 15 | - ??? 16 | 17 | Found a creative use for this widget? Tell me about it on Twitter ([@colebemis](https://twitter.com/colebemis)) 18 | 19 | ## Installation 20 | 21 | https://www.figma.com/community/widget/1034005547769330556 22 | 23 | ## Global variables 24 | 25 | Every live code block has access to the following variables: 26 | 27 | | Name | Type | Description | 28 | | ---------------------- | ---------- | --------------------------------------------------------------------------------------------------- | 29 | | `fetch()` | `function` | [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/fetch) | 30 | | `fetchJson()` | `function` | A convenient wrapper around `fetch()` specifically for fetching JSON data | 31 | | `Math` | `object` | [MDN docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math) | 32 | | `Array` | `object` | [MDN docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array) | 33 | | `Object` | `object` | [MDN docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object) | 34 | 35 | ## Local development 36 | 37 | 1. Clone the repository 38 | 39 | ```shell 40 | git clone https://github.com/colebemis/figjam-javascript-repl.git 41 | cd figjam-javascript-repl 42 | ``` 43 | 44 | 1. Install the dependencies 45 | 46 | ```shell 47 | yarn 48 | ``` 49 | 50 | 1. Run local development scripts 51 | 52 | ```shell 53 | yarn start 54 | ``` 55 | 56 | 1. Open the [Figma desktop app](https://www.figma.com/downloads/) 57 | 58 | 1. Inside a FigJam file, go to `Menu > Widgets > Development > Import widget from manifest...` 59 | 60 | 1. Select `/path/to/figjam-javascript-repl/manifest.json` 61 | 62 | 1. Add the widget to the canvas by selecting `Menu > Widgets > Developement > JavaScript REPL` or search for `JavaScript REPL` in the quick actions bar (`⌘ /`) 63 | 64 | ## Prior art 65 | 66 | - [natto.dev](https://natto.dev/) by [@\_paulshen](https://twitter.com/_paulshen) 67 | -------------------------------------------------------------------------------- /assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colebemis/figjam-live-code-block/350c1508abe1a8466f63a334555fb643962dd17d/assets/demo.gif -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colebemis/figjam-live-code-block/350c1508abe1a8466f63a334555fb643962dd17d/assets/icon.png -------------------------------------------------------------------------------- /editor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "editor", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "BROWSER=none react-scripts start", 7 | "build": "react-scripts build", 8 | "test": "react-scripts test", 9 | "eject": "react-scripts eject" 10 | }, 11 | "dependencies": { 12 | "@monaco-editor/react": "^4.3.1", 13 | "@testing-library/jest-dom": "^5.11.4", 14 | "@testing-library/react": "^11.1.0", 15 | "@testing-library/user-event": "^12.1.10", 16 | "@types/debounce": "^1.2.1", 17 | "@types/jest": "^27.0.2", 18 | "@types/node": "^16.11.4", 19 | "@types/react": "^17.0.31", 20 | "@types/react-dom": "^17.0.10", 21 | "debounce": "^1.2.1", 22 | "map-obj": "^5.0.0", 23 | "monaco-editor": "^0.29.1", 24 | "react": "^17.0.2", 25 | "react-dom": "^17.0.2", 26 | "react-scripts": "4.0.3", 27 | "typescript": "^4.4.4", 28 | "web-vitals": "^1.0.1" 29 | }, 30 | "eslintConfig": { 31 | "extends": [ 32 | "react-app", 33 | "react-app/jest" 34 | ] 35 | }, 36 | "browserslist": { 37 | "production": [ 38 | ">0.2%", 39 | "not dead", 40 | "not op_mini all" 41 | ], 42 | "development": [ 43 | "last 1 chrome version", 44 | "last 1 firefox version", 45 | "last 1 safari version" 46 | ] 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /editor/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | React App 7 | 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /editor/src/components/editor.tsx: -------------------------------------------------------------------------------- 1 | import MonacoEditor, { useMonaco } from "@monaco-editor/react"; 2 | import React from "react"; 3 | import { EditorMessage, WidgetMessage } from "../../../types"; 4 | import mapObj from "map-obj"; 5 | import debounce from "debounce"; 6 | 7 | const debouncedPostMessage = debounce(postMessage, 1000); 8 | 9 | export function Editor() { 10 | const [code, setCode] = React.useState(""); 11 | const [inputs, setInputs] = React.useState>({}); 12 | 13 | window.onmessage = async ( 14 | event: MessageEvent<{ pluginMessage?: EditorMessage }> 15 | ) => { 16 | const message = event.data.pluginMessage; 17 | 18 | if (!message) return; 19 | 20 | switch (message.type) { 21 | case "initialize": 22 | setCode(message.code); 23 | setInputs(parseInputValues(message.inputs)); 24 | break; 25 | 26 | case "evaluate": 27 | try { 28 | const { code, inputs } = message; 29 | 30 | const scope = { 31 | fetch, 32 | fetchJson, 33 | ...parseInputValues(inputs), 34 | }; 35 | 36 | // eslint-disable-next-line no-new-func 37 | const fn = new Function(...Object.keys(scope), `return ${code}`); 38 | const value = await fn(...Object.values(scope)); 39 | 40 | postMessage({ 41 | type: "codeEvaluated", 42 | value: valueToString(value), 43 | valueType: typeof value, 44 | error: "", 45 | }); 46 | } catch (error) { 47 | const errorMessage = 48 | error instanceof Error ? error.message : String(error); 49 | 50 | postMessage({ 51 | type: "codeEvaluated", 52 | value: null, 53 | valueType: null, 54 | error: errorMessage, 55 | }); 56 | } 57 | break; 58 | } 59 | }; 60 | 61 | const monaco = useMonaco(); 62 | 63 | // Add IntelliSense support for input variables 64 | React.useEffect(() => { 65 | const inputsLib = Object.entries(inputs) 66 | .map(([name, value]) => `declare const ${name}: ${valueToType(value)}`) 67 | .join("\n"); 68 | 69 | monaco?.languages.typescript.javascriptDefaults.addExtraLib( 70 | inputsLib, 71 | "inputs" 72 | ); 73 | }, [monaco, inputs]); 74 | 75 | return ( 76 | { 80 | const code = value || ""; 81 | setCode(code); 82 | debouncedPostMessage({ type: "codeChanged", code }); 83 | }} 84 | height="100vh" 85 | options={{ 86 | minimap: { enabled: false }, 87 | }} 88 | /> 89 | ); 90 | } 91 | 92 | function postMessage(message: WidgetMessage) { 93 | // eslint-disable-next-line no-restricted-globals 94 | parent.postMessage({ pluginMessage: message, pluginId: "*" }, "*"); 95 | } 96 | 97 | /** A convenient wrapper around `fetch` just for JSON */ 98 | async function fetchJson(input: RequestInfo, init?: RequestInit) { 99 | const response = await fetch(input, init); 100 | return response.json(); 101 | } 102 | 103 | function valueToString(value: any) { 104 | switch (typeof value) { 105 | case "function": 106 | case "undefined": 107 | return String(value); 108 | 109 | default: 110 | return JSON.stringify(value, null, 2); 111 | } 112 | } 113 | 114 | function parseInputValues(inputs: Record): Record { 115 | return mapObj(inputs, (key, value) => { 116 | // eslint-disable-next-line no-new-func 117 | const parsedValue = new Function(`return ${value}`)(); 118 | return [key, parsedValue]; 119 | }); 120 | } 121 | 122 | function valueToType(value: any): string { 123 | if (value === null) { 124 | return "null"; 125 | } 126 | 127 | if (Array.isArray(value)) { 128 | if (value.length === 0) return "Array"; 129 | 130 | return `Array<${valueToType(value[0])}>`; 131 | } 132 | 133 | const valueType = typeof value; 134 | 135 | switch (valueType) { 136 | case "object": 137 | const entries: string[] = Object.entries(value).map( 138 | ([key, value]) => `${key}: ${valueToType(value)}` 139 | ); 140 | 141 | return `{ ${entries.join(";")} }`; 142 | 143 | case "function": 144 | // TODO: get parameter names 145 | return "Function"; 146 | 147 | default: 148 | return valueType; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /editor/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | -------------------------------------------------------------------------------- /editor/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { Editor } from "./components/editor"; 4 | import "./index.css"; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById("root") 11 | ); 12 | -------------------------------------------------------------------------------- /editor/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /editor/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /editor/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "figjam-javascript-repl", 3 | "private": true, 4 | "workspaces": [ 5 | "widget", 6 | "editor" 7 | ], 8 | "scripts": { 9 | "start": "concurrently -n widget,editor \"yarn workspace widget start\" \"yarn workspace editor start\"" 10 | }, 11 | "dependencies": { 12 | "concurrently": "^6.3.0" 13 | } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /types.ts: -------------------------------------------------------------------------------- 1 | export type ValueType = 2 | | "string" 3 | | "number" 4 | | "bigint" 5 | | "boolean" 6 | | "symbol" 7 | | "undefined" 8 | | "object" 9 | | "function"; 10 | 11 | // Messages that the editor can receive 12 | export type EditorMessage = 13 | | { type: "initialize"; code: string; inputs: Record } 14 | | { type: "evaluate"; code: string; inputs: Record }; 15 | 16 | // Messages that the widget can recieve 17 | export type WidgetMessage = 18 | | { type: "codeChanged"; code: string } 19 | | { type: "codeEvaluated"; value: string; valueType: ValueType; error: "" } 20 | | { type: "codeEvaluated"; value: null; valueType: null; error: string }; 21 | -------------------------------------------------------------------------------- /widget/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Live Code Block", 3 | "id": "1034005547769330556", 4 | "api": "1.0.0", 5 | "widgetApi": "1.0.0", 6 | "editorType": ["figjam"], 7 | "permissions": [], 8 | "containsWidget": true, 9 | "main": "build/widget.js" 10 | } 11 | -------------------------------------------------------------------------------- /widget/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "widget", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node scripts/start.js", 7 | "build": "node scripts/build.js", 8 | "test": "tsc --noEmit" 9 | }, 10 | "dependencies": { 11 | "tailwindcss": "^2.2.17", 12 | "@figma/plugin-typings": "^1.37.0", 13 | "@figma/widget-typings": "^1.0.1", 14 | "@types/node": "^16.11.4", 15 | "@types/tailwindcss": "^2.2.1", 16 | "esbuild": "^0.13.8", 17 | "typescript": "^4.4.4" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /widget/scripts/build.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const esbuild = require("esbuild"); 3 | 4 | esbuild 5 | .build({ 6 | entryPoints: [path.resolve(__dirname, "../src/widget.tsx")], 7 | outfile: "build/widget.js", 8 | bundle: true, 9 | minify: true, 10 | define: { 11 | "process.env.NODE_ENV": '"production"', 12 | }, 13 | }) 14 | .catch(() => process.exit(1)); 15 | -------------------------------------------------------------------------------- /widget/scripts/start.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const esbuild = require("esbuild"); 3 | 4 | esbuild 5 | .build({ 6 | entryPoints: [path.resolve(__dirname, "../src/widget.tsx")], 7 | outfile: "build/widget.js", 8 | bundle: true, 9 | watch: true, 10 | define: { 11 | "process.env.NODE_ENV": '"development"', 12 | }, 13 | }) 14 | .catch(() => process.exit(1)); 15 | -------------------------------------------------------------------------------- /widget/src/icons.ts: -------------------------------------------------------------------------------- 1 | export const playIcon = ``; 2 | 3 | export const plusIcon = ``; 4 | 5 | export const codeIcon = ` 6 | 7 | 8 | `; 9 | -------------------------------------------------------------------------------- /widget/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { ValueType, EditorMessage, WidgetMessage } from "../../types"; 2 | 3 | export function postMessage(message: EditorMessage) { 4 | figma.ui.postMessage(message); 5 | } 6 | 7 | export function getEditorUI() { 8 | const editorUrl = 9 | process.env.NODE_ENV === "production" 10 | ? "https://figjam-live-code-block.vercel.app/" 11 | : "http://localhost:3000"; 12 | 13 | return ``; 14 | } 15 | 16 | export function getInputs(widgetId: string) { 17 | const inputs: Record = {}; 18 | 19 | // Search all nodes in the document 20 | for (const node of figma.currentPage.children) { 21 | // Ignore nodes that aren't connectors 22 | if (node.type !== "CONNECTOR") continue; 23 | 24 | // Ignore connectors that don't end at a node 25 | if (!("endpointNodeId" in node.connectorEnd)) continue; 26 | 27 | // Ignore connectors that don't end at the current widget 28 | if (node.connectorEnd.endpointNodeId !== widgetId) continue; 29 | 30 | // Ignore connectors that don't start at a node 31 | if (!("endpointNodeId" in node.connectorStart)) continue; 32 | 33 | const startNode = figma.getNodeById(node.connectorStart.endpointNodeId); 34 | 35 | // Ignore connectors that don't start at a widget 36 | if (startNode?.type !== "WIDGET") continue; 37 | 38 | // Ignore connectors that don't start at a widget with a value 39 | if (typeof startNode.widgetSyncedState.value === "undefined") continue; 40 | 41 | const variableName = node.text.characters; 42 | 43 | // Don't store variables without a name 44 | if (!variableName) continue; 45 | 46 | // Get widget value 47 | const widgetState = startNode.widgetSyncedState; 48 | const value = widgetState.error ? undefined : widgetState.value; 49 | 50 | inputs[variableName] = value; 51 | } 52 | 53 | return inputs; 54 | } 55 | 56 | export async function connectNodes( 57 | startNode: BaseNode, 58 | endNode: BaseNode, 59 | connectorText?: string 60 | ) { 61 | const connector = figma.createConnector(); 62 | 63 | connector.connectorStart = { 64 | endpointNodeId: startNode.id, 65 | magnet: "AUTO", 66 | }; 67 | 68 | connector.connectorEnd = { 69 | endpointNodeId: endNode.id, 70 | magnet: "AUTO", 71 | }; 72 | 73 | if (connectorText) { 74 | // Font needs to be loaded before changing the text characters 75 | // Reference: https://www.figma.com/plugin-docs/api/properties/TextNode-characters/ 76 | await figma.loadFontAsync({ family: "Inter", style: "Medium" }); 77 | connector.text.characters = connectorText; 78 | } 79 | } 80 | 81 | export function transferConnectors(from: BaseNode, to: BaseNode) { 82 | for (const node of figma.currentPage.children) { 83 | // Ignore nodes that aren't connectors 84 | if (node.type !== "CONNECTOR") continue; 85 | 86 | // Tranfer connectors that start at `from` node 87 | if ( 88 | "endpointNodeId" in node.connectorStart && 89 | node.connectorStart.endpointNodeId === from.id 90 | ) { 91 | node.connectorStart = { 92 | ...node.connectorStart, 93 | endpointNodeId: to.id, 94 | }; 95 | } 96 | 97 | // Tranfer connectors that end at `from` node 98 | if ( 99 | "endpointNodeId" in node.connectorEnd && 100 | node.connectorEnd.endpointNodeId === from.id 101 | ) { 102 | node.connectorEnd = { 103 | ...node.connectorEnd, 104 | endpointNodeId: to.id, 105 | }; 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /widget/src/widget.tsx: -------------------------------------------------------------------------------- 1 | import colors from "tailwindcss/colors"; 2 | import { ValueType, WidgetMessage } from "../../types"; 3 | import { codeIcon, playIcon, plusIcon } from "./icons"; 4 | import { 5 | connectNodes, 6 | getEditorUI, 7 | getInputs, 8 | postMessage, 9 | transferConnectors, 10 | } from "./utils"; 11 | const { widget } = figma; 12 | const { 13 | AutoLayout, 14 | Frame, 15 | Text, 16 | useSyncedState, 17 | usePropertyMenu, 18 | useEffect, 19 | useWidgetId, 20 | waitForTask, 21 | } = widget; 22 | 23 | const initialState = { 24 | code: "1 + 1", 25 | value: "2", 26 | valueType: "number", 27 | error: "", 28 | isExpanded: false, 29 | } as const; 30 | 31 | function Widget() { 32 | const widgetId = useWidgetId(); 33 | 34 | // Initialize state 35 | const [code, setCode] = useSyncedState("code", initialState.code); 36 | const [value, setValue] = useSyncedState("value", initialState.value); 37 | const [valueType, setValueType] = useSyncedState( 38 | "valueType", 39 | initialState.valueType 40 | ); 41 | const [error, setError] = useSyncedState("error", initialState.error); 42 | const [isExpanded, setIsExpanded] = useSyncedState( 43 | "isExpanded", 44 | initialState.isExpanded 45 | ); 46 | 47 | // The `editor` UI (src/editor.html) must be running when the `run` function 48 | // is called because we evaluate code in the UI environment. 49 | // This enables us to evaluate code with network requests. 50 | // Reference: https://www.figma.com/widget-docs/making-network-requests/ 51 | function run(code: string) { 52 | return new Promise(resolve => { 53 | const inputs = getInputs(widgetId); 54 | 55 | // Send code to the UI to evaluate 56 | postMessage({ type: "evaluate", code, inputs }); 57 | 58 | // Wait for the UI to send back evaluated code 59 | figma.ui.on("message", handleMessage); 60 | 61 | function handleMessage(message: WidgetMessage) { 62 | if (message.type === "codeEvaluated") { 63 | const { value, valueType, error } = message; 64 | 65 | // Update state 66 | setError(error); 67 | if (value) setValue(value); 68 | if (valueType) setValueType(valueType); 69 | 70 | // Clean up 71 | figma.ui.off("message", handleMessage); 72 | 73 | resolve(null); 74 | } 75 | } 76 | }); 77 | } 78 | 79 | async function add(widgetId: string) { 80 | const widgetNode = figma.getNodeById(widgetId) as WidgetNode; 81 | 82 | // Clone the current widget 83 | const clonedWidgetNode = widgetNode.clone(); 84 | 85 | // Move the current widget to the right of the clone 86 | widgetNode.x += clonedWidgetNode.width + 160; 87 | 88 | // Transfer connectors to clone 89 | transferConnectors(widgetNode, clonedWidgetNode); 90 | 91 | // Add connector between clone and the current widget 92 | await connectNodes(clonedWidgetNode, widgetNode, "value"); 93 | 94 | // Change code of current widget 95 | setCode("value"); 96 | } 97 | 98 | usePropertyMenu( 99 | [ 100 | { 101 | tooltip: "Run", 102 | propertyName: "run", 103 | itemType: "action", 104 | icon: playIcon, 105 | }, 106 | { 107 | tooltip: "Edit", 108 | propertyName: "edit", 109 | itemType: "action", 110 | icon: codeIcon, 111 | }, 112 | { 113 | tooltip: "Add", 114 | propertyName: "add", 115 | itemType: "action", 116 | icon: plusIcon, 117 | }, 118 | ], 119 | ({ propertyName }) => { 120 | switch (propertyName) { 121 | case "edit": 122 | figma.showUI(getEditorUI(), { width: 500, height: 300 }); 123 | const inputs = getInputs(widgetId); 124 | postMessage({ type: "initialize", code, inputs }); 125 | 126 | // Keep UI open 127 | return new Promise(() => {}); 128 | 129 | case "run": 130 | figma.showUI(getEditorUI(), { visible: false }); 131 | waitForTask(run(code)); 132 | return; 133 | 134 | case "add": 135 | waitForTask(add(widgetId)); 136 | return; 137 | } 138 | } 139 | ); 140 | 141 | useEffect(() => { 142 | figma.ui.onmessage = (message: WidgetMessage) => { 143 | switch (message.type) { 144 | case "codeChanged": 145 | const { code } = message; 146 | setCode(code); 147 | 148 | run(code); 149 | break; 150 | } 151 | }; 152 | }); 153 | 154 | return ( 155 | 180 | {/* HACK: Set min-width of widget to 400 */} 181 | 182 | {/* 191 | 196 | name 197 | 198 | */} 199 | 207 | {code.split("\n").map((line, index) => { 208 | return line ? ( 209 | 214 | {line} 215 | 216 | ) : null; 217 | })} 218 | 219 | 220 | 228 | {error ? ( 229 | 230 | {error} 231 | 232 | ) : ( 233 | 240 | {value.split("\n").length > 10 ? ( 241 | setIsExpanded(!isExpanded)} 247 | > 248 | {isExpanded ? "Show less" : "Show more"} 249 | 250 | ) : null} 251 | 258 | {value 259 | .split("\n") 260 | .filter((_, index) => isExpanded || index < 10) 261 | .map((line, index) => { 262 | return line ? ( 263 | 268 | {line} 269 | 270 | ) : null; 271 | })} 272 | {!isExpanded && value.split("\n").length > 10 ? ( 273 | setIsExpanded(true)} 277 | > 278 | ... 279 | 280 | ) : null} 281 | 282 | 283 | )} 284 | 289 | {error ? "error" : valueType} 290 | 291 | 292 | 293 | ); 294 | } 295 | 296 | widget.register(Widget); 297 | -------------------------------------------------------------------------------- /widget/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "jsxFactory": "figma.widget.h", 5 | "target": "es6", 6 | "lib": ["es2017"], 7 | "types": ["node", "@figma/plugin-typings", "@figma/widget-typings"], 8 | "outDir": "build", 9 | "moduleResolution": "node", 10 | "esModuleInterop": true, 11 | "strict": true 12 | } 13 | } 14 | --------------------------------------------------------------------------------