@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 +59 -0
- package/README.md +52 -9
- package/dist/{chunk-T6S3S5RH.js → chunk-G2JAAZKS.js} +108 -19
- package/dist/chunk-G2JAAZKS.js.map +1 -0
- package/dist/fields.cjs +3 -0
- package/dist/fields.cjs.map +1 -1
- package/dist/fields.d.cts +22 -1
- package/dist/fields.d.ts +22 -1
- package/dist/fields.js +3 -0
- package/dist/fields.js.map +1 -1
- package/dist/index.d.cts +42 -5
- package/dist/index.d.ts +42 -5
- package/dist/next.d.ts +21 -11
- package/dist/next.js +94 -26
- package/dist/next.js.map +1 -1
- package/dist/preview.d.ts +119 -9
- package/dist/preview.js +154 -6
- package/dist/preview.js.map +1 -1
- package/dist/react.d.ts +43 -7
- package/dist/react.js +33 -4
- package/dist/react.js.map +1 -1
- package/dist/{suggest-KLS6B7AM.js → suggest-FDD4MBRP.js} +109 -43
- package/dist/suggest-FDD4MBRP.js.map +1 -0
- package/package.json +70 -33
- package/dist/chunk-T6S3S5RH.js.map +0 -1
- package/dist/suggest-KLS6B7AM.js.map +0 -1
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
|
|
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
|
|
111
|
-
|
|
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
|
|
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
|
|
419
|
-
`{origin}{route}?token=…&path
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
|
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) &&
|
|
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
|
|
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: [
|
|
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-
|
|
1319
|
+
//# sourceMappingURL=chunk-G2JAAZKS.js.map
|