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

This commit is contained in:
upstream-mirror
2026-09-23 16:12:52 +05:00
commit a774ac673a
4541 changed files with 770499 additions and 0 deletions

View File

@@ -0,0 +1,40 @@
import { AssetRecordType, TLAsset, TLBookmarkAsset, getHashForString } from 'tldraw'
// How does our server handle bookmark unfurling?
export async function getBookmarkPreview({ url }: { url: string }): Promise<TLAsset> {
// we start with an empty asset record
const asset: TLBookmarkAsset = {
id: AssetRecordType.createId(getHashForString(url)),
typeName: 'asset',
type: 'bookmark',
meta: {},
props: {
src: url,
description: '',
image: '',
favicon: '',
title: '',
},
}
try {
// try to fetch the preview data from the server
const response = await fetch(`/api/unfurl?url=${encodeURIComponent(url)}`)
const data: any = await response.json()
// fill in our asset with whatever info we found
asset.props.description = data?.description ?? ''
asset.props.image = data?.image ?? ''
asset.props.favicon = data?.favicon ?? ''
asset.props.title = data?.title ?? ''
// carry the social image's dimensions on `meta` so embeds (e.g. Vimeo/YouTube) can size
// themselves to the content's real aspect ratio instead of staying letterboxed
if (typeof data?.imageWidth === 'number') asset.meta.imageWidth = data.imageWidth
if (typeof data?.imageHeight === 'number') asset.meta.imageHeight = data.imageHeight
} catch (e) {
console.error(e)
}
return asset
}

View File

@@ -0,0 +1,88 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@500;700&display=swap');
@import url('tldraw/tldraw.css');
body {
font-family: 'Inter', sans-serif;
overscroll-behavior: none;
}
.RoomWrapper {
--background: white;
--text: hsl(0, 0%, 20%);
--gray-light: #f5f5f5;
--gray-dark: #e5e5e5;
--black-transparent-lighter: rgba(0, 0, 0, 0.15);
--black-transparent-light: rgba(0, 0, 0, 0.4);
--black-transparent-dark: rgba(0, 0, 0, 0.6);
--black-transparent-darker: rgba(0, 0, 0, 0.7);
--focus: hsl(217, 89%, 61%);
--whiteboard: hsl(240, 5%, 96%);
color-scheme: light;
}
:root:has(.tl-theme__dark) .RoomWrapper {
--background: black;
--text: hsl(0, 9%, 94%);
--gray-light: #333;
--gray-dark: #444;
--black-transparent-lighter: rgba(255, 255, 255, 0.15);
--black-transparent-light: rgba(255, 255, 255, 0.4);
--black-transparent-dark: rgba(255, 255, 255, 0.6);
--black-transparent-darker: rgba(255, 255, 255, 0.7);
--focus: hsl(217, 89%, 61%);
--whiteboard: hsl(240, 5%, 6.5%);
color-scheme: dark;
}
.RoomWrapper {
display: flex;
flex-direction: column;
position: absolute;
inset: 0px;
}
.RoomWrapper-header {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
padding: 8px 16px;
gap: 16px;
background: var(--background);
border-bottom: 1px solid var(--gray-dark);
color: var(--text);
font-size: 14px;
}
.RoomWrapper-copy {
background: var(--black-transparent-lighter);
border: 1px solid var(--gray-dark);
border-radius: 4px;
padding: 4px 12px;
height: 24px;
cursor: pointer;
position: relative;
text-align: center;
}
.RoomWrapper-copy:has(.RoomWrapper-copied) {
color: transparent;
}
.RoomWrapper-copied {
color: var(--text);
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
}
.RoomWrapper-copy:hover {
border: 1px solid var(--black-transparent-lighter);
}
.RoomWrapper-content {
position: relative;
flex: 1;
}

View File

@@ -0,0 +1,21 @@
/**
* Safe localStorage helpers that handle cases where localStorage is not available
* or access is restricted by the browser.
*/
export function getLocalStorageItem(key: string): string | null {
try {
return localStorage.getItem(key)
} catch (error) {
console.warn('Failed to access localStorage:', error)
return null
}
}
export function setLocalStorageItem(key: string, value: string): void {
try {
localStorage.setItem(key, value)
} catch (error) {
console.warn('Failed to set localStorage item:', error)
}
}

