@nebula-rn/components 0.0.1

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.
Files changed (56) hide show
  1. package/dist/Camera/index.d.ts +31 -0
  2. package/dist/Camera/index.js +96 -0
  3. package/dist/Map/index.d.ts +86 -0
  4. package/dist/Map/index.js +83 -0
  5. package/dist/Progress/index.d.ts +19 -0
  6. package/dist/Progress/index.js +82 -0
  7. package/dist/RichText/index.d.ts +7 -0
  8. package/dist/RichText/index.js +56 -0
  9. package/dist/Slider/index.d.ts +22 -0
  10. package/dist/Slider/index.js +58 -0
  11. package/dist/Swiper/carousel.d.ts +17 -0
  12. package/dist/Swiper/carousel.js +39 -0
  13. package/dist/Swiper/index.d.ts +19 -0
  14. package/dist/Swiper/index.js +15 -0
  15. package/dist/Swiper/pagination.d.ts +11 -0
  16. package/dist/Swiper/pagination.js +50 -0
  17. package/dist/Video/index.d.ts +42 -0
  18. package/dist/Video/index.js +168 -0
  19. package/dist/Video/utils.d.ts +3 -0
  20. package/dist/Video/utils.js +13 -0
  21. package/dist/WebView/index.d.ts +9 -0
  22. package/dist/WebView/index.js +6 -0
  23. package/dist/assets/loading.png +0 -0
  24. package/dist/assets/video/full.png +0 -0
  25. package/dist/assets/video/mute.png +0 -0
  26. package/dist/assets/video/pause.png +0 -0
  27. package/dist/assets/video/play.png +0 -0
  28. package/dist/assets/video/shrink.png +0 -0
  29. package/dist/assets/video/unmute.png +0 -0
  30. package/dist/assets/video/volume.png +0 -0
  31. package/dist/index.d.ts +8 -0
  32. package/dist/index.js +10 -0
  33. package/dist/utils/index.d.ts +4 -0
  34. package/dist/utils/index.js +8 -0
  35. package/package.json +58 -0
  36. package/src/Camera/index.tsx +179 -0
  37. package/src/Map/index.tsx +275 -0
  38. package/src/Progress/index.tsx +142 -0
  39. package/src/RichText/index.tsx +82 -0
  40. package/src/Slider/index.tsx +118 -0
  41. package/src/Swiper/carousel.tsx +119 -0
  42. package/src/Swiper/index.tsx +64 -0
  43. package/src/Swiper/pagination.tsx +70 -0
  44. package/src/Video/index.tsx +303 -0
  45. package/src/Video/utils.ts +14 -0
  46. package/src/WebView/index.tsx +25 -0
  47. package/src/assets/loading.png +0 -0
  48. package/src/assets/video/full.png +0 -0
  49. package/src/assets/video/mute.png +0 -0
  50. package/src/assets/video/pause.png +0 -0
  51. package/src/assets/video/play.png +0 -0
  52. package/src/assets/video/shrink.png +0 -0
  53. package/src/assets/video/unmute.png +0 -0
  54. package/src/assets/video/volume.png +0 -0
  55. package/src/index.ts +10 -0
  56. package/src/utils/index.ts +12 -0
