blocfeed 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,8 +1,32 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 0.3.0 — 2026-02-20
4
4
 
5
- - (none)
5
+ ### New features
6
+
7
+ - **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`.
8
+ - **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`.
9
+ - **Widget positioning** — new `config.ui.position` option places the trigger button in any corner: `"bottom-right"` (default), `"bottom-left"`, `"top-right"`, `"top-left"`.
10
+ - **Theme customization** — new `config.ui.theme` option overrides visual appearance via CSS variables: `accentColor`, `panelBackground`, `panelForeground`, `fontFamily`.
11
+ - **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.
12
+ - **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"`.
13
+ - **Client-side rate limiting** — minimum 2-second interval enforced between submissions to prevent accidental spam.
14
+ - **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.
15
+ - **Capture progress indicator** — pulsing spinner animation shown during screenshot capture and submission phases.
16
+
17
+ ### Improvements
18
+
19
+ - **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.
20
+ - **Full-page screenshot storage** — full-page screenshots are now processed and stored on the platform (previously captured but discarded server-side).
21
+ - **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.
22
+ - **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.
23
+ - **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.
24
+
25
+ ### Platform changes (blocfeed-frontend)
26
+
27
+ - `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.
28
+ - New `POST /api/feedback/:id/screenshots` fallback endpoint for uploading screenshots as base64 when object storage is unavailable.
29
+ - New SQL migration `scripts/026_fullpage_screenshots.sql` adds `screenshot_fullpage_*` and `meta_user_*` columns.
6
30
 
7
31
  ## 0.2.2 — 2026-02-17
8
32
 
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,140 @@ 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
106
+ ui: {
107
+ position: "bottom-right", // "bottom-left" | "top-right" | "top-left"
108
+ zIndex: 99999,
109
+ theme: {
110
+ accentColor: "#12D393",
111
+ panelBackground: "rgba(0, 0, 0, 0.95)",
112
+ panelForeground: "#ffffff",
113
+ fontFamily: "Inter, sans-serif",
114
+ },
115
+ },
116
+
117
+ // Screenshot defaults
118
+ capture: {
119
+ element: true,
120
+ fullPage: false,
121
+ mime: "image/png", // or "image/jpeg"
122
+ quality: 0.92, // JPEG quality (0–1)
123
+ timeoutMs: 12000,
124
+ maxDimension: 2048,
125
+ pixelRatio: 2,
126
+ },
127
+
128
+ // Retry & transport
129
+ transport: {
130
+ timeoutMs: 12000, // per-request timeout
131
+ maxAttempts: 2, // retry count
132
+ backoffMs: 500, // base backoff delay
133
+ },
134
+
135
+ // Metadata enrichment
136
+ metadata: {
137
+ enabled: true,
138
+ enrich: async (context) => ({
139
+ orgId: "org_abc",
140
+ plan: "pro",
141
+ }),
142
+ },
143
+
144
+ // Picker rules
145
+ picker: {
146
+ ignoreSelectors: ["[data-private]"],
147
+ isSelectable: (el) => el.tagName !== "HTML",
148
+ },
149
+ }}
150
+ />
151
+ ```
152
+
153
+ ## User Identity
154
+
155
+ Attach user information to every feedback submission:
156
+
157
+ ```tsx
158
+ <BlocFeedWidget
159
+ blocfeed_id="bf_..."
160
+ config={{
161
+ user: {
162
+ id: currentUser.id,
163
+ email: currentUser.email,
164
+ name: currentUser.name,
165
+ },
166
+ }}
167
+ />
168
+ ```
169
+
170
+ 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`.
171
+
172
+ ## Widget Position
173
+
174
+ Place the trigger button in any corner:
175
+
176
+ ```tsx
177
+ config={{
178
+ ui: { position: "bottom-left" } // default: "bottom-right"
179
+ }}
180
+ ```
181
+
182
+ Options: `"bottom-right"` | `"bottom-left"` | `"top-right"` | `"top-left"`
183
+
184
+ ## Theme Customization
185
+
186
+ Override the widget's visual appearance via CSS variables:
187
+
188
+ ```tsx
189
+ config={{
190
+ ui: {
191
+ theme: {
192
+ accentColor: "#12D393", // buttons, highlights, focus rings
193
+ panelBackground: "rgba(0,0,0,0.95)", // panel & trigger background
194
+ panelForeground: "#ffffff", // text color
195
+ fontFamily: "Inter, sans-serif", // font stack
196
+ },
197
+ },
198
+ }}
199
+ ```
200
+
201
+ ## Retry & Transport
202
+
203
+ Configure retry behavior for unreliable networks:
204
+
205
+ ```tsx
206
+ config={{
207
+ transport: {
208
+ timeoutMs: 15000, // 15s timeout per attempt
209
+ maxAttempts: 3, // retry up to 3 times
210
+ backoffMs: 1000, // 1s base backoff (exponential with jitter)
211
+ },
212
+ }}
213
+ ```
214
+
215
+ ## Offline Queue
216
+
217
+ When a submission fails due to a network error, the payload is automatically saved to `localStorage` and retried:
218
+
219
+ - On the next page load (1s after controller initialization)
220
+ - When the browser comes back online (`online` event)
221
+
222
+ Screenshots are stripped from queued payloads to stay within `localStorage` limits. The queue holds up to 50 items (FIFO eviction).
223
+
86
224
  ## Picking rules (ignore / filter)
87
225
 
88
226
  ### Ignore a subtree (recommended)
@@ -116,7 +254,7 @@ export function CheckoutButton() {
116
254
  }
117
255
  ```
118
256
 
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.
257
+ 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
258
 
121
259
  ### `selection.testId`
122
260
 
@@ -124,8 +262,44 @@ BlocFeed extracts a best-effort `testId` from common attributes like `data-testi
124
262
 
125
263
  ## Screenshots
126
264
 
127
- - Screenshot output is stored as Data URLs on `payload.screenshots`.
128
- - Convert to `Blob` if needed:
265
+ Screenshots are uploaded efficiently using a two-phase flow:
266
+
267
+ 1. The text payload (metadata, selection, message) is sent first without screenshot data
268
+ 2. If the platform returns presigned upload URLs (Wasabi/S3), screenshots are uploaded directly to object storage
269
+ 3. If presigned URLs are unavailable, screenshots fall back to a secondary POST endpoint
270
+
271
+ Both **element** and **full-page** screenshots are supported and stored on the platform.
272
+
273
+ ### Known limitations
274
+
275
+ The default screenshot engine (`html-to-image`) has known issues with:
276
+ - Cross-origin images without permissive CORS headers
277
+ - CSS `clip-path` and `backdrop-filter`
278
+ - Some web fonts
279
+
280
+ ### Alternative screenshot adapter
281
+
282
+ For better compatibility, you can use `modern-screenshot` as a drop-in replacement:
283
+
284
+ ```bash
285
+ npm install modern-screenshot
286
+ ```
287
+
288
+ ```tsx
289
+ import { createModernScreenshotAdapter } from "blocfeed/engine";
290
+ import * as modernScreenshot from "modern-screenshot";
291
+
292
+ <BlocFeedWidget
293
+ blocfeed_id="bf_..."
294
+ config={{
295
+ capture: {
296
+ adapter: createModernScreenshotAdapter(modernScreenshot),
297
+ },
298
+ }}
299
+ />
300
+ ```
301
+
302
+ ### Convert to Blob
129
303
 
130
304
  ```ts
131
305
  import { dataUrlToBlob } from "blocfeed/engine";
@@ -133,11 +307,25 @@ import { dataUrlToBlob } from "blocfeed/engine";
133
307
  const blob = dataUrlToBlob(payload.screenshots?.element?.dataUrl ?? "");
134
308
  ```
135
309
 
136
- Note: cross-origin images without permissive CORS headers may be missing from captures.
310
+ ## Keyboard Accessibility
311
+
312
+ The widget supports full keyboard navigation:
313
+
314
+ - **Tab** cycles through interactive elements in the feedback panel
315
+ - **Shift+Tab** cycles backward
316
+ - **Escape** cancels picking or closes the panel
317
+ - **Ctrl/Cmd+Enter** submits feedback from the textarea
318
+ - Focus is trapped within the panel when open
319
+ - All interactive elements have `aria-label` attributes
320
+ - Status messages use `aria-live="polite"` for screen reader announcements
321
+
322
+ ## Client-Side Rate Limiting
323
+
324
+ A minimum 2-second interval is enforced between submissions to prevent accidental spam. Rapid submissions return a descriptive error.
137
325
 
138
326
  ## Custom UI
139
327
 
140
- If you dont want the default widget UI:
328
+ If you don't want the default widget UI:
141
329
 
142
330
  ```tsx
143
331
  import { BlocFeedProvider, useBlocFeed } from "blocfeed";
@@ -165,6 +353,31 @@ import { createBlocFeedController } from "blocfeed/engine";
165
353
  const controller = createBlocFeedController({ blocfeed_id: "bf_your_project_blocfeed_id" });
166
354
  ```
167
355
 
356
+ ### Offline queue utilities (headless)
357
+
358
+ ```ts
359
+ import { enqueue, dequeueAll, clearQueue, getQueueSize } from "blocfeed/engine";
360
+ ```
361
+
362
+ ## TypeScript
363
+
364
+ All types are exported from both entry points:
365
+
366
+ ```ts
367
+ import type {
368
+ BlocFeedConfig,
369
+ BlocFeedUser,
370
+ TransportConfig,
371
+ WidgetPosition,
372
+ ThemeConfig,
373
+ FeedbackPayload,
374
+ FeedbackApiResponse,
375
+ BlocFeedState,
376
+ SessionPhase,
377
+ // ... and more
378
+ } from "blocfeed";
379
+ ```
380
+
168
381
  ## Local development
169
382
 
170
383
  ```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,26 @@ 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
