@tldraw/tlschema 4.1.0-canary.ccd6179e1cb2 → 4.1.0-canary.e23ee15a46bc

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.
Files changed (212) hide show
  1. package/dist-cjs/TLStore.js +3 -10
  2. package/dist-cjs/TLStore.js.map +2 -2
  3. package/dist-cjs/assets/TLBaseAsset.js.map +2 -2
  4. package/dist-cjs/assets/TLBookmarkAsset.js.map +2 -2
  5. package/dist-cjs/assets/TLImageAsset.js.map +2 -2
  6. package/dist-cjs/assets/TLVideoAsset.js.map +2 -2
  7. package/dist-cjs/bindings/TLArrowBinding.js.map +2 -2
  8. package/dist-cjs/bindings/TLBaseBinding.js.map +2 -2
  9. package/dist-cjs/createPresenceStateDerivation.js.map +2 -2
  10. package/dist-cjs/createTLSchema.js.map +2 -2
  11. package/dist-cjs/index.d.ts +4412 -223
  12. package/dist-cjs/index.js +1 -1
  13. package/dist-cjs/index.js.map +2 -2
  14. package/dist-cjs/misc/TLColor.js.map +2 -2
  15. package/dist-cjs/misc/TLCursor.js.map +2 -2
  16. package/dist-cjs/misc/TLHandle.js.map +2 -2
  17. package/dist-cjs/misc/TLOpacity.js.map +2 -2
  18. package/dist-cjs/misc/TLRichText.js.map +2 -2
  19. package/dist-cjs/misc/TLScribble.js.map +2 -2
  20. package/dist-cjs/misc/geometry-types.js.map +2 -2
  21. package/dist-cjs/misc/id-validator.js.map +2 -2
  22. package/dist-cjs/records/TLAsset.js.map +2 -2
  23. package/dist-cjs/records/TLBinding.js.map +2 -2
  24. package/dist-cjs/records/TLCamera.js.map +2 -2
  25. package/dist-cjs/records/TLDocument.js.map +2 -2
  26. package/dist-cjs/records/TLInstance.js.map +2 -2
  27. package/dist-cjs/records/TLPage.js.map +2 -2
  28. package/dist-cjs/records/TLPageState.js.map +2 -2
  29. package/dist-cjs/records/TLPointer.js.map +2 -2
  30. package/dist-cjs/records/TLPresence.js.map +2 -2
  31. package/dist-cjs/records/TLRecord.js.map +1 -1
  32. package/dist-cjs/records/TLShape.js.map +2 -2
  33. package/dist-cjs/recordsWithProps.js.map +2 -2
  34. package/dist-cjs/shapes/ShapeWithCrop.js.map +1 -1
  35. package/dist-cjs/shapes/TLArrowShape.js.map +2 -2
  36. package/dist-cjs/shapes/TLBaseShape.js.map +2 -2
  37. package/dist-cjs/shapes/TLBookmarkShape.js.map +2 -2
  38. package/dist-cjs/shapes/TLDrawShape.js.map +2 -2
  39. package/dist-cjs/shapes/TLEmbedShape.js.map +2 -2
  40. package/dist-cjs/shapes/TLFrameShape.js.map +2 -2
  41. package/dist-cjs/shapes/TLGeoShape.js.map +2 -2
  42. package/dist-cjs/shapes/TLGroupShape.js.map +2 -2
  43. package/dist-cjs/shapes/TLHighlightShape.js.map +2 -2
  44. package/dist-cjs/shapes/TLImageShape.js.map +2 -2
  45. package/dist-cjs/shapes/TLLineShape.js.map +2 -2
  46. package/dist-cjs/shapes/TLNoteShape.js.map +2 -2
  47. package/dist-cjs/shapes/TLTextShape.js.map +2 -2
  48. package/dist-cjs/shapes/TLVideoShape.js.map +2 -2
  49. package/dist-cjs/store-migrations.js.map +2 -2
  50. package/dist-cjs/styles/TLColorStyle.js.map +2 -2
  51. package/dist-cjs/styles/TLDashStyle.js.map +2 -2
  52. package/dist-cjs/styles/TLFillStyle.js.map +2 -2
  53. package/dist-cjs/styles/TLFontStyle.js.map +2 -2
  54. package/dist-cjs/styles/TLHorizontalAlignStyle.js.map +2 -2
  55. package/dist-cjs/styles/TLSizeStyle.js.map +2 -2
  56. package/dist-cjs/styles/TLTextAlignStyle.js.map +2 -2
  57. package/dist-cjs/styles/TLVerticalAlignStyle.js.map +2 -2
  58. package/dist-cjs/translations/translations.js +1 -1
  59. package/dist-cjs/translations/translations.js.map +2 -2
  60. package/dist-cjs/util-types.js.map +1 -1
  61. package/dist-esm/TLStore.mjs +3 -10
  62. package/dist-esm/TLStore.mjs.map +2 -2
  63. package/dist-esm/assets/TLBaseAsset.mjs.map +2 -2
  64. package/dist-esm/assets/TLBookmarkAsset.mjs.map +2 -2
  65. package/dist-esm/assets/TLImageAsset.mjs.map +2 -2
  66. package/dist-esm/assets/TLVideoAsset.mjs.map +2 -2
  67. package/dist-esm/bindings/TLArrowBinding.mjs.map +2 -2
  68. package/dist-esm/bindings/TLBaseBinding.mjs.map +2 -2
  69. package/dist-esm/createPresenceStateDerivation.mjs.map +2 -2
  70. package/dist-esm/createTLSchema.mjs.map +2 -2
  71. package/dist-esm/index.d.mts +4412 -223
  72. package/dist-esm/index.mjs +1 -1
  73. package/dist-esm/index.mjs.map +2 -2
  74. package/dist-esm/misc/TLColor.mjs.map +2 -2
  75. package/dist-esm/misc/TLCursor.mjs.map +2 -2
  76. package/dist-esm/misc/TLHandle.mjs.map +2 -2
  77. package/dist-esm/misc/TLOpacity.mjs.map +2 -2
  78. package/dist-esm/misc/TLRichText.mjs.map +2 -2
  79. package/dist-esm/misc/TLScribble.mjs.map +2 -2
  80. package/dist-esm/misc/geometry-types.mjs.map +2 -2
  81. package/dist-esm/misc/id-validator.mjs.map +2 -2
  82. package/dist-esm/records/TLAsset.mjs.map +2 -2
  83. package/dist-esm/records/TLBinding.mjs.map +2 -2
  84. package/dist-esm/records/TLCamera.mjs.map +2 -2
  85. package/dist-esm/records/TLDocument.mjs.map +2 -2
  86. package/dist-esm/records/TLInstance.mjs.map +2 -2
  87. package/dist-esm/records/TLPage.mjs.map +2 -2
  88. package/dist-esm/records/TLPageState.mjs.map +2 -2
  89. package/dist-esm/records/TLPointer.mjs.map +2 -2
  90. package/dist-esm/records/TLPresence.mjs.map +2 -2
  91. package/dist-esm/records/TLShape.mjs.map +2 -2
  92. package/dist-esm/recordsWithProps.mjs.map +2 -2
  93. package/dist-esm/shapes/TLArrowShape.mjs.map +2 -2
  94. package/dist-esm/shapes/TLBaseShape.mjs.map +2 -2
  95. package/dist-esm/shapes/TLBookmarkShape.mjs.map +2 -2
  96. package/dist-esm/shapes/TLDrawShape.mjs.map +2 -2
  97. package/dist-esm/shapes/TLEmbedShape.mjs.map +2 -2
  98. package/dist-esm/shapes/TLFrameShape.mjs.map +2 -2
  99. package/dist-esm/shapes/TLGeoShape.mjs.map +2 -2
  100. package/dist-esm/shapes/TLGroupShape.mjs.map +2 -2
  101. package/dist-esm/shapes/TLHighlightShape.mjs.map +2 -2
  102. package/dist-esm/shapes/TLImageShape.mjs.map +2 -2
  103. package/dist-esm/shapes/TLLineShape.mjs.map +2 -2
  104. package/dist-esm/shapes/TLNoteShape.mjs.map +2 -2
  105. package/dist-esm/shapes/TLTextShape.mjs.map +2 -2
  106. package/dist-esm/shapes/TLVideoShape.mjs.map +2 -2
  107. package/dist-esm/store-migrations.mjs.map +2 -2
  108. package/dist-esm/styles/TLColorStyle.mjs.map +2 -2
  109. package/dist-esm/styles/TLDashStyle.mjs.map +2 -2
  110. package/dist-esm/styles/TLFillStyle.mjs.map +2 -2
  111. package/dist-esm/styles/TLFontStyle.mjs.map +2 -2
  112. package/dist-esm/styles/TLHorizontalAlignStyle.mjs.map +2 -2
  113. package/dist-esm/styles/TLSizeStyle.mjs.map +2 -2
  114. package/dist-esm/styles/TLTextAlignStyle.mjs.map +2 -2
  115. package/dist-esm/styles/TLVerticalAlignStyle.mjs.map +2 -2
  116. package/dist-esm/translations/translations.mjs +1 -1
  117. package/dist-esm/translations/translations.mjs.map +2 -2
  118. package/package.json +5 -5
  119. package/src/TLStore.test.ts +644 -0
  120. package/src/TLStore.ts +205 -20
  121. package/src/assets/TLBaseAsset.ts +90 -7
  122. package/src/assets/TLBookmarkAsset.test.ts +96 -0
  123. package/src/assets/TLBookmarkAsset.ts +52 -2
  124. package/src/assets/TLImageAsset.test.ts +213 -0
  125. package/src/assets/TLImageAsset.ts +60 -2
  126. package/src/assets/TLVideoAsset.test.ts +105 -0
  127. package/src/assets/TLVideoAsset.ts +93 -4
  128. package/src/bindings/TLArrowBinding.test.ts +55 -0
  129. package/src/bindings/TLArrowBinding.ts +132 -10
  130. package/src/bindings/TLBaseBinding.ts +140 -3
  131. package/src/createPresenceStateDerivation.test.ts +158 -0
  132. package/src/createPresenceStateDerivation.ts +71 -2
  133. package/src/createTLSchema.test.ts +181 -0
  134. package/src/createTLSchema.ts +164 -7
  135. package/src/index.ts +32 -0
  136. package/src/misc/TLColor.ts +50 -6
  137. package/src/misc/TLCursor.ts +110 -8
  138. package/src/misc/TLHandle.ts +82 -6
  139. package/src/misc/TLOpacity.ts +51 -2
  140. package/src/misc/TLRichText.ts +56 -3
  141. package/src/misc/TLScribble.ts +105 -5
  142. package/src/misc/geometry-types.ts +30 -2
  143. package/src/misc/id-validator.test.ts +50 -0
  144. package/src/misc/id-validator.ts +20 -1
  145. package/src/records/TLAsset.test.ts +234 -0
  146. package/src/records/TLAsset.ts +165 -8
  147. package/src/records/TLBinding.test.ts +22 -0
  148. package/src/records/TLBinding.ts +277 -11
  149. package/src/records/TLCamera.test.ts +19 -0
  150. package/src/records/TLCamera.ts +118 -7
  151. package/src/records/TLDocument.test.ts +35 -0
  152. package/src/records/TLDocument.ts +148 -8
  153. package/src/records/TLInstance.test.ts +201 -0
  154. package/src/records/TLInstance.ts +117 -9
  155. package/src/records/TLPage.test.ts +110 -0
  156. package/src/records/TLPage.ts +106 -8
  157. package/src/records/TLPageState.test.ts +228 -0
  158. package/src/records/TLPageState.ts +88 -7
  159. package/src/records/TLPointer.test.ts +63 -0
  160. package/src/records/TLPointer.ts +105 -7
  161. package/src/records/TLPresence.test.ts +190 -0
  162. package/src/records/TLPresence.ts +99 -5
  163. package/src/records/TLRecord.test.ts +70 -0
  164. package/src/records/TLRecord.ts +43 -1
  165. package/src/records/TLShape.test.ts +232 -0
  166. package/src/records/TLShape.ts +289 -12
  167. package/src/recordsWithProps.test.ts +188 -0
  168. package/src/recordsWithProps.ts +131 -2
  169. package/src/shapes/ShapeWithCrop.test.ts +18 -0
  170. package/src/shapes/ShapeWithCrop.ts +64 -2
  171. package/src/shapes/TLArrowShape.test.ts +505 -0
  172. package/src/shapes/TLArrowShape.ts +188 -10
  173. package/src/shapes/TLBaseShape.test.ts +142 -0
  174. package/src/shapes/TLBaseShape.ts +103 -4
  175. package/src/shapes/TLBookmarkShape.test.ts +122 -0
  176. package/src/shapes/TLBookmarkShape.ts +58 -4
  177. package/src/shapes/TLDrawShape.test.ts +177 -0
  178. package/src/shapes/TLDrawShape.ts +97 -6
  179. package/src/shapes/TLEmbedShape.test.ts +286 -0
  180. package/src/shapes/TLEmbedShape.ts +57 -4
  181. package/src/shapes/TLFrameShape.test.ts +71 -0
  182. package/src/shapes/TLFrameShape.ts +59 -4
  183. package/src/shapes/TLGeoShape.test.ts +247 -0
  184. package/src/shapes/TLGeoShape.ts +103 -7
  185. package/src/shapes/TLGroupShape.test.ts +59 -0
  186. package/src/shapes/TLGroupShape.ts +52 -4
  187. package/src/shapes/TLHighlightShape.test.ts +325 -0
  188. package/src/shapes/TLHighlightShape.ts +79 -4
  189. package/src/shapes/TLImageShape.test.ts +534 -0
  190. package/src/shapes/TLImageShape.ts +105 -5
  191. package/src/shapes/TLLineShape.test.ts +269 -0
  192. package/src/shapes/TLLineShape.ts +128 -8
  193. package/src/shapes/TLNoteShape.test.ts +1568 -0
  194. package/src/shapes/TLNoteShape.ts +97 -4
  195. package/src/shapes/TLTextShape.test.ts +407 -0
  196. package/src/shapes/TLTextShape.ts +94 -4
  197. package/src/shapes/TLVideoShape.test.ts +112 -0
  198. package/src/shapes/TLVideoShape.ts +99 -4
  199. package/src/store-migrations.test.ts +88 -0
  200. package/src/store-migrations.ts +47 -1
  201. package/src/styles/TLColorStyle.test.ts +439 -0
  202. package/src/styles/TLColorStyle.ts +228 -10
  203. package/src/styles/TLDashStyle.ts +54 -2
  204. package/src/styles/TLFillStyle.ts +54 -2
  205. package/src/styles/TLFontStyle.ts +72 -3
  206. package/src/styles/TLHorizontalAlignStyle.ts +55 -2
  207. package/src/styles/TLSizeStyle.ts +54 -2
  208. package/src/styles/TLTextAlignStyle.ts +52 -2
  209. package/src/styles/TLVerticalAlignStyle.ts +52 -2
  210. package/src/translations/translations.test.ts +378 -35
  211. package/src/translations/translations.ts +157 -10
  212. package/src/util-types.ts +51 -1
