@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.
@@ -0,0 +1,678 @@
1
+ "use strict";
2
+
3
+ import * as React from "react";
4
+ import { ActivityIndicator, Appearance, FlatList, Modal, Pressable, ScrollView, StyleSheet, Text, TextInput, View } from "react-native";
5
+ import { HeedKitClient } from "./client.js";
6
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
7
+ export { HeedKitClient };
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Per-kind metadata. Kept here (not the client) because it's view-layer copy.
11
+ // ---------------------------------------------------------------------------
12
+
13
+ const KIND_META = {
14
+ feature_request: {
15
+ label: "Features",
16
+ placeholder: "What should we build?",
17
+ tabIcon: "💡"
18
+ },
19
+ bug_report: {
20
+ label: "Bugs",
21
+ placeholder: "What's broken?",
22
+ tabIcon: "🐞"
23
+ },
24
+ improvement: {
25
+ label: "Improvements",
26
+ placeholder: "What could be better?",
27
+ tabIcon: "✨"
28
+ },
29
+ appreciation: {
30
+ label: "Appreciation",
31
+ placeholder: "What did you love?",
32
+ tabIcon: "❤️"
33
+ },
34
+ other: {
35
+ label: "Other",
36
+ placeholder: "Tell us anything",
37
+ tabIcon: "💬"
38
+ }
39
+ };
40
+ const INTERACTION_META = {
41
+ upvote: {
42
+ icon: "▲",
43
+ label: "Upvote"
44
+ },
45
+ downvote: {
46
+ icon: "▼",
47
+ label: "Downvote"
48
+ },
49
+ plus_one: {
50
+ icon: "+1",
51
+ label: "+1"
52
+ },
53
+ like: {
54
+ icon: "♥",
55
+ label: "Like"
56
+ }
57
+ };
58
+ const FONT_SIZES = {
59
+ sm: 13,
60
+ md: 14,
61
+ lg: 16
62
+ };
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // Context
66
+ // ---------------------------------------------------------------------------
67
+
68
+ const HeedKitContext = /*#__PURE__*/React.createContext(null);
69
+ export function HeedKitProvider({
70
+ projectKey,
71
+ apiUrl,
72
+ user,
73
+ children
74
+ }) {
75
+ const [client] = React.useState(() => new HeedKitClient({
76
+ projectKey,
77
+ apiUrl,
78
+ user
79
+ }));
80
+ const [ready, setReady] = React.useState(false);
81
+ const [theme, setTheme] = React.useState({});
82
+ React.useEffect(() => {
83
+ client.init({
84
+ ...user,
85
+ platform: user?.platform || "react-native"
86
+ }).then(() => {
87
+ setTheme(client.getTheme());
88
+ setReady(true);
89
+ });
90
+ }, [client]); // eslint-disable-line react-hooks/exhaustive-deps
91
+
92
+ return /*#__PURE__*/_jsx(HeedKitContext.Provider, {
93
+ value: {
94
+ client,
95
+ ready,
96
+ theme
97
+ },
98
+ children: children
99
+ });
100
+ }
101
+ export function useHeedKit() {
102
+ const ctx = React.useContext(HeedKitContext);
103
+ if (!ctx) throw new Error("useHeedKit must be used inside <HeedKitProvider>");
104
+ return ctx;
105
+ }
106
+
107
+ // Mobile-friendly singleton for users who don't want a provider.
108
+ export const HeedKit = {
109
+ _instance: null,
110
+ async init(config) {
111
+ this._instance = new HeedKitClient(config);
112
+ return this._instance.init({
113
+ ...config.user,
114
+ platform: config.user?.platform || "react-native"
115
+ });
116
+ },
117
+ client() {
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) {
129
+ const [systemScheme, setSystemScheme] = React.useState(Appearance.getColorScheme());
130
+ React.useEffect(() => {
131
+ const sub = Appearance.addChangeListener(({
132
+ colorScheme
133
+ }) => setSystemScheme(colorScheme));
134
+ return () => sub.remove();
135
+ }, []);
136
+ const mode = theme.mode === "system" ? systemScheme === "dark" ? "dark" : "light" : theme.mode || "light";
137
+ const dark = mode === "dark";
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({
158
+ label = "Feedback"
159
+ }) {
160
+ const ctx = React.useContext(HeedKitContext);
161
+ const [open, setOpen] = React.useState(false);
162
+ const p = usePalette(ctx?.theme || {});
163
+ if (!ctx?.ready) return null;
164
+ return /*#__PURE__*/_jsxs(_Fragment, {
165
+ children: [/*#__PURE__*/_jsx(Pressable, {
166
+ onPress: () => setOpen(true),
167
+ style: [styles.fab, {
168
+ backgroundColor: p.primary
169
+ }],
170
+ children: /*#__PURE__*/_jsx(Text, {
171
+ style: [styles.fabLabel, {
172
+ fontSize: p.fs
173
+ }],
174
+ children: label
175
+ })
176
+ }), /*#__PURE__*/_jsx(Modal, {
177
+ visible: open,
178
+ animationType: "slide",
179
+ onRequestClose: () => setOpen(false),
180
+ children: /*#__PURE__*/_jsx(FeedbackScreen, {
181
+ onClose: () => setOpen(false)
182
+ })
183
+ })]
184
+ });
185
+ }
186
+ export function FeedbackScreen({
187
+ onClose
188
+ }) {
189
+ const ctx = React.useContext(HeedKitContext);
190
+ const p = usePalette(ctx?.theme || {});
191
+ if (!ctx?.ready) {
192
+ return /*#__PURE__*/_jsx(View, {
193
+ style: [styles.center, {
194
+ backgroundColor: p.bg,
195
+ flex: 1
196
+ }],
197
+ children: /*#__PURE__*/_jsx(ActivityIndicator, {
198
+ color: p.primary
199
+ })
200
+ });
201
+ }
202
+ const enabledKinds = ctx.client.getEnabledKinds();
203
+ const groupMode = ctx.theme.group_mode || "tabs";
204
+ const showCounts = ctx.theme.show_counts || {};
205
+ const [mode, setMode] = React.useState("browse");
206
+ const [activeKind, setActiveKind] = React.useState(groupMode === "tabs" && enabledKinds.length > 0 ? enabledKinds[0] : "all");
207
+ const [features, setFeatures] = React.useState([]);
208
+ const [loading, setLoading] = React.useState(true);
209
+ async function refresh() {
210
+ setLoading(true);
211
+ try {
212
+ const opts = {
213
+ sort: "top"
214
+ };
215
+ if (activeKind !== "all") opts.kind = activeKind;
216
+ setFeatures(await ctx.client.list(opts));
217
+ } finally {
218
+ setLoading(false);
219
+ }
220
+ }
221
+ React.useEffect(() => {
222
+ refresh();
223
+ // eslint-disable-next-line react-hooks/exhaustive-deps
224
+ }, [activeKind]);
225
+ async function performInteraction(f, _i) {
226
+ // Backend currently has one vote toggle endpoint per feature; richer
227
+ // per-interaction storage can be wired here later.
228
+ const r = await ctx.client.vote(f.id);
229
+ setFeatures(arr => arr.map(x => x.id === f.id ? {
230
+ ...x,
231
+ voted: r.voted,
232
+ vote_count: r.vote_count
233
+ } : x));
234
+ }
235
+ return /*#__PURE__*/_jsxs(View, {
236
+ style: [styles.container, {
237
+ backgroundColor: p.bg
238
+ }],
239
+ children: [/*#__PURE__*/_jsxs(View, {
240
+ style: [styles.header, {
241
+ borderColor: p.border
242
+ }],
243
+ children: [/*#__PURE__*/_jsx(Text, {
244
+ style: [styles.title, {
245
+ color: p.fg,
246
+ fontSize: p.fs + 6
247
+ }],
248
+ children: ctx.client.getProjectName() || "Feedback"
249
+ }), onClose && /*#__PURE__*/_jsx(Pressable, {
250
+ onPress: onClose,
251
+ children: /*#__PURE__*/_jsx(Text, {
252
+ style: {
253
+ color: p.primary,
254
+ fontWeight: "600",
255
+ fontSize: p.fs
256
+ },
257
+ children: "Close"
258
+ })
259
+ })]
260
+ }), /*#__PURE__*/_jsx(View, {
261
+ style: styles.modeRow,
262
+ children: ["browse", "suggest"].map(m => /*#__PURE__*/_jsx(Pressable, {
263
+ onPress: () => setMode(m),
264
+ style: [styles.modeBtn, {
265
+ backgroundColor: mode === m ? p.primary : "transparent"
266
+ }],
267
+ children: /*#__PURE__*/_jsx(Text, {
268
+ style: {
269
+ color: mode === m ? "#fff" : p.muted,
270
+ fontWeight: "600",
271
+ fontSize: p.fs - 1
272
+ },
273
+ children: m === "browse" ? "Browse" : "Suggest"
274
+ })
275
+ }, m))
276
+ }), mode === "browse" && groupMode === "tabs" && enabledKinds.length > 0 && /*#__PURE__*/_jsx(ScrollView, {
277
+ horizontal: true,
278
+ showsHorizontalScrollIndicator: false,
279
+ style: [styles.tabRow, {
280
+ borderColor: p.border
281
+ }],
282
+ contentContainerStyle: {
283
+ gap: 6,
284
+ paddingHorizontal: 16,
285
+ paddingVertical: 10
286
+ },
287
+ children: ["all", ...enabledKinds].map(k => {
288
+ const active = activeKind === k;
289
+ return /*#__PURE__*/_jsx(Pressable, {
290
+ onPress: () => setActiveKind(k),
291
+ style: [styles.tab, {
292
+ backgroundColor: active ? p.primary : p.row
293
+ }],
294
+ children: /*#__PURE__*/_jsx(Text, {
295
+ style: {
296
+ color: active ? "#fff" : p.fg,
297
+ fontSize: p.fs - 1,
298
+ fontWeight: "500"
299
+ },
300
+ children: k === "all" ? "All" : `${KIND_META[k].tabIcon} ${KIND_META[k].label}`
301
+ })
302
+ }, k);
303
+ })
304
+ }), mode === "browse" ? loading ? /*#__PURE__*/_jsx(View, {
305
+ style: [styles.center, {
306
+ flex: 1
307
+ }],
308
+ children: /*#__PURE__*/_jsx(ActivityIndicator, {
309
+ color: p.primary
310
+ })
311
+ }) : features.length === 0 ? /*#__PURE__*/_jsx(View, {
312
+ style: [styles.center, {
313
+ flex: 1
314
+ }],
315
+ children: /*#__PURE__*/_jsx(Text, {
316
+ style: {
317
+ color: p.muted,
318
+ fontSize: p.fs
319
+ },
320
+ children: "No items yet \u2014 be the first!"
321
+ })
322
+ }) : /*#__PURE__*/_jsx(FlatList, {
323
+ data: features,
324
+ keyExtractor: f => f.id,
325
+ contentContainerStyle: {
326
+ padding: 16,
327
+ gap: 8
328
+ },
329
+ renderItem: ({
330
+ item
331
+ }) => /*#__PURE__*/_jsx(Row, {
332
+ feature: item,
333
+ palette: p,
334
+ interactions: ctx.client.getInteractionsFor(item.kind),
335
+ showCount: showCounts[item.kind] !== false,
336
+ onInteraction: i => performInteraction(item, i)
337
+ })
338
+ }) : /*#__PURE__*/_jsx(SubmitForm, {
339
+ palette: p,
340
+ enabledKinds: enabledKinds.length > 0 ? enabledKinds : ["other"],
341
+ onSubmitted: async () => {
342
+ setMode("browse");
343
+ await refresh();
344
+ }
345
+ })]
346
+ });
347
+ }
348
+
349
+ // ---------------------------------------------------------------------------
350
+ // Row + form (private)
351
+ // ---------------------------------------------------------------------------
352
+
353
+ function Row({
354
+ feature: f,
355
+ palette: p,
356
+ interactions,
357
+ showCount,
358
+ onInteraction
359
+ }) {
360
+ return /*#__PURE__*/_jsxs(View, {
361
+ style: [styles.row, {
362
+ backgroundColor: p.row,
363
+ borderRadius: p.radius
364
+ }],
365
+ children: [/*#__PURE__*/_jsx(View, {
366
+ style: styles.actionCol,
367
+ children: interactions.length === 0 ? showCount && /*#__PURE__*/_jsx(View, {
368
+ style: [styles.actBtn, {
369
+ borderColor: p.border,
370
+ borderRadius: p.radius - 4
371
+ }],
372
+ children: /*#__PURE__*/_jsx(Text, {
373
+ style: {
374
+ color: p.fg,
375
+ fontWeight: "600",
376
+ fontSize: p.fs - 1
377
+ },
378
+ children: f.vote_count
379
+ })
380
+ }) : interactions.map(i => /*#__PURE__*/_jsxs(Pressable, {
381
+ onPress: () => onInteraction(i),
382
+ style: [styles.actBtn, {
383
+ borderRadius: p.radius - 4,
384
+ borderColor: f.voted ? p.primary : p.border,
385
+ borderWidth: f.voted ? 2 : 1,
386
+ backgroundColor: f.voted ? p.primary + "22" : "transparent"
387
+ }],
388
+ children: [/*#__PURE__*/_jsx(Text, {
389
+ style: {
390
+ color: f.voted ? p.primary : p.fg,
391
+ fontSize: p.fs + 1
392
+ },
393
+ children: INTERACTION_META[i].icon
394
+ }), showCount && /*#__PURE__*/_jsx(Text, {
395
+ style: {
396
+ color: f.voted ? p.primary : p.fg,
397
+ fontSize: p.fs - 2,
398
+ fontWeight: "600"
399
+ },
400
+ children: f.vote_count
401
+ })]
402
+ }, i))
403
+ }), /*#__PURE__*/_jsxs(View, {
404
+ style: {
405
+ flex: 1
406
+ },
407
+ children: [/*#__PURE__*/_jsx(Text, {
408
+ style: {
409
+ color: p.fg,
410
+ fontWeight: "600",
411
+ fontSize: p.fs
412
+ },
413
+ children: f.title
414
+ }), !!f.description && /*#__PURE__*/_jsx(Text, {
415
+ style: {
416
+ color: p.muted,
417
+ fontSize: p.fs - 1,
418
+ marginTop: 4
419
+ },
420
+ numberOfLines: 3,
421
+ children: f.description
422
+ }), (f.status !== "open" || f.tag) && /*#__PURE__*/_jsxs(View, {
423
+ style: styles.badges,
424
+ children: [f.status !== "open" && /*#__PURE__*/_jsx(Text, {
425
+ style: [styles.badge, {
426
+ backgroundColor: p.border,
427
+ color: p.muted,
428
+ fontSize: p.fs - 3
429
+ }],
430
+ children: f.status.replace("_", " ")
431
+ }), !!f.tag && /*#__PURE__*/_jsx(Text, {
432
+ style: [styles.badge, {
433
+ backgroundColor: p.border,
434
+ color: p.muted,
435
+ fontSize: p.fs - 3
436
+ }],
437
+ children: f.tag
438
+ })]
439
+ })]
440
+ })]
441
+ });
442
+ }
443
+ function SubmitForm({
444
+ palette: p,
445
+ enabledKinds,
446
+ onSubmitted
447
+ }) {
448
+ const ctx = React.useContext(HeedKitContext);
449
+ const [kind, setKind] = React.useState(enabledKinds[0]);
450
+ const [title, setTitle] = React.useState("");
451
+ const [description, setDescription] = React.useState("");
452
+ const [submitting, setSubmitting] = React.useState(false);
453
+ async function submit() {
454
+ if (!title.trim()) return;
455
+ setSubmitting(true);
456
+ try {
457
+ await ctx.client.submit({
458
+ title,
459
+ description,
460
+ kind
461
+ });
462
+ setTitle("");
463
+ setDescription("");
464
+ onSubmitted();
465
+ } finally {
466
+ setSubmitting(false);
467
+ }
468
+ }
469
+ return /*#__PURE__*/_jsxs(ScrollView, {
470
+ contentContainerStyle: {
471
+ padding: 16,
472
+ gap: 12
473
+ },
474
+ children: [/*#__PURE__*/_jsx(Text, {
475
+ style: {
476
+ color: p.fg,
477
+ fontWeight: "500",
478
+ fontSize: p.fs - 1
479
+ },
480
+ children: "What's this about?"
481
+ }), /*#__PURE__*/_jsx(View, {
482
+ style: [styles.segmented, {
483
+ backgroundColor: p.row
484
+ }],
485
+ children: enabledKinds.map(k => {
486
+ const active = k === kind;
487
+ return /*#__PURE__*/_jsx(Pressable, {
488
+ onPress: () => setKind(k),
489
+ style: [styles.seg, {
490
+ backgroundColor: active ? p.bg : "transparent",
491
+ borderRadius: 999
492
+ }],
493
+ children: /*#__PURE__*/_jsx(Text, {
494
+ style: {
495
+ color: active ? p.fg : p.muted,
496
+ fontWeight: "500",
497
+ fontSize: p.fs - 2
498
+ },
499
+ children: KIND_META[k].label
500
+ })
501
+ }, k);
502
+ })
503
+ }), /*#__PURE__*/_jsx(Text, {
504
+ style: {
505
+ color: p.fg,
506
+ fontWeight: "500",
507
+ fontSize: p.fs - 1
508
+ },
509
+ children: "Title"
510
+ }), /*#__PURE__*/_jsx(TextInput, {
511
+ value: title,
512
+ onChangeText: setTitle,
513
+ placeholder: KIND_META[kind].placeholder,
514
+ placeholderTextColor: p.muted,
515
+ style: [styles.input, {
516
+ borderColor: p.inputBorder,
517
+ color: p.fg,
518
+ backgroundColor: p.inputBg,
519
+ borderRadius: p.radius - 2
520
+ }]
521
+ }), /*#__PURE__*/_jsx(Text, {
522
+ style: {
523
+ color: p.fg,
524
+ fontWeight: "500",
525
+ fontSize: p.fs - 1
526
+ },
527
+ children: "Description"
528
+ }), /*#__PURE__*/_jsx(TextInput, {
529
+ value: description,
530
+ onChangeText: setDescription,
531
+ placeholder: "Any extra context helps.",
532
+ placeholderTextColor: p.muted,
533
+ multiline: true,
534
+ numberOfLines: 4,
535
+ style: [styles.input, {
536
+ borderColor: p.inputBorder,
537
+ color: p.fg,
538
+ backgroundColor: p.inputBg,
539
+ borderRadius: p.radius - 2,
540
+ height: 100
541
+ }]
542
+ }), /*#__PURE__*/_jsx(Pressable, {
543
+ disabled: !title || submitting,
544
+ onPress: submit,
545
+ style: [styles.submit, {
546
+ backgroundColor: p.primary,
547
+ opacity: !title || submitting ? 0.6 : 1,
548
+ borderRadius: p.radius
549
+ }],
550
+ children: /*#__PURE__*/_jsx(Text, {
551
+ style: {
552
+ color: "#fff",
553
+ fontWeight: "600",
554
+ fontSize: p.fs
555
+ },
556
+ children: submitting ? "Submitting…" : "Submit"
557
+ })
558
+ })]
559
+ });
560
+ }
561
+
562
+ // ---------------------------------------------------------------------------
563
+ // Styles
564
+ // ---------------------------------------------------------------------------
565
+
566
+ const styles = StyleSheet.create({
567
+ container: {
568
+ flex: 1
569
+ },
570
+ center: {
571
+ alignItems: "center",
572
+ justifyContent: "center"
573
+ },
574
+ header: {
575
+ flexDirection: "row",
576
+ justifyContent: "space-between",
577
+ alignItems: "center",
578
+ paddingHorizontal: 16,
579
+ paddingVertical: 14,
580
+ borderBottomWidth: 1
581
+ },
582
+ title: {
583
+ fontWeight: "700"
584
+ },
585
+ modeRow: {
586
+ flexDirection: "row",
587
+ gap: 6,
588
+ paddingHorizontal: 16,
589
+ paddingTop: 10
590
+ },
591
+ modeBtn: {
592
+ paddingHorizontal: 14,
593
+ paddingVertical: 6,
594
+ borderRadius: 999
595
+ },
596
+ tabRow: {
597
+ maxHeight: 48,
598
+ borderBottomWidth: 1
599
+ },
600
+ tab: {
601
+ paddingHorizontal: 12,
602
+ paddingVertical: 6,
603
+ borderRadius: 999
604
+ },
605
+ row: {
606
+ flexDirection: "row",
607
+ gap: 12,
608
+ padding: 12,
609
+ alignItems: "flex-start"
610
+ },
611
+ actionCol: {
612
+ gap: 4
613
+ },
614
+ actBtn: {
615
+ minWidth: 44,
616
+ paddingHorizontal: 8,
617
+ paddingVertical: 6,
618
+ alignItems: "center",
619
+ justifyContent: "center",
620
+ borderWidth: 1
621
+ },
622
+ badges: {
623
+ flexDirection: "row",
624
+ gap: 6,
625
+ marginTop: 6,
626
+ flexWrap: "wrap"
627
+ },
628
+ badge: {
629
+ paddingHorizontal: 8,
630
+ paddingVertical: 2,
631
+ borderRadius: 999,
632
+ textTransform: "uppercase",
633
+ letterSpacing: 0.4
634
+ },
635
+ segmented: {
636
+ flexDirection: "row",
637
+ padding: 4,
638
+ borderRadius: 999,
639
+ gap: 4,
640
+ alignSelf: "flex-start",
641
+ flexWrap: "wrap"
642
+ },
643
+ seg: {
644
+ paddingHorizontal: 12,
645
+ paddingVertical: 6
646
+ },
647
+ input: {
648
+ borderWidth: 1,
649
+ padding: 12,
650
+ fontSize: 14
651
+ },
652
+ submit: {
653
+ padding: 14,
654
+ alignItems: "center",
655
+ marginTop: 8
656
+ },
657
+ fab: {
658
+ position: "absolute",
659
+ bottom: 24,
660
+ right: 24,
661
+ paddingHorizontal: 18,
662
+ paddingVertical: 12,
663
+ borderRadius: 999,
664
+ shadowColor: "#000",
665
+ shadowOpacity: 0.2,
666
+ shadowRadius: 8,
667
+ shadowOffset: {
668
+ width: 0,
669
+ height: 4
670
+ },
671
+ elevation: 6
672
+ },
673
+ fabLabel: {
674
+ color: "#fff",
675
+ fontWeight: "600"
676
+ }
677
+ });
678
+ //# sourceMappingURL=index.js.map