feedtack 1.0.1 → 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/{theme-BdqpMipn.d.ts → adapter-Cn59URIG.d.ts} +68 -22
- package/dist/chunk-GD2SY64K.js +1308 -0
- package/dist/index.d.ts +75 -4
- package/dist/index.js +50 -3
- package/dist/node/index.d.ts +27 -0
- package/dist/node/index.js +169 -0
- package/dist/react/index.d.ts +61 -7
- package/dist/react/index.js +911 -1204
- package/dist/types-CHrWe7xT.d.ts +61 -0
- package/package.json +5 -1
- package/dist/chunk-2A5LLDLP.js +0 -237
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,25 +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
|
-
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 FeedtackArchive as j, type FeedtackBoundingRect as k, type FeedtackPin as l, type FeedtackScope as m, type FeedtackSentiment as n, type FeedtackTheme as o, type FeedtackUser as p, themeToCSS as t };
|
|
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 };
|