@umituz/react-native-design-system 2.1.7 → 2.3.0
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/package.json +1 -1
- package/src/atoms/AtomicText.tsx +15 -2
- package/src/atoms/index.ts +9 -0
- package/src/atoms/skeleton/AtomicSkeleton.tsx +143 -0
- package/src/atoms/skeleton/AtomicSkeleton.types.ts +55 -0
- package/src/atoms/skeleton/index.ts +9 -0
- package/src/index.ts +19 -0
- package/src/molecules/Container/Container.tsx +76 -0
- package/src/molecules/Container/index.ts +1 -0
- package/src/molecules/Grid/Grid.tsx +94 -0
- package/src/molecules/Grid/index.ts +1 -0
- package/src/molecules/List/List.tsx +85 -0
- package/src/molecules/List/index.ts +1 -0
- package/src/molecules/index.ts +5 -0
- package/src/organisms/FormLayout/FormLayout.tsx +117 -0
- package/src/organisms/FormLayout/index.ts +1 -0
- package/src/organisms/index.ts +6 -2
- package/src/theme/core/ResponsiveTokenFactory.ts +266 -0
- package/src/theme/hooks/useResponsiveDesignTokens.ts +82 -0
- package/src/theme/index.ts +13 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-design-system",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "Universal design system for React Native apps - Consolidated package with atoms, molecules, organisms, theme, typography, responsive and safe area utilities",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
package/src/atoms/AtomicText.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Text, StyleProp, TextStyle } from 'react-native';
|
|
3
|
-
import { useAppDesignTokens } from '../theme';
|
|
3
|
+
import { useAppDesignTokens, useResponsiveDesignTokens } from '../theme';
|
|
4
4
|
import type { TextStyleVariant, ColorVariant } from '../typography';
|
|
5
5
|
import { getTextColor } from '../typography';
|
|
6
6
|
|
|
@@ -13,6 +13,8 @@ export interface AtomicTextProps {
|
|
|
13
13
|
textAlign?: 'auto' | 'left' | 'right' | 'center' | 'justify';
|
|
14
14
|
style?: StyleProp<TextStyle>;
|
|
15
15
|
testID?: string;
|
|
16
|
+
/** Enable responsive font sizing (scales based on device) */
|
|
17
|
+
responsive?: boolean;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export const AtomicText: React.FC<AtomicTextProps> = ({
|
|
@@ -24,8 +26,13 @@ export const AtomicText: React.FC<AtomicTextProps> = ({
|
|
|
24
26
|
textAlign,
|
|
25
27
|
style,
|
|
26
28
|
testID,
|
|
29
|
+
responsive = false,
|
|
27
30
|
}) => {
|
|
28
|
-
const
|
|
31
|
+
const staticTokens = useAppDesignTokens();
|
|
32
|
+
const responsiveTokens = useResponsiveDesignTokens();
|
|
33
|
+
|
|
34
|
+
// Use responsive tokens if enabled, otherwise use static
|
|
35
|
+
const tokens = responsive ? responsiveTokens : staticTokens;
|
|
29
36
|
|
|
30
37
|
// Get typography style from tokens
|
|
31
38
|
const typographyStyle = (tokens.typography as Record<string, any>)[type];
|
|
@@ -33,10 +40,16 @@ export const AtomicText: React.FC<AtomicTextProps> = ({
|
|
|
33
40
|
// Get color from tokens or use custom color using utility function
|
|
34
41
|
const resolvedColor = getTextColor(color, tokens);
|
|
35
42
|
|
|
43
|
+
// Use responsive font size if enabled and available
|
|
44
|
+
const fontSize = responsive && typographyStyle.responsiveFontSize
|
|
45
|
+
? typographyStyle.responsiveFontSize
|
|
46
|
+
: typographyStyle.fontSize;
|
|
47
|
+
|
|
36
48
|
const textStyle: StyleProp<TextStyle> = [
|
|
37
49
|
typographyStyle,
|
|
38
50
|
{
|
|
39
51
|
color: resolvedColor,
|
|
52
|
+
...(fontSize && { fontSize }),
|
|
40
53
|
...(textAlign && { textAlign }),
|
|
41
54
|
},
|
|
42
55
|
style,
|
package/src/atoms/index.ts
CHANGED
|
@@ -70,3 +70,12 @@ export {
|
|
|
70
70
|
|
|
71
71
|
// DatePicker
|
|
72
72
|
export { AtomicDatePicker, type AtomicDatePickerProps } from './AtomicDatePicker';
|
|
73
|
+
|
|
74
|
+
// Skeleton
|
|
75
|
+
export {
|
|
76
|
+
AtomicSkeleton,
|
|
77
|
+
type AtomicSkeletonProps,
|
|
78
|
+
type SkeletonPattern,
|
|
79
|
+
type SkeletonConfig,
|
|
80
|
+
SKELETON_PATTERNS,
|
|
81
|
+
} from './skeleton';
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AtomicSkeleton - Skeleton Loader Component
|
|
3
|
+
*
|
|
4
|
+
* Skeleton placeholder loader with shimmer animation for loading states.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```tsx
|
|
8
|
+
* // List skeleton
|
|
9
|
+
* <AtomicSkeleton pattern="list" count={5} />
|
|
10
|
+
*
|
|
11
|
+
* // Card skeleton
|
|
12
|
+
* <AtomicSkeleton pattern="card" count={3} />
|
|
13
|
+
*
|
|
14
|
+
* // Custom skeleton
|
|
15
|
+
* <AtomicSkeleton
|
|
16
|
+
* pattern="custom"
|
|
17
|
+
* custom={[
|
|
18
|
+
* { width: '100%', height: 200, borderRadius: 12 },
|
|
19
|
+
* { width: '80%', height: 20, borderRadius: 4 }
|
|
20
|
+
* ]}
|
|
21
|
+
* />
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import React, { useEffect, useRef } from 'react';
|
|
26
|
+
import { View, StyleSheet, Animated, type StyleProp, type ViewStyle } from 'react-native';
|
|
27
|
+
import { useAppDesignTokens } from '../../theme';
|
|
28
|
+
import type { SkeletonPattern, SkeletonConfig } from './AtomicSkeleton.types';
|
|
29
|
+
import { SKELETON_PATTERNS } from './AtomicSkeleton.types';
|
|
30
|
+
|
|
31
|
+
const SHIMMER_DURATION = 1200;
|
|
32
|
+
|
|
33
|
+
export interface AtomicSkeletonProps {
|
|
34
|
+
/** Skeleton pattern preset */
|
|
35
|
+
pattern?: SkeletonPattern;
|
|
36
|
+
/** Custom skeleton configurations */
|
|
37
|
+
custom?: SkeletonConfig[];
|
|
38
|
+
/** Number of skeleton items to render */
|
|
39
|
+
count?: number;
|
|
40
|
+
/** Custom container style */
|
|
41
|
+
style?: StyleProp<ViewStyle>;
|
|
42
|
+
/** Disable shimmer animation */
|
|
43
|
+
disableAnimation?: boolean;
|
|
44
|
+
/** Test ID for testing */
|
|
45
|
+
testID?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Skeleton loader component with shimmer animation
|
|
50
|
+
*
|
|
51
|
+
* Provides visual feedback during content loading with customizable patterns
|
|
52
|
+
*/
|
|
53
|
+
export const AtomicSkeleton: React.FC<AtomicSkeletonProps> = ({
|
|
54
|
+
pattern = 'list',
|
|
55
|
+
custom,
|
|
56
|
+
count = 1,
|
|
57
|
+
style,
|
|
58
|
+
disableAnimation = false,
|
|
59
|
+
testID,
|
|
60
|
+
}) => {
|
|
61
|
+
const tokens = useAppDesignTokens();
|
|
62
|
+
const skeletonConfigs = pattern === 'custom' && custom
|
|
63
|
+
? custom
|
|
64
|
+
: SKELETON_PATTERNS[pattern];
|
|
65
|
+
|
|
66
|
+
const shimmerAnim = useRef(new Animated.Value(0)).current;
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (disableAnimation) return;
|
|
70
|
+
|
|
71
|
+
const shimmerAnimation = Animated.loop(
|
|
72
|
+
Animated.sequence([
|
|
73
|
+
Animated.timing(shimmerAnim, {
|
|
74
|
+
toValue: 1,
|
|
75
|
+
duration: SHIMMER_DURATION,
|
|
76
|
+
useNativeDriver: false,
|
|
77
|
+
}),
|
|
78
|
+
Animated.timing(shimmerAnim, {
|
|
79
|
+
toValue: 0,
|
|
80
|
+
duration: 0,
|
|
81
|
+
useNativeDriver: false,
|
|
82
|
+
}),
|
|
83
|
+
])
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
shimmerAnimation.start();
|
|
87
|
+
|
|
88
|
+
return () => {
|
|
89
|
+
shimmerAnimation.stop();
|
|
90
|
+
};
|
|
91
|
+
}, [shimmerAnim, disableAnimation]);
|
|
92
|
+
|
|
93
|
+
const backgroundColor = shimmerAnim.interpolate({
|
|
94
|
+
inputRange: [0, 0.5, 1],
|
|
95
|
+
outputRange: [
|
|
96
|
+
tokens.colors.surfaceSecondary,
|
|
97
|
+
tokens.colors.border,
|
|
98
|
+
tokens.colors.surfaceSecondary,
|
|
99
|
+
],
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const renderSkeletonItem = (index: number) => (
|
|
103
|
+
<View key={`skeleton-group-${index}`} style={styles.skeletonGroup}>
|
|
104
|
+
{skeletonConfigs.map((config, configIndex) => (
|
|
105
|
+
<Animated.View
|
|
106
|
+
key={`skeleton-${index}-${configIndex}`}
|
|
107
|
+
style={[
|
|
108
|
+
styles.skeleton,
|
|
109
|
+
{
|
|
110
|
+
width: config.width as number | `${number}%` | undefined,
|
|
111
|
+
height: config.height,
|
|
112
|
+
borderRadius: config.borderRadius,
|
|
113
|
+
marginBottom: config.marginBottom,
|
|
114
|
+
backgroundColor: disableAnimation
|
|
115
|
+
? tokens.colors.surfaceSecondary
|
|
116
|
+
: backgroundColor,
|
|
117
|
+
} as any,
|
|
118
|
+
]}
|
|
119
|
+
/>
|
|
120
|
+
))}
|
|
121
|
+
</View>
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<View style={[styles.container, style]} testID={testID}>
|
|
126
|
+
{Array.from({ length: count }).map((_, index) => renderSkeletonItem(index))}
|
|
127
|
+
</View>
|
|
128
|
+
);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
AtomicSkeleton.displayName = 'AtomicSkeleton';
|
|
132
|
+
|
|
133
|
+
const styles = StyleSheet.create({
|
|
134
|
+
container: {
|
|
135
|
+
width: '100%',
|
|
136
|
+
},
|
|
137
|
+
skeletonGroup: {
|
|
138
|
+
width: '100%',
|
|
139
|
+
},
|
|
140
|
+
skeleton: {
|
|
141
|
+
overflow: 'hidden',
|
|
142
|
+
},
|
|
143
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AtomicSkeleton Types
|
|
3
|
+
*
|
|
4
|
+
* Type definitions and configurations for skeleton loading states.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Skeleton loader pattern presets
|
|
9
|
+
*/
|
|
10
|
+
export type SkeletonPattern = 'list' | 'card' | 'profile' | 'text' | 'custom';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Skeleton loader configuration for individual skeleton elements
|
|
14
|
+
*/
|
|
15
|
+
export interface SkeletonConfig {
|
|
16
|
+
/** Width of skeleton element (number in pixels or percentage string) */
|
|
17
|
+
width?: number | string;
|
|
18
|
+
/** Height of skeleton element in pixels */
|
|
19
|
+
height?: number;
|
|
20
|
+
/** Border radius in pixels */
|
|
21
|
+
borderRadius?: number;
|
|
22
|
+
/** Bottom margin in pixels */
|
|
23
|
+
marginBottom?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Predefined skeleton pattern configurations
|
|
28
|
+
*
|
|
29
|
+
* - **list**: Single row skeleton for list items
|
|
30
|
+
* - **card**: Card-style skeleton with image and text placeholders
|
|
31
|
+
* - **profile**: Profile skeleton with avatar and text
|
|
32
|
+
* - **text**: Multiple text line skeletons
|
|
33
|
+
* - **custom**: Use custom configuration
|
|
34
|
+
*/
|
|
35
|
+
export const SKELETON_PATTERNS: Record<SkeletonPattern, SkeletonConfig[]> = {
|
|
36
|
+
list: [
|
|
37
|
+
{ width: '100%', height: 60, borderRadius: 8, marginBottom: 12 },
|
|
38
|
+
],
|
|
39
|
+
card: [
|
|
40
|
+
{ width: '100%', height: 200, borderRadius: 12, marginBottom: 16 },
|
|
41
|
+
{ width: '80%', height: 20, borderRadius: 4, marginBottom: 8 },
|
|
42
|
+
{ width: '60%', height: 16, borderRadius: 4, marginBottom: 0 },
|
|
43
|
+
],
|
|
44
|
+
profile: [
|
|
45
|
+
{ width: 80, height: 80, borderRadius: 40, marginBottom: 16 },
|
|
46
|
+
{ width: '60%', height: 24, borderRadius: 4, marginBottom: 8 },
|
|
47
|
+
{ width: '40%', height: 16, borderRadius: 4, marginBottom: 0 },
|
|
48
|
+
],
|
|
49
|
+
text: [
|
|
50
|
+
{ width: '100%', height: 16, borderRadius: 4, marginBottom: 8 },
|
|
51
|
+
{ width: '90%', height: 16, borderRadius: 4, marginBottom: 8 },
|
|
52
|
+
{ width: '95%', height: 16, borderRadius: 4, marginBottom: 0 },
|
|
53
|
+
],
|
|
54
|
+
custom: [],
|
|
55
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AtomicSkeleton - Skeleton Loader
|
|
3
|
+
*
|
|
4
|
+
* Exports skeleton loader component and types
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { AtomicSkeleton, type AtomicSkeletonProps } from './AtomicSkeleton';
|
|
8
|
+
export type { SkeletonPattern, SkeletonConfig } from './AtomicSkeleton.types';
|
|
9
|
+
export { SKELETON_PATTERNS } from './AtomicSkeleton.types';
|
package/src/index.ts
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
export {
|
|
20
20
|
useAppDesignTokens,
|
|
21
|
+
useResponsiveDesignTokens,
|
|
21
22
|
useCommonStyles,
|
|
22
23
|
useDesignSystemTheme,
|
|
23
24
|
useTheme,
|
|
@@ -33,6 +34,7 @@ export {
|
|
|
33
34
|
typography,
|
|
34
35
|
borders,
|
|
35
36
|
createDesignTokens,
|
|
37
|
+
createResponsiveDesignTokens,
|
|
36
38
|
lightTheme,
|
|
37
39
|
darkTheme,
|
|
38
40
|
createResponsiveValue,
|
|
@@ -51,6 +53,10 @@ export {
|
|
|
51
53
|
type AvatarSizes,
|
|
52
54
|
type ComponentSizes,
|
|
53
55
|
type DesignTokens,
|
|
56
|
+
type ResponsiveDesignTokens,
|
|
57
|
+
type ResponsiveSpacing,
|
|
58
|
+
type ResponsiveTypography,
|
|
59
|
+
type ResponsiveBorderRadius,
|
|
54
60
|
type Theme,
|
|
55
61
|
type ExtendedColorPalette,
|
|
56
62
|
type NavigationTheme,
|
|
@@ -136,6 +142,7 @@ export {
|
|
|
136
142
|
AtomicProgress,
|
|
137
143
|
AtomicPicker,
|
|
138
144
|
AtomicDatePicker,
|
|
145
|
+
AtomicSkeleton,
|
|
139
146
|
type IconName,
|
|
140
147
|
type IconSize,
|
|
141
148
|
type IconColor,
|
|
@@ -161,6 +168,10 @@ export {
|
|
|
161
168
|
type PickerOption,
|
|
162
169
|
type PickerSize,
|
|
163
170
|
type AtomicDatePickerProps,
|
|
171
|
+
type AtomicSkeletonProps,
|
|
172
|
+
type SkeletonPattern,
|
|
173
|
+
type SkeletonConfig,
|
|
174
|
+
SKELETON_PATTERNS,
|
|
164
175
|
} from './atoms';
|
|
165
176
|
|
|
166
177
|
// =============================================================================
|
|
@@ -177,7 +188,13 @@ export {
|
|
|
177
188
|
ConfirmationModal,
|
|
178
189
|
useConfirmationModal,
|
|
179
190
|
StepProgress,
|
|
191
|
+
Grid,
|
|
192
|
+
List,
|
|
193
|
+
Container,
|
|
180
194
|
type BaseModalProps,
|
|
195
|
+
type GridProps,
|
|
196
|
+
type ListProps,
|
|
197
|
+
type ContainerProps,
|
|
181
198
|
} from './molecules';
|
|
182
199
|
|
|
183
200
|
// =============================================================================
|
|
@@ -188,6 +205,8 @@ export {
|
|
|
188
205
|
ScreenLayout,
|
|
189
206
|
AppHeader,
|
|
190
207
|
FormContainer,
|
|
208
|
+
FormLayout,
|
|
209
|
+
type FormLayoutProps,
|
|
191
210
|
} from './organisms';
|
|
192
211
|
|
|
193
212
|
// =============================================================================
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Container - Max-Width Content Container (Molecule)
|
|
3
|
+
*
|
|
4
|
+
* Centers content with responsive max-width based on device
|
|
5
|
+
* Prevents content from being too wide on tablets
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useMemo } from 'react';
|
|
9
|
+
import { View, StyleSheet, type StyleProp, type ViewStyle } from 'react-native';
|
|
10
|
+
import { useResponsive } from '../../responsive';
|
|
11
|
+
import { useResponsiveDesignTokens } from '../../theme';
|
|
12
|
+
|
|
13
|
+
export interface ContainerProps {
|
|
14
|
+
/** Container content */
|
|
15
|
+
children: React.ReactNode;
|
|
16
|
+
|
|
17
|
+
/** Maximum width (default: responsive based on device) */
|
|
18
|
+
maxWidth?: number;
|
|
19
|
+
|
|
20
|
+
/** Horizontal padding (uses responsive tokens) */
|
|
21
|
+
padding?: boolean;
|
|
22
|
+
|
|
23
|
+
/** Center content horizontally */
|
|
24
|
+
center?: boolean;
|
|
25
|
+
|
|
26
|
+
/** Container style */
|
|
27
|
+
style?: StyleProp<ViewStyle>;
|
|
28
|
+
|
|
29
|
+
/** Test ID */
|
|
30
|
+
testID?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Responsive container with max-width
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```tsx
|
|
38
|
+
* <Container maxWidth={600} padding center>
|
|
39
|
+
* <Text>Centered content with max width</Text>
|
|
40
|
+
* </Container>
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export const Container: React.FC<ContainerProps> = ({
|
|
44
|
+
children,
|
|
45
|
+
maxWidth,
|
|
46
|
+
padding = true,
|
|
47
|
+
center = true,
|
|
48
|
+
style,
|
|
49
|
+
testID,
|
|
50
|
+
}) => {
|
|
51
|
+
const { maxContentWidth } = useResponsive();
|
|
52
|
+
const tokens = useResponsiveDesignTokens();
|
|
53
|
+
|
|
54
|
+
const containerWidth = maxWidth || maxContentWidth;
|
|
55
|
+
|
|
56
|
+
const styles = useMemo(
|
|
57
|
+
() =>
|
|
58
|
+
StyleSheet.create({
|
|
59
|
+
container: {
|
|
60
|
+
width: '100%',
|
|
61
|
+
maxWidth: containerWidth,
|
|
62
|
+
...(center && { alignSelf: 'center' }),
|
|
63
|
+
...(padding && {
|
|
64
|
+
paddingHorizontal: tokens.spacing.screenPadding,
|
|
65
|
+
}),
|
|
66
|
+
},
|
|
67
|
+
}),
|
|
68
|
+
[containerWidth, center, padding, tokens.spacing.screenPadding]
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<View style={[styles.container, style]} testID={testID}>
|
|
73
|
+
{children}
|
|
74
|
+
</View>
|
|
75
|
+
);
|
|
76
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Container, type ContainerProps } from './Container';
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grid - Responsive Grid Layout (Molecule)
|
|
3
|
+
*
|
|
4
|
+
* Automatic responsive grid that adjusts columns based on device
|
|
5
|
+
* Uses design system responsive utilities
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useMemo } from 'react';
|
|
9
|
+
import { View, StyleSheet, type StyleProp, type ViewStyle } from 'react-native';
|
|
10
|
+
import { useResponsive } from '../../responsive';
|
|
11
|
+
import { useResponsiveDesignTokens } from '../../theme';
|
|
12
|
+
|
|
13
|
+
export interface GridProps {
|
|
14
|
+
/** Grid items to render */
|
|
15
|
+
children: React.ReactNode;
|
|
16
|
+
|
|
17
|
+
/** Number of columns on mobile (default: 2) */
|
|
18
|
+
mobileColumns?: number;
|
|
19
|
+
|
|
20
|
+
/** Number of columns on tablet (default: 4) */
|
|
21
|
+
tabletColumns?: number;
|
|
22
|
+
|
|
23
|
+
/** Gap between grid items (uses design tokens spacing) */
|
|
24
|
+
gap?: number;
|
|
25
|
+
|
|
26
|
+
/** Container style */
|
|
27
|
+
style?: StyleProp<ViewStyle>;
|
|
28
|
+
|
|
29
|
+
/** Test ID for testing */
|
|
30
|
+
testID?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Responsive grid component
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```tsx
|
|
38
|
+
* <Grid mobileColumns={2} tabletColumns={4} gap={16}>
|
|
39
|
+
* <Card />
|
|
40
|
+
* <Card />
|
|
41
|
+
* <Card />
|
|
42
|
+
* </Grid>
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export const Grid: React.FC<GridProps> = ({
|
|
46
|
+
children,
|
|
47
|
+
mobileColumns = 2,
|
|
48
|
+
tabletColumns = 4,
|
|
49
|
+
gap,
|
|
50
|
+
style,
|
|
51
|
+
testID,
|
|
52
|
+
}) => {
|
|
53
|
+
const { gridColumns, spacingMultiplier } = useResponsive();
|
|
54
|
+
const tokens = useResponsiveDesignTokens();
|
|
55
|
+
|
|
56
|
+
// Calculate responsive columns
|
|
57
|
+
const columns = gridColumns || (mobileColumns && tabletColumns
|
|
58
|
+
? undefined
|
|
59
|
+
: mobileColumns);
|
|
60
|
+
|
|
61
|
+
// Use responsive gap or default
|
|
62
|
+
const responsiveGap = gap ? gap * spacingMultiplier : tokens.spacing.md;
|
|
63
|
+
|
|
64
|
+
const styles = useMemo(
|
|
65
|
+
() =>
|
|
66
|
+
StyleSheet.create({
|
|
67
|
+
container: {
|
|
68
|
+
flexDirection: 'row',
|
|
69
|
+
flexWrap: 'wrap',
|
|
70
|
+
gap: responsiveGap,
|
|
71
|
+
},
|
|
72
|
+
}),
|
|
73
|
+
[responsiveGap]
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
// Convert children to array for mapping
|
|
77
|
+
const childArray = React.Children.toArray(children);
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<View style={[styles.container, style]} testID={testID}>
|
|
81
|
+
{childArray.map((child, index) => (
|
|
82
|
+
<View
|
|
83
|
+
key={index}
|
|
84
|
+
style={{
|
|
85
|
+
flex: columns ? 1 / columns - 0.01 : undefined,
|
|
86
|
+
minWidth: columns ? `${100 / columns - 1}%` : undefined,
|
|
87
|
+
}}
|
|
88
|
+
>
|
|
89
|
+
{child}
|
|
90
|
+
</View>
|
|
91
|
+
))}
|
|
92
|
+
</View>
|
|
93
|
+
);
|
|
94
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Grid, type GridProps } from './Grid';
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List - Responsive List Wrapper (Molecule)
|
|
3
|
+
*
|
|
4
|
+
* FlatList wrapper with responsive item sizing and built-in pull-to-refresh
|
|
5
|
+
* Uses design system responsive utilities
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React from 'react';
|
|
9
|
+
import { FlatList, RefreshControl, type FlatListProps, type ListRenderItem } from 'react-native';
|
|
10
|
+
import { useResponsiveDesignTokens } from '../../theme';
|
|
11
|
+
|
|
12
|
+
export interface ListProps<T> extends Omit<FlatListProps<T>, 'renderItem'> {
|
|
13
|
+
/** Data array */
|
|
14
|
+
data: readonly T[] | null | undefined;
|
|
15
|
+
|
|
16
|
+
/** Render function for each item */
|
|
17
|
+
renderItem: ListRenderItem<T>;
|
|
18
|
+
|
|
19
|
+
/** Pull-to-refresh handler */
|
|
20
|
+
onRefresh?: () => void;
|
|
21
|
+
|
|
22
|
+
/** Refreshing state */
|
|
23
|
+
refreshing?: boolean;
|
|
24
|
+
|
|
25
|
+
/** Key extractor (required for proper list performance) */
|
|
26
|
+
keyExtractor: (item: T, index: number) => string;
|
|
27
|
+
|
|
28
|
+
/** Content container padding (uses responsive tokens) */
|
|
29
|
+
contentPadding?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Responsive list component with pull-to-refresh
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```tsx
|
|
37
|
+
* <List
|
|
38
|
+
* data={items}
|
|
39
|
+
* renderItem={({ item }) => <ItemCard item={item} />}
|
|
40
|
+
* keyExtractor={(item) => item.id}
|
|
41
|
+
* onRefresh={handleRefresh}
|
|
42
|
+
* refreshing={isRefreshing}
|
|
43
|
+
* contentPadding
|
|
44
|
+
* />
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export const List = <T,>({
|
|
48
|
+
data,
|
|
49
|
+
renderItem,
|
|
50
|
+
onRefresh,
|
|
51
|
+
refreshing = false,
|
|
52
|
+
keyExtractor,
|
|
53
|
+
contentPadding = false,
|
|
54
|
+
...rest
|
|
55
|
+
}: ListProps<T>) => {
|
|
56
|
+
const tokens = useResponsiveDesignTokens();
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<FlatList
|
|
60
|
+
data={data}
|
|
61
|
+
renderItem={renderItem}
|
|
62
|
+
keyExtractor={keyExtractor}
|
|
63
|
+
refreshControl={
|
|
64
|
+
onRefresh ? (
|
|
65
|
+
<RefreshControl
|
|
66
|
+
refreshing={refreshing}
|
|
67
|
+
onRefresh={onRefresh}
|
|
68
|
+
tintColor={tokens.colors.primary}
|
|
69
|
+
colors={[tokens.colors.primary]}
|
|
70
|
+
/>
|
|
71
|
+
) : undefined
|
|
72
|
+
}
|
|
73
|
+
contentContainerStyle={
|
|
74
|
+
contentPadding
|
|
75
|
+
? {
|
|
76
|
+
paddingHorizontal: tokens.spacing.screenPadding,
|
|
77
|
+
paddingBottom: tokens.spacing.lg,
|
|
78
|
+
}
|
|
79
|
+
: undefined
|
|
80
|
+
}
|
|
81
|
+
showsVerticalScrollIndicator={false}
|
|
82
|
+
{...rest}
|
|
83
|
+
/>
|
|
84
|
+
);
|
|
85
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { List, type ListProps } from './List';
|
package/src/molecules/index.ts
CHANGED
|
@@ -22,3 +22,8 @@ export type {
|
|
|
22
22
|
// Divider
|
|
23
23
|
export * from './Divider';
|
|
24
24
|
export * from "./StepProgress";
|
|
25
|
+
|
|
26
|
+
// Responsive Components
|
|
27
|
+
export { Grid, type GridProps } from './Grid';
|
|
28
|
+
export { List, type ListProps } from './List';
|
|
29
|
+
export { Container, type ContainerProps } from './Container';
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FormLayout - Responsive Form Container
|
|
3
|
+
*
|
|
4
|
+
* Organism for creating responsive forms with consistent spacing
|
|
5
|
+
* Automatically handles keyboard avoidance and scrolling
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useMemo } from 'react';
|
|
9
|
+
import { View, ScrollView, KeyboardAvoidingView, StyleSheet, type StyleProp, type ViewStyle } from 'react-native';
|
|
10
|
+
import { useResponsiveDesignTokens } from '../../theme';
|
|
11
|
+
import { useResponsive } from '../../responsive';
|
|
12
|
+
|
|
13
|
+
export interface FormLayoutProps {
|
|
14
|
+
/** Form fields and content */
|
|
15
|
+
children: React.ReactNode;
|
|
16
|
+
|
|
17
|
+
/** Footer content (e.g., submit button) */
|
|
18
|
+
footer?: React.ReactNode;
|
|
19
|
+
|
|
20
|
+
/** Form container style */
|
|
21
|
+
style?: StyleProp<ViewStyle>;
|
|
22
|
+
|
|
23
|
+
/** Disable keyboard avoiding behavior */
|
|
24
|
+
disableKeyboardAvoid?: boolean;
|
|
25
|
+
|
|
26
|
+
/** Disable scroll */
|
|
27
|
+
disableScroll?: boolean;
|
|
28
|
+
|
|
29
|
+
/** Test ID */
|
|
30
|
+
testID?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Responsive form layout with keyboard avoidance
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```tsx
|
|
38
|
+
* <FormLayout
|
|
39
|
+
* footer={<AtomicButton title="Submit" onPress={handleSubmit} />}
|
|
40
|
+
* >
|
|
41
|
+
* <FormField label="Name" value={name} onChangeText={setName} />
|
|
42
|
+
* <FormField label="Email" value={email} onChangeText={setEmail} />
|
|
43
|
+
* </FormLayout>
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export const FormLayout: React.FC<FormLayoutProps> = ({
|
|
47
|
+
children,
|
|
48
|
+
footer,
|
|
49
|
+
style,
|
|
50
|
+
disableKeyboardAvoid = false,
|
|
51
|
+
disableScroll = false,
|
|
52
|
+
testID,
|
|
53
|
+
}) => {
|
|
54
|
+
const tokens = useResponsiveDesignTokens();
|
|
55
|
+
const { insets } = useResponsive();
|
|
56
|
+
|
|
57
|
+
const styles = useMemo(
|
|
58
|
+
() =>
|
|
59
|
+
StyleSheet.create({
|
|
60
|
+
container: {
|
|
61
|
+
flex: 1,
|
|
62
|
+
},
|
|
63
|
+
scrollContent: {
|
|
64
|
+
flexGrow: 1,
|
|
65
|
+
paddingHorizontal: tokens.spacing.screenPadding,
|
|
66
|
+
paddingTop: tokens.spacing.md,
|
|
67
|
+
paddingBottom: tokens.spacing.xl,
|
|
68
|
+
},
|
|
69
|
+
formContent: {
|
|
70
|
+
gap: tokens.spacing.lg,
|
|
71
|
+
},
|
|
72
|
+
footer: {
|
|
73
|
+
paddingHorizontal: tokens.spacing.screenPadding,
|
|
74
|
+
paddingBottom: Math.max(insets.bottom, tokens.spacing.md),
|
|
75
|
+
paddingTop: tokens.spacing.md,
|
|
76
|
+
backgroundColor: tokens.colors.surface,
|
|
77
|
+
borderTopWidth: 1,
|
|
78
|
+
borderTopColor: tokens.colors.border,
|
|
79
|
+
},
|
|
80
|
+
}),
|
|
81
|
+
[tokens, insets.bottom]
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const content = (
|
|
85
|
+
<View style={styles.formContent} testID={testID}>
|
|
86
|
+
{children}
|
|
87
|
+
</View>
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const scrollableContent = disableScroll ? (
|
|
91
|
+
<View style={styles.scrollContent}>{content}</View>
|
|
92
|
+
) : (
|
|
93
|
+
<ScrollView
|
|
94
|
+
style={styles.container}
|
|
95
|
+
contentContainerStyle={styles.scrollContent}
|
|
96
|
+
keyboardShouldPersistTaps="handled"
|
|
97
|
+
showsVerticalScrollIndicator={false}
|
|
98
|
+
>
|
|
99
|
+
{content}
|
|
100
|
+
</ScrollView>
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const mainContent = disableKeyboardAvoid ? (
|
|
104
|
+
scrollableContent
|
|
105
|
+
) : (
|
|
106
|
+
<KeyboardAvoidingView style={styles.container} behavior="padding">
|
|
107
|
+
{scrollableContent}
|
|
108
|
+
</KeyboardAvoidingView>
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<View style={[styles.container, style]}>
|
|
113
|
+
{mainContent}
|
|
114
|
+
{footer && <View style={styles.footer}>{footer}</View>}
|
|
115
|
+
</View>
|
|
116
|
+
);
|
|
117
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { FormLayout, type FormLayoutProps } from './FormLayout';
|
package/src/organisms/index.ts
CHANGED
|
@@ -14,18 +14,22 @@
|
|
|
14
14
|
export { AppHeader } from './AppHeader';
|
|
15
15
|
export { ScreenLayout } from './ScreenLayout';
|
|
16
16
|
export { FormContainer } from './FormContainer';
|
|
17
|
+
export { FormLayout } from './FormLayout';
|
|
17
18
|
|
|
18
19
|
// Type exports
|
|
19
20
|
export type { AppHeaderProps } from './AppHeader';
|
|
20
21
|
export type { ScreenLayoutProps } from './ScreenLayout';
|
|
21
22
|
export type { FormContainerProps } from './FormContainer';
|
|
23
|
+
export type { FormLayoutProps } from './FormLayout';
|
|
22
24
|
|
|
23
25
|
// Union type for all organism props (used for type narrowing)
|
|
24
26
|
import type { AppHeaderProps } from './AppHeader';
|
|
25
27
|
import type { ScreenLayoutProps } from './ScreenLayout';
|
|
26
28
|
import type { FormContainerProps } from './FormContainer';
|
|
29
|
+
import type { FormLayoutProps } from './FormLayout';
|
|
27
30
|
|
|
28
|
-
export type OrganismComponentProps =
|
|
31
|
+
export type OrganismComponentProps =
|
|
29
32
|
| { type: 'AppHeader'; props: AppHeaderProps }
|
|
30
33
|
| { type: 'ScreenLayout'; props: ScreenLayoutProps }
|
|
31
|
-
| { type: 'FormContainer'; props: FormContainerProps }
|
|
34
|
+
| { type: 'FormContainer'; props: FormContainerProps }
|
|
35
|
+
| { type: 'FormLayout'; props: FormLayoutProps };
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RESPONSIVE TOKEN FACTORY
|
|
3
|
+
*
|
|
4
|
+
* ✅ Extends base TokenFactory with responsive capabilities
|
|
5
|
+
* ✅ Device-aware spacing, typography, and sizing
|
|
6
|
+
* ✅ Automatically scales all tokens based on device type
|
|
7
|
+
* ✅ Backward compatible with existing token system
|
|
8
|
+
*
|
|
9
|
+
* @module ResponsiveTokenFactory
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { BASE_TOKENS } from './BaseTokens';
|
|
13
|
+
import { createDesignTokens, type DesignTokens, type ThemeMode } from './TokenFactory';
|
|
14
|
+
import { type CustomThemeColors } from './CustomColors';
|
|
15
|
+
|
|
16
|
+
// =============================================================================
|
|
17
|
+
// RESPONSIVE DESIGN TOKENS TYPE
|
|
18
|
+
// =============================================================================
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Responsive spacing tokens that scale based on device
|
|
22
|
+
*/
|
|
23
|
+
export type ResponsiveSpacing = {
|
|
24
|
+
// Base Spacing Scale (scales with spacingMultiplier)
|
|
25
|
+
xs: number;
|
|
26
|
+
sm: number;
|
|
27
|
+
md: number;
|
|
28
|
+
lg: number;
|
|
29
|
+
xl: number;
|
|
30
|
+
xxl: number;
|
|
31
|
+
xxxl: number;
|
|
32
|
+
|
|
33
|
+
// Semantic Spacing (scales with device)
|
|
34
|
+
screenPadding: number;
|
|
35
|
+
cardPadding: number;
|
|
36
|
+
buttonPadding: number;
|
|
37
|
+
inputPadding: number;
|
|
38
|
+
sectionSpacing: number;
|
|
39
|
+
|
|
40
|
+
// Icon Sizes (scales with device)
|
|
41
|
+
iconSizeSmall: number;
|
|
42
|
+
iconSizeMedium: number;
|
|
43
|
+
iconSizeLarge: number;
|
|
44
|
+
iconSizeXLarge: number;
|
|
45
|
+
iconSizeHero: number;
|
|
46
|
+
|
|
47
|
+
// Component Heights (scales with device)
|
|
48
|
+
buttonHeight: number;
|
|
49
|
+
inputHeight: number;
|
|
50
|
+
appBarHeight: number;
|
|
51
|
+
tabBarHeight: number;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Responsive typography tokens that scale based on device
|
|
56
|
+
*/
|
|
57
|
+
export type ResponsiveTypography = typeof BASE_TOKENS.typography & {
|
|
58
|
+
// Each typography level gets responsive fontSize
|
|
59
|
+
displayLarge: typeof BASE_TOKENS.typography.displayLarge & { responsiveFontSize: number };
|
|
60
|
+
displayMedium: typeof BASE_TOKENS.typography.displayMedium & { responsiveFontSize: number };
|
|
61
|
+
displaySmall: typeof BASE_TOKENS.typography.displaySmall & { responsiveFontSize: number };
|
|
62
|
+
headlineLarge: typeof BASE_TOKENS.typography.headlineLarge & { responsiveFontSize: number };
|
|
63
|
+
headlineMedium: typeof BASE_TOKENS.typography.headlineMedium & { responsiveFontSize: number };
|
|
64
|
+
headlineSmall: typeof BASE_TOKENS.typography.headlineSmall & { responsiveFontSize: number };
|
|
65
|
+
titleLarge: typeof BASE_TOKENS.typography.titleLarge & { responsiveFontSize: number };
|
|
66
|
+
titleMedium: typeof BASE_TOKENS.typography.titleMedium & { responsiveFontSize: number };
|
|
67
|
+
titleSmall: typeof BASE_TOKENS.typography.titleSmall & { responsiveFontSize: number };
|
|
68
|
+
bodyLarge: typeof BASE_TOKENS.typography.bodyLarge & { responsiveFontSize: number };
|
|
69
|
+
bodyMedium: typeof BASE_TOKENS.typography.bodyMedium & { responsiveFontSize: number };
|
|
70
|
+
bodySmall: typeof BASE_TOKENS.typography.bodySmall & { responsiveFontSize: number };
|
|
71
|
+
labelLarge: typeof BASE_TOKENS.typography.labelLarge & { responsiveFontSize: number };
|
|
72
|
+
labelMedium: typeof BASE_TOKENS.typography.labelMedium & { responsiveFontSize: number };
|
|
73
|
+
labelSmall: typeof BASE_TOKENS.typography.labelSmall & { responsiveFontSize: number };
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Responsive border radius tokens that scale based on device
|
|
78
|
+
*/
|
|
79
|
+
export type ResponsiveBorderRadius = {
|
|
80
|
+
none: number;
|
|
81
|
+
xs: number;
|
|
82
|
+
sm: number;
|
|
83
|
+
md: number;
|
|
84
|
+
lg: number;
|
|
85
|
+
xl: number;
|
|
86
|
+
xxl: number;
|
|
87
|
+
full: number;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Complete responsive design tokens
|
|
92
|
+
* Extends base DesignTokens with responsive capabilities
|
|
93
|
+
*/
|
|
94
|
+
export type ResponsiveDesignTokens = Omit<DesignTokens, 'spacing' | 'typography' | 'borderRadius'> & {
|
|
95
|
+
spacing: ResponsiveSpacing;
|
|
96
|
+
typography: ResponsiveTypography;
|
|
97
|
+
borderRadius: ResponsiveBorderRadius;
|
|
98
|
+
|
|
99
|
+
// Original base tokens (for backward compatibility)
|
|
100
|
+
baseSpacing: typeof BASE_TOKENS.spacing;
|
|
101
|
+
baseTypography: typeof BASE_TOKENS.typography;
|
|
102
|
+
baseBorderRadius: typeof BASE_TOKENS.borders.radius;
|
|
103
|
+
|
|
104
|
+
// Responsive multiplier value
|
|
105
|
+
spacingMultiplier: number;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// =============================================================================
|
|
109
|
+
// RESPONSIVE TOKEN FACTORY FUNCTION
|
|
110
|
+
// =============================================================================
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Create responsive design tokens for a specific theme mode
|
|
114
|
+
*
|
|
115
|
+
* @param mode - Theme mode ('light' or 'dark')
|
|
116
|
+
* @param spacingMultiplier - Device-based spacing multiplier (from useResponsive)
|
|
117
|
+
* @param getFontSize - Function to get responsive font size (from useResponsive)
|
|
118
|
+
* @param customColors - Optional custom colors to override default colors
|
|
119
|
+
* @returns Complete responsive design tokens object
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```typescript
|
|
123
|
+
* const { spacingMultiplier, getFontSize } = useResponsive();
|
|
124
|
+
* const tokens = createResponsiveDesignTokens('light', spacingMultiplier, getFontSize);
|
|
125
|
+
*
|
|
126
|
+
* // Use in components
|
|
127
|
+
* <View style={{ padding: tokens.spacing.md }}> // Auto-scales: 16px * 1.2 = 19.2px on tablet
|
|
128
|
+
* <Text style={{ fontSize: tokens.typography.bodyLarge.responsiveFontSize }}>
|
|
129
|
+
* Hello!
|
|
130
|
+
* </Text>
|
|
131
|
+
* </View>
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
134
|
+
export const createResponsiveDesignTokens = (
|
|
135
|
+
mode: ThemeMode,
|
|
136
|
+
spacingMultiplier: number,
|
|
137
|
+
getFontSize: (baseFontSize: number) => number,
|
|
138
|
+
customColors?: CustomThemeColors,
|
|
139
|
+
): ResponsiveDesignTokens => {
|
|
140
|
+
// Get base tokens from existing factory
|
|
141
|
+
const baseTokens = createDesignTokens(mode, customColors);
|
|
142
|
+
|
|
143
|
+
// Create responsive spacing (multiply all base spacing values)
|
|
144
|
+
const responsiveSpacing: ResponsiveSpacing = {
|
|
145
|
+
// Base Spacing Scale
|
|
146
|
+
xs: BASE_TOKENS.spacing.xs * spacingMultiplier,
|
|
147
|
+
sm: BASE_TOKENS.spacing.sm * spacingMultiplier,
|
|
148
|
+
md: BASE_TOKENS.spacing.md * spacingMultiplier,
|
|
149
|
+
lg: BASE_TOKENS.spacing.lg * spacingMultiplier,
|
|
150
|
+
xl: BASE_TOKENS.spacing.xl * spacingMultiplier,
|
|
151
|
+
xxl: BASE_TOKENS.spacing.xxl * spacingMultiplier,
|
|
152
|
+
xxxl: BASE_TOKENS.spacing.xxxl * spacingMultiplier,
|
|
153
|
+
|
|
154
|
+
// Semantic Spacing
|
|
155
|
+
screenPadding: BASE_TOKENS.spacing.screenPadding * spacingMultiplier,
|
|
156
|
+
cardPadding: BASE_TOKENS.spacing.cardPadding * spacingMultiplier,
|
|
157
|
+
buttonPadding: BASE_TOKENS.spacing.buttonPadding * spacingMultiplier,
|
|
158
|
+
inputPadding: BASE_TOKENS.spacing.inputPadding * spacingMultiplier,
|
|
159
|
+
sectionSpacing: BASE_TOKENS.spacing.sectionSpacing * spacingMultiplier,
|
|
160
|
+
|
|
161
|
+
// Icon Sizes
|
|
162
|
+
iconSizeSmall: Math.round(BASE_TOKENS.spacing.iconSizeSmall * spacingMultiplier),
|
|
163
|
+
iconSizeMedium: Math.round(BASE_TOKENS.spacing.iconSizeMedium * spacingMultiplier),
|
|
164
|
+
iconSizeLarge: Math.round(BASE_TOKENS.spacing.iconSizeLarge * spacingMultiplier),
|
|
165
|
+
iconSizeXLarge: Math.round(BASE_TOKENS.spacing.iconSizeXLarge * spacingMultiplier),
|
|
166
|
+
iconSizeHero: Math.round(BASE_TOKENS.spacing.iconSizeHero * spacingMultiplier),
|
|
167
|
+
|
|
168
|
+
// Component Heights
|
|
169
|
+
buttonHeight: Math.round(BASE_TOKENS.spacing.buttonHeight * spacingMultiplier),
|
|
170
|
+
inputHeight: Math.round(BASE_TOKENS.spacing.inputHeight * spacingMultiplier),
|
|
171
|
+
appBarHeight: Math.round(BASE_TOKENS.spacing.appBarHeight * spacingMultiplier),
|
|
172
|
+
tabBarHeight: Math.round(BASE_TOKENS.spacing.tabBarHeight * spacingMultiplier),
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// Create responsive typography (add responsiveFontSize to each level)
|
|
176
|
+
const responsiveTypography: ResponsiveTypography = {
|
|
177
|
+
displayLarge: {
|
|
178
|
+
...BASE_TOKENS.typography.displayLarge,
|
|
179
|
+
responsiveFontSize: getFontSize(BASE_TOKENS.typography.displayLarge.fontSize!),
|
|
180
|
+
},
|
|
181
|
+
displayMedium: {
|
|
182
|
+
...BASE_TOKENS.typography.displayMedium,
|
|
183
|
+
responsiveFontSize: getFontSize(BASE_TOKENS.typography.displayMedium.fontSize!),
|
|
184
|
+
},
|
|
185
|
+
displaySmall: {
|
|
186
|
+
...BASE_TOKENS.typography.displaySmall,
|
|
187
|
+
responsiveFontSize: getFontSize(BASE_TOKENS.typography.displaySmall.fontSize!),
|
|
188
|
+
},
|
|
189
|
+
headlineLarge: {
|
|
190
|
+
...BASE_TOKENS.typography.headlineLarge,
|
|
191
|
+
responsiveFontSize: getFontSize(BASE_TOKENS.typography.headlineLarge.fontSize!),
|
|
192
|
+
},
|
|
193
|
+
headlineMedium: {
|
|
194
|
+
...BASE_TOKENS.typography.headlineMedium,
|
|
195
|
+
responsiveFontSize: getFontSize(BASE_TOKENS.typography.headlineMedium.fontSize!),
|
|
196
|
+
},
|
|
197
|
+
headlineSmall: {
|
|
198
|
+
...BASE_TOKENS.typography.headlineSmall,
|
|
199
|
+
responsiveFontSize: getFontSize(BASE_TOKENS.typography.headlineSmall.fontSize!),
|
|
200
|
+
},
|
|
201
|
+
titleLarge: {
|
|
202
|
+
...BASE_TOKENS.typography.titleLarge,
|
|
203
|
+
responsiveFontSize: getFontSize(BASE_TOKENS.typography.titleLarge.fontSize!),
|
|
204
|
+
},
|
|
205
|
+
titleMedium: {
|
|
206
|
+
...BASE_TOKENS.typography.titleMedium,
|
|
207
|
+
responsiveFontSize: getFontSize(BASE_TOKENS.typography.titleMedium.fontSize!),
|
|
208
|
+
},
|
|
209
|
+
titleSmall: {
|
|
210
|
+
...BASE_TOKENS.typography.titleSmall,
|
|
211
|
+
responsiveFontSize: getFontSize(BASE_TOKENS.typography.titleSmall.fontSize!),
|
|
212
|
+
},
|
|
213
|
+
bodyLarge: {
|
|
214
|
+
...BASE_TOKENS.typography.bodyLarge,
|
|
215
|
+
responsiveFontSize: getFontSize(BASE_TOKENS.typography.bodyLarge.fontSize!),
|
|
216
|
+
},
|
|
217
|
+
bodyMedium: {
|
|
218
|
+
...BASE_TOKENS.typography.bodyMedium,
|
|
219
|
+
responsiveFontSize: getFontSize(BASE_TOKENS.typography.bodyMedium.fontSize!),
|
|
220
|
+
},
|
|
221
|
+
bodySmall: {
|
|
222
|
+
...BASE_TOKENS.typography.bodySmall,
|
|
223
|
+
responsiveFontSize: getFontSize(BASE_TOKENS.typography.bodySmall.fontSize!),
|
|
224
|
+
},
|
|
225
|
+
labelLarge: {
|
|
226
|
+
...BASE_TOKENS.typography.labelLarge,
|
|
227
|
+
responsiveFontSize: getFontSize(BASE_TOKENS.typography.labelLarge.fontSize!),
|
|
228
|
+
},
|
|
229
|
+
labelMedium: {
|
|
230
|
+
...BASE_TOKENS.typography.labelMedium,
|
|
231
|
+
responsiveFontSize: getFontSize(BASE_TOKENS.typography.labelMedium.fontSize!),
|
|
232
|
+
},
|
|
233
|
+
labelSmall: {
|
|
234
|
+
...BASE_TOKENS.typography.labelSmall,
|
|
235
|
+
responsiveFontSize: getFontSize(BASE_TOKENS.typography.labelSmall.fontSize!),
|
|
236
|
+
},
|
|
237
|
+
} as ResponsiveTypography;
|
|
238
|
+
|
|
239
|
+
// Create responsive border radius
|
|
240
|
+
const responsiveBorderRadius: ResponsiveBorderRadius = {
|
|
241
|
+
none: 0,
|
|
242
|
+
xs: Math.round(BASE_TOKENS.borders.radius.xs * spacingMultiplier),
|
|
243
|
+
sm: Math.round(BASE_TOKENS.borders.radius.sm * spacingMultiplier),
|
|
244
|
+
md: Math.round(BASE_TOKENS.borders.radius.md * spacingMultiplier),
|
|
245
|
+
lg: Math.round(BASE_TOKENS.borders.radius.lg * spacingMultiplier),
|
|
246
|
+
xl: Math.round(BASE_TOKENS.borders.radius.xl * spacingMultiplier),
|
|
247
|
+
xxl: Math.round(BASE_TOKENS.borders.radius.xxl * spacingMultiplier),
|
|
248
|
+
full: 9999, // Always full circle
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
// Return complete responsive tokens
|
|
252
|
+
return {
|
|
253
|
+
...baseTokens,
|
|
254
|
+
spacing: responsiveSpacing,
|
|
255
|
+
typography: responsiveTypography,
|
|
256
|
+
borderRadius: responsiveBorderRadius,
|
|
257
|
+
|
|
258
|
+
// Keep original base tokens for backward compatibility
|
|
259
|
+
baseSpacing: BASE_TOKENS.spacing,
|
|
260
|
+
baseTypography: BASE_TOKENS.typography,
|
|
261
|
+
baseBorderRadius: BASE_TOKENS.borders.radius,
|
|
262
|
+
|
|
263
|
+
// Store multiplier for reference
|
|
264
|
+
spacingMultiplier,
|
|
265
|
+
};
|
|
266
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useResponsiveDesignTokens Hook
|
|
3
|
+
*
|
|
4
|
+
* ✅ Combines theme system + responsive utilities
|
|
5
|
+
* ✅ Returns device-aware design tokens
|
|
6
|
+
* ✅ Auto-updates on theme changes, orientation changes, screen resize
|
|
7
|
+
* ✅ Drop-in replacement for useAppDesignTokens with responsive capabilities
|
|
8
|
+
*
|
|
9
|
+
* @module useResponsiveDesignTokens
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { useMemo } from 'react';
|
|
13
|
+
import { useDesignSystemTheme } from '../infrastructure/globalThemeStore';
|
|
14
|
+
import { createResponsiveDesignTokens, type ResponsiveDesignTokens } from '../core/ResponsiveTokenFactory';
|
|
15
|
+
import { useResponsive } from '../../responsive/useResponsive';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Hook for responsive design tokens
|
|
19
|
+
*
|
|
20
|
+
* Returns complete design tokens with automatic responsive scaling based on device type.
|
|
21
|
+
* All spacing, typography, and border radius values automatically scale for tablets and large phones.
|
|
22
|
+
*
|
|
23
|
+
* @returns ResponsiveDesignTokens - Complete tokens with responsive spacing, typography, borders
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* import { useResponsiveDesignTokens } from '@umituz/react-native-design-system';
|
|
28
|
+
*
|
|
29
|
+
* const MyComponent = () => {
|
|
30
|
+
* const tokens = useResponsiveDesignTokens();
|
|
31
|
+
*
|
|
32
|
+
* return (
|
|
33
|
+
* <View style={{
|
|
34
|
+
* padding: tokens.spacing.md, // Auto-scales: 16px on phone, 19.2px on tablet
|
|
35
|
+
* borderRadius: tokens.borderRadius.lg, // Auto-scales based on device
|
|
36
|
+
* }}>
|
|
37
|
+
* <Text style={{
|
|
38
|
+
* fontSize: tokens.typography.bodyLarge.responsiveFontSize, // Responsive font
|
|
39
|
+
* color: tokens.colors.textPrimary, // Theme-aware color
|
|
40
|
+
* }}>
|
|
41
|
+
* Hello World!
|
|
42
|
+
* </Text>
|
|
43
|
+
* </View>
|
|
44
|
+
* );
|
|
45
|
+
* };
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* @example Using backward-compatible base tokens
|
|
49
|
+
* ```typescript
|
|
50
|
+
* const tokens = useResponsiveDesignTokens();
|
|
51
|
+
*
|
|
52
|
+
* // Use responsive tokens (recommended)
|
|
53
|
+
* const padding = tokens.spacing.md; // 16px * spacingMultiplier
|
|
54
|
+
*
|
|
55
|
+
* // Use original base tokens (backward compatibility)
|
|
56
|
+
* const basePadding = tokens.baseSpacing.md; // Always 16px
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* @example Manual responsive calculation
|
|
60
|
+
* ```typescript
|
|
61
|
+
* const tokens = useResponsiveDesignTokens();
|
|
62
|
+
*
|
|
63
|
+
* // Custom responsive value
|
|
64
|
+
* const customPadding = 20 * tokens.spacingMultiplier; // 20px * 1.2 = 24px on tablet
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export const useResponsiveDesignTokens = (): ResponsiveDesignTokens => {
|
|
68
|
+
// Get current theme mode and custom colors from theme store
|
|
69
|
+
const { themeMode, customColors } = useDesignSystemTheme();
|
|
70
|
+
|
|
71
|
+
// Get responsive utilities
|
|
72
|
+
const { spacingMultiplier, getFontSize } = useResponsive();
|
|
73
|
+
|
|
74
|
+
// Create and memoize responsive tokens
|
|
75
|
+
// Recalculates when: theme changes, screen size changes, orientation changes
|
|
76
|
+
const responsiveTokens = useMemo(
|
|
77
|
+
() => createResponsiveDesignTokens(themeMode, spacingMultiplier, getFontSize, customColors),
|
|
78
|
+
[themeMode, spacingMultiplier, getFontSize, customColors]
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
return responsiveTokens;
|
|
82
|
+
};
|
package/src/theme/index.ts
CHANGED
|
@@ -55,11 +55,24 @@ export {
|
|
|
55
55
|
type DesignTokens,
|
|
56
56
|
} from './core/TokenFactory';
|
|
57
57
|
|
|
58
|
+
// =============================================================================
|
|
59
|
+
// RESPONSIVE TOKEN FACTORY
|
|
60
|
+
// =============================================================================
|
|
61
|
+
|
|
62
|
+
export {
|
|
63
|
+
createResponsiveDesignTokens,
|
|
64
|
+
type ResponsiveDesignTokens,
|
|
65
|
+
type ResponsiveSpacing,
|
|
66
|
+
type ResponsiveTypography,
|
|
67
|
+
type ResponsiveBorderRadius,
|
|
68
|
+
} from './core/ResponsiveTokenFactory';
|
|
69
|
+
|
|
58
70
|
// =============================================================================
|
|
59
71
|
// HOOKS
|
|
60
72
|
// =============================================================================
|
|
61
73
|
|
|
62
74
|
export { useAppDesignTokens } from './hooks/useAppDesignTokens';
|
|
75
|
+
export { useResponsiveDesignTokens } from './hooks/useResponsiveDesignTokens';
|
|
63
76
|
export { useDesignSystemTheme } from './infrastructure/globalThemeStore';
|
|
64
77
|
export { useTheme } from './infrastructure/stores/themeStore';
|
|
65
78
|
export { useThemedStyles, useThemedStyleSheet } from './hooks/useThemedStyles';
|