@@ -10,17 +10,58 @@ import { JsonObject } from '@tldraw/utils'
10
10
  import { T } from '@tldraw/validate'
11
11
 
12
12
  /**
13
- * TLDocument
13
+ * Document record containing global settings and metadata for a tldraw document.
14
+ * There is exactly one document record per tldraw instance with a fixed ID.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * const document: TLDocument = {
19
+ * id: 'document:document',
20
+ * typeName: 'document',
21
+ * gridSize: 20, // Grid snap size in pixels
22
+ * name: 'My Drawing', // Document name
23
+ * meta: {
24
+ * createdAt: Date.now(),
25
+ * author: 'user123',
26
+ * version: '1.0.0'
27
+ * }
28
+ * }
29
+ *
30
+ * // Update document settings
31
+ * editor.updateDocumentSettings({
32
+ * name: 'Updated Drawing',
33
+ * gridSize: 25
34
+ * })
35
+ * ```
14
36
  *
15
37
  * @public
16
38
  */
17
39
  export interface TLDocument extends BaseRecord<'document', RecordId<TLDocument>> {
40
+ /** Grid snap size in pixels. Used for shape positioning and alignment */
18
41
  gridSize: number
42
+ /** Human-readable name of the document */
19
43
  name: string
44
+ /** User-defined metadata for the document */
20
45
  meta: JsonObject
21
46
  }
