feedtack 1.1.0 → 1.3.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/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 };
@@ -0,0 +1,169 @@
1
+ // src/adapters/DiskAdapter.ts
2
+ import { mkdir, readdir, readFile, writeFile } from "fs/promises";
3
+ import { join } from "path";
4
+ var DiskAdapter = class {
5
+ constructor(config = {}) {
6
+ this.dir = config.directory ?? ".feedback";
7
+ this.approvalsDir = join(this.dir, "approvals");
8
+ this.fieldsDir = join(this.dir, "fields");
9
+ }
10
+ async submit(payload) {
11
+ await mkdir(this.dir, { recursive: true });
12
+ const item = {
13
+ payload,
14
+ replies: [],
15
+ resolutions: [],
16
+ archives: []
17
+ };
18
+ await this.write(payload.id, item);
19
+ }
20
+ async reply(feedbackId, reply) {
21
+ const item = await this.read(feedbackId);
22
+ item.replies.push({
23
+ id: `r_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`,
24
+ feedbackId,
25
+ ...reply
26
+ });
27
+ await this.write(feedbackId, item);
28
+ }
29
+ async resolve(feedbackId, resolution) {
30
+ const item = await this.read(feedbackId);
31
+ item.resolutions.push({ feedbackId, ...resolution });
32
+ await this.write(feedbackId, item);
33
+ }
34
+ async archive(feedbackId, userId) {
35
+ const item = await this.read(feedbackId);
36
+ item.archives.push({
37
+ feedbackId,
38
+ archivedBy: { id: userId, name: "", role: "" },
39
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
40
+ });
41
+ await this.write(feedbackId, item);
42
+ }
43
+ async loadFeedback(filter) {
44
+ let files;
45
+ try {
46
+ await mkdir(this.dir, { recursive: true });
47
+ files = (await readdir(this.dir)).filter((f) => f.endsWith(".json"));
48
+ } catch {
49
+ return [];
50
+ }
51
+ const items = await Promise.all(
52
+ files.map(
53
+ async (f) => JSON.parse(
54
+ await readFile(join(this.dir, f), "utf-8")
55
+ )
56
+ )
57
+ );
58
+ if (!filter) return items;
59
+ return items.filter((item) => {
60
+ if (filter.scope && item.payload.scope !== filter.scope) return false;
61
+ if (filter.pathname && item.payload.page.pathname !== filter.pathname)
62
+ return false;
63
+ if (filter.url && item.payload.page.url !== filter.url) return false;
64
+ if (filter.userId && item.payload.submittedBy.id !== filter.userId)
65
+ return false;
66
+ return true;
67
+ });
68
+ }
69
+ // ContentAdapter implementation
70
+ async approve(fieldPath, approval) {
71
+ await mkdir(this.approvalsDir, { recursive: true });
72
+ const safeName = fieldPath.replace(/[^a-zA-Z0-9._-]/g, "_");
73
+ await writeFile(
74
+ join(this.approvalsDir, `${safeName}.json`),
75
+ JSON.stringify(approval, null, 2)
76
+ );
77
+ }
78
+ async revokeApproval(fieldPath, userId) {
79
+ await mkdir(this.approvalsDir, { recursive: true });
80
+ const safeName = fieldPath.replace(/[^a-zA-Z0-9._-]/g, "_");
81
+ const filePath = join(this.approvalsDir, `${safeName}.json`);
82
+ let approval;
83
+ try {
84
+ approval = JSON.parse(await readFile(filePath, "utf-8"));
85
+ } catch {
86
+ return;
87
+ }
88
+ approval.by = approval.by.filter((id) => id !== userId);
89
+ if (approval.by.length === 0) {
90
+ const { unlink } = await import("fs/promises");
91
+ await unlink(filePath).catch(() => {
92
+ });
93
+ } else {
94
+ await writeFile(filePath, JSON.stringify(approval, null, 2));
95
+ }
96
+ }
97
+ async loadApprovals(filter) {
98
+ let files;
99
+ try {
100
+ await mkdir(this.approvalsDir, { recursive: true });
101
+ files = (await readdir(this.approvalsDir)).filter(
102
+ (f) => f.endsWith(".json")
103
+ );
104
+ } catch {
105
+ return [];
106
+ }
107
+ const states = await Promise.all(
108
+ files.map(async (f) => {
109
+ const fieldPath = f.replace(/\.json$/, "").replace(/_/g, ".");
110
+ const approval = JSON.parse(
111
+ await readFile(join(this.approvalsDir, f), "utf-8")
112
+ );
113
+ return {
114
+ fieldPath,
115
+ approval,
116
+ stale: false
117
+ };
118
+ })
119
+ );
120
+ if (!filter) return states;
121
+ return states.filter((s) => {
122
+ if (filter.fieldPath && s.fieldPath !== filter.fieldPath) return false;
123
+ return true;
124
+ });
125
+ }
126
+ // ContentEditAdapter implementation
127
+ async loadFields() {
128
+ let files;
129
+ try {
130
+ await mkdir(this.fieldsDir, { recursive: true });
131
+ files = (await readdir(this.fieldsDir)).filter((f) => f.endsWith(".json"));
132
+ } catch {
133
+ return {};
134
+ }
135
+ const entries = await Promise.all(
136
+ files.map(async (f) => {
137
+ const fieldPath = f.replace(/\.json$/, "").replace(/_/g, ".");
138
+ const value = JSON.parse(
139
+ await readFile(join(this.fieldsDir, f), "utf-8")
140
+ );
141
+ return [fieldPath, value];
142
+ })
143
+ );
144
+ return Object.fromEntries(entries);
145
+ }
146
+ async saveField(fieldPath, value) {
147
+ const safeName = fieldPath.replace(/[^a-zA-Z0-9._-]/g, "_");
148
+ await mkdir(this.fieldsDir, { recursive: true });
149
+ await writeFile(
150
+ join(this.fieldsDir, `${safeName}.json`),
151
+ JSON.stringify(value, null, 2)
152
+ );
153
+ const approvalPath = join(this.approvalsDir, `${safeName}.json`);
154
+ const { unlink } = await import("fs/promises");
155
+ await unlink(approvalPath).catch(() => {
156
+ });
157
+ }
158
+ async read(id) {
159
+ return JSON.parse(
160
+ await readFile(join(this.dir, `${id}.json`), "utf-8")
161
+ );
162
+ }
163
+ async write(id, item) {
164
+ await writeFile(join(this.dir, `${id}.json`), JSON.stringify(item, null, 2));
165
+ }
166
+ };
167
+ export {
168
+ DiskAdapter
169
+ };
@@ -1,11 +1,49 @@
1
- import { F as FeedtackAdapter, t as FeedtackUser, s as FeedtackTheme, e as FeedbackItem, o as FeedtackFlushEvent } from '../types-Cu4Oahg4.js';
1
+ import { d as FeedtackTheme, c as FeedtackFlushEvent } from '../types-CHrWe7xT.js';
2
2
  import * as react_jsx_runtime from 'react/jsx-runtime';
