tldraw 3.16.0-canary.8eb57ca47f51 → 3.16.0-canary.940daaae9993

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 (51) hide show
  1. package/dist-cjs/index.js +1 -1
  2. package/dist-cjs/lib/shapes/arrow/arrowLabel.js +6 -0
  3. package/dist-cjs/lib/shapes/arrow/arrowLabel.js.map +3 -3
  4. package/dist-cjs/lib/shapes/frame/FrameShapeUtil.js +2 -1
  5. package/dist-cjs/lib/shapes/frame/FrameShapeUtil.js.map +2 -2
  6. package/dist-cjs/lib/shapes/geo/GeoShapeUtil.js +1 -0
  7. package/dist-cjs/lib/shapes/geo/GeoShapeUtil.js.map +2 -2
  8. package/dist-cjs/lib/shapes/note/NoteShapeUtil.js +2 -1
  9. package/dist-cjs/lib/shapes/note/NoteShapeUtil.js.map +2 -2
  10. package/dist-cjs/lib/ui/components/primitives/TldrawUiTooltip.js +14 -5
  11. package/dist-cjs/lib/ui/components/primitives/TldrawUiTooltip.js.map +2 -2
  12. package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuItem.js +5 -5
  13. package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuItem.js.map +1 -1
  14. package/dist-cjs/lib/ui/hooks/useTools.js +1 -1
  15. package/dist-cjs/lib/ui/hooks/useTools.js.map +2 -2
  16. package/dist-cjs/lib/ui/hooks/useTranslation/defaultTranslation.js +2 -2
  17. package/dist-cjs/lib/ui/hooks/useTranslation/defaultTranslation.js.map +1 -1
  18. package/dist-cjs/lib/ui/version.js +3 -3
  19. package/dist-cjs/lib/ui/version.js.map +1 -1
  20. package/dist-esm/index.mjs +1 -1
  21. package/dist-esm/lib/shapes/arrow/arrowLabel.mjs +6 -0
  22. package/dist-esm/lib/shapes/arrow/arrowLabel.mjs.map +3 -3
  23. package/dist-esm/lib/shapes/frame/FrameShapeUtil.mjs +2 -1
  24. package/dist-esm/lib/shapes/frame/FrameShapeUtil.mjs.map +2 -2
  25. package/dist-esm/lib/shapes/geo/GeoShapeUtil.mjs +1 -0
  26. package/dist-esm/lib/shapes/geo/GeoShapeUtil.mjs.map +2 -2
  27. package/dist-esm/lib/shapes/note/NoteShapeUtil.mjs +2 -1
  28. package/dist-esm/lib/shapes/note/NoteShapeUtil.mjs.map +2 -2
  29. package/dist-esm/lib/ui/components/primitives/TldrawUiTooltip.mjs +14 -5
  30. package/dist-esm/lib/ui/components/primitives/TldrawUiTooltip.mjs.map +2 -2
  31. package/dist-esm/lib/ui/components/primitives/menus/TldrawUiMenuItem.mjs +5 -5
  32. package/dist-esm/lib/ui/components/primitives/menus/TldrawUiMenuItem.mjs.map +1 -1
  33. package/dist-esm/lib/ui/hooks/useTools.mjs +1 -1
  34. package/dist-esm/lib/ui/hooks/useTools.mjs.map +2 -2
  35. package/dist-esm/lib/ui/hooks/useTranslation/defaultTranslation.mjs +2 -2
  36. package/dist-esm/lib/ui/hooks/useTranslation/defaultTranslation.mjs.map +1 -1
  37. package/dist-esm/lib/ui/version.mjs +3 -3
  38. package/dist-esm/lib/ui/version.mjs.map +1 -1
  39. package/package.json +3 -3
  40. package/src/lib/shapes/arrow/ArrowShapeUtil.test.ts +41 -0
  41. package/src/lib/shapes/arrow/arrowLabel.ts +8 -0
  42. package/src/lib/shapes/frame/FrameShapeUtil.tsx +1 -0
  43. package/src/lib/shapes/geo/GeoShapeUtil.tsx +1 -0
  44. package/src/lib/shapes/note/NoteShapeUtil.tsx +1 -0
  45. package/src/lib/ui/components/primitives/TldrawUiTooltip.tsx +18 -8
  46. package/src/lib/ui/components/primitives/menus/TldrawUiMenuItem.tsx +6 -6
  47. package/src/lib/ui/hooks/useTools.tsx +1 -1
  48. package/src/lib/ui/hooks/useTranslation/defaultTranslation.ts +2 -2
  49. package/src/lib/ui/version.ts +3 -3
  50. package/src/test/getCulledShapes.test.tsx +71 -2
  51. package/tldraw.css +8 -3
