@pixelmatters/markup 1.18.1 → 1.18.2

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
@@ -37,9 +37,9 @@ CDN drop-in (no build step) — paste this just before `</body>`:
37
37
  ```html
38
38
  <script type="module">
39
39
  // Pin the exact version — esm.sh resolves it from npm
40
- import { init } from 'https://esm.sh/@pixelmatters/markup@1.18.1'
40
+ import { init } from 'https://esm.sh/@pixelmatters/markup@1.18.2'
41
41
  // or
42
- // import { init } from 'https://esm.run/@pixelmatters/markup@1.18.1'
42
+ // import { init } from 'https://esm.run/@pixelmatters/markup@1.18.2'
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.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.18.2`).
54
54
 
55
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.1"
60
+ src="https://esm.sh/@pixelmatters/markup@1.18.2"
61
61
  data-markup-widget="true"
62
62
  data-api-url="https://your-deployment.convex.site"
63
63
  data-api-key="markup_..."
@@ -307,12 +307,24 @@ Signed-in authors render their profile picture — on their comments and on the
307
307
 
308
308
  ## AI prompt
309
309
 
310
- If your IDE / agent supports the [`skills`](https://www.npmjs.com/package/skills) CLI (Claude Code, Cursor, etc.), install the bundled skill instead of pasting a prompt it gives the agent persistent, up-to-date install + troubleshooting guidance:
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.
311
+
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`, …):
311
313
 
312
314
  ```bash
313
- npx skills add Pixelmatters/markup --skill pixelmatters-markup
315
+ npx @tanstack/intent@latest install
314
316
  ```
315
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.
319
+
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
+
322
+ ```bash
323
+ npx skills add Pixelmatters/markup/packages/widget/skills/install-markup-widget
324
+ ```
325
+
326
+ The path matters — it's where the skill lives in the repo, and without it the CLI picks up this repo's internal contributor skills instead.
327
+
316
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.
317
329
 
318
330
  ````markdown
@@ -358,7 +370,7 @@ For a `<script>` tag drop-in (no bundler), use the inline ESM form and **pin the
358
370
 
359
371
  ```html
360
372
  <script type="module">
361
- import { init } from 'https://esm.sh/@pixelmatters/markup@1.18.1'
373
+ import { init } from 'https://esm.sh/@pixelmatters/markup@1.18.2'
362
374
 
363
375
  init({
364
376
  apiUrl: '...',
@@ -374,7 +386,7 @@ If inline JS is disallowed (some CMS / page-builder editors), use the auto-init
374
386
  ```html
375
387
  <script
376
388
  type="module"
377
- src="https://esm.sh/@pixelmatters/markup@1.18.1"
389
+ src="https://esm.sh/@pixelmatters/markup@1.18.2"
378
390
  data-markup-widget="true"
379
391
  data-api-url="..."
380
392
  data-api-key="..."
package/dist/widget.js CHANGED
@@ -7495,7 +7495,7 @@ function Dc({ apiUrl: e, apiKey: t, position: n = "bottom-right", screenshots: r
7495
7495
  onOpenPrivacy: () => C("privacy"),
7496
7496
  onOpenShortcuts: () => C("shortcuts"),
7497
7497
  onHideForSession: () => Ce({ type: "hide-with-toast" }),
7498
- version: "1.18.1",
7498
+ version: "1.18.2",
7499
7499
  dashboardUrl: i
7500
7500
  })
7501
7501
  }) : null,
@@ -7592,11 +7592,11 @@ function jc(e, t) {
7592
7592
  a.className = "markup-shadow-root", r.appendChild(a), je({
7593
7593
  apiUrl: t.apiUrl,
7594
7594
  apiKey: t.apiKey,
7595
- widgetVersion: "1.18.1"
7595
+ widgetVersion: "1.18.2"
7596
7596
  }), be(t.analytics === !1 ? null : {
7597
7597
  apiUrl: t.apiUrl,
7598
7598
  apiKey: t.apiKey,
7599
- widgetVersion: "1.18.1"
7599
+ widgetVersion: "1.18.2"
7600
7600
  }), F("widget_loaded");
7601
7601
  let { theme: o, analytics: s, ...c } = t;
7602
7602
  return de(/* @__PURE__ */ Q(Ce, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixelmatters/markup",
3
- "version": "1.18.1",
3
+ "version": "1.18.2",
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.2'
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.2'
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.2"
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.2`, 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.