@pixelmatters/markup 1.31.6 → 1.32.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 +27 -23
- package/dist/react.js +1 -1
- package/dist/react.js.map +1 -1
- package/dist/vue.js +1 -1
- package/dist/vue.js.map +1 -1
- package/dist/widget.d.ts +2 -1
- package/dist/widget.js +10 -8
- package/dist/widget.js.map +1 -1
- package/package.json +2 -2
- package/skills/install-markup-widget/SKILL.md +10 -9
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.
|
|
40
|
+
import { init } from 'https://esm.sh/@pixelmatters/markup@1.32.0'
|
|
41
41
|
// or
|
|
42
|
-
// import { init } from 'https://esm.run/@pixelmatters/markup@1.
|
|
42
|
+
// import { init } from 'https://esm.run/@pixelmatters/markup@1.32.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.
|
|
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.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.
|
|
60
|
+
src="https://esm.sh/@pixelmatters/markup@1.32.0"
|
|
61
61
|
data-markup-widget="true"
|
|
62
62
|
data-api-url="https://your-deployment.convex.site"
|
|
63
63
|
data-api-key="markup_..."
|
|
@@ -190,9 +190,9 @@ The floating action button was replaced by the toolbar pill in 1.15.0, and a pil
|
|
|
190
190
|
|
|
191
191
|
### Product telemetry
|
|
192
192
|
|
|
193
|
-
The widget reports counts of its own interactions, such as a
|
|
194
|
-
|
|
195
|
-
which parts of it earn their place. Turn it off with `analytics: false`, or
|
|
193
|
+
The widget reports counts of its own interactions, such as a pin placed,
|
|
194
|
+
a comment submitted or abandoned, or a toolbar setting changed, so we can
|
|
195
|
+
tell which parts of it earn their place. Turn it off with `analytics: false`, or
|
|
196
196
|
`data-analytics="false"` on the script tag.
|
|
197
197
|
|
|
198
198
|
What it is not, concretely:
|
|
@@ -201,7 +201,8 @@ What it is not, concretely:
|
|
|
201
201
|
`convex.site`, the same origin the widget already talks to. Nothing new
|
|
202
202
|
for your CSP, and no vendor SDK on your page.
|
|
203
203
|
- **No identifier for your users.** Events are keyed on the Markup
|
|
204
|
-
project, not the person.
|
|
204
|
+
project, not the person. They say whether the visitor was anonymous or
|
|
205
|
+
signed in, never who. The session id is generated per page load, held
|
|
205
206
|
in memory, and gone when the tab closes.
|
|
206
207
|
- **No cookie, no `localStorage`.** Telemetry adds nothing to your page's
|
|
207
208
|
storage.
|
|
@@ -268,7 +269,7 @@ close.
|
|
|
268
269
|
|
|
269
270
|
## Screenshots & privacy
|
|
270
271
|
|
|
271
|
-
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.
|
|
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.
|
|
272
273
|
|
|
273
274
|
**Auto-scrubbed (zero config):**
|
|
274
275
|
|
|
@@ -277,20 +278,22 @@ By default, the widget captures the visible viewport as a WebP image before you
|
|
|
277
278
|
|
|
278
279
|
**Attribute API.** Add any of these to an element to control capture:
|
|
279
280
|
|
|
280
|
-
| Attribute | Behaviour
|
|
281
|
-
| --------------------- |
|
|
282
|
-
| `data-markup-private` | Always mask
|
|
283
|
-
| `data-markup-redact` | Alias for `data-markup-private`.
|
|
284
|
-
| `data-markup-safe` | Exempts descendants
|
|
285
|
-
| `data-markup-skip` | Removes the element from the screenshot entirely.
|
|
281
|
+
| Attribute | Behaviour |
|
|
282
|
+
| --------------------- | -------------------------------------------------------------------------------------------------------------------- |
|
|
283
|
+
| `data-markup-private` | Always mask: the element and everything inside it become a solid block. |
|
|
284
|
+
| `data-markup-redact` | Alias for `data-markup-private`. |
|
|
285
|
+
| `data-markup-safe` | Exempts descendants (not the element itself) from auto-detection, `redactSelector` and `strictScrub` (escape hatch). |
|
|
286
|
+
| `data-markup-skip` | Removes the element from the screenshot entirely. |
|
|
287
|
+
|
|
288
|
+
**Web components.** Masking reaches into open shadow roots, nested ones included, because the capture shows their content. A `data-markup-safe` on a component's host, or on any element around it, exempts the fields in its shadow root, as it does any other descendant. `redactSelector` is matched inside each shadow root on its own, so a selector can't span a shadow boundary: `.card-field` reaches into a component, `checkout-form .card-field` doesn't. A closed shadow root's own content never appears in the screenshot; light-DOM children slotted into it still do, and are masked like any other element.
|
|
286
289
|
|
|
287
290
|
**`init()` `screenshots` config:**
|
|
288
291
|
|
|
289
|
-
| Option | Type | Default | Description
|
|
290
|
-
| ---------------------------- | --------- | ------- |
|
|
291
|
-
| `screenshots.enabled` | `boolean` | `true` | Set to `false` to disable capture entirely.
|
|
292
|
-
| `screenshots.strictScrub` | `boolean` | `false` | Also masks all `input`, `select`, and `textarea` elements.
|
|
293
|
-
| `screenshots.redactSelector` | `string` | none | Custom CSS selector; matched elements are
|
|
292
|
+
| Option | Type | Default | Description |
|
|
293
|
+
| ---------------------------- | --------- | ------- | -------------------------------------------------------------------------------------------- |
|
|
294
|
+
| `screenshots.enabled` | `boolean` | `true` | Set to `false` to disable capture entirely. |
|
|
295
|
+
| `screenshots.strictScrub` | `boolean` | `false` | Also masks all `input`, `select`, and `textarea` elements. |
|
|
296
|
+
| `screenshots.redactSelector` | `string` | none | Custom CSS selector; matched elements are masked unless inside a `data-markup-safe` element. |
|
|
294
297
|
|
|
295
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.
|
|
296
299
|
|
|
@@ -303,8 +306,9 @@ The overflow menu's **Privacy & data** panel restates all of this for the person
|
|
|
303
306
|
Capture degrades instead of failing outright:
|
|
304
307
|
|
|
305
308
|
- **An image the browser won't hand over** comes through blank, and the rest of the page still captures. A third-party avatar served without CORS headers is the usual culprit. It used to abort the whole screenshot.
|
|
306
|
-
- **Icons from an SVG sprite** are fetched and inlined before the capture. The capture renders your page as an SVG document, which is not allowed to load anything external, so a `<use href="/sprite.svg#icon">` would otherwise draw nothing — on a design system that ships its icons that way, every icon in the screenshot went missing. A sprite the widget can't read (cross-origin without CORS headers, or outside your `connect-src`) leaves those icons blank and the rest captures as before.
|
|
309
|
+
- **Icons from an SVG sprite** are fetched and inlined before the capture. The capture renders your page as an SVG document, which is not allowed to load anything external, so a `<use href="/sprite.svg#icon">` would otherwise draw nothing — on a design system that ships its icons that way, every icon in the screenshot went missing. A sprite the widget can't read (cross-origin without CORS headers, or outside your `connect-src`) leaves those icons blank and the rest captures as before. One that hasn't answered within 3 seconds leaves them blank in that screenshot only; the next one asks again.
|
|
307
310
|
- **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. A 640px-wide thumbnail of the same redacted image is uploaded alongside it for the inline preview in a thread, so the full screenshot downloads only when someone opens it.
|
|
311
|
+
- **A font or image that never answers**, behind a blackholing proxy or a stalled CDN, is given up on after 3 seconds and treated like one the browser refuses: an image comes through blank, a font falls back to the next one in its stack. The next screenshot asks for it again, and waits for it again. An image the browser refuses outright is not asked for again until the page reloads. A capture still unfinished after 30 seconds is abandoned as unavailable, rather than leaving the composer on _Capturing…_.
|
|
308
312
|
- **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.
|
|
309
313
|
|
|
310
314
|
## Routing
|
|
@@ -497,7 +501,7 @@ For a `<script>` tag drop-in (no bundler), use the inline ESM form and **pin the
|
|
|
497
501
|
|
|
498
502
|
```html
|
|
499
503
|
<script type="module">
|
|
500
|
-
import { init } from 'https://esm.sh/@pixelmatters/markup@1.
|
|
504
|
+
import { init } from 'https://esm.sh/@pixelmatters/markup@1.32.0'
|
|
501
505
|
|
|
502
506
|
init({
|
|
503
507
|
apiUrl: '...',
|
|
@@ -513,7 +517,7 @@ If inline JS is disallowed (some CMS / page-builder editors), use the auto-init
|
|
|
513
517
|
```html
|
|
514
518
|
<script
|
|
515
519
|
type="module"
|
|
516
|
-
src="https://esm.sh/@pixelmatters/markup@1.
|
|
520
|
+
src="https://esm.sh/@pixelmatters/markup@1.32.0"
|
|
517
521
|
data-markup-widget="true"
|
|
518
522
|
data-api-url="..."
|
|
519
523
|
data-api-key="..."
|
package/dist/react.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use client";import{init as e,t}from"./widget.js";import{t as
|
|
1
|
+
"use client";import{init as e,n as t,t as n}from"./widget.js";import{t as r}from"./shared-mount-B9ij4vuL.js";import{useEffect as i}from"react";function a(a){let{enabled:o=!0,...s}=a,c=o?n(s):null;i(()=>{if(c!==null)return t(`react`),r(c,e)},[c])}export{a as useMarkup};
|
|
2
2
|
//# sourceMappingURL=react.js.map
|
package/dist/react.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react.js","names":[],"sources":["../src/react.ts"],"sourcesContent":["'use client'\n\nimport { useEffect } from 'react'\n\nimport { configKey } from './runtime/config-key'\nimport { acquireMount } from './runtime/shared-mount'\nimport { init, type WidgetConfig } from './widget'\n\nexport type { WidgetConfig } from './widget'\n\nexport type UseMarkupOptions = WidgetConfig & {\n /**\n * `false` unmounts the widget, or never mounts it. Drive it from your build\n * environment to keep the widget off production.\n *\n * @default true\n */\n enabled?: boolean\n}\n\n/**\n * Mounts the widget for as long as the calling component is mounted. Call it\n * once near your app root.\n *\n * An inline options object is fine: the widget remounts only when a value\n * changes, not when the object is recreated. Two components using the same\n * options share one widget, and it stays until both unmount. Server rendering\n * mounts nothing.\n */\nexport function useMarkup(options: UseMarkupOptions): void {\n const { enabled = true, ...config } = options\n const key = enabled ? configKey(config) : null\n\n useEffect(() => {\n if (key === null) return\n return acquireMount(key, init)\n }, [key])\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"react.js","names":[],"sources":["../src/react.ts"],"sourcesContent":["'use client'\n\nimport { useEffect } from 'react'\n\nimport { setInstallEntry } from './runtime/analytics'\nimport { configKey } from './runtime/config-key'\nimport { acquireMount } from './runtime/shared-mount'\nimport { init, type WidgetConfig } from './widget'\n\nexport type { WidgetConfig } from './widget'\n\nexport type UseMarkupOptions = WidgetConfig & {\n /**\n * `false` unmounts the widget, or never mounts it. Drive it from your build\n * environment to keep the widget off production.\n *\n * @default true\n */\n enabled?: boolean\n}\n\n/**\n * Mounts the widget for as long as the calling component is mounted. Call it\n * once near your app root.\n *\n * An inline options object is fine: the widget remounts only when a value\n * changes, not when the object is recreated. Two components using the same\n * options share one widget, and it stays until both unmount. Server rendering\n * mounts nothing.\n */\nexport function useMarkup(options: UseMarkupOptions): void {\n const { enabled = true, ...config } = options\n const key = enabled ? configKey(config) : null\n\n useEffect(() => {\n if (key === null) return\n setInstallEntry('react')\n return acquireMount(key, init)\n }, [key])\n}\n"],"mappings":"+IA8BA,SAAgB,EAAU,EAAiC,CACzD,GAAM,CAAE,UAAU,GAAM,GAAG,GAAW,EAChC,EAAM,EAAU,EAAU,CAAM,EAAI,KAE1C,MAAgB,CACV,OAAQ,KAEZ,OADA,EAAgB,OAAO,EAChB,EAAa,EAAK,CAAI,CAC/B,EAAG,CAAC,CAAG,CAAC,CACV"}
|
package/dist/vue.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{init as e,t}from"./widget.js";import{t as
|
|
1
|
+
import{init as e,n as t,t as n}from"./widget.js";import{t as r}from"./shared-mount-B9ij4vuL.js";import{computed as i,onBeforeUnmount as a,onMounted as o,toValue as s,watch as c}from"vue";function l(l){let u=i(()=>{let{enabled:e=!0,...t}=s(l);return e?n(t):null}),d=null,f=n=>{d?.(),n!==null&&t(`vue`),d=n===null?null:r(n,e)},p=null;o(()=>{f(u.value),p=c(u,f)}),a(()=>{p?.(),f(null)})}export{l as useMarkup};
|
|
2
2
|
//# sourceMappingURL=vue.js.map
|
package/dist/vue.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vue.js","names":[],"sources":["../src/vue.ts"],"sourcesContent":["import { computed, onBeforeUnmount, onMounted, toValue, watch, type MaybeRefOrGetter } from 'vue'\n\nimport { configKey } from './runtime/config-key'\nimport { acquireMount } from './runtime/shared-mount'\nimport { init, type WidgetConfig } from './widget'\n\nexport type { WidgetConfig } from './widget'\n\nexport type UseMarkupOptions = WidgetConfig & {\n /**\n * `false` unmounts the widget, or never mounts it. Drive it from your build\n * environment to keep the widget off production.\n *\n * @default true\n */\n enabled?: boolean\n}\n\n/**\n * Mounts the widget for as long as the calling component is mounted. Call it\n * once from your root component's `setup`.\n *\n * Pass a plain object, a ref, or a getter. The widget remounts only when a\n * value changes, not when the object is replaced by an equal one. Two\n * components using the same options share one widget, and it stays until\n * both unmount. Nothing mounts during server rendering, since mounting waits\n * for `onMounted`.\n */\nexport function useMarkup(options: MaybeRefOrGetter<UseMarkupOptions>): void {\n const key = computed(() => {\n const { enabled = true, ...config } = toValue(options)\n return enabled ? configKey(config) : null\n })\n\n let release: (() => void) | null = null\n const apply = (next: string | null) => {\n release?.()\n release = next === null ? null : acquireMount(next, init)\n }\n\n let stopWatching: (() => void) | null = null\n onMounted(() => {\n apply(key.value)\n stopWatching = watch(key, apply)\n })\n onBeforeUnmount(() => {\n stopWatching?.()\n apply(null)\n })\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"vue.js","names":[],"sources":["../src/vue.ts"],"sourcesContent":["import { computed, onBeforeUnmount, onMounted, toValue, watch, type MaybeRefOrGetter } from 'vue'\n\nimport { setInstallEntry } from './runtime/analytics'\nimport { configKey } from './runtime/config-key'\nimport { acquireMount } from './runtime/shared-mount'\nimport { init, type WidgetConfig } from './widget'\n\nexport type { WidgetConfig } from './widget'\n\nexport type UseMarkupOptions = WidgetConfig & {\n /**\n * `false` unmounts the widget, or never mounts it. Drive it from your build\n * environment to keep the widget off production.\n *\n * @default true\n */\n enabled?: boolean\n}\n\n/**\n * Mounts the widget for as long as the calling component is mounted. Call it\n * once from your root component's `setup`.\n *\n * Pass a plain object, a ref, or a getter. The widget remounts only when a\n * value changes, not when the object is replaced by an equal one. Two\n * components using the same options share one widget, and it stays until\n * both unmount. Nothing mounts during server rendering, since mounting waits\n * for `onMounted`.\n */\nexport function useMarkup(options: MaybeRefOrGetter<UseMarkupOptions>): void {\n const key = computed(() => {\n const { enabled = true, ...config } = toValue(options)\n return enabled ? configKey(config) : null\n })\n\n let release: (() => void) | null = null\n const apply = (next: string | null) => {\n release?.()\n if (next !== null) setInstallEntry('vue')\n release = next === null ? null : acquireMount(next, init)\n }\n\n let stopWatching: (() => void) | null = null\n onMounted(() => {\n apply(key.value)\n stopWatching = watch(key, apply)\n })\n onBeforeUnmount(() => {\n stopWatching?.()\n apply(null)\n })\n}\n"],"mappings":"2LA6BA,SAAgB,EAAU,EAAmD,CAC3E,IAAM,EAAM,MAAe,CACzB,GAAM,CAAE,UAAU,GAAM,GAAG,GAAW,EAAQ,CAAO,EACrD,OAAO,EAAU,EAAU,CAAM,EAAI,IACvC,CAAC,EAEG,EAA+B,KAC7B,EAAS,GAAwB,CACrC,IAAU,EACN,IAAS,MAAM,EAAgB,KAAK,EACxC,EAAU,IAAS,KAAO,KAAO,EAAa,EAAM,CAAI,CAC1D,EAEI,EAAoC,KACxC,MAAgB,CACd,EAAM,EAAI,KAAK,EACf,EAAe,EAAM,EAAK,CAAK,CACjC,CAAC,EACD,MAAsB,CACpB,IAAe,EACf,EAAM,IAAI,CACZ,CAAC,CACH"}
|
package/dist/widget.d.ts
CHANGED
|
@@ -20,7 +20,8 @@ export interface ScreenshotsConfig {
|
|
|
20
20
|
*/
|
|
21
21
|
strictScrub?: boolean;
|
|
22
22
|
/**
|
|
23
|
-
* Custom CSS selector
|
|
23
|
+
* Custom CSS selector. Matched elements are masked unless a `data-markup-safe` element contains them.
|
|
24
|
+
* It is matched inside each open shadow root on its own, so it can't span a shadow boundary.
|
|
24
25
|
*
|
|
25
26
|
* @default undefined
|
|
26
27
|
*/
|