@pixelmatters/markup 1.18.1 → 1.18.3
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 +20 -8
- package/dist/widget.js +12 -8
- package/dist/widget.js.map +1 -1
- package/package.json +7 -2
- package/skills/install-markup-widget/SKILL.md +221 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pixelmatters/markup",
|
|
3
|
-
"version": "1.18.
|
|
3
|
+
"version": "1.18.3",
|
|
4
4
|
"description": "Embeddable feedback widget for collecting visual bug reports, screenshots, and comments on live web apps.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"annotation",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"pixelmatters",
|
|
11
11
|
"preact",
|
|
12
12
|
"screenshot",
|
|
13
|
+
"tanstack-intent",
|
|
13
14
|
"widget"
|
|
14
15
|
],
|
|
15
16
|
"homepage": "https://github.com/Pixelmatters/markup#readme",
|
|
@@ -27,6 +28,8 @@
|
|
|
27
28
|
"dist/widget.js",
|
|
28
29
|
"dist/widget.js.map",
|
|
29
30
|
"dist/widget.d.ts",
|
|
31
|
+
"skills",
|
|
32
|
+
"!skills/_artifacts",
|
|
30
33
|
"README.md",
|
|
31
34
|
"LICENSE"
|
|
32
35
|
],
|
|
@@ -47,6 +50,7 @@
|
|
|
47
50
|
"@medv/finder": "^4.0.2",
|
|
48
51
|
"@playwright/test": "^1.62.1",
|
|
49
52
|
"@preact/preset-vite": "^2.10.6",
|
|
53
|
+
"@tanstack/intent": "0.3.6",
|
|
50
54
|
"convex": "^1.38.0",
|
|
51
55
|
"html-to-image": "^1.11.13",
|
|
52
56
|
"preact": "^10.29.7",
|
|
@@ -62,6 +66,7 @@
|
|
|
62
66
|
"dev": "vite",
|
|
63
67
|
"test:e2e": "playwright test",
|
|
64
68
|
"test:e2e:ui": "playwright test --ui",
|
|
65
|
-
"typecheck": "tsc --noEmit"
|
|
69
|
+
"typecheck": "tsc --noEmit",
|
|
70
|
+
"validate:skills": "intent validate"
|
|
66
71
|
}
|
|
67
72
|
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: install-markup-widget
|
|
3
|
+
description: Install, configure, and troubleshoot the `@pixelmatters/markup` feedback widget — a drop-in script that lets end-users pin threaded comments on any web page. Use this skill whenever the host project depends on `@pixelmatters/markup`, imports `init` / `destroy` from it, references a `markup_…` API key, or the user asks about installing / debugging the Markup feedback widget.
|
|
4
|
+
sources:
|
|
5
|
+
- 'Pixelmatters/markup:packages/widget/README.md'
|
|
6
|
+
- 'Pixelmatters/markup:docs/widget-guide.md'
|
|
7
|
+
- 'Pixelmatters/markup:packages/widget/src/widget.ts'
|
|
8
|
+
metadata:
|
|
9
|
+
library: pixelmatters-markup
|
|
10
|
+
library_version: '1.18.3'
|
|
11
|
+
---
|
|
12
|
+
|
|
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 — pin anchoring, threads, mentions, identity, screenshots, real-time updates — lives inside the widget bundle.
|
|
14
|
+
|
|
15
|
+
The widget renders a single compact **toolbar pill** in one bottom corner: comment, inbox (signed-in users only), pin-visibility toggle, identity menu, and an overflow menu holding Appearance, Position, an auto-capture toggle, and "Hide for this session". Installs predating 1.15.0 described a floating action button — that's gone, along with drag-to-reposition.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
Detect the package manager (presence of `pnpm-lock.yaml` / `yarn.lock` / `package-lock.json`) and use it:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pnpm add @pixelmatters/markup # or: yarn add … / npm install …
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
For a `<script>`-tag drop-in (no bundler / CMS / page-builder), use the inline ESM form and **always pin the version**:
|
|
26
|
+
|
|
27
|
+
```html
|
|
28
|
+
<script type="module">
|
|
29
|
+
import { init } from 'https://esm.sh/@pixelmatters/markup@1.18.3'
|
|
30
|
+
init({ apiUrl: '…', apiKey: '…' })
|
|
31
|
+
</script>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
If the host disallows inline JS, use the auto-init form. `data-markup-widget="true"` is required — it's how the bundle finds its own script element under `type="module"`:
|
|
35
|
+
|
|
36
|
+
```html
|
|
37
|
+
<script
|
|
38
|
+
type="module"
|
|
39
|
+
src="https://esm.sh/@pixelmatters/markup@1.18.3"
|
|
40
|
+
data-markup-widget="true"
|
|
41
|
+
data-api-url="…"
|
|
42
|
+
data-api-key="…"
|
|
43
|
+
data-position="bottom-right"
|
|
44
|
+
data-theme="auto"
|
|
45
|
+
></script>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The recognised attributes are `data-api-url`, `data-api-key`, `data-position`, `data-theme`, and `data-dashboard-url`. The `screenshots` options are `init()`-only.
|
|
49
|
+
|
|
50
|
+
## Wire-up
|
|
51
|
+
|
|
52
|
+
There is no framework-specific entrypoint — no `@pixelmatters/markup/react`, no provider to wrap. Every framework calls `init()` from a mount hook (`useEffect`, `onMounted`, `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
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { init, destroy } from '@pixelmatters/markup'
|
|
56
|
+
```
|
|
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` itself, so SPAs work without remounting. Calling `init` repeatedly is safe (the history wrapper is module-scoped, so init/destroy cycles don't stack), but it tears down the previous mount each time — so a render loop calling `init` will thrash. Always anchor it to a one-time mount lifecycle.
|
|
59
|
+
|
|
60
|
+
If `apiUrl` or `apiKey` is missing, `init` logs a `[markup]` console warning and returns a no-op `destroy` rather than mounting — useful to know when debugging "nothing showed up."
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
// React — App.tsx
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
return init({
|
|
66
|
+
apiUrl: import.meta.env.VITE_MARKUP_API_URL,
|
|
67
|
+
apiKey: import.meta.env.VITE_MARKUP_API_KEY,
|
|
68
|
+
})
|
|
69
|
+
}, [])
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
```vue
|
|
73
|
+
<!-- Vue 3 -->
|
|
74
|
+
<script setup lang="ts">
|
|
75
|
+
import { onMounted, onBeforeUnmount } from 'vue'
|
|
76
|
+
import { init } from '@pixelmatters/markup'
|
|
77
|
+
let stop: (() => void) | undefined
|
|
78
|
+
onMounted(() => { stop = init({ apiUrl: …, apiKey: … }) })
|
|
79
|
+
onBeforeUnmount(() => stop?.())
|
|
80
|
+
</script>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
// Plain HTML / vanilla
|
|
85
|
+
const stop = init({ apiUrl, apiKey })
|
|
86
|
+
window.addEventListener('beforeunload', stop)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Tear down on logout if you don't want the widget visible to signed-out users. `init` is safe to call repeatedly — each call destroys the previous instance first.
|
|
90
|
+
|
|
91
|
+
## Credentials
|
|
92
|
+
|
|
93
|
+
- `apiUrl`: the project's Convex deployment site URL — copy from **Markup dashboard → Settings → Install**. Format: `https://<deployment>.convex.site`.
|
|
94
|
+
- `apiKey`: mint from **Settings → API Keys**. The raw key is shown **exactly once**; if it's lost, revoke and mint a new one.
|
|
95
|
+
|
|
96
|
+
Conventions:
|
|
97
|
+
|
|
98
|
+
- Store both in env vars (`VITE_MARKUP_API_URL`, `VITE_MARKUP_API_KEY`, or the framework equivalent — `NEXT_PUBLIC_…`, `PUBLIC_…`).
|
|
99
|
+
- Add to `.env.example`. Ensure `.env*.local` is in `.gitignore` (most Vite / Next templates already do this).
|
|
100
|
+
- The API key is public-by-design — it's gated server-side by the project's `allowedDomains`. Treating it like a secret anyway keeps it out of git history and forces rotation through the dashboard rather than ad-hoc.
|
|
101
|
+
|
|
102
|
+
## Domain allowlist
|
|
103
|
+
|
|
104
|
+
Production deployments **do not** auto-allow `localhost`. Before the widget will load on a new host:
|
|
105
|
+
|
|
106
|
+
1. **Settings → Domains** in the dashboard.
|
|
107
|
+
2. Add the exact host (`app.example.com`) or a glob (`*.staging.example.com`).
|
|
108
|
+
3. To test against the live deployment from local dev, add `localhost` explicitly.
|
|
109
|
+
|
|
110
|
+
A request from an unlisted origin gets `403 Origin not allowed` with no CORS headers — in browser devtools it shows as a CORS error, not a clean JSON error.
|
|
111
|
+
|
|
112
|
+
## Config options
|
|
113
|
+
|
|
114
|
+
| Option | Type | Default | Notes |
|
|
115
|
+
| -------------- | ---------------------------------------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
|
116
|
+
| `apiUrl` | `string` | required | `https://*.convex.site` |
|
|
117
|
+
| `apiKey` | `string` | required | `markup_…` |
|
|
118
|
+
| `position` | `'bottom-right' \| 'bottom-left' \| 'bottom-center'` | `'bottom-right'` | Initial toolbar corner — users move it with the Position picker in the overflow menu; the choice is per-mount and resets on reload |
|
|
119
|
+
| `theme` | `'light' \| 'dark' \| 'auto'` | `'auto'` | `'auto'` follows the host's `prefers-color-scheme`. A user's pick in the overflow menu persists and outranks this on later `init()` |
|
|
120
|
+
| `screenshots` | `ScreenshotsConfig` (see below) | enabled, defaults | Capture toggle + PII-scrub knobs |
|
|
121
|
+
| `dashboardUrl` | `string` | — | Adds an **Account →** link to the signed-in identity menu and retargets the "Powered by Markup" line. For self-hosters |
|
|
122
|
+
| `fab` | `'default' \| 'icon-only'` | — | **Deprecated, ignored since 1.15.0.** Accepted so old configs still compile; `init()` drops it and warns once. Remove it |
|
|
123
|
+
|
|
124
|
+
### Screenshots & PII scrub
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
init({
|
|
128
|
+
apiUrl,
|
|
129
|
+
apiKey,
|
|
130
|
+
screenshots: {
|
|
131
|
+
enabled: true, // default true — set false to disable screenshot capture
|
|
132
|
+
strictScrub: false, // mask EVERY input/select/textarea (not just sensitive)
|
|
133
|
+
redactSelector: '.private', // extra CSS selector — matched elements always masked
|
|
134
|
+
},
|
|
135
|
+
})
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Auto-masked by default (regardless of `strictScrub`):
|
|
139
|
+
|
|
140
|
+
- `input[type="password"]`
|
|
141
|
+
- `input[autocomplete~="cc-number" | "cc-csc" | "cc-exp" | "cc-name" | "cc-type"]`
|
|
142
|
+
- `input[autocomplete~="current-password" | "new-password" | "one-time-code"]`
|
|
143
|
+
|
|
144
|
+
HTML attributes the widget honours on the host page:
|
|
145
|
+
|
|
146
|
+
- `data-markup-private` / `data-markup-redact` — always masked (in addition to auto-detection).
|
|
147
|
+
- `data-markup-safe` — any element with this attribute (or an ancestor that has it) is **exempted** from auto-detection and from `redactSelector` / `strictScrub`. Use sparingly — putting it on a wrapper accidentally un-protects everything inside.
|
|
148
|
+
- `data-markup-skip` — removes the element from the screenshot entirely, rather than masking it.
|
|
149
|
+
|
|
150
|
+
Tell users with strict compliance needs to opt into `strictScrub: true` and pin sensitive areas with `data-markup-private` rather than relying on the default heuristics.
|
|
151
|
+
|
|
152
|
+
End users can also switch capture off for themselves with **Auto-capture screenshots** in the overflow menu. `screenshots.enabled: false` from the host still wins — that row renders disabled.
|
|
153
|
+
|
|
154
|
+
Capture degrades rather than failing silently: 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; an oversized capture is re-encoded down a quality-then-scale ladder to fit the server's 2 MB cap; and if it still can't produce an image the composer says _Screenshot unavailable_ and the comment posts without one.
|
|
155
|
+
|
|
156
|
+
## Mentions & inbox
|
|
157
|
+
|
|
158
|
+
Typing `@` in a composer opens a picker of project members and inserts a chip — 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.
|
|
159
|
+
|
|
160
|
+
Mentioned teammates get a notification. **Signed-in** users read those from the inbox button on the toolbar, which carries an unread badge; anonymous visitors don't get the button. A mention that doesn't highlight means nobody was notified — the target isn't a project member, has left, or is the author themselves.
|
|
161
|
+
|
|
162
|
+
Comments written by an AI agent through Markup's MCP server carry a bot badge. They're posted under a team member's name, so the badge is the only signal that a machine wrote it.
|
|
163
|
+
|
|
164
|
+
## Verify the install
|
|
165
|
+
|
|
166
|
+
Don't call it done on a clean build — the widget can compile in and still never
|
|
167
|
+
mount. Run the dev server, then:
|
|
168
|
+
|
|
169
|
+
1. Confirm the toolbar pill is in the configured bottom corner.
|
|
170
|
+
2. Click the comment button, drop a pin anywhere on the page, submit a comment.
|
|
171
|
+
3. Confirm the thread appears on the project in the Markup dashboard. It arrives
|
|
172
|
+
over WebSocket, so it should show without a refresh.
|
|
173
|
+
|
|
174
|
+
A pin that posts but never lands in the dashboard is a project mismatch — the
|
|
175
|
+
API key belongs to a different project than the one being watched.
|
|
176
|
+
|
|
177
|
+
## Diagnostics
|
|
178
|
+
|
|
179
|
+
Walk these in order when the widget is misbehaving:
|
|
180
|
+
|
|
181
|
+
| Symptom | Cause | Fix |
|
|
182
|
+
| --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
183
|
+
| 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 |
|
|
184
|
+
| 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) |
|
|
185
|
+
| 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** |
|
|
186
|
+
| 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 |
|
|
187
|
+
| `@` 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 |
|
|
188
|
+
| `401 Invalid api key` | Key revoked, wrong project, or copy-paste truncation | Mint a fresh key and replace the env var |
|
|
189
|
+
| `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 |
|
|
190
|
+
| `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, authorEmail > 320) | The bundled widget always stays inside these limits; this only fires if something between the widget and the API rewrote the payload — check for a misbehaving service worker or proxy |
|
|
191
|
+
| 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 |
|
|
192
|
+
| 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 |
|
|
193
|
+
| Widget styling looks broken inside an iframe | Shadow DOM doesn't pierce frame boundaries | Mount the widget **inside** the iframe document, not the parent |
|
|
194
|
+
| 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 |
|
|
195
|
+
| 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 silently dropped on next load — sign in again |
|
|
196
|
+
| HMR leaves duplicate toolbars in dev | Hot reload re-runs `init` without cleanup | Return the `destroy` from `useEffect` / `onMounted` so HMR can call it. (The history wrapper itself is module-scoped, so repeated init/destroy cycles no longer leak listeners.) |
|
|
197
|
+
| 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 |
|
|
198
|
+
| 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 |
|
|
199
|
+
|
|
200
|
+
## Don'ts
|
|
201
|
+
|
|
202
|
+
- **Don't import CSS** — there is none to import. Styles live inside the shadow root.
|
|
203
|
+
- **Don't wrap in a provider component.** `init` is the entire public surface.
|
|
204
|
+
- **Don't call `init` inside a route component.** Mount at the app root once.
|
|
205
|
+
- **Don't hardcode the API key.** Use env vars so rotations don't require a code change.
|
|
206
|
+
- **Don't ship CDN URLs without a version pin.** `@pixelmatters/markup@1.18.3`, not `@pixelmatters/markup`.
|
|
207
|
+
- **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` — the widget derives that origin 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.
|
|
208
|
+
|
|
209
|
+
## Versioning & releases
|
|
210
|
+
|
|
211
|
+
The widget follows semver. Pin to a major in `package.json` (`^1.5.0`) so patch + minor updates flow through `pnpm update`, but a future `2.0` cannot break the host silently. For CDN consumers, pin the exact version — there's no lockfile to catch a surprise major.
|
|
212
|
+
|
|
213
|
+
## Reference
|
|
214
|
+
|
|
215
|
+
- Install tutorial: https://pixelmatters-markup.pages.dev/docs/install
|
|
216
|
+
- Package on npm: https://www.npmjs.com/package/@pixelmatters/markup
|
|
217
|
+
- Source: https://github.com/Pixelmatters/markup
|
|
218
|
+
|
|
219
|
+
## When in doubt
|
|
220
|
+
|
|
221
|
+
The widget's `init({ apiUrl, apiKey })` is intentionally the entire API. If a question seems to need a richer surface (custom triggers, programmatic pin creation, headless mode), it's almost certainly something the widget doesn't do — say so plainly rather than inventing options.
|