tldraw 4.2.0-next.094f21ae35eb → 4.2.0-next.24451dedd631

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 (102) hide show
  1. package/dist-cjs/index.d.ts +3 -2
  2. package/dist-cjs/index.js +1 -1
  3. package/dist-cjs/lib/shapes/frame/components/FrameLabelInput.js +63 -36
  4. package/dist-cjs/lib/shapes/frame/components/FrameLabelInput.js.map +2 -2
  5. package/dist-cjs/lib/shapes/note/NoteShapeUtil.js +3 -3
  6. package/dist-cjs/lib/shapes/note/NoteShapeUtil.js.map +2 -2
  7. package/dist-cjs/lib/shapes/shared/RichTextLabel.js +1 -1
  8. package/dist-cjs/lib/shapes/shared/RichTextLabel.js.map +2 -2
  9. package/dist-cjs/lib/shapes/shared/ShapeFill.js +3 -0
  10. package/dist-cjs/lib/shapes/shared/ShapeFill.js.map +2 -2
  11. package/dist-cjs/lib/tools/SelectTool/childStates/DraggingHandle.js +14 -6
  12. package/dist-cjs/lib/tools/SelectTool/childStates/DraggingHandle.js.map +2 -2
  13. package/dist-cjs/lib/tools/SelectTool/childStates/Idle.js +2 -2
  14. package/dist-cjs/lib/tools/SelectTool/childStates/Idle.js.map +2 -2
  15. package/dist-cjs/lib/ui/components/DefaultDebugPanel.js +1 -1
  16. package/dist-cjs/lib/ui/components/DefaultDebugPanel.js.map +2 -2
  17. package/dist-cjs/lib/ui/components/Dialogs.js +2 -14
  18. package/dist-cjs/lib/ui/components/Dialogs.js.map +2 -2
  19. package/dist-cjs/lib/ui/components/PageMenu/DefaultPageMenu.js +5 -4
  20. package/dist-cjs/lib/ui/components/PageMenu/DefaultPageMenu.js.map +2 -2
  21. package/dist-cjs/lib/ui/components/Toolbar/DefaultRichTextToolbarContent.js +2 -1
  22. package/dist-cjs/lib/ui/components/Toolbar/DefaultRichTextToolbarContent.js.map +2 -2
  23. package/dist-cjs/lib/ui/components/Toolbar/LinkEditor.js +1 -1
  24. package/dist-cjs/lib/ui/components/Toolbar/LinkEditor.js.map +2 -2
  25. package/dist-cjs/lib/ui/components/primitives/Button/TldrawUiButton.js +2 -2
  26. package/dist-cjs/lib/ui/components/primitives/Button/TldrawUiButton.js.map +2 -2
  27. package/dist-cjs/lib/ui/context/actions.js +16 -0
  28. package/dist-cjs/lib/ui/context/actions.js.map +2 -2
  29. package/dist-cjs/lib/ui/context/events.js.map +2 -2
  30. package/dist-cjs/lib/ui/getLocalFiles.js +18 -3
  31. package/dist-cjs/lib/ui/getLocalFiles.js.map +2 -2
  32. package/dist-cjs/lib/ui/hooks/useClipboardEvents.js +18 -16
  33. package/dist-cjs/lib/ui/hooks/useClipboardEvents.js.map +3 -3
  34. package/dist-cjs/lib/ui/hooks/useTranslation/TLUiTranslationKey.js.map +1 -1
  35. package/dist-cjs/lib/ui/hooks/useTranslation/defaultTranslation.js +1 -0
  36. package/dist-cjs/lib/ui/hooks/useTranslation/defaultTranslation.js.map +2 -2
  37. package/dist-cjs/lib/ui/hooks/useTranslation/useTranslation.js +1 -0
  38. package/dist-cjs/lib/ui/hooks/useTranslation/useTranslation.js.map +2 -2
  39. package/dist-cjs/lib/ui/version.js +3 -3
  40. package/dist-cjs/lib/ui/version.js.map +1 -1
  41. package/dist-esm/index.d.mts +3 -2
  42. package/dist-esm/index.mjs +1 -1
  43. package/dist-esm/lib/shapes/frame/components/FrameLabelInput.mjs +65 -38
  44. package/dist-esm/lib/shapes/frame/components/FrameLabelInput.mjs.map +2 -2
  45. package/dist-esm/lib/shapes/note/NoteShapeUtil.mjs +5 -5
  46. package/dist-esm/lib/shapes/note/NoteShapeUtil.mjs.map +2 -2
  47. package/dist-esm/lib/shapes/shared/RichTextLabel.mjs +2 -1
  48. package/dist-esm/lib/shapes/shared/RichTextLabel.mjs.map +2 -2
  49. package/dist-esm/lib/shapes/shared/ShapeFill.mjs +3 -0
  50. package/dist-esm/lib/shapes/shared/ShapeFill.mjs.map +2 -2
  51. package/dist-esm/lib/tools/SelectTool/childStates/DraggingHandle.mjs +14 -6
  52. package/dist-esm/lib/tools/SelectTool/childStates/DraggingHandle.mjs.map +2 -2
  53. package/dist-esm/lib/tools/SelectTool/childStates/Idle.mjs +2 -2
  54. package/dist-esm/lib/tools/SelectTool/childStates/Idle.mjs.map +2 -2
  55. package/dist-esm/lib/ui/components/DefaultDebugPanel.mjs +1 -1
  56. package/dist-esm/lib/ui/components/DefaultDebugPanel.mjs.map +2 -2
  57. package/dist-esm/lib/ui/components/Dialogs.mjs +2 -14
  58. package/dist-esm/lib/ui/components/Dialogs.mjs.map +2 -2
  59. package/dist-esm/lib/ui/components/PageMenu/DefaultPageMenu.mjs +5 -5
  60. package/dist-esm/lib/ui/components/PageMenu/DefaultPageMenu.mjs.map +2 -2
  61. package/dist-esm/lib/ui/components/Toolbar/DefaultRichTextToolbarContent.mjs +2 -1
  62. package/dist-esm/lib/ui/components/Toolbar/DefaultRichTextToolbarContent.mjs.map +2 -2
  63. package/dist-esm/lib/ui/components/Toolbar/LinkEditor.mjs +2 -2
  64. package/dist-esm/lib/ui/components/Toolbar/LinkEditor.mjs.map +2 -2
  65. package/dist-esm/lib/ui/components/primitives/Button/TldrawUiButton.mjs +2 -2
  66. package/dist-esm/lib/ui/components/primitives/Button/TldrawUiButton.mjs.map +2 -2
  67. package/dist-esm/lib/ui/context/actions.mjs +16 -0
  68. package/dist-esm/lib/ui/context/actions.mjs.map +2 -2
  69. package/dist-esm/lib/ui/context/events.mjs.map +2 -2
  70. package/dist-esm/lib/ui/getLocalFiles.mjs +18 -3
  71. package/dist-esm/lib/ui/getLocalFiles.mjs.map +2 -2
  72. package/dist-esm/lib/ui/hooks/useClipboardEvents.mjs +18 -16
  73. package/dist-esm/lib/ui/hooks/useClipboardEvents.mjs.map +3 -3
  74. package/dist-esm/lib/ui/hooks/useTranslation/defaultTranslation.mjs +1 -0
  75. package/dist-esm/lib/ui/hooks/useTranslation/defaultTranslation.mjs.map +2 -2
  76. package/dist-esm/lib/ui/hooks/useTranslation/useTranslation.mjs +1 -0
  77. package/dist-esm/lib/ui/hooks/useTranslation/useTranslation.mjs.map +2 -2
  78. package/dist-esm/lib/ui/version.mjs +3 -3
  79. package/dist-esm/lib/ui/version.mjs.map +1 -1
  80. package/package.json +3 -3
  81. package/src/lib/shapes/frame/components/FrameLabelInput.tsx +48 -24
  82. package/src/lib/shapes/note/NoteShapeUtil.tsx +6 -5
  83. package/src/lib/shapes/shared/RichTextLabel.tsx +2 -1
  84. package/src/lib/shapes/shared/ShapeFill.tsx +3 -0
  85. package/src/lib/tools/SelectTool/childStates/DraggingHandle.tsx +19 -8
  86. package/src/lib/tools/SelectTool/childStates/Idle.ts +2 -2
  87. package/src/lib/ui/components/DefaultDebugPanel.tsx +1 -1
  88. package/src/lib/ui/components/Dialogs.tsx +2 -14
  89. package/src/lib/ui/components/PageMenu/DefaultPageMenu.tsx +6 -5
  90. package/src/lib/ui/components/Toolbar/DefaultRichTextToolbarContent.tsx +4 -1
  91. package/src/lib/ui/components/Toolbar/LinkEditor.tsx +2 -2
  92. package/src/lib/ui/components/primitives/Button/TldrawUiButton.tsx +3 -2
  93. package/src/lib/ui/context/actions.tsx +16 -0
  94. package/src/lib/ui/context/events.tsx +1 -0
  95. package/src/lib/ui/getLocalFiles.ts +20 -3
  96. package/src/lib/ui/hooks/useClipboardEvents.ts +12 -9
  97. package/src/lib/ui/hooks/useTranslation/TLUiTranslationKey.ts +1 -0
  98. package/src/lib/ui/hooks/useTranslation/defaultTranslation.ts +1 -0
  99. package/src/lib/ui/hooks/useTranslation/useTranslation.tsx +2 -1
  100. package/src/lib/ui/version.ts +3 -3
  101. package/src/test/TldrawEditor.test.tsx +74 -29
  102. package/src/test/customSnapping.test.tsx +185 -0
