@zuilib/text-editor 0.0.2 → 0.1.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/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/MarkdownEditor.tsx
2
- import { useCallback, useEffect as useEffect5, useMemo, useRef as useRef2, useState } from "react";
2
+ import { useCallback as useCallback3, useEffect as useEffect7, useMemo, useRef as useRef3, useState as useState3 } from "react";
3
3
  import { LexicalComposer } from "@lexical/react/LexicalComposer";
4
4
  import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
5
5
  import { ContentEditable } from "@lexical/react/LexicalContentEditable";
@@ -10,12 +10,14 @@ import { CheckListPlugin } from "@lexical/react/LexicalCheckListPlugin";
10
10
  import { LinkPlugin } from "@lexical/react/LexicalLinkPlugin";
11
11
  import { MarkdownShortcutPlugin } from "@lexical/react/LexicalMarkdownShortcutPlugin";
12
12
  import { AutoFocusPlugin } from "@lexical/react/LexicalAutoFocusPlugin";
13
- import { CHECK_LIST, CODE, TRANSFORMERS } from "@lexical/markdown";
14
- import { useLexicalComposerContext as useLexicalComposerContext5 } from "@lexical/react/LexicalComposerContext";
13
+ import { TablePlugin } from "@lexical/react/LexicalTablePlugin";
14
+ import { CHECK_LIST, CODE, TRANSFORMERS as TRANSFORMERS2 } from "@lexical/markdown";
15
+ import { useLexicalComposerContext as useLexicalComposerContext7 } from "@lexical/react/LexicalComposerContext";
15
16
  import { HeadingNode, QuoteNode } from "@lexical/rich-text";
16
17
  import { ListItemNode, ListNode as ListNode2 } from "@lexical/list";
17
18
  import { CodeHighlightNode, CodeNode } from "@lexical/code";
18
19
  import { AutoLinkNode, LinkNode } from "@lexical/link";
20
+ import { TableCellNode as TableCellNode2, TableNode as TableNode2, TableRowNode as TableRowNode2 } from "@lexical/table";
19
21
 
20
22
  // src/plugins/ChecklistShortcutPlugin.tsx
21
23
  import { useEffect } from "react";
