feedtack 0.5.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,7 @@ import {
6
6
  getTargetMeta,
7
7
  getViewportMeta,
8
8
  themeToCSS
9
- } from "../chunk-NCW2V5JL.js";
9
+ } from "../chunk-2A5LLDLP.js";
10
10
 
11
11
  // src/ui/colors.ts
12
12
  var PIN_PALETTE = [
@@ -92,20 +92,18 @@ function CommentForm({
92
92
  "button",
93
93
  {
94
94
  type: "button",
95
- className: sentiment === "satisfied" ? "selected" : "",
96
- onClick: () => onSentimentChange(sentiment === "satisfied" ? null : "satisfied"),
97
- children: sentimentLabels.satisfied ?? "\u{1F60A} Satisfied"
95
+ className: sentiment === "good" ? "selected" : "",
96
+ onClick: () => onSentimentChange(sentiment === "good" ? null : "good"),
97
+ children: sentimentLabels.satisfied ?? "Good"
98
98
  }
99
99
  ),
100
100
  /* @__PURE__ */ jsx(
101
101
  "button",
102
102
  {
103
103
  type: "button",
104
- className: sentiment === "dissatisfied" ? "selected" : "",
105
- onClick: () => onSentimentChange(
106
- sentiment === "dissatisfied" ? null : "dissatisfied"
107
- ),
108
- children: sentimentLabels.dissatisfied ?? "\u{1F61E} Dissatisfied"
104
+ className: sentiment === "bad" ? "selected" : "",
105
+ onClick: () => onSentimentChange(sentiment === "bad" ? null : "bad"),
106
+ children: sentimentLabels.dissatisfied ?? "Bad"
109
107
  }
110
108
  )
111
109
  ] }),
@@ -145,8 +143,311 @@ function useFeedtackContext() {
145
143
  return ctx;
146
144
  }
147
145
 
