@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.
Files changed (56) hide show
  1. package/README.md +29 -1
  2. package/lib/typescript/src/components/BottomSheet/BottomSheet.d.ts +8 -1
  3. package/lib/typescript/src/components/BottomSheet/BottomSheet.d.ts.map +1 -1
  4. package/lib/typescript/src/components/ChatInput/AttachmentsView.d.ts.map +1 -1
  5. package/lib/typescript/src/components/ChatInput/ChatInput.d.ts.map +1 -1
  6. package/lib/typescript/src/components/CheckBox/CheckBox.d.ts +7 -1
  7. package/lib/typescript/src/components/CheckBox/CheckBox.d.ts.map +1 -1
  8. package/lib/typescript/src/components/Input/Input.d.ts +12 -11
  9. package/lib/typescript/src/components/Input/Input.d.ts.map +1 -1
  10. package/lib/typescript/src/components/Input/InputFrame.d.ts +41 -0
  11. package/lib/typescript/src/components/Input/InputFrame.d.ts.map +1 -0
  12. package/lib/typescript/src/components/Input/index.d.ts +1 -0
  13. package/lib/typescript/src/components/Input/index.d.ts.map +1 -1
  14. package/lib/typescript/src/components/NeetoUIProvider/NeetoUIProvider.d.ts.map +1 -1
  15. package/lib/typescript/src/components/RadioButton/RadioButton.d.ts +6 -1
  16. package/lib/typescript/src/components/RadioButton/RadioButton.d.ts.map +1 -1
  17. package/lib/typescript/src/components/SearchBar/SearchBar.d.ts.map +1 -1
  18. package/lib/typescript/src/components/Skeleton/Skeleton.d.ts.map +1 -1
  19. package/lib/typescript/src/components/Typography/Typography.d.ts +5 -0
  20. package/lib/typescript/src/components/Typography/Typography.d.ts.map +1 -1
  21. package/lib/typescript/src/components/Typography/index.d.ts +1 -1
  22. package/lib/typescript/src/components/Typography/index.d.ts.map +1 -1
  23. package/lib/typescript/src/index.d.ts +2 -2
  24. package/lib/typescript/src/index.d.ts.map +1 -1
  25. package/lib/typescript/src/unistyles.d.ts +28 -1
  26. package/lib/typescript/src/unistyles.d.ts.map +1 -1
  27. package/lib/typescript/src/utils/scale.d.ts +22 -0
  28. package/lib/typescript/src/utils/scale.d.ts.map +1 -1
  29. package/package.json +8 -6
  30. package/src/components/Alert/Alert.tsx +51 -44
  31. package/src/components/BottomSheet/BottomSheet.tsx +98 -55
  32. package/src/components/BottomSheetMenu/BottomSheetMenu.tsx +55 -48
  33. package/src/components/ChatInput/AttachmentsView.tsx +32 -18
  34. package/src/components/ChatInput/ChatInput.tsx +148 -99
  35. package/src/components/ChatInput/IconButton.tsx +21 -10
  36. package/src/components/ChatInput/MentionsInput.tsx +34 -27
  37. package/src/components/CheckBox/CheckBox.tsx +54 -33
  38. package/src/components/EmptyView/EmptyView.tsx +27 -20
  39. package/src/components/Header/Header.tsx +25 -18
  40. package/src/components/Header/ScreenHeader.tsx +17 -10
  41. package/src/components/Input/Input.tsx +111 -67
  42. package/src/components/Input/InputFrame.tsx +261 -0
  43. package/src/components/Input/index.ts +1 -0
  44. package/src/components/NeetoUIProvider/NeetoUIProvider.tsx +2 -1
  45. package/src/components/OnBoarding/OnBoarding.tsx +61 -54
  46. package/src/components/RadioButton/RadioButton.tsx +72 -31
  47. package/src/components/SearchBar/SearchBar.tsx +34 -26
  48. package/src/components/Skeleton/Skeleton.tsx +18 -11
  49. package/src/components/Toast/Toast.tsx +38 -31
  50. package/src/components/Typography/Typography.tsx +31 -2
  51. package/src/components/Typography/index.ts +5 -1
  52. package/src/index.ts +7 -2
  53. package/src/unistyles-init.ts +4 -0
  54. package/src/unistyles.ts +52 -4
  55. package/src/utils/scale.ts +36 -9
  56. 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 { moderateScale } from "../../utils/scale";
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
- container: {
27
- height: Platform.OS === "android" ? moderateScale(48) : moderateScale(42),
28
- },
29
- title: {
30
- includeFontPadding: false,
31
- textAlignVertical: "center",
32
- ...(Platform.OS === "android" ? { lineHeight: moderateScale(28) } : {}),
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
- TextInput,
4
- View,
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 { Typography } from "../Typography";
6
+ import { InputFrame, type InputFrameProps } from "./InputFrame";
12
7
 
13
- export type InputProps = Omit<TextInputProps, "editable"> & {
14
- label?: string;
15
- error?: string;
16
- disabled?: boolean;
17
- containerStyle?: StyleProp<ViewStyle>;
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
- styles.useVariants({ focused: isFocused, error: !!error, disabled });
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
- <View style={[styles.container, containerStyle]}>
41
- {!!label && (
42
- <Typography size="xs" style={styles.label}>
43
- {label}
44
- </Typography>
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
- {...rest}
116
+ defaultValue={defaultValue}
52
117
  editable={!disabled}
53
- ref={ref}
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
- {!!error && (
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
- fontSize: theme.fontSizes.s,
91
- paddingHorizontal: theme.spacing.md,
92
- paddingVertical: theme.spacing.sm,
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
- focused: {
95
- true: { borderColor: theme.colors.primary },
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
+ });
@@ -1 +1,2 @@
1
1
  export { Input, type InputProps } from "./Input";
2
+ export { InputFrame, type InputFrameProps } from "./InputFrame";
@@ -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
- root: {
127
- backgroundColor: theme.colors.background.primary,
128
- flex: 1,
129
- },
130
- logoContainer: {
131
- alignItems: "center",
132
- },
133
- slide: {
134
- flex: 1,
135
- justifyContent: "space-between",
136
- },
137
- illustration: {
138
- alignItems: "center",
139
- flex: 1,
140
- justifyContent: "center",
141
- marginHorizontal: moderateScale(24),
142
- marginVertical: moderateScale(12),
143
- },
144
- slideText: {
145
- alignItems: "center",
146
- marginHorizontal: moderateScale(24),
147
- marginVertical: moderateScale(12),
148
- },
149
- centered: {
150
- textAlign: "center",
151
- },
152
- description: {
153
- color: theme.colors.text.muted,
154
- },
155
- dots: {
156
- alignItems: "center",
157
- flexDirection: "row",
158
- justifyContent: "center",
159
- marginVertical: moderateScale(20),
160
- },
161
- dot: {
162
- backgroundColor: theme.colors.text.primary,
163
- borderRadius: moderateScale(5),
164
- height: moderateScale(10),
165
- marginHorizontal: moderateScale(4),
166
- width: moderateScale(10),
167
- },
168
- inactiveDot: {
169
- backgroundColor: theme.colors.palette.grey400,
170
- },
171
- buttonContainer: {
172
- marginHorizontal: moderateScale(24),
173
- },
174
- buttonContainerInsetless: {
175
- marginBottom: moderateScale(16),
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
+ });