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 +49 -40
- package/dist/{types-Cu4Oahg4.d.ts → adapter-Cn59URIG.d.ts} +68 -59
- package/dist/{chunk-3INDOI4N.js → chunk-GD2SY64K.js} +74 -1
- package/dist/index.d.ts +30 -21
- package/dist/index.js +46 -83
- package/dist/node/index.d.ts +27 -0
- package/dist/node/index.js +169 -0
- package/dist/react/index.d.ts +61 -2
- package/dist/react/index.js +830 -232
- package/dist/types-CHrWe7xT.d.ts +61 -0
- package/package.json +5 -1
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
|
|
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
|
|
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
|
-
|
|
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
|
|
2
|
-
export { A as AncestorNode,
|
|
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,
|
|
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
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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 };
|