@lazlon-platform/html-editor 0.5.0 → 0.6.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.
- package/lib/hooks/actions.ts +51 -24
- package/lib/hooks/batch.ts +17 -7
- package/lib/hooks/index.ts +1 -0
- package/lib/hooks/pointer/movePoint.ts +75 -0
- package/lib/hooks/pointer/moveable.ts +22 -8
- package/lib/hooks/pointer/pointer.ts +2 -3
- package/lib/hooks/pointer/resize.ts +2 -2
- package/lib/hooks/pointer/rotation.ts +74 -39
- package/lib/hooks/pointer/selector.ts +16 -2
- package/lib/hooks/pointer/snap.ts +3 -3
- package/lib/hooks/textMarks.ts +1 -3
- package/lib/lib/googleFonts.ts +1 -5
- package/lib/model/editor.ts +13 -9
- package/lib/model/geometry/math.ts +139 -0
- package/lib/model/history.ts +10 -13
- package/lib/model/index.ts +7 -10
- package/lib/model/node/{editable → editableNode}/index.ts +13 -29
- package/lib/model/node/{formattable.ts → formattableNode/index.ts} +5 -11
- package/lib/model/node/{group.ts → groupNode.ts} +9 -13
- package/lib/model/node/{image.ts → imageNode.ts} +5 -11
- package/lib/model/node/lineNode.ts +59 -0
- package/lib/model/node/{shape/shape.ts → shapeNode/index.ts} +30 -15
- package/lib/model/node/shapeNode/shape.ts +96 -0
- package/lib/model/node/{text.ts → textNode.ts} +6 -12
- package/lib/model/node.ts +9 -23
- package/lib/model/page.ts +1 -2
- package/lib/model/traversal.ts +1 -1
- package/lib/ui/extractor.ts +3 -3
- package/lib/ui/index.ts +2 -4
- package/lib/ui/node/{EditableContent.tsx → EditableContent/index.tsx} +4 -3
- package/lib/ui/node/GroupContent.tsx +1 -1
- package/lib/ui/node/ImageContent.tsx +1 -1
- package/lib/ui/node/LineContent.tsx +32 -0
- package/lib/ui/node/ShapeContent/ArrowContent.tsx +57 -0
- package/lib/ui/node/ShapeContent/EllipseContent.tsx +37 -0
- package/lib/ui/node/ShapeContent/PolygonContent.tsx +62 -0
- package/lib/ui/node/ShapeContent/RectangleContent.tsx +35 -0
- package/lib/ui/node/ShapeContent/StarContent.tsx +75 -0
- package/lib/ui/node/ShapeContent/index.tsx +43 -0
- package/lib/ui/node/TextContent.tsx +1 -1
- package/package.json +1 -1
- package/lib/model/node/shape/arrow.ts +0 -50
- package/lib/model/node/shape/ellipse.ts +0 -26
- package/lib/model/node/shape/polygon.ts +0 -130
- package/lib/model/node/shape/star.ts +0 -91
- package/lib/ui/node/ArrowContent.tsx +0 -60
- package/lib/ui/node/EllipseContent.tsx +0 -49
- package/lib/ui/node/PolygonContent.tsx +0 -81
- package/lib/ui/node/StarContent.tsx +0 -60
- /package/lib/model/node/{editable → editableNode}/letterSpacing.ts +0 -0
- /package/lib/model/node/{editable → editableNode}/persistentMarks.ts +0 -0
- /package/lib/model/node/{editable → editableNode}/tiptapExtensions.ts +0 -0
- /package/lib/ui/node/{useDoubleClick.ts → EditableContent/useDoubleClick.ts} +0 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// shapes are not reactive stores because the batch dispatcher and history model
|
|
2
|
+
// is designed for non-nested stores
|
|
3
|
+
|
|
4
|
+
import { clamp } from "es-toolkit"
|
|
5
|
+
|
|
6
|
+
export interface StarShape {
|
|
7
|
+
name: "star"
|
|
8
|
+
props: {
|
|
9
|
+
corners: number
|
|
10
|
+
roundness: number
|
|
11
|
+
depth: number
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function newStarShape(props?: Partial<StarShape["props"]>): StarShape {
|
|
16
|
+
return {
|
|
17
|
+
name: "star",
|
|
18
|
+
props: {
|
|
19
|
+
corners: Math.max(props?.corners ?? 5, 3),
|
|
20
|
+
roundness: Math.max(props?.roundness ?? 0, 0),
|
|
21
|
+
depth: clamp(props?.depth ?? 0.4, 0, 1),
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ArrowShape {
|
|
27
|
+
name: "arrow"
|
|
28
|
+
props: {
|
|
29
|
+
roundness: number
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function newArrowShape(props?: Partial<ArrowShape["props"]>): ArrowShape {
|
|
34
|
+
return {
|
|
35
|
+
name: "arrow",
|
|
36
|
+
props: {
|
|
37
|
+
roundness: Math.max(props?.roundness ?? 0, 0),
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface EllipseShape {
|
|
43
|
+
name: "ellipse"
|
|
44
|
+
props: { _?: never }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function newEllipseShape(): EllipseShape {
|
|
48
|
+
return {
|
|
49
|
+
name: "ellipse",
|
|
50
|
+
props: {},
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface PolygonShape {
|
|
55
|
+
name: "polygon"
|
|
56
|
+
props: {
|
|
57
|
+
roundness: number
|
|
58
|
+
sides: number
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function newPolygonShape(props?: Partial<PolygonShape["props"]>): PolygonShape {
|
|
63
|
+
return {
|
|
64
|
+
name: "polygon",
|
|
65
|
+
props: {
|
|
66
|
+
roundness: Math.max(props?.roundness ?? 0, 0),
|
|
67
|
+
sides: clamp(props?.sides ?? 5, 3, 20),
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface RectangleShape {
|
|
73
|
+
name: "rectangle"
|
|
74
|
+
props: {
|
|
75
|
+
cornerTopLeft: number
|
|
76
|
+
cornerTopRight: number
|
|
77
|
+
cornerBottomLeft: number
|
|
78
|
+
cornerBottomRight: number
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function newRectangleShape(
|
|
83
|
+
props?: Partial<RectangleShape["props"]>,
|
|
84
|
+
): RectangleShape {
|
|
85
|
+
return {
|
|
86
|
+
name: "rectangle",
|
|
87
|
+
props: {
|
|
88
|
+
cornerTopLeft: Math.max(props?.cornerTopLeft ?? 0, 0),
|
|
89
|
+
cornerTopRight: Math.max(props?.cornerTopRight ?? 0, 0),
|
|
90
|
+
cornerBottomLeft: Math.max(props?.cornerBottomLeft ?? 0, 0),
|
|
91
|
+
cornerBottomRight: Math.max(props?.cornerBottomRight ?? 0, 0),
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export type Shape = RectangleShape | ArrowShape | PolygonShape | StarShape | EllipseShape
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import { computed, state } from "react-bolt"
|
|
2
|
-
import type { Editor } from "../editor"
|
|
3
2
|
import type { Page } from "../page"
|
|
4
|
-
import type
|
|
5
|
-
import { EditableNode, type EditableNodeProps } from "./editable"
|
|
3
|
+
import { EditableNode, type EditableNodeProps } from "./editableNode"
|
|
6
4
|
|
|
7
5
|
export type TextNodeProps = EditableNodeProps & Partial<Pick<TextNode, "halign" | "size">>
|
|
8
6
|
|
|
9
7
|
export class TextNode extends EditableNode {
|
|
10
|
-
get name() {
|
|
8
|
+
get name(): "text" {
|
|
11
9
|
return "text"
|
|
12
10
|
}
|
|
13
11
|
|
|
@@ -27,8 +25,8 @@ export class TextNode extends EditableNode {
|
|
|
27
25
|
// no-op: TextNode's height can be set using its font-size
|
|
28
26
|
}
|
|
29
27
|
|
|
30
|
-
constructor(
|
|
31
|
-
super(
|
|
28
|
+
constructor(page: Page, { halign, size, ...props }: TextNodeProps) {
|
|
29
|
+
super(page, props)
|
|
32
30
|
this.halign = halign ?? "center"
|
|
33
31
|
this.size = size ?? 16
|
|
34
32
|
|
|
@@ -53,12 +51,8 @@ export class TextNode extends EditableNode {
|
|
|
53
51
|
return super.contentRef
|
|
54
52
|
}
|
|
55
53
|
|
|
56
|
-
props(): TextNodeProps {
|
|
54
|
+
get props(): TextNodeProps {
|
|
57
55
|
const { halign, size } = this
|
|
58
|
-
return { ...super.props
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
serialize(): SerializedNode<this["name"], TextNodeProps> {
|
|
62
|
-
return super.serialize()
|
|
56
|
+
return { ...super.props, halign, size }
|
|
63
57
|
}
|
|
64
58
|
}
|
package/lib/model/node.ts
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
import { computed, state } from "react-bolt"
|
|
2
|
-
import type { Editor } from "./editor"
|
|
3
|
-
import type { Page } from "./page"
|
|
4
2
|
import { deg, type Deg } from "./geometry/math"
|
|
3
|
+
import type { Page } from "./page"
|
|
5
4
|
|
|
6
5
|
export type Schema = Map<string, typeof Node>
|
|
7
6
|
|
|
8
|
-
export type SerializedNode<
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
> = {
|
|
12
|
-
name: Name
|
|
13
|
-
props: Props
|
|
7
|
+
export type SerializedNode<N extends Node = Node> = {
|
|
8
|
+
name: N["name"]
|
|
9
|
+
props: N["props"]
|
|
14
10
|
}
|
|
15
11
|
|
|
16
12
|
export interface NodeProps
|
|
@@ -23,6 +19,7 @@ export interface NodeProps
|
|
|
23
19
|
|
|
24
20
|
export abstract class Node {
|
|
25
21
|
abstract get name(): string
|
|
22
|
+
|
|
26
23
|
readonly id: string
|
|
27
24
|
readonly page: Page
|
|
28
25
|
|
|
@@ -34,8 +31,8 @@ export abstract class Node {
|
|
|
34
31
|
@state accessor rotation: Deg
|
|
35
32
|
@state accessor role: string | null
|
|
36
33
|
@state accessor locked: boolean
|
|
37
|
-
@state accessor x: number
|
|
38
|
-
@state accessor y: number
|
|
34
|
+
@state accessor x: number = 0
|
|
35
|
+
@state accessor y: number = 0
|
|
39
36
|
@state accessor css: string
|
|
40
37
|
|
|
41
38
|
@computed get width() {
|
|
@@ -56,7 +53,7 @@ export abstract class Node {
|
|
|
56
53
|
this._height = n
|
|
57
54
|
}
|
|
58
55
|
|
|
59
|
-
constructor(
|
|
56
|
+
constructor(page: Page, props: NodeProps) {
|
|
60
57
|
this.page = page
|
|
61
58
|
this.id = props.id
|
|
62
59
|
this.x = props.x
|
|
@@ -69,11 +66,7 @@ export abstract class Node {
|
|
|
69
66
|
this.css = props.css ?? ""
|
|
70
67
|
}
|
|
71
68
|
|
|
72
|
-
|
|
73
|
-
return !!this.locked
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
props(): NodeProps {
|
|
69
|
+
get props(): NodeProps {
|
|
77
70
|
const { id, role, locked, rotation, css, x, y, width, height } = this
|
|
78
71
|
return {
|
|
79
72
|
id,
|
|
@@ -87,11 +80,4 @@ export abstract class Node {
|
|
|
87
80
|
...(rotation && { rotation }),
|
|
88
81
|
}
|
|
89
82
|
}
|
|
90
|
-
|
|
91
|
-
serialize(): SerializedNode {
|
|
92
|
-
return {
|
|
93
|
-
name: this.name,
|
|
94
|
-
props: this.props(),
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
83
|
}
|
package/lib/model/page.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { state } from "react-bolt"
|
|
2
2
|
import type { Editor } from "./editor"
|
|
3
3
|
import { Node, type SerializedNode } from "./node"
|
|
4
|
-
// import { createRef } from "react"
|
|
5
4
|
|
|
6
5
|
export interface PageProps
|
|
7
6
|
extends Partial<Pick<Page, "background" | "width" | "height">> {
|
|
@@ -46,7 +45,7 @@ export class Page {
|
|
|
46
45
|
height: this.height,
|
|
47
46
|
nodes: this.nodes
|
|
48
47
|
.values()
|
|
49
|
-
.map((node) =>
|
|
48
|
+
.map((node) => this.editor.serializeNode(node))
|
|
50
49
|
.toArray(),
|
|
51
50
|
}
|
|
52
51
|
}
|
package/lib/model/traversal.ts
CHANGED
package/lib/ui/extractor.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { JSONContent } from "@tiptap/core"
|
|
2
2
|
import { uniq } from "es-toolkit"
|
|
3
3
|
import { Node } from "../model/node"
|
|
4
|
-
import { EditableNode } from "../model/node/
|
|
5
|
-
import { FormattableNode } from "../model/node/
|
|
6
|
-
import { ShapeNode } from "../model/node/
|
|
4
|
+
import { EditableNode } from "../model/node/editableNode"
|
|
5
|
+
import { FormattableNode } from "../model/node/formattableNode"
|
|
6
|
+
import { ShapeNode } from "../model/node/shapeNode"
|
|
7
7
|
import { Page } from "../model/page"
|
|
8
8
|
import { flattenNodes } from "../model/traversal"
|
|
9
9
|
|
package/lib/ui/index.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
export { getReadableForeground } from "./colors"
|
|
2
2
|
export { getColors, getFontFamilies } from "./extractor"
|
|
3
|
-
export { ArrowContent } from "./node/ArrowContent"
|
|
4
3
|
export { EditableContent } from "./node/EditableContent"
|
|
5
|
-
export { EllipseContent } from "./node/EllipseContent"
|
|
6
4
|
export { GroupContent } from "./node/GroupContent"
|
|
7
5
|
export { ImageContent } from "./node/ImageContent"
|
|
6
|
+
export { LineContent } from "./node/LineContent"
|
|
8
7
|
export { NodeView } from "./node/NodeView"
|
|
9
|
-
export {
|
|
10
|
-
export { StarContent } from "./node/StarContent"
|
|
8
|
+
export { ShapeContent } from "./node/ShapeContent"
|
|
11
9
|
export { TextContent } from "./node/TextContent"
|
|
@@ -3,8 +3,8 @@ import clsx from "clsx"
|
|
|
3
3
|
import { debounce, isEqual } from "es-toolkit"
|
|
4
4
|
import { useCallback, useEffect, useRef, useState } from "react"
|
|
5
5
|
import { useStore } from "react-bolt"
|
|
6
|
-
import { useEditor } from "
|
|
7
|
-
import type { EditableNode
|
|
6
|
+
import { useEditor } from "../../../hooks/editor"
|
|
7
|
+
import type { EditableNode } from "../../../model"
|
|
8
8
|
import { useDoubleClick } from "./useDoubleClick"
|
|
9
9
|
|
|
10
10
|
export function EditableContent(props: {
|
|
@@ -38,6 +38,7 @@ export function EditableContent(props: {
|
|
|
38
38
|
useEffect(() => {
|
|
39
39
|
if (!selection.has(node)) {
|
|
40
40
|
node.blur()
|
|
41
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
41
42
|
setClicked(false)
|
|
42
43
|
}
|
|
43
44
|
}, [selection, node])
|
|
@@ -47,7 +48,7 @@ export function EditableContent(props: {
|
|
|
47
48
|
const next = node.tiptap.getJSON()
|
|
48
49
|
const prev = prevContent.current
|
|
49
50
|
if (!isEqual(prev, next)) {
|
|
50
|
-
editor.history.push<
|
|
51
|
+
editor.history.push<EditableNode>({
|
|
51
52
|
undo: ["set-node-props", [node.id, { content: prev }]],
|
|
52
53
|
redo: ["set-node-props", [node.id, { content: next }]],
|
|
53
54
|
})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useStore } from "react-bolt"
|
|
2
2
|
import type { Node } from "../../model/node"
|
|
3
|
-
import type { GroupNode } from "../../model
|
|
3
|
+
import type { GroupNode } from "../../model"
|
|
4
4
|
|
|
5
5
|
function GroupedNodeContent(props: { node: Node; children: React.ReactNode }) {
|
|
6
6
|
const { node, children } = props
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import clsx from "clsx"
|
|
2
2
|
import { useStore } from "react-bolt"
|
|
3
|
-
import type { ImageNode } from "../../model
|
|
3
|
+
import type { ImageNode } from "../../model"
|
|
4
4
|
|
|
5
5
|
export function ImageContent(props: { node: ImageNode; fallback?: React.ReactNode }) {
|
|
6
6
|
const { node, fallback } = props
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useStore } from "react-bolt"
|
|
2
|
+
import type { LineNode } from "../../model/node/lineNode"
|
|
3
|
+
|
|
4
|
+
export function LineContent(props: { node: LineNode }) {
|
|
5
|
+
const [width, height, points, strokeWidth, strokeColor] = useStore(
|
|
6
|
+
props.node,
|
|
7
|
+
"width",
|
|
8
|
+
"height",
|
|
9
|
+
"points",
|
|
10
|
+
"strokeWidth",
|
|
11
|
+
"strokeColor",
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
const polylinePoints = points.map(({ x, y }) => `${x},${y}`).join(" ")
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<svg
|
|
18
|
+
className="absolute inset-0 overflow-visible"
|
|
19
|
+
width={width}
|
|
20
|
+
height={height}
|
|
21
|
+
fill="none"
|
|
22
|
+
>
|
|
23
|
+
<polyline
|
|
24
|
+
points={polylinePoints}
|
|
25
|
+
stroke={strokeColor}
|
|
26
|
+
strokeWidth={strokeWidth}
|
|
27
|
+
strokeLinecap="round"
|
|
28
|
+
strokeLinejoin="round"
|
|
29
|
+
/>
|
|
30
|
+
</svg>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useId } from "react"
|
|
2
|
+
import { useStore } from "react-bolt"
|
|
3
|
+
import { roundedPathData } from "../../../model/geometry/svg"
|
|
4
|
+
import type { ArrowShape, ShapeNode } from "../../../model/node/shapeNode"
|
|
5
|
+
|
|
6
|
+
function arrowPath(props: { height: number; width: number; roundness: number }) {
|
|
7
|
+
const { height, width, roundness } = props
|
|
8
|
+
const bodyRight = width * 0.68
|
|
9
|
+
const inset = height * 0.28
|
|
10
|
+
|
|
11
|
+
const arrowPoints = [
|
|
12
|
+
{ x: 0, y: inset },
|
|
13
|
+
{ x: bodyRight, y: inset },
|
|
14
|
+
{ x: bodyRight, y: 0 },
|
|
15
|
+
{ x: width, y: height / 2 },
|
|
16
|
+
{ x: bodyRight, y: height },
|
|
17
|
+
{ x: bodyRight, y: height - inset },
|
|
18
|
+
{ x: 0, y: height - inset },
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
return roundedPathData(arrowPoints, roundness)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function ArrowContent(props: { node: ShapeNode; shape: ArrowShape["props"] }) {
|
|
25
|
+
const maskId = useId()
|
|
26
|
+
const { node, shape } = props
|
|
27
|
+
|
|
28
|
+
const [background, borderWidth, borderColor] = useStore(
|
|
29
|
+
node,
|
|
30
|
+
"background",
|
|
31
|
+
"borderWidth",
|
|
32
|
+
"borderColor",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
const [width, height] = useStore(node, "width", "height")
|
|
36
|
+
const { roundness } = shape
|
|
37
|
+
const d = arrowPath({ width, height, roundness })
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<svg width={width} height={height} className="absolute inset-0">
|
|
41
|
+
<defs>
|
|
42
|
+
<mask id={maskId} maskUnits="userSpaceOnUse">
|
|
43
|
+
<path d={d} fill="white" />
|
|
44
|
+
</mask>
|
|
45
|
+
</defs>
|
|
46
|
+
|
|
47
|
+
<path d={d} fill={background} />
|
|
48
|
+
<path
|
|
49
|
+
d={d}
|
|
50
|
+
fill="none"
|
|
51
|
+
stroke={borderColor}
|
|
52
|
+
strokeWidth={borderWidth}
|
|
53
|
+
mask={`url(#${maskId})`}
|
|
54
|
+
/>
|
|
55
|
+
</svg>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useStore } from "react-bolt"
|
|
2
|
+
import type { ShapeNode } from "../../../model/node/shapeNode"
|
|
3
|
+
|
|
4
|
+
export function EllipseContent(props: { node: ShapeNode }) {
|
|
5
|
+
const { node } = props
|
|
6
|
+
|
|
7
|
+
const [background, borderWidth, borderColor] = useStore(
|
|
8
|
+
node,
|
|
9
|
+
"background",
|
|
10
|
+
"borderWidth",
|
|
11
|
+
"borderColor",
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<svg
|
|
16
|
+
aria-hidden="true"
|
|
17
|
+
className="absolute"
|
|
18
|
+
onDragStart={(e) => e.preventDefault()}
|
|
19
|
+
style={{
|
|
20
|
+
inset: borderWidth / 2,
|
|
21
|
+
width: `calc(100% - ${borderWidth}px)`,
|
|
22
|
+
height: `calc(100% - ${borderWidth}px)`,
|
|
23
|
+
overflow: "visible",
|
|
24
|
+
}}
|
|
25
|
+
>
|
|
26
|
+
<ellipse
|
|
27
|
+
cx="50%"
|
|
28
|
+
cy="50%"
|
|
29
|
+
rx="50%"
|
|
30
|
+
ry="50%"
|
|
31
|
+
fill={background}
|
|
32
|
+
stroke={borderColor}
|
|
33
|
+
strokeWidth={borderWidth}
|
|
34
|
+
/>
|
|
35
|
+
</svg>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { useId } from "react"
|
|
2
|
+
import { useStore } from "react-bolt"
|
|
3
|
+
import type { Point } from "../../../model/geometry/math"
|
|
4
|
+
import { roundedPathData } from "../../../model/geometry/svg"
|
|
5
|
+
import type { PolygonShape, ShapeNode } from "../../../model/node/shapeNode"
|
|
6
|
+
|
|
7
|
+
function regularPolygonPoints(props: {
|
|
8
|
+
width: number
|
|
9
|
+
height: number
|
|
10
|
+
sides: number
|
|
11
|
+
}): Point[] {
|
|
12
|
+
const { width, height, sides } = props
|
|
13
|
+
const rotation = sides % 2 === 0 ? Math.PI / sides : 0
|
|
14
|
+
const cx = width / 2
|
|
15
|
+
const cy = height / 2
|
|
16
|
+
const rx = width / 2
|
|
17
|
+
const ry = height / 2
|
|
18
|
+
|
|
19
|
+
return Array.from({ length: sides }, (_, i) => {
|
|
20
|
+
const angle = (i * Math.PI * 2) / sides - Math.PI / 2 + rotation
|
|
21
|
+
return {
|
|
22
|
+
x: cx + rx * Math.cos(angle),
|
|
23
|
+
y: cy + ry * Math.sin(angle),
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function PolygonContent(props: { node: ShapeNode; shape: PolygonShape["props"] }) {
|
|
29
|
+
const maskId = useId()
|
|
30
|
+
const { node, shape } = props
|
|
31
|
+
|
|
32
|
+
const [background, borderWidth, borderColor] = useStore(
|
|
33
|
+
node,
|
|
34
|
+
"background",
|
|
35
|
+
"borderWidth",
|
|
36
|
+
"borderColor",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
const [width, height] = useStore(node, "width", "height")
|
|
40
|
+
const { sides, roundness } = shape
|
|
41
|
+
|
|
42
|
+
const d = roundedPathData(regularPolygonPoints({ width, height, sides }), roundness)
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<svg width={width} height={height} className="absolute inset-0">
|
|
46
|
+
<defs>
|
|
47
|
+
<mask id={maskId} maskUnits="userSpaceOnUse">
|
|
48
|
+
<path d={d} fill="white" />
|
|
49
|
+
</mask>
|
|
50
|
+
</defs>
|
|
51
|
+
|
|
52
|
+
<path d={d} fill={background} />
|
|
53
|
+
<path
|
|
54
|
+
d={d}
|
|
55
|
+
fill="none"
|
|
56
|
+
stroke={borderColor}
|
|
57
|
+
strokeWidth={borderWidth}
|
|
58
|
+
mask={`url(#${maskId})`}
|
|
59
|
+
/>
|
|
60
|
+
</svg>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { useStore } from "react-bolt"
|
|
2
|
+
import type { RectangleShape, ShapeNode } from "../../../model/node/shapeNode"
|
|
3
|
+
|
|
4
|
+
export function RectangleShape(props: {
|
|
5
|
+
node: ShapeNode
|
|
6
|
+
shape: RectangleShape["props"]
|
|
7
|
+
}) {
|
|
8
|
+
const { node, shape } = props
|
|
9
|
+
|
|
10
|
+
const [background, borderWidth, borderColor] = useStore(
|
|
11
|
+
node,
|
|
12
|
+
"background",
|
|
13
|
+
"borderWidth",
|
|
14
|
+
"borderColor",
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
const radii = [
|
|
18
|
+
shape["cornerTopLeft"],
|
|
19
|
+
shape["cornerTopRight"],
|
|
20
|
+
shape["cornerBottomRight"],
|
|
21
|
+
shape["cornerBottomLeft"],
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div
|
|
26
|
+
className="absolute inset-0 size-full border-4"
|
|
27
|
+
onDragStart={(e) => e.preventDefault()}
|
|
28
|
+
style={{
|
|
29
|
+
borderRadius: radii.map((r) => `${r}px`).join(" "),
|
|
30
|
+
background: background,
|
|
31
|
+
border: `${borderWidth}px solid ${borderColor}`,
|
|
32
|
+
}}
|
|
33
|
+
/>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { useId } from "react"
|
|
2
|
+
import { useStore } from "react-bolt"
|
|
3
|
+
import type { Point } from "../../../model/geometry/math"
|
|
4
|
+
import { roundedPathData } from "../../../model/geometry/svg"
|
|
5
|
+
import type { ShapeNode, StarShape } from "../../../model/node/shapeNode"
|
|
6
|
+
|
|
7
|
+
function starPoints(props: {
|
|
8
|
+
width: number
|
|
9
|
+
height: number
|
|
10
|
+
corners: number
|
|
11
|
+
depth: number
|
|
12
|
+
}): Point[] {
|
|
13
|
+
const { width, height, corners, depth } = props
|
|
14
|
+
const cx = width / 2
|
|
15
|
+
const cy = height / 2
|
|
16
|
+
const outerRx = width / 2
|
|
17
|
+
const outerRy = height / 2
|
|
18
|
+
const innerRx = outerRx * depth
|
|
19
|
+
const innerRy = outerRy * depth
|
|
20
|
+
|
|
21
|
+
return Array.from({ length: corners * 2 }, (_, i) => {
|
|
22
|
+
const angle = (i * Math.PI) / corners - Math.PI / 2
|
|
23
|
+
const isOuter = i % 2 === 0
|
|
24
|
+
const rx = isOuter ? outerRx : innerRx
|
|
25
|
+
const ry = isOuter ? outerRy : innerRy
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
x: cx + rx * Math.cos(angle),
|
|
29
|
+
y: cy + ry * Math.sin(angle),
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function StarContent(props: { node: ShapeNode; shape: StarShape["props"] }) {
|
|
35
|
+
const maskId = useId()
|
|
36
|
+
const { node, shape } = props
|
|
37
|
+
|
|
38
|
+
const [background, borderWidth, borderColor] = useStore(
|
|
39
|
+
node,
|
|
40
|
+
"background",
|
|
41
|
+
"borderWidth",
|
|
42
|
+
"borderColor",
|
|
43
|
+
)
|
|
44
|
+
const [width, height] = useStore(node, "width", "height")
|
|
45
|
+
const { corners, depth, roundness } = shape
|
|
46
|
+
|
|
47
|
+
const d = roundedPathData(
|
|
48
|
+
starPoints({
|
|
49
|
+
width,
|
|
50
|
+
height,
|
|
51
|
+
corners,
|
|
52
|
+
depth,
|
|
53
|
+
}),
|
|
54
|
+
roundness,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<svg width={width} height={height} className="absolute inset-0">
|
|
59
|
+
<defs>
|
|
60
|
+
<mask id={maskId} maskUnits="userSpaceOnUse">
|
|
61
|
+
<path d={d} fill="white" />
|
|
62
|
+
</mask>
|
|
63
|
+
</defs>
|
|
64
|
+
|
|
65
|
+
<path d={d} fill={background} />
|
|
66
|
+
<path
|
|
67
|
+
d={d}
|
|
68
|
+
fill="none"
|
|
69
|
+
stroke={borderColor}
|
|
70
|
+
strokeWidth={borderWidth}
|
|
71
|
+
mask={`url(#${maskId})`}
|
|
72
|
+
/>
|
|
73
|
+
</svg>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import clsx from "clsx"
|
|
2
|
+
import { useStore } from "react-bolt"
|
|
3
|
+
import type { ShapeNode } from "../../../model"
|
|
4
|
+
import { EditableContent } from "../EditableContent"
|
|
5
|
+
import { ArrowContent } from "./ArrowContent"
|
|
6
|
+
import { EllipseContent } from "./EllipseContent"
|
|
7
|
+
import { PolygonContent } from "./PolygonContent"
|
|
8
|
+
import { RectangleShape } from "./RectangleContent"
|
|
9
|
+
import { StarContent } from "./StarContent"
|
|
10
|
+
|
|
11
|
+
export function ShapeContent(props: { node: ShapeNode; isStatic?: boolean }) {
|
|
12
|
+
const { node, isStatic } = props
|
|
13
|
+
const [valign, halign, shape] = useStore(node, "valign", "halign", "shape")
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div className="relative size-full">
|
|
17
|
+
{shape.name === "rectangle" && <RectangleShape node={node} shape={shape.props} />}
|
|
18
|
+
{shape.name === "polygon" && <PolygonContent node={node} shape={shape.props} />}
|
|
19
|
+
{shape.name === "star" && <StarContent node={node} shape={shape.props} />}
|
|
20
|
+
{shape.name === "ellipse" && <EllipseContent node={node} />}
|
|
21
|
+
{shape.name === "arrow" && <ArrowContent node={node} shape={shape.props} />}
|
|
22
|
+
|
|
23
|
+
<div
|
|
24
|
+
className={clsx(
|
|
25
|
+
"flex size-full",
|
|
26
|
+
valign === "top" && "items-start",
|
|
27
|
+
valign === "center" && "items-center",
|
|
28
|
+
valign === "bottom" && "items-end",
|
|
29
|
+
halign === "left" && "justify-start text-left",
|
|
30
|
+
halign === "center" && "justify-center text-center",
|
|
31
|
+
halign === "right" && "justify-end text-right",
|
|
32
|
+
halign === "justify" && "w-full text-justify",
|
|
33
|
+
)}
|
|
34
|
+
>
|
|
35
|
+
<EditableContent
|
|
36
|
+
isStatic={isStatic}
|
|
37
|
+
node={node}
|
|
38
|
+
className={clsx(halign === "justify" && "w-full")}
|
|
39
|
+
/>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
@@ -2,7 +2,7 @@ import clsx from "clsx"
|
|
|
2
2
|
import { useEffect } from "react"
|
|
3
3
|
import { useStore } from "react-bolt"
|
|
4
4
|
import { useEditor } from "../../hooks/editor"
|
|
5
|
-
import type { TextNode } from "../../model
|
|
5
|
+
import type { TextNode } from "../../model"
|
|
6
6
|
import { EditableContent } from "./EditableContent"
|
|
7
7
|
|
|
8
8
|
export function TextContent(props: {
|