Prevent iframe embedding for dotcom (except on tldraw.com) (#2947)
This PR fixes a check on whether the dot com multiplayer editor has been loaded in an iframe. It tries to keep it working on tldraw.com itself. ### Change Type - [x] `patch` — Bug fix ### Test Plan 1. Load me in an iframe
This commit is contained in:
parent
4c1425076e
commit
6d417577be
29 changed files with 299 additions and 137 deletions
|
@ -28,6 +28,7 @@ export async function getRoomSnapshot(request: IRequest, env: Environment): Prom
|
|||
// Send back the snapshot!
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
roomId,
|
||||
records: data.documents.map((d) => d.state),
|
||||
schema: data.schema,
|
||||
error: false,
|
||||
|
|
|
@ -1,85 +0,0 @@
|
|||
import React from 'react'
|
||||
import { useUrl } from '../hooks/useUrl'
|
||||
|
||||
export function EmbeddedInIFrameWarning() {
|
||||
// check if this still works
|
||||
const url = useUrl()
|
||||
|
||||
const [copied, setCopied] = React.useState(false)
|
||||
const rTimeout = React.useRef<any>(0)
|
||||
|
||||
const handleCopy = React.useCallback(() => {
|
||||
setCopied(true)
|
||||
clearTimeout(rTimeout.current)
|
||||
rTimeout.current = setTimeout(() => {
|
||||
setCopied(false)
|
||||
}, 1200)
|
||||
|
||||
const textarea = document.createElement('textarea')
|
||||
textarea.setAttribute('position', 'fixed')
|
||||
textarea.setAttribute('top', '0')
|
||||
textarea.setAttribute('readonly', 'true')
|
||||
textarea.setAttribute('contenteditable', 'true')
|
||||
textarea.style.position = 'fixed'
|
||||
textarea.value = url
|
||||
document.body.appendChild(textarea)
|
||||
textarea.focus()
|
||||
textarea.select()
|
||||
try {
|
||||
const range = document.createRange()
|
||||
range.selectNodeContents(textarea)
|
||||
const sel = window.getSelection()
|
||||
if (sel) {
|
||||
sel.removeAllRanges()
|
||||
sel.addRange(range)
|
||||
textarea.setSelectionRange(0, textarea.value.length)
|
||||
}
|
||||
// eslint-disable-next-line deprecation/deprecation
|
||||
document.execCommand('copy')
|
||||
} catch (err) {
|
||||
null // Could not copy to clipboard
|
||||
} finally {
|
||||
document.body.removeChild(textarea)
|
||||
}
|
||||
}, [url])
|
||||
|
||||
return (
|
||||
<div className="tldraw__editor tl-container">
|
||||
<div className="iframe-warning__container">
|
||||
<a className="iframe-warning__link" href={url} target="_parent">
|
||||
{'Visit this page on tldraw.com '}
|
||||
<svg
|
||||
width="15"
|
||||
height="15"
|
||||
viewBox="0 0 15 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M3 2C2.44772 2 2 2.44772 2 3V12C2 12.5523 2.44772 13 3 13H12C12.5523 13 13 12.5523 13 12V8.5C13 8.22386 12.7761 8 12.5 8C12.2239 8 12 8.22386 12 8.5V12H3V3L6.5 3C6.77614 3 7 2.77614 7 2.5C7 2.22386 6.77614 2 6.5 2H3ZM12.8536 2.14645C12.9015 2.19439 12.9377 2.24964 12.9621 2.30861C12.9861 2.36669 12.9996 2.4303 13 2.497L13 2.5V2.50049V5.5C13 5.77614 12.7761 6 12.5 6C12.2239 6 12 5.77614 12 5.5V3.70711L6.85355 8.85355C6.65829 9.04882 6.34171 9.04882 6.14645 8.85355C5.95118 8.65829 5.95118 8.34171 6.14645 8.14645L11.2929 3H9.5C9.22386 3 9 2.77614 9 2.5C9 2.22386 9.22386 2 9.5 2H12.4999H12.5C12.5678 2 12.6324 2.01349 12.6914 2.03794C12.7504 2.06234 12.8056 2.09851 12.8536 2.14645Z"
|
||||
fill="currentColor"
|
||||
stroke="black"
|
||||
strokeWidth=".5"
|
||||
></path>
|
||||
</svg>
|
||||
</a>
|
||||
<button className="iframe-warning__copy" onClick={handleCopy}>
|
||||
<span>{url}</span>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 15 15"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
{copied ? (
|
||||
<path d="M11.4669 3.72684C11.7558 3.91574 11.8369 4.30308 11.648 4.59198L7.39799 11.092C7.29783 11.2452 7.13556 11.3467 6.95402 11.3699C6.77247 11.3931 6.58989 11.3355 6.45446 11.2124L3.70446 8.71241C3.44905 8.48022 3.43023 8.08494 3.66242 7.82953C3.89461 7.57412 4.28989 7.55529 4.5453 7.78749L6.75292 9.79441L10.6018 3.90792C10.7907 3.61902 11.178 3.53795 11.4669 3.72684Z" />
|
||||
) : (
|
||||
<path d="M5 2V1H10V2H5ZM4.75 0C4.33579 0 4 0.335786 4 0.75V1H3.5C2.67157 1 2 1.67157 2 2.5V12.5C2 13.3284 2.67157 14 3.5 14H7V13H3.5C3.22386 13 3 12.7761 3 12.5V2.5C3 2.22386 3.22386 2 3.5 2H4V2.25C4 2.66421 4.33579 3 4.75 3H10.25C10.6642 3 11 2.66421 11 2.25V2H11.5C11.7761 2 12 2.22386 12 2.5V7H13V2.5C13 1.67157 12.3284 1 11.5 1H11V0.75C11 0.335786 10.6642 0 10.25 0H4.75ZM9 8.5C9 8.77614 8.77614 9 8.5 9C8.22386 9 8 8.77614 8 8.5C8 8.22386 8.22386 8 8.5 8C8.77614 8 9 8.22386 9 8.5ZM10.5 9C10.7761 9 11 8.77614 11 8.5C11 8.22386 10.7761 8 10.5 8C10.2239 8 10 8.22386 10 8.5C10 8.77614 10.2239 9 10.5 9ZM13 8.5C13 8.77614 12.7761 9 12.5 9C12.2239 9 12 8.77614 12 8.5C12 8.22386 12.2239 8 12.5 8C12.7761 8 13 8.22386 13 8.5ZM14.5 9C14.7761 9 15 8.77614 15 8.5C15 8.22386 14.7761 8 14.5 8C14.2239 8 14 8.22386 14 8.5C14 8.77614 14.2239 9 14.5 9ZM15 10.5C15 10.7761 14.7761 11 14.5 11C14.2239 11 14 10.7761 14 10.5C14 10.2239 14.2239 10 14.5 10C14.7761 10 15 10.2239 15 10.5ZM14.5 13C14.7761 13 15 12.7761 15 12.5C15 12.2239 14.7761 12 14.5 12C14.2239 12 14 12.2239 14 12.5C14 12.7761 14.2239 13 14.5 13ZM14.5 15C14.7761 15 15 14.7761 15 14.5C15 14.2239 14.7761 14 14.5 14C14.2239 14 14 14.2239 14 14.5C14 14.7761 14.2239 15 14.5 15ZM8.5 11C8.77614 11 9 10.7761 9 10.5C9 10.2239 8.77614 10 8.5 10C8.22386 10 8 10.2239 8 10.5C8 10.7761 8.22386 11 8.5 11ZM9 12.5C9 12.7761 8.77614 13 8.5 13C8.22386 13 8 12.7761 8 12.5C8 12.2239 8.22386 12 8.5 12C8.77614 12 9 12.2239 9 12.5ZM8.5 15C8.77614 15 9 14.7761 9 14.5C9 14.2239 8.77614 14 8.5 14C8.22386 14 8 14.2239 8 14.5C8 14.7761 8.22386 15 8.5 15ZM11 14.5C11 14.7761 10.7761 15 10.5 15C10.2239 15 10 14.7761 10 14.5C10 14.2239 10.2239 14 10.5 14C10.7761 14 11 14.2239 11 14.5ZM12.5 15C12.7761 15 13 14.7761 13 14.5C13 14.2239 12.7761 14 12.5 14C12.2239 14 12 14.2239 12 14.5C12 14.7761 12.2239 15 12.5 15Z" />
|
||||
)}
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
138
apps/dotcom/src/components/IFrameProtector.tsx
Normal file
138
apps/dotcom/src/components/IFrameProtector.tsx
Normal file
|
@ -0,0 +1,138 @@
|
|||
import { LoadingScreen } from '@tldraw/tldraw'
|
||||
import { useEffect, useState, version } from 'react'
|
||||
import { useUrl } from '../hooks/useUrl'
|
||||
import { trackAnalyticsEvent } from '../utils/trackAnalyticsEvent'
|
||||
|
||||
/*
|
||||
If we're in an iframe, we need to figure out whether we're on a whitelisted host (e.g. tldraw itself)
|
||||
or a not-allowed host (e.g. someone else's website). Some websites embed tldraw in iframes and this is kinda
|
||||
risky for us and for them, too—and hey, if we decide to offer a hosted thing, then that's another stor
|
||||
|
||||
Figuring this out is a little tricky because the same code here is going to run on:
|
||||
- the website as a top window (tldraw-top)
|
||||
- the website in an iframe (tldraw-iframe)
|
||||
|
||||
We set a listener on the current window (which may be top or not) to listen for a "are-we-cool" message,
|
||||
which responds "yes" with the current library version.
|
||||
|
||||
If we detect that we're in an iframe (i.e. that our window is not the top window) then we send this
|
||||
"are-we-cool" message to the parent window. If we get back the "yes" + version message, then that means
|
||||
the iframe is embedded inside of another tldraw window, and so we can show the contents of the iframe.
|
||||
|
||||
If we don't get a message back in time, then that means the iframe is embedded in a not-allowed website,
|
||||
and we should show an annoying messsage.
|
||||
|
||||
If we're not in an iframe, we don't need to do anything.
|
||||
*/
|
||||
|
||||
// Which routes do we allow to be embedded in tldraw.com itself?
|
||||
const WHITELIST_CONTEXT = ['public-multiplayer', 'public-readonly', 'public-snapshot']
|
||||
const EXPECTED_QUESTION = 'are we cool?'
|
||||
const EXPECTED_RESPONSE = 'yes' + version
|
||||
|
||||
const isInIframe = () => {
|
||||
return typeof window !== 'undefined' && (window !== window.top || window.self !== window.parent)
|
||||
}
|
||||
|
||||
export function IFrameProtector({
|
||||
slug,
|
||||
context,
|
||||
children,
|
||||
}: {
|
||||
slug: string
|
||||
context:
|
||||
| 'public-multiplayer'
|
||||
| 'public-readonly'
|
||||
| 'public-snapshot'
|
||||
| 'history-snapshot'
|
||||
| 'history'
|
||||
| 'local'
|
||||
children: any
|
||||
}) {
|
||||
const [embeddedState, setEmbeddedState] = useState<
|
||||
'iframe-unknown' | 'iframe-not-allowed' | 'not-iframe' | 'iframe-ok'
|
||||
>(isInIframe() ? 'iframe-unknown' : 'not-iframe')
|
||||
|
||||
const url = useUrl()
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return
|
||||
}
|
||||
|
||||
let timeout: any | undefined
|
||||
|
||||
function handleMessageEvent(event: MessageEvent) {
|
||||
if (!event.source) return
|
||||
|
||||
if (event.data === EXPECTED_QUESTION) {
|
||||
if (!isInIframe()) {
|
||||
// If _we're_ in an iframe, then we don't want to show a nested
|
||||
// iframe, even if we're on a whitelisted host / context
|
||||
event.source.postMessage(EXPECTED_RESPONSE)
|
||||
}
|
||||
}
|
||||
|
||||
if (event.data === EXPECTED_RESPONSE) {
|
||||
// todo: check the origin?
|
||||
setEmbeddedState('iframe-ok')
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('message', handleMessageEvent, false)
|
||||
|
||||
if (embeddedState === 'iframe-unknown') {
|
||||
// We iframe embeddings on multiplayer or readonly
|
||||
if (WHITELIST_CONTEXT.includes(context)) {
|
||||
window.parent.postMessage(EXPECTED_QUESTION, '*') // todo: send to a specific origin?
|
||||
timeout = setTimeout(() => {
|
||||
setEmbeddedState('iframe-not-allowed')
|
||||
trackAnalyticsEvent('connect_to_room_in_iframe', { slug, context })
|
||||
}, 1000)
|
||||
} else {
|
||||
// We don't allow iframe embeddings on other routes
|
||||
setEmbeddedState('iframe-not-allowed')
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout)
|
||||
window.removeEventListener('message', handleMessageEvent)
|
||||
}
|
||||
}, [embeddedState, slug, context])
|
||||
|
||||
if (embeddedState === 'iframe-unknown') {
|
||||
// We're in an iframe, but we don't know if it's a tldraw iframe
|
||||
return <LoadingScreen>Loading in an iframe...</LoadingScreen>
|
||||
}
|
||||
|
||||
if (embeddedState === 'iframe-not-allowed') {
|
||||
// We're in an iframe and its not one of ours
|
||||
return (
|
||||
<div className="tldraw__editor tl-container">
|
||||
<div className="iframe-warning__container">
|
||||
<a className="iframe-warning__link" href={url} target="_parent">
|
||||
{'Visit this page on tldraw.com '}
|
||||
<svg
|
||||
width="15"
|
||||
height="15"
|
||||
viewBox="0 0 15 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M3 2C2.44772 2 2 2.44772 2 3V12C2 12.5523 2.44772 13 3 13H12C12.5523 13 13 12.5523 13 12V8.5C13 8.22386 12.7761 8 12.5 8C12.2239 8 12 8.22386 12 8.5V12H3V3L6.5 3C6.77614 3 7 2.77614 7 2.5C7 2.22386 6.77614 2 6.5 2H3ZM12.8536 2.14645C12.9015 2.19439 12.9377 2.24964 12.9621 2.30861C12.9861 2.36669 12.9996 2.4303 13 2.497L13 2.5V2.50049V5.5C13 5.77614 12.7761 6 12.5 6C12.2239 6 12 5.77614 12 5.5V3.70711L6.85355 8.85355C6.65829 9.04882 6.34171 9.04882 6.14645 8.85355C5.95118 8.65829 5.95118 8.34171 6.14645 8.14645L11.2929 3H9.5C9.22386 3 9 2.77614 9 2.5C9 2.22386 9.22386 2 9.5 2H12.4999H12.5C12.5678 2 12.6324 2.01349 12.6914 2.03794C12.7504 2.06234 12.8056 2.09851 12.8536 2.14645Z"
|
||||
fill="currentColor"
|
||||
stroke="black"
|
||||
strokeWidth=".5"
|
||||
></path>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return children
|
||||
}
|
|
@ -32,13 +32,11 @@ import { CursorChatMenuItem } from '../utils/context-menu/CursorChatMenuItem'
|
|||
import { createAssetFromFile } from '../utils/createAssetFromFile'
|
||||
import { createAssetFromUrl } from '../utils/createAssetFromUrl'
|
||||
import { useSharing } from '../utils/sharing'
|
||||
import { trackAnalyticsEvent } from '../utils/trackAnalyticsEvent'
|
||||
import { CURSOR_CHAT_ACTION, useCursorChat } from '../utils/useCursorChat'
|
||||
import { OPEN_FILE_ACTION, SAVE_FILE_COPY_ACTION, useFileSystem } from '../utils/useFileSystem'
|
||||
import { useHandleUiEvents } from '../utils/useHandleUiEvent'
|
||||
import { CursorChatBubble } from './CursorChatBubble'
|
||||
import { DocumentTopZone } from './DocumentName/DocumentName'
|
||||
import { EmbeddedInIFrameWarning } from './EmbeddedInIFrameWarning'
|
||||
import { MultiplayerFileMenu } from './FileMenu'
|
||||
import { Links } from './Links'
|
||||
import { PeopleMenu } from './PeopleMenu/PeopleMenu'
|
||||
|
@ -138,7 +136,6 @@ export function MultiplayerEditor({
|
|||
shittyOfflineAtom.set(isOffline)
|
||||
}, [isOffline])
|
||||
|
||||
const isEmbedded = useIsEmbedded(roomSlug)
|
||||
const sharingUiOverrides = useSharing()
|
||||
const fileSystemUiOverrides = useFileSystem({ isMultiplayer: true })
|
||||
const cursorChatOverrides = useCursorChat()
|
||||
|
@ -156,10 +153,6 @@ export function MultiplayerEditor({
|
|||
return <StoreErrorScreen error={storeWithStatus.error} />
|
||||
}
|
||||
|
||||
if (isEmbedded) {
|
||||
return <EmbeddedInIFrameWarning />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="tldraw__editor">
|
||||
<Tldraw
|
||||
|
@ -195,20 +188,3 @@ export function UrlStateSync() {
|
|||
|
||||
return null
|
||||
}
|
||||
|
||||
function useIsEmbedded(slug: string) {
|
||||
const isEmbedded =
|
||||
typeof window !== 'undefined' &&
|
||||
window.self !== window.top &&
|
||||
window.location.host !== 'www.tldraw.com'
|
||||
|
||||
useEffect(() => {
|
||||
if (isEmbedded) {
|
||||
trackAnalyticsEvent('connect_to_room_in_iframe', {
|
||||
roomId: slug,
|
||||
})
|
||||
}
|
||||
}, [slug, isEmbedded])
|
||||
|
||||
return isEmbedded
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { RoomSnapshot } from '@tldraw/tlsync'
|
||||
import '../../styles/globals.css'
|
||||
import { BoardHistorySnapshot } from '../components/BoardHistorySnapshot/BoardHistorySnapshot'
|
||||
import { ErrorPage } from '../components/ErrorPage/ErrorPage'
|
||||
import { IFrameProtector } from '../components/IFrameProtector'
|
||||
import { defineLoader } from '../utils/defineLoader'
|
||||
|
||||
const { loader, useData } = defineLoader(async (args) => {
|
||||
|
@ -22,8 +24,22 @@ export { loader }
|
|||
|
||||
export function Component() {
|
||||
const result = useData()
|
||||
if (!result || !result.timestamp) return <div>Not found</div>
|
||||
if (!result || !result.timestamp)
|
||||
return (
|
||||
<ErrorPage
|
||||
icon
|
||||
messages={{
|
||||
header: 'Page not found',
|
||||
para1: 'The page you are looking does not exist or has been moved.',
|
||||
}}
|
||||
redirectTo="/"
|
||||
/>
|
||||
)
|
||||
|
||||
const { data, roomId, timestamp } = result
|
||||
return <BoardHistorySnapshot data={data} roomId={roomId} timestamp={timestamp} />
|
||||
return (
|
||||
<IFrameProtector slug={roomId} context="history-snapshot">
|
||||
<BoardHistorySnapshot data={data} roomId={roomId} timestamp={timestamp} />
|
||||
</IFrameProtector>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import { BoardHistoryLog } from '../components/BoardHistoryLog/BoardHistoryLog'
|
||||
import { ErrorPage } from '../components/ErrorPage/ErrorPage'
|
||||
import { IFrameProtector } from '../components/IFrameProtector'
|
||||
import { defineLoader } from '../utils/defineLoader'
|
||||
|
||||
const { loader, useData } = defineLoader(async (args) => {
|
||||
|
@ -12,13 +14,27 @@ const { loader, useData } = defineLoader(async (args) => {
|
|||
if (!result.ok) return null
|
||||
const data = await result.json()
|
||||
|
||||
return data as string[]
|
||||
return { data, boardId } as { data: string[]; boardId: string }
|
||||
})
|
||||
|
||||
export { loader }
|
||||
|
||||
export function Component() {
|
||||
const data = useData()
|
||||
if (!data) throw Error('Project not found')
|
||||
return <BoardHistoryLog data={data} />
|
||||
if (!data)
|
||||
return (
|
||||
<ErrorPage
|
||||
icon
|
||||
messages={{
|
||||
header: 'Page not found',
|
||||
para1: 'The page you are looking does not exist or has been moved.',
|
||||
}}
|
||||
redirectTo="/"
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<IFrameProtector slug={data.boardId} context="history">
|
||||
<BoardHistoryLog data={data.data} />
|
||||
</IFrameProtector>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
import { useParams } from 'react-router-dom'
|
||||
import '../../styles/globals.css'
|
||||
import { IFrameProtector } from '../components/IFrameProtector'
|
||||
import { MultiplayerEditor } from '../components/MultiplayerEditor'
|
||||
|
||||
export function Component() {
|
||||
const id = useParams()['roomId'] as string
|
||||
return <MultiplayerEditor isReadOnly={false} roomSlug={id} />
|
||||
return (
|
||||
<IFrameProtector slug={id} context="public-multiplayer">
|
||||
<MultiplayerEditor isReadOnly={false} roomSlug={id} />
|
||||
</IFrameProtector>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
import { useParams } from 'react-router-dom'
|
||||
import '../../styles/globals.css'
|
||||
import { IFrameProtector } from '../components/IFrameProtector'
|
||||
import { MultiplayerEditor } from '../components/MultiplayerEditor'
|
||||
|
||||
export function Component() {
|
||||
const id = useParams()['roomId'] as string
|
||||
return <MultiplayerEditor isReadOnly={true} roomSlug={id} />
|
||||
return (
|
||||
<IFrameProtector slug={id} context="public-readonly">
|
||||
<MultiplayerEditor isReadOnly={true} roomSlug={id} />
|
||||
</IFrameProtector>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { SerializedSchema, TLRecord } from '@tldraw/tldraw'
|
||||
import '../../styles/globals.css'
|
||||
import { IFrameProtector } from '../components/IFrameProtector'
|
||||
import { SnapshotsEditor } from '../components/SnapshotsEditor'
|
||||
import { defineLoader } from '../utils/defineLoader'
|
||||
|
||||
|
@ -8,6 +9,7 @@ const { loader, useData } = defineLoader(async (args) => {
|
|||
const result = await fetch(`/api/snapshot/${roomId}`)
|
||||
return result.ok
|
||||
? ((await result.json()) as {
|
||||
roomId: string
|
||||
schema: SerializedSchema
|
||||
records: TLRecord[]
|
||||
})
|
||||
|
@ -17,7 +19,12 @@ const { loader, useData } = defineLoader(async (args) => {
|
|||
export { loader }
|
||||
|
||||
export function Component() {
|
||||
const roomData = useData()
|
||||
if (!roomData) throw Error('Room not found')
|
||||
return <SnapshotsEditor records={roomData.records} schema={roomData.schema} />
|
||||
const result = useData()
|
||||
if (!result) throw Error('Room not found')
|
||||
const { roomId, records, schema } = result
|
||||
return (
|
||||
<IFrameProtector slug={roomId} context="public-snapshot">
|
||||
<SnapshotsEditor records={records} schema={schema} />
|
||||
</IFrameProtector>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import '../../styles/globals.css'
|
||||
import { IFrameProtector } from '../components/IFrameProtector'
|
||||
import { LocalEditor } from '../components/LocalEditor'
|
||||
|
||||
export function Component() {
|
||||
return <LocalEditor />
|
||||
return (
|
||||
<IFrameProtector slug="home" context="local">
|
||||
<LocalEditor />
|
||||
</IFrameProtector>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,8 +1,17 @@
|
|||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@500;600;800&display=swap');
|
||||
|
||||
:root {
|
||||
font-family: Inter, -apple-system, 'system-ui', 'Segoe UI', 'Noto Sans', Helvetica, Arial,
|
||||
sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji';
|
||||
font-family:
|
||||
Inter,
|
||||
-apple-system,
|
||||
'system-ui',
|
||||
'Segoe UI',
|
||||
'Noto Sans',
|
||||
Helvetica,
|
||||
Arial,
|
||||
sans-serif,
|
||||
'Apple Color Emoji',
|
||||
'Segoe UI Emoji';
|
||||
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
|
@ -198,3 +207,28 @@ a {
|
|||
top: 8px;
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
/* ----------------- Iframe warning ----------------- */
|
||||
|
||||
.iframe-warning__container {
|
||||
position: absolute;
|
||||
inset: 0px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.iframe-warning__link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 32px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.iframe-warning__link svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
|
4
apps/dotcom/version.ts
Normal file
4
apps/dotcom/version.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
// This file is automatically generated by scripts/refresh-assets.ts.
|
||||
// Do not edit manually. Or do, I'm a comment, not a cop.
|
||||
|
||||
export const version = '2.0.0-beta.4'
|
|
@ -1,5 +1,5 @@
|
|||
// This file is automatically generated by scripts/refresh-assets.ts.
|
||||
// Do not edit manually.
|
||||
// Do not edit manually. Or do, I'm a comment, not a cop.
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
|
||||
/// <reference path="./modules.d.ts" />
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// This file is automatically generated by scripts/refresh-assets.ts.
|
||||
// Do not edit manually.
|
||||
// Do not edit manually. Or do, I'm a comment, not a cop.
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
|
||||
/// <reference path="./modules.d.ts" />
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// This file is automatically generated by scripts/refresh-assets.ts.
|
||||
// Do not edit manually.
|
||||
// Do not edit manually. Or do, I'm a comment, not a cop.
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
|
||||
/// <reference path="./modules.d.ts" />
|
||||
|
|
2
packages/assets/types.d.ts
vendored
2
packages/assets/types.d.ts
vendored
|
@ -1,5 +1,5 @@
|
|||
// This file is automatically generated by scripts/refresh-assets.ts.
|
||||
// Do not edit manually.
|
||||
// Do not edit manually. Or do, I'm a comment, not a cop.
|
||||
|
||||
export type AssetUrl = string | { src: string }
|
||||
export type AssetUrlOptions = { baseUrl?: string } | ((assetUrl: string) => string)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// This file is automatically generated by scripts/refresh-assets.ts.
|
||||
// Do not edit manually.
|
||||
// Do not edit manually. Or do, I'm a comment, not a cop.
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
|
||||
/// <reference path="./modules.d.ts" />
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { LoadingScreen } from '../../TldrawEditor'
|
||||
import { useEditorComponents } from '../../hooks/useEditorComponents'
|
||||
|
||||
/** @public */
|
||||
export const DefaultLoadingScreen = () => {
|
||||
return <LoadingScreen>Connecting...</LoadingScreen>
|
||||
const { Spinner } = useEditorComponents()
|
||||
return <LoadingScreen>{Spinner ? <Spinner /> : null}</LoadingScreen>
|
||||
}
|
||||
|
|
|
@ -1 +1,4 @@
|
|||
export const version = '2.0.0-beta.2'
|
||||
// This file is automatically generated by scripts/refresh-assets.ts.
|
||||
// Do not edit manually. Or do, I'm a comment, not a cop.
|
||||
|
||||
export const version = '2.0.0-beta.4'
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// This file is automatically generated by scripts/refresh-assets.ts.
|
||||
// Do not edit manually.
|
||||
// Do not edit manually. Or do, I'm a comment, not a cop.
|
||||
|
||||
/** @public */
|
||||
export type TLUiTranslationKey =
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// This file is automatically generated by scripts/refresh-assets.ts.
|
||||
// Do not edit manually.
|
||||
// Do not edit manually. Or do, I'm a comment, not a cop.
|
||||
|
||||
/** @internal */
|
||||
export const DEFAULT_TRANSLATION = {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// This file is automatically generated by scripts/refresh-assets.ts.
|
||||
// Do not edit manually.
|
||||
// Do not edit manually. Or do, I'm a comment, not a cop.
|
||||
|
||||
/** @public */
|
||||
export type TLUiIconType =
|
||||
|
|
|
@ -1 +1,4 @@
|
|||
// This file is automatically generated by scripts/refresh-assets.ts.
|
||||
// Do not edit manually. Or do, I'm a comment, not a cop.
|
||||
|
||||
export const version = '2.0.0-beta.4'
|
||||
|
|
|
@ -212,13 +212,16 @@ export const drawShapeProps: {
|
|||
export const EMBED_DEFINITIONS: readonly [{
|
||||
readonly type: "tldraw";
|
||||
readonly title: "tldraw";
|
||||
readonly hostnames: readonly ["beta.tldraw.com", "tldraw.com"];
|
||||
readonly hostnames: readonly ["beta.tldraw.com", "tldraw.com", "localhost:3000"];
|
||||
readonly minWidth: 300;
|
||||
readonly minHeight: 300;
|
||||
readonly width: 720;
|
||||
readonly height: 500;
|
||||
readonly doesResize: true;
|
||||
readonly canUnmount: true;
|
||||
readonly overridePermissions: {
|
||||
readonly 'allow-top-navigation': true;
|
||||
};
|
||||
readonly toEmbedUrl: (url: string) => string | undefined;
|
||||
readonly fromEmbedUrl: (url: string) => string | undefined;
|
||||
}, {
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -18,13 +18,16 @@ export const EMBED_DEFINITIONS = [
|
|||
{
|
||||
type: 'tldraw',
|
||||
title: 'tldraw',
|
||||
hostnames: ['beta.tldraw.com', 'tldraw.com'],
|
||||
hostnames: ['beta.tldraw.com', 'tldraw.com', 'localhost:3000'],
|
||||
minWidth: 300,
|
||||
minHeight: 300,
|
||||
width: 720,
|
||||
height: 500,
|
||||
doesResize: true,
|
||||
canUnmount: true,
|
||||
overridePermissions: {
|
||||
'allow-top-navigation': true,
|
||||
},
|
||||
toEmbedUrl: (url) => {
|
||||
const urlObj = safeParseUrl(url)
|
||||
if (urlObj && urlObj.pathname.match(TLDRAW_APP_RE)) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// This file is automatically generated by scripts/refresh-assets.ts.
|
||||
// Do not edit manually.
|
||||
// Do not edit manually. Or do, I'm a comment, not a cop.
|
||||
|
||||
/** @public */
|
||||
export const LANGUAGES = [
|
||||
|
|
|
@ -45,7 +45,7 @@ export async function writeCodeFile(
|
|||
const formattedCode = await prettier.format(
|
||||
`
|
||||
// This file is automatically generated by ${generator}.
|
||||
// Do not edit manually.
|
||||
// Do not edit manually. Or do, I'm a comment, not a cop.
|
||||
|
||||
${code}
|
||||
`,
|
||||
|
|
|
@ -401,6 +401,31 @@ async function writeAssetDeclarationDTSFile() {
|
|||
await writeCodeFile('scripts/refresh-assets.ts', 'typescript', assetDeclarationFilePath, dts)
|
||||
}
|
||||
|
||||
async function copyVersionToDotCom() {
|
||||
const packageVersion = await import(join(REPO_ROOT, 'packages', 'tldraw', 'package.json')).then(
|
||||
(pkg) => pkg.version
|
||||
)
|
||||
const file = `export const version = '${packageVersion}'`
|
||||
await writeCodeFile(
|
||||
'scripts/refresh-assets.ts',
|
||||
'typescript',
|
||||
join(REPO_ROOT, 'apps', 'dotcom', 'version.ts'),
|
||||
file
|
||||
)
|
||||
await writeCodeFile(
|
||||
'scripts/refresh-assets.ts',
|
||||
'typescript',
|
||||
join(REPO_ROOT, 'packages', 'editor', 'src', 'version.ts'),
|
||||
file
|
||||
)
|
||||
await writeCodeFile(
|
||||
'scripts/refresh-assets.ts',
|
||||
'typescript',
|
||||
join(REPO_ROOT, 'packages', 'tldraw', 'src', 'lib', 'ui', 'version.ts'),
|
||||
file
|
||||
)
|
||||
}
|
||||
|
||||
// --- RUN
|
||||
async function main() {
|
||||
nicelog('Copying icons...')
|
||||
|
@ -417,6 +442,7 @@ async function main() {
|
|||
await writeImportBasedAssetDeclarationFile('', 'imports.js')
|
||||
await writeImportBasedAssetDeclarationFile('?url', 'imports.vite.js')
|
||||
await writeSelfHostedAssetDeclarationFile()
|
||||
await copyVersionToDotCom()
|
||||
nicelog('Done!')
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue