@tldraw/tlschema 4.2.1 → 4.2.2

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 (117) hide show
  1. package/dist-cjs/bindings/TLBaseBinding.js.map +2 -2
  2. package/dist-cjs/createTLSchema.js.map +2 -2
  3. package/dist-cjs/index.d.ts +242 -71
  4. package/dist-cjs/index.js +4 -1
  5. package/dist-cjs/index.js.map +2 -2
  6. package/dist-cjs/misc/TLOpacity.js +1 -5
  7. package/dist-cjs/misc/TLOpacity.js.map +2 -2
  8. package/dist-cjs/misc/TLRichText.js +5 -1
  9. package/dist-cjs/misc/TLRichText.js.map +2 -2
  10. package/dist-cjs/misc/b64Vecs.js +224 -0
  11. package/dist-cjs/misc/b64Vecs.js.map +7 -0
  12. package/dist-cjs/records/TLAsset.js.map +1 -1
  13. package/dist-cjs/records/TLBinding.js.map +2 -2
  14. package/dist-cjs/records/TLShape.js.map +2 -2
  15. package/dist-cjs/shapes/ShapeWithCrop.js.map +1 -1
  16. package/dist-cjs/shapes/TLArrowShape.js +26 -13
  17. package/dist-cjs/shapes/TLArrowShape.js.map +2 -2
  18. package/dist-cjs/shapes/TLBaseShape.js.map +2 -2
  19. package/dist-cjs/shapes/TLDrawShape.js +37 -4
  20. package/dist-cjs/shapes/TLDrawShape.js.map +2 -2
  21. package/dist-cjs/shapes/TLEmbedShape.js +17 -0
  22. package/dist-cjs/shapes/TLEmbedShape.js.map +2 -2
  23. package/dist-cjs/shapes/TLGeoShape.js +12 -1
  24. package/dist-cjs/shapes/TLGeoShape.js.map +2 -2
  25. package/dist-cjs/shapes/TLHighlightShape.js +29 -2
  26. package/dist-cjs/shapes/TLHighlightShape.js.map +2 -2
  27. package/dist-cjs/shapes/TLNoteShape.js +12 -1
  28. package/dist-cjs/shapes/TLNoteShape.js.map +2 -2
  29. package/dist-cjs/shapes/TLTextShape.js +12 -1
  30. package/dist-cjs/shapes/TLTextShape.js.map +2 -2
  31. package/dist-cjs/store-migrations.js +15 -15
  32. package/dist-cjs/store-migrations.js.map +2 -2
  33. package/dist-esm/bindings/TLBaseBinding.mjs.map +2 -2
  34. package/dist-esm/createTLSchema.mjs.map +2 -2
  35. package/dist-esm/index.d.mts +242 -71
  36. package/dist-esm/index.mjs +5 -1
  37. package/dist-esm/index.mjs.map +2 -2
  38. package/dist-esm/misc/TLOpacity.mjs +1 -5
  39. package/dist-esm/misc/TLOpacity.mjs.map +2 -2
  40. package/dist-esm/misc/TLRichText.mjs +5 -1
  41. package/dist-esm/misc/TLRichText.mjs.map +2 -2
  42. package/dist-esm/misc/b64Vecs.mjs +204 -0
  43. package/dist-esm/misc/b64Vecs.mjs.map +7 -0
  44. package/dist-esm/records/TLAsset.mjs.map +1 -1
  45. package/dist-esm/records/TLBinding.mjs.map +2 -2
  46. package/dist-esm/records/TLShape.mjs.map +2 -2
  47. package/dist-esm/shapes/TLArrowShape.mjs +26 -13
  48. package/dist-esm/shapes/TLArrowShape.mjs.map +2 -2
  49. package/dist-esm/shapes/TLBaseShape.mjs.map +2 -2
  50. package/dist-esm/shapes/TLDrawShape.mjs +37 -4
  51. package/dist-esm/shapes/TLDrawShape.mjs.map +2 -2
  52. package/dist-esm/shapes/TLEmbedShape.mjs +17 -0
  53. package/dist-esm/shapes/TLEmbedShape.mjs.map +2 -2
  54. package/dist-esm/shapes/TLGeoShape.mjs +12 -1
  55. package/dist-esm/shapes/TLGeoShape.mjs.map +2 -2
  56. package/dist-esm/shapes/TLHighlightShape.mjs +29 -2
  57. package/dist-esm/shapes/TLHighlightShape.mjs.map +2 -2
  58. package/dist-esm/shapes/TLNoteShape.mjs +12 -1
  59. package/dist-esm/shapes/TLNoteShape.mjs.map +2 -2
  60. package/dist-esm/shapes/TLTextShape.mjs +12 -1
  61. package/dist-esm/shapes/TLTextShape.mjs.map +2 -2
  62. package/dist-esm/store-migrations.mjs +15 -15
  63. package/dist-esm/store-migrations.mjs.map +2 -2
  64. package/package.json +8 -8
  65. package/src/__tests__/migrationTestUtils.ts +9 -3
  66. package/src/bindings/TLBaseBinding.ts +25 -14
  67. package/src/createTLSchema.ts +8 -2
  68. package/src/index.ts +9 -0
  69. package/src/migrations.test.ts +149 -1
  70. package/src/misc/TLOpacity.ts +1 -5
  71. package/src/misc/TLRichText.ts +6 -1
  72. package/src/misc/b64Vecs.ts +308 -0
  73. package/src/records/TLAsset.ts +2 -2
  74. package/src/records/TLBinding.ts +65 -23
  75. package/src/records/TLShape.ts +100 -5
  76. package/src/shapes/ShapeWithCrop.ts +2 -2
  77. package/src/shapes/TLArrowShape.ts +28 -14
  78. package/src/shapes/TLBaseShape.ts +34 -10
  79. package/src/shapes/TLDrawShape.ts +59 -12
  80. package/src/shapes/TLEmbedShape.ts +17 -0
  81. package/src/shapes/TLGeoShape.ts +14 -1
  82. package/src/shapes/TLHighlightShape.ts +37 -0
  83. package/src/shapes/TLNoteShape.ts +15 -1
  84. package/src/shapes/TLTextShape.ts +16 -2
  85. package/src/store-migrations.ts +17 -16
  86. package/src/assets/TLBookmarkAsset.test.ts +0 -96
  87. package/src/assets/TLImageAsset.test.ts +0 -213
  88. package/src/assets/TLVideoAsset.test.ts +0 -105
  89. package/src/bindings/TLArrowBinding.test.ts +0 -55
  90. package/src/misc/id-validator.test.ts +0 -50
  91. package/src/records/TLAsset.test.ts +0 -234
  92. package/src/records/TLBinding.test.ts +0 -22
  93. package/src/records/TLCamera.test.ts +0 -19
  94. package/src/records/TLDocument.test.ts +0 -35
  95. package/src/records/TLInstance.test.ts +0 -201
  96. package/src/records/TLPage.test.ts +0 -110
  97. package/src/records/TLPageState.test.ts +0 -228
  98. package/src/records/TLPointer.test.ts +0 -63
  99. package/src/records/TLPresence.test.ts +0 -190
  100. package/src/records/TLRecord.test.ts +0 -70
  101. package/src/records/TLShape.test.ts +0 -232
  102. package/src/shapes/ShapeWithCrop.test.ts +0 -18
  103. package/src/shapes/TLArrowShape.test.ts +0 -505
  104. package/src/shapes/TLBaseShape.test.ts +0 -142
  105. package/src/shapes/TLBookmarkShape.test.ts +0 -122
  106. package/src/shapes/TLDrawShape.test.ts +0 -177
  107. package/src/shapes/TLEmbedShape.test.ts +0 -286
  108. package/src/shapes/TLFrameShape.test.ts +0 -71
  109. package/src/shapes/TLGeoShape.test.ts +0 -247
  110. package/src/shapes/TLGroupShape.test.ts +0 -59
  111. package/src/shapes/TLHighlightShape.test.ts +0 -325
  112. package/src/shapes/TLImageShape.test.ts +0 -534
  113. package/src/shapes/TLLineShape.test.ts +0 -269
  114. package/src/shapes/TLNoteShape.test.ts +0 -1568
  115. package/src/shapes/TLTextShape.test.ts +0 -407
  116. package/src/shapes/TLVideoShape.test.ts +0 -112
  117. package/src/styles/TLColorStyle.test.ts +0 -439
