@qaiddev/thumbs-embed 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,393 @@
1
+ # @qaiddev/thumbs-embed
2
+
3
+ A zero-dependency, lightweight feedback embed that adds thumbs up/down buttons to any website. Users can optionally target specific page elements, leave messages, and capture screenshots — all submitted to your API endpoint or the [QAid.dev hosted dashboard](https://qaid.dev).
4
+
5
+ ## Install
6
+
7
+ ### npm
8
+
9
+ ```bash
10
+ npm install @qaiddev/thumbs-embed
11
+ ```
12
+
13
+ ```typescript
14
+ import { FeedbackEmbed } from '@qaiddev/thumbs-embed';
15
+
16
+ const feedback = new FeedbackEmbed({
17
+ endpoint: 'https://qaid.dev/api/feedback',
18
+ apiKey: 'YOUR_API_KEY',
19
+ });
20
+ ```
21
+
22
+ ### CDN / Script Tag
23
+
24
+ ```html
25
+ <script
26
+ src="https://unpkg.com/@qaiddev/thumbs-embed/dist/qaid.umd.cjs"
27
+ data-endpoint="https://qaid.dev/api/feedback"
28
+ data-api-key="YOUR_API_KEY"
29
+ ></script>
30
+ ```
31
+
32
+ The embed auto-initializes when it detects a `data-endpoint` attribute on its script tag.
33
+
34
+ ### JSON Config (Script Tag)
35
+
36
+ For complex configurations, use a separate JSON config element:
37
+
38
+ ```html
39
+ <script type="application/json" data-feedback-config>
40
+ {
41
+ "endpoint": "https://qaid.dev/api/feedback",
42
+ "apiKey": "YOUR_API_KEY",
43
+ "position": "bottom-left",
44
+ "colors": {
45
+ "positive": "#22c55e",
46
+ "negative": "#ef4444",
47
+ "marker": "#8b5cf6"
48
+ },
49
+ "text": {
50
+ "modalTitle": "How are we doing?",
51
+ "placeholder": "Tell us what you think..."
52
+ }
53
+ }
54
+ </script>
55
+ <script src="https://unpkg.com/@qaiddev/thumbs-embed/dist/qaid.umd.cjs"></script>
56
+ ```
57
+
58
+ ## How It Works
59
+
60
+ 1. Thumbs up/down buttons appear on your page
61
+ 2. User clicks a thumb button
62
+ 3. A targeting overlay activates — the user clicks on any page element to attach their feedback to it
63
+ 4. Optional permission to save a screenshot of the current tab.
64
+ 4. A modal appears where the user can optionally leave a more detailed message
65
+ 5. Feedback is submitted to your endpoint as a JSON POST.
66
+
67
+ If `skipTargeting` is `true`, step 3 is skipped and the modal opens immediately.
68
+
69
+ If `captureScreenshot` is `false`, we skip step 4.
70
+
71
+ ## Configuration
72
+
73
+ All are optional except `endpoint`.
74
+
75
+ We offer a Free Plan for a compatible endpoint and a dashboard to manage your site's feedback.
76
+
77
+ ### Core Options
78
+
79
+ | Option | Type | Default | Description |
80
+ |--------|------|---------|-------------|
81
+ | `endpoint` | `string` | **(required)** | API endpoint URL for submitting feedback data |
82
+ | `apiKey` | `string` | `""` | API key for authenticating with the feedback service |
83
+ | `skipTargeting` | `boolean` | `false` | Skip element targeting and go directly to the feedback modal |
84
+
85
+ ### Positioning
86
+
87
+ | Option | Type | Default | Description |
88
+ |--------|------|---------|-------------|
89
+ | `position` | `string` | `"bottom-right"` | Button position: `"bottom-right"`, `"bottom-left"`, `"top-right"`, `"top-left"` |
90
+ | `offset` | `{ x?: number, y?: number }` | `{ x: 16, y: 16 }` | Distance from viewport edge in pixels |
91
+ | `container` | `string` | `""` | CSS selector for a custom container element. When set, `position`, `offset`, and `zIndex` are ignored |
92
+ | `zIndex` | `number` | `50` | z-index for the embed elements |
93
+
94
+ ### Appearance
95
+
96
+ | Option | Type | Default | Description |
97
+ |--------|------|---------|-------------|
98
+ | `buttonSize` | `string` | `"medium"` | Button size: `"small"` (36px), `"medium"` (48px), `"large"` (64px) |
99
+ | `buttonClass` | `string` | `""` | Custom CSS class for buttons. When set, default button styles are not applied |
100
+ | `incognito` | `boolean` | `false` | Buttons are invisible until hovered |
101
+ | `modalWidth` | `number` | `400` | Width of the feedback modal in pixels |
102
+ | `backdropOpacity` | `number` | `0.3` | Opacity of the dark backdrop behind the modal (0-1) |
103
+ | `fontFamily` | `string` | `"system-ui, -apple-system, sans-serif"` | Font family for all text |
104
+ | `fontSize` | `number` | `16` | Base font size in pixels |
105
+
106
+ ### Colors
107
+
108
+ Pass a `colors` object to customize the color scheme:
109
+
110
+ ```typescript
111
+ new FeedbackEmbed({
112
+ endpoint: '/api/feedback',
113
+ colors: {
114
+ positive: 'rgb(0, 200, 83)', // Thumbs up color (default: green)
115
+ negative: 'rgb(255, 0, 0)', // Thumbs down color (default: red)
116
+ marker: '#6366f1', // Selected element outline & submit button (default: indigo)
117
+ },
118
+ });
119
+ ```
120
+
121
+ Colors accept hex (`#ABC`, `#AABBCC`) or `rgb(r, g, b)` format.
122
+
123
+ ### Custom Icons
124
+
125
+ Replace the default thumb icons with SVG strings or emoji:
126
+
127
+ ```typescript
128
+ new FeedbackEmbed({
129
+ endpoint: '/api/feedback',
130
+ positiveIcon: '<svg viewBox="0 0 24 24">...</svg>',
131
+ negativeIcon: '<svg viewBox="0 0 24 24">...</svg>',
132
+ });
133
+ ```
134
+
135
+ ### Text Customization
136
+
137
+ Every user-facing string can be overridden via the `text` object:
138
+
139
+ ```typescript
140
+ new FeedbackEmbed({
141
+ endpoint: '/api/feedback',
142
+ text: {
143
+ tooltip: 'Any feedback? Click to start, Esc to cancel',
144
+ bannerText: 'Click on any element to target it with your feedback',
145
+ bannerHint: '(Press Escape to cancel)',
146
+ modalTitle: 'Thank you for your feedback!',
147
+ modalSubtitle: 'Would you like to add a message to help us understand your feedback better?',
148
+ placeholder: 'Optional: Tell us more about your experience...',
149
+ submitButton: 'Submit',
150
+ skipButton: 'Skip',
151
+ },
152
+ });
153
+ ```
154
+
155
+ ### Screenshots
156
+
157
+ Enable automatic screenshot capture with feedback submissions:
158
+
159
+ ```typescript
160
+ new FeedbackEmbed({
161
+ endpoint: '/api/feedback',
162
+ captureScreenshot: true,
163
+ screenshotOptions: {
164
+ quality: 0.8, // WebP compression quality (0-1)
165
+ maxWidth: 1280, // Max screenshot width in pixels
166
+ maxHeight: 800, // Max screenshot height in pixels
167
+ },
168
+ });
169
+ ```
170
+
171
+ Screenshots use the browser's Screen Capture API. The user will see a permission dialog. If they decline or the API is unavailable, the feedback is still submitted without a screenshot.
172
+
173
+ ## Script Tag Data Attributes
174
+
175
+ When using the script tag method, all config options are available as `data-*` attributes:
176
+
177
+ | Attribute | Maps To |
178
+ |-----------|---------|
179
+ | `data-endpoint` | `endpoint` |
180
+ | `data-api-key` | `apiKey` |
181
+ | `data-position` | `position` |
182
+ | `data-zindex` | `zIndex` |
183
+ | `data-button-size` | `buttonSize` |
184
+ | `data-button-class` | `buttonClass` |
185
+ | `data-modal-width` | `modalWidth` |
186
+ | `data-backdrop-opacity` | `backdropOpacity` |
187
+ | `data-offset-x` | `offset.x` |
188
+ | `data-offset-y` | `offset.y` |
189
+ | `data-positive-color` | `colors.positive` |
190
+ | `data-negative-color` | `colors.negative` |
191
+ | `data-marker-color` | `colors.marker` |
192
+ | `data-container` | `container` |
193
+ | `data-skip-targeting` | `skipTargeting` |
194
+ | `data-incognito` | `incognito` |
195
+ | `data-font-family` | `fontFamily` |
196
+ | `data-font-size` | `fontSize` |
197
+ | `data-tooltip` | `text.tooltip` |
198
+ | `data-banner-text` | `text.bannerText` |
199
+ | `data-banner-hint` | `text.bannerHint` |
200
+ | `data-modal-title` | `text.modalTitle` |
201
+ | `data-modal-subtitle` | `text.modalSubtitle` |
202
+ | `data-placeholder` | `text.placeholder` |
203
+ | `data-submit-button` | `text.submitButton` |
204
+ | `data-skip-button` | `text.skipButton` |
205
+ | `data-positive-icon` | `positiveIcon` |
206
+ | `data-negative-icon` | `negativeIcon` |
207
+
208
+ ## Custom Button Container
209
+
210
+ By default, the embed creates a fixed-position container in the viewport corner. To place the buttons inside your own element:
211
+
212
+ ```html
213
+ <div id="my-feedback-spot"></div>
214
+
215
+ <script>
216
+ new FeedbackEmbed({
217
+ endpoint: '/api/feedback',
218
+ container: '#my-feedback-spot',
219
+ });
220
+ </script>
221
+ ```
222
+
223
+ When `container` is set, the `position`, `offset`, and `zIndex` options are ignored. You control the layout.
224
+
225
+ ## Custom Button Styling
226
+
227
+ Use `buttonClass` to apply your own CSS instead of the default button styles:
228
+
229
+ ```typescript
230
+ new FeedbackEmbed({
231
+ endpoint: '/api/feedback',
232
+ buttonClass: 'my-feedback-btn',
233
+ });
234
+ ```
235
+
236
+ When `buttonClass` is provided, default button colors, sizing, and shadows are not applied. Structural styles (display, alignment, cursor) are still applied via `.qaid-btn-structural`. Your class controls everything visual.
237
+
238
+ ```css
239
+ .my-feedback-btn {
240
+ width: 40px;
241
+ height: 40px;
242
+ background: #1a1a2e;
243
+ border: 2px solid #e94560;
244
+ border-radius: 8px;
245
+ }
246
+
247
+ .my-feedback-btn:hover {
248
+ background: #e94560;
249
+ }
250
+ ```
251
+
252
+ ## API
253
+
254
+ ### Constructor
255
+
256
+ ```typescript
257
+ const feedback = new FeedbackEmbed(config: FeedbackConfig);
258
+ ```
259
+
260
+ ### Methods
261
+
262
+ | Method | Description |
263
+ |--------|-------------|
264
+ | `destroy()` | Remove all DOM elements, event listeners, and injected styles. Safe to call multiple times. |
265
+
266
+ ### Payload
267
+
268
+ The embed submits feedback in two steps:
269
+
270
+ **1. Initial submission** — `POST` to your endpoint:
271
+
272
+ ```json
273
+ {
274
+ "feedbackType": "up",
275
+ "pageUrl": "https://example.com/page",
276
+ "apiKey": "YOUR_API_KEY",
277
+ "elementSelector": "body > main:nth-child(2) > button:nth-child(3)",
278
+ "elementText": "Submit Order",
279
+ "consoleErrors": [
280
+ { "message": "TypeError: Cannot read properties of undefined", "timestamp": 1704067200000, "level": "error" }
281
+ ],
282
+ "screenWidth": 1920,
283
+ "screenHeight": 1080,
284
+ "clickX": 450,
285
+ "clickY": 320,
286
+ "scrollX": 0,
287
+ "scrollY": 150,
288
+ "screenshot": "data:image/webp;base64,...",
289
+ "visitorId": "a1b2c3d4-...",
290
+ "elementBounds": { "x": 400, "y": 300, "width": 120, "height": 40 },
291
+ "userAgent": "Mozilla/5.0 ..."
292
+ }
293
+ ```
294
+
295
+ Your endpoint should return `{ "id": 123 }`.
296
+
297
+ **2. Message update** — `PATCH` to `{endpoint}/{id}`:
298
+
299
+ ```json
300
+ {
301
+ "message": "The submit button doesn't work on mobile"
302
+ }
303
+ ```
304
+
305
+ ## Console Capture
306
+
307
+ The embed automatically captures up to 20 recent `console.error`, `console.warn`, and `console.log` calls during the user's session. These are included in the feedback payload as `consoleErrors`, giving you context about what went wrong before the user submitted feedback.
308
+
309
+ Console methods are restored to their originals when `destroy()` is called.
310
+
311
+ ## Element Targeting
312
+
313
+ When the user clicks on an element during targeting mode, the embed generates a stable CSS selector using this priority:
314
+
315
+ 1. **Data attributes** — `data-comp`, `data-qa`, `data-testid`, `data-id` (checked on the element and its ancestors)
316
+ 2. **Element ID** — e.g. `#submit-button`
317
+ 3. **nth-child path** — e.g. `body > main:nth-child(2) > button:nth-child(3)` (always unique, always valid)
318
+
319
+ The first 100 characters of the element's text content are also captured.
320
+
321
+ ## Responsive Behavior
322
+
323
+ - **Desktop**: The feedback modal is positioned near the selected element with an arrow pointing at the click location
324
+ - **Mobile** (viewport < 640px): The modal displays as a bottom sheet, sliding up from the bottom of the screen with safe-area inset support
325
+
326
+ ## CSS Custom Properties
327
+
328
+ The embed injects CSS custom properties you can use or override:
329
+
330
+ ```css
331
+ :root {
332
+ --qaid-positive: rgb(0, 200, 83);
333
+ --qaid-negative: rgb(255, 0, 0);
334
+ --qaid-marker: #6366f1;
335
+ --qaid-btn-size: 48px;
336
+ --qaid-icon-size: 24px;
337
+ --qaid-modal-width: 400px;
338
+ --qaid-backdrop-opacity: 0.3;
339
+ --qaid-font-family: system-ui, -apple-system, sans-serif;
340
+ --qaid-font-size: 16px;
341
+ }
342
+ ```
343
+
344
+ The embed also supports dark mode automatically via the `light-dark()` CSS function.
345
+
346
+ ## Hosted Dashboard
347
+
348
+ [Qaid](https://qaid.dev) provides a hosted dashboard for managing feedback collected by this embed:
349
+
350
+ - **Feedback inbox** with archiving and admin notes
351
+ - **Project management** with multiple API keys
352
+ - **Team collaboration** with owners, members, and invitations
353
+ - **Browser context** — console errors, viewport info, user agent captured with each submission
354
+ - **Screenshot viewing** for visual bug reports
355
+ - **Analytics** and data retention
356
+
357
+ ### Plans
358
+
359
+ | | Cadet (Free) | Commander ($9/mo) | Admiral (Enterprise) |
360
+ |---|---|---|---|
361
+ | Projects | 1 | Unlimited | Unlimited |
362
+ | Messages | 100/mo | Unlimited | Unlimited |
363
+ | Data retention | 7 days | 1 year | Unlimited |
364
+ | Screenshots | - | Yes | Yes |
365
+ | Notifications | - | Email/Text | Email/Text |
366
+ | Integrations | - | GitHub + more | Custom |
367
+
368
+ Sign up at [qaid.dev](https://qaid.dev). You can also self-host — the embed works with any endpoint that accepts the payload format described above.
369
+
370
+ ## Self-Hosting
371
+
372
+ The embed is endpoint-agnostic. Point it at your own server:
373
+
374
+ ```typescript
375
+ new FeedbackEmbed({
376
+ endpoint: 'https://your-server.com/api/feedback',
377
+ });
378
+ ```
379
+
380
+ Your server needs to handle:
381
+
382
+ 1. `POST /api/feedback` — Accept the feedback payload, return `{ "id": number }`
383
+ 2. `PATCH /api/feedback/:id` — Accept `{ "message": string | null }` or `{ "feedbackType": "up" | "down" }`
384
+
385
+ ## Project Badges
386
+
387
+ <a href="https://www.npmjs.com/package/@qaiddev/thumbs-embed">
388
+ <img alt="npm version" src="https://img.shields.io/npm/v/@qaiddev/thumbs-embed" />
389
+ </a>
390
+
391
+ <a href="https://github.com/qaiddev/thumbs-embed/blob/prod/LICENSE">
392
+ <img alt="license" src="https://img.shields.io/npm/l/@qaiddev/thumbs-embed"/>
393
+ </a>
package/dist/qaid.js CHANGED
@@ -5,7 +5,7 @@ const y = `<svg xmlns="http://www.w3.org/2000/svg" class="qaid-icon" fill="none"
5
5
  <path stroke-linecap="round" stroke-linejoin="round" d="M14 10h4.764a2 2 0 011.789 2.894l-3.5 7A2 2 0 0115.263 21h-4.017c-.163 0-.326-.02-.485-.06L7 20m7-10V5a2 2 0 00-2-2h-.095c-.5 0-.905.405-.905.905 0 .714-.211 1.412-.608 2.006L7 11v9m7-10h-2M7 20H5a2 2 0 01-2-2v-6a2 2 0 012-2h2.5"/>
6
6
  </svg>`, q = `<svg xmlns="http://www.w3.org/2000/svg" class="qaid-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
7
7
  <path stroke-linecap="round" stroke-linejoin="round" d="M10 14H5.236a2 2 0 01-1.789-2.894l3.5-7A2 2 0 018.737 3h4.018a2 2 0 01.485.06l3.76.94m-7 10v5a2 2 0 002 2h.095c.5 0 .905-.405.905-.905 0-.714.211-1.412.608-2.006L17 13V4m-7 10h2m5-10h2a2 2 0 012 2v6a2 2 0 01-2 2h-2.5"/>
8
- </svg>`, P = "body.qaid-targeting,body.qaid-targeting *{cursor:none!important}.qaid-widget,.qaid-widget *,.qaid-modal-container,.qaid-modal-container *{cursor:pointer!important}.qaid-widget.qaid-auto-container{position:fixed;z-index:50;display:flex;gap:.5rem}.qaid-widget.qaid-auto-container.qaid-bottom-right{bottom:1rem;right:1rem}.qaid-widget.qaid-auto-container.qaid-bottom-left{bottom:1rem;left:1rem}.qaid-widget.qaid-auto-container.qaid-top-right{top:1rem;right:1rem}.qaid-widget.qaid-auto-container.qaid-top-left{top:1rem;left:1rem}.qaid-widget.qaid-incognito{opacity:0;transition:opacity .2s ease-in-out}.qaid-widget.qaid-incognito:hover{opacity:1}.qaid-btn-structural{display:inline-flex;align-items:center;justify-content:center;cursor:pointer}.qaid-icon{width:24px;height:24px}.qaid-btn-structural:not(:has(svg)),.qaid-btn:not(:has(svg)){font-size:var(--qaid-icon-size, 24px);line-height:1}.qaid-emoji-icon{font-size:var(--qaid-icon-size, 24px);line-height:1}.qaid-tooltip-wrapper{position:relative}.qaid-tooltip-text{position:fixed;padding:.5rem .75rem;background:#1f2937;color:#fff;font-size:1rem;font-weight:600;border-radius:.5rem;white-space:nowrap;opacity:0;pointer-events:none;transition:opacity .15s;z-index:99999}.qaid-tooltip-text.qaid-tooltip-visible{opacity:1}.qaid-targeting-overlay{position:fixed;top:0;right:0;bottom:0;left:0;z-index:40;pointer-events:none}.qaid-capture-layer{position:fixed;top:0;right:0;bottom:0;left:0;cursor:crosshair;pointer-events:auto;z-index:9999}.qaid-banner{position:fixed;top:0;left:0;right:0;padding:.75rem 1rem;text-align:center;z-index:10000;font-family:var(--qaid-font-family, system-ui, -apple-system, sans-serif);font-size:var(--qaid-font-size, 16px);animation:qaid-slideDown .2s ease-out}.qaid-banner-up{background:color-mix(in srgb,var(--qaid-positive) 90%,transparent);color:#fff}.qaid-banner-down{background:color-mix(in srgb,var(--qaid-negative) 90%,transparent);color:#fff}.qaid-banner-text{font-weight:500}.qaid-banner-hint{margin-left:.5rem;opacity:.75}@keyframes qaid-slideDown{0%{transform:translateY(-100%)}to{transform:translateY(0)}}.qaid-vignette{position:fixed;top:0;right:0;bottom:0;left:0;pointer-events:none;background:radial-gradient(ellipse at center,transparent 30%,rgba(0,0,0,.3) 70%,rgba(0,0,0,.6) 100%);z-index:41}.qaid-crosshair-h,.qaid-crosshair-v{position:fixed;pointer-events:none;z-index:42}.qaid-crosshair-h{left:0;right:0;height:1px;background:color-mix(in srgb,var(--qaid-negative) 60%,transparent)}.qaid-crosshair-v{top:0;bottom:0;width:1px;background:color-mix(in srgb,var(--qaid-negative) 60%,transparent)}.qaid-type-up .qaid-crosshair-h,.qaid-type-up .qaid-crosshair-v{background:color-mix(in srgb,var(--qaid-positive) 60%,transparent)}.qaid-scope{position:fixed;width:80px;height:80px;pointer-events:none;z-index:43;transform:translate(-50%,-50%)}.qaid-scope-ring{position:absolute;top:10px;right:10px;bottom:10px;left:10px;border:2px solid color-mix(in srgb,var(--qaid-negative) 80%,transparent);border-radius:50%}.qaid-type-up .qaid-scope-ring{border-color:color-mix(in srgb,var(--qaid-positive) 80%,transparent)}.qaid-scope-ring-inner{position:absolute;top:20px;right:20px;bottom:20px;left:20px;border:1px solid color-mix(in srgb,var(--qaid-negative) 50%,transparent);border-radius:50%}.qaid-type-up .qaid-scope-ring-inner{border-color:color-mix(in srgb,var(--qaid-positive) 50%,transparent)}.qaid-scope-dot{position:absolute;top:50%;left:50%;width:4px;height:4px;background:var(--qaid-negative);border-radius:50%;transform:translate(-50%,-50%)}.qaid-type-up .qaid-scope-dot{background:var(--qaid-positive)}.qaid-highlight{outline:3px solid color-mix(in srgb,var(--qaid-negative) 80%,transparent)!important;outline-offset:2px!important;background-color:color-mix(in srgb,var(--qaid-negative) 10%,transparent)!important;transition:outline .1s ease,background-color .1s ease}body.qaid-targeting.qaid-type-up .qaid-highlight{outline-color:color-mix(in srgb,var(--qaid-positive) 80%,transparent)!important;background-color:color-mix(in srgb,var(--qaid-positive) 10%,transparent)!important}.qaid-selected-marker{position:fixed;border:3px solid var(--qaid-marker, #6366f1);border-radius:50%;pointer-events:none;z-index:44;animation:qaid-markerPulse 1.5s ease-in-out infinite}@keyframes qaid-markerPulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.7;transform:scale(1.05)}}.qaid-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:45;background:#0000004d}.qaid-modal-container{position:fixed;z-index:50;display:flex;flex-direction:column;align-items:flex-start;max-height:calc(100vh - 32px);font-family:var(--qaid-font-family, system-ui, -apple-system, sans-serif);font-size:var(--qaid-font-size, 16px)}.qaid-modal-container.qaid-above{flex-direction:column-reverse}.qaid-modal-arrow{width:0;height:0;border-left:12px solid transparent;border-right:12px solid transparent;position:relative;align-self:flex-start}.qaid-modal-container.qaid-below .qaid-modal-arrow{border-bottom:12px solid light-dark(#ffffff,#1f2937)}.qaid-modal-container.qaid-above .qaid-modal-arrow{border-top:12px solid light-dark(#ffffff,#1f2937)}.qaid-modal-box{color-scheme:inherit;background:light-dark(#ffffff,#1f2937);border-radius:1rem;padding:1.5rem;box-shadow:0 25px 50px -12px light-dark(rgba(0,0,0,.25),rgba(0,0,0,.5));width:var(--qaid-modal-width, 400px);max-width:calc(100vw - 32px);max-height:calc(100vh - 60px);overflow-y:auto}.qaid-modal-header{display:flex;align-items:flex-start;gap:.75rem;margin-bottom:1rem}.qaid-type-toggle{border:none;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:all .2s;flex-shrink:0}.qaid-type-toggle:hover{transform:scale(1.1)}.qaid-type-toggle:not(.qaid-type-toggle-custom){width:2.5rem;height:2.5rem;border-radius:50%}.qaid-type-toggle:not(.qaid-type-toggle-custom) svg{width:1.25rem;height:1.25rem}.qaid-type-toggle.qaid-type-up{background:var(--qaid-positive);color:#fff}.qaid-type-toggle.qaid-type-down{background:var(--qaid-negative);color:#fff}.qaid-modal-header-text{flex:1;min-width:0}.qaid-modal-title{font-size:1.125rem;font-weight:700;margin:0 0 .25rem;color:light-dark(#1f2937,#f9fafb)}.qaid-modal-subtitle{color:light-dark(#6b7280,#9ca3af);margin:0;font-size:.875rem}.qaid-textarea{width:100%;height:6rem;padding:.75rem;border:1px solid light-dark(#d1d5db,#374151);border-radius:.5rem;font-family:inherit;font-size:1rem;resize:vertical;margin-bottom:1rem;box-sizing:border-box;background:light-dark(#ffffff,#111827);color:light-dark(#1f2937,#f9fafb)}.qaid-textarea:focus{outline:none;border-color:var(--qaid-marker);box-shadow:0 0 0 3px color-mix(in srgb,var(--qaid-marker) 20%,transparent)}.qaid-btn-row{display:flex;gap:.5rem;justify-content:flex-end}.qaid-btn-submit{padding:.5rem 1rem;background:var(--qaid-marker);color:var(--qaid-marker-text, white);border:none;border-radius:.5rem;font-size:.875rem;font-weight:500;cursor:pointer;transition:background-color .2s,filter .2s}.qaid-btn-submit:hover{filter:brightness(.85)}.qaid-btn-submit:focus{outline:none;box-shadow:0 0 0 3px color-mix(in srgb,var(--qaid-marker) 30%,transparent)}.qaid-bottom-sheet{position:fixed;bottom:0;left:0;right:0;z-index:50;animation:qaid-slideUpSheet .3s ease-out;font-family:var(--qaid-font-family, system-ui, -apple-system, sans-serif);font-size:var(--qaid-font-size, 16px)}.qaid-bottom-sheet-content{background:light-dark(#ffffff,#1f2937);border-radius:1rem 1rem 0 0;padding:1.5rem;padding-bottom:max(1.5rem,env(safe-area-inset-bottom))}.qaid-bottom-sheet-handle{width:36px;height:4px;background:light-dark(rgba(0,0,0,.2),rgba(255,255,255,.2));border-radius:2px;margin:0 auto 1rem}@keyframes qaid-slideUpSheet{0%{transform:translateY(100%)}to{transform:translateY(0)}}";
8
+ </svg>`, P = "body.qaid-targeting,body.qaid-targeting *{cursor:none!important}.qaid-widget,.qaid-widget *,.qaid-modal-container,.qaid-modal-container *{cursor:pointer!important}.qaid-widget.qaid-auto-container{position:fixed;z-index:50;display:flex;gap:.5rem}.qaid-widget.qaid-auto-container.qaid-bottom-right{bottom:1rem;right:1rem}.qaid-widget.qaid-auto-container.qaid-bottom-left{bottom:1rem;left:1rem}.qaid-widget.qaid-auto-container.qaid-top-right{top:1rem;right:1rem}.qaid-widget.qaid-auto-container.qaid-top-left{top:1rem;left:1rem}.qaid-widget.qaid-incognito{opacity:0;transition:opacity .2s ease-in-out}.qaid-widget.qaid-incognito:hover{opacity:1}button.qaid-btn-structural{display:inline-flex;align-items:center;justify-content:center;cursor:pointer;-webkit-appearance:none;-moz-appearance:none;appearance:none}.qaid-icon{width:24px;height:24px}.qaid-btn-structural:not(:has(svg)),.qaid-btn:not(:has(svg)){font-size:var(--qaid-icon-size, 24px);line-height:1}.qaid-emoji-icon{font-size:var(--qaid-icon-size, 24px);line-height:1}.qaid-tooltip-wrapper{position:relative}.qaid-tooltip-text{position:fixed;padding:.5rem .75rem;background:#1f2937;color:#fff;font-size:1rem;font-weight:600;border-radius:.5rem;white-space:nowrap;opacity:0;pointer-events:none;transition:opacity .15s;z-index:99999}.qaid-tooltip-text.qaid-tooltip-visible{opacity:1}.qaid-targeting-overlay{position:fixed;top:0;right:0;bottom:0;left:0;z-index:40;pointer-events:none}.qaid-capture-layer{position:fixed;top:0;right:0;bottom:0;left:0;cursor:crosshair;pointer-events:auto;z-index:9999}.qaid-banner{position:fixed;top:0;left:0;right:0;padding:.75rem 1rem;text-align:center;z-index:10000;font-family:var(--qaid-font-family, system-ui, -apple-system, sans-serif);font-size:var(--qaid-font-size, 16px);animation:qaid-slideDown .2s ease-out}.qaid-banner-up{background:color-mix(in srgb,var(--qaid-positive) 90%,transparent);color:#fff}.qaid-banner-down{background:color-mix(in srgb,var(--qaid-negative) 90%,transparent);color:#fff}.qaid-banner-text{font-weight:500}.qaid-banner-hint{margin-left:.5rem;opacity:.75}@keyframes qaid-slideDown{0%{transform:translateY(-100%)}to{transform:translateY(0)}}.qaid-vignette{position:fixed;top:0;right:0;bottom:0;left:0;pointer-events:none;background:radial-gradient(ellipse at center,transparent 30%,rgba(0,0,0,.3) 70%,rgba(0,0,0,.6) 100%);z-index:41}.qaid-crosshair-h,.qaid-crosshair-v{position:fixed;pointer-events:none;z-index:42}.qaid-crosshair-h{left:0;right:0;height:1px;background:color-mix(in srgb,var(--qaid-negative) 60%,transparent)}.qaid-crosshair-v{top:0;bottom:0;width:1px;background:color-mix(in srgb,var(--qaid-negative) 60%,transparent)}.qaid-type-up .qaid-crosshair-h,.qaid-type-up .qaid-crosshair-v{background:color-mix(in srgb,var(--qaid-positive) 60%,transparent)}.qaid-scope{position:fixed;width:80px;height:80px;pointer-events:none;z-index:43;transform:translate(-50%,-50%)}.qaid-scope-ring{position:absolute;top:10px;right:10px;bottom:10px;left:10px;border:2px solid color-mix(in srgb,var(--qaid-negative) 80%,transparent);border-radius:50%}.qaid-type-up .qaid-scope-ring{border-color:color-mix(in srgb,var(--qaid-positive) 80%,transparent)}.qaid-scope-ring-inner{position:absolute;top:20px;right:20px;bottom:20px;left:20px;border:1px solid color-mix(in srgb,var(--qaid-negative) 50%,transparent);border-radius:50%}.qaid-type-up .qaid-scope-ring-inner{border-color:color-mix(in srgb,var(--qaid-positive) 50%,transparent)}.qaid-scope-dot{position:absolute;top:50%;left:50%;width:4px;height:4px;background:var(--qaid-negative);border-radius:50%;transform:translate(-50%,-50%)}.qaid-type-up .qaid-scope-dot{background:var(--qaid-positive)}.qaid-highlight{outline:3px solid color-mix(in srgb,var(--qaid-negative) 80%,transparent)!important;outline-offset:2px!important;background-color:color-mix(in srgb,var(--qaid-negative) 10%,transparent)!important;transition:outline .1s ease,background-color .1s ease}body.qaid-targeting.qaid-type-up .qaid-highlight{outline-color:color-mix(in srgb,var(--qaid-positive) 80%,transparent)!important;background-color:color-mix(in srgb,var(--qaid-positive) 10%,transparent)!important}.qaid-selected-marker{position:fixed;border:3px solid var(--qaid-marker, #6366f1);border-radius:50%;pointer-events:none;z-index:44;animation:qaid-markerPulse 1.5s ease-in-out infinite}@keyframes qaid-markerPulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.7;transform:scale(1.05)}}.qaid-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:45;background:#0000004d}.qaid-modal-container{position:fixed;z-index:50;display:flex;flex-direction:column;align-items:flex-start;max-height:calc(100vh - 32px);font-family:var(--qaid-font-family, system-ui, -apple-system, sans-serif);font-size:var(--qaid-font-size, 16px)}.qaid-modal-container.qaid-above{flex-direction:column-reverse}.qaid-modal-arrow{width:0;height:0;border-left:12px solid transparent;border-right:12px solid transparent;position:relative;align-self:flex-start}.qaid-modal-container.qaid-below .qaid-modal-arrow{border-bottom:12px solid light-dark(#ffffff,#1f2937)}.qaid-modal-container.qaid-above .qaid-modal-arrow{border-top:12px solid light-dark(#ffffff,#1f2937)}.qaid-modal-box{color-scheme:inherit;background:light-dark(#ffffff,#1f2937);border-radius:1rem;padding:1.5rem;box-shadow:0 25px 50px -12px light-dark(rgba(0,0,0,.25),rgba(0,0,0,.5));width:var(--qaid-modal-width, 400px);max-width:calc(100vw - 32px);max-height:calc(100vh - 60px);overflow-y:auto}.qaid-modal-header{display:flex;align-items:flex-start;gap:.75rem;margin-bottom:1rem}button.qaid-type-toggle{border:none;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:all .2s;flex-shrink:0;-webkit-appearance:none;-moz-appearance:none;appearance:none}button.qaid-type-toggle:hover{transform:scale(1.1)}button.qaid-type-toggle:not(.qaid-type-toggle-custom){width:2.5rem;height:2.5rem;border-radius:50%}button.qaid-type-toggle:not(.qaid-type-toggle-custom) svg{width:1.25rem;height:1.25rem}button.qaid-type-toggle.qaid-type-up{background:var(--qaid-positive);color:#fff}button.qaid-type-toggle.qaid-type-down{background:var(--qaid-negative);color:#fff}.qaid-modal-header-text{flex:1;min-width:0}.qaid-modal-title{font-size:1.125rem;font-weight:700;margin:0 0 .25rem;color:light-dark(#1f2937,#f9fafb)}.qaid-modal-subtitle{color:light-dark(#6b7280,#9ca3af);margin:0;font-size:.875rem}.qaid-textarea{width:100%;height:6rem;padding:.75rem;border:1px solid light-dark(#d1d5db,#374151);border-radius:.5rem;font-family:inherit;font-size:1rem;resize:vertical;margin-bottom:1rem;box-sizing:border-box;background:light-dark(#ffffff,#111827);color:light-dark(#1f2937,#f9fafb)}.qaid-textarea:focus{outline:none;border-color:var(--qaid-marker);box-shadow:0 0 0 3px color-mix(in srgb,var(--qaid-marker) 20%,transparent)}.qaid-btn-row{display:flex;gap:.5rem;justify-content:flex-end}button.qaid-btn-submit{padding:.5rem 1rem;background:var(--qaid-marker);color:var(--qaid-marker-text, white);border:none;border-radius:.5rem;font-size:.875rem;font-weight:500;cursor:pointer;transition:background-color .2s,filter .2s;-webkit-appearance:none;-moz-appearance:none;appearance:none}button.qaid-btn-submit:hover{filter:brightness(.85)}button.qaid-btn-submit:focus{outline:none;box-shadow:0 0 0 3px color-mix(in srgb,var(--qaid-marker) 30%,transparent)}.qaid-bottom-sheet{position:fixed;bottom:0;left:0;right:0;z-index:50;animation:qaid-slideUpSheet .3s ease-out;font-family:var(--qaid-font-family, system-ui, -apple-system, sans-serif);font-size:var(--qaid-font-size, 16px)}.qaid-bottom-sheet-content{background:light-dark(#ffffff,#1f2937);border-radius:1rem 1rem 0 0;padding:1.5rem;padding-bottom:max(1.5rem,env(safe-area-inset-bottom))}.qaid-bottom-sheet-handle{width:36px;height:4px;background:light-dark(rgba(0,0,0,.2),rgba(255,255,255,.2));border-radius:2px;margin:0 auto 1rem}@keyframes qaid-slideUpSheet{0%{transform:translateY(100%)}to{transform:translateY(0)}}";
9
9
  let k = !1;
10
10
  const U = {
11
11
  small: 36,
@@ -53,7 +53,7 @@ function R(i = {}) {
53
53
  backdropOpacity: s = 0.3,
54
54
  fontFamily: l = "system-ui, -apple-system, sans-serif",
55
55
  fontSize: r = 16
56
- } = i, c = U[n], p = W[n], f = Y(o), h = `:root{--qaid-positive:${t};--qaid-negative:${e};--qaid-marker:${o};--qaid-marker-text:${f};--qaid-btn-size:${c}px;--qaid-icon-size:${p}px;--qaid-modal-width:${a}px;--qaid-backdrop-opacity:${s};--qaid-font-family:${l};--qaid-font-size:${r}px}`, u = ".qaid-btn{width:var(--qaid-btn-size);height:var(--qaid-btn-size);border-radius:50%;border:none;background:#f3f4f6;color:#374151;display:flex;align-items:center;justify-content:center;cursor:pointer;box-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -1px rgba(0,0,0,.06);transition:background-color .2s,color .2s,transform .2s}.qaid-btn:hover{transform:scale(1.05)}.qaid-btn-up:hover{background:var(--qaid-positive);color:#fff}.qaid-btn-down:hover{background:var(--qaid-negative);color:#fff}.qaid-btn svg{width:var(--qaid-icon-size);height:var(--qaid-icon-size)}", m = document.createElement("style");
56
+ } = i, c = U[n], p = W[n], f = Y(o), h = `:root{--qaid-positive:${t};--qaid-negative:${e};--qaid-marker:${o};--qaid-marker-text:${f};--qaid-btn-size:${c}px;--qaid-icon-size:${p}px;--qaid-modal-width:${a}px;--qaid-backdrop-opacity:${s};--qaid-font-family:${l};--qaid-font-size:${r}px}`, u = "button.qaid-btn{width:var(--qaid-btn-size);height:var(--qaid-btn-size);border-radius:50%;border:none;background:#f3f4f6;color:#374151;display:flex;align-items:center;justify-content:center;cursor:pointer;box-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -1px rgba(0,0,0,.06);transition:background-color .2s,color .2s,transform .2s;-webkit-appearance:none;appearance:none}button.qaid-btn:hover{transform:scale(1.05)}button.qaid-btn-up:hover{background:var(--qaid-positive);color:#fff}button.qaid-btn-down:hover{background:var(--qaid-negative);color:#fff}button.qaid-btn svg{width:var(--qaid-icon-size);height:var(--qaid-icon-size)}", m = document.createElement("style");
57
57
  m.id = "qaid-styles", m.textContent = h + P + u, document.head.appendChild(m);
58
58
  }
59
59
  function K() {