feedtack 0.1.0 → 0.2.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.
@@ -23,8 +23,9 @@ interface FeedtackProviderProps {
23
23
  classes?: FeedtackClasses;
24
24
  sentimentLabels?: FeedtackSentimentLabels;
25
25
  onError?: (err: Error) => void;
26
+ disabled?: boolean;
26
27
  }
27
- declare function FeedtackProvider({ children, adapter, currentUser, hotkey, adminOnly, theme, classes, sentimentLabels, onError, }: FeedtackProviderProps): react_jsx_runtime.JSX.Element;
28
+ declare function FeedtackProvider({ children, adapter, currentUser, hotkey, adminOnly, theme, classes, sentimentLabels, onError, disabled, }: FeedtackProviderProps): react_jsx_runtime.JSX.Element;
28
29
 
29
30
  interface FeedtackContextValue {
30
31
  activatePinMode: () => void;
@@ -24,16 +24,6 @@ var PIN_PALETTE = [
24
24
  // pink
25
25
  ];
26
26
 
27
- // src/react/context.ts
28
- import { createContext, useContext } from "react";
29
- var FeedtackContext = createContext(null);
30
- function useFeedtackContext() {
31
- const ctx = useContext(FeedtackContext);
32
- if (!ctx)
33
- throw new Error("useFeedtack must be used inside <FeedtackProvider>");
34
- return ctx;
35
- }
36
-
37
27
  // src/react/utils.ts
38
28
  function generateId() {
39
29
  return `ft_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`;
@@ -55,8 +45,98 @@ function cx(...parts) {
55
45
  return parts.filter(Boolean).join(" ");
56
46
  }
57
47
 
58
- // src/react/ThreadPanel.tsx
48
+ // src/react/CommentForm.tsx
59
49
  import { jsx, jsxs } from "react/jsx-runtime";
50
+ function CommentForm({
51
+ comment,
52
+ commentError,
53
+ sentiment,
54
+ submitting,
55
+ formPos,
56
+ classes,
57
+ sentimentLabels,
58
+ onCommentChange,
59
+ onSentimentChange,
60
+ onSubmit,
61
+ onCancel
62
+ }) {
63
+ return /* @__PURE__ */ jsxs(
64
+ "div",
65
+ {
66
+ className: cx("feedtack-form", classes.form),
67
+ style: { position: "fixed", ...formPos },
68
+ children: [
69
+ /* @__PURE__ */ jsx(
70
+ "textarea",
71
+ {
72
+ className: commentError ? "error" : "",
73
+ placeholder: "What's the issue? (required)",
74
+ value: comment,
75
+ onChange: (e) => onCommentChange(e.target.value),
76
+ ref: (el) => el?.focus()
77
+ }
78
+ ),
79
+ commentError && /* @__PURE__ */ jsx("span", { className: "feedtack-error-msg", children: "Comment is required" }),
80
+ /* @__PURE__ */ jsxs("div", { className: "feedtack-sentiment", children: [
81
+ /* @__PURE__ */ jsx(
82
+ "button",
83
+ {
84
+ type: "button",
85
+ className: sentiment === "satisfied" ? "selected" : "",
86
+ onClick: () => onSentimentChange(sentiment === "satisfied" ? null : "satisfied"),
87
+ children: sentimentLabels.satisfied ?? "\u{1F60A} Satisfied"
88
+ }
89
+ ),
90
+ /* @__PURE__ */ jsx(
91
+ "button",
92
+ {
93
+ type: "button",
94
+ className: sentiment === "dissatisfied" ? "selected" : "",
95
+ onClick: () => onSentimentChange(
96
+ sentiment === "dissatisfied" ? null : "dissatisfied"
97
+ ),
98
+ children: sentimentLabels.dissatisfied ?? "\u{1F61E} Dissatisfied"
99
+ }
100
+ )
101
+ ] }),
102
+ /* @__PURE__ */ jsxs("div", { className: "feedtack-form-actions", children: [
103
+ /* @__PURE__ */ jsx(
104
+ "button",
105
+ {
106
+ type: "button",
107
+ className: "feedtack-btn-cancel",
108
+ onClick: onCancel,
109
+ children: "Cancel"
110
+ }
111
+ ),
112
+ /* @__PURE__ */ jsx(
113
+ "button",
114
+ {
115
+ type: "button",
116
+ className: "feedtack-btn-submit",
117
+ onClick: onSubmit,
118
+ disabled: submitting,
119
+ children: submitting ? "Sending\u2026" : "Submit"
120
+ }
121
+ )
122
+ ] })
123
+ ]
124
+ }
125
+ );
126
+ }
127
+
128
+ // src/react/context.ts
129
+ import { createContext, useContext } from "react";
130
+ var FeedtackContext = createContext(null);
131
+ function useFeedtackContext() {
132
+ const ctx = useContext(FeedtackContext);
133
+ if (!ctx)
134
+ throw new Error("useFeedtack must be used inside <FeedtackProvider>");
135
+ return ctx;
136
+ }
137
+
138
+ // src/react/ThreadPanel.tsx
139
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
60
140
  function ThreadPanel({
61
141
  item,
62
142
  replyBody,
@@ -69,26 +149,26 @@ function ThreadPanel({
69
149
  }) {
70
150
  const pin = item.payload.pins[0];
71
151
  const pos = getAnchoredPosition(pin.x, pin.y);
72
- return /* @__PURE__ */ jsxs(
152
+ return /* @__PURE__ */ jsxs2(
73
153
  "div",
74
154
  {
75
155
  className: cx("feedtack-thread", className),
76
156
  style: { position: "fixed", ...pos },
77
157
  children: [
78
- /* @__PURE__ */ jsx("strong", { style: { fontSize: 13 }, children: item.payload.submittedBy.name }),
79
- /* @__PURE__ */ jsx("p", { style: { fontSize: 13 }, children: item.payload.comment }),
80
- item.replies.map((r) => /* @__PURE__ */ jsxs(
158
+ /* @__PURE__ */ jsx2("strong", { style: { fontSize: 13 }, children: item.payload.submittedBy.name }),
159
+ /* @__PURE__ */ jsx2("p", { style: { fontSize: 13 }, children: item.payload.comment }),
160
+ item.replies.map((r) => /* @__PURE__ */ jsxs2(
81
161
  "div",
82
162
  {
83
163
  style: { borderTop: "1px solid #f3f4f6", paddingTop: 8 },
84
164
  children: [
85
- /* @__PURE__ */ jsx("span", { style: { fontSize: 12, fontWeight: 600 }, children: r.author.name }),
86
- /* @__PURE__ */ jsx("p", { style: { fontSize: 12 }, children: r.body })
165
+ /* @__PURE__ */ jsx2("span", { style: { fontSize: 12, fontWeight: 600 }, children: r.author.name }),
166
+ /* @__PURE__ */ jsx2("p", { style: { fontSize: 12 }, children: r.body })
87
167
  ]
88
168
  },
89
169
  r.id
90
170
  )),
91
- /* @__PURE__ */ jsx(
171
+ /* @__PURE__ */ jsx2(
92
172
  "textarea",
93
173
  {
94
174
  placeholder: "Reply\u2026",
@@ -104,8 +184,8 @@ function ThreadPanel({
104
184
  }
105
185
  }
106
186
  ),
107
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 6, flexWrap: "wrap" }, children: [
108
- /* @__PURE__ */ jsx(
187
+ /* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: 6, flexWrap: "wrap" }, children: [
188
+ /* @__PURE__ */ jsx2(
109
189
  "button",
110
190
  {
111
191
  type: "button",
@@ -115,7 +195,7 @@ function ThreadPanel({
115
195
  children: "Reply"
116
196
  }
117
197
  ),
118
- /* @__PURE__ */ jsx(
198
+ /* @__PURE__ */ jsx2(
119
199
  "button",
120
200
  {
121
201
  type: "button",
@@ -125,7 +205,7 @@ function ThreadPanel({
125
205
  children: "Mark Resolved"
126
206
  }
127
207
  ),
128
- /* @__PURE__ */ jsx(
208
+ /* @__PURE__ */ jsx2(
129
209
  "button",
130
210
  {
131
211
  type: "button",
@@ -135,7 +215,7 @@ function ThreadPanel({
135
215
  children: "Archive"
136
216
  }
137
217
  ),
138
- /* @__PURE__ */ jsx(
218
+ /* @__PURE__ */ jsx2(
139
219
  "button",
140
220
  {
141
221
  type: "button",
@@ -385,9 +465,10 @@ var FEEDTACK_STYLES = `
385
465
  `;
386
466
 
387
467
  // src/react/useFeedtackDom.ts
388
- function useFeedtackDom(theme) {
468
+ function useFeedtackDom(theme, disabled) {
389
469
  const rootRef = useRef(null);
390
470
  useEffect(() => {
471
+ if (disabled) return;
391
472
  if (document.getElementById("feedtack-styles")) return;
392
473
  const style = document.createElement("style");
393
474
  style.id = "feedtack-styles";
@@ -396,8 +477,9 @@ function useFeedtackDom(theme) {
396
477
  return () => {
397
478
  style.remove();
398
479
  };
399
- }, []);
480
+ }, [disabled]);
400
481
  useEffect(() => {
482
+ if (disabled) return;
401
483
  const root = document.createElement("div");
402
484
  root.id = "feedtack-root";
403
485
  document.body.appendChild(root);
@@ -405,21 +487,22 @@ function useFeedtackDom(theme) {
405
487
  return () => {
406
488
  root.remove();
407
489
  };
408
- }, []);
490
+ }, [disabled]);
409
491
  useEffect(() => {
492
+ if (disabled) return;
410
493
  const root = document.getElementById("feedtack-root");
411
494
  if (!root || !theme) return;
412
495
  const tokens = themeToCSS(theme);
413
496
  for (const [k, v] of Object.entries(tokens)) {
414
497
  root.style.setProperty(k, v);
415
498
  }
416
- }, [theme]);
499
+ }, [theme, disabled]);
417
500
  return rootRef;
418
501
  }
419
502
 
420
503
  // src/react/usePinMode.ts
421
504
  import { useCallback, useEffect as useEffect2, useState } from "react";
422
- function usePinMode({ hotkey, onDeactivate }) {
505
+ function usePinMode({ hotkey, onDeactivate, disabled }) {
423
506
  const [isActive, setIsActive] = useState(false);
424
507
  const [pendingPins, setPendingPins] = useState([]);
425
508
  const [selectedColor, setSelectedColor] = useState(PIN_PALETTE[0]);
@@ -440,6 +523,7 @@ function usePinMode({ hotkey, onDeactivate }) {
440
523
  return () => document.documentElement.classList.remove("feedtack-crosshair");
441
524
  }, [isActive]);
442
525
  useEffect2(() => {
526
+ if (disabled) return;
443
527
  const handler = (e) => {
444
528
  if (e.key === hotkey.toUpperCase() && e.shiftKey) {
445
529
  setIsActive((prev) => !prev);
@@ -458,7 +542,7 @@ function usePinMode({ hotkey, onDeactivate }) {
458
542
  };
459
543
  window.addEventListener("keydown", handler);
460
544
  return () => window.removeEventListener("keydown", handler);
461
- }, [hotkey, deactivate, isActive]);
545
+ }, [hotkey, deactivate, isActive, disabled]);
462
546
  const handlePageClick = useCallback(
463
547
  (e) => {
464
548
  if (!isActive) return;
@@ -480,9 +564,10 @@ function usePinMode({ hotkey, onDeactivate }) {
480
564
  [isActive, selectedColor]
481
565
  );
482
566
  useEffect2(() => {
567
+ if (disabled) return;
483
568
  document.addEventListener("click", handlePageClick, true);
484
569
  return () => document.removeEventListener("click", handlePageClick, true);
485
- }, [handlePageClick]);
570
+ }, [handlePageClick, disabled]);
486
571
  return {
487
572
  isActive,
488
573
  activate,
@@ -500,9 +585,30 @@ function useFeedtackState({
500
585
  currentUser,
501
586
  hotkey,
502
587
  theme,
503
- onError
588
+ onError,
589
+ disabled
504
590
  }) {
505
- useFeedtackDom(theme);
591
+ useFeedtackDom(theme, disabled);
592
+ const [pathname, setPathname] = useState2(() => window.location.pathname);
593
+ useEffect3(() => {
594
+ const update = () => setPathname(window.location.pathname);
595
+ const origPush = history.pushState.bind(history);
596
+ const origReplace = history.replaceState.bind(history);
597
+ history.pushState = (...args) => {
598
+ origPush(...args);
599
+ update();
600
+ };
601
+ history.replaceState = (...args) => {
602
+ origReplace(...args);
603
+ update();
604
+ };
605
+ window.addEventListener("popstate", update);
606
+ return () => {
607
+ window.removeEventListener("popstate", update);
608
+ history.pushState = origPush;
609
+ history.replaceState = origReplace;
610
+ };
611
+ }, []);
506
612
  const [comment, setComment] = useState2("");
507
613
  const [sentiment, setSentiment] = useState2(null);
508
614
  const [commentError, setCommentError] = useState2(false);
@@ -518,6 +624,7 @@ function useFeedtackState({
518
624
  }, []);
519
625
  const pinMode = usePinMode({
520
626
  hotkey,
627
+ disabled,
521
628
  onDeactivate: () => {
522
629
  resetForm();
523
630
  setOpenThreadId(null);
@@ -525,8 +632,8 @@ function useFeedtackState({
525
632
  });
526
633
  useEffect3(() => {
527
634
  setLoading(true);
528
- adapter.loadFeedback({ pathname: window.location.pathname }).then(setFeedbackItems).catch((err) => onError?.(err)).finally(() => setLoading(false));
529
- }, [adapter, onError]);
635
+ adapter.loadFeedback({ pathname }).then(setFeedbackItems).catch((err) => onError?.(err)).finally(() => setLoading(false));
636
+ }, [adapter, onError, pathname]);
530
637
  const updateItem = (id, fn) => setFeedbackItems(
531
638
  (prev) => prev.map((i) => i.payload.id === id ? fn(i) : i)
532
639
  );
@@ -635,6 +742,7 @@ function useFeedtackState({
635
742
  commentError,
636
743
  setCommentError,
637
744
  submitting,
745
+ pathname,
638
746
  feedbackItems,
639
747
  loading,
640
748
  openThreadId,
@@ -651,7 +759,7 @@ function useFeedtackState({
651
759
  }
652
760
 
653
761
  // src/react/FeedtackProvider.tsx
654
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
762
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
655
763
  function FeedtackProvider({
656
764
  children,
657
765
  adapter,
@@ -661,30 +769,34 @@ function FeedtackProvider({
661
769
  theme,
662
770
  classes = {},
663
771
  sentimentLabels = {},
664
- onError
772
+ onError,
773
+ disabled = false
665
774
  }) {
666
775
  const state = useFeedtackState({
667
776
  adapter,
668
777
  currentUser,
669
778
  hotkey,
670
779
  theme,
671
- onError
780
+ onError,
781
+ disabled
672
782
  });
673
783
  const firstPin = state.pendingPins[0];
674
784
  const formPos = firstPin ? getAnchoredPosition(firstPin.x, firstPin.y) : {};
675
785
  const showButton = !adminOnly || currentUser.role === "admin";
676
786
  const openItem = state.openThreadId ? state.feedbackItems.find((i) => i.payload.id === state.openThreadId) : null;
677
- return /* @__PURE__ */ jsxs2(
787
+ return /* @__PURE__ */ jsxs3(
678
788
  FeedtackContext.Provider,
679
789
  {
680
790
  value: {
681
- activatePinMode: state.activatePinMode,
682
- deactivatePinMode: state.deactivatePinMode,
683
- isPinModeActive: state.isPinModeActive
791
+ activatePinMode: disabled ? () => {
792
+ } : state.activatePinMode,
793
+ deactivatePinMode: disabled ? () => {
794
+ } : state.deactivatePinMode,
795
+ isPinModeActive: disabled ? false : state.isPinModeActive
684
796
  },
685
797
  children: [
686
798
  children,
687
- showButton && /* @__PURE__ */ jsxs2(
799
+ !disabled && showButton && /* @__PURE__ */ jsxs3(
688
800
  "button",
689
801
  {
690
802
  type: "button",
@@ -702,7 +814,7 @@ function FeedtackProvider({
702
814
  ]
703
815
  }
704
816
  ),
705
- state.isPinModeActive && /* @__PURE__ */ jsx2("div", { className: cx("feedtack-color-picker", classes.colorPicker), children: PIN_PALETTE.map((color) => /* @__PURE__ */ jsx2(
817
+ state.isPinModeActive && /* @__PURE__ */ jsx3("div", { className: cx("feedtack-color-picker", classes.colorPicker), children: PIN_PALETTE.map((color) => /* @__PURE__ */ jsx3(
706
818
  "button",
707
819
  {
708
820
  type: "button",
@@ -716,7 +828,7 @@ function FeedtackProvider({
716
828
  },
717
829
  color
718
830
  )) }),
719
- state.pendingPins.map((pin) => /* @__PURE__ */ jsx2(
831
+ state.pendingPins.map((pin) => /* @__PURE__ */ jsx3(
720
832
  "div",
721
833
  {
722
834
  className: cx("feedtack-pin-marker", classes.pinMarker),
@@ -729,77 +841,28 @@ function FeedtackProvider({
729
841
  },
730
842
  `${pin.x}-${pin.y}-${pin.color}`
731
843
  )),
732
- state.showForm && /* @__PURE__ */ jsxs2(
733
- "div",
844
+ state.showForm && /* @__PURE__ */ jsx3(
845
+ CommentForm,
734
846
  {
735
- className: cx("feedtack-form", classes.form),
736
- style: { position: "fixed", ...formPos },
737
- children: [
738
- /* @__PURE__ */ jsx2(
739
- "textarea",
740
- {
741
- className: state.commentError ? "error" : "",
742
- placeholder: "What's the issue? (required)",
743
- value: state.comment,
744
- onChange: (e) => {
745
- state.setComment(e.target.value);
746
- state.setCommentError(false);
747
- },
748
- ref: (el) => el?.focus()
749
- }
750
- ),
751
- state.commentError && /* @__PURE__ */ jsx2("span", { className: "feedtack-error-msg", children: "Comment is required" }),
752
- /* @__PURE__ */ jsxs2("div", { className: "feedtack-sentiment", children: [
753
- /* @__PURE__ */ jsx2(
754
- "button",
755
- {
756
- type: "button",
757
- className: state.sentiment === "satisfied" ? "selected" : "",
758
- onClick: () => state.setSentiment(
759
- state.sentiment === "satisfied" ? null : "satisfied"
760
- ),
761
- children: sentimentLabels.satisfied ?? "\u{1F60A} Satisfied"
762
- }
763
- ),
764
- /* @__PURE__ */ jsx2(
765
- "button",
766
- {
767
- type: "button",
768
- className: state.sentiment === "dissatisfied" ? "selected" : "",
769
- onClick: () => state.setSentiment(
770
- state.sentiment === "dissatisfied" ? null : "dissatisfied"
771
- ),
772
- children: sentimentLabels.dissatisfied ?? "\u{1F61E} Dissatisfied"
773
- }
774
- )
775
- ] }),
776
- /* @__PURE__ */ jsxs2("div", { className: "feedtack-form-actions", children: [
777
- /* @__PURE__ */ jsx2(
778
- "button",
779
- {
780
- type: "button",
781
- className: "feedtack-btn-cancel",
782
- onClick: state.deactivatePinMode,
783
- children: "Cancel"
784
- }
785
- ),
786
- /* @__PURE__ */ jsx2(
787
- "button",
788
- {
789
- type: "button",
790
- className: "feedtack-btn-submit",
791
- onClick: state.handleSubmit,
792
- disabled: state.submitting,
793
- children: state.submitting ? "Sending\u2026" : "Submit"
794
- }
795
- )
796
- ] })
797
- ]
847
+ comment: state.comment,
848
+ commentError: state.commentError,
849
+ sentiment: state.sentiment,
850
+ submitting: state.submitting,
851
+ formPos,
852
+ classes,
853
+ sentimentLabels,
854
+ onCommentChange: (v) => {
855
+ state.setComment(v);
856
+ state.setCommentError(false);
857
+ },
858
+ onSentimentChange: state.setSentiment,
859
+ onSubmit: state.handleSubmit,
860
+ onCancel: state.deactivatePinMode
798
861
  }
799
862
  ),
800
- !state.loading && state.feedbackItems.filter((item) => !state.isArchivedForUser(item)).map((item) => {
863
+ !state.loading && state.feedbackItems.filter((item) => item.payload.page.pathname === state.pathname).filter((item) => !state.isArchivedForUser(item)).map((item) => {
801
864
  const pin = item.payload.pins[0];
802
- return /* @__PURE__ */ jsx2(
865
+ return /* @__PURE__ */ jsx3(
803
866
  "button",
804
867
  {
805
868
  type: "button",
@@ -814,12 +877,12 @@ function FeedtackProvider({
814
877
  onClick: () => state.setOpenThreadId(
815
878
  state.openThreadId === item.payload.id ? null : item.payload.id
816
879
  ),
817
- children: state.hasUnread(item) && /* @__PURE__ */ jsx2("div", { className: "feedtack-pin-badge" })
880
+ children: state.hasUnread(item) && /* @__PURE__ */ jsx3("div", { className: "feedtack-pin-badge" })
818
881
  },
819
882
  item.payload.id
820
883
  );
821
884
  }),
822
- openItem && /* @__PURE__ */ jsx2(
885
+ openItem && /* @__PURE__ */ jsx3(
823
886
  ThreadPanel,
824
887
  {
825
888
  item: openItem,
@@ -832,7 +895,7 @@ function FeedtackProvider({
832
895
  className: classes.thread
833
896
  }
834
897
  ),
835
- state.loading && /* @__PURE__ */ jsx2("div", { className: "feedtack-loading", children: "Loading feedback\u2026" })
898
+ state.loading && /* @__PURE__ */ jsx3("div", { className: "feedtack-loading", children: "Loading feedback\u2026" })
836
899
  ]
837
900
  }
838
901
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "feedtack",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Click anywhere. Drop a pin. Get a payload a developer can act on.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -30,7 +30,7 @@
30
30
  "test:watch": "vitest",
31
31
  "lint": "biome check src/",
32
32
  "lint:fix": "biome check --fix src/",
33
- "release": "release-it",
33
+ "release": "release-it --ci",
34
34
  "prepublishOnly": "pnpm test && pnpm build",
35
35
  "prepare": "husky"
36
36
  },