@umituz/react-native-design-system 4.25.95 → 4.25.97
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-design-system",
|
|
3
|
-
"version": "4.25.
|
|
3
|
+
"version": "4.25.97",
|
|
4
4
|
"description": "Universal design system for React Native apps - Consolidated package with atoms, molecules, organisms, theme, typography, responsive, safe area, exception, infinite scroll, UUID, image, timezone, offline, onboarding, and loading utilities",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IconGrid - Reusable icon card grid
|
|
3
|
+
*
|
|
4
|
+
* Self-measuring grid that calculates item widths from actual container width.
|
|
5
|
+
* Works correctly inside ScreenLayout (or any padded container) without needing
|
|
6
|
+
* to know the parent's padding upfront.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React, { useCallback, useMemo, useState } from 'react';
|
|
10
|
+
import {
|
|
11
|
+
View,
|
|
12
|
+
TouchableOpacity,
|
|
13
|
+
StyleSheet,
|
|
14
|
+
type LayoutChangeEvent,
|
|
15
|
+
type StyleProp,
|
|
16
|
+
type ViewStyle,
|
|
17
|
+
} from 'react-native';
|
|
18
|
+
import { useAppDesignTokens } from '../../theme';
|
|
19
|
+
import { AtomicIcon } from '../../atoms';
|
|
20
|
+
import { AtomicText } from '../../atoms';
|
|
21
|
+
import type { IconName } from '../../atoms';
|
|
22
|
+
|
|
23
|
+
export interface IconGridItem {
|
|
24
|
+
/** Unique identifier */
|
|
25
|
+
id: string;
|
|
26
|
+
/** Label shown below the icon */
|
|
27
|
+
label: string;
|
|
28
|
+
/** Icon name (AtomicIcon / Lucide-based) */
|
|
29
|
+
icon: IconName;
|
|
30
|
+
/** Called when the card is tapped */
|
|
31
|
+
onPress: () => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface IconGridProps {
|
|
35
|
+
/** Items to display */
|
|
36
|
+
items: IconGridItem[];
|
|
37
|
+
/** Number of columns (default: 3) */
|
|
38
|
+
columns?: number;
|
|
39
|
+
/** Gap between items in pixels (default: 10) */
|
|
40
|
+
gap?: number;
|
|
41
|
+
/** Vertical gap between rows (default: same as gap) */
|
|
42
|
+
rowGap?: number;
|
|
43
|
+
/** Optional container style */
|
|
44
|
+
style?: StyleProp<ViewStyle>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A self-sizing icon card grid.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```tsx
|
|
52
|
+
* <IconGrid
|
|
53
|
+
* columns={3}
|
|
54
|
+
* items={[
|
|
55
|
+
* { id: 'aging', label: 'Aging', icon: 'flash', onPress: () => navigate('Aging') },
|
|
56
|
+
* { id: 'wardrobe', label: 'Wardrobe', icon: 'shirt', onPress: () => navigate('Wardrobe') },
|
|
57
|
+
* ]}
|
|
58
|
+
* />
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export const IconGrid: React.FC<IconGridProps> = ({
|
|
62
|
+
items,
|
|
63
|
+
columns = 3,
|
|
64
|
+
gap = 10,
|
|
65
|
+
rowGap,
|
|
66
|
+
style,
|
|
67
|
+
}) => {
|
|
68
|
+
const tokens = useAppDesignTokens();
|
|
69
|
+
const [containerWidth, setContainerWidth] = useState(0);
|
|
70
|
+
|
|
71
|
+
const handleLayout = useCallback(
|
|
72
|
+
(e: LayoutChangeEvent) => {
|
|
73
|
+
const { width } = e.nativeEvent.layout;
|
|
74
|
+
if (width > 0 && width !== containerWidth) {
|
|
75
|
+
setContainerWidth(width);
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
[containerWidth],
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
// Total gap space between columns (N columns = N-1 gaps)
|
|
82
|
+
const itemWidth = useMemo(() => {
|
|
83
|
+
if (containerWidth <= 0) return 0;
|
|
84
|
+
const totalGap = gap * (columns - 1);
|
|
85
|
+
// Subtract 1px safety margin to prevent sub-pixel wrapping
|
|
86
|
+
return Math.floor((containerWidth - totalGap) / columns) - 1;
|
|
87
|
+
}, [containerWidth, columns, gap]);
|
|
88
|
+
|
|
89
|
+
const { cardBackground, borderLight, textPrimary } = tokens.colors;
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<View
|
|
93
|
+
style={[styles.grid, { columnGap: gap, rowGap: rowGap ?? gap }, style]}
|
|
94
|
+
onLayout={handleLayout}
|
|
95
|
+
>
|
|
96
|
+
{items.map((item) =>
|
|
97
|
+
itemWidth > 0 ? (
|
|
98
|
+
<TouchableOpacity
|
|
99
|
+
key={item.id}
|
|
100
|
+
activeOpacity={0.7}
|
|
101
|
+
onPress={item.onPress}
|
|
102
|
+
style={[styles.card, { width: itemWidth }]}
|
|
103
|
+
>
|
|
104
|
+
<View
|
|
105
|
+
style={[
|
|
106
|
+
styles.iconBox,
|
|
107
|
+
{ width: itemWidth, backgroundColor: cardBackground, borderColor: borderLight },
|
|
108
|
+
]}
|
|
109
|
+
>
|
|
110
|
+
<AtomicIcon name={item.icon} size="lg" color="textPrimary" />
|
|
111
|
+
</View>
|
|
112
|
+
<AtomicText
|
|
113
|
+
style={[styles.label, { color: textPrimary }]}
|
|
114
|
+
numberOfLines={1}
|
|
115
|
+
>
|
|
116
|
+
{item.label}
|
|
117
|
+
</AtomicText>
|
|
118
|
+
</TouchableOpacity>
|
|
119
|
+
) : (
|
|
120
|
+
// Placeholder — keeps grid stable before first layout measurement
|
|
121
|
+
<View key={item.id} style={{ width: 0, height: 0 }} />
|
|
122
|
+
),
|
|
123
|
+
)}
|
|
124
|
+
</View>
|
|
125
|
+
);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const styles = StyleSheet.create({
|
|
129
|
+
grid: {
|
|
130
|
+
flexDirection: 'row',
|
|
131
|
+
flexWrap: 'wrap',
|
|
132
|
+
},
|
|
133
|
+
card: {
|
|
134
|
+
alignItems: 'center',
|
|
135
|
+
gap: 8,
|
|
136
|
+
},
|
|
137
|
+
iconBox: {
|
|
138
|
+
aspectRatio: 1,
|
|
139
|
+
borderRadius: 24,
|
|
140
|
+
alignItems: 'center',
|
|
141
|
+
justifyContent: 'center',
|
|
142
|
+
borderWidth: 1,
|
|
143
|
+
},
|
|
144
|
+
label: {
|
|
145
|
+
fontSize: 11,
|
|
146
|
+
fontWeight: '700',
|
|
147
|
+
textAlign: 'center',
|
|
148
|
+
},
|
|
149
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { IconGrid, type IconGridItem, type IconGridProps } from './IconGrid';
|