diff --git a/apps/dotcom-worker/src/lib/routes/getRoomSnapshot.ts b/apps/dotcom-worker/src/lib/routes/getRoomSnapshot.ts index 1ec5b2c42..fc109e47b 100644 --- a/apps/dotcom-worker/src/lib/routes/getRoomSnapshot.ts +++ b/apps/dotcom-worker/src/lib/routes/getRoomSnapshot.ts @@ -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, diff --git a/apps/dotcom/src/components/EmbeddedInIFrameWarning.tsx b/apps/dotcom/src/components/EmbeddedInIFrameWarning.tsx deleted file mode 100644 index a194a3b06..000000000 --- a/apps/dotcom/src/components/EmbeddedInIFrameWarning.tsx +++ /dev/null @@ -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(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 ( -
-
- - {'Visit this page on tldraw.com '} - - - - - -
-
- ) -} diff --git a/apps/dotcom/src/components/IFrameProtector.tsx b/apps/dotcom/src/components/IFrameProtector.tsx new file mode 100644 index 000000000..a2ec93d23 --- /dev/null +++ b/apps/dotcom/src/components/IFrameProtector.tsx @@ -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 Loading in an iframe... + } + + if (embeddedState === 'iframe-not-allowed') { + // We're in an iframe and its not one of ours + return ( +
+
+ + {'Visit this page on tldraw.com '} + + + + +
+
+ ) + } + + return children +} diff --git a/apps/dotcom/src/components/MultiplayerEditor.tsx b/apps/dotcom/src/components/MultiplayerEditor.tsx index 078e206e5..8df196161 100644 --- a/apps/dotcom/src/components/MultiplayerEditor.tsx +++ b/apps/dotcom/src/components/MultiplayerEditor.tsx @@ -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 } - if (isEmbedded) { - return - } - return (
{ - if (isEmbedded) { - trackAnalyticsEvent('connect_to_room_in_iframe', { - roomId: slug, - }) - } - }, [slug, isEmbedded]) - - return isEmbedded -} diff --git a/apps/dotcom/src/pages/history-snapshot.tsx b/apps/dotcom/src/pages/history-snapshot.tsx index e810699f0..b9e16c432 100644 --- a/apps/dotcom/src/pages/history-snapshot.tsx +++ b/apps/dotcom/src/pages/history-snapshot.tsx @@ -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
Not found
+ if (!result || !result.timestamp) + return ( + + ) const { data, roomId, timestamp } = result - return + return ( + + + + ) } diff --git a/apps/dotcom/src/pages/history.tsx b/apps/dotcom/src/pages/history.tsx index 87a1c4f26..e2921775a 100644 --- a/apps/dotcom/src/pages/history.tsx +++ b/apps/dotcom/src/pages/history.tsx @@ -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 + if (!data) + return ( + + ) + return ( + + + + ) } diff --git a/apps/dotcom/src/pages/public-multiplayer.tsx b/apps/dotcom/src/pages/public-multiplayer.tsx index 2d8340c41..ffb5dc11e 100644 --- a/apps/dotcom/src/pages/public-multiplayer.tsx +++ b/apps/dotcom/src/pages/public-multiplayer.tsx @@ -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 + return ( + + + + ) } diff --git a/apps/dotcom/src/pages/public-readonly.tsx b/apps/dotcom/src/pages/public-readonly.tsx index f49664b6a..dba771e1e 100644 --- a/apps/dotcom/src/pages/public-readonly.tsx +++ b/apps/dotcom/src/pages/public-readonly.tsx @@ -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 + return ( + + + + ) } diff --git a/apps/dotcom/src/pages/public-snapshot.tsx b/apps/dotcom/src/pages/public-snapshot.tsx index fb07c9738..e2a2b2c29 100644 --- a/apps/dotcom/src/pages/public-snapshot.tsx +++ b/apps/dotcom/src/pages/public-snapshot.tsx @@ -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 + const result = useData() + if (!result) throw Error('Room not found') + const { roomId, records, schema } = result + return ( + + + + ) } diff --git a/apps/dotcom/src/pages/root.tsx b/apps/dotcom/src/pages/root.tsx index a2cbada1f..ddb48b87e 100644 --- a/apps/dotcom/src/pages/root.tsx +++ b/apps/dotcom/src/pages/root.tsx @@ -1,6 +1,11 @@ import '../../styles/globals.css' +import { IFrameProtector } from '../components/IFrameProtector' import { LocalEditor } from '../components/LocalEditor' export function Component() { - return + return ( + + + + ) } diff --git a/apps/dotcom/styles/core.css b/apps/dotcom/styles/core.css index d1614ccbb..9650d147e 100644 --- a/apps/dotcom/styles/core.css +++ b/apps/dotcom/styles/core.css @@ -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; +} diff --git a/apps/dotcom/version.ts b/apps/dotcom/version.ts new file mode 100644 index 000000000..716d8609d --- /dev/null +++ b/apps/dotcom/version.ts @@ -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' diff --git a/packages/assets/imports.js b/packages/assets/imports.js index 6068d5b26..49a615e17 100644 --- a/packages/assets/imports.js +++ b/packages/assets/imports.js @@ -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 /// diff --git a/packages/assets/imports.vite.js b/packages/assets/imports.vite.js index 61537408e..880a44d3a 100644 --- a/packages/assets/imports.vite.js +++ b/packages/assets/imports.vite.js @@ -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 /// diff --git a/packages/assets/selfHosted.js b/packages/assets/selfHosted.js index f1baf102d..e303193d7 100644 --- a/packages/assets/selfHosted.js +++ b/packages/assets/selfHosted.js @@ -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 /// diff --git a/packages/assets/types.d.ts b/packages/assets/types.d.ts index 092dd5c34..6b4a882f7 100644 --- a/packages/assets/types.d.ts +++ b/packages/assets/types.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. export type AssetUrl = string | { src: string } export type AssetUrlOptions = { baseUrl?: string } | ((assetUrl: string) => string) diff --git a/packages/assets/urls.js b/packages/assets/urls.js index e47e1ae0a..8545423f1 100644 --- a/packages/assets/urls.js +++ b/packages/assets/urls.js @@ -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 /// diff --git a/packages/editor/src/lib/components/default-components/DefaultLoadingScreen.tsx b/packages/editor/src/lib/components/default-components/DefaultLoadingScreen.tsx index 6e4ffb8ef..3802a361d 100644 --- a/packages/editor/src/lib/components/default-components/DefaultLoadingScreen.tsx +++ b/packages/editor/src/lib/components/default-components/DefaultLoadingScreen.tsx @@ -1,6 +1,8 @@ import { LoadingScreen } from '../../TldrawEditor' +import { useEditorComponents } from '../../hooks/useEditorComponents' /** @public */ export const DefaultLoadingScreen = () => { - return Connecting... + const { Spinner } = useEditorComponents() + return {Spinner ? : null} } diff --git a/packages/editor/src/version.ts b/packages/editor/src/version.ts index 5397de25d..716d8609d 100644 --- a/packages/editor/src/version.ts +++ b/packages/editor/src/version.ts @@ -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' diff --git a/packages/tldraw/src/lib/ui/hooks/useTranslation/TLUiTranslationKey.ts b/packages/tldraw/src/lib/ui/hooks/useTranslation/TLUiTranslationKey.ts index 9eafa0a00..9d67ac915 100644 --- a/packages/tldraw/src/lib/ui/hooks/useTranslation/TLUiTranslationKey.ts +++ b/packages/tldraw/src/lib/ui/hooks/useTranslation/TLUiTranslationKey.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. /** @public */ export type TLUiTranslationKey = diff --git a/packages/tldraw/src/lib/ui/hooks/useTranslation/defaultTranslation.ts b/packages/tldraw/src/lib/ui/hooks/useTranslation/defaultTranslation.ts index 99cc449b5..4edd36e79 100644 --- a/packages/tldraw/src/lib/ui/hooks/useTranslation/defaultTranslation.ts +++ b/packages/tldraw/src/lib/ui/hooks/useTranslation/defaultTranslation.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. /** @internal */ export const DEFAULT_TRANSLATION = { diff --git a/packages/tldraw/src/lib/ui/icon-types.ts b/packages/tldraw/src/lib/ui/icon-types.ts index 7e400cd77..4eb1ac072 100644 --- a/packages/tldraw/src/lib/ui/icon-types.ts +++ b/packages/tldraw/src/lib/ui/icon-types.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. /** @public */ export type TLUiIconType = diff --git a/packages/tldraw/src/lib/ui/version.ts b/packages/tldraw/src/lib/ui/version.ts index 08e287ecf..716d8609d 100644 --- a/packages/tldraw/src/lib/ui/version.ts +++ b/packages/tldraw/src/lib/ui/version.ts @@ -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' diff --git a/packages/tlschema/api-report.md b/packages/tlschema/api-report.md index c15480d8d..ae6241346 100644 --- a/packages/tlschema/api-report.md +++ b/packages/tlschema/api-report.md @@ -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; }, { diff --git a/packages/tlschema/api/api.json b/packages/tlschema/api/api.json index 24eb2c131..54048629c 100644 --- a/packages/tlschema/api/api.json +++ b/packages/tlschema/api/api.json @@ -1801,7 +1801,7 @@ }, { "kind": "Content", - "text": "readonly [{\n readonly type: \"tldraw\";\n readonly title: \"tldraw\";\n readonly hostnames: readonly [\"beta.tldraw.com\", \"tldraw.com\"];\n readonly minWidth: 300;\n readonly minHeight: 300;\n readonly width: 720;\n readonly height: 500;\n readonly doesResize: true;\n readonly canUnmount: true;\n readonly toEmbedUrl: (url: string) => string | undefined;\n readonly fromEmbedUrl: (url: string) => string | undefined;\n}, {\n readonly type: \"figma\";\n readonly title: \"Figma\";\n readonly hostnames: readonly [\"figma.com\"];\n readonly width: 720;\n readonly height: 500;\n readonly doesResize: true;\n readonly canUnmount: true;\n readonly toEmbedUrl: (url: string) => string | undefined;\n readonly fromEmbedUrl: (url: string) => string | undefined;\n}, {\n readonly type: \"google_maps\";\n readonly title: \"Google Maps\";\n readonly hostnames: readonly [\"google.*\"];\n readonly width: 720;\n readonly height: 500;\n readonly doesResize: true;\n readonly canUnmount: false;\n readonly toEmbedUrl: (url: string) => string | undefined;\n readonly fromEmbedUrl: (url: string) => string | undefined;\n}, {\n readonly type: \"val_town\";\n readonly title: \"Val Town\";\n readonly hostnames: readonly [\"val.town\"];\n readonly minWidth: 260;\n readonly minHeight: 100;\n readonly width: 720;\n readonly height: 500;\n readonly doesResize: true;\n readonly canUnmount: false;\n readonly toEmbedUrl: (url: string) => string | undefined;\n readonly fromEmbedUrl: (url: string) => string | undefined;\n}, {\n readonly type: \"codesandbox\";\n readonly title: \"CodeSandbox\";\n readonly hostnames: readonly [\"codesandbox.io\"];\n readonly minWidth: 300;\n readonly minHeight: 300;\n readonly width: 720;\n readonly height: 500;\n readonly doesResize: true;\n readonly canUnmount: false;\n readonly toEmbedUrl: (url: string) => string | undefined;\n readonly fromEmbedUrl: (url: string) => string | undefined;\n}, {\n readonly type: \"codepen\";\n readonly title: \"Codepen\";\n readonly hostnames: readonly [\"codepen.io\"];\n readonly minWidth: 300;\n readonly minHeight: 300;\n readonly width: 520;\n readonly height: 400;\n readonly doesResize: true;\n readonly canUnmount: false;\n readonly toEmbedUrl: (url: string) => string | undefined;\n readonly fromEmbedUrl: (url: string) => string | undefined;\n}, {\n readonly type: \"scratch\";\n readonly title: \"Scratch\";\n readonly hostnames: readonly [\"scratch.mit.edu\"];\n readonly width: 520;\n readonly height: 400;\n readonly doesResize: false;\n readonly canUnmount: false;\n readonly toEmbedUrl: (url: string) => string | undefined;\n readonly fromEmbedUrl: (url: string) => string | undefined;\n}, {\n readonly type: \"youtube\";\n readonly title: \"YouTube\";\n readonly hostnames: readonly [\"*.youtube.com\", \"youtube.com\", \"youtu.be\"];\n readonly width: 800;\n readonly height: 450;\n readonly doesResize: true;\n readonly canUnmount: false;\n readonly overridePermissions: {\n readonly 'allow-presentation': true;\n };\n readonly isAspectRatioLocked: true;\n readonly toEmbedUrl: (url: string) => string | undefined;\n readonly fromEmbedUrl: (url: string) => string | undefined;\n}, {\n readonly type: \"google_calendar\";\n readonly title: \"Google Calendar\";\n readonly hostnames: readonly [\"calendar.google.*\"];\n readonly width: 720;\n readonly height: 500;\n readonly minWidth: 460;\n readonly minHeight: 360;\n readonly doesResize: true;\n readonly canUnmount: false;\n readonly instructionLink: \"https://support.google.com/calendar/answer/41207?hl=en\";\n readonly toEmbedUrl: (url: string) => string | undefined;\n readonly fromEmbedUrl: (url: string) => string | undefined;\n}, {\n readonly type: \"google_slides\";\n readonly title: \"Google Slides\";\n readonly hostnames: readonly [\"docs.google.*\"];\n readonly width: 720;\n readonly height: 500;\n readonly minWidth: 460;\n readonly minHeight: 360;\n readonly doesResize: true;\n readonly canUnmount: false;\n readonly toEmbedUrl: (url: string) => string | undefined;\n readonly fromEmbedUrl: (url: string) => string | undefined;\n}, {\n readonly type: \"github_gist\";\n readonly title: \"GitHub Gist\";\n readonly hostnames: readonly [\"gist.github.com\"];\n readonly width: 720;\n readonly height: 500;\n readonly doesResize: true;\n readonly canUnmount: true;\n readonly toEmbedUrl: (url: string) => string | undefined;\n readonly fromEmbedUrl: (url: string) => string | undefined;\n}, {\n readonly type: \"replit\";\n readonly title: \"Replit\";\n readonly hostnames: readonly [\"replit.com\"];\n readonly width: 720;\n readonly height: 500;\n readonly doesResize: true;\n readonly canUnmount: false;\n readonly toEmbedUrl: (url: string) => string | undefined;\n readonly fromEmbedUrl: (url: string) => string | undefined;\n}, {\n readonly type: \"felt\";\n readonly title: \"Felt\";\n readonly hostnames: readonly [\"felt.com\"];\n readonly width: 720;\n readonly height: 500;\n readonly doesResize: true;\n readonly canUnmount: false;\n readonly toEmbedUrl: (url: string) => string | undefined;\n readonly fromEmbedUrl: (url: string) => string | undefined;\n}, {\n readonly type: \"spotify\";\n readonly title: \"Spotify\";\n readonly hostnames: readonly [\"open.spotify.com\"];\n readonly width: 720;\n readonly height: 500;\n readonly minHeight: 500;\n readonly overrideOutlineRadius: 12;\n readonly doesResize: true;\n readonly canUnmount: false;\n readonly toEmbedUrl: (url: string) => string | undefined;\n readonly fromEmbedUrl: (url: string) => string | undefined;\n}, {\n readonly type: \"vimeo\";\n readonly title: \"Vimeo\";\n readonly hostnames: readonly [\"vimeo.com\", \"player.vimeo.com\"];\n readonly width: 640;\n readonly height: 360;\n readonly doesResize: true;\n readonly canUnmount: false;\n readonly isAspectRatioLocked: true;\n readonly toEmbedUrl: (url: string) => string | undefined;\n readonly fromEmbedUrl: (url: string) => string | undefined;\n}, {\n readonly type: \"excalidraw\";\n readonly title: \"Excalidraw\";\n readonly hostnames: readonly [\"excalidraw.com\"];\n readonly width: 720;\n readonly height: 500;\n readonly doesResize: true;\n readonly canUnmount: false;\n readonly isAspectRatioLocked: true;\n readonly toEmbedUrl: (url: string) => string | undefined;\n readonly fromEmbedUrl: (url: string) => string | undefined;\n}, {\n readonly type: \"observable\";\n readonly title: \"Observable\";\n readonly hostnames: readonly [\"observablehq.com\"];\n readonly width: 720;\n readonly height: 500;\n readonly doesResize: true;\n readonly canUnmount: false;\n readonly isAspectRatioLocked: false;\n readonly backgroundColor: \"#fff\";\n readonly toEmbedUrl: (url: string) => string | undefined;\n readonly fromEmbedUrl: (url: string) => string | undefined;\n}]" + "text": "readonly [{\n readonly type: \"tldraw\";\n readonly title: \"tldraw\";\n readonly hostnames: readonly [\"beta.tldraw.com\", \"tldraw.com\", \"localhost:3000\"];\n readonly minWidth: 300;\n readonly minHeight: 300;\n readonly width: 720;\n readonly height: 500;\n readonly doesResize: true;\n readonly canUnmount: true;\n readonly overridePermissions: {\n readonly 'allow-top-navigation': true;\n };\n readonly toEmbedUrl: (url: string) => string | undefined;\n readonly fromEmbedUrl: (url: string) => string | undefined;\n}, {\n readonly type: \"figma\";\n readonly title: \"Figma\";\n readonly hostnames: readonly [\"figma.com\"];\n readonly width: 720;\n readonly height: 500;\n readonly doesResize: true;\n readonly canUnmount: true;\n readonly toEmbedUrl: (url: string) => string | undefined;\n readonly fromEmbedUrl: (url: string) => string | undefined;\n}, {\n readonly type: \"google_maps\";\n readonly title: \"Google Maps\";\n readonly hostnames: readonly [\"google.*\"];\n readonly width: 720;\n readonly height: 500;\n readonly doesResize: true;\n readonly canUnmount: false;\n readonly toEmbedUrl: (url: string) => string | undefined;\n readonly fromEmbedUrl: (url: string) => string | undefined;\n}, {\n readonly type: \"val_town\";\n readonly title: \"Val Town\";\n readonly hostnames: readonly [\"val.town\"];\n readonly minWidth: 260;\n readonly minHeight: 100;\n readonly width: 720;\n readonly height: 500;\n readonly doesResize: true;\n readonly canUnmount: false;\n readonly toEmbedUrl: (url: string) => string | undefined;\n readonly fromEmbedUrl: (url: string) => string | undefined;\n}, {\n readonly type: \"codesandbox\";\n readonly title: \"CodeSandbox\";\n readonly hostnames: readonly [\"codesandbox.io\"];\n readonly minWidth: 300;\n readonly minHeight: 300;\n readonly width: 720;\n readonly height: 500;\n readonly doesResize: true;\n readonly canUnmount: false;\n readonly toEmbedUrl: (url: string) => string | undefined;\n readonly fromEmbedUrl: (url: string) => string | undefined;\n}, {\n readonly type: \"codepen\";\n readonly title: \"Codepen\";\n readonly hostnames: readonly [\"codepen.io\"];\n readonly minWidth: 300;\n readonly minHeight: 300;\n readonly width: 520;\n readonly height: 400;\n readonly doesResize: true;\n readonly canUnmount: false;\n readonly toEmbedUrl: (url: string) => string | undefined;\n readonly fromEmbedUrl: (url: string) => string | undefined;\n}, {\n readonly type: \"scratch\";\n readonly title: \"Scratch\";\n readonly hostnames: readonly [\"scratch.mit.edu\"];\n readonly width: 520;\n readonly height: 400;\n readonly doesResize: false;\n readonly canUnmount: false;\n readonly toEmbedUrl: (url: string) => string | undefined;\n readonly fromEmbedUrl: (url: string) => string | undefined;\n}, {\n readonly type: \"youtube\";\n readonly title: \"YouTube\";\n readonly hostnames: readonly [\"*.youtube.com\", \"youtube.com\", \"youtu.be\"];\n readonly width: 800;\n readonly height: 450;\n readonly doesResize: true;\n readonly canUnmount: false;\n readonly overridePermissions: {\n readonly 'allow-presentation': true;\n };\n readonly isAspectRatioLocked: true;\n readonly toEmbedUrl: (url: string) => string | undefined;\n readonly fromEmbedUrl: (url: string) => string | undefined;\n}, {\n readonly type: \"google_calendar\";\n readonly title: \"Google Calendar\";\n readonly hostnames: readonly [\"calendar.google.*\"];\n readonly width: 720;\n readonly height: 500;\n readonly minWidth: 460;\n readonly minHeight: 360;\n readonly doesResize: true;\n readonly canUnmount: false;\n readonly instructionLink: \"https://support.google.com/calendar/answer/41207?hl=en\";\n readonly toEmbedUrl: (url: string) => string | undefined;\n readonly fromEmbedUrl: (url: string) => string | undefined;\n}, {\n readonly type: \"google_slides\";\n readonly title: \"Google Slides\";\n readonly hostnames: readonly [\"docs.google.*\"];\n readonly width: 720;\n readonly height: 500;\n readonly minWidth: 460;\n readonly minHeight: 360;\n readonly doesResize: true;\n readonly canUnmount: false;\n readonly toEmbedUrl: (url: string) => string | undefined;\n readonly fromEmbedUrl: (url: string) => string | undefined;\n}, {\n readonly type: \"github_gist\";\n readonly title: \"GitHub Gist\";\n readonly hostnames: readonly [\"gist.github.com\"];\n readonly width: 720;\n readonly height: 500;\n readonly doesResize: true;\n readonly canUnmount: true;\n readonly toEmbedUrl: (url: string) => string | undefined;\n readonly fromEmbedUrl: (url: string) => string | undefined;\n}, {\n readonly type: \"replit\";\n readonly title: \"Replit\";\n readonly hostnames: readonly [\"replit.com\"];\n readonly width: 720;\n readonly height: 500;\n readonly doesResize: true;\n readonly canUnmount: false;\n readonly toEmbedUrl: (url: string) => string | undefined;\n readonly fromEmbedUrl: (url: string) => string | undefined;\n}, {\n readonly type: \"felt\";\n readonly title: \"Felt\";\n readonly hostnames: readonly [\"felt.com\"];\n readonly width: 720;\n readonly height: 500;\n readonly doesResize: true;\n readonly canUnmount: false;\n readonly toEmbedUrl: (url: string) => string | undefined;\n readonly fromEmbedUrl: (url: string) => string | undefined;\n}, {\n readonly type: \"spotify\";\n readonly title: \"Spotify\";\n readonly hostnames: readonly [\"open.spotify.com\"];\n readonly width: 720;\n readonly height: 500;\n readonly minHeight: 500;\n readonly overrideOutlineRadius: 12;\n readonly doesResize: true;\n readonly canUnmount: false;\n readonly toEmbedUrl: (url: string) => string | undefined;\n readonly fromEmbedUrl: (url: string) => string | undefined;\n}, {\n readonly type: \"vimeo\";\n readonly title: \"Vimeo\";\n readonly hostnames: readonly [\"vimeo.com\", \"player.vimeo.com\"];\n readonly width: 640;\n readonly height: 360;\n readonly doesResize: true;\n readonly canUnmount: false;\n readonly isAspectRatioLocked: true;\n readonly toEmbedUrl: (url: string) => string | undefined;\n readonly fromEmbedUrl: (url: string) => string | undefined;\n}, {\n readonly type: \"excalidraw\";\n readonly title: \"Excalidraw\";\n readonly hostnames: readonly [\"excalidraw.com\"];\n readonly width: 720;\n readonly height: 500;\n readonly doesResize: true;\n readonly canUnmount: false;\n readonly isAspectRatioLocked: true;\n readonly toEmbedUrl: (url: string) => string | undefined;\n readonly fromEmbedUrl: (url: string) => string | undefined;\n}, {\n readonly type: \"observable\";\n readonly title: \"Observable\";\n readonly hostnames: readonly [\"observablehq.com\"];\n readonly width: 720;\n readonly height: 500;\n readonly doesResize: true;\n readonly canUnmount: false;\n readonly isAspectRatioLocked: false;\n readonly backgroundColor: \"#fff\";\n readonly toEmbedUrl: (url: string) => string | undefined;\n readonly fromEmbedUrl: (url: string) => string | undefined;\n}]" } ], "fileUrlPath": "packages/tlschema/src/shapes/TLEmbedShape.ts", diff --git a/packages/tlschema/src/shapes/TLEmbedShape.ts b/packages/tlschema/src/shapes/TLEmbedShape.ts index d91c74e1c..6037f2aad 100644 --- a/packages/tlschema/src/shapes/TLEmbedShape.ts +++ b/packages/tlschema/src/shapes/TLEmbedShape.ts @@ -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)) { diff --git a/packages/tlschema/src/translations/languages.ts b/packages/tlschema/src/translations/languages.ts index 36ecab23d..807bb4a66 100644 --- a/packages/tlschema/src/translations/languages.ts +++ b/packages/tlschema/src/translations/languages.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. /** @public */ export const LANGUAGES = [ diff --git a/scripts/lib/file.ts b/scripts/lib/file.ts index 468489ea2..75803bb89 100644 --- a/scripts/lib/file.ts +++ b/scripts/lib/file.ts @@ -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} `, diff --git a/scripts/refresh-assets.ts b/scripts/refresh-assets.ts index dfbe6da9a..81217565d 100644 --- a/scripts/refresh-assets.ts +++ b/scripts/refresh-assets.ts @@ -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!') }