148
- // src/react/ThreadPanel.tsx
146
+ // src/react/FeedbackModal.tsx
147
+ import { useEffect, useRef } from "react";
148
+
149
+ // src/react/ThreadView.tsx
149
150
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
151
+ function ThreadView({
152
+ item,
153
+ replyBody,
154
+ onReplyBodyChange,
155
+ onReply,
156
+ onResolve,
157
+ onArchive,
158
+ onBack
159
+ }) {
160
+ return /* @__PURE__ */ jsxs2("div", { className: "feedtack-modal-thread-view", children: [
161
+ /* @__PURE__ */ jsx2("button", { type: "button", className: "feedtack-modal-back", onClick: onBack, children: "\u2190 Back" }),
162
+ /* @__PURE__ */ jsxs2("div", { className: "feedtack-modal-thread-content", children: [
163
+ /* @__PURE__ */ jsx2("strong", { children: item.payload.submittedBy.name }),
164
+ /* @__PURE__ */ jsx2("p", { children: item.payload.comment })
165
+ ] }),
166
+ item.replies.map((r) => /* @__PURE__ */ jsxs2("div", { className: "feedtack-modal-reply", children: [
167
+ /* @__PURE__ */ jsx2("span", { className: "feedtack-reply-author", children: r.author.name }),
168
+ /* @__PURE__ */ jsx2("p", { children: r.body })
169
+ ] }, r.id)),
170
+ /* @__PURE__ */ jsx2(
171
+ "textarea",
172
+ {
173
+ className: "feedtack-modal-textarea",
174
+ placeholder: "Reply\u2026",
175
+ value: replyBody,
176
+ onChange: (e) => onReplyBodyChange(e.target.value)
177
+ }
178
+ ),
179
+ /* @__PURE__ */ jsxs2("div", { className: "feedtack-modal-actions", children: [
180
+ /* @__PURE__ */ jsx2("button", { type: "button", className: "feedtack-btn-submit", onClick: onReply, children: "Reply" }),
181
+ /* @__PURE__ */ jsx2(
182
+ "button",
183
+ {
184
+ type: "button",
185
+ className: "feedtack-btn-cancel",
186
+ onClick: onResolve,
187
+ children: "Resolve"
188
+ }
189
+ ),
190
+ /* @__PURE__ */ jsx2(
191
+ "button",
192
+ {
193
+ type: "button",
194
+ className: "feedtack-btn-cancel",
195
+ onClick: onArchive,
196
+ children: "Archive"
197
+ }
198
+ )
199
+ ] })
200
+ ] });
201
+ }
202
+
203
+ // src/react/FeedbackModal.tsx
204
+ import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
205
+ function FeedbackModal({
206
+ isOpen,
207
+ onClose,
208
+ activeTab,
209
+ onTabChange,
210
+ siteFeedback,
211
+ pageFeedback,
212
+ comment,
213
+ onCommentChange,
214
+ commentError,
215
+ sentiment,
216
+ onSentimentChange,
217
+ submitting,
218
+ onSubmit,
219
+ onPlacePin,
220
+ replyBody,
221
+ onReplyBodyChange,
222
+ onReply,
223
+ onResolve,
224
+ onArchive,
225
+ openThreadId,
226
+ onOpenThread
227
+ }) {
228
+ const panelRef = useRef(null);
229
+ useEffect(() => {
230
+ if (!isOpen) return;
231
+ const onKey = (e) => {
232
+ if (e.key === "Escape") onClose();
233
+ };
234
+ const onDown = (e) => {
235
+ if (panelRef.current && !panelRef.current.contains(e.target)) {
236
+ const btn = document.querySelector(".feedtack-btn");
237
+ if (btn?.contains(e.target)) return;
238
+ onClose();
239
+ }
240
+ };
241
+ window.addEventListener("keydown", onKey);
242
+ document.addEventListener("mousedown", onDown);
243
+ return () => {
244
+ window.removeEventListener("keydown", onKey);
245
+ document.removeEventListener("mousedown", onDown);
246
+ };
247
+ }, [isOpen, onClose]);
248
+ if (!isOpen) return null;
249
+ const threads = activeTab === "site" ? siteFeedback : pageFeedback;
250
+ const openItem = openThreadId ? threads.find((i) => i.payload.id === openThreadId) : null;
251
+ return /* @__PURE__ */ jsxs3(
252
+ "div",
253
+ {
254
+ ref: panelRef,
255
+ className: "feedtack-modal",
256
+ role: "dialog",
257
+ "aria-label": "Feedback",
258
+ "aria-modal": "true",
259
+ children: [
260
+ /* @__PURE__ */ jsxs3("div", { className: "feedtack-modal-header", children: [
261
+ /* @__PURE__ */ jsx3("span", { className: "feedtack-modal-title", children: "Feedback" }),
262
+ /* @__PURE__ */ jsx3(
263
+ "button",
264
+ {
265
+ type: "button",
266
+ className: "feedtack-modal-close",
267
+ onClick: onClose,
268
+ "aria-label": "Close",
269
+ children: "\xD7"
270
+ }
271
+ )
272
+ ] }),
273
+ /* @__PURE__ */ jsxs3("div", { className: "feedtack-modal-tabs", children: [
274
+ /* @__PURE__ */ jsxs3(
275
+ "button",
276
+ {
277
+ type: "button",
278
+ className: cx("feedtack-modal-tab", activeTab === "site" && "active"),
279
+ onClick: () => onTabChange("site"),
280
+ children: [
281
+ "Site",
282
+ siteFeedback.length > 0 && /* @__PURE__ */ jsx3("span", { className: "feedtack-tab-count", children: siteFeedback.length })
283
+ ]
284
+ }
285
+ ),
286
+ /* @__PURE__ */ jsxs3(
287
+ "button",
288
+ {
289
+ type: "button",
290
+ className: cx("feedtack-modal-tab", activeTab === "page" && "active"),
291
+ onClick: () => onTabChange("page"),
292
+ children: [
293
+ "Page",
294
+ pageFeedback.length > 0 && /* @__PURE__ */ jsx3("span", { className: "feedtack-tab-count", children: pageFeedback.length })
295
+ ]
296
+ }
297
+ )
298
+ ] }),
299
+ /* @__PURE__ */ jsx3("div", { className: "feedtack-modal-body", children: openItem ? /* @__PURE__ */ jsx3(
300
+ ThreadView,
301
+ {
302
+ item: openItem,
303
+ replyBody,
304
+ onReplyBodyChange,
305
+ onReply: () => onReply(openItem.payload.id),
306
+ onResolve: () => onResolve(openItem.payload.id),
307
+ onArchive: () => onArchive(openItem.payload.id),
308
+ onBack: () => onOpenThread(null)
309
+ }
310
+ ) : /* @__PURE__ */ jsxs3(Fragment, { children: [
311
+ threads.length > 0 && /* @__PURE__ */ jsx3("div", { className: "feedtack-modal-threads", children: threads.map((item) => /* @__PURE__ */ jsxs3(
312
+ "button",
313
+ {
314
+ type: "button",
315
+ className: "feedtack-modal-thread-item",
316
+ onClick: () => onOpenThread(item.payload.id),
317
+ children: [
318
+ /* @__PURE__ */ jsx3("span", { className: "feedtack-thread-author", children: item.payload.submittedBy.name }),
319
+ /* @__PURE__ */ jsx3("span", { className: "feedtack-thread-comment", children: item.payload.comment }),
320
+ /* @__PURE__ */ jsxs3("span", { className: "feedtack-thread-meta", children: [
321
+ item.replies.length > 0 && `${item.replies.length} ${item.replies.length === 1 ? "reply" : "replies"}`,
322
+ item.resolutions.length > 0 && " \xB7 resolved"
323
+ ] })
324
+ ]
325
+ },
326
+ item.payload.id
327
+ )) }),
328
+ /* @__PURE__ */ jsxs3("div", { className: "feedtack-modal-compose", children: [
329
+ /* @__PURE__ */ jsx3(
330
+ "textarea",
331
+ {
332
+ className: cx(
333
+ "feedtack-modal-textarea",
334
+ commentError && "error"
335
+ ),
336
+ placeholder: "What's on your mind? (required)",
337
+ value: comment,
338
+ onChange: (e) => onCommentChange(e.target.value),
339
+ onKeyDown: (e) => {
340
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
341
+ e.preventDefault();
342
+ onSubmit();
343
+ }
344
+ },
345
+ "aria-invalid": commentError || void 0
346
+ }
347
+ ),
348
+ commentError && /* @__PURE__ */ jsx3("span", { className: "feedtack-error-msg", children: "Comment is required" }),
349
+ /* @__PURE__ */ jsxs3("div", { className: "feedtack-sentiment", children: [
350
+ /* @__PURE__ */ jsx3(
351
+ "button",
352
+ {
353
+ type: "button",
354
+ className: sentiment === "good" ? "selected" : "",
355
+ onClick: () => onSentimentChange(sentiment === "good" ? null : "good"),
356
+ children: "Good"
357
+ }
358
+ ),
359
+ /* @__PURE__ */ jsx3(
360
+ "button",
361
+ {
362
+ type: "button",
363
+ className: sentiment === "bad" ? "selected" : "",
364
+ onClick: () => onSentimentChange(sentiment === "bad" ? null : "bad"),
365
+ children: "Bad"
366
+ }
367
+ )
368
+ ] }),
369
+ /* @__PURE__ */ jsx3(
370
+ "button",
371
+ {
372
+ type: "button",
373
+ className: "feedtack-btn-submit",
374
+ onClick: onSubmit,
375
+ disabled: submitting,
376
+ children: submitting ? "Sending\u2026" : "Submit"
377
+ }
378
+ )
379
+ ] })
380
+ ] }) }),
381
+ /* @__PURE__ */ jsx3("div", { className: "feedtack-modal-footer", children: /* @__PURE__ */ jsx3(
382
+ "button",
383
+ {
384
+ type: "button",
385
+ className: "feedtack-modal-pin-btn",
386
+ onClick: onPlacePin,
387
+ children: "Place a pin"
388
+ }
389
+ ) })
390
+ ]
391
+ }
392
+ );
393
+ }
394
+
395
+ // src/react/PinOverlay.tsx
396
+ import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
397
+ function PinOverlay({
398
+ feedbackItems,
399
+ pathname,
400
+ isArchivedForUser,
401
+ hasValidPins,
402
+ hasUnread,
403
+ openThreadId,
404
+ setOpenThreadId,
405
+ getPosition,
406
+ renderPinIcon,
407
+ pinMarkerClass
408
+ }) {
409
+ return /* @__PURE__ */ jsx4(Fragment2, { children: feedbackItems.filter((item) => item.payload.page.pathname === pathname).filter((item) => !isArchivedForUser(item)).filter((item) => hasValidPins(item)).map((item) => {
410
+ const pin = item.payload.pins[0];
411
+ const pos = getPosition(item.payload.id, pin);
412
+ return /* @__PURE__ */ jsxs4(
413
+ "button",
414
+ {
415
+ type: "button",
416
+ className: cx(
417
+ "feedtack-pin-marker",
418
+ item.resolutions.length > 0 && "feedtack-pin-resolved",
419
+ pinMarkerClass
420
+ ),
421
+ style: {
422
+ background: pin.color,
423
+ left: pos.x,
424
+ top: pos.y,
425
+ position: "absolute",
426
+ cursor: "pointer"
427
+ },
428
+ onClick: () => setOpenThreadId(
429
+ openThreadId === item.payload.id ? null : item.payload.id
430
+ ),
431
+ children: [
432
+ renderPinIcon ? /* @__PURE__ */ jsx4("span", { className: "feedtack-pin-icon", children: renderPinIcon(item) }) : item.resolutions.length > 0 && /* @__PURE__ */ jsx4(
433
+ "span",
434
+ {
435
+ className: "feedtack-pin-icon",
436
+ role: "img",
437
+ "aria-label": "Resolved",
438
+ children: "\u2713"
439
+ }
440
+ ),
441
+ hasUnread(item) && /* @__PURE__ */ jsx4("div", { className: "feedtack-pin-badge" })
442
+ ]
443
+ },
444
+ item.payload.id
445
+ );
446
+ }) });
447
+ }
448
+
449
+ // src/react/ThreadPanel.tsx
450
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
150
451
  function ThreadPanel({
151
452
  item,
152
453
  replyBody,
@@ -162,26 +463,26 @@ function ThreadPanel({
162
463
  if (!pin) return null;
163
464
  const { x, y } = pinPosition ?? pin;
164
465
  const pos = getAnchoredPosition(x, y);
165
- return /* @__PURE__ */ jsxs2(
466
+ return /* @__PURE__ */ jsxs5(
166
467
  "div",
167
468
  {
168
469
  className: cx("feedtack-thread", className),
169
470
  style: { position: "fixed", ...pos },
170
471
  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(
472
+ /* @__PURE__ */ jsx5("strong", { style: { fontSize: 13 }, children: item.payload.submittedBy.name }),
473
+ /* @__PURE__ */ jsx5("p", { style: { fontSize: 13 }, children: item.payload.comment }),
474
+ item.replies.map((r) => /* @__PURE__ */ jsxs5(
174
475
  "div",
175
476
  {
176
477
  style: { borderTop: "1px solid var(--ft-border)", paddingTop: 8 },
177
478
  children: [
178
- /* @__PURE__ */ jsx2("span", { style: { fontSize: 12, fontWeight: 600 }, children: r.author.name }),
179
- /* @__PURE__ */ jsx2("p", { style: { fontSize: 12 }, children: r.body })
479
+ /* @__PURE__ */ jsx5("span", { style: { fontSize: 12, fontWeight: 600 }, children: r.author.name }),
480
+ /* @__PURE__ */ jsx5("p", { style: { fontSize: 12 }, children: r.body })
180
481
  ]
181
482
  },
182
483
  r.id
183
484
  )),
184
- /* @__PURE__ */ jsx2(
485
+ /* @__PURE__ */ jsx5(
185
486
  "textarea",
186
487
  {
187
488
  placeholder: "Reply\u2026",
@@ -201,8 +502,8 @@ function ThreadPanel({
201
502
  }
202
503
  }
203
504
  ),
204
- /* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: 6, flexWrap: "wrap" }, children: [
205
- /* @__PURE__ */ jsx2(
505
+ /* @__PURE__ */ jsxs5("div", { style: { display: "flex", gap: 6, flexWrap: "wrap" }, children: [
506
+ /* @__PURE__ */ jsx5(
206
507
  "button",
207
508
  {
208
509
  type: "button",
@@ -212,7 +513,7 @@ function ThreadPanel({
212
513
  children: "Reply"
213
514
  }
214
515
  ),
215
- /* @__PURE__ */ jsx2(
516
+ /* @__PURE__ */ jsx5(
216
517
  "button",
217
518
  {
218
519
  type: "button",
@@ -222,7 +523,7 @@ function ThreadPanel({
222
523
  children: "Mark Resolved"
223
524
  }
224
525
  ),
225
- /* @__PURE__ */ jsx2(
526
+ /* @__PURE__ */ jsx5(
226
527
  "button",
227
528
  {
228
529
  type: "button",
@@ -232,7 +533,7 @@ function ThreadPanel({
232
533
  children: "Archive"
233
534
  }
234
535
  ),
235
- /* @__PURE__ */ jsx2(
536
+ /* @__PURE__ */ jsx5(
236
537
  "button",
237
538
  {
238
539
  type: "button",
@@ -249,7 +550,7 @@ function ThreadPanel({
249
550
  }
250
551
 
251
552
  // src/react/useAnchoredPins.ts
252
- import { useCallback, useEffect, useState } from "react";
553
+ import { useCallback, useEffect as useEffect2, useState } from "react";
253
554
  function useAnchoredPins(items, pathname) {
254
555
  const [positions, setPositions] = useState(
255
556
  /* @__PURE__ */ new Map()
@@ -265,10 +566,10 @@ function useAnchoredPins(items, pathname) {
265
566
  }
266
567
  setPositions(next);
267
568
  }, [items, pathname]);
268
- useEffect(() => {
569
+ useEffect2(() => {
269
570
  resolve();
270
571
  }, [resolve]);
271
- useEffect(() => {
572
+ useEffect2(() => {
272
573
  let raf = 0;
273
574
  const handler = () => {
274
575
  cancelAnimationFrame(raf);
@@ -310,7 +611,7 @@ function resolvePin(pin) {
310
611
  }
311
612
 
312
613
  // src/react/useFeedtackState.ts
313
- import { useCallback as useCallback5, useEffect as useEffect5, useState as useState3 } from "react";
614
+ import { useCallback as useCallback5, useEffect as useEffect6, useState as useState3 } from "react";
314
615
 
315
616
  // src/react/useFeedtackActions.ts
316
617
  import { useCallback as useCallback2 } from "react";
@@ -333,6 +634,7 @@ function useFeedtackActions(deps) {
333
634
  schemaVersion: SCHEMA_VERSION,
334
635
  id: generateId(),
335
636
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
637
+ scope: deps.getScope(),
336
638
  submittedBy: currentUser,
337
639
  comment: comment.trim(),
338
640
  sentiment: deps.getSentiment(),
@@ -436,11 +738,251 @@ function useFeedtackActions(deps) {
436
738
  }
437
739
 
438
740
  // src/react/useFeedtackDom.ts
439
- import { useEffect as useEffect2, useRef } from "react";
741
+ import { useEffect as useEffect3, useRef as useRef2 } from "react";
742
+
743
+ // src/ui/modalStyles.ts
744
+ var FEEDTACK_MODAL_STYLES = `
745
+ .feedtack-loading {
746
+ position: fixed;
747
+ bottom: 70px;
748
+ right: 24px;
749
+ font-size: 12px;
750
+ color: var(--ft-text-muted);
751
+ z-index: 2147483640;
752
+ }
753
+
754
+ .feedtack-modal {
755
+ position: fixed;
756
+ bottom: 72px;
757
+ right: 24px;
758
+ width: 360px;
759
+ max-height: 70vh;
760
+ background: var(--ft-bg);
761
+ border: 1px solid var(--ft-border);
762
+ border-radius: calc(var(--ft-radius) + 4px);
763
+ box-shadow: 0 8px 32px rgba(0,0,0,0.18);
764
+ z-index: 2147483643;
765
+ display: flex;
766
+ flex-direction: column;
767
+ overflow: hidden;
768
+ }
769
+
770
+ .feedtack-modal-header {
771
+ display: flex;
772
+ align-items: center;
773
+ justify-content: space-between;
774
+ padding: 14px 16px 0;
775
+ }
776
+
777
+ .feedtack-modal-title {
778
+ font-size: 15px;
779
+ font-weight: 600;
780
+ color: var(--ft-text);
781
+ }
782
+
783
+ .feedtack-modal-close {
784
+ background: none;
785
+ border: none;
786
+ font-size: 20px;
787
+ cursor: pointer;
788
+ color: var(--ft-text-muted);
789
+ line-height: 1;
790
+ padding: 0 4px;
791
+ }
792
+
793
+ .feedtack-modal-tabs {
794
+ display: flex;
795
+ gap: 0;
796
+ padding: 12px 16px 0;
797
+ border-bottom: 1px solid var(--ft-border);
798
+ }
799
+
800
+ .feedtack-modal-tab {
801
+ flex: 1;
802
+ padding: 8px 12px;
803
+ border: none;
804
+ background: none;
805
+ font-size: 13px;
806
+ font-weight: 500;
807
+ cursor: pointer;
808
+ color: var(--ft-text-muted);
809
+ border-bottom: 2px solid transparent;
810
+ margin-bottom: -1px;
811
+ display: flex;
812
+ align-items: center;
813
+ justify-content: center;
814
+ gap: 6px;
815
+ }
816
+
817
+ .feedtack-modal-tab.active {
818
+ color: var(--ft-primary);
819
+ border-bottom-color: var(--ft-primary);
820
+ }
821
+
822
+ .feedtack-tab-count {
823
+ font-size: 11px;
824
+ background: var(--ft-surface);
825
+ color: var(--ft-text-muted);
826
+ padding: 1px 6px;
827
+ border-radius: 10px;
828
+ }
829
+
830
+ .feedtack-modal-body {
831
+ flex: 1;
832
+ overflow-y: auto;
833
+ padding: 12px 16px;
834
+ display: flex;
835
+ flex-direction: column;
836
+ gap: 12px;
837
+ }
838
+
839
+ .feedtack-modal-threads {
840
+ display: flex;
841
+ flex-direction: column;
842
+ gap: 6px;
843
+ }
844
+
845
+ .feedtack-modal-thread-item {
846
+ display: flex;
847
+ flex-direction: column;
848
+ gap: 2px;
849
+ text-align: left;
850
+ padding: 10px 12px;
851
+ background: var(--ft-surface);
852
+ border: 1px solid var(--ft-border);
853
+ border-radius: var(--ft-radius);
854
+ cursor: pointer;
855
+ }
856
+
857
+ .feedtack-modal-thread-item:hover {
858
+ border-color: var(--ft-primary);
859
+ }
860
+
861
+ .feedtack-thread-author {
862
+ font-size: 12px;
863
+ font-weight: 600;
864
+ color: var(--ft-text);
865
+ }
866
+
867
+ .feedtack-thread-comment {
868
+ font-size: 13px;
869
+ color: var(--ft-text);
870
+ overflow: hidden;
871
+ text-overflow: ellipsis;
872
+ white-space: nowrap;
873
+ }
874
+
875
+ .feedtack-thread-meta {
876
+ font-size: 11px;
877
+ color: var(--ft-text-muted);
878
+ }
879
+
880
+ .feedtack-modal-compose {
881
+ display: flex;
882
+ flex-direction: column;
883
+ gap: 8px;
884
+ }
885
+
886
+ .feedtack-modal-textarea {
887
+ width: 100%;
888
+ border: 1.5px solid var(--ft-border);
889
+ border-radius: var(--ft-radius);
890
+ padding: 8px;
891
+ font-size: 13px;
892
+ resize: vertical;
893
+ min-height: 72px;
894
+ outline: none;
895
+ background: var(--ft-surface);
896
+ color: var(--ft-text);
897
+ }
898
+
899
+ .feedtack-modal-textarea:focus {
900
+ border-color: var(--ft-primary);
901
+ }
902
+
903
+ .feedtack-modal-textarea.error {
904
+ border-color: var(--ft-error);
905
+ }
906
+
907
+ .feedtack-modal-footer {
908
+ padding: 10px 16px 14px;
909
+ border-top: 1px solid var(--ft-border);
910
+ }
911
+
912
+ .feedtack-modal-pin-btn {
913
+ width: 100%;
914
+ padding: 8px 14px;
915
+ border: 1.5px solid var(--ft-border);
916
+ border-radius: var(--ft-radius);
917
+ background: var(--ft-bg);
918
+ color: var(--ft-text);
919
+ font-size: 13px;
920
+ font-weight: 500;
921
+ cursor: pointer;
922
+ transition: border-color 0.15s;
923
+ }
924
+
925
+ .feedtack-modal-pin-btn:hover {
926
+ border-color: var(--ft-primary);
927
+ color: var(--ft-primary);
928
+ }
929
+
930
+ .feedtack-modal-thread-view {
931
+ display: flex;
932
+ flex-direction: column;
933
+ gap: 10px;
934
+ }
935
+
936
+ .feedtack-modal-back {
937
+ background: none;
938
+ border: none;
939
+ font-size: 13px;
940
+ color: var(--ft-primary);
941
+ cursor: pointer;
942
+ padding: 0;
943
+ text-align: left;
944
+ }
945
+
946
+ .feedtack-modal-thread-content {
947
+ display: flex;
948
+ flex-direction: column;
949
+ gap: 4px;
950
+ font-size: 13px;
951
+ }
952
+
953
+ .feedtack-modal-reply {
954
+ border-top: 1px solid var(--ft-border);
955
+ padding-top: 8px;
956
+ font-size: 12px;
957
+ }
958
+
959
+ .feedtack-reply-author {
960
+ font-weight: 600;
961
+ }
962
+
963
+ .feedtack-modal-actions {
964
+ display: flex;
965
+ gap: 6px;
966
+ flex-wrap: wrap;
967
+ }
968
+
969
+ @media (max-width: 480px) {
970
+ .feedtack-modal {
971
+ right: 0;
972
+ bottom: 64px;
973
+ width: 100vw;
974
+ max-height: 85vh;
975
+ border-radius: var(--ft-radius) var(--ft-radius) 0 0;
976
+ border-left: none;
977
+ border-right: none;
978
+ border-bottom: none;
979
+ }
980
+ }
981
+ `;
440
982
 
441
983
  // src/ui/styles.ts
442
984
  var FEEDTACK_DEFAULT_TOKENS = `
443
- #feedtack-root, .feedtack-form, .feedtack-thread {
985
+ #feedtack-root, .feedtack-form, .feedtack-thread, .feedtack-modal {
444
986
  --ft-primary: #2563eb;
445
987
  --ft-primary-hover: #1d4ed8;
446
988
  --ft-bg: #ffffff;
@@ -676,20 +1218,12 @@ var FEEDTACK_STYLES = `
676
1218
  overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
677
1219
  }
678
1220
 
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
- `;
1221
+ ` + FEEDTACK_MODAL_STYLES;
688
1222
 
689
1223
  // src/react/useFeedtackDom.ts
690
1224
  function useFeedtackDom(theme, disabled) {
691
- const rootRef = useRef(null);
692
- useEffect2(() => {
1225
+ const rootRef = useRef2(null);
1226
+ useEffect3(() => {
693
1227
  if (disabled) return;
694
1228
  if (document.getElementById("feedtack-styles")) return;
695
1229
  const style = document.createElement("style");
@@ -700,7 +1234,7 @@ function useFeedtackDom(theme, disabled) {
700
1234
  style.remove();
701
1235
  };
702
1236
  }, [disabled]);
703
- useEffect2(() => {
1237
+ useEffect3(() => {
704
1238
  if (disabled) return;
705
1239
  const root = document.createElement("div");
706
1240
  root.id = "feedtack-root";
@@ -710,7 +1244,7 @@ function useFeedtackDom(theme, disabled) {
710
1244
  root.remove();
711
1245
  };
712
1246
  }, [disabled]);
713
- useEffect2(() => {
1247
+ useEffect3(() => {
714
1248
  if (disabled) return;
715
1249
  const root = document.getElementById("feedtack-root");
716
1250
  if (!root || !theme) return;
@@ -723,7 +1257,7 @@ function useFeedtackDom(theme, disabled) {
723
1257
  }
724
1258
 
725
1259
  // src/react/useFeedtackFlush.ts
726
- import { useCallback as useCallback3, useEffect as useEffect3, useRef as useRef2 } from "react";
1260
+ import { useCallback as useCallback3, useEffect as useEffect4, useRef as useRef3 } from "react";
727
1261
  var DEFAULT_IDLE_MS = 5 * 60 * 1e3;
728
1262
  function useFeedtackFlush({
729
1263
  pathname,
@@ -732,9 +1266,9 @@ function useFeedtackFlush({
732
1266
  flushIdleMs = DEFAULT_IDLE_MS,
733
1267
  disabled
734
1268
  }) {
735
- const flushedRef = useRef2(/* @__PURE__ */ new Set());
736
- const prevPathnameRef = useRef2(pathname);
737
- const idleTimerRef = useRef2(null);
1269
+ const flushedRef = useRef3(/* @__PURE__ */ new Set());
1270
+ const prevPathnameRef = useRef3(pathname);
1271
+ const idleTimerRef = useRef3(null);
738
1272
  const flush = useCallback3(
739
1273
  (path, items) => {
740
1274
  if (!onFlush || flushedRef.current.has(path)) return;
@@ -745,7 +1279,7 @@ function useFeedtackFlush({
745
1279
  },
746
1280
  [onFlush]
747
1281
  );
748
- useEffect3(() => {
1282
+ useEffect4(() => {
749
1283
  if (disabled || !onFlush) return;
750
1284
  const prev = prevPathnameRef.current;
751
1285
  prevPathnameRef.current = pathname;
@@ -753,7 +1287,7 @@ function useFeedtackFlush({
753
1287
  flush(prev, feedbackItems);
754
1288
  }
755
1289
  }, [pathname, feedbackItems, flush, onFlush, disabled]);
756
- useEffect3(() => {
1290
+ useEffect4(() => {
757
1291
  if (disabled || !onFlush || flushIdleMs <= 0) return;
758
1292
  const resetTimer = () => {
759
1293
  if (idleTimerRef.current) clearTimeout(idleTimerRef.current);
@@ -770,7 +1304,7 @@ function useFeedtackFlush({
770
1304
  for (const e of events) window.removeEventListener(e, resetTimer);
771
1305
  };
772
1306
  }, [pathname, feedbackItems, flush, onFlush, flushIdleMs, disabled]);
773
- useEffect3(() => {
1307
+ useEffect4(() => {
774
1308
  if (disabled || !onFlush) return;
775
1309
  const handleUnload = () => flush(pathname, feedbackItems);
776
1310
  window.addEventListener("beforeunload", handleUnload);
@@ -783,12 +1317,13 @@ function useFeedtackFlush({
783
1317
  }
784
1318
 
785
1319
  // src/react/usePinMode.ts
786
- import { useCallback as useCallback4, useEffect as useEffect4, useState as useState2 } from "react";
1320
+ import { useCallback as useCallback4, useEffect as useEffect5, useState as useState2 } from "react";
787
1321
  function usePinMode({
788
1322
  hotkey,
789
1323
  onDeactivate,
790
1324
  disabled,
791
- isModalOpen
1325
+ isModalOpen,
1326
+ onHotkey
792
1327
  }) {
793
1328
  const [isActive, setIsActive] = useState2(false);
794
1329
  const [pendingPins, setPendingPins] = useState2([]);
@@ -801,7 +1336,7 @@ function usePinMode({
801
1336
  setShowForm(false);
802
1337
  onDeactivate?.();
803
1338
  }, [onDeactivate]);
804
- useEffect4(() => {
1339
+ useEffect5(() => {
805
1340
  if (isActive) {
806
1341
  document.documentElement.classList.add("feedtack-crosshair");
807
1342
  } else {
@@ -809,11 +1344,15 @@ function usePinMode({
809
1344
  }
810
1345
  return () => document.documentElement.classList.remove("feedtack-crosshair");
811
1346
  }, [isActive]);
812
- useEffect4(() => {
1347
+ useEffect5(() => {
813
1348
  if (disabled) return;
814
1349
  const handler = (e) => {
815
1350
  if (e.key === hotkey.toUpperCase() && e.shiftKey) {
816
- setIsActive((prev) => !prev);
1351
+ if (onHotkey) {
1352
+ onHotkey();
1353
+ } else {
1354
+ setIsActive((prev) => !prev);
1355
+ }
817
1356
  }
818
1357
  if (e.key === "Escape") {
819
1358
  deactivate();
@@ -829,7 +1368,7 @@ function usePinMode({
829
1368
  };
830
1369
  window.addEventListener("keydown", handler);
831
1370
  return () => window.removeEventListener("keydown", handler);
832
- }, [hotkey, deactivate, isActive, disabled, isModalOpen, showForm]);
1371
+ }, [hotkey, deactivate, isActive, disabled, isModalOpen, showForm, onHotkey]);
833
1372
  const placePin = useCallback4(
834
1373
  (coords, target) => {
835
1374
  if (target.closest("#feedtack-root, .feedtack-form, .feedtack-color-picker"))
@@ -867,7 +1406,7 @@ function usePinMode({
867
1406
  },
868
1407
  [isActive, placePin]
869
1408
  );
870
- useEffect4(() => {
1409
+ useEffect5(() => {
871
1410
  if (disabled) return;
872
1411
  document.addEventListener("click", handlePageClick, true);
873
1412
  document.addEventListener("touchend", handleTouchEnd, true);
@@ -903,7 +1442,7 @@ function useFeedtackState({
903
1442
  const [pathname, setPathname] = useState3(
904
1443
  () => typeof window === "undefined" ? "/" : window.location.pathname
905
1444
  );
906
- useEffect5(() => {
1445
+ useEffect6(() => {
907
1446
  const update = () => setPathname(window.location.pathname);
908
1447
  const origPush = history.pushState.bind(history);
909
1448
  const origReplace = history.replaceState.bind(history);
@@ -930,6 +1469,12 @@ function useFeedtackState({
930
1469
  const [loading, setLoading] = useState3(true);
931
1470
  const [openThreadId, setOpenThreadId] = useState3(null);
932
1471
  const [replyBody, setReplyBody] = useState3("");
1472
+ const [isModalOpen, setIsModalOpen] = useState3(false);
1473
+ const [composeScope, setComposeScope] = useState3("site");
1474
+ const [siteFeedback, setSiteFeedback] = useState3([]);
1475
+ const [pageFeedback, setPageFeedback] = useState3([]);
1476
+ const openModal = useCallback5(() => setIsModalOpen(true), []);
1477
+ const closeModal = useCallback5(() => setIsModalOpen(false), []);
933
1478
  const resetForm = useCallback5(() => {
934
1479
  setComment("");
935
1480
  setSentiment(null);
@@ -938,7 +1483,8 @@ function useFeedtackState({
938
1483
  const pinMode = usePinMode({
939
1484
  hotkey,
940
1485
  disabled,
941
- isModalOpen: openThreadId !== null,
1486
+ isModalOpen: openThreadId !== null || isModalOpen,
1487
+ onHotkey: openModal,
942
1488
  onDeactivate: () => {
943
1489
  resetForm();
944
1490
  setOpenThreadId(null);
@@ -951,12 +1497,29 @@ function useFeedtackState({
951
1497
  flushIdleMs,
952
1498
  disabled
953
1499
  });
954
- useEffect5(() => {
1500
+ useEffect6(() => {
955
1501
  setLoading(true);
956
- adapter.loadFeedback({ pathname }).then(setFeedbackItems).catch((err) => onError?.(err)).finally(() => setLoading(false));
1502
+ adapter.loadFeedback({ pathname }).then((items) => {
1503
+ const elementItems = [];
1504
+ const siteItems = [];
1505
+ const pageItems = [];
1506
+ for (const item of items) {
1507
+ if (item.payload.scope === "site") siteItems.push(item);
1508
+ else if (item.payload.scope === "page") pageItems.push(item);
1509
+ else elementItems.push(item);
1510
+ }
1511
+ setFeedbackItems(elementItems);
1512
+ setSiteFeedback(siteItems);
1513
+ setPageFeedback(pageItems);
1514
+ }).catch((err) => onError?.(err)).finally(() => setLoading(false));
957
1515
  }, [adapter, onError, pathname]);
1516
+ const getCurrentScope = useCallback5(() => {
1517
+ if (pinMode.isActive || pinMode.pendingPins.length > 0) return "element";
1518
+ return composeScope;
1519
+ }, [pinMode.isActive, pinMode.pendingPins.length, composeScope]);
958
1520
  const commentRef = () => comment;
959
1521
  const sentimentRef = () => sentiment;
1522
+ const scopeRef = () => getCurrentScope();
960
1523
  const pinsRef = () => pinMode.pendingPins;
961
1524
  const replyRef = () => replyBody;
962
1525
  const pathRef = () => pathname;
@@ -966,6 +1529,7 @@ function useFeedtackState({
966
1529
  onError,
967
1530
  getComment: commentRef,
968
1531
  getSentiment: sentimentRef,
1532
+ getScope: scopeRef,
969
1533
  getPendingPins: pinsRef,
970
1534
  getReplyBody: replyRef,
971
1535
  getPathname: pathRef,
@@ -979,6 +1543,27 @@ function useFeedtackState({
979
1543
  shouldRescope: rescopeRoles ? (role) => rescopeRoles.includes(role) : void 0,
980
1544
  hasFlush: !!onFlush
981
1545
  });
1546
+ const handleModalSubmit = useCallback5(async () => {
1547
+ if (!comment.trim()) {
1548
+ setCommentError(true);
1549
+ return;
1550
+ }
1551
+ await actions.handleSubmit();
1552
+ const scope = composeScope;
1553
+ setFeedbackItems((prev) => {
1554
+ const newItem = prev[prev.length - 1];
1555
+ if (newItem && newItem.payload.scope === scope) {
1556
+ if (scope === "site") {
1557
+ setSiteFeedback((s) => [...s, newItem]);
1558
+ } else {
1559
+ setPageFeedback((p) => [...p, newItem]);
1560
+ }
1561
+ return prev.slice(0, -1);
1562
+ }
1563
+ return prev;
1564
+ });
1565
+ resetForm();
1566
+ }, [actions, composeScope, resetForm, comment]);
982
1567
  const isArchivedForUser = (item) => item.archives.some((a) => a.archivedBy.id === currentUser.id);
983
1568
  const hasUnread = (item) => item.replies.length > 0;
984
1569
  const hasValidPins = (item) => Array.isArray(item.payload?.pins) && item.payload.pins.length > 0;
@@ -996,11 +1581,20 @@ function useFeedtackState({
996
1581
  submitting,
997
1582
  pathname,
998
1583
  feedbackItems,
1584
+ siteFeedback,
1585
+ pageFeedback,
999
1586
  loading,
1000
1587
  openThreadId,
1001
1588
  setOpenThreadId,
1002
1589
  replyBody,
1003
1590
  setReplyBody,
1591
+ // Modal state
1592
+ isModalOpen,
1593
+ openModal,
1594
+ closeModal,
1595
+ composeScope,
1596
+ setComposeScope,
1597
+ handleModalSubmit,
1004
1598
  ...actions,
1005
1599
  isArchivedForUser,
1006
1600
  hasUnread,
@@ -1009,7 +1603,7 @@ function useFeedtackState({
1009
1603
  }
1010
1604
 
1011
1605
  // src/react/FeedtackProvider.tsx
1012
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1606
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1013
1607
  function FeedtackProvider({
1014
1608
  children,
1015
1609
  adapter,
@@ -1042,7 +1636,11 @@ function FeedtackProvider({
1042
1636
  const formPos = firstPin ? getAnchoredPosition(firstPin.x, firstPin.y) : {};
1043
1637
  const showButton = !adminOnly || currentUser.role === "admin";
1044
1638
  const openItem = state.openThreadId ? state.feedbackItems.find((i) => i.payload.id === state.openThreadId) : null;
1045
- return /* @__PURE__ */ jsxs3(
1639
+ const handlePlacePin = () => {
1640
+ state.closeModal();
1641
+ state.activatePinMode();
1642
+ };
1643
+ return /* @__PURE__ */ jsxs6(
1046
1644
  FeedtackContext.Provider,
1047
1645
  {
1048
1646
  value: {
@@ -1050,31 +1648,35 @@ function FeedtackProvider({
1050
1648
  } : state.activatePinMode,
1051
1649
  deactivatePinMode: disabled ? () => {
1052
1650
  } : state.deactivatePinMode,
1053
- isPinModeActive: disabled ? false : state.isPinModeActive
1651
+ isPinModeActive: disabled ? false : state.isPinModeActive,
1652
+ selectedColor: state.selectedColor,
1653
+ setSelectedColor: disabled ? () => {
1654
+ } : state.setSelectedColor,
1655
+ pinPalette: PIN_PALETTE,
1656
+ openModal: disabled ? () => {
1657
+ } : state.openModal,
1658
+ closeModal: disabled ? () => {
1659
+ } : state.closeModal,
1660
+ isModalOpen: disabled ? false : state.isModalOpen
1054
1661
  },
1055
1662
  children: [
1056
1663
  children,
1057
- !disabled && showButton && /* @__PURE__ */ jsxs3(
1664
+ !disabled && showButton && /* @__PURE__ */ jsx6(
1058
1665
  "button",
1059
1666
  {
1060
1667
  type: "button",
1061
1668
  className: cx(
1062
1669
  "feedtack-btn",
1063
- state.isPinModeActive && "active",
1670
+ (state.isPinModeActive || state.isModalOpen) && "active",
1064
1671
  classes.button
1065
1672
  ),
1066
- onClick: () => state.isPinModeActive ? state.deactivatePinMode() : state.activatePinMode(),
1067
- title: "Toggle feedback pin mode",
1068
- "aria-label": "Toggle feedback pin mode",
1069
- "aria-pressed": state.isPinModeActive,
1070
- children: [
1071
- "Drop Pin [Shift+",
1072
- hotkey.toUpperCase(),
1073
- "]"
1074
- ]
1673
+ onClick: () => state.openModal(),
1674
+ title: "Open feedback",
1675
+ "aria-label": "Open feedback",
1676
+ children: "Feedback"
1075
1677
  }
1076
1678
  ),
1077
- state.isPinModeActive && /* @__PURE__ */ jsx3("div", { className: cx("feedtack-color-picker", classes.colorPicker), children: PIN_PALETTE.map((color) => /* @__PURE__ */ jsx3(
1679
+ state.isPinModeActive && /* @__PURE__ */ jsx6("div", { className: cx("feedtack-color-picker", classes.colorPicker), children: PIN_PALETTE.map((color) => /* @__PURE__ */ jsx6(
1078
1680
  "button",
1079
1681
  {
1080
1682
  type: "button",
@@ -1088,7 +1690,7 @@ function FeedtackProvider({
1088
1690
  },
1089
1691
  color
1090
1692
  )) }),
1091
- state.pendingPins.map((pin) => /* @__PURE__ */ jsx3(
1693
+ state.pendingPins.map((pin) => /* @__PURE__ */ jsx6(
1092
1694
  "div",
1093
1695
  {
1094
1696
  className: cx("feedtack-pin-marker", classes.pinMarker),
@@ -1101,7 +1703,7 @@ function FeedtackProvider({
1101
1703
  },
1102
1704
  `${pin.x}-${pin.y}-${pin.color}`
1103
1705
  )),
1104
- state.showForm && /* @__PURE__ */ jsx3(
1706
+ state.showForm && /* @__PURE__ */ jsx6(
1105
1707
  CommentForm,
1106
1708
  {
1107
1709
  comment: state.comment,
@@ -1120,45 +1722,22 @@ function FeedtackProvider({
1120
1722
  onCancel: state.deactivatePinMode
1121
1723
  }
1122
1724
  ),
1123
- !state.loading && state.feedbackItems.filter((item) => item.payload.page.pathname === state.pathname).filter((item) => !state.isArchivedForUser(item)).filter((item) => state.hasValidPins(item)).map((item) => {
1124
- const pin = item.payload.pins[0];
1125
- const pos = getPosition(item.payload.id, pin);
1126
- return /* @__PURE__ */ jsxs3(
1127
- "button",
1128
- {
1129
- type: "button",
1130
- className: cx(
1131
- "feedtack-pin-marker",
1132
- item.resolutions.length > 0 && "feedtack-pin-resolved",
1133
- classes.pinMarker
1134
- ),
1135
- style: {
1136
- background: pin.color,
1137
- left: pos.x,
1138
- top: pos.y,
1139
- position: "absolute",
1140
- cursor: "pointer"
1141
- },
1142
- onClick: () => state.setOpenThreadId(
1143
- state.openThreadId === item.payload.id ? null : item.payload.id
1144
- ),
1145
- children: [
1146
- renderPinIcon ? /* @__PURE__ */ jsx3("span", { className: "feedtack-pin-icon", children: renderPinIcon(item) }) : item.resolutions.length > 0 && /* @__PURE__ */ jsx3(
1147
- "span",
1148
- {
1149
- className: "feedtack-pin-icon",
1150
- role: "img",
1151
- "aria-label": "Resolved",
1152
- children: "\u2713"
1153
- }
1154
- ),
1155
- state.hasUnread(item) && /* @__PURE__ */ jsx3("div", { className: "feedtack-pin-badge" })
1156
- ]
1157
- },
1158
- item.payload.id
1159
- );
1160
- }),
1161
- openItem && /* @__PURE__ */ jsx3(
1725
+ !state.loading && /* @__PURE__ */ jsx6(
1726
+ PinOverlay,
1727
+ {
1728
+ feedbackItems: state.feedbackItems,
1729
+ pathname: state.pathname,
1730
+ isArchivedForUser: state.isArchivedForUser,
1731
+ hasValidPins: state.hasValidPins,
1732
+ hasUnread: state.hasUnread,
1733
+ openThreadId: state.openThreadId,
1734
+ setOpenThreadId: state.setOpenThreadId,
1735
+ getPosition,
1736
+ renderPinIcon,
1737
+ pinMarkerClass: classes.pinMarker
1738
+ }
1739
+ ),
1740
+ openItem && /* @__PURE__ */ jsx6(
1162
1741
  ThreadPanel,
1163
1742
  {
1164
1743
  item: openItem,
@@ -1175,7 +1754,36 @@ function FeedtackProvider({
1175
1754
  className: classes.thread
1176
1755
  }
1177
1756
  ),
1178
- state.loading && /* @__PURE__ */ jsx3("div", { className: "feedtack-loading", children: "Loading feedback\u2026" })
1757
+ !disabled && /* @__PURE__ */ jsx6(
1758
+ FeedbackModal,
1759
+ {
1760
+ isOpen: state.isModalOpen,
1761
+ onClose: state.closeModal,
1762
+ activeTab: state.composeScope,
1763
+ onTabChange: state.setComposeScope,
1764
+ siteFeedback: state.siteFeedback,
1765
+ pageFeedback: state.pageFeedback,
1766
+ comment: state.comment,
1767
+ onCommentChange: (v) => {
1768
+ state.setComment(v);
1769
+ state.setCommentError(false);
1770
+ },
1771
+ commentError: state.commentError,
1772
+ sentiment: state.sentiment,
1773
+ onSentimentChange: state.setSentiment,
1774
+ submitting: state.submitting,
1775
+ onSubmit: state.handleModalSubmit,
1776
+ onPlacePin: handlePlacePin,
1777
+ replyBody: state.replyBody,
1778
+ onReplyBodyChange: state.setReplyBody,
1779
+ onReply: (id) => state.handleReply(id),
1780
+ onResolve: (id) => state.handleResolve(id),
1781
+ onArchive: (id) => state.handleArchive(id),
1782
+ openThreadId: state.openThreadId,
1783
+ onOpenThread: state.setOpenThreadId
1784
+ }
1785
+ ),
1786
+ state.loading && /* @__PURE__ */ jsx6("div", { className: "feedtack-loading", children: "Loading feedback\u2026" })
1179
1787
  ]
1180
1788
  }
1181
1789
  );
@@ -1187,5 +1795,6 @@ function useFeedtack() {
1187
1795
  }
1188
1796
  export {
1189
1797
  FeedtackProvider,
1798
+ PIN_PALETTE,
1190
1799
  useFeedtack
1191
1800
  };