zelune 1.5.4 → 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 CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  All notable changes to the **Zelune — React Native Visual Canvas & MCP** project are documented in this file.
4
4
 
5
+ ## [1.5.5] - 2026-10-23
6
+
7
+ ### Added
8
+ - **Zod, React Hook Form & Sizing Utility Shims (`src/runtime/MockNavigationContext.tsx`, `plugins/viteReactNativeFallbackPlugin.ts`)**: Added `createSafeChainableProxy()` / `SAFE_CHAINABLE_PROXY` (wrapping a callable function target in a recursive `Proxy` for unlimited method/property chains), full Zod schema shims (`z`, `string`, `number`, `boolean`, `object`, `array`, `union`, `literal`, `record`, `tuple`, `any`, `unknown`, `optional`, `nullable`, `coerce`, `zodResolver`, `yupResolver`), React Hook Form shims (`useForm`, `useFormContext`, `FormProvider`, `useWatch`, `useFieldArray`, `Controller` supporting `render={({ field, fieldState, formState }) => ...}`), and React Native responsive sizing/classname helpers (`scale`, `verticalScale`, `moderateScale`, `RFValue`, `RFPercentage`, `wp`, `hp`, `clsx`, `cn`, `twMerge`).
9
+ - **Universal Unmounted React Context Fallback (`src/runtime/MockNavigationContext.tsx`, `src/main.tsx`)**: Wrapped `React.createContext` and `React.useContext` (after `react-native-web` initializes its internal contexts) with `SAFE_CONTEXT_FALLBACK` (pre-populated with preview `user`, `profile`, `session`, `isAuthenticated: true`, `isLoading: false`, `colors`, `items: []`, `signIn`, `signOut`, `logout`, `refresh`, etc.) so screens rendered outside their root `app/_layout.tsx` providers never throw `useAuth must be used within an AuthProvider` (or `useTheme` / `useCart` / `useNotifications` equivalents).
10
+
11
+ ### Fixed
12
+ - **Eliminated Canvas Panning Vibration / Jitter (`src/canvas/CanvasApp.tsx`, `src/canvas/ScreenArtboard.tsx`, `src/canvas/FlowArrowsLayer.tsx`)**:
13
+ - Updated `handleCanvasWheel` and `handleCanvasPointerDown` in `CanvasApp.tsx` to mutate `worldContainerRef.current.style.transform` (`translate3d(...) scale(...)`) directly on the DOM node with device-pixel rounding (`snapToDevicePixel`) and debounce `setPan` state synchronization to 120ms after movement stops, eliminating 120Hz React re-renders of all artboards while panning.
14
+ - Wrapped `ScreenArtboard` and `FlowArrowsLayer` in `React.memo`, scoped `selectedElement` and `inspectedElement` props to only the active screen, and suppressed `handlePointerMoveCapture` (`computeRelativeBox` / `setHoverBox`) while the canvas is moving or pointer buttons are pressed.
15
+ - Stabilized `useFocusEffect`, `useSegments`, `useQuery`, `useInfiniteQuery`, `useMutation`, and `useQueryClient` in `MockNavigationContext.tsx` so screens with inline `useFocusEffect` or `useEffect` dependencies do not trigger continuous re-render loops.
16
+ - **Light-Themed Screen Root Background & Image Overflow (`src/canvas/ScreenArtboard.tsx`)**: Set `backgroundColor: '#FFFFFF'`, `color: '#0F172A'`, `overflowY: 'auto'`, `overflowX: 'hidden'`, `minWidth: 0`, and `minHeight: 0` on the inner `<ScreenComponent />` mount container so screens that rely on React Native's default white root window background render with proper contrast and horizontally overflowing images are clipped to the 393px device frame.
17
+
5
18
  ## [1.5.4] - 2026-10-23
6
19
 
7
20
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zelune",
3
- "version": "1.5.4",
3
+ "version": "1.5.5",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "type": "module",
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "zelune",
3
3
  "displayName": "Zelune — React Native Visual Canvas",
4
- "version": "1.5.4",
4
+ "version": "1.5.5",
5
5
  "description": "Zelune MCP & Figma-style multi-screen React Native visual canvas with live surgical AST patching and navigation flow visualization for Google Antigravity.",
6
6
  "logo": "assets/logo.svg",
