zelune 1.5.5 → 1.5.7

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,26 @@
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.7] - 2026-10-24
6
+
7
+ ### Added
8
+ - **Universal NativeWind v2/v4 (`styled`, `cssInterop`, `remapProps`), `twrnc` (`tw`, `createTw`), & TypeScript `tailwind.config.ts` Support (`src/runtime/nativewindShim.ts`, `src/runtime/MockNavigationContext.tsx`, `vite.config.ts`)**:
9
+ - Added `styled(Component, baseClassName)` (NativeWind v2), `cssInterop` / `remapProps` (NativeWind v4), and `tw` / `createTw` (`twrnc` tagged templates and `.style()` helper) to `MockNavigationContext.tsx` so any React Native project worldwide using NativeWind v2, NativeWind v4, or `twrnc` works out-of-the-box.
10
+ - Added dynamic `fontFamily` token resolution from `window.__ZELUNE_TAILWIND_THEME__.fontFamily` in `src/runtime/nativewindShim.ts` and TypeScript type-stripping (`const config: Config =`, `satisfies Config`, `as const`) in `loadProjectTailwindTheme` (`vite.config.ts`).
11
+
12
+ ## [1.5.6] - 2026-10-24
13
+
14
+ ### Added
15
+ - **NativeWind / Tailwind CSS `className` Runtime Engine (`src/runtime/nativewindShim.ts`, `src/runtime/reactNativeShim.ts`, `src/runtime/MockNavigationContext.tsx`, `vite.config.ts`)**:
16
+ - Automatically loads the target project's `tailwind.config.js` / `tailwind.config.ts` (`colors`, `fontSize`, `borderRadius`, `spacing`, `opacity`, `fontFamily`) in `vite.config.ts` and injects `window.__ZELUNE_TAILWIND_THEME__`.
17
+ - Added `resolveTailwindClassNameToStyle` and `withNativeWind` wrapper across all React Native and Expo primitives (`View`, `Text`, `Pressable`, `TouchableOpacity`, `ScrollView`, `FlatList`, `KeyboardAvoidingView`, `TextInput`, `Image`, `SafeAreaView`, `LinearGradient`, `BlurView`, `createAnimatedComponent`) so screens and UI components styled with NativeWind `className="..."` (including M3 semantic tokens like `bg-surface-container-high`, `text-on-surface`, `text-headline-md`, `text-title-sm`, `bg-primary/10`, etc.) render with full CSS styles instead of unstyled stacks.
18
+
19
+ ### Fixed
20
+ - **Pinned Mobile Artboard `Dimensions` (`src/runtime/reactNativeShim.ts`, `src/runtime/MockNavigationContext.tsx`)**: Overrode `Dimensions.get('window' | 'screen')` and `useWindowDimensions()` to return `{ width: 393, height: 852, scale: 3, fontScale: 1 }` instead of the desktop browser window's `1440×900` dimensions, and upgraded the `expo-image` `<Image>` shim to use CSS `object-fit` inside an `overflow: hidden` container so carousel cards and hero images (e.g., on `HomeScreen`) size to exact 393×852 mobile proportions.
21
+ - **Wide Zoom-Out Range (`MIN_ZOOM = 0.08`), Multi-Row Grid Layout & Zero-Re-Render Pinch Zoom (`src/canvas/CanvasApp.tsx`)**:
22
+ - Lowered `MIN_ZOOM` from `25%` (`0.25`) to `8%` (`0.08`), updated `buildDefaultLayout` ("Grid" button) to arrange 6+ screens into a balanced multi-row grid, and enabled automatic fit-all on initial load so all screens can be viewed at once.
23
+ - Updated `applyFocalZoom` to keep full floating-point precision during active trackpad pinch/Ctrl-wheel zoom (eliminating subpixel focal-point oscillation), mutate `worldContainerRef.current.style.transform` and `zoomLabelRef.current.textContent` directly on the DOM without triggering 120Hz React re-renders of `<ScreenArtboard />`, and removed `willChange: 'transform'` bitmap caching so zooming in and out is completely smooth and vibration-free.
24
+
5
25
  ## [1.5.5] - 2026-10-23
6
26
 
