@bbearai/react-native 0.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/dist/index.mjs ADDED
@@ -0,0 +1,1640 @@
1
+ // src/BugBearProvider.tsx
2
+ import React, { createContext, useContext, useEffect, useState } from "react";
3
+ import {
4
+ createBugBear
5
+ } from "@bbearai/core";
6
+ import { Platform, Dimensions } from "react-native";
7
+ var BugBearContext = createContext({
8
+ client: null,
9
+ isTester: false,
10
+ isQAEnabled: false,
11
+ shouldShowWidget: false,
12
+ testerInfo: null,
13
+ assignments: [],
14
+ currentAssignment: null,
15
+ refreshAssignments: async () => {
16
+ },
17
+ isLoading: true,
18
+ getDeviceInfo: () => ({ platform: "ios" }),
19
+ onNavigate: void 0,
20
+ threads: [],
21
+ unreadCount: 0,
22
+ refreshThreads: async () => {
23
+ },
24
+ getThreadMessages: async () => [],
25
+ sendMessage: async () => false,
26
+ markAsRead: async () => {
27
+ }
28
+ });
29
+ function useBugBear() {
30
+ return useContext(BugBearContext);
31
+ }
32
+ function BugBearProvider({ config, children, appVersion }) {
33
+ const [client] = useState(() => createBugBear(config));
34
+ const [isTester, setIsTester] = useState(false);
35
+ const [isQAEnabled, setIsQAEnabled] = useState(false);
36
+ const [testerInfo, setTesterInfo] = useState(null);
37
+ const [assignments, setAssignments] = useState([]);
38
+ const [isLoading, setIsLoading] = useState(true);
39
+ const [threads, setThreads] = useState([]);
40
+ const [unreadCount, setUnreadCount] = useState(0);
41
+ const getDeviceInfo = () => {
42
+ const { width, height } = Dimensions.get("window");
43
+ return {
44
+ platform: Platform.OS,
45
+ osVersion: Platform.Version?.toString(),
46
+ appVersion,
47
+ screenSize: { width, height }
48
+ };
49
+ };
50
+ const refreshAssignments = async () => {
51
+ const newAssignments = await client.getAssignedTests();
52
+ setAssignments(newAssignments);
53
+ };
54
+ const refreshThreads = async () => {
55
+ const newThreads = await client.getThreadsForTester();
56
+ setThreads(newThreads);
57
+ const totalUnread = newThreads.reduce((sum, t) => sum + t.unreadCount, 0);
58
+ setUnreadCount(totalUnread);
59
+ };
60
+ const getThreadMessages = async (threadId) => {
61
+ return client.getThreadMessages(threadId);
62
+ };
63
+ const sendMessage = async (threadId, content) => {
64
+ const success = await client.sendMessage(threadId, content);
65
+ if (success) {
66
+ await refreshThreads();
67
+ }
68
+ return success;
69
+ };
70
+ const markAsRead = async (threadId) => {
71
+ await client.markThreadAsRead(threadId);
72
+ await refreshThreads();
73
+ };
74
+ useEffect(() => {
75
+ const init = async () => {
76
+ setIsLoading(true);
77
+ try {
78
+ const [qaEnabled, info] = await Promise.all([
79
+ client.isQAEnabled(),
80
+ client.getTesterInfo()
81
+ ]);
82
+ setIsQAEnabled(qaEnabled);
83
+ setTesterInfo(info);
84
+ setIsTester(!!info);
85
+ if (info && qaEnabled) {
86
+ await Promise.all([
87
+ refreshAssignments(),
88
+ refreshThreads()
89
+ ]);
90
+ }
91
+ } catch (err) {
92
+ console.error("BugBear: Init error", err);
93
+ } finally {
94
+ setIsLoading(false);
95
+ }
96
+ };
97
+ init();
98
+ }, [client]);
99
+ const currentAssignment = assignments.find(
100
+ (a) => a.status === "in_progress"
101
+ ) || assignments[0] || null;
102
+ const shouldShowWidget = isQAEnabled && isTester;
103
+ return /* @__PURE__ */ React.createElement(
104
+ BugBearContext.Provider,
105
+ {
106
+ value: {
107
+ client,
108
+ isTester,
109
+ isQAEnabled,
110
+ shouldShowWidget,
111
+ testerInfo,
112
+ assignments,
113
+ currentAssignment,
114
+ refreshAssignments,
115
+ isLoading,
116
+ getDeviceInfo,
117
+ onNavigate: config.onNavigate,
118
+ threads,
119
+ unreadCount,
120
+ refreshThreads,
121
+ getThreadMessages,
122
+ sendMessage,
123
+ markAsRead
124
+ }
125
+ },
126
+ children
127
+ );
128
+ }
129
+
130
+ // src/BugBearButton.tsx
131
+ import React2, { useState as useState2, useEffect as useEffect2, useRef } from "react";
132
+ import {
133
+ View,
134
+ Text,
135
+ TouchableOpacity,
136
+ Modal,
137
+ TextInput,
138
+ ScrollView,
139
+ StyleSheet,
140
+ Dimensions as Dimensions2,
141
+ KeyboardAvoidingView,
142
+ Platform as Platform2,
143
+ PanResponder,
144
+ Animated
145
+ } from "react-native";
146
+ var { width: screenWidth, height: screenHeight } = Dimensions2.get("window");
147
+ var templateInfo = {
148
+ steps: { name: "Steps", icon: "\u{1F4DD}", action: "Follow each step" },
149
+ checklist: { name: "Checklist", icon: "\u2705", action: "Pass/Fail each item" },
150
+ rubric: { name: "Rubric", icon: "\u{1F4CA}", action: "Rate each criterion" },
151
+ freeform: { name: "Freeform", icon: "\u{1F4AD}", action: "Provide observations" }
152
+ };
153
+ function BugBearButton({
154
+ getAppContext,
155
+ position = "bottom-right",
156
+ buttonStyle,
157
+ draggable = true,
158
+ initialX,
159
+ initialY,
160
+ minY = 100,
161
+ maxYOffset = 160
162
+ }) {
163
+ const {
164
+ client,
165
+ shouldShowWidget,
166
+ testerInfo,
167
+ assignments,
168
+ currentAssignment,
169
+ refreshAssignments,
170
+ isLoading,
171
+ getDeviceInfo,
172
+ onNavigate,
173
+ threads,
174
+ unreadCount,
175
+ refreshThreads,
176
+ getThreadMessages,
177
+ sendMessage,
178
+ markAsRead
179
+ } = useBugBear();
180
+ const [modalVisible, setModalVisible] = useState2(false);
181
+ const [activeTab, setActiveTab] = useState2("tests");
182
+ const [showSteps, setShowSteps] = useState2(false);
183
+ const [messageView, setMessageView] = useState2("list");
184
+ const [selectedThread, setSelectedThread] = useState2(null);
185
+ const [threadMessages, setThreadMessages] = useState2([]);
186
+ const [replyText, setReplyText] = useState2("");
187
+ const [sendingReply, setSendingReply] = useState2(false);
188
+ const [loadingMessages, setLoadingMessages] = useState2(false);
189
+ const getInitialPosition = () => {
190
+ const buttonSize = 56;
191
+ const margin = 16;
192
+ if (initialX !== void 0 && initialY !== void 0) {
193
+ return { x: initialX, y: initialY };
194
+ }
195
+ const x = position === "bottom-right" ? screenWidth - buttonSize - margin : margin;
196
+ const y = screenHeight - 160;
197
+ return { x, y };
198
+ };
199
+ const initialPos = getInitialPosition();
200
+ const pan = useRef(new Animated.ValueXY(initialPos)).current;
201
+ const isDragging = useRef(false);
202
+ const panResponder = useRef(
203
+ PanResponder.create({
204
+ onStartShouldSetPanResponder: () => draggable,
205
+ onMoveShouldSetPanResponder: (_, gestureState) => {
206
+ return draggable && (Math.abs(gestureState.dx) > 5 || Math.abs(gestureState.dy) > 5);
207
+ },
208
+ onPanResponderGrant: () => {
209
+ isDragging.current = false;
210
+ pan.setOffset({
211
+ x: pan.x._value,
212
+ y: pan.y._value
213
+ });
214
+ pan.setValue({ x: 0, y: 0 });
215
+ },
216
+ onPanResponderMove: (_, gestureState) => {
217
+ if (Math.abs(gestureState.dx) > 5 || Math.abs(gestureState.dy) > 5) {
218
+ isDragging.current = true;
219
+ }
220
+ Animated.event(
221
+ [null, { dx: pan.x, dy: pan.y }],
222
+ { useNativeDriver: false }
223
+ )(_, gestureState);
224
+ },
225
+ onPanResponderRelease: (_, gestureState) => {
226
+ pan.flattenOffset();
227
+ const currentX = pan.x._value;
228
+ const currentY = pan.y._value;
229
+ const buttonSize = 56;
230
+ const margin = 16;
231
+ const snapX = currentX < screenWidth / 2 ? margin : screenWidth - buttonSize - margin;
232
+ const snapY = Math.max(minY, Math.min(currentY, screenHeight - maxYOffset));
233
+ Animated.spring(pan, {
234
+ toValue: { x: snapX, y: snapY },
235
+ useNativeDriver: false,
236
+ friction: 7,
237
+ tension: 40
238
+ }).start();
239
+ if (!isDragging.current && Math.abs(gestureState.dx) < 5 && Math.abs(gestureState.dy) < 5) {
240
+ setModalVisible(true);
241
+ }
242
+ isDragging.current = false;
243
+ }
244
+ })
245
+ ).current;
246
+ const [testView, setTestView] = useState2("detail");
247
+ const [selectedTestId, setSelectedTestId] = useState2(null);
248
+ const displayedAssignment = selectedTestId ? assignments.find((a) => a.id === selectedTestId) || currentAssignment : currentAssignment;
249
+ const [reportType, setReportType] = useState2("bug");
250
+ const [description, setDescription] = useState2("");
251
+ const [severity, setSeverity] = useState2("medium");
252
+ const [submitting, setSubmitting] = useState2(false);
253
+ const [submitted, setSubmitted] = useState2(false);
254
+ const [justPassed, setJustPassed] = useState2(false);
255
+ const [criteriaResults, setCriteriaResults] = useState2({});
256
+ useEffect2(() => {
257
+ setCriteriaResults({});
258
+ setShowSteps(false);
259
+ }, [displayedAssignment?.id]);
260
+ if (isLoading || !shouldShowWidget) {
261
+ return null;
262
+ }
263
+ const pendingCount = assignments.filter((a) => a.status === "pending").length;
264
+ const inProgressCount = assignments.filter((a) => a.status === "in_progress").length;
265
+ const handlePass = async () => {
266
+ if (!client || !displayedAssignment) return;
267
+ setSubmitting(true);
268
+ await client.submitReport({
269
+ type: "test_pass",
270
+ description: `Test passed: ${displayedAssignment.testCase.title}`,
271
+ assignmentId: displayedAssignment.id,
272
+ testCaseId: displayedAssignment.testCase.id,
273
+ appContext: getAppContext?.() || { currentRoute: "unknown" },
274
+ deviceInfo: getDeviceInfo()
275
+ });
276
+ await refreshAssignments();
277
+ setSubmitting(false);
278
+ setJustPassed(true);
279
+ setTimeout(() => {
280
+ setJustPassed(false);
281
+ setSelectedTestId(null);
282
+ setTestView("detail");
283
+ }, 1200);
284
+ };
285
+ const handleFail = () => {
286
+ setActiveTab("report");
287
+ setReportType("test_fail");
288
+ };
289
+ const handleSubmitReport = async () => {
290
+ if (!client || !description.trim()) return;
291
+ setSubmitting(true);
292
+ const isTestFailure = reportType === "test_fail" && displayedAssignment;
293
+ await client.submitReport({
294
+ type: reportType,
295
+ description,
296
+ severity: reportType === "bug" || reportType === "test_fail" ? severity : void 0,
297
+ assignmentId: isTestFailure ? displayedAssignment.id : void 0,
298
+ testCaseId: isTestFailure ? displayedAssignment.testCase.id : void 0,
299
+ appContext: getAppContext?.() || { currentRoute: "unknown" },
300
+ deviceInfo: getDeviceInfo()
301
+ });
302
+ setDescription("");
303
+ setSeverity("medium");
304
+ setSubmitted(true);
305
+ if (isTestFailure) {
306
+ await refreshAssignments();
307
+ }
308
+ setTimeout(() => {
309
+ setSubmitted(false);
310
+ setActiveTab("tests");
311
+ if (isTestFailure) {
312
+ setSelectedTestId(null);
313
+ setTestView("list");
314
+ }
315
+ }, 2e3);
316
+ setSubmitting(false);
317
+ };
318
+ const handleNavigate = () => {
319
+ if (displayedAssignment?.testCase.targetRoute && onNavigate) {
320
+ onNavigate(displayedAssignment.testCase.targetRoute);
321
+ setModalVisible(false);
322
+ }
323
+ };
324
+ const handleOpenThread = async (thread) => {
325
+ setSelectedThread(thread);
326
+ setMessageView("thread");
327
+ setLoadingMessages(true);
328
+ const messages = await getThreadMessages(thread.id);
329
+ setThreadMessages(messages);
330
+ setLoadingMessages(false);
331
+ if (thread.unreadCount > 0) {
332
+ await markAsRead(thread.id);
333
+ }
334
+ };
335
+ const handleSendReply = async () => {
336
+ if (!selectedThread || !replyText.trim()) return;
337
+ setSendingReply(true);
338
+ const success = await sendMessage(selectedThread.id, replyText.trim());
339
+ if (success) {
340
+ setReplyText("");
341
+ const messages = await getThreadMessages(selectedThread.id);
342
+ setThreadMessages(messages);
343
+ }
344
+ setSendingReply(false);
345
+ };
346
+ const handleBackToThreadList = () => {
347
+ setMessageView("list");
348
+ setSelectedThread(null);
349
+ setThreadMessages([]);
350
+ setReplyText("");
351
+ };
352
+ const formatRelativeTime = (dateString) => {
353
+ const date = new Date(dateString);
354
+ const now = /* @__PURE__ */ new Date();
355
+ const diffMs = now.getTime() - date.getTime();
356
+ const diffMins = Math.floor(diffMs / 6e4);
357
+ const diffHours = Math.floor(diffMs / 36e5);
358
+ const diffDays = Math.floor(diffMs / 864e5);
359
+ if (diffMins < 1) return "Just now";
360
+ if (diffMins < 60) return `${diffMins}m ago`;
361
+ if (diffHours < 24) return `${diffHours}h ago`;
362
+ if (diffDays === 1) return "Yesterday";
363
+ if (diffDays < 7) return `${diffDays}d ago`;
364
+ return date.toLocaleDateString();
365
+ };
366
+ const formatMessageTime = (dateString) => {
367
+ const date = new Date(dateString);
368
+ return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
369
+ };
370
+ const getPriorityColor = (priority) => {
371
+ switch (priority) {
372
+ case "urgent":
373
+ return "#DC2626";
374
+ case "high":
375
+ return "#F97316";
376
+ case "normal":
377
+ return "#6B7280";
378
+ case "low":
379
+ return "#9CA3AF";
380
+ default:
381
+ return "#6B7280";
382
+ }
383
+ };
384
+ const getThreadTypeIcon = (type) => {
385
+ switch (type) {
386
+ case "announcement":
387
+ return "\u{1F4E2}";
388
+ case "direct":
389
+ return "\u{1F4AC}";
390
+ case "report":
391
+ return "\u{1F41B}";
392
+ case "general_note":
393
+ return "\u{1F4DD}";
394
+ default:
395
+ return "\u{1F4AC}";
396
+ }
397
+ };
398
+ const staticPositionStyle = position === "bottom-right" ? { right: 16, bottom: 100 } : { left: 16, bottom: 100 };
399
+ const renderTestContent = () => {
400
+ if (!displayedAssignment) return null;
401
+ const template = displayedAssignment.testCase.track?.testTemplate || "steps";
402
+ const steps = displayedAssignment.testCase.steps;
403
+ const info = templateInfo[template];
404
+ const rubricMode = displayedAssignment.testCase.track?.rubricMode || "pass_fail";
405
+ return /* @__PURE__ */ React2.createElement(React2.Fragment, null, /* @__PURE__ */ React2.createElement(View, { style: [styles.templateBadge, { backgroundColor: displayedAssignment.testCase.track?.color ? `${displayedAssignment.testCase.track.color}20` : "#F3F4F6" }] }, /* @__PURE__ */ React2.createElement(Text, { style: styles.templateIcon }, info.icon), /* @__PURE__ */ React2.createElement(Text, { style: styles.templateName }, info.name), /* @__PURE__ */ React2.createElement(Text, { style: styles.templateAction }, "\u2022 ", info.action)), /* @__PURE__ */ React2.createElement(TouchableOpacity, { onPress: () => setShowSteps(!showSteps), style: styles.stepsToggle }, /* @__PURE__ */ React2.createElement(Text, { style: styles.stepsToggleText }, showSteps ? "\u25BC" : "\u25B6", " ", template === "freeform" ? "Instructions" : `${steps.length} ${template === "checklist" ? "items" : template === "rubric" ? "criteria" : "steps"}`)), showSteps && template === "steps" && /* @__PURE__ */ React2.createElement(View, { style: styles.stepsList }, steps.map((step, idx) => /* @__PURE__ */ React2.createElement(View, { key: idx, style: styles.step }, /* @__PURE__ */ React2.createElement(View, { style: styles.stepNumber }, /* @__PURE__ */ React2.createElement(Text, { style: styles.stepNumberText }, step.stepNumber)), /* @__PURE__ */ React2.createElement(View, { style: styles.stepContent }, /* @__PURE__ */ React2.createElement(Text, { style: styles.stepAction }, step.action), step.expectedResult && /* @__PURE__ */ React2.createElement(Text, { style: styles.stepExpected }, "\u2192 ", step.expectedResult))))), showSteps && template === "checklist" && /* @__PURE__ */ React2.createElement(View, { style: styles.stepsList }, steps.map((step, idx) => /* @__PURE__ */ React2.createElement(
406
+ TouchableOpacity,
407
+ {
408
+ key: idx,
409
+ onPress: () => setCriteriaResults((prev) => {
410
+ const newResults = { ...prev };
411
+ if (prev[idx] === true) {
412
+ delete newResults[idx];
413
+ } else {
414
+ newResults[idx] = true;
415
+ }
416
+ return newResults;
417
+ }),
418
+ style: [
419
+ styles.checklistItem,
420
+ criteriaResults[idx] === true && styles.checklistItemChecked
421
+ ]
422
+ },
423
+ /* @__PURE__ */ React2.createElement(View, { style: [
424
+ styles.checkbox,
425
+ criteriaResults[idx] === true && styles.checkboxChecked
426
+ ] }, criteriaResults[idx] === true && /* @__PURE__ */ React2.createElement(Text, { style: styles.checkmark }, "\u2713")),
427
+ /* @__PURE__ */ React2.createElement(Text, { style: [
428
+ styles.checklistText,
429
+ criteriaResults[idx] === true && styles.checklistTextChecked
430
+ ] }, step.action)
431
+ )), /* @__PURE__ */ React2.createElement(View, { style: styles.resetRow }, /* @__PURE__ */ React2.createElement(Text, { style: styles.helperText }, "Tap to check off each item"), Object.keys(criteriaResults).length > 0 && /* @__PURE__ */ React2.createElement(TouchableOpacity, { onPress: () => setCriteriaResults({}) }, /* @__PURE__ */ React2.createElement(Text, { style: styles.resetText }, "\u21BA Reset")))), showSteps && template === "rubric" && /* @__PURE__ */ React2.createElement(View, { style: styles.stepsList }, steps.map((step, idx) => /* @__PURE__ */ React2.createElement(View, { key: idx, style: styles.rubricItem }, /* @__PURE__ */ React2.createElement(View, { style: styles.rubricHeader }, /* @__PURE__ */ React2.createElement(View, { style: styles.rubricNumber }, /* @__PURE__ */ React2.createElement(Text, { style: styles.rubricNumberText }, idx + 1)), /* @__PURE__ */ React2.createElement(Text, { style: styles.rubricTitle }, step.action)), step.expectedResult && /* @__PURE__ */ React2.createElement(Text, { style: styles.rubricExpected }, step.expectedResult), rubricMode === "pass_fail" && /* @__PURE__ */ React2.createElement(View, { style: styles.passFailButtons }, /* @__PURE__ */ React2.createElement(
432
+ TouchableOpacity,
433
+ {
434
+ onPress: () => setCriteriaResults((prev) => ({ ...prev, [idx]: true })),
435
+ style: [
436
+ styles.passFailButton,
437
+ criteriaResults[idx] === true && styles.passButtonActive
438
+ ]
439
+ },
440
+ /* @__PURE__ */ React2.createElement(Text, { style: [
441
+ styles.passFailButtonText,
442
+ criteriaResults[idx] === true && styles.passButtonTextActive
443
+ ] }, "\u2713 Pass")
444
+ ), /* @__PURE__ */ React2.createElement(
445
+ TouchableOpacity,
446
+ {
447
+ onPress: () => setCriteriaResults((prev) => ({ ...prev, [idx]: false })),
448
+ style: [
449
+ styles.passFailButton,
450
+ criteriaResults[idx] === false && styles.failButtonActive
451
+ ]
452
+ },
453
+ /* @__PURE__ */ React2.createElement(Text, { style: [
454
+ styles.passFailButtonText,
455
+ criteriaResults[idx] === false && styles.failButtonTextActive
456
+ ] }, "\u2717 Fail")
457
+ )), rubricMode === "rating" && /* @__PURE__ */ React2.createElement(View, { style: styles.ratingButtons }, [1, 2, 3, 4, 5].map((n) => /* @__PURE__ */ React2.createElement(
458
+ TouchableOpacity,
459
+ {
460
+ key: n,
461
+ onPress: () => setCriteriaResults((prev) => ({ ...prev, [idx]: n })),
462
+ style: [
463
+ styles.ratingButton,
464
+ criteriaResults[idx] === n && styles.ratingButtonActive
465
+ ]
466
+ },
467
+ /* @__PURE__ */ React2.createElement(Text, { style: [
468
+ styles.ratingButtonText,
469
+ criteriaResults[idx] === n && styles.ratingButtonTextActive
470
+ ] }, n)
471
+ ))))), /* @__PURE__ */ React2.createElement(View, { style: styles.resetRow }, /* @__PURE__ */ React2.createElement(Text, { style: styles.helperText }, rubricMode === "rating" ? "Rate 1-5 for each criterion" : "Mark each as Pass or Fail"), Object.keys(criteriaResults).length > 0 && /* @__PURE__ */ React2.createElement(TouchableOpacity, { onPress: () => setCriteriaResults({}) }, /* @__PURE__ */ React2.createElement(Text, { style: styles.resetText }, "\u21BA Reset")))), showSteps && template === "freeform" && /* @__PURE__ */ React2.createElement(View, { style: styles.freeformBox }, /* @__PURE__ */ React2.createElement(Text, { style: styles.freeformTitle }, "\u{1F4AD} Open Observation"), /* @__PURE__ */ React2.createElement(Text, { style: styles.freeformText }, "Review the area and note:"), /* @__PURE__ */ React2.createElement(Text, { style: styles.freeformBullet }, "\u2022 What works well"), /* @__PURE__ */ React2.createElement(Text, { style: styles.freeformBullet }, "\u2022 Issues or concerns"), /* @__PURE__ */ React2.createElement(Text, { style: styles.freeformBullet }, "\u2022 Suggestions")));
472
+ };
473
+ const totalBadgeCount = pendingCount + unreadCount;
474
+ const renderButtonContent = () => /* @__PURE__ */ React2.createElement(React2.Fragment, null, /* @__PURE__ */ React2.createElement(Text, { style: styles.fabEmoji }, "\u{1F43B}"), totalBadgeCount > 0 && /* @__PURE__ */ React2.createElement(View, { style: [styles.badge, unreadCount > 0 && pendingCount === 0 && styles.messageBadge] }, /* @__PURE__ */ React2.createElement(Text, { style: styles.badgeText }, totalBadgeCount)));
475
+ return /* @__PURE__ */ React2.createElement(React2.Fragment, null, draggable ? /* @__PURE__ */ React2.createElement(
476
+ Animated.View,
477
+ {
478
+ ...panResponder.panHandlers,
479
+ style: [
480
+ styles.fab,
481
+ styles.fabDraggable,
482
+ buttonStyle,
483
+ {
484
+ transform: [
485
+ { translateX: pan.x },
486
+ { translateY: pan.y }
487
+ ]
488
+ }
489
+ ]
490
+ },
491
+ renderButtonContent()
492
+ ) : /* @__PURE__ */ React2.createElement(
493
+ TouchableOpacity,
494
+ {
495
+ style: [styles.fab, staticPositionStyle, buttonStyle],
496
+ onPress: () => setModalVisible(true),
497
+ activeOpacity: 0.8
498
+ },
499
+ renderButtonContent()
500
+ ), /* @__PURE__ */ React2.createElement(
501
+ Modal,
502
+ {
503
+ visible: modalVisible,
504
+ animationType: "slide",
505
+ transparent: true,
506
+ onRequestClose: () => setModalVisible(false)
507
+ },
508
+ /* @__PURE__ */ React2.createElement(
509
+ KeyboardAvoidingView,
510
+ {
511
+ behavior: Platform2.OS === "ios" ? "padding" : "height",
512
+ style: styles.modalContainer
513
+ },
514
+ /* @__PURE__ */ React2.createElement(View, { style: styles.modalContent }, /* @__PURE__ */ React2.createElement(View, { style: styles.header }, /* @__PURE__ */ React2.createElement(View, { style: styles.headerLeft }, /* @__PURE__ */ React2.createElement(Text, { style: styles.headerEmoji }, "\u{1F43B}"), /* @__PURE__ */ React2.createElement(View, null, /* @__PURE__ */ React2.createElement(Text, { style: styles.headerTitle }, "BugBear"), /* @__PURE__ */ React2.createElement(Text, { style: styles.headerSubtitle }, testerInfo?.name))), /* @__PURE__ */ React2.createElement(
515
+ TouchableOpacity,
516
+ {
517
+ onPress: () => setModalVisible(false),
518
+ style: styles.closeButton
519
+ },
520
+ /* @__PURE__ */ React2.createElement(Text, { style: styles.closeButtonText }, "\u2715")
521
+ )), /* @__PURE__ */ React2.createElement(View, { style: styles.tabs }, /* @__PURE__ */ React2.createElement(
522
+ TouchableOpacity,
523
+ {
524
+ style: [styles.tab, activeTab === "tests" && styles.activeTab],
525
+ onPress: () => setActiveTab("tests")
526
+ },
527
+ /* @__PURE__ */ React2.createElement(Text, { style: [styles.tabText, activeTab === "tests" && styles.activeTabText] }, "Tests ", pendingCount > 0 && `(${pendingCount})`)
528
+ ), /* @__PURE__ */ React2.createElement(
529
+ TouchableOpacity,
530
+ {
531
+ style: [styles.tab, activeTab === "messages" && styles.activeTab],
532
+ onPress: () => setActiveTab("messages")
533
+ },
534
+ /* @__PURE__ */ React2.createElement(View, { style: styles.tabWithBadge }, /* @__PURE__ */ React2.createElement(Text, { style: [styles.tabText, activeTab === "messages" && styles.activeTabText] }, "Messages"), unreadCount > 0 && /* @__PURE__ */ React2.createElement(View, { style: styles.tabBadge }, /* @__PURE__ */ React2.createElement(Text, { style: styles.tabBadgeText }, unreadCount)))
535
+ ), /* @__PURE__ */ React2.createElement(
536
+ TouchableOpacity,
537
+ {
538
+ style: [styles.tab, activeTab === "report" && styles.activeTab],
539
+ onPress: () => setActiveTab("report")
540
+ },
541
+ /* @__PURE__ */ React2.createElement(Text, { style: [styles.tabText, activeTab === "report" && styles.activeTabText] }, "Report")
542
+ )), /* @__PURE__ */ React2.createElement(ScrollView, { style: styles.content }, activeTab === "tests" && /* @__PURE__ */ React2.createElement(View, null, assignments.length === 0 ? /* @__PURE__ */ React2.createElement(View, { style: styles.emptyState }, /* @__PURE__ */ React2.createElement(Text, { style: styles.emptyEmoji }, "\u2705"), /* @__PURE__ */ React2.createElement(Text, { style: styles.emptyTitle }, "All caught up!"), /* @__PURE__ */ React2.createElement(Text, { style: styles.emptySubtitle }, "No tests assigned")) : testView === "list" ? (
543
+ /* List View - Show all tests */
544
+ /* @__PURE__ */ React2.createElement(View, null, /* @__PURE__ */ React2.createElement(Text, { style: styles.listHeader }, assignments.length, " test", assignments.length !== 1 ? "s" : "", " assigned"), assignments.map((assignment) => /* @__PURE__ */ React2.createElement(
545
+ TouchableOpacity,
546
+ {
547
+ key: assignment.id,
548
+ onPress: () => {
549
+ setSelectedTestId(assignment.id);
550
+ setTestView("detail");
551
+ setShowSteps(false);
552
+ },
553
+ style: [
554
+ styles.listItem,
555
+ assignment.id === currentAssignment?.id && styles.listItemCurrent
556
+ ]
557
+ },
558
+ /* @__PURE__ */ React2.createElement(View, { style: styles.listItemHeader }, /* @__PURE__ */ React2.createElement(Text, { style: styles.listItemKey }, assignment.testCase.testKey), /* @__PURE__ */ React2.createElement(View, { style: styles.listItemBadges }, assignment.testCase.track && /* @__PURE__ */ React2.createElement(View, { style: [styles.trackBadge, { backgroundColor: assignment.testCase.track.color }] }, /* @__PURE__ */ React2.createElement(Text, { style: styles.trackBadgeText }, templateInfo[assignment.testCase.track.testTemplate || "steps"].icon)), /* @__PURE__ */ React2.createElement(View, { style: [
559
+ styles.priorityBadge,
560
+ assignment.testCase.priority === "P0" && styles.priorityP0,
561
+ assignment.testCase.priority === "P1" && styles.priorityP1
562
+ ] }, /* @__PURE__ */ React2.createElement(Text, { style: styles.priorityText }, assignment.testCase.priority)))),
563
+ /* @__PURE__ */ React2.createElement(Text, { style: styles.listItemTitle, numberOfLines: 2 }, assignment.testCase.title),
564
+ /* @__PURE__ */ React2.createElement(View, { style: styles.listItemMeta }, /* @__PURE__ */ React2.createElement(Text, { style: styles.listItemMetaText }, assignment.testCase.steps.length, " ", assignment.testCase.track?.testTemplate === "checklist" ? "items" : assignment.testCase.track?.testTemplate === "rubric" ? "criteria" : "steps"), assignment.id === currentAssignment?.id && /* @__PURE__ */ React2.createElement(Text, { style: styles.currentBadge }, "\u2022 Current"))
565
+ )))
566
+ ) : justPassed ? (
567
+ /* Success state after passing */
568
+ /* @__PURE__ */ React2.createElement(View, { style: styles.emptyState }, /* @__PURE__ */ React2.createElement(Text, { style: styles.passedEmoji }, "\u2713"), /* @__PURE__ */ React2.createElement(Text, { style: styles.passedTitle }, "Passed!"), /* @__PURE__ */ React2.createElement(Text, { style: styles.emptySubtitle }, "Loading next test..."))
569
+ ) : displayedAssignment ? (
570
+ /* Detail View - Show single test */
571
+ /* @__PURE__ */ React2.createElement(View, null, /* @__PURE__ */ React2.createElement(
572
+ TouchableOpacity,
573
+ {
574
+ onPress: () => {
575
+ setTestView("list");
576
+ setSelectedTestId(null);
577
+ },
578
+ style: styles.backButton
579
+ },
580
+ /* @__PURE__ */ React2.createElement(Text, { style: styles.backButtonText }, "\u2190 All Tests (", assignments.length, ")")
581
+ ), /* @__PURE__ */ React2.createElement(View, { style: styles.testCard }, /* @__PURE__ */ React2.createElement(View, { style: styles.testHeader }, /* @__PURE__ */ React2.createElement(Text, { style: styles.testKey }, displayedAssignment.testCase.testKey), /* @__PURE__ */ React2.createElement(View, { style: styles.testHeaderBadges }, displayedAssignment.testCase.track && /* @__PURE__ */ React2.createElement(View, { style: [styles.trackBadge, { backgroundColor: displayedAssignment.testCase.track.color }] }, /* @__PURE__ */ React2.createElement(Text, { style: styles.trackBadgeText }, templateInfo[displayedAssignment.testCase.track.testTemplate || "steps"].icon)), /* @__PURE__ */ React2.createElement(View, { style: [
582
+ styles.priorityBadge,
583
+ displayedAssignment.testCase.priority === "P0" && styles.priorityP0,
584
+ displayedAssignment.testCase.priority === "P1" && styles.priorityP1
585
+ ] }, /* @__PURE__ */ React2.createElement(Text, { style: styles.priorityText }, displayedAssignment.testCase.priority)))), /* @__PURE__ */ React2.createElement(Text, { style: styles.testTitle }, displayedAssignment.testCase.title), displayedAssignment.testCase.description && /* @__PURE__ */ React2.createElement(Text, { style: styles.testDescription }, displayedAssignment.testCase.description), displayedAssignment.testCase.targetRoute && onNavigate && /* @__PURE__ */ React2.createElement(TouchableOpacity, { onPress: handleNavigate, style: styles.navigateButton }, /* @__PURE__ */ React2.createElement(Text, { style: styles.navigateButtonText }, "Go to test location \u2192")), renderTestContent(), /* @__PURE__ */ React2.createElement(View, { style: styles.expectedResult }, /* @__PURE__ */ React2.createElement(Text, { style: styles.expectedLabel }, displayedAssignment.testCase.track?.testTemplate === "checklist" ? "Pass criteria:" : displayedAssignment.testCase.track?.testTemplate === "rubric" ? "Target score:" : "Expected:"), /* @__PURE__ */ React2.createElement(Text, { style: styles.expectedText }, displayedAssignment.testCase.expectedResult))), /* @__PURE__ */ React2.createElement(View, { style: styles.actionButtons }, /* @__PURE__ */ React2.createElement(
586
+ TouchableOpacity,
587
+ {
588
+ style: styles.failButton,
589
+ onPress: handleFail,
590
+ disabled: submitting
591
+ },
592
+ /* @__PURE__ */ React2.createElement(Text, { style: styles.failButtonText }, "\u2717 Fail")
593
+ ), /* @__PURE__ */ React2.createElement(
594
+ TouchableOpacity,
595
+ {
596
+ style: styles.passButton,
597
+ onPress: handlePass,
598
+ disabled: submitting
599
+ },
600
+ /* @__PURE__ */ React2.createElement(Text, { style: styles.passButtonText }, submitting ? "..." : "\u2713 Pass")
601
+ )))
602
+ ) : null), activeTab === "messages" && /* @__PURE__ */ React2.createElement(View, null, messageView === "list" ? (
603
+ /* Thread List View */
604
+ /* @__PURE__ */ React2.createElement(View, null, threads.length === 0 ? /* @__PURE__ */ React2.createElement(View, { style: styles.emptyState }, /* @__PURE__ */ React2.createElement(Text, { style: styles.emptyEmoji }, "\u{1F4AC}"), /* @__PURE__ */ React2.createElement(Text, { style: styles.emptyTitle }, "No messages yet"), /* @__PURE__ */ React2.createElement(Text, { style: styles.emptySubtitle }, "Messages from admins will appear here")) : /* @__PURE__ */ React2.createElement(View, null, threads.map((thread) => /* @__PURE__ */ React2.createElement(
605
+ TouchableOpacity,
606
+ {
607
+ key: thread.id,
608
+ onPress: () => handleOpenThread(thread),
609
+ style: [
610
+ styles.threadItem,
611
+ thread.unreadCount > 0 && styles.threadItemUnread
612
+ ]
613
+ },
614
+ /* @__PURE__ */ React2.createElement(View, { style: styles.threadHeader }, /* @__PURE__ */ React2.createElement(View, { style: styles.threadTitleRow }, thread.isPinned && /* @__PURE__ */ React2.createElement(Text, { style: styles.pinnedIcon }, "\u{1F4CC}"), /* @__PURE__ */ React2.createElement(Text, { style: styles.threadTypeIcon }, getThreadTypeIcon(thread.threadType)), /* @__PURE__ */ React2.createElement(
615
+ Text,
616
+ {
617
+ style: [styles.threadSubject, thread.unreadCount > 0 && styles.threadSubjectUnread],
618
+ numberOfLines: 1
619
+ },
620
+ thread.subject || "No subject"
621
+ )), (thread.priority === "high" || thread.priority === "urgent") && /* @__PURE__ */ React2.createElement(View, { style: [styles.priorityDot, { backgroundColor: getPriorityColor(thread.priority) }] })),
622
+ thread.lastMessage && /* @__PURE__ */ React2.createElement(Text, { style: styles.threadPreview, numberOfLines: 1 }, thread.lastMessage.senderName, ": ", thread.lastMessage.content),
623
+ /* @__PURE__ */ React2.createElement(View, { style: styles.threadMeta }, thread.unreadCount > 0 ? /* @__PURE__ */ React2.createElement(View, { style: styles.unreadBadge }, /* @__PURE__ */ React2.createElement(Text, { style: styles.unreadBadgeText }, thread.unreadCount, " new")) : /* @__PURE__ */ React2.createElement(Text, { style: styles.threadMetaText }, "Read"), /* @__PURE__ */ React2.createElement(Text, { style: styles.threadTime }, formatRelativeTime(thread.lastMessageAt)))
624
+ ))))
625
+ ) : (
626
+ /* Thread Detail View */
627
+ /* @__PURE__ */ React2.createElement(View, { style: styles.threadDetailContainer }, /* @__PURE__ */ React2.createElement(TouchableOpacity, { onPress: handleBackToThreadList, style: styles.backButton }, /* @__PURE__ */ React2.createElement(Text, { style: styles.backButtonText }, "\u2190 Back to Messages")), selectedThread && /* @__PURE__ */ React2.createElement(View, { style: styles.threadDetailHeader }, /* @__PURE__ */ React2.createElement(Text, { style: styles.threadDetailIcon }, getThreadTypeIcon(selectedThread.threadType)), /* @__PURE__ */ React2.createElement(Text, { style: styles.threadDetailSubject, numberOfLines: 2 }, selectedThread.subject || "No subject")), loadingMessages ? /* @__PURE__ */ React2.createElement(View, { style: styles.loadingMessages }, /* @__PURE__ */ React2.createElement(Text, { style: styles.loadingText }, "Loading messages...")) : /* @__PURE__ */ React2.createElement(View, { style: styles.messagesContainer }, threadMessages.map((message) => /* @__PURE__ */ React2.createElement(
628
+ View,
629
+ {
630
+ key: message.id,
631
+ style: [
632
+ styles.messageBubble,
633
+ message.senderType === "tester" ? styles.messageBubbleTester : styles.messageBubbleAdmin
634
+ ]
635
+ },
636
+ /* @__PURE__ */ React2.createElement(Text, { style: [
637
+ styles.messageSender,
638
+ message.senderType === "tester" && styles.messageSenderTester
639
+ ] }, message.senderType === "tester" ? "You" : message.senderName),
640
+ /* @__PURE__ */ React2.createElement(Text, { style: [
641
+ styles.messageContent,
642
+ message.senderType === "tester" && styles.messageContentTester
643
+ ] }, message.content),
644
+ /* @__PURE__ */ React2.createElement(Text, { style: [
645
+ styles.messageTime,
646
+ message.senderType === "tester" && styles.messageTimeTester
647
+ ] }, formatMessageTime(message.createdAt))
648
+ ))))
649
+ )), activeTab === "report" && /* @__PURE__ */ React2.createElement(View, null, submitted ? /* @__PURE__ */ React2.createElement(View, { style: styles.emptyState }, /* @__PURE__ */ React2.createElement(Text, { style: styles.emptyEmoji }, "\u{1F389}"), /* @__PURE__ */ React2.createElement(Text, { style: styles.emptyTitle }, "Report submitted!")) : /* @__PURE__ */ React2.createElement(React2.Fragment, null, /* @__PURE__ */ React2.createElement(View, { style: styles.reportTypes }, [
650
+ { type: "bug", label: "\u{1F41B} Bug" },
651
+ { type: "feedback", label: "\u{1F4A1} Feedback" },
652
+ { type: "suggestion", label: "\u2728 Idea" }
653
+ ].map(({ type, label }) => /* @__PURE__ */ React2.createElement(
654
+ TouchableOpacity,
655
+ {
656
+ key: type,
657
+ style: [
658
+ styles.reportTypeButton,
659
+ reportType === type && styles.reportTypeActive
660
+ ],
661
+ onPress: () => setReportType(type)
662
+ },
663
+ /* @__PURE__ */ React2.createElement(Text, { style: [
664
+ styles.reportTypeText,
665
+ reportType === type && styles.reportTypeTextActive
666
+ ] }, label)
667
+ ))), (reportType === "bug" || reportType === "test_fail") && /* @__PURE__ */ React2.createElement(View, { style: styles.severitySection }, /* @__PURE__ */ React2.createElement(Text, { style: styles.label }, "Severity"), /* @__PURE__ */ React2.createElement(View, { style: styles.severityButtons }, ["critical", "high", "medium", "low"].map((sev) => {
668
+ const activeStyles = {
669
+ critical: styles.severityCriticalActive,
670
+ high: styles.severityHighActive,
671
+ medium: styles.severityMediumActive,
672
+ low: styles.severityLowActive
673
+ };
674
+ return /* @__PURE__ */ React2.createElement(
675
+ TouchableOpacity,
676
+ {
677
+ key: sev,
678
+ style: [
679
+ styles.severityButton,
680
+ severity === sev && activeStyles[sev]
681
+ ],
682
+ onPress: () => setSeverity(sev)
683
+ },
684
+ /* @__PURE__ */ React2.createElement(Text, { style: [
685
+ styles.severityText,
686
+ severity === sev && styles.severityTextActive
687
+ ] }, sev)
688
+ );
689
+ }))), /* @__PURE__ */ React2.createElement(View, { style: styles.descriptionSection }, /* @__PURE__ */ React2.createElement(Text, { style: styles.label }, "What happened?"), /* @__PURE__ */ React2.createElement(
690
+ TextInput,
691
+ {
692
+ style: styles.textInput,
693
+ value: description,
694
+ onChangeText: setDescription,
695
+ placeholder: "Describe the issue...",
696
+ placeholderTextColor: "#9CA3AF",
697
+ multiline: true,
698
+ numberOfLines: 4,
699
+ textAlignVertical: "top"
700
+ }
701
+ )), /* @__PURE__ */ React2.createElement(
702
+ TouchableOpacity,
703
+ {
704
+ style: [
705
+ styles.submitButton,
706
+ (!description.trim() || submitting) && styles.submitButtonDisabled
707
+ ],
708
+ onPress: handleSubmitReport,
709
+ disabled: !description.trim() || submitting
710
+ },
711
+ /* @__PURE__ */ React2.createElement(Text, { style: styles.submitButtonText }, submitting ? "Submitting..." : "Submit Report")
712
+ )))), activeTab === "messages" && messageView === "thread" && selectedThread ? (
713
+ /* Reply Composer */
714
+ /* @__PURE__ */ React2.createElement(View, { style: styles.replyComposer }, /* @__PURE__ */ React2.createElement(
715
+ TextInput,
716
+ {
717
+ style: styles.replyInput,
718
+ value: replyText,
719
+ onChangeText: setReplyText,
720
+ placeholder: "Type a reply...",
721
+ placeholderTextColor: "#9CA3AF",
722
+ multiline: true,
723
+ maxLength: 1e3
724
+ }
725
+ ), /* @__PURE__ */ React2.createElement(
726
+ TouchableOpacity,
727
+ {
728
+ style: [
729
+ styles.sendButton,
730
+ (!replyText.trim() || sendingReply) && styles.sendButtonDisabled
731
+ ],
732
+ onPress: handleSendReply,
733
+ disabled: !replyText.trim() || sendingReply
734
+ },
735
+ /* @__PURE__ */ React2.createElement(Text, { style: styles.sendButtonText }, sendingReply ? "..." : "Send")
736
+ ))
737
+ ) : (
738
+ /* Standard Footer */
739
+ /* @__PURE__ */ React2.createElement(View, { style: styles.footer }, activeTab === "messages" ? /* @__PURE__ */ React2.createElement(React2.Fragment, null, /* @__PURE__ */ React2.createElement(Text, { style: styles.footerText }, threads.length, " thread", threads.length !== 1 ? "s" : "", " \xB7 ", unreadCount, " unread"), /* @__PURE__ */ React2.createElement(TouchableOpacity, { onPress: refreshThreads }, /* @__PURE__ */ React2.createElement(Text, { style: styles.refreshText }, "\u21BB Refresh"))) : /* @__PURE__ */ React2.createElement(React2.Fragment, null, /* @__PURE__ */ React2.createElement(Text, { style: styles.footerText }, pendingCount, " pending \xB7 ", inProgressCount, " in progress"), /* @__PURE__ */ React2.createElement(TouchableOpacity, { onPress: refreshAssignments }, /* @__PURE__ */ React2.createElement(Text, { style: styles.refreshText }, "\u21BB Refresh"))))
740
+ ))
741
+ )
742
+ ));
743
+ }
744
+ var styles = StyleSheet.create({
745
+ fab: {
746
+ position: "absolute",
747
+ width: 56,
748
+ height: 56,
749
+ borderRadius: 28,
750
+ backgroundColor: "#7C3AED",
751
+ justifyContent: "center",
752
+ alignItems: "center",
753
+ shadowColor: "#000",
754
+ shadowOffset: { width: 0, height: 4 },
755
+ shadowOpacity: 0.3,
756
+ shadowRadius: 8,
757
+ elevation: 8,
758
+ zIndex: 999999
759
+ },
760
+ fabDraggable: {
761
+ // When draggable, position is controlled by transforms
762
+ // so we use top/left: 0 as the base
763
+ top: 0,
764
+ left: 0
765
+ },
766
+ fabEmoji: {
767
+ fontSize: 28
768
+ },
769
+ badge: {
770
+ position: "absolute",
771
+ top: -4,
772
+ right: -4,
773
+ backgroundColor: "#EF4444",
774
+ borderRadius: 10,
775
+ minWidth: 20,
776
+ height: 20,
777
+ justifyContent: "center",
778
+ alignItems: "center",
779
+ paddingHorizontal: 6
780
+ },
781
+ badgeText: {
782
+ color: "#fff",
783
+ fontSize: 12,
784
+ fontWeight: "bold"
785
+ },
786
+ modalContainer: {
787
+ flex: 1,
788
+ justifyContent: "flex-end",
789
+ backgroundColor: "rgba(0, 0, 0, 0.5)"
790
+ },
791
+ modalContent: {
792
+ backgroundColor: "#fff",
793
+ borderTopLeftRadius: 20,
794
+ borderTopRightRadius: 20,
795
+ maxHeight: "85%"
796
+ },
797
+ header: {
798
+ flexDirection: "row",
799
+ alignItems: "center",
800
+ justifyContent: "space-between",
801
+ backgroundColor: "#7C3AED",
802
+ paddingHorizontal: 16,
803
+ paddingVertical: 12,
804
+ borderTopLeftRadius: 20,
805
+ borderTopRightRadius: 20
806
+ },
807
+ headerLeft: {
808
+ flexDirection: "row",
809
+ alignItems: "center"
810
+ },
811
+ headerEmoji: {
812
+ fontSize: 24,
813
+ marginRight: 10
814
+ },
815
+ headerTitle: {
816
+ color: "#fff",
817
+ fontSize: 16,
818
+ fontWeight: "600"
819
+ },
820
+ headerSubtitle: {
821
+ color: "#DDD6FE",
822
+ fontSize: 12
823
+ },
824
+ closeButton: {
825
+ padding: 8
826
+ },
827
+ closeButtonText: {
828
+ color: "#fff",
829
+ fontSize: 18
830
+ },
831
+ tabs: {
832
+ flexDirection: "row",
833
+ borderBottomWidth: 1,
834
+ borderBottomColor: "#E5E7EB"
835
+ },
836
+ tab: {
837
+ flex: 1,
838
+ paddingVertical: 12,
839
+ alignItems: "center"
840
+ },
841
+ activeTab: {
842
+ borderBottomWidth: 2,
843
+ borderBottomColor: "#7C3AED"
844
+ },
845
+ tabText: {
846
+ fontSize: 14,
847
+ fontWeight: "500",
848
+ color: "#6B7280"
849
+ },
850
+ activeTabText: {
851
+ color: "#7C3AED"
852
+ },
853
+ content: {
854
+ padding: 16,
855
+ maxHeight: 400
856
+ },
857
+ footer: {
858
+ flexDirection: "row",
859
+ justifyContent: "space-between",
860
+ alignItems: "center",
861
+ paddingHorizontal: 16,
862
+ paddingVertical: 12,
863
+ borderTopWidth: 1,
864
+ borderTopColor: "#E5E7EB",
865
+ backgroundColor: "#F9FAFB"
866
+ },
867
+ footerText: {
868
+ fontSize: 12,
869
+ color: "#9CA3AF"
870
+ },
871
+ refreshText: {
872
+ fontSize: 12,
873
+ color: "#6B7280"
874
+ },
875
+ emptyState: {
876
+ alignItems: "center",
877
+ paddingVertical: 40
878
+ },
879
+ emptyEmoji: {
880
+ fontSize: 48
881
+ },
882
+ emptyTitle: {
883
+ fontSize: 18,
884
+ fontWeight: "600",
885
+ color: "#374151",
886
+ marginTop: 12
887
+ },
888
+ passedEmoji: {
889
+ fontSize: 56,
890
+ color: "#22C55E"
891
+ },
892
+ passedTitle: {
893
+ fontSize: 20,
894
+ fontWeight: "600",
895
+ color: "#22C55E",
896
+ marginTop: 12
897
+ },
898
+ emptySubtitle: {
899
+ fontSize: 14,
900
+ color: "#9CA3AF",
901
+ marginTop: 4
902
+ },
903
+ // List view styles
904
+ listHeader: {
905
+ fontSize: 12,
906
+ color: "#6B7280",
907
+ marginBottom: 12
908
+ },
909
+ listItem: {
910
+ backgroundColor: "#F9FAFB",
911
+ borderRadius: 12,
912
+ padding: 12,
913
+ marginBottom: 8,
914
+ borderWidth: 1,
915
+ borderColor: "#E5E7EB"
916
+ },
917
+ listItemCurrent: {
918
+ backgroundColor: "#EDE9FE",
919
+ borderColor: "#C4B5FD"
920
+ },
921
+ listItemHeader: {
922
+ flexDirection: "row",
923
+ justifyContent: "space-between",
924
+ alignItems: "center",
925
+ marginBottom: 4
926
+ },
927
+ listItemKey: {
928
+ fontSize: 12,
929
+ fontFamily: Platform2.OS === "ios" ? "Menlo" : "monospace",
930
+ color: "#6B7280"
931
+ },
932
+ listItemBadges: {
933
+ flexDirection: "row",
934
+ gap: 4
935
+ },
936
+ listItemTitle: {
937
+ fontSize: 14,
938
+ fontWeight: "600",
939
+ color: "#111827",
940
+ marginBottom: 4
941
+ },
942
+ listItemMeta: {
943
+ flexDirection: "row",
944
+ alignItems: "center"
945
+ },
946
+ listItemMetaText: {
947
+ fontSize: 12,
948
+ color: "#9CA3AF"
949
+ },
950
+ currentBadge: {
951
+ fontSize: 12,
952
+ color: "#7C3AED",
953
+ fontWeight: "600",
954
+ marginLeft: 8
955
+ },
956
+ // Back button
957
+ backButton: {
958
+ marginBottom: 12
959
+ },
960
+ backButtonText: {
961
+ fontSize: 14,
962
+ color: "#7C3AED",
963
+ fontWeight: "500"
964
+ },
965
+ // Test card styles
966
+ testCard: {
967
+ backgroundColor: "#F9FAFB",
968
+ borderRadius: 12,
969
+ padding: 16,
970
+ marginBottom: 16
971
+ },
972
+ testHeader: {
973
+ flexDirection: "row",
974
+ justifyContent: "space-between",
975
+ alignItems: "center",
976
+ marginBottom: 8
977
+ },
978
+ testHeaderBadges: {
979
+ flexDirection: "row",
980
+ gap: 4
981
+ },
982
+ testKey: {
983
+ fontSize: 12,
984
+ fontFamily: Platform2.OS === "ios" ? "Menlo" : "monospace",
985
+ color: "#6B7280"
986
+ },
987
+ trackBadge: {
988
+ paddingHorizontal: 6,
989
+ paddingVertical: 2,
990
+ borderRadius: 4
991
+ },
992
+ trackBadgeText: {
993
+ fontSize: 12,
994
+ color: "#fff"
995
+ },
996
+ priorityBadge: {
997
+ backgroundColor: "#E5E7EB",
998
+ paddingHorizontal: 8,
999
+ paddingVertical: 2,
1000
+ borderRadius: 4
1001
+ },
1002
+ priorityP0: {
1003
+ backgroundColor: "#FEE2E2"
1004
+ },
1005
+ priorityP1: {
1006
+ backgroundColor: "#FED7AA"
1007
+ },
1008
+ priorityText: {
1009
+ fontSize: 12,
1010
+ fontWeight: "600",
1011
+ color: "#374151"
1012
+ },
1013
+ testTitle: {
1014
+ fontSize: 16,
1015
+ fontWeight: "600",
1016
+ color: "#111827",
1017
+ marginBottom: 4
1018
+ },
1019
+ testDescription: {
1020
+ fontSize: 14,
1021
+ color: "#6B7280",
1022
+ marginBottom: 8
1023
+ },
1024
+ // Navigate button
1025
+ navigateButton: {
1026
+ backgroundColor: "#EFF6FF",
1027
+ borderWidth: 1,
1028
+ borderColor: "#BFDBFE",
1029
+ borderRadius: 8,
1030
+ paddingVertical: 10,
1031
+ paddingHorizontal: 16,
1032
+ alignItems: "center",
1033
+ marginBottom: 12
1034
+ },
1035
+ navigateButtonText: {
1036
+ fontSize: 14,
1037
+ fontWeight: "500",
1038
+ color: "#1D4ED8"
1039
+ },
1040
+ // Template badge
1041
+ templateBadge: {
1042
+ flexDirection: "row",
1043
+ alignItems: "center",
1044
+ paddingHorizontal: 10,
1045
+ paddingVertical: 6,
1046
+ borderRadius: 6,
1047
+ marginBottom: 8
1048
+ },
1049
+ templateIcon: {
1050
+ fontSize: 14,
1051
+ marginRight: 6
1052
+ },
1053
+ templateName: {
1054
+ fontSize: 12,
1055
+ fontWeight: "600",
1056
+ color: "#374151",
1057
+ marginRight: 4
1058
+ },
1059
+ templateAction: {
1060
+ fontSize: 12,
1061
+ color: "#6B7280"
1062
+ },
1063
+ // Steps toggle
1064
+ stepsToggle: {
1065
+ paddingVertical: 8
1066
+ },
1067
+ stepsToggleText: {
1068
+ fontSize: 14,
1069
+ color: "#7C3AED",
1070
+ fontWeight: "500"
1071
+ },
1072
+ stepsList: {
1073
+ marginTop: 8
1074
+ },
1075
+ step: {
1076
+ flexDirection: "row",
1077
+ marginBottom: 12
1078
+ },
1079
+ stepNumber: {
1080
+ width: 24,
1081
+ height: 24,
1082
+ borderRadius: 12,
1083
+ backgroundColor: "#EDE9FE",
1084
+ justifyContent: "center",
1085
+ alignItems: "center",
1086
+ marginRight: 12
1087
+ },
1088
+ stepNumberText: {
1089
+ fontSize: 12,
1090
+ fontWeight: "600",
1091
+ color: "#7C3AED"
1092
+ },
1093
+ stepContent: {
1094
+ flex: 1
1095
+ },
1096
+ stepAction: {
1097
+ fontSize: 14,
1098
+ color: "#374151"
1099
+ },
1100
+ stepExpected: {
1101
+ fontSize: 12,
1102
+ color: "#9CA3AF",
1103
+ marginTop: 2
1104
+ },
1105
+ // Checklist styles
1106
+ checklistItem: {
1107
+ flexDirection: "row",
1108
+ alignItems: "center",
1109
+ backgroundColor: "#fff",
1110
+ borderWidth: 1,
1111
+ borderColor: "#E5E7EB",
1112
+ borderRadius: 8,
1113
+ padding: 12,
1114
+ marginBottom: 8
1115
+ },
1116
+ checklistItemChecked: {
1117
+ backgroundColor: "#ECFDF5",
1118
+ borderColor: "#86EFAC"
1119
+ },
1120
+ checkbox: {
1121
+ width: 24,
1122
+ height: 24,
1123
+ borderRadius: 4,
1124
+ borderWidth: 2,
1125
+ borderColor: "#22D3EE",
1126
+ marginRight: 12,
1127
+ justifyContent: "center",
1128
+ alignItems: "center"
1129
+ },
1130
+ checkboxChecked: {
1131
+ backgroundColor: "#22C55E",
1132
+ borderColor: "#22C55E"
1133
+ },
1134
+ checkmark: {
1135
+ color: "#fff",
1136
+ fontSize: 14,
1137
+ fontWeight: "bold"
1138
+ },
1139
+ checklistText: {
1140
+ flex: 1,
1141
+ fontSize: 14,
1142
+ color: "#374151"
1143
+ },
1144
+ checklistTextChecked: {
1145
+ color: "#15803D"
1146
+ },
1147
+ // Rubric styles
1148
+ rubricItem: {
1149
+ backgroundColor: "#fff",
1150
+ borderWidth: 1,
1151
+ borderColor: "#E5E7EB",
1152
+ borderRadius: 8,
1153
+ padding: 12,
1154
+ marginBottom: 8
1155
+ },
1156
+ rubricHeader: {
1157
+ flexDirection: "row",
1158
+ alignItems: "center",
1159
+ marginBottom: 4
1160
+ },
1161
+ rubricNumber: {
1162
+ width: 24,
1163
+ height: 24,
1164
+ borderRadius: 4,
1165
+ backgroundColor: "#EDE9FE",
1166
+ justifyContent: "center",
1167
+ alignItems: "center",
1168
+ marginRight: 8
1169
+ },
1170
+ rubricNumberText: {
1171
+ fontSize: 12,
1172
+ fontWeight: "600",
1173
+ color: "#7C3AED"
1174
+ },
1175
+ rubricTitle: {
1176
+ flex: 1,
1177
+ fontSize: 14,
1178
+ fontWeight: "500",
1179
+ color: "#111827"
1180
+ },
1181
+ rubricExpected: {
1182
+ fontSize: 12,
1183
+ color: "#6B7280",
1184
+ marginLeft: 32,
1185
+ marginBottom: 8
1186
+ },
1187
+ passFailButtons: {
1188
+ flexDirection: "row",
1189
+ gap: 8,
1190
+ marginLeft: 32
1191
+ },
1192
+ passFailButton: {
1193
+ flex: 1,
1194
+ paddingVertical: 8,
1195
+ paddingHorizontal: 12,
1196
+ borderRadius: 6,
1197
+ backgroundColor: "#F3F4F6",
1198
+ alignItems: "center"
1199
+ },
1200
+ passButtonActive: {
1201
+ backgroundColor: "#22C55E"
1202
+ },
1203
+ failButtonActive: {
1204
+ backgroundColor: "#EF4444"
1205
+ },
1206
+ passFailButtonText: {
1207
+ fontSize: 12,
1208
+ fontWeight: "600",
1209
+ color: "#6B7280"
1210
+ },
1211
+ passButtonTextActive: {
1212
+ color: "#fff"
1213
+ },
1214
+ failButtonTextActive: {
1215
+ color: "#fff"
1216
+ },
1217
+ ratingButtons: {
1218
+ flexDirection: "row",
1219
+ gap: 6,
1220
+ marginLeft: 32
1221
+ },
1222
+ ratingButton: {
1223
+ width: 36,
1224
+ height: 36,
1225
+ borderRadius: 6,
1226
+ backgroundColor: "#F3F4F6",
1227
+ justifyContent: "center",
1228
+ alignItems: "center"
1229
+ },
1230
+ ratingButtonActive: {
1231
+ backgroundColor: "#7C3AED"
1232
+ },
1233
+ ratingButtonText: {
1234
+ fontSize: 14,
1235
+ fontWeight: "600",
1236
+ color: "#6B7280"
1237
+ },
1238
+ ratingButtonTextActive: {
1239
+ color: "#fff"
1240
+ },
1241
+ // Reset row
1242
+ resetRow: {
1243
+ flexDirection: "row",
1244
+ justifyContent: "space-between",
1245
+ alignItems: "center",
1246
+ marginTop: 8
1247
+ },
1248
+ helperText: {
1249
+ fontSize: 12,
1250
+ color: "#9CA3AF"
1251
+ },
1252
+ resetText: {
1253
+ fontSize: 12,
1254
+ color: "#9CA3AF"
1255
+ },
1256
+ // Freeform styles
1257
+ freeformBox: {
1258
+ backgroundColor: "#FFFBEB",
1259
+ borderWidth: 1,
1260
+ borderColor: "#FDE68A",
1261
+ borderRadius: 8,
1262
+ padding: 12,
1263
+ marginTop: 8
1264
+ },
1265
+ freeformTitle: {
1266
+ fontSize: 14,
1267
+ fontWeight: "600",
1268
+ color: "#92400E",
1269
+ marginBottom: 4
1270
+ },
1271
+ freeformText: {
1272
+ fontSize: 12,
1273
+ color: "#A16207",
1274
+ marginBottom: 4
1275
+ },
1276
+ freeformBullet: {
1277
+ fontSize: 12,
1278
+ color: "#A16207",
1279
+ marginLeft: 8
1280
+ },
1281
+ // Expected result
1282
+ expectedResult: {
1283
+ backgroundColor: "#ECFDF5",
1284
+ padding: 12,
1285
+ borderRadius: 8,
1286
+ marginTop: 12
1287
+ },
1288
+ expectedLabel: {
1289
+ fontSize: 12,
1290
+ fontWeight: "600",
1291
+ color: "#065F46",
1292
+ marginBottom: 4
1293
+ },
1294
+ expectedText: {
1295
+ fontSize: 14,
1296
+ color: "#047857"
1297
+ },
1298
+ // Action buttons
1299
+ actionButtons: {
1300
+ flexDirection: "row",
1301
+ gap: 12
1302
+ },
1303
+ failButton: {
1304
+ flex: 1,
1305
+ backgroundColor: "#FEE2E2",
1306
+ paddingVertical: 14,
1307
+ borderRadius: 12,
1308
+ alignItems: "center"
1309
+ },
1310
+ failButtonText: {
1311
+ fontSize: 16,
1312
+ fontWeight: "600",
1313
+ color: "#DC2626"
1314
+ },
1315
+ passButton: {
1316
+ flex: 1,
1317
+ backgroundColor: "#16A34A",
1318
+ paddingVertical: 14,
1319
+ borderRadius: 12,
1320
+ alignItems: "center"
1321
+ },
1322
+ passButtonText: {
1323
+ fontSize: 16,
1324
+ fontWeight: "600",
1325
+ color: "#fff"
1326
+ },
1327
+ // Report form styles
1328
+ reportTypes: {
1329
+ flexDirection: "row",
1330
+ gap: 8,
1331
+ marginBottom: 16
1332
+ },
1333
+ reportTypeButton: {
1334
+ flex: 1,
1335
+ backgroundColor: "#F3F4F6",
1336
+ paddingVertical: 10,
1337
+ borderRadius: 8,
1338
+ alignItems: "center"
1339
+ },
1340
+ reportTypeActive: {
1341
+ backgroundColor: "#EDE9FE",
1342
+ borderWidth: 2,
1343
+ borderColor: "#7C3AED"
1344
+ },
1345
+ reportTypeText: {
1346
+ fontSize: 14,
1347
+ color: "#6B7280"
1348
+ },
1349
+ reportTypeTextActive: {
1350
+ color: "#7C3AED",
1351
+ fontWeight: "600"
1352
+ },
1353
+ severitySection: {
1354
+ marginBottom: 16
1355
+ },
1356
+ label: {
1357
+ fontSize: 14,
1358
+ fontWeight: "500",
1359
+ color: "#374151",
1360
+ marginBottom: 8
1361
+ },
1362
+ severityButtons: {
1363
+ flexDirection: "row",
1364
+ gap: 6
1365
+ },
1366
+ severityButton: {
1367
+ flex: 1,
1368
+ backgroundColor: "#F3F4F6",
1369
+ paddingVertical: 8,
1370
+ borderRadius: 6,
1371
+ alignItems: "center"
1372
+ },
1373
+ severityCriticalActive: {
1374
+ backgroundColor: "#DC2626"
1375
+ },
1376
+ severityHighActive: {
1377
+ backgroundColor: "#F97316"
1378
+ },
1379
+ severityMediumActive: {
1380
+ backgroundColor: "#EAB308"
1381
+ },
1382
+ severityLowActive: {
1383
+ backgroundColor: "#6B7280"
1384
+ },
1385
+ severityText: {
1386
+ fontSize: 12,
1387
+ color: "#6B7280",
1388
+ textTransform: "capitalize"
1389
+ },
1390
+ severityTextActive: {
1391
+ color: "#fff",
1392
+ fontWeight: "600"
1393
+ },
1394
+ descriptionSection: {
1395
+ marginBottom: 16
1396
+ },
1397
+ textInput: {
1398
+ backgroundColor: "#F9FAFB",
1399
+ borderWidth: 1,
1400
+ borderColor: "#E5E7EB",
1401
+ borderRadius: 12,
1402
+ padding: 12,
1403
+ fontSize: 14,
1404
+ minHeight: 100,
1405
+ color: "#111827"
1406
+ },
1407
+ submitButton: {
1408
+ backgroundColor: "#7C3AED",
1409
+ paddingVertical: 14,
1410
+ borderRadius: 12,
1411
+ alignItems: "center"
1412
+ },
1413
+ submitButtonDisabled: {
1414
+ opacity: 0.5
1415
+ },
1416
+ submitButtonText: {
1417
+ fontSize: 16,
1418
+ fontWeight: "600",
1419
+ color: "#fff"
1420
+ },
1421
+ // Message badge (when only messages, not tests)
1422
+ messageBadge: {
1423
+ backgroundColor: "#3B82F6"
1424
+ },
1425
+ // Tab with badge
1426
+ tabWithBadge: {
1427
+ flexDirection: "row",
1428
+ alignItems: "center"
1429
+ },
1430
+ tabBadge: {
1431
+ backgroundColor: "#EF4444",
1432
+ borderRadius: 8,
1433
+ minWidth: 16,
1434
+ height: 16,
1435
+ justifyContent: "center",
1436
+ alignItems: "center",
1437
+ marginLeft: 4,
1438
+ paddingHorizontal: 4
1439
+ },
1440
+ tabBadgeText: {
1441
+ color: "#fff",
1442
+ fontSize: 10,
1443
+ fontWeight: "bold"
1444
+ },
1445
+ // Thread list styles
1446
+ threadItem: {
1447
+ backgroundColor: "#F9FAFB",
1448
+ borderRadius: 12,
1449
+ padding: 12,
1450
+ marginBottom: 8,
1451
+ borderWidth: 1,
1452
+ borderColor: "#E5E7EB"
1453
+ },
1454
+ threadItemUnread: {
1455
+ backgroundColor: "#EFF6FF",
1456
+ borderColor: "#BFDBFE"
1457
+ },
1458
+ threadHeader: {
1459
+ flexDirection: "row",
1460
+ justifyContent: "space-between",
1461
+ alignItems: "center",
1462
+ marginBottom: 4
1463
+ },
1464
+ threadTitleRow: {
1465
+ flexDirection: "row",
1466
+ alignItems: "center",
1467
+ flex: 1
1468
+ },
1469
+ pinnedIcon: {
1470
+ fontSize: 12,
1471
+ marginRight: 4
1472
+ },
1473
+ threadTypeIcon: {
1474
+ fontSize: 14,
1475
+ marginRight: 6
1476
+ },
1477
+ threadSubject: {
1478
+ fontSize: 14,
1479
+ fontWeight: "500",
1480
+ color: "#374151",
1481
+ flex: 1
1482
+ },
1483
+ threadSubjectUnread: {
1484
+ fontWeight: "600",
1485
+ color: "#111827"
1486
+ },
1487
+ priorityDot: {
1488
+ width: 8,
1489
+ height: 8,
1490
+ borderRadius: 4,
1491
+ marginLeft: 8
1492
+ },
1493
+ threadPreview: {
1494
+ fontSize: 13,
1495
+ color: "#6B7280",
1496
+ marginBottom: 4
1497
+ },
1498
+ threadMeta: {
1499
+ flexDirection: "row",
1500
+ justifyContent: "space-between",
1501
+ alignItems: "center"
1502
+ },
1503
+ threadMetaText: {
1504
+ fontSize: 12,
1505
+ color: "#9CA3AF"
1506
+ },
1507
+ unreadBadge: {
1508
+ backgroundColor: "#3B82F6",
1509
+ paddingHorizontal: 8,
1510
+ paddingVertical: 2,
1511
+ borderRadius: 10
1512
+ },
1513
+ unreadBadgeText: {
1514
+ fontSize: 11,
1515
+ fontWeight: "600",
1516
+ color: "#fff"
1517
+ },
1518
+ threadTime: {
1519
+ fontSize: 12,
1520
+ color: "#9CA3AF"
1521
+ },
1522
+ // Thread detail styles
1523
+ threadDetailContainer: {
1524
+ flex: 1
1525
+ },
1526
+ threadDetailHeader: {
1527
+ flexDirection: "row",
1528
+ alignItems: "center",
1529
+ backgroundColor: "#F3F4F6",
1530
+ padding: 12,
1531
+ borderRadius: 8,
1532
+ marginBottom: 12
1533
+ },
1534
+ threadDetailIcon: {
1535
+ fontSize: 18,
1536
+ marginRight: 8
1537
+ },
1538
+ threadDetailSubject: {
1539
+ flex: 1,
1540
+ fontSize: 15,
1541
+ fontWeight: "600",
1542
+ color: "#111827"
1543
+ },
1544
+ loadingMessages: {
1545
+ padding: 20,
1546
+ alignItems: "center"
1547
+ },
1548
+ loadingText: {
1549
+ fontSize: 14,
1550
+ color: "#6B7280"
1551
+ },
1552
+ messagesContainer: {
1553
+ paddingBottom: 8
1554
+ },
1555
+ messageBubble: {
1556
+ maxWidth: "80%",
1557
+ padding: 12,
1558
+ borderRadius: 16,
1559
+ marginBottom: 8
1560
+ },
1561
+ messageBubbleAdmin: {
1562
+ alignSelf: "flex-start",
1563
+ backgroundColor: "#F3F4F6",
1564
+ borderBottomLeftRadius: 4
1565
+ },
1566
+ messageBubbleTester: {
1567
+ alignSelf: "flex-end",
1568
+ backgroundColor: "#7C3AED",
1569
+ borderBottomRightRadius: 4
1570
+ },
1571
+ messageSender: {
1572
+ fontSize: 12,
1573
+ fontWeight: "600",
1574
+ color: "#6B7280",
1575
+ marginBottom: 2
1576
+ },
1577
+ messageSenderTester: {
1578
+ color: "#DDD6FE"
1579
+ },
1580
+ messageContent: {
1581
+ fontSize: 14,
1582
+ color: "#111827",
1583
+ lineHeight: 20
1584
+ },
1585
+ messageContentTester: {
1586
+ color: "#fff"
1587
+ },
1588
+ messageTime: {
1589
+ fontSize: 11,
1590
+ color: "#9CA3AF",
1591
+ marginTop: 4,
1592
+ textAlign: "right"
1593
+ },
1594
+ messageTimeTester: {
1595
+ color: "#C4B5FD"
1596
+ },
1597
+ // Reply composer styles
1598
+ replyComposer: {
1599
+ flexDirection: "row",
1600
+ alignItems: "flex-end",
1601
+ padding: 12,
1602
+ borderTopWidth: 1,
1603
+ borderTopColor: "#E5E7EB",
1604
+ backgroundColor: "#fff"
1605
+ },
1606
+ replyInput: {
1607
+ flex: 1,
1608
+ backgroundColor: "#F9FAFB",
1609
+ borderWidth: 1,
1610
+ borderColor: "#E5E7EB",
1611
+ borderRadius: 20,
1612
+ paddingHorizontal: 16,
1613
+ paddingVertical: 10,
1614
+ fontSize: 14,
1615
+ color: "#111827",
1616
+ maxHeight: 100,
1617
+ marginRight: 8
1618
+ },
1619
+ sendButton: {
1620
+ backgroundColor: "#7C3AED",
1621
+ paddingHorizontal: 16,
1622
+ paddingVertical: 10,
1623
+ borderRadius: 20,
1624
+ justifyContent: "center",
1625
+ alignItems: "center"
1626
+ },
1627
+ sendButtonDisabled: {
1628
+ backgroundColor: "#C4B5FD"
1629
+ },
1630
+ sendButtonText: {
1631
+ fontSize: 14,
1632
+ fontWeight: "600",
1633
+ color: "#fff"
1634
+ }
1635
+ });
1636
+ export {
1637
+ BugBearButton,
1638
+ BugBearProvider,
1639
+ useBugBear
1640
+ };