diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 91c5e76..ce30178 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,11 +9,11 @@ jobs: ci: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: - node-version: 20 + node-version: 22 cache: npm - run: npm ci diff --git a/README.md b/README.md index fae5d61..ad4c8d7 100644 --- a/README.md +++ b/README.md @@ -236,8 +236,14 @@ Supported attributes: | `theme-selected` | Background color of the selected thumb. | | `theme-selected-color` | Icon color of the selected thumb. | | `inject-styles` | Set to `"false"` to skip automatic style injection. | - -> **Note:** `colorScheme`, `showLabel`, `modalTitle`, `modalPlaceholder`, `showTitleField`, `showEmailField`, and `source` are not available as web component attributes. Use `InputBufferIO.createBar(config)` for those options. +| `color-scheme` | `"light"`, `"dark"`, or `"auto"`. | +| `show-label` | `"true"` to show the label, `"false"` to hide it. | +| `modal-title` | Title shown above the feedback textarea. | +| `modal-placeholder` | Placeholder text for the feedback textarea. | +| `show-title-field` | `"true"` to show an optional title input. | +| `show-email-field` | `"true"` to show an optional email input. | +| `source` | Identifier for the feedback source. | +| `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. | ### `InputBufferIO.createBar(config)` @@ -275,6 +281,7 @@ document.getElementById('my-slot').appendChild(bar.element); | `showEmailField` | boolean | `false` | Show/hide the email field in the follow-up popover. | | `showTitleField` | boolean | `false` | Show/hide the title field in the follow-up popover. | | `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. | +| `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. | | `injectStyles` | boolean | `true` | Set to `false` to skip automatic style injection. | ### `bar.on(event, handler)` @@ -291,7 +298,7 @@ bar.on('error', (err) => console.error('Submission failed:', err)); | Event | Handler signature | When it fires | |---|---|---| -| `vote` | `({ sentiment: 'positive' \| 'negative' }) => void` | User clicks a thumb before submitting. | +| `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. | | `open` | `({ sentiment: 'positive' \| 'negative' }) => void` | The follow-up popover opens. | | `submit` | `({ id: string }) => void` | Feedback was submitted successfully. | | `close` | `() => void` | The follow-up popover closes. | @@ -631,6 +638,27 @@ document.getElementById('my-slot').appendChild(bar.element); ``` +## `source` vs `target` + +These are two separate concepts: + +- **`source`** — *where* your widget is deployed. Identifies the platform or product surface, + e.g. `"website"`, `"ios-app"`, `"chrome-extension"`. Use this to filter feedback by deployment + environment in your dashboard. + +- **`target`** — *what* the feedback is about. A structured object describing the specific content + or feature, e.g. a REST endpoint, a docs page, or a CLI command. Use this to group feedback by + the thing being reviewed, regardless of where the widget is embedded. + +You can use both together: +```js +InputBufferIO.createBar({ + apiKey: 'YOUR_WIDGET_TOKEN', + source: 'website', + target: { type: 'documentation', metadata: { page_slug: 'getting-started' } }, +}); + + --- ## License diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..d09ec46 --- /dev/null +++ b/TODO.md @@ -0,0 +1 @@ +1. investigate thumbsup/down single icon, mouseover expand to both clickable options. \ No newline at end of file diff --git a/coverage/api.ts.html b/coverage/api.ts.html index 8f02128..583fa5d 100644 --- a/coverage/api.ts.html +++ b/coverage/api.ts.html @@ -23,16 +23,16 @@

All files api.ts

- 90% + 88% Statements - 18/20 + 22/25
- 91.66% + 77.27% Branches - 11/12 + 17/22
@@ -44,9 +44,9 @@

All files api.ts

- 94.44% + 90.9% Lines - 17/18 + 20/22
@@ -123,13 +123,20 @@

All files api.ts

58 59 60 -61  +61 +62 +63 +64 +65 +66 +67 +68 +69      -2x -  -2x +3x   +3x       @@ -137,34 +144,40 @@

