@pixelmatters/markup 1.15.0 → 1.16.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
@@ -10,8 +10,10 @@ Pin-anchored feedback for live web apps. Drop in a script tag and your stakehold
10
10
 
11
11
  - **Pin anywhere** — click anywhere on the page to attach a comment to that exact element. Pins re-anchor across reflow using a CSS selector + viewport-fraction fallback.
12
12
  - **Threads, in real time** — replies stream in via WebSocket. Per-comment edit, delete (with tombstones), and emoji reactions.
13
+ - **@-mentions** — typing `@` in the composer opens a project-member picker and inserts a chip; the `@[Name](userId)` wire format never reaches the screen. Mentioned teammates are notified, and signed-in users read those notifications from the toolbar's inbox.
13
14
  - **Annotated screenshots** — opt-in capture with the pin marker drawn on the image and embedded fonts so the snapshot matches what the user saw.
14
- - **Drop-in identity** — anonymous by default, with a popup-based sign-in that survives Safari ITP / Chrome storage partitioning. Verified users get a team badge.
15
+ - **Drop-in identity** — anonymous by default, with a popup-based sign-in that survives Safari ITP / Chrome storage partitioning. Signed-in authors show their profile picture; everyone else gets initials.
16
+ - **Agent replies are labelled** — a comment written by an AI agent through Markup's MCP server carries a bot badge. It's posted under a team member's name, so the badge is the only way a visitor can tell a machine answered.
15
17
  - **Style-isolated** — runs inside an open shadow root with `:host { all: initial }`, so host CSS can't bleed in and widget CSS can't bleed out.
16
18
  - **SPA-aware** — patches `history.pushState` / `replaceState` and follows `popstate` to refresh threads on route changes.
17
19
  - **Respects the platform** — honours `prefers-reduced-motion` and `prefers-color-scheme`; full keyboard navigation with focus traps in popovers.
@@ -35,9 +37,9 @@ CDN drop-in (no build step) — paste this just before `</body>`:
35
37
  ```html
36
38
  <script type="module">
37
39
  // Pin the exact version — esm.sh resolves it from npm
38
- import { init } from 'https://esm.sh/@pixelmatters/markup@1.15.0'
40
+ import { init } from 'https://esm.sh/@pixelmatters/markup@1.16.1'
39
41
  // or
40
- // import { init } from 'https://esm.run/@pixelmatters/markup@1.15.0'
42
+ // import { init } from 'https://esm.run/@pixelmatters/markup@1.16.1'
41
43
 
42
44
  init({
43
45
  apiUrl: 'https://your-deployment.convex.site',
@@ -48,14 +50,14 @@ CDN drop-in (no build step) — paste this just before `</body>`:
48
50
  </script>
49
51
  ```
50
52
 
51
- > **Why pin the version?** CDN URLs without a version (`@pixelmatters/markup`) resolve to whatever's `latest` on npm — a future major release will break your page silently. Always pin (`@pixelmatters/markup@1.15.0`).
53
+ > **Why pin the version?** CDN URLs without a version (`@pixelmatters/markup`) resolve to whatever's `latest` on npm — a future major release will break your page silently. Always pin (`@pixelmatters/markup@1.16.1`).
52
54
 
53
55
  If your platform doesn't allow inline JS (some CMS / page-builder editors), use the auto-init form instead — point a `<script src=…>` at the bundle and pass config via `data-*` attributes:
54
56
 
55
57
  ```html
56
58
  <script
57
59
  type="module"
58
- src="https://esm.sh/@pixelmatters/markup@1.15.0"
60
+ src="https://esm.sh/@pixelmatters/markup@1.16.1"
59
61
  data-markup-widget="true"
60
62
  data-api-url="https://your-deployment.convex.site"
61
63
  data-api-key="markup_..."
@@ -64,7 +66,7 @@ If your platform doesn't allow inline JS (some CMS / page-builder editors), use
64
66
  ></script>
65
67
  ```
66
68
 
67
- `data-markup-widget="true"` is required — it's how the bootstrap finds its own `<script>` tag (since `document.currentScript` is `null` for `type="module"`).
69
+ `data-markup-widget="true"` is required — it's how the bootstrap finds its own `<script>` tag (since `document.currentScript` is `null` for `type="module"`). The recognised attributes are `data-api-url`, `data-api-key`, `data-position`, `data-theme`, and `data-dashboard-url`; the `screenshots` options are only available through `init()`.
68
70
 
69
71
  ## Quickstart
70
72
 
