@terreno/ui 0.7.0 → 0.7.1

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 (31) hide show
  1. package/dist/AiSuggestionBox.d.ts +6 -0
  2. package/dist/AiSuggestionBox.js +87 -0
  3. package/dist/AiSuggestionBox.js.map +1 -0
  4. package/dist/Common.d.ts +12 -0
  5. package/dist/Common.js.map +1 -1
  6. package/dist/TextField.js +46 -41
  7. package/dist/TextField.js.map +1 -1
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.js +1 -0
  10. package/dist/index.js.map +1 -1
  11. package/package.json +1 -1
  12. package/src/AiSuggestionBox.test.tsx +373 -0
  13. package/src/AiSuggestionBox.tsx +233 -0
  14. package/src/Common.ts +14 -0
  15. package/src/TextField.tsx +87 -70
  16. package/src/__snapshots__/AddressField.test.tsx.snap +208 -156
  17. package/src/__snapshots__/AiSuggestionBox.test.tsx.snap +1031 -0
  18. package/src/__snapshots__/CustomSelectField.test.tsx.snap +51 -38
  19. package/src/__snapshots__/EmailField.test.tsx.snap +111 -85
  20. package/src/__snapshots__/Field.test.tsx.snap +616 -460
  21. package/src/__snapshots__/MobileAddressAutoComplete.test.tsx.snap +51 -38
  22. package/src/__snapshots__/NumberField.test.tsx.snap +51 -38
  23. package/src/__snapshots__/PhoneNumberField.test.tsx.snap +264 -199
  24. package/src/__snapshots__/TapToEdit.test.tsx.snap +51 -38
  25. package/src/__snapshots__/TextArea.test.tsx.snap +255 -190
  26. package/src/__snapshots__/TextField.test.tsx.snap +264 -199
  27. package/src/__snapshots__/UnifiedAddressAutoComplete.test.tsx.snap +204 -152
  28. package/src/__snapshots__/WebAddressAutocomplete.test.tsx.snap +153 -114
  29. package/src/index.tsx +1 -0
  30. package/src/login/__snapshots__/LoginScreen.test.tsx.snap +104 -78
  31. package/src/signUp/__snapshots__/SignUpScreen.test.tsx.snap +156 -117
