@tldraw/editor 3.14.0-canary.d926f92ca8d6 → 3.14.0-canary.db789786fb06

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 (237) hide show
  1. package/dist-cjs/index.d.ts +212 -117
  2. package/dist-cjs/index.js +11 -8
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/config/TLSessionStateSnapshot.js +1 -12
  5. package/dist-cjs/lib/config/TLSessionStateSnapshot.js.map +3 -3
  6. package/dist-cjs/lib/editor/Editor.js +131 -99
  7. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  8. package/dist-cjs/lib/editor/bindings/BindingUtil.js.map +2 -2
  9. package/dist-cjs/lib/editor/derivations/bindingsIndex.js +22 -22
  10. package/dist-cjs/lib/editor/derivations/bindingsIndex.js.map +2 -2
  11. package/dist-cjs/lib/editor/derivations/parentsToChildren.js +16 -16
  12. package/dist-cjs/lib/editor/derivations/parentsToChildren.js.map +2 -2
  13. package/dist-cjs/lib/editor/managers/{ClickManager.js → ClickManager/ClickManager.js} +1 -1
  14. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js.map +7 -0
  15. package/dist-cjs/lib/editor/managers/{EdgeScrollManager.js → EdgeScrollManager/EdgeScrollManager.js} +2 -2
  16. package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js.map +7 -0
  17. package/dist-cjs/lib/editor/managers/FocusManager/FocusManager.js.map +7 -0
  18. package/dist-cjs/lib/editor/managers/{FontManager.js → FontManager/FontManager.js} +4 -1
  19. package/dist-cjs/lib/editor/managers/FontManager/FontManager.js.map +7 -0
  20. package/dist-cjs/lib/editor/managers/{HistoryManager.js → HistoryManager/HistoryManager.js} +67 -7
  21. package/dist-cjs/lib/editor/managers/HistoryManager/HistoryManager.js.map +7 -0
  22. package/dist-cjs/lib/editor/managers/{ScribbleManager.js → ScribbleManager/ScribbleManager.js} +1 -1
  23. package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js.map +7 -0
  24. package/dist-cjs/lib/editor/managers/{TextManager.js → TextManager/TextManager.js} +73 -42
  25. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +7 -0
  26. package/dist-cjs/lib/editor/managers/{TickManager.js → TickManager/TickManager.js} +1 -1
  27. package/dist-cjs/lib/editor/managers/TickManager/TickManager.js.map +7 -0
  28. package/dist-cjs/lib/editor/managers/{UserPreferencesManager.js → UserPreferencesManager/UserPreferencesManager.js} +1 -1
  29. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +7 -0
  30. package/dist-cjs/lib/editor/shapes/ShapeUtil.js +0 -10
  31. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  32. package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js +1 -1
  33. package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js.map +1 -1
  34. package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js +10 -6
  35. package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js.map +3 -3
  36. package/dist-cjs/lib/editor/tools/StateNode.js +3 -3
  37. package/dist-cjs/lib/editor/tools/StateNode.js.map +2 -2
  38. package/dist-cjs/lib/editor/types/emit-types.js.map +1 -1
  39. package/dist-cjs/lib/editor/types/external-content.js.map +1 -1
  40. package/dist-cjs/lib/exports/getSvgJsx.js.map +1 -1
  41. package/dist-cjs/lib/hooks/useCanvasEvents.js +1 -2
  42. package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
  43. package/dist-cjs/lib/primitives/Box.js +33 -33
  44. package/dist-cjs/lib/primitives/Box.js.map +2 -2
  45. package/dist-cjs/lib/primitives/Vec.js +13 -8
  46. package/dist-cjs/lib/primitives/Vec.js.map +2 -2
  47. package/dist-cjs/lib/primitives/geometry/Arc2d.js +41 -21
  48. package/dist-cjs/lib/primitives/geometry/Arc2d.js.map +2 -2
  49. package/dist-cjs/lib/primitives/geometry/Circle2d.js +11 -11
  50. package/dist-cjs/lib/primitives/geometry/Circle2d.js.map +2 -2
  51. package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js +13 -16
  52. package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js.map +2 -2
  53. package/dist-cjs/lib/primitives/geometry/CubicSpline2d.js +4 -4
  54. package/dist-cjs/lib/primitives/geometry/CubicSpline2d.js.map +2 -2
  55. package/dist-cjs/lib/primitives/geometry/Edge2d.js +14 -17
  56. package/dist-cjs/lib/primitives/geometry/Edge2d.js.map +2 -2
  57. package/dist-cjs/lib/primitives/geometry/Ellipse2d.js +10 -10
  58. package/dist-cjs/lib/primitives/geometry/Ellipse2d.js.map +2 -2
  59. package/dist-cjs/lib/primitives/geometry/Geometry2d.js +6 -2
  60. package/dist-cjs/lib/primitives/geometry/Geometry2d.js.map +2 -2
  61. package/dist-cjs/lib/primitives/geometry/Point2d.js +6 -6
  62. package/dist-cjs/lib/primitives/geometry/Point2d.js.map +2 -2
  63. package/dist-cjs/lib/primitives/geometry/Polygon2d.js +3 -0
  64. package/dist-cjs/lib/primitives/geometry/Polygon2d.js.map +2 -2
  65. package/dist-cjs/lib/primitives/geometry/Polyline2d.js +8 -5
  66. package/dist-cjs/lib/primitives/geometry/Polyline2d.js.map +2 -2
  67. package/dist-cjs/lib/primitives/geometry/Rectangle2d.js +22 -11
  68. package/dist-cjs/lib/primitives/geometry/Rectangle2d.js.map +2 -2
  69. package/dist-cjs/lib/primitives/geometry/Stadium2d.js +22 -22
  70. package/dist-cjs/lib/primitives/geometry/Stadium2d.js.map +2 -2
  71. package/dist-cjs/lib/utils/reorderShapes.js +11 -10
  72. package/dist-cjs/lib/utils/reorderShapes.js.map +2 -2
  73. package/dist-cjs/lib/utils/reparenting.js +232 -0
  74. package/dist-cjs/lib/utils/reparenting.js.map +7 -0
  75. package/dist-cjs/lib/utils/richText.js +7 -2
  76. package/dist-cjs/lib/utils/richText.js.map +2 -2
  77. package/dist-cjs/version.js +3 -3
  78. package/dist-cjs/version.js.map +1 -1
  79. package/dist-esm/index.d.mts +212 -117
  80. package/dist-esm/index.mjs +15 -8
  81. package/dist-esm/index.mjs.map +2 -2
  82. package/dist-esm/lib/config/TLSessionStateSnapshot.mjs +1 -1
  83. package/dist-esm/lib/config/TLSessionStateSnapshot.mjs.map +2 -2
  84. package/dist-esm/lib/editor/Editor.mjs +131 -99
  85. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  86. package/dist-esm/lib/editor/bindings/BindingUtil.mjs.map +2 -2
  87. package/dist-esm/lib/editor/derivations/bindingsIndex.mjs +22 -22
  88. package/dist-esm/lib/editor/derivations/bindingsIndex.mjs.map +2 -2
  89. package/dist-esm/lib/editor/derivations/parentsToChildren.mjs +16 -16
  90. package/dist-esm/lib/editor/derivations/parentsToChildren.mjs.map +2 -2
  91. package/dist-esm/lib/editor/managers/{ClickManager.mjs → ClickManager/ClickManager.mjs} +1 -1
  92. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs.map +7 -0
  93. package/dist-esm/lib/editor/managers/{EdgeScrollManager.mjs → EdgeScrollManager/EdgeScrollManager.mjs} +2 -2
  94. package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs.map +7 -0
  95. package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs.map +7 -0
  96. package/dist-esm/lib/editor/managers/{FontManager.mjs → FontManager/FontManager.mjs} +4 -1
  97. package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs.map +7 -0
  98. package/dist-esm/lib/editor/managers/{HistoryManager.mjs → HistoryManager/HistoryManager.mjs} +63 -3
  99. package/dist-esm/lib/editor/managers/HistoryManager/HistoryManager.mjs.map +7 -0
  100. package/dist-esm/lib/editor/managers/{ScribbleManager.mjs → ScribbleManager/ScribbleManager.mjs} +1 -1
  101. package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs.map +7 -0
  102. package/dist-esm/lib/editor/managers/{TextManager.mjs → TextManager/TextManager.mjs} +73 -42
  103. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +7 -0
  104. package/dist-esm/lib/editor/managers/{TickManager.mjs → TickManager/TickManager.mjs} +1 -1
  105. package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs.map +7 -0
  106. package/dist-esm/lib/editor/managers/{UserPreferencesManager.mjs → UserPreferencesManager/UserPreferencesManager.mjs} +1 -1
  107. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +7 -0
  108. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +0 -10
  109. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  110. package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs +1 -1
  111. package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs.map +1 -1
  112. package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs +10 -6
  113. package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs.map +3 -3
  114. package/dist-esm/lib/editor/tools/StateNode.mjs +3 -3
  115. package/dist-esm/lib/editor/tools/StateNode.mjs.map +2 -2
  116. package/dist-esm/lib/exports/getSvgJsx.mjs.map +1 -1
  117. package/dist-esm/lib/hooks/useCanvasEvents.mjs +1 -2
  118. package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
  119. package/dist-esm/lib/primitives/Box.mjs +33 -33
  120. package/dist-esm/lib/primitives/Box.mjs.map +2 -2
  121. package/dist-esm/lib/primitives/Vec.mjs +13 -8
  122. package/dist-esm/lib/primitives/Vec.mjs.map +2 -2
  123. package/dist-esm/lib/primitives/geometry/Arc2d.mjs +41 -21
  124. package/dist-esm/lib/primitives/geometry/Arc2d.mjs.map +2 -2
  125. package/dist-esm/lib/primitives/geometry/Circle2d.mjs +11 -11
  126. package/dist-esm/lib/primitives/geometry/Circle2d.mjs.map +2 -2
  127. package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs +13 -16
  128. package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs.map +2 -2
  129. package/dist-esm/lib/primitives/geometry/CubicSpline2d.mjs +4 -4
  130. package/dist-esm/lib/primitives/geometry/CubicSpline2d.mjs.map +2 -2
  131. package/dist-esm/lib/primitives/geometry/Edge2d.mjs +14 -17
  132. package/dist-esm/lib/primitives/geometry/Edge2d.mjs.map +2 -2
  133. package/dist-esm/lib/primitives/geometry/Ellipse2d.mjs +11 -11
  134. package/dist-esm/lib/primitives/geometry/Ellipse2d.mjs.map +2 -2
  135. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs +6 -2
  136. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs.map +2 -2
  137. package/dist-esm/lib/primitives/geometry/Point2d.mjs +6 -6
  138. package/dist-esm/lib/primitives/geometry/Point2d.mjs.map +2 -2
  139. package/dist-esm/lib/primitives/geometry/Polygon2d.mjs +3 -0
  140. package/dist-esm/lib/primitives/geometry/Polygon2d.mjs.map +2 -2
  141. package/dist-esm/lib/primitives/geometry/Polyline2d.mjs +8 -5
  142. package/dist-esm/lib/primitives/geometry/Polyline2d.mjs.map +2 -2
  143. package/dist-esm/lib/primitives/geometry/Rectangle2d.mjs +22 -11
  144. package/dist-esm/lib/primitives/geometry/Rectangle2d.mjs.map +2 -2
  145. package/dist-esm/lib/primitives/geometry/Stadium2d.mjs +22 -22
  146. package/dist-esm/lib/primitives/geometry/Stadium2d.mjs.map +2 -2
  147. package/dist-esm/lib/utils/reorderShapes.mjs +11 -10
  148. package/dist-esm/lib/utils/reorderShapes.mjs.map +2 -2
  149. package/dist-esm/lib/utils/reparenting.mjs +216 -0
  150. package/dist-esm/lib/utils/reparenting.mjs.map +7 -0
  151. package/dist-esm/lib/utils/richText.mjs +8 -3
  152. package/dist-esm/lib/utils/richText.mjs.map +2 -2
  153. package/dist-esm/version.mjs +3 -3
  154. package/dist-esm/version.mjs.map +1 -1
  155. package/editor.css +442 -492
  156. package/package.json +8 -9
  157. package/src/index.ts +20 -7
  158. package/src/lib/config/TLSessionStateSnapshot.ts +1 -1
  159. package/src/lib/editor/Editor.test.ts +252 -3
  160. package/src/lib/editor/Editor.ts +149 -107
  161. package/src/lib/editor/bindings/BindingUtil.ts +6 -0
  162. package/src/lib/editor/derivations/bindingsIndex.ts +27 -26
  163. package/src/lib/editor/derivations/parentsToChildren.ts +28 -25
  164. package/src/lib/editor/managers/ClickManager/ClickManager.test.ts +442 -0
  165. package/src/lib/editor/managers/{ClickManager.ts → ClickManager/ClickManager.ts} +3 -3
  166. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.test.ts +374 -0
  167. package/src/lib/editor/managers/{EdgeScrollManager.ts → EdgeScrollManager/EdgeScrollManager.ts} +3 -3
  168. package/src/lib/editor/managers/FocusManager/FocusManager.test.ts +455 -0
  169. package/src/lib/editor/managers/{FocusManager.ts → FocusManager/FocusManager.ts} +1 -1
  170. package/src/lib/editor/managers/FontManager/FontManager.test.ts +263 -0
  171. package/src/lib/editor/managers/{FontManager.ts → FontManager/FontManager.ts} +5 -2
  172. package/src/lib/editor/managers/{HistoryManager.test.ts → HistoryManager/HistoryManager.test.ts} +388 -1
  173. package/src/lib/editor/managers/{HistoryManager.ts → HistoryManager/HistoryManager.ts} +76 -3
  174. package/src/lib/editor/managers/ScribbleManager/ScribbleManager.test.ts +624 -0
  175. package/src/lib/editor/managers/{ScribbleManager.ts → ScribbleManager/ScribbleManager.ts} +2 -2
  176. package/src/lib/editor/managers/SnapManager/SnapManager.test.ts +485 -0
  177. package/src/lib/editor/managers/TextManager/TextManager.test.ts +407 -0
  178. package/src/lib/editor/managers/{TextManager.ts → TextManager/TextManager.ts} +119 -87
  179. package/src/lib/editor/managers/TickManager/TickManager.test.ts +314 -0
  180. package/src/lib/editor/managers/{TickManager.ts → TickManager/TickManager.ts} +2 -2
  181. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +591 -0
  182. package/src/lib/editor/managers/{UserPreferencesManager.ts → UserPreferencesManager/UserPreferencesManager.ts} +2 -2
  183. package/src/lib/editor/shapes/ShapeUtil.ts +48 -16
  184. package/src/lib/editor/shapes/group/GroupShapeUtil.tsx +1 -1
  185. package/src/lib/editor/tools/BaseBoxShapeTool/children/Pointing.ts +22 -17
  186. package/src/lib/editor/tools/StateNode.ts +3 -3
  187. package/src/lib/editor/types/emit-types.ts +4 -0
  188. package/src/lib/editor/types/external-content.ts +11 -2
  189. package/src/lib/exports/getSvgJsx.tsx +1 -1
  190. package/src/lib/hooks/useCanvasEvents.ts +0 -1
  191. package/src/lib/primitives/Box.test.ts +588 -7
  192. package/src/lib/primitives/Box.ts +33 -33
  193. package/src/lib/primitives/Vec.test.ts +2 -2
  194. package/src/lib/primitives/Vec.ts +13 -8
  195. package/src/lib/primitives/geometry/Arc2d.ts +42 -23
  196. package/src/lib/primitives/geometry/Circle2d.ts +12 -12
  197. package/src/lib/primitives/geometry/CubicBezier2d.test.ts +5 -0
  198. package/src/lib/primitives/geometry/CubicBezier2d.ts +13 -17
  199. package/src/lib/primitives/geometry/CubicSpline2d.ts +5 -5
  200. package/src/lib/primitives/geometry/Edge2d.ts +14 -18
  201. package/src/lib/primitives/geometry/Ellipse2d.ts +12 -13
  202. package/src/lib/primitives/geometry/Geometry2d.ts +7 -2
  203. package/src/lib/primitives/geometry/Point2d.ts +6 -6
  204. package/src/lib/primitives/geometry/Polygon2d.ts +4 -0
  205. package/src/lib/primitives/geometry/Polyline2d.ts +10 -7
  206. package/src/lib/primitives/geometry/Rectangle2d.ts +24 -11
  207. package/src/lib/primitives/geometry/Stadium2d.ts +22 -23
  208. package/src/lib/utils/reorderShapes.ts +10 -13
  209. package/src/lib/utils/reparenting.ts +383 -0
  210. package/src/lib/utils/richText.ts +10 -4
  211. package/src/version.ts +3 -3
  212. package/dist-cjs/lib/editor/managers/ClickManager.js.map +0 -7
  213. package/dist-cjs/lib/editor/managers/EdgeScrollManager.js.map +0 -7
  214. package/dist-cjs/lib/editor/managers/FocusManager.js.map +0 -7
  215. package/dist-cjs/lib/editor/managers/FontManager.js.map +0 -7
  216. package/dist-cjs/lib/editor/managers/HistoryManager.js.map +0 -7
  217. package/dist-cjs/lib/editor/managers/ScribbleManager.js.map +0 -7
  218. package/dist-cjs/lib/editor/managers/Stack.js +0 -82
  219. package/dist-cjs/lib/editor/managers/Stack.js.map +0 -7
  220. package/dist-cjs/lib/editor/managers/TextManager.js.map +0 -7
  221. package/dist-cjs/lib/editor/managers/TickManager.js.map +0 -7
  222. package/dist-cjs/lib/editor/managers/UserPreferencesManager.js.map +0 -7
  223. package/dist-esm/lib/editor/managers/ClickManager.mjs.map +0 -7
  224. package/dist-esm/lib/editor/managers/EdgeScrollManager.mjs.map +0 -7
  225. package/dist-esm/lib/editor/managers/FocusManager.mjs.map +0 -7
  226. package/dist-esm/lib/editor/managers/FontManager.mjs.map +0 -7
  227. package/dist-esm/lib/editor/managers/HistoryManager.mjs.map +0 -7
  228. package/dist-esm/lib/editor/managers/ScribbleManager.mjs.map +0 -7
  229. package/dist-esm/lib/editor/managers/Stack.mjs +0 -62
  230. package/dist-esm/lib/editor/managers/Stack.mjs.map +0 -7
  231. package/dist-esm/lib/editor/managers/TextManager.mjs.map +0 -7
  232. package/dist-esm/lib/editor/managers/TickManager.mjs.map +0 -7
  233. package/dist-esm/lib/editor/managers/UserPreferencesManager.mjs.map +0 -7
  234. package/src/lib/editor/managers/ScribbleManager.test.ts +0 -32
  235. package/src/lib/editor/managers/Stack.ts +0 -71
  236. /package/dist-cjs/lib/editor/managers/{FocusManager.js → FocusManager/FocusManager.js} +0 -0
  237. /package/dist-esm/lib/editor/managers/{FocusManager.mjs → FocusManager/FocusManager.mjs} +0 -0
