@inputbuffer/feedback 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,6 +19,7 @@ This package is a lightweight embeddable widget you can drop into your documenta
19
19
  - [CDN](#cdn-recommended)
20
20
  - [npm](#npm)
21
21
  - [Browser support](#browser-support)
22
+ - [Authentication](#authentication)
22
23
  - [Reference](#reference)
23
24
  - [Feedback bar](#feedback-bar)
24
25
  - [Web component](#web-component)
@@ -33,7 +34,10 @@ This package is a lightweight embeddable widget you can drop into your documenta
33
34
  - [`instance.destroy()`](#instancedestroy)
34
35
  - [`instance.on(event, handler)`](#instanceonevent-handler)
35
36
  - [`InputBufferIO.version`](#inputbufferioversion)
36
- - [Target metadata schemas](#target-metadata-schemas)
37
+ - [Targets](#targets)
38
+ - [Target metadata schemas](#target-metadata-schemas)
39
+ - [Target attributes](#target-attributes)
40
+ - [Errors](#errors)
37
41
  - [CSS customization](#css-customization)
38
42
  - [Modal selectors](#modal-selectors)
39
43
  - [Bar selectors](#bar-selectors)
@@ -45,15 +49,15 @@ This package is a lightweight embeddable widget you can drop into your documenta
45
49
 
46
50
  ## Quick start
47
51
 
48
- Before you start you will need a widget API key from your [InputBuffer dashboard](https://inputbuffer.io).
52
+ Before you start you will need a **widget token** (`ibw_…`) from your [InputBuffer dashboard](https://inputbuffer.io) — see [Authentication](#authentication). Widget tokens are designed to be embedded in a browser; your full-access `ib_…` token is not and will be rejected.
49
53
 
50
54
  There are three bundles — pick the one that matches your use case:
51
55
 
52
56
  | Bundle | Size | What it includes |
53
57
  |---|---|---|
54
58
  | `bar.js` | 18 KB | Thumbs up/down bar with optional follow-up form |
55
- | `modal.js` | 19 KB | Full-text feedback modal |
56
- | `widget.js` | 36 KB | Both bar and modal |
59
+ | `modal.js` | 16 KB | Full-text feedback modal |
60
+ | `widget.js` | 32 KB | Both bar and modal |
57
61
 
58
62
  ### Inline thumbs bar (web component)
59
63
 
@@ -119,7 +123,7 @@ If you want a full-text feedback modal triggered by a button, load `modal.js` wi
119
123
  </script>
120
124
  ```
121
125
 
122
- When the page loads the widget connects to `#feedback-btn`. Clicking it opens a modal with a text field and optional email field.
126
+ When the page loads the widget connects to `#feedback-btn`. Clicking it opens a modal with a text field and an optional title field.
123
127
 
124
128
  ### Verify it's working
125
129
 
@@ -129,6 +133,7 @@ Submit a test message from your page and open your InputBuffer dashboard. It sho
129
133
 
130
134
  - **Nothing happens on click:** check that the `data-attach-to` selector matches your button's `id` exactly, including the `#`.
131
135
  - **401 error:** your `data-api-key` is invalid — try again with a fresh copy from the dashboard, and if it still fails [contact us](https://inputbuffer.io/contact).
136
+ - **403 error:** either you used a full-access `ib_…` token instead of a widget `ibw_…` token, or the page's origin is not on your token's allowlist. The widget logs the specific reason to the browser console — see [Errors](#errors).
132
137
 
133
138
  ### Where to go from here
134
139
 
@@ -145,12 +150,12 @@ Submit a test message from your page and open your InputBuffer dashboard. It sho
145
150
  Load only the component you need — each is roughly half the full bundle:
146
151
 
147
152
  ```html
148
- <!-- Full bundle (modal + bar) — 36 KB -->
153
+ <!-- Full bundle (modal + bar) — 32 KB -->
149
154
  <script src="https://cdn.jsdelivr.net/npm/@inputbuffer/feedback/dist/widget.js"
150
155
  data-api-key="YOUR_WIDGET_TOKEN">
151
156
  </script>
152
157
 
153
- <!-- Modal only — 19 KB -->
158
+ <!-- Modal only — 16 KB -->
154
159
  <script src="https://cdn.jsdelivr.net/npm/@inputbuffer/feedback/dist/modal.js"
155
160
  data-api-key="YOUR_WIDGET_TOKEN">
156
161
  </script>
@@ -161,7 +166,7 @@ Load only the component you need — each is roughly half the full bundle:
161
166
 
162
167
  > **SRI note:** For production deployments, add a Subresource Integrity `integrity` attribute to guard against CDN compromise. Generate the hash for each pinned version with:
163
168
  > ```bash
164
- > curl -s https://cdn.jsdelivr.net/npm/@inputbuffer/feedback@0.1.0/dist/widget.js | openssl dgst -sha384 -binary | openssl base64 -A
169
+ > curl -s https://cdn.jsdelivr.net/npm/@inputbuffer/feedback@0.3.0/dist/widget.js | openssl dgst -sha384 -binary | openssl base64 -A
165
170
  > ```
166
171
  > Then use `integrity="sha384-<hash>" crossorigin="anonymous"` on the `<script>` tag.
167
172
 
@@ -178,10 +183,10 @@ npm install @inputbuffer/feedback
178
183
  Import only what you use — your bundler will tree-shake the rest:
179
184
 
180
185
  ```js
181
- // Modal only (~19 KB unminified)
186
+ // Modal only (~16 KB minified)
182
187
  import { createModal } from '@inputbuffer/feedback/modal';
183
188
 
184
- // Bar only (~18 KB unminified)
189
+ // Bar only (~18 KB minified)
185
190
  import { createBar } from '@inputbuffer/feedback/bar';
186
191
 
187
192
  // Full bundle
@@ -196,6 +201,25 @@ Chrome 111+, Firefox 113+, Safari 16.2+. The bundles target ES2019 and rely on C
196
201
 
197
202
  ---
198
203
 
204
+ ## Authentication
205
+
206
+ InputBuffer issues two kinds of token, and this widget needs the browser-safe one.
207
+
208
+ | Token | Prefix | Where it belongs |
209
+ |---|---|---|
210
+ | Widget | `ibw_` | **Use this.** Safe to embed in a public page. Scoped to submitting feedback and reactions, nothing else. |
211
+ | Full access | `ib_` | Server-side only. Rejected with `403 widget-token-restricted` when sent from a browser. |
212
+
213
+ Create a widget token in **Settings → API Tokens** with the Widget scope, then add the origins your widget is embedded on to the token's allowlist. A request from an origin that isn't allowlisted is rejected with `403 forbidden-origin`.
214
+
215
+ Because the token is public, it can only create feedback and reactions — it cannot read your existing feedback, browse buffers, or touch anything else in your organization.
216
+
217
+ > **Privacy:** feedback text is sent to third-party AI services for classification and search, and is not scrubbed first. Don't prompt users for personal information, credentials, or production secrets.
218
+
219
+ The full API this widget speaks to is documented at [inputbuffer.io/docs/api](https://inputbuffer.io/docs/api/getting-started), with the machine-readable spec at [openapi.yaml](https://inputbuffer.io/docs/api/openapi.yaml).
220
+
221
+ ---
222
+
199
223
  ## Reference
200
224
 
201
225
  Each CDN bundle sets `window.InputBufferIO` at parse time — no `DOMContentLoaded` needed. What's exposed depends on which bundle you load:
@@ -226,8 +250,8 @@ Supported attributes:
226
250
 
227
251
  | Attribute | Description |
228
252
  |---|---|
229
- | `api-key` | **Required.** Your widget API key. |
230
- | `api-url` | Override the API endpoint. |
253
+ | `api-key` | **Required.** Your widget token (`ibw_…`). |
254
+ | `api-url` | Override the API base origin (e.g. `http://localhost:8080`). Paths are appended by the widget. |
231
255
  | `label` | Text shown next to the thumbs. |
232
256
  | `placement` | `"inline"` (default) or `"fixed"` (pins to bottom of viewport). |
233
257
  | `theme-primary` | Primary color. |
@@ -241,9 +265,21 @@ Supported attributes:
241
265
  | `modal-title` | Title shown above the feedback textarea. |
242
266
  | `modal-placeholder` | Placeholder text for the feedback textarea. |
243
267
  | `show-title-field` | `"true"` to show an optional title input. |
244
- | `show-email-field` | `"true"` to show an optional email input. |
245
- | `source` | Identifier for the feedback source. |
268
+ | `submitted-by` | Opaque identifier for whoever is submitting, e.g. your own user id. Sent as `submitted_by`. |
246
269
  | `user-id` | Stable user identifier for reaction deduplication. When set, only one reaction per user is recorded per target (all-time). When omitted, deduplication falls back to IP address with a 24-hour window. |
270
+ | `target-*` | Attaches the bar to a specific docs page, endpoint, or command. See [Target attributes](#target-attributes). |
271
+
272
+ Attach a target to record thumb votes against it:
273
+
274
+ ```html
275
+ <inputbuffer-feedback
276
+ api-key="ibw_YOUR_WIDGET_TOKEN"
277
+ label="Was this helpful?"
278
+ target-type="documentation">
279
+ </inputbuffer-feedback>
280
+ ```
281
+
282
+ Without a target, a thumb click still opens the follow-up form, but there is nothing for the reactions API to count the vote against.
247
283
 
248
284
  ### `InputBufferIO.createBar(config)`
249
285
 
@@ -259,8 +295,8 @@ document.getElementById('my-slot').appendChild(bar.element);
259
295
 
260
296
  | Property | Type | Default | Description |
261
297
  |---|---|---|---|
262
- | `apiKey` | string | — | **Required.** Your widget API key. |
263
- | `apiUrl` | string | | Override the API endpoint. |
298
+ | `apiKey` | string | — | **Required.** Your widget token (`ibw_…`). |
299
+ | `apiUrl` | string | `'https://inputbuffer.io'` | API base origin. Paths are appended by the widget. |
264
300
  | `label` | string | — | Text shown next to the thumbs. |
265
301
  | `showLabel` | boolean | `true` | Set to `false` to show thumbs only. |
266
302
  | `placement` | `'inline'` \| `'fixed'` | `'inline'` | `fixed` pins the bar to the bottom of the viewport. |
@@ -271,16 +307,12 @@ document.getElementById('my-slot').appendChild(bar.element);
271
307
  | `theme.text` | string | — | Text color. |
272
308
  | `theme.selected` | string | — | Background color of the selected thumb. |
273
309
  | `theme.selectedColor` | string | — | Icon color of the selected thumb. |
274
- | `target.type` | `'documentation'` \| `'rest_endpoint'` \| `'cli_command'` | — | The kind of thing the user is giving feedback on. Used for AI categorization. |
275
- | `target.targetId` | string | — | Optional stable ID for this target (used for deduplication on the server). |
276
- | `target.displayName` | string | — | Human-readable name shown in the InputBuffer dashboard (max 500 chars). |
277
- | `target.dedupKey` | string | — | Custom deduplication key (max 500 chars). |
278
- | `target.metadata` | object | — | Type-specific fields — see [Target metadata schemas](#target-metadata-schemas). |
310
+ | `target.type` | `'documentation'` \| `'rest_endpoint'` \| `'cli_command'` | — | The kind of thing the user is giving feedback on. Required to record thumb votes. |
311
+ | `target.metadata` | object | — | **Required** when `target` is set. Type-specific fields see [Target metadata schemas](#target-metadata-schemas). |
279
312
  | `modalTitle` | string | — | Heading for the follow-up popover. |
280
313
  | `modalPlaceholder` | string | — | Textarea placeholder for the follow-up popover. |
281
- | `showEmailField` | boolean | `false` | Show/hide the email field in the follow-up popover. |
282
314
  | `showTitleField` | boolean | `false` | Show/hide the title field in the follow-up popover. |
283
- | `source` | string | — | Tag identifying which of your surfaces this widget is embedded on (e.g. `"ios-app"`, `"docs-site"`). Stored on every submission for filtering in the dashboard. |
315
+ | `submittedBy` | string | — | Opaque identifier for whoever is submitting, e.g. your own user id or an anonymous token. Sent as `submitted_by` (max 300 chars). |
284
316
  | `userId` | string | — | Stable user identifier for reaction deduplication. When set, only one reaction per user is recorded per target (all-time). When omitted, deduplication falls back to IP address with a 24-hour window. |
285
317
  | `injectStyles` | boolean | `true` | Set to `false` to skip automatic style injection. |
286
318
 
@@ -298,7 +330,7 @@ bar.on('error', (err) => console.error('Submission failed:', err));
298
330
 
299
331
  | Event | Handler signature | When it fires |
300
332
  |---|---|---|
301
- | `vote` | `({ sentiment: 'positive' \| 'negative' }) => void` | User clicks a thumb. The reaction is recorded immediately via the reactions API (if a `target` is configured), and the selection is persisted in `localStorage` for 24 hours so it survives page reloads. |
333
+ | `vote` | `({ sentiment: 'positive' \| 'negative' }) => void` | User clicks a thumb. If a `target` is configured, the reaction is recorded immediately via `POST /api/v0/reactions` (fire-and-forget a failure never blocks the follow-up form). The selection is persisted in `localStorage` for 24 hours so it survives page reloads. |
302
334
  | `open` | `({ sentiment: 'positive' \| 'negative' }) => void` | The follow-up popover opens. |
303
335
  | `submit` | `({ id: string }) => void` | Feedback was submitted successfully. |
304
336
  | `close` | `() => void` | The follow-up popover closes. |
@@ -320,11 +352,16 @@ When `data-api-key` is present on the script tag, the widget initializes automat
320
352
 
321
353
  | Attribute | Type | Description |
322
354
  |---|---|---|
323
- | `data-api-key` | string | **Required.** Your widget API key. |
324
- | `data-api-url` | string | Override the API endpoint (useful for local dev/testing). |
355
+ | `data-api-key` | string | **Required.** Your widget token (`ibw_…`). |
356
+ | `data-api-url` | string | Override the API base origin (useful for local dev/testing). |
325
357
  | `data-attach-to` | string | CSS selector for the element that opens the modal on click. |
326
358
  | `data-inject-styles` | boolean | Set to `"false"` to skip automatic style injection. |
327
359
  | `data-color-scheme` | string | `"light"`, `"dark"`, or `"auto"`. |
360
+ | `data-title` | string | Modal heading. |
361
+ | `data-placeholder` | string | Textarea placeholder text. |
362
+ | `data-show-title-field` | boolean | `"true"` to show an optional title field. |
363
+ | `data-show-sentiment` | boolean | `"true"` to show thumbs up/down buttons in the modal. |
364
+ | `data-submitted-by` | string | Opaque identifier for whoever is submitting. Sent as `submitted_by`. |
328
365
  | `data-theme-primary` | string | Primary color (buttons, focus rings). Any CSS color value. |
329
366
  | `data-theme-background` | string | Modal background color. |
330
367
  | `data-theme-text` | string | Modal text color. |
@@ -345,16 +382,15 @@ const ib = InputBufferIO.createModal({
345
382
 
346
383
  | Property | Type | Default | Description |
347
384
  |---|---|---|---|
348
- | `apiKey` | string | — | **Required.** Your widget API key. |
349
- | `apiUrl` | string | | Override the API endpoint (useful for local dev/testing). |
385
+ | `apiKey` | string | — | **Required.** Your widget token (`ibw_…`). |
386
+ | `apiUrl` | string | `'https://inputbuffer.io'` | API base origin (useful for local dev/testing). Paths are appended by the widget. |
350
387
  | `attachTo` | string | — | CSS selector. Clicking the matched element calls `open()`. |
351
388
  | `injectStyles` | boolean | `true` | Set to `false` to skip automatic style injection. |
352
389
  | `title` | string | — | Modal heading. Omit to render no title. |
353
390
  | `placeholder` | string | `"What's on your mind?"` | Textarea placeholder text. |
354
- | `showEmailField` | boolean | `false` | Set to `true` to show an optional email field. |
355
391
  | `showTitleField` | boolean | `false` | Set to `true` to show an optional title field. |
356
392
  | `showSentiment` | boolean | `false` | Show thumbs up/down sentiment buttons in the modal. |
357
- | `source` | string | — | Tag identifying which of your surfaces this widget is embedded on (e.g. `"ios-app"`, `"docs-site"`). Stored on every submission for filtering in the dashboard. |
393
+ | `submittedBy` | string | — | Opaque identifier for whoever is submitting, e.g. your own user id or an anonymous token. Sent as `submitted_by` (max 300 chars). |
358
394
  | `colorScheme` | `'light'` \| `'dark'` \| `'auto'` | `'auto'` | Force a color scheme or follow the system setting. |
359
395
  | `theme.primary` | string | — | Primary color (buttons, focus rings). |
360
396
  | `theme.background` | string | — | Modal background color. |
@@ -372,8 +408,6 @@ ib.open({
372
408
  title: 'Was this helpful?',
373
409
  target: {
374
410
  type: 'documentation',
375
- targetId: 'auth-overview',
376
- displayName: 'Authentication overview',
377
411
  metadata: {
378
412
  page_url: window.location.href,
379
413
  section_heading: 'Authentication',
@@ -388,15 +422,11 @@ ib.open({
388
422
  | Property | Type | Description |
389
423
  |---|---|---|
390
424
  | `title` | string | Overrides the modal heading for this open call. |
391
- | `sentiment` | `'positive'` \| `'negative'` | Pre-selects a sentiment thumb. Only relevant when `showSentiment` is enabled. |
392
- | `target.type` | `'documentation'` \| `'rest_endpoint'` \| `'cli_command'` | The kind of thing the user is giving feedback on. Used for AI categorization. |
393
- | `target.targetId` | string | Optional stable ID for this target (used for deduplication on the server). |
394
- | `target.displayName` | string | Human-readable name shown in the InputBuffer dashboard (max 500 chars). |
395
- | `target.dedupKey` | string | Custom deduplication key (max 500 chars). |
396
- | `target.metadata` | object | Type-specific fields — see [Target metadata schemas](#target-metadata-schemas). |
397
- | `prefill.email` | string | Pre-populates the email field. |
425
+ | `sentiment` | `'positive'` \| `'negative'` | Pre-selects a sentiment thumb. Only relevant when `showSentiment` is enabled. Drives the UI only — the feedback API has no sentiment field. |
426
+ | `target.type` | `'documentation'` \| `'rest_endpoint'` \| `'cli_command'` | The kind of thing the user is giving feedback on. |
427
+ | `target.metadata` | object | **Required** when `target` is set. Type-specific fields see [Target metadata schemas](#target-metadata-schemas). |
398
428
  | `prefill.description` | string | Pre-populates the textarea. |
399
- | `source` | string | Overrides the `source` set in `createModal(config)` for this open call. |
429
+ | `submittedBy` | string | Overrides the `submittedBy` set in `createModal(config)` for this open call. |
400
430
 
401
431
  ### `instance.close()`
402
432
 
@@ -427,19 +457,26 @@ ib.on('error', (err) => console.error('Submission failed:', err));
427
457
  The currently loaded widget version string.
428
458
 
429
459
  ```js
430
- console.log(InputBufferIO.version); // e.g. "0.1.0"
460
+ console.log(InputBufferIO.version); // e.g. "0.3.0"
431
461
  ```
432
462
 
463
+ ---
464
+
465
+ ## Targets
466
+
467
+ A target is the thing feedback is *about* — a docs page, an API endpoint, a CLI command. Two targets of the same type with the same metadata are the same target, which is how InputBuffer groups feedback and counts reactions. You don't register targets ahead of time: send the type and metadata, and the API resolves an existing target or creates one.
468
+
469
+ Targets are also what make the thumbs bar's votes countable. Without a target, a thumb click opens the follow-up form but records no reaction.
470
+
433
471
  ### Target metadata schemas
434
472
 
435
- The fields accepted in `target.metadata` depend on `target.type`. The server validates required fields; the client does not enforce them.
473
+ `metadata` is required whenever you set a target, and the fields it accepts depend on `target.type`. The server rejects a target that is missing a required field with a `422`.
436
474
 
437
475
  **`documentation`**
438
476
 
439
477
  | Field | Required | Description |
440
478
  |---|---|---|
441
- | `page_url` | No | Full URL of the page. |
442
- | `page_slug` | No | Slug or path of the page. |
479
+ | `page_url` | **Yes** | URL of the documentation page. |
443
480
  | `section_heading` | No | Heading of the section the user is viewing. |
444
481
  | `doc_version` | No | Documentation version string. |
445
482
 
@@ -447,8 +484,8 @@ The fields accepted in `target.metadata` depend on `target.type`. The server val
447
484
 
448
485
  | Field | Required | Description |
449
486
  |---|---|---|
450
- | `method` | Yes* | HTTP method (`GET`, `POST`, etc.). |
451
- | `path` | Yes* | API path (e.g. `/v1/users`). |
487
+ | `method` | **Yes** | HTTP method (`GET`, `POST`, etc.). |
488
+ | `path` | **Yes** | API path (e.g. `/users/{id}`). |
452
489
  | `host` | No | Hostname (e.g. `api.example.com`). |
453
490
  | `api_version` | No | API version string. |
454
491
 
@@ -456,11 +493,83 @@ The fields accepted in `target.metadata` depend on `target.type`. The server val
456
493
 
457
494
  | Field | Required | Description |
458
495
  |---|---|---|
459
- | `command` | Yes* | Top-level CLI command (e.g. `auth`). |
460
- | `subcommand` | No | Subcommand (e.g. `setup`). |
496
+ | `command` | **Yes** | Top-level CLI command (e.g. `deploy`). |
497
+ | `subcommand` | No | Subcommand, which may be multi-word (e.g. `container run`). |
461
498
  | `cli_version` | No | CLI version string. |
499
+ | `args` | No | Documented flags and args, e.g. `['--rm', '--network']`. When feedback is submitted with more than one, each flag becomes its own target. |
500
+
501
+ ### Target attributes
462
502
 
463
- \* Required by the server; omitting them will result in a validation error response.
503
+ The `<inputbuffer-feedback>` element builds a target from `target-type` plus the metadata attributes for that type:
504
+
505
+ | Attribute | Applies to | Maps to |
506
+ |---|---|---|
507
+ | `target-type` | all | `type` — `documentation`, `rest_endpoint`, or `cli_command` |
508
+ | `target-page-url` | `documentation` | `page_url` — defaults to the current page URL |
509
+ | `target-section-heading` | `documentation` | `section_heading` |
510
+ | `target-doc-version` | `documentation` | `doc_version` |
511
+ | `target-method` | `rest_endpoint` | `method` |
512
+ | `target-path` | `rest_endpoint` | `path` |
513
+ | `target-host` | `rest_endpoint` | `host` |
514
+ | `target-api-version` | `rest_endpoint` | `api_version` |
515
+ | `target-command` | `cli_command` | `command` |
516
+ | `target-subcommand` | `cli_command` | `subcommand` |
517
+ | `target-cli-version` | `cli_command` | `cli_version` |
518
+ | `target-args` | `cli_command` | `args` — comma-separated |
519
+
520
+ Because `target-page-url` defaults to `window.location.href`, rating the current docs page needs one attribute:
521
+
522
+ ```html
523
+ <inputbuffer-feedback api-key="ibw_YOUR_WIDGET_TOKEN" target-type="documentation"></inputbuffer-feedback>
524
+ ```
525
+
526
+ An endpoint reference needs its identifying fields spelled out:
527
+
528
+ ```html
529
+ <inputbuffer-feedback
530
+ api-key="ibw_YOUR_WIDGET_TOKEN"
531
+ target-type="rest_endpoint"
532
+ target-method="POST"
533
+ target-path="/v1/uploads">
534
+ </inputbuffer-feedback>
535
+ ```
536
+
537
+ If a required attribute is missing the widget logs a warning and drops the target rather than sending a request the server would reject.
538
+
539
+ ---
540
+
541
+ ## Errors
542
+
543
+ API errors arrive as [RFC 7807 Problem Details](https://inputbuffer.io/docs/api/problems). The `error` event hands you an `ApiError` with `type`, `title`, `status`, `detail`, `category`, and (on validation failures) `field`. Switch on `type` rather than `status` — several types share a status code.
544
+
545
+ `category` decides what the user sees:
546
+
547
+ - **`user`** — they sent something the API rejected. `detail` is written for them and is shown in the form as-is.
548
+ - **`integration`** — your embed is misconfigured. The user sees a generic message, and the widget logs the real reason to the browser console.
549
+
550
+ | Problem type | Status | What went wrong |
551
+ |---|---|---|
552
+ | `unauthorized`, `invalid-token`, `invalid-token-format` | 401 | The token is missing, malformed, or revoked. |
553
+ | `widget-token-restricted` | 403 | A full-access `ib_…` token was used from a browser. Use a widget `ibw_…` token. |
554
+ | `forbidden-origin` | 403 | This page's origin isn't on the token's allowlist. |
555
+ | `usage-limit-reached` | 402 | The organization hit its lifetime feedback limit. |
556
+ | `missing-required-field`, `invalid-field-value` | 422 | A field is missing or invalid; `field` names it. |
557
+ | `rate-limited` | 429 | Too many requests. |
558
+ | `internal-error` | 500 | Something failed on InputBuffer's end. |
559
+
560
+ `ApiError` is exported from every entry point, so you can narrow with `instanceof`:
561
+
562
+ ```js
563
+ import { ApiError } from '@inputbuffer/feedback/modal';
564
+
565
+ ib.on('error', (err) => {
566
+ if (err instanceof ApiError && err.category === 'integration') {
567
+ console.error('Fix your embed:', err.type, err.detail);
568
+ }
569
+ });
570
+ ```
571
+
572
+ Network failures and the 10-second request timeout surface as ordinary `Error`s, not `ApiError`s.
464
573
 
465
574
  ---
466
575
 
@@ -478,7 +587,7 @@ Each component uses stable selectors you can target directly in your stylesheet.
478
587
  | `#ib-modal-body` | — | Body area |
479
588
  | `#ib-title` | `.ib-modal-title` | Modal heading |
480
589
  | `#ib-textarea` | `.ib-modal-textarea` | Feedback text field |
481
- | `#ib-email` | `.ib-modal-email` | Email input |
590
+ | `#ib-title-input` | `.ib-modal-title-input` | Optional title input |
482
591
  | `#ib-submit` | `.ib-modal-submit` | Submit button |
483
592
  | `#ib-close` | `.ib-modal-close` | Close button |
484
593
  | `#ib-success` | `.ib-modal-success` | Success message |
@@ -499,7 +608,7 @@ The IDs are the stable public API and will not change between releases. The `.ib
499
608
  | `.ib-bar-body` | Popover body |
500
609
  | `.ib-bar-title` | Popover heading |
501
610
  | `.ib-bar-textarea` | Feedback text field |
502
- | `.ib-bar-email` | Email input |
611
+ | `.ib-bar-title-input` | Optional title input |
503
612
  | `.ib-bar-submit` | Submit button |
504
613
  | `.ib-bar-success` | Success message |
505
614
  | `.ib-bar-error` | Error message |
@@ -537,8 +646,6 @@ document.getElementById('feedback-btn').addEventListener('click', () => {
537
646
  title: 'Was this page helpful?',
538
647
  target: {
539
648
  type: 'documentation',
540
- targetId: window.location.pathname,
541
- displayName: document.title,
542
649
  metadata: {
543
650
  page_url: window.location.href,
544
651
  section_heading: document.querySelector('h1')?.textContent ?? '',
@@ -557,8 +664,6 @@ const ib = InputBufferIO.createModal({ apiKey: 'YOUR_WIDGET_TOKEN' });
557
664
  ib.open({
558
665
  target: {
559
666
  type: 'rest_endpoint',
560
- targetId: 'POST /v1/uploads',
561
- displayName: 'Upload a file',
562
667
  metadata: { method: 'POST', path: '/v1/uploads' },
563
668
  },
564
669
  });
@@ -573,8 +678,6 @@ const ib = InputBufferIO.createModal({ apiKey: 'YOUR_WIDGET_TOKEN' });
573
678
  ib.open({
574
679
  target: {
575
680
  type: 'cli_command',
576
- targetId: 'deploy',
577
- displayName: 'my-cli deploy',
578
681
  metadata: { command: 'deploy' },
579
682
  },
580
683
  });
@@ -638,26 +741,31 @@ document.getElementById('my-slot').appendChild(bar.element);
638
741
  </script>
639
742
  ```
640
743
 
641
- ## `source` vs `target`
744
+ ### Identify who submitted feedback
642
745
 
643
- These are two separate concepts:
746
+ `submittedBy` is an opaque string — your own user id, or an anonymous token. It's stored exactly as sent and never parsed, so prefer an id over an email address.
644
747
 
645
- - **`source`** — *where* your widget is deployed. Identifies the platform or product surface,
646
- e.g. `"website"`, `"ios-app"`, `"chrome-extension"`. Use this to filter feedback by deployment
647
- environment in your dashboard.
748
+ ```js
749
+ InputBufferIO.createBar({
750
+ apiKey: 'ibw_YOUR_WIDGET_TOKEN',
751
+ target: { type: 'documentation', metadata: { page_url: window.location.href } },
752
+ submittedBy: currentUser.id, // attached to submitted feedback
753
+ userId: currentUser.id, // deduplicates thumb votes
754
+ });
755
+ ```
648
756
 
649
- - **`target`** *what* the feedback is about. A structured object describing the specific content
650
- or feature, e.g. a REST endpoint, a docs page, or a CLI command. Use this to group feedback by
651
- the thing being reviewed, regardless of where the widget is embedded.
757
+ `submittedBy` and `userId` do different jobs and can hold the same value. `submittedBy` records who wrote a piece of feedback; `userId` holds each person to one reaction per target for all time. Without `userId`, reactions fall back to one per target per IP address every 24 hours, which is best-effort — people behind a shared network can overwrite each other.
758
+
759
+ ### Point the widget at a local API
652
760
 
653
- You can use both together:
654
761
  ```js
655
- InputBufferIO.createBar({
656
- apiKey: 'YOUR_WIDGET_TOKEN',
657
- source: 'website',
658
- target: { type: 'documentation', metadata: { page_slug: 'getting-started' } },
762
+ InputBufferIO.createModal({
763
+ apiKey: 'ibw_YOUR_WIDGET_TOKEN',
764
+ apiUrl: 'http://localhost:8080',
659
765
  });
766
+ ```
660
767
 
768
+ `apiUrl` is a base origin, not an endpoint. The widget appends `/api/v0/feedback` and `/api/v0/reactions` itself.
661
769
 
662
770
  ---
663
771
 
package/dist/api.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import type { OpenOptions, TargetSpec } from './types.js';
1
+ import type { OpenOptions, ReactionResult, TargetSpec } from './types.js';
2
2
  export declare const WIDGET_VERSION: string;
3
- export declare function submitFeedback(apiKey: string, description: string, email: string | null, title: string | null, options?: OpenOptions, apiUrl?: string): Promise<{
3
+ export declare function submitFeedback(apiKey: string, description: string, title: string | null, options?: OpenOptions, apiUrl?: string): Promise<{
4
4
  id: string;
5
5
  }>;
6
- export declare function submitReaction(apiKey: string, reactionValue: 1 | -1, target: TargetSpec, userId?: string | null, apiUrl?: string): Promise<void>;
6
+ export declare function submitReaction(apiKey: string, reactionValue: 1 | -1, target: TargetSpec, userId?: string | null, apiUrl?: string): Promise<ReactionResult>;
@@ -0,0 +1,13 @@
1
+ import type { FeedbackBarConfig, TargetSpec, WidgetConfig } from './types.js';
2
+ /**
3
+ * Builds a target from `target-type` plus the metadata attributes for that type. The API
4
+ * requires a target's identifying fields, so a target missing one is dropped with a warning
5
+ * rather than sent for the server to reject.
6
+ */
7
+ export declare function targetFromAttributes(el: Element): TargetSpec | undefined;
8
+ /** Reads a complete bar config off an `<inputbuffer-feedback>` element. */
9
+ export declare function barConfigFromElement(el: Element, apiKey: string): FeedbackBarConfig;
10
+ /** True unless `inject-styles="false"` — absent means inject. */
11
+ export declare function shouldInjectStyles(el: Element): boolean;
12
+ /** Reads a modal config off the `data-*` attributes of the loading `<script>` tag. */
13
+ export declare function modalConfigFromScript(script: HTMLScriptElement, apiKey: string): WidgetConfig;
@@ -5,4 +5,6 @@ declare const InputBufferIO: {
5
5
  version: string;
6
6
  };
7
7
  export type { FeedbackBarConfig, FeedbackBarInstance };
8
+ export type { TargetSpec, RestEndpointTarget, DocumentationTarget, CliCommandTarget, TargetRef, ReactionResult, ProblemDetails, ProblemType, } from './types.js';
9
+ export { ApiError } from './types.js';
8
10
  export { InputBufferIO, createBar };
package/dist/bar.css CHANGED
@@ -1 +1 @@
1
- .ib-bar-wrapper{--ib-primary: #6366f1;--ib-primary-hover: #4338ca;--ib-background: #f6f6f8;--ib-surface: #ffffff;--ib-text: #111827;--ib-muted: #8d99ae;--ib-border: var(--ib-primary);--ib-focus-color: var(--ib-primary);--ib-input-hover-border: var(--ib-border);--ib-selected: var(--ib-primary);--ib-selected-color: var(--ib-surface);--ib-radius: 2px;--ib-radius-input: 2px;position:relative;display:inline-block;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif}.ib-bar-wrapper--fixed{position:fixed;bottom:32px;right:32px;z-index:9998}.ib-bar{display:inline-flex;align-items:stretch;height:40px;width:100%;overflow:hidden;background-color:var(--ib-background);border:1px solid var(--ib-border);border-radius:2px;box-shadow:0 1px 4px #00000014;box-sizing:border-box}.ib-bar-popover{display:none;position:fixed;width:320px;border:1px solid var(--ib-border);border-radius:var(--ib-radius);box-shadow:0 1px 3px #0000001f;box-sizing:border-box;z-index:9999;overflow:hidden}.ib-bar-popover--visible{display:block}.ib-bar-header{background:var(--ib-surface);padding:12px 16px;border-bottom:1px solid var(--ib-border)}.ib-bar-body{background:var(--ib-background);padding:16px}.ib-bar-title{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.1em;color:var(--ib-text);margin:0}.ib-bar-textarea{width:100%;background:var(--ib-background);border:1px solid var(--ib-border);border-radius:var(--ib-radius-input);padding:10px 12px;font-size:14px;font-family:inherit;color:var(--ib-text);resize:vertical;min-height:80px;box-sizing:border-box;outline:none;display:block}.ib-bar-textarea:hover{border-color:var(--ib-input-hover-border)}.ib-bar-textarea:focus{border-color:var(--ib-focus-color);box-shadow:0 0 0 2px color-mix(in srgb,var(--ib-focus-color) 15%,transparent)}.ib-bar-wrapper .ib-bar-body .ib-bar-title-input,.ib-bar-wrapper .ib-bar-body .ib-bar-email{width:100%;background:var(--ib-background);border:1px solid var(--ib-border);border-radius:var(--ib-radius-input);padding:8px 12px;font-size:14px;font-family:inherit;color:var(--ib-text);box-sizing:border-box;outline:none;display:block}.ib-bar-title-input{margin-bottom:8px}.ib-bar-email{margin-top:8px}.ib-bar-wrapper .ib-bar-body .ib-bar-title-input:hover,.ib-bar-wrapper .ib-bar-body .ib-bar-email:hover{border-color:var(--ib-input-hover-border)}.ib-bar-wrapper .ib-bar-body .ib-bar-title-input:focus,.ib-bar-wrapper .ib-bar-body .ib-bar-email:focus{border-color:var(--ib-focus-color);box-shadow:0 0 0 2px color-mix(in srgb,var(--ib-focus-color) 15%,transparent)}.ib-bar-submit{background:var(--ib-primary);color:#fff;border:none;border-radius:var(--ib-radius-input);padding:8px 16px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.08em;font-family:inherit;cursor:pointer;display:inline-block;transition:background .15s}.ib-bar-submit:hover{background:var(--ib-primary-hover)}.ib-bar-submit:disabled{opacity:.6;cursor:not-allowed}.ib-bar-footer{display:flex;align-items:center;justify-content:space-between;margin-top:10px}.ib-branding a{font-size:10px;color:var(--ib-muted);text-decoration:none;letter-spacing:.03em}.ib-branding a:hover{color:var(--ib-primary);text-decoration:underline}.ib-bar-error,.ib-bar-success{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.08em;margin:8px 0 0}.ib-bar-error:empty,.ib-bar-success:empty{display:none}.ib-bar-error{color:#dc2626}.ib-bar-success{color:#16a34a}.ib-bar-label-area{flex:1;display:flex;align-items:center;padding:0 16px}.ib-bar-label{font-size:11px;font-weight:600;color:var(--ib-muted);text-transform:uppercase;letter-spacing:.1em;white-space:nowrap;user-select:none}.ib-bar-actions{display:flex;align-items:stretch;background:var(--ib-background);border-left:1px solid var(--ib-border)}.ib-bar-actions--no-label{border-left:none}.ib-bar-btn{width:40px;display:flex;align-items:center;justify-content:center;background:var(--ib-background);border:none;border-right:1px solid var(--ib-border);border-radius:0;color:var(--ib-primary);cursor:pointer;transition:background .15s,color .15s,transform .1s;padding:0;-webkit-appearance:none;appearance:none;box-sizing:border-box;margin:0}.ib-bar-btn--down{border-right:none}.ib-bar-btn:hover{background:var(--ib-primary);color:var(--ib-surface);transform:scale(1.15)}.ib-bar-btn:active{transform:scale(.95)}.ib-bar-btn:focus{outline:1px solid var(--ib-primary);outline-offset:-1px}.ib-bar-btn--active{background:var(--ib-selected);color:var(--ib-selected-color)}.ib-bar-btn--active:hover{background:var(--ib-primary-hover, var(--ib-primary));color:var(--ib-surface);transform:scale(1.1)}@media (prefers-color-scheme: dark){.ib-bar-wrapper:not(.ib-theme-light),.ib-bar-wrapper.ib-theme-dark{--ib-primary: #3A5244;--ib-primary-hover: #4E6857;--ib-background: #25272B;--ib-surface: #2C3630;--ib-text: #E5E7EB;--ib-muted: #9CA3AF;--ib-border: #363840;--ib-focus-color: #7B9B82;--ib-input-hover-border: rgba(255, 255, 255, .2);--ib-selected: #7B9B82;--ib-selected-color: #1a2420;--ib-radius: 8px;--ib-radius-input: 4px}}.ib-bar-wrapper.ib-theme-dark{--ib-primary: #3A5244;--ib-primary-hover: #4E6857;--ib-background: #25272B;--ib-surface: #2C3630;--ib-text: #E5E7EB;--ib-muted: #9CA3AF;--ib-border: #363840;--ib-focus-color: #7B9B82;--ib-input-hover-border: rgba(255, 255, 255, .2);--ib-selected: #7B9B82;--ib-selected-color: #1a2420;--ib-radius: 8px;--ib-radius-input: 4px}
1
+ .ib-bar-wrapper{--ib-primary: #6366f1;--ib-primary-hover: #4338ca;--ib-background: #f6f6f8;--ib-surface: #ffffff;--ib-text: #111827;--ib-muted: #8d99ae;--ib-border: var(--ib-primary);--ib-focus-color: var(--ib-primary);--ib-input-hover-border: var(--ib-border);--ib-selected: var(--ib-primary);--ib-selected-color: var(--ib-surface);--ib-radius: 2px;--ib-radius-input: 2px;position:relative;display:inline-block;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif}.ib-bar-wrapper--fixed{position:fixed;bottom:32px;right:32px;z-index:9998}.ib-bar{display:inline-flex;align-items:stretch;height:40px;width:100%;overflow:hidden;background-color:var(--ib-background);border:1px solid var(--ib-border);border-radius:2px;box-shadow:0 1px 4px #00000014;box-sizing:border-box}.ib-bar-popover{display:none;position:fixed;width:320px;border:1px solid var(--ib-border);border-radius:var(--ib-radius);box-shadow:0 1px 3px #0000001f;box-sizing:border-box;z-index:9999;overflow:hidden}.ib-bar-popover--visible{display:block}.ib-bar-header{background:var(--ib-surface);padding:12px 16px;border-bottom:1px solid var(--ib-border)}.ib-bar-body{background:var(--ib-background);padding:16px}.ib-bar-title{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.1em;color:var(--ib-text);margin:0}.ib-bar-textarea{width:100%;background:var(--ib-background);border:1px solid var(--ib-border);border-radius:var(--ib-radius-input);padding:10px 12px;font-size:14px;font-family:inherit;color:var(--ib-text);resize:vertical;min-height:80px;box-sizing:border-box;outline:none;display:block}.ib-bar-textarea:hover{border-color:var(--ib-input-hover-border)}.ib-bar-textarea:focus{border-color:var(--ib-focus-color);box-shadow:0 0 0 2px color-mix(in srgb,var(--ib-focus-color) 15%,transparent)}.ib-bar-wrapper .ib-bar-body .ib-bar-title-input{width:100%;background:var(--ib-background);border:1px solid var(--ib-border);border-radius:var(--ib-radius-input);padding:8px 12px;font-size:14px;font-family:inherit;color:var(--ib-text);box-sizing:border-box;outline:none;display:block}.ib-bar-title-input{margin-bottom:8px}.ib-bar-wrapper .ib-bar-body .ib-bar-title-input:hover{border-color:var(--ib-input-hover-border)}.ib-bar-wrapper .ib-bar-body .ib-bar-title-input:focus{border-color:var(--ib-focus-color);box-shadow:0 0 0 2px color-mix(in srgb,var(--ib-focus-color) 15%,transparent)}.ib-bar-submit{background:var(--ib-primary);color:#fff;border:none;border-radius:var(--ib-radius-input);padding:8px 16px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.08em;font-family:inherit;cursor:pointer;display:inline-block;transition:background .15s}.ib-bar-submit:hover{background:var(--ib-primary-hover)}.ib-bar-submit:disabled{opacity:.6;cursor:not-allowed}.ib-bar-footer{display:flex;align-items:center;justify-content:space-between;margin-top:10px}.ib-branding a{font-size:10px;color:var(--ib-muted);text-decoration:none;letter-spacing:.03em}.ib-branding a:hover{color:var(--ib-primary);text-decoration:underline}.ib-bar-error,.ib-bar-success{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.08em;margin:8px 0 0}.ib-bar-error:empty,.ib-bar-success:empty{display:none}.ib-bar-error{color:#dc2626}.ib-bar-success{color:#16a34a}.ib-bar-label-area{flex:1;display:flex;align-items:center;padding:0 16px}.ib-bar-label{font-size:11px;font-weight:600;color:var(--ib-muted);text-transform:uppercase;letter-spacing:.1em;white-space:nowrap;user-select:none}.ib-bar-actions{display:flex;align-items:stretch;background:var(--ib-background);border-left:1px solid var(--ib-border)}.ib-bar-actions--no-label{border-left:none}.ib-bar-btn{width:40px;display:flex;align-items:center;justify-content:center;background:var(--ib-background);border:none;border-right:1px solid var(--ib-border);border-radius:0;color:var(--ib-primary);cursor:pointer;transition:background .15s,color .15s,transform .1s;padding:0;-webkit-appearance:none;appearance:none;box-sizing:border-box;margin:0}.ib-bar-btn--down{border-right:none}.ib-bar-btn:hover{background:var(--ib-primary);color:var(--ib-surface);transform:scale(1.15)}.ib-bar-btn:active{transform:scale(.95)}.ib-bar-btn:focus{outline:1px solid var(--ib-primary);outline-offset:-1px}.ib-bar-btn--active{background:var(--ib-selected);color:var(--ib-selected-color)}.ib-bar-btn--active:hover{background:var(--ib-primary-hover, var(--ib-primary));color:var(--ib-surface);transform:scale(1.1)}@media(prefers-color-scheme:dark){.ib-bar-wrapper:not(.ib-theme-light),.ib-bar-wrapper.ib-theme-dark{--ib-primary: #3A5244;--ib-primary-hover: #4E6857;--ib-background: #25272B;--ib-surface: #2C3630;--ib-text: #E5E7EB;--ib-muted: #9CA3AF;--ib-border: #363840;--ib-focus-color: #7B9B82;--ib-input-hover-border: rgba(255, 255, 255, .2);--ib-selected: #7B9B82;--ib-selected-color: #1a2420;--ib-radius: 8px;--ib-radius-input: 4px}}.ib-bar-wrapper.ib-theme-dark{--ib-primary: #3A5244;--ib-primary-hover: #4E6857;--ib-background: #25272B;--ib-surface: #2C3630;--ib-text: #E5E7EB;--ib-muted: #9CA3AF;--ib-border: #363840;--ib-focus-color: #7B9B82;--ib-input-hover-border: rgba(255, 255, 255, .2);--ib-selected: #7B9B82;--ib-selected-color: #1a2420;--ib-radius: 8px;--ib-radius-input: 4px}
package/dist/bar.esm.js CHANGED
@@ -1,4 +1,4 @@
1
- var C=class extends Error{constructor(i){super(i.detail),this.name="ApiError",this.type=i.type,this.status=i.status,this.detail=i.detail,this.category=i.category,this.field=i.field}};var N="0.2.0",ee="https://inputbuffer.io/api/v0/inputs";async function W(t,i,s,g,d,h){var p,u,v,n;let l={description:i};if(g&&(l.title=g),s&&(l.contact_email=s),d!=null&&d.source&&(l.source=d.source),d!=null&&d.target){let o=d.target;l.targets=[{type:o.type,metadata:o.metadata}]}let a=new AbortController,x=setTimeout(()=>a.abort(),1e4),r;try{r=await fetch(h!=null?h:ee,{method:"POST",headers:{Authorization:`Bearer ${t}`,"Content-Type":"application/json","X-IB-Client":`inputbuffer-widget/${N} (javascript)`},body:JSON.stringify(l),credentials:"omit",signal:a.signal})}finally{clearTimeout(x)}if(!r.ok){let o=await r.json().catch(()=>({}));throw new C({type:(p=o.type)!=null?p:"https://inputbuffer.io/problems/internal-error",title:(u=o.title)!=null?u:"Error",detail:(v=o.detail)!=null?v:"Submission failed. Please try again.",status:(n=o.status)!=null?n:r.status,category:o.category,field:o.field})}let{data:c}=await r.json();return{id:c.id}}var te="https://inputbuffer.io/api/v0/reactions";async function U(t,i,s,g,d){var c,p,u,v;let h=d?d.replace(/\/inputs$/,"/reactions"):te,l={reaction_value:i,target:{type:s.type,metadata:s.metadata}};g&&(l.user_id=g);let a=new AbortController,x=setTimeout(()=>a.abort(),1e4),r;try{r=await fetch(h,{method:"POST",headers:{Authorization:`Bearer ${t}`,"Content-Type":"application/json","X-IB-Client":`inputbuffer-widget/${N} (javascript)`},body:JSON.stringify(l),credentials:"omit",signal:a.signal})}finally{clearTimeout(x)}if(!r.ok){let n=await r.json().catch(()=>({}));throw new C({type:(c=n.type)!=null?c:"https://inputbuffer.io/problems/internal-error",title:(p=n.title)!=null?p:"Error",detail:(u=n.detail)!=null?u:"Submission failed. Please try again.",status:(v=n.status)!=null?v:r.status,category:n.category,field:n.field})}}function V(t){let i=document.createElementNS("http://www.w3.org/2000/svg","svg");i.setAttribute("viewBox","0 0 24 24"),i.setAttribute("width","18"),i.setAttribute("height","18"),i.setAttribute("fill","currentColor"),i.setAttribute("aria-hidden","true"),i.style.display="block";let s=document.createElementNS("http://www.w3.org/2000/svg","path");return s.setAttribute("d",t),i.appendChild(s),i}var re="M9 21h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2zM9 9l4.34-4.34L12 10h9v2l-3 7H9V9zM1 9h2v12H1z",ie="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L10.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm0 12l-4.34 4.34L12 14H3v-2l3-7h9v10zm4-12h2v12h-2z",ae=24*60*60*1e3;function O(t){var $,K;let i;function s(){let e=t.target;return e?`ib:reaction:${t.apiKey}:${e.type}:${JSON.stringify(e.metadata)}`:`ib:reaction:${t.apiKey}:${window.location.pathname}`}function g(){try{let e=localStorage.getItem(s());if(!e)return null;let{sentiment:f,ts:m}=JSON.parse(e);return Date.now()-m>ae?(localStorage.removeItem(s()),null):f}catch{return null}}function d(e){try{localStorage.setItem(s(),JSON.stringify({sentiment:e,ts:Date.now()}))}catch{}}let h={vote:[],open:[],submit:[],close:[],error:[]};function l(e,f){var m;(m=h[e])==null||m.forEach(B=>B(f))}let a=document.createElement("div"),x=t.colorScheme==="dark"?" ib-theme-dark":t.colorScheme==="light"?" ib-theme-light":"";a.className="ib-bar-wrapper"+(t.placement==="fixed"?" ib-bar-wrapper--fixed":"")+x;let{theme:r={}}=t;r.primary&&a.style.setProperty("--ib-primary",r.primary),r.background&&a.style.setProperty("--ib-background",r.background),r.surface&&a.style.setProperty("--ib-surface",r.surface),r.text&&a.style.setProperty("--ib-text",r.text),r.selected&&a.style.setProperty("--ib-selected",r.selected),r.selectedColor&&a.style.setProperty("--ib-selected-color",r.selectedColor);let c=document.createElement("div");c.className="ib-bar";let p=document.createElement("div");p.className="ib-bar-label-area";let u=document.createElement("span");u.className="ib-bar-label",u.textContent=($=t.label)!=null?$:"Was this helpful?",p.appendChild(u);let v=document.createElement("div");v.className="ib-bar-actions"+(t.showLabel===!1?" ib-bar-actions--no-label":"");let n=document.createElement("button");n.type="button",n.className="ib-bar-btn ib-bar-btn--up",n.setAttribute("aria-label","Yes"),n.appendChild(V(re));let o=document.createElement("button");o.type="button",o.className="ib-bar-btn ib-bar-btn--down",o.setAttribute("aria-label","No"),o.appendChild(V(ie)),v.appendChild(n),v.appendChild(o),t.showLabel!==!1&&c.appendChild(p),c.appendChild(v);let b=document.createElement("div");b.className="ib-bar-popover",b.setAttribute("role","dialog"),b.setAttribute("aria-modal","false");let F=document.createElement("div");if(F.className="ib-bar-header",t.modalTitle){b.setAttribute("aria-labelledby","ib-bar-title");let e=document.createElement("p");e.id="ib-bar-title",e.className="ib-bar-title",e.textContent=t.modalTitle,F.appendChild(e)}else b.setAttribute("aria-label","Feedback");let w=document.createElement("div");w.className="ib-bar-body";let k=document.createElement("textarea");k.className="ib-bar-textarea",k.placeholder=(K=t.modalPlaceholder)!=null?K:"What's on your mind?",k.setAttribute("aria-label","Feedback");let S=document.createElement("p");S.className="ib-bar-error";let I=document.createElement("p");I.className="ib-bar-success";let y=document.createElement("button");if(y.type="button",y.className="ib-bar-submit",y.textContent="Send feedback",t.showTitleField===!0){let e=document.createElement("input");e.type="text",e.className="ib-bar-title-input",e.placeholder="Title (optional)",e.setAttribute("aria-label","Feedback title"),w.appendChild(e)}if(w.appendChild(k),t.showEmailField===!0){let e=document.createElement("input");e.type="email",e.className="ib-bar-email",e.placeholder="Your email (optional)",e.setAttribute("aria-label","Email address"),w.appendChild(e)}w.appendChild(S),w.appendChild(I);let L=document.createElement("div");L.className="ib-bar-footer",L.appendChild(y);let P=document.createElement("div");P.className="ib-branding";let T=document.createElement("a");T.href="https://inputbuffer.io",T.target="_blank",T.rel="noopener noreferrer",T.textContent="Powered by inputbuffer.io",P.appendChild(T),L.appendChild(P),w.appendChild(L),b.appendChild(F),b.appendChild(w),a.appendChild(b),a.appendChild(c);function A(){let e=c.getBoundingClientRect();b.style.top=`${e.top-b.offsetHeight-8}px`,b.style.left=`${e.right-320}px`}function q(e){i=e,b.classList.add("ib-bar-popover--visible"),requestAnimationFrame(A),S.textContent="",I.textContent="",k.focus(),setTimeout(()=>document.addEventListener("click",j),0),document.addEventListener("keydown",D),window.addEventListener("scroll",A,{passive:!0}),window.addEventListener("resize",A,{passive:!0}),l("open",{sentiment:e})}function _(){b.classList.contains("ib-bar-popover--visible")&&(b.classList.remove("ib-bar-popover--visible"),document.removeEventListener("click",j),document.removeEventListener("keydown",D),window.removeEventListener("scroll",A),window.removeEventListener("resize",A),k.value="",l("close"))}function X(){n.classList.remove("ib-bar-btn--active"),o.classList.remove("ib-bar-btn--active"),i=void 0}function j(e){a.contains(e.target)||_()}function D(e){e.key==="Escape"&&_()}async function Y(){let e=k.value.trim();if(e.length<10){S.textContent="Please enter at least 10 characters.";return}S.textContent="",y.disabled=!0,y.textContent="Sending\u2026";let f=b.querySelector(".ib-bar-title-input"),m=(f==null?void 0:f.value.trim())||null,B=b.querySelector(".ib-bar-email"),Q=(B==null?void 0:B.value.trim())||null;try{let E=await W(t.apiKey,e,Q,m,{sentiment:i,target:t.target,source:t.source},t.apiUrl);l("submit",E),I.textContent="Thanks for your feedback!",y.textContent="Send feedback",setTimeout(()=>{_(),X()},2e3)}catch(E){l("error",E instanceof Error?E:new Error("Something went wrong."));let Z=E instanceof C&&E.category==="user"?E.detail:"Something went wrong. Please try again.";S.textContent=Z,y.disabled=!1,y.textContent="Send feedback"}}function M(e){n.classList.remove("ib-bar-btn--active"),o.classList.remove("ib-bar-btn--active"),e.classList.add("ib-bar-btn--active")}let z=g();z&&(i=z,M(z==="positive"?n:o));function H(e,f){var m;d(e),M(f),q(e),l("vote",{sentiment:e}),t.target&&U(t.apiKey,e==="positive"?1:-1,t.target,(m=t.userId)!=null?m:null,t.apiUrl).catch(()=>{})}return n.addEventListener("click",()=>H("positive",n)),o.addEventListener("click",()=>H("negative",o)),y.addEventListener("click",Y),{element:a,on(e,f){var m;(m=h[e])==null||m.push(f)},destroy(){_(),a.remove()}}}var G=`/* InputBuffer Feedback Bar Styles
1
+ var x=class extends Error{constructor(t){super(t.detail),this.name="ApiError",this.type=t.type,this.title=t.title,this.status=t.status,this.detail=t.detail,this.category=t.category,this.field=t.field}};var K="Something went wrong. Please try again.";function V(e){return e instanceof x?e.category==="user"?e.detail:(e.category==="integration"&&w(`${e.title}: ${e.detail}`,e.type),K):K}function w(e,t){typeof console!="undefined"&&console.warn(`[inputbuffer] ${e}`,t!=null?t:"")}var z="0.3.1",ae="https://inputbuffer.io",se="/api/v0/feedback",de="/api/v0/reactions",ce=1e4,le=5e3,be=500,pe=300;function q(e,t){return`${(e!=null?e:ae).replace(/\/+$/,"")}${t}`}async function G(e,t,i){var b,a,p,d;let n=new AbortController,l=setTimeout(()=>n.abort(),ce),s;try{s=await fetch(e,{method:"POST",headers:{Authorization:`Bearer ${t}`,"Content-Type":"application/json","X-IB-Client":`inputbuffer-widget/${z} (javascript)`},body:JSON.stringify(i),credentials:"omit",signal:n.signal})}finally{clearTimeout(l)}if(!s.ok){let u=await s.json().catch(()=>({}));throw new x({type:(b=u.type)!=null?b:"https://inputbuffer.io/docs/api/problems/internal-error",title:(a=u.title)!=null?a:"Error",detail:(p=u.detail)!=null?p:"Submission failed. Please try again.",status:(d=u.status)!=null?d:s.status,category:u.category,field:u.field})}return s}async function J(e,t,i,n,l){let s={description:t.slice(0,le)};if(i&&(s.title=i.slice(0,be)),n!=null&&n.submittedBy&&(s.submitted_by=n.submittedBy.slice(0,pe)),n!=null&&n.target){let p=n.target;s.targets=[{type:p.type,metadata:p.metadata}]}let b=await G(q(l,se),e,s),{data:a}=await b.json();return{id:a.id}}async function X(e,t,i,n,l){let s={reaction_value:t,target:{type:i.type,metadata:i.metadata}};n&&(s.user_id=n);let b;try{b=await G(q(l,de),e,s)}catch(p){throw p instanceof x&&p.status===422&&p.field==="user_id"&&w("Reaction rejected: no user_id and no client IP. Set the `userId` option (or the `user-id` attribute)."),p}let{data:a}=await b.json();return a}function Y(e){let t=document.createElementNS("http://www.w3.org/2000/svg","svg");t.setAttribute("viewBox","0 0 24 24"),t.setAttribute("width","18"),t.setAttribute("height","18"),t.setAttribute("fill","currentColor"),t.setAttribute("aria-hidden","true"),t.style.display="block";let i=document.createElementNS("http://www.w3.org/2000/svg","path");return i.setAttribute("d",e),t.appendChild(i),t}var ue="M9 21h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2zM9 9l4.34-4.34L12 10h9v2l-3 7H9V9zM1 9h2v12H1z",me="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L10.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm0 12l-4.34 4.34L12 14H3v-2l3-7h9v10zm4-12h2v12h-2z",fe=1440*60*1e3;function O(e){var $,W;let t;function i(){let r=e.target;return r?`ib:reaction:${e.apiKey}:${r.type}:${JSON.stringify(r.metadata)}`:`ib:reaction:${e.apiKey}:${window.location.pathname}`}function n(){try{let r=localStorage.getItem(i());if(!r)return null;let{sentiment:f,ts:m}=JSON.parse(r);return Date.now()-m>fe?(localStorage.removeItem(i()),null):f}catch{return null}}function l(r){try{localStorage.setItem(i(),JSON.stringify({sentiment:r,ts:Date.now()}))}catch{}}let s={vote:[],open:[],submit:[],close:[],error:[]};function b(r,f){var m;(m=s[r])==null||m.forEach(E=>E(f))}let a=document.createElement("div"),p=e.colorScheme==="dark"?" ib-theme-dark":e.colorScheme==="light"?" ib-theme-light":"";a.className="ib-bar-wrapper"+(e.placement==="fixed"?" ib-bar-wrapper--fixed":"")+p;let{theme:d={}}=e;d.primary&&a.style.setProperty("--ib-primary",d.primary),d.background&&a.style.setProperty("--ib-background",d.background),d.surface&&a.style.setProperty("--ib-surface",d.surface),d.text&&a.style.setProperty("--ib-text",d.text),d.selected&&a.style.setProperty("--ib-selected",d.selected),d.selectedColor&&a.style.setProperty("--ib-selected-color",d.selectedColor);let u=document.createElement("div");u.className="ib-bar";let F=document.createElement("div");F.className="ib-bar-label-area";let L=document.createElement("span");L.className="ib-bar-label",L.textContent=($=e.label)!=null?$:"Was this helpful?",F.appendChild(L);let B=document.createElement("div");B.className="ib-bar-actions"+(e.showLabel===!1?" ib-bar-actions--no-label":"");let g=document.createElement("button");g.type="button",g.className="ib-bar-btn ib-bar-btn--up",g.setAttribute("aria-label","Yes"),g.appendChild(Y(ue));let h=document.createElement("button");h.type="button",h.className="ib-bar-btn ib-bar-btn--down",h.setAttribute("aria-label","No"),h.appendChild(Y(me)),B.appendChild(g),B.appendChild(h),e.showLabel!==!1&&u.appendChild(F),u.appendChild(B);let c=document.createElement("div");c.className="ib-bar-popover",c.setAttribute("role","dialog"),c.setAttribute("aria-modal","false");let P=document.createElement("div");if(P.className="ib-bar-header",e.modalTitle){c.setAttribute("aria-labelledby","ib-bar-title");let r=document.createElement("p");r.id="ib-bar-title",r.className="ib-bar-title",r.textContent=e.modalTitle,P.appendChild(r)}else c.setAttribute("aria-label","Feedback");let k=document.createElement("div");k.className="ib-bar-body";let y=document.createElement("textarea");y.className="ib-bar-textarea",y.placeholder=(W=e.modalPlaceholder)!=null?W:"What's on your mind?",y.maxLength=5e3,y.setAttribute("aria-label","Feedback");let C=document.createElement("p");C.className="ib-bar-error";let _=document.createElement("p");_.className="ib-bar-success";let v=document.createElement("button");if(v.type="button",v.className="ib-bar-submit",v.textContent="Send feedback",e.showTitleField===!0){let r=document.createElement("input");r.type="text",r.className="ib-bar-title-input",r.placeholder="Title (optional)",r.maxLength=500,r.setAttribute("aria-label","Feedback title"),k.appendChild(r)}k.appendChild(y),k.appendChild(C),k.appendChild(_);let A=document.createElement("div");A.className="ib-bar-footer",A.appendChild(v);let R=document.createElement("div");R.className="ib-branding";let T=document.createElement("a");T.href="https://inputbuffer.io",T.target="_blank",T.rel="noopener noreferrer",T.textContent="Powered by inputbuffer.io",R.appendChild(T),A.appendChild(R),k.appendChild(A),c.appendChild(P),c.appendChild(k),a.appendChild(c),a.appendChild(u);function S(){let r=u.getBoundingClientRect();c.style.top=`${r.top-c.offsetHeight-8}px`,c.style.left=`${r.right-320}px`}function ie(r){t=r,c.classList.add("ib-bar-popover--visible"),requestAnimationFrame(S),C.textContent="",_.textContent="",y.focus(),setTimeout(()=>document.addEventListener("click",M),0),document.addEventListener("keydown",D),window.addEventListener("scroll",S,{passive:!0}),window.addEventListener("resize",S,{passive:!0}),b("open",{sentiment:r})}function I(){c.classList.contains("ib-bar-popover--visible")&&(c.classList.remove("ib-bar-popover--visible"),document.removeEventListener("click",M),document.removeEventListener("keydown",D),window.removeEventListener("scroll",S),window.removeEventListener("resize",S),y.value="",b("close"))}function ne(){g.classList.remove("ib-bar-btn--active"),h.classList.remove("ib-bar-btn--active"),t=void 0}function M(r){a.contains(r.target)||I()}function D(r){r.key==="Escape"&&I()}async function oe(){let r=y.value.trim();if(r.length<10){C.textContent="Please enter at least 10 characters.";return}C.textContent="",v.disabled=!0,v.textContent="Sending\u2026";let f=c.querySelector(".ib-bar-title-input"),m=(f==null?void 0:f.value.trim())||null;try{let E=await J(e.apiKey,r,m,{sentiment:t,target:e.target,submittedBy:e.submittedBy},e.apiUrl);b("submit",E),_.textContent="Thanks for your feedback!",v.textContent="Send feedback",setTimeout(()=>{I(),ne()},2e3)}catch(E){b("error",E instanceof Error?E:new Error("Something went wrong.")),C.textContent=V(E),v.disabled=!1,v.textContent="Send feedback"}}function H(r){g.classList.remove("ib-bar-btn--active"),h.classList.remove("ib-bar-btn--active"),r.classList.add("ib-bar-btn--active")}let N=n();N&&(t=N,H(N==="positive"?g:h));function U(r,f){var m;l(r),H(f),ie(r),b("vote",{sentiment:r}),e.target&&X(e.apiKey,r==="positive"?1:-1,e.target,(m=e.userId)!=null?m:null,e.apiUrl).catch(()=>{})}return g.addEventListener("click",()=>U("positive",g)),h.addEventListener("click",()=>U("negative",h)),v.addEventListener("click",oe),{element:a,on(r,f){var m;(m=s[r])==null||m.push(f)},destroy(){I(),a.remove()}}}function o(e,t){var i;return(i=e.getAttribute(t))!=null?i:void 0}function Q(e,t){let i=e.getAttribute(t);return i===null?void 0:i==="true"}function ge(e){return{primary:o(e,"theme-primary"),background:o(e,"theme-background"),text:o(e,"theme-text"),selected:o(e,"theme-selected"),selectedColor:o(e,"theme-selected-color")}}function he(e){var i;let t=e.getAttribute("target-type");if(t)switch(t){case"documentation":{let n=(i=o(e,"target-page-url"))!=null?i:typeof window!="undefined"?window.location.href:void 0;if(!n){w('target-type="documentation" needs target-page-url. Ignoring the target.');return}return{type:"documentation",metadata:{page_url:n,section_heading:o(e,"target-section-heading"),doc_version:o(e,"target-doc-version")}}}case"rest_endpoint":{let n=o(e,"target-method"),l=o(e,"target-path");if(!n||!l){w('target-type="rest_endpoint" needs target-method and target-path. Ignoring the target.');return}return{type:"rest_endpoint",metadata:{method:n,path:l,host:o(e,"target-host"),api_version:o(e,"target-api-version")}}}case"cli_command":{let n=o(e,"target-command");if(!n){w('target-type="cli_command" needs target-command. Ignoring the target.');return}let l=o(e,"target-args");return{type:"cli_command",metadata:{command:n,subcommand:o(e,"target-subcommand"),cli_version:o(e,"target-cli-version"),args:l?l.split(",").map(s=>s.trim()).filter(Boolean):void 0}}}default:w(`Unknown target-type "${t}". Expected documentation, rest_endpoint, or cli_command.`);return}}function Z(e,t){return{apiKey:t,apiUrl:o(e,"api-url"),label:o(e,"label"),showLabel:Q(e,"show-label"),placement:e.getAttribute("placement")==="fixed"?"fixed":"inline",colorScheme:o(e,"color-scheme"),modalTitle:o(e,"modal-title"),modalPlaceholder:o(e,"modal-placeholder"),showTitleField:Q(e,"show-title-field"),submittedBy:o(e,"submitted-by"),userId:o(e,"user-id"),target:he(e),theme:ge(e)}}function ee(e){return e.getAttribute("inject-styles")!=="false"}var te=`/* InputBuffer Feedback Bar Styles
2
2
  * .ib-bar-* and .ib-bar-popover* classes are stable public API \u2014 do not rename them.
3
3
  * Developers may target these classes from their own stylesheets to customize the widget.
4
4
  */
@@ -106,8 +106,7 @@ var C=class extends Error{constructor(i){super(i.detail),this.name="ApiError",th
106
106
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--ib-focus-color) 15%, transparent);
107
107
  }
108
108
 
109
- .ib-bar-wrapper .ib-bar-body .ib-bar-title-input,
110
- .ib-bar-wrapper .ib-bar-body .ib-bar-email {
109
+ .ib-bar-wrapper .ib-bar-body .ib-bar-title-input {
111
110
  width: 100%;
112
111
  background: var(--ib-background);
113
112
  border: 1px solid var(--ib-border);
@@ -125,17 +124,11 @@ var C=class extends Error{constructor(i){super(i.detail),this.name="ApiError",th
125
124
  margin-bottom: 8px;
126
125
  }
127
126
 
128
- .ib-bar-email {
129
- margin-top: 8px;
130
- }
131
-
132
- .ib-bar-wrapper .ib-bar-body .ib-bar-title-input:hover,
133
- .ib-bar-wrapper .ib-bar-body .ib-bar-email:hover {
127
+ .ib-bar-wrapper .ib-bar-body .ib-bar-title-input:hover {
134
128
  border-color: var(--ib-input-hover-border);
135
129
  }
136
130
 
137
- .ib-bar-wrapper .ib-bar-body .ib-bar-title-input:focus,
138
- .ib-bar-wrapper .ib-bar-body .ib-bar-email:focus {
131
+ .ib-bar-wrapper .ib-bar-body .ib-bar-title-input:focus {
139
132
  border-color: var(--ib-focus-color);
140
133
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--ib-focus-color) 15%, transparent);
141
134
  }
@@ -319,4 +312,4 @@ var C=class extends Error{constructor(i){super(i.detail),this.name="ApiError",th
319
312
  --ib-radius: 8px;
320
313
  --ib-radius-input: 4px;
321
314
  }
322
- `;function J(){if(typeof document=="undefined"||document.getElementById("ib-bar-styles"))return;let t=document.createElement("style");t.id="ib-bar-styles",t.textContent=G,document.head.appendChild(t)}function oe(t){return t.injectStyles!==!1&&J(),O(t)}var R=class extends HTMLElement{constructor(){super(...arguments);this._bar=null}connectedCallback(){var l,a,x,r,c,p,u;let s=this.getAttribute("api-key");if(!s)return;let g=this.getAttribute("inject-styles");(g===null?!0:g!=="false")&&J();let h=this.getAttribute("placement");this._bar=O({apiKey:s,apiUrl:(l=this.getAttribute("api-url"))!=null?l:void 0,label:(a=this.getAttribute("label"))!=null?a:void 0,placement:h==="fixed"?"fixed":"inline",theme:{primary:(x=this.getAttribute("theme-primary"))!=null?x:void 0,background:(r=this.getAttribute("theme-background"))!=null?r:void 0,text:(c=this.getAttribute("theme-text"))!=null?c:void 0,selected:(p=this.getAttribute("theme-selected"))!=null?p:void 0,selectedColor:(u=this.getAttribute("theme-selected-color"))!=null?u:void 0}}),this.appendChild(this._bar.element)}disconnectedCallback(){var s;(s=this._bar)==null||s.destroy(),this._bar=null}};typeof customElements!="undefined"&&!customElements.get("inputbuffer-feedback")&&customElements.define("inputbuffer-feedback",R);var se={createBar:oe,version:N};typeof window!="undefined"&&(window.InputBufferIO=se);export{se as InputBufferIO,oe as createBar};
315
+ `;function re(){if(typeof document=="undefined"||document.getElementById("ib-bar-styles"))return;let e=document.createElement("style");e.id="ib-bar-styles",e.textContent=te,document.head.appendChild(e)}function ye(e){return e.injectStyles!==!1&&re(),O(e)}var j=class extends HTMLElement{constructor(){super(...arguments);this._bar=null}connectedCallback(){let i=this.getAttribute("api-key");i&&(ee(this)&&re(),this._bar=O(Z(this,i)),this.appendChild(this._bar.element))}disconnectedCallback(){var i;(i=this._bar)==null||i.destroy(),this._bar=null}};typeof customElements!="undefined"&&!customElements.get("inputbuffer-feedback")&&customElements.define("inputbuffer-feedback",j);var xe={createBar:ye,version:z};typeof window!="undefined"&&(window.InputBufferIO=xe);export{x as ApiError,xe as InputBufferIO,ye as createBar};