@@ -1,9 +1,10 @@
1
1
  import { createMigrationSequence } from '@tldraw/store'
2
+ import { structuredClone } from '@tldraw/utils'
2
3
  import { T } from '@tldraw/validate'
3
4
  import { TLRichText, richTextValidator, toRichText } from '../misc/TLRichText'
4
5
  import { VecModel, vecModelValidator } from '../misc/geometry-types'
5
6
  import { createBindingId } from '../records/TLBinding'
6
- import { TLShapeId, createShapePropsMigrationIds } from '../records/TLShape'
7
+ import { TLShape, TLShapeId, createShapePropsMigrationIds } from '../records/TLShape'
7
8
  import { RecordProps, TLPropsMigration, createPropsMigration } from '../recordsWithProps'
8
9
  import { StyleProp } from '../styles/StyleProp'
9
10
  import {
@@ -276,6 +277,7 @@ export const arrowShapeVersions = createShapePropsMigrationIds('arrow', {
276
277
  AddScale: 5,
277
278
  AddElbow: 6,
278
279
  AddRichText: 7,
280
+ AddRichTextAttrs: 8,
279
281
  })
280
282
 
281
283
  function propsMigration(migration: TLPropsMigration) {
@@ -341,8 +343,8 @@ export const arrowShapeMigrations = createMigrationSequence({
341
343
 
342
344
  {
343
345
  id: arrowShapeVersions.ExtractBindings,
344
- scope: 'store',
345
- up: (oldStore) => {
346
+ scope: 'storage',
347
+ up: (storage) => {
346
348
  type OldArrowTerminal =
347
349
  | {
348
350
  type: 'point'
@@ -361,11 +363,10 @@ export const arrowShapeMigrations = createMigrationSequence({
361
363
 
362
364
  type OldArrow = TLBaseShape<'arrow', { start: OldArrowTerminal; end: OldArrowTerminal }>
363
365
 
364
- const arrows = Object.values(oldStore).filter(
365
- (r: any): r is OldArrow => r.typeName === 'shape' && r.type === 'arrow'
366
- )
367
-
368
- for (const arrow of arrows) {
366
+ for (const record of storage.values()) {
367
+ if (record.typeName !== 'shape' || (record as TLShape).type !== 'arrow') continue
368
+ const arrow = record as OldArrow
369
+ const newArrow = structuredClone(arrow)
369
370
  const { start, end } = arrow.props
370
371
  if (start.type === 'binding') {
371
372
  const id = createBindingId()
@@ -384,10 +385,10 @@ export const arrowShapeMigrations = createMigrationSequence({
384
385
  },
385
386
  }
386
387
 
387
- oldStore[id] = binding
388
- arrow.props.start = { x: 0, y: 0 }
388
+ storage.set(id, binding as any)
389
+ newArrow.props.start = { x: 0, y: 0 }
389
390
  } else {
390
- delete arrow.props.start.type
391
+ delete newArrow.props.start.type
391
392
  }
392
393
  if (end.type === 'binding') {
393
394
  const id = createBindingId()
@@ -406,11 +407,12 @@ export const arrowShapeMigrations = createMigrationSequence({
406
407
  },
407
408
  }
408
409
 
409
- oldStore[id] = binding
410
- arrow.props.end = { x: 0, y: 0 }
410
+ storage.set(id, binding as any)
411
+ newArrow.props.end = { x: 0, y: 0 }
411
412
  } else {
412
- delete arrow.props.end.type
413
+ delete newArrow.props.end.type
413
414
  }
415
+ storage.set(arrow.id, newArrow)
414
416
  }
415
417
  },
416
418
  },
@@ -445,5 +447,17 @@ export const arrowShapeMigrations = createMigrationSequence({
445
447
  // delete props.richText
446
448
  // },
447
449
  }),
450
+ propsMigration({
451
+ id: arrowShapeVersions.AddRichTextAttrs,
452
+ up: (_props) => {
453
+ // noop - attrs is optional so old records are valid
454
+ },
455
+ down: (props) => {
456
+ // Remove attrs from richText when migrating down
457
+ if (props.richText && 'attrs' in props.richText) {
458
+ delete props.richText.attrs
459
+ }
460
+ },
461
+ }),
448
462
  ],
449
463
  })
@@ -1,4 +1,3 @@
1
- import { BaseRecord } from '@tldraw/store'
2
1
  import { IndexKey, JsonObject } from '@tldraw/utils'
3
2
  import { T } from '@tldraw/validate'
4
3
  import { TLOpacityType, opacityValidator } from '../misc/TLOpacity'
@@ -9,18 +8,37 @@ import { TLParentId, TLShapeId } from '../records/TLShape'
9
8
  * Base interface for all shapes in tldraw.
10
9
  *
11
10
  * This interface defines the common properties that all shapes share, regardless of their
12
- * specific type. Every shape extends this base with additional type-specific properties.
11
+ * specific type. Every default shape extends this base with additional type-specific properties.
12
+ *
13
+ * Custom shapes should be defined by augmenting the TLGlobalShapePropsMap type and getting the shape type from the TLShape type.
13
14
  *
14
15
  * @example
15
16
  * ```ts
16
- * // Define a custom shape type
17
- * interface MyCustomShape extends TLBaseShape<'custom', { size: number; color: string }> {}
17
+ * // Define a default shape type
18
+ * interface TLArrowShape extends TLBaseShape<'arrow', {
19
+ * kind: TLArrowShapeKind
20
+ * labelColor: TLDefaultColorStyle
21
+ * color: TLDefaultColorStyle
22
+ * fill: TLDefaultFillStyle
23
+ * dash: TLDefaultDashStyle
24
+ * size: TLDefaultSizeStyle
25
+ * arrowheadStart: TLArrowShapeArrowheadStyle
26
+ * arrowheadEnd: TLArrowShapeArrowheadStyle
27
+ * font: TLDefaultFontStyle
28
+ * start: VecModel
29
+ * end: VecModel
30
+ * bend: number
31
+ * richText: TLRichText
32
+ * labelPosition: number
33
+ * scale: number
34
+ * elbowMidPoint: number
35
+ * }> {}
18
36
  *
19
37
  * // Create a shape instance
20
- * const myShape: MyCustomShape = {
38
+ * const arrowShape: TLArrowShape = {
21
39
  * id: 'shape:abc123',
22
40
  * typeName: 'shape',
23
- * type: 'custom',
41
+ * type: 'arrow',
24
42
  * x: 100,
25
43
  * y: 200,
26
44
  * rotation: 0,
@@ -29,8 +47,10 @@ import { TLParentId, TLShapeId } from '../records/TLShape'
29
47
  * isLocked: false,
30
48
  * opacity: 1,
31
49
  * props: {
32
- * size: 50,
33
- * color: 'blue'
50
+ * kind: 'arc',
51
+ * start: { x: 0, y: 0 },
52
+ * end: { x: 100, y: 100 },
53
+ * // ... other props
34
54
  * },
35
55
  * meta: {}
36
56
  * }
@@ -38,8 +58,12 @@ import { TLParentId, TLShapeId } from '../records/TLShape'
38
58
  *
39
59
  * @public
40
60
  */
41
- export interface TLBaseShape<Type extends string, Props extends object>
42
- extends BaseRecord<'shape', TLShapeId> {
61
+ export interface TLBaseShape<Type extends string, Props extends object> {
62
+ // using real `extends BaseRecord<'shape', TLShapeId>` introduces a circularity in the types
63
+ // and for that reason those "base members" have to be declared manually here
64
+ readonly id: TLShapeId
65
+ readonly typeName: 'shape'
66
+
43
67
  type: Type
44
68
  x: number
45
69
  y: number
@@ -1,5 +1,6 @@
1
1
  import { T } from '@tldraw/validate'
2
- import { VecModel, vecModelValidator } from '../misc/geometry-types'
2
+ import { b64Vecs } from '../misc/b64Vecs'
3
+ import { VecModel } from '../misc/geometry-types'
3
4
  import { createShapePropsMigrationIds, createShapePropsMigrationSequence } from '../records/TLShape'
4
5
  import { RecordProps } from '../recordsWithProps'
5
6
  import { DefaultColorStyle, TLDefaultColorStyle } from '../styles/TLColorStyle'
@@ -16,26 +17,18 @@ import { TLBaseShape } from './TLBaseShape'
16
17
  export interface TLDrawShapeSegment {
17
18
  /** Type of drawing segment - 'free' for freehand curves, 'straight' for line segments */
18
19
  type: 'free' | 'straight'
19
- /** Array of points defining the segment path with x, y coordinates and pressure (z) */
20
- points: VecModel[]
20
+ /** Base64-encoded points (x, y, z triplets stored as Float16) */
21
+ points: string
21
22
  }
22
23
 
23
24
  /**
24
25
  * Validator for draw shape segments ensuring proper structure and data types.
25
26
  *
26
27
  * @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
28
  */
36
29
  export const DrawShapeSegment: T.ObjectValidator<TLDrawShapeSegment> = T.object({
37
30
  type: T.literalEnum('free', 'straight'),
38
- points: T.arrayOf(vecModelValidator),
31
+ points: T.string,
39
32
  })
40
33
 
41
34
  /**
@@ -62,6 +55,10 @@ export interface TLDrawShapeProps {
62
55
  isPen: boolean
63
56
  /** Scale factor applied to the drawing */
64
57
  scale: number
58
+ /** Horizontal scale factor for lazy resize */
59
+ scaleX: number
60
+ /** Vertical scale factor for lazy resize */
61
+ scaleY: number
65
62
  }
66
63
 
67
64
  /**
@@ -118,6 +115,7 @@ export type TLDrawShape = TLBaseShape<'draw', TLDrawShapeProps>
118
115
  * const isValid = drawShapeProps.color.isValid(props.color)
119
116
  * ```
120
117
  */
118
+ /** @public */
121
119
  export const drawShapeProps: RecordProps<TLDrawShape> = {
122
120
  color: DefaultColorStyle,
123
121
  fill: DefaultFillStyle,
@@ -128,11 +126,14 @@ export const drawShapeProps: RecordProps<TLDrawShape> = {
128
126
  isClosed: T.boolean,
129
127
  isPen: T.boolean,
130
128
  scale: T.nonZeroNumber,
129
+ scaleX: T.nonZeroFiniteNumber,
130
+ scaleY: T.nonZeroFiniteNumber,
131
131
  }
132
132
 
133
133
  const Versions = createShapePropsMigrationIds('draw', {
134
134
  AddInPen: 1,
135
135
  AddScale: 2,
136
+ Base64: 3,
136
137
  })
137
138
 
138
139
  /**
@@ -184,5 +185,51 @@ export const drawShapeMigrations = createShapePropsMigrationSequence({
184
185
  delete props.scale
185
186
  },
186
187
  },
188
+ {
189
+ id: Versions.Base64,
190
+ up: (props) => {
191
+ props.segments = props.segments.map((segment: any) => {
192
+ return {
193
+ ...segment,
194
+ // Only encode if points is an array (not already base64 string)
195
+ points:
196
+ typeof segment.points === 'string'
197
+ ? segment.points
198
+ : b64Vecs.encodePoints(segment.points),
199
+ }
200
+ })
201
+ props.scaleX = props.scaleX ?? 1
202
+ props.scaleY = props.scaleY ?? 1
203
+ },
204
+ down: (props) => {
205
+ props.segments = props.segments.map((segment: any) => ({
206
+ ...segment,
207
+ // Only decode if points is a string (not already VecModel[])
208
+ points: Array.isArray(segment.points)
209
+ ? segment.points
210
+ : b64Vecs.decodePoints(segment.points),
211
+ }))
212
+ delete props.scaleX
213
+ delete props.scaleY
214
+ },
215
+ },
187
216
  ],
188
217
  })
218
+
219
+ /**
220
+ * Compress legacy draw shape segments by converting VecModel[] points to base64 format.
221
+ * This function is useful for converting old draw shape data to the new compressed format.
222
+ *
223
+ * @public
224
+ */
225
+ export function compressLegacySegments(
226
+ segments: {
227
+ type: 'free' | 'straight'
228
+ points: VecModel[]
229
+ }[]
230
+ ): TLDrawShapeSegment[] {
231
+ return segments.map((segment) => ({
232
+ ...segment,
233
+ points: b64Vecs.encodePoints(segment.points),
234
+ }))
235
+ }
@@ -10,6 +10,7 @@ const TLDRAW_APP_RE = /(^\/r\/[^/]+\/?$)/
10
10
  const EMBED_DEFINITIONS = [
11
11
  {
12
12
  hostnames: ['beta.tldraw.com', 'tldraw.com', 'localhost:3000'],
13
+ canEditWhileLocked: true,
13
14
  fromEmbedUrl: (url: string) => {
14
15
  const urlObj = safeParseUrl(url)
15
16
  if (urlObj && urlObj.pathname.match(TLDRAW_APP_RE)) {
@@ -20,6 +21,7 @@ const EMBED_DEFINITIONS = [
20
21
  },
21
22
  {
22
23
  hostnames: ['figma.com'],
24
+ canEditWhileLocked: true,
23
25
  fromEmbedUrl: (url: string) => {
24
26
  const urlObj = safeParseUrl(url)
25
27
  if (urlObj && urlObj.pathname.match(/^\/embed\/?$/)) {
@@ -33,6 +35,7 @@ const EMBED_DEFINITIONS = [
33
35
  },
34
36
  {
35
37
  hostnames: ['google.*'],
38
+ canEditWhileLocked: true,
36
39
  fromEmbedUrl: (url: string) => {
37
40
  const urlObj = safeParseUrl(url)
38
41
  if (!urlObj) return
@@ -48,6 +51,7 @@ const EMBED_DEFINITIONS = [
48
51
  },
49
52
  {
50
53
  hostnames: ['val.town'],
54
+ canEditWhileLocked: true,
51
55
  fromEmbedUrl: (url: string) => {
52
56
  const urlObj = safeParseUrl(url)
53
57
  // e.g. extract "steveruizok/mathFact" from https://www.val.town/v/steveruizok/mathFact
@@ -60,6 +64,7 @@ const EMBED_DEFINITIONS = [
60
64
  },
61
65
  {
62
66
  hostnames: ['codesandbox.io'],
67
+ canEditWhileLocked: true,
63
68
  fromEmbedUrl: (url: string) => {
64
69
  const urlObj = safeParseUrl(url)
65
70
  const matches = urlObj && urlObj.pathname.match(/\/embed\/([^/]+)\/?/)
@@ -71,6 +76,7 @@ const EMBED_DEFINITIONS = [
71
76
  },
72
77
  {
73
78
  hostnames: ['codepen.io'],
79
+ canEditWhileLocked: true,
74
80
  fromEmbedUrl: (url: string) => {
75
81
  const CODEPEN_EMBED_REGEXP = /https:\/\/codepen.io\/([^/]+)\/embed\/([^/]+)/
76
82
  const matches = url.match(CODEPEN_EMBED_REGEXP)
@@ -83,6 +89,7 @@ const EMBED_DEFINITIONS = [
83
89
  },
84
90
  {
85
91
  hostnames: ['scratch.mit.edu'],
92
+ canEditWhileLocked: true,
86
93
  fromEmbedUrl: (url: string) => {
87
94
  const SCRATCH_EMBED_REGEXP = /https:\/\/scratch.mit.edu\/projects\/embed\/([^/]+)/
88
95
  const matches = url.match(SCRATCH_EMBED_REGEXP)
@@ -95,6 +102,7 @@ const EMBED_DEFINITIONS = [
95
102
  },
96
103
  {
97
104
  hostnames: ['*.youtube.com', 'youtube.com', 'youtu.be'],
105
+ canEditWhileLocked: true,
98
106
  fromEmbedUrl: (url: string) => {
99
107
  const urlObj = safeParseUrl(url)
100
108
  if (!urlObj) return
@@ -111,6 +119,7 @@ const EMBED_DEFINITIONS = [
111
119
  },
112
120
  {
113
121
  hostnames: ['calendar.google.*'],
122
+ canEditWhileLocked: true,
114
123
  fromEmbedUrl: (url: string) => {
115
124
  const urlObj = safeParseUrl(url)
116
125
  const srcQs = urlObj?.searchParams.get('src')
@@ -129,6 +138,7 @@ const EMBED_DEFINITIONS = [
129
138
  },
130
139
  {
131
140
  hostnames: ['docs.google.*'],
141
+ canEditWhileLocked: true,
132
142
  fromEmbedUrl: (url: string) => {
133
143
  const urlObj = safeParseUrl(url)
134
144
 
@@ -145,6 +155,7 @@ const EMBED_DEFINITIONS = [
145
155
  },
146
156
  {
147
157
  hostnames: ['gist.github.com'],
158
+ canEditWhileLocked: true,
148
159
  fromEmbedUrl: (url: string) => {
149
160
  const urlObj = safeParseUrl(url)
150
161
  if (urlObj && urlObj.pathname.match(/\/([^/]+)\/([^/]+)/)) {
@@ -156,6 +167,7 @@ const EMBED_DEFINITIONS = [
156
167
  },
157
168
  {
158
169
  hostnames: ['replit.com'],
170
+ canEditWhileLocked: true,
159
171
  fromEmbedUrl: (url: string) => {
160
172
  const urlObj = safeParseUrl(url)
161
173
  if (
@@ -171,6 +183,7 @@ const EMBED_DEFINITIONS = [
171
183
  },
172
184
  {
173
185
  hostnames: ['felt.com'],
186
+ canEditWhileLocked: true,
174
187
  fromEmbedUrl: (url: string) => {
175
188
  const urlObj = safeParseUrl(url)
176
189
  if (urlObj && urlObj.pathname.match(/^\/embed\/map\//)) {
@@ -182,6 +195,7 @@ const EMBED_DEFINITIONS = [
182
195
  },
183
196
  {
184
197
  hostnames: ['open.spotify.com'],
198
+ canEditWhileLocked: true,
185
199
  fromEmbedUrl: (url: string) => {
186
200
  const urlObj = safeParseUrl(url)
187
201
  if (urlObj && urlObj.pathname.match(/^\/embed\/(artist|album)\//)) {
@@ -192,6 +206,7 @@ const EMBED_DEFINITIONS = [
192
206
  },
193
207
  {
194
208
  hostnames: ['vimeo.com', 'player.vimeo.com'],
209
+ canEditWhileLocked: true,
195
210
  fromEmbedUrl: (url: string) => {
196
211
  const urlObj = safeParseUrl(url)
197
212
  if (urlObj && urlObj.hostname === 'player.vimeo.com') {
@@ -205,6 +220,7 @@ const EMBED_DEFINITIONS = [
205
220
  },
206
221
  {
207
222
  hostnames: ['observablehq.com'],
223
+ canEditWhileLocked: true,
208
224
  fromEmbedUrl: (url: string) => {
209
225
  const urlObj = safeParseUrl(url)
210
226
  if (urlObj && urlObj.pathname.match(/^\/embed\/@([^/]+)\/([^/]+)\/?$/)) {
@@ -219,6 +235,7 @@ const EMBED_DEFINITIONS = [
219
235
  },
220
236
  {
221
237
  hostnames: ['desmos.com'],
238
+ canEditWhileLocked: true,
222
239
  fromEmbedUrl: (url: string) => {
223
240
  const urlObj = safeParseUrl(url)
224
241
  if (
@@ -193,6 +193,7 @@ const geoShapeVersions = createShapePropsMigrationIds('geo', {
193
193
  MakeUrlsValid: 8,
194
194
  AddScale: 9,
195
195
  AddRichText: 10,
196
+ AddRichTextAttrs: 11,
196
197
  })
197
198
 
198
199
  /**
@@ -205,7 +206,7 @@ export { geoShapeVersions as geoShapeVersions }
205
206
  /**
206
207
  * Migration sequence for geo shape properties across different schema versions.
207
208
  * Handles evolution of geo shapes including URL support, label colors, alignment changes,
208
- * and the transition from plain text to rich text.
209
+ * the transition from plain text to rich text, and support for attrs property on richText.
209
210
  *
210
211
  * @public
211
212
  */
@@ -305,5 +306,17 @@ export const geoShapeMigrations = createShapePropsMigrationSequence({
305
306
  // delete props.richText
306
307
  // },
307
308
  },
309
+ {
310
+ id: geoShapeVersions.AddRichTextAttrs,
311
+ up: (_props) => {
312
+ // noop - attrs is optional so old records are valid
313
+ },
314
+ down: (props) => {
315
+ // Remove attrs from richText when migrating down
316
+ if (props.richText && 'attrs' in props.richText) {
317
+ delete props.richText.attrs
318
+ }
319
+ },
320
+ },
308
321
  ],
309
322
  })
@@ -1,4 +1,5 @@
1
1
  import { T } from '@tldraw/validate'
2
+ import { b64Vecs } from '../misc/b64Vecs'
2
3
  import { createShapePropsMigrationIds, createShapePropsMigrationSequence } from '../records/TLShape'
3
4
  import { RecordProps } from '../recordsWithProps'
4
5
  import { DefaultColorStyle, TLDefaultColorStyle } from '../styles/TLColorStyle'
@@ -36,6 +37,10 @@ export interface TLHighlightShapeProps {
36
37
  isPen: boolean
37
38
  /** Scale factor applied to the highlight shape for display */
38
39
  scale: number
40
+ /** Horizontal scale factor for lazy resize */
41
+ scaleX: number
42
+ /** Vertical scale factor for lazy resize */
43
+ scaleY: number
39
44
  }
40
45
 
41
46
  /**
@@ -84,6 +89,7 @@ export type TLHighlightShape = TLBaseShape<'highlight', TLHighlightShapeProps>
84
89
  * const validatedProps = validator.validate(someHighlightProps)
85
90
  * ```
86
91
  */
92
+ /** @public */
87
93
  export const highlightShapeProps: RecordProps<TLHighlightShape> = {
88
94
  color: DefaultColorStyle,
89
95
  size: DefaultSizeStyle,
@@ -91,10 +97,13 @@ export const highlightShapeProps: RecordProps<TLHighlightShape> = {
91
97
  isComplete: T.boolean,
92
98
  isPen: T.boolean,
93
99
  scale: T.nonZeroNumber,
100
+ scaleX: T.nonZeroFiniteNumber,
101
+ scaleY: T.nonZeroFiniteNumber,
94
102
  }
95
103
 
96
104
  const Versions = createShapePropsMigrationIds('highlight', {
97
105
  AddScale: 1,
106
+ Base64: 2,
98
107
  })
99
108
 
100
109
  /**
@@ -122,5 +131,33 @@ export const highlightShapeMigrations = createShapePropsMigrationSequence({
122
131
  delete props.scale
123
132
  },
124
133
  },
134
+ {
135
+ id: Versions.Base64,
136
+ up: (props) => {
137
+ props.segments = props.segments.map((segment: any) => {
138
+ return {
139
+ ...segment,
140
+ // Only encode if points is an array (not already base64 string)
141
+ points:
142
+ typeof segment.points === 'string'
143
+ ? segment.points
144
+ : b64Vecs.encodePoints(segment.points),
145
+ }
146
+ })
147
+ props.scaleX = props.scaleX ?? 1
148
+ props.scaleY = props.scaleY ?? 1
149
+ },
150
+ down: (props) => {
151
+ props.segments = props.segments.map((segment: any) => ({
152
+ ...segment,
153
+ // Only decode if points is a string (not already VecModel[])
154
+ points: Array.isArray(segment.points)
155
+ ? segment.points
156
+ : b64Vecs.decodePoints(segment.points),
157
+ }))
158
+ delete props.scaleX
159
+ delete props.scaleY
160
+ },
161
+ },
125
162
  ],
126
163
  })
@@ -142,6 +142,7 @@ const Versions = createShapePropsMigrationIds('note', {
142
142
  AddScale: 7,
143
143
  AddLabelColor: 8,
144
144
  AddRichText: 9,
145
+ AddRichTextAttrs: 10,
145
146
  })
146
147
 
147
148
  /**
@@ -156,7 +157,8 @@ export { Versions as noteShapeVersions }
156
157
  * Migration sequence for note shapes. Handles schema evolution over time by defining
157
158
  * how to upgrade and downgrade note shape data between different versions. Includes
158
159
  * migrations for URL properties, text alignment changes, vertical alignment addition,
159
- * font size adjustments, scaling support, label color, and the transition from plain text to rich text.
160
+ * font size adjustments, scaling support, label color, the transition from plain text to rich text,
161
+ * and support for attrs property on richText.
160
162
  *
161
163
  * @public
162
164
  */
@@ -251,5 +253,17 @@ export const noteShapeMigrations = createShapePropsMigrationSequence({
251
253
  // delete props.richText
252
254
  // },
253
255
  },
256
+ {
257
+ id: Versions.AddRichTextAttrs,
258
+ up: (_props) => {
259
+ // noop - attrs is optional so old records are valid
260
+ },
261
+ down: (props) => {
262
+ // Remove attrs from richText when migrating down
263
+ if (props.richText && 'attrs' in props.richText) {
264
+ delete props.richText.attrs
265
+ }
266
+ },
267
+ },
254
268
  ],
255
269
  })
@@ -104,6 +104,7 @@ const Versions = createShapePropsMigrationIds('text', {
104
104
  RemoveJustify: 1,
105
105
  AddTextAlign: 2,
106
106
  AddRichText: 3,
107
+ AddRichTextAttrs: 4,
107
108
  })
108
109
 
109
110
  /**
@@ -116,8 +117,8 @@ const Versions = createShapePropsMigrationIds('text', {
116
117
  * import { textShapeVersions } from '@tldraw/tlschema'
117
118
  *
118
119
  * // Check if shape data needs migration
119
- * if (shapeVersion < textShapeVersions.AddRichText) {
120
- * // Apply rich text migration
120
+ * if (shapeVersion < textShapeVersions.AddRichTextAttrs) {
121
+ * // Apply rich text attrs migration
121
122
  * }
122
123
  * ```
123
124
  */
@@ -131,6 +132,7 @@ export { Versions as textShapeVersions }
131
132
  * - RemoveJustify: Replaced 'justify' alignment with 'start'
132
133
  * - AddTextAlign: Migrated from 'align' to 'textAlign' property
133
134
  * - AddRichText: Converted plain text to rich text format
135
+ * - AddRichTextAttrs: Added support for attrs property on richText
134
136
  *
135
137
  * @public
136
138
  */
@@ -167,5 +169,17 @@ export const textShapeMigrations = createShapePropsMigrationSequence({
167
169
  // delete props.richText
168
170
  // },
169
171
  },
172
+ {
173
+ id: Versions.AddRichTextAttrs,
174
+ up: (_props) => {
175
+ // noop - attrs is optional so old records are valid
176
+ },
177
+ down: (props) => {
178
+ // Remove attrs from richText when migrating down
179
+ if (props.richText && 'attrs' in props.richText) {
180
+ delete props.richText.attrs
181
+ }
182
+ },
183
+ },
170
184
  ],
171
185
  })
@@ -67,35 +67,36 @@ export const storeMigrations = createMigrationSequence({
67
67
  sequence: [
68
68
  {
69
69
  id: Versions.RemoveCodeAndIconShapeTypes,
70
- scope: 'store',
71
- up: (store) => {
72
- for (const [id, record] of objectMapEntries(store)) {
70
+ scope: 'storage',
71
+ up: (storage) => {
72
+ for (const [id, record] of storage.entries()) {
73
73
  if (
74
74
  record.typeName === 'shape' &&
75
- ((record as TLShape).type === 'icon' || (record as TLShape).type === 'code')
75
+ 'type' in record &&
76
+ (record.type === 'icon' || record.type === 'code')
76
77
  ) {
77
- delete store[id]
78
+ storage.delete(id)
78
79
  }
79
80
  }
80
81
  },
81
82
  },
82
83
  {
83
84
  id: Versions.AddInstancePresenceType,
84
- scope: 'store',
85
- up(_store) {
85
+ scope: 'storage',
86
+ up(_storage) {
86
87
  // noop
87
88
  // there used to be a down migration for this but we made down migrations optional
88
- // and we don't use them on store-level migrations so we can just remove it
89
+ // and we don't use them on storage-level migrations so we can just remove it
89
90
  },
90
91
  },
91
92
  {
92
93
  // remove user and presence records and add pointer records
93
94
  id: Versions.RemoveTLUserAndPresenceAndAddPointer,
94
- scope: 'store',
95
- up: (store) => {
96
- for (const [id, record] of objectMapEntries(store)) {
95
+ scope: 'storage',
96
+ up: (storage) => {
97
+ for (const [id, record] of storage.entries()) {
97
98
  if (record.typeName.match(/^(user|user_presence)$/)) {
98
- delete store[id]
99
+ storage.delete(id)
99
100
  }
100
101
  }
101
102
  },
@@ -103,11 +104,11 @@ export const storeMigrations = createMigrationSequence({
103
104
  {
104
105
  // remove user document records
105
106
  id: Versions.RemoveUserDocument,
106
- scope: 'store',
107
- up: (store) => {
108
- for (const [id, record] of objectMapEntries(store)) {
107
+ scope: 'storage',
108
+ up: (storage) => {
109
+ for (const [id, record] of storage.entries()) {
109
110
  if (record.typeName.match('user_document')) {
110
- delete store[id]
111
+ storage.delete(id)
111
112
  }
112
113
  }
113
114
  },