@8ux-co/eelzap 0.10.2 → 0.11.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/CHANGELOG.md CHANGED
@@ -5,6 +5,65 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.11.1] - 2026-10-16
9
+
10
+ ### Added
11
+
12
+ - Install marker (#1113): `ZapPreview` (`./react` and `./next`) now renders `<meta name="zap-client" content="<version>" data-site="<siteKey>">`, server-side and without JavaScript (React 19 hoists it into `<head>`). Zap reads it to show «Instalado» in site settings without opening a preview. No record tag and no stega: a published page stays untagged. The marker's version lives in `src/version.ts`, held equal to the runtime's by a spec.
13
+ - `previewPath` (the «Ruta en tu sitio» of a collection or a document) now comes back on reads, not only on `update()`: `collections.list()`, `collections.get()`, `documents.list()` and `documents.get()` carry it, `null` when unset, so a seed or infrastructure-as-code can detect drift. Server code with a `secret_` key or a suite credential only; a `public_` key's answer is unchanged.
14
+ - `structured-values` capability in `zap:ready`. To a client that has it, the editor sends a CURRENCY field's live value as `{ amountMinor, currency, text }` and an ENUM's as `{ value, label }`, the delivery API's shapes; the overlay writes `text` / `label`, and `useZapLiveUpdates` / `mergeLiveValues` merge `{ amountMinor, currency, amount, text }` and `{ value, label }`, so code reading `price.amountMinor` or `category.value` keeps working while the editor types. Clients without it keep getting text.
15
+ - `data-zap-site-renders` and `fields(…).derived(key)`: mark an element that shows what your site computes from a field (an italic-split title, a long text split into paragraphs, an uppercased label). The overlay outlines it, opens the field on a click and anchors comments to it, and never writes into it; stega markers inside it are not indexed. The value is `recordRef#fieldKey`, or a bare field key inside `data-zap-entry`. Your site keeps the element current with `useZapLiveUpdates` or `onValues`.
16
+ - `CurrencyValue.amount` (reads): the amount in major units, `amountMinor` divided by ten to the currency's ISO 4217 exponent.
17
+
18
+ ### Changed
19
+
20
+ - Smoother scroll sync in Comparar. The page that follows applies the latest `zap:scroll-to` once per animation frame, always instantly (a site's `scroll-behavior: smooth` no longer animates against the next update), instead of every message as it arrives. `zap:scroll` now anchors to the last tagged field at or above the viewport's top and adds two optional fields, `next` (`{ recordRef, fieldKey, nth }`, the tagged field below it) and `progress` (0..1, how far the top is from one to the other); the following page places its top that far between its own two fields, so it no longer jumps when the anchor changes on pages that space their fields differently. Both fields are optional: a 0.11.0 page or editor ignores them and keeps the anchor and offset.
21
+ - The delivery API's `amount` on a CURRENCY value is in major units (`48000` for COP `amountMinor: 4800000`, `4800` for JPY `4800`); it used to repeat `amountMinor`.
22
+ - The currency exponent table adds the four-digit codes (CLF, UYW).
23
+ - «Editar» on a CURRENCY field is a text input in the reader's locale: it shows `46.000` (es) or `46,000.00` (en), and reads typed or pasted text the way Zap's own money field does, so `30.000` in es-CO, `$ 30.000` and `30,000.50` save the amount you meant. It used to read `30.000` as thirty.
24
+ - `zap:ready` reports client version `0.11.1`.
25
+
26
+ ## [0.11.0] - 2026-10-08
27
+
28
+ ### Added
29
+
30
+ - Comparar support in the preview client, for Zap's side-by-side view of the published page and the draft. Two new capabilities in `zap:ready`, so the editor offers each only to a client that has it:
31
+ - `scroll-sync`: `zap:scroll-sync` `{ enabled }` turns on `zap:scroll` `{ y, x?, anchor? }` reports, one per animation frame at most, with the scroll position as fractions and the tagged field nearest the top as an anchor (`{ recordRef, fieldKey, nth, offset }`). `zap:scroll-to` (same shape) places the page by the anchor, or by the fraction when the page lacks that field, and is never reported back, so two frames cannot chase each other.
32
+ - `diff-marks`: `zap:marks` `{ marks: [{ recordRef, fieldKey, tone, label, active? }] }` (up to 200) marks every element of each field, red for `removed` and green for `added`, the active change tinted and named. An empty list clears them; marks follow scrolling and re-rendering.
33
+ - The editor bridge (`@8ux-co/eelzap/internal/editor`) adds `setScrollSync`, `scrollTo`, `setMarks` and the `onScroll` handler.
34
+ - Opt-in fragment transport for preview tokens (#947). The client advertises
35
+ `fragment-token` to the editor and sends `tokenTransport: 'fragment'` to the
36
+ live-site session API. Without either opt-in, legacy query URLs remain the
37
+ default, so sites on SDK 0.10.2 and earlier keep working, including the editor's
38
+ first load before the client announces its capabilities.
39
+ - `createDraftModeRoute` exchanges fragment tokens through a same-origin POST
40
+ body and redirects with `303` to a validated clean path. Its exchange page
41
+ loads an external script from the same route (`?script=1`), supporting
42
+ `script-src 'self'`. POST requires `Sec-Fetch-Site: same-origin` or an `Origin`
43
+ equal to the request's own origin; otherwise it returns `403` without cookies.
44
+
45
+ ### Changed
46
+
47
+ - `zap:ready` reports client version `0.11.0`.
48
+ - The 0.11 client renews live-site draft sessions with a same-origin POST to the
49
+ site's draft route.
50
+
51
+ ### Migration
52
+
53
+ - **Upgrading to 0.11 requires the site's draft route to export `POST` as well
54
+ as `GET`**, for example `export const POST = createDraftModeRoute({ … })` and
55
+ `export const GET = POST` (see the README), or `export const { GET, POST } = …`
56
+ in an equivalent wrapper. A route that exports only `GET` answers `405` to the
57
+ new client and editor preview breaks. Upgrade the route handler and the
58
+ browser client together. Sites on the script-tag (CDN boot) snippet that adopt
59
+ the new snippet need the same `GET` and `POST` route.
60
+ - Legacy `GET ?token=…` remains supported for older clients; its query tokens
61
+ can still appear in site request logs. Only fragment exchanges keep tokens out
62
+ of request URLs.
63
+ - Deploy order: Zap must ship to production (the session API's
64
+ `tokenTransport: 'fragment'` and the 0.11.0 CDN bundles) before 0.11.0 is
65
+ published to npm, since the published boot pins those bundles by path and SRI.
66
+
8
67
  ## [0.10.2] - Unreleased
9
68
 
10
69
  ### Added
package/README.md CHANGED
@@ -66,7 +66,7 @@ const { data: posts } = await cms.items.list('blog-posts', {
66
66
  | `retry` | `boolean \| RetryOptions` | No | On |
67
67
 
68
68
  `apiKey` is a site API key (`secret_…` or `public_…`), or a preview token
69
- (`zpt_…`) from a draft-mode URL.
69
+ (`zpt_…`) from a draft-mode exchange.
70
70
 
71
71
  ### Draft preview
72
72
 
@@ -107,8 +107,10 @@ createClient({
107
107
 
108
108
  ### Rate limits and retries
109
109
 
110
- A site key may send 100 requests a minute (`public_`: 60), and past that the
111
- API answers `429` with `Retry-After`. The client waits that long and tries
110
+ A site key may send 300 requests a minute (`public_`: 180), all at once after a
111
+ quiet spell, and a published read the API has already answered, with nothing
112
+ changed on the site since, is served from its cache for free (`X-Zap-Cache:
113
+ HIT`). Past the limit the API answers `429` with `Retry-After`. The client waits that long and tries
112
114
  again, so a build that renders every page, or a seed script, does not fail
113
115
  half-way. A `503` that carries `Retry-After` is retried the same way.
114
116
 
@@ -404,22 +406,49 @@ export default async function RootLayout({ children }: { children: React.ReactNo
404
406
  import { cookies, draftMode } from 'next/headers'
405
407
  import { createDraftModeRoute } from '@8ux-co/eelzap/next'
406
408
 
407
- export const GET = createDraftModeRoute({
409
+ export const POST = createDraftModeRoute({
408
410
  siteKey: 'your-site-key',
409
411
  apiKey: process.env.EELZAP_API_KEY!, // one of the site's own API keys
410
412
  draftMode,
411
413
  cookies,
412
414
  })
415
+ export const GET = POST
413
416
  ```
414
417
 
415
418
  `<ZapPreview />` tells the editor where the route lives (`/api/zap-preview`
416
419
  by default; `draftRoute="/es/preview"` or `draftRoute={null}` otherwise).
417
420
  The page names only a path: the editor joins it to the page's own verified
418
- origin, mints a ten-minute preview token and loads
419
- `{origin}{route}?token=…&path=…`. The route checks the token with Zap
420
- (minted for this site, live), enables draft mode with cookies that survive
421
- inside Zap's frame (`SameSite=None; Secure; Partitioned`) and redirects to the
422
- page without the token. `createDraftModeExitRoute({ draftMode })` ends it.
421
+ origin and mints a ten-minute preview token. The editor uses the legacy
422
+ `{origin}{route}?token=…&path=…` unless the site's client has announced the
423
+ `fragment-token` capability. The new client advertises that capability; the
424
+ editor then uses `{origin}{route}#token=…&path=…`. Before the first ready/hello
425
+ message, it keeps the query form, so older clients keep working.
426
+
427
+ URL fragments never reach the HTTP server or its request logs. The route's
428
+ tiny exchange page loads its script from the same route (`?script=1`,
429
+ `application/javascript`), so `script-src 'self'` works without an inline
430
+ script. The script clears the fragment
431
+ from browser history, then submits `token` and `path` in an
432
+ `application/x-www-form-urlencoded` POST body to the same clean route URL.
433
+ POST requires `Sec-Fetch-Site: same-origin` or an `Origin` header equal to the
434
+ request's own origin. Other POSTs, including those missing both headers,
435
+ return `403` without setting cookies. The route checks the token with Zap
436
+ (minted for this site, live), enables
437
+ draft mode with cookies that survive inside Zap's frame
438
+ (`SameSite=None; Secure; Partitioned`) and answers `303` to the clean `path`.
439
+ The target must be a same-origin relative path beginning with a single `/`;
440
+ external URLs, protocol-relative paths, backslashes, control characters and
441
+ paths containing preview tokens are refused. `createDraftModeExitRoute({ draftMode })`
442
+ ends it.
443
+
444
+ Upgrade the site's route handler and browser client together and export both
445
+ `GET` and `POST` as above before opting into the new exchange. The new live-site
446
+ client sends `tokenTransport: 'fragment'` to `POST /api/public/v1/preview/session`
447
+ and renews drafts with a same-origin POST. Without that field, the API returns
448
+ the legacy query URL. Legacy `GET ?token=…&path=…` remains supported and keeps
449
+ older sites working; those requests can still expose tokens to URL logs.
450
+ Live-site draft-session renewal uses a POST body directly, without navigating
451
+ the tab or putting a token in a request URL.
423
452
 
424
453
  Read drafts on the server with the token as the credential:
425
454
 
@@ -563,6 +592,20 @@ the unsaved values merged in, and `onValues(entry, handler)` from
563
592
  `@8ux-co/eelzap/react` calls you with each patch (it is framework-free; the
564
593
  overlay also announces each patch as an `eelzap:values` window event).
565
594
 
595
+ When an element shows something your code builds from a field (a title split
596
+ around an italic word, a long text split into paragraphs, an uppercased
597
+ label), spread `f.derived('title')` on it, or write
598
+ `data-zap-site-renders="blog-posts/my-post#title"` by hand (a bare field key
599
+ inside `data-zap-entry`). The overlay still outlines it, opens the field on a
600
+ click and anchors comments to it, but never writes into it: your component
601
+ re-renders it from `useZapLiveUpdates` or `onValues`.
602
+
603
+ ```tsx
604
+ const live = useZapLiveUpdates(post)
605
+ const [head, tail] = splitTitle(cleanStega(live.content.title))
606
+ <h1 {...fields(post).derived('title')}>{head} <em>{tail}</em></h1>
607
+ ```
608
+
566
609
  ### Suggestions on the live site
567
610
 
568
611
  With `siteId`, people with a Zap seat in your workspace can suggest changes on
@@ -62,14 +62,22 @@ var MAX_URL_LENGTH = 2048;
62
62
  var MAX_TAG_REPORT = 2e3;
63
63
  var MAX_LABELS = 500;
64
64
  var MAX_PINS = 50;
65
+ var MAX_MARKS = 200;
66
+ var MAX_MARK_LABEL = 200;
67
+ var MAX_ANCHOR_NTH = 5e3;
65
68
  var OVERLAY_MODES = ["inspect", "select", "spot", "off"];
69
+ var MAX_CAPABILITIES = 32;
66
70
  var CAPABILITIES = [
67
71
  "overlay",
68
72
  "values",
69
73
  "stega",
70
74
  "pins",
71
75
  "links",
72
- "refresh"
76
+ "refresh",
77
+ "scroll-sync",
78
+ "diff-marks",
79
+ "fragment-token",
80
+ "structured-values"
73
81
  ];
74
82
  var MAX_EMAIL_LENGTH = 320;
75
83
  function emptyDropStats() {
@@ -151,13 +159,30 @@ function isPin(value) {
151
159
  function isFieldRef(value) {
152
160
  return isObject(value) && isRecordRef(value.recordRef) && isFieldKey(value.fieldKey);
153
161
  }
162
+ function isScrollRef(value) {
163
+ return isFieldRef(value) && Number.isInteger(value.nth) && isFiniteNumber(value.nth, 0, MAX_ANCHOR_NTH);
164
+ }
165
+ function isScrollPayload(value) {
166
+ if (!isObject(value) || !isFiniteNumber(value.y, 0, 1)) return false;
167
+ if (value.x !== void 0 && !isFiniteNumber(value.x, 0, 1)) return false;
168
+ if (value.progress !== void 0 && !isFiniteNumber(value.progress, 0, 1)) return false;
169
+ const anchor = value.anchor;
170
+ return isOptional(value.next, isScrollRef) && (anchor === void 0 || isScrollRef(anchor) && isFiniteNumber(anchor.offset, -1e7, 1e7));
171
+ }
172
+ function isDiffMark(value) {
173
+ return isFieldRef(value) && (value.tone === "removed" || value.tone === "added") && isString(value.label, MAX_MARK_LABEL) && (value.active === void 0 || typeof value.active === "boolean");
174
+ }
154
175
  function isPreviewValue(value) {
155
176
  if (value === null || typeof value === "boolean") return true;
156
177
  if (typeof value === "string") return true;
157
178
  if (typeof value === "number") return Number.isFinite(value);
158
179
  if (!isObject(value)) return false;
159
- const keys = Object.keys(value);
160
- if (keys.length !== 1) return false;
180
+ const keys = Object.keys(value).sort().join(",");
181
+ if (keys === "amountMinor,currency,text") {
182
+ return Number.isSafeInteger(value.amountMinor) && isString(value.currency, 8) && typeof value.text === "string";
183
+ }
184
+ if (keys === "label,value") return isString(value.value, 2e3) && isString(value.label, 2e3);
185
+ if (keys.includes(",")) return false;
161
186
  if ("text" in value) return typeof value.text === "string";
162
187
  if ("html" in value) return typeof value.html === "string";
163
188
  if ("url" in value) return isString(value.url, MAX_URL_LENGTH);
@@ -203,7 +228,10 @@ function parseTag(value) {
203
228
  return { recordRef, fieldKey };
204
229
  }
205
230
  var PAGE_VALIDATORS = {
206
- "zap:ready": (p) => isObject(p) && isString(p.version, 32) && Array.isArray(p.capabilities) && p.capabilities.length <= CAPABILITIES.length && p.capabilities.every((c) => CAPABILITIES.includes(c)) && isHttpUrl(p.pageUrl) && (p.draftRoute === void 0 || p.draftRoute === null || isDraftRoutePath(p.draftRoute)),
231
+ "zap:ready": (p) => isObject(p) && isString(p.version, 32) && Array.isArray(p.capabilities) && // A capability this editor does not know is IGNORED, never a refusal: a
232
+ // newer client on a site must not go dark against an editor that has not
233
+ // shipped its protocol yet (#953, 0.11.0 added two). Bounded all the same.
234
+ p.capabilities.length <= MAX_CAPABILITIES && p.capabilities.every((c) => isString(c, 32)) && isHttpUrl(p.pageUrl) && (p.draftRoute === void 0 || p.draftRoute === null || isDraftRoutePath(p.draftRoute)),
207
235
  "zap:tags": (p) => Array.isArray(p) && p.length <= MAX_TAG_REPORT && p.every(
208
236
  (t) => isObject(t) && typeof t.tag === "string" && isRecordRef(t.recordRef) && isFieldKey(t.fieldKey) && t.tag === `${t.recordRef}#${t.fieldKey}` && Number.isInteger(t.count) && t.count > 0 && (t.source === void 0 || t.source === "stega" || t.source === "attr")
209
237
  ),
@@ -213,7 +241,8 @@ var PAGE_VALIDATORS = {
213
241
  "zap:error": (p) => isObject(p) && typeof p.code === "string" && ERROR_CODE_RE.test(p.code) && isOptional(p.detail, (d) => isString(d, 500)),
214
242
  "zap:spot": (p) => isObject(p) && isSpotPoint(p.spot) && isObject(p.viewport) && isFiniteNumber(p.viewport.w, 1, 1e5) && isFiniteNumber(p.viewport.h, 1, 1e7) && isHttpUrl(p.pageUrl),
215
243
  "zap:pin": (p) => isObject(p) && isPinId(p.id),
216
- "zap:paused": (p) => isObject(p) && (p.reason === "runaway" || p.reason === "cap") && Number.isInteger(p.count) && p.count >= 0
244
+ "zap:paused": (p) => isObject(p) && (p.reason === "runaway" || p.reason === "cap") && Number.isInteger(p.count) && p.count >= 0,
245
+ "zap:scroll": isScrollPayload
217
246
  };
218
247
  var EDITOR_VALIDATORS = {
219
248
  "zap:hello": (p) => {
@@ -239,7 +268,10 @@ var EDITOR_VALIDATORS = {
239
268
  "zap:zoom": (p) => isObject(p) && isZoom(p.zoom),
240
269
  "zap:refresh": isObject,
241
270
  "zap:highlight": (p) => isObject(p) && isAnchorList(p.anchors, isDomAnchor),
242
- "zap:pins": (p) => isObject(p) && Array.isArray(p.pins) && p.pins.length <= MAX_PINS && p.pins.every(isPin)
271
+ "zap:pins": (p) => isObject(p) && Array.isArray(p.pins) && p.pins.length <= MAX_PINS && p.pins.every(isPin),
272
+ "zap:scroll-sync": (p) => isObject(p) && typeof p.enabled === "boolean",
273
+ "zap:scroll-to": isScrollPayload,
274
+ "zap:marks": (p) => isObject(p) && Array.isArray(p.marks) && p.marks.length <= MAX_MARKS && p.marks.every(isDiffMark)
243
275
  };
244
276
  var PAGE_MESSAGE_TYPES = /* @__PURE__ */ Object.keys(PAGE_VALIDATORS);
245
277
  var EDITOR_MESSAGE_TYPES = /* @__PURE__ */ Object.keys(
@@ -528,6 +560,10 @@ var DEFAULT_THEME = {
528
560
  };
529
561
  var SELECTED_TINT = "rgba(207,135,0,0.10)";
530
562
  var CHECK_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>';
563
+ var TONES = {
564
+ removed: ["#FB2C36", "rgba(251,44,54,0.08)", "#C10007", "\u2212"],
565
+ added: ["#00C950", "rgba(0,201,80,0.08)", "#008236", "+"]
566
+ };
531
567
  var Overlay = class {
532
568
  constructor(options) {
533
569
  __publicField(this, "options", options);
@@ -543,6 +579,7 @@ var Overlay = class {
543
579
  __publicField(this, "highlighted", []);
544
580
  __publicField(this, "selected", []);
545
581
  __publicField(this, "pins", []);
582
+ __publicField(this, "marks", []);
546
583
  __publicField(this, "pending", null);
547
584
  __publicField(this, "capture", null);
548
585
  __publicField(this, "frame", null);
@@ -669,6 +706,11 @@ var Overlay = class {
669
706
  this.pins = pins.slice(0, MAX_PINS);
670
707
  this.clearPending();
671
708
  }
709
+ /** Draw these diff marks (Comparar), in every mode; an empty list clears them. */
710
+ setMarks(marks) {
711
+ this.marks = marks.slice();
712
+ this.schedule();
713
+ }
672
714
  /** Drop the pending spot pin. */
673
715
  clearPending() {
674
716
  this.pending = null;
@@ -688,7 +730,8 @@ var Overlay = class {
688
730
  const nodes = Array.from(this.layer?.children ?? []);
689
731
  const boxes = nodes.filter((node) => node.className === "box").map((node) => ({
690
732
  kind: node.dataset.kind,
691
- label: node.querySelector(".chip")?.textContent ?? null
733
+ label: node.querySelector(".chip")?.textContent ?? null,
734
+ ...node.dataset.tone ? { tone: node.dataset.tone } : {}
692
735
  }));
693
736
  const capture = this.capture?.style.display === "block" ? this.capture : null;
694
737
  return { boxes, pins: nodes.filter((n) => n.className === "pin"), capture, zoom: this.zoom };
@@ -810,7 +853,12 @@ var Overlay = class {
810
853
  render() {
811
854
  const layer = this.layer;
812
855
  if (!layer) return;
813
- const boxes = [];
856
+ const boxes = this.marks.filter((mark) => mark.element.isConnected).map(({ element, tone, label, active }) => ({
857
+ element,
858
+ kind: "mark",
859
+ label: active ? label : null,
860
+ mark: { tone, active }
861
+ }));
814
862
  const seen = /* @__PURE__ */ new Set();
815
863
  const add = (element, kind) => {
816
864
  if (seen.has(element) || !element.isConnected) return;
@@ -894,14 +942,17 @@ var Overlay = class {
894
942
  node.className = "box";
895
943
  node.dataset.kind = box.kind;
896
944
  const offset = 3 / z;
945
+ const mark = box.mark;
946
+ const [markOutline, markTint, markChip, sign] = mark ? TONES[mark.tone] : [];
947
+ if (mark) node.dataset.tone = mark.tone;
897
948
  Object.assign(node.style, {
898
949
  left: `${rect.left - offset}px`,
899
950
  top: `${rect.top - offset}px`,
900
951
  width: `${rect.width + offset * 2}px`,
901
952
  height: `${rect.height + offset * 2}px`,
902
- outline: `${px(2)} solid ${this.theme.outline}`,
953
+ outline: mark ? mark.active ? `${px(2)} solid ${markOutline}` : `${px(1.5)} dashed ${markOutline}` : `${px(2)} solid ${this.theme.outline}`,
903
954
  borderRadius: px(3),
904
- background: box.kind === "selected" ? SELECTED_TINT : "transparent"
955
+ background: box.kind === "selected" ? SELECTED_TINT : mark?.active ? markTint : "transparent"
905
956
  });
906
957
  const tooSmall = box.kind === "selected" && box.element !== this.hover && Math.min(rect.width, rect.height) * z < CHIP_HEIGHT_PX;
907
958
  if (box.label && box.kind !== "highlight" && !tooSmall) {
@@ -917,9 +968,15 @@ var Overlay = class {
917
968
  gap: px(4),
918
969
  borderRadius: px(6),
919
970
  fontSize: px(12),
920
- background: this.theme.chip,
921
- color: this.theme.chipText
971
+ background: markChip ?? this.theme.chip,
972
+ color: mark ? "#FFFFFF" : this.theme.chipText
922
973
  });
974
+ if (sign) {
975
+ const glyph = doc.createElement("span");
976
+ glyph.className = "sign";
977
+ glyph.textContent = sign;
978
+ chip.appendChild(glyph);
979
+ }
923
980
  if (box.kind === "selected") {
924
981
  const icon = doc.createElement("span");
925
982
  icon.className = "icon";
@@ -959,6 +1016,7 @@ var STYLES = `
959
1016
  letter-spacing: 0; text-transform: none; font-style: normal; box-sizing: border-box;
960
1017
  }
961
1018
  .of { font-weight: 400; opacity: .72; }
1019
+ .sign { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; }
962
1020
  .capture { position: fixed; inset: 0; pointer-events: auto; cursor: crosshair; display: none; }
963
1021
  .pin {
964
1022
  position: fixed; box-sizing: border-box; pointer-events: auto; cursor: pointer; text-align: center;
@@ -970,10 +1028,26 @@ var STYLES = `
970
1028
  `;
971
1029
 
972
1030
  // src/preview/tags.ts
973
- var TAG_SELECTOR = "[data-zap],[data-zap-field]";
1031
+ var TAG_SELECTOR = "[data-zap],[data-zap-field],[data-zap-site-renders]";
1032
+ var SITE_RENDERS_SELECTOR = "[data-zap-site-renders]";
974
1033
  function readElementTag(element) {
975
1034
  const locale = readLocale(element);
976
1035
  const html = element.hasAttribute("data-zap-html");
1036
+ const siteRenders = element.getAttribute("data-zap-site-renders");
1037
+ if (siteRenders !== null) {
1038
+ const value = siteRenders.trim();
1039
+ if (value.includes("#")) {
1040
+ const parsed = parseTag(value);
1041
+ if (!parsed) {
1042
+ return { ok: false, problem: { attribute: "data-zap-site-renders", value: clip(value) } };
1043
+ }
1044
+ return {
1045
+ ok: true,
1046
+ tagged: build(element, parsed.recordRef, parsed.fieldKey, locale, false, true)
1047
+ };
1048
+ }
1049
+ return readEntryField(element, value, "data-zap-site-renders", locale, false, true);
1050
+ }
977
1051
  const direct = element.getAttribute("data-zap");
978
1052
  if (direct !== null) {
979
1053
  const parsed = parseTag(direct);
@@ -982,18 +1056,21 @@ function readElementTag(element) {
982
1056
  }
983
1057
  const field = element.getAttribute("data-zap-field");
984
1058
  if (field === null) return null;
1059
+ return readEntryField(element, field, "data-zap-field", locale, html, false);
1060
+ }
1061
+ function readEntryField(element, field, attribute, locale, html, siteRenders) {
985
1062
  const fieldKey = field.trim();
986
1063
  if (!parseTag(`doc:x#${fieldKey}`)) {
987
- return { ok: false, problem: { attribute: "data-zap-field", value: clip(field) } };
1064
+ return { ok: false, problem: { attribute, value: clip(field) } };
988
1065
  }
989
1066
  const container = element.closest("[data-zap-entry]");
990
1067
  const entry = container?.getAttribute("data-zap-entry")?.trim();
991
1068
  if (!entry || !parseRecordRef(entry)) {
992
1069
  return { ok: false, problem: { attribute: "data-zap-entry", value: clip(entry ?? "") } };
993
1070
  }
994
- return { ok: true, tagged: build(element, entry, fieldKey, locale, html) };
1071
+ return { ok: true, tagged: build(element, entry, fieldKey, locale, html, siteRenders) };
995
1072
  }
996
- function build(element, recordRef, fieldKey, locale, html) {
1073
+ function build(element, recordRef, fieldKey, locale, html, siteRenders = false) {
997
1074
  return {
998
1075
  element,
999
1076
  recordRef,
@@ -1001,7 +1078,8 @@ function build(element, recordRef, fieldKey, locale, html) {
1001
1078
  tag: `${recordRef}#${fieldKey}`,
1002
1079
  locale,
1003
1080
  html,
1004
- source: "attr"
1081
+ source: "attr",
1082
+ ...siteRenders ? { siteRenders: true } : {}
1005
1083
  };
1006
1084
  }
1007
1085
  var NON_RENDERED = /^(?:head|title|script|style|template|noscript|textarea)$/;
@@ -1053,6 +1131,7 @@ var TagIndex = class {
1053
1131
  if (!element || !hasStega(text.data)) continue;
1054
1132
  if (NON_RENDERED.test(element.localName) || element.closest("head")) continue;
1055
1133
  if (byElement.get(element)?.source === "attr") continue;
1134
+ if (element.closest(SITE_RENDERS_SELECTOR)) continue;
1056
1135
  for (const found of stegaDecode(text.data)) {
1057
1136
  add(element, {
1058
1137
  element,
@@ -1145,7 +1224,13 @@ function observeTags(target, onChange, options = {}) {
1145
1224
  subtree: true,
1146
1225
  childList: true,
1147
1226
  attributes: true,
1148
- attributeFilter: ["data-zap", "data-zap-field", "data-zap-entry", "data-zap-locale"],
1227
+ attributeFilter: [
1228
+ "data-zap",
1229
+ "data-zap-field",
1230
+ "data-zap-entry",
1231
+ "data-zap-locale",
1232
+ "data-zap-site-renders"
1233
+ ],
1149
1234
  characterData: true
1150
1235
  });
1151
1236
  return () => {
@@ -1184,6 +1269,9 @@ export {
1184
1269
  MAX_TAG_REPORT,
1185
1270
  MAX_LABELS,
1186
1271
  MAX_PINS,
1272
+ MAX_MARKS,
1273
+ MAX_MARK_LABEL,
1274
+ MAX_ANCHOR_NTH,
1187
1275
  OVERLAY_MODES,
1188
1276
  CAPABILITIES,
1189
1277
  MAX_EMAIL_LENGTH,
@@ -1194,6 +1282,7 @@ export {
1194
1282
  isDraftRoutePath,
1195
1283
  isPinId,
1196
1284
  isPin,
1285
+ isDiffMark,
1197
1286
  isPreviewValue,
1198
1287
  utf8Length,
1199
1288
  isDomAnchor,
@@ -1227,4 +1316,4 @@ export {
1227
1316
  DEFAULT_SHORTCUT,
1228
1317
  isShortcut
1229
1318
  };
1230
- //# sourceMappingURL=chunk-T6S3S5RH.js.map
1319
+ //# sourceMappingURL=chunk-G2JAAZKS.js.map