@@ -227,6 +227,14 @@ interface ArrowheadInfo {
227
227
  hasEndArrowhead: boolean
228
228
  }
229
229
  export function getArrowLabelPosition(editor: Editor, shape: TLArrowShape) {
230
+ const isEditing = editor.getEditingShapeId() === shape.id
231
+ if (!isEditing && isEmptyRichText(shape.props.richText)) {
232
+ // Short-circuit for empty labels.
233
+ const bodyGeom = getArrowBodyGeometry(editor, shape)
234
+ const labelCenter = bodyGeom.interpolateAlongEdge(0.5)
235
+ return { box: Box.FromCenter(labelCenter, new Vec(0, 0)), debugGeom: [] }
236
+ }
237
+
230
238
  const debugGeom: Geometry2d[] = []
231
239
  const info = getArrowInfo(editor, shape)!
232
240
 
@@ -196,6 +196,7 @@ export class FrameShapeUtil extends BaseBoxShapeUtil<TLFrameShape> {
196
196
  height,
197
197
  isFilled: true,
198
198
  isLabel: true,
199
+ excludeFromShapeBounds: true,
199
200
  }),
200
201
  ],
201
202
  })
@@ -126,6 +126,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
126
126
  height: unscaledLabelHeight * shape.props.scale,
127
127
  isFilled: true,
128
128
  isLabel: true,
129
+ excludeFromShapeBounds: true,
129
130
  isEmptyLabel: isEmptyRichText(shape.props.richText),
130
131
  }),
131
132
  ],
@@ -147,6 +147,7 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
147
147
  height: lh,
148
148
  isFilled: true,
149
149
  isLabel: true,
150
+ excludeFromShapeBounds: true,
150
151
  }),
151
152
  ],
152
153
  })
