feedtack 0.5.1 → 1.0.1
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 +45 -9
- package/dist/{chunk-PPM4AIJU.js → chunk-2A5LLDLP.js} +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -1
- package/dist/react/index.d.ts +4 -1
- package/dist/react/index.js +720 -116
- package/dist/{theme-C-uctIoI.d.ts → theme-BdqpMipn.d.ts} +8 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# feedtack
|
|
2
2
|
|
|
3
|
-
> Click anywhere. Drop a pin. Get a payload a developer can act on.
|
|
3
|
+
> Click anywhere. Drop a pin. Leave a note. Get a payload a developer can act on.
|
|
4
4
|
|
|
5
|
-
**feedtack** is a drop-in React feedback overlay.
|
|
5
|
+
**feedtack** is a drop-in React feedback overlay. A "Feedback" button opens a modal where anyone can leave site-wide notes, page-level comments, or place a pin on a specific element — all from one entry point. feedtack emits a structured JSON payload so complete that an LLM can attempt a first-pass fix before consuming developer hours.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -166,16 +166,19 @@ class SupabaseAdapter implements FeedtackAdapter {
|
|
|
166
166
|
|
|
167
167
|
## The payload
|
|
168
168
|
|
|
169
|
-
Every
|
|
169
|
+
Every submission emits a versioned JSON payload. The `scope` field indicates where the feedback lives.
|
|
170
|
+
|
|
171
|
+
**Element-scoped (pinned to a specific element):**
|
|
170
172
|
|
|
171
173
|
```json
|
|
172
174
|
{
|
|
173
|
-
"schemaVersion": "
|
|
175
|
+
"schemaVersion": "2.0.0",
|
|
174
176
|
"id": "ft_01j...",
|
|
175
177
|
"timestamp": "2026-04-09T13:42:00.000Z",
|
|
176
178
|
"submittedBy": { "id": "u1", "name": "Alice", "role": "designer" },
|
|
179
|
+
"scope": "element",
|
|
177
180
|
"comment": "This button doesn't do anything",
|
|
178
|
-
"sentiment": "
|
|
181
|
+
"sentiment": "bad",
|
|
179
182
|
"pins": [{
|
|
180
183
|
"index": 1,
|
|
181
184
|
"color": "#ef4444",
|
|
@@ -185,8 +188,8 @@ Every pin emits a versioned JSON payload:
|
|
|
185
188
|
"selector": "#submit-btn",
|
|
186
189
|
"best_effort": false,
|
|
187
190
|
"tagName": "BUTTON",
|
|
188
|
-
"
|
|
189
|
-
"
|
|
191
|
+
"dataTestId": "submit-btn",
|
|
192
|
+
"ancestors": ["form#checkout", "main"],
|
|
190
193
|
"boundingRect": { "x": 420, "y": 812, "width": 200, "height": 44 }
|
|
191
194
|
}
|
|
192
195
|
}],
|
|
@@ -196,14 +199,47 @@ Every pin emits a versioned JSON payload:
|
|
|
196
199
|
}
|
|
197
200
|
```
|
|
198
201
|
|
|
202
|
+
**Page or site-scoped (no pin):**
|
|
203
|
+
|
|
204
|
+
```json
|
|
205
|
+
{
|
|
206
|
+
"schemaVersion": "2.0.0",
|
|
207
|
+
"id": "ft_01k...",
|
|
208
|
+
"scope": "page",
|
|
209
|
+
"comment": "This page is really confusing — too many steps",
|
|
210
|
+
"sentiment": "bad",
|
|
211
|
+
"pins": [],
|
|
212
|
+
"page": { "url": "https://app.example.com/checkout", "pathname": "/checkout", "title": "Checkout" },
|
|
213
|
+
...
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
`sentiment` values: `"good"` | `"bad"` | `null`
|
|
218
|
+
|
|
219
|
+
## Feedback scopes
|
|
220
|
+
|
|
221
|
+
Feedtack supports three levels of feedback, all accessible from the modal:
|
|
222
|
+
|
|
223
|
+
| Scope | When to use | Pins |
|
|
224
|
+
|-------|-------------|------|
|
|
225
|
+
| `site` | Config-level feedback affecting the whole site (e.g. "change the font everywhere") | None |
|
|
226
|
+
| `page` | Feedback specific to the current page (e.g. "this page is confusing") | None |
|
|
227
|
+
| `element` | Feedback pinned to a specific element (e.g. "this button doesn't work") | One or more |
|
|
228
|
+
|
|
229
|
+
**How it works:**
|
|
230
|
+
- Clicking "Feedback" opens the modal
|
|
231
|
+
- The modal has **Site** and **Page** tabs for scope-level feedback
|
|
232
|
+
- "Place a pin" in the modal footer activates crosshair mode — useful on mobile too
|
|
233
|
+
- `Shift+P` anywhere on the page opens the modal
|
|
234
|
+
|
|
199
235
|
## `useFeedtack` hook
|
|
200
236
|
|
|
201
237
|
```tsx
|
|
202
238
|
import { useFeedtack } from 'feedtack/react'
|
|
203
239
|
|
|
204
240
|
function MyButton() {
|
|
205
|
-
const { activatePinMode, isPinModeActive } = useFeedtack()
|
|
206
|
-
return <button onClick={
|
|
241
|
+
const { openModal, closeModal, isModalOpen, activatePinMode, isPinModeActive } = useFeedtack()
|
|
242
|
+
return <button onClick={openModal}>{isModalOpen ? 'Close' : 'Give Feedback'}</button>
|
|
207
243
|
}
|
|
208
244
|
```
|
|
209
245
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { F as FeedtackAdapter, a as FeedtackPayload, b as FeedtackReply, c as FeedtackResolution, d as FeedtackFilter, e as FeedbackItem, f as FeedtackDeviceMeta, g as FeedtackPageMeta, h as FeedtackViewportMeta, i as FeedtackPinTarget } from './theme-
|
|
2
|
-
export { A as AncestorNode, j as FeedtackArchive, k as FeedtackBoundingRect, l as FeedtackPin, m as
|
|
1
|
+
import { F as FeedtackAdapter, a as FeedtackPayload, b as FeedtackReply, c as FeedtackResolution, d as FeedtackFilter, e as FeedbackItem, f as FeedtackDeviceMeta, g as FeedtackPageMeta, h as FeedtackViewportMeta, i as FeedtackPinTarget } from './theme-BdqpMipn.js';
|
|
2
|
+
export { A as AncestorNode, j as FeedtackArchive, k as FeedtackBoundingRect, l as FeedtackPin, m as FeedtackScope, n as FeedtackSentiment, o as FeedtackTheme, p as FeedtackUser, S as SCHEMA_VERSION, t as themeToCSS } from './theme-BdqpMipn.js';
|
|
3
3
|
|
|
4
4
|
/** Development adapter — logs all operations to the browser console */
|
|
5
5
|
declare class ConsoleAdapter implements FeedtackAdapter {
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
getTargetMeta,
|
|
8
8
|
getViewportMeta,
|
|
9
9
|
themeToCSS
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-2A5LLDLP.js";
|
|
11
11
|
|
|
12
12
|
// src/adapters/ConsoleAdapter.ts
|
|
13
13
|
var ConsoleAdapter = class {
|
|
@@ -83,6 +83,7 @@ var LocalStorageAdapter = class {
|
|
|
83
83
|
const items = this.read();
|
|
84
84
|
if (!filter) return items;
|
|
85
85
|
return items.filter((item) => {
|
|
86
|
+
if (filter.scope && item.payload.scope !== filter.scope) return false;
|
|
86
87
|
if (filter.pathname && item.payload.page.pathname !== filter.pathname)
|
|
87
88
|
return false;
|
|
88
89
|
if (filter.url && item.payload.page.url !== filter.url) return false;
|
package/dist/react/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import { e as FeedbackItem, F as FeedtackAdapter,
|
|
3
|
+
import { e as FeedbackItem, F as FeedtackAdapter, p as FeedtackUser, o as FeedtackTheme } from '../theme-BdqpMipn.js';
|
|
4
4
|
|
|
5
5
|
/** Fixed palette of 6 colors for pin markers */
|
|
6
6
|
declare const PIN_PALETTE: readonly ["#ef4444", "#3b82f6", "#22c55e", "#f59e0b", "#a855f7", "#ec4899"];
|
|
@@ -13,6 +13,9 @@ interface FeedtackContextValue {
|
|
|
13
13
|
selectedColor: string;
|
|
14
14
|
setSelectedColor: (color: string) => void;
|
|
15
15
|
pinPalette: readonly string[];
|
|
16
|
+
openModal: () => void;
|
|
17
|
+
closeModal: () => void;
|
|
18
|
+
isModalOpen: boolean;
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
interface FeedtackFlushEvent {
|
package/dist/react/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
getTargetMeta,
|
|
7
7
|
getViewportMeta,
|
|
8
8
|
themeToCSS
|
|
9
|
-
} from "../chunk-
|
|
9
|
+
} from "../chunk-2A5LLDLP.js";
|
|
10
10
|
|
|
11
11
|
// src/ui/colors.ts
|
|
12
12
|
var PIN_PALETTE = [
|
|
@@ -92,20 +92,18 @@ function CommentForm({
|
|
|
92
92
|
"button",
|
|
93
93
|
{
|
|
94
94
|
type: "button",
|
|
95
|
-
className: sentiment === "
|
|
96
|
-
onClick: () => onSentimentChange(sentiment === "
|
|
97
|
-
children: sentimentLabels.satisfied ?? "
|
|
95
|
+
className: sentiment === "good" ? "selected" : "",
|
|
96
|
+
onClick: () => onSentimentChange(sentiment === "good" ? null : "good"),
|
|
97
|
+
children: sentimentLabels.satisfied ?? "Good"
|
|
98
98
|
}
|
|
99
99
|
),
|
|
100
100
|
/* @__PURE__ */ jsx(
|
|
101
101
|
"button",
|
|
102
102
|
{
|
|
103
103
|
type: "button",
|
|
104
|
-
className: sentiment === "
|
|
105
|
-
onClick: () => onSentimentChange(
|
|
106
|
-
|
|
107
|
-
),
|
|
108
|
-
children: sentimentLabels.dissatisfied ?? "\u{1F61E} Dissatisfied"
|
|
104
|
+
className: sentiment === "bad" ? "selected" : "",
|
|
105
|
+
onClick: () => onSentimentChange(sentiment === "bad" ? null : "bad"),
|
|
106
|
+
children: sentimentLabels.dissatisfied ?? "Bad"
|
|
109
107
|
}
|
|
110
108
|
)
|
|
111
109
|
] }),
|
|
@@ -145,8 +143,311 @@ function useFeedtackContext() {
|
|
|
145
143
|
return ctx;
|
|
146
144
|
}
|
|
147
145
|
|
|
148
|
-
// src/react/
|
|
146
|
+
// src/react/FeedbackModal.tsx
|
|
147
|
+
import { useEffect, useRef } from "react";
|
|
148
|
+
|
|
149
|
+
// src/react/ThreadView.tsx
|
|
149
150
|
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
151
|
+
function ThreadView({
|
|
152
|
+
item,
|
|
153
|
+
replyBody,
|
|
154
|
+
onReplyBodyChange,
|
|
155
|
+
onReply,
|
|
156
|
+
onResolve,
|
|
157
|
+
onArchive,
|
|
158
|
+
onBack
|
|
159
|
+
}) {
|
|
160
|
+
return /* @__PURE__ */ jsxs2("div", { className: "feedtack-modal-thread-view", children: [
|
|
161
|
+
/* @__PURE__ */ jsx2("button", { type: "button", className: "feedtack-modal-back", onClick: onBack, children: "\u2190 Back" }),
|
|
162
|
+
/* @__PURE__ */ jsxs2("div", { className: "feedtack-modal-thread-content", children: [
|
|
163
|
+
/* @__PURE__ */ jsx2("strong", { children: item.payload.submittedBy.name }),
|
|
164
|
+
/* @__PURE__ */ jsx2("p", { children: item.payload.comment })
|
|
165
|
+
] }),
|
|
166
|
+
item.replies.map((r) => /* @__PURE__ */ jsxs2("div", { className: "feedtack-modal-reply", children: [
|
|
167
|
+
/* @__PURE__ */ jsx2("span", { className: "feedtack-reply-author", children: r.author.name }),
|
|
168
|
+
/* @__PURE__ */ jsx2("p", { children: r.body })
|
|
169
|
+
] }, r.id)),
|
|
170
|
+
/* @__PURE__ */ jsx2(
|
|
171
|
+
"textarea",
|
|
172
|
+
{
|
|
173
|
+
className: "feedtack-modal-textarea",
|
|
174
|
+
placeholder: "Reply\u2026",
|
|
175
|
+
value: replyBody,
|
|
176
|
+
onChange: (e) => onReplyBodyChange(e.target.value)
|
|
177
|
+
}
|
|
178
|
+
),
|
|
179
|
+
/* @__PURE__ */ jsxs2("div", { className: "feedtack-modal-actions", children: [
|
|
180
|
+
/* @__PURE__ */ jsx2("button", { type: "button", className: "feedtack-btn-submit", onClick: onReply, children: "Reply" }),
|
|
181
|
+
/* @__PURE__ */ jsx2(
|
|
182
|
+
"button",
|
|
183
|
+
{
|
|
184
|
+
type: "button",
|
|
185
|
+
className: "feedtack-btn-cancel",
|
|
186
|
+
onClick: onResolve,
|
|
187
|
+
children: "Resolve"
|
|
188
|
+
}
|
|
189
|
+
),
|
|
190
|
+
/* @__PURE__ */ jsx2(
|
|
191
|
+
"button",
|
|
192
|
+
{
|
|
193
|
+
type: "button",
|
|
194
|
+
className: "feedtack-btn-cancel",
|
|
195
|
+
onClick: onArchive,
|
|
196
|
+
children: "Archive"
|
|
197
|
+
}
|
|
198
|
+
)
|
|
199
|
+
] })
|
|
200
|
+
] });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/react/FeedbackModal.tsx
|
|
204
|
+
import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
205
|
+
function FeedbackModal({
|
|
206
|
+
isOpen,
|
|
207
|
+
onClose,
|
|
208
|
+
activeTab,
|
|
209
|
+
onTabChange,
|
|
210
|
+
siteFeedback,
|
|
211
|
+
pageFeedback,
|
|
212
|
+
comment,
|
|
213
|
+
onCommentChange,
|
|
214
|
+
commentError,
|
|
215
|
+
sentiment,
|
|
216
|
+
onSentimentChange,
|
|
217
|
+
submitting,
|
|
218
|
+
onSubmit,
|
|
219
|
+
onPlacePin,
|
|
220
|
+
replyBody,
|
|
221
|
+
onReplyBodyChange,
|
|
222
|
+
onReply,
|
|
223
|
+
onResolve,
|
|
224
|
+
onArchive,
|
|
225
|
+
openThreadId,
|
|
226
|
+
onOpenThread
|
|
227
|
+
}) {
|
|
228
|
+
const panelRef = useRef(null);
|
|
229
|
+
useEffect(() => {
|
|
230
|
+
if (!isOpen) return;
|
|
231
|
+
const onKey = (e) => {
|
|
232
|
+
if (e.key === "Escape") onClose();
|
|
233
|
+
};
|
|
234
|
+
const onDown = (e) => {
|
|
235
|
+
if (panelRef.current && !panelRef.current.contains(e.target)) {
|
|
236
|
+
const btn = document.querySelector(".feedtack-btn");
|
|
237
|
+
if (btn?.contains(e.target)) return;
|
|
238
|
+
onClose();
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
window.addEventListener("keydown", onKey);
|
|
242
|
+
document.addEventListener("mousedown", onDown);
|
|
243
|
+
return () => {
|
|
244
|
+
window.removeEventListener("keydown", onKey);
|
|
245
|
+
document.removeEventListener("mousedown", onDown);
|
|
246
|
+
};
|
|
247
|
+
}, [isOpen, onClose]);
|
|
248
|
+
if (!isOpen) return null;
|
|
249
|
+
const threads = activeTab === "site" ? siteFeedback : pageFeedback;
|
|
250
|
+
const openItem = openThreadId ? threads.find((i) => i.payload.id === openThreadId) : null;
|
|
251
|
+
return /* @__PURE__ */ jsxs3(
|
|
252
|
+
"div",
|
|
253
|
+
{
|
|
254
|
+
ref: panelRef,
|
|
255
|
+
className: "feedtack-modal",
|
|
256
|
+
role: "dialog",
|
|
257
|
+
"aria-label": "Feedback",
|
|
258
|
+
"aria-modal": "true",
|
|
259
|
+
children: [
|
|
260
|
+
/* @__PURE__ */ jsxs3("div", { className: "feedtack-modal-header", children: [
|
|
261
|
+
/* @__PURE__ */ jsx3("span", { className: "feedtack-modal-title", children: "Feedback" }),
|
|
262
|
+
/* @__PURE__ */ jsx3(
|
|
263
|
+
"button",
|
|
264
|
+
{
|
|
265
|
+
type: "button",
|
|
266
|
+
className: "feedtack-modal-close",
|
|
267
|
+
onClick: onClose,
|
|
268
|
+
"aria-label": "Close",
|
|
269
|
+
children: "\xD7"
|
|
270
|
+
}
|
|
271
|
+
)
|
|
272
|
+
] }),
|
|
273
|
+
/* @__PURE__ */ jsxs3("div", { className: "feedtack-modal-tabs", children: [
|
|
274
|
+
/* @__PURE__ */ jsxs3(
|
|
275
|
+
"button",
|
|
276
|
+
{
|
|
277
|
+
type: "button",
|
|
278
|
+
className: cx("feedtack-modal-tab", activeTab === "site" && "active"),
|
|
279
|
+
onClick: () => onTabChange("site"),
|
|
280
|
+
children: [
|
|
281
|
+
"Site",
|
|
282
|
+
siteFeedback.length > 0 && /* @__PURE__ */ jsx3("span", { className: "feedtack-tab-count", children: siteFeedback.length })
|
|
283
|
+
]
|
|
284
|
+
}
|
|
285
|
+
),
|
|
286
|
+
/* @__PURE__ */ jsxs3(
|
|
287
|
+
"button",
|
|
288
|
+
{
|
|
289
|
+
type: "button",
|
|
290
|
+
className: cx("feedtack-modal-tab", activeTab === "page" && "active"),
|
|
291
|
+
onClick: () => onTabChange("page"),
|
|
292
|
+
children: [
|
|
293
|
+
"Page",
|
|
294
|
+
pageFeedback.length > 0 && /* @__PURE__ */ jsx3("span", { className: "feedtack-tab-count", children: pageFeedback.length })
|
|
295
|
+
]
|
|
296
|
+
}
|
|
297
|
+
)
|
|
298
|
+
] }),
|
|
299
|
+
/* @__PURE__ */ jsx3("div", { className: "feedtack-modal-body", children: openItem ? /* @__PURE__ */ jsx3(
|
|
300
|
+
ThreadView,
|
|
301
|
+
{
|
|
302
|
+
item: openItem,
|
|
303
|
+
replyBody,
|
|
304
|
+
onReplyBodyChange,
|
|
305
|
+
onReply: () => onReply(openItem.payload.id),
|
|
306
|
+
onResolve: () => onResolve(openItem.payload.id),
|
|
307
|
+
onArchive: () => onArchive(openItem.payload.id),
|
|
308
|
+
onBack: () => onOpenThread(null)
|
|
309
|
+
}
|
|
310
|
+
) : /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
311
|
+
threads.length > 0 && /* @__PURE__ */ jsx3("div", { className: "feedtack-modal-threads", children: threads.map((item) => /* @__PURE__ */ jsxs3(
|
|
312
|
+
"button",
|
|
313
|
+
{
|
|
314
|
+
type: "button",
|
|
315
|
+
className: "feedtack-modal-thread-item",
|
|
316
|
+
onClick: () => onOpenThread(item.payload.id),
|
|
317
|
+
children: [
|
|
318
|
+
/* @__PURE__ */ jsx3("span", { className: "feedtack-thread-author", children: item.payload.submittedBy.name }),
|
|
319
|
+
/* @__PURE__ */ jsx3("span", { className: "feedtack-thread-comment", children: item.payload.comment }),
|
|
320
|
+
/* @__PURE__ */ jsxs3("span", { className: "feedtack-thread-meta", children: [
|
|
321
|
+
item.replies.length > 0 && `${item.replies.length} ${item.replies.length === 1 ? "reply" : "replies"}`,
|
|
322
|
+
item.resolutions.length > 0 && " \xB7 resolved"
|
|
323
|
+
] })
|
|
324
|
+
]
|
|
325
|
+
},
|
|
326
|
+
item.payload.id
|
|
327
|
+
)) }),
|
|
328
|
+
/* @__PURE__ */ jsxs3("div", { className: "feedtack-modal-compose", children: [
|
|
329
|
+
/* @__PURE__ */ jsx3(
|
|
330
|
+
"textarea",
|
|
331
|
+
{
|
|
332
|
+
className: cx(
|
|
333
|
+
"feedtack-modal-textarea",
|
|
334
|
+
commentError && "error"
|
|
335
|
+
),
|
|
336
|
+
placeholder: "What's on your mind? (required)",
|
|
337
|
+
value: comment,
|
|
338
|
+
onChange: (e) => onCommentChange(e.target.value),
|
|
339
|
+
onKeyDown: (e) => {
|
|
340
|
+
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
|
341
|
+
e.preventDefault();
|
|
342
|
+
onSubmit();
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
"aria-invalid": commentError || void 0
|
|
346
|
+
}
|
|
347
|
+
),
|
|
348
|
+
commentError && /* @__PURE__ */ jsx3("span", { className: "feedtack-error-msg", children: "Comment is required" }),
|
|
349
|
+
/* @__PURE__ */ jsxs3("div", { className: "feedtack-sentiment", children: [
|
|
350
|
+
/* @__PURE__ */ jsx3(
|
|
351
|
+
"button",
|
|
352
|
+
{
|
|
353
|
+
type: "button",
|
|
354
|
+
className: sentiment === "good" ? "selected" : "",
|
|
355
|
+
onClick: () => onSentimentChange(sentiment === "good" ? null : "good"),
|
|
356
|
+
children: "Good"
|
|
357
|
+
}
|
|
358
|
+
),
|
|
359
|
+
/* @__PURE__ */ jsx3(
|
|
360
|
+
"button",
|
|
361
|
+
{
|
|
362
|
+
type: "button",
|
|
363
|
+
className: sentiment === "bad" ? "selected" : "",
|
|
364
|
+
onClick: () => onSentimentChange(sentiment === "bad" ? null : "bad"),
|
|
365
|
+
children: "Bad"
|
|
366
|
+
}
|
|
367
|
+
)
|
|
368
|
+
] }),
|
|
369
|
+
/* @__PURE__ */ jsx3(
|
|
370
|
+
"button",
|
|
371
|
+
{
|
|
372
|
+
type: "button",
|
|
373
|
+
className: "feedtack-btn-submit",
|
|
374
|
+
onClick: onSubmit,
|
|
375
|
+
disabled: submitting,
|
|
376
|
+
children: submitting ? "Sending\u2026" : "Submit"
|
|
377
|
+
}
|
|
378
|
+
)
|
|
379
|
+
] })
|
|
380
|
+
] }) }),
|
|
381
|
+
/* @__PURE__ */ jsx3("div", { className: "feedtack-modal-footer", children: /* @__PURE__ */ jsx3(
|
|
382
|
+
"button",
|
|
383
|
+
{
|
|
384
|
+
type: "button",
|
|
385
|
+
className: "feedtack-modal-pin-btn",
|
|
386
|
+
onClick: onPlacePin,
|
|
387
|
+
children: "Place a pin"
|
|
388
|
+
}
|
|
389
|
+
) })
|
|
390
|
+
]
|
|
391
|
+
}
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// src/react/PinOverlay.tsx
|
|
396
|
+
import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
397
|
+
function PinOverlay({
|
|
398
|
+
feedbackItems,
|
|
399
|
+
pathname,
|
|
400
|
+
isArchivedForUser,
|
|
401
|
+
hasValidPins,
|
|
402
|
+
hasUnread,
|
|
403
|
+
openThreadId,
|
|
404
|
+
setOpenThreadId,
|
|
405
|
+
getPosition,
|
|
406
|
+
renderPinIcon,
|
|
407
|
+
pinMarkerClass
|
|
408
|
+
}) {
|
|
409
|
+
return /* @__PURE__ */ jsx4(Fragment2, { children: feedbackItems.filter((item) => item.payload.page.pathname === pathname).filter((item) => !isArchivedForUser(item)).filter((item) => hasValidPins(item)).map((item) => {
|
|
410
|
+
const pin = item.payload.pins[0];
|
|
411
|
+
const pos = getPosition(item.payload.id, pin);
|
|
412
|
+
return /* @__PURE__ */ jsxs4(
|
|
413
|
+
"button",
|
|
414
|
+
{
|
|
415
|
+
type: "button",
|
|
416
|
+
className: cx(
|
|
417
|
+
"feedtack-pin-marker",
|
|
418
|
+
item.resolutions.length > 0 && "feedtack-pin-resolved",
|
|
419
|
+
pinMarkerClass
|
|
420
|
+
),
|
|
421
|
+
style: {
|
|
422
|
+
background: pin.color,
|
|
423
|
+
left: pos.x,
|
|
424
|
+
top: pos.y,
|
|
425
|
+
position: "absolute",
|
|
426
|
+
cursor: "pointer"
|
|
427
|
+
},
|
|
428
|
+
onClick: () => setOpenThreadId(
|
|
429
|
+
openThreadId === item.payload.id ? null : item.payload.id
|
|
430
|
+
),
|
|
431
|
+
children: [
|
|
432
|
+
renderPinIcon ? /* @__PURE__ */ jsx4("span", { className: "feedtack-pin-icon", children: renderPinIcon(item) }) : item.resolutions.length > 0 && /* @__PURE__ */ jsx4(
|
|
433
|
+
"span",
|
|
434
|
+
{
|
|
435
|
+
className: "feedtack-pin-icon",
|
|
436
|
+
role: "img",
|
|
437
|
+
"aria-label": "Resolved",
|
|
438
|
+
children: "\u2713"
|
|
439
|
+
}
|
|
440
|
+
),
|
|
441
|
+
hasUnread(item) && /* @__PURE__ */ jsx4("div", { className: "feedtack-pin-badge" })
|
|
442
|
+
]
|
|
443
|
+
},
|
|
444
|
+
item.payload.id
|
|
445
|
+
);
|
|
446
|
+
}) });
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// src/react/ThreadPanel.tsx
|
|
450
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
150
451
|
function ThreadPanel({
|
|
151
452
|
item,
|
|
152
453
|
replyBody,
|
|
@@ -162,26 +463,26 @@ function ThreadPanel({
|
|
|
162
463
|
if (!pin) return null;
|
|
163
464
|
const { x, y } = pinPosition ?? pin;
|
|
164
465
|
const pos = getAnchoredPosition(x, y);
|
|
165
|
-
return /* @__PURE__ */
|
|
466
|
+
return /* @__PURE__ */ jsxs5(
|
|
166
467
|
"div",
|
|
167
468
|
{
|
|
168
469
|
className: cx("feedtack-thread", className),
|
|
169
470
|
style: { position: "fixed", ...pos },
|
|
170
471
|
children: [
|
|
171
|
-
/* @__PURE__ */
|
|
172
|
-
/* @__PURE__ */
|
|
173
|
-
item.replies.map((r) => /* @__PURE__ */
|
|
472
|
+
/* @__PURE__ */ jsx5("strong", { style: { fontSize: 13 }, children: item.payload.submittedBy.name }),
|
|
473
|
+
/* @__PURE__ */ jsx5("p", { style: { fontSize: 13 }, children: item.payload.comment }),
|
|
474
|
+
item.replies.map((r) => /* @__PURE__ */ jsxs5(
|
|
174
475
|
"div",
|
|
175
476
|
{
|
|
176
477
|
style: { borderTop: "1px solid var(--ft-border)", paddingTop: 8 },
|
|
177
478
|
children: [
|
|
178
|
-
/* @__PURE__ */
|
|
179
|
-
/* @__PURE__ */
|
|
479
|
+
/* @__PURE__ */ jsx5("span", { style: { fontSize: 12, fontWeight: 600 }, children: r.author.name }),
|
|
480
|
+
/* @__PURE__ */ jsx5("p", { style: { fontSize: 12 }, children: r.body })
|
|
180
481
|
]
|
|
181
482
|
},
|
|
182
483
|
r.id
|
|
183
484
|
)),
|
|
184
|
-
/* @__PURE__ */
|
|
485
|
+
/* @__PURE__ */ jsx5(
|
|
185
486
|
"textarea",
|
|
186
487
|
{
|
|
187
488
|
placeholder: "Reply\u2026",
|
|
@@ -201,8 +502,8 @@ function ThreadPanel({
|
|
|
201
502
|
}
|
|
202
503
|
}
|
|
203
504
|
),
|
|
204
|
-
/* @__PURE__ */
|
|
205
|
-
/* @__PURE__ */
|
|
505
|
+
/* @__PURE__ */ jsxs5("div", { style: { display: "flex", gap: 6, flexWrap: "wrap" }, children: [
|
|
506
|
+
/* @__PURE__ */ jsx5(
|
|
206
507
|
"button",
|
|
207
508
|
{
|
|
208
509
|
type: "button",
|
|
@@ -212,7 +513,7 @@ function ThreadPanel({
|
|
|
212
513
|
children: "Reply"
|
|
213
514
|
}
|
|
214
515
|
),
|
|
215
|
-
/* @__PURE__ */
|
|
516
|
+
/* @__PURE__ */ jsx5(
|
|
216
517
|
"button",
|
|
217
518
|
{
|
|
218
519
|
type: "button",
|
|
@@ -222,7 +523,7 @@ function ThreadPanel({
|
|
|
222
523
|
children: "Mark Resolved"
|
|
223
524
|
}
|
|
224
525
|
),
|
|
225
|
-
/* @__PURE__ */
|
|
526
|
+
/* @__PURE__ */ jsx5(
|
|
226
527
|
"button",
|
|
227
528
|
{
|
|
228
529
|
type: "button",
|
|
@@ -232,7 +533,7 @@ function ThreadPanel({
|
|
|
232
533
|
children: "Archive"
|
|
233
534
|
}
|
|
234
535
|
),
|
|
235
|
-
/* @__PURE__ */
|
|
536
|
+
/* @__PURE__ */ jsx5(
|
|
236
537
|
"button",
|
|
237
538
|
{
|
|
238
539
|
type: "button",
|
|
@@ -249,7 +550,7 @@ function ThreadPanel({
|
|
|
249
550
|
}
|
|
250
551
|
|
|
251
552
|
// src/react/useAnchoredPins.ts
|
|
252
|
-
import { useCallback, useEffect, useState } from "react";
|
|
553
|
+
import { useCallback, useEffect as useEffect2, useState } from "react";
|
|
253
554
|
function useAnchoredPins(items, pathname) {
|
|
254
555
|
const [positions, setPositions] = useState(
|
|
255
556
|
/* @__PURE__ */ new Map()
|
|
@@ -265,10 +566,10 @@ function useAnchoredPins(items, pathname) {
|
|
|
265
566
|
}
|
|
266
567
|
setPositions(next);
|
|
267
568
|
}, [items, pathname]);
|
|
268
|
-
|
|
569
|
+
useEffect2(() => {
|
|
269
570
|
resolve();
|
|
270
571
|
}, [resolve]);
|
|
271
|
-
|
|
572
|
+
useEffect2(() => {
|
|
272
573
|
let raf = 0;
|
|
273
574
|
const handler = () => {
|
|
274
575
|
cancelAnimationFrame(raf);
|
|
@@ -310,7 +611,7 @@ function resolvePin(pin) {
|
|
|
310
611
|
}
|
|
311
612
|
|
|
312
613
|
// src/react/useFeedtackState.ts
|
|
313
|
-
import { useCallback as useCallback5, useEffect as
|
|
614
|
+
import { useCallback as useCallback5, useEffect as useEffect6, useState as useState3 } from "react";
|
|
314
615
|
|
|
315
616
|
// src/react/useFeedtackActions.ts
|
|
316
617
|
import { useCallback as useCallback2 } from "react";
|
|
@@ -333,6 +634,7 @@ function useFeedtackActions(deps) {
|
|
|
333
634
|
schemaVersion: SCHEMA_VERSION,
|
|
334
635
|
id: generateId(),
|
|
335
636
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
637
|
+
scope: deps.getScope(),
|
|
336
638
|
submittedBy: currentUser,
|
|
337
639
|
comment: comment.trim(),
|
|
338
640
|
sentiment: deps.getSentiment(),
|
|
@@ -436,11 +738,251 @@ function useFeedtackActions(deps) {
|
|
|
436
738
|
}
|
|
437
739
|
|
|
438
740
|
// src/react/useFeedtackDom.ts
|
|
439
|
-
import { useEffect as
|
|
741
|
+
import { useEffect as useEffect3, useRef as useRef2 } from "react";
|
|
742
|
+
|
|
743
|
+
// src/ui/modalStyles.ts
|
|
744
|
+
var FEEDTACK_MODAL_STYLES = `
|
|
745
|
+
.feedtack-loading {
|
|
746
|
+
position: fixed;
|
|
747
|
+
bottom: 70px;
|
|
748
|
+
right: 24px;
|
|
749
|
+
font-size: 12px;
|
|
750
|
+
color: var(--ft-text-muted);
|
|
751
|
+
z-index: 2147483640;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
.feedtack-modal {
|
|
755
|
+
position: fixed;
|
|
756
|
+
bottom: 72px;
|
|
757
|
+
right: 24px;
|
|
758
|
+
width: 360px;
|
|
759
|
+
max-height: 70vh;
|
|
760
|
+
background: var(--ft-bg);
|
|
761
|
+
border: 1px solid var(--ft-border);
|
|
762
|
+
border-radius: calc(var(--ft-radius) + 4px);
|
|
763
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.18);
|
|
764
|
+
z-index: 2147483643;
|
|
765
|
+
display: flex;
|
|
766
|
+
flex-direction: column;
|
|
767
|
+
overflow: hidden;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
.feedtack-modal-header {
|
|
771
|
+
display: flex;
|
|
772
|
+
align-items: center;
|
|
773
|
+
justify-content: space-between;
|
|
774
|
+
padding: 14px 16px 0;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
.feedtack-modal-title {
|
|
778
|
+
font-size: 15px;
|
|
779
|
+
font-weight: 600;
|
|
780
|
+
color: var(--ft-text);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
.feedtack-modal-close {
|
|
784
|
+
background: none;
|
|
785
|
+
border: none;
|
|
786
|
+
font-size: 20px;
|
|
787
|
+
cursor: pointer;
|
|
788
|
+
color: var(--ft-text-muted);
|
|
789
|
+
line-height: 1;
|
|
790
|
+
padding: 0 4px;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
.feedtack-modal-tabs {
|
|
794
|
+
display: flex;
|
|
795
|
+
gap: 0;
|
|
796
|
+
padding: 12px 16px 0;
|
|
797
|
+
border-bottom: 1px solid var(--ft-border);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
.feedtack-modal-tab {
|
|
801
|
+
flex: 1;
|
|
802
|
+
padding: 8px 12px;
|
|
803
|
+
border: none;
|
|
804
|
+
background: none;
|
|
805
|
+
font-size: 13px;
|
|
806
|
+
font-weight: 500;
|
|
807
|
+
cursor: pointer;
|
|
808
|
+
color: var(--ft-text-muted);
|
|
809
|
+
border-bottom: 2px solid transparent;
|
|
810
|
+
margin-bottom: -1px;
|
|
811
|
+
display: flex;
|
|
812
|
+
align-items: center;
|
|
813
|
+
justify-content: center;
|
|
814
|
+
gap: 6px;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
.feedtack-modal-tab.active {
|
|
818
|
+
color: var(--ft-primary);
|
|
819
|
+
border-bottom-color: var(--ft-primary);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
.feedtack-tab-count {
|
|
823
|
+
font-size: 11px;
|
|
824
|
+
background: var(--ft-surface);
|
|
825
|
+
color: var(--ft-text-muted);
|
|
826
|
+
padding: 1px 6px;
|
|
827
|
+
border-radius: 10px;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
.feedtack-modal-body {
|
|
831
|
+
flex: 1;
|
|
832
|
+
overflow-y: auto;
|
|
833
|
+
padding: 12px 16px;
|
|
834
|
+
display: flex;
|
|
835
|
+
flex-direction: column;
|
|
836
|
+
gap: 12px;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
.feedtack-modal-threads {
|
|
840
|
+
display: flex;
|
|
841
|
+
flex-direction: column;
|
|
842
|
+
gap: 6px;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
.feedtack-modal-thread-item {
|
|
846
|
+
display: flex;
|
|
847
|
+
flex-direction: column;
|
|
848
|
+
gap: 2px;
|
|
849
|
+
text-align: left;
|
|
850
|
+
padding: 10px 12px;
|
|
851
|
+
background: var(--ft-surface);
|
|
852
|
+
border: 1px solid var(--ft-border);
|
|
853
|
+
border-radius: var(--ft-radius);
|
|
854
|
+
cursor: pointer;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
.feedtack-modal-thread-item:hover {
|
|
858
|
+
border-color: var(--ft-primary);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
.feedtack-thread-author {
|
|
862
|
+
font-size: 12px;
|
|
863
|
+
font-weight: 600;
|
|
864
|
+
color: var(--ft-text);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
.feedtack-thread-comment {
|
|
868
|
+
font-size: 13px;
|
|
869
|
+
color: var(--ft-text);
|
|
870
|
+
overflow: hidden;
|
|
871
|
+
text-overflow: ellipsis;
|
|
872
|
+
white-space: nowrap;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
.feedtack-thread-meta {
|
|
876
|
+
font-size: 11px;
|
|
877
|
+
color: var(--ft-text-muted);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
.feedtack-modal-compose {
|
|
881
|
+
display: flex;
|
|
882
|
+
flex-direction: column;
|
|
883
|
+
gap: 8px;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
.feedtack-modal-textarea {
|
|
887
|
+
width: 100%;
|
|
888
|
+
border: 1.5px solid var(--ft-border);
|
|
889
|
+
border-radius: var(--ft-radius);
|
|
890
|
+
padding: 8px;
|
|
891
|
+
font-size: 13px;
|
|
892
|
+
resize: vertical;
|
|
893
|
+
min-height: 72px;
|
|
894
|
+
outline: none;
|
|
895
|
+
background: var(--ft-surface);
|
|
896
|
+
color: var(--ft-text);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
.feedtack-modal-textarea:focus {
|
|
900
|
+
border-color: var(--ft-primary);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
.feedtack-modal-textarea.error {
|
|
904
|
+
border-color: var(--ft-error);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
.feedtack-modal-footer {
|
|
908
|
+
padding: 10px 16px 14px;
|
|
909
|
+
border-top: 1px solid var(--ft-border);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
.feedtack-modal-pin-btn {
|
|
913
|
+
width: 100%;
|
|
914
|
+
padding: 8px 14px;
|
|
915
|
+
border: 1.5px solid var(--ft-border);
|
|
916
|
+
border-radius: var(--ft-radius);
|
|
917
|
+
background: var(--ft-bg);
|
|
918
|
+
color: var(--ft-text);
|
|
919
|
+
font-size: 13px;
|
|
920
|
+
font-weight: 500;
|
|
921
|
+
cursor: pointer;
|
|
922
|
+
transition: border-color 0.15s;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
.feedtack-modal-pin-btn:hover {
|
|
926
|
+
border-color: var(--ft-primary);
|
|
927
|
+
color: var(--ft-primary);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
.feedtack-modal-thread-view {
|
|
931
|
+
display: flex;
|
|
932
|
+
flex-direction: column;
|
|
933
|
+
gap: 10px;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
.feedtack-modal-back {
|
|
937
|
+
background: none;
|
|
938
|
+
border: none;
|
|
939
|
+
font-size: 13px;
|
|
940
|
+
color: var(--ft-primary);
|
|
941
|
+
cursor: pointer;
|
|
942
|
+
padding: 0;
|
|
943
|
+
text-align: left;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
.feedtack-modal-thread-content {
|
|
947
|
+
display: flex;
|
|
948
|
+
flex-direction: column;
|
|
949
|
+
gap: 4px;
|
|
950
|
+
font-size: 13px;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
.feedtack-modal-reply {
|
|
954
|
+
border-top: 1px solid var(--ft-border);
|
|
955
|
+
padding-top: 8px;
|
|
956
|
+
font-size: 12px;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
.feedtack-reply-author {
|
|
960
|
+
font-weight: 600;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
.feedtack-modal-actions {
|
|
964
|
+
display: flex;
|
|
965
|
+
gap: 6px;
|
|
966
|
+
flex-wrap: wrap;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
@media (max-width: 480px) {
|
|
970
|
+
.feedtack-modal {
|
|
971
|
+
right: 0;
|
|
972
|
+
bottom: 64px;
|
|
973
|
+
width: 100vw;
|
|
974
|
+
max-height: 85vh;
|
|
975
|
+
border-radius: var(--ft-radius) var(--ft-radius) 0 0;
|
|
976
|
+
border-left: none;
|
|
977
|
+
border-right: none;
|
|
978
|
+
border-bottom: none;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
`;
|
|
440
982
|
|
|
441
983
|
// src/ui/styles.ts
|
|
442
984
|
var FEEDTACK_DEFAULT_TOKENS = `
|
|
443
|
-
#feedtack-root, .feedtack-form, .feedtack-thread {
|
|
985
|
+
#feedtack-root, .feedtack-form, .feedtack-thread, .feedtack-modal {
|
|
444
986
|
--ft-primary: #2563eb;
|
|
445
987
|
--ft-primary-hover: #1d4ed8;
|
|
446
988
|
--ft-bg: #ffffff;
|
|
@@ -676,20 +1218,12 @@ var FEEDTACK_STYLES = `
|
|
|
676
1218
|
overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
|
|
677
1219
|
}
|
|
678
1220
|
|
|
679
|
-
|
|
680
|
-
position: fixed;
|
|
681
|
-
bottom: 70px;
|
|
682
|
-
right: 24px;
|
|
683
|
-
font-size: 12px;
|
|
684
|
-
color: var(--ft-text-muted);
|
|
685
|
-
z-index: 2147483640;
|
|
686
|
-
}
|
|
687
|
-
`;
|
|
1221
|
+
` + FEEDTACK_MODAL_STYLES;
|
|
688
1222
|
|
|
689
1223
|
// src/react/useFeedtackDom.ts
|
|
690
1224
|
function useFeedtackDom(theme, disabled) {
|
|
691
|
-
const rootRef =
|
|
692
|
-
|
|
1225
|
+
const rootRef = useRef2(null);
|
|
1226
|
+
useEffect3(() => {
|
|
693
1227
|
if (disabled) return;
|
|
694
1228
|
if (document.getElementById("feedtack-styles")) return;
|
|
695
1229
|
const style = document.createElement("style");
|
|
@@ -700,7 +1234,7 @@ function useFeedtackDom(theme, disabled) {
|
|
|
700
1234
|
style.remove();
|
|
701
1235
|
};
|
|
702
1236
|
}, [disabled]);
|
|
703
|
-
|
|
1237
|
+
useEffect3(() => {
|
|
704
1238
|
if (disabled) return;
|
|
705
1239
|
const root = document.createElement("div");
|
|
706
1240
|
root.id = "feedtack-root";
|
|
@@ -710,7 +1244,7 @@ function useFeedtackDom(theme, disabled) {
|
|
|
710
1244
|
root.remove();
|
|
711
1245
|
};
|
|
712
1246
|
}, [disabled]);
|
|
713
|
-
|
|
1247
|
+
useEffect3(() => {
|
|
714
1248
|
if (disabled) return;
|
|
715
1249
|
const root = document.getElementById("feedtack-root");
|
|
716
1250
|
if (!root || !theme) return;
|
|
@@ -723,7 +1257,7 @@ function useFeedtackDom(theme, disabled) {
|
|
|
723
1257
|
}
|
|
724
1258
|
|
|
725
1259
|
// src/react/useFeedtackFlush.ts
|
|
726
|
-
import { useCallback as useCallback3, useEffect as
|
|
1260
|
+
import { useCallback as useCallback3, useEffect as useEffect4, useRef as useRef3 } from "react";
|
|
727
1261
|
var DEFAULT_IDLE_MS = 5 * 60 * 1e3;
|
|
728
1262
|
function useFeedtackFlush({
|
|
729
1263
|
pathname,
|
|
@@ -732,9 +1266,9 @@ function useFeedtackFlush({
|
|
|
732
1266
|
flushIdleMs = DEFAULT_IDLE_MS,
|
|
733
1267
|
disabled
|
|
734
1268
|
}) {
|
|
735
|
-
const flushedRef =
|
|
736
|
-
const prevPathnameRef =
|
|
737
|
-
const idleTimerRef =
|
|
1269
|
+
const flushedRef = useRef3(/* @__PURE__ */ new Set());
|
|
1270
|
+
const prevPathnameRef = useRef3(pathname);
|
|
1271
|
+
const idleTimerRef = useRef3(null);
|
|
738
1272
|
const flush = useCallback3(
|
|
739
1273
|
(path, items) => {
|
|
740
1274
|
if (!onFlush || flushedRef.current.has(path)) return;
|
|
@@ -745,7 +1279,7 @@ function useFeedtackFlush({
|
|
|
745
1279
|
},
|
|
746
1280
|
[onFlush]
|
|
747
1281
|
);
|
|
748
|
-
|
|
1282
|
+
useEffect4(() => {
|
|
749
1283
|
if (disabled || !onFlush) return;
|
|
750
1284
|
const prev = prevPathnameRef.current;
|
|
751
1285
|
prevPathnameRef.current = pathname;
|
|
@@ -753,7 +1287,7 @@ function useFeedtackFlush({
|
|
|
753
1287
|
flush(prev, feedbackItems);
|
|
754
1288
|
}
|
|
755
1289
|
}, [pathname, feedbackItems, flush, onFlush, disabled]);
|
|
756
|
-
|
|
1290
|
+
useEffect4(() => {
|
|
757
1291
|
if (disabled || !onFlush || flushIdleMs <= 0) return;
|
|
758
1292
|
const resetTimer = () => {
|
|
759
1293
|
if (idleTimerRef.current) clearTimeout(idleTimerRef.current);
|
|
@@ -770,7 +1304,7 @@ function useFeedtackFlush({
|
|
|
770
1304
|
for (const e of events) window.removeEventListener(e, resetTimer);
|
|
771
1305
|
};
|
|
772
1306
|
}, [pathname, feedbackItems, flush, onFlush, flushIdleMs, disabled]);
|
|
773
|
-
|
|
1307
|
+
useEffect4(() => {
|
|
774
1308
|
if (disabled || !onFlush) return;
|
|
775
1309
|
const handleUnload = () => flush(pathname, feedbackItems);
|
|
776
1310
|
window.addEventListener("beforeunload", handleUnload);
|
|
@@ -783,12 +1317,13 @@ function useFeedtackFlush({
|
|
|
783
1317
|
}
|
|
784
1318
|
|
|
785
1319
|
// src/react/usePinMode.ts
|
|
786
|
-
import { useCallback as useCallback4, useEffect as
|
|
1320
|
+
import { useCallback as useCallback4, useEffect as useEffect5, useState as useState2 } from "react";
|
|
787
1321
|
function usePinMode({
|
|
788
1322
|
hotkey,
|
|
789
1323
|
onDeactivate,
|
|
790
1324
|
disabled,
|
|
791
|
-
isModalOpen
|
|
1325
|
+
isModalOpen,
|
|
1326
|
+
onHotkey
|
|
792
1327
|
}) {
|
|
793
1328
|
const [isActive, setIsActive] = useState2(false);
|
|
794
1329
|
const [pendingPins, setPendingPins] = useState2([]);
|
|
@@ -801,7 +1336,7 @@ function usePinMode({
|
|
|
801
1336
|
setShowForm(false);
|
|
802
1337
|
onDeactivate?.();
|
|
803
1338
|
}, [onDeactivate]);
|
|
804
|
-
|
|
1339
|
+
useEffect5(() => {
|
|
805
1340
|
if (isActive) {
|
|
806
1341
|
document.documentElement.classList.add("feedtack-crosshair");
|
|
807
1342
|
} else {
|
|
@@ -809,11 +1344,15 @@ function usePinMode({
|
|
|
809
1344
|
}
|
|
810
1345
|
return () => document.documentElement.classList.remove("feedtack-crosshair");
|
|
811
1346
|
}, [isActive]);
|
|
812
|
-
|
|
1347
|
+
useEffect5(() => {
|
|
813
1348
|
if (disabled) return;
|
|
814
1349
|
const handler = (e) => {
|
|
815
1350
|
if (e.key === hotkey.toUpperCase() && e.shiftKey) {
|
|
816
|
-
|
|
1351
|
+
if (onHotkey) {
|
|
1352
|
+
onHotkey();
|
|
1353
|
+
} else {
|
|
1354
|
+
setIsActive((prev) => !prev);
|
|
1355
|
+
}
|
|
817
1356
|
}
|
|
818
1357
|
if (e.key === "Escape") {
|
|
819
1358
|
deactivate();
|
|
@@ -829,7 +1368,7 @@ function usePinMode({
|
|
|
829
1368
|
};
|
|
830
1369
|
window.addEventListener("keydown", handler);
|
|
831
1370
|
return () => window.removeEventListener("keydown", handler);
|
|
832
|
-
}, [hotkey, deactivate, isActive, disabled, isModalOpen, showForm]);
|
|
1371
|
+
}, [hotkey, deactivate, isActive, disabled, isModalOpen, showForm, onHotkey]);
|
|
833
1372
|
const placePin = useCallback4(
|
|
834
1373
|
(coords, target) => {
|
|
835
1374
|
if (target.closest("#feedtack-root, .feedtack-form, .feedtack-color-picker"))
|
|
@@ -867,7 +1406,7 @@ function usePinMode({
|
|
|
867
1406
|
},
|
|
868
1407
|
[isActive, placePin]
|
|
869
1408
|
);
|
|
870
|
-
|
|
1409
|
+
useEffect5(() => {
|
|
871
1410
|
if (disabled) return;
|
|
872
1411
|
document.addEventListener("click", handlePageClick, true);
|
|
873
1412
|
document.addEventListener("touchend", handleTouchEnd, true);
|
|
@@ -903,7 +1442,7 @@ function useFeedtackState({
|
|
|
903
1442
|
const [pathname, setPathname] = useState3(
|
|
904
1443
|
() => typeof window === "undefined" ? "/" : window.location.pathname
|
|
905
1444
|
);
|
|
906
|
-
|
|
1445
|
+
useEffect6(() => {
|
|
907
1446
|
const update = () => setPathname(window.location.pathname);
|
|
908
1447
|
const origPush = history.pushState.bind(history);
|
|
909
1448
|
const origReplace = history.replaceState.bind(history);
|
|
@@ -930,6 +1469,12 @@ function useFeedtackState({
|
|
|
930
1469
|
const [loading, setLoading] = useState3(true);
|
|
931
1470
|
const [openThreadId, setOpenThreadId] = useState3(null);
|
|
932
1471
|
const [replyBody, setReplyBody] = useState3("");
|
|
1472
|
+
const [isModalOpen, setIsModalOpen] = useState3(false);
|
|
1473
|
+
const [composeScope, setComposeScope] = useState3("site");
|
|
1474
|
+
const [siteFeedback, setSiteFeedback] = useState3([]);
|
|
1475
|
+
const [pageFeedback, setPageFeedback] = useState3([]);
|
|
1476
|
+
const openModal = useCallback5(() => setIsModalOpen(true), []);
|
|
1477
|
+
const closeModal = useCallback5(() => setIsModalOpen(false), []);
|
|
933
1478
|
const resetForm = useCallback5(() => {
|
|
934
1479
|
setComment("");
|
|
935
1480
|
setSentiment(null);
|
|
@@ -938,7 +1483,8 @@ function useFeedtackState({
|
|
|
938
1483
|
const pinMode = usePinMode({
|
|
939
1484
|
hotkey,
|
|
940
1485
|
disabled,
|
|
941
|
-
isModalOpen: openThreadId !== null,
|
|
1486
|
+
isModalOpen: openThreadId !== null || isModalOpen,
|
|
1487
|
+
onHotkey: openModal,
|
|
942
1488
|
onDeactivate: () => {
|
|
943
1489
|
resetForm();
|
|
944
1490
|
setOpenThreadId(null);
|
|
@@ -951,12 +1497,29 @@ function useFeedtackState({
|
|
|
951
1497
|
flushIdleMs,
|
|
952
1498
|
disabled
|
|
953
1499
|
});
|
|
954
|
-
|
|
1500
|
+
useEffect6(() => {
|
|
955
1501
|
setLoading(true);
|
|
956
|
-
adapter.loadFeedback({ pathname }).then(
|
|
1502
|
+
adapter.loadFeedback({ pathname }).then((items) => {
|
|
1503
|
+
const elementItems = [];
|
|
1504
|
+
const siteItems = [];
|
|
1505
|
+
const pageItems = [];
|
|
1506
|
+
for (const item of items) {
|
|
1507
|
+
if (item.payload.scope === "site") siteItems.push(item);
|
|
1508
|
+
else if (item.payload.scope === "page") pageItems.push(item);
|
|
1509
|
+
else elementItems.push(item);
|
|
1510
|
+
}
|
|
1511
|
+
setFeedbackItems(elementItems);
|
|
1512
|
+
setSiteFeedback(siteItems);
|
|
1513
|
+
setPageFeedback(pageItems);
|
|
1514
|
+
}).catch((err) => onError?.(err)).finally(() => setLoading(false));
|
|
957
1515
|
}, [adapter, onError, pathname]);
|
|
1516
|
+
const getCurrentScope = useCallback5(() => {
|
|
1517
|
+
if (pinMode.isActive || pinMode.pendingPins.length > 0) return "element";
|
|
1518
|
+
return composeScope;
|
|
1519
|
+
}, [pinMode.isActive, pinMode.pendingPins.length, composeScope]);
|
|
958
1520
|
const commentRef = () => comment;
|
|
959
1521
|
const sentimentRef = () => sentiment;
|
|
1522
|
+
const scopeRef = () => getCurrentScope();
|
|
960
1523
|
const pinsRef = () => pinMode.pendingPins;
|
|
961
1524
|
const replyRef = () => replyBody;
|
|
962
1525
|
const pathRef = () => pathname;
|
|
@@ -966,6 +1529,7 @@ function useFeedtackState({
|
|
|
966
1529
|
onError,
|
|
967
1530
|
getComment: commentRef,
|
|
968
1531
|
getSentiment: sentimentRef,
|
|
1532
|
+
getScope: scopeRef,
|
|
969
1533
|
getPendingPins: pinsRef,
|
|
970
1534
|
getReplyBody: replyRef,
|
|
971
1535
|
getPathname: pathRef,
|
|
@@ -979,6 +1543,27 @@ function useFeedtackState({
|
|
|
979
1543
|
shouldRescope: rescopeRoles ? (role) => rescopeRoles.includes(role) : void 0,
|
|
980
1544
|
hasFlush: !!onFlush
|
|
981
1545
|
});
|
|
1546
|
+
const handleModalSubmit = useCallback5(async () => {
|
|
1547
|
+
if (!comment.trim()) {
|
|
1548
|
+
setCommentError(true);
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
await actions.handleSubmit();
|
|
1552
|
+
const scope = composeScope;
|
|
1553
|
+
setFeedbackItems((prev) => {
|
|
1554
|
+
const newItem = prev[prev.length - 1];
|
|
1555
|
+
if (newItem && newItem.payload.scope === scope) {
|
|
1556
|
+
if (scope === "site") {
|
|
1557
|
+
setSiteFeedback((s) => [...s, newItem]);
|
|
1558
|
+
} else {
|
|
1559
|
+
setPageFeedback((p) => [...p, newItem]);
|
|
1560
|
+
}
|
|
1561
|
+
return prev.slice(0, -1);
|
|
1562
|
+
}
|
|
1563
|
+
return prev;
|
|
1564
|
+
});
|
|
1565
|
+
resetForm();
|
|
1566
|
+
}, [actions, composeScope, resetForm, comment]);
|
|
982
1567
|
const isArchivedForUser = (item) => item.archives.some((a) => a.archivedBy.id === currentUser.id);
|
|
983
1568
|
const hasUnread = (item) => item.replies.length > 0;
|
|
984
1569
|
const hasValidPins = (item) => Array.isArray(item.payload?.pins) && item.payload.pins.length > 0;
|
|
@@ -996,11 +1581,20 @@ function useFeedtackState({
|
|
|
996
1581
|
submitting,
|
|
997
1582
|
pathname,
|
|
998
1583
|
feedbackItems,
|
|
1584
|
+
siteFeedback,
|
|
1585
|
+
pageFeedback,
|
|
999
1586
|
loading,
|
|
1000
1587
|
openThreadId,
|
|
1001
1588
|
setOpenThreadId,
|
|
1002
1589
|
replyBody,
|
|
1003
1590
|
setReplyBody,
|
|
1591
|
+
// Modal state
|
|
1592
|
+
isModalOpen,
|
|
1593
|
+
openModal,
|
|
1594
|
+
closeModal,
|
|
1595
|
+
composeScope,
|
|
1596
|
+
setComposeScope,
|
|
1597
|
+
handleModalSubmit,
|
|
1004
1598
|
...actions,
|
|
1005
1599
|
isArchivedForUser,
|
|
1006
1600
|
hasUnread,
|
|
@@ -1009,7 +1603,7 @@ function useFeedtackState({
|
|
|
1009
1603
|
}
|
|
1010
1604
|
|
|
1011
1605
|
// src/react/FeedtackProvider.tsx
|
|
1012
|
-
import { jsx as
|
|
1606
|
+
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1013
1607
|
function FeedtackProvider({
|
|
1014
1608
|
children,
|
|
1015
1609
|
adapter,
|
|
@@ -1042,7 +1636,11 @@ function FeedtackProvider({
|
|
|
1042
1636
|
const formPos = firstPin ? getAnchoredPosition(firstPin.x, firstPin.y) : {};
|
|
1043
1637
|
const showButton = !adminOnly || currentUser.role === "admin";
|
|
1044
1638
|
const openItem = state.openThreadId ? state.feedbackItems.find((i) => i.payload.id === state.openThreadId) : null;
|
|
1045
|
-
|
|
1639
|
+
const handlePlacePin = () => {
|
|
1640
|
+
state.closeModal();
|
|
1641
|
+
state.activatePinMode();
|
|
1642
|
+
};
|
|
1643
|
+
return /* @__PURE__ */ jsxs6(
|
|
1046
1644
|
FeedtackContext.Provider,
|
|
1047
1645
|
{
|
|
1048
1646
|
value: {
|
|
@@ -1054,31 +1652,31 @@ function FeedtackProvider({
|
|
|
1054
1652
|
selectedColor: state.selectedColor,
|
|
1055
1653
|
setSelectedColor: disabled ? () => {
|
|
1056
1654
|
} : state.setSelectedColor,
|
|
1057
|
-
pinPalette: PIN_PALETTE
|
|
1655
|
+
pinPalette: PIN_PALETTE,
|
|
1656
|
+
openModal: disabled ? () => {
|
|
1657
|
+
} : state.openModal,
|
|
1658
|
+
closeModal: disabled ? () => {
|
|
1659
|
+
} : state.closeModal,
|
|
1660
|
+
isModalOpen: disabled ? false : state.isModalOpen
|
|
1058
1661
|
},
|
|
1059
1662
|
children: [
|
|
1060
1663
|
children,
|
|
1061
|
-
!disabled && showButton && /* @__PURE__ */
|
|
1664
|
+
!disabled && showButton && /* @__PURE__ */ jsx6(
|
|
1062
1665
|
"button",
|
|
1063
1666
|
{
|
|
1064
1667
|
type: "button",
|
|
1065
1668
|
className: cx(
|
|
1066
1669
|
"feedtack-btn",
|
|
1067
|
-
state.isPinModeActive && "active",
|
|
1670
|
+
(state.isPinModeActive || state.isModalOpen) && "active",
|
|
1068
1671
|
classes.button
|
|
1069
1672
|
),
|
|
1070
|
-
onClick: () => state.
|
|
1071
|
-
title: "
|
|
1072
|
-
"aria-label": "
|
|
1073
|
-
"
|
|
1074
|
-
children: [
|
|
1075
|
-
"Drop Pin [Shift+",
|
|
1076
|
-
hotkey.toUpperCase(),
|
|
1077
|
-
"]"
|
|
1078
|
-
]
|
|
1673
|
+
onClick: () => state.openModal(),
|
|
1674
|
+
title: "Open feedback",
|
|
1675
|
+
"aria-label": "Open feedback",
|
|
1676
|
+
children: "Feedback"
|
|
1079
1677
|
}
|
|
1080
1678
|
),
|
|
1081
|
-
state.isPinModeActive && /* @__PURE__ */
|
|
1679
|
+
state.isPinModeActive && /* @__PURE__ */ jsx6("div", { className: cx("feedtack-color-picker", classes.colorPicker), children: PIN_PALETTE.map((color) => /* @__PURE__ */ jsx6(
|
|
1082
1680
|
"button",
|
|
1083
1681
|
{
|
|
1084
1682
|
type: "button",
|
|
@@ -1092,7 +1690,7 @@ function FeedtackProvider({
|
|
|
1092
1690
|
},
|
|
1093
1691
|
color
|
|
1094
1692
|
)) }),
|
|
1095
|
-
state.pendingPins.map((pin) => /* @__PURE__ */
|
|
1693
|
+
state.pendingPins.map((pin) => /* @__PURE__ */ jsx6(
|
|
1096
1694
|
"div",
|
|
1097
1695
|
{
|
|
1098
1696
|
className: cx("feedtack-pin-marker", classes.pinMarker),
|
|
@@ -1105,7 +1703,7 @@ function FeedtackProvider({
|
|
|
1105
1703
|
},
|
|
1106
1704
|
`${pin.x}-${pin.y}-${pin.color}`
|
|
1107
1705
|
)),
|
|
1108
|
-
state.showForm && /* @__PURE__ */
|
|
1706
|
+
state.showForm && /* @__PURE__ */ jsx6(
|
|
1109
1707
|
CommentForm,
|
|
1110
1708
|
{
|
|
1111
1709
|
comment: state.comment,
|
|
@@ -1124,45 +1722,22 @@ function FeedtackProvider({
|
|
|
1124
1722
|
onCancel: state.deactivatePinMode
|
|
1125
1723
|
}
|
|
1126
1724
|
),
|
|
1127
|
-
!state.loading &&
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
position: "absolute",
|
|
1144
|
-
cursor: "pointer"
|
|
1145
|
-
},
|
|
1146
|
-
onClick: () => state.setOpenThreadId(
|
|
1147
|
-
state.openThreadId === item.payload.id ? null : item.payload.id
|
|
1148
|
-
),
|
|
1149
|
-
children: [
|
|
1150
|
-
renderPinIcon ? /* @__PURE__ */ jsx3("span", { className: "feedtack-pin-icon", children: renderPinIcon(item) }) : item.resolutions.length > 0 && /* @__PURE__ */ jsx3(
|
|
1151
|
-
"span",
|
|
1152
|
-
{
|
|
1153
|
-
className: "feedtack-pin-icon",
|
|
1154
|
-
role: "img",
|
|
1155
|
-
"aria-label": "Resolved",
|
|
1156
|
-
children: "\u2713"
|
|
1157
|
-
}
|
|
1158
|
-
),
|
|
1159
|
-
state.hasUnread(item) && /* @__PURE__ */ jsx3("div", { className: "feedtack-pin-badge" })
|
|
1160
|
-
]
|
|
1161
|
-
},
|
|
1162
|
-
item.payload.id
|
|
1163
|
-
);
|
|
1164
|
-
}),
|
|
1165
|
-
openItem && /* @__PURE__ */ jsx3(
|
|
1725
|
+
!state.loading && /* @__PURE__ */ jsx6(
|
|
1726
|
+
PinOverlay,
|
|
1727
|
+
{
|
|
1728
|
+
feedbackItems: state.feedbackItems,
|
|
1729
|
+
pathname: state.pathname,
|
|
1730
|
+
isArchivedForUser: state.isArchivedForUser,
|
|
1731
|
+
hasValidPins: state.hasValidPins,
|
|
1732
|
+
hasUnread: state.hasUnread,
|
|
1733
|
+
openThreadId: state.openThreadId,
|
|
1734
|
+
setOpenThreadId: state.setOpenThreadId,
|
|
1735
|
+
getPosition,
|
|
1736
|
+
renderPinIcon,
|
|
1737
|
+
pinMarkerClass: classes.pinMarker
|
|
1738
|
+
}
|
|
1739
|
+
),
|
|
1740
|
+
openItem && /* @__PURE__ */ jsx6(
|
|
1166
1741
|
ThreadPanel,
|
|
1167
1742
|
{
|
|
1168
1743
|
item: openItem,
|
|
@@ -1179,7 +1754,36 @@ function FeedtackProvider({
|
|
|
1179
1754
|
className: classes.thread
|
|
1180
1755
|
}
|
|
1181
1756
|
),
|
|
1182
|
-
|
|
1757
|
+
!disabled && /* @__PURE__ */ jsx6(
|
|
1758
|
+
FeedbackModal,
|
|
1759
|
+
{
|
|
1760
|
+
isOpen: state.isModalOpen,
|
|
1761
|
+
onClose: state.closeModal,
|
|
1762
|
+
activeTab: state.composeScope,
|
|
1763
|
+
onTabChange: state.setComposeScope,
|
|
1764
|
+
siteFeedback: state.siteFeedback,
|
|
1765
|
+
pageFeedback: state.pageFeedback,
|
|
1766
|
+
comment: state.comment,
|
|
1767
|
+
onCommentChange: (v) => {
|
|
1768
|
+
state.setComment(v);
|
|
1769
|
+
state.setCommentError(false);
|
|
1770
|
+
},
|
|
1771
|
+
commentError: state.commentError,
|
|
1772
|
+
sentiment: state.sentiment,
|
|
1773
|
+
onSentimentChange: state.setSentiment,
|
|
1774
|
+
submitting: state.submitting,
|
|
1775
|
+
onSubmit: state.handleModalSubmit,
|
|
1776
|
+
onPlacePin: handlePlacePin,
|
|
1777
|
+
replyBody: state.replyBody,
|
|
1778
|
+
onReplyBodyChange: state.setReplyBody,
|
|
1779
|
+
onReply: (id) => state.handleReply(id),
|
|
1780
|
+
onResolve: (id) => state.handleResolve(id),
|
|
1781
|
+
onArchive: (id) => state.handleArchive(id),
|
|
1782
|
+
openThreadId: state.openThreadId,
|
|
1783
|
+
onOpenThread: state.setOpenThreadId
|
|
1784
|
+
}
|
|
1785
|
+
),
|
|
1786
|
+
state.loading && /* @__PURE__ */ jsx6("div", { className: "feedtack-loading", children: "Loading feedback\u2026" })
|
|
1183
1787
|
]
|
|
1184
1788
|
}
|
|
1185
1789
|
);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
declare const SCHEMA_VERSION = "
|
|
1
|
+
declare const SCHEMA_VERSION = "2.0.0";
|
|
2
|
+
type FeedtackScope = 'site' | 'page' | 'element';
|
|
2
3
|
interface FeedtackUser {
|
|
3
4
|
/** Unique identifier — used for attribution across pins, replies, resolutions, archives */
|
|
4
5
|
id: string;
|
|
@@ -81,17 +82,19 @@ interface FeedtackDeviceMeta {
|
|
|
81
82
|
platform: string;
|
|
82
83
|
touchEnabled: boolean;
|
|
83
84
|
}
|
|
84
|
-
type FeedtackSentiment = '
|
|
85
|
+
type FeedtackSentiment = 'good' | 'bad' | null;
|
|
85
86
|
interface FeedtackPayload {
|
|
86
87
|
schemaVersion: string;
|
|
87
88
|
/** Unique feedback ID, e.g. ft_01j... */
|
|
88
89
|
id: string;
|
|
89
90
|
/** ISO 8601 UTC */
|
|
90
91
|
timestamp: string;
|
|
92
|
+
/** Feedback scope: site-wide, page-level, or element-specific */
|
|
93
|
+
scope: FeedtackScope;
|
|
91
94
|
submittedBy: FeedtackUser;
|
|
92
95
|
comment: string;
|
|
93
96
|
sentiment: FeedtackSentiment;
|
|
94
|
-
/**
|
|
97
|
+
/** Pins placed on the page. Empty for site/page scope. */
|
|
95
98
|
pins: FeedtackPin[];
|
|
96
99
|
page: FeedtackPageMeta;
|
|
97
100
|
viewport: FeedtackViewportMeta;
|
|
@@ -127,6 +130,7 @@ interface FeedtackFilter {
|
|
|
127
130
|
url?: string;
|
|
128
131
|
pathname?: string;
|
|
129
132
|
userId?: string;
|
|
133
|
+
scope?: FeedtackScope;
|
|
130
134
|
}
|
|
131
135
|
|
|
132
136
|
/** Plugin contract — implement this interface to create a custom feedtack backend */
|
|
@@ -164,4 +168,4 @@ interface FeedtackTheme {
|
|
|
164
168
|
/** Maps FeedtackTheme fields to CSS custom properties on #feedtack-root */
|
|
165
169
|
declare function themeToCSS(theme: FeedtackTheme): Record<string, string>;
|
|
166
170
|
|
|
167
|
-
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
|
|
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 };
|