@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.
Files changed (53) hide show
  1. package/lib/hooks/actions.ts +51 -24
  2. package/lib/hooks/batch.ts +17 -7
  3. package/lib/hooks/index.ts +1 -0
  4. package/lib/hooks/pointer/movePoint.ts +75 -0
  5. package/lib/hooks/pointer/moveable.ts +22 -8
  6. package/lib/hooks/pointer/pointer.ts +2 -3
  7. package/lib/hooks/pointer/resize.ts +2 -2
  8. package/lib/hooks/pointer/rotation.ts +74 -39
  9. package/lib/hooks/pointer/selector.ts +16 -2
  10. package/lib/hooks/pointer/snap.ts +3 -3
  11. package/lib/hooks/textMarks.ts +1 -3
  12. package/lib/lib/googleFonts.ts +1 -5
  13. package/lib/model/editor.ts +13 -9
  14. package/lib/model/geometry/math.ts +139 -0
  15. package/lib/model/history.ts +10 -13
  16. package/lib/model/index.ts +7 -10
  17. package/lib/model/node/{editable → editableNode}/index.ts +13 -29
  18. package/lib/model/node/{formattable.ts → formattableNode/index.ts} +5 -11
  19. package/lib/model/node/{group.ts → groupNode.ts} +9 -13
  20. package/lib/model/node/{image.ts → imageNode.ts} +5 -11
  21. package/lib/model/node/lineNode.ts +59 -0
  22. package/lib/model/node/{shape/shape.ts → shapeNode/index.ts} +30 -15
  23. package/lib/model/node/shapeNode/shape.ts +96 -0
  24. package/lib/model/node/{text.ts → textNode.ts} +6 -12
  25. package/lib/model/node.ts +9 -23
  26. package/lib/model/page.ts +1 -2
  27. package/lib/model/traversal.ts +1 -1
  28. package/lib/ui/extractor.ts +3 -3
  29. package/lib/ui/index.ts +2 -4
  30. package/lib/ui/node/{EditableContent.tsx → EditableContent/index.tsx} +4 -3
  31. package/lib/ui/node/GroupContent.tsx +1 -1
  32. package/lib/ui/node/ImageContent.tsx +1 -1
  33. package/lib/ui/node/LineContent.tsx +32 -0
  34. package/lib/ui/node/ShapeContent/ArrowContent.tsx +57 -0
  35. package/lib/ui/node/ShapeContent/EllipseContent.tsx +37 -0
  36. package/lib/ui/node/ShapeContent/PolygonContent.tsx +62 -0
  37. package/lib/ui/node/ShapeContent/RectangleContent.tsx +35 -0
  38. package/lib/ui/node/ShapeContent/StarContent.tsx +75 -0
  39. package/lib/ui/node/ShapeContent/index.tsx +43 -0
  40. package/lib/ui/node/TextContent.tsx +1 -1
  41. package/package.json +1 -1
  42. package/lib/model/node/shape/arrow.ts +0 -50
  43. package/lib/model/node/shape/ellipse.ts +0 -26
  44. package/lib/model/node/shape/polygon.ts +0 -130
  45. package/lib/model/node/shape/star.ts +0 -91
  46. package/lib/ui/node/ArrowContent.tsx +0 -60
  47. package/lib/ui/node/EllipseContent.tsx +0 -49
  48. package/lib/ui/node/PolygonContent.tsx +0 -81
  49. package/lib/ui/node/StarContent.tsx +0 -60
  50. /package/lib/model/node/{editable → editableNode}/letterSpacing.ts +0 -0
  51. /package/lib/model/node/{editable → editableNode}/persistentMarks.ts +0 -0
  52. /package/lib/model/node/{editable → editableNode}/tiptapExtensions.ts +0 -0
  53. /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 { SerializedNode } from "../node"
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(editor: Editor, page: Page, { halign, size, ...props }: TextNodeProps) {
31
- super(editor, page, props)
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(), halign, size }
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
- Name extends string = string,
10
- Props extends NodeProps = NodeProps,
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(_: Editor, page: Page, props: NodeProps) {
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
- blockMove(_: React.PointerEvent): boolean {
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) => node.serialize())
48
+ .map((node) => this.editor.serializeNode(node))
50
49
  .toArray(),
51
50
  }
52
51
  }
@@ -1,5 +1,5 @@
1
1
  import { Node } from "./node"
2
- import { GroupNode } from "./node/group"
2
+ import { GroupNode } from "./node/groupNode"
3
3
  import { Page } from "./page"
4
4
 
5
5
  export function flattenNodes(root: Node | Page): Array<Node> {
@@ -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/editable"
5
- import { FormattableNode } from "../model/node/formattable"
6
- import { ShapeNode } from "../model/node/shape/shape"
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 { PolygonContent } from "./node/PolygonContent"
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 "../../hooks/editor"
7
- import type { EditableNode, EditableNodeProps } from "../../model/node/editable"
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<EditableNodeProps>({
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/node/group"
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/node/image"
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/node/text"
5
+ import type { TextNode } from "../../model"
6
6
  import { EditableContent } from "./EditableContent"
7
7
 
8
8
  export function TextContent(props: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazlon-platform/html-editor",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "lib"