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.
@@ -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 imports React Native / Expo
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 importLines: string[] = [
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, index) => {
55
- const varName = `_screenMod_${index}`;
173
+ sorted.forEach((scr) => {
56
174
  const normalizedFullPath = scr.fullPath.replace(/\\/g, '/');
57
- importLines.push(`import * as ${varName} from ${JSON.stringify(normalizedFullPath)};`);
58
- mapEntries.push(` ${JSON.stringify(scr.id)}: resolveScreenComponent(${varName})`);
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
- ...importLines,
194
+ ...lines,
64
195
  `export const SCREEN_COMPONENTS = {`,
65
196
  mapEntries.join(',\n'),
66
197
  `};`,
@@ -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
- setIsCameraAnimating(false);
445
- const nextPan = {
446
- x: panRef.current.x - e.deltaX,
447
- y: panRef.current.y - e.deltaY,
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
- // Canvas background pointer pan (0ms direct manipulation override)
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 = pan.x;
600
- const startPanY = pan.y;
690
+ const startPanX = panRef.current.x;
691
+ const startPanY = panRef.current.y;
601
692
 
602
693
  const handleMove = (moveEvent: PointerEvent) => {
603
- const nextPan = {
604
- x: startPanX + (moveEvent.clientX - startX),
605
- y: startPanY + (moveEvent.clientY - startY),
606
- };
607
- panRef.current = nextPan;
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 style={{ fontSize: 13.5, fontWeight: 700, letterSpacing: '-0.02em', color: '#F8FAFC' }}>
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' ? '#38BDF8' : 'var(--studio-text-secondary)',
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">Inspect &amp; Edit</span>
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 &amp; 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(6, 78, 59, 0.45)' : 'transparent',
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(30, 27, 75, 0.55)' : 'var(--studio-surface-2)',
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: historyState.undoAvailable > 0 ? '#E2E8F0' : '#475569',
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: historyState.redoAvailable > 0 ? '#E2E8F0' : '#475569',
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: '#CBD5E1',
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: '#CBD5E1',
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: '#CBD5E1',
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: '#CBD5E1',
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: '#CBD5E1',
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 ? '#F8FAFC' : 'var(--studio-text-secondary)',
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: '#E2E8F0',
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: '#F1F5F9',
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.65)',
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: '#F1F5F9',
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.65)',
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="#94A3B8" />
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={(edge: NavigationEdge) => focusOnScreen(edge.toScreen)}
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
- selectedElement={selectedElement}
1256
- inspectedElement={inspectedData}
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}