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.
@@ -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 };