@umituz/react-native-design-system 4.28.11 → 4.28.12

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.
Files changed (47) hide show
  1. package/package.json +31 -8
  2. package/src/atoms/AtomicAvatar.tsx +69 -40
  3. package/src/atoms/AtomicSpinner.tsx +24 -22
  4. package/src/atoms/AtomicText.tsx +32 -27
  5. package/src/atoms/AtomicTextArea.tsx +17 -15
  6. package/src/atoms/EmptyState.tsx +44 -41
  7. package/src/atoms/button/AtomicButton.tsx +8 -9
  8. package/src/atoms/card/AtomicCard.tsx +26 -8
  9. package/src/atoms/datepicker/components/DatePickerButton.tsx +8 -8
  10. package/src/atoms/datepicker/components/DatePickerModal.tsx +7 -7
  11. package/src/atoms/fab/styles/fabStyles.ts +0 -21
  12. package/src/atoms/icon/index.ts +6 -20
  13. package/src/atoms/picker/components/PickerModal.tsx +24 -4
  14. package/src/atoms/skeleton/AtomicSkeleton.tsx +9 -11
  15. package/src/carousel/Carousel.tsx +43 -20
  16. package/src/carousel/carouselCalculations.ts +12 -9
  17. package/src/carousel/index.ts +0 -1
  18. package/src/device/detection/iPadDetection.ts +5 -14
  19. package/src/device/infrastructure/services/DeviceFeatureService.ts +89 -9
  20. package/src/device/infrastructure/services/DeviceInfoService.ts +33 -0
  21. package/src/device/infrastructure/services/UserFriendlyIdService.ts +8 -6
  22. package/src/device/infrastructure/utils/__tests__/stringUtils.test.ts +56 -20
  23. package/src/device/infrastructure/utils/nativeModuleUtils.ts +16 -2
  24. package/src/device/infrastructure/utils/stringUtils.ts +51 -5
  25. package/src/filesystem/domain/utils/FileUtils.ts +5 -1
  26. package/src/image/domain/utils/ImageUtils.ts +6 -0
  27. package/src/layouts/AppHeader/AppHeader.tsx +13 -3
  28. package/src/layouts/Container/Container.tsx +19 -1
  29. package/src/layouts/FormLayout/FormLayout.tsx +20 -1
  30. package/src/layouts/Grid/Grid.tsx +34 -4
  31. package/src/layouts/ScreenHeader/ScreenHeader.tsx +4 -0
  32. package/src/layouts/ScreenLayout/ScreenLayout.tsx +42 -3
  33. package/src/molecules/SearchBar/SearchBar.tsx +27 -23
  34. package/src/molecules/action-footer/ActionFooter.tsx +32 -31
  35. package/src/molecules/alerts/AlertService.ts +60 -15
  36. package/src/molecules/avatar/Avatar.tsx +3 -3
  37. package/src/molecules/avatar/AvatarGroup.tsx +7 -7
  38. package/src/molecules/calendar/infrastructure/utils/DateUtilities.ts +12 -1
  39. package/src/molecules/calendar/presentation/components/CalendarDayCell.tsx +48 -32
  40. package/src/molecules/info-grid/InfoGrid.tsx +5 -3
  41. package/src/organisms/FormContainer.tsx +11 -1
  42. package/src/tanstack/domain/utils/ErrorHelpers.ts +2 -2
  43. package/src/tanstack/domain/utils/MetricsCalculator.ts +6 -1
  44. package/src/theme/core/colors/ColorUtils.ts +7 -4
  45. package/src/utils/formatters/stringFormatter.ts +18 -3
  46. package/src/utils/index.ts +6 -4
  47. package/src/utils/math/CalculationUtils.ts +10 -1
@@ -25,7 +25,34 @@ const getDeviceModule = (): typeof import('expo-device') | null => {
25
25
  };
26
26
 