@@ -0,0 +1,591 @@
1
+ import { atom } from '@tldraw/state'
2
+ import { TLUserPreferences, defaultUserPreferences } from '../../../config/TLUserPreferences'
3
+ import { TLUser } from '../../../config/createTLUser'
4
+ import { UserPreferencesManager } from './UserPreferencesManager'
5
+
6
+ // Mock window.matchMedia
7
+ const mockMatchMedia = jest.fn()
8
+ Object.defineProperty(window, 'matchMedia', {
9
+ writable: true,
10
+ value: mockMatchMedia,
11
+ })
12
+
13
+ describe('UserPreferencesManager', () => {
14
+ let mockUser: jest.Mocked<TLUser>
15
+ let mockUserPreferences: TLUserPreferences
16
+ let userPreferencesAtom: any
17
+ let userPreferencesManager: UserPreferencesManager
18
+
19
+ const createMockUserPreferences = (
20
+ overrides: Partial<TLUserPreferences> = {}
21
+ ): TLUserPreferences => ({
22
+ id: 'test-user-id',
23
+ name: 'Test User',
24
+ color: '#FF802B',
25
+ locale: 'en',
26
+ animationSpeed: 1,
27
+ edgeScrollSpeed: 1,
28
+ colorScheme: 'light',
29
+ isSnapMode: false,
30
+ isWrapMode: false,
31
+ isDynamicSizeMode: false,
32
+ isPasteAtCursorMode: false,
33
+ ...overrides,
34
+ })
35
+
36
+ beforeEach(() => {
37
+ jest.clearAllMocks()
38
+
39
+ mockUserPreferences = createMockUserPreferences()
40
+ userPreferencesAtom = atom('userPreferences', mockUserPreferences)
41
+
42
+ mockUser = {
43
+ userPreferences: userPreferencesAtom,
44
+ setUserPreferences: jest.fn((prefs) => {
45
+ userPreferencesAtom.set(prefs)
46
+ }),
47
+ }
48
+
49
+ // Default matchMedia mock - no dark mode preference
50
+ mockMatchMedia.mockReturnValue({
51
+ matches: false,
52
+ addEventListener: jest.fn(),
53
+ removeEventListener: jest.fn(),
54
+ })
55
+ })
56
+
57
+ describe('constructor', () => {
58
+ it('should initialize with light system color scheme when matchMedia not available', () => {
59
+ // Test when window.matchMedia is not available
60
+ delete (window as any).matchMedia
61
+
62
+ userPreferencesManager = new UserPreferencesManager(mockUser, false)
63
+
64
+ expect(userPreferencesManager.systemColorScheme.get()).toBe('light')
65
+
66
+ // Restore matchMedia
67
+ Object.defineProperty(window, 'matchMedia', {
68
+ writable: true,
69
+ value: mockMatchMedia,
70
+ })
71
+ })
72
+
73
+ it('should initialize with light system color scheme when dark mode not preferred', () => {
74
+ mockMatchMedia.mockReturnValue({
75
+ matches: false,
76
+ addEventListener: jest.fn(),
77
+ removeEventListener: jest.fn(),
78
+ })
79
+
80
+ userPreferencesManager = new UserPreferencesManager(mockUser, false)
81
+
82
+ expect(userPreferencesManager.systemColorScheme.get()).toBe('light')
83
+ })
84
+
85
+ it('should initialize with dark system color scheme when dark mode preferred', () => {
86
+ mockMatchMedia.mockReturnValue({
87
+ matches: true,
88
+ addEventListener: jest.fn(),
89
+ removeEventListener: jest.fn(),
90
+ })
91
+
92
+ userPreferencesManager = new UserPreferencesManager(mockUser, false)
93
+
94
+ expect(userPreferencesManager.systemColorScheme.get()).toBe('dark')
95
+ })
96
+
97
+ it('should set up media query listener for color scheme changes', () => {
98
+ const mockAddEventListener = jest.fn()
99
+ const mockRemoveEventListener = jest.fn()
100
+
101
+ mockMatchMedia.mockReturnValue({
102
+ matches: false,
103
+ addEventListener: mockAddEventListener,
104
+ removeEventListener: mockRemoveEventListener,
105
+ })
106
+
107
+ userPreferencesManager = new UserPreferencesManager(mockUser, false)
108
+
109
+ expect(mockAddEventListener).toHaveBeenCalledWith('change', expect.any(Function))
110
+ })
111
+
112
+ it('should handle media query change events', () => {
113
+ const mockAddEventListener = jest.fn()
114
+ let changeHandler: (e: MediaQueryListEvent) => void
115
+
116
+ mockMatchMedia.mockReturnValue({
117
+ matches: false,
118
+ addEventListener: (event: string, handler: any) => {
119
+ if (event === 'change') {
120
+ changeHandler = handler
121
+ }
122
+ mockAddEventListener(event, handler)
123
+ },
124
+ removeEventListener: jest.fn(),
125
+ })
126
+
127
+ userPreferencesManager = new UserPreferencesManager(mockUser, false)
128
+
129
+ expect(userPreferencesManager.systemColorScheme.get()).toBe('light')
130
+
131
+ // Simulate dark mode change
132
+ changeHandler!({ matches: true } as MediaQueryListEvent)
133
+ expect(userPreferencesManager.systemColorScheme.get()).toBe('dark')
134
+
135
+ // Simulate light mode change
136
+ changeHandler!({ matches: false } as MediaQueryListEvent)
137
+ expect(userPreferencesManager.systemColorScheme.get()).toBe('light')
138
+ })
139
+
140
+ it('should work in server environment (no window)', () => {
141
+ const originalWindow = global.window
142
+ delete (global as any).window
143
+
144
+ expect(() => {
145
+ userPreferencesManager = new UserPreferencesManager(mockUser, false)
146
+ }).not.toThrow()
147
+
148
+ global.window = originalWindow
149
+ })
150
+ })
151
+
152
+ describe('dispose', () => {
153
+ it('should remove media query listener on dispose', () => {
154
+ const mockRemoveEventListener = jest.fn()
155
+
156
+ mockMatchMedia.mockReturnValue({
157
+ matches: false,
158
+ addEventListener: jest.fn(),
159
+ removeEventListener: mockRemoveEventListener,
160
+ })
161
+
162
+ userPreferencesManager = new UserPreferencesManager(mockUser, false)
163
+ userPreferencesManager.dispose()
164
+
165
+ expect(mockRemoveEventListener).toHaveBeenCalledWith('change', expect.any(Function))
166
+ })
167
+
168
+ it('should call all disposables', () => {
169
+ userPreferencesManager = new UserPreferencesManager(mockUser, false)
170
+
171
+ const mockDisposable1 = jest.fn()
172
+ const mockDisposable2 = jest.fn()
173
+
174
+ userPreferencesManager.disposables.add(mockDisposable1)
175
+ userPreferencesManager.disposables.add(mockDisposable2)
176
+
177
+ userPreferencesManager.dispose()
178
+
179
+ expect(mockDisposable1).toHaveBeenCalled()
180
+ expect(mockDisposable2).toHaveBeenCalled()
181
+ })
182
+ })
183
+
184
+ describe('updateUserPreferences', () => {
185
+ beforeEach(() => {
186
+ userPreferencesManager = new UserPreferencesManager(mockUser, false)
187
+ })
188
+
189
+ it('should update user preferences with partial data', () => {
190
+ const updates = { name: 'Updated Name', color: '#EC5E41' }
191
+
192
+ userPreferencesManager.updateUserPreferences(updates)
193
+
194
+ expect(mockUser.setUserPreferences).toHaveBeenCalledWith({
195
+ ...mockUserPreferences,
196
+ ...updates,
197
+ })
198
+ })
199
+
200
+ it('should preserve existing preferences when updating', () => {
201
+ const updates = { animationSpeed: 0.5 }
202
+
203
+ userPreferencesManager.updateUserPreferences(updates)
204
+
205
+ expect(mockUser.setUserPreferences).toHaveBeenCalledWith({
206
+ ...mockUserPreferences,
207
+ animationSpeed: 0.5,
208
+ })
209
+ })
210
+
211
+ it('should handle empty updates', () => {
212
+ userPreferencesManager.updateUserPreferences({})
213
+
214
+ expect(mockUser.setUserPreferences).toHaveBeenCalledWith(mockUserPreferences)
215
+ })
216
+ })
217
+
218
+ describe('getUserPreferences', () => {
219
+ beforeEach(() => {
220
+ userPreferencesManager = new UserPreferencesManager(mockUser, false)
221
+ })
222
+
223
+ it('should return complete user preferences with computed values', () => {
224
+ const result = userPreferencesManager.getUserPreferences()
225
+
226
+ expect(result).toEqual({
227
+ id: mockUserPreferences.id,
228
+ name: mockUserPreferences.name,
229
+ locale: mockUserPreferences.locale,
230
+ color: mockUserPreferences.color,
231
+ animationSpeed: mockUserPreferences.animationSpeed,
232
+ isSnapMode: mockUserPreferences.isSnapMode,
233
+ colorScheme: mockUserPreferences.colorScheme,
234
+ isDarkMode: false, // light mode
235
+ isWrapMode: mockUserPreferences.isWrapMode,
236
+ isDynamicResizeMode: mockUserPreferences.isDynamicSizeMode,
237
+ })
238
+ })
239
+
240
+ it('should use default values for missing properties', () => {
241
+ const minimalPrefs: TLUserPreferences = { id: 'test-id' }
242
+ userPreferencesAtom.set(minimalPrefs)
243
+
244
+ const result = userPreferencesManager.getUserPreferences()
245
+
246
+ expect(result.name).toBe(defaultUserPreferences.name)
247
+ expect(result.color).toBe(defaultUserPreferences.color)
248
+ expect(result.locale).toBe(defaultUserPreferences.locale)
249
+ expect(result.animationSpeed).toBe(defaultUserPreferences.animationSpeed)
250
+ })
251
+ })
252
+
253
+ describe('getIsDarkMode', () => {
254
+ beforeEach(() => {
255
+ userPreferencesManager = new UserPreferencesManager(mockUser, false)
256
+ })
257
+
258
+ it('should return true when colorScheme is dark', () => {
259
+ userPreferencesAtom.set({ ...mockUserPreferences, colorScheme: 'dark' })
260
+
261
+ expect(userPreferencesManager.getIsDarkMode()).toBe(true)
262
+ })
263
+
264
+ it('should return false when colorScheme is light', () => {
265
+ userPreferencesAtom.set({ ...mockUserPreferences, colorScheme: 'light' })
266
+
267
+ expect(userPreferencesManager.getIsDarkMode()).toBe(false)
268
+ })
269
+
270
+ it('should follow system preference when colorScheme is system', () => {
271
+ userPreferencesAtom.set({ ...mockUserPreferences, colorScheme: 'system' })
272
+
273
+ // System is light
274
+ userPreferencesManager.systemColorScheme.set('light')
275
+ expect(userPreferencesManager.getIsDarkMode()).toBe(false)
276
+
277
+ // System is dark
278
+ userPreferencesManager.systemColorScheme.set('dark')
279
+ expect(userPreferencesManager.getIsDarkMode()).toBe(true)
280
+ })
281
+
282
+ it('should use inferDarkMode when colorScheme is undefined', () => {
283
+ userPreferencesAtom.set({ ...mockUserPreferences, colorScheme: undefined })
284
+
285
+ // With inferDarkMode = true
286
+ const managerWithInfer = new UserPreferencesManager(mockUser, true)
287
+ managerWithInfer.systemColorScheme.set('dark')
288
+ expect(managerWithInfer.getIsDarkMode()).toBe(true)
289
+
290
+ // With inferDarkMode = false
291
+ const managerWithoutInfer = new UserPreferencesManager(mockUser, false)
292
+ managerWithoutInfer.systemColorScheme.set('dark')
293
+ expect(managerWithoutInfer.getIsDarkMode()).toBe(false)
294
+ })
295
+ })
296
+
297
+ describe('individual preference getters', () => {
298
+ beforeEach(() => {
299
+ userPreferencesManager = new UserPreferencesManager(mockUser, false)
300
+ })
301
+
302
+ describe('getId', () => {
303
+ it('should return user id', () => {
304
+ expect(userPreferencesManager.getId()).toBe(mockUserPreferences.id)
305
+ })
306
+ })
307
+
308
+ describe('getName', () => {
309
+ it('should return trimmed user name', () => {
310
+ userPreferencesAtom.set({ ...mockUserPreferences, name: ' Test User ' })
311
+ expect(userPreferencesManager.getName()).toBe('Test User')
312
+ })
313
+
314
+ it('should return default name when name is null', () => {
315
+ userPreferencesAtom.set({ ...mockUserPreferences, name: null })
316
+ expect(userPreferencesManager.getName()).toBe(defaultUserPreferences.name)
317
+ })
318
+
319
+ it('should return default name when name is undefined', () => {
320
+ userPreferencesAtom.set({ ...mockUserPreferences, name: undefined })
321
+ expect(userPreferencesManager.getName()).toBe(defaultUserPreferences.name)
322
+ })
323
+
324
+ it('should return default name when name is empty after trimming', () => {
325
+ userPreferencesAtom.set({ ...mockUserPreferences, name: ' ' })
326
+ expect(userPreferencesManager.getName()).toBe(defaultUserPreferences.name)
327
+ })
328
+ })
329
+
330
+ describe('getLocale', () => {
331
+ it('should return user locale', () => {
332
+ expect(userPreferencesManager.getLocale()).toBe(mockUserPreferences.locale)
333
+ })
334
+
335
+ it('should return default locale when locale is null', () => {
336
+ userPreferencesAtom.set({ ...mockUserPreferences, locale: null })
337
+ expect(userPreferencesManager.getLocale()).toBe(defaultUserPreferences.locale)
338
+ })
339
+ })
340
+
341
+ describe('getColor', () => {
342
+ it('should return user color', () => {
343
+ expect(userPreferencesManager.getColor()).toBe(mockUserPreferences.color)
344
+ })
345
+
346
+ it('should return default color when color is null', () => {
347
+ userPreferencesAtom.set({ ...mockUserPreferences, color: null })
348
+ expect(userPreferencesManager.getColor()).toBe(defaultUserPreferences.color)
349
+ })
350
+ })
351
+
352
+ describe('getAnimationSpeed', () => {
353
+ it('should return user animation speed', () => {
354
+ expect(userPreferencesManager.getAnimationSpeed()).toBe(mockUserPreferences.animationSpeed)
355
+ })
356
+
357
+ it('should return default animation speed when null', () => {
358
+ userPreferencesAtom.set({ ...mockUserPreferences, animationSpeed: null })
359
+ expect(userPreferencesManager.getAnimationSpeed()).toBe(
360
+ defaultUserPreferences.animationSpeed
361
+ )
362
+ })
363
+ })
364
+
365
+ describe('getEdgeScrollSpeed', () => {
366
+ it('should return user edge scroll speed', () => {
367
+ expect(userPreferencesManager.getEdgeScrollSpeed()).toBe(
368
+ mockUserPreferences.edgeScrollSpeed
369
+ )
370
+ })
371
+
372
+ it('should return default edge scroll speed when null', () => {
373
+ userPreferencesAtom.set({ ...mockUserPreferences, edgeScrollSpeed: null })
374
+ expect(userPreferencesManager.getEdgeScrollSpeed()).toBe(
375
+ defaultUserPreferences.edgeScrollSpeed
376
+ )
377
+ })
378
+ })
379
+
380
+ describe('getIsSnapMode', () => {
381
+ it('should return user snap mode setting', () => {
382
+ expect(userPreferencesManager.getIsSnapMode()).toBe(mockUserPreferences.isSnapMode)
383
+ })
384
+
385
+ it('should return default snap mode when null', () => {
386
+ userPreferencesAtom.set({ ...mockUserPreferences, isSnapMode: null })
387
+ expect(userPreferencesManager.getIsSnapMode()).toBe(defaultUserPreferences.isSnapMode)
388
+ })
389
+ })
390
+
391
+ describe('getIsWrapMode', () => {
392
+ it('should return user wrap mode setting', () => {
393
+ expect(userPreferencesManager.getIsWrapMode()).toBe(mockUserPreferences.isWrapMode)
394
+ })
395
+
396
+ it('should return default wrap mode when null', () => {
397
+ userPreferencesAtom.set({ ...mockUserPreferences, isWrapMode: null })
398
+ expect(userPreferencesManager.getIsWrapMode()).toBe(defaultUserPreferences.isWrapMode)
399
+ })
400
+ })
401
+
402
+ describe('getIsDynamicResizeMode', () => {
403
+ it('should return user dynamic resize mode setting', () => {
404
+ expect(userPreferencesManager.getIsDynamicResizeMode()).toBe(
405
+ mockUserPreferences.isDynamicSizeMode
406
+ )
407
+ })
408
+
409
+ it('should return default dynamic resize mode when null', () => {
410
+ userPreferencesAtom.set({ ...mockUserPreferences, isDynamicSizeMode: null })
411
+ expect(userPreferencesManager.getIsDynamicResizeMode()).toBe(
412
+ defaultUserPreferences.isDynamicSizeMode
413
+ )
414
+ })
415
+ })
416
+
417
+ describe('getIsPasteAtCursorMode', () => {
418
+ it('should return user paste at cursor mode setting', () => {
419
+ expect(userPreferencesManager.getIsPasteAtCursorMode()).toBe(
420
+ mockUserPreferences.isPasteAtCursorMode
421
+ )
422
+ })
423
+
424
+ it('should return default paste at cursor mode when null', () => {
425
+ userPreferencesAtom.set({ ...mockUserPreferences, isPasteAtCursorMode: null })
426
+ expect(userPreferencesManager.getIsPasteAtCursorMode()).toBe(
427
+ defaultUserPreferences.isPasteAtCursorMode
428
+ )
429
+ })
430
+ })
431
+ })
432
+
433
+ describe('reactive behavior', () => {
434
+ beforeEach(() => {
435
+ userPreferencesManager = new UserPreferencesManager(mockUser, false)
436
+ })
437
+
438
+ it('should react to user preferences changes', () => {
439
+ expect(userPreferencesManager.getName()).toBe('Test User')
440
+
441
+ userPreferencesManager.updateUserPreferences({ name: 'Updated User' })
442
+
443
+ expect(userPreferencesManager.getName()).toBe('Updated User')
444
+ })
445
+
446
+ it('should react to system color scheme changes', () => {
447
+ userPreferencesAtom.set({ ...mockUserPreferences, colorScheme: 'system' })
448
+
449
+ expect(userPreferencesManager.getIsDarkMode()).toBe(false)
450
+
451
+ userPreferencesManager.systemColorScheme.set('dark')
452
+
453
+ expect(userPreferencesManager.getIsDarkMode()).toBe(true)
454
+ })
455
+
456
+ it('should update getUserPreferences when individual preferences change', () => {
457
+ const initialPrefs = userPreferencesManager.getUserPreferences()
458
+ expect(initialPrefs.name).toBe('Test User')
459
+
460
+ userPreferencesManager.updateUserPreferences({ name: 'Changed Name' })
461
+
462
+ const updatedPrefs = userPreferencesManager.getUserPreferences()
463
+ expect(updatedPrefs.name).toBe('Changed Name')
464
+ })
465
+ })
466
+
467
+ describe('edge cases and error handling', () => {
468
+ beforeEach(() => {
469
+ userPreferencesManager = new UserPreferencesManager(mockUser, false)
470
+ })
471
+
472
+ it('should handle undefined user preferences gracefully', () => {
473
+ userPreferencesAtom.set({} as TLUserPreferences)
474
+
475
+ expect(() => userPreferencesManager.getUserPreferences()).not.toThrow()
476
+ expect(userPreferencesManager.getName()).toBe(defaultUserPreferences.name)
477
+ expect(userPreferencesManager.getColor()).toBe(defaultUserPreferences.color)
478
+ })
479
+
480
+ it('should handle null values in preferences', () => {
481
+ const nullPrefs = createMockUserPreferences({
482
+ name: null,
483
+ color: null,
484
+ locale: null,
485
+ animationSpeed: null,
486
+ edgeScrollSpeed: null,
487
+ isSnapMode: null,
488
+ isWrapMode: null,
489
+ isDynamicSizeMode: null,
490
+ isPasteAtCursorMode: null,
491
+ })
492
+
493
+ userPreferencesAtom.set(nullPrefs)
494
+
495
+ expect(userPreferencesManager.getName()).toBe(defaultUserPreferences.name)
496
+ expect(userPreferencesManager.getColor()).toBe(defaultUserPreferences.color)
497
+ expect(userPreferencesManager.getLocale()).toBe(defaultUserPreferences.locale)
498
+ expect(userPreferencesManager.getAnimationSpeed()).toBe(defaultUserPreferences.animationSpeed)
499
+ expect(userPreferencesManager.getEdgeScrollSpeed()).toBe(
500
+ defaultUserPreferences.edgeScrollSpeed
501
+ )
502
+ expect(userPreferencesManager.getIsSnapMode()).toBe(defaultUserPreferences.isSnapMode)
503
+ expect(userPreferencesManager.getIsWrapMode()).toBe(defaultUserPreferences.isWrapMode)
504
+ expect(userPreferencesManager.getIsDynamicResizeMode()).toBe(
505
+ defaultUserPreferences.isDynamicSizeMode
506
+ )
507
+ expect(userPreferencesManager.getIsPasteAtCursorMode()).toBe(
508
+ defaultUserPreferences.isPasteAtCursorMode
509
+ )
510
+ })
511
+
512
+ it('should handle matchMedia with null response', () => {
513
+ // Mock matchMedia returning null (like in some environments)
514
+ mockMatchMedia.mockReturnValue(null)
515
+
516
+ expect(() => {
517
+ userPreferencesManager = new UserPreferencesManager(mockUser, false)
518
+ }).not.toThrow()
519
+
520
+ expect(userPreferencesManager.systemColorScheme.get()).toBe('light')
521
+ })
522
+
523
+ it('should handle dispose gracefully in all cases', () => {
524
+ userPreferencesManager = new UserPreferencesManager(mockUser, false)
525
+
526
+ // Should not throw even if dispose is called multiple times
527
+ expect(() => userPreferencesManager.dispose()).not.toThrow()
528
+ expect(() => userPreferencesManager.dispose()).not.toThrow()
529
+ })
530
+
531
+ it('should handle empty disposables set', () => {
532
+ // Test in server environment where no event listeners are added
533
+ const originalWindow = global.window
534
+ delete (global as any).window
535
+
536
+ userPreferencesManager = new UserPreferencesManager(mockUser, false)
537
+
538
+ expect(() => userPreferencesManager.dispose()).not.toThrow()
539
+ expect(userPreferencesManager.disposables.size).toBe(0)
540
+
541
+ global.window = originalWindow
542
+ })
543
+ })
544
+
545
+ describe('integration scenarios', () => {
546
+ it('should work with real-world preference scenarios', () => {
547
+ userPreferencesManager = new UserPreferencesManager(mockUser, true)
548
+
549
+ // User starts with system preference
550
+ userPreferencesManager.updateUserPreferences({ colorScheme: 'system' })
551
+ userPreferencesManager.systemColorScheme.set('dark')
552
+
553
+ expect(userPreferencesManager.getIsDarkMode()).toBe(true)
554
+ expect(userPreferencesManager.getUserPreferences().isDarkMode).toBe(true)
555
+
556
+ // User switches to light mode explicitly
557
+ userPreferencesManager.updateUserPreferences({ colorScheme: 'light' })
558
+
559
+ expect(userPreferencesManager.getIsDarkMode()).toBe(false)
560
+ expect(userPreferencesManager.getUserPreferences().isDarkMode).toBe(false)
561
+
562
+ // System changes but user preference is respected
563
+ userPreferencesManager.systemColorScheme.set('light')
564
+
565
+ expect(userPreferencesManager.getIsDarkMode()).toBe(false)
566
+ })
567
+
568
+ it('should handle preference updates with multiple fields', () => {
569
+ userPreferencesManager = new UserPreferencesManager(mockUser, false)
570
+
571
+ const updates = {
572
+ name: 'New User',
573
+ color: '#F2555A',
574
+ animationSpeed: 0.5,
575
+ isSnapMode: true,
576
+ colorScheme: 'dark' as const,
577
+ }
578
+
579
+ userPreferencesManager.updateUserPreferences(updates)
580
+
581
+ const prefs = userPreferencesManager.getUserPreferences()
582
+
583
+ expect(prefs.name).toBe('New User')
584
+ expect(prefs.color).toBe('#F2555A')
585
+ expect(prefs.animationSpeed).toBe(0.5)
586
+ expect(prefs.isSnapMode).toBe(true)
587
+ expect(prefs.colorScheme).toBe('dark')
588
+ expect(prefs.isDarkMode).toBe(true)
589
+ })
590
+ })
591
+ })
@@ -1,6 +1,6 @@
1
1
  import { atom, computed } from '@tldraw/state'
2
- import { TLUserPreferences, defaultUserPreferences } from '../../config/TLUserPreferences'
3
- import { TLUser } from '../../config/createTLUser'
2
+ import { TLUserPreferences, defaultUserPreferences } from '../../../config/TLUserPreferences'
3
+ import { TLUser } from '../../../config/createTLUser'
4
4
 
5
5
  /** @public */
6
6
  export class UserPreferencesManager {