@8ux-co/eelzap 0.10.0 → 0.10.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.
@@ -3,24 +3,34 @@ import {
3
3
  Overlay,
4
4
  TagIndex,
5
5
  buildAnchor,
6
+ cleanStega,
6
7
  findAnchorElement,
8
+ isDraftRoutePath,
7
9
  isRecordRef,
10
+ isShortcut,
8
11
  normalizeText,
9
12
  observeTags,
10
- parseRecordRef
11
- } from "./chunk-VW4EH7NY.js";
13
+ parseRecordRef,
14
+ reveal,
15
+ tagGuard
16
+ } from "./chunk-7CXQK2PR.js";
12
17
  import {
13
18
  SUGGEST_MESSAGES,
14
19
  TOKENS,
15
20
  el,
16
21
  icon,
17
22
  initials,
18
- mountHost
19
- } from "./chunk-2IUU53QA.js";
23
+ mountHost,
24
+ readTokenAnswer
25
+ } from "./chunk-7ZPXZZUT.js";
20
26
  import {
27
+ EXPIRY_MARGIN_MS,
28
+ accessLive,
21
29
  clearToken,
22
- readToken
23
- } from "./chunk-QN53G5P7.js";
30
+ readSession,
31
+ refreshLive,
32
+ writeToken
33
+ } from "./chunk-6XHCA4U4.js";
24
34
 
25
35
  // src/preview/site/api.ts
26
36
  var BASE = "/api/public/v1/comments";
@@ -73,6 +83,387 @@ function newIdempotencyKey(win) {
73
83
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
74
84
  }
75
85
 
