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 +20 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/plugins/viteReactNativeFallbackPlugin.ts +1 -1
- package/src/canvas/CanvasApp.tsx +112 -57
- package/src/runtime/MockNavigationContext.tsx +136 -19
- package/src/runtime/nativewindShim.ts +997 -0
- package/src/runtime/reactNativeShim.ts +99 -0
- package/vite.config.ts +61 -0
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
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
|
+
"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, {`,
|
package/src/canvas/CanvasApp.tsx
CHANGED
|
@@ -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.
|
|
42
|
-
const MAX_ZOOM =
|
|
42
|
+
const MIN_ZOOM = 0.08;
|
|
43
|
+
const MAX_ZOOM = 2.0;
|
|
43
44
|
|
|
44
45
|
function clampZoom(z: number): number {
|
|
45
|
-
return
|
|
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 +
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
506
|
-
y:
|
|
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
|
-
|
|
517
|
-
|
|
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
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
653
|
+
const fitCameraToLayout = useCallback(
|
|
654
|
+
(targetLayout: ArtboardLayoutMap) => {
|
|
655
|
+
const viewport = canvasViewportRef.current;
|
|
656
|
+
if (!viewport) return;
|
|
657
|
+
const rect = viewport.getBoundingClientRect();
|
|
611
658
|
|
|
612
|
-
|
|
613
|
-
|
|
659
|
+
const positions = Object.values(targetLayout);
|
|
660
|
+
if (positions.length === 0) return;
|
|
614
661
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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
|
-
|
|
621
|
-
|
|
667
|
+
const contentWidth = maxX - minX + 140;
|
|
668
|
+
const contentHeight = maxY - minY + 140;
|
|
622
669
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
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
|
-
|
|
628
|
-
|
|
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
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
}
|
|
636
|
-
|
|
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
|
|
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,
|
|
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<{
|
|
295
|
-
children
|
|
296
|
-
style
|
|
297
|
-
|
|
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
|
|
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
|
-
}> = ({
|
|
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
|
-
|
|
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
|
-
<
|
|
535
|
-
|
|
536
|
-
|
|
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
|
|
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<{
|
|
759
|
-
children
|
|
760
|
-
style
|
|
761
|
-
|
|
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 ?? {};
|