blocfeed 0.2.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,8 +1,56 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 0.4.0 — 2026-02-20
4
4
 
5
- - (none)
5
+ ### New features
6
+
7
+ - **Animated trigger styles** — new `config.ui.triggerStyle` option with 7 selectable styles:
8
+ - `"classic"` (default) — standard pill button, no framer-motion required
9
+ - `"dot"` — breathing dot with glow that springs into a pill on hover
10
+ - `"bubble"` — floating chat-bubble icon with tooltip on hover
11
+ - `"edge-tab"` — thin vertical tab flush to screen edge that slides out on hover
12
+ - `"pulse-ring"` — sonar-style pulsing rings around a dot that contracts to a pill on hover
13
+ - `"minimal"` — text-only "Feedback" with animated underline on hover
14
+ - `"icon-pop"` — wobbling message icon that pops and reveals text on hover
15
+ - All animated styles include smooth spring-based enter/exit transitions via framer-motion
16
+ - `framer-motion` added as an optional peer dependency (only required for non-classic styles)
17
+ - New `TriggerStyle` type exported from both `blocfeed` and `blocfeed/engine`
18
+
19
+ ### Architecture
20
+
21
+ - Trigger button extracted from `BlocFeedWidget` into modular components under `src/react/triggers/`
22
+ - Each trigger variant is a self-contained component with its own animations
23
+ - `ClassicTrigger` has zero framer-motion imports for consumers who don't need animations
24
+
25
+ ---
26
+
27
+ ## 0.3.0 — 2026-02-20
28
+
29
+ ### New features
30
+
31
+ - **User identity** — new `config.user` prop attaches `id`, `email`, and `name` to every submission. Values are included as a top-level `user` field in the payload and merged into `metadata` as `userId`, `userEmail`, `userName`.
32
+ - **Offline queue** — failed submissions (network errors) are automatically saved to `localStorage` and retried on the next page load or when the browser comes back online. Queue holds up to 50 items (FIFO eviction); screenshots are stripped to stay within storage limits. Headless utilities: `enqueue`, `dequeueAll`, `clearQueue`, `getQueueSize` exported from `blocfeed/engine`.
33
+ - **Widget positioning** — new `config.ui.position` option places the trigger button in any corner: `"bottom-right"` (default), `"bottom-left"`, `"top-right"`, `"top-left"`.
34
+ - **Theme customization** — new `config.ui.theme` option overrides visual appearance via CSS variables: `accentColor`, `panelBackground`, `panelForeground`, `fontFamily`.
35
+ - **Configurable retry & transport** — new `config.transport` option with `timeoutMs`, `maxAttempts`, and `backoffMs` (exponential backoff with jitter). Defaults unchanged: 12s timeout, 2 attempts, 500ms base backoff.
36
+ - **Keyboard accessibility** — full keyboard navigation with focus trapping, `Tab`/`Shift+Tab` cycling, `Escape` to cancel, `Ctrl/Cmd+Enter` to submit. All interactive elements have `aria-label` attributes; status messages use `aria-live="polite"`.
37
+ - **Client-side rate limiting** — minimum 2-second interval enforced between submissions to prevent accidental spam.
38
+ - **Alternative screenshot adapter** — `createModernScreenshotAdapter()` exported from `blocfeed/engine` for using `modern-screenshot` as a drop-in replacement with better cross-origin, `clip-path`, and `backdrop-filter` support.
39
+ - **Capture progress indicator** — pulsing spinner animation shown during screenshot capture and submission phases.
40
+
41
+ ### Improvements
42
+
43
+ - **Two-phase screenshot upload** — the text payload (metadata, selection, message) is sent first without base64 data. If the platform returns presigned upload URLs, screenshots are uploaded directly to object storage. Falls back to a secondary POST endpoint when presigned URLs are unavailable.
44
+ - **Full-page screenshot storage** — full-page screenshots are now processed and stored on the platform (previously captured but discarded server-side).
45
+ - **Production fiber optimization** — React fiber introspection for `componentName` now checks for `_debugOwner` before traversing. In production builds (where debug info is stripped), the expensive 80-node traversal is skipped entirely; only a 10-node parent chain is checked.
46
+ - **TypeScript strict exports** — all new types (`BlocFeedUser`, `TransportConfig`, `WidgetPosition`, `ThemeConfig`, `FeedbackApiResponse`, `ScreenshotIntent`) exported from both `blocfeed` and `blocfeed/engine`. Added `typesVersions` in `package.json` for older bundler compatibility.
47
+ - **Test coverage** — expanded from 6 tests (2 files) to 49 tests (7 files) covering the controller state machine, API client (retries, rate limiting, abort), offline queue, metadata collection, error normalization, and picker behavior.
48
+
49
+ ### Platform changes (blocfeed-frontend)
50
+
51
+ - `POST /api/feedback` now stores full-page screenshots, reads `payload.user` for dedicated identity columns, and returns `feedback_id` + presigned `upload_urls` in the response.
52
+ - New `POST /api/feedback/:id/screenshots` fallback endpoint for uploading screenshots as base64 when object storage is unavailable.
53
+ - New SQL migration `scripts/026_fullpage_screenshots.sql` adds `screenshot_fullpage_*` and `meta_user_*` columns.
6
54
 
7
55
  ## 0.2.2 — 2026-02-17
8
56
 
package/README.md CHANGED
@@ -2,11 +2,15 @@
2
2
 
3
3
  Drop-in in-app feedback widget for **Next.js** and **React**:
4
4
 
5
- - Safe feedback mode with **DOM element picking** (blocks host app clicks while active)
5
+ - Safe "feedback mode" with **DOM element picking** (blocks host app clicks while active)
6
6
  - Optional **element** + **full page** screenshots (capture code is lazy-loaded)
7
7
  - Typed, JSON-serializable `FeedbackPayload` with contextual metadata
8
8
  - Submits directly to the BlocFeed platform via `blocfeed_id`
9
9
  - Optional `selection.componentName` + `selection.testId` for faster triage
10
+ - **First-class user identity** — attach `user.id`, `email`, `name` to every submission
11
+ - **Offline queue** — failed submissions are stored and retried automatically
12
+ - **Customizable position, theme, and retry behavior**
13
+ - **Accessible** — keyboard navigation, focus trapping, ARIA attributes
10
14
 
11
15
  Docs live in `docs/` (start at `docs/index.md`). Architecture pointers are in `ARCHITECTURE.md`.
12
16
 
@@ -83,6 +87,171 @@ export function App() {
83
87
  }
