@oneplatformdev/react-native-uikit 1.1.1 → 1.2.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/README.md +33 -1
- package/package.json +1 -1
- package/registry/manifest.json +56 -0
- package/src/components/Avatar/index.ts +2 -0
- package/src/components/Avatar/presenters/useAvatar.ts +33 -0
- package/src/components/Avatar/types/types.ts +22 -0
- package/src/components/Avatar/ui/Avatar.tsx +84 -0
- package/src/components/Avatar/ui/styles.ts +72 -0
- package/src/components/Button/index.ts +2 -8
- package/src/components/Checkbox/index.ts +2 -0
- package/src/components/Checkbox/types/types.ts +24 -0
- package/src/components/Checkbox/ui/Checkbox.tsx +72 -0
- package/src/components/Checkbox/ui/styles.ts +67 -0
- package/src/components/EmptyListView/index.ts +2 -0
- package/src/components/EmptyListView/types/types.ts +21 -0
- package/src/components/EmptyListView/ui/EmptyListView.tsx +58 -0
- package/src/components/EmptyListView/ui/styles.ts +32 -0
- package/src/components/PhotoPreview/index.ts +2 -0
- package/src/components/PhotoPreview/types/types.ts +22 -0
- package/src/components/PhotoPreview/ui/PhotoPreview.tsx +83 -0
- package/src/components/PhotoPreview/ui/styles.ts +68 -0
- package/src/components/Switch/index.ts +2 -0
- package/src/components/Switch/presenters/useSwitch.ts +21 -0
- package/src/components/Switch/types/types.ts +20 -0
- package/src/components/Switch/ui/Switch.tsx +60 -0
- package/src/components/Switch/ui/styles.ts +70 -0
- package/src/index.ts +5 -0
package/README.md
CHANGED
|
@@ -9,12 +9,17 @@ Reusable React Native theme, scaling, and UI components.
|
|
|
9
9
|
Install with `npm install @oneplatformdev/react-native-uikit` and import ready-made components:
|
|
10
10
|
|
|
11
11
|
```tsx
|
|
12
|
-
import { Button, Input, Typography, UIProvider } from '@oneplatformdev/react-native-uikit';
|
|
12
|
+
import { Avatar, Button, Checkbox, EmptyListView, Input, PhotoPreview, Switch, Typography, UIProvider } from '@oneplatformdev/react-native-uikit';
|
|
13
13
|
|
|
14
14
|
<UIProvider>
|
|
15
15
|
<Typography text="Hello" />
|
|
16
16
|
<Input label="Name" value={name} onChangeText={setName} />
|
|
17
17
|
<Button text="Continue" onPress={onContinue} />
|
|
18
|
+
<Switch value={notificationsEnabled} onValueChange={setNotificationsEnabled} accessibilityLabel="Notifications" />
|
|
19
|
+
<Checkbox checked={accepted} onValueChange={setAccepted} accessibilityLabel="Accept terms" />
|
|
20
|
+
<PhotoPreview source={{ uri: photoUri }} onPress={onOpenPhoto} accessibilityLabel="Open photo" />
|
|
21
|
+
<EmptyListView title="No items" description="Try again later" />
|
|
22
|
+
<Avatar source={{ uri: avatarUri }} name="Mykhailo Novikov" />
|
|
18
23
|
</UIProvider>
|
|
19
24
|
```
|
|
20
25
|
|
|
@@ -26,15 +31,20 @@ Run from the consuming project root after declaring `@oneplatformdev/react-nativ
|
|
|
26
31
|
|
|
27
32
|
```sh
|
|
28
33
|
npx @oneplatformdev/react-native-uikit add typography
|
|
34
|
+
npx @oneplatformdev/react-native-uikit add avatar
|
|
29
35
|
npx @oneplatformdev/react-native-uikit add button
|
|
36
|
+
npx @oneplatformdev/react-native-uikit add checkbox
|
|
30
37
|
npx @oneplatformdev/react-native-uikit add input
|
|
31
38
|
npx @oneplatformdev/react-native-uikit add screen-container
|
|
39
|
+
npx @oneplatformdev/react-native-uikit add switch
|
|
32
40
|
npx @oneplatformdev/react-native-uikit add custom-alert
|
|
41
|
+
npx @oneplatformdev/react-native-uikit add empty-list-view
|
|
33
42
|
npx @oneplatformdev/react-native-uikit add bottom-modal
|
|
34
43
|
npx @oneplatformdev/react-native-uikit add connection-container
|
|
35
44
|
npx @oneplatformdev/react-native-uikit add header-with-back-button
|
|
36
45
|
npx @oneplatformdev/react-native-uikit add loader
|
|
37
46
|
npx @oneplatformdev/react-native-uikit add logger
|
|
47
|
+
npx @oneplatformdev/react-native-uikit add photo-preview
|
|
38
48
|
npx @oneplatformdev/react-native-uikit add button --dir src/UIKit
|
|
39
49
|
```
|
|
40
50
|
|
|
@@ -295,6 +305,28 @@ Input derives its native accessibility label from `label` only when the caller o
|
|
|
295
305
|
|
|
296
306
|
Owned/local source installation for Input: **available** with `npx @oneplatformdev/react-native-uikit add input`.
|
|
297
307
|
|
|
308
|
+
## Switch and Checkbox
|
|
309
|
+
|
|
310
|
+
Both are controlled: pass `value`/`onValueChange` to `Switch` or `checked`/`onValueChange` to `Checkbox`. They use theme colors by default, accept accessibility labels and hints, and keep a usable press target even with small visual dimensions.
|
|
311
|
+
|
|
312
|
+
`Switch` defaults to a 40×24 track with a 20-unit thumb. Override `width`, `height`, `thumbSize`, track/thumb colors, or provide a custom `thumb` node. The consumer owns the custom node's appearance. The thumb animates when `value` changes.
|
|
313
|
+
|
|
314
|
+
`Checkbox` defaults to a 22-unit slightly rounded square with a built-in checkmark. Override `size`, state colors, `borderWidth`, and `iconSize`/`iconColor`. Use `shape="circle"` for a radius derived from its size, or `shape="square"` with `borderRadius` (including `0`) for custom corners. Supply an `icon` node to replace the built-in indicator; the consumer owns that node's size and color.
|
|
315
|
+
|
|
316
|
+
Owned source is available through `npx @oneplatformdev/react-native-uikit add switch` and `npx @oneplatformdev/react-native-uikit add checkbox`. Both are standalone components; Switch uses the existing `react-native-reanimated` peer.
|
|
317
|
+
|
|
318
|
+
## PhotoPreview and EmptyListView
|
|
319
|
+
|
|
320
|
+
`PhotoPreview` displays a supplied React Native image source. It defaults to a 120-unit square with the theme's medium radius. Set `width` and/or `height` for a different size; `aspectRatio` derives the missing dimension (width wins if both dimensions and a ratio are supplied). `style` is applied after the base container geometry, and `imageStyle` after the fill-image style. Supply `onPress` to make the image a button, `onRemovePress` for a separate remove control, or `overlay` for caller-owned content. A built-in × is used unless `removeIcon` is provided; supply localized accessibility labels for actions. The component does not open a modal or own image state.
|
|
321
|
+
|
|
322
|
+
`EmptyListView` centers optional `visual`, `title`, and `description` in available list space. Strings and numbers use themed Typography (`title` and `body` by default); custom React nodes render as supplied. `titleStyle` and `descriptionStyle` apply to built-in text, while custom nodes own their styling. A supplied `action` replaces the built-in Button; otherwise the Button appears only when both `actionLabel` and `onActionPress` are given. The optional `accessibilityLabel` labels the visual without grouping the readable text or action.
|
|
323
|
+
|
|
324
|
+
Owned source is available with `npx @oneplatformdev/react-native-uikit add photo-preview` and `npx @oneplatformdev/react-native-uikit add empty-list-view`. The latter also copies local Typography and Button dependencies.
|
|
325
|
+
|
|
326
|
+
## Avatar
|
|
327
|
+
|
|
328
|
+
`Avatar` accepts a React Native image `source` and defaults to a 40-unit circle. Override `size`, use `shape="square"` with `borderRadius` (including `0`), or customize `backgroundColor` and `textColor`. Fallback order is image → custom `fallback` → `fallbackText` → up to two initials from `name` → a dependency-free silhouette. If an image fails, the fallback appears; changing `source` allows the new image to load. `badge` is positioned at the edge without being clipped. Pass `onPress` for a labeled button; otherwise the avatar exposes image semantics. For a button, `accessibilityLabel` names the action and `imageAccessibilityLabel` is ignored; without `onPress`, `imageAccessibilityLabel` overrides the image label. `size` and `shape` remain authoritative over geometry in `style`/`imageStyle`. Provide localized accessibility text where needed. Owned source: `npx @oneplatformdev/react-native-uikit add avatar` (also installs local Typography).
|
|
329
|
+
|
|
298
330
|
## ScreenContainer, CustomAlert, and BottomModal
|
|
299
331
|
|
|
300
332
|
`ScreenContainer` provides a theme-backed safe-area layout, optional scrolling, and optional keyboard-aware scrolling or avoidance. Pass `gradient={{ colors, locations, start, end }}` to render a caller-configured background across the entire screen, behind the safe-area content; omit it for the solid theme background. Gradient coordinates are normalized and are not scaled. Pass ordinary ScrollView props when scrolling is enabled; use `containerStyle` and `contentContainerStyle` for layout overrides.
|
package/package.json
CHANGED
package/registry/manifest.json
CHANGED
|
@@ -11,10 +11,22 @@
|
|
|
11
11
|
"../../../utils": "@oneplatformdev/react-native-uikit"
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
|
+
"avatar": {
|
|
15
|
+
"folder": "Avatar",
|
|
16
|
+
"files": ["ui/Avatar.tsx", "presenters/useAvatar.ts", "ui/styles.ts", "types/types.ts", "index.ts"],
|
|
17
|
+
"componentDependencies": ["typography"],
|
|
18
|
+
"foundations": ["useUITheme", "useScaling", "Typography", "UIColors", "UIRadius", "ScalingFunctions"],
|
|
19
|
+
"dependencies": ["react", "react-native", "@oneplatformdev/react-native-uikit"],
|
|
20
|
+
"rewrites": {
|
|
21
|
+
"../../../theme": "@oneplatformdev/react-native-uikit",
|
|
22
|
+
"../../../utils": "@oneplatformdev/react-native-uikit"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
14
25
|
"button": {
|
|
15
26
|
"folder": "Button",
|
|
16
27
|
"files": ["ui/Button.tsx", "presenters/useButton.ts", "ui/styles.ts", "types/types.ts", "index.ts"],
|
|
17
28
|
"componentDependencies": ["typography"],
|
|
29
|
+
"publicExports": { "Button": "ui/Button.tsx", "ButtonProps": "types/types.ts" },
|
|
18
30
|
"foundations": ["useUITheme", "useScaling", "Typography", "TypographyProps", "UIColors", "UIButtonSizeTheme", "UIButtonVariantTheme", "ScalingFunctions"],
|
|
19
31
|
"dependencies": ["react", "react-native", "@oneplatformdev/react-native-uikit"],
|
|
20
32
|
"rewrites": {
|
|
@@ -22,6 +34,50 @@
|
|
|
22
34
|
"../../../utils": "@oneplatformdev/react-native-uikit"
|
|
23
35
|
}
|
|
24
36
|
},
|
|
37
|
+
"checkbox": {
|
|
38
|
+
"folder": "Checkbox",
|
|
39
|
+
"files": ["ui/Checkbox.tsx", "ui/styles.ts", "types/types.ts", "index.ts"],
|
|
40
|
+
"componentDependencies": [],
|
|
41
|
+
"foundations": ["useUITheme", "useScaling", "UIColors", "UIRadius", "ScalingFunctions"],
|
|
42
|
+
"dependencies": ["react", "react-native", "@oneplatformdev/react-native-uikit"],
|
|
43
|
+
"rewrites": {
|
|
44
|
+
"../../../theme": "@oneplatformdev/react-native-uikit",
|
|
45
|
+
"../../../utils": "@oneplatformdev/react-native-uikit"
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"switch": {
|
|
49
|
+
"folder": "Switch",
|
|
50
|
+
"files": ["ui/Switch.tsx", "presenters/useSwitch.ts", "ui/styles.ts", "types/types.ts", "index.ts"],
|
|
51
|
+
"componentDependencies": [],
|
|
52
|
+
"foundations": ["useUITheme", "useScaling", "ScalingFunctions"],
|
|
53
|
+
"dependencies": ["react", "react-native", "react-native-reanimated", "@oneplatformdev/react-native-uikit"],
|
|
54
|
+
"rewrites": {
|
|
55
|
+
"../../../theme": "@oneplatformdev/react-native-uikit",
|
|
56
|
+
"../../../utils": "@oneplatformdev/react-native-uikit"
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"photo-preview": {
|
|
60
|
+
"folder": "PhotoPreview",
|
|
61
|
+
"files": ["ui/PhotoPreview.tsx", "ui/styles.ts", "types/types.ts", "index.ts"],
|
|
62
|
+
"componentDependencies": [],
|
|
63
|
+
"foundations": ["useUITheme", "useScaling", "UIColors", "UIRadius", "UISpacing", "ScalingFunctions"],
|
|
64
|
+
"dependencies": ["react", "react-native", "@oneplatformdev/react-native-uikit"],
|
|
65
|
+
"rewrites": {
|
|
66
|
+
"../../../theme": "@oneplatformdev/react-native-uikit",
|
|
67
|
+
"../../../utils": "@oneplatformdev/react-native-uikit"
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"empty-list-view": {
|
|
71
|
+
"folder": "EmptyListView",
|
|
72
|
+
"files": ["ui/EmptyListView.tsx", "ui/styles.ts", "types/types.ts", "index.ts"],
|
|
73
|
+
"componentDependencies": ["typography", "button"],
|
|
74
|
+
"foundations": ["useUITheme", "useScaling", "Typography", "Button", "TypographyProps", "UISpacing", "ScalingFunctions"],
|
|
75
|
+
"dependencies": ["react", "react-native", "@oneplatformdev/react-native-uikit"],
|
|
76
|
+
"rewrites": {
|
|
77
|
+
"../../../theme": "@oneplatformdev/react-native-uikit",
|
|
78
|
+
"../../../utils": "@oneplatformdev/react-native-uikit"
|
|
79
|
+
}
|
|
80
|
+
},
|
|
25
81
|
"input": {
|
|
26
82
|
"folder": "Input",
|
|
27
83
|
"files": ["ui/Input.tsx", "presenters/useInput.ts", "ui/styles.ts", "types/types.ts", "index.ts"],
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
import type { ImageSourcePropType } from 'react-native';
|
|
3
|
+
|
|
4
|
+
const getSourceKey = (source?: ImageSourcePropType): string | null => {
|
|
5
|
+
if (typeof source === 'number') return Number.isFinite(source) ? `asset:${source}` : null;
|
|
6
|
+
if (Array.isArray(source)) {
|
|
7
|
+
return source.some(item => typeof item.uri === 'string' && item.uri.trim()) ? JSON.stringify(source) : null;
|
|
8
|
+
}
|
|
9
|
+
return source && 'uri' in source && typeof source.uri === 'string' && source.uri.trim()
|
|
10
|
+
? JSON.stringify(source) : null;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const getInitials = (name?: string): string => name?.trim().split(/\s+/).slice(0, 2)
|
|
14
|
+
.map(word => Array.from(word)[0]?.toUpperCase() ?? '').join('') ?? '';
|
|
15
|
+
|
|
16
|
+
export const useAvatar = (source?: ImageSourcePropType, name?: string) => {
|
|
17
|
+
const sourceKey = getSourceKey(source);
|
|
18
|
+
const [failedSourceKey, setFailedSourceKey] = useState<string | null>(null);
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
setFailedSourceKey(null);
|
|
22
|
+
}, [sourceKey]);
|
|
23
|
+
|
|
24
|
+
const onImageError = useCallback(() => {
|
|
25
|
+
setFailedSourceKey(sourceKey);
|
|
26
|
+
}, [sourceKey]);
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
showImage: sourceKey !== null && failedSourceKey !== sourceKey,
|
|
30
|
+
onImageError,
|
|
31
|
+
initials: getInitials(name),
|
|
32
|
+
};
|
|
33
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { ImageSourcePropType, ImageStyle, StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
export interface AvatarProps {
|
|
5
|
+
source?: ImageSourcePropType;
|
|
6
|
+
name?: string;
|
|
7
|
+
fallbackText?: string;
|
|
8
|
+
fallback?: ReactNode;
|
|
9
|
+
size?: number;
|
|
10
|
+
shape?: 'circle' | 'square';
|
|
11
|
+
borderRadius?: number;
|
|
12
|
+
backgroundColor?: string;
|
|
13
|
+
textColor?: string;
|
|
14
|
+
imageStyle?: StyleProp<ImageStyle>;
|
|
15
|
+
style?: StyleProp<ViewStyle>;
|
|
16
|
+
badge?: ReactNode;
|
|
17
|
+
onPress?: () => void;
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
accessibilityLabel?: string;
|
|
20
|
+
accessibilityHint?: string;
|
|
21
|
+
imageAccessibilityLabel?: string;
|
|
22
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { Image, Pressable, View } from 'react-native';
|
|
3
|
+
import { Typography } from '../../Typography';
|
|
4
|
+
import { useUITheme } from '../../../theme';
|
|
5
|
+
import { useScaling } from '../../../utils';
|
|
6
|
+
import { useAvatar } from '../presenters/useAvatar';
|
|
7
|
+
import { getStyles } from './styles';
|
|
8
|
+
import type { AvatarProps } from '../types/types';
|
|
9
|
+
|
|
10
|
+
export const Avatar = ({
|
|
11
|
+
source,
|
|
12
|
+
name,
|
|
13
|
+
fallbackText,
|
|
14
|
+
fallback,
|
|
15
|
+
size = 40,
|
|
16
|
+
shape = 'circle',
|
|
17
|
+
borderRadius,
|
|
18
|
+
backgroundColor,
|
|
19
|
+
textColor,
|
|
20
|
+
imageStyle,
|
|
21
|
+
style,
|
|
22
|
+
badge,
|
|
23
|
+
onPress,
|
|
24
|
+
disabled = false,
|
|
25
|
+
accessibilityLabel,
|
|
26
|
+
accessibilityHint,
|
|
27
|
+
imageAccessibilityLabel,
|
|
28
|
+
}: AvatarProps) => {
|
|
29
|
+
const { colors, radius } = useUITheme();
|
|
30
|
+
const scaling = useScaling();
|
|
31
|
+
const { styles, hitSlop, textSize, textLineHeight } = useMemo(
|
|
32
|
+
() => getStyles(colors, radius, scaling, size, shape, borderRadius, backgroundColor, textColor),
|
|
33
|
+
[colors, radius, scaling, size, shape, borderRadius, backgroundColor, textColor],
|
|
34
|
+
);
|
|
35
|
+
const { showImage, onImageError, initials } = useAvatar(source, name);
|
|
36
|
+
const text = fallbackText?.trim() || initials;
|
|
37
|
+
const fallbackLabel = name?.trim() || fallbackText?.trim() || 'Avatar';
|
|
38
|
+
const label = onPress
|
|
39
|
+
? accessibilityLabel ?? fallbackLabel
|
|
40
|
+
: imageAccessibilityLabel ?? accessibilityLabel ?? fallbackLabel;
|
|
41
|
+
const content = showImage ? (
|
|
42
|
+
<Image source={source} onError={onImageError} accessible={false} style={[imageStyle, styles.image]} />
|
|
43
|
+
) : fallback != null && typeof fallback !== 'boolean' ? (
|
|
44
|
+
typeof fallback === 'string' || typeof fallback === 'number'
|
|
45
|
+
? <Typography text={fallback} size={textSize} lineHeight={textLineHeight} style={styles.text} accessible={false} />
|
|
46
|
+
: fallback
|
|
47
|
+
) : text ? (
|
|
48
|
+
<Typography text={text} size={textSize} lineHeight={textLineHeight} style={styles.text} accessible={false} numberOfLines={1} />
|
|
49
|
+
) : (
|
|
50
|
+
<View style={styles.generic} accessible={false}>
|
|
51
|
+
<View style={styles.genericHead} />
|
|
52
|
+
<View style={styles.genericShoulders} />
|
|
53
|
+
</View>
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<View style={[style, styles.wrapper]}>
|
|
58
|
+
{onPress ? (
|
|
59
|
+
<Pressable
|
|
60
|
+
accessibilityRole="button"
|
|
61
|
+
accessibilityLabel={label}
|
|
62
|
+
accessibilityHint={accessibilityHint}
|
|
63
|
+
accessibilityState={{ disabled }}
|
|
64
|
+
disabled={disabled}
|
|
65
|
+
hitSlop={hitSlop}
|
|
66
|
+
onPress={onPress}
|
|
67
|
+
style={styles.surface}
|
|
68
|
+
>
|
|
69
|
+
{content}
|
|
70
|
+
</Pressable>
|
|
71
|
+
) : (
|
|
72
|
+
<View accessible accessibilityRole="image" accessibilityLabel={label} style={styles.surface}>
|
|
73
|
+
{content}
|
|
74
|
+
</View>
|
|
75
|
+
)}
|
|
76
|
+
{badge != null && typeof badge !== 'boolean' ? (
|
|
77
|
+
<View pointerEvents="box-none" style={styles.badge}>
|
|
78
|
+
{typeof badge === 'string' || typeof badge === 'number'
|
|
79
|
+
? <Typography text={badge} variant="caption" /> : badge}
|
|
80
|
+
</View>
|
|
81
|
+
) : null}
|
|
82
|
+
</View>
|
|
83
|
+
);
|
|
84
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { StyleSheet } from 'react-native';
|
|
2
|
+
import type { UIColors, UIRadius } from '../../../theme';
|
|
3
|
+
import type { ScalingFunctions } from '../../../utils';
|
|
4
|
+
import type { AvatarProps } from '../types/types';
|
|
5
|
+
|
|
6
|
+
export const getStyles = (
|
|
7
|
+
colors: UIColors,
|
|
8
|
+
radius: UIRadius,
|
|
9
|
+
scaling: ScalingFunctions,
|
|
10
|
+
size: number,
|
|
11
|
+
shape: AvatarProps['shape'],
|
|
12
|
+
borderRadius: AvatarProps['borderRadius'],
|
|
13
|
+
backgroundColor: AvatarProps['backgroundColor'],
|
|
14
|
+
textColor: AvatarProps['textColor'],
|
|
15
|
+
) => {
|
|
16
|
+
const designSize = Number.isFinite(size) && size > 0 ? size : 40;
|
|
17
|
+
const scaledSize = scaling.scaleHorizontal(designSize);
|
|
18
|
+
const designRadius = borderRadius !== undefined && Number.isFinite(borderRadius)
|
|
19
|
+
? Math.max(0, borderRadius) : radius.sm;
|
|
20
|
+
const foreground = textColor ?? colors.textMuted;
|
|
21
|
+
|
|
22
|
+
const styles = StyleSheet.create({
|
|
23
|
+
wrapper: {
|
|
24
|
+
width: scaledSize,
|
|
25
|
+
height: scaledSize,
|
|
26
|
+
overflow: 'visible',
|
|
27
|
+
},
|
|
28
|
+
surface: {
|
|
29
|
+
width: scaledSize,
|
|
30
|
+
height: scaledSize,
|
|
31
|
+
borderRadius: shape === 'square' ? scaling.scaleHorizontal(designRadius) : scaledSize / 2,
|
|
32
|
+
overflow: 'hidden',
|
|
33
|
+
backgroundColor: backgroundColor ?? colors.surface,
|
|
34
|
+
alignItems: 'center',
|
|
35
|
+
justifyContent: 'center',
|
|
36
|
+
},
|
|
37
|
+
image: {
|
|
38
|
+
...StyleSheet.absoluteFill,
|
|
39
|
+
},
|
|
40
|
+
text: {
|
|
41
|
+
color: foreground,
|
|
42
|
+
textAlign: 'center',
|
|
43
|
+
maxWidth: '100%',
|
|
44
|
+
},
|
|
45
|
+
generic: {
|
|
46
|
+
alignItems: 'center',
|
|
47
|
+
justifyContent: 'center',
|
|
48
|
+
},
|
|
49
|
+
genericHead: {
|
|
50
|
+
width: scaledSize * 0.23,
|
|
51
|
+
height: scaledSize * 0.23,
|
|
52
|
+
borderRadius: scaledSize * 0.115,
|
|
53
|
+
backgroundColor: foreground,
|
|
54
|
+
},
|
|
55
|
+
genericShoulders: {
|
|
56
|
+
width: scaledSize * 0.48,
|
|
57
|
+
height: scaledSize * 0.22,
|
|
58
|
+
marginTop: scaledSize * 0.04,
|
|
59
|
+
borderTopLeftRadius: scaledSize * 0.24,
|
|
60
|
+
borderTopRightRadius: scaledSize * 0.24,
|
|
61
|
+
backgroundColor: foreground,
|
|
62
|
+
},
|
|
63
|
+
badge: {
|
|
64
|
+
position: 'absolute',
|
|
65
|
+
right: -scaledSize * 0.08,
|
|
66
|
+
bottom: -scaledSize * 0.08,
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
const hitSlop = scaling.scaleHorizontal(Math.max(8, (44 - designSize) / 2));
|
|
70
|
+
|
|
71
|
+
return { styles, hitSlop, textSize: designSize * 0.34, textLineHeight: designSize * 0.42 };
|
|
72
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
export interface CheckboxProps {
|
|
5
|
+
checked: boolean;
|
|
6
|
+
onValueChange?: (checked: boolean) => void;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
size?: number;
|
|
9
|
+
shape?: 'square' | 'circle';
|
|
10
|
+
borderRadius?: number;
|
|
11
|
+
borderWidth?: number;
|
|
12
|
+
checkedBackgroundColor?: string;
|
|
13
|
+
uncheckedBackgroundColor?: string;
|
|
14
|
+
checkedBorderColor?: string;
|
|
15
|
+
uncheckedBorderColor?: string;
|
|
16
|
+
disabledBackgroundColor?: string;
|
|
17
|
+
disabledBorderColor?: string;
|
|
18
|
+
icon?: ReactNode;
|
|
19
|
+
iconSize?: number;
|
|
20
|
+
iconColor?: string;
|
|
21
|
+
accessibilityLabel?: string;
|
|
22
|
+
accessibilityHint?: string;
|
|
23
|
+
style?: StyleProp<ViewStyle>;
|
|
24
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { useCallback, useMemo } from 'react';
|
|
2
|
+
import { Pressable, Text, View } from 'react-native';
|
|
3
|
+
import { useUITheme } from '../../../theme';
|
|
4
|
+
import { useScaling } from '../../../utils';
|
|
5
|
+
import { getStyles } from './styles';
|
|
6
|
+
import type { CheckboxProps } from '../types/types';
|
|
7
|
+
|
|
8
|
+
export const Checkbox = ({
|
|
9
|
+
checked,
|
|
10
|
+
onValueChange,
|
|
11
|
+
disabled = false,
|
|
12
|
+
size = 22,
|
|
13
|
+
shape = 'square',
|
|
14
|
+
borderRadius,
|
|
15
|
+
borderWidth,
|
|
16
|
+
checkedBackgroundColor,
|
|
17
|
+
uncheckedBackgroundColor,
|
|
18
|
+
checkedBorderColor,
|
|
19
|
+
uncheckedBorderColor,
|
|
20
|
+
disabledBackgroundColor,
|
|
21
|
+
disabledBorderColor,
|
|
22
|
+
icon,
|
|
23
|
+
iconSize,
|
|
24
|
+
iconColor,
|
|
25
|
+
accessibilityLabel,
|
|
26
|
+
accessibilityHint,
|
|
27
|
+
style,
|
|
28
|
+
}: CheckboxProps) => {
|
|
29
|
+
const { colors, radius } = useUITheme();
|
|
30
|
+
const scaling = useScaling();
|
|
31
|
+
const { styles, hitSlop } = useMemo(() => getStyles(colors, radius, scaling, checked, disabled, {
|
|
32
|
+
size,
|
|
33
|
+
shape,
|
|
34
|
+
borderRadius,
|
|
35
|
+
borderWidth,
|
|
36
|
+
checkedBackgroundColor,
|
|
37
|
+
uncheckedBackgroundColor,
|
|
38
|
+
checkedBorderColor,
|
|
39
|
+
uncheckedBorderColor,
|
|
40
|
+
disabledBackgroundColor,
|
|
41
|
+
disabledBorderColor,
|
|
42
|
+
iconSize,
|
|
43
|
+
iconColor,
|
|
44
|
+
}), [
|
|
45
|
+
colors, radius, scaling, checked, disabled, size, shape, borderRadius, borderWidth,
|
|
46
|
+
checkedBackgroundColor, uncheckedBackgroundColor, checkedBorderColor, uncheckedBorderColor,
|
|
47
|
+
disabledBackgroundColor, disabledBorderColor, iconSize, iconColor,
|
|
48
|
+
]);
|
|
49
|
+
const onPress = useCallback(() => {
|
|
50
|
+
if (!disabled) onValueChange?.(!checked);
|
|
51
|
+
}, [checked, disabled, onValueChange]);
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Pressable
|
|
55
|
+
accessibilityRole="checkbox"
|
|
56
|
+
accessibilityLabel={accessibilityLabel}
|
|
57
|
+
accessibilityHint={accessibilityHint}
|
|
58
|
+
accessibilityState={{ checked, disabled }}
|
|
59
|
+
disabled={disabled}
|
|
60
|
+
hitSlop={hitSlop}
|
|
61
|
+
onPress={onPress}
|
|
62
|
+
style={[styles.container, style]}
|
|
63
|
+
>
|
|
64
|
+
{checked ? (
|
|
65
|
+
<View pointerEvents="none">
|
|
66
|
+
{icon === undefined ? <View style={styles.indicator} />
|
|
67
|
+
: typeof icon === 'string' || typeof icon === 'number' ? <Text>{icon}</Text> : icon}
|
|
68
|
+
</View>
|
|
69
|
+
) : null}
|
|
70
|
+
</Pressable>
|
|
71
|
+
);
|
|
72
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { StyleSheet } from 'react-native';
|
|
2
|
+
import type { UIColors, UIRadius } from '../../../theme';
|
|
3
|
+
import type { ScalingFunctions } from '../../../utils';
|
|
4
|
+
import type { CheckboxProps } from '../types/types';
|
|
5
|
+
|
|
6
|
+
type CheckboxVisualProps = Pick<CheckboxProps,
|
|
7
|
+
| 'size' | 'shape' | 'borderRadius' | 'borderWidth' | 'checkedBackgroundColor' | 'uncheckedBackgroundColor'
|
|
8
|
+
| 'checkedBorderColor' | 'uncheckedBorderColor' | 'disabledBackgroundColor' | 'disabledBorderColor'
|
|
9
|
+
| 'iconSize' | 'iconColor'>;
|
|
10
|
+
|
|
11
|
+
const positiveOr = (value: number | undefined, fallback: number) =>
|
|
12
|
+
value !== undefined && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
13
|
+
|
|
14
|
+
export const getStyles = (
|
|
15
|
+
colors: UIColors,
|
|
16
|
+
radius: UIRadius,
|
|
17
|
+
scaling: ScalingFunctions,
|
|
18
|
+
checked: boolean,
|
|
19
|
+
disabled: boolean,
|
|
20
|
+
props: CheckboxVisualProps,
|
|
21
|
+
) => {
|
|
22
|
+
const designSize = positiveOr(props.size, 22);
|
|
23
|
+
const designBorderWidth = Math.min(
|
|
24
|
+
props.borderWidth !== undefined && Number.isFinite(props.borderWidth) ? Math.max(0, props.borderWidth) : 2,
|
|
25
|
+
designSize / 4,
|
|
26
|
+
);
|
|
27
|
+
const designIconSize = Math.min(positiveOr(props.iconSize, 12), designSize - 2 * designBorderWidth);
|
|
28
|
+
const designRadius = props.borderRadius !== undefined && Number.isFinite(props.borderRadius)
|
|
29
|
+
? Math.max(0, props.borderRadius) : radius.sm;
|
|
30
|
+
const size = scaling.scaleHorizontal(designSize);
|
|
31
|
+
const borderWidth = scaling.scaleHorizontal(designBorderWidth);
|
|
32
|
+
const iconSize = scaling.scaleHorizontal(designIconSize);
|
|
33
|
+
const backgroundColor = disabled
|
|
34
|
+
? props.disabledBackgroundColor ?? colors.disabled
|
|
35
|
+
: checked ? props.checkedBackgroundColor ?? colors.primary : props.uncheckedBackgroundColor ?? colors.background;
|
|
36
|
+
const borderColor = disabled
|
|
37
|
+
? props.disabledBorderColor ?? colors.disabled
|
|
38
|
+
: checked ? props.checkedBorderColor ?? colors.primary : props.uncheckedBorderColor ?? colors.border;
|
|
39
|
+
const indicatorColor = props.iconColor ?? (disabled ? colors.onDisabled : colors.onPrimary);
|
|
40
|
+
|
|
41
|
+
const styles = StyleSheet.create({
|
|
42
|
+
container: {
|
|
43
|
+
width: size,
|
|
44
|
+
height: size,
|
|
45
|
+
borderWidth,
|
|
46
|
+
borderRadius: props.shape === 'circle'
|
|
47
|
+
? size / 2
|
|
48
|
+
: scaling.scaleHorizontal(designRadius),
|
|
49
|
+
backgroundColor,
|
|
50
|
+
borderColor,
|
|
51
|
+
alignItems: 'center',
|
|
52
|
+
justifyContent: 'center',
|
|
53
|
+
flexShrink: 0,
|
|
54
|
+
},
|
|
55
|
+
indicator: {
|
|
56
|
+
width: iconSize * 0.55,
|
|
57
|
+
height: iconSize * 0.8,
|
|
58
|
+
borderRightWidth: scaling.scaleHorizontal(Math.min(2, designIconSize / 4)),
|
|
59
|
+
borderBottomWidth: scaling.scaleHorizontal(Math.min(2, designIconSize / 4)),
|
|
60
|
+
borderColor: indicatorColor,
|
|
61
|
+
transform: [{ rotate: '45deg' }],
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
const hitSlop = scaling.scaleHorizontal(Math.max(11, (44 - designSize) / 2));
|
|
65
|
+
|
|
66
|
+
return { styles, hitSlop };
|
|
67
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { StyleProp, TextStyle, ViewStyle } from 'react-native';
|
|
3
|
+
import type { TypographyProps } from '../../Typography';
|
|
4
|
+
|
|
5
|
+
export interface EmptyListViewProps {
|
|
6
|
+
visual?: ReactNode;
|
|
7
|
+
title?: ReactNode;
|
|
8
|
+
description?: ReactNode;
|
|
9
|
+
titleVariant?: TypographyProps['variant'];
|
|
10
|
+
descriptionVariant?: TypographyProps['variant'];
|
|
11
|
+
actionLabel?: string;
|
|
12
|
+
onActionPress?: () => void;
|
|
13
|
+
action?: ReactNode;
|
|
14
|
+
/** Labels the optional visual without grouping the readable text and action. */
|
|
15
|
+
accessibilityLabel?: string;
|
|
16
|
+
style?: StyleProp<ViewStyle>;
|
|
17
|
+
contentStyle?: StyleProp<ViewStyle>;
|
|
18
|
+
visualStyle?: StyleProp<ViewStyle>;
|
|
19
|
+
titleStyle?: StyleProp<TextStyle>;
|
|
20
|
+
descriptionStyle?: StyleProp<TextStyle>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
import { Button } from '../../Button';
|
|
4
|
+
import { Typography } from '../../Typography';
|
|
5
|
+
import { useUITheme } from '../../../theme';
|
|
6
|
+
import { useScaling } from '../../../utils';
|
|
7
|
+
import { getStyles } from './styles';
|
|
8
|
+
import type { EmptyListViewProps } from '../types/types';
|
|
9
|
+
|
|
10
|
+
export const EmptyListView = ({
|
|
11
|
+
visual,
|
|
12
|
+
title,
|
|
13
|
+
description,
|
|
14
|
+
titleVariant = 'title',
|
|
15
|
+
descriptionVariant = 'body',
|
|
16
|
+
actionLabel,
|
|
17
|
+
onActionPress,
|
|
18
|
+
action,
|
|
19
|
+
accessibilityLabel,
|
|
20
|
+
style,
|
|
21
|
+
contentStyle,
|
|
22
|
+
visualStyle,
|
|
23
|
+
titleStyle,
|
|
24
|
+
descriptionStyle,
|
|
25
|
+
}: EmptyListViewProps) => {
|
|
26
|
+
const { spacing } = useUITheme();
|
|
27
|
+
const scaling = useScaling();
|
|
28
|
+
const styles = useMemo(() => getStyles(spacing, scaling), [spacing, scaling]);
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<View style={[styles.container, style]}>
|
|
32
|
+
<View style={[styles.content, contentStyle]}>
|
|
33
|
+
{visual != null && visual !== false ? (
|
|
34
|
+
<View
|
|
35
|
+
accessible={accessibilityLabel !== undefined}
|
|
36
|
+
accessibilityLabel={accessibilityLabel}
|
|
37
|
+
style={[styles.visual, visualStyle]}
|
|
38
|
+
>
|
|
39
|
+
{typeof visual === 'string' || typeof visual === 'number'
|
|
40
|
+
? <Typography text={visual} /> : visual}
|
|
41
|
+
</View>
|
|
42
|
+
) : null}
|
|
43
|
+
{title != null ? (
|
|
44
|
+
typeof title === 'string' || typeof title === 'number'
|
|
45
|
+
? <Typography text={title} variant={titleVariant} style={[styles.title, titleStyle]} />
|
|
46
|
+
: title
|
|
47
|
+
) : null}
|
|
48
|
+
{description != null ? (
|
|
49
|
+
typeof description === 'string' || typeof description === 'number'
|
|
50
|
+
? <Typography text={description} variant={descriptionVariant} color="textMuted" style={[styles.description, descriptionStyle]} />
|
|
51
|
+
: description
|
|
52
|
+
) : null}
|
|
53
|
+
{action != null && action !== false ? action : actionLabel && onActionPress
|
|
54
|
+
? <Button text={actionLabel} onPress={onActionPress} /> : null}
|
|
55
|
+
</View>
|
|
56
|
+
</View>
|
|
57
|
+
);
|
|
58
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { StyleSheet } from 'react-native';
|
|
2
|
+
import type { UISpacing } from '../../../theme';
|
|
3
|
+
import type { ScalingFunctions } from '../../../utils';
|
|
4
|
+
|
|
5
|
+
export const getStyles = (spacing: UISpacing, scaling: ScalingFunctions) => {
|
|
6
|
+
const styles = StyleSheet.create({
|
|
7
|
+
container: {
|
|
8
|
+
flexGrow: 1,
|
|
9
|
+
justifyContent: 'center',
|
|
10
|
+
alignItems: 'center',
|
|
11
|
+
paddingHorizontal: scaling.scaleHorizontal(spacing.lg),
|
|
12
|
+
paddingVertical: scaling.scaleVertical(spacing.xl),
|
|
13
|
+
},
|
|
14
|
+
content: {
|
|
15
|
+
width: '100%',
|
|
16
|
+
alignItems: 'center',
|
|
17
|
+
gap: scaling.scaleVertical(spacing.md),
|
|
18
|
+
},
|
|
19
|
+
visual: {
|
|
20
|
+
alignItems: 'center',
|
|
21
|
+
justifyContent: 'center',
|
|
22
|
+
},
|
|
23
|
+
title: {
|
|
24
|
+
textAlign: 'center',
|
|
25
|
+
},
|
|
26
|
+
description: {
|
|
27
|
+
textAlign: 'center',
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return styles;
|
|
32
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { ImageResizeMode, ImageSourcePropType, ImageStyle, StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
export interface PhotoPreviewProps {
|
|
5
|
+
source: ImageSourcePropType;
|
|
6
|
+
width?: number;
|
|
7
|
+
height?: number;
|
|
8
|
+
aspectRatio?: number;
|
|
9
|
+
borderRadius?: number;
|
|
10
|
+
resizeMode?: ImageResizeMode;
|
|
11
|
+
overlay?: ReactNode;
|
|
12
|
+
removeIcon?: ReactNode;
|
|
13
|
+
onRemovePress?: () => void;
|
|
14
|
+
removeAccessibilityLabel?: string;
|
|
15
|
+
removeAccessibilityHint?: string;
|
|
16
|
+
onPress?: () => void;
|
|
17
|
+
accessibilityLabel?: string;
|
|
18
|
+
accessibilityHint?: string;
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
style?: StyleProp<ViewStyle>;
|
|
21
|
+
imageStyle?: StyleProp<ImageStyle>;
|
|
22
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { Image, Pressable, StyleSheet, Text, View } from 'react-native';
|
|
3
|
+
import { useUITheme } from '../../../theme';
|
|
4
|
+
import { useScaling } from '../../../utils';
|
|
5
|
+
import { getStyles } from './styles';
|
|
6
|
+
import type { PhotoPreviewProps } from '../types/types';
|
|
7
|
+
|
|
8
|
+
export const PhotoPreview = ({
|
|
9
|
+
source,
|
|
10
|
+
width,
|
|
11
|
+
height,
|
|
12
|
+
aspectRatio,
|
|
13
|
+
borderRadius,
|
|
14
|
+
resizeMode = 'cover',
|
|
15
|
+
overlay,
|
|
16
|
+
removeIcon,
|
|
17
|
+
onRemovePress,
|
|
18
|
+
removeAccessibilityLabel,
|
|
19
|
+
removeAccessibilityHint,
|
|
20
|
+
onPress,
|
|
21
|
+
accessibilityLabel,
|
|
22
|
+
accessibilityHint,
|
|
23
|
+
disabled = false,
|
|
24
|
+
style,
|
|
25
|
+
imageStyle,
|
|
26
|
+
}: PhotoPreviewProps) => {
|
|
27
|
+
const { colors, radius, spacing } = useUITheme();
|
|
28
|
+
const scaling = useScaling();
|
|
29
|
+
const { styles, removeHitSlop } = useMemo(
|
|
30
|
+
() => getStyles(colors, radius, spacing, scaling, width, height, aspectRatio, borderRadius),
|
|
31
|
+
[colors, radius, spacing, scaling, width, height, aspectRatio, borderRadius],
|
|
32
|
+
);
|
|
33
|
+
const image = (
|
|
34
|
+
<Image
|
|
35
|
+
source={source}
|
|
36
|
+
resizeMode={resizeMode}
|
|
37
|
+
accessible={!onPress}
|
|
38
|
+
accessibilityRole={onPress ? undefined : 'image'}
|
|
39
|
+
accessibilityLabel={onPress ? undefined : accessibilityLabel}
|
|
40
|
+
accessibilityHint={onPress ? undefined : accessibilityHint}
|
|
41
|
+
style={[styles.image, imageStyle]}
|
|
42
|
+
/>
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<View style={[styles.container, style]}>
|
|
47
|
+
{onPress ? (
|
|
48
|
+
<Pressable
|
|
49
|
+
accessibilityRole="button"
|
|
50
|
+
accessibilityLabel={accessibilityLabel}
|
|
51
|
+
accessibilityHint={accessibilityHint}
|
|
52
|
+
accessibilityState={{ disabled }}
|
|
53
|
+
disabled={disabled}
|
|
54
|
+
onPress={onPress}
|
|
55
|
+
style={StyleSheet.absoluteFill}
|
|
56
|
+
>
|
|
57
|
+
{image}
|
|
58
|
+
</Pressable>
|
|
59
|
+
) : image}
|
|
60
|
+
{overlay != null && overlay !== false ? (
|
|
61
|
+
<View pointerEvents={disabled ? 'none' : 'box-none'} style={styles.overlay}>
|
|
62
|
+
{typeof overlay === 'string' || typeof overlay === 'number' ? <Text>{overlay}</Text> : overlay}
|
|
63
|
+
</View>
|
|
64
|
+
) : null}
|
|
65
|
+
{onRemovePress ? (
|
|
66
|
+
<Pressable
|
|
67
|
+
accessibilityRole="button"
|
|
68
|
+
accessibilityLabel={removeAccessibilityLabel ?? 'Remove photo'}
|
|
69
|
+
accessibilityHint={removeAccessibilityHint}
|
|
70
|
+
accessibilityState={{ disabled }}
|
|
71
|
+
disabled={disabled}
|
|
72
|
+
hitSlop={removeHitSlop}
|
|
73
|
+
onPress={onRemovePress}
|
|
74
|
+
style={styles.remove}
|
|
75
|
+
>
|
|
76
|
+
{removeIcon === undefined ? <Text style={styles.removeText}>×</Text>
|
|
77
|
+
: typeof removeIcon === 'string' || typeof removeIcon === 'number'
|
|
78
|
+
? <Text style={styles.removeText}>{removeIcon}</Text> : removeIcon}
|
|
79
|
+
</Pressable>
|
|
80
|
+
) : null}
|
|
81
|
+
</View>
|
|
82
|
+
);
|
|
83
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { StyleSheet } from 'react-native';
|
|
2
|
+
import type { UIColors, UIRadius, UISpacing } from '../../../theme';
|
|
3
|
+
import type { ScalingFunctions } from '../../../utils';
|
|
4
|
+
import type { PhotoPreviewProps } from '../types/types';
|
|
5
|
+
|
|
6
|
+
const positiveOrUndefined = (value: number | undefined) =>
|
|
7
|
+
value !== undefined && Number.isFinite(value) && value > 0 ? value : undefined;
|
|
8
|
+
|
|
9
|
+
export const getStyles = (
|
|
10
|
+
colors: UIColors,
|
|
11
|
+
radius: UIRadius,
|
|
12
|
+
spacing: UISpacing,
|
|
13
|
+
scaling: ScalingFunctions,
|
|
14
|
+
width: PhotoPreviewProps['width'],
|
|
15
|
+
height: PhotoPreviewProps['height'],
|
|
16
|
+
aspectRatio: PhotoPreviewProps['aspectRatio'],
|
|
17
|
+
borderRadius: PhotoPreviewProps['borderRadius'],
|
|
18
|
+
) => {
|
|
19
|
+
const designWidth = positiveOrUndefined(width);
|
|
20
|
+
const designHeight = positiveOrUndefined(height);
|
|
21
|
+
const ratio = positiveOrUndefined(aspectRatio);
|
|
22
|
+
const useWidth = designWidth !== undefined || designHeight === undefined;
|
|
23
|
+
const derivedRatio = ratio ?? (designWidth === undefined || designHeight === undefined ? 1 : undefined);
|
|
24
|
+
const containerWidth = useWidth ? scaling.scaleHorizontal(designWidth ?? 120) : undefined;
|
|
25
|
+
const containerHeight = derivedRatio === undefined || !useWidth
|
|
26
|
+
? designHeight === undefined ? undefined : scaling.scaleVertical(designHeight)
|
|
27
|
+
: undefined;
|
|
28
|
+
const designRadius = borderRadius !== undefined && Number.isFinite(borderRadius)
|
|
29
|
+
? Math.max(0, borderRadius) : radius.md;
|
|
30
|
+
|
|
31
|
+
const styles = StyleSheet.create({
|
|
32
|
+
container: {
|
|
33
|
+
width: containerWidth,
|
|
34
|
+
height: containerHeight,
|
|
35
|
+
aspectRatio: derivedRatio,
|
|
36
|
+
borderRadius: scaling.scaleHorizontal(designRadius),
|
|
37
|
+
backgroundColor: colors.surface,
|
|
38
|
+
overflow: 'hidden',
|
|
39
|
+
},
|
|
40
|
+
image: {
|
|
41
|
+
...StyleSheet.absoluteFill,
|
|
42
|
+
},
|
|
43
|
+
overlay: {
|
|
44
|
+
...StyleSheet.absoluteFill,
|
|
45
|
+
},
|
|
46
|
+
remove: {
|
|
47
|
+
position: 'absolute',
|
|
48
|
+
top: scaling.scaleVertical(spacing.sm),
|
|
49
|
+
right: scaling.scaleHorizontal(spacing.sm),
|
|
50
|
+
zIndex: 1,
|
|
51
|
+
width: scaling.scaleHorizontal(28),
|
|
52
|
+
height: scaling.scaleHorizontal(28),
|
|
53
|
+
borderRadius: scaling.scaleHorizontal(14),
|
|
54
|
+
backgroundColor: colors.surface,
|
|
55
|
+
alignItems: 'center',
|
|
56
|
+
justifyContent: 'center',
|
|
57
|
+
},
|
|
58
|
+
removeText: {
|
|
59
|
+
color: colors.text,
|
|
60
|
+
fontSize: scaling.scaleFontSize(22),
|
|
61
|
+
lineHeight: scaling.scaleLineHeight(24),
|
|
62
|
+
textAlign: 'center',
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
const removeHitSlop = scaling.scaleHorizontal(8);
|
|
66
|
+
|
|
67
|
+
return { styles, removeHitSlop };
|
|
68
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useCallback, useEffect } from 'react';
|
|
2
|
+
import { ReduceMotion, useSharedValue, withTiming } from 'react-native-reanimated';
|
|
3
|
+
import type { SwitchProps } from '../types/types';
|
|
4
|
+
|
|
5
|
+
export const useSwitch = (
|
|
6
|
+
value: boolean,
|
|
7
|
+
disabled: boolean,
|
|
8
|
+
onValueChange: SwitchProps['onValueChange'],
|
|
9
|
+
) => {
|
|
10
|
+
const progress = useSharedValue(value ? 1 : 0);
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
progress.value = withTiming(value ? 1 : 0, { duration: 200, reduceMotion: ReduceMotion.System });
|
|
14
|
+
}, [progress, value]);
|
|
15
|
+
|
|
16
|
+
const onPress = useCallback(() => {
|
|
17
|
+
if (!disabled) onValueChange?.(!value);
|
|
18
|
+
}, [disabled, onValueChange, value]);
|
|
19
|
+
|
|
20
|
+
return { onPress, progress };
|
|
21
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
export interface SwitchProps {
|
|
5
|
+
value: boolean;
|
|
6
|
+
onValueChange?: (value: boolean) => void;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
width?: number;
|
|
9
|
+
height?: number;
|
|
10
|
+
thumbSize?: number;
|
|
11
|
+
activeTrackColor?: string;
|
|
12
|
+
inactiveTrackColor?: string;
|
|
13
|
+
disabledTrackColor?: string;
|
|
14
|
+
thumbColor?: string;
|
|
15
|
+
disabledThumbColor?: string;
|
|
16
|
+
thumb?: ReactNode;
|
|
17
|
+
accessibilityLabel?: string;
|
|
18
|
+
accessibilityHint?: string;
|
|
19
|
+
style?: StyleProp<ViewStyle>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { Pressable, Text } from 'react-native';
|
|
3
|
+
import Reanimated from 'react-native-reanimated';
|
|
4
|
+
import { useUITheme } from '../../../theme';
|
|
5
|
+
import { useScaling } from '../../../utils';
|
|
6
|
+
import { useSwitch } from '../presenters/useSwitch';
|
|
7
|
+
import { getStyles, getSwitchGeometry, useSwitchAnimatedStyles } from './styles';
|
|
8
|
+
import type { SwitchProps } from '../types/types';
|
|
9
|
+
|
|
10
|
+
const AnimatedPressable = Reanimated.createAnimatedComponent(Pressable);
|
|
11
|
+
|
|
12
|
+
export const Switch = ({
|
|
13
|
+
value,
|
|
14
|
+
onValueChange,
|
|
15
|
+
disabled = false,
|
|
16
|
+
width = 40,
|
|
17
|
+
height = 24,
|
|
18
|
+
thumbSize = 20,
|
|
19
|
+
activeTrackColor,
|
|
20
|
+
inactiveTrackColor,
|
|
21
|
+
disabledTrackColor,
|
|
22
|
+
thumbColor,
|
|
23
|
+
disabledThumbColor,
|
|
24
|
+
thumb,
|
|
25
|
+
accessibilityLabel,
|
|
26
|
+
accessibilityHint,
|
|
27
|
+
style,
|
|
28
|
+
}: SwitchProps) => {
|
|
29
|
+
const { colors } = useUITheme();
|
|
30
|
+
const scaling = useScaling();
|
|
31
|
+
const geometry = useMemo(() => getSwitchGeometry(scaling, width, height, thumbSize), [scaling, width, height, thumbSize]);
|
|
32
|
+
const styles = useMemo(
|
|
33
|
+
() => getStyles(geometry, disabled ? disabledThumbColor ?? colors.surface : thumbColor ?? colors.background),
|
|
34
|
+
[colors.background, colors.surface, disabled, disabledThumbColor, geometry, thumbColor],
|
|
35
|
+
);
|
|
36
|
+
const { onPress, progress } = useSwitch(value, disabled, onValueChange);
|
|
37
|
+
const { trackStyle, thumbStyle } = useSwitchAnimatedStyles(
|
|
38
|
+
progress,
|
|
39
|
+
geometry.travelDistance,
|
|
40
|
+
disabled ? disabledTrackColor ?? colors.disabled : inactiveTrackColor ?? colors.border,
|
|
41
|
+
disabled ? disabledTrackColor ?? colors.disabled : activeTrackColor ?? colors.primary,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<AnimatedPressable
|
|
46
|
+
accessibilityRole="switch"
|
|
47
|
+
accessibilityLabel={accessibilityLabel}
|
|
48
|
+
accessibilityHint={accessibilityHint}
|
|
49
|
+
accessibilityState={{ checked: value, disabled }}
|
|
50
|
+
disabled={disabled}
|
|
51
|
+
hitSlop={geometry.hitSlop}
|
|
52
|
+
onPress={onPress}
|
|
53
|
+
style={[styles.track, trackStyle, style]}
|
|
54
|
+
>
|
|
55
|
+
<Reanimated.View style={[styles.thumb, thumbStyle]} pointerEvents="none">
|
|
56
|
+
{typeof thumb === 'string' || typeof thumb === 'number' ? <Text>{thumb}</Text> : thumb}
|
|
57
|
+
</Reanimated.View>
|
|
58
|
+
</AnimatedPressable>
|
|
59
|
+
);
|
|
60
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { StyleSheet } from 'react-native';
|
|
2
|
+
import { interpolateColor, useAnimatedStyle } from 'react-native-reanimated';
|
|
3
|
+
import type { SharedValue } from 'react-native-reanimated';
|
|
4
|
+
import type { ScalingFunctions } from '../../../utils';
|
|
5
|
+
|
|
6
|
+
const positiveOr = (value: number, fallback: number) => Number.isFinite(value) && value > 0 ? value : fallback;
|
|
7
|
+
|
|
8
|
+
export const getSwitchGeometry = (scaling: ScalingFunctions, width: number, height: number, thumbSize: number) => {
|
|
9
|
+
const designWidth = positiveOr(width, 40);
|
|
10
|
+
const designHeight = positiveOr(height, 24);
|
|
11
|
+
const designPadding = Math.min(2, designWidth / 4, designHeight / 4);
|
|
12
|
+
const designThumb = Math.min(positiveOr(thumbSize, 20), Math.min(designWidth, designHeight) - 2 * designPadding);
|
|
13
|
+
const trackWidth = scaling.scaleHorizontal(designWidth);
|
|
14
|
+
const trackHeight = scaling.scaleHorizontal(designHeight);
|
|
15
|
+
const padding = scaling.scaleHorizontal(designPadding);
|
|
16
|
+
const thumb = scaling.scaleHorizontal(designThumb);
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
trackWidth,
|
|
20
|
+
trackHeight,
|
|
21
|
+
padding,
|
|
22
|
+
thumb,
|
|
23
|
+
travelDistance: Math.max(0, trackWidth - 2 * padding - thumb),
|
|
24
|
+
hitSlop: {
|
|
25
|
+
top: scaling.scaleHorizontal(Math.max(10, (44 - designHeight) / 2)),
|
|
26
|
+
bottom: scaling.scaleHorizontal(Math.max(10, (44 - designHeight) / 2)),
|
|
27
|
+
left: scaling.scaleHorizontal(Math.max(10, (44 - designWidth) / 2)),
|
|
28
|
+
right: scaling.scaleHorizontal(Math.max(10, (44 - designWidth) / 2)),
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const getStyles = (geometry: ReturnType<typeof getSwitchGeometry>, thumbColor: string) => {
|
|
34
|
+
const styles = StyleSheet.create({
|
|
35
|
+
track: {
|
|
36
|
+
width: geometry.trackWidth,
|
|
37
|
+
height: geometry.trackHeight,
|
|
38
|
+
padding: geometry.padding,
|
|
39
|
+
borderRadius: geometry.trackHeight / 2,
|
|
40
|
+
justifyContent: 'center',
|
|
41
|
+
flexShrink: 0,
|
|
42
|
+
},
|
|
43
|
+
thumb: {
|
|
44
|
+
width: geometry.thumb,
|
|
45
|
+
height: geometry.thumb,
|
|
46
|
+
borderRadius: geometry.thumb / 2,
|
|
47
|
+
backgroundColor: thumbColor,
|
|
48
|
+
alignItems: 'center',
|
|
49
|
+
justifyContent: 'center',
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return styles;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const useSwitchAnimatedStyles = (
|
|
57
|
+
progress: SharedValue<number>,
|
|
58
|
+
travelDistance: number,
|
|
59
|
+
inactiveTrackColor: string,
|
|
60
|
+
activeTrackColor: string,
|
|
61
|
+
) => {
|
|
62
|
+
const trackStyle = useAnimatedStyle(() => ({
|
|
63
|
+
backgroundColor: interpolateColor(progress.value, [0, 1], [inactiveTrackColor, activeTrackColor]),
|
|
64
|
+
}), [activeTrackColor, inactiveTrackColor]);
|
|
65
|
+
const thumbStyle = useAnimatedStyle(() => ({
|
|
66
|
+
transform: [{ translateX: progress.value * travelDistance }],
|
|
67
|
+
}), [travelDistance]);
|
|
68
|
+
|
|
69
|
+
return { trackStyle, thumbStyle };
|
|
70
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
|
+
export * from './components/Avatar';
|
|
1
2
|
export * from './components/Button';
|
|
2
3
|
export * from './components/BottomModal';
|
|
4
|
+
export * from './components/Checkbox';
|
|
3
5
|
export * from './components/ConnectionContainer';
|
|
4
6
|
export * from './components/CustomAlert';
|
|
7
|
+
export * from './components/EmptyListView';
|
|
5
8
|
export * from './components/HeaderWithBackButton';
|
|
6
9
|
export * from './components/Input';
|
|
7
10
|
export * from './components/Loader';
|
|
8
11
|
export * from './components/Logger';
|
|
12
|
+
export * from './components/PhotoPreview';
|
|
9
13
|
export * from './components/ScreenContainer';
|
|
14
|
+
export * from './components/Switch';
|
|
10
15
|
export * from './components/Typography';
|
|
11
16
|
export * from './provider';
|
|
12
17
|
export * from './theme';
|