22
47
 
23
- /** @public */
48
+ /**
49
+ * Validator for TLDocument records that ensures runtime type safety.
50
+ * Enforces the fixed document ID and validates all document properties.
51
+ *
52
+ * @example
53
+ * ```ts
54
+ * // Validation happens automatically when document is stored
55
+ * try {
56
+ * const validatedDocument = documentValidator.validate(documentData)
57
+ * store.put([validatedDocument])
58
+ * } catch (error) {
59
+ * console.error('Document validation failed:', error.message)
60
+ * }
61
+ * ```
62
+ *
63
+ * @public
64
+ */
24
65
  export const documentValidator: T.Validator<TLDocument> = T.model(
25
66
  'document',
26
67
  T.object({
@@ -32,19 +73,72 @@ export const documentValidator: T.Validator<TLDocument> = T.model(
32
73
  })
33
74
  )
34
75
 
35
- /** @public */
76
+ /**
77
+ * Type guard to check if a record is a TLDocument.
78
+ * Useful for filtering or type narrowing when working with mixed record types.
79
+ *
80
+ * @param record - The record to check
81
+ * @returns True if the record is a document, false otherwise
82
+ *
83
+ * @example
84
+ * ```ts
85
+ * // Type guard usage
86
+ * function processRecord(record: UnknownRecord) {
87
+ * if (isDocument(record)) {
88
+ * // record is now typed as TLDocument
89
+ * console.log(`Document: ${record.name}, Grid: ${record.gridSize}px`)
90
+ * }
91
+ * }
92
+ *
93
+ * // Filter documents from mixed records
94
+ * const allRecords = store.allRecords()
95
+ * const documents = allRecords.filter(isDocument) // Should be exactly one
96
+ * ```
97
+ *
98
+ * @public
99
+ */
36
100
  export function isDocument(record?: UnknownRecord): record is TLDocument {
37
101
  if (!record) return false
38
102
  return record.typeName === 'document'
39
103
  }
40
104
 
41
- /** @public */
105
+ /**
106
+ * Migration version identifiers for document record schema evolution.
107
+ * Each version represents a breaking change that requires data migration.
108
+ *
109
+ * @example
110
+ * ```ts
111
+ * // Check if document needs migration
112
+ * const needsNameMigration = currentVersion < documentVersions.AddName
113
+ * const needsMetaMigration = currentVersion < documentVersions.AddMeta
114
+ * ```
115
+ *
116
+ * @public
117
+ */
42
118
  export const documentVersions = createMigrationIds('com.tldraw.document', {
43
119
  AddName: 1,
44
120
  AddMeta: 2,
45
121
  } as const)
46
122
 
47
- /** @public */
123
+ /**
124
+ * Migration sequence for evolving document record structure over time.
125
+ * Handles converting document records from older schema versions to current format.
126
+ *
127
+ * @example
128
+ * ```ts
129
+ * // Migration is applied automatically when loading old documents
130
+ * const migratedStore = migrator.migrateStoreSnapshot({
131
+ * schema: oldSchema,
132
+ * store: oldStoreSnapshot
133
+ * })
134
+ *
135
+ * // The migrations:
136
+ * // v1: Added 'name' property with empty string default
137
+ * // v2: Added 'meta' property with empty object default
138
+ * ```
139
+ *
140
+ * @public
141
+ */
48
142
  export const documentMigrations = createRecordMigrationSequence({
49
143
  sequenceId: 'com.tldraw.document',
50
144
  recordType: 'document',
@@ -67,7 +161,32 @@ export const documentMigrations = createRecordMigrationSequence({
67
161
  ],
68
162
  })
69
163
 
70
- /** @public */
164
+ /**
165
+ * Record type definition for TLDocument with validation and default properties.
166
+ * Configures the document as a document-scoped record that persists across sessions.
167
+ *
168
+ * @example
169
+ * ```ts
170
+ * // Create a document record (usually done automatically)
171
+ * const documentRecord = DocumentRecordType.create({
172
+ * id: TLDOCUMENT_ID,
173
+ * name: 'My Drawing',
174
+ * gridSize: 20,
175
+ * meta: { createdAt: Date.now() }
176
+ * })
177
+ *
178
+ * // Create with defaults
179
+ * const defaultDocument = DocumentRecordType.create({
180
+ * id: TLDOCUMENT_ID
181
+ * // gridSize: 10, name: '', meta: {} are applied as defaults
182
+ * })
183
+ *
184
+ * // Store the document
185
+ * store.put([documentRecord])
186
+ * ```
187
+ *
188
+ * @public
189
+ */
71
190
  export const DocumentRecordType = createRecordType<TLDocument>('document', {
72
191
  validator: documentValidator,
73
192
  scope: 'document',
@@ -79,6 +198,27 @@ export const DocumentRecordType = createRecordType<TLDocument>('document', {
79
198
  })
80
199
  )
81
200
 
82
- // all document records have the same ID: 'document:document'
83
- /** @public */
201
+ /**
202
+ * The fixed ID for the singleton document record in every tldraw store.
203
+ * All document records use this same ID: 'document:document'
204
+ *
205
+ * @example
206
+ * ```ts
207
+ * // Get the document from store
208
+ * const document = store.get(TLDOCUMENT_ID)
209
+ *
210
+ * // Update document settings
211
+ * store.put([{
212
+ * ...document,
213
+ * name: 'Updated Name',
214
+ * gridSize: 25
215
+ * }])
216
+ *
217
+ * // Access via editor
218
+ * const documentSettings = editor.getDocumentSettings()
219
+ * editor.updateDocumentSettings({ name: 'New Name' })
220
+ * ```
221
+ *
222
+ * @public
223
+ */
84
224
  export const TLDOCUMENT_ID: RecordId<TLDocument> = DocumentRecordType.createId('document')
@@ -0,0 +1,201 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { StyleProp } from '../styles/StyleProp'
3
+ import {
4
+ createInstanceRecordType,
5
+ instanceIdValidator,
6
+ instanceMigrations,
7
+ instanceVersions,
8
+ pluckPreservingValues,
9
+ shouldKeyBePreservedBetweenSessions,
10
+ TLInstance,
11
+ TLINSTANCE_ID,
12
+ } from './TLInstance'
13
+
14
+ // Mock style prop for testing
15
+ const mockColorStyle = {
16
+ type: 'color',
17
+ defaultValue: 'black',
18
+ getDefaultValue: () => 'black',
19
+ } as unknown as StyleProp<string>
20
+
21
+ const mockStylesMap = new Map([['color', mockColorStyle]])
22
+ createInstanceRecordType(mockStylesMap)
23
+
24
+ describe('shouldKeyBePreservedBetweenSessions', () => {
25
+ it('should preserve user preferences', () => {
26
+ const userPreferences = [
27
+ 'isFocusMode',
28
+ 'isDebugMode',
29
+ 'isToolLocked',
30
+ 'exportBackground',
31
+ 'isGridMode',
32
+ 'isReadonly',
33
+ ]
34
+
35
+ userPreferences.forEach((key) => {
36
+ expect(
37
+ shouldKeyBePreservedBetweenSessions[key as keyof typeof shouldKeyBePreservedBetweenSessions]
38
+ ).toBe(true)
39
+ })
40
+ })
41
+
42
+ it('should not preserve temporary state', () => {
43
+ const temporaryState = [
44
+ 'currentPageId',
45
+ 'opacityForNextShape',
46
+ 'stylesForNextShape',
47
+ 'followingUserId',
48
+ 'brush',
49
+ 'cursor',
50
+ 'scribbles',
51
+ 'zoomBrush',
52
+ 'chatMessage',
53
+ 'isChatting',
54
+ 'isPenMode',
55
+ 'isHoveringCanvas',
56
+ 'openMenus',
57
+ 'isChangingStyle',
58
+ 'duplicateProps',
59
+ ]
60
+
61
+ temporaryState.forEach((key) => {
62
+ expect(
63
+ shouldKeyBePreservedBetweenSessions[key as keyof typeof shouldKeyBePreservedBetweenSessions]
64
+ ).toBe(false)
65
+ })
66
+ })
67
+ })
68
+
69
+ describe('pluckPreservingValues', () => {
70
+ it('should return null for null or undefined input', () => {
71
+ expect(pluckPreservingValues(null)).toBe(null)
72
+ expect(pluckPreservingValues(undefined)).toBe(null)
73
+ })
74
+
75
+ it('should filter properties according to preservation rules', () => {
76
+ const fullInstance: TLInstance = {
77
+ id: TLINSTANCE_ID,
78
+ typeName: 'instance',
79
+ currentPageId: 'page:page1' as any,
80
+ opacityForNextShape: 0.5,
81
+ stylesForNextShape: {},
82
+ followingUserId: null,
83
+ highlightedUserIds: [],
84
+ brush: null,
85
+ cursor: { type: 'default', rotation: 0 },
86
+ scribbles: [],
87
+ isFocusMode: true,
88
+ isDebugMode: false,
89
+ isToolLocked: true,
90
+ exportBackground: true,
91
+ screenBounds: { x: 0, y: 0, w: 1920, h: 1080 },
92
+ insets: [false, false, false, false],
93
+ zoomBrush: null,
94
+ chatMessage: '',
95
+ isChatting: false,
96
+ isPenMode: false,
97
+ isGridMode: true,
98
+ isFocused: true,
99
+ devicePixelRatio: 2,
100
+ isCoarsePointer: false,
101
+ isHoveringCanvas: null,
102
+ openMenus: [],
103
+ isChangingStyle: false,
104
+ isReadonly: false,
105
+ meta: {},
106
+ duplicateProps: null,
107
+ }
108
+
109
+ const preserved = pluckPreservingValues(fullInstance)
110
+
111
+ // Should preserve user preferences
112
+ expect(preserved?.isFocusMode).toBe(true)
113
+ expect(preserved?.isDebugMode).toBe(false)
114
+ expect(preserved?.isToolLocked).toBe(true)
115
+ expect(preserved?.exportBackground).toBe(true)
116
+ expect(preserved?.isGridMode).toBe(true)
117
+
118
+ // Should not preserve temporary state
119
+ expect(preserved?.currentPageId).toBeUndefined()
120
+ expect(preserved?.opacityForNextShape).toBeUndefined()
121
+ expect(preserved?.brush).toBeUndefined()
122
+ expect(preserved?.cursor).toBeUndefined()
123
+ expect(preserved?.chatMessage).toBeUndefined()
124
+ expect(preserved?.openMenus).toBeUndefined()
125
+ })
126
+ })
127
+
128
+ describe('instanceIdValidator', () => {
129
+ it('should validate correct instance IDs and reject invalid ones', () => {
130
+ expect(() => instanceIdValidator.validate('instance:instance')).not.toThrow()
131
+ expect(() => instanceIdValidator.validate('instance:test')).not.toThrow()
132
+ expect(() => instanceIdValidator.validate(TLINSTANCE_ID)).not.toThrow()
133
+
134
+ expect(() => instanceIdValidator.validate('invalid')).toThrow()
135
+ expect(() => instanceIdValidator.validate('page:instance')).toThrow()
136
+ expect(() => instanceIdValidator.validate('')).toThrow()
137
+ })
138
+ })
139
+
140
+ describe('createInstanceRecordType', () => {
141
+ it('should create a valid record type with correct configuration', () => {
142
+ const recordType = createInstanceRecordType(mockStylesMap)
143
+ expect(recordType.typeName).toBe('instance')
144
+ expect(recordType.scope).toBe('session')
145
+ })
146
+ })
147
+
148
+ describe('instanceMigrations', () => {
149
+ it('should have correct migration configuration', () => {
150
+ expect(instanceMigrations.sequenceId).toBe('com.tldraw.instance')
151
+ expect(Array.isArray(instanceMigrations.sequence)).toBe(true)
152
+ expect(instanceMigrations.sequence.length).toBe(25)
153
+ })
154
+
155
+ it('should migrate HoistOpacity correctly', () => {
156
+ const migration = instanceMigrations.sequence.find(
157
+ (m) => m.id === instanceVersions.HoistOpacity
158
+ )!
159
+ const oldRecord: any = {
160
+ id: TLINSTANCE_ID,
161
+ typeName: 'instance',
162
+ propsForNextShape: {
163
+ opacity: '0.5',
164
+ color: 'red',
165
+ },
166
+ }
167
+ const result = migration.up(oldRecord)
168
+
169
+ expect((result as any).opacityForNextShape).toBe(0.5)
170
+ expect((result as any).propsForNextShape.opacity).toBeUndefined()
171
+ expect((result as any).propsForNextShape.color).toBe('red')
172
+ })
173
+
174
+ it('should migrate RemoveDialog correctly', () => {
175
+ const migration = instanceMigrations.sequence.find(
176
+ (m) => m.id === instanceVersions.RemoveDialog
177
+ )!
178
+ const oldRecord: any = {
179
+ id: TLINSTANCE_ID,
180
+ typeName: 'instance',
181
+ dialog: 'some-dialog',
182
+ otherProp: 'keep-me',
183
+ }
184
+ const result = migration.up(oldRecord)
185
+
186
+ expect((result as any).dialog).toBeUndefined()
187
+ expect((result as any).otherProp).toBe('keep-me')
188
+ })
189
+
190
+ it('should have bidirectional migrations where applicable', () => {
191
+ const addInsetMigration = instanceMigrations.sequence.find(
192
+ (m) => m.id === instanceVersions.AddInset
193
+ )!
194
+ expect(addInsetMigration.down).toBeDefined()
195
+
196
+ const removeCameraMigration = instanceMigrations.sequence.find(
197
+ (m) => m.id === instanceVersions.RemoveCanMoveCamera
198
+ )!
199
+ expect(removeCameraMigration.down).toBeDefined()
200
+ })
201
+ })
@@ -17,9 +17,25 @@ import { pageIdValidator, TLPageId } from './TLPage'
17
17
  import { TLShapeId } from './TLShape'
18
18
 
19
19
  /**
20
- * TLInstance
20
+ * State that is particular to a single browser tab. The TLInstance record stores
21
+ * all session-specific state including cursor position, selected tools, UI preferences,
22
+ * and temporary interaction state.
21
23
  *
22
- * State that is particular to a single browser tab
24
+ * Each browser tab has exactly one TLInstance record that persists for the duration
25
+ * of the session and tracks the user's current interaction state.
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * const instance: TLInstance = {
30
+ * id: 'instance:instance',
31
+ * typeName: 'instance',
32
+ * currentPageId: 'page:page1',
33
+ * cursor: { type: 'default', rotation: 0 },
34
+ * screenBounds: { x: 0, y: 0, w: 1920, h: 1080 },
35
+ * isFocusMode: false,
36
+ * isGridMode: true
37
+ * }
38
+ * ```
23
39
  *
24
40
  * @public
25
41
  */
@@ -68,7 +84,14 @@ export interface TLInstance extends BaseRecord<'instance', TLInstanceId> {
68
84
  } | null
69
85
  }
70
86
 
71
- /** @internal */
87
+ /**
88
+ * Configuration object defining which TLInstance properties should be preserved
89
+ * when loading snapshots across browser sessions. Properties marked as `true`
90
+ * represent user preferences that should persist, while `false` indicates
91
+ * temporary state that should reset.
92
+ *
93
+ * @internal
94
+ */
72
95
  export const shouldKeyBePreservedBetweenSessions = {
73
96
  // This object defines keys that should be preserved across calls to loadSnapshot()
74
97
 
@@ -108,7 +131,15 @@ export const shouldKeyBePreservedBetweenSessions = {
108
131
  duplicateProps: false, //
109
132
  } as const satisfies { [K in keyof TLInstance]: boolean }
110
133
 
111
- /** @internal */
134
+ /**
135
+ * Extracts only the properties from a TLInstance that should be preserved
136
+ * between browser sessions, filtering out temporary state.
137
+ *
138
+ * @param val - The TLInstance to filter, or null/undefined
139
+ * @returns A partial TLInstance containing only preservable properties, or null
140
+ *
141
+ * @internal
142
+ */
112
143
  export function pluckPreservingValues(val?: TLInstance | null): null | Partial<TLInstance> {
113
144
  return val
114
145
  ? (filterEntries(val, (key) => {
@@ -117,12 +148,51 @@ export function pluckPreservingValues(val?: TLInstance | null): null | Partial<T
117
148
  : null
118
149
  }
119
150
 
120
- /** @public */
151
+ /**
152
+ * A unique identifier for TLInstance records.
153
+ *
154
+ * TLInstance IDs are always the constant 'instance:instance' since there
155
+ * is exactly one instance record per browser tab.
156
+ *
157
+ * @public
158
+ */
121
159
  export type TLInstanceId = RecordId<TLInstance>
122
160
 
123
- /** @public */
161
+ /**
162
+ * Validator for TLInstanceId values. Ensures the ID follows the correct
163
+ * format for instance records.
164
+ *
165
+ * @example
166
+ * ```ts
167
+ * const isValid = instanceIdValidator.isValid('instance:instance') // true
168
+ * const isValid2 = instanceIdValidator.isValid('invalid') // false
169
+ * ```
170
+ *
171
+ * @public
172
+ */
124
173
  export const instanceIdValidator = idValidator<TLInstanceId>('instance')
125
174
 
175
+ /**
176
+ * Creates the record type definition for TLInstance records, including validation
177
+ * and default properties. The function takes a map of available style properties
178
+ * to configure validation for the stylesForNextShape field.
179
+ *
180
+ * @param stylesById - Map of style property IDs to their corresponding StyleProp definitions
181
+ * @returns A configured RecordType for TLInstance records
182
+ *
183
+ * @example
184
+ * ```ts
185
+ * const stylesMap = new Map([['color', DefaultColorStyle]])
186
+ * const InstanceRecordType = createInstanceRecordType(stylesMap)
187
+ *
188
+ * const instance = InstanceRecordType.create({
189
+ * id: 'instance:instance',
190
+ * currentPageId: 'page:page1'
191
+ * })
192
+ * ```
193
+ *
194
+ * @public
195
+ */
126
196
  export function createInstanceRecordType(stylesById: Map<string, StyleProp<unknown>>) {
127
197
  const stylesForNextShapeValidators = {} as Record<string, T.Validator<unknown>>
128
198
  for (const [id, style] of stylesById) {
@@ -241,7 +311,15 @@ export function createInstanceRecordType(stylesById: Map<string, StyleProp<unkno
241
311
  )
242
312
  }
243
313
 
244
- /** @public */
314
+ /**
315
+ * Migration version identifiers for TLInstance records. Each version represents
316
+ * a schema change that requires data transformation when loading older documents.
317
+ *
318
+ * The versions track the evolution of the instance record structure over time,
319
+ * enabling backward and forward compatibility.
320
+ *
321
+ * @public
322
+ */
245
323
  export const instanceVersions = createMigrationIds('com.tldraw.instance', {
246
324
  AddTransparentExportBgs: 1,
247
325
  RemoveDialog: 2,
@@ -272,7 +350,22 @@ export const instanceVersions = createMigrationIds('com.tldraw.instance', {
272
350
 
273
351
  // TODO: rewrite these to use mutation
274
352
 
275
- /** @public */
353
+ /**
354
+ * Migration sequence for TLInstance records. Defines how to transform instance
355
+ * records between different schema versions, ensuring data compatibility when
356
+ * loading documents created with different versions of tldraw.
357
+ *
358
+ * Each migration includes an 'up' function to migrate forward and optionally
359
+ * a 'down' function for reverse migration.
360
+ *
361
+ * @example
362
+ * ```ts
363
+ * // Migrations are applied automatically when loading documents
364
+ * const migratedInstance = instanceMigrations.migrate(oldInstance, targetVersion)
365
+ * ```
366
+ *
367
+ * @public
368
+ */
276
369
  export const instanceMigrations = createRecordMigrationSequence({
277
370
  sequenceId: 'com.tldraw.instance',
278
371
  recordType: 'instance',
@@ -524,5 +617,20 @@ export const instanceMigrations = createRecordMigrationSequence({
524
617
  ],
525
618
  })
526
619
 
527
- /** @public */
620
+ /**
621
+ * The constant ID used for the singleton TLInstance record.
622
+ *
623
+ * Since each browser tab has exactly one instance, this constant ID
624
+ * is used universally across the application.
625
+ *
626
+ * @example
627
+ * ```ts
628
+ * const instance = store.get(TLINSTANCE_ID)
629
+ * if (instance) {
630
+ * console.log('Current page:', instance.currentPageId)
631
+ * }
632
+ * ```
633
+ *
634
+ * @public
635
+ */
528
636
  export const TLINSTANCE_ID = 'instance:instance' as TLInstanceId
@@ -0,0 +1,110 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ isPageId,
4
+ pageIdValidator,
5
+ pageMigrations,
6
+ PageRecordType,
7
+ pageValidator,
8
+ pageVersions,
9
+ TLPageId,
10
+ } from './TLPage'
11
+
12
+ describe('pageIdValidator', () => {
13
+ it('should validate correct page IDs', () => {
14
+ expect(() => pageIdValidator.validate('page:main')).not.toThrow()
15
+ expect(() => pageIdValidator.validate('page:page1')).not.toThrow()
16
+ })
17
+
18
+ it('should reject invalid page IDs', () => {
19
+ expect(() => pageIdValidator.validate('invalid')).toThrow()
20
+ expect(() => pageIdValidator.validate('shape:page1')).toThrow()
21
+ expect(() => pageIdValidator.validate('')).toThrow()
22
+ })
23
+ })
24
+
25
+ describe('pageValidator', () => {
26
+ it('should validate valid page records', () => {
27
+ const validPage = {
28
+ typeName: 'page',
29
+ id: 'page:test' as TLPageId,
30
+ name: 'Test Page',
31
+ index: 'a1' as any,
32
+ meta: {},
33
+ }
34
+
35
+ expect(() => pageValidator.validate(validPage)).not.toThrow()
36
+ })
37
+
38
+ it('should reject pages with invalid typeName', () => {
39
+ const invalidPage = {
40
+ typeName: 'not-page',
41
+ id: 'page:test' as TLPageId,
42
+ name: 'Test',
43
+ index: 'a1' as any,
44
+ meta: {},
45
+ }
46
+
47
+ expect(() => pageValidator.validate(invalidPage)).toThrow()
48
+ })
49
+
50
+ it('should reject pages with missing required fields', () => {
51
+ const incompletePages = [
52
+ {
53
+ typeName: 'page',
54
+ id: 'page:test' as TLPageId,
55
+ // missing name, index, meta
56
+ },
57
+ {
58
+ typeName: 'page',
59
+ id: 'page:test' as TLPageId,
60
+ name: 'Test',
61
+ // missing index, meta
62
+ },
63
+ ]
64
+
65
+ incompletePages.forEach((page) => {
66
+ expect(() => pageValidator.validate(page)).toThrow()
67
+ })
68
+ })
69
+ })
70
+
71
+ describe('pageMigrations', () => {
72
+ it('should apply AddMeta migration correctly', () => {
73
+ const addMetaMigration = pageMigrations.sequence.find((m) => m.id === pageVersions.AddMeta)!
74
+
75
+ const oldRecord: any = {
76
+ typeName: 'page',
77
+ id: 'page:test',
78
+ name: 'Test Page',
79
+ index: 'a1',
80
+ }
81
+
82
+ addMetaMigration.up(oldRecord)
83
+ expect(oldRecord.meta).toEqual({})
84
+ })
85
+ })
86
+
87
+ describe('PageRecordType', () => {
88
+ it('should create page records with defaults', () => {
89
+ const page = PageRecordType.create({
90
+ id: 'page:test' as TLPageId,
91
+ name: 'Test Page',
92
+ index: 'a1' as any,
93
+ })
94
+
95
+ expect(page.meta).toEqual({})
96
+ })
97
+ })
98
+
99
+ describe('isPageId', () => {
100
+ it('should return true for valid page IDs', () => {
101
+ expect(isPageId('page:main')).toBe(true)
102
+ expect(isPageId('page:page1')).toBe(true)
103
+ })
104
+
105
+ it('should return false for invalid page IDs', () => {
106
+ expect(isPageId('shape:main')).toBe(false)
107
+ expect(isPageId('invalid')).toBe(false)
108
+ expect(isPageId('')).toBe(false)
109
+ })
110
+ })