feedtack 0.3.0 → 0.3.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.
package/README.md CHANGED
@@ -74,6 +74,96 @@ class MySupabaseAdapter implements FeedtackAdapter {
74
74
  }
75
75
  ```
76
76
 
77
+ ## Adapter recipes
78
+
79
+ The `FeedtackAdapter` interface has five methods. Here are copy-paste implementations for common backends.
80
+
81
+ ### Disk / JSON files (Node.js)
82
+
83
+ Git-trackable feedback — each submission becomes a JSON file in `.feedback/`.
84
+
85
+ ```ts
86
+ import type { FeedtackAdapter, FeedbackItem, FeedtackPayload } from 'feedtack'
87
+ import { readdir, readFile, writeFile, mkdir } from 'node:fs/promises'
88
+ import { join } from 'node:path'
89
+
90
+ const DIR = '.feedback'
91
+
92
+ class DiskAdapter implements FeedtackAdapter {
93
+ async submit(payload: FeedtackPayload) {
94
+ await mkdir(DIR, { recursive: true })
95
+ const item: FeedbackItem = { payload, replies: [], resolutions: [], archives: [] }
96
+ await writeFile(join(DIR, `${payload.id}.json`), JSON.stringify(item, null, 2))
97
+ }
98
+
99
+ async reply(feedbackId: string, reply: Omit<FeedbackItem['replies'][0], 'id' | 'feedbackId'>) {
100
+ const item = await this.read(feedbackId)
101
+ item.replies.push({ ...reply, id: crypto.randomUUID(), feedbackId })
102
+ await this.write(feedbackId, item)
103
+ }
104
+
105
+ async resolve(feedbackId: string, resolution: Omit<FeedbackItem['resolutions'][0], 'feedbackId'>) {
106
+ const item = await this.read(feedbackId)
107
+ item.resolutions.push({ ...resolution, feedbackId })
108
+ await this.write(feedbackId, item)
109
+ }
110
+
111
+ async archive(feedbackId: string, userId: string) {
112
+ const item = await this.read(feedbackId)
113
+ item.archives.push({ feedbackId, archivedBy: { id: userId, name: '', role: '' }, timestamp: new Date().toISOString() })
114
+ await this.write(feedbackId, item)
115
+ }
116
+
117
+ async loadFeedback() {
118
+ await mkdir(DIR, { recursive: true })
119
+ const files = (await readdir(DIR)).filter((f) => f.endsWith('.json'))
120
+ return Promise.all(files.map(async (f) => JSON.parse(await readFile(join(DIR, f), 'utf-8')) as FeedbackItem))
121
+ }
122
+
123
+ private async read(id: string) { return JSON.parse(await readFile(join(DIR, `${id}.json`), 'utf-8')) as FeedbackItem }
124
+ private async write(id: string, item: FeedbackItem) { await writeFile(join(DIR, `${id}.json`), JSON.stringify(item, null, 2)) }
125
+ }
126
+ ```
127
+
128
+ ### Supabase
129
+
130
+ ```ts
131
+ import type { FeedtackAdapter, FeedbackItem, FeedtackFilter, FeedtackPayload } from 'feedtack'
132
+ import type { SupabaseClient } from '@supabase/supabase-js'
133
+
134
+ class SupabaseAdapter implements FeedtackAdapter {
135
+ constructor(private supabase: SupabaseClient) {}
136
+
137
+ async submit(payload: FeedtackPayload) {
138
+ await this.supabase.from('feedtack_submissions').insert({ id: payload.id, data: payload })
139
+ }
140
+
141
+ async reply(feedbackId: string, reply: Omit<FeedbackItem['replies'][0], 'id' | 'feedbackId'>) {
142
+ await this.supabase.from('feedtack_replies').insert({ feedback_id: feedbackId, ...reply })
143
+ }
144
+
145
+ async resolve(feedbackId: string, resolution: Omit<FeedbackItem['resolutions'][0], 'feedbackId'>) {
146
+ await this.supabase.from('feedtack_resolutions').insert({ feedback_id: feedbackId, ...resolution })
147
+ }
148
+
149
+ async archive(feedbackId: string, userId: string) {
150
+ await this.supabase.from('feedtack_archives').insert({ feedback_id: feedbackId, user_id: userId })
151
+ }
152
+
153
+ async loadFeedback(filter?: FeedtackFilter): Promise<FeedbackItem[]> {
154
+ let query = this.supabase.from('feedtack_submissions').select('*, feedtack_replies(*), feedtack_resolutions(*), feedtack_archives(*)')
155
+ if (filter?.pathname) query = query.eq('data->>page->>pathname', filter.pathname)
156
+ const { data } = await query
157
+ return (data ?? []).map((row) => ({
158
+ payload: row.data,
159
+ replies: row.feedtack_replies ?? [],
160
+ resolutions: row.feedtack_resolutions ?? [],
161
+ archives: row.feedtack_archives ?? [],
162
+ }))
163
+ }
164
+ }
165
+ ```
166
+
77
167
  ## The payload
78
168
 
79
169
  Every pin emits a versioned JSON payload:
@@ -171,7 +171,7 @@ function ThreadPanel({
171
171
  item.replies.map((r) => /* @__PURE__ */ jsxs2(
172
172
  "div",
173
173
  {
174
- style: { borderTop: "1px solid #f3f4f6", paddingTop: 8 },
174
+ style: { borderTop: "1px solid var(--ft-border)", paddingTop: 8 },
175
175
  children: [
176
176
  /* @__PURE__ */ jsx2("span", { style: { fontSize: 12, fontWeight: 600 }, children: r.author.name }),
177
177
  /* @__PURE__ */ jsx2("p", { style: { fontSize: 12 }, children: r.body })
@@ -190,7 +190,7 @@ function ThreadPanel({
190
190
  fontSize: 12,
191
191
  padding: 6,
192
192
  borderRadius: 6,
193
- border: "1px solid #e5e7eb",
193
+ border: "1px solid var(--ft-border)",
194
194
  marginTop: 4
195
195
  }
196
196
  }
@@ -534,7 +534,12 @@ function useFeedtackDom(theme, disabled) {
534
534
 
535
535
  // src/react/usePinMode.ts
536
536
  import { useCallback, useEffect as useEffect2, useState } from "react";
537
- function usePinMode({ hotkey, onDeactivate, disabled }) {
537
+ function usePinMode({
538
+ hotkey,
539
+ onDeactivate,
540
+ disabled,
541
+ isModalOpen
542
+ }) {
538
543
  const [isActive, setIsActive] = useState(false);
539
544
  const [pendingPins, setPendingPins] = useState([]);
540
545
  const [selectedColor, setSelectedColor] = useState(PIN_PALETTE[0]);
@@ -563,7 +568,7 @@ function usePinMode({ hotkey, onDeactivate, disabled }) {
563
568
  if (e.key === "Escape") {
564
569
  deactivate();
565
570
  }
566
- if (isActive && (e.key === "ArrowLeft" || e.key === "ArrowRight")) {
571
+ if (isActive && !isModalOpen && !showForm && (e.key === "ArrowLeft" || e.key === "ArrowRight")) {
567
572
  e.preventDefault();
568
573
  setSelectedColor((prev) => {
569
574
  const idx = PIN_PALETTE.indexOf(prev);
@@ -574,7 +579,7 @@ function usePinMode({ hotkey, onDeactivate, disabled }) {
574
579
  };
575
580
  window.addEventListener("keydown", handler);
576
581
  return () => window.removeEventListener("keydown", handler);
577
- }, [hotkey, deactivate, isActive, disabled]);
582
+ }, [hotkey, deactivate, isActive, disabled, isModalOpen, showForm]);
578
583
  const handlePageClick = useCallback(
579
584
  (e) => {
580
585
  if (!isActive) return;
@@ -657,6 +662,7 @@ function useFeedtackState({
657
662
  const pinMode = usePinMode({
658
663
  hotkey,
659
664
  disabled,
665
+ isModalOpen: openThreadId !== null,
660
666
  onDeactivate: () => {
661
667
  resetForm();
662
668
  setOpenThreadId(null);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "feedtack",
3
- "version": "0.3.0",
3
+ "version": "0.3.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",