diff --git a/README.md b/README.md index 606fe52d8..7e865bc39 100644 --- a/README.md +++ b/README.md @@ -2,125 +2,44 @@ -# @tldraw/tldraw - -This package contains the [tldraw](https://tldraw.com) editor as a React component named ``. You can use this package to embed the editor in any React application. - -💕 Love this library? Consider [becoming a sponsor](https://github.com/sponsors/steveruizok?frequency=recurring&sponsor=steveruizok). +Welcome to the [tldraw](https://tldraw.com) monorepo. Here you'll find the source code for [@tldraw/tldraw](https://www.npmjs.com/package/@tldraw/tldraw), [@tldraw/core](https://www.npmjs.com/package/@tldraw/core), and the tldraw.com website. 🙌 Questions? Join the [Discord channel](https://discord.gg/SBBEVCA4PG) or start a [discussion](https://github.com/tldraw/tldraw/discussions/new). -🎨 Want to build your own tldraw-ish app instead? Try [@tldraw/core](https://github.com/tldraw/core). +💕 Love this project? Consider [becoming a sponsor](https://github.com/sponsors/steveruizok?frequency=recurring&sponsor=steveruizok). -## Installation +## Contents -Use your package manager of choice to install `@tldraw/tldraw` and its peer dependencies. +This repository is a monorepo containing two packages: -```bash -yarn add @tldraw/tldraw -# or -npm i @tldraw/tldraw -``` +- **packages/tldraw** contains the source for the [@tldraw/tldraw](https://www.npmjs.com/package/@tldraw/tldraw) package. This is an editor as a React component named ``. You can use this package to embed the tldraw editor in any React application. +- **packages/core** contains the source for the [@tldraw/core](https://www.npmjs.com/package/@tldraw/core) package. This is a renderer for React components in a canvas-style UI. It is used by `@tldraw/tldraw` as well as several other projects. -## Usage +...three apps: -Import the `tldraw` React component and use it in your app. +- **apps/www** contains the source for the [tldraw.com](https://tldraw.com) website. +- **apps/vscode** contains the source for the [tldraw VS Code extension](https://marketplace.visualstudio.com/items?itemName=tldraw-org.tldraw-vscode). +- **apps/electron** contains the source for an experimental Electron app. -```tsx -import { Tldraw } from '@tldraw/tldraw' +...and three examples: -function App() { - return -} -``` - -### Persisting the State - -You can use the `id` to persist the state in a user's browser storage. - -```tsx -import { Tldraw } from '@tldraw/tldraw' - -function App() { - return -} -``` - -### Controlling the Component through Props - -You can control the `` component through its props. - -```tsx -import { Tldraw, TDDocument } from '@tldraw/tldraw' - -function App() { - const myDocument: TDDocument = {} - - return -} -``` - -### Controlling the Component through the tldrawApp API - -You can also control the `` component imperatively through the `TldrawApp` API. - -```tsx -import { Tldraw, tldrawApp } from '@tldraw/tldraw' - -function App() { - const handleMount = React.useCallback((app: TldrawApp) => { - app.selectAll() - }, []) - - return -} -``` - -Internally, the `` component's user interface uses this API to make changes to the component's state. See the `tldrawApp` section of the [documentation](guides/documentation) for more on this API. - -### Responding to Changes - -You can respond to changes and user actions using the `onChange` callback. For more specific changes, you can also use the `onPatch`, `onCommand`, or `onPersist` callbacks. See the [documentation](guides/documentation) for more. - -```tsx -import { Tldraw, TldrawApp } from '@tldraw/tldraw' - -function App() { - const handleChange = React.useCallback((app: TldrawApp, reason: string) => { - // Do something with the change - }, []) - - return -} -``` - -## Documentation - -See the project's [documentation](/guides/documentation.md). +- **examples/core-example** is a simple example for `@tldraw/core`. +- **examples/core-example-advanced** is a second example for `@tldraw/core`. +- **examples/tldraw-example** is an example for `@tldraw/tldraw`. ## Contribution See the [contributing guide](/CONTRIBUTING.md). -## Development - -See the [development guide](/guides/development.md). - -## Example - -See the `example` folder for examples of how to use the `` component. - -## Community - -### Support +## Support Need help? Please [open an issue](https://github.com/tldraw/tldraw/issues/new) for support. -### Discussion +## Discussion Want to connect with other devs? Visit the [Discord channel](https://discord.gg/SBBEVCA4PG). -### License +## License This project is licensed under MIT. diff --git a/package.json b/package.json index 900727c4b..7e18d7920 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "tldraw", + "name": "@tldraw/monorepo", "private": true, "description": "A tiny little drawing app.", "author": "@steveruizok", diff --git a/packages/core/README.md b/packages/core/README.md index 606fe52d8..39b60ed3c 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -2,129 +2,473 @@ -# @tldraw/tldraw +# @tldraw/core -This package contains the [tldraw](https://tldraw.com) editor as a React component named ``. You can use this package to embed the editor in any React application. +This package contains the `Renderer` and core utilities used by [tldraw](https://tldraw.com). + +You can use this package to build projects like [tldraw](https://tldraw.com), where React components are rendered on a canvas user interface. Check out the [advanced example](https://core-steveruiz.vercel.app/). 💕 Love this library? Consider [becoming a sponsor](https://github.com/sponsors/steveruizok?frequency=recurring&sponsor=steveruizok). -🙌 Questions? Join the [Discord channel](https://discord.gg/SBBEVCA4PG) or start a [discussion](https://github.com/tldraw/tldraw/discussions/new). - -🎨 Want to build your own tldraw-ish app instead? Try [@tldraw/core](https://github.com/tldraw/core). - ## Installation -Use your package manager of choice to install `@tldraw/tldraw` and its peer dependencies. +Use your package manager of choice to install `@tldraw/core` and its peer dependencies. ```bash -yarn add @tldraw/tldraw +yarn add @tldraw/core # or -npm i @tldraw/tldraw +npm i @tldraw/core ``` +## Examples + +There are two examples in this repository. + +The **simple** example in the `example` folder shows a minimal use of the library. It does not do much but this should be a good reference for the API without too much else built on top. + +The **advanced** example in the `example-advanced` folder shows a more realistic use of the library. (Try it [here](https://core-steveruiz.vercel.app/)). While the fundamental patterns are the same, this example contains features such as: panning, pinching, and zooming the camera; creating, cloning, resizing, and deleting shapes; keyboard shortcuts, brush-selection; shape-snapping; undo, redo; and more. Much of the code in the advanced example comes from the [@tldraw/tldraw](https://tldraw.com) codebase. + +If you're working on an app that uses this library, I recommend referring back to the advanced example for tips on how you might implement these features for your own project. + ## Usage -Import the `tldraw` React component and use it in your app. +Import the `Renderer` React component and pass it the required props. ```tsx -import { Tldraw } from '@tldraw/tldraw' +import * as React from "react" +import { Renderer, TLShape, TLShapeUtil, Vec } from '@tldraw/core' +import { BoxShape, BoxUtil } from "./shapes/box" + +const shapeUtils = { box: new BoxUtil() } function App() { - return -} -``` + const [page, setPage] = React.useState({ + id: "page" + shapes: { + "box1": { + id: 'box1', + type: 'box', + parentId: 'page', + childIndex: 0, + point: [0, 0], + size: [100, 100], + rotation: 0, + } + }, + bindings: {} + }) -### Persisting the State + const [pageState, setPageState] = React.useState({ + id: "page", + selectedIds: [], + camera: { + point: [0,0], + zoom: 1 + } + }) -You can use the `id` to persist the state in a user's browser storage. - -```tsx -import { Tldraw } from '@tldraw/tldraw' - -function App() { - return -} -``` - -### Controlling the Component through Props - -You can control the `` component through its props. - -```tsx -import { Tldraw, TDDocument } from '@tldraw/tldraw' - -function App() { - const myDocument: TDDocument = {} - - return -} -``` - -### Controlling the Component through the tldrawApp API - -You can also control the `` component imperatively through the `TldrawApp` API. - -```tsx -import { Tldraw, tldrawApp } from '@tldraw/tldraw' - -function App() { - const handleMount = React.useCallback((app: TldrawApp) => { - app.selectAll() - }, []) - - return -} -``` - -Internally, the `` component's user interface uses this API to make changes to the component's state. See the `tldrawApp` section of the [documentation](guides/documentation) for more on this API. - -### Responding to Changes - -You can respond to changes and user actions using the `onChange` callback. For more specific changes, you can also use the `onPatch`, `onCommand`, or `onPersist` callbacks. See the [documentation](guides/documentation) for more. - -```tsx -import { Tldraw, TldrawApp } from '@tldraw/tldraw' - -function App() { - const handleChange = React.useCallback((app: TldrawApp, reason: string) => { - // Do something with the change - }, []) - - return + return () } ``` ## Documentation -See the project's [documentation](/guides/documentation.md). +### `Renderer` -## Contribution +To avoid unnecessary renders, be sure to pass "stable" values as props to the `Renderer`. Either define these values outside of the parent component, or place them in React state, or memoize them with `React.useMemo`. -See the [contributing guide](/CONTRIBUTING.md). +| Prop | Type | Description | +| ------------ | ------------------------------- | ---------------------------------------------- | +| `page` | [`TLPage`](#tlpage) | The current page object. | +| `pageState` | [`TLPageState`](#tlpagestate) | The current page's state. | +| `shapeUtils` | [`TLShapeUtils`](#tlshapeutils) | The shape utilities used to render the shapes. | -## Development +In addition to these required props, the Renderer accents many other **optional** props. -See the [development guide](/guides/development.md). +| Property | Type | Description | +| -------------------- | ----------------------------- | ----------------------------------------------------------------- | +| `containerRef` | `React.MutableRefObject` | A React ref for the container, where CSS variables will be added. | +| `theme` | `object` | An object with overrides for the Renderer's default colors. | +| `hideBounds` | `boolean` | Do not show the bounding box for selected shapes. | +| `hideHandles` | `boolean` | Do not show handles for shapes with handles. | +| `hideBindingHandles` | `boolean` | Do not show binding controls for selected shapes with bindings. | +| `hideResizeHandles` | `boolean` | Do not show resize handles for selected shapes. | +| `hideRotateHandles` | `boolean` | Do not show rotate handles for selected shapes. | +| `snapLines` | [`TLSnapLine`](#tlsnapline)[] | An array of "snap" lines. | +| `users` | `object` | A table of [`TLUser`](#tluser)s. | +| `userId` | `object` | The current user's [`TLUser`](#tluser) id. | + +The theme object accepts valid CSS colors for the following properties: + +| Property | Description | +| -------------- | ---------------------------------------------------- | +| `foreground` | The primary (usually "text") color | +| `background` | The default page's background color | +| `brushFill` | The fill color of the brush selection box | +| `brushStroke` | The stroke color of the brush selection box | +| `selectFill` | The fill color of the selection bounds | +| `selectStroke` | The stroke color of the selection bounds and handles | + +The Renderer also accepts many (optional) event callbacks. + +| Prop | Description | +| --------------------------- | ----------------------------------------------------------- | +| `onPan` | Panned with the mouse wheel | +| `onZoom` | Zoomed with the mouse wheel | +| `onPinchStart` | Began a two-pointer pinch | +| `onPinch` | Moved their pointers during a pinch | +| `onPinchEnd` | Stopped a two-pointer pinch | +| `onPointerDown` | Started pointing | +| `onPointerMove` | Moved their pointer | +| `onPointerUp` | Ended a point | +| `onPointCanvas` | Pointed the canvas | +| `onDoubleClickCanvas` | Double-pointed the canvas | +| `onRightPointCanvas` | Right-pointed the canvas | +| `onDragCanvas` | Dragged the canvas | +| `onReleaseCanvas` | Stopped pointing the canvas | +| `onHoverShape` | Moved their pointer onto a shape | +| `onUnhoverShape` | Moved their pointer off of a shape | +| `onPointShape` | Pointed a shape | +| `onDoubleClickShape` | Double-pointed a shape | +| `onRightPointShape` | Right-pointed a shape | +| `onDragShape` | Dragged a shape | +| `onReleaseShape` | Stopped pointing a shape | +| `onHoverHandle` | Moved their pointer onto a shape handle | +| `onUnhoverHandle` | Moved their pointer off of a shape handle | +| `onPointHandle` | Pointed a shape handle | +| `onDoubleClickHandle` | Double-pointed a shape handle | +| `onRightPointHandle` | Right-pointed a shape handle | +| `onDragHandle` | Dragged a shape handle | +| `onReleaseHandle` | Stopped pointing shape handle | +| `onHoverBounds` | Moved their pointer onto the selection bounds | +| `onUnhoverBounds` | Moved their pointer off of the selection bounds | +| `onPointBounds` | Pointed the selection bounds | +| `onDoubleClickBounds` | Double-pointed the selection bounds | +| `onRightPointBounds` | Right-pointed the selection bounds | +| `onDragBounds` | Dragged the selection bounds | +| `onReleaseBounds` | Stopped the selection bounds | +| `onHoverBoundsHandle` | Moved their pointer onto a selection bounds handle | +| `onUnhoverBoundsHandle` | Moved their pointer off of a selection bounds handle | +| `onPointBoundsHandle` | Pointed a selection bounds handle | +| `onDoubleClickBoundsHandle` | Double-pointed a selection bounds handle | +| `onRightPointBoundsHandle` | Right-pointed a selection bounds handle | +| `onDragBoundsHandle` | Dragged a selection bounds handle | +| `onReleaseBoundsHandle` | Stopped a selection bounds handle | +| `onShapeClone` | Clicked on a shape's clone handle | +| `onShapeChange` | A shape's component prompted a change | +| `onShapeBlur` | A shape's component was prompted a blur | +| `onRenderCountChange` | The number of rendered shapes changed | +| `onBoundsChange` | The Renderer's screen bounding box of the component changed | +| `onError` | The Renderer encountered an error | + +The `@tldraw/core` library provides types for most of the event handlers: + +| Type | +| ---------------------------- | +| `TLPinchEventHandler` | +| `TLPointerEventHandler` | +| `TLCanvasEventHandler` | +| `TLBoundsEventHandler` | +| `TLBoundsHandleEventHandler` | +| `TLShapeChangeHandler` | +| `TLShapeBlurHandler` | +| `TLShapeCloneHandler` | + +### `TLPage` + +An object describing the current page. It contains: + +| Property | Type | Description | +| ----------------- | --------------------------- | --------------------------------------------------------------------------- | +| `id` | `string` | A unique id for the page. | +| `shapes` | [`TLShape{}`](#tlshape) | A table of shapes. | +| `bindings` | [`TLBinding{}`](#tlbinding) | A table of bindings. | +| `backgroundColor` | `string` | (optional) The page's background fill color. Will also overwrite the theme. | + +### `TLPageState` + +An object describing the current page. It contains: + +| Property | Type | Description | +| -------------- | ---------- | --------------------------------------------------- | +| `id` | `string` | The corresponding page's id | +| `selectedIds` | `string[]` | An array of selected shape ids | +| `camera` | `object` | An object describing the camera state | +| `camera.point` | `number[]` | The camera's `[x, y]` coordinates | +| `camera.zoom` | `number` | The camera's zoom level | +| `pointedId` | `string` | (optional) The currently pointed shape id | +| `hoveredId` | `string` | (optional) The currently hovered shape id | +| `editingId` | `string` | (optional) The currently editing shape id | +| `bindingId` | `string` | (optional) The currently editing binding. | +| `brush` | `TLBounds` | (optional) A `Bounds` for the current selection box | + +### `TLShape` + +An object that describes a shape on the page. The shapes in your document should extend this interface with other properties. See [Shape Type](#shape-type). + +| Property | Type | Description | +| --------------------- | ---------- | ------------------------------------------------------------------------------------- | +| `id` | `string` | The shape's id. | +| `type` | `string` | The type of the shape, corresponding to the `type` of a [`TLShapeUtil`](#tlshapeutil) | +| `parentId` | `string` | The id of the shape's parent (either the current page or another shape) | +| `childIndex` | `number` | the order of the shape among its parent's children | +| `name` | `string` | the name of the shape | +| `point` | `number[]` | the shape's current `[x, y]` coordinates on the page | +| `rotation` | `number` | (optiona) The shape's current rotation in radians | +| `children` | `string[]` | (optional) An array containing the ids of this shape's children | +| `handles` | `{}` | (optional) A table of [`TLHandle`](#tlhandle) objects | +| `isGhost` | `boolean` | (optional) True if the shape is "ghosted", e.g. while deleting | +| `isLocked` | `boolean` | (optional) True if the shape is locked | +| `isHidden` | `boolean` | (optional) True if the shape is hidden | +| `isEditing` | `boolean` | (optional) True if the shape is currently editing | +| `isGenerated` | `boolean` | optional) True if the shape is generated programatically | +| `isAspectRatioLocked` | `boolean` | (optional) True if the shape's aspect ratio is locked | + +### `TLHandle` + +An object that describes a relationship between two shapes on the page. + +| Property | Type | Description | +| -------- | ---------- | --------------------------------------------- | +| `id` | `string` | An id for the handle | +| `index` | `number` | The handle's order within the shape's handles | +| `point` | `number[]` | The handle's `[x, y]` coordinates | + +When a shape with handles is the only selected shape, the `Renderer` will display its handles. You can respond to interactions with these handles using the `on + +### `TLBinding` + +An object that describes a relationship between two shapes on the page. + +| Property | Type | Description | +| -------- | -------- | -------------------------------------------- | +| `id` | `string` | A unique id for the binding | +| `fromId` | `string` | The id of the shape where the binding begins | +| `toId` | `string` | The id of the shape where the binding begins | + +### `TLSnapLine` + +A snapline is an array of points (formatted as `[x, y]`) that represent a "snapping" line. + +### `TLShapeUtil` + +The `TLShapeUtil` is an abstract class that you can extend to create utilities for your custom shapes. See the [Creating Shapes](#creating-shapes) guide to learn more. + +### `TLUser` + +A `TLUser` is the presence information for a multiplayer user. The user's pointer location and selections will be shown on the canvas. If the `TLUser`'s id matches the `Renderer`'s `userId` prop, then the user's cursor and selections will not be shown. + +| Property | Type | Description | +| --------------- | ---------- | --------------------------------------- | +| `id` | `string` | A unique id for the user | +| `color` | `string` | The user's color, used for indicators | +| `point` | `number[]` | The user's pointer location on the page | +| `selectedIds[]` | `string[]` | The user's selected shape ids | + +### `Utils` + +A general purpose utility class. See source for more. + +## Guide: Creating Shapes + +The `Renderer` component has no built-in shapes. It's up to you to define every shape that you want to see on the canvas. While these shapes are highly reusable between projects, you'll need to define them using the API described below. + +> For several example shapes, see the folder `/example/src/shapes/`. + +### Shape Type + +Your first task is to define an interface for the shape that extends `TLShape`. It must have a `type` property. + +```ts +// BoxShape.ts +import type { TLShape } from '@tldraw/core' + +export interface BoxShape extends TLShape { + type: 'box' + size: number[] +} +``` + +### Component + +Next, use `TLShapeUtil.Indicator` to create a second component for your shape's `Component`. The `Renderer` will use this component to display the shape on the canvas. + +```tsx +// BoxComponent.ts + +import * as React from 'react' +import { shapeComponent, SVGContainer } from '@tldraw/core' +import type { BoxShape } from './BoxShape' + +export const BoxComponent = TLShapeUtil.Component( + ({ shape, events, meta }, ref) => { + const color = meta.isDarkMode ? 'white' : 'black' + + return ( + + + + ) + } +) +``` + +Your component can return HTML elements or SVG elements. If your shape is returning only SVG elements, wrap it in an `SVGContainer`. If your shape returns HTML elements, wrap it in an `HTMLContainer`. Not that you must set `pointerEvents` manually on the shapes you wish to receive pointer events. + +The component will receive the following props: + +| Name | Type | Description | +| ------------------- | ---------- | ------------------------------------------------------------------ | +| `shape` | `TLShape` | The shape from `page.shapes` that is being rendered | +| `meta` | `{}` | The value provided to the `Renderer`'s `meta` prop | +| `events` | `{}` | Several pointer events that should be set on the container element | +| `isSelected` | `boolean` | The shape is selected (its `id` is in `pageState.selectedIds`) | +| `isHovered` | `boolean` | The shape is hovered (its `id` is `pageState.hoveredId`) | +| `isEditing` | `boolean` | The shape is being edited (its `id` is `pageState.editingId`) | +| `isGhost` | `boolean` | The shape is ghosted or is the child of a ghosted shape. | +| `isChildOfSelected` | `boolean` | The shape is the child of a selected shape. | +| `onShapeChange` | `Function` | The callback provided to the `Renderer`'s `onShapeChange` prop | +| `onShapeBlur` | `Function` | The callback provided to the `Renderer`'s `onShapeBlur` prop | + +### Indicator + +Next, use `TLShapeUtil.Indicator` to create a second component for your shape's `Indicator`. This component is shown when the shape is hovered or selected. Your `Indicator` must return SVG elements only. + +```tsx +// BoxIndicator.ts + +export const BoxIndicator = TLShapeUtil.Indicator(({ shape }) => { + return ( + + ) +}) +``` + +The indicator component will receive the following props: + +| Name | Type | Description | +| ------------ | --------- | -------------------------------------------------------------------------------------- | +| `shape` | `TLShape` | The shape from `page.shapes` that is being rendered | +| `meta` | {} | The value provided to the `Renderer`'s `meta` prop | +| `user` | `TLUser` | The user when shown in a multiplayer session | +| `isSelected` | `boolean` | Whether the current shape is selected (true if its `id` is in `pageState.selectedIds`) | +| `isHovered` | `boolean` | Whether the current shape is hovered (true if its `id` is `pageState.hoveredId`) | + +### ShapeUtil + +Next, create a "shape util" for your shape. This is a class that extends `TLShapeUtil`. The `Renderer` will use an instance of this class to answer questions about the shape: what it should look like, where it is on screen, whether it can rotate, etc. + +```ts +// BoxUtil.ts + +import { Utils, TLBounds, TLShapeUtil } from '@tldraw/core' +import { BoxComponent } from './BoxComponent' +import { BoxIndicator } from './BoxIndicator' +import type { BoxShape } from './BoxShape' + +export class BoxUtil extends TLShapeUtil { + Component = BoxComponent + + Indicator = BoxIndicator + + getBounds = (shape: BoxShape): TLBounds => { + const [width, height] = shape.size + + const bounds = { + minX: 0, + maxX: width, + minY: 0, + maxY: height, + width, + height, + } + + return Utils.translateBounds(bounds, shape.point) + } +} +``` + +Set the `Component` field to your component and the `Indicator` field to your indicator component. Then define the `getBounds` method. This method will receive a shape and should return a `TLBounds` object. + +You may also set the following fields: + +| Name | Type | Default | Description | +| ------------------ | --------- | ------- | ----------------------------------------------------------------------------------------------------- | +| `showCloneHandles` | `boolean` | `false` | Whether to display clone handles when the shape is the only selected shape | +| `hideBounds` | `boolean` | `false` | Whether to hide the bounds when the shape is the only selected shape | +| `isStateful` | `boolean` | `false` | Whether the shape has its own React state. When true, the shape will not be unmounted when off-screen | + +### ShapeUtils Object + +Finally, create a mapping of your project's shape utils and the `type` properties of their corresponding shapes. Pass this object to the `Renderer`'s `shapeUtils` prop. + +```tsx +// App.tsx + +const shapeUtils = { + box: new BoxUtil(), + circle: new CircleUtil(), + text: new TextUtil(), +} + +export function App() { + // ... + + return +} +``` + +## Local Development + +To start the development servers for the package and the advanced example: + +- Run `yarn` to install dependencies. +- Run `yarn start`. +- Open `localhost:5420`. + +You can also run: + +- `start:advanced` to start development servers for the package and the advanced example. +- `start:simple` to start development servers for the package and the simple example. +- `test` to execute unit tests via [Jest](https://jestjs.io). +- `docs` to build the docs via [ts-doc](https://typedoc.org/). +- `build` to build the package. ## Example -See the `example` folder for examples of how to use the `` component. +See the `example` folder or this [CodeSandbox](https://codesandbox.io/s/laughing-elion-gp0kx) example. ## Community ### Support -Need help? Please [open an issue](https://github.com/tldraw/tldraw/issues/new) for support. +Need help? Please [open an issue](https://github.com/tldraw/core/issues/new) for support. ### Discussion -Want to connect with other devs? Visit the [Discord channel](https://discord.gg/SBBEVCA4PG). +Want to connect with other devs? Visit the [Discord channel](https://discord.gg/s4FXZ6fppJ). ### License -This project is licensed under MIT. - -If you're using the library in a commercial product, please consider [becoming a sponsor](https://github.com/sponsors/steveruizok?frequency=recurring&sponsor=steveruizok). +This project is licensed under MIT. If you're using the library in a commercial product, please consider [becoming a sponsor](https://github.com/sponsors/steveruizok?frequency=recurring&sponsor=steveruizok). ## Author diff --git a/packages/core/package.json b/packages/core/package.json index 990e32f72..eed2b2193 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -27,7 +27,7 @@ "start": "node scripts/dev & yarn types:dev", "build:core": "yarn build", "build:packages": "yarn build", - "build": "node scripts/build && yarn types:build && node scripts/copy-readme", + "build": "node scripts/build && yarn types:build", "types:dev": "tsc -w --p tsconfig.build.json", "types:build": "tsc -p tsconfig.build.json && tsconfig-replace-paths -p tsconfig.build.json", "lint": "eslint src/ --ext .ts,.tsx", diff --git a/packages/core/scripts/copy-readme.js b/packages/core/scripts/copy-readme.js deleted file mode 100644 index ee14c9c7c..000000000 --- a/packages/core/scripts/copy-readme.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ -const fs = require('fs') - -const filesToCopy = ['README.md'] - -filesToCopy.forEach((file) => { - fs.copyFile(`../../${file}`, `./${file}`, (err) => { - if (err) throw err - }) -}) diff --git a/packages/core/src/components/brush/brush.test.tsx b/packages/core/src/components/brush/brush.test.tsx index ccd345680..605d24829 100644 --- a/packages/core/src/components/brush/brush.test.tsx +++ b/packages/core/src/components/brush/brush.test.tsx @@ -1,5 +1,6 @@ import * as React from 'react' import { renderWithSvg } from '~test' +import { screen } from '@testing-library/react' import { Brush } from './brush' describe('brush', () => { @@ -18,3 +19,25 @@ describe('brush', () => { ) }) }) + +test('validate attributes for brush component', () => { + renderWithSvg( + + ) + + const brush = screen.getByLabelText('brush') + expect(brush).toHaveAttribute('width', '100') + expect(brush).toHaveAttribute('height', '100') + expect(brush).toHaveAttribute('opacity', '1') + expect(brush).toHaveAttribute('x', '0') + expect(brush).toHaveAttribute('y', '0') +}) diff --git a/packages/core/src/components/brush/brush.tsx b/packages/core/src/components/brush/brush.tsx index 0cb7d1062..934d3f942 100644 --- a/packages/core/src/components/brush/brush.tsx +++ b/packages/core/src/components/brush/brush.tsx @@ -14,6 +14,7 @@ export const Brush = React.memo(function Brush({ brush }: { brush: TLBounds }): y={0} width={brush.width} height={brush.height} + aria-label="brush" /> diff --git a/packages/core/src/components/container/container.tsx b/packages/core/src/components/container/container.tsx index 0ff2ef91d..a74de0ee5 100644 --- a/packages/core/src/components/container/container.tsx +++ b/packages/core/src/components/container/container.tsx @@ -20,7 +20,12 @@ export const Container = React.memo(function Container({ const rPositioned = usePosition(bounds, rotation) return ( -
+
{children}
) diff --git a/packages/core/src/components/handles/handle.test.tsx b/packages/core/src/components/handles/handle.test.tsx new file mode 100644 index 000000000..658614cf5 --- /dev/null +++ b/packages/core/src/components/handles/handle.test.tsx @@ -0,0 +1,15 @@ +import * as React from 'react' +import { renderWithContext } from '~test' +import { screen } from '@testing-library/react' +import { Handle } from './handle' + +describe('handle', () => { + test('mounts component without crashing', () => { + renderWithContext() + }) + test('validate attributes for handle component', () => { + renderWithContext() + const handle = screen.getByLabelText('handle') + expect(handle.querySelectorAll('circle').length).toBe(2) + }) +}) diff --git a/packages/core/src/components/handles/handle.tsx b/packages/core/src/components/handles/handle.tsx index bec9101e6..545c592c5 100644 --- a/packages/core/src/components/handles/handle.tsx +++ b/packages/core/src/components/handles/handle.tsx @@ -27,7 +27,7 @@ export const Handle = React.memo(function Handle({ id, point }: HandleProps) { )} > - + diff --git a/packages/core/src/components/handles/handles.test.tsx b/packages/core/src/components/handles/handles.test.tsx index 84bc392df..52be3ed51 100644 --- a/packages/core/src/components/handles/handles.test.tsx +++ b/packages/core/src/components/handles/handles.test.tsx @@ -2,9 +2,48 @@ import * as React from 'react' import { renderWithContext } from '~test' import { Handles } from './handles' import { boxShape } from '~shape-utils/TLShapeUtil.spec' +import { screen } from '@testing-library/react' describe('handles', () => { test('mounts component without crashing', () => { renderWithContext() }) + test('validate attributes for handles component', () => { + const boxShapeWithHandles = { + ...boxShape, + handles: { + 'handle-1': { + id: 'handle-1', + index: 0, + point: [10, 10], + }, + 'handle-2': { + id: 'handle-2', + index: 1, + point: [200, 200], + }, + }, + } + + renderWithContext() + const containers = screen.getAllByLabelText('container') + const handles = screen.getAllByLabelText('handle') + + expect(containers.length).toBe(2) + expect(handles.length).toBe(2) + }) + + test.todo('Expect transform to match.') + + // Due to whitespaces, the below compare is failing + // Custom matcher should be explored to make below works + // expect(containers[0]).toHaveAttribute( + // 'style', + // `transform: + // translate( + // calc(10px - var(--tl-padding)), + // calc(10px - var(--tl-padding)) + // ) + // rotate(0rad);` + // ) }) diff --git a/packages/tldraw/README.md b/packages/tldraw/README.md index 9c78304f2..ee39f6fbb 100644 --- a/packages/tldraw/README.md +++ b/packages/tldraw/README.md @@ -6,8 +6,126 @@ This package contains the [tldraw](https://tldraw.com) editor as a React component named ``. You can use this package to embed the editor in any React application. -🎨 Want to build your own tldraw-ish app instead? Try [@tldraw/core](https://github.com/tldraw/core). - 💕 Love this library? Consider [becoming a sponsor](https://github.com/sponsors/steveruizok?frequency=recurring&sponsor=steveruizok). -For documentation, see the [tldraw](https://github.com/tldraw) repository. +🙌 Questions? Join the [Discord channel](https://discord.gg/SBBEVCA4PG) or start a [discussion](https://github.com/tldraw/tldraw/discussions/new). + +🎨 Want to build your own tldraw-ish app instead? Try the **@tldraw/core** folder instead. + +## Installation + +Use your package manager of choice to install `@tldraw/tldraw` and its peer dependencies. + +```bash +yarn add @tldraw/tldraw +# or +npm i @tldraw/tldraw +``` + +## Usage + +Import the `tldraw` React component and use it in your app. + +```tsx +import { Tldraw } from '@tldraw/tldraw' + +function App() { + return +} +``` + +### Persisting the State + +You can use the `id` to persist the state in a user's browser storage. + +```tsx +import { Tldraw } from '@tldraw/tldraw' + +function App() { + return +} +``` + +### Controlling the Component through Props + +You can control the `` component through its props. + +```tsx +import { Tldraw, TDDocument } from '@tldraw/tldraw' + +function App() { + const myDocument: TDDocument = {} + + return +} +``` + +### Controlling the Component through the tldrawApp API + +You can also control the `` component imperatively through the `TldrawApp` API. + +```tsx +import { Tldraw, tldrawApp } from '@tldraw/tldraw' + +function App() { + const handleMount = React.useCallback((app: TldrawApp) => { + app.selectAll() + }, []) + + return +} +``` + +Internally, the `` component's user interface uses this API to make changes to the component's state. See the `tldrawApp` section of the [documentation](guides/documentation) for more on this API. + +### Responding to Changes + +You can respond to changes and user actions using the `onChange` callback. For more specific changes, you can also use the `onPatch`, `onCommand`, or `onPersist` callbacks. See the [documentation](guides/documentation) for more. + +```tsx +import { Tldraw, TldrawApp } from '@tldraw/tldraw' + +function App() { + const handleChange = React.useCallback((app: TldrawApp, reason: string) => { + // Do something with the change + }, []) + + return +} +``` + +## Documentation + +See the project's [documentation](/packages/tldraw/guides/documentation.md). + +## Contribution + +See the [contributing guide](/CONTRIBUTING.md). + +## Development + +See the [development guide](/packages/tldraw/guides/development.md). + +## Example + +See the `example` folder for examples of how to use the `` component. + +## Community + +### Support + +Need help? Please [open an issue](https://github.com/tldraw/tldraw/issues/new) for support. + +### Discussion + +Want to connect with other devs? Visit the [Discord channel](https://discord.gg/SBBEVCA4PG). + +### License + +This project is licensed under MIT. + +If you're using the library in a commercial product, please consider [becoming a sponsor](https://github.com/sponsors/steveruizok?frequency=recurring&sponsor=steveruizok). + +## Author + +- [@steveruizok](https://twitter.com/steveruizok) diff --git a/guides/development.md b/packages/tldraw/guides/development.md similarity index 100% rename from guides/development.md rename to packages/tldraw/guides/development.md diff --git a/guides/documentation.md b/packages/tldraw/guides/documentation.md similarity index 100% rename from guides/documentation.md rename to packages/tldraw/guides/documentation.md diff --git a/guides/publishing.md b/packages/tldraw/guides/publishing.md similarity index 100% rename from guides/publishing.md rename to packages/tldraw/guides/publishing.md diff --git a/tsconfig.base.json b/tsconfig.base.json index beda42fbd..c055d1da9 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -29,6 +29,6 @@ "stripInternal": true, "target": "es6", "typeRoots": ["node_modules/@types", "node_modules/jest"], - "types": ["node", "jest"] + "types": ["node", "jest", "@testing-library/jest-dom", "@testing-library/react"] } }