7
7
  "suggestedPrompts": [
@@ -484,53 +484,18 @@ export function viteReactNativeFallbackPlugin(
484
484
  `import { View } from 'react-native';`,
485
485
  `export * from ${JSON.stringify(normalizedShimPath)};`,
486
486
  `import * as KnownShims from ${JSON.stringify(normalizedShimPath)};`,
487
- `import DefaultShim from ${JSON.stringify(normalizedShimPath)};`,
488
- `function createSafeProxy() {`,
489
- ` const fnTarget = function(...args) {`,
490
- ` if (args.length === 1 && (typeof args[0] === 'number' || typeof args[0] === 'string')) {`,
491
- ` return args[0];`,
492
- ` }`,
493
- ` return proxy;`,
494
- ` };`,
495
- ` const arrTarget = [];`,
496
- ` const proxy = new Proxy(arrTarget, {`,
497
- ` get(target, prop) {`,
498
- ` if (prop === 'length') return 0;`,
499
- ` if (prop === Symbol.iterator) {`,
500
- ` return function* () {`,
501
- ` yield proxy;`,
502
- ` yield fnTarget;`,
503
- ` };`,
504
- ` }`,
505
- ` if (prop === Symbol.toPrimitive) {`,
506
- ` return (hint) => (hint === 'number' ? 0 : '');`,
507
- ` }`,
508
- ` if (prop === 'valueOf') return () => 0;`,
509
- ` if (prop === 'toString') return () => '';`,
510
- ` if (prop === 'then') {`,
511
- ` return (onFulfilled, onRejected) => Promise.resolve({ data: null, error: null }).then(onFulfilled, onRejected);`,
512
- ` }`,
513
- ` if (prop === 'catch') return () => Promise.resolve({ data: null, error: null });`,
514
- ` if (prop === 'finally') return (cb) => Promise.resolve({ data: null, error: null }).finally(cb);`,
515
- ` if (prop === 'isLoading' || prop === 'loading' || prop === 'isPending' || prop === 'isError' || prop === 'disabled') return false;`,
516
- ` if (prop === 'isReady' || prop === 'granted' || prop === 'isSuccess' || prop === ' isAuthenticated') return true;`,
517
- ` if (prop === 'error' || prop === 'user' || prop === 'session') return null;`,
518
- ` if (prop === 'status') return 'granted';`,
519
- ` if (prop === 'data' || prop === 'items' || prop === 'results' || prop === 'list') return [];`,
520
- ` if (prop === 'ref') return { current: null };`,
521
- ` if (prop === 'value') return 0;`,
522
- ` if (prop === 'map' || prop === 'filter' || prop === 'slice' || prop === 'flatMap') return () => [];`,
523
- ` if (prop === 'forEach') return () => undefined;`,
524
- ` if (prop === 'reduce') return (_fn, init) => init;`,
525
- ` if (prop in target) return Reflect.get(target, prop);`,
526
- ` return fnTarget;`,
527
- ` },`,
528
- ` });`,
529
- ` return proxy;`,
530
- `}`,
487
+ `import DefaultShim, { SAFE_CHAINABLE_PROXY } from ${JSON.stringify(normalizedShimPath)};`,
531
488
  `function createUniversalComponentStub(displayName) {`,
532
489
  ` const Comp = React.forwardRef(function UniversalNativeStub(props, ref) {`,
533
- ` return React.createElement(View, { style: props?.style, ref }, props?.children);`,
490
+ ` if (typeof props?.render === 'function') {`,
491
+ ` return props.render({`,
492
+ ` field: { name: props?.name ?? '', value: props?.defaultValue ?? '', onChange: () => undefined, onBlur: () => undefined, ref: () => undefined },`,
493
+ ` fieldState: { invalid: false, isTouched: false, isDirty: false, error: undefined },`,
494
+ ` formState: { errors: {}, isSubmitting: false, isValid: true },`,
495
+ ` });`,
496
+ ` }`,
497
+ ` const content = typeof props?.children === 'function' ? props.children(SAFE_CHAINABLE_PROXY) : props?.children;`,
498
+ ` return React.createElement(View, { style: props?.style, ref }, content);`,
534
499
  ` });`,
535
500
  ` Comp.displayName = displayName;`,
536
501
  ` return new Proxy(Comp, {`,
@@ -539,21 +504,16 @@ export function viteReactNativeFallbackPlugin(
539
504
  ` if (typeof prop === 'string' && /^[A-Z]/.test(prop)) {`,
540
505
  ` return createUniversalComponentStub(displayName + '.' + prop);`,
541
506
  ` }`,
542
- ` return () => createSafeProxy();`,
507
+ ` return SAFE_CHAINABLE_PROXY;`,
543
508
  ` },`,
544
509
  ` });`,
545
510
  `}`,
546
511
  `function resolveStubExport(name) {`,
547
512
  ` if (name in KnownShims) return KnownShims[name];`,
548
513
  ` if (DefaultShim && name in DefaultShim) return DefaultShim[name];`,
549
- ` if (/^use[A-Z]/.test(name)) return () => createSafeProxy();`,
514
+ ` if (/^use[A-Z]/.test(name)) return () => SAFE_CHAINABLE_PROXY;`,
550
515
  ` if (/^[A-Z]/.test(name)) return createUniversalComponentStub(name);`,
551
- ` return (...args) => {`,
552
- ` if (args.length === 1 && (typeof args[0] === 'number' || typeof args[0] === 'string')) {`,
553
- ` return args[0];`,
554
- ` }`,
555
- ` return createSafeProxy();`,
556
- ` };`,
516
+ ` return SAFE_CHAINABLE_PROXY;`,
557
517
  `}`,
558
518
  `export default DefaultShim;`,
559
519
  ];
@@ -45,6 +45,11 @@ function clampZoom(z: number): number {
45
45
  return Number(Math.min(Math.max(z, MIN_ZOOM), MAX_ZOOM).toFixed(3));
46
46
  }
47
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
+
48
53
  function buildDefaultLayout(screens: ScreenMetadata[]): ArtboardLayoutMap {
49
54
  const map: ArtboardLayoutMap = {};
50
55
  screens.forEach((screen, idx) => {
@@ -128,7 +133,9 @@ export const CanvasApp: React.FC = () => {
128
133
  }>({ undoAvailable: 0, redoAvailable: 0 });
129
134
 
130
135
  const canvasViewportRef = useRef<HTMLDivElement | null>(null);
136
+ const worldContainerRef = useRef<HTMLDivElement | null>(null);
131
137
  const cameraTimerRef = useRef<number | null>(null);
138
+ const panSyncTimerRef = useRef<number | null>(null);
132
139
 
133
140
  // Refs for synchronous access inside non-passive native wheel/gesture listeners
134
141
  const zoomRef = useRef<number>(zoom);
@@ -140,6 +147,39 @@ export const CanvasApp: React.FC = () => {
140
147
  panRef.current = pan;
141
148
  }, [pan]);
142
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
+
143
183
  const triggerCameraTransition = useCallback(() => {
144
184
  if (cameraTimerRef.current) {
145
185
  window.clearTimeout(cameraTimerRef.current);
@@ -462,13 +502,17 @@ export const CanvasApp: React.FC = () => {
462
502
  const worldY = (cursorY - prevPan.y) / prevZoom;
463
503
 
464
504
  const nextPan = {
465
- x: cursorX - worldX * nextZoom,
466
- y: cursorY - worldY * nextZoom,
505
+ x: snapToDevicePixel(cursorX - worldX * nextZoom),
506
+ y: snapToDevicePixel(cursorY - worldY * nextZoom),
467
507
  };
468
508
 
469
509
  setIsCameraAnimating(false);
470
510
  zoomRef.current = nextZoom;
471
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
+ }
472
516
  setZoom(nextZoom);
473
517
  setPan(nextPan);
474
518
  };
@@ -483,13 +527,11 @@ export const CanvasApp: React.FC = () => {
483
527
  const zoomFactor = Math.exp(-e.deltaY * 0.0085);
484
528
  applyFocalZoom(zoomRef.current * zoomFactor, e.clientX, e.clientY);
485
529
  } else {
486
- setIsCameraAnimating(false);
487
- const nextPan = {
488
- x: panRef.current.x - e.deltaX,
489
- y: panRef.current.y - e.deltaY,
490
- };
491
- panRef.current = nextPan;
492
- setPan(nextPan);
530
+ applyDirectPanTransform(
531
+ panRef.current.x - e.deltaX,
532
+ panRef.current.y - e.deltaY,
533
+ false
534
+ );
493
535
  }
494
536
  };
495
537
 
@@ -537,7 +579,7 @@ export const CanvasApp: React.FC = () => {
537
579
  document.removeEventListener('gesturechange', handleGestureChange);
538
580
  document.removeEventListener('gestureend', handleGestureEnd);
539
581
  };
540
- }, []);
582
+ }, [applyDirectPanTransform]);
541
583
 
542
584
  // Center viewport on a specific screen artboard using Emil Kowalski ease-out-expo camera physics
543
585
  const focusOnScreen = useCallback(
@@ -630,7 +672,14 @@ export const CanvasApp: React.FC = () => {
630
672
  [focusOnScreen]
631
673
  );
632
674
 
633
- // 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)
634
683
  const handleCanvasPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
635
684
  if (e.button !== 0 && e.button !== 1) return;
636
685
  setIsCameraAnimating(false);
@@ -638,20 +687,20 @@ export const CanvasApp: React.FC = () => {
638
687
 
639
688
  const startX = e.clientX;
640
689
  const startY = e.clientY;
641
- const startPanX = pan.x;
642
- const startPanY = pan.y;
690
+ const startPanX = panRef.current.x;
691
+ const startPanY = panRef.current.y;
643
692
 
644
693
  const handleMove = (moveEvent: PointerEvent) => {
645
- const nextPan = {
646
- x: startPanX + (moveEvent.clientX - startX),
647
- y: startPanY + (moveEvent.clientY - startY),
648
- };
649
- panRef.current = nextPan;
650
- setPan(nextPan);
694
+ applyDirectPanTransform(
695
+ startPanX + (moveEvent.clientX - startX),
696
+ startPanY + (moveEvent.clientY - startY),
697
+ false
698
+ );
651
699
  };
652
700
 
653
701
  const handleUp = () => {
654
702
  setIsPanning(false);
703
+ applyDirectPanTransform(panRef.current.x, panRef.current.y, true);
655
704
  window.removeEventListener('pointermove', handleMove);
656
705
  window.removeEventListener('pointerup', handleUp);
657
706
  };
@@ -1299,6 +1348,7 @@ export const CanvasApp: React.FC = () => {
1299
1348
 
1300
1349
  {/* World Transform Container: Emil Kowalski ease-out-expo on camera focus, 0ms on direct drag/pinch */}
1301
1350
  <div
1351
+ ref={worldContainerRef}
1302
1352
  style={{
1303
1353
  position: 'absolute',
1304
1354
  top: 0,
@@ -1306,6 +1356,7 @@ export const CanvasApp: React.FC = () => {
1306
1356
  transform: `translate3d(${pan.x}px, ${pan.y}px, 0) scale(${zoom})`,
1307
1357
  transformOrigin: '0 0',
1308
1358
  willChange: 'transform',
1359
+ backfaceVisibility: 'hidden',
1309
1360
  transition: isCameraAnimating
1310
1361
  ? 'transform var(--duration-camera) var(--ease-out-expo)'
1311
1362
  : 'none',
@@ -1317,7 +1368,7 @@ export const CanvasApp: React.FC = () => {
1317
1368
  layout={layout}
1318
1369
  activeScreenId={activeScreenId}
1319
1370
  highlightedScreenId={highlightedScreenId}
1320
- onSelectFlow={(edge: NavigationEdge) => focusOnScreen(edge.toScreen)}
1371
+ onSelectFlow={handleSelectFlow}
1321
1372
  />
1322
1373
  )}
1323
1374
 
@@ -1329,6 +1380,7 @@ export const CanvasApp: React.FC = () => {
1329
1380
  height: DEFAULT_HEIGHT,
1330
1381
  };
1331
1382
  const ScreenComponent = SCREEN_COMPONENTS[screen.id];
1383
+ const isSelectedScreen = selectedElement?.screenId === screen.id;
1332
1384
 
1333
1385
  return (
1334
1386
  <ScreenArtboard
@@ -1340,8 +1392,9 @@ export const CanvasApp: React.FC = () => {
1340
1392
  mode={mode}
1341
1393
  isActiveScreen={activeScreenId === screen.id}
1342
1394
  isHighlightedScreen={highlightedScreenId === screen.id}
1343
- selectedElement={selectedElement}
1344
- inspectedElement={inspectedData}
1395
+ isCanvasMoving={isPanning}
1396
+ selectedElement={isSelectedScreen ? selectedElement : null}
1397
+ inspectedElement={isSelectedScreen ? inspectedData : null}
1345
1398
  onSelectElement={handleSelectElement}
1346
1399
  onSelectScreen={setActiveScreenId}
1347
1400
  onFocusScreen={focusOnScreen}
@@ -11,7 +11,7 @@ interface FlowArrowsLayerProps {
11
11
 
12
12
  const ARTBOARD_HEADER_OFFSET = 38;
13
13
 
14
- export const FlowArrowsLayer: React.FC<FlowArrowsLayerProps> = ({
14
+ export const FlowArrowsLayer: React.FC<FlowArrowsLayerProps> = React.memo(({
15
15
  screens,
16
16
  layout,
17
17
  activeScreenId,
@@ -194,4 +194,4 @@ export const FlowArrowsLayer: React.FC<FlowArrowsLayerProps> = ({
194
194
  })}
195
195
  </svg>
196
196
  );
197
- };
197
+ });
@@ -29,6 +29,7 @@ interface ScreenArtboardProps {
29
29
  mode: CanvasMode;
30
30
  isActiveScreen: boolean;
31
31
  isHighlightedScreen: boolean;
32
+ isCanvasMoving?: boolean;
32
33
  selectedElement: SelectedElementRef | null;
33
34
  inspectedElement: InspectedElement | null;
34
35
  onSelectElement: (ref: SelectedElementRef) => void;
@@ -83,7 +84,7 @@ function escapeAstPathSelector(astPath: string): string {
83
84
  : astPath.replace(/["\\]/g, '\\$&');
84
85
  }
85
86
 
86
- export const ScreenArtboard: React.FC<ScreenArtboardProps> = ({
87
+ export const ScreenArtboard: React.FC<ScreenArtboardProps> = React.memo(({
87
88
  screen,
88
89
  ScreenComponent,
89
90
  position,
@@ -91,6 +92,7 @@ export const ScreenArtboard: React.FC<ScreenArtboardProps> = ({
91
92
  mode,
92
93
  isActiveScreen,
93
94
  isHighlightedScreen,
95
+ isCanvasMoving = false,
94
96
  selectedElement,
95
97
  inspectedElement,
96
98
  onSelectElement,
@@ -732,8 +734,14 @@ export const ScreenArtboard: React.FC<ScreenArtboardProps> = ({
732
734
  };
733
735
 
734
736
  // Inspect Mode pointer handlers
737
+ useEffect(() => {
738
+ if (isCanvasMoving) {
739
+ setHoverBox(null);
740
+ }
741
+ }, [isCanvasMoving]);
742
+
735
743
  const handlePointerMoveCapture = (e: React.PointerEvent<HTMLDivElement>) => {
736
- if (mode !== 'inspect' || activeGizmoAction) return;
744
+ if (mode !== 'inspect' || activeGizmoAction || isCanvasMoving || e.buttons !== 0) return;
737
745
  const targetEl = findNearestAgyElement(e.target, viewportRef.current);
738
746
  if (!targetEl) {
739
747
  setHoverBox(null);
@@ -992,6 +1000,12 @@ export const ScreenArtboard: React.FC<ScreenArtboardProps> = ({
992
1000
  flexDirection: 'column',
993
1001
  height: '100%',
994
1002
  width: '100%',
1003
+ minHeight: 0,
1004
+ minWidth: 0,
1005
+ backgroundColor: '#FFFFFF',
1006
+ color: '#0F172A',
1007
+ overflowY: 'auto',
1008
+ overflowX: 'hidden',
995
1009
  }}
996
1010
  >
997
1011
  <ScreenComponent />
@@ -1474,4 +1488,4 @@ export const ScreenArtboard: React.FC<ScreenArtboardProps> = ({
1474
1488
  </div>
1475
1489
  </div>
1476
1490
  );
1477
- };
1491
+ });
package/src/main.tsx CHANGED
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
2
  import ReactDOM from 'react-dom/client';
3
+ import './runtime/MockNavigationContext';
3
4
  import { CanvasApp } from './canvas/CanvasApp';
4
5
  import './styles.css';
5
6
 
@@ -128,13 +128,17 @@ export function useRoute(): { key: string; name: string; params: Record<string,
128
128
  }
129
129
 
130
130
  export function useFocusEffect(effect: () => void | (() => void)): void {
131
+ const ctx = useContext(MockNavigationContext);
132
+ const effectRef = useRef(effect);
133
+ effectRef.current = effect;
131
134
  useEffect(() => {
132
135
  try {
133
- return effect();
136
+ const cleanup = effectRef.current();
137
+ return typeof cleanup === 'function' ? cleanup : undefined;
134
138
  } catch {
135
139
  return undefined;
136
140
  }
137
- }, [effect]);
141
+ }, [ctx.currentRoute]);
138
142
  }
139
143
 
140
144
  export function useIsFocused(): boolean {
@@ -242,7 +246,7 @@ export function usePathname(): string {
242
246
 
243
247
  export function useSegments(): string[] {
244
248
  const ctx = useContext(MockNavigationContext);
245
- return [ctx.currentRoute.toLowerCase()];
249
+ return useMemo(() => [ctx.currentRoute.toLowerCase()], [ctx.currentRoute]);
246
250
  }
247
251
 
248
252
  export function useRootNavigationState() {
@@ -973,53 +977,477 @@ export const QueryClientProvider: React.FC<{ client?: unknown; children?: React.
973
977
  children,
974
978
  }) => <>{children}</>;
975
979
 
980
+ const singletonQueryClient = new QueryClient();
981
+
976
982
  export function useQueryClient() {
977
- return new QueryClient();
983
+ return singletonQueryClient;
978
984
  }
979
985
 
986
+ const STABLE_EMPTY_ARRAY: unknown[] = [];
987
+
988
+ const STABLE_QUERY_RESULT = {
989
+ data: undefined,
990
+ error: null,
991
+ isLoading: false,
992
+ isPending: false,
993
+ isFetching: false,
994
+ isError: false,
995
+ isSuccess: true,
996
+ status: 'success',
997
+ refetch: async () => ({ data: undefined, error: null }),
998
+ };
999
+
980
1000
  export function useQuery() {
981
- return {
982
- data: undefined,
983
- error: null,
984
- isLoading: false,
985
- isPending: false,
986
- isFetching: false,
987
- isError: false,
988
- isSuccess: true,
989
- status: 'success',
990
- refetch: async () => ({ data: undefined, error: null }),
991
- };
992
- }
1001
+ return STABLE_QUERY_RESULT;
1002
+ }
1003
+
1004
+ const STABLE_INFINITE_QUERY_RESULT = {
1005
+ data: { pages: STABLE_EMPTY_ARRAY, pageParams: STABLE_EMPTY_ARRAY },
1006
+ error: null,
1007
+ isLoading: false,
1008
+ isPending: false,
1009
+ isFetching: false,
1010
+ isFetchingNextPage: false,
1011
+ hasNextPage: false,
1012
+ fetchNextPage: async () => ({}),
1013
+ refetch: async () => ({}),
1014
+ };
993
1015
 
994
1016
  export function useInfiniteQuery() {
995
- return {
996
- data: { pages: [], pageParams: [] },
997
- error: null,
998
- isLoading: false,
999
- isPending: false,
1000
- isFetching: false,
1001
- isFetchingNextPage: false,
1002
- hasNextPage: false,
1003
- fetchNextPage: async () => ({}),
1004
- refetch: async () => ({}),
1005
- };
1017
+ return STABLE_INFINITE_QUERY_RESULT;
1006
1018
  }
1007
1019
 
1020
+ const STABLE_MUTATION_RESULT = {
1021
+ mutate: () => undefined,
1022
+ mutateAsync: async () => ({}),
1023
+ reset: () => undefined,
1024
+ data: undefined,
1025
+ error: null,
1026
+ isLoading: false,
1027
+ isPending: false,
1028
+ isError: false,
1029
+ isSuccess: false,
1030
+ status: 'idle',
1031
+ };
1032
+
1008
1033
  export function useMutation() {
1034
+ return STABLE_MUTATION_RESULT;
1035
+ }
1036
+
1037
+ // ============================================================================
1038
+ // 11. Universal Chainable Proxy, Zod, React Hook Form & Context Fallbacks
1039
+ // ============================================================================
1040
+ export function createSafeChainableProxy(): unknown {
1041
+ const fnTarget = function (..._args: unknown[]) {
1042
+ return proxy;
1043
+ };
1044
+ const proxy: unknown = new Proxy(fnTarget, {
1045
+ get(_target, prop) {
1046
+ if (prop === Symbol.toPrimitive) {
1047
+ return (hint: string) => (hint === 'number' ? 0 : '');
1048
+ }
1049
+ if (prop === 'valueOf') return () => 0;
1050
+ if (prop === 'toString') return () => '';
1051
+ if (prop === Symbol.iterator) {
1052
+ return function* () {
1053
+ yield proxy;
1054
+ yield proxy;
1055
+ };
1056
+ }
1057
+ if (prop === 'then') {
1058
+ return (onFulfilled?: (v: unknown) => unknown, onRejected?: (e: unknown) => unknown) =>
1059
+ Promise.resolve({ data: null, error: null }).then(onFulfilled, onRejected);
1060
+ }
1061
+ if (prop === 'catch') return () => Promise.resolve({ data: null, error: null });
1062
+ if (prop === 'finally')
1063
+ return (cb?: () => void) => Promise.resolve({ data: null, error: null }).finally(cb);
1064
+ if (prop === 'safeParse' || prop === 'spa' || prop === 'safeParseAsync') {
1065
+ return (val?: unknown) => ({ success: true, data: val ?? {}, error: null });
1066
+ }
1067
+ if (prop === 'parse' || prop === 'parseAsync') {
1068
+ return (val?: unknown) => val ?? {};
1069
+ }
1070
+ if (
1071
+ prop === 'isLoading' ||
1072
+ prop === 'loading' ||
1073
+ prop === 'isPending' ||
1074
+ prop === 'isError' ||
1075
+ prop === 'disabled' ||
1076
+ prop === 'isSubmitting'
1077
+ ) {
1078
+ return false;
1079
+ }
1080
+ if (
1081
+ prop === 'isReady' ||
1082
+ prop === 'granted' ||
1083
+ prop === 'isSuccess' ||
1084
+ prop === 'isAuthenticated' ||
1085
+ prop === 'isValid'
1086
+ ) {
1087
+ return true;
1088
+ }
1089
+ if (prop === 'error' || prop === 'errors') return null;
1090
+ if (prop === 'status') return 'granted';
1091
+ if (prop === 'data' || prop === 'items' || prop === 'results' || prop === 'list') {
1092
+ return STABLE_EMPTY_ARRAY;
1093
+ }
1094
+ if (prop === 'length' || prop === 'size' || prop === 'count' || prop === 'value') return 0;
1095
+ if (prop === 'ref') return { current: null };
1096
+ if (prop === 'map' || prop === 'filter' || prop === 'slice' || prop === 'flatMap') {
1097
+ return () => STABLE_EMPTY_ARRAY;
1098
+ }
1099
+ if (prop === 'forEach') return () => undefined;
1100
+ if (prop === 'reduce') return (_fn: unknown, init: unknown) => init;
1101
+ if (prop === 'shape' || prop === '_def') return {};
1102
+ return proxy;
1103
+ },
1104
+ });
1105
+ return proxy;
1106
+ }
1107
+
1108
+ export const SAFE_CHAINABLE_PROXY = createSafeChainableProxy();
1109
+
1110
+ // Zod Shims (import { z } from 'zod' or import * as z from 'zod')
1111
+ export const z = SAFE_CHAINABLE_PROXY as Record<string, unknown>;
1112
+ export const string = () => SAFE_CHAINABLE_PROXY;
1113
+ export const number = () => SAFE_CHAINABLE_PROXY;
1114
+ export const boolean = () => SAFE_CHAINABLE_PROXY;
1115
+ export const object = () => SAFE_CHAINABLE_PROXY;
1116
+ export const array = () => SAFE_CHAINABLE_PROXY;
1117
+ export const union = () => SAFE_CHAINABLE_PROXY;
1118
+ export const literal = () => SAFE_CHAINABLE_PROXY;
1119
+ export const record = () => SAFE_CHAINABLE_PROXY;
1120
+ export const tuple = () => SAFE_CHAINABLE_PROXY;
1121
+ export const any = () => SAFE_CHAINABLE_PROXY;
1122
+ export const unknown = () => SAFE_CHAINABLE_PROXY;
1123
+ export const optional = () => SAFE_CHAINABLE_PROXY;
1124
+ export const nullable = () => SAFE_CHAINABLE_PROXY;
1125
+ export const coerce = SAFE_CHAINABLE_PROXY;
1126
+ export const zodResolver = () => async (values: unknown) => ({
1127
+ values: values ?? {},
1128
+ errors: {},
1129
+ });
1130
+ export const yupResolver = zodResolver;
1131
+
1132
+ // Common React Native Sizing & Classname Utility Shims
1133
+ export const scale = (size: number) => size;
1134
+ export const verticalScale = (size: number) => size;
1135
+ export const moderateScale = (size: number) => size;
1136
+ export const moderateVerticalScale = (size: number) => size;
1137
+ export const s = scale;
1138
+ export const vs = verticalScale;
1139
+ export const ms = moderateScale;
1140
+ export const mvs = moderateVerticalScale;
1141
+ export const RFValue = (size: number) => size;
1142
+ export const RFPercentage = (pct: number) => Math.round(pct * 8);
1143
+ export const widthPercentageToDP = (pct: number | string) =>
1144
+ typeof pct === 'number' ? Math.round((393 * pct) / 100) : 100;
1145
+ export const heightPercentageToDP = (pct: number | string) =>
1146
+ typeof pct === 'number' ? Math.round((852 * pct) / 100) : 100;
1147
+ export const wp = widthPercentageToDP;
1148
+ export const hp = heightPercentageToDP;
1149
+ export const clsx = (...args: unknown[]) =>
1150
+ args.filter((a): a is string => typeof a === 'string' && a.length > 0).join(' ');
1151
+ export const cn = clsx;
1152
+ export const twMerge = clsx;
1153
+
1154
+ // React Hook Form Shims (react-hook-form)
1155
+ export function useForm(config?: { defaultValues?: Record<string, unknown> }) {
1156
+ const defaults = config?.defaultValues ?? {};
1009
1157
  return {
1010
- mutate: () => undefined,
1011
- mutateAsync: async () => ({}),
1158
+ control: {},
1159
+ register: (name: string) => ({
1160
+ name,
1161
+ onChange: () => undefined,
1162
+ onBlur: () => undefined,
1163
+ ref: () => undefined,
1164
+ }),
1165
+ handleSubmit: (onValid?: (data: Record<string, unknown>) => unknown) => () =>
1166
+ onValid?.(defaults),
1167
+ watch: (name?: string) => (name ? defaults[name] ?? '' : defaults),
1168
+ getValues: (name?: string) => (name ? defaults[name] : defaults),
1169
+ setValue: () => undefined,
1012
1170
  reset: () => undefined,
1013
- data: undefined,
1014
- error: null,
1015
- isLoading: false,
1016
- isPending: false,
1017
- isError: false,
1018
- isSuccess: false,
1019
- status: 'idle',
1171
+ clearErrors: () => undefined,
1172
+ setError: () => undefined,
1173
+ trigger: async () => true,
1174
+ formState: {
1175
+ errors: {},
1176
+ isDirty: false,
1177
+ isLoading: false,
1178
+ isSubmitted: false,
1179
+ isSubmitting: false,
1180
+ isSubmitSuccessful: false,
1181
+ isValid: true,
1182
+ dirtyFields: {},
1183
+ touchedFields: {},
1184
+ },
1020
1185
  };
1021
1186
  }
1022
1187
 
1188
+ export const useFormContext = () => useForm();
1189
+ export const FormProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) => (
1190
+ <>{children}</>
1191
+ );
1192
+ export const useWatch = () => '';
1193
+ export const useFieldArray = () => ({
1194
+ fields: STABLE_EMPTY_ARRAY,
1195
+ append: () => undefined,
1196
+ prepend: () => undefined,
1197
+ remove: () => undefined,
1198
+ insert: () => undefined,
1199
+ move: () => undefined,
1200
+ update: () => undefined,
1201
+ replace: () => undefined,
1202
+ });
1203
+
1204
+ export const Controller: React.FC<{
1205
+ name?: string;
1206
+ defaultValue?: unknown;
1207
+ render?: (params: {
1208
+ field: {
1209
+ name: string;
1210
+ value: unknown;
1211
+ onChange: (...args: unknown[]) => void;
1212
+ onBlur: () => void;
1213
+ ref: () => void;
1214
+ };
1215
+ fieldState: { invalid: boolean; isTouched: boolean; isDirty: boolean; error: undefined };
1216
+ formState: { errors: Record<string, unknown>; isSubmitting: boolean; isValid: boolean };
1217
+ }) => React.ReactNode;
1218
+ children?: React.ReactNode;
1219
+ }> = ({ name = '', defaultValue = '', render, children }) => {
1220
+ if (typeof render === 'function') {
1221
+ return (
1222
+ <>
1223
+ {render({
1224
+ field: {
1225
+ name,
1226
+ value: defaultValue ?? '',
1227
+ onChange: () => undefined,
1228
+ onBlur: () => undefined,
1229
+ ref: () => undefined,
1230
+ },
1231
+ fieldState: { invalid: false, isTouched: false, isDirty: false, error: undefined },
1232
+ formState: { errors: {}, isSubmitting: false, isValid: true },
1233
+ })}
1234
+ </>
1235
+ );
1236
+ }
1237
+ return <>{children}</>;
1238
+ };
1239
+
1240
+ // Universal Unmounted Context Fallback (prevents "useAuth must be used within an AuthProvider")
1241
+ const previewUserBase: Record<string, unknown> = {
1242
+ id: 'usr_preview_01',
1243
+ uid: 'usr_preview_01',
1244
+ email: 'alex.morgan@example.com',
1245
+ name: 'Alex Morgan',
1246
+ fullName: 'Alex Morgan',
1247
+ full_name: 'Alex Morgan',
1248
+ firstName: 'Alex',
1249
+ first_name: 'Alex',
1250
+ lastName: 'Morgan',
1251
+ last_name: 'Morgan',
1252
+ displayName: 'Alex Morgan',
1253
+ display_name: 'Alex Morgan',
1254
+ username: 'alexmorgan',
1255
+ phone: '+1 (555) 019-2834',
1256
+ phoneNumber: '+1 (555) 019-2834',
1257
+ phone_number: '+1 (555) 019-2834',
1258
+ role: 'buyer',
1259
+ accountType: 'buyer',
1260
+ account_type: 'buyer',
1261
+ location: 'San Francisco, CA',
1262
+ city: 'San Francisco',
1263
+ bio: 'Product designer & automotive enthusiast.',
1264
+ avatar: '',
1265
+ avatarUrl: '',
1266
+ avatar_url: '',
1267
+ photoURL: '',
1268
+ profileImage: '',
1269
+ profile_image: '',
1270
+ isVerified: true,
1271
+ is_verified: true,
1272
+ verified: true,
1273
+ createdAt: '2025-01-15T10:00:00Z',
1274
+ created_at: '2025-01-15T10:00:00Z',
1275
+ user_metadata: {
1276
+ full_name: 'Alex Morgan',
1277
+ name: 'Alex Morgan',
1278
+ email: 'alex.morgan@example.com',
1279
+ role: 'buyer',
1280
+ avatar_url: '',
1281
+ },
1282
+ app_metadata: {
1283
+ provider: 'email',
1284
+ role: 'buyer',
1285
+ },
1286
+ };
1287
+
1288
+ const previewUserProxy = new Proxy(previewUserBase, {
1289
+ get(target, prop) {
1290
+ if (prop in target) return target[prop as keyof typeof target];
1291
+ if (prop === Symbol.toPrimitive) return () => 'Alex Morgan';
1292
+ if (typeof prop === 'string') return '';
1293
+ return undefined;
1294
+ },
1295
+ });
1296
+
1297
+ const previewColorsBase: Record<string, string> = {
1298
+ primary: '#2563EB',
1299
+ primaryLight: '#DBEAFE',
1300
+ secondary: '#64748B',
1301
+ accent: '#4F46E5',
1302
+ background: '#FFFFFF',
1303
+ backgroundSecondary: '#F8FAFC',
1304
+ surface: '#F8FAFC',
1305
+ card: '#FFFFFF',
1306
+ text: '#0F172A',
1307
+ textPrimary: '#0F172A',
1308
+ textSecondary: '#475569',
1309
+ textMuted: '#94A3B8',
1310
+ muted: '#94A3B8',
1311
+ placeholder: '#94A3B8',
1312
+ border: '#E2E8F0',
1313
+ divider: '#E2E8F0',
1314
+ error: '#EF4444',
1315
+ danger: '#EF4444',
1316
+ success: '#10B981',
1317
+ warning: '#F59E0B',
1318
+ info: '#3B82F6',
1319
+ white: '#FFFFFF',
1320
+ black: '#0F172A',
1321
+ tint: '#2563EB',
1322
+ icon: '#64748B',
1323
+ tabIconDefault: '#94A3B8',
1324
+ tabIconSelected: '#2563EB',
1325
+ notification: '#EF4444',
1326
+ };
1327
+
1328
+ const previewColorsProxy = new Proxy(previewColorsBase, {
1329
+ get(target, prop) {
1330
+ if (prop in target) return target[prop as keyof typeof target];
1331
+ return '#64748B';
1332
+ },
1333
+ });
1334
+
1335
+ const safeContextFallbackBase: Record<string, unknown> = {
1336
+ user: previewUserProxy,
1337
+ currentUser: previewUserProxy,
1338
+ profile: previewUserProxy,
1339
+ session: {
1340
+ access_token: 'preview-access-token',
1341
+ refresh_token: 'preview-refresh-token',
1342
+ user: previewUserProxy,
1343
+ },
1344
+ isAuthenticated: true,
1345
+ isLoggedIn: true,
1346
+ isSignedIn: true,
1347
+ isLoading: false,
1348
+ loading: false,
1349
+ isPending: false,
1350
+ isRefreshing: false,
1351
+ isInitialized: true,
1352
+ isReady: true,
1353
+ error: null,
1354
+ theme: 'light',
1355
+ colorScheme: 'light',
1356
+ isDark: false,
1357
+ dark: false,
1358
+ colors: previewColorsProxy,
1359
+ items: STABLE_EMPTY_ARRAY,
1360
+ cart: STABLE_EMPTY_ARRAY,
1361
+ favorites: STABLE_EMPTY_ARRAY,
1362
+ wishlist: STABLE_EMPTY_ARRAY,
1363
+ notifications: STABLE_EMPTY_ARRAY,
1364
+ bookings: STABLE_EMPTY_ARRAY,
1365
+ listings: STABLE_EMPTY_ARRAY,
1366
+ vehicles: STABLE_EMPTY_ARRAY,
1367
+ cars: STABLE_EMPTY_ARRAY,
1368
+ orders: STABLE_EMPTY_ARRAY,
1369
+ transactions: STABLE_EMPTY_ARRAY,
1370
+ messages: STABLE_EMPTY_ARRAY,
1371
+ chats: STABLE_EMPTY_ARRAY,
1372
+ unreadCount: 0,
1373
+ count: 0,
1374
+ total: 0,
1375
+ balance: 0,
1376
+ signIn: async () => ({ data: { user: previewUserProxy }, error: null }),
1377
+ signUp: async () => ({ data: { user: previewUserProxy }, error: null }),
1378
+ signOut: async () => ({ error: null }),
1379
+ login: async () => ({ data: { user: previewUserProxy }, error: null }),
1380
+ logout: async () => undefined,
1381
+ register: async () => ({ data: { user: previewUserProxy }, error: null }),
1382
+ refresh: async () => undefined,
1383
+ refetch: async () => undefined,
1384
+ updateProfile: async () => ({ error: null }),
1385
+ toggleTheme: () => undefined,
1386
+ setTheme: () => undefined,
1387
+ };
1388
+
1389
+ export const SAFE_CONTEXT_FALLBACK: Record<string, unknown> = new Proxy(safeContextFallbackBase, {
1390
+ get(target, prop) {
1391
+ if (prop in target) return target[prop as keyof typeof target];
1392
+ if (prop === Symbol.iterator) {
1393
+ return function* () {
1394
+ yield SAFE_CONTEXT_FALLBACK;
1395
+ yield () => undefined;
1396
+ };
1397
+ }
1398
+ return SAFE_CHAINABLE_PROXY;
1399
+ },
1400
+ });
1401
+
1402
+ // Install unmounted user-context fallback on React.createContext & React.useContext
1403
+ // (runs after react-native-web has already initialized its internal VirtualizedListContext)
1404
+ const origCreateContext = React.createContext;
1405
+ const origUseContext = React.useContext;
1406
+
1407
+ const wrappedCreateContext = function zeluneCreateContext(
1408
+ defaultValue: unknown,
1409
+ ...rest: unknown[]
1410
+ ) {
1411
+ const isEmptyDefault =
1412
+ defaultValue === undefined ||
1413
+ defaultValue === null ||
1414
+ (typeof defaultValue === 'object' &&
1415
+ defaultValue !== null &&
1416
+ Object.keys(defaultValue as object).length === 0);
1417
+ const effectiveDefault = isEmptyDefault ? SAFE_CONTEXT_FALLBACK : defaultValue;
1418
+ const ctx = (origCreateContext as (...a: unknown[]) => Record<string, unknown>).call(
1419
+ React,
1420
+ effectiveDefault,
1421
+ ...rest
1422
+ );
1423
+ if (ctx && typeof ctx === 'object' && isEmptyDefault) {
1424
+ ctx.__zeluneUserContext = true;
1425
+ }
1426
+ return ctx;
1427
+ } as unknown as typeof React.createContext;
1428
+
1429
+ const wrappedUseContext = function zeluneUseContext(
1430
+ Context: React.Context<unknown>,
1431
+ ...rest: unknown[]
1432
+ ) {
1433
+ const val = (origUseContext as (...a: unknown[]) => unknown).call(React, Context, ...rest);
1434
+ if (
1435
+ val === undefined ||
1436
+ (val === null &&
1437
+ Boolean((Context as unknown as Record<string, unknown>)?.__zeluneUserContext))
1438
+ ) {
1439
+ return SAFE_CONTEXT_FALLBACK;
1440
+ }
1441
+ return val;
1442
+ } as unknown as typeof React.useContext;
1443
+
1444
+ try {
1445
+ React.createContext = wrappedCreateContext;
1446
+ React.useContext = wrappedUseContext;
1447
+ } catch {
1448
+ // Ignore if React namespace is frozen in test environment
1449
+ }
1450
+
1023
1451
  function createSupabaseQueryBuilder(): Record<string, unknown> {
1024
1452
  const resultPromise = Promise.resolve({ data: [], error: null, count: 0 });
1025
1453
  const builder: Record<string, unknown> = new Proxy(
@@ -1146,6 +1574,29 @@ const defaultExportProperties = {
1146
1574
  useInfiniteQuery,
1147
1575
  useMutation,
1148
1576
  createClient,
1577
+ z,
1578
+ string,
1579
+ number,
1580
+ boolean,
1581
+ object,
1582
+ array,
1583
+ union,
1584
+ literal,
1585
+ record,
1586
+ tuple,
1587
+ any,
1588
+ unknown,
1589
+ optional,
1590
+ nullable,
1591
+ coerce,
1592
+ zodResolver,
1593
+ yupResolver,
1594
+ useForm,
1595
+ useFormContext,
1596
+ FormProvider,
1597
+ useWatch,
1598
+ useFieldArray,
1599
+ Controller,
1149
1600
  };
1150
1601
 
1151
1602
  Object.assign(UniversalDefaultExportComponent, defaultExportProperties);
@@ -1161,7 +1612,7 @@ const defaultExportProxy = new Proxy(UniversalDefaultExportComponent, {
1161
1612
  if (typeof prop === 'string' && /^[A-Z]/.test(prop)) {
1162
1613
  return createVectorIconComponent(prop);
1163
1614
  }
1164
- return () => undefined;
1615
+ return SAFE_CHAINABLE_PROXY;
1165
1616
  },
1166
1617
  });
1167
1618