@@ -79,6 +81,7 @@ import { useLexicalComposerContext as useLexicalComposerContext2 } from "@lexica
79
81
  import {
80
82
  $createParagraphNode,
81
83
  $getSelection,
84
+ $isLineBreakNode,
82
85
  $isParagraphNode,
83
86
  $isRangeSelection,
84
87
  $isRootOrShadowRoot,
@@ -122,10 +125,20 @@ function CodeBlockShortcutPlugin() {
122
125
  if (!$isCodeNode(topNode)) return false;
123
126
  const root = topNode.getParent();
124
127
  if (!root || root.getLastChild()?.getKey() !== topNode.getKey()) return false;
125
- const codeText = topNode.getTextContent();
126
- const lastNewline = codeText.lastIndexOf("\n");
127
- const isOnLastLine = anchor.offset >= lastNewline + 1;
128
- if (!isOnLastLine) return false;
128
+ const hasNewline = (node) => $isLineBreakNode(node) || node.getTextContent().includes("\n");
129
+ let hasContentBelow;
130
+ if (anchorNode.getKey() === topNode.getKey()) {
131
+ hasContentBelow = topNode.getChildren().slice(anchor.offset).some(hasNewline);
132
+ } else if (anchorNode.getTextContent().slice(anchor.offset).includes("\n")) {
133
+ hasContentBelow = true;
134
+ } else {
135
+ let child = anchorNode;
136
+ while (child.getParent() && child.getParent().getKey() !== topNode.getKey()) {
137
+ child = child.getParent();
138
+ }
139
+ hasContentBelow = child.getNextSiblings().some(hasNewline);
140
+ }
141
+ if (hasContentBelow) return false;
129
142
  const paragraph = $createParagraphNode();
130
143
  topNode.insertAfter(paragraph);
131
144
  paragraph.selectStart();
@@ -192,9 +205,1830 @@ function MarkdownSyncPlugin({
192
205
  return null;
193
206
  }
194
207
 
195
- // src/nodes/FrontmatterNode.ts
208
+ // src/plugins/ToolbarPlugin.tsx
209
+ import { useCallback as useCallback2, useEffect as useEffect6, useState as useState2 } from "react";
210
+ import {
211
+ $getSelection as $getSelection2,
212
+ $isRangeSelection as $isRangeSelection2,
213
+ FORMAT_TEXT_COMMAND
214
+ } from "lexical";
215
+ import { useLexicalComposerContext as useLexicalComposerContext6 } from "@lexical/react/LexicalComposerContext";
216
+ import { INSERT_TABLE_COMMAND } from "@lexical/table";
217
+ import { $insertNodeToNearestRoot } from "@lexical/utils";
218
+
219
+ // src/nodes/DrawingNode.tsx
196
220
  import {
197
221
  $applyNodeReplacement,
222
+ DecoratorNode
223
+ } from "lexical";
224
+
225
+ // src/components/DrawingCanvas.tsx
226
+ import {
227
+ useCallback,
228
+ useEffect as useEffect5,
229
+ useRef as useRef2,
230
+ useState
231
+ } from "react";
232
+ import { $getNodeByKey as $getNodeByKey2 } from "lexical";
233
+ import { useLexicalComposerContext as useLexicalComposerContext5 } from "@lexical/react/LexicalComposerContext";
234
+ import { useLexicalEditable } from "@lexical/react/useLexicalEditable";
235
+
236
+ // src/components/drawingTypes.ts
237
+ var BOX_TYPES = ["rect", "ellipse", "triangle", "pentagon"];
238
+ function isBoxType(type) {
239
+ return BOX_TYPES.includes(type);
240
+ }
241
+ function isConnectorType(type) {
242
+ return type === "arrow" || type === "line";
243
+ }
244
+ var DEFAULT_DRAWING_HEIGHT = 320;
245
+ var FONT_SIZE = 15;
246
+ var SMALL_FONT_SIZE = 11;
247
+ var LINE_HEIGHT = 1.35;
248
+ var EMPTY_DRAWING = {
249
+ version: 1,
250
+ height: DEFAULT_DRAWING_HEIGHT,
251
+ shapes: []
252
+ };
253
+ var STROKE_COLORS = [
254
+ "#1e1e1e",
255
+ "#e03131",
256
+ "#2f9e44",
257
+ "#1971c2",
258
+ "#f08c00"
259
+ ];
260
+ var FILL_COLORS = [
261
+ "transparent",
262
+ "#ffc9c9",
263
+ "#b2f2bb",
264
+ "#a5d8ff",
265
+ "#ffec99"
266
+ ];
267
+ var shapeIdCounter = 0;
268
+ function createShapeId() {
269
+ shapeIdCounter += 1;
270
+ return `s${Date.now().toString(36)}${shapeIdCounter.toString(36)}`;
271
+ }
272
+ function serializeDrawingData(data) {
273
+ return JSON.stringify(data);
274
+ }
275
+ function parseDrawingData(json) {
276
+ try {
277
+ const parsed = JSON.parse(json);
278
+ if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.shapes)) {
279
+ return EMPTY_DRAWING;
280
+ }
281
+ const raw = parsed;
282
+ const shapes = raw.shapes.filter(isValidShape);
283
+ return {
284
+ version: 1,
285
+ height: typeof raw.height === "number" && raw.height >= 80 ? raw.height : DEFAULT_DRAWING_HEIGHT,
286
+ shapes
287
+ };
288
+ } catch {
289
+ return EMPTY_DRAWING;
290
+ }
291
+ }
292
+ function isValidShape(value) {
293
+ if (typeof value !== "object" || value === null) return false;
294
+ const shape = value;
295
+ return typeof shape.id === "string" && ["rect", "ellipse", "triangle", "pentagon", "arrow", "line", "text"].includes(
296
+ shape.type
297
+ ) && typeof shape.x === "number" && typeof shape.y === "number" && typeof shape.w === "number" && typeof shape.h === "number" && typeof shape.stroke === "string" && typeof shape.fill === "string" && typeof shape.strokeWidth === "number" && (shape.text === void 0 || typeof shape.text === "string") && (shape.label === void 0 || typeof shape.label === "string") && (shape.footer === void 0 || typeof shape.footer === "string") && (shape.startBinding === void 0 || typeof shape.startBinding === "string") && (shape.endBinding === void 0 || typeof shape.endBinding === "string") && (shape.bidirectional === void 0 || typeof shape.bidirectional === "boolean") && (shape.routing === void 0 || shape.routing === "elbow") && (shape.elbow === void 0 || typeof shape.elbow === "number") && (shape.waypoints === void 0 || Array.isArray(shape.waypoints) && shape.waypoints.every(
298
+ (p) => typeof p === "object" && p !== null && typeof p.x === "number" && typeof p.y === "number"
299
+ ));
300
+ }
301
+
302
+ // src/components/drawingGeometry.ts
303
+ var BINDING_GAP = 6;
304
+ var BINDING_TOLERANCE = 8;
305
+ function bbox(shape) {
306
+ return {
307
+ x: Math.min(shape.x, shape.x + shape.w),
308
+ y: Math.min(shape.y, shape.y + shape.h),
309
+ w: Math.abs(shape.w),
310
+ h: Math.abs(shape.h)
311
+ };
312
+ }
313
+ function normalize(shape) {
314
+ if (isConnectorType(shape.type)) return shape;
315
+ const b = bbox(shape);
316
+ return { ...shape, x: b.x, y: b.y, w: b.w, h: b.h };
317
+ }
318
+ function polygonPoints(shape) {
319
+ const b = bbox(shape);
320
+ if (shape.type === "triangle") {
321
+ return [
322
+ { x: b.x + b.w / 2, y: b.y },
323
+ { x: b.x + b.w, y: b.y + b.h },
324
+ { x: b.x, y: b.y + b.h }
325
+ ];
326
+ }
327
+ const cx = b.x + b.w / 2;
328
+ const cy = b.y + b.h / 2;
329
+ return Array.from({ length: 5 }, (_, i) => {
330
+ const angle = -Math.PI / 2 + i * 2 * Math.PI / 5;
331
+ return {
332
+ x: cx + b.w / 2 * Math.cos(angle),
333
+ y: cy + b.h / 2 * Math.sin(angle)
334
+ };
335
+ });
336
+ }
337
+ function center(shape) {
338
+ const b = bbox(shape);
339
+ return { x: b.x + b.w / 2, y: b.y + b.h / 2 };
340
+ }
341
+ function wrapText(text, maxWidth, fontSize) {
342
+ const charW = fontSize * 0.6;
343
+ const maxChars = Math.max(1, Math.floor(maxWidth / charW));
344
+ const lines = [];
345
+ for (const para of text.split("\n")) {
346
+ if (para.length <= maxChars) {
347
+ lines.push(para);
348
+ continue;
349
+ }
350
+ let current = "";
351
+ for (let word of para.split(" ")) {
352
+ while (word.length > maxChars) {
353
+ if (current) {
354
+ lines.push(current);
355
+ current = "";
356
+ }
357
+ lines.push(word.slice(0, maxChars));
358
+ word = word.slice(maxChars);
359
+ }
360
+ if (!current) {
361
+ current = word;
362
+ } else if (current.length + 1 + word.length <= maxChars) {
363
+ current += ` ${word}`;
364
+ } else {
365
+ lines.push(current);
366
+ current = word;
367
+ }
368
+ }
369
+ lines.push(current);
370
+ }
371
+ return lines;
372
+ }
373
+ function textBoxSize(text, fontSize = FONT_SIZE) {
374
+ const lines = text.split("\n");
375
+ const longest = lines.reduce((max, line) => Math.max(max, line.length), 0);
376
+ return {
377
+ w: Math.max(20, longest * fontSize * 0.6),
378
+ h: lines.length * fontSize * LINE_HEIGHT
379
+ };
380
+ }
381
+ function arrowHead(x, y, angle, length) {
382
+ const spread = Math.PI / 7;
383
+ const hx1 = x - length * Math.cos(angle - spread);
384
+ const hy1 = y - length * Math.sin(angle - spread);
385
+ const hx2 = x - length * Math.cos(angle + spread);
386
+ const hy2 = y - length * Math.sin(angle + spread);
387
+ return `M ${hx1} ${hy1} L ${x} ${y} L ${hx2} ${hy2}`;
388
+ }
389
+ function connectorPoints(shape) {
390
+ const p1 = { x: shape.x, y: shape.y };
391
+ const p2 = { x: shape.x + shape.w, y: shape.y + shape.h };
392
+ if (shape.waypoints?.length) return [p1, ...shape.waypoints, p2];
393
+ if (shape.routing !== "elbow") return [p1, p2];
394
+ const t = Math.min(1, Math.max(0, shape.elbow ?? 0.5));
395
+ if (Math.abs(shape.w) >= Math.abs(shape.h)) {
396
+ const midX = shape.x + shape.w * t;
397
+ return [p1, { x: midX, y: p1.y }, { x: midX, y: p2.y }, p2];
398
+ }
399
+ const midY = shape.y + shape.h * t;
400
+ return [p1, { x: p1.x, y: midY }, { x: p2.x, y: midY }, p2];
401
+ }
402
+ function connectorPath(shape) {
403
+ const points = connectorPoints(shape);
404
+ return points.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x} ${p.y}`).join(" ");
405
+ }
406
+ function connectorMidpoint(shape) {
407
+ const points = connectorPoints(shape);
408
+ const a = points[Math.floor((points.length - 1) / 2)];
409
+ const b = points[Math.ceil((points.length + 1) / 2) - 1];
410
+ return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
411
+ }
412
+ function segmentAngleAt(points, index, backward) {
413
+ const step = backward ? 1 : -1;
414
+ for (let i = index + step; i >= 0 && i < points.length; i += step) {
415
+ const dx = points[index].x - points[i].x;
416
+ const dy = points[index].y - points[i].y;
417
+ if (Math.abs(dx) > 0.01 || Math.abs(dy) > 0.01) {
418
+ return Math.atan2(dy, dx);
419
+ }
420
+ }
421
+ return 0;
422
+ }
423
+ function arrowPath(shape) {
424
+ const points = connectorPoints(shape);
425
+ const line = connectorPath(shape);
426
+ const length = Math.hypot(shape.w, shape.h);
427
+ if (length < 1) return line;
428
+ const headLength = Math.min(14, 4 + length / 4);
429
+ const last = points.length - 1;
430
+ const parts = [
431
+ line,
432
+ arrowHead(
433
+ points[last].x,
434
+ points[last].y,
435
+ segmentAngleAt(points, last, false),
436
+ headLength
437
+ )
438
+ ];
439
+ if (shape.bidirectional) {
440
+ parts.push(
441
+ arrowHead(
442
+ points[0].x,
443
+ points[0].y,
444
+ segmentAngleAt(points, 0, true),
445
+ headLength
446
+ )
447
+ );
448
+ }
449
+ return parts.join(" ");
450
+ }
451
+ function boxContainsPoint(box, point) {
452
+ const b = bbox(box);
453
+ const pad = BINDING_TOLERANCE;
454
+ if (box.type === "ellipse") {
455
+ const rx = b.w / 2 + pad;
456
+ const ry = b.h / 2 + pad;
457
+ if (rx <= 0 || ry <= 0) return false;
458
+ const dx = (point.x - (b.x + b.w / 2)) / rx;
459
+ const dy = (point.y - (b.y + b.h / 2)) / ry;
460
+ return dx * dx + dy * dy <= 1;
461
+ }
462
+ return point.x >= b.x - pad && point.x <= b.x + b.w + pad && point.y >= b.y - pad && point.y <= b.y + b.h + pad;
463
+ }
464
+ function findBoxAt(shapes, point, excludeId) {
465
+ for (let i = shapes.length - 1; i >= 0; i--) {
466
+ const shape = shapes[i];
467
+ if (shape.id === excludeId || !isBoxType(shape.type)) continue;
468
+ if (boxContainsPoint(shape, point)) return shape;
469
+ }
470
+ return null;
471
+ }
472
+ function borderPoint(box, toward) {
473
+ const c = center(box);
474
+ const dx = toward.x - c.x;
475
+ const dy = toward.y - c.y;
476
+ const dist = Math.hypot(dx, dy);
477
+ if (dist < 1) return c;
478
+ let t;
479
+ const b = bbox(box);
480
+ if (box.type === "ellipse") {
481
+ const rx = b.w / 2;
482
+ const ry = b.h / 2;
483
+ if (rx <= 0 || ry <= 0) return c;
484
+ t = 1 / Math.hypot(dx / rx, dy / ry);
485
+ } else if (box.type === "triangle" || box.type === "pentagon") {
486
+ t = rayPolygonT(c, { x: dx, y: dy }, polygonPoints(box)) ?? 0.5;
487
+ } else {
488
+ const sx = dx !== 0 ? b.w / 2 / Math.abs(dx) : Infinity;
489
+ const sy = dy !== 0 ? b.h / 2 / Math.abs(dy) : Infinity;
490
+ t = Math.min(sx, sy);
491
+ }
492
+ const gapT = BINDING_GAP / dist;
493
+ return { x: c.x + dx * (t + gapT), y: c.y + dy * (t + gapT) };
494
+ }
495
+ function rayPolygonT(origin, dir, points) {
496
+ let best = null;
497
+ for (let i = 0; i < points.length; i++) {
498
+ const p1 = points[i];
499
+ const p2 = points[(i + 1) % points.length];
500
+ const ex = p2.x - p1.x;
501
+ const ey = p2.y - p1.y;
502
+ const denom = dir.x * ey - dir.y * ex;
503
+ if (Math.abs(denom) < 1e-9) continue;
504
+ const qx = p1.x - origin.x;
505
+ const qy = p1.y - origin.y;
506
+ const t = (qx * ey - qy * ex) / denom;
507
+ const s = (qx * dir.y - qy * dir.x) / denom;
508
+ if (t > 0 && s >= 0 && s <= 1 && (best === null || t < best)) {
509
+ best = t;
510
+ }
511
+ }
512
+ return best;
513
+ }
514
+ function resolveBindings(shapes) {
515
+ const byId = new Map(shapes.map((s) => [s.id, s]));
516
+ return shapes.map((shape) => {
517
+ if (shape.type !== "arrow" && shape.type !== "line") return shape;
518
+ if (!shape.startBinding && !shape.endBinding) return shape;
519
+ const startBox = shape.startBinding ? byId.get(shape.startBinding) : void 0;
520
+ const endBox = shape.endBinding ? byId.get(shape.endBinding) : void 0;
521
+ if (!startBox && !endBox) {
522
+ return { ...shape, startBinding: void 0, endBinding: void 0 };
523
+ }
524
+ const freeStart = { x: shape.x, y: shape.y };
525
+ const freeEnd = { x: shape.x + shape.w, y: shape.y + shape.h };
526
+ const waypoints = shape.waypoints;
527
+ const startTarget = waypoints?.length ? waypoints[0] : endBox ? center(endBox) : freeEnd;
528
+ const endTarget = waypoints?.length ? waypoints[waypoints.length - 1] : startBox ? center(startBox) : freeStart;
529
+ const p1 = startBox ? borderPoint(startBox, startTarget) : freeStart;
530
+ const p2 = endBox ? borderPoint(endBox, endTarget) : freeEnd;
531
+ return {
532
+ ...shape,
533
+ x: p1.x,
534
+ y: p1.y,
535
+ w: p2.x - p1.x,
536
+ h: p2.y - p1.y,
537
+ startBinding: startBox ? shape.startBinding : void 0,
538
+ endBinding: endBox ? shape.endBinding : void 0
539
+ };
540
+ });
541
+ }
542
+
543
+ // src/components/DrawingCanvas.tsx
544
+ import { jsx, jsxs } from "react/jsx-runtime";
545
+ import { createElement } from "react";
546
+ var MIN_HEIGHT = 120;
547
+ var MAX_HEIGHT = 900;
548
+ var SELECTION_COLOR = "#6965db";
549
+ var CONNECTOR_FONT_SIZE = 12;
550
+ var CONNECTOR_LABEL_MAX_WIDTH = 160;
551
+ var BOX_TEXT_PADDING = 8;
552
+ var WAYPOINT_SNAP = 8;
553
+ function BoxTexts({
554
+ shape,
555
+ hideField,
556
+ showHints
557
+ }) {
558
+ const b = bbox(shape);
559
+ const cx = b.x + b.w / 2;
560
+ const wrapW = Math.max(b.w - BOX_TEXT_PADDING * 2, 20);
561
+ const mainLines = wrapText(shape.text ?? "", wrapW, FONT_SIZE);
562
+ const labelLines = wrapText(shape.label ?? "", wrapW, SMALL_FONT_SIZE);
563
+ const footerLines = wrapText(shape.footer ?? "", wrapW, SMALL_FONT_SIZE);
564
+ const mainLineH = FONT_SIZE * LINE_HEIGHT;
565
+ const smallLineH = SMALL_FONT_SIZE * LINE_HEIGHT;
566
+ const mainStart = b.y + b.h / 2 - (mainLines.length - 1) * mainLineH / 2 + FONT_SIZE * 0.35;
567
+ const common = {
568
+ fill: shape.stroke,
569
+ fontFamily: "ui-sans-serif, system-ui, sans-serif",
570
+ textAnchor: "middle",
571
+ style: { userSelect: "none" }
572
+ };
573
+ const hint = {
574
+ ...common,
575
+ opacity: 0.35,
576
+ style: { userSelect: "none", pointerEvents: "none" }
577
+ };
578
+ return /* @__PURE__ */ jsxs("g", { children: [
579
+ hideField !== "label" && (shape.label ? labelLines.map((line, i) => /* @__PURE__ */ createElement(
580
+ "text",
581
+ {
582
+ ...common,
583
+ key: i,
584
+ x: cx,
585
+ y: b.y + SMALL_FONT_SIZE + 7 + i * smallLineH,
586
+ fontSize: SMALL_FONT_SIZE,
587
+ fontWeight: 600,
588
+ opacity: 0.85
589
+ },
590
+ line
591
+ )) : showHints && /* @__PURE__ */ jsx("text", { ...hint, x: cx, y: b.y + SMALL_FONT_SIZE + 7, fontSize: SMALL_FONT_SIZE, children: "Label" })),
592
+ hideField !== "text" && (shape.text ? mainLines.map((line, i) => /* @__PURE__ */ createElement(
593
+ "text",
594
+ {
595
+ ...common,
596
+ key: i,
597
+ x: cx,
598
+ y: mainStart + i * mainLineH,
599
+ fontSize: FONT_SIZE
600
+ },
601
+ line
602
+ )) : showHints && /* @__PURE__ */ jsx("text", { ...hint, x: cx, y: mainStart, fontSize: FONT_SIZE, children: "Text" })),
603
+ hideField !== "footer" && (shape.footer ? footerLines.map((line, i) => /* @__PURE__ */ createElement(
604
+ "text",
605
+ {
606
+ ...common,
607
+ key: i,
608
+ x: cx,
609
+ y: b.y + b.h - 9 - (footerLines.length - 1 - i) * smallLineH,
610
+ fontSize: SMALL_FONT_SIZE,
611
+ opacity: 0.65
612
+ },
613
+ line
614
+ )) : showHints && /* @__PURE__ */ jsx("text", { ...hint, x: cx, y: b.y + b.h - 9, fontSize: SMALL_FONT_SIZE, children: "Footer" }))
615
+ ] });
616
+ }
617
+ function ShapeView({
618
+ shape,
619
+ hideField,
620
+ showHints
621
+ }) {
622
+ const common = {
623
+ stroke: shape.stroke,
624
+ strokeWidth: shape.strokeWidth,
625
+ strokeLinecap: "round",
626
+ strokeLinejoin: "round"
627
+ };
628
+ switch (shape.type) {
629
+ case "rect": {
630
+ const b = bbox(shape);
631
+ return /* @__PURE__ */ jsxs("g", { children: [
632
+ /* @__PURE__ */ jsx(
633
+ "rect",
634
+ {
635
+ ...common,
636
+ x: b.x,
637
+ y: b.y,
638
+ width: b.w,
639
+ height: b.h,
640
+ rx: Math.min(8, b.w / 4, b.h / 4),
641
+ fill: shape.fill
642
+ }
643
+ ),
644
+ /* @__PURE__ */ jsx(BoxTexts, { shape, hideField, showHints })
645
+ ] });
646
+ }
647
+ case "ellipse": {
648
+ const b = bbox(shape);
649
+ return /* @__PURE__ */ jsxs("g", { children: [
650
+ /* @__PURE__ */ jsx(
651
+ "ellipse",
652
+ {
653
+ ...common,
654
+ cx: b.x + b.w / 2,
655
+ cy: b.y + b.h / 2,
656
+ rx: b.w / 2,
657
+ ry: b.h / 2,
658
+ fill: shape.fill
659
+ }
660
+ ),
661
+ /* @__PURE__ */ jsx(BoxTexts, { shape, hideField, showHints })
662
+ ] });
663
+ }
664
+ case "triangle":
665
+ case "pentagon":
666
+ return /* @__PURE__ */ jsxs("g", { children: [
667
+ /* @__PURE__ */ jsx(
668
+ "polygon",
669
+ {
670
+ ...common,
671
+ points: polygonPoints(shape).map((p) => `${p.x},${p.y}`).join(" "),
672
+ fill: shape.fill
673
+ }
674
+ ),
675
+ /* @__PURE__ */ jsx(BoxTexts, { shape, hideField, showHints })
676
+ ] });
677
+ case "line":
678
+ case "arrow":
679
+ return /* @__PURE__ */ jsxs("g", { children: [
680
+ /* @__PURE__ */ jsx(
681
+ "path",
682
+ {
683
+ ...common,
684
+ d: shape.type === "line" ? connectorPath(shape) : arrowPath(shape),
685
+ fill: "none"
686
+ }
687
+ ),
688
+ shape.text && hideField !== "text" && /* @__PURE__ */ jsx(ConnectorLabel, { shape })
689
+ ] });
690
+ case "text":
691
+ return /* @__PURE__ */ jsx(
692
+ "text",
693
+ {
694
+ x: shape.x,
695
+ y: shape.y + FONT_SIZE,
696
+ fill: shape.stroke,
697
+ fontSize: FONT_SIZE,
698
+ fontFamily: "ui-sans-serif, system-ui, sans-serif",
699
+ style: { userSelect: "none", whiteSpace: "pre" },
700
+ children: (shape.text ?? "").split("\n").map((line, i) => /* @__PURE__ */ jsx("tspan", { x: shape.x, dy: i === 0 ? 0 : FONT_SIZE * 1.35, children: line }, i))
701
+ }
702
+ );
703
+ }
704
+ }
705
+ function ConnectorLabel({ shape }) {
706
+ const text = shape.text ?? "";
707
+ const lines = wrapText(text, CONNECTOR_LABEL_MAX_WIDTH, CONNECTOR_FONT_SIZE);
708
+ const size = textBoxSize(lines.join("\n"), CONNECTOR_FONT_SIZE);
709
+ const { x: midX, y: midY } = connectorMidpoint(shape);
710
+ const lineH = CONNECTOR_FONT_SIZE * LINE_HEIGHT;
711
+ const startY = midY - (lines.length - 1) * lineH / 2 + CONNECTOR_FONT_SIZE * 0.35;
712
+ return /* @__PURE__ */ jsxs("g", { children: [
713
+ /* @__PURE__ */ jsx(
714
+ "rect",
715
+ {
716
+ x: midX - size.w / 2 - 5,
717
+ y: midY - size.h / 2 - 3,
718
+ width: size.w + 10,
719
+ height: size.h + 6,
720
+ rx: 4,
721
+ fill: "#ffffff",
722
+ opacity: 0.92
723
+ }
724
+ ),
725
+ lines.map((line, i) => /* @__PURE__ */ jsx(
726
+ "text",
727
+ {
728
+ x: midX,
729
+ y: startY + i * lineH,
730
+ fill: shape.stroke,
731
+ fontSize: CONNECTOR_FONT_SIZE,
732
+ fontFamily: "ui-sans-serif, system-ui, sans-serif",
733
+ textAnchor: "middle",
734
+ style: { userSelect: "none" },
735
+ children: line
736
+ },
737
+ i
738
+ ))
739
+ ] });
740
+ }
741
+ var TOOL_ICONS = {
742
+ select: /* @__PURE__ */ jsx("path", { d: "M4 2l12 6.5-5.2 1.6L8 16z", fill: "currentColor", stroke: "none" }),
743
+ rect: /* @__PURE__ */ jsx("rect", { x: "3", y: "4.5", width: "14", height: "11", rx: "2" }),
744
+ ellipse: /* @__PURE__ */ jsx("ellipse", { cx: "10", cy: "10", rx: "7", ry: "5.5" }),
745
+ triangle: /* @__PURE__ */ jsx("path", { d: "M10 3.5L17 16.5H3z", strokeLinejoin: "round" }),
746
+ pentagon: /* @__PURE__ */ jsx("path", { d: "M10 3L16.8 8.1L14.2 16.2H5.8L3.2 8.1z", strokeLinejoin: "round" }),
747
+ arrow: /* @__PURE__ */ jsxs("g", { children: [
748
+ /* @__PURE__ */ jsx("path", { d: "M4 16L16 4" }),
749
+ /* @__PURE__ */ jsx("path", { d: "M9 4h7v7" })
750
+ ] }),
751
+ line: /* @__PURE__ */ jsx("path", { d: "M4 16L16 4" }),
752
+ text: /* @__PURE__ */ jsxs("g", { children: [
753
+ /* @__PURE__ */ jsx("path", { d: "M4 5V3.5h12V5" }),
754
+ /* @__PURE__ */ jsx("path", { d: "M10 3.5V16.5" }),
755
+ /* @__PURE__ */ jsx("path", { d: "M7 16.5h6" })
756
+ ] })
757
+ };
758
+ function bindEndpoints(shape, shapes) {
759
+ const startBox = findBoxAt(shapes, { x: shape.x, y: shape.y }, shape.id);
760
+ const endBox = findBoxAt(
761
+ shapes,
762
+ { x: shape.x + shape.w, y: shape.y + shape.h },
763
+ shape.id
764
+ );
765
+ if (startBox && endBox && startBox.id === endBox.id) return shape;
766
+ return { ...shape, startBinding: startBox?.id, endBinding: endBox?.id };
767
+ }
768
+ var TOOLS = [
769
+ { tool: "select", label: "Select" },
770
+ { tool: "rect", label: "Rectangle" },
771
+ { tool: "ellipse", label: "Ellipse" },
772
+ { tool: "triangle", label: "Triangle" },
773
+ { tool: "pentagon", label: "Pentagon" },
774
+ { tool: "arrow", label: "Arrow" },
775
+ { tool: "line", label: "Line" },
776
+ { tool: "text", label: "Text" }
777
+ ];
778
+ function DrawingCanvas({ nodeKey, data }) {
779
+ const [editor] = useLexicalComposerContext5();
780
+ const isEditable = useLexicalEditable();
781
+ const [shapes, setShapes] = useState(data.shapes);
782
+ const [height, setHeight] = useState(data.height);
783
+ const [tool, setTool] = useState("select");
784
+ const [selectedId, setSelectedId] = useState(null);
785
+ const [editingText, setEditingText] = useState(null);
786
+ const [stroke, setStroke] = useState(STROKE_COLORS[0]);
787
+ const [fill, setFill] = useState(FILL_COLORS[0]);
788
+ const svgRef = useRef2(null);
789
+ const dragRef = useRef2(null);
790
+ const lastCommittedRef = useRef2(serializeDrawingData(data));
791
+ const shapesRef = useRef2(shapes);
792
+ shapesRef.current = shapes;
793
+ const heightRef = useRef2(height);
794
+ heightRef.current = height;
795
+ useEffect5(() => {
796
+ const incoming = serializeDrawingData(data);
797
+ if (incoming !== lastCommittedRef.current) {
798
+ lastCommittedRef.current = incoming;
799
+ setShapes(data.shapes);
800
+ setHeight(data.height);
801
+ setSelectedId(null);
802
+ setEditingText(null);
803
+ }
804
+ }, [data]);
805
+ const commit = useCallback(
806
+ (nextShapes, nextHeight) => {
807
+ const payload = {
808
+ version: 1,
809
+ height: nextHeight ?? heightRef.current,
810
+ shapes: nextShapes
811
+ };
812
+ const json = serializeDrawingData(payload);
813
+ if (json === lastCommittedRef.current) return;
814
+ lastCommittedRef.current = json;
815
+ editor.update(() => {
816
+ const node = $getNodeByKey2(nodeKey);
817
+ if ($isDrawingNode(node)) {
818
+ node.setData(payload);
819
+ }
820
+ });
821
+ },
822
+ [editor, nodeKey]
823
+ );
824
+ const updateShapes = useCallback(
825
+ (updater, options) => {
826
+ setShapes((prev) => {
827
+ const next = resolveBindings(updater(prev));
828
+ if (options?.commit) commit(next);
829
+ return next;
830
+ });
831
+ },
832
+ [commit]
833
+ );
834
+ const getPoint = useCallback((e) => {
835
+ const rect = svgRef.current?.getBoundingClientRect();
836
+ if (!rect) return { x: 0, y: 0 };
837
+ return { x: e.clientX - rect.left, y: e.clientY - rect.top };
838
+ }, []);
839
+ const selectedShape = shapes.find((s) => s.id === selectedId) ?? null;
840
+ const startTextEditing = useCallback((id, field) => {
841
+ setEditingText({ id, field });
842
+ setSelectedId(id);
843
+ }, []);
844
+ const handleBackgroundPointerDown = useCallback(
845
+ (e) => {
846
+ if (!isEditable || editingText) return;
847
+ if (e.button !== 0) return;
848
+ const point = getPoint(e);
849
+ svgRef.current?.setPointerCapture(e.pointerId);
850
+ if (tool === "select") {
851
+ setSelectedId(null);
852
+ return;
853
+ }
854
+ if (tool === "text") {
855
+ const id2 = createShapeId();
856
+ const size = textBoxSize("");
857
+ updateShapes((prev) => [
858
+ ...prev,
859
+ {
860
+ id: id2,
861
+ type: "text",
862
+ x: point.x,
863
+ y: point.y - FONT_SIZE / 2,
864
+ w: size.w,
865
+ h: size.h,
866
+ stroke,
867
+ fill: "transparent",
868
+ strokeWidth: 2,
869
+ text: ""
870
+ }
871
+ ]);
872
+ setTool("select");
873
+ startTextEditing(id2, "text");
874
+ return;
875
+ }
876
+ const id = createShapeId();
877
+ dragRef.current = { mode: "draw", id };
878
+ updateShapes((prev) => [
879
+ ...prev,
880
+ {
881
+ id,
882
+ type: tool,
883
+ x: point.x,
884
+ y: point.y,
885
+ w: 0,
886
+ h: 0,
887
+ stroke,
888
+ fill: isBoxType(tool) ? fill : "transparent",
889
+ strokeWidth: 2
890
+ }
891
+ ]);
892
+ },
893
+ [
894
+ isEditable,
895
+ editingText,
896
+ getPoint,
897
+ tool,
898
+ stroke,
899
+ fill,
900
+ updateShapes,
901
+ startTextEditing
902
+ ]
903
+ );
904
+ const handleShapePointerDown = useCallback(
905
+ (e, shape) => {
906
+ if (!isEditable || tool !== "select" || editingText) return;
907
+ if (e.button !== 0) return;
908
+ e.stopPropagation();
909
+ const point = getPoint(e);
910
+ svgRef.current?.setPointerCapture(e.pointerId);
911
+ dragRef.current = {
912
+ mode: "move",
913
+ id: shape.id,
914
+ startX: point.x,
915
+ startY: point.y,
916
+ orig: shape,
917
+ wasSelected: selectedId === shape.id
918
+ };
919
+ setSelectedId(shape.id);
920
+ },
921
+ [isEditable, tool, editingText, getPoint, selectedId]
922
+ );
923
+ const handleHandlePointerDown = useCallback(
924
+ (e, drag) => {
925
+ if (!isEditable) return;
926
+ if (e.button !== 0) return;
927
+ e.stopPropagation();
928
+ svgRef.current?.setPointerCapture(e.pointerId);
929
+ dragRef.current = drag;
930
+ if (drag.mode === "endpoint") {
931
+ const key = drag.end === "start" ? "startBinding" : "endBinding";
932
+ updateShapes(
933
+ (prev) => prev.map((s) => s.id === drag.id ? { ...s, [key]: void 0 } : s)
934
+ );
935
+ }
936
+ },
937
+ [isEditable, updateShapes]
938
+ );
939
+ const handlePointerMove = useCallback(
940
+ (e) => {
941
+ const drag = dragRef.current;
942
+ if (!drag) return;
943
+ const point = getPoint(e);
944
+ if (drag.mode === "height") {
945
+ const next = Math.round(
946
+ Math.min(
947
+ MAX_HEIGHT,
948
+ Math.max(MIN_HEIGHT, drag.origHeight + (e.clientY - drag.startY))
949
+ )
950
+ );
951
+ setHeight(next);
952
+ return;
953
+ }
954
+ updateShapes(
955
+ (prev) => prev.map((shape) => {
956
+ if (shape.id !== ("id" in drag ? drag.id : "")) return shape;
957
+ switch (drag.mode) {
958
+ case "draw":
959
+ return { ...shape, w: point.x - shape.x, h: point.y - shape.y };
960
+ case "move": {
961
+ const dx = point.x - drag.startX;
962
+ const dy = point.y - drag.startY;
963
+ return {
964
+ ...shape,
965
+ x: drag.orig.x + dx,
966
+ y: drag.orig.y + dy,
967
+ waypoints: drag.orig.waypoints?.map((p) => ({
968
+ x: p.x + dx,
969
+ y: p.y + dy
970
+ }))
971
+ };
972
+ }
973
+ case "resize": {
974
+ const o = drag.orig;
975
+ const right = o.x + o.w;
976
+ const bottom = o.y + o.h;
977
+ const nx = drag.corner === "nw" || drag.corner === "sw" ? point.x : o.x;
978
+ const ny = drag.corner === "nw" || drag.corner === "ne" ? point.y : o.y;
979
+ const nr = drag.corner === "ne" || drag.corner === "se" ? point.x : right;
980
+ const nb = drag.corner === "sw" || drag.corner === "se" ? point.y : bottom;
981
+ return { ...shape, x: nx, y: ny, w: nr - nx, h: nb - ny };
982
+ }
983
+ case "endpoint": {
984
+ const o = drag.orig;
985
+ if (drag.end === "start") {
986
+ return {
987
+ ...shape,
988
+ x: point.x,
989
+ y: point.y,
990
+ w: o.x + o.w - point.x,
991
+ h: o.y + o.h - point.y
992
+ };
993
+ }
994
+ return { ...shape, w: point.x - o.x, h: point.y - o.y };
995
+ }
996
+ case "elbow": {
997
+ const o = drag.orig;
998
+ const horizontal = Math.abs(o.w) >= Math.abs(o.h);
999
+ const span = horizontal ? o.w : o.h;
1000
+ if (Math.abs(span) < 1) return shape;
1001
+ const raw = horizontal ? (point.x - o.x) / span : (point.y - o.y) / span;
1002
+ return {
1003
+ ...shape,
1004
+ elbow: Math.min(0.95, Math.max(0.05, raw))
1005
+ };
1006
+ }
1007
+ case "waypoint": {
1008
+ const waypoints = shape.waypoints;
1009
+ if (!waypoints || drag.index < 0 || drag.index >= waypoints.length) {
1010
+ return shape;
1011
+ }
1012
+ const prevPt = drag.index === 0 ? { x: shape.x, y: shape.y } : waypoints[drag.index - 1];
1013
+ const nextPt = drag.index === waypoints.length - 1 ? { x: shape.x + shape.w, y: shape.y + shape.h } : waypoints[drag.index + 1];
1014
+ let px = point.x;
1015
+ let py = point.y;
1016
+ if (Math.abs(px - prevPt.x) < WAYPOINT_SNAP) px = prevPt.x;
1017
+ else if (Math.abs(px - nextPt.x) < WAYPOINT_SNAP) px = nextPt.x;
1018
+ if (Math.abs(py - prevPt.y) < WAYPOINT_SNAP) py = prevPt.y;
1019
+ else if (Math.abs(py - nextPt.y) < WAYPOINT_SNAP) py = nextPt.y;
1020
+ const next = [...waypoints];
1021
+ next[drag.index] = { x: px, y: py };
1022
+ return { ...shape, waypoints: next };
1023
+ }
1024
+ }
1025
+ })
1026
+ );
1027
+ },
1028
+ [getPoint, updateShapes]
1029
+ );
1030
+ const handlePointerUp = useCallback(() => {
1031
+ const drag = dragRef.current;
1032
+ dragRef.current = null;
1033
+ if (!drag) return;
1034
+ if (drag.mode === "height") {
1035
+ commit(shapesRef.current, heightRef.current);
1036
+ return;
1037
+ }
1038
+ if (drag.mode === "move" && drag.wasSelected) {
1039
+ const current = shapesRef.current.find((s) => s.id === drag.id);
1040
+ if (current && current.x === drag.orig.x && current.y === drag.orig.y) {
1041
+ if (current.type === "text" || isConnectorType(current.type)) {
1042
+ startTextEditing(current.id, "text");
1043
+ return;
1044
+ }
1045
+ if (isBoxType(current.type)) {
1046
+ const b = bbox(current);
1047
+ const rel = (drag.startY - b.y) / Math.max(b.h, 1);
1048
+ startTextEditing(
1049
+ current.id,
1050
+ rel < 0.3 ? "label" : rel > 0.7 ? "footer" : "text"
1051
+ );
1052
+ return;
1053
+ }
1054
+ }
1055
+ }
1056
+ if (drag.mode === "draw") {
1057
+ const drawn = shapesRef.current.find((s) => s.id === drag.id);
1058
+ if (drawn && Math.abs(drawn.w) < 4 && Math.abs(drawn.h) < 4) {
1059
+ updateShapes((prev) => prev.filter((s) => s.id !== drag.id));
1060
+ return;
1061
+ }
1062
+ if (drawn && (drawn.type === "arrow" || drawn.type === "line")) {
1063
+ updateShapes((prev) => prev.map((s) => s.id === drag.id ? bindEndpoints(s, prev) : s));
1064
+ }
1065
+ setTool("select");
1066
+ setSelectedId(drag.id);
1067
+ }
1068
+ if (drag.mode === "endpoint") {
1069
+ updateShapes(
1070
+ (prev) => prev.map((s) => {
1071
+ if (s.id !== drag.id) return s;
1072
+ const point = drag.end === "start" ? { x: s.x, y: s.y } : { x: s.x + s.w, y: s.y + s.h };
1073
+ const box = findBoxAt(prev, point, s.id);
1074
+ const otherBinding = drag.end === "start" ? s.endBinding : s.startBinding;
1075
+ if (box && box.id === otherBinding) return s;
1076
+ const key = drag.end === "start" ? "startBinding" : "endBinding";
1077
+ return { ...s, [key]: box?.id };
1078
+ })
1079
+ );
1080
+ }
1081
+ updateShapes((prev) => prev.map(normalize), { commit: true });
1082
+ }, [commit, updateShapes, startTextEditing]);
1083
+ const deleteSelected = useCallback(() => {
1084
+ if (!selectedId) return;
1085
+ setEditingText(null);
1086
+ setSelectedId(null);
1087
+ updateShapes((prev) => prev.filter((s) => s.id !== selectedId), {
1088
+ commit: true
1089
+ });
1090
+ }, [selectedId, updateShapes]);
1091
+ const handleKeyDown = useCallback(
1092
+ (e) => {
1093
+ if (!isEditable || editingText) return;
1094
+ if ((e.key === "Delete" || e.key === "Backspace") && selectedId) {
1095
+ e.preventDefault();
1096
+ e.stopPropagation();
1097
+ deleteSelected();
1098
+ } else if (e.key === "Enter" && selectedId) {
1099
+ const shape = shapes.find((s) => s.id === selectedId);
1100
+ if (shape) {
1101
+ e.preventDefault();
1102
+ e.stopPropagation();
1103
+ startTextEditing(shape.id, "text");
1104
+ }
1105
+ } else if (e.key === "Escape") {
1106
+ setSelectedId(null);
1107
+ setTool("select");
1108
+ }
1109
+ },
1110
+ [
1111
+ isEditable,
1112
+ editingText,
1113
+ selectedId,
1114
+ deleteSelected,
1115
+ shapes,
1116
+ startTextEditing
1117
+ ]
1118
+ );
1119
+ const applyStroke = useCallback(
1120
+ (color) => {
1121
+ setStroke(color);
1122
+ if (selectedId) {
1123
+ updateShapes(
1124
+ (prev) => prev.map((s) => s.id === selectedId ? { ...s, stroke: color } : s),
1125
+ { commit: true }
1126
+ );
1127
+ }
1128
+ },
1129
+ [selectedId, updateShapes]
1130
+ );
1131
+ const applyFill = useCallback(
1132
+ (color) => {
1133
+ setFill(color);
1134
+ if (selectedId) {
1135
+ updateShapes(
1136
+ (prev) => prev.map((s) => s.id === selectedId ? { ...s, fill: color } : s),
1137
+ { commit: true }
1138
+ );
1139
+ }
1140
+ },
1141
+ [selectedId, updateShapes]
1142
+ );
1143
+ const applyRouting = useCallback(
1144
+ (elbow) => {
1145
+ if (!selectedId) return;
1146
+ updateShapes(
1147
+ (prev) => prev.map(
1148
+ (s) => s.id === selectedId && isConnectorType(s.type) ? {
1149
+ ...s,
1150
+ routing: elbow ? "elbow" : void 0,
1151
+ elbow: elbow ? s.elbow : void 0,
1152
+ waypoints: void 0
1153
+ } : s
1154
+ ),
1155
+ { commit: true }
1156
+ );
1157
+ },
1158
+ [selectedId, updateShapes]
1159
+ );
1160
+ const addWaypoint = useCallback(
1161
+ (e, shapeId, segmentIndex, point) => {
1162
+ if (!isEditable) return;
1163
+ if (e.button !== 0) return;
1164
+ e.stopPropagation();
1165
+ svgRef.current?.setPointerCapture(e.pointerId);
1166
+ updateShapes(
1167
+ (prev) => prev.map((s) => {
1168
+ if (s.id !== shapeId || !isConnectorType(s.type)) return s;
1169
+ const interior = connectorPoints(s).slice(1, -1);
1170
+ const waypoints = [
1171
+ ...interior.slice(0, segmentIndex),
1172
+ point,
1173
+ ...interior.slice(segmentIndex)
1174
+ ];
1175
+ return { ...s, waypoints, routing: void 0, elbow: void 0 };
1176
+ })
1177
+ );
1178
+ dragRef.current = { mode: "waypoint", id: shapeId, index: segmentIndex };
1179
+ },
1180
+ [isEditable, updateShapes]
1181
+ );
1182
+ const removeWaypoint = useCallback(
1183
+ (shapeId, index) => {
1184
+ updateShapes(
1185
+ (prev) => prev.map((s) => {
1186
+ if (s.id !== shapeId || !s.waypoints) return s;
1187
+ const waypoints = s.waypoints.filter((_, i) => i !== index);
1188
+ return { ...s, waypoints: waypoints.length ? waypoints : void 0 };
1189
+ }),
1190
+ { commit: true }
1191
+ );
1192
+ },
1193
+ [updateShapes]
1194
+ );
1195
+ const applyDirection = useCallback(
1196
+ (bidirectional) => {
1197
+ if (!selectedId) return;
1198
+ updateShapes(
1199
+ (prev) => prev.map(
1200
+ (s) => s.id === selectedId && s.type === "arrow" ? { ...s, bidirectional: bidirectional || void 0 } : s
1201
+ ),
1202
+ { commit: true }
1203
+ );
1204
+ },
1205
+ [selectedId, updateShapes]
1206
+ );
1207
+ const commitText = useCallback(
1208
+ (id, field, value) => {
1209
+ setEditingText(null);
1210
+ updateShapes(
1211
+ (prev) => prev.flatMap((s) => {
1212
+ if (s.id !== id) return [s];
1213
+ if (s.type === "text") {
1214
+ return value.trim() === "" ? [] : [{ ...s, text: value, ...textBoxSize(value) }];
1215
+ }
1216
+ return [
1217
+ { ...s, [field]: value.trim() === "" ? void 0 : value }
1218
+ ];
1219
+ }),
1220
+ { commit: true }
1221
+ );
1222
+ },
1223
+ [updateShapes]
1224
+ );
1225
+ const editingShape = shapes.find((s) => s.id === editingText?.id) ?? null;
1226
+ const showFill = selectedShape != null ? isBoxType(selectedShape.type) : tool !== "select" && isBoxType(tool);
1227
+ const canvasCursor = tool === "select" ? "default" : tool === "text" ? "text" : "crosshair";
1228
+ return /* @__PURE__ */ jsxs(
1229
+ "div",
1230
+ {
1231
+ className: "zui-drawing-canvas",
1232
+ tabIndex: isEditable ? 0 : void 0,
1233
+ onKeyDown: handleKeyDown,
1234
+ children: [
1235
+ isEditable && /* @__PURE__ */ jsxs("div", { className: "zui-drawing-toolbar", onPointerDown: (e) => e.stopPropagation(), children: [
1236
+ /* @__PURE__ */ jsx("div", { className: "zui-drawing-toolbar-group", children: TOOLS.map(({ tool: t, label }) => /* @__PURE__ */ jsx(
1237
+ "button",
1238
+ {
1239
+ type: "button",
1240
+ title: label,
1241
+ "aria-label": label,
1242
+ className: `zui-drawing-tool ${tool === t ? "is-active" : ""}`,
1243
+ onClick: () => {
1244
+ setTool(t);
1245
+ if (t !== "select") setSelectedId(null);
1246
+ },
1247
+ children: /* @__PURE__ */ jsx(
1248
+ "svg",
1249
+ {
1250
+ viewBox: "0 0 20 20",
1251
+ width: "16",
1252
+ height: "16",
1253
+ fill: "none",
1254
+ stroke: "currentColor",
1255
+ strokeWidth: "1.6",
1256
+ strokeLinecap: "round",
1257
+ strokeLinejoin: "round",
1258
+ children: TOOL_ICONS[t]
1259
+ }
1260
+ )
1261
+ },
1262
+ t
1263
+ )) }),
1264
+ /* @__PURE__ */ jsxs("div", { className: "zui-drawing-toolbar-group", children: [
1265
+ /* @__PURE__ */ jsx("span", { className: "zui-drawing-swatch-label", children: "Stroke" }),
1266
+ STROKE_COLORS.map((color) => /* @__PURE__ */ jsx(
1267
+ "button",
1268
+ {
1269
+ type: "button",
1270
+ title: `Stroke ${color}`,
1271
+ "aria-label": `Stroke color ${color}`,
1272
+ className: `zui-drawing-swatch ${(selectedShape?.stroke ?? stroke) === color ? "is-active" : ""}`,
1273
+ style: { backgroundColor: color },
1274
+ onClick: () => applyStroke(color)
1275
+ },
1276
+ color
1277
+ ))
1278
+ ] }),
1279
+ showFill && /* @__PURE__ */ jsxs("div", { className: "zui-drawing-toolbar-group", children: [
1280
+ /* @__PURE__ */ jsx("span", { className: "zui-drawing-swatch-label", children: "Fill" }),
1281
+ FILL_COLORS.map((color) => /* @__PURE__ */ jsx(
1282
+ "button",
1283
+ {
1284
+ type: "button",
1285
+ title: `Fill ${color === "transparent" ? "none" : color}`,
1286
+ "aria-label": `Fill color ${color === "transparent" ? "none" : color}`,
1287
+ className: `zui-drawing-swatch ${color === "transparent" ? "is-transparent" : ""} ${(selectedShape?.fill ?? fill) === color ? "is-active" : ""}`,
1288
+ style: { backgroundColor: color },
1289
+ onClick: () => applyFill(color)
1290
+ },
1291
+ color
1292
+ ))
1293
+ ] }),
1294
+ selectedShape && isConnectorType(selectedShape.type) && /* @__PURE__ */ jsxs("div", { className: "zui-drawing-toolbar-group", children: [
1295
+ /* @__PURE__ */ jsx(
1296
+ "button",
1297
+ {
1298
+ type: "button",
1299
+ title: "Straight connector",
1300
+ "aria-label": "Straight connector",
1301
+ className: `zui-drawing-tool ${selectedShape.routing !== "elbow" ? "is-active" : ""}`,
1302
+ onClick: () => applyRouting(false),
1303
+ children: /* @__PURE__ */ jsx(
1304
+ "svg",
1305
+ {
1306
+ viewBox: "0 0 20 20",
1307
+ width: "16",
1308
+ height: "16",
1309
+ fill: "none",
1310
+ stroke: "currentColor",
1311
+ strokeWidth: "1.6",
1312
+ strokeLinecap: "round",
1313
+ children: /* @__PURE__ */ jsx("path", { d: "M4 16L16 4" })
1314
+ }
1315
+ )
1316
+ }
1317
+ ),
1318
+ /* @__PURE__ */ jsx(
1319
+ "button",
1320
+ {
1321
+ type: "button",
1322
+ title: "Elbow connector (right angles)",
1323
+ "aria-label": "Elbow connector",
1324
+ className: `zui-drawing-tool ${selectedShape.routing === "elbow" ? "is-active" : ""}`,
1325
+ onClick: () => applyRouting(true),
1326
+ children: /* @__PURE__ */ jsx(
1327
+ "svg",
1328
+ {
1329
+ viewBox: "0 0 20 20",
1330
+ width: "16",
1331
+ height: "16",
1332
+ fill: "none",
1333
+ stroke: "currentColor",
1334
+ strokeWidth: "1.6",
1335
+ strokeLinecap: "round",
1336
+ strokeLinejoin: "round",
1337
+ children: /* @__PURE__ */ jsx("path", { d: "M4 16v-6h12V4" })
1338
+ }
1339
+ )
1340
+ }
1341
+ )
1342
+ ] }),
1343
+ selectedShape?.type === "arrow" && /* @__PURE__ */ jsxs("div", { className: "zui-drawing-toolbar-group", children: [
1344
+ /* @__PURE__ */ jsx(
1345
+ "button",
1346
+ {
1347
+ type: "button",
1348
+ title: "One-way arrow",
1349
+ "aria-label": "One-way arrow",
1350
+ className: `zui-drawing-tool ${!selectedShape.bidirectional ? "is-active" : ""}`,
1351
+ onClick: () => applyDirection(false),
1352
+ children: /* @__PURE__ */ jsx(
1353
+ "svg",
1354
+ {
1355
+ viewBox: "0 0 20 20",
1356
+ width: "16",
1357
+ height: "16",
1358
+ fill: "none",
1359
+ stroke: "currentColor",
1360
+ strokeWidth: "1.6",
1361
+ strokeLinecap: "round",
1362
+ strokeLinejoin: "round",
1363
+ children: /* @__PURE__ */ jsx("path", { d: "M3 10h13M12 5.5L16.5 10L12 14.5" })
1364
+ }
1365
+ )
1366
+ }
1367
+ ),
1368
+ /* @__PURE__ */ jsx(
1369
+ "button",
1370
+ {
1371
+ type: "button",
1372
+ title: "Two-way arrow",
1373
+ "aria-label": "Two-way arrow",
1374
+ className: `zui-drawing-tool ${selectedShape.bidirectional ? "is-active" : ""}`,
1375
+ onClick: () => applyDirection(true),
1376
+ children: /* @__PURE__ */ jsx(
1377
+ "svg",
1378
+ {
1379
+ viewBox: "0 0 20 20",
1380
+ width: "16",
1381
+ height: "16",
1382
+ fill: "none",
1383
+ stroke: "currentColor",
1384
+ strokeWidth: "1.6",
1385
+ strokeLinecap: "round",
1386
+ strokeLinejoin: "round",
1387
+ children: /* @__PURE__ */ jsx("path", { d: "M3.5 10h13M8 5.5L3.5 10L8 14.5M12 5.5L16.5 10L12 14.5" })
1388
+ }
1389
+ )
1390
+ }
1391
+ )
1392
+ ] }),
1393
+ selectedShape && /* @__PURE__ */ jsx("div", { className: "zui-drawing-toolbar-group", children: /* @__PURE__ */ jsx(
1394
+ "button",
1395
+ {
1396
+ type: "button",
1397
+ title: "Delete shape",
1398
+ "aria-label": "Delete shape",
1399
+ className: "zui-drawing-tool zui-drawing-tool-danger",
1400
+ onClick: deleteSelected,
1401
+ children: /* @__PURE__ */ jsx(
1402
+ "svg",
1403
+ {
1404
+ viewBox: "0 0 20 20",
1405
+ width: "16",
1406
+ height: "16",
1407
+ fill: "none",
1408
+ stroke: "currentColor",
1409
+ strokeWidth: "1.6",
1410
+ strokeLinecap: "round",
1411
+ children: /* @__PURE__ */ jsx("path", { d: "M4 6h12M8 6V4h4v2M6 6l1 10h6l1-10M8.5 9v4M11.5 9v4" })
1412
+ }
1413
+ )
1414
+ }
1415
+ ) })
1416
+ ] }),
1417
+ /* @__PURE__ */ jsxs(
1418
+ "svg",
1419
+ {
1420
+ ref: svgRef,
1421
+ className: "zui-drawing-surface",
1422
+ style: { height, cursor: canvasCursor },
1423
+ onPointerDown: handleBackgroundPointerDown,
1424
+ onPointerMove: handlePointerMove,
1425
+ onPointerUp: handlePointerUp,
1426
+ onDoubleClick: (e) => {
1427
+ if (!isEditable) return;
1428
+ const target = e.target.closest("[data-shape-id]");
1429
+ const id = target?.getAttribute("data-shape-id");
1430
+ const shape = id ? shapes.find((s) => s.id === id) : null;
1431
+ if (!shape) return;
1432
+ if (shape.type === "text" || isConnectorType(shape.type)) {
1433
+ startTextEditing(shape.id, "text");
1434
+ } else if (isBoxType(shape.type)) {
1435
+ const b = bbox(shape);
1436
+ const rel = (getPoint(e).y - b.y) / Math.max(b.h, 1);
1437
+ startTextEditing(
1438
+ shape.id,
1439
+ rel < 0.3 ? "label" : rel > 0.7 ? "footer" : "text"
1440
+ );
1441
+ }
1442
+ },
1443
+ children: [
1444
+ shapes.map((shape) => /* @__PURE__ */ jsxs(
1445
+ "g",
1446
+ {
1447
+ "data-shape-id": shape.id,
1448
+ style: {
1449
+ cursor: isEditable && tool === "select" ? "move" : void 0
1450
+ },
1451
+ onPointerDown: (e) => handleShapePointerDown(e, shape),
1452
+ children: [
1453
+ isConnectorType(shape.type) && /* @__PURE__ */ jsx(
1454
+ "path",
1455
+ {
1456
+ d: connectorPath(shape),
1457
+ fill: "none",
1458
+ stroke: "transparent",
1459
+ strokeWidth: 14
1460
+ }
1461
+ ),
1462
+ (isBoxType(shape.type) || shape.type === "text") && /* @__PURE__ */ jsx(HitArea, { shape }),
1463
+ shape.id === editingText?.id && shape.type === "text" ? null : /* @__PURE__ */ jsx(
1464
+ ShapeView,
1465
+ {
1466
+ shape,
1467
+ hideField: shape.id === editingText?.id ? editingText.field : null,
1468
+ showHints: isEditable && tool === "select" && shape.id === selectedId && shape.id !== editingText?.id
1469
+ }
1470
+ )
1471
+ ]
1472
+ },
1473
+ shape.id
1474
+ )),
1475
+ selectedShape && isEditable && !editingText && /* @__PURE__ */ jsx(
1476
+ SelectionOverlay,
1477
+ {
1478
+ shape: selectedShape,
1479
+ onHandlePointerDown: handleHandlePointerDown,
1480
+ onWaypointAdd: addWaypoint,
1481
+ onWaypointRemove: removeWaypoint
1482
+ }
1483
+ )
1484
+ ]
1485
+ }
1486
+ ),
1487
+ editingShape && editingText && /* @__PURE__ */ jsx(
1488
+ TextEditOverlay,
1489
+ {
1490
+ shape: editingShape,
1491
+ field: editingText.field,
1492
+ onCommit: commitText
1493
+ },
1494
+ `${editingShape.id}:${editingText.field}`
1495
+ ),
1496
+ isEditable && /* @__PURE__ */ jsx(
1497
+ "div",
1498
+ {
1499
+ className: "zui-drawing-resize",
1500
+ title: "Drag to resize canvas",
1501
+ onPointerDown: (e) => {
1502
+ e.preventDefault();
1503
+ e.currentTarget.setPointerCapture(e.pointerId);
1504
+ dragRef.current = {
1505
+ mode: "height",
1506
+ startY: e.clientY,
1507
+ origHeight: heightRef.current
1508
+ };
1509
+ },
1510
+ onPointerMove: (e) => {
1511
+ const drag = dragRef.current;
1512
+ if (drag?.mode !== "height") return;
1513
+ setHeight(
1514
+ Math.round(
1515
+ Math.min(
1516
+ MAX_HEIGHT,
1517
+ Math.max(MIN_HEIGHT, drag.origHeight + (e.clientY - drag.startY))
1518
+ )
1519
+ )
1520
+ );
1521
+ },
1522
+ onPointerUp: () => {
1523
+ if (dragRef.current?.mode !== "height") return;
1524
+ dragRef.current = null;
1525
+ commit(shapesRef.current, heightRef.current);
1526
+ }
1527
+ }
1528
+ )
1529
+ ]
1530
+ }
1531
+ );
1532
+ }
1533
+ function HitArea({ shape }) {
1534
+ const b = bbox(shape);
1535
+ return /* @__PURE__ */ jsx(
1536
+ "rect",
1537
+ {
1538
+ x: b.x,
1539
+ y: b.y,
1540
+ width: Math.max(b.w, 8),
1541
+ height: Math.max(b.h, 8),
1542
+ fill: "transparent",
1543
+ stroke: "none"
1544
+ }
1545
+ );
1546
+ }
1547
+ function SelectionOverlay({
1548
+ shape,
1549
+ onHandlePointerDown,
1550
+ onWaypointAdd,
1551
+ onWaypointRemove
1552
+ }) {
1553
+ if (shape.type === "line" || shape.type === "arrow") {
1554
+ const ends = [
1555
+ { end: "start", x: shape.x, y: shape.y },
1556
+ { end: "end", x: shape.x + shape.w, y: shape.y + shape.h }
1557
+ ];
1558
+ const points = connectorPoints(shape);
1559
+ const waypoints = shape.waypoints ?? [];
1560
+ const hasElbowHandle = shape.routing === "elbow" && !waypoints.length;
1561
+ const elbowMid = hasElbowHandle ? connectorMidpoint(shape) : null;
1562
+ const elbowCursor = Math.abs(shape.w) >= Math.abs(shape.h) ? "ew-resize" : "ns-resize";
1563
+ const ghosts = points.slice(0, -1).flatMap((p, i) => {
1564
+ const q = points[i + 1];
1565
+ if (Math.hypot(q.x - p.x, q.y - p.y) < 28) return [];
1566
+ if (hasElbowHandle && i === 1) return [];
1567
+ return [{ index: i, x: (p.x + q.x) / 2, y: (p.y + q.y) / 2 }];
1568
+ });
1569
+ return /* @__PURE__ */ jsxs("g", { children: [
1570
+ ghosts.map(({ index, x, y }) => /* @__PURE__ */ jsx(
1571
+ "circle",
1572
+ {
1573
+ cx: x,
1574
+ cy: y,
1575
+ r: 4.5,
1576
+ fill: "#fff",
1577
+ stroke: SELECTION_COLOR,
1578
+ strokeWidth: 1.5,
1579
+ strokeDasharray: "2 2",
1580
+ opacity: 0.6,
1581
+ style: { cursor: "copy" },
1582
+ onPointerDown: (e) => onWaypointAdd(e, shape.id, index, { x, y }),
1583
+ children: /* @__PURE__ */ jsx("title", { children: "Drag to add a point" })
1584
+ },
1585
+ `ghost-${index}`
1586
+ )),
1587
+ waypoints.map((p, i) => /* @__PURE__ */ jsx(
1588
+ "rect",
1589
+ {
1590
+ x: p.x - 4,
1591
+ y: p.y - 4,
1592
+ width: 8,
1593
+ height: 8,
1594
+ rx: 2,
1595
+ fill: "#fff",
1596
+ stroke: SELECTION_COLOR,
1597
+ strokeWidth: 1.5,
1598
+ style: { cursor: "move" },
1599
+ onPointerDown: (e) => onHandlePointerDown(e, {
1600
+ mode: "waypoint",
1601
+ id: shape.id,
1602
+ index: i
1603
+ }),
1604
+ onDoubleClick: (e) => {
1605
+ e.stopPropagation();
1606
+ onWaypointRemove(shape.id, i);
1607
+ },
1608
+ children: /* @__PURE__ */ jsx("title", { children: "Drag to move, double-click to remove" })
1609
+ },
1610
+ `wp-${i}`
1611
+ )),
1612
+ ends.map(({ end, x, y }) => /* @__PURE__ */ jsx(
1613
+ "circle",
1614
+ {
1615
+ cx: x,
1616
+ cy: y,
1617
+ r: 5,
1618
+ fill: "#fff",
1619
+ stroke: SELECTION_COLOR,
1620
+ strokeWidth: 1.5,
1621
+ style: { cursor: "crosshair" },
1622
+ onPointerDown: (e) => onHandlePointerDown(e, {
1623
+ mode: "endpoint",
1624
+ id: shape.id,
1625
+ end,
1626
+ orig: shape
1627
+ })
1628
+ },
1629
+ end
1630
+ )),
1631
+ elbowMid && /* @__PURE__ */ jsx(
1632
+ "rect",
1633
+ {
1634
+ x: elbowMid.x - 4,
1635
+ y: elbowMid.y - 4,
1636
+ width: 8,
1637
+ height: 8,
1638
+ rx: 2,
1639
+ fill: "#fff",
1640
+ stroke: SELECTION_COLOR,
1641
+ strokeWidth: 1.5,
1642
+ style: { cursor: elbowCursor },
1643
+ onPointerDown: (e) => onHandlePointerDown(e, {
1644
+ mode: "elbow",
1645
+ id: shape.id,
1646
+ orig: shape
1647
+ })
1648
+ }
1649
+ )
1650
+ ] });
1651
+ }
1652
+ const b = bbox(shape);
1653
+ const pad = 5;
1654
+ const corners = [
1655
+ { corner: "nw", x: b.x - pad, y: b.y - pad },
1656
+ { corner: "ne", x: b.x + b.w + pad, y: b.y - pad },
1657
+ { corner: "sw", x: b.x - pad, y: b.y + b.h + pad },
1658
+ { corner: "se", x: b.x + b.w + pad, y: b.y + b.h + pad }
1659
+ ];
1660
+ const resizable = shape.type !== "text";
1661
+ return /* @__PURE__ */ jsxs("g", { children: [
1662
+ /* @__PURE__ */ jsx(
1663
+ "rect",
1664
+ {
1665
+ x: b.x - pad,
1666
+ y: b.y - pad,
1667
+ width: b.w + pad * 2,
1668
+ height: b.h + pad * 2,
1669
+ fill: "none",
1670
+ stroke: SELECTION_COLOR,
1671
+ strokeWidth: 1,
1672
+ strokeDasharray: "4 3",
1673
+ pointerEvents: "none"
1674
+ }
1675
+ ),
1676
+ resizable && corners.map(({ corner, x, y }) => /* @__PURE__ */ jsx(
1677
+ "rect",
1678
+ {
1679
+ x: x - 4,
1680
+ y: y - 4,
1681
+ width: 8,
1682
+ height: 8,
1683
+ rx: 2,
1684
+ fill: "#fff",
1685
+ stroke: SELECTION_COLOR,
1686
+ strokeWidth: 1.5,
1687
+ style: {
1688
+ cursor: corner === "nw" || corner === "se" ? "nwse-resize" : "nesw-resize"
1689
+ },
1690
+ onPointerDown: (e) => onHandlePointerDown(e, { mode: "resize", id: shape.id, corner, orig: shape })
1691
+ },
1692
+ corner
1693
+ ))
1694
+ ] });
1695
+ }
1696
+ function TextEditOverlay({
1697
+ shape,
1698
+ field,
1699
+ onCommit
1700
+ }) {
1701
+ const original = shape[field] ?? "";
1702
+ const [value, setValue] = useState(original);
1703
+ const ref = useRef2(null);
1704
+ useEffect5(() => {
1705
+ ref.current?.focus();
1706
+ ref.current?.select();
1707
+ }, []);
1708
+ const style = { color: shape.stroke };
1709
+ if (shape.type === "text") {
1710
+ Object.assign(style, {
1711
+ left: shape.x,
1712
+ top: shape.y,
1713
+ fontSize: FONT_SIZE,
1714
+ minWidth: Math.max(80, textBoxSize(value).w + 20),
1715
+ height: textBoxSize(value).h + 8
1716
+ });
1717
+ } else if (isConnectorType(shape.type)) {
1718
+ const lines = wrapText(value, CONNECTOR_LABEL_MAX_WIDTH, CONNECTOR_FONT_SIZE);
1719
+ const size = textBoxSize(lines.join("\n"), CONNECTOR_FONT_SIZE);
1720
+ const width = Math.max(70, size.w + 16);
1721
+ const height = size.h + 8;
1722
+ const mid = connectorMidpoint(shape);
1723
+ Object.assign(style, {
1724
+ left: mid.x - width / 2,
1725
+ top: mid.y - height / 2,
1726
+ width,
1727
+ height,
1728
+ fontSize: CONNECTOR_FONT_SIZE,
1729
+ textAlign: "center",
1730
+ whiteSpace: "pre-wrap"
1731
+ });
1732
+ } else {
1733
+ const b = bbox(shape);
1734
+ const fontSize = field === "text" ? FONT_SIZE : SMALL_FONT_SIZE;
1735
+ const width = Math.max(b.w - 8, 40);
1736
+ const lines = wrapText(value, Math.max(width - 8, 20), fontSize).length;
1737
+ const boxH = lines * fontSize * LINE_HEIGHT + 8;
1738
+ const top = field === "label" ? b.y + 3 : field === "footer" ? b.y + b.h - boxH - 3 : b.y + b.h / 2 - boxH / 2;
1739
+ Object.assign(style, {
1740
+ left: b.x + 4,
1741
+ top,
1742
+ width,
1743
+ height: boxH,
1744
+ fontSize,
1745
+ fontWeight: field === "label" ? 600 : void 0,
1746
+ textAlign: "center",
1747
+ background: "transparent",
1748
+ whiteSpace: "pre-wrap"
1749
+ });
1750
+ }
1751
+ const placeholder = field === "label" ? "Label" : field === "footer" ? "Footer" : "Text";
1752
+ return /* @__PURE__ */ jsx(
1753
+ "textarea",
1754
+ {
1755
+ ref,
1756
+ className: "zui-drawing-text-input",
1757
+ style,
1758
+ value,
1759
+ placeholder,
1760
+ onChange: (e) => setValue(e.target.value),
1761
+ onBlur: () => onCommit(shape.id, field, value),
1762
+ onKeyDown: (e) => {
1763
+ e.stopPropagation();
1764
+ if (e.key === "Enter" && !e.shiftKey) {
1765
+ e.preventDefault();
1766
+ onCommit(shape.id, field, value);
1767
+ } else if (e.key === "Escape") {
1768
+ e.preventDefault();
1769
+ onCommit(shape.id, field, original);
1770
+ }
1771
+ },
1772
+ onPointerDown: (e) => e.stopPropagation()
1773
+ }
1774
+ );
1775
+ }
1776
+
1777
+ // src/nodes/DrawingNode.tsx
1778
+ import { jsx as jsx2 } from "react/jsx-runtime";
1779
+ var DrawingNode = class _DrawingNode extends DecoratorNode {
1780
+ static getType() {
1781
+ return "drawing";
1782
+ }
1783
+ static clone(node) {
1784
+ return new _DrawingNode(node.__data, node.__key);
1785
+ }
1786
+ constructor(data, key) {
1787
+ super(key);
1788
+ this.__data = data;
1789
+ }
1790
+ static importJSON(serializedNode) {
1791
+ return $createDrawingNode(serializedNode.data);
1792
+ }
1793
+ exportJSON() {
1794
+ return {
1795
+ data: this.__data,
1796
+ type: "drawing",
1797
+ version: 1
1798
+ };
1799
+ }
1800
+ createDOM(config) {
1801
+ const div = document.createElement("div");
1802
+ const className = config.theme.drawing;
1803
+ div.className = typeof className === "string" ? className : "zui-drawing";
1804
+ return div;
1805
+ }
1806
+ updateDOM() {
1807
+ return false;
1808
+ }
1809
+ exportDOM() {
1810
+ const element = document.createElement("pre");
1811
+ element.setAttribute("data-drawing", this.__data);
1812
+ return { element };
1813
+ }
1814
+ static importDOM() {
1815
+ return {
1816
+ pre: (domNode) => {
1817
+ if (!domNode.hasAttribute("data-drawing")) return null;
1818
+ return {
1819
+ conversion: (element) => ({
1820
+ node: $createDrawingNode(
1821
+ serializeDrawingData(
1822
+ parseDrawingData(element.getAttribute("data-drawing") ?? "")
1823
+ )
1824
+ )
1825
+ }),
1826
+ priority: 2
1827
+ };
1828
+ }
1829
+ };
1830
+ }
1831
+ getData() {
1832
+ return parseDrawingData(this.getLatest().__data);
1833
+ }
1834
+ setData(data) {
1835
+ const writable = this.getWritable();
1836
+ writable.__data = serializeDrawingData(data);
1837
+ }
1838
+ isInline() {
1839
+ return false;
1840
+ }
1841
+ decorate(_editor, _config) {
1842
+ return /* @__PURE__ */ jsx2(DrawingCanvas, { nodeKey: this.getKey(), data: this.getData() });
1843
+ }
1844
+ };
1845
+ function $createDrawingNode(data = serializeDrawingData(EMPTY_DRAWING)) {
1846
+ return $applyNodeReplacement(new DrawingNode(data));
1847
+ }
1848
+ function $isDrawingNode(node) {
1849
+ return node instanceof DrawingNode;
1850
+ }
1851
+ var DRAWING_START = /^```drawing\s*$/;
1852
+ var DRAWING_END = /^```\s*$/;
1853
+ var DRAWING = {
1854
+ dependencies: [DrawingNode],
1855
+ export: (node) => {
1856
+ if (!$isDrawingNode(node)) return null;
1857
+ return `\`\`\`drawing
1858
+ ${node.getLatest().__data}
1859
+ \`\`\``;
1860
+ },
1861
+ regExpStart: DRAWING_START,
1862
+ regExpEnd: DRAWING_END,
1863
+ replace: (rootNode, children, _startMatch, _endMatch, linesInBetween, isImport) => {
1864
+ if (!isImport || children != null || linesInBetween == null) return false;
1865
+ const json = linesInBetween.join("\n").trim();
1866
+ const data = parseDrawingData(json);
1867
+ rootNode.append($createDrawingNode(serializeDrawingData(data)));
1868
+ },
1869
+ type: "multiline-element"
1870
+ };
1871
+
1872
+ // src/plugins/ToolbarPlugin.tsx
1873
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
1874
+ var FORMAT_BUTTONS = [
1875
+ {
1876
+ format: "bold",
1877
+ label: "Bold",
1878
+ icon: /* @__PURE__ */ jsx3(
1879
+ "path",
1880
+ {
1881
+ d: "M6 3.5h5a3 3 0 0 1 0 6H6zm0 6h6a3 3 0 0 1 0 6H6z",
1882
+ strokeWidth: "1.8"
1883
+ }
1884
+ )
1885
+ },
1886
+ {
1887
+ format: "italic",
1888
+ label: "Italic",
1889
+ icon: /* @__PURE__ */ jsx3("path", { d: "M8.5 3.5h6M5.5 16.5h6M11.5 3.5l-3 13", strokeWidth: "1.6" })
1890
+ },
1891
+ {
1892
+ format: "strikethrough",
1893
+ label: "Strikethrough",
1894
+ icon: /* @__PURE__ */ jsx3(
1895
+ "path",
1896
+ {
1897
+ d: "M4 10h12M13.5 5.5c-.6-1.2-2-2-3.5-2-2 0-3.5 1.2-3.5 2.8 0 .5.1.9.4 1.3m-.4 5c.5 1.6 2 2.9 3.9 2.9 2 0 3.6-1.2 3.6-2.9 0-.4-.1-.8-.2-1.1",
1898
+ strokeWidth: "1.6"
1899
+ }
1900
+ )
1901
+ },
1902
+ {
1903
+ format: "code",
1904
+ label: "Inline code",
1905
+ icon: /* @__PURE__ */ jsx3("path", { d: "M7.5 6L4 10l3.5 4M12.5 6L16 10l-3.5 4", strokeWidth: "1.6" })
1906
+ }
1907
+ ];
1908
+ function ToolbarPlugin() {
1909
+ const [editor] = useLexicalComposerContext6();
1910
+ const [activeFormats, setActiveFormats] = useState2(
1911
+ /* @__PURE__ */ new Set()
1912
+ );
1913
+ useEffect6(() => {
1914
+ return editor.registerUpdateListener(({ editorState }) => {
1915
+ editorState.read(() => {
1916
+ const selection = $getSelection2();
1917
+ if (!$isRangeSelection2(selection)) {
1918
+ setActiveFormats(/* @__PURE__ */ new Set());
1919
+ return;
1920
+ }
1921
+ const next = /* @__PURE__ */ new Set();
1922
+ for (const { format } of FORMAT_BUTTONS) {
1923
+ if (selection.hasFormat(format)) next.add(format);
1924
+ }
1925
+ setActiveFormats(next);
1926
+ });
1927
+ });
1928
+ }, [editor]);
1929
+ const insertTable = useCallback2(() => {
1930
+ editor.dispatchCommand(INSERT_TABLE_COMMAND, {
1931
+ columns: "3",
1932
+ rows: "3",
1933
+ includeHeaders: { rows: true, columns: false }
1934
+ });
1935
+ }, [editor]);
1936
+ const insertDrawing = useCallback2(() => {
1937
+ editor.update(() => {
1938
+ const drawing = $createDrawingNode();
1939
+ $insertNodeToNearestRoot(drawing);
1940
+ });
1941
+ }, [editor]);
1942
+ return /* @__PURE__ */ jsxs2("div", { className: "zui-text-editor-toolbar", children: [
1943
+ FORMAT_BUTTONS.map(({ format, label, icon }) => /* @__PURE__ */ jsx3(
1944
+ "button",
1945
+ {
1946
+ type: "button",
1947
+ title: label,
1948
+ "aria-label": label,
1949
+ "aria-pressed": activeFormats.has(format),
1950
+ className: `zui-text-editor-toolbar-button ${activeFormats.has(format) ? "is-active" : ""}`,
1951
+ onMouseDown: (e) => e.preventDefault(),
1952
+ onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, format),
1953
+ children: /* @__PURE__ */ jsx3(
1954
+ "svg",
1955
+ {
1956
+ viewBox: "0 0 20 20",
1957
+ width: "16",
1958
+ height: "16",
1959
+ fill: "none",
1960
+ stroke: "currentColor",
1961
+ strokeLinecap: "round",
1962
+ strokeLinejoin: "round",
1963
+ children: icon
1964
+ }
1965
+ )
1966
+ },
1967
+ format
1968
+ )),
1969
+ /* @__PURE__ */ jsx3("div", { className: "zui-text-editor-toolbar-divider" }),
1970
+ /* @__PURE__ */ jsx3(
1971
+ "button",
1972
+ {
1973
+ type: "button",
1974
+ title: "Insert table",
1975
+ "aria-label": "Insert table",
1976
+ className: "zui-text-editor-toolbar-button",
1977
+ onMouseDown: (e) => e.preventDefault(),
1978
+ onClick: insertTable,
1979
+ children: /* @__PURE__ */ jsxs2(
1980
+ "svg",
1981
+ {
1982
+ viewBox: "0 0 20 20",
1983
+ width: "16",
1984
+ height: "16",
1985
+ fill: "none",
1986
+ stroke: "currentColor",
1987
+ strokeWidth: "1.5",
1988
+ strokeLinecap: "round",
1989
+ children: [
1990
+ /* @__PURE__ */ jsx3("rect", { x: "3", y: "3.5", width: "14", height: "13", rx: "1.5" }),
1991
+ /* @__PURE__ */ jsx3("path", { d: "M3 8h14M8 8v8.5M13 8v8.5" })
1992
+ ]
1993
+ }
1994
+ )
1995
+ }
1996
+ ),
1997
+ /* @__PURE__ */ jsx3(
1998
+ "button",
1999
+ {
2000
+ type: "button",
2001
+ title: "Insert drawing",
2002
+ "aria-label": "Insert drawing",
2003
+ className: "zui-text-editor-toolbar-button",
2004
+ onMouseDown: (e) => e.preventDefault(),
2005
+ onClick: insertDrawing,
2006
+ children: /* @__PURE__ */ jsxs2(
2007
+ "svg",
2008
+ {
2009
+ viewBox: "0 0 20 20",
2010
+ width: "16",
2011
+ height: "16",
2012
+ fill: "none",
2013
+ stroke: "currentColor",
2014
+ strokeWidth: "1.5",
2015
+ strokeLinecap: "round",
2016
+ strokeLinejoin: "round",
2017
+ children: [
2018
+ /* @__PURE__ */ jsx3("rect", { x: "3", y: "5", width: "8", height: "6", rx: "1.5" }),
2019
+ /* @__PURE__ */ jsx3("path", { d: "M13.5 8h3M14.5 6.5L17.5 8l-3 1.5" }),
2020
+ /* @__PURE__ */ jsx3("ellipse", { cx: "13", cy: "14.5", rx: "4", ry: "2.8" })
2021
+ ]
2022
+ }
2023
+ )
2024
+ }
2025
+ )
2026
+ ] });
2027
+ }
2028
+
2029
+ // src/nodes/FrontmatterNode.ts
2030
+ import {
2031
+ $applyNodeReplacement as $applyNodeReplacement2,
198
2032
  $createLineBreakNode,
199
2033
  $createTextNode,
200
2034
  ElementNode
@@ -239,7 +2073,7 @@ var FrontmatterNode = class _FrontmatterNode extends ElementNode {
239
2073
  }
240
2074
  };
