zelune 1.5.2 → 1.5.5
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 +38 -0
- package/bin/cli.mjs +17 -6
- package/index.html +9 -2
- package/package.json +2 -1
- package/plugin.json +1 -1
- package/plugins/viteReactNativeFallbackPlugin.ts +528 -0
- package/plugins/viteVirtualScreensPlugin.ts +139 -8
- package/src/canvas/CanvasApp.tsx +186 -45
- package/src/canvas/FlowArrowsLayer.tsx +7 -7
- package/src/canvas/InspectorPanel.tsx +58 -49
- package/src/canvas/ScreenArtboard.tsx +107 -32
- package/src/main.tsx +71 -1
- package/src/runtime/MockNavigationContext.tsx +1460 -40
- package/src/runtime/reactNativeShim.ts +141 -0
- package/src/screens/ProductDetailScreen.tsx +32 -59
- package/src/server/astEngine.ts +10 -1
- package/src/styles.css +182 -21
- package/vite.config.ts +136 -67
|
@@ -7,12 +7,19 @@ const RESOLVED_VIRTUAL_MODULE_ID = '\0' + VIRTUAL_MODULE_ID;
|
|
|
7
7
|
const PREFERRED_ORDER = ['Login', 'Home', 'ProductDetail', 'Checkout', 'Profile'];
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Vite Virtual Module plugin that dynamically discovers and
|
|
11
|
-
* screen components from any target project (`--project /path/to/app`)
|
|
10
|
+
* Vite Virtual Module plugin that dynamically discovers and loads React Native / Expo
|
|
11
|
+
* screen components from any target project (`--project /path/to/app`) with per-screen
|
|
12
|
+
* dynamic import() fault isolation so a broken import or top-level exception in one screen
|
|
13
|
+
* can never blank out the entire Zelune studio canvas.
|
|
12
14
|
*/
|
|
13
15
|
export function viteVirtualScreensPlugin(workspaceRoot: string): Plugin {
|
|
16
|
+
let isBuild = false;
|
|
17
|
+
|
|
14
18
|
return {
|
|
15
19
|
name: 'vite-virtual-zelune-screens',
|
|
20
|
+
configResolved(config) {
|
|
21
|
+
isBuild = config.command === 'build';
|
|
22
|
+
},
|
|
16
23
|
resolveId(id) {
|
|
17
24
|
if (id === VIRTUAL_MODULE_ID) {
|
|
18
25
|
return RESOLVED_VIRTUAL_MODULE_ID;
|
|
@@ -25,7 +32,7 @@ export function viteVirtualScreensPlugin(workspaceRoot: string): Plugin {
|
|
|
25
32
|
}
|
|
26
33
|
|
|
27
34
|
const discovered = discoverScreenFilesSync(workspaceRoot);
|
|
28
|
-
const
|
|
35
|
+
const lines: string[] = [
|
|
29
36
|
`import React from 'react';`,
|
|
30
37
|
`function resolveScreenComponent(mod) {`,
|
|
31
38
|
` if (!mod) return null;`,
|
|
@@ -37,6 +44,118 @@ export function viteVirtualScreensPlugin(workspaceRoot: string): Plugin {
|
|
|
37
44
|
` }`,
|
|
38
45
|
` return null;`,
|
|
39
46
|
`}`,
|
|
47
|
+
`function createIsolatedScreenComponent(screenId, filePath, loader) {`,
|
|
48
|
+
` let cachedComp = null;`,
|
|
49
|
+
` return function IsolatedScreenWrapper(props) {`,
|
|
50
|
+
` const [state, setState] = React.useState(() => ({`,
|
|
51
|
+
` Comp: cachedComp,`,
|
|
52
|
+
` error: null,`,
|
|
53
|
+
` attempt: 0,`,
|
|
54
|
+
` }));`,
|
|
55
|
+
` React.useEffect(() => {`,
|
|
56
|
+
` let cancelled = false;`,
|
|
57
|
+
` Promise.resolve()`,
|
|
58
|
+
` .then(() => loader(state.attempt))`,
|
|
59
|
+
` .then((mod) => {`,
|
|
60
|
+
` if (cancelled) return;`,
|
|
61
|
+
` const resolved = resolveScreenComponent(mod);`,
|
|
62
|
+
` if (!resolved) {`,
|
|
63
|
+
` setState((prev) => ({`,
|
|
64
|
+
` ...prev,`,
|
|
65
|
+
` Comp: null,`,
|
|
66
|
+
` error: 'No exported React component found in ' + filePath,`,
|
|
67
|
+
` }));`,
|
|
68
|
+
` } else {`,
|
|
69
|
+
` cachedComp = resolved;`,
|
|
70
|
+
` setState((prev) => ({ ...prev, Comp: resolved, error: null }));`,
|
|
71
|
+
` }`,
|
|
72
|
+
` })`,
|
|
73
|
+
` .catch((err) => {`,
|
|
74
|
+
` if (cancelled) return;`,
|
|
75
|
+
` const msg = err instanceof Error ? err.message : String(err);`,
|
|
76
|
+
` setState((prev) => ({ ...prev, Comp: null, error: msg }));`,
|
|
77
|
+
` });`,
|
|
78
|
+
` return () => {`,
|
|
79
|
+
` cancelled = true;`,
|
|
80
|
+
` };`,
|
|
81
|
+
` }, [state.attempt]);`,
|
|
82
|
+
` if (state.error) {`,
|
|
83
|
+
` return React.createElement(`,
|
|
84
|
+
` 'div',`,
|
|
85
|
+
` {`,
|
|
86
|
+
` style: {`,
|
|
87
|
+
` flex: 1,`,
|
|
88
|
+
` padding: 24,`,
|
|
89
|
+
` backgroundColor: '#18181B',`,
|
|
90
|
+
` color: '#FCA5A5',`,
|
|
91
|
+
` display: 'flex',`,
|
|
92
|
+
` flexDirection: 'column',`,
|
|
93
|
+
` justifyContent: 'center',`,
|
|
94
|
+
` gap: 10,`,
|
|
95
|
+
` fontFamily: 'var(--font-mono)',`,
|
|
96
|
+
` },`,
|
|
97
|
+
` },`,
|
|
98
|
+
` React.createElement(`,
|
|
99
|
+
` 'div',`,
|
|
100
|
+
` { style: { color: '#F87171', fontWeight: 700, fontSize: 13 } },`,
|
|
101
|
+
` 'Module Load Error in ' + filePath`,
|
|
102
|
+
` ),`,
|
|
103
|
+
` React.createElement(`,
|
|
104
|
+
` 'div',`,
|
|
105
|
+
` {`,
|
|
106
|
+
` style: {`,
|
|
107
|
+
` fontSize: 11,`,
|
|
108
|
+
` color: '#FECACA',`,
|
|
109
|
+
` whiteSpace: 'pre-wrap',`,
|
|
110
|
+
` wordBreak: 'break-word',`,
|
|
111
|
+
` maxHeight: 360,`,
|
|
112
|
+
` overflowY: 'auto',`,
|
|
113
|
+
` },`,
|
|
114
|
+
` },`,
|
|
115
|
+
` state.error`,
|
|
116
|
+
` ),`,
|
|
117
|
+
` React.createElement(`,
|
|
118
|
+
` 'button',`,
|
|
119
|
+
` {`,
|
|
120
|
+
` type: 'button',`,
|
|
121
|
+
` onClick: () => setState((prev) => ({ ...prev, error: null, attempt: prev.attempt + 1 })),`,
|
|
122
|
+
` style: {`,
|
|
123
|
+
` alignSelf: 'flex-start',`,
|
|
124
|
+
` marginTop: 6,`,
|
|
125
|
+
` padding: '6px 12px',`,
|
|
126
|
+
` borderRadius: 6,`,
|
|
127
|
+
` border: '1px solid rgba(248, 113, 113, 0.45)',`,
|
|
128
|
+
` backgroundColor: 'rgba(127, 29, 29, 0.4)',`,
|
|
129
|
+
` color: '#FEE2E2',`,
|
|
130
|
+
` fontSize: 11,`,
|
|
131
|
+
` fontWeight: 600,`,
|
|
132
|
+
` cursor: 'pointer',`,
|
|
133
|
+
` },`,
|
|
134
|
+
` },`,
|
|
135
|
+
` 'Retry Screen Load'`,
|
|
136
|
+
` )`,
|
|
137
|
+
` );`,
|
|
138
|
+
` }`,
|
|
139
|
+
` if (!state.Comp) {`,
|
|
140
|
+
` return React.createElement(`,
|
|
141
|
+
` 'div',`,
|
|
142
|
+
` {`,
|
|
143
|
+
` style: {`,
|
|
144
|
+
` flex: 1,`,
|
|
145
|
+
` display: 'flex',`,
|
|
146
|
+
` alignItems: 'center',`,
|
|
147
|
+
` justifyContent: 'center',`,
|
|
148
|
+
` color: '#64748B',`,
|
|
149
|
+
` fontSize: 12,`,
|
|
150
|
+
` fontFamily: 'var(--font-mono)',`,
|
|
151
|
+
` },`,
|
|
152
|
+
` },`,
|
|
153
|
+
` 'Loading ' + screenId + '...'`,
|
|
154
|
+
` );`,
|
|
155
|
+
` }`,
|
|
156
|
+
` return React.createElement(state.Comp, props);`,
|
|
157
|
+
` };`,
|
|
158
|
+
`}`,
|
|
40
159
|
];
|
|
41
160
|
|
|
42
161
|
const mapEntries: string[] = [];
|
|
@@ -51,16 +170,28 @@ export function viteVirtualScreensPlugin(workspaceRoot: string): Plugin {
|
|
|
51
170
|
return a.id.localeCompare(b.id);
|
|
52
171
|
});
|
|
53
172
|
|
|
54
|
-
sorted.forEach((scr
|
|
55
|
-
const varName = `_screenMod_${index}`;
|
|
173
|
+
sorted.forEach((scr) => {
|
|
56
174
|
const normalizedFullPath = scr.fullPath.replace(/\\/g, '/');
|
|
57
|
-
|
|
58
|
-
|
|
175
|
+
const fsUrl =
|
|
176
|
+
'/@fs' + (normalizedFullPath.startsWith('/') ? '' : '/') + normalizedFullPath;
|
|
177
|
+
const loaderExpr = isBuild
|
|
178
|
+
? `(attempt) => attempt > 0 ? import(/* @vite-ignore */ ${JSON.stringify(
|
|
179
|
+
normalizedFullPath
|
|
180
|
+
)} + '?t=' + Date.now()) : import(${JSON.stringify(normalizedFullPath)})`
|
|
181
|
+
: `(attempt) => import(/* @vite-ignore */ ${JSON.stringify(
|
|
182
|
+
fsUrl
|
|
183
|
+
)} + (attempt > 0 ? '?t=' + Date.now() : ''))`;
|
|
184
|
+
|
|
185
|
+
mapEntries.push(
|
|
186
|
+
` ${JSON.stringify(scr.id)}: createIsolatedScreenComponent(${JSON.stringify(
|
|
187
|
+
scr.id
|
|
188
|
+
)}, ${JSON.stringify(scr.relPath)}, ${loaderExpr})`
|
|
189
|
+
);
|
|
59
190
|
orderIds.push(scr.id);
|
|
60
191
|
});
|
|
61
192
|
|
|
62
193
|
return [
|
|
63
|
-
...
|
|
194
|
+
...lines,
|
|
64
195
|
`export const SCREEN_COMPONENTS = {`,
|
|
65
196
|
mapEntries.join(',\n'),
|
|
66
197
|
`};`,
|
package/src/canvas/CanvasApp.tsx
CHANGED
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
PanelRightClose,
|
|
17
17
|
PanelRightOpen,
|
|
18
18
|
Sliders,
|
|
19
|
+
Sun,
|
|
20
|
+
Moon,
|
|
19
21
|
} from 'lucide-react';
|
|
20
22
|
import { SCREEN_COMPONENTS, SCREEN_ORDER } from '../screens';
|
|
21
23
|
import { ScreenArtboard } from './ScreenArtboard';
|
|
@@ -43,6 +45,11 @@ function clampZoom(z: number): number {
|
|
|
43
45
|
return Number(Math.min(Math.max(z, MIN_ZOOM), MAX_ZOOM).toFixed(3));
|
|
44
46
|
}
|
|
45
47
|
|
|
48
|
+
function snapToDevicePixel(v: number): number {
|
|
49
|
+
const dpr = typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1;
|
|
50
|
+
return Math.round(v * dpr) / dpr;
|
|
51
|
+
}
|
|
52
|
+
|
|
46
53
|
function buildDefaultLayout(screens: ScreenMetadata[]): ArtboardLayoutMap {
|
|
47
54
|
const map: ArtboardLayoutMap = {};
|
|
48
55
|
screens.forEach((screen, idx) => {
|
|
@@ -67,6 +74,46 @@ export const CanvasApp: React.FC = () => {
|
|
|
67
74
|
const [isLeftPanelOpen, setIsLeftPanelOpen] = useState<boolean>(true);
|
|
68
75
|
const [isRightPanelOpen, setIsRightPanelOpen] = useState<boolean>(true);
|
|
69
76
|
|
|
77
|
+
// Studio color-scheme state ('dark' default, persisted in localStorage & <meta name="color-scheme">)
|
|
78
|
+
const [colorScheme, setColorScheme] = useState<'dark' | 'light'>(() => {
|
|
79
|
+
try {
|
|
80
|
+
return localStorage.getItem('color-scheme') === 'light' ? 'light' : 'dark';
|
|
81
|
+
} catch {
|
|
82
|
+
return 'dark';
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const applyColorScheme = useCallback((next: 'dark' | 'light') => {
|
|
87
|
+
setColorScheme(next);
|
|
88
|
+
const meta = document.querySelector<HTMLMetaElement>('meta[name="color-scheme"]');
|
|
89
|
+
if (meta) {
|
|
90
|
+
meta.content = next;
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
localStorage.setItem('color-scheme', next);
|
|
94
|
+
} catch {
|
|
95
|
+
// Ignore storage write errors in restricted environments
|
|
96
|
+
}
|
|
97
|
+
}, []);
|
|
98
|
+
|
|
99
|
+
const toggleColorScheme = useCallback(() => {
|
|
100
|
+
applyColorScheme(colorScheme === 'dark' ? 'light' : 'dark');
|
|
101
|
+
}, [colorScheme, applyColorScheme]);
|
|
102
|
+
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
const handleStorage = (e: StorageEvent) => {
|
|
105
|
+
if (e.key !== 'color-scheme') return;
|
|
106
|
+
const next = e.newValue === 'light' ? 'light' : 'dark';
|
|
107
|
+
setColorScheme(next);
|
|
108
|
+
const meta = document.querySelector<HTMLMetaElement>('meta[name="color-scheme"]');
|
|
109
|
+
if (meta) {
|
|
110
|
+
meta.content = next;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
window.addEventListener('storage', handleStorage);
|
|
114
|
+
return () => window.removeEventListener('storage', handleStorage);
|
|
115
|
+
}, []);
|
|
116
|
+
|
|
70
117
|
const [pan, setPan] = useState<{ x: number; y: number }>({ x: 20, y: 10 });
|
|
71
118
|
const [zoom, setZoom] = useState<number>(0.68);
|
|
72
119
|
const [isPanning, setIsPanning] = useState<boolean>(false);
|
|
@@ -86,7 +133,9 @@ export const CanvasApp: React.FC = () => {
|
|
|
86
133
|
}>({ undoAvailable: 0, redoAvailable: 0 });
|
|
87
134
|
|
|
88
135
|
const canvasViewportRef = useRef<HTMLDivElement | null>(null);
|
|
136
|
+
const worldContainerRef = useRef<HTMLDivElement | null>(null);
|
|
89
137
|
const cameraTimerRef = useRef<number | null>(null);
|
|
138
|
+
const panSyncTimerRef = useRef<number | null>(null);
|
|
90
139
|
|
|
91
140
|
// Refs for synchronous access inside non-passive native wheel/gesture listeners
|
|
92
141
|
const zoomRef = useRef<number>(zoom);
|
|
@@ -98,6 +147,39 @@ export const CanvasApp: React.FC = () => {
|
|
|
98
147
|
panRef.current = pan;
|
|
99
148
|
}, [pan]);
|
|
100
149
|
|
|
150
|
+
const applyDirectPanTransform = useCallback(
|
|
151
|
+
(rawX: number, rawY: number, commitImmediately = false) => {
|
|
152
|
+
const nextPan = {
|
|
153
|
+
x: snapToDevicePixel(rawX),
|
|
154
|
+
y: snapToDevicePixel(rawY),
|
|
155
|
+
};
|
|
156
|
+
panRef.current = nextPan;
|
|
157
|
+
|
|
158
|
+
const worldEl = worldContainerRef.current;
|
|
159
|
+
if (worldEl) {
|
|
160
|
+
worldEl.style.transition = 'none';
|
|
161
|
+
worldEl.style.transform = `translate3d(${nextPan.x}px, ${nextPan.y}px, 0) scale(${zoomRef.current})`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (panSyncTimerRef.current) {
|
|
165
|
+
window.clearTimeout(panSyncTimerRef.current);
|
|
166
|
+
panSyncTimerRef.current = null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (commitImmediately) {
|
|
170
|
+
setIsCameraAnimating(false);
|
|
171
|
+
setPan(nextPan);
|
|
172
|
+
} else {
|
|
173
|
+
panSyncTimerRef.current = window.setTimeout(() => {
|
|
174
|
+
panSyncTimerRef.current = null;
|
|
175
|
+
setIsCameraAnimating(false);
|
|
176
|
+
setPan(panRef.current);
|
|
177
|
+
}, 120);
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
[]
|
|
181
|
+
);
|
|
182
|
+
|
|
101
183
|
const triggerCameraTransition = useCallback(() => {
|
|
102
184
|
if (cameraTimerRef.current) {
|
|
103
185
|
window.clearTimeout(cameraTimerRef.current);
|
|
@@ -420,13 +502,17 @@ export const CanvasApp: React.FC = () => {
|
|
|
420
502
|
const worldY = (cursorY - prevPan.y) / prevZoom;
|
|
421
503
|
|
|
422
504
|
const nextPan = {
|
|
423
|
-
x: cursorX - worldX * nextZoom,
|
|
424
|
-
y: cursorY - worldY * nextZoom,
|
|
505
|
+
x: snapToDevicePixel(cursorX - worldX * nextZoom),
|
|
506
|
+
y: snapToDevicePixel(cursorY - worldY * nextZoom),
|
|
425
507
|
};
|
|
426
508
|
|
|
427
509
|
setIsCameraAnimating(false);
|
|
428
510
|
zoomRef.current = nextZoom;
|
|
429
511
|
panRef.current = nextPan;
|
|
512
|
+
if (worldContainerRef.current) {
|
|
513
|
+
worldContainerRef.current.style.transition = 'none';
|
|
514
|
+
worldContainerRef.current.style.transform = `translate3d(${nextPan.x}px, ${nextPan.y}px, 0) scale(${nextZoom})`;
|
|
515
|
+
}
|
|
430
516
|
setZoom(nextZoom);
|
|
431
517
|
setPan(nextPan);
|
|
432
518
|
};
|
|
@@ -441,13 +527,11 @@ export const CanvasApp: React.FC = () => {
|
|
|
441
527
|
const zoomFactor = Math.exp(-e.deltaY * 0.0085);
|
|
442
528
|
applyFocalZoom(zoomRef.current * zoomFactor, e.clientX, e.clientY);
|
|
443
529
|
} else {
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
panRef.current = nextPan;
|
|
450
|
-
setPan(nextPan);
|
|
530
|
+
applyDirectPanTransform(
|
|
531
|
+
panRef.current.x - e.deltaX,
|
|
532
|
+
panRef.current.y - e.deltaY,
|
|
533
|
+
false
|
|
534
|
+
);
|
|
451
535
|
}
|
|
452
536
|
};
|
|
453
537
|
|
|
@@ -495,7 +579,7 @@ export const CanvasApp: React.FC = () => {
|
|
|
495
579
|
document.removeEventListener('gesturechange', handleGestureChange);
|
|
496
580
|
document.removeEventListener('gestureend', handleGestureEnd);
|
|
497
581
|
};
|
|
498
|
-
}, []);
|
|
582
|
+
}, [applyDirectPanTransform]);
|
|
499
583
|
|
|
500
584
|
// Center viewport on a specific screen artboard using Emil Kowalski ease-out-expo camera physics
|
|
501
585
|
const focusOnScreen = useCallback(
|
|
@@ -588,7 +672,14 @@ export const CanvasApp: React.FC = () => {
|
|
|
588
672
|
[focusOnScreen]
|
|
589
673
|
);
|
|
590
674
|
|
|
591
|
-
|
|
675
|
+
const handleSelectFlow = useCallback(
|
|
676
|
+
(edge: NavigationEdge) => {
|
|
677
|
+
focusOnScreen(edge.toScreen);
|
|
678
|
+
},
|
|
679
|
+
[focusOnScreen]
|
|
680
|
+
);
|
|
681
|
+
|
|
682
|
+
// Canvas background pointer pan (0ms direct DOM manipulation override)
|
|
592
683
|
const handleCanvasPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
|
|
593
684
|
if (e.button !== 0 && e.button !== 1) return;
|
|
594
685
|
setIsCameraAnimating(false);
|
|
@@ -596,20 +687,20 @@ export const CanvasApp: React.FC = () => {
|
|
|
596
687
|
|
|
597
688
|
const startX = e.clientX;
|
|
598
689
|
const startY = e.clientY;
|
|
599
|
-
const startPanX =
|
|
600
|
-
const startPanY =
|
|
690
|
+
const startPanX = panRef.current.x;
|
|
691
|
+
const startPanY = panRef.current.y;
|
|
601
692
|
|
|
602
693
|
const handleMove = (moveEvent: PointerEvent) => {
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
setPan(nextPan);
|
|
694
|
+
applyDirectPanTransform(
|
|
695
|
+
startPanX + (moveEvent.clientX - startX),
|
|
696
|
+
startPanY + (moveEvent.clientY - startY),
|
|
697
|
+
false
|
|
698
|
+
);
|
|
609
699
|
};
|
|
610
700
|
|
|
611
701
|
const handleUp = () => {
|
|
612
702
|
setIsPanning(false);
|
|
703
|
+
applyDirectPanTransform(panRef.current.x, panRef.current.y, true);
|
|
613
704
|
window.removeEventListener('pointermove', handleMove);
|
|
614
705
|
window.removeEventListener('pointerup', handleUp);
|
|
615
706
|
};
|
|
@@ -691,7 +782,14 @@ export const CanvasApp: React.FC = () => {
|
|
|
691
782
|
ZL
|
|
692
783
|
</div>
|
|
693
784
|
<div>
|
|
694
|
-
<div
|
|
785
|
+
<div
|
|
786
|
+
style={{
|
|
787
|
+
fontSize: 13.5,
|
|
788
|
+
fontWeight: 700,
|
|
789
|
+
letterSpacing: '-0.02em',
|
|
790
|
+
color: 'var(--studio-text-primary)',
|
|
791
|
+
}}
|
|
792
|
+
>
|
|
695
793
|
Zelune
|
|
696
794
|
</div>
|
|
697
795
|
<div
|
|
@@ -731,14 +829,16 @@ export const CanvasApp: React.FC = () => {
|
|
|
731
829
|
borderRadius: 5,
|
|
732
830
|
border: mode === 'inspect' ? '1px solid rgba(56, 189, 248, 0.35)' : '1px solid transparent',
|
|
733
831
|
backgroundColor: mode === 'inspect' ? 'var(--studio-surface-3)' : 'transparent',
|
|
734
|
-
color: mode === 'inspect' ? '#
|
|
832
|
+
color: mode === 'inspect' ? '#0284C7' : 'var(--studio-text-secondary)',
|
|
735
833
|
fontSize: 12,
|
|
736
834
|
fontWeight: 600,
|
|
737
835
|
cursor: 'pointer',
|
|
738
836
|
}}
|
|
739
837
|
>
|
|
740
|
-
<MousePointer size={14} style={{ flexShrink: 0 }} />
|
|
741
|
-
<span className="zelune-reveal-label"
|
|
838
|
+
<MousePointer size={14} style={{ flexShrink: 0, color: mode === 'inspect' ? '#38BDF8' : 'currentColor' }} />
|
|
839
|
+
<span className="zelune-reveal-label" style={{ color: mode === 'inspect' ? 'var(--studio-text-primary)' : 'inherit' }}>
|
|
840
|
+
Inspect & Edit
|
|
841
|
+
</span>
|
|
742
842
|
</button>
|
|
743
843
|
|
|
744
844
|
<button
|
|
@@ -751,8 +851,8 @@ export const CanvasApp: React.FC = () => {
|
|
|
751
851
|
padding: '6px 9px',
|
|
752
852
|
borderRadius: 5,
|
|
753
853
|
border: mode === 'interact' ? '1px solid rgba(16, 185, 129, 0.4)' : '1px solid transparent',
|
|
754
|
-
backgroundColor: mode === 'interact' ? 'rgba(
|
|
755
|
-
color: mode === 'interact' ? '#34D399' : 'var(--studio-text-secondary)',
|
|
854
|
+
backgroundColor: mode === 'interact' ? 'rgba(16, 185, 129, 0.14)' : 'transparent',
|
|
855
|
+
color: mode === 'interact' ? (colorScheme === 'dark' ? '#34D399' : '#059669') : 'var(--studio-text-secondary)',
|
|
756
856
|
fontSize: 12,
|
|
757
857
|
fontWeight: 600,
|
|
758
858
|
cursor: 'pointer',
|
|
@@ -773,8 +873,8 @@ export const CanvasApp: React.FC = () => {
|
|
|
773
873
|
padding: '7px 9px',
|
|
774
874
|
borderRadius: 7,
|
|
775
875
|
border: `1px solid ${showFlows ? 'rgba(99, 102, 241, 0.45)' : 'var(--studio-border-subtle)'}`,
|
|
776
|
-
backgroundColor: showFlows ? 'rgba(
|
|
777
|
-
color: showFlows ? '#C7D2FE' : 'var(--studio-text-secondary)',
|
|
876
|
+
backgroundColor: showFlows ? 'rgba(99, 102, 241, 0.14)' : 'var(--studio-surface-2)',
|
|
877
|
+
color: showFlows ? (colorScheme === 'dark' ? '#C7D2FE' : '#4F46E5') : 'var(--studio-text-secondary)',
|
|
778
878
|
fontSize: 12,
|
|
779
879
|
fontWeight: 600,
|
|
780
880
|
cursor: 'pointer',
|
|
@@ -785,7 +885,7 @@ export const CanvasApp: React.FC = () => {
|
|
|
785
885
|
</button>
|
|
786
886
|
</div>
|
|
787
887
|
|
|
788
|
-
{/* Right: Undo/Redo, Viewport Controls & Right Inspector Toggle */}
|
|
888
|
+
{/* Right: Undo/Redo, Viewport Controls, Theme Toggle & Right Inspector Toggle */}
|
|
789
889
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
|
790
890
|
<button
|
|
791
891
|
type="button"
|
|
@@ -801,7 +901,10 @@ export const CanvasApp: React.FC = () => {
|
|
|
801
901
|
borderRadius: 6,
|
|
802
902
|
border: '1px solid var(--studio-border-subtle)',
|
|
803
903
|
backgroundColor: 'var(--studio-surface-2)',
|
|
804
|
-
color:
|
|
904
|
+
color:
|
|
905
|
+
historyState.undoAvailable > 0
|
|
906
|
+
? 'var(--studio-text-primary)'
|
|
907
|
+
: 'var(--studio-text-muted)',
|
|
805
908
|
fontSize: 11.5,
|
|
806
909
|
fontFamily: 'var(--font-mono)',
|
|
807
910
|
cursor: historyState.undoAvailable > 0 ? 'pointer' : 'not-allowed',
|
|
@@ -825,7 +928,10 @@ export const CanvasApp: React.FC = () => {
|
|
|
825
928
|
borderRadius: 6,
|
|
826
929
|
border: '1px solid var(--studio-border-subtle)',
|
|
827
930
|
backgroundColor: 'var(--studio-surface-2)',
|
|
828
|
-
color:
|
|
931
|
+
color:
|
|
932
|
+
historyState.redoAvailable > 0
|
|
933
|
+
? 'var(--studio-text-primary)'
|
|
934
|
+
: 'var(--studio-text-muted)',
|
|
829
935
|
fontSize: 11.5,
|
|
830
936
|
cursor: historyState.redoAvailable > 0 ? 'pointer' : 'not-allowed',
|
|
831
937
|
}}
|
|
@@ -848,7 +954,7 @@ export const CanvasApp: React.FC = () => {
|
|
|
848
954
|
borderRadius: 6,
|
|
849
955
|
border: '1px solid var(--studio-border-subtle)',
|
|
850
956
|
backgroundColor: 'var(--studio-surface-2)',
|
|
851
|
-
color: '
|
|
957
|
+
color: 'var(--studio-text-secondary)',
|
|
852
958
|
cursor: 'pointer',
|
|
853
959
|
}}
|
|
854
960
|
>
|
|
@@ -862,7 +968,7 @@ export const CanvasApp: React.FC = () => {
|
|
|
862
968
|
textAlign: 'center',
|
|
863
969
|
fontSize: 11.5,
|
|
864
970
|
fontFamily: 'var(--font-mono)',
|
|
865
|
-
color: '
|
|
971
|
+
color: 'var(--studio-text-secondary)',
|
|
866
972
|
}}
|
|
867
973
|
>
|
|
868
974
|
{Math.round(zoom * 100)}%
|
|
@@ -881,7 +987,7 @@ export const CanvasApp: React.FC = () => {
|
|
|
881
987
|
borderRadius: 6,
|
|
882
988
|
border: '1px solid var(--studio-border-subtle)',
|
|
883
989
|
backgroundColor: 'var(--studio-surface-2)',
|
|
884
|
-
color: '
|
|
990
|
+
color: 'var(--studio-text-secondary)',
|
|
885
991
|
cursor: 'pointer',
|
|
886
992
|
}}
|
|
887
993
|
>
|
|
@@ -899,7 +1005,7 @@ export const CanvasApp: React.FC = () => {
|
|
|
899
1005
|
borderRadius: 6,
|
|
900
1006
|
border: '1px solid var(--studio-border-subtle)',
|
|
901
1007
|
backgroundColor: 'var(--studio-surface-2)',
|
|
902
|
-
color: '
|
|
1008
|
+
color: 'var(--studio-text-secondary)',
|
|
903
1009
|
fontSize: 11.5,
|
|
904
1010
|
fontWeight: 500,
|
|
905
1011
|
cursor: 'pointer',
|
|
@@ -920,7 +1026,7 @@ export const CanvasApp: React.FC = () => {
|
|
|
920
1026
|
borderRadius: 6,
|
|
921
1027
|
border: '1px solid var(--studio-border-subtle)',
|
|
922
1028
|
backgroundColor: 'var(--studio-surface-2)',
|
|
923
|
-
color: '
|
|
1029
|
+
color: 'var(--studio-text-secondary)',
|
|
924
1030
|
fontSize: 11.5,
|
|
925
1031
|
fontWeight: 500,
|
|
926
1032
|
cursor: 'pointer',
|
|
@@ -930,6 +1036,37 @@ export const CanvasApp: React.FC = () => {
|
|
|
930
1036
|
<span className="zelune-reveal-label">Grid</span>
|
|
931
1037
|
</button>
|
|
932
1038
|
|
|
1039
|
+
<button
|
|
1040
|
+
type="button"
|
|
1041
|
+
className="zelune-interactive zelune-icon-reveal-btn"
|
|
1042
|
+
onClick={toggleColorScheme}
|
|
1043
|
+
title={
|
|
1044
|
+
colorScheme === 'dark' ? 'Switch Canvas to Light Mode' : 'Switch Canvas to Dark Mode'
|
|
1045
|
+
}
|
|
1046
|
+
aria-label={
|
|
1047
|
+
colorScheme === 'dark' ? 'Switch Canvas to Light Mode' : 'Switch Canvas to Dark Mode'
|
|
1048
|
+
}
|
|
1049
|
+
style={{
|
|
1050
|
+
padding: '6px 8px',
|
|
1051
|
+
borderRadius: 6,
|
|
1052
|
+
border: '1px solid var(--studio-border-subtle)',
|
|
1053
|
+
backgroundColor: 'var(--studio-surface-2)',
|
|
1054
|
+
color: 'var(--studio-text-secondary)',
|
|
1055
|
+
fontSize: 11.5,
|
|
1056
|
+
fontWeight: 500,
|
|
1057
|
+
cursor: 'pointer',
|
|
1058
|
+
}}
|
|
1059
|
+
>
|
|
1060
|
+
{colorScheme === 'dark' ? (
|
|
1061
|
+
<Sun size={13} style={{ flexShrink: 0, color: '#F59E0B' }} />
|
|
1062
|
+
) : (
|
|
1063
|
+
<Moon size={13} style={{ flexShrink: 0, color: '#6366F1' }} />
|
|
1064
|
+
)}
|
|
1065
|
+
<span className="zelune-reveal-label">
|
|
1066
|
+
{colorScheme === 'dark' ? 'Light' : 'Dark'}
|
|
1067
|
+
</span>
|
|
1068
|
+
</button>
|
|
1069
|
+
|
|
933
1070
|
<div style={{ width: 1, height: 20, backgroundColor: 'var(--studio-border-subtle)', margin: '0 3px' }} />
|
|
934
1071
|
|
|
935
1072
|
<button
|
|
@@ -1068,7 +1205,7 @@ export const CanvasApp: React.FC = () => {
|
|
|
1068
1205
|
isSelected ? 'rgba(56, 189, 248, 0.35)' : 'transparent'
|
|
1069
1206
|
}`,
|
|
1070
1207
|
backgroundColor: isSelected ? 'var(--studio-surface-2)' : 'transparent',
|
|
1071
|
-
color: isSelected ? '
|
|
1208
|
+
color: isSelected ? 'var(--studio-text-primary)' : 'var(--studio-text-secondary)',
|
|
1072
1209
|
fontSize: 12.5,
|
|
1073
1210
|
fontWeight: isSelected ? 600 : 500,
|
|
1074
1211
|
cursor: 'pointer',
|
|
@@ -1108,7 +1245,7 @@ export const CanvasApp: React.FC = () => {
|
|
|
1108
1245
|
fontSize: 10,
|
|
1109
1246
|
fontWeight: 600,
|
|
1110
1247
|
letterSpacing: '0.05em',
|
|
1111
|
-
color: '
|
|
1248
|
+
color: 'var(--studio-text-primary)',
|
|
1112
1249
|
marginBottom: 4,
|
|
1113
1250
|
}}
|
|
1114
1251
|
>
|
|
@@ -1157,12 +1294,12 @@ export const CanvasApp: React.FC = () => {
|
|
|
1157
1294
|
borderRadius: 8,
|
|
1158
1295
|
backgroundColor: 'var(--studio-surface-1)',
|
|
1159
1296
|
border: '1px solid var(--studio-border-subtle)',
|
|
1160
|
-
color: '
|
|
1297
|
+
color: 'var(--studio-text-primary)',
|
|
1161
1298
|
fontSize: 11.5,
|
|
1162
1299
|
fontFamily: 'var(--font-mono)',
|
|
1163
1300
|
fontWeight: 600,
|
|
1164
1301
|
cursor: 'pointer',
|
|
1165
|
-
boxShadow: '0 10px 24px -6px rgba(0, 0, 0, 0.
|
|
1302
|
+
boxShadow: '0 10px 24px -6px rgba(0, 0, 0, 0.28)',
|
|
1166
1303
|
}}
|
|
1167
1304
|
>
|
|
1168
1305
|
<PanelLeftOpen size={13} color="#38BDF8" />
|
|
@@ -1193,24 +1330,25 @@ export const CanvasApp: React.FC = () => {
|
|
|
1193
1330
|
borderRadius: 8,
|
|
1194
1331
|
backgroundColor: 'var(--studio-surface-1)',
|
|
1195
1332
|
border: '1px solid var(--studio-border-subtle)',
|
|
1196
|
-
color: '
|
|
1333
|
+
color: 'var(--studio-text-primary)',
|
|
1197
1334
|
fontSize: 11.5,
|
|
1198
1335
|
fontFamily: 'var(--font-mono)',
|
|
1199
1336
|
fontWeight: 600,
|
|
1200
1337
|
cursor: 'pointer',
|
|
1201
|
-
boxShadow: '0 10px 24px -6px rgba(0, 0, 0, 0.
|
|
1338
|
+
boxShadow: '0 10px 24px -6px rgba(0, 0, 0, 0.28)',
|
|
1202
1339
|
}}
|
|
1203
1340
|
>
|
|
1204
1341
|
<Sliders size={13} color="#38BDF8" />
|
|
1205
1342
|
<span>
|
|
1206
1343
|
{inspectedData ? `<${inspectedData.tagName}> L${inspectedData.line}` : 'AST Inspector'}
|
|
1207
1344
|
</span>
|
|
1208
|
-
<PanelRightOpen size={13} color="
|
|
1345
|
+
<PanelRightOpen size={13} color="var(--studio-text-secondary)" />
|
|
1209
1346
|
</button>
|
|
1210
1347
|
)}
|
|
1211
1348
|
|
|
1212
1349
|
{/* World Transform Container: Emil Kowalski ease-out-expo on camera focus, 0ms on direct drag/pinch */}
|
|
1213
1350
|
<div
|
|
1351
|
+
ref={worldContainerRef}
|
|
1214
1352
|
style={{
|
|
1215
1353
|
position: 'absolute',
|
|
1216
1354
|
top: 0,
|
|
@@ -1218,6 +1356,7 @@ export const CanvasApp: React.FC = () => {
|
|
|
1218
1356
|
transform: `translate3d(${pan.x}px, ${pan.y}px, 0) scale(${zoom})`,
|
|
1219
1357
|
transformOrigin: '0 0',
|
|
1220
1358
|
willChange: 'transform',
|
|
1359
|
+
backfaceVisibility: 'hidden',
|
|
1221
1360
|
transition: isCameraAnimating
|
|
1222
1361
|
? 'transform var(--duration-camera) var(--ease-out-expo)'
|
|
1223
1362
|
: 'none',
|
|
@@ -1229,7 +1368,7 @@ export const CanvasApp: React.FC = () => {
|
|
|
1229
1368
|
layout={layout}
|
|
1230
1369
|
activeScreenId={activeScreenId}
|
|
1231
1370
|
highlightedScreenId={highlightedScreenId}
|
|
1232
|
-
onSelectFlow={
|
|
1371
|
+
onSelectFlow={handleSelectFlow}
|
|
1233
1372
|
/>
|
|
1234
1373
|
)}
|
|
1235
1374
|
|
|
@@ -1241,6 +1380,7 @@ export const CanvasApp: React.FC = () => {
|
|
|
1241
1380
|
height: DEFAULT_HEIGHT,
|
|
1242
1381
|
};
|
|
1243
1382
|
const ScreenComponent = SCREEN_COMPONENTS[screen.id];
|
|
1383
|
+
const isSelectedScreen = selectedElement?.screenId === screen.id;
|
|
1244
1384
|
|
|
1245
1385
|
return (
|
|
1246
1386
|
<ScreenArtboard
|
|
@@ -1252,8 +1392,9 @@ export const CanvasApp: React.FC = () => {
|
|
|
1252
1392
|
mode={mode}
|
|
1253
1393
|
isActiveScreen={activeScreenId === screen.id}
|
|
1254
1394
|
isHighlightedScreen={highlightedScreenId === screen.id}
|
|
1255
|
-
|
|
1256
|
-
|
|
1395
|
+
isCanvasMoving={isPanning}
|
|
1396
|
+
selectedElement={isSelectedScreen ? selectedElement : null}
|
|
1397
|
+
inspectedElement={isSelectedScreen ? inspectedData : null}
|
|
1257
1398
|
onSelectElement={handleSelectElement}
|
|
1258
1399
|
onSelectScreen={setActiveScreenId}
|
|
1259
1400
|
onFocusScreen={focusOnScreen}
|