@tldraw/tlschema 4.1.0-canary.8e597b345c40 → 4.1.0-canary.95d46c96eb30

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
@@ -0,0 +1,534 @@
1
+ import { T } from '@tldraw/validate'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { getTestMigration } from '../__tests__/migrationTestUtils'
4
+ import { TLAssetId } from '../records/TLAsset'
5
+ import { TLShapeCrop } from './ShapeWithCrop'
6
+ import { ImageShapeCrop, imageShapeProps, imageShapeVersions } from './TLImageShape'
7
+
8
+ describe('TLImageShape', () => {
9
+ describe('ImageShapeCrop validator', () => {
10
+ it('should validate valid crop data', () => {
11
+ const validCrop: TLShapeCrop = {
12
+ topLeft: { x: 0.1, y: 0.1 },
13
+ bottomRight: { x: 0.9, y: 0.9 },
14
+ }
15
+
16
+ expect(() => ImageShapeCrop.validate(validCrop)).not.toThrow()
17
+ const result = ImageShapeCrop.validate(validCrop)
18
+ expect(result.topLeft).toEqual({ x: 0.1, y: 0.1 })
19
+ expect(result.bottomRight).toEqual({ x: 0.9, y: 0.9 })
20
+ })
21
+
22
+ it('should validate crop data with isCircle flag', () => {
23
+ const cropWithCircle: TLShapeCrop = {
24
+ topLeft: { x: 0, y: 0 },
25
+ bottomRight: { x: 1, y: 1 },
26
+ isCircle: true,
27
+ }
28
+
29
+ expect(() => ImageShapeCrop.validate(cropWithCircle)).not.toThrow()
30
+ const result = ImageShapeCrop.validate(cropWithCircle)
31
+ expect(result.isCircle).toBe(true)
32
+ })
33
+
34
+ it('should validate crop data without isCircle flag', () => {
35
+ const cropWithoutCircle: TLShapeCrop = {
36
+ topLeft: { x: 0.25, y: 0.25 },
37
+ bottomRight: { x: 0.75, y: 0.75 },
38
+ }
39
+
40
+ expect(() => ImageShapeCrop.validate(cropWithoutCircle)).not.toThrow()
41
+ const result = ImageShapeCrop.validate(cropWithoutCircle)
42
+ expect(result.isCircle).toBeUndefined()
43
+ })
44
+
45
+ it('should validate crop data with explicit false isCircle', () => {
46
+ const cropWithFalseCircle: TLShapeCrop = {
47
+ topLeft: { x: 0, y: 0.5 },
48
+ bottomRight: { x: 1, y: 1 },
49
+ isCircle: false,
50
+ }
51
+
52
+ expect(() => ImageShapeCrop.validate(cropWithFalseCircle)).not.toThrow()
53
+ const result = ImageShapeCrop.validate(cropWithFalseCircle)
54
+ expect(result.isCircle).toBe(false)
55
+ })
56
+
57
+ it('should validate edge case coordinate values', () => {
58
+ const edgeCaseCrops = [
59
+ {
60
+ topLeft: { x: 0, y: 0 },
61
+ bottomRight: { x: 1, y: 1 },
62
+ },
63
+ {
64
+ topLeft: { x: 0.5, y: 0.5 },
65
+ bottomRight: { x: 0.5, y: 0.5 },
66
+ },
67
+ {
68
+ topLeft: { x: 0.001, y: 0.999 },
69
+ bottomRight: { x: 0.999, y: 0.001 },
70
+ },
71
+ ]
72
+
73
+ edgeCaseCrops.forEach((crop, _index) => {
74
+ expect(() => ImageShapeCrop.validate(crop)).not.toThrow()
75
+ })
76
+ })
77
+
78
+ it('should reject invalid crop data structures', () => {
79
+ const invalidCrops = [
80
+ {}, // Missing required properties
81
+ { topLeft: { x: 0.1, y: 0.1 } }, // Missing bottomRight
82
+ { bottomRight: { x: 0.9, y: 0.9 } }, // Missing topLeft
83
+ {
84
+ topLeft: { x: 'invalid', y: 0.1 },
85
+ bottomRight: { x: 0.9, y: 0.9 },
86
+ }, // Invalid coordinate type
87
+ {
88
+ topLeft: { x: 0.1, y: 0.1 },
89
+ bottomRight: { x: 0.9, y: 'invalid' },
90
+ }, // Invalid coordinate type
91
+ {
92
+ topLeft: { x: 0.1 }, // Missing y
93
+ bottomRight: { x: 0.9, y: 0.9 },
94
+ },
95
+ {
96
+ topLeft: { x: 0.1, y: 0.1 },
97
+ bottomRight: { y: 0.9 }, // Missing x
98
+ },
99
+ {
100
+ topLeft: { x: 0.1, y: 0.1 },
101
+ bottomRight: { x: 0.9, y: 0.9 },
102
+ isCircle: 'not-boolean', // Invalid isCircle type
103
+ },
104
+ null,
105
+ undefined,
106
+ 'not-an-object',
107
+ 123,
108
+ [],
109
+ ]
110
+
111
+ invalidCrops.forEach((crop) => {
112
+ expect(() => ImageShapeCrop.validate(crop)).toThrow()
113
+ })
114
+ })
115
+ })
116
+
117
+ describe('imageShapeMigrations - AddUrlProp migration', () => {
118
+ const { up, down } = getTestMigration(imageShapeVersions.AddUrlProp)
119
+
120
+ describe('AddUrlProp up migration', () => {
121
+ it('should add url property with empty string default', () => {
122
+ const oldRecord = {
123
+ id: 'shape:image1',
124
+ typeName: 'shape',
125
+ type: 'image',
126
+ x: 100,
127
+ y: 200,
128
+ rotation: 0,
129
+ index: 'a1',
130
+ parentId: 'page:main',
131
+ isLocked: false,
132
+ opacity: 1,
133
+ props: {
134
+ w: 400,
135
+ h: 300,
136
+ playing: true,
137
+ assetId: 'asset:image123',
138
+ },
139
+ meta: {},
140
+ }
141
+
142
+ const result = up(oldRecord)
143
+ expect(result.props.url).toBe('')
144
+ expect(result.props.w).toBe(400) // Preserve other props
145
+ expect(result.props.playing).toBe(true)
146
+ })
147
+
148
+ it('should preserve all existing properties during migration', () => {
149
+ const oldRecord = {
150
+ id: 'shape:image2',
151
+ props: {
152
+ w: 500,
153
+ h: 400,
154
+ playing: false,
155
+ assetId: null,
156
+ },
157
+ }
158
+
159
+ const result = up(oldRecord)
160
+ expect(result.props.url).toBe('')
161
+ expect(result.props.w).toBe(500)
162
+ expect(result.props.h).toBe(400)
163
+ expect(result.props.playing).toBe(false)
164
+ expect(result.props.assetId).toBeNull()
165
+ })
166
+ })
167
+
168
+ describe('AddUrlProp down migration', () => {
169
+ it('should be retired (no down migration)', () => {
170
+ expect(() => {
171
+ down({})
172
+ }).toThrow('Migration com.tldraw.shape.image/1 does not have a down function')
173
+ })
174
+ })
175
+ })
176
+
177
+ describe('imageShapeMigrations - AddCropProp migration', () => {
178
+ const { up, down } = getTestMigration(imageShapeVersions.AddCropProp)
179
+
180
+ describe('AddCropProp up migration', () => {
181
+ it('should add crop property with null default', () => {
182
+ const oldRecord = {
183
+ id: 'shape:image1',
184
+ props: {
185
+ w: 300,
186
+ h: 200,
187
+ playing: true,
188
+ url: 'https://example.com/image.jpg',
189
+ assetId: 'asset:image123',
190
+ },
191
+ }
192
+
193
+ const result = up(oldRecord)
194
+ expect(result.props.crop).toBeNull()
195
+ expect(result.props.w).toBe(300) // Preserve other props
196
+ expect(result.props.url).toBe('https://example.com/image.jpg')
197
+ })
198
+
199
+ it('should preserve all existing properties during migration', () => {
200
+ const oldRecord = {
201
+ id: 'shape:image2',
202
+ props: {
203
+ w: 400,
204
+ h: 300,
205
+ playing: false,
206
+ url: '',
207
+ assetId: null,
208
+ },
209
+ }
210
+
211
+ const result = up(oldRecord)
212
+ expect(result.props.crop).toBeNull()
213
+ expect(result.props.w).toBe(400)
214
+ expect(result.props.playing).toBe(false)
215
+ })
216
+ })
217
+
218
+ describe('AddCropProp down migration', () => {
219
+ it('should remove crop property', () => {
220
+ const newRecord = {
221
+ id: 'shape:image1',
222
+ props: {
223
+ w: 300,
224
+ h: 200,
225
+ playing: true,
226
+ url: 'https://example.com/image.jpg',
227
+ assetId: 'asset:image123',
228
+ crop: {
229
+ topLeft: { x: 0.1, y: 0.1 },
230
+ bottomRight: { x: 0.9, y: 0.9 },
231
+ },
232
+ },
233
+ }
234
+
235
+ const result = down(newRecord)
236
+ expect(result.props.crop).toBeUndefined()
237
+ expect(result.props.w).toBe(300) // Preserve other props
238
+ })
239
+ })
240
+ })
241
+
242
+ describe('imageShapeMigrations - MakeUrlsValid migration', () => {
243
+ const { up, down } = getTestMigration(imageShapeVersions.MakeUrlsValid)
244
+
245
+ describe('MakeUrlsValid up migration', () => {
246
+ it('should clear invalid URLs', () => {
247
+ const oldRecord = {
248
+ id: 'shape:image1',
249
+ props: {
250
+ w: 300,
251
+ h: 200,
252
+ playing: true,
253
+ url: 'invalid-url-format',
254
+ assetId: 'asset:image123',
255
+ crop: null,
256
+ },
257
+ }
258
+
259
+ const result = up(oldRecord)
260
+ expect(result.props.url).toBe('')
261
+ expect(result.props.w).toBe(300) // Preserve other props
262
+ })
263
+
264
+ it('should preserve valid URLs', () => {
265
+ const validUrls = [
266
+ '',
267
+ 'https://example.com/image.jpg',
268
+ 'http://test.com/photo.png',
269
+ 'https://subdomain.example.com/path/image.gif',
270
+ ]
271
+
272
+ validUrls.forEach((url) => {
273
+ const oldRecord = {
274
+ id: 'shape:image1',
275
+ props: {
276
+ w: 300,
277
+ h: 200,
278
+ playing: true,
279
+ url,
280
+ assetId: 'asset:image123',
281
+ crop: null,
282
+ },
283
+ }
284
+
285
+ const result = up(oldRecord)
286
+ expect(result.props.url).toBe(url)
287
+ })
288
+ })
289
+
290
+ it('should preserve all other properties during migration', () => {
291
+ const oldRecord = {
292
+ id: 'shape:image1',
293
+ props: {
294
+ w: 400,
295
+ h: 300,
296
+ playing: false,
297
+ url: 'not-valid-url',
298
+ assetId: null,
299
+ crop: {
300
+ topLeft: { x: 0.2, y: 0.2 },
301
+ bottomRight: { x: 0.8, y: 0.8 },
302
+ },
303
+ },
304
+ }
305
+
306
+ const result = up(oldRecord)
307
+ expect(result.props.url).toBe('')
308
+ expect(result.props.w).toBe(400)
309
+ expect(result.props.playing).toBe(false)
310
+ expect(result.props.crop).toEqual({
311
+ topLeft: { x: 0.2, y: 0.2 },
312
+ bottomRight: { x: 0.8, y: 0.8 },
313
+ })
314
+ })
315
+ })
316
+
317
+ describe('MakeUrlsValid down migration', () => {
318
+ it('should be a no-op migration', () => {
319
+ const newRecord = {
320
+ id: 'shape:image1',
321
+ props: {
322
+ w: 300,
323
+ h: 200,
324
+ playing: true,
325
+ url: 'https://example.com/image.jpg',
326
+ assetId: 'asset:image123',
327
+ crop: null,
328
+ },
329
+ }
330
+
331
+ const result = down(newRecord)
332
+ expect(result).toEqual(newRecord)
333
+ })
334
+ })
335
+ })
336
+
337
+ describe('imageShapeMigrations - AddFlipProps migration', () => {
338
+ const { up, down } = getTestMigration(imageShapeVersions.AddFlipProps)
339
+
340
+ describe('AddFlipProps up migration', () => {
341
+ it('should add flipX and flipY properties with false defaults', () => {
342
+ const oldRecord = {
343
+ id: 'shape:image1',
344
+ props: {
345
+ w: 300,
346
+ h: 200,
347
+ playing: true,
348
+ url: 'https://example.com/image.jpg',
349
+ assetId: 'asset:image123',
350
+ crop: null,
351
+ },
352
+ }
353
+
354
+ const result = up(oldRecord)
355
+ expect(result.props.flipX).toBe(false)
356
+ expect(result.props.flipY).toBe(false)
357
+ expect(result.props.w).toBe(300) // Preserve other props
358
+ })
359
+
360
+ it('should preserve all existing properties during migration', () => {
361
+ const oldRecord = {
362
+ id: 'shape:image2',
363
+ props: {
364
+ w: 400,
365
+ h: 300,
366
+ playing: false,
367
+ url: '',
368
+ assetId: null,
369
+ crop: {
370
+ topLeft: { x: 0, y: 0 },
371
+ bottomRight: { x: 1, y: 1 },
372
+ isCircle: true,
373
+ },
374
+ },
375
+ }
376
+
377
+ const result = up(oldRecord)
378
+ expect(result.props.flipX).toBe(false)
379
+ expect(result.props.flipY).toBe(false)
380
+ expect(result.props.w).toBe(400)
381
+ expect(result.props.crop?.isCircle).toBe(true)
382
+ })
383
+ })
384
+
385
+ describe('AddFlipProps down migration', () => {
386
+ it('should remove flipX and flipY properties', () => {
387
+ const newRecord = {
388
+ id: 'shape:image1',
389
+ props: {
390
+ w: 300,
391
+ h: 200,
392
+ playing: true,
393
+ url: 'https://example.com/image.jpg',
394
+ assetId: 'asset:image123',
395
+ crop: null,
396
+ flipX: true,
397
+ flipY: false,
398
+ },
399
+ }
400
+
401
+ const result = down(newRecord)
402
+ expect(result.props.flipX).toBeUndefined()
403
+ expect(result.props.flipY).toBeUndefined()
404
+ expect(result.props.w).toBe(300) // Preserve other props
405
+ })
406
+ })
407
+ })
408
+
409
+ describe('imageShapeMigrations - AddAltText migration', () => {
410
+ const { up, down } = getTestMigration(imageShapeVersions.AddAltText)
411
+
412
+ describe('AddAltText up migration', () => {
413
+ it('should add altText property with empty string default', () => {
414
+ const oldRecord = {
415
+ id: 'shape:image1',
416
+ props: {
417
+ w: 300,
418
+ h: 200,
419
+ playing: true,
420
+ url: 'https://example.com/image.jpg',
421
+ assetId: 'asset:image123',
422
+ crop: null,
423
+ flipX: false,
424
+ flipY: true,
425
+ },
426
+ }
427
+
428
+ const result = up(oldRecord)
429
+ expect(result.props.altText).toBe('')
430
+ expect(result.props.flipY).toBe(true) // Preserve other props
431
+ })
432
+
433
+ it('should preserve all existing properties during migration', () => {
434
+ const oldRecord = {
435
+ id: 'shape:image2',
436
+ props: {
437
+ w: 500,
438
+ h: 400,
439
+ playing: false,
440
+ url: '',
441
+ assetId: null,
442
+ crop: {
443
+ topLeft: { x: 0.25, y: 0.25 },
444
+ bottomRight: { x: 0.75, y: 0.75 },
445
+ },
446
+ flipX: true,
447
+ flipY: false,
448
+ },
449
+ }
450
+
451
+ const result = up(oldRecord)
452
+ expect(result.props.altText).toBe('')
453
+ expect(result.props.flipX).toBe(true)
454
+ expect(result.props.crop).toEqual({
455
+ topLeft: { x: 0.25, y: 0.25 },
456
+ bottomRight: { x: 0.75, y: 0.75 },
457
+ })
458
+ })
459
+ })
460
+
461
+ describe('AddAltText down migration', () => {
462
+ it('should remove altText property', () => {
463
+ const newRecord = {
464
+ id: 'shape:image1',
465
+ props: {
466
+ w: 300,
467
+ h: 200,
468
+ playing: true,
469
+ url: 'https://example.com/image.jpg',
470
+ assetId: 'asset:image123',
471
+ crop: null,
472
+ flipX: false,
473
+ flipY: false,
474
+ altText: 'Sample image description',
475
+ },
476
+ }
477
+
478
+ const result = down(newRecord)
479
+ expect(result.props.altText).toBeUndefined()
480
+ expect(result.props.flipX).toBe(false) // Preserve other props
481
+ })
482
+ })
483
+ })
484
+
485
+ describe('integration tests', () => {
486
+ it('should work with complete image shape record validation', () => {
487
+ const completeValidator = T.object({
488
+ id: T.string,
489
+ typeName: T.literal('shape'),
490
+ type: T.literal('image'),
491
+ x: T.number,
492
+ y: T.number,
493
+ rotation: T.number,
494
+ index: T.string,
495
+ parentId: T.string,
496
+ isLocked: T.boolean,
497
+ opacity: T.number,
498
+ props: T.object(imageShapeProps),
499
+ meta: T.jsonValue,
500
+ })
501
+
502
+ const validImageShape = {
503
+ id: 'shape:image123',
504
+ typeName: 'shape' as const,
505
+ type: 'image' as const,
506
+ x: 100,
507
+ y: 200,
508
+ rotation: 0.5,
509
+ index: 'a1',
510
+ parentId: 'page:main',
511
+ isLocked: false,
512
+ opacity: 0.8,
513
+ props: {
514
+ w: 400,
515
+ h: 300,
516
+ playing: true,
517
+ url: 'https://example.com/image.jpg',
518
+ assetId: 'asset:image123' as TLAssetId,
519
+ crop: {
520
+ topLeft: { x: 0.1, y: 0.1 },
521
+ bottomRight: { x: 0.9, y: 0.9 },
522
+ isCircle: false,
523
+ },
524
+ flipX: false,
525
+ flipY: true,
526
+ altText: 'Sample image',
527
+ },
528
+ meta: { custom: 'data' },
529
+ }
530
+
531
+ expect(() => completeValidator.validate(validImageShape)).not.toThrow()
532
+ })
533
+ })
534
+ })
@@ -7,30 +7,118 @@ import { RecordProps } from '../recordsWithProps'
7
7
  import { TLShapeCrop } from './ShapeWithCrop'
