@umituz/react-native-design-system 1.14.0 → 2.0.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 +26 -19
- package/src/atoms/AtomicAvatar.tsx +161 -0
- package/src/atoms/AtomicButton.tsx +241 -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 +148 -79
- 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,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
|
+
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input Validation Utilities
|
|
3
|
+
*
|
|
4
|
+
* Centralized validation for all responsive utility functions.
|
|
5
|
+
* Ensures type safety and prevents runtime errors.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { VALIDATION_CONSTRAINTS } from './config';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Custom error class for responsive utilities
|
|
12
|
+
*/
|
|
13
|
+
export class ResponsiveValidationError extends Error {
|
|
14
|
+
constructor(message: string) {
|
|
15
|
+
super(`[Responsive] ${message}`);
|
|
16
|
+
this.name = 'ResponsiveValidationError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Validates a numeric input parameter
|
|
22
|
+
* @param value - The value to validate
|
|
23
|
+
* @param paramName - Parameter name for error messages
|
|
24
|
+
* @param min - Minimum allowed value
|
|
25
|
+
* @param max - Maximum allowed value
|
|
26
|
+
* @throws ResponsiveValidationError if validation fails
|
|
27
|
+
*/
|
|
28
|
+
export const validateNumber = (
|
|
29
|
+
value: number | undefined,
|
|
30
|
+
paramName: string,
|
|
31
|
+
min: number = VALIDATION_CONSTRAINTS.MIN_BASE_SIZE,
|
|
32
|
+
max: number = VALIDATION_CONSTRAINTS.MAX_BASE_SIZE
|
|
33
|
+
): number => {
|
|
34
|
+
if (value === undefined || value === null) {
|
|
35
|
+
throw new ResponsiveValidationError(`${paramName} is required`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (typeof value !== 'number' || isNaN(value)) {
|
|
39
|
+
throw new ResponsiveValidationError(`${paramName} must be a valid number`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!isFinite(value)) {
|
|
43
|
+
throw new ResponsiveValidationError(`${paramName} must be a finite number`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (value < min) {
|
|
47
|
+
throw new ResponsiveValidationError(`${paramName} must be at least ${min}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (value > max) {
|
|
51
|
+
throw new ResponsiveValidationError(`${paramName} must be at most ${max}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return value;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Validates font size input
|
|
59
|
+
* @param fontSize - Font size to validate
|
|
60
|
+
* @param paramName - Parameter name for error messages
|
|
61
|
+
* @returns Validated font size
|
|
62
|
+
*/
|
|
63
|
+
export const validateFontSize = (fontSize: number, paramName: string = 'fontSize'): number => {
|
|
64
|
+
return validateNumber(
|
|
65
|
+
fontSize,
|
|
66
|
+
paramName,
|
|
67
|
+
VALIDATION_CONSTRAINTS.MIN_BASE_FONT_SIZE,
|
|
68
|
+
VALIDATION_CONSTRAINTS.MAX_BASE_FONT_SIZE
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Validates screen dimensions
|
|
74
|
+
* @param width - Screen width
|
|
75
|
+
* @param height - Screen height
|
|
76
|
+
* @throws ResponsiveValidationError if validation fails
|
|
77
|
+
*/
|
|
78
|
+
export const validateScreenDimensions = (width: number, height: number): void => {
|
|
79
|
+
validateNumber(
|
|
80
|
+
width,
|
|
81
|
+
'width',
|
|
82
|
+
VALIDATION_CONSTRAINTS.MIN_SCREEN_DIMENSION,
|
|
83
|
+
VALIDATION_CONSTRAINTS.MAX_SCREEN_DIMENSION
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
validateNumber(
|
|
87
|
+
height,
|
|
88
|
+
'height',
|
|
89
|
+
VALIDATION_CONSTRAINTS.MIN_SCREEN_DIMENSION,
|
|
90
|
+
VALIDATION_CONSTRAINTS.MAX_SCREEN_DIMENSION
|
|
91
|
+
);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Validates safe area insets
|
|
96
|
+
* @param insets - Safe area insets object
|
|
97
|
+
* @throws ResponsiveValidationError if validation fails
|
|
98
|
+
*/
|
|
99
|
+
export const validateSafeAreaInsets = (insets: {
|
|
100
|
+
top?: number;
|
|
101
|
+
bottom?: number;
|
|
102
|
+
left?: number;
|
|
103
|
+
right?: number;
|
|
104
|
+
}): void => {
|
|
105
|
+
if (!insets || typeof insets !== 'object') {
|
|
106
|
+
throw new ResponsiveValidationError('Safe area insets must be an object');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const { top = 0, bottom = 0, left = 0, right = 0 } = insets;
|
|
110
|
+
|
|
111
|
+
validateNumber(top, 'insets.top', 0, 1000);
|
|
112
|
+
validateNumber(bottom, 'insets.bottom', 0, 1000);
|
|
113
|
+
validateNumber(left, 'insets.left', 0, 1000);
|
|
114
|
+
validateNumber(right, 'insets.right', 0, 1000);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Validates grid column parameters
|
|
119
|
+
* @param mobileColumns - Number of columns for mobile
|
|
120
|
+
* @param tabletColumns - Number of columns for tablet
|
|
121
|
+
* @throws ResponsiveValidationError if validation fails
|
|
122
|
+
*/
|
|
123
|
+
export const validateGridColumns = (
|
|
124
|
+
mobileColumns?: number,
|
|
125
|
+
tabletColumns?: number
|
|
126
|
+
): void => {
|
|
127
|
+
if (mobileColumns !== undefined) {
|
|
128
|
+
validateNumber(mobileColumns, 'mobileColumns', 1, 20);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (tabletColumns !== undefined) {
|
|
132
|
+
validateNumber(tabletColumns, 'tabletColumns', 1, 20);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Clamps a value between min and max bounds
|
|
138
|
+
* @param value - Value to clamp
|
|
139
|
+
* @param min - Minimum value
|
|
140
|
+
* @param max - Maximum value
|
|
141
|
+
* @returns Clamped value
|
|
142
|
+
*/
|
|
143
|
+
export const clamp = (value: number, min: number, max: number): number => {
|
|
144
|
+
return Math.min(Math.max(value, min), max);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Safely calculates a percentage of a value
|
|
149
|
+
* @param value - Base value
|
|
150
|
+
* @param percentage - Percentage (0-1)
|
|
151
|
+
* @returns Calculated percentage
|
|
152
|
+
*/
|
|
153
|
+
export const safePercentage = (value: number, percentage: number): number => {
|
|
154
|
+
const validatedValue = validateNumber(value, 'value', 0, Infinity);
|
|
155
|
+
const validatedPercentage = clamp(percentage, 0, 1);
|
|
156
|
+
|
|
157
|
+
return validatedValue * validatedPercentage;
|
|
158
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BASE TOKENS - Main Export
|
|
3
|
+
*
|
|
4
|
+
* Aggregates all token modules into BASE_TOKENS
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { spacing } from './tokens/Spacing';
|
|
8
|
+
import { typography } from './tokens/Typography';
|
|
9
|
+
import { borders } from './tokens/Borders';
|
|
10
|
+
import { iconSizes, opacity, avatarSizes, sizes } from './tokens/Sizes';
|
|
11
|
+
import type { BaseTokens } from './tokens/BaseTokens';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* BASE_TOKENS - Static design tokens
|
|
15
|
+
* These values don't change with theme (light/dark)
|
|
16
|
+
*/
|
|
17
|
+
export const BASE_TOKENS: BaseTokens = {
|
|
18
|
+
spacing,
|
|
19
|
+
typography,
|
|
20
|
+
borders,
|
|
21
|
+
iconSizes,
|
|
22
|
+
opacity,
|
|
23
|
+
avatarSizes,
|
|
24
|
+
sizes,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Convenience exports
|
|
28
|
+
export { spacing, typography, borders, iconSizes, opacity, avatarSizes, sizes };
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
// Type exports
|
|
33
|
+
export type {
|
|
34
|
+
BaseTokens,
|
|
35
|
+
Spacing,
|
|
36
|
+
Typography,
|
|
37
|
+
Borders,
|
|
38
|
+
IconSizes,
|
|
39
|
+
Opacity,
|
|
40
|
+
AvatarSizes,
|
|
41
|
+
ComponentSizes
|
|
42
|
+
} from './tokens/BaseTokens';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* COLOR PALETTE - Main Export
|
|
3
|
+
*
|
|
4
|
+
* Aggregates all color modules and provides theme utilities
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { lightColors } from './colors/LightColors';
|
|
8
|
+
import { darkColors } from './colors/DarkColors';
|
|
9
|
+
import { withAlpha, isValidHexColor } from './colors/ColorUtils';
|
|
10
|
+
|
|
11
|
+
export type ColorPalette = typeof lightColors;
|
|
12
|
+
export type ThemeMode = 'light' | 'dark';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get color palette for specific theme mode
|
|
16
|
+
* @param mode - 'light' or 'dark'
|
|
17
|
+
* @returns Color palette object
|
|
18
|
+
*/
|
|
19
|
+
export const getColorPalette = (mode: ThemeMode): ColorPalette => {
|
|
20
|
+
return mode === 'dark' ? darkColors : lightColors;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Export all colors and utilities
|
|
24
|
+
export {
|
|
25
|
+
lightColors,
|
|
26
|
+
darkColors,
|
|
27
|
+
withAlpha,
|
|
28
|
+
isValidHexColor
|
|
29
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom Colors Types
|
|
3
|
+
*
|
|
4
|
+
* Types for custom theme color overrides
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ColorPalette } from './ColorPalette';
|
|
8
|
+
import { isValidHexColor } from './colors/ColorUtils';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Custom theme colors that can override default colors
|
|
12
|
+
*/
|
|
13
|
+
export interface CustomThemeColors {
|
|
14
|
+
primary?: string;
|
|
15
|
+
primaryLight?: string;
|
|
16
|
+
primaryDark?: string;
|
|
17
|
+
secondary?: string;
|
|
18
|
+
secondaryLight?: string;
|
|
19
|
+
secondaryDark?: string;
|
|
20
|
+
accent?: string;
|
|
21
|
+
accentLight?: string;
|
|
22
|
+
accentDark?: string;
|
|
23
|
+
buttonPrimary?: string;
|
|
24
|
+
buttonSecondary?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Validate custom colors object
|
|
29
|
+
* @param customColors - Custom colors to validate
|
|
30
|
+
* @returns true if all colors are valid hex format
|
|
31
|
+
*/
|
|
32
|
+
export const validateCustomColors = (customColors: CustomThemeColors): boolean => {
|
|
33
|
+
const colorValues = Object.values(customColors).filter(Boolean) as string[];
|
|
34
|
+
|
|
35
|
+
for (const color of colorValues) {
|
|
36
|
+
if (!isValidHexColor(color)) {
|
|
37
|
+
if (__DEV__) {
|
|
38
|
+
console.warn('[validateCustomColors] Invalid hex color:', color);
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return true;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Apply custom colors to color palette
|
|
49
|
+
* @param palette - Base color palette
|
|
50
|
+
* @param customColors - Custom colors to apply
|
|
51
|
+
* @returns Color palette with custom colors applied
|
|
52
|
+
*/
|
|
53
|
+
export const applyCustomColors = (
|
|
54
|
+
palette: ColorPalette,
|
|
55
|
+
customColors?: CustomThemeColors,
|
|
56
|
+
): ColorPalette => {
|
|
57
|
+
if (!customColors) {
|
|
58
|
+
return palette;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Validate custom colors
|
|
62
|
+
if (!validateCustomColors(customColors)) {
|
|
63
|
+
if (__DEV__) {
|
|
64
|
+
console.error('[applyCustomColors] Invalid custom colors provided, using defaults');
|
|
65
|
+
}
|
|
66
|
+
return palette;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const result: Partial<ColorPalette> = {
|
|
70
|
+
...palette,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Apply custom primary colors
|
|
74
|
+
if (customColors.primary) {
|
|
75
|
+
result.primary = customColors.primary;
|
|
76
|
+
if (!customColors.buttonPrimary) {
|
|
77
|
+
result.buttonPrimary = customColors.primary;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (customColors.primaryLight) {
|
|
81
|
+
result.primaryLight = customColors.primaryLight;
|
|
82
|
+
}
|
|
83
|
+
if (customColors.primaryDark) {
|
|
84
|
+
result.primaryDark = customColors.primaryDark;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Apply custom secondary colors
|
|
88
|
+
if (customColors.secondary) {
|
|
89
|
+
result.secondary = customColors.secondary;
|
|
90
|
+
if (!customColors.buttonSecondary) {
|
|
91
|
+
result.buttonSecondary = customColors.secondary;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (customColors.secondaryLight) {
|
|
95
|
+
result.secondaryLight = customColors.secondaryLight;
|
|
96
|
+
}
|
|
97
|
+
if (customColors.secondaryDark) {
|
|
98
|
+
result.secondaryDark = customColors.secondaryDark;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Apply custom accent colors
|
|
102
|
+
if (customColors.accent) {
|
|
103
|
+
result.accent = customColors.accent;
|
|
104
|
+
}
|
|
105
|
+
if (customColors.accentLight) {
|
|
106
|
+
result.accentLight = customColors.accentLight;
|
|
107
|
+
}
|
|
108
|
+
if (customColors.accentDark) {
|
|
109
|
+
result.accentDark = customColors.accentDark;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Apply custom button colors (override primary/secondary if set)
|
|
113
|
+
if (customColors.buttonPrimary) {
|
|
114
|
+
result.buttonPrimary = customColors.buttonPrimary;
|
|
115
|
+
}
|
|
116
|
+
if (customColors.buttonSecondary) {
|
|
117
|
+
result.buttonSecondary = customColors.buttonSecondary;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return result as ColorPalette;
|
|
121
|
+
};
|
|
122
|
+
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { ColorPalette, ThemeMode } from "./ColorPalette";
|
|
2
|
+
import type { ExtendedColorPalette } from "./themes";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Font weight type compatible with React Navigation v7
|
|
6
|
+
*/
|
|
7
|
+
type FontWeight =
|
|
8
|
+
| "normal"
|
|
9
|
+
| "bold"
|
|
10
|
+
| "100"
|
|
11
|
+
| "200"
|
|
12
|
+
| "300"
|
|
13
|
+
| "400"
|
|
14
|
+
| "500"
|
|
15
|
+
| "600"
|
|
16
|
+
| "700"
|
|
17
|
+
| "800"
|
|
18
|
+
| "900";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Font configuration for navigation theme
|
|
22
|
+
*/
|
|
23
|
+
interface FontConfig {
|
|
24
|
+
fontFamily: string;
|
|
25
|
+
fontWeight: FontWeight;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Navigation theme type compatible with React Navigation v7
|
|
30
|
+
*/
|
|
31
|
+
export interface NavigationTheme {
|
|
32
|
+
dark: boolean;
|
|
33
|
+
colors: {
|
|
34
|
+
primary: string;
|
|
35
|
+
background: string;
|
|
36
|
+
card: string;
|
|
37
|
+
text: string;
|
|
38
|
+
border: string;
|
|
39
|
+
notification: string;
|
|
40
|
+
};
|
|
41
|
+
fonts: {
|
|
42
|
+
regular: FontConfig;
|
|
43
|
+
medium: FontConfig;
|
|
44
|
+
bold: FontConfig;
|
|
45
|
+
heavy: FontConfig;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Creates a React Navigation theme from design system colors
|
|
51
|
+
* Compatible with React Navigation v7+
|
|
52
|
+
*/
|
|
53
|
+
export const createNavigationTheme = (
|
|
54
|
+
colors: ColorPalette | ExtendedColorPalette,
|
|
55
|
+
themeMode: ThemeMode
|
|
56
|
+
): NavigationTheme => ({
|
|
57
|
+
dark: themeMode === "dark",
|
|
58
|
+
colors: {
|
|
59
|
+
primary: colors.primary,
|
|
60
|
+
background: colors.backgroundPrimary,
|
|
61
|
+
card: colors.surface,
|
|
62
|
+
text: colors.textPrimary,
|
|
63
|
+
border: colors.borderLight,
|
|
64
|
+
notification: colors.error,
|
|
65
|
+
},
|
|
66
|
+
fonts: {
|
|
67
|
+
regular: { fontFamily: "System", fontWeight: "400" },
|
|
68
|
+
medium: { fontFamily: "System", fontWeight: "500" },
|
|
69
|
+
bold: { fontFamily: "System", fontWeight: "700" },
|
|
70
|
+
heavy: { fontFamily: "System", fontWeight: "800" },
|
|
71
|
+
},
|
|
72
|
+
});
|