241
2075
  function $createFrontmatterNode() {
242
- return $applyNodeReplacement(new FrontmatterNode());
2076
+ return $applyNodeReplacement2(new FrontmatterNode());
243
2077
  }
244
2078
  function $isFrontmatterNode(node) {
245
2079
  return node instanceof FrontmatterNode;
@@ -272,6 +2106,122 @@ ${body}
272
2106
  type: "multiline-element"
273
2107
  };
274
2108
 
2109
+ // src/transformers/tableTransformer.ts
2110
+ import {
2111
+ $isParagraphNode as $isParagraphNode2,
2112
+ $isTextNode
2113
+ } from "lexical";
2114
+ import {
2115
+ $convertFromMarkdownString as $convertFromMarkdownString2,
2116
+ $convertToMarkdownString as $convertToMarkdownString2,
2117
+ TRANSFORMERS
2118
+ } from "@lexical/markdown";
2119
+ import {
2120
+ $createTableCellNode,
2121
+ $createTableNode,
2122
+ $createTableRowNode,
2123
+ $isTableCellNode,
2124
+ $isTableNode,
2125
+ $isTableRowNode,
2126
+ TableCellHeaderStates,
2127
+ TableCellNode,
2128
+ TableNode,
2129
+ TableRowNode
2130
+ } from "@lexical/table";
2131
+ var TABLE_ROW_REG_EXP = /^\|(.+)\|\s?$/;
2132
+ var TABLE_ROW_DIVIDER_REG_EXP = /^(\| ?:?-*:? ?)+\|\s?$/;
2133
+ function getTableColumnsSize(table) {
2134
+ const row = table.getFirstChild();
2135
+ return $isTableRowNode(row) ? row.getChildrenSize() : 0;
2136
+ }
2137
+ function createTableCell(textContent) {
2138
+ const unescaped = textContent.replace(/\\n/g, "\n");
2139
+ const cell = $createTableCellNode(TableCellHeaderStates.NO_STATUS);
2140
+ $convertFromMarkdownString2(unescaped, TRANSFORMERS, cell);
2141
+ return cell;
2142
+ }
2143
+ function mapToTableCells(textContent) {
2144
+ const match = textContent.match(TABLE_ROW_REG_EXP);
2145
+ if (!match || !match[1]) return null;
2146
+ return match[1].split("|").map((text) => createTableCell(text.trim()));
2147
+ }
2148
+ var TABLE = {
2149
+ dependencies: [TableNode, TableRowNode, TableCellNode],
2150
+ export: (node) => {
2151
+ if (!$isTableNode(node)) return null;
2152
+ const output = [];
2153
+ for (const row of node.getChildren()) {
2154
+ if (!$isTableRowNode(row)) continue;
2155
+ const rowOutput = [];
2156
+ let isHeaderRow = false;
2157
+ for (const cell of row.getChildren()) {
2158
+ if (!$isTableCellNode(cell)) continue;
2159
+ rowOutput.push(
2160
+ $convertToMarkdownString2(TRANSFORMERS, cell).replace(/\n/g, "\\n")
2161
+ );
2162
+ if (cell.__headerState === TableCellHeaderStates.ROW) {
2163
+ isHeaderRow = true;
2164
+ }
2165
+ }
2166
+ output.push(`| ${rowOutput.join(" | ")} |`);
2167
+ if (isHeaderRow) {
2168
+ output.push(`| ${rowOutput.map(() => "---").join(" | ")} |`);
2169
+ }
2170
+ }
2171
+ return output.join("\n");
2172
+ },
2173
+ regExp: TABLE_ROW_REG_EXP,
2174
+ replace: (parentNode, _children, match) => {
2175
+ if (TABLE_ROW_DIVIDER_REG_EXP.test(match[0])) {
2176
+ const table2 = parentNode.getPreviousSibling();
2177
+ if (!table2 || !$isTableNode(table2)) return;
2178
+ const lastRow = table2.getLastChild();
2179
+ if (!lastRow || !$isTableRowNode(lastRow)) return;
2180
+ lastRow.getChildren().forEach((cell) => {
2181
+ if ($isTableCellNode(cell)) {
2182
+ cell.setHeaderStyles(TableCellHeaderStates.ROW, TableCellHeaderStates.ROW);
2183
+ }
2184
+ });
2185
+ parentNode.remove();
2186
+ return;
2187
+ }
2188
+ const matchCells = mapToTableCells(match[0]);
2189
+ if (matchCells == null) return;
2190
+ const rows = [matchCells];
2191
+ let sibling = parentNode.getPreviousSibling();
2192
+ let maxCells = matchCells.length;
2193
+ while (sibling) {
2194
+ if (!$isParagraphNode2(sibling) || sibling.getChildrenSize() !== 1) break;
2195
+ const firstChild = sibling.getFirstChild();
2196
+ if (!$isTextNode(firstChild)) break;
2197
+ const cells = mapToTableCells(firstChild.getTextContent());
2198
+ if (cells == null) break;
2199
+ maxCells = Math.max(maxCells, cells.length);
2200
+ rows.unshift(cells);
2201
+ const previousSibling2 = sibling.getPreviousSibling();
2202
+ sibling.remove();
2203
+ sibling = previousSibling2;
2204
+ }
2205
+ const table = $createTableNode();
2206
+ for (const cells of rows) {
2207
+ const tableRow = $createTableRowNode();
2208
+ table.append(tableRow);
2209
+ for (let i = 0; i < maxCells; i++) {
2210
+ tableRow.append(i < cells.length ? cells[i] : createTableCell(""));
2211
+ }
2212
+ }
2213
+ const previousSibling = parentNode.getPreviousSibling();
2214
+ if ($isTableNode(previousSibling) && getTableColumnsSize(previousSibling) === maxCells) {
2215
+ previousSibling.append(...table.getChildren());
2216
+ parentNode.remove();
2217
+ } else {
2218
+ parentNode.replace(table);
2219
+ }
2220
+ table.selectEnd();
2221
+ },
2222
+ type: "element"
2223
+ };
2224
+
275
2225
  // src/theme.ts
