feedtack 1.1.0 → 1.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.
package/README.md CHANGED
@@ -80,49 +80,12 @@ The `FeedtackAdapter` interface has five methods. Here are copy-paste implementa
80
80
 
81
81
  ### Disk / JSON files (Node.js)
82
82
 
83
- Git-trackable feedback — each submission becomes a JSON file in `.feedback/`.
83
+ Git-trackable feedback — each submission becomes a JSON file in `.feedback/`. `DiskAdapter` ships with the package and also implements `ContentAdapter` + `ContentEditAdapter`.
84
84
 
85
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'
86
+ import { DiskAdapter } from 'feedtack/node'
89
87
 
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
- }
88
+ const adapter = new DiskAdapter({ directory: '.feedback' }) // default: '.feedback'
126
89
  ```
127
90
 
128
91
  ### Supabase
@@ -243,6 +206,51 @@ function MyButton() {
243
206
  }
244
207
  ```
245
208
 
209
+ ## Content approval (opt-in)
210
+
211
+ Feedtack can also track whether copywriting fields on a page have been reviewed and approved. Annotate elements with `data-feedtack-field`, use an adapter that implements `ContentAdapter` (`DiskAdapter` and `WebhookAdapter` both do), and call the hook:
212
+
213
+ ```tsx
214
+ <h1 data-feedtack-field="hero.heading">Welcome to Acme</h1>
215
+ <p data-feedtack-field="hero.subheading">The best tools for your team.</p>
216
+ ```
217
+
218
+ ```tsx
219
+ import { useContentApproval } from 'feedtack/react'
220
+
221
+ const { fields, approve, checkDeploy } = useContentApproval(adapter, currentUser.id)
222
+
223
+ // Gate deploys on all fields being approved
224
+ const result = await checkDeploy()
225
+ // => { approved: false, pending: ['hero.subheading'] }
226
+ ```
227
+
228
+ Approvals are hash-based — if the content changes, the approval goes stale automatically. See the [Content Approval docs](site-docs/content/docs/concepts/content-approval.mdx) for the full API.
229
+
230
+ ## Content editing (opt-in)
231
+
232
+ For teams that want to edit content inline, `useContentEdit` builds on top of content approval. It hydrates the DOM with stored values, makes annotated fields `contenteditable`, and auto-saves on blur. The adapter must implement `ContentEditAdapter` (`DiskAdapter` and `WebhookAdapter` both do).
233
+
234
+ ```tsx
235
+ import { useContentEdit, ContentEditToolbar } from 'feedtack/react'
236
+
237
+ function AdminLayout({ adapter, children }) {
238
+ const edit = useContentEdit(adapter, currentUser.id)
239
+
240
+ return (
241
+ <>
242
+ {children}
243
+ {edit.active && <ContentEditToolbar {...edit.toolbarProps} />}
244
+ <button onClick={edit.active ? edit.deactivate : edit.activate}>
245
+ {edit.active ? 'Exit edit mode' : 'Edit content'}
246
+ </button>
247
+ </>
248
+ )
249
+ }
250
+ ```
251
+
252
+ The toolbar shows approve/revert actions per field, a session changes panel, and a deploy gate. See the [Content Editing docs](site-docs/content/docs/concepts/content-editing.mdx) for details.
253
+
246
254
  ## What feedtack does NOT do
247
255
 
248
256
  - LLM triage or routing (downstream concern — feedtack emits, others act)