8
8
  import { TLBaseShape } from './TLBaseShape'
9
9
 
10
- /** @public */
10
+ /**
11
+ * Validator for image shape crop data. Defines the structure for cropping an image,
12
+ * specifying the visible region within the original image bounds.
13
+ *
14
+ * @public
15
+ * @example
16
+ * ```ts
17
+ * const cropData: TLShapeCrop = {
18
+ * topLeft: { x: 0.1, y: 0.1 },
19
+ * bottomRight: { x: 0.9, y: 0.9 },
20
+ * isCircle: false
21
+ * }
22
+ *
23
+ * const isValid = ImageShapeCrop.isValid(cropData)
24
+ * ```
25
+ */
11
26
  export const ImageShapeCrop: T.ObjectValidator<TLShapeCrop> = T.object({
12
27
  topLeft: vecModelValidator,
13
28
  bottomRight: vecModelValidator,
14
29
  isCircle: T.boolean.optional(),
15
30
  })
16
31
 
17
- /** @public */
32
+ /**
33
+ * Properties for an image shape. Image shapes display raster images on the canvas,
34
+ * with support for cropping, flipping, and asset management.
35
+ *
36
+ * @public
37
+ * @example
38
+ * ```ts
39
+ * const imageProps: TLImageShapeProps = {
40
+ * w: 300,
41
+ * h: 200,
42
+ * playing: true,
43
+ * url: 'https://example.com/image.jpg',
44
+ * assetId: 'asset:image123',
45
+ * crop: null,
46
+ * flipX: false,
47
+ * flipY: false,
48
+ * altText: 'A sample image'
49
+ * }
50
+ * ```
51
+ */
18
52
  export interface TLImageShapeProps {
53
+ /** Width of the image shape in canvas units */
19
54
  w: number
55
+ /** Height of the image shape in canvas units */
20
56
  h: number
57
+ /** Whether animated images (like GIFs) should play */
21
58
  playing: boolean
59
+ /** URL of the image resource */
22
60
  url: string
61
+ /** ID of the associated asset record, null if no asset */
23
62
  assetId: TLAssetId | null
63
+ /** Crop data defining visible region of the image, null for no cropping */
24
64
  crop: TLShapeCrop | null
65
+ /** Whether to flip the image horizontally */
25
66
  flipX: boolean
67
+ /** Whether to flip the image vertically */
26
68
  flipY: boolean
69
+ /** Alternative text for accessibility and when image fails to load */
27
70
  altText: string
28
71
  }
