@pixelmatters/markup 1.26.0 → 1.27.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 +97 -15
- package/dist/widget.d.ts +39 -0
- package/dist/widget.js +1395 -1310
- package/dist/widget.js.map +1 -1
- package/package.json +1 -1
- package/skills/install-markup-widget/SKILL.md +14 -13
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ Pin-anchored feedback for live web apps. Drop in a script tag and your stakehold
|
|
|
15
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. A project can be set to **members only** in the dashboard, in which case visitors are asked to sign in before commenting; everyone still sees the pins.
|
|
16
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
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` and `hashchange` to refresh threads on route changes. Hash routers are supported
|
|
18
|
+
- **SPA-aware.** Patches `history.pushState` / `replaceState` and follows `popstate` and `hashchange` to refresh threads on route changes. Hash routers are supported, and a query string is left out of the route unless you opt it in with `routeParams`. See [Routing](#routing).
|
|
19
19
|
- **Respects the platform.** Honours `prefers-reduced-motion` and `prefers-color-scheme`, with full keyboard navigation and focus traps in popovers.
|
|
20
20
|
- **Tiny API, tiny config.** `init({ apiUrl, apiKey })` is enough to start. No global CSS to import, no provider to wrap.
|
|
21
21
|
|
|
@@ -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.
|
|
40
|
+
import { init } from 'https://esm.sh/@pixelmatters/markup@1.27.1'
|
|
41
41
|
// or
|
|
42
|
-
// import { init } from 'https://esm.run/@pixelmatters/markup@1.
|
|
42
|
+
// import { init } from 'https://esm.run/@pixelmatters/markup@1.27.1'
|
|
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, so a future major release will break your page with no warning. Always pin (`@pixelmatters/markup@1.
|
|
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.27.1`).
|
|
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.
|
|
60
|
+
src="https://esm.sh/@pixelmatters/markup@1.27.1"
|
|
61
61
|
data-markup-widget="true"
|
|
62
62
|
data-api-url="https://your-deployment.convex.site"
|
|
63
63
|
data-api-key="markup_..."
|
|
@@ -162,6 +162,7 @@ Mounts the widget. Always tears down any existing instance before mounting, so c
|
|
|
162
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
163
|
| `screenshots` | `ScreenshotsConfig` | capture enabled | Capture and PII-scrub options. See [Screenshots & privacy](#screenshots--privacy) |
|
|
164
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
|
+
| `routeParams` | `string[]` | `[]` | Query params that name a _view_ rather than filter one, and so belong in the route a thread is filed under. See [Routing](#routing) |
|
|
165
166
|
|
|
166
167
|
<details>
|
|
167
168
|
<summary><code>fab</code> (deprecated, ignored since 1.15.0)</summary>
|
|
@@ -207,15 +208,26 @@ Unmounts the widget and removes the host element. Safe to call when nothing is m
|
|
|
207
208
|
|
|
208
209
|
The widget mounts a single compact pill in the corner set by `position`:
|
|
209
210
|
|
|
210
|
-
| Control | What it does
|
|
211
|
-
| ------------------- |
|
|
212
|
-
| **Comment** | Arms placement. The next click on the page drops a pin, and it flips to a cancel icon while armed.
|
|
213
|
-
| **Inbox** | Mention notifications for this project, with an unread badge. Signed-in users only; anonymous visitors don't get the button.
|
|
214
|
-
| **Pins** (eye) | Hides or shows every pin without hiding the toolbar.
|
|
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
|
-
| **Overflow** (`☰`) | **Appearance** (Light / Dark / Auto), **Position** (left / center / right), an **Auto-capture screenshots** toggle, a **Show resolved threads** toggle, **Privacy & data**, **Keyboard shortcuts**, **Hide for this session**, and the widget version. |
|
|
211
|
+
| Control | What it does |
|
|
212
|
+
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
213
|
+
| **Comment** | Arms placement. The next click on the page drops a pin, and it flips to a cancel icon while armed. |
|
|
214
|
+
| **Inbox** | Mention notifications for this project, with an unread badge. Signed-in users only; anonymous visitors don't get the button. |
|
|
215
|
+
| **Pins** (eye) | Hides or shows every pin without hiding the toolbar. |
|
|
216
|
+
| **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**. |
|
|
217
|
+
| **Overflow** (`☰`) | **Appearance** (Light / Dark / Auto), **Position** (left / center / right), an **Auto-capture screenshots** toggle, an **Only this URL's pins** toggle, a **Show resolved threads** toggle, **Privacy & data**, **Keyboard shortcuts**, **Hide for this session**, and the widget version. |
|
|
217
218
|
|
|
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, the auto-capture toggle and the resolved-threads toggle are per-mount. They reset on reload, and `position` seeds the toolbar again.
|
|
219
|
+
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, the auto-capture toggle, the URL-scope toggle and the resolved-threads toggle are per-mount. They reset on reload, and `position` seeds the toolbar again.
|
|
220
|
+
|
|
221
|
+
**Only this URL's pins** is off by default. A route is `host + pathname`, so
|
|
222
|
+
views your app distinguishes only by a query param share one set of pins and
|
|
223
|
+
draw each other's. Turned on, the widget renders only the pins left on the URL
|
|
224
|
+
you are on, by query string, and the overflow button carries
|
|
225
|
+
a badge counting what is being held back — the pins are hidden from the page,
|
|
226
|
+
never dropped, and an open thread stays drawn whichever URL it belongs to.
|
|
227
|
+
|
|
228
|
+
Leave it off if your params are filters rather than views (`?page=2&sort=name`):
|
|
229
|
+
it would split one page's feedback across every combination a reader happens to
|
|
230
|
+
have on. A pin's tooltip names the URL it was left on either way.
|
|
219
231
|
|
|
220
232
|
**Show resolved threads** is off by default. Turned on, the widget also renders the route's most recently resolved threads (up to 100) as muted check-mark pins. Opening one shows the thread read-only: no replies, edits, reactions or resolve button. Reopening still happens from the dashboard.
|
|
221
233
|
|
|
@@ -279,6 +291,76 @@ Capture degrades instead of failing outright:
|
|
|
279
291
|
- **Captures are sized for storage, not for zooming.** The raster is capped at 1.5x device pixel ratio, so a 2x or 3x display doesn't bank detail nobody looks at in a lightbox. The image is then encoded down a ladder — quality drops first (0.85 → 0.6), then the raster shrinks (full → ¾ → ½) — until it lands under roughly 400 KB. A page that can't get there at any rung keeps the sharpest version that still fits the server's 2 MB hard cap, because a smaller sharp screenshot beats a full-size illegible one but not by any margin.
|
|
280
292
|
- **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.
|
|
281
293
|
|
|
294
|
+
## Routing
|
|
295
|
+
|
|
296
|
+
A thread is filed under a **route**, and pins are drawn for the route you are
|
|
297
|
+
standing on. A route is `host + pathname`, plus a hash-router path when the
|
|
298
|
+
fragment is one:
|
|
299
|
+
|
|
300
|
+
| URL | Route |
|
|
301
|
+
| ------------------------------------- | ------------------------- |
|
|
302
|
+
| `https://app.acme.com/orders` | `app.acme.com/orders` |
|
|
303
|
+
| `https://app.acme.com/orders?page=2` | `app.acme.com/orders` |
|
|
304
|
+
| `https://app.acme.com/#/orders` | `app.acme.com/#/orders` |
|
|
305
|
+
| `https://app.acme.com/#/orders?tab=2` | `app.acme.com/#/orders` |
|
|
306
|
+
| `https://staging.acme.com/orders` | `staging.acme.com/orders` |
|
|
307
|
+
|
|
308
|
+
The host is part of it, so staging and production never share pins. The query
|
|
309
|
+
string is not, in either position — `?page=2` is a filter over a page, not a
|
|
310
|
+
different page, and keying on it would split one conversation across every
|
|
311
|
+
combination a reader happens to have on.
|
|
312
|
+
|
|
313
|
+
### When a query param _is_ the page
|
|
314
|
+
|
|
315
|
+
Some apps route a view by param: a multi-step form at one path with
|
|
316
|
+
`?step=shipping`, `?step=payment` and so on. Those are different pages that
|
|
317
|
+
happen to share a URL path, and by default they share one set of pins — every
|
|
318
|
+
step's feedback is drawn on whichever step you are reading, at positions that
|
|
319
|
+
mean nothing there.
|
|
320
|
+
|
|
321
|
+
List the params that name a view:
|
|
322
|
+
|
|
323
|
+
```js
|
|
324
|
+
init({
|
|
325
|
+
apiUrl: '…',
|
|
326
|
+
apiKey: '…',
|
|
327
|
+
routeParams: ['step'],
|
|
328
|
+
})
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
`/checkout?step=shipping` and `/checkout?step=payment` now have their own
|
|
332
|
+
routes and their own pins. Params you don't list are still ignored, so
|
|
333
|
+
`?step=payment&page=2` and `?step=payment&page=3` remain one page.
|
|
334
|
+
|
|
335
|
+
Order doesn't matter — the key sorts them, so a router free to reorder its
|
|
336
|
+
query can't split a view in two. A listed param the URL omits contributes
|
|
337
|
+
nothing, so if a view is reachable both bare and with the param, those are two
|
|
338
|
+
routes; make your router always write it.
|
|
339
|
+
|
|
340
|
+
> **`routeParams` reads the real query string only.** On a hash router the
|
|
341
|
+
> query lives inside the fragment (`#/orders?tab=2`), where `location.search`
|
|
342
|
+
> is empty — so listing `tab` there does nothing. A hash-routed app that needs
|
|
343
|
+
> per-param views has to put the distinguishing part in the router _path_
|
|
344
|
+
> (`#/orders/tab/2`), which the key already carries. Tell us if that's you.
|
|
345
|
+
|
|
346
|
+
> **Changing `routeParams` re-keys new threads.** Pins already stored under the
|
|
347
|
+
> old route stop appearing until an operator runs
|
|
348
|
+
> `threads/migration:rekeySearchRoutes` with the same list. Threads store their
|
|
349
|
+
> full URL, so nothing is lost and the backfill can be re-run whenever the list
|
|
350
|
+
> changes — widening, narrowing and dropping the option are all recoverable —
|
|
351
|
+
> but there is a window each time. Coordinate the config change with the
|
|
352
|
+
> backfill.
|
|
353
|
+
|
|
354
|
+
Keep the listed params short. The route key is capped at 256 characters at the
|
|
355
|
+
API edge, so a param carrying a serialized filter or a token can push a long
|
|
356
|
+
path over it — and the failure is one-sided: pins still render, but posting a
|
|
357
|
+
comment on those views is rejected.
|
|
358
|
+
|
|
359
|
+
Leaving `routeParams` empty is the right answer for most apps. Where views
|
|
360
|
+
share a route, each pin's tooltip names the URL it was left on, and the
|
|
361
|
+
overflow menu's **Only this URL's pins** narrows the page to one view without
|
|
362
|
+
changing how anything is stored.
|
|
363
|
+
|
|
282
364
|
## How it works
|
|
283
365
|
|
|
284
366
|
- The widget mounts `<div id="markup-widget">` on `document.body` and attaches an open shadow root.
|
|
@@ -375,7 +457,7 @@ For a `<script>` tag drop-in (no bundler), use the inline ESM form and **pin the
|
|
|
375
457
|
|
|
376
458
|
```html
|
|
377
459
|
<script type="module">
|
|
378
|
-
import { init } from 'https://esm.sh/@pixelmatters/markup@1.
|
|
460
|
+
import { init } from 'https://esm.sh/@pixelmatters/markup@1.27.1'
|
|
379
461
|
|
|
380
462
|
init({
|
|
381
463
|
apiUrl: '...',
|
|
@@ -391,7 +473,7 @@ If inline JS is disallowed (some CMS / page-builder editors), use the auto-init
|
|
|
391
473
|
```html
|
|
392
474
|
<script
|
|
393
475
|
type="module"
|
|
394
|
-
src="https://esm.sh/@pixelmatters/markup@1.
|
|
476
|
+
src="https://esm.sh/@pixelmatters/markup@1.27.1"
|
|
395
477
|
data-markup-widget="true"
|
|
396
478
|
data-api-url="..."
|
|
397
479
|
data-api-key="..."
|
package/dist/widget.d.ts
CHANGED
|
@@ -76,6 +76,45 @@ export interface WidgetConfig {
|
|
|
76
76
|
* @default 'https://markup.pixelmatters.dev'
|
|
77
77
|
*/
|
|
78
78
|
dashboardUrl?: string;
|
|
79
|
+
/**
|
|
80
|
+
* Query params that name a *view* rather than filter one, and so belong in
|
|
81
|
+
* the route a thread is filed under.
|
|
82
|
+
*
|
|
83
|
+
* A route is `host + pathname` (plus a hash-router path). Views your app
|
|
84
|
+
* distinguishes only by a param — `/checkout?step=shipping` and
|
|
85
|
+
* `/checkout?step=payment` — therefore share one route and draw each
|
|
86
|
+
* other's pins. Listing `'step'` gives each its own.
|
|
87
|
+
*
|
|
88
|
+
* Empty by default, and left that way for most apps: a param that filters
|
|
89
|
+
* a list (`?page=2&sort=name`) is not a different page, and keying on it
|
|
90
|
+
* splits one conversation across every combination a reader happens to
|
|
91
|
+
* have on. List only the params that change *what the page is*.
|
|
92
|
+
*
|
|
93
|
+
* Order doesn't matter — the key sorts them, so a router free to reorder
|
|
94
|
+
* its query can't split a view in two. A listed param the URL omits
|
|
95
|
+
* contributes nothing, so if a view is reachable both bare and with the
|
|
96
|
+
* param, those are two routes.
|
|
97
|
+
*
|
|
98
|
+
* Reads `location.search` only. A hash router's query lives inside the
|
|
99
|
+
* fragment (`#/orders?tab=2`), where there is no query string, so this
|
|
100
|
+
* option cannot reach it — such an app needs the distinguishing part in
|
|
101
|
+
* the router path, which the route key already carries.
|
|
102
|
+
*
|
|
103
|
+
* **Changing this re-keys new threads.** Pins already stored under the old
|
|
104
|
+
* route stop appearing until an operator runs the matching backfill
|
|
105
|
+
* (`threads/migration:rekeySearchRoutes`) with the same list. The backfill
|
|
106
|
+
* can be re-run whenever the list changes, so widening, narrowing and
|
|
107
|
+
* dropping the option are all recoverable — but there is a window each
|
|
108
|
+
* time, so coordinate the two.
|
|
109
|
+
*
|
|
110
|
+
* Keep the listed params short. The route key is capped at 256 characters
|
|
111
|
+
* at the API edge, and a param carrying a serialized filter or a token can
|
|
112
|
+
* push a long path past it; comments on those views are then rejected
|
|
113
|
+
* while the pins still render.
|
|
114
|
+
*
|
|
115
|
+
* @default []
|
|
116
|
+
*/
|
|
117
|
+
routeParams?: string[];
|
|
79
118
|
/**
|
|
80
119
|
* Product telemetry — counts of widget interactions, sent to Markup.
|
|
81
120
|
*
|