@preprio/toolkit 0.2.0 → 0.3.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
@@ -1,5 +1,10 @@
1
1
  # @preprio/toolkit
2
2
 
3
+ > **Beta — pre-1.0.** The package is published to `latest` and is safe to use, but the
4
+ > public API can still change between minor versions (`0.2.x` → `0.3.0`). Every change
5
+ > is documented in [Breaking changes](https://github.com/preprio/prepr-toolkit/blob/main/RELEASING.md#breaking-changes). Pin an exact
6
+ > version in production, or use a tilde range (`~0.2.0`) so you only pick up patches.
7
+
3
8
  A framework-agnostic TypeScript library that provides preview functionality, visual editing, and A/B testing for [Prepr CMS](https://prepr.io). Ships thin wrappers for Next.js, Nuxt, Astro, and SvelteKit on top of a vanilla core that runs anywhere.
4
9
 
5
10
  Every integration is the same three steps:
@@ -10,6 +15,8 @@ Every integration is the same three steps:
10
15
 
11
16
  Jump to your framework: [Next.js](#nextjs) · [Astro](#astro) · [SvelteKit](#sveltekit) · [Nuxt](#nuxt) · [React without a framework](#react-no-framework) · [anything else](#any-other-framework).
12
17
 
18
+ Coming from `@preprio/prepr-nextjs`? See the [migration guide](https://github.com/preprio/prepr-toolkit/blob/main/MIGRATION.md).
19
+
13
20
  ## Quick Start (Next.js)
14
21
 
15
22
  ```bash
@@ -812,6 +819,12 @@ createPreprPreview({ props, options: { locale: 'nl' } });
812
819
  - **Confirm the middleware matcher**: if `config.matcher` excludes the current path, no Prepr headers were set for it. `getPreprUUID()` returning `null` confirms this.
813
820
  - **Check for `prepr_hide_bar=true`** in the URL, which suppresses the bar by design.
814
821
 
822
+ ### Click-to-edit not working on some elements
823
+
824
+ - **Check the token permissions**: "Enable edit mode" must be checked on the token in Prepr.
825
+ - **Check whether the encoded text sits in a hidden element.** The parent of the encoded text node is what becomes editable, and a parent that renders no box (`display: none`, `visibility: hidden`, the `hidden` attribute) is skipped — it cannot be hovered or clicked. Put `data-prepr-edit-target` on the visible ancestor that should be editable instead. See [Choosing the element that becomes editable](#choosing-the-element-that-becomes-editable).
826
+ - **Inspect the tagged elements**: `document.querySelectorAll('[data-prepr-encoded]')` in the console lists everything edit mode found. An element you expected to be in that list points at one of the two cases above.
827
+
815
828
  ### Headers not working
816
829
 
817
830
  - **Middleware setup**: confirm `middleware.ts` sits in the project root (or `src/`) and its matcher covers the route.
@@ -883,7 +896,40 @@ The toolbar renders with Preact into a shadow-DOM custom element (`<prepr-toolba
883
896
 
884
897
  With edit mode enabled, the toolkit scans for stega-encoded content, strips the invisible Unicode characters after load so they cannot cause layout shifts, highlights editable elements by cursor proximity, and talks to the Prepr editor over a `postMessage` bridge when running inside the live-preview iframe.
885
898
 
886
- Stega cleaning is automatic. There is no need to call `vercelStegaSplit` or hand-manage hidden spans — the data attributes the toolkit relies on survive the clean, so edit mode still activates instantly.
899
+ Stega cleaning is automatic. There is no need to call `vercelStegaSplit` or to strip the payload yourself before rendering — the data attributes the toolkit relies on survive the clean, so edit mode still activates instantly.
900
+
901
+ #### Choosing the element that becomes editable
902
+
903
+ The element that gets the outline, the tooltip and the click handler is the **parent of the encoded text node**. Rendering `{title}` inside an `<h2>` makes the `<h2>` editable, which is what you want almost every time.
904
+
905
+ Two rules govern the choice:
906
+
907
+ - A parent that renders no box is skipped. An element hidden with `display: none`, `visibility: hidden`, or the `hidden` attribute cannot be hovered or outlined, so tagging it would produce an element that looks editable to the code and is unreachable to the visitor. Nothing is tagged for it.
908
+ - `data-prepr-edit-target` overrides the parent. The closest ancestor carrying it becomes the editable element instead.
909
+
910
+ That attribute is the supported way to separate _where the payload lives_ from _what the visitor clicks_. It is what makes the hidden-payload pattern work:
911
+
912
+ ```html
913
+ <!-- The payload rides in a visually hidden span; the <article> is what
914
+ gets outlined and clicked. -->
915
+ <article data-prepr-edit-target>
916
+ <h2>Product title</h2>
917
+ <span hidden>{encodedPayload}</span>
918
+ </article>
919
+ ```
920
+
921
+ Without the attribute that span is skipped entirely — it renders no box, so there is nothing to click.
922
+
923
+ It is equally useful when the encoded text sits deep inside markup you do not control, and the natural click target is an ancestor:
924
+
925
+ ```html
926
+ <figure data-prepr-edit-target>
927
+ <img src="..." alt="..." />
928
+ <figcaption><em>{encodedCaption}</em></figcaption>
929
+ </figure>
930
+ ```
931
+
932
+ Without it the `<em>` would be the editable element; with it the whole figure is.
887
933
 
888
934
  ## Examples
889
935
 
@@ -897,6 +943,14 @@ Runnable examples live in the repository root:
897
943
  | `examples/nuxt` | `server/middleware`, `useAsyncData`, `.vue` components |
898
944
  | `examples/express` | Vanilla core — a hand-written adapter for an unsupported framework |
899
945
 
946
+ ## Migrating from `@preprio/prepr-nextjs`
947
+
948
+ `@preprio/toolkit` replaces the Next.js-only `@preprio/prepr-nextjs` package. Most of
949
+ the change is import paths — the middleware signature is unchanged and the server
950
+ helpers keep their names — but the toolbar's provider and the stylesheet are gone.
951
+
952
+ The [migration guide](https://github.com/preprio/prepr-toolkit/blob/main/MIGRATION.md) has the diffs.
953
+
900
954
  ## Support
901
955
 
902
956
  - **Documentation**: [Prepr Documentation](https://docs.prepr.io)
@@ -30,7 +30,7 @@ __export(astro_exports, {
30
30
  module.exports = __toCommonJS(astro_exports);
31
31
 
32
32
  // src/version.ts
33
- var VERSION = "0.2.0";
33
+ var VERSION = "0.3.2";
34
34
 
35
35
  // src/core/features.ts
36
36
  function isEnabled(config) {
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var VERSION = "0.2.0";
2
+ var VERSION = "0.3.2";
3
3
 
4
4
  // src/core/features.ts
5
5
  function isEnabled(config) {
@@ -169,6 +169,20 @@ function walkTextNodes(root, visit) {
169
169
  visit(node);
170
170
  }
171
171
  }
172
+ function resolveEditTarget(node) {
173
+ const parent = node.parentElement;
174
+ if (!parent) return null;
175
+ const editTarget = parent.closest("[data-prepr-edit-target]");
176
+ if (editTarget) return editTarget;
177
+ return isRenderedElement(parent) ? parent : null;
178
+ }
179
+ function isRenderedElement(element) {
180
+ if (element.hidden) return false;
181
+ if (element.getClientRects().length === 0) return false;
182
+ const style = element.ownerDocument.defaultView?.getComputedStyle(element);
183
+ if (!style) return true;
184
+ return style.visibility !== "hidden" && style.display !== "none";
185
+ }
172
186
  function tagEncodedElement(element, decoded) {
173
187
  element.setAttribute("data-prepr-encoded", "");
174
188
  element.setAttribute("data-prepr-href", decoded.href);
@@ -192,7 +206,7 @@ var StegaElements = class {
192
206
  tagFromTextNode(node) {
193
207
  const decoded = this.decode(node.textContent);
194
208
  if (!decoded?.href) return false;
195
- const target = node.parentElement;
209
+ const target = resolveEditTarget(node);
196
210
  if (!target || target.hasAttribute("data-prepr-encoded")) return false;
197
211
  tagEncodedElement(target, decoded);
198
212
  return true;
@@ -630,11 +644,7 @@ function createStegaAutoClean() {
630
644
  if (!decoded?.href) return false;
631
645
  cleaned.set(textNode, { original: textContent, cleaned: stripped });
632
646
  textNode.textContent = stripped;
633
- let target = textNode.parentElement;
634
- const editTargetParent = textNode.parentElement?.closest(
635
- "[data-prepr-edit-target]"
636
- );
637
- if (editTargetParent) target = editTargetParent;
647
+ const target = resolveEditTarget(textNode);
638
648
  if (target) {
639
649
  tagEncodedElement(target, decoded);
640
650
  return true;
package/dist/index.cjs CHANGED
@@ -44,7 +44,7 @@ __export(src_exports, {
44
44
  module.exports = __toCommonJS(src_exports);
45
45
 
46
46
  // src/version.ts
47
- var VERSION = "0.2.0";
47
+ var VERSION = "0.3.2";
48
48
 
49
49
  // src/core/features.ts
50
50
  function isEnabled(config) {
@@ -420,6 +420,20 @@ function walkTextNodes(root, visit) {
420
420
  visit(node);
421
421
  }
422
422
  }
423
+ function resolveEditTarget(node) {
424
+ const parent = node.parentElement;
425
+ if (!parent) return null;
426
+ const editTarget = parent.closest("[data-prepr-edit-target]");
427
+ if (editTarget) return editTarget;
428
+ return isRenderedElement(parent) ? parent : null;
429
+ }
430
+ function isRenderedElement(element) {
431
+ if (element.hidden) return false;
432
+ if (element.getClientRects().length === 0) return false;
433
+ const style = element.ownerDocument.defaultView?.getComputedStyle(element);
434
+ if (!style) return true;
435
+ return style.visibility !== "hidden" && style.display !== "none";
436
+ }
423
437
  function tagEncodedElement(element, decoded) {
424
438
  element.setAttribute("data-prepr-encoded", "");
425
439
  element.setAttribute("data-prepr-href", decoded.href);
@@ -445,11 +459,7 @@ function createStegaAutoClean() {
445
459
  if (!decoded?.href) return false;
446
460
  cleaned.set(textNode, { original: textContent, cleaned: stripped });
447
461
  textNode.textContent = stripped;
448
- let target = textNode.parentElement;
449
- const editTargetParent = textNode.parentElement?.closest(
450
- "[data-prepr-edit-target]"
451
- );
452
- if (editTargetParent) target = editTargetParent;
462
+ const target = resolveEditTarget(textNode);
453
463
  if (target) {
454
464
  tagEncodedElement(target, decoded);
455
465
  return true;
@@ -545,7 +555,7 @@ var StegaElements = class {
545
555
  tagFromTextNode(node) {
546
556
  const decoded = this.decode(node.textContent);
547
557
  if (!decoded?.href) return false;
548
- const target = node.parentElement;
558
+ const target = resolveEditTarget(node);
549
559
  if (!target || target.hasAttribute("data-prepr-encoded")) return false;
550
560
  tagEncodedElement(target, decoded);
551
561
  return true;
package/dist/index.d.cts CHANGED
@@ -5,7 +5,7 @@ export { g as PreprEnvironment, h as PreprEventType, i as PreprFeatureConfig, j
5
5
  * Package version; must match package.json. Lives in its own leaf module so
6
6
  * core can import it without cycling through the barrel.
7
7
  */
8
- declare const VERSION = "0.2.0";
8
+ declare const VERSION = "0.3.2";
9
9
 
10
10
  /**
11
11
  * Resolve a `PreprFeatures` bag to plain booleans, defaulting every omitted key
package/dist/index.d.ts CHANGED
@@ -5,7 +5,7 @@ export { g as PreprEnvironment, h as PreprEventType, i as PreprFeatureConfig, j
5
5
  * Package version; must match package.json. Lives in its own leaf module so
6
6
  * core can import it without cycling through the barrel.
7
7
  */
8
- declare const VERSION = "0.2.0";
8
+ declare const VERSION = "0.3.2";
9
9
 
10
10
  /**
11
11
  * Resolve a `PreprFeatures` bag to plain booleans, defaulting every omitted key
package/dist/index.js CHANGED
@@ -17,10 +17,10 @@ import {
17
17
  setTrackingParam,
18
18
  stegaClean,
19
19
  trackEvent
20
- } from "./chunk-4PJK6IQ4.js";
20
+ } from "./chunk-GIEOTQD4.js";
21
21
 
22
22
  // src/version.ts
23
- var VERSION = "0.2.0";
23
+ var VERSION = "0.3.2";
24
24
 
25
25
  // src/core/server.ts
26
26
  var PreprError = class extends Error {
@@ -396,6 +396,20 @@ function walkTextNodes(root, visit) {
396
396
  visit(node);
397
397
  }
398
398
  }
399
+ function resolveEditTarget(node) {
400
+ const parent = node.parentElement;
401
+ if (!parent) return null;
402
+ const editTarget = parent.closest("[data-prepr-edit-target]");
403
+ if (editTarget) return editTarget;
404
+ return isRenderedElement(parent) ? parent : null;
405
+ }
406
+ function isRenderedElement(element) {
407
+ if (element.hidden) return false;
408
+ if (element.getClientRects().length === 0) return false;
409
+ const style = element.ownerDocument.defaultView?.getComputedStyle(element);
410
+ if (!style) return true;
411
+ return style.visibility !== "hidden" && style.display !== "none";
412
+ }
399
413
  function tagEncodedElement(element, decoded) {
400
414
  element.setAttribute("data-prepr-encoded", "");
401
415
  element.setAttribute("data-prepr-href", decoded.href);
@@ -421,11 +435,7 @@ function createStegaAutoClean() {
421
435
  if (!decoded?.href) return false;
422
436
  cleaned.set(textNode, { original: textContent, cleaned: stripped });
423
437
  textNode.textContent = stripped;
424
- let target = textNode.parentElement;
425
- const editTargetParent = textNode.parentElement?.closest(
426
- "[data-prepr-edit-target]"
427
- );
428
- if (editTargetParent) target = editTargetParent;
438
+ const target = resolveEditTarget(textNode);
429
439
  if (target) {
430
440
  tagEncodedElement(target, decoded);
431
441
  return true;
@@ -521,7 +531,7 @@ var StegaElements = class {
521
531
  tagFromTextNode(node) {
522
532
  const decoded = this.decode(node.textContent);
523
533
  if (!decoded?.href) return false;
524
- const target = node.parentElement;
534
+ const target = resolveEditTarget(node);
525
535
  if (!target || target.hasAttribute("data-prepr-encoded")) return false;
526
536
  tagEncodedElement(target, decoded);
527
537
  return true;
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  createPreprPreview,
4
4
  loadTrackingPixel
5
- } from "../chunk-4PJK6IQ4.js";
5
+ } from "../chunk-GIEOTQD4.js";
6
6
 
7
7
  // src/nextjs/components.ts
8
8
  import { usePathname, useRouter } from "next/navigation";
@@ -38,7 +38,7 @@ module.exports = __toCommonJS(nextjs_exports);
38
38
  var import_server = require("next/server");
39
39
 
40
40
  // src/version.ts
41
- var VERSION = "0.2.0";
41
+ var VERSION = "0.3.2";
42
42
 
43
43
  // src/core/constants.ts
44
44
  var COOKIE_PREVIEW_MODE = "Prepr-Preview-Mode";
@@ -2,7 +2,7 @@
2
2
  import { NextResponse } from "next/server";
3
3
 
4
4
  // src/version.ts
5
- var VERSION = "0.2.0";
5
+ var VERSION = "0.3.2";
6
6
 
7
7
  // src/core/constants.ts
8
8
  var COOKIE_PREVIEW_MODE = "Prepr-Preview-Mode";
@@ -35,7 +35,7 @@ __export(nuxt_exports, {
35
35
  module.exports = __toCommonJS(nuxt_exports);
36
36
 
37
37
  // src/version.ts
38
- var VERSION = "0.2.0";
38
+ var VERSION = "0.3.2";
39
39
 
40
40
  // src/core/constants.ts
41
41
  var COOKIE_PREVIEW_MODE = "Prepr-Preview-Mode";
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var VERSION = "0.2.0";
2
+ var VERSION = "0.3.2";
3
3
 
4
4
  // src/core/constants.ts
5
5
  var COOKIE_PREVIEW_MODE = "Prepr-Preview-Mode";
@@ -395,6 +395,20 @@ function walkTextNodes(root, visit) {
395
395
  visit(node);
396
396
  }
397
397
  }
398
+ function resolveEditTarget(node) {
399
+ const parent = node.parentElement;
400
+ if (!parent) return null;
401
+ const editTarget = parent.closest("[data-prepr-edit-target]");
402
+ if (editTarget) return editTarget;
403
+ return isRenderedElement(parent) ? parent : null;
404
+ }
405
+ function isRenderedElement(element) {
406
+ if (element.hidden) return false;
407
+ if (element.getClientRects().length === 0) return false;
408
+ const style = element.ownerDocument.defaultView?.getComputedStyle(element);
409
+ if (!style) return true;
410
+ return style.visibility !== "hidden" && style.display !== "none";
411
+ }
398
412
  function tagEncodedElement(element, decoded) {
399
413
  element.setAttribute("data-prepr-encoded", "");
400
414
  element.setAttribute("data-prepr-href", decoded.href);
@@ -420,11 +434,7 @@ function createStegaAutoClean() {
420
434
  if (!decoded?.href) return false;
421
435
  cleaned.set(textNode, { original: textContent, cleaned: stripped });
422
436
  textNode.textContent = stripped;
423
- let target = textNode.parentElement;
424
- const editTargetParent = textNode.parentElement?.closest(
425
- "[data-prepr-edit-target]"
426
- );
427
- if (editTargetParent) target = editTargetParent;
437
+ const target = resolveEditTarget(textNode);
428
438
  if (target) {
429
439
  tagEncodedElement(target, decoded);
430
440
  return true;
@@ -520,7 +530,7 @@ var StegaElements = class {
520
530
  tagFromTextNode(node) {
521
531
  const decoded = this.decode(node.textContent);
522
532
  if (!decoded?.href) return false;
523
- const target = node.parentElement;
533
+ const target = resolveEditTarget(node);
524
534
  if (!target || target.hasAttribute("data-prepr-encoded")) return false;
525
535
  tagEncodedElement(target, decoded);
526
536
  return true;
@@ -371,6 +371,20 @@ function walkTextNodes(root, visit) {
371
371
  visit(node);
372
372
  }
373
373
  }
374
+ function resolveEditTarget(node) {
375
+ const parent = node.parentElement;
376
+ if (!parent) return null;
377
+ const editTarget = parent.closest("[data-prepr-edit-target]");
378
+ if (editTarget) return editTarget;
379
+ return isRenderedElement(parent) ? parent : null;
380
+ }
381
+ function isRenderedElement(element) {
382
+ if (element.hidden) return false;
383
+ if (element.getClientRects().length === 0) return false;
384
+ const style = element.ownerDocument.defaultView?.getComputedStyle(element);
385
+ if (!style) return true;
386
+ return style.visibility !== "hidden" && style.display !== "none";
387
+ }
374
388
  function tagEncodedElement(element, decoded) {
375
389
  element.setAttribute("data-prepr-encoded", "");
376
390
  element.setAttribute("data-prepr-href", decoded.href);
@@ -396,11 +410,7 @@ function createStegaAutoClean() {
396
410
  if (!decoded?.href) return false;
397
411
  cleaned.set(textNode, { original: textContent, cleaned: stripped });
398
412
  textNode.textContent = stripped;
399
- let target = textNode.parentElement;
400
- const editTargetParent = textNode.parentElement?.closest(
401
- "[data-prepr-edit-target]"
402
- );
403
- if (editTargetParent) target = editTargetParent;
413
+ const target = resolveEditTarget(textNode);
404
414
  if (target) {
405
415
  tagEncodedElement(target, decoded);
406
416
  return true;
@@ -496,7 +506,7 @@ var StegaElements = class {
496
506
  tagFromTextNode(node) {
497
507
  const decoded = this.decode(node.textContent);
498
508
  if (!decoded?.href) return false;
499
- const target = node.parentElement;
509
+ const target = resolveEditTarget(node);
500
510
  if (!target || target.hasAttribute("data-prepr-encoded")) return false;
501
511
  tagEncodedElement(target, decoded);
502
512
  return true;
@@ -35,7 +35,7 @@ __export(sveltekit_exports, {
35
35
  module.exports = __toCommonJS(sveltekit_exports);
36
36
 
37
37
  // src/version.ts
38
- var VERSION = "0.2.0";
38
+ var VERSION = "0.3.2";
39
39
 
40
40
  // src/core/constants.ts
41
41
  var COOKIE_PREVIEW_MODE = "Prepr-Preview-Mode";
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var VERSION = "0.2.0";
2
+ var VERSION = "0.3.2";
3
3
 
4
4
  // src/core/constants.ts
5
5
  var COOKIE_PREVIEW_MODE = "Prepr-Preview-Mode";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@preprio/toolkit",
3
- "version": "0.2.0",
3
+ "version": "0.3.2",
4
4
  "description": "Prepr personalization, A/B testing and preview toolkit for React, Next.js, Nuxt, Astro, SvelteKit and Express",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Prepr B.V. (https://prepr.io)",