@heedkit/sdk-react 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.js ADDED
@@ -0,0 +1,735 @@
1
+ // src/index.tsx
2
+ import * as React from "react";
3
+
4
+ // src/client.ts
5
+ var DEFAULT_API = "https://api.heedkit.com";
6
+ var DEVICE_ID_KEY = "heedkit.device_id";
7
+ function getOrCreateDeviceId() {
8
+ try {
9
+ if (typeof window === "undefined" || !window.localStorage) return null;
10
+ const existing = window.localStorage.getItem(DEVICE_ID_KEY);
11
+ if (existing) return existing;
12
+ const next = "dev_" + (crypto?.randomUUID?.() ?? Math.random().toString(36).slice(2));
13
+ window.localStorage.setItem(DEVICE_ID_KEY, next);
14
+ return next;
15
+ } catch {
16
+ return null;
17
+ }
18
+ }
19
+ function normalizeFeature(f) {
20
+ return {
21
+ id: String(f.id),
22
+ title: f.title,
23
+ description: f.description ?? "",
24
+ status: f.status,
25
+ kind: f.kind,
26
+ visibility: f.visibility,
27
+ on_roadmap: f.on_roadmap ?? false,
28
+ tag: f.tag ?? null,
29
+ vote_count: f.vote_count ?? 0,
30
+ voted: f.voted ?? false,
31
+ platform: f.platform ?? null,
32
+ author_name: f.author_name ?? f.author ?? null,
33
+ created_at: f.created_at
34
+ };
35
+ }
36
+ function normalizeComment(c) {
37
+ return {
38
+ id: String(c.id),
39
+ body: c.body,
40
+ author_name: c.author_name ?? c.author ?? null,
41
+ // The SDK endpoint only ever returns public comments.
42
+ is_internal: c.is_internal ?? false,
43
+ created_at: c.created_at
44
+ };
45
+ }
46
+ var HeedKitClient = class {
47
+ constructor(config) {
48
+ this.endUserId = null;
49
+ this.theme = {};
50
+ this.projectName = "";
51
+ this.enabledKinds = [];
52
+ this.kindVisibility = {};
53
+ this.kindInteractions = {};
54
+ this.apiUrl = config.apiUrl || DEFAULT_API;
55
+ this.projectKey = config.projectKey;
56
+ }
57
+ async init(user = {}) {
58
+ const externalId = user.externalId ?? getOrCreateDeviceId() ?? void 0;
59
+ const body = {
60
+ external_id: externalId,
61
+ email: user.email,
62
+ name: user.name,
63
+ avatar_url: user.avatarUrl,
64
+ platform: user.platform || "web"
65
+ };
66
+ const res = await this.request("/sdk/init", "POST", body);
67
+ this.endUserId = res.end_user_id;
68
+ const p = res.project ?? res;
69
+ this.theme = p.theme || {};
70
+ this.projectName = p.name ?? p.project_name ?? "";
71
+ this.enabledKinds = p.enabled_kinds || [];
72
+ this.kindVisibility = p.kind_visibility || {};
73
+ this.kindInteractions = p.kind_interactions || {};
74
+ return res;
75
+ }
76
+ getTheme() {
77
+ return this.theme;
78
+ }
79
+ getEnabledKinds() {
80
+ return this.enabledKinds;
81
+ }
82
+ getKindVisibility() {
83
+ return this.kindVisibility;
84
+ }
85
+ getKindInteractions() {
86
+ return this.kindInteractions;
87
+ }
88
+ getProjectName() {
89
+ return this.projectName;
90
+ }
91
+ getEndUserId() {
92
+ return this.endUserId;
93
+ }
94
+ /// Convenience: which interactions are enabled for a given kind, in stable order.
95
+ getInteractionsFor(kind) {
96
+ const row = this.kindInteractions[kind] || {};
97
+ return ["upvote", "downvote", "plus_one", "like"].filter(
98
+ (i) => row[i]
99
+ );
100
+ }
101
+ async list(opts = {}) {
102
+ this.ensureInit();
103
+ const params = new URLSearchParams({ end_user_id: this.endUserId });
104
+ if (opts.status) params.set("status", opts.status);
105
+ if (opts.kind) params.set("kind", opts.kind);
106
+ if (opts.sort) params.set("sort", opts.sort);
107
+ const res = await this.request(`/sdk/features?${params}`, "GET");
108
+ const features = Array.isArray(res) ? res : res.features ?? [];
109
+ return features.map((f) => normalizeFeature(f));
110
+ }
111
+ async submit(input) {
112
+ this.ensureInit();
113
+ const res = await this.request("/sdk/features", "POST", {
114
+ end_user_id: this.endUserId,
115
+ title: input.title,
116
+ description: input.description || "",
117
+ tag: input.tag || null,
118
+ kind: input.kind || "feature_request"
119
+ });
120
+ return normalizeFeature(res);
121
+ }
122
+ async vote(featureId) {
123
+ this.ensureInit();
124
+ return this.request(`/sdk/features/${featureId}/vote`, "POST", {
125
+ end_user_id: this.endUserId
126
+ });
127
+ }
128
+ async listComments(featureId) {
129
+ const res = await this.request(`/sdk/features/${featureId}/comments`, "GET");
130
+ const comments = Array.isArray(res) ? res : res.comments ?? [];
131
+ return comments.map((c) => normalizeComment(c));
132
+ }
133
+ async comment(featureId, body) {
134
+ this.ensureInit();
135
+ const res = await this.request(`/sdk/features/${featureId}/comments`, "POST", {
136
+ end_user_id: this.endUserId,
137
+ body
138
+ });
139
+ return normalizeComment(res);
140
+ }
141
+ ensureInit() {
142
+ if (!this.endUserId) throw new Error("HeedKit not initialized \u2014 call init() first");
143
+ }
144
+ async request(path, method, body) {
145
+ const res = await fetch(`${this.apiUrl}${path}`, {
146
+ method,
147
+ headers: {
148
+ "Content-Type": "application/json",
149
+ "X-Project-Key": this.projectKey
150
+ },
151
+ body: body ? JSON.stringify(body) : void 0
152
+ });
153
+ if (!res.ok) {
154
+ let detail = `HTTP ${res.status}`;
155
+ try {
156
+ const j = await res.json();
157
+ detail = j.error || j.detail || detail;
158
+ } catch {
159
+ }
160
+ throw new Error(detail);
161
+ }
162
+ return res.json();
163
+ }
164
+ };
165
+
166
+ // src/widget.ts
167
+ var KIND_OPTIONS = {
168
+ feature_request: { label: "Features", placeholder: "What should we build?", tabIcon: "\u{1F4A1}" },
169
+ bug_report: { label: "Bugs", placeholder: "What's broken?", tabIcon: "\u{1F41E}" },
170
+ improvement: { label: "Improvements", placeholder: "What could be better?", tabIcon: "\u2728" },
171
+ appreciation: { label: "Appreciation", placeholder: "What did you love?", tabIcon: "\u2764\uFE0F" },
172
+ other: { label: "Other", placeholder: "Tell us anything", tabIcon: "\u{1F4AC}" }
173
+ };
174
+ var INTERACTION_META = {
175
+ upvote: { icon: "\u25B2", label: "Upvote" },
176
+ downvote: { icon: "\u25BC", label: "Downvote" },
177
+ plus_one: { icon: "+1", label: "+1" },
178
+ like: { icon: "\u2665", label: "Like" }
179
+ };
180
+ var FONT_SIZES = { sm: "13px", md: "14px", lg: "16px" };
181
+ var STYLE_ID = "heedkit-styles";
182
+ var CSS = `
183
+ .fk-launcher {
184
+ position: fixed; bottom: 24px; right: 24px; z-index: 2147483645;
185
+ background: var(--fh-primary); color: #fff; border: 0; border-radius: 999px;
186
+ padding: 12px 18px; font-weight: 600; font-size: var(--fh-fs); cursor: pointer;
187
+ box-shadow: 0 10px 24px rgba(0,0,0,.18); font-family: var(--fh-font);
188
+ transition: transform .15s ease;
189
+ }
190
+ .fk-launcher:hover { transform: translateY(-1px); }
191
+ .fk-overlay {
192
+ position: fixed; inset: 0; z-index: 2147483646; display: flex;
193
+ align-items: center; justify-content: center; padding: 16px;
194
+ background: rgba(0,0,0,.45); backdrop-filter: blur(2px);
195
+ font-family: var(--fh-font); font-size: var(--fh-fs);
196
+ }
197
+ .fk-panel {
198
+ width: 100%; max-width: 520px; max-height: 85vh; display: flex; flex-direction: column;
199
+ background: var(--fh-bg); color: var(--fh-fg);
200
+ border-radius: calc(var(--fh-radius) * 1.5); overflow: hidden;
201
+ box-shadow: 0 20px 60px rgba(0,0,0,.3);
202
+ }
203
+ .fk-head { padding: 18px 20px 12px; border-bottom: 1px solid var(--fh-border); }
204
+ .fk-titlerow { display:flex; justify-content:space-between; align-items:center; }
205
+ .fk-title { font-size: calc(var(--fh-fs) + 6px); font-weight: 700; }
206
+ .fk-close {
207
+ background: transparent; border: 0; color: var(--fh-muted);
208
+ font-size: 22px; cursor: pointer; line-height: 1; padding: 0 4px;
209
+ }
210
+ .fk-modes { display:flex; gap: 6px; margin-top: 12px; }
211
+ .fk-mode {
212
+ border: 0; background: transparent; padding: 6px 12px; border-radius: 999px;
213
+ font-size: calc(var(--fh-fs) - 1px); font-weight: 600; cursor: pointer;
214
+ color: var(--fh-muted); font-family: inherit;
215
+ }
216
+ .fk-mode[data-active="true"] { background: var(--fh-primary); color: #fff; }
217
+ .fk-tabs {
218
+ display: flex; flex-wrap: wrap; gap: 6px; padding: 10px 20px; border-bottom: 1px solid var(--fh-border);
219
+ }
220
+ .fk-tab {
221
+ border: 0; background: var(--fh-row); color: var(--fh-fg);
222
+ padding: 6px 12px; border-radius: 999px;
223
+ font-size: calc(var(--fh-fs) - 1px); font-weight: 500; cursor: pointer; font-family: inherit;
224
+ display: inline-flex; align-items: center; gap: 6px;
225
+ }
226
+ .fk-tab[data-active="true"] { background: var(--fh-primary); color: #fff; }
227
+ .fk-body { flex: 1; overflow-y: auto; padding: 16px 20px; }
228
+ .fk-empty { text-align: center; padding: 32px; opacity: .6; }
229
+ .fk-loading { text-align: center; padding: 32px; opacity: .6; }
230
+ .fk-row {
231
+ display: flex; gap: 12px; padding: 12px; margin-bottom: 8px;
232
+ background: var(--fh-row); border-radius: var(--fh-radius);
233
+ }
234
+ .fk-actions { display:flex; flex-direction: column; gap: 4px; }
235
+ .fk-act {
236
+ border: 1px solid var(--fh-border); background: transparent;
237
+ color: var(--fh-fg); border-radius: calc(var(--fh-radius) - 4px);
238
+ min-width: 44px; padding: 6px 8px; cursor: pointer;
239
+ display: flex; flex-direction: column; align-items: center; gap: 2px;
240
+ font-weight: 600; font-size: calc(var(--fh-fs) - 1px); font-family: inherit;
241
+ }
242
+ .fk-act[data-voted="true"] {
243
+ border: 2px solid var(--fh-primary);
244
+ background: color-mix(in srgb, var(--fh-primary) 14%, transparent);
245
+ color: var(--fh-primary);
246
+ }
247
+ .fk-act[disabled] { cursor: default; opacity: .85; }
248
+ .fk-act .fk-glyph { font-size: calc(var(--fh-fs) + 1px); line-height: 1; }
249
+ .fk-meta { flex:1; min-width:0; cursor: pointer; }
250
+ .fk-item-title { font-weight: 600; }
251
+ .fk-item-desc { opacity: .7; font-size: calc(var(--fh-fs) - 1px); margin-top: 4px; }
252
+ .fk-item-badges { display:flex; gap:6px; margin-top:6px; flex-wrap: wrap; }
253
+ .fk-badge {
254
+ font-size: calc(var(--fh-fs) - 3px); padding: 2px 8px; border-radius: 999px;
255
+ background: var(--fh-border); color: var(--fh-muted); text-transform: uppercase; letter-spacing: .04em;
256
+ }
257
+ .fk-badge[data-status="planned"] { background: rgba(59,130,246,.15); color: rgb(37,99,235); }
258
+ .fk-badge[data-status="in_progress"] { background: rgba(234,179,8,.18); color: rgb(161,98,7); }
259
+ .fk-badge[data-status="shipped"] { background: rgba(34,197,94,.18); color: rgb(22,101,52); }
260
+ .fk-comments { margin-top: 10px; border-top: 1px solid var(--fh-border); padding-top: 10px; }
261
+ .fk-comment { padding: 6px 0; border-top: 1px dashed var(--fh-border); font-size: calc(var(--fh-fs) - 1px); }
262
+ .fk-comment:first-child { border-top: 0; }
263
+ .fk-comment-author { font-weight: 600; }
264
+ .fk-form { display: flex; flex-direction: column; gap: 12px; }
265
+ .fk-label { font-size: calc(var(--fh-fs) - 1px); font-weight: 500; }
266
+ .fk-input, .fk-textarea {
267
+ width: 100%; padding: 10px 12px; margin-top: 4px;
268
+ border-radius: calc(var(--fh-radius) - 2px);
269
+ border: 1px solid var(--fh-input-border); background: var(--fh-input-bg);
270
+ color: var(--fh-fg); font-size: var(--fh-fs); font-family: inherit;
271
+ box-sizing: border-box;
272
+ }
273
+ .fk-textarea { resize: vertical; min-height: 96px; }
274
+ .fk-input:focus, .fk-textarea:focus { outline: 2px solid var(--fh-primary); outline-offset: 1px; }
275
+ .fk-submit {
276
+ background: var(--fh-primary); color: #fff; border: 0;
277
+ padding: 12px 14px; border-radius: var(--fh-radius);
278
+ font-weight: 600; font-size: var(--fh-fs); cursor: pointer; font-family: inherit;
279
+ }
280
+ .fk-submit[disabled] { opacity: .6; cursor: not-allowed; }
281
+ .fk-segmented {
282
+ display: inline-flex; gap: 4px; padding: 4px; margin-top: 6px;
283
+ background: var(--fh-row); border-radius: 999px;
284
+ }
285
+ .fk-seg {
286
+ border: 0; background: transparent; cursor: pointer;
287
+ padding: 6px 12px; border-radius: 999px; font-size: calc(var(--fh-fs) - 2px);
288
+ font-weight: 500; color: var(--fh-muted); font-family: inherit;
289
+ }
290
+ .fk-seg[data-active="true"] {
291
+ background: var(--fh-bg); color: var(--fh-fg);
292
+ box-shadow: 0 1px 2px rgba(0,0,0,.06), 0 2px 8px rgba(0,0,0,.04);
293
+ }
294
+ .fk-error { color: rgb(220,38,38); font-size: calc(var(--fh-fs) - 1px); }
295
+ `;
296
+ function injectStyles() {
297
+ if (document.getElementById(STYLE_ID)) return;
298
+ const style = document.createElement("style");
299
+ style.id = STYLE_ID;
300
+ style.textContent = CSS;
301
+ document.head.appendChild(style);
302
+ }
303
+ function effectiveMode(theme) {
304
+ const m = theme.mode || "light";
305
+ if (m === "system") {
306
+ return typeof window !== "undefined" && window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
307
+ }
308
+ return m === "dark" ? "dark" : "light";
309
+ }
310
+ function applyTheme(root, theme) {
311
+ const primary = theme.primary || "#0D9488";
312
+ const radius = `${theme.radius ?? 12}px`;
313
+ const dark = effectiveMode(theme) === "dark";
314
+ const font = theme.font_family || theme.fontFamily || "system-ui, -apple-system, sans-serif";
315
+ const fs = FONT_SIZES[theme.font_size ?? "md"] ?? FONT_SIZES.md;
316
+ const vars = {
317
+ "--fh-primary": primary,
318
+ "--fh-radius": radius,
319
+ "--fh-font": font,
320
+ "--fh-fs": fs,
321
+ "--fh-bg": dark ? "#0F172A" : "#FFFFFF",
322
+ "--fh-fg": dark ? "#F1F5F9" : "#0F172A",
323
+ "--fh-muted": dark ? "#94A3B8" : "#64748B",
324
+ "--fh-row": dark ? "#1E293B" : "#F8FAFC",
325
+ "--fh-border": dark ? "#1E293B" : "#E2E8F0",
326
+ "--fh-input-bg": dark ? "#0F172A" : "#FFFFFF",
327
+ "--fh-input-border": dark ? "#334155" : "#CBD5E1"
328
+ };
329
+ for (const [k, v] of Object.entries(vars)) root.style.setProperty(k, v);
330
+ }
331
+ function el(tag, attrs, children) {
332
+ const node = document.createElement(tag);
333
+ if (attrs) for (const [k, v] of Object.entries(attrs)) {
334
+ if (k === "class") node.className = v;
335
+ else node.setAttribute(k, v);
336
+ }
337
+ if (children) {
338
+ for (const c of children) {
339
+ if (c == null) continue;
340
+ node.appendChild(typeof c === "string" ? document.createTextNode(c) : c);
341
+ }
342
+ }
343
+ return node;
344
+ }
345
+ function mount(options) {
346
+ injectStyles();
347
+ const container = options.container || document.body;
348
+ const client = new HeedKitClient(options);
349
+ const initPromise = client.init(options.user || {}).catch((e) => {
350
+ console.warn("[HeedKit] widget init failed; launcher disabled.", e);
351
+ return null;
352
+ });
353
+ let overlay = null;
354
+ let launcher = null;
355
+ let destroyed = false;
356
+ function close() {
357
+ if (overlay) {
358
+ overlay.remove();
359
+ overlay = null;
360
+ }
361
+ }
362
+ async function open() {
363
+ const r = await initPromise;
364
+ if (destroyed) return;
365
+ if (!r) return;
366
+ if (overlay) return;
367
+ overlay = renderPanel(client, client.getTheme(), close);
368
+ container.appendChild(overlay);
369
+ }
370
+ if (!options.hideLauncher) {
371
+ initPromise.then((r) => {
372
+ if (destroyed) return;
373
+ if (!r) return;
374
+ launcher = el("button", { class: "fk-launcher", type: "button" }, [
375
+ options.label || "Feedback"
376
+ ]);
377
+ applyTheme(launcher, client.getTheme());
378
+ launcher.addEventListener("click", open);
379
+ container.appendChild(launcher);
380
+ });
381
+ }
382
+ return {
383
+ client,
384
+ open,
385
+ close,
386
+ destroy() {
387
+ destroyed = true;
388
+ close();
389
+ launcher?.remove();
390
+ }
391
+ };
392
+ }
393
+ function renderPanel(client, theme, onClose) {
394
+ const overlay = el("div", { class: "fk-overlay", role: "dialog" });
395
+ applyTheme(overlay, theme);
396
+ overlay.addEventListener("click", (e) => {
397
+ if (e.target === overlay) onClose();
398
+ });
399
+ const panel = el("div", { class: "fk-panel" });
400
+ overlay.appendChild(panel);
401
+ const enabledKinds = client.getEnabledKinds();
402
+ const groupMode = theme.group_mode || "tabs";
403
+ let mode = "browse";
404
+ let activeKind = groupMode === "tabs" && enabledKinds.length > 0 ? enabledKinds[0] : "all";
405
+ let features = [];
406
+ const head = el("div", { class: "fk-head" });
407
+ const titlerow = el("div", { class: "fk-titlerow" });
408
+ titlerow.appendChild(el("div", { class: "fk-title" }, [client.getProjectName() || "Feedback"]));
409
+ const closeBtn = el("button", { class: "fk-close", type: "button", "aria-label": "Close" }, ["\xD7"]);
410
+ closeBtn.addEventListener("click", onClose);
411
+ titlerow.appendChild(closeBtn);
412
+ head.appendChild(titlerow);
413
+ const modes = el("div", { class: "fk-modes" });
414
+ const modeBrowse = el("button", { class: "fk-mode", type: "button" }, ["Browse"]);
415
+ const modeSuggest = el("button", { class: "fk-mode", type: "button" }, ["Suggest"]);
416
+ modes.append(modeBrowse, modeSuggest);
417
+ head.appendChild(modes);
418
+ panel.appendChild(head);
419
+ let tabsEl = null;
420
+ if (groupMode === "tabs" && enabledKinds.length > 0) {
421
+ tabsEl = el("div", { class: "fk-tabs" });
422
+ const all = el("button", { class: "fk-tab", type: "button" }, ["All"]);
423
+ all.dataset.kind = "all";
424
+ tabsEl.appendChild(all);
425
+ for (const k of enabledKinds) {
426
+ const meta = KIND_OPTIONS[k];
427
+ const tab = el("button", { class: "fk-tab", type: "button" }, [
428
+ el("span", {}, [meta.tabIcon]),
429
+ el("span", {}, [meta.label])
430
+ ]);
431
+ tab.dataset.kind = k;
432
+ tabsEl.appendChild(tab);
433
+ }
434
+ panel.appendChild(tabsEl);
435
+ tabsEl.addEventListener("click", (e) => {
436
+ const target = e.target.closest("[data-kind]");
437
+ if (!target) return;
438
+ activeKind = target.dataset.kind;
439
+ paintTabs();
440
+ refresh();
441
+ });
442
+ }
443
+ const body = el("div", { class: "fk-body" });
444
+ panel.appendChild(body);
445
+ function paintModes() {
446
+ modeBrowse.setAttribute("data-active", String(mode === "browse"));
447
+ modeSuggest.setAttribute("data-active", String(mode === "suggest"));
448
+ if (tabsEl) tabsEl.style.display = mode === "browse" ? "" : "none";
449
+ }
450
+ function paintTabs() {
451
+ if (!tabsEl) return;
452
+ for (const t of Array.from(tabsEl.children)) {
453
+ t.setAttribute(
454
+ "data-active",
455
+ String(t.dataset.kind === String(activeKind))
456
+ );
457
+ }
458
+ }
459
+ async function refresh() {
460
+ body.replaceChildren(el("div", { class: "fk-loading" }, ["Loading\u2026"]));
461
+ try {
462
+ const opts = { sort: "top" };
463
+ if (activeKind !== "all") opts.kind = activeKind;
464
+ features = await client.list(opts);
465
+ } catch (e) {
466
+ body.replaceChildren(
467
+ el("div", { class: "fk-empty" }, [`Failed to load: ${e.message}`])
468
+ );
469
+ return;
470
+ }
471
+ renderList();
472
+ }
473
+ async function performAction(f, interaction) {
474
+ try {
475
+ const r = await client.vote(f.id);
476
+ f.voted = r.voted;
477
+ f.vote_count = r.vote_count;
478
+ renderList();
479
+ } catch (e) {
480
+ alert(e.message);
481
+ }
482
+ }
483
+ function renderActions(f) {
484
+ const interactions = client.getInteractionsFor(f.kind);
485
+ const showCount = (client.getTheme().show_counts || {})[f.kind] !== false;
486
+ const wrap = el("div", { class: "fk-actions" });
487
+ if (interactions.length === 0) {
488
+ if (showCount) {
489
+ const btn = el("button", { class: "fk-act", type: "button", disabled: "true" }, [
490
+ el("span", {}, [String(f.vote_count)])
491
+ ]);
492
+ wrap.appendChild(btn);
493
+ }
494
+ return wrap;
495
+ }
496
+ for (const i of interactions) {
497
+ const meta = INTERACTION_META[i];
498
+ const btn = el("button", {
499
+ class: "fk-act",
500
+ type: "button",
501
+ "aria-label": meta.label
502
+ }, [
503
+ el("span", { class: "fk-glyph" }, [meta.icon]),
504
+ ...showCount ? [el("span", {}, [String(f.vote_count)])] : []
505
+ ]);
506
+ if (i === "upvote" || i === "like" || i === "plus_one") {
507
+ btn.setAttribute("data-voted", String(f.voted));
508
+ }
509
+ btn.addEventListener("click", (e) => {
510
+ e.stopPropagation();
511
+ performAction(f, i);
512
+ });
513
+ wrap.appendChild(btn);
514
+ }
515
+ return wrap;
516
+ }
517
+ function renderList() {
518
+ body.innerHTML = "";
519
+ if (features.length === 0) {
520
+ body.appendChild(el("div", { class: "fk-empty" }, ["No items yet \u2014 be the first!"]));
521
+ return;
522
+ }
523
+ for (const f of features) {
524
+ body.appendChild(renderRow(f));
525
+ }
526
+ }
527
+ function renderRow(f) {
528
+ const row = el("div", { class: "fk-row" });
529
+ row.appendChild(renderActions(f));
530
+ const meta = el("div", { class: "fk-meta" });
531
+ meta.appendChild(el("div", { class: "fk-item-title" }, [f.title]));
532
+ if (f.description) meta.appendChild(el("div", { class: "fk-item-desc" }, [f.description]));
533
+ const badges = el("div", { class: "fk-item-badges" });
534
+ if (f.status && f.status !== "open") {
535
+ const b = el("span", { class: "fk-badge" }, [f.status.replace("_", " ")]);
536
+ b.setAttribute("data-status", f.status);
537
+ badges.appendChild(b);
538
+ }
539
+ if (f.tag) badges.appendChild(el("span", { class: "fk-badge" }, [f.tag]));
540
+ if (badges.children.length) meta.appendChild(badges);
541
+ let commentsLoaded = false;
542
+ const commentsEl = el("div", { class: "fk-comments" });
543
+ commentsEl.style.display = "none";
544
+ meta.appendChild(commentsEl);
545
+ meta.addEventListener("click", async () => {
546
+ const opening = commentsEl.style.display === "none";
547
+ commentsEl.style.display = opening ? "" : "none";
548
+ if (opening && !commentsLoaded) {
549
+ commentsLoaded = true;
550
+ commentsEl.replaceChildren(el("div", { class: "fk-loading" }, ["Loading\u2026"]));
551
+ try {
552
+ const cs = await client.listComments(f.id);
553
+ commentsEl.replaceChildren(...renderComments(f, cs));
554
+ } catch (e) {
555
+ commentsEl.replaceChildren(
556
+ el("div", { class: "fk-error" }, [e.message])
557
+ );
558
+ }
559
+ }
560
+ });
561
+ row.appendChild(meta);
562
+ return row;
563
+ }
564
+ function renderComments(f, comments) {
565
+ const nodes = [];
566
+ if (comments.length === 0) {
567
+ nodes.push(el("div", { class: "fk-empty" }, ["No replies yet."]));
568
+ } else {
569
+ for (const c of comments) {
570
+ nodes.push(el("div", { class: "fk-comment" }, [
571
+ el("span", { class: "fk-comment-author" }, [c.author_name || "Anonymous"]),
572
+ " \u2014 ",
573
+ c.body
574
+ ]));
575
+ }
576
+ }
577
+ const input = el("textarea", {
578
+ class: "fk-textarea",
579
+ placeholder: "Add a reply\u2026",
580
+ rows: "2"
581
+ });
582
+ const send = el("button", { class: "fk-submit", type: "button" }, ["Reply"]);
583
+ send.addEventListener("click", async (e) => {
584
+ e.stopPropagation();
585
+ if (!input.value.trim()) return;
586
+ send.disabled = true;
587
+ try {
588
+ const c = await client.comment(f.id, input.value);
589
+ input.value = "";
590
+ send.disabled = false;
591
+ const refreshed = await client.listComments(f.id);
592
+ const parent = send.parentElement?.parentElement;
593
+ if (parent) parent.replaceChildren(...renderComments(f, refreshed));
594
+ } catch (err) {
595
+ send.disabled = false;
596
+ alert(err.message);
597
+ }
598
+ });
599
+ nodes.push(el("div", {}, [input, send]));
600
+ return nodes;
601
+ }
602
+ function renderForm() {
603
+ body.innerHTML = "";
604
+ const enabled = client.getEnabledKinds();
605
+ const enabledOptions = enabled.map((value) => ({ value, ...KIND_OPTIONS[value] }));
606
+ const safeOptions = enabledOptions.length > 0 ? enabledOptions : [
607
+ { value: "other", ...KIND_OPTIONS.other }
608
+ ];
609
+ let kind = safeOptions[0].value;
610
+ const form = el("form", { class: "fk-form" });
611
+ const kindLabel = el("label", { class: "fk-label" }, ["What's this about?"]);
612
+ const segmented = el("div", { class: "fk-segmented" });
613
+ const segButtons = [];
614
+ for (const opt of safeOptions) {
615
+ const btn = el("button", { class: "fk-seg", type: "button" }, [opt.label]);
616
+ btn.setAttribute("data-active", String(opt.value === kind));
617
+ btn.addEventListener("click", () => {
618
+ kind = opt.value;
619
+ segButtons.forEach(
620
+ (b, i) => b.setAttribute("data-active", String(safeOptions[i].value === kind))
621
+ );
622
+ titleInput.placeholder = safeOptions.find((o) => o.value === kind).placeholder;
623
+ });
624
+ segButtons.push(btn);
625
+ segmented.appendChild(btn);
626
+ }
627
+ kindLabel.appendChild(segmented);
628
+ const titleLabel = el("label", { class: "fk-label" }, ["Title"]);
629
+ const titleInput = el("input", {
630
+ class: "fk-input",
631
+ type: "text",
632
+ placeholder: safeOptions[0].placeholder,
633
+ required: "true"
634
+ });
635
+ titleLabel.appendChild(titleInput);
636
+ const descLabel = el("label", { class: "fk-label" }, ["Description"]);
637
+ const descInput = el("textarea", {
638
+ class: "fk-textarea",
639
+ placeholder: "Any extra context helps.",
640
+ rows: "4"
641
+ });
642
+ descLabel.appendChild(descInput);
643
+ const submit = el("button", { class: "fk-submit", type: "submit" }, ["Submit"]);
644
+ form.append(kindLabel, titleLabel, descLabel, submit);
645
+ body.appendChild(form);
646
+ form.addEventListener("submit", async (e) => {
647
+ e.preventDefault();
648
+ if (!titleInput.value.trim()) return;
649
+ submit.disabled = true;
650
+ submit.textContent = "Submitting\u2026";
651
+ try {
652
+ await client.submit({
653
+ title: titleInput.value,
654
+ description: descInput.value,
655
+ kind
656
+ });
657
+ titleInput.value = "";
658
+ descInput.value = "";
659
+ setMode("browse");
660
+ if (tabsEl) {
661
+ activeKind = kind;
662
+ paintTabs();
663
+ }
664
+ await refresh();
665
+ } catch (err) {
666
+ submit.disabled = false;
667
+ submit.textContent = "Submit";
668
+ alert(err.message);
669
+ }
670
+ });
671
+ }
672
+ function setMode(m) {
673
+ mode = m;
674
+ paintModes();
675
+ if (mode === "browse") refresh();
676
+ else renderForm();
677
+ }
678
+ modeBrowse.addEventListener("click", () => setMode("browse"));
679
+ modeSuggest.addEventListener("click", () => setMode("suggest"));
680
+ paintModes();
681
+ paintTabs();
682
+ setMode("browse");
683
+ return overlay;
684
+ }
685
+
686
+ // src/index.tsx
687
+ import { jsx } from "react/jsx-runtime";
688
+ var HeedKitContext = React.createContext(null);
689
+ function HeedKitProvider({
690
+ projectKey,
691
+ apiUrl,
692
+ user,
693
+ children
694
+ }) {
695
+ const [client] = React.useState(() => new HeedKitClient({ projectKey, apiUrl, user }));
696
+ const [ready, setReady] = React.useState(false);
697
+ const [theme, setTheme] = React.useState({});
698
+ React.useEffect(() => {
699
+ client.init(user || {}).then(() => {
700
+ setTheme(client.getTheme());
701
+ setReady(true);
702
+ });
703
+ }, [client]);
704
+ return /* @__PURE__ */ jsx(HeedKitContext.Provider, { value: { client, ready, theme }, children });
705
+ }
706
+ function useHeedKit() {
707
+ const ctx = React.useContext(HeedKitContext);
708
+ if (!ctx) throw new Error("useHeedKit must be used inside <HeedKitProvider>");
709
+ return ctx;
710
+ }
711
+ var FeedbackButton = React.forwardRef(
712
+ function FeedbackButton2({ projectKey, apiUrl, user, label, hideLauncher }, ref) {
713
+ const widgetRef = React.useRef(null);
714
+ React.useEffect(() => {
715
+ const w = mount({ projectKey, apiUrl, user, label, hideLauncher });
716
+ widgetRef.current = w;
717
+ if (typeof ref === "function") ref(w);
718
+ else if (ref) ref.current = w;
719
+ return () => {
720
+ w.destroy();
721
+ widgetRef.current = null;
722
+ if (typeof ref === "function") ref(null);
723
+ else if (ref) ref.current = null;
724
+ };
725
+ }, [projectKey, apiUrl, label, hideLauncher, JSON.stringify(user)]);
726
+ return null;
727
+ }
728
+ );
729
+ export {
730
+ FeedbackButton,
731
+ HeedKitClient,
732
+ HeedKitProvider,
733
+ mount,
734
+ useHeedKit
735
+ };