@@ -151,26 +153,59 @@ export default function App() {
151
153
 
152
154
  Mounts the widget. Always tears down any existing instance before mounting, so calling `init` again (with the same or a different config) is safe. Returns the `destroy` function.
153
155
 
154
- | Option | Type | Default | Description |
155
- | ---------- | ---------------------------------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
156
- | `apiUrl` | `string` | required | Convex deployment site URL (`https://*.convex.site`) |
157
- | `apiKey` | `string` | required | Project API key — mint one in the dashboard |
158
- | `position` | `'bottom-right' \| 'bottom-left' \| 'bottom-center'` | `'bottom-right'` | Initial placement for the toolbar — users can move it with the Position picker in the toolbar's overflow menu |
159
- | `theme` | `'light' \| 'dark' \| 'auto'` | `'auto'` | Initial theme — `'light'` or `'dark'`, or `'auto'` (default) to follow the host's `prefers-color-scheme`. Users can change this from the overflow menu; their choice persists and wins over this option from then on |
156
+ | Option | Type | Default | Description |
157
+ | -------------- | ---------------------------------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
158
+ | `apiUrl` | `string` | required | Convex deployment site URL (`https://*.convex.site`) |
159
+ | `apiKey` | `string` | required | Project API key — mint one in the dashboard |
160
+ | `position` | `'bottom-right' \| 'bottom-left' \| 'bottom-center'` | `'bottom-right'` | Initial placement for the toolbar — users can move it with the Position picker in the toolbar's overflow menu |
161
+ | `theme` | `'light' \| 'dark' \| 'auto'` | `'auto'` | Initial theme — `'light'` or `'dark'`, or `'auto'` (default) to follow the host's `prefers-color-scheme`. Users can change this from the overflow menu; their choice persists and wins over this option from then on |
162
+ | `screenshots` | `ScreenshotsConfig` | capture enabled | Capture and PII-scrub options — see [Screenshots & privacy](#screenshots--privacy) |
163
+ | `dashboardUrl` | `string` | — | Dashboard URL the identity menu links to as **Account →** for signed-in users; it also retargets the overflow menu's "Powered by Markup" line. Omit it and the Account entry is hidden — mostly useful for self-hosters, whose dashboard origin the widget can't know statically |
164
+
165
+ <details>
166
+ <summary><code>fab</code> (deprecated, ignored since 1.15.0)</summary>
167
+
168
+ The floating action button was replaced by the toolbar pill in 1.15.0, and a pill has no variants — neither `'default'` nor `'icon-only'` describes anything the widget can render. The option and the `WidgetFab` type are still exported so upgrading needs no code change; `init()` drops the value and logs a one-time console warning. Both go away in the next major:
169
+
170
+ ```diff
171
+ init({
172
+ apiUrl: 'https://your-deployment.convex.site',
173
+ apiKey: 'markup_...',
174
+ - fab: 'icon-only',
175
+ })
176
+ ```
177
+
178
+ </details>
160
179
 
161
180
  ### `destroy()`
162
181
 
163
182
  Unmounts the widget and removes the host element. Safe to call when nothing is mounted.
164
183
 
184
+ ## The toolbar
185
+
186
+ The widget mounts a single compact pill in the corner set by `position`:
187
+
188
+ | Control | What it does |
189
+ | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
190
+ | **Comment** | Arms placement — the next click on the page drops a pin. Flips to a cancel icon while armed. |
191
+ | **Inbox** | Mention notifications for this project, with an unread badge. Signed-in users only; anonymous visitors don't get the button. |
192
+ | **Pins** (eye) | Hides or shows every pin without hiding the toolbar. |
193
+ | **Identity** | Avatar button. Anonymous: a sign-in prompt plus "Forget me on this site". Signed in: name, email, an **Account →** link when `dashboardUrl` is set, and **Sign out**. |
194
+ | **Overflow** (`☰`) | **Appearance** (Light / Dark / Auto), **Position** (left / center / right), an **Auto-capture screenshots** toggle, **Hide for this session**, and the widget version. |
195
+
196
+ Appearance persists to the host page's `localStorage` (`markup:widget:theme`) and, once a user has set it, takes precedence over the `theme` option on every later `init()`. Position, pin visibility, and the auto-capture toggle are per-mount — they reset on reload, and `position` seeds the toolbar again.
197
+
165
198
  ### Keyboard & mouse
166
199
 
167
- | Shortcut | Action |
168
- | --------------------- | ---------------------------------------------------------------------- |
169
- | `c` | Start placing a markup (ignored while typing) |
170
- | `esc` | Cancel placement, or close the open popover |
171
- | `cmd/ctrl + .` | Toggle HUD visibility |
172
- | `cmd/ctrl + click` | Click the toolbar's comment button to hide the HUD with a hint toast |
173
- | Drag a popover header | Move the open thread / new-thread popover; resets to the pin on reopen |
200
+ | Shortcut | Action |
201
+ | --------------------- | --------------------------------------------------------------------------------- |
202
+ | `c` | Start placing a markup (ignored while typing) |
203
+ | `@` | In a composer, open the member picker — `↑`/`↓` to move, `enter` or `tab` to pick |
204
+ | `cmd/ctrl + enter` | Post the comment being written |
205
+ | `esc` | Cancel placement, dismiss the mention picker, or close the open popover / menu |
206
+ | `cmd/ctrl + .` | Toggle HUD visibility |
207
+ | `cmd/ctrl + click` | Click the toolbar's comment button to hide the HUD with a hint toast |
208
+ | Drag a popover header | Move the open thread / new-thread popover; resets to the pin on reopen |
174
209
 
175
210
  ## Screenshots & privacy
176
211
 
@@ -200,12 +235,23 @@ By default, the widget captures the visible viewport as a JPEG before you submit
200
235
 
201
236
  When a screenshot is attached in the composer, a chip shows how many fields were redacted. Clicking it expands the list of CSS selectors that were masked.
202
237
 
238
+ Users can also switch capture off for themselves with **Auto-capture screenshots** in the toolbar's overflow menu. A host that set `screenshots.enabled: false` still wins — the row renders disabled and says so, rather than offering a control that does nothing.
239
+
240
+ ### When a capture doesn't work out
241
+
242
+ Capture degrades instead of failing silently:
243
+
244
+ - **An image the browser won't hand over** — a third-party avatar served without CORS headers is the usual culprit — comes through blank, and the rest of the page still captures. It used to abort the whole screenshot.
245
+ - **An oversized capture** is re-encoded until it fits the server's 2 MB cap: quality drops first (0.85 → 0.6), then the raster shrinks (full → ¾ → ½), because a smaller sharp screenshot beats a full-size illegible one.
246
+ - **If nothing works**, the composer reads _Screenshot unavailable_ and the comment posts without one. Previously the row just disappeared, which looked identical to screenshots being switched off for the project.
247
+
203
248
  ## How it works
204
249
 
205
250
  - The widget mounts `<div id="markup-widget">` on `document.body` and attaches an open shadow root.
206
251
  - All UI lives in that shadow root, with `:host { all: initial }` blocking style inheritance.
207
252
  - The host element is `position: fixed; inset: 0; pointer-events: none`, so the widget paints over the entire viewport without blocking the host's clicks; only the toolbar and active popovers opt back in to pointer events.
208
253
  - Pins are anchored as `(x, y)` fractions of the document plus a best-effort CSS selector (via [`@medv/finder`](https://github.com/antonmedv/finder)). The selector wins when it still resolves; the fraction is the fallback so pins survive layout changes.
254
+ - Live thread updates come over a WebSocket to the deployment's `*.convex.cloud` origin, which the widget derives from `apiUrl` — everything else (comments, identity, screenshots, error reports) goes to `*.convex.site` over HTTP.
209
255
  - Identity lives in **host-page** `localStorage` under `markup.identity`, keyed to the top-level site. On first load the widget mints a server-signed anonymous JWT via `POST /widget/anon-identity` so the backend can verify the `authorClientId` on every anon write — tampering with the cached `clientId` invalidates the signature. Verified identities upgrade to a `Bearer` JWT via the popup flow described below.
210
256
 
211
257
  ## Identity
@@ -217,6 +263,10 @@ Anonymous by default, with two opt-in upgrade paths:
217
263
 
218
264
  The popup origin is validated against the project's `allowedDomains` before any identity is returned, so only embeds on approved domains can resolve dashboard sessions. Verified JWTs can be invalidated before their TTL via the dashboard's "Sign out everywhere" action.
219
265
 
266
+ Signed-in authors render their profile picture — on their comments and on the toolbar's identity button. The URL is validated as `http(s)` both when it arrives and when it's read back out of `localStorage`, because that record lives on the customer's origin where any script could rewrite a value the widget hands to an `<img src>`. Anonymous visitors, accounts with no picture, and images the host's CSP blocks all fall back to initials.
267
+
268
+ **Forget me on this site** (in the anonymous identity menu) clears the cached name, email, and client id from the host page's `localStorage`. Comments already posted stay where they are.
269
+
220
270
  > **Why a popup, not auto-detect?** Safari ITP, Chrome Storage Partitioning, and Firefox TCP all partition third-party storage and cookies by top-level site. A cross-origin fetch from `customer.com` to `convex.site` cannot see the dashboard session. The popup is the only reliable way to bridge identity across sites without per-host configuration.
221
271
 
222
272
  ## Getting an API key
@@ -259,9 +309,17 @@ init({
259
309
  apiKey: string, // required
260
310
  position?: 'bottom-right' | 'bottom-left' | 'bottom-center', // default 'bottom-right'
261
311
  theme?: 'light' | 'dark' | 'auto', // default 'auto'
312
+ dashboardUrl?: string, // optional: adds an "Account →" link to the identity menu
313
+ screenshots?: {
314
+ enabled?: boolean, // default true
315
+ strictScrub?: boolean, // default false — also masks every input/select/textarea
316
+ redactSelector?: string, // extra CSS selector to mask
317
+ },
262
318
  }) // returns a destroy() function — call it on unmount / logout / route teardown
263
319
  ```
264
320
 
321
+ There is no `fab` option any more — it's accepted, ignored, and warns once. Drop it if you find one in my config.
322
+
265
323
  For React/Vue/Solid hosts, call `init()` from a mount lifecycle hook
266
324
  (`useEffect`, `onMounted`, `onMount`) and call the returned `destroy` on
267
325
  cleanup. There's no framework-specific entrypoint — `init` is the entire
@@ -271,7 +329,7 @@ For a `<script>` tag drop-in (no bundler), use the inline ESM form and **pin the
271
329
 
272
330
  ```html
273
331
  <script type="module">
274
- import { init } from 'https://esm.sh/@pixelmatters/markup@1.15.0'
332
+ import { init } from 'https://esm.sh/@pixelmatters/markup@1.16.1'
275
333
 
276
334
  init({
277
335
  apiUrl: '...',
@@ -287,7 +345,7 @@ If inline JS is disallowed (some CMS / page-builder editors), use the auto-init
287
345
  ```html
288
346
  <script
289
347
  type="module"
290
- src="https://esm.sh/@pixelmatters/markup@1.15.0"
348
+ src="https://esm.sh/@pixelmatters/markup@1.16.1"
291
349
  data-markup-widget="true"
292
350
  data-api-url="..."
293
351
  data-api-key="..."
@@ -308,9 +366,23 @@ Constraints:
308
366
 
309
367
  - Do **not** add CSS imports or provider components — the widget needs neither.
310
368
  - Do **not** hardcode the key.
311
- - If the project has a CSP, add `https://esm.sh` to `script-src` only if I'm using the `<script>` tag path.
369
+ - If the project has a CSP: add `https://<MY_DEPLOYMENT>.convex.site` and `wss://<MY_DEPLOYMENT>.convex.cloud` to `connect-src`, `blob:` and `data:` (plus the host of our profile pictures) to `img-src`, and `'unsafe-inline'` to `style-src`. Add `https://esm.sh` to `script-src` only if I'm using the `<script>` tag path.
312
370
  ````
313
371
 
372
+ ## Content Security Policy
373
+
374
+ If the host page ships a CSP, the widget needs:
375
+
376
+ | Directive | Value | Why |
377
+ | ------------- | -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
378
+ | `connect-src` | `https://<deployment>.convex.site` | threads, comments, identity, screenshot upload, error reports |
379
+ | `connect-src` | `wss://<deployment>.convex.cloud` | live thread updates |
380
+ | `img-src` | wherever your team's profile pictures are hosted, plus `blob:` and `data:` | avatars, the screenshot preview thumbnail, and the capture pipeline |
381
+ | `script-src` | `https://esm.sh` | CDN path only — a bundled install needs nothing here |
382
+ | `style-src` | `'unsafe-inline'` | the widget appends its stylesheet as a `<style>` element inside its own shadow root |
383
+
384
+ Only `img-src` degrades gracefully: a blocked avatar falls back to initials, and a blocked image in a capture comes through blank. A missing `connect-src` entry stops the widget working at all, and a missing `style-src` one leaves it unstyled.
385
+
314
386
  ## Browser support
315
387
 
316
388
  Modern evergreen browsers (Chrome, Edge, Firefox, Safari) and their mobile equivalents. The widget uses native ESM, shadow DOM, and `IntersectionObserver` — no IE11 / legacy bundle.