feedtack 0.4.0 → 0.5.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.
@@ -200,8 +200,8 @@ function getTargetMeta(element) {
200
200
  tagName: resolved.tagName,
201
201
  ancestors,
202
202
  boundingRect: {
203
- x: rect.x,
204
- y: rect.y,
203
+ x: rect.x + window.scrollX,
204
+ y: rect.y + window.scrollY,
205
205
  width: rect.width,
206
206
  height: rect.height
207
207
  }
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  getTargetMeta,
8
8
  getViewportMeta,
9
9
  themeToCSS
10
- } from "./chunk-NCW2V5JL.js";
10
+ } from "./chunk-PPM4AIJU.js";
11
11
 
12
12
  // src/adapters/ConsoleAdapter.ts
13
13
  var ConsoleAdapter = class {
@@ -2,6 +2,19 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import React from 'react';
3
3
  import { e as FeedbackItem, F as FeedtackAdapter, o as FeedtackUser, n as FeedtackTheme } from '../theme-C-uctIoI.js';
4
4
 
5
+ /** Fixed palette of 6 colors for pin markers */
6
+ declare const PIN_PALETTE: readonly ["#ef4444", "#3b82f6", "#22c55e", "#f59e0b", "#a855f7", "#ec4899"];
7
+ type PinColor = (typeof PIN_PALETTE)[number];
8
+
9
+ interface FeedtackContextValue {
10
+ activatePinMode: () => void;
11
+ deactivatePinMode: () => void;
12
+ isPinModeActive: boolean;
13
+ selectedColor: string;
14
+ setSelectedColor: (color: string) => void;
15
+ pinPalette: readonly string[];
16
+ }
17
+
5
18
  interface FeedtackFlushEvent {
6
19
  pathname: string;
7
20
  items: FeedbackItem[];
@@ -40,13 +53,7 @@ interface FeedtackProviderProps {
40
53
  }
41
54
  declare function FeedtackProvider({ children, adapter, currentUser, hotkey, adminOnly, theme, classes, sentimentLabels, onError, disabled, renderPinIcon, onFlush, flushIdleMs, rescopeRoles, }: FeedtackProviderProps): react_jsx_runtime.JSX.Element;
42
55
 
43
- interface FeedtackContextValue {
44
- activatePinMode: () => void;
45
- deactivatePinMode: () => void;
46
- isPinModeActive: boolean;
47
- }
48
-
49
56
  /** Hook for host app to programmatically control feedtack */
50
57
  declare function useFeedtack(): FeedtackContextValue;
51
58
 
52
- export { type FeedtackClasses, type FeedtackFlushEvent, FeedtackProvider, type FeedtackProviderProps, type FeedtackSentimentLabels, useFeedtack };
59
+ export { type FeedtackClasses, type FeedtackContextValue, type FeedtackFlushEvent, FeedtackProvider, type FeedtackProviderProps, type FeedtackSentimentLabels, PIN_PALETTE, type PinColor, useFeedtack };
@@ -6,7 +6,7 @@ import {
6
6
  getTargetMeta,
7
7
  getViewportMeta,
8
8
  themeToCSS
9
- } from "../chunk-NCW2V5JL.js";
9
+ } from "../chunk-PPM4AIJU.js";
10
10
 
11
11
  // src/ui/colors.ts
12
12
  var PIN_PALETTE = [
@@ -155,11 +155,13 @@ function ThreadPanel({
155
155
  onResolve,
156
156
  onArchive,
157
157
  onClose,
158
- className
158
+ className,
159
+ pinPosition
159
160
  }) {
160
161
  const pin = item.payload?.pins?.[0];
161
162
  if (!pin) return null;
162
- const pos = getAnchoredPosition(pin.x, pin.y);
163
+ const { x, y } = pinPosition ?? pin;
164
+ const pos = getAnchoredPosition(x, y);
163
165
  return /* @__PURE__ */ jsxs2(
164
166
  "div",
165
167
  {
@@ -246,11 +248,195 @@ function ThreadPanel({
246
248
  );
247
249
  }
248
250
 
251
+ // src/react/useAnchoredPins.ts
252
+ import { useCallback, useEffect, useState } from "react";
253
+ function useAnchoredPins(items, pathname) {
254
+ const [positions, setPositions] = useState(
255
+ /* @__PURE__ */ new Map()
256
+ );
257
+ const resolve = useCallback(() => {
258
+ const next = /* @__PURE__ */ new Map();
259
+ for (const item of items) {
260
+ if (item.payload.page.pathname !== pathname) continue;
261
+ const pin = item.payload.pins[0];
262
+ if (!pin) continue;
263
+ const pos = resolvePin(pin);
264
+ next.set(item.payload.id, pos);
265
+ }
266
+ setPositions(next);
267
+ }, [items, pathname]);
268
+ useEffect(() => {
269
+ resolve();
270
+ }, [resolve]);
271
+ useEffect(() => {
272
+ let raf = 0;
273
+ const handler = () => {
274
+ cancelAnimationFrame(raf);
275
+ raf = requestAnimationFrame(resolve);
276
+ };
277
+ window.addEventListener("resize", handler, { passive: true });
278
+ window.addEventListener("scroll", handler, { passive: true });
279
+ return () => {
280
+ cancelAnimationFrame(raf);
281
+ window.removeEventListener("resize", handler);
282
+ window.removeEventListener("scroll", handler);
283
+ };
284
+ }, [resolve]);
285
+ const getPosition = useCallback(
286
+ (itemId, fallbackPin) => {
287
+ return positions.get(itemId) ?? { x: fallbackPin.x, y: fallbackPin.y };
288
+ },
289
+ [positions]
290
+ );
291
+ return { getPosition };
292
+ }
293
+ function resolvePin(pin) {
294
+ const { target } = pin;
295
+ if (!target.selector) return { x: pin.x, y: pin.y };
296
+ let el = null;
297
+ try {
298
+ el = document.querySelector(target.selector);
299
+ } catch {
300
+ return { x: pin.x, y: pin.y };
301
+ }
302
+ if (!el) return { x: pin.x, y: pin.y };
303
+ const rect = el.getBoundingClientRect();
304
+ const origRect = target.boundingRect;
305
+ const ratioX = origRect.width > 0 ? (pin.x - origRect.x) / origRect.width : 0.5;
306
+ const ratioY = origRect.height > 0 ? (pin.y - origRect.y) / origRect.height : 0.5;
307
+ const x = rect.x + window.scrollX + ratioX * rect.width;
308
+ const y = rect.y + window.scrollY + ratioY * rect.height;
309
+ return { x, y };
310
+ }
311
+
249
312
  // src/react/useFeedtackState.ts
250
- import { useCallback as useCallback3, useEffect as useEffect4, useState as useState2 } from "react";
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
+ }
251
437
 
252
438
  // src/react/useFeedtackDom.ts
253
- import { useEffect, useRef } from "react";
439
+ import { useEffect as useEffect2, useRef } from "react";
254
440
 
255
441
  // src/ui/styles.ts
256
442
  var FEEDTACK_DEFAULT_TOKENS = `
@@ -503,7 +689,7 @@ var FEEDTACK_STYLES = `
503
689
  // src/react/useFeedtackDom.ts
504
690
  function useFeedtackDom(theme, disabled) {
505
691
  const rootRef = useRef(null);
506
- useEffect(() => {
692
+ useEffect2(() => {
507
693
  if (disabled) return;
508
694
  if (document.getElementById("feedtack-styles")) return;
509
695
  const style = document.createElement("style");
@@ -514,7 +700,7 @@ function useFeedtackDom(theme, disabled) {
514
700
  style.remove();
515
701
  };
516
702
  }, [disabled]);
517
- useEffect(() => {
703
+ useEffect2(() => {
518
704
  if (disabled) return;
519
705
  const root = document.createElement("div");
520
706
  root.id = "feedtack-root";
@@ -524,7 +710,7 @@ function useFeedtackDom(theme, disabled) {
524
710
  root.remove();
525
711
  };
526
712
  }, [disabled]);
527
- useEffect(() => {
713
+ useEffect2(() => {
528
714
  if (disabled) return;
529
715
  const root = document.getElementById("feedtack-root");
530
716
  if (!root || !theme) return;
@@ -537,7 +723,7 @@ function useFeedtackDom(theme, disabled) {
537
723
  }
538
724
 
539
725
  // src/react/useFeedtackFlush.ts
540
- import { useCallback, useEffect as useEffect2, useRef as useRef2 } from "react";
726
+ import { useCallback as useCallback3, useEffect as useEffect3, useRef as useRef2 } from "react";
541
727
  var DEFAULT_IDLE_MS = 5 * 60 * 1e3;
542
728
  function useFeedtackFlush({
543
729
  pathname,
@@ -549,7 +735,7 @@ function useFeedtackFlush({
549
735
  const flushedRef = useRef2(/* @__PURE__ */ new Set());
550
736
  const prevPathnameRef = useRef2(pathname);
551
737
  const idleTimerRef = useRef2(null);
552
- const flush = useCallback(
738
+ const flush = useCallback3(
553
739
  (path, items) => {
554
740
  if (!onFlush || flushedRef.current.has(path)) return;
555
741
  const pageItems = items.filter((i) => i.payload.page.pathname === path);
@@ -559,7 +745,7 @@ function useFeedtackFlush({
559
745
  },
560
746
  [onFlush]
561
747
  );
562
- useEffect2(() => {
748
+ useEffect3(() => {
563
749
  if (disabled || !onFlush) return;
564
750
  const prev = prevPathnameRef.current;
565
751
  prevPathnameRef.current = pathname;
@@ -567,7 +753,7 @@ function useFeedtackFlush({
567
753
  flush(prev, feedbackItems);
568
754
  }
569
755
  }, [pathname, feedbackItems, flush, onFlush, disabled]);
570
- useEffect2(() => {
756
+ useEffect3(() => {
571
757
  if (disabled || !onFlush || flushIdleMs <= 0) return;
572
758
  const resetTimer = () => {
573
759
  if (idleTimerRef.current) clearTimeout(idleTimerRef.current);
@@ -584,38 +770,38 @@ function useFeedtackFlush({
584
770
  for (const e of events) window.removeEventListener(e, resetTimer);
585
771
  };
586
772
  }, [pathname, feedbackItems, flush, onFlush, flushIdleMs, disabled]);
587
- useEffect2(() => {
773
+ useEffect3(() => {
588
774
  if (disabled || !onFlush) return;
589
775
  const handleUnload = () => flush(pathname, feedbackItems);
590
776
  window.addEventListener("beforeunload", handleUnload);
591
777
  return () => window.removeEventListener("beforeunload", handleUnload);
592
778
  }, [pathname, feedbackItems, flush, onFlush, disabled]);
593
- const clearFlushed = useCallback((path) => {
779
+ const clearFlushed = useCallback3((path) => {
594
780
  flushedRef.current.delete(path);
595
781
  }, []);
596
782
  return { clearFlushed };
597
783
  }
598
784
 
599
785
  // src/react/usePinMode.ts
600
- import { useCallback as useCallback2, useEffect as useEffect3, useState } from "react";
786
+ import { useCallback as useCallback4, useEffect as useEffect4, useState as useState2 } from "react";
601
787
  function usePinMode({
602
788
  hotkey,
603
789
  onDeactivate,
604
790
  disabled,
605
791
  isModalOpen
606
792
  }) {
607
- const [isActive, setIsActive] = useState(false);
608
- const [pendingPins, setPendingPins] = useState([]);
609
- const [selectedColor, setSelectedColor] = useState(PIN_PALETTE[0]);
610
- const [showForm, setShowForm] = useState(false);
611
- const activate = useCallback2(() => setIsActive(true), []);
612
- const deactivate = useCallback2(() => {
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(() => {
613
799
  setIsActive(false);
614
800
  setPendingPins([]);
615
801
  setShowForm(false);
616
802
  onDeactivate?.();
617
803
  }, [onDeactivate]);
618
- useEffect3(() => {
804
+ useEffect4(() => {
619
805
  if (isActive) {
620
806
  document.documentElement.classList.add("feedtack-crosshair");
621
807
  } else {
@@ -623,7 +809,7 @@ function usePinMode({
623
809
  }
624
810
  return () => document.documentElement.classList.remove("feedtack-crosshair");
625
811
  }, [isActive]);
626
- useEffect3(() => {
812
+ useEffect4(() => {
627
813
  if (disabled) return;
628
814
  const handler = (e) => {
629
815
  if (e.key === hotkey.toUpperCase() && e.shiftKey) {
@@ -644,7 +830,7 @@ function usePinMode({
644
830
  window.addEventListener("keydown", handler);
645
831
  return () => window.removeEventListener("keydown", handler);
646
832
  }, [hotkey, deactivate, isActive, disabled, isModalOpen, showForm]);
647
- const placePin = useCallback2(
833
+ const placePin = useCallback4(
648
834
  (coords, target) => {
649
835
  if (target.closest("#feedtack-root, .feedtack-form, .feedtack-color-picker"))
650
836
  return;
@@ -660,7 +846,7 @@ function usePinMode({
660
846
  },
661
847
  [selectedColor]
662
848
  );
663
- const handlePageClick = useCallback2(
849
+ const handlePageClick = useCallback4(
664
850
  (e) => {
665
851
  if (!isActive) return;
666
852
  e.preventDefault();
@@ -669,7 +855,7 @@ function usePinMode({
669
855
  },
670
856
  [isActive, placePin]
671
857
  );
672
- const handleTouchEnd = useCallback2(
858
+ const handleTouchEnd = useCallback4(
673
859
  (e) => {
674
860
  if (!isActive) return;
675
861
  const touch = e.changedTouches[0];
@@ -681,7 +867,7 @@ function usePinMode({
681
867
  },
682
868
  [isActive, placePin]
683
869
  );
684
- useEffect3(() => {
870
+ useEffect4(() => {
685
871
  if (disabled) return;
686
872
  document.addEventListener("click", handlePageClick, true);
687
873
  document.addEventListener("touchend", handleTouchEnd, true);
@@ -714,10 +900,10 @@ function useFeedtackState({
714
900
  rescopeRoles
715
901
  }) {
716
902
  useFeedtackDom(theme, disabled);
717
- const [pathname, setPathname] = useState2(
903
+ const [pathname, setPathname] = useState3(
718
904
  () => typeof window === "undefined" ? "/" : window.location.pathname
719
905
  );
720
- useEffect4(() => {
906
+ useEffect5(() => {
721
907
  const update = () => setPathname(window.location.pathname);
722
908
  const origPush = history.pushState.bind(history);
723
909
  const origReplace = history.replaceState.bind(history);
@@ -736,15 +922,15 @@ function useFeedtackState({
736
922
  history.replaceState = origReplace;
737
923
  };
738
924
  }, []);
739
- const [comment, setComment] = useState2("");
740
- const [sentiment, setSentiment] = useState2(null);
741
- const [commentError, setCommentError] = useState2(false);
742
- const [submitting, setSubmitting] = useState2(false);
743
- const [feedbackItems, setFeedbackItems] = useState2([]);
744
- const [loading, setLoading] = useState2(true);
745
- const [openThreadId, setOpenThreadId] = useState2(null);
746
- const [replyBody, setReplyBody] = useState2("");
747
- const resetForm = useCallback3(() => {
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(() => {
748
934
  setComment("");
749
935
  setSentiment(null);
750
936
  setCommentError(false);
@@ -765,113 +951,34 @@ function useFeedtackState({
765
951
  flushIdleMs,
766
952
  disabled
767
953
  });
768
- useEffect4(() => {
954
+ useEffect5(() => {
769
955
  setLoading(true);
770
956
  adapter.loadFeedback({ pathname }).then(setFeedbackItems).catch((err) => onError?.(err)).finally(() => setLoading(false));
771
957
  }, [adapter, onError, pathname]);
772
- const updateItem = (id, fn) => setFeedbackItems(
773
- (prev) => prev.map((i) => i.payload.id === id ? fn(i) : i)
774
- );
775
- const handleSubmit = async () => {
776
- if (!comment.trim()) {
777
- setCommentError(true);
778
- return;
779
- }
780
- setSubmitting(true);
781
- const payload = {
782
- schemaVersion: SCHEMA_VERSION,
783
- id: generateId(),
784
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
785
- submittedBy: currentUser,
786
- comment: comment.trim(),
787
- sentiment,
788
- pins: pinMode.pendingPins.map((p, i) => ({ ...p, index: i + 1 })),
789
- page: getPageMeta(),
790
- viewport: getViewportMeta(),
791
- device: getDeviceMeta()
792
- };
793
- try {
794
- await adapter.submit(payload);
795
- setFeedbackItems((prev) => [
796
- ...prev,
797
- { payload, replies: [], resolutions: [], archives: [] }
798
- ]);
799
- pinMode.deactivate();
800
- } catch (err) {
801
- onError?.(err);
802
- } finally {
803
- setSubmitting(false);
804
- }
805
- };
806
- const handleReply = async (feedbackId) => {
807
- if (!replyBody.trim()) return;
808
- const ts = (/* @__PURE__ */ new Date()).toISOString();
809
- const body = replyBody.trim();
810
- try {
811
- await adapter.reply(feedbackId, {
812
- author: currentUser,
813
- body,
814
- timestamp: ts
815
- });
816
- updateItem(feedbackId, (item) => {
817
- const updated = {
818
- ...item,
819
- replies: [
820
- ...item.replies,
821
- {
822
- id: generateId(),
823
- feedbackId,
824
- author: currentUser,
825
- body,
826
- timestamp: ts
827
- }
828
- ]
829
- };
830
- const triggerRescope = rescopeRoles ? rescopeRoles.includes(currentUser.role) : currentUser.role !== "agent";
831
- if (triggerRescope && updated.resolutions.length === 0 && onFlush) {
832
- clearFlushed(pathname);
833
- }
834
- return updated;
835
- });
836
- setReplyBody("");
837
- } catch (err) {
838
- onError?.(err);
839
- }
840
- };
841
- const handleResolve = async (feedbackId) => {
842
- const ts = (/* @__PURE__ */ new Date()).toISOString();
843
- try {
844
- await adapter.resolve(feedbackId, {
845
- resolvedBy: currentUser,
846
- timestamp: ts
847
- });
848
- updateItem(feedbackId, (item) => ({
849
- ...item,
850
- resolutions: [
851
- ...item.resolutions,
852
- { feedbackId, resolvedBy: currentUser, timestamp: ts }
853
- ]
854
- }));
855
- } catch (err) {
856
- onError?.(err);
857
- }
858
- };
859
- const handleArchive = async (feedbackId) => {
860
- const ts = (/* @__PURE__ */ new Date()).toISOString();
861
- try {
862
- await adapter.archive(feedbackId, currentUser.id);
863
- updateItem(feedbackId, (item) => ({
864
- ...item,
865
- archives: [
866
- ...item.archives,
867
- { feedbackId, archivedBy: currentUser, timestamp: ts }
868
- ]
869
- }));
870
- setOpenThreadId(null);
871
- } catch (err) {
872
- onError?.(err);
873
- }
874
- };
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
+ });
875
982
  const isArchivedForUser = (item) => item.archives.some((a) => a.archivedBy.id === currentUser.id);
876
983
  const hasUnread = (item) => item.replies.length > 0;
877
984
  const hasValidPins = (item) => Array.isArray(item.payload?.pins) && item.payload.pins.length > 0;
@@ -894,10 +1001,7 @@ function useFeedtackState({
894
1001
  setOpenThreadId,
895
1002
  replyBody,
896
1003
  setReplyBody,
897
- handleSubmit,
898
- handleReply,
899
- handleResolve,
900
- handleArchive,
1004
+ ...actions,
901
1005
  isArchivedForUser,
902
1006
  hasUnread,
903
1007
  hasValidPins
@@ -933,6 +1037,7 @@ function FeedtackProvider({
933
1037
  flushIdleMs,
934
1038
  rescopeRoles
935
1039
  });
1040
+ const { getPosition } = useAnchoredPins(state.feedbackItems, state.pathname);
936
1041
  const firstPin = state.pendingPins[0];
937
1042
  const formPos = firstPin ? getAnchoredPosition(firstPin.x, firstPin.y) : {};
938
1043
  const showButton = !adminOnly || currentUser.role === "admin";
@@ -945,7 +1050,11 @@ function FeedtackProvider({
945
1050
  } : state.activatePinMode,
946
1051
  deactivatePinMode: disabled ? () => {
947
1052
  } : state.deactivatePinMode,
948
- isPinModeActive: disabled ? false : state.isPinModeActive
1053
+ isPinModeActive: disabled ? false : state.isPinModeActive,
1054
+ selectedColor: state.selectedColor,
1055
+ setSelectedColor: disabled ? () => {
1056
+ } : state.setSelectedColor,
1057
+ pinPalette: PIN_PALETTE
949
1058
  },
950
1059
  children: [
951
1060
  children,
@@ -1017,6 +1126,7 @@ function FeedtackProvider({
1017
1126
  ),
1018
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) => {
1019
1128
  const pin = item.payload.pins[0];
1129
+ const pos = getPosition(item.payload.id, pin);
1020
1130
  return /* @__PURE__ */ jsxs3(
1021
1131
  "button",
1022
1132
  {
@@ -1028,8 +1138,8 @@ function FeedtackProvider({
1028
1138
  ),
1029
1139
  style: {
1030
1140
  background: pin.color,
1031
- left: pin.x,
1032
- top: pin.y,
1141
+ left: pos.x,
1142
+ top: pos.y,
1033
1143
  position: "absolute",
1034
1144
  cursor: "pointer"
1035
1145
  },
@@ -1062,6 +1172,10 @@ function FeedtackProvider({
1062
1172
  onResolve: () => state.handleResolve(openItem.payload.id),
1063
1173
  onArchive: () => state.handleArchive(openItem.payload.id),
1064
1174
  onClose: () => state.setOpenThreadId(null),
1175
+ pinPosition: getPosition(
1176
+ openItem.payload.id,
1177
+ openItem.payload.pins[0]
1178
+ ),
1065
1179
  className: classes.thread
1066
1180
  }
1067
1181
  ),
@@ -1077,5 +1191,6 @@ function useFeedtack() {
1077
1191
  }
1078
1192
  export {
1079
1193
  FeedtackProvider,
1194
+ PIN_PALETTE,
1080
1195
  useFeedtack
1081
1196
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "feedtack",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Click anywhere. Drop a pin. Get a payload a developer can act on.",
5
5
  "type": "module",
6
6
  "license": "MIT",