7
27
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zelune",
3
- "version": "1.5.5",
3
+ "version": "1.5.7",
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.5",
4
+ "version": "1.5.6",
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": [
@@ -495,7 +495,7 @@ export function viteReactNativeFallbackPlugin(
495
495
  ` });`,
496
496
  ` }`,
497
497
  ` const content = typeof props?.children === 'function' ? props.children(SAFE_CHAINABLE_PROXY) : props?.children;`,
498
- ` return React.createElement(View, { style: props?.style, ref }, content);`,
498
+ ` return React.createElement(View, { style: props?.style, className: props?.className, ref }, content);`,
499
499
  ` });`,
500
500
  ` Comp.displayName = displayName;`,
501
501
  ` return new Proxy(Comp, {`,
@@ -36,13 +36,14 @@ import type { PatchPayload, ReorderPayload } from '../server/astEngine';
36
36
  const DEFAULT_WIDTH = 393;
37
37
  const DEFAULT_HEIGHT = 852;
38
38
  const ARTBOARD_GAP = 130;
39
+ const ARTBOARD_ROW_GAP = 140;
39
40
  const INITIAL_OFFSET_X = 80;
40
41
  const INITIAL_OFFSET_Y = 70;
41
- const MIN_ZOOM = 0.25;
42
- const MAX_ZOOM = 1.75;
42
+ const MIN_ZOOM = 0.08;
43
+ const MAX_ZOOM = 2.0;
43
44
 
44
45
  function clampZoom(z: number): number {
45
- return Number(Math.min(Math.max(z, MIN_ZOOM), MAX_ZOOM).toFixed(3));
46
+ return Math.min(Math.max(z, MIN_ZOOM), MAX_ZOOM);
46
47
  }
47
48
 
48
49
  function snapToDevicePixel(v: number): number {
@@ -52,10 +53,16 @@ function snapToDevicePixel(v: number): number {
52
53
 
53
54
  function buildDefaultLayout(screens: ScreenMetadata[]): ArtboardLayoutMap {
54
55
  const map: ArtboardLayoutMap = {};
56
+ const cols =
57
+ screens.length <= 5
58
+ ? Math.max(1, screens.length)
59
+ : Math.min(7, Math.ceil(Math.sqrt(screens.length * 1.6)));
55
60
  screens.forEach((screen, idx) => {
61
+ const col = idx % cols;
62
+ const row = Math.floor(idx / cols);
56
63
  map[screen.id] = {
57
- x: INITIAL_OFFSET_X + idx * (DEFAULT_WIDTH + ARTBOARD_GAP),
58
- y: INITIAL_OFFSET_Y,
64
+ x: INITIAL_OFFSET_X + col * (DEFAULT_WIDTH + ARTBOARD_GAP),
65
+ y: INITIAL_OFFSET_Y + row * (DEFAULT_HEIGHT + ARTBOARD_ROW_GAP),
59
66
  width: DEFAULT_WIDTH,
60
67
  height: DEFAULT_HEIGHT,
61
68
  };
@@ -134,19 +141,43 @@ export const CanvasApp: React.FC = () => {
134
141
 
135
142
  const canvasViewportRef = useRef<HTMLDivElement | null>(null);
136
143
  const worldContainerRef = useRef<HTMLDivElement | null>(null);
144
+ const zoomLabelRef = useRef<HTMLSpanElement | null>(null);
137
145
  const cameraTimerRef = useRef<number | null>(null);
138
146
  const panSyncTimerRef = useRef<number | null>(null);
147
+ const hasInitialFitRef = useRef<boolean>(false);
139
148
 
140
149
  // Refs for synchronous access inside non-passive native wheel/gesture listeners
141
150
  const zoomRef = useRef<number>(zoom);
142
151
  const panRef = useRef<{ x: number; y: number }>(pan);
143
152
  useEffect(() => {
144
153
  zoomRef.current = zoom;
154
+ if (zoomLabelRef.current) {
155
+ zoomLabelRef.current.textContent = `${Math.round(zoom * 100)}%`;
156
+ }
145
157
  }, [zoom]);
146
158
  useEffect(() => {
147
159
  panRef.current = pan;
148
160
  }, [pan]);
149
161
 
162
+ const scheduleCameraStateSync = useCallback((commitImmediately = false) => {
163
+ if (panSyncTimerRef.current) {
164
+ window.clearTimeout(panSyncTimerRef.current);
165
+ panSyncTimerRef.current = null;
166
+ }
167
+ if (commitImmediately) {
168
+ setIsCameraAnimating(false);
169
+ setZoom(zoomRef.current);
170
+ setPan(panRef.current);
171
+ } else {
172
+ panSyncTimerRef.current = window.setTimeout(() => {
173
+ panSyncTimerRef.current = null;
174
+ setIsCameraAnimating(false);
175
+ setZoom(zoomRef.current);
176
+ setPan(panRef.current);
177
+ }, 140);
178
+ }
179
+ }, []);
180
+
150
181
  const applyDirectPanTransform = useCallback(
151
182
  (rawX: number, rawY: number, commitImmediately = false) => {
152
183
  const nextPan = {
@@ -161,23 +192,9 @@ export const CanvasApp: React.FC = () => {
161
192
  worldEl.style.transform = `translate3d(${nextPan.x}px, ${nextPan.y}px, 0) scale(${zoomRef.current})`;
162
193
  }
163
194
 
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
- }
195
+ scheduleCameraStateSync(commitImmediately);
179
196
  },
180
- []
197
+ [scheduleCameraStateSync]
181
198
  );
182
199
 
183
200
  const triggerCameraTransition = useCallback(() => {
@@ -231,7 +248,34 @@ export const CanvasApp: React.FC = () => {
231
248
  setLayout((prev) => {
232
249
  const base = buildDefaultLayout(sorted);
233
250
  const saved = layoutJson.ok && layoutJson.layout ? layoutJson.layout : {};
234
- return { ...base, ...saved, ...prev };
251
+ const merged = { ...base, ...saved, ...prev };
252
+ if (!hasInitialFitRef.current && sorted.length > 0 && canvasViewportRef.current) {
253
+ hasInitialFitRef.current = true;
254
+ const rect = canvasViewportRef.current.getBoundingClientRect();
255
+ const positions = Object.values(merged);
256
+ if (positions.length > 0 && rect.width > 0 && rect.height > 0) {
257
+ const minX = Math.min(...positions.map((p) => p.x));
258
+ const minY = Math.min(...positions.map((p) => p.y));
259
+ const maxX = Math.max(...positions.map((p) => p.x + p.width));
260
+ const maxY = Math.max(...positions.map((p) => p.y + p.height + 50));
261
+ const contentWidth = maxX - minX + 140;
262
+ const contentHeight = maxY - minY + 140;
263
+ const nextZoom = clampZoom(
264
+ Math.min(rect.width / contentWidth, rect.height / contentHeight, 0.85)
265
+ );
266
+ const centerX = (minX + maxX) / 2;
267
+ const centerY = (minY + maxY) / 2;
268
+ const nextPan = {
269
+ x: Math.round(rect.width / 2 - centerX * nextZoom),
270
+ y: Math.round(rect.height / 2 - centerY * nextZoom),
271
+ };
272
+ zoomRef.current = nextZoom;
273
+ panRef.current = nextPan;
274
+ setZoom(nextZoom);
275
+ setPan(nextPan);
276
+ }
277
+ }
278
+ return merged;
235
279
  });
236
280
  }
237
281
  } catch (err) {
@@ -496,25 +540,27 @@ export const CanvasApp: React.FC = () => {
496
540
  if (nextZoom === prevZoom) return;
497
541
 
498
542
  // Pin the world coordinate directly under the user's cursor during two-finger pinch
543
+ // Keep full floating-point precision during active zoom gesture so (cursorX - worldX * nextZoom) never oscillates
499
544
  const cursorX = clientX - rect.left;
500
545
  const cursorY = clientY - rect.top;
501
546
  const worldX = (cursorX - prevPan.x) / prevZoom;
502
547
  const worldY = (cursorY - prevPan.y) / prevZoom;
503
548
 
504
549
  const nextPan = {
505
- x: snapToDevicePixel(cursorX - worldX * nextZoom),
506
- y: snapToDevicePixel(cursorY - worldY * nextZoom),
550
+ x: cursorX - worldX * nextZoom,
551
+ y: cursorY - worldY * nextZoom,
507
552
  };
508
553
 
509
- setIsCameraAnimating(false);
510
554
  zoomRef.current = nextZoom;
511
555
  panRef.current = nextPan;
512
556
  if (worldContainerRef.current) {
513
557
  worldContainerRef.current.style.transition = 'none';
514
558
  worldContainerRef.current.style.transform = `translate3d(${nextPan.x}px, ${nextPan.y}px, 0) scale(${nextZoom})`;
515
559
  }
516
- setZoom(nextZoom);
517
- setPan(nextPan);
560
+ if (zoomLabelRef.current) {
561
+ zoomLabelRef.current.textContent = `${Math.round(nextZoom * 100)}%`;
562
+ }
563
+ scheduleCameraStateSync(false);
518
564
  };
519
565
 
520
566
  // 1. Non-passive wheel listener on the canvas viewport for 0ms pan & focal pinch-zoom
@@ -579,7 +625,7 @@ export const CanvasApp: React.FC = () => {
579
625
  document.removeEventListener('gesturechange', handleGestureChange);
580
626
  document.removeEventListener('gestureend', handleGestureEnd);
581
627
  };
582
- }, [applyDirectPanTransform]);
628
+ }, [applyDirectPanTransform, scheduleCameraStateSync]);
583
629
 
584
630
  // Center viewport on a specific screen artboard using Emil Kowalski ease-out-expo camera physics
585
631
  const focusOnScreen = useCallback(
@@ -604,48 +650,58 @@ export const CanvasApp: React.FC = () => {
604
650
  );
605
651
 
606
652
  // Fit all screens in viewport
607
- const handleFitAllScreens = useCallback(() => {
608
- const viewport = canvasViewportRef.current;
609
- if (!viewport || screens.length === 0) return;
610
- const rect = viewport.getBoundingClientRect();
653
+ const fitCameraToLayout = useCallback(
654
+ (targetLayout: ArtboardLayoutMap) => {
655
+ const viewport = canvasViewportRef.current;
656
+ if (!viewport) return;
657
+ const rect = viewport.getBoundingClientRect();
611
658
 
612
- const positions = Object.values(layout);
613
- if (positions.length === 0) return;
659
+ const positions = Object.values(targetLayout);
660
+ if (positions.length === 0) return;
614
661
 
615
- const minX = Math.min(...positions.map((p) => p.x));
616
- const minY = Math.min(...positions.map((p) => p.y));
617
- const maxX = Math.max(...positions.map((p) => p.x + p.width));
618
- const maxY = Math.max(...positions.map((p) => p.y + p.height + 50));
662
+ const minX = Math.min(...positions.map((p) => p.x));
663
+ const minY = Math.min(...positions.map((p) => p.y));
664
+ const maxX = Math.max(...positions.map((p) => p.x + p.width));
665
+ const maxY = Math.max(...positions.map((p) => p.y + p.height + 50));
619
666
 
620
- const contentWidth = maxX - minX + 140;
621
- const contentHeight = maxY - minY + 140;
667
+ const contentWidth = maxX - minX + 140;
668
+ const contentHeight = maxY - minY + 140;
622
669
 
623
- const scaleX = rect.width / contentWidth;
624
- const scaleY = rect.height / contentHeight;
625
- const nextZoom = Number(Math.min(Math.max(Math.min(scaleX, scaleY), 0.28), 1.0).toFixed(2));
670
+ const scaleX = rect.width / contentWidth;
671
+ const scaleY = rect.height / contentHeight;
672
+ const nextZoom = clampZoom(Math.min(scaleX, scaleY, 1.0));
626
673
 
627
- const centerX = (minX + maxX) / 2;
628
- const centerY = (minY + maxY) / 2;
674
+ const centerX = (minX + maxX) / 2;
675
+ const centerY = (minY + maxY) / 2;
676
+ const nextPan = {
677
+ x: Math.round(rect.width / 2 - centerX * nextZoom),
678
+ y: Math.round(rect.height / 2 - centerY * nextZoom),
679
+ };
629
680
 
630
- triggerCameraTransition();
631
- setZoom(nextZoom);
632
- setPan({
633
- x: Math.round(rect.width / 2 - centerX * nextZoom),
634
- y: Math.round(rect.height / 2 - centerY * nextZoom),
635
- });
636
- }, [layout, screens.length, triggerCameraTransition]);
681
+ triggerCameraTransition();
682
+ zoomRef.current = nextZoom;
683
+ panRef.current = nextPan;
684
+ setZoom(nextZoom);
685
+ setPan(nextPan);
686
+ },
687
+ [triggerCameraTransition]
688
+ );
689
+
690
+ const handleFitAllScreens = useCallback(() => {
691
+ fitCameraToLayout(layout);
692
+ }, [fitCameraToLayout, layout]);
637
693
 
638
- // Reset layout to clean horizontal grid
694
+ // Reset layout to clean multi-row grid and fit in viewport
639
695
  const handleResetGridLayout = useCallback(() => {
640
696
  const reset = buildDefaultLayout(screens);
641
- triggerCameraTransition();
642
697
  setLayout(reset);
698
+ fitCameraToLayout(reset);
643
699
  void fetch('/api/layout', {
644
700
  method: 'POST',
645
701
  headers: { 'Content-Type': 'application/json' },
646
702
  body: JSON.stringify(reset),
647
703
  });
648
- }, [screens, triggerCameraTransition]);
704
+ }, [screens, fitCameraToLayout]);
649
705
 
650
706
  // Move single artboard
651
707
  const handleMoveArtboard = useCallback((screenId: string, nextX: number, nextY: number) => {
@@ -962,6 +1018,7 @@ export const CanvasApp: React.FC = () => {
962
1018
  </button>
963
1019
 
964
1020
  <span
1021
+ ref={zoomLabelRef}
965
1022
  className="tabular-nums"
966
1023
  style={{
967
1024
  minWidth: 46,
@@ -1355,8 +1412,6 @@ export const CanvasApp: React.FC = () => {
1355
1412
  left: 0,
1356
1413
  transform: `translate3d(${pan.x}px, ${pan.y}px, 0) scale(${zoom})`,
1357
1414
  transformOrigin: '0 0',
1358
- willChange: 'transform',
1359
- backfaceVisibility: 'hidden',
1360
1415
  transition: isCameraAnimating
1361
1416
  ? 'transform var(--duration-camera) var(--ease-out-expo)'
1362
1417
  : 'none',
@@ -11,6 +11,7 @@ import {
11
11
  ScrollView as RNScrollView,
12
12
  FlatList as RNFlatList,
13
13
  } from 'react-native';
14
+ import { withNativeWind, resolveTailwindClassNameToStyle } from './nativewindShim';
14
15
 
15
16
  export interface NavigationController {
16
17
  navigate: (targetRoute: string, params?: Record<string, unknown>) => void;
@@ -291,16 +292,26 @@ export const SplashScreen = {
291
292
  // ============================================================================
292
293
  // 3. React Native Safe Area Context Shims (react-native-safe-area-context)
293
294
  // ============================================================================
294
- export const SafeAreaProvider: React.FC<{ children?: React.ReactNode; style?: unknown }> = ({
295
- children,
296
- style,
297
- }) => <View style={[{ flex: 1 }, style as never]}>{children}</View>;
295
+ export const SafeAreaProvider: React.FC<{
296
+ children?: React.ReactNode;
297
+ style?: unknown;
298
+ className?: string;
299
+ }> = ({ children, style, className, ...rest }) => (
300
+ <View className={className as never} style={[{ flex: 1 }, style as never]} {...rest}>
301
+ {children}
302
+ </View>
303
+ );
298
304
 
299
305
  export const SafeAreaView: React.FC<{
300
306
  children?: React.ReactNode;
301
307
  style?: unknown;
308
+ className?: string;
302
309
  edges?: string[];
303
- }> = ({ children, style }) => <View style={[{ flex: 1 }, style as never]}>{children}</View>;
310
+ }> = ({ children, style, className, edges: _edges, ...rest }) => (
311
+ <View className={className as never} style={[{ flex: 1 }, style as never]} {...rest}>
312
+ {children}
313
+ </View>
314
+ );
304
315
 
305
316
  export function useSafeAreaInsets(): { top: number; right: number; bottom: number; left: number } {
306
317
  return { top: 0, right: 0, bottom: 0, left: 0 };
@@ -478,16 +489,27 @@ export function createIconSetFromFontello() {
478
489
  // 6. Expo Linear Gradient, Blur, Image & FlashList Shims
479
490
  // ============================================================================
480
491
  export const LinearGradient: React.FC<{
481
- colors?: string[];
482
- locations?: number[];
492
+ colors?: readonly string[] | string[];
493
+ locations?: readonly number[] | number[];
483
494
  start?: { x: number; y: number } | [number, number];
484
495
  end?: { x: number; y: number } | [number, number];
485
496
  style?: unknown;
497
+ className?: string;
486
498
  children?: React.ReactNode;
487
- }> = ({ colors = ['#1E293B', '#0F172A'], style, children }) => {
499
+ }> = ({
500
+ colors = ['#1E293B', '#0F172A'],
501
+ locations: _locations,
502
+ start: _start,
503
+ end: _end,
504
+ style,
505
+ className,
506
+ children,
507
+ ...rest
508
+ }) => {
488
509
  const safeColors = Array.isArray(colors) && colors.length >= 2 ? colors : ['#1E293B', '#0F172A'];
489
510
  return (
490
511
  <View
512
+ className={className as never}
491
513
  style={[
492
514
  {
493
515
  backgroundColor: safeColors[0],
@@ -495,6 +517,7 @@ export const LinearGradient: React.FC<{
495
517
  } as never,
496
518
  style as never,
497
519
  ]}
520
+ {...rest}
498
521
  >
499
522
  {children}
500
523
  </View>
@@ -505,11 +528,13 @@ export const BlurView: React.FC<{
505
528
  intensity?: number;
506
529
  tint?: string;
507
530
  style?: unknown;
531
+ className?: string;
508
532
  children?: React.ReactNode;
509
- }> = ({ intensity = 40, tint = 'dark', style, children }) => {
533
+ }> = ({ intensity = 40, tint = 'dark', style, className, children, ...rest }) => {
510
534
  const bg = tint === 'light' ? 'rgba(255, 255, 255, 0.65)' : 'rgba(15, 23, 42, 0.65)';
511
535
  return (
512
536
  <View
537
+ className={className as never}
513
538
  style={[
514
539
  {
515
540
  backgroundColor: bg,
@@ -517,6 +542,7 @@ export const BlurView: React.FC<{
517
542
  } as never,
518
543
  style as never,
519
544
  ]}
545
+ {...rest}
520
546
  >
521
547
  {children}
522
548
  </View>
@@ -527,15 +553,52 @@ export const Image: React.FC<Record<string, unknown>> = ({
527
553
  contentFit,
528
554
  resizeMode,
529
555
  source,
556
+ placeholder,
557
+ transition: _transition,
558
+ className,
559
+ style,
530
560
  ...rest
531
561
  }) => {
532
- const resolvedSource = typeof source === 'string' ? { uri: source } : source;
562
+ let uri = '';
563
+ if (typeof source === 'string') {
564
+ uri = source;
565
+ } else if (Array.isArray(source) && source[0] && typeof source[0] === 'object') {
566
+ uri = String((source[0] as Record<string, unknown>).uri ?? '');
567
+ } else if (source && typeof source === 'object') {
568
+ uri = String((source as Record<string, unknown>).uri ?? '');
569
+ }
570
+ const placeholderBg =
571
+ placeholder && typeof placeholder === 'object' && 'color' in placeholder
572
+ ? String((placeholder as Record<string, unknown>).color ?? 'transparent')
573
+ : undefined;
574
+ const fit = String(contentFit ?? resizeMode ?? 'cover') as React.CSSProperties['objectFit'];
575
+
533
576
  return (
534
- <RNImage
535
- source={resolvedSource as never}
536
- resizeMode={(contentFit ?? resizeMode ?? 'cover') as never}
577
+ <View
578
+ className={className as never}
579
+ style={[
580
+ {
581
+ overflow: 'hidden',
582
+ ...(placeholderBg ? { backgroundColor: placeholderBg } : {}),
583
+ } as never,
584
+ style as never,
585
+ ]}
537
586
  {...(rest as Record<string, unknown>)}
538
- />
587
+ >
588
+ {uri ? (
589
+ <img
590
+ src={uri}
591
+ alt=""
592
+ draggable={false}
593
+ style={{
594
+ width: '100%',
595
+ height: '100%',
596
+ objectFit: fit,
597
+ display: 'block',
598
+ }}
599
+ />
600
+ ) : null}
601
+ </View>
539
602
  );
540
603
  };
541
604
 
@@ -730,7 +793,9 @@ export const Easing = {
730
793
  };
731
794
 
732
795
  export function createAnimatedComponent<P extends object>(Component: React.ComponentType<P>) {
733
- return Component;
796
+ return withNativeWind(
797
+ Component as unknown as React.ComponentType<Record<string, unknown>>
798
+ ) as unknown as React.ComponentType<P>;
734
799
  }
735
800
 
736
801
  export const Animated = {
@@ -755,10 +820,15 @@ export const Animated = {
755
820
  };
756
821
 
757
822
  // React Native Gesture Handler Shims
758
- export const GestureHandlerRootView: React.FC<{ children?: React.ReactNode; style?: unknown }> = ({
759
- children,
760
- style,
761
- }) => <View style={[{ flex: 1 }, style as never]}>{children}</View>;
823
+ export const GestureHandlerRootView: React.FC<{
824
+ children?: React.ReactNode;
825
+ style?: unknown;
826
+ className?: string;
827
+ }> = ({ children, style, className, ...rest }) => (
828
+ <View className={className as never} style={[{ flex: 1 }, style as never]} {...rest}>
829
+ {children}
830
+ </View>
831
+ );
762
832
 
763
833
  export const ScrollView = RNScrollView;
764
834
  export const FlatList = RNFlatList;
@@ -1151,6 +1221,53 @@ export const clsx = (...args: unknown[]) =>
1151
1221
  export const cn = clsx;
1152
1222
  export const twMerge = clsx;
1153
1223
 
1224
+ // NativeWind (v2 / v4) & twrnc Universal Shims
1225
+ export function styled<P extends Record<string, unknown>>(
1226
+ Component: React.ComponentType<P>,
1227
+ baseClassName?: string
1228
+ ): React.ComponentType<P> {
1229
+ const Wrapped = withNativeWind(Component as unknown as React.ComponentType<Record<string, unknown>>);
1230
+ if (!baseClassName) return Wrapped as unknown as React.ComponentType<P>;
1231
+ return React.forwardRef<unknown, P>((props, ref) => {
1232
+ const combinedClass = [baseClassName, props?.className].filter(Boolean).join(' ');
1233
+ return React.createElement(Wrapped, { ...props, className: combinedClass, ref });
1234
+ }) as unknown as React.ComponentType<P>;
1235
+ }
1236
+
1237
+ export function cssInterop<P extends Record<string, unknown>>(
1238
+ Component: React.ComponentType<P>
1239
+ ): React.ComponentType<P> {
1240
+ return withNativeWind(Component as unknown as React.ComponentType<Record<string, unknown>>) as unknown as React.ComponentType<P>;
1241
+ }
1242
+
1243
+ export const remapProps = cssInterop;
1244
+
1245
+ function twFn(stringsOrClass: TemplateStringsArray | string, ...values: unknown[]): Record<string, unknown> {
1246
+ if (typeof stringsOrClass === 'string') {
1247
+ return resolveTailwindClassNameToStyle(stringsOrClass) ?? {};
1248
+ }
1249
+ if (Array.isArray(stringsOrClass)) {
1250
+ const built = stringsOrClass.reduce(
1251
+ (acc, str, idx) => acc + str + (values[idx] !== undefined ? String(values[idx]) : ''),
1252
+ ''
1253
+ );
1254
+ return resolveTailwindClassNameToStyle(built) ?? {};
1255
+ }
1256
+ return {};
1257
+ }
1258
+
1259
+ twFn.style = (...args: unknown[]) => {
1260
+ const mergedClass = args.filter((a): a is string => typeof a === 'string' && a.length > 0).join(' ');
1261
+ return resolveTailwindClassNameToStyle(mergedClass) ?? {};
1262
+ };
1263
+ twFn.color = (token: string) => {
1264
+ const s = resolveTailwindClassNameToStyle(`text-${token}`);
1265
+ return (s?.color as string | undefined) ?? '#0F172A';
1266
+ };
1267
+
1268
+ export const tw = twFn;
1269
+ export const createTw = () => twFn;
1270
+
1154
1271
  // React Hook Form Shims (react-hook-form)
1155
1272
  export function useForm(config?: { defaultValues?: Record<string, unknown> }) {
1156
1273
  const defaults = config?.defaultValues ?? {};