Feature/mobx - adds mobx support to @tldraw/core (#383)
* Initial commit with mobx * Convert more to mobx * Make useCameraCss reactive (autorun) * Move more items to mobx * Fix more invalid components and layout hooks that needed to be reactive * Add autorun to css layout effect * Remove centric specific yarn.lock changes * mild cleanup * update from main * add tests, example * cleanup * minor tweak to advanced example * Update app.tsx * Optimizations around events not being memoized * Support className property on SVGContainer * Add data-type to shape container to aid with external styling * Fix classnames * Fixes bug on text shapes / shapes with refs * v1.1.9-beta.1 * v1.1.9-beta.2 * Drop mobx as a dependency for core * v1.1.9-beta.3 * rename * Revert "Drop mobx as a dependency for core" This reverts commit 2d93f84a87f0c709e55fb2766519bfde03f8e854. * remove unused code from utils, move curve to separate package * v1.1.9-beta.4 * Add pretty-quick * Update package.json * Renamings Co-authored-by: Noah Shipley <nshipley@centricsoftware.com> Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
This commit is contained in:
parent
a9458b20fb
commit
98cc0d4cac
141 changed files with 1949 additions and 1983 deletions
|
@ -16,6 +16,8 @@ import './styles.css'
|
|||
import styled from 'stitches.config'
|
||||
import { Api } from 'state/api'
|
||||
|
||||
declare const window: Window & { api: Api }
|
||||
|
||||
const onHoverShape: TLPointerEventHandler = (info, e) => {
|
||||
machine.send('HOVERED_SHAPE', info)
|
||||
}
|
||||
|
@ -181,7 +183,6 @@ export default function App({ onMount }: AppProps): JSX.Element {
|
|||
React.useEffect(() => {
|
||||
const api = new Api(appState)
|
||||
onMount?.(api)
|
||||
// @ts-ignore
|
||||
window['api'] = api
|
||||
}, [])
|
||||
|
||||
|
@ -189,6 +190,13 @@ export default function App({ onMount }: AppProps): JSX.Element {
|
|||
|
||||
// const hideBounds = appState.isInAny('transformingSelection', 'translating', 'creating')
|
||||
|
||||
const firstShapeId = appState.data.pageState.selectedIds[0]
|
||||
const firstShape = firstShapeId ? appState.data.page.shapes[firstShapeId] : null
|
||||
const hideResizeHandles = firstShape
|
||||
? appState.data.pageState.selectedIds.length === 1 &&
|
||||
shapeUtils[firstShape.type].hideResizeHandles
|
||||
: false
|
||||
|
||||
return (
|
||||
<AppContainer>
|
||||
<Renderer
|
||||
|
@ -216,6 +224,7 @@ export default function App({ onMount }: AppProps): JSX.Element {
|
|||
onKeyUp={onKeyUp}
|
||||
hideBounds={hideBounds}
|
||||
hideHandles={hideBounds}
|
||||
hideResizeHandles={hideResizeHandles}
|
||||
hideIndicators={hideBounds}
|
||||
hideBindingHandles={true}
|
||||
/>
|
||||
|
|
|
@ -15,6 +15,10 @@ export class ArrowUtil extends CustomShapeUtil<T, E> {
|
|||
|
||||
Indicator = ArrowIndicator
|
||||
|
||||
hideResizeHandles = true
|
||||
|
||||
hideBounds = true
|
||||
|
||||
getBounds = (shape: T) => {
|
||||
const bounds = Utils.getFromCache(this.boundsCache, shape, () => {
|
||||
const { start, end } = shape.handles
|
||||
|
@ -26,8 +30,6 @@ export class ArrowUtil extends CustomShapeUtil<T, E> {
|
|||
|
||||
/* ----------------- Custom Methods ----------------- */
|
||||
|
||||
hideBounds = true
|
||||
|
||||
canBind = false
|
||||
|
||||
getShape = (props: Partial<T>): T => {
|
||||
|
|
|
@ -14,6 +14,8 @@ export class BoxUtil extends CustomShapeUtil<T, E> {
|
|||
|
||||
Indicator = BoxIndicator
|
||||
|
||||
hideResizeHandles = false
|
||||
|
||||
getBounds = (shape: T) => {
|
||||
const bounds = Utils.getFromCache(this.boundsCache, shape, () => {
|
||||
const [width, height] = shape.size
|
||||
|
@ -48,6 +50,10 @@ export class BoxUtil extends CustomShapeUtil<T, E> {
|
|||
}
|
||||
}
|
||||
|
||||
shouldRender = (prev: T, next: T) => {
|
||||
return next.size !== prev.size
|
||||
}
|
||||
|
||||
getCenter = (shape: T) => {
|
||||
return Utils.getBoundsCenter(this.getBounds(shape))
|
||||
}
|
||||
|
|
|
@ -20,7 +20,9 @@ export class PencilUtil extends CustomShapeUtil<T, E> {
|
|||
|
||||
Indicator = PencilIndicator
|
||||
|
||||
startTime = Date.now()
|
||||
hideResizeHandles = true
|
||||
|
||||
hideBounds = false
|
||||
|
||||
getBounds = (shape: T) => {
|
||||
const bounds = Utils.getFromCache(this.boundsCache, shape, () => {
|
||||
|
@ -32,8 +34,6 @@ export class PencilUtil extends CustomShapeUtil<T, E> {
|
|||
|
||||
/* ----------------- Custom Methods ----------------- */
|
||||
|
||||
hideBounds = true
|
||||
|
||||
canBind = false
|
||||
|
||||
getShape = (props: Partial<T>): T => {
|
||||
|
|
|
@ -24,6 +24,8 @@
|
|||
"concurrently": "6.0.1",
|
||||
"esbuild": "^0.13.8",
|
||||
"esbuild-serve": "^1.0.1",
|
||||
"mobx": "^6.3.7",
|
||||
"mobx-react-lite": "^3.2.2",
|
||||
"react": ">=16.8",
|
||||
"react-dom": "^16.8 || ^17.0",
|
||||
"rimraf": "3.0.2",
|
||||
|
|
|
@ -1,14 +1,86 @@
|
|||
import * as React from 'react'
|
||||
import { Renderer, TLShapeUtilsMap } from '@tldraw/core'
|
||||
import { BoxUtil, Shape } from './shapes'
|
||||
import { useAppState } from 'hooks/useAppState'
|
||||
import { Renderer, TLPointerEventHandler, TLShapeUtilsMap } from '@tldraw/core'
|
||||
import { RectUtil, Shape } from './shapes'
|
||||
import { Page, PageState } from './stores'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
|
||||
export const shapeUtils: TLShapeUtilsMap<Shape> = {
|
||||
box: new BoxUtil(),
|
||||
const page = new Page({
|
||||
id: 'page1',
|
||||
shapes: {
|
||||
rect1: {
|
||||
id: 'rect1',
|
||||
type: 'rect',
|
||||
parentId: 'page1',
|
||||
name: 'Rect',
|
||||
childIndex: 1,
|
||||
rotation: 0,
|
||||
point: [0, 0],
|
||||
size: [100, 100],
|
||||
},
|
||||
},
|
||||
bindings: {},
|
||||
})
|
||||
|
||||
const pageState = new PageState()
|
||||
|
||||
const shapeUtils: TLShapeUtilsMap<Shape> = {
|
||||
rect: new RectUtil(),
|
||||
}
|
||||
|
||||
export default function App(): JSX.Element {
|
||||
const { page, pageState, meta, theme, events } = useAppState()
|
||||
export default observer(function App(): JSX.Element {
|
||||
const onHoverShape: TLPointerEventHandler = (e) => {
|
||||
pageState.setHoveredId(e.target)
|
||||
}
|
||||
|
||||
const onUnhoverShape: TLPointerEventHandler = () => {
|
||||
pageState.setHoveredId(undefined)
|
||||
}
|
||||
|
||||
const onPointShape: TLPointerEventHandler = (info) => {
|
||||
pageState.setSelectedIds(info.target)
|
||||
}
|
||||
|
||||
const onPointCanvas: TLPointerEventHandler = () => {
|
||||
pageState.clearSelectedIds()
|
||||
}
|
||||
|
||||
const onDragShape: TLPointerEventHandler = (e) => {
|
||||
page.dragShape(e.target, e.point)
|
||||
}
|
||||
|
||||
const onPointerMove: TLPointerEventHandler = (info) => {
|
||||
if (info.shiftKey) {
|
||||
pageState.pan(info.delta)
|
||||
}
|
||||
}
|
||||
|
||||
const [meta] = React.useState({
|
||||
isDarkMode: false,
|
||||
})
|
||||
|
||||
const theme = React.useMemo(
|
||||
() =>
|
||||
meta.isDarkMode
|
||||
? {
|
||||
accent: 'rgb(255, 0, 0)',
|
||||
brushFill: 'rgba(0,0,0,.05)',
|
||||
brushStroke: 'rgba(0,0,0,.25)',
|
||||
selectStroke: 'rgb(66, 133, 244)',
|
||||
selectFill: 'rgba(65, 132, 244, 0.05)',
|
||||
background: 'rgb(248, 249, 250)',
|
||||
foreground: 'rgb(51, 51, 51)',
|
||||
}
|
||||
: {
|
||||
accent: 'rgb(255, 0, 0)',
|
||||
brushFill: 'rgba(0,0,0,.05)',
|
||||
brushStroke: 'rgba(0,0,0,.25)',
|
||||
selectStroke: 'rgb(66, 133, 244)',
|
||||
selectFill: 'rgba(65, 132, 244, 0.05)',
|
||||
background: 'rgb(248, 249, 250)',
|
||||
foreground: 'rgb(51, 51, 51)',
|
||||
},
|
||||
[meta]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="tldraw">
|
||||
|
@ -16,7 +88,12 @@ export default function App(): JSX.Element {
|
|||
shapeUtils={shapeUtils} // Required
|
||||
page={page} // Required
|
||||
pageState={pageState} // Required
|
||||
{...events}
|
||||
onHoverShape={onHoverShape}
|
||||
onUnhoverShape={onUnhoverShape}
|
||||
onPointShape={onPointShape}
|
||||
onPointCanvas={onPointCanvas}
|
||||
onPointerMove={onPointerMove}
|
||||
onDragShape={onDragShape}
|
||||
meta={meta}
|
||||
theme={theme}
|
||||
id={undefined}
|
||||
|
@ -34,4 +111,4 @@ export default function App(): JSX.Element {
|
|||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,157 +0,0 @@
|
|||
import type { TLPage, TLPageState, TLPointerEventHandler, TLShapeChangeHandler } from '@tldraw/core'
|
||||
import Vec from '@tldraw/vec'
|
||||
import * as React from 'react'
|
||||
import type { Shape } from '../shapes'
|
||||
|
||||
export function useAppState() {
|
||||
/* -------------------- Document -------------------- */
|
||||
|
||||
const [page, setPage] = React.useState<TLPage<Shape>>({
|
||||
id: 'page1',
|
||||
shapes: {
|
||||
box1: {
|
||||
id: 'box1',
|
||||
type: 'box',
|
||||
parentId: 'page1',
|
||||
name: 'Box',
|
||||
childIndex: 1,
|
||||
rotation: 0,
|
||||
point: [0, 0],
|
||||
size: [100, 100],
|
||||
},
|
||||
},
|
||||
bindings: {},
|
||||
})
|
||||
|
||||
const [pageState, setPageState] = React.useState<TLPageState>({
|
||||
id: 'page1',
|
||||
selectedIds: [],
|
||||
hoveredId: undefined,
|
||||
camera: {
|
||||
point: [0, 0],
|
||||
zoom: 1,
|
||||
},
|
||||
})
|
||||
|
||||
const [meta] = React.useState({
|
||||
isDarkMode: false,
|
||||
})
|
||||
|
||||
/* ---------------------- Theme --------------------- */
|
||||
|
||||
const theme = React.useMemo(
|
||||
() =>
|
||||
meta.isDarkMode
|
||||
? {
|
||||
accent: 'rgb(255, 0, 0)',
|
||||
brushFill: 'rgba(0,0,0,.05)',
|
||||
brushStroke: 'rgba(0,0,0,.25)',
|
||||
selectStroke: 'rgb(66, 133, 244)',
|
||||
selectFill: 'rgba(65, 132, 244, 0.05)',
|
||||
background: 'rgb(248, 249, 250)',
|
||||
foreground: 'rgb(51, 51, 51)',
|
||||
}
|
||||
: {
|
||||
accent: 'rgb(255, 0, 0)',
|
||||
brushFill: 'rgba(0,0,0,.05)',
|
||||
brushStroke: 'rgba(0,0,0,.25)',
|
||||
selectStroke: 'rgb(66, 133, 244)',
|
||||
selectFill: 'rgba(65, 132, 244, 0.05)',
|
||||
background: 'rgb(248, 249, 250)',
|
||||
foreground: 'rgb(51, 51, 51)',
|
||||
},
|
||||
[meta]
|
||||
)
|
||||
|
||||
/* --------------------- Events --------------------- */
|
||||
|
||||
const onHoverShape = React.useCallback<TLPointerEventHandler>((e) => {
|
||||
setPageState((pageState) => {
|
||||
return {
|
||||
...pageState,
|
||||
hoveredId: e.target,
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
const onUnhoverShape = React.useCallback<TLPointerEventHandler>(() => {
|
||||
setPageState((pageState) => {
|
||||
return {
|
||||
...pageState,
|
||||
hoveredId: undefined,
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
const onPointShape = React.useCallback<TLPointerEventHandler>((e) => {
|
||||
setPageState((pageState) => {
|
||||
return pageState.selectedIds.includes(e.target)
|
||||
? pageState
|
||||
: {
|
||||
...pageState,
|
||||
selectedIds: [e.target],
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
const onPointCanvas = React.useCallback<TLPointerEventHandler>((e) => {
|
||||
setPageState((pageState) => {
|
||||
return {
|
||||
...pageState,
|
||||
selectedIds: [],
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
const onDragShape = React.useCallback<TLPointerEventHandler>(
|
||||
(e) => {
|
||||
setPage((page) => {
|
||||
const shape = page.shapes[e.target]
|
||||
|
||||
return {
|
||||
...page,
|
||||
shapes: {
|
||||
...page.shapes,
|
||||
[shape.id]: {
|
||||
...shape,
|
||||
point: Vec.sub(e.point, Vec.div(shape.size, 2)),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
},
|
||||
[setPage]
|
||||
)
|
||||
|
||||
const onShapeChange = React.useCallback<TLShapeChangeHandler<Shape>>((changes) => {
|
||||
setPage((page) => {
|
||||
const shape = page.shapes[changes.id]
|
||||
|
||||
return {
|
||||
...page,
|
||||
shapes: {
|
||||
...page.shapes,
|
||||
[shape.id]: {
|
||||
...shape,
|
||||
...changes,
|
||||
} as Shape,
|
||||
},
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
return {
|
||||
page,
|
||||
pageState,
|
||||
meta,
|
||||
theme,
|
||||
events: {
|
||||
onHoverShape,
|
||||
onUnhoverShape,
|
||||
onPointShape,
|
||||
onDragShape,
|
||||
onPointCanvas,
|
||||
onShapeChange,
|
||||
},
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import type { TLShape } from '@tldraw/core'
|
||||
|
||||
export interface RectShape extends TLShape {
|
||||
type: 'box'
|
||||
type: 'rect'
|
||||
size: number[]
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import type { RectShape } from './RectShape'
|
|||
type T = RectShape
|
||||
type E = SVGSVGElement
|
||||
|
||||
export class BoxUtil extends TLShapeUtil<T, E> {
|
||||
export class RectUtil extends TLShapeUtil<T, E> {
|
||||
Component = RectComponent
|
||||
|
||||
Indicator = RectIndicator
|
||||
|
|
69
examples/core-example/src/stores.ts
Normal file
69
examples/core-example/src/stores.ts
Normal file
|
@ -0,0 +1,69 @@
|
|||
import { TLBounds, TLPage, TLPageState, Utils } from '@tldraw/core'
|
||||
import Vec from '@tldraw/vec'
|
||||
import { action, makeAutoObservable } from 'mobx'
|
||||
import type { Shape } from './shapes'
|
||||
|
||||
export class Page implements TLPage<Shape> {
|
||||
id
|
||||
name
|
||||
shapes
|
||||
bindings
|
||||
|
||||
constructor(opts = {} as TLPage<Shape>) {
|
||||
const { id = Utils.uniqueId(), name = 'page', shapes = {}, bindings = {} } = opts
|
||||
this.id = id
|
||||
this.name = name
|
||||
this.shapes = shapes
|
||||
this.bindings = bindings
|
||||
makeAutoObservable(this)
|
||||
}
|
||||
|
||||
@action dragShape(id: string, point: number[]) {
|
||||
const shape = this.shapes[id]
|
||||
shape.point = Vec.sub(point, Vec.div(shape.size, 2))
|
||||
}
|
||||
}
|
||||
|
||||
export class PageState implements TLPageState {
|
||||
id
|
||||
selectedIds
|
||||
camera
|
||||
brush?: TLBounds
|
||||
pointedId?: string
|
||||
hoveredId?: string
|
||||
editingId?: string
|
||||
bindingId?: string
|
||||
|
||||
constructor(opts = {} as TLPageState) {
|
||||
const {
|
||||
id = Utils.uniqueId(),
|
||||
selectedIds = [],
|
||||
camera = {
|
||||
point: [0, 0],
|
||||
zoom: 1,
|
||||
},
|
||||
} = opts
|
||||
this.id = id
|
||||
this.camera = camera
|
||||
this.selectedIds = selectedIds
|
||||
makeAutoObservable(this)
|
||||
}
|
||||
|
||||
@action setHoveredId = (id: string | undefined) => {
|
||||
this.hoveredId = id
|
||||
}
|
||||
|
||||
@action setSelectedIds = (id: string) => {
|
||||
if (!this.selectedIds.includes(id)) {
|
||||
this.selectedIds = [id]
|
||||
}
|
||||
}
|
||||
|
||||
@action clearSelectedIds = () => {
|
||||
this.selectedIds = []
|
||||
}
|
||||
|
||||
@action pan = (point: number[]) => {
|
||||
this.camera.point = Vec.add(this.camera.point, point)
|
||||
}
|
||||
}
|
|
@ -7,6 +7,8 @@
|
|||
"baseUrl": ".",
|
||||
"rootDir": "src",
|
||||
"emitDeclarationOnly": false,
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": true,
|
||||
"paths": {
|
||||
"*": ["src/*"],
|
||||
"@tldraw/core": ["../packages/core"]
|
||||
|
|
11
package.json
11
package.json
|
@ -9,6 +9,7 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"packages/curve",
|
||||
"packages/vec",
|
||||
"packages/intersect",
|
||||
"packages/core",
|
||||
|
@ -28,19 +29,21 @@
|
|||
"build:packages": "lerna run build:packages --stream",
|
||||
"build:apps": "lerna run build:apps",
|
||||
"start": "yarn build:packages && lerna run start --stream --parallel",
|
||||
"start:all": "yarn build:packages && lerna run start:all --stream --parallel",
|
||||
"start:core": "lerna run start:core --stream --parallel",
|
||||
"start:www": "yarn build:packages && lerna run start --parallel & cd apps/www && yarn dev",
|
||||
"start:electron": "lerna run start:electron --stream --parallel",
|
||||
"start:vscode": "code apps/vscode/extension & lerna run start:vscode --parallel; ",
|
||||
"publish:patch": "yarn test && yarn build:packages && lerna publish patch",
|
||||
"fix:style": "yarn run prettier ./packages/tldraw/src --write",
|
||||
"fix:style": "yarn run prettier ./packages/core/src --write && yarn run prettier ./packages/tldraw/src --write",
|
||||
"lerna": "lerna",
|
||||
"test": "lerna run test --stream",
|
||||
"test:ci": "lerna run test:ci --stream",
|
||||
"test:watch": "lerna run test:watch --stream",
|
||||
"docs": "lerna run typedoc",
|
||||
"docs:watch": "lerna run typedoc --watch",
|
||||
"postinstall": "husky install"
|
||||
"postinstall": "husky install",
|
||||
"pretty-quick": "pretty-quick"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@swc-node/jest": "^1.3.3",
|
||||
|
@ -64,10 +67,12 @@
|
|||
"resize-observer-polyfill": "^1.5.1",
|
||||
"tslib": "^2.3.0",
|
||||
"typedoc": "^0.22.3",
|
||||
"typescript": "^4.5.2"
|
||||
"typescript": "^4.5.2",
|
||||
"pretty-quick": "^3.1.2"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "pretty-quick --staged",
|
||||
"pre-push": "fix:style && eslint && test"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "1.1.5",
|
||||
"version": "1.1.9-beta.4",
|
||||
"name": "@tldraw/core",
|
||||
"description": "The tldraw core renderer and utilities.",
|
||||
"author": "@steveruizok",
|
||||
|
@ -39,7 +39,8 @@
|
|||
"dependencies": {
|
||||
"@tldraw/intersect": "^1.1.5",
|
||||
"@tldraw/vec": "^1.1.5",
|
||||
"@use-gesture/react": "^10.1.3"
|
||||
"@use-gesture/react": "^10.1.3",
|
||||
"mobx-react-lite": "^3.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8",
|
||||
|
@ -53,14 +54,22 @@
|
|||
"@types/node": "^16.11.6",
|
||||
"@types/react": "^17.0.33",
|
||||
"@types/react-dom": "^17.0.10",
|
||||
"tsconfig-replace-paths": "^0.0.5"
|
||||
"mobx": "^6.3.8",
|
||||
"tsconfig-replace-paths": "^0.0.11"
|
||||
},
|
||||
"jest": {
|
||||
"setupFilesAfterEnv": [
|
||||
"<rootDir>/../../setupTests.ts"
|
||||
],
|
||||
"transform": {
|
||||
"^.+\\.(tsx|jsx|ts|js|mjs)?$": "@swc-node/jest"
|
||||
"^.+\\.(tsx|jsx|ts|js|mjs)?$": [
|
||||
"@swc-node/jest",
|
||||
{
|
||||
"dynamicImport": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
|
||||
"moduleFileExtensions": [
|
||||
|
|
|
@ -44,10 +44,10 @@ async function main() {
|
|||
sourcemap: true,
|
||||
})
|
||||
|
||||
let esmSize = 0
|
||||
Object.values(esmResult.metafile.outputs).forEach((output) => {
|
||||
esmSize += output.bytes
|
||||
})
|
||||
const esmSize = Object.values(esmResult.metafile.outputs).reduce(
|
||||
(acc, { bytes }) => acc + bytes,
|
||||
0
|
||||
)
|
||||
|
||||
fs.readFile('./dist/esm/index.js', (_err, data) => {
|
||||
gzip(data, (_err, result) => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { Binding } from './binding'
|
||||
import { Binding } from './Binding'
|
||||
|
||||
jest.spyOn(console, 'error').mockImplementation(() => void null)
|
||||
|
1
packages/core/src/components/Binding/index.ts
Normal file
1
packages/core/src/components/Binding/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './Binding'
|
|
@ -1,14 +1,15 @@
|
|||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import * as React from 'react'
|
||||
import { TLBoundsEdge, TLBoundsCorner, TLBounds } from '~types'
|
||||
import { CenterHandle } from './center-handle'
|
||||
import { RotateHandle } from './rotate-handle'
|
||||
import { CornerHandle } from './corner-handle'
|
||||
import { EdgeHandle } from './edge-handle'
|
||||
import { CloneButtons } from './clone-buttons'
|
||||
import { Container } from '~components/container'
|
||||
import { SVGContainer } from '~components/svg-container'
|
||||
import { LinkHandle } from './link-handle'
|
||||
import { CenterHandle } from './CenterHandle'
|
||||
import { RotateHandle } from './RotateHandle'
|
||||
import { CornerHandle } from './CornerHandle'
|
||||
import { LinkHandle } from './LinkHandle'
|
||||
import { EdgeHandle } from './EdgeHandle'
|
||||
import { CloneButtons } from './CloneButtons'
|
||||
import { Container } from '~components/Container'
|
||||
import { SVGContainer } from '~components/SVGContainer'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
|
||||
interface BoundsProps {
|
||||
zoom: number
|
||||
|
@ -24,7 +25,7 @@ interface BoundsProps {
|
|||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
export const Bounds = React.memo(function Bounds({
|
||||
export const Bounds = observer<BoundsProps>(function Bounds({
|
||||
zoom,
|
||||
bounds,
|
||||
viewportWidth,
|
|
@ -1,9 +1,10 @@
|
|||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import * as React from 'react'
|
||||
import type { TLBounds } from '~types'
|
||||
import { useBoundsEvents } from '~hooks'
|
||||
import { Container } from '~components/container'
|
||||
import { SVGContainer } from '~components/svg-container'
|
||||
import { Container } from '~components/Container'
|
||||
import { SVGContainer } from '~components/SVGContainer'
|
||||
|
||||
interface BoundsBgProps {
|
||||
bounds: TLBounds
|
||||
|
@ -11,11 +12,11 @@ interface BoundsBgProps {
|
|||
isHidden: boolean
|
||||
}
|
||||
|
||||
export const BoundsBg = React.memo(function BoundsBg({
|
||||
export const BoundsBg = observer<BoundsBgProps>(function BoundsBg({
|
||||
bounds,
|
||||
rotation,
|
||||
isHidden,
|
||||
}: BoundsBgProps): JSX.Element {
|
||||
}): JSX.Element {
|
||||
const events = useBoundsEvents()
|
||||
|
||||
return (
|
|
@ -1,3 +1,4 @@
|
|||
import { observer } from 'mobx-react-lite'
|
||||
import * as React from 'react'
|
||||
import type { TLBounds } from '~types'
|
||||
|
||||
|
@ -7,11 +8,11 @@ export interface CenterHandleProps {
|
|||
isHidden: boolean
|
||||
}
|
||||
|
||||
export const CenterHandle = React.memo(function CenterHandle({
|
||||
export const CenterHandle = observer<CenterHandleProps>(function CenterHandle({
|
||||
bounds,
|
||||
isLocked,
|
||||
isHidden,
|
||||
}: CenterHandleProps): JSX.Element {
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<rect
|
||||
className={isLocked ? 'tl-bounds-center tl-dashed' : 'tl-bounds-center'}
|
|
@ -1,3 +1,4 @@
|
|||
import { observer } from 'mobx-react-lite'
|
||||
import * as React from 'react'
|
||||
import { useTLContext } from '~hooks'
|
||||
import type { TLBounds } from '~types'
|
||||
|
@ -20,7 +21,12 @@ export interface CloneButtonProps {
|
|||
side: 'top' | 'right' | 'bottom' | 'left' | 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight'
|
||||
}
|
||||
|
||||
export function CloneButton({ bounds, side, targetSize, size }: CloneButtonProps) {
|
||||
export const CloneButton = observer<CloneButtonProps>(function CloneButton({
|
||||
bounds,
|
||||
side,
|
||||
targetSize,
|
||||
size,
|
||||
}: CloneButtonProps) {
|
||||
const x = {
|
||||
left: -44,
|
||||
topLeft: -44,
|
||||
|
@ -77,4 +83,4 @@ export function CloneButton({ bounds, side, targetSize, size }: CloneButtonProps
|
|||
</g>
|
||||
</g>
|
||||
)
|
||||
}
|
||||
})
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from 'react'
|
||||
import type { TLBounds } from '~types'
|
||||
import { CloneButton } from './clone-button'
|
||||
import { CloneButton } from './CloneButton'
|
||||
|
||||
export interface CloneButtonsProps {
|
||||
bounds: TLBounds
|
|
@ -1,3 +1,4 @@
|
|||
import { observer } from 'mobx-react-lite'
|
||||
import * as React from 'react'
|
||||
import { useBoundsHandleEvents } from '~hooks'
|
||||
import { TLBoundsCorner, TLBounds } from '~types'
|
||||
|
@ -17,7 +18,7 @@ interface CornerHandleProps {
|
|||
isHidden?: boolean
|
||||
}
|
||||
|
||||
export const CornerHandle = React.memo(function CornerHandle({
|
||||
export const CornerHandle = observer(function CornerHandle({
|
||||
size,
|
||||
targetSize,
|
||||
isHidden,
|
|
@ -1,3 +1,4 @@
|
|||
import { observer } from 'mobx-react-lite'
|
||||
import * as React from 'react'
|
||||
import { useBoundsHandleEvents } from '~hooks'
|
||||
import { TLBoundsEdge, TLBounds } from '~types'
|
||||
|
@ -17,7 +18,7 @@ interface EdgeHandleProps {
|
|||
isHidden: boolean
|
||||
}
|
||||
|
||||
export const EdgeHandle = React.memo(function EdgeHandle({
|
||||
export const EdgeHandle = observer<EdgeHandleProps>(function EdgeHandle({
|
||||
size,
|
||||
isHidden,
|
||||
bounds,
|
|
@ -1,3 +1,4 @@
|
|||
import { observer } from 'mobx-react-lite'
|
||||
import * as React from 'react'
|
||||
import { useBoundsHandleEvents } from '~hooks'
|
||||
import type { TLBounds } from '~types'
|
||||
|
@ -9,12 +10,12 @@ interface RotateHandleProps {
|
|||
isHidden: boolean
|
||||
}
|
||||
|
||||
export const RotateHandle = React.memo(function RotateHandle({
|
||||
export const RotateHandle = observer<RotateHandleProps>(function RotateHandle({
|
||||
bounds,
|
||||
targetSize,
|
||||
size,
|
||||
isHidden,
|
||||
}: RotateHandleProps): JSX.Element {
|
||||
}): JSX.Element {
|
||||
const events = useBoundsHandleEvents('rotate')
|
||||
|
||||
return (
|
|
@ -1,7 +1,7 @@
|
|||
import { renderWithContext } from '~test'
|
||||
import { screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { Bounds } from '../bounds'
|
||||
import { Bounds } from '../Bounds'
|
||||
|
||||
describe('bounds', () => {
|
||||
test('mounts component without crashing', () => {
|
|
@ -1,6 +1,6 @@
|
|||
import { render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { BoundsBg } from '../bounds-bg'
|
||||
import { BoundsBg } from '../BoundsBg'
|
||||
|
||||
jest.spyOn(console, 'error').mockImplementation(() => void null)
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { CenterHandle } from '../center-handle'
|
||||
import { CenterHandle } from '../CenterHandle'
|
||||
|
||||
jest.spyOn(console, 'error').mockImplementation(() => void null)
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
import { renderWithContext } from '~test'
|
||||
import { screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { CloneButton } from '../clone-button'
|
||||
import { TLBoundsCorner } from '~types'
|
||||
import { CloneButton } from '../CloneButton'
|
||||
|
||||
jest.spyOn(console, 'error').mockImplementation(() => void null)
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { renderWithContext } from '~test'
|
||||
import { screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { CornerHandle } from '../corner-handle'
|
||||
import { CornerHandle } from '../CornerHandle'
|
||||
import { TLBoundsCorner } from '~types'
|
||||
|
||||
jest.spyOn(console, 'error').mockImplementation(() => void null)
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from 'react'
|
||||
import { renderWithContext } from '~test'
|
||||
import { screen } from '@testing-library/react'
|
||||
import { EdgeHandle } from '../edge-handle'
|
||||
import { EdgeHandle } from '../EdgeHandle'
|
||||
import { TLBoundsEdge } from '~types'
|
||||
|
||||
jest.spyOn(console, 'error').mockImplementation(() => void null)
|
|
@ -1,7 +1,7 @@
|
|||
import { renderWithContext } from '~test'
|
||||
import { screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { LinkHandle } from '../link-handle'
|
||||
import { LinkHandle } from '../LinkHandle'
|
||||
|
||||
jest.spyOn(console, 'error').mockImplementation(() => void null)
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { renderWithContext } from '~test'
|
||||
import { screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { RotateHandle } from '../rotate-handle'
|
||||
import { RotateHandle } from '../RotateHandle'
|
||||
|
||||
jest.spyOn(console, 'error').mockImplementation(() => void null)
|
||||
|
1
packages/core/src/components/Bounds/index.ts
Normal file
1
packages/core/src/components/Bounds/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './Bounds'
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from 'react'
|
||||
import { renderWithSvg } from '~test'
|
||||
import { screen } from '@testing-library/react'
|
||||
import { Brush } from './brush'
|
||||
import { Brush } from './Brush'
|
||||
|
||||
describe('brush', () => {
|
||||
test('mounts component without crashing', () => {
|
|
@ -1,9 +1,10 @@
|
|||
import { observer } from 'mobx-react-lite'
|
||||
import { SVGContainer } from '~components'
|
||||
import { Container } from '~components/container'
|
||||
import { Container } from '~components/Container'
|
||||
import type { TLBounds } from '~types'
|
||||
import * as React from 'react'
|
||||
|
||||
export const Brush = React.memo(function Brush({ brush }: { brush: TLBounds }): JSX.Element | null {
|
||||
export const Brush = observer<{ brush: TLBounds }>(function Brush({ brush }): JSX.Element | null {
|
||||
return (
|
||||
<Container bounds={brush} rotation={0}>
|
||||
<SVGContainer>
|
1
packages/core/src/components/Brush/index.ts
Normal file
1
packages/core/src/components/Brush/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './Brush'
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from 'react'
|
||||
import { mockDocument, renderWithContext } from '~test'
|
||||
import { Canvas } from './canvas'
|
||||
import { Canvas } from './Canvas'
|
||||
|
||||
describe('page', () => {
|
||||
test('mounts component without crashing', () => {
|
||||
|
@ -9,13 +9,16 @@ describe('page', () => {
|
|||
page={mockDocument.page}
|
||||
pageState={mockDocument.pageState}
|
||||
hideBounds={false}
|
||||
hideGrid={false}
|
||||
hideIndicators={false}
|
||||
hideHandles={false}
|
||||
hideBindingHandles={false}
|
||||
hideResizeHandles={false}
|
||||
hideCloneHandles={false}
|
||||
hideRotateHandle={false}
|
||||
onBoundsChange={() => {}}
|
||||
onBoundsChange={() => {
|
||||
// noop
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})
|
|
@ -1,4 +1,5 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import * as React from 'react'
|
||||
import {
|
||||
usePreventNavigation,
|
||||
|
@ -9,17 +10,17 @@ import {
|
|||
useKeyEvents,
|
||||
} from '~hooks'
|
||||
import type { TLBinding, TLBounds, TLPage, TLPageState, TLShape, TLSnapLine, TLUsers } from '~types'
|
||||
import { ErrorFallback } from '~components/error-fallback'
|
||||
import { ErrorBoundary } from '~components/error-boundary'
|
||||
import { Brush } from '~components/brush'
|
||||
import { Page } from '~components/page'
|
||||
import { Users } from '~components/users'
|
||||
import { ErrorFallback } from '~components/ErrorFallback'
|
||||
import { ErrorBoundary } from '~components/ErrorBoundary'
|
||||
import { Brush } from '~components/Brush'
|
||||
import { Page } from '~components/Page'
|
||||
import { Users } from '~components/Users'
|
||||
import { useResizeObserver } from '~hooks/useResizeObserver'
|
||||
import { inputs } from '~inputs'
|
||||
import { UsersIndicators } from '~components/users-indicators'
|
||||
import { SnapLines } from '~components/snap-lines/snap-lines'
|
||||
import { Grid } from '~components/grid'
|
||||
import { Overlay } from '~components/overlay'
|
||||
import { UsersIndicators } from '~components/UsersIndicators'
|
||||
import { SnapLines } from '~components/SnapLines/SnapLines'
|
||||
import { Grid } from '~components/Grid'
|
||||
import { Overlay } from '~components/Overlay'
|
||||
|
||||
function resetError() {
|
||||
void null
|
||||
|
@ -46,7 +47,10 @@ interface CanvasProps<T extends TLShape, M extends Record<string, unknown>> {
|
|||
onBoundsChange: (bounds: TLBounds) => void
|
||||
}
|
||||
|
||||
export function Canvas<T extends TLShape, M extends Record<string, unknown>>({
|
||||
export const Canvas = observer(function _Canvas<
|
||||
T extends TLShape,
|
||||
M extends Record<string, unknown>
|
||||
>({
|
||||
id,
|
||||
page,
|
||||
pageState,
|
||||
|
@ -91,7 +95,7 @@ export function Canvas<T extends TLShape, M extends Record<string, unknown>>({
|
|||
<div id="canvas" className="tl-absolute tl-canvas" ref={rCanvas} {...events}>
|
||||
<ErrorBoundary FallbackComponent={ErrorFallback} onReset={resetError}>
|
||||
{!hideGrid && grid && <Grid grid={grid} camera={pageState.camera} />}
|
||||
<div ref={rLayer} className="tl-absolute tl-layer">
|
||||
<div ref={rLayer} className="tl-absolute tl-layer" data-testid="layer">
|
||||
<Page
|
||||
page={page}
|
||||
pageState={pageState}
|
||||
|
@ -117,4 +121,4 @@ export function Canvas<T extends TLShape, M extends Record<string, unknown>>({
|
|||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
1
packages/core/src/components/Canvas/index.ts
Normal file
1
packages/core/src/components/Canvas/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './Canvas'
|
|
@ -1,8 +1,10 @@
|
|||
import { observer } from 'mobx-react-lite'
|
||||
import type {HTMLProps} from 'react'
|
||||
import * as React from 'react'
|
||||
import type { TLBounds } from '~types'
|
||||
import { usePosition } from '~hooks'
|
||||
|
||||
interface ContainerProps {
|
||||
interface ContainerProps extends HTMLProps<HTMLDivElement> {
|
||||
id?: string
|
||||
bounds: TLBounds
|
||||
isGhost?: boolean
|
||||
|
@ -10,13 +12,14 @@ interface ContainerProps {
|
|||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export const Container = React.memo(function Container({
|
||||
export const Container = observer<ContainerProps>(function Container({
|
||||
id,
|
||||
bounds,
|
||||
rotation = 0,
|
||||
isGhost = false,
|
||||
children,
|
||||
}: ContainerProps) {
|
||||
...props
|
||||
}) {
|
||||
const rPositioned = usePosition(bounds, rotation)
|
||||
|
||||
return (
|
||||
|
@ -25,6 +28,8 @@ export const Container = React.memo(function Container({
|
|||
ref={rPositioned}
|
||||
className={isGhost ? 'tl-positioned tl-ghost' : 'tl-positioned'}
|
||||
aria-label="container"
|
||||
data-testid="container"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
1
packages/core/src/components/Container/index.ts
Normal file
1
packages/core/src/components/Container/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './Container'
|
1
packages/core/src/components/ErrorBoundary/index.ts
Normal file
1
packages/core/src/components/ErrorBoundary/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './ErrorBoundary'
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from 'react'
|
||||
import { renderWithContext } from '~test'
|
||||
import { ErrorFallback } from './error-fallback'
|
||||
import { ErrorFallback } from './ErrorFallback'
|
||||
|
||||
describe('error fallback', () => {
|
||||
test('mounts component without crashing', () => {
|
1
packages/core/src/components/ErrorFallback/index.ts
Normal file
1
packages/core/src/components/ErrorFallback/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './ErrorFallback'
|
1
packages/core/src/components/Grid/index.ts
Normal file
1
packages/core/src/components/Grid/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './Grid'
|
20
packages/core/src/components/HTMLContainer/HTMLContainer.tsx
Normal file
20
packages/core/src/components/HTMLContainer/HTMLContainer.tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { Observer } from 'mobx-react-lite'
|
||||
import * as React from 'react'
|
||||
|
||||
interface HTMLContainerProps extends React.HTMLProps<HTMLDivElement> {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export const HTMLContainer = React.forwardRef<HTMLDivElement, HTMLContainerProps>(
|
||||
function HTMLContainer({ children, className = '', ...rest }, ref) {
|
||||
return (
|
||||
<Observer>
|
||||
{() => (
|
||||
<div ref={ref} className={`tl-positioned-div ${className}`} {...rest}>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</Observer>
|
||||
)
|
||||
}
|
||||
)
|
1
packages/core/src/components/HTMLContainer/index.ts
Normal file
1
packages/core/src/components/HTMLContainer/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './HTMLContainer'
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from 'react'
|
||||
import { renderWithContext } from '~test'
|
||||
import { screen } from '@testing-library/react'
|
||||
import { Handle } from './handle'
|
||||
import { Handle } from './Handle'
|
||||
|
||||
describe('handle', () => {
|
||||
test('mounts component without crashing', () => {
|
|
@ -1,15 +1,16 @@
|
|||
import * as React from 'react'
|
||||
import { useHandleEvents } from '~hooks'
|
||||
import { Container } from '~components/container'
|
||||
import { Container } from '~components/Container'
|
||||
import Utils from '~utils'
|
||||
import { SVGContainer } from '~components/svg-container'
|
||||
import { SVGContainer } from '~components/SVGContainer'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
|
||||
interface HandleProps {
|
||||
id: string
|
||||
point: number[]
|
||||
}
|
||||
|
||||
export const Handle = React.memo(function Handle({ id, point }: HandleProps) {
|
||||
export const Handle = observer(function Handle({ id, point }: HandleProps) {
|
||||
const events = useHandleEvents(id)
|
||||
|
||||
return (
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from 'react'
|
||||
import { renderWithContext } from '~test'
|
||||
import { Handles } from './handles'
|
||||
import { Handles } from './Handles'
|
||||
import { boxShape } from '~TLShapeUtil/TLShapeUtil.spec'
|
||||
import { screen } from '@testing-library/react'
|
||||
|
|
@ -1,14 +1,15 @@
|
|||
import { observer } from 'mobx-react-lite'
|
||||
import * as React from 'react'
|
||||
import { Vec } from '@tldraw/vec'
|
||||
import type { TLHandle, TLShape } from '~types'
|
||||
import { Handle } from './handle'
|
||||
import { Handle } from './Handle'
|
||||
|
||||
interface HandlesProps {
|
||||
shape: TLShape
|
||||
zoom: number
|
||||
}
|
||||
|
||||
export const Handles = React.memo(function Handles({
|
||||
export const Handles = observer(function Handles({
|
||||
shape,
|
||||
zoom,
|
||||
}: HandlesProps): JSX.Element | null {
|
1
packages/core/src/components/Handles/index.ts
Normal file
1
packages/core/src/components/Handles/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './Handles'
|
|
@ -1,13 +1,13 @@
|
|||
import { observer } from 'mobx-react-lite'
|
||||
import * as React from 'react'
|
||||
|
||||
export function Overlay({
|
||||
camera,
|
||||
children,
|
||||
}: {
|
||||
type MyProps = {
|
||||
camera: { point: number[]; zoom: number }
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const l = 2.5 / camera.zoom
|
||||
}
|
||||
|
||||
export const Overlay = observer<MyProps>(function Overlay({ camera: { zoom, point }, children }) {
|
||||
const l = 2.5 / zoom
|
||||
return (
|
||||
<svg className="tl-overlay">
|
||||
<defs>
|
||||
|
@ -18,7 +18,7 @@ export function Overlay({
|
|||
/>
|
||||
</g>
|
||||
</defs>
|
||||
<g transform={`scale(${camera.zoom}) translate(${camera.point})`}>{children}</g>
|
||||
<g transform={`scale(${zoom}) translate(${point})`}>{children}</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
})
|
1
packages/core/src/components/Overlay/index.ts
Normal file
1
packages/core/src/components/Overlay/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './Overlay'
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from 'react'
|
||||
import { mockDocument, renderWithContext } from '~test'
|
||||
import { Page } from './page'
|
||||
import { Page } from './Page'
|
||||
|
||||
describe('page', () => {
|
||||
test('mounts component without crashing', () => {
|
|
@ -1,12 +1,13 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import * as React from 'react'
|
||||
import type { TLBinding, TLPage, TLPageState, TLShape } from '~types'
|
||||
import { useSelection, useShapeTree, useTLContext } from '~hooks'
|
||||
import { Bounds } from '~components/bounds'
|
||||
import { BoundsBg } from '~components/bounds/bounds-bg'
|
||||
import { Handles } from '~components/handles'
|
||||
import { ShapeNode } from '~components/shape'
|
||||
import { ShapeIndicator } from '~components/shape-indicator'
|
||||
import { Bounds } from '~components/Bounds'
|
||||
import { BoundsBg } from '~components/Bounds/BoundsBg'
|
||||
import { Handles } from '~components/Handles'
|
||||
import { ShapeNode } from '~components/Shape'
|
||||
import { ShapeIndicator } from '~components/ShapeIndicator'
|
||||
import type { TLShapeUtil } from '~TLShapeUtil'
|
||||
|
||||
interface PageProps<T extends TLShape, M extends Record<string, unknown>> {
|
||||
|
@ -25,7 +26,7 @@ interface PageProps<T extends TLShape, M extends Record<string, unknown>> {
|
|||
/**
|
||||
* The Page component renders the current page.
|
||||
*/
|
||||
export const Page = React.memo(function Page<T extends TLShape, M extends Record<string, unknown>>({
|
||||
export const Page = observer(function _Page<T extends TLShape, M extends Record<string, unknown>>({
|
||||
page,
|
||||
pageState,
|
||||
hideBounds,
|
||||
|
@ -76,13 +77,18 @@ export const Page = React.memo(function Page<T extends TLShape, M extends Record
|
|||
))}
|
||||
{!hideIndicators &&
|
||||
selectedShapes.map((shape) => (
|
||||
<ShapeIndicator key={'selected_' + shape.id} shape={shape} meta={meta} isSelected />
|
||||
<ShapeIndicator
|
||||
key={'selected_' + shape.id}
|
||||
shape={shape}
|
||||
meta={meta as any}
|
||||
isSelected
|
||||
/>
|
||||
))}
|
||||
{!hideIndicators && hoveredId && (
|
||||
<ShapeIndicator
|
||||
key={'hovered_' + hoveredId}
|
||||
shape={page.shapes[hoveredId]}
|
||||
meta={meta}
|
||||
meta={meta as any}
|
||||
isHovered
|
||||
/>
|
||||
)}
|
1
packages/core/src/components/Page/index.ts
Normal file
1
packages/core/src/components/Page/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './Page'
|
124
packages/core/src/components/Renderer/Renderer.test.tsx
Normal file
124
packages/core/src/components/Renderer/Renderer.test.tsx
Normal file
|
@ -0,0 +1,124 @@
|
|||
import * as React from 'react'
|
||||
import { mockDocument, mockUtils } from '~test'
|
||||
import { render } from '@testing-library/react'
|
||||
import { Renderer } from './Renderer'
|
||||
import { action, makeAutoObservable } from 'mobx'
|
||||
import type { TLBinding, TLBounds, TLPage, TLPageState } from '~types'
|
||||
import Utils from '~utils'
|
||||
import type { BoxShape } from '~TLShapeUtil/TLShapeUtil.spec'
|
||||
import Vec from '@tldraw/vec'
|
||||
|
||||
describe('renderer', () => {
|
||||
test('mounts component without crashing', () => {
|
||||
render(
|
||||
<Renderer
|
||||
shapeUtils={mockUtils}
|
||||
page={mockDocument.page}
|
||||
pageState={mockDocument.pageState}
|
||||
/>
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('When passing observables', () => {
|
||||
it('updates when the observables change', () => {
|
||||
class PageState implements TLPageState {
|
||||
id
|
||||
selectedIds
|
||||
camera
|
||||
brush?: TLBounds
|
||||
pointedId?: string
|
||||
hoveredId?: string
|
||||
editingId?: string
|
||||
bindingId?: string
|
||||
|
||||
constructor(opts = {} as TLPageState) {
|
||||
const {
|
||||
id = Utils.uniqueId(),
|
||||
selectedIds = [],
|
||||
camera = {
|
||||
point: [0, 0],
|
||||
zoom: 1,
|
||||
},
|
||||
} = opts
|
||||
this.id = id
|
||||
this.camera = camera
|
||||
this.selectedIds = selectedIds
|
||||
makeAutoObservable(this)
|
||||
}
|
||||
|
||||
@action pan = (point: number[]) => {
|
||||
this.camera.point = Vec.add(this.camera.point, point)
|
||||
}
|
||||
}
|
||||
|
||||
class Page implements TLPage<BoxShape> {
|
||||
id = 'page1'
|
||||
shapes = {
|
||||
box1: {
|
||||
id: 'box1',
|
||||
type: 'box' as const,
|
||||
parentId: 'page1',
|
||||
name: 'Box',
|
||||
childIndex: 1,
|
||||
rotation: 0,
|
||||
point: [0, 0],
|
||||
size: [100, 100],
|
||||
},
|
||||
} as Record<string, BoxShape>
|
||||
bindings = {}
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this)
|
||||
}
|
||||
|
||||
@action moveShape = (id: string, point: number[]) => {
|
||||
const shape = this.shapes[id]
|
||||
shape.point = point
|
||||
}
|
||||
}
|
||||
|
||||
const page = new Page()
|
||||
const pageState = new PageState()
|
||||
|
||||
const wrapper = render(<Renderer shapeUtils={mockUtils} page={page} pageState={pageState} />)
|
||||
|
||||
// PageState
|
||||
|
||||
expect(wrapper.getByTestId('layer')).toHaveProperty(
|
||||
'style.transform',
|
||||
`scale(1) translateX(0px) translateY(0px)`
|
||||
)
|
||||
|
||||
pageState.pan([10, 10])
|
||||
|
||||
expect(wrapper.getByTestId('layer')).toHaveProperty(
|
||||
'style.transform',
|
||||
`scale(1) translateX(10px) translateY(10px)`
|
||||
)
|
||||
|
||||
// Page
|
||||
|
||||
expect(wrapper.getByTestId('container')).toHaveProperty(
|
||||
'style.transform',
|
||||
`
|
||||
translate(
|
||||
calc(0px - var(--tl-padding)),
|
||||
calc(0px - var(--tl-padding))
|
||||
)
|
||||
rotate(0rad)`
|
||||
)
|
||||
|
||||
page.moveShape('box1', [10, 10])
|
||||
|
||||
expect(wrapper.getByTestId('container')).toHaveProperty(
|
||||
'style.transform',
|
||||
`
|
||||
translate(
|
||||
calc(10px - var(--tl-padding)),
|
||||
calc(10px - var(--tl-padding))
|
||||
)
|
||||
rotate(0rad)`
|
||||
)
|
||||
})
|
||||
})
|
|
@ -1,4 +1,5 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import * as React from 'react'
|
||||
import type {
|
||||
TLShape,
|
||||
|
@ -11,7 +12,7 @@ import type {
|
|||
TLSnapLine,
|
||||
TLUsers,
|
||||
} from '../../types'
|
||||
import { Canvas } from '../canvas'
|
||||
import { Canvas } from '../Canvas'
|
||||
import { Inputs } from '../../inputs'
|
||||
import { useTLTheme, TLContext, TLContextType } from '../../hooks'
|
||||
import type { TLShapeUtilsMap } from '../../TLShapeUtil'
|
||||
|
@ -112,7 +113,10 @@ export interface RendererProps<T extends TLShape, M = any> extends Partial<TLCal
|
|||
* @param props
|
||||
* @returns
|
||||
*/
|
||||
export function Renderer<T extends TLShape, M extends Record<string, unknown>>({
|
||||
export const Renderer = observer(function _Renderer<
|
||||
T extends TLShape,
|
||||
M extends Record<string, unknown>
|
||||
>({
|
||||
id = 'tl',
|
||||
shapeUtils,
|
||||
page,
|
||||
|
@ -191,4 +195,4 @@ export function Renderer<T extends TLShape, M extends Record<string, unknown>>({
|
|||
/>
|
||||
</TLContext.Provider>
|
||||
)
|
||||
}
|
||||
})
|
1
packages/core/src/components/Renderer/index.tsx
Normal file
1
packages/core/src/components/Renderer/index.tsx
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './Renderer'
|
23
packages/core/src/components/SVGContainer/SVGContainer.tsx
Normal file
23
packages/core/src/components/SVGContainer/SVGContainer.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { Observer } from 'mobx-react-lite'
|
||||
import * as React from 'react'
|
||||
|
||||
interface SvgContainerProps extends React.SVGProps<SVGSVGElement> {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const SVGContainer = React.forwardRef<SVGSVGElement, SvgContainerProps>(
|
||||
function SVGContainer({ id, className = '', children, ...rest }, ref) {
|
||||
return (
|
||||
<Observer>
|
||||
{() => (
|
||||
<svg ref={ref} className={`tl-positioned-svg ${className}`} {...rest}>
|
||||
<g id={id} className="tl-centered-g">
|
||||
{children}
|
||||
</g>
|
||||
</svg>
|
||||
)}
|
||||
</Observer>
|
||||
)
|
||||
}
|
||||
)
|
1
packages/core/src/components/SVGContainer/index.ts
Normal file
1
packages/core/src/components/SVGContainer/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './SVGContainer'
|
40
packages/core/src/components/Shape/RenderedShape.tsx
Normal file
40
packages/core/src/components/Shape/RenderedShape.tsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import * as React from 'react'
|
||||
import type { TLComponentProps, TLShape } from '~types'
|
||||
import type { TLShapeUtil } from '~TLShapeUtil'
|
||||
|
||||
interface RenderedShapeProps<T extends TLShape, E extends Element, M>
|
||||
extends TLComponentProps<T, E, M> {
|
||||
shape: T
|
||||
utils: TLShapeUtil<T, E, M>
|
||||
}
|
||||
|
||||
const _RenderedShape = observer(function RenderedShape<T extends TLShape, E extends Element, M>(
|
||||
props: RenderedShapeProps<T, E, M>
|
||||
) {
|
||||
const ref = props.utils.getRef(props.shape)
|
||||
return <props.utils.Component ref={ref} {...props} />
|
||||
})
|
||||
|
||||
export const RenderedShape = React.memo(_RenderedShape, (prev, next) => {
|
||||
// If these have changed, then definitely render
|
||||
if (
|
||||
prev.isHovered !== next.isHovered ||
|
||||
prev.isSelected !== next.isSelected ||
|
||||
prev.isEditing !== next.isEditing ||
|
||||
prev.isBinding !== next.isBinding ||
|
||||
prev.isGhost !== next.isGhost ||
|
||||
prev.meta !== next.meta
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// If not, and if the shape has changed, ask the shape's class
|
||||
// whether it should render
|
||||
if (next.shape !== prev.shape) {
|
||||
return !next.utils.shouldRender(next.shape, prev.shape)
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from 'react'
|
||||
import { renderWithContext } from '~test'
|
||||
import { Shape } from './shape'
|
||||
import { Shape } from './Shape'
|
||||
import { BoxUtil, boxShape } from '~TLShapeUtil/TLShapeUtil.spec'
|
||||
import type { TLShapeUtil } from '~TLShapeUtil'
|
||||
import type { TLShape } from '~types'
|
|
@ -1,32 +1,28 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import * as React from 'react'
|
||||
import { useShapeEvents } from '~hooks'
|
||||
import type { IShapeTreeNode, TLShape } from '~types'
|
||||
import { RenderedShape } from './rendered-shape'
|
||||
import { Container } from '~components/container'
|
||||
import { RenderedShape } from './RenderedShape'
|
||||
import { Container } from '~components/Container'
|
||||
import { useTLContext } from '~hooks'
|
||||
import { useForceUpdate } from '~hooks/useForceUpdate'
|
||||
import type { TLShapeUtil } from '~TLShapeUtil'
|
||||
|
||||
interface ShapeProps<T extends TLShape, M> extends IShapeTreeNode<T, M> {
|
||||
utils: TLShapeUtil<T>
|
||||
interface ShapeProps<T extends TLShape, E extends Element, M> extends IShapeTreeNode<T, M> {
|
||||
utils: TLShapeUtil<T, E, M>
|
||||
}
|
||||
|
||||
export const Shape = React.memo(function Shape<T extends TLShape, M>({
|
||||
export const Shape = observer(function Shape<T extends TLShape, E extends Element, M>({
|
||||
shape,
|
||||
utils,
|
||||
meta,
|
||||
...rest
|
||||
}: ShapeProps<T, M>) {
|
||||
}: ShapeProps<T, E, M>) {
|
||||
const { callbacks } = useTLContext()
|
||||
const bounds = utils.getBounds(shape)
|
||||
const events = useShapeEvents(shape.id)
|
||||
|
||||
useForceUpdate()
|
||||
|
||||
return (
|
||||
<Container id={shape.id} bounds={bounds} rotation={shape.rotation}>
|
||||
<Container id={shape.id} bounds={bounds} rotation={shape.rotation} data-shape={shape.type}>
|
||||
<RenderedShape
|
||||
shape={shape}
|
||||
utils={utils as any}
|
|
@ -1,13 +1,14 @@
|
|||
import { observer } from 'mobx-react-lite'
|
||||
import * as React from 'react'
|
||||
import type { IShapeTreeNode, TLShape } from '~types'
|
||||
import { Shape } from './shape'
|
||||
import { Shape } from './Shape'
|
||||
import type { TLShapeUtilsMap } from '~TLShapeUtil'
|
||||
|
||||
interface ShapeNodeProps<T extends TLShape> extends IShapeTreeNode<T> {
|
||||
utils: TLShapeUtilsMap<TLShape>
|
||||
}
|
||||
|
||||
export const ShapeNode = React.memo(function ShapeNode<T extends TLShape>({
|
||||
export const ShapeNode = observer(function ShapeNode<T extends TLShape>({
|
||||
shape,
|
||||
utils,
|
||||
meta,
|
1
packages/core/src/components/Shape/index.ts
Normal file
1
packages/core/src/components/Shape/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './ShapeNode'
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from 'react'
|
||||
import { renderWithSvg } from '~test'
|
||||
import { ShapeIndicator } from './shape-indicator'
|
||||
import { ShapeIndicator } from './ShapeIndicator'
|
||||
import { boxShape } from '~TLShapeUtil/TLShapeUtil.spec'
|
||||
|
||||
describe('shape indicator', () => {
|
|
@ -1,17 +1,17 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import * as React from 'react'
|
||||
import type { TLShape, TLUser } from '~types'
|
||||
import { usePosition, useTLContext } from '~hooks'
|
||||
|
||||
interface IndicatorProps<T extends TLShape, M = any> {
|
||||
interface IndicatorProps<T extends TLShape, M = unknown> {
|
||||
shape: T
|
||||
meta: M extends any ? M : undefined
|
||||
meta: M extends unknown ? M : undefined
|
||||
isSelected?: boolean
|
||||
isHovered?: boolean
|
||||
user?: TLUser<T>
|
||||
}
|
||||
|
||||
export const ShapeIndicator = React.memo(function ShapeIndicator<T extends TLShape, M = any>({
|
||||
export const ShapeIndicator = observer(function ShapeIndicator<T extends TLShape, M>({
|
||||
isHovered = false,
|
||||
isSelected = false,
|
||||
shape,
|
1
packages/core/src/components/ShapeIndicator/index.ts
Normal file
1
packages/core/src/components/ShapeIndicator/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './ShapeIndicator'
|
|
@ -1,8 +1,9 @@
|
|||
import { observer } from 'mobx-react-lite'
|
||||
import * as React from 'react'
|
||||
import type { TLSnapLine } from '~types'
|
||||
import Utils from '~utils'
|
||||
|
||||
export function SnapLines({ snapLines }: { snapLines: TLSnapLine[] }) {
|
||||
export const SnapLines = observer<{ snapLines: TLSnapLine[] }>(function SnapLines({ snapLines }) {
|
||||
return (
|
||||
<>
|
||||
{snapLines.map((snapLine, i) => (
|
||||
|
@ -10,9 +11,9 @@ export function SnapLines({ snapLines }: { snapLines: TLSnapLine[] }) {
|
|||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export function SnapLine({ snapLine }: { snapLine: TLSnapLine }) {
|
||||
export const SnapLine = observer<{ snapLine: TLSnapLine }>(function SnapLine({ snapLine }) {
|
||||
const bounds = Utils.getBoundsFromPoints(snapLine)
|
||||
|
||||
return (
|
||||
|
@ -29,4 +30,4 @@ export function SnapLine({ snapLine }: { snapLine: TLSnapLine }) {
|
|||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
})
|
1
packages/core/src/components/SnapLines/index.ts
Normal file
1
packages/core/src/components/SnapLines/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './SnapLines'
|
1
packages/core/src/components/User/index.ts
Normal file
1
packages/core/src/components/User/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './User'
|
|
@ -1,5 +1,5 @@
|
|||
import * as React from 'react'
|
||||
import { User } from '~components/user/user'
|
||||
import { User } from '~components/User/User'
|
||||
import type { TLShape, TLUsers } from '~types'
|
||||
|
||||
export interface UserProps {
|
1
packages/core/src/components/Users/index.ts
Normal file
1
packages/core/src/components/Users/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './Users'
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from 'react'
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { ShapeIndicator } from '~components/shape-indicator'
|
||||
import { ShapeIndicator } from '~components/ShapeIndicator'
|
||||
import type { TLPage, TLShape, TLUsers } from '~types'
|
||||
import Utils from '~utils'
|
||||
import { useTLContext } from '~hooks'
|
1
packages/core/src/components/UsersIndicators/index.ts
Normal file
1
packages/core/src/components/UsersIndicators/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './UsersIndicators'
|
|
@ -1 +0,0 @@
|
|||
export * from './binding'
|
|
@ -1 +0,0 @@
|
|||
export * from './bounds'
|
|
@ -1 +0,0 @@
|
|||
export * from './brush'
|
|
@ -1 +0,0 @@
|
|||
export * from './canvas'
|
|
@ -1 +0,0 @@
|
|||
export * from './container'
|
|
@ -1 +0,0 @@
|
|||
export * from './error-boundary'
|
|
@ -1 +0,0 @@
|
|||
export * from './error-fallback'
|
|
@ -1 +0,0 @@
|
|||
export * from './grid'
|
|
@ -1 +0,0 @@
|
|||
export * from './handles'
|
|
@ -1,18 +0,0 @@
|
|||
import * as React from 'react'
|
||||
|
||||
interface HTMLContainerProps extends React.HTMLProps<HTMLDivElement> {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export const HTMLContainer = React.memo(
|
||||
React.forwardRef<HTMLDivElement, HTMLContainerProps>(function HTMLContainer(
|
||||
{ children, ...rest },
|
||||
ref
|
||||
) {
|
||||
return (
|
||||
<div ref={ref} className="tl-positioned-div" {...rest}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
)
|
|
@ -1 +0,0 @@
|
|||
export * from './html-container'
|
|
@ -1,3 +1,3 @@
|
|||
export * from './renderer'
|
||||
export * from './svg-container'
|
||||
export * from './html-container'
|
||||
export * from './Renderer'
|
||||
export * from './SVGContainer'
|
||||
export * from './HTMLContainer'
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
export * from './overlay'
|
|
@ -1 +0,0 @@
|
|||
export * from './page'
|
|
@ -1 +0,0 @@
|
|||
export * from './renderer'
|
|
@ -1,16 +0,0 @@
|
|||
import * as React from 'react'
|
||||
import { mockDocument, mockUtils } from '~test'
|
||||
import { render } from '@testing-library/react'
|
||||
import { Renderer } from './renderer'
|
||||
|
||||
describe('renderer', () => {
|
||||
test('mounts component without crashing', () => {
|
||||
render(
|
||||
<Renderer
|
||||
shapeUtils={mockUtils}
|
||||
page={mockDocument.page}
|
||||
pageState={mockDocument.pageState}
|
||||
/>
|
||||
)
|
||||
})
|
||||
})
|
|
@ -1 +0,0 @@
|
|||
export * from './shape-indicator'
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue