@inputbuffer/feedback 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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. |
@@ -236,8 +260,26 @@ Supported attributes:
236
260
  | `theme-selected` | Background color of the selected thumb. |
237
261
  | `theme-selected-color` | Icon color of the selected thumb. |
238
262
  | `inject-styles` | Set to `"false"` to skip automatic style injection. |
263
+ | `color-scheme` | `"light"`, `"dark"`, or `"auto"`. |
264
+ | `show-label` | `"true"` to show the label, `"false"` to hide it. |
265
+ | `modal-title` | Title shown above the feedback textarea. |
266
+ | `modal-placeholder` | Placeholder text for the feedback textarea. |
267
+ | `show-title-field` | `"true"` to show an optional title input. |
268
+ | `submitted-by` | Opaque identifier for whoever is submitting, e.g. your own user id. Sent as `submitted_by`. |
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
+ ```
239
281
 
240
- > **Note:** `colorScheme`, `showLabel`, `modalTitle`, `modalPlaceholder`, `showTitleField`, `showEmailField`, and `source` are not available as web component attributes. Use `InputBufferIO.createBar(config)` for those options.
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.
241
283
 
242
284
  ### `InputBufferIO.createBar(config)`
243
285
 
@@ -253,8 +295,8 @@ document.getElementById('my-slot').appendChild(bar.element);
253
295
 
254
296
  | Property | Type | Default | Description |
255
297
  |---|---|---|---|
256
- | `apiKey` | string | — | **Required.** Your widget API key. |
257
- | `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. |
258
300
  | `label` | string | — | Text shown next to the thumbs. |
259
301
  | `showLabel` | boolean | `true` | Set to `false` to show thumbs only. |
260
302
  | `placement` | `'inline'` \| `'fixed'` | `'inline'` | `fixed` pins the bar to the bottom of the viewport. |
@@ -265,16 +307,13 @@ document.getElementById('my-slot').appendChild(bar.element);
265
307
  | `theme.text` | string | — | Text color. |
266
308
  | `theme.selected` | string | — | Background color of the selected thumb. |
267
309
  | `theme.selectedColor` | string | — | Icon color of the selected thumb. |
268
- | `target.type` | `'documentation'` \| `'rest_endpoint'` \| `'cli_command'` | — | The kind of thing the user is giving feedback on. Used for AI categorization. |
269
- | `target.targetId` | string | — | Optional stable ID for this target (used for deduplication on the server). |
270
- | `target.displayName` | string | — | Human-readable name shown in the InputBuffer dashboard (max 500 chars). |
271
- | `target.dedupKey` | string | — | Custom deduplication key (max 500 chars). |
272
- | `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). |
273
312
  | `modalTitle` | string | — | Heading for the follow-up popover. |
274
313
  | `modalPlaceholder` | string | — | Textarea placeholder for the follow-up popover. |
275
- | `showEmailField` | boolean | `false` | Show/hide the email field in the follow-up popover. |
276
314
  | `showTitleField` | boolean | `false` | Show/hide the title field in the follow-up popover. |
277
- | `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). |
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. |
278
317
  | `injectStyles` | boolean | `true` | Set to `false` to skip automatic style injection. |
279
318
 
280
319
  ### `bar.on(event, handler)`
@@ -291,7 +330,7 @@ bar.on('error', (err) => console.error('Submission failed:', err));
291
330
 
292
331
  | Event | Handler signature | When it fires |
293
332
  |---|---|---|
294
- | `vote` | `({ sentiment: 'positive' \| 'negative' }) => void` | User clicks a thumb before submitting. |
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. |
295
334
  | `open` | `({ sentiment: 'positive' \| 'negative' }) => void` | The follow-up popover opens. |
296
335
  | `submit` | `({ id: string }) => void` | Feedback was submitted successfully. |
297
336
  | `close` | `() => void` | The follow-up popover closes. |
@@ -313,11 +352,16 @@ When `data-api-key` is present on the script tag, the widget initializes automat
313
352
 
314
353
  | Attribute | Type | Description |
315
354
  |---|---|---|
316
- | `data-api-key` | string | **Required.** Your widget API key. |
317
- | `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). |
318
357
  | `data-attach-to` | string | CSS selector for the element that opens the modal on click. |
319
358
  | `data-inject-styles` | boolean | Set to `"false"` to skip automatic style injection. |
320
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`. |
321
365
  | `data-theme-primary` | string | Primary color (buttons, focus rings). Any CSS color value. |
