zelune 0.0.0-stage → 1.5.1

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,197 @@
1
+ import React from 'react';
2
+ import type { ArtboardLayoutMap, NavigationEdge, ScreenMetadata } from './types';
3
+
4
+ interface FlowArrowsLayerProps {
5
+ screens: ScreenMetadata[];
6
+ layout: ArtboardLayoutMap;
7
+ activeScreenId: string | null;
8
+ highlightedScreenId: string | null;
9
+ onSelectFlow?: (edge: NavigationEdge) => void;
10
+ }
11
+
12
+ const ARTBOARD_HEADER_OFFSET = 38;
13
+
14
+ export const FlowArrowsLayer: React.FC<FlowArrowsLayerProps> = ({
15
+ screens,
16
+ layout,
17
+ activeScreenId,
18
+ highlightedScreenId,
19
+ onSelectFlow,
20
+ }) => {
21
+ const edges: NavigationEdge[] = [];
22
+ for (const screen of screens) {
23
+ for (const edge of screen.outgoingFlows) {
24
+ if (layout[edge.fromScreen] && layout[edge.toScreen]) {
25
+ edges.push(edge);
26
+ }
27
+ }
28
+ }
29
+
30
+ if (edges.length === 0) return null;
31
+
32
+ return (
33
+ <svg
34
+ style={{
35
+ position: 'absolute',
36
+ top: 0,
37
+ left: 0,
38
+ width: 8000,
39
+ height: 4000,
40
+ pointerEvents: 'none',
41
+ overflow: 'visible',
42
+ zIndex: 5,
43
+ }}
44
+ >
45
+ <defs>
46
+ <marker
47
+ id="flow-arrow-default"
48
+ viewBox="0 0 10 10"
49
+ refX="8"
50
+ refY="5"
51
+ markerWidth="6"
52
+ markerHeight="6"
53
+ orient="auto-start-reverse"
54
+ >
55
+ <path d="M 0 1.5 L 8.5 5 L 0 8.5 z" fill="#6366F1" />
56
+ </marker>
57
+ <marker
58
+ id="flow-arrow-active"
59
+ viewBox="0 0 10 10"
60
+ refX="8"
61
+ refY="5"
62
+ markerWidth="7"
63
+ markerHeight="7"
64
+ orient="auto-start-reverse"
65
+ >
66
+ <path d="M 0 1.5 L 8.5 5 L 0 8.5 z" fill="#34D399" />
67
+ </marker>
68
+ </defs>
69
+
70
+ {edges.map((edge, idx) => {
71
+ const fromBox = layout[edge.fromScreen];
72
+ const toBox = layout[edge.toScreen];
73
+ if (!fromBox || !toBox) return null;
74
+
75
+ const isForward = toBox.x >= fromBox.x;
76
+ const isHighlighted =
77
+ activeScreenId === edge.fromScreen ||
78
+ highlightedScreenId === edge.toScreen ||
79
+ activeScreenId === edge.toScreen;
80
+
81
+ // Vertical staggering so multiple arrows don't overlap
82
+ const verticalSlot = ((idx % 4) - 1.5) * 56;
83
+
84
+ let startX: number;
85
+ let startY: number;
86
+ let endX: number;
87
+ let endY: number;
88
+ let pathD: string;
89
+ let midX: number;
90
+ let midY: number;
91
+
92
+ if (isForward) {
93
+ startX = fromBox.x + fromBox.width;
94
+ startY = fromBox.y + ARTBOARD_HEADER_OFFSET + fromBox.height * 0.45 + verticalSlot;
95
+ endX = toBox.x;
96
+ endY = toBox.y + ARTBOARD_HEADER_OFFSET + toBox.height * 0.35 + verticalSlot * 0.6;
97
+
98
+ const dx = Math.max(80, Math.abs(endX - startX) * 0.45);
99
+ pathD = `M ${startX} ${startY} C ${startX + dx} ${startY}, ${endX - dx} ${endY}, ${endX} ${endY}`;
100
+ midX = (startX + endX) / 2;
101
+ midY = (startY + endY) / 2;
102
+ } else {
103
+ // Return loop arched underneath the artboards
104
+ startX = fromBox.x + fromBox.width * 0.5;
105
+ startY = fromBox.y + ARTBOARD_HEADER_OFFSET + fromBox.height;
106
+ endX = toBox.x + toBox.width * 0.5;
107
+ endY = toBox.y + ARTBOARD_HEADER_OFFSET + toBox.height;
108
+
109
+ const archDepth = 110 + (idx % 3) * 35;
110
+ pathD = `M ${startX} ${startY} C ${startX} ${startY + archDepth}, ${endX} ${endY + archDepth}, ${endX} ${endY}`;
111
+ midX = (startX + endX) / 2;
112
+ midY = Math.max(startY, endY) + archDepth * 0.72;
113
+ }
114
+
115
+ const strokeColor = isHighlighted ? '#34D399' : '#6366F1';
116
+ const strokeOpacity = isHighlighted ? 0.95 : 0.55;
117
+
118
+ return (
119
+ <g key={edge.id}>
120
+ {/* Subtle glow path */}
121
+ {isHighlighted && (
122
+ <path
123
+ d={pathD}
124
+ fill="none"
125
+ stroke="#34D399"
126
+ strokeWidth={6}
127
+ strokeOpacity={0.18}
128
+ />
129
+ )}
130
+
131
+ {/* Main cubic-bezier connector */}
132
+ <path
133
+ d={pathD}
134
+ fill="none"
135
+ stroke={strokeColor}
136
+ strokeWidth={isHighlighted ? 2.25 : 1.75}
137
+ strokeOpacity={strokeOpacity}
138
+ strokeDasharray={isForward ? undefined : '6 5'}
139
+ markerEnd={
140
+ isHighlighted ? 'url(#flow-arrow-active)' : 'url(#flow-arrow-default)'
141
+ }
142
+ />
143
+
144
+ {/* Origin anchor dot */}
145
+ <circle
146
+ cx={startX}
147
+ cy={startY}
148
+ r={4}
149
+ fill="#0B0D12"
150
+ stroke={strokeColor}
151
+ strokeWidth={2}
152
+ />
153
+
154
+ {/* Interactive Flow Label Pill */}
155
+ <foreignObject
156
+ x={midX - 75}
157
+ y={midY - 13}
158
+ width={150}
159
+ height={28}
160
+ style={{ pointerEvents: 'auto', overflow: 'visible' }}
161
+ >
162
+ <div
163
+ onClick={(e) => {
164
+ e.stopPropagation();
165
+ onSelectFlow?.(edge);
166
+ }}
167
+ title={`${edge.fromScreen} → ${edge.toScreen} (${edge.triggerLabel})`}
168
+ style={{
169
+ display: 'inline-flex',
170
+ alignItems: 'center',
171
+ justifyContent: 'center',
172
+ gap: 4,
173
+ padding: '3px 9px',
174
+ borderRadius: 999,
175
+ backgroundColor: isHighlighted ? '#064E3B' : '#131724',
176
+ border: `1px solid ${isHighlighted ? '#10B981' : '#312E81'}`,
177
+ color: isHighlighted ? '#A7F3D0' : '#C7D2FE',
178
+ fontSize: 10,
179
+ fontWeight: 600,
180
+ fontFamily: 'JetBrains Mono, monospace',
181
+ whiteSpace: 'nowrap',
182
+ cursor: 'pointer',
183
+ boxShadow: '0 4px 12px rgba(0,0,0,0.45)',
184
+ margin: '0 auto',
185
+ }}
186
+ >
187
+ <span>{edge.fromScreen}</span>
188
+ <span>→</span>
189
+ <span>{edge.toScreen}</span>
190
+ </div>
191
+ </foreignObject>
192
+ </g>
193
+ );
194
+ })}
195
+ </svg>
196
+ );
197
+ };