Skip to content

Commit d667f8b

Browse files
committed
various fixes
1 parent 00f1ae7 commit d667f8b

File tree

15 files changed

+140
-250
lines changed

15 files changed

+140
-250
lines changed

.vscode/tasks.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"type": "shell",
99
"command": "yarn dev",
1010
"options": { "cwd": "${workspaceFolder}/packages/plugin-app" },
11+
"problemMatcher": [],
1112
"presentation": {
1213
"focus": false,
1314
"panel": "shared",

package.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@
1818
"typescript": "^5.2.2"
1919
},
2020
"resolutions": {
21-
"has-symbols": "patch:[email protected]#./.patches/has-symbols.patch",
22-
"@types/react": "^17.0.1",
23-
"@types/react-dom": "^17.0.0"
21+
"has-symbols": "patch:[email protected]#./.patches/has-symbols.patch"
2422
}
2523
}

packages/plugin-app/src/components/Editor.tsx

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import React, { useMemo } from 'react'
44
import * as shiki from 'shiki'
55
import wasm from 'shiki/dist/onigasm.wasm?url'
66

7+
import { readTextFromClipboard, writeTextToClipboard } from '../utils/clipboard'
8+
79
shiki.setOnigasmWASM(wasm)
810
shiki.setCDN('./assets/shiki/')
911

@@ -16,7 +18,8 @@ export const Editor: FC<{
1618
fontSize: number
1719
lineHeight: number
1820
fontFamily: string
19-
}> = ({ code, setCode, shikiTokens, themeData, includeLineNumbers, fontSize, lineHeight, fontFamily }) => {
21+
env: 'Figma' | 'Browser'
22+
}> = ({ code, setCode, shikiTokens, themeData, includeLineNumbers, fontSize, lineHeight, fontFamily, env }) => {
2023
const highlightedText = useMemo(() => shiki.renderToHtml(shikiTokens, { ...themeData }), [shikiTokens, themeData])
2124
const lineCount = useMemo(() => code.split('\n').length, [code])
2225
// 20 for bottom padding
@@ -38,10 +41,32 @@ export const Editor: FC<{
3841
)}
3942
<div className="relative w-max">
4043
<textarea
41-
ref={textareaRef}
44+
ref={textareaRef}
4245
style={{ caretColor: themeData.fg, height }}
4346
value={code}
4447
className="absolute w-full h-full text-transparent bg-transparent outline-none resize-none"
48+
onKeyDown={(e) => {
49+
if (env === 'Browser') return
50+
51+
// NOTE it seems common keyboard shortcuts are not working in Figma, so we have to implement them ourselves
52+
if (e.metaKey && e.code === 'KeyA') {
53+
e.preventDefault()
54+
e.stopPropagation()
55+
textareaRef.current?.select()
56+
} else if (e.metaKey && e.code === 'KeyV') {
57+
e.preventDefault()
58+
e.stopPropagation()
59+
readTextFromClipboard().then((text) => setCode(text))
60+
} else if (e.metaKey && e.code === 'KeyC') {
61+
e.preventDefault()
62+
e.stopPropagation()
63+
const selectedText = e.currentTarget.value.substring(
64+
e.currentTarget.selectionStart,
65+
e.currentTarget.selectionEnd,
66+
)
67+
writeTextToClipboard(selectedText)
68+
}
69+
}}
4570
onChange={(e) => setCode(e.target.value)}
4671
autoCorrect="off"
4772
spellCheck="false"

packages/plugin-app/src/components/Main.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ const Main: React.FC = () => {
7676
availableFigmaFonts.resolve(event.data.pluginMessage.monoFontFamilies)
7777
}
7878
}
79+
80+
parent.postMessage({ pluginMessage: { type: 'INIT' } }, '*')
7981
}, [setCode, setFontSize, availableFigmaFonts])
8082

