@pixelmatters/markup 1.16.0 → 1.17.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 +101 -22
- package/dist/widget.js +1304 -993
- package/dist/widget.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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.
|
|
40
|
+
import { init } from 'https://esm.sh/@pixelmatters/markup@1.17.0'
|
|
39
41
|
// or
|
|
40
|
-
// import { init } from 'https://esm.run/@pixelmatters/markup@1.
|
|
42
|
+
// import { init } from 'https://esm.run/@pixelmatters/markup@1.17.0'
|
|
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.
|
|
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.17.0`).
|
|
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.
|
|
60
|
+
src="https://esm.sh/@pixelmatters/markup@1.17.0"
|
|
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,64 @@ 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
|
|
155
|
-
|
|
|
156
|
-
| `apiUrl`
|
|
157
|
-
| `apiKey`
|
|
158
|
-
| `position`
|
|
159
|
-
| `theme`
|
|
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, **Privacy & data**, **Keyboard shortcuts**, **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
|
+
|
|
198
|
+
The last two menu rows open panels anchored to the same corner as the toolbar:
|
|
199
|
+
|
|
200
|
+
- **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.
|
|
201
|
+
- **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.
|
|
202
|
+
|
|
165
203
|
### Keyboard & mouse
|
|
166
204
|
|
|
167
|
-
| Shortcut | Action
|
|
168
|
-
| --------------------- |
|
|
169
|
-
| `c` | Start placing a markup (ignored while typing)
|
|
170
|
-
|
|
|
171
|
-
| `cmd/ctrl +
|
|
172
|
-
| `
|
|
173
|
-
|
|
|
205
|
+
| Shortcut | Action |
|
|
206
|
+
| --------------------- | --------------------------------------------------------------------------------- |
|
|
207
|
+
| `c` | Start placing a markup (ignored while typing) |
|
|
208
|
+
| `@` | In a composer, open the member picker — `↑`/`↓` to move, `enter` or `tab` to pick |
|
|
209
|
+
| `cmd/ctrl + enter` | Post the comment being written |
|
|
210
|
+
| `esc` | Cancel placement, dismiss the mention picker, or close the open popover / menu |
|
|
211
|
+
| `cmd/ctrl + .` | Toggle HUD visibility |
|
|
212
|
+
| `cmd/ctrl + click` | Click the toolbar's comment button to hide the HUD with a hint toast |
|
|
213
|
+
| Drag a popover header | Move the open thread / new-thread popover; resets to the pin on reopen |
|
|
174
214
|
|
|
175
215
|
## Screenshots & privacy
|
|
176
216
|
|
|
@@ -200,12 +240,25 @@ By default, the widget captures the visible viewport as a JPEG before you submit
|
|
|
200
240
|
|
|
201
241
|
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
242
|
|
|
243
|
+
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.
|
|
244
|
+
|
|
245
|
+
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.
|
|
246
|
+
|
|
247
|
+
### When a capture doesn't work out
|
|
248
|
+
|
|
249
|
+
Capture degrades instead of failing silently:
|
|
250
|
+
|
|
251
|
+
- **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.
|
|
252
|
+
- **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.
|
|
253
|
+
- **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.
|
|
254
|
+
|
|
203
255
|
## How it works
|
|
204
256
|
|
|
205
257
|
- The widget mounts `<div id="markup-widget">` on `document.body` and attaches an open shadow root.
|
|
206
258
|
- All UI lives in that shadow root, with `:host { all: initial }` blocking style inheritance.
|
|
207
259
|
- 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
260
|
- 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.
|
|
261
|
+
- 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
262
|
- 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
263
|
|
|
211
264
|
## Identity
|
|
@@ -217,6 +270,10 @@ Anonymous by default, with two opt-in upgrade paths:
|
|
|
217
270
|
|
|
218
271
|
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
272
|
|
|
273
|
+
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.
|
|
274
|
+
|
|
275
|
+
**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.
|
|
276
|
+
|
|
220
277
|
> **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
278
|
|
|
222
279
|
## Getting an API key
|
|
@@ -259,9 +316,17 @@ init({
|
|
|
259
316
|
apiKey: string, // required
|
|
260
317
|
position?: 'bottom-right' | 'bottom-left' | 'bottom-center', // default 'bottom-right'
|
|
261
318
|
theme?: 'light' | 'dark' | 'auto', // default 'auto'
|
|
319
|
+
dashboardUrl?: string, // optional: adds an "Account →" link to the identity menu
|
|
320
|
+
screenshots?: {
|
|
321
|
+
enabled?: boolean, // default true
|
|
322
|
+
strictScrub?: boolean, // default false — also masks every input/select/textarea
|
|
323
|
+
redactSelector?: string, // extra CSS selector to mask
|
|
324
|
+
},
|
|
262
325
|
}) // returns a destroy() function — call it on unmount / logout / route teardown
|
|
263
326
|
```
|
|
264
327
|
|
|
328
|
+
There is no `fab` option any more — it's accepted, ignored, and warns once. Drop it if you find one in my config.
|
|
329
|
+
|
|
265
330
|
For React/Vue/Solid hosts, call `init()` from a mount lifecycle hook
|
|
266
331
|
(`useEffect`, `onMounted`, `onMount`) and call the returned `destroy` on
|
|
267
332
|
cleanup. There's no framework-specific entrypoint — `init` is the entire
|
|
@@ -271,7 +336,7 @@ For a `<script>` tag drop-in (no bundler), use the inline ESM form and **pin the
|
|
|
271
336
|
|
|
272
337
|
```html
|
|
273
338
|
<script type="module">
|
|
274
|
-
import { init } from 'https://esm.sh/@pixelmatters/markup@1.
|
|
339
|
+
import { init } from 'https://esm.sh/@pixelmatters/markup@1.17.0'
|
|
275
340
|
|
|
276
341
|
init({
|
|
277
342
|
apiUrl: '...',
|
|
@@ -287,7 +352,7 @@ If inline JS is disallowed (some CMS / page-builder editors), use the auto-init
|
|
|
287
352
|
```html
|
|
288
353
|
<script
|
|
289
354
|
type="module"
|
|
290
|
-
src="https://esm.sh/@pixelmatters/markup@1.
|
|
355
|
+
src="https://esm.sh/@pixelmatters/markup@1.17.0"
|
|
291
356
|
data-markup-widget="true"
|
|
292
357
|
data-api-url="..."
|
|
293
358
|
data-api-key="..."
|
|
@@ -308,9 +373,23 @@ Constraints:
|
|
|
308
373
|
|
|
309
374
|
- Do **not** add CSS imports or provider components — the widget needs neither.
|
|
310
375
|
- Do **not** hardcode the key.
|
|
311
|
-
- If the project has a CSP
|
|
376
|
+
- 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
377
|
````
|
|
313
378
|
|
|
379
|
+
## Content Security Policy
|
|
380
|
+
|
|
381
|
+
If the host page ships a CSP, the widget needs:
|
|
382
|
+
|
|
383
|
+
| Directive | Value | Why |
|
|
384
|
+
| ------------- | -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
|
|
385
|
+
| `connect-src` | `https://<deployment>.convex.site` | threads, comments, identity, screenshot upload, error reports |
|
|
386
|
+
| `connect-src` | `wss://<deployment>.convex.cloud` | live thread updates |
|
|
387
|
+
| `img-src` | wherever your team's profile pictures are hosted, plus `blob:` and `data:` | avatars, the screenshot preview thumbnail, and the capture pipeline |
|
|
388
|
+
| `script-src` | `https://esm.sh` | CDN path only — a bundled install needs nothing here |
|
|
389
|
+
| `style-src` | `'unsafe-inline'` | the widget appends its stylesheet as a `<style>` element inside its own shadow root |
|
|
390
|
+
|
|
391
|
+
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.
|
|
392
|
+
|
|
314
393
|
## Browser support
|
|
315
394
|
|
|
316
395
|
Modern evergreen browsers (Chrome, Edge, Firefox, Safari) and their mobile equivalents. The widget uses native ESM, shadow DOM, and `IntersectionObserver` — no IE11 / legacy bundle.
|