84
88
  ```
85
89
 
90
+ ## Configuration
91
+
92
+ All configuration is passed via the `config` prop on `<BlocFeedWidget>` or `<BlocFeedProvider>`:
93
+
94
+ ```tsx
95
+ <BlocFeedWidget
96
+ blocfeed_id="bf_..."
97
+ config={{
98
+ // User identity (attached to every submission)
99
+ user: {
100
+ id: "user_123",
101
+ email: "jane@example.com",
102
+ name: "Jane Doe",
103
+ },
104
+
105
+ // Widget position, theme & trigger style
106
+ ui: {
107
+ position: "bottom-right", // "bottom-left" | "top-right" | "top-left"
108
+ triggerStyle: "dot", // "classic" | "bubble" | "edge-tab" | "pulse-ring" | "minimal" | "icon-pop"
109
+ zIndex: 99999,
110
+ theme: {
111
+ accentColor: "#12D393",
112
+ panelBackground: "rgba(0, 0, 0, 0.95)",
113
+ panelForeground: "#ffffff",
114
+ fontFamily: "Inter, sans-serif",
115
+ },
116
+ },
117
+
118
+ // Screenshot defaults
119
+ capture: {
120
+ element: true,
121
+ fullPage: false,
122
+ mime: "image/png", // or "image/jpeg"
123
+ quality: 0.92, // JPEG quality (0–1)
124
+ timeoutMs: 12000,
125
+ maxDimension: 2048,
126
+ pixelRatio: 2,
127
+ },
128
+
129
+ // Retry & transport
130
+ transport: {
131
+ timeoutMs: 12000, // per-request timeout
132
+ maxAttempts: 2, // retry count
133
+ backoffMs: 500, // base backoff delay
134
+ },
135
+
136
+ // Metadata enrichment
137
+ metadata: {
138
+ enabled: true,
139
+ enrich: async (context) => ({
140
+ orgId: "org_abc",
141
+ plan: "pro",
142
+ }),
143
+ },
144
+
145
+ // Picker rules
146
+ picker: {
147
+ ignoreSelectors: ["[data-private]"],
148
+ isSelectable: (el) => el.tagName !== "HTML",
149
+ },
150
+ }}
151
+ />
152
+ ```
153
+
154
+ ## User Identity
155
+
156
+ Attach user information to every feedback submission:
157
+
158
+ ```tsx
159
+ <BlocFeedWidget
160
+ blocfeed_id="bf_..."
161
+ config={{
162
+ user: {
163
+ id: currentUser.id,
164
+ email: currentUser.email,
165
+ name: currentUser.name,
166
+ },
167
+ }}
168
+ />
169
+ ```
170
+
171
+ The `user` object is included in the payload as a top-level field and its values are also merged into `metadata` as `userId`, `userEmail`, `userName`.
172
+
173
+ ## Widget Position
174
+
175
+ Place the trigger button in any corner:
176
+
177
+ ```tsx
178
+ config={{
179
+ ui: { position: "bottom-left" } // default: "bottom-right"
180
+ }}
181
+ ```
182
+
183
+ Options: `"bottom-right"` | `"bottom-left"` | `"top-right"` | `"top-left"`
184
+
185
+ ## Trigger Styles
186
+
187
+ Customize the trigger button's appearance and animation:
188
+
189
+ ```tsx
190
+ config={{
191
+ ui: { triggerStyle: "dot" }
192
+ }}
193
+ ```
194
+
195
+ | Style | Description | Requires framer-motion |
196
+ |-------|-------------|----------------------|
197
+ | `"classic"` | Default pill button with colored dot (no animation) | No |
198
+ | `"dot"` | Breathing dot that expands to pill on hover | Yes |
199
+ | `"bubble"` | Floating chat-bubble icon with tooltip on hover | Yes |
200
+ | `"edge-tab"` | Thin tab anchored to screen edge, slides out on hover | Yes |
201
+ | `"pulse-ring"` | Sonar-style pulsing rings around a dot, expands on hover | Yes |
202
+ | `"minimal"` | Text-only "Feedback" with animated underline on hover | Yes |
203
+ | `"icon-pop"` | Wobbling icon that pops and reveals text on hover | Yes |
204
+
205
+ All animated styles include smooth enter/exit transitions powered by framer-motion's spring physics.
206
+
207
+ ### Installing framer-motion
208
+
209
+ All styles except `"classic"` require `framer-motion` as a peer dependency:
210
+
211
+ ```bash
212
+ npm install framer-motion
213
+ ```
214
+
215
+ ## Theme Customization
216
+
217
+ Override the widget's visual appearance via CSS variables:
218
+
219
+ ```tsx
220
+ config={{
221
+ ui: {
222
+ theme: {
223
+ accentColor: "#12D393", // buttons, highlights, focus rings
224
+ panelBackground: "rgba(0,0,0,0.95)", // panel & trigger background
225
+ panelForeground: "#ffffff", // text color
226
+ fontFamily: "Inter, sans-serif", // font stack
227
+ },
228
+ },
229
+ }}
230
+ ```
231
+
232
+ ## Retry & Transport
233
+
234
+ Configure retry behavior for unreliable networks:
235
+
236
+ ```tsx
237
+ config={{
238
+ transport: {
239
+ timeoutMs: 15000, // 15s timeout per attempt
240
+ maxAttempts: 3, // retry up to 3 times
241
+ backoffMs: 1000, // 1s base backoff (exponential with jitter)
242
+ },
243
+ }}
244
+ ```
245
+
246
+ ## Offline Queue
247
+
248
+ When a submission fails due to a network error, the payload is automatically saved to `localStorage` and retried:
249
+
250
+ - On the next page load (1s after controller initialization)
251
+ - When the browser comes back online (`online` event)
252
+
253
+ Screenshots are stripped from queued payloads to stay within `localStorage` limits. The queue holds up to 50 items (FIFO eviction).
254
+
86
255
  ## Picking rules (ignore / filter)
87
256
 
88
257
  ### Ignore a subtree (recommended)
@@ -116,7 +285,7 @@ export function CheckoutButton() {
116
285
  }
117
286
  ```
118
287
 
119
- BlocFeed will also attempt a **best-effort** component name in React/Next **dev builds** (no setup), but it may be missing/minified in production.
288
+ BlocFeed will also attempt a **best-effort** component name in React/Next **dev builds** (no setup), but it may be missing/minified in production. In production, the fiber traversal is limited to 10 nodes (vs 80 in dev) to avoid wasting cycles on minified names.
120
289
 
121
290
  ### `selection.testId`
122
291
 
@@ -124,8 +293,44 @@ BlocFeed extracts a best-effort `testId` from common attributes like `data-testi
124
293
 
125
294
  ## Screenshots
126
295
 
127
- - Screenshot output is stored as Data URLs on `payload.screenshots`.
128
- - Convert to `Blob` if needed:
296
+ Screenshots are uploaded efficiently using a two-phase flow:
297
+
298
+ 1. The text payload (metadata, selection, message) is sent first without screenshot data
299
+ 2. If the platform returns presigned upload URLs (Wasabi/S3), screenshots are uploaded directly to object storage
300
+ 3. If presigned URLs are unavailable, screenshots fall back to a secondary POST endpoint
301
+
302
+ Both **element** and **full-page** screenshots are supported and stored on the platform.
303
+
304
+ ### Known limitations
305
+
306
+ The default screenshot engine (`html-to-image`) has known issues with:
307
+ - Cross-origin images without permissive CORS headers
308
+ - CSS `clip-path` and `backdrop-filter`
309
+ - Some web fonts
310
+
311
+ ### Alternative screenshot adapter
312
+
313
+ For better compatibility, you can use `modern-screenshot` as a drop-in replacement:
314
+
315
+ ```bash
316
+ npm install modern-screenshot
317
+ ```
318
+
319
+ ```tsx
320
+ import { createModernScreenshotAdapter } from "blocfeed/engine";
321
+ import * as modernScreenshot from "modern-screenshot";
322
+
323
+ <BlocFeedWidget
324
+ blocfeed_id="bf_..."
325
+ config={{
326
+ capture: {
327
+ adapter: createModernScreenshotAdapter(modernScreenshot),
328
+ },
329
+ }}
330
+ />
331
+ ```
332
+
333
+ ### Convert to Blob
129
334
 
130
335
  ```ts
131
336
  import { dataUrlToBlob } from "blocfeed/engine";
@@ -133,11 +338,25 @@ import { dataUrlToBlob } from "blocfeed/engine";
133
338
  const blob = dataUrlToBlob(payload.screenshots?.element?.dataUrl ?? "");
134
339
  ```
135
340
 
136
- Note: cross-origin images without permissive CORS headers may be missing from captures.
341
+ ## Keyboard Accessibility
342
+
343
+ The widget supports full keyboard navigation:
344
+
345
+ - **Tab** cycles through interactive elements in the feedback panel
346
+ - **Shift+Tab** cycles backward
347
+ - **Escape** cancels picking or closes the panel
348
+ - **Ctrl/Cmd+Enter** submits feedback from the textarea
349
+ - Focus is trapped within the panel when open
350
+ - All interactive elements have `aria-label` attributes
351
+ - Status messages use `aria-live="polite"` for screen reader announcements
352
+
353
+ ## Client-Side Rate Limiting
354
+
355
+ A minimum 2-second interval is enforced between submissions to prevent accidental spam. Rapid submissions return a descriptive error.
137
356
 
138
357
  ## Custom UI
139
358
 
140
- If you dont want the default widget UI:
359
+ If you don't want the default widget UI:
141
360
 
142
361
  ```tsx
143
362
  import { BlocFeedProvider, useBlocFeed } from "blocfeed";
@@ -165,6 +384,32 @@ import { createBlocFeedController } from "blocfeed/engine";
165
384
  const controller = createBlocFeedController({ blocfeed_id: "bf_your_project_blocfeed_id" });
166
385
  ```
167
386
 
387
+ ### Offline queue utilities (headless)
388
+
389
+ ```ts
390
+ import { enqueue, dequeueAll, clearQueue, getQueueSize } from "blocfeed/engine";
391
+ ```
392
+
393
+ ## TypeScript
394
+
395
+ All types are exported from both entry points:
396
+
397
+ ```ts
398
+ import type {
399
+ BlocFeedConfig,
400
+ BlocFeedUser,
401
+ TransportConfig,
402
+ TriggerStyle,
403
+ WidgetPosition,
404
+ ThemeConfig,
405
+ FeedbackPayload,
406
+ FeedbackApiResponse,
407
+ BlocFeedState,
408
+ SessionPhase,
409
+ // ... and more
410
+ } from "blocfeed";
411
+ ```
412
+
168
413
  ## Local development
169
414
 
170
415
  ```bash
