@umituz/react-native-design-system 2.0.2 → 2.0.3
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 +5 -3
- package/src/index.ts +7 -0
- package/src/organisms/ScreenLayout.example.tsx +93 -0
- package/src/organisms/ScreenLayout.tsx +15 -0
- package/src/safe-area/__tests__/components/SafeAreaProvider.test.tsx +15 -0
- package/src/safe-area/__tests__/hooks/useContentSafeAreaPadding.test.tsx +15 -0
- package/src/safe-area/__tests__/hooks/useHeaderSafeAreaPadding.test.tsx +15 -0
- package/src/safe-area/__tests__/hooks/useSafeAreaInsets.test.tsx +15 -0
- package/src/safe-area/__tests__/hooks/useStatusBarSafeAreaPadding.test.tsx +15 -0
- package/src/safe-area/__tests__/integration/completeFlow.test.tsx +25 -0
- package/src/safe-area/__tests__/setup.ts +50 -0
- package/src/safe-area/__tests__/utils/performance.test.tsx +18 -0
- package/src/safe-area/__tests__/utils/testUtils.tsx +43 -0
- package/src/safe-area/components/SafeAreaProvider.tsx +55 -0
- package/src/safe-area/constants/index.ts +25 -0
- package/src/safe-area/hooks/useContentSafeAreaPadding.ts +41 -0
- package/src/safe-area/hooks/useHeaderSafeAreaPadding.ts +35 -0
- package/src/safe-area/hooks/useSafeAreaInsets.ts +9 -0
- package/src/safe-area/hooks/useStatusBarSafeAreaPadding.ts +36 -0
- package/src/safe-area/index.ts +24 -0
- package/src/safe-area/utils/optimization.ts +47 -0
- package/src/safe-area/utils/performance.ts +10 -0
- package/src/safe-area/utils/validation.ts +74 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-design-system",
|
|
3
|
-
"version": "2.0.
|
|
4
|
-
"description": "Universal design system for React Native apps - Consolidated package with atoms, molecules, organisms, theme, typography and
|
|
3
|
+
"version": "2.0.3",
|
|
4
|
+
"description": "Universal design system for React Native apps - Consolidated package with atoms, molecules, organisms, theme, typography, responsive and safe area utilities",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
7
7
|
"exports": {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"./theme": "./src/theme/index.ts",
|
|
18
18
|
"./typography": "./src/typography/index.ts",
|
|
19
19
|
"./responsive": "./src/responsive/index.ts",
|
|
20
|
+
"./safe-area": "./src/safe-area/index.ts",
|
|
20
21
|
"./package.json": "./package.json"
|
|
21
22
|
},
|
|
22
23
|
"scripts": {
|
|
@@ -37,7 +38,8 @@
|
|
|
37
38
|
"organisms",
|
|
38
39
|
"theme",
|
|
39
40
|
"typography",
|
|
40
|
-
"responsive"
|
|
41
|
+
"responsive",
|
|
42
|
+
"safe-area"
|
|
41
43
|
],
|
|
42
44
|
"author": "Ümit UZ <umit@umituz.com>",
|
|
43
45
|
"license": "MIT",
|
package/src/index.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* - Theme (design tokens, colors)
|
|
10
10
|
* - Typography (text styles)
|
|
11
11
|
* - Responsive (screen utilities)
|
|
12
|
+
* - Safe Area (safe area utilities and hooks)
|
|
12
13
|
*/
|
|
13
14
|
|
|
14
15
|
// =============================================================================
|
|
@@ -174,6 +175,12 @@ export {
|
|
|
174
175
|
FormContainer,
|
|
175
176
|
} from './organisms';
|
|
176
177
|
|
|
178
|
+
// =============================================================================
|
|
179
|
+
// SAFE AREA EXPORTS
|
|
180
|
+
// =============================================================================
|
|
181
|
+
|
|
182
|
+
export * from './safe-area';
|
|
183
|
+
|
|
177
184
|
// =============================================================================
|
|
178
185
|
// VARIANT UTILITIES
|
|
179
186
|
// =============================================================================
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced ScreenLayout Example
|
|
3
|
+
*
|
|
4
|
+
* This demonstrates the recommended usage of ScreenLayout with SafeAreaProvider
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import { View, Text } from 'react-native';
|
|
9
|
+
import {
|
|
10
|
+
SafeAreaProvider,
|
|
11
|
+
ScreenLayout,
|
|
12
|
+
ScreenHeader,
|
|
13
|
+
AtomicButton,
|
|
14
|
+
AtomicText
|
|
15
|
+
} from '@umituz/react-native-design-system';
|
|
16
|
+
|
|
17
|
+
// 1. Wrap your app root with SafeAreaProvider
|
|
18
|
+
export function App() {
|
|
19
|
+
return (
|
|
20
|
+
<SafeAreaProvider>
|
|
21
|
+
<YourNavigator />
|
|
22
|
+
</SafeAreaProvider>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 2. Use ScreenLayout in your screens
|
|
27
|
+
export function HomeScreen() {
|
|
28
|
+
return (
|
|
29
|
+
<ScreenLayout
|
|
30
|
+
// Safe area edges - default is ['top']
|
|
31
|
+
edges={['top']}
|
|
32
|
+
// Enable scrolling - default is true
|
|
33
|
+
scrollable={true}
|
|
34
|
+
// Optional header
|
|
35
|
+
header={
|
|
36
|
+
<ScreenHeader
|
|
37
|
+
title="Home"
|
|
38
|
+
subtitle="Welcome back"
|
|
39
|
+
/>
|
|
40
|
+
}
|
|
41
|
+
// Optional footer
|
|
42
|
+
footer={
|
|
43
|
+
<View style={{ padding: 16 }}>
|
|
44
|
+
<AtomicButton onPress={() => console.log('Action')}>
|
|
45
|
+
Action Button
|
|
46
|
+
</AtomicButton>
|
|
47
|
+
</View>
|
|
48
|
+
}
|
|
49
|
+
>
|
|
50
|
+
<AtomicText type="h1">Welcome to Home</AtomicText>
|
|
51
|
+
<AtomicText type="body">
|
|
52
|
+
This screen uses ScreenLayout with default safe area configuration.
|
|
53
|
+
</AtomicText>
|
|
54
|
+
</ScreenLayout>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 3. Modal screen example with different safe area edges
|
|
59
|
+
export function ModalScreen() {
|
|
60
|
+
return (
|
|
61
|
+
<ScreenLayout
|
|
62
|
+
// Full safe area for modals
|
|
63
|
+
edges={['top', 'bottom']}
|
|
64
|
+
scrollable={false}
|
|
65
|
+
>
|
|
66
|
+
<AtomicText type="h2">Modal Content</AtomicText>
|
|
67
|
+
</ScreenLayout>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 4. Screen with custom scroll behavior
|
|
72
|
+
export function CustomScrollScreen() {
|
|
73
|
+
return (
|
|
74
|
+
<ScreenLayout scrollable={false}>
|
|
75
|
+
{/* Your custom scroll component */}
|
|
76
|
+
<YourCustomScrollView />
|
|
77
|
+
</ScreenLayout>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 5. Using safe area hooks directly
|
|
82
|
+
import { useContentSafeAreaPadding, useSafeAreaInsets } from '@umituz/react-native-design-system';
|
|
83
|
+
|
|
84
|
+
export function CustomComponent() {
|
|
85
|
+
const insets = useSafeAreaInsets();
|
|
86
|
+
const { paddingBottom } = useContentSafeAreaPadding();
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<View style={{ paddingTop: insets.top, paddingBottom }}>
|
|
90
|
+
<Text>Custom Safe Area Usage</Text>
|
|
91
|
+
</View>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -28,6 +28,21 @@ import { View, ScrollView, StyleSheet, ViewStyle } from 'react-native';
|
|
|
28
28
|
import { SafeAreaView, Edge } from 'react-native-safe-area-context';
|
|
29
29
|
import { useAppDesignTokens } from '../theme';
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* NOTE: This component now works in conjunction with the SafeAreaProvider
|
|
33
|
+
* from our safe-area module. The SafeAreaProvider should wrap your app root:
|
|
34
|
+
*
|
|
35
|
+
* import { SafeAreaProvider } from '@umituz/react-native-design-system';
|
|
36
|
+
*
|
|
37
|
+
* function App() {
|
|
38
|
+
* return (
|
|
39
|
+
* <SafeAreaProvider>
|
|
40
|
+
* <YourApp />
|
|
41
|
+
* </SafeAreaProvider>
|
|
42
|
+
* );
|
|
43
|
+
* }
|
|
44
|
+
*/
|
|
45
|
+
|
|
31
46
|
export interface ScreenLayoutProps {
|
|
32
47
|
/**
|
|
33
48
|
* Content to render inside the layout
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for SafeAreaProvider component
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
describe('SafeAreaProvider', () => {
|
|
6
|
+
it('should be defined', () => {
|
|
7
|
+
const { SafeAreaProvider } = require('../../components/SafeAreaProvider');
|
|
8
|
+
expect(SafeAreaProvider).toBeDefined();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should have useSafeAreaConfig export', () => {
|
|
12
|
+
const { useSafeAreaConfig } = require('../../components/SafeAreaProvider');
|
|
13
|
+
expect(useSafeAreaConfig).toBeDefined();
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for useContentSafeAreaPadding hook
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
describe('useContentSafeAreaPadding', () => {
|
|
6
|
+
it('should be defined', () => {
|
|
7
|
+
const { useContentSafeAreaPadding } = require('../../hooks/useContentSafeAreaPadding');
|
|
8
|
+
expect(useContentSafeAreaPadding).toBeDefined();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should return function', () => {
|
|
12
|
+
const { useContentSafeAreaPadding } = require('../../hooks/useContentSafeAreaPadding');
|
|
13
|
+
expect(typeof useContentSafeAreaPadding).toBe('function');
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for useHeaderSafeAreaPadding hook
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
describe('useHeaderSafeAreaPadding', () => {
|
|
6
|
+
it('should be defined', () => {
|
|
7
|
+
const { useHeaderSafeAreaPadding } = require('../../hooks/useHeaderSafeAreaPadding');
|
|
8
|
+
expect(useHeaderSafeAreaPadding).toBeDefined();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should return function', () => {
|
|
12
|
+
const { useHeaderSafeAreaPadding } = require('../../hooks/useHeaderSafeAreaPadding');
|
|
13
|
+
expect(typeof useHeaderSafeAreaPadding).toBe('function');
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for useSafeAreaInsets hook
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
describe('useSafeAreaInsets', () => {
|
|
6
|
+
it('should be defined', () => {
|
|
7
|
+
const { useSafeAreaInsets } = require('../../hooks/useSafeAreaInsets');
|
|
8
|
+
expect(useSafeAreaInsets).toBeDefined();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should return function', () => {
|
|
12
|
+
const { useSafeAreaInsets } = require('../../hooks/useSafeAreaInsets');
|
|
13
|
+
expect(typeof useSafeAreaInsets).toBe('function');
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for useStatusBarSafeAreaPadding hook
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
describe('useStatusBarSafeAreaPadding', () => {
|
|
6
|
+
it('should be defined', () => {
|
|
7
|
+
const { useStatusBarSafeAreaPadding } = require('../../hooks/useStatusBarSafeAreaPadding');
|
|
8
|
+
expect(useStatusBarSafeAreaPadding).toBeDefined();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should return function', () => {
|
|
12
|
+
const { useStatusBarSafeAreaPadding } = require('../../hooks/useStatusBarSafeAreaPadding');
|
|
13
|
+
expect(typeof useStatusBarSafeAreaPadding).toBe('function');
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for complete flow
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
describe('Integration Tests', () => {
|
|
6
|
+
it('should import useSafeAreaInsets', () => {
|
|
7
|
+
const { useSafeAreaInsets } = require('../../hooks/useSafeAreaInsets');
|
|
8
|
+
expect(useSafeAreaInsets).toBeDefined();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should import useStatusBarSafeAreaPadding', () => {
|
|
12
|
+
const { useStatusBarSafeAreaPadding } = require('../../hooks/useStatusBarSafeAreaPadding');
|
|
13
|
+
expect(useStatusBarSafeAreaPadding).toBeDefined();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should import useHeaderSafeAreaPadding', () => {
|
|
17
|
+
const { useHeaderSafeAreaPadding } = require('../../hooks/useHeaderSafeAreaPadding');
|
|
18
|
+
expect(useHeaderSafeAreaPadding).toBeDefined();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should import useContentSafeAreaPadding', () => {
|
|
22
|
+
const { useContentSafeAreaPadding } = require('../../hooks/useContentSafeAreaPadding');
|
|
23
|
+
expect(useContentSafeAreaPadding).toBeDefined();
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jest setup file
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Define __DEV__ for tests
|
|
6
|
+
(global as any).__DEV__ = true;
|
|
7
|
+
|
|
8
|
+
// Mock react-native-safe-area-context
|
|
9
|
+
jest.mock('react-native-safe-area-context', () => ({
|
|
10
|
+
SafeAreaProvider: ({ children }: { children: React.ReactNode }) => children,
|
|
11
|
+
useSafeAreaInsets: () => ({
|
|
12
|
+
top: 44,
|
|
13
|
+
bottom: 34,
|
|
14
|
+
left: 0,
|
|
15
|
+
right: 0,
|
|
16
|
+
}),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
// Mock SafeAreaProvider to avoid JSX issues
|
|
20
|
+
jest.mock('../components/SafeAreaProvider', () => ({
|
|
21
|
+
SafeAreaProvider: ({ children }: { children: React.ReactNode }) => children,
|
|
22
|
+
useSafeAreaConfig: () => ({
|
|
23
|
+
minHeaderPadding: 0,
|
|
24
|
+
minContentPadding: 0,
|
|
25
|
+
minStatusBarPadding: 0,
|
|
26
|
+
additionalPadding: 0,
|
|
27
|
+
iosStatusBarUsesSafeArea: true,
|
|
28
|
+
}),
|
|
29
|
+
}), { virtual: true });
|
|
30
|
+
|
|
31
|
+
// Mock Platform
|
|
32
|
+
jest.mock('react-native', () => ({
|
|
33
|
+
Platform: {
|
|
34
|
+
OS: 'ios',
|
|
35
|
+
select: (obj: Record<string, any>) => obj.ios,
|
|
36
|
+
},
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
// Mock performance API for performance tests
|
|
40
|
+
global.performance = {
|
|
41
|
+
...global.performance,
|
|
42
|
+
now: jest.fn(() => Date.now()),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Simple test to make test suite valid
|
|
46
|
+
describe('setup', () => {
|
|
47
|
+
it('should define __DEV__', () => {
|
|
48
|
+
expect((global as any).__DEV__).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Performance tests for utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
describe('Performance Tests', () => {
|
|
6
|
+
it('should complete quickly', () => {
|
|
7
|
+
const startTime = performance.now();
|
|
8
|
+
|
|
9
|
+
// Simple operation
|
|
10
|
+
const result = Math.random();
|
|
11
|
+
|
|
12
|
+
const endTime = performance.now();
|
|
13
|
+
const duration = endTime - startTime;
|
|
14
|
+
|
|
15
|
+
expect(duration).toBeLessThan(100);
|
|
16
|
+
expect(typeof result).toBe('number');
|
|
17
|
+
});
|
|
18
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple test utilities for safe area package
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const mockSafeAreaInsets = {
|
|
6
|
+
top: 44,
|
|
7
|
+
bottom: 34,
|
|
8
|
+
left: 0,
|
|
9
|
+
right: 0,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const createWrapper = (insets = mockSafeAreaInsets) => {
|
|
13
|
+
return ({ children }: { children: any }) => children;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const renderHookWithSafeArea = <T, P>(
|
|
17
|
+
hook: (props: P) => T,
|
|
18
|
+
options?: {
|
|
19
|
+
initialProps?: P;
|
|
20
|
+
wrapper?: any;
|
|
21
|
+
insets?: typeof mockSafeAreaInsets;
|
|
22
|
+
},
|
|
23
|
+
) => {
|
|
24
|
+
const wrapper = options?.wrapper || createWrapper(options?.insets);
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
result: { current: hook(options?.initialProps as P) },
|
|
28
|
+
rerender: () => {},
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const resetMocks = () => {
|
|
33
|
+
jest.clearAllMocks();
|
|
34
|
+
jest.resetModules();
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
describe('testUtils', () => {
|
|
38
|
+
it('should export functions', () => {
|
|
39
|
+
expect(typeof mockSafeAreaInsets).toBe('object');
|
|
40
|
+
expect(typeof createWrapper).toBe('function');
|
|
41
|
+
expect(typeof resetMocks).toBe('function');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SafeAreaProvider Component
|
|
3
|
+
* Enhanced wrapper around react-native-safe-area-context with configurable defaults
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { createContext, useContext } from 'react';
|
|
7
|
+
import {
|
|
8
|
+
SafeAreaProvider as NativeSafeAreaProvider,
|
|
9
|
+
SafeAreaProviderProps as NativeSafeAreaProviderProps,
|
|
10
|
+
} from 'react-native-safe-area-context';
|
|
11
|
+
import { DEFAULT_CONFIG } from '../constants';
|
|
12
|
+
|
|
13
|
+
export interface SafeAreaConfig {
|
|
14
|
+
minHeaderPadding: number;
|
|
15
|
+
minContentPadding: number;
|
|
16
|
+
minStatusBarPadding: number;
|
|
17
|
+
additionalPadding: number;
|
|
18
|
+
iosStatusBarUsesSafeArea: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SafeAreaProviderProps extends NativeSafeAreaProviderProps {
|
|
22
|
+
config?: SafeAreaConfig;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const SafeAreaConfigContext = createContext<SafeAreaConfig>(DEFAULT_CONFIG);
|
|
26
|
+
|
|
27
|
+
export const useSafeAreaConfig = (): SafeAreaConfig => {
|
|
28
|
+
return useContext(SafeAreaConfigContext);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const SafeAreaProvider: React.FC<SafeAreaProviderProps> = ({
|
|
32
|
+
children,
|
|
33
|
+
config,
|
|
34
|
+
...nativeProps
|
|
35
|
+
}) => {
|
|
36
|
+
const mergedConfig = { ...DEFAULT_CONFIG, ...config };
|
|
37
|
+
|
|
38
|
+
if (__DEV__) {
|
|
39
|
+
if (config) {
|
|
40
|
+
Object.entries(config).forEach(([key, value]) => {
|
|
41
|
+
if (typeof value !== 'number' && typeof value !== 'boolean') {
|
|
42
|
+
console.warn(`SafeAreaProvider: ${key} must be a number or boolean, got ${typeof value}`);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<SafeAreaConfigContext.Provider value={mergedConfig}>
|
|
50
|
+
<NativeSafeAreaProvider {...nativeProps}>
|
|
51
|
+
{children}
|
|
52
|
+
</NativeSafeAreaProvider>
|
|
53
|
+
</SafeAreaConfigContext.Provider>
|
|
54
|
+
);
|
|
55
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constants for safe area calculations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const SAFE_AREA_DEFAULTS = {
|
|
6
|
+
MIN_HEADER_PADDING: 0,
|
|
7
|
+
MIN_CONTENT_PADDING: 0,
|
|
8
|
+
MIN_STATUS_BAR_PADDING: 0,
|
|
9
|
+
ADDITIONAL_PADDING: 0,
|
|
10
|
+
} as const;
|
|
11
|
+
|
|
12
|
+
export const PLATFORM_BEHAVIORS = {
|
|
13
|
+
IOS_STATUS_BAR_USES_SAFE_AREA: true,
|
|
14
|
+
} as const;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Default configuration that can be overridden by applications
|
|
18
|
+
*/
|
|
19
|
+
export const DEFAULT_CONFIG = {
|
|
20
|
+
minHeaderPadding: SAFE_AREA_DEFAULTS.MIN_HEADER_PADDING,
|
|
21
|
+
minContentPadding: SAFE_AREA_DEFAULTS.MIN_CONTENT_PADDING,
|
|
22
|
+
minStatusBarPadding: SAFE_AREA_DEFAULTS.MIN_STATUS_BAR_PADDING,
|
|
23
|
+
additionalPadding: SAFE_AREA_DEFAULTS.ADDITIONAL_PADDING,
|
|
24
|
+
iosStatusBarUsesSafeArea: PLATFORM_BEHAVIORS.IOS_STATUS_BAR_USES_SAFE_AREA,
|
|
25
|
+
} as const;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useContentSafeAreaPadding Hook
|
|
3
|
+
* Calculate safe area padding for content components
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useMemo } from 'react';
|
|
7
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
8
|
+
import { useSafeAreaConfig } from '../components/SafeAreaProvider';
|
|
9
|
+
import { useStableOptions } from '../utils/optimization';
|
|
10
|
+
import { validateNumericInput } from '../utils/validation';
|
|
11
|
+
|
|
12
|
+
export interface ContentPaddingOptions {
|
|
13
|
+
minBottomPadding?: number;
|
|
14
|
+
additionalPadding?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ContentPaddingResult {
|
|
18
|
+
paddingBottom: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const useContentSafeAreaPadding = (
|
|
22
|
+
options: ContentPaddingOptions = {},
|
|
23
|
+
): ContentPaddingResult => {
|
|
24
|
+
const insets = useSafeAreaInsets();
|
|
25
|
+
const config = useSafeAreaConfig();
|
|
26
|
+
const stableOptions = useStableOptions(options);
|
|
27
|
+
const minBottomPadding = stableOptions.minBottomPadding ?? config.minContentPadding;
|
|
28
|
+
const additionalPadding = stableOptions.additionalPadding ?? config.additionalPadding;
|
|
29
|
+
|
|
30
|
+
// Validate inputs once
|
|
31
|
+
useMemo(() => {
|
|
32
|
+
validateNumericInput(minBottomPadding, 'useContentSafeAreaPadding.minBottomPadding');
|
|
33
|
+
validateNumericInput(additionalPadding, 'useContentSafeAreaPadding.additionalPadding');
|
|
34
|
+
}, [minBottomPadding, additionalPadding]);
|
|
35
|
+
|
|
36
|
+
const paddingBottom = useMemo(() => {
|
|
37
|
+
return Math.max(insets.bottom, minBottomPadding) + additionalPadding;
|
|
38
|
+
}, [insets.bottom, minBottomPadding, additionalPadding]);
|
|
39
|
+
|
|
40
|
+
return useMemo(() => ({ paddingBottom }), [paddingBottom]);
|
|
41
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useHeaderSafeAreaPadding Hook
|
|
3
|
+
* Calculate safe area padding for header components
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useMemo } from 'react';
|
|
7
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
8
|
+
import { useSafeAreaConfig } from '../components/SafeAreaProvider';
|
|
9
|
+
import { useStableOptions } from '../utils/optimization';
|
|
10
|
+
import { validateNumericInput } from '../utils/validation';
|
|
11
|
+
|
|
12
|
+
export interface HeaderPaddingOptions {
|
|
13
|
+
minPadding?: number;
|
|
14
|
+
additionalPadding?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const useHeaderSafeAreaPadding = (
|
|
18
|
+
options: HeaderPaddingOptions = {},
|
|
19
|
+
): number => {
|
|
20
|
+
const insets = useSafeAreaInsets();
|
|
21
|
+
const config = useSafeAreaConfig();
|
|
22
|
+
const stableOptions = useStableOptions(options);
|
|
23
|
+
const minPadding = stableOptions.minPadding ?? config.minHeaderPadding;
|
|
24
|
+
const additionalPadding = stableOptions.additionalPadding ?? config.additionalPadding;
|
|
25
|
+
|
|
26
|
+
// Validate inputs once
|
|
27
|
+
useMemo(() => {
|
|
28
|
+
validateNumericInput(minPadding, 'useHeaderSafeAreaPadding.minPadding');
|
|
29
|
+
validateNumericInput(additionalPadding, 'useHeaderSafeAreaPadding.additionalPadding');
|
|
30
|
+
}, [minPadding, additionalPadding]);
|
|
31
|
+
|
|
32
|
+
return useMemo(() => {
|
|
33
|
+
return Math.max(insets.top, minPadding) + additionalPadding;
|
|
34
|
+
}, [insets.top, minPadding, additionalPadding]);
|
|
35
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useStatusBarSafeAreaPadding Hook
|
|
3
|
+
* Calculate safe area padding for status bar components
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useMemo } from 'react';
|
|
7
|
+
import { Platform } from 'react-native';
|
|
8
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
9
|
+
import { useSafeAreaConfig } from '../components/SafeAreaProvider';
|
|
10
|
+
import { useStableOptions } from '../utils/optimization';
|
|
11
|
+
import { validateNumericInput } from '../utils/validation';
|
|
12
|
+
|
|
13
|
+
export interface StatusBarPaddingOptions {
|
|
14
|
+
minPadding?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const useStatusBarSafeAreaPadding = (
|
|
18
|
+
options: StatusBarPaddingOptions = {},
|
|
19
|
+
): number => {
|
|
20
|
+
const insets = useSafeAreaInsets();
|
|
21
|
+
const config = useSafeAreaConfig();
|
|
22
|
+
const stableOptions = useStableOptions(options);
|
|
23
|
+
const minPadding = stableOptions.minPadding ?? config.minStatusBarPadding;
|
|
24
|
+
|
|
25
|
+
// Validate input once
|
|
26
|
+
useMemo(() => {
|
|
27
|
+
validateNumericInput(minPadding, 'useStatusBarSafeAreaPadding.minPadding');
|
|
28
|
+
}, [minPadding]);
|
|
29
|
+
|
|
30
|
+
return useMemo(() => {
|
|
31
|
+
if (Platform.OS === 'ios' && config.iosStatusBarUsesSafeArea) {
|
|
32
|
+
return minPadding;
|
|
33
|
+
}
|
|
34
|
+
return Math.max(insets.top, minPadding);
|
|
35
|
+
}, [insets.top, minPadding, config.iosStatusBarUsesSafeArea]);
|
|
36
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safe Area Module
|
|
3
|
+
* Configurable safe area provider and hooks for React Native apps
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { SafeAreaProvider, useSafeAreaConfig } from './components/SafeAreaProvider';
|
|
7
|
+
export type { SafeAreaProviderProps, SafeAreaConfig } from './components/SafeAreaProvider';
|
|
8
|
+
|
|
9
|
+
export { useSafeAreaInsets } from './hooks/useSafeAreaInsets';
|
|
10
|
+
export { useStatusBarSafeAreaPadding } from './hooks/useStatusBarSafeAreaPadding';
|
|
11
|
+
export type { StatusBarPaddingOptions } from './hooks/useStatusBarSafeAreaPadding';
|
|
12
|
+
|
|
13
|
+
export { useHeaderSafeAreaPadding } from './hooks/useHeaderSafeAreaPadding';
|
|
14
|
+
export type { HeaderPaddingOptions } from './hooks/useHeaderSafeAreaPadding';
|
|
15
|
+
|
|
16
|
+
export { useContentSafeAreaPadding } from './hooks/useContentSafeAreaPadding';
|
|
17
|
+
export type { ContentPaddingOptions, ContentPaddingResult } from './hooks/useContentSafeAreaPadding';
|
|
18
|
+
|
|
19
|
+
export { SAFE_AREA_DEFAULTS, PLATFORM_BEHAVIORS, DEFAULT_CONFIG } from './constants';
|
|
20
|
+
export { useStableOptions, clearPerformanceCaches } from './utils/optimization';
|
|
21
|
+
export { validateNumericInput, throttledWarn, clearValidationCache } from './utils/validation';
|
|
22
|
+
|
|
23
|
+
// Re-export from react-native-safe-area-context for convenience
|
|
24
|
+
export { initialWindowMetrics } from 'react-native-safe-area-context';
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Performance optimization utilities for safe area hooks
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useMemo, useRef } from 'react';
|
|
6
|
+
import { clearValidationCache } from './validation';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Memoize options object to prevent unnecessary re-renders
|
|
10
|
+
* Uses deep comparison for better stability
|
|
11
|
+
*/
|
|
12
|
+
export const useStableOptions = <T extends Record<string, any>>(options: T): T => {
|
|
13
|
+
const prevOptionsRef = useRef<T | undefined>(undefined);
|
|
14
|
+
|
|
15
|
+
return useMemo(() => {
|
|
16
|
+
if (!prevOptionsRef.current) {
|
|
17
|
+
prevOptionsRef.current = options;
|
|
18
|
+
return options;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Check if keys match first
|
|
22
|
+
const prevKeys = Object.keys(prevOptionsRef.current);
|
|
23
|
+
const currentKeys = Object.keys(options);
|
|
24
|
+
|
|
25
|
+
if (prevKeys.length !== currentKeys.length) {
|
|
26
|
+
prevOptionsRef.current = options;
|
|
27
|
+
return options;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Check values
|
|
31
|
+
const hasChanged = prevKeys.some(key => prevOptionsRef.current![key] !== options[key]);
|
|
32
|
+
|
|
33
|
+
if (hasChanged) {
|
|
34
|
+
prevOptionsRef.current = options;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return prevOptionsRef.current;
|
|
38
|
+
}, [options]);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Cleanup function to clear all performance caches
|
|
43
|
+
* Call this in useEffect cleanup if needed
|
|
44
|
+
*/
|
|
45
|
+
export const clearPerformanceCaches = (): void => {
|
|
46
|
+
clearValidationCache();
|
|
47
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Performance utilities for safe area hooks
|
|
3
|
+
* @deprecated This file is deprecated. Use individual utility modules instead.
|
|
4
|
+
* - useStableOptions and clearPerformanceCaches are now in './optimization'
|
|
5
|
+
* - validateNumericInput and throttledWarn are now in './validation'
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Re-export for backward compatibility
|
|
9
|
+
export { useStableOptions, clearPerformanceCaches } from './optimization';
|
|
10
|
+
export { validateNumericInput, throttledWarn, clearValidationCache } from './validation';
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation utilities for safe area hooks
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Validate numeric input with performance optimization
|
|
6
|
+
// Caches validation results to avoid repeated checks
|
|
7
|
+
const validationCache = new Map<string, boolean>();
|
|
8
|
+
|
|
9
|
+
export const validateNumericInput = (
|
|
10
|
+
value: number,
|
|
11
|
+
name: string,
|
|
12
|
+
allowNegative = false,
|
|
13
|
+
): boolean => {
|
|
14
|
+
if (!__DEV__) {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const cacheKey = `${name}:${value}:${allowNegative}`;
|
|
19
|
+
|
|
20
|
+
if (validationCache.has(cacheKey)) {
|
|
21
|
+
return validationCache.get(cacheKey)!;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const isValid = typeof value === 'number' && !isNaN(value) && (allowNegative || value >= 0);
|
|
25
|
+
|
|
26
|
+
if (!isValid) {
|
|
27
|
+
throttledWarn(`${name}: must be a ${allowNegative ? 'number' : 'non-negative number'}, got ${value}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Limit cache size to prevent memory leaks
|
|
31
|
+
if (validationCache.size > 100) {
|
|
32
|
+
const firstKey = validationCache.keys().next().value;
|
|
33
|
+
if (firstKey) {
|
|
34
|
+
validationCache.delete(firstKey);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
validationCache.set(cacheKey, isValid);
|
|
39
|
+
return isValid;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Throttled console warning to prevent spam
|
|
43
|
+
// Uses requestAnimationFrame for better performance
|
|
44
|
+
const warningTimes = new Map<string, number>();
|
|
45
|
+
const WARNING_THROTTLE = 1000; // 1 second
|
|
46
|
+
|
|
47
|
+
export const throttledWarn = (message: string): void => {
|
|
48
|
+
if (!__DEV__) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const now = Date.now();
|
|
53
|
+
const lastTime = warningTimes.get(message) || 0;
|
|
54
|
+
|
|
55
|
+
if (now - lastTime > WARNING_THROTTLE) {
|
|
56
|
+
console.warn(message);
|
|
57
|
+
warningTimes.set(message, now);
|
|
58
|
+
|
|
59
|
+
// Clean up old entries to prevent memory leaks
|
|
60
|
+
if (warningTimes.size > 50) {
|
|
61
|
+
const cutoffTime = now - WARNING_THROTTLE * 10;
|
|
62
|
+
for (const [key, time] of warningTimes.entries()) {
|
|
63
|
+
if (time < cutoffTime) {
|
|
64
|
+
warningTimes.delete(key);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Cleanup function to clear validation cache
|
|
72
|
+
export const clearValidationCache = (): void => {
|
|
73
|
+
validationCache.clear();
|
|
74
|
+
};
|