@tldraw/editor 3.12.0-canary.dfe1ebbad12e → 3.12.0-internal.34d12af75e37

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 (70) hide show
  1. package/dist-cjs/index.d.ts +3 -0
  2. package/dist-cjs/index.js +1 -1
  3. package/dist-cjs/lib/components/Shape.js +10 -14
  4. package/dist-cjs/lib/components/Shape.js.map +2 -2
  5. package/dist-cjs/lib/editor/Editor.js +7 -12
  6. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  7. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js +1 -1
  8. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js.map +2 -2
  9. package/dist-cjs/lib/editor/managers/FontManager.js +1 -1
  10. package/dist-cjs/lib/editor/managers/FontManager.js.map +2 -2
  11. package/dist-cjs/lib/editor/tools/StateNode.js +4 -1
  12. package/dist-cjs/lib/editor/tools/StateNode.js.map +2 -2
  13. package/dist-cjs/lib/exports/StyleEmbedder.js +80 -24
  14. package/dist-cjs/lib/exports/StyleEmbedder.js.map +2 -2
  15. package/dist-cjs/lib/exports/cssRules.js +127 -0
  16. package/dist-cjs/lib/exports/cssRules.js.map +7 -0
  17. package/dist-cjs/lib/exports/exportToSvg.js +4 -1
  18. package/dist-cjs/lib/exports/exportToSvg.js.map +2 -2
  19. package/dist-cjs/lib/exports/parseCss.js +0 -69
  20. package/dist-cjs/lib/exports/parseCss.js.map +2 -2
  21. package/dist-cjs/lib/hooks/useCanvasEvents.js +12 -7
  22. package/dist-cjs/lib/hooks/useCanvasEvents.js.map +3 -3
  23. package/dist-cjs/lib/hooks/useGestureEvents.js +12 -6
  24. package/dist-cjs/lib/hooks/useGestureEvents.js.map +2 -2
  25. package/dist-cjs/lib/utils/debug-flags.js +2 -1
  26. package/dist-cjs/lib/utils/debug-flags.js.map +2 -2
  27. package/dist-cjs/version.js +3 -3
  28. package/dist-cjs/version.js.map +1 -1
  29. package/dist-esm/index.d.mts +3 -0
  30. package/dist-esm/index.mjs +1 -1
  31. package/dist-esm/lib/components/Shape.mjs +11 -15
  32. package/dist-esm/lib/components/Shape.mjs.map +2 -2
  33. package/dist-esm/lib/editor/Editor.mjs +7 -12
  34. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  35. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs +1 -1
  36. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs.map +2 -2
  37. package/dist-esm/lib/editor/managers/FontManager.mjs +1 -1
  38. package/dist-esm/lib/editor/managers/FontManager.mjs.map +2 -2
  39. package/dist-esm/lib/editor/tools/StateNode.mjs +4 -1
  40. package/dist-esm/lib/editor/tools/StateNode.mjs.map +2 -2
  41. package/dist-esm/lib/exports/StyleEmbedder.mjs +82 -31
  42. package/dist-esm/lib/exports/StyleEmbedder.mjs.map +2 -2
  43. package/dist-esm/lib/exports/cssRules.mjs +107 -0
  44. package/dist-esm/lib/exports/cssRules.mjs.map +7 -0
  45. package/dist-esm/lib/exports/exportToSvg.mjs +4 -1
  46. package/dist-esm/lib/exports/exportToSvg.mjs.map +2 -2
  47. package/dist-esm/lib/exports/parseCss.mjs +0 -69
  48. package/dist-esm/lib/exports/parseCss.mjs.map +2 -2
  49. package/dist-esm/lib/hooks/useCanvasEvents.mjs +12 -7
  50. package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +3 -3
  51. package/dist-esm/lib/hooks/useGestureEvents.mjs +12 -6
  52. package/dist-esm/lib/hooks/useGestureEvents.mjs.map +2 -2
  53. package/dist-esm/lib/utils/debug-flags.mjs +2 -1
  54. package/dist-esm/lib/utils/debug-flags.mjs.map +2 -2
  55. package/dist-esm/version.mjs +3 -3
  56. package/dist-esm/version.mjs.map +1 -1
  57. package/package.json +7 -7
  58. package/src/lib/components/Shape.tsx +15 -19
  59. package/src/lib/editor/Editor.ts +7 -13
  60. package/src/lib/editor/derivations/notVisibleShapes.ts +1 -1
  61. package/src/lib/editor/managers/FontManager.ts +1 -1
  62. package/src/lib/editor/tools/StateNode.ts +6 -1
  63. package/src/lib/exports/StyleEmbedder.ts +107 -36
  64. package/src/lib/exports/cssRules.ts +125 -0
  65. package/src/lib/exports/exportToSvg.tsx +5 -1
  66. package/src/lib/exports/parseCss.ts +0 -79
  67. package/src/lib/hooks/useCanvasEvents.ts +14 -7
  68. package/src/lib/hooks/useGestureEvents.ts +12 -6
  69. package/src/lib/utils/debug-flags.ts +1 -0
  70. package/src/version.ts +3 -3
