tldraw 5.3.1 → 5.4.0-canary.042da660fa66

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 (99) hide show
  1. package/DOCS.md +2822 -3224
  2. package/RELEASE_NOTES.md +21 -33
  3. package/dist-cjs/index.d.ts +264 -275
  4. package/dist-cjs/index.js +1 -1
  5. package/dist-cjs/lib/defaultExternalContentHandlers.js +2 -2
  6. package/dist-cjs/lib/defaultExternalContentHandlers.js.map +2 -2
  7. package/dist-cjs/lib/shapes/draw/toolStates/Drawing.js +2 -2
  8. package/dist-cjs/lib/shapes/draw/toolStates/Drawing.js.map +2 -2
  9. package/dist-cjs/lib/shapes/frame/frameHelpers.js +3 -0
  10. package/dist-cjs/lib/shapes/frame/frameHelpers.js.map +2 -2
  11. package/dist-cjs/lib/shapes/video/VideoShapeUtil.js +3 -2
  12. package/dist-cjs/lib/shapes/video/VideoShapeUtil.js.map +2 -2
  13. package/dist-cjs/lib/ui/TldrawUi.js +2 -3
  14. package/dist-cjs/lib/ui/TldrawUi.js.map +2 -2
  15. package/dist-cjs/lib/ui/components/ActionsMenu/DefaultActionsMenu.js +0 -3
  16. package/dist-cjs/lib/ui/components/ActionsMenu/DefaultActionsMenu.js.map +2 -2
  17. package/dist-cjs/lib/ui/components/ContextMenu/DefaultContextMenuContent.js +1 -1
  18. package/dist-cjs/lib/ui/components/ContextMenu/DefaultContextMenuContent.js.map +2 -2
  19. package/dist-cjs/lib/ui/components/Dialogs.js +8 -0
  20. package/dist-cjs/lib/ui/components/Dialogs.js.map +2 -2
  21. package/dist-cjs/lib/ui/components/SharePanel/DefaultPeopleMenuItem.js +1 -2
  22. package/dist-cjs/lib/ui/components/SharePanel/DefaultPeopleMenuItem.js.map +2 -2
  23. package/dist-cjs/lib/ui/context/actions.js +0 -1
  24. package/dist-cjs/lib/ui/context/actions.js.map +2 -2
  25. package/dist-cjs/lib/ui/hooks/useClipboardEvents.js +2 -0
  26. package/dist-cjs/lib/ui/hooks/useClipboardEvents.js.map +2 -2
  27. package/dist-cjs/lib/ui/hooks/useCommentingEnabled.js +1 -1
  28. package/dist-cjs/lib/ui/hooks/useCommentingEnabled.js.map +2 -2
  29. package/dist-cjs/lib/ui/hooks/useFlatten.js +42 -23
  30. package/dist-cjs/lib/ui/hooks/useFlatten.js.map +2 -2
  31. package/dist-cjs/lib/ui/hooks/useKeyboardShortcuts.js +5 -17
  32. package/dist-cjs/lib/ui/hooks/useKeyboardShortcuts.js.map +2 -2
  33. package/dist-cjs/lib/ui/kbd-utils.js +22 -3
  34. package/dist-cjs/lib/ui/kbd-utils.js.map +2 -2
  35. package/dist-cjs/lib/ui/version.js +3 -3
  36. package/dist-cjs/lib/ui/version.js.map +1 -1
  37. package/dist-esm/index.d.mts +264 -275
  38. package/dist-esm/index.mjs +1 -1
  39. package/dist-esm/lib/defaultExternalContentHandlers.mjs +2 -2
  40. package/dist-esm/lib/defaultExternalContentHandlers.mjs.map +2 -2
  41. package/dist-esm/lib/shapes/draw/toolStates/Drawing.mjs +2 -2
  42. package/dist-esm/lib/shapes/draw/toolStates/Drawing.mjs.map +2 -2
  43. package/dist-esm/lib/shapes/frame/frameHelpers.mjs +3 -0
  44. package/dist-esm/lib/shapes/frame/frameHelpers.mjs.map +2 -2
  45. package/dist-esm/lib/shapes/video/VideoShapeUtil.mjs +3 -2
  46. package/dist-esm/lib/shapes/video/VideoShapeUtil.mjs.map +2 -2
  47. package/dist-esm/lib/ui/TldrawUi.mjs +2 -3
  48. package/dist-esm/lib/ui/TldrawUi.mjs.map +2 -2
  49. package/dist-esm/lib/ui/components/ActionsMenu/DefaultActionsMenu.mjs +2 -5
  50. package/dist-esm/lib/ui/components/ActionsMenu/DefaultActionsMenu.mjs.map +2 -2
  51. package/dist-esm/lib/ui/components/ContextMenu/DefaultContextMenuContent.mjs +1 -1
  52. package/dist-esm/lib/ui/components/ContextMenu/DefaultContextMenuContent.mjs.map +2 -2
  53. package/dist-esm/lib/ui/components/Dialogs.mjs +9 -1
  54. package/dist-esm/lib/ui/components/Dialogs.mjs.map +2 -2
  55. package/dist-esm/lib/ui/components/SharePanel/DefaultPeopleMenuItem.mjs +1 -2
  56. package/dist-esm/lib/ui/components/SharePanel/DefaultPeopleMenuItem.mjs.map +2 -2
  57. package/dist-esm/lib/ui/context/actions.mjs +0 -1
  58. package/dist-esm/lib/ui/context/actions.mjs.map +2 -2
  59. package/dist-esm/lib/ui/hooks/useClipboardEvents.mjs +2 -0
  60. package/dist-esm/lib/ui/hooks/useClipboardEvents.mjs.map +2 -2
  61. package/dist-esm/lib/ui/hooks/useCommentingEnabled.mjs +2 -2
  62. package/dist-esm/lib/ui/hooks/useCommentingEnabled.mjs.map +2 -2
  63. package/dist-esm/lib/ui/hooks/useFlatten.mjs +42 -23
  64. package/dist-esm/lib/ui/hooks/useFlatten.mjs.map +2 -2
  65. package/dist-esm/lib/ui/hooks/useKeyboardShortcuts.mjs +5 -17
  66. package/dist-esm/lib/ui/hooks/useKeyboardShortcuts.mjs.map +2 -2
  67. package/dist-esm/lib/ui/kbd-utils.mjs +22 -3
  68. package/dist-esm/lib/ui/kbd-utils.mjs.map +2 -2
  69. package/dist-esm/lib/ui/version.mjs +3 -3
  70. package/dist-esm/lib/ui/version.mjs.map +1 -1
  71. package/package.json +12 -6
  72. package/src/lib/defaultExternalContentHandlers.ts +2 -2
  73. package/src/lib/shapes/draw/DrawShapeTool.test.ts +19 -0
  74. package/src/lib/shapes/draw/toolStates/Drawing.ts +2 -2
  75. package/src/lib/shapes/frame/frameHelpers.ts +10 -1
  76. package/src/lib/shapes/video/VideoShapeUtil.tsx +4 -2
  77. package/src/lib/ui/TldrawUi.tsx +1 -2
  78. package/src/lib/ui/components/ActionsMenu/DefaultActionsMenu.tsx +2 -6
  79. package/src/lib/ui/components/ContextMenu/DefaultContextMenuContent.tsx +5 -1
  80. package/src/lib/ui/components/Dialogs.tsx +17 -1
  81. package/src/lib/ui/components/SharePanel/DefaultPeopleMenuItem.tsx +2 -3
  82. package/src/lib/ui/context/actions.tsx +0 -1
  83. package/src/lib/ui/hooks/useClipboardEvents.ts +2 -0
  84. package/src/lib/ui/hooks/useCommentingEnabled.ts +5 -3
  85. package/src/lib/ui/hooks/useFlatten.ts +56 -23
  86. package/src/lib/ui/hooks/useKeyboardShortcuts.ts +5 -23
  87. package/src/lib/ui/kbd-utils.test.ts +11 -0
  88. package/src/lib/ui/kbd-utils.ts +27 -4
  89. package/src/lib/ui/version.ts +3 -3
  90. package/src/lib/ui.css +7 -16
  91. package/src/test/TldrawEditor.test.tsx +3 -0
  92. package/src/test/VideoShapeUtil.test.tsx +126 -0
  93. package/src/test/bookmark-shapes.test.ts +110 -0
  94. package/src/test/commands/alignShapes.test.tsx +19 -10
  95. package/src/test/commands/clipboard.test.ts +36 -1
  96. package/src/test/flatten.test.ts +135 -0
  97. package/src/test/modifiers.test.ts +104 -0
  98. package/src/test/ui-outside-editor.test.tsx +74 -0
  99. package/tldraw.css +7 -16