8183
const execRun = async () => {
@@ -96,7 +98,8 @@ const Main: React.FC = () => {
9698

9799
const runPrettier = async () => {
98100
const { formatText } = await import('../utils/prettier')
99-
setCode(formatText(code, language))
101+
const formattedCode = await formatText(code, language)
102+
setCode(formattedCode)
100103
}
101104

102105
if (fontRes.isLoading || isHighlighterLoading) {
@@ -126,6 +129,7 @@ const Main: React.FC = () => {
126129
code: code,
127130
setCode: setCode,
128131
lineHeight: fontSize * 1.5,
132+
env,
129133
}}
130134
/>
131135
<Sidebar

packages/plugin-app/src/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import './index.css'
22
import 'react-figma-plugin-ds/figma-plugin-ds.css'
33

44
import React from 'react'
5-
import ReactDOM from 'react-dom'
5+
import { createRoot } from 'react-dom/client'
66

77
import Main from './components/Main'
88

9-
ReactDOM.render(<Main />, document.getElementById('root'))
9+
createRoot(document.getElementById('root')!).render(<Main />)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Based on https://forum.figma.com/t/write-to-clipboard-from-custom-plugin/11860/15
2+
3+
export const writeTextToClipboard = (str: string) => {
4+
const prevActive = document.activeElement as HTMLElement
5+
const textArea = document.createElement('textarea')
6+
7+
textArea.value = str
8+
9+
textArea.style.position = 'fixed'
10+
textArea.style.left = '-999999px'
11+
textArea.style.top = '-999999px'
12+
13+
document.body.appendChild(textArea)
14+
15+
textArea.focus()
16+
textArea.select()
17+
18+
return new Promise((res, rej) => {
19+
document.execCommand('copy') ? res(void 0) : rej()
20+
textArea.remove()
21+
22+
prevActive?.focus()
23+
})
24+
}
25+
26+
export const readTextFromClipboard = () => {
27+
const textArea = document.createElement('textarea')
28+
29+
textArea.style.position = 'fixed'
30+
textArea.style.left = '-999999px'
31+
textArea.style.top = '-999999px'
32+
33+
document.body.appendChild(textArea)
34+
35+
textArea.focus()
36+
textArea.select()
37+
38+
return new Promise<string>((res, rej) => {
39+
document.execCommand('paste') ? res(textArea.value) : rej()
40+
textArea.remove()
41+
})
42+
}

packages/plugin-app/src/utils/hooks.ts

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import type { DependencyList } from 'react'
2+
import React from 'react'
23
import { useEffect, useMemo, useState } from 'react'
34

45
export const useAsyncMemo = <T>(fn: () => Promise<T>, deps: DependencyList): T | undefined => {
56
const [val, setVal] = useState<T | undefined>(undefined)
67

78
useEffect(() => {
89
fn().then((_) => setVal(_))
9-
// eslint-disable-next-line react-hooks/exhaustive-deps
10+
// eslint-disable-next-line react-hooks/exhaustive-deps
1011
}, deps)
1112

1213
return val
@@ -27,24 +28,27 @@ export const usePersistedState = <T>({
2728
}, [storageKey])
2829
const [val, setVal] = useState<T>(persistedVal ?? initialValue)
2930

30-
let timeout: number | undefined = undefined
31-
const updateValue = (_: T) => {
32-
setVal(_)
33-
clearTimeout(timeout)
34-
timeout = setTimeout(() => {
35-
const jsonVal = JSON.stringify(_)
36-
localStorage.setItem(storageKey, jsonVal)
37-
}, storageDebounceMs)
38-
}
31+
const timeoutRef = React.useRef<number | undefined>(undefined)
32+
const updateValue = React.useCallback(
33+
(_: T) => {
34+
setVal(_)
35+
clearTimeout(timeoutRef.current)
36+
timeoutRef.current = setTimeout(() => {
37+
const jsonVal = JSON.stringify(_)
38+
localStorage.setItem(storageKey, jsonVal)
39+
}, storageDebounceMs)
40+
},
41+
[storageDebounceMs, storageKey],
42+
)
3943

4044
useEffect(() => {
4145
const jsonVal = localStorage.getItem(storageKey)
4246
if (jsonVal) {
4347
setVal(JSON.parse(jsonVal))
4448
}
4549

46-
return () => clearTimeout(timeout)
47-
}, [initialValue, storageKey, storageDebounceMs, timeout])
50+
return () => clearTimeout(timeoutRef.current)
51+
}, [initialValue, storageKey, storageDebounceMs])
4852

4953
return [val, updateValue]
5054
}

packages/plugin-app/src/utils/prettier.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import type { BuiltInParserName } from 'prettier'
2-
import babel from 'prettier/parser-babel'
2+
import babel from 'prettier/plugins/babel'
3+
import estree from 'prettier/plugins/estree'
34
import prettier from 'prettier/standalone'
45
import type { Lang } from 'shiki'
56

6-
export const formatText = (text: string, language: Lang): string => {
7+
export const formatText = (text: string, language: Lang): Promise<string> => {
78
return prettier.format(text, {
89
// TODO needs to be more selective whether prettier supports a given language
910
parser: getParser(language),
10-
plugins: [babel],
11+
plugins: [estree, babel],
1112
singleQuote: true,
1213
})
1314
}

packages/plugin-shared/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
"shiki": "^0.9.3"
99
},
1010
"devDependencies": {
11-
"@figma/plugin-typings": "^1.76.0",
12-
"@types/color": "^3.0.4"
11+
"@figma/plugin-typings": "^1.76.0"
1312
}
1413
}

