zelune 0.0.0-stage → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,451 @@
1
+ import React, { useRef, useState, useEffect, useCallback } from 'react';
2
+ import { GripHorizontal, Maximize2, AlertTriangle } from 'lucide-react';
3
+ import { MockNavigationProvider } from '../runtime/MockNavigationContext';
4
+ import { ScreenErrorBoundary } from '../runtime/ScreenErrorBoundary';
5
+ import type {
6
+ ArtboardPosition,
7
+ CanvasMode,
8
+ ScreenMetadata,
9
+ SelectedElementRef,
10
+ } from './types';
11
+
12
+ interface ScreenArtboardProps {
13
+ screen: ScreenMetadata;
14
+ ScreenComponent: React.ComponentType | undefined;
15
+ position: ArtboardPosition;
16
+ zoom: number;
17
+ mode: CanvasMode;
18
+ isActiveScreen: boolean;
19
+ isHighlightedScreen: boolean;
20
+ selectedElement: SelectedElementRef | null;
21
+ onSelectElement: (ref: SelectedElementRef) => void;
22
+ onSelectScreen: (screenId: string) => void;
23
+ onFocusScreen: (screenId: string) => void;
24
+ onMoveArtboard: (screenId: string, nextX: number, nextY: number) => void;
25
+ onNavigateFlow: (fromScreen: string, toScreen: string) => void;
26
+ }
27
+
28
+ interface BoxOverlay {
29
+ top: number;
30
+ left: number;
31
+ width: number;
32
+ height: number;
33
+ tagName: string;
34
+ astPath: string;
35
+ }
36
+
37
+ function findNearestAgyElement(
38
+ target: EventTarget | null,
39
+ container: HTMLElement | null
40
+ ): HTMLElement | null {
41
+ if (!target || !(target instanceof HTMLElement) || !container) return null;
42
+ let curr: HTMLElement | null = target;
43
+ while (curr && curr !== container && container.contains(curr)) {
44
+ if (curr.dataset?.agyAstPath && curr.dataset?.agyFile) {
45
+ return curr;
46
+ }
47
+ curr = curr.parentElement;
48
+ }
49
+ return null;
50
+ }
51
+
52
+ export const ScreenArtboard: React.FC<ScreenArtboardProps> = ({
53
+ screen,
54
+ ScreenComponent,
55
+ position,
56
+ zoom,
57
+ mode,
58
+ isActiveScreen,
59
+ isHighlightedScreen,
60
+ selectedElement,
61
+ onSelectElement,
62
+ onSelectScreen,
63
+ onFocusScreen,
64
+ onMoveArtboard,
65
+ onNavigateFlow,
66
+ }) => {
67
+ const viewportRef = useRef<HTMLDivElement | null>(null);
68
+ const [hoverBox, setHoverBox] = useState<BoxOverlay | null>(null);
69
+ const [selectedBox, setSelectedBox] = useState<BoxOverlay | null>(null);
70
+ const [isDraggingHeader, setIsDraggingHeader] = useState(false);
71
+
72
+ const computeRelativeBox = useCallback(
73
+ (domEl: HTMLElement): BoxOverlay | null => {
74
+ const container = viewportRef.current;
75
+ if (!container) return null;
76
+ const containerRect = container.getBoundingClientRect();
77
+ const elRect = domEl.getBoundingClientRect();
78
+
79
+ return {
80
+ top: (elRect.top - containerRect.top) / zoom,
81
+ left: (elRect.left - containerRect.left) / zoom,
82
+ width: elRect.width / zoom,
83
+ height: elRect.height / zoom,
84
+ tagName: domEl.dataset.agyTag || 'View',
85
+ astPath: domEl.dataset.agyAstPath || '0',
86
+ };
87
+ },
88
+ [zoom]
89
+ );
90
+
91
+ // Keep selected element bounding box updated when screen re-renders or styles change
92
+ useEffect(() => {
93
+ const container = viewportRef.current;
94
+ if (
95
+ !container ||
96
+ !selectedElement ||
97
+ selectedElement.screenId !== screen.id
98
+ ) {
99
+ setSelectedBox(null);
100
+ return;
101
+ }
102
+
103
+ const updateSelectionOverlay = () => {
104
+ const match = container.querySelector<HTMLElement>(
105
+ `[data-agy-ast-path="${selectedElement.astPath}"]`
106
+ );
107
+ if (match) {
108
+ setSelectedBox(computeRelativeBox(match));
109
+ } else {
110
+ setSelectedBox(null);
111
+ }
112
+ };
113
+
114
+ updateSelectionOverlay();
115
+ const timer = window.setTimeout(updateSelectionOverlay, 90);
116
+ return () => window.clearTimeout(timer);
117
+ }, [selectedElement, screen, computeRelativeBox]);
118
+
119
+ // Artboard header drag handler to reposition the screen on the infinite canvas
120
+ const handleHeaderPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
121
+ if (e.button !== 0) return;
122
+ e.stopPropagation();
123
+ onSelectScreen(screen.id);
124
+ setIsDraggingHeader(true);
125
+
126
+ const startClientX = e.clientX;
127
+ const startClientY = e.clientY;
128
+ const initialX = position.x;
129
+ const initialY = position.y;
130
+
131
+ const handlePointerMove = (moveEvent: PointerEvent) => {
132
+ const dx = (moveEvent.clientX - startClientX) / zoom;
133
+ const dy = (moveEvent.clientY - startClientY) / zoom;
134
+ onMoveArtboard(
135
+ screen.id,
136
+ Math.round(initialX + dx),
137
+ Math.round(initialY + dy)
138
+ );
139
+ };
140
+
141
+ const handlePointerUp = () => {
142
+ setIsDraggingHeader(false);
143
+ window.removeEventListener('pointermove', handlePointerMove);
144
+ window.removeEventListener('pointerup', handlePointerUp);
145
+ };
146
+
147
+ window.addEventListener('pointermove', handlePointerMove);
148
+ window.addEventListener('pointerup', handlePointerUp);
149
+ };
150
+
151
+ // Inspect Mode pointer handlers
152
+ const handlePointerMoveCapture = (e: React.PointerEvent<HTMLDivElement>) => {
153
+ if (mode !== 'inspect') return;
154
+ const targetEl = findNearestAgyElement(e.target, viewportRef.current);
155
+ if (!targetEl) {
156
+ setHoverBox(null);
157
+ return;
158
+ }
159
+ setHoverBox(computeRelativeBox(targetEl));
160
+ };
161
+
162
+ const handlePointerLeave = () => {
163
+ setHoverBox(null);
164
+ };
165
+
166
+ const handleClickCapture = (e: React.MouseEvent<HTMLDivElement>) => {
167
+ if (mode !== 'inspect') return;
168
+ e.stopPropagation();
169
+ e.preventDefault();
170
+
171
+ onSelectScreen(screen.id);
172
+ const targetEl = findNearestAgyElement(e.target, viewportRef.current);
173
+ if (!targetEl) return;
174
+
175
+ const file = targetEl.dataset.agyFile || screen.filePath;
176
+ const astPath = targetEl.dataset.agyAstPath || '0';
177
+ const line = Number.parseInt(targetEl.dataset.agyLine || '1', 10);
178
+ const col = Number.parseInt(targetEl.dataset.agyCol || '0', 10);
179
+ const tagName = targetEl.dataset.agyTag || 'View';
180
+
181
+ setSelectedBox(computeRelativeBox(targetEl));
182
+ onSelectElement({
183
+ screenId: screen.id,
184
+ file,
185
+ astPath,
186
+ line,
187
+ col,
188
+ tagName,
189
+ });
190
+ };
191
+
192
+ const borderColor = isHighlightedScreen
193
+ ? '#10B981'
194
+ : isActiveScreen
195
+ ? '#38BDF8'
196
+ : 'rgba(255, 255, 255, 0.09)';
197
+
198
+ return (
199
+ <div
200
+ style={{
201
+ position: 'absolute',
202
+ left: position.x,
203
+ top: position.y,
204
+ width: position.width,
205
+ userSelect: 'none',
206
+ zIndex: isActiveScreen || isHighlightedScreen ? 20 : 10,
207
+ }}
208
+ onPointerDown={(e) => {
209
+ // Prevent background canvas pan when interacting inside an artboard
210
+ e.stopPropagation();
211
+ onSelectScreen(screen.id);
212
+ }}
213
+ >
214
+ {/* Minimalist Architectural Artboard Title Header */}
215
+ <div
216
+ onPointerDown={handleHeaderPointerDown}
217
+ style={{
218
+ height: 30,
219
+ marginBottom: 8,
220
+ display: 'flex',
221
+ alignItems: 'center',
222
+ justifyContent: 'space-between',
223
+ padding: '0 4px',
224
+ cursor: isDraggingHeader ? 'grabbing' : 'grab',
225
+ color: isActiveScreen ? '#38BDF8' : '#94A3B8',
226
+ }}
227
+ >
228
+ <div style={{ display: 'flex', alignItems: 'center', gap: 7 }}>
229
+ <GripHorizontal size={14} style={{ opacity: 0.7 }} />
230
+ <span
231
+ style={{
232
+ fontSize: 13,
233
+ fontWeight: 600,
234
+ letterSpacing: '-0.015em',
235
+ color: isActiveScreen ? '#F8FAFC' : '#CBD5E1',
236
+ }}
237
+ >
238
+ {screen.name}
239
+ </span>
240
+ <span
241
+ style={{
242
+ fontSize: 11,
243
+ fontFamily: 'var(--font-mono)',
244
+ color: '#64748B',
245
+ }}
246
+ >
247
+ /{screen.routeName}
248
+ </span>
249
+ </div>
250
+
251
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
252
+ <span
253
+ className="tabular-nums"
254
+ style={{
255
+ fontSize: 10.5,
256
+ fontFamily: 'var(--font-mono)',
257
+ color: '#64748B',
258
+ }}
259
+ >
260
+ {position.width} × {position.height}
261
+ </span>
262
+ <button
263
+ type="button"
264
+ className="zelune-interactive"
265
+ onClick={(e) => {
266
+ e.stopPropagation();
267
+ onFocusScreen(screen.id);
268
+ }}
269
+ title="Center screen in viewport"
270
+ style={{
271
+ display: 'inline-flex',
272
+ alignItems: 'center',
273
+ justifyContent: 'center',
274
+ width: 22,
275
+ height: 22,
276
+ borderRadius: 5,
277
+ border: '1px solid var(--studio-border-subtle)',
278
+ backgroundColor: 'var(--studio-surface-2)',
279
+ color: '#94A3B8',
280
+ cursor: 'pointer',
281
+ }}
282
+ >
283
+ <Maximize2 size={11} />
284
+ </button>
285
+ </div>
286
+ </div>
287
+
288
+ {/* Pure Rectangular Screen Artboard (No Device Bezel/Frame, Soft Diffusion Shadows) */}
289
+ <div
290
+ ref={viewportRef}
291
+ onPointerMoveCapture={handlePointerMoveCapture}
292
+ onPointerLeave={handlePointerLeave}
293
+ onClickCapture={handleClickCapture}
294
+ style={{
295
+ position: 'relative',
296
+ width: position.width,
297
+ height: position.height,
298
+ backgroundColor: '#0A0B0E',
299
+ border: `1px solid ${borderColor}`,
300
+ borderRadius: 6,
301
+ overflow: 'hidden',
302
+ boxShadow: isHighlightedScreen
303
+ ? '0 0 0 3px rgba(16, 185, 129, 0.28), 0 28px 56px -14px rgba(0, 0, 0, 0.78)'
304
+ : isActiveScreen
305
+ ? '0 0 0 2px rgba(56, 189, 248, 0.22), 0 28px 56px -14px rgba(0, 0, 0, 0.78)'
306
+ : '0 22px 44px -15px rgba(0, 0, 0, 0.68)',
307
+ transition: 'border-color var(--duration-micro) var(--ease-out), box-shadow var(--duration-micro) var(--ease-out)',
308
+ display: 'flex',
309
+ flexDirection: 'column',
310
+ cursor: mode === 'inspect' ? 'crosshair' : 'default',
311
+ }}
312
+ >
313
+ {screen.parseError ? (
314
+ <div
315
+ style={{
316
+ flex: 1,
317
+ padding: 24,
318
+ backgroundColor: '#18181B',
319
+ color: '#FCA5A5',
320
+ display: 'flex',
321
+ flexDirection: 'column',
322
+ justifyContent: 'center',
323
+ gap: 10,
324
+ fontFamily: 'var(--font-mono)',
325
+ }}
326
+ >
327
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8, color: '#F87171', fontWeight: 700, fontSize: 13 }}>
328
+ <AlertTriangle size={16} />
329
+ <span>AST Syntax Error in {screen.filePath}</span>
330
+ </div>
331
+ <div style={{ fontSize: 11, color: '#FECACA', whiteSpace: 'pre-wrap' }}>
332
+ {screen.parseError}
333
+ </div>
334
+ </div>
335
+ ) : ScreenComponent ? (
336
+ <ScreenErrorBoundary screenName={screen.name}>
337
+ <MockNavigationProvider
338
+ routeName={screen.routeName}
339
+ onNavigate={onNavigateFlow}
340
+ >
341
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column', height: '100%', width: '100%' }}>
342
+ <ScreenComponent />
343
+ </div>
344
+ </MockNavigationProvider>
345
+ </ScreenErrorBoundary>
346
+ ) : (
347
+ <div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#64748B', fontSize: 13 }}>
348
+ Mounting {screen.name}...
349
+ </div>
350
+ )}
351
+
352
+ {/* Figma-style Hover Bounding Box in Inspect Mode */}
353
+ {mode === 'inspect' &&
354
+ hoverBox &&
355
+ (!selectedBox || hoverBox.astPath !== selectedBox.astPath) && (
356
+ <div
357
+ className="zelune-box-overlay"
358
+ style={{
359
+ position: 'absolute',
360
+ top: hoverBox.top,
361
+ left: hoverBox.left,
362
+ width: hoverBox.width,
363
+ height: hoverBox.height,
364
+ border: '1px dashed #38BDF8',
365
+ backgroundColor: 'rgba(56, 189, 248, 0.06)',
366
+ pointerEvents: 'none',
367
+ zIndex: 40,
368
+ }}
369
+ >
370
+ <span
371
+ className="tabular-nums"
372
+ style={{
373
+ position: 'absolute',
374
+ top: -19,
375
+ left: -1,
376
+ backgroundColor: '#0284C7',
377
+ color: '#FFFFFF',
378
+ fontSize: 10,
379
+ fontWeight: 600,
380
+ fontFamily: 'var(--font-mono)',
381
+ padding: '1px 5px',
382
+ borderRadius: 3,
383
+ whiteSpace: 'nowrap',
384
+ }}
385
+ >
386
+ &lt;{hoverBox.tagName}&gt;
387
+ </span>
388
+ </div>
389
+ )}
390
+
391
+ {/* Figma-style Active Selection Bounding Box */}
392
+ {selectedBox && selectedElement?.screenId === screen.id && (
393
+ <div
394
+ className="zelune-box-overlay"
395
+ style={{
396
+ position: 'absolute',
397
+ top: selectedBox.top,
398
+ left: selectedBox.left,
399
+ width: selectedBox.width,
400
+ height: selectedBox.height,
401
+ border: '1.5px solid #6366F1',
402
+ backgroundColor: 'rgba(99, 102, 241, 0.08)',
403
+ pointerEvents: 'none',
404
+ zIndex: 45,
405
+ }}
406
+ >
407
+ {/* Corner resize handles aesthetic */}
408
+ {[
409
+ { top: -3.5, left: -3.5 },
410
+ { top: -3.5, right: -3.5 },
411
+ { bottom: -3.5, left: -3.5 },
412
+ { bottom: -3.5, right: -3.5 },
413
+ ].map((pos, idx) => (
414
+ <span
415
+ key={idx}
416
+ style={{
417
+ position: 'absolute',
418
+ ...pos,
419
+ width: 6,
420
+ height: 6,
421
+ backgroundColor: '#FFFFFF',
422
+ border: '1.5px solid #4F46E5',
423
+ borderRadius: 1,
424
+ }}
425
+ />
426
+ ))}
427
+ <span
428
+ className="tabular-nums"
429
+ style={{
430
+ position: 'absolute',
431
+ top: -21,
432
+ left: -1,
433
+ backgroundColor: '#4F46E5',
434
+ color: '#FFFFFF',
435
+ fontSize: 10,
436
+ fontWeight: 700,
437
+ fontFamily: 'var(--font-mono)',
438
+ padding: '2px 6px',
439
+ borderRadius: 3,
440
+ whiteSpace: 'nowrap',
441
+ boxShadow: '0 2px 6px rgba(0,0,0,0.4)',
442
+ }}
443
+ >
444
+ &lt;{selectedBox.tagName}&gt; · L{selectedElement.line}
445
+ </span>
446
+ </div>
447
+ )}
448
+ </div>
449
+ </div>
450
+ );
451
+ };
@@ -0,0 +1,23 @@
1
+ import type { InspectedElement, NavigationEdge, ScreenMetadata } from '../server/astEngine';
2
+
3
+ export type CanvasMode = 'inspect' | 'interact';
4
+
5
+ export interface ArtboardPosition {
6
+ x: number;
7
+ y: number;
8
+ width: number;
9
+ height: number;
10
+ }
11
+
12
+ export type ArtboardLayoutMap = Record<string, ArtboardPosition>;
13
+
14
+ export interface SelectedElementRef {
15
+ screenId: string;
16
+ file: string;
17
+ astPath: string;
18
+ line: number;
19
+ col: number;
20
+ tagName: string;
21
+ }
22
+
23
+ export type { InspectedElement, NavigationEdge, ScreenMetadata };
package/src/main.tsx ADDED
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import { CanvasApp } from './canvas/CanvasApp';
4
+ import './styles.css';
5
+
6
+ ReactDOM.createRoot(document.getElementById('root')!).render(
7
+ <React.StrictMode>
8
+ <CanvasApp />
9
+ </React.StrictMode>
10
+ );
@@ -0,0 +1,199 @@
1
+ import React, { createContext, useContext, useMemo, useEffect } from 'react';
2
+ import { TouchableOpacity, View } from 'react-native';
3
+
4
+ export interface NavigationController {
5
+ navigate: (targetRoute: string, params?: Record<string, unknown>) => void;
6
+ goBack: () => void;
7
+ push: (targetRoute: string, params?: Record<string, unknown>) => void;
8
+ replace: (targetRoute: string, params?: Record<string, unknown>) => void;
9
+ back: () => void;
10
+ setOptions: (options?: Record<string, unknown>) => void;
11
+ canGoBack: () => boolean;
12
+ }
13
+
14
+ interface MockNavigationContextValue {
15
+ currentRoute: string;
16
+ params: Record<string, unknown>;
17
+ navigation: NavigationController;
18
+ }
19
+
20
+ function normalizeNavTarget(raw: string): string {
21
+ const cleaned = String(raw ?? '')
22
+ .replace(/^\/+/, '')
23
+ .replace(/\([^)]+\)\/?/g, '')
24
+ .replace(/Screen$/, '')
25
+ .replace(/\/index$/, '');
26
+ if (!cleaned || cleaned === 'index') return 'Home';
27
+ if (cleaned.includes('-') || cleaned.includes('/') || cleaned === cleaned.toLowerCase()) {
28
+ return cleaned
29
+ .split(/[-_/]/)
30
+ .filter(Boolean)
31
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
32
+ .join('');
33
+ }
34
+ return cleaned;
35
+ }
36
+
37
+ const MockNavigationContext = createContext<MockNavigationContextValue>({
38
+ currentRoute: 'Home',
39
+ params: {},
40
+ navigation: {
41
+ navigate: () => undefined,
42
+ goBack: () => undefined,
43
+ push: () => undefined,
44
+ replace: () => undefined,
45
+ back: () => undefined,
46
+ setOptions: () => undefined,
47
+ canGoBack: () => true,
48
+ },
49
+ });
50
+
51
+ export interface MockNavigationProviderProps {
52
+ routeName: string;
53
+ params?: Record<string, unknown>;
54
+ onNavigate?: (fromScreen: string, toScreen: string, params?: Record<string, unknown>) => void;
55
+ children: React.ReactNode;
56
+ }
57
+
58
+ export const MockNavigationProvider: React.FC<MockNavigationProviderProps> = ({
59
+ routeName,
60
+ params = {},
61
+ onNavigate,
62
+ children,
63
+ }) => {
64
+ const value = useMemo<MockNavigationContextValue>(() => {
65
+ const trigger = (targetRoute: string, navParams?: Record<string, unknown>) => {
66
+ onNavigate?.(routeName, normalizeNavTarget(targetRoute), navParams);
67
+ };
68
+ return {
69
+ currentRoute: routeName,
70
+ params,
71
+ navigation: {
72
+ navigate: trigger,
73
+ push: trigger,
74
+ replace: trigger,
75
+ goBack: () => onNavigate?.(routeName, 'Home'),
76
+ back: () => onNavigate?.(routeName, 'Home'),
77
+ setOptions: () => undefined,
78
+ canGoBack: () => true,
79
+ },
80
+ };
81
+ }, [routeName, params, onNavigate]);
82
+
83
+ return (
84
+ <MockNavigationContext.Provider value={value}>
85
+ {children}
86
+ </MockNavigationContext.Provider>
87
+ );
88
+ };
89
+
90
+ // React Navigation Shims (@react-navigation/native)
91
+ export function useNavigation(): NavigationController {
92
+ return useContext(MockNavigationContext).navigation;
93
+ }
94
+
95
+ export function useRoute(): { key: string; name: string; params: Record<string, unknown> } {
96
+ const ctx = useContext(MockNavigationContext);
97
+ return {
98
+ key: ctx.currentRoute,
99
+ name: ctx.currentRoute,
100
+ params: ctx.params,
101
+ };
102
+ }
103
+
104
+ export function useFocusEffect(effect: () => void | (() => void)): void {
105
+ useEffect(() => {
106
+ return effect();
107
+ }, [effect]);
108
+ }
109
+
110
+ export function useIsFocused(): boolean {
111
+ return true;
112
+ }
113
+
114
+ export const NavigationContainer: React.FC<{ children?: React.ReactNode }> = ({ children }) => (
115
+ <>{children}</>
116
+ );
117
+
118
+ // Expo Router Shims (expo-router)
119
+ export function useRouter(): NavigationController {
120
+ return useContext(MockNavigationContext).navigation;
121
+ }
122
+
123
+ export const router: NavigationController = {
124
+ navigate: () => undefined,
125
+ push: () => undefined,
126
+ replace: () => undefined,
127
+ goBack: () => undefined,
128
+ back: () => undefined,
129
+ setOptions: () => undefined,
130
+ canGoBack: () => true,
131
+ };
132
+
133
+ export function useLocalSearchParams<T extends Record<string, unknown> = Record<string, string>>(): T {
134
+ const ctx = useContext(MockNavigationContext);
135
+ return ctx.params as T;
136
+ }
137
+
138
+ export function useGlobalSearchParams<T extends Record<string, unknown> = Record<string, string>>(): T {
139
+ const ctx = useContext(MockNavigationContext);
140
+ return ctx.params as T;
141
+ }
142
+
143
+ export function usePathname(): string {
144
+ const ctx = useContext(MockNavigationContext);
145
+ return `/${ctx.currentRoute.toLowerCase()}`;
146
+ }
147
+
148
+ export function useSegments(): string[] {
149
+ const ctx = useContext(MockNavigationContext);
150
+ return [ctx.currentRoute.toLowerCase()];
151
+ }
152
+
153
+ export const Link: React.FC<{
154
+ href: string | { pathname: string; params?: Record<string, unknown> };
155
+ asChild?: boolean;
156
+ children?: React.ReactNode;
157
+ style?: unknown;
158
+ }> = ({ href, children, style }) => {
159
+ const nav = useContext(MockNavigationContext).navigation;
160
+ const target = typeof href === 'string' ? href : href.pathname;
161
+ return (
162
+ <TouchableOpacity
163
+ activeOpacity={0.78}
164
+ style={style as never}
165
+ onPress={() => nav.push(target)}
166
+ >
167
+ {children}
168
+ </TouchableOpacity>
169
+ );
170
+ };
171
+
172
+ const NoopLayoutScreen: React.FC<{ options?: unknown }> & {
173
+ Screen: React.FC<{ name?: string; options?: unknown }>;
174
+ } = ({ children }: { children?: React.ReactNode }) => <>{children}</>;
175
+ NoopLayoutScreen.Screen = () => null;
176
+
177
+ export const Stack = NoopLayoutScreen;
178
+ export const Tabs = NoopLayoutScreen;
179
+ export const Slot: React.FC<{ children?: React.ReactNode }> = ({ children }) => <>{children}</>;
180
+
181
+ // React Native Safe Area Context Shims (react-native-safe-area-context)
182
+ export const SafeAreaProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) => (
183
+ <View style={{ flex: 1 }}>{children}</View>
184
+ );
185
+
186
+ export const SafeAreaView: React.FC<{
187
+ children?: React.ReactNode;
188
+ style?: unknown;
189
+ edges?: string[];
190
+ }> = ({ children, style }) => (
191
+ <View style={[{ flex: 1 }, style as never]}>{children}</View>
192
+ );
193
+
194
+ export function useSafeAreaInsets(): { top: number; right: number; bottom: number; left: number } {
195
+ return { top: 0, right: 0, bottom: 0, left: 0 };
196
+ }
197
+
198
+ // Expo Status Bar Shim (expo-status-bar)
199
+ export const StatusBar: React.FC<{ style?: string; hidden?: boolean; backgroundColor?: string }> = () => null;