@lazlon-platform/html-editor 0.1.0 → 0.2.1

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 (42) hide show
  1. package/package.json +14 -11
  2. package/.claude/settings.local.json +0 -9
  3. package/.github/workflows/ci.yml +0 -34
  4. package/demo/App.tsx +0 -62
  5. package/demo/EditorView/PageView/NodeContent.tsx +0 -35
  6. package/demo/EditorView/PageView/SnapLines.tsx +0 -28
  7. package/demo/EditorView/PageView/index.tsx +0 -45
  8. package/demo/EditorView/SelectionFrame/Corner.tsx +0 -24
  9. package/demo/EditorView/SelectionFrame/Edge.tsx +0 -21
  10. package/demo/EditorView/SelectionFrame/index.tsx +0 -27
  11. package/demo/EditorView/SelectionOverlay/ActionHud.tsx +0 -32
  12. package/demo/EditorView/SelectionOverlay/Rotation.tsx +0 -39
  13. package/demo/EditorView/SelectionOverlay/Toolbar.tsx +0 -128
  14. package/demo/EditorView/SelectionOverlay/index.tsx +0 -21
  15. package/demo/EditorView/Toolbar/index.tsx +0 -68
  16. package/demo/EditorView/index.tsx +0 -47
  17. package/demo/Navbar/index.tsx +0 -33
  18. package/demo/Sidebar/index.tsx +0 -71
  19. package/demo/hotkeys.ts +0 -93
  20. package/demo/main.tsx +0 -10
  21. package/demo/style.css +0 -1
  22. package/eslint.config.js +0 -43
  23. package/index.html +0 -14
  24. package/tests/createTestEditor.ts +0 -19
  25. package/tests/hooks/actions.test.tsx +0 -736
  26. package/tests/hooks/batch.test.tsx +0 -332
  27. package/tests/hooks/editor.test.tsx +0 -56
  28. package/tests/hooks/page.test.tsx +0 -135
  29. package/tests/hooks/pointer/pointer.test.tsx +0 -244
  30. package/tests/hooks/textMarks.test.tsx +0 -624
  31. package/tests/model/editor.test.ts +0 -384
  32. package/tests/model/history.test.ts +0 -293
  33. package/tests/model/node/group.test.ts +0 -294
  34. package/tests/model/node/image.test.ts +0 -150
  35. package/tests/model/node/polygon.test.ts +0 -408
  36. package/tests/model/node/text.test.ts +0 -158
  37. package/tests/model/node.test.ts +0 -276
  38. package/tests/model/page.test.ts +0 -150
  39. package/tests/setup.ts +0 -7
  40. package/tsconfig.json +0 -28
  41. package/vite.config.ts +0 -9
  42. package/vitest.config.ts +0 -13
