@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.
- package/package.json +14 -11
- package/.claude/settings.local.json +0 -9
- package/.github/workflows/ci.yml +0 -34
- package/demo/App.tsx +0 -62
- package/demo/EditorView/PageView/NodeContent.tsx +0 -35
- package/demo/EditorView/PageView/SnapLines.tsx +0 -28
- package/demo/EditorView/PageView/index.tsx +0 -45
- package/demo/EditorView/SelectionFrame/Corner.tsx +0 -24
- package/demo/EditorView/SelectionFrame/Edge.tsx +0 -21
- package/demo/EditorView/SelectionFrame/index.tsx +0 -27
- package/demo/EditorView/SelectionOverlay/ActionHud.tsx +0 -32
- package/demo/EditorView/SelectionOverlay/Rotation.tsx +0 -39
- package/demo/EditorView/SelectionOverlay/Toolbar.tsx +0 -128
- package/demo/EditorView/SelectionOverlay/index.tsx +0 -21
- package/demo/EditorView/Toolbar/index.tsx +0 -68
- package/demo/EditorView/index.tsx +0 -47
- package/demo/Navbar/index.tsx +0 -33
- package/demo/Sidebar/index.tsx +0 -71
- package/demo/hotkeys.ts +0 -93
- package/demo/main.tsx +0 -10
- package/demo/style.css +0 -1
- package/eslint.config.js +0 -43
- package/index.html +0 -14
- package/tests/createTestEditor.ts +0 -19
- package/tests/hooks/actions.test.tsx +0 -736
- package/tests/hooks/batch.test.tsx +0 -332
- package/tests/hooks/editor.test.tsx +0 -56
- package/tests/hooks/page.test.tsx +0 -135
- package/tests/hooks/pointer/pointer.test.tsx +0 -244
- package/tests/hooks/textMarks.test.tsx +0 -624
- package/tests/model/editor.test.ts +0 -384
- package/tests/model/history.test.ts +0 -293
- package/tests/model/node/group.test.ts +0 -294
- package/tests/model/node/image.test.ts +0 -150
- package/tests/model/node/polygon.test.ts +0 -408
- package/tests/model/node/text.test.ts +0 -158
- package/tests/model/node.test.ts +0 -276
- package/tests/model/page.test.ts +0 -150
- package/tests/setup.ts +0 -7
- package/tsconfig.json +0 -28
- package/vite.config.ts +0 -9
- 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
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
|
|
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
|
+
}
|
package/.github/workflows/ci.yml
DELETED
|
@@ -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
|
-
}
|