@@ -0,0 +1,50 @@
1
+ import React from 'react';
2
+ import { StyleSheet, View } from 'react-native';
3
+ export const DefaultPagination = (props) => {
4
+ const { current, vertical, count, dotStyle, dotActiveStyle } = props;
5
+ const positionStyle = vertical ? 'paginationY' : 'paginationX';
6
+ const flexDirection = vertical ? 'column' : 'row';
7
+ const arr = [];
8
+ for (let i = 0; i < count; i++) {
9
+ arr.push(<View key={`dot-${i}`} style={[
10
+ styles.pointStyle,
11
+ styles.spaceStyle,
12
+ dotStyle,
13
+ i === current && styles.pointActiveStyle,
14
+ i === current && dotActiveStyle,
15
+ ]}/>);
16
+ }
17
+ return (<View style={[styles.pagination, styles[positionStyle]]}>
18
+ <View style={{ flexDirection }}>{arr}</View>
19
+ </View>);
20
+ };
21
+ const styles = StyleSheet.create({
22
+ pagination: {
23
+ position: 'absolute',
24
+ alignItems: 'center',
25
+ justifyContent: 'center',
26
+ },
27
+ paginationX: {
28
+ bottom: 10,
29
+ left: 0,
30
+ right: 0,
31
+ },
32
+ paginationY: {
33
+ right: 10,
34
+ top: 0,
35
+ bottom: 0,
36
+ },
37
+ pointStyle: {
38
+ width: 8,
39
+ height: 8,
40
+ borderRadius: 8,
41
+ backgroundColor: '#999',
42
+ },
43
+ pointActiveStyle: {
44
+ backgroundColor: '#333',
45
+ },
46
+ spaceStyle: {
47
+ marginHorizontal: 2.5,
48
+ marginVertical: 3,
49
+ },
50
+ });
@@ -0,0 +1,42 @@
1
+ import { FC, ReactNode } from 'react';
2
+ import { StyleProp, ViewStyle } from 'react-native';
3
+ export interface VideoTimeUpdateEvent {
4
+ currentTime: number;
5
+ duration: number;
6
+ }
7
+ export interface VideoFullscreenChangeEvent {
8
+ direction: 'vertical' | 'horizontal';
9
+ fullScreen: boolean;
10
+ }
11
+ export interface VideoMetaDataEvent {
12
+ width: number;
13
+ height: number;
14
+ duration: number;
15
+ durationMillis?: number;
16
+ }
17
+ export interface VideoErrorEvent {
18
+ errMsg: string;
19
+ }
20
+ export interface VideoProps {
21
+ src: string;
22
+ duration?: number;
23
+ controls?: boolean;
24
+ autoplay?: boolean;
25
+ loop?: boolean;
26
+ muted?: boolean;
27
+ initialTime?: number;
28
+ objectFit?: 'contain' | 'fill' | 'cover';
29
+ poster?: string;
30
+ showCenterPlayBtn?: boolean;
31
+ style?: StyleProp<ViewStyle>;
32
+ children?: ReactNode;
33
+ onLoad?: () => void;
34
+ onPlay?: () => void;
35
+ onPause?: () => void;
36
+ onEnded?: () => void;
37
+ onError?: (event: VideoErrorEvent) => void;
38
+ onTimeUpdate?: (event: VideoTimeUpdateEvent) => void;
39
+ onFullscreenChange?: (event: VideoFullscreenChangeEvent) => void;
40
+ onLoadedMetaData?: (event: VideoMetaDataEvent) => void;
41
+ }
42
+ export declare const Video: FC<VideoProps>;
@@ -0,0 +1,168 @@
1
+ import { useCallback, useMemo, useRef, useState } from 'react';
2
+ import { Image, Pressable, StyleSheet, Text, View, } from 'react-native';
3
+ import RNCamera, { ResizeMode, } from 'react-native-video';
4
+ import { formatTime } from './utils';
5
+ export const Video = props => {
6
+ const { src = '', autoplay = false, style, initialTime = 0, loop = false, muted = false, objectFit = 'contain', poster, controls = true, showCenterPlayBtn = true, duration: durationProp, onLoad, onPlay, onPause, onEnded, onError, onLoadedMetaData, onFullscreenChange, onTimeUpdate, children, } = props;
7
+ const videoRef = useRef(null);
8
+ const [isFullScreen, setIsFullScreen] = useState(false);
9
+ const [isPlaying, setIsPlaying] = useState(autoplay);
10
+ const [isFirst, setIsFirst] = useState(true);
11
+ const [durationMs, setDurationMs] = useState(null);
12
+ const onLoadHandler = useCallback((data) => {
13
+ const loadedDurationMs = data.duration * 1000;
14
+ setDurationMs(loadedDurationMs);
15
+ if (initialTime > 0) {
16
+ videoRef.current?.seek(initialTime / 1000);
17
+ }
18
+ if (onLoad) {
19
+ onLoad();
20
+ }
21
+ if (onLoadedMetaData) {
22
+ onLoadedMetaData({
23
+ width: data.naturalSize.width,
24
+ height: data.naturalSize.height,
25
+ duration: loadedDurationMs,
26
+ durationMillis: loadedDurationMs,
27
+ });
28
+ }
29
+ }, [initialTime, onLoad, onLoadedMetaData]);
30
+ const onProgressHandler = useCallback((data) => {
31
+ if (onTimeUpdate) {
32
+ onTimeUpdate({
33
+ currentTime: data.currentTime * 1000,
34
+ duration: durationMs || data.seekableDuration * 1000,
35
+ });
36
+ }
37
+ }, [durationMs, onTimeUpdate]);
38
+ const onErrorHandler = useCallback((error) => {
39
+ if (onError) {
40
+ onError({
41
+ errMsg: error.error?.localizedDescription || 'Video Error',
42
+ });
43
+ }
44
+ }, [onError]);
45
+ const onEndedHandler = useCallback(() => {
46
+ if (onEnded) {
47
+ onEnded();
48
+ }
49
+ }, [onEnded]);
50
+ const onPlaybackStateHandler = useCallback(({ isPlaying: playing }) => {
51
+ setIsPlaying(playing);
52
+ if (playing) {
53
+ setIsFirst(false);
54
+ if (onPlay)
55
+ onPlay();
56
+ }
57
+ else if (!isFirst) {
58
+ if (onPause)
59
+ onPause();
60
+ }
61
+ }, [isFirst, onPlay, onPause]);
62
+ const onFullscreenWillPresentHandler = useCallback(() => {
63
+ setIsFullScreen(true);
64
+ if (onFullscreenChange) {
65
+ onFullscreenChange({
66
+ fullScreen: true,
67
+ direction: 'vertical',
68
+ });
69
+ }
70
+ }, [onFullscreenChange]);
71
+ const onFullscreenWillDismissHandler = useCallback(() => {
72
+ setIsFullScreen(false);
73
+ if (onFullscreenChange) {
74
+ onFullscreenChange({
75
+ fullScreen: false,
76
+ direction: 'vertical',
77
+ });
78
+ }
79
+ }, [onFullscreenChange]);
80
+ const onPlayVideoHandler = useCallback(() => {
81
+ setIsPlaying(true);
82
+ setIsFirst(false);
83
+ }, []);
84
+ const resizeMode = useMemo(() => {
85
+ const map = {
86
+ contain: ResizeMode.CONTAIN,
87
+ cover: ResizeMode.COVER,
88
+ fill: ResizeMode.STRETCH,
89
+ };
90
+ return map[objectFit] || ResizeMode.CONTAIN;
91
+ }, [objectFit]);
92
+ const computedDuration = formatTime(durationProp || durationMs || 0);
93
+ const showPlayBtn = (isFirst || showCenterPlayBtn) && !isPlaying;
94
+ return (<View style={[styles.video, style]}>
95
+ <View style={[
96
+ styles.videoContainer,
97
+ isFullScreen && styles.videoTypeFullscreen,
98
+ ]}>
99
+ <RNCamera ref={videoRef} source={{ uri: src }} style={styles.fullSize} paused={!isPlaying} repeat={loop} muted={muted} controls={controls} resizeMode={resizeMode} onLoad={onLoadHandler} onProgress={onProgressHandler} onEnd={onEndedHandler} onError={onErrorHandler} onPlaybackStateChanged={onPlaybackStateHandler} onFullscreenPlayerWillPresent={onFullscreenWillPresentHandler} onFullscreenPlayerWillDismiss={onFullscreenWillDismissHandler} progressUpdateInterval={250}/>
100
+
101
+ {showPlayBtn && (<View style={styles.videoCover}>
102
+ {poster && isFirst && (<Image source={{ uri: poster }} style={styles.videoPoster}/>)}
103
+ <Pressable onPress={onPlayVideoHandler}>
104
+ <Image source={require('../assets/video/play.png')} style={styles.videoCoverPlayButton}/>
105
+ </Pressable>
106
+ {computedDuration && (<Text style={styles.videoCoverDuration}>{computedDuration}</Text>)}
107
+ </View>)}
108
+ {children}
109
+ </View>
110
+ </View>);
111
+ };
112
+ const styles = StyleSheet.create({
113
+ fullSize: {
114
+ width: '100%',
115
+ height: '100%',
116
+ },
117
+ video: {
118
+ width: '100%',
119
+ height: 225,
120
+ overflow: 'hidden',
121
+ position: 'relative',
122
+ },
123
+ videoContainer: {
124
+ width: '100%',
125
+ height: '100%',
126
+ backgroundColor: '#000',
127
+ position: 'absolute',
128
+ top: 0,
129
+ left: 0,
130
+ right: 0,
131
+ bottom: 0,
132
+ overflow: 'hidden',
133
+ },
134
+ videoTypeFullscreen: {
135
+ position: 'absolute',
136
+ top: '50%',
137
+ left: '50%',
138
+ zIndex: 999,
139
+ },
140
+ videoCover: {
141
+ position: 'absolute',
142
+ top: 0,
143
+ left: 0,
144
+ bottom: 0,
145
+ right: 0,
146
+ width: '100%',
147
+ display: 'flex',
148
+ flexDirection: 'column',
149
+ justifyContent: 'center',
150
+ alignItems: 'center',
151
+ backgroundColor: 'rgba(1, 1, 1, 0.5)',
152
+ zIndex: 1,
153
+ },
154
+ videoPoster: {
155
+ position: 'absolute',
156
+ width: '100%',
157
+ height: '100%',
158
+ },
159
+ videoCoverPlayButton: {
160
+ width: 30,
161
+ height: 30,
162
+ },
163
+ videoCoverDuration: {
164
+ color: '#fff',
165
+ fontSize: 16,
166
+ marginTop: 10,
167
+ },
168
+ });
@@ -0,0 +1,3 @@
1
+ export declare const formatTime: (time: number) => string;
2
+ export declare const calcDist: (x: number, y: number) => number;
3
+ export declare const normalizeNumber: (number: number) => number;
@@ -0,0 +1,13 @@
1
+ export const formatTime = (time) => {
2
+ if (time === null)
3
+ return '';
4
+ const sec = Math.round((time / 1000) % 60);
5
+ const min = Math.floor((time - sec) / 1000 / 60);
6
+ return `${min < 10 ? `0${min}` : min}:${sec < 10 ? `0${sec}` : sec}`;
7
+ };
8
+ export const calcDist = (x, y) => {
9
+ return Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
10
+ };
11
+ export const normalizeNumber = (number) => {
12
+ return Math.max(-1, Math.min(number, 1));
13
+ };
@@ -0,0 +1,9 @@
1
+ import { FC } from 'react';
2
+ import { StyleProp, ViewStyle } from 'react-native';
3
+ import { WebViewProps as RNWebViewProps } from 'react-native-webview';
4
+ export interface WebViewProps extends Partial<RNWebViewProps> {
5
+ style?: StyleProp<ViewStyle>;
6
+ src?: string;
7
+ html?: string;
8
+ }
9
+ export declare const WebView: FC<WebViewProps>;
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ import { WebView as RNWebView, } from 'react-native-webview';
3
+ export const WebView = ({ style, src, html, ...rest }) => {
4
+ const source = html ? { html } : { uri: src || '' };
5
+ return (<RNWebView {...rest} source={source} style={style} originWhitelist={['*']}/>);
6
+ };
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,8 @@
1
+ export * from './Camera';
2
+ export * from './Map';
3
+ export * from './Progress';
4
+ export * from './RichText';
5
+ export * from './Slider';
6
+ export * from './Swiper';
7
+ export * from './Video';
8
+ export * from './WebView';
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ // Thanks to the Taro project for the inspiration that helped shape this component library.
2
+ // https://github.com/NervJS/taro.git
3
+ export * from './Camera';
4
+ export * from './Map';
5
+ export * from './Progress';
6
+ export * from './RichText';
7
+ export * from './Slider';
8
+ export * from './Swiper';
9
+ export * from './Video';
10
+ export * from './WebView';
@@ -0,0 +1,4 @@
1
+ export declare const omit: (obj?: any, fields?: string[]) => {
2
+ [key: string]: any;
3
+ };
4
+ export declare const noop: (..._args: any[]) => void;
@@ -0,0 +1,8 @@
1
+ export const omit = (obj = {}, fields = []) => {
2
+ const shallowCopy = { ...obj };
3
+ fields.forEach(key => {
4
+ delete shallowCopy[key];
5
+ });
6
+ return shallowCopy;
7
+ };
8
+ export const noop = (..._args) => { };
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@nebula-rn/components",
3
+ "version": "0.0.1",
4
+ "description": "Nebula UI Components for React Native",
5
+ "author": "Hector Zhuang",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/Hector-Zhuang/nebula.git",
10
+ "directory": "packages/components"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/Hector-Zhuang/nebula/issues"
14
+ },
15
+ "homepage": "https://github.com/Hector-Zhuang/nebula/tree/main/packages/components#readme",
16
+ "keywords": ["nebula", "superapp", "miniapp", "react-native", "components"],
17
+ "main": "./dist/index.js",
18
+ "react-native": "./src/index.ts",
19
+ "source": "./src/index.ts",
20
+ "types": "./dist/index.d.ts",
21
+ "files": ["dist", "src", "README.md"],
22
+ "publishConfig": { "access": "public" },
23
+ "scripts": {
24
+ "build": "tsc && npm run assets",
25
+ "assets": "cpy 'src/**/*.png' '!src/__tests__/*' dist",
26
+ "clean": "rimraf ./dist",
27
+ "prebuild": "npm run clean",
28
+ "prepack": "npm run build",
29
+ "dev": "npm run assets && tsc --watch",
30
+ "lint": "eslint src --ext .js,.jsx,.ts,.tsx",
31
+ "test": "jest --silent",
32
+ "test:ci": "jest --ci -i --coverage --silent",
33
+ "test:dev": "jest --watch",
34
+ "test:coverage": "jest --coverage"
35
+ },
36
+ "engines": {
37
+ "node": ">= 18"
38
+ },
39
+ "devDependencies": {
40
+ "@babel/core": "^7.24.4",
41
+ "@babel/preset-env": "^7.24.4",
42
+ "@react-native/babel-preset": "^0.73.18",
43
+ "cpy-cli": "^5.0.0",
44
+ "dpdm": "^3.14.0"
45
+ },
46
+ "peerDependencies": {
47
+ "react": ">=18",
48
+ "react-native": ">=0.83"
49
+ },
50
+ "dependencies": {
51
+ "react-native-gesture-handler": "^2.30.0",
52
+ "react-native-nitro-modules": "^0.35.2",
53
+ "react-native-reanimated": "~4.2.3",
54
+ "react-native-reanimated-carousel": "^4.0.3",
55
+ "react-native-video": "^6.19.1",
56
+ "react-native-vision-camera": "^4.7.3"
57
+ }
58
+ }
@@ -0,0 +1,179 @@
1
+ import React, {
2
+ useEffect,
3
+ useCallback,
4
+ FC,
5
+ useMemo,
6
+ PropsWithChildren,
7
+ } from 'react';
8
+ import { StyleSheet, Text, View } from 'react-native';
9
+ import {
10
+ Camera as RNCamera,
11
+ useCameraDevice,
12
+ useCameraPermission,
13
+ useCodeScanner,
14
+ Code,
15
+ CodeType,
16
+ } from 'react-native-vision-camera';
17
+
18
+ export interface CameraInitEventDetail {
19
+ maxZoom: number;
20
+ }
21
+
22
+ export interface CameraScanCodeEventDetail {
23
+ charSet: string;
24
+ rawData: string;
25
+ type: CodeType | 'unknown';
26
+ result: string;
27
+ fullResult: string;
28
+ }
29
+
30
+ export interface CameraError {
31
+ message: string;
32
+ code?: string;
33
+ nativeError?: unknown;
34
+ }
35
+
36
+ export interface CameraProps {
37
+ id?: string;
38
+ className?: string;
39
+ style?: any;
40
+ mode?: 'normal' | 'scanCode';
41
+ resolution?: 'low' | 'medium' | 'high';
42
+ devicePosition?: 'front' | 'back';
43
+ flash?: 'auto' | 'on' | 'off' | 'torch';
44
+ onInitDone?: (event: CameraInitEventDetail) => void;
45
+ onReady?: (event: CameraInitEventDetail) => void;
46
+ onScanCode?: (event: CameraScanCodeEventDetail) => void;
47
+ onError?: (error: CameraError) => void;
48
+ }
49
+
50
+ export const Camera: FC<PropsWithChildren<CameraProps>> = props => {
51
+ const {
52
+ devicePosition = 'back',
53
+ style,
54
+ mode,
55
+ flash,
56
+ onScanCode,
57
+ onError,
58
+ onInitDone,
59
+ } = props;
60
+
61
+ const { hasPermission, requestPermission } = useCameraPermission();
62
+ const device = useCameraDevice(devicePosition);
63
+
64
+ useEffect(() => {
65
+ onHandlePermissionRequest();
66
+ }, []);
67
+
68
+ const onHandlePermissionRequest = useCallback(async () => {
69
+ if (!hasPermission) {
70
+ try {
71
+ const granted = await requestPermission();
72
+ if (!granted && onError) {
73
+ onError({
74
+ message: 'Camera permission denied',
75
+ code: 'PERMISSION_DENIED',
76
+ });
77
+ }
78
+ } catch (error) {
79
+ if (onError) {
80
+ onError({
81
+ message: error instanceof Error ? error.message : 'Unknown error',
82
+ code: 'PERMISSION_ERROR',
83
+ nativeError: error,
84
+ });
85
+ }
86
+ }
87
+ }
88
+ }, [hasPermission, requestPermission, onError]);
89
+
90
+ const onHandleCodeScanned = useCallback(
91
+ (codes: Code[]) => {
92
+ if (mode === 'scanCode' && codes.length > 0 && onScanCode) {
93
+ const code = codes[0];
94
+ onScanCode({
95
+ result: code.value || '',
96
+ type: code.type,
97
+ charSet: '',
98
+ rawData: code.value || '',
99
+ fullResult: code.value || '',
100
+ });
101
+ }
102
+ },
103
+ [mode, onScanCode],
104
+ );
105
+
106
+ const codeScanner = useCodeScanner({
107
+ codeTypes: ['qr', 'ean-13'],
108
+ onCodeScanned: onHandleCodeScanned,
109
+ });
110
+
111
+ const onHandleError = useCallback(
112
+ (error: unknown) => {
113
+ if (onError) {
114
+ if (error && typeof error === 'object' && 'message' in error) {
115
+ onError({ message: (error as any).message, nativeError: error });
116
+ } else {
117
+ onError({ message: 'Unknown error', nativeError: error });
118
+ }
119
+ }
120
+ },
121
+ [onError],
122
+ );
123
+
124
+ const onHandleInitialized = useCallback(() => {
125
+ if (onInitDone) {
126
+ onInitDone({ maxZoom: device?.maxZoom ?? 1 });
127
+ }
128
+ }, [onInitDone, device]);
129
+
130
+ const torchState = useMemo(() => {
131
+ if (flash === 'on' || flash === 'torch') return 'on';
132
+ return 'off';
133
+ }, [flash]);
134
+
135
+ if (!hasPermission) {
136
+ return (
137
+ <View style={styles.center}>
138
+ <Text>No access to camera</Text>
139
+ </View>
140
+ );
141
+ }
142
+
143
+ if (!device) {
144
+ return (
145
+ <View style={styles.center}>
146
+ <Text>No camera device found</Text>
147
+ </View>
148
+ );
149
+ }
150
+
151
+ return (
152
+ <View style={[styles.container, style]}>
153
+ <RNCamera
154
+ style={StyleSheet.absoluteFill}
155
+ device={device}
156
+ isActive
157
+ codeScanner={mode === 'scanCode' ? codeScanner : undefined}
158
+ torch={torchState}
159
+ onError={onHandleError}
160
+ onInitialized={onHandleInitialized}
161
+ />
162
+ {props.children}
163
+ </View>
164
+ );
165
+ };
166
+
167
+ const styles = StyleSheet.create({
168
+ container: {
169
+ width: 300,
170
+ height: 300,
171
+ overflow: 'hidden',
172
+ backgroundColor: 'black',
173
+ },
174
+ center: {
175
+ flex: 1,
176
+ justifyContent: 'center',
177
+ alignItems: 'center',
178
+ },
179
+ });