feedtack 0.5.1 → 1.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.
@@ -1,33 +1,9 @@
1
1
  import {
2
- SCHEMA_VERSION,
3
- getDeviceMeta,
4
- getPageMeta,
5
- getPinCoords,
6
- getTargetMeta,
7
- getViewportMeta,
8
- themeToCSS
9
- } from "../chunk-PPM4AIJU.js";
10
-
11
- // src/ui/colors.ts
12
- var PIN_PALETTE = [
13
- "#ef4444",
14
- // red
15
- "#3b82f6",
16
- // blue
17
- "#22c55e",
18
- // green
19
- "#f59e0b",
20
- // amber
21
- "#a855f7",
22
- // purple
23
- "#ec4899"
24
- // pink
25
- ];
2
+ FeedtackEngine,
3
+ PIN_PALETTE
4
+ } from "../chunk-3INDOI4N.js";
26
5
 
27
6
  // src/react/utils.ts
28
- function generateId() {
29
- return `ft_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`;
30
- }
31
7
  function getAnchoredPosition(x, y) {
32
8
  const FORM_HEIGHT = 220;
33
9
  const EDGE = 300;
@@ -92,20 +68,18 @@ function CommentForm({
92
68
  "button",
93
69
  {
94
70
  type: "button",
95
- className: sentiment === "satisfied" ? "selected" : "",
96
- onClick: () => onSentimentChange(sentiment === "satisfied" ? null : "satisfied"),
97
- children: sentimentLabels.satisfied ?? "\u{1F60A} Satisfied"
71
+ className: sentiment === "good" ? "selected" : "",
72
+ onClick: () => onSentimentChange(sentiment === "good" ? null : "good"),
73
+ children: sentimentLabels.satisfied ?? "Good"
98
74
  }
99
75
  ),
100
76
  /* @__PURE__ */ jsx(
101
77
  "button",
102
78
  {
103
79
  type: "button",
104
- className: sentiment === "dissatisfied" ? "selected" : "",
105
- onClick: () => onSentimentChange(
106
- sentiment === "dissatisfied" ? null : "dissatisfied"
107
- ),
108
- children: sentimentLabels.dissatisfied ?? "\u{1F61E} Dissatisfied"
80
+ className: sentiment === "bad" ? "selected" : "",
81
+ onClick: () => onSentimentChange(sentiment === "bad" ? null : "bad"),
82
+ children: sentimentLabels.dissatisfied ?? "Bad"
109
83
  }
110
84
  )
111
85
  ] }),
@@ -145,8 +119,311 @@ function useFeedtackContext() {
145
119
  return ctx;
146
120
  }
147
121
 