27
27
  export class DeviceInfoService {
28
+ // Static cache for device info (app-lifetime)
29
+ private static cachedDeviceInfo: DeviceInfo | null = null;
30
+ private static cachePromise: Promise<DeviceInfo> | null = null;
31
+
28
32
  static async getDeviceInfo(): Promise<DeviceInfo> {
33
+ // Return cached data if available
34
+ if (this.cachedDeviceInfo) {
35
+ return this.cachedDeviceInfo;
36
+ }
37
+
38
+ // Return existing promise if cache is being populated
39
+ if (this.cachePromise) {
40
+ return this.cachePromise;
41
+ }
42
+
43
+ // Populate cache
44
+ this.cachePromise = this.fetchDeviceInfo();
45
+
46
+ try {
47
+ const deviceInfo = await this.cachePromise;
48
+ this.cachedDeviceInfo = deviceInfo;
49
+ return deviceInfo;
50
+ } finally {
51
+ this.cachePromise = null;
52
+ }
53
+ }
54
+
55
+ private static async fetchDeviceInfo(): Promise<DeviceInfo> {
29
56
  try {
30
57
  const Device = getDeviceModule();
31
58
 
@@ -74,6 +101,12 @@ export class DeviceInfoService {
74
101
  }
75
102
  }
76
103
 
104
+ // Clear cache (useful for testing)
105
+ static clearCache(): void {
106
+ this.cachedDeviceInfo = null;
107
+ this.cachePromise = null;
108
+ }
109
+
77
110
  private static getMinimalDeviceInfo(): DeviceInfo {
78
111
  return {
79
112
  brand: null,
@@ -32,7 +32,8 @@ export class UserFriendlyIdService {
32
32
  static async getUserFriendlyId(): Promise<string> {
33
33
  // Web platform - no native modules needed
34
34
  if (Platform.OS === 'web') {
35
- return `WebUser-${generateRandomId()}`;
35
+ const randomId = await generateRandomId();
36
+ return `WebUser-${randomId}`;
36
37
  }
37
38
 
38
39
  try {
@@ -44,25 +45,26 @@ export class UserFriendlyIdService {
44
45
  if (deviceInfo && (deviceInfo.modelName || deviceInfo.deviceName)) {
45
46
  const model = deviceInfo.modelName || deviceInfo.deviceName || 'Device';
46
47
  const cleanModel = cleanModelName(model);
47
- const idPart = extractIdPart(deviceId, 6);
48
+ const idPart = await extractIdPart(deviceId, 6);
48
49
 
49
50
  return `${cleanModel}-${idPart}`;
50
51
  }
51
52
 
52
53
  // Fallback: Use platform + random ID
53
- return this.generateFallbackId();
54
+ return await this.generateFallbackId();
54
55
  } catch {
55
56
  // Final fallback: Generate safe random ID
56
- return this.generateFallbackId();
57
+ return await this.generateFallbackId();
57
58
  }
58
59
  }
59
60
 
60
61
  /**
61
62
  * Generate fallback ID when native modules are not available
62
63
  */
63
- private static generateFallbackId(): string {
64
+ private static async generateFallbackId(): Promise<string> {
64
65
  const platformPrefix = getPlatformPrefix(Platform.OS);
65
- return `${platformPrefix}-${generateRandomId()}`;
66
+ const randomId = await generateRandomId();
67
+ return `${platformPrefix}-${randomId}`;
66
68
  }
67
69
  }
68
70
 
@@ -2,7 +2,7 @@
2
2
  * String Utils Tests
3
3
  */
4
4
 
5
- import { cleanModelName, extractIdPart, generateRandomId, getPlatformPrefix } from '../stringUtils';
5
+ import { cleanModelName, extractIdPart, generateRandomId, generateRandomIdSync, extractIdPartSync, getPlatformPrefix } from '../stringUtils';
6
6
 
7
7
  describe('String Utils', () => {
8
8
  describe('cleanModelName', () => {
@@ -33,60 +33,96 @@ describe('String Utils', () => {
33
33
  });
34
34
 
35
35
  describe('extractIdPart', () => {
36
- it('should extract last N characters from device ID', () => {
36
+ it('should extract last N characters from device ID', async () => {
37
37
  const deviceId = '12345678-1234-1234-1234-123456789012';
38
- const result = extractIdPart(deviceId, 6);
38
+ const result = await extractIdPart(deviceId, 6);
39
39
  expect(result).toBe('789012');
40
40
  });
41
41
 
42
- it('should use default length of 6 when not specified', () => {
42
+ it('should use default length of 6 when not specified', async () => {
43
43
  const deviceId = '12345678-1234-1234-1234-123456789012';
44
- const result = extractIdPart(deviceId);
44
+ const result = await extractIdPart(deviceId);
45
45
  expect(result).toBe('789012');
46
46
  });
47
47
 
48
- it('should handle null device ID by generating random ID', () => {
49
- const result = extractIdPart(null, 6);
48
+ it('should handle null device ID by generating random ID', async () => {
49
+ const result = await extractIdPart(null, 6);
50
50
  expect(result).toMatch(/^[A-Z0-9]{6}$/);
51
51
  });
52
52
 
53
- it('should handle empty device ID by generating random ID', () => {
54
- const result = extractIdPart('', 6);
53
+ it('should handle empty device ID by generating random ID', async () => {
54
+ const result = await extractIdPart('', 6);
55
55
  expect(result).toMatch(/^[A-Z0-9]{6}$/);
56
56
  });
57
57
 
58
- it('should handle device ID shorter than requested length', () => {
58
+ it('should handle device ID shorter than requested length', async () => {
59
59
  const deviceId = 'ABC';
60
- const result = extractIdPart(deviceId, 6);
60
+ const result = await extractIdPart(deviceId, 6);
61
61
  expect(result).toBe('ABC');
62
62
  });
63
63
 
64
+ it('should convert to uppercase', async () => {
65
+ const deviceId = 'abcdef';
66
+ const result = await extractIdPart(deviceId, 3);
67
+ expect(result).toBe('DEF');
68
+ });
69
+ });
70
+
71
+ describe('extractIdPartSync', () => {
72
+ it('should extract last N characters from device ID', () => {
73
+ const deviceId = '12345678-1234-1234-1234-123456789012';
74
+ const result = extractIdPartSync(deviceId, 6);
75
+ expect(result).toBe('789012');
76
+ });
77
+
78
+ it('should handle null device ID by generating random ID', () => {
79
+ const result = extractIdPartSync(null, 6);
80
+ expect(result).toMatch(/^[A-Z0-9]{6}$/);
81
+ });
82
+
64
83
  it('should convert to uppercase', () => {
65
84
  const deviceId = 'abcdef';
66
- const result = extractIdPart(deviceId, 3);
85
+ const result = extractIdPartSync(deviceId, 3);
67
86
  expect(result).toBe('DEF');
68
87
  });
69
88
  });
70
89
 
71
90
  describe('generateRandomId', () => {
72
- it('should generate random ID with default length', () => {
73
- const result = generateRandomId();
91
+ it('should generate random ID with default length', async () => {
92
+ const result = await generateRandomId();
74
93
  expect(result).toMatch(/^[A-Z0-9]{6}$/);
75
94
  });
76
95
 
77
- it('should generate random ID with specified length', () => {
78
- const result = generateRandomId(10);
96
+ it('should generate random ID with specified length', async () => {
97
+ const result = await generateRandomId(10);
79
98
  expect(result).toMatch(/^[A-Z0-9]{10}$/);
80
99
  });
81
100
 
82
- it('should generate different IDs on multiple calls', () => {
83
- const id1 = generateRandomId(6);
84
- const id2 = generateRandomId(6);
101
+ it('should generate different IDs on multiple calls', async () => {
102
+ const id1 = await generateRandomId(6);
103
+ const id2 = await generateRandomId(6);
85
104
  expect(id1).not.toBe(id2);
86
105
  });
87
106
 
107
+ it('should handle length of 1', async () => {
108
+ const result = await generateRandomId(1);
109
+ expect(result).toMatch(/^[A-Z0-9]{1}$/);
110
+ });
111
+ });
112
+
113
+ describe('generateRandomIdSync', () => {
114
+ it('should generate random ID with default length', () => {
115
+ const result = generateRandomIdSync();
116
+ expect(result).toMatch(/^[A-Z0-9]{6}$/);
117
+ });
118
+
119
+ it('should generate random ID with specified length', () => {
120
+ const result = generateRandomIdSync(10);
121
+ expect(result).toMatch(/^[A-Z0-9]{10}$/);
122
+ });
123
+
88
124
  it('should handle length of 1', () => {
89
- const result = generateRandomId(1);
125
+ const result = generateRandomIdSync(1);
90
126
  expect(result).toMatch(/^[A-Z0-9]{1}$/);
91
127
  });
92
128
  });
@@ -15,14 +15,21 @@ export async function withTimeout<T>(
15
15
  operation: () => Promise<T>,
16
16
  timeoutMs: number = 1000,
17
17
  ): Promise<T | null> {
18
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
19
+
18
20
  try {
19
21
  const timeoutPromise = new Promise<never>((_, reject) => {
20
- setTimeout(() => reject(new Error('Operation timeout')), timeoutMs);
22
+ timeoutId = setTimeout(() => reject(new Error('Operation timeout')), timeoutMs);
21
23
  });
22
24
 
23
25
  return await Promise.race([operation(), timeoutPromise]);
24
26
  } catch {
25
27
  return null;
28
+ } finally {
29
+ // Always clear timeout to prevent memory leak
30
+ if (timeoutId !== undefined) {
31
+ clearTimeout(timeoutId);
32
+ }
26
33
  }
27
34
  }
28
35
 
@@ -51,9 +58,11 @@ export async function withTimeoutAll<T>(
51
58
  operations: Array<() => Promise<T>>,
52
59
  timeoutMs: number = 2000,
53
60
  ): Promise<Array<T | null>> {
61
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
62
+
54
63
  try {
55
64
  const timeoutPromise = new Promise<never>((_, reject) => {
56
- setTimeout(() => reject(new Error('Operations timeout')), timeoutMs);
65
+ timeoutId = setTimeout(() => reject(new Error('Operations timeout')), timeoutMs);
57
66
  });
58
67
 
59
68
  const results = await Promise.race([
@@ -64,6 +73,11 @@ export async function withTimeoutAll<T>(
64
73
  return results as Array<T | null>;
65
74
  } catch {
66
75
  return operations.map(() => null);
76
+ } finally {
77
+ // Always clear timeout to prevent memory leak
78
+ if (timeoutId !== undefined) {
79
+ clearTimeout(timeoutId);
80
+ }
67
81
  }
68
82
  }
69
83
 
@@ -24,21 +24,67 @@ export function cleanModelName(model: string | null | undefined): string {
24
24
  * @param length - Length of ID part to extract (default: 6)
25
25
  * @returns Last N characters of device ID in uppercase
26
26
  */
27
- export function extractIdPart(deviceId: string | null, length: number = 6): string {
27
+ export async function extractIdPart(deviceId: string | null, length: number = 6): Promise<string> {
28
28
  if (!deviceId) {
29
- return generateRandomId(length);
29
+ return await generateRandomId(length);
30
30
  }
31
31
  const start = Math.max(0, deviceId.length - length);
32
32
  return deviceId.substring(start).toUpperCase();
33
33
  }
34
34
 
35
35
  /**
36
- * Generate random alphanumeric ID
36
+ * Synchronous version of extractIdPart (uses fallback for null deviceId)
37
+ * @param deviceId - Full device ID
38
+ * @param length - Length of ID part to extract (default: 6)
39
+ * @returns Last N characters of device ID in uppercase
40
+ */
41
+ export function extractIdPartSync(deviceId: string | null, length: number = 6): string {
42
+ if (!deviceId) {
43
+ return generateRandomIdSync(length);
44
+ }
45
+ const start = Math.max(0, deviceId.length - length);
46
+ return deviceId.substring(start).toUpperCase();
47
+ }
48
+
49
+ /**
50
+ * Generate random alphanumeric ID using cryptographically secure random bytes
51
+ * @param length - Length of ID to generate (default: 6)
52
+ * @returns Random ID in uppercase
53
+ */
54
+ export async function generateRandomId(length: number = 6): Promise<string> {
55
+ try {
56
+ // Use expo-crypto for cryptographically secure random bytes
57
+ const { getRandomBytesAsync } = require('expo-crypto');
58
+ const bytes: Uint8Array = await getRandomBytesAsync(length);
59
+
60
+ return Array.from(bytes)
61
+ .map(byte => byte.toString(36))
62
+ .join('')
63
+ .substring(0, length)
64
+ .toUpperCase();
65
+ } catch {
66
+ // Fallback with __DEV__ warning for environments without expo-crypto
67
+ if (__DEV__) {
68
+ console.warn('[stringUtils] expo-crypto not available, using insecure fallback');
69
+ }
70
+ return Array.from({ length }, () =>
71
+ Math.floor(Math.random() * 36).toString(36)
72
+ ).join('').toUpperCase();
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Synchronous version of generateRandomId (uses fallback)
37
78
  * @param length - Length of ID to generate (default: 6)
38
79
  * @returns Random ID in uppercase
39
80
  */
40
- export function generateRandomId(length: number = 6): string {
41
- return Math.random().toString(36).substring(2, 2 + length).toUpperCase();
81
+ export function generateRandomIdSync(length: number = 6): string {
82
+ if (__DEV__) {
83
+ console.warn('[stringUtils] Using insecure fallback for random ID generation');
84
+ }
85
+ return Array.from({ length }, () =>
86
+ Math.floor(Math.random() * 36).toString(36)
87
+ ).join('').toUpperCase();
42
88
  }
43
89
 
44
90
  /**
@@ -59,7 +59,11 @@ export class FileUtils {
59
59
  */
60
60
  static getFileExtension(filename: string): string {
61
61
  const lastDot = filename.lastIndexOf('.');
62
- return lastDot > 0 ? filename.substring(lastDot) : '';
62
+ // Check dot is not at position 0 (dotfile like .gitignore) and not at end
63
+ if (lastDot > 0 && lastDot < filename.length - 1) {
64
+ return filename.substring(lastDot);
65
+ }
66
+ return '';
63
67
  }
64
68
 
65
69
  /**
@@ -12,6 +12,12 @@ export class ImageUtils {
12
12
  }
13
13
 
14
14
  static getAspectRatio(width: number, height: number): number {
15
+ if (height === 0) {
16
+ if (__DEV__) {
17
+ console.warn('[ImageUtils] Cannot calculate aspect ratio when height is zero');
18
+ }
19
+ return 1; // Default to square aspect ratio
20
+ }
15
21
  return width / height;
16
22
  }
17
23
 
@@ -8,7 +8,7 @@
8
8
  * Composition: AtomicIcon + AtomicText + AtomicButton
9
9
  */
10
10
 
11
- import React from 'react';
11
+ import React, { useMemo } from 'react';
12
12
  import { View, type ViewStyle } from 'react-native';
13
13
  import { SafeAreaView } from '../../safe-area';
14
14
  import { useAppDesignTokens } from '../../theme';
@@ -51,13 +51,23 @@ export const AppHeader: React.FC<AppHeaderProps> = ({
51
51
  onRightPress,
52
52
  backgroundColor,
53
53
  style,
54
+ accessibilityLabel,
55
+ accessibilityHint,
56
+ accessible,
54
57
  }) => {
55
58
  const tokens = useAppDesignTokens();
56
59
  const bgColor = backgroundColor || tokens.colors.surface;
57
- const styles = createAppHeaderStyles(tokens);
60
+
61
+ const styles = useMemo(() => createAppHeaderStyles(tokens), [tokens]);
58
62
 
59
63
  return (
60
- <SafeAreaView style={[styles.safeArea, { backgroundColor: bgColor }]}>
64
+ <SafeAreaView
65
+ style={[styles.safeArea, { backgroundColor: bgColor }]}
66
+ accessibilityLabel={accessibilityLabel || title}
67
+ accessibilityHint={accessibilityHint}
68
+ accessible={accessible !== false}
69
+ accessibilityRole="header"
70
+ >
61
71
  <View style={[styles.container, { backgroundColor: bgColor }, style]}>
62
72
  {/* Left Action */}
63
73
  <View style={styles.leftContainer}>
@@ -28,6 +28,15 @@ export interface ContainerProps {
28
28
 
29
29
  /** Test ID */
30
30
  testID?: string;
31
+
32
+ /** Accessibility label for the container */
33
+ accessibilityLabel?: string;
34
+
35
+ /** Accessibility role for the container */
36
+ accessibilityRole?: 'region' | 'section' | 'article';
37
+
38
+ /** Whether the container is accessible */
39
+ accessible?: boolean;
31
40
  }
32
41
 
33
42
  /**
@@ -47,6 +56,9 @@ export const Container: React.FC<ContainerProps> = ({
47
56
  center = true,
48
57
  style,
49
58
  testID,
59
+ accessibilityLabel,
60
+ accessibilityRole = 'region',
61
+ accessible,
50
62
  }) => {
51
63
  const { maxContentWidth } = useResponsive();
52
64
  const tokens = useAppDesignTokens();
@@ -69,7 +81,13 @@ export const Container: React.FC<ContainerProps> = ({
69
81
  );
70
82
 
71
83
  return (
72
- <View style={[styles.container, style]} testID={testID}>
84
+ <View
85
+ style={[styles.container, style]}
86
+ testID={testID}
87
+ accessibilityLabel={accessibilityLabel}
88
+ accessibilityRole={accessibilityRole as any}
89
+ accessible={accessible !== false}
90
+ >
73
91
  {children}
74
92
  </View>
75
93
  );
@@ -29,6 +29,15 @@ export interface FormLayoutProps {
29
29
 
30
30
  /** Test ID */
31
31
  testID?: string;
32
+
33
+ /** Accessibility label for the form */
34
+ accessibilityLabel?: string;
35
+
36
+ /** Accessibility hint for the form */
37
+ accessibilityHint?: string;
38
+
39
+ /** Whether the form is accessible */
40
+ accessible?: boolean;
32
41
  }
33
42
 
34
43
  /**
@@ -51,6 +60,9 @@ export const FormLayout: React.FC<FormLayoutProps> = ({
51
60
  disableKeyboardAvoid = false,
52
61
  disableScroll = false,
53
62
  testID,
63
+ accessibilityLabel,
64
+ accessibilityHint,
65
+ accessible,
54
66
  }) => {
55
67
  const tokens = useAppDesignTokens();
56
68
  const { insets } = useResponsive();
@@ -83,7 +95,14 @@ export const FormLayout: React.FC<FormLayoutProps> = ({
83
95
  );
84
96
 
85
97
  const content = (
86
- <View style={styles.formContent} testID={testID}>
98
+ <View
99
+ style={styles.formContent}
100
+ testID={testID}
101
+ accessibilityLabel={accessibilityLabel || "Form"}
102
+ accessibilityHint={accessibilityHint}
103
+ accessible={accessible !== false}
104
+ accessibilityRole="form"
105
+ >
87
106
  {children}
88
107
  </View>
89
108
  );
@@ -5,7 +5,7 @@
5
5
  * Uses design system responsive utilities
6
6
  */
7
7
 
8
- import React, { useMemo } from 'react';
8
+ import React, { useMemo, useId } from 'react';
9
9
  import { View, StyleSheet, type StyleProp, type ViewStyle } from 'react-native';
10
10
  import { useResponsive } from '../../responsive';
11
11
  import { useAppDesignTokens } from '../../theme';
@@ -28,6 +28,12 @@ export interface GridProps {
28
28
 
29
29
  /** Test ID for testing */
30
30
  testID?: string;
31
+
32
+ /** Accessibility label for the grid */
33
+ accessibilityLabel?: string;
34
+
35
+ /** Whether the grid is accessible */
36
+ accessible?: boolean;
31
37
  }
32
38
 
33
39
  /**
@@ -49,9 +55,12 @@ export const Grid: React.FC<GridProps> = ({
49
55
  gap,
50
56
  style,
51
57
  testID,
58
+ accessibilityLabel,
59
+ accessible,
52
60
  }) => {
53
61
  const { gridColumns, spacingMultiplier } = useResponsive();
54
62
  const tokens = useAppDesignTokens();
63
+ const generatedIdPrefix = useId();
55
64
 
56
65
  // Calculate responsive columns
57
66
  const columns = gridColumns || (mobileColumns && tabletColumns
@@ -82,12 +91,33 @@ export const Grid: React.FC<GridProps> = ({
82
91
  const childArray = React.Children.toArray(children);
83
92
 
84
93
  return (
85
- <View style={[styles.container, style]} testID={testID}>
94
+ <View
95
+ style={[styles.container, style]}
96
+ testID={testID}
97
+ accessibilityLabel={accessibilityLabel}
98
+ accessibilityRole={"grid" as any}
99
+ accessible={accessible !== false}
100
+ >
86
101
  {childArray.map((child, index) => {
87
- const key = (child as React.ReactElement).key || `grid-item-${index}`;
102
+ const childKey = (child as React.ReactElement).key;
103
+
104
+ // Warn in development if child is missing key
105
+ if (__DEV__ && !childKey) {
106
+ console.warn(
107
+ `[Grid] Child at index ${index} is missing a "key" prop. ` +
108
+ `This may cause issues with React reconciliation. ` +
109
+ `Please ensure all grid children have unique keys.`
110
+ );
111
+ }
112
+
113
+ const key = childKey || `${generatedIdPrefix}-grid-item-${index}`;
88
114
 
89
115
  return (
90
- <View key={key} style={itemStyle}>
116
+ <View
117
+ key={key}
118
+ style={itemStyle}
119
+ accessibilityRole={"gridcell" as any}
120
+ >
91
121
  {child}
92
122
  </View>
93
123
  );
@@ -86,6 +86,10 @@ const ScreenHeaderBackButton: React.FC<{
86
86
  onPress={handleBackPress}
87
87
  hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
88
88
  testID={`${testID}-back-button`}
89
+ accessibilityRole="button"
90
+ accessibilityLabel="Go back"
91
+ accessibilityState={{ disabled: !onBackPress }}
92
+ accessible
89
93
  >
90
94
  <AtomicIcon name={backIconName || 'arrow-back'} color={backIconColor} />
91
95
  </TouchableOpacity>
@@ -3,13 +3,34 @@
3
3
  */
4
4
 
5
5
  import React, { useMemo } from 'react';
6
- import { View, ScrollView, KeyboardAvoidingView, Platform } from 'react-native';
6
+ import { View, ScrollView, KeyboardAvoidingView, Platform, type StyleProp, type ViewStyle } from 'react-native';
7
7
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
8
8
  import { useAppDesignTokens } from '../../theme';
9
9
  import { getScreenLayoutConfig } from '../../responsive/responsiveLayout';
10
10
  import { getScreenLayoutStyles } from './styles/screenLayoutStyles';
11
11
  import type { ScreenLayoutProps } from './types';
12
12
 
13
+ export interface ScreenLayoutProps {
14
+ children: React.ReactNode;
15
+ scrollable?: boolean;
16
+ edges?: ('top' | 'bottom' | 'left' | 'right')[];
17
+ header?: React.ReactNode;
18
+ footer?: React.ReactNode;
19
+ backgroundColor?: string;
20
+ containerStyle?: StyleProp<ViewStyle>;
21
+ contentContainerStyle?: StyleProp<ViewStyle>;
22
+ testID?: string;
23
+ hideScrollIndicator?: boolean;
24
+ keyboardAvoiding?: boolean;
25
+ keyboardVerticalOffset?: number;
26
+ maxWidth?: number;
27
+ fullWidth?: boolean;
28
+ refreshControl?: React.ReactElement;
29
+ accessibilityLabel?: string;
30
+ accessibilityRole?: 'window' | 'region' | 'section';
31
+ accessible?: boolean;
32
+ }
33
+
13
34
  // Lazy-load react-native-keyboard-controller (optional peer dep).
14
35
  // Falls back to React Native's built-in components when not installed.
15
36
  let KCKeyboardAvoidingView: React.ComponentType<any> | null = null;
@@ -40,6 +61,9 @@ export const ScreenLayout: React.FC<ScreenLayoutProps> = (props: ScreenLayoutPro
40
61
  maxWidth,
41
62
  fullWidth = false,
42
63
  refreshControl,
64
+ accessibilityLabel,
65
+ accessibilityRole = 'region',
66
+ accessible,
43
67
  } = props;
44
68
 
45
69
  const tokens = useAppDesignTokens();
@@ -80,6 +104,9 @@ export const ScreenLayout: React.FC<ScreenLayoutProps> = (props: ScreenLayoutPro
80
104
  paddingRight,
81
105
  },
82
106
  ]}
107
+ accessibilityLabel={accessibilityLabel}
108
+ accessibilityRole={accessibilityRole}
109
+ accessible={accessible !== false}
83
110
  >
84
111
  {header}
85
112
  {scrollable ? (
@@ -109,7 +136,13 @@ export const ScreenLayout: React.FC<ScreenLayoutProps> = (props: ScreenLayoutPro
109
136
  if (keyboardAvoiding) {
110
137
  const KAV = KCKeyboardAvoidingView ?? KeyboardAvoidingView;
111
138
  return (
112
- <View style={[styles.container, { backgroundColor: bgColor }, containerStyle]} testID={testID}>
139
+ <View
140
+ style={[styles.container, { backgroundColor: bgColor }, containerStyle]}
141
+ testID={testID}
142
+ accessibilityLabel={accessibilityLabel}
143
+ accessibilityRole={accessibilityRole}
144
+ accessible={accessible !== false}
145
+ >
113
146
  <KAV
114
147
  style={styles.keyboardAvoidingView}
115
148
  behavior={Platform.OS === 'ios' ? 'padding' : undefined}
@@ -122,7 +155,13 @@ export const ScreenLayout: React.FC<ScreenLayoutProps> = (props: ScreenLayoutPro
122
155
  }
123
156
 
124
157
  return (
125
- <View style={[styles.container, { backgroundColor: bgColor }, containerStyle]} testID={testID}>
158
+ <View
159
+ style={[styles.container, { backgroundColor: bgColor }, containerStyle]}
160
+ testID={testID}
161
+ accessibilityLabel={accessibilityLabel}
162
+ accessibilityRole={accessibilityRole}
163
+ accessible={accessible !== false}
164
+ >
126
165
  <View style={styles.keyboardAvoidingView}>{content}</View>
127
166
  </View>
128
167
  );