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

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 (49) hide show
  1. package/package.json +31 -8
  2. package/src/atoms/AtomicAvatar.tsx +69 -40
  3. package/src/atoms/AtomicDatePicker.tsx +6 -6
  4. package/src/atoms/AtomicSpinner.tsx +24 -22
  5. package/src/atoms/AtomicText.tsx +32 -27
  6. package/src/atoms/AtomicTextArea.tsx +17 -15
  7. package/src/atoms/EmptyState.tsx +44 -41
  8. package/src/atoms/button/AtomicButton.tsx +8 -9
  9. package/src/atoms/card/AtomicCard.tsx +26 -8
  10. package/src/atoms/datepicker/components/DatePickerButton.tsx +8 -8
  11. package/src/atoms/datepicker/components/DatePickerModal.tsx +7 -7
  12. package/src/atoms/fab/styles/fabStyles.ts +0 -21
  13. package/src/atoms/icon/index.ts +6 -20
  14. package/src/atoms/picker/components/PickerModal.tsx +24 -4
  15. package/src/atoms/skeleton/AtomicSkeleton.tsx +9 -11
  16. package/src/carousel/Carousel.tsx +43 -20
  17. package/src/carousel/carouselCalculations.ts +12 -9
  18. package/src/carousel/index.ts +0 -1
  19. package/src/device/detection/iPadDetection.ts +5 -14
  20. package/src/device/infrastructure/services/DeviceFeatureService.ts +89 -9
  21. package/src/device/infrastructure/services/DeviceInfoService.ts +33 -0
  22. package/src/device/infrastructure/services/UserFriendlyIdService.ts +8 -6
  23. package/src/device/infrastructure/utils/__tests__/stringUtils.test.ts +56 -20
  24. package/src/device/infrastructure/utils/nativeModuleUtils.ts +16 -2
  25. package/src/device/infrastructure/utils/stringUtils.ts +51 -5
  26. package/src/filesystem/domain/utils/FileUtils.ts +5 -1
  27. package/src/image/domain/utils/ImageUtils.ts +6 -0
  28. package/src/layouts/AppHeader/AppHeader.tsx +13 -3
  29. package/src/layouts/Container/Container.tsx +19 -1
  30. package/src/layouts/FormLayout/FormLayout.tsx +20 -1
  31. package/src/layouts/Grid/Grid.tsx +34 -4
  32. package/src/layouts/ScreenHeader/ScreenHeader.tsx +4 -0
  33. package/src/layouts/ScreenLayout/ScreenLayout.tsx +42 -3
  34. package/src/molecules/SearchBar/SearchBar.tsx +27 -23
  35. package/src/molecules/action-footer/ActionFooter.tsx +32 -31
  36. package/src/molecules/alerts/AlertService.ts +60 -15
  37. package/src/molecules/avatar/Avatar.tsx +3 -3
  38. package/src/molecules/avatar/AvatarGroup.tsx +7 -7
  39. package/src/molecules/bottom-sheet/components/BottomSheet.tsx +3 -3
  40. package/src/molecules/calendar/infrastructure/utils/DateUtilities.ts +12 -1
  41. package/src/molecules/calendar/presentation/components/CalendarDayCell.tsx +48 -32
  42. package/src/molecules/info-grid/InfoGrid.tsx +5 -3
  43. package/src/organisms/FormContainer.tsx +11 -1
  44. package/src/tanstack/domain/utils/ErrorHelpers.ts +2 -2
  45. package/src/tanstack/domain/utils/MetricsCalculator.ts +6 -1
  46. package/src/theme/core/colors/ColorUtils.ts +7 -4
  47. package/src/utils/formatters/stringFormatter.ts +18 -3
  48. package/src/utils/index.ts +6 -4
  49. package/src/utils/math/CalculationUtils.ts +10 -1
