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.
@@ -1,7 +1,265 @@
1
1
  import {
2
2
  FeedtackEngine,
3
- PIN_PALETTE
4
- } from "../chunk-3INDOI4N.js";
3
+ PIN_PALETTE,
4
+ hashField,
5
+ isContentAdapter,
6
+ isContentEditAdapter,
7
+ scanFields,
8
+ warnIfNotContentAdapter,
9
+ warnIfNotContentEditAdapter
10
+ } from "../chunk-GD2SY64K.js";
11
+
12
+ // src/react/ContentEditToolbar.tsx
13
+ import { useState } from "react";
14
+
15
+ // src/react/toolbarStyles.ts
16
+ var styles = {
17
+ toolbar: {
18
+ position: "fixed",
19
+ bottom: 16,
20
+ right: 16,
21
+ zIndex: 9999,
22
+ background: "#1a1614",
23
+ border: "1px solid #3a3330",
24
+ borderRadius: 8,
25
+ padding: "8px 12px",
26
+ display: "flex",
27
+ alignItems: "center",
28
+ gap: 8,
29
+ fontFamily: "monospace",
30
+ fontSize: 13,
31
+ color: "#e5e0d8",
32
+ boxShadow: "0 4px 24px rgba(0,0,0,0.5)",
33
+ flexWrap: "wrap",
34
+ maxWidth: 480
35
+ },
36
+ fieldSection: {
37
+ display: "flex",
38
+ alignItems: "center",
39
+ gap: 6,
40
+ borderRight: "1px solid #3a3330",
41
+ paddingRight: 8
42
+ },
43
+ fieldPath: {
44
+ color: "#888",
45
+ fontSize: 11,
46
+ maxWidth: 160,
47
+ overflow: "hidden",
48
+ textOverflow: "ellipsis",
49
+ whiteSpace: "nowrap"
50
+ },
51
+ savingBadge: {
52
+ color: "#f59e0b",
53
+ fontSize: 11
54
+ },
55
+ btnPrimary: {
56
+ background: "#10b981",
57
+ color: "#fff",
58
+ border: "none",
59
+ borderRadius: 4,
60
+ padding: "3px 10px",
61
+ fontSize: 12,
62
+ cursor: "pointer"
63
+ },
64
+ btnSecondary: {
65
+ background: "transparent",
66
+ color: "#888",
67
+ border: "1px solid #3a3330",
68
+ borderRadius: 4,
69
+ padding: "3px 10px",
70
+ fontSize: 12,
71
+ cursor: "pointer"
72
+ },
73
+ btnGhost: {
74
+ background: "transparent",
75
+ color: "#e5e0d8",
76
+ border: "1px solid #3a3330",
77
+ borderRadius: 4,
78
+ padding: "3px 10px",
79
+ fontSize: 12,
80
+ cursor: "pointer"
81
+ },
82
+ btnSuccess: {
83
+ background: "#10b981",
84
+ color: "#fff",
85
+ border: "none",
86
+ borderRadius: 4,
87
+ padding: "3px 10px",
88
+ fontSize: 12,
89
+ cursor: "pointer"
90
+ },
91
+ btnDanger: {
92
+ background: "transparent",
93
+ color: "#ef4444",
94
+ border: "1px solid #ef4444",
95
+ borderRadius: 4,
96
+ padding: "2px 8px",
97
+ fontSize: 11,
98
+ cursor: "pointer"
99
+ },
100
+ panel: {
101
+ position: "absolute",
102
+ bottom: "100%",
103
+ right: 0,
104
+ marginBottom: 8,
105
+ background: "#1a1614",
106
+ border: "1px solid #3a3330",
107
+ borderRadius: 8,
108
+ padding: "10px 14px",
109
+ minWidth: 280,
110
+ maxWidth: 420,
111
+ boxShadow: "0 4px 24px rgba(0,0,0,0.5)"
112
+ },
113
+ panelTitle: {
114
+ fontSize: 11,
115
+ color: "#888",
116
+ marginBottom: 8,
117
+ textTransform: "uppercase",
118
+ letterSpacing: "0.05em"
119
+ },
120
+ changeRow: {
121
+ display: "flex",
122
+ alignItems: "center",
123
+ gap: 8,
124
+ padding: "4px 0",
125
+ borderBottom: "1px solid #2a2421"
126
+ },
127
+ changePath: {
128
+ fontSize: 11,
129
+ color: "#888",
130
+ minWidth: 100,
131
+ flexShrink: 0
132
+ },
133
+ changeValue: {
134
+ fontSize: 12,
135
+ color: "#e5e0d8",
136
+ flex: 1,
137
+ overflow: "hidden",
138
+ textOverflow: "ellipsis",
139
+ whiteSpace: "nowrap"
140
+ },
141
+ deployOk: {
142
+ color: "#10b981",
143
+ fontSize: 13
144
+ },
145
+ deployPendingTitle: {
146
+ color: "#f59e0b",
147
+ fontSize: 13,
148
+ marginBottom: 6
149
+ },
150
+ deployPendingItem: {
151
+ fontSize: 12,
152
+ color: "#888",
153
+ padding: "2px 0"
154
+ }
155
+ };
156
+
157
+ // src/react/ContentEditToolbar.tsx
158
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
159
+ function ContentEditToolbar({
160
+ focusedField,
161
+ approvalState,
162
+ changes,
163
+ saving,
164
+ onApprove,
165
+ onRevoke,
166
+ onRevert,
167
+ onCheckDeploy
168
+ }) {
169
+ const [showChanges, setShowChanges] = useState(false);
170
+ const [deployResult, setDeployResult] = useState(
171
+ null
172
+ );
173
+ const [checkingDeploy, setCheckingDeploy] = useState(false);
174
+ const handleCheckDeploy = async () => {
175
+ setCheckingDeploy(true);
176
+ try {
177
+ const result = await onCheckDeploy();
178
+ setDeployResult(result);
179
+ } finally {
180
+ setCheckingDeploy(false);
181
+ }
182
+ };
183
+ return /* @__PURE__ */ jsxs("div", { "data-feedtack-edit-ui": true, style: styles.toolbar, children: [
184
+ focusedField && /* @__PURE__ */ jsxs("div", { style: styles.fieldSection, children: [
185
+ /* @__PURE__ */ jsx("span", { style: styles.fieldPath, children: focusedField.fieldPath }),
186
+ saving === focusedField.fieldPath && /* @__PURE__ */ jsx("span", { style: styles.savingBadge, children: "saving\u2026" }),
187
+ approvalState?.stale === false ? /* @__PURE__ */ jsx(
188
+ "button",
189
+ {
190
+ type: "button",
191
+ style: styles.btnSecondary,
192
+ onClick: () => onRevoke(focusedField.fieldPath),
193
+ children: "Unaccept"
194
+ }
195
+ ) : /* @__PURE__ */ jsx(
196
+ "button",
197
+ {
198
+ type: "button",
199
+ style: styles.btnPrimary,
200
+ onClick: () => onApprove(focusedField.fieldPath),
201
+ disabled: saving === focusedField.fieldPath,
202
+ children: "Approve"
203
+ }
204
+ )
205
+ ] }),
206
+ /* @__PURE__ */ jsxs(
207
+ "button",
208
+ {
209
+ type: "button",
210
+ style: styles.btnGhost,
211
+ onClick: () => {
212
+ setShowChanges((v) => !v);
213
+ setDeployResult(null);
214
+ },
215
+ children: [
216
+ "Changes (",
217
+ changes.length,
218
+ ")"
219
+ ]
220
+ }
221
+ ),
222
+ /* @__PURE__ */ jsx(
223
+ "button",
224
+ {
225
+ type: "button",
226
+ style: deployResult?.approved ? styles.btnSuccess : styles.btnGhost,
227
+ onClick: handleCheckDeploy,
228
+ disabled: checkingDeploy,
229
+ children: checkingDeploy ? "Checking\u2026" : "Check deploy"
230
+ }
231
+ ),
232
+ showChanges && changes.length > 0 && /* @__PURE__ */ jsxs("div", { style: styles.panel, children: [
233
+ /* @__PURE__ */ jsx("div", { style: styles.panelTitle, children: "Session changes" }),
234
+ changes.map((c) => /* @__PURE__ */ jsxs("div", { style: styles.changeRow, children: [
235
+ /* @__PURE__ */ jsx("div", { style: styles.changePath, children: c.fieldPath }),
236
+ /* @__PURE__ */ jsxs("div", { style: styles.changeValue, title: c.to, children: [
237
+ c.to.slice(0, 60),
238
+ c.to.length > 60 ? "\u2026" : ""
239
+ ] }),
240
+ /* @__PURE__ */ jsx(
241
+ "button",
242
+ {
243
+ type: "button",
244
+ style: styles.btnDanger,
245
+ onClick: () => onRevert(c.fieldPath),
246
+ disabled: saving === c.fieldPath,
247
+ children: "Revert"
248
+ }
249
+ )
250
+ ] }, c.fieldPath))
251
+ ] }),
252
+ deployResult && /* @__PURE__ */ jsx("div", { style: styles.panel, children: deployResult.approved ? /* @__PURE__ */ jsx("div", { style: styles.deployOk, children: "All fields approved \u2014 ready to deploy" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
253
+ /* @__PURE__ */ jsxs("div", { style: styles.deployPendingTitle, children: [
254
+ deployResult.pending.length,
255
+ " field",
256
+ deployResult.pending.length !== 1 ? "s" : "",
257
+ " need approval:"
258
+ ] }),
259
+ deployResult.pending.map((p) => /* @__PURE__ */ jsx("div", { style: styles.deployPendingItem, children: p }, p))
260
+ ] }) })
261
+ ] });
262
+ }
5
263
 