+ interface ThemeConfig {
28
+ accentColor?: string;
29
+ panelBackground?: string;
30
+ panelForeground?: string;
31
+ fontFamily?: string;
32
+ }
13
33
  interface Rect {
14
34
  x: number;
15
35
  y: number;
@@ -121,11 +141,29 @@ interface BlocFeedConfig {
121
141
  capture?: CaptureConfig;
122
142
  picker?: PickerConfig;
123
143
  metadata?: MetadataConfig;
144
+ /** First-class user identity attached to every submission. */
145
+ user?: BlocFeedUser;
146
+ /** Transport / retry configuration. */
147
+ transport?: TransportConfig;
124
148
  ui?: {
125
- /**
126
- * z-index for the widget overlay/panel.
127
- */
149
+ /** z-index for the widget overlay/panel. */
128
150
  zIndex?: number;
151
+ /** Position of the trigger button. Default: "bottom-right" */
152
+ position?: WidgetPosition;
153
+ /** Theme overrides. */
154
+ theme?: ThemeConfig;
155
+ };
156
+ }
157
+ interface ScreenshotIntent {
158
+ element?: {
159
+ mime: string;
160
+ width: number;
161
+ height: number;
162
+ };
163
+ fullPage?: {
164
+ mime: string;
165
+ width: number;
166
+ height: number;
129
167
  };
130
168
  }
131
169
  interface FeedbackPayload {
@@ -135,8 +173,20 @@ interface FeedbackPayload {
135
173
  message: string;
136
174
  selection?: ElementDescriptor;
137
175
  screenshots?: CaptureResult;
176
+ /** Lightweight screenshot metadata sent instead of base64 dataUrls. */
177
+ screenshot_intent?: ScreenshotIntent;
178
+ /** First-class user identity. */
179
+ user?: BlocFeedUser;
138
180
  metadata: Record<string, unknown>;
139
181
  }
182
+ interface FeedbackApiResponse {
183
+ success: boolean;
184
+ feedback_id?: string;
185
+ upload_urls?: {
186
+ element?: string;
187
+ fullPage?: string;
188
+ };
189
+ }
140
190
  interface TransportResult {
141
191
  ok: boolean;
142
192
  error?: BlocFeedError;
@@ -179,4 +229,4 @@ interface BlocFeedController {
179
229
  }
180
230
  declare function createBlocFeedController(config: BlocFeedConfig): BlocFeedController;
181
231
 
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 };
232
+ 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 StateListener as q, createBlocFeedController as r };
@@ -10,6 +10,26 @@ 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
+ interface ThemeConfig {
28
+ accentColor?: string;
29
+ panelBackground?: string;
30
+ panelForeground?: string;
31
+ fontFamily?: string;
32
+ }
13
33
  interface Rect {
14
34
  x: number;
15
35
  y: number;
@@ -121,11 +141,29 @@ interface BlocFeedConfig {
121
141
  capture?: CaptureConfig;
122
142
  picker?: PickerConfig;
123
143
  metadata?: MetadataConfig;
144
+ /** First-class user identity attached to every submission. */
145
+ user?: BlocFeedUser;
146
+ /** Transport / retry configuration. */
147
+ transport?: TransportConfig;
124
148
  ui?: {
125
- /**
126
- * z-index for the widget overlay/panel.
127
- */
149
+ /** z-index for the widget overlay/panel. */
128
150
  zIndex?: number;
151
+ /** Position of the trigger button. Default: "bottom-right" */
152
+ position?: WidgetPosition;
153
+ /** Theme overrides. */
154
+ theme?: ThemeConfig;
155
+ };
156
+ }
157
+ interface ScreenshotIntent {
158
+ element?: {
159
+ mime: string;
160
+ width: number;
161
+ height: number;
162
+ };
163
+ fullPage?: {
164
+ mime: string;
165
+ width: number;
166
+ height: number;
129
167
  };
130
168
  }
131
169
  interface FeedbackPayload {
@@ -135,8 +173,20 @@ interface FeedbackPayload {
135
173
  message: string;
136
174
  selection?: ElementDescriptor;
137
175
  screenshots?: CaptureResult;
176
+ /** Lightweight screenshot metadata sent instead of base64 dataUrls. */
177
+ screenshot_intent?: ScreenshotIntent;
178
+ /** First-class user identity. */
179
+ user?: BlocFeedUser;
138
180
  metadata: Record<string, unknown>;
139
181
  }
182
+ interface FeedbackApiResponse {
183
+ success: boolean;
184
+ feedback_id?: string;
185
+ upload_urls?: {
186
+ element?: string;
187
+ fullPage?: string;
188
+ };
189
+ }
140
190
  interface TransportResult {
141
191
  ok: boolean;
142
192
  error?: BlocFeedError;
@@ -179,4 +229,4 @@ interface BlocFeedController {
179
229
  }
180
230
  declare function createBlocFeedController(config: BlocFeedConfig): BlocFeedController;
181
231
 
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 };
232
+ 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 StateListener as q, createBlocFeedController as r };
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 S(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 S(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 y(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=y;
package/dist/engine.d.cts CHANGED
@@ -1,8 +1,37 @@
1
- import { h as ScreenshotAdapter, C as CaptureConfig, e as CaptureResult, f as MetadataConfig, g as MetadataContext } from './controller-DmWFVHoj.cjs';
2
- export { B as BlocFeedConfig, b as BlocFeedController, c as BlocFeedError, a as BlocFeedState, E as ElementDescriptor, F as FeedbackPayload, H as HoverListener, I as ImageAsset, R as Rect, i as ScreenshotAdapterOptions, j as ScreenshotMime, k as SessionPhase, l as StateListener, S as SubmitResult, m as createBlocFeedController } from './controller-DmWFVHoj.cjs';
1
+ import { j as ScreenshotAdapter, C as CaptureConfig, f as CaptureResult, h as MetadataConfig, i as MetadataContext, d as BlocFeedUser, g as FeedbackPayload } from './controller-olqOZLhp.cjs';
2
+ export { B as BlocFeedConfig, b as BlocFeedController, c as BlocFeedError, a as BlocFeedState, E as ElementDescriptor, F as FeedbackApiResponse, H as HoverListener, I as ImageAsset, R as Rect, k as ScreenshotAdapterOptions, l as ScreenshotIntent, m as ScreenshotMime, n as SessionPhase, q as StateListener, S as SubmitResult, T as ThemeConfig, o as TransportConfig, W as WidgetPosition, r as createBlocFeedController } from './controller-olqOZLhp.cjs';
3
3
 
4
4
  declare function createHtmlToImageAdapter(): ScreenshotAdapter;
5
5
 
6
+ /**
7
+ * Minimal interface that `modern-screenshot` satisfies.
8
+ * The consumer must install `modern-screenshot` themselves:
9
+ *
10
+ * npm install modern-screenshot
11
+ *
12
+ * Then pass the module into this factory:
13
+ *
14
+ * ```ts
15
+ * import { createModernScreenshotAdapter } from "blocfeed/engine";
16
+ * import * as ms from "modern-screenshot";
17
+ *
18
+ * <BlocFeedWidget
19
+ * blocfeed_id="bf_..."
20
+ * config={{ capture: { adapter: createModernScreenshotAdapter(ms) } }}
21
+ * />
22
+ * ```
23
+ *
24
+ * Benefits over `html-to-image`:
25
+ * - Better cross-origin image handling
26
+ * - Supports CSS `backdrop-filter`, `clip-path`
27
+ * - Better web font rendering
28
+ */
29
+ interface ModernScreenshotModule {
30
+ domToPng: (node: Node, options?: Record<string, unknown>) => Promise<string>;
31
+ domToJpeg: (node: Node, options?: Record<string, unknown>) => Promise<string>;
32
+ }
33
+ declare function createModernScreenshotAdapter(mod: ModernScreenshotModule): ScreenshotAdapter;
34
+
6
35
  declare function runCapture(params: {
7
36
  selectionElement: Element | null;
8
37
  capture: CaptureConfig | undefined;
@@ -12,8 +41,27 @@ declare function runCapture(params: {
12
41
  declare function collectMetadata(params: {
13
42
  config: MetadataConfig | undefined;
14
43
  context: MetadataContext;
44
+ user?: BlocFeedUser;
15
45
  }): Promise<Record<string, unknown>>;
16
46
 
47
+ /**
48
+ * Add a failed payload to the offline queue.
49
+ * Screenshots are stripped to stay within localStorage limits.
50
+ */
51
+ declare function enqueue(payload: FeedbackPayload): void;
52
+ /**
53
+ * Remove and return all queued payloads.
54
+ */
55
+ declare function dequeueAll(): FeedbackPayload[];
56
+ /**
57
+ * Clear the entire queue.
58
+ */
59
+ declare function clearQueue(): void;
60
+ /**
61
+ * Get the number of items in the queue.
62
+ */
63
+ declare function getQueueSize(): number;
64
+
17
65
  declare function dataUrlToBlob(dataUrl: string): Blob;
18
66
 
19
- export { CaptureResult, ScreenshotAdapter, collectMetadata, createHtmlToImageAdapter, dataUrlToBlob, runCapture };
67
+ export { BlocFeedUser, CaptureResult, FeedbackPayload, type ModernScreenshotModule, ScreenshotAdapter, clearQueue, collectMetadata, createHtmlToImageAdapter, createModernScreenshotAdapter, dataUrlToBlob, dequeueAll, enqueue, getQueueSize, runCapture };
package/dist/engine.d.ts CHANGED
@@ -1,8 +1,37 @@
1
- import { h as ScreenshotAdapter, C as CaptureConfig, e as CaptureResult, f as MetadataConfig, g as MetadataContext } from './controller-DmWFVHoj.js';
2
- export { B as BlocFeedConfig, b as BlocFeedController, c as BlocFeedError, a as BlocFeedState, E as ElementDescriptor, F as FeedbackPayload, H as HoverListener, I as ImageAsset, R as Rect, i as ScreenshotAdapterOptions, j as ScreenshotMime, k as SessionPhase, l as StateListener, S as SubmitResult, m as createBlocFeedController } from './controller-DmWFVHoj.js';
1
+ import { j as ScreenshotAdapter, C as CaptureConfig, f as CaptureResult, h as MetadataConfig, i as MetadataContext, d as BlocFeedUser, g as FeedbackPayload } from './controller-olqOZLhp.js';
2
+ export { B as BlocFeedConfig, b as BlocFeedController, c as BlocFeedError, a as BlocFeedState, E as ElementDescriptor, F as FeedbackApiResponse, H as HoverListener, I as ImageAsset, R as Rect, k as ScreenshotAdapterOptions, l as ScreenshotIntent, m as ScreenshotMime, n as SessionPhase, q as StateListener, S as SubmitResult, T as ThemeConfig, o as TransportConfig, W as WidgetPosition, r as createBlocFeedController } from './controller-olqOZLhp.js';
3
3
 
4
4
  declare function createHtmlToImageAdapter(): ScreenshotAdapter;
5
5
 
6
+ /**
7
+ * Minimal interface that `modern-screenshot` satisfies.
8
+ * The consumer must install `modern-screenshot` themselves:
9
+ *
10
+ * npm install modern-screenshot
11
+ *
12
+ * Then pass the module into this factory:
13
+ *
14
+ * ```ts
15
+ * import { createModernScreenshotAdapter } from "blocfeed/engine";
16
+ * import * as ms from "modern-screenshot";
17
+ *
18
+ * <BlocFeedWidget
19
+ * blocfeed_id="bf_..."
20
+ * config={{ capture: { adapter: createModernScreenshotAdapter(ms) } }}
21
+ * />
22
+ * ```
23
+ *
24
+ * Benefits over `html-to-image`:
25
+ * - Better cross-origin image handling
26
+ * - Supports CSS `backdrop-filter`, `clip-path`
27
+ * - Better web font rendering
28
+ */
29
+ interface ModernScreenshotModule {
30
+ domToPng: (node: Node, options?: Record<string, unknown>) => Promise<string>;
31
+ domToJpeg: (node: Node, options?: Record<string, unknown>) => Promise<string>;
32
+ }
33
+ declare function createModernScreenshotAdapter(mod: ModernScreenshotModule): ScreenshotAdapter;
34
+
6
35
  declare function runCapture(params: {
7
36
  selectionElement: Element | null;
8
37
  capture: CaptureConfig | undefined;
@@ -12,8 +41,27 @@ declare function runCapture(params: {
12
41
  declare function collectMetadata(params: {
13
42
  config: MetadataConfig | undefined;
14
43
  context: MetadataContext;
44
+ user?: BlocFeedUser;
15
45
  }): Promise<Record<string, unknown>>;
16
46
 
47
+ /**
48
+ * Add a failed payload to the offline queue.
49
+ * Screenshots are stripped to stay within localStorage limits.
50
+ */
51
+ declare function enqueue(payload: FeedbackPayload): void;
52
+ /**
53
+ * Remove and return all queued payloads.
54
+ */
55
+ declare function dequeueAll(): FeedbackPayload[];
56
+ /**
57
+ * Clear the entire queue.
58
+ */
59
+ declare function clearQueue(): void;
60
+ /**
61
+ * Get the number of items in the queue.
62
+ */
63
+ declare function getQueueSize(): number;
64
+
17
65
  declare function dataUrlToBlob(dataUrl: string): Blob;
18
66
 
19
- export { CaptureResult, ScreenshotAdapter, collectMetadata, createHtmlToImageAdapter, dataUrlToBlob, runCapture };
67
+ export { BlocFeedUser, CaptureResult, FeedbackPayload, type ModernScreenshotModule, ScreenshotAdapter, clearQueue, collectMetadata, createHtmlToImageAdapter, createModernScreenshotAdapter, dataUrlToBlob, dequeueAll, enqueue, getQueueSize, runCapture };
package/dist/engine.js CHANGED
@@ -1 +1 @@
1
- export{e as collectMetadata,f as createBlocFeedController,c as createHtmlToImageAdapter,d as runCapture}from'./chunk-37P3GL6V.js';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})}export{d as dataUrlToBlob};
1
+ import {a}from'./chunk-FMFONS5S.js';export{h as clearQueue,e as collectMetadata,j as createBlocFeedController,c as createHtmlToImageAdapter,g as dequeueAll,f as enqueue,i as getQueueSize,d as runCapture}from'./chunk-FMFONS5S.js';function c(o){if(o?.aborted)throw new Error("Aborted")}async function S(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 S(o);return {dataUrl:o,mime:t,width:e,height:r}}function x(o){return {async captureElement(t,e){if(!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(!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$1=Math.min(1,t.maxDimension/Math.max(r,n)),s={width:Math.max(1,Math.round(r*a$1)),height:Math.max(1,Math.round(n*a$1)),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 y(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})}export{x as createModernScreenshotAdapter,y as dataUrlToBlob};
package/dist/main.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- 'use strict';var chunkPEPIK3FN_cjs=require('./chunk-PEPIK3FN.cjs'),react=require('react'),jsxRuntime=require('react/jsx-runtime'),reactDom=require('react-dom');var g=react.createContext(null);function w(t){let e=react.useMemo(()=>chunkPEPIK3FN_cjs.f({...t.config??{},blocfeed_id:t.blocfeed_id}),[]),[a,l]=react.useState(()=>e.getState());return react.useEffect(()=>e.subscribe(l),[e]),react.useEffect(()=>e.setConfig({...t.config??{},blocfeed_id:t.blocfeed_id}),[e,t.config,t.blocfeed_id]),react.useEffect(()=>()=>e.destroy(),[e]),jsxRuntime.jsx(g.Provider,{value:{controller:e,state:a},children:t.children})}var H="blocfeed-styles-v1",D=`
2
+ 'use strict';var chunkJLPJP7DD_cjs=require('./chunk-JLPJP7DD.cjs'),react=require('react'),jsxRuntime=require('react/jsx-runtime'),reactDom=require('react-dom');var h=react.createContext(null);function k(t){let e=react.useMemo(()=>chunkJLPJP7DD_cjs.j({...t.config??{},blocfeed_id:t.blocfeed_id}),[]),[i,c]=react.useState(()=>e.getState());return react.useEffect(()=>e.subscribe(c),[e]),react.useEffect(()=>e.setConfig({...t.config??{},blocfeed_id:t.blocfeed_id}),[e,t.config,t.blocfeed_id]),react.useEffect(()=>()=>e.destroy(),[e]),jsxRuntime.jsx(h.Provider,{value:{controller:e,state:i},children:t.children})}var L="blocfeed-styles-v1",G=`
3
3
  :where([data-blocfeed-ui-root]),
4
4
  :where([data-blocfeed-ui-root]) * {
5
5
  box-sizing: border-box;
@@ -20,6 +20,10 @@
20
20
  color: var(--bf-panel-fg);
21
21
  }
22
22
 
23
+ /* ------------------------------------------------------------------ */
24
+ /* Trigger button \u2014 default bottom-right */
25
+ /* ------------------------------------------------------------------ */
26
+
23
27
  :where([data-blocfeed-ui-root]) .bf-trigger {
24
28
  position: fixed;
25
29
  right: 18px;
@@ -40,11 +44,26 @@
40
44
  -webkit-tap-highlight-color: transparent;
41
45
  }
42
46
 
47
+ :where([data-blocfeed-ui-root]) .bf-trigger:hover {
48
+ border-color: var(--bf-accent);
49
+ }
50
+
43
51
  :where([data-blocfeed-ui-root]) .bf-trigger:focus-visible {
44
52
  outline: 2px solid var(--bf-accent);
45
53
  outline-offset: 2px;
46
54
  }
47
55
 
56
+ /* Position variants (item 9) */
57
+ :where([data-blocfeed-ui-root]) .bf-trigger-bl {
58
+ left: 18px; bottom: 18px; right: auto; top: auto;
59
+ }
60
+ :where([data-blocfeed-ui-root]) .bf-trigger-tr {
61
+ right: 18px; top: 18px; bottom: auto; left: auto;
62
+ }
63
+ :where([data-blocfeed-ui-root]) .bf-trigger-tl {
64
+ left: 18px; top: 18px; right: auto; bottom: auto;
65
+ }
66
+
48
67
  :where([data-blocfeed-ui-root]) .bf-dot {
49
68
  width: 10px;
50
69
  height: 10px;
@@ -53,6 +72,10 @@
53
72
  box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.18);
54
73
  }
55
74
 
75
+ /* ------------------------------------------------------------------ */
76
+ /* Overlay, blocker, highlight */
77
+ /* ------------------------------------------------------------------ */
78
+
56
79
  :where([data-blocfeed-ui-root]) .bf-overlay {
57
80
  position: fixed;
58
81
  inset: 0;
@@ -75,6 +98,10 @@
75
98
  pointer-events: none;
76
99
  }
77
100
 
101
+ /* ------------------------------------------------------------------ */
102
+ /* Hint bar */
103
+ /* ------------------------------------------------------------------ */
104
+
78
105
  :where([data-blocfeed-ui-root]) .bf-hint {
79
106
  position: fixed;
80
107
  top: 16px;
@@ -99,6 +126,10 @@
99
126
  color: var(--bf-muted);
100
127
  }
101
128
 
129
+ /* ------------------------------------------------------------------ */
130
+ /* Buttons */
131
+ /* ------------------------------------------------------------------ */
132
+
102
133
  :where([data-blocfeed-ui-root]) .bf-btn {
103
134
  border: 1px solid var(--bf-border);
104
135
  background: transparent;
@@ -109,6 +140,11 @@
109
140
  user-select: none;
110
141
  }
111
142
 
143
+ :where([data-blocfeed-ui-root]) .bf-btn:focus-visible {
144
+ outline: 2px solid var(--bf-accent);
145
+ outline-offset: 2px;
146
+ }
147
+
112
148
  :where([data-blocfeed-ui-root]) .bf-btn[disabled] {
113
149
  opacity: 0.6;
114
150
  cursor: not-allowed;
@@ -120,6 +156,10 @@
120
156
  color: white;
121
157
  }
122
158
 
159
+ /* ------------------------------------------------------------------ */
160
+ /* Panel */
161
+ /* ------------------------------------------------------------------ */
162
+
123
163
  :where([data-blocfeed-ui-root]) .bf-panel {
124
164
  position: fixed;
125
165
  width: 360px;
@@ -154,6 +194,10 @@
154
194
  gap: 10px;
155
195
  }
156
196
 
197
+ /* ------------------------------------------------------------------ */
198
+ /* Form elements */
199
+ /* ------------------------------------------------------------------ */
200
+
157
201
  :where([data-blocfeed-ui-root]) .bf-textarea {
158
202
  width: 100%;
159
203
  min-height: 96px;
@@ -195,9 +239,16 @@
195
239
  padding-top: 4px;
196
240
  }
197
241
 
242
+ /* ------------------------------------------------------------------ */
243
+ /* Status, errors, toast */
244
+ /* ------------------------------------------------------------------ */
245
+
198
246
  :where([data-blocfeed-ui-root]) .bf-status {
199
247
  font-size: 12px;
200
248
  color: var(--bf-muted);
249
+ display: flex;
250
+ align-items: center;
251
+ gap: 8px;
201
252
  }
202
253
 
203
254
  :where([data-blocfeed-ui-root]) .bf-error {
@@ -220,4 +271,38 @@
220
271
  font-size: 13px;
221
272
  color: rgba(243, 244, 246, 0.9);
222
273
  }
223
- `;function T(){if(!chunkPEPIK3FN_cjs.a()||document.getElementById(H))return;let t=document.createElement("style");t.id=H,t.textContent=D,document.head.appendChild(t);}function y(){let t=react.useContext(g);if(!t)throw new Error("useBlocFeed must be used within a <BlocFeedProvider />");return {state:t.state,controller:t.controller,start:t.controller.start,stop:t.controller.stop,clearSelection:t.controller.clearSelection,submit:t.controller.submit}}function X(t,e,a){return Math.max(e,Math.min(a,t))}function G(t,e){let l=window.innerWidth,r=window.innerHeight,s=X(t.x,12,Math.max(12,l-e-12)),p=t.y+t.height+12,m=Math.max(12,t.y-240);return {top:p+240<=r?p:m,left:s}}function V(t){let{rect:e}=t,a={left:`${e.x}px`,top:`${e.y}px`,width:`${Math.max(0,e.width)}px`,height:`${Math.max(0,e.height)}px`};return jsxRuntime.jsx("div",{className:"bf-highlight",style:a})}function Y(t){let{state:e,controller:a,start:l,stop:r,clearSelection:s,submit:p}=y(),[m,C]=react.useState(null),[h,F]=react.useState(""),[B,k]=react.useState(t.config.capture?.element??true),[S,P]=react.useState(t.config.capture?.fullPage??false),[A,N]=react.useState(null);react.useEffect(()=>a.subscribeHover(C),[a]),react.useEffect(()=>{let n=a.__unsafeGetSelectedElement();if(!n||e.phase==="idle"||e.phase==="picking"){N(null);return}let z=()=>{N(chunkPEPIK3FN_cjs.b(n.getBoundingClientRect()));};z();let u=()=>z();return window.addEventListener("scroll",u,{capture:true,passive:true}),window.addEventListener("resize",u,{passive:true}),()=>{window.removeEventListener("scroll",u,{capture:true}),window.removeEventListener("resize",u);}},[a,e.phase,e.selection?.selector]),react.useEffect(()=>{e.phase==="review"&&(F(""),k(t.config.capture?.element??true),P(t.config.capture?.fullPage??false));},[e.phase,e.selection?.selector,t.config.capture?.element,t.config.capture?.fullPage]),react.useEffect(()=>{if(e.phase!=="success")return;let n=window.setTimeout(()=>r(),1200);return ()=>window.clearTimeout(n)},[e.phase,r]);let d=e.phase==="capturing"||e.phase==="submitting",c=e.phase==="picking"?m?.rect??null:A??e.selection?.rect??null,x=react.useMemo(()=>c?G(c,360):null,[c?.x,c?.y,c?.width,c?.height]),R=e.lastError?.message;return jsxRuntime.jsxs(jsxRuntime.Fragment,{children:[e.phase==="idle"&&jsxRuntime.jsxs("button",{className:"bf-trigger",type:"button",onClick:()=>l(),"aria-label":"Give feedback",children:[jsxRuntime.jsx("span",{className:"bf-dot","aria-hidden":"true"}),"Feedback"]}),e.phase!=="idle"&&jsxRuntime.jsxs("div",{className:"bf-overlay",children:[e.phase!=="picking"&&jsxRuntime.jsx("div",{className:"bf-blocker",role:"presentation",onClick:()=>r()}),c&&jsxRuntime.jsx(V,{rect:c}),e.phase==="picking"&&jsxRuntime.jsxs("div",{className:"bf-hint",children:[jsxRuntime.jsxs("p",{children:["Click an element to attach your feedback. Press ",jsxRuntime.jsx("strong",{children:"Esc"})," to cancel."]}),jsxRuntime.jsx("button",{type:"button",className:"bf-btn",onClick:()=>r(),children:"Cancel"})]}),(e.phase==="review"||e.phase==="capturing"||e.phase==="submitting"||e.phase==="error"||e.phase==="success")&&x&&jsxRuntime.jsxs("div",{className:"bf-panel",style:{left:x.left,top:x.top},children:[jsxRuntime.jsxs("div",{className:"bf-panelHeader",children:[jsxRuntime.jsx("div",{className:"bf-title",children:"Feedback"}),jsxRuntime.jsxs("div",{className:"bf-row",style:{justifyContent:"flex-end"},children:[jsxRuntime.jsx("button",{type:"button",className:"bf-btn",onClick:()=>s(),disabled:d,children:"Re-pick"}),jsxRuntime.jsx("button",{type:"button",className:"bf-btn",onClick:()=>r(),disabled:d,children:"Close"})]})]}),jsxRuntime.jsxs("div",{className:"bf-panelBody",children:[jsxRuntime.jsx("textarea",{className:"bf-textarea",placeholder:"What\u2019s happening? What did you expect?",value:h,onChange:n=>F(n.target.value),disabled:d}),jsxRuntime.jsxs("div",{className:"bf-row",children:[jsxRuntime.jsxs("label",{children:[jsxRuntime.jsx("input",{type:"checkbox",checked:B,onChange:n=>k(n.target.checked),disabled:d}),"Screenshot element"]}),jsxRuntime.jsxs("label",{children:[jsxRuntime.jsx("input",{type:"checkbox",checked:S,onChange:n=>P(n.target.checked),disabled:d}),"Full page"]})]}),e.phase==="capturing"&&jsxRuntime.jsx("div",{className:"bf-status",children:"Capturing screenshots\u2026"}),e.phase==="submitting"&&jsxRuntime.jsx("div",{className:"bf-status",children:"Submitting\u2026"}),e.phase==="success"&&jsxRuntime.jsx("div",{className:"bf-status",children:"Sent. Thank you!"}),e.phase==="error"&&R&&jsxRuntime.jsx("div",{className:"bf-error",children:R}),jsxRuntime.jsxs("div",{className:"bf-actions",children:[jsxRuntime.jsx("button",{type:"button",className:"bf-btn",onClick:()=>r(),disabled:d,children:"Cancel"}),jsxRuntime.jsx("button",{type:"button",className:"bf-btn bf-btnPrimary",onClick:()=>p(h,{capture:{element:B,fullPage:S}}),disabled:d||h.trim().length===0,children:"Send"})]})]})]})]}),e.phase==="success"&&jsxRuntime.jsx("div",{className:"bf-toast","aria-live":"polite",children:"Feedback sent"})]})}function q(t){let e={...t.config??{},blocfeed_id:t.blocfeed_id},[a,l]=react.useState(null);return react.useEffect(()=>{T();let r=document.createElement("div");r.setAttribute("data-blocfeed-ui-root","true"),r.setAttribute("data-blocfeed-ui","true");let s=e.ui?.zIndex;return typeof s=="number"&&r.style.setProperty("--bf-z",String(s)),document.body.appendChild(r),l(r),()=>{r.remove(),l(null);}},[e.ui?.zIndex]),a?reactDom.createPortal(jsxRuntime.jsx(w,{blocfeed_id:e.blocfeed_id,...t.config?{config:t.config}:{},children:jsxRuntime.jsx(Y,{config:e})}),a):null}exports.BlocFeedProvider=w;exports.BlocFeedWidget=q;exports.useBlocFeed=y;
274
+
275
+ /* ------------------------------------------------------------------ */
276
+ /* Capture spinner (item 12) */
277
+ /* ------------------------------------------------------------------ */
278
+
279
+ :where([data-blocfeed-ui-root]) .bf-spinner {
280
+ display: inline-block;
281
+ width: 10px;
282
+ height: 10px;
283
+ border-radius: 50%;
284
+ background: var(--bf-accent);
285
+ animation: bf-pulse 1s ease-in-out infinite;
286
+ }
287
+
288
+ @keyframes bf-pulse {
289
+ 0%, 100% { opacity: 0.4; transform: scale(0.8); }
290
+ 50% { opacity: 1; transform: scale(1.2); }
291
+ }
292
+
293
+ /* ------------------------------------------------------------------ */
294
+ /* Screen reader only */
295
+ /* ------------------------------------------------------------------ */
296
+
297
+ :where([data-blocfeed-ui-root]) .bf-sr-only {
298
+ position: absolute;
299
+ width: 1px;
300
+ height: 1px;
301
+ padding: 0;
302
+ margin: -1px;
303
+ overflow: hidden;
304
+ clip: rect(0, 0, 0, 0);
305
+ white-space: nowrap;
306
+ border: 0;
307
+ }
308
+ `;function W(){if(!chunkJLPJP7DD_cjs.a()||document.getElementById(L))return;let t=document.createElement("style");t.id=L,t.textContent=G,document.head.appendChild(t);}function B(){let t=react.useContext(h);if(!t)throw new Error("useBlocFeed must be used within a <BlocFeedProvider />");return {state:t.state,controller:t.controller,start:t.controller.start,stop:t.controller.stop,clearSelection:t.controller.clearSelection,submit:t.controller.submit}}function I(t,e,i){return Math.max(e,Math.min(i,t))}function Z(t,e,i="bottom-right"){let o=window.innerWidth,s=window.innerHeight,a;a=I(t.x,12,Math.max(12,o-e-12));let d=t.y+t.height+12,x=Math.max(12,t.y-240);return {top:d+240<=s?d:x,left:a}}function ee(t){switch(t){case "bottom-left":return "bf-trigger bf-trigger-bl";case "top-right":return "bf-trigger bf-trigger-tr";case "top-left":return "bf-trigger bf-trigger-tl";default:return "bf-trigger"}}function te(t){let{rect:e}=t,i={left:`${e.x}px`,top:`${e.y}px`,width:`${Math.max(0,e.width)}px`,height:`${Math.max(0,e.height)}px`};return jsxRuntime.jsx("div",{className:"bf-highlight",style:i,"aria-hidden":"true"})}function oe(t){let e=react.useRef(null);return react.useEffect(()=>{if(!t||!e.current)return;e.current.querySelector(".bf-textarea")?.focus();let c=o=>{if(o.key!=="Tab"||!e.current)return;let s=e.current.querySelectorAll('button:not([disabled]), textarea:not([disabled]), input:not([disabled]), [tabindex]:not([tabindex="-1"])');if(s.length===0)return;let a=s[0],d=s[s.length-1];o.shiftKey&&document.activeElement===a?(o.preventDefault(),d.focus()):!o.shiftKey&&document.activeElement===d&&(o.preventDefault(),a.focus());};return document.addEventListener("keydown",c,{capture:true}),()=>document.removeEventListener("keydown",c,{capture:true})},[t]),e}function re(t){let{state:e,controller:i,start:c,stop:o,clearSelection:s,submit:a}=B(),d=t.config.ui?.position,[x,S]=react.useState(null),[p,P]=react.useState(""),[v,E]=react.useState(t.config.capture?.element??true),[y,R]=react.useState(t.config.capture?.fullPage??false),[K,N]=react.useState(null),z=e.phase==="review"||e.phase==="capturing"||e.phase==="submitting"||e.phase==="error"||e.phase==="success",j=oe(z);react.useEffect(()=>i.subscribeHover(S),[i]),react.useEffect(()=>{let n=i.__unsafeGetSelectedElement();if(!n||e.phase==="idle"||e.phase==="picking"){N(null);return}let M=()=>{N(chunkJLPJP7DD_cjs.b(n.getBoundingClientRect()));};M();let m=()=>M();return window.addEventListener("scroll",m,{capture:true,passive:true}),window.addEventListener("resize",m,{passive:true}),()=>{window.removeEventListener("scroll",m,{capture:true}),window.removeEventListener("resize",m);}},[i,e.phase,e.selection?.selector]),react.useEffect(()=>{e.phase==="review"&&(P(""),E(t.config.capture?.element??true),R(t.config.capture?.fullPage??false));},[e.phase,e.selection?.selector,t.config.capture?.element,t.config.capture?.fullPage]),react.useEffect(()=>{if(e.phase!=="success")return;let n=window.setTimeout(()=>o(),1200);return ()=>window.clearTimeout(n)},[e.phase,o]);let f=e.phase==="capturing"||e.phase==="submitting",b=e.phase==="picking"?x?.rect??null:K??e.selection?.rect??null,w=react.useMemo(()=>b?Z(b,360,d):null,[b?.x,b?.y,b?.width,b?.height,d]),T=e.lastError?.message,C=react.useCallback(()=>{a(p,{capture:{element:v,fullPage:y}});},[a,p,v,y]),O=react.useCallback(n=>{(n.metaKey||n.ctrlKey)&&n.key==="Enter"&&p.trim().length>0&&!f&&(n.preventDefault(),C());},[C,p,f]);return jsxRuntime.jsxs(jsxRuntime.Fragment,{children:[e.phase==="idle"&&jsxRuntime.jsxs("button",{className:ee(d),type:"button",onClick:()=>c(),"aria-label":"Give feedback",children:[jsxRuntime.jsx("span",{className:"bf-dot","aria-hidden":"true"}),"Feedback"]}),e.phase!=="idle"&&jsxRuntime.jsxs("div",{className:"bf-overlay",role:"presentation",children:[e.phase!=="picking"&&jsxRuntime.jsx("div",{className:"bf-blocker",role:"presentation",onClick:()=>o()}),b&&jsxRuntime.jsx(te,{rect:b}),e.phase==="picking"&&jsxRuntime.jsxs("div",{className:"bf-hint",role:"status","aria-live":"polite",children:[jsxRuntime.jsxs("p",{id:"bf-hint-text",children:["Click an element to attach your feedback. Press ",jsxRuntime.jsx("strong",{children:"Esc"})," to cancel."]}),jsxRuntime.jsx("button",{type:"button",className:"bf-btn",onClick:()=>o(),"aria-label":"Cancel element selection",children:"Cancel"})]}),z&&w&&jsxRuntime.jsxs("div",{ref:j,className:"bf-panel",style:{left:w.left,top:w.top},role:"dialog","aria-modal":"true","aria-label":"Feedback form",children:[jsxRuntime.jsxs("div",{className:"bf-panelHeader",children:[jsxRuntime.jsx("div",{className:"bf-title",id:"bf-panel-title",children:"Feedback"}),jsxRuntime.jsxs("div",{className:"bf-row",style:{justifyContent:"flex-end"},children:[jsxRuntime.jsx("button",{type:"button",className:"bf-btn",onClick:()=>s(),disabled:f,"aria-label":"Re-pick element",children:"Re-pick"}),jsxRuntime.jsx("button",{type:"button",className:"bf-btn",onClick:()=>o(),disabled:f,"aria-label":"Close feedback form",children:"Close"})]})]}),jsxRuntime.jsxs("div",{className:"bf-panelBody",children:[jsxRuntime.jsx("textarea",{className:"bf-textarea",placeholder:"What's happening? What did you expect?",value:p,onChange:n=>P(n.target.value),onKeyDown:O,disabled:f,"aria-label":"Feedback message"}),jsxRuntime.jsxs("div",{className:"bf-row",role:"group","aria-label":"Screenshot options",children:[jsxRuntime.jsxs("label",{children:[jsxRuntime.jsx("input",{type:"checkbox",checked:v,onChange:n=>E(n.target.checked),disabled:f}),"Screenshot element"]}),jsxRuntime.jsxs("label",{children:[jsxRuntime.jsx("input",{type:"checkbox",checked:y,onChange:n=>R(n.target.checked),disabled:f}),"Full page"]})]}),e.phase==="capturing"&&jsxRuntime.jsxs("div",{className:"bf-status",role:"status","aria-live":"polite",children:[jsxRuntime.jsx("span",{className:"bf-spinner","aria-hidden":"true"}),"Capturing screenshots\u2026"]}),e.phase==="submitting"&&jsxRuntime.jsxs("div",{className:"bf-status",role:"status","aria-live":"polite",children:[jsxRuntime.jsx("span",{className:"bf-spinner","aria-hidden":"true"}),"Submitting\u2026"]}),e.phase==="success"&&jsxRuntime.jsx("div",{className:"bf-status",role:"status","aria-live":"polite",children:"Sent. Thank you!"}),e.phase==="error"&&T&&jsxRuntime.jsx("div",{className:"bf-error",role:"alert",children:T}),jsxRuntime.jsxs("div",{className:"bf-actions",children:[jsxRuntime.jsx("button",{type:"button",className:"bf-btn",onClick:()=>o(),disabled:f,"aria-label":"Cancel feedback",children:"Cancel"}),jsxRuntime.jsx("button",{type:"button",className:"bf-btn bf-btnPrimary",onClick:C,disabled:f||p.trim().length===0,"aria-label":"Send feedback",children:"Send"})]})]})]})]}),e.phase==="success"&&jsxRuntime.jsx("div",{className:"bf-toast",role:"status","aria-live":"polite",children:"Feedback sent"})]})}function ae(t){let e={...t.config??{},blocfeed_id:t.blocfeed_id},[i,c]=react.useState(null);return react.useEffect(()=>{W();let o=document.createElement("div");o.setAttribute("data-blocfeed-ui-root","true"),o.setAttribute("data-blocfeed-ui","true");let s=e.ui?.zIndex;typeof s=="number"&&o.style.setProperty("--bf-z",String(s));let a=e.ui?.theme;return a&&(a.accentColor&&o.style.setProperty("--bf-accent",a.accentColor),a.panelBackground&&o.style.setProperty("--bf-panel-bg",a.panelBackground),a.panelForeground&&o.style.setProperty("--bf-panel-fg",a.panelForeground),a.fontFamily&&o.style.setProperty("--bf-font",a.fontFamily)),document.body.appendChild(o),c(o),()=>{o.remove(),c(null);}},[e.ui?.zIndex,e.ui?.theme?.accentColor,e.ui?.theme?.panelBackground,e.ui?.theme?.panelForeground,e.ui?.theme?.fontFamily]),i?reactDom.createPortal(jsxRuntime.jsx(k,{blocfeed_id:e.blocfeed_id,...t.config?{config:t.config}:{},children:jsxRuntime.jsx(re,{config:e})}),i):null}exports.BlocFeedProvider=k;exports.BlocFeedWidget=ae;exports.useBlocFeed=B;
package/dist/main.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { B as BlocFeedConfig, a as BlocFeedState, b as BlocFeedController, C as CaptureConfig, S as SubmitResult } from './controller-DmWFVHoj.cjs';
2
- export { c as BlocFeedError, d as CaptureDiagnostics, e as CaptureResult, E as ElementDescriptor, F as FeedbackPayload, I as ImageAsset, M as MaybePromise, f as MetadataConfig, g as MetadataContext, P as PickerConfig, R as Rect, h as ScreenshotAdapter, i as ScreenshotAdapterOptions, j as ScreenshotMime, k as SessionPhase, T as TransportResult } from './controller-DmWFVHoj.cjs';
1
+ import { B as BlocFeedConfig, a as BlocFeedState, b as BlocFeedController, C as CaptureConfig, S as SubmitResult } from './controller-olqOZLhp.cjs';
2
+ export { c as BlocFeedError, d as BlocFeedUser, e as CaptureDiagnostics, f as CaptureResult, E as ElementDescriptor, F as FeedbackApiResponse, g as FeedbackPayload, I as ImageAsset, M as MaybePromise, h as MetadataConfig, i as MetadataContext, P as PickerConfig, R as Rect, j as ScreenshotAdapter, k as ScreenshotAdapterOptions, l as ScreenshotIntent, m as ScreenshotMime, n as SessionPhase, T as ThemeConfig, o as TransportConfig, p as TransportResult, W as WidgetPosition } from './controller-olqOZLhp.cjs';
3
3
  import * as react_jsx_runtime from 'react/jsx-runtime';
4
4
  import * as react from 'react';
5
5
  import { ReactNode } from 'react';
package/dist/main.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { B as BlocFeedConfig, a as BlocFeedState, b as BlocFeedController, C as CaptureConfig, S as SubmitResult } from './controller-DmWFVHoj.js';
2
- export { c as BlocFeedError, d as CaptureDiagnostics, e as CaptureResult, E as ElementDescriptor, F as FeedbackPayload, I as ImageAsset, M as MaybePromise, f as MetadataConfig, g as MetadataContext, P as PickerConfig, R as Rect, h as ScreenshotAdapter, i as ScreenshotAdapterOptions, j as ScreenshotMime, k as SessionPhase, T as TransportResult } from './controller-DmWFVHoj.js';
1
+ import { B as BlocFeedConfig, a as BlocFeedState, b as BlocFeedController, C as CaptureConfig, S as SubmitResult } from './controller-olqOZLhp.js';
2
+ export { c as BlocFeedError, d as BlocFeedUser, e as CaptureDiagnostics, f as CaptureResult, E as ElementDescriptor, F as FeedbackApiResponse, g as FeedbackPayload, I as ImageAsset, M as MaybePromise, h as MetadataConfig, i as MetadataContext, P as PickerConfig, R as Rect, j as ScreenshotAdapter, k as ScreenshotAdapterOptions, l as ScreenshotIntent, m as ScreenshotMime, n as SessionPhase, T as ThemeConfig, o as TransportConfig, p as TransportResult, W as WidgetPosition } from './controller-olqOZLhp.js';
3
3
  import * as react_jsx_runtime from 'react/jsx-runtime';
4
4
  import * as react from 'react';
5
5
  import { ReactNode } from 'react';
package/dist/main.js CHANGED
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import {f,a,b}from'./chunk-37P3GL6V.js';import {createContext,useMemo,useState,useEffect,useContext}from'react';import {jsx,jsxs,Fragment}from'react/jsx-runtime';import {createPortal}from'react-dom';var g=createContext(null);function w(t){let e=useMemo(()=>f({...t.config??{},blocfeed_id:t.blocfeed_id}),[]),[a,l]=useState(()=>e.getState());return useEffect(()=>e.subscribe(l),[e]),useEffect(()=>e.setConfig({...t.config??{},blocfeed_id:t.blocfeed_id}),[e,t.config,t.blocfeed_id]),useEffect(()=>()=>e.destroy(),[e]),jsx(g.Provider,{value:{controller:e,state:a},children:t.children})}var H="blocfeed-styles-v1",D=`
2
+ import {j,a,b}from'./chunk-FMFONS5S.js';import {createContext,useMemo,useState,useEffect,useContext,useCallback,useRef}from'react';import {jsx,jsxs,Fragment}from'react/jsx-runtime';import {createPortal}from'react-dom';var h=createContext(null);function k(t){let e=useMemo(()=>j({...t.config??{},blocfeed_id:t.blocfeed_id}),[]),[i,c]=useState(()=>e.getState());return useEffect(()=>e.subscribe(c),[e]),useEffect(()=>e.setConfig({...t.config??{},blocfeed_id:t.blocfeed_id}),[e,t.config,t.blocfeed_id]),useEffect(()=>()=>e.destroy(),[e]),jsx(h.Provider,{value:{controller:e,state:i},children:t.children})}var L="blocfeed-styles-v1",G=`
3
3
  :where([data-blocfeed-ui-root]),
4
4
  :where([data-blocfeed-ui-root]) * {
5
5
  box-sizing: border-box;
@@ -20,6 +20,10 @@ import {f,a,b}from'./chunk-37P3GL6V.js';import {createContext,useMemo,useState,u
20
20
  color: var(--bf-panel-fg);
21
21
  }
22
22
 
23
+ /* ------------------------------------------------------------------ */
24
+ /* Trigger button \u2014 default bottom-right */
25
+ /* ------------------------------------------------------------------ */
26
+
23
27
  :where([data-blocfeed-ui-root]) .bf-trigger {
24
28
  position: fixed;
25
29
  right: 18px;
@@ -40,11 +44,26 @@ import {f,a,b}from'./chunk-37P3GL6V.js';import {createContext,useMemo,useState,u
40
44
  -webkit-tap-highlight-color: transparent;
41
45
  }
42
46
 
47
+ :where([data-blocfeed-ui-root]) .bf-trigger:hover {
48
+ border-color: var(--bf-accent);
49
+ }
50
+
43
51
  :where([data-blocfeed-ui-root]) .bf-trigger:focus-visible {
44
52
  outline: 2px solid var(--bf-accent);
45
53
  outline-offset: 2px;
46
54
  }
47
55
 
56
+ /* Position variants (item 9) */
57
+ :where([data-blocfeed-ui-root]) .bf-trigger-bl {
58
+ left: 18px; bottom: 18px; right: auto; top: auto;
59
+ }
60
+ :where([data-blocfeed-ui-root]) .bf-trigger-tr {
61
+ right: 18px; top: 18px; bottom: auto; left: auto;
62
+ }
63
+ :where([data-blocfeed-ui-root]) .bf-trigger-tl {
64
+ left: 18px; top: 18px; right: auto; bottom: auto;
65
+ }
66
+
48
67
  :where([data-blocfeed-ui-root]) .bf-dot {
49
68
  width: 10px;
50
69
  height: 10px;
@@ -53,6 +72,10 @@ import {f,a,b}from'./chunk-37P3GL6V.js';import {createContext,useMemo,useState,u
53
72
  box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.18);
54
73
  }
55
74
 
75
+ /* ------------------------------------------------------------------ */
76
+ /* Overlay, blocker, highlight */
77
+ /* ------------------------------------------------------------------ */
78
+
56
79
  :where([data-blocfeed-ui-root]) .bf-overlay {
57
80
  position: fixed;
58
81
  inset: 0;
@@ -75,6 +98,10 @@ import {f,a,b}from'./chunk-37P3GL6V.js';import {createContext,useMemo,useState,u
75
98
  pointer-events: none;
76
99
  }
77
100
 
101
+ /* ------------------------------------------------------------------ */
102
+ /* Hint bar */
103
+ /* ------------------------------------------------------------------ */
104
+
78
105
  :where([data-blocfeed-ui-root]) .bf-hint {
79
106
  position: fixed;
80
107
  top: 16px;
@@ -99,6 +126,10 @@ import {f,a,b}from'./chunk-37P3GL6V.js';import {createContext,useMemo,useState,u
99
126
  color: var(--bf-muted);
100
127
  }
101
128
 
129
+ /* ------------------------------------------------------------------ */
130
+ /* Buttons */
131
+ /* ------------------------------------------------------------------ */
132
+
102
133
  :where([data-blocfeed-ui-root]) .bf-btn {
103
134
  border: 1px solid var(--bf-border);
104
135
  background: transparent;
@@ -109,6 +140,11 @@ import {f,a,b}from'./chunk-37P3GL6V.js';import {createContext,useMemo,useState,u
109
140
  user-select: none;
110
141
  }
111
142
 
143
+ :where([data-blocfeed-ui-root]) .bf-btn:focus-visible {
144
+ outline: 2px solid var(--bf-accent);
145
+ outline-offset: 2px;
146
+ }
147
+
112
148
  :where([data-blocfeed-ui-root]) .bf-btn[disabled] {
113
149
  opacity: 0.6;
114
150
  cursor: not-allowed;
@@ -120,6 +156,10 @@ import {f,a,b}from'./chunk-37P3GL6V.js';import {createContext,useMemo,useState,u
120
156
  color: white;
121
157
  }
122
158
 
159
+ /* ------------------------------------------------------------------ */
160
+ /* Panel */
161
+ /* ------------------------------------------------------------------ */
162
+
123
163
  :where([data-blocfeed-ui-root]) .bf-panel {
124
164
  position: fixed;
125
165
  width: 360px;
@@ -154,6 +194,10 @@ import {f,a,b}from'./chunk-37P3GL6V.js';import {createContext,useMemo,useState,u
154
194
  gap: 10px;
155
195
  }
156
196
 
197
+ /* ------------------------------------------------------------------ */
198
+ /* Form elements */
199
+ /* ------------------------------------------------------------------ */
200
+
157
201
  :where([data-blocfeed-ui-root]) .bf-textarea {
158
202
  width: 100%;
159
203
  min-height: 96px;
@@ -195,9 +239,16 @@ import {f,a,b}from'./chunk-37P3GL6V.js';import {createContext,useMemo,useState,u
195
239
  padding-top: 4px;
196
240
  }
197
241
 
242
+ /* ------------------------------------------------------------------ */
243
+ /* Status, errors, toast */
244
+ /* ------------------------------------------------------------------ */
245
+
198
246
  :where([data-blocfeed-ui-root]) .bf-status {
199
247
  font-size: 12px;
200
248
  color: var(--bf-muted);
249
+ display: flex;
250
+ align-items: center;
251
+ gap: 8px;
201
252
  }
202
253
 
203
254
  :where([data-blocfeed-ui-root]) .bf-error {
@@ -220,4 +271,38 @@ import {f,a,b}from'./chunk-37P3GL6V.js';import {createContext,useMemo,useState,u
220
271
  font-size: 13px;
221
272
  color: rgba(243, 244, 246, 0.9);
222
273
  }
223
- `;function T(){if(!a()||document.getElementById(H))return;let t=document.createElement("style");t.id=H,t.textContent=D,document.head.appendChild(t);}function y(){let t=useContext(g);if(!t)throw new Error("useBlocFeed must be used within a <BlocFeedProvider />");return {state:t.state,controller:t.controller,start:t.controller.start,stop:t.controller.stop,clearSelection:t.controller.clearSelection,submit:t.controller.submit}}function X(t,e,a){return Math.max(e,Math.min(a,t))}function G(t,e){let l=window.innerWidth,r=window.innerHeight,s=X(t.x,12,Math.max(12,l-e-12)),p=t.y+t.height+12,m=Math.max(12,t.y-240);return {top:p+240<=r?p:m,left:s}}function V(t){let{rect:e}=t,a={left:`${e.x}px`,top:`${e.y}px`,width:`${Math.max(0,e.width)}px`,height:`${Math.max(0,e.height)}px`};return jsx("div",{className:"bf-highlight",style:a})}function Y(t){let{state:e,controller:a,start:l,stop:r,clearSelection:s,submit:p}=y(),[m,C]=useState(null),[h,F]=useState(""),[B,k]=useState(t.config.capture?.element??true),[S,P]=useState(t.config.capture?.fullPage??false),[A,N]=useState(null);useEffect(()=>a.subscribeHover(C),[a]),useEffect(()=>{let n=a.__unsafeGetSelectedElement();if(!n||e.phase==="idle"||e.phase==="picking"){N(null);return}let z=()=>{N(b(n.getBoundingClientRect()));};z();let u=()=>z();return window.addEventListener("scroll",u,{capture:true,passive:true}),window.addEventListener("resize",u,{passive:true}),()=>{window.removeEventListener("scroll",u,{capture:true}),window.removeEventListener("resize",u);}},[a,e.phase,e.selection?.selector]),useEffect(()=>{e.phase==="review"&&(F(""),k(t.config.capture?.element??true),P(t.config.capture?.fullPage??false));},[e.phase,e.selection?.selector,t.config.capture?.element,t.config.capture?.fullPage]),useEffect(()=>{if(e.phase!=="success")return;let n=window.setTimeout(()=>r(),1200);return ()=>window.clearTimeout(n)},[e.phase,r]);let d=e.phase==="capturing"||e.phase==="submitting",c=e.phase==="picking"?m?.rect??null:A??e.selection?.rect??null,x=useMemo(()=>c?G(c,360):null,[c?.x,c?.y,c?.width,c?.height]),R=e.lastError?.message;return jsxs(Fragment,{children:[e.phase==="idle"&&jsxs("button",{className:"bf-trigger",type:"button",onClick:()=>l(),"aria-label":"Give feedback",children:[jsx("span",{className:"bf-dot","aria-hidden":"true"}),"Feedback"]}),e.phase!=="idle"&&jsxs("div",{className:"bf-overlay",children:[e.phase!=="picking"&&jsx("div",{className:"bf-blocker",role:"presentation",onClick:()=>r()}),c&&jsx(V,{rect:c}),e.phase==="picking"&&jsxs("div",{className:"bf-hint",children:[jsxs("p",{children:["Click an element to attach your feedback. Press ",jsx("strong",{children:"Esc"})," to cancel."]}),jsx("button",{type:"button",className:"bf-btn",onClick:()=>r(),children:"Cancel"})]}),(e.phase==="review"||e.phase==="capturing"||e.phase==="submitting"||e.phase==="error"||e.phase==="success")&&x&&jsxs("div",{className:"bf-panel",style:{left:x.left,top:x.top},children:[jsxs("div",{className:"bf-panelHeader",children:[jsx("div",{className:"bf-title",children:"Feedback"}),jsxs("div",{className:"bf-row",style:{justifyContent:"flex-end"},children:[jsx("button",{type:"button",className:"bf-btn",onClick:()=>s(),disabled:d,children:"Re-pick"}),jsx("button",{type:"button",className:"bf-btn",onClick:()=>r(),disabled:d,children:"Close"})]})]}),jsxs("div",{className:"bf-panelBody",children:[jsx("textarea",{className:"bf-textarea",placeholder:"What\u2019s happening? What did you expect?",value:h,onChange:n=>F(n.target.value),disabled:d}),jsxs("div",{className:"bf-row",children:[jsxs("label",{children:[jsx("input",{type:"checkbox",checked:B,onChange:n=>k(n.target.checked),disabled:d}),"Screenshot element"]}),jsxs("label",{children:[jsx("input",{type:"checkbox",checked:S,onChange:n=>P(n.target.checked),disabled:d}),"Full page"]})]}),e.phase==="capturing"&&jsx("div",{className:"bf-status",children:"Capturing screenshots\u2026"}),e.phase==="submitting"&&jsx("div",{className:"bf-status",children:"Submitting\u2026"}),e.phase==="success"&&jsx("div",{className:"bf-status",children:"Sent. Thank you!"}),e.phase==="error"&&R&&jsx("div",{className:"bf-error",children:R}),jsxs("div",{className:"bf-actions",children:[jsx("button",{type:"button",className:"bf-btn",onClick:()=>r(),disabled:d,children:"Cancel"}),jsx("button",{type:"button",className:"bf-btn bf-btnPrimary",onClick:()=>p(h,{capture:{element:B,fullPage:S}}),disabled:d||h.trim().length===0,children:"Send"})]})]})]})]}),e.phase==="success"&&jsx("div",{className:"bf-toast","aria-live":"polite",children:"Feedback sent"})]})}function q(t){let e={...t.config??{},blocfeed_id:t.blocfeed_id},[a,l]=useState(null);return useEffect(()=>{T();let r=document.createElement("div");r.setAttribute("data-blocfeed-ui-root","true"),r.setAttribute("data-blocfeed-ui","true");let s=e.ui?.zIndex;return typeof s=="number"&&r.style.setProperty("--bf-z",String(s)),document.body.appendChild(r),l(r),()=>{r.remove(),l(null);}},[e.ui?.zIndex]),a?createPortal(jsx(w,{blocfeed_id:e.blocfeed_id,...t.config?{config:t.config}:{},children:jsx(Y,{config:e})}),a):null}export{w as BlocFeedProvider,q as BlocFeedWidget,y as useBlocFeed};
274
+
275
+ /* ------------------------------------------------------------------ */
276
+ /* Capture spinner (item 12) */
277
+ /* ------------------------------------------------------------------ */
278
+
279
+ :where([data-blocfeed-ui-root]) .bf-spinner {
280
+ display: inline-block;
281
+ width: 10px;
282
+ height: 10px;
283
+ border-radius: 50%;
284
+ background: var(--bf-accent);
285
+ animation: bf-pulse 1s ease-in-out infinite;
286
+ }
287
+
288
+ @keyframes bf-pulse {
289
+ 0%, 100% { opacity: 0.4; transform: scale(0.8); }
290
+ 50% { opacity: 1; transform: scale(1.2); }
291
+ }
292
+
293
+ /* ------------------------------------------------------------------ */
294
+ /* Screen reader only */
295
+ /* ------------------------------------------------------------------ */
296
+
297
+ :where([data-blocfeed-ui-root]) .bf-sr-only {
298
+ position: absolute;
299
+ width: 1px;
300
+ height: 1px;
301
+ padding: 0;
302
+ margin: -1px;
303
+ overflow: hidden;
304
+ clip: rect(0, 0, 0, 0);
305
+ white-space: nowrap;
306
+ border: 0;
307
+ }
308
+ `;function W(){if(!a()||document.getElementById(L))return;let t=document.createElement("style");t.id=L,t.textContent=G,document.head.appendChild(t);}function B(){let t=useContext(h);if(!t)throw new Error("useBlocFeed must be used within a <BlocFeedProvider />");return {state:t.state,controller:t.controller,start:t.controller.start,stop:t.controller.stop,clearSelection:t.controller.clearSelection,submit:t.controller.submit}}function I(t,e,i){return Math.max(e,Math.min(i,t))}function Z(t,e,i="bottom-right"){let o=window.innerWidth,s=window.innerHeight,a;a=I(t.x,12,Math.max(12,o-e-12));let d=t.y+t.height+12,x=Math.max(12,t.y-240);return {top:d+240<=s?d:x,left:a}}function ee(t){switch(t){case "bottom-left":return "bf-trigger bf-trigger-bl";case "top-right":return "bf-trigger bf-trigger-tr";case "top-left":return "bf-trigger bf-trigger-tl";default:return "bf-trigger"}}function te(t){let{rect:e}=t,i={left:`${e.x}px`,top:`${e.y}px`,width:`${Math.max(0,e.width)}px`,height:`${Math.max(0,e.height)}px`};return jsx("div",{className:"bf-highlight",style:i,"aria-hidden":"true"})}function oe(t){let e=useRef(null);return useEffect(()=>{if(!t||!e.current)return;e.current.querySelector(".bf-textarea")?.focus();let c=o=>{if(o.key!=="Tab"||!e.current)return;let s=e.current.querySelectorAll('button:not([disabled]), textarea:not([disabled]), input:not([disabled]), [tabindex]:not([tabindex="-1"])');if(s.length===0)return;let a=s[0],d=s[s.length-1];o.shiftKey&&document.activeElement===a?(o.preventDefault(),d.focus()):!o.shiftKey&&document.activeElement===d&&(o.preventDefault(),a.focus());};return document.addEventListener("keydown",c,{capture:true}),()=>document.removeEventListener("keydown",c,{capture:true})},[t]),e}function re(t){let{state:e,controller:i,start:c,stop:o,clearSelection:s,submit:a}=B(),d=t.config.ui?.position,[x,S]=useState(null),[p,P]=useState(""),[v,E]=useState(t.config.capture?.element??true),[y,R]=useState(t.config.capture?.fullPage??false),[K,N]=useState(null),z=e.phase==="review"||e.phase==="capturing"||e.phase==="submitting"||e.phase==="error"||e.phase==="success",j=oe(z);useEffect(()=>i.subscribeHover(S),[i]),useEffect(()=>{let n=i.__unsafeGetSelectedElement();if(!n||e.phase==="idle"||e.phase==="picking"){N(null);return}let M=()=>{N(b(n.getBoundingClientRect()));};M();let m=()=>M();return window.addEventListener("scroll",m,{capture:true,passive:true}),window.addEventListener("resize",m,{passive:true}),()=>{window.removeEventListener("scroll",m,{capture:true}),window.removeEventListener("resize",m);}},[i,e.phase,e.selection?.selector]),useEffect(()=>{e.phase==="review"&&(P(""),E(t.config.capture?.element??true),R(t.config.capture?.fullPage??false));},[e.phase,e.selection?.selector,t.config.capture?.element,t.config.capture?.fullPage]),useEffect(()=>{if(e.phase!=="success")return;let n=window.setTimeout(()=>o(),1200);return ()=>window.clearTimeout(n)},[e.phase,o]);let f=e.phase==="capturing"||e.phase==="submitting",b$1=e.phase==="picking"?x?.rect??null:K??e.selection?.rect??null,w=useMemo(()=>b$1?Z(b$1,360,d):null,[b$1?.x,b$1?.y,b$1?.width,b$1?.height,d]),T=e.lastError?.message,C=useCallback(()=>{a(p,{capture:{element:v,fullPage:y}});},[a,p,v,y]),O=useCallback(n=>{(n.metaKey||n.ctrlKey)&&n.key==="Enter"&&p.trim().length>0&&!f&&(n.preventDefault(),C());},[C,p,f]);return jsxs(Fragment,{children:[e.phase==="idle"&&jsxs("button",{className:ee(d),type:"button",onClick:()=>c(),"aria-label":"Give feedback",children:[jsx("span",{className:"bf-dot","aria-hidden":"true"}),"Feedback"]}),e.phase!=="idle"&&jsxs("div",{className:"bf-overlay",role:"presentation",children:[e.phase!=="picking"&&jsx("div",{className:"bf-blocker",role:"presentation",onClick:()=>o()}),b$1&&jsx(te,{rect:b$1}),e.phase==="picking"&&jsxs("div",{className:"bf-hint",role:"status","aria-live":"polite",children:[jsxs("p",{id:"bf-hint-text",children:["Click an element to attach your feedback. Press ",jsx("strong",{children:"Esc"})," to cancel."]}),jsx("button",{type:"button",className:"bf-btn",onClick:()=>o(),"aria-label":"Cancel element selection",children:"Cancel"})]}),z&&w&&jsxs("div",{ref:j,className:"bf-panel",style:{left:w.left,top:w.top},role:"dialog","aria-modal":"true","aria-label":"Feedback form",children:[jsxs("div",{className:"bf-panelHeader",children:[jsx("div",{className:"bf-title",id:"bf-panel-title",children:"Feedback"}),jsxs("div",{className:"bf-row",style:{justifyContent:"flex-end"},children:[jsx("button",{type:"button",className:"bf-btn",onClick:()=>s(),disabled:f,"aria-label":"Re-pick element",children:"Re-pick"}),jsx("button",{type:"button",className:"bf-btn",onClick:()=>o(),disabled:f,"aria-label":"Close feedback form",children:"Close"})]})]}),jsxs("div",{className:"bf-panelBody",children:[jsx("textarea",{className:"bf-textarea",placeholder:"What's happening? What did you expect?",value:p,onChange:n=>P(n.target.value),onKeyDown:O,disabled:f,"aria-label":"Feedback message"}),jsxs("div",{className:"bf-row",role:"group","aria-label":"Screenshot options",children:[jsxs("label",{children:[jsx("input",{type:"checkbox",checked:v,onChange:n=>E(n.target.checked),disabled:f}),"Screenshot element"]}),jsxs("label",{children:[jsx("input",{type:"checkbox",checked:y,onChange:n=>R(n.target.checked),disabled:f}),"Full page"]})]}),e.phase==="capturing"&&jsxs("div",{className:"bf-status",role:"status","aria-live":"polite",children:[jsx("span",{className:"bf-spinner","aria-hidden":"true"}),"Capturing screenshots\u2026"]}),e.phase==="submitting"&&jsxs("div",{className:"bf-status",role:"status","aria-live":"polite",children:[jsx("span",{className:"bf-spinner","aria-hidden":"true"}),"Submitting\u2026"]}),e.phase==="success"&&jsx("div",{className:"bf-status",role:"status","aria-live":"polite",children:"Sent. Thank you!"}),e.phase==="error"&&T&&jsx("div",{className:"bf-error",role:"alert",children:T}),jsxs("div",{className:"bf-actions",children:[jsx("button",{type:"button",className:"bf-btn",onClick:()=>o(),disabled:f,"aria-label":"Cancel feedback",children:"Cancel"}),jsx("button",{type:"button",className:"bf-btn bf-btnPrimary",onClick:C,disabled:f||p.trim().length===0,"aria-label":"Send feedback",children:"Send"})]})]})]})]}),e.phase==="success"&&jsx("div",{className:"bf-toast",role:"status","aria-live":"polite",children:"Feedback sent"})]})}function ae(t){let e={...t.config??{},blocfeed_id:t.blocfeed_id},[i,c]=useState(null);return useEffect(()=>{W();let o=document.createElement("div");o.setAttribute("data-blocfeed-ui-root","true"),o.setAttribute("data-blocfeed-ui","true");let s=e.ui?.zIndex;typeof s=="number"&&o.style.setProperty("--bf-z",String(s));let a=e.ui?.theme;return a&&(a.accentColor&&o.style.setProperty("--bf-accent",a.accentColor),a.panelBackground&&o.style.setProperty("--bf-panel-bg",a.panelBackground),a.panelForeground&&o.style.setProperty("--bf-panel-fg",a.panelForeground),a.fontFamily&&o.style.setProperty("--bf-font",a.fontFamily)),document.body.appendChild(o),c(o),()=>{o.remove(),c(null);}},[e.ui?.zIndex,e.ui?.theme?.accentColor,e.ui?.theme?.panelBackground,e.ui?.theme?.panelForeground,e.ui?.theme?.fontFamily]),i?createPortal(jsx(k,{blocfeed_id:e.blocfeed_id,...t.config?{config:t.config}:{},children:jsx(re,{config:e})}),i):null}export{k as BlocFeedProvider,ae as BlocFeedWidget,B as useBlocFeed};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blocfeed",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "Drop-in feedback + screenshot widget for React.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -42,6 +42,16 @@
42
42
  "require": "./dist/engine.cjs"
43
43
  }
44
44
  },
45
+ "typesVersions": {
46
+ "*": {
47
+ ".": [
48
+ "./dist/main.d.ts"
49
+ ],
50
+ "engine": [
51
+ "./dist/engine.d.ts"
52
+ ]
53
+ }
54
+ },
45
55
  "scripts": {
46
56
  "build": "tsup",
47
57
  "build:debug": "tsup --sourcemap",
@@ -1,2 +0,0 @@
1
- function b(){return typeof window<"u"&&typeof document<"u"}function W(e){let n=globalThis.CSS;return typeof n?.escape=="function"?n.escape(e):e.replace(/[^a-zA-Z0-9_-]/g,t=>{let r=t.codePointAt(0);return r===void 0?"":`\\${r.toString(16)} `})}function H(e){return {x:e.x,y:e.y,width:e.width,height:e.height}}function de(e){return {x:e.x+window.scrollX,y:e.y+window.scrollY,width:e.width,height:e.height}}function fe(e){return e.replace(/\s+/g," ").trim()}function me(e,n=140){let t=e.textContent;if(!t)return;let r=fe(t);if(r)return r.length<=n?r:`${r.slice(0,n-1)}\u2026`}function pe(e){let n=1;for(let t=e.previousElementSibling;t;t=t.previousElementSibling)t.tagName===e.tagName&&(n+=1);return n}var K=["data-testid","data-test-id","data-test","data-qa","data-cy"],z="data-blocfeed-component";function ge(e){let n=e.closest(`[${z}]`);if(!n)return;let r=n.getAttribute(z)?.trim();return r||void 0}function we(e){for(let n of K){let t=e.closest(`[${n}]`);if(!t)continue;let o=t.getAttribute(n)?.trim();if(o)return o}}function he(e){try{let n=e,t=Object.getOwnPropertyNames(n);for(let r of t)if(r.startsWith("__reactFiber$")||r.startsWith("__reactInternalInstance$")){let o=n[r];if(o&&typeof o=="object")return o}}catch{}return null}function C(e){if(e&&typeof e!="string"){if(typeof e=="function"){let n=e;return typeof n.displayName=="string"&&n.displayName?n.displayName:typeof n.name=="string"&&n.name?n.name:void 0}if(typeof e=="object"){let n=e,t=n.displayName;if(typeof t=="string"&&t)return t;let r=n.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=n.type;return C(o)}}}function be(e){let n=he(e);if(!n)return;let t=n._debugOwner;for(let o=0;t&&o<50;o+=1){let i=C(t.type)??C(t.elementType);if(i)return i;t=t._debugOwner;}let r=n;for(let o=0;r&&o<80;o+=1){let i=C(r.type)??C(r.elementType);if(i)return i;r=r.return;}}function ye(e){let n=e.tagName.toLowerCase(),t=e.getAttribute("id");if(t)return `#${W(t)}`;for(let r of K){let o=e.getAttribute(r);if(o)return `${n}[${r}="${W(o)}"]`}return `${n}:nth-of-type(${pe(e)})`}function Ee(e,n=10){let t=[],r=e;for(;r&&t.length<n;){let o=ye(r);if(t.unshift(o),o.startsWith("#"))break;r=r.parentElement;}return t.join(" > ")}function Y(e,n){if(!n||n.length===0)return false;for(let t of n)if(e.closest(t))return true;return false}function N(e,n){if(!e||Y(e,n.ignoreSelectors))return null;let t=e;for(;t;){if(Y(t,n.ignoreSelectors))return null;if(n.isSelectable?.(t)??ve(t))return t;t=t.parentElement;}return null}function ve(e){let n=e.tagName;return !(n==="HTML"||n==="BODY")}function Q(e){let n=e.getBoundingClientRect(),t={selector:Ee(e),tagName:e.tagName.toLowerCase(),rect:H(n),pageRect:de(n)},r=e.getAttribute("id");r&&(t.id=r);let o=e.className;typeof o=="string"&&o.trim()&&(t.className=o);let i=me(e);i&&(t.textSnippet=i);let l=ge(e)??be(e);l&&(t.componentName=l);let s=we(e);return s&&(t.testId=s),t}var O=null;async function X(){return O||(O=import('html-to-image')),O}async function Se(e){return await new Promise((n,t)=>{let r=new Image;r.onload=()=>n({width:r.naturalWidth,height:r.naturalHeight}),r.onerror=()=>t(new Error("Failed to load generated screenshot")),r.src=e;})}async function J(e,n){let{width:t,height:r}=await Se(e);return {dataUrl:e,mime:n,width:t,height:r}}function T(e){if(e?.aborted)throw new Error("Aborted")}function G(){return {async captureElement(e,n){if(!b())throw new Error("captureElement can only run in the browser");T(n.signal);let t=await X();T(n.signal);let r=n.mime==="image/jpeg"?await t.toJpeg(e,{quality:n.quality??.92,pixelRatio:n.pixelRatio}):await t.toPng(e,{pixelRatio:n.pixelRatio});return T(n.signal),await J(r,n.mime)},async captureFullPage(e){if(!b())throw new Error("captureFullPage can only run in the browser");T(e.signal);let n=document.documentElement,t=Math.max(n.scrollWidth,n.clientWidth),r=Math.max(n.scrollHeight,n.clientHeight),o=Math.min(1,e.maxDimension/Math.max(t,r)),i=Math.max(1,Math.round(t*o)),l=Math.max(1,Math.round(r*o)),s=await X();T(e.signal);let u=e.mime==="image/jpeg"?await s.toJpeg(n,{width:i,height:l,quality:e.quality??.92,pixelRatio:e.pixelRatio}):await s.toPng(n,{width:i,height:l,pixelRatio:e.pixelRatio});return T(e.signal),await J(u,e.mime)}}}function ke(e){if(e instanceof Error)return e.message;if(typeof e=="string")return e;try{return JSON.stringify(e)}catch{return "Unknown error"}}function m(e,n,t){let r={kind:e,message:ke(n)};return t&&(r.detail=t),r}var Fe=12e3,xe=2048,Pe=.92;function Z(){return Date.now()}function Te(e){return new Promise((n,t)=>{let r=()=>t(new Error("Aborted"));if(e.aborted){r();return}e.addEventListener("abort",r,{once:true});})}async function V(e,n,t){let r=new Promise((i,l)=>{let s=setTimeout(()=>l(new Error("Timeout")),n);typeof s.unref=="function"&&s.unref();}),o=[e,r];return t&&o.push(Te(t)),await Promise.race(o)}function Ae(e){if(!b())return 1;let n=window.devicePixelRatio||1,t=e?.pixelRatio??Math.min(n,2);return Math.max(.1,t)}function Ce(e){return !!(e?.element||e?.fullPage)}function ee(e){let n={mime:e.mime,pixelRatio:e.pixelRatio,maxDimension:e.maxDimension};return e.includeQuality&&(n.quality=e.quality),e.signal&&(n.signal=e.signal),n}async function te(e){let{selectionElement:n,capture:t,signal:r}=e;if(!b()||!Ce(t))return;let o=Z(),i=[],l=t?.timeoutMs??Fe,s=t?.maxDimension??xe,u=t?.mime??"image/png",d=t?.quality??Pe,v=t?.adapter??G(),y={},p=Ae(t);if(t?.element&&n)try{let f=n.getBoundingClientRect(),g=Math.min(1,s/Math.max(f.width,f.height)),c=Math.min(p,p*g),E=await V(Promise.resolve(v.captureElement(n,{...ee({mime:u,quality:d,pixelRatio:c,maxDimension:s,includeQuality:u==="image/jpeg",...r?{signal:r}:{}})})),l,r);y.element=E;}catch(f){if(r?.aborted)throw f;i.push(m("capture_failed",f,{target:"element"}));}if(t?.fullPage)try{let f=await V(Promise.resolve(v.captureFullPage(ee({mime:u,quality:d,pixelRatio:p,maxDimension:s,includeQuality:u==="image/jpeg",...r?{signal:r}:{}}))),l,r);y.fullPage=f;}catch(f){if(r?.aborted)throw f;i.push(m("capture_failed",f,{target:"fullPage"}));}let S=Z(),a={startedAt:o,finishedAt:S,durationMs:Math.max(0,S-o)};return i.length>0&&(a.errors=i),{...y,diagnostics:a}}function Re(){try{return Intl.DateTimeFormat().resolvedOptions().timeZone}catch{return}}function Me(){return b()?{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:Re()}:{}}async function ne(e){let{config:n,context:t}=e;if(n?.enabled===false)return {};let r=Me(),o=n?.enrich;if(!o)return r;try{let i=await o(t);return {...r,...i}}catch(i){let l=m("unknown",i);return {...r,blocfeedMetadataError:l.message}}}function U(e){let n=null,t=null,r=(...o)=>{t=o,n===null&&(n=requestAnimationFrame(()=>{if(n=null,!t)return;let i=t;t=null,e(...i);}));};return r.cancel=()=>{n!==null&&cancelAnimationFrame(n),n=null,t=null;},r}function M(e){return e instanceof Element?!!e.closest("[data-blocfeed-ui]"):false}function B(e){e.stopPropagation(),e.stopImmediatePropagation?.();}function re(e,n){if(!b())throw new Error("BlocFeed picker can only run in a browser environment.");let t=e.ignoreSelectors,r=e.isSelectable,o={};t&&t.length>0&&(o.ignoreSelectors=t),r&&(o.isSelectable=r);let i=null,l=null,s=(a,f=false)=>{if(!a){i=null,l=null,n.onHover(null);return}let g=H(a.getBoundingClientRect()),c=`${Math.round(g.x)}:${Math.round(g.y)}:${Math.round(g.width)}:${Math.round(g.height)}`;!f&&a===i&&c===l||(i=a,l=c,n.onHover({element:a,rect:g}));},u=U(a=>{if(M(a.target))return;let f=document.elementFromPoint(a.clientX,a.clientY),g=N(f,o);s(g);}),d=U(()=>{i&&s(i,true);}),v=a=>{M(a.target)||(B(a),a.pointerType==="mouse"&&a.preventDefault());},y=a=>{M(a.target)||(B(a),a.pointerType==="mouse"&&a.preventDefault());},p=a=>{if(M(a.target))return;B(a),a.preventDefault();let f=document.elementFromPoint(a.clientX,a.clientY),g=N(f,o);g&&n.onSelect({element:g,descriptor:Q(g)});},S=a=>{a.key==="Escape"&&(B(a),a.preventDefault(),n.onCancel());};return window.addEventListener("pointermove",u,{capture:true,passive:true}),window.addEventListener("pointerdown",v,{capture:true}),window.addEventListener("pointerup",y,{capture:true}),window.addEventListener("click",p,{capture:true}),window.addEventListener("keydown",S,{capture:true}),window.addEventListener("scroll",d,{capture:true,passive:true}),window.addEventListener("resize",d,{passive:true}),{stop(){window.removeEventListener("pointermove",u,{capture:true}),window.removeEventListener("pointerdown",v,{capture:true}),window.removeEventListener("pointerup",y,{capture:true}),window.removeEventListener("click",p,{capture:true}),window.removeEventListener("keydown",S,{capture:true}),window.removeEventListener("scroll",d,{capture:true}),window.removeEventListener("resize",d),u.cancel(),d.cancel(),n.onHover(null);}}}var Be=12e3,$=2,oe=500,_e="https://blocfeed.com/api/feedback";function ie(e,n){return new Promise((t,r)=>{if(n?.aborted){r(new Error("Aborted"));return}let o=setTimeout(t,e),i=()=>{clearTimeout(o),r(new Error("Aborted"));};n?.addEventListener("abort",i,{once:true});})}function Le(e){return e>=500&&e<=599}async function ae(e){let{payload:n,signal:t}=e;for(let r=1;r<=$;r+=1){let o=new AbortController,i=setTimeout(()=>o.abort(),Be),l=()=>o.abort();t&&t.addEventListener("abort",l,{once:true});try{let s=await fetch(_e,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(n),signal:o.signal});if(s.ok)return {ok:!0,status:s.status};if(r<$&&Le(s.status)){let u=.85+Math.random()*.3,d=Math.round(oe*2**(r-1)*u);await ie(d,t);continue}return {ok:!1,status:s.status,error:m("api_failed",new Error(`HTTP ${s.status}`))}}catch(s){if(o.signal.aborted||t?.aborted)return {ok:false,error:m("aborted",s)};if(r<$){let u=.85+Math.random()*.3,d=Math.round(oe*2**(r-1)*u);await ie(d,t);continue}return {ok:false,error:m("api_failed",s)}}finally{clearTimeout(i),t&&t.removeEventListener("abort",l);}}return {ok:false,error:m("api_failed",new Error("Failed"))}}async function se(e){let{signal:n}=e,t={ok:false};try{let r=n?{payload:e.payload,signal:n}:{payload:e.payload};t.api=await ae(r),t.ok=!!t.api?.ok;}catch(r){t.api={ok:false,error:m("api_failed",r)},t.ok=false;}return {payload:e.payload,result:t}}var Ie=["[data-blocfeed-ui]","[data-blocfeed-ignore]"];function De(e){let n=[...Ie,...e?.ignoreSelectors??[]],t=Array.from(new Set(n));return {...e,ignoreSelectors:t}}function le(){return {phase:"idle"}}function ut(e){let n=e,t=le(),r=new Set,o=new Set,i=null,l=null,s=null,u=null,d=0,v=()=>{for(let c of r)c(t);},y=c=>{for(let E of o)E(c);},p=c=>{t=c,v();},S=()=>{d+=1,u?.abort(),u=null;},a=()=>{i?.stop(),i=null,y(null),s!==null&&b()&&(document.documentElement.style.cursor=s,s=null);},f=()=>{S(),a(),l=null,p(le());},g=()=>{if(!b())return;a(),l=null;let c=De(n.picker);s=document.documentElement.style.cursor,document.documentElement.style.cursor="crosshair",p({phase:"picking"}),i=re(c,{onHover:y,onSelect:({element:E,descriptor:R})=>{l=E,a(),p({phase:"review",selection:R});},onCancel:()=>{f();}});};return {getState:()=>t,subscribe(c){return r.add(c),()=>r.delete(c)},subscribeHover(c){return o.add(c),()=>o.delete(c)},start(){t.phase==="capturing"||t.phase==="submitting"||t.phase!=="picking"&&g();},stop(){f();},clearSelection(){t.phase==="capturing"||t.phase==="submitting"||g();},setConfig(c){n=c;},async submit(c,E){if(!b()){let w=m("configuration",new Error("BlocFeed submit can only run in the browser"));return p({phase:"error",lastError:w}),{ok:false}}let R=n.blocfeed_id?.trim?.()??"";if(!R){let k={phase:"error",lastError:m("configuration",new Error("Missing blocfeed_id. Create a project in BlocFeed and pass its blocfeed_id."))};return t.selection&&(k.selection=t.selection),p(k),{ok:false}}if(t.phase==="capturing"||t.phase==="submitting")return {ok:false};let A=d+1;d=A,u?.abort(),u=new AbortController;let x=u.signal,h=t.selection,_=E?.capture?{...n.capture,...E.capture}:n.capture,q=!!(_?.element||_?.fullPage),j={phase:q?"capturing":"submitting"};h&&(j.selection=h),p(j);try{let w=q?await te({selectionElement:l,capture:_,signal:x}):void 0;if(x.aborted||d!==A)return {ok:!1};let k={phase:"submitting"};h&&(k.selection=h),w&&(k.capture=w),p(k);let P={};h&&(P.selection=h),w&&(P.capture=w);let ce=await ne({config:n.metadata,context:P}),L={version:1,createdAt:new Date().toISOString(),blocfeed_id:R,message:c,metadata:ce};h&&(L.selection=h),w&&(L.screenshots=w);let{result:F}=await se({payload:L,signal:x});if(x.aborted||d!==A)return F;if(F.ok){let D={phase:"success",lastSubmit:F};return h&&(D.selection=h),w&&(D.capture=w),p(D),F}let ue=F.api?.error??m("unknown",new Error("Submission failed")),I={phase:"error",lastSubmit:F,lastError:ue};return h&&(I.selection=h),w&&(I.capture=w),p(I),F}catch(w){if(x.aborted||d!==A)return {ok:false};let P={phase:"error",lastError:x.aborted?m("aborted",w):m("unknown",w)};return h&&(P.selection=h),p(P),{ok:false}}finally{d===A&&(u=null);}},__unsafeGetSelectedElement(){return l},destroy(){f(),r.clear(),o.clear();}}}
2
- export{b as a,H as b,G as c,te as d,ne as e,ut as f};
@@ -1,2 +0,0 @@
1
- 'use strict';function b(){return typeof window<"u"&&typeof document<"u"}function W(e){let n=globalThis.CSS;return typeof n?.escape=="function"?n.escape(e):e.replace(/[^a-zA-Z0-9_-]/g,t=>{let r=t.codePointAt(0);return r===void 0?"":`\\${r.toString(16)} `})}function H(e){return {x:e.x,y:e.y,width:e.width,height:e.height}}function de(e){return {x:e.x+window.scrollX,y:e.y+window.scrollY,width:e.width,height:e.height}}function fe(e){return e.replace(/\s+/g," ").trim()}function me(e,n=140){let t=e.textContent;if(!t)return;let r=fe(t);if(r)return r.length<=n?r:`${r.slice(0,n-1)}\u2026`}function pe(e){let n=1;for(let t=e.previousElementSibling;t;t=t.previousElementSibling)t.tagName===e.tagName&&(n+=1);return n}var K=["data-testid","data-test-id","data-test","data-qa","data-cy"],z="data-blocfeed-component";function ge(e){let n=e.closest(`[${z}]`);if(!n)return;let r=n.getAttribute(z)?.trim();return r||void 0}function we(e){for(let n of K){let t=e.closest(`[${n}]`);if(!t)continue;let o=t.getAttribute(n)?.trim();if(o)return o}}function he(e){try{let n=e,t=Object.getOwnPropertyNames(n);for(let r of t)if(r.startsWith("__reactFiber$")||r.startsWith("__reactInternalInstance$")){let o=n[r];if(o&&typeof o=="object")return o}}catch{}return null}function C(e){if(e&&typeof e!="string"){if(typeof e=="function"){let n=e;return typeof n.displayName=="string"&&n.displayName?n.displayName:typeof n.name=="string"&&n.name?n.name:void 0}if(typeof e=="object"){let n=e,t=n.displayName;if(typeof t=="string"&&t)return t;let r=n.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=n.type;return C(o)}}}function be(e){let n=he(e);if(!n)return;let t=n._debugOwner;for(let o=0;t&&o<50;o+=1){let i=C(t.type)??C(t.elementType);if(i)return i;t=t._debugOwner;}let r=n;for(let o=0;r&&o<80;o+=1){let i=C(r.type)??C(r.elementType);if(i)return i;r=r.return;}}function ye(e){let n=e.tagName.toLowerCase(),t=e.getAttribute("id");if(t)return `#${W(t)}`;for(let r of K){let o=e.getAttribute(r);if(o)return `${n}[${r}="${W(o)}"]`}return `${n}:nth-of-type(${pe(e)})`}function Ee(e,n=10){let t=[],r=e;for(;r&&t.length<n;){let o=ye(r);if(t.unshift(o),o.startsWith("#"))break;r=r.parentElement;}return t.join(" > ")}function Y(e,n){if(!n||n.length===0)return false;for(let t of n)if(e.closest(t))return true;return false}function N(e,n){if(!e||Y(e,n.ignoreSelectors))return null;let t=e;for(;t;){if(Y(t,n.ignoreSelectors))return null;if(n.isSelectable?.(t)??ve(t))return t;t=t.parentElement;}return null}function ve(e){let n=e.tagName;return !(n==="HTML"||n==="BODY")}function Q(e){let n=e.getBoundingClientRect(),t={selector:Ee(e),tagName:e.tagName.toLowerCase(),rect:H(n),pageRect:de(n)},r=e.getAttribute("id");r&&(t.id=r);let o=e.className;typeof o=="string"&&o.trim()&&(t.className=o);let i=me(e);i&&(t.textSnippet=i);let l=ge(e)??be(e);l&&(t.componentName=l);let s=we(e);return s&&(t.testId=s),t}var O=null;async function X(){return O||(O=import('html-to-image')),O}async function Se(e){return await new Promise((n,t)=>{let r=new Image;r.onload=()=>n({width:r.naturalWidth,height:r.naturalHeight}),r.onerror=()=>t(new Error("Failed to load generated screenshot")),r.src=e;})}async function J(e,n){let{width:t,height:r}=await Se(e);return {dataUrl:e,mime:n,width:t,height:r}}function T(e){if(e?.aborted)throw new Error("Aborted")}function G(){return {async captureElement(e,n){if(!b())throw new Error("captureElement can only run in the browser");T(n.signal);let t=await X();T(n.signal);let r=n.mime==="image/jpeg"?await t.toJpeg(e,{quality:n.quality??.92,pixelRatio:n.pixelRatio}):await t.toPng(e,{pixelRatio:n.pixelRatio});return T(n.signal),await J(r,n.mime)},async captureFullPage(e){if(!b())throw new Error("captureFullPage can only run in the browser");T(e.signal);let n=document.documentElement,t=Math.max(n.scrollWidth,n.clientWidth),r=Math.max(n.scrollHeight,n.clientHeight),o=Math.min(1,e.maxDimension/Math.max(t,r)),i=Math.max(1,Math.round(t*o)),l=Math.max(1,Math.round(r*o)),s=await X();T(e.signal);let u=e.mime==="image/jpeg"?await s.toJpeg(n,{width:i,height:l,quality:e.quality??.92,pixelRatio:e.pixelRatio}):await s.toPng(n,{width:i,height:l,pixelRatio:e.pixelRatio});return T(e.signal),await J(u,e.mime)}}}function ke(e){if(e instanceof Error)return e.message;if(typeof e=="string")return e;try{return JSON.stringify(e)}catch{return "Unknown error"}}function m(e,n,t){let r={kind:e,message:ke(n)};return t&&(r.detail=t),r}var Fe=12e3,xe=2048,Pe=.92;function Z(){return Date.now()}function Te(e){return new Promise((n,t)=>{let r=()=>t(new Error("Aborted"));if(e.aborted){r();return}e.addEventListener("abort",r,{once:true});})}async function V(e,n,t){let r=new Promise((i,l)=>{let s=setTimeout(()=>l(new Error("Timeout")),n);typeof s.unref=="function"&&s.unref();}),o=[e,r];return t&&o.push(Te(t)),await Promise.race(o)}function Ae(e){if(!b())return 1;let n=window.devicePixelRatio||1,t=e?.pixelRatio??Math.min(n,2);return Math.max(.1,t)}function Ce(e){return !!(e?.element||e?.fullPage)}function ee(e){let n={mime:e.mime,pixelRatio:e.pixelRatio,maxDimension:e.maxDimension};return e.includeQuality&&(n.quality=e.quality),e.signal&&(n.signal=e.signal),n}async function te(e){let{selectionElement:n,capture:t,signal:r}=e;if(!b()||!Ce(t))return;let o=Z(),i=[],l=t?.timeoutMs??Fe,s=t?.maxDimension??xe,u=t?.mime??"image/png",d=t?.quality??Pe,v=t?.adapter??G(),y={},p=Ae(t);if(t?.element&&n)try{let f=n.getBoundingClientRect(),g=Math.min(1,s/Math.max(f.width,f.height)),c=Math.min(p,p*g),E=await V(Promise.resolve(v.captureElement(n,{...ee({mime:u,quality:d,pixelRatio:c,maxDimension:s,includeQuality:u==="image/jpeg",...r?{signal:r}:{}})})),l,r);y.element=E;}catch(f){if(r?.aborted)throw f;i.push(m("capture_failed",f,{target:"element"}));}if(t?.fullPage)try{let f=await V(Promise.resolve(v.captureFullPage(ee({mime:u,quality:d,pixelRatio:p,maxDimension:s,includeQuality:u==="image/jpeg",...r?{signal:r}:{}}))),l,r);y.fullPage=f;}catch(f){if(r?.aborted)throw f;i.push(m("capture_failed",f,{target:"fullPage"}));}let S=Z(),a={startedAt:o,finishedAt:S,durationMs:Math.max(0,S-o)};return i.length>0&&(a.errors=i),{...y,diagnostics:a}}function Re(){try{return Intl.DateTimeFormat().resolvedOptions().timeZone}catch{return}}function Me(){return b()?{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:Re()}:{}}async function ne(e){let{config:n,context:t}=e;if(n?.enabled===false)return {};let r=Me(),o=n?.enrich;if(!o)return r;try{let i=await o(t);return {...r,...i}}catch(i){let l=m("unknown",i);return {...r,blocfeedMetadataError:l.message}}}function U(e){let n=null,t=null,r=(...o)=>{t=o,n===null&&(n=requestAnimationFrame(()=>{if(n=null,!t)return;let i=t;t=null,e(...i);}));};return r.cancel=()=>{n!==null&&cancelAnimationFrame(n),n=null,t=null;},r}function M(e){return e instanceof Element?!!e.closest("[data-blocfeed-ui]"):false}function B(e){e.stopPropagation(),e.stopImmediatePropagation?.();}function re(e,n){if(!b())throw new Error("BlocFeed picker can only run in a browser environment.");let t=e.ignoreSelectors,r=e.isSelectable,o={};t&&t.length>0&&(o.ignoreSelectors=t),r&&(o.isSelectable=r);let i=null,l=null,s=(a,f=false)=>{if(!a){i=null,l=null,n.onHover(null);return}let g=H(a.getBoundingClientRect()),c=`${Math.round(g.x)}:${Math.round(g.y)}:${Math.round(g.width)}:${Math.round(g.height)}`;!f&&a===i&&c===l||(i=a,l=c,n.onHover({element:a,rect:g}));},u=U(a=>{if(M(a.target))return;let f=document.elementFromPoint(a.clientX,a.clientY),g=N(f,o);s(g);}),d=U(()=>{i&&s(i,true);}),v=a=>{M(a.target)||(B(a),a.pointerType==="mouse"&&a.preventDefault());},y=a=>{M(a.target)||(B(a),a.pointerType==="mouse"&&a.preventDefault());},p=a=>{if(M(a.target))return;B(a),a.preventDefault();let f=document.elementFromPoint(a.clientX,a.clientY),g=N(f,o);g&&n.onSelect({element:g,descriptor:Q(g)});},S=a=>{a.key==="Escape"&&(B(a),a.preventDefault(),n.onCancel());};return window.addEventListener("pointermove",u,{capture:true,passive:true}),window.addEventListener("pointerdown",v,{capture:true}),window.addEventListener("pointerup",y,{capture:true}),window.addEventListener("click",p,{capture:true}),window.addEventListener("keydown",S,{capture:true}),window.addEventListener("scroll",d,{capture:true,passive:true}),window.addEventListener("resize",d,{passive:true}),{stop(){window.removeEventListener("pointermove",u,{capture:true}),window.removeEventListener("pointerdown",v,{capture:true}),window.removeEventListener("pointerup",y,{capture:true}),window.removeEventListener("click",p,{capture:true}),window.removeEventListener("keydown",S,{capture:true}),window.removeEventListener("scroll",d,{capture:true}),window.removeEventListener("resize",d),u.cancel(),d.cancel(),n.onHover(null);}}}var Be=12e3,$=2,oe=500,_e="https://blocfeed.com/api/feedback";function ie(e,n){return new Promise((t,r)=>{if(n?.aborted){r(new Error("Aborted"));return}let o=setTimeout(t,e),i=()=>{clearTimeout(o),r(new Error("Aborted"));};n?.addEventListener("abort",i,{once:true});})}function Le(e){return e>=500&&e<=599}async function ae(e){let{payload:n,signal:t}=e;for(let r=1;r<=$;r+=1){let o=new AbortController,i=setTimeout(()=>o.abort(),Be),l=()=>o.abort();t&&t.addEventListener("abort",l,{once:true});try{let s=await fetch(_e,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(n),signal:o.signal});if(s.ok)return {ok:!0,status:s.status};if(r<$&&Le(s.status)){let u=.85+Math.random()*.3,d=Math.round(oe*2**(r-1)*u);await ie(d,t);continue}return {ok:!1,status:s.status,error:m("api_failed",new Error(`HTTP ${s.status}`))}}catch(s){if(o.signal.aborted||t?.aborted)return {ok:false,error:m("aborted",s)};if(r<$){let u=.85+Math.random()*.3,d=Math.round(oe*2**(r-1)*u);await ie(d,t);continue}return {ok:false,error:m("api_failed",s)}}finally{clearTimeout(i),t&&t.removeEventListener("abort",l);}}return {ok:false,error:m("api_failed",new Error("Failed"))}}async function se(e){let{signal:n}=e,t={ok:false};try{let r=n?{payload:e.payload,signal:n}:{payload:e.payload};t.api=await ae(r),t.ok=!!t.api?.ok;}catch(r){t.api={ok:false,error:m("api_failed",r)},t.ok=false;}return {payload:e.payload,result:t}}var Ie=["[data-blocfeed-ui]","[data-blocfeed-ignore]"];function De(e){let n=[...Ie,...e?.ignoreSelectors??[]],t=Array.from(new Set(n));return {...e,ignoreSelectors:t}}function le(){return {phase:"idle"}}function ut(e){let n=e,t=le(),r=new Set,o=new Set,i=null,l=null,s=null,u=null,d=0,v=()=>{for(let c of r)c(t);},y=c=>{for(let E of o)E(c);},p=c=>{t=c,v();},S=()=>{d+=1,u?.abort(),u=null;},a=()=>{i?.stop(),i=null,y(null),s!==null&&b()&&(document.documentElement.style.cursor=s,s=null);},f=()=>{S(),a(),l=null,p(le());},g=()=>{if(!b())return;a(),l=null;let c=De(n.picker);s=document.documentElement.style.cursor,document.documentElement.style.cursor="crosshair",p({phase:"picking"}),i=re(c,{onHover:y,onSelect:({element:E,descriptor:R})=>{l=E,a(),p({phase:"review",selection:R});},onCancel:()=>{f();}});};return {getState:()=>t,subscribe(c){return r.add(c),()=>r.delete(c)},subscribeHover(c){return o.add(c),()=>o.delete(c)},start(){t.phase==="capturing"||t.phase==="submitting"||t.phase!=="picking"&&g();},stop(){f();},clearSelection(){t.phase==="capturing"||t.phase==="submitting"||g();},setConfig(c){n=c;},async submit(c,E){if(!b()){let w=m("configuration",new Error("BlocFeed submit can only run in the browser"));return p({phase:"error",lastError:w}),{ok:false}}let R=n.blocfeed_id?.trim?.()??"";if(!R){let k={phase:"error",lastError:m("configuration",new Error("Missing blocfeed_id. Create a project in BlocFeed and pass its blocfeed_id."))};return t.selection&&(k.selection=t.selection),p(k),{ok:false}}if(t.phase==="capturing"||t.phase==="submitting")return {ok:false};let A=d+1;d=A,u?.abort(),u=new AbortController;let x=u.signal,h=t.selection,_=E?.capture?{...n.capture,...E.capture}:n.capture,q=!!(_?.element||_?.fullPage),j={phase:q?"capturing":"submitting"};h&&(j.selection=h),p(j);try{let w=q?await te({selectionElement:l,capture:_,signal:x}):void 0;if(x.aborted||d!==A)return {ok:!1};let k={phase:"submitting"};h&&(k.selection=h),w&&(k.capture=w),p(k);let P={};h&&(P.selection=h),w&&(P.capture=w);let ce=await ne({config:n.metadata,context:P}),L={version:1,createdAt:new Date().toISOString(),blocfeed_id:R,message:c,metadata:ce};h&&(L.selection=h),w&&(L.screenshots=w);let{result:F}=await se({payload:L,signal:x});if(x.aborted||d!==A)return F;if(F.ok){let D={phase:"success",lastSubmit:F};return h&&(D.selection=h),w&&(D.capture=w),p(D),F}let ue=F.api?.error??m("unknown",new Error("Submission failed")),I={phase:"error",lastSubmit:F,lastError:ue};return h&&(I.selection=h),w&&(I.capture=w),p(I),F}catch(w){if(x.aborted||d!==A)return {ok:false};let P={phase:"error",lastError:x.aborted?m("aborted",w):m("unknown",w)};return h&&(P.selection=h),p(P),{ok:false}}finally{d===A&&(u=null);}},__unsafeGetSelectedElement(){return l},destroy(){f(),r.clear(),o.clear();}}}
2
- exports.a=b;exports.b=H;exports.c=G;exports.d=te;exports.e=ne;exports.f=ut;