@tldraw/tlschema 4.1.0-next.b6dfe9bccde9 → 4.1.0-next.b9999db71010

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 +4416 -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 +4416 -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 +86 -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
@@ -5,18 +5,62 @@ import { createShapePropsMigrationIds, createShapePropsMigrationSequence } from
5
5
  import { RecordProps } from '../recordsWithProps'
6
6
  import { TLBaseShape } from './TLBaseShape'
7
7
 
8
- /** @public */
8
+ /**
9
+ * Properties for the bookmark shape, which displays website bookmarks as interactive cards.
10
+ *
11
+ * @public
12
+ */
9
13
  export interface TLBookmarkShapeProps {
14
+ /** Width of the bookmark shape in pixels */
10
15
  w: number
16
+ /** Height of the bookmark shape in pixels */
11
17
  h: number
18
+ /** Asset ID for the bookmark's preview image, or null if no image is available */
12
19
  assetId: TLAssetId | null
20
+ /** The URL that this bookmark points to */
13
21
  url: string
14
22
  }
15
23
 
16
- /** @public */
24
+ /**
25
+ * A bookmark shape represents a website link with optional preview content.
26
+ * Bookmark shapes display as cards showing the page title, description, and preview image.
27
+ *
28
+ * @public
29
+ * @example
30
+ * ```ts
31
+ * const bookmarkShape: TLBookmarkShape = {
32
+ * id: createShapeId(),
33
+ * typeName: 'shape',
34
+ * type: 'bookmark',
35
+ * x: 100,
36
+ * y: 100,
37
+ * rotation: 0,
38
+ * index: 'a1',
39
+ * parentId: 'page:page1',
40
+ * isLocked: false,
41
+ * opacity: 1,
42
+ * props: {
43
+ * w: 300,
44
+ * h: 320,
45
+ * assetId: 'asset:bookmark123',
46
+ * url: 'https://www.example.com'
47
+ * },
48
+ * meta: {}
49
+ * }
50
+ * ```
51
+ */
17
52
  export type TLBookmarkShape = TLBaseShape<'bookmark', TLBookmarkShapeProps>
18
53
 