@@ -142,6 +142,7 @@ function TooltipSingleton() {
142
142
  const [isOpen, setIsOpen] = useState(false)
143
143
  const triggerRef = useRef<HTMLDivElement>(null)
144
144
  const isFirstShowRef = useRef(true)
145
+ const editor = useMaybeEditor()
145
146
 
146
147
  const currentTooltip = useValue(
147
148
  'current tooltip',
@@ -149,6 +150,15 @@ function TooltipSingleton() {
149
150
  []
150
151
  )
151
152
 
153
+ const cameraState = useValue('camera state', () => editor?.getCameraState(), [editor])
154
+
155
+ // Hide tooltip when camera is moving (panning/zooming)
156
+ useEffect(() => {
157
+ if (cameraState === 'moving' && isOpen && currentTooltip) {
158
+ tooltipManager.hideTooltip(editor, currentTooltip.id, true)
159
+ }
160
+ }, [cameraState, isOpen, currentTooltip, editor])
161
+
152
162
  // Update open state and trigger position
153
163
  useEffect(() => {
154
164
  let timer: ReturnType<typeof setTimeout> | null = null
@@ -231,12 +241,11 @@ export const TldrawUiTooltip = forwardRef<HTMLButtonElement, TldrawUiTooltipProp
231
241
  const editor = useMaybeEditor()
232
242
  const tooltipId = useRef<string>(uniqueId())
233
243
  const hasProvider = useContext(TooltipSingletonContext)
244
+ const showUiLabels = useValue('showUiLabels', () => editor?.user.getShowUiLabels(), [editor])
234
245
 
235
246
  const orientationCtx = useTldrawUiOrientation()
236
247
  const sideToUse = side ?? orientationCtx.tooltipSide
237
248
 
238
- const camera = useValue('camera', () => editor?.getCamera(), [])
239
-
240
249
  useEffect(() => {
241
250
  const currentTooltipId = tooltipId.current
242
251
  return () => {
@@ -246,17 +255,18 @@ export const TldrawUiTooltip = forwardRef<HTMLButtonElement, TldrawUiTooltipProp
246
255
  }
247
256
  }, [editor, hasProvider])
248
257
 
249
- useEffect(() => {
250
- tooltipManager.hideTooltip(editor, tooltipId.current, true)
251
- }, [editor, camera])
252
-
253
258
  // Don't show tooltip if disabled, no content, or UI labels are disabled
254
259
  if (disabled || !content) {
255
260
  return <>{children}</>
256
261
  }
257
262
 
258
- const delayDurationToUse =
259
- delayDuration ?? (editor?.options.tooltipDelayMs || DEFAULT_TOOLTIP_DELAY_MS)
263
+ let delayDurationToUse
264
+ if (showUiLabels) {
265
+ delayDurationToUse = 0
266
+ } else {
267
+ delayDurationToUse =
268
+ delayDuration ?? (editor?.options.tooltipDelayMs || DEFAULT_TOOLTIP_DELAY_MS)
269
+ }
260
270
 
261
271
  // Fallback to old behavior if no provider
262
272
  if (!hasProvider) {
@@ -213,7 +213,7 @@ export function TldrawUiMenuItem<
213
213
  icon={icon}
214
214
  onSelect={onSelect}
215
215
  onDragStart={onDragStart}
216
- labelToUse={labelToUse}
216
+ labelStr={labelStr}
217
217
  titleStr={titleStr}
218
218
  disabled={disabled}
219
219
  isSelected={isSelected}
@@ -247,7 +247,7 @@ export function TldrawUiMenuItem<
247
247
  icon={icon}
248
248
  onSelect={onSelect}
249
249
  onDragStart={onDragStart}
250
- labelToUse={labelToUse}
250
+ labelStr={labelStr}
251
251
  titleStr={titleStr}
252
252
  disabled={disabled}
253
253
  isSelected={isSelected}
@@ -392,7 +392,7 @@ function useDraggableEvents(
392
392
 
393
393
  function DraggableToolbarButton({
394
394
  id,
395
- labelToUse,
395
+ labelStr,
396
396
  titleStr,
397
397
  disabled,
398
398
  isSelected,
@@ -403,7 +403,7 @@ function DraggableToolbarButton({
403
403
  }: {
404
404
  id: string
405
405
  disabled: boolean
406
- labelToUse?: string
406
+ labelStr?: string
407
407
  titleStr?: string
408
408
  isSelected?: boolean
409
409
  icon: TLUiMenuItemProps['icon']
@@ -416,7 +416,7 @@ function DraggableToolbarButton({
416
416
  if (overflow) {
417
417
  return (
418
418
  <TldrawUiToolbarButton
419
- aria-label={labelToUse}
419
+ aria-label={labelStr}
420
420
  aria-pressed={isSelected ? 'true' : 'false'}
421
421
  isActive={isSelected}
422
422
  className="tlui-button-grid__button"
@@ -434,7 +434,7 @@ function DraggableToolbarButton({
434
434
 
435
435
  return (
436
436
  <TldrawUiToolbarButton
437
- aria-label={labelToUse}
437
+ aria-label={labelStr}
438
438
  aria-pressed={isSelected ? 'true' : 'false'}
439
439
  data-testid={`tools.${id}`}
440
440
  data-value={id}
@@ -176,7 +176,7 @@ export function ToolsProvider({ overrides, children }: TLUiToolsProviderProps) {
176
176
  editor.createShape({
177
177
  id,
178
178
  type: 'arrow',
179
- props: { start: { x: 0, y: 0 }, end: { x: 200, y: 0 } },
179
+ props: { start: { x: 0, y: 200 }, end: { x: 200, y: 0 } },
180
180
  }),
181
181
  })
182
182
  trackEvent('drag-tool', { source, id: 'arrow' })
@@ -92,9 +92,9 @@ export const DEFAULT_TRANSLATION = {
92
92
  'action.toggle-wrap-mode': 'Toggle Select on wrap',
93
93
  'action.toggle-reduce-motion.menu': 'Reduce motion',
94
94
  'action.toggle-reduce-motion': 'Toggle reduce motion',
95
- 'action.toggle-keyboard-shortcuts.menu': 'Keyboard shortcuts',
95
+ 'action.toggle-keyboard-shortcuts.menu': 'Enable keyboard shortcuts',
96
96
  'action.toggle-keyboard-shortcuts': 'Toggle keyboard shortcuts',
97
- 'action.toggle-ui-labels.menu': 'UI labels',
97
+ 'action.toggle-ui-labels.menu': 'Enable UI labels',
98
98
  'action.toggle-ui-labels': 'Toggle UI labels',
99
99
  'action.toggle-edge-scrolling.menu': 'Edge scrolling',
100
100
  'action.toggle-edge-scrolling': 'Toggle edge scrolling',
@@ -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.16.0-canary.8eb57ca47f51'
4
+ export const version = '3.16.0-canary.940daaae9993'
5
5
  export const publishDates = {
6
6
  major: '2024-09-13T14:36:29.063Z',
7
- minor: '2025-09-02T14:32:18.218Z',
8
- patch: '2025-09-02T14:32:18.218Z',
7
+ minor: '2025-09-11T09:41:08.921Z',
8
+ patch: '2025-09-11T09:41:08.921Z',
9
9
  }
@@ -1,12 +1,50 @@
1
- import { Box, TLShapeId, createShapeId } from '@tldraw/editor'
1
+ import {
2
+ BaseBoxShapeUtil,
3
+ Box,
4
+ RecordProps,
5
+ T,
6
+ TLBaseShape,
7
+ TLShapeId,
8
+ createShapeId,
9
+ } from '@tldraw/editor'
2
10
  import { vi } from 'vitest'
3
11
  import { TestEditor } from './TestEditor'
4
12
  import { TL } from './test-jsx'
5
13
 
14
+ // Custom uncullable shape type for testing canCull override
15
+ type UncullableShape = TLBaseShape<'uncullable', { w: number; h: number }>
16
+
17
+ class UncullableShapeUtil extends BaseBoxShapeUtil<UncullableShape> {
18
+ static override type = 'uncullable' as const
19
+ static override props: RecordProps<UncullableShape> = {
20
+ w: T.number,
21
+ h: T.number,
22
+ }
23
+
24
+ override canCull() {
25
+ return false
26
+ }
27
+
28
+ override getDefaultProps(): UncullableShape['props'] {
29
+ return {
30
+ w: 100,
31
+ h: 100,
32
+ }
33
+ }
34
+
35
+ override component() {
36
+ return <div>Uncullable shape</div>
37
+ }
38
+
39
+ override indicator() {
40
+ return <div>Uncullable shape</div>
41
+ }
42
+ }
43
+
6
44
  let editor: TestEditor
7
45
 
8
46
  beforeEach(() => {
9
- editor = new TestEditor()
47
+ editor = new TestEditor({ shapeUtils: [UncullableShapeUtil] })
10
48
  editor.setScreenBounds({ x: 0, y: 0, w: 1800, h: 900 })
11
49
  })
12
50
 
@@ -203,3 +241,34 @@ it('works for shapes that are outside of the viewport, but are then moved inside
203
241
  // Arrow should also not be culled
204
242
  expect(editor.getCulledShapes()).toEqual(new Set())
205
243
  })
244
+
245
+ it('respects canCull override - shapes that cannot be culled are never culled', () => {
246
+ const cullableShapeId = createShapeId()
247
+ const uncullableShapeId = createShapeId()
248
+
249
+ // Create both shapes outside the viewport
250
+ editor.createShapes([
251
+ {
252
+ id: cullableShapeId,
253
+ type: 'geo',
254
+ x: -2000, // Way outside viewport
255
+ y: -2000,
256
+ props: { w: 100, h: 100 },
257
+ },
258
+ {
259
+ id: uncullableShapeId,
260
+ type: 'uncullable',
261
+ x: -2000, // Way outside viewport
262
+ y: -2000,
263
+ props: { w: 100, h: 100 },
264
+ },
265
+ ])
266
+
267
+ const culledShapes = editor.getCulledShapes()
268
+
269
+ // The regular geo shape should be culled since it's outside the viewport
270
+ expect(culledShapes).toContain(cullableShapeId)
271
+
272
+ // The uncullable shape should NOT be culled even though it's outside the viewport
273
+ expect(culledShapes).not.toContain(uncullableShapeId)
274
+ })
package/tldraw.css CHANGED
@@ -693,11 +693,17 @@ input,
693
693
  }
694
694
 
695
695
  .tl-text-measure {
696
- position: absolute;
697
696
  z-index: var(--tl-layer-canvas-hidden);
697
+ opacity: 0;
698
+ visibility: hidden;
699
+
700
+ /* pointer-events: all; */
701
+ /* opacity: 1; */
702
+ /* z-index: 99999; */
703
+
704
+ position: absolute;
698
705
  top: 0px;
699
706
  left: 0px;
700
- opacity: 0;
701
707
  width: max-content;
702
708
  box-sizing: border-box;
703
709
  pointer-events: none;
@@ -708,7 +714,6 @@ input,
708
714
  border: none;
709
715
  user-select: none;
710
716
  contain: style paint;
711
- visibility: hidden;
712
717
  /* N.B. This property, while discouraged ("intended for Document Type Definition (DTD) designers") is necessary for ensuring correct mixed RTL/LTR behavior when exporting SVGs. */
713
718
  unicode-bidi: plaintext;
714
719
  -webkit-user-select: none;