@@ -251,6 +259,7 @@ function MyButton() {
251
259
 
252
260
  ## ICEBOX
253
261
 
262
+ - Vanilla JS content editing (no React dependency)
254
263
  - Script tag CDN distribution
255
264
  - Next.js plugin
256
265
  - `allowedCaptures` config for scoping DOM access
@@ -132,7 +132,74 @@ interface FeedtackFilter {
132
132
  userId?: string;
133
133
  scope?: FeedtackScope;
134
134
  }
135
+ /** Recorded approval for a content field — keyed by dot-path (e.g. "hero.heading") */
136
+ interface FieldApproval {
137
+ /** 12-char truncated SHA-256 of the approved content */
138
+ hash: string;
139
+ /** User IDs who have approved this field */
140
+ by: string[];
141
+ /** ISO 8601 UTC timestamp of most recent approval */
142
+ at: string;
143
+ }
144
+ /** Combined field identity, approval record, and staleness flag */
145
+ interface FieldApprovalState {
146
+ /** Dot-path field identifier (value of data-feedtack-field attribute) */
147
+ fieldPath: string;
148
+ /** Null when the field has never been approved */
149
+ approval: FieldApproval | null;
150
+ /** True when the current content hash differs from the stored approval hash */
151
+ stale: boolean;
152
+ }
153
+ interface FieldFilter {
154
+ pathname?: string;
155
+ fieldPath?: string;
156
+ }
157
+ /** A single field edit recorded in the current editing session */
158
+ interface FieldChange {
159
+ fieldPath: string;
160
+ /** Value at session start (pre-first-edit for this field) */
161
+ from: string;
162
+ /** Most recent saved value */
163
+ to: string;
164
+ /** Unix ms timestamp of most recent save */
165
+ savedAt: number;
166
+ }
167
+ /** Info about the currently focused content field — used by the edit toolbar */
168
+ interface FocusedFieldInfo {
169
+ element: HTMLElement;
170
+ fieldPath: string;
171
+ }
135
172
 
173
+ /**
174
+ * Optional extension interface for adapters that support content field approvals.
175
+ * Implement alongside FeedtackAdapter — adapters that omit this continue to work unchanged.
176
+ */
177
+ interface ContentAdapter {
178
+ /** Record approval for a field at its current hash */
179
+ approve(fieldPath: string, approval: FieldApproval): Promise<void>;
180
+ /** Remove approval for a field by a specific user */
181
+ revokeApproval(fieldPath: string, userId: string): Promise<void>;
182
+ /** Load approval states, optionally filtered */
183
+ loadApprovals(filter?: FieldFilter): Promise<FieldApprovalState[]>;
184
+ }
185
+ /**
186
+ * Optional extension of ContentAdapter for adapters that support inline content editing.
187
+ * saveField MUST atomically persist the new value AND clear the stored approval.
188
+ */
189
+ interface ContentEditAdapter extends ContentAdapter {
190
+ /** Fetch all stored field values for DOM hydration — keyed by dot-path */
191
+ loadFields(): Promise<Record<string, string>>;
192
+ /** Persist a field edit. MUST clear the stored approval for the field. */
193
+ saveField(fieldPath: string, value: string): Promise<void>;
194
+ }
195
+ /** Returns true if the adapter implements ContentEditAdapter */
196
+ declare function isContentEditAdapter(adapter: unknown): adapter is ContentEditAdapter;
197
+ /** Warn in dev mode when an edit method is called on a non-ContentEditAdapter */
198
+ declare function warnIfNotContentEditAdapter(adapter: unknown, method: string): void;
199
+ /** Returns true if the adapter implements ContentAdapter */
200
+ declare function isContentAdapter(adapter: unknown): adapter is ContentAdapter;
201
+ /** Warn in dev mode when a ContentAdapter method is called on a non-ContentAdapter */
202
+ declare function warnIfNotContentAdapter(adapter: unknown, method: string): void;
136
203
  /** Plugin contract — implement this interface to create a custom feedtack backend */
137
204
  interface FeedtackAdapter {
138
205
  /** Submit new feedback payload */
@@ -147,62 +214,4 @@ interface FeedtackAdapter {
147
214
  loadFeedback(filter?: FeedtackFilter): Promise<FeedbackItem[]>;
148
215
  }
149
216
 
150
- interface FeedtackTheme {
151
- /** Accent color — pin button active state, focus rings, selected states */
152
- primary?: string;
153
- /** Panel and picker background */
154
- background?: string;
155
- /** Input and card surface background */
156
- surface?: string;
157
- /** Primary text color */
158
- text?: string;
159
- /** Secondary/placeholder text color */
160
- textMuted?: string;
161
- /** Panel and input border color */
162
- border?: string;
163
- /** Border radius applied to panels and inputs */
164
- radius?: string;
165
- /** Notification badge color */
166
- badge?: string;
167
- }
168
- /** Maps FeedtackTheme fields to CSS custom properties on #feedtack-root */
169
- declare function themeToCSS(theme: FeedtackTheme): Record<string, string>;
170
-
171
- declare function generateId(): string;
172
- interface FeedtackFlushEvent {
173
- pathname: string;
174
- items: FeedbackItem[];
175
- }
176
- interface FeedtackEngineOpts {
177
- adapter: FeedtackAdapter;
178
- currentUser: FeedtackUser;
179
- hotkey?: string;
180
- theme?: FeedtackTheme;
181
- onError?: (err: Error) => void;
182
- disabled?: boolean;
183
- onFlush?: (event: FeedtackFlushEvent) => void;
184
- flushIdleMs?: number;
185
- rescopeRoles?: string[];
186
- }
187
- interface FeedtackEngineState {
188
- isPinModeActive: boolean;
189
- pendingPins: Array<Omit<FeedtackPin, 'index'>>;
190
- selectedColor: string;
191
- showForm: boolean;
192
- comment: string;
193
- sentiment: FeedtackSentiment;
194
- commentError: boolean;
195
- submitting: boolean;
196
- feedbackItems: FeedbackItem[];
197
- siteFeedback: FeedbackItem[];
198
- pageFeedback: FeedbackItem[];
199
- loading: boolean;
200
- openThreadId: string | null;
201
- replyBody: string;
202
- isModalOpen: boolean;
203
- composeScope: 'site' | 'page';
204
- pathname: string;
205
- }
206
- type FeedtackStateListener = (state: FeedtackEngineState, changedKeys: Array<keyof FeedtackEngineState>) => void;
207
-
208
- export { type AncestorNode as A, type FeedtackAdapter as F, SCHEMA_VERSION as S, type FeedtackPayload as a, type FeedtackReply as b, type FeedtackResolution as c, type FeedtackFilter as d, type FeedbackItem as e, type FeedtackDeviceMeta as f, type FeedtackPageMeta as g, type FeedtackViewportMeta as h, type FeedtackPinTarget as i, type FeedtackEngineOpts as j, type FeedtackEngineState as k, type FeedtackStateListener as l, type FeedtackArchive as m, type FeedtackBoundingRect as n, type FeedtackFlushEvent as o, type FeedtackPin as p, type FeedtackScope as q, type FeedtackSentiment as r, type FeedtackTheme as s, type FeedtackUser as t, generateId as u, themeToCSS as v };
217
+ export { type AncestorNode as A, type ContentAdapter as C, type FeedtackAdapter as F, SCHEMA_VERSION as S, type FeedtackPayload as a, type FeedtackReply as b, type FeedtackResolution as c, type FeedtackFilter as d, type FeedbackItem as e, type ContentEditAdapter as f, type FieldFilter as g, type FieldApprovalState as h, type FieldApproval as i, type FeedtackDeviceMeta as j, type FeedtackPageMeta as k, type FeedtackViewportMeta as l, type FeedtackPinTarget as m, type FeedtackArchive as n, type FeedtackBoundingRect as o, type FeedtackPin as p, type FeedtackScope as q, type FeedtackSentiment as r, type FeedtackUser as s, type FieldChange as t, type FocusedFieldInfo as u, isContentAdapter as v, isContentEditAdapter as w, warnIfNotContentAdapter as x, warnIfNotContentEditAdapter as y };
@@ -1,3 +1,40 @@
1
+ // src/capture/content.ts
2
+ var DEV = typeof process !== "undefined" && process.env.NODE_ENV !== "production";
3
+ function scanFields(root) {
4
+ const searchRoot = root ?? document.body;
5
+ const nodes = Array.from(
6
+ searchRoot.querySelectorAll("[data-feedtack-field]")
7
+ );
8
+ const seen = /* @__PURE__ */ new Map();
9
+ const fields = [];
10
+ for (const el of nodes) {
11
+ const fieldPath = el.dataset.feedtackField ?? "";
12
+ if (!fieldPath) continue;
13
+ seen.set(fieldPath, (seen.get(fieldPath) ?? 0) + 1);
14
+ fields.push({
15
+ fieldPath,
16
+ element: el,
17
+ content: el.textContent ?? ""
18
+ });
19
+ }
20
+ if (DEV) {
21
+ for (const [path, count] of seen) {
22
+ if (count > 1) {
23
+ console.warn(
24
+ `[feedtack] Duplicate data-feedtack-field="${path}" found ${count} times on this page. Field paths must be unique.`
25
+ );
26
+ }
27
+ }
28
+ }
29
+ return fields;
30
+ }
31
+ async function hashField(content) {
32
+ const encoded = new TextEncoder().encode(content);
33
+ const buffer = await crypto.subtle.digest("SHA-256", encoded);
34
+ const hex = Array.from(new Uint8Array(buffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
35
+ return hex.slice(0, 12);
36
+ }
37
+
1
38
  // src/capture/meta.ts
2
39
  function getViewportMeta() {
3
40
  return {
@@ -406,6 +443,8 @@ var FEEDTACK_MODAL_STYLES = `
406
443
  right: 24px;
407
444
  width: 360px;
408
445
  max-height: 70vh;
446
+ margin: 0;
447
+ padding: 0;
409
448
  background: var(--ft-bg);
410
449
  border: 1px solid var(--ft-border);
411
450
  border-radius: calc(var(--ft-radius) + 4px);
@@ -416,6 +455,10 @@ var FEEDTACK_MODAL_STYLES = `
416
455
  overflow: hidden;
417
456
  }
418
457
 
458
+ .feedtack-modal::backdrop {
459
+ background: transparent;
460
+ }
461
+
419
462
  .feedtack-modal-header {
420
463
  display: flex;
421
464
  align-items: center;
@@ -621,6 +664,7 @@ var FEEDTACK_MODAL_STYLES = `
621
664
  bottom: 64px;
622
665
  width: 100vw;
623
666
  max-height: 85vh;
667
+ margin: 0;
624
668
  border-radius: var(--ft-radius) var(--ft-radius) 0 0;
625
669
  border-left: none;
626
670
  border-right: none;
@@ -1220,7 +1264,32 @@ var FeedtackEngine = class {
1220
1264
  }
1221
1265
  };
1222
1266
 
1267
+ // src/types/adapter.ts
1268
+ function isContentEditAdapter(adapter) {
1269
+ return isContentAdapter(adapter) && typeof adapter.loadFields === "function" && typeof adapter.saveField === "function";
1270
+ }
1271
+ function warnIfNotContentEditAdapter(adapter, method) {
1272
+ if (DEV2 && !isContentEditAdapter(adapter)) {
1273
+ console.warn(
1274
+ `[feedtack] ${method}() called but the adapter does not implement ContentEditAdapter. Content editing features are unavailable with this adapter.`
1275
+ );
1276
+ }
1277
+ }
1278
+ var DEV2 = typeof process !== "undefined" && process.env.NODE_ENV !== "production";
1279
+ function isContentAdapter(adapter) {
1280
+ return typeof adapter === "object" && adapter !== null && typeof adapter.approve === "function" && typeof adapter.revokeApproval === "function" && typeof adapter.loadApprovals === "function";
1281
+ }
1282
+ function warnIfNotContentAdapter(adapter, method) {
1283
+ if (DEV2 && !isContentAdapter(adapter)) {
1284
+ console.warn(
1285
+ `[feedtack] ${method}() called but the adapter does not implement ContentAdapter. Content approval features are unavailable with this adapter.`
1286
+ );
1287
+ }
1288
+ }
1289
+
1223
1290
  export {
1291
+ scanFields,
1292
+ hashField,
1224
1293
  getViewportMeta,
1225
1294
  getPageMeta,
1226
1295
  getDeviceMeta,
@@ -1231,5 +1300,9 @@ export {
1231
1300
  SCHEMA_VERSION,
1232
1301
  generateId,
1233
1302
  themeToCSS,
1234
- FeedtackEngine
1303
+ FeedtackEngine,
1304
+ isContentEditAdapter,
1305
+ warnIfNotContentEditAdapter,
1306
+ isContentAdapter,
1307
+ warnIfNotContentAdapter
1235
1308
  };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
- import { F as FeedtackAdapter, a as FeedtackPayload, b as FeedtackReply, c as FeedtackResolution, d as FeedtackFilter, e as FeedbackItem, f as FeedtackDeviceMeta, g as FeedtackPageMeta, h as FeedtackViewportMeta, i as FeedtackPinTarget, j as FeedtackEngineOpts, k as FeedtackEngineState, l as FeedtackStateListener } from './types-Cu4Oahg4.js';
2
- export { A as AncestorNode, m as FeedtackArchive, n as FeedtackBoundingRect, o as FeedtackFlushEvent, p as FeedtackPin, q as FeedtackScope, r as FeedtackSentiment, s as FeedtackTheme, t as FeedtackUser, S as SCHEMA_VERSION, u as generateId, v as themeToCSS } from './types-Cu4Oahg4.js';
1
+ import { F as FeedtackAdapter, a as FeedtackPayload, b as FeedtackReply, c as FeedtackResolution, d as FeedtackFilter, e as FeedbackItem, C as ContentAdapter, f as ContentEditAdapter, g as FieldFilter, h as FieldApprovalState, i as FieldApproval, j as FeedtackDeviceMeta, k as FeedtackPageMeta, l as FeedtackViewportMeta, m as FeedtackPinTarget } from './adapter-Cn59URIG.js';
2
+ export { A as AncestorNode, n as FeedtackArchive, o as FeedtackBoundingRect, p as FeedtackPin, q as FeedtackScope, r as FeedtackSentiment, s as FeedtackUser, t as FieldChange, u as FocusedFieldInfo, S as SCHEMA_VERSION, v as isContentAdapter, w as isContentEditAdapter, x as warnIfNotContentAdapter, y as warnIfNotContentEditAdapter } from './adapter-Cn59URIG.js';
3
+ import { F as FeedtackEngineOpts, a as FeedtackEngineState, b as FeedtackStateListener } from './types-CHrWe7xT.js';
4
+ export { c as FeedtackFlushEvent, d as FeedtackTheme, g as generateId, t as themeToCSS } from './types-CHrWe7xT.js';
3
5
 
4
6
  /** Development adapter — logs all operations to the browser console */
5
7
  declare class ConsoleAdapter implements FeedtackAdapter {
@@ -10,23 +12,6 @@ declare class ConsoleAdapter implements FeedtackAdapter {
10
12
  loadFeedback(_filter?: FeedtackFilter): Promise<FeedbackItem[]>;
11
13
  }
12
14
 
13
- interface DiskAdapterConfig {
14
- /** Directory to store JSON files in. Default: '.feedback' */
15
- directory?: string;
16
- }
17
- /** Node.js adapter — persists each feedback item as a JSON file on disk */
18
- declare class DiskAdapter implements FeedtackAdapter {
19
- private dir;
20
- constructor(config?: DiskAdapterConfig);
21
- submit(payload: FeedtackPayload): Promise<void>;
22
- reply(feedbackId: string, reply: Omit<FeedtackReply, 'id' | 'feedbackId'>): Promise<void>;
23
- resolve(feedbackId: string, resolution: Omit<FeedtackResolution, 'feedbackId'>): Promise<void>;
24
- archive(feedbackId: string, userId: string): Promise<void>;
25
- loadFeedback(filter?: FeedtackFilter): Promise<FeedbackItem[]>;
26
- private read;
27
- private write;
28
- }
29
-
30
15
  interface LocalStorageAdapterConfig {
31
16
  /** localStorage key. Default: 'feedtack' */
32
17
  key?: string;
@@ -51,9 +36,13 @@ interface WebhookAdapterConfig {
51
36
  updateUrl?: string;
52
37
  /** Required: async function that returns persisted feedback items */
53
38
  loadFeedback: (filter?: FeedtackFilter) => Promise<FeedbackItem[]>;
39
+ /** Optional: async function that returns stored field approvals */
40
+ loadApprovals?: (filter?: FieldFilter) => Promise<FieldApprovalState[]>;
41
+ /** Optional: async function that returns all stored field values for hydration */
42
+ loadFields?: () => Promise<Record<string, string>>;
54
43
  }
55
44
  /** Production adapter — POSTs feedback as JSON to a webhook endpoint */
56
- declare class WebhookAdapter implements FeedtackAdapter {
45
+ declare class WebhookAdapter implements FeedtackAdapter, ContentAdapter, ContentEditAdapter {
57
46
  private config;
58
47
  constructor(config: WebhookAdapterConfig);
59
48
  private post;
@@ -62,7 +51,27 @@ declare class WebhookAdapter implements FeedtackAdapter {
62
51
  resolve(feedbackId: string, resolution: Omit<FeedtackResolution, 'feedbackId'>): Promise<void>;
63
52
  archive(feedbackId: string, userId: string): Promise<void>;
64
53
  loadFeedback(filter?: FeedtackFilter): Promise<FeedbackItem[]>;
54
+ approve(fieldPath: string, approval: FieldApproval): Promise<void>;
55
+ revokeApproval(fieldPath: string, userId: string): Promise<void>;
56
+ loadApprovals(filter?: FieldFilter): Promise<FieldApprovalState[]>;
57
+ loadFields(): Promise<Record<string, string>>;
58
+ saveField(fieldPath: string, value: string): Promise<void>;
59
+ }
60
+
61
+ interface ScannedField {
62
+ fieldPath: string;
63
+ element: Element;
64
+ content: string;
65
65
  }
66
+ /**
67
+ * Scans the DOM for elements annotated with data-feedtack-field.
68
+ * @param root - Element to search within. Defaults to document.body.
69
+ */
70
+ declare function scanFields(root?: Element): ScannedField[];
71
+ /**
72
+ * Computes a 12-character truncated SHA-256 hash of a string using Web Crypto API.
73
+ */
74
+ declare function hashField(content: string): Promise<string>;
66
75
 
67
76
  declare function getViewportMeta(): FeedtackViewportMeta;
68
77
  declare function getPageMeta(): FeedtackPageMeta;
@@ -127,4 +136,4 @@ declare class FeedtackEngine {
127
136
  handleArchive(id: string): Promise<void>;
128
137
  }
129
138
 
130
- export { ConsoleAdapter, DiskAdapter, type DiskAdapterConfig, FeedbackItem, FeedtackAdapter, FeedtackDeviceMeta, FeedtackEngine, FeedtackEngineOpts, FeedtackEngineState, FeedtackFilter, FeedtackPageMeta, FeedtackPayload, FeedtackPinTarget, FeedtackReply, FeedtackResolution, FeedtackStateListener, FeedtackViewportMeta, LocalStorageAdapter, type LocalStorageAdapterConfig, WebhookAdapter, type WebhookAdapterConfig, getCSSSelector, getDeviceMeta, getPageMeta, getPinCoords, getTargetMeta, getViewportMeta };
139
+ export { ConsoleAdapter, ContentAdapter, ContentEditAdapter, FeedbackItem, FeedtackAdapter, FeedtackDeviceMeta, FeedtackEngine, FeedtackEngineOpts, FeedtackEngineState, FeedtackFilter, FeedtackPageMeta, FeedtackPayload, FeedtackPinTarget, FeedtackReply, FeedtackResolution, FeedtackStateListener, FeedtackViewportMeta, FieldApproval, FieldApprovalState, FieldFilter, LocalStorageAdapter, type LocalStorageAdapterConfig, WebhookAdapter, type WebhookAdapterConfig, getCSSSelector, getDeviceMeta, getPageMeta, getPinCoords, getTargetMeta, getViewportMeta, hashField, scanFields };
package/dist/index.js CHANGED
@@ -8,8 +8,14 @@ import {
8
8
  getPinCoords,
9
9
  getTargetMeta,
10
10
  getViewportMeta,
11
- themeToCSS
12
- } from "./chunk-3INDOI4N.js";
11
+ hashField,
12
+ isContentAdapter,
13
+ isContentEditAdapter,
14
+ scanFields,
15
+ themeToCSS,
16
+ warnIfNotContentAdapter,
17
+ warnIfNotContentEditAdapter
18
+ } from "./chunk-GD2SY64K.js";
13
19
 
14
20
  // src/adapters/ConsoleAdapter.ts
15
21
  var ConsoleAdapter = class {
@@ -31,85 +37,6 @@ var ConsoleAdapter = class {
31
37
  }
32
38
  };
33
39
 
34
- // src/adapters/DiskAdapter.ts
35
- import { readdir, readFile, writeFile, mkdir } from "fs/promises";
36
- import { join } from "path";
37
- var DiskAdapter = class {
38
- constructor(config = {}) {
39
- this.dir = config.directory ?? ".feedback";
40
- }
41
- async submit(payload) {
42
- await mkdir(this.dir, { recursive: true });
43
- const item = {
44
- payload,
45
- replies: [],
46
- resolutions: [],
47
- archives: []
48
- };
49
- await this.write(payload.id, item);
50
- }
51
- async reply(feedbackId, reply) {
52
- const item = await this.read(feedbackId);
53
- item.replies.push({
54
- id: `r_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`,
55
- feedbackId,
56
- ...reply
57
- });
58
- await this.write(feedbackId, item);
59
- }
60
- async resolve(feedbackId, resolution) {
61
- const item = await this.read(feedbackId);
62
- item.resolutions.push({ feedbackId, ...resolution });
63
- await this.write(feedbackId, item);
64
- }
65
- async archive(feedbackId, userId) {
66
- const item = await this.read(feedbackId);
67
- item.archives.push({
68
- feedbackId,
69
- archivedBy: { id: userId, name: "", role: "" },
70
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
71
- });
72
- await this.write(feedbackId, item);
73
- }
74
- async loadFeedback(filter) {
75
- let files;
76
- try {
77
- await mkdir(this.dir, { recursive: true });
78
- files = (await readdir(this.dir)).filter((f) => f.endsWith(".json"));
79
- } catch {
80
- return [];
81
- }
82
- const items = await Promise.all(
83
- files.map(
84
- async (f) => JSON.parse(
85
- await readFile(join(this.dir, f), "utf-8")
86
- )
87
- )
88
- );
89
- if (!filter) return items;
90
- return items.filter((item) => {
91
- if (filter.scope && item.payload.scope !== filter.scope) return false;
92
- if (filter.pathname && item.payload.page.pathname !== filter.pathname)
93
- return false;
94
- if (filter.url && item.payload.page.url !== filter.url) return false;
95
- if (filter.userId && item.payload.submittedBy.id !== filter.userId)
96
- return false;
97
- return true;
98
- });
99
- }
100
- async read(id) {
101
- return JSON.parse(
102
- await readFile(join(this.dir, `${id}.json`), "utf-8")
103
- );
104
- }
105
- async write(id, item) {
106
- await writeFile(
107
- join(this.dir, `${id}.json`),
108
- JSON.stringify(item, null, 2)
109
- );
110
- }
111
- };
112
-
113
40
  // src/adapters/LocalStorageAdapter.ts
114
41
  var LocalStorageAdapter = class {
115
42
  constructor(config = {}) {
@@ -213,10 +140,40 @@ var WebhookAdapter = class {
213
140
  async loadFeedback(filter) {
214
141
  return this.config.loadFeedback(filter);
215
142
  }
143
+ // ContentAdapter implementation
144
+ async approve(fieldPath, approval) {
145
+ const url = this.config.updateUrl ?? this.config.submitUrl;
146
+ await this.post(url, { type: "approve", fieldPath, ...approval });
147
+ }
148
+ async revokeApproval(fieldPath, userId) {
149
+ const url = this.config.updateUrl ?? this.config.submitUrl;
150
+ await this.post(url, { type: "revoke", fieldPath, userId });
151
+ }
152
+ async loadApprovals(filter) {
153
+ if (this.config.loadApprovals) {
154
+ return this.config.loadApprovals(filter);
155
+ }
156
+ return [];
157
+ }
158
+ // ContentEditAdapter implementation
159
+ async loadFields() {
160
+ if (this.config.loadFields) {
161
+ return this.config.loadFields();
162
+ }
163
+ return {};
164
+ }
165
+ async saveField(fieldPath, value) {
166
+ const url = this.config.updateUrl ?? this.config.submitUrl;
167
+ await this.post(url, {
168
+ type: "save-field",
169
+ fieldPath,
170
+ value,
171
+ clearApproval: true
172
+ });
173
+ }
216
174
  };
217
175
  export {
218
176
  ConsoleAdapter,
219
- DiskAdapter,
220
177
  FeedtackEngine,
221
178
  LocalStorageAdapter,
222
179
  SCHEMA_VERSION,
@@ -228,5 +185,11 @@ export {
228
185
  getPinCoords,
229
186
  getTargetMeta,
230
187
  getViewportMeta,
231
- themeToCSS
188
+ hashField,
189
+ isContentAdapter,
190
+ isContentEditAdapter,
191
+ scanFields,
192
+ themeToCSS,
193
+ warnIfNotContentAdapter,
194
+ warnIfNotContentEditAdapter
232
195
  };
@@ -0,0 +1,27 @@
1
+ import { F as FeedtackAdapter, C as ContentAdapter, f as ContentEditAdapter, a as FeedtackPayload, b as FeedtackReply, c as FeedtackResolution, d as FeedtackFilter, e as FeedbackItem, i as FieldApproval, g as FieldFilter, h as FieldApprovalState } from '../adapter-Cn59URIG.js';
2
+
3
+ interface DiskAdapterConfig {
4
+ /** Directory to store JSON files in. Default: '.feedback' */
5
+ directory?: string;
6
+ }
7
+ /** Node.js adapter — persists each feedback item as a JSON file on disk */
8
+ declare class DiskAdapter implements FeedtackAdapter, ContentAdapter, ContentEditAdapter {
9
+ private dir;
10
+ private approvalsDir;
11
+ private fieldsDir;
12
+ constructor(config?: DiskAdapterConfig);
13
+ submit(payload: FeedtackPayload): Promise<void>;
14
+ reply(feedbackId: string, reply: Omit<FeedtackReply, 'id' | 'feedbackId'>): Promise<void>;
15
+ resolve(feedbackId: string, resolution: Omit<FeedtackResolution, 'feedbackId'>): Promise<void>;
16
+ archive(feedbackId: string, userId: string): Promise<void>;
17
+ loadFeedback(filter?: FeedtackFilter): Promise<FeedbackItem[]>;
18
+ approve(fieldPath: string, approval: FieldApproval): Promise<void>;
19
+ revokeApproval(fieldPath: string, userId: string): Promise<void>;
20
+ loadApprovals(filter?: FieldFilter): Promise<FieldApprovalState[]>;
21
+ loadFields(): Promise<Record<string, string>>;
22
+ saveField(fieldPath: string, value: string): Promise<void>;
23
+ private read;
24
+ private write;
25
+ }
26
+
27
+ export { DiskAdapter, type DiskAdapterConfig };