All files api.ts

      -12x     +13x   +13x   -12x +13x 1x     -12x +13x       -12x +13x +  +  +  +13x +1x 1x           -12x -12x   -12x -12x     +13x +13x   +13x +13x       @@ -173,34 +186,37 @@

All files api.ts

      -12x     -12x +  +13x +  +  +13x 2x 2x     -10x +11x    
import type { OpenOptions } from './types.js';
  
 declare const __WIDGET_VERSION__: string;
 export const WIDGET_VERSION = __WIDGET_VERSION__;
  
-const DEFAULT_API_URL = 'https://inputbuffer.io/api/widget/inputs';
+const DEFAULT_API_URL = 'https://inputbuffer.io/api/v0/inputs';
  
 export async function submitFeedback(
     apiKey: string,
     description: string,
     email: string | null,
+    title: string | null,
     options?: OpenOptions,
     apiUrl?: string
 ): Promise<{ id: string }> {
-    const body: Record<string, unknown> = {
-        title: 'Widget feedback',
-        description,
-    };
+    const body: Record<string, unknown> = { description };
+ 
+    if (title) body.title = title;
  
     if (email) {
         body.contactEmail = email;
@@ -209,11 +225,19 @@ 

All files api.ts

Iif (options?.sentiment) { body.sentiment = options.sentiment; } +  + Iif (options?.source) { + body.source = options.source; + }   if (options?.target) { + const t = options.target; body.targets = [{ - target_type: options.target.type, - metadata: options.target.metadata, + target_type: t.type, + ...(t.targetId && { target_id: t.targetId }), + ...(t.displayName && { display_name: t.displayName }), + ...(t.dedupKey && { dedup_key: t.dedupKey }), + metadata: t.metadata, }]; }   @@ -250,7 +274,7 @@

All files api.ts

+ + + + + + \ No newline at end of file diff --git a/coverage/bar.css.html b/coverage/bar.css.html index 4732674..0393ef4 100644 --- a/coverage/bar.css.html +++ b/coverage/bar.css.html @@ -356,7 +356,63 @@

All files bar.css

291 292 293 -294  +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +        @@ -689,7 +745,6 @@

All files bar.css

overflow: hidden;   background-color: var(--ib-background); - background-image: url("data:image/svg+xml,%3Csvg width='20' height='20' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M 20 0 L 0 0 0 20' fill='none' stroke='%238d99ae' stroke-width='0.5' stroke-opacity='0.3'/%3E%3C/svg%3E"); border: 1px solid var(--ib-border); border-radius: 2px; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08); @@ -698,9 +753,7 @@

All files bar.css

  .ib-bar-popover { display: none; - position: absolute; - bottom: calc(100% + 8px); - right: 0; + position: fixed; width: 320px;   border: 1px solid var(--ib-border); @@ -760,7 +813,8 @@

All files bar.css

box-shadow: 0 0 0 2px color-mix(in srgb, var(--ib-focus-color) 15%, transparent); }   -.ib-bar-email { +.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); @@ -770,16 +824,25 @@

All files bar.css

font-family: inherit; color: var(--ib-text); box-sizing: border-box; - margin-top: 8px; outline: none; display: block; }   -.ib-bar-email:hover { +.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-email:focus { +.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); } @@ -796,7 +859,6 @@

All files bar.css

letter-spacing: 0.08em; font-family: inherit; cursor: pointer; - margin-top: 10px; display: inline-block; transition: background 0.15s; } @@ -810,24 +872,45 @@

All files bar.css

cursor: not-allowed; }   -.ib-bar-error { - color: #dc2626; +.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: 0.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: 0.08em; margin: 8px 0 0; - min-height: 0; +} +  +.ib-bar-error:empty, +.ib-bar-success:empty { + display: none; +} +  +.ib-bar-error { + color: #dc2626; }   .ib-bar-success { color: #16a34a; - font-size: 11px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.08em; - margin: 8px 0 0; - min-height: 0; }   .ib-bar-label-area { @@ -844,6 +927,7 @@

All files bar.css

text-transform: uppercase; letter-spacing: 0.1em; white-space: nowrap; + user-select: none; }   .ib-bar-actions { @@ -949,7 +1033,7 @@

All files bar.css