86
+ // src/preview/site/drag.ts
87
+ var BAR_STORAGE_KEY = "eelzap:bar";
88
+ var GUTTER = 16;
89
+ var STEP = 16;
90
+ var BIG_STEP = 64;
91
+ var GAP = 10;
92
+ var SLOP = 3;
93
+ var DOUBLE_MS = 400;
94
+ var clamp = (value, min, max) => Math.min(Math.max(value, min), Math.max(min, max));
95
+ function storage(win) {
96
+ try {
97
+ return win.localStorage;
98
+ } catch {
99
+ return null;
100
+ }
101
+ }
102
+ function read(win) {
103
+ try {
104
+ const value = JSON.parse(storage(win)?.getItem(BAR_STORAGE_KEY) ?? "null");
105
+ return {
106
+ pos: value && Number.isFinite(value.x) && Number.isFinite(value.y) ? { x: value.x, y: value.y } : null,
107
+ hidden: value?.hidden === true
108
+ };
109
+ } catch {
110
+ return { pos: null, hidden: false };
111
+ }
112
+ }
113
+ function write(win, point, hidden) {
114
+ try {
115
+ const store = storage(win);
116
+ if (point || hidden)
117
+ store?.setItem(BAR_STORAGE_KEY, JSON.stringify({ ...point, hidden: hidden || void 0 }));
118
+ else store?.removeItem(BAR_STORAGE_KEY);
119
+ } catch {
120
+ }
121
+ }
122
+ function barDrag(win, doc, stack, label, tip) {
123
+ const saved = read(win);
124
+ let pos = saved.pos;
125
+ let hidden = saved.hidden;
126
+ let drag = null;
127
+ let dragged = false;
128
+ let lastTap = 0;
129
+ const viewport = () => {
130
+ const root = doc.documentElement;
131
+ return [root.clientWidth || win.innerWidth, root.clientHeight || win.innerHeight];
132
+ };
133
+ const bar = () => stack.querySelector(".toolbar, .pill");
134
+ const rectOf = (node) => {
135
+ const rect = node.getBoundingClientRect();
136
+ return { x: rect.left, y: rect.top };
137
+ };
138
+ function place() {
139
+ const toolbar = bar();
140
+ const cards = Array.from(stack.children);
141
+ const [vw, vh] = viewport();
142
+ if (!pos || !toolbar) {
143
+ stack.removeAttribute("style");
144
+ stack.removeAttribute("data-below");
145
+ for (const card of cards) {
146
+ card.style.marginLeft = "";
147
+ card.style.maxHeight = "";
148
+ }
149
+ placeMenu(toolbar, vw);
150
+ return;
151
+ }
152
+ const w = toolbar.offsetWidth;
153
+ const h = toolbar.offsetHeight;
154
+ const x = clamp(pos.x, GUTTER, vw - w - GUTTER);
155
+ const y = clamp(pos.y, GUTTER, vh - h - GUTTER);
156
+ const below = vh - y - h > y;
157
+ const room = (below ? vh - y - h : y) - GAP - GUTTER;
158
+ Object.assign(stack.style, {
159
+ padding: "0",
160
+ alignItems: "flex-start",
161
+ flexDirection: below ? "column-reverse" : "column",
162
+ top: below ? `${y}px` : "auto",
163
+ bottom: below ? "auto" : `${vh - y - h}px`
164
+ });
165
+ stack.toggleAttribute("data-below", below);
166
+ for (const card of cards) {
167
+ if (card.classList.contains("toast") || card.classList.contains("menu")) continue;
168
+ if (card.classList.contains("list")) card.style.maxHeight = `min(50vh, ${room}px)`;
169
+ const cw = card.offsetWidth;
170
+ card.style.marginLeft = `${card === toolbar ? x : clamp(x + (w - cw) / 2, GUTTER / 2, vw - cw - GUTTER / 2)}px`;
171
+ }
172
+ placeMenu(toolbar, vw);
173
+ }
174
+ function placeMenu(toolbar, vw) {
175
+ const menu = stack.querySelector(".menu");
176
+ if (!menu || !toolbar) return;
177
+ const left = stack.getBoundingClientRect().left + (parseFloat(win.getComputedStyle(stack).paddingLeft) || 0);
178
+ const mw = menu.offsetWidth;
179
+ menu.style.alignSelf = "flex-start";
180
+ menu.style.marginLeft = `${clamp(toolbar.getBoundingClientRect().right - 35 - mw, GUTTER / 2, vw - mw - GUTTER / 2) - left}px`;
181
+ }
182
+ function moveTo(point, save) {
183
+ pos = point;
184
+ place();
185
+ const toolbar = bar();
186
+ if (toolbar) pos = rectOf(toolbar);
187
+ if (save) write(win, pos, hidden);
188
+ }
189
+ function reset() {
190
+ pos = null;
191
+ write(win, null, hidden);
192
+ place();
193
+ }
194
+ const onMove = (event) => {
195
+ if (!drag || event.pointerId !== drag.id) return;
196
+ const dx = event.clientX - drag.px;
197
+ const dy = event.clientY - drag.py;
198
+ if (!drag.moved && Math.hypot(dx, dy) < SLOP) return;
199
+ drag.moved = true;
200
+ event.preventDefault();
201
+ pos = { x: drag.from.x + dx, y: drag.from.y + dy };
202
+ place();
203
+ };
204
+ const onUp = (event) => {
205
+ if (!drag || event.pointerId !== drag.id) return;
206
+ const { moved, node } = drag;
207
+ drag = null;
208
+ node.removeAttribute("data-dragging");
209
+ if (moved && pos) {
210
+ moveTo(pos, true);
211
+ lastTap = 0;
212
+ dragged = true;
213
+ win.setTimeout(() => dragged = false);
214
+ } else if (node === handle && event.type === "pointerup") {
215
+ const now = Date.now();
216
+ if (now - lastTap < DOUBLE_MS) {
217
+ lastTap = 0;
218
+ reset();
219
+ } else lastTap = now;
220
+ }
221
+ };
222
+ const stop = (event) => event.stopPropagation();
223
+ function begin(e, node) {
224
+ const toolbar = bar();
225
+ if (e.button !== 0 || !toolbar) return;
226
+ drag = {
227
+ id: e.pointerId,
228
+ px: e.clientX,
229
+ py: e.clientY,
230
+ from: rectOf(toolbar),
231
+ moved: false,
232
+ node
233
+ };
234
+ node.setAttribute("data-dragging", "");
235
+ try {
236
+ node.setPointerCapture(e.pointerId);
237
+ } catch {
238
+ }
239
+ }
240
+ const handle = el(doc, "button", {
241
+ class: "grip",
242
+ attrs: { type: "button", "aria-label": label, title: tip },
243
+ on: {
244
+ pointerdown: (event) => {
245
+ event.stopPropagation();
246
+ begin(event, handle);
247
+ },
248
+ keydown: (event) => {
249
+ const e = event;
250
+ const step = e.shiftKey ? BIG_STEP : STEP;
251
+ const delta = {
252
+ ArrowLeft: [-step, 0],
253
+ ArrowRight: [step, 0],
254
+ ArrowUp: [0, -step],
255
+ ArrowDown: [0, step]
256
+ };
257
+ const move = delta[e.key];
258
+ const toolbar = bar();
259
+ if (!move || !toolbar) return;
260
+ e.preventDefault();
261
+ e.stopPropagation();
262
+ const from = rectOf(toolbar);
263
+ moveTo({ x: from.x + move[0], y: from.y + move[1] }, true);
264
+ }
265
+ }
266
+ });
267
+ for (const type of [
268
+ "pointerup",
269
+ "dblclick",
270
+ "mousedown",
271
+ "mouseup",
272
+ "click",
273
+ "touchstart",
274
+ "touchend"
275
+ ]) {
276
+ handle.addEventListener(type, stop);
277
+ }
278
+ handle.append(icon(doc, "grip"));
279
+ const onResize = () => place();
280
+ win.addEventListener("pointermove", onMove, true);
281
+ win.addEventListener("pointerup", onUp, true);
282
+ win.addEventListener("pointercancel", onUp, true);
283
+ win.addEventListener("resize", onResize);
284
+ win.addEventListener("orientationchange", onResize);
285
+ return {
286
+ handle,
287
+ grab(node) {
288
+ node.addEventListener("pointerdown", (event) => begin(event, node));
289
+ node.addEventListener(
290
+ "click",
291
+ (event) => {
292
+ if (dragged) event.stopImmediatePropagation();
293
+ },
294
+ true
295
+ );
296
+ return node;
297
+ },
298
+ place,
299
+ hidden: () => hidden,
300
+ setHidden(value) {
301
+ hidden = value;
302
+ write(win, pos, hidden);
303
+ },
304
+ destroy() {
305
+ win.removeEventListener("pointermove", onMove, true);
306
+ win.removeEventListener("pointerup", onUp, true);
307
+ win.removeEventListener("pointercancel", onUp, true);
308
+ win.removeEventListener("resize", onResize);
309
+ win.removeEventListener("orientationchange", onResize);
310
+ }
311
+ };
312
+ }
313
+ var DRAG_CSS = `
314
+ .grip { width: 20px; height: 30px; justify-content: center; border-radius: 7px; color: ${TOKENS.muted};
315
+ cursor: grab; touch-action: none; user-select: none; -webkit-user-select: none; }
316
+ .grip[data-dragging], .pill[data-dragging] { cursor: grabbing; }
317
+ .pill { touch-action: none; user-select: none; -webkit-user-select: none; }
318
+ .bottom[data-below] .seg[data-tip]:hover::after { bottom: auto; top: 34px; }
319
+ `;
320
+
321
+ // src/preview/site/draft-session.ts
322
+ var DRAFT_SESSION_PATH = "/api/public/v1/preview/session";
323
+ var EDIT_INTENT_KEY = "eelzap:edit-intent";
324
+ var DRAFT_SESSION_KEY = "eelzap:draft-session";
325
+ var INTENT_MAX_AGE_MS = 2 * 6e4;
326
+ var DRAFT_SESSION_MESSAGES = {
327
+ es: {
328
+ preparing: "Preparando la edici\xF3n\u2026",
329
+ noRoute: "Este sitio no tiene la ruta de vista previa que Editar necesita. Usa Comentar o \xE1brelo desde Zap.",
330
+ failed: "No pudimos preparar la p\xE1gina para editar. Int\xE9ntalo de nuevo o usa Comentar.",
331
+ rate: (seconds) => `Demasiados intentos. Vuelve a intentarlo en ${seconds} ${seconds === 1 ? "segundo" : "segundos"}.`
332
+ },
333
+ en: {
334
+ preparing: "Getting the page ready to edit\u2026",
335
+ noRoute: "This site does not have the preview route Edit needs. Use Comment or open it from Zap.",
336
+ failed: "We could not get the page ready to edit. Try again or use Comment.",
337
+ rate: (seconds) => `Too many tries. Try again in ${seconds} ${seconds === 1 ? "second" : "seconds"}.`
338
+ }
339
+ };
340
+ function storage2(win) {
341
+ try {
342
+ return win.sessionStorage;
343
+ } catch {
344
+ return null;
345
+ }
346
+ }
347
+ function readJson(win, key) {
348
+ try {
349
+ const raw = storage2(win)?.getItem(key);
350
+ if (!raw) return null;
351
+ const value = JSON.parse(raw);
352
+ return value && typeof value === "object" ? value : null;
353
+ } catch {
354
+ return null;
355
+ }
356
+ }
357
+ function write2(win, key, value) {
358
+ try {
359
+ storage2(win)?.setItem(key, JSON.stringify(value));
360
+ } catch {
361
+ }
362
+ }
363
+ function remove(win, key) {
364
+ try {
365
+ storage2(win)?.removeItem(key);
366
+ } catch {
367
+ }
368
+ }
369
+ function currentPath(win) {
370
+ const { pathname, search, hash } = win.location;
371
+ return `${pathname}${search}${hash}`;
372
+ }
373
+ function inDraftSession(win, now = Date.now()) {
374
+ const value = readJson(win, DRAFT_SESSION_KEY);
375
+ return !!value && typeof value.exp === "number" && value.exp > now && value.origin === win.location.origin;
376
+ }
377
+ function takeEditIntent(win, now = Date.now()) {
378
+ const value = readJson(win, EDIT_INTENT_KEY);
379
+ remove(win, EDIT_INTENT_KEY);
380
+ return !!value && value.path === win.location.pathname && typeof value.at === "number" && now - value.at >= 0 && now - value.at < INTENT_MAX_AGE_MS;
381
+ }
382
+ function acceptSessionUrl(raw, win, draftRoute) {
383
+ if (typeof raw !== "string") return null;
384
+ let url;
385
+ try {
386
+ url = new URL(raw);
387
+ } catch {
388
+ return null;
389
+ }
390
+ if (url.origin !== win.location.origin || url.pathname !== draftRoute) return null;
391
+ if (!/^zpt_[A-Za-z0-9_-]{43}$/.test(url.searchParams.get("token") ?? "")) return null;
392
+ const path = url.searchParams.get("path") ?? "";
393
+ if (!path.startsWith("/") || path.startsWith("//")) return null;
394
+ return url.toString();
395
+ }
396
+ async function requestDraftSession(ctx, token, now = Date.now) {
397
+ const { win } = ctx;
398
+ const draftRoute = ctx.draftRoute;
399
+ if (!isDraftRoutePath(draftRoute)) return { ok: false, reason: "no-route" };
400
+ let response;
401
+ try {
402
+ response = await win.fetch(`${ctx.zapOrigin}${DRAFT_SESSION_PATH}`, {
403
+ method: "POST",
404
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
405
+ credentials: "omit",
406
+ body: JSON.stringify({ draftRoute, path: currentPath(win) })
407
+ });
408
+ } catch {
409
+ return { ok: false, reason: "refused" };
410
+ }
411
+ if (response.status === 401) return { ok: false, reason: "expired" };
412
+ if (response.status === 429) {
413
+ const seconds = Number.parseInt(response.headers.get("Retry-After") ?? "", 10);
414
+ return { ok: false, reason: "rate", retryAfter: seconds > 0 ? Math.min(seconds, 3600) : 30 };
415
+ }
416
+ if (!response.ok) return { ok: false, reason: "failed" };
417
+ let body;
418
+ try {
419
+ body = await response.json();
420
+ } catch {
421
+ return { ok: false, reason: "failed" };
422
+ }
423
+ const url = acceptSessionUrl(body?.url, win, draftRoute);
424
+ if (!url) return { ok: false, reason: "failed" };
425
+ const at = now();
426
+ const parsed = typeof body?.expiresAt === "string" ? Date.parse(body.expiresAt) : NaN;
427
+ const expiresAt = Number.isFinite(parsed) ? parsed : at + 10 * 6e4;
428
+ write2(win, EDIT_INTENT_KEY, { path: win.location.pathname, at });
429
+ write2(win, DRAFT_SESSION_KEY, { exp: expiresAt, origin: win.location.origin });
430
+ return { ok: true, url, expiresAt };
431
+ }
432
+ function draftExitUrl(win, draftRoute) {
433
+ const url = new URL(`${draftRoute.replace(/\/+$/, "")}/exit`, win.location.origin);
434
+ url.searchParams.set("path", currentPath(win));
435
+ return url.toString();
436
+ }
437
+ async function leaveDraftSession(ctx, token, tagged) {
438
+ const { win } = ctx;
439
+ const entered = inDraftSession(win);
440
+ remove(win, EDIT_INTENT_KEY);
441
+ remove(win, DRAFT_SESSION_KEY);
442
+ if (!entered && !tagged) return false;
443
+ try {
444
+ await win.fetch(`${ctx.zapOrigin}${DRAFT_SESSION_PATH}`, {
445
+ method: "DELETE",
446
+ headers: { Authorization: `Bearer ${token}` },
447
+ credentials: "omit"
448
+ });
449
+ } catch {
450
+ }
451
+ if (!isDraftRoutePath(ctx.draftRoute)) return false;
452
+ try {
453
+ const response = await win.fetch(draftExitUrl(win, ctx.draftRoute), {
454
+ method: "GET",
455
+ credentials: "same-origin",
456
+ redirect: "manual",
457
+ cache: "no-store"
458
+ });
459
+ if (response.type !== "opaqueredirect" && !response.ok) return false;
460
+ } catch {
461
+ return false;
462
+ }
463
+ win.location.reload();
464
+ return true;
465
+ }
466
+
76
467
  // src/preview/site/suggestion.ts
