diff --git a/packages/core/package.json b/packages/core/package.json index d943144ce..b56c55843 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -48,11 +48,11 @@ "react-dom": "^16.8 || ^17.0" }, "devDependencies": { - "@tldraw/intersect": "*", - "@tldraw/vec": "*", "@swc-node/jest": "^1.4.3", "@testing-library/jest-dom": "^5.16.2", "@testing-library/react": "^12.1.2", + "@tldraw/intersect": "*", + "@tldraw/vec": "*", "@types/node": "^17.0.14", "@types/react": "^17.0.38", "@typescript-eslint/eslint-plugin": "^5.10.2", @@ -97,4 +97,4 @@ } }, "gitHead": "4b1137849ad07da36fc8f0f19cb64e7535a79296" -} \ No newline at end of file +} diff --git a/packages/core/src/components/Canvas/Canvas.tsx b/packages/core/src/components/Canvas/Canvas.tsx index 5ba64afde..f7d0d8927 100644 --- a/packages/core/src/components/Canvas/Canvas.tsx +++ b/packages/core/src/components/Canvas/Canvas.tsx @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { observer } from 'mobx-react-lite' import * as React from 'react' +import { observer } from 'mobx-react-lite' import { usePreventNavigationCss, useZoomEvents, @@ -136,5 +136,3 @@ export const Canvas = observer(function _Canvas< ) }) - -const stopPropagation: React.ClipboardEventHandler = (e) => e.stopPropagation() diff --git a/packages/core/src/hooks/useSelection.tsx b/packages/core/src/hooks/useSelection.tsx index b30b6570c..020d5846c 100644 --- a/packages/core/src/hooks/useSelection.tsx +++ b/packages/core/src/hooks/useSelection.tsx @@ -29,14 +29,24 @@ export function useSelection( if (selectedIds.length === 1) { const id = selectedIds[0] const shape = page.shapes[id] + + if (!shape) { + throw Error(`selectedIds is set to the id of a shape that doesn't exist: ${id}`) + } + rotation = shape.rotation || 0 isLocked = shape.isLocked || false + const utils = getShapeUtils(shapeUtils, shape) + bounds = utils.hideBounds ? undefined : utils.getBounds(shape) } else if (selectedIds.length > 1) { const selectedShapes = selectedIds.map((id) => page.shapes[id]) + rotation = 0 + isLocked = selectedShapes.every((shape) => shape.isLocked) + bounds = selectedShapes.reduce((acc, shape, i) => { if (i === 0) { return getShapeUtils(shapeUtils, shape).getRotatedBounds(shape) @@ -48,9 +58,11 @@ export function useSelection( if (bounds) { const [minX, minY] = canvasToScreen([bounds.minX, bounds.minY], pageState.camera) const [maxX, maxY] = canvasToScreen([bounds.maxX, bounds.maxY], pageState.camera) + isLinked = !!Object.values(page.bindings).find( (binding) => selectedIds.includes(binding.toId) || selectedIds.includes(binding.fromId) ) + rSelectionBounds.current = { minX, minY, diff --git a/packages/core/src/hooks/useShapeTree.tsx b/packages/core/src/hooks/useShapeTree.tsx index 0a02b4546..21e35e658 100644 --- a/packages/core/src/hooks/useShapeTree.tsx +++ b/packages/core/src/hooks/useShapeTree.tsx @@ -138,8 +138,14 @@ export function useShapeTree[] = [] - shapesToRender.forEach((shape) => + shapesToRender.forEach((shape) => { + if (shape === undefined) { + throw Error('Rendered shapes included a missing shape') + } + addToShapeTree( shape, tree, @@ -174,7 +184,7 @@ export function useShapeTree a.shape.childIndex - b.shape.childIndex) diff --git a/packages/core/src/hooks/useStyle.tsx b/packages/core/src/hooks/useStyle.tsx index 14e93af44..722fabd6b 100644 --- a/packages/core/src/hooks/useStyle.tsx +++ b/packages/core/src/hooks/useStyle.tsx @@ -79,36 +79,6 @@ const defaultTheme: TLTheme = { } export const TLCSS = css` - @font-face { - font-family: 'Recursive'; - font-style: normal; - font-weight: 500; - font-display: swap; - src: url(https://fonts.gstatic.com/s/recursive/v23/8vI-7wMr0mhh-RQChyHEH06TlXhq_gukbYrFMk1QuAIcyEwG_X-dpEfaE5YaERmK-CImKsvxvU-MXGX2fSqasNfUlTGZnI14ZeY.woff2) - format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, - U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; - } - @font-face { - font-family: 'Recursive'; - font-style: normal; - font-weight: 700; - font-display: swap; - src: url(https://fonts.gstatic.com/s/recursive/v23/8vI-7wMr0mhh-RQChyHEH06TlXhq_gukbYrFMk1QuAIcyEwG_X-dpEfaE5YaERmK-CImKsvxvU-MXGX2fSqasNfUlTGZnI14ZeY.woff2) - format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, - U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; - } - @font-face { - font-family: 'Recursive Mono'; - font-style: normal; - font-weight: 420; - font-display: swap; - src: url(https://fonts.gstatic.com/s/recursive/v23/8vI-7wMr0mhh-RQChyHEH06TlXhq_gukbYrFMk1QuAIcyEwG_X-dpEfaE5YaERmK-CImqvTxvU-MXGX2fSqasNfUlTGZnI14ZeY.woff2) - format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, - U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; - } .tl-container { --tl-zoom: 1; --tl-scale: calc(1 / var(--tl-zoom)); @@ -351,11 +321,6 @@ export const TLCSS = css` .tl-brush.dashed { stroke: none; } - .tl-dot { - fill: var(--tl-background); - stroke: var(--tl-foreground); - stroke-width: 2px; - } .tl-handle { pointer-events: all; cursor: grab; diff --git a/packages/tldraw/package.json b/packages/tldraw/package.json index ce96de6d8..d5a6900c8 100644 --- a/packages/tldraw/package.json +++ b/packages/tldraw/package.json @@ -56,6 +56,7 @@ "idb-keyval": "^6.1.0", "lz-string": "^1.4.4", "perfect-freehand": "^1.0.16", + "react-error-boundary": "^3.1.4", "react-hotkey-hook": "^1.0.2", "react-hotkeys-hook": "^3.4.4", "tslib": "^2.3.1", @@ -105,4 +106,4 @@ } }, "gitHead": "4b1137849ad07da36fc8f0f19cb64e7535a79296" -} \ No newline at end of file +} diff --git a/packages/tldraw/src/Tldraw.tsx b/packages/tldraw/src/Tldraw.tsx index 4023596e7..2843c7c88 100644 --- a/packages/tldraw/src/Tldraw.tsx +++ b/packages/tldraw/src/Tldraw.tsx @@ -12,6 +12,8 @@ import { FocusButton } from '~components/FocusButton' import { TLDR } from '~state/TLDR' import { GRID_SIZE } from '~constants' import { Loading } from '~components/Loading' +import { ErrorBoundary } from 'react-error-boundary' +import { ErrorFallback } from '~components/ErrorFallback' export interface TldrawProps extends TDCallbacks { /** @@ -415,81 +417,83 @@ const InnerTldraw = React.memo(function InnerTldraw({ - + + + {showUI && ( diff --git a/packages/tldraw/src/components/ErrorFallback/ErrorFallback.tsx b/packages/tldraw/src/components/ErrorFallback/ErrorFallback.tsx new file mode 100644 index 000000000..3777c6a72 --- /dev/null +++ b/packages/tldraw/src/components/ErrorFallback/ErrorFallback.tsx @@ -0,0 +1,122 @@ +import * as React from 'react' +import { FallbackProps } from 'react-error-boundary' +import { Divider } from '~components/Primitives/Divider' +import { RowButton } from '~components/Primitives/RowButton' +import { useTldrawApp } from '~hooks' +import { styled } from '~styles' + +export function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) { + const app = useTldrawApp() + + const refreshPage = () => { + window.location.reload() + resetErrorBoundary() + } + + const copyError = () => { + const textarea = document.createElement('textarea') + textarea.value = error.message + document.body.appendChild(textarea) + textarea.select() + document.execCommand('copy') + textarea.remove() + } + + const downloadBackup = () => { + app.saveProjectAs() + } + + const resetDocument = () => { + app.resetDocument() + resetErrorBoundary() + } + + return ( + + +
We've encountered an error!
+
+          {error.message}
+        
+ + Copy Error + Refresh Page + + +

+ Keep getting this error?{' '} + + Download your project + {' '} + as a backup and then{' '} + + reset the document + + . +

+
+
+ ) +} + +const Container = styled('div', { + position: 'absolute', + top: 0, + left: 0, + width: '100%', + height: '100%', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + backgroundColor: '$canvas', +}) + +const InnerContainer = styled('div', { + backgroundColor: '$panel', + border: '1px solid $panelContrast', + padding: '$5', + borderRadius: 8, + boxShadow: '$panel', + maxWidth: 320, + color: '$text', + fontFamily: '$ui', + fontSize: '$2', + textAlign: 'center', + display: 'flex', + flexDirection: 'column', + gap: '$3', + '& > pre': { + marginTop: '$3', + marginBottom: '$3', + textAlign: 'left', + whiteSpace: 'pre-wrap', + backgroundColor: '$hover', + padding: '$4', + borderRadius: '$2', + fontFamily: '"Menlo", "Monaco", monospace', + fontWeight: 500, + }, + '& p': { + fontFamily: '$body', + lineHeight: 1.7, + padding: '$5', + margin: 0, + }, + '& a': { + color: '$text', + cursor: 'pointer', + textDecoration: 'underline', + }, + '& hr': { + marginLeft: '-$5', + marginRight: '-$5', + }, +}) + +const Buttons = styled('div', { + display: 'flex', + '& > button > div': { + justifyContent: 'center', + textAlign: 'center', + }, +}) diff --git a/packages/tldraw/src/components/ErrorFallback/index.ts b/packages/tldraw/src/components/ErrorFallback/index.ts new file mode 100644 index 000000000..4211f3752 --- /dev/null +++ b/packages/tldraw/src/components/ErrorFallback/index.ts @@ -0,0 +1 @@ +export * from './ErrorFallback' diff --git a/packages/tldraw/src/hooks/useStylesheet.ts b/packages/tldraw/src/hooks/useStylesheet.ts index 51abfb4a4..4ba74e68a 100644 --- a/packages/tldraw/src/hooks/useStylesheet.ts +++ b/packages/tldraw/src/hooks/useStylesheet.ts @@ -3,9 +3,43 @@ import * as React from 'react' const styles = new Map() const UID = `tldraw-fonts` -const WEBFONT_URL = - 'https://fonts.googleapis.com/css2?family=Caveat+Brush&family=Source+Code+Pro&family=Source+Sans+Pro&family=Crimson+Pro&display=block' -const CSS = `@import url('${WEBFONT_URL}');` + +const CSS = ` +@import url('https://fonts.googleapis.com/css2?family=Caveat+Brush&family=Source+Code+Pro&family=Source+Sans+Pro&family=Crimson+Pro&display=block'); + +@font-face { + font-family: 'Recursive'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(https://fonts.gstatic.com/s/recursive/v23/8vI-7wMr0mhh-RQChyHEH06TlXhq_gukbYrFMk1QuAIcyEwG_X-dpEfaE5YaERmK-CImKsvxvU-MXGX2fSqasNfUlTGZnI14ZeY.woff2) + format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, + U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@font-face { + font-family: 'Recursive'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url(https://fonts.gstatic.com/s/recursive/v23/8vI-7wMr0mhh-RQChyHEH06TlXhq_gukbYrFMk1QuAIcyEwG_X-dpEfaE5YaERmK-CImKsvxvU-MXGX2fSqasNfUlTGZnI14ZeY.woff2) + format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, + U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@font-face { + font-family: 'Recursive Mono'; + font-style: normal; + font-weight: 420; + font-display: swap; + src: url(https://fonts.gstatic.com/s/recursive/v23/8vI-7wMr0mhh-RQChyHEH06TlXhq_gukbYrFMk1QuAIcyEwG_X-dpEfaE5YaERmK-CImqvTxvU-MXGX2fSqasNfUlTGZnI14ZeY.woff2) + format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, + U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +` export function useStylesheet() { React.useLayoutEffect(() => { diff --git a/packages/tldraw/src/styles/stitches.config.ts b/packages/tldraw/src/styles/stitches.config.ts index 720dcd75c..867f47939 100644 --- a/packages/tldraw/src/styles/stitches.config.ts +++ b/packages/tldraw/src/styles/stitches.config.ts @@ -21,6 +21,7 @@ const { styled, createTheme } = createStitches({ tooltip: '#1d1d1d', tooltipContrast: '#ffffff', warn: 'rgba(255, 100, 100, 1)', + canvas: 'rgb(248, 249, 250)', }, shadows: { 2: '0px 1px 1px rgba(0, 0, 0, 0.14)', @@ -110,6 +111,7 @@ export const dark = createTheme({ text: '#f8f9fa', tooltip: '#1d1d1d', tooltipContrast: '#ffffff', + canvas: '#212529', }, shadows: { 2: '0px 1px 1px rgba(0, 0, 0, 0.24)', diff --git a/yarn.lock b/yarn.lock index 2c3b7ea1f..87279b2e4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9385,6 +9385,13 @@ rc@^1.2.7, rc@^1.2.8: object-assign "^4.1.1" scheduler "^0.20.2" +react-error-boundary@^3.1.4: + version "3.1.4" + resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-3.1.4.tgz#255db92b23197108757a888b01e5b729919abde0" + integrity sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA== + dependencies: + "@babel/runtime" "^7.12.5" + react-feather@^2.0.9: version "2.0.9" resolved "https://registry.yarnpkg.com/react-feather/-/react-feather-2.0.9.tgz#6e42072130d2fa9a09d4476b0e61b0ed17814480"