148
- // src/react/ThreadPanel.tsx
122
+ // src/react/FeedbackModal.tsx
123
+ import { useEffect, useRef } from "react";
124
+
125
+ // src/react/ThreadView.tsx
149
126
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
127
+ function ThreadView({
128
+ item,
129
+ replyBody,
130
+ onReplyBodyChange,
131
+ onReply,
132
+ onResolve,
133
+ onArchive,
134
+ onBack
135
+ }) {
136
+ return /* @__PURE__ */ jsxs2("div", { className: "feedtack-modal-thread-view", children: [
137
+ /* @__PURE__ */ jsx2("button", { type: "button", className: "feedtack-modal-back", onClick: onBack, children: "\u2190 Back" }),
138
+ /* @__PURE__ */ jsxs2("div", { className: "feedtack-modal-thread-content", children: [
139
+ /* @__PURE__ */ jsx2("strong", { children: item.payload.submittedBy.name }),
140
+ /* @__PURE__ */ jsx2("p", { children: item.payload.comment })
141
+ ] }),
142
+ item.replies.map((r) => /* @__PURE__ */ jsxs2("div", { className: "feedtack-modal-reply", children: [
143
+ /* @__PURE__ */ jsx2("span", { className: "feedtack-reply-author", children: r.author.name }),
144
+ /* @__PURE__ */ jsx2("p", { children: r.body })
145
+ ] }, r.id)),
146
+ /* @__PURE__ */ jsx2(
147
+ "textarea",
148
+ {
149
+ className: "feedtack-modal-textarea",
150
+ placeholder: "Reply\u2026",
151
+ value: replyBody,
152
+ onChange: (e) => onReplyBodyChange(e.target.value)
153
+ }
154
+ ),
155
+ /* @__PURE__ */ jsxs2("div", { className: "feedtack-modal-actions", children: [
156
+ /* @__PURE__ */ jsx2("button", { type: "button", className: "feedtack-btn-submit", onClick: onReply, children: "Reply" }),
157
+ /* @__PURE__ */ jsx2(
158
+ "button",
159
+ {
160
+ type: "button",
161
+ className: "feedtack-btn-cancel",
162
+ onClick: onResolve,
163
+ children: "Resolve"
164
+ }
165
+ ),
166
+ /* @__PURE__ */ jsx2(
167
+ "button",
168
+ {
169
+ type: "button",
170
+ className: "feedtack-btn-cancel",
171
+ onClick: onArchive,
172
+ children: "Archive"
173
+ }
174
+ )
175
+ ] })
176
+ ] });
177
+ }
178
+
179
+ // src/react/FeedbackModal.tsx
180
+ import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
181
+ function FeedbackModal({
182
+ isOpen,
183
+ onClose,
184
+ activeTab,
185
+ onTabChange,
186
+ siteFeedback,
187
+ pageFeedback,
188
+ comment,
189
+ onCommentChange,
190
+ commentError,
191
+ sentiment,
192
+ onSentimentChange,
193
+ submitting,
194
+ onSubmit,
195
+ onPlacePin,
196
+ replyBody,
197
+ onReplyBodyChange,
198
+ onReply,
199
+ onResolve,
200
+ onArchive,
201
+ openThreadId,
202
+ onOpenThread
203
+ }) {
204
+ const panelRef = useRef(null);
205
+ useEffect(() => {
206
+ if (!isOpen) return;
207
+ const onKey = (e) => {
208
+ if (e.key === "Escape") onClose();
209
+ };
210
+ const onDown = (e) => {
211
+ if (panelRef.current && !panelRef.current.contains(e.target)) {
212
+ const btn = document.querySelector(".feedtack-btn");
213
+ if (btn?.contains(e.target)) return;
214
+ onClose();
215
+ }
216
+ };
217
+ window.addEventListener("keydown", onKey);
218
+ document.addEventListener("mousedown", onDown);
219
+ return () => {
220
+ window.removeEventListener("keydown", onKey);
221
+ document.removeEventListener("mousedown", onDown);
222
+ };
223
+ }, [isOpen, onClose]);
224
+ if (!isOpen) return null;
225
+ const threads = activeTab === "site" ? siteFeedback : pageFeedback;
226
+ const openItem = openThreadId ? threads.find((i) => i.payload.id === openThreadId) : null;
227
+ return /* @__PURE__ */ jsxs3(
228
+ "div",
229
+ {
230
+ ref: panelRef,
231
+ className: "feedtack-modal",
232
+ role: "dialog",
233
+ "aria-label": "Feedback",
234
+ "aria-modal": "true",
235
+ children: [
236
+ /* @__PURE__ */ jsxs3("div", { className: "feedtack-modal-header", children: [
237
+ /* @__PURE__ */ jsx3("span", { className: "feedtack-modal-title", children: "Feedback" }),
238
+ /* @__PURE__ */ jsx3(
239
+ "button",
240
+ {
241
+ type: "button",
242
+ className: "feedtack-modal-close",
243
+ onClick: onClose,
244
+ "aria-label": "Close",
245
+ children: "\xD7"
246
+ }
247
+ )
248
+ ] }),
249
+ /* @__PURE__ */ jsxs3("div", { className: "feedtack-modal-tabs", children: [
250
+ /* @__PURE__ */ jsxs3(
251
+ "button",
252
+ {
253
+ type: "button",
254
+ className: cx("feedtack-modal-tab", activeTab === "site" && "active"),
255
+ onClick: () => onTabChange("site"),
256
+ children: [
257
+ "Site",
258
+ siteFeedback.length > 0 && /* @__PURE__ */ jsx3("span", { className: "feedtack-tab-count", children: siteFeedback.length })
259
+ ]
260
+ }
261
+ ),
262
+ /* @__PURE__ */ jsxs3(
263
+ "button",
264
+ {
265
+ type: "button",
266
+ className: cx("feedtack-modal-tab", activeTab === "page" && "active"),
267
+ onClick: () => onTabChange("page"),
268
+ children: [
269
+ "Page",
270
+ pageFeedback.length > 0 && /* @__PURE__ */ jsx3("span", { className: "feedtack-tab-count", children: pageFeedback.length })
271
+ ]
272
+ }
273
+ )
274
+ ] }),
275
+ /* @__PURE__ */ jsx3("div", { className: "feedtack-modal-body", children: openItem ? /* @__PURE__ */ jsx3(
276
+ ThreadView,
277
+ {
278
+ item: openItem,
279
+ replyBody,
280
+ onReplyBodyChange,
281
+ onReply: () => onReply(openItem.payload.id),
282
+ onResolve: () => onResolve(openItem.payload.id),
283
+ onArchive: () => onArchive(openItem.payload.id),
284
+ onBack: () => onOpenThread(null)
285
+ }
286
+ ) : /* @__PURE__ */ jsxs3(Fragment, { children: [
287
+ threads.length > 0 && /* @__PURE__ */ jsx3("div", { className: "feedtack-modal-threads", children: threads.map((item) => /* @__PURE__ */ jsxs3(
288
+ "button",
289
+ {
290
+ type: "button",
291
+ className: "feedtack-modal-thread-item",
292
+ onClick: () => onOpenThread(item.payload.id),
293
+ children: [
294
+ /* @__PURE__ */ jsx3("span", { className: "feedtack-thread-author", children: item.payload.submittedBy.name }),
295
+ /* @__PURE__ */ jsx3("span", { className: "feedtack-thread-comment", children: item.payload.comment }),
296
+ /* @__PURE__ */ jsxs3("span", { className: "feedtack-thread-meta", children: [
297
+ item.replies.length > 0 && `${item.replies.length} ${item.replies.length === 1 ? "reply" : "replies"}`,
298
+ item.resolutions.length > 0 && " \xB7 resolved"
299
+ ] })
300
+ ]
301
+ },
302
+ item.payload.id
303
+ )) }),
304
+ /* @__PURE__ */ jsxs3("div", { className: "feedtack-modal-compose", children: [
305
+ /* @__PURE__ */ jsx3(
306
+ "textarea",
307
+ {
308
+ className: cx(
309
+ "feedtack-modal-textarea",
310
+ commentError && "error"
311
+ ),
312
+ placeholder: "What's on your mind? (required)",
313
+ value: comment,
314
+ onChange: (e) => onCommentChange(e.target.value),
315
+ onKeyDown: (e) => {
316
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
317
+ e.preventDefault();
318
+ onSubmit();
319
+ }
320
+ },
321
+ "aria-invalid": commentError || void 0
322
+ }
323
+ ),
324
+ commentError && /* @__PURE__ */ jsx3("span", { className: "feedtack-error-msg", children: "Comment is required" }),
325
+ /* @__PURE__ */ jsxs3("div", { className: "feedtack-sentiment", children: [
326
+ /* @__PURE__ */ jsx3(
327
+ "button",
328
+ {
329
+ type: "button",
330
+ className: sentiment === "good" ? "selected" : "",
331
+ onClick: () => onSentimentChange(sentiment === "good" ? null : "good"),
332
+ children: "Good"
333
+ }
334
+ ),
335
+ /* @__PURE__ */ jsx3(
336
+ "button",
337
+ {
338
+ type: "button",
339
+ className: sentiment === "bad" ? "selected" : "",
340
+ onClick: () => onSentimentChange(sentiment === "bad" ? null : "bad"),
341
+ children: "Bad"
342
+ }
343
+ )
344
+ ] }),
345
+ /* @__PURE__ */ jsx3(
346
+ "button",
347
+ {
348
+ type: "button",
349
+ className: "feedtack-btn-submit",
350
+ onClick: onSubmit,
351
+ disabled: submitting,
352
+ children: submitting ? "Sending\u2026" : "Submit"
353
+ }
354
+ )
355
+ ] })
356
+ ] }) }),
357
+ /* @__PURE__ */ jsx3("div", { className: "feedtack-modal-footer", children: /* @__PURE__ */ jsx3(
358
+ "button",
359
+ {
360
+ type: "button",
361
+ className: "feedtack-modal-pin-btn",
362
+ onClick: onPlacePin,
363
+ children: "Place a pin"
364
+ }
365
+ ) })
366
+ ]
367
+ }
368
+ );
369
+ }
370
+
371
+ // src/react/PinOverlay.tsx
372
+ import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
373
+ function PinOverlay({
374
+ feedbackItems,
375
+ pathname,
376
+ isArchivedForUser,
377
+ hasValidPins,
378
+ hasUnread,
379
+ openThreadId,
380
+ setOpenThreadId,
381
+ getPosition,
382
+ renderPinIcon,
383
+ pinMarkerClass
384
+ }) {
385
+ return /* @__PURE__ */ jsx4(Fragment2, { children: feedbackItems.filter((item) => item.payload.page.pathname === pathname).filter((item) => !isArchivedForUser(item)).filter((item) => hasValidPins(item)).map((item) => {
386
+ const pin = item.payload.pins[0];
387
+ const pos = getPosition(item.payload.id, pin);
388
+ return /* @__PURE__ */ jsxs4(
389
+ "button",
390
+ {
391
+ type: "button",
392
+ className: cx(
393
+ "feedtack-pin-marker",
394
+ item.resolutions.length > 0 && "feedtack-pin-resolved",
395
+ pinMarkerClass
396
+ ),
397
+ style: {
398
+ background: pin.color,
399
+ left: pos.x,
400
+ top: pos.y,
401
+ position: "absolute",
402
+ cursor: "pointer"
403
+ },
404
+ onClick: () => setOpenThreadId(
405
+ openThreadId === item.payload.id ? null : item.payload.id
406
+ ),
407
+ children: [
408
+ renderPinIcon ? /* @__PURE__ */ jsx4("span", { className: "feedtack-pin-icon", children: renderPinIcon(item) }) : item.resolutions.length > 0 && /* @__PURE__ */ jsx4(
409
+ "span",
410
+ {
411
+ className: "feedtack-pin-icon",
412
+ role: "img",
413
+ "aria-label": "Resolved",
414
+ children: "\u2713"
415
+ }
416
+ ),
417
+ hasUnread(item) && /* @__PURE__ */ jsx4("div", { className: "feedtack-pin-badge" })
418
+ ]
419
+ },
420
+ item.payload.id
421
+ );
422
+ }) });
423
+ }
424
+
425
+ // src/react/ThreadPanel.tsx
426
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
150
427
  function ThreadPanel({
151
428
  item,
152
429
  replyBody,
@@ -162,26 +439,26 @@ function ThreadPanel({
162
439
  if (!pin) return null;
163
440
  const { x, y } = pinPosition ?? pin;
164
441
  const pos = getAnchoredPosition(x, y);
165
- return /* @__PURE__ */ jsxs2(
442
+ return /* @__PURE__ */ jsxs5(
166
443
  "div",
167
444
  {
168
445
  className: cx("feedtack-thread", className),
169
446
  style: { position: "fixed", ...pos },
170
447
  children: [
171
- /* @__PURE__ */ jsx2("strong", { style: { fontSize: 13 }, children: item.payload.submittedBy.name }),
172
- /* @__PURE__ */ jsx2("p", { style: { fontSize: 13 }, children: item.payload.comment }),
173
- item.replies.map((r) => /* @__PURE__ */ jsxs2(
448
+ /* @__PURE__ */ jsx5("strong", { style: { fontSize: 13 }, children: item.payload.submittedBy.name }),
449
+ /* @__PURE__ */ jsx5("p", { style: { fontSize: 13 }, children: item.payload.comment }),
450
+ item.replies.map((r) => /* @__PURE__ */ jsxs5(
174
451
  "div",
175
452
  {
176
453
  style: { borderTop: "1px solid var(--ft-border)", paddingTop: 8 },
177
454
  children: [
178
- /* @__PURE__ */ jsx2("span", { style: { fontSize: 12, fontWeight: 600 }, children: r.author.name }),
179
- /* @__PURE__ */ jsx2("p", { style: { fontSize: 12 }, children: r.body })
455
+ /* @__PURE__ */ jsx5("span", { style: { fontSize: 12, fontWeight: 600 }, children: r.author.name }),
456
+ /* @__PURE__ */ jsx5("p", { style: { fontSize: 12 }, children: r.body })
180
457
  ]
181
458
  },
182
459
  r.id
183
460
  )),
184
- /* @__PURE__ */ jsx2(
461
+ /* @__PURE__ */ jsx5(
185
462
  "textarea",
186
463
  {
187
464
  placeholder: "Reply\u2026",
@@ -201,8 +478,8 @@ function ThreadPanel({
201
478
  }
202
479
  }
203
480
  ),
204
- /* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: 6, flexWrap: "wrap" }, children: [
205
- /* @__PURE__ */ jsx2(
481
+ /* @__PURE__ */ jsxs5("div", { style: { display: "flex", gap: 6, flexWrap: "wrap" }, children: [
482
+ /* @__PURE__ */ jsx5(
206
483
  "button",
207
484
  {
208
485
  type: "button",
@@ -212,7 +489,7 @@ function ThreadPanel({
212
489
  children: "Reply"
213
490
  }
214
491
  ),
215
- /* @__PURE__ */ jsx2(
492
+ /* @__PURE__ */ jsx5(
216
493
  "button",
217
494
  {
218
495
  type: "button",
@@ -222,7 +499,7 @@ function ThreadPanel({
222
499
  children: "Mark Resolved"
223
500
  }
224
501
  ),
225
- /* @__PURE__ */ jsx2(
502
+ /* @__PURE__ */ jsx5(
226
503
  "button",
227
504
  {
228
505
  type: "button",
@@ -232,7 +509,7 @@ function ThreadPanel({
232
509
  children: "Archive"
233
510
  }
234
511
  ),
235
- /* @__PURE__ */ jsx2(
512
+ /* @__PURE__ */ jsx5(
236
513
  "button",
237
514
  {
238
515
  type: "button",
@@ -249,7 +526,7 @@ function ThreadPanel({
249
526
  }
250
527
 
251
528
  // src/react/useAnchoredPins.ts
252
- import { useCallback, useEffect, useState } from "react";
529
+ import { useCallback, useEffect as useEffect2, useState } from "react";
253
530
  function useAnchoredPins(items, pathname) {
254
531
  const [positions, setPositions] = useState(
255
532
  /* @__PURE__ */ new Map()
@@ -265,10 +542,10 @@ function useAnchoredPins(items, pathname) {
265
542
  }
266
543
  setPositions(next);
267
544
  }, [items, pathname]);
268
- useEffect(() => {
545
+ useEffect2(() => {
269
546
  resolve();
270
547
  }, [resolve]);
271
- useEffect(() => {
548
+ useEffect2(() => {
272
549
  let raf = 0;
273
550
  const handler = () => {
274
551
  cancelAnimationFrame(raf);
@@ -310,584 +587,7 @@ function resolvePin(pin) {
310
587
  }
311
588
 
312
589
  // src/react/useFeedtackState.ts
313
- import { useCallback as useCallback5, useEffect as useEffect5, useState as useState3 } from "react";
314
-
315
- // src/react/useFeedtackActions.ts
316
- import { useCallback as useCallback2 } from "react";
317
- function useFeedtackActions(deps) {
318
- const { adapter, currentUser, onError } = deps;
319
- const updateItem = useCallback2(
320
- (id, fn) => deps.setFeedbackItems(
321
- (prev) => prev.map((i) => i.payload.id === id ? fn(i) : i)
322
- ),
323
- [deps.setFeedbackItems]
324
- );
325
- const handleSubmit = useCallback2(async () => {
326
- const comment = deps.getComment();
327
- if (!comment.trim()) {
328
- deps.setCommentError(true);
329
- return;
330
- }
331
- deps.setSubmitting(true);
332
- const payload = {
333
- schemaVersion: SCHEMA_VERSION,
334
- id: generateId(),
335
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
336
- submittedBy: currentUser,
337
- comment: comment.trim(),
338
- sentiment: deps.getSentiment(),
339
- pins: deps.getPendingPins().map((p, i) => ({ ...p, index: i + 1 })),
340
- page: getPageMeta(),
341
- viewport: getViewportMeta(),
342
- device: getDeviceMeta()
343
- };
344
- try {
345
- await adapter.submit(payload);
346
- deps.setFeedbackItems((prev) => [
347
- ...prev,
348
- { payload, replies: [], resolutions: [], archives: [] }
349
- ]);
350
- deps.deactivatePinMode();
351
- } catch (err) {
352
- onError?.(err);
353
- } finally {
354
- deps.setSubmitting(false);
355
- }
356
- }, [adapter, currentUser, onError, deps]);
357
- const handleReply = useCallback2(
358
- async (feedbackId) => {
359
- const body = deps.getReplyBody().trim();
360
- if (!body) return;
361
- const ts = (/* @__PURE__ */ new Date()).toISOString();
362
- try {
363
- await adapter.reply(feedbackId, {
364
- author: currentUser,
365
- body,
366
- timestamp: ts
367
- });
368
- updateItem(feedbackId, (item) => {
369
- const updated = {
370
- ...item,
371
- replies: [
372
- ...item.replies,
373
- {
374
- id: generateId(),
375
- feedbackId,
376
- author: currentUser,
377
- body,
378
- timestamp: ts
379
- }
380
- ]
381
- };
382
- const rescope = deps.shouldRescope?.(currentUser.role) ?? currentUser.role !== "agent";
383
- if (rescope && updated.resolutions.length === 0 && deps.hasFlush) {
384
- deps.clearFlushed?.(deps.getPathname());
385
- }
386
- return updated;
387
- });
388
- deps.setReplyBody("");
389
- } catch (err) {
390
- onError?.(err);
391
- }
392
- },
393
- [adapter, currentUser, onError, updateItem, deps]
394
- );
395
- const handleResolve = useCallback2(
396
- async (feedbackId) => {
397
- const ts = (/* @__PURE__ */ new Date()).toISOString();
398
- try {
399
- await adapter.resolve(feedbackId, {
400
- resolvedBy: currentUser,
401
- timestamp: ts
402
- });
403
- updateItem(feedbackId, (item) => ({
404
- ...item,
405
- resolutions: [
406
- ...item.resolutions,
407
- { feedbackId, resolvedBy: currentUser, timestamp: ts }
408
- ]
409
- }));
410
- } catch (err) {
411
- onError?.(err);
412
- }
413
- },
414
- [adapter, currentUser, onError, updateItem]
415
- );
416
- const handleArchive = useCallback2(
417
- async (feedbackId) => {
418
- const ts = (/* @__PURE__ */ new Date()).toISOString();
419
- try {
420
- await adapter.archive(feedbackId, currentUser.id);
421
- updateItem(feedbackId, (item) => ({
422
- ...item,
423
- archives: [
424
- ...item.archives,
425
- { feedbackId, archivedBy: currentUser, timestamp: ts }
426
- ]
427
- }));
428
- deps.setOpenThreadId(null);
429
- } catch (err) {
430
- onError?.(err);
431
- }
432
- },
433
- [adapter, currentUser, onError, updateItem, deps]
434
- );
435
- return { handleSubmit, handleReply, handleResolve, handleArchive };
436
- }
437
-
438
- // src/react/useFeedtackDom.ts
439
- import { useEffect as useEffect2, useRef } from "react";
440
-
441
- // src/ui/styles.ts
442
- var FEEDTACK_DEFAULT_TOKENS = `
443
- #feedtack-root, .feedtack-form, .feedtack-thread {
444
- --ft-primary: #2563eb;
445
- --ft-primary-hover: #1d4ed8;
446
- --ft-bg: #ffffff;
447
- --ft-surface: #f9fafb;
448
- --ft-text: #111827;
449
- --ft-text-muted: #6b7280;
450
- --ft-border: #e5e7eb;
451
- --ft-radius: 8px;
452
- --ft-error: #ef4444;
453
- --ft-badge: #f59e0b;
454
- }
455
- `;
456
- var FEEDTACK_STYLES = `
457
- #feedtack-root * {
458
- box-sizing: border-box;
459
- margin: 0;
460
- padding: 0;
461
- font-family: system-ui, -apple-system, sans-serif;
462
- line-height: 1.5;
463
- }
464
-
465
- .feedtack-btn {
466
- position: fixed;
467
- bottom: 24px;
468
- right: 24px;
469
- z-index: 2147483640;
470
- background: var(--ft-text);
471
- color: var(--ft-bg);
472
- border: none;
473
- border-radius: var(--ft-radius);
474
- padding: 8px 14px;
475
- font-size: 13px;
476
- font-weight: 500;
477
- cursor: pointer;
478
- box-shadow: 0 2px 8px rgba(0,0,0,0.25);
479
- display: flex;
480
- align-items: center;
481
- gap: 6px;
482
- transition: background 0.15s;
483
- }
484
-
485
- .feedtack-btn:hover {
486
- opacity: 0.85;
487
- }
488
-
489
- .feedtack-btn.active {
490
- background: var(--ft-primary);
491
- }
492
-
493
- .feedtack-crosshair * {
494
- cursor: crosshair !important;
495
- }
496
-
497
- .feedtack-pin-marker {
498
- position: absolute;
499
- z-index: 2147483641;
500
- width: 24px;
501
- height: 24px;
502
- border-radius: 50% 50% 50% 0;
503
- transform: translate(-50%, -100%) rotate(-45deg);
504
- transform-origin: bottom center;
505
- border: 2px solid rgba(255,255,255,0.8);
506
- box-shadow: 0 2px 6px rgba(0,0,0,0.3);
507
- cursor: pointer;
508
- pointer-events: all;
509
- }
510
-
511
- .feedtack-pin-resolved { opacity: 0.6; }
512
-
513
- .feedtack-pin-icon {
514
- position: absolute;
515
- inset: 0;
516
- display: flex;
517
- align-items: center;
518
- justify-content: center;
519
- transform: rotate(45deg);
520
- font-size: 12px;
521
- font-weight: 700;
522
- color: #fff;
523
- line-height: 1;
524
- pointer-events: none;
525
- }
526
-
527
- .feedtack-pin-badge {
528
- position: absolute;
529
- top: -4px;
530
- right: -4px;
531
- width: 10px;
532
- height: 10px;
533
- background: var(--ft-badge);
534
- border-radius: 50%;
535
- border: 1.5px solid var(--ft-bg);
536
- }
537
-
538
- .feedtack-color-picker {
539
- display: flex;
540
- gap: 6px;
541
- padding: 8px;
542
- background: var(--ft-bg) !important;
543
- border-radius: var(--ft-radius);
544
- box-shadow: 0 2px 8px rgba(0,0,0,0.15);
545
- position: fixed;
546
- bottom: 72px;
547
- right: 24px;
548
- z-index: 2147483641;
549
- }
550
-
551
- .feedtack-color-swatch {
552
- width: 20px;
553
- height: 20px;
554
- border-radius: 50%;
555
- border: 2px solid transparent;
556
- cursor: pointer;
557
- transition: transform 0.1s;
558
- }
559
-
560
- .feedtack-color-swatch.selected {
561
- border-color: var(--ft-text);
562
- transform: scale(1.15);
563
- }
564
-
565
- .feedtack-form {
566
- position: absolute;
567
- z-index: 2147483642;
568
- background: var(--ft-bg) !important;
569
- border-radius: calc(var(--ft-radius) + 2px);
570
- box-shadow: 0 4px 20px rgba(0,0,0,0.18);
571
- padding: 16px;
572
- width: 280px;
573
- display: flex;
574
- flex-direction: column;
575
- gap: 10px;
576
- }
577
-
578
- .feedtack-form textarea {
579
- width: 100%;
580
- border: 1.5px solid var(--ft-border);
581
- border-radius: var(--ft-radius);
582
- padding: 8px;
583
- font-size: 13px;
584
- resize: vertical;
585
- min-height: 80px;
586
- outline: none;
587
- background: var(--ft-surface);
588
- color: var(--ft-text);
589
- }
590
-
591
- .feedtack-form textarea:focus {
592
- border-color: var(--ft-primary);
593
- }
594
-
595
- .feedtack-form textarea.error {
596
- border-color: var(--ft-error);
597
- }
598
-
599
- .feedtack-error-msg {
600
- font-size: 12px;
601
- color: var(--ft-error);
602
- }
603
-
604
- .feedtack-sentiment {
605
- display: flex;
606
- gap: 8px;
607
- }
608
-
609
- .feedtack-sentiment button {
610
- flex: 1;
611
- padding: 6px 10px;
612
- border: 1.5px solid var(--ft-border);
613
- border-radius: var(--ft-radius);
614
- background: var(--ft-bg);
615
- color: var(--ft-text);
616
- font-size: 12px;
617
- cursor: pointer;
618
- transition: all 0.1s;
619
- }
620
-
621
- .feedtack-sentiment button.selected {
622
- border-color: var(--ft-primary);
623
- background: var(--ft-surface);
624
- color: var(--ft-primary);
625
- }
626
-
627
- .feedtack-form-actions {
628
- display: flex;
629
- gap: 8px;
630
- justify-content: flex-end;
631
- }
632
-
633
- .feedtack-btn-cancel {
634
- padding: 6px 12px;
635
- border: 1.5px solid var(--ft-border);
636
- border-radius: var(--ft-radius);
637
- background: var(--ft-bg);
638
- color: var(--ft-text);
639
- font-size: 13px;
640
- cursor: pointer;
641
- }
642
-
643
- .feedtack-btn-submit {
644
- padding: 6px 12px;
645
- border: none;
646
- border-radius: var(--ft-radius);
647
- background: var(--ft-primary);
648
- color: #fff;
649
- font-size: 13px;
650
- font-weight: 500;
651
- cursor: pointer;
652
- }
653
-
654
- .feedtack-btn-submit:disabled {
655
- opacity: 0.5;
656
- cursor: not-allowed;
657
- }
658
-
659
- .feedtack-thread {
660
- position: absolute;
661
- z-index: 2147483642;
662
- background: var(--ft-bg) !important;
663
- border-radius: calc(var(--ft-radius) + 2px);
664
- box-shadow: 0 4px 20px rgba(0,0,0,0.18);
665
- padding: 16px;
666
- width: 300px;
667
- max-height: 400px;
668
- overflow-y: auto;
669
- display: flex;
670
- flex-direction: column;
671
- gap: 10px;
672
- }
673
-
674
- .feedtack-sr-only {
675
- position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
676
- overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
677
- }
678
-
679
- .feedtack-loading {
680
- position: fixed;
681
- bottom: 70px;
682
- right: 24px;
683
- font-size: 12px;
684
- color: var(--ft-text-muted);
685
- z-index: 2147483640;
686
- }
687
- `;
688
-
689
- // src/react/useFeedtackDom.ts
690
- function useFeedtackDom(theme, disabled) {
691
- const rootRef = useRef(null);
692
- useEffect2(() => {
693
- if (disabled) return;
694
- if (document.getElementById("feedtack-styles")) return;
695
- const style = document.createElement("style");
696
- style.id = "feedtack-styles";
697
- style.textContent = FEEDTACK_DEFAULT_TOKENS + FEEDTACK_STYLES;
698
- document.head.appendChild(style);
699
- return () => {
700
- style.remove();
701
- };
702
- }, [disabled]);
703
- useEffect2(() => {
704
- if (disabled) return;
705
- const root = document.createElement("div");
706
- root.id = "feedtack-root";
707
- document.body.appendChild(root);
708
- rootRef.current = root;
709
- return () => {
710
- root.remove();
711
- };
712
- }, [disabled]);
713
- useEffect2(() => {
714
- if (disabled) return;
715
- const root = document.getElementById("feedtack-root");
716
- if (!root || !theme) return;
717
- const tokens = themeToCSS(theme);
718
- for (const [k, v] of Object.entries(tokens)) {
719
- root.style.setProperty(k, v);
720
- }
721
- }, [theme, disabled]);
722
- return rootRef;
723
- }
724
-
725
- // src/react/useFeedtackFlush.ts
726
- import { useCallback as useCallback3, useEffect as useEffect3, useRef as useRef2 } from "react";
727
- var DEFAULT_IDLE_MS = 5 * 60 * 1e3;
728
- function useFeedtackFlush({
729
- pathname,
730
- feedbackItems,
731
- onFlush,
732
- flushIdleMs = DEFAULT_IDLE_MS,
733
- disabled
734
- }) {
735
- const flushedRef = useRef2(/* @__PURE__ */ new Set());
736
- const prevPathnameRef = useRef2(pathname);
737
- const idleTimerRef = useRef2(null);
738
- const flush = useCallback3(
739
- (path, items) => {
740
- if (!onFlush || flushedRef.current.has(path)) return;
741
- const pageItems = items.filter((i) => i.payload.page.pathname === path);
742
- if (pageItems.length === 0) return;
743
- flushedRef.current.add(path);
744
- onFlush({ pathname: path, items: pageItems });
745
- },
746
- [onFlush]
747
- );
748
- useEffect3(() => {
749
- if (disabled || !onFlush) return;
750
- const prev = prevPathnameRef.current;
751
- prevPathnameRef.current = pathname;
752
- if (prev !== pathname) {
753
- flush(prev, feedbackItems);
754
- }
755
- }, [pathname, feedbackItems, flush, onFlush, disabled]);
756
- useEffect3(() => {
757
- if (disabled || !onFlush || flushIdleMs <= 0) return;
758
- const resetTimer = () => {
759
- if (idleTimerRef.current) clearTimeout(idleTimerRef.current);
760
- idleTimerRef.current = setTimeout(() => {
761
- flush(pathname, feedbackItems);
762
- }, flushIdleMs);
763
- };
764
- const events = ["mousemove", "keydown", "scroll", "touchstart"];
765
- for (const e of events)
766
- window.addEventListener(e, resetTimer, { passive: true });
767
- resetTimer();
768
- return () => {
769
- if (idleTimerRef.current) clearTimeout(idleTimerRef.current);
770
- for (const e of events) window.removeEventListener(e, resetTimer);
771
- };
772
- }, [pathname, feedbackItems, flush, onFlush, flushIdleMs, disabled]);
773
- useEffect3(() => {
774
- if (disabled || !onFlush) return;
775
- const handleUnload = () => flush(pathname, feedbackItems);
776
- window.addEventListener("beforeunload", handleUnload);
777
- return () => window.removeEventListener("beforeunload", handleUnload);
778
- }, [pathname, feedbackItems, flush, onFlush, disabled]);
779
- const clearFlushed = useCallback3((path) => {
780
- flushedRef.current.delete(path);
781
- }, []);
782
- return { clearFlushed };
783
- }
784
-
785
- // src/react/usePinMode.ts
786
- import { useCallback as useCallback4, useEffect as useEffect4, useState as useState2 } from "react";
787
- function usePinMode({
788
- hotkey,
789
- onDeactivate,
790
- disabled,
791
- isModalOpen
792
- }) {
793
- const [isActive, setIsActive] = useState2(false);
794
- const [pendingPins, setPendingPins] = useState2([]);
795
- const [selectedColor, setSelectedColor] = useState2(PIN_PALETTE[0]);
796
- const [showForm, setShowForm] = useState2(false);
797
- const activate = useCallback4(() => setIsActive(true), []);
798
- const deactivate = useCallback4(() => {
799
- setIsActive(false);
800
- setPendingPins([]);
801
- setShowForm(false);
802
- onDeactivate?.();
803
- }, [onDeactivate]);
804
- useEffect4(() => {
805
- if (isActive) {
806
- document.documentElement.classList.add("feedtack-crosshair");
807
- } else {
808
- document.documentElement.classList.remove("feedtack-crosshair");
809
- }
810
- return () => document.documentElement.classList.remove("feedtack-crosshair");
811
- }, [isActive]);
812
- useEffect4(() => {
813
- if (disabled) return;
814
- const handler = (e) => {
815
- if (e.key === hotkey.toUpperCase() && e.shiftKey) {
816
- setIsActive((prev) => !prev);
817
- }
818
- if (e.key === "Escape") {
819
- deactivate();
820
- }
821
- if (isActive && !isModalOpen && !showForm && (e.key === "ArrowLeft" || e.key === "ArrowRight")) {
822
- e.preventDefault();
823
- setSelectedColor((prev) => {
824
- const idx = PIN_PALETTE.indexOf(prev);
825
- const dir = e.key === "ArrowRight" ? 1 : -1;
826
- return PIN_PALETTE[(idx + dir + PIN_PALETTE.length) % PIN_PALETTE.length];
827
- });
828
- }
829
- };
830
- window.addEventListener("keydown", handler);
831
- return () => window.removeEventListener("keydown", handler);
832
- }, [hotkey, deactivate, isActive, disabled, isModalOpen, showForm]);
833
- const placePin = useCallback4(
834
- (coords, target) => {
835
- if (target.closest("#feedtack-root, .feedtack-form, .feedtack-color-picker"))
836
- return;
837
- setPendingPins((prev) => [
838
- ...prev,
839
- {
840
- color: selectedColor,
841
- ...getPinCoords(coords),
842
- target: getTargetMeta(target)
843
- }
844
- ]);
845
- setShowForm(true);
846
- },
847
- [selectedColor]
848
- );
849
- const handlePageClick = useCallback4(
850
- (e) => {
851
- if (!isActive) return;
852
- e.preventDefault();
853
- e.stopPropagation();
854
- placePin(e, e.target);
855
- },
856
- [isActive, placePin]
857
- );
858
- const handleTouchEnd = useCallback4(
859
- (e) => {
860
- if (!isActive) return;
861
- const touch = e.changedTouches[0];
862
- if (!touch) return;
863
- const target = document.elementFromPoint(touch.clientX, touch.clientY);
864
- if (!target) return;
865
- e.preventDefault();
866
- placePin(touch, target);
867
- },
868
- [isActive, placePin]
869
- );
870
- useEffect4(() => {
871
- if (disabled) return;
872
- document.addEventListener("click", handlePageClick, true);
873
- document.addEventListener("touchend", handleTouchEnd, true);
874
- return () => {
875
- document.removeEventListener("click", handlePageClick, true);
876
- document.removeEventListener("touchend", handleTouchEnd, true);
877
- };
878
- }, [handlePageClick, handleTouchEnd, disabled]);
879
- return {
880
- isActive,
881
- activate,
882
- deactivate,
883
- pendingPins,
884
- selectedColor,
885
- setSelectedColor,
886
- showForm
887
- };
888
- }
889
-
890
- // src/react/useFeedtackState.ts
590
+ import { useCallback as useCallback2, useEffect as useEffect3, useRef as useRef2, useSyncExternalStore } from "react";
891
591
  function useFeedtackState({
892
592
  adapter,
893
593
  currentUser,
@@ -899,109 +599,112 @@ function useFeedtackState({
899
599
  flushIdleMs,
900
600
  rescopeRoles
901
601
  }) {
902
- useFeedtackDom(theme, disabled);
903
- const [pathname, setPathname] = useState3(
904
- () => typeof window === "undefined" ? "/" : window.location.pathname
602
+ const engineRef = useRef2(null);
603
+ if (!engineRef.current) {
604
+ engineRef.current = new FeedtackEngine({
605
+ adapter,
606
+ currentUser,
607
+ hotkey,
608
+ theme,
609
+ onError,
610
+ disabled,
611
+ onFlush,
612
+ flushIdleMs,
613
+ rescopeRoles
614
+ });
615
+ }
616
+ const engine = engineRef.current;
617
+ useEffect3(() => {
618
+ engine.mount();
619
+ return () => engine.destroy();
620
+ }, [engine]);
621
+ const subscribe = useCallback2(
622
+ (cb) => engine.subscribe(cb),
623
+ [engine]
624
+ );
625
+ const getSnapshot = useCallback2(() => engine.getState(), [engine]);
626
+ const state = useSyncExternalStore(
627
+ subscribe,
628
+ getSnapshot,
629
+ getSnapshot
630
+ );
631
+ const isArchivedForUser = useCallback2(
632
+ (item) => engine.isArchivedForUser(item),
633
+ [engine]
634
+ );
635
+ const hasUnread = useCallback2(
636
+ (item) => engine.hasUnread(item),
637
+ [engine]
638
+ );
639
+ const hasValidPins = useCallback2(
640
+ (item) => engine.hasValidPins(item),
641
+ [engine]
905
642
  );
906
- useEffect5(() => {
907
- const update = () => setPathname(window.location.pathname);
908
- const origPush = history.pushState.bind(history);
909
- const origReplace = history.replaceState.bind(history);
910
- history.pushState = (...args) => {
911
- origPush(...args);
912
- queueMicrotask(update);
913
- };
914
- history.replaceState = (...args) => {
915
- origReplace(...args);
916
- queueMicrotask(update);
917
- };
918
- window.addEventListener("popstate", update);
919
- return () => {
920
- window.removeEventListener("popstate", update);
921
- history.pushState = origPush;
922
- history.replaceState = origReplace;
923
- };
924
- }, []);
925
- const [comment, setComment] = useState3("");
926
- const [sentiment, setSentiment] = useState3(null);
927
- const [commentError, setCommentError] = useState3(false);
928
- const [submitting, setSubmitting] = useState3(false);
929
- const [feedbackItems, setFeedbackItems] = useState3([]);
930
- const [loading, setLoading] = useState3(true);
931
- const [openThreadId, setOpenThreadId] = useState3(null);
932
- const [replyBody, setReplyBody] = useState3("");
933
- const resetForm = useCallback5(() => {
934
- setComment("");
935
- setSentiment(null);
936
- setCommentError(false);
937
- }, []);
938
- const pinMode = usePinMode({
939
- hotkey,
940
- disabled,
941
- isModalOpen: openThreadId !== null,
942
- onDeactivate: () => {
943
- resetForm();
944
- setOpenThreadId(null);
945
- }
946
- });
947
- const { clearFlushed } = useFeedtackFlush({
948
- pathname,
949
- feedbackItems,
950
- onFlush,
951
- flushIdleMs,
952
- disabled
953
- });
954
- useEffect5(() => {
955
- setLoading(true);
956
- adapter.loadFeedback({ pathname }).then(setFeedbackItems).catch((err) => onError?.(err)).finally(() => setLoading(false));
957
- }, [adapter, onError, pathname]);
958
- const commentRef = () => comment;
959
- const sentimentRef = () => sentiment;
960
- const pinsRef = () => pinMode.pendingPins;
961
- const replyRef = () => replyBody;
962
- const pathRef = () => pathname;
963
- const actions = useFeedtackActions({
964
- adapter,
965
- currentUser,
966
- onError,
967
- getComment: commentRef,
968
- getSentiment: sentimentRef,
969
- getPendingPins: pinsRef,
970
- getReplyBody: replyRef,
971
- getPathname: pathRef,
972
- setCommentError,
973
- setSubmitting,
974
- setFeedbackItems,
975
- setReplyBody,
976
- setOpenThreadId,
977
- deactivatePinMode: pinMode.deactivate,
978
- clearFlushed,
979
- shouldRescope: rescopeRoles ? (role) => rescopeRoles.includes(role) : void 0,
980
- hasFlush: !!onFlush
981
- });
982
- const isArchivedForUser = (item) => item.archives.some((a) => a.archivedBy.id === currentUser.id);
983
- const hasUnread = (item) => item.replies.length > 0;
984
- const hasValidPins = (item) => Array.isArray(item.payload?.pins) && item.payload.pins.length > 0;
985
643
  return {
986
- ...pinMode,
987
- isPinModeActive: pinMode.isActive,
988
- activatePinMode: pinMode.activate,
989
- deactivatePinMode: pinMode.deactivate,
990
- comment,
991
- setComment,
992
- sentiment,
993
- setSentiment,
994
- commentError,
995
- setCommentError,
996
- submitting,
997
- pathname,
998
- feedbackItems,
999
- loading,
1000
- openThreadId,
1001
- setOpenThreadId,
1002
- replyBody,
1003
- setReplyBody,
1004
- ...actions,
644
+ // Pin mode
645
+ isPinModeActive: state.isPinModeActive,
646
+ isActive: state.isPinModeActive,
647
+ activatePinMode: useCallback2(() => engine.activatePinMode(), [engine]),
648
+ activate: useCallback2(() => engine.activatePinMode(), [engine]),
649
+ deactivatePinMode: useCallback2(() => engine.deactivatePinMode(), [engine]),
650
+ deactivate: useCallback2(() => engine.deactivatePinMode(), [engine]),
651
+ pendingPins: state.pendingPins,
652
+ selectedColor: state.selectedColor,
653
+ setSelectedColor: useCallback2(
654
+ (c) => engine.setSelectedColor(c),
655
+ [engine]
656
+ ),
657
+ showForm: state.showForm,
658
+ // Form
659
+ comment: state.comment,
660
+ setComment: useCallback2((v) => engine.setComment(v), [engine]),
661
+ sentiment: state.sentiment,
662
+ setSentiment: useCallback2(
663
+ (v) => engine.setSentiment(v),
664
+ [engine]
665
+ ),
666
+ commentError: state.commentError,
667
+ setCommentError: useCallback2(
668
+ (v) => engine.setCommentError(v),
669
+ [engine]
670
+ ),
671
+ submitting: state.submitting,
672
+ // Feedback
673
+ feedbackItems: state.feedbackItems,
674
+ siteFeedback: state.siteFeedback,
675
+ pageFeedback: state.pageFeedback,
676
+ loading: state.loading,
677
+ pathname: state.pathname,
678
+ // Thread
679
+ openThreadId: state.openThreadId,
680
+ setOpenThreadId: useCallback2(
681
+ (id) => engine.setOpenThreadId(id),
682
+ [engine]
683
+ ),
684
+ replyBody: state.replyBody,
685
+ setReplyBody: useCallback2((v) => engine.setReplyBody(v), [engine]),
686
+ // Modal
687
+ isModalOpen: state.isModalOpen,
688
+ openModal: useCallback2(() => engine.openModal(), [engine]),
689
+ closeModal: useCallback2(() => engine.closeModal(), [engine]),
690
+ composeScope: state.composeScope,
691
+ setComposeScope: useCallback2(
692
+ (s) => engine.setComposeScope(s),
693
+ [engine]
694
+ ),
695
+ // Actions
696
+ handleSubmit: useCallback2(() => engine.handleSubmit(), [engine]),
697
+ handleModalSubmit: useCallback2(() => engine.handleModalSubmit(), [engine]),
698
+ handleReply: useCallback2((id) => engine.handleReply(id), [engine]),
699
+ handleResolve: useCallback2(
700
+ (id) => engine.handleResolve(id),
701
+ [engine]
702
+ ),
703
+ handleArchive: useCallback2(
704
+ (id) => engine.handleArchive(id),
705
+ [engine]
706
+ ),
707
+ // Derived helpers
1005
708
  isArchivedForUser,
1006
709
  hasUnread,
1007
710
  hasValidPins
@@ -1009,7 +712,7 @@ function useFeedtackState({
1009
712
  }
1010
713
 
1011
714
  // src/react/FeedtackProvider.tsx
1012
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
715
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1013
716
  function FeedtackProvider({
1014
717
  children,
1015
718
  adapter,
@@ -1042,7 +745,11 @@ function FeedtackProvider({
1042
745
  const formPos = firstPin ? getAnchoredPosition(firstPin.x, firstPin.y) : {};
1043
746
  const showButton = !adminOnly || currentUser.role === "admin";
1044
747
  const openItem = state.openThreadId ? state.feedbackItems.find((i) => i.payload.id === state.openThreadId) : null;
1045
- return /* @__PURE__ */ jsxs3(
748
+ const handlePlacePin = () => {
749
+ state.closeModal();
750
+ state.activatePinMode();
751
+ };
752
+ return /* @__PURE__ */ jsxs6(
1046
753
  FeedtackContext.Provider,
1047
754
  {
1048
755
  value: {
@@ -1054,31 +761,31 @@ function FeedtackProvider({
1054
761
  selectedColor: state.selectedColor,
1055
762
  setSelectedColor: disabled ? () => {
1056
763
  } : state.setSelectedColor,
1057
- pinPalette: PIN_PALETTE
764
+ pinPalette: PIN_PALETTE,
765
+ openModal: disabled ? () => {
766
+ } : state.openModal,
767
+ closeModal: disabled ? () => {
768
+ } : state.closeModal,
769
+ isModalOpen: disabled ? false : state.isModalOpen
1058
770
  },
1059
771
  children: [
1060
772
  children,
1061
- !disabled && showButton && /* @__PURE__ */ jsxs3(
773
+ !disabled && showButton && /* @__PURE__ */ jsx6(
1062
774
  "button",
1063
775
  {
1064
776
  type: "button",
1065
777
  className: cx(
1066
778
  "feedtack-btn",
1067
- state.isPinModeActive && "active",
779
+ (state.isPinModeActive || state.isModalOpen) && "active",
1068
780
  classes.button
1069
781
  ),
1070
- onClick: () => state.isPinModeActive ? state.deactivatePinMode() : state.activatePinMode(),
1071
- title: "Toggle feedback pin mode",
1072
- "aria-label": "Toggle feedback pin mode",
1073
- "aria-pressed": state.isPinModeActive,
1074
- children: [
1075
- "Drop Pin [Shift+",
1076
- hotkey.toUpperCase(),
1077
- "]"
1078
- ]
782
+ onClick: () => state.openModal(),
783
+ title: "Open feedback",
784
+ "aria-label": "Open feedback",
785
+ children: "Feedback"
1079
786
  }
1080
787
  ),
1081
- state.isPinModeActive && /* @__PURE__ */ jsx3("div", { className: cx("feedtack-color-picker", classes.colorPicker), children: PIN_PALETTE.map((color) => /* @__PURE__ */ jsx3(
788
+ state.isPinModeActive && /* @__PURE__ */ jsx6("div", { className: cx("feedtack-color-picker", classes.colorPicker), children: PIN_PALETTE.map((color) => /* @__PURE__ */ jsx6(
1082
789
  "button",
1083
790
  {
1084
791
  type: "button",
@@ -1092,7 +799,7 @@ function FeedtackProvider({
1092
799
  },
1093
800
  color
1094
801
  )) }),
1095
- state.pendingPins.map((pin) => /* @__PURE__ */ jsx3(
802
+ state.pendingPins.map((pin) => /* @__PURE__ */ jsx6(
1096
803
  "div",
1097
804
  {
1098
805
  className: cx("feedtack-pin-marker", classes.pinMarker),
@@ -1105,7 +812,7 @@ function FeedtackProvider({
1105
812
  },
1106
813
  `${pin.x}-${pin.y}-${pin.color}`
1107
814
  )),
1108
- state.showForm && /* @__PURE__ */ jsx3(
815
+ state.showForm && /* @__PURE__ */ jsx6(
1109
816
  CommentForm,
1110
817
  {
1111
818
  comment: state.comment,
@@ -1124,45 +831,22 @@ function FeedtackProvider({
1124
831
  onCancel: state.deactivatePinMode
1125
832
  }
1126
833
  ),
1127
- !state.loading && state.feedbackItems.filter((item) => item.payload.page.pathname === state.pathname).filter((item) => !state.isArchivedForUser(item)).filter((item) => state.hasValidPins(item)).map((item) => {
1128
- const pin = item.payload.pins[0];
1129
- const pos = getPosition(item.payload.id, pin);
1130
- return /* @__PURE__ */ jsxs3(
1131
- "button",
1132
- {
1133
- type: "button",
1134
- className: cx(
1135
- "feedtack-pin-marker",
1136
- item.resolutions.length > 0 && "feedtack-pin-resolved",
1137
- classes.pinMarker
1138
- ),
1139
- style: {
1140
- background: pin.color,
1141
- left: pos.x,
1142
- top: pos.y,
1143
- position: "absolute",
1144
- cursor: "pointer"
1145
- },
1146
- onClick: () => state.setOpenThreadId(
1147
- state.openThreadId === item.payload.id ? null : item.payload.id
1148
- ),
1149
- children: [
1150
- renderPinIcon ? /* @__PURE__ */ jsx3("span", { className: "feedtack-pin-icon", children: renderPinIcon(item) }) : item.resolutions.length > 0 && /* @__PURE__ */ jsx3(
1151
- "span",
1152
- {
1153
- className: "feedtack-pin-icon",
1154
- role: "img",
1155
- "aria-label": "Resolved",
1156
- children: "\u2713"
1157
- }
1158
- ),
1159
- state.hasUnread(item) && /* @__PURE__ */ jsx3("div", { className: "feedtack-pin-badge" })
1160
- ]
1161
- },
1162
- item.payload.id
1163
- );
1164
- }),
1165
- openItem && /* @__PURE__ */ jsx3(
834
+ !state.loading && /* @__PURE__ */ jsx6(
835
+ PinOverlay,
836
+ {
837
+ feedbackItems: state.feedbackItems,
838
+ pathname: state.pathname,
839
+ isArchivedForUser: state.isArchivedForUser,
840
+ hasValidPins: state.hasValidPins,
841
+ hasUnread: state.hasUnread,
842
+ openThreadId: state.openThreadId,
843
+ setOpenThreadId: state.setOpenThreadId,
844
+ getPosition,
845
+ renderPinIcon,
846
+ pinMarkerClass: classes.pinMarker
847
+ }
848
+ ),
849
+ openItem && /* @__PURE__ */ jsx6(
1166
850
  ThreadPanel,
1167
851
  {
1168
852
  item: openItem,
@@ -1179,7 +863,36 @@ function FeedtackProvider({
1179
863
  className: classes.thread
1180
864
  }
1181
865
  ),
1182
- state.loading && /* @__PURE__ */ jsx3("div", { className: "feedtack-loading", children: "Loading feedback\u2026" })
866
+ !disabled && /* @__PURE__ */ jsx6(
867
+ FeedbackModal,
868
+ {
869
+ isOpen: state.isModalOpen,
870
+ onClose: state.closeModal,
871
+ activeTab: state.composeScope,
872
+ onTabChange: state.setComposeScope,
873
+ siteFeedback: state.siteFeedback,
874
+ pageFeedback: state.pageFeedback,
875
+ comment: state.comment,
876
+ onCommentChange: (v) => {
877
+ state.setComment(v);
878
+ state.setCommentError(false);
879
+ },
880
+ commentError: state.commentError,
881
+ sentiment: state.sentiment,
882
+ onSentimentChange: state.setSentiment,
883
+ submitting: state.submitting,
884
+ onSubmit: state.handleModalSubmit,
885
+ onPlacePin: handlePlacePin,
886
+ replyBody: state.replyBody,
887
+ onReplyBodyChange: state.setReplyBody,
888
+ onReply: (id) => state.handleReply(id),
889
+ onResolve: (id) => state.handleResolve(id),
890
+ onArchive: (id) => state.handleArchive(id),
891
+ openThreadId: state.openThreadId,
892
+ onOpenThread: state.setOpenThreadId
893
+ }
894
+ ),
895
+ state.loading && /* @__PURE__ */ jsx6("div", { className: "feedtack-loading", children: "Loading feedback\u2026" })
1183
896
  ]
1184
897
  }
1185
898
  );