322
366
  | `data-theme-background` | string | Modal background color. |
323
367
  | `data-theme-text` | string | Modal text color. |
@@ -338,16 +382,15 @@ const ib = InputBufferIO.createModal({
338
382
 
339
383
  | Property | Type | Default | Description |
340
384
  |---|---|---|---|
341
- | `apiKey` | string | — | **Required.** Your widget API key. |
342
- | `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. |
343
387
  | `attachTo` | string | — | CSS selector. Clicking the matched element calls `open()`. |
344
388
  | `injectStyles` | boolean | `true` | Set to `false` to skip automatic style injection. |
345
389
  | `title` | string | — | Modal heading. Omit to render no title. |
346
390
  | `placeholder` | string | `"What's on your mind?"` | Textarea placeholder text. |
347
- | `showEmailField` | boolean | `false` | Set to `true` to show an optional email field. |
348
391
  | `showTitleField` | boolean | `false` | Set to `true` to show an optional title field. |
349
392
  | `showSentiment` | boolean | `false` | Show thumbs up/down sentiment buttons in the modal. |
350
- | `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). |
351
394
  | `colorScheme` | `'light'` \| `'dark'` \| `'auto'` | `'auto'` | Force a color scheme or follow the system setting. |
352
395
  | `theme.primary` | string | — | Primary color (buttons, focus rings). |
353
396
  | `theme.background` | string | — | Modal background color. |
@@ -365,8 +408,6 @@ ib.open({
365
408
  title: 'Was this helpful?',
366
409
  target: {
367
410
  type: 'documentation',
368
- targetId: 'auth-overview',
369
- displayName: 'Authentication overview',
370
411
  metadata: {
371
412
  page_url: window.location.href,
372
413
  section_heading: 'Authentication',
@@ -381,15 +422,11 @@ ib.open({
381
422
  | Property | Type | Description |
382
423
  |---|---|---|
383
424
  | `title` | string | Overrides the modal heading for this open call. |
384
- | `sentiment` | `'positive'` \| `'negative'` | Pre-selects a sentiment thumb. Only relevant when `showSentiment` is enabled. |
385
- | `target.type` | `'documentation'` \| `'rest_endpoint'` \| `'cli_command'` | The kind of thing the user is giving feedback on. Used for AI categorization. |
386
- | `target.targetId` | string | Optional stable ID for this target (used for deduplication on the server). |
387
- | `target.displayName` | string | Human-readable name shown in the InputBuffer dashboard (max 500 chars). |
388
- | `target.dedupKey` | string | Custom deduplication key (max 500 chars). |
389
- | `target.metadata` | object | Type-specific fields — see [Target metadata schemas](#target-metadata-schemas). |
390
- | `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). |
391
428
  | `prefill.description` | string | Pre-populates the textarea. |
392
- | `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. |
393
430
 
394
431
  ### `instance.close()`
395
432
 
@@ -420,19 +457,26 @@ ib.on('error', (err) => console.error('Submission failed:', err));
420
457
  The currently loaded widget version string.
421
458
 
422
459
  ```js
423
- console.log(InputBufferIO.version); // e.g. "0.1.0"
460
+ console.log(InputBufferIO.version); // e.g. "0.3.0"
424
461
  ```
425
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
+
426
471
  ### Target metadata schemas
427
472
 
428
- 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`.
429
474
 
430
475
  **`documentation`**
431
476
 
432
477
  | Field | Required | Description |
433
478
  |---|---|---|
434
- | `page_url` | No | Full URL of the page. |
435
- | `page_slug` | No | Slug or path of the page. |
479
+ | `page_url` | **Yes** | URL of the documentation page. |
436
480
  | `section_heading` | No | Heading of the section the user is viewing. |
437
481
  | `doc_version` | No | Documentation version string. |
438
482
 
@@ -440,8 +484,8 @@ The fields accepted in `target.metadata` depend on `target.type`. The server val
440
484
 
441
485
  | Field | Required | Description |
442
486
  |---|---|---|
443
- | `method` | Yes* | HTTP method (`GET`, `POST`, etc.). |
444
- | `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}`). |
445
489
  | `host` | No | Hostname (e.g. `api.example.com`). |
446
490
  | `api_version` | No | API version string. |
447
491
 
@@ -449,11 +493,83 @@ The fields accepted in `target.metadata` depend on `target.type`. The server val
449
493
 
450
494
  | Field | Required | Description |
451
495
  |---|---|---|
452
- | `command` | Yes* | Top-level CLI command (e.g. `auth`). |
453
- | `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`). |
454
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
502
+
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
+ ```
455
525
 
456
- \* Required by the server; omitting them will result in a validation error response.
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.
457
573
 
458
574
  ---
459
575
 
@@ -471,7 +587,7 @@ Each component uses stable selectors you can target directly in your stylesheet.
471
587
  | `#ib-modal-body` | — | Body area |
472
588
  | `#ib-title` | `.ib-modal-title` | Modal heading |
473
589
  | `#ib-textarea` | `.ib-modal-textarea` | Feedback text field |
474
- | `#ib-email` | `.ib-modal-email` | Email input |
590
+ | `#ib-title-input` | `.ib-modal-title-input` | Optional title input |
475
591
  | `#ib-submit` | `.ib-modal-submit` | Submit button |
476
592
  | `#ib-close` | `.ib-modal-close` | Close button |
477
593
  | `#ib-success` | `.ib-modal-success` | Success message |
@@ -492,7 +608,7 @@ The IDs are the stable public API and will not change between releases. The `.ib
492
608
  | `.ib-bar-body` | Popover body |
493
609
  | `.ib-bar-title` | Popover heading |
494
610
  | `.ib-bar-textarea` | Feedback text field |
495
- | `.ib-bar-email` | Email input |
611
+ | `.ib-bar-title-input` | Optional title input |
496
612
  | `.ib-bar-submit` | Submit button |
497
613
  | `.ib-bar-success` | Success message |
498
614
  | `.ib-bar-error` | Error message |
@@ -530,8 +646,6 @@ document.getElementById('feedback-btn').addEventListener('click', () => {
530
646
  title: 'Was this page helpful?',
531
647
  target: {
532
648
  type: 'documentation',
533
- targetId: window.location.pathname,
534
- displayName: document.title,
535
649
  metadata: {
536
650
  page_url: window.location.href,
537
651
  section_heading: document.querySelector('h1')?.textContent ?? '',
@@ -550,8 +664,6 @@ const ib = InputBufferIO.createModal({ apiKey: 'YOUR_WIDGET_TOKEN' });
550
664
  ib.open({
551
665
  target: {
552
666
  type: 'rest_endpoint',
553
- targetId: 'POST /v1/uploads',
554
- displayName: 'Upload a file',
555
667
  metadata: { method: 'POST', path: '/v1/uploads' },
556
668
  },
557
669
  });
@@ -566,8 +678,6 @@ const ib = InputBufferIO.createModal({ apiKey: 'YOUR_WIDGET_TOKEN' });
566
678
  ib.open({
567
679
  target: {
568
680
  type: 'cli_command',
569
- targetId: 'deploy',
570
- displayName: 'my-cli deploy',
571
681
  metadata: { command: 'deploy' },
572
682
  },
573
683
  });
@@ -631,6 +741,32 @@ document.getElementById('my-slot').appendChild(bar.element);
631
741
  </script>
632
742
  ```
633
743
 
744
+ ### Identify who submitted feedback
745
+
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.
747
+
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
+ ```
756
+
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
760
+
761
+ ```js
762
+ InputBufferIO.createModal({
763
+ apiKey: 'ibw_YOUR_WIDGET_TOKEN',
764
+ apiUrl: 'http://localhost:8080',
765
+ });
766
+ ```
767
+
768
+ `apiUrl` is a base origin, not an endpoint. The widget appends `/api/v0/feedback` and `/api/v0/reactions` itself.
769
+
634
770
  ---
635
771
 
636
772
  ## License
package/dist/api.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- import type { OpenOptions } 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<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 S="0.1.1",U="https://inputbuffer.io/api/v0/inputs";async function M(t,o,b,s,r,h){var p;let a={description:o};if(s&&(a.title=s),b&&(a.contactEmail=b),r!=null&&r.sentiment&&(a.sentiment=r.sentiment),r!=null&&r.source&&(a.source=r.source),r!=null&&r.target){let i=r.target;a.targets=[{target_type:i.type,...i.targetId&&{target_id:i.targetId},...i.displayName&&{display_name:i.displayName},...i.dedupKey&&{dedup_key:i.dedupKey},metadata:i.metadata}]}let l=new AbortController,m=setTimeout(()=>l.abort(),1e4),c;try{c=await fetch(h!=null?h:U,{method:"POST",headers:{Authorization:`Bearer ${t}`,"Content-Type":"application/json","X-IB-Client":`inputbuffer-widget/${S} (javascript)`},body:JSON.stringify(a),credentials:"omit",signal:l.signal})}finally{clearTimeout(m)}if(!c.ok){let i=await c.json().catch(()=>({}));throw new Error(((p=i==null?void 0:i.error)==null?void 0:p.message)||"Submission failed. Please try again.")}return c.json()}function R(t){let o=document.createElementNS("http://www.w3.org/2000/svg","svg");o.setAttribute("viewBox","0 0 24 24"),o.setAttribute("width","18"),o.setAttribute("height","18"),o.setAttribute("fill","currentColor"),o.setAttribute("aria-hidden","true"),o.style.display="block";let b=document.createElementNS("http://www.w3.org/2000/svg","path");return b.setAttribute("d",t),o.appendChild(b),o}var $="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",q="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";function _(t){var O,H;let o,b={vote:[],open:[],submit:[],close:[],error:[]};function s(e,g){var y;(y=b[e])==null||y.forEach(C=>C(g))}let r=document.createElement("div"),h=t.colorScheme==="dark"?" ib-theme-dark":t.colorScheme==="light"?" ib-theme-light":"";r.className="ib-bar-wrapper"+(t.placement==="fixed"?" ib-bar-wrapper--fixed":"")+h;let{theme:a={}}=t;a.primary&&r.style.setProperty("--ib-primary",a.primary),a.background&&r.style.setProperty("--ib-background",a.background),a.surface&&r.style.setProperty("--ib-surface",a.surface),a.text&&r.style.setProperty("--ib-text",a.text),a.selected&&r.style.setProperty("--ib-selected",a.selected),a.selectedColor&&r.style.setProperty("--ib-selected-color",a.selectedColor);let l=document.createElement("div");l.className="ib-bar";let m=document.createElement("div");m.className="ib-bar-label-area";let c=document.createElement("span");c.className="ib-bar-label",c.textContent=(O=t.label)!=null?O:"Was this helpful?",m.appendChild(c);let p=document.createElement("div");p.className="ib-bar-actions"+(t.showLabel===!1?" ib-bar-actions--no-label":"");let i=document.createElement("button");i.type="button",i.className="ib-bar-btn ib-bar-btn--up",i.setAttribute("aria-label","Yes"),i.appendChild(R($));let d=document.createElement("button");d.type="button",d.className="ib-bar-btn ib-bar-btn--down",d.setAttribute("aria-label","No"),d.appendChild(R(q)),p.appendChild(i),p.appendChild(d),t.showLabel!==!1&&l.appendChild(m),l.appendChild(p);let n=document.createElement("div");n.className="ib-bar-popover",n.setAttribute("role","dialog"),n.setAttribute("aria-modal","false");let N=document.createElement("div");if(N.className="ib-bar-header",t.modalTitle){n.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,N.appendChild(e)}else n.setAttribute("aria-label","Feedback");let f=document.createElement("div");f.className="ib-bar-body";let v=document.createElement("textarea");v.className="ib-bar-textarea",v.placeholder=(H=t.modalPlaceholder)!=null?H:"What's on your mind?",v.setAttribute("aria-label","Feedback");let x=document.createElement("p");x.className="ib-bar-error";let B=document.createElement("p");B.className="ib-bar-success";let u=document.createElement("button");if(u.type="button",u.className="ib-bar-submit",u.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"),f.appendChild(e)}if(f.appendChild(v),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"),f.appendChild(e)}f.appendChild(x),f.appendChild(B);let A=document.createElement("div");A.className="ib-bar-footer",A.appendChild(u);let I=document.createElement("div");I.className="ib-branding";let w=document.createElement("a");w.href="https://inputbuffer.io",w.target="_blank",w.rel="noopener noreferrer",w.textContent="Powered by inputbuffer.io",I.appendChild(w),A.appendChild(I),f.appendChild(A),n.appendChild(N),n.appendChild(f),r.appendChild(n),r.appendChild(l);function E(){let e=l.getBoundingClientRect();n.style.top=`${e.top-n.offsetHeight-8}px`,n.style.left=`${e.right-320}px`}function z(e){o=e,n.classList.add("ib-bar-popover--visible"),requestAnimationFrame(E),x.textContent="",B.textContent="",v.focus(),setTimeout(()=>document.addEventListener("click",T),0),document.addEventListener("keydown",P),window.addEventListener("scroll",E,{passive:!0}),window.addEventListener("resize",E,{passive:!0}),s("open",{sentiment:e})}function L(){n.classList.contains("ib-bar-popover--visible")&&(n.classList.remove("ib-bar-popover--visible"),document.removeEventListener("click",T),document.removeEventListener("keydown",P),window.removeEventListener("scroll",E),window.removeEventListener("resize",E),v.value="",s("close"))}function W(){i.classList.remove("ib-bar-btn--active"),d.classList.remove("ib-bar-btn--active"),o=void 0}function T(e){r.contains(e.target)||L()}function P(e){e.key==="Escape"&&L()}async function K(){let e=v.value.trim();if(e.length<10){x.textContent="Please enter at least 10 characters.";return}x.textContent="",u.disabled=!0,u.textContent="Sending\u2026";let g=n.querySelector(".ib-bar-title-input"),y=(g==null?void 0:g.value.trim())||null,C=n.querySelector(".ib-bar-email"),G=(C==null?void 0:C.value.trim())||null;try{let k=await M(t.apiKey,e,G,y,{sentiment:o,target:t.target,source:t.source},t.apiUrl);s("submit",k),B.textContent="Thanks for your feedback!",u.textContent="Send feedback",setTimeout(()=>{L(),W()},2e3)}catch(k){s("error",k instanceof Error?k:new Error("Something went wrong.")),x.textContent=k instanceof Error?k.message:"Something went wrong.",u.disabled=!1,u.textContent="Send feedback"}}function j(e){i.classList.remove("ib-bar-btn--active"),d.classList.remove("ib-bar-btn--active"),e.classList.add("ib-bar-btn--active")}return i.addEventListener("click",()=>{s("vote",{sentiment:"positive"}),j(i),z("positive")}),d.addEventListener("click",()=>{s("vote",{sentiment:"negative"}),j(d),z("negative")}),u.addEventListener("click",K),{element:r,on(e,g){var y;(y=b[e])==null||y.push(g)},destroy(){L(),r.remove()}}}var D=`/* 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.0",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=24*60*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 S="0.1.1",U="https://inputbuffer.io/api/v0/inputs";async function M(t,o,b,s,
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 S="0.1.1",U="https://inputbuffer.io/api/v0/inputs";async function M(t,o,b,s,
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 S="0.1.1",U="https://inputbuffer.io/api/v0/inputs";async function M(t,o,b,s,
319
312
  --ib-radius: 8px;
320
313
  --ib-radius-input: 4px;
321
314
  }
322
- `;function V(){if(typeof document=="undefined"||document.getElementById("ib-bar-styles"))return;let t=document.createElement("style");t.id="ib-bar-styles",t.textContent=D,document.head.appendChild(t)}function J(t){return t.injectStyles!==!1&&V(),_(t)}var F=class extends HTMLElement{constructor(){super(...arguments);this._bar=null}connectedCallback(){var a,l,m,c,p,i,d;let b=this.getAttribute("api-key");if(!b)return;let s=this.getAttribute("inject-styles");(s===null?!0:s!=="false")&&V();let h=this.getAttribute("placement");this._bar=_({apiKey:b,apiUrl:(a=this.getAttribute("api-url"))!=null?a:void 0,label:(l=this.getAttribute("label"))!=null?l:void 0,placement:h==="fixed"?"fixed":"inline",theme:{primary:(m=this.getAttribute("theme-primary"))!=null?m:void 0,background:(c=this.getAttribute("theme-background"))!=null?c:void 0,text:(p=this.getAttribute("theme-text"))!=null?p:void 0,selected:(i=this.getAttribute("theme-selected"))!=null?i:void 0,selectedColor:(d=this.getAttribute("theme-selected-color"))!=null?d:void 0}}),this.appendChild(this._bar.element)}disconnectedCallback(){var b;(b=this._bar)==null||b.destroy(),this._bar=null}};typeof customElements!="undefined"&&!customElements.get("inputbuffer-feedback")&&customElements.define("inputbuffer-feedback",F);var X={createBar:J,version:S};typeof window!="undefined"&&(window.InputBufferIO=X);export{X as InputBufferIO,J 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};