@@ -1,408 +0,0 @@
1
- import { describe, it, expect, beforeEach } from "vitest"
2
- import { Page } from "../../../lib/model/page"
3
- import { PolygonNode } from "../../../lib/model/node/shape/polygon"
4
- import { createTestEditor } from "../../createTestEditor"
5
-
6
- describe("PolygonNode", () => {
7
- let editor: ReturnType<typeof createTestEditor>
8
- let page: Page
9
-
10
- beforeEach(() => {
11
- editor = createTestEditor()
12
- page = new Page(editor, { id: "page-1" })
13
- })
14
-
15
- describe("construction", () => {
16
- it("has name 'polygon'", () => {
17
- const polygon = new PolygonNode(editor, page, {
18
- id: "poly-1",
19
- x: 0,
20
- y: 0,
21
- width: 100,
22
- height: 100,
23
- })
24
- expect(polygon.name).toBe("polygon")
25
- })
26
-
27
- it("defaults to 4 sides (rectangle)", () => {
28
- const polygon = new PolygonNode(editor, page, {
29
- id: "poly-1",
30
- x: 0,
31
- y: 0,
32
- width: 100,
33
- height: 100,
34
- })
35
- expect(polygon.sides).toBe(4)
36
- })
37
-
38
- it("defaults to 0 roundness", () => {
39
- const polygon = new PolygonNode(editor, page, {
40
- id: "poly-1",
41
- x: 0,
42
- y: 0,
43
- width: 100,
44
- height: 100,
45
- })
46
- expect(polygon.roundness).toBe(0)
47
- })
48
-
49
- it("accepts custom sides", () => {
50
- const polygon = new PolygonNode(editor, page, {
51
- id: "poly-1",
52
- x: 0,
53
- y: 0,
54
- width: 100,
55
- height: 100,
56
- sides: 6,
57
- })
58
- expect(polygon.sides).toBe(6)
59
- })
60
-
61
- it("initializes corner radii from roundness for rectangles", () => {
62
- const polygon = new PolygonNode(editor, page, {
63
- id: "poly-1",
64
- x: 0,
65
- y: 0,
66
- width: 100,
67
- height: 100,
68
- sides: 4,
69
- roundness: 10,
70
- })
71
-
72
- expect(polygon.cornerTopLeft).toBe(10)
73
- expect(polygon.cornerTopRight).toBe(10)
74
- expect(polygon.cornerBottomLeft).toBe(10)
75
- expect(polygon.cornerBottomRight).toBe(10)
76
- })
77
-
78
- it("accepts individual corner radii", () => {
79
- const polygon = new PolygonNode(editor, page, {
80
- id: "poly-1",
81
- x: 0,
82
- y: 0,
83
- width: 100,
84
- height: 100,
85
- cornerTopLeft: 5,
86
- cornerTopRight: 10,
87
- cornerBottomLeft: 15,
88
- cornerBottomRight: 20,
89
- })
90
-
91
- expect(polygon.cornerTopLeft).toBe(5)
92
- expect(polygon.cornerTopRight).toBe(10)
93
- expect(polygon.cornerBottomLeft).toBe(15)
94
- expect(polygon.cornerBottomRight).toBe(20)
95
- })
96
-
97
- it("inherits ShapeNode properties", () => {
98
- const polygon = new PolygonNode(editor, page, {
99
- id: "poly-1",
100
- x: 0,
101
- y: 0,
102
- width: 100,
103
- height: 100,
104
- background: "#ff0000",
105
- borderWidth: 2,
106
- borderColor: "#000000",
107
- halign: "left",
108
- valign: "top",
109
- })
110
-
111
- expect(polygon.background).toBe("#ff0000")
112
- expect(polygon.borderWidth).toBe(2)
113
- expect(polygon.borderColor).toBe("#000000")
114
- expect(polygon.halign).toBe("left")
115
- expect(polygon.valign).toBe("top")
116
- })
117
- })
118
-
119
- describe("roundness getter", () => {
120
- it("returns uniform roundness when all corners are equal", () => {
121
- const polygon = new PolygonNode(editor, page, {
122
- id: "poly-1",
123
- x: 0,
124
- y: 0,
125
- width: 100,
126
- height: 100,
127
- sides: 4,
128
- roundness: 15,
129
- })
130
-
131
- expect(polygon.roundness).toBe(15)
132
- })
133
-
134
- it("returns 0 when corners have different values", () => {
135
- const polygon = new PolygonNode(editor, page, {
136
- id: "poly-1",
137
- x: 0,
138
- y: 0,
139
- width: 100,
140
- height: 100,
141
- sides: 4,
142
- cornerTopLeft: 5,
143
- cornerTopRight: 10,
144
- cornerBottomLeft: 15,
145
- cornerBottomRight: 20,
146
- })
147
-
148
- expect(polygon.roundness).toBe(0)
149
- })
150
-
151
- it("returns internal roundness for non-rectangles", () => {
152
- const polygon = new PolygonNode(editor, page, {
153
- id: "poly-1",
154
- x: 0,
155
- y: 0,
156
- width: 100,
157
- height: 100,
158
- sides: 6,
159
- roundness: 8,
160
- })
161
-
162
- expect(polygon.roundness).toBe(8)
163
- })
164
- })
165
-
166
- describe("roundness setter", () => {
167
- it("updates all corners for rectangles", () => {
168
- const polygon = new PolygonNode(editor, page, {
169
- id: "poly-1",
170
- x: 0,
171
- y: 0,
172
- width: 100,
173
- height: 100,
174
- sides: 4,
175
- })
176
-
177
- polygon.roundness = 20
178
-
179
- expect(polygon.cornerTopLeft).toBe(20)
180
- expect(polygon.cornerTopRight).toBe(20)
181
- expect(polygon.cornerBottomLeft).toBe(20)
182
- expect(polygon.cornerBottomRight).toBe(20)
183
- })
184
-
185
- it("does not update corners for non-rectangles", () => {
186
- const polygon = new PolygonNode(editor, page, {
187
- id: "poly-1",
188
- x: 0,
189
- y: 0,
190
- width: 100,
191
- height: 100,
192
- sides: 6,
193
- cornerTopLeft: 0,
194
- })
195
-
196
- polygon.roundness = 20
197
-
198
- expect(polygon.cornerTopLeft).toBe(0)
199
- expect(polygon.roundness).toBe(20)
200
- })
201
- })
202
-
203
- describe("svgPathData", () => {
204
- it("generates valid SVG path for triangle", () => {
205
- const polygon = new PolygonNode(editor, page, {
206
- id: "poly-1",
207
- x: 0,
208
- y: 0,
209
- width: 100,
210
- height: 100,
211
- sides: 3,
212
- })
213
-
214
- const path = polygon.svgPathData
215
-
216
- expect(path).toContain("M")
217
- expect(path).toContain("L")
218
- expect(path).toContain("Z")
219
- })
220
-
221
- it("generates SVG path for square", () => {
222
- const polygon = new PolygonNode(editor, page, {
223
- id: "poly-1",
224
- x: 0,
225
- y: 0,
226
- width: 100,
227
- height: 100,
228
- sides: 4,
229
- })
230
-
231
- const path = polygon.svgPathData
232
-
233
- expect(path).toContain("M")
234
- expect(path).toContain("L")
235
- expect(path).toContain("Z")
236
- })
237
-
238
- it("generates SVG path with rounded corners", () => {
239
- const polygon = new PolygonNode(editor, page, {
240
- id: "poly-1",
241
- x: 0,
242
- y: 0,
243
- width: 100,
244
- height: 100,
245
- sides: 4,
246
- roundness: 10,
247
- })
248
-
249
- const path = polygon.svgPathData
250
-
251
- expect(path).toContain("A") // Arc command for rounded corners
252
- })
253
-
254
- it("generates SVG path for hexagon", () => {
255
- const polygon = new PolygonNode(editor, page, {
256
- id: "poly-1",
257
- x: 0,
258
- y: 0,
259
- width: 100,
260
- height: 100,
261
- sides: 6,
262
- })
263
-
264
- const path = polygon.svgPathData
265
-
266
- // Should have M + 5 L commands for 6 sides
267
- const lineCommands = path.match(/[ML]/g) || []
268
- expect(lineCommands.length).toBe(6)
269
- })
270
-
271
- it("handles very small roundness", () => {
272
- const polygon = new PolygonNode(editor, page, {
273
- id: "poly-1",
274
- x: 0,
275
- y: 0,
276
- width: 100,
277
- height: 100,
278
- sides: 4,
279
- roundness: 0.001,
280
- })
281
-
282
- const path = polygon.svgPathData
283
-
284
- // Should not throw and should produce valid path
285
- expect(path).toContain("Z")
286
- })
287
- })
288
-
289
- describe("serialization", () => {
290
- it("includes all PolygonNode-specific properties", () => {
291
- const polygon = new PolygonNode(editor, page, {
292
- id: "poly-1",
293
- x: 10,
294
- y: 20,
295
- width: 150,
296
- height: 150,
297
- sides: 5,
298
- cornerTopLeft: 5,
299
- cornerTopRight: 10,
300
- cornerBottomLeft: 15,
301
- cornerBottomRight: 20,
302
- })
303
-
304
- const serialized = polygon.serialize()
305
-
306
- expect(serialized.name).toBe("polygon")
307
- expect(serialized.props).toMatchObject({
308
- id: "poly-1",
309
- sides: 5,
310
- cornerTopLeft: 5,
311
- cornerTopRight: 10,
312
- cornerBottomLeft: 15,
313
- cornerBottomRight: 20,
314
- })
315
- })
316
-
317
- it("includes inherited ShapeNode properties", () => {
318
- const polygon = new PolygonNode(editor, page, {
319
- id: "poly-1",
320
- x: 0,
321
- y: 0,
322
- width: 100,
323
- height: 100,
324
- background: "#ff6600",
325
- borderWidth: 3,
326
- borderColor: "#333333",
327
- halign: "right",
328
- valign: "bottom",
329
- })
330
-
331
- const props = polygon.props()
332
-
333
- expect(props.background).toBe("#ff6600")
334
- expect(props.borderWidth).toBe(3)
335
- expect(props.borderColor).toBe("#333333")
336
- expect(props.halign).toBe("right")
337
- expect(props.valign).toBe("bottom")
338
- })
339
- })
340
-
341
- describe("deserialization round-trip", () => {
342
- it("produces identical props after serialize -> deserialize -> serialize", () => {
343
- const original = new PolygonNode(editor, page, {
344
- id: "poly-1",
345
- x: 50,
346
- y: 100,
347
- width: 200,
348
- height: 180,
349
- sides: 8,
350
- roundness: 12,
351
- background: "#00ff00",
352
- borderWidth: 4,
353
- borderColor: "#000000",
354
- rotation: 22.5,
355
- halign: "center",
356
- valign: "center",
357
- })
358
-
359
- const firstSerialized = original.serialize()
360
- const restored = editor.deserializeNode(page, firstSerialized) as PolygonNode
361
- const secondSerialized = restored.serialize()
362
-
363
- expect(secondSerialized).toEqual(firstSerialized)
364
- })
365
-
366
- it("preserves individual corner radii through round-trip", () => {
367
- const original = new PolygonNode(editor, page, {
368
- id: "poly-1",
369
- x: 0,
370
- y: 0,
371
- width: 100,
372
- height: 100,
373
- sides: 4,
374
- cornerTopLeft: 5,
375
- cornerTopRight: 10,
376
- cornerBottomLeft: 15,
377
- cornerBottomRight: 20,
378
- })
379
-
380
- const firstSerialized = original.serialize()
381
- const restored = editor.deserializeNode(page, firstSerialized) as PolygonNode
382
- const secondSerialized = restored.serialize()
383
-
384
- expect(secondSerialized).toEqual(firstSerialized)
385
-
386
- expect(restored.cornerTopLeft).toBe(5)
387
- expect(restored.cornerTopRight).toBe(10)
388
- expect(restored.cornerBottomLeft).toBe(15)
389
- expect(restored.cornerBottomRight).toBe(20)
390
- })
391
-
392
- it("preserves default values through round-trip", () => {
393
- const original = new PolygonNode(editor, page, {
394
- id: "poly-1",
395
- x: 0,
396
- y: 0,
397
- width: 100,
398
- height: 100,
399
- })
400
-
401
- const firstSerialized = original.serialize()
402
- const restored = editor.deserializeNode(page, firstSerialized) as PolygonNode
403
- const secondSerialized = restored.serialize()
404
-
405
- expect(secondSerialized).toEqual(firstSerialized)
406
- })
407
- })
408
- })
@@ -1,158 +0,0 @@
1
- import { describe, it, expect, beforeEach } from "vitest"
2
- import { Page } from "../../../lib/model/page"
3
- import { TextNode } from "../../../lib/model/node/text"
4
- import { createTestEditor } from "../../createTestEditor"
5
-
6
- describe("TextNode", () => {
7
- let editor: ReturnType<typeof createTestEditor>
8
- let page: Page
9
-
10
- beforeEach(() => {
11
- editor = createTestEditor()
12
- page = new Page(editor, { id: "page-1" })
13
- })
14
-
15
- describe("construction", () => {
16
- it("has name 'text'", () => {
17
- const node = new TextNode(editor, page, {
18
- id: "text-1",
19
- x: 0,
20
- y: 0,
21
- width: 200,
22
- height: 50,
23
- })
24
- expect(node.name).toBe("text")
25
- })
26
-
27
- it("applies default horizontal alignment", () => {
28
- const node = new TextNode(editor, page, {
29
- id: "text-1",
30
- x: 0,
31
- y: 0,
32
- width: 200,
33
- height: 50,
34
- })
35
- expect(node.halign).toBe("center")
36
- })
37
-
38
- it("accepts custom alignment", () => {
39
- const node = new TextNode(editor, page, {
40
- id: "text-1",
41
- x: 0,
42
- y: 0,
43
- width: 200,
44
- height: 50,
45
- halign: "left",
46
- })
47
- expect(node.halign).toBe("left")
48
- })
49
-
50
- it("inherits EditableNode properties", () => {
51
- const node = new TextNode(editor, page, {
52
- id: "text-1",
53
- x: 0,
54
- y: 0,
55
- width: 200,
56
- height: 50,
57
- lineHeight: 1.5,
58
- content: { type: "doc", content: [{ type: "paragraph" }] },
59
- })
60
-
61
- expect(node.lineHeight).toBe(1.5)
62
- expect(node.content.type).toBe("doc")
63
- expect(node.content.content?.[0]?.type).toBe("paragraph")
64
- })
65
- })
66
-
67
- describe("serialization", () => {
68
- it("includes TextNode-specific properties", () => {
69
- const node = new TextNode(editor, page, {
70
- id: "text-1",
71
- x: 10,
72
- y: 20,
73
- width: 300,
74
- height: 100,
75
- halign: "right",
76
- })
77
-
78
- const serialized = node.serialize()
79
-
80
- expect(serialized.name).toBe("text")
81
- expect(serialized.props.id).toBe("text-1")
82
- // halign is not exported by TextNode.props() - it's inherited from EditableNode
83
- })
84
-
85
- it("includes content from EditableNode", () => {
86
- const node = new TextNode(editor, page, {
87
- id: "text-1",
88
- x: 0,
89
- y: 0,
90
- width: 200,
91
- height: 50,
92
- content: {
93
- type: "doc",
94
- content: [
95
- {
96
- type: "paragraph",
97
- content: [{ type: "text", text: "Hello World" }],
98
- },
99
- ],
100
- },
101
- })
102
-
103
- const props = node.props()
104
- expect(props.content).toBeDefined()
105
- })
106
- })
107
-
108
- describe("deserialization round-trip", () => {
109
- it("produces identical props after serialize -> deserialize -> serialize", () => {
110
- const original = new TextNode(editor, page, {
111
- id: "text-1",
112
- x: 50,
113
- y: 100,
114
- width: 400,
115
- height: 80,
116
- halign: "justify",
117
- lineHeight: 1.8,
118
- rotation: 10,
119
- })
120
-
121
- const firstSerialized = original.serialize()
122
- const restored = editor.deserializeNode(page, firstSerialized) as TextNode
123
- const secondSerialized = restored.serialize()
124
-
125
- expect(secondSerialized).toEqual(firstSerialized)
126
- })
127
-
128
- it("preserves content through round-trip", () => {
129
- const content = {
130
- type: "doc" as const,
131
- content: [
132
- {
133
- type: "paragraph",
134
- content: [
135
- { type: "text", text: "Bold ", marks: [{ type: "bold" }] },
136
- { type: "text", text: "and italic", marks: [{ type: "italic" }] },
137
- ],
138
- },
139
- ],
140
- }
141
-
142
- const original = new TextNode(editor, page, {
143
- id: "text-1",
144
- x: 0,
145
- y: 0,
146
- width: 200,
147
- height: 50,
148
- content,
149
- })
150
-
151
- const firstSerialized = original.serialize()
152
- const restored = editor.deserializeNode(page, firstSerialized) as TextNode
153
- const secondSerialized = restored.serialize()
154
-
155
- expect(secondSerialized.props.content).toEqual(firstSerialized.props.content)
156
- })
157
- })
158
- })