@tldraw/tlschema 4.1.0-next.1b89b40eff1c → 4.1.0-next.9f145d10c7d0
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/dist-cjs/TLStore.js +3 -10
- package/dist-cjs/TLStore.js.map +2 -2
- package/dist-cjs/assets/TLBaseAsset.js.map +2 -2
- package/dist-cjs/assets/TLBookmarkAsset.js.map +2 -2
- package/dist-cjs/assets/TLImageAsset.js.map +2 -2
- package/dist-cjs/assets/TLVideoAsset.js.map +2 -2
- package/dist-cjs/bindings/TLArrowBinding.js.map +2 -2
- package/dist-cjs/bindings/TLBaseBinding.js.map +2 -2
- package/dist-cjs/createPresenceStateDerivation.js.map +2 -2
- package/dist-cjs/createTLSchema.js.map +2 -2
- package/dist-cjs/index.d.ts +4416 -223
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/misc/TLColor.js.map +2 -2
- package/dist-cjs/misc/TLCursor.js.map +2 -2
- package/dist-cjs/misc/TLHandle.js.map +2 -2
- package/dist-cjs/misc/TLOpacity.js.map +2 -2
- package/dist-cjs/misc/TLRichText.js.map +2 -2
- package/dist-cjs/misc/TLScribble.js.map +2 -2
- package/dist-cjs/misc/geometry-types.js.map +2 -2
- package/dist-cjs/misc/id-validator.js.map +2 -2
- package/dist-cjs/records/TLAsset.js.map +2 -2
- package/dist-cjs/records/TLBinding.js.map +2 -2
- package/dist-cjs/records/TLCamera.js.map +2 -2
- package/dist-cjs/records/TLDocument.js.map +2 -2
- package/dist-cjs/records/TLInstance.js.map +2 -2
- package/dist-cjs/records/TLPage.js.map +2 -2
- package/dist-cjs/records/TLPageState.js.map +2 -2
- package/dist-cjs/records/TLPointer.js.map +2 -2
- package/dist-cjs/records/TLPresence.js.map +2 -2
- package/dist-cjs/records/TLRecord.js.map +1 -1
- package/dist-cjs/records/TLShape.js.map +2 -2
- package/dist-cjs/recordsWithProps.js.map +2 -2
- package/dist-cjs/shapes/ShapeWithCrop.js.map +1 -1
- package/dist-cjs/shapes/TLArrowShape.js.map +2 -2
- package/dist-cjs/shapes/TLBaseShape.js.map +2 -2
- package/dist-cjs/shapes/TLBookmarkShape.js.map +2 -2
- package/dist-cjs/shapes/TLDrawShape.js.map +2 -2
- package/dist-cjs/shapes/TLEmbedShape.js.map +2 -2
- package/dist-cjs/shapes/TLFrameShape.js.map +2 -2
- package/dist-cjs/shapes/TLGeoShape.js.map +2 -2
- package/dist-cjs/shapes/TLGroupShape.js.map +2 -2
- package/dist-cjs/shapes/TLHighlightShape.js.map +2 -2
- package/dist-cjs/shapes/TLImageShape.js.map +2 -2
- package/dist-cjs/shapes/TLLineShape.js.map +2 -2
- package/dist-cjs/shapes/TLNoteShape.js.map +2 -2
- package/dist-cjs/shapes/TLTextShape.js.map +2 -2
- package/dist-cjs/shapes/TLVideoShape.js.map +2 -2
- package/dist-cjs/store-migrations.js.map +2 -2
- package/dist-cjs/styles/TLColorStyle.js.map +2 -2
- package/dist-cjs/styles/TLDashStyle.js.map +2 -2
- package/dist-cjs/styles/TLFillStyle.js.map +2 -2
- package/dist-cjs/styles/TLFontStyle.js.map +2 -2
- package/dist-cjs/styles/TLHorizontalAlignStyle.js.map +2 -2
- package/dist-cjs/styles/TLSizeStyle.js.map +2 -2
- package/dist-cjs/styles/TLTextAlignStyle.js.map +2 -2
- package/dist-cjs/styles/TLVerticalAlignStyle.js.map +2 -2
- package/dist-cjs/translations/translations.js +1 -1
- package/dist-cjs/translations/translations.js.map +2 -2
- package/dist-cjs/util-types.js.map +1 -1
- package/dist-esm/TLStore.mjs +3 -10
- package/dist-esm/TLStore.mjs.map +2 -2
- package/dist-esm/assets/TLBaseAsset.mjs.map +2 -2
- package/dist-esm/assets/TLBookmarkAsset.mjs.map +2 -2
- package/dist-esm/assets/TLImageAsset.mjs.map +2 -2
- package/dist-esm/assets/TLVideoAsset.mjs.map +2 -2
- package/dist-esm/bindings/TLArrowBinding.mjs.map +2 -2
- package/dist-esm/bindings/TLBaseBinding.mjs.map +2 -2
- package/dist-esm/createPresenceStateDerivation.mjs.map +2 -2
- package/dist-esm/createTLSchema.mjs.map +2 -2
- package/dist-esm/index.d.mts +4416 -223
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/misc/TLColor.mjs.map +2 -2
- package/dist-esm/misc/TLCursor.mjs.map +2 -2
- package/dist-esm/misc/TLHandle.mjs.map +2 -2
- package/dist-esm/misc/TLOpacity.mjs.map +2 -2
- package/dist-esm/misc/TLRichText.mjs.map +2 -2
- package/dist-esm/misc/TLScribble.mjs.map +2 -2
- package/dist-esm/misc/geometry-types.mjs.map +2 -2
- package/dist-esm/misc/id-validator.mjs.map +2 -2
- package/dist-esm/records/TLAsset.mjs.map +2 -2
- package/dist-esm/records/TLBinding.mjs.map +2 -2
- package/dist-esm/records/TLCamera.mjs.map +2 -2
- package/dist-esm/records/TLDocument.mjs.map +2 -2
- package/dist-esm/records/TLInstance.mjs.map +2 -2
- package/dist-esm/records/TLPage.mjs.map +2 -2
- package/dist-esm/records/TLPageState.mjs.map +2 -2
- package/dist-esm/records/TLPointer.mjs.map +2 -2
- package/dist-esm/records/TLPresence.mjs.map +2 -2
- package/dist-esm/records/TLShape.mjs.map +2 -2
- package/dist-esm/recordsWithProps.mjs.map +2 -2
- package/dist-esm/shapes/TLArrowShape.mjs.map +2 -2
- package/dist-esm/shapes/TLBaseShape.mjs.map +2 -2
- package/dist-esm/shapes/TLBookmarkShape.mjs.map +2 -2
- package/dist-esm/shapes/TLDrawShape.mjs.map +2 -2
- package/dist-esm/shapes/TLEmbedShape.mjs.map +2 -2
- package/dist-esm/shapes/TLFrameShape.mjs.map +2 -2
- package/dist-esm/shapes/TLGeoShape.mjs.map +2 -2
- package/dist-esm/shapes/TLGroupShape.mjs.map +2 -2
- package/dist-esm/shapes/TLHighlightShape.mjs.map +2 -2
- package/dist-esm/shapes/TLImageShape.mjs.map +2 -2
- package/dist-esm/shapes/TLLineShape.mjs.map +2 -2
- package/dist-esm/shapes/TLNoteShape.mjs.map +2 -2
- package/dist-esm/shapes/TLTextShape.mjs.map +2 -2
- package/dist-esm/shapes/TLVideoShape.mjs.map +2 -2
- package/dist-esm/store-migrations.mjs.map +2 -2
- package/dist-esm/styles/TLColorStyle.mjs.map +2 -2
- package/dist-esm/styles/TLDashStyle.mjs.map +2 -2
- package/dist-esm/styles/TLFillStyle.mjs.map +2 -2
- package/dist-esm/styles/TLFontStyle.mjs.map +2 -2
- package/dist-esm/styles/TLHorizontalAlignStyle.mjs.map +2 -2
- package/dist-esm/styles/TLSizeStyle.mjs.map +2 -2
- package/dist-esm/styles/TLTextAlignStyle.mjs.map +2 -2
- package/dist-esm/styles/TLVerticalAlignStyle.mjs.map +2 -2
- package/dist-esm/translations/translations.mjs +1 -1
- package/dist-esm/translations/translations.mjs.map +2 -2
- package/package.json +5 -5
- package/src/TLStore.test.ts +644 -0
- package/src/TLStore.ts +205 -20
- package/src/assets/TLBaseAsset.ts +90 -7
- package/src/assets/TLBookmarkAsset.test.ts +96 -0
- package/src/assets/TLBookmarkAsset.ts +52 -2
- package/src/assets/TLImageAsset.test.ts +213 -0
- package/src/assets/TLImageAsset.ts +60 -2
- package/src/assets/TLVideoAsset.test.ts +105 -0
- package/src/assets/TLVideoAsset.ts +93 -4
- package/src/bindings/TLArrowBinding.test.ts +55 -0
- package/src/bindings/TLArrowBinding.ts +132 -10
- package/src/bindings/TLBaseBinding.ts +140 -3
- package/src/createPresenceStateDerivation.test.ts +158 -0
- package/src/createPresenceStateDerivation.ts +71 -2
- package/src/createTLSchema.test.ts +181 -0
- package/src/createTLSchema.ts +164 -7
- package/src/index.ts +32 -0
- package/src/misc/TLColor.ts +50 -6
- package/src/misc/TLCursor.ts +110 -8
- package/src/misc/TLHandle.ts +86 -6
- package/src/misc/TLOpacity.ts +51 -2
- package/src/misc/TLRichText.ts +56 -3
- package/src/misc/TLScribble.ts +105 -5
- package/src/misc/geometry-types.ts +30 -2
- package/src/misc/id-validator.test.ts +50 -0
- package/src/misc/id-validator.ts +20 -1
- package/src/records/TLAsset.test.ts +234 -0
- package/src/records/TLAsset.ts +165 -8
- package/src/records/TLBinding.test.ts +22 -0
- package/src/records/TLBinding.ts +277 -11
- package/src/records/TLCamera.test.ts +19 -0
- package/src/records/TLCamera.ts +118 -7
- package/src/records/TLDocument.test.ts +35 -0
- package/src/records/TLDocument.ts +148 -8
- package/src/records/TLInstance.test.ts +201 -0
- package/src/records/TLInstance.ts +117 -9
- package/src/records/TLPage.test.ts +110 -0
- package/src/records/TLPage.ts +106 -8
- package/src/records/TLPageState.test.ts +228 -0
- package/src/records/TLPageState.ts +88 -7
- package/src/records/TLPointer.test.ts +63 -0
- package/src/records/TLPointer.ts +105 -7
- package/src/records/TLPresence.test.ts +190 -0
- package/src/records/TLPresence.ts +99 -5
- package/src/records/TLRecord.test.ts +70 -0
- package/src/records/TLRecord.ts +43 -1
- package/src/records/TLShape.test.ts +232 -0
- package/src/records/TLShape.ts +289 -12
- package/src/recordsWithProps.test.ts +188 -0
- package/src/recordsWithProps.ts +131 -2
- package/src/shapes/ShapeWithCrop.test.ts +18 -0
- package/src/shapes/ShapeWithCrop.ts +64 -2
- package/src/shapes/TLArrowShape.test.ts +505 -0
- package/src/shapes/TLArrowShape.ts +188 -10
- package/src/shapes/TLBaseShape.test.ts +142 -0
- package/src/shapes/TLBaseShape.ts +103 -4
- package/src/shapes/TLBookmarkShape.test.ts +122 -0
- package/src/shapes/TLBookmarkShape.ts +58 -4
- package/src/shapes/TLDrawShape.test.ts +177 -0
- package/src/shapes/TLDrawShape.ts +97 -6
- package/src/shapes/TLEmbedShape.test.ts +286 -0
- package/src/shapes/TLEmbedShape.ts +57 -4
- package/src/shapes/TLFrameShape.test.ts +71 -0
- package/src/shapes/TLFrameShape.ts +59 -4
- package/src/shapes/TLGeoShape.test.ts +247 -0
- package/src/shapes/TLGeoShape.ts +103 -7
- package/src/shapes/TLGroupShape.test.ts +59 -0
- package/src/shapes/TLGroupShape.ts +52 -4
- package/src/shapes/TLHighlightShape.test.ts +325 -0
- package/src/shapes/TLHighlightShape.ts +79 -4
- package/src/shapes/TLImageShape.test.ts +534 -0
- package/src/shapes/TLImageShape.ts +105 -5
- package/src/shapes/TLLineShape.test.ts +269 -0
- package/src/shapes/TLLineShape.ts +128 -8
- package/src/shapes/TLNoteShape.test.ts +1568 -0
- package/src/shapes/TLNoteShape.ts +97 -4
- package/src/shapes/TLTextShape.test.ts +407 -0
- package/src/shapes/TLTextShape.ts +94 -4
- package/src/shapes/TLVideoShape.test.ts +112 -0
- package/src/shapes/TLVideoShape.ts +99 -4
- package/src/store-migrations.test.ts +88 -0
- package/src/store-migrations.ts +47 -1
- package/src/styles/TLColorStyle.test.ts +439 -0
- package/src/styles/TLColorStyle.ts +228 -10
- package/src/styles/TLDashStyle.ts +54 -2
- package/src/styles/TLFillStyle.ts +54 -2
- package/src/styles/TLFontStyle.ts +72 -3
- package/src/styles/TLHorizontalAlignStyle.ts +55 -2
- package/src/styles/TLSizeStyle.ts +54 -2
- package/src/styles/TLTextAlignStyle.ts +52 -2
- package/src/styles/TLVerticalAlignStyle.ts +52 -2
- package/src/translations/translations.test.ts +378 -35
- package/src/translations/translations.ts +157 -10
- package/src/util-types.ts +51 -1
package/src/records/TLPointer.ts
CHANGED
|
@@ -10,7 +10,25 @@ import { T } from '@tldraw/validate'
|
|
|
10
10
|
import { idValidator } from '../misc/id-validator'
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
13
|
+
* Represents the current pointer/cursor position and activity state.
|
|
14
|
+
* This record tracks the mouse or touch pointer coordinates and when
|
|
15
|
+
* the pointer was last active, useful for cursor synchronization in
|
|
16
|
+
* collaborative environments.
|
|
17
|
+
*
|
|
18
|
+
* There is typically one pointer record per browser tab that gets updated
|
|
19
|
+
* as the user moves their mouse or touches the screen.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* const pointer: TLPointer = {
|
|
24
|
+
* id: 'pointer:pointer',
|
|
25
|
+
* typeName: 'pointer',
|
|
26
|
+
* x: 150,
|
|
27
|
+
* y: 200,
|
|
28
|
+
* lastActivityTimestamp: Date.now(),
|
|
29
|
+
* meta: {}
|
|
30
|
+
* }
|
|
31
|
+
* ```
|
|
14
32
|
*
|
|
15
33
|
* @public
|
|
16
34
|
*/
|
|
@@ -21,10 +39,40 @@ export interface TLPointer extends BaseRecord<'pointer', TLPointerId> {
|
|
|
21
39
|
meta: JsonObject
|
|
22
40
|
}
|
|
23
41
|
|
|
24
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* A unique identifier for TLPointer records.
|
|
44
|
+
*
|
|
45
|
+
* Pointer IDs follow the format 'pointer:' followed by a unique identifier.
|
|
46
|
+
* Typically there is one pointer record with a constant ID per session.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```ts
|
|
50
|
+
* const pointerId: TLPointerId = 'pointer:pointer'
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* @public
|
|
54
|
+
*/
|
|
25
55
|
export type TLPointerId = RecordId<TLPointer>
|
|
26
56
|
|
|
27
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* Runtime validator for TLPointer records. Validates the structure
|
|
59
|
+
* and types of all pointer properties to ensure data integrity.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```ts
|
|
63
|
+
* const pointer = {
|
|
64
|
+
* id: 'pointer:pointer',
|
|
65
|
+
* typeName: 'pointer',
|
|
66
|
+
* x: 100,
|
|
67
|
+
* y: 200,
|
|
68
|
+
* lastActivityTimestamp: Date.now(),
|
|
69
|
+
* meta: {}
|
|
70
|
+
* }
|
|
71
|
+
* const isValid = pointerValidator.isValid(pointer) // true
|
|
72
|
+
* ```
|
|
73
|
+
*
|
|
74
|
+
* @public
|
|
75
|
+
*/
|
|
28
76
|
export const pointerValidator: T.Validator<TLPointer> = T.model(
|
|
29
77
|
'pointer',
|
|
30
78
|
T.object({
|
|
@@ -37,12 +85,30 @@ export const pointerValidator: T.Validator<TLPointer> = T.model(
|
|
|
37
85
|
})
|
|
38
86
|
)
|
|
39
87
|
|
|
40
|
-
/**
|
|
88
|
+
/**
|
|
89
|
+
* Migration version identifiers for TLPointer records. Each version
|
|
90
|
+
* represents a schema change that requires data transformation when
|
|
91
|
+
* loading older documents.
|
|
92
|
+
*
|
|
93
|
+
* @public
|
|
94
|
+
*/
|
|
41
95
|
export const pointerVersions = createMigrationIds('com.tldraw.pointer', {
|
|
42
96
|
AddMeta: 1,
|
|
43
97
|
})
|
|
44
98
|
|
|
45
|
-
/**
|
|
99
|
+
/**
|
|
100
|
+
* Migration sequence for TLPointer records. Defines how to transform
|
|
101
|
+
* pointer records between different schema versions, ensuring data
|
|
102
|
+
* compatibility when loading documents created with different versions.
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```ts
|
|
106
|
+
* // Migrations are applied automatically when loading documents
|
|
107
|
+
* const migratedPointer = pointerMigrations.migrate(oldPointer, targetVersion)
|
|
108
|
+
* ```
|
|
109
|
+
*
|
|
110
|
+
* @public
|
|
111
|
+
*/
|
|
46
112
|
export const pointerMigrations = createRecordMigrationSequence({
|
|
47
113
|
sequenceId: 'com.tldraw.pointer',
|
|
48
114
|
recordType: 'pointer',
|
|
@@ -56,7 +122,24 @@ export const pointerMigrations = createRecordMigrationSequence({
|
|
|
56
122
|
],
|
|
57
123
|
})
|
|
58
124
|
|
|
59
|
-
/**
|
|
125
|
+
/**
|
|
126
|
+
* The RecordType definition for TLPointer records. Defines validation,
|
|
127
|
+
* scope, and default properties for pointer records in the tldraw store.
|
|
128
|
+
*
|
|
129
|
+
* Pointer records are scoped to the session level, meaning they are
|
|
130
|
+
* specific to a browser tab and don't persist across sessions.
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```ts
|
|
134
|
+
* const pointer = PointerRecordType.create({
|
|
135
|
+
* id: 'pointer:pointer',
|
|
136
|
+
* x: 0,
|
|
137
|
+
* y: 0
|
|
138
|
+
* })
|
|
139
|
+
* ```
|
|
140
|
+
*
|
|
141
|
+
* @public
|
|
142
|
+
*/
|
|
60
143
|
export const PointerRecordType = createRecordType<TLPointer>('pointer', {
|
|
61
144
|
validator: pointerValidator,
|
|
62
145
|
scope: 'session',
|
|
@@ -69,5 +152,20 @@ export const PointerRecordType = createRecordType<TLPointer>('pointer', {
|
|
|
69
152
|
})
|
|
70
153
|
)
|
|
71
154
|
|
|
72
|
-
/**
|
|
155
|
+
/**
|
|
156
|
+
* The constant ID used for the singleton TLPointer record.
|
|
157
|
+
*
|
|
158
|
+
* Since each browser tab typically has one pointer, this constant ID
|
|
159
|
+
* is used universally across the application.
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```ts
|
|
163
|
+
* const pointer = store.get(TLPOINTER_ID)
|
|
164
|
+
* if (pointer) {
|
|
165
|
+
* console.log('Pointer at:', pointer.x, pointer.y)
|
|
166
|
+
* }
|
|
167
|
+
* ```
|
|
168
|
+
*
|
|
169
|
+
* @public
|
|
170
|
+
*/
|
|
73
171
|
export const TLPOINTER_ID = PointerRecordType.createId('pointer')
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
instancePresenceMigrations,
|
|
4
|
+
instancePresenceValidator,
|
|
5
|
+
instancePresenceVersions,
|
|
6
|
+
TLInstancePresenceID,
|
|
7
|
+
} from './TLPresence'
|
|
8
|
+
|
|
9
|
+
describe('instancePresenceValidator', () => {
|
|
10
|
+
it('should validate valid instance presence records', () => {
|
|
11
|
+
const validPresence = {
|
|
12
|
+
typeName: 'instance_presence',
|
|
13
|
+
id: 'instance_presence:test' as TLInstancePresenceID,
|
|
14
|
+
userId: 'user123',
|
|
15
|
+
userName: 'Test User',
|
|
16
|
+
lastActivityTimestamp: null,
|
|
17
|
+
color: '#007AFF',
|
|
18
|
+
camera: null,
|
|
19
|
+
selectedShapeIds: [],
|
|
20
|
+
currentPageId: 'page:main' as any,
|
|
21
|
+
brush: null,
|
|
22
|
+
scribbles: [],
|
|
23
|
+
screenBounds: null,
|
|
24
|
+
followingUserId: null,
|
|
25
|
+
cursor: null,
|
|
26
|
+
chatMessage: '',
|
|
27
|
+
meta: {},
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
expect(() => instancePresenceValidator.validate(validPresence)).not.toThrow()
|
|
31
|
+
const validated = instancePresenceValidator.validate(validPresence)
|
|
32
|
+
expect(validated).toEqual(validPresence)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('should validate presence with complete data', () => {
|
|
36
|
+
const complexPresence = {
|
|
37
|
+
typeName: 'instance_presence',
|
|
38
|
+
id: 'instance_presence:complex' as TLInstancePresenceID,
|
|
39
|
+
userId: 'user456',
|
|
40
|
+
userName: 'Complex User',
|
|
41
|
+
lastActivityTimestamp: Date.now(),
|
|
42
|
+
color: '#FF3B30',
|
|
43
|
+
camera: { x: -100, y: 200, z: 0.75 },
|
|
44
|
+
selectedShapeIds: ['shape:1' as any, 'shape:2' as any],
|
|
45
|
+
currentPageId: 'page:design' as any,
|
|
46
|
+
brush: { x: 50, y: 75, w: 150, h: 100 },
|
|
47
|
+
scribbles: [
|
|
48
|
+
{
|
|
49
|
+
id: 'scribble:1',
|
|
50
|
+
points: [{ x: 0, y: 0, z: 0.5 }],
|
|
51
|
+
size: 4,
|
|
52
|
+
color: 'black',
|
|
53
|
+
opacity: 1,
|
|
54
|
+
state: 'starting',
|
|
55
|
+
delay: 0,
|
|
56
|
+
shrink: 0,
|
|
57
|
+
taper: false,
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
screenBounds: { x: 0, y: 0, w: 2560, h: 1440 },
|
|
61
|
+
followingUserId: 'leader123',
|
|
62
|
+
cursor: { x: 300, y: 400, type: 'pointer', rotation: 45 },
|
|
63
|
+
chatMessage: 'Working on design!',
|
|
64
|
+
meta: { team: 'design', role: 'designer' },
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
expect(() => instancePresenceValidator.validate(complexPresence)).not.toThrow()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('should reject invalid typeName', () => {
|
|
71
|
+
const invalidPresence = {
|
|
72
|
+
typeName: 'not-instance-presence',
|
|
73
|
+
id: 'instance_presence:test' as TLInstancePresenceID,
|
|
74
|
+
userId: 'user123',
|
|
75
|
+
userName: 'Test',
|
|
76
|
+
lastActivityTimestamp: null,
|
|
77
|
+
color: '#000000',
|
|
78
|
+
camera: null,
|
|
79
|
+
selectedShapeIds: [],
|
|
80
|
+
currentPageId: 'page:main' as any,
|
|
81
|
+
brush: null,
|
|
82
|
+
scribbles: [],
|
|
83
|
+
screenBounds: null,
|
|
84
|
+
followingUserId: null,
|
|
85
|
+
cursor: null,
|
|
86
|
+
chatMessage: '',
|
|
87
|
+
meta: {},
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
expect(() => instancePresenceValidator.validate(invalidPresence)).toThrow()
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
describe('instancePresenceMigrations', () => {
|
|
95
|
+
it('should migrate AddScribbleDelay correctly', () => {
|
|
96
|
+
const migration = instancePresenceMigrations.sequence.find(
|
|
97
|
+
(m) => m.id === instancePresenceVersions.AddScribbleDelay
|
|
98
|
+
)!
|
|
99
|
+
|
|
100
|
+
const oldRecordWithScribble: any = {
|
|
101
|
+
scribble: { points: [], size: 4, color: 'black' },
|
|
102
|
+
}
|
|
103
|
+
migration.up(oldRecordWithScribble)
|
|
104
|
+
expect(oldRecordWithScribble.scribble.delay).toBe(0)
|
|
105
|
+
|
|
106
|
+
const oldRecordWithoutScribble: any = {
|
|
107
|
+
scribble: null,
|
|
108
|
+
}
|
|
109
|
+
migration.up(oldRecordWithoutScribble)
|
|
110
|
+
expect(oldRecordWithoutScribble.scribble).toBe(null)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('should migrate RemoveInstanceId correctly', () => {
|
|
114
|
+
const migration = instancePresenceMigrations.sequence.find(
|
|
115
|
+
(m) => m.id === instancePresenceVersions.RemoveInstanceId
|
|
116
|
+
)!
|
|
117
|
+
const oldRecord: any = {
|
|
118
|
+
instanceId: 'instance:removed',
|
|
119
|
+
otherProp: 'keep-me',
|
|
120
|
+
}
|
|
121
|
+
migration.up(oldRecord)
|
|
122
|
+
|
|
123
|
+
expect(oldRecord.instanceId).toBeUndefined()
|
|
124
|
+
expect(oldRecord.otherProp).toBe('keep-me')
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('should migrate AddChatMessage correctly', () => {
|
|
128
|
+
const migration = instancePresenceMigrations.sequence.find(
|
|
129
|
+
(m) => m.id === instancePresenceVersions.AddChatMessage
|
|
130
|
+
)!
|
|
131
|
+
const oldRecord: any = { id: 'instance_presence:test' }
|
|
132
|
+
migration.up(oldRecord)
|
|
133
|
+
|
|
134
|
+
expect(oldRecord.chatMessage).toBe('')
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('should migrate AddMeta correctly', () => {
|
|
138
|
+
const migration = instancePresenceMigrations.sequence.find(
|
|
139
|
+
(m) => m.id === instancePresenceVersions.AddMeta
|
|
140
|
+
)!
|
|
141
|
+
const oldRecord: any = { id: 'instance_presence:test' }
|
|
142
|
+
migration.up(oldRecord)
|
|
143
|
+
|
|
144
|
+
expect(oldRecord.meta).toEqual({})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('should handle RenameSelectedShapeIds migration (noop)', () => {
|
|
148
|
+
const migration = instancePresenceMigrations.sequence.find(
|
|
149
|
+
(m) => m.id === instancePresenceVersions.RenameSelectedShapeIds
|
|
150
|
+
)!
|
|
151
|
+
const oldRecord: any = { selectedShapeIds: ['shape:1'] }
|
|
152
|
+
const originalRecord = { ...oldRecord }
|
|
153
|
+
migration.up(oldRecord)
|
|
154
|
+
|
|
155
|
+
// Should be a noop
|
|
156
|
+
expect(oldRecord).toEqual(originalRecord)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('should handle NullableCameraCursor migration up (noop)', () => {
|
|
160
|
+
const migration = instancePresenceMigrations.sequence.find(
|
|
161
|
+
(m) => m.id === instancePresenceVersions.NullableCameraCursor
|
|
162
|
+
)!
|
|
163
|
+
const record: any = { camera: null, cursor: null }
|
|
164
|
+
const originalRecord = { ...record }
|
|
165
|
+
migration.up(record)
|
|
166
|
+
|
|
167
|
+
// Should be a noop
|
|
168
|
+
expect(record).toEqual(originalRecord)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('should handle NullableCameraCursor migration down', () => {
|
|
172
|
+
const migration = instancePresenceMigrations.sequence.find(
|
|
173
|
+
(m) => m.id === instancePresenceVersions.NullableCameraCursor
|
|
174
|
+
)!
|
|
175
|
+
expect(migration.down).toBeDefined()
|
|
176
|
+
|
|
177
|
+
const record: any = {
|
|
178
|
+
camera: null,
|
|
179
|
+
lastActivityTimestamp: null,
|
|
180
|
+
cursor: null,
|
|
181
|
+
screenBounds: null,
|
|
182
|
+
}
|
|
183
|
+
migration.down!(record)
|
|
184
|
+
|
|
185
|
+
expect(record.camera).toEqual({ x: 0, y: 0, z: 1 })
|
|
186
|
+
expect(record.lastActivityTimestamp).toBe(0)
|
|
187
|
+
expect(record.cursor).toEqual({ type: 'default', x: 0, y: 0, rotation: 0 })
|
|
188
|
+
expect(record.screenBounds).toEqual({ x: 0, y: 0, w: 1, h: 1 })
|
|
189
|
+
})
|
|
190
|
+
})
|
|
@@ -14,7 +14,30 @@ import { scribbleValidator, TLScribble } from '../misc/TLScribble'
|
|
|
14
14
|
import { TLPageId } from './TLPage'
|
|
15
15
|
import { TLShapeId } from './TLShape'
|
|
16
16
|
|
|
17
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* Represents the presence state of a user in a collaborative tldraw session.
|
|
19
|
+
* This record tracks what another user is doing: their cursor position, selected
|
|
20
|
+
* shapes, current page, and other real-time activity indicators.
|
|
21
|
+
*
|
|
22
|
+
* Instance presence records are used in multiplayer environments to show
|
|
23
|
+
* where other collaborators are working and what they're doing.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* const presence: TLInstancePresence = {
|
|
28
|
+
* id: 'instance_presence:user123',
|
|
29
|
+
* typeName: 'instance_presence',
|
|
30
|
+
* userId: 'user123',
|
|
31
|
+
* userName: 'Alice',
|
|
32
|
+
* color: '#FF6B6B',
|
|
33
|
+
* cursor: { x: 100, y: 150, type: 'default', rotation: 0 },
|
|
34
|
+
* currentPageId: 'page:main',
|
|
35
|
+
* selectedShapeIds: ['shape:rect1']
|
|
36
|
+
* }
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* @public
|
|
40
|
+
*/
|
|
18
41
|
export interface TLInstancePresence extends BaseRecord<'instance_presence', TLInstancePresenceID> {
|
|
19
42
|
userId: string
|
|
20
43
|
userName: string
|
|
@@ -37,10 +60,42 @@ export interface TLInstancePresence extends BaseRecord<'instance_presence', TLIn
|
|
|
37
60
|
meta: JsonObject
|
|
38
61
|
}
|
|
39
62
|
|
|
40
|
-
/**
|
|
63
|
+
/**
|
|
64
|
+
* A unique identifier for TLInstancePresence records.
|
|
65
|
+
*
|
|
66
|
+
* Instance presence IDs follow the format 'instance_presence:' followed
|
|
67
|
+
* by a unique identifier, typically the user ID.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```ts
|
|
71
|
+
* const presenceId: TLInstancePresenceID = 'instance_presence:user123'
|
|
72
|
+
* ```
|
|
73
|
+
*
|
|
74
|
+
* @public
|
|
75
|
+
*/
|
|
41
76
|
export type TLInstancePresenceID = RecordId<TLInstancePresence>
|
|
42
77
|
|
|
43
|
-
/**
|
|
78
|
+
/**
|
|
79
|
+
* Runtime validator for TLInstancePresence records. Validates the structure
|
|
80
|
+
* and types of all instance presence properties to ensure data integrity.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```ts
|
|
84
|
+
* const presence = {
|
|
85
|
+
* id: 'instance_presence:user1',
|
|
86
|
+
* typeName: 'instance_presence',
|
|
87
|
+
* userId: 'user1',
|
|
88
|
+
* userName: 'John',
|
|
89
|
+
* color: '#007AFF',
|
|
90
|
+
* cursor: { x: 0, y: 0, type: 'default', rotation: 0 },
|
|
91
|
+
* currentPageId: 'page:main',
|
|
92
|
+
* selectedShapeIds: []
|
|
93
|
+
* }
|
|
94
|
+
* const isValid = instancePresenceValidator.isValid(presence) // true
|
|
95
|
+
* ```
|
|
96
|
+
*
|
|
97
|
+
* @public
|
|
98
|
+
*/
|
|
44
99
|
export const instancePresenceValidator: T.Validator<TLInstancePresence> = T.model(
|
|
45
100
|
'instance_presence',
|
|
46
101
|
T.object({
|
|
@@ -72,7 +127,13 @@ export const instancePresenceValidator: T.Validator<TLInstancePresence> = T.mode
|
|
|
72
127
|
})
|
|
73
128
|
)
|
|
74
129
|
|
|
75
|
-
/**
|
|
130
|
+
/**
|
|
131
|
+
* Migration version identifiers for TLInstancePresence records. Each version
|
|
132
|
+
* represents a schema change that requires data transformation when loading
|
|
133
|
+
* older documents.
|
|
134
|
+
*
|
|
135
|
+
* @public
|
|
136
|
+
*/
|
|
76
137
|
export const instancePresenceVersions = createMigrationIds('com.tldraw.instance_presence', {
|
|
77
138
|
AddScribbleDelay: 1,
|
|
78
139
|
RemoveInstanceId: 2,
|
|
@@ -82,6 +143,19 @@ export const instancePresenceVersions = createMigrationIds('com.tldraw.instance_
|
|
|
82
143
|
NullableCameraCursor: 6,
|
|
83
144
|
} as const)
|
|
84
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Migration sequence for TLInstancePresence records. Defines how to transform
|
|
148
|
+
* instance presence records between different schema versions, ensuring data
|
|
149
|
+
* compatibility when loading documents created with different versions.
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```ts
|
|
153
|
+
* // Migrations are applied automatically when loading documents
|
|
154
|
+
* const migrated = instancePresenceMigrations.migrate(oldPresence, targetVersion)
|
|
155
|
+
* ```
|
|
156
|
+
*
|
|
157
|
+
* @public
|
|
158
|
+
*/
|
|
85
159
|
export const instancePresenceMigrations = createRecordMigrationSequence({
|
|
86
160
|
sequenceId: 'com.tldraw.instance_presence',
|
|
87
161
|
recordType: 'instance_presence',
|
|
@@ -141,7 +215,27 @@ export const instancePresenceMigrations = createRecordMigrationSequence({
|
|
|
141
215
|
],
|
|
142
216
|
})
|
|
143
217
|
|
|
144
|
-
/**
|
|
218
|
+
/**
|
|
219
|
+
* The RecordType definition for TLInstancePresence records. Defines validation,
|
|
220
|
+
* scope, and default properties for instance presence records.
|
|
221
|
+
*
|
|
222
|
+
* Instance presence records are scoped to the presence level, meaning they
|
|
223
|
+
* represent real-time collaborative state that is ephemeral and tied to
|
|
224
|
+
* active user sessions.
|
|
225
|
+
*
|
|
226
|
+
* @example
|
|
227
|
+
* ```ts
|
|
228
|
+
* const presence = InstancePresenceRecordType.create({
|
|
229
|
+
* id: 'instance_presence:user1',
|
|
230
|
+
* userId: 'user1',
|
|
231
|
+
* userName: 'Alice',
|
|
232
|
+
* color: '#FF6B6B',
|
|
233
|
+
* currentPageId: 'page:main'
|
|
234
|
+
* })
|
|
235
|
+
* ```
|
|
236
|
+
*
|
|
237
|
+
* @public
|
|
238
|
+
*/
|
|
145
239
|
export const InstancePresenceRecordType = createRecordType<TLInstancePresence>(
|
|
146
240
|
'instance_presence',
|
|
147
241
|
{
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { TLRecord } from './TLRecord'
|
|
3
|
+
|
|
4
|
+
describe('TLRecord', () => {
|
|
5
|
+
it('should support type discrimination by typeName', () => {
|
|
6
|
+
function processRecord(record: TLRecord): string {
|
|
7
|
+
// TypeScript should be able to narrow types based on typeName
|
|
8
|
+
switch (record.typeName) {
|
|
9
|
+
case 'shape':
|
|
10
|
+
return `Shape at (${record.x}, ${record.y})`
|
|
11
|
+
case 'page':
|
|
12
|
+
return `Page: ${record.name}`
|
|
13
|
+
case 'asset':
|
|
14
|
+
return `Asset: ${record.type}`
|
|
15
|
+
case 'binding':
|
|
16
|
+
return `Binding from ${record.fromId} to ${record.toId}`
|
|
17
|
+
case 'camera':
|
|
18
|
+
return `Camera at (${record.x}, ${record.y}) zoom: ${record.z}`
|
|
19
|
+
case 'document':
|
|
20
|
+
return `Document: ${record.name}, grid: ${record.gridSize}`
|
|
21
|
+
case 'instance':
|
|
22
|
+
return `Instance on page ${record.currentPageId}`
|
|
23
|
+
case 'instance_page_state':
|
|
24
|
+
return `Page state for ${record.pageId}`
|
|
25
|
+
case 'instance_presence':
|
|
26
|
+
return `Presence of ${record.userName}`
|
|
27
|
+
case 'pointer':
|
|
28
|
+
return `Pointer at (${record.x}, ${record.y})`
|
|
29
|
+
default:
|
|
30
|
+
// This should never be reached if all cases are handled
|
|
31
|
+
return 'Unknown record type'
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Test that the discriminated union works correctly
|
|
36
|
+
const shapeRecord: TLRecord = {
|
|
37
|
+
id: 'shape:test' as any,
|
|
38
|
+
typeName: 'shape',
|
|
39
|
+
type: 'geo',
|
|
40
|
+
x: 50,
|
|
41
|
+
y: 100,
|
|
42
|
+
rotation: 0,
|
|
43
|
+
index: 'a1' as any,
|
|
44
|
+
parentId: 'page:main' as any,
|
|
45
|
+
isLocked: false,
|
|
46
|
+
opacity: 1,
|
|
47
|
+
props: {
|
|
48
|
+
geo: 'rectangle',
|
|
49
|
+
w: 80,
|
|
50
|
+
h: 80,
|
|
51
|
+
color: 'black',
|
|
52
|
+
fill: 'none',
|
|
53
|
+
dash: 'draw',
|
|
54
|
+
size: 'm',
|
|
55
|
+
},
|
|
56
|
+
meta: {},
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const pageRecord: TLRecord = {
|
|
60
|
+
id: 'page:test' as any,
|
|
61
|
+
typeName: 'page',
|
|
62
|
+
name: 'Test Page',
|
|
63
|
+
index: 'a1' as any,
|
|
64
|
+
meta: {},
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
expect(processRecord(shapeRecord)).toBe('Shape at (50, 100)')
|
|
68
|
+
expect(processRecord(pageRecord)).toBe('Page: Test Page')
|
|
69
|
+
})
|
|
70
|
+
})
|
package/src/records/TLRecord.ts
CHANGED
|
@@ -9,7 +9,49 @@ import { TLPointer } from './TLPointer'
|
|
|
9
9
|
import { TLInstancePresence } from './TLPresence'
|
|
10
10
|
import { TLShape } from './TLShape'
|
|
11
11
|
|
|
12
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* Union type representing all possible record types in a tldraw store.
|
|
14
|
+
* This includes both persistent records (documents, pages, shapes, assets, bindings)
|
|
15
|
+
* and session/presence records (cameras, instances, pointers, page states).
|
|
16
|
+
*
|
|
17
|
+
* Records are organized by scope:
|
|
18
|
+
* - **document**: Persisted across sessions (shapes, pages, assets, bindings, documents)
|
|
19
|
+
* - **session**: Local to current session (cameras, instances, page states)
|
|
20
|
+
* - **presence**: Ephemeral user presence data (pointers, instance presence)
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* // Function that works with any record type
|
|
25
|
+
* function processRecord(record: TLRecord) {
|
|
26
|
+
* switch (record.typeName) {
|
|
27
|
+
* case 'shape':
|
|
28
|
+
* console.log(`Shape: ${record.type} at (${record.x}, ${record.y})`)
|
|
29
|
+
* break
|
|
30
|
+
* case 'page':
|
|
31
|
+
* console.log(`Page: ${record.name}`)
|
|
32
|
+
* break
|
|
33
|
+
* case 'asset':
|
|
34
|
+
* console.log(`Asset: ${record.type}`)
|
|
35
|
+
* break
|
|
36
|
+
* case 'camera':
|
|
37
|
+
* console.log(`Camera at (${record.x}, ${record.y}) zoom: ${record.z}`)
|
|
38
|
+
* break
|
|
39
|
+
* // ... handle other record types
|
|
40
|
+
* }
|
|
41
|
+
* }
|
|
42
|
+
*
|
|
43
|
+
* // Get all records from store
|
|
44
|
+
* const allRecords: TLRecord[] = store.allRecords()
|
|
45
|
+
*
|
|
46
|
+
* // Filter by record type using type guards
|
|
47
|
+
* import { isShape, isPage, isAsset } from '@tldraw/tlschema'
|
|
48
|
+
* const shapes = allRecords.filter(isShape)
|
|
49
|
+
* const pages = allRecords.filter(isPage)
|
|
50
|
+
* const assets = allRecords.filter(isAsset)
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* @public
|
|
54
|
+
*/
|
|
13
55
|
export type TLRecord =
|
|
14
56
|
| TLAsset
|
|
15
57
|
| TLBinding
|