[5/5] Move bemo from dotcom to examples (#4135)
Move our bemo playground from dotcom to the examples app. In preparation for more multiplayer examples, I built our a little bit of chrome around example room IDs: if you create an example with `multiplayer: true`, the examples app will render a little room ID picker above your example. The room IDs are scoped to each example, and each deploy of the examples app. By default people on the same example will be in the same room, but the default ID changes every hour. As I was doing this, I noticed you could get an ugly situation where the docs site was in dark mode, tldraw was in dark mode, but the little bit of examples chrome was in light mode. To fix this I through together an extremely rough dark mode for the examples which switches on whenever the tldraw instance inside is in dark mode. ### Change type - [x] `other` --------- Co-authored-by: David Sheldrick <d.j.sheldrick@gmail.com>
This commit is contained in:
parent
d684866f90
commit
64e1fac897
23 changed files with 306 additions and 62 deletions
|
@ -25,10 +25,10 @@ export class BemoDO extends DurableObject<Environment> {
|
||||||
r2: R2Bucket
|
r2: R2Bucket
|
||||||
_slug: string | null = null
|
_slug: string | null = null
|
||||||
|
|
||||||
analytics: AnalyticsEngineDataset
|
analytics?: AnalyticsEngineDataset
|
||||||
|
|
||||||
writeEvent({ type, origin, sessionKey, slug }: AnalyticsEvent) {
|
writeEvent({ type, origin, sessionKey, slug }: AnalyticsEvent) {
|
||||||
this.analytics.writeDataPoint({
|
this.analytics?.writeDataPoint({
|
||||||
blobs: [type, origin, slug, sessionKey],
|
blobs: [type, origin, slug, sessionKey],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,5 +13,5 @@ export interface Environment {
|
||||||
IS_LOCAL: string | undefined
|
IS_LOCAL: string | undefined
|
||||||
WORKER_NAME: string | undefined
|
WORKER_NAME: string | undefined
|
||||||
|
|
||||||
BEMO_ANALYTICS: AnalyticsEngineDataset
|
BEMO_ANALYTICS?: AnalyticsEngineDataset
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ export default function ExampleCodeBlock({
|
||||||
useEffect(() => setIsClientSide(true), [])
|
useEffect(() => setIsClientSide(true), [])
|
||||||
const SERVER =
|
const SERVER =
|
||||||
process.env.NODE_ENV === 'development'
|
process.env.NODE_ENV === 'development'
|
||||||
? `http://${location.host}:5420`
|
? `http://${location.hostname}:5420`
|
||||||
: location.host.includes('staging') || location.host.includes('canary')
|
: location.host.includes('staging') || location.host.includes('canary')
|
||||||
? `https://examples-canary.tldraw.com`
|
? `https://examples-canary.tldraw.com`
|
||||||
: 'https://examples.tldraw.com'
|
: 'https://examples.tldraw.com'
|
||||||
|
|
|
@ -1432,6 +1432,8 @@ html[data-theme='light'] .hero__dark {
|
||||||
min-height: 500px;
|
min-height: 500px;
|
||||||
height: 50vh;
|
height: 50vh;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
border-radius: var(--border-radius-menu);
|
||||||
|
border: 1px solid var(--color-tint-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-example .sandpack {
|
.code-example .sandpack {
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
import { useMultiplayerDemo } from '@tldraw/sync'
|
|
||||||
import { Tldraw } from 'tldraw'
|
|
||||||
import { assetUrls } from '../utils/assetUrls'
|
|
||||||
|
|
||||||
export function TemporaryBemoDevEditor({ slug }: { slug: string }) {
|
|
||||||
const store = useMultiplayerDemo({ host: 'http://127.0.0.1:8989', roomId: slug })
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="tldraw__editor">
|
|
||||||
<Tldraw store={store} assetUrls={assetUrls} inferDarkMode />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
import { useParams } from 'react-router-dom'
|
|
||||||
import '../../styles/globals.css'
|
|
||||||
import { IFrameProtector, ROOM_CONTEXT } from '../components/IFrameProtector'
|
|
||||||
import { TemporaryBemoDevEditor } from '../components/TemporaryBemoDevEditor'
|
|
||||||
|
|
||||||
export function Component() {
|
|
||||||
const id = useParams()['roomId'] as string
|
|
||||||
return (
|
|
||||||
<IFrameProtector slug={id} context={ROOM_CONTEXT.PUBLIC_MULTIPLAYER}>
|
|
||||||
<TemporaryBemoDevEditor slug={id} />
|
|
||||||
</IFrameProtector>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -10,11 +10,6 @@ import { Outlet, Route, createRoutesFromElements, useRouteError } from 'react-ro
|
||||||
import { DefaultErrorFallback } from './components/DefaultErrorFallback/DefaultErrorFallback'
|
import { DefaultErrorFallback } from './components/DefaultErrorFallback/DefaultErrorFallback'
|
||||||
import { ErrorPage } from './components/ErrorPage/ErrorPage'
|
import { ErrorPage } from './components/ErrorPage/ErrorPage'
|
||||||
|
|
||||||
const enableTemporaryLocalBemo =
|
|
||||||
window.location.hostname === 'localhost' &&
|
|
||||||
window.location.port === '3000' &&
|
|
||||||
typeof jest === 'undefined'
|
|
||||||
|
|
||||||
export const router = createRoutesFromElements(
|
export const router = createRoutesFromElements(
|
||||||
<Route
|
<Route
|
||||||
element={
|
element={
|
||||||
|
@ -55,9 +50,6 @@ export const router = createRoutesFromElements(
|
||||||
lazy={() => import('./pages/public-readonly-legacy')}
|
lazy={() => import('./pages/public-readonly-legacy')}
|
||||||
/>
|
/>
|
||||||
<Route path={`/${READ_ONLY_PREFIX}/:roomId`} lazy={() => import('./pages/public-readonly')} />
|
<Route path={`/${READ_ONLY_PREFIX}/:roomId`} lazy={() => import('./pages/public-readonly')} />
|
||||||
{enableTemporaryLocalBemo && (
|
|
||||||
<Route path={`/bemo/:roomId`} lazy={() => import('./pages/temporary-bemo')} />
|
|
||||||
)}
|
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="*" lazy={() => import('./pages/not-found')} />
|
<Route path="*" lazy={() => import('./pages/not-found')} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
|
@ -37,6 +37,7 @@
|
||||||
"@radix-ui/react-alert-dialog": "^1.0.5",
|
"@radix-ui/react-alert-dialog": "^1.0.5",
|
||||||
"@tldraw/assets": "workspace:*",
|
"@tldraw/assets": "workspace:*",
|
||||||
"@tldraw/dotcom-shared": "workspace:*",
|
"@tldraw/dotcom-shared": "workspace:*",
|
||||||
|
"@tldraw/sync": "workspace:*",
|
||||||
"@vercel/analytics": "^1.1.1",
|
"@vercel/analytics": "^1.1.1",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
"lazyrepo": "0.0.0-alpha.27",
|
"lazyrepo": "0.0.0-alpha.27",
|
||||||
|
|
|
@ -228,12 +228,13 @@ function Dialogs() {
|
||||||
|
|
||||||
function SocialIcon({ icon }: { icon: string }) {
|
function SocialIcon({ icon }: { icon: string }) {
|
||||||
return (
|
return (
|
||||||
<img
|
<div
|
||||||
className="example__sidebar__icon"
|
className="example__sidebar__icon"
|
||||||
src={`/icons/${icon}.svg`}
|
|
||||||
style={{
|
style={{
|
||||||
mask: `url(/icons/${icon}.svg) center 100% / 100% no-repeat`,
|
mask: `url(/icons/${icon}.svg) center 100% / 100% no-repeat`,
|
||||||
WebkitMask: `url(/icons/${icon}.svg) center 100% / 100% no-repeat`,
|
WebkitMask: `url(/icons/${icon}.svg) center 100% / 100% no-repeat`,
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
83
apps/examples/src/ExampleWrapper.tsx
Normal file
83
apps/examples/src/ExampleWrapper.tsx
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { Example } from './examples'
|
||||||
|
|
||||||
|
export function ExampleWrapper({
|
||||||
|
example,
|
||||||
|
component: Component,
|
||||||
|
}: {
|
||||||
|
example: Example
|
||||||
|
component: React.ComponentType<{ roomId?: string }>
|
||||||
|
}) {
|
||||||
|
if (!example.multiplayer) {
|
||||||
|
return <Component />
|
||||||
|
}
|
||||||
|
|
||||||
|
return <MultiplayerExampleWrapper component={Component} example={example} />
|
||||||
|
}
|
||||||
|
|
||||||
|
const hour = 60 * 1000 * 1000
|
||||||
|
function MultiplayerExampleWrapper({
|
||||||
|
component: Component,
|
||||||
|
example,
|
||||||
|
}: {
|
||||||
|
example: Example
|
||||||
|
component: React.ComponentType<{ roomId?: string }>
|
||||||
|
}) {
|
||||||
|
const prefix = `tldraw-example-${example.path.replace(/\//g, '-')}-${process.env.TLDRAW_DEPLOY_ID}`
|
||||||
|
|
||||||
|
const [roomId, setRoomId] = useState(String(Math.floor(Date.now() / hour)))
|
||||||
|
const [nextRoomId, setNextRoomId] = useState(roomId)
|
||||||
|
|
||||||
|
const trimmed = nextRoomId.trim()
|
||||||
|
const canSet = trimmed && roomId !== trimmed
|
||||||
|
function setIfPossible() {
|
||||||
|
if (!canSet) return
|
||||||
|
setRoomId(trimmed)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="MultiplayerExampleWrapper">
|
||||||
|
<div className="MultiplayerExampleWrapper-picker">
|
||||||
|
<label>
|
||||||
|
<div>Room ID:</div>
|
||||||
|
<input
|
||||||
|
value={nextRoomId}
|
||||||
|
onChange={(e) => setNextRoomId(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' && canSet) {
|
||||||
|
e.preventDefault()
|
||||||
|
setIfPossible()
|
||||||
|
}
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
e.preventDefault()
|
||||||
|
setNextRoomId(roomId)
|
||||||
|
e.currentTarget.blur()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<button onClick={() => setIfPossible()} disabled={!canSet} aria-label="join">
|
||||||
|
<ArrowIcon />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="MultiplayerExampleWrapper-example">
|
||||||
|
<div>
|
||||||
|
<Component roomId={`${prefix}_${encodeURIComponent(roomId)}`} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ArrowIcon() {
|
||||||
|
return (
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M9.25012 4.75L12.5001 8M12.5001 8L9.25012 11.25M12.5001 8H3.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
|
@ -11,7 +11,8 @@ export interface Example {
|
||||||
priority: number
|
priority: number
|
||||||
componentFile: string
|
componentFile: string
|
||||||
keywords: string[]
|
keywords: string[]
|
||||||
loadComponent: () => Promise<ComponentType>
|
multiplayer: boolean
|
||||||
|
loadComponent: () => Promise<ComponentType<{ roomId?: string }>>
|
||||||
}
|
}
|
||||||
|
|
||||||
type Category =
|
type Category =
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { useMultiplayerDemo } from '@tldraw/sync'
|
||||||
|
import { Tldraw } from 'tldraw'
|
||||||
|
import 'tldraw/tldraw.css'
|
||||||
|
|
||||||
|
export default function MultiplayerDemoExample({ roomId }: { roomId: string }) {
|
||||||
|
const store = useMultiplayerDemo({ roomId })
|
||||||
|
return (
|
||||||
|
<div className="tldraw__editor">
|
||||||
|
<Tldraw store={store} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
14
apps/examples/src/examples/multiplayer-demo/README.md
Normal file
14
apps/examples/src/examples/multiplayer-demo/README.md
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
title: Multiplayer demo
|
||||||
|
component: ./MultiplayerDemoExample.tsx
|
||||||
|
category: basic
|
||||||
|
priority: 3
|
||||||
|
keywords: [basic, intro, simple, quick, start, multiplayer, sync, collaboration]
|
||||||
|
multiplayer: true
|
||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
The `useMultiplayerDemo` hook can be used to quickly prototype multiplayer experiences in tldraw,
|
||||||
|
although rooms will be deleted after one day. Note that the board below is shared with all visitors
|
||||||
|
to this page.
|
|
@ -43,7 +43,6 @@ export default function MultipleExample() {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: '#fff',
|
|
||||||
padding: 32,
|
padding: 32,
|
||||||
}}
|
}}
|
||||||
// Sorry you need to do this yourself
|
// Sorry you need to do this yourself
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
setDefaultUiAssetUrls,
|
setDefaultUiAssetUrls,
|
||||||
} from 'tldraw'
|
} from 'tldraw'
|
||||||
import { ExamplePage } from './ExamplePage'
|
import { ExamplePage } from './ExamplePage'
|
||||||
|
import { ExampleWrapper } from './ExampleWrapper'
|
||||||
import { examples } from './examples'
|
import { examples } from './examples'
|
||||||
import Develop from './misc/develop'
|
import Develop from './misc/develop'
|
||||||
import EndToEnd from './misc/end-to-end'
|
import EndToEnd from './misc/end-to-end'
|
||||||
|
@ -24,9 +25,6 @@ if (!gettingStartedExamples) throw new Error('Could not find getting started exa
|
||||||
const basicExample = gettingStartedExamples.value.find((e) => e.title === 'Tldraw component')
|
const basicExample = gettingStartedExamples.value.find((e) => e.title === 'Tldraw component')
|
||||||
if (!basicExample) throw new Error('Could not find initial example')
|
if (!basicExample) throw new Error('Could not find initial example')
|
||||||
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log('bemo', process.env.TLDRAW_BEMO_URL)
|
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
path: '*',
|
path: '*',
|
||||||
|
@ -39,7 +37,7 @@ const router = createBrowserRouter([
|
||||||
return {
|
return {
|
||||||
element: (
|
element: (
|
||||||
<ExamplePage example={basicExample}>
|
<ExamplePage example={basicExample}>
|
||||||
<Component />
|
<ExampleWrapper example={basicExample} component={Component} />
|
||||||
</ExamplePage>
|
</ExamplePage>
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
@ -62,7 +60,7 @@ const router = createBrowserRouter([
|
||||||
return {
|
return {
|
||||||
element: (
|
element: (
|
||||||
<ExamplePage example={example}>
|
<ExamplePage example={example}>
|
||||||
<Component />
|
<ExampleWrapper example={example} component={Component} />
|
||||||
</ExamplePage>
|
</ExamplePage>
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
@ -73,7 +71,7 @@ const router = createBrowserRouter([
|
||||||
lazy: async () => {
|
lazy: async () => {
|
||||||
const Component = await example.loadComponent()
|
const Component = await example.loadComponent()
|
||||||
return {
|
return {
|
||||||
element: <Component />,
|
element: <ExampleWrapper example={example} component={Component} />,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,11 +1,17 @@
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@500;700&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@500;700&display=swap');
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
--background: white;
|
||||||
|
--text: hsl(0, 0%, 11%);
|
||||||
--gray-light: #f5f5f5;
|
--gray-light: #f5f5f5;
|
||||||
--gray-dark: #e8e8e8;
|
--gray-dark: #e8e8e8;
|
||||||
--black-transparent-lighter: rgba(0, 0, 0, 0.07);
|
--black-transparent-lighter: rgba(0, 0, 0, 0.07);
|
||||||
--black-transparent-light: rgba(0, 0, 0, 0.3);
|
--black-transparent-light: rgba(0, 0, 0, 0.3);
|
||||||
--black-transparent-dark: rgba(0, 0, 0, 0.5);
|
--black-transparent-dark: rgba(0, 0, 0, 0.5);
|
||||||
|
--black-transparent-darker: rgba(0, 0, 0, 0.7);
|
||||||
|
--focus: hsl(214, 84%, 56%);
|
||||||
|
--whiteboard: hsl(210, 20%, 98%);
|
||||||
|
color-scheme: light;
|
||||||
}
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
|
@ -19,6 +25,8 @@ body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
/* prevent two-finger swipe to go back */
|
/* prevent two-finger swipe to go back */
|
||||||
overscroll-behavior-x: none;
|
overscroll-behavior-x: none;
|
||||||
|
background-color: var(--background);
|
||||||
|
color: var(--text);
|
||||||
}
|
}
|
||||||
@media screen and (max-width: 600px) {
|
@media screen and (max-width: 600px) {
|
||||||
html,
|
html,
|
||||||
|
@ -245,7 +253,7 @@ li.examples__sidebar__item {
|
||||||
.example__sidebar__footer-link {
|
.example__sidebar__footer-link {
|
||||||
padding: 8px 8px;
|
padding: 8px 8px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background-color: hsl(214, 84%, 56%);
|
background-color: var(--focus);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@ -279,7 +287,7 @@ a.example__sidebar__header-link {
|
||||||
}
|
}
|
||||||
|
|
||||||
.example__sidebar__header__socials a {
|
.example__sidebar__header__socials a {
|
||||||
color: black;
|
color: var(--text);
|
||||||
height: 32px;
|
height: 32px;
|
||||||
width: 32px;
|
width: 32px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -292,8 +300,8 @@ a.example__sidebar__header-link {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
color: black;
|
color: var(--text);
|
||||||
background-color: black;
|
background-color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --------------------- Scroll --------------------- */
|
/* --------------------- Scroll --------------------- */
|
||||||
|
@ -321,7 +329,7 @@ a.example__sidebar__header-link {
|
||||||
min-height: 36px;
|
min-height: 36px;
|
||||||
border: 2px solid transparent;
|
border: 2px solid transparent;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background-color: rgba(0, 0, 0, 0.25);
|
background-color: var(--black-transparent-light);
|
||||||
}
|
}
|
||||||
.scroll-light::-webkit-scrollbar-thumb:hover {
|
.scroll-light::-webkit-scrollbar-thumb:hover {
|
||||||
background-color: var(--black-transparent-light);
|
background-color: var(--black-transparent-light);
|
||||||
|
@ -428,3 +436,138 @@ a.example__sidebar__header-link {
|
||||||
.example__dialog__close {
|
.example__dialog__close {
|
||||||
all: unset;
|
all: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root:has(.tl-theme__dark) .MultiplayerExampleWrapper {
|
||||||
|
--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;
|
||||||
|
}
|
||||||
|
.MultiplayerExampleWrapper {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
container: example / size;
|
||||||
|
}
|
||||||
|
.MultiplayerExampleWrapper-picker {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px;
|
||||||
|
padding-bottom: 7px;
|
||||||
|
}
|
||||||
|
.MultiplayerExampleWrapper-picker::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 6px;
|
||||||
|
height: 29px;
|
||||||
|
width: 232px;
|
||||||
|
margin-left: 28px;
|
||||||
|
background: var(--whiteboard);
|
||||||
|
z-index: -1;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.MultiplayerExampleWrapper-picker label {
|
||||||
|
flex: 0 1 200px;
|
||||||
|
font-size: 14px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px solid var(--gray-dark);
|
||||||
|
color: var(--black-transparent-dark);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
background: var(--background);
|
||||||
|
/* Add space to the left the same as the button to keep the input centered */
|
||||||
|
margin-left: 32px;
|
||||||
|
}
|
||||||
|
.MultiplayerExampleWrapper-picker label:focus-within {
|
||||||
|
border-color: var(--focus);
|
||||||
|
}
|
||||||
|
.MultiplayerExampleWrapper-picker label div {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
.MultiplayerExampleWrapper-picker input {
|
||||||
|
flex: 0 1 auto;
|
||||||
|
width: 100%;
|
||||||
|
font: inherit;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
color: var(--black-transparent-darker);
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.MultiplayerExampleWrapper-picker button {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: none;
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--black-transparent-darker);
|
||||||
|
cursor: pointer;
|
||||||
|
height: 32px;
|
||||||
|
width: 32px;
|
||||||
|
padding: 0;
|
||||||
|
margin: -4px 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.MultiplayerExampleWrapper-picker button:hover::after {
|
||||||
|
content: ' ';
|
||||||
|
position: absolute;
|
||||||
|
inset: 3px;
|
||||||
|
background: var(--black-transparent-lighter);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.MultiplayerExampleWrapper-picker button:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.MultiplayerExampleWrapper-example {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@container example (width < 584px) {
|
||||||
|
.MultiplayerExampleWrapper-picker {
|
||||||
|
position: static;
|
||||||
|
border-bottom: 1px solid var(--gray-dark);
|
||||||
|
background: var(--background);
|
||||||
|
}
|
||||||
|
.MultiplayerExampleWrapper-picker::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.MultiplayerExampleWrapper-picker label {
|
||||||
|
width: 100%;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.MultiplayerExampleWrapper-picker button {
|
||||||
|
margin-right: -4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@container example (width <= 904px) and (width > 840px) {
|
||||||
|
.MultiplayerExampleWrapper-picker {
|
||||||
|
left: 346px;
|
||||||
|
right: 0;
|
||||||
|
justify-content: start;
|
||||||
|
}
|
||||||
|
.MultiplayerExampleWrapper-picker label {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -10,6 +10,9 @@
|
||||||
{
|
{
|
||||||
"path": "../../packages/assets"
|
"path": "../../packages/assets"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "../../packages/sync"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "../../packages/tldraw"
|
"path": "../../packages/tldraw"
|
||||||
},
|
},
|
||||||
|
|
|
@ -42,7 +42,9 @@ const TLDRAW_BEMO_URL_STRING =
|
||||||
? '"https://demo.tldraw.xyz"'
|
? '"https://demo.tldraw.xyz"'
|
||||||
: env === 'canary'
|
: env === 'canary'
|
||||||
? '"https://canary-demo.tldraw.xyz"'
|
? '"https://canary-demo.tldraw.xyz"'
|
||||||
: `"https://pr-${PR_NUMBER}-demo.tldraw.xyz"`
|
: PR_NUMBER
|
||||||
|
? `"https://pr-${PR_NUMBER}-demo.tldraw.xyz"`
|
||||||
|
: undefined
|
||||||
|
|
||||||
export default defineConfig(({ mode }) => ({
|
export default defineConfig(({ mode }) => ({
|
||||||
plugins: [react({ tsDecorators: true }), exampleReadmePlugin()],
|
plugins: [react({ tsDecorators: true }), exampleReadmePlugin()],
|
||||||
|
@ -68,7 +70,15 @@ export default defineConfig(({ mode }) => ({
|
||||||
},
|
},
|
||||||
define: {
|
define: {
|
||||||
'process.env.TLDRAW_ENV': JSON.stringify(process.env.VERCEL_ENV ?? 'development'),
|
'process.env.TLDRAW_ENV': JSON.stringify(process.env.VERCEL_ENV ?? 'development'),
|
||||||
|
'process.env.TLDRAW_DEPLOY_ID': JSON.stringify(
|
||||||
|
process.env.VERCEL_GIT_COMMIT_SHA ?? `local-${Date.now()}`
|
||||||
|
),
|
||||||
'process.env.TLDRAW_BEMO_URL': urlOrLocalFallback(mode, TLDRAW_BEMO_URL_STRING, 8989),
|
'process.env.TLDRAW_BEMO_URL': urlOrLocalFallback(mode, TLDRAW_BEMO_URL_STRING, 8989),
|
||||||
|
'process.env.TLDRAW_IMAGE_URL': urlOrLocalFallback(
|
||||||
|
mode,
|
||||||
|
env === 'development' ? undefined : 'https://images.tldraw.xyz',
|
||||||
|
8989
|
||||||
|
),
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
@ -104,6 +114,7 @@ function exampleReadmePlugin(): PluginOption {
|
||||||
`export const priority = ${JSON.stringify(frontmatter.priority ?? '100000')};`,
|
`export const priority = ${JSON.stringify(frontmatter.priority ?? '100000')};`,
|
||||||
`export const category = ${JSON.stringify(frontmatter.category)};`,
|
`export const category = ${JSON.stringify(frontmatter.category)};`,
|
||||||
`export const hide = ${JSON.stringify(frontmatter.hide)};`,
|
`export const hide = ${JSON.stringify(frontmatter.hide)};`,
|
||||||
|
`export const multiplayer = ${JSON.stringify(frontmatter.multiplayer)};`,
|
||||||
`export const description = ${JSON.stringify(description)};`,
|
`export const description = ${JSON.stringify(description)};`,
|
||||||
`export const details = ${JSON.stringify(details)};`,
|
`export const details = ${JSON.stringify(details)};`,
|
||||||
`export const codeUrl = ${JSON.stringify(codeUrl)};`,
|
`export const codeUrl = ${JSON.stringify(codeUrl)};`,
|
||||||
|
@ -154,6 +165,11 @@ function parseFrontMatter(data: unknown, fileName: string) {
|
||||||
throw new Error(`Frontmatter key 'keywords' must be array in ${fileName}`)
|
throw new Error(`Frontmatter key 'keywords' must be array in ${fileName}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const multiplayer = 'multiplayer' in data ? data.multiplayer : false
|
||||||
|
if (typeof multiplayer !== 'boolean') {
|
||||||
|
throw new Error(`Frontmatter key 'multiplayer' must be boolean in ${fileName}`)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: data.title,
|
title: data.title,
|
||||||
component: data.component,
|
component: data.component,
|
||||||
|
@ -161,5 +177,6 @@ function parseFrontMatter(data: unknown, fileName: string) {
|
||||||
category,
|
category,
|
||||||
hide,
|
hide,
|
||||||
keywords,
|
keywords,
|
||||||
|
multiplayer,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -311,7 +311,7 @@ class ReconnectManager {
|
||||||
)
|
)
|
||||||
|
|
||||||
this.lastAttemptStart = Date.now()
|
this.lastAttemptStart = Date.now()
|
||||||
this.socketAdapter._setNewSocket(new WebSocket(uri))
|
this.socketAdapter._setNewSocket(new WebSocket(httpToWs(uri)))
|
||||||
this.state = 'pendingAttemptResult'
|
this.state = 'pendingAttemptResult'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -456,3 +456,7 @@ class ReconnectManager {
|
||||||
this.isDisposed = true
|
this.isDisposed = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function httpToWs(url: string) {
|
||||||
|
return url.replace(/^http(s)?:/, 'ws$1:')
|
||||||
|
}
|
||||||
|
|
|
@ -52,11 +52,7 @@ export function useMultiplayerSync(opts: UseMultiplayerSyncOptions): RemoteTLSto
|
||||||
schema,
|
schema,
|
||||||
} = opts
|
} = opts
|
||||||
|
|
||||||
const error: NonNullable<typeof state>['error'] = state?.error ?? undefined
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (error) return
|
|
||||||
|
|
||||||
const storeId = uniqueId()
|
const storeId = uniqueId()
|
||||||
|
|
||||||
const userPreferences = computed<{ id: string; color: string; name: string }>(
|
const userPreferences = computed<{ id: string; color: string; name: string }>(
|
||||||
|
@ -134,8 +130,9 @@ export function useMultiplayerSync(opts: UseMultiplayerSyncOptions): RemoteTLSto
|
||||||
didCancel = true
|
didCancel = true
|
||||||
client.close()
|
client.close()
|
||||||
socket.close()
|
socket.close()
|
||||||
|
setState(null)
|
||||||
}
|
}
|
||||||
}, [assets, error, onEditorMount, prefs, roomId, setState, track, uri, schema])
|
}, [assets, onEditorMount, prefs, roomId, schema, setState, track, uri])
|
||||||
|
|
||||||
return useValue<RemoteTLStoreWithStatus>(
|
return useValue<RemoteTLStoreWithStatus>(
|
||||||
'remote synced store',
|
'remote synced store',
|
||||||
|
|
|
@ -38,8 +38,8 @@ function getEnv(cb: () => string | undefined): string | undefined {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEMO_WORKER = getEnv(() => process.env.DEMO_WORKER) ?? 'https://demo.tldraw.xyz'
|
const DEMO_WORKER = getEnv(() => process.env.TLDRAW_BEMO_URL) ?? 'https://demo.tldraw.xyz'
|
||||||
const IMAGE_WORKER = getEnv(() => process.env.IMAGE_WORKER) ?? 'https://images.tldraw.xyz'
|
const IMAGE_WORKER = getEnv(() => process.env.TLDRAW_IMAGE_URL) ?? 'https://images.tldraw.xyz'
|
||||||
|
|
||||||
export function useMultiplayerDemo({
|
export function useMultiplayerDemo({
|
||||||
roomId,
|
roomId,
|
||||||
|
|
|
@ -55,6 +55,8 @@ class MiniflareMonitor {
|
||||||
this.restart()
|
this.restart()
|
||||||
} else if (!err) {
|
} else if (!err) {
|
||||||
console.log(output.replace('[mf:inf]', '')) // or handle the output differently
|
console.log(output.replace('[mf:inf]', '')) // or handle the output differently
|
||||||
|
} else {
|
||||||
|
console.error(output.replace('[mf:err]', '')) // or handle the output differently
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11850,6 +11850,7 @@ __metadata:
|
||||||
"@radix-ui/react-alert-dialog": "npm:^1.0.5"
|
"@radix-ui/react-alert-dialog": "npm:^1.0.5"
|
||||||
"@tldraw/assets": "workspace:*"
|
"@tldraw/assets": "workspace:*"
|
||||||
"@tldraw/dotcom-shared": "workspace:*"
|
"@tldraw/dotcom-shared": "workspace:*"
|
||||||
|
"@tldraw/sync": "workspace:*"
|
||||||
"@types/lodash": "npm:^4.14.188"
|
"@types/lodash": "npm:^4.14.188"
|
||||||
"@vercel/analytics": "npm:^1.1.1"
|
"@vercel/analytics": "npm:^1.1.1"
|
||||||
"@vitejs/plugin-react-swc": "npm:^3.6.0"
|
"@vitejs/plugin-react-swc": "npm:^3.6.0"
|
||||||
|
|
Loading…
Reference in a new issue