├── .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 | 
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 = `
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 |
--------------------------------------------------------------------------------