@@ -1216,7 +1216,6 @@ export class Editor extends EventEmitter<TLEventMap> {
1216
1216
  run(fn: () => void, opts?: TLEditorRunOptions): this {
1217
1217
  const previousIgnoreShapeLock = this._shouldIgnoreShapeLock
1218
1218
  this._shouldIgnoreShapeLock = opts?.ignoreShapeLock ?? previousIgnoreShapeLock
1219
-
1220
1219
  try {
1221
1220
  this.history.batch(fn, opts)
1222
1221
  } finally {
@@ -9859,12 +9858,12 @@ export class Editor extends EventEmitter<TLEventMap> {
9859
9858
 
9860
9859
  const { x: cx, y: cy, z: cz } = unsafe__withoutCapture(() => this.getCamera())
9861
9860
 
9862
- const { panSpeed, zoomSpeed } = cameraOptions
9861
+ const { panSpeed } = cameraOptions
9863
9862
  this._setCamera(
9864
9863
  new Vec(
9865
- cx + (dx * panSpeed) / cz - x / cz + x / (z * zoomSpeed),
9866
- cy + (dy * panSpeed) / cz - y / cz + y / (z * zoomSpeed),
9867
- z * zoomSpeed
9864
+ cx + (dx * panSpeed) / cz - x / cz + x / z,
9865
+ cy + (dy * panSpeed) / cz - y / cz + y / z,
9866
+ z
9868
9867
  ),
9869
9868
  { immediate: true }
9870
9869
  )
@@ -9939,14 +9938,9 @@ export class Editor extends EventEmitter<TLEventMap> {
9939
9938
  }
9940
9939
 
9941
9940
  const zoom = cz + (delta ?? 0) * zoomSpeed * cz
9942
- this._setCamera(
9943
- new Vec(
9944
- cx + (x / zoom - x) - (x / cz - x),
9945
- cy + (y / zoom - y) - (y / cz - y),
9946
- zoom
9947
- ),
9948
- { immediate: true }
9949
- )
9941
+ this._setCamera(new Vec(cx + x / zoom - x / cz, cy + y / zoom - y / cz, zoom), {
9942
+ immediate: true,
9943
+ })
9950
9944
  this.maybeTrackPerformance('Zooming')
9951
9945
  return
9952
9946
  }
@@ -31,7 +31,7 @@ export const notVisibleShapes = (editor: Editor) => {
31
31
  })
32
32
  return notVisibleShapes
33
33
  }
34
- return computed<Set<TLShapeId>>('getCulledShapes', (prevValue) => {
34
+ return computed<Set<TLShapeId>>('notVisibleShapes', (prevValue) => {
35
35
  if (isUninitialized(prevValue)) {
36
36
  return fromScratch(editor)
37
37
  }
@@ -94,7 +94,7 @@ export class FontManager {
94
94
  const shapeUtil = this.editor.getShapeUtil(shape)
95
95
  return shapeUtil.getFontFaces(shape)
96
96
  },
97
- { areResultsEqual: areArraysShallowEqual }
97
+ { areResultsEqual: areArraysShallowEqual, areRecordsEqual: (a, b) => a.props === b.props }
98
98
  )
99
99
 
100
100
  this.shapeFontLoadStateCache = editor.store.createCache<(FontState | null)[], TLShape>(
@@ -38,6 +38,7 @@ export interface TLStateNodeConstructor {
38
38
  initial?: string
39
39
  children?(): TLStateNodeConstructor[]
40
40
  isLockable: boolean
41
+ useCoalescedEvents: boolean
41
42
  }
42
43
 
43
44
  /** @public */
@@ -47,7 +48,8 @@ export abstract class StateNode implements Partial<TLEventHandlers> {
47
48
  public editor: Editor,
48
49
  parent?: StateNode
49
50
  ) {
50
- const { id, children, initial, isLockable } = this.constructor as TLStateNodeConstructor
51
+ const { id, children, initial, isLockable, useCoalescedEvents } = this
52
+ .constructor as TLStateNodeConstructor
51
53
 
52
54
  this.id = id
53
55
  this._isActive = atom<boolean>('toolIsActive' + this.id, false)
@@ -83,6 +85,7 @@ export abstract class StateNode implements Partial<TLEventHandlers> {
83
85
  }
84
86
  }
85
87
  this.isLockable = isLockable
88
+ this.useCoalescedEvents = useCoalescedEvents
86
89
  this.performanceTracker = new PerformanceTracker()
87
90
  }
88
91
 
@@ -90,6 +93,7 @@ export abstract class StateNode implements Partial<TLEventHandlers> {
90
93
  static initial?: string
91
94
  static children?: () => TLStateNodeConstructor[]
92
95
  static isLockable = true
96
+ static useCoalescedEvents = false
93
97
 
94
98
  id: string
95
99
  type: 'branch' | 'leaf' | 'root'
@@ -97,6 +101,7 @@ export abstract class StateNode implements Partial<TLEventHandlers> {
97
101
  initial?: string
98
102
  children?: Record<string, StateNode>
99
103
  isLockable: boolean
104
+ useCoalescedEvents: boolean
100
105
  parent: StateNode
101
106
 
102
107
  /**
@@ -1,5 +1,6 @@
1
- import { assertExists, objectMapValues, uniqueId } from '@tldraw/utils'
1
+ import { assert, getOwnProperty, objectMapValues, uniqueId } from '@tldraw/utils'
2
2
  import { FontEmbedder } from './FontEmbedder'
3
+ import { ReadonlyStyles, Styles, cssRules } from './cssRules'
3
4
  import {
4
5
  elementStyle,
5
6
  getComputedStyle,
@@ -7,15 +8,8 @@ import {
7
8
  getRenderedChildren,
8
9
  } from './domUtils'
9
10
  import { resourceToDataUrl } from './fetchCache'
10
- import {
11
- isPropertyCoveredByCurrentColor,
12
- isPropertyInherited,
13
- parseCssValueUrls,
14
- shouldIncludeCssProperty,
15
- } from './parseCss'
16
-
17
- type Styles = { [K in string]?: string }
18
- type ReadonlyStyles = { readonly [K in string]?: string }
11
+ import { parseCssValueUrls, shouldIncludeCssProperty } from './parseCss'
12
+
19
13
  const NO_STYLES = {} as const
20
14
 
21
15
  interface ElementStyleInfo {
@@ -29,6 +23,18 @@ export class StyleEmbedder {
29
23
  private readonly styles = new Map<Element, ElementStyleInfo>()
30
24
  readonly fonts = new FontEmbedder()
31
25
 
26
+ async collectDefaultStyles(elements: Element[]) {
27
+ const collected = new Set<string>()
28
+ const promises = []
29
+ for (const element of elements) {
30
+ const tagName = element.tagName.toLowerCase()
31
+ if (collected.has(tagName)) continue
32
+ collected.add(tagName)
33
+ promises.push(populateDefaultStylesForTagName(tagName))
34
+ }
35
+ await Promise.all(promises)
36
+ }
37
+
32
38
  readRootElementStyles(rootElement: Element) {
33
39
  // when reading a root, we always apply _all_ the styles, even if they match the defaults
34
40
  this.readElementStyles(rootElement, {
@@ -239,15 +245,22 @@ function styleFromComputedStyleMap(
239
245
  { defaultStyles, parentStyles }: ReadStyleOpts
240
246
  ) {
241
247
  const styles: Record<string, string> = {}
248
+ const currentColor = style.get('color')?.toString() || ''
249
+ const ruleOptions = {
250
+ currentColor,
251
+ parentStyles,
252
+ defaultStyles,
253
+ getStyle: (property: string) => style.get(property)?.toString() ?? '',
254
+ }
242
255
  for (const property of style.keys()) {
243
256
  if (!shouldIncludeCssProperty(property)) continue
244
257
 
245
258
  const value = style.get(property)!.toString()
246
259
 
247
260
  if (defaultStyles[property] === value) continue
248
- if (parentStyles[property] === value && isPropertyInherited(property)) continue
249
- if (isPropertyCoveredByCurrentColor(style.get('color')?.toString() || '', property, value))
250
- continue
261
+
262
+ const rule = getOwnProperty(cssRules, property)
263
+ if (rule && rule(value, property, ruleOptions)) continue
251
264
 
252
265
  styles[property] = value
253
266
  }
@@ -260,14 +273,23 @@ function styleFromComputedStyle(
260
273
  { defaultStyles, parentStyles }: ReadStyleOpts
261
274
  ) {
262
275
  const styles: Record<string, string> = {}
276
+ const currentColor = style.color
277
+ const ruleOptions = {
278
+ currentColor,
279
+ parentStyles,
280
+ defaultStyles,
281
+ getStyle: (property: string) => style.getPropertyValue(property),
282
+ }
283
+
263
284
  for (const property in style) {
264
285
  if (!shouldIncludeCssProperty(property)) continue
265
286
 
266
287
  const value = style.getPropertyValue(property)
267
288
 
268
289
  if (defaultStyles[property] === value) continue
269
- if (parentStyles[property] === value && isPropertyInherited(property)) continue
270
- if (isPropertyCoveredByCurrentColor(style.color, property, value)) continue
290
+
291
+ const rule = getOwnProperty(cssRules, property)
292
+ if (rule && rule(value, property, ruleOptions)) continue
271
293
 
272
294
  styles[property] = value
273
295
  }
@@ -285,43 +307,92 @@ function formatCss(style: ReadonlyStyles) {
285
307
  // when we're figuring out the default values for a tag, we need read them from a separate document
286
308
  // so they're not affected by the current document's styles
287
309
  let defaultStyleFrame:
288
- | { iframe: HTMLIFrameElement; foreignObject: SVGForeignObjectElement; document: Document }
310
+ | Promise<{
311
+ url: string
312
+ iframe: HTMLIFrameElement
313
+ foreignObject: SVGForeignObjectElement
314
+ document: Document
315
+ }>
289
316
  | undefined
290
- const defaultStylesByTagName: Record<string, ReadonlyStyles> = {}
317
+
318
+ const defaultStylesByTagName: Record<
319
+ string,
320
+ | { type: 'resolved'; styles: ReadonlyStyles; promise: Promise<ReadonlyStyles> }
321
+ | { type: 'pending'; promise: Promise<ReadonlyStyles> }
322
+ > = {}
323
+
291
324
  function getDefaultStyleFrame() {
292
325
  if (!defaultStyleFrame) {
293
- const frame = document.createElement('iframe')
294
- frame.style.display = 'none'
295
- document.body.appendChild(frame)
296
- const frameDocument = assertExists(frame.contentDocument, 'frame must have a document')
297
- const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
298
- const foreignObject = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject')
299
- svg.appendChild(foreignObject)
300
- frameDocument.body.appendChild(svg)
301
- defaultStyleFrame = { iframe: frame, foreignObject, document: frameDocument }
326
+ defaultStyleFrame = new Promise((resolve) => {
327
+ const frame = document.createElement('iframe')
328
+ Object.assign(frame.style, {
329
+ position: 'absolute',
330
+ top: '-10000px',
331
+ left: '-10000px',
332
+ width: '1px',
333
+ height: '1px',
334
+ opacity: '0',
335
+ pointerEvents: 'none',
336
+ })
337
+
338
+ const emptyFrameBlob = new Blob(
339
+ ['<svg xmlns="http://www.w3.org/2000/svg"><foreignObject/></svg>'],
340
+ { type: 'image/svg+xml' }
341
+ )
342
+ const emptyFrameUrl = URL.createObjectURL(emptyFrameBlob)
343
+
344
+ frame.onload = () => {
345
+ const contentDocument = frame.contentDocument!
346
+ const foreignObject = contentDocument.querySelector('foreignObject')!
347
+ resolve({ url: emptyFrameUrl, iframe: frame, foreignObject, document: contentDocument })
348
+ }
349
+
350
+ frame.src = emptyFrameUrl
351
+ document.body.appendChild(frame)
352
+ })
302
353
  }
303
354
  return defaultStyleFrame
304
355
  }
305
356
 
306
357
  function destroyDefaultStyleFrame() {
307
358
  if (defaultStyleFrame) {
308
- document.body.removeChild(defaultStyleFrame.iframe)
359
+ defaultStyleFrame.then(({ url, iframe }) => {
360
+ URL.revokeObjectURL(url)
361
+ document.body.removeChild(iframe)
362
+ })
309
363
  defaultStyleFrame = undefined
310
364
  }
311
365
  }
312
366
 
313
367
  const defaultStyleReadOptions: ReadStyleOpts = { defaultStyles: NO_STYLES, parentStyles: NO_STYLES }
314
- function getDefaultStylesForTagName(tagName: string) {
315
- let existing = defaultStylesByTagName[tagName]
316
- if (!existing) {
317
- const { foreignObject, document } = getDefaultStyleFrame()
318
- const element = document.createElement(tagName)
368
+ function populateDefaultStylesForTagName(tagName: string) {
369
+ const existing = defaultStylesByTagName[tagName]
370
+ if (existing && existing.type === 'resolved') {
371
+ return existing.promise
372
+ }
373
+
374
+ if (existing && existing.type === 'pending') {
375
+ return existing.promise
376
+ }
377
+
378
+ const promise = getDefaultStyleFrame().then(({ foreignObject, document }) => {
379
+ const element = document.createElementNS('http://www.w3.org/1999/xhtml', tagName)
380
+ element.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml')
319
381
  foreignObject.appendChild(element)
320
- existing = element.computedStyleMap
382
+ const styles = element.computedStyleMap
321
383
  ? styleFromComputedStyleMap(element.computedStyleMap(), defaultStyleReadOptions)
322
384
  : styleFromComputedStyle(getComputedStyle(element), defaultStyleReadOptions)
323
385
  foreignObject.removeChild(element)
324
- defaultStylesByTagName[tagName] = existing
325
- }
326
- return existing
386
+ defaultStylesByTagName[tagName] = { type: 'resolved', styles, promise }
387
+ return styles
388
+ })
389
+
390
+ defaultStylesByTagName[tagName] = { type: 'pending', promise }
391
+ return promise
392
+ }
393
+
394
+ function getDefaultStylesForTagName(tagName: string) {
395
+ const existing = defaultStylesByTagName[tagName]
396
+ assert(existing && existing.type === 'resolved', 'default styles must be populated & resolved')
397
+ return existing.styles
327
398
  }
@@ -0,0 +1,125 @@
1
+ export type Styles = { [K in string]?: string }
2
+ export type ReadonlyStyles = { readonly [K in string]?: string }
3
+
4
+ type CanSkipRule = (
5
+ value: string,
6
+ property: string,
7
+ options: {
8
+ getStyle(property: string): string
9
+ parentStyles: ReadonlyStyles
10
+ defaultStyles: ReadonlyStyles
11
+ currentColor: string
12
+ }
13
+ ) => boolean
14
+
15
+ const isCoveredByCurrentColor: CanSkipRule = (value, property, { currentColor }) => {
16
+ return value === 'currentColor' || value === currentColor
17
+ }
18
+
19
+ const isInherited: CanSkipRule = (value, property, { parentStyles }) => {
20
+ return parentStyles[property] === value
21
+ }
22
+
23
+ const isExcludedBorder =
24
+ (borderDirection: string): CanSkipRule =>
25
+ (value, property, { getStyle }) => {
26
+ const borderWidth = getStyle(`border-${borderDirection}-width`)
27
+ const borderStyle = getStyle(`border-${borderDirection}-style`)
28
+
29
+ if (borderWidth === '0px') return true
30
+ if (borderStyle === 'none') return true
31
+ return false
32
+ }
33
+
34
+ export const cssRules = {
35
+ // currentColor properties:
36
+ 'border-block-end-color': isCoveredByCurrentColor,
37
+ 'border-block-start-color': isCoveredByCurrentColor,
38
+ 'border-bottom-color': isCoveredByCurrentColor,
39
+ 'border-inline-end-color': isCoveredByCurrentColor,
40
+ 'border-inline-start-color': isCoveredByCurrentColor,
41
+ 'border-left-color': isCoveredByCurrentColor,
42
+ 'border-right-color': isCoveredByCurrentColor,
43
+ 'border-top-color': isCoveredByCurrentColor,
44
+ 'caret-color': isCoveredByCurrentColor,
45
+ 'column-rule-color': isCoveredByCurrentColor,
46
+ 'outline-color': isCoveredByCurrentColor,
47
+ 'text-decoration': (value, property, { currentColor }) => {
48
+ return value === 'none solid currentColor' || value === 'none solid ' + currentColor
49
+ },
50
+ 'text-decoration-color': isCoveredByCurrentColor,
51
+ 'text-emphasis-color': isCoveredByCurrentColor,
52
+
53
+ // inherited properties:
54
+ 'border-collapse': isInherited,
55
+ 'border-spacing': isInherited,
56
+ 'caption-side': isInherited,
57
+ // N.B. We shouldn't inherit 'color' because there's some UA styling, e.g. `mark` elements
58
+ // 'color': isInherited,
59
+ cursor: isInherited,
60
+ direction: isInherited,
61
+ 'empty-cells': isInherited,
62
+ 'font-family': isInherited,
63
+ 'font-size': isInherited,
64
+ 'font-style': isInherited,
65
+ 'font-variant': isInherited,
66
+ 'font-weight': isInherited,
67
+ 'font-size-adjust': isInherited,
68
+ 'font-stretch': isInherited,
69
+ font: isInherited,
70
+ 'letter-spacing': isInherited,
71
+ 'line-height': isInherited,
72
+ 'list-style-image': isInherited,
73
+ 'list-style-position': isInherited,
74
+ 'list-style-type': isInherited,
75
+ 'list-style': isInherited,
76
+ orphans: isInherited,
77
+ 'overflow-wrap': isInherited,
78
+ quotes: isInherited,
79
+ 'stroke-linecap': isInherited,
80
+ 'stroke-linejoin': isInherited,
81
+ 'tab-size': isInherited,
82
+ 'text-align': isInherited,
83
+ 'text-align-last': isInherited,
84
+ 'text-indent': isInherited,
85
+ 'text-justify': isInherited,
86
+ 'text-shadow': isInherited,
87
+ 'text-transform': isInherited,
88
+ visibility: isInherited,
89
+ 'white-space': isInherited,
90
+ 'white-space-collapse': isInherited,
91
+ widows: isInherited,
92
+ 'word-break': isInherited,
93
+ 'word-spacing': isInherited,
94
+ 'word-wrap': isInherited,
95
+
96
+ // special border cases - we have a weird case (tailwind seems to trigger this) where all
97
+ // border-styles sometimes get set to 'solid', but the border-width is 0 so they don't render.
98
+ // but in SVGs, **sometimes**, the border-width defaults (i think from a UA style-sheet? but
99
+ // honestly can't tell) to 1.5px so the border displays. we work around this by only including
100
+ // border styles at all if both the border-width and border-style are set to something that
101
+ // would show a border.
102
+ 'border-top': isExcludedBorder('top'),
103
+ 'border-right': isExcludedBorder('right'),
104
+ 'border-bottom': isExcludedBorder('bottom'),
105
+ 'border-left': isExcludedBorder('left'),
106
+ 'border-block-end': isExcludedBorder('block-end'),
107
+ 'border-block-start': isExcludedBorder('block-start'),
108
+ 'border-inline-end': isExcludedBorder('inline-end'),
109
+ 'border-inline-start': isExcludedBorder('inline-start'),
110
+ 'border-top-style': isExcludedBorder('top'),
111
+ 'border-right-style': isExcludedBorder('right'),
112
+ 'border-bottom-style': isExcludedBorder('bottom'),
113
+ 'border-left-style': isExcludedBorder('left'),
114
+ 'border-block-end-style': isExcludedBorder('block-end'),
115
+ 'border-block-start-style': isExcludedBorder('block-start'),
116
+ 'border-inline-end-style': isExcludedBorder('inline-end'),
117
+ 'border-inline-start-style': isExcludedBorder('inline-start'),
118
+ 'border-top-width': isExcludedBorder('top'),
119
+ 'border-right-width': isExcludedBorder('right'),
120
+ 'border-bottom-width': isExcludedBorder('bottom'),
121
+ 'border-left-width': isExcludedBorder('left'),
122
+ 'border-block-end-width': isExcludedBorder('block-end'),
123
+ 'border-block-start-width': isExcludedBorder('block-start'),
124
+ 'border-inline-end-width': isExcludedBorder('inline-end'),
125
+ } satisfies Record<string, CanSkipRule>
@@ -101,11 +101,15 @@ async function applyChangesToForeignObjects(svg: SVGSVGElement) {
101
101
  // urls, and things like videos will be converted to images.
102
102
  await Promise.all(foreignObjectChildren.map((el) => embedMedia(el as HTMLElement)))
103
103
 
104
+ await styleEmbedder.collectDefaultStyles([
105
+ ...svg.querySelectorAll('foreignObject.tl-export-embed-styles *'),
106
+ ])
107
+
104
108
  // read the computed styles of every element (+ it's children & pseudo-elements) in the
105
109
  // document. we do this in a single pass before we start embedding any CSS stuff to avoid
106
110
  // constantly forcing the browser to recompute styles & layout.
107
111
  for (const el of foreignObjectChildren) {
108
- styleEmbedder.readRootElementStyles(el as HTMLElement)
112
+ await styleEmbedder.readRootElementStyles(el as HTMLElement)
109
113
  }
110
114
 
111
115
  // fetch any resources that we need to embed in the CSS, like background images.
@@ -110,82 +110,3 @@ export function parseCssValueUrls(value: string) {
110
110
  url: m[1] || m[2] || m[3],
111
111
  }))
112
112
  }
113
-
114
- const currentColorProperties = new Set([
115
- 'border-block-end-color',
116
- 'border-block-start-color',
117
- 'border-bottom-color',
118
- 'border-inline-end-color',
119
- 'border-inline-start-color',
120
- 'border-left-color',
121
- 'border-right-color',
122
- 'border-top-color',
123
- 'caret-color',
124
- 'column-rule-color',
125
- 'outline-color',
126
- 'text-decoration',
127
- 'text-decoration-color',
128
- 'text-emphasis-color',
129
- ])
130
-
131
- export function isPropertyCoveredByCurrentColor(
132
- currentColor: string,
133
- property: string,
134
- value: string
135
- ) {
136
- if (currentColorProperties.has(property)) {
137
- return (
138
- value === 'currentColor' ||
139
- value === currentColor ||
140
- (property === 'text-decoration' && value === `none solid ${currentColor}`)
141
- )
142
- }
143
- }
144
-
145
- const inheritedProperties = new Set([
146
- 'border-collapse',
147
- 'border-spacing',
148
- 'caption-side',
149
- // N.B. We shouldn't inherit 'color' because there's some UA styling, e.g. `mark` elements
150
- // 'color',
151
- 'cursor',
152
- 'direction',
153
- 'empty-cells',
154
- 'font-family',
155
- 'font-size',
156
- 'font-style',
157
- 'font-variant',
158
- 'font-weight',
159
- 'font-size-adjust',
160
- 'font-stretch',
161
- 'font',
162
- 'letter-spacing',
163
- 'line-height',
164
- 'list-style-image',
165
- 'list-style-position',
166
- 'list-style-type',
167
- 'list-style',
168
- 'orphans',
169
- 'overflow-wrap',
170
- 'quotes',
171
- 'stroke-linecap',
172
- 'stroke-linejoin',
173
- 'tab-size',
174
- 'text-align',
175
- 'text-align-last',
176
- 'text-indent',
177
- 'text-justify',
178
- 'text-shadow',
179
- 'text-transform',
180
- 'visibility',
181
- 'white-space',
182
- 'white-space-collapse',
183
- 'widows',
184
- 'word-break',
185
- 'word-spacing',
186
- 'word-wrap',
187
- ])
188
-
189
- export function isPropertyInherited(property: string) {
190
- return inheritedProperties.has(property)
191
- }
@@ -1,3 +1,4 @@
1
+ import { useValue } from '@tldraw/state-react'
1
2
  import React, { useMemo } from 'react'
2
3
  import { RIGHT_MOUSE_BUTTON } from '../constants'
3
4
  import {
@@ -11,6 +12,7 @@ import { useEditor } from './useEditor'
11
12
 
12
13
  export function useCanvasEvents() {
13
14
  const editor = useEditor()
15
+ const currentTool = useValue('current tool', () => editor.getCurrentTool(), [editor])
14
16
 
15
17
  const events = useMemo(
16
18
  function canvasEvents() {
@@ -49,12 +51,17 @@ export function useCanvasEvents() {
49
51
  lastX = e.clientX
50
52
  lastY = e.clientY
51
53
 
52
- editor.dispatch({
53
- type: 'pointer',
54
- target: 'canvas',
55
- name: 'pointer_move',
56
- ...getPointerInfo(e),
57
- })
54
+ // For tools that benefit from a higher fidelity of events,
55
+ // we dispatch the coalesced events.
56
+ const events = currentTool.useCoalescedEvents ? e.nativeEvent.getCoalescedEvents() : [e]
57
+ for (const singleEvent of events) {
58
+ editor.dispatch({
59
+ type: 'pointer',
60
+ target: 'canvas',
61
+ name: 'pointer_move',
62
+ ...getPointerInfo(singleEvent),
63
+ })
64
+ }
58
65
  }
59
66
 
60
67
  function onPointerUp(e: React.PointerEvent) {
@@ -159,7 +166,7 @@ export function useCanvasEvents() {
159
166
  onClick,
160
167
  }
161
168
  },
162
- [editor]
169
+ [editor, currentTool]
163
170
  )
164
171
 
165
172
  return events
@@ -135,7 +135,6 @@ export function useGestureEvents(ref: React.RefObject<HTMLDivElement>) {
135
135
 
136
136
  let initDistanceBetweenFingers = 1 // the distance between the two fingers when the pinch starts
137
137
  let initZoom = 1 // the browser's zoom level when the pinch starts
138
- let currZoom = 1 // the current zoom level according to the pinch gesture recognizer
139
138
  let currDistanceBetweenFingers = 0
140
139
  const initPointBetweenFingers = new Vec()
141
140
  const prevPointBetweenFingers = new Vec()
@@ -239,7 +238,7 @@ export function useGestureEvents(ref: React.RefObject<HTMLDivElement>) {
239
238
 
240
239
  switch (pinchState) {
241
240
  case 'zooming': {
242
- currZoom = offset[0]
241
+ const currZoom = offset[0] ** editor.getCameraOptions().zoomSpeed
243
242
 
244
243
  editor.dispatch({
245
244
  type: 'pinch',
@@ -278,7 +277,7 @@ export function useGestureEvents(ref: React.RefObject<HTMLDivElement>) {
278
277
  if (event instanceof WheelEvent) return
279
278
  if (!(event.target === elm || elm?.contains(event.target as Node))) return
280
279
 
281
- const scale = offset[0]
280
+ const scale = offset[0] ** editor.getCameraOptions().zoomSpeed
282
281
 
283
282
  pinchState = 'not sure'
284
283
 
@@ -309,14 +308,21 @@ export function useGestureEvents(ref: React.RefObject<HTMLDivElement>) {
309
308
  target: ref,
310
309
  eventOptions: { passive: false },
311
310
  pinch: {
312
- from: () => [editor.getZoomLevel(), 0], // Return the camera z to use when pinch starts
311
+ from: () => {
312
+ const { zoomSpeed } = editor.getCameraOptions()
313
+ const level = editor.getZoomLevel() ** (1 / zoomSpeed)
314
+ return [level, 0]
315
+ }, // Return the camera z to use when pinch starts
313
316
  scaleBounds: () => {
314
317
  const baseZoom = editor.getBaseZoom()
315
- const zoomSteps = editor.getCameraOptions().zoomSteps
318
+ const { zoomSteps, zoomSpeed } = editor.getCameraOptions()
316
319
  const zoomMin = zoomSteps[0] * baseZoom
317
320
  const zoomMax = zoomSteps[zoomSteps.length - 1] * baseZoom
318
321
 
319
- return { from: editor.getZoomLevel(), max: zoomMax, min: zoomMin }
322
+ return {
323
+ max: zoomMax ** (1 / zoomSpeed),
324
+ min: zoomMin ** (1 / zoomSpeed),
325
+ }
320
326
  },
321
327
  },
322
328
  })
@@ -53,6 +53,7 @@ export const debugFlags = {
53
53
  debugGeometry: createDebugValue('debugGeometry', { defaults: { all: false } }),
54
54
  hideShapes: createDebugValue('hideShapes', { defaults: { all: false } }),
55
55
  editOnType: createDebugValue('editOnType', { defaults: { all: false } }),
56
+ a11y: createDebugValue('a11y', { defaults: { all: false } }),
56
57
  } as const
57
58
 
58
59
  declare global {
package/src/version.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  // This file is automatically generated by internal/scripts/refresh-assets.ts.
2
2
  // Do not edit manually. Or do, I'm a comment, not a cop.
3
3
 
4
- export const version = '3.12.0-canary.dfe1ebbad12e'
4
+ export const version = '3.12.0-internal.34d12af75e37'
5
5
  export const publishDates = {
6
6
  major: '2024-09-13T14:36:29.063Z',
7
- minor: '2025-03-24T12:26:06.068Z',
8
- patch: '2025-03-24T12:26:06.068Z',
7
+ minor: '2025-04-01T17:24:24.434Z',
8
+ patch: '2025-04-01T17:24:24.434Z',
9
9
  }