77
468
  function editKindFor(tagged, field) {
78
469
  if (!tagged || !field || tagged.html) return null;
@@ -171,7 +562,33 @@ function proposedPart(proposal) {
171
562
  ]
172
563
  };
173
564
  }
565
+ function sameValue(a, b) {
566
+ if (typeof a === "number" || typeof b === "number") return a === b;
567
+ return normalizeText(a) === normalizeText(b);
568
+ }
569
+ var LINE_BLOCKS = /* @__PURE__ */ new Set(["div", "p", "li"]);
570
+ function editableText(element) {
571
+ let out = "";
572
+ const walk = (node) => {
573
+ for (let child = node.firstChild; child; child = child.nextSibling) {
574
+ if (child.nodeType === 3) {
575
+ out += child.data;
576
+ } else if (child.nodeType === 1) {
577
+ const name = child.localName;
578
+ if (name === "br") {
579
+ out += "\n";
580
+ continue;
581
+ }
582
+ if (LINE_BLOCKS.has(name) && out !== "" && !out.endsWith("\n")) out += "\n";
583
+ walk(child);
584
+ }
585
+ }
586
+ };
587
+ walk(element);
588
+ return out;
589
+ }
174
590
  function parseProposed(type, raw) {
591
+ raw = cleanStega(raw);
175
592
  if (type === "NUMBER") {
176
593
  const text = raw.trim().replace(",", ".");
177
594
  const value = Number(text);
@@ -224,6 +641,137 @@ function asDomAnchor(anchor) {
224
641
  return { tag, selector, textQuote: null, rect: NOWHERE, viewport: { w: 1, h: 1 } };
225
642
  }
226
643
 
644
+ // src/preview/site/session.ts
645
+ var REFRESH_LEAD_MS = 2 * 6e4;
646
+ function createTokenSession(ctx, stored, hooks, deps = {}) {
647
+ const { win, doc } = ctx;
648
+ const now = deps.now ?? (() => Date.now());
649
+ const setT = deps.setTimeout ?? ((fn, ms) => win.setTimeout(fn, ms));
650
+ const clearT = deps.clearTimeout ?? ((handle) => win.clearTimeout(handle));
651
+ let current = stored;
652
+ let expired = false;
653
+ let destroyed = false;
654
+ let inflight = null;
655
+ let timer = null;
656
+ function markExpired() {
657
+ if (expired || destroyed) return;
658
+ expired = true;
659
+ stop();
660
+ clearToken(win);
661
+ hooks.onExpired();
662
+ }
663
+ async function renew() {
664
+ if (!refreshLive(current, now()) || !current.refresh) return false;
665
+ let response;
666
+ try {
667
+ response = await win.fetch(`${ctx.authOrigin}/api/oauth/token`, {
668
+ method: "POST",
669
+ credentials: "omit",
670
+ body: new URLSearchParams({
671
+ grant_type: "refresh_token",
672
+ refresh_token: current.refresh,
673
+ client_id: ctx.clientId
674
+ })
675
+ });
676
+ } catch {
677
+ return false;
678
+ }
679
+ if (!response.ok) return false;
680
+ try {
681
+ const next = await readTokenAnswer(response, ctx.siteId, now());
682
+ current = next;
683
+ writeToken(win, next);
684
+ return true;
685
+ } catch {
686
+ return false;
687
+ }
688
+ }
689
+ function refresh() {
690
+ if (expired || destroyed) return Promise.resolve(false);
691
+ inflight ?? (inflight = renew().finally(() => {
692
+ inflight = null;
693
+ if (!destroyed && !expired) schedule();
694
+ }));
695
+ return inflight;
696
+ }
697
+ async function check() {
698
+ if (expired || destroyed) return;
699
+ if (current.exp - REFRESH_LEAD_MS > now()) {
700
+ schedule();
701
+ return;
702
+ }
703
+ const ok = await refresh();
704
+ if (!ok && !accessLive(current, now())) markExpired();
705
+ else if (!ok) {
706
+ stop();
707
+ timer = setT(() => void lastTry(), Math.max(0, current.exp - EXPIRY_MARGIN_MS - now()));
708
+ }
709
+ }
710
+ async function lastTry() {
711
+ if (!await refresh()) markExpired();
712
+ }
713
+ function stop() {
714
+ if (timer !== null) clearT(timer);
715
+ timer = null;
716
+ }
717
+ function schedule() {
718
+ stop();
719
+ if (expired || destroyed) return;
720
+ timer = setT(() => void check(), Math.max(0, current.exp - REFRESH_LEAD_MS - now()));
721
+ }
722
+ const onVisible = () => {
723
+ if (doc.visibilityState === "visible") void check();
724
+ };
725
+ doc.addEventListener("visibilitychange", onVisible);
726
+ async function ensure() {
727
+ if (expired) return null;
728
+ if (accessLive(current, now())) return current.token;
729
+ if (await refresh()) return current.token;
730
+ markExpired();
731
+ return null;
732
+ }
733
+ async function call2(request) {
734
+ const token = await ensure();
735
+ if (token === null) return { ok: false, kind: "expired" };
736
+ const first = await request(token);
737
+ if (first.ok || first.kind !== "expired") return first;
738
+ if (await refresh()) {
739
+ const second = await request(current.token);
740
+ if (!second.ok && second.kind === "expired") markExpired();
741
+ return second;
742
+ }
743
+ markExpired();
744
+ return first;
745
+ }
746
+ void check();
747
+ return {
748
+ token: () => current.token,
749
+ get expired() {
750
+ return expired;
751
+ },
752
+ ensure,
753
+ refresh,
754
+ call: call2,
755
+ destroy() {
756
+ destroyed = true;
757
+ stop();
758
+ doc.removeEventListener("visibilitychange", onVisible);
759
+ }
760
+ };
761
+ }
762
+ var SESSION_MESSAGES = {
763
+ es: {
764
+ expiredTitle: "Tu sesi\xF3n de Zap expir\xF3",
765
+ expiredBody: "Vuelve a entrar para seguir editando y comentando.",
766
+ signInAgain: "Volver a entrar"
767
+ },
768
+ en: {
769
+ expiredTitle: "Your Zap session expired",
770
+ expiredBody: "Sign in again to keep editing and commenting.",
771
+ signInAgain: "Sign in again"
772
+ }
773
+ };
774
+
227
775
  // src/preview/site/suggest.ts
228
776
  var OVERLAY_MODE = {
229
777
  navigate: "off",
@@ -236,24 +784,31 @@ var TOAST_MS = 8e3;
236
784
  var SAVED_MS = 4e3;
237
785
  function start(ctx, options = {}) {
238
786
  const { win, doc } = ctx;
239
- const stored = readToken(win, ctx.siteId);
787
+ const stored = readSession(win, ctx.siteId);
240
788
  if (!stored) {
241
789
  void ctx.open("signin", { reason: "expired" });
242
790
  return { destroy() {
243
791
  } };
244
792
  }
245
- const token = stored.token;
793
+ const auth = createTokenSession(ctx, stored, { onExpired: () => expire() });
794
+ const sm = SESSION_MESSAGES[ctx.locale];
246
795
  const m = SUGGEST_MESSAGES[ctx.locale];
247
- const host = mountHost(doc, CSS);
796
+ const dm = DRAFT_SESSION_MESSAGES[ctx.locale];
797
+ let reopenEdit = takeEditIntent(win);
798
+ const host = mountHost(doc, CSS + DRAG_CSS);
248
799
  const layer = el(doc, "div", { class: "layer" });
249
800
  const marks = el(doc, "div", { class: "marks" });
250
801
  const bottom = el(doc, "div", { class: "bottom" });
251
- layer.append(marks, bottom);
802
+ const tip = el(doc, "div", { class: "tip", attrs: { role: "tooltip" } });
803
+ layer.append(marks, bottom, tip);
804
+ const drag = barDrag(win, doc, bottom, m.move, m.moveTip);
805
+ const key = ctx.shortcut ? ctx.shortcut.toUpperCase() : null;
252
806
  host.root.append(layer);
253
807
  let mode = "navigate";
254
808
  let selection = [];
255
809
  let data = null;
256
810
  let refused = false;
811
+ let expired = false;
257
812
  let composer = null;
258
813
  let editing = null;
259
814
  let list = null;
@@ -266,6 +821,10 @@ function start(ctx, options = {}) {
266
821
  let pins = [];
267
822
  let numbers = /* @__PURE__ */ new Map();
268
823
  let lastClick = { x: 0, y: 0, additive: false };
824
+ let menuOpen = false;
825
+ let focusNext = null;
826
+ let entering = false;
827
+ let paused = null;
269
828
  const onClickFirst = (event) => {
270
829
  lastClick = {
271
830
  x: event.clientX,
@@ -274,6 +833,15 @@ function start(ctx, options = {}) {
274
833
  };
275
834
  };
276
835
  win.addEventListener("click", onClickFirst, true);
836
+ const onPressFirst = (event) => {
837
+ if (menuOpen && event.target !== host.host) setMenu(false);
838
+ };
839
+ win.addEventListener("pointerdown", onPressFirst, true);
840
+ layer.addEventListener("pointerdown", (event) => {
841
+ if (!menuOpen) return;
842
+ const keep = Array.from(bottom.querySelectorAll('.menu, [data-action="account"]'));
843
+ if (!keep.some((node) => node.contains(event.target))) setMenu(false);
844
+ });
277
845
  const index = new TagIndex(doc);
278
846
  index.scan();
279
847
  const overlay = new Overlay({
@@ -289,26 +857,47 @@ function start(ctx, options = {}) {
289
857
  const found = data?.comments.find((comment) => comment.id === id);
290
858
  if (!found) return;
291
859
  list = [found];
292
- renderBottom();
860
+ if (drag.hidden()) setHidden(false, false);
861
+ else renderBottom();
293
862
  }
294
863
  });
295
864
  overlay.mount();
296
- const stopObserving = observeTags(
297
- doc.documentElement,
298
- () => {
299
- index.scan();
300
- placePins();
301
- },
302
- {
303
- ignore: (node) => {
304
- const own = overlay.hostElement;
305
- return node === host.host || !!own && (node === own || own.contains(node));
865
+ doc.documentElement.appendChild(host.host);
866
+ const check = tagGuard();
867
+ let stopObserving = () => {
868
+ };
869
+ const atBoot = check(index.elements.length, true);
870
+ if (atBoot) {
871
+ paused = atBoot;
872
+ console.warn(`eelzap: preview paused (${atBoot})`);
873
+ } else
874
+ stopObserving = observeTags(
875
+ doc.documentElement,
876
+ (page) => {
877
+ index.scan();
878
+ const reason = check(index.elements.length, page);
879
+ if (reason) pause(reason);
880
+ else placePins();
881
+ },
882
+ {
883
+ ignore: (node) => {
884
+ const own = overlay.hostElement;
885
+ return node === host.host || !!own && (node === own || own.contains(node));
886
+ }
306
887
  }
307
- }
308
- );
888
+ );
889
+ function pause(reason) {
890
+ if (paused) return;
891
+ paused = reason;
892
+ stopObserving();
893
+ console.warn(`eelzap: preview paused (${reason})`);
894
+ if (destroyed) return;
895
+ overlay.setPins([]);
896
+ setMode("navigate");
897
+ }
309
898
  async function load() {
310
899
  const refs = Array.from(new Set(index.elements.map((tagged) => tagged.recordRef)));
311
- const result = await fetchOnPage(ctx, token, currentPageUrl(win), refs);
900
+ const result = await auth.call((token) => fetchOnPage(ctx, token, currentPageUrl(win), refs));
312
901
  if (destroyed) return;
313
902
  if (!result.ok) {
314
903
  onFailure(result);
@@ -317,13 +906,21 @@ function start(ctx, options = {}) {
317
906
  const first = data === null;
318
907
  data = result.data;
319
908
  const labels = {};
320
- for (const fields of Object.values(data.fields ?? {})) {
321
- for (const [key, field] of Object.entries(fields)) {
322
- if (typeof field?.label === "string") labels[key] = field.label;
909
+ const foreign = {};
910
+ for (const [ref, fields] of Object.entries(data.fields ?? {})) {
911
+ const name = data.records?.[ref];
912
+ for (const [key2, field] of Object.entries(fields)) {
913
+ if (typeof field?.label !== "string") continue;
914
+ if (name) foreign[`${ref}#${key2}`] = [field.label, m.inRecord(name)];
915
+ else labels[key2] = field.label;
323
916
  }
324
917
  }
325
- overlay.setLabels(labels);
918
+ overlay.setLabels(labels, foreign);
326
919
  if (!data.liveEditing && mode === "edit") setMode("navigate");
920
+ if (reopenEdit) {
921
+ reopenEdit = false;
922
+ if (canEdit()) setMode("edit");
923
+ }
327
924
  if (first && options.reason === "renewed") {
328
925
  const name = data.viewer?.name;
329
926
  setToast({ title: m.renewed, body: name ? m.renewedAs(name) : m.renewedBody });
@@ -357,51 +954,253 @@ function start(ctx, options = {}) {
357
954
  }
358
955
  function expire() {
359
956
  clearToken(win);
360
- void ctx.open("signin", { reason: "expired" });
957
+ if (expired || destroyed) return;
958
+ expired = true;
959
+ closeComposer();
960
+ overlay.setMode("off");
961
+ renderBottom();
962
+ }
963
+ function signInAgain() {
964
+ void ctx.open("signin", { reason: "expired", signIn: true });
361
965
  }
362
966
  function canEdit() {
363
967
  return !!data?.liveEditing;
364
968
  }
365
969
  function setMode(next) {
366
- if (next === "edit" && !canEdit()) return;
970
+ if (next === "edit" && !canEdit() || paused && next !== "navigate") return;
971
+ if (status === "preparing") return;
972
+ if (next === "edit" && !index.elements.length && !inDraftSession(win)) {
973
+ void prepareEdit();
974
+ return;
975
+ }
367
976
  endEdit();
368
977
  closeComposer();
369
978
  mode = next;
370
979
  selection = [];
371
980
  overlay.clearSelection();
372
981
  overlay.setMode(OVERLAY_MODE[next]);
982
+ if (next === "edit" && !index.elements.length)
983
+ setToast({ title: m.noFields, body: m.noFieldsBody });
373
984
  renderBottom();
374
985
  }
986
+ async function prepareEdit() {
987
+ setStatus("preparing");
988
+ const token = await auth.ensure();
989
+ if (token === null || destroyed) return;
990
+ const result = await requestDraftSession(ctx, token);
991
+ if (destroyed) return;
992
+ if (result.ok) {
993
+ win.location.assign(result.url);
994
+ return;
995
+ }
996
+ setStatus(null);
997
+ if (result.reason === "expired") {
998
+ expire();
999
+ return;
1000
+ }
1001
+ const body = result.reason === "no-route" ? dm.noRoute : result.reason === "rate" ? dm.rate(result.retryAfter) : dm.failed;
1002
+ setToast({ title: m.noFields, body, error: result.reason !== "no-route" });
1003
+ }
375
1004
  function renderBottom() {
376
1005
  if (destroyed) return;
1006
+ hideTip();
1007
+ const active = host.root.activeElement?.getAttribute("data-action");
1008
+ const hidden = drag.hidden();
377
1009
  const children = [];
378
1010
  if (toast) children.push(toastCard(toast));
1011
+ if (expired) {
1012
+ children.push(expiredCard());
1013
+ bottom.replaceChildren(...children);
1014
+ drag.place();
1015
+ marks.replaceChildren();
1016
+ return;
1017
+ }
379
1018
  if (refused) {
380
1019
  children.push(refusedCard());
381
1020
  bottom.replaceChildren(...children);
1021
+ drag.place();
382
1022
  marks.replaceChildren();
383
1023
  return;
384
1024
  }
385
- if (list) children.push(listCard(list));
386
- if (selection.length > 0 && !composer) children.push(trayCard());
1025
+ if (paused && !hidden) children.push(pausedCard(paused));
1026
+ if (list && !hidden) children.push(listCard(list));
1027
+ if (selection.length > 0 && !composer && !hidden) children.push(trayCard());
387
1028
  if (status) {
388
1029
  children.push(
389
1030
  el(doc, "div", { class: "card status", attrs: { role: "status" } }, [
390
1031
  status === "saved" ? icon(doc, "check", 14) : null,
391
- status === "saved" ? m.saved : m.saving
1032
+ status === "saved" ? m.saved : status === "preparing" ? dm.preparing : m.saving
392
1033
  ])
393
1034
  );
394
1035
  }
395
- children.push(toolbar());
1036
+ if (menuOpen && !hidden) children.push(menuCard());
1037
+ const bar = hidden ? pill() : toolbar();
1038
+ if (entering) bar.setAttribute("data-enter", "");
1039
+ entering = false;
1040
+ children.push(bar);
396
1041
  bottom.replaceChildren(...children);
1042
+ drag.place();
397
1043
  drawMarks();
1044
+ const want = focusNext ?? active;
1045
+ focusNext = null;
1046
+ if (want)
1047
+ bottom.querySelector(`[data-action="${want}"]`)?.focus({ preventScroll: true });
398
1048
  }
399
- function toolbar() {
1049
+ function openCount() {
400
1050
  const open = data?.comments.length ?? 0;
1051
+ return [open, data?.truncated ? `${open}+` : String(open)];
1052
+ }
1053
+ function setMenu(open, focus = null) {
1054
+ if (menuOpen === open && !focus) return;
1055
+ menuOpen = open;
1056
+ focusNext = focus;
1057
+ renderBottom();
1058
+ }
1059
+ function setHidden(value, fromUi) {
1060
+ if (refused || drag.hidden() === value) return;
1061
+ if (value) {
1062
+ endEdit();
1063
+ closeComposer();
1064
+ selection = [];
1065
+ overlay.clearSelection();
1066
+ list = null;
1067
+ menuOpen = false;
1068
+ }
1069
+ drag.setHidden(value);
1070
+ overlay.setMode(value ? "off" : OVERLAY_MODE[mode]);
1071
+ focusNext = fromUi ? value ? "show" : "hide" : null;
1072
+ entering = true;
1073
+ renderBottom();
1074
+ }
1075
+ function withTip(target, text, end) {
1076
+ const show = () => {
1077
+ tip.replaceChildren(text);
1078
+ if (key) tip.append(el(doc, "span", { class: "kbd", text: `Shift ${key}` }));
1079
+ tip.classList.add("on");
1080
+ const rect = target.getBoundingClientRect();
1081
+ const vw = doc.documentElement.clientWidth || win.innerWidth;
1082
+ const w = tip.offsetWidth;
1083
+ const gap = end ? 15 : 8;
1084
+ const x = end ? rect.right + 7 - w : rect.left + (rect.width - w) / 2;
1085
+ tip.style.left = `${Math.min(Math.max(x, 8), vw - w - 8)}px`;
1086
+ tip.style.top = bottom.hasAttribute("data-below") ? `${rect.bottom + gap}px` : `${rect.top - gap - tip.offsetHeight}px`;
1087
+ };
1088
+ const focus = () => {
1089
+ try {
1090
+ if (target.matches(":focus-visible")) show();
1091
+ } catch {
1092
+ show();
1093
+ }
1094
+ };
1095
+ for (const [type, handler] of [
1096
+ ["mouseenter", show],
1097
+ ["focus", focus],
1098
+ ["mouseleave", hideTip],
1099
+ ["blur", hideTip],
1100
+ ["pointerdown", hideTip]
1101
+ ]) {
1102
+ target.addEventListener(type, handler);
1103
+ }
1104
+ if (key) target.setAttribute("aria-keyshortcuts", `Shift+${key}`);
1105
+ return target;
1106
+ }
1107
+ function hideTip() {
1108
+ tip.classList.remove("on");
1109
+ }
1110
+ function avatar(name, large = false) {
1111
+ return el(doc, "span", {
1112
+ class: large ? "avatar lg" : "avatar",
1113
+ attrs: { "aria-hidden": "true" },
1114
+ text: initials(name)
1115
+ });
1116
+ }
1117
+ function menuCard() {
1118
+ const name = data?.viewer?.name ?? null;
1119
+ const email = data?.viewer?.email ?? null;
1120
+ const item = (action, label, glyph, run) => el(
1121
+ doc,
1122
+ "button",
1123
+ {
1124
+ class: "mi",
1125
+ attrs: { type: "button", role: "menuitem", tabindex: "-1", "data-action": action },
1126
+ on: {
1127
+ click: run,
1128
+ pointerenter: (event) => event.currentTarget.focus()
1129
+ }
1130
+ },
1131
+ [icon(doc, glyph), label]
1132
+ );
1133
+ return el(
1134
+ doc,
1135
+ "div",
1136
+ {
1137
+ class: "card menu",
1138
+ attrs: { role: "menu", "aria-label": m.accountMenu, tabindex: "-1", "data-action": "menu" },
1139
+ on: { keydown: onMenuKey }
1140
+ },
1141
+ [
1142
+ el(doc, "div", { class: "menu-head" }, [
1143
+ avatar(name, true),
1144
+ el(doc, "div", { class: "menu-who" }, [
1145
+ el(doc, "span", { class: "menu-name", text: name ?? m.someone }),
1146
+ email ? el(doc, "span", { class: "menu-mail", text: email }) : null
1147
+ ])
1148
+ ]),
1149
+ el(doc, "div", { class: "sep", attrs: { role: "separator" } }),
1150
+ item("open-zap", m.openInZap, "external", () => {
1151
+ setMenu(false, "account");
1152
+ win.open(zapUrl(data?.viewer?.editorUrl, ctx.zapOrigin), "_blank", "noopener");
1153
+ }),
1154
+ el(doc, "div", { class: "sep", attrs: { role: "separator" } }),
1155
+ item("sign-out", m.signOut, "exit", exit)
1156
+ ]
1157
+ );
1158
+ }
1159
+ function onMenuKey(event) {
1160
+ const e = event;
1161
+ if (e.key === "Tab") {
1162
+ setMenu(false, "account");
1163
+ return;
1164
+ }
1165
+ const items = Array.from(bottom.querySelectorAll('.menu [role="menuitem"]'));
1166
+ const at = items.indexOf(host.root.activeElement);
1167
+ const to = { ArrowDown: at + 1, ArrowUp: at < 0 ? -1 : at - 1, Home: 0, End: -1 }[e.key];
1168
+ if (to === void 0) return;
1169
+ e.preventDefault();
1170
+ items[(to + items.length) % items.length]?.focus();
1171
+ }
1172
+ function pill() {
1173
+ const [open, shown] = openCount();
1174
+ return withTip(
1175
+ drag.grab(
1176
+ el(
1177
+ doc,
1178
+ "button",
1179
+ {
1180
+ class: "card pill",
1181
+ attrs: {
1182
+ type: "button",
1183
+ "aria-label": m.showBar(open).replace(String(open), shown) + (paused ? `. ${m.paused}` : ""),
1184
+ "data-action": "show"
1185
+ },
1186
+ on: { click: () => setHidden(false, true) }
1187
+ },
1188
+ [
1189
+ icon(doc, "zap", 20),
1190
+ el(doc, "span", { class: "pill-count" }, [icon(doc, "thread", 14), shown])
1191
+ ]
1192
+ )
1193
+ ),
1194
+ m.show,
1195
+ false
1196
+ );
1197
+ }
1198
+ function toolbar() {
1199
+ const [open, shown] = openCount();
401
1200
  const name = data?.viewer?.name ?? null;
402
1201
  const tool = (value, label, glyph) => {
403
- const off = value === "edit" && !canEdit();
404
- const why = off && data ? m.editOff : null;
1202
+ const off = value === "edit" && !canEdit() || !!paused && value !== "navigate";
1203
+ const why = paused && off ? m.paused : off && data ? m.editOff : null;
405
1204
  return el(
406
1205
  doc,
407
1206
  "button",
@@ -425,6 +1224,8 @@ function start(ctx, options = {}) {
425
1224
  "div",
426
1225
  { class: "card toolbar", attrs: { role: "toolbar", "aria-label": m.toolbar } },
427
1226
  [
1227
+ drag.handle,
1228
+ el(doc, "span", { class: "divider", attrs: { "aria-hidden": "true" } }),
428
1229
  icon(doc, "zap", 20),
429
1230
  el(doc, "div", { class: "segments", attrs: { role: "group", "aria-label": m.tools } }, [
430
1231
  tool("navigate", m.navigate, "pointer"),
@@ -445,18 +1246,50 @@ function start(ctx, options = {}) {
445
1246
  }
446
1247
  }
447
1248
  },
448
- [
449
- icon(doc, "thread"),
450
- data?.truncated ? m.open(open).replace(String(open), `${open}+`) : m.open(open)
451
- ]
1249
+ [icon(doc, "thread"), m.open(open).replace(String(open), shown)]
452
1250
  ),
453
1251
  el(doc, "span", { class: "divider", attrs: { "aria-hidden": "true" } }),
454
- name ? el(doc, "span", { class: "avatar", attrs: { title: name }, text: initials(name) }) : null,
455
1252
  el(
456
1253
  doc,
457
1254
  "button",
458
- { class: "tool", attrs: { type: "button", "data-action": "exit" }, on: { click: exit } },
459
- [icon(doc, "exit"), m.exit]
1255
+ {
1256
+ class: "tool account",
1257
+ attrs: {
1258
+ type: "button",
1259
+ "aria-haspopup": "menu",
1260
+ "aria-expanded": String(menuOpen),
1261
+ "aria-label": name ? m.account(name) : m.accountMenu,
1262
+ "data-action": "account"
1263
+ },
1264
+ on: {
1265
+ // A pointer opens onto the menu itself; Enter or Space onto its first item.
1266
+ click: (event) => setMenu(
1267
+ !menuOpen,
1268
+ menuOpen ? "account" : event.detail ? "menu" : "open-zap"
1269
+ ),
1270
+ keydown: (event) => {
1271
+ const k = event.key;
1272
+ if (k !== "ArrowDown" && k !== "ArrowUp") return;
1273
+ event.preventDefault();
1274
+ setMenu(true, k === "ArrowUp" ? "sign-out" : "open-zap");
1275
+ }
1276
+ }
1277
+ },
1278
+ [avatar(name), icon(doc, "chevron", 13)]
1279
+ ),
1280
+ withTip(
1281
+ el(
1282
+ doc,
1283
+ "button",
1284
+ {
1285
+ class: "tool hide",
1286
+ attrs: { type: "button", "aria-label": m.hide, "data-action": "hide" },
1287
+ on: { click: () => setHidden(true, true) }
1288
+ },
1289
+ [icon(doc, "eyeOff", 16)]
1290
+ ),
1291
+ m.hide,
1292
+ true
460
1293
  )
461
1294
  ]
462
1295
  );
@@ -517,7 +1350,7 @@ function start(ctx, options = {}) {
517
1350
  {
518
1351
  class: "item",
519
1352
  attrs: { type: "button", "data-comment": comment.id },
520
- on: { click: () => reveal(comment.id) }
1353
+ on: { click: () => reveal2(comment.id) }
521
1354
  },
522
1355
  [
523
1356
  n ? el(doc, "span", { class: "num", text: String(n) }) : null,
@@ -534,9 +1367,9 @@ function start(ctx, options = {}) {
534
1367
  ]
535
1368
  );
536
1369
  }
537
- function reveal(id) {
1370
+ function reveal2(id) {
538
1371
  const pin = pins.find((entry) => entry.id === id);
539
- if (pin?.element) pin.element.scrollIntoView?.({ block: "center", behavior: "smooth" });
1372
+ if (pin?.element) reveal(win, pin.element);
540
1373
  else if (pin?.spot) {
541
1374
  const root = doc.documentElement;
542
1375
  win.scrollTo?.({
@@ -563,6 +1396,43 @@ function start(ctx, options = {}) {
563
1396
  ]
564
1397
  );
565
1398
  }
1399
+ function pausedCard(reason) {
1400
+ return el(doc, "div", { class: "card refused paused", attrs: { role: "status" } }, [
1401
+ icon(doc, "alert", 18),
1402
+ el(doc, "div", { class: "toast-text" }, [
1403
+ el(doc, "span", { class: "title", text: m.paused }),
1404
+ el(doc, "span", {
1405
+ class: "muted",
1406
+ text: reason === "cap" ? m.pausedCap : m.pausedRunaway
1407
+ })
1408
+ ]),
1409
+ el(doc, "button", {
1410
+ class: "tool",
1411
+ text: m.reload,
1412
+ attrs: { type: "button", "data-action": "reload" },
1413
+ on: { click: () => win.location.reload() }
1414
+ })
1415
+ ]);
1416
+ }
1417
+ function expiredCard() {
1418
+ return el(doc, "div", { class: "card refused", attrs: { role: "alert" } }, [
1419
+ icon(doc, "alert", 18),
1420
+ el(doc, "div", { class: "toast-text" }, [
1421
+ el(doc, "span", { class: "title", text: sm.expiredTitle }),
1422
+ el(doc, "span", { class: "muted", text: sm.expiredBody })
1423
+ ]),
1424
+ el(
1425
+ doc,
1426
+ "button",
1427
+ {
1428
+ class: "tool",
1429
+ attrs: { type: "button", "data-action": "sign-in-again" },
1430
+ on: { click: signInAgain }
1431
+ },
1432
+ [icon(doc, "eel"), sm.signInAgain]
1433
+ )
1434
+ ]);
1435
+ }
566
1436
  function refusedCard() {
567
1437
  return el(doc, "div", { class: "card refused", attrs: { role: "alert" } }, [
568
1438
  icon(doc, "alert", 18),
@@ -603,11 +1473,12 @@ function start(ctx, options = {}) {
603
1473
  renderBottom();
604
1474
  }
605
1475
  function exit() {
1476
+ void leaveDraftSession(ctx, auth.token(), index.elements.length > 0);
606
1477
  clearToken(win);
607
1478
  ctx.close();
608
1479
  }
609
1480
  function placePins() {
610
- if (destroyed || refused || !data) return;
1481
+ if (destroyed || refused || paused || !data) return;
611
1482
  const placed = pinsFor(data.comments ?? [], index, doc, win);
612
1483
  pins = placed.pins;
613
1484
  numbers = placed.numbers;
@@ -693,7 +1564,7 @@ function start(ctx, options = {}) {
693
1564
  const field = fieldOf(data, tagged);
694
1565
  const proposable = editKindFor(tagged, field) !== null;
695
1566
  const target = single;
696
- const before = field?.type === "URL" && target?.localName === "a" ? target.getAttribute("href") ?? "" : (target?.textContent ?? "").replace(/\s+/g, " ").trim();
1567
+ const before = field?.type === "URL" && target?.localName === "a" ? target.getAttribute("href") ?? "" : normalizeText(target?.textContent ?? "");
697
1568
  const spotPin = point ? anchors[0] : null;
698
1569
  const textarea = el(doc, "textarea", {
699
1570
  class: "composer",
@@ -879,7 +1750,7 @@ function start(ctx, options = {}) {
879
1750
  const parsed = parseProposed(session.field.type, session.proposal.value);
880
1751
  if (!parsed.ok) return "invalid";
881
1752
  const before = parseProposed(session.field.type, session.before);
882
- if (before.ok && before.value === parsed.value) return null;
1753
+ if (before.ok && sameValue(before.value, parsed.value)) return null;
883
1754
  return { fieldKey: session.tagged.fieldKey, locale: session.tagged.locale, value: parsed.value };
884
1755
  }
885
1756
  function refreshSend() {
@@ -921,7 +1792,8 @@ function start(ctx, options = {}) {
921
1792
  session.busy = true;
922
1793
  delete session.error.dataset.sticky;
923
1794
  refreshSend();
924
- const result = await createComment(ctx, token, body, session.idempotency.key);
1795
+ const key2 = session.idempotency.key;
1796
+ const result = await auth.call((token) => createComment(ctx, token, body, key2));
925
1797
  if (destroyed || composer !== session) return;
926
1798
  session.busy = false;
927
1799
  if (!result.ok) {
@@ -968,7 +1840,7 @@ function start(ctx, options = {}) {
968
1840
  const element = tagged.element;
969
1841
  const pageUrl = currentPageUrl(win);
970
1842
  const anchors = [toApiAnchor(buildAnchor(element, index, pageUrl, doc, win), tagged.recordRef)];
971
- const before = field.type === "URL" && element.localName === "a" ? element.getAttribute("href") ?? "" : (element.textContent ?? "").replace(/\s+/g, " ").trim();
1843
+ const before = field.type === "URL" && element.localName === "a" ? element.getAttribute("href") ?? "" : normalizeText(element.textContent ?? "");
972
1844
  const inline = kind === "text" ? beginInlineEdit(element) : null;
973
1845
  const session = {
974
1846
  element,
@@ -1075,8 +1947,7 @@ function start(ctx, options = {}) {
1075
1947
  }
1076
1948
  function typedValue(session) {
1077
1949
  if (session.input) return session.input.value;
1078
- const element = session.element;
1079
- return element.innerText ?? element.textContent ?? "";
1950
+ return editableText(session.element);
1080
1951
  }
1081
1952
  function finishEdit(session, keepTyped) {
1082
1953
  session.done = true;
@@ -1099,7 +1970,7 @@ function start(ctx, options = {}) {
1099
1970
  return;
1100
1971
  }
1101
1972
  const before = parseProposed(session.field.type, session.before);
1102
- if (before.ok && before.value === parsed.value) {
1973
+ if (before.ok && sameValue(before.value, parsed.value)) {
1103
1974
  finishEdit(session, false);
1104
1975
  return;
1105
1976
  }
@@ -1118,7 +1989,8 @@ function start(ctx, options = {}) {
1118
1989
  anchors: session.anchors,
1119
1990
  proposal
1120
1991
  });
1121
- const result = await saveToDraft(ctx, token, body, newIdempotencyKey(win));
1992
+ const draftKey = newIdempotencyKey(win);
1993
+ const result = await auth.call((token) => saveToDraft(ctx, token, body, draftKey));
1122
1994
  if (destroyed) return;
1123
1995
  if (!result.ok) {
1124
1996
  session.revert();
@@ -1132,6 +2004,16 @@ function start(ctx, options = {}) {
1132
2004
  if (editing && !editing.done) commitEdit(editing);
1133
2005
  }
1134
2006
  const onKey = (event) => {
2007
+ if (event.key === "Escape") hideTip();
2008
+ if (menuOpen && event.key === "Escape") {
2009
+ event.preventDefault();
2010
+ setMenu(false, "account");
2011
+ return;
2012
+ }
2013
+ if (ctx.shortcut && !editing && !composer && isShortcut(event, ctx.shortcut)) {
2014
+ setHidden(!drag.hidden(), !!host.root.activeElement);
2015
+ return;
2016
+ }
1135
2017
  if (composer && event.key === "Escape") {
1136
2018
  event.preventDefault();
1137
2019
  closeComposer();
@@ -1147,17 +2029,21 @@ function start(ctx, options = {}) {
1147
2029
  commitEdit(session);
1148
2030
  }
1149
2031
  };
2032
+ const blocks = () => mode !== "navigate" && !drag.hidden();
1150
2033
  const onClick = (event) => {
1151
- if (editing?.kind === "text" && !editing.done && editing.element.contains(event.target)) {
2034
+ if (editing?.kind === "text" && !editing.done && editing.element.contains(event.target) || blocks() && event.target.closest?.("a[href],area[href]")) {
1152
2035
  event.preventDefault();
1153
2036
  }
1154
2037
  };
2038
+ const onSubmit = (event) => {
2039
+ if (blocks()) event.preventDefault();
2040
+ };
1155
2041
  win.addEventListener("keydown", onKey, true);
1156
2042
  win.addEventListener("click", onClick, true);
2043
+ win.addEventListener("submit", onSubmit, true);
1157
2044
  win.addEventListener("scroll", schedule, true);
1158
2045
  win.addEventListener("resize", schedule);
1159
- const expiry = win.setTimeout(expire, Math.max(0, stored.exp - Date.now() - 3e4));
1160
- overlay.setMode(OVERLAY_MODE[mode]);
2046
+ overlay.setMode(drag.hidden() ? "off" : OVERLAY_MODE[mode]);
1161
2047
  renderBottom();
1162
2048
  void load();
1163
2049
  return {
@@ -1165,37 +2051,84 @@ function start(ctx, options = {}) {
1165
2051
  endEdit();
1166
2052
  destroyed = true;
1167
2053
  closeComposer();
1168
- win.clearTimeout(expiry);
2054
+ auth.destroy();
1169
2055
  if (toastTimer !== null) win.clearTimeout(toastTimer);
1170
2056
  if (statusTimer !== null) win.clearTimeout(statusTimer);
1171
2057
  win.removeEventListener("click", onClickFirst, true);
2058
+ win.removeEventListener("pointerdown", onPressFirst, true);
1172
2059
  win.removeEventListener("keydown", onKey, true);
1173
2060
  win.removeEventListener("click", onClick, true);
2061
+ win.removeEventListener("submit", onSubmit, true);
1174
2062
  win.removeEventListener("scroll", schedule, true);
1175
2063
  win.removeEventListener("resize", schedule);
1176
2064
  stopObserving();
1177
2065
  overlay.destroy();
2066
+ drag.destroy();
1178
2067
  host.destroy();
1179
2068
  }
1180
2069
  };
1181
2070
  }
2071
+ function zapUrl(editorUrl, zapOrigin) {
2072
+ const home = new URL("/", zapOrigin);
2073
+ try {
2074
+ const url = new URL(editorUrl ?? "");
2075
+ if (url.protocol === "https:" || url.protocol === "http:") return url.href;
2076
+ } catch {
2077
+ }
2078
+ return home.href;
2079
+ }
1182
2080
  var CSS = `
1183
2081
  .marks { position: fixed; inset: 0; pointer-events: none; }
1184
2082
  .mark { position: fixed; border-radius: 4px; outline: 2px solid rgba(207,135,0,.55); }
1185
2083
  .bottom { position: fixed; left: 0; right: 0; bottom: 22px; display: flex; flex-direction: column;
1186
2084
  align-items: center; gap: 10px; pointer-events: none; padding: 0 8px; }
1187
2085
  .bottom > .card { position: relative; }
1188
- .toolbar { display: flex; align-items: center; gap: 8px; padding: 6px 6px 6px 12px; white-space: nowrap;
2086
+ .toolbar { display: flex; align-items: center; gap: 8px; padding: 6px 6px 6px 4px; white-space: nowrap;
1189
2087
  max-width: 100%; overflow-x: auto; }
1190
- .segments { display: inline-flex; gap: 2px; padding: 2px; border-radius: 7px; background: ${TOKENS.subtle}; }
1191
- .seg { position: relative; height: 26px; padding: 0 9px; border-radius: 5px; font-size: 12px; color: ${TOKENS.muted}; }
2088
+ .segments { display: inline-flex; gap: 2px; padding: 2px; border-radius: 7px; background: ${TOKENS.subtle};
2089
+ background: oklch(0.94 0.008 250); }
2090
+ .seg { position: relative; height: 28px; padding: 0 10px; border-radius: 5px; font-size: 12px; color: ${TOKENS.muted}; }
1192
2091
  .seg[aria-pressed="true"] { background: ${TOKENS.bg}; color: ${TOKENS.fg}; box-shadow: 0 1px 2px rgba(15,23,42,.05); }
1193
2092
  .seg[aria-disabled="true"] { opacity: .5; cursor: default; }
1194
2093
  .seg[data-tip]:hover::after { content: attr(data-tip); position: absolute; bottom: 34px; left: 50%;
1195
2094
  transform: translateX(-50%); padding: 6px 10px; border-radius: 9px; background: ${TOKENS.dark}; color: #FFFFFF;
1196
2095
  font-size: 12px; white-space: nowrap; }
1197
2096
  .tool { height: 30px; padding: 0 9px; border-radius: 9px; color: ${TOKENS.muted}; }
1198
- .tool:hover, .seg:not([aria-disabled="true"]):hover { background: ${TOKENS.subtle}; }
2097
+ /* Hover as the suite's SegmentedControl and ghost button (SitioBarraEstados): a tool on the track
2098
+ gets foreground text over white at 60%, no shadow; the bar's own buttons, on the white card, the
2099
+ ghost hover: foreground text over slate-100. Both keep the focus ring. */
2100
+ .seg:not([aria-pressed="true"]):not([aria-disabled="true"]):hover { background: rgba(255,255,255,.6); color: ${TOKENS.fg}; }
2101
+ .tool:hover, .grip:hover, .account[aria-expanded="true"] { background: #F1F5F9; color: ${TOKENS.fg}; }
2102
+ .seg, .tool, .grip, .pill { transition: color .15s cubic-bezier(.4,0,.2,1), background-color .15s cubic-bezier(.4,0,.2,1); }
2103
+ .seg:focus-visible, .tool:focus-visible, .grip:focus-visible { box-shadow: 0 0 0 3px ${TOKENS.focus}; }
2104
+ /* The account chip and the hide arrow (SitioBarraEstados): ghost buttons, chevrons muted until hover. */
2105
+ .account { padding: 0 4px 0 2px; gap: 3px; }
2106
+ .hide { width: 30px; padding: 0; justify-content: center; }
2107
+ .avatar.lg { width: 32px; height: 32px; }
2108
+ .menu { width: 260px; padding: 4px; border-radius: 11px; outline: none;
2109
+ box-shadow: 0 10px 15px -3px rgba(15,23,42,.1), 0 4px 6px -4px rgba(15,23,42,.08); }
2110
+ .menu-head { display: flex; align-items: center; gap: 10px; padding: 8px 8px 10px; }
2111
+ .menu-who { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
2112
+ .menu-name { font-weight: 500; }
2113
+ .menu-mail { font-size: 12px; color: ${TOKENS.muted}; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2114
+ .sep { height: 1px; background: ${TOKENS.border}; margin: 4px -4px; }
2115
+ .menu-head + .sep { margin-top: 0; }
2116
+ .mi { width: 100%; height: 32px; gap: 9px; padding: 0 8px; border-radius: 7px; font-weight: 400; color: ${TOKENS.fg}; }
2117
+ .mi > .icon { color: ${TOKENS.muted}; }
2118
+ .mi:focus, .mi:focus-visible { outline: none; background: #F1F5F9; box-shadow: none; }
2119
+ .pill { height: 44px; gap: 8px; padding: 0 12px 0 11px; border-radius: 999px; }
2120
+ .pill:hover { background: #F8FAFC; }
2121
+ .pill:focus-visible { box-shadow: 0 0 0 3px ${TOKENS.focus}, 0 20px 25px -5px rgba(15,23,42,.12); }
2122
+ .pill-count { display: inline-flex; align-items: center; gap: 4px; }
2123
+ .tip { position: fixed; display: inline-flex; align-items: center; gap: 8px; padding: 5px 6px 5px 9px;
2124
+ border-radius: 7px; background: #101828; color: #FFFFFF; font-weight: 500; font-size: 12px; line-height: 18px;
2125
+ white-space: nowrap; pointer-events: none; }
2126
+ .tip:not(.on) { display: none; }
2127
+ .kbd { font-size: 11px; line-height: 16px; color: #CBD5E1; padding: 1px 5px; border-radius: 4px;
2128
+ background: rgba(255,255,255,.12); }
2129
+ [data-enter] { animation: eelzap-in .16s cubic-bezier(.4,0,.2,1); }
2130
+ @keyframes eelzap-in { from { opacity: 0; transform: scale(.96); } }
2131
+ @media (prefers-reduced-motion: reduce) { .seg, .tool, .grip, .pill { transition: none; } [data-enter] { animation: none; } }
1199
2132
  .tool.strong { color: ${TOKENS.fg}; }
1200
2133
  .divider { width: 1px; height: 22px; background: ${TOKENS.border}; }
1201
2134
  .avatar { width: 26px; height: 26px; border-radius: 7px; background: #DBEAFE; color: #1D4ED8;
@@ -1219,6 +2152,7 @@ var CSS = `
1219
2152
  .refused { align-self: center; margin-right: 0; width: min(420px, calc(100vw - 16px)); align-items: flex-start; }
1220
2153
  .toast > .icon { color: #16A34A; }
1221
2154
  .refused > .icon, .toast-error > .icon { color: ${TOKENS.danger}; }
2155
+ .paused > .icon { color: ${TOKENS.muted}; }
1222
2156
  .toast-text { display: flex; flex-direction: column; gap: 3px; flex: 1; min-width: 0; }
1223
2157
  .title { font-weight: 600; }
1224
2158
  .head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
@@ -1251,6 +2185,7 @@ input.value { height: 36px; border-radius: 9px; }
1251
2185
  `;
1252
2186
  export {
1253
2187
  OVERLAY_HOST_TAG,
1254
- start
2188
+ start,
2189
+ zapUrl
1255
2190
  };
1256
- //# sourceMappingURL=suggest-5RHURHED.js.map
2191
+ //# sourceMappingURL=suggest-JZ63L3WP.js.map