@terreno/ui 0.13.3 → 0.14.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 (140) hide show
  1. package/dist/ActionSheet.d.ts +4 -4
  2. package/dist/ActionSheet.js.map +1 -1
  3. package/dist/Avatar.js +1 -1
  4. package/dist/Avatar.js.map +1 -1
  5. package/dist/Banner.js.map +1 -1
  6. package/dist/Box.js +2 -0
  7. package/dist/Box.js.map +1 -1
  8. package/dist/Button.d.ts +2 -2
  9. package/dist/Button.js +35 -23
  10. package/dist/Button.js.map +1 -1
  11. package/dist/Common.d.ts +8 -2
  12. package/dist/Common.js.map +1 -1
  13. package/dist/ConsentFormScreen.js +1 -1
  14. package/dist/ConsentFormScreen.js.map +1 -1
  15. package/dist/ConsentNavigator.d.ts +1 -1
  16. package/dist/ConsentNavigator.js +2 -1
  17. package/dist/ConsentNavigator.js.map +1 -1
  18. package/dist/CustomSelectField.js +3 -1
  19. package/dist/CustomSelectField.js.map +1 -1
  20. package/dist/DataTable.js +1 -1
  21. package/dist/DataTable.js.map +1 -1
  22. package/dist/DateTimeActionSheet.js +2 -1
  23. package/dist/DateTimeActionSheet.js.map +1 -1
  24. package/dist/DateTimeField.js +3 -2
  25. package/dist/DateTimeField.js.map +1 -1
  26. package/dist/DateUtilities.js.map +1 -1
  27. package/dist/HeightField.js.map +1 -1
  28. package/dist/Hyperlink.js +19 -9
  29. package/dist/Hyperlink.js.map +1 -1
  30. package/dist/IconButton.js.map +1 -1
  31. package/dist/ImageBackground.d.ts +2 -5
  32. package/dist/ImageBackground.js +1 -1
  33. package/dist/ImageBackground.js.map +1 -1
  34. package/dist/ModalSheet.d.ts +3 -2
  35. package/dist/ModalSheet.js +1 -1
  36. package/dist/ModalSheet.js.map +1 -1
  37. package/dist/OfflineBanner.d.ts +21 -0
  38. package/dist/OfflineBanner.js +25 -0
  39. package/dist/OfflineBanner.js.map +1 -0
  40. package/dist/OpenAPIContext.js +1 -1
  41. package/dist/OpenAPIContext.js.map +1 -1
  42. package/dist/Page.js +1 -0
  43. package/dist/Page.js.map +1 -1
  44. package/dist/Pagination.js.map +1 -1
  45. package/dist/Permissions.js +3 -0
  46. package/dist/Permissions.js.map +1 -1
  47. package/dist/PickerSelect.js +7 -4
  48. package/dist/PickerSelect.js.map +1 -1
  49. package/dist/SelectField.js +1 -1
  50. package/dist/SelectField.js.map +1 -1
  51. package/dist/SplitPage.js +7 -2
  52. package/dist/SplitPage.js.map +1 -1
  53. package/dist/SplitPage.native.js +4 -1
  54. package/dist/SplitPage.native.js.map +1 -1
  55. package/dist/TapToEdit.js +10 -11
  56. package/dist/TapToEdit.js.map +1 -1
  57. package/dist/Toast.js.map +1 -1
  58. package/dist/ToastNotifications.js.map +1 -1
  59. package/dist/Unifier.d.ts +2 -2
  60. package/dist/Unifier.js +1 -1
  61. package/dist/Unifier.js.map +1 -1
  62. package/dist/Utilities.d.ts +8 -4
  63. package/dist/Utilities.js +1 -1
  64. package/dist/Utilities.js.map +1 -1
  65. package/dist/index.d.ts +1 -0
  66. package/dist/index.js +1 -0
  67. package/dist/index.js.map +1 -1
  68. package/package.json +2 -1
  69. package/src/ActionSheet.test.tsx +1 -0
  70. package/src/ActionSheet.tsx +6 -4
  71. package/src/Avatar.tsx +9 -2
  72. package/src/Badge.test.tsx +1 -0
  73. package/src/Banner.tsx +1 -1
  74. package/src/Box.test.tsx +1 -0
  75. package/src/Box.tsx +10 -6
  76. package/src/Button.test.tsx +35 -0
  77. package/src/Button.tsx +65 -34
  78. package/src/Common.ts +32 -15
  79. package/src/ConsentFormScreen.test.tsx +102 -0
  80. package/src/ConsentFormScreen.tsx +9 -3
  81. package/src/ConsentNavigator.test.tsx +1 -0
  82. package/src/ConsentNavigator.tsx +5 -3
  83. package/src/CustomSelectField.tsx +3 -1
  84. package/src/DataTable.test.tsx +1 -0
  85. package/src/DataTable.tsx +1 -1
  86. package/src/DateTimeActionSheet.tsx +7 -3
  87. package/src/DateTimeField.test.tsx +1 -0
  88. package/src/DateTimeField.tsx +3 -2
  89. package/src/DateUtilities.test.ts +111 -0
  90. package/src/DateUtilities.tsx +6 -6
  91. package/src/DecimalRangeActionSheet.test.tsx +28 -0
  92. package/src/ErrorBoundary.test.tsx +1 -0
  93. package/src/HeightField.tsx +2 -1
  94. package/src/Hyperlink.tsx +83 -52
  95. package/src/IconButton.tsx +1 -1
  96. package/src/ImageBackground.tsx +5 -6
  97. package/src/ModalSheet.test.tsx +1 -5
  98. package/src/ModalSheet.tsx +15 -6
  99. package/src/NumberField.test.tsx +14 -0
  100. package/src/OfflineBanner.test.tsx +70 -0
  101. package/src/OfflineBanner.tsx +54 -0
  102. package/src/OpenAPIContext.tsx +3 -2
  103. package/src/Page.tsx +1 -0
  104. package/src/Pagination.tsx +1 -1
  105. package/src/Permissions.ts +3 -0
  106. package/src/PickerSelect.tsx +17 -14
  107. package/src/SelectBadge.test.tsx +1 -0
  108. package/src/SelectField.tsx +1 -1
  109. package/src/Signature.test.tsx +1 -0
  110. package/src/SplitPage.native.tsx +2 -0
  111. package/src/SplitPage.tsx +6 -1
  112. package/src/TapToEdit.test.tsx +17 -0
  113. package/src/TapToEdit.tsx +11 -11
  114. package/src/Toast.tsx +1 -1
  115. package/src/ToastNotifications.tsx +1 -4
  116. package/src/Tooltip.test.tsx +0 -7
  117. package/src/Unifier.ts +6 -5
  118. package/src/Utilities.tsx +9 -6
  119. package/src/__snapshots__/AddressField.test.tsx.snap +3 -1
  120. package/src/__snapshots__/Button.test.tsx.snap +92 -50
  121. package/src/__snapshots__/CustomSelectField.test.tsx.snap +21 -7
  122. package/src/__snapshots__/DecimalRangeActionSheet.test.tsx.snap +14 -8
  123. package/src/__snapshots__/ErrorPage.test.tsx.snap +7 -4
  124. package/src/__snapshots__/Field.test.tsx.snap +18 -6
  125. package/src/__snapshots__/HeightActionSheet.test.tsx.snap +14 -8
  126. package/src/__snapshots__/HeightField.test.tsx.snap +35 -20
  127. package/src/__snapshots__/InfoModalIcon.test.tsx.snap +28 -16
  128. package/src/__snapshots__/Modal.test.tsx.snap +19 -10
  129. package/src/__snapshots__/ModalSheet.test.tsx.snap +0 -1
  130. package/src/__snapshots__/NumberPickerActionSheet.test.tsx.snap +14 -8
  131. package/src/__snapshots__/Page.test.tsx.snap +7 -4
  132. package/src/__snapshots__/SelectField.test.tsx.snap +18 -6
  133. package/src/__snapshots__/TerrenoProvider.test.tsx.snap +0 -2
  134. package/src/__snapshots__/TimezonePicker.test.tsx.snap +18 -6
  135. package/src/bunSetup.ts +25 -2
  136. package/src/index.tsx +1 -0
  137. package/src/login/__snapshots__/LoginScreen.test.tsx.snap +15 -6
  138. package/src/signUp/__snapshots__/SignUpScreen.test.tsx.snap +10 -4
  139. package/src/table/__snapshots__/TableBadge.test.tsx.snap +3 -1
  140. package/src/types/react-native-swiper-flatlist.d.ts +1 -0