19
- /** @public */
54
+ /**
55
+ * Validation schema for bookmark shape properties.
56
+ *
57
+ * @public
58
+ * @example
59
+ * ```ts
60
+ * // Validates bookmark shape properties
61
+ * const isValid = bookmarkShapeProps.url.isValid('https://example.com')
62
+ * ```
63
+ */
20
64
  export const bookmarkShapeProps: RecordProps<TLBookmarkShape> = {
21
65
  w: T.nonZeroNumber,
22
66
  h: T.nonZeroNumber,
@@ -29,9 +73,19 @@ const Versions = createShapePropsMigrationIds('bookmark', {
29
73
  MakeUrlsValid: 2,
30
74
  })
31
75
 
76
+ /**
77
+ * Version identifiers for bookmark shape migrations.
78
+ *
79
+ * @public
80
+ */
32
81
  export { Versions as bookmarkShapeVersions }
33
82
 
34
- /** @public */
83
+ /**
84
+ * Migration sequence for bookmark shape properties across different schema versions.
85
+ * Handles backwards compatibility when bookmark shape structure changes.
86
+ *
87
+ * @public
88
+ */
35
89
  export const bookmarkShapeMigrations = createShapePropsMigrationSequence({
36
90
  sequence: [
37
91
  {
@@ -0,0 +1,177 @@
1
+ import { T } from '@tldraw/validate'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { getTestMigration } from '../__tests__/migrationTestUtils'
4
+ import { VecModel } from '../misc/geometry-types'
5
+ import { DefaultColorStyle } from '../styles/TLColorStyle'
6
+ import { DefaultDashStyle } from '../styles/TLDashStyle'
7
+ import { DefaultFillStyle } from '../styles/TLFillStyle'
8
+ import { DefaultSizeStyle } from '../styles/TLSizeStyle'
9
+ import { DrawShapeSegment, drawShapeProps, drawShapeVersions } from './TLDrawShape'
10
+
11
+ describe('TLDrawShape', () => {
12
+ describe('DrawShapeSegment validator', () => {
13
+ it('should validate valid segment structures', () => {
14
+ const validSegments = [
15
+ { type: 'free', points: [{ x: 0, y: 0 }] },
16
+ { type: 'straight', points: [{ x: 0, y: 0, z: 0.5 }] },
17
+ { type: 'free', points: [] },
18
+ ]
19
+
20
+ validSegments.forEach((segment) => {
21
+ expect(() => DrawShapeSegment.validate(segment)).not.toThrow()
22
+ })
23
+ })
24
+
25
+ it('should reject invalid segment types and points', () => {
26
+ const invalidSegments = [
27
+ { type: 'invalid', points: [{ x: 0, y: 0 }] },
28
+ { type: 'free', points: [{ x: 'invalid', y: 0 }] },
29
+ { type: 'free', points: 'not-array' },
30
+ {}, // Missing required fields
31
+ ]
32
+
33
+ invalidSegments.forEach((segment) => {
34
+ expect(() => DrawShapeSegment.validate(segment)).toThrow()
35
+ })
36
+ })
37
+ })
38
+
39
+ describe('drawShapeProps validation schema', () => {
40
+ it('should validate complete valid props object', () => {
41
+ const fullValidator = T.object(drawShapeProps)
42
+
43
+ const validProps = {
44
+ color: 'red' as const,
45
+ fill: 'solid' as const,
46
+ dash: 'dashed' as const,
47
+ size: 'l' as const,
48
+ segments: [{ type: 'free' as const, points: [{ x: 0, y: 0, z: 0.5 }] as VecModel[] }],
49
+ isComplete: true,
50
+ isClosed: true,
51
+ isPen: true,
52
+ scale: 1.5,
53
+ }
54
+
55
+ expect(() => fullValidator.validate(validProps)).not.toThrow()
56
+ })
57
+
58
+ it('should reject invalid property values', () => {
59
+ // Test key invalid cases that matter for business logic
60
+ expect(() => drawShapeProps.scale.validate(0)).toThrow() // zero scale invalid
61
+ expect(() => drawShapeProps.scale.validate(-1)).toThrow() // negative scale invalid
62
+ expect(() => drawShapeProps.segments.validate('not-array')).toThrow()
63
+ expect(() => drawShapeProps.segments.validate([{ type: 'invalid' }])).toThrow()
64
+ })
65
+
66
+ it('should use correct default style validators', () => {
67
+ expect(drawShapeProps.color).toBe(DefaultColorStyle)
68
+ expect(drawShapeProps.fill).toBe(DefaultFillStyle)
69
+ expect(drawShapeProps.dash).toBe(DefaultDashStyle)
70
+ expect(drawShapeProps.size).toBe(DefaultSizeStyle)
71
+ })
72
+ })
73
+
74
+ describe('AddInPen migration', () => {
75
+ const { up } = getTestMigration(drawShapeVersions.AddInPen)
76
+
77
+ it('should detect pen from non-standard pressure values', () => {
78
+ const recordWithPen = {
79
+ props: {
80
+ segments: [
81
+ {
82
+ type: 'free',
83
+ points: [
84
+ { x: 0, y: 0, z: 0.3 }, // Non-standard pressure
85
+ { x: 10, y: 10, z: 0.7 }, // Non-standard pressure
86
+ ],
87
+ },
88
+ ],
89
+ },
90
+ }
91
+
92
+ const result = up(recordWithPen)
93
+ expect(result.props.isPen).toBe(true)
94
+ })
95
+
96
+ it('should not detect pen from standard pressure values', () => {
97
+ const recordWithoutPen = {
98
+ props: {
99
+ segments: [
100
+ {
101
+ type: 'free',
102
+ points: [
103
+ { x: 0, y: 0, z: 0 }, // Standard mouse pressure
104
+ { x: 10, y: 10, z: 0.5 }, // Standard touch pressure
105
+ ],
106
+ },
107
+ ],
108
+ },
109
+ }
110
+
111
+ const result = up(recordWithoutPen)
112
+ expect(result.props.isPen).toBe(false)
113
+ })
114
+
115
+ it('should handle empty segments', () => {
116
+ const recordEmpty = {
117
+ props: {
118
+ segments: [{ type: 'free', points: [] }],
119
+ },
120
+ }
121
+
122
+ const result = up(recordEmpty)
123
+ expect(result.props.isPen).toBe(false)
124
+ })
125
+
126
+ it('should require both points to have non-standard pressure', () => {
127
+ const recordMixed = {
128
+ props: {
129
+ segments: [
130
+ {
131
+ type: 'free',
132
+ points: [
133
+ { x: 0, y: 0, z: 0.3 }, // Non-standard
134
+ { x: 10, y: 10, z: 0.5 }, // Standard
135
+ ],
136
+ },
137
+ ],
138
+ },
139
+ }
140
+
141
+ const result = up(recordMixed)
142
+ expect(result.props.isPen).toBe(false)
143
+ })
144
+ })
145
+
146
+ describe('AddScale migration', () => {
147
+ const { up, down } = getTestMigration(drawShapeVersions.AddScale)
148
+
149
+ it('should add scale property with default value 1', () => {
150
+ const oldRecord = {
151
+ props: {
152
+ color: 'blue',
153
+ segments: [{ type: 'free', points: [{ x: 0, y: 0 }] }],
154
+ isPen: false,
155
+ },
156
+ }
157
+
158
+ const result = up(oldRecord)
159
+ expect(result.props.scale).toBe(1)
160
+ })
161
+
162
+ it('should remove scale property on down migration', () => {
163
+ const newRecord = {
164
+ props: {
165
+ color: 'blue',
166
+ segments: [{ type: 'free', points: [{ x: 0, y: 0 }] }],
167
+ isPen: false,
168
+ scale: 1.5,
169
+ },
170
+ }
171
+
172
+ const result = down(newRecord)
173
+ expect(result.props.scale).toBeUndefined()
174
+ expect(result.props.color).toBe('blue') // Other props preserved
175
+ })
176
+ })
177
+ })
@@ -8,35 +8,116 @@ import { DefaultFillStyle, TLDefaultFillStyle } from '../styles/TLFillStyle'
8
8
  import { DefaultSizeStyle, TLDefaultSizeStyle } from '../styles/TLSizeStyle'
9
9
  import { TLBaseShape } from './TLBaseShape'
10
10
 
11
- /** @public */
11
+ /**
12
+ * A segment of a draw shape representing either freehand drawing or straight line segments.
13
+ *
14
+ * @public
15
+ */
12
16
  export interface TLDrawShapeSegment {
17
+ /** Type of drawing segment - 'free' for freehand curves, 'straight' for line segments */
13
18
  type: 'free' | 'straight'
19
+ /** Array of points defining the segment path with x, y coordinates and pressure (z) */
14
20
  points: VecModel[]
15
21
  }
16
22
 
17
- /** @public */
23
+ /**
24
+ * Validator for draw shape segments ensuring proper structure and data types.
25
+ *
26
+ * @public
27
+ * @example
28
+ * ```ts
29
+ * const segment: TLDrawShapeSegment = {
30
+ * type: 'free',
31
+ * points: [{ x: 0, y: 0, z: 0.5 }, { x: 10, y: 10, z: 0.7 }]
32
+ * }
33
+ * const isValid = DrawShapeSegment.isValid(segment)
34
+ * ```
35
+ */
18
36
  export const DrawShapeSegment: T.ObjectValidator<TLDrawShapeSegment> = T.object({
19
37
  type: T.literalEnum('free', 'straight'),
20
38
  points: T.arrayOf(vecModelValidator),
21
39
  })
22
40
 
23
- /** @public */
41
+ /**
42
+ * Properties for the draw shape, which represents freehand drawing and sketching.
43
+ *
44
+ * @public
45
+ */
24
46
  export interface TLDrawShapeProps {
47
+ /** Color style for the drawing stroke */
25
48
  color: TLDefaultColorStyle
49
+ /** Fill style for closed drawing shapes */
26
50
  fill: TLDefaultFillStyle
51
+ /** Dash pattern style for the stroke */
27
52
  dash: TLDefaultDashStyle
53
+ /** Size/thickness of the drawing stroke */
28
54
  size: TLDefaultSizeStyle
55
+ /** Array of segments that make up the complete drawing path */
29
56
  segments: TLDrawShapeSegment[]
57
+ /** Whether the drawing is complete (user finished drawing) */
30
58
  isComplete: boolean
59
+ /** Whether the drawing path forms a closed shape */
31
60
  isClosed: boolean
61
+ /** Whether this drawing was created with a pen/stylus device */
32
62
  isPen: boolean
63
+ /** Scale factor applied to the drawing */
33
64
  scale: number
34
65
  }
35
66
 
36
- /** @public */
67
+ /**
68
+ * A draw shape represents freehand drawing, sketching, and pen input on the canvas.
69
+ * Draw shapes are composed of segments that can be either smooth curves or straight lines.
70
+ *
71
+ * @public
72
+ * @example
73
+ * ```ts
74
+ * const drawShape: TLDrawShape = {
75
+ * id: createShapeId(),
76
+ * typeName: 'shape',
77
+ * type: 'draw',
78
+ * x: 50,
79
+ * y: 50,
80
+ * rotation: 0,
81
+ * index: 'a1',
82
+ * parentId: 'page:page1',
83
+ * isLocked: false,
84
+ * opacity: 1,
85
+ * props: {
86
+ * color: 'black',
87
+ * fill: 'none',
88
+ * dash: 'solid',
89
+ * size: 'm',
90
+ * segments: [{
91
+ * type: 'free',
92
+ * points: [{ x: 0, y: 0, z: 0.5 }, { x: 20, y: 15, z: 0.6 }]
93
+ * }],
94
+ * isComplete: true,
95
+ * isClosed: false,
96
+ * isPen: false,
97
+ * scale: 1
98
+ * },
99
+ * meta: {}
100
+ * }
101
+ * ```
102
+ */
37
103
  export type TLDrawShape = TLBaseShape<'draw', TLDrawShapeProps>
38
104
 
39
- /** @public */
105
+ /**
106
+ * Validation schema for draw shape properties.
107
+ *
108
+ * @public
109
+ * @example
110
+ * ```ts
111
+ * // Validate draw shape properties
112
+ * const props = {
113
+ * color: 'red',
114
+ * fill: 'solid',
115
+ * segments: [{ type: 'free', points: [] }],
116
+ * isComplete: true
117
+ * }
118
+ * const isValid = drawShapeProps.color.isValid(props.color)
119
+ * ```
120
+ */
40
121
  export const drawShapeProps: RecordProps<TLDrawShape> = {
41
122
  color: DefaultColorStyle,
42
123
  fill: DefaultFillStyle,
@@ -54,9 +135,19 @@ const Versions = createShapePropsMigrationIds('draw', {
54
135
  AddScale: 2,
55
136
  })
56
137
 
138
+ /**
139
+ * Version identifiers for draw shape migrations.
140
+ *
141
+ * @public
142
+ */
57
143
  export { Versions as drawShapeVersions }
58
144
 
59
- /** @public */
145
+ /**
146
+ * Migration sequence for draw shape properties across different schema versions.
147
+ * Handles adding pen detection and scale properties to existing draw shapes.
148
+ *
149
+ * @public
150
+ */
60
151
  export const drawShapeMigrations = createShapePropsMigrationSequence({
61
152
  sequence: [
62
153
  {
@@ -0,0 +1,286 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { getTestMigration } from '../__tests__/migrationTestUtils'
3
+ import { embedShapeProps, embedShapeVersions } from './TLEmbedShape'
4
+
5
+ describe('TLEmbedShape', () => {
6
+ describe('embedShapeProps validation schema', () => {
7
+ it('should validate width as nonZeroNumber', () => {
8
+ const validWidths = [0.1, 0.5, 1, 10, 100, 560, 1920, 1000.5, 9999.99]
9
+
10
+ validWidths.forEach((w) => {
11
+ expect(() => embedShapeProps.w.validate(w)).not.toThrow()
12
+ })
13
+
14
+ const invalidWidths = [0, -1, -10, -0.1, 'not-number', null, undefined, {}, [], true, false]
15
+
16
+ invalidWidths.forEach((w) => {
17
+ expect(() => embedShapeProps.w.validate(w)).toThrow()
18
+ })
19
+ })
20
+
21
+ it('should validate height as nonZeroNumber', () => {
22
+ const validHeights = [0.1, 0.5, 1, 10, 100, 315, 1080, 1000.5, 9999.99]
23
+
24
+ validHeights.forEach((h) => {
25
+ expect(() => embedShapeProps.h.validate(h)).not.toThrow()
26
+ })
27
+
28
+ const invalidHeights = [0, -1, -10, -0.1, 'not-number', null, undefined, {}, [], true, false]
29
+
30
+ invalidHeights.forEach((h) => {
31
+ expect(() => embedShapeProps.h.validate(h)).toThrow()
32
+ })
33
+ })
34
+
35
+ it('should validate url as string', () => {
36
+ const validUrls = [
37
+ '',
38
+ 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
39
+ 'https://codepen.io/team/codepen/pen/PNaGbb',
40
+ 'https://codesandbox.io/s/new',
41
+ 'https://vimeo.com/123456789',
42
+ 'https://tldraw.com/r/room123',
43
+ 'invalid-url-format', // Still valid as string
44
+ 'javascript:alert("test")', // Still valid as string
45
+ 'file:///local/file', // Still valid as string
46
+ 'relative/path',
47
+ 'text without protocol',
48
+ ]
49
+
50
+ validUrls.forEach((url) => {
51
+ expect(() => embedShapeProps.url.validate(url)).not.toThrow()
52
+ })
53
+
54
+ const invalidUrls = [123, null, undefined, {}, [], true, false]
55
+
56
+ invalidUrls.forEach((url) => {
57
+ expect(() => embedShapeProps.url.validate(url)).toThrow()
58
+ })
59
+ })
60
+ })
61
+
62
+ describe('embedShapeMigrations - GenOriginalUrlInEmbed migration', () => {
63
+ const { up, down } = getTestMigration(embedShapeVersions.GenOriginalUrlInEmbed)
64
+
65
+ describe('GenOriginalUrlInEmbed up migration', () => {
66
+ it('should extract original URL from tldraw embed URLs', () => {
67
+ const tldrawUrls = [
68
+ 'https://tldraw.com/r/room123',
69
+ 'https://beta.tldraw.com/r/room456',
70
+ 'http://localhost:3000/r/local-room',
71
+ ]
72
+
73
+ tldrawUrls.forEach((url) => {
74
+ const oldRecord = {
75
+ id: 'shape:embed1',
76
+ props: {
77
+ w: 560,
78
+ h: 315,
79
+ url,
80
+ },
81
+ }
82
+
83
+ const result = up(oldRecord)
84
+ expect(result.props.url).toBe(url) // Should keep the URL as-is for tldraw
85
+ expect(result.props.tmpOldUrl).toBe(url)
86
+ })
87
+ })
88
+
89
+ it('should extract original URL from YouTube embed URLs', () => {
90
+ const testCases = [
91
+ {
92
+ embed: 'https://www.youtube.com/embed/dQw4w9WgXcQ',
93
+ expected: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
94
+ },
95
+ {
96
+ embed: 'https://youtube.com/embed/abc123',
97
+ expected: 'https://www.youtube.com/watch?v=abc123',
98
+ },
99
+ ]
100
+
101
+ testCases.forEach(({ embed, expected }) => {
102
+ const oldRecord = {
103
+ id: 'shape:embed1',
104
+ props: {
105
+ w: 560,
106
+ h: 315,
107
+ url: embed,
108
+ },
109
+ }
110
+
111
+ const result = up(oldRecord)
112
+ expect(result.props.url).toBe(expected)
113
+ expect(result.props.tmpOldUrl).toBe(embed)
114
+ })
115
+ })
116
+
117
+ it('should extract original URL from CodePen embed URLs', () => {
118
+ const oldRecord = {
119
+ id: 'shape:embed1',
120
+ props: {
121
+ w: 560,
122
+ h: 315,
123
+ url: 'https://codepen.io/user/embed/abcdef',
124
+ },
125
+ }
126
+
127
+ const result = up(oldRecord)
128
+ expect(result.props.url).toBe('https://codepen.io/user/pen/abcdef')
129
+ expect(result.props.tmpOldUrl).toBe('https://codepen.io/user/embed/abcdef')
130
+ })
131
+
132
+ it('should handle Google Maps embed URLs (documents hostname matching limitation)', () => {
133
+ const oldRecord = {
134
+ id: 'shape:embed1',
135
+ props: {
136
+ w: 560,
137
+ h: 315,
138
+ url: 'https://www.google.com/maps/embed/v1/view?center=40.7128,-74.0060&zoom=10',
139
+ },
140
+ }
141
+
142
+ const result = up(oldRecord)
143
+ // NOTE: The wildcard 'google.*' doesn't match 'google.com' due to exact string matching
144
+ // The URL is valid and parseable, so it goes through normal flow but doesn't match any hostname
145
+ expect(result.props.url).toBe('') // originalUrl is undefined, so becomes empty string
146
+ expect(result.props.tmpOldUrl).toBe(
147
+ 'https://www.google.com/maps/embed/v1/view?center=40.7128,-74.0060&zoom=10'
148
+ )
149
+ })
150
+
151
+ it('should extract original URL from Vimeo embed URLs', () => {
152
+ const oldRecord = {
153
+ id: 'shape:embed1',
154
+ props: {
155
+ w: 560,
156
+ h: 315,
157
+ url: 'https://player.vimeo.com/video/123456789',
158
+ },
159
+ }
160
+
161
+ const result = up(oldRecord)
162
+ expect(result.props.url).toBe('https://vimeo.com/123456789')
163
+ expect(result.props.tmpOldUrl).toBe('https://player.vimeo.com/video/123456789')
164
+ })
165
+ })
166
+
167
+ describe('GenOriginalUrlInEmbed down migration', () => {
168
+ it('should be retired (no down migration)', () => {
169
+ expect(() => {
170
+ down({})
171
+ }).toThrow('Migration com.tldraw.shape.embed/1 does not have a down function')
172
+ })
173
+ })
174
+ })
175
+
176
+ describe('embedShapeMigrations - RemoveDoesResize migration', () => {
177
+ const { up, down } = getTestMigration(embedShapeVersions.RemoveDoesResize)
178
+
179
+ describe('RemoveDoesResize up migration', () => {
180
+ it('should remove doesResize property', () => {
181
+ const oldRecord = {
182
+ id: 'shape:embed1',
183
+ props: {
184
+ w: 560,
185
+ h: 315,
186
+ url: 'https://example.com',
187
+ doesResize: true,
188
+ },
189
+ }
190
+
191
+ const result = up(oldRecord)
192
+ expect(result.props.doesResize).toBeUndefined()
193
+ })
194
+ })
195
+
196
+ describe('RemoveDoesResize down migration', () => {
197
+ it('should be retired (no down migration)', () => {
198
+ expect(() => {
199
+ down({})
200
+ }).toThrow('Migration com.tldraw.shape.embed/2 does not have a down function')
201
+ })
202
+ })
203
+ })
204
+
205
+ describe('embedShapeMigrations - RemoveTmpOldUrl migration', () => {
206
+ const { up, down } = getTestMigration(embedShapeVersions.RemoveTmpOldUrl)
207
+
208
+ describe('RemoveTmpOldUrl up migration', () => {
209
+ it('should remove tmpOldUrl property', () => {
210
+ const oldRecord = {
211
+ id: 'shape:embed1',
212
+ props: {
213
+ w: 560,
214
+ h: 315,
215
+ url: 'https://example.com',
216
+ tmpOldUrl: 'https://old-url.com',
217
+ },
218
+ }
219
+
220
+ const result = up(oldRecord)
221
+ expect(result.props.tmpOldUrl).toBeUndefined()
222
+ })
223
+ })
224
+
225
+ describe('RemoveTmpOldUrl down migration', () => {
226
+ it('should be retired (no down migration)', () => {
227
+ expect(() => {
228
+ down({})
229
+ }).toThrow('Migration com.tldraw.shape.embed/3 does not have a down function')
230
+ })
231
+ })
232
+ })
233
+
234
+ describe('embedShapeMigrations - RemovePermissionOverrides migration', () => {
235
+ const { up, down } = getTestMigration(embedShapeVersions.RemovePermissionOverrides)
236
+
237
+ describe('RemovePermissionOverrides up migration', () => {
238
+ it('should remove overridePermissions property', () => {
239
+ const oldRecord = {
240
+ id: 'shape:embed1',
241
+ props: {
242
+ w: 560,
243
+ h: 315,
244
+ url: 'https://example.com',
245
+ overridePermissions: { allowScripts: true },
246
+ },
247
+ }
248
+
249
+ const result = up(oldRecord)
250
+ expect(result.props.overridePermissions).toBeUndefined()
251
+ })
252
+ })
253
+
254
+ describe('RemovePermissionOverrides down migration', () => {
255
+ it('should be retired (no down migration)', () => {
256
+ expect(() => {
257
+ down({})
258
+ }).toThrow('Migration com.tldraw.shape.embed/4 does not have a down function')
259
+ })
260
+ })
261
+ })
262
+
263
+ describe('edge cases and error handling', () => {
264
+ it('should handle zero dimension validation correctly', () => {
265
+ // Zero should be invalid for width and height (nonZeroNumber)
266
+ expect(() => embedShapeProps.w.validate(0)).toThrow()
267
+ expect(() => embedShapeProps.h.validate(0)).toThrow()
268
+
269
+ // Negative numbers should also be invalid
270
+ expect(() => embedShapeProps.w.validate(-1)).toThrow()
271
+ expect(() => embedShapeProps.h.validate(-10.5)).toThrow()
272
+ })
273
+
274
+ it('should handle migration errors when props is null', () => {
275
+ const malformedRecord = {
276
+ id: 'shape:malformed',
277
+ props: null,
278
+ }
279
+
280
+ expect(() => {
281
+ const migration = getTestMigration(embedShapeVersions.GenOriginalUrlInEmbed)
282
+ migration.up(malformedRecord)
283
+ }).toThrow('Cannot set properties of null')
284
+ })
285
+ })
286
+ })