@@ -1,5 +1,8 @@
1
- import { TLFrameShape, TLShapeId, useEditor } from '@tldraw/editor'
2
- import { forwardRef, useCallback } from 'react'
1
+ import { TLFrameShape, TLShapeId, useEditor, useValue } from '@tldraw/editor'
2
+ import { forwardRef, useCallback, useEffect, useRef } from 'react'
3
+ import { PORTRAIT_BREAKPOINT } from '../../../ui/constants'
4
+ import { useBreakpoint } from '../../../ui/context/breakpoints'
5
+ import { useTranslation } from '../../../ui/hooks/useTranslation/useTranslation'
3
6
  import { defaultEmptyAs } from '../FrameShapeUtil'
4
7
 
5
8
  export const FrameLabelInput = forwardRef<
@@ -7,6 +10,15 @@ export const FrameLabelInput = forwardRef<
7
10
  { id: TLShapeId; name: string; isEditing: boolean }
8
11
  >(({ id, name, isEditing }, ref) => {
9
12
  const editor = useEditor()
13
+ const breakpoint = useBreakpoint()
14
+ const isCoarsePointer = useValue(
15
+ 'isCoarsePointer',
16
+ () => editor.getInstanceState().isCoarsePointer,
17
+ [editor]
18
+ )
19
+ const shouldUseWindowPrompt = breakpoint < PORTRAIT_BREAKPOINT.TABLET_SM && isCoarsePointer
20
+ const promptOpen = useRef<boolean>(false)
21
+ const msg = useTranslation()
10
22
 
11
23
  const handlePointerDown = useCallback(
12
24
  (e: React.PointerEvent) => {
@@ -28,13 +40,12 @@ export const FrameLabelInput = forwardRef<
28
40
  [editor]
29
41
  )
30
42
 
31
- const handleBlur = useCallback(
32
- (e: React.FocusEvent<HTMLInputElement>) => {
43
+ const renameFrame = useCallback(
44
+ (value: string) => {
33
45
  const shape = editor.getShape<TLFrameShape>(id)
34
46
  if (!shape) return
35
47
 
36
48
  const name = shape.props.name
37
- const value = e.currentTarget.value.trim()
38
49
  if (name === value) return
39
50
 
40
51
  editor.updateShapes([
@@ -48,36 +59,49 @@ export const FrameLabelInput = forwardRef<
48
59
  [id, editor]
49
60
  )
50
61
 
62
+ const handleBlur = useCallback(
63
+ (e: React.FocusEvent<HTMLInputElement>) => {
64
+ renameFrame(e.currentTarget.value)
65
+ },
66
+ [renameFrame]
67
+ )
68
+
51
69
  const handleChange = useCallback(
52
70
  (e: React.ChangeEvent<HTMLInputElement>) => {
53
- const shape = editor.getShape<TLFrameShape>(id)
54
- if (!shape) return
55
-
56
- const name = shape.props.name
57
- const value = e.currentTarget.value
58
- if (name === value) return
59
-
60
- editor.updateShapes([
61
- {
62
- id,
63
- type: 'frame',
64
- props: { name: value },
65
- },
66
- ])
71
+ renameFrame(e.currentTarget.value)
67
72
  },
68
- [id, editor]
73
+ [renameFrame]
69
74
  )
70
75
 
76
+ /* Mobile rename uses window.prompt */
77
+ useEffect(() => {
78
+ if (!isEditing) {
79
+ promptOpen.current = false
80
+ return
81
+ }
82
+ if (isEditing && shouldUseWindowPrompt && !promptOpen.current) {
83
+ promptOpen.current = true
84
+ const shape = editor.getShape<TLFrameShape>(id)
85
+ const currentName = shape?.props.name ?? ''
86
+ const newName = window.prompt(msg('action.rename'), currentName)
87
+ promptOpen.current = false
88
+ if (newName !== null) renameFrame(newName)
89
+ editor.setEditingShape(null)
90
+ }
91
+ }, [isEditing, shouldUseWindowPrompt, id, msg, renameFrame, editor])
92
+
71
93
  return (
72
- <div className={`tl-frame-label ${isEditing ? 'tl-frame-label__editing' : ''}`}>
94
+ <div
95
+ className={`tl-frame-label ${isEditing && !shouldUseWindowPrompt ? 'tl-frame-label__editing' : ''}`}
96
+ >
73
97
  <input
74
98
  className="tl-frame-name-input"
75
99
  ref={ref}
76
- disabled={!isEditing}
77
- readOnly={!isEditing}
100
+ disabled={!isEditing || shouldUseWindowPrompt}
101
+ readOnly={!isEditing || shouldUseWindowPrompt}
78
102
  style={{ display: isEditing ? undefined : 'none' }}
79
103
  value={name}
80
- autoFocus
104
+ autoFocus={!shouldUseWindowPrompt}
81
105
  onKeyDown={handleKeyDown}
82
106
  onBlur={handleBlur}
83
107
  onChange={handleChange}
@@ -31,9 +31,9 @@ import {
31
31
  useEditor,
32
32
  useValue,
33
33
  } from '@tldraw/editor'
34
- import { useCallback } from 'react'
34
+ import { useCallback, useContext } from 'react'
35
35
  import { startEditingShapeWithLabel } from '../../tools/SelectTool/selectHelpers'
36
- import { useCurrentTranslation } from '../../ui/hooks/useTranslation/useTranslation'
36
+ import { TranslationsContext } from '../../ui/hooks/useTranslation/useTranslation'
37
37
  import {
38
38
  isEmptyRichText,
39
39
  renderHtmlFromRichTextForMeasurement,
@@ -493,7 +493,8 @@ function getLabelSize(editor: Editor, shape: TLNoteShape) {
493
493
 
494
494
  function useNoteKeydownHandler(id: TLShapeId) {
495
495
  const editor = useEditor()
496
- const translation = useCurrentTranslation()
496
+ // Try to get the translation context, but fallback to ltr if it doesn't exist
497
+ const translation = useContext(TranslationsContext)
497
498
 
498
499
  return useCallback(
499
500
  (e: KeyboardEvent) => {
@@ -512,7 +513,7 @@ function useNoteKeydownHandler(id: TLShapeId) {
512
513
  // tab controls x axis (shift inverts direction set by RTL)
513
514
  // cmd enter is the y axis (shift inverts direction)
514
515
  const isRTL = !!(
515
- translation.dir === 'rtl' ||
516
+ translation?.dir === 'rtl' ||
516
517
  // todo: can we check a partial of the text, so that we don't have to render the whole thing?
517
518
  isRightToLeftLanguage(renderPlaintextFromRichText(editor, shape.props.richText))
518
519
  )
@@ -540,7 +541,7 @@ function useNoteKeydownHandler(id: TLShapeId) {
540
541
  }
541
542
  }
542
543
  },
543
- [id, editor, translation.dir]
544
+ [id, editor, translation?.dir]
544
545
  )
545
546
  }
546
547
 
@@ -8,6 +8,7 @@ import {
8
8
  TLEventInfo,
9
9
  TLRichText,
10
10
  TLShapeId,
11
+ openWindow,
11
12
  preventDefault,
12
13
  useEditor,
13
14
  useReactor,
@@ -112,7 +113,7 @@ export const RichTextLabel = React.memo(function RichTextLabel({
112
113
  if (e.name !== 'pointer_up' || !link) return
113
114
 
114
115
  if (!isDragging.current) {
115
- window.open(link, '_blank', 'noopener, noreferrer')
116
+ openWindow(link, '_blank', false)
116
117
  }
117
118
  editor.off('event', handlePointerUp)
118
119
  }
@@ -41,6 +41,9 @@ export const ShapeFill = React.memo(function ShapeFill({
41
41
  case 'pattern': {
42
42
  return <PatternFill theme={theme} color={color} fill={fill} d={d} scale={scale} />
43
43
  }
44
+ case 'lined-fill': {
45
+ return <path fill={getColorValue(theme, color, 'linedFill')} d={d} />
46
+ }
44
47
  }
45
48
  })
46
49
 
@@ -83,24 +83,35 @@ export class DraggingHandle extends StateNode {
83
83
  // Find the adjacent handle
84
84
  this.initialAdjacentHandle = null
85
85
 
86
- // Start from the handle and work forward
87
- for (let i = index + 1; i < handles.length; i++) {
88
- const handle = handles[i]
89
- if (handle.type === 'vertex' && handle.id !== 'middle' && handle.id !== info.handle.id) {
90
- this.initialAdjacentHandle = handle
91
- break
86
+ // First, check if the handle specifies a custom reference handle
87
+ if (info.handle.snapReferenceHandleId) {
88
+ const customHandle = handles.find((h) => h.id === info.handle.snapReferenceHandleId)
89
+ if (customHandle) {
90
+ this.initialAdjacentHandle = customHandle
92
91
  }
93
92
  }
94
93
 
95
- // If still no handle, start from the end and work backward
94
+ // If no custom reference handle, use default behavior
96
95
  if (!this.initialAdjacentHandle) {
97
- for (let i = handles.length - 1; i >= 0; i--) {
96
+ // Start from the handle and work forward
97
+ for (let i = index + 1; i < handles.length; i++) {
98
98
  const handle = handles[i]
99
99
  if (handle.type === 'vertex' && handle.id !== 'middle' && handle.id !== info.handle.id) {
100
100
  this.initialAdjacentHandle = handle
101
101
  break
102
102
  }
103
103
  }
104
+
105
+ // If still no handle, start from the end and work backward
106
+ if (!this.initialAdjacentHandle) {
107
+ for (let i = handles.length - 1; i >= 0; i--) {
108
+ const handle = handles[i]
109
+ if (handle.type === 'vertex' && handle.id !== 'middle' && handle.id !== info.handle.id) {
110
+ this.initialAdjacentHandle = handle
111
+ break
112
+ }
113
+ }
114
+ }
104
115
  }
105
116
 
106
117
  // <!-- Only relevant to arrows
@@ -507,7 +507,7 @@ export class Idle extends StateNode {
507
507
  }
508
508
  case 'Tab': {
509
509
  const selectedShapes = this.editor.getSelectedShapes()
510
- if (selectedShapes.length) {
510
+ if (selectedShapes.length && !info.altKey) {
511
511
  this.editor.selectAdjacentShape(info.shiftKey ? 'prev' : 'next')
512
512
  }
513
513
  break
@@ -557,7 +557,7 @@ export class Idle extends StateNode {
557
557
  }
558
558
  case 'Tab': {
559
559
  const selectedShapes = this.editor.getSelectedShapes()
560
- if (selectedShapes.length) {
560
+ if (selectedShapes.length && !info.altKey) {
561
561
  this.editor.selectAdjacentShape(info.shiftKey ? 'prev' : 'next')
562
562
  }
563
563
  break
@@ -110,7 +110,7 @@ function FPS() {
110
110
  isSlow = !isSlow
111
111
  }
112
112
 
113
- fpsRef.current!.innerHTML = `FPS ${fps.toString()} (max: ${maxKnownFps})`
113
+ fpsRef.current!.innerHTML = `FPS ${fps.toString()}`
114
114
  fpsRef.current!.className =
115
115
  `tlui-debug-panel__fps` + (isSlow ? ` tlui-debug-panel__fps__slow` : ``)
116
116
 
@@ -4,12 +4,7 @@ import { memo, useCallback, useRef } from 'react'
4
4
  import { TLUiDialog, useDialogs } from '../context/dialogs'
5
5
 
6
6
  /** @internal */
7
- const TldrawUiDialog = ({
8
- id,
9
- component: ModalContent,
10
- onClose,
11
- preventBackgroundClose,
12
- }: TLUiDialog) => {
7
+ const TldrawUiDialog = ({ id, component: ModalContent, preventBackgroundClose }: TLUiDialog) => {
13
8
  const { removeDialog } = useDialogs()
14
9
  const mouseDownInsideContentRef = useRef(false)
15
10
 
@@ -18,17 +13,10 @@ const TldrawUiDialog = ({
18
13
  const handleOpenChange = useCallback(
19
14
  (isOpen: boolean) => {
20
15
  if (!isOpen) {
21
- if (onClose) {
22
- try {
23
- onClose()
24
- } catch (err: any) {
25
- console.warn(err)
26
- }
27
- }
28
16
  removeDialog(id)
29
17
  }
30
18
  },
31
- [id, onClose, removeDialog]
19
+ [id, removeDialog]
32
20
  )
33
21
 
34
22
  return (
@@ -3,7 +3,6 @@ import {
3
3
  TLPageId,
4
4
  releasePointerCapture,
5
5
  setPointerCapture,
6
- tlenv,
7
6
  useEditor,
8
7
  useValue,
9
8
  } from '@tldraw/editor'
@@ -306,6 +305,8 @@ export const DefaultPageMenu = memo(function DefaultPageMenu() {
306
305
  [editor, trackEvent]
307
306
  )
308
307
 
308
+ const shouldUseWindowPrompt = breakpoint < PORTRAIT_BREAKPOINT.TABLET_SM && isCoarsePointer
309
+
309
310
  return (
310
311
  <TldrawUiPopover id="pages" onOpenChange={onOpenChange} open={isOpen}>
311
312
  <TldrawUiPopoverTrigger data-testid="main.page-menu">
@@ -390,7 +391,7 @@ export const DefaultPageMenu = memo(function DefaultPageMenu() {
390
391
  >
391
392
  <TldrawUiButtonIcon icon="drag-handle-dots" />
392
393
  </TldrawUiButton>
393
- {breakpoint < PORTRAIT_BREAKPOINT.TABLET_SM && isCoarsePointer ? (
394
+ {shouldUseWindowPrompt ? (
394
395
  // sigh, this is a workaround for iOS Safari
395
396
  // because the device and the radix popover seem
396
397
  // to be fighting over scroll position. Nothing
@@ -399,7 +400,7 @@ export const DefaultPageMenu = memo(function DefaultPageMenu() {
399
400
  type="normal"
400
401
  className="tlui-page-menu__item__button"
401
402
  onClick={() => {
402
- const name = window.prompt('Rename page', page.name)
403
+ const name = window.prompt(msg('action.rename'), page.name)
403
404
  if (name && name !== page.name) {
404
405
  renamePage(page.id, name)
405
406
  }
@@ -465,8 +466,8 @@ export const DefaultPageMenu = memo(function DefaultPageMenu() {
465
466
  item={page}
466
467
  listSize={pages.length}
467
468
  onRename={() => {
468
- if (tlenv.isIos) {
469
- const name = window.prompt('Rename page', page.name)
469
+ if (shouldUseWindowPrompt) {
470
+ const name = window.prompt(msg('action.rename'), page.name)
470
471
  if (name && name !== page.name) {
471
472
  renamePage(page.id, name)
472
473
  }
@@ -54,6 +54,9 @@ export function DefaultRichTextToolbarContent({
54
54
  // todo: we could make this a prop
55
55
  const actions = useMemo(() => {
56
56
  function handleOp(name: string, op: string) {
57
+ // Check if the editor view is available before calling operations
58
+ if (!textEditor.view) return
59
+
57
60
  trackEvent('rich-text', { operation: name as any, source })
58
61
  // @ts-expect-error typing this is annoying at the moment.
59
62
  textEditor.chain().focus()[op]().run()
@@ -109,7 +112,7 @@ export function DefaultRichTextToolbarContent({
109
112
  }, [textEditor, trackEvent, onEditLinkStart])
110
113
 
111
114
  return actions.map(({ name, attrs, onSelect }) => {
112
- const isActive = textEditor.isActive(name, attrs)
115
+ const isActive = textEditor.view ? textEditor.isActive(name, attrs) : false
113
116
  return (
114
117
  <TldrawUiToolbarButton
115
118
  key={name}
@@ -1,4 +1,4 @@
1
- import { preventDefault, TiptapEditor, useEditor } from '@tldraw/editor'
1
+ import { openWindow, preventDefault, TiptapEditor, useEditor } from '@tldraw/editor'
2
2
  import { useEffect, useRef, useState } from 'react'
3
3
  import { useUiEvents } from '../../context/events'
4
4
  import { useTranslation } from '../../hooks/useTranslation/useTranslation'
@@ -44,7 +44,7 @@ export function LinkEditor({ textEditor, value: initialValue, onClose }: LinkEdi
44
44
 
45
45
  const handleVisitLink = () => {
46
46
  trackEvent('rich-text', { operation: 'link-visit', source })
47
- window.open(linkifiedValue, '_blank', 'noopener, noreferrer')
47
+ openWindow(linkifiedValue, '_blank')
48
48
  onClose()
49
49
  }
50
50
 
@@ -6,6 +6,7 @@ export interface TLUiButtonProps extends React.HTMLAttributes<HTMLButtonElement>
6
6
  disabled?: boolean
7
7
  isActive?: boolean
8
8
  type: 'normal' | 'primary' | 'danger' | 'low' | 'icon' | 'tool' | 'menu' | 'help'
9
+ htmlButtonType?: 'button' | 'submit' | 'reset'
9
10
  }
10
11
 
11
12
  const namedClassNamesSoThatICanGrepForThis = {
@@ -21,11 +22,11 @@ const namedClassNamesSoThatICanGrepForThis = {
21
22
 
22
23
  /** @public @react */
23
24
  export const TldrawUiButton = React.forwardRef<HTMLButtonElement, TLUiButtonProps>(
24
- function TldrawUiButton({ children, type, isActive, ...props }, ref) {
25
+ function TldrawUiButton({ children, type, htmlButtonType, isActive, ...props }, ref) {
25
26
  return (
26
27
  <button
27
28
  ref={ref}
28
- type="button"
29
+ type={htmlButtonType || 'button'}
29
30
  draggable={false}
30
31
  data-isactive={isActive}
31
32
  {...props}
@@ -1495,6 +1495,22 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
1495
1495
  trackEvent('set-style', { source, id: style.id, value: 'fill' })
1496
1496
  },
1497
1497
  },
1498
+ {
1499
+ id: 'select-fill-lined-fill',
1500
+ label: 'fill-style.lined-fill',
1501
+ kbd: 'alt+shift+f',
1502
+ onSelect(source) {
1503
+ const style = DefaultFillStyle
1504
+ editor.run(() => {
1505
+ editor.markHistoryStoppingPoint('change-fill')
1506
+ if (editor.isIn('select')) {
1507
+ editor.setStyleForSelectedShapes(style, 'lined-fill')
1508
+ }
1509
+ editor.setStyleForNextShapes(style, 'lined-fill')
1510
+ })
1511
+ trackEvent('set-style', { source, id: style.id, value: 'lined-fill' })
1512
+ },
1513
+ },
1498
1514
  {
1499
1515
  id: 'flatten-to-image',
1500
1516
  label: 'action.flatten-to-image',
@@ -24,6 +24,7 @@ export type TLUiEventSource =
24
24
  | 'rich-text-menu'
25
25
  | 'image-toolbar'
26
26
  | 'video-toolbar'
27
+ | 'fairy-panel'
27
28
  | 'unknown'
28
29
 
29
30
  /** @public */
@@ -9,17 +9,34 @@ export function getLocalFiles(options?: {
9
9
  input.type = 'file'
10
10
  input.accept = mimeTypes?.join(',')
11
11
  input.multiple = allowMultiple
12
+ input.style.display = 'none'
13
+
14
+ function dispose() {
15
+ input.removeEventListener('change', onchange)
16
+ input.removeEventListener('cancel', oncancel)
17
+ input.remove()
18
+ }
12
19
 
13
20
  async function onchange(e: Event) {
14
21
  const fileList = (e.target as HTMLInputElement).files
15
- if (!fileList || fileList.length === 0) return
22
+ if (!fileList || fileList.length === 0) {
23
+ resolve([])
24
+ dispose()
25
+ return
26
+ }
16
27
  const files = Array.from(fileList)
17
28
  input.value = ''
18
29
  resolve(files)
19
- input.removeEventListener('change', onchange)
20
- input.remove()
30
+ dispose()
31
+ }
32
+
33
+ function oncancel() {
34
+ resolve([])
35
+ dispose()
21
36
  }
22
37
 
38
+ document.body.appendChild(input)
39
+ input.addEventListener('cancel', oncancel)
23
40
  input.addEventListener('change', onchange)
24
41
  input?.click()
25
42
  })
@@ -586,6 +586,8 @@ async function handleClipboardThings(editor: Editor, things: ClipboardThing[], p
586
586
  * @public
587
587
  */
588
588
  const handleNativeOrMenuCopy = async (editor: Editor) => {
589
+ const navigator =
590
+ editor.getContainer().ownerDocument?.defaultView?.navigator ?? globalThis.navigator
589
591
  const content = await editor.resolveAssetsInContent(
590
592
  editor.getContentFromCurrentPage(editor.getSelectedShapeIds())
591
593
  )
@@ -713,6 +715,7 @@ export function useMenuClipboardEvents() {
713
715
  /** @public */
714
716
  export function useNativeClipboardEvents() {
715
717
  const editor = useEditor()
718
+ const ownerDocument = editor.getContainer().ownerDocument
716
719
  const trackEvent = useUiEvents()
717
720
 
718
721
  const appIsFocused = useValue('editor.isFocused', () => editor.getInstanceState().isFocused, [
@@ -817,16 +820,16 @@ export function useNativeClipboardEvents() {
817
820
  trackEvent('paste', { source: 'kbd' })
818
821
  }
819
822
 
820
- document.addEventListener('copy', copy)
821
- document.addEventListener('cut', cut)
822
- document.addEventListener('paste', paste)
823
- document.addEventListener('pointerup', pointerUpHandler)
823
+ ownerDocument?.addEventListener('copy', copy)
824
+ ownerDocument?.addEventListener('cut', cut)
825
+ ownerDocument?.addEventListener('paste', paste)
826
+ ownerDocument?.addEventListener('pointerup', pointerUpHandler)
824
827
 
825
828
  return () => {
826
- document.removeEventListener('copy', copy)
827
- document.removeEventListener('cut', cut)
828
- document.removeEventListener('paste', paste)
829
- document.removeEventListener('pointerup', pointerUpHandler)
829
+ ownerDocument?.removeEventListener('copy', copy)
830
+ ownerDocument?.removeEventListener('cut', cut)
831
+ ownerDocument?.removeEventListener('paste', paste)
832
+ ownerDocument?.removeEventListener('pointerup', pointerUpHandler)
830
833
  }
831
- }, [editor, trackEvent, appIsFocused])
834
+ }, [editor, trackEvent, appIsFocused, ownerDocument])
832
835
  }
@@ -154,6 +154,7 @@ export type TLUiTranslationKey =
154
154
  | 'fill-style.solid'
155
155
  | 'fill-style.pattern'
156
156
  | 'fill-style.fill'
157
+ | 'fill-style.lined-fill'
157
158
  | 'dash-style.dashed'
158
159
  | 'dash-style.dotted'
159
160
  | 'dash-style.draw'
@@ -155,6 +155,7 @@ export const DEFAULT_TRANSLATION = {
155
155
  'fill-style.solid': 'Solid',
156
156
  'fill-style.pattern': 'Pattern',
157
157
  'fill-style.fill': 'Fill',
158
+ 'fill-style.lined-fill': 'Lined fill',
158
159
  'dash-style.dashed': 'Dashed',
159
160
  'dash-style.dotted': 'Dotted',
160
161
  'dash-style.draw': 'Draw',
@@ -23,7 +23,8 @@ export interface TLUiTranslationProviderProps {
23
23
  /** @public */
24
24
  export type TLUiTranslationContextType = TLUiTranslation
25
25
 
26
- const TranslationsContext = React.createContext<TLUiTranslationContextType | null>(null)
26
+ /** @internal */
27
+ export const TranslationsContext = React.createContext<TLUiTranslationContextType | null>(null)
27
28
 
28
29
  /** @public */
29
30
  export function useCurrentTranslation() {
@@ -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 = '4.2.0-next.094f21ae35eb'
4
+ export const version = '4.2.0-next.24451dedd631'
5
5
  export const publishDates = {
6
6
  major: '2025-09-18T14:39:22.803Z',
7
- minor: '2025-10-22T14:17:08.191Z',
8
- patch: '2025-10-22T14:17:08.191Z',
7
+ minor: '2025-11-14T12:20:57.684Z',
8
+ patch: '2025-11-14T12:20:57.684Z',
9
9
  }