@@ -0,0 +1,2 @@
1
+ function h(){return typeof window<"u"&&typeof document<"u"}function X(t){let e=globalThis.CSS;return typeof e?.escape=="function"?e.escape(t):t.replace(/[^a-zA-Z0-9_-]/g,n=>{let r=n.codePointAt(0);return r===void 0?"":`\\${r.toString(16)} `})}function O(t){return {x:t.x,y:t.y,width:t.width,height:t.height}}function ye(t){return {x:t.x+window.scrollX,y:t.y+window.scrollY,width:t.width,height:t.height}}function Ee(t){return t.replace(/\s+/g," ").trim()}function Se(t,e=140){let n=t.textContent;if(!n)return;let r=Ee(n);if(r)return r.length<=e?r:`${r.slice(0,e-1)}\u2026`}function ke(t){let e=1;for(let n=t.previousElementSibling;n;n=n.previousElementSibling)n.tagName===t.tagName&&(e+=1);return e}var V=["data-testid","data-test-id","data-test","data-qa","data-cy"],G="data-blocfeed-component";function ve(t){let e=t.closest(`[${G}]`);if(!e)return;let r=e.getAttribute(G)?.trim();return r||void 0}function Pe(t){for(let e of V){let n=t.closest(`[${e}]`);if(!n)continue;let o=n.getAttribute(e)?.trim();if(o)return o}}function xe(t){try{let e=t,n=Object.getOwnPropertyNames(e);for(let r of n)if(r.startsWith("__reactFiber$")||r.startsWith("__reactInternalInstance$")){let o=e[r];if(o&&typeof o=="object")return o}}catch{}return null}function B(t){if(t&&typeof t!="string"){if(typeof t=="function"){let e=t;return typeof e.displayName=="string"&&e.displayName?e.displayName:typeof e.name=="string"&&e.name?e.name:void 0}if(typeof t=="object"){let e=t,n=e.displayName;if(typeof n=="string"&&n)return n;let r=e.render;if(typeof r=="function"){let i=r;if(typeof i.displayName=="string"&&i.displayName)return i.displayName;if(typeof i.name=="string"&&i.name)return i.name}let o=e.type;return B(o)}}}function Fe(t){let e=xe(t);if(!e)return;let n=e._debugOwner!==void 0;if(n){let i=e._debugOwner;for(let s=0;i&&s<50;s+=1){let l=B(i.type)??B(i.elementType);if(l)return l;i=i._debugOwner;}}let r=e,o=n?80:10;for(let i=0;r&&i<o;i+=1){let s=B(r.type)??B(r.elementType);if(s)return s;r=r.return;}}function Ae(t){let e=t.tagName.toLowerCase(),n=t.getAttribute("id");if(n)return `#${X(n)}`;for(let r of V){let o=t.getAttribute(r);if(o)return `${e}[${r}="${X(o)}"]`}return `${e}:nth-of-type(${ke(t)})`}function Te(t,e=10){let n=[],r=t;for(;r&&n.length<e;){let o=Ae(r);if(n.unshift(o),o.startsWith("#"))break;r=r.parentElement;}return n.join(" > ")}function Z(t,e){if(!e||e.length===0)return false;for(let n of e)if(t.closest(n))return true;return false}function H(t,e){if(!t||Z(t,e.ignoreSelectors))return null;let n=t;for(;n;){if(Z(n,e.ignoreSelectors))return null;if(e.isSelectable?.(n)??Re(n))return n;n=n.parentElement;}return null}function Re(t){let e=t.tagName;return !(e==="HTML"||e==="BODY")}function ee(t){let e=t.getBoundingClientRect(),n={selector:Te(t),tagName:t.tagName.toLowerCase(),rect:O(e),pageRect:ye(e)},r=t.getAttribute("id");r&&(n.id=r);let o=t.className;typeof o=="string"&&o.trim()&&(n.className=o);let i=Se(t);i&&(n.textSnippet=i);let s=ve(t)??Fe(t);s&&(n.componentName=s);let l=Pe(t);return l&&(n.testId=l),n}var q=null;async function te(){return q||(q=import('html-to-image')),q}async function Ce(t){return await new Promise((e,n)=>{let r=new Image;r.onload=()=>e({width:r.naturalWidth,height:r.naturalHeight}),r.onerror=()=>n(new Error("Failed to load generated screenshot")),r.src=t;})}async function ne(t,e){let{width:n,height:r}=await Ce(t);return {dataUrl:t,mime:e,width:n,height:r}}function R(t){if(t?.aborted)throw new Error("Aborted")}function re(){return {async captureElement(t,e){if(!h())throw new Error("captureElement can only run in the browser");R(e.signal);let n=await te();R(e.signal);let r=e.mime==="image/jpeg"?await n.toJpeg(t,{quality:e.quality??.92,pixelRatio:e.pixelRatio}):await n.toPng(t,{pixelRatio:e.pixelRatio});return R(e.signal),await ne(r,e.mime)},async captureFullPage(t){if(!h())throw new Error("captureFullPage can only run in the browser");R(t.signal);let e=document.documentElement,n=Math.max(e.scrollWidth,e.clientWidth),r=Math.max(e.scrollHeight,e.clientHeight),o=Math.min(1,t.maxDimension/Math.max(n,r)),i=Math.max(1,Math.round(n*o)),s=Math.max(1,Math.round(r*o)),l=await te();R(t.signal);let f=t.mime==="image/jpeg"?await l.toJpeg(e,{width:i,height:s,quality:t.quality??.92,pixelRatio:t.pixelRatio}):await l.toPng(e,{width:i,height:s,pixelRatio:t.pixelRatio});return R(t.signal),await ne(f,t.mime)}}}function Me(t){if(t instanceof Error)return t.message;if(typeof t=="string")return t;try{return JSON.stringify(t)}catch{return "Unknown error"}}function m(t,e,n){let r={kind:t,message:Me(e)};return n&&(r.detail=n),r}var Be=12e3,_e=2048,Ie=.92;function oe(){return Date.now()}function Le(t){return new Promise((e,n)=>{let r=()=>n(new Error("Aborted"));if(t.aborted){r();return}t.addEventListener("abort",r,{once:true});})}async function ie(t,e,n){let r=new Promise((i,s)=>{let l=setTimeout(()=>s(new Error("Timeout")),e);typeof l.unref=="function"&&l.unref();}),o=[t,r];return n&&o.push(Le(n)),await Promise.race(o)}function De(t){if(!h())return 1;let e=window.devicePixelRatio||1,n=t?.pixelRatio??Math.min(e,2);return Math.max(.1,n)}function Ne(t){return !!(t?.element||t?.fullPage)}function ae(t){let e={mime:t.mime,pixelRatio:t.pixelRatio,maxDimension:t.maxDimension};return t.includeQuality&&(e.quality=t.quality),t.signal&&(e.signal=t.signal),e}async function se(t){let{selectionElement:e,capture:n,signal:r}=t;if(!h()||!Ne(n))return;let o=oe(),i=[],s=n?.timeoutMs??Be,l=n?.maxDimension??_e,f=n?.mime??"image/png",p=n?.quality??Ie,w=n?.adapter??re(),y={},E=De(n);if(n?.element&&e)try{let c=e.getBoundingClientRect(),u=Math.min(1,l/Math.max(c.width,c.height)),v=Math.min(E,E*u),x=await ie(Promise.resolve(w.captureElement(e,{...ae({mime:f,quality:p,pixelRatio:v,maxDimension:l,includeQuality:f==="image/jpeg",...r?{signal:r}:{}})})),s,r);y.element=x;}catch(c){if(r?.aborted)throw c;i.push(m("capture_failed",c,{target:"element"}));}if(n?.fullPage)try{let c=await ie(Promise.resolve(w.captureFullPage(ae({mime:f,quality:p,pixelRatio:E,maxDimension:l,includeQuality:f==="image/jpeg",...r?{signal:r}:{}}))),s,r);y.fullPage=c;}catch(c){if(r?.aborted)throw c;i.push(m("capture_failed",c,{target:"fullPage"}));}let g=oe(),a={startedAt:o,finishedAt:g,durationMs:Math.max(0,g-o)};return i.length>0&&(a.errors=i),{...y,diagnostics:a}}function Ue(){try{return Intl.DateTimeFormat().resolvedOptions().timeZone}catch{return}}function Oe(){return h()?{url:window.location.href,title:document.title,referrer:document.referrer||void 0,userAgent:navigator.userAgent,language:navigator.language,platform:navigator.platform,viewport:{width:window.innerWidth,height:window.innerHeight},screen:{width:window.screen.width,height:window.screen.height},scroll:{x:window.scrollX,y:window.scrollY},devicePixelRatio:window.devicePixelRatio||1,timezone:Ue()}:{}}function He(t){if(!t)return {};let e={};return t.id&&(e.userId=t.id),t.email&&(e.userEmail=t.email),t.name&&(e.userName=t.name),e}async function le(t){let{config:e,context:n,user:r}=t;if(e?.enabled===false)return {};let o={...Oe(),...He(r)},i=e?.enrich;if(!i)return o;try{let s=await i(n);return {...o,...s}}catch(s){let l=m("unknown",s);return {...o,blocfeedMetadataError:l.message}}}var $="blocfeed-queue",qe=50;function j(){if(!h())return [];try{let t=localStorage.getItem($);if(!t)return [];let e=JSON.parse(t);return Array.isArray(e)?e:[]}catch{return []}}function Q(t){if(h())try{t.length===0?localStorage.removeItem($):localStorage.setItem($,JSON.stringify(t));}catch{}}function $e(t){let e={...t};if(e.screenshots){let n={...e.screenshots};n.element&&(n.element={...n.element,dataUrl:""}),n.fullPage&&(n.fullPage={...n.fullPage,dataUrl:""}),e.screenshots=n;}return e}function W(t){let e=j(),n=$e(t);for(n.metadata={...n.metadata,_queued:true},e.push({payload:n,timestamp:Date.now()});e.length>qe;)e.shift();Q(e);}function ce(){let t=j();return t.length===0?[]:(Q([]),t.map(e=>e.payload))}function mt(){Q([]);}function pt(){return j().length}function z(t){let e=null,n=null,r=(...o)=>{n=o,e===null&&(e=requestAnimationFrame(()=>{if(e=null,!n)return;let i=n;n=null,t(...i);}));};return r.cancel=()=>{e!==null&&cancelAnimationFrame(e),e=null,n=null;},r}function I(t){return t instanceof Element?!!t.closest("[data-blocfeed-ui]"):false}function L(t){t.stopPropagation(),t.stopImmediatePropagation?.();}function ue(t,e){if(!h())throw new Error("BlocFeed picker can only run in a browser environment.");let n=t.ignoreSelectors,r=t.isSelectable,o={};n&&n.length>0&&(o.ignoreSelectors=n),r&&(o.isSelectable=r);let i=null,s=null,l=(a,c=false)=>{if(!a){i=null,s=null,e.onHover(null);return}let u=O(a.getBoundingClientRect()),v=`${Math.round(u.x)}:${Math.round(u.y)}:${Math.round(u.width)}:${Math.round(u.height)}`;!c&&a===i&&v===s||(i=a,s=v,e.onHover({element:a,rect:u}));},f=z(a=>{if(I(a.target))return;let c=document.elementFromPoint(a.clientX,a.clientY),u=H(c,o);l(u);}),p=z(()=>{i&&l(i,true);}),w=a=>{I(a.target)||(L(a),a.pointerType==="mouse"&&a.preventDefault());},y=a=>{I(a.target)||(L(a),a.pointerType==="mouse"&&a.preventDefault());},E=a=>{if(I(a.target))return;L(a),a.preventDefault();let c=document.elementFromPoint(a.clientX,a.clientY),u=H(c,o);u&&e.onSelect({element:u,descriptor:ee(u)});},g=a=>{a.key==="Escape"&&(L(a),a.preventDefault(),e.onCancel());};return window.addEventListener("pointermove",f,{capture:true,passive:true}),window.addEventListener("pointerdown",w,{capture:true}),window.addEventListener("pointerup",y,{capture:true}),window.addEventListener("click",E,{capture:true}),window.addEventListener("keydown",g,{capture:true}),window.addEventListener("scroll",p,{capture:true,passive:true}),window.addEventListener("resize",p,{passive:true}),{stop(){window.removeEventListener("pointermove",f,{capture:true}),window.removeEventListener("pointerdown",w,{capture:true}),window.removeEventListener("pointerup",y,{capture:true}),window.removeEventListener("click",E,{capture:true}),window.removeEventListener("keydown",g,{capture:true}),window.removeEventListener("scroll",p,{capture:true}),window.removeEventListener("resize",p),f.cancel(),p.cancel(),e.onHover(null);}}}var je=12e3,Qe=2,We=500,ze=2e3,pe="https://blocfeed.com/api/feedback",de=0;function fe(t,e){return new Promise((n,r)=>{if(e?.aborted){r(new Error("Aborted"));return}let o=setTimeout(n,t),i=()=>{clearTimeout(o),r(new Error("Aborted"));};e?.addEventListener("abort",i,{once:true});})}function Je(t){return t>=500&&t<=599}function Ye(t){let[e,n]=t.split(",",2);if(!e||!n)throw new Error("Invalid data URL");let o=/data:(.*?);base64/.exec(e)?.[1]||"application/octet-stream",i=atob(n),s=new Uint8Array(i.length);for(let l=0;l<i.length;l+=1)s[l]=i.charCodeAt(l);return new Blob([s],{type:o})}function Ke(t){let e={},n={...t};if(n.screenshots){let r={},o={...n.screenshots};o.element&&(e.element=o.element.dataUrl,r.element={mime:o.element.mime,width:o.element.width,height:o.element.height},o.element={...o.element,dataUrl:""}),o.fullPage&&(e.fullPage=o.fullPage.dataUrl,r.fullPage={mime:o.fullPage.mime,width:o.fullPage.width,height:o.fullPage.height},o.fullPage={...o.fullPage,dataUrl:""}),n.screenshots=o,(r.element||r.fullPage)&&(n.screenshot_intent=r);}return {lean:n,extracted:e}}async function me(t,e,n){let r=Ye(e);await fetch(t,{method:"PUT",body:r,headers:{"content-type":r.type},...n?{signal:n}:{}});}async function Xe(t){let{feedbackId:e,extracted:n,screenshots:r,signal:o}=t,i={};n.element&&r?.element&&(i.element={dataUrl:n.element,mime:r.element.mime,width:r.element.width,height:r.element.height}),n.fullPage&&r?.fullPage&&(i.fullPage={dataUrl:n.fullPage,mime:r.fullPage.mime,width:r.fullPage.width,height:r.fullPage.height}),Object.keys(i).length!==0&&await fetch(`${pe}/${e}/screenshots`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(i),...o?{signal:o}:{}});}async function ge(t){let{signal:e,transport:n}=t;if(Date.now()-de<ze)return {ok:false,error:m("configuration",new Error("Please wait before submitting again"))};let o=n?.timeoutMs??je,i=n?.maxAttempts??Qe,s=n?.backoffMs??We,l=!!(t.payload.screenshots?.element?.dataUrl||t.payload.screenshots?.fullPage?.dataUrl),{lean:f,extracted:p}=l?Ke(t.payload):{lean:t.payload,extracted:{}},w={...p,...t.screenshotDataUrls};for(let y=1;y<=i;y+=1){let E=new AbortController,g=setTimeout(()=>E.abort(),o),a=()=>E.abort();e&&e.addEventListener("abort",a,{once:true});try{let c=await fetch(pe,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(f),signal:E.signal});if(c.ok){de=Date.now();let u;try{u=await c.json();}catch{}if((w.element||w.fullPage)&&u){let d=u.upload_urls;if(d){let k=[];w.element&&d.element&&k.push(me(d.element,w.element,e)),w.fullPage&&d.fullPage&&k.push(me(d.fullPage,w.fullPage,e));try{await Promise.all(k);}catch{}}else if(u.feedback_id)try{await Xe({feedbackId:u.feedback_id,extracted:w,screenshots:t.payload.screenshots,...e?{signal:e}:{}});}catch{}}let x={ok:!0,status:c.status};return u&&(x.apiResponse=u),x}if(y<i&&Je(c.status)){let u=.85+Math.random()*.3,v=Math.round(s*2**(y-1)*u);await fe(v,e);continue}return {ok:!1,status:c.status,error:m("api_failed",new Error(`HTTP ${c.status}`))}}catch(c){if(E.signal.aborted||e?.aborted)return {ok:false,error:m("aborted",c)};if(y<i){let u=.85+Math.random()*.3,v=Math.round(s*2**(y-1)*u);await fe(v,e);continue}return {ok:false,error:m("api_failed",c)}}finally{clearTimeout(g),e&&e.removeEventListener("abort",a);}}return {ok:false,error:m("api_failed",new Error("Failed"))}}async function J(t){let{signal:e,transport:n}=t,r={ok:false};try{let o={payload:t.payload,...e?{signal:e}:{},...n?{transport:n}:{}};r.api=await ge(o),r.ok=!!r.api?.ok;}catch(o){r.api={ok:false,error:m("api_failed",o)},r.ok=false;}return {payload:t.payload,result:r}}var Ge=["[data-blocfeed-ui]","[data-blocfeed-ignore]"];function Ze(t){let e=[...Ge,...t?.ignoreSelectors??[]],n=Array.from(new Set(e));return {...t,ignoreSelectors:n}}function he(){return {phase:"idle"}}function Ve(t){if(t.ok)return false;let e=t.api;return e?e.status&&e.status>=400&&e.status<500||e.error?.kind==="aborted"||e.error?.kind==="configuration"?false:!e.ok:true}function Bt(t){let e=t,n=he(),r=new Set,o=new Set,i=null,s=null,l=null,f=null,p=0,w=null,y=()=>{for(let d of r)d(n);},E=d=>{for(let k of o)k(d);},g=d=>{n=d,y();},a=()=>{p+=1,f?.abort(),f=null;},c=()=>{i?.stop(),i=null,E(null),l!==null&&h()&&(document.documentElement.style.cursor=l,l=null);},u=()=>{a(),c(),s=null,g(he());},v=()=>{if(!h())return;c(),s=null;let d=Ze(e.picker);l=document.documentElement.style.cursor,document.documentElement.style.cursor="crosshair",g({phase:"picking"}),i=ue(d,{onHover:E,onSelect:({element:k,descriptor:_})=>{s=k,c(),g({phase:"review",selection:_});},onCancel:()=>{u();}});},x=()=>{let d=ce();if(d.length!==0)for(let k of d)J({payload:k,...e.transport?{transport:e.transport}:{}}).catch(()=>{W(k);});};if(h()){setTimeout(x,1e3);let d=()=>x();window.addEventListener("online",d),w=()=>window.removeEventListener("online",d);}return {getState:()=>n,subscribe(d){return r.add(d),()=>r.delete(d)},subscribeHover(d){return o.add(d),()=>o.delete(d)},start(){n.phase==="capturing"||n.phase==="submitting"||n.phase!=="picking"&&v();},stop(){u();},clearSelection(){n.phase==="capturing"||n.phase==="submitting"||v();},setConfig(d){e=d;},async submit(d,k){if(!h()){let b=m("configuration",new Error("BlocFeed submit can only run in the browser"));return g({phase:"error",lastError:b}),{ok:false}}let _=e.blocfeed_id?.trim?.()??"";if(!_){let F={phase:"error",lastError:m("configuration",new Error("Missing blocfeed_id. Create a project in BlocFeed and pass its blocfeed_id."))};return n.selection&&(F.selection=n.selection),g(F),{ok:false}}if(n.phase==="capturing"||n.phase==="submitting")return {ok:false};let C=p+1;p=C,f?.abort(),f=new AbortController;let A=f.signal,S=n.selection,D=k?.capture?{...e.capture,...k.capture}:e.capture,Y=!!(D?.element||D?.fullPage),K={phase:Y?"capturing":"submitting"};S&&(K.selection=S),g(K);try{let b=Y?await se({selectionElement:s,capture:D,signal:A}):void 0;if(A.aborted||p!==C)return {ok:!1};let F={phase:"submitting"};S&&(F.selection=S),b&&(F.capture=b),g(F);let T={};S&&(T.selection=S),b&&(T.capture=b);let we=await le({config:e.metadata,context:T,...e.user?{user:e.user}:{}}),M={version:1,createdAt:new Date().toISOString(),blocfeed_id:_,message:d,metadata:we};e.user&&(M.user=e.user),S&&(M.selection=S),b&&(M.screenshots=b);let{result:P}=await J({payload:M,signal:A,...e.transport?{transport:e.transport}:{}});if(A.aborted||p!==C)return P;if(P.ok){let U={phase:"success",lastSubmit:P};return S&&(U.selection=S),b&&(U.capture=b),g(U),P}Ve(P)&&W(M);let be=P.api?.error??m("unknown",new Error("Submission failed")),N={phase:"error",lastSubmit:P,lastError:be};return S&&(N.selection=S),b&&(N.capture=b),g(N),P}catch(b){if(A.aborted||p!==C)return {ok:false};let T={phase:"error",lastError:A.aborted?m("aborted",b):m("unknown",b)};return S&&(T.selection=S),g(T),{ok:false}}finally{p===C&&(f=null);}},__unsafeGetSelectedElement(){return s},destroy(){u(),r.clear(),o.clear(),w?.(),w=null;}}}
2
+ export{h as a,O as b,re as c,se as d,le as e,W as f,ce as g,mt as h,pt as i,Bt as j};
@@ -0,0 +1,2 @@
1
+ 'use strict';function h(){return typeof window<"u"&&typeof document<"u"}function X(t){let e=globalThis.CSS;return typeof e?.escape=="function"?e.escape(t):t.replace(/[^a-zA-Z0-9_-]/g,n=>{let r=n.codePointAt(0);return r===void 0?"":`\\${r.toString(16)} `})}function O(t){return {x:t.x,y:t.y,width:t.width,height:t.height}}function ye(t){return {x:t.x+window.scrollX,y:t.y+window.scrollY,width:t.width,height:t.height}}function Ee(t){return t.replace(/\s+/g," ").trim()}function Se(t,e=140){let n=t.textContent;if(!n)return;let r=Ee(n);if(r)return r.length<=e?r:`${r.slice(0,e-1)}\u2026`}function ke(t){let e=1;for(let n=t.previousElementSibling;n;n=n.previousElementSibling)n.tagName===t.tagName&&(e+=1);return e}var V=["data-testid","data-test-id","data-test","data-qa","data-cy"],G="data-blocfeed-component";function ve(t){let e=t.closest(`[${G}]`);if(!e)return;let r=e.getAttribute(G)?.trim();return r||void 0}function Pe(t){for(let e of V){let n=t.closest(`[${e}]`);if(!n)continue;let o=n.getAttribute(e)?.trim();if(o)return o}}function xe(t){try{let e=t,n=Object.getOwnPropertyNames(e);for(let r of n)if(r.startsWith("__reactFiber$")||r.startsWith("__reactInternalInstance$")){let o=e[r];if(o&&typeof o=="object")return o}}catch{}return null}function B(t){if(t&&typeof t!="string"){if(typeof t=="function"){let e=t;return typeof e.displayName=="string"&&e.displayName?e.displayName:typeof e.name=="string"&&e.name?e.name:void 0}if(typeof t=="object"){let e=t,n=e.displayName;if(typeof n=="string"&&n)return n;let r=e.render;if(typeof r=="function"){let i=r;if(typeof i.displayName=="string"&&i.displayName)return i.displayName;if(typeof i.name=="string"&&i.name)return i.name}let o=e.type;return B(o)}}}function Fe(t){let e=xe(t);if(!e)return;let n=e._debugOwner!==void 0;if(n){let i=e._debugOwner;for(let s=0;i&&s<50;s+=1){let l=B(i.type)??B(i.elementType);if(l)return l;i=i._debugOwner;}}let r=e,o=n?80:10;for(let i=0;r&&i<o;i+=1){let s=B(r.type)??B(r.elementType);if(s)return s;r=r.return;}}function Ae(t){let e=t.tagName.toLowerCase(),n=t.getAttribute("id");if(n)return `#${X(n)}`;for(let r of V){let o=t.getAttribute(r);if(o)return `${e}[${r}="${X(o)}"]`}return `${e}:nth-of-type(${ke(t)})`}function Te(t,e=10){let n=[],r=t;for(;r&&n.length<e;){let o=Ae(r);if(n.unshift(o),o.startsWith("#"))break;r=r.parentElement;}return n.join(" > ")}function Z(t,e){if(!e||e.length===0)return false;for(let n of e)if(t.closest(n))return true;return false}function H(t,e){if(!t||Z(t,e.ignoreSelectors))return null;let n=t;for(;n;){if(Z(n,e.ignoreSelectors))return null;if(e.isSelectable?.(n)??Re(n))return n;n=n.parentElement;}return null}function Re(t){let e=t.tagName;return !(e==="HTML"||e==="BODY")}function ee(t){let e=t.getBoundingClientRect(),n={selector:Te(t),tagName:t.tagName.toLowerCase(),rect:O(e),pageRect:ye(e)},r=t.getAttribute("id");r&&(n.id=r);let o=t.className;typeof o=="string"&&o.trim()&&(n.className=o);let i=Se(t);i&&(n.textSnippet=i);let s=ve(t)??Fe(t);s&&(n.componentName=s);let l=Pe(t);return l&&(n.testId=l),n}var q=null;async function te(){return q||(q=import('html-to-image')),q}async function Ce(t){return await new Promise((e,n)=>{let r=new Image;r.onload=()=>e({width:r.naturalWidth,height:r.naturalHeight}),r.onerror=()=>n(new Error("Failed to load generated screenshot")),r.src=t;})}async function ne(t,e){let{width:n,height:r}=await Ce(t);return {dataUrl:t,mime:e,width:n,height:r}}function R(t){if(t?.aborted)throw new Error("Aborted")}function re(){return {async captureElement(t,e){if(!h())throw new Error("captureElement can only run in the browser");R(e.signal);let n=await te();R(e.signal);let r=e.mime==="image/jpeg"?await n.toJpeg(t,{quality:e.quality??.92,pixelRatio:e.pixelRatio}):await n.toPng(t,{pixelRatio:e.pixelRatio});return R(e.signal),await ne(r,e.mime)},async captureFullPage(t){if(!h())throw new Error("captureFullPage can only run in the browser");R(t.signal);let e=document.documentElement,n=Math.max(e.scrollWidth,e.clientWidth),r=Math.max(e.scrollHeight,e.clientHeight),o=Math.min(1,t.maxDimension/Math.max(n,r)),i=Math.max(1,Math.round(n*o)),s=Math.max(1,Math.round(r*o)),l=await te();R(t.signal);let f=t.mime==="image/jpeg"?await l.toJpeg(e,{width:i,height:s,quality:t.quality??.92,pixelRatio:t.pixelRatio}):await l.toPng(e,{width:i,height:s,pixelRatio:t.pixelRatio});return R(t.signal),await ne(f,t.mime)}}}function Me(t){if(t instanceof Error)return t.message;if(typeof t=="string")return t;try{return JSON.stringify(t)}catch{return "Unknown error"}}function m(t,e,n){let r={kind:t,message:Me(e)};return n&&(r.detail=n),r}var Be=12e3,_e=2048,Ie=.92;function oe(){return Date.now()}function Le(t){return new Promise((e,n)=>{let r=()=>n(new Error("Aborted"));if(t.aborted){r();return}t.addEventListener("abort",r,{once:true});})}async function ie(t,e,n){let r=new Promise((i,s)=>{let l=setTimeout(()=>s(new Error("Timeout")),e);typeof l.unref=="function"&&l.unref();}),o=[t,r];return n&&o.push(Le(n)),await Promise.race(o)}function De(t){if(!h())return 1;let e=window.devicePixelRatio||1,n=t?.pixelRatio??Math.min(e,2);return Math.max(.1,n)}function Ne(t){return !!(t?.element||t?.fullPage)}function ae(t){let e={mime:t.mime,pixelRatio:t.pixelRatio,maxDimension:t.maxDimension};return t.includeQuality&&(e.quality=t.quality),t.signal&&(e.signal=t.signal),e}async function se(t){let{selectionElement:e,capture:n,signal:r}=t;if(!h()||!Ne(n))return;let o=oe(),i=[],s=n?.timeoutMs??Be,l=n?.maxDimension??_e,f=n?.mime??"image/png",p=n?.quality??Ie,w=n?.adapter??re(),y={},E=De(n);if(n?.element&&e)try{let c=e.getBoundingClientRect(),u=Math.min(1,l/Math.max(c.width,c.height)),v=Math.min(E,E*u),x=await ie(Promise.resolve(w.captureElement(e,{...ae({mime:f,quality:p,pixelRatio:v,maxDimension:l,includeQuality:f==="image/jpeg",...r?{signal:r}:{}})})),s,r);y.element=x;}catch(c){if(r?.aborted)throw c;i.push(m("capture_failed",c,{target:"element"}));}if(n?.fullPage)try{let c=await ie(Promise.resolve(w.captureFullPage(ae({mime:f,quality:p,pixelRatio:E,maxDimension:l,includeQuality:f==="image/jpeg",...r?{signal:r}:{}}))),s,r);y.fullPage=c;}catch(c){if(r?.aborted)throw c;i.push(m("capture_failed",c,{target:"fullPage"}));}let g=oe(),a={startedAt:o,finishedAt:g,durationMs:Math.max(0,g-o)};return i.length>0&&(a.errors=i),{...y,diagnostics:a}}function Ue(){try{return Intl.DateTimeFormat().resolvedOptions().timeZone}catch{return}}function Oe(){return h()?{url:window.location.href,title:document.title,referrer:document.referrer||void 0,userAgent:navigator.userAgent,language:navigator.language,platform:navigator.platform,viewport:{width:window.innerWidth,height:window.innerHeight},screen:{width:window.screen.width,height:window.screen.height},scroll:{x:window.scrollX,y:window.scrollY},devicePixelRatio:window.devicePixelRatio||1,timezone:Ue()}:{}}function He(t){if(!t)return {};let e={};return t.id&&(e.userId=t.id),t.email&&(e.userEmail=t.email),t.name&&(e.userName=t.name),e}async function le(t){let{config:e,context:n,user:r}=t;if(e?.enabled===false)return {};let o={...Oe(),...He(r)},i=e?.enrich;if(!i)return o;try{let s=await i(n);return {...o,...s}}catch(s){let l=m("unknown",s);return {...o,blocfeedMetadataError:l.message}}}var $="blocfeed-queue",qe=50;function j(){if(!h())return [];try{let t=localStorage.getItem($);if(!t)return [];let e=JSON.parse(t);return Array.isArray(e)?e:[]}catch{return []}}function Q(t){if(h())try{t.length===0?localStorage.removeItem($):localStorage.setItem($,JSON.stringify(t));}catch{}}function $e(t){let e={...t};if(e.screenshots){let n={...e.screenshots};n.element&&(n.element={...n.element,dataUrl:""}),n.fullPage&&(n.fullPage={...n.fullPage,dataUrl:""}),e.screenshots=n;}return e}function W(t){let e=j(),n=$e(t);for(n.metadata={...n.metadata,_queued:true},e.push({payload:n,timestamp:Date.now()});e.length>qe;)e.shift();Q(e);}function ce(){let t=j();return t.length===0?[]:(Q([]),t.map(e=>e.payload))}function mt(){Q([]);}function pt(){return j().length}function z(t){let e=null,n=null,r=(...o)=>{n=o,e===null&&(e=requestAnimationFrame(()=>{if(e=null,!n)return;let i=n;n=null,t(...i);}));};return r.cancel=()=>{e!==null&&cancelAnimationFrame(e),e=null,n=null;},r}function I(t){return t instanceof Element?!!t.closest("[data-blocfeed-ui]"):false}function L(t){t.stopPropagation(),t.stopImmediatePropagation?.();}function ue(t,e){if(!h())throw new Error("BlocFeed picker can only run in a browser environment.");let n=t.ignoreSelectors,r=t.isSelectable,o={};n&&n.length>0&&(o.ignoreSelectors=n),r&&(o.isSelectable=r);let i=null,s=null,l=(a,c=false)=>{if(!a){i=null,s=null,e.onHover(null);return}let u=O(a.getBoundingClientRect()),v=`${Math.round(u.x)}:${Math.round(u.y)}:${Math.round(u.width)}:${Math.round(u.height)}`;!c&&a===i&&v===s||(i=a,s=v,e.onHover({element:a,rect:u}));},f=z(a=>{if(I(a.target))return;let c=document.elementFromPoint(a.clientX,a.clientY),u=H(c,o);l(u);}),p=z(()=>{i&&l(i,true);}),w=a=>{I(a.target)||(L(a),a.pointerType==="mouse"&&a.preventDefault());},y=a=>{I(a.target)||(L(a),a.pointerType==="mouse"&&a.preventDefault());},E=a=>{if(I(a.target))return;L(a),a.preventDefault();let c=document.elementFromPoint(a.clientX,a.clientY),u=H(c,o);u&&e.onSelect({element:u,descriptor:ee(u)});},g=a=>{a.key==="Escape"&&(L(a),a.preventDefault(),e.onCancel());};return window.addEventListener("pointermove",f,{capture:true,passive:true}),window.addEventListener("pointerdown",w,{capture:true}),window.addEventListener("pointerup",y,{capture:true}),window.addEventListener("click",E,{capture:true}),window.addEventListener("keydown",g,{capture:true}),window.addEventListener("scroll",p,{capture:true,passive:true}),window.addEventListener("resize",p,{passive:true}),{stop(){window.removeEventListener("pointermove",f,{capture:true}),window.removeEventListener("pointerdown",w,{capture:true}),window.removeEventListener("pointerup",y,{capture:true}),window.removeEventListener("click",E,{capture:true}),window.removeEventListener("keydown",g,{capture:true}),window.removeEventListener("scroll",p,{capture:true}),window.removeEventListener("resize",p),f.cancel(),p.cancel(),e.onHover(null);}}}var je=12e3,Qe=2,We=500,ze=2e3,pe="https://blocfeed.com/api/feedback",de=0;function fe(t,e){return new Promise((n,r)=>{if(e?.aborted){r(new Error("Aborted"));return}let o=setTimeout(n,t),i=()=>{clearTimeout(o),r(new Error("Aborted"));};e?.addEventListener("abort",i,{once:true});})}function Je(t){return t>=500&&t<=599}function Ye(t){let[e,n]=t.split(",",2);if(!e||!n)throw new Error("Invalid data URL");let o=/data:(.*?);base64/.exec(e)?.[1]||"application/octet-stream",i=atob(n),s=new Uint8Array(i.length);for(let l=0;l<i.length;l+=1)s[l]=i.charCodeAt(l);return new Blob([s],{type:o})}function Ke(t){let e={},n={...t};if(n.screenshots){let r={},o={...n.screenshots};o.element&&(e.element=o.element.dataUrl,r.element={mime:o.element.mime,width:o.element.width,height:o.element.height},o.element={...o.element,dataUrl:""}),o.fullPage&&(e.fullPage=o.fullPage.dataUrl,r.fullPage={mime:o.fullPage.mime,width:o.fullPage.width,height:o.fullPage.height},o.fullPage={...o.fullPage,dataUrl:""}),n.screenshots=o,(r.element||r.fullPage)&&(n.screenshot_intent=r);}return {lean:n,extracted:e}}async function me(t,e,n){let r=Ye(e);await fetch(t,{method:"PUT",body:r,headers:{"content-type":r.type},...n?{signal:n}:{}});}async function Xe(t){let{feedbackId:e,extracted:n,screenshots:r,signal:o}=t,i={};n.element&&r?.element&&(i.element={dataUrl:n.element,mime:r.element.mime,width:r.element.width,height:r.element.height}),n.fullPage&&r?.fullPage&&(i.fullPage={dataUrl:n.fullPage,mime:r.fullPage.mime,width:r.fullPage.width,height:r.fullPage.height}),Object.keys(i).length!==0&&await fetch(`${pe}/${e}/screenshots`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(i),...o?{signal:o}:{}});}async function ge(t){let{signal:e,transport:n}=t;if(Date.now()-de<ze)return {ok:false,error:m("configuration",new Error("Please wait before submitting again"))};let o=n?.timeoutMs??je,i=n?.maxAttempts??Qe,s=n?.backoffMs??We,l=!!(t.payload.screenshots?.element?.dataUrl||t.payload.screenshots?.fullPage?.dataUrl),{lean:f,extracted:p}=l?Ke(t.payload):{lean:t.payload,extracted:{}},w={...p,...t.screenshotDataUrls};for(let y=1;y<=i;y+=1){let E=new AbortController,g=setTimeout(()=>E.abort(),o),a=()=>E.abort();e&&e.addEventListener("abort",a,{once:true});try{let c=await fetch(pe,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(f),signal:E.signal});if(c.ok){de=Date.now();let u;try{u=await c.json();}catch{}if((w.element||w.fullPage)&&u){let d=u.upload_urls;if(d){let k=[];w.element&&d.element&&k.push(me(d.element,w.element,e)),w.fullPage&&d.fullPage&&k.push(me(d.fullPage,w.fullPage,e));try{await Promise.all(k);}catch{}}else if(u.feedback_id)try{await Xe({feedbackId:u.feedback_id,extracted:w,screenshots:t.payload.screenshots,...e?{signal:e}:{}});}catch{}}let x={ok:!0,status:c.status};return u&&(x.apiResponse=u),x}if(y<i&&Je(c.status)){let u=.85+Math.random()*.3,v=Math.round(s*2**(y-1)*u);await fe(v,e);continue}return {ok:!1,status:c.status,error:m("api_failed",new Error(`HTTP ${c.status}`))}}catch(c){if(E.signal.aborted||e?.aborted)return {ok:false,error:m("aborted",c)};if(y<i){let u=.85+Math.random()*.3,v=Math.round(s*2**(y-1)*u);await fe(v,e);continue}return {ok:false,error:m("api_failed",c)}}finally{clearTimeout(g),e&&e.removeEventListener("abort",a);}}return {ok:false,error:m("api_failed",new Error("Failed"))}}async function J(t){let{signal:e,transport:n}=t,r={ok:false};try{let o={payload:t.payload,...e?{signal:e}:{},...n?{transport:n}:{}};r.api=await ge(o),r.ok=!!r.api?.ok;}catch(o){r.api={ok:false,error:m("api_failed",o)},r.ok=false;}return {payload:t.payload,result:r}}var Ge=["[data-blocfeed-ui]","[data-blocfeed-ignore]"];function Ze(t){let e=[...Ge,...t?.ignoreSelectors??[]],n=Array.from(new Set(e));return {...t,ignoreSelectors:n}}function he(){return {phase:"idle"}}function Ve(t){if(t.ok)return false;let e=t.api;return e?e.status&&e.status>=400&&e.status<500||e.error?.kind==="aborted"||e.error?.kind==="configuration"?false:!e.ok:true}function Bt(t){let e=t,n=he(),r=new Set,o=new Set,i=null,s=null,l=null,f=null,p=0,w=null,y=()=>{for(let d of r)d(n);},E=d=>{for(let k of o)k(d);},g=d=>{n=d,y();},a=()=>{p+=1,f?.abort(),f=null;},c=()=>{i?.stop(),i=null,E(null),l!==null&&h()&&(document.documentElement.style.cursor=l,l=null);},u=()=>{a(),c(),s=null,g(he());},v=()=>{if(!h())return;c(),s=null;let d=Ze(e.picker);l=document.documentElement.style.cursor,document.documentElement.style.cursor="crosshair",g({phase:"picking"}),i=ue(d,{onHover:E,onSelect:({element:k,descriptor:_})=>{s=k,c(),g({phase:"review",selection:_});},onCancel:()=>{u();}});},x=()=>{let d=ce();if(d.length!==0)for(let k of d)J({payload:k,...e.transport?{transport:e.transport}:{}}).catch(()=>{W(k);});};if(h()){setTimeout(x,1e3);let d=()=>x();window.addEventListener("online",d),w=()=>window.removeEventListener("online",d);}return {getState:()=>n,subscribe(d){return r.add(d),()=>r.delete(d)},subscribeHover(d){return o.add(d),()=>o.delete(d)},start(){n.phase==="capturing"||n.phase==="submitting"||n.phase!=="picking"&&v();},stop(){u();},clearSelection(){n.phase==="capturing"||n.phase==="submitting"||v();},setConfig(d){e=d;},async submit(d,k){if(!h()){let b=m("configuration",new Error("BlocFeed submit can only run in the browser"));return g({phase:"error",lastError:b}),{ok:false}}let _=e.blocfeed_id?.trim?.()??"";if(!_){let F={phase:"error",lastError:m("configuration",new Error("Missing blocfeed_id. Create a project in BlocFeed and pass its blocfeed_id."))};return n.selection&&(F.selection=n.selection),g(F),{ok:false}}if(n.phase==="capturing"||n.phase==="submitting")return {ok:false};let C=p+1;p=C,f?.abort(),f=new AbortController;let A=f.signal,S=n.selection,D=k?.capture?{...e.capture,...k.capture}:e.capture,Y=!!(D?.element||D?.fullPage),K={phase:Y?"capturing":"submitting"};S&&(K.selection=S),g(K);try{let b=Y?await se({selectionElement:s,capture:D,signal:A}):void 0;if(A.aborted||p!==C)return {ok:!1};let F={phase:"submitting"};S&&(F.selection=S),b&&(F.capture=b),g(F);let T={};S&&(T.selection=S),b&&(T.capture=b);let we=await le({config:e.metadata,context:T,...e.user?{user:e.user}:{}}),M={version:1,createdAt:new Date().toISOString(),blocfeed_id:_,message:d,metadata:we};e.user&&(M.user=e.user),S&&(M.selection=S),b&&(M.screenshots=b);let{result:P}=await J({payload:M,signal:A,...e.transport?{transport:e.transport}:{}});if(A.aborted||p!==C)return P;if(P.ok){let U={phase:"success",lastSubmit:P};return S&&(U.selection=S),b&&(U.capture=b),g(U),P}Ve(P)&&W(M);let be=P.api?.error??m("unknown",new Error("Submission failed")),N={phase:"error",lastSubmit:P,lastError:be};return S&&(N.selection=S),b&&(N.capture=b),g(N),P}catch(b){if(A.aborted||p!==C)return {ok:false};let T={phase:"error",lastError:A.aborted?m("aborted",b):m("unknown",b)};return S&&(T.selection=S),g(T),{ok:false}}finally{p===C&&(f=null);}},__unsafeGetSelectedElement(){return s},destroy(){u(),r.clear(),o.clear(),w?.(),w=null;}}}
2
+ exports.a=h;exports.b=O;exports.c=re;exports.d=se;exports.e=le;exports.f=W;exports.g=ce;exports.h=mt;exports.i=pt;exports.j=Bt;
@@ -10,6 +10,27 @@ interface ImageAsset {
10
10
  width: number;
11
11
  height: number;
12
12
  }