packages/plugin-shared/src/event-messages.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ export function isRunMessage(message: any): message is RunMessage {
66
return message?.type === 'RUN'
77
}
88

9+
export function isInitMessage(message: any): message is RunMessage {
10+
return message?.type === 'INIT'
11+
}
12+
913
export type SelectionChangeMessage = {
1014
type: 'SELECTION_CHANGE'
1115
selection?: {

packages/plugin/build.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,12 @@
44
const { build, cliopts } = require('estrella')
55
const path = require('path')
66
const fs = require('fs')
7-
const fetch = require('node-fetch').default
87

98
const isDev = cliopts.watch
109
const buildDirSuffix = isDev ? 'dev' : 'prod'
1110
const buildDir = `build-${buildDirSuffix}`
1211

13-
const appUrl = isDev ? '"http://localhost:3000"' : '"https://figma-code.vercel.app"'
12+
const appUrl = isDev ? '"http://localhost:5173"' : '"https://figma-code.vercel.app"'
1413

1514
build({
1615
entry: 'src/ui/index.tsx',
@@ -46,6 +45,7 @@ const htmlTemplate = (js, css) => `\
4645
<style>
4746
${css}
4847
</style>
48+
<script src="https://cdn.tailwindcss.com"></script>
4949
</head>
5050
<body>
5151
<div id="iframe"></div>
@@ -77,7 +77,8 @@ async function makeHTML() {
7777
return
7878
}
7979

80-
const css = await fetch('https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css').then((_) => _.text())
80+
// const css = await fetch('https://cdn.tailwindcss.com').then((_) => _.text())
81+
const css = ''
8182

8283
const content = htmlTemplate(cache['main'], css)
8384
const filePath = path.join(__dirname, buildDir, 'ui.html')
@@ -94,6 +95,7 @@ async function makeManifest() {
9495
name,
9596
main: 'plugin.js',
9697
ui: 'ui.html',
98+
editorType: ["figma", ...(isDev ? ["dev"] : [])]
9799
}
98100
await fs.promises.mkdir(path.join(__dirname, buildDir), { recursive: true })
99101
await fs.promises.writeFile(path.join(__dirname, buildDir, 'manifest.json'), JSON.stringify(data, null, 2))

packages/plugin/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
"@types/react": "^18.2.21",
77
"@types/react-dom": "^18.2.7",
88
"monaco-editor": "0.25.0",
9-
"node-fetch": "^3.3.2",
109
"react": "^18.2.0",
1110
"react-dom": "^18.2.0"
1211
},

packages/plugin/src/plugin/index.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// NOTE import directly from source to avoid bundling of monaco-editor
22
// TOOD clean this up
33
import type { RunDoneMessage, SelectionChangeMessage } from '@internal/plugin-shared/src/event-messages'
4-
import { isRunMessage } from '@internal/plugin-shared/src/event-messages'
4+
import { isInitMessage, isRunMessage } from '@internal/plugin-shared/src/event-messages'
55
import type { FontStyles } from '@internal/plugin-shared/src/run'
66
import { pick } from '@internal/plugin-shared/src/utils'
77

@@ -45,14 +45,15 @@ async function main() {
4545
.map((_) => _.fontName.family)
4646
.filter((_) => ['mono', 'code'].some((monoish) => _.toLowerCase().includes(monoish))),
4747
)
48-
figma.ui.postMessage({ type: 'AVAILABLE_FONTS', monoFontFamilies })
4948

5049
figma.on('selectionchange', updateSelection)
5150

5251
figma.ui.onmessage = async (msg) => {
5352
console.log({ msg })
5453

55-
if (isRunMessage(msg)) {
54+
if (isInitMessage(msg)) {
55+
figma.ui.postMessage({ type: 'AVAILABLE_FONTS', monoFontFamilies })
56+
} else if (isRunMessage(msg)) {
5657
try {
5758
await run({
5859
...pick(msg, [

packages/plugin/src/ui/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { FC } from 'react'
22
import React, { useEffect, useRef } from 'react'
3-
import ReactDOM from 'react-dom'
3+
import { createRoot } from 'react-dom/client'
44

55
const Iframe: FC = () => {
66
const iframe = useRef<HTMLIFrameElement>(null)
@@ -17,4 +17,4 @@ const Iframe: FC = () => {
1717
return <iframe src={`${process.env.APP_URL}?env=figma`} className="w-full h-full" ref={iframe} />
1818
}
1919

20-
ReactDOM.render(<Iframe />, document.getElementById('iframe'))
20+
createRoot(document.getElementById('iframe')!).render(<Iframe />)

0 commit comments

Comments
 (0)