[improvement] Migrations (#217)

* Add better migrations

* Improves migrations

* rename dev to example

* renames migrate

* Removes workers from git

* Remove rogue dependency

* Fix dropdown navigation by keyboard
This commit is contained in:
Steve Ruiz 2021-11-04 15:48:39 +00:00 committed by GitHub
parent cb777c85d1
commit fe2e3c81fe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
53 changed files with 516 additions and 271 deletions

4
.gitignore vendored
View file

@ -8,3 +8,7 @@ docs/
.DS_Store
coverage
*.log
www/public/worker-*
www/public/sw.js
www/public/sw.js.map

193
README.md
View file

@ -1,19 +1,23 @@
<div style="text-align: center; transform: scale(.5);">
<img src="card-repo.png"/>
</div>
# @tldraw/tldraw
> `This library is not yet released and these docs are partially out of date!`
This package contains the [tldraw](https://tldraw.com) editor as a React component named `<TLDraw>`. You can use this package to embed the editor in any React application.
This package contains the [tldraw](https://tldraw.com) editor as a standalone React component.
🎨 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).
## Installation
```bash
npm i @tldraw/tldraw
```
or
Use your package manager of choice to install `@tldraw/core` and its peer dependencies.
```bash
yarn add @tldraw/tldraw
# or
npm i @tldraw/tldraw
```
## Usage
@ -28,31 +32,70 @@ function App() {
}
```
You can control the `TLDraw` component through props:
```tsx
import { TLDraw, TLDrawDocument } from '@tldraw/tldraw'
function App() {
const myDocument: TLDrawDocument = {}
return <TLDraw document={document} />
}
```
Or imperatively through the `TLDrawState` instance:
```tsx
import { TLDraw, TLDrawState } from '@tldraw/tldraw'
function App() {
const handleMount = React.useCallback((tlstate: TLDrawState) => {
const myDocument: TLDrawDocument = {}
tlstate.loadDocument(myDocument).selectAll()
}, [])
return <TLDraw onMount={handleMount} />
}
```
## Documentation
### `TLDraw`
The `TLDraw` React component is the [tldraw](https://tldraw.com) editor exported as a standalone component. You can control the editor through props, or through the `TLDrawState`'s imperative API.
The `TLDraw` React component is the [tldraw](https://tldraw.com) editor exported as a standalone component. You can control the editor through props, or through the `TLDrawState`'s imperative API. **All props are optional.**
| Prop | Type | Description |
| --------------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id` | `string` | (optional) An id under which to persist the component's state. |
| `document` | `TLDrawDocument` | (optional) An initial [`TLDrawDocument`](#tldrawdocument) object. |
| `currentPageId` | `string` | (optional) A current page id, referencing the `TLDrawDocument` object provided via the `document` prop. |
| `onMount` | `(TLDrawState) => void` | (optional) A callback function that will be called when the editor first mounts, receiving the current `TLDrawState`. |
| `onChange` | `(TLDrawState, string) => void` | (optional) A callback function that will be called whenever the `TLDrawState` updates. The update will include the current `TLDrawState` and the reason for the change. |
| Prop | Type | Description |
| --------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `id` | `string` | An id under which to persist the component's state. |
| `document` | `TLDrawDocument` | An initial [`TLDrawDocument`](#tldrawdocument) object. |
| `currentPageId` | `string` | A current page id, referencing the `TLDrawDocument` object provided via the `document` prop. |
| `onMount` | `Function` | A callback function that will be called when the editor first mounts, receiving the current `TLDrawState`. |
| `onChange` | `Function` | A callback function that will be called whenever the `TLDrawState` updates. The update will include the current `TLDrawState` and the reason for the change. |
| `onUserChange` | `Function` | A callback function that will be fired when the user's "presence" information changes. |
| `autofocus` | `boolean` | Whether the editor should immediately receive focus. Defaults to true. |
| `showMenu` | `boolean` | Whether to show the menu. |
| `showPages` | `boolean` | Whether to show the pages menu. |
| `showStyles` | `boolean` | Whether to show the styles menu. |
| `showTools` | `boolean` | Whether to show the tools. |
| `showUI` | `boolean` | Whether to show any UI other than the canvas. |
### `TLDrawDocument`
A `TLDrawDocument` is an object with three properties:
- `id` - A unique ID for this document
- `pages` - A table of `TLPage` objects
- `pages` - A table of `TLDrawPage` objects
- `pageStates` - A table of `TLPageState` objects
- `version` - The document's version, used internally for migrations.
```ts
import { TLDrawDocument, TLDrawState } from '@tldraw/tldraw'
const tldocument: TLDrawDocument = {
id: 'doc',
version: TLDrawState.version,
pages: {
page1: {
id: 'page1',
@ -74,30 +117,30 @@ const tldocument: TLDrawDocument = {
}
```
**Important:** In the `pages` object, each `TLPage` object must be keyed under its `id` property. Likewise, each `TLPageState` object must be keyed under its `id`. In addition, each `TLPageState` object must have an `id` that matches its corresponding page.
**Tip:** TLDraw is built [@tldraw/core](https://github.com/tldraw/core). The pages and pagestates in TLDraw are just objects containing `TLPage` and `TLPageState` objects from the core library. For more about these types, check out the [@tldraw/core](https://github.com/tldraw/core) documentation.
In the example above, the page above with the id `page1`is at `tldocument.pages["page1"]`. Its corresponding page state has the same id (`page1`) and is at `tldocument.pageStates["page1"]`.
**Important:** In the `pages` object, each `TLPage` object must be keyed under its `id` property. Likewise, each `TLPageState` object must be keyed under its `id`. In addition, each `TLPageState` object must have an `id` that matches its corresponding page.
### Shapes
Your `TLPage` objects may include shapes: objects that fit one of the `TLDrawShape` interfaces listed below. All `TLDrawShapes` extends a common interface:
| Property | Type | Description |
| --------------------- | ------------ | --------------------------------------------------------------- |
| `id` | `string` | A unique ID for the shape. |
| `name` | `string` | The shape's name. |
| `type` | `string` | The shape's type. |
| `parentId` | `string` | The ID of the shape's parent (a shape or its page). |
| `childIndex` | `number` | The shape's order within its parent's children, indexed from 1. |
| `point` | `number[]` | The `[x, y]` position of the shape. |
| `rotation` | `number[]` | (optional) The shape's rotation in radians. |
| `children` | `string[]` | (optional) The shape's child shape ids. |
| `handles` | `TLHandle{}` | (optional) A table of `TLHandle` objects. |
| `isLocked` | `boolean` | True if the shape is locked. |
| `isHidden` | `boolean` | True if the shape is hidden. |
| `isEditing` | `boolean` | True if the shape is currently editing. |
| `isGenerated` | `boolean` | True if the shape is generated. |
| `isAspectRatioLocked` | `boolean` | True if the shape's aspect ratio is locked. |
| Property | Type | Description |
| --------------------- | ---------------- | --------------------------------------------------------------- |
| `id` | `string` | A unique ID for the shape. |
| `name` | `string` | The shape's name. |
| `type` | `string` | The shape's type. |
| `parentId` | `string` | The ID of the shape's parent (a shape or its page). |
| `childIndex` | `number` | The shape's order within its parent's children, indexed from 1. |
| `point` | `number[]` | The `[x, y]` position of the shape. |
| `rotation` | `number[]` | (optional) The shape's rotation in radians. |
| `children` | `string[]` | (optional) The shape's child shape ids. |
| `handles` | `TLDrawHandle{}` | (optional) A table of `TLHandle` objects. |
| `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. |
| `isAspectRatioLocked` | `boolean` | (optional) True if the shape's aspect ratio is locked. |
> **Important:** In order for re-ordering to work correctly, a shape's `childIndex` values _must_ start from 1, not 0. The page or parent shape's "bottom-most" child should have a `childIndex` of 1.
@ -110,38 +153,98 @@ The `ShapeStyle` object is a common style API for all shapes.
| `color` | `ColorStyle` | The shape's color. |
| `isFilled` | `boolean` | (optional) True if the shape is filled. |
#### Draw
#### `DrawShape`
A hand-drawn line.
| Property | Type | Description |
| -------- | ------------ | ----------------------------------------- |
| `points` | `number[][]` | An array of points as `[x, y, pressure]`. |
##### Rectangle
##### `RectangleShape`
A rectangular shape.
| Property | Type | Description |
| -------- | ---------- | --------------------------------------- |
| `size` | `number[]` | The `[width, height]` of the rectangle. |
#### Ellipse
#### `EllipseShape`
An elliptical shape.
| Property | Type | Description |
| -------- | ---------- | ----------------------------------- |
| `radius` | `number[]` | The `[x, y]` radius of the ellipse. |
#### Arrow
#### `ArrowShape`
| Property | Type | Description |
| --------- | -------- | ----------------------------------------------------------------------- |
| `handles` | `object` | An object with three `TLHandle` properties: `start`, `end`, and `bend`. |
An arrow that can connect shapes.
#### Text
| Property | Type | Description |
| ------------- | -------- | ----------------------------------------------------------------------- |
| `handles` | `object` | An object with three `TLHandle` properties: `start`, `end`, and `bend`. |
| `decorations` | `object` | An object with two properties `start`, `end`, and `bend`. |
#### `TextShape`
A line of text.
| Property | Type | Description |
| -------- | -------- | ------------------------- |
| `text` | `string` | The shape's text content. |
## Development
#### `StickyShape`
### Running unit tests
A sticky note.
Run `nx test tldraw` to execute the unit tests via [Jest](https://jestjs.io).
| Property | Type | Description |
| -------- | -------- | ------------------------- |
| `text` | `string` | The shape's text content. |
### Bindings
A binding is a connection **from** one shape and **to** another shape. At the moment, only arrows may be bound "from". Most shapes may be bound "to", except other `ArrowShape` and `DrawShape`s.
| Property | Type | Description |
| ---------- | ---------------- | -------------------------------------------------------- |
| `id` | `string` | The binding's own unique ID. |
| `fromId` | `string` | The id of the `ArrowShape` that the binding is bound to. |
| `toId` | `string` | The id of the other shape that the binding is bound to. |
| `handleId` | `start` or `end` | The connected arrow handle. |
| `distance` | `number` | The distance from the bound point. |
| `point` | `number[]` | A normalized point representing the bound point. |
## Local Development
- Run `yarn` to install dependencies.
- Run `yarn start` to start the development server for the package and for the example.
- Open `localhost:5000` to view the example project.
- Run `yarn test` to execute unit tests via [Jest](https://jestjs.io).
- Run `yarn docs` to build the docs via [ts-doc](https://typedoc.org/).
## Example
See the `example` folder.
## 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/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).
## Author
- [@steveruizok](https://twitter.com/steveruizok)

View file

@ -1,6 +0,0 @@
# @tldraw/dev
A very fast dev server.
You probably do not need to start the server here: it is started as
part of `yarn start` in the root directory.

6
example/README.md Normal file
View file

@ -0,0 +1,6 @@
# @tldraw/tldraw-example
An example for @tldraw/tldraw with a very fast dev server.
**Note:** You probably do not need to start the server here: it is started as
part of `yarn start` in the root directory.

View file

@ -1,8 +1,8 @@
{
"name": "@tldraw/dev",
"name": "@tldraw/tldraw-example",
"version": "0.1.1",
"private": true,
"description": "A tiny little drawing app (dev)",
"description": "A tiny little drawing app example.",
"author": "@steveruizok",
"license": "MIT",
"keywords": [
@ -18,14 +18,10 @@
"src"
],
"sideEffects": false,
"dependencies": {
"@liveblocks/client": "^0.12.1",
"@liveblocks/react": "^0.12.1",
"@tldraw/tldraw": "^0.1.1",
"dependencies": {},
"peerDependencies": {
"react": ">=16.8",
"react-dom": "^16.8 || ^17.0",
"react-router": "^5.2.1",
"react-router-dom": "^5.3.0"
"react-dom": "^16.8 || ^17.0"
},
"devDependencies": {
"@types/node": "^14.14.35",
@ -36,7 +32,12 @@
"create-serve": "1.0.1",
"esbuild": "^0.13.8",
"rimraf": "3.0.2",
"typescript": "4.2.3"
"typescript": "4.2.3",
"@liveblocks/client": "^0.12.1",
"@liveblocks/react": "^0.12.1",
"@tldraw/tldraw": "^0.1.1",
"react-router": "^5.2.1",
"react-router-dom": "^5.3.0"
},
"gitHead": "a7dac0f83ad998e205c2aab58182cb4ba4e099a6"
}

View file

@ -40,24 +40,8 @@ function TLDrawWrapper() {
const doc = useObject<{ uuid: string; document: TLDrawDocument }>('doc', {
uuid: docId,
document: {
...TLDrawState.defaultDocument,
id: 'test-room',
pages: {
page: {
id: 'page',
shapes: {},
bindings: {},
},
},
pageStates: {
page: {
id: 'page',
selectedIds: [],
camera: {
point: [0, 0],
zoom: 1,
},
},
},
},
})

View file

@ -10,7 +10,7 @@
"license": "MIT",
"workspaces": [
"packages/tldraw",
"dev",
"example",
"www"
],
"scripts": {
@ -45,9 +45,6 @@
"typedoc": "^0.22.3",
"typescript": "^4.4.2"
},
"dependencies": {
"www": "0.0.133"
},
"prettier": {
"trailingComma": "es5",
"singleQuote": true,

View file

@ -1,19 +1,23 @@
<div style="text-align: center; transform: scale(.5);">
<img src="card-repo.png"/>
</div>
# @tldraw/tldraw
> `This library is not yet released and these docs are partially out of date!`
This package contains the [tldraw](https://tldraw.com) editor as a React component named `<TLDraw>`. You can use this package to embed the editor in any React application.
This package contains the [tldraw](https://tldraw.com) editor as a standalone React component.
🎨 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).
## Installation
```bash
npm i @tldraw/tldraw
```
or
Use your package manager of choice to install `@tldraw/core` and its peer dependencies.
```bash
yarn add @tldraw/tldraw
# or
npm i @tldraw/tldraw
```
## Usage
@ -28,31 +32,70 @@ function App() {
}
```
You can control the `TLDraw` component through props:
```tsx
import { TLDraw, TLDrawDocument } from '@tldraw/tldraw'
function App() {
const myDocument: TLDrawDocument = {}
return <TLDraw document={document} />
}
```
Or imperatively through the `TLDrawState` instance:
```tsx
import { TLDraw, TLDrawState } from '@tldraw/tldraw'
function App() {
const handleMount = React.useCallback((tlstate: TLDrawState) => {
const myDocument: TLDrawDocument = {}
tlstate.loadDocument(myDocument).selectAll()
}, [])
return <TLDraw onMount={handleMount} />
}
```
## Documentation
### `TLDraw`
The `TLDraw` React component is the [tldraw](https://tldraw.com) editor exported as a standalone component. You can control the editor through props, or through the `TLDrawState`'s imperative API.
The `TLDraw` React component is the [tldraw](https://tldraw.com) editor exported as a standalone component. You can control the editor through props, or through the `TLDrawState`'s imperative API. **All props are optional.**
| Prop | Type | Description |
| --------------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id` | `string` | (optional) An id under which to persist the component's state. |
| `document` | `TLDrawDocument` | (optional) An initial [`TLDrawDocument`](#tldrawdocument) object. |
| `currentPageId` | `string` | (optional) A current page id, referencing the `TLDrawDocument` object provided via the `document` prop. |
| `onMount` | `(TLDrawState) => void` | (optional) A callback function that will be called when the editor first mounts, receiving the current `TLDrawState`. |
| `onChange` | `(TLDrawState, string) => void` | (optional) A callback function that will be called whenever the `TLDrawState` updates. The update will include the current `TLDrawState` and the reason for the change. |
| Prop | Type | Description |
| --------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `id` | `string` | An id under which to persist the component's state. |
| `document` | `TLDrawDocument` | An initial [`TLDrawDocument`](#tldrawdocument) object. |
| `currentPageId` | `string` | A current page id, referencing the `TLDrawDocument` object provided via the `document` prop. |
| `onMount` | `Function` | A callback function that will be called when the editor first mounts, receiving the current `TLDrawState`. |
| `onChange` | `Function` | A callback function that will be called whenever the `TLDrawState` updates. The update will include the current `TLDrawState` and the reason for the change. |
| `onUserChange` | `Function` | A callback function that will be fired when the user's "presence" information changes. |
| `autofocus` | `boolean` | Whether the editor should immediately receive focus. Defaults to true. |
| `showMenu` | `boolean` | Whether to show the menu. |
| `showPages` | `boolean` | Whether to show the pages menu. |
| `showStyles` | `boolean` | Whether to show the styles menu. |
| `showTools` | `boolean` | Whether to show the tools. |
| `showUI` | `boolean` | Whether to show any UI other than the canvas. |
### `TLDrawDocument`
A `TLDrawDocument` is an object with three properties:
- `id` - A unique ID for this document
- `pages` - A table of `TLPage` objects
- `pages` - A table of `TLDrawPage` objects
- `pageStates` - A table of `TLPageState` objects
- `version` - The document's version, used internally for migrations.
```ts
import { TLDrawDocument, TLDrawState } from '@tldraw/tldraw'
const tldocument: TLDrawDocument = {
id: 'doc',
version: TLDrawState.version,
pages: {
page1: {
id: 'page1',
@ -74,30 +117,30 @@ const tldocument: TLDrawDocument = {
}
```
**Important:** In the `pages` object, each `TLPage` object must be keyed under its `id` property. Likewise, each `TLPageState` object must be keyed under its `id`. In addition, each `TLPageState` object must have an `id` that matches its corresponding page.
**Tip:** TLDraw is built [@tldraw/core](https://github.com/tldraw/core). The pages and pagestates in TLDraw are just objects containing `TLPage` and `TLPageState` objects from the core library. For more about these types, check out the [@tldraw/core](https://github.com/tldraw/core) documentation.
In the example above, the page above with the id `page1`is at `tldocument.pages["page1"]`. Its corresponding page state has the same id (`page1`) and is at `tldocument.pageStates["page1"]`.
**Important:** In the `pages` object, each `TLPage` object must be keyed under its `id` property. Likewise, each `TLPageState` object must be keyed under its `id`. In addition, each `TLPageState` object must have an `id` that matches its corresponding page.
### Shapes
Your `TLPage` objects may include shapes: objects that fit one of the `TLDrawShape` interfaces listed below. All `TLDrawShapes` extends a common interface:
| Property | Type | Description |
| --------------------- | ------------ | --------------------------------------------------------------- |
| `id` | `string` | A unique ID for the shape. |
| `name` | `string` | The shape's name. |
| `type` | `string` | The shape's type. |
| `parentId` | `string` | The ID of the shape's parent (a shape or its page). |
| `childIndex` | `number` | The shape's order within its parent's children, indexed from 1. |
| `point` | `number[]` | The `[x, y]` position of the shape. |
| `rotation` | `number[]` | (optional) The shape's rotation in radians. |
| `children` | `string[]` | (optional) The shape's child shape ids. |
| `handles` | `TLHandle{}` | (optional) A table of `TLHandle` objects. |
| `isLocked` | `boolean` | True if the shape is locked. |
| `isHidden` | `boolean` | True if the shape is hidden. |
| `isEditing` | `boolean` | True if the shape is currently editing. |
| `isGenerated` | `boolean` | True if the shape is generated. |
| `isAspectRatioLocked` | `boolean` | True if the shape's aspect ratio is locked. |
| Property | Type | Description |
| --------------------- | ---------------- | --------------------------------------------------------------- |
| `id` | `string` | A unique ID for the shape. |
| `name` | `string` | The shape's name. |
| `type` | `string` | The shape's type. |
| `parentId` | `string` | The ID of the shape's parent (a shape or its page). |
| `childIndex` | `number` | The shape's order within its parent's children, indexed from 1. |
| `point` | `number[]` | The `[x, y]` position of the shape. |
| `rotation` | `number[]` | (optional) The shape's rotation in radians. |
| `children` | `string[]` | (optional) The shape's child shape ids. |
| `handles` | `TLDrawHandle{}` | (optional) A table of `TLHandle` objects. |
| `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. |
| `isAspectRatioLocked` | `boolean` | (optional) True if the shape's aspect ratio is locked. |
> **Important:** In order for re-ordering to work correctly, a shape's `childIndex` values _must_ start from 1, not 0. The page or parent shape's "bottom-most" child should have a `childIndex` of 1.
@ -110,38 +153,98 @@ The `ShapeStyle` object is a common style API for all shapes.
| `color` | `ColorStyle` | The shape's color. |
| `isFilled` | `boolean` | (optional) True if the shape is filled. |
#### Draw
#### `DrawShape`
A hand-drawn line.
| Property | Type | Description |
| -------- | ------------ | ----------------------------------------- |
| `points` | `number[][]` | An array of points as `[x, y, pressure]`. |
##### Rectangle
##### `RectangleShape`
A rectangular shape.
| Property | Type | Description |
| -------- | ---------- | --------------------------------------- |
| `size` | `number[]` | The `[width, height]` of the rectangle. |
#### Ellipse
#### `EllipseShape`
An elliptical shape.
| Property | Type | Description |
| -------- | ---------- | ----------------------------------- |
| `radius` | `number[]` | The `[x, y]` radius of the ellipse. |
#### Arrow
#### `ArrowShape`
| Property | Type | Description |
| --------- | -------- | ----------------------------------------------------------------------- |
| `handles` | `object` | An object with three `TLHandle` properties: `start`, `end`, and `bend`. |
An arrow that can connect shapes.
#### Text
| Property | Type | Description |
| ------------- | -------- | ----------------------------------------------------------------------- |
| `handles` | `object` | An object with three `TLHandle` properties: `start`, `end`, and `bend`. |
| `decorations` | `object` | An object with two properties `start`, `end`, and `bend`. |
#### `TextShape`
A line of text.
| Property | Type | Description |
| -------- | -------- | ------------------------- |
| `text` | `string` | The shape's text content. |
## Development
#### `StickyShape`
### Running unit tests
A sticky note.
Run `nx test tldraw` to execute the unit tests via [Jest](https://jestjs.io).
| Property | Type | Description |
| -------- | -------- | ------------------------- |
| `text` | `string` | The shape's text content. |
### Bindings
A binding is a connection **from** one shape and **to** another shape. At the moment, only arrows may be bound "from". Most shapes may be bound "to", except other `ArrowShape` and `DrawShape`s.
| Property | Type | Description |
| ---------- | ---------------- | -------------------------------------------------------- |
| `id` | `string` | The binding's own unique ID. |
| `fromId` | `string` | The id of the `ArrowShape` that the binding is bound to. |
| `toId` | `string` | The id of the other shape that the binding is bound to. |
| `handleId` | `start` or `end` | The connected arrow handle. |
| `distance` | `number` | The distance from the bound point. |
| `point` | `number[]` | A normalized point representing the bound point. |
## Local Development
- Run `yarn` to install dependencies.
- Run `yarn start` to start the development server for the package and for the example.
- Open `localhost:5000` to view the example project.
- Run `yarn test` to execute unit tests via [Jest](https://jestjs.io).
- Run `yarn docs` to build the docs via [ts-doc](https://typedoc.org/).
## Example
See the `example` folder.
## 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/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).
## Author
- [@steveruizok](https://twitter.com/steveruizok)

View file

@ -50,7 +50,7 @@
"@tldraw/vec": "^0.1.3",
"perfect-freehand": "^1.0.16",
"react-hotkeys-hook": "^3.4.0",
"rko": "^0.5.25"
"rko": "^0.6.0"
},
"devDependencies": {
"tsconfig-replace-paths": "^0.0.5"

View file

@ -4,6 +4,7 @@ import { Tooltip } from '~components/Tooltip'
import styled from '~styles'
export interface ToolButtonProps {
onClick?: () => void
onSelect?: () => void
onDoubleClick?: () => void
isActive?: boolean
@ -12,12 +13,13 @@ export interface ToolButtonProps {
}
export const ToolButton = React.forwardRef<HTMLButtonElement, ToolButtonProps>(
({ onSelect, onDoubleClick, isActive = false, variant, children, ...rest }, ref) => {
({ onSelect, onClick, onDoubleClick, isActive = false, variant, children, ...rest }, ref) => {
return (
<StyledToolButton
ref={ref}
isActive={isActive}
variant={variant}
onClick={onClick}
onPointerDown={onSelect}
onDoubleClick={onDoubleClick}
bp={breakpoints}

View file

@ -4,14 +4,14 @@ import { strokes } from '~shape-utils'
import { useTheme, useTLDrawContext } from '~hooks'
import type { Data, ColorStyle } from '~types'
import CircleIcon from '~components/icons/CircleIcon'
import { DMContent, DMRadioItem, DMTriggerIcon } from '~components/DropdownMenu'
import { DMContent, DMTriggerIcon } from '~components/DropdownMenu'
import { BoxIcon } from '~components/icons'
import { IconButton } from '~components/IconButton'
import { ToolButton } from '~components/ToolButton'
import { Tooltip } from '~components/Tooltip'
const selectColor = (s: Data) => s.appState.selectedStyle.color
const preventEvent = (e: Event) => e.preventDefault()
export const ColorMenu = React.memo((): JSX.Element => {
const { theme } = useTheme()
const { tlstate, useSelector } = useTLDrawContext()
@ -25,17 +25,18 @@ export const ColorMenu = React.memo((): JSX.Element => {
</DMTriggerIcon>
<DMContent variant="grid">
{Object.keys(strokes[theme]).map((colorStyle: string) => (
<ToolButton
key={colorStyle}
variant="icon"
isActive={color === colorStyle}
onSelect={() => tlstate.style({ color: colorStyle as ColorStyle })}
>
<BoxIcon
fill={strokes[theme][colorStyle as ColorStyle]}
stroke={strokes[theme][colorStyle as ColorStyle]}
/>
</ToolButton>
<DropdownMenu.Item key={colorStyle} onSelect={preventEvent} asChild>
<ToolButton
variant="icon"
isActive={color === colorStyle}
onClick={() => tlstate.style({ color: colorStyle as ColorStyle })}
>
<BoxIcon
fill={strokes[theme][colorStyle as ColorStyle]}
stroke={strokes[theme][colorStyle as ColorStyle]}
/>
</ToolButton>
</DropdownMenu.Item>
))}
</DMContent>
</DropdownMenu.Root>

View file

@ -15,6 +15,8 @@ const dashes = {
const selectDash = (s: Data) => s.appState.selectedStyle.dash
const preventEvent = (e: Event) => e.preventDefault()
export const DashMenu = React.memo((): JSX.Element => {
const { tlstate, useSelector } = useTLDrawContext()
@ -24,15 +26,16 @@ export const DashMenu = React.memo((): JSX.Element => {
<DropdownMenu.Root dir="ltr">
<DMTriggerIcon>{dashes[dash]}</DMTriggerIcon>
<DMContent variant="horizontal">
{Object.keys(DashStyle).map((dashStyle) => (
<ToolButton
key={dashStyle}
variant="icon"
isActive={dash === dashStyle}
onSelect={() => tlstate.style({ dash: dashStyle as DashStyle })}
>
{dashes[dashStyle as DashStyle]}
</ToolButton>
{Object.values(DashStyle).map((dashStyle) => (
<DropdownMenu.Item key={dashStyle} onSelect={preventEvent} asChild>
<ToolButton
variant="icon"
isActive={dash === dashStyle}
onClick={() => tlstate.style({ dash: dashStyle as DashStyle })}
>
{dashes[dashStyle as DashStyle]}
</ToolButton>
</DropdownMenu.Item>
))}
</DMContent>
</DropdownMenu.Root>

View file

@ -128,6 +128,8 @@ export const StyledDialogOverlay = styled(Dialog.Overlay, {
right: 0,
bottom: 0,
left: 0,
width: '100%',
height: '100%',
})
function DialogAction({ onSelect, ...rest }: RowButtonProps) {

View file

@ -2,7 +2,7 @@ import * as React from 'react'
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import { Data, SizeStyle } from '~types'
import { useTLDrawContext } from '~hooks'
import { DMContent, DMTriggerIcon } from '~components/DropdownMenu'
import { DMContent, DMItem, DMTriggerIcon } from '~components/DropdownMenu'
import { ToolButton } from '~components/ToolButton'
import { SizeSmallIcon, SizeMediumIcon, SizeLargeIcon } from '~components/icons'
@ -14,6 +14,8 @@ const sizes = {
const selectSize = (s: Data) => s.appState.selectedStyle.size
const preventEvent = (e: Event) => e.preventDefault()
export const SizeMenu = React.memo((): JSX.Element => {
const { tlstate, useSelector } = useTLDrawContext()
@ -23,14 +25,16 @@ export const SizeMenu = React.memo((): JSX.Element => {
<DropdownMenu.Root dir="ltr">
<DMTriggerIcon>{sizes[size as SizeStyle]}</DMTriggerIcon>
<DMContent variant="horizontal">
{Object.keys(SizeStyle).map((sizeStyle: string) => (
<ToolButton
key={sizeStyle}
isActive={size === sizeStyle}
onSelect={() => tlstate.style({ size: sizeStyle as SizeStyle })}
>
{sizes[sizeStyle as SizeStyle]}
</ToolButton>
{Object.values(SizeStyle).map((sizeStyle: string) => (
<DropdownMenu.Item key={sizeStyle} onSelect={preventEvent} asChild>
<ToolButton
isActive={size === sizeStyle}
variant="icon"
onClick={() => tlstate.style({ size: sizeStyle as SizeStyle })}
>
{sizes[sizeStyle as SizeStyle]}
</ToolButton>
</DropdownMenu.Item>
))}
</DMContent>
</DropdownMenu.Root>

View file

@ -5,7 +5,7 @@ Object {
"bend": 0,
"childIndex": 1,
"decorations": Object {
"end": "Arrow",
"end": "arrow",
},
"handles": Object {
"bend": Object {
@ -44,10 +44,10 @@ Object {
],
"rotation": 0,
"style": Object {
"color": "Black",
"dash": "Draw",
"color": "black",
"dash": "draw",
"isFilled": false,
"size": "Small",
"size": "small",
},
"type": "arrow",
}

View file

@ -14,10 +14,10 @@ Object {
"points": Array [],
"rotation": 0,
"style": Object {
"color": "Black",
"dash": "Draw",
"color": "black",
"dash": "draw",
"isFilled": false,
"size": "Small",
"size": "small",
},
"type": "draw",
}

View file

@ -16,10 +16,10 @@ Object {
],
"rotation": 0,
"style": Object {
"color": "Black",
"dash": "Draw",
"color": "black",
"dash": "draw",
"isFilled": false,
"size": "Small",
"size": "small",
},
"type": "ellipse",
}

View file

@ -17,10 +17,10 @@ Object {
100,
],
"style": Object {
"color": "Black",
"dash": "Draw",
"color": "black",
"dash": "draw",
"isFilled": false,
"size": "Small",
"size": "small",
},
"type": "group",
}

View file

@ -16,10 +16,10 @@ Object {
1,
],
"style": Object {
"color": "Black",
"dash": "Draw",
"color": "black",
"dash": "draw",
"isFilled": false,
"size": "Small",
"size": "small",
},
"type": "rectangle",
}

View file

@ -12,10 +12,10 @@ Object {
],
"rotation": 0,
"style": Object {
"color": "Black",
"dash": "Draw",
"color": "black",
"dash": "draw",
"isFilled": false,
"size": "Small",
"size": "small",
},
"text": " ",
"type": "text",

View file

@ -33,10 +33,10 @@ Array [
200,
],
"style": Object {
"color": "Black",
"dash": "Draw",
"color": "black",
"dash": "draw",
"isFilled": false,
"size": "Small",
"size": "small",
},
"type": "rectangle",
},
@ -93,10 +93,10 @@ Array [
200,
],
"style": Object {
"color": "Black",
"dash": "Draw",
"color": "black",
"dash": "draw",
"isFilled": false,
"size": "Small",
"size": "small",
},
"type": "rectangle",
},

View file

@ -0,0 +1,53 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { Decoration, TLDrawDocument, TLDrawShapeType } from '~types'
export function migrate(document: TLDrawDocument, newVersion: number): TLDrawDocument {
const { version = 0 } = document
if (version === newVersion) return document
if (version <= 12) {
Object.values(document.pages).forEach((page) => {
Object.values(page.bindings).forEach((binding) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Object.assign(binding, (binding as any).meta)
})
Object.values(page.shapes).forEach((shape) => {
Object.entries(shape.style).forEach(([id, style]) => {
if (typeof style === 'string') {
// @ts-ignore
shape.style[id] = style.toLowerCase()
}
})
if (shape.type === TLDrawShapeType.Arrow) {
if (shape.decorations) {
Object.entries(shape.decorations).forEach(([id, decoration]) => {
if ((decoration as unknown) === 'Arrow') {
shape.decorations = {
...shape.decorations,
[id]: Decoration.Arrow,
}
}
})
}
}
})
})
}
Object.values(document.pageStates).forEach((pageState) => {
pageState.selectedIds = pageState.selectedIds.filter((id) => {
return document.pages[pageState.id].shapes[id] !== undefined
})
pageState.bindingId = undefined
pageState.editingId = undefined
pageState.hoveredId = undefined
pageState.pointedId = undefined
})
document.version = newVersion
return document
}

View file

@ -47,6 +47,7 @@ import { sample } from './utils'
import { createTools, ToolType } from './tool'
import type { BaseTool } from './tool/BaseTool'
import { USER_COLORS, FIT_TO_SCREEN_PADDING } from '~constants'
import { migrate } from './migrate'
const uuid = Utils.uniqueId()
@ -100,21 +101,15 @@ export class TLDrawState extends StateManager<Data> {
onUserChange?: (tlstate: TLDrawState, user: TLDrawUser) => void
) {
super(TLDrawState.defaultState, id, TLDrawState.version, (prev, next) => {
Object.values(prev.document.pages).forEach((page) => {
Object.values(page.bindings).forEach((binding) => {
if ('meta' in binding) {
// @ts-ignore
Object.assign(binding, binding.meta)
}
})
})
return {
...next,
document: { ...next.document, ...prev.document },
}
})
this.loadDocument(this.document)
this.patchState({ document: migrate(this.document, TLDrawState.version) })
this._onChange = onChange
this._onMount = onMount
this._onUserChange = onUserChange
@ -130,6 +125,7 @@ export class TLDrawState extends StateManager<Data> {
appState: {
status: TLDrawStatus.Idle,
},
document: migrate(this.document, TLDrawState.version),
})
} catch (e) {
console.error('The data appears to be corrupted. Resetting!', e)
@ -641,7 +637,7 @@ export class TLDrawState extends StateManager<Data> {
...this.appState,
currentPageId: Object.keys(document.pages)[0],
},
document,
document: migrate(document, TLDrawState.version),
})
return this
}
@ -709,7 +705,7 @@ export class TLDrawState extends StateManager<Data> {
...this.state,
appState: nextAppState,
document: {
...document,
...migrate(document, TLDrawState.version),
pageStates: currentPageStates,
},
},
@ -803,7 +799,7 @@ export class TLDrawState extends StateManager<Data> {
return this.replaceState(
{
...TLDrawState.defaultState,
document,
document: migrate(document, TLDrawState.version),
appState: {
...TLDrawState.defaultState.appState,
currentPageId: Object.keys(document.pages)[0],
@ -1068,8 +1064,6 @@ export class TLDrawState extends StateManager<Data> {
Utils.deepClone(this.getShape(id, this.currentPageId))
)
console.log(copyingShapes.length)
if (copyingShapes.length === 0) return this
const copyingBindings: TLDrawBinding[] = Object.values(this.page.bindings).filter(
@ -2463,10 +2457,11 @@ export class TLDrawState extends StateManager<Data> {
}
}
static version = 11
static version = 12.5
static defaultDocument: TLDrawDocument = {
id: 'doc',
version: 12.4,
pages: {
page: {
id: 'page',

View file

@ -46,8 +46,6 @@ export abstract class BaseTool<T extends string = any> {
}
onCancel = () => {
console.log('cancelling')
if (this.status === Status.Idle) {
this.state.selectTool('select')
} else {

View file

@ -11,8 +11,6 @@ export class TextTool extends BaseTool {
stopEditingShape = () => {
this.setStatus(Status.Idle)
console.log(this.state.appState.isToolLocked)
if (!this.state.appState.isToolLocked) {
this.state.selectTool('select')
}

View file

@ -5,14 +5,14 @@ import oldDoc from './old-doc'
describe('When migrating bindings', () => {
it('migrates', () => {
Object.values((oldDoc as unknown as TLDrawDocument).pages).forEach((page) => {
Object.values(page.bindings).forEach((binding) => {
if ('meta' in binding) {
// @ts-ignore
Object.assign(binding, binding.meta)
}
})
})
// Object.values((oldDoc as unknown as TLDrawDocument).pages).forEach((page) => {
// Object.values(page.bindings).forEach((binding) => {
// if ('meta' in binding) {
// // @ts-ignore
// Object.assign(binding, binding.meta)
// }
// })
// })
new TLDrawState().loadDocument(oldDoc as unknown as TLDrawDocument)
})

View file

@ -1,6 +1,7 @@
import { TLDrawDocument, ColorStyle, DashStyle, SizeStyle, TLDrawShapeType } from '~types'
export const mockDocument: TLDrawDocument = {
version: 0,
id: 'doc',
pages: {
page1: {

View file

@ -9,7 +9,6 @@ import type {
TLHandle,
TLBounds,
TLSnapLine,
TLComponentProps,
} from '@tldraw/core'
import type { TLPage, TLUser, TLPageState } from '@tldraw/core'
import type { StoreApi } from 'zustand'
@ -28,12 +27,6 @@ export interface TLDrawTransformInfo<T extends TLShape> {
transformOrigin: number[]
}
export type TLDrawComponentProps<T extends TLDrawShape, E extends Element = any> = TLComponentProps<
T,
E,
TLDrawMeta
>
// old
export type TLStore = StoreApi<Data>
@ -45,6 +38,7 @@ export interface TLDrawDocument {
id: string
pages: Record<string, TLDrawPage>
pageStates: Record<string, TLPageState>
version: number
}
export interface TLDrawSettings {
@ -229,7 +223,7 @@ export enum TLDrawShapeType {
}
export enum Decoration {
Arrow = 'Arrow',
Arrow = 'arrow',
}
export interface TLDrawBaseShape extends TLShape {
@ -304,38 +298,38 @@ export interface ArrowBinding extends TLBinding {
export type TLDrawBinding = ArrowBinding
export enum ColorStyle {
White = 'White',
LightGray = 'LightGray',
Gray = 'Gray',
Black = 'Black',
Green = 'Green',
Cyan = 'Cyan',
Blue = 'Blue',
Indigo = 'Indigo',
Violet = 'Violet',
Red = 'Red',
Orange = 'Orange',
Yellow = 'Yellow',
White = 'white',
LightGray = 'lightGray',
Gray = 'gray',
Black = 'black',
Green = 'green',
Cyan = 'cyan',
Blue = 'blue',
Indigo = 'indigo',
Violet = 'violet',
Red = 'red',
Orange = 'orange',
Yellow = 'yellow',
}
export enum SizeStyle {
Small = 'Small',
Medium = 'Medium',
Large = 'Large',
Small = 'small',
Medium = 'medium',
Large = 'large',
}
export enum DashStyle {
Draw = 'Draw',
Solid = 'Solid',
Dashed = 'Dashed',
Dotted = 'Dotted',
Draw = 'draw',
Solid = 'solid',
Dashed = 'dashed',
Dotted = 'dotted',
}
export enum FontSize {
Small = 'Small',
Medium = 'Medium',
Large = 'Large',
ExtraLarge = 'ExtraLarge',
Small = 'small',
Medium = 'medium',
Large = 'large',
ExtraLarge = 'extraLarge',
}
export type ShapeStyles = {

11
www/.babelrc Normal file
View file

@ -0,0 +1,11 @@
{
"presets": [
[
"next/babel",
{
"preset-env": { "targets": { "node": true } }
}
]
],
"plugins": []
}

View file

@ -37,23 +37,7 @@ function Editor({ id }: { id: string }) {
uuid: docId,
document: {
id: 'test-room',
pages: {
page: {
id: 'page',
shapes: {},
bindings: {},
},
},
pageStates: {
page: {
id: 'page',
selectedIds: [],
camera: {
point: [0, 0],
zoom: 1,
},
},
},
...TLDrawState.defaultDocument,
},
})
@ -156,7 +140,11 @@ function Editor({ id }: { id: string }) {
doc.subscribe(handleDocumentUpdates)
// Load the shared document
tlstate.loadDocument(doc.toObject().document)
const newDocument = doc.toObject().document
if (newDocument) {
tlstate.loadDocument(newDocument)
}
return () => {
window.removeEventListener('beforeunload', handleExit)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,7 +1,7 @@
{
"compilerOptions": {
"composite": true,
"target": "es5",
"target": "es6",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,

View file

@ -9920,7 +9920,7 @@ raw-body@2.4.1:
iconv-lite "0.4.24"
unpipe "1.0.0"
react-dom@17.0.2, "react-dom@^16.8 || ^17.0":
react-dom@17.0.2:
version "17.0.2"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==
@ -10008,7 +10008,7 @@ react-style-singleton@^2.1.0:
invariant "^2.2.4"
tslib "^1.0.0"
react@17.0.2, react@>=16.8:
react@17.0.2:
version "17.0.2"
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==
@ -10445,10 +10445,10 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
hash-base "^3.0.0"
inherits "^2.0.1"
rko@^0.5.25:
version "0.5.25"
resolved "https://registry.yarnpkg.com/rko/-/rko-0.5.25.tgz#1095803900e3f912f6adf8a1c113b8227d3d88bf"
integrity sha512-HU6M3PxK3VEqrr6QZKAsqO98juQX24kEgJkKSdFJhw8U/DBUGAnU/fgyxNIaTw7TCI7vjIy/RzBEXf5I4sijKg==
rko@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/rko/-/rko-0.6.0.tgz#fa640384b4e82fdcd90fc58c958256148c4eb10c"
integrity sha512-u05SAiyz02Sw+QyGaQb3NGPXf3xXxQ9AwNG+tItHx2MpAsPEEH84NqYDyG9jem/ji/FPQPQHuRKcy2MHb1a1Ow==
dependencies:
idb-keyval "^5.1.3"
zustand "^3.5.9"