3
+ import { h as FieldApprovalState, F as FeedtackAdapter, u as FocusedFieldInfo, t as FieldChange, s as FeedtackUser, e as FeedbackItem } from '../adapter-Cn59URIG.js';
3
4
  import React from 'react';
4
5
 
5
6
  /** Fixed palette of 6 colors for pin markers */
6
7
  declare const PIN_PALETTE: readonly ["#ef4444", "#3b82f6", "#22c55e", "#f59e0b", "#a855f7", "#ec4899"];
7
8
  type PinColor = (typeof PIN_PALETTE)[number];
8
9
 
10
+ interface DeployCheckResult {
11
+ approved: boolean;
12
+ pending: string[];
13
+ }
14
+ interface UseContentApprovalOptions {
15
+ /**
16
+ * When provided, hash computation uses stored values from this map instead of
17
+ * reading element.textContent from the DOM. Use when static-build values may
18
+ * differ from live stored values (e.g. when used alongside useContentEdit).
19
+ */
20
+ storedValues?: Map<string, string>;
21
+ }
22
+ interface UseContentApprovalResult {
23
+ fields: FieldApprovalState[];
24
+ approve: (fieldPath: string) => Promise<void>;
25
+ revoke: (fieldPath: string) => Promise<void>;
26
+ rescan: () => Promise<void>;
27
+ checkDeploy: () => Promise<DeployCheckResult>;
28
+ }
29
+ /**
30
+ * Hook for managing content field approvals.
31
+ * Requires an adapter that implements ContentAdapter.
32
+ */
33
+ declare function useContentApproval(adapter: FeedtackAdapter, userId: string, options?: UseContentApprovalOptions): UseContentApprovalResult;
34
+
35
+ interface ContentEditToolbarProps {
36
+ focusedField: FocusedFieldInfo | null;
37
+ approvalState: FieldApprovalState | null;
38
+ changes: FieldChange[];
39
+ saving: string | null;
40
+ onApprove: (fieldPath: string) => Promise<void>;
41
+ onRevoke: (fieldPath: string) => Promise<void>;
42
+ onRevert: (fieldPath: string) => Promise<void>;
43
+ onCheckDeploy: () => Promise<DeployCheckResult>;
44
+ }
45
+ declare function ContentEditToolbar({ focusedField, approvalState, changes, saving, onApprove, onRevoke, onRevert, onCheckDeploy, }: ContentEditToolbarProps): react_jsx_runtime.JSX.Element;
46
+
9
47
  interface FeedtackContextValue {
10
48
  activatePinMode: () => void;
11
49
  deactivatePinMode: () => void;
@@ -48,10 +86,31 @@ interface FeedtackProviderProps {
48
86
  flushIdleMs?: number;
49
87
  /** User roles that trigger re-scope on reply (default: any non-'agent' role) */
50
88
  rescopeRoles?: string[];
89
+ /**
90
+ * Called by the consumer (e.g. on a Deploy button click) to check whether all
91
+ * content fields have current approvals. Feedtack surfaces the data; the consumer
92
+ * decides what to do with the result.
93
+ */
94
+ onDeployCheck?: () => Promise<{
95
+ approved: boolean;
96
+ pending: string[];
97
+ }>;
51
98
  }
52
99
  declare function FeedtackProvider({ children, adapter, currentUser, hotkey, adminOnly, theme, classes, sentimentLabels, onError, disabled, renderPinIcon, onFlush, flushIdleMs, rescopeRoles, }: FeedtackProviderProps): react_jsx_runtime.JSX.Element;
53
100
 
101
+ interface UseContentEditResult extends UseContentApprovalResult {
102
+ active: boolean;
103
+ activate: () => Promise<void>;
104
+ deactivate: () => void;
105
+ changes: FieldChange[];
106
+ revert: (fieldPath: string) => Promise<void>;
107
+ saving: string | null;
108
+ focusedField: FocusedFieldInfo | null;
109
+ toolbarProps: ContentEditToolbarProps;
110
+ }
111
+ declare function useContentEdit(adapter: FeedtackAdapter, userId: string): UseContentEditResult;
112
+
54
113
  /** Hook for host app to programmatically control feedtack */
55
114
  declare function useFeedtack(): FeedtackContextValue;
56
115
 
57
- export { type FeedtackClasses, type FeedtackContextValue, FeedtackFlushEvent, FeedtackProvider, type FeedtackProviderProps, type FeedtackSentimentLabels, PIN_PALETTE, type PinColor, useFeedtack };
116
+ export { ContentEditToolbar, type ContentEditToolbarProps, type DeployCheckResult, type FeedtackClasses, type FeedtackContextValue, FeedtackFlushEvent, FeedtackProvider, type FeedtackProviderProps, type FeedtackSentimentLabels, PIN_PALETTE, type PinColor, type UseContentApprovalOptions, type UseContentApprovalResult, type UseContentEditResult, useContentApproval, useContentEdit, useFeedtack };