@umituz/react-native-design-system 1.15.0 → 2.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.
- package/package.json +26 -19
- package/src/atoms/AtomicAvatar.tsx +161 -0
- package/src/atoms/AtomicButton.tsx +241 -0
- package/src/atoms/AtomicCard.tsx +84 -0
- package/src/atoms/AtomicChip.tsx +226 -0
- package/src/atoms/AtomicDatePicker.tsx +255 -0
- package/src/atoms/AtomicFab.tsx +99 -0
- package/src/atoms/AtomicIcon.tsx +149 -0
- package/src/atoms/AtomicInput.tsx +308 -0
- package/src/atoms/AtomicPicker.tsx +310 -0
- package/src/atoms/AtomicProgress.tsx +149 -0
- package/src/atoms/AtomicText.tsx +55 -0
- package/src/atoms/__tests__/AtomicButton.test.tsx +107 -0
- package/src/atoms/__tests__/AtomicIcon.test.tsx +110 -0
- package/src/atoms/__tests__/AtomicInput.test.tsx +195 -0
- package/src/atoms/datepicker/components/DatePickerButton.tsx +112 -0
- package/src/atoms/datepicker/components/DatePickerModal.tsx +143 -0
- package/src/atoms/fab/styles/fabStyles.ts +98 -0
- package/src/atoms/fab/types/index.ts +88 -0
- package/src/atoms/index.ts +70 -0
- package/src/atoms/input/hooks/useInputState.ts +63 -0
- package/src/atoms/input/styles/inputStylesHelper.ts +120 -0
- package/src/atoms/picker/components/PickerChips.tsx +57 -0
- package/src/atoms/picker/components/PickerModal.tsx +214 -0
- package/src/atoms/picker/styles/pickerStyles.ts +223 -0
- package/src/atoms/picker/types/index.ts +42 -0
- package/src/index.ts +133 -52
- package/src/molecules/ConfirmationModal.tsx +42 -0
- package/src/molecules/ConfirmationModalContent.tsx +87 -0
- package/src/molecules/ConfirmationModalMain.tsx +91 -0
- package/src/molecules/FormField.tsx +155 -0
- package/src/molecules/IconContainer.tsx +79 -0
- package/src/molecules/ListItem.tsx +35 -0
- package/src/molecules/ScreenHeader.tsx +171 -0
- package/src/molecules/SearchBar.tsx +198 -0
- package/src/molecules/confirmation-modal/components.tsx +94 -0
- package/src/molecules/confirmation-modal/index.ts +7 -0
- package/src/molecules/confirmation-modal/styles/confirmationModalStyles.ts +133 -0
- package/src/molecules/confirmation-modal/types/index.ts +41 -0
- package/src/molecules/confirmation-modal/useConfirmationModal.ts +50 -0
- package/src/molecules/index.ts +19 -0
- package/src/molecules/listitem/index.ts +6 -0
- package/src/molecules/listitem/styles/listItemStyles.ts +37 -0
- package/src/molecules/listitem/types/index.ts +21 -0
- package/src/organisms/AppHeader.tsx +136 -0
- package/src/organisms/FormContainer.tsx +169 -0
- package/src/organisms/ScreenLayout.tsx +183 -0
- package/src/organisms/index.ts +31 -0
- package/src/responsive/config.ts +139 -0
- package/src/responsive/deviceDetection.ts +155 -0
- package/src/responsive/gridUtils.ts +79 -0
- package/src/responsive/index.ts +52 -0
- package/src/responsive/platformConstants.ts +98 -0
- package/src/responsive/responsive.ts +61 -0
- package/src/responsive/responsiveLayout.ts +137 -0
- package/src/responsive/responsiveSizing.ts +134 -0
- package/src/responsive/useResponsive.ts +140 -0
- package/src/responsive/validation.ts +158 -0
- package/src/theme/core/BaseTokens.ts +42 -0
- package/src/theme/core/ColorPalette.ts +29 -0
- package/src/theme/core/CustomColors.ts +122 -0
- package/src/theme/core/NavigationTheme.ts +72 -0
- package/src/theme/core/TokenFactory.ts +103 -0
- package/src/theme/core/colors/ColorUtils.ts +53 -0
- package/src/theme/core/colors/DarkColors.ts +146 -0
- package/src/theme/core/colors/LightColors.ts +146 -0
- package/src/theme/core/constants/DesignConstants.ts +31 -0
- package/src/theme/core/themes.ts +118 -0
- package/src/theme/core/tokens/BaseTokens.ts +144 -0
- package/src/theme/core/tokens/Borders.ts +43 -0
- package/src/theme/core/tokens/Sizes.ts +51 -0
- package/src/theme/core/tokens/Spacing.ts +38 -0
- package/src/theme/core/tokens/Typography.ts +143 -0
- package/src/theme/hooks/useAppDesignTokens.ts +45 -0
- package/src/theme/hooks/useCommonStyles.ts +248 -0
- package/src/theme/hooks/useThemedStyles.ts +68 -0
- package/src/theme/index.ts +94 -0
- package/src/theme/infrastructure/globalThemeStore.ts +69 -0
- package/src/theme/infrastructure/storage/ThemeStorage.ts +93 -0
- package/src/theme/infrastructure/stores/themeStore.ts +109 -0
- package/src/typography/__tests__/colorValidationUtils.test.ts +180 -0
- package/src/typography/__tests__/textColorUtils.test.ts +185 -0
- package/src/typography/__tests__/textStyleUtils.test.ts +168 -0
- package/src/typography/domain/entities/TypographyTypes.ts +88 -0
- package/src/typography/index.ts +53 -0
- package/src/typography/presentation/utils/colorValidationUtils.ts +133 -0
- package/src/typography/presentation/utils/textColorUtils.ts +205 -0
- package/src/typography/presentation/utils/textStyleUtils.ts +159 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grid Utilities
|
|
3
|
+
* Responsive grid sizing and column calculations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { getScreenDimensions } from './deviceDetection';
|
|
7
|
+
import { DEVICE_BREAKPOINTS, GRID_CONFIG } from './config';
|
|
8
|
+
import { validateNumber } from './validation';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Responsive grid columns
|
|
12
|
+
* Returns number of columns for grid layouts
|
|
13
|
+
*
|
|
14
|
+
* @param mobileColumns - Number of columns for mobile devices (default: 2)
|
|
15
|
+
* @param tabletColumns - Number of columns for tablet devices (default: 4)
|
|
16
|
+
* @returns Responsive number of grid columns
|
|
17
|
+
*/
|
|
18
|
+
export const getResponsiveGridColumns = (
|
|
19
|
+
mobileColumns: number = GRID_CONFIG.DEFAULT_MOBILE_COLUMNS,
|
|
20
|
+
tabletColumns: number = GRID_CONFIG.DEFAULT_TABLET_COLUMNS
|
|
21
|
+
): number => {
|
|
22
|
+
try {
|
|
23
|
+
const validatedMobile = validateNumber(mobileColumns, 'mobileColumns', 1, 20);
|
|
24
|
+
const validatedTablet = validateNumber(tabletColumns, 'tabletColumns', 1, 20);
|
|
25
|
+
|
|
26
|
+
const { width } = getScreenDimensions();
|
|
27
|
+
return width >= DEVICE_BREAKPOINTS.TABLET ? validatedTablet : validatedMobile;
|
|
28
|
+
} catch {
|
|
29
|
+
return 2;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export interface GridCellSizeConfig {
|
|
34
|
+
columns: number;
|
|
35
|
+
rows: number;
|
|
36
|
+
horizontalPadding?: number;
|
|
37
|
+
verticalPadding?: number;
|
|
38
|
+
gap?: number;
|
|
39
|
+
headerHeight?: number;
|
|
40
|
+
tabBarHeight?: number;
|
|
41
|
+
statusBarHeight?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Responsive grid cell size
|
|
46
|
+
* Calculates optimal square cell size for a grid that fills available space
|
|
47
|
+
*
|
|
48
|
+
* @param config - Grid configuration
|
|
49
|
+
* @returns Responsive cell size (width = height for square cells)
|
|
50
|
+
*/
|
|
51
|
+
export const getResponsiveGridCellSize = (config: GridCellSizeConfig): number => {
|
|
52
|
+
try {
|
|
53
|
+
const {
|
|
54
|
+
columns,
|
|
55
|
+
rows,
|
|
56
|
+
horizontalPadding = 32,
|
|
57
|
+
verticalPadding = 32,
|
|
58
|
+
gap = 8,
|
|
59
|
+
headerHeight = 120,
|
|
60
|
+
tabBarHeight = 80,
|
|
61
|
+
statusBarHeight = 50,
|
|
62
|
+
} = config;
|
|
63
|
+
|
|
64
|
+
const { width, height } = getScreenDimensions();
|
|
65
|
+
|
|
66
|
+
const totalHorizontalGap = gap * (columns - 1);
|
|
67
|
+
const availableWidth = width - horizontalPadding - totalHorizontalGap;
|
|
68
|
+
const maxCellWidth = availableWidth / columns;
|
|
69
|
+
|
|
70
|
+
const totalVerticalGap = gap * (rows - 1);
|
|
71
|
+
const usedHeight = headerHeight + tabBarHeight + statusBarHeight + verticalPadding;
|
|
72
|
+
const availableHeight = height - usedHeight - totalVerticalGap;
|
|
73
|
+
const maxCellHeight = availableHeight / rows;
|
|
74
|
+
|
|
75
|
+
return Math.floor(Math.min(maxCellWidth, maxCellHeight));
|
|
76
|
+
} catch {
|
|
77
|
+
return 60;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @umituz/react-native-design-system-responsive - Public API
|
|
3
|
+
*
|
|
4
|
+
* Responsive design utilities for React Native - Screen dimensions, device detection,
|
|
5
|
+
* and responsive sizing utilities following Material Design 3 and iOS HIG principles.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { useResponsive, isTablet, getResponsiveLogoSize } from '@umituz/react-native-design-system-responsive';
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// Hook exports
|
|
14
|
+
export { useResponsive } from './useResponsive';
|
|
15
|
+
export type { UseResponsiveReturn } from './useResponsive';
|
|
16
|
+
|
|
17
|
+
// Utility function exports
|
|
18
|
+
export {
|
|
19
|
+
getScreenDimensions,
|
|
20
|
+
isSmallPhone,
|
|
21
|
+
isTablet,
|
|
22
|
+
getResponsiveLogoSize,
|
|
23
|
+
getResponsiveInputHeight,
|
|
24
|
+
getResponsiveHorizontalPadding,
|
|
25
|
+
getResponsiveBottomPosition,
|
|
26
|
+
getResponsiveFABPosition,
|
|
27
|
+
getResponsiveModalMaxHeight,
|
|
28
|
+
getResponsiveMinModalHeight,
|
|
29
|
+
getResponsiveIconContainerSize,
|
|
30
|
+
getResponsiveGridColumns,
|
|
31
|
+
getResponsiveGridCellSize,
|
|
32
|
+
type GridCellSizeConfig,
|
|
33
|
+
getResponsiveMaxWidth,
|
|
34
|
+
getResponsiveFontSize,
|
|
35
|
+
isLandscape,
|
|
36
|
+
getDeviceType,
|
|
37
|
+
DeviceType,
|
|
38
|
+
} from './responsive';
|
|
39
|
+
|
|
40
|
+
// Device detection exports
|
|
41
|
+
export { getSpacingMultiplier } from './deviceDetection';
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
// Platform constants exports
|
|
46
|
+
export {
|
|
47
|
+
IOS_HIG,
|
|
48
|
+
PLATFORM_CONSTANTS,
|
|
49
|
+
isValidTouchTarget,
|
|
50
|
+
getMinTouchTarget,
|
|
51
|
+
} from './platformConstants';
|
|
52
|
+
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform-Specific Constants
|
|
3
|
+
*
|
|
4
|
+
* Design system constants that ensure compliance with platform guidelines.
|
|
5
|
+
* These values are based on official Human Interface Guidelines (HIG) from Apple and Material Design from Google.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* iOS Human Interface Guidelines (HIG) Constants
|
|
10
|
+
*
|
|
11
|
+
* @see https://developer.apple.com/design/human-interface-guidelines/layout
|
|
12
|
+
*/
|
|
13
|
+
export const IOS_HIG = {
|
|
14
|
+
/**
|
|
15
|
+
* Minimum Touch Target Size
|
|
16
|
+
*
|
|
17
|
+
* Apple requires a minimum tappable area of 44pt x 44pt for ALL interactive controls.
|
|
18
|
+
* This is enforced during App Store review.
|
|
19
|
+
*
|
|
20
|
+
* @critical Violating this can result in App Store rejection
|
|
21
|
+
*/
|
|
22
|
+
MIN_TOUCH_TARGET: 44,
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Recommended Minimum Touch Target Size
|
|
26
|
+
*
|
|
27
|
+
* For better accessibility and usability, Apple recommends 48pt x 48pt.
|
|
28
|
+
*/
|
|
29
|
+
RECOMMENDED_TOUCH_TARGET: 48,
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Minimum Text Size
|
|
33
|
+
*
|
|
34
|
+
* Minimum font size for body text to ensure readability.
|
|
35
|
+
*/
|
|
36
|
+
MIN_TEXT_SIZE: 17,
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
} as const;
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Universal Platform Constants
|
|
45
|
+
*
|
|
46
|
+
* These values work across both iOS and Android, taking the more restrictive requirement.
|
|
47
|
+
*/
|
|
48
|
+
export const PLATFORM_CONSTANTS = {
|
|
49
|
+
/**
|
|
50
|
+
* Minimum Touch Target Size
|
|
51
|
+
*
|
|
52
|
+
* Uses iOS requirement (44pt) as it's more restrictive than Android (48dp).
|
|
53
|
+
* This ensures compliance on both platforms.
|
|
54
|
+
*/
|
|
55
|
+
MIN_TOUCH_TARGET: Math.max(IOS_HIG.MIN_TOUCH_TARGET, 48),
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Recommended Touch Target Size
|
|
59
|
+
*
|
|
60
|
+
* Uses the higher value between iOS and Android recommendations.
|
|
61
|
+
*/
|
|
62
|
+
RECOMMENDED_TOUCH_TARGET: 48,
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Minimum Text Size
|
|
66
|
+
*
|
|
67
|
+
* Uses iOS requirement as it's larger.
|
|
68
|
+
*/
|
|
69
|
+
MIN_TEXT_SIZE: Math.max(IOS_HIG.MIN_TEXT_SIZE, 14),
|
|
70
|
+
} as const;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Helper function to validate touch target size
|
|
74
|
+
*
|
|
75
|
+
* @param size - The size to validate (in pt/dp)
|
|
76
|
+
* @returns true if size meets platform requirements
|
|
77
|
+
*/
|
|
78
|
+
export const isValidTouchTarget = (size: number): boolean => {
|
|
79
|
+
return size >= IOS_HIG.MIN_TOUCH_TARGET;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Helper function to get minimum touch target for component
|
|
84
|
+
*
|
|
85
|
+
* @param componentType - The type of component ('button' | 'input' | 'icon' | 'generic')
|
|
86
|
+
* @returns The minimum touch target size for that component type
|
|
87
|
+
*/
|
|
88
|
+
export const getMinTouchTarget = (componentType: 'button' | 'input' | 'icon' | 'generic' = 'generic'): number => {
|
|
89
|
+
switch (componentType) {
|
|
90
|
+
case 'button':
|
|
91
|
+
case 'input':
|
|
92
|
+
return PLATFORM_CONSTANTS.RECOMMENDED_TOUCH_TARGET; // 48pt recommended for buttons and inputs
|
|
93
|
+
case 'icon':
|
|
94
|
+
case 'generic':
|
|
95
|
+
default:
|
|
96
|
+
return IOS_HIG.MIN_TOUCH_TARGET; // 44pt minimum for icons and generic elements
|
|
97
|
+
}
|
|
98
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Responsive Design Utilities
|
|
3
|
+
*
|
|
4
|
+
* Centralized responsive sizing and spacing utilities to prevent
|
|
5
|
+
* Apple App Store rejection due to layout issues on different devices.
|
|
6
|
+
*
|
|
7
|
+
* This is the main export file that imports from specialized modules.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// Device detection
|
|
11
|
+
export {
|
|
12
|
+
getScreenDimensions,
|
|
13
|
+
isSmallPhone,
|
|
14
|
+
isTablet,
|
|
15
|
+
isLandscape,
|
|
16
|
+
getDeviceType,
|
|
17
|
+
DeviceType,
|
|
18
|
+
} from './deviceDetection';
|
|
19
|
+
|
|
20
|
+
// Responsive sizing
|
|
21
|
+
export {
|
|
22
|
+
getResponsiveLogoSize,
|
|
23
|
+
getResponsiveInputHeight,
|
|
24
|
+
getResponsiveIconContainerSize,
|
|
25
|
+
getResponsiveMaxWidth,
|
|
26
|
+
getResponsiveFontSize,
|
|
27
|
+
getResponsiveGridColumns,
|
|
28
|
+
getResponsiveGridCellSize,
|
|
29
|
+
type GridCellSizeConfig,
|
|
30
|
+
} from './responsiveSizing';
|
|
31
|
+
|
|
32
|
+
// Responsive layout
|
|
33
|
+
export {
|
|
34
|
+
getResponsiveHorizontalPadding,
|
|
35
|
+
getResponsiveBottomPosition,
|
|
36
|
+
getResponsiveFABPosition,
|
|
37
|
+
getResponsiveModalMaxHeight,
|
|
38
|
+
getResponsiveMinModalHeight,
|
|
39
|
+
} from './responsiveLayout';
|
|
40
|
+
|
|
41
|
+
// Platform constants
|
|
42
|
+
export {
|
|
43
|
+
IOS_HIG,
|
|
44
|
+
PLATFORM_CONSTANTS,
|
|
45
|
+
isValidTouchTarget,
|
|
46
|
+
getMinTouchTarget,
|
|
47
|
+
} from './platformConstants';
|
|
48
|
+
|
|
49
|
+
// Configuration
|
|
50
|
+
export {
|
|
51
|
+
DEVICE_BREAKPOINTS,
|
|
52
|
+
RESPONSIVE_PERCENTAGES,
|
|
53
|
+
SIZE_CONSTRAINTS,
|
|
54
|
+
LAYOUT_CONSTANTS,
|
|
55
|
+
HEIGHT_THRESHOLDS,
|
|
56
|
+
GRID_CONFIG,
|
|
57
|
+
VALIDATION_CONSTRAINTS,
|
|
58
|
+
} from './config';
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Responsive Layout Utilities
|
|
3
|
+
* Layout utilities for positioning and spacing.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { getScreenDimensions } from './deviceDetection';
|
|
7
|
+
import {
|
|
8
|
+
DEVICE_BREAKPOINTS,
|
|
9
|
+
LAYOUT_CONSTANTS,
|
|
10
|
+
HEIGHT_THRESHOLDS,
|
|
11
|
+
SIZE_CONSTRAINTS,
|
|
12
|
+
} from './config';
|
|
13
|
+
import { validateNumber, validateSafeAreaInsets } from './validation';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Responsive horizontal padding
|
|
17
|
+
* @param basePadding - Base padding value (default: 16)
|
|
18
|
+
* @param insets - Safe area insets object
|
|
19
|
+
*/
|
|
20
|
+
export const getResponsiveHorizontalPadding = (
|
|
21
|
+
basePadding: number = LAYOUT_CONSTANTS.HORIZONTAL_PADDING_BASE,
|
|
22
|
+
insets: { left?: number; right?: number } = { left: 0, right: 0 }
|
|
23
|
+
): number => {
|
|
24
|
+
try {
|
|
25
|
+
const validatedBasePadding = validateNumber(basePadding, 'basePadding', 0, 100);
|
|
26
|
+
validateSafeAreaInsets(insets);
|
|
27
|
+
|
|
28
|
+
const { width } = getScreenDimensions();
|
|
29
|
+
const { left = 0, right = 0 } = insets;
|
|
30
|
+
|
|
31
|
+
if (width >= DEVICE_BREAKPOINTS.TABLET) {
|
|
32
|
+
const tabletPadding = validatedBasePadding * 1.5;
|
|
33
|
+
return Math.max(
|
|
34
|
+
tabletPadding,
|
|
35
|
+
left + LAYOUT_CONSTANTS.HORIZONTAL_PADDING_BASE,
|
|
36
|
+
right + LAYOUT_CONSTANTS.HORIZONTAL_PADDING_BASE
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return Math.max(
|
|
41
|
+
validatedBasePadding,
|
|
42
|
+
left + LAYOUT_CONSTANTS.SAFE_AREA_OFFSET,
|
|
43
|
+
right + LAYOUT_CONSTANTS.SAFE_AREA_OFFSET
|
|
44
|
+
);
|
|
45
|
+
} catch {
|
|
46
|
+
return 16;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Responsive bottom positioning
|
|
52
|
+
* @param basePosition - Base bottom position (default: 32)
|
|
53
|
+
* @param insets - Safe area insets object
|
|
54
|
+
*/
|
|
55
|
+
export const getResponsiveBottomPosition = (
|
|
56
|
+
basePosition: number = LAYOUT_CONSTANTS.BOTTOM_POSITION_BASE,
|
|
57
|
+
insets: { bottom?: number } = { bottom: 0 }
|
|
58
|
+
): number => {
|
|
59
|
+
try {
|
|
60
|
+
const validatedBasePosition = validateNumber(basePosition, 'basePosition', 0, 500);
|
|
61
|
+
validateSafeAreaInsets(insets);
|
|
62
|
+
|
|
63
|
+
const { bottom = 0 } = insets;
|
|
64
|
+
return Math.max(validatedBasePosition, bottom + LAYOUT_CONSTANTS.SAFE_AREA_OFFSET);
|
|
65
|
+
} catch {
|
|
66
|
+
return 32;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Responsive FAB position
|
|
72
|
+
* @param insets - Safe area insets object
|
|
73
|
+
*/
|
|
74
|
+
export const getResponsiveFABPosition = (
|
|
75
|
+
insets: { bottom?: number; right?: number } = { bottom: 0, right: 0 }
|
|
76
|
+
): { bottom: number; right: number } => {
|
|
77
|
+
try {
|
|
78
|
+
validateSafeAreaInsets(insets);
|
|
79
|
+
const { width } = getScreenDimensions();
|
|
80
|
+
const { bottom = 0, right = 0 } = insets;
|
|
81
|
+
|
|
82
|
+
if (width >= DEVICE_BREAKPOINTS.TABLET) {
|
|
83
|
+
return {
|
|
84
|
+
bottom: Math.max(LAYOUT_CONSTANTS.FAB_BOTTOM_TABLET, bottom + LAYOUT_CONSTANTS.TAB_BAR_OFFSET),
|
|
85
|
+
right: Math.max(LAYOUT_CONSTANTS.FAB_RIGHT_TABLET, right + LAYOUT_CONSTANTS.HORIZONTAL_PADDING_BASE),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
bottom: Math.max(LAYOUT_CONSTANTS.TAB_BAR_OFFSET, bottom + LAYOUT_CONSTANTS.SAFE_AREA_OFFSET),
|
|
91
|
+
right: Math.max(LAYOUT_CONSTANTS.FAB_RIGHT_PHONE, right + LAYOUT_CONSTANTS.SAFE_AREA_OFFSET),
|
|
92
|
+
};
|
|
93
|
+
} catch {
|
|
94
|
+
return { bottom: 90, right: 20 };
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Responsive modal max height
|
|
100
|
+
*/
|
|
101
|
+
export const getResponsiveModalMaxHeight = (): string => {
|
|
102
|
+
try {
|
|
103
|
+
const { height } = getScreenDimensions();
|
|
104
|
+
|
|
105
|
+
if (height <= HEIGHT_THRESHOLDS.SMALL_DEVICE) {
|
|
106
|
+
return LAYOUT_CONSTANTS.MODAL_HEIGHT_SMALL;
|
|
107
|
+
} else if (height >= HEIGHT_THRESHOLDS.LARGE_DEVICE) {
|
|
108
|
+
return LAYOUT_CONSTANTS.MODAL_HEIGHT_TABLET;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return LAYOUT_CONSTANTS.MODAL_HEIGHT_STANDARD;
|
|
112
|
+
} catch {
|
|
113
|
+
return '70%';
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Responsive modal min height
|
|
119
|
+
*/
|
|
120
|
+
export const getResponsiveMinModalHeight = (): number => {
|
|
121
|
+
try {
|
|
122
|
+
const { height } = getScreenDimensions();
|
|
123
|
+
|
|
124
|
+
if (height <= HEIGHT_THRESHOLDS.SMALL_DEVICE) {
|
|
125
|
+
const calculatedHeight = height * 0.4;
|
|
126
|
+
return Math.max(calculatedHeight, SIZE_CONSTRAINTS.MODAL_MIN_SMALL);
|
|
127
|
+
} else if (height >= HEIGHT_THRESHOLDS.LARGE_DEVICE) {
|
|
128
|
+
const calculatedHeight = height * 0.35;
|
|
129
|
+
return Math.min(Math.max(calculatedHeight, SIZE_CONSTRAINTS.MODAL_MIN_TABLET), SIZE_CONSTRAINTS.MODAL_MAX_TABLET);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const calculatedHeight = height * 0.45;
|
|
133
|
+
return Math.max(calculatedHeight, SIZE_CONSTRAINTS.MODAL_MIN_STANDARD);
|
|
134
|
+
} catch {
|
|
135
|
+
return 300;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Responsive Sizing Utilities
|
|
3
|
+
* Responsive sizing utilities for UI components.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { getScreenDimensions } from './deviceDetection';
|
|
7
|
+
import {
|
|
8
|
+
DEVICE_BREAKPOINTS,
|
|
9
|
+
RESPONSIVE_PERCENTAGES,
|
|
10
|
+
SIZE_CONSTRAINTS,
|
|
11
|
+
HEIGHT_THRESHOLDS,
|
|
12
|
+
} from './config';
|
|
13
|
+
import { validateNumber, validateFontSize, safePercentage, clamp } from './validation';
|
|
14
|
+
|
|
15
|
+
// Re-export grid utilities
|
|
16
|
+
export {
|
|
17
|
+
getResponsiveGridColumns,
|
|
18
|
+
getResponsiveGridCellSize,
|
|
19
|
+
type GridCellSizeConfig,
|
|
20
|
+
} from './gridUtils';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Responsive logo/icon size
|
|
24
|
+
* @param baseSize - Base logo size (default: 140)
|
|
25
|
+
*/
|
|
26
|
+
export const getResponsiveLogoSize = (baseSize: number = 140): number => {
|
|
27
|
+
try {
|
|
28
|
+
const validatedBaseSize = validateNumber(baseSize, 'baseSize', 50, 500);
|
|
29
|
+
const { width } = getScreenDimensions();
|
|
30
|
+
|
|
31
|
+
if (width <= DEVICE_BREAKPOINTS.SMALL_PHONE) {
|
|
32
|
+
const calculatedSize = safePercentage(width, RESPONSIVE_PERCENTAGES.LOGO_SMALL_PHONE_MAX);
|
|
33
|
+
return clamp(calculatedSize, SIZE_CONSTRAINTS.LOGO_MIN_SMALL, SIZE_CONSTRAINTS.LOGO_MAX_SMALL);
|
|
34
|
+
} else if (width >= DEVICE_BREAKPOINTS.TABLET) {
|
|
35
|
+
const calculatedSize = safePercentage(width, RESPONSIVE_PERCENTAGES.LOGO_TABLET_MAX);
|
|
36
|
+
return clamp(calculatedSize, SIZE_CONSTRAINTS.LOGO_MIN_TABLET, SIZE_CONSTRAINTS.LOGO_MAX_TABLET);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return validatedBaseSize;
|
|
40
|
+
} catch {
|
|
41
|
+
return 140;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Responsive multiline input height
|
|
47
|
+
* @param baseHeight - Base input height (default: 200)
|
|
48
|
+
*/
|
|
49
|
+
export const getResponsiveInputHeight = (baseHeight: number = 200): number => {
|
|
50
|
+
try {
|
|
51
|
+
const validatedBaseHeight = validateNumber(baseHeight, 'baseHeight', 50, 500);
|
|
52
|
+
const { height } = getScreenDimensions();
|
|
53
|
+
|
|
54
|
+
if (height <= HEIGHT_THRESHOLDS.SMALL_DEVICE) {
|
|
55
|
+
const calculatedHeight = safePercentage(height, RESPONSIVE_PERCENTAGES.INPUT_SMALL_DEVICE);
|
|
56
|
+
return Math.min(calculatedHeight, SIZE_CONSTRAINTS.INPUT_MAX_SMALL);
|
|
57
|
+
} else if (height <= HEIGHT_THRESHOLDS.MEDIUM_DEVICE) {
|
|
58
|
+
const calculatedHeight = safePercentage(height, RESPONSIVE_PERCENTAGES.INPUT_MEDIUM_DEVICE);
|
|
59
|
+
return Math.min(calculatedHeight, SIZE_CONSTRAINTS.INPUT_MAX_MEDIUM);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return Math.min(validatedBaseHeight, SIZE_CONSTRAINTS.INPUT_MAX_LARGE);
|
|
63
|
+
} catch {
|
|
64
|
+
return 200;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Responsive icon container size
|
|
70
|
+
* @param baseSize - Base container size (default: 140)
|
|
71
|
+
*/
|
|
72
|
+
export const getResponsiveIconContainerSize = (baseSize: number = 140): number => {
|
|
73
|
+
try {
|
|
74
|
+
const validatedBaseSize = validateNumber(baseSize, 'baseSize', 50, 300);
|
|
75
|
+
const { width } = getScreenDimensions();
|
|
76
|
+
|
|
77
|
+
if (width <= DEVICE_BREAKPOINTS.SMALL_PHONE) {
|
|
78
|
+
const calculatedSize = safePercentage(width, RESPONSIVE_PERCENTAGES.ICON_CONTAINER_SMALL_PHONE);
|
|
79
|
+
return Math.min(calculatedSize, SIZE_CONSTRAINTS.ICON_MAX_SMALL);
|
|
80
|
+
} else if (width >= DEVICE_BREAKPOINTS.TABLET) {
|
|
81
|
+
const calculatedSize = safePercentage(width, RESPONSIVE_PERCENTAGES.ICON_CONTAINER_TABLET);
|
|
82
|
+
return Math.min(calculatedSize, SIZE_CONSTRAINTS.ICON_MAX_TABLET);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return validatedBaseSize;
|
|
86
|
+
} catch {
|
|
87
|
+
return 140;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Responsive max width for content
|
|
93
|
+
* @param baseWidth - Base content width (default: 400)
|
|
94
|
+
*/
|
|
95
|
+
export const getResponsiveMaxWidth = (baseWidth: number = 400): number => {
|
|
96
|
+
try {
|
|
97
|
+
const validatedBaseWidth = validateNumber(baseWidth, 'baseWidth', 100, 1000);
|
|
98
|
+
const { width } = getScreenDimensions();
|
|
99
|
+
|
|
100
|
+
if (width <= DEVICE_BREAKPOINTS.SMALL_PHONE) {
|
|
101
|
+
return safePercentage(width, RESPONSIVE_PERCENTAGES.CONTENT_SMALL_PHONE);
|
|
102
|
+
} else if (width >= DEVICE_BREAKPOINTS.TABLET) {
|
|
103
|
+
const calculatedWidth = safePercentage(width, RESPONSIVE_PERCENTAGES.CONTENT_TABLET);
|
|
104
|
+
return Math.min(calculatedWidth, SIZE_CONSTRAINTS.CONTENT_MAX_TABLET);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const maxWidth = safePercentage(width, RESPONSIVE_PERCENTAGES.CONTENT_PHONE);
|
|
108
|
+
return Math.min(maxWidth, validatedBaseWidth);
|
|
109
|
+
} catch {
|
|
110
|
+
return 400;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Responsive font size
|
|
116
|
+
* @param baseFontSize - Base font size
|
|
117
|
+
*/
|
|
118
|
+
export const getResponsiveFontSize = (baseFontSize: number): number => {
|
|
119
|
+
try {
|
|
120
|
+
const validatedBaseSize = validateFontSize(baseFontSize);
|
|
121
|
+
const { width } = getScreenDimensions();
|
|
122
|
+
|
|
123
|
+
if (width <= DEVICE_BREAKPOINTS.SMALL_PHONE) {
|
|
124
|
+
const scaledSize = validatedBaseSize * RESPONSIVE_PERCENTAGES.FONT_SMALL_PHONE;
|
|
125
|
+
return Math.max(scaledSize, SIZE_CONSTRAINTS.FONT_MIN_SIZE);
|
|
126
|
+
} else if (width >= DEVICE_BREAKPOINTS.TABLET) {
|
|
127
|
+
return validatedBaseSize * RESPONSIVE_PERCENTAGES.FONT_TABLET;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return validatedBaseSize;
|
|
131
|
+
} catch {
|
|
132
|
+
return 16;
|
|
133
|
+
}
|
|
134
|
+
};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useResponsive Hook
|
|
3
|
+
*
|
|
4
|
+
* React Hook for accessing responsive utilities with real-time dimension updates
|
|
5
|
+
* and safe area insets integration.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* ```tsx
|
|
9
|
+
* const { logoSize, inputHeight, fabPosition, isSmallDevice } = useResponsive();
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { useCallback, useMemo } from 'react';
|
|
14
|
+
import { useWindowDimensions } from 'react-native';
|
|
15
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
16
|
+
import {
|
|
17
|
+
getResponsiveLogoSize,
|
|
18
|
+
getResponsiveInputHeight,
|
|
19
|
+
getResponsiveHorizontalPadding,
|
|
20
|
+
getResponsiveBottomPosition,
|
|
21
|
+
getResponsiveFABPosition,
|
|
22
|
+
getResponsiveModalMaxHeight,
|
|
23
|
+
getResponsiveMinModalHeight,
|
|
24
|
+
getResponsiveIconContainerSize,
|
|
25
|
+
getResponsiveGridColumns,
|
|
26
|
+
getResponsiveMaxWidth,
|
|
27
|
+
getResponsiveFontSize,
|
|
28
|
+
isSmallPhone,
|
|
29
|
+
isTablet,
|
|
30
|
+
isLandscape,
|
|
31
|
+
getDeviceType,
|
|
32
|
+
getMinTouchTarget,
|
|
33
|
+
DeviceType,
|
|
34
|
+
} from './responsive';
|
|
35
|
+
import { getSpacingMultiplier } from './deviceDetection';
|
|
36
|
+
|
|
37
|
+
export interface UseResponsiveReturn {
|
|
38
|
+
// Device info
|
|
39
|
+
width: number;
|
|
40
|
+
height: number;
|
|
41
|
+
isSmallDevice: boolean;
|
|
42
|
+
isTabletDevice: boolean;
|
|
43
|
+
isLandscapeDevice: boolean;
|
|
44
|
+
deviceType: DeviceType;
|
|
45
|
+
|
|
46
|
+
// Safe area insets
|
|
47
|
+
insets: {
|
|
48
|
+
top: number;
|
|
49
|
+
bottom: number;
|
|
50
|
+
left: number;
|
|
51
|
+
right: number;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Responsive sizes
|
|
55
|
+
logoSize: number;
|
|
56
|
+
inputHeight: number;
|
|
57
|
+
iconContainerSize: number;
|
|
58
|
+
maxContentWidth: number;
|
|
59
|
+
minTouchTarget: number;
|
|
60
|
+
|
|
61
|
+
// Responsive positioning
|
|
62
|
+
horizontalPadding: number;
|
|
63
|
+
bottomPosition: number;
|
|
64
|
+
fabPosition: { bottom: number; right: number };
|
|
65
|
+
|
|
66
|
+
// Responsive layout
|
|
67
|
+
modalMaxHeight: string;
|
|
68
|
+
modalMinHeight: number;
|
|
69
|
+
gridColumns: number;
|
|
70
|
+
spacingMultiplier: number;
|
|
71
|
+
|
|
72
|
+
// Utility functions
|
|
73
|
+
getLogoSize: (baseSize?: number) => number;
|
|
74
|
+
getInputHeight: (baseHeight?: number) => number;
|
|
75
|
+
getIconSize: (baseSize?: number) => number;
|
|
76
|
+
getMaxWidth: (baseWidth?: number) => number;
|
|
77
|
+
getFontSize: (baseFontSize: number) => number;
|
|
78
|
+
getGridCols: (mobile?: number, tablet?: number) => number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Hook for responsive design utilities
|
|
83
|
+
* Automatically updates when screen dimensions or orientation changes
|
|
84
|
+
*/
|
|
85
|
+
export const useResponsive = (): UseResponsiveReturn => {
|
|
86
|
+
const { width, height } = useWindowDimensions();
|
|
87
|
+
const insets = useSafeAreaInsets();
|
|
88
|
+
|
|
89
|
+
// Memoize utility functions to prevent unnecessary re-renders
|
|
90
|
+
const getLogoSize = useCallback((baseSize?: number) => getResponsiveLogoSize(baseSize), []);
|
|
91
|
+
const getInputHeight = useCallback((baseHeight?: number) => getResponsiveInputHeight(baseHeight), []);
|
|
92
|
+
const getIconSize = useCallback((baseSize?: number) => getResponsiveIconContainerSize(baseSize), []);
|
|
93
|
+
const getMaxWidth = useCallback((baseWidth?: number) => getResponsiveMaxWidth(baseWidth), []);
|
|
94
|
+
const getFontSize = useCallback((baseFontSize: number) => getResponsiveFontSize(baseFontSize), []);
|
|
95
|
+
const getGridCols = useCallback((mobile?: number, tablet?: number) => getResponsiveGridColumns(mobile, tablet), []);
|
|
96
|
+
|
|
97
|
+
// Memoize responsive values to prevent unnecessary recalculations
|
|
98
|
+
const responsiveValues = useMemo(() => ({
|
|
99
|
+
// Device info
|
|
100
|
+
width,
|
|
101
|
+
height,
|
|
102
|
+
isSmallDevice: isSmallPhone(),
|
|
103
|
+
isTabletDevice: isTablet(),
|
|
104
|
+
isLandscapeDevice: isLandscape(),
|
|
105
|
+
deviceType: getDeviceType(),
|
|
106
|
+
|
|
107
|
+
// Safe area insets
|
|
108
|
+
insets,
|
|
109
|
+
|
|
110
|
+
// Responsive sizes (with default values)
|
|
111
|
+
logoSize: getResponsiveLogoSize(),
|
|
112
|
+
inputHeight: getResponsiveInputHeight(),
|
|
113
|
+
iconContainerSize: getResponsiveIconContainerSize(),
|
|
114
|
+
maxContentWidth: getResponsiveMaxWidth(),
|
|
115
|
+
minTouchTarget: getMinTouchTarget(),
|
|
116
|
+
|
|
117
|
+
// Responsive positioning
|
|
118
|
+
horizontalPadding: getResponsiveHorizontalPadding(undefined, insets),
|
|
119
|
+
bottomPosition: getResponsiveBottomPosition(undefined, insets),
|
|
120
|
+
fabPosition: getResponsiveFABPosition(insets),
|
|
121
|
+
|
|
122
|
+
// Responsive layout
|
|
123
|
+
modalMaxHeight: getResponsiveModalMaxHeight(),
|
|
124
|
+
modalMinHeight: getResponsiveMinModalHeight(),
|
|
125
|
+
gridColumns: getResponsiveGridColumns(),
|
|
126
|
+
spacingMultiplier: getSpacingMultiplier(),
|
|
127
|
+
|
|
128
|
+
// Utility functions (memoized)
|
|
129
|
+
getLogoSize,
|
|
130
|
+
getInputHeight,
|
|
131
|
+
getIconSize,
|
|
132
|
+
getMaxWidth,
|
|
133
|
+
getFontSize,
|
|
134
|
+
getGridCols,
|
|
135
|
+
}), [width, height, insets]);
|
|
136
|
+
|
|
137
|
+
return responsiveValues;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
|