tldraw 5.3.0-next.fc37bd96825b → 5.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DOCS.md +1029 -27
- package/RELEASE_NOTES.md +45 -1
- package/dist-cjs/index.d.ts +109 -8
- package/dist-cjs/index.js +8 -3
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/TldrawImage.js +5 -1
- package/dist-cjs/lib/TldrawImage.js.map +2 -2
- package/dist-cjs/lib/defaultEmbedDefinitions.js +4 -0
- package/dist-cjs/lib/defaultEmbedDefinitions.js.map +2 -2
- package/dist-cjs/lib/defaultExternalContentHandlers.js +8 -0
- package/dist-cjs/lib/defaultExternalContentHandlers.js.map +2 -2
- package/dist-cjs/lib/defaultOverlayUtils.js +0 -2
- package/dist-cjs/lib/defaultOverlayUtils.js.map +2 -2
- package/dist-cjs/lib/defaultSideEffects.js +18 -0
- package/dist-cjs/lib/defaultSideEffects.js.map +2 -2
- package/dist-cjs/lib/overlays/CollaboratorCursorOverlayUtil.js.map +2 -2
- package/dist-cjs/lib/overlays/CollaboratorHintOverlayUtil.js +2 -6
- package/dist-cjs/lib/overlays/CollaboratorHintOverlayUtil.js.map +2 -2
- package/dist-cjs/lib/shapes/embed/EmbedShapeUtil.js +72 -4
- package/dist-cjs/lib/shapes/embed/EmbedShapeUtil.js.map +2 -2
- package/dist-cjs/lib/shapes/geo/GeoShapeUtil.js +5 -1
- package/dist-cjs/lib/shapes/geo/GeoShapeUtil.js.map +2 -2
- package/dist-cjs/lib/shapes/geo/getGeoShapePath.js +7 -1
- package/dist-cjs/lib/shapes/geo/getGeoShapePath.js.map +2 -2
- package/dist-cjs/lib/shapes/image/ImageShapeUtil.js +2 -3
- package/dist-cjs/lib/shapes/image/ImageShapeUtil.js.map +2 -2
- package/dist-cjs/lib/shapes/note/NoteShapeUtil.js +7 -0
- package/dist-cjs/lib/shapes/note/NoteShapeUtil.js.map +2 -2
- package/dist-cjs/lib/shapes/shared/PathBuilder.js +38 -0
- package/dist-cjs/lib/shapes/shared/PathBuilder.js.map +2 -2
- package/dist-cjs/lib/shapes/shared/flip.js +42 -0
- package/dist-cjs/lib/shapes/shared/flip.js.map +7 -0
- package/dist-cjs/lib/ui/components/MainMenu/DefaultMainMenuContent.js +3 -0
- package/dist-cjs/lib/ui/components/MainMenu/DefaultMainMenuContent.js.map +2 -2
- package/dist-cjs/lib/ui/components/Minimap/MinimapManager.js +8 -0
- package/dist-cjs/lib/ui/components/Minimap/MinimapManager.js.map +2 -2
- package/dist-cjs/lib/ui/components/QuickActions/DefaultQuickActionsContent.js +15 -1
- package/dist-cjs/lib/ui/components/QuickActions/DefaultQuickActionsContent.js.map +2 -2
- package/dist-cjs/lib/ui/components/SharePanel/DefaultPeopleMenuAvatar.js +1 -1
- package/dist-cjs/lib/ui/components/SharePanel/DefaultPeopleMenuAvatar.js.map +2 -2
- package/dist-cjs/lib/ui/components/SharePanel/DefaultPeopleMenuFacePile.js +2 -1
- package/dist-cjs/lib/ui/components/SharePanel/DefaultPeopleMenuFacePile.js.map +2 -2
- package/dist-cjs/lib/ui/components/Toolbar/DefaultToolbar.js +4 -4
- package/dist-cjs/lib/ui/components/Toolbar/DefaultToolbar.js.map +2 -2
- package/dist-cjs/lib/ui/components/Toolbar/DefaultToolbarContent.js +7 -0
- package/dist-cjs/lib/ui/components/Toolbar/DefaultToolbarContent.js.map +2 -2
- package/dist-cjs/lib/ui/components/menu-items.js +17 -8
- package/dist-cjs/lib/ui/components/menu-items.js.map +2 -2
- package/dist-cjs/lib/ui/components/primitives/TldrawUiContextualToolbar.js +0 -1
- package/dist-cjs/lib/ui/components/primitives/TldrawUiContextualToolbar.js.map +2 -2
- package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuItem.js +2 -0
- package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuItem.js.map +2 -2
- package/dist-cjs/lib/ui/hooks/menu-hooks.js +1 -1
- package/dist-cjs/lib/ui/hooks/menu-hooks.js.map +2 -2
- package/dist-cjs/lib/ui/hooks/useClipboardEvents.js +21 -22
- package/dist-cjs/lib/ui/hooks/useClipboardEvents.js.map +2 -2
- package/dist-cjs/lib/ui/hooks/useCommentingEnabled.js +28 -0
- package/dist-cjs/lib/ui/hooks/useCommentingEnabled.js.map +7 -0
- package/dist-cjs/lib/ui/hooks/useKeyboardShortcuts.js +8 -1
- package/dist-cjs/lib/ui/hooks/useKeyboardShortcuts.js.map +2 -2
- package/dist-cjs/lib/ui/hooks/useTranslation/TLUiTranslationKey.js.map +1 -1
- package/dist-cjs/lib/ui/hooks/useTranslation/defaultTranslation.js +44 -1
- package/dist-cjs/lib/ui/hooks/useTranslation/defaultTranslation.js.map +2 -2
- package/dist-cjs/lib/ui/hooks/useTranslation/useTranslation.js +6 -2
- package/dist-cjs/lib/ui/hooks/useTranslation/useTranslation.js.map +2 -2
- package/dist-cjs/lib/ui/icon-types.js +1 -0
- package/dist-cjs/lib/ui/icon-types.js.map +2 -2
- package/dist-cjs/lib/ui/version.js +3 -3
- package/dist-cjs/lib/ui/version.js.map +1 -1
- package/dist-cjs/lib/utils/embeds/embeds.js +12 -0
- package/dist-cjs/lib/utils/embeds/embeds.js.map +2 -2
- package/dist-cjs/lib/utils/text/richText.js +32 -23
- package/dist-cjs/lib/utils/text/richText.js.map +3 -3
- package/dist-esm/index.d.mts +109 -8
- package/dist-esm/index.mjs +15 -7
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/TldrawImage.mjs +5 -1
- package/dist-esm/lib/TldrawImage.mjs.map +2 -2
- package/dist-esm/lib/defaultEmbedDefinitions.mjs +4 -0
- package/dist-esm/lib/defaultEmbedDefinitions.mjs.map +2 -2
- package/dist-esm/lib/defaultExternalContentHandlers.mjs +9 -0
- package/dist-esm/lib/defaultExternalContentHandlers.mjs.map +2 -2
- package/dist-esm/lib/defaultOverlayUtils.mjs +0 -2
- package/dist-esm/lib/defaultOverlayUtils.mjs.map +2 -2
- package/dist-esm/lib/defaultSideEffects.mjs +18 -0
- package/dist-esm/lib/defaultSideEffects.mjs.map +2 -2
- package/dist-esm/lib/overlays/CollaboratorCursorOverlayUtil.mjs.map +2 -2
- package/dist-esm/lib/overlays/CollaboratorHintOverlayUtil.mjs +3 -7
- package/dist-esm/lib/overlays/CollaboratorHintOverlayUtil.mjs.map +2 -2
- package/dist-esm/lib/shapes/embed/EmbedShapeUtil.mjs +73 -5
- package/dist-esm/lib/shapes/embed/EmbedShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/shapes/geo/GeoShapeUtil.mjs +5 -1
- package/dist-esm/lib/shapes/geo/GeoShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/shapes/geo/getGeoShapePath.mjs +7 -1
- package/dist-esm/lib/shapes/geo/getGeoShapePath.mjs.map +2 -2
- package/dist-esm/lib/shapes/image/ImageShapeUtil.mjs +2 -3
- package/dist-esm/lib/shapes/image/ImageShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/shapes/note/NoteShapeUtil.mjs +7 -0
- package/dist-esm/lib/shapes/note/NoteShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/shapes/shared/PathBuilder.mjs +39 -0
- package/dist-esm/lib/shapes/shared/PathBuilder.mjs.map +2 -2
- package/dist-esm/lib/shapes/shared/flip.mjs +22 -0
- package/dist-esm/lib/shapes/shared/flip.mjs.map +7 -0
- package/dist-esm/lib/ui/components/MainMenu/DefaultMainMenuContent.mjs +3 -0
- package/dist-esm/lib/ui/components/MainMenu/DefaultMainMenuContent.mjs.map +2 -2
- package/dist-esm/lib/ui/components/Minimap/MinimapManager.mjs +9 -0
- package/dist-esm/lib/ui/components/Minimap/MinimapManager.mjs.map +2 -2
- package/dist-esm/lib/ui/components/QuickActions/DefaultQuickActionsContent.mjs +15 -1
- package/dist-esm/lib/ui/components/QuickActions/DefaultQuickActionsContent.mjs.map +2 -2
- package/dist-esm/lib/ui/components/SharePanel/DefaultPeopleMenuAvatar.mjs +2 -2
- package/dist-esm/lib/ui/components/SharePanel/DefaultPeopleMenuAvatar.mjs.map +2 -2
- package/dist-esm/lib/ui/components/SharePanel/DefaultPeopleMenuFacePile.mjs +2 -1
- package/dist-esm/lib/ui/components/SharePanel/DefaultPeopleMenuFacePile.mjs.map +2 -2
- package/dist-esm/lib/ui/components/Toolbar/DefaultToolbar.mjs +4 -4
- package/dist-esm/lib/ui/components/Toolbar/DefaultToolbar.mjs.map +2 -2
- package/dist-esm/lib/ui/components/Toolbar/DefaultToolbarContent.mjs +7 -0
- package/dist-esm/lib/ui/components/Toolbar/DefaultToolbarContent.mjs.map +2 -2
- package/dist-esm/lib/ui/components/menu-items.mjs +18 -9
- package/dist-esm/lib/ui/components/menu-items.mjs.map +2 -2
- package/dist-esm/lib/ui/components/primitives/TldrawUiContextualToolbar.mjs +0 -2
- package/dist-esm/lib/ui/components/primitives/TldrawUiContextualToolbar.mjs.map +2 -2
- package/dist-esm/lib/ui/components/primitives/menus/TldrawUiMenuItem.mjs +2 -0
- package/dist-esm/lib/ui/components/primitives/menus/TldrawUiMenuItem.mjs.map +2 -2
- package/dist-esm/lib/ui/hooks/menu-hooks.mjs +1 -1
- package/dist-esm/lib/ui/hooks/menu-hooks.mjs.map +2 -2
- package/dist-esm/lib/ui/hooks/useClipboardEvents.mjs +21 -22
- package/dist-esm/lib/ui/hooks/useClipboardEvents.mjs.map +2 -2
- package/dist-esm/lib/ui/hooks/useCommentingEnabled.mjs +8 -0
- package/dist-esm/lib/ui/hooks/useCommentingEnabled.mjs.map +7 -0
- package/dist-esm/lib/ui/hooks/useKeyboardShortcuts.mjs +8 -1
- package/dist-esm/lib/ui/hooks/useKeyboardShortcuts.mjs.map +2 -2
- package/dist-esm/lib/ui/hooks/useTranslation/defaultTranslation.mjs +44 -1
- package/dist-esm/lib/ui/hooks/useTranslation/defaultTranslation.mjs.map +2 -2
- package/dist-esm/lib/ui/hooks/useTranslation/useTranslation.mjs +6 -2
- package/dist-esm/lib/ui/hooks/useTranslation/useTranslation.mjs.map +2 -2
- package/dist-esm/lib/ui/icon-types.mjs +1 -0
- package/dist-esm/lib/ui/icon-types.mjs.map +2 -2
- package/dist-esm/lib/ui/version.mjs +3 -3
- package/dist-esm/lib/ui/version.mjs.map +1 -1
- package/dist-esm/lib/utils/embeds/embeds.mjs +12 -0
- package/dist-esm/lib/utils/embeds/embeds.mjs.map +2 -2
- package/dist-esm/lib/utils/text/richText.mjs +32 -23
- package/dist-esm/lib/utils/text/richText.mjs.map +3 -3
- package/package.json +7 -4
- package/src/index.ts +14 -8
- package/src/lib/TldrawImage.tsx +12 -1
- package/src/lib/defaultEmbedDefinitions.ts +12 -0
- package/src/lib/defaultExternalContentHandlers.test.ts +59 -1
- package/src/lib/defaultExternalContentHandlers.ts +17 -0
- package/src/lib/defaultOverlayUtils.ts +0 -2
- package/src/lib/defaultSideEffects.ts +28 -1
- package/src/lib/overlays/CollaboratorCursorOverlayUtil.ts +12 -1
- package/src/lib/overlays/CollaboratorHintOverlayUtil.ts +3 -17
- package/src/lib/shapes/embed/EmbedShapeUtil.tsx +98 -7
- package/src/lib/shapes/geo/GeoShapeUtil.test.tsx +65 -0
- package/src/lib/shapes/geo/GeoShapeUtil.tsx +4 -0
- package/src/lib/shapes/geo/getGeoShapePath.ts +14 -1
- package/src/lib/shapes/image/ImageShapeUtil.tsx +2 -3
- package/src/lib/shapes/note/NoteShapeUtil.tsx +15 -0
- package/src/lib/shapes/shared/PathBuilder.test.tsx +31 -0
- package/src/lib/shapes/shared/PathBuilder.tsx +44 -0
- package/src/lib/shapes/shared/flip.ts +59 -0
- package/src/lib/ui/components/MainMenu/DefaultMainMenuContent.tsx +6 -0
- package/src/lib/ui/components/Minimap/MinimapManager.ts +12 -0
- package/src/lib/ui/components/QuickActions/DefaultQuickActionsContent.tsx +17 -0
- package/src/lib/ui/components/SharePanel/DefaultPeopleMenuAvatar.tsx +2 -2
- package/src/lib/ui/components/SharePanel/DefaultPeopleMenuFacePile.tsx +2 -2
- package/src/lib/ui/components/Toolbar/DefaultToolbar.tsx +4 -4
- package/src/lib/ui/components/Toolbar/DefaultToolbarContent.tsx +14 -0
- package/src/lib/ui/components/menu-items.tsx +27 -11
- package/src/lib/ui/components/primitives/TldrawUiContextualToolbar.tsx +0 -2
- package/src/lib/ui/components/primitives/menus/TldrawUiMenuItem.tsx +2 -0
- package/src/lib/ui/hooks/menu-hooks.ts +2 -1
- package/src/lib/ui/hooks/useClipboardEvents.ts +37 -31
- package/src/lib/ui/hooks/useCommentingEnabled.ts +13 -0
- package/src/lib/ui/hooks/useKeyboardShortcuts.ts +19 -1
- package/src/lib/ui/hooks/useTranslation/TLUiTranslationKey.ts +43 -0
- package/src/lib/ui/hooks/useTranslation/defaultTranslation.ts +43 -0
- package/src/lib/ui/hooks/useTranslation/useTranslation.test.tsx +72 -0
- package/src/lib/ui/hooks/useTranslation/useTranslation.tsx +12 -2
- package/src/lib/ui/icon-types.ts +2 -0
- package/src/lib/ui/version.ts +3 -3
- package/src/lib/ui.css +19 -0
- package/src/lib/utils/embeds/embeds.test.ts +35 -1
- package/src/lib/utils/embeds/embeds.ts +29 -0
- package/src/lib/utils/excalidraw/__snapshots__/putExcalidrawContent.test.tsx.snap +16 -0
- package/src/lib/utils/text/richText.test.ts +52 -1
- package/src/lib/utils/text/richText.ts +61 -26
- package/src/lib/utils/tldr/__snapshots__/buildFromV1Document.test.ts.snap +12 -0
- package/src/test/attribution.test.ts +127 -0
- package/src/test/commands/__snapshots__/packShapes.test.ts.snap +18 -0
- package/src/test/commands/clipboardPaste.test.ts +544 -0
- package/src/test/embed-aspect-ratio.test.ts +254 -0
- package/src/test/flipShapes.test.ts +7 -2
- package/src/test/overlays/CollaboratorCursorOverlayUtil.test.ts +9 -2
- package/src/test/ui/ContextMenu.test.tsx +80 -1
- package/src/test/ui/EditorPortal.test.tsx +35 -0
- package/src/test/ui/keyboardShortcuts.test.tsx +50 -0
- package/tldraw.css +62 -4
package/DOCS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# tldraw Documentation
|
|
2
2
|
|
|
3
|
-
Version: `5.3.0
|
|
3
|
+
Version: `5.3.0`
|
|
4
4
|
|
|
5
5
|
This file is generated during package publishing from the tldraw docs content for this exact package version.
|
|
6
6
|
|
|
@@ -661,6 +661,91 @@ export default function () {
|
|
|
661
661
|
}
|
|
662
662
|
```
|
|
663
663
|
|
|
664
|
+
#### Meta
|
|
665
|
+
|
|
666
|
+
Every shape has a `meta` property for your own data. Tldraw stores and syncs this data but doesn't use it itself. It's an escape hatch for attaching extra information to shapes, like the name of the user who created a shape or the date it was last changed.
|
|
667
|
+
|
|
668
|
+
Like `props`, the data in `meta` must be JSON-serializable. Shapes aren't the only records with a `meta` property: pages, bindings, assets, and the document record have one too.
|
|
669
|
+
|
|
670
|
+
By default, a shape's `meta` is an empty object typed as `JsonObject`. To type your meta data, use an intersection:
|
|
671
|
+
|
|
672
|
+
```ts
|
|
673
|
+
type ShapeWithMyMeta = TLGeoShape & { meta: { createdBy: string } }
|
|
674
|
+
|
|
675
|
+
const shape = editor.getShape<ShapeWithMyMeta>(myGeoShapeId)
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
You can update a shape's `meta` with `Editor#updateShapes`, the same way you update its props:
|
|
679
|
+
|
|
680
|
+
```ts
|
|
681
|
+
editor.updateShapes<ShapeWithMyMeta>([
|
|
682
|
+
{
|
|
683
|
+
id: myGeoShapeId,
|
|
684
|
+
type: 'geo',
|
|
685
|
+
meta: { createdBy: 'Steve' },
|
|
686
|
+
},
|
|
687
|
+
])
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
##### Initial meta
|
|
691
|
+
|
|
692
|
+
When `Editor#createShapes` creates a shape, it sets the shape's meta using `Editor#getInitialMetaForShape`. By default this method returns an empty object. Replace it to provide your own initial meta:
|
|
693
|
+
|
|
694
|
+
```tsx
|
|
695
|
+
editor.getInitialMetaForShape = (shape) => {
|
|
696
|
+
if (shape.type === 'text') {
|
|
697
|
+
return { createdBy: currentUser.id, lastModified: Date.now() }
|
|
698
|
+
}
|
|
699
|
+
return { createdBy: currentUser.id }
|
|
700
|
+
}
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
> For a working example, see our [shape meta on create example](https://tldraw.dev/examples/events/meta-on-create).
|
|
704
|
+
|
|
705
|
+
##### Updating meta with side effects
|
|
706
|
+
|
|
707
|
+
To keep meta up to date as shapes change, register a side effect that runs before each shape update:
|
|
708
|
+
|
|
709
|
+
```tsx
|
|
710
|
+
editor.sideEffects.registerBeforeChangeHandler('shape', (_prev, next, source) => {
|
|
711
|
+
if (source !== 'user') return next
|
|
712
|
+
return {
|
|
713
|
+
...next,
|
|
714
|
+
meta: { updatedBy: editor.user.getExternalId(), updatedAt: Date.now() },
|
|
715
|
+
}
|
|
716
|
+
})
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
Side effects can run on create, update, and delete for shapes and other records. See [Side effects](https://tldraw.dev/sdk-features/side-effects) for the full API.
|
|
720
|
+
|
|
721
|
+
> For a working example, see our [shape meta on change example](https://tldraw.dev/examples/events/meta-on-change).
|
|
722
|
+
|
|
723
|
+
##### Validating meta
|
|
724
|
+
|
|
725
|
+
By default, the store accepts any JSON value in `meta`. To validate meta data at runtime, build your own schema with `createTLSchema` and pass validators for each shape type's meta:
|
|
726
|
+
|
|
727
|
+
```tsx
|
|
728
|
+
import { useState } from 'react'
|
|
729
|
+
import { createTLSchema, createTLStore, defaultShapeSchemas, T, Tldraw } from 'tldraw'
|
|
730
|
+
|
|
731
|
+
const schema = createTLSchema({
|
|
732
|
+
shapes: {
|
|
733
|
+
...defaultShapeSchemas,
|
|
734
|
+
geo: {
|
|
735
|
+
...defaultShapeSchemas.geo,
|
|
736
|
+
meta: { createdBy: T.string },
|
|
737
|
+
},
|
|
738
|
+
},
|
|
739
|
+
})
|
|
740
|
+
|
|
741
|
+
export default function App() {
|
|
742
|
+
const [store] = useState(() => createTLStore({ schema }))
|
|
743
|
+
return <Tldraw store={store} />
|
|
744
|
+
}
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
Bindings, assets, and user records accept meta validators the same way. For user records, see our [custom user metadata example](https://tldraw.dev/examples/users/custom-user).
|
|
748
|
+
|
|
664
749
|
#### Extending shapes
|
|
665
750
|
|
|
666
751
|
- **`BaseBoxShapeUtil`** - Extend this for standard rectangular shape behavior
|
|
@@ -1264,6 +1349,52 @@ Any apps connecting to the same room ID will enter a shared collaboration sessio
|
|
|
1264
1349
|
|
|
1265
1350
|
The demo server is great for prototyping, but data only lasts 24 hours and rooms are publicly accessible. For production, you'll need to self-host the tldraw sync server—see our full guide on [tldraw sync](https://tldraw.dev/docs/sync).
|
|
1266
1351
|
|
|
1352
|
+
#### Comments
|
|
1353
|
+
|
|
1354
|
+
Collaborators can also leave anchored comments on the canvas. The `@tldraw/commenting` package provides a comment tool, pins that stick to points and shapes, thread popovers with replies, mentions, reactions, and resolve, and a sidebar list of threads.
|
|
1355
|
+
|
|
1356
|
+
```tsx
|
|
1357
|
+
import {
|
|
1358
|
+
CanvasComments,
|
|
1359
|
+
CommentAuthor,
|
|
1360
|
+
commentToolOverrides,
|
|
1361
|
+
commentTools,
|
|
1362
|
+
} from '@tldraw/commenting'
|
|
1363
|
+
import { useSync } from '@tldraw/sync'
|
|
1364
|
+
import { commentSchemaRecords, TLComponents, Tldraw } from 'tldraw'
|
|
1365
|
+
import '@tldraw/commenting/commenting.css'
|
|
1366
|
+
|
|
1367
|
+
// Your app's user directory, however you already hold it.
|
|
1368
|
+
const AUTHORS: Record<string, CommentAuthor> = {
|
|
1369
|
+
'user-123': { name: 'You', color: '#EC5E41' },
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
const components: TLComponents = {
|
|
1373
|
+
InFrontOfTheCanvas: () => (
|
|
1374
|
+
<CanvasComments currentUserId="user-123" resolveAuthor={(id) => AUTHORS[id]} />
|
|
1375
|
+
),
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
function MyApp() {
|
|
1379
|
+
const store = useSync({
|
|
1380
|
+
uri: 'wss://your-server.com/sync/my-room',
|
|
1381
|
+
assets: myAssetStore,
|
|
1382
|
+
records: commentSchemaRecords,
|
|
1383
|
+
})
|
|
1384
|
+
|
|
1385
|
+
return (
|
|
1386
|
+
<Tldraw
|
|
1387
|
+
store={store}
|
|
1388
|
+
tools={commentTools}
|
|
1389
|
+
overrides={[commentToolOverrides]}
|
|
1390
|
+
components={components}
|
|
1391
|
+
/>
|
|
1392
|
+
)
|
|
1393
|
+
}
|
|
1394
|
+
```
|
|
1395
|
+
|
|
1396
|
+
Comment records are opt-in, so register `commentSchemaRecords` on your server's schema too. Commenting is a licensed feature. See [Commenting](https://tldraw.dev/docs/commenting) for an introduction, or the [full guide](https://tldraw.dev/sdk-features/commenting) for anchors, options, and server setup.
|
|
1397
|
+
|
|
1267
1398
|
#### Using other backends
|
|
1268
1399
|
|
|
1269
1400
|
While [tldraw sync](https://tldraw.dev/docs/sync) is our recommended solution, the tldraw SDK works with any real-time data backend. For example, [Liveblocks offers a tldraw integration](https://liveblocks.io/examples/tldraw-whiteboard), and many teams have connected tldraw to their own infrastructure.
|
|
@@ -1278,6 +1409,7 @@ Collaboration involves three concerns:
|
|
|
1278
1409
|
|
|
1279
1410
|
- [tldraw sync](https://tldraw.dev/docs/sync) — Our recommended multiplayer solution
|
|
1280
1411
|
- [Collaboration](https://tldraw.dev/sdk-features/collaboration) — Deep dive on presence, sync hooks, and building custom sync
|
|
1412
|
+
- [Commenting](https://tldraw.dev/docs/commenting) — Anchored comment threads on the canvas
|
|
1281
1413
|
- [Cursors](https://tldraw.dev/sdk-features/cursors) — Cursor types and collaborator cursor customization
|
|
1282
1414
|
- [User following](https://tldraw.dev/sdk-features/user-following) — Track collaborator viewports in real-time
|
|
1283
1415
|
- [Store](https://tldraw.dev/sdk-features/store) — Understanding the reactive store
|
|
@@ -1522,6 +1654,132 @@ We provide several starter kits for AI integrations:
|
|
|
1522
1654
|
- [Bindings](https://tldraw.dev/sdk-features/bindings) — Connecting shapes for workflow systems
|
|
1523
1655
|
- [Image export](https://tldraw.dev/sdk-features/image-export) — Exporting canvas content for AI analysis
|
|
1524
1656
|
|
|
1657
|
+
### Commenting
|
|
1658
|
+
|
|
1659
|
+
In tldraw, a comment is a message pinned to a place on the canvas. Comments group into threads, one conversation per pin. Every comment records who wrote it, as an id your app resolves to a name.
|
|
1660
|
+
|
|
1661
|
+
The `@tldraw/commenting` package works at two levels. `CanvasComments` is a comments layer you render in front of the canvas, and it handles the whole flow. Everything it's built from is exported too, so you can replace any part of it.
|
|
1662
|
+
|
|
1663
|
+
Commenting is a licensed feature. It runs in development without a key. In production it needs a tldraw license that includes commenting.
|
|
1664
|
+
|
|
1665
|
+
#### Quick start
|
|
1666
|
+
|
|
1667
|
+
Three pieces: register the comment record types, register the comment tool, and render the layer.
|
|
1668
|
+
|
|
1669
|
+
```tsx
|
|
1670
|
+
import {
|
|
1671
|
+
CanvasComments,
|
|
1672
|
+
CommentAuthor,
|
|
1673
|
+
commentToolOverrides,
|
|
1674
|
+
commentTools,
|
|
1675
|
+
} from '@tldraw/commenting'
|
|
1676
|
+
import { useMemo } from 'react'
|
|
1677
|
+
import { commentSchemaRecords, createTLSchema, createTLStore, TLComponents, Tldraw } from 'tldraw'
|
|
1678
|
+
import '@tldraw/commenting/commenting.css'
|
|
1679
|
+
import 'tldraw/tldraw.css'
|
|
1680
|
+
|
|
1681
|
+
const AUTHORS: Record<string, CommentAuthor> = { me: { name: 'You', color: '#EC5E41' } }
|
|
1682
|
+
const resolveAuthor = (id: string) => AUTHORS[id]
|
|
1683
|
+
|
|
1684
|
+
const components: TLComponents = {
|
|
1685
|
+
InFrontOfTheCanvas: () => <CanvasComments currentUserId="me" resolveAuthor={resolveAuthor} />,
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
export default function App() {
|
|
1689
|
+
const store = useMemo(
|
|
1690
|
+
() => createTLStore({ schema: createTLSchema({ records: commentSchemaRecords }) }),
|
|
1691
|
+
[]
|
|
1692
|
+
)
|
|
1693
|
+
|
|
1694
|
+
return (
|
|
1695
|
+
<div style={{ position: 'fixed', inset: 0 }}>
|
|
1696
|
+
<Tldraw
|
|
1697
|
+
licenseKey={YOUR_LICENSE_KEY}
|
|
1698
|
+
store={store}
|
|
1699
|
+
tools={commentTools}
|
|
1700
|
+
overrides={[commentToolOverrides]}
|
|
1701
|
+
components={components}
|
|
1702
|
+
/>
|
|
1703
|
+
</div>
|
|
1704
|
+
)
|
|
1705
|
+
}
|
|
1706
|
+
```
|
|
1707
|
+
|
|
1708
|
+
Pick the comment tool from Quick Actions, or press `C`, then click the canvas to start a thread.
|
|
1709
|
+
|
|
1710
|
+
The layer's two required inputs are both about identity: `currentUserId` is the id stamped on whatever the user posts, and `resolveAuthor` turns an author id into a name, a color, and an optional avatar image. Commenting reads no user directory of its own, so these are where you connect it to whatever your app already knows about its users. With the read-status and mention callbacks they make up the `CommentingContext`, which `CanvasCommentsSidebar` takes too — build it once and spread it into both.
|
|
1711
|
+
|
|
1712
|
+
#### Comments are records
|
|
1713
|
+
|
|
1714
|
+
Comment threads and comments are records in the editor's store, exactly like shapes. That's the fact most worth holding onto, because almost everything else follows from it.
|
|
1715
|
+
|
|
1716
|
+
They aren't in the default schema, so you opt in by registering `commentSchemaRecords`. Once you have, comments persist and sync however your document already does. Adding a `persistenceKey` or a sync backend carries them along with no extra work.
|
|
1717
|
+
|
|
1718
|
+
It also means you can read and write them yourself. Query the store for threads, seed a document with review notes, or build a panel that does something the built-in sidebar doesn't.
|
|
1719
|
+
|
|
1720
|
+
```tsx
|
|
1721
|
+
import { useCommentThreads } from '@tldraw/commenting'
|
|
1722
|
+
import { useEditor } from 'tldraw'
|
|
1723
|
+
|
|
1724
|
+
function OpenThreadCount() {
|
|
1725
|
+
const editor = useEditor()
|
|
1726
|
+
const threads = useCommentThreads(editor)
|
|
1727
|
+
return <div>{threads.filter((thread) => !thread.resolved).length} open threads</div>
|
|
1728
|
+
}
|
|
1729
|
+
```
|
|
1730
|
+
|
|
1731
|
+
#### Anchors
|
|
1732
|
+
|
|
1733
|
+
What pins a thread to the canvas is its anchor. A thread can anchor to a point on the page, to a shape it then follows as that shape moves and resizes, to a rectangular region, or to the page as a whole.
|
|
1734
|
+
|
|
1735
|
+
Clicking empty canvas gives you a point anchor and clicking a shape gives you a shape anchor. Region anchors are off by default; turn them on and dragging the tool out covers an area.
|
|
1736
|
+
|
|
1737
|
+
Shape-anchored threads outlive their shape. Delete the shape and the thread converts to a point anchor where its pin last sat, so the conversation doesn't disappear along with the thing it was about.
|
|
1738
|
+
|
|
1739
|
+
#### What you get
|
|
1740
|
+
|
|
1741
|
+
The comments layer covers the whole flow out of the box:
|
|
1742
|
+
|
|
1743
|
+
| Feature | Description |
|
|
1744
|
+
| ------------ | ------------------------------------------------------------------ |
|
|
1745
|
+
| Threads | Pins on the canvas, opening to replies, edit, resolve, and delete. |
|
|
1746
|
+
| Mentions | `@`-mentions in composers, resolved against a roster you supply. |
|
|
1747
|
+
| Reactions | Emoji reactions on comments, with a pluggable palette. |
|
|
1748
|
+
| A sidebar | A filterable list of threads beside the canvas. |
|
|
1749
|
+
| Clustering | Nearby pins fold into count badges as you zoom out. |
|
|
1750
|
+
| Unread state | Pin badges and an unread filter, driven by your app's read data. |
|
|
1751
|
+
|
|
1752
|
+
Each of these has options, and each visible piece is a component slot you can replace. See [Commenting](https://tldraw.dev/sdk-features/commenting) for the full guide.
|
|
1753
|
+
|
|
1754
|
+
#### Configuring
|
|
1755
|
+
|
|
1756
|
+
Commenting options live on the tool. `CommentTool.configure()` returns a configured subclass to register, mirroring `ShapeUtil.configure`:
|
|
1757
|
+
|
|
1758
|
+
```tsx
|
|
1759
|
+
const tools = [CommentTool.configure({ enableRegions: true })]
|
|
1760
|
+
```
|
|
1761
|
+
|
|
1762
|
+
The option worth knowing about first is `history`. Comment writes default to `'ignore'`, which keeps them off the undo stack, and that default matters in a shared document: an undoable delete would resurrect a thread a collaborator already removed. See [Comments and undo](https://tldraw.dev/sdk-features/commenting#comments-and-undo).
|
|
1763
|
+
|
|
1764
|
+
#### Permissions
|
|
1765
|
+
|
|
1766
|
+
`canComment` decides whether the viewer may participate, and the UI follows it: composers give way to a fallback slot and the action affordances hide. `canModifyComment` decides the writes that belong to someone in particular — editing a comment, deleting a comment, deleting a thread. Unset, each is its record's owner's to make; a callback widens that (a workspace admin who may remove anyone's comment) or narrows it.
|
|
1767
|
+
|
|
1768
|
+
They're UI-level controls and nothing more. Comment records carry a client-supplied author id, so rules about who may post, edit, or delete belong in your sync server's record authorization, where each incoming record is checked against the session's identity.
|
|
1769
|
+
|
|
1770
|
+
#### Syncing
|
|
1771
|
+
|
|
1772
|
+
Comments sync like the rest of your document, with one registration on each side. Pass `records: commentSchemaRecords` to your sync hook, and the same map to `createTLSchema` on the server.
|
|
1773
|
+
|
|
1774
|
+
On the server you can go a step further and serve comments through the room's object-store lane. Lane records are gated by their own per-session permission rather than by `isReadonly`, which is how a viewer who can't edit the document can still comment. See [Syncing comments](https://tldraw.dev/sdk-features/commenting#syncing-comments).
|
|
1775
|
+
|
|
1776
|
+
#### Related
|
|
1777
|
+
|
|
1778
|
+
- [Commenting](https://tldraw.dev/sdk-features/commenting) — The full guide: anchors, options, components, and sync
|
|
1779
|
+
- [Collaboration](https://tldraw.dev/docs/collaboration) — Adding multiplayer to your project
|
|
1780
|
+
- [tldraw sync](https://tldraw.dev/docs/sync) — Running a sync server
|
|
1781
|
+
- [Commenting example](https://tldraw.dev/examples/collaboration/commenting) — The flow end to end
|
|
1782
|
+
|
|
1525
1783
|
### tldraw sync
|
|
1526
1784
|
|
|
1527
1785
|
You can add realtime multi-user collaboration to your tldraw app by using **tldraw sync**. It's our library for fast, fault-tolerant shared document syncing. We use it in production on our flagship app [tldraw.com](https://tldraw.com).
|
|
@@ -1851,6 +2109,43 @@ Both `props` and `migrations` are optional. If you omit `props`, you won't have
|
|
|
1851
2109
|
validation for your shape, which could result in bad data being stored. If you omit `migrations`,
|
|
1852
2110
|
clients on different versions won't be able to collaborate without errors.
|
|
1853
2111
|
|
|
2112
|
+
##### Comments and the object-store lane
|
|
2113
|
+
|
|
2114
|
+
Comment threads, comments, and reactions are record types that aren't in the default schema. Register
|
|
2115
|
+
them with the `records` option on both ends—`useSync` on the client, `createTLSchema` on the
|
|
2116
|
+
server. Both sides must register the same types, or the connection will fail schema validation.
|
|
2117
|
+
|
|
2118
|
+
```ts
|
|
2119
|
+
import { TLSocketRoom } from '@tldraw/sync-core'
|
|
2120
|
+
import { commentSchemaRecords, createTLSchema } from '@tldraw/tlschema'
|
|
2121
|
+
|
|
2122
|
+
const schema = createTLSchema({ records: commentSchemaRecords })
|
|
2123
|
+
|
|
2124
|
+
const room = new TLSocketRoom({
|
|
2125
|
+
schema,
|
|
2126
|
+
objectTypes: ['comment', 'comment-thread', 'comment-reaction'],
|
|
2127
|
+
})
|
|
2128
|
+
```
|
|
2129
|
+
|
|
2130
|
+
`objectTypes` serves those record types through the room's object-store lane instead of the document
|
|
2131
|
+
lane. Lane records are persisted separately from the document, left out of document snapshots, and
|
|
2132
|
+
gated by a per-session `objectAccess` permission rather than by `isReadonly`. That's how a session
|
|
2133
|
+
can be allowed to comment without being allowed to edit:
|
|
2134
|
+
|
|
2135
|
+
```ts
|
|
2136
|
+
room.handleSocketConnect({
|
|
2137
|
+
sessionId,
|
|
2138
|
+
socket,
|
|
2139
|
+
isReadonly: true,
|
|
2140
|
+
objectAccess: 'write',
|
|
2141
|
+
})
|
|
2142
|
+
```
|
|
2143
|
+
|
|
2144
|
+
`objectAccess` is `'read'` or `'write'` and defaults to `'write'`. Read the lane's contents with
|
|
2145
|
+
`TLSocketRoom`'s `getCurrentObjectsSnapshot()`, and use the room's `onCommittedChanges` callback to
|
|
2146
|
+
mirror committed records into your own database. See [Commenting](https://tldraw.dev/sdk-features/commenting) for the
|
|
2147
|
+
client side.
|
|
2148
|
+
|
|
1854
2149
|
##### Deployment concerns
|
|
1855
2150
|
|
|
1856
2151
|
You must make sure that the tldraw version in your client matches the version on the server. We
|
|
@@ -6134,6 +6429,26 @@ export default function App({ roomId }: { roomId: string }) {
|
|
|
6134
6429
|
|
|
6135
6430
|
Pass the shape and binding utilities to both the sync hook (for schema registration) and the Tldraw component (for rendering). If they don't match, shapes may fail to sync or render correctly.
|
|
6136
6431
|
|
|
6432
|
+
#### Comments
|
|
6433
|
+
|
|
6434
|
+
Comment threads sync the same way, through record types you opt into with the `records` option:
|
|
6435
|
+
|
|
6436
|
+
```tsx
|
|
6437
|
+
import { useSync } from '@tldraw/sync'
|
|
6438
|
+
import { commentSchemaRecords } from 'tldraw'
|
|
6439
|
+
|
|
6440
|
+
const store = useSync({
|
|
6441
|
+
uri: `wss://your-server.com/sync/${roomId}`,
|
|
6442
|
+
assets: myAssetStore,
|
|
6443
|
+
records: commentSchemaRecords,
|
|
6444
|
+
})
|
|
6445
|
+
```
|
|
6446
|
+
|
|
6447
|
+
Your server's schema needs the same registration. On the server you can also serve comments through
|
|
6448
|
+
the room's object-store lane, which gates them by their own per-session permission—so a viewer who
|
|
6449
|
+
can't edit the document can still comment. See [Commenting](https://tldraw.dev/sdk-features/commenting) for the tool,
|
|
6450
|
+
the canvas layer, and the server setup.
|
|
6451
|
+
|
|
6137
6452
|
#### Building custom sync
|
|
6138
6453
|
|
|
6139
6454
|
The `@tldraw/sync` package handles many complexities: connection management, reconnection, conflict resolution, and protocol versioning. For most applications, it's the right choice. However, you might need custom sync when integrating with existing infrastructure, using a different transport (like WebRTC), or implementing specialized conflict resolution.
|
|
@@ -6337,6 +6652,690 @@ This example omits important concerns like initial state sync, reconnection hand
|
|
|
6337
6652
|
- [Custom presence](https://tldraw.dev/examples/collaboration/sync-custom-presence) — Customizing presence data sent to collaborators
|
|
6338
6653
|
- [Custom shapes](https://tldraw.dev/examples/collaboration/sync-custom-shape) — Syncing custom shapes with multiplayer
|
|
6339
6654
|
|
|
6655
|
+
### Commenting
|
|
6656
|
+
|
|
6657
|
+
In tldraw, a comment is a message pinned to a place on the canvas. Comments group into threads, one conversation per pin. Every comment records who wrote it and comments can mention other people.
|
|
6658
|
+
|
|
6659
|
+
The `@tldraw/commenting` package works at two levels. `CanvasComments` is a comments layer you render in front of the canvas: it draws the pins, opens the threads, and reads and writes the records itself. Everything it's built from is exported too, so you can replace any part of it or assemble your own layer from the pieces.
|
|
6660
|
+
|
|
6661
|
+
Commenting is a licensed feature. It runs in development without a key. In production it needs a tldraw license that includes commenting.
|
|
6662
|
+
|
|
6663
|
+
#### Setup
|
|
6664
|
+
|
|
6665
|
+
There are three pieces: register the comment record types with your store, register the comment tool, and render the comments layer.
|
|
6666
|
+
|
|
6667
|
+
```tsx
|
|
6668
|
+
import {
|
|
6669
|
+
CanvasComments,
|
|
6670
|
+
CommentAuthor,
|
|
6671
|
+
commentTools,
|
|
6672
|
+
commentToolOverrides,
|
|
6673
|
+
} from '@tldraw/commenting'
|
|
6674
|
+
import { useMemo } from 'react'
|
|
6675
|
+
import { commentSchemaRecords, createTLSchema, createTLStore, TLComponents, Tldraw } from 'tldraw'
|
|
6676
|
+
import '@tldraw/commenting/commenting.css'
|
|
6677
|
+
import 'tldraw/tldraw.css'
|
|
6678
|
+
|
|
6679
|
+
// Your app's user directory. Any id you can't resolve falls back to a generic byline.
|
|
6680
|
+
const AUTHORS: Record<string, CommentAuthor> = {
|
|
6681
|
+
me: { name: 'You', color: '#EC5E41' },
|
|
6682
|
+
ada: { name: 'Ada Lovelace', color: '#0E9F6E' },
|
|
6683
|
+
}
|
|
6684
|
+
const resolveAuthor = (id: string) => AUTHORS[id]
|
|
6685
|
+
|
|
6686
|
+
const components: TLComponents = {
|
|
6687
|
+
InFrontOfTheCanvas: () => <CanvasComments currentUserId="me" resolveAuthor={resolveAuthor} />,
|
|
6688
|
+
}
|
|
6689
|
+
|
|
6690
|
+
export default function App() {
|
|
6691
|
+
const store = useMemo(
|
|
6692
|
+
() => createTLStore({ schema: createTLSchema({ records: commentSchemaRecords }) }),
|
|
6693
|
+
[]
|
|
6694
|
+
)
|
|
6695
|
+
|
|
6696
|
+
return (
|
|
6697
|
+
<div style={{ position: 'fixed', inset: 0 }}>
|
|
6698
|
+
<Tldraw
|
|
6699
|
+
licenseKey={YOUR_LICENSE_KEY}
|
|
6700
|
+
store={store}
|
|
6701
|
+
tools={commentTools}
|
|
6702
|
+
overrides={[commentToolOverrides]}
|
|
6703
|
+
components={components}
|
|
6704
|
+
/>
|
|
6705
|
+
</div>
|
|
6706
|
+
)
|
|
6707
|
+
}
|
|
6708
|
+
```
|
|
6709
|
+
|
|
6710
|
+
Comments are records in the editor's store, exactly like shapes. `commentSchemaRecords` registers the `comment-thread` and `comment` types, which aren't in the default schema. Once they're registered, comments persist and sync however your document already does: add a `persistenceKey` or a sync backend and they come along with it.
|
|
6711
|
+
|
|
6712
|
+
`CanvasComments` needs two things, and both are about identity. `currentUserId` is the id of the person commenting, or `null` for a viewer who can read comments but not post them. `resolveAuthor` maps an author id to a `CommentAuthor`: a `name`, an optional `color` for their avatar and pin, and an optional `image`. Return `undefined` for an id you can't resolve and the layer falls back to an anonymous user. Together with the read-status and mention callbacks below they make up the `CommentingContext`, which the sidebar takes too — so a host mounting both surfaces builds one object and spreads it into each.
|
|
6713
|
+
|
|
6714
|
+
Mount the layer through the `InFrontOfTheCanvas` component slot so it sits above the canvas but below the UI, and import `commenting.css` alongside `tldraw.css`.
|
|
6715
|
+
|
|
6716
|
+
Without a license key that includes commenting, every commenting component renders nothing in
|
|
6717
|
+
production. The feature is fully enabled in development, so a missing key shows up as a blank
|
|
6718
|
+
canvas after you deploy. See [License key](https://tldraw.dev/sdk-features/license-key).
|
|
6719
|
+
|
|
6720
|
+
#### Placing comments
|
|
6721
|
+
|
|
6722
|
+
`commentToolOverrides` puts the comment tool in Quick Actions with the `C` shortcut. With the tool active, clicking empty canvas starts a thread anchored to that point, and clicking a shape anchors a comment to the shape. You can also turn on region comments, where dragging the tool out covers an area. Either way the click only opens a composer: nothing is written to the store until the comment is posted.
|
|
6723
|
+
|
|
6724
|
+
Once a comment thread exists, its pin is the handle for everything else.
|
|
6725
|
+
|
|
6726
|
+
| Input | Result |
|
|
6727
|
+
| ----------- | -------------------------------------------------------------------- |
|
|
6728
|
+
| Click a pin | Opens the thread, with its replies and a reply composer. |
|
|
6729
|
+
| Drag a pin | Re-anchors the thread. Drop it on a shape to attach it to the shape. |
|
|
6730
|
+
| `Escape` | Closes the open thread. |
|
|
6731
|
+
| `Shift+C` | Hides and shows the pins on the canvas. |
|
|
6732
|
+
|
|
6733
|
+
In the built-in layer, resolve and delete-thread sit in the thread's header, and the edit and delete controls for a single comment appear only on the author's own comments by default. [`canModifyComment`](#who-can-edit-and-delete) changes who gets those controls, and [Syncing comments](#syncing-comments) covers enforcing the same rules on the server.
|
|
6734
|
+
|
|
6735
|
+
To put the show and hide toggle in a menu of your own, use `CommentsMenuItem`. It's a checkbox item wired to the same state as the `Shift+C` shortcut.
|
|
6736
|
+
|
|
6737
|
+
```tsx
|
|
6738
|
+
import { CommentsMenuItem } from '@tldraw/commenting'
|
|
6739
|
+
import { DefaultMainMenu, TldrawUiMenuGroup } from 'tldraw'
|
|
6740
|
+
|
|
6741
|
+
function MainMenu() {
|
|
6742
|
+
return (
|
|
6743
|
+
<DefaultMainMenu>
|
|
6744
|
+
<TldrawUiMenuGroup id="comments">
|
|
6745
|
+
<CommentsMenuItem />
|
|
6746
|
+
</TldrawUiMenuGroup>
|
|
6747
|
+
</DefaultMainMenu>
|
|
6748
|
+
)
|
|
6749
|
+
}
|
|
6750
|
+
```
|
|
6751
|
+
|
|
6752
|
+
#### Configuring the tool
|
|
6753
|
+
|
|
6754
|
+
Commenting options live on the comment tool. Set them with `CommentTool.configure()`, which mirrors `ShapeUtil.configure` and returns a configured subclass to register:
|
|
6755
|
+
|
|
6756
|
+
```tsx
|
|
6757
|
+
import { CommentTool, commentToolOverrides } from '@tldraw/commenting'
|
|
6758
|
+
import { Tldraw } from 'tldraw'
|
|
6759
|
+
|
|
6760
|
+
const tools = [CommentTool.configure({ enableRegions: true })]
|
|
6761
|
+
|
|
6762
|
+
function App() {
|
|
6763
|
+
return <Tldraw tools={tools} overrides={[commentToolOverrides]} components={components} />
|
|
6764
|
+
}
|
|
6765
|
+
```
|
|
6766
|
+
|
|
6767
|
+
Options are fixed once the tool is registered, so this is static configuration only. Live values, like the current user and the author resolver, are the `CommentingContext` instead, passed as props to each surface. Calls to `configure` can be chained, and each one layers over the last.
|
|
6768
|
+
|
|
6769
|
+
The sections below introduce the relevant comment options, and [All options](#all-options) lists every one together.
|
|
6770
|
+
|
|
6771
|
+
#### Anchors
|
|
6772
|
+
|
|
6773
|
+
A thread's `anchor` says where it lives. It's a discriminated union, so new anchor kinds can arrive without breaking existing threads.
|
|
6774
|
+
|
|
6775
|
+
| Anchor | Description |
|
|
6776
|
+
| -------- | ------------------------------------------------------------------------------------------------------------------------ |
|
|
6777
|
+
| `point` | A fixed page point. What a click on empty canvas produces. |
|
|
6778
|
+
| `shape` | A shape, with `x`/`y` as a normalized (0–1) offset in its bounds. The pin keeps its spot as the shape moves and resizes. |
|
|
6779
|
+
| `region` | A rectangular area of the page, with the pin on one corner. |
|
|
6780
|
+
| `page` | The page itself, with no spatial anchor. These threads have no pin and surface in a list instead. |
|
|
6781
|
+
|
|
6782
|
+
Shape-anchored threads outlive their shape. Delete the shape and the thread converts to a `point` anchor where its pin last sat, so the conversation doesn't vanish with the thing it was about. Bring the shape back, whether by undoing the delete or by a page move that re-creates it, and the shape anchor is restored. The exception is a pin someone moved by hand in the meantime: a manual placement wins over the restore.
|
|
6783
|
+
|
|
6784
|
+
Threads follow their shape across pages too. Move the shape to another page and the thread's `pageId`, along with the denormalized `pageId` on each of its comments, updates to match. Comments do not move when you cut or copy a shape.
|
|
6785
|
+
|
|
6786
|
+
##### Shape anchor precision
|
|
6787
|
+
|
|
6788
|
+
A comment that lands on a shape anchors in one of two ways. A **precise** anchor pins to the exact clicked spot. An **imprecise** one addresses the shape as a whole and renders its pin at a spot you choose, the `impreciseShapeAnchor`, which is the shape's top-right corner by default. Both track the shape as it moves and resizes, and both store the clicked `x`/`y`, so precision governs rendering rather than data.
|
|
6789
|
+
|
|
6790
|
+
Shape comments are precise by default. `shouldBePrecise` makes the call, and it receives the gesture: the target shape, the release point, and whether Alt was held.
|
|
6791
|
+
|
|
6792
|
+
```tsx
|
|
6793
|
+
// Precise on notes, shape-level everywhere else
|
|
6794
|
+
const tools = [
|
|
6795
|
+
CommentTool.configure({
|
|
6796
|
+
shouldBePrecise: (editor, { shapeId }) => editor.getShape(shapeId)?.type === 'note',
|
|
6797
|
+
}),
|
|
6798
|
+
]
|
|
6799
|
+
```
|
|
6800
|
+
|
|
6801
|
+
Return `() => false` for shape-level anchoring throughout, or `(editor, { altKey }) => altKey` to make precision a per-placement choice the user holds Alt for. The predicate governs new placements only. Anchors already stored render the way they were made.
|
|
6802
|
+
|
|
6803
|
+
#### Comments and undo
|
|
6804
|
+
|
|
6805
|
+
Comment writes are not undoable by default. The `history` option governs all of them, including posting, replying, editing and resolving, and it defaults to `'ignore'`. It covers your own writes too: a record you write with `putCommentRecords` lands on the undo stack, or doesn't, exactly like one the built-in UI writes.
|
|
6806
|
+
|
|
6807
|
+
In a shared document an undoable delete resurrects a thread a collaborator already removed, and an undoable resolve reverts their newer state. `'record'` is safe single-player, or against a comment store that isn't synced.
|
|
6808
|
+
|
|
6809
|
+
Deleting is the one write `history` doesn't reach. It's never undoable, for a reason particular to how deletes work — see [Deleting is a soft delete](#deleting-is-a-soft-delete).
|
|
6810
|
+
|
|
6811
|
+
Pin drags are the exception worth configuring separately. Re-anchoring a comment is a spatial edit that may reasonably undo alongside the shape move that prompted it, so `dragHistory` overrides `history` for drags alone:
|
|
6812
|
+
|
|
6813
|
+
```tsx
|
|
6814
|
+
CommentTool.configure({ dragHistory: 'record' })
|
|
6815
|
+
```
|
|
6816
|
+
|
|
6817
|
+
The [comments and undo example](https://tldraw.dev/examples/collaboration/comment-history) lets you feel the difference: post a comment, press undo, and watch whether the thread count moves.
|
|
6818
|
+
|
|
6819
|
+
#### Who can comment
|
|
6820
|
+
|
|
6821
|
+
Left unset, `canComment` allows participation whenever `currentUserId` is set. Participation covers composing threads and replies, editing and deleting your own comments, resolving threads, and moving pins. Pass a callback to decide for yourself:
|
|
6822
|
+
|
|
6823
|
+
```tsx
|
|
6824
|
+
CommentTool.configure({
|
|
6825
|
+
canComment: ({ currentUserId }) => currentUserId !== null && getRole() !== 'viewer',
|
|
6826
|
+
components: {
|
|
6827
|
+
ComposerFallback: ({ context }) => (context === 'thread' ? <SignInPrompt /> : null),
|
|
6828
|
+
},
|
|
6829
|
+
})
|
|
6830
|
+
```
|
|
6831
|
+
|
|
6832
|
+
When `canComment` returns false, composers give way to the `ComposerFallback` slot and the action affordances hide. That slot's `context` says which surface is asking: the bottom of an open thread (`'thread'`), or the placement popover the tool opens (`'pending'`). Leave the slot unset and those surfaces render nothing.
|
|
6833
|
+
|
|
6834
|
+
`canComment` is read during render through `useCanComment`, so a callback that reads signals re-evaluates when they change.
|
|
6835
|
+
|
|
6836
|
+
#### Who can edit and delete
|
|
6837
|
+
|
|
6838
|
+
`canComment` is about the viewer; `canModifyComment` is about the viewer and one particular record. It's asked for the three writes that belong to someone in particular — editing a comment, deleting a comment, deleting a thread — and where it returns false, that affordance isn't rendered.
|
|
6839
|
+
|
|
6840
|
+
Left unset, each is its record's owner's to make: you edit and delete your own comments, and delete threads you started. Pass a callback to widen that, composing with `defaultCanModifyComment` so the owner keeps what they already had:
|
|
6841
|
+
|
|
6842
|
+
```tsx
|
|
6843
|
+
import { CommentTool, defaultCanModifyComment } from '@tldraw/commenting'
|
|
6844
|
+
|
|
6845
|
+
CommentTool.configure({
|
|
6846
|
+
canModifyComment: (ctx) =>
|
|
6847
|
+
// Moderators may remove anything. Editing stays the author's, whoever you are.
|
|
6848
|
+
(ctx.action !== 'edit-comment' && isModerator(ctx.currentUserId)) ||
|
|
6849
|
+
defaultCanModifyComment(ctx),
|
|
6850
|
+
})
|
|
6851
|
+
```
|
|
6852
|
+
|
|
6853
|
+
The `ctx` carries the editor, the viewer's `currentUserId`, and the write itself as a discriminated union: `{ action: 'edit-comment' | 'delete-comment', comment }` or `{ action: 'delete-thread', thread }`. Narrowing works too — return false to close edits after an hour, or on a resolved thread.
|
|
6854
|
+
|
|
6855
|
+
Resolving, reopening, reacting, and moving a pin aren't asked about: none of them is anyone's in particular, so `canComment` is the only gate on them. `canModifyComment` is checked after `canComment`, so a viewer who may not participate gets no action affordances whatever it returns. Like `canComment`, it's read during render (through `useCanModifyComment`), so a callback that reads signals re-evaluates when they change.
|
|
6856
|
+
|
|
6857
|
+
Whatever you decide here, decide it again on the server: `createCommentAuthorizers` takes a `canModifyComment` of its own, and it's the one that counts. See [enforcing the same rules on the server](#syncing-comments).
|
|
6858
|
+
|
|
6859
|
+
`canComment` and `canModifyComment` hide UI. They don't enforce anything. Comment records carry a
|
|
6860
|
+
client-supplied `createdBy` and `authorId`, and a client can write whatever your sync server
|
|
6861
|
+
accepts. Enforce permissions in your server's record authorization, and verify author ids against
|
|
6862
|
+
the session's authenticated identity. See [Syncing comments](#syncing-comments).
|
|
6863
|
+
|
|
6864
|
+
#### Mentions
|
|
6865
|
+
|
|
6866
|
+
Composers support `@`-mentions. Supply the roster with `getMentionSuggestions`, which can be synchronous or async:
|
|
6867
|
+
|
|
6868
|
+
```tsx
|
|
6869
|
+
import { CanvasComments, filterMentionMembers, MentionMember } from '@tldraw/commenting'
|
|
6870
|
+
|
|
6871
|
+
const MEMBERS: MentionMember[] = [
|
|
6872
|
+
{ id: 'me', name: 'You', color: '#EC5E41', you: true },
|
|
6873
|
+
{ id: 'ada', name: 'Ada Lovelace', color: '#0E9F6E' },
|
|
6874
|
+
{ id: 'grace', name: 'Grace Hopper', color: '#4465E9', secondary: 'grace@example.com' },
|
|
6875
|
+
]
|
|
6876
|
+
|
|
6877
|
+
function Comments() {
|
|
6878
|
+
return (
|
|
6879
|
+
<CanvasComments
|
|
6880
|
+
currentUserId="me"
|
|
6881
|
+
resolveAuthor={resolveAuthor}
|
|
6882
|
+
getMentionSuggestions={(query) => filterMentionMembers(MEMBERS, query)}
|
|
6883
|
+
/>
|
|
6884
|
+
)
|
|
6885
|
+
}
|
|
6886
|
+
```
|
|
6887
|
+
|
|
6888
|
+
A `MentionMember` is a `CommentAuthor` plus an `id`, an optional `secondary` line for the picker, and `you` to mark the current user. `filterMentionMembers` does the matching; supply your own filter to query a server instead. To change how a row looks, pass `renderMentionSuggestion`.
|
|
6889
|
+
|
|
6890
|
+
A mention is a node in the body rather than text in it, so you can't find one by searching the string. To detect a mention, look for `{ type: 'mention', attrs: { id } }` in the body's `content` tree.
|
|
6891
|
+
|
|
6892
|
+
The mention components come from `@tldraw/mentions` and are re-exported here. That package also powers mentions in shape rich text. See [Rich text](https://tldraw.dev/sdk-features/rich-text).
|
|
6893
|
+
|
|
6894
|
+
#### Reactions
|
|
6895
|
+
|
|
6896
|
+
Hover a comment and open the picker to react with an emoji. A pill appears with a live count, and hovering it names who reacted.
|
|
6897
|
+
|
|
6898
|
+
Each reaction is its own `comment-reaction` record, one per (comment, user, emoji), rather than a field on the comment. Two people reacting at once therefore write different records and neither can clobber the other. The record id is derived from the triple, so re-picking an emoji toggles it. Registering `commentSchemaRecords` registers the reaction type along with the rest.
|
|
6899
|
+
|
|
6900
|
+
Reactions are multi-select by default, where each emoji toggles independently. Set `allowMultipleReactions: false` for single-select, where a new emoji replaces your existing one.
|
|
6901
|
+
|
|
6902
|
+
The palette is pluggable. A reaction's emoji is treated as an opaque token: the layer stores it, syncs it, and hands it to a renderer, and never assumes it's a glyph. So you can swap in tokens of your own.
|
|
6903
|
+
|
|
6904
|
+
```tsx
|
|
6905
|
+
CommentTool.configure({
|
|
6906
|
+
components: {
|
|
6907
|
+
ReactionContent: MyTokenRenderer, // how a token is drawn
|
|
6908
|
+
ReactionPalette: MyPalette, // what the add-reaction button opens
|
|
6909
|
+
},
|
|
6910
|
+
isAllowedReaction: (token) => isMyToken(token) || isAllowedReactionEmoji(token),
|
|
6911
|
+
})
|
|
6912
|
+
```
|
|
6913
|
+
|
|
6914
|
+
`isAllowedReaction` is enforced client-side. If arbitrary tokens would be a problem for you, validate them on your server too.
|
|
6915
|
+
|
|
6916
|
+
#### Region comments
|
|
6917
|
+
|
|
6918
|
+
A region thread covers a rectangular area rather than a point. Regions are off by default, so the tool stays click-only until you turn them on:
|
|
6919
|
+
|
|
6920
|
+
```tsx
|
|
6921
|
+
CommentTool.configure({ enableRegions: true })
|
|
6922
|
+
```
|
|
6923
|
+
|
|
6924
|
+
That's the whole configuration. A region reveals its dashed box and resize handles while the pointer is inside it, moves by its pin, and resizes from its corners. The pin sits on whichever corner the creating drag was released on, which the anchor remembers.
|
|
6925
|
+
|
|
6926
|
+
#### The sidebar
|
|
6927
|
+
|
|
6928
|
+
`CanvasCommentsSidebar` lists comment threads in a panel beside the canvas. Clicking a row brings that thread's pin into view and opens it.
|
|
6929
|
+
|
|
6930
|
+
Its open state is a signal you drive, so the toggle lives wherever your app wants it. `useCommentsSidebarOpen` reads it and `toggleCommentsSidebar` flips it; the underlying `commentsSidebarOpen` atom is exported too, for code that has an editor but no React context:
|
|
6931
|
+
|
|
6932
|
+
```tsx
|
|
6933
|
+
import {
|
|
6934
|
+
CanvasComments,
|
|
6935
|
+
CanvasCommentsSidebar,
|
|
6936
|
+
CommentingContext,
|
|
6937
|
+
toggleCommentsSidebar,
|
|
6938
|
+
useCommentsSidebarOpen,
|
|
6939
|
+
} from '@tldraw/commenting'
|
|
6940
|
+
import { TldrawUiButton, TldrawUiButtonLabel, useEditor } from 'tldraw'
|
|
6941
|
+
|
|
6942
|
+
function SidebarToggle() {
|
|
6943
|
+
const editor = useEditor()
|
|
6944
|
+
const open = useCommentsSidebarOpen()
|
|
6945
|
+
return (
|
|
6946
|
+
<TldrawUiButton type="normal" onClick={() => toggleCommentsSidebar(editor)}>
|
|
6947
|
+
<TldrawUiButtonLabel>{open ? 'Hide comments' : 'Comments'}</TldrawUiButtonLabel>
|
|
6948
|
+
</TldrawUiButton>
|
|
6949
|
+
)
|
|
6950
|
+
}
|
|
6951
|
+
|
|
6952
|
+
// Both surfaces read the same context, so build it once and spread it into each.
|
|
6953
|
+
const commenting: CommentingContext = { currentUserId: 'me', resolveAuthor }
|
|
6954
|
+
|
|
6955
|
+
const components: TLComponents = {
|
|
6956
|
+
InFrontOfTheCanvas: () => (
|
|
6957
|
+
<>
|
|
6958
|
+
<CanvasComments {...commenting} />
|
|
6959
|
+
<CanvasCommentsSidebar {...commenting} />
|
|
6960
|
+
</>
|
|
6961
|
+
),
|
|
6962
|
+
SharePanel: SidebarToggle,
|
|
6963
|
+
}
|
|
6964
|
+
```
|
|
6965
|
+
|
|
6966
|
+
The sidebar's filters cover resolved threads, only comments you made, only unread comments, and only the current page. They're held per editor, so a user's choices survive the panel closing. Pass `header` and `empty` to replace the chrome around the list.
|
|
6967
|
+
|
|
6968
|
+
#### Unread state
|
|
6969
|
+
|
|
6970
|
+
The layer doesn't track who has read what. That data lives in your app. Supply it as two callbacks. `isCommentUnread` reports whether a comment is unread, and `onCommentRead` fires for each unread comment the user actually sees in an open thread.
|
|
6971
|
+
|
|
6972
|
+
```tsx
|
|
6973
|
+
<CanvasComments
|
|
6974
|
+
currentUserId="me"
|
|
6975
|
+
resolveAuthor={resolveAuthor}
|
|
6976
|
+
isCommentUnread={(commentId) => !readReceipts.has(commentId)}
|
|
6977
|
+
onCommentRead={(commentId) => markCommentRead(commentId)}
|
|
6978
|
+
onPostComment={(comment) => notifyMentionedUsers(comment)}
|
|
6979
|
+
/>
|
|
6980
|
+
```
|
|
6981
|
+
|
|
6982
|
+
Unread state drives the pin badges and the sidebar's unread filter. Without `isCommentUnread`, both are hidden. `onPostComment` fires when this user posts a comment through one of the built-in composers — a new thread or a reply — which is where notifications and mention emails belong. It does not fire for comments arriving over sync, so the sender is the one who notifies.
|
|
6983
|
+
|
|
6984
|
+
#### Custom components
|
|
6985
|
+
|
|
6986
|
+
Every visible piece of the layer is a slot. Set them through the `components` option, and leave a slot unset to keep its default.
|
|
6987
|
+
|
|
6988
|
+
| Slot | Replaces |
|
|
6989
|
+
| ------------------ | ---------------------------------------------------------------------- |
|
|
6990
|
+
| `CommentBody` | A comment's body, normally the rich-text renderer. |
|
|
6991
|
+
| `PinContent` | A pin's inner content, normally the author's initial. |
|
|
6992
|
+
| `ThreadPreview` | A sidebar row's preview, normally the body as plain text. |
|
|
6993
|
+
| `ThreadRow` | A whole sidebar row, normally `CommentListItem`. |
|
|
6994
|
+
| `ThreadActions` | Adds controls to an open thread's header. Additive, not a replacement. |
|
|
6995
|
+
| `ComposerFallback` | What shows where a composer would sit when the viewer can't comment. |
|
|
6996
|
+
| `ReactionContent` | How a reaction token is drawn. |
|
|
6997
|
+
| `ReactionPalette` | What the add-reaction button opens. |
|
|
6998
|
+
| `ReactionTooltip` | The list of who reacted, shown on a reaction pill. |
|
|
6999
|
+
|
|
7000
|
+
```tsx
|
|
7001
|
+
import { CommentTool, richTextToPlaintext } from '@tldraw/commenting'
|
|
7002
|
+
import { TLComment } from 'tldraw'
|
|
7003
|
+
|
|
7004
|
+
function PriorityBody({ comment }: { comment: TLComment }) {
|
|
7005
|
+
const urgent = comment.meta.priority === 'urgent'
|
|
7006
|
+
return (
|
|
7007
|
+
<div className={urgent ? 'urgent-comment' : 'comment'}>{richTextToPlaintext(comment.body)}</div>
|
|
7008
|
+
)
|
|
7009
|
+
}
|
|
7010
|
+
|
|
7011
|
+
const tools = [CommentTool.configure({ components: { CommentBody: PriorityBody } })]
|
|
7012
|
+
```
|
|
7013
|
+
|
|
7014
|
+
Both record types carry a `meta` field the layer never reads. Use it for priorities, categories, external ticket ids, or anything else your app tracks alongside a comment.
|
|
7015
|
+
|
|
7016
|
+
`ThreadRow` and `ThreadActions` are the two slots that add to a surface rather than replace a piece of it, so they get the records behind what's on screen.
|
|
7017
|
+
|
|
7018
|
+
`ThreadRow` receives the summarized row along with the thread record, and the default row is exported — so a row that only adds something can spread the props into `CommentListItem` rather than start over:
|
|
7019
|
+
|
|
7020
|
+
```tsx
|
|
7021
|
+
import { CommentListItem, CommentTool } from '@tldraw/commenting'
|
|
7022
|
+
|
|
7023
|
+
function StatusRow({ thread, ...row }: CommentListItemRenderProps & { thread: TLCommentThread }) {
|
|
7024
|
+
return (
|
|
7025
|
+
<div className="status-row">
|
|
7026
|
+
<CommentListItem {...row} />
|
|
7027
|
+
{thread.meta.status === 'blocked' && <span className="blocked">Blocked</span>}
|
|
7028
|
+
</div>
|
|
7029
|
+
)
|
|
7030
|
+
}
|
|
7031
|
+
|
|
7032
|
+
const tools = [CommentTool.configure({ components: { ThreadRow: StatusRow } })]
|
|
7033
|
+
```
|
|
7034
|
+
|
|
7035
|
+
`ThreadActions` adds controls to an open thread's header, ahead of the built-in resolve and dismiss buttons. This is where host verbs go — assign a thread, link it to a ticket, mark it as a to-do. It adds alongside the built-in actions rather than replacing them, so a thread never loses the ability to be resolved.
|
|
7036
|
+
|
|
7037
|
+
```tsx
|
|
7038
|
+
function AssignAction({ thread }: { thread: TLCommentThread }) {
|
|
7039
|
+
return (
|
|
7040
|
+
<button type="button" className="tlui-cmt-thread__action" onClick={() => assign(thread.id)}>
|
|
7041
|
+
Assign
|
|
7042
|
+
</button>
|
|
7043
|
+
)
|
|
7044
|
+
}
|
|
7045
|
+
|
|
7046
|
+
const tools = [CommentTool.configure({ components: { ThreadActions: AssignAction } })]
|
|
7047
|
+
```
|
|
7048
|
+
|
|
7049
|
+
A link affordance doesn't need this slot: see [Linking to a thread](#linking-to-a-thread).
|
|
7050
|
+
|
|
7051
|
+
#### Working with comment records
|
|
7052
|
+
|
|
7053
|
+
Comment records aren't part of the `TLRecord` union, so `editor.store` doesn't know their types statically. These helpers own that reinterpretation and keep your call sites typed.
|
|
7054
|
+
|
|
7055
|
+
| Helper | Description |
|
|
7056
|
+
| ----------------------- | ------------------------------------------------------------------------------------ |
|
|
7057
|
+
| `getLiveCommentThreads` | The threads that render. |
|
|
7058
|
+
| `getLiveComments` | The comments that render. |
|
|
7059
|
+
| `getCommentThreads` | Every thread in the store, including deleted ones. |
|
|
7060
|
+
| `getComments` | Every comment in the store, including deleted ones. |
|
|
7061
|
+
| `getCommentRecord` | One record by id, or `undefined`. |
|
|
7062
|
+
| `putCommentRecords` | Write threads and comments. |
|
|
7063
|
+
| `removeCommentRecords` | Remove them by id. Rarely what you want — see [Writing comments](#writing-comments). |
|
|
7064
|
+
|
|
7065
|
+
Prefer the live reads. Deleting a comment doesn't remove its record — it flags it and lets the server prune it, as [Writing comments](#writing-comments) explains — so the unfiltered reads include records that nothing renders. `getLiveCommentThreads` also drops threads whose comments have all gone, which have no surface left.
|
|
7066
|
+
|
|
7067
|
+
Those reads are non-reactive. In React use the hooks instead, which read the live set: `useCommentThreads`, `useComments`, and `useThreadComments` for one thread's replies, oldest first.
|
|
7068
|
+
|
|
7069
|
+
```tsx
|
|
7070
|
+
import { useCommentThreads } from '@tldraw/commenting'
|
|
7071
|
+
import { useEditor } from 'tldraw'
|
|
7072
|
+
|
|
7073
|
+
function OpenThreadCount() {
|
|
7074
|
+
const editor = useEditor()
|
|
7075
|
+
const threads = useCommentThreads(editor)
|
|
7076
|
+
const open = threads.filter((thread) => !thread.resolved)
|
|
7077
|
+
return <div>{open.length} open threads</div>
|
|
7078
|
+
}
|
|
7079
|
+
```
|
|
7080
|
+
|
|
7081
|
+
#### Writing comments
|
|
7082
|
+
|
|
7083
|
+
To post a thread yourself, whether you're seeding a document with review notes or importing comments from another system, build the records and write them:
|
|
7084
|
+
|
|
7085
|
+
```tsx
|
|
7086
|
+
import { putCommentRecords } from '@tldraw/commenting'
|
|
7087
|
+
import { createComment, createCommentThread, toRichText } from 'tldraw'
|
|
7088
|
+
|
|
7089
|
+
const thread = createCommentThread({
|
|
7090
|
+
pageId: editor.getCurrentPageId(),
|
|
7091
|
+
anchor: { type: 'point', x: 120, y: 240 },
|
|
7092
|
+
createdBy: 'ada',
|
|
7093
|
+
})
|
|
7094
|
+
|
|
7095
|
+
const comment = createComment({
|
|
7096
|
+
threadId: thread.id,
|
|
7097
|
+
pageId: thread.pageId,
|
|
7098
|
+
authorId: 'ada',
|
|
7099
|
+
body: toRichText('Can we make this arrow dashed?'),
|
|
7100
|
+
})
|
|
7101
|
+
|
|
7102
|
+
putCommentRecords(editor, [thread, comment])
|
|
7103
|
+
```
|
|
7104
|
+
|
|
7105
|
+
The other writes each have a rule attached — a timestamp to stamp, or the delete protocol below — so they come as functions. The built-in thread view calls exactly these, so a UI of your own behaves like the one in the box.
|
|
7106
|
+
|
|
7107
|
+
| Helper | Description |
|
|
7108
|
+
| ------------------------------- | ------------------------------------------------------ |
|
|
7109
|
+
| `editComment` | Replace a comment's body and mark it edited. |
|
|
7110
|
+
| `resolveThread`, `reopenThread` | Resolve a thread, stamping who and when, or reopen it. |
|
|
7111
|
+
| `deleteComment` | Delete a comment. |
|
|
7112
|
+
| `deleteThread` | Delete a thread and its whole conversation. |
|
|
7113
|
+
|
|
7114
|
+
```tsx
|
|
7115
|
+
import { deleteComment, resolveThread } from '@tldraw/commenting'
|
|
7116
|
+
|
|
7117
|
+
resolveThread(editor, thread, currentUserId)
|
|
7118
|
+
deleteComment(editor, comment)
|
|
7119
|
+
```
|
|
7120
|
+
|
|
7121
|
+
The record you pass says which comment or thread to act on; the change itself lands on the version currently in the store. So a record you took a copy of earlier is safe to pass: it won't put back a field that has moved since — a thread's anchor changes on its own as pinned shapes move, and a comment's body changes when its author edits it elsewhere — and it won't re-create a record that has already been deleted, which a plain `putCommentRecords` of a stale copy would. If the record is gone, the call does nothing.
|
|
7122
|
+
|
|
7123
|
+
##### Deleting is a soft delete
|
|
7124
|
+
|
|
7125
|
+
`deleteComment` and `deleteThread` don't remove records. They set an `isDeleted` flag and leave the pruning to the server, which then removes the thread, its comments, and their reactions.
|
|
7126
|
+
|
|
7127
|
+
That indirection is what makes deleting safe in a shared document. A reaction belongs to whoever left it, not to whoever is deleting the comment underneath it, so no client should be removing it — and a server enforcing per-record permissions gets a write it can check against the session's identity rather than a deletion it can only refuse. `removeCommentRecords` is a hard delete, and against such a server it will simply be rejected. Reach for it on a local, unsynced comment store.
|
|
7128
|
+
|
|
7129
|
+
The flag is write-once server-side, so deletes are never undoable, whatever `history` says: an undo clearing the flag would be vetoed and rebased rather than bring the comment back.
|
|
7130
|
+
|
|
7131
|
+
Deleting a thread's last comment leaves the thread with nothing to render. The record stays for the server to prune, since whoever deleted the comment may not be the thread's creator.
|
|
7132
|
+
|
|
7133
|
+
#### Revealing a thread
|
|
7134
|
+
|
|
7135
|
+
To open a thread from outside the canvas, whether from a notification, a shared link, or a list of your own, call `revealThread` with a thread or comment id. `CanvasComments` serves the request: it waits for the records to arrive, switches pages if it needs to, zooms in if the pin is inside a cluster, then opens the thread.
|
|
7136
|
+
|
|
7137
|
+
```tsx
|
|
7138
|
+
import { revealThread } from '@tldraw/commenting'
|
|
7139
|
+
|
|
7140
|
+
// e.g. from a ?comment=<id> search param
|
|
7141
|
+
revealThread(editor, commentId)
|
|
7142
|
+
```
|
|
7143
|
+
|
|
7144
|
+
An unserved request is inert, so it's safe to call before the records have synced in. `useRevealThreadPending` returns the id of a request that hasn't been served yet, which is how you notice a deep link to a comment that no longer exists — give it a grace period first, since a request also sits there while its records are still arriving, and re-check with `getRevealThreadPending(editor)` when the grace period elapses so you don't act on a request that cleared inside it.
|
|
7145
|
+
|
|
7146
|
+
For a thread you already hold, `focusThread(editor, thread)` centers and opens it directly.
|
|
7147
|
+
|
|
7148
|
+
#### Linking to a thread
|
|
7149
|
+
|
|
7150
|
+
The other half of a deep link is producing one. Give the commenting context a `getThreadHref` and every surface that can link to a thread does:
|
|
7151
|
+
|
|
7152
|
+
```tsx
|
|
7153
|
+
<CanvasComments
|
|
7154
|
+
currentUserId="me"
|
|
7155
|
+
resolveAuthor={resolveAuthor}
|
|
7156
|
+
getThreadHref={(threadId) => `/file/${fileId}?comment=${encodeURIComponent(threadId)}`}
|
|
7157
|
+
/>
|
|
7158
|
+
```
|
|
7159
|
+
|
|
7160
|
+
Sidebar rows become anchors, so ctrl/cmd-click and middle-click open a thread in a new tab, and an open thread's header menu offers **Copy link**. A relative href is resolved against the current document before it reaches the clipboard, so what gets pasted is a whole URL.
|
|
7161
|
+
|
|
7162
|
+
Without `getThreadHref` neither appears — a link the host can't construct isn't one the layer can invent.
|
|
7163
|
+
|
|
7164
|
+
#### Pin clustering
|
|
7165
|
+
|
|
7166
|
+
Zoom out far enough and pins pile on top of each other. Clustering folds nearby pins into a count badge as you zoom out, then splits them apart as you zoom back in. Splits happen at a wider spacing than merges, so pins don't flicker at the boundary. Clicking a badge zooms to just past the point where that cluster breaks up.
|
|
7167
|
+
|
|
7168
|
+
Clustering is on by default. Turn it off with `enableClustering: false`.
|
|
7169
|
+
|
|
7170
|
+
The work is precomputed once per comment add, remove, or move, so per-frame camera changes cost a single walk over a sorted event table. See the [comment clustering example](https://tldraw.dev/examples/collaboration/comment-clustering).
|
|
7171
|
+
|
|
7172
|
+
#### Syncing comments
|
|
7173
|
+
|
|
7174
|
+
Comment records don't ship in the default schema, so both ends of the connection have to register them, and they have to match. A client with the types talking to a server without them will fail schema validation.
|
|
7175
|
+
|
|
7176
|
+
On the client, pass `records` to the sync hook:
|
|
7177
|
+
|
|
7178
|
+
```tsx
|
|
7179
|
+
const store = useSync({
|
|
7180
|
+
uri: `wss://your-server.com/sync/${roomId}`,
|
|
7181
|
+
assets: myAssetStore,
|
|
7182
|
+
records: commentSchemaRecords,
|
|
7183
|
+
})
|
|
7184
|
+
```
|
|
7185
|
+
|
|
7186
|
+
On the server, pass the same map to `createTLSchema`:
|
|
7187
|
+
|
|
7188
|
+
```ts
|
|
7189
|
+
import { TLSocketRoom } from '@tldraw/sync-core'
|
|
7190
|
+
import { commentSchemaRecords, createTLSchema } from '@tldraw/tlschema'
|
|
7191
|
+
|
|
7192
|
+
const schema = createTLSchema({ records: commentSchemaRecords })
|
|
7193
|
+
|
|
7194
|
+
const room = new TLSocketRoom({
|
|
7195
|
+
schema,
|
|
7196
|
+
// Serve comments through the object-store lane rather than the document
|
|
7197
|
+
objectTypes: ['comment', 'comment-thread', 'comment-reaction'],
|
|
7198
|
+
})
|
|
7199
|
+
```
|
|
7200
|
+
|
|
7201
|
+
`objectTypes` moves those record types onto a separate lane. Lane records are stored apart from the document and left out of document snapshots. More usefully, they're gated by their own per-session permission rather than by `isReadonly`, so a session can be allowed to comment without being allowed to edit:
|
|
7202
|
+
|
|
7203
|
+
```ts
|
|
7204
|
+
room.handleSocketConnect({
|
|
7205
|
+
sessionId,
|
|
7206
|
+
socket,
|
|
7207
|
+
isReadonly: true, // can't touch the document
|
|
7208
|
+
objectAccess: 'write', // but can still comment
|
|
7209
|
+
})
|
|
7210
|
+
```
|
|
7211
|
+
|
|
7212
|
+
`objectAccess` is `'read'` or `'write'` and defaults to `'write'`. To persist the lane separately, read it with `TLSocketRoom`'s `getCurrentObjectsSnapshot()`. To mirror comments into your own database as they commit, use the room's `onCommittedChanges` callback:
|
|
7213
|
+
|
|
7214
|
+
```ts
|
|
7215
|
+
const room = new TLSocketRoom({
|
|
7216
|
+
schema,
|
|
7217
|
+
objectTypes: ['comment', 'comment-thread', 'comment-reaction'],
|
|
7218
|
+
onCommittedChanges({ diff }) {
|
|
7219
|
+
// Project comment records into Postgres for notifications and search
|
|
7220
|
+
projectComments(diff)
|
|
7221
|
+
},
|
|
7222
|
+
})
|
|
7223
|
+
```
|
|
7224
|
+
|
|
7225
|
+
`onCommittedChanges` only fires for client pushes. Server-initiated writes don't trigger it, including `updateStore`, `loadSnapshot`, and writing to storage directly, so anything mirroring room state has to handle those paths itself.
|
|
7226
|
+
|
|
7227
|
+
This is also where per-record permissions belong. The lane's write access is all or nothing, so rules like "only the author may edit a comment" or "only the thread's creator may delete it" are enforced as your server validates each incoming record against the session's identity. `createCommentAuthorizers` is that validation, ready-made — pass it to the room's `authorizeRecord`:
|
|
7228
|
+
|
|
7229
|
+
```ts
|
|
7230
|
+
import { createCommentAuthorizers } from '@tldraw/sync-collaboration'
|
|
7231
|
+
|
|
7232
|
+
const room = new TLSocketRoom({
|
|
7233
|
+
schema,
|
|
7234
|
+
objectTypes: ['comment', 'comment-thread', 'comment-reaction'],
|
|
7235
|
+
authorizeRecord: {
|
|
7236
|
+
...createCommentAuthorizers<SessionMeta>({
|
|
7237
|
+
getUserId: (session) => session.meta.userId,
|
|
7238
|
+
// Moderators may take anything down. Editing stays the author's, whoever you are.
|
|
7239
|
+
canModifyComment: (ctx) =>
|
|
7240
|
+
(ctx.action !== 'edit-comment' && isModerator(ctx.session.meta)) ||
|
|
7241
|
+
ctx.userId === ctx.ownerId,
|
|
7242
|
+
}),
|
|
7243
|
+
},
|
|
7244
|
+
})
|
|
7245
|
+
```
|
|
7246
|
+
|
|
7247
|
+
It stamps authorship from the session so nothing can be posted or resolved in someone else's name, keeps `isDeleted` write-once, and — through `canModifyComment` — decides who may edit a comment, delete a comment, or delete a thread. That last one is the same question the client's [`canModifyComment`](#who-can-edit-and-delete) answers, and this is the answer that counts: the client's only decides which affordances the UI offers. Widen the two together. A moderator offered a delete the server then rejects sees the comment disappear and come back, with nothing to explain it.
|
|
7248
|
+
|
|
7249
|
+
Left unset it defaults to the record's owner, matching the client. The callback is asked after the structural rules, so widening it grants those three writes and nothing else: attribution stays immutable, a soft delete stays write-once, and clients still can't hard-delete a record.
|
|
7250
|
+
|
|
7251
|
+
#### Building your own comments UI
|
|
7252
|
+
|
|
7253
|
+
`CanvasComments` is one way to assemble the parts, and every part it uses is exported. You can rebuild it, or build something quite different, from the same pieces.
|
|
7254
|
+
|
|
7255
|
+
| Export | Description |
|
|
7256
|
+
| ---------------------------------------- | ----------------------------------------------------------- |
|
|
7257
|
+
| `CommentTool`, `commentToolOverrides` | Placement: the tool state machine and its toolbar entry. |
|
|
7258
|
+
| `CommentPin`, `CountBadge` | The pin marker and the clustered-count badge. |
|
|
7259
|
+
| `CommentThread`, `CommentCard`, `Byline` | A thread and its comments. |
|
|
7260
|
+
| `CommentComposer`, `SendButton` | The composer and its send control. |
|
|
7261
|
+
| `CommentsList`, `CommentListItem` | A list of threads and one row of it. |
|
|
7262
|
+
| `EmptyState`, `sortSidebarRows` | The list's empty state, and the sidebar's ordering. |
|
|
7263
|
+
| `Avatar`, `Mention`, `MentionList` | Author avatars and the mention picker. |
|
|
7264
|
+
| `Reaction`, `Reactions` | A reaction pill and a comment's row of them. |
|
|
7265
|
+
| `anchorPagePoint`, `shapeAnchorAt` | Anchor math: page position from an anchor, and the reverse. |
|
|
7266
|
+
| `editComment`, `deleteComment` | The write verbs. See [Writing comments](#writing-comments). |
|
|
7267
|
+
| `registerCommentAnchorLifecycle` | Keeps shape-anchored threads alive across shape deletion. |
|
|
7268
|
+
|
|
7269
|
+
The presentational components take plain props and know nothing about the editor. The canvas layer, the tool, and the hooks build on them. `CanvasComments` registers the anchor lifecycle for you, so call `registerCommentAnchorLifecycle` yourself only if you're replacing the layer wholesale.
|
|
7270
|
+
|
|
7271
|
+
#### All options
|
|
7272
|
+
|
|
7273
|
+
Every option below is set with `CommentTool.configure()`. Anything you leave unset falls back to `defaultCommentingOptions`.
|
|
7274
|
+
|
|
7275
|
+
##### History
|
|
7276
|
+
|
|
7277
|
+
| Option | Default | Description |
|
|
7278
|
+
| ------------- | ----------- | ----------------------------------------------------------------------- |
|
|
7279
|
+
| `history` | `'ignore'` | How comment writes interact with the undo stack. |
|
|
7280
|
+
| `dragHistory` | `undefined` | History mode for pin drags specifically. Unset, drags follow `history`. |
|
|
7281
|
+
|
|
7282
|
+
##### Anchoring
|
|
7283
|
+
|
|
7284
|
+
| Option | Default | Description |
|
|
7285
|
+
| ---------------------- | -------------- | --------------------------------------------------------- |
|
|
7286
|
+
| `shouldBePrecise` | `() => true` | Whether a shape placement anchors precisely. |
|
|
7287
|
+
| `impreciseShapeAnchor` | `{x: 1, y: 0}` | Where imprecise shape pins sit within the shape's bounds. |
|
|
7288
|
+
|
|
7289
|
+
##### Permissions
|
|
7290
|
+
|
|
7291
|
+
| Option | Default | Description |
|
|
7292
|
+
| ------------------ | ----------- | ----------------------------------------------------------------------------------- |
|
|
7293
|
+
| `canComment` | `undefined` | Whether the viewer may participate. Unset, allowed whenever `currentUserId` is set. |
|
|
7294
|
+
| `canModifyComment` | `undefined` | Whether the viewer may edit or delete a record. Unset, each is its owner's to make. |
|
|
7295
|
+
|
|
7296
|
+
##### Reactions
|
|
7297
|
+
|
|
7298
|
+
| Option | Default | Description |
|
|
7299
|
+
| ------------------------ | ------------- | ------------------------------------------------------------ |
|
|
7300
|
+
| `allowMultipleReactions` | `true` | Whether a user can hold more than one reaction on a comment. |
|
|
7301
|
+
| `isAllowedReaction` | emoji palette | Which reaction tokens may be written. |
|
|
7302
|
+
|
|
7303
|
+
##### Regions
|
|
7304
|
+
|
|
7305
|
+
| Option | Default | Description |
|
|
7306
|
+
| --------------- | ------- | ---------------------------------------------------------- |
|
|
7307
|
+
| `enableRegions` | `false` | Whether dragging the comment tool creates a region anchor. |
|
|
7308
|
+
|
|
7309
|
+
##### Clustering
|
|
7310
|
+
|
|
7311
|
+
| Option | Default | Description |
|
|
7312
|
+
| ------------------ | ------- | ----------------------------------------------------------- |
|
|
7313
|
+
| `enableClustering` | `true` | Fold nearby pins into count badges as the camera zooms out. |
|
|
7314
|
+
|
|
7315
|
+
##### Components
|
|
7316
|
+
|
|
7317
|
+
| Option | Default | Description |
|
|
7318
|
+
| ------------ | ------- | ----------------------------------------------------------------- |
|
|
7319
|
+
| `components` | `{}` | Component overrides. See [Custom components](#custom-components). |
|
|
7320
|
+
|
|
7321
|
+
#### Related articles
|
|
7322
|
+
|
|
7323
|
+
- [Commenting](https://tldraw.dev/docs/commenting) — A shorter introduction to the feature
|
|
7324
|
+
- [Collaboration](https://tldraw.dev/sdk-features/collaboration) — Presence, sync hooks, and custom sync
|
|
7325
|
+
- [tldraw sync](https://tldraw.dev/docs/sync) — Running a sync server
|
|
7326
|
+
- [Rich text](https://tldraw.dev/sdk-features/rich-text) — The `TLRichText` format comment bodies use
|
|
7327
|
+
- [License key](https://tldraw.dev/sdk-features/license-key) — Enabling licensed features in production
|
|
7328
|
+
|
|
7329
|
+
#### Related examples
|
|
7330
|
+
|
|
7331
|
+
- [Commenting](https://tldraw.dev/examples/collaboration/commenting) — The full flow: tool, pins, threads, mentions
|
|
7332
|
+
- [Commenting sidebar](https://tldraw.dev/examples/collaboration/commenting-sidebar) — A thread list panel beside the canvas
|
|
7333
|
+
- [Comment clustering](https://tldraw.dev/examples/collaboration/comment-clustering) — Merging pins into count badges as you zoom out
|
|
7334
|
+
- [Comment anchors](https://tldraw.dev/examples/collaboration/comment-anchors) — The ways a comment can attach to the canvas
|
|
7335
|
+
- [Shape comment precision](https://tldraw.dev/examples/collaboration/comment-shape-precision) — Precise, shape-level, and Alt-gated anchoring
|
|
7336
|
+
- [Comments and undo](https://tldraw.dev/examples/collaboration/comment-history) — How comment writes interact with the undo stack
|
|
7337
|
+
- [Region comments](https://tldraw.dev/examples/collaboration/comment-regions) — Commenting on an area, and tuning the interaction
|
|
7338
|
+
|
|
6340
7339
|
### Coordinates
|
|
6341
7340
|
|
|
6342
7341
|
The editor uses three coordinate systems: screen space, viewport space, and page space. When you move the mouse over the canvas, the browser gives you screen coordinates. To create a shape at that location, you need to convert those coordinates to page space.
|
|
@@ -6746,7 +7745,7 @@ export default function App() {
|
|
|
6746
7745
|
}
|
|
6747
7746
|
```
|
|
6748
7747
|
|
|
6749
|
-
Remote users' chat messages render as part of the collaborator cursor
|
|
7748
|
+
Remote users' chat messages render as part of the collaborator cursor, in the DOM cursor layer. To customize how they appear, replace the `CollaboratorCursor` component — see [Cursors](https://tldraw.dev/sdk-features/cursors#rendering-collaborator-cursors) for details.
|
|
6750
7749
|
|
|
6751
7750
|
#### Availability
|
|
6752
7751
|
|
|
@@ -6916,48 +7915,50 @@ editor.user.updateUserPreferences({ color: '#FF802B' })
|
|
|
6916
7915
|
|
|
6917
7916
|
##### Rendering collaborator cursors
|
|
6918
7917
|
|
|
6919
|
-
Remote cursors render
|
|
7918
|
+
Remote cursors render as DOM elements in a dedicated layer stacked above the canvas and below the UI panels. Each visible collaborator gets a cursor showing:
|
|
6920
7919
|
|
|
6921
7920
|
- The cursor arrow in the user's color
|
|
6922
7921
|
- The user's name as a label next to the cursor
|
|
6923
7922
|
- Any active chat message in a bubble
|
|
6924
7923
|
|
|
6925
|
-
|
|
7924
|
+
A collaborator whose cursor is outside your viewport shows as a small hint arrow clamped to the viewport edge, pointing toward them — the hints stay canvas-drawn (`CollaboratorHintOverlayUtil`), like the other drawing chrome.
|
|
7925
|
+
|
|
7926
|
+
To customize the cursor, pass your own component via the `components` prop's `CollaboratorCursor` slot (see `DefaultCursor` and `TLCursorProps`):
|
|
6926
7927
|
|
|
6927
7928
|
```tsx
|
|
6928
|
-
import {
|
|
7929
|
+
import { TLCursorProps, Tldraw } from 'tldraw'
|
|
6929
7930
|
import 'tldraw/tldraw.css'
|
|
6930
7931
|
|
|
6931
|
-
|
|
6932
|
-
|
|
6933
|
-
|
|
6934
|
-
|
|
6935
|
-
|
|
6936
|
-
|
|
6937
|
-
|
|
6938
|
-
|
|
6939
|
-
|
|
6940
|
-
|
|
6941
|
-
|
|
6942
|
-
|
|
6943
|
-
|
|
6944
|
-
|
|
6945
|
-
|
|
6946
|
-
}
|
|
6947
|
-
|
|
6948
|
-
|
|
7932
|
+
function CustomCollaboratorCursor({ point, color, name, zoom }: TLCursorProps) {
|
|
7933
|
+
if (!point) return null
|
|
7934
|
+
return (
|
|
7935
|
+
<div
|
|
7936
|
+
style={{
|
|
7937
|
+
position: 'absolute',
|
|
7938
|
+
// The layer is scaled by the camera, so counter-scale by 1 / zoom to keep the
|
|
7939
|
+
// cursor a constant on-screen size (the default components do the same).
|
|
7940
|
+
transform: `translate(${point.x}px, ${point.y}px) scale(${1 / zoom})`,
|
|
7941
|
+
transformOrigin: 'top left',
|
|
7942
|
+
pointerEvents: 'none',
|
|
7943
|
+
}}
|
|
7944
|
+
>
|
|
7945
|
+
{/* A dot instead of the default arrow */}
|
|
7946
|
+
<div style={{ width: 16, height: 16, borderRadius: '50%', backgroundColor: color }} />
|
|
7947
|
+
{name && <div style={{ color }}>{name}</div>}
|
|
7948
|
+
</div>
|
|
7949
|
+
)
|
|
6949
7950
|
}
|
|
6950
7951
|
|
|
6951
7952
|
export default function App() {
|
|
6952
7953
|
return (
|
|
6953
7954
|
<div style={{ position: 'fixed', inset: 0 }}>
|
|
6954
|
-
<Tldraw
|
|
7955
|
+
<Tldraw components={{ CollaboratorCursor: CustomCollaboratorCursor }} />
|
|
6955
7956
|
</div>
|
|
6956
7957
|
)
|
|
6957
7958
|
}
|
|
6958
7959
|
```
|
|
6959
7960
|
|
|
6960
|
-
|
|
7961
|
+
Cursor components are positioned in page space inside a camera-transformed layer, so `point` can be used directly as a translation; sizes in screen pixels stay constant on screen by scaling with `1 / zoom` (the default components do this via their `zoom` prop).
|
|
6961
7962
|
|
|
6962
7963
|
##### Cursor position in presence
|
|
6963
7964
|
|
|
@@ -13405,10 +14406,9 @@ The `tldraw` package includes these overlay utils by default:
|
|
|
13405
14406
|
| `ScribbleOverlayUtil` | Eraser and lasso scribbles |
|
|
13406
14407
|
| `ArrowHintOverlayUtil` | Target hints while drawing arrows |
|
|
13407
14408
|
| `ArrowBindingHintOverlayUtil` | Binding hints when dragging arrow handles |
|
|
13408
|
-
| `CollaboratorCursorOverlayUtil` | Remote users' cursors, name tags, and chat |
|
|
13409
14409
|
| `CollaboratorBrushOverlayUtil` | Remote users' brush rectangles |
|
|
13410
14410
|
| `CollaboratorScribbleOverlayUtil` | Remote users' scribbles |
|
|
13411
|
-
| `CollaboratorHintOverlayUtil` | Remote users' viewport
|
|
14411
|
+
| `CollaboratorHintOverlayUtil` | Remote users' viewport-edge hint arrows |
|
|
13412
14412
|
| `CollaboratorShapeIndicatorOverlayUtil` | Remote users' selection indicators |
|
|
13413
14413
|
|
|
13414
14414
|
These are exported as `defaultOverlayUtils` from `tldraw`.
|
|
@@ -16079,6 +17079,8 @@ Shapes support parent-child hierarchies for grouping and frames, participate in
|
|
|
16079
17079
|
|
|
16080
17080
|
A shape record is a plain object stored in the editor's reactive store. All shape types extend `TLBaseShape`, which defines the common properties every shape has: a unique identifier, position and rotation, z-ordering index, parent reference, lock state, opacity, and a `props` field for shape-specific properties. The `props` field contains data unique to each shape type. A geo shape stores its width, height, and geometry type. A text shape stores its text content and font size. Each shape type defines its own props structure.
|
|
16081
17081
|
|
|
17082
|
+
Shapes also have a `meta` field for your own application data, which tldraw stores but doesn't use itself. See [Meta](https://tldraw.dev/docs/shapes#Meta) for how to set, type, and validate it.
|
|
17083
|
+
|
|
16082
17084
|
##### Shape types in tldraw
|
|
16083
17085
|
|
|
16084
17086
|
The default tldraw installation includes these shape types:
|