@pixelmatters/markup 1.18.4 → 1.18.6

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
@@ -4,22 +4,22 @@
4
4
  [![license](https://img.shields.io/npm/l/@pixelmatters/markup.svg)](./LICENSE)
5
5
  [![bundle](https://img.shields.io/bundlephobia/minzip/@pixelmatters/markup)](https://bundlephobia.com/package/@pixelmatters/markup)
6
6
 
7
- Pin-anchored feedback for live web apps. Drop in a script tag and your stakeholders can leave threaded comments, reactions, and annotated screenshots on top of any page without touching the host's CSS, build, or routing.
7
+ Pin-anchored feedback for live web apps. Drop in a script tag and your stakeholders can leave threaded comments, reactions, and annotated screenshots on top of any page, without touching the host's CSS, build, or routing.
8
8
 
9
9
  ## Highlights
10
10
 
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
- - **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.
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.
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.
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.
18
- - **SPA-aware** patches `history.pushState` / `replaceState` and follows `popstate` to refresh threads on route changes.
19
- - **Respects the platform** honours `prefers-reduced-motion` and `prefers-color-scheme`; full keyboard navigation with focus traps in popovers.
20
- - **Tiny surface, tiny config** `init({ apiUrl, apiKey })` is enough to start. No global CSS to import, no provider to wrap.
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 plus a viewport-fraction fallback.
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.
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.
15
+ - **Drop-in identity.** Anonymous by default, with a popup-based sign-in that survives Safari ITP and 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.
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.
18
+ - **SPA-aware.** Patches `history.pushState` / `replaceState` and follows `popstate` to refresh threads on route changes.
19
+ - **Respects the platform.** Honours `prefers-reduced-motion` and `prefers-color-scheme`, with full keyboard navigation and focus traps in popovers.
20
+ - **Tiny API, tiny config.** `init({ apiUrl, apiKey })` is enough to start. No global CSS to import, no provider to wrap.
21
21
 
22
- Built with [Preact](https://preactjs.com). Ships as a single ESM bundle wire it into any framework with a one-liner in your root component (snippets below).
22
+ Built with [Preact](https://preactjs.com). It ships as a single ESM bundle, so wiring it into any framework takes a one-liner in your root component. Snippets below.
23
23
 
24
24
  ## Install
25
25
 
@@ -32,14 +32,14 @@ yarn add @pixelmatters/markup
32
32
  npm install @pixelmatters/markup
33
33
  ```
34
34
 
35
- CDN drop-in (no build step) paste this just before `</body>`:
35
+ CDN drop-in, no build step. Paste this just before `</body>`:
36
36
 
37
37
  ```html
38
38
  <script type="module">
39
- // Pin the exact version esm.sh resolves it from npm
40
- import { init } from 'https://esm.sh/@pixelmatters/markup@1.18.4'
39
+ // Pin the exact version; esm.sh resolves it from npm
40
+ import { init } from 'https://esm.sh/@pixelmatters/markup@1.18.6'
41
41
  // or
42
- // import { init } from 'https://esm.run/@pixelmatters/markup@1.18.4'
42
+ // import { init } from 'https://esm.run/@pixelmatters/markup@1.18.6'
43
43
 
44
44
  init({
45
45
  apiUrl: 'https://your-deployment.convex.site',
@@ -50,14 +50,14 @@ CDN drop-in (no build step) — paste this just before `</body>`:
50
50
  </script>
51
51
  ```
52
52
 
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.18.4`).
53
+ > **Why pin the version?** CDN URLs without a version (`@pixelmatters/markup`) resolve to whatever's `latest` on npm, so a future major release will break your page with no warning. Always pin (`@pixelmatters/markup@1.18.6`).
54
54
 
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:
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:
56
56
 
57
57
  ```html
58
58
  <script
59
59
  type="module"
60
- src="https://esm.sh/@pixelmatters/markup@1.18.4"
60
+ src="https://esm.sh/@pixelmatters/markup@1.18.6"
61
61
  data-markup-widget="true"
62
62
  data-api-url="https://your-deployment.convex.site"
63
63
  data-api-key="markup_..."
@@ -66,7 +66,7 @@ If your platform doesn't allow inline JS (some CMS / page-builder editors), use
66
66
  ></script>
67
67
  ```
68
68
 
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()`.
69
+ `data-markup-widget="true"` is required, since it's how the bootstrap finds its own `<script>` tag (`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()`.
70
70
 
71
71
  ## Quickstart
72
72
 
@@ -153,20 +153,20 @@ export default function App() {
153
153
 
154
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.
155
155
 
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
- | `analytics` | `boolean` | `true` | Product telemetry counts of widget interactions, sent to Markup. Adds no third-party script, sets no cookie, writes nothing to storage, and carries no identifier for your users. See [Product telemetry](#product-telemetry) |
163
- | `screenshots` | `ScreenshotsConfig` | capture enabled | Capture and PII-scrub options see [Screenshots & privacy](#screenshots--privacy) |
164
- | `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 |
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
+ | `analytics` | `boolean` | `true` | Product telemetry: counts of widget interactions, sent to Markup. Adds no third-party script, sets no cookie, writes nothing to storage, and carries no identifier for your users. See [Product telemetry](#product-telemetry) |
163
+ | `screenshots` | `ScreenshotsConfig` | capture enabled | Capture and PII-scrub options. See [Screenshots & privacy](#screenshots--privacy) |
164
+ | `dashboardUrl` | `string` | none | 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 |
165
165
 
166
166
  <details>
167
167
  <summary><code>fab</code> (deprecated, ignored since 1.15.0)</summary>
168
168
 
169
- 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
+ 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:
170
170
 
171
171
  ```diff
172
172
  init({
@@ -180,9 +180,9 @@ The floating action button was replaced by the toolbar pill in 1.15.0, and a pil
180
180
 
181
181
  ### Product telemetry
182
182
 
183
- The widget reports counts of its own interactions a comment started, a
184
- comment submitted, a screenshot captured so we can tell which parts of
185
- it earn their place. Turn it off with `analytics: false`, or
183
+ The widget reports counts of its own interactions, such as a comment
184
+ started, a comment submitted, or a screenshot captured, so we can tell
185
+ which parts of it earn their place. Turn it off with `analytics: false`, or
186
186
  `data-analytics="false"` on the script tag.
187
187
 
188
188
  What it is not, concretely:
@@ -209,45 +209,45 @@ The widget mounts a single compact pill in the corner set by `position`:
209
209
 
210
210
  | Control | What it does |
211
211
  | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
212
- | **Comment** | Arms placement the next click on the page drops a pin. Flips to a cancel icon while armed. |
212
+ | **Comment** | Arms placement. The next click on the page drops a pin, and it flips to a cancel icon while armed. |
213
213
  | **Inbox** | Mention notifications for this project, with an unread badge. Signed-in users only; anonymous visitors don't get the button. |
214
214
  | **Pins** (eye) | Hides or shows every pin without hiding the toolbar. |
215
215
  | **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**. |
216
216
  | **Overflow** (`☰`) | **Appearance** (Light / Dark / Auto), **Position** (left / center / right), an **Auto-capture screenshots** toggle, **Privacy & data**, **Keyboard shortcuts**, **Hide for this session**, and the widget version. |
217
217
 
218
- 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.
218
+ 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.
219
219
 
220
220
  The last two menu rows open panels anchored to the same corner as the toolbar:
221
221
 
222
- - **Privacy & data** a plain-language account of what the widget holds, generated from the live session rather than from a policy document: who you're posting as, what a comment sends, what sits in this site's storage, whether captures are on, and the single host the widget talks to. Anonymous visitors also get **Forget me on this site** here, with the same arm-then-confirm as the identity menu.
223
- - **Keyboard shortcuts** the table below, in the modifier vocabulary of the reader's platform. The menu is the only way in on purpose: a key binding for it (`?` being the conventional one) would compete with whatever your app already binds, and the widget can't see your handlers to know.
222
+ - **Privacy & data.** A plain-language account of what the widget holds, generated from the live session rather than from a policy document: who you're posting as, what a comment sends, what sits in this site's storage, whether captures are on, and the single host the widget talks to. Anonymous visitors also get **Forget me on this site** here, with the same arm-then-confirm as the identity menu.
223
+ - **Keyboard shortcuts.** The table below, in the modifier vocabulary of the reader's platform. The menu is the only way in on purpose: a key binding for it (`?` being the conventional one) would compete with whatever your app already binds, and the widget can't see your handlers to know.
224
224
 
225
225
  ### Keyboard & mouse
226
226
 
227
- | Shortcut | Action |
228
- | --------------------- | --------------------------------------------------------------------------------- |
229
- | `c` | Start placing a markup (ignored while typing) |
230
- | `@` | In a composer, open the member picker `↑`/`↓` to move, `enter` or `tab` to pick |
231
- | `cmd/ctrl + enter` | Post the comment being written |
232
- | `esc` | Cancel placement, dismiss the mention picker, or close the open popover / menu |
233
- | `cmd/ctrl + .` | Toggle HUD visibility |
234
- | `cmd/ctrl + click` | Click the toolbar's comment button to hide the HUD with a hint toast |
235
- | Drag a popover header | Move the open thread / new-thread popover; resets to the pin on reopen |
227
+ | Shortcut | Action |
228
+ | --------------------- | -------------------------------------------------------------------------------- |
229
+ | `c` | Start placing a markup (ignored while typing) |
230
+ | `@` | In a composer, open the member picker. `↑`/`↓` to move, `enter` or `tab` to pick |
231
+ | `cmd/ctrl + enter` | Post the comment being written |
232
+ | `esc` | Cancel placement, dismiss the mention picker, or close the open popover / menu |
233
+ | `cmd/ctrl + .` | Toggle HUD visibility |
234
+ | `cmd/ctrl + click` | Click the toolbar's comment button to hide the HUD with a hint toast |
235
+ | Drag a popover header | Move the open thread / new-thread popover; resets to the pin on reopen |
236
236
 
237
237
  ## Screenshots & privacy
238
238
 
239
- By default, the widget captures the visible viewport as a JPEG before you submit a thread. Sensitive fields are blacked out **before** the image is produced the live DOM is mutated only for the duration of the capture and immediately restored. No image content leaves the browser until the user explicitly attaches the screenshot and posts.
239
+ By default, the widget captures the visible viewport as a JPEG before you submit a thread. Sensitive fields are blacked out **before** the image is produced. The live DOM is mutated only for the duration of the capture, then restored. No image content leaves the browser until the user explicitly attaches the screenshot and posts.
240
240
 
241
241
  **Auto-scrubbed (zero config):**
242
242
 
243
243
  - `input[type="password"]`
244
244
  - Any `<input>` whose `autocomplete` attribute contains `cc-number`, `cc-csc`, `cc-exp`, `cc-name`, `cc-type`, `current-password`, `new-password`, or `one-time-code`
245
245
 
246
- **Attribute API** add to any element to control capture:
246
+ **Attribute API.** Add any of these to an element to control capture:
247
247
 
248
248
  | Attribute | Behaviour |
249
249
  | --------------------- | ---------------------------------------------------------------------- |
250
- | `data-markup-private` | Always mask contents replaced with a solid block. |
250
+ | `data-markup-private` | Always mask; contents are replaced with a solid block. |
251
251
  | `data-markup-redact` | Alias for `data-markup-private`. |
252
252
  | `data-markup-safe` | Exempts descendants from the default auto-detect rules (escape hatch). |
253
253
  | `data-markup-skip` | Removes the element from the screenshot entirely. |
@@ -258,19 +258,19 @@ By default, the widget captures the visible viewport as a JPEG before you submit
258
258
  | ---------------------------- | --------- | ------- | ---------------------------------------------------------- |
259
259
  | `screenshots.enabled` | `boolean` | `true` | Set to `false` to disable capture entirely. |
260
260
  | `screenshots.strictScrub` | `boolean` | `false` | Also masks all `input`, `select`, and `textarea` elements. |
261
- | `screenshots.redactSelector` | `string` | | Custom CSS selector matched elements are always masked. |
261
+ | `screenshots.redactSelector` | `string` | none | Custom CSS selector; matched elements are always masked. |
262
262
 
263
263
  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.
264
264
 
265
- 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.
265
+ 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.
266
266
 
267
267
  The overflow menu's **Privacy & data** panel restates all of this for the person using the widget, and reflects whichever of the three capture states is live: captures on, switched off by the user, or disabled by the host.
268
268
 
269
269
  ### When a capture doesn't work out
270
270
 
271
- Capture degrades instead of failing silently:
271
+ Capture degrades instead of failing outright:
272
272
 
273
- - **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.
273
+ - **An image the browser won't hand over** comes through blank, and the rest of the page still captures. A third-party avatar served without CORS headers is the usual culprit. It used to abort the whole screenshot.
274
274
  - **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.
275
275
  - **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.
276
276
 
@@ -280,19 +280,19 @@ Capture degrades instead of failing silently:
280
280
  - All UI lives in that shadow root, with `:host { all: initial }` blocking style inheritance.
281
281
  - 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.
282
282
  - 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.
283
- - 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.
284
- - 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.
283
+ - Live thread updates come over a WebSocket to the deployment's `*.convex.cloud` origin, which the widget derives from `apiUrl`. Everything else, meaning comments, identity, screenshots and error reports, goes to `*.convex.site` over HTTP.
284
+ - 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.
285
285
 
286
286
  ## Identity
287
287
 
288
288
  Anonymous by default, with two opt-in upgrade paths:
289
289
 
290
- - **Sign in with Markup** the widget POSTs to `/widget/popup-exchange` with the apiKey + Origin gate, gets back a single-use 60-second code, then opens `${apiUrl}/widget/auth?code=…` in a popup. Because the popup is first-party to the deployment origin, the better-auth session cookie is sent normally (sidestepping third-party cookie blocks). The popup `postMessage`s a verified identity plus a short-lived server-signed `Bearer` JWT back to the host page, which attaches it as `Authorization: Bearer` on subsequent writes. Where partitioned (CHIPS) cookies are available the tokens are held in `HttpOnly` cookies and the session refreshes for up to 7 days; where they aren't, the widget falls back to `markup.identity` in the host page's `localStorage` and the session is capped at 24 hours, since a token stored there is readable by any script on the page. The apiKey never appears in the popup URL.
291
- - **Continue as a guest** name (and optional email), stored alongside the server-issued anonymous `clientId` + JWT minted on first load.
290
+ - **Sign in with Markup.** The widget POSTs to `/widget/popup-exchange` with the apiKey + Origin gate, gets back a single-use 60-second code, then opens `${apiUrl}/widget/auth?code=…` in a popup. Because the popup is first-party to the deployment origin, the better-auth session cookie is sent normally (sidestepping third-party cookie blocks). The popup `postMessage`s a verified identity plus a short-lived server-signed `Bearer` JWT back to the host page, which attaches it as `Authorization: Bearer` on subsequent writes. Where partitioned (CHIPS) cookies are available the tokens are held in `HttpOnly` cookies and the session refreshes for up to 7 days; where they aren't, the widget falls back to `markup.identity` in the host page's `localStorage` and the session is capped at 24 hours, since a token stored there is readable by any script on the page. The apiKey never appears in the popup URL.
291
+ - **Continue as a guest.** Name and optional email, stored alongside the server-issued anonymous `clientId` and JWT minted on first load.
292
292
 
293
293
  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.
294
294
 
295
- 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.
295
+ 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.
296
296
 
297
297
  **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.
298
298
 
@@ -302,12 +302,12 @@ Signed-in authors render their profile picture — on their comments and on the
302
302
 
303
303
  1. Sign in to the Markup dashboard.
304
304
  2. Open your project → **Settings → API Keys**.
305
- 3. Click **New key**, label it, and copy the raw key it's shown once.
305
+ 3. Click **New key**, label it, and copy the raw key. It is shown once.
306
306
  4. Open **Settings → Domains** and add the host domain (`app.example.com`, `*.staging.example.com`). Production deployments do not auto-allow `localhost`; add it to the allowlist explicitly if you test against the deployment from `http://localhost`.
307
307
 
308
308
  ## AI prompt
309
309
 
310
- This package ships its own Agent Skill persistent install + troubleshooting guidance for your agent, instead of a prompt you paste once and lose. Install it rather than the prompt below where you can.
310
+ This package ships its own Agent Skill: persistent install and troubleshooting guidance for your agent, instead of a prompt you paste once and lose. Install it rather than the prompt below where you can.
311
311
 
312
312
  If `@pixelmatters/markup` is already a dependency, the skill is already on disk under `node_modules`, versioned with the release you installed. [TanStack Intent](https://tanstack.com/intent/latest) wires it into whatever agent config you use (`AGENTS.md`, `CLAUDE.md`, `.cursorrules`, …):
313
313
 
@@ -315,7 +315,7 @@ If `@pixelmatters/markup` is already a dependency, the skill is already on disk
315
315
  npx @tanstack/intent@latest install
316
316
  ```
317
317
 
318
- Re-run it when you add another Intent-enabled package. A `pnpm update` of this one is enough on its own the skill travels with the tarball, so it refreshes in place.
318
+ Re-run it when you add another Intent-enabled package. A `pnpm update` of this one is enough on its own, because the skill travels with the tarball and refreshes in place.
319
319
 
320
320
  If you'd rather pull it straight from the repo, or you're deciding whether to adopt the widget at all and haven't installed it yet, the [`skills`](https://www.npmjs.com/package/skills) CLI takes it by path:
321
321
 
@@ -323,7 +323,7 @@ If you'd rather pull it straight from the repo, or you're deciding whether to ad
323
323
  npx skills add Pixelmatters/markup/packages/widget/skills/install-markup-widget
324
324
  ```
325
325
 
326
- Pass the full path, not just the repository it's where this skill lives, and a bare repository reference resolves to a different set of skills.
326
+ Pass the full path, not just the repository. That is where this skill lives, and a bare repository reference resolves to a different set of skills.
327
327
 
328
328
  Otherwise, paste the block below into Claude, ChatGPT, Cursor, or any other LLM and it'll wire the widget into your codebase end-to-end.
329
329
 
@@ -353,24 +353,24 @@ init({
353
353
  dashboardUrl?: string, // optional: adds an "Account →" link to the identity menu
354
354
  screenshots?: {
355
355
  enabled?: boolean, // default true
356
- strictScrub?: boolean, // default false also masks every input/select/textarea
356
+ strictScrub?: boolean, // default false; also masks every input/select/textarea
357
357
  redactSelector?: string, // extra CSS selector to mask
358
358
  },
359
- }) // returns a destroy() function call it on unmount / logout / route teardown
359
+ }) // returns a destroy() function; call it on unmount / logout / route teardown
360
360
  ```
361
361
 
362
- There is no `fab` option any more — it's accepted, ignored, and warns once. Drop it if you find one in my config.
362
+ There is no `fab` option any more. It's accepted, ignored, and warns once. Drop it if you find one in my config.
363
363
 
364
364
  For React/Vue/Solid hosts, call `init()` from a mount lifecycle hook
365
365
  (`useEffect`, `onMounted`, `onMount`) and call the returned `destroy` on
366
- cleanup. There's no framework-specific entrypoint `init` is the entire
367
- public surface.
366
+ cleanup. There's no framework-specific entrypoint; `init` is the whole
367
+ public API.
368
368
 
369
369
  For a `<script>` tag drop-in (no bundler), use the inline ESM form and **pin the version**:
370
370
 
371
371
  ```html
372
372
  <script type="module">
373
- import { init } from 'https://esm.sh/@pixelmatters/markup@1.18.4'
373
+ import { init } from 'https://esm.sh/@pixelmatters/markup@1.18.6'
374
374
 
375
375
  init({
376
376
  apiUrl: '...',
@@ -386,7 +386,7 @@ If inline JS is disallowed (some CMS / page-builder editors), use the auto-init
386
386
  ```html
387
387
  <script
388
388
  type="module"
389
- src="https://esm.sh/@pixelmatters/markup@1.18.4"
389
+ src="https://esm.sh/@pixelmatters/markup@1.18.6"
390
390
  data-markup-widget="true"
391
391
  data-api-url="..."
392
392
  data-api-key="..."
@@ -405,7 +405,7 @@ If inline JS is disallowed (some CMS / page-builder editors), use the auto-init
405
405
 
406
406
  Constraints:
407
407
 
408
- - Do **not** add CSS imports or provider components the widget needs neither.
408
+ - Do **not** add CSS imports or provider components. The widget needs neither.
409
409
  - Do **not** hardcode the key.
410
410
  - 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.
411
411
  ````
@@ -419,14 +419,14 @@ If the host page ships a CSP, the widget needs:
419
419
  | `connect-src` | `https://<deployment>.convex.site` | threads, comments, identity, screenshot upload, error reports |
420
420
  | `connect-src` | `wss://<deployment>.convex.cloud` | live thread updates |
421
421
  | `img-src` | wherever your team's profile pictures are hosted, plus `blob:` and `data:` | avatars, the screenshot preview thumbnail, and the capture pipeline |
422
- | `script-src` | `https://esm.sh` | CDN path only a bundled install needs nothing here |
422
+ | `script-src` | `https://esm.sh` | CDN path only; a bundled install needs nothing here |
423
423
  | `style-src` | `'unsafe-inline'` | the widget appends its stylesheet as a `<style>` element inside its own shadow root |
424
424
 
425
425
  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.
426
426
 
427
427
  ## Browser support
428
428
 
429
- Modern evergreen browsers (Chrome, Edge, Firefox, Safari) and their mobile equivalents. The widget uses native ESM, shadow DOM, and `IntersectionObserver` no IE11 / legacy bundle.
429
+ Modern evergreen browsers (Chrome, Edge, Firefox, Safari) and their mobile equivalents. The widget uses native ESM, shadow DOM, and `IntersectionObserver`, with no IE11 or legacy bundle.
430
430
 
431
431
  ## Self-hosting Markup
432
432