snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
This commit is contained in:
9
apps/vscode/editor/LICENSE.md
Normal file
9
apps/vscode/editor/LICENSE.md
Normal file
@@ -0,0 +1,9 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 tldraw Inc.
|
||||
|
||||
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:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
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.
|
||||
23
apps/vscode/editor/README.md
Normal file
23
apps/vscode/editor/README.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# @tldraw/vscode-editor
|
||||
|
||||
This folder contains the React app used by the tldraw VS Code extension.
|
||||
|
||||
## Contribution
|
||||
|
||||
Found a bug? Please [submit an issue](https://github.com/tldraw/tldraw/issues/new).
|
||||
|
||||
## License
|
||||
|
||||
This project is provided under the MIT license found [here](https://github.com/tldraw/tldraw/blob/main/apps/vscode/editor/LICENSE.md). The tldraw SDK is provided under the [tldraw license](https://github.com/tldraw/tldraw/blob/main/LICENSE.md).
|
||||
|
||||
## Trademarks
|
||||
|
||||
Copyright (c) 2024-present tldraw Inc. The tldraw name and logo are trademarks of tldraw. Please see our [trademark guidelines](https://github.com/tldraw/tldraw/blob/main/TRADEMARKS.md) for info on acceptable usage.
|
||||
|
||||
## Contact
|
||||
|
||||
Find us on Twitter/X at [@tldraw](https://twitter.com/tldraw).
|
||||
|
||||
## Community
|
||||
|
||||
Have questions, comments or feedback? [Join our discord](https://discord.tldraw.com/?utm_source=github&utm_medium=readme&utm_campaign=sociallink). For the latest news and release notes, visit [tldraw.dev](https://tldraw.dev).
|
||||
57
apps/vscode/editor/package.json
Normal file
57
apps/vscode/editor/package.json
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"name": "@tldraw/vscode-editor",
|
||||
"description": "An an editor for the tldraw vscode extension.",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"author": {
|
||||
"name": "tldraw Inc.",
|
||||
"email": "hello@tldraw.com"
|
||||
},
|
||||
"homepage": "https://tldraw.dev",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/tldraw/tldraw"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/tldraw/tldraw/issues"
|
||||
},
|
||||
"keywords": [
|
||||
"tldraw",
|
||||
"sdk",
|
||||
"drawing",
|
||||
"app",
|
||||
"development",
|
||||
"whiteboard",
|
||||
"canvas",
|
||||
"infinite"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "yarn run -T tsx scripts/build.ts",
|
||||
"dev": "yarn run -T tsx scripts/dev.ts",
|
||||
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist",
|
||||
"lint": "yarn run -T tsx ../../../internal/scripts/lint.ts",
|
||||
"test": "yarn run -T vitest --passWithNoTests",
|
||||
"test-ci": "yarn run -T vitest run --passWithNoTests",
|
||||
"test-coverage": "yarn run -T vitest run --coverage --passWithNoTests"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tldraw/assets": "workspace:*",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/node": "^22.15.31",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"concurrently": "^9.1.2",
|
||||
"create-serve": "1.0.1",
|
||||
"dotenv": "^16.4.7",
|
||||
"esbuild": "^0.28.0",
|
||||
"fs-extra": "^11.3.0",
|
||||
"lazyrepo": "0.0.0-alpha.27",
|
||||
"react": "^19.2.1",
|
||||
"react-dom": "^19.2.1",
|
||||
"tldraw": "workspace:*",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"gitHead": "a7dac0f83ad998e205c2aab58182cb4ba4e099a6"
|
||||
}
|
||||
46
apps/vscode/editor/public/index.css
Normal file
46
apps/vscode/editor/public/index.css
Normal file
@@ -0,0 +1,46 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@500;700&display=swap');
|
||||
|
||||
html,
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
overscroll-behavior: none;
|
||||
touch-action: none;
|
||||
overflow: hidden;
|
||||
min-height: 100vh;
|
||||
/* mobile viewport bug fix */
|
||||
min-height: -webkit-fill-available;
|
||||
}
|
||||
|
||||
#root {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tldraw--editor {
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background-color: var(--tl-color-background);
|
||||
}
|
||||
58
apps/vscode/editor/scripts/build.ts
Normal file
58
apps/vscode/editor/scripts/build.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import esbuild from 'esbuild'
|
||||
import fse, { exists } from 'fs-extra'
|
||||
import { logEnv } from './cli'
|
||||
import { getDirname } from './path'
|
||||
|
||||
const rootDir = getDirname(import.meta.url, '../')
|
||||
const log = logEnv('editor')
|
||||
|
||||
export async function build() {
|
||||
try {
|
||||
const targetFolder = `${rootDir}dist/`
|
||||
if (await exists(targetFolder)) {
|
||||
log({ cmd: 'remove', args: { target: targetFolder } })
|
||||
await fs.promises.rm(targetFolder, { recursive: true })
|
||||
}
|
||||
|
||||
await fs.promises.mkdir(targetFolder)
|
||||
|
||||
const topSource = `${rootDir}public`
|
||||
const files = await fs.promises.readdir(topSource)
|
||||
for (const file of files) {
|
||||
const dest = targetFolder + path.basename(file)
|
||||
const source = path.join(topSource, file)
|
||||
log({ cmd: 'copy', args: { source, dest } })
|
||||
await fse.copy(source, dest)
|
||||
}
|
||||
const entryPoints = [`${rootDir}src/index.tsx`]
|
||||
|
||||
log({ cmd: 'esbuild', args: { entryPoints } })
|
||||
esbuild.buildSync({
|
||||
entryPoints,
|
||||
outfile: `${rootDir}/dist/index.js`,
|
||||
minify: false,
|
||||
bundle: true,
|
||||
target: 'es6',
|
||||
jsxFactory: 'React.createElement',
|
||||
jsxFragment: 'React.Fragment',
|
||||
loader: {
|
||||
'.woff2': 'dataurl',
|
||||
'.woff': 'dataurl',
|
||||
'.svg': 'file',
|
||||
'.png': 'file',
|
||||
'.json': 'file',
|
||||
},
|
||||
define: {
|
||||
'process.env.NODE_ENV': '"production"',
|
||||
},
|
||||
})
|
||||
log({ cmd: 'esbuild:success', args: {} })
|
||||
} catch (error) {
|
||||
log({ cmd: 'esbuild:error', args: { error } })
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
build()
|
||||
58
apps/vscode/editor/scripts/cli.ts
Normal file
58
apps/vscode/editor/scripts/cli.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import path from 'path'
|
||||
|
||||
const displayRelative = (from: string, to: string) => {
|
||||
const outpath = path.relative(from, to)
|
||||
if (!outpath.match(/^\./)) {
|
||||
return `./${outpath}`
|
||||
}
|
||||
return outpath
|
||||
}
|
||||
|
||||
type LogDef =
|
||||
| { cmd: 'remove'; env: string; args: { target: string } }
|
||||
| { cmd: 'copy'; env: string; args: { source: string; dest: string } }
|
||||
| { cmd: 'esbuild'; env: string; args: { entryPoints: string[] } }
|
||||
| { cmd: 'esbuild:success'; env: string; args: any }
|
||||
| { cmd: 'esbuild:error'; env: string; args: { error: string } }
|
||||
| { cmd: 'esbuild:serve'; env: string; args: { host: string; port: number | string } }
|
||||
|
||||
export function log(def: LogDef) {
|
||||
const printStderr = (icon: string, cmd: string, ...args: unknown[]) => {
|
||||
console.error(`${icon} [${def.env ?? 'unknown'}/${cmd}]`, ...args)
|
||||
}
|
||||
|
||||
if (def.cmd === 'remove') {
|
||||
const { target } = def.args
|
||||
printStderr('🗑 ', 'remove', displayRelative(process.cwd(), target))
|
||||
} else if (def.cmd === 'copy') {
|
||||
const { source, dest } = def.args
|
||||
printStderr(
|
||||
'🏠',
|
||||
'copy',
|
||||
`${displayRelative(process.cwd(), source)} -> ${displayRelative(process.cwd(), dest)}`
|
||||
)
|
||||
} else if (def.cmd === 'esbuild') {
|
||||
printStderr(
|
||||
'🤖',
|
||||
'esbuild',
|
||||
`${def.args.entryPoints.map((pathname) => displayRelative(process.cwd(), pathname))}`
|
||||
)
|
||||
} else if (def.cmd === 'esbuild:success') {
|
||||
printStderr('✅', `esbuild`, `build successful (${new Date().toISOString()})`)
|
||||
} else if (def.cmd === 'esbuild:error') {
|
||||
printStderr(`❌`, `esbuild`, `error`)
|
||||
console.error(def.args.error)
|
||||
} else if (def.cmd === 'esbuild:serve') {
|
||||
const { host = 'localhost', port } = def.args
|
||||
printStderr(`🌎`, `esbuild`, `serving <http://${host}:${port}>`)
|
||||
} else {
|
||||
// @ts-ignore
|
||||
printStderr(`❓`, def.cmd, JSON.stringify(def.args))
|
||||
}
|
||||
}
|
||||
|
||||
export function logEnv(env: string) {
|
||||
return (opts: any) => {
|
||||
log({ ...opts, env })
|
||||
}
|
||||
}
|
||||
77
apps/vscode/editor/scripts/dev.ts
Normal file
77
apps/vscode/editor/scripts/dev.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import dotenv from 'dotenv'
|
||||
import esbuild from 'esbuild'
|
||||
import fse, { exists } from 'fs-extra'
|
||||
import { logEnv } from './cli'
|
||||
import { copyEditor } from './helpers'
|
||||
import { getDirname } from './path'
|
||||
|
||||
dotenv.config()
|
||||
const rootDir = getDirname(import.meta.url, '../')
|
||||
const log = logEnv('editor')
|
||||
|
||||
export async function run() {
|
||||
try {
|
||||
const targetFolder = `${rootDir}dist/`
|
||||
if (await exists(targetFolder)) {
|
||||
log({ cmd: 'remove', args: { target: targetFolder } })
|
||||
await fs.promises.rm(targetFolder, { recursive: true })
|
||||
}
|
||||
|
||||
await fs.promises.mkdir(targetFolder)
|
||||
|
||||
const topSource = `${rootDir}public`
|
||||
const files = await fs.promises.readdir(topSource)
|
||||
for (const file of files) {
|
||||
const dest = targetFolder + path.basename(file)
|
||||
const source = path.join(topSource, file)
|
||||
log({ cmd: 'copy', args: { source, dest } })
|
||||
await fse.copy(source, dest)
|
||||
}
|
||||
const entryPoints = [`${rootDir}src/index.tsx`]
|
||||
|
||||
log({ cmd: 'esbuild', args: { entryPoints } })
|
||||
|
||||
const builder = await esbuild.context({
|
||||
entryPoints,
|
||||
outfile: `${rootDir}/dist/index.js`,
|
||||
minify: false,
|
||||
bundle: true,
|
||||
target: 'es6',
|
||||
jsxFactory: 'React.createElement',
|
||||
jsxFragment: 'React.Fragment',
|
||||
loader: {
|
||||
'.woff2': 'dataurl',
|
||||
'.woff': 'dataurl',
|
||||
'.svg': 'file',
|
||||
'.png': 'file',
|
||||
'.json': 'file',
|
||||
},
|
||||
define: {
|
||||
'process.env.NODE_ENV': '"development"',
|
||||
},
|
||||
plugins: [
|
||||
{
|
||||
name: 'log-builds',
|
||||
setup(build) {
|
||||
build.onEnd((result) => {
|
||||
if (result.errors.length) {
|
||||
log({ cmd: 'esbuild:error', args: { err: result.errors } })
|
||||
} else {
|
||||
copyEditor({ log })
|
||||
log({ cmd: 'esbuild:success', args: {} })
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
await builder.watch()
|
||||
} catch (error) {
|
||||
log({ cmd: 'esbuild:error', args: { error } })
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
run()
|
||||
25
apps/vscode/editor/scripts/helpers.ts
Normal file
25
apps/vscode/editor/scripts/helpers.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import fs from 'fs'
|
||||
import { join } from 'path'
|
||||
import fse from 'fs-extra'
|
||||
import { exists, getDirname } from './path'
|
||||
|
||||
const vscodeDir = getDirname(import.meta.url, '../../')
|
||||
|
||||
export async function copyEditor({ log }: { log(opts: any): void }) {
|
||||
const editorRoot = join(vscodeDir, 'editor')
|
||||
const extensionRoot = join(vscodeDir, 'extension')
|
||||
|
||||
const source = join(editorRoot, 'dist')
|
||||
const dest = join(extensionRoot, 'editor')
|
||||
|
||||
log({ cmd: 'copy', args: { source, dest } })
|
||||
await fse.copy(source, dest)
|
||||
}
|
||||
|
||||
export async function removeDistDirectory({ log }: { log(opts: any): void }) {
|
||||
const target = join(vscodeDir, 'extension', 'dist')
|
||||
if (await exists(target)) {
|
||||
log({ cmd: 'remove', args: { target } })
|
||||
await fs.promises.rm(target, { recursive: true })
|
||||
}
|
||||
}
|
||||
16
apps/vscode/editor/scripts/path.ts
Normal file
16
apps/vscode/editor/scripts/path.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
export function getDirname(metaUrl: string, targetPath: string) {
|
||||
const dirname = path.dirname(metaUrl.replace('file://', ''))
|
||||
return path.normalize(path.join(dirname, targetPath))
|
||||
}
|
||||
|
||||
export async function exists(targetFolder: string) {
|
||||
try {
|
||||
await fs.promises.access(targetFolder)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
69
apps/vscode/editor/src/ChangeResponder.tsx
Normal file
69
apps/vscode/editor/src/ChangeResponder.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
debounce,
|
||||
parseAndLoadDocument,
|
||||
serializeTldrawJson,
|
||||
useDefaultHelpers,
|
||||
useEditor,
|
||||
} from 'tldraw'
|
||||
// @ts-ignore
|
||||
import type { VscodeMessage } from '../../messages'
|
||||
import '../public/index.css'
|
||||
import { vscode } from './utils/vscode'
|
||||
|
||||
export function ChangeResponder() {
|
||||
const editor = useEditor()
|
||||
const { addToast, clearToasts, msg } = useDefaultHelpers()
|
||||
|
||||
React.useEffect(() => {
|
||||
// When a message is received from the VS Code extension, handle it
|
||||
function handleMessage({ data: message }: MessageEvent<VscodeMessage>) {
|
||||
switch (message.type) {
|
||||
// case 'vscode:undo': {
|
||||
// editor.undo()
|
||||
// break
|
||||
// }
|
||||
// case 'vscode:redo': {
|
||||
// editor.redo()
|
||||
// break
|
||||
// }
|
||||
case 'vscode:revert': {
|
||||
parseAndLoadDocument(editor, message.data.fileContents, msg, addToast)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('message', handleMessage)
|
||||
|
||||
return () => {
|
||||
clearToasts()
|
||||
window.removeEventListener('message', handleMessage)
|
||||
}
|
||||
}, [editor, msg, addToast, clearToasts])
|
||||
|
||||
React.useEffect(() => {
|
||||
// When the history changes, send the new file contents to VSCode
|
||||
const handleChange = debounce(async () => {
|
||||
vscode.postMessage({
|
||||
type: 'vscode:editor-updated',
|
||||
data: {
|
||||
fileContents: await serializeTldrawJson(editor),
|
||||
},
|
||||
})
|
||||
}, 250)
|
||||
|
||||
vscode.postMessage({
|
||||
type: 'vscode:editor-loaded',
|
||||
})
|
||||
|
||||
const dispose = editor.store.listen(handleChange, { scope: 'document' })
|
||||
|
||||
return () => {
|
||||
handleChange()
|
||||
dispose()
|
||||
}
|
||||
}, [editor])
|
||||
|
||||
return null
|
||||
}
|
||||
44
apps/vscode/editor/src/FileOpen.tsx
Normal file
44
apps/vscode/editor/src/FileOpen.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import React from 'react'
|
||||
import { parseAndLoadDocument, useDefaultHelpers, useEditor } from 'tldraw'
|
||||
import { vscode } from './utils/vscode'
|
||||
|
||||
export function FileOpen({
|
||||
fileContents,
|
||||
forceDarkMode,
|
||||
}: {
|
||||
fileContents: string
|
||||
forceDarkMode: boolean
|
||||
}) {
|
||||
const editor = useEditor()
|
||||
const { msg, addToast, clearToasts } = useDefaultHelpers()
|
||||
const [isFileLoaded, setIsFileLoaded] = React.useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isFileLoaded) return
|
||||
function onV1FileLoad() {
|
||||
vscode.postMessage({
|
||||
type: 'vscode:v1-file-opened',
|
||||
data: {
|
||||
description: msg('vscode.file-open.desc'),
|
||||
backup: msg('vscode.file-open.backup'),
|
||||
backupSaved: msg('vscode.file-open.backup-saved'),
|
||||
backupFailed: msg('vscode.file-open.backup-failed'),
|
||||
dontAskAgain: msg('vscode.file-open.dont-show-again'),
|
||||
open: msg('vscode.file-open.open'),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function loadFile() {
|
||||
await parseAndLoadDocument(editor, fileContents, msg, addToast, onV1FileLoad, forceDarkMode)
|
||||
}
|
||||
|
||||
loadFile()
|
||||
setIsFileLoaded(true)
|
||||
return () => {
|
||||
clearToasts()
|
||||
}
|
||||
}, [fileContents, editor, addToast, msg, clearToasts, forceDarkMode, isFileLoaded])
|
||||
|
||||
return null
|
||||
}
|
||||
17
apps/vscode/editor/src/FullPageMessage.tsx
Normal file
17
apps/vscode/editor/src/FullPageMessage.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
export function FullPageMessage({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'var(--vscode-editor-background)',
|
||||
color: 'var(--vscode-editor-foreground)',
|
||||
position: 'fixed',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
WebkitTransform: 'translate(-50%, -50%)',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
41
apps/vscode/editor/src/Links.tsx
Normal file
41
apps/vscode/editor/src/Links.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { TldrawUiMenuGroup, TldrawUiMenuItem } from 'tldraw'
|
||||
import { openUrl } from './utils/url'
|
||||
|
||||
export function Links() {
|
||||
return (
|
||||
<>
|
||||
<TldrawUiMenuGroup id="links">
|
||||
<TldrawUiMenuItem
|
||||
id="about"
|
||||
label="help-menu.terms"
|
||||
readonlyOk
|
||||
onSelect={() => {
|
||||
openUrl(
|
||||
'https://github.com/tldraw/tldraw/blob/main/apps/dotcom/client/TERMS_OF_SERVICE.md'
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<TldrawUiMenuItem
|
||||
id="about"
|
||||
label="help-menu.privacy"
|
||||
readonlyOk
|
||||
onSelect={() => {
|
||||
openUrl(
|
||||
'https://github.com/tldraw/tldraw/blob/main/apps/dotcom/client/PRIVACY_POLICY.md'
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</TldrawUiMenuGroup>
|
||||
<TldrawUiMenuGroup id="tldraw">
|
||||
<TldrawUiMenuItem
|
||||
id="about"
|
||||
label="help-menu.about"
|
||||
readonlyOk
|
||||
onSelect={() => {
|
||||
openUrl('https://tldraw.dev')
|
||||
}}
|
||||
/>
|
||||
</TldrawUiMenuGroup>
|
||||
</>
|
||||
)
|
||||
}
|
||||
153
apps/vscode/editor/src/app.tsx
Normal file
153
apps/vscode/editor/src/app.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
import { getAssetUrlsByImport } from '@tldraw/assets/imports'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
DefaultMainMenu,
|
||||
DefaultSpinner,
|
||||
EditSubmenu,
|
||||
Editor,
|
||||
ErrorBoundary,
|
||||
ExportFileContentSubMenu,
|
||||
ExtrasGroup,
|
||||
PreferencesGroup,
|
||||
TLComponents,
|
||||
Tldraw,
|
||||
ViewSubmenu,
|
||||
hardReset,
|
||||
setRuntimeOverrides,
|
||||
} from 'tldraw'
|
||||
import 'tldraw/tldraw.css'
|
||||
import { VscodeMessage } from '../../messages'
|
||||
import '../public/index.css'
|
||||
import { ChangeResponder } from './ChangeResponder'
|
||||
import { FileOpen } from './FileOpen'
|
||||
import { FullPageMessage } from './FullPageMessage'
|
||||
import { Links } from './Links'
|
||||
import { onCreateAssetFromUrl } from './utils/bookmarks'
|
||||
import { registerExternalUrlContentHandler } from './utils/externalUrlContentHandler'
|
||||
import { vscode } from './utils/vscode'
|
||||
|
||||
setRuntimeOverrides({
|
||||
openWindow: (url, target) => {
|
||||
if (url.includes('utm_campaign=watermark')) {
|
||||
url = url.replace('utm_source=sdk', 'utm_source=extension')
|
||||
}
|
||||
vscode.postMessage({
|
||||
type: 'vscode:open-window',
|
||||
data: {
|
||||
url,
|
||||
target,
|
||||
},
|
||||
})
|
||||
},
|
||||
refreshPage: () => {
|
||||
vscode.postMessage({
|
||||
type: 'vscode:refresh-page',
|
||||
})
|
||||
},
|
||||
hardReset: async () => {
|
||||
await hardReset({ shouldReload: false })
|
||||
vscode.postMessage({
|
||||
type: 'vscode:hard-reset',
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const handleError = (error: any) => {
|
||||
console.error(error.message)
|
||||
}
|
||||
|
||||
export function WrappedTldrawEditor() {
|
||||
return (
|
||||
<div className="tldraw--editor">
|
||||
<ErrorBoundary
|
||||
fallback={() => <FullPageMessage>Fallback</FullPageMessage>}
|
||||
onError={handleError}
|
||||
>
|
||||
<TldrawWrapper />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function TldrawWrapper() {
|
||||
const [tldrawInnerProps, setTldrawInnerProps] = useState<TLDrawInnerProps | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
function handleMessage({ data: message }: MessageEvent<VscodeMessage>) {
|
||||
switch (message.type) {
|
||||
case 'vscode:opened-file': {
|
||||
setTldrawInnerProps({
|
||||
assetSrc: message.data.assetSrc,
|
||||
fileContents: message.data.fileContents,
|
||||
uri: message.data.uri,
|
||||
isDarkMode: message.data.isDarkMode,
|
||||
})
|
||||
// We only want to listen for this message once
|
||||
window.removeEventListener('message', handleMessage)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('message', handleMessage)
|
||||
|
||||
vscode.postMessage({ type: 'vscode:ready-to-receive-file' })
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('message', handleMessage)
|
||||
}
|
||||
}, [setTldrawInnerProps])
|
||||
|
||||
return tldrawInnerProps === null ? (
|
||||
<FullPageMessage>
|
||||
<DefaultSpinner />
|
||||
</FullPageMessage>
|
||||
) : (
|
||||
<TldrawInner {...tldrawInnerProps} />
|
||||
)
|
||||
}
|
||||
|
||||
export interface TLDrawInnerProps {
|
||||
assetSrc: string
|
||||
fileContents: string
|
||||
uri: string
|
||||
isDarkMode: boolean
|
||||
}
|
||||
|
||||
const components: TLComponents = {
|
||||
MainMenu: () => (
|
||||
<DefaultMainMenu>
|
||||
<EditSubmenu />
|
||||
<ViewSubmenu />
|
||||
<ExportFileContentSubMenu />
|
||||
<ExtrasGroup />
|
||||
<PreferencesGroup />
|
||||
<Links />
|
||||
</DefaultMainMenu>
|
||||
),
|
||||
}
|
||||
function TldrawInner({ uri, assetSrc, isDarkMode, fileContents }: TLDrawInnerProps) {
|
||||
const assetUrls = useMemo(() => getAssetUrlsByImport({ baseUrl: assetSrc }), [assetSrc])
|
||||
|
||||
const handleMount = useCallback((editor: Editor) => {
|
||||
editor.registerExternalAssetHandler('url', onCreateAssetFromUrl)
|
||||
registerExternalUrlContentHandler(editor)
|
||||
}, [])
|
||||
|
||||
const licenseKey =
|
||||
'tldraw-tldraw-2027-05-22/WyJDOEVmdnNlQyIsWyJ2c2NvZGUtd2VidmlldzpcXC9cXC8uKnRsZHJhdy1vcmdcXC50bGRyYXctdnNjb2RlIl0sNDEsIjIwMjctMDUtMjIiXQ.s/4f2jMqZhK2eAHT0LjZZ48NAcrsZ7UdWMerUzQ6q8SZtpkMmuv3JoPviFo+xkfNxD4eaGTGWgHIYyX1VCbwXg'
|
||||
return (
|
||||
<Tldraw
|
||||
assetUrls={assetUrls}
|
||||
persistenceKey={uri}
|
||||
onMount={handleMount}
|
||||
components={components}
|
||||
licenseKey={licenseKey}
|
||||
>
|
||||
{/* <DarkModeHandler themeKind={themeKind} /> */}
|
||||
|
||||
<FileOpen fileContents={fileContents} forceDarkMode={isDarkMode} />
|
||||
<ChangeResponder />
|
||||
</Tldraw>
|
||||
)
|
||||
}
|
||||
9
apps/vscode/editor/src/index.tsx
Normal file
9
apps/vscode/editor/src/index.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import * as React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import { WrappedTldrawEditor } from './app'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<WrappedTldrawEditor />
|
||||
</React.StrictMode>
|
||||
)
|
||||
70
apps/vscode/editor/src/utils/bookmarks.ts
Normal file
70
apps/vscode/editor/src/utils/bookmarks.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { AssetRecordType, TLAsset, TLUrlExternalContent, fetch, getHashForString } from 'tldraw'
|
||||
import { rpc } from './rpc'
|
||||
|
||||
export async function onCreateAssetFromUrl({ url }: TLUrlExternalContent): Promise<TLAsset> {
|
||||
try {
|
||||
// First, try to get the data from the extension manager process, using node's fetch
|
||||
const meta = await rpc('vscode:bookmark', { url })
|
||||
|
||||
return {
|
||||
id: AssetRecordType.createId(getHashForString(url)),
|
||||
typeName: 'asset',
|
||||
type: 'bookmark',
|
||||
props: {
|
||||
src: url,
|
||||
description: meta.description ?? '',
|
||||
image: meta.image ?? '',
|
||||
favicon: meta.favicon ?? '',
|
||||
title: meta.title ?? '',
|
||||
},
|
||||
meta: {},
|
||||
}
|
||||
} catch {
|
||||
// Otherwise, fallback to fetching data from the url
|
||||
|
||||
let meta: { image: string; favicon: string; title: string; description: string }
|
||||
|
||||
try {
|
||||
const resp = await fetch(url, {
|
||||
method: 'GET',
|
||||
mode: 'no-cors',
|
||||
})
|
||||
const html = await resp.text()
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html')
|
||||
meta = {
|
||||
image: doc.head.querySelector('meta[property="og:image"]')?.getAttribute('content') ?? '',
|
||||
favicon:
|
||||
doc.head.querySelector('link[rel="apple-touch-icon"]')?.getAttribute('href') ??
|
||||
doc.head.querySelector('link[rel="icon"]')?.getAttribute('href') ??
|
||||
'',
|
||||
title: doc.head.querySelector('meta[property="og:title"]')?.getAttribute('content') ?? '',
|
||||
description:
|
||||
doc.head.querySelector('meta[property="og:description"]')?.getAttribute('content') ?? '',
|
||||
}
|
||||
if (meta.image && !meta.image.startsWith('http')) {
|
||||
meta.image = new URL(meta.image, url).href
|
||||
}
|
||||
if (meta.favicon && !meta.favicon.startsWith('http')) {
|
||||
meta.favicon = new URL(meta.favicon, url).href
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
meta = { image: '', favicon: '', title: '', description: '' }
|
||||
}
|
||||
|
||||
// Create the bookmark asset from the meta
|
||||
return {
|
||||
id: AssetRecordType.createId(getHashForString(url)),
|
||||
typeName: 'asset',
|
||||
type: 'bookmark',
|
||||
props: {
|
||||
src: url,
|
||||
image: meta.image,
|
||||
favicon: meta.favicon,
|
||||
title: meta.title,
|
||||
description: meta.description,
|
||||
},
|
||||
meta: {},
|
||||
}
|
||||
}
|
||||
}
|
||||
25
apps/vscode/editor/src/utils/externalUrlContentHandler.ts
Normal file
25
apps/vscode/editor/src/utils/externalUrlContentHandler.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Editor } from 'tldraw'
|
||||
import { rpc } from './rpc'
|
||||
|
||||
export function registerExternalUrlContentHandler(editor: Editor) {
|
||||
const currentFilesContentHandler = editor.externalContentHandlers['files']
|
||||
const currentUrlContentHandler = editor.externalContentHandlers['url']
|
||||
editor.registerExternalContentHandler('url', async (info) => {
|
||||
// This happens when you shift drag a file from the file explorer to the editor
|
||||
if (info.url.startsWith('file://')) {
|
||||
// We cannot fetch the file directly since we are sandboxed, we have to ask the extension manager to get it for us
|
||||
const data = await rpc('vscode:get-file', { url: info.url })
|
||||
const uint8Array = new Uint8Array(data.file)
|
||||
const blob = new Blob([uint8Array], { type: data.mimeType })
|
||||
const file = new File([blob], data.fileName, { type: data.mimeType })
|
||||
currentFilesContentHandler?.({
|
||||
type: 'files',
|
||||
files: [file],
|
||||
ignoreParent: false,
|
||||
})
|
||||
} else {
|
||||
// default logic if it's a regular url
|
||||
currentUrlContentHandler?.(info)
|
||||
}
|
||||
})
|
||||
}
|
||||
63
apps/vscode/editor/src/utils/rpc.ts
Normal file
63
apps/vscode/editor/src/utils/rpc.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { uniqueId } from 'tldraw'
|
||||
import type { VscodeMessage, VscodeMessagePairs } from '../../../messages'
|
||||
import { vscode } from './vscode'
|
||||
|
||||
interface SimpleRpcOpts {
|
||||
timeout: number
|
||||
}
|
||||
class SimpleRpcError extends Error {
|
||||
id: string
|
||||
data: any
|
||||
constructor(id: keyof VscodeMessagePairs, data: any) {
|
||||
super(`Failed ${id}`)
|
||||
this.id = id
|
||||
this.data = data
|
||||
}
|
||||
}
|
||||
|
||||
export function rpc<T extends keyof VscodeMessagePairs>(
|
||||
id: T,
|
||||
data: VscodeMessagePairs[T]['request']['data'],
|
||||
opts: SimpleRpcOpts = { timeout: 5 * 1000 }
|
||||
) {
|
||||
const { timeout } = opts
|
||||
type RequestType = VscodeMessagePairs[T]['request']
|
||||
type ResponseType = VscodeMessagePairs[T]['response']
|
||||
type ErrorType = VscodeMessagePairs[T]['error']
|
||||
|
||||
const type = (id + '/request') as RequestType['type']
|
||||
const uuid = uniqueId()
|
||||
return new Promise<ResponseType['data']>((resolve, reject) => {
|
||||
const inMessage: VscodeMessage = {
|
||||
uuid,
|
||||
type,
|
||||
data,
|
||||
}
|
||||
vscode.postMessage(inMessage)
|
||||
|
||||
const handler = ({ data: response }: MessageEvent<ResponseType | ErrorType>) => {
|
||||
// Only handle messages that are meant to be a direct response to the message we sent
|
||||
if (response.uuid !== uuid + '_response') {
|
||||
return
|
||||
}
|
||||
|
||||
const cleanup = () => {
|
||||
window.removeEventListener('message', handler)
|
||||
}
|
||||
|
||||
if (response.type === `${id}/response`) {
|
||||
cleanup()
|
||||
resolve(response.data as ResponseType['data'])
|
||||
}
|
||||
if (response.type === `${id}/error`) {
|
||||
cleanup()
|
||||
reject(new SimpleRpcError(id, response.data as ErrorType['data']))
|
||||
}
|
||||
setTimeout(() => {
|
||||
cleanup()
|
||||
reject(new SimpleRpcError(id, { timeout: true }))
|
||||
}, timeout)
|
||||
}
|
||||
window.addEventListener('message', handler)
|
||||
})
|
||||
}
|
||||
8
apps/vscode/editor/src/utils/url.ts
Normal file
8
apps/vscode/editor/src/utils/url.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { vscode } from './vscode'
|
||||
|
||||
export function openUrl(url: string) {
|
||||
vscode.postMessage({
|
||||
type: 'vscode:open-window',
|
||||
data: { url, target: '_blank' },
|
||||
})
|
||||
}
|
||||
9
apps/vscode/editor/src/utils/vscode.ts
Normal file
9
apps/vscode/editor/src/utils/vscode.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
// @ts-ignore
|
||||
import type { VscodeMessage } from '../../../messages'
|
||||
|
||||
// Will be placed in global scope by extension
|
||||
declare function acquireVsCodeApi(): {
|
||||
postMessage(options: VscodeMessage): void
|
||||
}
|
||||
|
||||
export const vscode = acquireVsCodeApi()
|
||||
26
apps/vscode/editor/tsconfig.json
Normal file
26
apps/vscode/editor/tsconfig.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"removeComments": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"noUncheckedSideEffectImports": false,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"composite": true,
|
||||
"importHelpers": false,
|
||||
"skipDefaultLibCheck": true
|
||||
},
|
||||
"include": ["src", "../messages", "scripts"],
|
||||
"references": [{ "path": "../../../packages/assets" }, { "path": "../../../packages/tldraw" }]
|
||||
}
|
||||
Reference in New Issue
Block a user