@heedkit/sdk-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/LICENSE +21 -0
- package/README.md +36 -0
- package/lib/commonjs/client.js +207 -0
- package/lib/commonjs/client.js.map +1 -0
- package/lib/commonjs/index.js +691 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/module/client.js +201 -0
- package/lib/module/client.js.map +1 -0
- package/lib/module/index.js +678 -0
- package/lib/module/index.js.map +1 -0
- package/lib/typescript/commonjs/client.d.ts +120 -0
- package/lib/typescript/commonjs/client.d.ts.map +1 -0
- package/lib/typescript/commonjs/index.d.ts +24 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/package.json +1 -0
- package/lib/typescript/module/client.d.ts +120 -0
- package/lib/typescript/module/client.d.ts.map +1 -0
- package/lib/typescript/module/index.d.ts +24 -0
- package/lib/typescript/module/index.d.ts.map +1 -0
- package/lib/typescript/module/package.json +1 -0
- package/package.json +83 -0
- package/src/client.test.ts +330 -0
- package/src/client.ts +301 -0
- package/src/index.tsx +526 -0
package/src/index.tsx
ADDED
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import {
|
|
3
|
+
ActivityIndicator,
|
|
4
|
+
Appearance,
|
|
5
|
+
FlatList,
|
|
6
|
+
Modal,
|
|
7
|
+
Pressable,
|
|
8
|
+
ScrollView,
|
|
9
|
+
StyleSheet,
|
|
10
|
+
Text,
|
|
11
|
+
TextInput,
|
|
12
|
+
View,
|
|
13
|
+
} from "react-native";
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
HeedKitClient,
|
|
17
|
+
type Comment,
|
|
18
|
+
type EndUser,
|
|
19
|
+
type Feature,
|
|
20
|
+
type FeatureKind,
|
|
21
|
+
type HeedKitConfig,
|
|
22
|
+
type GroupMode,
|
|
23
|
+
type InitResult,
|
|
24
|
+
type Interaction,
|
|
25
|
+
type KindInteractions,
|
|
26
|
+
type ShowCounts,
|
|
27
|
+
type Theme,
|
|
28
|
+
type Visibility,
|
|
29
|
+
} from "./client";
|
|
30
|
+
|
|
31
|
+
export {
|
|
32
|
+
HeedKitClient,
|
|
33
|
+
type Comment,
|
|
34
|
+
type EndUser,
|
|
35
|
+
type Feature,
|
|
36
|
+
type FeatureKind,
|
|
37
|
+
type HeedKitConfig,
|
|
38
|
+
type GroupMode,
|
|
39
|
+
type InitResult,
|
|
40
|
+
type Interaction,
|
|
41
|
+
type KindInteractions,
|
|
42
|
+
type ShowCounts,
|
|
43
|
+
type Theme,
|
|
44
|
+
type Visibility,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Per-kind metadata. Kept here (not the client) because it's view-layer copy.
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
const KIND_META: Record<FeatureKind, { label: string; placeholder: string; tabIcon: string }> = {
|
|
52
|
+
feature_request: { label: "Features", placeholder: "What should we build?", tabIcon: "💡" },
|
|
53
|
+
bug_report: { label: "Bugs", placeholder: "What's broken?", tabIcon: "🐞" },
|
|
54
|
+
improvement: { label: "Improvements", placeholder: "What could be better?", tabIcon: "✨" },
|
|
55
|
+
appreciation: { label: "Appreciation", placeholder: "What did you love?", tabIcon: "❤️" },
|
|
56
|
+
other: { label: "Other", placeholder: "Tell us anything", tabIcon: "💬" },
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const INTERACTION_META: Record<Interaction, { icon: string; label: string }> = {
|
|
60
|
+
upvote: { icon: "▲", label: "Upvote" },
|
|
61
|
+
downvote: { icon: "▼", label: "Downvote" },
|
|
62
|
+
plus_one: { icon: "+1", label: "+1" },
|
|
63
|
+
like: { icon: "♥", label: "Like" },
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const FONT_SIZES: Record<NonNullable<Theme["font_size"]>, number> = {
|
|
67
|
+
sm: 13,
|
|
68
|
+
md: 14,
|
|
69
|
+
lg: 16,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Context
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
type Ctx = { client: HeedKitClient; ready: boolean; theme: Theme };
|
|
77
|
+
|
|
78
|
+
const HeedKitContext = React.createContext<Ctx | null>(null);
|
|
79
|
+
|
|
80
|
+
export function HeedKitProvider({
|
|
81
|
+
projectKey,
|
|
82
|
+
apiUrl,
|
|
83
|
+
user,
|
|
84
|
+
children,
|
|
85
|
+
}: { children: React.ReactNode } & HeedKitConfig) {
|
|
86
|
+
const [client] = React.useState(() => new HeedKitClient({ projectKey, apiUrl, user }));
|
|
87
|
+
const [ready, setReady] = React.useState(false);
|
|
88
|
+
const [theme, setTheme] = React.useState<Theme>({});
|
|
89
|
+
|
|
90
|
+
React.useEffect(() => {
|
|
91
|
+
client.init({ ...user, platform: user?.platform || "react-native" }).then(() => {
|
|
92
|
+
setTheme(client.getTheme());
|
|
93
|
+
setReady(true);
|
|
94
|
+
});
|
|
95
|
+
}, [client]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<HeedKitContext.Provider value={{ client, ready, theme }}>
|
|
99
|
+
{children}
|
|
100
|
+
</HeedKitContext.Provider>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function useHeedKit() {
|
|
105
|
+
const ctx = React.useContext(HeedKitContext);
|
|
106
|
+
if (!ctx) throw new Error("useHeedKit must be used inside <HeedKitProvider>");
|
|
107
|
+
return ctx;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Mobile-friendly singleton for users who don't want a provider.
|
|
111
|
+
export const HeedKit = {
|
|
112
|
+
_instance: null as HeedKitClient | null,
|
|
113
|
+
async init(config: HeedKitConfig) {
|
|
114
|
+
this._instance = new HeedKitClient(config);
|
|
115
|
+
return this._instance.init({ ...config.user, platform: config.user?.platform || "react-native" });
|
|
116
|
+
},
|
|
117
|
+
client(): HeedKitClient {
|
|
118
|
+
if (!this._instance) throw new Error("HeedKit.init not called");
|
|
119
|
+
return this._instance;
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// Theme palette derived from the server theme + the device color scheme when
|
|
125
|
+
// the user picked "system".
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
function usePalette(theme: Theme) {
|
|
129
|
+
const [systemScheme, setSystemScheme] = React.useState(Appearance.getColorScheme());
|
|
130
|
+
React.useEffect(() => {
|
|
131
|
+
const sub = Appearance.addChangeListener(({ colorScheme }) => setSystemScheme(colorScheme));
|
|
132
|
+
return () => sub.remove();
|
|
133
|
+
}, []);
|
|
134
|
+
|
|
135
|
+
const mode = theme.mode === "system" ? (systemScheme === "dark" ? "dark" : "light") : theme.mode || "light";
|
|
136
|
+
const dark = mode === "dark";
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
primary: theme.primary || "#0D9488",
|
|
140
|
+
radius: theme.radius ?? 12,
|
|
141
|
+
fs: FONT_SIZES[theme.font_size ?? "md"] ?? 14,
|
|
142
|
+
bg: dark ? "#0F172A" : "#FFFFFF",
|
|
143
|
+
fg: dark ? "#F1F5F9" : "#0F172A",
|
|
144
|
+
muted: dark ? "#94A3B8" : "#64748B",
|
|
145
|
+
row: dark ? "#1E293B" : "#F8FAFC",
|
|
146
|
+
border: dark ? "#1E293B" : "#E2E8F0",
|
|
147
|
+
inputBorder: dark ? "#334155" : "#CBD5E1",
|
|
148
|
+
inputBg: dark ? "#0F172A" : "#FFFFFF",
|
|
149
|
+
dark,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// Public components
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
export function FeedbackButton({ label = "Feedback" }: { label?: string }) {
|
|
158
|
+
const ctx = React.useContext(HeedKitContext);
|
|
159
|
+
const [open, setOpen] = React.useState(false);
|
|
160
|
+
const p = usePalette(ctx?.theme || {});
|
|
161
|
+
if (!ctx?.ready) return null;
|
|
162
|
+
return (
|
|
163
|
+
<>
|
|
164
|
+
<Pressable
|
|
165
|
+
onPress={() => setOpen(true)}
|
|
166
|
+
style={[styles.fab, { backgroundColor: p.primary }]}
|
|
167
|
+
>
|
|
168
|
+
<Text style={[styles.fabLabel, { fontSize: p.fs }]}>{label}</Text>
|
|
169
|
+
</Pressable>
|
|
170
|
+
<Modal visible={open} animationType="slide" onRequestClose={() => setOpen(false)}>
|
|
171
|
+
<FeedbackScreen onClose={() => setOpen(false)} />
|
|
172
|
+
</Modal>
|
|
173
|
+
</>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
type Mode = "browse" | "suggest";
|
|
178
|
+
|
|
179
|
+
export function FeedbackScreen({ onClose }: { onClose?: () => void }) {
|
|
180
|
+
const ctx = React.useContext(HeedKitContext);
|
|
181
|
+
const p = usePalette(ctx?.theme || {});
|
|
182
|
+
|
|
183
|
+
if (!ctx?.ready) {
|
|
184
|
+
return (
|
|
185
|
+
<View style={[styles.center, { backgroundColor: p.bg, flex: 1 }]}>
|
|
186
|
+
<ActivityIndicator color={p.primary} />
|
|
187
|
+
</View>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const enabledKinds = ctx.client.getEnabledKinds();
|
|
192
|
+
const groupMode: GroupMode = (ctx.theme.group_mode as GroupMode) || "tabs";
|
|
193
|
+
const showCounts = ctx.theme.show_counts || {};
|
|
194
|
+
|
|
195
|
+
const [mode, setMode] = React.useState<Mode>("browse");
|
|
196
|
+
const [activeKind, setActiveKind] = React.useState<FeatureKind | "all">(
|
|
197
|
+
groupMode === "tabs" && enabledKinds.length > 0 ? enabledKinds[0] : "all",
|
|
198
|
+
);
|
|
199
|
+
const [features, setFeatures] = React.useState<Feature[]>([]);
|
|
200
|
+
const [loading, setLoading] = React.useState(true);
|
|
201
|
+
|
|
202
|
+
async function refresh() {
|
|
203
|
+
setLoading(true);
|
|
204
|
+
try {
|
|
205
|
+
const opts: { sort: "top" | "new"; kind?: FeatureKind } = { sort: "top" };
|
|
206
|
+
if (activeKind !== "all") opts.kind = activeKind;
|
|
207
|
+
setFeatures(await ctx!.client.list(opts));
|
|
208
|
+
} finally {
|
|
209
|
+
setLoading(false);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
React.useEffect(() => {
|
|
214
|
+
refresh();
|
|
215
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
216
|
+
}, [activeKind]);
|
|
217
|
+
|
|
218
|
+
async function performInteraction(f: Feature, _i: Interaction) {
|
|
219
|
+
// Backend currently has one vote toggle endpoint per feature; richer
|
|
220
|
+
// per-interaction storage can be wired here later.
|
|
221
|
+
const r = await ctx!.client.vote(f.id);
|
|
222
|
+
setFeatures((arr) =>
|
|
223
|
+
arr.map((x) => (x.id === f.id ? { ...x, voted: r.voted, vote_count: r.vote_count } : x)),
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return (
|
|
228
|
+
<View style={[styles.container, { backgroundColor: p.bg }]}>
|
|
229
|
+
<View style={[styles.header, { borderColor: p.border }]}>
|
|
230
|
+
<Text style={[styles.title, { color: p.fg, fontSize: p.fs + 6 }]}>
|
|
231
|
+
{ctx.client.getProjectName() || "Feedback"}
|
|
232
|
+
</Text>
|
|
233
|
+
{onClose && (
|
|
234
|
+
<Pressable onPress={onClose}>
|
|
235
|
+
<Text style={{ color: p.primary, fontWeight: "600", fontSize: p.fs }}>Close</Text>
|
|
236
|
+
</Pressable>
|
|
237
|
+
)}
|
|
238
|
+
</View>
|
|
239
|
+
|
|
240
|
+
<View style={styles.modeRow}>
|
|
241
|
+
{(["browse", "suggest"] as Mode[]).map((m) => (
|
|
242
|
+
<Pressable
|
|
243
|
+
key={m}
|
|
244
|
+
onPress={() => setMode(m)}
|
|
245
|
+
style={[
|
|
246
|
+
styles.modeBtn,
|
|
247
|
+
{ backgroundColor: mode === m ? p.primary : "transparent" },
|
|
248
|
+
]}
|
|
249
|
+
>
|
|
250
|
+
<Text style={{ color: mode === m ? "#fff" : p.muted, fontWeight: "600", fontSize: p.fs - 1 }}>
|
|
251
|
+
{m === "browse" ? "Browse" : "Suggest"}
|
|
252
|
+
</Text>
|
|
253
|
+
</Pressable>
|
|
254
|
+
))}
|
|
255
|
+
</View>
|
|
256
|
+
|
|
257
|
+
{mode === "browse" && groupMode === "tabs" && enabledKinds.length > 0 && (
|
|
258
|
+
<ScrollView
|
|
259
|
+
horizontal
|
|
260
|
+
showsHorizontalScrollIndicator={false}
|
|
261
|
+
style={[styles.tabRow, { borderColor: p.border }]}
|
|
262
|
+
contentContainerStyle={{ gap: 6, paddingHorizontal: 16, paddingVertical: 10 }}
|
|
263
|
+
>
|
|
264
|
+
{(["all", ...enabledKinds] as Array<FeatureKind | "all">).map((k) => {
|
|
265
|
+
const active = activeKind === k;
|
|
266
|
+
return (
|
|
267
|
+
<Pressable
|
|
268
|
+
key={k}
|
|
269
|
+
onPress={() => setActiveKind(k)}
|
|
270
|
+
style={[
|
|
271
|
+
styles.tab,
|
|
272
|
+
{ backgroundColor: active ? p.primary : p.row },
|
|
273
|
+
]}
|
|
274
|
+
>
|
|
275
|
+
<Text style={{ color: active ? "#fff" : p.fg, fontSize: p.fs - 1, fontWeight: "500" }}>
|
|
276
|
+
{k === "all" ? "All" : `${KIND_META[k].tabIcon} ${KIND_META[k].label}`}
|
|
277
|
+
</Text>
|
|
278
|
+
</Pressable>
|
|
279
|
+
);
|
|
280
|
+
})}
|
|
281
|
+
</ScrollView>
|
|
282
|
+
)}
|
|
283
|
+
|
|
284
|
+
{mode === "browse" ? (
|
|
285
|
+
loading ? (
|
|
286
|
+
<View style={[styles.center, { flex: 1 }]}><ActivityIndicator color={p.primary} /></View>
|
|
287
|
+
) : features.length === 0 ? (
|
|
288
|
+
<View style={[styles.center, { flex: 1 }]}>
|
|
289
|
+
<Text style={{ color: p.muted, fontSize: p.fs }}>No items yet — be the first!</Text>
|
|
290
|
+
</View>
|
|
291
|
+
) : (
|
|
292
|
+
<FlatList
|
|
293
|
+
data={features}
|
|
294
|
+
keyExtractor={(f) => f.id}
|
|
295
|
+
contentContainerStyle={{ padding: 16, gap: 8 }}
|
|
296
|
+
renderItem={({ item }) => (
|
|
297
|
+
<Row
|
|
298
|
+
feature={item}
|
|
299
|
+
palette={p}
|
|
300
|
+
interactions={ctx!.client.getInteractionsFor(item.kind)}
|
|
301
|
+
showCount={showCounts[item.kind] !== false}
|
|
302
|
+
onInteraction={(i) => performInteraction(item, i)}
|
|
303
|
+
/>
|
|
304
|
+
)}
|
|
305
|
+
/>
|
|
306
|
+
)
|
|
307
|
+
) : (
|
|
308
|
+
<SubmitForm
|
|
309
|
+
palette={p}
|
|
310
|
+
enabledKinds={enabledKinds.length > 0 ? enabledKinds : ["other"]}
|
|
311
|
+
onSubmitted={async () => {
|
|
312
|
+
setMode("browse");
|
|
313
|
+
await refresh();
|
|
314
|
+
}}
|
|
315
|
+
/>
|
|
316
|
+
)}
|
|
317
|
+
</View>
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ---------------------------------------------------------------------------
|
|
322
|
+
// Row + form (private)
|
|
323
|
+
// ---------------------------------------------------------------------------
|
|
324
|
+
|
|
325
|
+
function Row({
|
|
326
|
+
feature: f, palette: p, interactions, showCount, onInteraction,
|
|
327
|
+
}: {
|
|
328
|
+
feature: Feature;
|
|
329
|
+
palette: ReturnType<typeof usePalette>;
|
|
330
|
+
interactions: Interaction[];
|
|
331
|
+
showCount: boolean;
|
|
332
|
+
onInteraction: (i: Interaction) => void;
|
|
333
|
+
}) {
|
|
334
|
+
return (
|
|
335
|
+
<View style={[styles.row, { backgroundColor: p.row, borderRadius: p.radius }]}>
|
|
336
|
+
<View style={styles.actionCol}>
|
|
337
|
+
{interactions.length === 0 ? (
|
|
338
|
+
showCount && (
|
|
339
|
+
<View style={[styles.actBtn, { borderColor: p.border, borderRadius: p.radius - 4 }]}>
|
|
340
|
+
<Text style={{ color: p.fg, fontWeight: "600", fontSize: p.fs - 1 }}>{f.vote_count}</Text>
|
|
341
|
+
</View>
|
|
342
|
+
)
|
|
343
|
+
) : (
|
|
344
|
+
interactions.map((i) => (
|
|
345
|
+
<Pressable
|
|
346
|
+
key={i}
|
|
347
|
+
onPress={() => onInteraction(i)}
|
|
348
|
+
style={[
|
|
349
|
+
styles.actBtn,
|
|
350
|
+
{
|
|
351
|
+
borderRadius: p.radius - 4,
|
|
352
|
+
borderColor: f.voted ? p.primary : p.border,
|
|
353
|
+
borderWidth: f.voted ? 2 : 1,
|
|
354
|
+
backgroundColor: f.voted ? p.primary + "22" : "transparent",
|
|
355
|
+
},
|
|
356
|
+
]}
|
|
357
|
+
>
|
|
358
|
+
<Text style={{ color: f.voted ? p.primary : p.fg, fontSize: p.fs + 1 }}>
|
|
359
|
+
{INTERACTION_META[i].icon}
|
|
360
|
+
</Text>
|
|
361
|
+
{showCount && (
|
|
362
|
+
<Text style={{ color: f.voted ? p.primary : p.fg, fontSize: p.fs - 2, fontWeight: "600" }}>
|
|
363
|
+
{f.vote_count}
|
|
364
|
+
</Text>
|
|
365
|
+
)}
|
|
366
|
+
</Pressable>
|
|
367
|
+
))
|
|
368
|
+
)}
|
|
369
|
+
</View>
|
|
370
|
+
<View style={{ flex: 1 }}>
|
|
371
|
+
<Text style={{ color: p.fg, fontWeight: "600", fontSize: p.fs }}>{f.title}</Text>
|
|
372
|
+
{!!f.description && (
|
|
373
|
+
<Text
|
|
374
|
+
style={{ color: p.muted, fontSize: p.fs - 1, marginTop: 4 }}
|
|
375
|
+
numberOfLines={3}
|
|
376
|
+
>{f.description}</Text>
|
|
377
|
+
)}
|
|
378
|
+
{(f.status !== "open" || f.tag) && (
|
|
379
|
+
<View style={styles.badges}>
|
|
380
|
+
{f.status !== "open" && (
|
|
381
|
+
<Text style={[styles.badge, { backgroundColor: p.border, color: p.muted, fontSize: p.fs - 3 }]}>
|
|
382
|
+
{f.status.replace("_", " ")}
|
|
383
|
+
</Text>
|
|
384
|
+
)}
|
|
385
|
+
{!!f.tag && (
|
|
386
|
+
<Text style={[styles.badge, { backgroundColor: p.border, color: p.muted, fontSize: p.fs - 3 }]}>
|
|
387
|
+
{f.tag}
|
|
388
|
+
</Text>
|
|
389
|
+
)}
|
|
390
|
+
</View>
|
|
391
|
+
)}
|
|
392
|
+
</View>
|
|
393
|
+
</View>
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function SubmitForm({
|
|
398
|
+
palette: p, enabledKinds, onSubmitted,
|
|
399
|
+
}: {
|
|
400
|
+
palette: ReturnType<typeof usePalette>;
|
|
401
|
+
enabledKinds: FeatureKind[];
|
|
402
|
+
onSubmitted: () => void;
|
|
403
|
+
}) {
|
|
404
|
+
const ctx = React.useContext(HeedKitContext)!;
|
|
405
|
+
const [kind, setKind] = React.useState<FeatureKind>(enabledKinds[0]);
|
|
406
|
+
const [title, setTitle] = React.useState("");
|
|
407
|
+
const [description, setDescription] = React.useState("");
|
|
408
|
+
const [submitting, setSubmitting] = React.useState(false);
|
|
409
|
+
|
|
410
|
+
async function submit() {
|
|
411
|
+
if (!title.trim()) return;
|
|
412
|
+
setSubmitting(true);
|
|
413
|
+
try {
|
|
414
|
+
await ctx.client.submit({ title, description, kind });
|
|
415
|
+
setTitle("");
|
|
416
|
+
setDescription("");
|
|
417
|
+
onSubmitted();
|
|
418
|
+
} finally {
|
|
419
|
+
setSubmitting(false);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return (
|
|
424
|
+
<ScrollView contentContainerStyle={{ padding: 16, gap: 12 }}>
|
|
425
|
+
<Text style={{ color: p.fg, fontWeight: "500", fontSize: p.fs - 1 }}>What's this about?</Text>
|
|
426
|
+
<View style={[styles.segmented, { backgroundColor: p.row }]}>
|
|
427
|
+
{enabledKinds.map((k) => {
|
|
428
|
+
const active = k === kind;
|
|
429
|
+
return (
|
|
430
|
+
<Pressable
|
|
431
|
+
key={k}
|
|
432
|
+
onPress={() => setKind(k)}
|
|
433
|
+
style={[
|
|
434
|
+
styles.seg,
|
|
435
|
+
{
|
|
436
|
+
backgroundColor: active ? p.bg : "transparent",
|
|
437
|
+
borderRadius: 999,
|
|
438
|
+
},
|
|
439
|
+
]}
|
|
440
|
+
>
|
|
441
|
+
<Text style={{ color: active ? p.fg : p.muted, fontWeight: "500", fontSize: p.fs - 2 }}>
|
|
442
|
+
{KIND_META[k].label}
|
|
443
|
+
</Text>
|
|
444
|
+
</Pressable>
|
|
445
|
+
);
|
|
446
|
+
})}
|
|
447
|
+
</View>
|
|
448
|
+
|
|
449
|
+
<Text style={{ color: p.fg, fontWeight: "500", fontSize: p.fs - 1 }}>Title</Text>
|
|
450
|
+
<TextInput
|
|
451
|
+
value={title}
|
|
452
|
+
onChangeText={setTitle}
|
|
453
|
+
placeholder={KIND_META[kind].placeholder}
|
|
454
|
+
placeholderTextColor={p.muted}
|
|
455
|
+
style={[styles.input, { borderColor: p.inputBorder, color: p.fg, backgroundColor: p.inputBg, borderRadius: p.radius - 2 }]}
|
|
456
|
+
/>
|
|
457
|
+
|
|
458
|
+
<Text style={{ color: p.fg, fontWeight: "500", fontSize: p.fs - 1 }}>Description</Text>
|
|
459
|
+
<TextInput
|
|
460
|
+
value={description}
|
|
461
|
+
onChangeText={setDescription}
|
|
462
|
+
placeholder="Any extra context helps."
|
|
463
|
+
placeholderTextColor={p.muted}
|
|
464
|
+
multiline
|
|
465
|
+
numberOfLines={4}
|
|
466
|
+
style={[
|
|
467
|
+
styles.input,
|
|
468
|
+
{ borderColor: p.inputBorder, color: p.fg, backgroundColor: p.inputBg, borderRadius: p.radius - 2, height: 100 },
|
|
469
|
+
]}
|
|
470
|
+
/>
|
|
471
|
+
|
|
472
|
+
<Pressable
|
|
473
|
+
disabled={!title || submitting}
|
|
474
|
+
onPress={submit}
|
|
475
|
+
style={[
|
|
476
|
+
styles.submit,
|
|
477
|
+
{ backgroundColor: p.primary, opacity: !title || submitting ? 0.6 : 1, borderRadius: p.radius },
|
|
478
|
+
]}
|
|
479
|
+
>
|
|
480
|
+
<Text style={{ color: "#fff", fontWeight: "600", fontSize: p.fs }}>
|
|
481
|
+
{submitting ? "Submitting…" : "Submit"}
|
|
482
|
+
</Text>
|
|
483
|
+
</Pressable>
|
|
484
|
+
</ScrollView>
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// ---------------------------------------------------------------------------
|
|
489
|
+
// Styles
|
|
490
|
+
// ---------------------------------------------------------------------------
|
|
491
|
+
|
|
492
|
+
const styles = StyleSheet.create({
|
|
493
|
+
container: { flex: 1 },
|
|
494
|
+
center: { alignItems: "center", justifyContent: "center" },
|
|
495
|
+
header: {
|
|
496
|
+
flexDirection: "row", justifyContent: "space-between", alignItems: "center",
|
|
497
|
+
paddingHorizontal: 16, paddingVertical: 14, borderBottomWidth: 1,
|
|
498
|
+
},
|
|
499
|
+
title: { fontWeight: "700" },
|
|
500
|
+
modeRow: { flexDirection: "row", gap: 6, paddingHorizontal: 16, paddingTop: 10 },
|
|
501
|
+
modeBtn: { paddingHorizontal: 14, paddingVertical: 6, borderRadius: 999 },
|
|
502
|
+
tabRow: { maxHeight: 48, borderBottomWidth: 1 },
|
|
503
|
+
tab: { paddingHorizontal: 12, paddingVertical: 6, borderRadius: 999 },
|
|
504
|
+
row: { flexDirection: "row", gap: 12, padding: 12, alignItems: "flex-start" },
|
|
505
|
+
actionCol: { gap: 4 },
|
|
506
|
+
actBtn: {
|
|
507
|
+
minWidth: 44, paddingHorizontal: 8, paddingVertical: 6,
|
|
508
|
+
alignItems: "center", justifyContent: "center", borderWidth: 1,
|
|
509
|
+
},
|
|
510
|
+
badges: { flexDirection: "row", gap: 6, marginTop: 6, flexWrap: "wrap" },
|
|
511
|
+
badge: {
|
|
512
|
+
paddingHorizontal: 8, paddingVertical: 2, borderRadius: 999,
|
|
513
|
+
textTransform: "uppercase", letterSpacing: 0.4,
|
|
514
|
+
},
|
|
515
|
+
segmented: { flexDirection: "row", padding: 4, borderRadius: 999, gap: 4, alignSelf: "flex-start", flexWrap: "wrap" },
|
|
516
|
+
seg: { paddingHorizontal: 12, paddingVertical: 6 },
|
|
517
|
+
input: { borderWidth: 1, padding: 12, fontSize: 14 },
|
|
518
|
+
submit: { padding: 14, alignItems: "center", marginTop: 8 },
|
|
519
|
+
fab: {
|
|
520
|
+
position: "absolute", bottom: 24, right: 24,
|
|
521
|
+
paddingHorizontal: 18, paddingVertical: 12, borderRadius: 999,
|
|
522
|
+
shadowColor: "#000", shadowOpacity: 0.2, shadowRadius: 8, shadowOffset: { width: 0, height: 4 },
|
|
523
|
+
elevation: 6,
|
|
524
|
+
},
|
|
525
|
+
fabLabel: { color: "#fff", fontWeight: "600" },
|
|
526
|
+
});
|