@theodo-group/epure 0.1.0 → 0.2.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.
@@ -0,0 +1,1125 @@
1
+ import {
2
+ iconUrlById
3
+ } from "./chunk-KBK6AYWD.js";
4
+
5
+ // src/renderer/Area.tsx
6
+ import { useCallback, useRef } from "react";
7
+
8
+ // src/style/palette.ts
9
+ var PALETTE = {
10
+ // Black/gray stay neutral. Chromatic hues use Tailwind 500 — the most
11
+ // saturated step before colors start losing chroma to darken — for punchy
12
+ // strokes, paired with pale 50 fills so colored bodies remain readable.
13
+ black: { solid: "#0c0a09", fill: "#fafaf9" },
14
+ gray: { solid: "#78716c", fill: "#e7e5e4" },
15
+ red: { solid: "#ef4444", fill: "#fef2f2" },
16
+ orange: { solid: "#f97316", fill: "#fff7ed" },
17
+ yellow: { solid: "#eab308", fill: "#fefce8" },
18
+ green: { solid: "#22c55e", fill: "#f0fdf4" },
19
+ teal: { solid: "#14b8a6", fill: "#f0fdfa" },
20
+ blue: { solid: "#3b82f6", fill: "#eff6ff" },
21
+ purple: { solid: "#a855f7", fill: "#faf5ff" },
22
+ pink: { solid: "#ec4899", fill: "#fdf2f8" }
23
+ };
24
+ var resolveFill = (fc) => {
25
+ if (fc === void 0) return null;
26
+ if (fc === "transparent") return "transparent";
27
+ if (fc === "white") return "#ffffff";
28
+ return fillOf(fc);
29
+ };
30
+ var TEXT_SIZE = {
31
+ S: 10,
32
+ M: 12,
33
+ L: 14,
34
+ XL: 18
35
+ };
36
+ var STROKE_WIDTH = {
37
+ S: 1,
38
+ M: 1.5,
39
+ L: 2.5,
40
+ XL: 4
41
+ };
42
+ var dashArrayFor = (style, width = 1.5) => {
43
+ if (style === "solid") return void 0;
44
+ if (style === "dashed") return `${width * 4} ${width * 3}`;
45
+ return `${width} ${width * 2}`;
46
+ };
47
+ var solidOf = (color) => PALETTE[color ?? "black"].solid;
48
+ var fillOf = (color) => PALETTE[color ?? "gray"].fill;
49
+
50
+ // src/renderer/dragState.ts
51
+ var depth = 0;
52
+ var beginDrag = () => {
53
+ if (depth++ === 0) document.body.classList.add("epure-dragging");
54
+ };
55
+ var endDrag = () => {
56
+ if (depth > 0 && --depth === 0) {
57
+ document.body.classList.remove("epure-dragging");
58
+ }
59
+ };
60
+
61
+ // src/renderer/Area.tsx
62
+ import { jsx, jsxs } from "react/jsx-runtime";
63
+ var Area = ({
64
+ area,
65
+ selected,
66
+ onSelect,
67
+ onDragStart,
68
+ onDragMove
69
+ }) => {
70
+ const draggingRef = useRef(false);
71
+ const startRef = useRef({ mx: 0, my: 0 });
72
+ const handleMouseDown = useCallback(
73
+ (event) => {
74
+ event.stopPropagation();
75
+ onSelect?.(area.id, event.shiftKey);
76
+ if (!onDragMove) return;
77
+ const svg = event.target.ownerSVGElement;
78
+ if (!svg) return;
79
+ draggingRef.current = true;
80
+ beginDrag();
81
+ const pt = svg.createSVGPoint();
82
+ pt.x = event.clientX;
83
+ pt.y = event.clientY;
84
+ const inverse = svg.getScreenCTM()?.inverse();
85
+ if (!inverse) return;
86
+ const sp = pt.matrixTransform(inverse);
87
+ startRef.current = { mx: sp.x, my: sp.y };
88
+ onDragStart?.(area.id);
89
+ const onMove = (e) => {
90
+ if (!draggingRef.current) return;
91
+ const mp = svg.createSVGPoint();
92
+ mp.x = e.clientX;
93
+ mp.y = e.clientY;
94
+ const inv = svg.getScreenCTM()?.inverse();
95
+ if (!inv) return;
96
+ const cur = mp.matrixTransform(inv);
97
+ onDragMove(area.id, cur.x - startRef.current.mx, cur.y - startRef.current.my);
98
+ };
99
+ const onUp = () => {
100
+ draggingRef.current = false;
101
+ endDrag();
102
+ window.removeEventListener("mousemove", onMove);
103
+ window.removeEventListener("mouseup", onUp);
104
+ };
105
+ window.addEventListener("mousemove", onMove);
106
+ window.addEventListener("mouseup", onUp);
107
+ },
108
+ [area.id, onSelect, onDragStart, onDragMove]
109
+ );
110
+ return /* @__PURE__ */ jsxs(
111
+ "g",
112
+ {
113
+ "data-area-id": area.id,
114
+ onMouseDown: handleMouseDown,
115
+ style: { cursor: onDragMove ? "grab" : "default" },
116
+ children: [
117
+ /* @__PURE__ */ jsx(
118
+ "rect",
119
+ {
120
+ x: area.x,
121
+ y: area.y,
122
+ width: area.w,
123
+ height: area.h,
124
+ rx: 12,
125
+ ry: 12,
126
+ fill: resolveFill(area.fillColor) ?? "#f4f5f9",
127
+ stroke: area.borderColor ? solidOf(area.borderColor) : "#cdd2dd",
128
+ strokeWidth: 1,
129
+ strokeDasharray: dashArrayFor(area.borderStyle ?? "dashed", 1)
130
+ }
131
+ ),
132
+ selected ? /* @__PURE__ */ jsx(
133
+ "rect",
134
+ {
135
+ x: area.x - 4,
136
+ y: area.y - 4,
137
+ width: area.w + 8,
138
+ height: area.h + 8,
139
+ rx: 16,
140
+ ry: 16,
141
+ fill: "none",
142
+ stroke: "#3b82f6",
143
+ strokeWidth: 1.5,
144
+ strokeDasharray: "4 3",
145
+ pointerEvents: "none"
146
+ }
147
+ ) : null
148
+ ]
149
+ }
150
+ );
151
+ };
152
+ var LABEL_CHAR_PX = 7;
153
+ var LABEL_PAD_X = 10;
154
+ var LABEL_HEIGHT = 22;
155
+ var LABEL_FONT = 12;
156
+ var AreaLabel = ({
157
+ area,
158
+ textScale = 1,
159
+ fontFamily = "Inter, system-ui, sans-serif"
160
+ }) => {
161
+ if (!area.label) return null;
162
+ const accent = area.borderColor ? solidOf(area.borderColor) : "#5b6478";
163
+ const chipH = LABEL_HEIGHT * textScale;
164
+ const charW = LABEL_CHAR_PX * textScale;
165
+ const padX = LABEL_PAD_X * textScale;
166
+ const chipW = Math.max(40 * textScale, area.label.length * charW + padX * 2);
167
+ const chipX = area.x + 14;
168
+ const chipY = area.y - chipH / 2;
169
+ return /* @__PURE__ */ jsxs("g", { pointerEvents: "none", children: [
170
+ /* @__PURE__ */ jsx(
171
+ "rect",
172
+ {
173
+ x: chipX,
174
+ y: chipY,
175
+ width: chipW,
176
+ height: chipH,
177
+ rx: chipH / 2,
178
+ ry: chipH / 2,
179
+ fill: "#ffffff",
180
+ stroke: accent,
181
+ strokeWidth: 1
182
+ }
183
+ ),
184
+ /* @__PURE__ */ jsx(
185
+ "text",
186
+ {
187
+ x: chipX + chipW / 2,
188
+ y: chipY + chipH / 2 + 0.5,
189
+ textAnchor: "middle",
190
+ dominantBaseline: "middle",
191
+ fontFamily,
192
+ fontSize: LABEL_FONT * textScale,
193
+ fontWeight: 600,
194
+ fill: accent,
195
+ children: area.label
196
+ }
197
+ )
198
+ ] });
199
+ };
200
+
201
+ // src/renderer/Edge.tsx
202
+ import { useCallback as useCallback2, useRef as useRef2 } from "react";
203
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
204
+ var safeId = (id) => id.replace(/[^A-Za-z0-9_-]/g, (c) => `_${c.charCodeAt(0)}_`);
205
+ var EdgeDefs = () => /* @__PURE__ */ jsxs2("defs", { children: [
206
+ /* @__PURE__ */ jsx2("filter", { id: "ep-badge-shadow", x: "-60%", y: "-60%", width: "220%", height: "220%", children: /* @__PURE__ */ jsx2("feGaussianBlur", { stdDeviation: "1.6" }) }),
207
+ /* @__PURE__ */ jsx2("filter", { id: "ep-node-shadow", x: "-20%", y: "-20%", width: "140%", height: "160%", children: /* @__PURE__ */ jsx2(
208
+ "feDropShadow",
209
+ {
210
+ dx: "0",
211
+ dy: "2",
212
+ stdDeviation: "4",
213
+ floodColor: "#0f172a",
214
+ floodOpacity: "0.14"
215
+ }
216
+ ) }),
217
+ /* @__PURE__ */ jsx2("filter", { id: "ep-icon-halo", x: "-50%", y: "-50%", width: "200%", height: "200%", children: /* @__PURE__ */ jsx2("feGaussianBlur", { stdDeviation: "5" }) }),
218
+ /* @__PURE__ */ jsxs2("radialGradient", { id: "ep-edge-fade", children: [
219
+ /* @__PURE__ */ jsx2("stop", { offset: "0", stopColor: "#000" }),
220
+ /* @__PURE__ */ jsx2("stop", { offset: "0.55", stopColor: "#000" }),
221
+ /* @__PURE__ */ jsx2("stop", { offset: "1", stopColor: "#fff" })
222
+ ] })
223
+ ] });
224
+ var pointsToPath = (points) => points.length === 0 ? "" : points.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x} ${p.y}`).join(" ");
225
+ var labelPillSize = (label, textScale = 1) => ({
226
+ w: Math.max(20 * textScale, label.length * 6 * textScale + 12 * textScale),
227
+ h: 16 * textScale
228
+ });
229
+ var directionAt = (points, end) => {
230
+ if (points.length < 2) return { dx: 1, dy: 0 };
231
+ if (end === "end") {
232
+ const b2 = points[points.length - 1];
233
+ const a2 = points[points.length - 2];
234
+ const dx2 = b2.x - a2.x;
235
+ const dy2 = b2.y - a2.y;
236
+ const m2 = Math.hypot(dx2, dy2) || 1;
237
+ return { dx: dx2 / m2, dy: dy2 / m2 };
238
+ }
239
+ const a = points[0];
240
+ const b = points[1];
241
+ const dx = a.x - b.x;
242
+ const dy = a.y - b.y;
243
+ const m = Math.hypot(dx, dy) || 1;
244
+ return { dx: dx / m, dy: dy / m };
245
+ };
246
+ var renderCap = (cap, at, dir, color, width) => {
247
+ if (cap === "none") return null;
248
+ const size = Math.max(3, width * 4);
249
+ if (cap === "dot") {
250
+ return /* @__PURE__ */ jsx2("circle", { cx: at.x, cy: at.y, r: size / 2, fill: color });
251
+ }
252
+ if (cap === "diamond") {
253
+ const sx2 = -dir.dx;
254
+ const sy2 = -dir.dy;
255
+ const px2 = -sy2;
256
+ const py2 = sx2;
257
+ const tip = { x: at.x, y: at.y };
258
+ const back = { x: at.x + sx2 * size, y: at.y + sy2 * size };
259
+ const mid = { x: at.x + sx2 * size / 2, y: at.y + sy2 * size / 2 };
260
+ const left2 = { x: mid.x + px2 * size / 2, y: mid.y + py2 * size / 2 };
261
+ const right2 = { x: mid.x - px2 * size / 2, y: mid.y - py2 * size / 2 };
262
+ return /* @__PURE__ */ jsx2(
263
+ "path",
264
+ {
265
+ d: `M ${tip.x} ${tip.y} L ${left2.x} ${left2.y} L ${back.x} ${back.y} L ${right2.x} ${right2.y} Z`,
266
+ fill: color
267
+ }
268
+ );
269
+ }
270
+ const sx = -dir.dx;
271
+ const sy = -dir.dy;
272
+ const px = -sy;
273
+ const py = sx;
274
+ const base = { x: at.x + sx * size, y: at.y + sy * size };
275
+ const left = { x: base.x + px * size / 2, y: base.y + py * size / 2 };
276
+ const right = { x: base.x - px * size / 2, y: base.y - py * size / 2 };
277
+ return /* @__PURE__ */ jsx2(
278
+ "path",
279
+ {
280
+ d: `M ${at.x} ${at.y} L ${left.x} ${left.y} L ${right.x} ${right.y} Z`,
281
+ fill: color
282
+ }
283
+ );
284
+ };
285
+ var STYLE_FROM_PARSER = {
286
+ solid: "solid",
287
+ dashed: "dashed",
288
+ dotted: "dotted"
289
+ };
290
+ var Edge = ({
291
+ edge,
292
+ label,
293
+ style: parserStyle = "solid",
294
+ marker = "forward",
295
+ selected = false,
296
+ onSelect,
297
+ textScale = 1,
298
+ fontFamily = "Inter, system-ui, sans-serif",
299
+ gridSize = 40,
300
+ onMoveLabel,
301
+ crossings
302
+ }) => {
303
+ const labelDragging = useRef2(false);
304
+ const labelStart = useRef2({ mx: 0, my: 0, dx: 0, dy: 0 });
305
+ const handleLabelDown = useCallback2(
306
+ (event) => {
307
+ if (!onMoveLabel) return;
308
+ event.stopPropagation();
309
+ onSelect?.(edge.id, false);
310
+ const svg = event.target.ownerSVGElement;
311
+ const inv = svg?.getScreenCTM()?.inverse();
312
+ if (!svg || !inv) return;
313
+ const grid = gridSize || 40;
314
+ labelDragging.current = true;
315
+ beginDrag();
316
+ const pt = svg.createSVGPoint();
317
+ pt.x = event.clientX;
318
+ pt.y = event.clientY;
319
+ const sp = pt.matrixTransform(inv);
320
+ labelStart.current = {
321
+ mx: sp.x,
322
+ my: sp.y,
323
+ dx: edge.labelDx ?? 0,
324
+ dy: edge.labelDy ?? 0
325
+ };
326
+ const onPointerMove = (e) => {
327
+ if (!labelDragging.current) return;
328
+ const curInv = svg.getScreenCTM()?.inverse();
329
+ if (!curInv) return;
330
+ const mp = svg.createSVGPoint();
331
+ mp.x = e.clientX;
332
+ mp.y = e.clientY;
333
+ const cur = mp.matrixTransform(curInv);
334
+ const ndx = labelStart.current.dx + Math.round((cur.x - labelStart.current.mx) / grid);
335
+ const ndy = labelStart.current.dy + Math.round((cur.y - labelStart.current.my) / grid);
336
+ onMoveLabel(edge.id, ndx, ndy);
337
+ };
338
+ const onPointerUp = () => {
339
+ labelDragging.current = false;
340
+ endDrag();
341
+ window.removeEventListener("mousemove", onPointerMove);
342
+ window.removeEventListener("mouseup", onPointerUp);
343
+ };
344
+ window.addEventListener("mousemove", onPointerMove);
345
+ window.addEventListener("mouseup", onPointerUp);
346
+ },
347
+ [edge.id, edge.labelDx, edge.labelDy, gridSize, onMoveLabel, onSelect]
348
+ );
349
+ const color = solidOf(edge.color);
350
+ const width = STROKE_WIDTH[edge.width ?? "M"];
351
+ const lineStyle = edge.lineStyle ?? STYLE_FROM_PARSER[parserStyle];
352
+ const dash = dashArrayFor(lineStyle, width);
353
+ const defaultStart = marker === "backward" || marker === "bidirectional" ? "arrow" : "none";
354
+ const defaultEnd = marker === "forward" || marker === "bidirectional" ? "arrow" : "none";
355
+ const startCap = edge.startCap ?? defaultStart;
356
+ const endCap = edge.endCap ?? defaultEnd;
357
+ const startDir = directionAt(edge.points, "start");
358
+ const endDir = directionAt(edge.points, "end");
359
+ const startPt = edge.points[0] ?? { x: 0, y: 0 };
360
+ const endPt = edge.points[edge.points.length - 1] ?? { x: 0, y: 0 };
361
+ const path = pointsToPath(edge.points);
362
+ const fades = crossings ?? [];
363
+ const maskId = fades.length > 0 ? `ep-fade-${safeId(edge.id)}` : void 0;
364
+ let maskRegion;
365
+ if (maskId && edge.points.length > 0) {
366
+ let minX = Infinity;
367
+ let minY = Infinity;
368
+ let maxX = -Infinity;
369
+ let maxY = -Infinity;
370
+ for (const p of edge.points) {
371
+ minX = Math.min(minX, p.x);
372
+ minY = Math.min(minY, p.y);
373
+ maxX = Math.max(maxX, p.x);
374
+ maxY = Math.max(maxY, p.y);
375
+ }
376
+ const pad = Math.max(...fades.map((f) => f.r)) + width + 8;
377
+ maskRegion = {
378
+ x: minX - pad,
379
+ y: minY - pad,
380
+ w: maxX - minX + pad * 2,
381
+ h: maxY - minY + pad * 2
382
+ };
383
+ }
384
+ const maskRef = maskId ? `url(#${maskId})` : void 0;
385
+ return /* @__PURE__ */ jsxs2("g", { "data-edge-id": edge.id, children: [
386
+ maskId && maskRegion ? /* @__PURE__ */ jsxs2(
387
+ "mask",
388
+ {
389
+ id: maskId,
390
+ maskUnits: "userSpaceOnUse",
391
+ x: maskRegion.x,
392
+ y: maskRegion.y,
393
+ width: maskRegion.w,
394
+ height: maskRegion.h,
395
+ children: [
396
+ /* @__PURE__ */ jsx2(
397
+ "rect",
398
+ {
399
+ x: maskRegion.x,
400
+ y: maskRegion.y,
401
+ width: maskRegion.w,
402
+ height: maskRegion.h,
403
+ fill: "#fff"
404
+ }
405
+ ),
406
+ fades.map((f, i) => /* @__PURE__ */ jsx2("circle", { cx: f.x, cy: f.y, r: f.r, fill: "url(#ep-edge-fade)" }, i))
407
+ ]
408
+ }
409
+ ) : null,
410
+ selected ? /* @__PURE__ */ jsx2(
411
+ "path",
412
+ {
413
+ d: path,
414
+ fill: "none",
415
+ stroke: "#3b82f6",
416
+ strokeOpacity: 0.28,
417
+ strokeWidth: width + 6,
418
+ strokeLinecap: "round",
419
+ strokeLinejoin: "round",
420
+ pointerEvents: "none",
421
+ mask: maskRef
422
+ }
423
+ ) : null,
424
+ /* @__PURE__ */ jsx2(
425
+ "path",
426
+ {
427
+ d: path,
428
+ fill: "none",
429
+ stroke: color,
430
+ strokeWidth: width,
431
+ strokeDasharray: dash,
432
+ strokeLinecap: "round",
433
+ strokeLinejoin: "round",
434
+ pointerEvents: "none",
435
+ mask: maskRef
436
+ }
437
+ ),
438
+ renderCap(startCap, startPt, startDir, color, width),
439
+ renderCap(endCap, endPt, endDir, color, width),
440
+ onSelect ? /* @__PURE__ */ jsx2(
441
+ "path",
442
+ {
443
+ d: path,
444
+ fill: "none",
445
+ stroke: "transparent",
446
+ strokeWidth: Math.max(16, width * 3),
447
+ strokeLinecap: "round",
448
+ strokeLinejoin: "round",
449
+ pointerEvents: "stroke",
450
+ style: { cursor: "pointer" },
451
+ onMouseDown: (event) => {
452
+ event.stopPropagation();
453
+ onSelect(edge.id, event.shiftKey);
454
+ }
455
+ }
456
+ ) : null,
457
+ label && edge.labelAnchor ? (() => {
458
+ const fontSize = 11 * textScale;
459
+ const { w: pillW, h: pillH } = labelPillSize(label, textScale);
460
+ const draggable = !!onMoveLabel;
461
+ return /* @__PURE__ */ jsxs2(
462
+ "g",
463
+ {
464
+ transform: `translate(${edge.labelAnchor.x}, ${edge.labelAnchor.y})`,
465
+ onMouseDown: draggable ? handleLabelDown : void 0,
466
+ style: draggable ? { cursor: "move" } : void 0,
467
+ children: [
468
+ /* @__PURE__ */ jsx2(
469
+ "rect",
470
+ {
471
+ x: -pillW / 2,
472
+ y: -pillH / 2,
473
+ width: pillW,
474
+ height: pillH,
475
+ rx: 4,
476
+ ry: 4,
477
+ fill: "#ffffff",
478
+ pointerEvents: draggable ? "all" : "none"
479
+ }
480
+ ),
481
+ /* @__PURE__ */ jsx2(
482
+ "text",
483
+ {
484
+ textAnchor: "middle",
485
+ dominantBaseline: "middle",
486
+ fontFamily,
487
+ fontSize,
488
+ fill: "#1f2430",
489
+ pointerEvents: "none",
490
+ children: label
491
+ }
492
+ )
493
+ ]
494
+ }
495
+ );
496
+ })() : null
497
+ ] });
498
+ };
499
+
500
+ // src/renderer/Node.tsx
501
+ import { useCallback as useCallback3, useRef as useRef3 } from "react";
502
+
503
+ // src/renderer/richText.ts
504
+ var LABEL_TAG_RE = /^<\s*(\/?)(b|strong|i|em|small|br|ul|li)\s*\/?\s*>/i;
505
+ var tagKey = (raw) => {
506
+ const t = raw.toLowerCase();
507
+ if (t === "strong") return "b";
508
+ if (t === "em") return "i";
509
+ return t;
510
+ };
511
+ var parseRichText = (label) => {
512
+ const lines = [{ words: [] }];
513
+ let bold = 0;
514
+ let italic = 0;
515
+ let small = 0;
516
+ let word = "";
517
+ let afterList = false;
518
+ const cur = () => lines[lines.length - 1];
519
+ const flushWord = () => {
520
+ if (!word) return;
521
+ cur().words.push({
522
+ text: word,
523
+ ...bold > 0 ? { bold: true } : {},
524
+ ...italic > 0 ? { italic: true } : {},
525
+ ...small > 0 ? { small: true } : {}
526
+ });
527
+ word = "";
528
+ };
529
+ const startLine = (bullet) => {
530
+ flushWord();
531
+ const c = cur();
532
+ if (c.words.length === 0 && !c.bullet) {
533
+ if (bullet) c.bullet = true;
534
+ return;
535
+ }
536
+ lines.push(bullet ? { words: [], bullet: true } : { words: [] });
537
+ };
538
+ let i = 0;
539
+ while (i < label.length) {
540
+ const ch = label[i];
541
+ if (ch === "\n") {
542
+ flushWord();
543
+ afterList = false;
544
+ lines.push({ words: [] });
545
+ i++;
546
+ continue;
547
+ }
548
+ if (/\s/.test(ch)) {
549
+ flushWord();
550
+ i++;
551
+ continue;
552
+ }
553
+ if (ch === "<") {
554
+ const m = label.slice(i).match(LABEL_TAG_RE);
555
+ if (m) {
556
+ const closing = m[1] === "/";
557
+ const key = tagKey(m[2]);
558
+ if (key === "br") {
559
+ flushWord();
560
+ afterList = false;
561
+ lines.push({ words: [] });
562
+ } else if (key === "b") {
563
+ flushWord();
564
+ bold = Math.max(0, bold + (closing ? -1 : 1));
565
+ } else if (key === "i") {
566
+ flushWord();
567
+ italic = Math.max(0, italic + (closing ? -1 : 1));
568
+ } else if (key === "small") {
569
+ flushWord();
570
+ small = Math.max(0, small + (closing ? -1 : 1));
571
+ } else if (key === "li") {
572
+ if (!closing) {
573
+ afterList = false;
574
+ startLine(true);
575
+ } else flushWord();
576
+ } else if (key === "ul") {
577
+ flushWord();
578
+ if (closing) afterList = true;
579
+ }
580
+ i += m[0].length;
581
+ continue;
582
+ }
583
+ }
584
+ if (afterList) {
585
+ startLine(false);
586
+ afterList = false;
587
+ }
588
+ word += ch;
589
+ i++;
590
+ }
591
+ flushWord();
592
+ return lines;
593
+ };
594
+ var hasRichMarkup = (label) => /<\s*\/?\s*(b|strong|i|em|small|br|ul|li)\s*\/?\s*>/i.test(label) || label.includes("\n");
595
+ var wrapRichText = (lines, maxChars) => {
596
+ const out = [];
597
+ for (const line of lines) {
598
+ if (line.words.length === 0) {
599
+ out.push(line.bullet ? { words: [], bullet: true } : { words: [] });
600
+ continue;
601
+ }
602
+ let cur = [];
603
+ let curLen = 0;
604
+ let first = true;
605
+ const pushSeg = () => {
606
+ out.push(
607
+ first && line.bullet ? { words: cur, bullet: true } : { words: cur }
608
+ );
609
+ first = false;
610
+ };
611
+ for (const w of line.words) {
612
+ const wLen = w.text.length;
613
+ const sep = curLen === 0 ? 0 : 1;
614
+ if (curLen === 0 || curLen + sep + wLen <= maxChars) {
615
+ cur.push(w);
616
+ curLen += sep + wLen;
617
+ } else {
618
+ pushSeg();
619
+ cur = [w];
620
+ curLen = wLen;
621
+ }
622
+ }
623
+ pushSeg();
624
+ }
625
+ return out;
626
+ };
627
+
628
+ // src/renderer/shapes/rectangle.tsx
629
+ import { jsx as jsx3 } from "react/jsx-runtime";
630
+ var Rectangle = ({
631
+ x,
632
+ y,
633
+ w,
634
+ h,
635
+ fill = "#ffffff",
636
+ stroke = "#3b4252",
637
+ strokeWidth = 1.5,
638
+ strokeDasharray
639
+ }) => /* @__PURE__ */ jsx3(
640
+ "rect",
641
+ {
642
+ x,
643
+ y,
644
+ width: w,
645
+ height: h,
646
+ rx: 10,
647
+ ry: 10,
648
+ fill,
649
+ stroke,
650
+ strokeWidth,
651
+ strokeDasharray
652
+ }
653
+ );
654
+
655
+ // src/renderer/shapes/cylinder.tsx
656
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
657
+ var Cylinder = ({
658
+ x,
659
+ y,
660
+ w,
661
+ h,
662
+ fill = "#ffffff",
663
+ stroke = "#3b4252",
664
+ strokeWidth = 1.5,
665
+ strokeDasharray
666
+ }) => {
667
+ const rx = w / 2;
668
+ const ry = Math.min(h * 0.12, 12);
669
+ const topCy = y + ry;
670
+ const bodyH = h - ry * 2;
671
+ const path = [
672
+ `M ${x} ${topCy}`,
673
+ `a ${rx} ${ry} 0 0 0 ${w} 0`,
674
+ `v ${bodyH}`,
675
+ `a ${rx} ${ry} 0 0 1 ${-w} 0`,
676
+ "Z"
677
+ ].join(" ");
678
+ return /* @__PURE__ */ jsxs3("g", { children: [
679
+ /* @__PURE__ */ jsx4(
680
+ "path",
681
+ {
682
+ d: path,
683
+ fill,
684
+ stroke,
685
+ strokeWidth,
686
+ strokeDasharray
687
+ }
688
+ ),
689
+ /* @__PURE__ */ jsx4(
690
+ "ellipse",
691
+ {
692
+ cx: x + rx,
693
+ cy: topCy,
694
+ rx,
695
+ ry,
696
+ fill,
697
+ stroke,
698
+ strokeWidth,
699
+ strokeDasharray
700
+ }
701
+ )
702
+ ] });
703
+ };
704
+
705
+ // src/renderer/shapes/person.tsx
706
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
707
+ var Person = ({
708
+ x,
709
+ y,
710
+ w,
711
+ h,
712
+ fill = "#ffffff",
713
+ stroke = "#475569",
714
+ strokeWidth = 1.5,
715
+ strokeDasharray
716
+ }) => {
717
+ const size = Math.min(w, h);
718
+ const fx = x + (w - size) / 2;
719
+ const fy = y + (h - size) / 2;
720
+ const cx = fx + size / 2;
721
+ const headR = size * 0.16;
722
+ const headCy = fy + headR + 4;
723
+ const bodyTop = headCy + headR + 2;
724
+ const bodyH = fy + size - bodyTop - 2;
725
+ const bodyW = size * 0.7;
726
+ const bodyX = cx - bodyW / 2;
727
+ return /* @__PURE__ */ jsxs4("g", { children: [
728
+ /* @__PURE__ */ jsx5(
729
+ "circle",
730
+ {
731
+ cx,
732
+ cy: headCy,
733
+ r: headR,
734
+ fill,
735
+ stroke,
736
+ strokeWidth,
737
+ strokeDasharray
738
+ }
739
+ ),
740
+ /* @__PURE__ */ jsx5(
741
+ "path",
742
+ {
743
+ d: [
744
+ `M ${bodyX} ${bodyTop + bodyH}`,
745
+ `Q ${bodyX} ${bodyTop} ${cx} ${bodyTop}`,
746
+ `Q ${bodyX + bodyW} ${bodyTop} ${bodyX + bodyW} ${bodyTop + bodyH}`,
747
+ "Z"
748
+ ].join(" "),
749
+ fill,
750
+ stroke,
751
+ strokeWidth,
752
+ strokeDasharray
753
+ }
754
+ )
755
+ ] });
756
+ };
757
+
758
+ // src/renderer/Node.tsx
759
+ import { Fragment, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
760
+ var SHAPE_COMPONENTS = {
761
+ rectangle: Rectangle,
762
+ cylinder: Cylinder,
763
+ person: Person
764
+ };
765
+ var AVG_CHAR_PX = 6.6;
766
+ var RESIZE_HANDLE = 8;
767
+ var LINE_HEIGHT = 14;
768
+ var LABEL_BELOW_GAP = 8;
769
+ var wrapPlain = (label, w) => {
770
+ const maxChars = Math.max(4, Math.floor((w - 16) / AVG_CHAR_PX));
771
+ return wrapRichText([{ words: label.split(/\s+/).filter(Boolean).map((t) => ({ text: t })) }], maxChars);
772
+ };
773
+ var wrapRich = (label, w) => {
774
+ const maxChars = Math.max(4, Math.floor((w - 16) / AVG_CHAR_PX));
775
+ return wrapRichText(parseRichText(label), maxChars);
776
+ };
777
+ var isLineEmpty = (line) => line.words.length === 0 || line.words.every((wd) => !wd.text);
778
+ var Node = ({
779
+ id,
780
+ shape,
781
+ label,
782
+ x,
783
+ y,
784
+ w,
785
+ h,
786
+ selected,
787
+ textSize,
788
+ textColor,
789
+ borderColor,
790
+ borderStyle,
791
+ fillColor,
792
+ icon,
793
+ iconPosition,
794
+ onSelect,
795
+ onMove,
796
+ onResize,
797
+ onStartEdit,
798
+ editing,
799
+ gridSize,
800
+ textScale = 1,
801
+ fontFamily = "Inter, system-ui, sans-serif"
802
+ }) => {
803
+ const Shape = SHAPE_COMPONENTS[shape] ?? Rectangle;
804
+ const strokeColor = borderColor ? solidOf(borderColor) : "#475569";
805
+ const shapeFill = resolveFill(fillColor) ?? "#ffffff";
806
+ const dash = borderStyle ? dashArrayFor(borderStyle, 1.5) : void 0;
807
+ const fontSize = TEXT_SIZE[textSize ?? "M"] * textScale;
808
+ const lineHeight = LINE_HEIGHT * textScale;
809
+ const labelFill = textColor ? solidOf(textColor) : "#1f2430";
810
+ const labelBelow = shape === "person";
811
+ const iconUrl = icon && shape !== "person" ? iconUrlById(icon) : void 0;
812
+ const effectiveIconPos = iconPosition ?? "corner";
813
+ const badge = iconUrl && effectiveIconPos === "corner" && Math.min(w, h) >= 22 ? (() => {
814
+ const size = Math.min(Math.min(w, h) * 0.54, 66);
815
+ const cylinderLift = shape === "cylinder" ? Math.min(h * 0.12, 12) : 0;
816
+ return {
817
+ cx: x + w - size / 3,
818
+ cy: y + h - size / 3 - cylinderLift,
819
+ size
820
+ };
821
+ })() : null;
822
+ const topIcon = iconUrl && effectiveIconPos === "top" && Math.min(w, h) >= 28 ? (() => {
823
+ const size = Math.max(20, Math.min(Math.min(w * 0.45, h * 0.42), 56));
824
+ const topPad = Math.max(8, Math.min(14, h * 0.14));
825
+ return {
826
+ cx: x + w / 2,
827
+ cy: y + topPad + size / 2,
828
+ size,
829
+ bottom: y + topPad + size
830
+ };
831
+ })() : null;
832
+ const textWidth = (badge ? w - 14 : w) / textScale;
833
+ const labelBelowWidth = w * 2 / textScale;
834
+ const rich = label ? hasRichMarkup(label) ? wrapRich(label, labelBelow ? labelBelowWidth : textWidth) : wrapPlain(label, labelBelow ? labelBelowWidth : textWidth) : [];
835
+ const blockH = rich.length * lineHeight;
836
+ let startY;
837
+ if (labelBelow) {
838
+ startY = y + h + lineHeight + LABEL_BELOW_GAP * textScale;
839
+ } else if (topIcon) {
840
+ const availTop = topIcon.bottom + 4;
841
+ const availBottom = y + h - 2;
842
+ const center = (availTop + availBottom) / 2;
843
+ startY = Math.max(availTop + lineHeight * 0.8, center - blockH / 2 + lineHeight * 0.8);
844
+ } else {
845
+ const textCenterY = y + h / 2;
846
+ startY = Math.max(y + lineHeight * 0.8, textCenterY - blockH / 2 + lineHeight * 0.8);
847
+ }
848
+ const dragging = useRef3(false);
849
+ const dragStart = useRef3({ mx: 0, my: 0, cx: 0, cy: 0 });
850
+ const handlePointerDown = useCallback3(
851
+ (event) => {
852
+ event.stopPropagation();
853
+ const shift = event.shiftKey;
854
+ onSelect?.(id, shift);
855
+ if (!onMove) return;
856
+ const svg = event.target.ownerSVGElement;
857
+ if (!svg) return;
858
+ dragging.current = true;
859
+ beginDrag();
860
+ const pt = svg.createSVGPoint();
861
+ pt.x = event.clientX;
862
+ pt.y = event.clientY;
863
+ const svgPt = pt.matrixTransform(svg.getScreenCTM()?.inverse());
864
+ dragStart.current = { mx: svgPt.x, my: svgPt.y, cx: x + w / 2, cy: y + h / 2 };
865
+ const onPointerMove = (e) => {
866
+ if (!dragging.current) return;
867
+ const mp = svg.createSVGPoint();
868
+ mp.x = e.clientX;
869
+ mp.y = e.clientY;
870
+ const sp = mp.matrixTransform(svg.getScreenCTM()?.inverse());
871
+ const dx = sp.x - dragStart.current.mx;
872
+ const dy = sp.y - dragStart.current.my;
873
+ onMove(id, dragStart.current.cx + dx, dragStart.current.cy + dy, shift);
874
+ };
875
+ const onPointerUp = () => {
876
+ dragging.current = false;
877
+ endDrag();
878
+ window.removeEventListener("mousemove", onPointerMove);
879
+ window.removeEventListener("mouseup", onPointerUp);
880
+ };
881
+ window.addEventListener("mousemove", onPointerMove);
882
+ window.addEventListener("mouseup", onPointerUp);
883
+ },
884
+ [id, x, y, w, h, onSelect, onMove, gridSize]
885
+ );
886
+ const handleResizeDown = useCallback3(
887
+ (side) => (event) => {
888
+ event.stopPropagation();
889
+ onSelect?.(id, false);
890
+ if (!onResize) return;
891
+ const svg = event.target.ownerSVGElement;
892
+ if (!svg) return;
893
+ beginDrag();
894
+ const onMouseMove = (e) => {
895
+ const pt = svg.createSVGPoint();
896
+ pt.x = e.clientX;
897
+ pt.y = e.clientY;
898
+ const inv = svg.getScreenCTM()?.inverse();
899
+ if (!inv) return;
900
+ const sp = pt.matrixTransform(inv);
901
+ onResize(id, side, sp.x, sp.y);
902
+ };
903
+ const onMouseUp = () => {
904
+ endDrag();
905
+ window.removeEventListener("mousemove", onMouseMove);
906
+ window.removeEventListener("mouseup", onMouseUp);
907
+ };
908
+ window.addEventListener("mousemove", onMouseMove);
909
+ window.addEventListener("mouseup", onMouseUp);
910
+ },
911
+ [id, onSelect, onResize]
912
+ );
913
+ return /* @__PURE__ */ jsxs5(
914
+ "g",
915
+ {
916
+ "data-node-id": id,
917
+ onMouseDown: handlePointerDown,
918
+ onDoubleClick: onStartEdit ? (event) => {
919
+ event.stopPropagation();
920
+ event.preventDefault();
921
+ onStartEdit(id);
922
+ } : void 0,
923
+ style: { cursor: onMove ? "grab" : "pointer" },
924
+ children: [
925
+ /* @__PURE__ */ jsx6("g", { filter: "url(#ep-node-shadow)", children: /* @__PURE__ */ jsx6(
926
+ Shape,
927
+ {
928
+ x,
929
+ y,
930
+ w,
931
+ h,
932
+ fill: shapeFill,
933
+ stroke: strokeColor,
934
+ strokeDasharray: dash
935
+ }
936
+ ) }),
937
+ selected ? /* @__PURE__ */ jsx6(
938
+ "rect",
939
+ {
940
+ x: x - 3,
941
+ y: y - 3,
942
+ width: w + 6,
943
+ height: h + 6,
944
+ rx: 8,
945
+ ry: 8,
946
+ fill: "none",
947
+ stroke: "#3b82f6",
948
+ strokeWidth: 1.5,
949
+ strokeDasharray: "4 3",
950
+ pointerEvents: "none"
951
+ }
952
+ ) : null,
953
+ badge ? (() => {
954
+ const s = badge.size;
955
+ const bx = badge.cx - s / 2;
956
+ const by = badge.cy - s / 2;
957
+ const rad = s * 0.24;
958
+ const iconS = s * 0.68;
959
+ return /* @__PURE__ */ jsxs5("g", { pointerEvents: "none", children: [
960
+ /* @__PURE__ */ jsx6(
961
+ "rect",
962
+ {
963
+ x: bx,
964
+ y: by + 1.5,
965
+ width: s,
966
+ height: s,
967
+ rx: rad,
968
+ ry: rad,
969
+ fill: strokeColor,
970
+ opacity: 0.45,
971
+ filter: "url(#ep-badge-shadow)"
972
+ }
973
+ ),
974
+ /* @__PURE__ */ jsx6(
975
+ "rect",
976
+ {
977
+ x: bx,
978
+ y: by,
979
+ width: s,
980
+ height: s,
981
+ rx: rad,
982
+ ry: rad,
983
+ fill: shapeFill === "transparent" ? "#ffffff" : shapeFill,
984
+ stroke: "#e7e5e4",
985
+ strokeWidth: 1
986
+ }
987
+ ),
988
+ /* @__PURE__ */ jsx6(
989
+ "image",
990
+ {
991
+ href: iconUrl,
992
+ x: badge.cx - iconS / 2,
993
+ y: badge.cy - iconS / 2,
994
+ width: iconS,
995
+ height: iconS,
996
+ preserveAspectRatio: "xMidYMid meet"
997
+ }
998
+ )
999
+ ] });
1000
+ })() : null,
1001
+ topIcon ? /* @__PURE__ */ jsxs5("g", { pointerEvents: "none", children: [
1002
+ /* @__PURE__ */ jsx6(
1003
+ "circle",
1004
+ {
1005
+ cx: topIcon.cx,
1006
+ cy: topIcon.cy,
1007
+ r: topIcon.size * 0.62,
1008
+ fill: shapeFill === "transparent" ? "#ffffff" : shapeFill,
1009
+ filter: "url(#ep-icon-halo)"
1010
+ }
1011
+ ),
1012
+ /* @__PURE__ */ jsx6(
1013
+ "image",
1014
+ {
1015
+ href: iconUrl,
1016
+ x: topIcon.cx - topIcon.size / 2,
1017
+ y: topIcon.cy - topIcon.size / 2,
1018
+ width: topIcon.size,
1019
+ height: topIcon.size,
1020
+ preserveAspectRatio: "xMidYMid meet"
1021
+ }
1022
+ )
1023
+ ] }) : null,
1024
+ !editing && rich.map((line, i) => {
1025
+ if (isLineEmpty(line)) {
1026
+ return null;
1027
+ }
1028
+ return /* @__PURE__ */ jsxs5(
1029
+ "text",
1030
+ {
1031
+ x: x + w / 2,
1032
+ y: startY + i * lineHeight,
1033
+ textAnchor: "middle",
1034
+ fontFamily,
1035
+ fontSize,
1036
+ fill: labelFill,
1037
+ pointerEvents: "none",
1038
+ children: [
1039
+ line.bullet ? /* @__PURE__ */ jsx6("tspan", { children: "\u2022\u2002" }) : null,
1040
+ line.words.map((wd, j) => {
1041
+ const weight = wd.bold ? 700 : void 0;
1042
+ const style = wd.italic ? "italic" : void 0;
1043
+ const sz = wd.small ? Math.max(9, fontSize - 2) : void 0;
1044
+ const opacity = wd.small ? 0.7 : void 0;
1045
+ return /* @__PURE__ */ jsx6(
1046
+ "tspan",
1047
+ {
1048
+ fontWeight: weight,
1049
+ fontStyle: style,
1050
+ fontSize: sz,
1051
+ opacity,
1052
+ children: (j > 0 ? " " : "") + wd.text
1053
+ },
1054
+ j
1055
+ );
1056
+ })
1057
+ ]
1058
+ },
1059
+ i
1060
+ );
1061
+ }),
1062
+ onResize ? /* @__PURE__ */ jsxs5(Fragment, { children: [
1063
+ /* @__PURE__ */ jsx6(
1064
+ "rect",
1065
+ {
1066
+ x: x - RESIZE_HANDLE / 2,
1067
+ y: y + RESIZE_HANDLE,
1068
+ width: RESIZE_HANDLE,
1069
+ height: h - RESIZE_HANDLE * 2,
1070
+ fill: "transparent",
1071
+ style: { cursor: "ew-resize" },
1072
+ onMouseDown: handleResizeDown("W")
1073
+ }
1074
+ ),
1075
+ /* @__PURE__ */ jsx6(
1076
+ "rect",
1077
+ {
1078
+ x: x + w - RESIZE_HANDLE / 2,
1079
+ y: y + RESIZE_HANDLE,
1080
+ width: RESIZE_HANDLE,
1081
+ height: h - RESIZE_HANDLE * 2,
1082
+ fill: "transparent",
1083
+ style: { cursor: "ew-resize" },
1084
+ onMouseDown: handleResizeDown("E")
1085
+ }
1086
+ ),
1087
+ /* @__PURE__ */ jsx6(
1088
+ "rect",
1089
+ {
1090
+ x: x + RESIZE_HANDLE,
1091
+ y: y - RESIZE_HANDLE / 2,
1092
+ width: w - RESIZE_HANDLE * 2,
1093
+ height: RESIZE_HANDLE,
1094
+ fill: "transparent",
1095
+ style: { cursor: "ns-resize" },
1096
+ onMouseDown: handleResizeDown("N")
1097
+ }
1098
+ ),
1099
+ /* @__PURE__ */ jsx6(
1100
+ "rect",
1101
+ {
1102
+ x: x + RESIZE_HANDLE,
1103
+ y: y + h - RESIZE_HANDLE / 2,
1104
+ width: w - RESIZE_HANDLE * 2,
1105
+ height: RESIZE_HANDLE,
1106
+ fill: "transparent",
1107
+ style: { cursor: "ns-resize" },
1108
+ onMouseDown: handleResizeDown("S")
1109
+ }
1110
+ )
1111
+ ] }) : null
1112
+ ]
1113
+ }
1114
+ );
1115
+ };
1116
+
1117
+ export {
1118
+ STROKE_WIDTH,
1119
+ Area,
1120
+ AreaLabel,
1121
+ EdgeDefs,
1122
+ labelPillSize,
1123
+ Edge,
1124
+ Node
1125
+ };