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:
Noah Shipley 2021-11-28 04:05:40 -06:00 committed by GitHub
parent a9458b20fb
commit 98cc0d4cac
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
141 changed files with 1949 additions and 1983 deletions

View file

@ -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}
/>

View file

@ -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 => {

View file

@ -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))
}

View file

@ -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 => {

View file

@ -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",

View file

@ -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>
)
}
})

View file

@ -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,
},
}
}

View file

@ -1,6 +1,6 @@
import type { TLShape } from '@tldraw/core'
export interface RectShape extends TLShape {
type: 'box'
type: 'rect'
size: number[]
}

View file

@ -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

View 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)
}
}

View file

@ -7,6 +7,8 @@
"baseUrl": ".",
"rootDir": "src",
"emitDeclarationOnly": false,
"experimentalDecorators": true,
"useDefineForClassFields": true,
"paths": {
"*": ["src/*"],
"@tldraw/core": ["../packages/core"]

View file

@ -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"
}
}

View file

@ -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": [

View file

@ -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) => {

View file

@ -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)

View file

@ -0,0 +1 @@
export * from './Binding'

View file

@ -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,

View file

@ -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 (

View file

@ -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'}

View file

@ -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>
)
}
})

View file

@ -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

View file

@ -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,

View file

@ -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,

View file

@ -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 (

View file

@ -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', () => {

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -0,0 +1 @@
export * from './Bounds'

View file

@ -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', () => {

View file

@ -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>

View file

@ -0,0 +1 @@
export * from './Brush'

View file

@ -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
}}
/>
)
})

View file

@ -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>
)
}
})

View file

@ -0,0 +1 @@
export * from './Canvas'

View file

@ -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>

View file

@ -0,0 +1 @@
export * from './Container'

View file

@ -0,0 +1 @@
export * from './ErrorBoundary'

View file

@ -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', () => {

View file

@ -0,0 +1 @@
export * from './ErrorFallback'

View file

@ -0,0 +1 @@
export * from './Grid'

View 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>
)
}
)

View file

@ -0,0 +1 @@
export * from './HTMLContainer'

View file

@ -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', () => {

View file

@ -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 (

View file

@ -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'

View file

@ -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 {

View file

@ -0,0 +1 @@
export * from './Handles'

View file

@ -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>
)
}
})

View file

@ -0,0 +1 @@
export * from './Overlay'

View file

@ -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', () => {

View file

@ -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
/>
)}

View file

@ -0,0 +1 @@
export * from './Page'

View 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)`
)
})
})

View file

@ -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>
)
}
})

View file

@ -0,0 +1 @@
export * from './Renderer'

View 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>
)
}
)

View file

@ -0,0 +1 @@
export * from './SVGContainer'

View 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
})

View file

@ -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'

View file

@ -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}

View file

@ -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,

View file

@ -0,0 +1 @@
export * from './ShapeNode'

View file

@ -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', () => {

View file

@ -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,

View file

@ -0,0 +1 @@
export * from './ShapeIndicator'

View file

@ -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 }) {
))}
</>
)
}
})

View file

@ -0,0 +1 @@
export * from './SnapLines'

View file

@ -0,0 +1 @@
export * from './User'

View file

@ -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 {

View file

@ -0,0 +1 @@
export * from './Users'

View file

@ -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'

View file

@ -0,0 +1 @@
export * from './UsersIndicators'

View file

@ -1 +0,0 @@
export * from './binding'

View file

@ -1 +0,0 @@
export * from './bounds'

View file

@ -1 +0,0 @@
export * from './brush'

View file

@ -1 +0,0 @@
export * from './canvas'

View file

@ -1 +0,0 @@
export * from './container'

View file

@ -1 +0,0 @@
export * from './error-boundary'

View file

@ -1 +0,0 @@
export * from './error-fallback'

View file

@ -1 +0,0 @@
export * from './grid'

View file

@ -1 +0,0 @@
export * from './handles'

View file

@ -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>
)
})
)

View file

@ -1 +0,0 @@
export * from './html-container'

View file

@ -1,3 +1,3 @@
export * from './renderer'
export * from './svg-container'
export * from './html-container'
export * from './Renderer'
export * from './SVGContainer'
export * from './HTMLContainer'

View file

@ -1 +0,0 @@
export * from './overlay'

View file

@ -1 +0,0 @@
export * from './page'

View file

@ -1 +0,0 @@
export * from './renderer'

View file

@ -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}
/>
)
})
})

View file

@ -1 +0,0 @@
export * from './shape-indicator'

Some files were not shown because too many files have changed in this diff Show more