zelune 1.6.2 → 1.6.4
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/CHANGELOG.md +16 -0
- package/bin/cli.mjs +13 -54
- package/dist/assets/CheckoutScreen-6IdrpKgm.js +229 -0
- package/dist/assets/HomeScreen-Dxe9L4I2.js +316 -0
- package/dist/assets/LoginScreen-Dp1awI4F.js +189 -0
- package/dist/assets/ProductDetailScreen-Wa5_nlRP.js +196 -0
- package/dist/assets/ProfileScreen-riAU6vVh.js +185 -0
- package/dist/assets/index-hEzd4JVy.js +44697 -0
- package/{index.html → dist/index.html} +3 -7
- package/mcp/server.mjs +1370 -34
- package/package.json +10 -17
- package/plugin.json +1 -1
- package/socket.yml +11 -0
- package/mcp/server.ts +0 -446
- package/plugins/viteReactNativeFallbackPlugin.ts +0 -519
- package/plugins/viteRnSourceInject.ts +0 -117
- package/plugins/viteVirtualScreensPlugin.ts +0 -216
- package/src/canvas/CanvasApp.tsx +0 -1655
- package/src/canvas/FlowArrowsLayer.tsx +0 -239
- package/src/canvas/InspectorPanel.tsx +0 -1494
- package/src/canvas/ScreenArtboard.tsx +0 -1491
- package/src/canvas/types.ts +0 -23
- package/src/main.tsx +0 -80
- package/src/runtime/MockNavigationContext.tsx +0 -1795
- package/src/runtime/ScreenErrorBoundary.tsx +0 -86
- package/src/runtime/nativewindShim.ts +0 -997
- package/src/runtime/reactNativeShim.ts +0 -240
- package/src/screens/CheckoutScreen.tsx +0 -229
- package/src/screens/HomeScreen.tsx +0 -288
- package/src/screens/LoginScreen.tsx +0 -176
- package/src/screens/ProductDetailScreen.tsx +0 -207
- package/src/screens/ProfileScreen.tsx +0 -172
- package/src/screens/index.ts +0 -1
- package/src/server/apiMiddleware.ts +0 -375
- package/src/server/astEngine.ts +0 -2553
- package/src/vite-env.d.ts +0 -7
- package/tsconfig.json +0 -23
- package/vite.config.ts +0 -340
- /package/{src/styles.css → dist/assets/index-C7nrS82N.css} +0 -0
|
@@ -1,1491 +0,0 @@
|
|
|
1
|
-
import React, { useRef, useState, useEffect, useCallback } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
GripHorizontal,
|
|
4
|
-
Maximize2,
|
|
5
|
-
AlertTriangle,
|
|
6
|
-
ArrowUp,
|
|
7
|
-
ArrowDown,
|
|
8
|
-
Pin,
|
|
9
|
-
PinOff,
|
|
10
|
-
Type,
|
|
11
|
-
Move,
|
|
12
|
-
} from 'lucide-react';
|
|
13
|
-
import { MockNavigationProvider } from '../runtime/MockNavigationContext';
|
|
14
|
-
import { ScreenErrorBoundary } from '../runtime/ScreenErrorBoundary';
|
|
15
|
-
import type {
|
|
16
|
-
ArtboardPosition,
|
|
17
|
-
CanvasMode,
|
|
18
|
-
InspectedElement,
|
|
19
|
-
ScreenMetadata,
|
|
20
|
-
SelectedElementRef,
|
|
21
|
-
} from './types';
|
|
22
|
-
import type { PatchPayload, ReorderPayload } from '../server/astEngine';
|
|
23
|
-
|
|
24
|
-
interface ScreenArtboardProps {
|
|
25
|
-
screen: ScreenMetadata;
|
|
26
|
-
ScreenComponent: React.ComponentType | undefined;
|
|
27
|
-
position: ArtboardPosition;
|
|
28
|
-
zoom: number;
|
|
29
|
-
mode: CanvasMode;
|
|
30
|
-
isActiveScreen: boolean;
|
|
31
|
-
isHighlightedScreen: boolean;
|
|
32
|
-
isCanvasMoving?: boolean;
|
|
33
|
-
selectedElement: SelectedElementRef | null;
|
|
34
|
-
inspectedElement: InspectedElement | null;
|
|
35
|
-
onSelectElement: (ref: SelectedElementRef) => void;
|
|
36
|
-
onSelectScreen: (screenId: string) => void;
|
|
37
|
-
onFocusScreen: (screenId: string) => void;
|
|
38
|
-
onMoveArtboard: (screenId: string, nextX: number, nextY: number) => void;
|
|
39
|
-
onNavigateFlow: (fromScreen: string, toScreen: string) => void;
|
|
40
|
-
onPatchElement: (payload: PatchPayload) => Promise<void>;
|
|
41
|
-
onReorderElement: (payload: ReorderPayload) => Promise<void>;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
interface BoxOverlay {
|
|
45
|
-
top: number;
|
|
46
|
-
left: number;
|
|
47
|
-
width: number;
|
|
48
|
-
height: number;
|
|
49
|
-
tagName: string;
|
|
50
|
-
astPath: string;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
interface DropIndicator {
|
|
54
|
-
targetAstPath: string;
|
|
55
|
-
targetTagName: string;
|
|
56
|
-
placement: 'before' | 'after';
|
|
57
|
-
isRow: boolean;
|
|
58
|
-
lineTop: number;
|
|
59
|
-
lineLeft: number;
|
|
60
|
-
lineWidth: number;
|
|
61
|
-
lineHeight: number;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
type ResizeDirection = 'nw' | 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w';
|
|
65
|
-
|
|
66
|
-
function findNearestAgyElement(
|
|
67
|
-
target: EventTarget | null,
|
|
68
|
-
container: HTMLElement | null
|
|
69
|
-
): HTMLElement | null {
|
|
70
|
-
if (!target || !(target instanceof HTMLElement) || !container) return null;
|
|
71
|
-
let curr: HTMLElement | null = target;
|
|
72
|
-
while (curr && curr !== container && container.contains(curr)) {
|
|
73
|
-
if (curr.dataset?.agyAstPath && curr.dataset?.agyFile) {
|
|
74
|
-
return curr;
|
|
75
|
-
}
|
|
76
|
-
curr = curr.parentElement;
|
|
77
|
-
}
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function escapeAstPathSelector(astPath: string): string {
|
|
82
|
-
return typeof CSS !== 'undefined' && typeof CSS.escape === 'function'
|
|
83
|
-
? CSS.escape(astPath)
|
|
84
|
-
: astPath.replace(/["\\]/g, '\\$&');
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export const ScreenArtboard: React.FC<ScreenArtboardProps> = React.memo(({
|
|
88
|
-
screen,
|
|
89
|
-
ScreenComponent,
|
|
90
|
-
position,
|
|
91
|
-
zoom,
|
|
92
|
-
mode,
|
|
93
|
-
isActiveScreen,
|
|
94
|
-
isHighlightedScreen,
|
|
95
|
-
isCanvasMoving = false,
|
|
96
|
-
selectedElement,
|
|
97
|
-
inspectedElement,
|
|
98
|
-
onSelectElement,
|
|
99
|
-
onSelectScreen,
|
|
100
|
-
onFocusScreen,
|
|
101
|
-
onMoveArtboard,
|
|
102
|
-
onNavigateFlow,
|
|
103
|
-
onPatchElement,
|
|
104
|
-
onReorderElement,
|
|
105
|
-
}) => {
|
|
106
|
-
const viewportRef = useRef<HTMLDivElement | null>(null);
|
|
107
|
-
const inlineInputRef = useRef<HTMLTextAreaElement | null>(null);
|
|
108
|
-
|
|
109
|
-
const [hoverBox, setHoverBox] = useState<BoxOverlay | null>(null);
|
|
110
|
-
const [selectedBox, setSelectedBox] = useState<BoxOverlay | null>(null);
|
|
111
|
-
const [isDraggingHeader, setIsDraggingHeader] = useState(false);
|
|
112
|
-
|
|
113
|
-
// Direct On-Canvas Manipulation States
|
|
114
|
-
const [activeGizmoAction, setActiveGizmoAction] = useState<
|
|
115
|
-
'resize' | 'radius' | 'move' | null
|
|
116
|
-
>(null);
|
|
117
|
-
const [liveHudBadge, setLiveHudBadge] = useState<string | null>(null);
|
|
118
|
-
const [dropIndicator, setDropIndicator] = useState<DropIndicator | null>(null);
|
|
119
|
-
const [showCenterGuideX, setShowCenterGuideX] = useState<boolean>(false);
|
|
120
|
-
|
|
121
|
-
// Inline Double-Click Text Editing State
|
|
122
|
-
const [isEditingInlineText, setIsEditingInlineText] = useState<boolean>(false);
|
|
123
|
-
const [inlineTextDraft, setInlineTextDraft] = useState<string>('');
|
|
124
|
-
const initialInlineTextRef = useRef<string>('');
|
|
125
|
-
const prevSelectionKeyRef = useRef<string | null>(null);
|
|
126
|
-
|
|
127
|
-
const isSelectedOnThisScreen =
|
|
128
|
-
Boolean(selectedElement) && selectedElement?.screenId === screen.id;
|
|
129
|
-
const activeInspected =
|
|
130
|
-
isSelectedOnThisScreen &&
|
|
131
|
-
inspectedElement &&
|
|
132
|
-
inspectedElement.astPath === selectedElement?.astPath
|
|
133
|
-
? inspectedElement
|
|
134
|
-
: null;
|
|
135
|
-
|
|
136
|
-
const isAbsolutePositioned = activeInspected?.styles?.position === 'absolute';
|
|
137
|
-
|
|
138
|
-
const getSelectedDomNode = useCallback((): HTMLElement | null => {
|
|
139
|
-
const container = viewportRef.current;
|
|
140
|
-
if (!container || !selectedElement || selectedElement.screenId !== screen.id) {
|
|
141
|
-
return null;
|
|
142
|
-
}
|
|
143
|
-
const safeAstPath = escapeAstPathSelector(selectedElement.astPath);
|
|
144
|
-
return container.querySelector<HTMLElement>(`[data-agy-ast-path="${safeAstPath}"]`);
|
|
145
|
-
}, [selectedElement, screen.id]);
|
|
146
|
-
|
|
147
|
-
const computeRelativeBox = useCallback(
|
|
148
|
-
(domEl: HTMLElement): BoxOverlay | null => {
|
|
149
|
-
const container = viewportRef.current;
|
|
150
|
-
if (!container) return null;
|
|
151
|
-
const containerRect = container.getBoundingClientRect();
|
|
152
|
-
const elRect = domEl.getBoundingClientRect();
|
|
153
|
-
|
|
154
|
-
return {
|
|
155
|
-
top: (elRect.top - containerRect.top) / zoom,
|
|
156
|
-
left: (elRect.left - containerRect.left) / zoom,
|
|
157
|
-
width: elRect.width / zoom,
|
|
158
|
-
height: elRect.height / zoom,
|
|
159
|
-
tagName: domEl.dataset.agyTag || 'View',
|
|
160
|
-
astPath: domEl.dataset.agyAstPath || '0',
|
|
161
|
-
};
|
|
162
|
-
},
|
|
163
|
-
[zoom]
|
|
164
|
-
);
|
|
165
|
-
|
|
166
|
-
// Only close inline text editor when the selected element identity (screenId:astPath) actually changes
|
|
167
|
-
const selectionIdentityKey =
|
|
168
|
-
isSelectedOnThisScreen && selectedElement
|
|
169
|
-
? `${selectedElement.screenId}:${selectedElement.astPath}`
|
|
170
|
-
: null;
|
|
171
|
-
|
|
172
|
-
useEffect(() => {
|
|
173
|
-
if (prevSelectionKeyRef.current !== selectionIdentityKey) {
|
|
174
|
-
prevSelectionKeyRef.current = selectionIdentityKey;
|
|
175
|
-
setIsEditingInlineText(false);
|
|
176
|
-
}
|
|
177
|
-
}, [selectionIdentityKey]);
|
|
178
|
-
|
|
179
|
-
// Keep selected element bounding box updated when screen re-renders or styles change
|
|
180
|
-
useEffect(() => {
|
|
181
|
-
if (!isSelectedOnThisScreen) {
|
|
182
|
-
setSelectedBox(null);
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const updateSelectionOverlay = () => {
|
|
187
|
-
const match = getSelectedDomNode();
|
|
188
|
-
if (match) {
|
|
189
|
-
setSelectedBox(computeRelativeBox(match));
|
|
190
|
-
} else {
|
|
191
|
-
setSelectedBox(null);
|
|
192
|
-
}
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
updateSelectionOverlay();
|
|
196
|
-
const timer = window.setTimeout(updateSelectionOverlay, 90);
|
|
197
|
-
return () => window.clearTimeout(timer);
|
|
198
|
-
}, [
|
|
199
|
-
selectedElement,
|
|
200
|
-
inspectedElement,
|
|
201
|
-
screen,
|
|
202
|
-
isSelectedOnThisScreen,
|
|
203
|
-
getSelectedDomNode,
|
|
204
|
-
computeRelativeBox,
|
|
205
|
-
]);
|
|
206
|
-
|
|
207
|
-
// Focus inline text editor when activated
|
|
208
|
-
useEffect(() => {
|
|
209
|
-
if (isEditingInlineText && inlineInputRef.current) {
|
|
210
|
-
inlineInputRef.current.focus();
|
|
211
|
-
inlineInputRef.current.select();
|
|
212
|
-
}
|
|
213
|
-
}, [isEditingInlineText]);
|
|
214
|
-
|
|
215
|
-
const startInlineTextEdit = useCallback(
|
|
216
|
-
(explicitDomEl?: HTMLElement | null) => {
|
|
217
|
-
const domEl = explicitDomEl ?? getSelectedDomNode();
|
|
218
|
-
if (!domEl) return;
|
|
219
|
-
|
|
220
|
-
const domTag = domEl.dataset.agyTag || '';
|
|
221
|
-
const isDomTextNode =
|
|
222
|
-
domTag === 'Text' ||
|
|
223
|
-
domTag.endsWith('.Text') ||
|
|
224
|
-
(domEl.querySelectorAll('[data-agy-ast-path]').length === 0 &&
|
|
225
|
-
Boolean(domEl.textContent?.trim()));
|
|
226
|
-
|
|
227
|
-
if (activeInspected && activeInspected.astPath === domEl.dataset.agyAstPath) {
|
|
228
|
-
if (!activeInspected.isTextEditable && !isDomTextNode) return;
|
|
229
|
-
const initialVal = activeInspected.textContent ?? domEl.textContent?.trim() ?? '';
|
|
230
|
-
initialInlineTextRef.current = initialVal;
|
|
231
|
-
setInlineTextDraft(initialVal);
|
|
232
|
-
setIsEditingInlineText(true);
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
if (!isDomTextNode) return;
|
|
237
|
-
const initialVal = domEl.textContent?.replace(/\s+/g, ' ').trim() ?? '';
|
|
238
|
-
initialInlineTextRef.current = initialVal;
|
|
239
|
-
setInlineTextDraft(initialVal);
|
|
240
|
-
setIsEditingInlineText(true);
|
|
241
|
-
},
|
|
242
|
-
[activeInspected, getSelectedDomNode]
|
|
243
|
-
);
|
|
244
|
-
|
|
245
|
-
const commitInlineTextEdit = useCallback(() => {
|
|
246
|
-
if (!isEditingInlineText || !selectedElement) {
|
|
247
|
-
setIsEditingInlineText(false);
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
|
-
setIsEditingInlineText(false);
|
|
251
|
-
const baselineText = activeInspected?.textContent ?? initialInlineTextRef.current;
|
|
252
|
-
if (inlineTextDraft !== baselineText) {
|
|
253
|
-
void onPatchElement({
|
|
254
|
-
file: selectedElement.file,
|
|
255
|
-
astPath: selectedElement.astPath,
|
|
256
|
-
line: selectedElement.line,
|
|
257
|
-
text: inlineTextDraft,
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
}, [isEditingInlineText, selectedElement, activeInspected, inlineTextDraft, onPatchElement]);
|
|
261
|
-
|
|
262
|
-
// Artboard header drag handler to reposition the screen on the infinite canvas
|
|
263
|
-
const handleHeaderPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
|
|
264
|
-
if (e.button !== 0) return;
|
|
265
|
-
e.stopPropagation();
|
|
266
|
-
onSelectScreen(screen.id);
|
|
267
|
-
setIsDraggingHeader(true);
|
|
268
|
-
|
|
269
|
-
const startClientX = e.clientX;
|
|
270
|
-
const startClientY = e.clientY;
|
|
271
|
-
const initialX = position.x;
|
|
272
|
-
const initialY = position.y;
|
|
273
|
-
|
|
274
|
-
const handlePointerMove = (moveEvent: PointerEvent) => {
|
|
275
|
-
const dx = (moveEvent.clientX - startClientX) / zoom;
|
|
276
|
-
const dy = (moveEvent.clientY - startClientY) / zoom;
|
|
277
|
-
onMoveArtboard(
|
|
278
|
-
screen.id,
|
|
279
|
-
Math.round(initialX + dx),
|
|
280
|
-
Math.round(initialY + dy)
|
|
281
|
-
);
|
|
282
|
-
};
|
|
283
|
-
|
|
284
|
-
const handlePointerUp = () => {
|
|
285
|
-
setIsDraggingHeader(false);
|
|
286
|
-
window.removeEventListener('pointermove', handlePointerMove);
|
|
287
|
-
window.removeEventListener('pointerup', handlePointerUp);
|
|
288
|
-
};
|
|
289
|
-
|
|
290
|
-
window.addEventListener('pointermove', handlePointerMove);
|
|
291
|
-
window.addEventListener('pointerup', handlePointerUp);
|
|
292
|
-
};
|
|
293
|
-
|
|
294
|
-
// 8-Point Direct Resize Handler on Artboard
|
|
295
|
-
const handleResizePointerDown = (
|
|
296
|
-
e: React.PointerEvent<HTMLElement>,
|
|
297
|
-
dir: ResizeDirection
|
|
298
|
-
) => {
|
|
299
|
-
if (e.button !== 0 || !selectedBox || !selectedElement) return;
|
|
300
|
-
e.stopPropagation();
|
|
301
|
-
e.preventDefault();
|
|
302
|
-
|
|
303
|
-
const targetDomEl = getSelectedDomNode();
|
|
304
|
-
if (!targetDomEl) return;
|
|
305
|
-
|
|
306
|
-
const startClientX = e.clientX;
|
|
307
|
-
const startClientY = e.clientY;
|
|
308
|
-
const initialBox = { ...selectedBox };
|
|
309
|
-
const initialLeft = Number(activeInspected?.styles?.left ?? initialBox.left);
|
|
310
|
-
const initialTop = Number(activeInspected?.styles?.top ?? initialBox.top);
|
|
311
|
-
|
|
312
|
-
// Measure parent inner content width so dragging to parent edges can snap to '100%'
|
|
313
|
-
let parentInnerWidth = position.width;
|
|
314
|
-
if (targetDomEl.parentElement) {
|
|
315
|
-
const parentRect = targetDomEl.parentElement.getBoundingClientRect();
|
|
316
|
-
const parentComputed = window.getComputedStyle(targetDomEl.parentElement);
|
|
317
|
-
const padL = Number.parseFloat(parentComputed.paddingLeft) || 0;
|
|
318
|
-
const padR = Number.parseFloat(parentComputed.paddingRight) || 0;
|
|
319
|
-
parentInnerWidth = parentRect.width / zoom - padL - padR;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
setActiveGizmoAction('resize');
|
|
323
|
-
let pendingStyles: Record<string, string | number> = {};
|
|
324
|
-
|
|
325
|
-
const handlePointerMove = (moveEvent: PointerEvent) => {
|
|
326
|
-
const dx = (moveEvent.clientX - startClientX) / zoom;
|
|
327
|
-
const dy = (moveEvent.clientY - startClientY) / zoom;
|
|
328
|
-
|
|
329
|
-
let nextW = initialBox.width;
|
|
330
|
-
let nextH = initialBox.height;
|
|
331
|
-
let nextL = initialLeft;
|
|
332
|
-
let nextT = initialTop;
|
|
333
|
-
|
|
334
|
-
const affectsWidth = dir.includes('e') || dir.includes('w');
|
|
335
|
-
const affectsHeight = dir.includes('s') || dir.includes('n');
|
|
336
|
-
|
|
337
|
-
if (dir.includes('e')) {
|
|
338
|
-
nextW = Math.max(20, Math.round(initialBox.width + dx));
|
|
339
|
-
} else if (dir.includes('w')) {
|
|
340
|
-
nextW = Math.max(20, Math.round(initialBox.width - dx));
|
|
341
|
-
if (isAbsolutePositioned) {
|
|
342
|
-
nextL = Math.round(initialLeft + (initialBox.width - nextW));
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
if (dir.includes('s')) {
|
|
347
|
-
nextH = Math.max(18, Math.round(initialBox.height + dy));
|
|
348
|
-
} else if (dir.includes('n')) {
|
|
349
|
-
nextH = Math.max(18, Math.round(initialBox.height - dy));
|
|
350
|
-
if (isAbsolutePositioned) {
|
|
351
|
-
nextT = Math.round(initialTop + (initialBox.height - nextH));
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
pendingStyles = {};
|
|
356
|
-
let widthLabel = `${Math.round(nextW)}`;
|
|
357
|
-
|
|
358
|
-
if (affectsWidth) {
|
|
359
|
-
if (
|
|
360
|
-
!isAbsolutePositioned &&
|
|
361
|
-
!moveEvent.altKey &&
|
|
362
|
-
Math.abs(nextW - parentInnerWidth) <= 7
|
|
363
|
-
) {
|
|
364
|
-
targetDomEl.style.width = '100%';
|
|
365
|
-
pendingStyles.width = '100%';
|
|
366
|
-
widthLabel = '100%';
|
|
367
|
-
} else {
|
|
368
|
-
targetDomEl.style.width = `${nextW}px`;
|
|
369
|
-
pendingStyles.width = nextW;
|
|
370
|
-
}
|
|
371
|
-
if (isAbsolutePositioned && dir.includes('w')) {
|
|
372
|
-
targetDomEl.style.left = `${nextL}px`;
|
|
373
|
-
pendingStyles.left = nextL;
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
if (affectsHeight) {
|
|
378
|
-
targetDomEl.style.height = `${nextH}px`;
|
|
379
|
-
pendingStyles.height = nextH;
|
|
380
|
-
if (isAbsolutePositioned && dir.includes('n')) {
|
|
381
|
-
targetDomEl.style.top = `${nextT}px`;
|
|
382
|
-
pendingStyles.top = nextT;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
const updatedBox = computeRelativeBox(targetDomEl);
|
|
387
|
-
if (updatedBox) {
|
|
388
|
-
setSelectedBox(updatedBox);
|
|
389
|
-
}
|
|
390
|
-
setLiveHudBadge(`${widthLabel} × ${Math.round(nextH)}`);
|
|
391
|
-
};
|
|
392
|
-
|
|
393
|
-
const handlePointerUp = () => {
|
|
394
|
-
window.removeEventListener('pointermove', handlePointerMove);
|
|
395
|
-
window.removeEventListener('pointerup', handlePointerUp);
|
|
396
|
-
window.removeEventListener('pointercancel', handlePointerUp);
|
|
397
|
-
setActiveGizmoAction(null);
|
|
398
|
-
setLiveHudBadge(null);
|
|
399
|
-
|
|
400
|
-
if (Object.keys(pendingStyles).length > 0) {
|
|
401
|
-
void onPatchElement({
|
|
402
|
-
file: selectedElement.file,
|
|
403
|
-
astPath: selectedElement.astPath,
|
|
404
|
-
line: selectedElement.line,
|
|
405
|
-
styles: pendingStyles,
|
|
406
|
-
});
|
|
407
|
-
}
|
|
408
|
-
};
|
|
409
|
-
|
|
410
|
-
window.addEventListener('pointermove', handlePointerMove);
|
|
411
|
-
window.addEventListener('pointerup', handlePointerUp);
|
|
412
|
-
window.addEventListener('pointercancel', handlePointerUp);
|
|
413
|
-
};
|
|
414
|
-
|
|
415
|
-
// Inner Corner borderRadius Drag Handler
|
|
416
|
-
const handleRadiusPointerDown = (
|
|
417
|
-
e: React.PointerEvent<HTMLElement>,
|
|
418
|
-
corner: 'nw' | 'ne' | 'sw' | 'se'
|
|
419
|
-
) => {
|
|
420
|
-
if (e.button !== 0 || !selectedBox || !selectedElement) return;
|
|
421
|
-
e.stopPropagation();
|
|
422
|
-
e.preventDefault();
|
|
423
|
-
|
|
424
|
-
const targetDomEl = getSelectedDomNode();
|
|
425
|
-
if (!targetDomEl) return;
|
|
426
|
-
|
|
427
|
-
const startClientX = e.clientX;
|
|
428
|
-
const startClientY = e.clientY;
|
|
429
|
-
const initialRadius = Number(activeInspected?.styles?.borderRadius ?? 0) || 0;
|
|
430
|
-
const maxRadius = Math.max(0, Math.round(Math.min(selectedBox.width, selectedBox.height) / 2));
|
|
431
|
-
|
|
432
|
-
setActiveGizmoAction('radius');
|
|
433
|
-
let nextRadius = initialRadius;
|
|
434
|
-
let didChange = false;
|
|
435
|
-
|
|
436
|
-
const handlePointerMove = (moveEvent: PointerEvent) => {
|
|
437
|
-
const dx = (moveEvent.clientX - startClientX) / zoom;
|
|
438
|
-
const dy = (moveEvent.clientY - startClientY) / zoom;
|
|
439
|
-
const signedX = corner.includes('w') ? dx : -dx;
|
|
440
|
-
const signedY = corner.includes('n') ? dy : -dy;
|
|
441
|
-
const delta = Math.round((signedX + signedY) / 2);
|
|
442
|
-
|
|
443
|
-
nextRadius = Math.max(0, Math.min(maxRadius, initialRadius + delta));
|
|
444
|
-
didChange = true;
|
|
445
|
-
targetDomEl.style.borderRadius = `${nextRadius}px`;
|
|
446
|
-
setLiveHudBadge(`Radius ${nextRadius}px`);
|
|
447
|
-
};
|
|
448
|
-
|
|
449
|
-
const handlePointerUp = () => {
|
|
450
|
-
window.removeEventListener('pointermove', handlePointerMove);
|
|
451
|
-
window.removeEventListener('pointerup', handlePointerUp);
|
|
452
|
-
window.removeEventListener('pointercancel', handlePointerUp);
|
|
453
|
-
setActiveGizmoAction(null);
|
|
454
|
-
setLiveHudBadge(null);
|
|
455
|
-
|
|
456
|
-
if (didChange && nextRadius !== initialRadius) {
|
|
457
|
-
void onPatchElement({
|
|
458
|
-
file: selectedElement.file,
|
|
459
|
-
astPath: selectedElement.astPath,
|
|
460
|
-
line: selectedElement.line,
|
|
461
|
-
styles: { borderRadius: nextRadius },
|
|
462
|
-
});
|
|
463
|
-
}
|
|
464
|
-
};
|
|
465
|
-
|
|
466
|
-
window.addEventListener('pointermove', handlePointerMove);
|
|
467
|
-
window.addEventListener('pointerup', handlePointerUp);
|
|
468
|
-
window.addEventListener('pointercancel', handlePointerUp);
|
|
469
|
-
};
|
|
470
|
-
|
|
471
|
-
// Hybrid On-Canvas Move & Reorder Handler (Flexbox Reorder + Freeform Float + Shift Spacing Nudge)
|
|
472
|
-
const startMoveElementInteraction = useCallback(
|
|
473
|
-
(startClientX: number, startClientY: number, initialAltKey: boolean) => {
|
|
474
|
-
if (!selectedBox || !selectedElement) return;
|
|
475
|
-
const targetDomEl = getSelectedDomNode();
|
|
476
|
-
const container = viewportRef.current;
|
|
477
|
-
if (!targetDomEl || !container) return;
|
|
478
|
-
|
|
479
|
-
const initialBox = { ...selectedBox };
|
|
480
|
-
const initialLeft = Number(activeInspected?.styles?.left ?? initialBox.left);
|
|
481
|
-
const initialTop = Number(activeInspected?.styles?.top ?? initialBox.top);
|
|
482
|
-
const initialMarginTop = Number(activeInspected?.styles?.marginTop ?? 0) || 0;
|
|
483
|
-
const prevTransform = targetDomEl.style.transform;
|
|
484
|
-
const prevOpacity = targetDomEl.style.opacity;
|
|
485
|
-
const prevZIndex = targetDomEl.style.zIndex;
|
|
486
|
-
|
|
487
|
-
let hasExceededThreshold = false;
|
|
488
|
-
let activeDrop: DropIndicator | null = null;
|
|
489
|
-
let pendingStylePatch: Record<string, string | number> | null = null;
|
|
490
|
-
|
|
491
|
-
const handlePointerMove = (moveEvent: PointerEvent) => {
|
|
492
|
-
const rawDx = moveEvent.clientX - startClientX;
|
|
493
|
-
const rawDy = moveEvent.clientY - startClientY;
|
|
494
|
-
|
|
495
|
-
// Wait for 4px movement threshold before entering drag state (per modern-web-guidance)
|
|
496
|
-
if (!hasExceededThreshold) {
|
|
497
|
-
if (Math.hypot(rawDx, rawDy) < 4) return;
|
|
498
|
-
hasExceededThreshold = true;
|
|
499
|
-
setActiveGizmoAction('move');
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
const dx = rawDx / zoom;
|
|
503
|
-
const dy = rawDy / zoom;
|
|
504
|
-
const useFreeformFloat = isAbsolutePositioned || initialAltKey || moveEvent.altKey;
|
|
505
|
-
|
|
506
|
-
if (useFreeformFloat) {
|
|
507
|
-
setDropIndicator(null);
|
|
508
|
-
activeDrop = null;
|
|
509
|
-
|
|
510
|
-
let nextLeft = Math.round(initialLeft + dx);
|
|
511
|
-
const nextTop = Math.round(initialTop + dy);
|
|
512
|
-
|
|
513
|
-
// Snap to horizontal center of the 393px artboard within 4px
|
|
514
|
-
const elementCenterX = nextLeft + initialBox.width / 2;
|
|
515
|
-
const artboardCenterX = position.width / 2;
|
|
516
|
-
if (Math.abs(elementCenterX - artboardCenterX) <= 4) {
|
|
517
|
-
nextLeft = Math.round(artboardCenterX - initialBox.width / 2);
|
|
518
|
-
setShowCenterGuideX(true);
|
|
519
|
-
} else {
|
|
520
|
-
setShowCenterGuideX(false);
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
targetDomEl.style.transform = prevTransform;
|
|
524
|
-
targetDomEl.style.opacity = '0.95';
|
|
525
|
-
targetDomEl.style.position = 'absolute';
|
|
526
|
-
targetDomEl.style.left = `${nextLeft}px`;
|
|
527
|
-
targetDomEl.style.top = `${nextTop}px`;
|
|
528
|
-
targetDomEl.style.width = `${Math.round(initialBox.width)}px`;
|
|
529
|
-
targetDomEl.style.zIndex = '20';
|
|
530
|
-
|
|
531
|
-
pendingStylePatch = {
|
|
532
|
-
position: 'absolute',
|
|
533
|
-
left: nextLeft,
|
|
534
|
-
top: nextTop,
|
|
535
|
-
width: Math.round(initialBox.width),
|
|
536
|
-
};
|
|
537
|
-
|
|
538
|
-
const updatedBox = computeRelativeBox(targetDomEl);
|
|
539
|
-
if (updatedBox) setSelectedBox(updatedBox);
|
|
540
|
-
setLiveHudBadge(`Float X:${nextLeft} Y:${nextTop}`);
|
|
541
|
-
return;
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
setShowCenterGuideX(false);
|
|
545
|
-
|
|
546
|
-
// Shift + Drag in Flexbox mode = Nudge marginTop spacing
|
|
547
|
-
if (moveEvent.shiftKey) {
|
|
548
|
-
setDropIndicator(null);
|
|
549
|
-
activeDrop = null;
|
|
550
|
-
const nextMarginTop = Math.round(initialMarginTop + dy);
|
|
551
|
-
targetDomEl.style.transform = prevTransform;
|
|
552
|
-
targetDomEl.style.marginTop = `${nextMarginTop}px`;
|
|
553
|
-
pendingStylePatch = { marginTop: nextMarginTop };
|
|
554
|
-
const updatedBox = computeRelativeBox(targetDomEl);
|
|
555
|
-
if (updatedBox) setSelectedBox(updatedBox);
|
|
556
|
-
setLiveHudBadge(`marginTop: ${nextMarginTop}px`);
|
|
557
|
-
return;
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
// Default Flexbox Drag = Reorder among sibling JSX elements inside parent container
|
|
561
|
-
pendingStylePatch = null;
|
|
562
|
-
targetDomEl.style.transform = `translate3d(${Math.round(dx)}px, ${Math.round(dy)}px, 0)`;
|
|
563
|
-
targetDomEl.style.opacity = '0.84';
|
|
564
|
-
targetDomEl.style.zIndex = '30';
|
|
565
|
-
|
|
566
|
-
const parentEl = targetDomEl.parentElement;
|
|
567
|
-
if (!parentEl) return;
|
|
568
|
-
|
|
569
|
-
const parentStyle = window.getComputedStyle(parentEl);
|
|
570
|
-
const isRow = parentStyle.flexDirection.startsWith('row');
|
|
571
|
-
const containerRect = container.getBoundingClientRect();
|
|
572
|
-
const pointerX = (moveEvent.clientX - containerRect.left) / zoom;
|
|
573
|
-
const pointerY = (moveEvent.clientY - containerRect.top) / zoom;
|
|
574
|
-
|
|
575
|
-
const siblings = Array.from(parentEl.children).filter(
|
|
576
|
-
(child): child is HTMLElement =>
|
|
577
|
-
child instanceof HTMLElement &&
|
|
578
|
-
child !== targetDomEl &&
|
|
579
|
-
Boolean(child.dataset?.agyAstPath)
|
|
580
|
-
);
|
|
581
|
-
|
|
582
|
-
if (siblings.length === 0) {
|
|
583
|
-
// No siblings in parent container: fall back to marginTop nudge
|
|
584
|
-
const nextMarginTop = Math.round(initialMarginTop + dy);
|
|
585
|
-
pendingStylePatch = { marginTop: nextMarginTop };
|
|
586
|
-
setLiveHudBadge(`Offset Y: ${nextMarginTop}px`);
|
|
587
|
-
return;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
let closestSibling: HTMLElement | null = null;
|
|
591
|
-
let closestDist = Number.POSITIVE_INFINITY;
|
|
592
|
-
let closestBox: BoxOverlay | null = null;
|
|
593
|
-
|
|
594
|
-
for (const sib of siblings) {
|
|
595
|
-
const box = computeRelativeBox(sib);
|
|
596
|
-
if (!box) continue;
|
|
597
|
-
const centerX = box.left + box.width / 2;
|
|
598
|
-
const centerY = box.top + box.height / 2;
|
|
599
|
-
const dist = isRow
|
|
600
|
-
? Math.abs(pointerX - centerX)
|
|
601
|
-
: Math.abs(pointerY - centerY);
|
|
602
|
-
if (dist < closestDist) {
|
|
603
|
-
closestDist = dist;
|
|
604
|
-
closestSibling = sib;
|
|
605
|
-
closestBox = box;
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
if (closestSibling && closestBox) {
|
|
610
|
-
const placement: 'before' | 'after' = isRow
|
|
611
|
-
? pointerX < closestBox.left + closestBox.width / 2
|
|
612
|
-
? 'before'
|
|
613
|
-
: 'after'
|
|
614
|
-
: pointerY < closestBox.top + closestBox.height / 2
|
|
615
|
-
? 'before'
|
|
616
|
-
: 'after';
|
|
617
|
-
|
|
618
|
-
const indicator: DropIndicator = isRow
|
|
619
|
-
? {
|
|
620
|
-
targetAstPath: closestBox.astPath,
|
|
621
|
-
targetTagName: closestBox.tagName,
|
|
622
|
-
placement,
|
|
623
|
-
isRow: true,
|
|
624
|
-
lineTop: closestBox.top,
|
|
625
|
-
lineLeft:
|
|
626
|
-
placement === 'before'
|
|
627
|
-
? closestBox.left - 3
|
|
628
|
-
: closestBox.left + closestBox.width + 3,
|
|
629
|
-
lineWidth: 2,
|
|
630
|
-
lineHeight: closestBox.height,
|
|
631
|
-
}
|
|
632
|
-
: {
|
|
633
|
-
targetAstPath: closestBox.astPath,
|
|
634
|
-
targetTagName: closestBox.tagName,
|
|
635
|
-
placement,
|
|
636
|
-
isRow: false,
|
|
637
|
-
lineTop:
|
|
638
|
-
placement === 'before'
|
|
639
|
-
? closestBox.top - 3
|
|
640
|
-
: closestBox.top + closestBox.height + 3,
|
|
641
|
-
lineLeft: closestBox.left,
|
|
642
|
-
lineWidth: closestBox.width,
|
|
643
|
-
lineHeight: 2,
|
|
644
|
-
};
|
|
645
|
-
|
|
646
|
-
activeDrop = indicator;
|
|
647
|
-
setDropIndicator(indicator);
|
|
648
|
-
setLiveHudBadge(`Move ${placement} <${closestBox.tagName}>`);
|
|
649
|
-
}
|
|
650
|
-
};
|
|
651
|
-
|
|
652
|
-
const handlePointerUp = () => {
|
|
653
|
-
window.removeEventListener('pointermove', handlePointerMove);
|
|
654
|
-
window.removeEventListener('pointerup', handlePointerUp);
|
|
655
|
-
window.removeEventListener('pointercancel', handlePointerUp);
|
|
656
|
-
|
|
657
|
-
targetDomEl.style.transform = prevTransform;
|
|
658
|
-
targetDomEl.style.opacity = prevOpacity;
|
|
659
|
-
targetDomEl.style.zIndex = prevZIndex;
|
|
660
|
-
|
|
661
|
-
setActiveGizmoAction(null);
|
|
662
|
-
setLiveHudBadge(null);
|
|
663
|
-
setDropIndicator(null);
|
|
664
|
-
setShowCenterGuideX(false);
|
|
665
|
-
|
|
666
|
-
if (!hasExceededThreshold) return;
|
|
667
|
-
|
|
668
|
-
if (activeDrop) {
|
|
669
|
-
void onReorderElement({
|
|
670
|
-
file: selectedElement.file,
|
|
671
|
-
astPath: selectedElement.astPath,
|
|
672
|
-
targetAstPath: activeDrop.targetAstPath,
|
|
673
|
-
placement: activeDrop.placement,
|
|
674
|
-
});
|
|
675
|
-
} else if (pendingStylePatch) {
|
|
676
|
-
void onPatchElement({
|
|
677
|
-
file: selectedElement.file,
|
|
678
|
-
astPath: selectedElement.astPath,
|
|
679
|
-
line: selectedElement.line,
|
|
680
|
-
styles: pendingStylePatch,
|
|
681
|
-
});
|
|
682
|
-
}
|
|
683
|
-
};
|
|
684
|
-
|
|
685
|
-
window.addEventListener('pointermove', handlePointerMove);
|
|
686
|
-
window.addEventListener('pointerup', handlePointerUp);
|
|
687
|
-
window.addEventListener('pointercancel', handlePointerUp);
|
|
688
|
-
},
|
|
689
|
-
[
|
|
690
|
-
selectedBox,
|
|
691
|
-
selectedElement,
|
|
692
|
-
activeInspected,
|
|
693
|
-
isAbsolutePositioned,
|
|
694
|
-
zoom,
|
|
695
|
-
position.width,
|
|
696
|
-
getSelectedDomNode,
|
|
697
|
-
computeRelativeBox,
|
|
698
|
-
onPatchElement,
|
|
699
|
-
onReorderElement,
|
|
700
|
-
]
|
|
701
|
-
);
|
|
702
|
-
|
|
703
|
-
// Toggle between Flexbox flow and Freeform Float (position: 'absolute')
|
|
704
|
-
const handleToggleFloatMode = (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
705
|
-
e.stopPropagation();
|
|
706
|
-
if (!selectedElement || !selectedBox) return;
|
|
707
|
-
|
|
708
|
-
if (isAbsolutePositioned) {
|
|
709
|
-
void onPatchElement({
|
|
710
|
-
file: selectedElement.file,
|
|
711
|
-
astPath: selectedElement.astPath,
|
|
712
|
-
line: selectedElement.line,
|
|
713
|
-
styles: {
|
|
714
|
-
position: null,
|
|
715
|
-
left: null,
|
|
716
|
-
top: null,
|
|
717
|
-
zIndex: null,
|
|
718
|
-
},
|
|
719
|
-
});
|
|
720
|
-
} else {
|
|
721
|
-
void onPatchElement({
|
|
722
|
-
file: selectedElement.file,
|
|
723
|
-
astPath: selectedElement.astPath,
|
|
724
|
-
line: selectedElement.line,
|
|
725
|
-
styles: {
|
|
726
|
-
position: 'absolute',
|
|
727
|
-
left: Math.round(selectedBox.left),
|
|
728
|
-
top: Math.round(selectedBox.top),
|
|
729
|
-
width: Math.round(selectedBox.width),
|
|
730
|
-
zIndex: 10,
|
|
731
|
-
},
|
|
732
|
-
});
|
|
733
|
-
}
|
|
734
|
-
};
|
|
735
|
-
|
|
736
|
-
// Inspect Mode pointer handlers
|
|
737
|
-
useEffect(() => {
|
|
738
|
-
if (isCanvasMoving) {
|
|
739
|
-
setHoverBox(null);
|
|
740
|
-
}
|
|
741
|
-
}, [isCanvasMoving]);
|
|
742
|
-
|
|
743
|
-
const handlePointerMoveCapture = (e: React.PointerEvent<HTMLDivElement>) => {
|
|
744
|
-
if (mode !== 'inspect' || activeGizmoAction || isCanvasMoving || e.buttons !== 0) return;
|
|
745
|
-
const targetEl = findNearestAgyElement(e.target, viewportRef.current);
|
|
746
|
-
if (!targetEl) {
|
|
747
|
-
setHoverBox(null);
|
|
748
|
-
return;
|
|
749
|
-
}
|
|
750
|
-
setHoverBox(computeRelativeBox(targetEl));
|
|
751
|
-
};
|
|
752
|
-
|
|
753
|
-
const handlePointerLeave = () => {
|
|
754
|
-
setHoverBox(null);
|
|
755
|
-
};
|
|
756
|
-
|
|
757
|
-
const handleViewportPointerDownCapture = (e: React.PointerEvent<HTMLDivElement>) => {
|
|
758
|
-
if (mode !== 'inspect' || e.button !== 0 || isEditingInlineText) return;
|
|
759
|
-
const targetEl = findNearestAgyElement(e.target, viewportRef.current);
|
|
760
|
-
if (!targetEl) return;
|
|
761
|
-
|
|
762
|
-
// If user presses down on the already-selected element, allow dragging it after 4px threshold
|
|
763
|
-
if (
|
|
764
|
-
isSelectedOnThisScreen &&
|
|
765
|
-
selectedElement &&
|
|
766
|
-
targetEl.dataset.agyAstPath === selectedElement.astPath
|
|
767
|
-
) {
|
|
768
|
-
startMoveElementInteraction(e.clientX, e.clientY, e.altKey);
|
|
769
|
-
}
|
|
770
|
-
};
|
|
771
|
-
|
|
772
|
-
const resolveTextTargetForDoubleClick = (rawTarget: HTMLElement): HTMLElement => {
|
|
773
|
-
const tag = rawTarget.dataset.agyTag || '';
|
|
774
|
-
if (tag === 'Text' || tag.endsWith('.Text')) {
|
|
775
|
-
return rawTarget;
|
|
776
|
-
}
|
|
777
|
-
const childTextNodes = rawTarget.querySelectorAll<HTMLElement>('[data-agy-tag="Text"]');
|
|
778
|
-
if (childTextNodes.length === 1) {
|
|
779
|
-
return childTextNodes[0];
|
|
780
|
-
}
|
|
781
|
-
return rawTarget;
|
|
782
|
-
};
|
|
783
|
-
|
|
784
|
-
const handleClickCapture = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
785
|
-
if (mode !== 'inspect' || isEditingInlineText) return;
|
|
786
|
-
e.stopPropagation();
|
|
787
|
-
e.preventDefault();
|
|
788
|
-
|
|
789
|
-
onSelectScreen(screen.id);
|
|
790
|
-
const rawTargetEl = findNearestAgyElement(e.target, viewportRef.current);
|
|
791
|
-
if (!rawTargetEl) return;
|
|
792
|
-
|
|
793
|
-
const targetEl = e.detail >= 2 ? resolveTextTargetForDoubleClick(rawTargetEl) : rawTargetEl;
|
|
794
|
-
|
|
795
|
-
const file = targetEl.dataset.agyFile || screen.filePath;
|
|
796
|
-
const astPath = targetEl.dataset.agyAstPath || '0';
|
|
797
|
-
const line = Number.parseInt(targetEl.dataset.agyLine || '1', 10);
|
|
798
|
-
const col = Number.parseInt(targetEl.dataset.agyCol || '0', 10);
|
|
799
|
-
const tagName = targetEl.dataset.agyTag || 'View';
|
|
800
|
-
|
|
801
|
-
const isAlreadySelected =
|
|
802
|
-
selectedElement?.screenId === screen.id && selectedElement?.astPath === astPath;
|
|
803
|
-
|
|
804
|
-
setSelectedBox(computeRelativeBox(targetEl));
|
|
805
|
-
if (!isAlreadySelected) {
|
|
806
|
-
onSelectElement({
|
|
807
|
-
screenId: screen.id,
|
|
808
|
-
file,
|
|
809
|
-
astPath,
|
|
810
|
-
line,
|
|
811
|
-
col,
|
|
812
|
-
tagName,
|
|
813
|
-
});
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
if (e.detail >= 2) {
|
|
817
|
-
startInlineTextEdit(targetEl);
|
|
818
|
-
}
|
|
819
|
-
};
|
|
820
|
-
|
|
821
|
-
const handleDoubleClickCapture = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
822
|
-
if (mode !== 'inspect') return;
|
|
823
|
-
const rawTargetEl = findNearestAgyElement(e.target, viewportRef.current);
|
|
824
|
-
if (!rawTargetEl) return;
|
|
825
|
-
|
|
826
|
-
e.stopPropagation();
|
|
827
|
-
e.preventDefault();
|
|
828
|
-
const targetEl = resolveTextTargetForDoubleClick(rawTargetEl);
|
|
829
|
-
startInlineTextEdit(targetEl);
|
|
830
|
-
};
|
|
831
|
-
|
|
832
|
-
const borderColor = isHighlightedScreen
|
|
833
|
-
? '#10B981'
|
|
834
|
-
: isActiveScreen
|
|
835
|
-
? '#38BDF8'
|
|
836
|
-
: 'var(--studio-border-strong)';
|
|
837
|
-
|
|
838
|
-
const toolbarAbove = !selectedBox || selectedBox.top >= 30;
|
|
839
|
-
|
|
840
|
-
return (
|
|
841
|
-
<div
|
|
842
|
-
style={{
|
|
843
|
-
position: 'absolute',
|
|
844
|
-
left: position.x,
|
|
845
|
-
top: position.y,
|
|
846
|
-
width: position.width,
|
|
847
|
-
userSelect: 'none',
|
|
848
|
-
WebkitUserSelect: 'none',
|
|
849
|
-
zIndex: isActiveScreen || isHighlightedScreen ? 20 : 10,
|
|
850
|
-
}}
|
|
851
|
-
onPointerDown={(e) => {
|
|
852
|
-
// Prevent background canvas pan when interacting inside an artboard
|
|
853
|
-
e.stopPropagation();
|
|
854
|
-
onSelectScreen(screen.id);
|
|
855
|
-
}}
|
|
856
|
-
>
|
|
857
|
-
{/* Minimalist Architectural Artboard Title Header */}
|
|
858
|
-
<div
|
|
859
|
-
onPointerDown={handleHeaderPointerDown}
|
|
860
|
-
style={{
|
|
861
|
-
height: 30,
|
|
862
|
-
marginBottom: 8,
|
|
863
|
-
display: 'flex',
|
|
864
|
-
alignItems: 'center',
|
|
865
|
-
justifyContent: 'space-between',
|
|
866
|
-
padding: '0 4px',
|
|
867
|
-
cursor: isDraggingHeader ? 'grabbing' : 'grab',
|
|
868
|
-
color: isActiveScreen ? '#0284C7' : 'var(--studio-text-secondary)',
|
|
869
|
-
}}
|
|
870
|
-
>
|
|
871
|
-
<div style={{ display: 'flex', alignItems: 'center', gap: 7 }}>
|
|
872
|
-
<GripHorizontal size={14} style={{ opacity: 0.7 }} />
|
|
873
|
-
<span
|
|
874
|
-
style={{
|
|
875
|
-
fontSize: 13,
|
|
876
|
-
fontWeight: 600,
|
|
877
|
-
letterSpacing: '-0.015em',
|
|
878
|
-
color: isActiveScreen ? 'var(--studio-text-primary)' : 'var(--studio-text-secondary)',
|
|
879
|
-
}}
|
|
880
|
-
>
|
|
881
|
-
{screen.name}
|
|
882
|
-
</span>
|
|
883
|
-
<span
|
|
884
|
-
style={{
|
|
885
|
-
fontSize: 11,
|
|
886
|
-
fontFamily: 'var(--font-mono)',
|
|
887
|
-
color: 'var(--studio-text-muted)',
|
|
888
|
-
}}
|
|
889
|
-
>
|
|
890
|
-
/{screen.routeName}
|
|
891
|
-
</span>
|
|
892
|
-
</div>
|
|
893
|
-
|
|
894
|
-
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
895
|
-
<span
|
|
896
|
-
className="tabular-nums"
|
|
897
|
-
style={{
|
|
898
|
-
fontSize: 10.5,
|
|
899
|
-
fontFamily: 'var(--font-mono)',
|
|
900
|
-
color: 'var(--studio-text-muted)',
|
|
901
|
-
}}
|
|
902
|
-
>
|
|
903
|
-
{position.width} × {position.height}
|
|
904
|
-
</span>
|
|
905
|
-
<button
|
|
906
|
-
type="button"
|
|
907
|
-
className="zelune-interactive"
|
|
908
|
-
onClick={(e) => {
|
|
909
|
-
e.stopPropagation();
|
|
910
|
-
onFocusScreen(screen.id);
|
|
911
|
-
}}
|
|
912
|
-
title="Center screen in viewport"
|
|
913
|
-
style={{
|
|
914
|
-
display: 'inline-flex',
|
|
915
|
-
alignItems: 'center',
|
|
916
|
-
justifyContent: 'center',
|
|
917
|
-
width: 22,
|
|
918
|
-
height: 22,
|
|
919
|
-
borderRadius: 5,
|
|
920
|
-
border: '1px solid var(--studio-border-subtle)',
|
|
921
|
-
backgroundColor: 'var(--studio-surface-2)',
|
|
922
|
-
color: 'var(--studio-text-secondary)',
|
|
923
|
-
cursor: 'pointer',
|
|
924
|
-
}}
|
|
925
|
-
>
|
|
926
|
-
<Maximize2 size={11} />
|
|
927
|
-
</button>
|
|
928
|
-
</div>
|
|
929
|
-
</div>
|
|
930
|
-
|
|
931
|
-
{/* Pure Rectangular Screen Artboard */}
|
|
932
|
-
<div
|
|
933
|
-
ref={viewportRef}
|
|
934
|
-
onPointerDownCapture={handleViewportPointerDownCapture}
|
|
935
|
-
onPointerMoveCapture={handlePointerMoveCapture}
|
|
936
|
-
onPointerLeave={handlePointerLeave}
|
|
937
|
-
onClickCapture={handleClickCapture}
|
|
938
|
-
onDoubleClickCapture={handleDoubleClickCapture}
|
|
939
|
-
style={{
|
|
940
|
-
position: 'relative',
|
|
941
|
-
width: position.width,
|
|
942
|
-
height: position.height,
|
|
943
|
-
backgroundColor: '#0A0B0E',
|
|
944
|
-
border: `1px solid ${borderColor}`,
|
|
945
|
-
borderRadius: 6,
|
|
946
|
-
overflow: 'hidden',
|
|
947
|
-
boxShadow: isHighlightedScreen
|
|
948
|
-
? '0 0 0 3px rgba(16, 185, 129, 0.28), 0 28px 56px -14px rgba(0, 0, 0, 0.78)'
|
|
949
|
-
: isActiveScreen
|
|
950
|
-
? '0 0 0 2px rgba(56, 189, 248, 0.22), 0 28px 56px -14px rgba(0, 0, 0, 0.78)'
|
|
951
|
-
: '0 22px 44px -15px rgba(0, 0, 0, 0.68)',
|
|
952
|
-
transition:
|
|
953
|
-
'border-color var(--duration-micro) var(--ease-out), box-shadow var(--duration-micro) var(--ease-out)',
|
|
954
|
-
display: 'flex',
|
|
955
|
-
flexDirection: 'column',
|
|
956
|
-
cursor: mode === 'inspect' ? 'crosshair' : 'default',
|
|
957
|
-
}}
|
|
958
|
-
>
|
|
959
|
-
{screen.parseError ? (
|
|
960
|
-
<div
|
|
961
|
-
style={{
|
|
962
|
-
flex: 1,
|
|
963
|
-
padding: 24,
|
|
964
|
-
backgroundColor: '#18181B',
|
|
965
|
-
color: '#FCA5A5',
|
|
966
|
-
display: 'flex',
|
|
967
|
-
flexDirection: 'column',
|
|
968
|
-
justifyContent: 'center',
|
|
969
|
-
gap: 10,
|
|
970
|
-
fontFamily: 'var(--font-mono)',
|
|
971
|
-
}}
|
|
972
|
-
>
|
|
973
|
-
<div
|
|
974
|
-
style={{
|
|
975
|
-
display: 'flex',
|
|
976
|
-
alignItems: 'center',
|
|
977
|
-
gap: 8,
|
|
978
|
-
color: '#F87171',
|
|
979
|
-
fontWeight: 700,
|
|
980
|
-
fontSize: 13,
|
|
981
|
-
}}
|
|
982
|
-
>
|
|
983
|
-
<AlertTriangle size={16} />
|
|
984
|
-
<span>AST Syntax Error in {screen.filePath}</span>
|
|
985
|
-
</div>
|
|
986
|
-
<div style={{ fontSize: 11, color: '#FECACA', whiteSpace: 'pre-wrap' }}>
|
|
987
|
-
{screen.parseError}
|
|
988
|
-
</div>
|
|
989
|
-
</div>
|
|
990
|
-
) : ScreenComponent ? (
|
|
991
|
-
<ScreenErrorBoundary screenName={screen.name}>
|
|
992
|
-
<MockNavigationProvider
|
|
993
|
-
routeName={screen.routeName}
|
|
994
|
-
onNavigate={onNavigateFlow}
|
|
995
|
-
>
|
|
996
|
-
<div
|
|
997
|
-
style={{
|
|
998
|
-
flex: 1,
|
|
999
|
-
display: 'flex',
|
|
1000
|
-
flexDirection: 'column',
|
|
1001
|
-
height: '100%',
|
|
1002
|
-
width: '100%',
|
|
1003
|
-
minHeight: 0,
|
|
1004
|
-
minWidth: 0,
|
|
1005
|
-
backgroundColor: '#FFFFFF',
|
|
1006
|
-
color: '#0F172A',
|
|
1007
|
-
overflowY: 'auto',
|
|
1008
|
-
overflowX: 'hidden',
|
|
1009
|
-
}}
|
|
1010
|
-
>
|
|
1011
|
-
<ScreenComponent />
|
|
1012
|
-
</div>
|
|
1013
|
-
</MockNavigationProvider>
|
|
1014
|
-
</ScreenErrorBoundary>
|
|
1015
|
-
) : (
|
|
1016
|
-
<div
|
|
1017
|
-
style={{
|
|
1018
|
-
flex: 1,
|
|
1019
|
-
display: 'flex',
|
|
1020
|
-
alignItems: 'center',
|
|
1021
|
-
justifyContent: 'center',
|
|
1022
|
-
color: '#64748B',
|
|
1023
|
-
fontSize: 13,
|
|
1024
|
-
}}
|
|
1025
|
-
>
|
|
1026
|
-
Mounting {screen.name}...
|
|
1027
|
-
</div>
|
|
1028
|
-
)}
|
|
1029
|
-
|
|
1030
|
-
{/* Smart Horizontal Center Snap Guide Line during Freeform Float Move */}
|
|
1031
|
-
{showCenterGuideX && (
|
|
1032
|
-
<div
|
|
1033
|
-
style={{
|
|
1034
|
-
position: 'absolute',
|
|
1035
|
-
top: 0,
|
|
1036
|
-
bottom: 0,
|
|
1037
|
-
left: Math.round(position.width / 2),
|
|
1038
|
-
width: 1,
|
|
1039
|
-
borderLeft: '1px dashed #F43F5E',
|
|
1040
|
-
pointerEvents: 'none',
|
|
1041
|
-
zIndex: 55,
|
|
1042
|
-
}}
|
|
1043
|
-
/>
|
|
1044
|
-
)}
|
|
1045
|
-
|
|
1046
|
-
{/* Smart Flexbox Drop Insertion Line during Sibling Reorder Drag */}
|
|
1047
|
-
{dropIndicator && (
|
|
1048
|
-
<div
|
|
1049
|
-
style={{
|
|
1050
|
-
position: 'absolute',
|
|
1051
|
-
top: dropIndicator.lineTop,
|
|
1052
|
-
left: dropIndicator.lineLeft,
|
|
1053
|
-
width: dropIndicator.lineWidth,
|
|
1054
|
-
height: dropIndicator.lineHeight,
|
|
1055
|
-
backgroundColor: '#38BDF8',
|
|
1056
|
-
boxShadow: '0 0 8px rgba(56, 189, 248, 0.9)',
|
|
1057
|
-
pointerEvents: 'none',
|
|
1058
|
-
zIndex: 55,
|
|
1059
|
-
}}
|
|
1060
|
-
>
|
|
1061
|
-
<span
|
|
1062
|
-
style={{
|
|
1063
|
-
position: 'absolute',
|
|
1064
|
-
top: dropIndicator.isRow ? -3 : -2,
|
|
1065
|
-
left: dropIndicator.isRow ? -2 : -3,
|
|
1066
|
-
width: 6,
|
|
1067
|
-
height: 6,
|
|
1068
|
-
borderRadius: '50%',
|
|
1069
|
-
backgroundColor: '#38BDF8',
|
|
1070
|
-
}}
|
|
1071
|
-
/>
|
|
1072
|
-
<span
|
|
1073
|
-
style={{
|
|
1074
|
-
position: 'absolute',
|
|
1075
|
-
bottom: dropIndicator.isRow ? -3 : -2,
|
|
1076
|
-
right: dropIndicator.isRow ? -2 : -3,
|
|
1077
|
-
width: 6,
|
|
1078
|
-
height: 6,
|
|
1079
|
-
borderRadius: '50%',
|
|
1080
|
-
backgroundColor: '#38BDF8',
|
|
1081
|
-
}}
|
|
1082
|
-
/>
|
|
1083
|
-
</div>
|
|
1084
|
-
)}
|
|
1085
|
-
|
|
1086
|
-
{/* Figma-style Hover Bounding Box in Inspect Mode */}
|
|
1087
|
-
{mode === 'inspect' &&
|
|
1088
|
-
!activeGizmoAction &&
|
|
1089
|
-
hoverBox &&
|
|
1090
|
-
(!selectedBox || hoverBox.astPath !== selectedBox.astPath) && (
|
|
1091
|
-
<div
|
|
1092
|
-
className="zelune-box-overlay"
|
|
1093
|
-
style={{
|
|
1094
|
-
position: 'absolute',
|
|
1095
|
-
top: hoverBox.top,
|
|
1096
|
-
left: hoverBox.left,
|
|
1097
|
-
width: hoverBox.width,
|
|
1098
|
-
height: hoverBox.height,
|
|
1099
|
-
border: '1px dashed #38BDF8',
|
|
1100
|
-
backgroundColor: 'rgba(56, 189, 248, 0.06)',
|
|
1101
|
-
pointerEvents: 'none',
|
|
1102
|
-
zIndex: 40,
|
|
1103
|
-
}}
|
|
1104
|
-
>
|
|
1105
|
-
<span
|
|
1106
|
-
className="tabular-nums"
|
|
1107
|
-
style={{
|
|
1108
|
-
position: 'absolute',
|
|
1109
|
-
top: -19,
|
|
1110
|
-
left: -1,
|
|
1111
|
-
backgroundColor: '#0284C7',
|
|
1112
|
-
color: '#FFFFFF',
|
|
1113
|
-
fontSize: 10,
|
|
1114
|
-
fontWeight: 600,
|
|
1115
|
-
fontFamily: 'var(--font-mono)',
|
|
1116
|
-
padding: '1px 5px',
|
|
1117
|
-
borderRadius: 3,
|
|
1118
|
-
whiteSpace: 'nowrap',
|
|
1119
|
-
}}
|
|
1120
|
-
>
|
|
1121
|
-
<{hoverBox.tagName}>
|
|
1122
|
-
</span>
|
|
1123
|
-
</div>
|
|
1124
|
-
)}
|
|
1125
|
-
|
|
1126
|
-
{/* Interactive Direct-Manipulation Selection Gizmo */}
|
|
1127
|
-
{mode === 'inspect' && selectedBox && isSelectedOnThisScreen && selectedElement && (
|
|
1128
|
-
<div
|
|
1129
|
-
className="zelune-box-overlay"
|
|
1130
|
-
style={{
|
|
1131
|
-
position: 'absolute',
|
|
1132
|
-
top: selectedBox.top,
|
|
1133
|
-
left: selectedBox.left,
|
|
1134
|
-
width: selectedBox.width,
|
|
1135
|
-
height: selectedBox.height,
|
|
1136
|
-
border: `1.5px solid ${isAbsolutePositioned ? '#F59E0B' : '#6366F1'}`,
|
|
1137
|
-
backgroundColor: isAbsolutePositioned
|
|
1138
|
-
? 'rgba(245, 158, 11, 0.07)'
|
|
1139
|
-
: 'rgba(99, 102, 241, 0.07)',
|
|
1140
|
-
pointerEvents: 'none',
|
|
1141
|
-
zIndex: 45,
|
|
1142
|
-
}}
|
|
1143
|
-
>
|
|
1144
|
-
{/* 8-Point Interactive Resize Handles (4 Corners + 4 Edges) */}
|
|
1145
|
-
{(
|
|
1146
|
-
[
|
|
1147
|
-
{
|
|
1148
|
-
dir: 'nw' as ResizeDirection,
|
|
1149
|
-
style: { top: -5, left: -5, cursor: 'nwse-resize' },
|
|
1150
|
-
},
|
|
1151
|
-
{
|
|
1152
|
-
dir: 'n' as ResizeDirection,
|
|
1153
|
-
style: {
|
|
1154
|
-
top: -4,
|
|
1155
|
-
left: '50%',
|
|
1156
|
-
transform: 'translateX(-50%)',
|
|
1157
|
-
cursor: 'ns-resize',
|
|
1158
|
-
},
|
|
1159
|
-
},
|
|
1160
|
-
{
|
|
1161
|
-
dir: 'ne' as ResizeDirection,
|
|
1162
|
-
style: { top: -5, right: -5, cursor: 'nesw-resize' },
|
|
1163
|
-
},
|
|
1164
|
-
{
|
|
1165
|
-
dir: 'e' as ResizeDirection,
|
|
1166
|
-
style: {
|
|
1167
|
-
top: '50%',
|
|
1168
|
-
right: -4,
|
|
1169
|
-
transform: 'translateY(-50%)',
|
|
1170
|
-
cursor: 'ew-resize',
|
|
1171
|
-
},
|
|
1172
|
-
},
|
|
1173
|
-
{
|
|
1174
|
-
dir: 'se' as ResizeDirection,
|
|
1175
|
-
style: { bottom: -5, right: -5, cursor: 'nwse-resize' },
|
|
1176
|
-
},
|
|
1177
|
-
{
|
|
1178
|
-
dir: 's' as ResizeDirection,
|
|
1179
|
-
style: {
|
|
1180
|
-
bottom: -4,
|
|
1181
|
-
left: '50%',
|
|
1182
|
-
transform: 'translateX(-50%)',
|
|
1183
|
-
cursor: 'ns-resize',
|
|
1184
|
-
},
|
|
1185
|
-
},
|
|
1186
|
-
{
|
|
1187
|
-
dir: 'sw' as ResizeDirection,
|
|
1188
|
-
style: { bottom: -5, left: -5, cursor: 'nesw-resize' },
|
|
1189
|
-
},
|
|
1190
|
-
{
|
|
1191
|
-
dir: 'w' as ResizeDirection,
|
|
1192
|
-
style: {
|
|
1193
|
-
top: '50%',
|
|
1194
|
-
left: -4,
|
|
1195
|
-
transform: 'translateY(-50%)',
|
|
1196
|
-
cursor: 'ew-resize',
|
|
1197
|
-
},
|
|
1198
|
-
},
|
|
1199
|
-
] as const
|
|
1200
|
-
).map(({ dir, style }) => (
|
|
1201
|
-
<span
|
|
1202
|
-
key={dir}
|
|
1203
|
-
title={`Drag to resize (${dir.toUpperCase()})`}
|
|
1204
|
-
onPointerDown={(e) => handleResizePointerDown(e, dir)}
|
|
1205
|
-
onClick={(e) => e.stopPropagation()}
|
|
1206
|
-
style={{
|
|
1207
|
-
position: 'absolute',
|
|
1208
|
-
...style,
|
|
1209
|
-
width: dir.length === 2 ? 8 : 6,
|
|
1210
|
-
height: dir.length === 2 ? 8 : 6,
|
|
1211
|
-
backgroundColor: '#FFFFFF',
|
|
1212
|
-
border: `1.5px solid ${isAbsolutePositioned ? '#D97706' : '#4F46E5'}`,
|
|
1213
|
-
borderRadius: dir.length === 2 ? 2 : 1,
|
|
1214
|
-
pointerEvents: 'auto',
|
|
1215
|
-
touchAction: 'none',
|
|
1216
|
-
WebkitUserSelect: 'none',
|
|
1217
|
-
userSelect: 'none',
|
|
1218
|
-
boxShadow: '0 1px 3px rgba(0,0,0,0.45)',
|
|
1219
|
-
}}
|
|
1220
|
-
/>
|
|
1221
|
-
))}
|
|
1222
|
-
|
|
1223
|
-
{/* 4 Inner Corner borderRadius Handles (Figma-style circular dots) */}
|
|
1224
|
-
{selectedBox.width >= 44 &&
|
|
1225
|
-
selectedBox.height >= 32 &&
|
|
1226
|
-
!isEditingInlineText &&
|
|
1227
|
-
(
|
|
1228
|
-
[
|
|
1229
|
-
{ corner: 'nw' as const, style: { top: 6, left: 6 } },
|
|
1230
|
-
{ corner: 'ne' as const, style: { top: 6, right: 6 } },
|
|
1231
|
-
{ corner: 'sw' as const, style: { bottom: 6, left: 6 } },
|
|
1232
|
-
{ corner: 'se' as const, style: { bottom: 6, right: 6 } },
|
|
1233
|
-
] as const
|
|
1234
|
-
).map(({ corner, style }) => (
|
|
1235
|
-
<span
|
|
1236
|
-
key={corner}
|
|
1237
|
-
title="Drag inward/outward to adjust borderRadius"
|
|
1238
|
-
onPointerDown={(e) => handleRadiusPointerDown(e, corner)}
|
|
1239
|
-
onClick={(e) => e.stopPropagation()}
|
|
1240
|
-
style={{
|
|
1241
|
-
position: 'absolute',
|
|
1242
|
-
...style,
|
|
1243
|
-
width: 6,
|
|
1244
|
-
height: 6,
|
|
1245
|
-
borderRadius: '50%',
|
|
1246
|
-
backgroundColor: 'rgba(56, 189, 248, 0.9)',
|
|
1247
|
-
border: '1px solid #08090C',
|
|
1248
|
-
cursor: 'crosshair',
|
|
1249
|
-
pointerEvents: 'auto',
|
|
1250
|
-
touchAction: 'none',
|
|
1251
|
-
WebkitUserSelect: 'none',
|
|
1252
|
-
userSelect: 'none',
|
|
1253
|
-
opacity: 0.75,
|
|
1254
|
-
}}
|
|
1255
|
-
/>
|
|
1256
|
-
))}
|
|
1257
|
-
|
|
1258
|
-
{/* On-Canvas Floating Action & Move Toolbar Pill */}
|
|
1259
|
-
<div
|
|
1260
|
-
onClick={(e) => e.stopPropagation()}
|
|
1261
|
-
style={{
|
|
1262
|
-
position: 'absolute',
|
|
1263
|
-
...(toolbarAbove ? { top: -27 } : { bottom: -27 }),
|
|
1264
|
-
left: -1.5,
|
|
1265
|
-
display: 'inline-flex',
|
|
1266
|
-
alignItems: 'center',
|
|
1267
|
-
gap: 2,
|
|
1268
|
-
backgroundColor: isAbsolutePositioned ? '#B45309' : '#4F46E5',
|
|
1269
|
-
color: '#FFFFFF',
|
|
1270
|
-
fontSize: 10,
|
|
1271
|
-
fontWeight: 700,
|
|
1272
|
-
fontFamily: 'var(--font-mono)',
|
|
1273
|
-
padding: '2px 4px 2px 6px',
|
|
1274
|
-
borderRadius: 4,
|
|
1275
|
-
whiteSpace: 'nowrap',
|
|
1276
|
-
boxShadow: '0 4px 10px rgba(0, 0, 0, 0.5)',
|
|
1277
|
-
pointerEvents: 'auto',
|
|
1278
|
-
touchAction: 'none',
|
|
1279
|
-
WebkitUserSelect: 'none',
|
|
1280
|
-
userSelect: 'none',
|
|
1281
|
-
zIndex: 50,
|
|
1282
|
-
}}
|
|
1283
|
-
>
|
|
1284
|
-
{/* Drag Grip Handle on the Tag Pill */}
|
|
1285
|
-
<span
|
|
1286
|
-
className="tabular-nums"
|
|
1287
|
-
title="Drag to move or reorder element (Hold Option/Alt for freeform X/Y, Shift for margin offset)"
|
|
1288
|
-
onPointerDown={(e) => {
|
|
1289
|
-
if (e.button !== 0) return;
|
|
1290
|
-
e.stopPropagation();
|
|
1291
|
-
e.preventDefault();
|
|
1292
|
-
startMoveElementInteraction(e.clientX, e.clientY, e.altKey);
|
|
1293
|
-
}}
|
|
1294
|
-
style={{
|
|
1295
|
-
display: 'inline-flex',
|
|
1296
|
-
alignItems: 'center',
|
|
1297
|
-
gap: 4,
|
|
1298
|
-
cursor: activeGizmoAction === 'move' ? 'grabbing' : 'grab',
|
|
1299
|
-
paddingRight: 4,
|
|
1300
|
-
}}
|
|
1301
|
-
>
|
|
1302
|
-
<Move size={10} />
|
|
1303
|
-
<span>
|
|
1304
|
-
<{selectedBox.tagName}> · L{selectedElement.line}
|
|
1305
|
-
</span>
|
|
1306
|
-
</span>
|
|
1307
|
-
|
|
1308
|
-
{/* Quick Reorder Up inside Flexbox Parent */}
|
|
1309
|
-
<button
|
|
1310
|
-
type="button"
|
|
1311
|
-
title="Move element earlier/up in Flexbox stack"
|
|
1312
|
-
onClick={(e) => {
|
|
1313
|
-
e.stopPropagation();
|
|
1314
|
-
void onReorderElement({
|
|
1315
|
-
file: selectedElement.file,
|
|
1316
|
-
astPath: selectedElement.astPath,
|
|
1317
|
-
direction: 'up',
|
|
1318
|
-
});
|
|
1319
|
-
}}
|
|
1320
|
-
style={{
|
|
1321
|
-
display: 'inline-flex',
|
|
1322
|
-
alignItems: 'center',
|
|
1323
|
-
justifyContent: 'center',
|
|
1324
|
-
width: 17,
|
|
1325
|
-
height: 17,
|
|
1326
|
-
border: 'none',
|
|
1327
|
-
borderRadius: 3,
|
|
1328
|
-
backgroundColor: 'rgba(255, 255, 255, 0.14)',
|
|
1329
|
-
color: '#FFFFFF',
|
|
1330
|
-
cursor: 'pointer',
|
|
1331
|
-
padding: 0,
|
|
1332
|
-
}}
|
|
1333
|
-
>
|
|
1334
|
-
<ArrowUp size={10} />
|
|
1335
|
-
</button>
|
|
1336
|
-
|
|
1337
|
-
{/* Quick Reorder Down inside Flexbox Parent */}
|
|
1338
|
-
<button
|
|
1339
|
-
type="button"
|
|
1340
|
-
title="Move element later/down in Flexbox stack"
|
|
1341
|
-
onClick={(e) => {
|
|
1342
|
-
e.stopPropagation();
|
|
1343
|
-
void onReorderElement({
|
|
1344
|
-
file: selectedElement.file,
|
|
1345
|
-
astPath: selectedElement.astPath,
|
|
1346
|
-
direction: 'down',
|
|
1347
|
-
});
|
|
1348
|
-
}}
|
|
1349
|
-
style={{
|
|
1350
|
-
display: 'inline-flex',
|
|
1351
|
-
alignItems: 'center',
|
|
1352
|
-
justifyContent: 'center',
|
|
1353
|
-
width: 17,
|
|
1354
|
-
height: 17,
|
|
1355
|
-
border: 'none',
|
|
1356
|
-
borderRadius: 3,
|
|
1357
|
-
backgroundColor: 'rgba(255, 255, 255, 0.14)',
|
|
1358
|
-
color: '#FFFFFF',
|
|
1359
|
-
cursor: 'pointer',
|
|
1360
|
-
padding: 0,
|
|
1361
|
-
}}
|
|
1362
|
-
>
|
|
1363
|
-
<ArrowDown size={10} />
|
|
1364
|
-
</button>
|
|
1365
|
-
|
|
1366
|
-
{/* Toggle Freeform Float (position: 'absolute') vs Flexbox Flow */}
|
|
1367
|
-
<button
|
|
1368
|
-
type="button"
|
|
1369
|
-
title={
|
|
1370
|
-
isAbsolutePositioned
|
|
1371
|
-
? 'Unpin element back to Flexbox flow'
|
|
1372
|
-
: 'Pin/Float element for freeform (X, Y) dragging'
|
|
1373
|
-
}
|
|
1374
|
-
onClick={handleToggleFloatMode}
|
|
1375
|
-
style={{
|
|
1376
|
-
display: 'inline-flex',
|
|
1377
|
-
alignItems: 'center',
|
|
1378
|
-
justifyContent: 'center',
|
|
1379
|
-
width: 17,
|
|
1380
|
-
height: 17,
|
|
1381
|
-
border: 'none',
|
|
1382
|
-
borderRadius: 3,
|
|
1383
|
-
backgroundColor: isAbsolutePositioned
|
|
1384
|
-
? 'rgba(15, 23, 42, 0.45)'
|
|
1385
|
-
: 'rgba(255, 255, 255, 0.14)',
|
|
1386
|
-
color: '#FFFFFF',
|
|
1387
|
-
cursor: 'pointer',
|
|
1388
|
-
padding: 0,
|
|
1389
|
-
}}
|
|
1390
|
-
>
|
|
1391
|
-
{isAbsolutePositioned ? <PinOff size={10} /> : <Pin size={10} />}
|
|
1392
|
-
</button>
|
|
1393
|
-
|
|
1394
|
-
{/* Inline Text Edit Trigger Button (for editable <Text> nodes) */}
|
|
1395
|
-
{activeInspected?.isTextEditable && (
|
|
1396
|
-
<button
|
|
1397
|
-
type="button"
|
|
1398
|
-
title="Edit text directly on artboard (or double-click element)"
|
|
1399
|
-
onClick={(e) => {
|
|
1400
|
-
e.stopPropagation();
|
|
1401
|
-
startInlineTextEdit();
|
|
1402
|
-
}}
|
|
1403
|
-
style={{
|
|
1404
|
-
display: 'inline-flex',
|
|
1405
|
-
alignItems: 'center',
|
|
1406
|
-
justifyContent: 'center',
|
|
1407
|
-
width: 17,
|
|
1408
|
-
height: 17,
|
|
1409
|
-
border: 'none',
|
|
1410
|
-
borderRadius: 3,
|
|
1411
|
-
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
|
1412
|
-
color: '#FFFFFF',
|
|
1413
|
-
cursor: 'pointer',
|
|
1414
|
-
padding: 0,
|
|
1415
|
-
}}
|
|
1416
|
-
>
|
|
1417
|
-
<Type size={10} />
|
|
1418
|
-
</button>
|
|
1419
|
-
)}
|
|
1420
|
-
</div>
|
|
1421
|
-
|
|
1422
|
-
{/* Live HUD Readout Pill (Dimensions / Radius / Move Feedback) */}
|
|
1423
|
-
<span
|
|
1424
|
-
className="tabular-nums"
|
|
1425
|
-
style={{
|
|
1426
|
-
position: 'absolute',
|
|
1427
|
-
...(toolbarAbove ? { bottom: -20 } : { top: -20 }),
|
|
1428
|
-
right: -1,
|
|
1429
|
-
backgroundColor: '#0D0F14',
|
|
1430
|
-
border: '1px solid rgba(255, 255, 255, 0.15)',
|
|
1431
|
-
color: '#38BDF8',
|
|
1432
|
-
fontSize: 9.5,
|
|
1433
|
-
fontWeight: 600,
|
|
1434
|
-
fontFamily: 'var(--font-mono)',
|
|
1435
|
-
padding: '1px 5px',
|
|
1436
|
-
borderRadius: 3,
|
|
1437
|
-
whiteSpace: 'nowrap',
|
|
1438
|
-
pointerEvents: 'none',
|
|
1439
|
-
boxShadow: '0 2px 6px rgba(0, 0, 0, 0.5)',
|
|
1440
|
-
}}
|
|
1441
|
-
>
|
|
1442
|
-
{liveHudBadge ??
|
|
1443
|
-
`${Math.round(selectedBox.width)} × ${Math.round(selectedBox.height)}`}
|
|
1444
|
-
</span>
|
|
1445
|
-
|
|
1446
|
-
{/* Inline Double-Click On-Canvas <Text> Editor */}
|
|
1447
|
-
{isEditingInlineText && (
|
|
1448
|
-
<textarea
|
|
1449
|
-
ref={inlineInputRef}
|
|
1450
|
-
value={inlineTextDraft}
|
|
1451
|
-
onChange={(e) => setInlineTextDraft(e.target.value)}
|
|
1452
|
-
onBlur={commitInlineTextEdit}
|
|
1453
|
-
onClick={(e) => e.stopPropagation()}
|
|
1454
|
-
onPointerDown={(e) => e.stopPropagation()}
|
|
1455
|
-
onKeyDown={(e) => {
|
|
1456
|
-
e.stopPropagation();
|
|
1457
|
-
if (e.key === 'Enter' && !e.shiftKey) {
|
|
1458
|
-
e.preventDefault();
|
|
1459
|
-
commitInlineTextEdit();
|
|
1460
|
-
} else if (e.key === 'Escape') {
|
|
1461
|
-
e.preventDefault();
|
|
1462
|
-
setIsEditingInlineText(false);
|
|
1463
|
-
}
|
|
1464
|
-
}}
|
|
1465
|
-
style={{
|
|
1466
|
-
position: 'absolute',
|
|
1467
|
-
inset: -2,
|
|
1468
|
-
width: Math.max(selectedBox.width + 8, 90),
|
|
1469
|
-
height: Math.max(selectedBox.height + 6, 28),
|
|
1470
|
-
backgroundColor: 'rgba(13, 15, 20, 0.96)',
|
|
1471
|
-
color: String(activeInspected?.styles?.color ?? '#F8FAFC'),
|
|
1472
|
-
fontSize: Number(activeInspected?.styles?.fontSize ?? 14),
|
|
1473
|
-
fontWeight: String(activeInspected?.styles?.fontWeight ?? '500'),
|
|
1474
|
-
fontFamily: 'var(--font-sans)',
|
|
1475
|
-
border: '1.5px solid #38BDF8',
|
|
1476
|
-
borderRadius: 4,
|
|
1477
|
-
padding: '2px 5px',
|
|
1478
|
-
outline: 'none',
|
|
1479
|
-
resize: 'none',
|
|
1480
|
-
pointerEvents: 'auto',
|
|
1481
|
-
zIndex: 60,
|
|
1482
|
-
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.65)',
|
|
1483
|
-
}}
|
|
1484
|
-
/>
|
|
1485
|
-
)}
|
|
1486
|
-
</div>
|
|
1487
|
-
)}
|
|
1488
|
-
</div>
|
|
1489
|
-
</div>
|
|
1490
|
-
);
|
|
1491
|
-
});
|