@lazlon-platform/html-editor 0.1.0

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 (86) hide show
  1. package/.claude/settings.local.json +9 -0
  2. package/.github/workflows/ci.yml +34 -0
  3. package/README.md +24 -0
  4. package/demo/App.tsx +62 -0
  5. package/demo/EditorView/PageView/NodeContent.tsx +35 -0
  6. package/demo/EditorView/PageView/SnapLines.tsx +28 -0
  7. package/demo/EditorView/PageView/index.tsx +45 -0
  8. package/demo/EditorView/SelectionFrame/Corner.tsx +24 -0
  9. package/demo/EditorView/SelectionFrame/Edge.tsx +21 -0
  10. package/demo/EditorView/SelectionFrame/index.tsx +27 -0
  11. package/demo/EditorView/SelectionOverlay/ActionHud.tsx +32 -0
  12. package/demo/EditorView/SelectionOverlay/Rotation.tsx +39 -0
  13. package/demo/EditorView/SelectionOverlay/Toolbar.tsx +128 -0
  14. package/demo/EditorView/SelectionOverlay/index.tsx +21 -0
  15. package/demo/EditorView/Toolbar/index.tsx +68 -0
  16. package/demo/EditorView/index.tsx +47 -0
  17. package/demo/Navbar/index.tsx +33 -0
  18. package/demo/Sidebar/index.tsx +71 -0
  19. package/demo/hotkeys.ts +93 -0
  20. package/demo/main.tsx +10 -0
  21. package/demo/style.css +1 -0
  22. package/eslint.config.js +43 -0
  23. package/index.html +14 -0
  24. package/lib/hooks/actions.ts +426 -0
  25. package/lib/hooks/batch.ts +102 -0
  26. package/lib/hooks/editor.ts +18 -0
  27. package/lib/hooks/index.ts +23 -0
  28. package/lib/hooks/node.ts +33 -0
  29. package/lib/hooks/page.ts +26 -0
  30. package/lib/hooks/pointer/moveable.ts +98 -0
  31. package/lib/hooks/pointer/pointer.ts +56 -0
  32. package/lib/hooks/pointer/resize.ts +281 -0
  33. package/lib/hooks/pointer/rotation.ts +111 -0
  34. package/lib/hooks/pointer/selectionFrame.ts +97 -0
  35. package/lib/hooks/pointer/selector.ts +64 -0
  36. package/lib/hooks/pointer/snap.ts +97 -0
  37. package/lib/hooks/textMarks.ts +276 -0
  38. package/lib/lib/googleFonts.ts +162 -0
  39. package/lib/model/editor.ts +169 -0
  40. package/lib/model/geometry.ts +155 -0
  41. package/lib/model/history.ts +135 -0
  42. package/lib/model/index.ts +12 -0
  43. package/lib/model/node/editable/index.ts +85 -0
  44. package/lib/model/node/editable/letterSpacing.ts +61 -0
  45. package/lib/model/node/editable/persistentMarks.ts +45 -0
  46. package/lib/model/node/editable/tiptapExtensions.ts +33 -0
  47. package/lib/model/node/formattable.ts +108 -0
  48. package/lib/model/node/group.ts +79 -0
  49. package/lib/model/node/image.ts +41 -0
  50. package/lib/model/node/shape/polygon.ts +173 -0
  51. package/lib/model/node/shape/shape.ts +48 -0
  52. package/lib/model/node/text.ts +55 -0
  53. package/lib/model/node.ts +101 -0
  54. package/lib/model/page.ts +51 -0
  55. package/lib/model/traversal.ts +21 -0
  56. package/lib/ui/colors.ts +23 -0
  57. package/lib/ui/extractor.ts +57 -0
  58. package/lib/ui/index.ts +8 -0
  59. package/lib/ui/node/EditableContent.tsx +101 -0
  60. package/lib/ui/node/GroupContent.tsx +46 -0
  61. package/lib/ui/node/ImageContent.tsx +36 -0
  62. package/lib/ui/node/NodeView.tsx +68 -0
  63. package/lib/ui/node/PolygonContent.tsx +81 -0
  64. package/lib/ui/node/TextContent.tsx +40 -0
  65. package/lib/ui/node/useDoubleClick.ts +37 -0
  66. package/lib/ui/selection.ts +38 -0
  67. package/package.json +70 -0
  68. package/tests/createTestEditor.ts +19 -0
  69. package/tests/hooks/actions.test.tsx +736 -0
  70. package/tests/hooks/batch.test.tsx +332 -0
  71. package/tests/hooks/editor.test.tsx +56 -0
  72. package/tests/hooks/page.test.tsx +135 -0
  73. package/tests/hooks/pointer/pointer.test.tsx +244 -0
  74. package/tests/hooks/textMarks.test.tsx +624 -0
  75. package/tests/model/editor.test.ts +384 -0
  76. package/tests/model/history.test.ts +293 -0
  77. package/tests/model/node/group.test.ts +294 -0
  78. package/tests/model/node/image.test.ts +150 -0
  79. package/tests/model/node/polygon.test.ts +408 -0
  80. package/tests/model/node/text.test.ts +158 -0
  81. package/tests/model/node.test.ts +276 -0
  82. package/tests/model/page.test.ts +150 -0
  83. package/tests/setup.ts +7 -0
  84. package/tsconfig.json +28 -0
  85. package/vite.config.ts +9 -0
  86. package/vitest.config.ts +13 -0