@@ -21,8 +21,70 @@ import { ErrorHandler } from '../../../utils/errors/ErrorHandler';
21
21
  export class DeviceFeatureService {
22
22
  private static config: DeviceFeatureConfig = { features: {} };
23
23
 
24
+ // In-memory usage tracking for debouncing
25
+ private static inMemoryUsage = new Map<string, number>();
26
+ private static dirtyFeatures = new Set<string>();
27
+ private static flushInterval: ReturnType<typeof setInterval> | null = null;
28
+ private static FLUSH_DELAY = 5000; // 5 seconds
29
+
24
30
  static setConfig(config: DeviceFeatureConfig): void {
25
31
  this.config = config;
32
+ this.startPeriodicFlush();
33
+ }
34
+
35
+ /**
36
+ * Start periodic flush of in-memory usage to storage
37
+ */
38
+ private static startPeriodicFlush(): void {
39
+ if (this.flushInterval) return;
40
+
41
+ this.flushInterval = setInterval(() => {
42
+ this.flushDirtyFeatures();
43
+ }, this.FLUSH_DELAY);
44
+ }
45
+
46
+ /**
47
+ * Flush dirty features to storage
48
+ */
49
+ private static async flushDirtyFeatures(): Promise<void> {
50
+ if (this.dirtyFeatures.size === 0) return;
51
+
52
+ const featuresToFlush = Array.from(this.dirtyFeatures);
53
+ this.dirtyFeatures.clear();
54
+
55
+ for (const featureKey of featuresToFlush) {
56
+ const [deviceId, featureName] = featureKey.split(':');
57
+ const increment = this.inMemoryUsage.get(featureKey) || 0;
58
+
59
+ if (increment > 0) {
60
+ try {
61
+ const usage = await this.getFeatureUsage(deviceId, featureName);
62
+ const updatedUsage: DeviceFeatureUsage = {
63
+ ...usage,
64
+ usageCount: usage.usageCount + increment,
65
+ };
66
+
67
+ await this.setFeatureUsage(deviceId, featureName, updatedUsage);
68
+ this.inMemoryUsage.delete(featureKey);
69
+ } catch (error) {
70
+ ErrorHandler.log(error);
71
+ }
72
+ }
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Stop periodic flush (call on app cleanup)
78
+ */
79
+ static async destroy(): Promise<void> {
80
+ if (this.flushInterval) {
81
+ clearInterval(this.flushInterval);
82
+ this.flushInterval = null;
83
+ }
84
+
85
+ // Flush any remaining dirty features
86
+ await this.flushDirtyFeatures();
87
+ this.inMemoryUsage.clear();
26
88
  }
27
89
 
28
90
  static async checkFeatureAccess(
@@ -42,10 +104,17 @@ export class DeviceFeatureService {
42
104
  }
43
105
 
44
106
  const usage = await this.getFeatureUsage(deviceId, featureName);
107
+ const featureKey = `${deviceId}:${featureName}`;
108
+ const inMemoryIncrement = this.inMemoryUsage.get(featureKey) || 0;
109
+ const totalUsageCount = usage.usageCount + inMemoryIncrement;
110
+
45
111
  const shouldReset = this.shouldResetUsage(usage, featureConfig.resetPeriod);
46
112
 
47
113
  if (shouldReset) {
48
114
  await this.resetFeatureUsage(deviceId, featureName);
115
+ // Clear in-memory counter on reset
116
+ this.inMemoryUsage.delete(featureKey);
117
+ this.dirtyFeatures.delete(featureKey);
49
118
  return {
50
119
  isAllowed: true,
51
120
  remainingUses: featureConfig.maxUses - 1,
@@ -55,16 +124,16 @@ export class DeviceFeatureService {
55
124
  };
56
125
  }
57
126
 
58
- const isAllowed = usage.usageCount < featureConfig.maxUses;
127
+ const isAllowed = totalUsageCount < featureConfig.maxUses;
59
128
  const remainingUses = Math.max(
60
129
  0,
61
- featureConfig.maxUses - usage.usageCount
130
+ featureConfig.maxUses - totalUsageCount
62
131
  );
63
132
 
64
133
  return {
65
134
  isAllowed,
66
135
  remainingUses,
67
- usageCount: usage.usageCount,
136
+ usageCount: totalUsageCount,
68
137
  resetAt: this.calculateNextReset(featureConfig.resetPeriod),
69
138
  maxUses: featureConfig.maxUses,
70
139
  };
@@ -72,14 +141,25 @@ export class DeviceFeatureService {
72
141
 
73
142
  static async incrementFeatureUsage(featureName: string): Promise<void> {
74
143
  const deviceId = await PersistentDeviceIdService.getDeviceId();
75
- const usage = await this.getFeatureUsage(deviceId, featureName);
144
+ const featureKey = `${deviceId}:${featureName}`;
76
145
 
77
- const updatedUsage: DeviceFeatureUsage = {
78
- ...usage,
79
- usageCount: usage.usageCount + 1,
80
- };
146
+ // Increment in-memory counter
147
+ const currentCount = this.inMemoryUsage.get(featureKey) || 0;
148
+ this.inMemoryUsage.set(featureKey, currentCount + 1);
149
+
150
+ // Mark as dirty for periodic flush
151
+ this.dirtyFeatures.add(featureKey);
81
152
 
82
- await this.setFeatureUsage(deviceId, featureName, updatedUsage);
153
+ // If this is the first increment, fetch current usage and set baseline
154
+ if (currentCount === 0) {
155
+ try {
156
+ const usage = await this.getFeatureUsage(deviceId, featureName);
157
+ // Store baseline to avoid double-counting
158
+ this.inMemoryUsage.set(featureKey, 0);
159
+ } catch (error) {
160
+ ErrorHandler.log(error);
161
+ }
162
+ }
83
163
  }
84
164
 
85
165
  private static async getFeatureUsage(
@@ -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
  );