@@ -0,0 +1,373 @@
1
+ import {describe, expect, it, mock} from "bun:test";
2
+ import {fireEvent} from "@testing-library/react-native";
3
+
4
+ import {TextArea} from "./TextArea";
5
+ import {renderWithTheme} from "./test-utils";
6
+
7
+ describe("AiSuggestionBox in TextArea", () => {
8
+ describe("not-started state", () => {
9
+ it("should render default not-started text", () => {
10
+ const {getByText} = renderWithTheme(
11
+ <TextArea
12
+ aiSuggestion={{onAdd: () => {}, status: "not-started"}}
13
+ onChange={() => {}}
14
+ value=""
15
+ />
16
+ );
17
+
18
+ expect(getByText("AI note will be generated once the session ends.")).toBeTruthy();
19
+ });
20
+
21
+ it("should render custom not-started text", () => {
22
+ const {getByText} = renderWithTheme(
23
+ <TextArea
24
+ aiSuggestion={{
25
+ notStartedText: "Custom pending message",
26
+ onAdd: () => {},
27
+ status: "not-started",
28
+ }}
29
+ onChange={() => {}}
30
+ value=""
31
+ />
32
+ );
33
+
34
+ expect(getByText("Custom pending message")).toBeTruthy();
35
+ });
36
+ });
37
+
38
+ describe("generating state", () => {
39
+ it("should render default generating text", () => {
40
+ const {getByText} = renderWithTheme(
41
+ <TextArea
42
+ aiSuggestion={{onAdd: () => {}, status: "generating"}}
43
+ onChange={() => {}}
44
+ value=""
45
+ />
46
+ );
47
+
48
+ expect(getByText("AI note generation in progress...")).toBeTruthy();
49
+ });
50
+
51
+ it("should render custom generating text", () => {
52
+ const {getByText} = renderWithTheme(
53
+ <TextArea
54
+ aiSuggestion={{
55
+ generatingText: "Thinking...",
56
+ onAdd: () => {},
57
+ status: "generating",
58
+ }}
59
+ onChange={() => {}}
60
+ value=""
61
+ />
62
+ );
63
+
64
+ expect(getByText("Thinking...")).toBeTruthy();
65
+ });
66
+ });
67
+
68
+ describe("ready state", () => {
69
+ it("should render suggestion text", () => {
70
+ const {getByText} = renderWithTheme(
71
+ <TextArea
72
+ aiSuggestion={{
73
+ onAdd: () => {},
74
+ status: "ready",
75
+ text: "This is a suggestion.",
76
+ }}
77
+ onChange={() => {}}
78
+ value=""
79
+ />
80
+ );
81
+
82
+ expect(getByText("AI-generated note")).toBeTruthy();
83
+ expect(getByText("This is a suggestion.")).toBeTruthy();
84
+ });
85
+
86
+ it("should render Hide and Add to note buttons", () => {
87
+ const {getByText} = renderWithTheme(
88
+ <TextArea
89
+ aiSuggestion={{
90
+ onAdd: () => {},
91
+ status: "ready",
92
+ text: "Suggestion text",
93
+ }}
94
+ onChange={() => {}}
95
+ value=""
96
+ />
97
+ );
98
+
99
+ expect(getByText("Hide")).toBeTruthy();
100
+ expect(getByText("Add to note")).toBeTruthy();
101
+ });
102
+
103
+ it("should call onAdd when Add to note is pressed", () => {
104
+ const mockOnAdd = mock(() => {});
105
+ const {getByLabelText} = renderWithTheme(
106
+ <TextArea
107
+ aiSuggestion={{
108
+ onAdd: mockOnAdd,
109
+ status: "ready",
110
+ text: "Suggestion text",
111
+ }}
112
+ onChange={() => {}}
113
+ testID="test"
114
+ value=""
115
+ />
116
+ );
117
+
118
+ fireEvent.press(getByLabelText("Add to note"));
119
+ expect(mockOnAdd).toHaveBeenCalledTimes(1);
120
+ });
121
+
122
+ it("should collapse when Hide is pressed", () => {
123
+ const {getByLabelText, getByText, queryByText} = renderWithTheme(
124
+ <TextArea
125
+ aiSuggestion={{
126
+ onAdd: () => {},
127
+ status: "ready",
128
+ text: "Suggestion text",
129
+ }}
130
+ onChange={() => {}}
131
+ testID="test"
132
+ value=""
133
+ />
134
+ );
135
+
136
+ expect(getByText("Suggestion text")).toBeTruthy();
137
+
138
+ fireEvent.press(getByLabelText("Hide suggestion"));
139
+
140
+ expect(queryByText("Suggestion text")).toBeNull();
141
+ expect(getByText("AI-generated note (hidden)")).toBeTruthy();
142
+ expect(getByText("Show")).toBeTruthy();
143
+ });
144
+
145
+ it("should expand when Show is pressed after collapsing", () => {
146
+ const {getByLabelText, getByText} = renderWithTheme(
147
+ <TextArea
148
+ aiSuggestion={{
149
+ onAdd: () => {},
150
+ status: "ready",
151
+ text: "Suggestion text",
152
+ }}
153
+ onChange={() => {}}
154
+ testID="test"
155
+ value=""
156
+ />
157
+ );
158
+
159
+ fireEvent.press(getByLabelText("Hide suggestion"));
160
+ expect(getByText("AI-generated note (hidden)")).toBeTruthy();
161
+
162
+ fireEvent.press(getByLabelText("Show suggestion"));
163
+ expect(getByText("Suggestion text")).toBeTruthy();
164
+ expect(getByText("AI-generated note")).toBeTruthy();
165
+ });
166
+
167
+ it("should call onHide when Hide is pressed", () => {
168
+ const mockOnHide = mock(() => {});
169
+ const {getByLabelText} = renderWithTheme(
170
+ <TextArea
171
+ aiSuggestion={{
172
+ onAdd: () => {},
173
+ onHide: mockOnHide,
174
+ status: "ready",
175
+ text: "Suggestion text",
176
+ }}
177
+ onChange={() => {}}
178
+ testID="test"
179
+ value=""
180
+ />
181
+ );
182
+
183
+ fireEvent.press(getByLabelText("Hide suggestion"));
184
+ expect(mockOnHide).toHaveBeenCalledTimes(1);
185
+ });
186
+
187
+ it("should call onShow when Show is pressed", () => {
188
+ const mockOnShow = mock(() => {});
189
+ const {getByLabelText} = renderWithTheme(
190
+ <TextArea
191
+ aiSuggestion={{
192
+ onAdd: () => {},
193
+ onShow: mockOnShow,
194
+ status: "ready",
195
+ text: "Suggestion text",
196
+ }}
197
+ onChange={() => {}}
198
+ testID="test"
199
+ value=""
200
+ />
201
+ );
202
+
203
+ fireEvent.press(getByLabelText("Hide suggestion"));
204
+ fireEvent.press(getByLabelText("Show suggestion"));
205
+ expect(mockOnShow).toHaveBeenCalledTimes(1);
206
+ });
207
+ });
208
+
209
+ describe("added state", () => {
210
+ it("should render added heading", () => {
211
+ const {getByText} = renderWithTheme(
212
+ <TextArea
213
+ aiSuggestion={{
214
+ onAdd: () => {},
215
+ status: "added",
216
+ text: "Added suggestion",
217
+ }}
218
+ onChange={() => {}}
219
+ value=""
220
+ />
221
+ );
222
+
223
+ expect(getByText("AI-generated note added!")).toBeTruthy();
224
+ expect(getByText("Added suggestion")).toBeTruthy();
225
+ });
226
+ });
227
+
228
+ describe("feedback", () => {
229
+ it("should call onFeedback with 'like' when thumbs up is pressed", () => {
230
+ const mockOnFeedback = mock(() => {});
231
+ const {getByLabelText} = renderWithTheme(
232
+ <TextArea
233
+ aiSuggestion={{
234
+ feedback: null,
235
+ onAdd: () => {},
236
+ onFeedback: mockOnFeedback,
237
+ status: "ready",
238
+ text: "Suggestion",
239
+ }}
240
+ onChange={() => {}}
241
+ testID="test"
242
+ value=""
243
+ />
244
+ );
245
+
246
+ fireEvent.press(getByLabelText("Thumbs up"));
247
+ expect(mockOnFeedback).toHaveBeenCalledWith("like");
248
+ });
249
+
250
+ it("should call onFeedback with null when thumbs up is pressed while already liked", () => {
251
+ const mockOnFeedback = mock(() => {});
252
+ const {getByLabelText} = renderWithTheme(
253
+ <TextArea
254
+ aiSuggestion={{
255
+ feedback: "like",
256
+ onAdd: () => {},
257
+ onFeedback: mockOnFeedback,
258
+ status: "ready",
259
+ text: "Suggestion",
260
+ }}
261
+ onChange={() => {}}
262
+ testID="test"
263
+ value=""
264
+ />
265
+ );
266
+
267
+ fireEvent.press(getByLabelText("Thumbs up"));
268
+ expect(mockOnFeedback).toHaveBeenCalledWith(null);
269
+ });
270
+
271
+ it("should call onFeedback with 'dislike' when thumbs down is pressed", () => {
272
+ const mockOnFeedback = mock(() => {});
273
+ const {getByLabelText} = renderWithTheme(
274
+ <TextArea
275
+ aiSuggestion={{
276
+ feedback: null,
277
+ onAdd: () => {},
278
+ onFeedback: mockOnFeedback,
279
+ status: "ready",
280
+ text: "Suggestion",
281
+ }}
282
+ onChange={() => {}}
283
+ testID="test"
284
+ value=""
285
+ />
286
+ );
287
+
288
+ fireEvent.press(getByLabelText("Thumbs down"));
289
+ expect(mockOnFeedback).toHaveBeenCalledWith("dislike");
290
+ });
291
+ });
292
+
293
+ describe("testID propagation", () => {
294
+ it("should apply testIDs to interactive elements", () => {
295
+ const {getByTestId} = renderWithTheme(
296
+ <TextArea
297
+ aiSuggestion={{
298
+ onAdd: () => {},
299
+ status: "ready",
300
+ text: "Suggestion",
301
+ }}
302
+ onChange={() => {}}
303
+ testID="notes"
304
+ value=""
305
+ />
306
+ );
307
+
308
+ expect(getByTestId("notes-ai-suggestion")).toBeTruthy();
309
+ expect(getByTestId("notes-ai-suggestion-thumbs-up")).toBeTruthy();
310
+ expect(getByTestId("notes-ai-suggestion-thumbs-down")).toBeTruthy();
311
+ expect(getByTestId("notes-ai-suggestion-hide")).toBeTruthy();
312
+ expect(getByTestId("notes-ai-suggestion-add")).toBeTruthy();
313
+ });
314
+ });
315
+
316
+ describe("snapshots", () => {
317
+ it("should match snapshot for not-started state", () => {
318
+ const component = renderWithTheme(
319
+ <TextArea
320
+ aiSuggestion={{onAdd: () => {}, status: "not-started"}}
321
+ onChange={() => {}}
322
+ value=""
323
+ />
324
+ );
325
+ expect(component.toJSON()).toMatchSnapshot();
326
+ });
327
+
328
+ it("should match snapshot for generating state", () => {
329
+ const component = renderWithTheme(
330
+ <TextArea
331
+ aiSuggestion={{onAdd: () => {}, status: "generating"}}
332
+ onChange={() => {}}
333
+ value=""
334
+ />
335
+ );
336
+ expect(component.toJSON()).toMatchSnapshot();
337
+ });
338
+
339
+ it("should match snapshot for ready state", () => {
340
+ const component = renderWithTheme(
341
+ <TextArea
342
+ aiSuggestion={{
343
+ feedback: null,
344
+ onAdd: () => {},
345
+ status: "ready",
346
+ text: "AI-generated suggestion text.",
347
+ }}
348
+ onChange={() => {}}
349
+ testID="test"
350
+ value=""
351
+ />
352
+ );
353
+ expect(component.toJSON()).toMatchSnapshot();
354
+ });
355
+
356
+ it("should match snapshot for added state", () => {
357
+ const component = renderWithTheme(
358
+ <TextArea
359
+ aiSuggestion={{
360
+ feedback: "like",
361
+ onAdd: () => {},
362
+ status: "added",
363
+ text: "AI-generated suggestion text.",
364
+ }}
365
+ onChange={() => {}}
366
+ testID="test"
367
+ value=""
368
+ />
369
+ );
370
+ expect(component.toJSON()).toMatchSnapshot();
371
+ });
372
+ });
373
+ });
@@ -0,0 +1,233 @@
1
+ import {type FC, useCallback, useEffect, useState} from "react";
2
+ import {Pressable, View} from "react-native";
3
+
4
+ import type {AiSuggestionProps} from "./Common";
5
+ import {Icon} from "./Icon";
6
+ import {Text} from "./Text";
7
+ import {useTheme} from "./Theme";
8
+
9
+ export interface AiSuggestionBoxProps extends AiSuggestionProps {
10
+ testID?: string;
11
+ }
12
+
13
+ export const AiSuggestionBox: FC<AiSuggestionBoxProps> = ({
14
+ status,
15
+ text,
16
+ onAdd,
17
+ onHide,
18
+ onShow,
19
+ onFeedback,
20
+ feedback,
21
+ notStartedText = "AI note will be generated once the session ends.",
22
+ generatingText = "AI note generation in progress...",
23
+ testID,
24
+ }) => {
25
+ const {theme} = useTheme();
26
+ const [expanded, setExpanded] = useState(true);
27
+
28
+ // Re-expand when a new suggestion arrives or is added
29
+ useEffect(() => {
30
+ if (status === "ready" || status === "added") {
31
+ setExpanded(true);
32
+ }
33
+ }, [status]);
34
+
35
+ const isAdded = status === "added";
36
+
37
+ const backgroundColor = isAdded
38
+ ? theme.surface.successLight
39
+ : status === "not-started"
40
+ ? theme.primitives.neutral050
41
+ : theme.primitives.primary000;
42
+
43
+ const borderColor = isAdded
44
+ ? "#9BE7B2"
45
+ : status === "not-started"
46
+ ? theme.surface.secondaryLight
47
+ : theme.primitives.primary100;
48
+
49
+ const containerStyle = {
50
+ backgroundColor,
51
+ borderColor,
52
+ borderRadius: 8,
53
+ borderWidth: 1,
54
+ gap: 8,
55
+ padding: 8,
56
+ width: "100%" as const,
57
+ };
58
+
59
+ const headingText =
60
+ status === "not-started"
61
+ ? notStartedText
62
+ : status === "generating"
63
+ ? generatingText
64
+ : isAdded
65
+ ? "AI-generated note added!"
66
+ : expanded
67
+ ? "AI-generated note"
68
+ : "AI-generated note (hidden)";
69
+
70
+ const handleHide = useCallback(() => {
71
+ setExpanded(false);
72
+ onHide?.();
73
+ }, [onHide]);
74
+
75
+ const handleShow = useCallback(() => {
76
+ setExpanded(true);
77
+ onShow?.();
78
+ }, [onShow]);
79
+
80
+ const handleThumbsUp = useCallback(() => {
81
+ if (!onFeedback) {
82
+ return;
83
+ }
84
+ onFeedback(feedback === "like" ? null : "like");
85
+ }, [onFeedback, feedback]);
86
+
87
+ const handleThumbsDown = useCallback(() => {
88
+ if (!onFeedback) {
89
+ return;
90
+ }
91
+ onFeedback(feedback === "dislike" ? null : "dislike");
92
+ }, [onFeedback, feedback]);
93
+
94
+ const renderFeedback = () => (
95
+ <View
96
+ style={{alignItems: "center", flexDirection: "row"}}
97
+ testID={testID ? `${testID}-feedback` : undefined}
98
+ >
99
+ <Pressable
100
+ accessibilityLabel="Thumbs up"
101
+ accessibilityRole="button"
102
+ onPress={handleThumbsUp}
103
+ style={{alignItems: "center", height: 24, justifyContent: "center", width: 24}}
104
+ testID={testID ? `${testID}-thumbs-up` : undefined}
105
+ >
106
+ <Icon
107
+ color={feedback === "like" ? "secondaryDark" : "secondaryLight"}
108
+ iconName="thumbs-up"
109
+ size="xs"
110
+ type={feedback === "like" ? "solid" : "regular"}
111
+ />
112
+ </Pressable>
113
+ <Pressable
114
+ accessibilityLabel="Thumbs down"
115
+ accessibilityRole="button"
116
+ onPress={handleThumbsDown}
117
+ style={{alignItems: "center", height: 24, justifyContent: "center", width: 24}}
118
+ testID={testID ? `${testID}-thumbs-down` : undefined}
119
+ >
120
+ <Icon
121
+ color={feedback === "dislike" ? "secondaryDark" : "secondaryLight"}
122
+ iconName="thumbs-down"
123
+ size="xs"
124
+ type={feedback === "dislike" ? "solid" : "regular"}
125
+ />
126
+ </Pressable>
127
+ </View>
128
+ );
129
+
130
+ if (status === "not-started" || status === "generating") {
131
+ return (
132
+ <View style={containerStyle} testID={testID}>
133
+ <View style={{alignItems: "center", flexDirection: "row", gap: 4, width: "100%"}}>
134
+ <Icon color="secondaryDark" iconName="wand-magic-sparkles" size="xs" />
135
+ <View style={{flex: 1}}>
136
+ <Text color="secondaryDark" size="sm">
137
+ {headingText}
138
+ </Text>
139
+ </View>
140
+ </View>
141
+ </View>
142
+ );
143
+ }
144
+
145
+ if (!expanded) {
146
+ return (
147
+ <View style={{...containerStyle, flexDirection: "column"}} testID={testID}>
148
+ <View style={{alignItems: "center", flexDirection: "row", gap: 4, width: "100%"}}>
149
+ <Icon color="secondaryDark" iconName="wand-magic-sparkles" size="xs" />
150
+ <View style={{flex: 1}}>
151
+ <Text color="secondaryDark" size="sm">
152
+ {headingText}
153
+ </Text>
154
+ </View>
155
+ <View style={{alignItems: "center", flexDirection: "row", gap: 4}}>
156
+ <Pressable
157
+ accessibilityLabel="Show suggestion"
158
+ accessibilityRole="button"
159
+ onPress={handleShow}
160
+ style={{height: 28, justifyContent: "center", paddingHorizontal: 16}}
161
+ testID={testID ? `${testID}-show` : undefined}
162
+ >
163
+ <Text bold color="secondaryDark" size="sm">
164
+ Show
165
+ </Text>
166
+ </Pressable>
167
+ {renderFeedback()}
168
+ </View>
169
+ </View>
170
+ </View>
171
+ );
172
+ }
173
+
174
+ return (
175
+ <View style={{...containerStyle, alignItems: "flex-end"}} testID={testID}>
176
+ <View style={{alignItems: "center", flexDirection: "row", gap: 4, width: "100%"}}>
177
+ <Icon color="secondaryDark" iconName="wand-magic-sparkles" size="xs" />
178
+ <View style={{flex: 1}}>
179
+ <Text color="secondaryDark" size="sm">
180
+ {headingText}
181
+ </Text>
182
+ </View>
183
+ {renderFeedback()}
184
+ </View>
185
+
186
+ {Boolean(text) && (
187
+ <View style={{paddingBottom: 4, width: "100%"}}>
188
+ <Text size="md">{text}</Text>
189
+ </View>
190
+ )}
191
+
192
+ <View
193
+ style={{
194
+ alignItems: "center",
195
+ flexDirection: "row",
196
+ gap: 8,
197
+ justifyContent: "flex-end",
198
+ width: "100%",
199
+ }}
200
+ >
201
+ <Pressable
202
+ accessibilityLabel="Hide suggestion"
203
+ accessibilityRole="button"
204
+ onPress={handleHide}
205
+ style={{height: 28, justifyContent: "center", paddingHorizontal: 16}}
206
+ testID={testID ? `${testID}-hide` : undefined}
207
+ >
208
+ <Text bold color="secondaryDark" size="sm">
209
+ Hide
210
+ </Text>
211
+ </Pressable>
212
+ <Pressable
213
+ accessibilityLabel="Add to note"
214
+ accessibilityRole="button"
215
+ onPress={onAdd}
216
+ style={{
217
+ alignItems: "center",
218
+ backgroundColor: theme.surface.secondaryDark,
219
+ borderRadius: 360,
220
+ height: 28,
221
+ justifyContent: "center",
222
+ paddingHorizontal: 16,
223
+ }}
224
+ testID={testID ? `${testID}-add` : undefined}
225
+ >
226
+ <Text bold color="inverted" size="sm">
227
+ Add to note
228
+ </Text>
229
+ </Pressable>
230
+ </View>
231
+ </View>
232
+ );
233
+ };
package/src/Common.ts CHANGED
@@ -630,6 +630,18 @@ export interface ErrorTextProps {
630
630
  errorText?: string;
631
631
  }
632
632
 
633
+ export interface AiSuggestionProps {
634
+ status: "not-started" | "generating" | "ready" | "added";
635
+ text?: string;
636
+ onAdd?: () => void;
637
+ onHide?: () => void;
638
+ onShow?: () => void;
639
+ onFeedback?: (feedback: "like" | "dislike" | null) => void;
640
+ feedback?: "like" | "dislike" | null;
641
+ notStartedText?: string;
642
+ generatingText?: string;
643
+ }
644
+
633
645
  export interface TextFieldProps extends BaseFieldProps, HelperTextProps, ErrorTextProps {
634
646
  type?: "email" | "password" | "phoneNumber" | "search" | "text" | "url";
635
647
 
@@ -642,6 +654,8 @@ export interface TextFieldProps extends BaseFieldProps, HelperTextProps, ErrorTe
642
654
 
643
655
  inputRef?: any;
644
656
  trimOnBlur?: boolean;
657
+
658
+ aiSuggestion?: AiSuggestionProps;
645
659
  }
646
660
 
647
661
  export interface TextAreaProps extends Omit<TextFieldProps, "multiline" | "type"> {}