@upstep/react-native 0.5.1 → 0.6.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.
- package/package.json +2 -5
- package/src/FeedbackContext.tsx +9 -8
- package/src/FeedbackSheet.tsx +171 -137
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@upstep/react-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Upstep feedback SDK for React Native",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -15,11 +15,8 @@
|
|
|
15
15
|
"@upstep/types": "0.5.0"
|
|
16
16
|
},
|
|
17
17
|
"peerDependencies": {
|
|
18
|
-
"@gorhom/bottom-sheet": ">=4",
|
|
19
18
|
"react": ">=18",
|
|
20
|
-
"react-native": ">=0.73"
|
|
21
|
-
"react-native-gesture-handler": ">=2",
|
|
22
|
-
"react-native-reanimated": ">=3"
|
|
19
|
+
"react-native": ">=0.73"
|
|
23
20
|
},
|
|
24
21
|
"devDependencies": {
|
|
25
22
|
"@types/react": "^19.0.7",
|
package/src/FeedbackContext.tsx
CHANGED
|
@@ -4,7 +4,6 @@ import React, {
|
|
|
4
4
|
useEffect,
|
|
5
5
|
useState,
|
|
6
6
|
useCallback,
|
|
7
|
-
useRef,
|
|
8
7
|
type ReactNode,
|
|
9
8
|
} from "react";
|
|
10
9
|
import type {
|
|
@@ -15,7 +14,6 @@ import type {
|
|
|
15
14
|
VoteValue,
|
|
16
15
|
} from "@upstep/types";
|
|
17
16
|
import { UpstepApiClient } from "./api";
|
|
18
|
-
import type BottomSheet from "@gorhom/bottom-sheet";
|
|
19
17
|
|
|
20
18
|
const DEFAULT_ACCENT = "#D97757";
|
|
21
19
|
|
|
@@ -27,11 +25,10 @@ interface UpstepContextValue {
|
|
|
27
25
|
submit: (title: string, content: string, type?: FeedbackType) => Promise<void>;
|
|
28
26
|
vote: (feedbackId: string, value: VoteValue) => Promise<void>;
|
|
29
27
|
getItem: (feedbackId: string) => Promise<FeedbackWithComments>;
|
|
30
|
-
|
|
28
|
+
isOpen: boolean;
|
|
31
29
|
openSheet: () => void;
|
|
32
|
-
|
|
30
|
+
closeSheet: () => void;
|
|
33
31
|
identify: (userId: string | undefined) => void;
|
|
34
|
-
sheetRef: React.RefObject<BottomSheet | null>;
|
|
35
32
|
accentColor: string;
|
|
36
33
|
theme: "light" | "dark" | "auto";
|
|
37
34
|
}
|
|
@@ -45,7 +42,7 @@ export function FeedbackProvider({
|
|
|
45
42
|
const [client] = useState(() => new UpstepApiClient(config));
|
|
46
43
|
const [feedItems, setFeedItems] = useState<Feedback[]>([]);
|
|
47
44
|
const [feedLoading, setFeedLoading] = useState(true);
|
|
48
|
-
const
|
|
45
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
49
46
|
|
|
50
47
|
useEffect(() => {
|
|
51
48
|
client.setUserId(config.userId);
|
|
@@ -87,7 +84,11 @@ export function FeedbackProvider({
|
|
|
87
84
|
);
|
|
88
85
|
|
|
89
86
|
const openSheet = useCallback(() => {
|
|
90
|
-
|
|
87
|
+
setIsOpen(true);
|
|
88
|
+
}, []);
|
|
89
|
+
|
|
90
|
+
const closeSheet = useCallback(() => {
|
|
91
|
+
setIsOpen(false);
|
|
91
92
|
}, []);
|
|
92
93
|
|
|
93
94
|
const identify = useCallback(
|
|
@@ -99,7 +100,7 @@ export function FeedbackProvider({
|
|
|
99
100
|
<UpstepContext.Provider
|
|
100
101
|
value={{
|
|
101
102
|
client, feedItems, feedLoading, loadFeed, submit, vote, getItem,
|
|
102
|
-
openSheet,
|
|
103
|
+
isOpen, openSheet, closeSheet, identify,
|
|
103
104
|
accentColor: config.accentColor ?? DEFAULT_ACCENT,
|
|
104
105
|
theme: config.theme ?? "auto",
|
|
105
106
|
}}
|
package/src/FeedbackSheet.tsx
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import React, { useState,
|
|
1
|
+
import React, { useState, useCallback, useEffect } from "react";
|
|
2
2
|
import {
|
|
3
|
+
Modal,
|
|
3
4
|
View,
|
|
4
5
|
Text,
|
|
5
6
|
TextInput,
|
|
6
7
|
TouchableOpacity,
|
|
8
|
+
Pressable,
|
|
9
|
+
ScrollView,
|
|
10
|
+
KeyboardAvoidingView,
|
|
7
11
|
StyleSheet,
|
|
8
12
|
ActivityIndicator,
|
|
9
13
|
useColorScheme,
|
|
10
14
|
Platform,
|
|
15
|
+
SafeAreaView,
|
|
16
|
+
Linking,
|
|
11
17
|
} from "react-native";
|
|
12
|
-
import BottomSheet, {
|
|
13
|
-
BottomSheetView,
|
|
14
|
-
BottomSheetScrollView,
|
|
15
|
-
BottomSheetBackdrop,
|
|
16
|
-
} from "@gorhom/bottom-sheet";
|
|
17
|
-
import type { BottomSheetBackdropProps } from "@gorhom/bottom-sheet";
|
|
18
18
|
import type { Comment, Feedback, FeedbackType, FeedbackWithComments } from "@upstep/types";
|
|
19
19
|
import { useUpstep } from "./FeedbackContext";
|
|
20
20
|
|
|
@@ -58,116 +58,130 @@ type Nav =
|
|
|
58
58
|
| { screen: "create" };
|
|
59
59
|
|
|
60
60
|
// ─── FeedbackSheet ────────────────────────────────────────────────────────────
|
|
61
|
-
//
|
|
62
|
-
// IMPORTANT: BottomSheet requires stable direct children — never conditionally
|
|
63
|
-
// render <BottomSheetView> or <BottomSheetScrollView> as they set up gesture
|
|
64
|
-
// handlers on mount. We keep ONE of each always mounted; only their contents
|
|
65
|
-
// change based on `nav`. This also fixes TouchableOpacity not responding.
|
|
66
61
|
|
|
67
62
|
export function FeedbackSheet() {
|
|
68
|
-
const {
|
|
63
|
+
const { isOpen, closeSheet, accentColor, theme, loadFeed } = useUpstep();
|
|
69
64
|
const scheme = useColorScheme();
|
|
70
65
|
const isDark = theme === "dark" || (theme === "auto" && scheme === "dark");
|
|
71
66
|
const p = isDark ? DARK : LIGHT;
|
|
72
67
|
|
|
73
68
|
const [nav, setNav] = useState<Nav>({ screen: "list" });
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
),
|
|
86
|
-
[]
|
|
87
|
-
);
|
|
69
|
+
|
|
70
|
+
// Refresh list every time the modal opens
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (isOpen) loadFeed();
|
|
73
|
+
}, [isOpen, loadFeed]);
|
|
74
|
+
|
|
75
|
+
function handleClose() {
|
|
76
|
+
closeSheet();
|
|
77
|
+
// Reset to list when closed so next open starts fresh
|
|
78
|
+
setTimeout(() => setNav({ screen: "list" }), 300);
|
|
79
|
+
}
|
|
88
80
|
|
|
89
81
|
return (
|
|
90
|
-
<
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
backgroundStyle={{ backgroundColor: p.bg, borderRadius: 24 }}
|
|
97
|
-
handleIndicatorStyle={{ backgroundColor: p.handle, width: 40, height: 4 }}
|
|
98
|
-
onChange={(index) => { if (index >= 0) loadFeed(); }}
|
|
99
|
-
onClose={() => setNav({ screen: "list" })}
|
|
82
|
+
<Modal
|
|
83
|
+
visible={isOpen}
|
|
84
|
+
animationType="slide"
|
|
85
|
+
transparent
|
|
86
|
+
statusBarTranslucent
|
|
87
|
+
onRequestClose={handleClose}
|
|
100
88
|
>
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
89
|
+
<KeyboardAvoidingView
|
|
90
|
+
style={styles.overlay}
|
|
91
|
+
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
|
92
|
+
>
|
|
93
|
+
{/* Tap backdrop to dismiss */}
|
|
94
|
+
<Pressable style={styles.backdrop} onPress={handleClose} />
|
|
95
|
+
|
|
96
|
+
{/* Sheet */}
|
|
97
|
+
<View style={[styles.sheet, { backgroundColor: p.bg }]}>
|
|
98
|
+
{/* Drag handle */}
|
|
99
|
+
<View style={styles.handleWrap}>
|
|
100
|
+
<View style={[styles.handle, { backgroundColor: p.handle }]} />
|
|
101
|
+
</View>
|
|
102
|
+
|
|
103
|
+
{/* Fixed header */}
|
|
104
|
+
<View style={[styles.header, { borderBottomColor: p.border }]}>
|
|
105
|
+
{nav.screen === "list" ? (
|
|
106
|
+
<>
|
|
107
|
+
<Text style={[styles.headerTitle, { color: p.text }]}>Feedback</Text>
|
|
108
|
+
<TouchableOpacity
|
|
109
|
+
onPress={() => setNav({ screen: "create" })}
|
|
110
|
+
activeOpacity={0.8}
|
|
111
|
+
style={[styles.newBtn, { backgroundColor: accentColor }]}
|
|
112
|
+
>
|
|
113
|
+
<Text style={styles.newBtnText}>+ New</Text>
|
|
114
|
+
</TouchableOpacity>
|
|
115
|
+
</>
|
|
116
|
+
) : nav.screen === "create" ? (
|
|
117
|
+
<>
|
|
118
|
+
<TouchableOpacity
|
|
119
|
+
onPress={() => setNav({ screen: "list" })}
|
|
120
|
+
hitSlop={{ top: 10, bottom: 10, left: 10, right: 16 }}
|
|
121
|
+
>
|
|
122
|
+
<Text style={[styles.navBtn, { color: accentColor }]}>Cancel</Text>
|
|
123
|
+
</TouchableOpacity>
|
|
124
|
+
<Text style={[styles.headerTitle, { color: p.text }]}>New feedback</Text>
|
|
125
|
+
<View style={styles.navBtnSpacer} />
|
|
126
|
+
</>
|
|
127
|
+
) : (
|
|
122
128
|
<TouchableOpacity
|
|
123
129
|
onPress={() => setNav({ screen: "list" })}
|
|
124
|
-
hitSlop={{ top:
|
|
130
|
+
hitSlop={{ top: 10, bottom: 10, left: 10, right: 16 }}
|
|
125
131
|
>
|
|
126
|
-
<Text style={[styles.navBtn, { color: accentColor }]}
|
|
132
|
+
<Text style={[styles.navBtn, { color: accentColor }]}>← Back</Text>
|
|
127
133
|
</TouchableOpacity>
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
134
|
+
)}
|
|
135
|
+
</View>
|
|
136
|
+
|
|
137
|
+
{/* Scrollable body */}
|
|
138
|
+
<ScrollView
|
|
139
|
+
style={styles.scroll}
|
|
140
|
+
contentContainerStyle={styles.scrollContent}
|
|
141
|
+
keyboardShouldPersistTaps="handled"
|
|
142
|
+
showsVerticalScrollIndicator={false}
|
|
143
|
+
>
|
|
144
|
+
{nav.screen === "list" && (
|
|
145
|
+
<FeedListBody
|
|
146
|
+
p={p}
|
|
147
|
+
accent={accentColor}
|
|
148
|
+
onSelect={(id) => setNav({ screen: "detail", feedbackId: id })}
|
|
149
|
+
/>
|
|
150
|
+
)}
|
|
151
|
+
{nav.screen === "detail" && "feedbackId" in nav && (
|
|
152
|
+
<FeedDetailBody
|
|
153
|
+
p={p}
|
|
154
|
+
accent={accentColor}
|
|
155
|
+
feedbackId={(nav as { screen: "detail"; feedbackId: string }).feedbackId}
|
|
156
|
+
/>
|
|
157
|
+
)}
|
|
158
|
+
{nav.screen === "create" && (
|
|
159
|
+
<FeedCreateBody
|
|
160
|
+
p={p}
|
|
161
|
+
accent={accentColor}
|
|
162
|
+
onDone={() => {
|
|
163
|
+
setNav({ screen: "list" });
|
|
164
|
+
}}
|
|
165
|
+
/>
|
|
166
|
+
)}
|
|
167
|
+
</ScrollView>
|
|
168
|
+
|
|
169
|
+
{/* Branding */}
|
|
170
|
+
<TouchableOpacity
|
|
171
|
+
onPress={() => Linking.openURL("https://upstep.dev")}
|
|
172
|
+
activeOpacity={0.6}
|
|
173
|
+
style={[styles.poweredBy, { borderTopColor: p.border }]}
|
|
174
|
+
>
|
|
175
|
+
<Text style={[styles.poweredByText, { color: p.textFaint }]}>
|
|
176
|
+
Powered by <Text style={{ fontWeight: "700" }}>Upstep.dev</Text>
|
|
177
|
+
</Text>
|
|
178
|
+
</TouchableOpacity>
|
|
140
179
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
>
|
|
147
|
-
{nav.screen === "list" && (
|
|
148
|
-
<FeedListBody
|
|
149
|
-
p={p}
|
|
150
|
-
accent={accentColor}
|
|
151
|
-
onSelect={(id) => setNav({ screen: "detail", feedbackId: id })}
|
|
152
|
-
/>
|
|
153
|
-
)}
|
|
154
|
-
{nav.screen === "detail" && "feedbackId" in nav && (
|
|
155
|
-
<FeedDetailBody
|
|
156
|
-
p={p}
|
|
157
|
-
accent={accentColor}
|
|
158
|
-
feedbackId={(nav as { screen: "detail"; feedbackId: string }).feedbackId}
|
|
159
|
-
/>
|
|
160
|
-
)}
|
|
161
|
-
{nav.screen === "create" && (
|
|
162
|
-
<FeedCreateBody
|
|
163
|
-
p={p}
|
|
164
|
-
accent={accentColor}
|
|
165
|
-
onDone={() => setNav({ screen: "list" })}
|
|
166
|
-
/>
|
|
167
|
-
)}
|
|
168
|
-
</BottomSheetScrollView>
|
|
169
|
-
</BottomSheetView>
|
|
170
|
-
</BottomSheet>
|
|
180
|
+
{/* Safe area spacer — fills the home-indicator / nav-bar zone */}
|
|
181
|
+
<SafeAreaView style={{ backgroundColor: p.bg }} />
|
|
182
|
+
</View>
|
|
183
|
+
</KeyboardAvoidingView>
|
|
184
|
+
</Modal>
|
|
171
185
|
);
|
|
172
186
|
}
|
|
173
187
|
|
|
@@ -176,8 +190,7 @@ export function FeedbackSheet() {
|
|
|
176
190
|
function FeedListBody({
|
|
177
191
|
p, accent, onSelect,
|
|
178
192
|
}: {
|
|
179
|
-
p: Palette; accent: string;
|
|
180
|
-
onSelect: (id: string) => void;
|
|
193
|
+
p: Palette; accent: string; onSelect: (id: string) => void;
|
|
181
194
|
}) {
|
|
182
195
|
const { feedItems, feedLoading, vote } = useUpstep();
|
|
183
196
|
|
|
@@ -200,7 +213,7 @@ function FeedListBody({
|
|
|
200
213
|
}
|
|
201
214
|
|
|
202
215
|
return (
|
|
203
|
-
|
|
216
|
+
<>
|
|
204
217
|
{feedItems.map((f) => (
|
|
205
218
|
<FeedCard
|
|
206
219
|
key={f.id}
|
|
@@ -211,7 +224,7 @@ function FeedListBody({
|
|
|
211
224
|
onVote={() => vote(f.id, "UP")}
|
|
212
225
|
/>
|
|
213
226
|
))}
|
|
214
|
-
|
|
227
|
+
</>
|
|
215
228
|
);
|
|
216
229
|
}
|
|
217
230
|
|
|
@@ -219,8 +232,7 @@ function FeedCard({
|
|
|
219
232
|
item, p, accent, onPress, onVote,
|
|
220
233
|
}: {
|
|
221
234
|
item: Feedback; p: Palette; accent: string;
|
|
222
|
-
onPress: () => void;
|
|
223
|
-
onVote: () => void;
|
|
235
|
+
onPress: () => void; onVote: () => void;
|
|
224
236
|
}) {
|
|
225
237
|
const voted = item.userVote === "UP";
|
|
226
238
|
const displayTitle = item.title ?? item.content;
|
|
@@ -259,9 +271,7 @@ function FeedCard({
|
|
|
259
271
|
<View style={styles.cardMeta}>
|
|
260
272
|
<TypeBadge type={item.type} p={p} />
|
|
261
273
|
<StatusDot status={item.status} />
|
|
262
|
-
<Text style={[styles.cardDate, { color: p.textFaint }]}>
|
|
263
|
-
{fmtDate(item.createdAt)}
|
|
264
|
-
</Text>
|
|
274
|
+
<Text style={[styles.cardDate, { color: p.textFaint }]}>{fmtDate(item.createdAt)}</Text>
|
|
265
275
|
</View>
|
|
266
276
|
</View>
|
|
267
277
|
|
|
@@ -350,9 +360,7 @@ function FeedDetailBody({
|
|
|
350
360
|
|
|
351
361
|
{item.comments.length > 0 && (
|
|
352
362
|
<View style={[styles.commentsSection, { borderTopColor: p.border }]}>
|
|
353
|
-
<Text style={[styles.commentsLabel, { color: p.textFaint }]}>
|
|
354
|
-
DEVELOPER RESPONSE
|
|
355
|
-
</Text>
|
|
363
|
+
<Text style={[styles.commentsLabel, { color: p.textFaint }]}>DEVELOPER RESPONSE</Text>
|
|
356
364
|
{item.comments.map((c) => (
|
|
357
365
|
<CommentBubble key={c.id} comment={c} p={p} accent={accent} />
|
|
358
366
|
))}
|
|
@@ -371,9 +379,7 @@ function CommentBubble({ comment, p, accent }: { comment: Comment; p: Palette; a
|
|
|
371
379
|
{comment.authorName ?? "Team"}
|
|
372
380
|
</Text>
|
|
373
381
|
</View>
|
|
374
|
-
<Text style={[styles.commentDate, { color: p.textFaint }]}>
|
|
375
|
-
{fmtDate(comment.createdAt)}
|
|
376
|
-
</Text>
|
|
382
|
+
<Text style={[styles.commentDate, { color: p.textFaint }]}>{fmtDate(comment.createdAt)}</Text>
|
|
377
383
|
</View>
|
|
378
384
|
<Text style={[styles.commentContent, { color: p.textSoft }]}>{comment.content}</Text>
|
|
379
385
|
</View>
|
|
@@ -409,7 +415,7 @@ function FeedCreateBody({
|
|
|
409
415
|
try {
|
|
410
416
|
await submit(title.trim(), description.trim() || title.trim(), type);
|
|
411
417
|
setSuccess(true);
|
|
412
|
-
setTimeout(onDone,
|
|
418
|
+
setTimeout(onDone, 1200);
|
|
413
419
|
} catch {
|
|
414
420
|
setError("Something went wrong. Please try again.");
|
|
415
421
|
} finally {
|
|
@@ -419,7 +425,7 @@ function FeedCreateBody({
|
|
|
419
425
|
|
|
420
426
|
if (success) {
|
|
421
427
|
return (
|
|
422
|
-
<View style={[styles.centered, { paddingTop:
|
|
428
|
+
<View style={[styles.centered, { paddingTop: 40 }]}>
|
|
423
429
|
<Text style={[styles.successTitle, { color: p.text }]}>Submitted</Text>
|
|
424
430
|
<Text style={[styles.successSub, { color: p.textFaint }]}>
|
|
425
431
|
Thanks — we received your feedback.
|
|
@@ -519,11 +525,8 @@ function TypeBadge({ type, p }: { type: FeedbackType; p: Palette }) {
|
|
|
519
525
|
}
|
|
520
526
|
|
|
521
527
|
const STATUS_COLOR: Record<string, string> = {
|
|
522
|
-
OPEN: "#b45309",
|
|
523
|
-
|
|
524
|
-
DONE: "#15803d",
|
|
525
|
-
PENDING: "#ea580c",
|
|
526
|
-
CLOSED: "#6b7280",
|
|
528
|
+
OPEN: "#b45309", IN_PROGRESS: "#1d4ed8", DONE: "#15803d",
|
|
529
|
+
PENDING: "#ea580c", CLOSED: "#6b7280",
|
|
527
530
|
};
|
|
528
531
|
|
|
529
532
|
function StatusDot({ status }: { status: string }) {
|
|
@@ -534,9 +537,7 @@ function StatusBadge({ status, p }: { status: string; p: Palette }) {
|
|
|
534
537
|
return (
|
|
535
538
|
<View style={[styles.statusBadge, { backgroundColor: p.bgSoft, borderColor: p.border }]}>
|
|
536
539
|
<StatusDot status={status} />
|
|
537
|
-
<Text style={[styles.statusBadgeText, { color: p.textSoft }]}>
|
|
538
|
-
{status.replace("_", " ")}
|
|
539
|
-
</Text>
|
|
540
|
+
<Text style={[styles.statusBadgeText, { color: p.textSoft }]}>{status.replace("_", " ")}</Text>
|
|
540
541
|
</View>
|
|
541
542
|
);
|
|
542
543
|
}
|
|
@@ -548,9 +549,35 @@ function fmtDate(iso: string) {
|
|
|
548
549
|
// ─── Styles ───────────────────────────────────────────────────────────────────
|
|
549
550
|
|
|
550
551
|
const styles = StyleSheet.create({
|
|
551
|
-
//
|
|
552
|
-
|
|
552
|
+
// Modal overlay
|
|
553
|
+
overlay: {
|
|
554
|
+
flex: 1,
|
|
555
|
+
justifyContent: "flex-end",
|
|
556
|
+
},
|
|
557
|
+
backdrop: {
|
|
558
|
+
...StyleSheet.absoluteFill,
|
|
559
|
+
backgroundColor: "rgba(0,0,0,0.45)",
|
|
560
|
+
},
|
|
553
561
|
|
|
562
|
+
// Sheet container — fixed height so it's always fully open regardless of content
|
|
563
|
+
sheet: {
|
|
564
|
+
height: "92%",
|
|
565
|
+
borderTopLeftRadius: 24,
|
|
566
|
+
borderTopRightRadius: 24,
|
|
567
|
+
overflow: "hidden",
|
|
568
|
+
},
|
|
569
|
+
handleWrap: {
|
|
570
|
+
alignItems: "center",
|
|
571
|
+
paddingTop: 10,
|
|
572
|
+
paddingBottom: 4,
|
|
573
|
+
},
|
|
574
|
+
handle: {
|
|
575
|
+
width: 40,
|
|
576
|
+
height: 4,
|
|
577
|
+
borderRadius: 2,
|
|
578
|
+
},
|
|
579
|
+
|
|
580
|
+
// Header
|
|
554
581
|
header: {
|
|
555
582
|
flexDirection: "row",
|
|
556
583
|
alignItems: "center",
|
|
@@ -561,15 +588,15 @@ const styles = StyleSheet.create({
|
|
|
561
588
|
},
|
|
562
589
|
headerTitle: { fontSize: 16, fontWeight: "700", letterSpacing: -0.3 },
|
|
563
590
|
navBtn: { fontSize: 14, fontWeight: "600" },
|
|
564
|
-
|
|
591
|
+
navBtnSpacer: { width: 60 },
|
|
565
592
|
newBtn: { borderRadius: 20, paddingHorizontal: 14, paddingVertical: 7 },
|
|
566
593
|
newBtnText: { color: "#fff", fontSize: 13, fontWeight: "700" },
|
|
567
594
|
|
|
568
|
-
|
|
569
|
-
|
|
595
|
+
// Scroll — flex: 1 fills all remaining space after the fixed header
|
|
596
|
+
scroll: { flex: 1 },
|
|
597
|
+
scrollContent: { flexGrow: 1, padding: 16, paddingBottom: 32 },
|
|
570
598
|
|
|
571
599
|
// List
|
|
572
|
-
listPad: { padding: 16, paddingBottom: 40 },
|
|
573
600
|
centered: { alignItems: "center", justifyContent: "center", paddingVertical: 60 },
|
|
574
601
|
emptyText: { fontSize: 14 },
|
|
575
602
|
card: {
|
|
@@ -599,7 +626,7 @@ const styles = StyleSheet.create({
|
|
|
599
626
|
chevron: { fontSize: 22, fontWeight: "300", marginLeft: 4 },
|
|
600
627
|
|
|
601
628
|
// Detail
|
|
602
|
-
detailPad: {
|
|
629
|
+
detailPad: { paddingTop: 4 },
|
|
603
630
|
detailTitle: { fontSize: 22, fontWeight: "700", letterSpacing: -0.4, marginBottom: 10, lineHeight: 28 },
|
|
604
631
|
badgeRow: { flexDirection: "row", gap: 8, marginBottom: 16 },
|
|
605
632
|
detailContent: { fontSize: 15, lineHeight: 23, marginBottom: 24 },
|
|
@@ -615,8 +642,7 @@ const styles = StyleSheet.create({
|
|
|
615
642
|
commentsSection: { marginTop: 28, borderTopWidth: StyleSheet.hairlineWidth, paddingTop: 20 },
|
|
616
643
|
commentsLabel: { fontSize: 10, fontWeight: "700", letterSpacing: 0.8, marginBottom: 12 },
|
|
617
644
|
commentBubble: {
|
|
618
|
-
borderRadius: 12, borderWidth: StyleSheet.hairlineWidth,
|
|
619
|
-
padding: 14, marginBottom: 10,
|
|
645
|
+
borderRadius: 12, borderWidth: StyleSheet.hairlineWidth, padding: 14, marginBottom: 10,
|
|
620
646
|
},
|
|
621
647
|
commentMeta: { flexDirection: "row", alignItems: "center", gap: 8, marginBottom: 8 },
|
|
622
648
|
ownerBadge: { borderRadius: 20, paddingHorizontal: 8, paddingVertical: 3 },
|
|
@@ -625,7 +651,7 @@ const styles = StyleSheet.create({
|
|
|
625
651
|
commentContent: { fontSize: 14, lineHeight: 21 },
|
|
626
652
|
|
|
627
653
|
// Create
|
|
628
|
-
createPad: {
|
|
654
|
+
createPad: { paddingTop: 4 },
|
|
629
655
|
fieldLabel: { fontSize: 10, fontWeight: "700", letterSpacing: 0.8, marginBottom: 8 },
|
|
630
656
|
typeRow: { flexDirection: "row", gap: 8, marginBottom: 20, flexWrap: "wrap" },
|
|
631
657
|
typeBtn: { paddingVertical: 9, paddingHorizontal: 14, borderRadius: 10, borderWidth: 1 },
|
|
@@ -649,6 +675,14 @@ const styles = StyleSheet.create({
|
|
|
649
675
|
successTitle: { fontSize: 20, fontWeight: "700", textAlign: "center", marginBottom: 6 },
|
|
650
676
|
successSub: { fontSize: 14, textAlign: "center" },
|
|
651
677
|
|
|
678
|
+
// Branding
|
|
679
|
+
poweredBy: {
|
|
680
|
+
alignItems: "center",
|
|
681
|
+
paddingVertical: 10,
|
|
682
|
+
borderTopWidth: StyleSheet.hairlineWidth,
|
|
683
|
+
},
|
|
684
|
+
poweredByText: { fontSize: 11 },
|
|
685
|
+
|
|
652
686
|
// Shared badges
|
|
653
687
|
badge: { paddingHorizontal: 7, paddingVertical: 3, borderRadius: 6 },
|
|
654
688
|
badgeText: { fontSize: 10, fontWeight: "700", letterSpacing: 0.2 },
|