@pixelmatters/markup 1.32.4 → 1.33.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 CHANGED
@@ -14,7 +14,7 @@ Pin-anchored feedback for live web apps. Drop in a script tag and your stakehold
14
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.
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
- - **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.
17
+ - **Style-isolated.** Runs inside an open shadow root with `:host { all: initial !important }`, so host CSS can't bleed in or restyle the widget, and widget CSS can't bleed out.
18
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.
@@ -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.32.4'
40
+ import { init } from 'https://esm.sh/@pixelmatters/markup@1.33.0'
41
41
  // or
42
- // import { init } from 'https://esm.run/@pixelmatters/markup@1.32.4'
42
+ // import { init } from 'https://esm.run/@pixelmatters/markup@1.33.0'
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.32.4`).
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.33.0`).
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.32.4"
60
+ src="https://esm.sh/@pixelmatters/markup@1.33.0"
61
61
  data-markup-widget="true"
62
62
  data-api-url="https://your-deployment.convex.site"
63
63
  data-api-key="markup_..."
@@ -214,6 +214,26 @@ What it is not, concretely:
214
214
 
215
215
  Unmounts the widget and removes the host element. Safe to call when nothing is mounted.
216
216
 
217
+ ### Hiding the widget or stacking it lower
218
+
219
+ Ordinary CSS can't reach the widget's host element (see [How it works](#how-it-works)), so `#markup-widget { display: none }` or a `z-index` of yours has no effect. Set one of these custom properties instead, on `#markup-widget` or on any ancestor such as `:root`. Both inherit, so they work inside a media query or under a class you toggle:
220
+
221
+ ```css
222
+ /* Hide the widget on small screens. */
223
+ @media (max-width: 640px) {
224
+ :root {
225
+ --markup-display: none;
226
+ }
227
+ }
228
+
229
+ /* Keep the widget beneath your cookie banner. */
230
+ #markup-widget {
231
+ --markup-z-index: 999;
232
+ }
233
+ ```
234
+
235
+ `--markup-display` takes `none` to hide the widget, and defaults to `block`. `--markup-z-index` takes an integer, and defaults to `2147483647`. The `hidden` attribute on `#markup-widget` also hides it. The widget never prints.
236
+
217
237
  ## The toolbar
218
238
 
219
239
  The widget mounts a single compact pill in the corner set by `position`:
@@ -269,7 +289,11 @@ close.
269
289
 
270
290
  ## Screenshots & privacy
271
291
 
272
- By default, the widget captures the visible viewport as a WebP image before you submit a thread (JPEG on browsers that can't encode WebP). Sensitive fields are blacked out **before** the image is produced. Each masked element becomes one solid black block covering its box, with nothing inside it showing: no text, images, icons or child elements, even where they overflow the box. Masking a `<canvas>`, `<video>` or `<iframe>` also blacks out any image beside it in the same parent. The mask is applied to the copy of the page the capture renders, so the page itself never shows it. No image content leaves the browser until the user explicitly attaches the screenshot and posts.
292
+ By default, the widget captures the visible viewport as a WebP image before you submit a thread (JPEG on browsers that can't encode WebP). Sensitive fields are blacked out **before** the image is produced. The mask is drawn into the snapshot, never onto your page, so nothing on the page changes, and masking holds even under a `style-src` without `'unsafe-inline'`. Each masked element comes out as one solid black block covering its box, whatever your stylesheets say about it: its text, placeholder, images, canvases, SVG, icons, backgrounds and child elements are all left out, and its images and backgrounds aren't fetched or read. Sprite files and webfonts still are, since the rest of the page shares them and they hold nothing of the field. No image content leaves the browser until the user explicitly attaches the screenshot and posts.
293
+
294
+ The capture works in slices of a few milliseconds and leaves out what is outside the viewport. Your page's own timers, messages and frames run between slices, so a large page stays responsive while it runs. One step can't be split: the browser laying out the finished snapshot, which takes longer the more is on screen. Because the page keeps running, each part of it is captured as it is when the capture reaches it, so content your page moves or changes mid-capture appears where your page put it. A list or table your page renders again while the capture is partway through it is read again, so it comes out whole rather than half old and half new.
295
+
296
+ Two things don't follow your scroll position: an element that scrolls on its own is captured from its top (this includes an app shell that scrolls a root `<div>` rather than the window), and a `position: sticky` element is captured where it sits before it sticks.
273
297
 
274
298
  **Auto-scrubbed (zero config):**
275
299
 
@@ -295,7 +319,7 @@ By default, the widget captures the visible viewport as a WebP image before you
295
319
  | `screenshots.strictScrub` | `boolean` | `false` | Also masks all `input`, `select`, and `textarea` elements. |
296
320
  | `screenshots.redactSelector` | `string` | none | Custom CSS selector; matched elements are masked unless inside a `data-markup-safe` element. |
297
321
 
298
- 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.
322
+ The rules apply inside open shadow roots and same-origin iframes too. When a screenshot is attached in the composer, a chip shows how many masked fields the screenshot shows, those included; a matching field that is hidden, skipped or off screen isn't in it and isn't counted. Clicking it expands the list of CSS selectors that were masked.
299
323
 
300
324
  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.
301
325
 
@@ -406,8 +430,8 @@ to start from `anchorSource` and confirm it, not to trust it blindly.
406
430
 
407
431
  ## How it works
408
432
 
409
- - The widget mounts `<div id="markup-widget">` on `document.body` and attaches an open shadow root. It does so once the page is idle, or 2 s after `init()` on a page that never is, so `init()` returns before the element exists.
410
- - All UI lives in that shadow root, with `:host { all: initial }` blocking style inheritance.
433
+ - The widget mounts `<div id="markup-widget">` on `document.body` and attaches an open shadow root. It does so once the page is idle, or 2 s after `init()` on a page that never is, so `init()` returns before the element exists. The requests it needs before it can show pins, for the live-update credential and, on a first visit, an anonymous identity, go out from `init()` itself, so they overlap that wait.
434
+ - All UI lives in that shadow root, with `:host { all: initial !important }` blocking style inheritance. The `!important` also means no rule on your page can restyle the host element, whether it matches by accident (`div`, `body > div`, `[data-theme]`) or aims at `#markup-widget`: it can't paint or move the widget. To hide it or stack it lower, use the custom properties in [Hiding the widget or stacking it lower](#hiding-the-widget-or-stacking-it-lower); to take it off the page, call `destroy()`. It never prints.
411
435
  - 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.
412
436
  - A modal on your page, such as a Radix or Base UI dialog, doesn't lock the widget out. Focus, wheel and touch events inside the widget are not passed on to your page, so a focus trap or scroll lock never sees them. A field of yours still gets its `focusout` when focus moves into the widget, without a `relatedTarget`. The widget also removes the `aria-hidden` or `inert` a modal puts on it, so screen readers can reach it. A Base UI popup that allows presses outside it still closes on a click in the widget, and a native `<dialog>` opened with `showModal()` makes the widget inert.
413
437
  - Pins are anchored as `(x, y)` fractions of the document plus a CSS path from the nearest landmark (a stable `id`, `data-testid`, `main`, `form`, `table`, …) and the element's text. The path wins when it still resolves, relaxing from the top if a wrapper changed, and a match whose text differs is rejected; the fraction is the fallback so pins survive layout changes.
@@ -501,7 +525,7 @@ For a `<script>` tag drop-in (no bundler), use the inline ESM form and **pin the
501
525
 
502
526
  ```html
503
527
  <script type="module">
504
- import { init } from 'https://esm.sh/@pixelmatters/markup@1.32.4'
528
+ import { init } from 'https://esm.sh/@pixelmatters/markup@1.33.0'
505
529
 
506
530
  init({
507
531
  apiUrl: '...',
@@ -517,7 +541,7 @@ If inline JS is disallowed (some CMS / page-builder editors), use the auto-init
517
541
  ```html
518
542
  <script
519
543
  type="module"
520
- src="https://esm.sh/@pixelmatters/markup@1.32.4"
544
+ src="https://esm.sh/@pixelmatters/markup@1.33.0"
521
545
  data-markup-widget="true"
522
546
  data-api-url="..."
523
547
  data-api-key="..."
@@ -545,16 +569,16 @@ Constraints:
545
569
 
546
570
  If the host page ships a CSP, the widget needs:
547
571
 
548
- | Directive | Value | Why |
549
- | ------------- | -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
550
- | `connect-src` | `https://<deployment>.convex.site` | threads, comments, identity, screenshot upload, error reports |
551
- | `connect-src` | `wss://<deployment>.convex.cloud` | live thread updates |
552
- | `connect-src` | your own origin, if you serve icons from an SVG sprite | fetched so `<use href="/sprite.svg#icon">` icons aren't blank in screenshots |
553
- | `img-src` | wherever your team's profile pictures are hosted, plus `blob:` and `data:` | avatars, the screenshot preview thumbnail, and the capture pipeline |
554
- | `script-src` | `https://esm.sh` | CDN path only; a bundled install needs nothing here |
555
- | `style-src` | `'unsafe-inline'` | the widget appends its stylesheet as a `<style>` element inside its own shadow root |
572
+ | Directive | Value | Why |
573
+ | ------------- | ------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- |
574
+ | `connect-src` | `https://<deployment>.convex.site` | threads, comments, identity, screenshot upload, error reports |
575
+ | `connect-src` | `wss://<deployment>.convex.cloud` | live thread updates |
576
+ | `connect-src` | your own origin, and any CDN serving your webfonts, CSS backgrounds, cross-origin images or SVG sprite | fetched so screenshots include them, such as `<use href="/sprite.svg#icon">` icons |
577
+ | `img-src` | wherever your team's profile pictures are hosted, plus `blob:` and `data:` | avatars, the screenshot preview thumbnail, and the capture pipeline |
578
+ | `script-src` | `https://esm.sh` | CDN path only; a bundled install needs nothing here |
579
+ | `style-src` | `'unsafe-inline'` | the widget appends its stylesheet as a `<style>` element inside its own shadow root |
556
580
 
557
- Only `img-src` and the sprite entry degrade gracefully: a blocked avatar falls back to initials, and a blocked image or sprite in a capture comes through blank. A missing `https://…convex.site` entry stops the widget working at all. A missing `wss://…convex.cloud` entry leaves the toolbar up and posting working, but threads never load; the widget reports that once rather than retrying. A missing `style-src` entry leaves it unstyled.
581
+ Only `img-src` and the asset entry degrade gracefully: a blocked avatar falls back to initials, and a blocked image or sprite in a capture comes through blank. Without `data:` in `img-src` the capture itself can't load, and the composer reads _Screenshot unavailable_. A missing `https://…convex.site` entry stops the widget working at all. A missing `wss://…convex.cloud` entry leaves the toolbar up and posting working, but threads never load; the widget reports that once rather than retrying. A missing `style-src` entry leaves it unstyled.
558
582
 
559
583
  ## Browser support
560
584