276
2226
  var editorTheme = {
277
2227
  ltr: "text-left",
@@ -342,13 +2292,19 @@ var editorTheme = {
342
2292
  variable: "text-blue-500"
343
2293
  },
344
2294
  quote: "border-l-4 border-border pl-4 italic",
345
- frontmatter: "zui-frontmatter"
2295
+ frontmatter: "zui-frontmatter",
2296
+ table: "zui-table",
2297
+ tableCell: "zui-table-cell",
2298
+ tableCellHeader: "zui-table-cell-header",
2299
+ tableSelected: "zui-table-selected",
2300
+ tableSelection: "zui-table-selection",
2301
+ drawing: "zui-drawing"
346
2302
  };
347
2303
 
348
2304
  // src/MarkdownEditor.tsx
349
- import { jsx, jsxs } from "react/jsx-runtime";
350
- var SYNC_TRANSFORMERS = [FRONTMATTER, CHECK_LIST, ...TRANSFORMERS];
351
- var SHORTCUT_TRANSFORMERS = TRANSFORMERS.filter((t) => t !== CODE);
2305
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
2306
+ var SYNC_TRANSFORMERS = [FRONTMATTER, DRAWING, CHECK_LIST, TABLE, ...TRANSFORMERS2];
2307
+ var SHORTCUT_TRANSFORMERS = [TABLE, ...TRANSFORMERS2.filter((t) => t !== CODE)];
352
2308
  function onError(error) {
353
2309
  console.error(error);
354
2310
  }