@@ -0,0 +1,244 @@
1
+ import { act, renderHook } from "@testing-library/react"
2
+ import { beforeEach, describe, expect, it, vi } from "vitest"
3
+ import { usePointer } from "../../../lib/hooks/pointer/pointer"
4
+
5
+ describe("usePointer", () => {
6
+ function createPointerEvent(
7
+ type: "pointerdown" | "pointermove" | "pointerup",
8
+ props: Partial<React.PointerEvent> = {},
9
+ ): React.PointerEvent {
10
+ return {
11
+ type,
12
+ button: 0,
13
+ clientX: 0,
14
+ clientY: 0,
15
+ ...props,
16
+ } as unknown as React.PointerEvent
17
+ }
18
+
19
+ function createGlobalPointerEvent(
20
+ type: "pointermove" | "pointerup" | "pointercancel",
21
+ props: Partial<PointerEvent> = {},
22
+ ): PointerEvent {
23
+ return new PointerEvent(type, {
24
+ clientX: props.clientX ?? 0,
25
+ clientY: props.clientY ?? 0,
26
+ ...props,
27
+ })
28
+ }
29
+
30
+ beforeEach(() => {
31
+ // Clear any existing listeners
32
+ vi.clearAllMocks()
33
+ })
34
+
35
+ it("calls onDown handler on pointer down", () => {
36
+ const onDown = vi.fn()
37
+ const { result } = renderHook(() => usePointer({ onDown }))
38
+
39
+ act(() => {
40
+ result.current.onPointerDown(
41
+ createPointerEvent("pointerdown", { clientX: 100, clientY: 100 }),
42
+ )
43
+ })
44
+
45
+ expect(onDown).toHaveBeenCalledTimes(1)
46
+ })
47
+
48
+ it("does not proceed if onDown returns false", () => {
49
+ const onDown = vi.fn(() => false)
50
+ const onMove = vi.fn()
51
+ const { result } = renderHook(() => usePointer({ onDown, onMove }))
52
+
53
+ act(() => {
54
+ result.current.onPointerDown(createPointerEvent("pointerdown"))
55
+ })
56
+
57
+ // Simulate global pointermove - should not trigger onMove
58
+ act(() => {
59
+ window.dispatchEvent(
60
+ createGlobalPointerEvent("pointermove", { clientX: 50, clientY: 50 }),
61
+ )
62
+ })
63
+
64
+ expect(onMove).not.toHaveBeenCalled()
65
+ })
66
+
67
+ it("ignores non-left-button clicks", () => {
68
+ const onDown = vi.fn()
69
+ const { result } = renderHook(() => usePointer({ onDown }))
70
+
71
+ act(() => {
72
+ result.current.onPointerDown(createPointerEvent("pointerdown", { button: 2 }))
73
+ })
74
+
75
+ expect(onDown).not.toHaveBeenCalled()
76
+ })
77
+
78
+ it("tracks moving state", () => {
79
+ const { result } = renderHook(() => usePointer({}))
80
+
81
+ expect(result.current.isMoving()).toBe(false)
82
+
83
+ act(() => {
84
+ result.current.onPointerDown(createPointerEvent("pointerdown"))
85
+ })
86
+
87
+ // Initially not moving
88
+ expect(result.current.isMoving()).toBe(false)
89
+
90
+ // After move, should be moving
91
+ act(() => {
92
+ window.dispatchEvent(
93
+ createGlobalPointerEvent("pointermove", { clientX: 50, clientY: 50 }),
94
+ )
95
+ })
96
+
97
+ expect(result.current.isMoving()).toBe(true)
98
+
99
+ // After up, should not be moving
100
+ act(() => {
101
+ window.dispatchEvent(createGlobalPointerEvent("pointerup"))
102
+ })
103
+
104
+ expect(result.current.isMoving()).toBe(false)
105
+ })
106
+
107
+ it("calls onCancel when pointer up without move", () => {
108
+ const onCancel = vi.fn()
109
+ const onEnd = vi.fn()
110
+ const { result } = renderHook(() => usePointer({ onCancel, onEnd }))
111
+
112
+ act(() => {
113
+ result.current.onPointerDown(createPointerEvent("pointerdown"))
114
+ })
115
+
116
+ // Simulate pointerup without pointermove
117
+ act(() => {
118
+ window.dispatchEvent(createGlobalPointerEvent("pointerup"))
119
+ })
120
+
121
+ expect(onCancel).toHaveBeenCalledTimes(1)
122
+ expect(onEnd).not.toHaveBeenCalled()
123
+ })
124
+
125
+ it("calls onEnd when pointer up after move", () => {
126
+ const onEnd = vi.fn()
127
+ const onCancel = vi.fn()
128
+ const { result } = renderHook(() => usePointer({ onEnd, onCancel }))
129
+
130
+ act(() => {
131
+ result.current.onPointerDown(createPointerEvent("pointerdown"))
132
+ })
133
+
134
+ // Simulate pointermove
135
+ act(() => {
136
+ window.dispatchEvent(
137
+ createGlobalPointerEvent("pointermove", { clientX: 50, clientY: 50 }),
138
+ )
139
+ })
140
+
141
+ // Simulate pointerup
142
+ act(() => {
143
+ window.dispatchEvent(
144
+ createGlobalPointerEvent("pointerup", { clientX: 50, clientY: 50 }),
145
+ )
146
+ })
147
+
148
+ expect(onEnd).toHaveBeenCalledTimes(1)
149
+ expect(onCancel).not.toHaveBeenCalled()
150
+ })
151
+
152
+ it("provides start and current event to onMove", () => {
153
+ const onMove = vi.fn()
154
+ const { result } = renderHook(() => usePointer({ onMove }))
155
+
156
+ act(() => {
157
+ result.current.onPointerDown(
158
+ createPointerEvent("pointerdown", { clientX: 10, clientY: 20 }),
159
+ )
160
+ })
161
+
162
+ act(() => {
163
+ window.dispatchEvent(
164
+ createGlobalPointerEvent("pointermove", { clientX: 30, clientY: 40 }),
165
+ )
166
+ })
167
+
168
+ expect(onMove).toHaveBeenCalledWith(
169
+ expect.objectContaining({
170
+ start: expect.objectContaining({ clientX: 10, clientY: 20 }),
171
+ event: expect.objectContaining({ clientX: 30, clientY: 40 }),
172
+ }),
173
+ )
174
+ })
175
+
176
+ it("provides start and current event to onEnd", () => {
177
+ const onEnd = vi.fn()
178
+ const { result } = renderHook(() => usePointer({ onEnd }))
179
+
180
+ act(() => {
181
+ result.current.onPointerDown(
182
+ createPointerEvent("pointerdown", { clientX: 10, clientY: 20 }),
183
+ )
184
+ })
185
+
186
+ // Move first to trigger end instead of cancel
187
+ act(() => {
188
+ window.dispatchEvent(
189
+ createGlobalPointerEvent("pointermove", { clientX: 50, clientY: 60 }),
190
+ )
191
+ })
192
+
193
+ act(() => {
194
+ window.dispatchEvent(
195
+ createGlobalPointerEvent("pointerup", { clientX: 100, clientY: 120 }),
196
+ )
197
+ })
198
+
199
+ expect(onEnd).toHaveBeenCalledWith(
200
+ expect.objectContaining({
201
+ start: expect.objectContaining({ clientX: 10, clientY: 20 }),
202
+ event: expect.objectContaining({ clientX: 100, clientY: 120 }),
203
+ }),
204
+ )
205
+ })
206
+
207
+ it("calls onMove multiple times during drag", () => {
208
+ const onMove = vi.fn()
209
+ const { result } = renderHook(() => usePointer({ onMove }))
210
+
211
+ act(() => {
212
+ result.current.onPointerDown(createPointerEvent("pointerdown"))
213
+ })
214
+
215
+ act(() => {
216
+ window.dispatchEvent(
217
+ createGlobalPointerEvent("pointermove", { clientX: 10, clientY: 10 }),
218
+ )
219
+ window.dispatchEvent(
220
+ createGlobalPointerEvent("pointermove", { clientX: 20, clientY: 20 }),
221
+ )
222
+ window.dispatchEvent(
223
+ createGlobalPointerEvent("pointermove", { clientX: 30, clientY: 30 }),
224
+ )
225
+ })
226
+
227
+ expect(onMove).toHaveBeenCalledTimes(3)
228
+ })
229
+
230
+ it("handles pointercancel like pointerup", () => {
231
+ const onCancel = vi.fn()
232
+ const { result } = renderHook(() => usePointer({ onCancel }))
233
+
234
+ act(() => {
235
+ result.current.onPointerDown(createPointerEvent("pointerdown"))
236
+ })
237
+
238
+ act(() => {
239
+ window.dispatchEvent(createGlobalPointerEvent("pointercancel"))
240
+ })
241
+
242
+ expect(onCancel).toHaveBeenCalledTimes(1)
243
+ })
244
+ })