@8ux-co/eelzap 0.10.1 → 0.11.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/CHANGELOG.md CHANGED
@@ -5,7 +5,64 @@ 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.10.1] - Unreleased
8
+ ## [0.11.0] - Unreleased
9
+
10
+ ### Added
11
+
12
+ - 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:
13
+ - `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.
14
+ - `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.
15
+ - The editor bridge (`@8ux-co/eelzap/internal/editor`) adds `setScrollSync`, `scrollTo`, `setMarks` and the `onScroll` handler.
16
+ - Opt-in fragment transport for preview tokens (#947). The client advertises
17
+ `fragment-token` to the editor and sends `tokenTransport: 'fragment'` to the
18
+ live-site session API. Without either opt-in, legacy query URLs remain the
19
+ default, so sites on SDK 0.10.2 and earlier keep working, including the editor's
20
+ first load before the client announces its capabilities.
21
+ - `createDraftModeRoute` exchanges fragment tokens through a same-origin POST
22
+ body and redirects with `303` to a validated clean path. Its exchange page
23
+ loads an external script from the same route (`?script=1`), supporting
24
+ `script-src 'self'`. POST requires `Sec-Fetch-Site: same-origin` or an `Origin`
25
+ equal to the request's own origin; otherwise it returns `403` without cookies.
26
+
27
+ ### Changed
28
+
29
+ - `zap:ready` reports client version `0.11.0`.
30
+ - The 0.11 client renews live-site draft sessions with a same-origin POST to the
31
+ site's draft route.
32
+
33
+ ### Migration
34
+
35
+ - **Upgrading to 0.11 requires the site's draft route to export `POST` as well
36
+ as `GET`**, for example `export const POST = createDraftModeRoute({ … })` and
37
+ `export const GET = POST` (see the README), or `export const { GET, POST } = …`
38
+ in an equivalent wrapper. A route that exports only `GET` answers `405` to the
39
+ new client and editor preview breaks. Upgrade the route handler and the
40
+ browser client together. Sites on the script-tag (CDN boot) snippet that adopt
41
+ the new snippet need the same `GET` and `POST` route.
42
+ - Legacy `GET ?token=…` remains supported for older clients; its query tokens
43
+ can still appear in site request logs. Only fragment exchanges keep tokens out
44
+ of request URLs.
45
+ - Deploy order: Zap must ship to production (the session API's
46
+ `tokenTransport: 'fragment'` and the 0.11.0 CDN bundles) before 0.11.0 is
47
+ published to npm, since the published boot pins those bundles by path and SRI.
48
+
49
+ ## [0.10.2] - Unreleased
50
+
51
+ ### Added
52
+
53
+ - Live-site «Editar» edits every simple field type on the page: options (ENUM), numbers, integers and amounts, dates and date-times, and yes/no. Each opens a small editor under the element; Enter saves, Esc cancels.
54
+ - Fields edited only in Zap (rich text, images, galleries, video, files) show «Este campo se edita en Zap» with «Abrir en Zap» at that field and «Comentar».
55
+
56
+ ### Changed
57
+
58
+ - Editors and cards keep clear of the bar wherever it sits; key caps match the suite's `Kbd`.
59
+ - A tab whose draft session was lost re-enters it once before reporting no editable fields.
60
+
61
+ ### Fixed
62
+
63
+ - Amounts use ISO 4217 minor units (COP in hundredths), not the locale's display digits.
64
+
65
+ ## [0.10.1] - 2026-10-07
9
66
 
10
67
  ### Added
11
68
 
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
 
@@ -404,22 +404,49 @@ export default async function RootLayout({ children }: { children: React.ReactNo
404
404
  import { cookies, draftMode } from 'next/headers'
405
405
  import { createDraftModeRoute } from '@8ux-co/eelzap/next'
406
406
 
407
- export const GET = createDraftModeRoute({
407
+ export const POST = createDraftModeRoute({
408
408
  siteKey: 'your-site-key',
409
409
  apiKey: process.env.EELZAP_API_KEY!, // one of the site's own API keys
410
410
  draftMode,
411
411
  cookies,
412
412
  })
413
+ export const GET = POST
413
414
  ```
414
415
 
415
416
  `<ZapPreview />` tells the editor where the route lives (`/api/zap-preview`
416
417
  by default; `draftRoute="/es/preview"` or `draftRoute={null}` otherwise).
417
418
  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.
419
+ origin and mints a ten-minute preview token. The editor uses the legacy
420
+ `{origin}{route}?token=…&path=…` unless the site's client has announced the
421
+ `fragment-token` capability. The new client advertises that capability; the
422
+ editor then uses `{origin}{route}#token=…&path=…`. Before the first ready/hello
423
+ message, it keeps the query form, so older clients keep working.
424
+
425
+ URL fragments never reach the HTTP server or its request logs. The route's
426
+ tiny exchange page loads its script from the same route (`?script=1`,
427
+ `application/javascript`), so `script-src 'self'` works without an inline
428
+ script. The script clears the fragment
429
+ from browser history, then submits `token` and `path` in an
430
+ `application/x-www-form-urlencoded` POST body to the same clean route URL.
431
+ POST requires `Sec-Fetch-Site: same-origin` or an `Origin` header equal to the
432
+ request's own origin. Other POSTs, including those missing both headers,
433
+ return `403` without setting cookies. The route checks the token with Zap
434
+ (minted for this site, live), enables
435
+ draft mode with cookies that survive inside Zap's frame
436
+ (`SameSite=None; Secure; Partitioned`) and answers `303` to the clean `path`.
437
+ The target must be a same-origin relative path beginning with a single `/`;
438
+ external URLs, protocol-relative paths, backslashes, control characters and
439
+ paths containing preview tokens are refused. `createDraftModeExitRoute({ draftMode })`
440
+ ends it.
441
+
442
+ Upgrade the site's route handler and browser client together and export both
443
+ `GET` and `POST` as above before opting into the new exchange. The new live-site
444
+ client sends `tokenTransport: 'fragment'` to `POST /api/public/v1/preview/session`
445
+ and renews drafts with a same-origin POST. Without that field, the API returns
446
+ the legacy query URL. Legacy `GET ?token=…&path=…` remains supported and keeps
447
+ older sites working; those requests can still expose tokens to URL logs.
448
+ Live-site draft-session renewal uses a POST body directly, without navigating
449
+ the tab or putting a token in a request URL.
423
450
 
424
451
  Read drafts on the server with the token as the credential:
425
452
 
@@ -90,8 +90,27 @@ var SUGGEST_MESSAGES = {
90
90
  sentRequestBody: (name, proposal) => `${name ? `${name} la ver\xE1` : "La persona responsable la ver\xE1"} en Zap${proposal ? " con tu texto propuesto" : ""}. La p\xE1gina no cambia hasta que se publique.`,
91
91
  saving: "Guardando",
92
92
  saved: "Guardado en el borrador",
93
- notEditable: "Esto no se edita aqu\xED",
94
- notEditableBody: "Usa Comentar para pedir el cambio.",
93
+ editInZap: "Este campo se edita en Zap",
94
+ /**
95
+ * The field named, what it is, and where it changes («Historia es texto
96
+ * con formato. Cámbialo en el editor de la entrada.»).
97
+ */
98
+ editInZapBody: (label, type, document) => {
99
+ const where = `en el editor ${document ? "del documento" : "de la entrada"}`;
100
+ const kind = {
101
+ IMAGE: ["una imagen", "C\xE1mbiala"],
102
+ GALLERY: ["una galer\xEDa", "C\xE1mbiala"],
103
+ RICH_TEXT: ["texto con formato", "C\xE1mbialo"],
104
+ VIDEO: ["un video", "C\xE1mbialo"],
105
+ FILE: ["un archivo", "C\xE1mbialo"]
106
+ }[type];
107
+ return kind ? `${label} es ${kind[0]}. ${kind[1]} ${where}.` : `${label} se cambia ${where}.`;
108
+ },
109
+ enterSaves: "guarda",
110
+ escCancels: "cancela",
111
+ yes: "S\xED",
112
+ no: "No",
113
+ options: (label) => `Opciones de ${label}`,
95
114
  inRecord: (name) => `en ${name}`,
96
115
  noFields: "No hay campos para editar aqu\xED",
97
116
  noFieldsBody: "Esta p\xE1gina no marca sus campos. \xC1brela desde la vista previa de Zap o usa Comentar.",
@@ -162,8 +181,23 @@ var SUGGEST_MESSAGES = {
162
181
  sentRequestBody: (name, proposal) => `${name ?? "The person responsible"} will see it in Zap${proposal ? " with your proposed text" : ""}. The page does not change until it is published.`,
163
182
  saving: "Saving",
164
183
  saved: "Saved to the draft",
165
- notEditable: "This is not edited here",
166
- notEditableBody: "Use Comment to ask for the change.",
184
+ editInZap: "This field is edited in Zap",
185
+ editInZapBody: (label, type, document) => {
186
+ const where = `in the ${document ? "document's" : "entry's"} editor`;
187
+ const kind = {
188
+ IMAGE: "an image",
189
+ GALLERY: "a gallery",
190
+ RICH_TEXT: "formatted text",
191
+ VIDEO: "a video",
192
+ FILE: "a file"
193
+ }[type];
194
+ return kind ? `${label} is ${kind}. Change it ${where}.` : `${label} is changed ${where}.`;
195
+ },
196
+ enterSaves: "saves",
197
+ escCancels: "cancels",
198
+ yes: "Yes",
199
+ no: "No",
200
+ options: (label) => `${label} options`,
167
201
  inRecord: (name) => `in ${name}`,
168
202
  noFields: "Nothing to edit here",
169
203
  noFieldsBody: "This page does not mark its fields. Open it from the Zap preview or use Comment.",
@@ -326,6 +360,8 @@ var ICONS = {
326
360
  alert: stroke('<circle cx="12" cy="12" r="10"/><path d="M12 8v4"/><path d="M12 16h.01"/>'),
327
361
  field: stroke('<path d="M4 7V4h16v3"/><path d="M9 20h6"/><path d="M12 4v16"/>'),
328
362
  chevron: stroke('<path d="m6 9 6 6 6-6"/>'),
363
+ tick: stroke('<path d="M20 6 9 17l-5-5"/>'),
364
+ info: stroke('<circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/>'),
329
365
  /** Lucide `eye-off`, the suite's «Ocultar». */
330
366
  eyeOff: stroke(
331
367
  '<path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49"/><path d="M14.084 14.158a3 3 0 0 1-4.242-4.242"/><path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143"/><path d="m2 2 20 20"/>'
@@ -402,8 +438,25 @@ var TOKENS = {
402
438
  };
403
439
  var T = TOKENS;
404
440
  var FONT = "Poppins, ui-sans-serif, system-ui, sans-serif";
441
+ function kbd(doc, ...keys) {
442
+ return keys.length === 1 ? el(doc, "kbd", { text: keys[0] }) : el(
443
+ doc,
444
+ "span",
445
+ { class: "kbd-group" },
446
+ // A space between caps: invisible in the flex group, but the text
447
+ // (and a screen reader) reads «Shift Z», not «ShiftZ».
448
+ keys.flatMap((key, i) => [
449
+ ...i ? [doc.createTextNode(" ")] : [],
450
+ el(doc, "kbd", { text: key })
451
+ ])
452
+ );
453
+ }
405
454
  var BASE_CSS = `
406
455
  :host { all: initial; }
456
+ kbd { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; height: 20px; min-width: 20px;
457
+ padding: 0 6px; border: 1px solid ${T.border}; border-radius: 4px; background: ${T.bg}; color: ${T.muted};
458
+ font: 500 11px/12px ${FONT}; user-select: none; -webkit-user-select: none; pointer-events: none; }
459
+ .kbd-group { display: inline-flex; align-items: center; gap: 2px; }
407
460
  * { box-sizing: border-box; }
408
461
  .layer { position: fixed; inset: 0; pointer-events: none; font-family: ${FONT}; color: ${T.fg};
409
462
  font-size: 13px; line-height: 1.45; -webkit-font-smoothing: antialiased; }
@@ -447,6 +500,7 @@ export {
447
500
  el,
448
501
  mountHost,
449
502
  initials,
450
- TOKENS
503
+ TOKENS,
504
+ kbd
451
505
  };
452
- //# sourceMappingURL=chunk-7ZPXZZUT.js.map
506
+ //# sourceMappingURL=chunk-4ET7VBJA.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/preview/site/messages.ts","../src/preview/site/oauth.ts","../src/preview/site/ui.ts"],"sourcesContent":["/**\n * The site tools' own words («Editar» and «Comentar», zap-cms-v2 §3.4), Spanish and English (Spanish by default, as\n * the suite; `<html lang=\"en…\">` or `initZap({ locale: 'en' })` picks\n * English). Two objects, one per chunk, so each chunk carries only its own\n * copy. Plain strings and functions: the core has no i18n runtime, and these\n * never reach the server. Copy rules (`.docs/ui-patterns/copy.md`): no\n * ellipses, em dashes, middots or parentheses.\n */\n\ntype Locale = 'es' | 'en'\n\nconst plural = (count: number, one: string, other: string) =>\n (count === 1 ? one : other).replace('{n}', String(count))\n\nexport const SIGNIN_MESSAGES = {\n es: {\n launcher: 'Editar o comentar',\n hintBefore: 'Pulsa',\n hintAfter: 'para editar o comentar',\n title: 'Edita o comenta esta página',\n body: 'Escribe sobre el texto de la página o deja un comentario. Lo que edites queda en el borrador y se publica desde Zap.',\n signIn: 'Entrar con Eel',\n note: 'Solo para personas con Zap en este sitio. Los visitantes no ven nada de esto.',\n waiting: 'Esperando a Eel',\n waitingHelp: 'Termina en la ventana de Eel. Si la cerraste, vuelve a intentarlo.',\n retry: 'Intentar de nuevo',\n close: 'Cerrar',\n expired: 'Tu sesión terminó. Entra de nuevo para seguir.',\n failed: 'No pudimos completar la entrada. Intenta de nuevo.',\n },\n en: {\n launcher: 'Edit or comment',\n hintBefore: 'Press',\n hintAfter: 'to edit or comment',\n title: 'Edit or comment on this page',\n body: 'Type over the text on the page or leave a comment. What you edit goes to the draft and is published from Zap.',\n signIn: 'Sign in with Eel',\n note: 'Only for people with Zap on this site. Visitors see none of this.',\n waiting: 'Waiting for Eel',\n waitingHelp: 'Finish in the Eel window. If you closed it, try again.',\n retry: 'Try again',\n close: 'Close',\n expired: 'Your session ended. Sign in again to keep going.',\n failed: 'We could not finish signing you in. Try again.',\n },\n} satisfies Record<Locale, Record<string, string>>\n\nexport const SUGGEST_MESSAGES = {\n es: {\n toolbar: 'Zap en esta página',\n move: 'Mover la barra',\n moveTip: 'Arrastra para mover la barra',\n tools: 'Herramienta',\n navigate: 'Navegar',\n edit: 'Editar',\n comment: 'Comentar',\n editOff: 'Desactivado por un administrador',\n open: (n: number) => plural(n, '{n} abierto', '{n} abiertos'),\n openTitle: 'Comentarios abiertos en esta página',\n noneOpen: 'No hay comentarios abiertos en esta página.',\n changeRequest: 'Solicitud de cambio',\n exit: 'Salir',\n account: (name: string) => `${name}, cuenta`,\n accountMenu: 'Cuenta',\n openInZap: 'Abrir en Zap',\n signOut: 'Cerrar sesión en Zap',\n hide: 'Ocultar barra',\n show: 'Mostrar barra',\n showBar: (n: number) =>\n plural(n, 'Mostrar la barra de Zap, {n} abierto', 'Mostrar la barra de Zap, {n} abiertos'),\n paused: 'Vista previa pausada',\n pausedRunaway: 'La página no dejaba de crecer, así que dejamos de actualizarla.',\n pausedCap: 'La página tiene demasiados campos para seguirlos.',\n reload: 'Recargar',\n newComment: 'Nuevo comentario',\n commentTitle: 'Comentario',\n selected: (n: number) => plural(n, '{n} elemento seleccionado', '{n} elementos seleccionados'),\n clearSelection: 'Cancelar',\n commentLabel: 'Comentario',\n commentPlaceholder: 'Escribe tu comentario',\n requestChange: 'Solicitar cambio',\n requestHelp: 'Queda abierta hasta que alguien la resuelva o aplique el texto propuesto.',\n recipient: 'La recibe la persona responsable en Zap.',\n propose: 'Proponer texto',\n proposalFor: 'Texto propuesto para',\n removeProposal: 'Quitar texto propuesto',\n now: 'Ahora:',\n proposal: 'Texto propuesto',\n cancel: 'Cancelar',\n close: 'Cerrar',\n sending: 'Enviando',\n save: 'Guardar',\n sentComment: 'Comentario enviado',\n sentCommentBody: 'Tu equipo lo verá en Zap.',\n sentRequest: 'Solicitud de cambio enviada',\n sentRequestBody: (name: string | null, proposal: boolean) =>\n `${name ? `${name} la verá` : 'La persona responsable la verá'} en Zap${\n proposal ? ' con tu texto propuesto' : ''\n }. La página no cambia hasta que se publique.`,\n saving: 'Guardando',\n saved: 'Guardado en el borrador',\n editInZap: 'Este campo se edita en Zap',\n /**\n * The field named, what it is, and where it changes («Historia es texto\n * con formato. Cámbialo en el editor de la entrada.»).\n */\n editInZapBody: (label: string, type: string, document: boolean) => {\n const where = `en el editor ${document ? 'del documento' : 'de la entrada'}`\n const kind = (\n {\n IMAGE: ['una imagen', 'Cámbiala'],\n GALLERY: ['una galería', 'Cámbiala'],\n RICH_TEXT: ['texto con formato', 'Cámbialo'],\n VIDEO: ['un video', 'Cámbialo'],\n FILE: ['un archivo', 'Cámbialo'],\n } as Record<string, [string, string]>\n )[type]\n return kind ? `${label} es ${kind[0]}. ${kind[1]} ${where}.` : `${label} se cambia ${where}.`\n },\n enterSaves: 'guarda',\n escCancels: 'cancela',\n yes: 'Sí',\n no: 'No',\n options: (label: string) => `Opciones de ${label}`,\n inRecord: (name: string) => `en ${name}`,\n noFields: 'No hay campos para editar aquí',\n noFieldsBody:\n 'Esta página no marca sus campos. Ábrela desde la vista previa de Zap o usa Comentar.',\n editOffBody: 'Un administrador desactivó Editar en este sitio.',\n renewed: 'Sesión renovada',\n renewedAs: (name: string) => `Sigues como ${name}`,\n renewedBody: 'Sigues donde ibas.',\n dismiss: 'Cerrar aviso',\n someone: 'Alguien',\n refusedTitle: 'No puedes editar ni comentar en este sitio',\n refusedBody: 'Tu cuenta no tiene acceso a Zap aquí, o el sitio dejó de permitirlo.',\n rateLimited: (n: number) =>\n plural(\n n,\n 'Enviaste muchos seguidos. Intenta de nuevo en {n} segundo.',\n 'Enviaste muchos seguidos. Intenta de nuevo en {n} segundos.',\n ),\n failed: 'No se pudo enviar. Revisa el texto e intenta de nuevo.',\n saveFailed: 'No se pudo guardar. El texto volvió a como estaba.',\n noRecord: 'Esta parte de la página no es contenido de Zap.',\n invalidNumber: 'Escribe un número.',\n },\n en: {\n toolbar: 'Zap on this page',\n move: 'Move the bar',\n moveTip: 'Drag to move the bar',\n tools: 'Tool',\n navigate: 'Browse',\n edit: 'Edit',\n comment: 'Comment',\n editOff: 'Turned off by an administrator',\n open: (n: number) => plural(n, '{n} open', '{n} open'),\n openTitle: 'Open comments on this page',\n noneOpen: 'No open comments on this page.',\n changeRequest: 'Change request',\n exit: 'Sign out',\n account: (name: string) => `${name}, account`,\n accountMenu: 'Account',\n openInZap: 'Open in Zap',\n signOut: 'Sign out of Zap',\n hide: 'Hide the bar',\n show: 'Show the bar',\n showBar: (n: number) => plural(n, 'Show the Zap bar, {n} open', 'Show the Zap bar, {n} open'),\n paused: 'Preview paused',\n pausedRunaway: 'The page kept growing, so we stopped updating it.',\n pausedCap: 'The page has too many fields to follow.',\n reload: 'Reload',\n newComment: 'New comment',\n commentTitle: 'Comment',\n selected: (n: number) => plural(n, '{n} element selected', '{n} elements selected'),\n clearSelection: 'Cancel',\n commentLabel: 'Comment',\n commentPlaceholder: 'Write your comment',\n requestChange: 'Request a change',\n requestHelp: 'It stays open until someone resolves it or applies the proposed text.',\n recipient: 'The person responsible in Zap receives it.',\n propose: 'Propose text',\n proposalFor: 'Proposed text for',\n removeProposal: 'Remove proposed text',\n now: 'Now:',\n proposal: 'Proposed text',\n cancel: 'Cancel',\n close: 'Close',\n sending: 'Sending',\n save: 'Save',\n sentComment: 'Comment sent',\n sentCommentBody: 'Your team will see it in Zap.',\n sentRequest: 'Change request sent',\n sentRequestBody: (name: string | null, proposal: boolean) =>\n `${name ?? 'The person responsible'} will see it in Zap${\n proposal ? ' with your proposed text' : ''\n }. The page does not change until it is published.`,\n saving: 'Saving',\n saved: 'Saved to the draft',\n editInZap: 'This field is edited in Zap',\n editInZapBody: (label: string, type: string, document: boolean) => {\n const where = `in the ${document ? \"document's\" : \"entry's\"} editor`\n const kind = (\n {\n IMAGE: 'an image',\n GALLERY: 'a gallery',\n RICH_TEXT: 'formatted text',\n VIDEO: 'a video',\n FILE: 'a file',\n } as Record<string, string>\n )[type]\n return kind ? `${label} is ${kind}. Change it ${where}.` : `${label} is changed ${where}.`\n },\n enterSaves: 'saves',\n escCancels: 'cancels',\n yes: 'Yes',\n no: 'No',\n options: (label: string) => `${label} options`,\n inRecord: (name: string) => `in ${name}`,\n noFields: 'Nothing to edit here',\n noFieldsBody:\n 'This page does not mark its fields. Open it from the Zap preview or use Comment.',\n editOffBody: 'An administrator turned off Edit for this site.',\n renewed: 'Session renewed',\n renewedAs: (name: string) => `You are still ${name}`,\n renewedBody: 'Pick up where you left off.',\n dismiss: 'Dismiss',\n someone: 'Someone',\n refusedTitle: 'You cannot edit or comment on this site',\n refusedBody: 'Your account has no Zap access here, or the site stopped allowing it.',\n rateLimited: (n: number) =>\n plural(\n n,\n 'You sent many in a row. Try again in {n} second.',\n 'You sent many in a row. Try again in {n} seconds.',\n ),\n failed: 'Could not send. Check the text and try again.',\n saveFailed: 'Could not save. The text is back to what it was.',\n noRecord: 'This part of the page is not Zap content.',\n invalidNumber: 'Enter a number.',\n },\n}\n\nexport type SuggestMessages = (typeof SUGGEST_MESSAGES)['es']\nexport type SigninMessages = (typeof SIGNIN_MESSAGES)['es']\n","import {\n PENDING_STORAGE_KEY,\n SITE_CLIENT_SCOPE,\n STATE_PREFIX,\n type CallbackParams,\n type SiteContext,\n} from './config'\nimport { REFRESH_LIFETIME_MS, type SiteToken } from './token'\n\n/**\n * The site client's side of OAuth 2.1 with Nest (ADR 024, ADR 041): a public\n * client, PKCE S256, `state`, and an `iss` check (RFC 9207). Pure functions\n * plus the one `fetch`, so every refusal is a unit spec.\n *\n * - `state` is `zap1.<128 random bits>` for the popup, and\n * `zap1.<random>.<return path>` for the full-page redirect (spec §3.4 step 4:\n * \"the return path in `state`\"). The callback must bring back EXACTLY the\n * state this tab minted; the return path is honoured only as a same-origin\n * path.\n * - `iss` must equal the compiled-in issuer exactly.\n * - The code is exchanged once, with the verifier only this tab holds, as a\n * form POST with no custom header (no preflight; Nest answers CORS to the\n * client's own redirect origins only) and no cookies.\n * - Nest answers an hour of access and, since ADR 041's amendment of\n * 2026-10-06, a short rotating refresh token (eight hours, redeemable only\n * inside the remembered consent and from this origin). Both are kept in\n * `sessionStorage` only (`token.ts`); `session.ts` spends the refresh.\n */\n\nexport interface PendingSignIn {\n state: string\n verifier: string\n mode: 'popup' | 'redirect'\n}\n\nexport type CallbackProblem = 'state' | 'iss' | 'code'\n\nfunction base64url(bytes: Uint8Array): string {\n let binary = ''\n for (const byte of bytes) binary += String.fromCharCode(byte)\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\n}\n\nfunction randomToken(win: Window, size: number): string {\n return base64url(win.crypto.getRandomValues(new Uint8Array(size)))\n}\n\n/** A 43-character verifier (32 random bytes), RFC 7636 §4.1. */\nexport function createVerifier(win: Window): string {\n return randomToken(win, 32)\n}\n\n/** `BASE64URL(SHA256(verifier))`, RFC 7636 §4.2. */\nexport async function challengeFor(win: Window, verifier: string): Promise<string> {\n const digest = await win.crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier))\n return base64url(new Uint8Array(digest))\n}\n\n/** A same-origin path: one leading slash, no scheme, no `//host`, no backslash tricks. */\nexport function isReturnPath(value: string): boolean {\n return /^\\/(?![/\\\\])/.test(value) && !/[\\\\\\s]/.test(value) && value.length <= 2048\n}\n\nexport function createState(win: Window, returnPath?: string): string {\n const nonce = randomToken(win, 16)\n if (returnPath === undefined || !isReturnPath(returnPath)) return `${STATE_PREFIX}${nonce}`\n return `${STATE_PREFIX}${nonce}.${base64url(new TextEncoder().encode(returnPath))}`\n}\n\n/** The return path a redirect `state` carries, or null. */\nexport function returnPathOf(state: string): string | null {\n const encoded = state.slice(STATE_PREFIX.length).split('.')[1]\n if (!encoded) return null\n try {\n const binary = atob(encoded.replace(/-/g, '+').replace(/_/g, '/'))\n const path = new TextDecoder().decode(Uint8Array.from(binary, (c) => c.charCodeAt(0)))\n return isReturnPath(path) ? path : null\n } catch {\n return null\n }\n}\n\nexport function authorizeUrl(\n ctx: Pick<SiteContext, 'authOrigin' | 'clientId' | 'redirectUri'>,\n state: string,\n challenge: string,\n): string {\n const url = new URL('/oauth/authorize', ctx.authOrigin)\n url.search = new URLSearchParams({\n response_type: 'code',\n client_id: ctx.clientId,\n redirect_uri: ctx.redirectUri,\n code_challenge: challenge,\n code_challenge_method: 'S256',\n state,\n scope: SITE_CLIENT_SCOPE,\n }).toString()\n return url.toString()\n}\n\n/**\n * Null when the callback answers `pending`; otherwise what is wrong with it.\n * Exact comparisons only: a callback for another sign-in (another tab, an\n * attacker's code pushed at us) has another state; a code from another\n * issuer has another `iss`.\n */\nexport function checkCallback(\n callback: Partial<CallbackParams> | null | undefined,\n pending: PendingSignIn | null,\n authOrigin: string,\n): CallbackProblem | null {\n if (!pending || !callback || callback.state !== pending.state) return 'state'\n if (callback.iss !== authOrigin) return 'iss'\n if (typeof callback.code !== 'string' || !callback.code || callback.code.length > 512) {\n return 'code'\n }\n return null\n}\n\nexport class ExchangeError extends Error {\n constructor(readonly status: number) {\n super(`token exchange failed (${status})`)\n this.name = 'ExchangeError'\n }\n}\n\n/** Trade the code for the access token (and its refresh token). Never sends cookies. */\nexport async function exchangeCode(\n ctx: Pick<SiteContext, 'win' | 'authOrigin' | 'clientId' | 'redirectUri' | 'siteId'>,\n code: string,\n verifier: string,\n now = Date.now(),\n): Promise<SiteToken> {\n const response = await ctx.win.fetch(`${ctx.authOrigin}/api/oauth/token`, {\n method: 'POST',\n credentials: 'omit',\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n code,\n client_id: ctx.clientId,\n redirect_uri: ctx.redirectUri,\n code_verifier: verifier,\n }),\n })\n if (!response.ok) throw new ExchangeError(response.status)\n return readTokenAnswer(response, ctx.siteId, now)\n}\n\n/**\n * The token endpoint's answer, checked: an `eel_at_` token and its lifetime\n * (at most the hour the ADR promises, whatever the answer claims), and the\n * `eel_rt_` refresh token when there is one (eight hours at most).\n */\nexport async function readTokenAnswer(\n response: Response,\n siteId: string,\n now = Date.now(),\n): Promise<SiteToken> {\n const body = (await response.json().catch(() => null)) as {\n access_token?: unknown\n expires_in?: unknown\n refresh_token?: unknown\n } | null\n const token = body?.access_token\n const expiresIn = body?.expires_in\n if (\n typeof token !== 'string' ||\n !/^eel_at_[A-Za-z0-9_-]{8,200}$/.test(token) ||\n typeof expiresIn !== 'number' ||\n !(expiresIn > 0)\n ) {\n throw new ExchangeError(502)\n }\n const refresh = body?.refresh_token\n const withRefresh =\n typeof refresh === 'string' && /^eel_rt_[A-Za-z0-9_-]{8,200}$/.test(refresh)\n ? { refresh, rexp: now + REFRESH_LIFETIME_MS }\n : {}\n return { token, exp: now + Math.min(expiresIn, 3600) * 1000, site: siteId, ...withRefresh }\n}\n\n// ── The pending full-page sign-in (sessionStorage, this tab only) ───────────\n\nexport function savePending(win: Window, pending: PendingSignIn): void {\n try {\n win.sessionStorage.setItem(PENDING_STORAGE_KEY, JSON.stringify(pending))\n } catch {\n // Blocked storage: the redirect cannot be finished; the popup path still works.\n }\n}\n\n/** Read AND forget: a pending sign-in is good for one callback. */\nexport function takePending(win: Window): PendingSignIn | null {\n try {\n const raw = win.sessionStorage.getItem(PENDING_STORAGE_KEY)\n win.sessionStorage.removeItem(PENDING_STORAGE_KEY)\n const value = raw ? (JSON.parse(raw) as Partial<PendingSignIn>) : null\n if (\n value &&\n typeof value.state === 'string' &&\n typeof value.verifier === 'string' &&\n (value.mode === 'popup' || value.mode === 'redirect')\n ) {\n return { state: value.state, verifier: value.verifier, mode: value.mode }\n }\n } catch {\n // Nothing usable.\n }\n return null\n}\n","/**\n * The site tools' building blocks, shared by the `signin` and `suggest`\n * chunks (zap-cms-v2 §2.7, §7.3):\n *\n * - **A closed Shadow DOM** on a custom element appended to `<html>`, styled\n * `all: initial` with `!important` inline, so the site's CSS cannot restyle\n * it, it cannot restyle the site, and site scripts get no `shadowRoot`.\n * - **Eel tokens inlined** (`TOKENS`): the boards' colours, radii and Poppins\n * with system fallbacks; nothing is fetched.\n * - **Text only.** `el()` takes text as text: strings become text nodes, never\n * markup. The single `innerHTML` is `icon()`, which reads a constant SVG from\n * `ICONS` by name; no network string ever reaches it.\n */\n\nexport const SITE_HOST_TAG = 'eel-zap-site'\n\nconst ZAP_BOLT =\n '<svg viewBox=\"0 0 512 512\"><g transform=\"matrix(27.611111,0,0,24.833333,-74.833333,-42)\"><path d=\"M13,2L3,14L10.036,11.906L10,22L20.964,9.235L12.913,11.973L13,2Z\" fill=\"#F1A10D\" stroke=\"#F1A10D\" stroke-width=\"0.5\"/><path d=\"M13,2L6.518,12.953L10.036,11.906L12.913,11.973L13,2Z\" fill=\"#FFC03A\"/><path d=\"M10.036,11.906L10,22L20.964,9.235L12.913,11.973L10.036,11.906Z\" fill=\"#D98A05\"/><path d=\"M10,22L20.964,9.235L16.939,10.604L10,22Z\" fill=\"#BF7500\"/></g></svg>'\n\nconst EEL_MARK =\n '<svg viewBox=\"0 0 3000 3000\" fill=\"currentColor\"><g transform=\"matrix(1.01772,-0.122618,0.122163,1.013948,-241.899218,117.348723)\"><g transform=\"matrix(1.192409,0,0,1.144788,-367.077609,-248.030951)\"><path d=\"M2789.774,1920.921C2642.929,2454.806 2170.478,2846 1611,2846C934.352,2846 385,2273.795 385,1569C385,864.205 934.352,292 1611,292C1849.726,292 2072.607,363.224 2261.109,486.366C2132.562,436.833 1991.613,409.484 1843.83,409.484C1242.576,409.484 754.435,862.18 754.435,1419.774C754.435,1977.368 1242.576,2430.064 1843.83,2430.064C2248.492,2430.064 2601.918,2225.006 2789.774,1920.921Z\"/></g><g transform=\"matrix(1.434867,0,-0,0.933341,-1232.183743,68.591241)\"><path d=\"M2196.648,369.772C2557.019,431.746 2832,753.678 2832,1141C2832,1572.598 2490.559,1923 2070,1923C1813.847,1923 1587.045,1793.01 1448.867,1593.857C1561.131,1687.675 1700.963,1743.353 1852.482,1743.353C2222.082,1743.353 2522.15,1412.061 2522.15,1004.003C2522.15,734.845 2391.596,499.086 2196.648,369.772Z\"/></g></g></svg>'\n\nconst stroke = (paths: string) =>\n `<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">${paths}</svg>`\n\n/** Constant markup only. Adding an icon means adding a literal here. */\nexport const ICONS = {\n zap: ZAP_BOLT,\n eel: EEL_MARK,\n x: stroke('<path d=\"M18 6 6 18\"/><path d=\"m6 6 12 12\"/>'),\n lock: stroke(\n '<rect width=\"18\" height=\"11\" x=\"3\" y=\"11\" rx=\"2\" ry=\"2\"/><path d=\"M7 11V7a5 5 0 0 1 10 0v4\"/>',\n ),\n pointer: stroke(\n '<path d=\"M12.586 12.586 19 19\"/><path d=\"M3.688 3.037a.497.497 0 0 0-.651.651l6.5 15.999a.501.501 0 0 0 .947-.062l1.569-6.083a2 2 0 0 1 1.448-1.479l6.124-1.579a.5.5 0 0 0 .063-.947z\"/>',\n ),\n pencil: stroke(\n '<path d=\"M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z\"/>',\n ),\n commentPlus: stroke(\n '<path d=\"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z\"/><path d=\"M12 7v6\"/><path d=\"M9 10h6\"/>',\n ),\n thread: stroke(\n '<path d=\"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z\"/><path d=\"M13 8H7\"/><path d=\"M17 12H7\"/>',\n ),\n exit: stroke(\n '<path d=\"m16 17 5-5-5-5\"/><path d=\"M21 12H9\"/><path d=\"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4\"/>',\n ),\n check: stroke('<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"m9 12 2 2 4-4\"/>'),\n alert: stroke('<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 8v4\"/><path d=\"M12 16h.01\"/>'),\n field: stroke('<path d=\"M4 7V4h16v3\"/><path d=\"M9 20h6\"/><path d=\"M12 4v16\"/>'),\n chevron: stroke('<path d=\"m6 9 6 6 6-6\"/>'),\n tick: stroke('<path d=\"M20 6 9 17l-5-5\"/>'),\n info: stroke('<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 16v-4\"/><path d=\"M12 8h.01\"/>'),\n /** Lucide `eye-off`, the suite's «Ocultar». */\n eyeOff: stroke(\n '<path d=\"M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49\"/><path d=\"M14.084 14.158a3 3 0 0 1-4.242-4.242\"/><path d=\"M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143\"/><path d=\"m2 2 20 20\"/>',\n ),\n external: stroke(\n '<path d=\"M15 3h6v6\"/><path d=\"M10 14 21 3\"/><path d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\"/>',\n ),\n grip: stroke(\n '<circle cx=\"9\" cy=\"12\" r=\"1\"/><circle cx=\"9\" cy=\"5\" r=\"1\"/><circle cx=\"9\" cy=\"19\" r=\"1\"/><circle cx=\"15\" cy=\"12\" r=\"1\"/><circle cx=\"15\" cy=\"5\" r=\"1\"/><circle cx=\"15\" cy=\"19\" r=\"1\"/>',\n ),\n} as const\n\nexport type IconName = keyof typeof ICONS\n\nexport function icon(doc: Document, name: IconName, size = 15): HTMLElement {\n const span = doc.createElement('span')\n span.className = 'icon'\n span.style.width = `${size}px`\n span.style.height = `${size}px`\n span.setAttribute('aria-hidden', 'true')\n // A constant from ICONS, never network data (§7.3).\n span.innerHTML = ICONS[name]\n return span\n}\n\ntype Child = Node | string | number | null | undefined | false\n\nexport interface ElProps {\n class?: string\n attrs?: Record<string, string | null | undefined>\n text?: string\n on?: Partial<Record<string, (event: Event) => void>>\n}\n\n/** An element whose children are nodes or TEXT. Strings are never parsed as markup. */\nexport function el<K extends keyof HTMLElementTagNameMap>(\n doc: Document,\n tag: K,\n props: ElProps = {},\n children: Child[] = [],\n): HTMLElementTagNameMap[K] {\n const node = doc.createElement(tag)\n if (props.class) node.className = props.class\n for (const [name, value] of Object.entries(props.attrs ?? {})) {\n if (value !== null && value !== undefined) node.setAttribute(name, value)\n }\n if (props.text !== undefined) node.textContent = props.text\n for (const [type, handler] of Object.entries(props.on ?? {})) {\n if (handler) node.addEventListener(type, handler)\n }\n for (const child of children) {\n if (child === null || child === undefined || child === false) continue\n node.append(typeof child === 'string' || typeof child === 'number' ? String(child) : child)\n }\n return node\n}\n\nexport interface SiteHost {\n host: HTMLElement\n root: ShadowRoot\n destroy(): void\n}\n\n/** The closed shadow host, on `<html>` so a site that replaces `<body>` keeps it. */\nexport function mountHost(doc: Document, css: string): SiteHost {\n const host = doc.createElement(SITE_HOST_TAG)\n const style: Record<string, string> = {\n all: 'initial',\n position: 'fixed',\n inset: '0',\n 'pointer-events': 'none',\n 'z-index': '2147483647',\n display: 'block',\n }\n for (const [name, value] of Object.entries(style))\n host.style.setProperty(name, value, 'important')\n const root = host.attachShadow({ mode: 'closed' })\n const sheet = doc.createElement('style')\n sheet.textContent = BASE_CSS + css\n root.append(sheet)\n doc.documentElement.appendChild(host)\n return { host, root, destroy: () => host.remove() }\n}\n\n/** Initials for an avatar: first letters of the first two words. */\nexport function initials(name: string | null | undefined): string {\n const words = (name ?? '').trim().split(/\\s+/).filter(Boolean)\n return words\n .slice(0, 2)\n .map((word) => word.charAt(0).toUpperCase())\n .join('')\n}\n\n/** Eel tokens, inlined (boards SitioLanzador to SitioRenovacion). */\nexport const TOKENS = {\n fg: '#010313',\n muted: '#5A6C85',\n border: '#E2E8F0',\n bg: '#FFFFFF',\n subtle: '#EAEEF3',\n primary: '#287CCF',\n primaryBorder: '#4B97DE',\n dark: '#0F172B',\n gold: '#9A6200',\n goldTint: '#FDF3E1',\n goldText: '#7A4D00',\n danger: '#C62828',\n focus: 'rgba(144,161,185,0.5)',\n}\n\nconst T = TOKENS\nconst FONT = 'Poppins, ui-sans-serif, system-ui, sans-serif'\n\n/**\n * A key cap («Enter», «Shift Z»), the ONE way the site tools draw a key.\n * Mirrors `Kbd` in `packages/ui-web/src/kbd.tsx` (no React in a closed\n * shadow root): 20px tall, at least 20px wide, 0 6px, 4px radius, a border,\n * the background colour, muted 11px medium text, never selectable.\n */\nexport function kbd(doc: Document, ...keys: string[]): HTMLElement {\n // Several keys pressed together are a group, as `KbdGroup` (2px apart).\n return keys.length === 1\n ? el(doc, 'kbd', { text: keys[0] })\n : el(\n doc,\n 'span',\n { class: 'kbd-group' },\n // A space between caps: invisible in the flex group, but the text\n // (and a screen reader) reads «Shift Z», not «ShiftZ».\n keys.flatMap((key, i) => [\n ...(i ? [doc.createTextNode(' ')] : []),\n el(doc, 'kbd', { text: key }),\n ]),\n )\n}\n\nconst BASE_CSS = `\n:host { all: initial; }\nkbd { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; height: 20px; min-width: 20px;\n padding: 0 6px; border: 1px solid ${T.border}; border-radius: 4px; background: ${T.bg}; color: ${T.muted};\n font: 500 11px/12px ${FONT}; user-select: none; -webkit-user-select: none; pointer-events: none; }\n.kbd-group { display: inline-flex; align-items: center; gap: 2px; }\n* { box-sizing: border-box; }\n.layer { position: fixed; inset: 0; pointer-events: none; font-family: ${FONT}; color: ${T.fg};\n font-size: 13px; line-height: 1.45; -webkit-font-smoothing: antialiased; }\n.icon { display: inline-flex; flex-shrink: 0; }\n.icon svg { width: 100%; height: 100%; display: block; }\nbutton { font: 500 13px ${FONT}; color: inherit; cursor: pointer; border: 0; background: transparent;\n display: inline-flex; align-items: center; gap: 6px; padding: 0; margin: 0; }\nbutton:focus-visible, textarea:focus-visible, input:focus-visible { outline: none; box-shadow: 0 0 0 3px ${T.focus}; }\nbutton:disabled { opacity: .55; cursor: default; }\n.card { pointer-events: auto; position: fixed; background: ${T.bg}; border: 1px solid ${T.border};\n border-radius: 15px; box-shadow: 0 20px 25px -5px rgba(15,23,42,.12), 0 8px 10px -6px rgba(15,23,42,.08); }\n.muted { color: ${T.muted}; }\nh2 { margin: 0; font: 600 15px ${FONT}; color: ${T.fg}; display: flex; align-items: center; gap: 8px; }\np { margin: 0; }\n.iconbtn { width: 28px; height: 28px; justify-content: center; border-radius: 7px; color: ${T.muted}; }\n.iconbtn:hover { background: ${T.subtle}; }\n.btn { height: 34px; padding: 0 14px 0 11px; border-radius: 9px; white-space: nowrap; flex-shrink: 0;\n box-shadow: 0 1px 2px rgba(15,23,42,.05); }\n.btn-outline { border: 1px solid ${T.border}; background: ${T.bg}; color: ${T.fg}; }\n.btn-primary { border: 1px solid ${T.primaryBorder}; background: ${T.primary}; color: #FAFAFA; }\n.btn-dark { width: 100%; height: 38px; justify-content: center; gap: 8px; border-radius: 9px;\n background: ${T.dark}; color: #FFFFFF; }\n.error { color: ${T.danger}; font-size: 12px; }\n@media (max-width: 480px) { .card.sheet { left: 8px !important; right: 8px !important; width: auto !important; } }\n`\n"],"mappings":"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","names":[]}
@@ -62,14 +62,21 @@ 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"
73
80
  ];
74
81
  var MAX_EMAIL_LENGTH = 320;
75
82
  function emptyDropStats() {
@@ -151,6 +158,15 @@ function isPin(value) {
151
158
  function isFieldRef(value) {
152
159
  return isObject(value) && isRecordRef(value.recordRef) && isFieldKey(value.fieldKey);
153
160
  }
161
+ function isScrollPayload(value) {
162
+ if (!isObject(value) || !isFiniteNumber(value.y, 0, 1)) return false;
163
+ if (value.x !== void 0 && !isFiniteNumber(value.x, 0, 1)) return false;
164
+ const anchor = value.anchor;
165
+ return anchor === void 0 || isFieldRef(anchor) && Number.isInteger(anchor.nth) && isFiniteNumber(anchor.nth, 0, MAX_ANCHOR_NTH) && isFiniteNumber(anchor.offset, -1e7, 1e7);
166
+ }
167
+ function isDiffMark(value) {
168
+ return isFieldRef(value) && (value.tone === "removed" || value.tone === "added") && isString(value.label, MAX_MARK_LABEL) && (value.active === void 0 || typeof value.active === "boolean");
169
+ }
154
170
  function isPreviewValue(value) {
155
171
  if (value === null || typeof value === "boolean") return true;
156
172
  if (typeof value === "string") return true;
@@ -203,7 +219,10 @@ function parseTag(value) {
203
219
  return { recordRef, fieldKey };
204
220
  }
205
221
  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)),
222
+ "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
223
+ // newer client on a site must not go dark against an editor that has not
224
+ // shipped its protocol yet (#953, 0.11.0 added two). Bounded all the same.
225
+ 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
226
  "zap:tags": (p) => Array.isArray(p) && p.length <= MAX_TAG_REPORT && p.every(
208
227
  (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
228
  ),
@@ -213,7 +232,8 @@ var PAGE_VALIDATORS = {
213
232
  "zap:error": (p) => isObject(p) && typeof p.code === "string" && ERROR_CODE_RE.test(p.code) && isOptional(p.detail, (d) => isString(d, 500)),
214
233
  "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
234
  "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
235
+ "zap:paused": (p) => isObject(p) && (p.reason === "runaway" || p.reason === "cap") && Number.isInteger(p.count) && p.count >= 0,
236
+ "zap:scroll": isScrollPayload
217
237
  };
218
238
  var EDITOR_VALIDATORS = {
219
239
  "zap:hello": (p) => {
@@ -239,7 +259,10 @@ var EDITOR_VALIDATORS = {
239
259
  "zap:zoom": (p) => isObject(p) && isZoom(p.zoom),
240
260
  "zap:refresh": isObject,
241
261
  "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)
262
+ "zap:pins": (p) => isObject(p) && Array.isArray(p.pins) && p.pins.length <= MAX_PINS && p.pins.every(isPin),
263
+ "zap:scroll-sync": (p) => isObject(p) && typeof p.enabled === "boolean",
264
+ "zap:scroll-to": isScrollPayload,
265
+ "zap:marks": (p) => isObject(p) && Array.isArray(p.marks) && p.marks.length <= MAX_MARKS && p.marks.every(isDiffMark)
243
266
  };
244
267
  var PAGE_MESSAGE_TYPES = /* @__PURE__ */ Object.keys(PAGE_VALIDATORS);
245
268
  var EDITOR_MESSAGE_TYPES = /* @__PURE__ */ Object.keys(
@@ -528,6 +551,10 @@ var DEFAULT_THEME = {
528
551
  };
529
552
  var SELECTED_TINT = "rgba(207,135,0,0.10)";
530
553
  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>';
554
+ var TONES = {
555
+ removed: ["#FB2C36", "rgba(251,44,54,0.08)", "#C10007", "\u2212"],
556
+ added: ["#00C950", "rgba(0,201,80,0.08)", "#008236", "+"]
557
+ };
531
558
  var Overlay = class {
532
559
  constructor(options) {
533
560
  __publicField(this, "options", options);
@@ -543,6 +570,7 @@ var Overlay = class {
543
570
  __publicField(this, "highlighted", []);
544
571
  __publicField(this, "selected", []);
545
572
  __publicField(this, "pins", []);
573
+ __publicField(this, "marks", []);
546
574
  __publicField(this, "pending", null);
547
575
  __publicField(this, "capture", null);
548
576
  __publicField(this, "frame", null);
@@ -669,6 +697,11 @@ var Overlay = class {
669
697
  this.pins = pins.slice(0, MAX_PINS);
670
698
  this.clearPending();
671
699
  }
700
+ /** Draw these diff marks (Comparar), in every mode; an empty list clears them. */
701
+ setMarks(marks) {
702
+ this.marks = marks.slice();
703
+ this.schedule();
704
+ }
672
705
  /** Drop the pending spot pin. */
673
706
  clearPending() {
674
707
  this.pending = null;
@@ -688,7 +721,8 @@ var Overlay = class {
688
721
  const nodes = Array.from(this.layer?.children ?? []);
689
722
  const boxes = nodes.filter((node) => node.className === "box").map((node) => ({
690
723
  kind: node.dataset.kind,
691
- label: node.querySelector(".chip")?.textContent ?? null
724
+ label: node.querySelector(".chip")?.textContent ?? null,
725
+ ...node.dataset.tone ? { tone: node.dataset.tone } : {}
692
726
  }));
693
727
  const capture = this.capture?.style.display === "block" ? this.capture : null;
694
728
  return { boxes, pins: nodes.filter((n) => n.className === "pin"), capture, zoom: this.zoom };
@@ -810,7 +844,12 @@ var Overlay = class {
810
844
  render() {
811
845
  const layer = this.layer;
812
846
  if (!layer) return;
813
- const boxes = [];
847
+ const boxes = this.marks.filter((mark) => mark.element.isConnected).map(({ element, tone, label, active }) => ({
848
+ element,
849
+ kind: "mark",
850
+ label: active ? label : null,
851
+ mark: { tone, active }
852
+ }));
814
853
  const seen = /* @__PURE__ */ new Set();
815
854
  const add = (element, kind) => {
816
855
  if (seen.has(element) || !element.isConnected) return;
@@ -894,14 +933,17 @@ var Overlay = class {
894
933
  node.className = "box";
895
934
  node.dataset.kind = box.kind;
896
935
  const offset = 3 / z;
936
+ const mark = box.mark;
937
+ const [markOutline, markTint, markChip, sign] = mark ? TONES[mark.tone] : [];
938
+ if (mark) node.dataset.tone = mark.tone;
897
939
  Object.assign(node.style, {
898
940
  left: `${rect.left - offset}px`,
899
941
  top: `${rect.top - offset}px`,
900
942
  width: `${rect.width + offset * 2}px`,
901
943
  height: `${rect.height + offset * 2}px`,
902
- outline: `${px(2)} solid ${this.theme.outline}`,
944
+ outline: mark ? mark.active ? `${px(2)} solid ${markOutline}` : `${px(1.5)} dashed ${markOutline}` : `${px(2)} solid ${this.theme.outline}`,
903
945
  borderRadius: px(3),
904
- background: box.kind === "selected" ? SELECTED_TINT : "transparent"
946
+ background: box.kind === "selected" ? SELECTED_TINT : mark?.active ? markTint : "transparent"
905
947
  });
906
948
  const tooSmall = box.kind === "selected" && box.element !== this.hover && Math.min(rect.width, rect.height) * z < CHIP_HEIGHT_PX;
907
949
  if (box.label && box.kind !== "highlight" && !tooSmall) {
@@ -917,9 +959,15 @@ var Overlay = class {
917
959
  gap: px(4),
918
960
  borderRadius: px(6),
919
961
  fontSize: px(12),
920
- background: this.theme.chip,
921
- color: this.theme.chipText
962
+ background: markChip ?? this.theme.chip,
963
+ color: mark ? "#FFFFFF" : this.theme.chipText
922
964
  });
965
+ if (sign) {
966
+ const glyph = doc.createElement("span");
967
+ glyph.className = "sign";
968
+ glyph.textContent = sign;
969
+ chip.appendChild(glyph);
970
+ }
923
971
  if (box.kind === "selected") {
924
972
  const icon = doc.createElement("span");
925
973
  icon.className = "icon";
@@ -956,9 +1004,10 @@ var STYLES = `
956
1004
  .chip {
957
1005
  position: absolute; display: inline-flex; align-items: center; white-space: nowrap;
958
1006
  font-family: Poppins, ui-sans-serif, system-ui, sans-serif; font-weight: 600; line-height: 1;
959
- letter-spacing: 0; box-sizing: border-box;
1007
+ letter-spacing: 0; text-transform: none; font-style: normal; box-sizing: border-box;
960
1008
  }
961
1009
  .of { font-weight: 400; opacity: .72; }
1010
+ .sign { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; }
962
1011
  .capture { position: fixed; inset: 0; pointer-events: auto; cursor: crosshair; display: none; }
963
1012
  .pin {
964
1013
  position: fixed; box-sizing: border-box; pointer-events: auto; cursor: pointer; text-align: center;
@@ -1184,6 +1233,9 @@ export {
1184
1233
  MAX_TAG_REPORT,
1185
1234
  MAX_LABELS,
1186
1235
  MAX_PINS,
1236
+ MAX_MARKS,
1237
+ MAX_MARK_LABEL,
1238
+ MAX_ANCHOR_NTH,
1187
1239
  OVERLAY_MODES,
1188
1240
  CAPABILITIES,
1189
1241
  MAX_EMAIL_LENGTH,
@@ -1194,6 +1246,7 @@ export {
1194
1246
  isDraftRoutePath,
1195
1247
  isPinId,
1196
1248
  isPin,
1249
+ isDiffMark,
1197
1250
  isPreviewValue,
1198
1251
  utf8Length,
1199
1252
  isDomAnchor,
@@ -1227,4 +1280,4 @@ export {
1227
1280
  DEFAULT_SHORTCUT,
1228
1281
  isShortcut
1229
1282
  };
1230
- //# sourceMappingURL=chunk-7CXQK2PR.js.map
1283
+ //# sourceMappingURL=chunk-72YCJCBI.js.map