@@ -0,0 +1,135 @@
1
+ import {
2
+ AssetRecordType,
3
+ TLImageAsset,
4
+ TLShapeId,
5
+ TLShapePartial,
6
+ createShapeId,
7
+ } from '@tldraw/editor'
8
+ import { vi } from 'vitest'
9
+ import { flattenShapesToImages } from '../lib/ui/hooks/useFlatten'
10
+ import { TestEditor } from './TestEditor'
11
+
12
+ let editor: TestEditor
13
+ let assetCount = 0
14
+
15
+ function installFlattenMocks(target: TestEditor) {
16
+ // flattenShapesToImages calls editor.getSvgString and
17
+ // editor.getAssetForExternalContent. Mock both to bypass the real export
18
+ // pipeline (jsdom can't render SVGs and we don't need real pixels — these
19
+ // tests only care about which shapes end up in which group).
20
+ vi.spyOn(target, 'getSvgString').mockResolvedValue({
21
+ svg: '<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"></svg>',
22
+ width: 10,
23
+ height: 10,
24
+ trimPadding: 0,
25
+ })
26
+ vi.spyOn(target, 'getAssetForExternalContent').mockImplementation(async () => {
27
+ const asset: TLImageAsset = {
28
+ id: AssetRecordType.createId(`flat-${assetCount++}`),
29
+ typeName: 'asset',
30
+ type: 'image',
31
+ props: {
32
+ w: 10,
33
+ h: 10,
34
+ name: 'asset.svg',
35
+ isAnimated: false,
36
+ mimeType: 'image/svg+xml',
37
+ src: 'data:image/svg+xml;base64,',
38
+ },
39
+ meta: {},
40
+ }
41
+ return asset
42
+ })
43
+ }
44
+
45
+ beforeEach(() => {
46
+ editor = new TestEditor()
47
+ assetCount = 0
48
+ installFlattenMocks(editor)
49
+ })
50
+
51
+ afterEach(() => {
52
+ editor?.dispose()
53
+ })
54
+
55
+ const box = (id: TLShapeId, x: number, y: number, w = 50, h = 50): TLShapePartial => ({
56
+ id,
57
+ type: 'geo',
58
+ x,
59
+ y,
60
+ props: { w, h, fill: 'solid' },
61
+ })
62
+
63
+ function getImageShapeCount() {
64
+ return editor.getCurrentPageShapes().filter((s) => s.type === 'image').length
65
+ }
66
+
67
+ describe('flattenShapesToImages clustering', () => {
68
+ const expand = 64
69
+
70
+ it('groups shapes within the expand threshold into one image', async () => {
71
+ const a = createShapeId('a')
72
+ const b = createShapeId('b')
73
+ // gap of 30 < 2 * 64 expand, so the expanded boxes intersect
74
+ editor.createShapes([box(a, 0, 0), box(b, 80, 0)])
75
+
76
+ await flattenShapesToImages(editor, [a, b], expand)
77
+
78
+ expect(getImageShapeCount()).toBe(1)
79
+ })
80
+
81
+ it('keeps shapes in separate images when their gap exceeds the expand threshold', async () => {
82
+ const a = createShapeId('a')
83
+ const b = createShapeId('b')
84
+ // gap of 200 > 2 * 64 expand, so the expanded boxes do not intersect
85
+ editor.createShapes([box(a, 0, 0), box(b, 250, 0)])
86
+
87
+ await flattenShapesToImages(editor, [a, b], expand)
88
+
89
+ expect(getImageShapeCount()).toBe(2)
90
+ })
91
+
92
+ it('merges all shapes into one group when a bridging shape connects them, independent of input order', async () => {
93
+ // Layout: A — gap — bridge — gap — C. A and C alone do not intersect
94
+ // each other's expanded bounds, but `bridge` sits in the middle and
95
+ // should pull them all into a single group regardless of where it
96
+ // appears in the list. The previous greedy pass would only merge
97
+ // `bridge` into the first group it touched, leaving C as a separate
98
+ // cluster when `bridge` came after C in the list — which made the
99
+ // result depend on selection order.
100
+ const a = createShapeId('a')
101
+ const bridge = createShapeId('bridge')
102
+ const c = createShapeId('c')
103
+
104
+ const orders: TLShapeId[][] = [
105
+ [a, bridge, c],
106
+ [a, c, bridge],
107
+ [bridge, a, c],
108
+ [c, bridge, a],
109
+ ]
110
+
111
+ for (const order of orders) {
112
+ editor.dispose()
113
+ editor = new TestEditor()
114
+ assetCount = 0
115
+ installFlattenMocks(editor)
116
+ editor.createShapes([box(a, 0, 0), box(bridge, 100, 0), box(c, 200, 0)])
117
+
118
+ await flattenShapesToImages(editor, order, expand)
119
+
120
+ expect(getImageShapeCount(), `order ${order.join(',')}`).toBe(1)
121
+ }
122
+ })
123
+
124
+ it('uses a single Box.Common group when no expand value is provided', async () => {
125
+ // Two shapes far apart should still flatten to one image when called
126
+ // via the no-expand path (the default useFlatten() hook).
127
+ const a = createShapeId('a')
128
+ const b = createShapeId('b')
129
+ editor.createShapes([box(a, 0, 0), box(b, 1000, 0)])
130
+
131
+ await flattenShapesToImages(editor, [a, b])
132
+
133
+ expect(getImageShapeCount()).toBe(1)
134
+ })
135
+ })
@@ -37,6 +37,110 @@ it('Ctrl Key', () => {
37
37
  expect(editor.inputs.getCtrlKey()).toBe(false)
38
38
  })