6
264
  // src/react/utils.ts
7
265
  function getAnchoredPosition(x, y) {
@@ -22,7 +280,7 @@ function cx(...parts) {
22
280
  }
23
281
 
24
282
  // src/react/CommentForm.tsx
25
- import { jsx, jsxs } from "react/jsx-runtime";
283
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
26
284
  function CommentForm({
27
285
  comment,
28
286
  commentError,
@@ -36,14 +294,14 @@ function CommentForm({
36
294
  onSubmit,
37
295
  onCancel
38
296
  }) {
39
- return /* @__PURE__ */ jsxs(
297
+ return /* @__PURE__ */ jsxs2(
40
298
  "div",
41
299
  {
42
300
  className: cx("feedtack-form", classes.form),
43
301
  style: { position: "fixed", ...formPos },
44
302
  children: [
45
- /* @__PURE__ */ jsx("label", { htmlFor: "feedtack-comment", className: "feedtack-sr-only", children: "Feedback comment" }),
46
- /* @__PURE__ */ jsx(
303
+ /* @__PURE__ */ jsx2("label", { htmlFor: "feedtack-comment", className: "feedtack-sr-only", children: "Feedback comment" }),
304
+ /* @__PURE__ */ jsx2(
47
305
  "textarea",
48
306
  {
49
307
  id: "feedtack-comment",
@@ -62,9 +320,9 @@ function CommentForm({
62
320
  "aria-invalid": commentError || void 0
63
321
  }
64
322
  ),
65
- commentError && /* @__PURE__ */ jsx("span", { id: "feedtack-comment-error", className: "feedtack-error-msg", children: "Comment is required" }),
66
- /* @__PURE__ */ jsxs("div", { className: "feedtack-sentiment", children: [
67
- /* @__PURE__ */ jsx(
323
+ commentError && /* @__PURE__ */ jsx2("span", { id: "feedtack-comment-error", className: "feedtack-error-msg", children: "Comment is required" }),
324
+ /* @__PURE__ */ jsxs2("div", { className: "feedtack-sentiment", children: [
325
+ /* @__PURE__ */ jsx2(
68
326
  "button",
69
327
  {
70
328
  type: "button",
@@ -73,7 +331,7 @@ function CommentForm({
73
331
  children: sentimentLabels.satisfied ?? "Good"
74
332
  }
75
333
  ),
76
- /* @__PURE__ */ jsx(
334
+ /* @__PURE__ */ jsx2(
77
335
  "button",
78
336
  {
79
337
  type: "button",
@@ -83,8 +341,8 @@ function CommentForm({
83
341
  }
84
342
  )
85
343
  ] }),
86
- /* @__PURE__ */ jsxs("div", { className: "feedtack-form-actions", children: [
87
- /* @__PURE__ */ jsx(
344
+ /* @__PURE__ */ jsxs2("div", { className: "feedtack-form-actions", children: [
345
+ /* @__PURE__ */ jsx2(
88
346
  "button",
89
347
  {
90
348
  type: "button",
@@ -93,7 +351,7 @@ function CommentForm({
93
351
  children: "Cancel"
94
352
  }
95
353
  ),
96
- /* @__PURE__ */ jsx(
354
+ /* @__PURE__ */ jsx2(
97
355
  "button",
98
356
  {
99
357
  type: "button",
@@ -120,10 +378,10 @@ function useFeedtackContext() {
120
378
  }
121
379
 
122
380
  // src/react/FeedbackModal.tsx
123
- import { useEffect, useRef } from "react";
381
+ import { useCallback, useEffect, useRef } from "react";
124
382
 
125
383
  // src/react/ThreadView.tsx
126
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
384
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
127
385
  function ThreadView({
128
386
  item,
129
387
  replyBody,
@@ -133,17 +391,17 @@ function ThreadView({
133
391
  onArchive,
134
392
  onBack
135
393
  }) {
136
- return /* @__PURE__ */ jsxs2("div", { className: "feedtack-modal-thread-view", children: [
137
- /* @__PURE__ */ jsx2("button", { type: "button", className: "feedtack-modal-back", onClick: onBack, children: "\u2190 Back" }),
138
- /* @__PURE__ */ jsxs2("div", { className: "feedtack-modal-thread-content", children: [
139
- /* @__PURE__ */ jsx2("strong", { children: item.payload.submittedBy.name }),
140
- /* @__PURE__ */ jsx2("p", { children: item.payload.comment })
394
+ return /* @__PURE__ */ jsxs3("div", { className: "feedtack-modal-thread-view", children: [
395
+ /* @__PURE__ */ jsx3("button", { type: "button", className: "feedtack-modal-back", onClick: onBack, children: "\u2190 Back" }),
396
+ /* @__PURE__ */ jsxs3("div", { className: "feedtack-modal-thread-content", children: [
397
+ /* @__PURE__ */ jsx3("strong", { children: item.payload.submittedBy.name }),
398
+ /* @__PURE__ */ jsx3("p", { children: item.payload.comment })
141
399
  ] }),
142
- item.replies.map((r) => /* @__PURE__ */ jsxs2("div", { className: "feedtack-modal-reply", children: [
143
- /* @__PURE__ */ jsx2("span", { className: "feedtack-reply-author", children: r.author.name }),
144
- /* @__PURE__ */ jsx2("p", { children: r.body })
400
+ item.replies.map((r) => /* @__PURE__ */ jsxs3("div", { className: "feedtack-modal-reply", children: [
401
+ /* @__PURE__ */ jsx3("span", { className: "feedtack-reply-author", children: r.author.name }),
402
+ /* @__PURE__ */ jsx3("p", { children: r.body })
145
403
  ] }, r.id)),
146
- /* @__PURE__ */ jsx2(
404
+ /* @__PURE__ */ jsx3(
147
405
  "textarea",
148
406
  {
149
407
  className: "feedtack-modal-textarea",
@@ -152,9 +410,9 @@ function ThreadView({
152
410
  onChange: (e) => onReplyBodyChange(e.target.value)
153
411
  }
154
412
  ),
155
- /* @__PURE__ */ jsxs2("div", { className: "feedtack-modal-actions", children: [
156
- /* @__PURE__ */ jsx2("button", { type: "button", className: "feedtack-btn-submit", onClick: onReply, children: "Reply" }),
157
- /* @__PURE__ */ jsx2(
413
+ /* @__PURE__ */ jsxs3("div", { className: "feedtack-modal-actions", children: [
414
+ /* @__PURE__ */ jsx3("button", { type: "button", className: "feedtack-btn-submit", onClick: onReply, children: "Reply" }),
415
+ /* @__PURE__ */ jsx3(
158
416
  "button",
159
417
  {
160
418
  type: "button",
@@ -163,7 +421,7 @@ function ThreadView({
163
421
  children: "Resolve"
164
422
  }
165
423
  ),
166
- /* @__PURE__ */ jsx2(
424
+ /* @__PURE__ */ jsx3(
167
425
  "button",
168
426
  {
169
427
  type: "button",
@@ -177,7 +435,7 @@ function ThreadView({
177
435
  }
178
436
 
179
437
  // src/react/FeedbackModal.tsx
180
- import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
438
+ import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
181
439
  function FeedbackModal({
182
440
  isOpen,
183
441
  onClose,
@@ -201,175 +459,183 @@ function FeedbackModal({
201
459
  openThreadId,
202
460
  onOpenThread
203
461
  }) {
204
- const panelRef = useRef(null);
462
+ const dialogRef = useRef(null);
205
463
  useEffect(() => {
206
- if (!isOpen) return;
207
- const onKey = (e) => {
208
- if (e.key === "Escape") onClose();
209
- };
210
- const onDown = (e) => {
211
- if (panelRef.current && !panelRef.current.contains(e.target)) {
212
- const btn = document.querySelector(".feedtack-btn");
213
- if (btn?.contains(e.target)) return;
464
+ const dialog = dialogRef.current;
465
+ if (!dialog) return;
466
+ if (isOpen && !dialog.open) {
467
+ dialog.showModal();
468
+ } else if (!isOpen && dialog.open) {
469
+ dialog.close();
470
+ }
471
+ }, [isOpen]);
472
+ const handleCancel = useCallback(
473
+ (e) => {
474
+ e.preventDefault();
475
+ onClose();
476
+ },
477
+ [onClose]
478
+ );
479
+ const handleBackdropClick = useCallback(
480
+ (e) => {
481
+ if (e.target === dialogRef.current) {
214
482
  onClose();
215
483
  }
216
- };
217
- window.addEventListener("keydown", onKey);
218
- document.addEventListener("mousedown", onDown);
219
- return () => {
220
- window.removeEventListener("keydown", onKey);
221
- document.removeEventListener("mousedown", onDown);
222
- };
223
- }, [isOpen, onClose]);
484
+ },
485
+ [onClose]
486
+ );
224
487
  if (!isOpen) return null;
225
488
  const threads = activeTab === "site" ? siteFeedback : pageFeedback;
226
489
  const openItem = openThreadId ? threads.find((i) => i.payload.id === openThreadId) : null;
227
- return /* @__PURE__ */ jsxs3(
228
- "div",
229
- {
230
- ref: panelRef,
231
- className: "feedtack-modal",
232
- role: "dialog",
233
- "aria-label": "Feedback",
234
- "aria-modal": "true",
235
- children: [
236
- /* @__PURE__ */ jsxs3("div", { className: "feedtack-modal-header", children: [
237
- /* @__PURE__ */ jsx3("span", { className: "feedtack-modal-title", children: "Feedback" }),
238
- /* @__PURE__ */ jsx3(
239
- "button",
240
- {
241
- type: "button",
242
- className: "feedtack-modal-close",
243
- onClick: onClose,
244
- "aria-label": "Close",
245
- children: "\xD7"
246
- }
247
- )
248
- ] }),
249
- /* @__PURE__ */ jsxs3("div", { className: "feedtack-modal-tabs", children: [
250
- /* @__PURE__ */ jsxs3(
251
- "button",
252
- {
253
- type: "button",
254
- className: cx("feedtack-modal-tab", activeTab === "site" && "active"),
255
- onClick: () => onTabChange("site"),
256
- children: [
257
- "Site",
258
- siteFeedback.length > 0 && /* @__PURE__ */ jsx3("span", { className: "feedtack-tab-count", children: siteFeedback.length })
259
- ]
260
- }
261
- ),
262
- /* @__PURE__ */ jsxs3(
263
- "button",
264
- {
265
- type: "button",
266
- className: cx("feedtack-modal-tab", activeTab === "page" && "active"),
267
- onClick: () => onTabChange("page"),
268
- children: [
269
- "Page",
270
- pageFeedback.length > 0 && /* @__PURE__ */ jsx3("span", { className: "feedtack-tab-count", children: pageFeedback.length })
271
- ]
272
- }
273
- )
274
- ] }),
275
- /* @__PURE__ */ jsx3("div", { className: "feedtack-modal-body", children: openItem ? /* @__PURE__ */ jsx3(
276
- ThreadView,
277
- {
278
- item: openItem,
279
- replyBody,
280
- onReplyBodyChange,
281
- onReply: () => onReply(openItem.payload.id),
282
- onResolve: () => onResolve(openItem.payload.id),
283
- onArchive: () => onArchive(openItem.payload.id),
284
- onBack: () => onOpenThread(null)
285
- }
286
- ) : /* @__PURE__ */ jsxs3(Fragment, { children: [
287
- threads.length > 0 && /* @__PURE__ */ jsx3("div", { className: "feedtack-modal-threads", children: threads.map((item) => /* @__PURE__ */ jsxs3(
288
- "button",
289
- {
290
- type: "button",
291
- className: "feedtack-modal-thread-item",
292
- onClick: () => onOpenThread(item.payload.id),
293
- children: [
294
- /* @__PURE__ */ jsx3("span", { className: "feedtack-thread-author", children: item.payload.submittedBy.name }),
295
- /* @__PURE__ */ jsx3("span", { className: "feedtack-thread-comment", children: item.payload.comment }),
296
- /* @__PURE__ */ jsxs3("span", { className: "feedtack-thread-meta", children: [
297
- item.replies.length > 0 && `${item.replies.length} ${item.replies.length === 1 ? "reply" : "replies"}`,
298
- item.resolutions.length > 0 && " \xB7 resolved"
299
- ] })
300
- ]
301
- },
302
- item.payload.id
303
- )) }),
304
- /* @__PURE__ */ jsxs3("div", { className: "feedtack-modal-compose", children: [
305
- /* @__PURE__ */ jsx3(
306
- "textarea",
490
+ return (
491
+ // biome-ignore lint/a11y/useKeyWithClickEvents: native <dialog> handles keyboard via onCancel (Escape)
492
+ /* @__PURE__ */ jsxs4(
493
+ "dialog",
494
+ {
495
+ ref: dialogRef,
496
+ className: "feedtack-modal",
497
+ "aria-label": "Feedback",
498
+ onCancel: handleCancel,
499
+ onClick: handleBackdropClick,
500
+ children: [
501
+ /* @__PURE__ */ jsxs4("div", { className: "feedtack-modal-header", children: [
502
+ /* @__PURE__ */ jsx4("span", { className: "feedtack-modal-title", children: "Feedback" }),
503
+ /* @__PURE__ */ jsx4(
504
+ "button",
307
505
  {
308
- className: cx(
309
- "feedtack-modal-textarea",
310
- commentError && "error"
311
- ),
312
- placeholder: "What's on your mind? (required)",
313
- value: comment,
314
- onChange: (e) => onCommentChange(e.target.value),
315
- onKeyDown: (e) => {
316
- if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
317
- e.preventDefault();
318
- onSubmit();
319
- }
320
- },
321
- "aria-invalid": commentError || void 0
506
+ type: "button",
507
+ className: "feedtack-modal-close",
508
+ onClick: onClose,
509
+ "aria-label": "Close",
510
+ children: "\xD7"
511
+ }
512
+ )
513
+ ] }),
514
+ /* @__PURE__ */ jsxs4("div", { className: "feedtack-modal-tabs", children: [
515
+ /* @__PURE__ */ jsxs4(
516
+ "button",
517
+ {
518
+ type: "button",
519
+ className: cx("feedtack-modal-tab", activeTab === "site" && "active"),
520
+ onClick: () => onTabChange("site"),
521
+ children: [
522
+ "Site",
523
+ siteFeedback.length > 0 && /* @__PURE__ */ jsx4("span", { className: "feedtack-tab-count", children: siteFeedback.length })
524
+ ]
322
525
  }
323
526
  ),
324
- commentError && /* @__PURE__ */ jsx3("span", { className: "feedtack-error-msg", children: "Comment is required" }),
325
- /* @__PURE__ */ jsxs3("div", { className: "feedtack-sentiment", children: [
326
- /* @__PURE__ */ jsx3(
327
- "button",
527
+ /* @__PURE__ */ jsxs4(
528
+ "button",
529
+ {
530
+ type: "button",
531
+ className: cx("feedtack-modal-tab", activeTab === "page" && "active"),
532
+ onClick: () => onTabChange("page"),
533
+ children: [
534
+ "Page",
535
+ pageFeedback.length > 0 && /* @__PURE__ */ jsx4("span", { className: "feedtack-tab-count", children: pageFeedback.length })
536
+ ]
537
+ }
538
+ )
539
+ ] }),
540
+ /* @__PURE__ */ jsx4("div", { className: "feedtack-modal-body", children: openItem ? /* @__PURE__ */ jsx4(
541
+ ThreadView,
542
+ {
543
+ item: openItem,
544
+ replyBody,
545
+ onReplyBodyChange,
546
+ onReply: () => onReply(openItem.payload.id),
547
+ onResolve: () => onResolve(openItem.payload.id),
548
+ onArchive: () => onArchive(openItem.payload.id),
549
+ onBack: () => onOpenThread(null)
550
+ }
551
+ ) : /* @__PURE__ */ jsxs4(Fragment2, { children: [
552
+ threads.length > 0 && /* @__PURE__ */ jsx4("div", { className: "feedtack-modal-threads", children: threads.map((item) => /* @__PURE__ */ jsxs4(
553
+ "button",
554
+ {
555
+ type: "button",
556
+ className: "feedtack-modal-thread-item",
557
+ onClick: () => onOpenThread(item.payload.id),
558
+ children: [
559
+ /* @__PURE__ */ jsx4("span", { className: "feedtack-thread-author", children: item.payload.submittedBy.name }),
560
+ /* @__PURE__ */ jsx4("span", { className: "feedtack-thread-comment", children: item.payload.comment }),
561
+ /* @__PURE__ */ jsxs4("span", { className: "feedtack-thread-meta", children: [
562
+ item.replies.length > 0 && `${item.replies.length} ${item.replies.length === 1 ? "reply" : "replies"}`,
563
+ item.resolutions.length > 0 && " \xB7 resolved"
564
+ ] })
565
+ ]
566
+ },
567
+ item.payload.id
568
+ )) }),
569
+ /* @__PURE__ */ jsxs4("div", { className: "feedtack-modal-compose", children: [
570
+ /* @__PURE__ */ jsx4(
571
+ "textarea",
328
572
  {
329
- type: "button",
330
- className: sentiment === "good" ? "selected" : "",
331
- onClick: () => onSentimentChange(sentiment === "good" ? null : "good"),
332
- children: "Good"
573
+ className: cx(
574
+ "feedtack-modal-textarea",
575
+ commentError && "error"
576
+ ),
577
+ placeholder: "What's on your mind? (required)",
578
+ value: comment,
579
+ onChange: (e) => onCommentChange(e.target.value),
580
+ onKeyDown: (e) => {
581
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
582
+ e.preventDefault();
583
+ onSubmit();
584
+ }
585
+ },
586
+ "aria-invalid": commentError || void 0
333
587
  }
334
588
  ),
335
- /* @__PURE__ */ jsx3(
589
+ commentError && /* @__PURE__ */ jsx4("span", { className: "feedtack-error-msg", children: "Comment is required" }),
590
+ /* @__PURE__ */ jsxs4("div", { className: "feedtack-sentiment", children: [
591
+ /* @__PURE__ */ jsx4(
592
+ "button",
593
+ {
594
+ type: "button",
595
+ className: sentiment === "good" ? "selected" : "",
596
+ onClick: () => onSentimentChange(sentiment === "good" ? null : "good"),
597
+ children: "Good"
598
+ }
599
+ ),
600
+ /* @__PURE__ */ jsx4(
601
+ "button",
602
+ {
603
+ type: "button",
604
+ className: sentiment === "bad" ? "selected" : "",
605
+ onClick: () => onSentimentChange(sentiment === "bad" ? null : "bad"),
606
+ children: "Bad"
607
+ }
608
+ )
609
+ ] }),
610
+ /* @__PURE__ */ jsx4(
336
611
  "button",
337
612
  {
338
613
  type: "button",
339
- className: sentiment === "bad" ? "selected" : "",
340
- onClick: () => onSentimentChange(sentiment === "bad" ? null : "bad"),
341
- children: "Bad"
614
+ className: "feedtack-btn-submit",
615
+ onClick: onSubmit,
616
+ disabled: submitting,
617
+ children: submitting ? "Sending\u2026" : "Submit"
342
618
  }
343
619
  )
344
- ] }),
345
- /* @__PURE__ */ jsx3(
346
- "button",
347
- {
348
- type: "button",
349
- className: "feedtack-btn-submit",
350
- onClick: onSubmit,
351
- disabled: submitting,
352
- children: submitting ? "Sending\u2026" : "Submit"
353
- }
354
- )
355
- ] })
356
- ] }) }),
357
- /* @__PURE__ */ jsx3("div", { className: "feedtack-modal-footer", children: /* @__PURE__ */ jsx3(
358
- "button",
359
- {
360
- type: "button",
361
- className: "feedtack-modal-pin-btn",
362
- onClick: onPlacePin,
363
- children: "Place a pin"
364
- }
365
- ) })
366
- ]
367
- }
620
+ ] })
621
+ ] }) }),
622
+ /* @__PURE__ */ jsx4("div", { className: "feedtack-modal-footer", children: /* @__PURE__ */ jsx4(
623
+ "button",
624
+ {
625
+ type: "button",
626
+ className: "feedtack-modal-pin-btn",
627
+ onClick: onPlacePin,
628
+ children: "Place a pin"
629
+ }
630
+ ) })
631
+ ]
632
+ }
633
+ )
368
634
  );
369
635
  }
370
636
 
371
637
  // src/react/PinOverlay.tsx
372
- import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
638
+ import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
373
639
  function PinOverlay({
374
640
  feedbackItems,
375
641
  pathname,
@@ -382,10 +648,10 @@ function PinOverlay({
382
648
  renderPinIcon,
383
649
  pinMarkerClass
384
650
  }) {
385
- return /* @__PURE__ */ jsx4(Fragment2, { children: feedbackItems.filter((item) => item.payload.page.pathname === pathname).filter((item) => !isArchivedForUser(item)).filter((item) => hasValidPins(item)).map((item) => {
651
+ return /* @__PURE__ */ jsx5(Fragment3, { children: feedbackItems.filter((item) => item.payload.page.pathname === pathname).filter((item) => !isArchivedForUser(item)).filter((item) => hasValidPins(item)).map((item) => {
386
652
  const pin = item.payload.pins[0];
387
653
  const pos = getPosition(item.payload.id, pin);
388
- return /* @__PURE__ */ jsxs4(
654
+ return /* @__PURE__ */ jsxs5(
389
655
  "button",
390
656
  {
391
657
  type: "button",
@@ -405,7 +671,7 @@ function PinOverlay({
405
671
  openThreadId === item.payload.id ? null : item.payload.id
406
672
  ),
407
673
  children: [
408
- renderPinIcon ? /* @__PURE__ */ jsx4("span", { className: "feedtack-pin-icon", children: renderPinIcon(item) }) : item.resolutions.length > 0 && /* @__PURE__ */ jsx4(
674
+ renderPinIcon ? /* @__PURE__ */ jsx5("span", { className: "feedtack-pin-icon", children: renderPinIcon(item) }) : item.resolutions.length > 0 && /* @__PURE__ */ jsx5(
409
675
  "span",
410
676
  {
411
677
  className: "feedtack-pin-icon",
@@ -414,7 +680,7 @@ function PinOverlay({
414
680
  children: "\u2713"
415
681
  }
416
682
  ),
417
- hasUnread(item) && /* @__PURE__ */ jsx4("div", { className: "feedtack-pin-badge" })
683
+ hasUnread(item) && /* @__PURE__ */ jsx5("div", { className: "feedtack-pin-badge" })
418
684
  ]
419
685
  },
420
686
  item.payload.id
@@ -423,7 +689,7 @@ function PinOverlay({
423
689
  }
424
690
 
425
691
  // src/react/ThreadPanel.tsx
426
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
692
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
427
693
  function ThreadPanel({
428
694
  item,
429
695
  replyBody,
@@ -439,26 +705,26 @@ function ThreadPanel({
439
705
  if (!pin) return null;
440
706
  const { x, y } = pinPosition ?? pin;
441
707
  const pos = getAnchoredPosition(x, y);
442
- return /* @__PURE__ */ jsxs5(
708
+ return /* @__PURE__ */ jsxs6(
443
709
  "div",
444
710
  {
445
711
  className: cx("feedtack-thread", className),
446
712
  style: { position: "fixed", ...pos },
447
713
  children: [
448
- /* @__PURE__ */ jsx5("strong", { style: { fontSize: 13 }, children: item.payload.submittedBy.name }),
449
- /* @__PURE__ */ jsx5("p", { style: { fontSize: 13 }, children: item.payload.comment }),
450
- item.replies.map((r) => /* @__PURE__ */ jsxs5(
714
+ /* @__PURE__ */ jsx6("strong", { style: { fontSize: 13 }, children: item.payload.submittedBy.name }),
715
+ /* @__PURE__ */ jsx6("p", { style: { fontSize: 13 }, children: item.payload.comment }),
716
+ item.replies.map((r) => /* @__PURE__ */ jsxs6(
451
717
  "div",
452
718
  {
453
719
  style: { borderTop: "1px solid var(--ft-border)", paddingTop: 8 },
454
720
  children: [
455
- /* @__PURE__ */ jsx5("span", { style: { fontSize: 12, fontWeight: 600 }, children: r.author.name }),
456
- /* @__PURE__ */ jsx5("p", { style: { fontSize: 12 }, children: r.body })
721
+ /* @__PURE__ */ jsx6("span", { style: { fontSize: 12, fontWeight: 600 }, children: r.author.name }),
722
+ /* @__PURE__ */ jsx6("p", { style: { fontSize: 12 }, children: r.body })
457
723
  ]
458
724
  },
459
725
  r.id
460
726
  )),
461
- /* @__PURE__ */ jsx5(
727
+ /* @__PURE__ */ jsx6(
462
728
  "textarea",
463
729
  {
464
730
  placeholder: "Reply\u2026",
@@ -478,8 +744,8 @@ function ThreadPanel({
478
744
  }
479
745
  }
480
746
  ),
481
- /* @__PURE__ */ jsxs5("div", { style: { display: "flex", gap: 6, flexWrap: "wrap" }, children: [
482
- /* @__PURE__ */ jsx5(
747
+ /* @__PURE__ */ jsxs6("div", { style: { display: "flex", gap: 6, flexWrap: "wrap" }, children: [
748
+ /* @__PURE__ */ jsx6(
483
749
  "button",
484
750
  {
485
751
  type: "button",
@@ -489,7 +755,7 @@ function ThreadPanel({
489
755
  children: "Reply"
490
756
  }
491
757
  ),
492
- /* @__PURE__ */ jsx5(
758
+ /* @__PURE__ */ jsx6(
493
759
  "button",
494
760
  {
495
761
  type: "button",
@@ -499,7 +765,7 @@ function ThreadPanel({
499
765
  children: "Mark Resolved"
500
766
  }
501
767
  ),
502
- /* @__PURE__ */ jsx5(
768
+ /* @__PURE__ */ jsx6(
503
769
  "button",
504
770
  {
505
771
  type: "button",
@@ -509,7 +775,7 @@ function ThreadPanel({
509
775
  children: "Archive"
510
776
  }
511
777
  ),
512
- /* @__PURE__ */ jsx5(
778
+ /* @__PURE__ */ jsx6(
513
779
  "button",
514
780
  {
515
781
  type: "button",
@@ -526,12 +792,12 @@ function ThreadPanel({
526
792
  }
527
793
 
528
794
  // src/react/useAnchoredPins.ts
529
- import { useCallback, useEffect as useEffect2, useState } from "react";
795
+ import { useCallback as useCallback2, useEffect as useEffect2, useState as useState2 } from "react";
530
796
  function useAnchoredPins(items, pathname) {
531
- const [positions, setPositions] = useState(
797
+ const [positions, setPositions] = useState2(
532
798
  /* @__PURE__ */ new Map()
533
799
  );
534
- const resolve = useCallback(() => {
800
+ const resolve = useCallback2(() => {
535
801
  const next = /* @__PURE__ */ new Map();
536
802
  for (const item of items) {
537
803
  if (item.payload.page.pathname !== pathname) continue;
@@ -559,7 +825,7 @@ function useAnchoredPins(items, pathname) {
559
825
  window.removeEventListener("scroll", handler);
560
826
  };
561
827
  }, [resolve]);
562
- const getPosition = useCallback(
828
+ const getPosition = useCallback2(
563
829
  (itemId, fallbackPin) => {
564
830
  return positions.get(itemId) ?? { x: fallbackPin.x, y: fallbackPin.y };
565
831
  },
@@ -587,7 +853,7 @@ function resolvePin(pin) {
587
853
  }
588
854
 
589
855
  // src/react/useFeedtackState.ts
590
- import { useCallback as useCallback2, useEffect as useEffect3, useRef as useRef2, useSyncExternalStore } from "react";
856
+ import { useCallback as useCallback3, useEffect as useEffect3, useRef as useRef2, useSyncExternalStore } from "react";
591
857
  function useFeedtackState({
592
858
  adapter,
593
859
  currentUser,
@@ -618,25 +884,25 @@ function useFeedtackState({
618
884
  engine.mount();
619
885
  return () => engine.destroy();
620
886
  }, [engine]);
621
- const subscribe = useCallback2(
887
+ const subscribe = useCallback3(
622
888
  (cb) => engine.subscribe(cb),
623
889
  [engine]
624
890
  );
625
- const getSnapshot = useCallback2(() => engine.getState(), [engine]);
891
+ const getSnapshot = useCallback3(() => engine.getState(), [engine]);
626
892
  const state = useSyncExternalStore(
627
893
  subscribe,
628
894
  getSnapshot,
629
895
  getSnapshot
630
896
  );
631
- const isArchivedForUser = useCallback2(
897
+ const isArchivedForUser = useCallback3(
632
898
  (item) => engine.isArchivedForUser(item),
633
899
  [engine]
634
900
  );
635
- const hasUnread = useCallback2(
901
+ const hasUnread = useCallback3(
636
902
  (item) => engine.hasUnread(item),
637
903
  [engine]
638
904
  );
639
- const hasValidPins = useCallback2(
905
+ const hasValidPins = useCallback3(
640
906
  (item) => engine.hasValidPins(item),
641
907
  [engine]
642
908
  );
@@ -644,27 +910,27 @@ function useFeedtackState({
644
910
  // Pin mode
645
911
  isPinModeActive: state.isPinModeActive,
646
912
  isActive: state.isPinModeActive,
647
- activatePinMode: useCallback2(() => engine.activatePinMode(), [engine]),
648
- activate: useCallback2(() => engine.activatePinMode(), [engine]),
649
- deactivatePinMode: useCallback2(() => engine.deactivatePinMode(), [engine]),
650
- deactivate: useCallback2(() => engine.deactivatePinMode(), [engine]),
913
+ activatePinMode: useCallback3(() => engine.activatePinMode(), [engine]),
914
+ activate: useCallback3(() => engine.activatePinMode(), [engine]),
915
+ deactivatePinMode: useCallback3(() => engine.deactivatePinMode(), [engine]),
916
+ deactivate: useCallback3(() => engine.deactivatePinMode(), [engine]),
651
917
  pendingPins: state.pendingPins,
652
918
  selectedColor: state.selectedColor,
653
- setSelectedColor: useCallback2(
919
+ setSelectedColor: useCallback3(
654
920
  (c) => engine.setSelectedColor(c),
655
921
  [engine]
656
922
  ),
657
923
  showForm: state.showForm,
658
924
  // Form
659
925
  comment: state.comment,
660
- setComment: useCallback2((v) => engine.setComment(v), [engine]),
926
+ setComment: useCallback3((v) => engine.setComment(v), [engine]),
661
927
  sentiment: state.sentiment,
662
- setSentiment: useCallback2(
928
+ setSentiment: useCallback3(
663
929
  (v) => engine.setSentiment(v),
664
930
  [engine]
665
931
  ),
666
932
  commentError: state.commentError,
667
- setCommentError: useCallback2(
933
+ setCommentError: useCallback3(
668
934
  (v) => engine.setCommentError(v),
669
935
  [engine]
670
936
  ),
@@ -677,30 +943,30 @@ function useFeedtackState({
677
943
  pathname: state.pathname,
678
944
  // Thread
679
945
  openThreadId: state.openThreadId,
680
- setOpenThreadId: useCallback2(
946
+ setOpenThreadId: useCallback3(
681
947
  (id) => engine.setOpenThreadId(id),
682
948
  [engine]
683
949
  ),
684
950
  replyBody: state.replyBody,
685
- setReplyBody: useCallback2((v) => engine.setReplyBody(v), [engine]),
951
+ setReplyBody: useCallback3((v) => engine.setReplyBody(v), [engine]),
686
952
  // Modal
687
953
  isModalOpen: state.isModalOpen,
688
- openModal: useCallback2(() => engine.openModal(), [engine]),
689
- closeModal: useCallback2(() => engine.closeModal(), [engine]),
954
+ openModal: useCallback3(() => engine.openModal(), [engine]),
955
+ closeModal: useCallback3(() => engine.closeModal(), [engine]),
690
956
  composeScope: state.composeScope,
691
- setComposeScope: useCallback2(
957
+ setComposeScope: useCallback3(
692
958
  (s) => engine.setComposeScope(s),
693
959
  [engine]
694
960
  ),
695
961
  // Actions
696
- handleSubmit: useCallback2(() => engine.handleSubmit(), [engine]),
697
- handleModalSubmit: useCallback2(() => engine.handleModalSubmit(), [engine]),
698
- handleReply: useCallback2((id) => engine.handleReply(id), [engine]),
699
- handleResolve: useCallback2(
962
+ handleSubmit: useCallback3(() => engine.handleSubmit(), [engine]),
963
+ handleModalSubmit: useCallback3(() => engine.handleModalSubmit(), [engine]),
964
+ handleReply: useCallback3((id) => engine.handleReply(id), [engine]),
965
+ handleResolve: useCallback3(
700
966
  (id) => engine.handleResolve(id),
701
967
  [engine]
702
968
  ),
703
- handleArchive: useCallback2(
969
+ handleArchive: useCallback3(
704
970
  (id) => engine.handleArchive(id),
705
971
  [engine]
706
972
  ),
@@ -712,7 +978,7 @@ function useFeedtackState({
712
978
  }
713
979
 
714
980
  // src/react/FeedtackProvider.tsx
715
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
981
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
716
982
  function FeedtackProvider({
717
983
  children,
718
984
  adapter,
@@ -749,7 +1015,7 @@ function FeedtackProvider({
749
1015
  state.closeModal();
750
1016
  state.activatePinMode();
751
1017
  };
752
- return /* @__PURE__ */ jsxs6(
1018
+ return /* @__PURE__ */ jsxs7(
753
1019
  FeedtackContext.Provider,
754
1020
  {
755
1021
  value: {
@@ -770,7 +1036,7 @@ function FeedtackProvider({
770
1036
  },
771
1037
  children: [
772
1038
  children,
773
- !disabled && showButton && /* @__PURE__ */ jsx6(
1039
+ !disabled && showButton && /* @__PURE__ */ jsx7(
774
1040
  "button",
775
1041
  {
776
1042
  type: "button",
@@ -785,7 +1051,7 @@ function FeedtackProvider({
785
1051
  children: "Feedback"
786
1052
  }
787
1053
  ),
788
- state.isPinModeActive && /* @__PURE__ */ jsx6("div", { className: cx("feedtack-color-picker", classes.colorPicker), children: PIN_PALETTE.map((color) => /* @__PURE__ */ jsx6(
1054
+ state.isPinModeActive && /* @__PURE__ */ jsx7("div", { className: cx("feedtack-color-picker", classes.colorPicker), children: PIN_PALETTE.map((color) => /* @__PURE__ */ jsx7(
789
1055
  "button",
790
1056
  {
791
1057
  type: "button",
@@ -799,7 +1065,7 @@ function FeedtackProvider({
799
1065
  },
800
1066
  color
801
1067
  )) }),
802
- state.pendingPins.map((pin) => /* @__PURE__ */ jsx6(
1068
+ state.pendingPins.map((pin) => /* @__PURE__ */ jsx7(
803
1069
  "div",
804
1070
  {
805
1071
  className: cx("feedtack-pin-marker", classes.pinMarker),
@@ -812,7 +1078,7 @@ function FeedtackProvider({
812
1078
  },
813
1079
  `${pin.x}-${pin.y}-${pin.color}`
814
1080
  )),
815
- state.showForm && /* @__PURE__ */ jsx6(
1081
+ state.showForm && /* @__PURE__ */ jsx7(
816
1082
  CommentForm,
817
1083
  {
818
1084
  comment: state.comment,
@@ -831,7 +1097,7 @@ function FeedtackProvider({
831
1097
  onCancel: state.deactivatePinMode
832
1098
  }
833
1099
  ),
834
- !state.loading && /* @__PURE__ */ jsx6(
1100
+ !state.loading && /* @__PURE__ */ jsx7(
835
1101
  PinOverlay,
836
1102
  {
837
1103
  feedbackItems: state.feedbackItems,
@@ -846,7 +1112,7 @@ function FeedtackProvider({
846
1112
  pinMarkerClass: classes.pinMarker
847
1113
  }
848
1114
  ),
849
- openItem && /* @__PURE__ */ jsx6(
1115
+ openItem && /* @__PURE__ */ jsx7(
850
1116
  ThreadPanel,
851
1117
  {
852
1118
  item: openItem,
@@ -863,7 +1129,7 @@ function FeedtackProvider({
863
1129
  className: classes.thread
864
1130
  }
865
1131
  ),
866
- !disabled && /* @__PURE__ */ jsx6(
1132
+ !disabled && /* @__PURE__ */ jsx7(
867
1133
  FeedbackModal,
868
1134
  {
869
1135
  isOpen: state.isModalOpen,
@@ -892,18 +1158,350 @@ function FeedtackProvider({
892
1158
  onOpenThread: state.setOpenThreadId
893
1159
  }
894
1160
  ),
895
- state.loading && /* @__PURE__ */ jsx6("div", { className: "feedtack-loading", children: "Loading feedback\u2026" })
1161
+ state.loading && /* @__PURE__ */ jsx7("div", { className: "feedtack-loading", children: "Loading feedback\u2026" })
896
1162
  ]
897
1163
  }
898
1164
  );
899
1165
  }
900
1166
 
1167
+ // src/react/useContentApproval.ts
1168
+ import { useCallback as useCallback4, useEffect as useEffect4, useState as useState3 } from "react";
1169
+ function useContentApproval(adapter, userId, options) {
1170
+ const [fields, setFields] = useState3([]);
1171
+ const getContentForField = useCallback4(
1172
+ (fieldPath, domContent) => {
1173
+ return options?.storedValues?.get(fieldPath) ?? domContent;
1174
+ },
1175
+ [options?.storedValues]
1176
+ );
1177
+ const rescan = useCallback4(async () => {
1178
+ warnIfNotContentAdapter(adapter, "useContentApproval");
1179
+ if (!isContentAdapter(adapter)) {
1180
+ setFields([]);
1181
+ return;
1182
+ }
1183
+ const scanned = scanFields();
1184
+ const storedApprovals = await adapter.loadApprovals();
1185
+ const approvalMap = new Map(
1186
+ storedApprovals.map((s) => [s.fieldPath, s.approval])
1187
+ );
1188
+ const states = await Promise.all(
1189
+ scanned.map(async (f) => {
1190
+ const content = getContentForField(f.fieldPath, f.content);
1191
+ const currentHash = await hashField(content);
1192
+ const approval = approvalMap.get(f.fieldPath) ?? null;
1193
+ return {
1194
+ fieldPath: f.fieldPath,
1195
+ approval,
1196
+ stale: approval === null || approval.hash !== currentHash
1197
+ };
1198
+ })
1199
+ );
1200
+ setFields(states);
1201
+ }, [adapter, getContentForField]);
1202
+ useEffect4(() => {
1203
+ void rescan();
1204
+ }, [rescan]);
1205
+ const approve = useCallback4(
1206
+ async (fieldPath) => {
1207
+ warnIfNotContentAdapter(adapter, "approve");
1208
+ if (!isContentAdapter(adapter)) return;
1209
+ const scanned = scanFields();
1210
+ const field = scanned.find((f) => f.fieldPath === fieldPath);
1211
+ if (!field) return;
1212
+ const content = getContentForField(fieldPath, field.content);
1213
+ const hash = await hashField(content);
1214
+ const existing = fields.find((f) => f.fieldPath === fieldPath);
1215
+ const existingBy = existing?.approval?.by ?? [];
1216
+ const by = existingBy.includes(userId) ? existingBy : [...existingBy, userId];
1217
+ await adapter.approve(fieldPath, {
1218
+ hash,
1219
+ by,
1220
+ at: (/* @__PURE__ */ new Date()).toISOString()
1221
+ });
1222
+ setFields(
1223
+ (prev) => prev.map(
1224
+ (f) => f.fieldPath === fieldPath ? {
1225
+ ...f,
1226
+ approval: { hash, by, at: (/* @__PURE__ */ new Date()).toISOString() },
1227
+ stale: false
1228
+ } : f
1229
+ )
1230
+ );
1231
+ },
1232
+ [adapter, userId, fields, getContentForField]
1233
+ );
1234
+ const revoke = useCallback4(
1235
+ async (fieldPath) => {
1236
+ warnIfNotContentAdapter(adapter, "revokeApproval");
1237
+ if (!isContentAdapter(adapter)) return;
1238
+ await adapter.revokeApproval(fieldPath, userId);
1239
+ setFields(
1240
+ (prev) => prev.map((f) => {
1241
+ if (f.fieldPath !== fieldPath || !f.approval) return f;
1242
+ const by = f.approval.by.filter((id) => id !== userId);
1243
+ const approval = by.length > 0 ? { ...f.approval, by } : null;
1244
+ return { ...f, approval, stale: true };
1245
+ })
1246
+ );
1247
+ },
1248
+ [adapter, userId]
1249
+ );
1250
+ const checkDeploy = useCallback4(async () => {
1251
+ warnIfNotContentAdapter(adapter, "checkDeploy");
1252
+ if (!isContentAdapter(adapter)) {
1253
+ return { approved: false, pending: [] };
1254
+ }
1255
+ const scanned = scanFields();
1256
+ const storedApprovals = await adapter.loadApprovals();
1257
+ const approvalMap = new Map(
1258
+ storedApprovals.map((s) => [s.fieldPath, s.approval])
1259
+ );
1260
+ const pending = [];
1261
+ await Promise.all(
1262
+ scanned.map(async (f) => {
1263
+ const content = getContentForField(f.fieldPath, f.content);
1264
+ const currentHash = await hashField(content);
1265
+ const approval = approvalMap.get(f.fieldPath) ?? null;
1266
+ if (approval === null || approval.hash !== currentHash) {
1267
+ pending.push(f.fieldPath);
1268
+ }
1269
+ })
1270
+ );
1271
+ return { approved: pending.length === 0, pending };
1272
+ }, [adapter, getContentForField]);
1273
+ return { fields, approve, revoke, rescan, checkDeploy };
1274
+ }
1275
+
1276
+ // src/react/useContentEdit.ts
1277
+ import { useCallback as useCallback5, useRef as useRef3, useState as useState4 } from "react";
1278
+
1279
+ // src/react/fieldDom.ts
1280
+ function getFieldValue(el) {
1281
+ if (isFormField(el)) {
1282
+ return el.placeholder || "";
1283
+ }
1284
+ return el.innerText;
1285
+ }
1286
+ function setFieldValue(el, value) {
1287
+ if (isFormField(el)) {
1288
+ ;
1289
+ el.placeholder = value;
1290
+ } else {
1291
+ el.innerText = value;
1292
+ }
1293
+ }
1294
+ function isFormField(el) {
1295
+ const tag = el.tagName.toLowerCase();
1296
+ return tag === "input" || tag === "textarea";
1297
+ }
1298
+ function findFieldElement(fieldPath) {
1299
+ return document.querySelector(
1300
+ `[data-feedtack-field="${fieldPath}"]`
1301
+ );
1302
+ }
1303
+ function buildToolbarProps(focusedField, approvalFields, changes, saving, approval, revert) {
1304
+ const approvalState = focusedField ? approvalFields.find((f) => f.fieldPath === focusedField.fieldPath) ?? null : null;
1305
+ return {
1306
+ focusedField,
1307
+ approvalState,
1308
+ changes,
1309
+ saving,
1310
+ onApprove: approval.approve,
1311
+ onRevoke: approval.revoke,
1312
+ onRevert: revert,
1313
+ onCheckDeploy: approval.checkDeploy
1314
+ };
1315
+ }
1316
+
1317
+ // src/react/useContentEdit.ts
1318
+ function useContentEdit(adapter, userId) {
1319
+ const [active, setActive] = useState4(false);
1320
+ const [changes, setChanges] = useState4([]);
1321
+ const [saving, setSaving] = useState4(null);
1322
+ const [focusedField, setFocusedField] = useState4(
1323
+ null
1324
+ );
1325
+ const storedValuesRef = useRef3(/* @__PURE__ */ new Map());
1326
+ const [storedValuesVersion, setStoredValuesVersion] = useState4(0);
1327
+ const storedValuesMap = storedValuesRef.current;
1328
+ const approval = useContentApproval(adapter, userId, {
1329
+ storedValues: storedValuesMap
1330
+ });
1331
+ const observerRef = useRef3(null);
1332
+ const boundFieldsRef = useRef3(/* @__PURE__ */ new Set());
1333
+ const changesRef = useRef3([]);
1334
+ const updateStoredValue = useCallback5((fieldPath, value) => {
1335
+ storedValuesRef.current.set(fieldPath, value);
1336
+ setStoredValuesVersion((v) => v + 1);
1337
+ }, []);
1338
+ const handleFocus = useCallback5((e) => {
1339
+ const el = e.target;
1340
+ const fieldPath = el.dataset.feedtackField;
1341
+ if (!fieldPath) return;
1342
+ const storedValue = storedValuesRef.current.get(fieldPath) ?? getFieldValue(el);
1343
+ el.dataset.feedtackOriginal = storedValue;
1344
+ setFocusedField({ element: el, fieldPath });
1345
+ }, []);
1346
+ const handleBlur = useCallback5(
1347
+ async (e) => {
1348
+ const el = e.target;
1349
+ const fieldPath = el.dataset.feedtackField;
1350
+ if (!fieldPath) return;
1351
+ setTimeout(() => {
1352
+ const f = document.activeElement;
1353
+ if (!f || f === document.body || f.closest?.("[data-feedtack-edit-ui]"))
1354
+ return;
1355
+ setFocusedField(null);
1356
+ }, 150);
1357
+ const value = getFieldValue(el);
1358
+ const original = el.dataset.feedtackOriginal ?? "";
1359
+ if (value === original) return;
1360
+ if (!isContentEditAdapter(adapter)) return;
1361
+ setSaving(fieldPath);
1362
+ try {
1363
+ await adapter.saveField(fieldPath, value);
1364
+ updateStoredValue(fieldPath, value);
1365
+ const existing = changesRef.current.find(
1366
+ (c) => c.fieldPath === fieldPath
1367
+ );
1368
+ if (existing) {
1369
+ changesRef.current = changesRef.current.map(
1370
+ (c) => c.fieldPath === fieldPath ? { ...c, to: value, savedAt: Date.now() } : c
1371
+ );
1372
+ } else {
1373
+ changesRef.current = [
1374
+ ...changesRef.current,
1375
+ { fieldPath, from: original, to: value, savedAt: Date.now() }
1376
+ ];
1377
+ }
1378
+ setChanges([...changesRef.current]);
1379
+ void approval.rescan();
1380
+ } catch {
1381
+ setFieldValue(el, original);
1382
+ }
1383
+ setTimeout(() => setSaving(null), 1500);
1384
+ },
1385
+ [adapter, approval, updateStoredValue]
1386
+ );
1387
+ const bindField = useCallback5(
1388
+ (el) => {
1389
+ if (boundFieldsRef.current.has(el)) return;
1390
+ if (el.querySelector("[data-feedtack-field]")) return;
1391
+ boundFieldsRef.current.add(el);
1392
+ if (isFormField(el)) {
1393
+ el.style.cursor = "pointer";
1394
+ } else {
1395
+ el.setAttribute("contenteditable", "true");
1396
+ el.setAttribute("spellcheck", "true");
1397
+ }
1398
+ el.addEventListener("focus", handleFocus);
1399
+ el.addEventListener("blur", handleBlur);
1400
+ },
1401
+ [handleFocus, handleBlur]
1402
+ );
1403
+ const unbindField = useCallback5(
1404
+ (el) => {
1405
+ boundFieldsRef.current.delete(el);
1406
+ el.removeAttribute("contenteditable");
1407
+ el.removeAttribute("spellcheck");
1408
+ el.style.cursor = "";
1409
+ delete el.dataset.feedtackOriginal;
1410
+ el.removeEventListener("focus", handleFocus);
1411
+ el.removeEventListener("blur", handleBlur);
1412
+ },
1413
+ [handleFocus, handleBlur]
1414
+ );
1415
+ const setupFields = useCallback5(() => {
1416
+ document.querySelectorAll("[data-feedtack-field]").forEach(bindField);
1417
+ }, [bindField]);
1418
+ const activate = useCallback5(async () => {
1419
+ warnIfNotContentEditAdapter(adapter, "activate");
1420
+ if (!isContentEditAdapter(adapter)) return;
1421
+ document.body.setAttribute("data-feedtack-hydrating", "true");
1422
+ try {
1423
+ const fields = await adapter.loadFields();
1424
+ for (const [fp, value] of Object.entries(fields)) {
1425
+ storedValuesRef.current.set(fp, value);
1426
+ const el = findFieldElement(fp);
1427
+ if (el) setFieldValue(el, value);
1428
+ }
1429
+ setStoredValuesVersion((v) => v + 1);
1430
+ } finally {
1431
+ document.body.removeAttribute("data-feedtack-hydrating");
1432
+ }
1433
+ setupFields();
1434
+ const observer = new MutationObserver(() => setupFields());
1435
+ observer.observe(document.body, { childList: true, subtree: true });
1436
+ observerRef.current = observer;
1437
+ setActive(true);
1438
+ void approval.rescan();
1439
+ }, [adapter, setupFields, approval]);
1440
+ const deactivate = useCallback5(() => {
1441
+ observerRef.current?.disconnect();
1442
+ observerRef.current = null;
1443
+ boundFieldsRef.current.forEach(unbindField);
1444
+ boundFieldsRef.current.clear();
1445
+ setActive(false);
1446
+ setFocusedField(null);
1447
+ }, [unbindField]);
1448
+ const revert = useCallback5(
1449
+ async (fieldPath) => {
1450
+ warnIfNotContentEditAdapter(adapter, "revert");
1451
+ if (!isContentEditAdapter(adapter)) return;
1452
+ const change = changesRef.current.find((c) => c.fieldPath === fieldPath);
1453
+ if (!change) return;
1454
+ setSaving(fieldPath);
1455
+ try {
1456
+ await adapter.saveField(fieldPath, change.from);
1457
+ updateStoredValue(fieldPath, change.from);
1458
+ const el = findFieldElement(fieldPath);
1459
+ if (el) setFieldValue(el, change.from);
1460
+ changesRef.current = changesRef.current.filter(
1461
+ (c) => c.fieldPath !== fieldPath
1462
+ );
1463
+ setChanges([...changesRef.current]);
1464
+ void approval.rescan();
1465
+ } finally {
1466
+ setTimeout(() => setSaving(null), 1500);
1467
+ }
1468
+ },
1469
+ [adapter, approval, updateStoredValue]
1470
+ );
1471
+ const toolbarProps = buildToolbarProps(
1472
+ focusedField,
1473
+ approval.fields,
1474
+ changes,
1475
+ saving,
1476
+ approval,
1477
+ revert
1478
+ );
1479
+ return {
1480
+ active,
1481
+ activate,
1482
+ deactivate,
1483
+ changes,
1484
+ revert,
1485
+ saving,
1486
+ focusedField,
1487
+ toolbarProps,
1488
+ fields: approval.fields,
1489
+ approve: approval.approve,
1490
+ revoke: approval.revoke,
1491
+ rescan: approval.rescan,
1492
+ checkDeploy: approval.checkDeploy
1493
+ };
1494
+ }
1495
+
901
1496
  // src/react/useFeedtack.ts
902
1497
  function useFeedtack() {
903
1498
  return useFeedtackContext();
904
1499
  }
905
1500
  export {
1501
+ ContentEditToolbar,
906
1502
  FeedtackProvider,
907
1503
  PIN_PALETTE,
1504
+ useContentApproval,
1505
+ useContentEdit,
908
1506
  useFeedtack
909
1507
  };