@lazlon-platform/html-editor 0.4.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 (59) hide show
  1. package/lib/hooks/actions.ts +54 -26
  2. package/lib/hooks/batch.ts +17 -7
  3. package/lib/hooks/index.ts +1 -0
  4. package/lib/hooks/node.ts +14 -6
  5. package/lib/hooks/pointer/movePoint.ts +75 -0
  6. package/lib/hooks/pointer/moveable.ts +92 -57
  7. package/lib/hooks/pointer/pointer.ts +21 -11
  8. package/lib/hooks/pointer/resize.ts +176 -210
  9. package/lib/hooks/pointer/rotation.ts +89 -68
  10. package/lib/hooks/pointer/selectionFrame.ts +8 -11
  11. package/lib/hooks/pointer/selector.ts +62 -40
  12. package/lib/hooks/pointer/snap.ts +23 -23
  13. package/lib/hooks/textMarks.ts +1 -3
  14. package/lib/lib/googleFonts.ts +1 -5
  15. package/lib/model/editor.ts +13 -9
  16. package/lib/model/geometry/math.ts +623 -0
  17. package/lib/model/geometry/svg.ts +55 -0
  18. package/lib/model/history.ts +10 -13
  19. package/lib/model/index.ts +7 -10
  20. package/lib/model/node/{editable → editableNode}/index.ts +13 -29
  21. package/lib/model/node/{formattable.ts → formattableNode/index.ts} +5 -11
  22. package/lib/model/node/{group.ts → groupNode.ts} +9 -13
  23. package/lib/model/node/{image.ts → imageNode.ts} +5 -11
  24. package/lib/model/node/lineNode.ts +59 -0
  25. package/lib/model/node/{shape/shape.ts → shapeNode/index.ts} +30 -15
  26. package/lib/model/node/shapeNode/shape.ts +96 -0
  27. package/lib/model/node/{text.ts → textNode.ts} +19 -21
  28. package/lib/model/node.ts +11 -29
  29. package/lib/model/page.ts +4 -3
  30. package/lib/model/traversal.ts +1 -1
  31. package/lib/ui/extractor.ts +3 -3
  32. package/lib/ui/index.ts +2 -4
  33. package/lib/ui/node/{EditableContent.tsx → EditableContent/index.tsx} +4 -3
  34. package/lib/ui/node/GroupContent.tsx +1 -1
  35. package/lib/ui/node/ImageContent.tsx +1 -1
  36. package/lib/ui/node/LineContent.tsx +32 -0
  37. package/lib/ui/node/NodeView.tsx +1 -13
  38. package/lib/ui/node/ShapeContent/ArrowContent.tsx +57 -0
  39. package/lib/ui/node/ShapeContent/EllipseContent.tsx +37 -0
  40. package/lib/ui/node/ShapeContent/PolygonContent.tsx +62 -0
  41. package/lib/ui/node/ShapeContent/RectangleContent.tsx +35 -0
  42. package/lib/ui/node/ShapeContent/StarContent.tsx +75 -0
  43. package/lib/ui/node/ShapeContent/index.tsx +43 -0
  44. package/lib/ui/node/TextContent.tsx +1 -1
  45. package/lib/ui/selection.ts +9 -26
  46. package/package.json +34 -34
  47. package/lib/model/geometry.ts +0 -247
  48. package/lib/model/node/shape/arrow.ts +0 -50
  49. package/lib/model/node/shape/ellipse.ts +0 -26
  50. package/lib/model/node/shape/polygon.ts +0 -108
  51. package/lib/model/node/shape/star.ts +0 -63
  52. package/lib/ui/node/ArrowContent.tsx +0 -60
  53. package/lib/ui/node/EllipseContent.tsx +0 -49
  54. package/lib/ui/node/PolygonContent.tsx +0 -81
  55. package/lib/ui/node/StarContent.tsx +0 -60
  56. /package/lib/model/node/{editable → editableNode}/letterSpacing.ts +0 -0
  57. /package/lib/model/node/{editable → editableNode}/persistentMarks.ts +0 -0
  58. /package/lib/model/node/{editable → editableNode}/tiptapExtensions.ts +0 -0
  59. /package/lib/ui/node/{useDoubleClick.ts → EditableContent/useDoubleClick.ts} +0 -0