29
72
 
30
- /** @public */
73
+ /**
74
+ * An image shape representing a raster image on the canvas. Image shapes can display
75
+ * various image formats and support features like cropping, flipping, and asset management.
76
+ *
77
+ * @public
78
+ * @example
79
+ * ```ts
80
+ * const imageShape: TLImageShape = {
81
+ * id: 'shape:image1',
82
+ * type: 'image',
83
+ * x: 100,
84
+ * y: 100,
85
+ * rotation: 0,
86
+ * index: 'a1',
87
+ * parentId: 'page:main',
88
+ * isLocked: false,
89
+ * opacity: 1,
90
+ * props: {
91
+ * w: 400,
92
+ * h: 300,
93
+ * playing: true,
94
+ * url: '',
95
+ * assetId: 'asset:photo1',
96
+ * crop: null,
97
+ * flipX: false,
98
+ * flipY: false,
99
+ * altText: 'Sample photo'
100
+ * },
101
+ * meta: {},
102
+ * typeName: 'shape'
103
+ * }
104
+ * ```
105
+ */
31
106
  export type TLImageShape = TLBaseShape<'image', TLImageShapeProps>
32
107
 
33
- /** @public */
108
+ /**
109
+ * Validation schema for image shape properties. Defines the runtime validation rules
110
+ * for all properties of image shapes, ensuring data integrity and type safety.
111
+ *
112
+ * @public
113
+ * @example
114
+ * ```ts
115
+ * import { imageShapeProps } from '@tldraw/tlschema'
116
+ *
117
+ * // Used internally by the validation system
118
+ * const validator = T.object(imageShapeProps)
119
+ * const validatedProps = validator.validate(someImageProps)
120
+ * ```
121
+ */
34
122
  export const imageShapeProps: RecordProps<TLImageShape> = {
35
123
  w: T.nonZeroNumber,
36
124
  h: T.nonZeroNumber,
@@ -51,9 +139,21 @@ const Versions = createShapePropsMigrationIds('image', {
51
139
  AddAltText: 5,
52
140
  })
53
141
 
142
+ /**
143
+ * Version identifiers for image shape migrations. These version numbers track
144
+ * schema changes over time to enable proper data migration between versions.
145
+ *
146
+ * @public
147
+ */
54
148
  export { Versions as imageShapeVersions }
55
149
 
56
- /** @public */
150
+ /**
151
+ * Migration sequence for image shapes. Handles schema evolution over time by defining
152
+ * how to upgrade and downgrade image shape data between different versions. Includes
153
+ * migrations for URL properties, crop functionality, flip properties, and accessibility features.
154
+ *
155
+ * @public
156
+ */
57
157
  export const imageShapeMigrations = createShapePropsMigrationSequence({
58
158
  sequence: [
59
159
  {