39
39
 
40
+ it('pointer down releases a modifier still in its debounce window', () => {
41
+ // Collect any synthetic key_up events fired when the release is flushed.
42
+ const keyUps: string[] = []
43
+ editor.on('event', (info) => {
44
+ if (info.type === 'keyboard' && info.name === 'key_up') keyUps.push(info.key)
45
+ })
46
+
47
+ // Hold then release meta via pointer events; the release is debounced for 150ms.
48
+ editor.pointerMove(100, 100, { metaKey: true })
49
+ editor.pointerMove(100, 100, { metaKey: false })
50
+ expect(editor.inputs.getMetaKey()).toBe(true) // still held during the debounce window
51
+
52
+ // A new interaction starts with the key physically up: the lingering modifier
53
+ // should be released immediately (same path as the timer), including its
54
+ // synthetic key_up, without waiting for the timeout.
55
+ editor.pointerDown(100, 100, { metaKey: false })
56
+ expect(editor.inputs.getMetaKey()).toBe(false)
57
+ expect(keyUps).toEqual(['Meta'])
58
+
59
+ // The pending timer must have been cancelled, so advancing past it does nothing
60
+ // and doesn't fire the synthetic key_up a second time.
61
+ vi.advanceTimersByTime(200)
62
+ expect(editor.inputs.getMetaKey()).toBe(false)
63
+ expect(keyUps).toEqual(['Meta'])
64
+ })
65
+
66
+ it('pointer down releases every modifier still in its debounce window', () => {
67
+ const keyUps: string[] = []
68
+ editor.on('event', (info) => {
69
+ if (info.type === 'keyboard' && info.name === 'key_up') keyUps.push(info.key)
70
+ })
71
+
72
+ editor.pointerMove(100, 100, { shiftKey: true, altKey: true, ctrlKey: true, metaKey: true })
73
+ editor.pointerMove(100, 100, { shiftKey: false, altKey: false, ctrlKey: false, metaKey: false })
74
+ expect(editor.inputs.getShiftKey()).toBe(true)
75
+ expect(editor.inputs.getAltKey()).toBe(true)
76
+ expect(editor.inputs.getCtrlKey()).toBe(true)
77
+ expect(editor.inputs.getMetaKey()).toBe(true)
78
+
79
+ editor.pointerDown(100, 100, { shiftKey: false, altKey: false, ctrlKey: false, metaKey: false })
80
+ expect(editor.inputs.getShiftKey()).toBe(false)
81
+ expect(editor.inputs.getAltKey()).toBe(false)
82
+ expect(editor.inputs.getCtrlKey()).toBe(false)
83
+ expect(editor.inputs.getMetaKey()).toBe(false)
84
+
85
+ // Each flushed modifier fires its synthetic key_up so tools can react.
86
+ expect(keyUps).toEqual(expect.arrayContaining(['Shift', 'Alt', 'Ctrl', 'Meta']))
87
+ expect(keyUps).toHaveLength(4)
88
+
89
+ // All four timers were cancelled, so nothing flips back or fires again.
90
+ vi.advanceTimersByTime(200)
91
+ expect(editor.inputs.getShiftKey()).toBe(false)
92
+ expect(editor.inputs.getAltKey()).toBe(false)
93
+ expect(editor.inputs.getCtrlKey()).toBe(false)
94
+ expect(editor.inputs.getMetaKey()).toBe(false)
95
+ expect(keyUps).toHaveLength(4)
96
+ })
97
+
98
+ it('pointer down clears stale key codes when flushing a debounced modifier', () => {
99
+ // A real key_down records the code, then the release is only seen via pointer
100
+ // flags (no keyboard key_up), so the code lingers in inputs.keys until flushed.
101
+ editor.keyDown('Shift')
102
+ expect(editor.inputs.keys.has('ShiftLeft')).toBe(true)
103
+
104
+ editor.pointerMove(100, 100, { shiftKey: false })
105
+ expect(editor.inputs.getShiftKey()).toBe(true) // debounced, still counted as held
106
+ expect(editor.inputs.keys.has('ShiftLeft')).toBe(true) // code not yet cleared
107
+
108
+ // The pointer down flushes the debounce through the timer's path, which clears
109
+ // both the modifier atom and the stale code.
110
+ editor.pointerDown(100, 100, { shiftKey: false })
111
+ expect(editor.inputs.getShiftKey()).toBe(false)
112
+ expect(editor.inputs.keys.has('ShiftLeft')).toBe(false)
113
+ })
114
+
115
+ it('does not re-release a modifier after its natural debounce timer fired', () => {
116
+ const keyUps: string[] = []
117
+ editor.on('event', (info) => {
118
+ if (info.type === 'keyboard' && info.name === 'key_up') keyUps.push(info.key)
119
+ })
120
+
121
+ // Hold then release shift via pointer events and let the 150ms timer fire on its
122
+ // own, which releases shift with a single synthetic key_up.
123
+ editor.pointerMove(100, 100, { shiftKey: true })
124
+ editor.pointerMove(100, 100, { shiftKey: false })
125
+ vi.advanceTimersByTime(200)
126
+ expect(editor.inputs.getShiftKey()).toBe(false)
127
+ expect(keyUps).toEqual(['Shift'])
128
+
129
+ // A later pointer down must not treat the already-fired timer as an active
130
+ // debounce and emit another synthetic key_up.
131
+ editor.pointerDown(100, 100, { shiftKey: false })
132
+ expect(keyUps).toEqual(['Shift'])
133
+ })
134
+
135
+ it('pointer down keeps a genuinely held modifier', () => {
136
+ // Shift is actually held during the pointer down (flag stays true), so it must
137
+ // not be cleared by the debounce-flush.
138
+ editor.pointerMove(100, 100, { shiftKey: true })
139
+ expect(editor.inputs.getShiftKey()).toBe(true)
140
+ editor.pointerDown(100, 100, { shiftKey: true })
141
+ expect(editor.inputs.getShiftKey()).toBe(true)
142
+ })
143
+
40
144
  it('keyboard events carry currently-held modifiers', () => {
41
145
  const keyboardEvents: TLKeyboardEventInfo[] = []
42
146
  editor.on('event', (info) => {
@@ -0,0 +1,74 @@
1
+ import { render } from '@testing-library/react'
2
+ import { ContainerProvider, EditorProvider, useLicenseContext } from '@tldraw/editor'
3
+ import { vi } from 'vitest'
4
+ import { Tldraw } from '../lib/Tldraw'
5
+ import { DefaultToolbar } from '../lib/ui/components/Toolbar/DefaultToolbar'
6
+ import { TldrawUiContextProvider } from '../lib/ui/context/TldrawUiContextProvider'
7
+ import { useCommentingEnabled } from '../lib/ui/hooks/useCommentingEnabled'
8
+ import { renderTldrawComponentWithEditor } from './testutils/renderTldrawComponent'
9
+
10
+ // Default UI components can be mounted beside <Tldraw /> rather than inside it, wrapped in the
11
+ // providers the caller recreates (container, editor, ui context). The license provider is not
12
+ // among them — it only exists inside <TldrawEditor />.
13
+ describe('default UI mounted outside <Tldraw />', () => {
14
+ it('renders the default toolbar and resolves license feature flags through the editor', async () => {
15
+ const { editor } = await renderTldrawComponentWithEditor(
16
+ (onMount) => <Tldraw onMount={onMount} components={{ Toolbar: null }} />,
17
+ { waitForPatterns: true }
18
+ )
19
+
20
+ expect(editor.licenseManager).toBeDefined()
21
+
22
+ let commentingEnabled: boolean | undefined
23
+ function CommentingProbe() {
24
+ commentingEnabled = useCommentingEnabled()
25
+ return null
26
+ }
27
+
28
+ const result = render(
29
+ <ContainerProvider container={editor.getContainer()}>
30
+ <EditorProvider editor={editor}>
31
+ <TldrawUiContextProvider>
32
+ <DefaultToolbar />
33
+ <CommentingProbe />
34
+ </TldrawUiContextProvider>
35
+ </EditorProvider>
36
+ </ContainerProvider>
37
+ )
38
+
39
+ expect(await result.findByTestId('tools.select')).toBeTruthy()
40
+ // In tests the environment counts as development, where every feature is licensed — so a
41
+ // `true` here proves the flag resolved via the editor's license manager, not the
42
+ // fail-closed `false` of having no license manager at all.
43
+ expect(commentingEnabled).toBe(true)
44
+ })
45
+
46
+ it('license feature flags fail closed with no editor and no license context', () => {
47
+ let commentingEnabled: boolean | undefined
48
+ function CommentingProbe() {
49
+ commentingEnabled = useCommentingEnabled()
50
+ return null
51
+ }
52
+
53
+ render(<CommentingProbe />)
54
+
55
+ expect(commentingEnabled).toBe(false)
56
+ })
57
+
58
+ it('useLicenseContext throws when there is no license provider and no editor', () => {
59
+ function LicenseProbe() {
60
+ useLicenseContext()
61
+ return null
62
+ }
63
+
64
+ // silence React's error boundary logging for the expected throw
65
+ const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
66
+ try {
67
+ expect(() => render(<LicenseProbe />)).toThrow(
68
+ 'useLicenseContext must be used inside of the <Tldraw /> or <TldrawEditor /> components'
69
+ )
70
+ } finally {
71
+ consoleError.mockRestore()
72
+ }
73
+ })
74
+ })
package/tldraw.css CHANGED
@@ -3726,7 +3726,8 @@ tldraw? probably.
3726
3726
  align-items: center;
3727
3727
  }
3728
3728
 
3729
- .tlui-people-menu__user__color {
3729
+ .tlui-people-menu__user__color,
3730
+ .tlui-people-menu__user__edit {
3730
3731
  flex-shrink: 0;
3731
3732
  }
3732
3733
 
@@ -3772,7 +3773,8 @@ tldraw? probably.
3772
3773
  }
3773
3774
 
3774
3775
  .tlui-people-menu__item {
3775
- position: relative;
3776
+ display: flex;
3777
+ align-items: center;
3776
3778
  }
3777
3779
 
3778
3780
  .tlui-people-menu__item:last-of-type .tlui-button__menu {
@@ -3780,7 +3782,8 @@ tldraw? probably.
3780
3782
  }
3781
3783
 
3782
3784
  .tlui-people-menu__item__button {
3783
- padding: 0 11px;
3785
+ padding-inline-start: 12px;
3786
+ padding-inline-end: 0px;
3784
3787
  overflow: hidden;
3785
3788
  }
3786
3789
 
@@ -3790,7 +3793,7 @@ tldraw? probably.
3790
3793
  align-items: auto;
3791
3794
  justify-content: flex-start;
3792
3795
  flex-grow: 2;
3793
- gap: 11px;
3796
+ gap: 12px;
3794
3797
  }
3795
3798
 
3796
3799
  .tlui-people-menu__name {
@@ -3801,18 +3804,9 @@ tldraw? probably.
3801
3804
  }
3802
3805
 
3803
3806
  .tlui-people-menu__item__follow {
3804
- position: absolute;
3805
- top: 0px;
3806
- inset-inline-end: 0px;
3807
- max-width: 40px;
3808
3807
  flex-shrink: 0;
3809
3808
  }
3810
3809
 
3811
- .tlui-people-menu__item[data-follow='true'],
3812
- .tlui-people-menu__item:has(.tlui-button:focus-visible) {
3813
- padding-inline-end: 36px;
3814
- }
3815
-
3816
3810
  .tlui-people-menu__item[data-follow='true'] .tlui-people-menu__item__follow,
3817
3811
  .tlui-people-menu__item:has(.tlui-button:focus-visible) .tlui-people-menu__item__follow {
3818
3812
  opacity: 1;
@@ -3823,9 +3817,6 @@ tldraw? probably.
3823
3817
  opacity: 0;
3824
3818
  }
3825
3819
 
3826
- .tlui-people-menu__item:hover {
3827
- padding-right: 36px;
3828
- }
3829
3820
  .tlui-people-menu__item:hover .tlui-people-menu__item__follow {
3830
3821
  opacity: 1;
3831
3822
  }