View File

@@ -0,0 +1,23 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
import './index.css'
import { Room } from './pages/Room'
import { Root } from './pages/Root'
const router = createBrowserRouter([
{
path: '/',
element: <Root />,
},
{
path: '/:roomId',
element: <Room />,
},
])
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
)

View File

@@ -0,0 +1,30 @@
import { TLAssetStore, uniqueId } from 'tldraw'
// How does our server handle assets like images and videos?
export const multiplayerAssetStore: TLAssetStore = {
// to upload an asset, we...
async upload(_asset, file) {
// ...create a unique name & URL...
const objectName = `${uniqueId()}-${file.name}`.replace(/[^a-zA-Z0-9.]/g, '-')
const url = `/api/uploads/${objectName}`
// ...POST it to out worker to upload it...
const response = await fetch(url, {
method: 'POST',
body: file,
})
if (!response.ok) {
throw new Error(`Failed to upload asset: ${response.statusText}`)
}
// ...and return the URL to be stored with the asset record.
return { src: url }
},
// to retrieve an asset, we can just use the same URL. you could customize this to add extra
// auth, or to serve optimized versions / sizes of the asset.
resolve(asset) {
return asset.props.src
},
}

View File

@@ -0,0 +1,83 @@
import { useSync } from '@tldraw/sync'
import { ReactNode, useEffect, useState } from 'react'
import { useParams } from 'react-router-dom'
import { Tldraw } from 'tldraw'
import { getBookmarkPreview } from '../getBookmarkPreview'
import { multiplayerAssetStore } from '../multiplayerAssetStore'
export function Room() {
const { roomId } = useParams<{ roomId: string }>()
// Create a store connected to multiplayer.
const store = useSync({
// We need to know the websockets URI...
uri: `${window.location.origin}/api/connect/${roomId}`,
// ...and how to handle static assets like images & videos
assets: multiplayerAssetStore,
})
return (
<RoomWrapper roomId={roomId}>
<Tldraw
// we can pass the connected store into the Tldraw component which will handle
// loading states & enable multiplayer UX like cursors & a presence menu
store={store}
options={{ deepLinks: true }}
onMount={(editor) => {
// when the editor is ready, we need to register our bookmark unfurling service
editor.registerExternalAssetHandler('url', getBookmarkPreview)
}}
/>
</RoomWrapper>
)
}
function RoomWrapper({ children, roomId }: { children: ReactNode; roomId?: string }) {
const [didCopy, setDidCopy] = useState(false)
useEffect(() => {
if (!didCopy) return
const timeout = setTimeout(() => setDidCopy(false), 3000)
return () => clearTimeout(timeout)
}, [didCopy])
return (
<div className="RoomWrapper">
<div className="RoomWrapper-header">
<WifiIcon />
<div>{roomId}</div>
<button
className="RoomWrapper-copy"
onClick={() => {
navigator.clipboard.writeText(window.location.href)
setDidCopy(true)
}}
aria-label="copy room link"
>
Copy link
{didCopy && <div className="RoomWrapper-copied">Copied!</div>}
</button>
</div>
<div className="RoomWrapper-content">{children}</div>
</div>
)
}
function WifiIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
width={16}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M8.288 15.038a5.25 5.25 0 0 1 7.424 0M5.106 11.856c3.807-3.808 9.98-3.808 13.788 0M1.924 8.674c5.565-5.565 14.587-5.565 20.152 0M12.53 18.22l-.53.53-.53-.53a.75.75 0 0 1 1.06 0Z"
/>
</svg>
)
}

View File

@@ -0,0 +1,10 @@
import { Navigate } from 'react-router-dom'
import { uniqueId } from 'tldraw'
import { getLocalStorageItem, setLocalStorageItem } from '../localStorage'
const myLocalRoomId = getLocalStorageItem('my-local-room-id') ?? 'test-room-' + uniqueId()
setLocalStorageItem('my-local-room-id', myLocalRoomId)
export function Root() {
return <Navigate to={`/${myLocalRoomId}`} />
}

View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />