@bigbinary/neetoui-rn 3.0.3 → 3.1.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 +29 -1
- package/lib/typescript/src/components/BottomSheet/BottomSheet.d.ts +8 -1
- package/lib/typescript/src/components/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/lib/typescript/src/components/ChatInput/AttachmentsView.d.ts.map +1 -1
- package/lib/typescript/src/components/ChatInput/ChatInput.d.ts.map +1 -1
- package/lib/typescript/src/components/CheckBox/CheckBox.d.ts +7 -1
- package/lib/typescript/src/components/CheckBox/CheckBox.d.ts.map +1 -1
- package/lib/typescript/src/components/Input/Input.d.ts +12 -11
- package/lib/typescript/src/components/Input/Input.d.ts.map +1 -1
- package/lib/typescript/src/components/Input/InputFrame.d.ts +41 -0
- package/lib/typescript/src/components/Input/InputFrame.d.ts.map +1 -0
- package/lib/typescript/src/components/Input/index.d.ts +1 -0
- package/lib/typescript/src/components/Input/index.d.ts.map +1 -1
- package/lib/typescript/src/components/NeetoUIProvider/NeetoUIProvider.d.ts.map +1 -1
- package/lib/typescript/src/components/RadioButton/RadioButton.d.ts +6 -1
- package/lib/typescript/src/components/RadioButton/RadioButton.d.ts.map +1 -1
- package/lib/typescript/src/components/SearchBar/SearchBar.d.ts.map +1 -1
- package/lib/typescript/src/components/Skeleton/Skeleton.d.ts.map +1 -1
- package/lib/typescript/src/components/Typography/Typography.d.ts +5 -0
- package/lib/typescript/src/components/Typography/Typography.d.ts.map +1 -1
- package/lib/typescript/src/components/Typography/index.d.ts +1 -1
- package/lib/typescript/src/components/Typography/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +2 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/unistyles.d.ts +28 -1
- package/lib/typescript/src/unistyles.d.ts.map +1 -1
- package/lib/typescript/src/utils/scale.d.ts +22 -0
- package/lib/typescript/src/utils/scale.d.ts.map +1 -1
- package/package.json +8 -6
- package/src/components/Alert/Alert.tsx +51 -44
- package/src/components/BottomSheet/BottomSheet.tsx +98 -55
- package/src/components/BottomSheetMenu/BottomSheetMenu.tsx +55 -48
- package/src/components/ChatInput/AttachmentsView.tsx +32 -18
- package/src/components/ChatInput/ChatInput.tsx +148 -99
- package/src/components/ChatInput/IconButton.tsx +21 -10
- package/src/components/ChatInput/MentionsInput.tsx +34 -27
- package/src/components/CheckBox/CheckBox.tsx +54 -33
- package/src/components/EmptyView/EmptyView.tsx +27 -20
- package/src/components/Header/Header.tsx +25 -18
- package/src/components/Header/ScreenHeader.tsx +17 -10
- package/src/components/Input/Input.tsx +111 -67
- package/src/components/Input/InputFrame.tsx +261 -0
- package/src/components/Input/index.ts +1 -0
- package/src/components/NeetoUIProvider/NeetoUIProvider.tsx +2 -1
- package/src/components/OnBoarding/OnBoarding.tsx +61 -54
- package/src/components/RadioButton/RadioButton.tsx +72 -31
- package/src/components/SearchBar/SearchBar.tsx +34 -26
- package/src/components/Skeleton/Skeleton.tsx +18 -11
- package/src/components/Toast/Toast.tsx +38 -31
- package/src/components/Typography/Typography.tsx +31 -2
- package/src/components/Typography/index.ts +5 -1
- package/src/index.ts +7 -2
- package/src/unistyles-init.ts +4 -0
- package/src/unistyles.ts +52 -4
- package/src/utils/scale.ts +36 -9
- package/src/types/neeto-icons-rn.d.ts +0 -33
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Platform, type StyleProp, type ViewStyle } from "react-native";
|
|
2
2
|
import { StyleSheet } from "react-native-unistyles";
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { scaleFor } from "../../utils/scale";
|
|
5
5
|
import { Typography } from "../Typography";
|
|
6
6
|
import { BaseHeader } from "./BaseHeader";
|
|
7
7
|
|
|
@@ -22,13 +22,20 @@ export const ScreenHeader = ({ title, style }: ScreenHeaderProps) => (
|
|
|
22
22
|
</BaseHeader>
|
|
23
23
|
);
|
|
24
24
|
|
|
25
|
-
const styles = StyleSheet.create({
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
25
|
+
const styles = StyleSheet.create((_theme, rt) => {
|
|
26
|
+
// Reading rt.screen registers the dependency that has this sheet
|
|
27
|
+
// recomputed on a window resize, so scaled sizes follow rotation,
|
|
28
|
+
// split-screen and foldables instead of freezing at first render.
|
|
29
|
+
const moderateScale = scaleFor(rt.screen);
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
container: {
|
|
33
|
+
height: Platform.OS === "android" ? moderateScale(48) : moderateScale(42),
|
|
34
|
+
},
|
|
35
|
+
title: {
|
|
36
|
+
includeFontPadding: false,
|
|
37
|
+
textAlignVertical: "center",
|
|
38
|
+
...(Platform.OS === "android" ? { lineHeight: moderateScale(28) } : {}),
|
|
39
|
+
},
|
|
40
|
+
};
|
|
34
41
|
});
|
|
@@ -1,21 +1,29 @@
|
|
|
1
|
-
import { forwardRef, useState } from "react";
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
type StyleProp,
|
|
6
|
-
type TextInputProps,
|
|
7
|
-
type ViewStyle,
|
|
8
|
-
} from "react-native";
|
|
9
|
-
import { StyleSheet } from "react-native-unistyles";
|
|
1
|
+
import { forwardRef, useRef, useState } from "react";
|
|
2
|
+
import Close from "@bigbinary/neeto-icons-rn/icons/Close";
|
|
3
|
+
import { TextInput, type TextInputProps } from "react-native";
|
|
4
|
+
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
|
10
5
|
|
|
11
|
-
import {
|
|
6
|
+
import { InputFrame, type InputFrameProps } from "./InputFrame";
|
|
12
7
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
8
|
+
import { moderateScale } from "../../utils/scale";
|
|
9
|
+
import { Touchable } from "../Touchable";
|
|
10
|
+
|
|
11
|
+
export type InputProps = Omit<TextInputProps, "editable"> &
|
|
12
|
+
Pick<
|
|
13
|
+
InputFrameProps,
|
|
14
|
+
| "label"
|
|
15
|
+
| "error"
|
|
16
|
+
| "disabled"
|
|
17
|
+
| "isRequiredLabel"
|
|
18
|
+
| "noBorder"
|
|
19
|
+
| "containerStyle"
|
|
20
|
+
| "fieldStyle"
|
|
21
|
+
> & {
|
|
22
|
+
/** Shows a clear button inside the field whenever it holds text. */
|
|
23
|
+
isClearable?: boolean;
|
|
24
|
+
/** Called after the clear button has emptied the field. */
|
|
25
|
+
onClear?: () => void;
|
|
26
|
+
};
|
|
19
27
|
|
|
20
28
|
/** Forwards its ref to the underlying TextInput, so forms can focus it. */
|
|
21
29
|
export const Input = forwardRef<TextInput, InputProps>(
|
|
@@ -24,49 +32,112 @@ export const Input = forwardRef<TextInput, InputProps>(
|
|
|
24
32
|
label,
|
|
25
33
|
error,
|
|
26
34
|
disabled = false,
|
|
35
|
+
isRequiredLabel = false,
|
|
36
|
+
isClearable = false,
|
|
37
|
+
noBorder = false,
|
|
38
|
+
onClear,
|
|
27
39
|
containerStyle,
|
|
40
|
+
fieldStyle,
|
|
28
41
|
style,
|
|
42
|
+
value,
|
|
43
|
+
defaultValue,
|
|
44
|
+
placeholder,
|
|
45
|
+
multiline = false,
|
|
29
46
|
onFocus,
|
|
30
47
|
onBlur,
|
|
48
|
+
onChangeText,
|
|
31
49
|
...rest
|
|
32
50
|
},
|
|
33
51
|
ref
|
|
34
52
|
) => {
|
|
53
|
+
const { theme } = useUnistyles();
|
|
35
54
|
const [isFocused, setIsFocused] = useState(false);
|
|
36
55
|
|
|
37
|
-
|
|
56
|
+
// The clear button and the label both need to know whether the field
|
|
57
|
+
// holds text. A controlled caller answers that with `value`; an
|
|
58
|
+
// uncontrolled one has nothing to read, so its emptiness is tracked here.
|
|
59
|
+
const [hasText, setHasText] = useState(() => !!(value ?? defaultValue));
|
|
60
|
+
const isEmpty = value === undefined ? !hasText : !value;
|
|
61
|
+
|
|
62
|
+
const inputRef = useRef<TextInput | null>(null);
|
|
63
|
+
const attachRef = (node: TextInput | null) => {
|
|
64
|
+
inputRef.current = node;
|
|
65
|
+
|
|
66
|
+
if (typeof ref === "function") ref(node);
|
|
67
|
+
else if (ref) ref.current = node;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const handleClear = () => {
|
|
71
|
+
// clear() is what empties an uncontrolled field; a controlled one is
|
|
72
|
+
// emptied by the caller reacting to onChangeText.
|
|
73
|
+
inputRef.current?.clear();
|
|
74
|
+
setHasText(false);
|
|
75
|
+
onChangeText?.("");
|
|
76
|
+
onClear?.();
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const isLabelFloating = isFocused || !isEmpty;
|
|
80
|
+
|
|
81
|
+
styles.useVariants({ disabled });
|
|
38
82
|
|
|
39
83
|
return (
|
|
40
|
-
<
|
|
41
|
-
{
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
84
|
+
<InputFrame
|
|
85
|
+
containerStyle={containerStyle}
|
|
86
|
+
disabled={disabled}
|
|
87
|
+
error={error}
|
|
88
|
+
fieldStyle={fieldStyle}
|
|
89
|
+
isFocused={isFocused}
|
|
90
|
+
isLabelFloating={isLabelFloating}
|
|
91
|
+
isRequiredLabel={isRequiredLabel}
|
|
92
|
+
label={label}
|
|
93
|
+
multiline={multiline}
|
|
94
|
+
noBorder={noBorder}
|
|
95
|
+
trailing={
|
|
96
|
+
isClearable && !disabled && !isEmpty ? (
|
|
97
|
+
<Touchable
|
|
98
|
+
accessibilityLabel={label ? `Clear ${label}` : "Clear"}
|
|
99
|
+
accessibilityRole="button"
|
|
100
|
+
hitSlop={moderateScale(8)}
|
|
101
|
+
onPress={handleClear}
|
|
102
|
+
>
|
|
103
|
+
<Close color={theme.colors.text.muted} size={moderateScale(18)} />
|
|
104
|
+
</Touchable>
|
|
105
|
+
) : null
|
|
106
|
+
}
|
|
107
|
+
onPress={() => {
|
|
108
|
+
if (!inputRef.current?.isFocused()) inputRef.current?.focus();
|
|
109
|
+
}}
|
|
110
|
+
>
|
|
46
111
|
<TextInput
|
|
47
112
|
// Before the spread: the label is only a fallback, and setting it
|
|
48
113
|
// afterwards overwrote a caller's own accessibilityLabel with
|
|
49
114
|
// undefined whenever no label was passed.
|
|
50
115
|
accessibilityLabel={label}
|
|
51
|
-
{
|
|
116
|
+
defaultValue={defaultValue}
|
|
52
117
|
editable={!disabled}
|
|
53
|
-
|
|
118
|
+
multiline={multiline}
|
|
119
|
+
placeholderTextColor={theme.colors.text.muted}
|
|
120
|
+
// A resting label already occupies the value line, so showing the
|
|
121
|
+
// placeholder there too would print the two on top of each other.
|
|
122
|
+
placeholder={label && !isLabelFloating ? undefined : placeholder}
|
|
123
|
+
{...rest}
|
|
124
|
+
ref={attachRef}
|
|
54
125
|
style={[styles.input, style]}
|
|
126
|
+
value={value}
|
|
55
127
|
onBlur={event => {
|
|
56
128
|
setIsFocused(false);
|
|
57
129
|
onBlur?.(event);
|
|
58
130
|
}}
|
|
131
|
+
onChangeText={next => {
|
|
132
|
+
setHasText(!!next);
|
|
133
|
+
onChangeText?.(next);
|
|
134
|
+
}}
|
|
59
135
|
onFocus={event => {
|
|
60
136
|
setIsFocused(true);
|
|
61
137
|
onFocus?.(event);
|
|
62
138
|
}}
|
|
63
139
|
/>
|
|
64
|
-
|
|
65
|
-
<Typography size="2xs" style={styles.error}>
|
|
66
|
-
{error}
|
|
67
|
-
</Typography>
|
|
68
|
-
)}
|
|
69
|
-
</View>
|
|
140
|
+
</InputFrame>
|
|
70
141
|
);
|
|
71
142
|
}
|
|
72
143
|
);
|
|
@@ -74,49 +145,22 @@ export const Input = forwardRef<TextInput, InputProps>(
|
|
|
74
145
|
Input.displayName = "Input";
|
|
75
146
|
|
|
76
147
|
const styles = StyleSheet.create(theme => ({
|
|
77
|
-
container: {
|
|
78
|
-
gap: theme.spacing.xs,
|
|
79
|
-
},
|
|
80
|
-
label: {
|
|
81
|
-
color: theme.colors.text.secondary,
|
|
82
|
-
fontFamily: theme.fonts.medium,
|
|
83
|
-
},
|
|
84
148
|
input: {
|
|
85
|
-
borderColor: theme.colors.border.primary,
|
|
86
|
-
borderRadius: theme.radii.md,
|
|
87
|
-
borderWidth: 1,
|
|
88
149
|
color: theme.colors.text.primary,
|
|
89
150
|
fontFamily: theme.fonts.regular,
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
151
|
+
// 17 is what v2 typed at, and it is what the resting label shrinks
|
|
152
|
+
// from: any other size makes the label jump rather than lift.
|
|
153
|
+
fontSize: theme.fontSizes.xl,
|
|
154
|
+
// Android's EditText brings its own padding, which would stack on top
|
|
155
|
+
// of the box's and make the field taller on one platform only.
|
|
156
|
+
padding: 0,
|
|
93
157
|
variants: {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
false: {},
|
|
97
|
-
},
|
|
98
|
-
error: {
|
|
99
|
-
true: { borderColor: theme.colors.danger },
|
|
100
|
-
false: {},
|
|
101
|
-
},
|
|
158
|
+
// Dimmed by color, not by the frame's opacity: still legible, and it
|
|
159
|
+
// does not drag the label and border down with it.
|
|
102
160
|
disabled: {
|
|
103
|
-
true: {
|
|
104
|
-
backgroundColor: theme.colors.background.secondary,
|
|
105
|
-
opacity: 0.5,
|
|
106
|
-
},
|
|
161
|
+
true: { color: theme.colors.text.muted },
|
|
107
162
|
false: {},
|
|
108
163
|
},
|
|
109
164
|
},
|
|
110
|
-
// Error must win over focus when both are active.
|
|
111
|
-
compoundVariants: [
|
|
112
|
-
{
|
|
113
|
-
error: true,
|
|
114
|
-
focused: true,
|
|
115
|
-
styles: { borderColor: theme.colors.danger },
|
|
116
|
-
},
|
|
117
|
-
],
|
|
118
|
-
},
|
|
119
|
-
error: {
|
|
120
|
-
color: theme.colors.danger,
|
|
121
165
|
},
|
|
122
166
|
}));
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { useEffect, type ReactNode } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Pressable,
|
|
4
|
+
Text,
|
|
5
|
+
View,
|
|
6
|
+
type StyleProp,
|
|
7
|
+
type TextStyle,
|
|
8
|
+
type ViewStyle,
|
|
9
|
+
} from "react-native";
|
|
10
|
+
import Animated, {
|
|
11
|
+
interpolate,
|
|
12
|
+
useAnimatedStyle,
|
|
13
|
+
useSharedValue,
|
|
14
|
+
withTiming,
|
|
15
|
+
} from "react-native-reanimated";
|
|
16
|
+
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
|
17
|
+
|
|
18
|
+
import { scaleFor } from "../../utils/scale";
|
|
19
|
+
import { LINE_HEIGHT_RATIO, Typography } from "../Typography";
|
|
20
|
+
|
|
21
|
+
export type InputFrameProps = {
|
|
22
|
+
label?: string;
|
|
23
|
+
error?: string;
|
|
24
|
+
disabled?: boolean;
|
|
25
|
+
/** Appends a danger-colored `*` to the label. */
|
|
26
|
+
isRequiredLabel?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Lifts the label out of the value line and shrinks it. The caller decides
|
|
29
|
+
* because only it knows what "has a value" means: text for an Input,
|
|
30
|
+
* a selection for a picker, an open sheet for a dropdown.
|
|
31
|
+
*/
|
|
32
|
+
isLabelFloating?: boolean;
|
|
33
|
+
/** Paints the focus border. */
|
|
34
|
+
isFocused?: boolean;
|
|
35
|
+
/** Drops the border, for a field already framed by its container. */
|
|
36
|
+
noBorder?: boolean;
|
|
37
|
+
/** Pins the trailing slot to the first line of a field that grows. */
|
|
38
|
+
multiline?: boolean;
|
|
39
|
+
/** The value line: a TextInput, a Typography, a row of chips. */
|
|
40
|
+
children?: ReactNode;
|
|
41
|
+
/** Icons inside the frame, after the value. */
|
|
42
|
+
trailing?: ReactNode;
|
|
43
|
+
onPress?: () => void;
|
|
44
|
+
containerStyle?: StyleProp<ViewStyle>;
|
|
45
|
+
/** Styles the bordered box itself. */
|
|
46
|
+
fieldStyle?: StyleProp<ViewStyle>;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const LABEL_ANIMATION_DURATION = 200;
|
|
50
|
+
/** Shared by the box's padding and the label's inset, which must agree. */
|
|
51
|
+
const HORIZONTAL_PADDING = 12;
|
|
52
|
+
/** Shared by the box's height and the resting label, which is centred in it. */
|
|
53
|
+
const LABELLED_MIN_HEIGHT = 56;
|
|
54
|
+
const LABEL_FLOATING_TOP = 6;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The bordered box with a floating label, shared by every field-shaped
|
|
58
|
+
* control. v2's Input animated its label from the value line up to the top
|
|
59
|
+
* edge; v3 replaced it with a static caption above the box, which reads as a
|
|
60
|
+
* second, smaller heading and leaves an empty field with nothing in it.
|
|
61
|
+
*
|
|
62
|
+
* Split out of Input because a picker is the same box with a different value
|
|
63
|
+
* line — a date, a chevron, a row of chips — and duplicating the animation
|
|
64
|
+
* per screen is how four controls end up nearly, but not quite, aligned.
|
|
65
|
+
*/
|
|
66
|
+
export const InputFrame = ({
|
|
67
|
+
label,
|
|
68
|
+
error,
|
|
69
|
+
disabled = false,
|
|
70
|
+
isRequiredLabel = false,
|
|
71
|
+
isLabelFloating = false,
|
|
72
|
+
isFocused = false,
|
|
73
|
+
noBorder = false,
|
|
74
|
+
multiline = false,
|
|
75
|
+
children,
|
|
76
|
+
trailing,
|
|
77
|
+
onPress,
|
|
78
|
+
containerStyle,
|
|
79
|
+
fieldStyle,
|
|
80
|
+
}: InputFrameProps) => {
|
|
81
|
+
const { theme, rt } = useUnistyles();
|
|
82
|
+
// Read through the hook rather than the stylesheet: the worklet needs the
|
|
83
|
+
// numbers themselves, and useUnistyles re-renders on a window resize so
|
|
84
|
+
// they stay right when the window changes size.
|
|
85
|
+
const moderateScale = scaleFor(rt.screen);
|
|
86
|
+
|
|
87
|
+
const restingFontSize = theme.fontSizes.xl;
|
|
88
|
+
const floatingFontSize = theme.fontSizes.xs;
|
|
89
|
+
// Derived, not typed in: a resting label is the value line, so it has to
|
|
90
|
+
// sit on the box's centre line or the field looks top-heavy.
|
|
91
|
+
const restingTop =
|
|
92
|
+
(moderateScale(LABELLED_MIN_HEIGHT) - restingFontSize * LINE_HEIGHT_RATIO) /
|
|
93
|
+
2;
|
|
94
|
+
const floatingTop = moderateScale(LABEL_FLOATING_TOP);
|
|
95
|
+
|
|
96
|
+
// Plain object, not a sheet entry: Skeleton keeps unistyles styles off
|
|
97
|
+
// animated elements too, and these belong next to the interpolation that
|
|
98
|
+
// moves them anyway.
|
|
99
|
+
const labelStyle: TextStyle = {
|
|
100
|
+
// muted, the placeholder grey: at rest the label stands in for the value,
|
|
101
|
+
// and text.secondary is close enough to text.primary to read as one.
|
|
102
|
+
// A disabled field steps down one more, rather than the whole box fading.
|
|
103
|
+
color: disabled ? theme.colors.palette.grey400 : theme.colors.text.muted,
|
|
104
|
+
fontFamily: theme.fonts.regular,
|
|
105
|
+
includeFontPadding: false,
|
|
106
|
+
left: moderateScale(HORIZONTAL_PADDING),
|
|
107
|
+
// Positioned by the animated `top`, so it takes no part in the row.
|
|
108
|
+
position: "absolute",
|
|
109
|
+
right: moderateScale(HORIZONTAL_PADDING),
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const progress = useSharedValue(isLabelFloating ? 1 : 0);
|
|
113
|
+
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
progress.value = withTiming(isLabelFloating ? 1 : 0, {
|
|
116
|
+
duration: LABEL_ANIMATION_DURATION,
|
|
117
|
+
});
|
|
118
|
+
}, [isLabelFloating, progress]);
|
|
119
|
+
|
|
120
|
+
const animatedLabelStyle = useAnimatedStyle(() => {
|
|
121
|
+
const fontSize = interpolate(
|
|
122
|
+
progress.value,
|
|
123
|
+
[0, 1],
|
|
124
|
+
[restingFontSize, floatingFontSize]
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
fontSize,
|
|
129
|
+
lineHeight: fontSize * LINE_HEIGHT_RATIO,
|
|
130
|
+
top: interpolate(progress.value, [0, 1], [restingTop, floatingTop]),
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
styles.useVariants({
|
|
135
|
+
focused: isFocused,
|
|
136
|
+
error: !!error,
|
|
137
|
+
disabled,
|
|
138
|
+
multiline,
|
|
139
|
+
noBorder,
|
|
140
|
+
hasLabel: !!label,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<View style={[styles.container, containerStyle]}>
|
|
145
|
+
<Pressable
|
|
146
|
+
disabled={disabled || !onPress}
|
|
147
|
+
style={[styles.field, fieldStyle]}
|
|
148
|
+
onPress={onPress}
|
|
149
|
+
>
|
|
150
|
+
{!!label && (
|
|
151
|
+
<Animated.Text
|
|
152
|
+
numberOfLines={1}
|
|
153
|
+
style={[labelStyle, animatedLabelStyle]}
|
|
154
|
+
>
|
|
155
|
+
{label}
|
|
156
|
+
{/* A plain Text, so it inherits the animated font size and only
|
|
157
|
+
overrides the color. */}
|
|
158
|
+
{isRequiredLabel && <Text style={styles.required}>*</Text>}
|
|
159
|
+
</Animated.Text>
|
|
160
|
+
)}
|
|
161
|
+
<View style={styles.value}>{children}</View>
|
|
162
|
+
{trailing}
|
|
163
|
+
</Pressable>
|
|
164
|
+
{!!error && (
|
|
165
|
+
<Typography size="2xs" style={styles.error}>
|
|
166
|
+
{error}
|
|
167
|
+
</Typography>
|
|
168
|
+
)}
|
|
169
|
+
</View>
|
|
170
|
+
);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const styles = StyleSheet.create((theme, rt) => {
|
|
174
|
+
// Reading rt.screen registers the dependency that has this sheet
|
|
175
|
+
// recomputed on a window resize, so scaled sizes follow rotation,
|
|
176
|
+
// split-screen and foldables instead of freezing at first render.
|
|
177
|
+
const moderateScale = scaleFor(rt.screen);
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
container: {
|
|
181
|
+
gap: moderateScale(4),
|
|
182
|
+
},
|
|
183
|
+
field: {
|
|
184
|
+
alignItems: "center",
|
|
185
|
+
borderColor: theme.colors.border.primary,
|
|
186
|
+
borderRadius: moderateScale(8),
|
|
187
|
+
borderWidth: 1,
|
|
188
|
+
flexDirection: "row",
|
|
189
|
+
gap: moderateScale(8),
|
|
190
|
+
paddingHorizontal: moderateScale(HORIZONTAL_PADDING),
|
|
191
|
+
variants: {
|
|
192
|
+
// A labelled box reserves the top strip for the floated label; an
|
|
193
|
+
// unlabelled one is a plain field at the 44pt iOS HIG minimum.
|
|
194
|
+
hasLabel: {
|
|
195
|
+
true: {
|
|
196
|
+
minHeight: moderateScale(LABELLED_MIN_HEIGHT),
|
|
197
|
+
paddingBottom: moderateScale(6),
|
|
198
|
+
paddingTop: moderateScale(22),
|
|
199
|
+
},
|
|
200
|
+
false: {
|
|
201
|
+
minHeight: moderateScale(44),
|
|
202
|
+
paddingVertical: moderateScale(8),
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
focused: {
|
|
206
|
+
true: { borderColor: theme.colors.primary },
|
|
207
|
+
false: {},
|
|
208
|
+
},
|
|
209
|
+
error: {
|
|
210
|
+
true: { borderColor: theme.colors.danger },
|
|
211
|
+
false: {},
|
|
212
|
+
},
|
|
213
|
+
// No opacity: fading the container takes the value text with it, and
|
|
214
|
+
// grey-on-grey at half alpha drops under the 4.5:1 contrast floor
|
|
215
|
+
// exactly when a disabled field still has something to read. The
|
|
216
|
+
// greys are stated per element instead.
|
|
217
|
+
disabled: {
|
|
218
|
+
true: {
|
|
219
|
+
backgroundColor: theme.colors.background.secondary,
|
|
220
|
+
borderColor: theme.colors.palette.grey200,
|
|
221
|
+
},
|
|
222
|
+
false: {},
|
|
223
|
+
},
|
|
224
|
+
// A multiline field grows downward, so its trailing icon belongs at
|
|
225
|
+
// the first line rather than floating at the vertical centre.
|
|
226
|
+
multiline: {
|
|
227
|
+
true: { alignItems: "flex-start" },
|
|
228
|
+
false: {},
|
|
229
|
+
},
|
|
230
|
+
noBorder: {
|
|
231
|
+
true: { borderWidth: 0 },
|
|
232
|
+
false: {},
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
// Error must win over focus when both are active.
|
|
236
|
+
compoundVariants: [
|
|
237
|
+
{
|
|
238
|
+
error: true,
|
|
239
|
+
focused: true,
|
|
240
|
+
styles: { borderColor: theme.colors.danger },
|
|
241
|
+
},
|
|
242
|
+
// borderWidth 0 would otherwise still paint a focus/error color.
|
|
243
|
+
{
|
|
244
|
+
noBorder: true,
|
|
245
|
+
focused: true,
|
|
246
|
+
styles: { borderWidth: 0 },
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
},
|
|
250
|
+
required: {
|
|
251
|
+
color: theme.colors.danger,
|
|
252
|
+
},
|
|
253
|
+
value: {
|
|
254
|
+
flex: 1,
|
|
255
|
+
justifyContent: "center",
|
|
256
|
+
},
|
|
257
|
+
error: {
|
|
258
|
+
color: theme.colors.danger,
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
});
|
|
@@ -24,7 +24,8 @@ export type NeetoUIProviderProps = {
|
|
|
24
24
|
*/
|
|
25
25
|
export const NeetoUIProvider = ({ children }: NeetoUIProviderProps) => {
|
|
26
26
|
// Idempotent fallback — the root entry already configures on import
|
|
27
|
-
// (src/unistyles-init.ts); this covers deep-import edge cases.
|
|
27
|
+
// (src/unistyles-init.ts); this covers deep-import edge cases. Passing no
|
|
28
|
+
// options keeps an app's adaptiveThemes opt-in intact.
|
|
28
29
|
configureNeetoUI();
|
|
29
30
|
|
|
30
31
|
return (
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
10
10
|
import { StyleSheet } from "react-native-unistyles";
|
|
11
11
|
|
|
12
|
-
import { moderateScale } from "../../utils/scale";
|
|
12
|
+
import { moderateScale, scaleFor } from "../../utils/scale";
|
|
13
13
|
import { Button } from "../Button";
|
|
14
14
|
import { Typography } from "../Typography";
|
|
15
15
|
|
|
@@ -122,56 +122,63 @@ export const OnBoarding = ({
|
|
|
122
122
|
);
|
|
123
123
|
};
|
|
124
124
|
|
|
125
|
-
const styles = StyleSheet.create(theme =>
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
125
|
+
const styles = StyleSheet.create((theme, rt) => {
|
|
126
|
+
// Reading rt.screen registers the dependency that has this sheet
|
|
127
|
+
// recomputed on a window resize, so scaled sizes follow rotation,
|
|
128
|
+
// split-screen and foldables instead of freezing at first render.
|
|
129
|
+
const moderateScale = scaleFor(rt.screen);
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
root: {
|
|
133
|
+
backgroundColor: theme.colors.background.primary,
|
|
134
|
+
flex: 1,
|
|
135
|
+
},
|
|
136
|
+
logoContainer: {
|
|
137
|
+
alignItems: "center",
|
|
138
|
+
},
|
|
139
|
+
slide: {
|
|
140
|
+
flex: 1,
|
|
141
|
+
justifyContent: "space-between",
|
|
142
|
+
},
|
|
143
|
+
illustration: {
|
|
144
|
+
alignItems: "center",
|
|
145
|
+
flex: 1,
|
|
146
|
+
justifyContent: "center",
|
|
147
|
+
marginHorizontal: moderateScale(24),
|
|
148
|
+
marginVertical: moderateScale(12),
|
|
149
|
+
},
|
|
150
|
+
slideText: {
|
|
151
|
+
alignItems: "center",
|
|
152
|
+
marginHorizontal: moderateScale(24),
|
|
153
|
+
marginVertical: moderateScale(12),
|
|
154
|
+
},
|
|
155
|
+
centered: {
|
|
156
|
+
textAlign: "center",
|
|
157
|
+
},
|
|
158
|
+
description: {
|
|
159
|
+
color: theme.colors.text.muted,
|
|
160
|
+
},
|
|
161
|
+
dots: {
|
|
162
|
+
alignItems: "center",
|
|
163
|
+
flexDirection: "row",
|
|
164
|
+
justifyContent: "center",
|
|
165
|
+
marginVertical: moderateScale(20),
|
|
166
|
+
},
|
|
167
|
+
dot: {
|
|
168
|
+
backgroundColor: theme.colors.text.primary,
|
|
169
|
+
borderRadius: moderateScale(5),
|
|
170
|
+
height: moderateScale(10),
|
|
171
|
+
marginHorizontal: moderateScale(4),
|
|
172
|
+
width: moderateScale(10),
|
|
173
|
+
},
|
|
174
|
+
inactiveDot: {
|
|
175
|
+
backgroundColor: theme.colors.palette.grey400,
|
|
176
|
+
},
|
|
177
|
+
buttonContainer: {
|
|
178
|
+
marginHorizontal: moderateScale(24),
|
|
179
|
+
},
|
|
180
|
+
buttonContainerInsetless: {
|
|
181
|
+
marginBottom: moderateScale(16),
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
});
|