@@ -1,247 +0,0 @@
1
- export interface Point {
2
- x: number
3
- y: number
4
- }
5
-
6
- export interface Rect extends Point {
7
- width: number
8
- height: number
9
- }
10
-
11
- function rectCorners({ x, y, width, height }: Rect): Point[] {
12
- return [
13
- { x, y },
14
- { x: x + width, y },
15
- { x, y: y + height },
16
- { x: x + width, y: y + height },
17
- ]
18
- }
19
-
20
- /**
21
- * @example
22
- * ```ts
23
- * const center = { x: 0, y: 0 }
24
- * const P = { x: 2, y: 0 }
25
- * rotate(P, center, 90) // P' = { x: 0, y: 2 }
26
- * ```
27
- *
28
- * P
29
- * ●
30
- * │
31
- * │ 90°
32
- * ●─────● P'
33
- */
34
- export function rotatePoint(p: Point, center: Point, deg: number): Point {
35
- const rad = (deg * Math.PI) / 180
36
- const cos = Math.cos(rad)
37
- const sin = Math.sin(rad)
38
- const dx = p.x - center.x
39
- const dy = p.y - center.y
40
-
41
- return {
42
- x: center.x + dx * cos - dy * sin,
43
- y: center.y + dx * sin + dy * cos,
44
- }
45
- }
46
-
47
- export function boundingBox(args: Point[] | Rect[]): Rect {
48
- const points: Point[] = args.flatMap((arg) =>
49
- "width" in arg && "height" in arg ? rectCorners(arg) : arg,
50
- )
51
-
52
- let minX = Infinity
53
- let minY = Infinity
54
- let maxX = -Infinity
55
- let maxY = -Infinity
56
-
57
- for (const p of points) {
58
- if (p.x < minX) minX = p.x
59
- if (p.y < minY) minY = p.y
60
- if (p.x > maxX) maxX = p.x
61
- if (p.y > maxY) maxY = p.y
62
- }
63
-
64
- return {
65
- x: minX,
66
- y: minY,
67
- width: maxX - minX,
68
- height: maxY - minY,
69
- }
70
- }
71
-
72
- export function rectCenter({ x, y, width, height }: Rect): Point {
73
- return {
74
- x: x + width / 2,
75
- y: y + height / 2,
76
- }
77
- }
78
-
79
- /**
80
- * axis-aligned bounding box (AABB) of the rotated rectangle
81
- */
82
- export function rotatedAABB(rect: Rect, deg: number): Rect {
83
- return boundingBox(rectCorners(rect).map((c) => rotatePoint(c, rectCenter(rect), deg)))
84
- }
85
-
86
- export function dist(a: Point, b: Point) {
87
- const dx = a.x - b.x
88
- const dy = a.y - b.y
89
- return Math.hypot(dx, dy)
90
- }
91
-
92
- export function sub(a: Point, b: Point) {
93
- return {
94
- x: a.x - b.x,
95
- y: a.y - b.y,
96
- }
97
- }
98
-
99
- export function add(a: Point, b: Point) {
100
- return {
101
- x: a.x + b.x,
102
- y: a.y + b.y,
103
- }
104
- }
105
-
106
- export function mul(p: Point, n: number) {
107
- return {
108
- x: p.x * n,
109
- y: p.y * n,
110
- }
111
- }
112
-
113
- export function norm(p: Point) {
114
- const d = Math.hypot(p.x, p.y)
115
- return {
116
- x: d === 0 ? 0 : p.x / d,
117
- y: d === 0 ? 0 : p.y / d,
118
- }
119
- }
120
-
121
- export function dot(a: Point, b: Point) {
122
- return a.x * b.x + a.y * b.y
123
- }
124
-
125
- export function clamp(n: number, lo: number, hi: number) {
126
- return Math.max(lo, Math.min(hi, n))
127
- }
128
-
129
- export function tidyFloat(v: number) {
130
- return Math.abs(v) < 1e-10 ? 0 : +v.toFixed(3)
131
- }
132
-
133
- export function radToDeg(rad: number): number {
134
- return (rad * 180) / Math.PI
135
- }
136
-
137
- /**
138
- * @example
139
- * ```ts
140
- * const center = { x: 0, y: 0 }
141
- * const a = { x: 0, y: 2 }
142
- * const b = { x: 2, y: 0 }
143
- * angle(center, a, b) // 90
144
- * ```
145
- * a
146
- * ●
147
- * │
148
- * │ 90°
149
- * ●─────● b
150
- */
151
- export function angle(center: Point, a: Point, b: Point): number {
152
- const angA = radToDeg(Math.atan2(a.y - center.y, a.x - center.x))
153
- const angB = radToDeg(Math.atan2(b.y - center.y, b.x - center.x))
154
- return Math.round(((((angA + angB + 180) % 360) + 360) % 360) - 180)
155
- }
156
-
157
- export function roundedPathData(points: Point[], roundness: number): string {
158
- const count = points.length
159
- const winding = Math.sign(
160
- points.reduce((area, curr, i) => {
161
- const next = points[(i + 1) % count]
162
- return area + curr.x * next.y - next.x * curr.y
163
- }, 0),
164
- )
165
-
166
- const cmds = Array.from({ length: count }, (_, i) => {
167
- const prev = points[(i - 1 + count) % count]
168
- const curr = points[i]
169
- const next = points[(i + 1) % count]
170
- const cmd = i === 0 ? "M" : "L"
171
-
172
- const v1 = norm(sub(prev, curr))
173
- const v2 = norm(sub(next, curr))
174
-
175
- const cosTheta = clamp(dot(v1, v2), -1, 1)
176
- const theta = Math.acos(cosTheta)
177
- const cornerCross =
178
- (curr.x - prev.x) * (next.y - curr.y) - (curr.y - prev.y) * (next.x - curr.x)
179
-
180
- if (!isFinite(theta) || theta < 1e-6 || roundness <= 0) {
181
- return `${cmd} ${tidyFloat(curr.x)} ${tidyFloat(curr.y)}`
182
- }
183
-
184
- const dPrev = dist(curr, prev)
185
- const dNext = dist(curr, next)
186
- const tIdeal = roundness / Math.tan(theta / 2)
187
- const t = Math.min(tIdeal, dPrev / 2, dNext / 2)
188
- const rEff = t * Math.tan(theta / 2)
189
- const p1 = add(curr, mul(v1, t))
190
- const p2 = add(curr, mul(v2, t))
191
- const isConcave = winding !== 0 && Math.sign(cornerCross) !== winding
192
- const sweep = isConcave ? 0 : 1
193
- const arc = `A ${tidyFloat(rEff)} ${tidyFloat(rEff)} 0 0 ${sweep} ${tidyFloat(p2.x)} ${tidyFloat(p2.y)}`
194
-
195
- return `${cmd} ${tidyFloat(p1.x)} ${tidyFloat(p1.y)} ${arc}`
196
- })
197
-
198
- return cmds.concat("Z").join(" ")
199
- }
200
-
201
- export function regularPolygonPoints(props: {
202
- width: number
203
- height: number
204
- sides: number
205
- }): Point[] {
206
- const { width, height, sides } = props
207
- const rotation = sides % 2 === 0 ? Math.PI / sides : 0
208
- const cx = width / 2
209
- const cy = height / 2
210
- const rx = width / 2
211
- const ry = height / 2
212
-
213
- return Array.from({ length: sides }, (_, i) => {
214
- const angle = (i * Math.PI * 2) / sides - Math.PI / 2 + rotation
215
- return {
216
- x: cx + rx * Math.cos(angle),
217
- y: cy + ry * Math.sin(angle),
218
- }
219
- })
220
- }
221
-
222
- export function starPoints(props: {
223
- width: number
224
- height: number
225
- corners: number
226
- depth: number
227
- }): Point[] {
228
- const { width, height, corners, depth } = props
229
- const cx = width / 2
230
- const cy = height / 2
231
- const outerRx = width / 2
232
- const outerRy = height / 2
233
- const innerRx = outerRx * depth
234
- const innerRy = outerRy * depth
235
-
236
- return Array.from({ length: corners * 2 }, (_, i) => {
237
- const angle = (i * Math.PI) / corners - Math.PI / 2
238
- const isOuter = i % 2 === 0
239
- const rx = isOuter ? outerRx : innerRx
240
- const ry = isOuter ? outerRy : innerRy
241
-
242
- return {
243
- x: cx + rx * Math.cos(angle),
244
- y: cy + ry * Math.sin(angle),
245
- }
246
- })
247
- }
@@ -1,50 +0,0 @@
1
- import { computed, state } from "react-bolt"
2
- import type { Editor } from "../../editor"
3
- import type { SerializedNode } from "../../node"
4
- import type { Page } from "../../page"
5
- import { ShapeNode, type ShapeNodeProps } from "./shape"
6
- import { roundedPathData } from "../../geometry"
7
-
8
- export type ArrowNodeProps = ShapeNodeProps & Partial<Pick<ArrowNode, "roundness">>
9
-
10
- export class ArrowNode extends ShapeNode {
11
- get name() {
12
- return "arrow"
13
- }
14
-
15
- @state accessor roundness: number
16
-
17
- constructor(editor: Editor, page: Page, { roundness = 0, ...props }: ArrowNodeProps) {
18
- super(editor, page, props)
19
- this.roundness = roundness
20
- }
21
-
22
- props(): ArrowNodeProps {
23
- return {
24
- ...super.props(),
25
- roundness: this.roundness,
26
- }
27
- }
28
-
29
- serialize(): SerializedNode<this["name"], ArrowNodeProps> {
30
- return super.serialize()
31
- }
32
-
33
- @computed get svgPathData() {
34
- const { width, height } = this
35
- const bodyRight = width * 0.68
36
- const inset = height * 0.28
37
-
38
- const arrowPoints = [
39
- { x: 0, y: inset },
40
- { x: bodyRight, y: inset },
41
- { x: bodyRight, y: 0 },
42
- { x: width, y: height / 2 },
43
- { x: bodyRight, y: height },
44
- { x: bodyRight, y: height - inset },
45
- { x: 0, y: height - inset },
46
- ]
47
-
48
- return roundedPathData(arrowPoints, this.roundness)
49
- }
50
- }
@@ -1,26 +0,0 @@
1
- import type { Editor } from "../../editor"
2
- import type { SerializedNode } from "../../node"
3
- import type { Page } from "../../page"
4
- import { ShapeNode, type ShapeNodeProps } from "./shape"
5
-
6
- export type EllipseNodeProps = ShapeNodeProps
7
-
8
- export class EllipseNode extends ShapeNode {
9
- get name() {
10
- return "ellipse"
11
- }
12
-
13
- constructor(editor: Editor, page: Page, props: EllipseNodeProps) {
14
- super(editor, page, props)
15
- }
16
-
17
- props(): EllipseNodeProps {
18
- return {
19
- ...super.props(),
20
- }
21
- }
22
-
23
- serialize(): SerializedNode<this["name"], EllipseNodeProps> {
24
- return super.serialize()
25
- }
26
- }
@@ -1,108 +0,0 @@
1
- import { computed, state } from "react-bolt"
2
- import type { Editor } from "../../editor"
3
- import type { SerializedNode } from "../../node"
4
- import type { Page } from "../../page"
5
- import { ShapeNode, type ShapeNodeProps } from "./shape"
6
- import { regularPolygonPoints, roundedPathData } from "../../geometry"
7
-
8
- export type PolygonNodeProps = ShapeNodeProps &
9
- Partial<
10
- Pick<
11
- PolygonNode,
12
- | "roundness"
13
- | "sides"
14
- | "cornerTopLeft"
15
- | "cornerTopRight"
16
- | "cornerBottomLeft"
17
- | "cornerBottomRight"
18
- >
19
- >
20
-
21
- export class PolygonNode extends ShapeNode {
22
- get name() {
23
- return "polygon"
24
- }
25
-
26
- @state private accessor _roundness: number
27
- @state accessor sides: number
28
-
29
- // rectangles are special cased where they support by corner rounding
30
- @state accessor cornerTopLeft: number
31
- @state accessor cornerTopRight: number
32
- @state accessor cornerBottomLeft: number
33
- @state accessor cornerBottomRight: number
34
-
35
- set roundness(r: number) {
36
- if (this.sides === 4) {
37
- this.cornerTopLeft = r
38
- this.cornerTopRight = r
39
- this.cornerBottomLeft = r
40
- this.cornerBottomRight = r
41
- }
42
-
43
- this._roundness = r
44
- }
45
-
46
- @computed get roundness() {
47
- if (this.sides === 4) {
48
- const [first, ...radii] = [
49
- this.cornerTopLeft,
50
- this.cornerTopRight,
51
- this.cornerBottomLeft,
52
- this.cornerBottomRight,
53
- ]
54
-
55
- return radii.reduce((acc, it) => (it === acc ? acc : 0), first)
56
- }
57
-
58
- return this._roundness
59
- }
60
-
61
- constructor(
62
- editor: Editor,
63
- page: Page,
64
- {
65
- sides,
66
- cornerTopLeft,
67
- cornerTopRight,
68
- cornerBottomLeft,
69
- cornerBottomRight,
70
- roundness = 0,
71
- ...props
72
- }: PolygonNodeProps,
73
- ) {
74
- super(editor, page, props)
75
- this._roundness = roundness
76
- this.sides = sides ?? 4
77
- this.cornerTopLeft = cornerTopLeft ?? roundness
78
- this.cornerTopRight = cornerTopRight ?? roundness
79
- this.cornerBottomLeft = cornerBottomLeft ?? roundness
80
- this.cornerBottomRight = cornerBottomRight ?? roundness
81
- }
82
-
83
- props(): PolygonNodeProps {
84
- return {
85
- ...super.props(),
86
- sides: this.sides,
87
- cornerTopLeft: this.cornerTopLeft,
88
- cornerTopRight: this.cornerTopRight,
89
- cornerBottomLeft: this.cornerBottomLeft,
90
- cornerBottomRight: this.cornerBottomRight,
91
- }
92
- }
93
-
94
- serialize(): SerializedNode<this["name"], PolygonNodeProps> {
95
- return super.serialize()
96
- }
97
-
98
- @computed get svgPathData() {
99
- return roundedPathData(
100
- regularPolygonPoints({
101
- width: this.width,
102
- height: this.height,
103
- sides: this.sides,
104
- }),
105
- this._roundness,
106
- )
107
- }
108
- }
@@ -1,63 +0,0 @@
1
- import { computed, state } from "react-bolt"
2
- import { clamp, roundedPathData, starPoints } from "../../geometry"
3
- import type { Editor } from "../../editor"
4
- import type { SerializedNode } from "../../node"
5
- import type { Page } from "../../page"
6
- import { ShapeNode, type ShapeNodeProps } from "./shape"
7
-
8
- export type StarNodeProps = ShapeNodeProps &
9
- Partial<Pick<StarNode, "corners" | "roundness" | "depth">>
10
-
11
- export class StarNode extends ShapeNode {
12
- get name() {
13
- return "star"
14
- }
15
-
16
- @state accessor corners: number
17
- @state accessor roundness: number
18
- @state private accessor _depth: number
19
-
20
- @computed get depth() {
21
- return this._depth
22
- }
23
-
24
- set depth(value: number) {
25
- this._depth = clamp(value, 0, 1)
26
- }
27
-
28
- constructor(
29
- editor: Editor,
30
- page: Page,
31
- { corners = 5, roundness = 0, depth = 0.4, ...props }: StarNodeProps,
32
- ) {
33
- super(editor, page, props)
34
- this.corners = corners
35
- this.roundness = roundness
36
- this._depth = clamp(depth, 0, 1)
37
- }
38
-
39
- props(): StarNodeProps {
40
- return {
41
- ...super.props(),
42
- corners: this.corners,
43
- roundness: this.roundness,
44
- depth: this.depth,
45
- }
46
- }
47
-
48
- serialize(): SerializedNode<this["name"], StarNodeProps> {
49
- return super.serialize()
50
- }
51
-
52
- @computed get svgPathData() {
53
- return roundedPathData(
54
- starPoints({
55
- width: this.width,
56
- height: this.height,
57
- corners: this.corners,
58
- depth: this.depth,
59
- }),
60
- this.roundness,
61
- )
62
- }
63
- }
@@ -1,60 +0,0 @@
1
- import clsx from "clsx"
2
- import { useId } from "react"
3
- import { useStore } from "react-bolt"
4
- import { type ArrowNode } from "../../model/node/shape/arrow"
5
- import { EditableContent } from "./EditableContent"
6
-
7
- export function ArrowContent(props: { node: ArrowNode; isStatic?: boolean }) {
8
- const maskId = useId()
9
- const { node, isStatic } = props
10
-
11
- const [valign, halign, background, borderWidth, borderColor] = useStore(
12
- node,
13
- "valign",
14
- "halign",
15
- "background",
16
- "borderWidth",
17
- "borderColor",
18
- )
19
-
20
- const [w, h, d] = useStore(node, "width", "height", "svgPathData")
21
-
22
- return (
23
- <div className="relative size-full">
24
- <svg width={w} height={h} className="absolute inset-0">
25
- <defs>
26
- <mask id={maskId} maskUnits="userSpaceOnUse">
27
- <path d={d} fill="white" />
28
- </mask>
29
- </defs>
30
-
31
- <path d={d} fill={background} />
32
- <path
33
- d={d}
34
- fill="none"
35
- stroke={borderColor}
36
- strokeWidth={borderWidth}
37
- mask={`url(#${maskId})`}
38
- />
39
- </svg>
40
- <div
41
- className={clsx(
42
- "flex size-full",
43
- valign === "top" && "items-start",
44
- valign === "center" && "items-center",
45
- valign === "bottom" && "items-end",
46
- halign === "left" && "justify-start text-left",
47
- halign === "center" && "justify-center text-center",
48
- halign === "right" && "justify-end text-right",
49
- halign === "justify" && "w-full text-justify",
50
- )}
51
- >
52
- <EditableContent
53
- isStatic={isStatic}
54
- node={node}
55
- className={clsx(halign === "justify" && "w-full")}
56
- />
57
- </div>
58
- </div>
59
- )
60
- }
@@ -1,49 +0,0 @@
1
- import clsx from "clsx"
2
- import { useStore } from "react-bolt"
3
- import { type EllipseNode } from "../../model/node/shape/ellipse"
4
- import { EditableContent } from "./EditableContent"
5
-
6
- export function EllipseContent(props: { node: EllipseNode; isStatic?: boolean }) {
7
- const { node, isStatic } = props
8
-
9
- const [valign, halign, background, borderWidth, borderColor] = useStore(
10
- node,
11
- "valign",
12
- "halign",
13
- "background",
14
- "borderWidth",
15
- "borderColor",
16
- )
17
-
18
- return (
19
- <div className="relative size-full">
20
- <div
21
- className="absolute inset-0 size-full"
22
- onDragStart={(e) => e.preventDefault()}
23
- style={{
24
- borderRadius: "50%",
25
- background,
26
- border: `${borderWidth}px solid ${borderColor}`,
27
- }}
28
- />
29
- <div
30
- className={clsx(
31
- "flex size-full",
32
- valign === "top" && "items-start",
33
- valign === "center" && "items-center",
34
- valign === "bottom" && "items-end",
35
- halign === "left" && "justify-start text-left",
36
- halign === "center" && "justify-center text-center",
37
- halign === "right" && "justify-end text-right",
38
- halign === "justify" && "w-full text-justify",
39
- )}
40
- >
41
- <EditableContent
42
- isStatic={isStatic}
43
- node={node}
44
- className={clsx(halign === "justify" && "w-full")}
45
- />
46
- </div>
47
- </div>
48
- )
49
- }
@@ -1,81 +0,0 @@
1
- import clsx from "clsx"
2
- import { useId } from "react"
3
- import { useStore } from "react-bolt"
4
- import { type PolygonNode } from "../../model/node/shape/polygon"
5
- import { EditableContent } from "./EditableContent"
6
-
7
- export function PolygonContent(props: { node: PolygonNode; isStatic?: boolean }) {
8
- const maskId = useId()
9
- const { node, isStatic } = props
10
-
11
- const [valign, halign, background, borderWidth, borderColor, sides] = useStore(
12
- node,
13
- "valign",
14
- "halign",
15
- "background",
16
- "borderWidth",
17
- "borderColor",
18
- "sides",
19
- )
20
-
21
- const [w, h, d] = useStore(node, "width", "height", "svgPathData")
22
-
23
- const radii = useStore(
24
- node,
25
- "cornerTopLeft",
26
- "cornerTopRight",
27
- "cornerBottomRight",
28
- "cornerBottomLeft",
29
- )
30
-
31
- return (
32
- <div className="relative size-full">
33
- {sides !== 4 ? (
34
- <svg width={w} height={h} className="absolute inset-0">
35
- <defs>
36
- <mask id={maskId} maskUnits="userSpaceOnUse">
37
- <path d={d} fill="white" />
38
- </mask>
39
- </defs>
40
-
41
- <path d={d} fill={background} />
42
- <path
43
- d={d}
44
- fill="none"
45
- stroke={borderColor}
46
- strokeWidth={borderWidth}
47
- mask={`url(#${maskId})`}
48
- />
49
- </svg>
50
- ) : (
51
- <div
52
- className="absolute inset-0 size-full"
53
- onDragStart={(e) => e.preventDefault()}
54
- style={{
55
- borderRadius: radii.map((r) => `${r}px`).join(" "),
56
- background: background,
57
- border: `${borderWidth}px solid ${borderColor}`,
58
- }}
59
- />
60
- )}
61
- <div
62
- className={clsx(
63
- "flex size-full",
64
- valign === "top" && "items-start",
65
- valign === "center" && "items-center",
66
- valign === "bottom" && "items-end",
67
- halign === "left" && "justify-start text-left",
68
- halign === "center" && "justify-center text-center",
69
- halign === "right" && "justify-end text-right",
70
- halign === "justify" && "w-full text-justify",
71
- )}
72
- >
73
- <EditableContent
74
- isStatic={isStatic}
75
- node={node}
76
- className={clsx(halign === "justify" && "w-full")}
77
- />
78
- </div>
79
- </div>
80
- )
81
- }