@pixelmatters/markup 1.27.3 → 1.28.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 +75 -37
- package/dist/react.d.ts +21 -0
- package/dist/react.js +15 -0
- package/dist/react.js.map +1 -0
- package/dist/shared-mount-DABK4arh.js +27 -0
- package/dist/shared-mount-DABK4arh.js.map +1 -0
- package/dist/vue.d.ts +23 -0
- package/dist/vue.js +21 -0
- package/dist/vue.js.map +1 -0
- package/dist/widget.js +1879 -1786
- package/dist/widget.js.map +1 -1
- package/package.json +31 -6
- package/skills/install-markup-widget/SKILL.md +47 -37
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pixelmatters/markup",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.28.1",
|
|
4
4
|
"description": "Embeddable feedback widget for collecting visual bug reports, screenshots, and comments on live web apps.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"annotation",
|
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
"directory": "packages/widget"
|
|
26
26
|
},
|
|
27
27
|
"files": [
|
|
28
|
-
"dist
|
|
29
|
-
"dist
|
|
30
|
-
"dist
|
|
28
|
+
"dist/*.js",
|
|
29
|
+
"dist/*.js.map",
|
|
30
|
+
"dist/*.d.ts",
|
|
31
31
|
"skills",
|
|
32
32
|
"!skills/_artifacts",
|
|
33
33
|
"README.md",
|
|
@@ -40,6 +40,14 @@
|
|
|
40
40
|
"types": "./dist/widget.d.ts",
|
|
41
41
|
"import": "./dist/widget.js"
|
|
42
42
|
},
|
|
43
|
+
"./react": {
|
|
44
|
+
"types": "./dist/react.d.ts",
|
|
45
|
+
"import": "./dist/react.js"
|
|
46
|
+
},
|
|
47
|
+
"./vue": {
|
|
48
|
+
"types": "./dist/vue.d.ts",
|
|
49
|
+
"import": "./dist/vue.js"
|
|
50
|
+
},
|
|
43
51
|
"./package.json": "./package.json"
|
|
44
52
|
},
|
|
45
53
|
"publishConfig": {
|
|
@@ -50,18 +58,35 @@
|
|
|
50
58
|
"@playwright/test": "^1.63.0",
|
|
51
59
|
"@preact/preset-vite": "^2.10.6",
|
|
52
60
|
"@tanstack/intent": "^0.4.0",
|
|
61
|
+
"@types/react": "^19.3.0",
|
|
62
|
+
"@types/react-dom": "^19.3.0",
|
|
53
63
|
"convex": "^1.38.0",
|
|
54
64
|
"html-to-image": "^1.11.13",
|
|
55
65
|
"preact": "^10.29.8",
|
|
66
|
+
"react": "^19.3.0",
|
|
67
|
+
"react-dom": "^19.3.0",
|
|
56
68
|
"typescript": "^6.0.3",
|
|
57
69
|
"vite": "^8.3.0",
|
|
58
|
-
"vite-plugin-dts": "^5.1.0"
|
|
70
|
+
"vite-plugin-dts": "^5.1.0",
|
|
71
|
+
"vue": "^3.5.43"
|
|
72
|
+
},
|
|
73
|
+
"peerDependencies": {
|
|
74
|
+
"react": ">=18",
|
|
75
|
+
"vue": ">=3.3"
|
|
76
|
+
},
|
|
77
|
+
"peerDependenciesMeta": {
|
|
78
|
+
"react": {
|
|
79
|
+
"optional": true
|
|
80
|
+
},
|
|
81
|
+
"vue": {
|
|
82
|
+
"optional": true
|
|
83
|
+
}
|
|
59
84
|
},
|
|
60
85
|
"engines": {
|
|
61
86
|
"node": ">=18"
|
|
62
87
|
},
|
|
63
88
|
"scripts": {
|
|
64
|
-
"build": "vite build && rm -rf dist/runtime",
|
|
89
|
+
"build": "vite build && rm -rf dist/runtime dist/lib",
|
|
65
90
|
"dev": "vite",
|
|
66
91
|
"test:e2e": "playwright test",
|
|
67
92
|
"test:e2e:ui": "playwright test --ui",
|
|
@@ -7,7 +7,7 @@ sources:
|
|
|
7
7
|
- 'Pixelmatters/markup:packages/widget/src/widget.ts'
|
|
8
8
|
metadata:
|
|
9
9
|
library: pixelmatters-markup
|
|
10
|
-
library_version: '1.
|
|
10
|
+
library_version: '1.28.1'
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
`@pixelmatters/markup` is a Preact widget that runs inside a shadow DOM and talks to a hosted Convex backend at `https://<deployment>.convex.site`. The host page calls `init({ apiUrl, apiKey })` once at the app root and gets back a `destroy()` function. Everything else lives inside the widget bundle: pin anchoring, threads, mentions, identity, screenshots, real-time updates.
|
|
@@ -26,7 +26,7 @@ For a `<script>`-tag drop-in (no bundler / CMS / page-builder), use the inline E
|
|
|
26
26
|
|
|
27
27
|
```html
|
|
28
28
|
<script type="module">
|
|
29
|
-
import { init } from 'https://esm.sh/@pixelmatters/markup@1.
|
|
29
|
+
import { init } from 'https://esm.sh/@pixelmatters/markup@1.28.1'
|
|
30
30
|
init({ apiUrl: '…', apiKey: '…' })
|
|
31
31
|
</script>
|
|
32
32
|
```
|
|
@@ -36,7 +36,7 @@ If the host disallows inline JS, use the auto-init form. `data-markup-widget="tr
|
|
|
36
36
|
```html
|
|
37
37
|
<script
|
|
38
38
|
type="module"
|
|
39
|
-
src="https://esm.sh/@pixelmatters/markup@1.
|
|
39
|
+
src="https://esm.sh/@pixelmatters/markup@1.28.1"
|
|
40
40
|
data-markup-widget="true"
|
|
41
41
|
data-api-url="…"
|
|
42
42
|
data-api-key="…"
|
|
@@ -49,44 +49,48 @@ The recognised attributes are `data-api-url`, `data-api-key`, `data-position`, `
|
|
|
49
49
|
|
|
50
50
|
## Wire-up
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
React (18+) and Vue (3.3+) use `useMarkup` from `@pixelmatters/markup/react` or `@pixelmatters/markup/vue`, with no provider to wrap. Every other framework calls `init()` from a mount hook (`onMount`, …) and the function it returns on cleanup. A module-level `destroy` is exported too, for call sites that can't hold onto the returned one; it tears down whichever instance is currently mounted.
|
|
53
53
|
|
|
54
54
|
```ts
|
|
55
55
|
import { init, destroy } from '@pixelmatters/markup'
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
-
`init` returns the matching `destroy`. Mount **once at the app root**, not per route. The widget patches `history.pushState` / `replaceState` and listens for `popstate` and `hashchange` itself, so SPAs work without remounting, hash routers included. Calling `init`
|
|
58
|
+
`init` returns the matching `destroy`. Mount **once at the app root**, not per route. The widget patches `history.pushState` / `replaceState` and listens for `popstate` and `hashchange` itself, so SPAs work without remounting, hash routers included. Calling `init` again with an equal config is a no-op, and a different config tears the previous mount down first, so a render loop that keeps changing the config will thrash. Anchor it to a one-time mount lifecycle.
|
|
59
59
|
|
|
60
60
|
If `apiUrl` or `apiKey` is missing, `init` logs a `[markup]` console warning and returns a no-op `destroy` rather than mounting, which is worth knowing when debugging "nothing showed up."
|
|
61
61
|
|
|
62
|
-
```
|
|
63
|
-
// React
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
```tsx
|
|
63
|
+
// React: render <Markup /> once near the root. Add 'use client' on Next.js App Router.
|
|
64
|
+
import { useMarkup } from '@pixelmatters/markup/react'
|
|
65
|
+
|
|
66
|
+
export function Markup() {
|
|
67
|
+
useMarkup({
|
|
66
68
|
apiUrl: import.meta.env.VITE_MARKUP_API_URL,
|
|
67
69
|
apiKey: import.meta.env.VITE_MARKUP_API_KEY,
|
|
68
70
|
})
|
|
69
|
-
|
|
71
|
+
return null
|
|
72
|
+
}
|
|
70
73
|
```
|
|
71
74
|
|
|
75
|
+
The hook takes `init()`'s options plus `enabled` (default `true`; `false` unmounts). An inline object is fine: it remounts only when a value changes, survives StrictMode's double mount, and mounts nothing during server rendering. Don't combine it with a separate `init()` call.
|
|
76
|
+
|
|
72
77
|
```vue
|
|
73
|
-
<!-- Vue 3 -->
|
|
78
|
+
<!-- Vue 3, in the root component -->
|
|
74
79
|
<script setup lang="ts">
|
|
75
|
-
import {
|
|
76
|
-
|
|
77
|
-
let stop: (() => void) | undefined
|
|
78
|
-
onMounted(() => { stop = init({ apiUrl: …, apiKey: … }) })
|
|
79
|
-
onBeforeUnmount(() => stop?.())
|
|
80
|
+
import { useMarkup } from '@pixelmatters/markup/vue'
|
|
81
|
+
useMarkup({ apiUrl: …, apiKey: … })
|
|
80
82
|
</script>
|
|
81
83
|
```
|
|
82
84
|
|
|
85
|
+
The Vue composable takes the same options as a plain object, a ref, or a getter, and remounts only when a value changes.
|
|
86
|
+
|
|
83
87
|
```ts
|
|
84
88
|
// Plain HTML / vanilla
|
|
85
89
|
const stop = init({ apiUrl, apiKey })
|
|
86
90
|
window.addEventListener('beforeunload', stop)
|
|
87
91
|
```
|
|
88
92
|
|
|
89
|
-
Tear down on logout if you don't want the widget visible to signed-out users. `init` is safe to call repeatedly, and
|
|
93
|
+
Tear down on logout if you don't want the widget visible to signed-out users (`enabled: false` with `useMarkup`). `init` is safe to call repeatedly: an equal config is a no-op, and a different one replaces the previous instance.
|
|
90
94
|
|
|
91
95
|
## Credentials
|
|
92
96
|
|
|
@@ -154,6 +158,13 @@ End users can also switch capture off for themselves with **Auto-capture screens
|
|
|
154
158
|
|
|
155
159
|
Capture degrades rather than failing outright: an image the browser won't hand over (a third-party avatar served without CORS headers is the usual culprit) comes through blank while the rest of the page captures; the raster is capped at 1.5x device pixel ratio and encoded as WebP (JPEG where that isn't supported), then walked down a quality-then-scale ladder until it lands near 400 KB, with the server's 2 MB cap as the hard limit; and if it still can't produce an image the composer says _Screenshot unavailable_ and the comment posts without one.
|
|
156
160
|
|
|
161
|
+
### Anchors & source paths
|
|
162
|
+
|
|
163
|
+
Optional, and worth suggesting on a component-based app:
|
|
164
|
+
|
|
165
|
+
- `data-markup-anchor="<name>"` gives pin selectors a stable name ahead of ids and test attributes. Treat names like ids: a pin on a uniquely named element survives text changes, while a name repeated across elements only adds to the positional path. For a reorderable list, name each item with its record id (`data-markup-anchor="todo-42"`). Without names the widget still re-finds a moved item by its text when that text is unique in the list, but a pin on one of several identical controls (an **Edit** button per row) stays with whichever row now holds its old position.
|
|
166
|
+
- `data-markup-source="<repo-relative path>"` is stored with every thread pinned inside that element and returned to agents over MCP as `anchorSource`. Put it on the component's root element; values over 512 characters are ignored.
|
|
167
|
+
|
|
157
168
|
## Mentions & inbox
|
|
158
169
|
|
|
159
170
|
Typing `@` in a composer opens a picker of project members and inserts a chip, and the stored `@[Display Name](userId)` form never reaches the screen. `↑`/`↓` move, `enter` or `tab` picks, `esc` dismisses. It works for anonymous visitors too; the member list is fetched once per mount with the API key, and a failed fetch degrades to "no picker, no chips" rather than blocking the widget.
|
|
@@ -179,24 +190,24 @@ API key belongs to a different project than the one being watched.
|
|
|
179
190
|
|
|
180
191
|
Walk these in order when the widget is misbehaving:
|
|
181
192
|
|
|
182
|
-
| Symptom | Cause
|
|
183
|
-
| --------------------------------------------------------------------------------------- |
|
|
184
|
-
| Toolbar doesn't appear, console warns `[markup] init() requires both apiUrl and apiKey` | One of the env vars is unset / undefined at the call site
|
|
185
|
-
| Toolbar doesn't appear, console shows `403 Origin not allowed` | Host is not in `allowedDomains`
|
|
186
|
-
| Toolbar renders unstyled, or threads never load | CSP is missing `style-src 'unsafe-inline'` (the stylesheet is a `<style>` element in the shadow root) or the deployment origins under `connect-src`
|
|
187
|
-
| Comments show initials where profile pictures are expected | The host's CSP `img-src` blocks the picture host, or the account has none set
|
|
188
|
-
| `@` types a literal character, no picker | The member fetch failed (a `403`/`429` on `/widget/members` degrades silently by design)
|
|
189
|
-
| `401 Invalid api key` | Key revoked, wrong project, or copy-paste truncation
|
|
190
|
-
| `429` + `Retry-After` header | Rate-limited (per-key pre-auth or per-project bucket)
|
|
191
|
-
| `400 Invalid anchor` / `Invalid viewport` / `Field too long` | Widget POSTed a coord outside `[0..1]`, a non-finite viewport size, or an over-long string (route > 256, url > 2048, userAgent > 500, anchorSelector > 1024, anchorText > 256, authorEmail > 320) | The bundled widget always stays inside these limits, so this only fires if something between the widget and the API rewrote the payload. Check for a misbehaving service worker or proxy
|
|
192
|
-
| Pins drift after host layout changes | Selector resolution failed; falling back to viewport fractions
|
|
193
|
-
| Pins disappear after route change in an SPA | `init` was called per-route and remounted state
|
|
194
|
-
| Widget styling looks broken inside an iframe | Shadow DOM doesn't pierce frame boundaries
|
|
195
|
-
| Verified identity doesn't survive reload | Host code clears `localStorage['markup.identity']` on logout
|
|
196
|
-
| Popup sign-in flashes and closes | Popup origin failed `allowedDomains` check, or the stored JWT was expired and dropped
|
|
197
|
-
| HMR leaves duplicate toolbars in dev | Hot reload re-runs `init` without cleanup
|
|
198
|
-
| Screenshot is missing my custom field | Auto-mask matched the input (e.g. `autocomplete="cc-number"`) or your selector
|
|
199
|
-
| Widget invisible behind host UI | Z-index conflict on the shadow host element
|
|
193
|
+
| Symptom | Cause | Fix |
|
|
194
|
+
| --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
195
|
+
| Toolbar doesn't appear, console warns `[markup] init() requires both apiUrl and apiKey` | One of the env vars is unset / undefined at the call site | Confirm the framework's env-var prefix (`VITE_…`, `NEXT_PUBLIC_…`, etc.) and that `.env` is being loaded |
|
|
196
|
+
| Toolbar doesn't appear, console shows `403 Origin not allowed` | Host is not in `allowedDomains` | Dashboard → Settings → Domains, add the exact host (`app.example.com`) or a wildcard (`*.example.com` matches any subdomain depth) |
|
|
197
|
+
| Toolbar renders unstyled, or threads never load | CSP is missing `style-src 'unsafe-inline'` (the stylesheet is a `<style>` element in the shadow root) or the deployment origins under `connect-src` | See the CSP bullet under **Don'ts** |
|
|
198
|
+
| Comments show initials where profile pictures are expected | The host's CSP `img-src` blocks the picture host, or the account has none set | Add the picture host to `img-src`. Initials are the intended fallback, not a bug |
|
|
199
|
+
| `@` types a literal character, no picker | The member fetch failed (a `403`/`429` on `/widget/members` degrades silently by design) | Check the network tab for `/widget/members`; the usual cause is the domain allowlist |
|
|
200
|
+
| `401 Invalid api key` | Key revoked, wrong project, or copy-paste truncation | Mint a fresh key and replace the env var |
|
|
201
|
+
| `429` + `Retry-After` header | Rate-limited (per-key pre-auth or per-project bucket) | Stop polling; respect the `Retry-After` seconds. If hit during normal use, ask the user to contact support to raise the project limit |
|
|
202
|
+
| `400 Invalid anchor` / `Invalid viewport` / `Field too long` | Widget POSTed a coord outside `[0..1]`, a non-finite viewport size, or an over-long string (route > 256, url > 2048, userAgent > 500, anchorSelector > 1024, anchorText > 256, anchorSource > 512, authorEmail > 320) | The bundled widget always stays inside these limits, so this only fires if something between the widget and the API rewrote the payload. Check for a misbehaving service worker or proxy |
|
|
203
|
+
| Pins drift after host layout changes | Selector resolution failed; falling back to viewport fractions | Expected. Use stable IDs / data-attributes on anchor targets if precision matters |
|
|
204
|
+
| Pins disappear after route change in an SPA | `init` was called per-route and remounted state | Move `init` to a single root-level mount; the widget handles history itself |
|
|
205
|
+
| Widget styling looks broken inside an iframe | Shadow DOM doesn't pierce frame boundaries | Mount the widget **inside** the iframe document, not the parent |
|
|
206
|
+
| Verified identity doesn't survive reload | Host code clears `localStorage['markup.identity']` on logout | Only clear it on Markup-specific sign-out, not on host logout |
|
|
207
|
+
| Popup sign-in flashes and closes | Popup origin failed `allowedDomains` check, or the stored JWT was expired and dropped | Add the host to the project's allowlist; the popup re-validates. Expired JWTs are dropped on the next load, so sign in again |
|
|
208
|
+
| HMR leaves duplicate toolbars in dev | Hot reload re-runs `init` without cleanup | Use `useMarkup` on React, or return the `destroy` from `onMounted` / `onMount` so HMR can call it. (The history wrapper itself is module-scoped, so repeated init/destroy cycles no longer leak listeners.) |
|
|
209
|
+
| Screenshot is missing my custom field | Auto-mask matched the input (e.g. `autocomplete="cc-number"`) or your selector | Inspect with the widget devtools panel; add `data-markup-safe` on the wrapper to exempt it from auto-detection |
|
|
210
|
+
| Widget invisible behind host UI | Z-index conflict on the shadow host element | The widget mounts `<div id="markup-widget">` at `document.body`; raise its `z-index` from host CSS if you must |
|
|
200
211
|
|
|
201
212
|
## Don'ts
|
|
202
213
|
|
|
@@ -204,7 +215,7 @@ Walk these in order when the widget is misbehaving:
|
|
|
204
215
|
- **Don't wrap in a provider component.** `init` is the whole public API.
|
|
205
216
|
- **Don't call `init` inside a route component.** Mount at the app root once.
|
|
206
217
|
- **Don't hardcode the API key.** Use env vars so rotations don't require a code change.
|
|
207
|
-
- **Don't ship CDN URLs without a version pin.** `@pixelmatters/markup@1.
|
|
218
|
+
- **Don't ship CDN URLs without a version pin.** `@pixelmatters/markup@1.28.1`, not `@pixelmatters/markup`.
|
|
208
219
|
- **Don't add `https://*.convex.site` to `connect-src` and assume that's the whole CSP story.** The deployment domain (`<your-deployment>.convex.site`) is what the widget hits over HTTP, and it must be listed explicitly. Live thread updates go to `wss://<your-deployment>.convex.cloud`, an origin the widget derives from `apiUrl`, so `connect-src` needs both. `img-src` needs `blob:`, `data:`, and the host serving profile pictures; `style-src` needs `'unsafe-inline'` because the widget appends its stylesheet as a `<style>` element inside its shadow root. Add `https://esm.sh` to `script-src` only if you took the CDN path.
|
|
209
220
|
|
|
210
221
|
## Versioning & releases
|
|
@@ -215,7 +226,6 @@ The widget follows semver. Pin to a major in `package.json` (`^1.5.0`) so patch
|
|
|
215
226
|
|
|
216
227
|
- Install tutorial: https://markup.pixelmatters.dev/docs/install
|
|
217
228
|
- Package on npm: https://www.npmjs.com/package/@pixelmatters/markup
|
|
218
|
-
- Source: https://github.com/Pixelmatters/markup
|
|
219
229
|
|
|
220
230
|
## When in doubt
|
|
221
231
|
|