@metacells/mcellui-mcp-server 0.1.1 → 0.1.2
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/dist/index.js +8 -2
- package/package.json +5 -3
- package/registry/registry.json +717 -0
- package/registry/ui/accordion.tsx +416 -0
- package/registry/ui/action-sheet.tsx +396 -0
- package/registry/ui/alert-dialog.tsx +355 -0
- package/registry/ui/avatar-stack.tsx +278 -0
- package/registry/ui/avatar.tsx +116 -0
- package/registry/ui/badge.tsx +125 -0
- package/registry/ui/button.tsx +240 -0
- package/registry/ui/card.tsx +675 -0
- package/registry/ui/carousel.tsx +431 -0
- package/registry/ui/checkbox.tsx +252 -0
- package/registry/ui/chip.tsx +271 -0
- package/registry/ui/column.tsx +133 -0
- package/registry/ui/datetime-picker.tsx +578 -0
- package/registry/ui/dialog.tsx +292 -0
- package/registry/ui/fab.tsx +225 -0
- package/registry/ui/form.tsx +323 -0
- package/registry/ui/horizontal-list.tsx +200 -0
- package/registry/ui/icon-button.tsx +244 -0
- package/registry/ui/image-gallery.tsx +455 -0
- package/registry/ui/image.tsx +283 -0
- package/registry/ui/input.tsx +242 -0
- package/registry/ui/label.tsx +99 -0
- package/registry/ui/list.tsx +519 -0
- package/registry/ui/progress.tsx +168 -0
- package/registry/ui/pull-to-refresh.tsx +231 -0
- package/registry/ui/radio-group.tsx +294 -0
- package/registry/ui/rating.tsx +311 -0
- package/registry/ui/row.tsx +136 -0
- package/registry/ui/screen.tsx +153 -0
- package/registry/ui/search-input.tsx +281 -0
- package/registry/ui/section-header.tsx +258 -0
- package/registry/ui/segmented-control.tsx +229 -0
- package/registry/ui/select.tsx +311 -0
- package/registry/ui/separator.tsx +74 -0
- package/registry/ui/sheet.tsx +362 -0
- package/registry/ui/skeleton.tsx +156 -0
- package/registry/ui/slider.tsx +307 -0
- package/registry/ui/spinner.tsx +100 -0
- package/registry/ui/stepper.tsx +314 -0
- package/registry/ui/stories.tsx +463 -0
- package/registry/ui/swipeable-row.tsx +362 -0
- package/registry/ui/switch.tsx +246 -0
- package/registry/ui/tabs.tsx +348 -0
- package/registry/ui/textarea.tsx +265 -0
- package/registry/ui/toast.tsx +316 -0
- package/registry/ui/tooltip.tsx +369 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rating
|
|
3
|
+
*
|
|
4
|
+
* Interactive star rating for reviews and feedback.
|
|
5
|
+
* Supports half-star precision and readonly display mode.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* // Interactive rating
|
|
10
|
+
* const [rating, setRating] = useState(0);
|
|
11
|
+
* <Rating value={rating} onChange={setRating} />
|
|
12
|
+
*
|
|
13
|
+
* // Readonly display
|
|
14
|
+
* <Rating value={4.5} readonly />
|
|
15
|
+
*
|
|
16
|
+
* // With label
|
|
17
|
+
* <Rating value={3} readonly size="sm" />
|
|
18
|
+
* <Text>{rating} out of 5</Text>
|
|
19
|
+
*
|
|
20
|
+
* // Different sizes
|
|
21
|
+
* <Rating value={4} size="sm" readonly />
|
|
22
|
+
* <Rating value={4} size="md" readonly />
|
|
23
|
+
* <Rating value={4} size="lg" readonly />
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import React from 'react';
|
|
28
|
+
import {
|
|
29
|
+
View,
|
|
30
|
+
Pressable,
|
|
31
|
+
StyleSheet,
|
|
32
|
+
ViewStyle,
|
|
33
|
+
} from 'react-native';
|
|
34
|
+
import Animated, {
|
|
35
|
+
useAnimatedStyle,
|
|
36
|
+
useSharedValue,
|
|
37
|
+
withSpring,
|
|
38
|
+
withSequence,
|
|
39
|
+
withTiming,
|
|
40
|
+
} from 'react-native-reanimated';
|
|
41
|
+
import { useTheme, haptic } from '@nativeui/core';
|
|
42
|
+
|
|
43
|
+
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
|
|
44
|
+
|
|
45
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
46
|
+
// Types
|
|
47
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
export type RatingSize = 'sm' | 'md' | 'lg';
|
|
50
|
+
|
|
51
|
+
export interface RatingProps {
|
|
52
|
+
/** Current rating value (0-5) */
|
|
53
|
+
value: number;
|
|
54
|
+
/** Maximum rating value */
|
|
55
|
+
max?: number;
|
|
56
|
+
/** Size preset */
|
|
57
|
+
size?: RatingSize;
|
|
58
|
+
/** Whether rating is readonly (display only) */
|
|
59
|
+
readonly?: boolean;
|
|
60
|
+
/** Allow half-star precision */
|
|
61
|
+
precision?: 'full' | 'half';
|
|
62
|
+
/** Change handler */
|
|
63
|
+
onChange?: (value: number) => void;
|
|
64
|
+
/** Container style */
|
|
65
|
+
style?: ViewStyle;
|
|
66
|
+
/** Active star color (defaults to warning yellow) */
|
|
67
|
+
activeColor?: string;
|
|
68
|
+
/** Inactive star color */
|
|
69
|
+
inactiveColor?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
73
|
+
// Size configs
|
|
74
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
const SIZE_CONFIG = {
|
|
77
|
+
sm: {
|
|
78
|
+
starSize: 18,
|
|
79
|
+
gap: 2,
|
|
80
|
+
},
|
|
81
|
+
md: {
|
|
82
|
+
starSize: 26,
|
|
83
|
+
gap: 4,
|
|
84
|
+
},
|
|
85
|
+
lg: {
|
|
86
|
+
starSize: 34,
|
|
87
|
+
gap: 6,
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
92
|
+
// Star SVG Path (simplified 5-point star)
|
|
93
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
function StarIcon({ size, color }: { size: number; color: string }) {
|
|
96
|
+
// Simple star shape using View components
|
|
97
|
+
return (
|
|
98
|
+
<View style={{ width: size, height: size, alignItems: 'center', justifyContent: 'center' }}>
|
|
99
|
+
<View
|
|
100
|
+
style={{
|
|
101
|
+
width: size * 0.9,
|
|
102
|
+
height: size * 0.9,
|
|
103
|
+
backgroundColor: color,
|
|
104
|
+
// Create star using clip path workaround (simplified to rounded square for now)
|
|
105
|
+
borderRadius: size * 0.15,
|
|
106
|
+
transform: [{ rotate: '45deg' }],
|
|
107
|
+
}}
|
|
108
|
+
/>
|
|
109
|
+
{/* Star points overlay */}
|
|
110
|
+
<View
|
|
111
|
+
style={{
|
|
112
|
+
position: 'absolute',
|
|
113
|
+
width: size * 0.6,
|
|
114
|
+
height: size * 0.6,
|
|
115
|
+
backgroundColor: color,
|
|
116
|
+
borderRadius: size * 0.1,
|
|
117
|
+
}}
|
|
118
|
+
/>
|
|
119
|
+
</View>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Unicode star character approach (more reliable)
|
|
124
|
+
function StarText({ size, color, filled }: { size: number; color: string; filled: boolean }) {
|
|
125
|
+
return (
|
|
126
|
+
<Animated.Text
|
|
127
|
+
style={{
|
|
128
|
+
fontSize: size,
|
|
129
|
+
color,
|
|
130
|
+
lineHeight: size * 1.1,
|
|
131
|
+
textAlign: 'center',
|
|
132
|
+
}}
|
|
133
|
+
>
|
|
134
|
+
{filled ? '★' : '☆'}
|
|
135
|
+
</Animated.Text>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
140
|
+
// Individual Star Component
|
|
141
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
interface StarProps {
|
|
144
|
+
index: number;
|
|
145
|
+
filled: 'full' | 'half' | 'empty';
|
|
146
|
+
size: number;
|
|
147
|
+
activeColor: string;
|
|
148
|
+
inactiveColor: string;
|
|
149
|
+
readonly: boolean;
|
|
150
|
+
onPress: () => void;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function Star({
|
|
154
|
+
index,
|
|
155
|
+
filled,
|
|
156
|
+
size,
|
|
157
|
+
activeColor,
|
|
158
|
+
inactiveColor,
|
|
159
|
+
readonly,
|
|
160
|
+
onPress,
|
|
161
|
+
}: StarProps) {
|
|
162
|
+
const scale = useSharedValue(1);
|
|
163
|
+
|
|
164
|
+
const animatedStyle = useAnimatedStyle(() => ({
|
|
165
|
+
transform: [{ scale: scale.value }],
|
|
166
|
+
}));
|
|
167
|
+
|
|
168
|
+
const handlePressIn = () => {
|
|
169
|
+
if (readonly) return;
|
|
170
|
+
scale.value = withSpring(0.85, { damping: 15, stiffness: 400 });
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const handlePressOut = () => {
|
|
174
|
+
if (readonly) return;
|
|
175
|
+
scale.value = withSequence(
|
|
176
|
+
withSpring(1.15, { damping: 15, stiffness: 400 }),
|
|
177
|
+
withSpring(1, { damping: 15, stiffness: 400 })
|
|
178
|
+
);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const handlePress = () => {
|
|
182
|
+
if (readonly) return;
|
|
183
|
+
haptic('light');
|
|
184
|
+
onPress();
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const color = filled === 'empty' ? inactiveColor : activeColor;
|
|
188
|
+
|
|
189
|
+
if (readonly) {
|
|
190
|
+
return (
|
|
191
|
+
<View style={[styles.star, { width: size, height: size }]}>
|
|
192
|
+
{filled === 'half' ? (
|
|
193
|
+
<View style={styles.halfStarContainer}>
|
|
194
|
+
<View style={[styles.halfStar, { width: size / 2, overflow: 'hidden' }]}>
|
|
195
|
+
<StarText size={size} color={activeColor} filled />
|
|
196
|
+
</View>
|
|
197
|
+
<View style={[styles.halfStar, { width: size / 2, overflow: 'hidden', marginLeft: -size / 2 }]}>
|
|
198
|
+
<View style={{ marginLeft: -size / 2 }}>
|
|
199
|
+
<StarText size={size} color={inactiveColor} filled={false} />
|
|
200
|
+
</View>
|
|
201
|
+
</View>
|
|
202
|
+
</View>
|
|
203
|
+
) : (
|
|
204
|
+
<StarText size={size} color={color} filled={filled === 'full'} />
|
|
205
|
+
)}
|
|
206
|
+
</View>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
<AnimatedPressable
|
|
212
|
+
onPress={handlePress}
|
|
213
|
+
onPressIn={handlePressIn}
|
|
214
|
+
onPressOut={handlePressOut}
|
|
215
|
+
style={[styles.star, { width: size, height: size }, animatedStyle]}
|
|
216
|
+
accessibilityRole="button"
|
|
217
|
+
accessibilityLabel={`Rate ${index + 1} star${index > 0 ? 's' : ''}`}
|
|
218
|
+
>
|
|
219
|
+
<StarText size={size} color={color} filled={filled === 'full'} />
|
|
220
|
+
</AnimatedPressable>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
225
|
+
// Rating Component
|
|
226
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
227
|
+
|
|
228
|
+
export function Rating({
|
|
229
|
+
value,
|
|
230
|
+
max = 5,
|
|
231
|
+
size = 'md',
|
|
232
|
+
readonly = false,
|
|
233
|
+
precision = 'full',
|
|
234
|
+
onChange,
|
|
235
|
+
style,
|
|
236
|
+
activeColor,
|
|
237
|
+
inactiveColor,
|
|
238
|
+
}: RatingProps) {
|
|
239
|
+
const { colors } = useTheme();
|
|
240
|
+
const config = SIZE_CONFIG[size];
|
|
241
|
+
|
|
242
|
+
const starActiveColor = activeColor || '#F59E0B'; // Amber/warning yellow
|
|
243
|
+
const starInactiveColor = inactiveColor || colors.border;
|
|
244
|
+
|
|
245
|
+
const handleStarPress = (index: number) => {
|
|
246
|
+
if (readonly || !onChange) return;
|
|
247
|
+
const newValue = index + 1;
|
|
248
|
+
onChange(newValue);
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const getStarFill = (index: number): 'full' | 'half' | 'empty' => {
|
|
252
|
+
if (value >= index + 1) {
|
|
253
|
+
return 'full';
|
|
254
|
+
}
|
|
255
|
+
if (precision === 'half' && value >= index + 0.5) {
|
|
256
|
+
return 'half';
|
|
257
|
+
}
|
|
258
|
+
return 'empty';
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
return (
|
|
262
|
+
<View
|
|
263
|
+
style={[
|
|
264
|
+
styles.container,
|
|
265
|
+
{ gap: config.gap },
|
|
266
|
+
style,
|
|
267
|
+
]}
|
|
268
|
+
accessibilityRole="adjustable"
|
|
269
|
+
accessibilityValue={{
|
|
270
|
+
min: 0,
|
|
271
|
+
max,
|
|
272
|
+
now: value,
|
|
273
|
+
text: `${value} out of ${max} stars`,
|
|
274
|
+
}}
|
|
275
|
+
>
|
|
276
|
+
{Array.from({ length: max }, (_, index) => (
|
|
277
|
+
<Star
|
|
278
|
+
key={index}
|
|
279
|
+
index={index}
|
|
280
|
+
filled={getStarFill(index)}
|
|
281
|
+
size={config.starSize}
|
|
282
|
+
activeColor={starActiveColor}
|
|
283
|
+
inactiveColor={starInactiveColor}
|
|
284
|
+
readonly={readonly}
|
|
285
|
+
onPress={() => handleStarPress(index)}
|
|
286
|
+
/>
|
|
287
|
+
))}
|
|
288
|
+
</View>
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
293
|
+
// Styles
|
|
294
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
295
|
+
|
|
296
|
+
const styles = StyleSheet.create({
|
|
297
|
+
container: {
|
|
298
|
+
flexDirection: 'row',
|
|
299
|
+
alignItems: 'center',
|
|
300
|
+
},
|
|
301
|
+
star: {
|
|
302
|
+
alignItems: 'center',
|
|
303
|
+
justifyContent: 'center',
|
|
304
|
+
},
|
|
305
|
+
halfStarContainer: {
|
|
306
|
+
flexDirection: 'row',
|
|
307
|
+
},
|
|
308
|
+
halfStar: {
|
|
309
|
+
overflow: 'hidden',
|
|
310
|
+
},
|
|
311
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Row
|
|
3
|
+
*
|
|
4
|
+
* Horizontal flex container with gap, alignment, and padding props.
|
|
5
|
+
* Uses design tokens for consistent spacing.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* <Row gap="md" align="center" justify="between">
|
|
10
|
+
* <Text>Left</Text>
|
|
11
|
+
* <Text>Right</Text>
|
|
12
|
+
* </Row>
|
|
13
|
+
*
|
|
14
|
+
* <Row gap={4} wrap>
|
|
15
|
+
* {items.map(item => <Chip key={item.id}>{item.name}</Chip>)}
|
|
16
|
+
* </Row>
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import React, { forwardRef } from 'react';
|
|
21
|
+
import { View, ViewProps, ViewStyle } from 'react-native';
|
|
22
|
+
import { useTheme } from '@nativeui/core';
|
|
23
|
+
|
|
24
|
+
// Semantic gap values
|
|
25
|
+
type GapSemantic = 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';
|
|
26
|
+
|
|
27
|
+
// Gap can be either semantic string or numeric spacing key
|
|
28
|
+
export type GapValue = GapSemantic | number;
|
|
29
|
+
|
|
30
|
+
// Semantic → Spacing Key Mapping
|
|
31
|
+
const semanticGapMap: Record<GapSemantic, number> = {
|
|
32
|
+
'none': 0,
|
|
33
|
+
'xs': 2,
|
|
34
|
+
'sm': 3,
|
|
35
|
+
'md': 4,
|
|
36
|
+
'lg': 6,
|
|
37
|
+
'xl': 8,
|
|
38
|
+
'2xl': 12,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Alignment value mappings
|
|
42
|
+
const alignMap = {
|
|
43
|
+
'start': 'flex-start',
|
|
44
|
+
'center': 'center',
|
|
45
|
+
'end': 'flex-end',
|
|
46
|
+
'stretch': 'stretch',
|
|
47
|
+
'baseline': 'baseline',
|
|
48
|
+
} as const;
|
|
49
|
+
|
|
50
|
+
const justifyMap = {
|
|
51
|
+
'start': 'flex-start',
|
|
52
|
+
'center': 'center',
|
|
53
|
+
'end': 'flex-end',
|
|
54
|
+
'between': 'space-between',
|
|
55
|
+
'around': 'space-around',
|
|
56
|
+
'evenly': 'space-evenly',
|
|
57
|
+
} as const;
|
|
58
|
+
|
|
59
|
+
export interface RowProps extends ViewProps {
|
|
60
|
+
/** Gap between children (semantic or spacing key) */
|
|
61
|
+
gap?: GapValue;
|
|
62
|
+
/** Vertical alignment of children */
|
|
63
|
+
align?: 'start' | 'center' | 'end' | 'stretch' | 'baseline';
|
|
64
|
+
/** Horizontal distribution of children */
|
|
65
|
+
justify?: 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly';
|
|
66
|
+
/** Allow children to wrap to next line */
|
|
67
|
+
wrap?: boolean;
|
|
68
|
+
/** Flex value for the container */
|
|
69
|
+
flex?: number;
|
|
70
|
+
/** Padding (all sides) */
|
|
71
|
+
p?: GapValue;
|
|
72
|
+
/** Horizontal padding */
|
|
73
|
+
px?: GapValue;
|
|
74
|
+
/** Vertical padding */
|
|
75
|
+
py?: GapValue;
|
|
76
|
+
/** Additional styles */
|
|
77
|
+
style?: ViewStyle;
|
|
78
|
+
/** Children */
|
|
79
|
+
children?: React.ReactNode;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function resolveGap(
|
|
83
|
+
value: GapValue | undefined,
|
|
84
|
+
spacing: ReturnType<typeof useTheme>['spacing']
|
|
85
|
+
): number | undefined {
|
|
86
|
+
if (value === undefined) return undefined;
|
|
87
|
+
|
|
88
|
+
if (typeof value === 'string') {
|
|
89
|
+
const key = semanticGapMap[value as GapSemantic];
|
|
90
|
+
return (spacing as Record<number, number>)[key];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return (spacing as Record<number, number>)[value];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export const Row = forwardRef<View, RowProps>(function Row(
|
|
97
|
+
{
|
|
98
|
+
gap,
|
|
99
|
+
align,
|
|
100
|
+
justify,
|
|
101
|
+
wrap = false,
|
|
102
|
+
flex,
|
|
103
|
+
p,
|
|
104
|
+
px,
|
|
105
|
+
py,
|
|
106
|
+
style,
|
|
107
|
+
children,
|
|
108
|
+
...props
|
|
109
|
+
},
|
|
110
|
+
ref
|
|
111
|
+
) {
|
|
112
|
+
const { spacing } = useTheme();
|
|
113
|
+
|
|
114
|
+
const resolvedGap = resolveGap(gap, spacing);
|
|
115
|
+
const resolvedP = resolveGap(p, spacing);
|
|
116
|
+
const resolvedPx = resolveGap(px, spacing);
|
|
117
|
+
const resolvedPy = resolveGap(py, spacing);
|
|
118
|
+
|
|
119
|
+
const containerStyle: ViewStyle = {
|
|
120
|
+
flexDirection: 'row',
|
|
121
|
+
...(resolvedGap !== undefined && { gap: resolvedGap }),
|
|
122
|
+
...(align && { alignItems: alignMap[align] }),
|
|
123
|
+
...(justify && { justifyContent: justifyMap[justify] }),
|
|
124
|
+
...(wrap && { flexWrap: 'wrap' }),
|
|
125
|
+
...(flex !== undefined && { flex }),
|
|
126
|
+
...(resolvedP !== undefined && { padding: resolvedP }),
|
|
127
|
+
...(resolvedPx !== undefined && { paddingHorizontal: resolvedPx }),
|
|
128
|
+
...(resolvedPy !== undefined && { paddingVertical: resolvedPy }),
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<View ref={ref} style={[containerStyle, style]} {...props}>
|
|
133
|
+
{children}
|
|
134
|
+
</View>
|
|
135
|
+
);
|
|
136
|
+
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Screen
|
|
3
|
+
*
|
|
4
|
+
* Screen container with safe area, scroll support, and background variants.
|
|
5
|
+
* Uses design tokens for consistent styling.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* // Basic screen with safe area
|
|
10
|
+
* <Screen>
|
|
11
|
+
* <Text>Content</Text>
|
|
12
|
+
* </Screen>
|
|
13
|
+
*
|
|
14
|
+
* // Scrollable screen with padding
|
|
15
|
+
* <Screen scroll padded>
|
|
16
|
+
* <Text>Scrollable content</Text>
|
|
17
|
+
* </Screen>
|
|
18
|
+
*
|
|
19
|
+
* // Surface variant with custom edges
|
|
20
|
+
* <Screen variant="surface" edges={['top', 'bottom']}>
|
|
21
|
+
* <Text>Surface background</Text>
|
|
22
|
+
* </Screen>
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import React, { forwardRef } from 'react';
|
|
27
|
+
import {
|
|
28
|
+
View,
|
|
29
|
+
ScrollView,
|
|
30
|
+
ScrollViewProps,
|
|
31
|
+
ViewStyle,
|
|
32
|
+
StyleSheet,
|
|
33
|
+
} from 'react-native';
|
|
34
|
+
import { useSafeAreaInsets, Edge } from 'react-native-safe-area-context';
|
|
35
|
+
import { useTheme } from '@nativeui/core';
|
|
36
|
+
|
|
37
|
+
export type ScreenVariant = 'default' | 'surface' | 'muted';
|
|
38
|
+
|
|
39
|
+
export interface ScreenProps {
|
|
40
|
+
/** Enable scrolling */
|
|
41
|
+
scroll?: boolean;
|
|
42
|
+
/** Additional ScrollView props when scroll is enabled */
|
|
43
|
+
scrollProps?: Omit<ScrollViewProps, 'style' | 'contentContainerStyle'>;
|
|
44
|
+
/** Add horizontal padding (spacing[4] = 16px) */
|
|
45
|
+
padded?: boolean;
|
|
46
|
+
/** Background variant */
|
|
47
|
+
variant?: ScreenVariant;
|
|
48
|
+
/** Safe area edges to respect (default: all) */
|
|
49
|
+
edges?: Edge[];
|
|
50
|
+
/** Additional styles for the container */
|
|
51
|
+
style?: ViewStyle;
|
|
52
|
+
/** Additional styles for the content container (only when scroll is true) */
|
|
53
|
+
contentContainerStyle?: ViewStyle;
|
|
54
|
+
/** Children */
|
|
55
|
+
children?: React.ReactNode;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const Screen = forwardRef<View | ScrollView, ScreenProps>(function Screen(
|
|
59
|
+
{
|
|
60
|
+
scroll = false,
|
|
61
|
+
scrollProps,
|
|
62
|
+
padded = false,
|
|
63
|
+
variant = 'default',
|
|
64
|
+
edges = ['top', 'bottom', 'left', 'right'],
|
|
65
|
+
style,
|
|
66
|
+
contentContainerStyle,
|
|
67
|
+
children,
|
|
68
|
+
},
|
|
69
|
+
ref
|
|
70
|
+
) {
|
|
71
|
+
const { colors, spacing } = useTheme();
|
|
72
|
+
const insets = useSafeAreaInsets();
|
|
73
|
+
|
|
74
|
+
// Get background color based on variant
|
|
75
|
+
const backgroundColor = getBackgroundColor(variant, colors);
|
|
76
|
+
|
|
77
|
+
// Calculate safe area padding based on edges
|
|
78
|
+
const safeAreaStyle: ViewStyle = {
|
|
79
|
+
paddingTop: edges.includes('top') ? insets.top : 0,
|
|
80
|
+
paddingBottom: edges.includes('bottom') ? insets.bottom : 0,
|
|
81
|
+
paddingLeft: edges.includes('left') ? insets.left : 0,
|
|
82
|
+
paddingRight: edges.includes('right') ? insets.right : 0,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Content padding when padded is true
|
|
86
|
+
const paddedStyle: ViewStyle = padded
|
|
87
|
+
? { paddingHorizontal: spacing[4] }
|
|
88
|
+
: {};
|
|
89
|
+
|
|
90
|
+
if (scroll) {
|
|
91
|
+
return (
|
|
92
|
+
<ScrollView
|
|
93
|
+
ref={ref as React.ForwardedRef<ScrollView>}
|
|
94
|
+
style={[
|
|
95
|
+
styles.container,
|
|
96
|
+
{ backgroundColor },
|
|
97
|
+
safeAreaStyle,
|
|
98
|
+
style,
|
|
99
|
+
]}
|
|
100
|
+
contentContainerStyle={[
|
|
101
|
+
styles.scrollContent,
|
|
102
|
+
paddedStyle,
|
|
103
|
+
contentContainerStyle,
|
|
104
|
+
]}
|
|
105
|
+
showsVerticalScrollIndicator={false}
|
|
106
|
+
keyboardShouldPersistTaps="handled"
|
|
107
|
+
{...scrollProps}
|
|
108
|
+
>
|
|
109
|
+
{children}
|
|
110
|
+
</ScrollView>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<View
|
|
116
|
+
ref={ref as React.ForwardedRef<View>}
|
|
117
|
+
style={[
|
|
118
|
+
styles.container,
|
|
119
|
+
{ backgroundColor },
|
|
120
|
+
safeAreaStyle,
|
|
121
|
+
paddedStyle,
|
|
122
|
+
style,
|
|
123
|
+
]}
|
|
124
|
+
>
|
|
125
|
+
{children}
|
|
126
|
+
</View>
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
function getBackgroundColor(
|
|
131
|
+
variant: ScreenVariant,
|
|
132
|
+
colors: ReturnType<typeof useTheme>['colors']
|
|
133
|
+
): string {
|
|
134
|
+
switch (variant) {
|
|
135
|
+
case 'default':
|
|
136
|
+
return colors.background;
|
|
137
|
+
case 'surface':
|
|
138
|
+
return colors.backgroundSubtle;
|
|
139
|
+
case 'muted':
|
|
140
|
+
return colors.backgroundMuted;
|
|
141
|
+
default:
|
|
142
|
+
return colors.background;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const styles = StyleSheet.create({
|
|
147
|
+
container: {
|
|
148
|
+
flex: 1,
|
|
149
|
+
},
|
|
150
|
+
scrollContent: {
|
|
151
|
+
flexGrow: 1,
|
|
152
|
+
},
|
|
153
|
+
});
|