@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
package/package.json CHANGED
@@ -1,16 +1,10 @@
1
1
  {
2
2
  "name": "@lazlon-platform/html-editor",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
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
- },
5
+ "files": [
6
+ "lib"
7
+ ],
14
8
  "exports": {
15
9
  "./ui": "./lib/ui/index.ts",
16
10
  "./model": "./lib/model/index.ts",
@@ -66,5 +60,14 @@
66
60
  "prettier": {
67
61
  "semi": false,
68
62
  "printWidth": 90
63
+ },
64
+ "scripts": {
65
+ "dev": "vite",
66
+ "typecheck": "tsc",
67
+ "build": "tsc -b && vite build",
68
+ "lint": "eslint .",
69
+ "format": "prettier . --write",
70
+ "preview": "vite preview",
71
+ "test": "vitest run"
69
72
  }
70
- }
73
+ }
@@ -1,9 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(node --version:*)",
5
- "Bash(pnpm tsc:*)",
6
- "Bash(pnpm lint:*)"
7
- ]
8
- }
9
- }
@@ -1,34 +0,0 @@
1
- name: CI
2
-
3
- on:
4
- push:
5
- branches: [main]
6
- pull_request:
7
- branches: [main]
8
-
9
- jobs:
10
- ci:
11
- runs-on: ubuntu-latest
12
-
13
- steps:
14
- - uses: actions/checkout@v4
15
-
16
- - uses: pnpm/action-setup@v4
17
- with:
18
- version: 9
19
-
20
- - uses: actions/setup-node@v4
21
- with:
22
- node-version: 22
23
- cache: pnpm
24
-
25
- - run: pnpm install --frozen-lockfile
26
-
27
- - name: Typecheck
28
- run: pnpm typecheck
29
-
30
- - name: Lint
31
- run: pnpm lint
32
-
33
- - name: Test
34
- run: pnpm test
package/demo/App.tsx DELETED
@@ -1,62 +0,0 @@
1
- import { EditorContext } from "@lazlon/html-editor/hooks"
2
- import {
3
- Editor,
4
- GroupNode,
5
- ImageNode,
6
- Page,
7
- PolygonNode,
8
- TextNode,
9
- } from "@lazlon/html-editor/model"
10
- import EditorView from "./EditorView"
11
- import Hotkeys from "./hotkeys"
12
- import Navbar from "./Navbar"
13
- import SideBar from "./Sidebar"
14
-
15
- function createEditor() {
16
- const state = localStorage.getItem("state")
17
-
18
- const editor = new Editor({
19
- ...(state && { init: JSON.parse(state) }),
20
- schema: [GroupNode, TextNode, PolygonNode, ImageNode],
21
- onChange() {
22
- localStorage.setItem("state", JSON.stringify(editor.serialize()))
23
- },
24
- })
25
-
26
- if (state) return editor
27
-
28
- const page = new Page(editor, {
29
- id: editor.id(),
30
- background: "#ffffff",
31
- width: 841,
32
- height: 595,
33
- })
34
-
35
- const polygon = new PolygonNode(editor, page, {
36
- id: editor.id(),
37
- x: 50,
38
- y: 50,
39
- width: 100,
40
- height: 100,
41
- })
42
-
43
- page.nodes = new Map([[polygon.id, polygon]])
44
- editor.pages = new Map([[page.id, page]])
45
-
46
- return editor
47
- }
48
-
49
- export default function App() {
50
- return (
51
- <EditorContext value={createEditor()}>
52
- <div className="h-full flex flex-col bg-amber-50">
53
- <Navbar />
54
- <div className="h-full flex overflow-auto">
55
- <SideBar />
56
- <EditorView />
57
- </div>
58
- </div>
59
- <Hotkeys />
60
- </EditorContext>
61
- )
62
- }
@@ -1,35 +0,0 @@
1
- import {
2
- GroupNode,
3
- ImageNode,
4
- Node,
5
- PolygonNode,
6
- TextNode,
7
- } from "@lazlon/html-editor/model"
8
- import {
9
- GroupContent,
10
- ImageContent,
11
- PolygonContent,
12
- TextContent,
13
- } from "@lazlon/html-editor/ui"
14
-
15
- export default function NodeContent({ node }: { node: Node }) {
16
- if (node instanceof PolygonNode) {
17
- return <PolygonContent node={node} />
18
- }
19
-
20
- if (node instanceof TextNode) {
21
- return <TextContent placeholder="Text..." node={node} />
22
- }
23
-
24
- if (node instanceof GroupNode) {
25
- return (
26
- <GroupContent node={node}>{(node) => <NodeContent node={node} />}</GroupContent>
27
- )
28
- }
29
-
30
- if (node instanceof ImageNode) {
31
- return <ImageContent node={node} />
32
- }
33
-
34
- throw Error(`unknown node: ${node.name}`)
35
- }
@@ -1,28 +0,0 @@
1
- import { usePage } from "@lazlon/html-editor/hooks"
2
- import { useStore } from "react-bolt"
3
-
4
- export default function SnapLines() {
5
- const page = usePage()
6
- const snapLines = useStore(page, "snapLines")
7
- const [width, height] = useStore(page, "width", "height")
8
-
9
- return (
10
- <>
11
- {snapLines.map(({ x, y, h, w }, i) => (
12
- <div
13
- key={i}
14
- className="bg-black absolute"
15
- style={{
16
- top: `${y ?? 0}px`,
17
- left: `${x ?? 0}px`,
18
- width: `${w ?? (typeof y === "number" ? width : 1)}px`,
19
- height: `${h ?? (typeof x === "number" ? height : 1)}px`,
20
- background: "radial-gradient(circle, black 1px, transparent 1px",
21
- backgroundSize: typeof y === "number" ? "5px 1px" : "1px 5px",
22
- backgroundRepeat: typeof y === "number" ? "repeat-x" : "repeat-y",
23
- }}
24
- />
25
- ))}
26
- </>
27
- )
28
- }
@@ -1,45 +0,0 @@
1
- import { useEditor, useMoveable, usePage } from "@lazlon/html-editor/hooks"
2
- import { getReadableForeground, NodeView } from "@lazlon/html-editor/ui"
3
- import { useStore } from "react-bolt"
4
- import NodeContent from "./NodeContent"
5
- import SnapLines from "./SnapLines"
6
-
7
- export default function PageView() {
8
- const page = usePage()
9
- const zoom = useStore(useEditor(), "zoom")
10
- const [width, height, background] = useStore(page, "width", "height", "background")
11
- const nodes = useStore(page, "nodes").values().toArray()
12
- const movable = useMoveable()
13
-
14
- return (
15
- <div
16
- className="m-auto shrink-0"
17
- style={{
18
- width: Math.round(width * zoom),
19
- height: Math.round(height * zoom),
20
- }}
21
- >
22
- <div
23
- ref={(ref) => void (page.ref = ref)}
24
- className="origin-top-left relative shadow-xl"
25
- style={{
26
- width,
27
- height,
28
- background,
29
- transform: `scale(${zoom})`,
30
- color: getReadableForeground(background),
31
- }}
32
- onPointerDown={(event) => {
33
- movable.onPointerDown(event)
34
- }}
35
- >
36
- {nodes.map((node) => (
37
- <NodeView key={node.id} node={node}>
38
- <NodeContent node={node} />
39
- </NodeView>
40
- ))}
41
- <SnapLines />
42
- </div>
43
- </div>
44
- )
45
- }
@@ -1,24 +0,0 @@
1
- import { useResize } from "@lazlon/html-editor/hooks"
2
- import clsx from "clsx"
3
-
4
- export default function Corner(props: {
5
- className?: string
6
- corner: "ne" | "nw" | "se" | "sw"
7
- }) {
8
- const { className, corner } = props
9
- const { onPointerDown } = useResize(corner)
10
-
11
- return (
12
- <div
13
- onPointerDown={onPointerDown}
14
- className={clsx(
15
- className,
16
- "bg-black size-2 pointer-events-auto absolute select-none",
17
- corner == "ne" && "hover:cursor-ne-resize",
18
- corner == "nw" && "hover:cursor-nw-resize",
19
- corner == "se" && "hover:cursor-se-resize",
20
- corner == "sw" && "hover:cursor-nesw-resize",
21
- )}
22
- />
23
- )
24
- }
@@ -1,21 +0,0 @@
1
- import { useResize } from "@lazlon/html-editor/hooks"
2
- import clsx from "clsx"
3
-
4
- export default function Edge(props: { className?: string; side: "n" | "s" | "w" | "e" }) {
5
- const { className, side } = props
6
- const { onPointerDown } = useResize(side)
7
-
8
- return (
9
- <div
10
- onPointerDown={onPointerDown}
11
- className={clsx(
12
- className,
13
- "bg-black pointer-events-auto absolute select-none",
14
- side == "n" && "hover:cursor-n-resize",
15
- side == "s" && "hover:cursor-s-resize",
16
- side == "e" && "hover:cursor-e-resize",
17
- side == "w" && "hover:cursor-w-resize",
18
- )}
19
- />
20
- )
21
- }
@@ -1,27 +0,0 @@
1
- import { useEditor, useSelectionFrame } from "@lazlon/html-editor/hooks"
2
- import { useStore } from "react-bolt"
3
- import Corner from "./Corner"
4
- import Edge from "./Edge"
5
-
6
- export default function SelectionFrame() {
7
- const editor = useEditor()
8
- const selection = useStore(editor, "selection")
9
- const selectionFrameRef = useSelectionFrame<HTMLDivElement>({
10
- accountForSingleSelection: true,
11
- })
12
-
13
- return (
14
- selection.size > 0 && (
15
- <div ref={selectionFrameRef} className="pointer-events-none absolute inset-0">
16
- <Edge className="-top-1 h-1 w-full" side="n" />
17
- <Edge className="-right-1 h-full w-1" side="e" />
18
- <Edge className="-bottom-1 h-1 w-full" side="s" />
19
- <Edge className="-left-1 h-full w-1" side="w" />
20
- <Corner className="-top-1.5 -right-1.5" corner="ne" />
21
- <Corner className="-right-1.5 -bottom-1.5" corner="se" />
22
- <Corner className="-bottom-1.5 -left-1.5" corner="sw" />
23
- <Corner className="-top-1.5 -left-1.5" corner="nw" />
24
- </div>
25
- )
26
- )
27
- }
@@ -1,32 +0,0 @@
1
- import { useEditor } from "@lazlon/html-editor/hooks"
2
- import clsx from "clsx"
3
- import { useStore } from "react-bolt"
4
-
5
- export default function ActionHud(props: { className?: string }) {
6
- const editor = useEditor()
7
- const { action, payload } = useStore(editor, "action")
8
-
9
- return (
10
- payload && (
11
- <div
12
- className={clsx(
13
- "select-none rounded-lg py-1 px-2 text-sm shadow-lg",
14
- "ring-black/8 ring-1 ring-inset",
15
- props.className,
16
- )}
17
- >
18
- {action === "resize" && (
19
- <span className="text-nowrap">
20
- w:{payload.width}, h:{payload.height}
21
- </span>
22
- )}
23
- {action === "move" && (
24
- <span className="text-nowrap">
25
- x:{payload.x}, y:{payload.y}
26
- </span>
27
- )}
28
- {action === "rotate" && <span className="text-nowrap">r:{payload.deg}°</span>}
29
- </div>
30
- )
31
- )
32
- }
@@ -1,39 +0,0 @@
1
- import { useEditor, useNodeField, useRotation } from "@lazlon/html-editor/hooks"
2
- import clsx from "clsx"
3
- import { useStore } from "react-bolt"
4
-
5
- export default function Rotation(props: { className?: string }) {
6
- const { onPointerDown } = useRotation()
7
- const editor = useEditor()
8
- const selection = useStore(editor, "selection")
9
- const { action } = useStore(editor, "action")
10
- const isLocked = useNodeField(selection, "locked", false)
11
- const shouldShow = !action && !isLocked
12
-
13
- return (
14
- shouldShow && (
15
- <div
16
- className={clsx(
17
- props.className,
18
- "bg-white rounded-full shadow-lg",
19
- "ring-black/8 ring-1 ring-inset",
20
- )}
21
- >
22
- <div onPointerDown={onPointerDown} className="p-2">
23
- <RotateIcon />
24
- </div>
25
- </div>
26
- )
27
- )
28
- }
29
-
30
- function RotateIcon() {
31
- return (
32
- <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24">
33
- <path
34
- fill="currentColor"
35
- d="M15.25 18.48V15a.75.75 0 1 0-1.5 0v4c0 .97.78 1.75 1.75 1.75h4a.75.75 0 1 0 0-1.5h-2.6a8.75 8.75 0 0 0-2.07-15.53.75.75 0 1 0-.49 1.42 7.25 7.25 0 0 1 .91 13.34zM8.75 5.52V9a.75.75 0 0 0 1.5 0V5c0-.97-.78-1.75-1.75-1.75h-4a.75.75 0 0 0 0 1.5h2.6a8.75 8.75 0 0 0 2.18 15.57.75.75 0 0 0 .47-1.43 7.25 7.25 0 0 1-1-13.37z"
36
- />
37
- </svg>
38
- )
39
- }
@@ -1,128 +0,0 @@
1
- import {
2
- useAlignAction,
3
- useBatchSet,
4
- useDistributeAction,
5
- useDuplicateAction,
6
- useEditor,
7
- useGroupAction,
8
- useNodeField,
9
- useStackOrderAction,
10
- useToggleLockAction,
11
- useTrashAction,
12
- } from "@lazlon/html-editor/hooks"
13
- import { GroupNode } from "@lazlon/html-editor/model"
14
- import clsx from "clsx"
15
- import {
16
- Button,
17
- Menu,
18
- MenuItem,
19
- MenuTrigger,
20
- Popover,
21
- SubmenuTrigger,
22
- } from "react-aria-components"
23
- import { useStore } from "react-bolt"
24
-
25
- export default function Toolbar(props: { className?: string }) {
26
- const editor = useEditor()
27
- const selection = useStore(editor, "selection")
28
- const [firstNode, ...restNode] = selection
29
- const { action } = useStore(editor, "action")
30
- const trash = useTrashAction()
31
- const duplicate = useDuplicateAction()
32
- const { group, ungroup } = useGroupAction()
33
- const toggleLock = useToggleLockAction()
34
- const align = useAlignAction()
35
- const distribute = useDistributeAction()
36
- const stack = useStackOrderAction()
37
- const isLocked = useNodeField(selection, "locked", false)
38
- const batchSet = useBatchSet()
39
-
40
- return (
41
- !action && (
42
- <div
43
- className={clsx(
44
- props.className,
45
- "flex gap-2 rounded-lg bg-white border-black/8 border shadow-lg px-2 py-1",
46
- )}
47
- >
48
- <button onClick={() => trash()}>del</button>
49
- <button onClick={() => duplicate()}>dup</button>
50
- {restNode.length > 0 && <button onClick={() => group()}>group</button>}
51
- {firstNode instanceof GroupNode && (
52
- <button onClick={() => ungroup(firstNode)}>ungroup</button>
53
- )}
54
- <button onClick={() => toggleLock()}>{isLocked ? "unlock" : "lock"}</button>
55
- <MenuTrigger>
56
- <Button>menu</Button>
57
- <Popover
58
- offset={12}
59
- placement="right top"
60
- className="bg-white border-black/8 border shadow-lg px-2 py-1 rounded-lg w-52"
61
- >
62
- <Menu>
63
- <SubmenuTrigger>
64
- <MenuItem>align</MenuItem>
65
- <Popover
66
- offset={12}
67
- className="bg-white border-black/8 border shadow-lg px-2 py-1 rounded-lg w-52"
68
- >
69
- <Menu>
70
- <MenuItem onClick={align.right}>right</MenuItem>
71
- <MenuItem onClick={align.center}>center</MenuItem>
72
- <MenuItem onClick={align.left}>left</MenuItem>
73
- <MenuItem onClick={align.top}>top</MenuItem>
74
- <MenuItem onClick={align.middle}>middle</MenuItem>
75
- <MenuItem onClick={align.bottom}>bottom</MenuItem>
76
- </Menu>
77
- </Popover>
78
- </SubmenuTrigger>
79
- <SubmenuTrigger>
80
- <MenuItem>distribute</MenuItem>
81
- <Popover
82
- offset={12}
83
- className="bg-white border-black/8 border shadow-lg px-2 py-1 rounded-lg w-52"
84
- >
85
- <Menu>
86
- <MenuItem onClick={distribute.vertical}>vertically</MenuItem>
87
- <MenuItem onClick={distribute.horizontal}>horizontally</MenuItem>
88
- </Menu>
89
- </Popover>
90
- </SubmenuTrigger>
91
- <SubmenuTrigger>
92
- <MenuItem>stack</MenuItem>
93
- <Popover
94
- offset={12}
95
- className="bg-white border-black/8 border shadow-lg px-2 py-1 rounded-lg w-52"
96
- >
97
- <Menu>
98
- <MenuItem
99
- isDisabled={!stack.canBringBackward}
100
- onClick={stack.bringBackward}
101
- >
102
- bring backward
103
- </MenuItem>
104
- <MenuItem
105
- isDisabled={!stack.canBringForward}
106
- onClick={stack.bringForward}
107
- >
108
- bring forward
109
- </MenuItem>
110
- <MenuItem onClick={stack.bringToFront}>bring to front</MenuItem>
111
- <MenuItem onClick={stack.bringToBack}>bring to back</MenuItem>
112
- </Menu>
113
- </Popover>
114
- </SubmenuTrigger>
115
- <MenuItem
116
- onClick={() =>
117
- batchSet(selection, (n) => ({ rotation: n.rotation + 90 }))
118
- }
119
- >
120
- rotate 90°
121
- </MenuItem>
122
- </Menu>
123
- </Popover>
124
- </MenuTrigger>
125
- </div>
126
- )
127
- )
128
- }
@@ -1,21 +0,0 @@
1
- import { useEditor, useSelectionFrame } from "@lazlon/html-editor/hooks"
2
- import { useStore } from "react-bolt"
3
- import ActionHud from "./ActionHud"
4
- import Rotation from "./Rotation"
5
- import Toolbar from "./Toolbar"
6
-
7
- export default function SelectionOverlay() {
8
- const editor = useEditor()
9
- const selection = useStore(editor, "selection")
10
- const frameRef = useSelectionFrame<HTMLDivElement>()
11
-
12
- return (
13
- selection.size > 0 && (
14
- <div ref={frameRef} className="pointer-events-none absolute inset-0">
15
- <ActionHud className="absolute -bottom-14 left-1/2 -translate-x-1/2" />
16
- <Rotation className="pointer-events-auto absolute -bottom-18 left-1/2 -translate-x-1/2" />
17
- <Toolbar className="pointer-events-auto absolute -top-18 left-1/2 -translate-x-1/2" />
18
- </div>
19
- )
20
- )
21
- }
@@ -1,68 +0,0 @@
1
- import { useEditor, useTextMarks } from "@lazlon/html-editor/hooks"
2
- import { EditableNode, FormattableNode } from "@lazlon/html-editor/model"
3
- import { getReadableForeground } from "@lazlon/html-editor/ui"
4
- import clsx from "clsx"
5
- import { ColorField, Input, NumberField, Separator } from "react-aria-components"
6
- import { useStore } from "react-bolt"
7
-
8
- export default function Toolbar() {
9
- const editor = useEditor()
10
- const [page] = useStore(editor, "pages").values()
11
- const selection = useStore(editor, "selection").values().toArray()
12
-
13
- const { state, toggle, setColor, setSize } = useTextMarks({
14
- editables: selection.filter((n) => n instanceof EditableNode),
15
- formattables: selection.filter((n) => n instanceof FormattableNode),
16
- })
17
-
18
- return (
19
- <div
20
- className={clsx(
21
- "absolute top-4 left-1/2 -translate-x-1/2",
22
- "bg-white rounded-lg border-black/8 border shadow-lg px-2 py-1 ",
23
- "flex gap-4 items-center",
24
- )}
25
- >
26
- <div className="flex gap-2">
27
- <button onClick={() => (editor.zoom += 0.05)}>zoom+</button>
28
- <button onClick={() => (editor.zoom -= 0.05)}>zoom-</button>
29
- </div>
30
-
31
- {state && (
32
- <>
33
- <Separator className="h-7 bg-black w-px" />
34
- <div className="flex gap-2">
35
- {page && (
36
- <ColorField
37
- aria-label="color"
38
- value={state.color ?? getReadableForeground(page.background)}
39
- onChange={(c) =>
40
- setColor(c?.toString("hex") ?? getReadableForeground(page.background), {
41
- end: true,
42
- })
43
- }
44
- >
45
- <Input
46
- className="w-18"
47
- onKeyDown={(e) => {
48
- if (e.key === "Enter") {
49
- // HACK: ReactAria does not apply changes on enter by default
50
- e.currentTarget.blur()
51
- e.currentTarget.focus()
52
- }
53
- }}
54
- />
55
- </ColorField>
56
- )}
57
- <button onClick={() => toggle("Bold")}>bold</button>
58
- <button onClick={() => toggle("Italic")}>italic</button>
59
- <button onClick={() => toggle("Underline")}>underline</button>
60
- <NumberField value={state.size} aria-label="font size" onChange={setSize}>
61
- <Input className="w-8" />
62
- </NumberField>
63
- </div>
64
- </>
65
- )}
66
- </div>
67
- )
68
- }
@@ -1,47 +0,0 @@
1
- import { PageContext, useEditor, useSelector } from "@lazlon/html-editor/hooks"
2
- import clsx from "clsx"
3
- import { useRef } from "react"
4
- import { useStore } from "react-bolt"
5
- import PageView from "./PageView"
6
- import SelectionFrame from "./SelectionFrame"
7
- import SelectionOverlay from "./SelectionOverlay"
8
- import Toolbar from "./Toolbar"
9
-
10
- export default function EditorView(props: { className?: string }) {
11
- const editor = useEditor()
12
- const selection = useStore(editor, "selection")
13
- const selectorRef = useRef<HTMLDivElement>(null)
14
- const pages = useStore(editor, "pages").values().toArray()
15
- const selector = useSelector({ ref: selectorRef })
16
- const key = selection
17
- .values()
18
- .toArray()
19
- .map((n) => n.id)
20
- .join("")
21
-
22
- return (
23
- <div className={clsx("relative size-full overflow-auto", props.className)}>
24
- <div
25
- ref={(ref) => void (editor.ref = ref)}
26
- className="relative flex size-full overflow-auto p-14 gap-4"
27
- onPointerDown={(event) => {
28
- selector.onPointerDown(event)
29
- }}
30
- >
31
- {pages.map((page) => (
32
- <PageContext key={page.id} value={page}>
33
- <PageView />
34
- </PageContext>
35
- ))}
36
- </div>
37
- <div
38
- ref={selectorRef}
39
- className="absolute inset-0 border"
40
- style={{ display: "none" }}
41
- />
42
- <SelectionOverlay />
43
- <SelectionFrame />
44
- <Toolbar key={key} />
45
- </div>
46
- )
47
- }