@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,37 @@
1
+ import { useCallback, useEffect, useRef } from "react"
2
+
3
+ export function useDoubleClick(
4
+ onDoubleClick: (event: React.PointerEvent) => void,
5
+ threshold = 250,
6
+ ): React.PointerEventHandler {
7
+ const lastClickTimeRef = useRef(0)
8
+ const lastEventRef = useRef<React.PointerEvent | null>(null)
9
+
10
+ useEffect(
11
+ () => () => {
12
+ lastClickTimeRef.current = 0
13
+ lastEventRef.current = null
14
+ },
15
+ [onDoubleClick],
16
+ )
17
+
18
+ return useCallback(
19
+ function handlePointerDown(event) {
20
+ if (!event.isPrimary || event.button !== 0) return
21
+
22
+ const now = performance.now()
23
+ const delta = now - lastClickTimeRef.current
24
+
25
+ if (delta <= threshold && lastEventRef.current) {
26
+ lastClickTimeRef.current = 0
27
+ lastEventRef.current = null
28
+ onDoubleClick(event)
29
+ return
30
+ }
31
+
32
+ lastClickTimeRef.current = now
33
+ lastEventRef.current = event
34
+ },
35
+ [onDoubleClick, threshold],
36
+ )
37
+ }
@@ -0,0 +1,38 @@
1
+ import { boundingBox } from "../model/geometry"
2
+ import type { Node } from "../model/node"
3
+
4
+ export function getTargetDOMRect(selection: Set<Node>) {
5
+ const rects = selection
6
+ .values()
7
+ .map((node) => node.ref)
8
+ .filter((dom) => dom instanceof HTMLElement)
9
+ .map((dom) => dom.getBoundingClientRect())
10
+ .toArray()
11
+
12
+ return boundingBox(rects)
13
+ }
14
+
15
+ export function getTargetRect(selection: Set<Node>) {
16
+ const rects = selection
17
+ .values()
18
+ .map((node) => node.boundingBox)
19
+ .toArray()
20
+
21
+ return boundingBox(rects)
22
+ }
23
+
24
+ export function isPointerInSelectionRect(
25
+ selection: Set<Node>,
26
+ event: React.PointerEvent,
27
+ ) {
28
+ const rect = getTargetDOMRect(selection)
29
+ const right = rect.x + rect.width
30
+ const bottom = rect.y + rect.height
31
+
32
+ return (
33
+ event.clientX >= rect.x &&
34
+ event.clientX <= right &&
35
+ event.clientY >= rect.y &&
36
+ event.clientY <= bottom
37
+ )
38
+ }
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@lazlon-platform/html-editor",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "vite",
7
+ "typecheck": "tsc",
8
+ "build": "tsc -b && vite build",
9
+ "lint": "eslint .",
10
+ "format": "prettier . --write",
11
+ "preview": "vite preview",
12
+ "test": "vitest run"
13
+ },
14
+ "exports": {
15
+ "./ui": "./lib/ui/index.ts",
16
+ "./model": "./lib/model/index.ts",
17
+ "./model/geometry": "./lib/model/geometry.ts",
18
+ "./hooks": "./lib/hooks/index.ts",
19
+ "./googleFonts": "./lib/lib/googleFonts.ts"
20
+ },
21
+ "dependencies": {
22
+ "@tiptap/core": "^3.20.0",
23
+ "@tiptap/extension-bold": "^3.20.0",
24
+ "@tiptap/extension-document": "^3.20.0",
25
+ "@tiptap/extension-hard-break": "^3.20.0",
26
+ "@tiptap/extension-italic": "^3.20.0",
27
+ "@tiptap/extension-paragraph": "^3.20.0",
28
+ "@tiptap/extension-strike": "^3.20.0",
29
+ "@tiptap/extension-subscript": "^3.20.0",
30
+ "@tiptap/extension-superscript": "^3.20.0",
31
+ "@tiptap/extension-text": "^3.20.0",
32
+ "@tiptap/extension-text-style": "^3.20.0",
33
+ "@tiptap/extension-underline": "^3.20.0",
34
+ "@tiptap/pm": "^3.20.0",
35
+ "@tiptap/react": "^3.20.0",
36
+ "clsx": "^2.1.1",
37
+ "es-toolkit": "^1.45.0",
38
+ "react": "^19.2.0",
39
+ "react-aria-components": "^1.15.1",
40
+ "react-bolt": "^1.4.1",
41
+ "react-dom": "^19.2.0",
42
+ "react-hotkeys-hook": "^5.2.4",
43
+ "tailwindcss": "^4.2.1"
44
+ },
45
+ "devDependencies": {
46
+ "@eslint/js": "^9.39.1",
47
+ "@tailwindcss/vite": "^4.2.1",
48
+ "@testing-library/jest-dom": "^6.9.1",
49
+ "@testing-library/react": "^16.3.2",
50
+ "@types/node": "^24.10.1",
51
+ "@types/react": "^19.2.7",
52
+ "@types/react-dom": "^19.2.3",
53
+ "@vitejs/plugin-react": "^5.1.1",
54
+ "@vitest/coverage-v8": "^4.1.0",
55
+ "eslint": "^9.39.1",
56
+ "eslint-plugin-react-hooks": "^7.0.1",
57
+ "eslint-plugin-react-refresh": "^0.4.24",
58
+ "globals": "^16.5.0",
59
+ "happy-dom": "^20.8.3",
60
+ "typescript": "~5.9.3",
61
+ "typescript-eslint": "^8.48.0",
62
+ "vite": "^7.3.1",
63
+ "vite-tsconfig-paths": "^6.1.1",
64
+ "vitest": "^4.1.0"
65
+ },
66
+ "prettier": {
67
+ "semi": false,
68
+ "printWidth": 90
69
+ }
70
+ }
@@ -0,0 +1,19 @@
1
+ import { Editor, type NodeConstructor } from "@lazlon/html-editor/model"
2
+ import { TextNode } from "../lib/model/node/text"
3
+ import { ImageNode } from "../lib/model/node/image"
4
+ import { PolygonNode } from "../lib/model/node/shape/polygon"
5
+ import { GroupNode } from "../lib/model/node/group"
6
+
7
+ export function createTestEditor(options?: {
8
+ schema?: NodeConstructor[]
9
+ maxHistory?: number
10
+ }) {
11
+ return new Editor({
12
+ schema: options?.schema ?? [TextNode, ImageNode, PolygonNode, GroupNode],
13
+ options: {
14
+ loadFonts: async () => [],
15
+ maxHistory: options?.maxHistory ?? 100,
16
+ snapThreshold: 8,
17
+ },
18
+ })
19
+ }