@@ -1,3 +1,4 @@
1
+ // biome-ignore-all lint/suspicious/noExplicitAny: test mock typing
1
2
  import {afterAll, beforeAll, describe, expect, it, mock, spyOn} from "bun:test";
2
3
  import React from "react";
3
4
 
@@ -1,6 +1,7 @@
1
1
  import {type FC, useCallback, useEffect, useMemo, useRef, useState} from "react";
2
2
  import {Platform, Pressable, type StyleProp, TextInput, View} from "react-native";
3
3
 
4
+ import type {ActionSheet} from "./ActionSheet";
4
5
  import {Box} from "./Box";
5
6
  import type {HeightFieldProps, TextStyleWithOutline} from "./Common";
6
7
  import {FieldError, FieldHelperText, FieldTitle} from "./fieldElements";
@@ -154,7 +155,7 @@ export const HeightField: FC<HeightFieldProps> = ({
154
155
  max,
155
156
  }) => {
156
157
  const {theme} = useTheme();
157
- const actionSheetRef: React.RefObject<any> = useRef(null);
158
+ const actionSheetRef = useRef<ActionSheet | null>(null);
158
159
  const isMobileOrNative = isMobileDevice() || isNative();
159
160
 
160
161
  const minInches = min ?? DEFAULT_MIN_INCHES;
package/src/Hyperlink.tsx CHANGED
@@ -36,73 +36,94 @@
36
36
 
37
37
  import * as mdurl from "mdurl";
38
38
  import React from "react";
39
- import {Linking, Platform, Text, View} from "react-native";
39
+ import {Linking, Platform, type StyleProp, Text, type TextStyle, View} from "react-native";
40
40
 
41
41
  import type {HyperlinkProps} from "./Common";
42
42
 
43
- const linkifyLib = require("linkify-it")();
43
+ interface LinkifyMatch {
44
+ index: number;
45
+ lastIndex: number;
46
+ raw: string;
47
+ schema: string;
48
+ text: string;
49
+ url: string;
50
+ }
51
+
52
+ interface LinkifyItLike {
53
+ pretest: (text: string) => boolean;
54
+ test: (text: string) => boolean;
55
+ match: (text: string) => LinkifyMatch[] | null;
56
+ }
57
+
58
+ const linkifyLib: LinkifyItLike = require("linkify-it")();
44
59
 
45
60
  const {OS} = Platform;
46
61
 
47
62
  // Leaving this as a class component because it was easier to handle the `parse(this)` in
48
63
  // `render()`
49
64
  class HyperlinkComponent extends React.Component<HyperlinkProps> {
50
- isTextNested = (component: any) => {
65
+ isTextNested = (component: React.ReactElement) => {
51
66
  if (!React.isValidElement(component)) throw new Error("Invalid component");
52
- const {type: {displayName} = {} as any} = component;
67
+ const componentType = (component.type as {displayName?: string} | undefined) ?? {};
68
+ const {displayName} = componentType;
53
69
  if (displayName !== "Text") throw new Error("Not a Text component");
54
- return typeof (component.props as any).children !== "string";
70
+ return typeof (component.props as {children?: unknown}).children !== "string";
55
71
  };
56
72
 
57
- linkify = (component: any) => {
73
+ linkify = (component: React.ReactElement<{children: string; style?: unknown}>) => {
58
74
  const linkifyIt = this.props.linkify || linkifyLib;
59
75
 
60
76
  if (!linkifyIt.pretest(component.props.children) || !linkifyIt.test(component.props.children))
61
77
  return component;
62
78
 
63
- const elements = [];
79
+ const elements: React.ReactNode[] = [];
64
80
  let _lastIndex = 0;
65
81
 
66
- const {key: _key, ref: _ref, ...componentProps} = component.props;
82
+ const {key: _key, ref: _ref, ...componentProps} = component.props as Record<string, unknown>;
67
83
 
68
84
  try {
69
- linkifyIt.match(component.props.children).forEach(({index, lastIndex, text, url}: any) => {
70
- const nonLinkedText = component.props.children.substring(_lastIndex, index);
71
- nonLinkedText && elements.push(nonLinkedText);
72
- _lastIndex = lastIndex;
73
- if (this.props.linkText)
74
- text =
75
- typeof this.props.linkText === "function"
76
- ? this.props.linkText(url)
77
- : this.props.linkText;
78
-
79
- const clickHandlerProps: any = {};
80
- if (OS !== "web") {
81
- if (this.props.onLongPress) {
82
- clickHandlerProps.onLongPress = () => (this.props as any).onLongPress(url, text);
85
+ linkifyIt
86
+ .match(component.props.children)
87
+ ?.forEach(({index, lastIndex, text, url}: LinkifyMatch) => {
88
+ const nonLinkedText = component.props.children.substring(_lastIndex, index);
89
+ nonLinkedText && elements.push(nonLinkedText);
90
+ _lastIndex = lastIndex;
91
+ if (this.props.linkText)
92
+ text =
93
+ typeof this.props.linkText === "function"
94
+ ? this.props.linkText(url)
95
+ : this.props.linkText;
96
+
97
+ const clickHandlerProps: {onPress?: () => void; onLongPress?: () => void} = {};
98
+ if (OS !== "web") {
99
+ if (this.props.onLongPress) {
100
+ clickHandlerProps.onLongPress = () => this.props.onLongPress?.(url, text);
101
+ }
102
+ }
103
+ if (this.props.onPress) {
104
+ // The HyperlinkProps onPress signature is (url) => void per Common.ts, but this forked
105
+ // component invokes it with both url and text. Cast to avoid arity mismatch.
106
+ const onPressFn = this.props.onPress as (url: string, text: string) => void;
107
+ clickHandlerProps.onPress = () => onPressFn(url, text);
83
108
  }
84
- }
85
- if (this.props.onPress) {
86
- clickHandlerProps.onPress = () => (this.props as any).onPress(url, text);
87
- }
88
-
89
- let injected: any = {};
90
- if (this.props.injectViewProps) {
91
- injected = this.props.injectViewProps(url);
92
- }
93
-
94
- elements.push(
95
- <Text
96
- {...componentProps}
97
- {...clickHandlerProps}
98
- key={url + index}
99
- style={[component.props.style, this.props.linkStyle]}
100
- {...injected}
101
- >
102
- {text}
103
- </Text>
104
- );
105
- });
109
+
110
+ let injected: Record<string, unknown> = {};
111
+ if (this.props.injectViewProps) {
112
+ injected = this.props.injectViewProps(url);
113
+ }
114
+
115
+ elements.push(
116
+ <Text
117
+ {...componentProps}
118
+ {...clickHandlerProps}
119
+ key={url + index}
120
+ style={[component.props.style, this.props.linkStyle]}
121
+ {...injected}
122
+ >
123
+ {text}
124
+ </Text>
125
+ );
126
+ });
106
127
  elements.push(
107
128
  component.props.children.substring(_lastIndex, component.props.children.length)
108
129
  );
@@ -112,11 +133,13 @@ class HyperlinkComponent extends React.Component<HyperlinkProps> {
112
133
  }
113
134
  };
114
135
 
115
- parse = (component: any): React.ReactElement => {
116
- const {props: {children} = {} as any} = component || {};
136
+ parse = (component: React.ReactElement): React.ReactElement => {
137
+ const props =
138
+ (component?.props as {children?: React.ReactNode; style?: StyleProp<TextStyle>}) ?? {};
139
+ const {children} = props;
117
140
  if (!children) return component;
118
141
 
119
- const {key: _key, ref: _ref, ...componentProps} = component.props;
142
+ const {key: _key, ref: _ref, ...componentProps} = component.props as Record<string, unknown>;
120
143
 
121
144
  const linkifyIt = this.props.linkify || linkifyLib;
122
145
 
@@ -124,15 +147,19 @@ class HyperlinkComponent extends React.Component<HyperlinkProps> {
124
147
  component,
125
148
  componentProps,
126
149
  React.Children.map(children, (child) => {
127
- const {type: {displayName} = {} as any} = child || {};
150
+ const childType = (child as React.ReactElement | null)?.type as
151
+ | {displayName?: string}
152
+ | undefined;
153
+ const displayName = childType?.displayName;
128
154
  if (typeof child === "string" && linkifyIt.pretest(child))
129
155
  return this.linkify(
130
- <Text {...componentProps} style={component.props.style}>
156
+ <Text {...componentProps} style={props.style}>
131
157
  {child}
132
158
  </Text>
133
159
  );
134
- if (displayName === "Text" && !this.isTextNested(child)) return this.linkify(child);
135
- return this.parse(child);
160
+ if (displayName === "Text" && !this.isTextNested(child as React.ReactElement))
161
+ return this.linkify(child as React.ReactElement<{children: string; style?: unknown}>);
162
+ return this.parse(child as React.ReactElement);
136
163
  })
137
164
  );
138
165
  };
@@ -148,7 +175,11 @@ class HyperlinkComponent extends React.Component<HyperlinkProps> {
148
175
  <View {...viewProps} style={this.props.style}>
149
176
  {!this.props.onPress && !this.props.onLongPress && !this.props.linkStyle
150
177
  ? this.props.children
151
- : (this.parse(this).props as {children?: React.ReactNode}).children}
178
+ : (
179
+ this.parse(this as unknown as React.ReactElement).props as {
180
+ children?: React.ReactNode;
181
+ }
182
+ ).children}
152
183
  </View>
153
184
  );
154
185
  }
@@ -159,7 +159,7 @@ const IconButtonComponent: FC<IconButtonProps> = ({
159
159
  display: "flex",
160
160
  height: 12,
161
161
  justifyContent: "center",
162
- padding: theme.spacing.xs as any,
162
+ padding: theme.spacing.xs,
163
163
  position: "absolute",
164
164
  right: 0,
165
165
  width: 12,
@@ -1,11 +1,10 @@
1
1
  import React from "react";
2
- import {ImageBackground as ImageBackgroundNative} from "react-native";
2
+ import {
3
+ ImageBackground as ImageBackgroundNative,
4
+ type ImageBackgroundProps as NativeImageBackgroundProps,
5
+ } from "react-native";
3
6
 
4
- interface ImageBackgroundProps {
5
- children?: any;
6
- style?: any;
7
- source: any;
8
- }
7
+ type ImageBackgroundProps = NativeImageBackgroundProps;
9
8
 
10
9
  export class ImageBackground extends React.Component<ImageBackgroundProps, {}> {
11
10
  render() {
@@ -1,3 +1,4 @@
1
+ // biome-ignore-all lint/suspicious/noExplicitAny: test mock typing
1
2
  import {describe, expect, it, mock} from "bun:test";
2
3
  import {forwardRef, useRef} from "react";
3
4
  import {Text, View} from "react-native";
@@ -14,11 +15,6 @@ mock.module("react-native-modalize", () => ({
14
15
  )),
15
16
  }));
16
17
 
17
- // Mock react-native-portalize
18
- mock.module("react-native-portalize", () => ({
19
- Portal: ({children}: {children: React.ReactNode}) => <View testID="portal">{children}</View>,
20
- }));
21
-
22
18
  describe("ModalSheet", () => {
23
19
  it("renders correctly with children", () => {
24
20
  const {toJSON} = renderWithTheme(
@@ -1,14 +1,23 @@
1
- import {forwardRef, useEffect, useRef} from "react";
1
+ import {
2
+ forwardRef,
3
+ type MutableRefObject,
4
+ type ReactNode,
5
+ type Ref,
6
+ useEffect,
7
+ useRef,
8
+ } from "react";
2
9
  import {Animated} from "react-native";
3
10
  import {Modalize} from "react-native-modalize";
4
11
  import {Portal} from "react-native-portalize";
5
12
 
6
- export const useCombinedRefs = (...refs: any) => {
7
- const targetRef = useRef<any>(null);
13
+ export const useCombinedRefs = <T,>(
14
+ ...refs: Array<Ref<T> | undefined>
15
+ ): MutableRefObject<T | null> => {
16
+ const targetRef = useRef<T | null>(null);
8
17
 
9
18
  // Iterate through the refs array, and set the ref.current value to the targetRef
10
19
  useEffect(() => {
11
- refs.forEach((ref: any) => {
20
+ refs.forEach((ref) => {
12
21
  if (!ref) {
13
22
  return;
14
23
  }
@@ -16,7 +25,7 @@ export const useCombinedRefs = (...refs: any) => {
16
25
  if (typeof ref === "function") {
17
26
  ref(targetRef.current);
18
27
  } else {
19
- ref.current = targetRef.current;
28
+ (ref as MutableRefObject<T | null>).current = targetRef.current;
20
29
  }
21
30
  });
22
31
  }, [refs]);
@@ -25,7 +34,7 @@ export const useCombinedRefs = (...refs: any) => {
25
34
  };
26
35
 
27
36
  interface Props {
28
- children: any;
37
+ children: ReactNode;
29
38
  }
30
39
 
31
40
  export const SimpleContent = forwardRef((props: Props, ref) => {
@@ -110,6 +110,20 @@ describe("NumberField", () => {
110
110
  expect(queryByText(/must be/)).toBeNull();
111
111
  });
112
112
 
113
+ it("shows decimal error for non-numeric characters in decimal type", () => {
114
+ const {getByText} = renderWithTheme(
115
+ <NumberField label="Decimal" onChange={noOp} type="decimal" value="12abc" />
116
+ );
117
+ expect(getByText("Value must be a decimal")).toBeTruthy();
118
+ });
119
+
120
+ it("shows decimal error for multiple decimal points", () => {
121
+ const {getByText} = renderWithTheme(
122
+ <NumberField label="Decimal" onChange={noOp} type="decimal" value="1.2.3" />
123
+ );
124
+ expect(getByText("Value must be a decimal")).toBeTruthy();
125
+ });
126
+
113
127
  it("syncs value when prop changes", async () => {
114
128
  const handleChange = mock((_value: string): void => {});
115
129
  const {getByDisplayValue, unmount} = renderWithTheme(
@@ -0,0 +1,70 @@
1
+ import {describe, expect, it} from "bun:test";
2
+
3
+ import {OfflineBanner} from "./OfflineBanner";
4
+ import {renderWithTheme} from "./test-utils";
5
+
6
+ describe("OfflineBanner", () => {
7
+ it("renders nothing when online and not syncing", () => {
8
+ const {toJSON} = renderWithTheme(
9
+ <OfflineBanner isOnline={true} isSyncing={false} queueLength={0} />
10
+ );
11
+ expect(toJSON()).toBeNull();
12
+ });
13
+
14
+ it("shows offline banner when not online", () => {
15
+ const {getByTestId, getByText} = renderWithTheme(
16
+ <OfflineBanner isOnline={false} isSyncing={false} queueLength={0} />
17
+ );
18
+ expect(getByTestId("offline-banner")).toBeTruthy();
19
+ expect(getByText("You're offline.")).toBeTruthy();
20
+ });
21
+
22
+ it("shows queue count in offline banner text", () => {
23
+ const {getByText} = renderWithTheme(
24
+ <OfflineBanner isOnline={false} isSyncing={false} queueLength={5} />
25
+ );
26
+ expect(
27
+ getByText("You're offline. 5 pending changes will sync when you reconnect.")
28
+ ).toBeTruthy();
29
+ });
30
+
31
+ it("shows singular 'change' for 1 pending", () => {
32
+ const {getByText} = renderWithTheme(
33
+ <OfflineBanner isOnline={false} isSyncing={false} queueLength={1} />
34
+ );
35
+ expect(
36
+ getByText("You're offline. 1 pending change will sync when you reconnect.")
37
+ ).toBeTruthy();
38
+ });
39
+
40
+ it("shows plural 'changes' for multiple pending", () => {
41
+ const {getByText} = renderWithTheme(
42
+ <OfflineBanner isOnline={false} isSyncing={false} queueLength={3} />
43
+ );
44
+ expect(
45
+ getByText("You're offline. 3 pending changes will sync when you reconnect.")
46
+ ).toBeTruthy();
47
+ });
48
+
49
+ it("shows syncing banner when isSyncing is true", () => {
50
+ const {getByTestId, getByText} = renderWithTheme(
51
+ <OfflineBanner isOnline={false} isSyncing={true} queueLength={2} />
52
+ );
53
+ expect(getByTestId("offline-banner")).toBeTruthy();
54
+ expect(getByText("Syncing offline changes...")).toBeTruthy();
55
+ });
56
+
57
+ it("uses custom testID prop", () => {
58
+ const {getByTestId} = renderWithTheme(
59
+ <OfflineBanner isOnline={false} isSyncing={false} queueLength={0} testID="custom-banner" />
60
+ );
61
+ expect(getByTestId("custom-banner")).toBeTruthy();
62
+ });
63
+
64
+ it("uses custom testID prop while syncing", () => {
65
+ const {getByTestId} = renderWithTheme(
66
+ <OfflineBanner isOnline={false} isSyncing={true} queueLength={2} testID="custom-banner" />
67
+ );
68
+ expect(getByTestId("custom-banner")).toBeTruthy();
69
+ });
70
+ });
@@ -0,0 +1,54 @@
1
+ import type React from "react";
2
+
3
+ import {Banner} from "./Banner";
4
+ import {Box} from "./Box";
5
+
6
+ export interface OfflineBannerProps {
7
+ /** Whether the server is currently reachable */
8
+ isOnline: boolean;
9
+ /** Number of mutations waiting to be synced */
10
+ queueLength: number;
11
+ /** Whether mutations are currently being replayed */
12
+ isSyncing: boolean;
13
+ /** testID for the root element */
14
+ testID?: string;
15
+ }
16
+
17
+ /**
18
+ * Displays offline/syncing status banners. Renders nothing when online and idle.
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * const {isOnline, queueLength, isSyncing} = useServerStatus();
23
+ * <OfflineBanner isOnline={isOnline} queueLength={queueLength} isSyncing={isSyncing} />
24
+ * ```
25
+ */
26
+ export const OfflineBanner: React.FC<OfflineBannerProps> = ({
27
+ isOnline,
28
+ queueLength,
29
+ isSyncing,
30
+ testID = "offline-banner",
31
+ }) => {
32
+ if (isSyncing) {
33
+ return (
34
+ <Box marginBottom={4} testID={testID}>
35
+ <Banner id="syncing-status" status="info" text="Syncing offline changes..." />
36
+ </Box>
37
+ );
38
+ }
39
+
40
+ if (!isOnline) {
41
+ const suffix =
42
+ queueLength > 0
43
+ ? ` ${queueLength} pending change${queueLength !== 1 ? "s" : ""} will sync when you reconnect.`
44
+ : "";
45
+
46
+ return (
47
+ <Box marginBottom={4} testID={testID}>
48
+ <Banner id="offline-status" status="warning" text={`You're offline.${suffix}`} />
49
+ </Box>
50
+ );
51
+ }
52
+
53
+ return null;
54
+ };
@@ -8,6 +8,7 @@ import type {
8
8
  OpenAPIContextType,
9
9
  OpenAPIProviderProps,
10
10
  OpenAPISpec,
11
+ OpenApiProperty,
11
12
  } from "./Common";
12
13
 
13
14
  const OpenAPIContext = createContext<OpenAPIContextType | null>(null);
@@ -39,7 +40,7 @@ export const OpenAPIProvider = ({children, specUrl}: OpenAPIProviderProps): Reac
39
40
  }
40
41
 
41
42
  for (const dotField of dotFields.slice(1)) {
42
- field = (field?.properties as any)?.[dotField];
43
+ field = (field?.properties as Record<string, OpenApiProperty> | undefined)?.[dotField];
43
44
  }
44
45
  return field;
45
46
  };
@@ -55,7 +56,7 @@ export const OpenAPIProvider = ({children, specUrl}: OpenAPIProviderProps): Reac
55
56
  const data = (await response.json()) as OpenAPISpec;
56
57
  setSpec(data);
57
58
  })
58
- .catch((error: any) => console.error(`Error fetching OpenAPI spec: ${error}`));
59
+ .catch((error: unknown) => console.error(`Error fetching OpenAPI spec: ${String(error)}`));
59
60
  }, [specUrl]);
60
61
 
61
62
  return (
package/src/Page.tsx CHANGED
@@ -11,6 +11,7 @@ import {Spinner} from "./Spinner";
11
11
  import {Text} from "./Text";
12
12
 
13
13
  export class Page extends React.Component<PageProps, {}> {
14
+ // biome-ignore lint/suspicious/noExplicitAny: ActionSheet class is defined in ActionSheet.tsx which imports from Common.ts indirectly; using its type here would create a circular dependency
14
15
  actionSheetRef: React.RefObject<any> = React.createRef();
15
16
 
16
17
  renderHeader() {
@@ -134,7 +134,7 @@ export const Pagination: FC<PaginationProps> = ({totalPages, page, setPage}) =>
134
134
  alignItems: "center",
135
135
  display: "flex",
136
136
  flexDirection: "row",
137
- gap: theme.spacing.xs as any,
137
+ gap: theme.spacing.xs,
138
138
  }}
139
139
  >
140
140
  <PaginationButton
@@ -10,6 +10,7 @@ export async function requestPermissions(_kind: PermissionKind): Promise<Permiss
10
10
  // const userPropertyKey = `PermissionsFor${capitalize(kind)}`;
11
11
 
12
12
  // let k = kind;
13
+ // // noExplicitAny: Dead commented-out code; types cannot be resolved without the full uncommented context and Permissions library types
13
14
  // let options: any = undefined;
14
15
  // if (kind === "locationAlways") {
15
16
  // k = "location";
@@ -21,6 +22,7 @@ export async function requestPermissions(_kind: PermissionKind): Promise<Permiss
21
22
 
22
23
  // // TODO check soft request status.
23
24
 
25
+ // // noExplicitAny: Dead commented-out code; MAP[k] type depends on unreferenced MAP constant
24
26
  // const current = await Permissions.check(MAP[k] as any);
25
27
  // // Tracking.log(`[permissions] ${k} permissions are ${current}`);
26
28
  // if (current === "denied" || current === "limited") {
@@ -31,6 +33,7 @@ export async function requestPermissions(_kind: PermissionKind): Promise<Permiss
31
33
  // return resolve("authorized");
32
34
  // }
33
35
 
36
+ // // noExplicitAny: Dead commented-out code; MAP[k] type depends on unreferenced MAP constant
34
37
  // const response = await Permissions.request(MAP[k] as any, options);
35
38
  // if (response === "granted") {
36
39
  // // Tracking.setUserProperty(userPropertyKey, "true");
@@ -30,6 +30,7 @@ import {
30
30
  Keyboard,
31
31
  Modal,
32
32
  type ModalProps,
33
+ type NativeSyntheticEvent,
33
34
  Platform,
34
35
  Pressable,
35
36
  type PressableProps,
@@ -137,9 +138,7 @@ export function RNPickerSelect({
137
138
  InputAccessoryView,
138
139
  }: RNPickerSelectProps) {
139
140
  const [showPicker, setShowPicker] = useState<boolean>(false);
140
- const [animationType, setAnimationType] = useState<"none" | "slide" | "fade" | undefined>(
141
- undefined
142
- );
141
+ const [animationType, setAnimationType] = useState<ModalProps["animationType"]>(undefined);
143
142
  const [orientation, setOrientation] = useState<"portrait" | "landscape">("portrait");
144
143
  const [doneDepressed, setDoneDepressed] = useState<boolean>(false);
145
144
  const {theme} = useTheme();
@@ -185,7 +184,7 @@ export function RNPickerSelect({
185
184
  }
186
185
  return {
187
186
  idx,
188
- selectedItem: options[idx] || {},
187
+ selectedItem: (options[idx] || {}) as Partial<PickerSelectItem>,
189
188
  };
190
189
  },
191
190
  [options]
@@ -217,9 +216,7 @@ export function RNPickerSelect({
217
216
 
218
217
  const onOrientationChange = ({
219
218
  nativeEvent,
220
- }: {
221
- nativeEvent: {orientation: "portrait" | "landscape"};
222
- }) => {
219
+ }: NativeSyntheticEvent<{orientation: "portrait" | "landscape"}>) => {
223
220
  setOrientation(nativeEvent.orientation);
224
221
  };
225
222
 
@@ -256,12 +253,13 @@ export function RNPickerSelect({
256
253
 
257
254
  const renderPickerItems = () => {
258
255
  return options?.map((item) => {
256
+ if (!item) return null;
259
257
  return (
260
258
  <Picker.Item
261
- color={item?.color}
262
- key={item?.key || item?.label}
263
- label={item?.label}
264
- value={item?.value}
259
+ color={item.color}
260
+ key={item.key || item.label}
261
+ label={item.label}
262
+ value={item.value}
265
263
  />
266
264
  );
267
265
  });
@@ -484,9 +482,14 @@ export function RNPickerSelect({
484
482
  };
485
483
 
486
484
  const renderAndroidHeadless = () => {
487
- // noExplicitAny: Component is View or Pressable depending on a bug workaround flag. View
488
- // ignores Pressable-specific props (onPress) at runtime. A type-safe union cannot express this.
489
- const Component = (fixAndroidTouchableBug ? View : Pressable) as unknown as typeof Pressable;
485
+ // `View` and `Pressable` accept disjoint prop sets; the fork swaps between them to work
486
+ // around an Android touchable bug, so we cast to a structural component type that accepts
487
+ // the union of props actually used in JSX below.
488
+ const Component = (fixAndroidTouchableBug ? View : Pressable) as ComponentType<{
489
+ onPress?: PressableProps["onPress"];
490
+ testID?: string;
491
+ children?: ReactNode;
492
+ }>;
490
493
  return (
491
494
  <Component onPress={onOpen} testID="android_touchable_wrapper" {...touchableWrapperProps}>
492
495
  <View>
@@ -1,3 +1,4 @@
1
+ // biome-ignore-all lint/suspicious/noExplicitAny: test mock typing
1
2
  import {describe, expect, it, mock} from "bun:test";
2
3
  import {act, fireEvent} from "@testing-library/react-native";
3
4
 
@@ -19,7 +19,7 @@ export const SelectField: FC<SelectFieldProps> = ({
19
19
  const clearOption = {label: placeholder ?? "---", value: ""};
20
20
 
21
21
  return (
22
- <View>
22
+ <View style={{width: "100%"}}>
23
23
  {Boolean(title) && <FieldTitle text={title!} />}
24
24
  {Boolean(errorText) && <FieldError text={errorText!} />}
25
25
  <RNPickerSelect
@@ -1,3 +1,4 @@
1
+ // biome-ignore-all lint/suspicious/noExplicitAny: test mock typing
1
2
  import {describe, expect, it, mock} from "bun:test";
2
3
  import {fireEvent} from "@testing-library/react-native";
3
4
  import {forwardRef, useImperativeHandle} from "react";
@@ -34,6 +34,7 @@ export const SplitPage = ({
34
34
  const {width} = Dimensions.get("window");
35
35
 
36
36
  const onItemSelect = useCallback(
37
+ // biome-ignore lint/suspicious/noExplicitAny: SplitPage accepts heterogeneous list item shapes from consumers; the generic propagates from listViewData
37
38
  async (item: ListRenderItemInfo<any>) => {
38
39
  setSelectedId(item.index);
39
40
  await onSelectionChange(item);
@@ -58,6 +59,7 @@ export const SplitPage = ({
58
59
  return null;
59
60
  }
60
61
 
62
+ // biome-ignore lint/suspicious/noExplicitAny: SplitPage accepts heterogeneous list item shapes from consumers; the generic propagates from listViewData
61
63
  const renderItem = (itemInfo: ListRenderItemInfo<any>) => {
62
64
  return (
63
65
  <Box