@@ -361,11 +2317,15 @@ var editorNodes = [
361
2317
  CodeHighlightNode,
362
2318
  AutoLinkNode,
363
2319
  LinkNode,
364
- FrontmatterNode
2320
+ FrontmatterNode,
2321
+ TableNode2,
2322
+ TableRowNode2,
2323
+ TableCellNode2,
2324
+ DrawingNode
365
2325
  ];
366
2326
  function ReadOnlyPlugin({ readOnly }) {
367
- const [editor] = useLexicalComposerContext5();
368
- useEffect5(() => {
2327
+ const [editor] = useLexicalComposerContext7();
2328
+ useEffect7(() => {
369
2329
  editor.setEditable(!readOnly);
370
2330
  }, [editor, readOnly]);
371
2331
  return null;
@@ -377,17 +2337,18 @@ function MarkdownEditor({
377
2337
  readOnly = false,
378
2338
  className,
379
2339
  mode = "edit-md",
380
- autoFocus = false
2340
+ autoFocus = false,
2341
+ toolbar = true
381
2342
  }) {
382
- const latestValueRef = useRef2(value ?? "");
383
- const [mountKey, setMountKey] = useState(0);
384
- const [capturedMarkdown, setCapturedMarkdown] = useState(value ?? "");
385
- useEffect5(() => {
2343
+ const latestValueRef = useRef3(value ?? "");
2344
+ const [mountKey, setMountKey] = useState3(0);
2345
+ const [capturedMarkdown, setCapturedMarkdown] = useState3(value ?? "");
2346
+ useEffect7(() => {
386
2347
  if (value !== void 0) {
387
2348
  latestValueRef.current = value;
388
2349
  }
389
2350
  }, [value]);
390
- const handleTextChange = useCallback(
2351
+ const handleTextChange = useCallback3(
391
2352
  (e) => {
392
2353
  const newValue = e.target.value;
393
2354
  latestValueRef.current = newValue;
@@ -395,15 +2356,15 @@ function MarkdownEditor({
395
2356
  },
396
2357
  [onChange]
397
2358
  );
398
- const handleLexicalChange = useCallback(
2359
+ const handleLexicalChange = useCallback3(
399
2360
  (newValue) => {
400
2361
  latestValueRef.current = newValue;
401
2362
  onChange?.(newValue);
402
2363
  },
403
2364
  [onChange]
404
2365
  );
405
- const prevModeRef = useRef2(mode);
406
- useEffect5(() => {
2366
+ const prevModeRef = useRef3(mode);
2367
+ useEffect7(() => {
407
2368
  if (prevModeRef.current === "edit-raw" && mode !== "edit-raw") {
408
2369
  setCapturedMarkdown(latestValueRef.current);
409
2370
  setMountKey((k) => k + 1);
@@ -420,7 +2381,7 @@ function MarkdownEditor({
420
2381
  []
421
2382
  );
422
2383
  if (mode === "edit-raw") {
423
- return /* @__PURE__ */ jsx("div", { className: `zui-text-editor ${className ?? ""}`, children: /* @__PURE__ */ jsx(
2384
+ return /* @__PURE__ */ jsx4("div", { className: `zui-text-editor ${className ?? ""}`, children: /* @__PURE__ */ jsx4(
424
2385
  "textarea",
425
2386
  {
426
2387
  className: "zui-text-editor-textarea",
@@ -433,22 +2394,23 @@ function MarkdownEditor({
433
2394
  }
434
2395
  ) });
435
2396
  }
436
- return /* @__PURE__ */ jsx(LexicalComposer, { initialConfig, children: /* @__PURE__ */ jsxs("div", { className: `zui-text-editor ${className ?? ""}`, children: [
437
- /* @__PURE__ */ jsx(
2397
+ return /* @__PURE__ */ jsx4(LexicalComposer, { initialConfig, children: /* @__PURE__ */ jsxs3("div", { className: `zui-text-editor ${className ?? ""}`, children: [
2398
+ toolbar && mode === "edit-md" && !readOnly && /* @__PURE__ */ jsx4(ToolbarPlugin, {}),
2399
+ /* @__PURE__ */ jsx4(
438
2400
  RichTextPlugin,
439
2401
  {
440
- contentEditable: /* @__PURE__ */ jsx(
2402
+ contentEditable: /* @__PURE__ */ jsx4(
441
2403
  ContentEditable,
442
2404
  {
443
2405
  className: "zui-text-editor-content",
444
2406
  "aria-placeholder": placeholder,
445
- placeholder: /* @__PURE__ */ jsx("div", { className: "zui-text-editor-placeholder", children: placeholder })
2407
+ placeholder: /* @__PURE__ */ jsx4("div", { className: "zui-text-editor-placeholder", children: placeholder })
446
2408
  }
447
2409
  ),
448
2410
  ErrorBoundary: LexicalErrorBoundary
449
2411
  }
450
2412
  ),
451
- /* @__PURE__ */ jsx(
2413
+ /* @__PURE__ */ jsx4(
452
2414
  MarkdownSyncPlugin,
453
2415
  {
454
2416
  initialMarkdown: mode === "view" ? value ?? "" : capturedMarkdown,
@@ -456,22 +2418,28 @@ function MarkdownEditor({
456
2418
  transformers: SYNC_TRANSFORMERS
457
2419
  }
458
2420
  ),
459
- /* @__PURE__ */ jsx(HistoryPlugin, {}),
460
- /* @__PURE__ */ jsx(ListPlugin, {}),
461
- /* @__PURE__ */ jsx(CheckListPlugin, {}),
462
- /* @__PURE__ */ jsx(ChecklistShortcutPlugin, {}),
463
- /* @__PURE__ */ jsx(CodeBlockShortcutPlugin, {}),
464
- /* @__PURE__ */ jsx(CodeHighlightPlugin, {}),
465
- /* @__PURE__ */ jsx(LinkPlugin, {}),
466
- /* @__PURE__ */ jsx(MarkdownShortcutPlugin, { transformers: SHORTCUT_TRANSFORMERS }),
467
- /* @__PURE__ */ jsx(ReadOnlyPlugin, { readOnly: readOnly || mode === "view" }),
468
- autoFocus && /* @__PURE__ */ jsx(AutoFocusPlugin, {})
2421
+ /* @__PURE__ */ jsx4(HistoryPlugin, {}),
2422
+ /* @__PURE__ */ jsx4(ListPlugin, {}),
2423
+ /* @__PURE__ */ jsx4(CheckListPlugin, {}),
2424
+ /* @__PURE__ */ jsx4(ChecklistShortcutPlugin, {}),
2425
+ /* @__PURE__ */ jsx4(CodeBlockShortcutPlugin, {}),
2426
+ /* @__PURE__ */ jsx4(CodeHighlightPlugin, {}),
2427
+ /* @__PURE__ */ jsx4(TablePlugin, {}),
2428
+ /* @__PURE__ */ jsx4(LinkPlugin, {}),
2429
+ /* @__PURE__ */ jsx4(MarkdownShortcutPlugin, { transformers: SHORTCUT_TRANSFORMERS }),
2430
+ /* @__PURE__ */ jsx4(ReadOnlyPlugin, { readOnly: readOnly || mode === "view" }),
2431
+ autoFocus && /* @__PURE__ */ jsx4(AutoFocusPlugin, {})
469
2432
  ] }) }, mountKey);
470
2433
  }
471
2434
  export {
2435
+ $createDrawingNode,
472
2436
  $createFrontmatterNode,
2437
+ $isDrawingNode,
473
2438
  $isFrontmatterNode,
2439
+ DRAWING,
2440
+ DrawingNode,
474
2441
  FRONTMATTER,
475
2442
  FrontmatterNode,
476
- MarkdownEditor
2443
+ MarkdownEditor,
2444
+ TABLE
477
2445
  };