13
+ interface BlocFeedUser {
14
+ id?: string;
15
+ email?: string;
16
+ name?: string;
17
+ }
18
+ interface TransportConfig {
19
+ /** Timeout per request attempt in milliseconds. Default: 12 000 */
20
+ timeoutMs?: number;
21
+ /** Maximum number of retry attempts. Default: 2 */
22
+ maxAttempts?: number;
23
+ /** Base backoff delay in milliseconds. Default: 500 */
24
+ backoffMs?: number;
25
+ }
26
+ type WidgetPosition = "bottom-right" | "bottom-left" | "top-right" | "top-left";
27
+ type TriggerStyle = "classic" | "dot" | "bubble" | "edge-tab" | "pulse-ring" | "minimal" | "icon-pop";
28
+ interface ThemeConfig {
29
+ accentColor?: string;
30
+ panelBackground?: string;
31
+ panelForeground?: string;
32
+ fontFamily?: string;
33
+ }
13
34
  interface Rect {
14
35
  x: number;
15
36
  y: number;
@@ -121,11 +142,31 @@ interface BlocFeedConfig {
121
142
  capture?: CaptureConfig;
122
143
  picker?: PickerConfig;
123
144
  metadata?: MetadataConfig;
145
+ /** First-class user identity attached to every submission. */
146
+ user?: BlocFeedUser;
147
+ /** Transport / retry configuration. */
148
+ transport?: TransportConfig;
124
149
  ui?: {
125
- /**
126
- * z-index for the widget overlay/panel.
127
- */
150
+ /** z-index for the widget overlay/panel. */
128
151
  zIndex?: number;
152
+ /** Position of the trigger button. Default: "bottom-right" */
153
+ position?: WidgetPosition;
154
+ /** Theme overrides. */
155
+ theme?: ThemeConfig;
156
+ /** Trigger button animation style. Default: "classic" */
157
+ triggerStyle?: TriggerStyle;
158
+ };
159
+ }
160
+ interface ScreenshotIntent {
161
+ element?: {
162
+ mime: string;
163
+ width: number;
164
+ height: number;
165
+ };
166
+ fullPage?: {
167
+ mime: string;
168
+ width: number;
169
+ height: number;
129
170
  };
130
171
  }
131
172
  interface FeedbackPayload {
@@ -135,8 +176,20 @@ interface FeedbackPayload {
135
176
  message: string;
136
177
  selection?: ElementDescriptor;
137
178
  screenshots?: CaptureResult;
179
+ /** Lightweight screenshot metadata sent instead of base64 dataUrls. */
180
+ screenshot_intent?: ScreenshotIntent;
181
+ /** First-class user identity. */
182
+ user?: BlocFeedUser;
138
183
  metadata: Record<string, unknown>;
139
184
  }
185
+ interface FeedbackApiResponse {
186
+ success: boolean;
187
+ feedback_id?: string;
188
+ upload_urls?: {
189
+ element?: string;
190
+ fullPage?: string;
191
+ };
192
+ }
140
193
  interface TransportResult {
141
194
  ok: boolean;
142
195
  error?: BlocFeedError;
@@ -179,4 +232,4 @@ interface BlocFeedController {
179
232
  }
180
233
  declare function createBlocFeedController(config: BlocFeedConfig): BlocFeedController;
181
234
 
182
- export { type BlocFeedConfig as B, type CaptureConfig as C, type ElementDescriptor as E, type FeedbackPayload as F, type HoverListener as H, type ImageAsset as I, type MaybePromise as M, type PickerConfig as P, type Rect as R, type SubmitResult as S, type TransportResult as T, type BlocFeedState as a, type BlocFeedController as b, type BlocFeedError as c, type CaptureDiagnostics as d, type CaptureResult as e, type MetadataConfig as f, type MetadataContext as g, type ScreenshotAdapter as h, type ScreenshotAdapterOptions as i, type ScreenshotMime as j, type SessionPhase as k, type StateListener as l, createBlocFeedController as m };
235
+ export { type BlocFeedConfig as B, type CaptureConfig as C, type ElementDescriptor as E, type FeedbackApiResponse as F, type HoverListener as H, type ImageAsset as I, type MaybePromise as M, type PickerConfig as P, type Rect as R, type SubmitResult as S, type ThemeConfig as T, type WidgetPosition as W, type BlocFeedState as a, type BlocFeedController as b, type BlocFeedError as c, type BlocFeedUser as d, type CaptureDiagnostics as e, type CaptureResult as f, type FeedbackPayload as g, type MetadataConfig as h, type MetadataContext as i, type ScreenshotAdapter as j, type ScreenshotAdapterOptions as k, type ScreenshotIntent as l, type ScreenshotMime as m, type SessionPhase as n, type TransportConfig as o, type TransportResult as p, type TriggerStyle as q, type StateListener as r, createBlocFeedController as s };
@@ -10,6 +10,27 @@ interface ImageAsset {
10
10
  width: number;
11
11
  height: number;
12
12
  }
13
+ interface BlocFeedUser {
14
+ id?: string;
15
+ email?: string;
16
+ name?: string;
17
+ }
18
+ interface TransportConfig {
19
+ /** Timeout per request attempt in milliseconds. Default: 12 000 */
20
+ timeoutMs?: number;
21
+ /** Maximum number of retry attempts. Default: 2 */
22
+ maxAttempts?: number;
23
+ /** Base backoff delay in milliseconds. Default: 500 */
24
+ backoffMs?: number;
25
+ }
26
+ type WidgetPosition = "bottom-right" | "bottom-left" | "top-right" | "top-left";
27
+ type TriggerStyle = "classic" | "dot" | "bubble" | "edge-tab" | "pulse-ring" | "minimal" | "icon-pop";
28
+ interface ThemeConfig {
29
+ accentColor?: string;
30
+ panelBackground?: string;
31
+ panelForeground?: string;
32
+ fontFamily?: string;
33
+ }
13
34
  interface Rect {
14
35
  x: number;
15
36
  y: number;
@@ -121,11 +142,31 @@ interface BlocFeedConfig {
121
142
  capture?: CaptureConfig;
122
143
  picker?: PickerConfig;
123
144
  metadata?: MetadataConfig;
145
+ /** First-class user identity attached to every submission. */
146
+ user?: BlocFeedUser;
147
+ /** Transport / retry configuration. */
148
+ transport?: TransportConfig;
124
149
  ui?: {
125
- /**
126
- * z-index for the widget overlay/panel.
127
- */
150
+ /** z-index for the widget overlay/panel. */
128
151
  zIndex?: number;
152
+ /** Position of the trigger button. Default: "bottom-right" */
153
+ position?: WidgetPosition;
154
+ /** Theme overrides. */
155
+ theme?: ThemeConfig;
156
+ /** Trigger button animation style. Default: "classic" */
157
+ triggerStyle?: TriggerStyle;
158
+ };
159
+ }
160
+ interface ScreenshotIntent {
161
+ element?: {
162
+ mime: string;
163
+ width: number;
164
+ height: number;
165
+ };
166
+ fullPage?: {
167
+ mime: string;
168
+ width: number;
169
+ height: number;
129
170
  };
130
171
  }
131
172
  interface FeedbackPayload {
@@ -135,8 +176,20 @@ interface FeedbackPayload {
135
176
  message: string;
136
177
  selection?: ElementDescriptor;
137
178
  screenshots?: CaptureResult;
179
+ /** Lightweight screenshot metadata sent instead of base64 dataUrls. */
180
+ screenshot_intent?: ScreenshotIntent;
181
+ /** First-class user identity. */
182
+ user?: BlocFeedUser;
138
183
  metadata: Record<string, unknown>;
139
184
  }
185
+ interface FeedbackApiResponse {
186
+ success: boolean;
187
+ feedback_id?: string;
188
+ upload_urls?: {
189
+ element?: string;
190
+ fullPage?: string;
191
+ };
192
+ }
140
193
  interface TransportResult {
141
194
  ok: boolean;
142
195
  error?: BlocFeedError;
@@ -179,4 +232,4 @@ interface BlocFeedController {
179
232
  }
180
233
  declare function createBlocFeedController(config: BlocFeedConfig): BlocFeedController;
181
234
 
182
- export { type BlocFeedConfig as B, type CaptureConfig as C, type ElementDescriptor as E, type FeedbackPayload as F, type HoverListener as H, type ImageAsset as I, type MaybePromise as M, type PickerConfig as P, type Rect as R, type SubmitResult as S, type TransportResult as T, type BlocFeedState as a, type BlocFeedController as b, type BlocFeedError as c, type CaptureDiagnostics as d, type CaptureResult as e, type MetadataConfig as f, type MetadataContext as g, type ScreenshotAdapter as h, type ScreenshotAdapterOptions as i, type ScreenshotMime as j, type SessionPhase as k, type StateListener as l, createBlocFeedController as m };
235
+ export { type BlocFeedConfig as B, type CaptureConfig as C, type ElementDescriptor as E, type FeedbackApiResponse as F, type HoverListener as H, type ImageAsset as I, type MaybePromise as M, type PickerConfig as P, type Rect as R, type SubmitResult as S, type ThemeConfig as T, type WidgetPosition as W, type BlocFeedState as a, type BlocFeedController as b, type BlocFeedError as c, type BlocFeedUser as d, type CaptureDiagnostics as e, type CaptureResult as f, type FeedbackPayload as g, type MetadataConfig as h, type MetadataContext as i, type ScreenshotAdapter as j, type ScreenshotAdapterOptions as k, type ScreenshotIntent as l, type ScreenshotMime as m, type SessionPhase as n, type TransportConfig as o, type TransportResult as p, type TriggerStyle as q, type StateListener as r, createBlocFeedController as s };
package/dist/engine.cjs CHANGED
@@ -1 +1 @@
1
- 'use strict';var chunkPEPIK3FN_cjs=require('./chunk-PEPIK3FN.cjs');function d(n){let[r,o]=n.split(",",2);if(!r||!o)throw new Error("Invalid data URL");let c=/data:(.*?);base64/.exec(r)?.[1]||"application/octet-stream",t=atob(o),a=new Uint8Array(t.length);for(let e=0;e<t.length;e+=1)a[e]=t.charCodeAt(e);return new Blob([a],{type:c})}Object.defineProperty(exports,"collectMetadata",{enumerable:true,get:function(){return chunkPEPIK3FN_cjs.e}});Object.defineProperty(exports,"createBlocFeedController",{enumerable:true,get:function(){return chunkPEPIK3FN_cjs.f}});Object.defineProperty(exports,"createHtmlToImageAdapter",{enumerable:true,get:function(){return chunkPEPIK3FN_cjs.c}});Object.defineProperty(exports,"runCapture",{enumerable:true,get:function(){return chunkPEPIK3FN_cjs.d}});exports.dataUrlToBlob=d;
1
+ 'use strict';var chunkJLPJP7DD_cjs=require('./chunk-JLPJP7DD.cjs');function c(o){if(o?.aborted)throw new Error("Aborted")}async function A(o){return await new Promise((t,e)=>{let r=new Image;r.onload=()=>t({width:r.naturalWidth,height:r.naturalHeight}),r.onerror=()=>e(new Error("Failed to load generated screenshot")),r.src=o;})}async function l(o,t){let{width:e,height:r}=await A(o);return {dataUrl:o,mime:t,width:e,height:r}}function x(o){return {async captureElement(t,e){if(!chunkJLPJP7DD_cjs.a())throw new Error("captureElement can only run in the browser");c(e.signal);let r={scale:e.pixelRatio},n=e.mime==="image/jpeg"?await o.domToJpeg(t,{...r,quality:e.quality??.92}):await o.domToPng(t,r);return c(e.signal),await l(n,e.mime)},async captureFullPage(t){if(!chunkJLPJP7DD_cjs.a())throw new Error("captureFullPage can only run in the browser");c(t.signal);let e=document.documentElement,r=Math.max(e.scrollWidth,e.clientWidth),n=Math.max(e.scrollHeight,e.clientHeight),a=Math.min(1,t.maxDimension/Math.max(r,n)),s={width:Math.max(1,Math.round(r*a)),height:Math.max(1,Math.round(n*a)),scale:t.pixelRatio},i=t.mime==="image/jpeg"?await o.domToJpeg(e,{...s,quality:t.quality??.92}):await o.domToPng(e,s);return c(t.signal),await l(i,t.mime)}}}function b(o){let[t,e]=o.split(",",2);if(!t||!e)throw new Error("Invalid data URL");let n=/data:(.*?);base64/.exec(t)?.[1]||"application/octet-stream",a=atob(e),s=new Uint8Array(a.length);for(let i=0;i<a.length;i+=1)s[i]=a.charCodeAt(i);return new Blob([s],{type:n})}Object.defineProperty(exports,"clearQueue",{enumerable:true,get:function(){return chunkJLPJP7DD_cjs.h}});Object.defineProperty(exports,"collectMetadata",{enumerable:true,get:function(){return chunkJLPJP7DD_cjs.e}});Object.defineProperty(exports,"createBlocFeedController",{enumerable:true,get:function(){return chunkJLPJP7DD_cjs.j}});Object.defineProperty(exports,"createHtmlToImageAdapter",{enumerable:true,get:function(){return chunkJLPJP7DD_cjs.c}});Object.defineProperty(exports,"dequeueAll",{enumerable:true,get:function(){return chunkJLPJP7DD_cjs.g}});Object.defineProperty(exports,"enqueue",{enumerable:true,get:function(){return chunkJLPJP7DD_cjs.f}});Object.defineProperty(exports,"getQueueSize",{enumerable:true,get:function(){return chunkJLPJP7DD_cjs.i}});Object.defineProperty(exports,"runCapture",{enumerable:true,get:function(){return chunkJLPJP7DD_cjs.d}});exports.createModernScreenshotAdapter=x;exports.dataUrlToBlob=b;