@8ux-co/eelzap 0.10.0 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2583 @@
1
+ import {
2
+ OVERLAY_HOST_TAG,
3
+ Overlay,
4
+ TagIndex,
5
+ buildAnchor,
6
+ cleanStega,
7
+ findAnchorElement,
8
+ isDraftRoutePath,
9
+ isRecordRef,
10
+ isShortcut,
11
+ normalizeText,
12
+ observeTags,
13
+ parseRecordRef,
14
+ reveal,
15
+ tagGuard
16
+ } from "./chunk-T6S3S5RH.js";
17
+ import {
18
+ SUGGEST_MESSAGES,
19
+ TOKENS,
20
+ el,
21
+ icon,
22
+ initials,
23
+ kbd,
24
+ mountHost,
25
+ readTokenAnswer
26
+ } from "./chunk-4ET7VBJA.js";
27
+ import {
28
+ EXPIRY_MARGIN_MS,
29
+ accessLive,
30
+ clearToken,
31
+ readSession,
32
+ refreshLive,
33
+ writeToken
34
+ } from "./chunk-6XHCA4U4.js";
35
+
36
+ // src/preview/site/api.ts
37
+ var BASE = "/api/public/v1/comments";
38
+ async function call(ctx, token, path, init) {
39
+ const headers = { Authorization: `Bearer ${token}` };
40
+ if (init.body !== void 0) headers["Content-Type"] = "application/json";
41
+ if (init.idempotencyKey) headers["Idempotency-Key"] = init.idempotencyKey;
42
+ let response;
43
+ try {
44
+ response = await ctx.win.fetch(`${ctx.zapOrigin}${path}`, {
45
+ method: init.method,
46
+ headers,
47
+ credentials: "omit",
48
+ body: init.body === void 0 ? void 0 : JSON.stringify(init.body)
49
+ });
50
+ } catch {
51
+ return { ok: false, kind: "refused" };
52
+ }
53
+ if (response.status === 401) return { ok: false, kind: "expired" };
54
+ if (response.status === 403) return { ok: false, kind: "forbidden" };
55
+ if (response.status === 429) {
56
+ const seconds = Number.parseInt(response.headers.get("Retry-After") ?? "", 10);
57
+ return { ok: false, kind: "rate", retryAfter: seconds > 0 ? Math.min(seconds, 3600) : 30 };
58
+ }
59
+ if (response.status === 409) {
60
+ const body = await response.json().catch(() => null);
61
+ if (body?.error?.code === "NO_CHANGE") return { ok: false, kind: "unchanged" };
62
+ }
63
+ if (!response.ok) return { ok: false, kind: "invalid" };
64
+ try {
65
+ return { ok: true, data: await response.json() };
66
+ } catch {
67
+ return { ok: false, kind: "invalid" };
68
+ }
69
+ }
70
+ function fetchOnPage(ctx, token, pageUrl, refs) {
71
+ const query = new URLSearchParams({ url: pageUrl });
72
+ if (refs.length > 0) query.set("refs", refs.slice(0, 20).join(","));
73
+ return call(ctx, token, `${BASE}/on-page?${query.toString()}`, { method: "GET" });
74
+ }
75
+ function createComment(ctx, token, body, idempotencyKey) {
76
+ return call(ctx, token, BASE, { method: "POST", body, idempotencyKey });
77
+ }
78
+ function saveToDraft(ctx, token, body, idempotencyKey) {
79
+ return call(ctx, token, `${BASE}/save-to-draft`, { method: "POST", body, idempotencyKey });
80
+ }
81
+ function newIdempotencyKey(win) {
82
+ const native = win.crypto.randomUUID;
83
+ if (typeof native === "function") return native.call(win.crypto);
84
+ const bytes = win.crypto.getRandomValues(new Uint8Array(16));
85
+ bytes[6] = bytes[6] & 15 | 64;
86
+ bytes[8] = bytes[8] & 63 | 128;
87
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
88
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
89
+ }
90
+
91
+ // src/preview/site/drag.ts
92
+ var BAR_STORAGE_KEY = "eelzap:bar";
93
+ var GUTTER = 16;
94
+ var STEP = 16;
95
+ var BIG_STEP = 64;
96
+ var GAP = 10;
97
+ var SLOP = 3;
98
+ var DOUBLE_MS = 400;
99
+ var clamp = (value, min, max) => Math.min(Math.max(value, min), Math.max(min, max));
100
+ function storage(win) {
101
+ try {
102
+ return win.localStorage;
103
+ } catch {
104
+ return null;
105
+ }
106
+ }
107
+ function read(win) {
108
+ try {
109
+ const value = JSON.parse(storage(win)?.getItem(BAR_STORAGE_KEY) ?? "null");
110
+ return {
111
+ pos: value && Number.isFinite(value.x) && Number.isFinite(value.y) ? { x: value.x, y: value.y } : null,
112
+ hidden: value?.hidden === true
113
+ };
114
+ } catch {
115
+ return { pos: null, hidden: false };
116
+ }
117
+ }
118
+ function write(win, point, hidden) {
119
+ try {
120
+ const store = storage(win);
121
+ if (point || hidden)
122
+ store?.setItem(BAR_STORAGE_KEY, JSON.stringify({ ...point, hidden: hidden || void 0 }));
123
+ else store?.removeItem(BAR_STORAGE_KEY);
124
+ } catch {
125
+ }
126
+ }
127
+ function barDrag(win, doc, stack, label, tip) {
128
+ const saved = read(win);
129
+ let pos = saved.pos;
130
+ let hidden = saved.hidden;
131
+ let drag = null;
132
+ let dragged = false;
133
+ let lastTap = 0;
134
+ const viewport = () => {
135
+ const root = doc.documentElement;
136
+ return [root.clientWidth || win.innerWidth, root.clientHeight || win.innerHeight];
137
+ };
138
+ const bar = () => stack.querySelector(".toolbar, .pill");
139
+ const rectOf = (node) => {
140
+ const rect = node.getBoundingClientRect();
141
+ return { x: rect.left, y: rect.top };
142
+ };
143
+ function place() {
144
+ const toolbar = bar();
145
+ const cards = Array.from(stack.children);
146
+ const [vw, vh] = viewport();
147
+ if (!pos || !toolbar) {
148
+ stack.removeAttribute("style");
149
+ stack.removeAttribute("data-below");
150
+ for (const card of cards) {
151
+ card.style.marginLeft = "";
152
+ card.style.maxHeight = "";
153
+ }
154
+ placeMenu(toolbar, vw);
155
+ return;
156
+ }
157
+ const w = toolbar.offsetWidth;
158
+ const h = toolbar.offsetHeight;
159
+ const x = clamp(pos.x, GUTTER, vw - w - GUTTER);
160
+ const y = clamp(pos.y, GUTTER, vh - h - GUTTER);
161
+ const below = vh - y - h > y;
162
+ const room = (below ? vh - y - h : y) - GAP - GUTTER;
163
+ Object.assign(stack.style, {
164
+ padding: "0",
165
+ alignItems: "flex-start",
166
+ flexDirection: below ? "column-reverse" : "column",
167
+ top: below ? `${y}px` : "auto",
168
+ bottom: below ? "auto" : `${vh - y - h}px`
169
+ });
170
+ stack.toggleAttribute("data-below", below);
171
+ for (const card of cards) {
172
+ if (card.classList.contains("toast") || card.classList.contains("menu")) continue;
173
+ if (card.classList.contains("list")) card.style.maxHeight = `min(50vh, ${room}px)`;
174
+ const cw = card.offsetWidth;
175
+ card.style.marginLeft = `${card === toolbar ? x : clamp(x + (w - cw) / 2, GUTTER / 2, vw - cw - GUTTER / 2)}px`;
176
+ }
177
+ placeMenu(toolbar, vw);
178
+ }
179
+ function placeMenu(toolbar, vw) {
180
+ const menu = stack.querySelector(".menu");
181
+ if (!menu || !toolbar) return;
182
+ const left = stack.getBoundingClientRect().left + (parseFloat(win.getComputedStyle(stack).paddingLeft) || 0);
183
+ const mw = menu.offsetWidth;
184
+ menu.style.alignSelf = "flex-start";
185
+ menu.style.marginLeft = `${clamp(toolbar.getBoundingClientRect().right - 35 - mw, GUTTER / 2, vw - mw - GUTTER / 2) - left}px`;
186
+ }
187
+ function moveTo(point, save) {
188
+ pos = point;
189
+ place();
190
+ const toolbar = bar();
191
+ if (toolbar) pos = rectOf(toolbar);
192
+ if (save) write(win, pos, hidden);
193
+ }
194
+ function reset() {
195
+ pos = null;
196
+ write(win, null, hidden);
197
+ place();
198
+ }
199
+ const onMove = (event) => {
200
+ if (!drag || event.pointerId !== drag.id) return;
201
+ const dx = event.clientX - drag.px;
202
+ const dy = event.clientY - drag.py;
203
+ if (!drag.moved && Math.hypot(dx, dy) < SLOP) return;
204
+ drag.moved = true;
205
+ event.preventDefault();
206
+ pos = { x: drag.from.x + dx, y: drag.from.y + dy };
207
+ place();
208
+ };
209
+ const onUp = (event) => {
210
+ if (!drag || event.pointerId !== drag.id) return;
211
+ const { moved, node } = drag;
212
+ drag = null;
213
+ node.removeAttribute("data-dragging");
214
+ if (moved && pos) {
215
+ moveTo(pos, true);
216
+ lastTap = 0;
217
+ dragged = true;
218
+ win.setTimeout(() => dragged = false);
219
+ } else if (node === handle && event.type === "pointerup") {
220
+ const now = Date.now();
221
+ if (now - lastTap < DOUBLE_MS) {
222
+ lastTap = 0;
223
+ reset();
224
+ } else lastTap = now;
225
+ }
226
+ };
227
+ const stop = (event) => event.stopPropagation();
228
+ function begin(e, node) {
229
+ const toolbar = bar();
230
+ if (e.button !== 0 || !toolbar) return;
231
+ drag = {
232
+ id: e.pointerId,
233
+ px: e.clientX,
234
+ py: e.clientY,
235
+ from: rectOf(toolbar),
236
+ moved: false,
237
+ node
238
+ };
239
+ node.setAttribute("data-dragging", "");
240
+ try {
241
+ node.setPointerCapture(e.pointerId);
242
+ } catch {
243
+ }
244
+ }
245
+ const handle = el(doc, "button", {
246
+ class: "grip",
247
+ attrs: { type: "button", "aria-label": label, title: tip },
248
+ on: {
249
+ pointerdown: (event) => {
250
+ event.stopPropagation();
251
+ begin(event, handle);
252
+ },
253
+ keydown: (event) => {
254
+ const e = event;
255
+ const step = e.shiftKey ? BIG_STEP : STEP;
256
+ const delta = {
257
+ ArrowLeft: [-step, 0],
258
+ ArrowRight: [step, 0],
259
+ ArrowUp: [0, -step],
260
+ ArrowDown: [0, step]
261
+ };
262
+ const move = delta[e.key];
263
+ const toolbar = bar();
264
+ if (!move || !toolbar) return;
265
+ e.preventDefault();
266
+ e.stopPropagation();
267
+ const from = rectOf(toolbar);
268
+ moveTo({ x: from.x + move[0], y: from.y + move[1] }, true);
269
+ }
270
+ }
271
+ });
272
+ for (const type of [
273
+ "pointerup",
274
+ "dblclick",
275
+ "mousedown",
276
+ "mouseup",
277
+ "click",
278
+ "touchstart",
279
+ "touchend"
280
+ ]) {
281
+ handle.addEventListener(type, stop);
282
+ }
283
+ handle.append(icon(doc, "grip"));
284
+ const onResize = () => place();
285
+ win.addEventListener("pointermove", onMove, true);
286
+ win.addEventListener("pointerup", onUp, true);
287
+ win.addEventListener("pointercancel", onUp, true);
288
+ win.addEventListener("resize", onResize);
289
+ win.addEventListener("orientationchange", onResize);
290
+ return {
291
+ handle,
292
+ grab(node) {
293
+ node.addEventListener("pointerdown", (event) => begin(event, node));
294
+ node.addEventListener(
295
+ "click",
296
+ (event) => {
297
+ if (dragged) event.stopImmediatePropagation();
298
+ },
299
+ true
300
+ );
301
+ return node;
302
+ },
303
+ place,
304
+ hidden: () => hidden,
305
+ setHidden(value) {
306
+ hidden = value;
307
+ write(win, pos, hidden);
308
+ },
309
+ destroy() {
310
+ win.removeEventListener("pointermove", onMove, true);
311
+ win.removeEventListener("pointerup", onUp, true);
312
+ win.removeEventListener("pointercancel", onUp, true);
313
+ win.removeEventListener("resize", onResize);
314
+ win.removeEventListener("orientationchange", onResize);
315
+ }
316
+ };
317
+ }
318
+ var DRAG_CSS = `
319
+ .grip { width: 20px; height: 30px; justify-content: center; border-radius: 7px; color: ${TOKENS.muted};
320
+ cursor: grab; touch-action: none; user-select: none; -webkit-user-select: none; }
321
+ .grip[data-dragging], .pill[data-dragging] { cursor: grabbing; }
322
+ .pill { touch-action: none; user-select: none; -webkit-user-select: none; }
323
+ .bottom[data-below] .seg[data-tip]:hover::after { bottom: auto; top: 34px; }
324
+ `;
325
+
326
+ // src/preview/site/draft-session.ts
327
+ var DRAFT_SESSION_PATH = "/api/public/v1/preview/session";
328
+ var EDIT_INTENT_KEY = "eelzap:edit-intent";
329
+ var DRAFT_SESSION_KEY = "eelzap:draft-session";
330
+ var INTENT_MAX_AGE_MS = 2 * 6e4;
331
+ var DRAFT_SESSION_MESSAGES = {
332
+ es: {
333
+ preparing: "Preparando la edici\xF3n\u2026",
334
+ noRoute: "Este sitio no tiene la ruta de vista previa que Editar necesita. Usa Comentar o \xE1brelo desde Zap.",
335
+ failed: "No pudimos preparar la p\xE1gina para editar. Int\xE9ntalo de nuevo o usa Comentar.",
336
+ rate: (seconds) => `Demasiados intentos. Vuelve a intentarlo en ${seconds} ${seconds === 1 ? "segundo" : "segundos"}.`
337
+ },
338
+ en: {
339
+ preparing: "Getting the page ready to edit\u2026",
340
+ noRoute: "This site does not have the preview route Edit needs. Use Comment or open it from Zap.",
341
+ failed: "We could not get the page ready to edit. Try again or use Comment.",
342
+ rate: (seconds) => `Too many tries. Try again in ${seconds} ${seconds === 1 ? "second" : "seconds"}.`
343
+ }
344
+ };
345
+ function storage2(win) {
346
+ try {
347
+ return win.sessionStorage;
348
+ } catch {
349
+ return null;
350
+ }
351
+ }
352
+ function readJson(win, key) {
353
+ try {
354
+ const raw = storage2(win)?.getItem(key);
355
+ if (!raw) return null;
356
+ const value = JSON.parse(raw);
357
+ return value && typeof value === "object" ? value : null;
358
+ } catch {
359
+ return null;
360
+ }
361
+ }
362
+ function write2(win, key, value) {
363
+ try {
364
+ storage2(win)?.setItem(key, JSON.stringify(value));
365
+ } catch {
366
+ }
367
+ }
368
+ function remove(win, key) {
369
+ try {
370
+ storage2(win)?.removeItem(key);
371
+ } catch {
372
+ }
373
+ }
374
+ function currentPath(win) {
375
+ const { pathname, search, hash } = win.location;
376
+ return `${pathname}${search}${hash}`;
377
+ }
378
+ function inDraftSession(win, now = Date.now()) {
379
+ const value = readJson(win, DRAFT_SESSION_KEY);
380
+ return !!value && typeof value.exp === "number" && value.exp > now && value.origin === win.location.origin;
381
+ }
382
+ function rememberEditIntent(win, now = Date.now()) {
383
+ write2(win, EDIT_INTENT_KEY, { path: win.location.pathname, at: now });
384
+ }
385
+ function takeEditIntent(win, now = Date.now()) {
386
+ const value = readJson(win, EDIT_INTENT_KEY);
387
+ remove(win, EDIT_INTENT_KEY);
388
+ return !!value && value.path === win.location.pathname && typeof value.at === "number" && now - value.at >= 0 && now - value.at < INTENT_MAX_AGE_MS;
389
+ }
390
+ function acceptSessionUrl(raw, win, draftRoute) {
391
+ if (typeof raw !== "string") return null;
392
+ let url;
393
+ try {
394
+ url = new URL(raw);
395
+ } catch {
396
+ return null;
397
+ }
398
+ if (url.origin !== win.location.origin || url.pathname !== draftRoute) return null;
399
+ if (!/^zpt_[A-Za-z0-9_-]{43}$/.test(url.searchParams.get("token") ?? "")) return null;
400
+ const path = url.searchParams.get("path") ?? "";
401
+ if (!path.startsWith("/") || path.startsWith("//")) return null;
402
+ return url.toString();
403
+ }
404
+ async function requestDraftSession(ctx, token, now = Date.now) {
405
+ const { win } = ctx;
406
+ const draftRoute = ctx.draftRoute;
407
+ if (!isDraftRoutePath(draftRoute)) return { ok: false, reason: "no-route" };
408
+ let response;
409
+ try {
410
+ response = await win.fetch(`${ctx.zapOrigin}${DRAFT_SESSION_PATH}`, {
411
+ method: "POST",
412
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
413
+ credentials: "omit",
414
+ body: JSON.stringify({ draftRoute, path: currentPath(win) })
415
+ });
416
+ } catch {
417
+ return { ok: false, reason: "refused" };
418
+ }
419
+ if (response.status === 401) return { ok: false, reason: "expired" };
420
+ if (response.status === 429) {
421
+ const seconds = Number.parseInt(response.headers.get("Retry-After") ?? "", 10);
422
+ return { ok: false, reason: "rate", retryAfter: seconds > 0 ? Math.min(seconds, 3600) : 30 };
423
+ }
424
+ if (!response.ok) return { ok: false, reason: "failed" };
425
+ let body;
426
+ try {
427
+ body = await response.json();
428
+ } catch {
429
+ return { ok: false, reason: "failed" };
430
+ }
431
+ const url = acceptSessionUrl(body?.url, win, draftRoute);
432
+ if (!url) return { ok: false, reason: "failed" };
433
+ const at = now();
434
+ const parsed = typeof body?.expiresAt === "string" ? Date.parse(body.expiresAt) : NaN;
435
+ const expiresAt = Number.isFinite(parsed) ? parsed : at + 10 * 6e4;
436
+ write2(win, EDIT_INTENT_KEY, { path: win.location.pathname, at });
437
+ write2(win, DRAFT_SESSION_KEY, { exp: expiresAt, origin: win.location.origin });
438
+ return { ok: true, url, expiresAt };
439
+ }
440
+ function draftExitUrl(win, draftRoute) {
441
+ const url = new URL(`${draftRoute.replace(/\/+$/, "")}/exit`, win.location.origin);
442
+ url.searchParams.set("path", currentPath(win));
443
+ return url.toString();
444
+ }
445
+ async function leaveDraftSession(ctx, token, tagged) {
446
+ const { win } = ctx;
447
+ const entered = inDraftSession(win);
448
+ remove(win, EDIT_INTENT_KEY);
449
+ remove(win, DRAFT_SESSION_KEY);
450
+ if (!entered && !tagged) return false;
451
+ try {
452
+ await win.fetch(`${ctx.zapOrigin}${DRAFT_SESSION_PATH}`, {
453
+ method: "DELETE",
454
+ headers: { Authorization: `Bearer ${token}` },
455
+ credentials: "omit"
456
+ });
457
+ } catch {
458
+ }
459
+ if (!isDraftRoutePath(ctx.draftRoute)) return false;
460
+ try {
461
+ const response = await win.fetch(draftExitUrl(win, ctx.draftRoute), {
462
+ method: "GET",
463
+ credentials: "same-origin",
464
+ redirect: "manual",
465
+ cache: "no-store"
466
+ });
467
+ if (response.type !== "opaqueredirect" && !response.ok) return false;
468
+ } catch {
469
+ return false;
470
+ }
471
+ win.location.reload();
472
+ return true;
473
+ }
474
+
475
+ // src/preview/site/suggestion.ts
476
+ var SUGGESTIBLE_TYPES = ["TEXT", "LONG_TEXT", "NUMBER", "URL"];
477
+ var INPUT_TYPES = /* @__PURE__ */ new Set(["URL", "NUMBER", "INTEGER", "CURRENCY", "DATE", "DATETIME"]);
478
+ function proposableField(tagged, field) {
479
+ return !!tagged && !!field && !tagged.html && SUGGESTIBLE_TYPES.includes(field.type);
480
+ }
481
+ function editKindFor(tagged, field) {
482
+ if (!tagged || !field || tagged.html) return null;
483
+ if (field.type === "TEXT" || field.type === "LONG_TEXT") return "text";
484
+ if (INPUT_TYPES.has(field.type)) return "input";
485
+ if (field.type === "ENUM") return field.options?.length ? "choice" : null;
486
+ if (field.type === "BOOLEAN") return "switch";
487
+ return null;
488
+ }
489
+ var ZERO_DIGITS = /* @__PURE__ */ new Set([
490
+ "BIF",
491
+ "CLP",
492
+ "DJF",
493
+ "GNF",
494
+ "ISK",
495
+ "JPY",
496
+ "KMF",
497
+ "KRW",
498
+ "PYG",
499
+ "RWF",
500
+ "UGX",
501
+ "UYI",
502
+ "VND",
503
+ "VUV",
504
+ "XAF",
505
+ "XOF",
506
+ "XPF"
507
+ ]);
508
+ var THREE_DIGITS = /* @__PURE__ */ new Set(["BHD", "IQD", "JOD", "KWD", "LYD", "OMR", "TND"]);
509
+ function currencyDigits(code) {
510
+ const upper = (code ?? "").toUpperCase();
511
+ if (ZERO_DIGITS.has(upper)) return 0;
512
+ if (THREE_DIGITS.has(upper)) return 3;
513
+ return 2;
514
+ }
515
+ var pad = (n) => String(n).padStart(2, "0");
516
+ function inputSpec(field, value) {
517
+ const digits = currencyDigits(field.currency);
518
+ const limits = { min: field.min, max: field.max };
519
+ const number = typeof value === "number" && Number.isFinite(value) ? value : null;
520
+ switch (field.type) {
521
+ case "NUMBER":
522
+ return {
523
+ type: "number",
524
+ text: number === null ? "" : String(number),
525
+ ...limits,
526
+ step: field.step ?? "any"
527
+ };
528
+ case "INTEGER":
529
+ return { type: "number", text: number === null ? "" : String(number), ...limits, step: 1 };
530
+ case "CURRENCY":
531
+ return {
532
+ type: "number",
533
+ text: number === null ? "" : (number / 10 ** digits).toFixed(digits),
534
+ ...limits,
535
+ step: field.step ?? 10 ** -digits
536
+ };
537
+ case "DATE":
538
+ return { type: "date", text: typeof value === "string" ? value.slice(0, 10) : "" };
539
+ case "DATETIME": {
540
+ const at = typeof value === "string" ? new Date(value) : null;
541
+ return {
542
+ type: "datetime-local",
543
+ text: at && Number.isFinite(at.getTime()) ? `${at.getFullYear()}-${pad(at.getMonth() + 1)}-${pad(at.getDate())}T${pad(at.getHours())}:${pad(at.getMinutes())}` : ""
544
+ };
545
+ }
546
+ default:
547
+ return { type: "text", text: typeof value === "string" ? value : "" };
548
+ }
549
+ }
550
+ function parseInput(field, raw) {
551
+ const text = cleanStega(raw).trim();
552
+ if (field.type === "URL") return { ok: true, value: normalizeText(text) };
553
+ if (text === "") return { ok: true, value: null };
554
+ const number = Number(text.replace(",", "."));
555
+ switch (field.type) {
556
+ case "NUMBER":
557
+ return Number.isFinite(number) ? { ok: true, value: number } : { ok: false };
558
+ case "INTEGER":
559
+ return Number.isInteger(number) ? { ok: true, value: number } : { ok: false };
560
+ case "CURRENCY":
561
+ return Number.isFinite(number) ? { ok: true, value: Math.round(number * 10 ** currencyDigits(field.currency)) } : { ok: false };
562
+ case "DATE":
563
+ return /^\d{4}-\d{2}-\d{2}$/.test(text) && Number.isFinite(Date.parse(text)) ? { ok: true, value: text } : { ok: false };
564
+ case "DATETIME": {
565
+ const at = new Date(text);
566
+ return Number.isFinite(at.getTime()) ? { ok: true, value: at.toISOString() } : { ok: false };
567
+ }
568
+ default:
569
+ return { ok: true, value: text };
570
+ }
571
+ }
572
+ function sameRaw(type, a, b) {
573
+ const before = a ?? null;
574
+ if (typeof before === "string" && typeof b === "string") {
575
+ if (type === "DATETIME") {
576
+ return Math.floor(Date.parse(before) / 6e4) === Math.floor(Date.parse(b) / 6e4);
577
+ }
578
+ return normalizeText(before) === normalizeText(b);
579
+ }
580
+ return before === b;
581
+ }
582
+ function fieldOf(data, tagged) {
583
+ if (!data || !tagged) return null;
584
+ const field = data.fields?.[tagged.recordRef]?.[tagged.fieldKey];
585
+ return field && typeof field.type === "string" ? field : null;
586
+ }
587
+ function recordFor(element, index) {
588
+ const tagged = index.closest(element);
589
+ if (tagged) return tagged.recordRef;
590
+ const entry = element.closest("[data-zap-entry]")?.getAttribute("data-zap-entry")?.trim();
591
+ if (entry && isRecordRef(entry)) return entry;
592
+ const counts = /* @__PURE__ */ new Map();
593
+ for (const row of index.summary()) {
594
+ counts.set(row.recordRef, (counts.get(row.recordRef) ?? 0) + row.count);
595
+ }
596
+ let best = null;
597
+ let most = 0;
598
+ for (const [ref, count] of counts) {
599
+ if (count > most) {
600
+ best = ref;
601
+ most = count;
602
+ }
603
+ }
604
+ return best;
605
+ }
606
+ function currentPageUrl(win) {
607
+ const { origin, pathname, search } = win.location;
608
+ return `${origin}${pathname}${search}`.slice(0, 2048);
609
+ }
610
+ var unit = (value) => Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : 0;
611
+ var whole = (value, max) => Number.isFinite(value) ? Math.min(max, Math.max(1, Math.round(value))) : 1;
612
+ function toApiAnchor(anchor, recordRef) {
613
+ const dom = anchor.dom;
614
+ const out = {
615
+ dom: {
616
+ ...dom.tag && dom.tag.length <= 500 ? { tag: dom.tag } : {},
617
+ ...dom.selector ? { selector: dom.selector } : {},
618
+ ...dom.textQuote ? {
619
+ textQuote: {
620
+ exact: dom.textQuote.exact.slice(0, 500),
621
+ prefix: dom.textQuote.prefix.slice(-32),
622
+ suffix: dom.textQuote.suffix.slice(0, 32)
623
+ }
624
+ } : {},
625
+ rect: { x: unit(dom.rect.x), y: unit(dom.rect.y), w: unit(dom.rect.w), h: unit(dom.rect.h) },
626
+ viewport: { w: whole(dom.viewport.w, 1e5), h: whole(dom.viewport.h, 1e6) }
627
+ }
628
+ };
629
+ if (anchor.field && anchor.field.recordRef === recordRef) {
630
+ out.field = {
631
+ key: anchor.field.key,
632
+ ...anchor.field.locale ? { locale: anchor.field.locale } : {}
633
+ };
634
+ }
635
+ return out;
636
+ }
637
+ function recordKeys(recordRef) {
638
+ const parsed = parseRecordRef(recordRef);
639
+ if (!parsed) return {};
640
+ return parsed.kind === "document" ? { document: parsed.key } : { collection: parsed.collection, slug: parsed.slug };
641
+ }
642
+ function buildCreateBody(input) {
643
+ const proposal = input.isChangeRequest ? input.proposal : null;
644
+ const comment = input.comment.trim();
645
+ const body = {
646
+ ...recordKeys(input.recordRef),
647
+ isChangeRequest: input.isChangeRequest,
648
+ pageUrl: input.pageUrl,
649
+ anchors: input.anchors.slice(0, 20),
650
+ body: (comment || (proposal ? String(proposal.value) : "")).slice(0, 1e4)
651
+ };
652
+ if (proposal) Object.assign(body, proposedPart(proposal));
653
+ return body;
654
+ }
655
+ function buildDraftBody(input) {
656
+ return {
657
+ ...recordKeys(input.recordRef),
658
+ pageUrl: input.pageUrl,
659
+ anchors: input.anchors.slice(0, 20),
660
+ body: (input.display ?? String(input.proposal.value ?? "")).slice(0, 1e4) || " ",
661
+ ...proposedPart(input.proposal)
662
+ };
663
+ }
664
+ function proposedPart(proposal) {
665
+ const locale = proposal.locale;
666
+ return {
667
+ ...locale ? { locale } : {},
668
+ proposedValues: [
669
+ { fieldKey: proposal.fieldKey, ...locale ? { locale } : {}, value: proposal.value }
670
+ ]
671
+ };
672
+ }
673
+ function sameValue(a, b) {
674
+ if (typeof a === "number" || typeof b === "number") return a === b;
675
+ return normalizeText(a) === normalizeText(b);
676
+ }
677
+ var LINE_BLOCKS = /* @__PURE__ */ new Set(["div", "p", "li"]);
678
+ function editableText(element) {
679
+ let out = "";
680
+ const walk = (node) => {
681
+ for (let child = node.firstChild; child; child = child.nextSibling) {
682
+ if (child.nodeType === 3) {
683
+ out += child.data;
684
+ } else if (child.nodeType === 1) {
685
+ const name = child.localName;
686
+ if (name === "br") {
687
+ out += "\n";
688
+ continue;
689
+ }
690
+ if (LINE_BLOCKS.has(name) && out !== "" && !out.endsWith("\n")) out += "\n";
691
+ walk(child);
692
+ }
693
+ }
694
+ };
695
+ walk(element);
696
+ return out;
697
+ }
698
+ function parseProposed(type, raw) {
699
+ raw = cleanStega(raw);
700
+ if (type === "NUMBER") {
701
+ const text = raw.trim().replace(",", ".");
702
+ const value = Number(text);
703
+ return text !== "" && Number.isFinite(value) ? { ok: true, value } : { ok: false };
704
+ }
705
+ if (type === "TEXT" || type === "URL") return { ok: true, value: normalizeText(raw) };
706
+ return { ok: true, value: raw.replace(/\r\n?/g, "\n").trim() };
707
+ }
708
+ function spotAnchor(x, y, doc, win) {
709
+ const root = doc.documentElement;
710
+ const w = Math.max(root.scrollWidth, 1);
711
+ const h = Math.max(root.scrollHeight, 1);
712
+ return {
713
+ spot: { x: unit((x + win.scrollX) / w), y: unit((y + win.scrollY) / h) },
714
+ viewport: { w: whole(w, 1e5), h: whole(h, 1e6) }
715
+ };
716
+ }
717
+ function onPage(anchor) {
718
+ return !!(anchor?.spot || anchor?.rect || anchor?.tag || anchor?.selector);
719
+ }
720
+ function pinsFor(threads, index, doc, win) {
721
+ const numbers = /* @__PURE__ */ new Map();
722
+ const pins = [];
723
+ const numbered = threads.filter((thread) => Array.isArray(thread.anchors) && thread.anchors.some(onPage)).sort(
724
+ (a, b) => String(a.createdAt).localeCompare(String(b.createdAt)) || a.id.localeCompare(b.id)
725
+ );
726
+ numbered.forEach((thread, i) => {
727
+ const n = i + 1;
728
+ numbers.set(thread.id, n);
729
+ const spot = thread.anchors.find((anchor) => anchor?.spot)?.spot;
730
+ if (spot && Number.isFinite(spot.x) && Number.isFinite(spot.y)) {
731
+ pins.push({ id: thread.id, n, spot: { x: unit(spot.x), y: unit(spot.y) } });
732
+ return;
733
+ }
734
+ for (const anchor of thread.anchors) {
735
+ if (!onPage(anchor) || anchor.spot) continue;
736
+ const element = findAnchorElement(asDomAnchor(anchor), index, doc, win);
737
+ if (element && element !== doc.body && element !== doc.documentElement) {
738
+ pins.push({ id: thread.id, n, element });
739
+ return;
740
+ }
741
+ }
742
+ });
743
+ return { pins: pins.slice(0, 50), numbers };
744
+ }
745
+ var NOWHERE = { x: -1, y: -1, w: 0, h: 0 };
746
+ function asDomAnchor(anchor) {
747
+ const tag = typeof anchor?.tag === "string" && anchor.tag.length <= 500 ? anchor.tag : null;
748
+ const selector = typeof anchor?.selector === "string" && anchor.selector.length <= 1024 ? anchor.selector : null;
749
+ return { tag, selector, textQuote: null, rect: NOWHERE, viewport: { w: 1, h: 1 } };
750
+ }
751
+
752
+ // src/preview/site/session.ts
753
+ var REFRESH_LEAD_MS = 2 * 6e4;
754
+ function createTokenSession(ctx, stored, hooks, deps = {}) {
755
+ const { win, doc } = ctx;
756
+ const now = deps.now ?? (() => Date.now());
757
+ const setT = deps.setTimeout ?? ((fn, ms) => win.setTimeout(fn, ms));
758
+ const clearT = deps.clearTimeout ?? ((handle) => win.clearTimeout(handle));
759
+ let current = stored;
760
+ let expired = false;
761
+ let destroyed = false;
762
+ let inflight = null;
763
+ let timer = null;
764
+ function markExpired() {
765
+ if (expired || destroyed) return;
766
+ expired = true;
767
+ stop();
768
+ clearToken(win);
769
+ hooks.onExpired();
770
+ }
771
+ async function renew() {
772
+ if (!refreshLive(current, now()) || !current.refresh) return false;
773
+ let response;
774
+ try {
775
+ response = await win.fetch(`${ctx.authOrigin}/api/oauth/token`, {
776
+ method: "POST",
777
+ credentials: "omit",
778
+ body: new URLSearchParams({
779
+ grant_type: "refresh_token",
780
+ refresh_token: current.refresh,
781
+ client_id: ctx.clientId
782
+ })
783
+ });
784
+ } catch {
785
+ return false;
786
+ }
787
+ if (!response.ok) return false;
788
+ try {
789
+ const next = await readTokenAnswer(response, ctx.siteId, now());
790
+ current = next;
791
+ writeToken(win, next);
792
+ return true;
793
+ } catch {
794
+ return false;
795
+ }
796
+ }
797
+ function refresh() {
798
+ if (expired || destroyed) return Promise.resolve(false);
799
+ inflight ?? (inflight = renew().finally(() => {
800
+ inflight = null;
801
+ if (!destroyed && !expired) schedule();
802
+ }));
803
+ return inflight;
804
+ }
805
+ async function check() {
806
+ if (expired || destroyed) return;
807
+ if (current.exp - REFRESH_LEAD_MS > now()) {
808
+ schedule();
809
+ return;
810
+ }
811
+ const ok = await refresh();
812
+ if (!ok && !accessLive(current, now())) markExpired();
813
+ else if (!ok) {
814
+ stop();
815
+ timer = setT(() => void lastTry(), Math.max(0, current.exp - EXPIRY_MARGIN_MS - now()));
816
+ }
817
+ }
818
+ async function lastTry() {
819
+ if (!await refresh()) markExpired();
820
+ }
821
+ function stop() {
822
+ if (timer !== null) clearT(timer);
823
+ timer = null;
824
+ }
825
+ function schedule() {
826
+ stop();
827
+ if (expired || destroyed) return;
828
+ timer = setT(() => void check(), Math.max(0, current.exp - REFRESH_LEAD_MS - now()));
829
+ }
830
+ const onVisible = () => {
831
+ if (doc.visibilityState === "visible") void check();
832
+ };
833
+ doc.addEventListener("visibilitychange", onVisible);
834
+ async function ensure() {
835
+ if (expired) return null;
836
+ if (accessLive(current, now())) return current.token;
837
+ if (await refresh()) return current.token;
838
+ markExpired();
839
+ return null;
840
+ }
841
+ async function call2(request) {
842
+ const token = await ensure();
843
+ if (token === null) return { ok: false, kind: "expired" };
844
+ const first = await request(token);
845
+ if (first.ok || first.kind !== "expired") return first;
846
+ if (await refresh()) {
847
+ const second = await request(current.token);
848
+ if (!second.ok && second.kind === "expired") markExpired();
849
+ return second;
850
+ }
851
+ markExpired();
852
+ return first;
853
+ }
854
+ void check();
855
+ return {
856
+ token: () => current.token,
857
+ get expired() {
858
+ return expired;
859
+ },
860
+ ensure,
861
+ refresh,
862
+ call: call2,
863
+ destroy() {
864
+ destroyed = true;
865
+ stop();
866
+ doc.removeEventListener("visibilitychange", onVisible);
867
+ }
868
+ };
869
+ }
870
+ var SESSION_MESSAGES = {
871
+ es: {
872
+ expiredTitle: "Tu sesi\xF3n de Zap expir\xF3",
873
+ expiredBody: "Vuelve a entrar para seguir editando y comentando.",
874
+ signInAgain: "Volver a entrar"
875
+ },
876
+ en: {
877
+ expiredTitle: "Your Zap session expired",
878
+ expiredBody: "Sign in again to keep editing and commenting.",
879
+ signInAgain: "Sign in again"
880
+ }
881
+ };
882
+
883
+ // src/preview/site/suggest.ts
884
+ var OVERLAY_MODE = {
885
+ navigate: "off",
886
+ edit: "inspect",
887
+ comment: "select"
888
+ };
889
+ var PENDING_PIN = "eelzap:pending";
890
+ var LARGE_SHARE = 0.5;
891
+ var TOAST_MS = 8e3;
892
+ var RELOAD_AFTER_SAVE_MS = 700;
893
+ var SAVED_MS = 4e3;
894
+ function start(ctx, options = {}) {
895
+ const { win, doc } = ctx;
896
+ const stored = readSession(win, ctx.siteId);
897
+ if (!stored) {
898
+ void ctx.open("signin", { reason: "expired" });
899
+ return { destroy() {
900
+ } };
901
+ }
902
+ const auth = createTokenSession(ctx, stored, { onExpired: () => expire() });
903
+ const sm = SESSION_MESSAGES[ctx.locale];
904
+ const m = SUGGEST_MESSAGES[ctx.locale];
905
+ const dm = DRAFT_SESSION_MESSAGES[ctx.locale];
906
+ let reopenEdit = takeEditIntent(win);
907
+ let retryDraft = !reopenEdit;
908
+ const host = mountHost(doc, CSS + DRAG_CSS);
909
+ const layer = el(doc, "div", { class: "layer" });
910
+ const marks = el(doc, "div", { class: "marks" });
911
+ const bottom = el(doc, "div", { class: "bottom" });
912
+ const tip = el(doc, "div", { class: "tip", attrs: { role: "tooltip" } });
913
+ layer.append(marks, bottom, tip);
914
+ const drag = barDrag(win, doc, bottom, m.move, m.moveTip);
915
+ const key = ctx.shortcut ? ctx.shortcut.toUpperCase() : null;
916
+ host.root.append(layer);
917
+ let mode = "navigate";
918
+ let selection = [];
919
+ let data = null;
920
+ let refused = false;
921
+ let expired = false;
922
+ let composer = null;
923
+ let editing = null;
924
+ let list = null;
925
+ let toast = null;
926
+ let info = null;
927
+ let toastTimer = null;
928
+ let status = null;
929
+ let statusTimer = null;
930
+ let frame = null;
931
+ let destroyed = false;
932
+ let pins = [];
933
+ let numbers = /* @__PURE__ */ new Map();
934
+ let lastClick = { x: 0, y: 0, additive: false };
935
+ let menuOpen = false;
936
+ let focusNext = null;
937
+ let entering = false;
938
+ let paused = null;
939
+ const onClickFirst = (event) => {
940
+ lastClick = {
941
+ x: event.clientX,
942
+ y: event.clientY,
943
+ additive: event.shiftKey || event.metaKey || event.ctrlKey
944
+ };
945
+ };
946
+ win.addEventListener("click", onClickFirst, true);
947
+ const onPressFirst = (event) => {
948
+ if (event.target === host.host) return;
949
+ if (menuOpen) setMenu(false);
950
+ if (editing?.card && !editing.done) commitEdit(editing);
951
+ if (info) closeInfo();
952
+ };
953
+ win.addEventListener("pointerdown", onPressFirst, true);
954
+ layer.addEventListener("pointerdown", (event) => {
955
+ if (!menuOpen) return;
956
+ const keep = Array.from(bottom.querySelectorAll('.menu, [data-action="account"]'));
957
+ if (!keep.some((node) => node.contains(event.target))) setMenu(false);
958
+ });
959
+ const index = new TagIndex(doc);
960
+ index.scan();
961
+ const overlay = new Overlay({
962
+ doc,
963
+ win,
964
+ index,
965
+ pinsWhenOff: true,
966
+ ignore: (target) => target === host.host,
967
+ onInspect: (tagged) => beginEdit(tagged),
968
+ onSelect: (elements) => onSelect(elements),
969
+ onPin: ({ id }) => {
970
+ if (id === PENDING_PIN) return;
971
+ const found = data?.comments.find((comment) => comment.id === id);
972
+ if (!found) return;
973
+ list = [found];
974
+ if (drag.hidden()) setHidden(false, false);
975
+ else renderBottom();
976
+ }
977
+ });
978
+ overlay.mount();
979
+ doc.documentElement.appendChild(host.host);
980
+ const check = tagGuard();
981
+ let stopObserving = () => {
982
+ };
983
+ const atBoot = check(index.elements.length, true);
984
+ if (atBoot) {
985
+ paused = atBoot;
986
+ console.warn(`eelzap: preview paused (${atBoot})`);
987
+ } else
988
+ stopObserving = observeTags(
989
+ doc.documentElement,
990
+ (page) => {
991
+ index.scan();
992
+ const reason = check(index.elements.length, page);
993
+ if (reason) pause(reason);
994
+ else placePins();
995
+ },
996
+ {
997
+ ignore: (node) => {
998
+ const own = overlay.hostElement;
999
+ return node === host.host || !!own && (node === own || own.contains(node));
1000
+ }
1001
+ }
1002
+ );
1003
+ function pause(reason) {
1004
+ if (paused) return;
1005
+ paused = reason;
1006
+ stopObserving();
1007
+ console.warn(`eelzap: preview paused (${reason})`);
1008
+ if (destroyed) return;
1009
+ overlay.setPins([]);
1010
+ setMode("navigate");
1011
+ }
1012
+ async function load() {
1013
+ const refs = Array.from(new Set(index.elements.map((tagged) => tagged.recordRef)));
1014
+ const result = await auth.call((token) => fetchOnPage(ctx, token, currentPageUrl(win), refs));
1015
+ if (destroyed) return;
1016
+ if (!result.ok) {
1017
+ onFailure(result);
1018
+ return;
1019
+ }
1020
+ const first = data === null;
1021
+ data = result.data;
1022
+ const labels = {};
1023
+ const foreign = {};
1024
+ for (const [ref, fields] of Object.entries(data.fields ?? {})) {
1025
+ const name = data.records?.[ref];
1026
+ for (const [key2, field] of Object.entries(fields)) {
1027
+ if (typeof field?.label !== "string") continue;
1028
+ if (name) foreign[`${ref}#${key2}`] = [field.label, m.inRecord(name)];
1029
+ else labels[key2] = field.label;
1030
+ }
1031
+ }
1032
+ overlay.setLabels(labels, foreign);
1033
+ if (!data.liveEditing && mode === "edit") setMode("navigate");
1034
+ if (reopenEdit) {
1035
+ reopenEdit = false;
1036
+ if (canEdit()) setMode("edit");
1037
+ }
1038
+ if (first && options.reason === "renewed") {
1039
+ const name = data.viewer?.name;
1040
+ setToast({ title: m.renewed, body: name ? m.renewedAs(name) : m.renewedBody });
1041
+ }
1042
+ renderBottom();
1043
+ placePins();
1044
+ }
1045
+ function onFailure(failure, during = "read") {
1046
+ if (failure.kind === "expired") {
1047
+ expire();
1048
+ return;
1049
+ }
1050
+ if (failure.kind === "forbidden" && during === "edit") {
1051
+ if (data) data.liveEditing = false;
1052
+ setMode("navigate");
1053
+ setToast({ title: m.editOff, body: m.editOffBody, error: true });
1054
+ return;
1055
+ }
1056
+ if (failure.kind === "refused" || failure.kind === "forbidden") {
1057
+ refused = true;
1058
+ closeComposer();
1059
+ overlay.setMode("off");
1060
+ overlay.setPins([]);
1061
+ renderBottom();
1062
+ return;
1063
+ }
1064
+ const message = failure.kind === "rate" ? m.rateLimited(failure.retryAfter) : m.failed;
1065
+ if (during === "comment" && composer) showError(message);
1066
+ else
1067
+ setToast({ title: during === "edit" ? m.saveFailed : m.failed, body: message, error: true });
1068
+ }
1069
+ function expire() {
1070
+ clearToken(win);
1071
+ if (expired || destroyed) return;
1072
+ expired = true;
1073
+ closeComposer();
1074
+ overlay.setMode("off");
1075
+ renderBottom();
1076
+ }
1077
+ function signInAgain() {
1078
+ void ctx.open("signin", { reason: "expired", signIn: true });
1079
+ }
1080
+ function canEdit() {
1081
+ return !!data?.liveEditing;
1082
+ }
1083
+ function setMode(next) {
1084
+ if (next === "edit" && !canEdit() || paused && next !== "navigate") return;
1085
+ if (status === "preparing") return;
1086
+ if (next === "edit" && !index.elements.length && (retryDraft || !inDraftSession(win))) {
1087
+ retryDraft = false;
1088
+ void prepareEdit();
1089
+ return;
1090
+ }
1091
+ endEdit();
1092
+ closeComposer();
1093
+ closeInfo();
1094
+ mode = next;
1095
+ selection = [];
1096
+ overlay.clearSelection();
1097
+ overlay.setMode(OVERLAY_MODE[next]);
1098
+ if (next === "edit" && !index.elements.length)
1099
+ setToast({ title: m.noFields, body: m.noFieldsBody });
1100
+ renderBottom();
1101
+ }
1102
+ async function prepareEdit() {
1103
+ setStatus("preparing");
1104
+ const token = await auth.ensure();
1105
+ if (token === null || destroyed) return;
1106
+ const result = await requestDraftSession(ctx, token);
1107
+ if (destroyed) return;
1108
+ if (result.ok) {
1109
+ win.location.assign(result.url);
1110
+ return;
1111
+ }
1112
+ setStatus(null);
1113
+ if (result.reason === "expired") {
1114
+ expire();
1115
+ return;
1116
+ }
1117
+ const body = result.reason === "no-route" ? dm.noRoute : result.reason === "rate" ? dm.rate(result.retryAfter) : dm.failed;
1118
+ setToast({ title: m.noFields, body, error: result.reason !== "no-route" });
1119
+ }
1120
+ function renderBottom() {
1121
+ if (destroyed) return;
1122
+ hideTip();
1123
+ const active = host.root.activeElement?.getAttribute("data-action");
1124
+ const hidden = drag.hidden();
1125
+ const children = [];
1126
+ if (toast) children.push(toastCard(toast));
1127
+ if (expired) {
1128
+ children.push(expiredCard());
1129
+ bottom.replaceChildren(...children);
1130
+ drag.place();
1131
+ marks.replaceChildren();
1132
+ return;
1133
+ }
1134
+ if (refused) {
1135
+ children.push(refusedCard());
1136
+ bottom.replaceChildren(...children);
1137
+ drag.place();
1138
+ marks.replaceChildren();
1139
+ return;
1140
+ }
1141
+ if (paused && !hidden) children.push(pausedCard(paused));
1142
+ if (list && !hidden) children.push(listCard(list));
1143
+ if (selection.length > 0 && !composer && !hidden) children.push(trayCard());
1144
+ if (status) {
1145
+ children.push(
1146
+ el(doc, "div", { class: "card status", attrs: { role: "status" } }, [
1147
+ status === "saved" ? icon(doc, "check", 14) : null,
1148
+ status === "saved" ? m.saved : status === "preparing" ? dm.preparing : m.saving
1149
+ ])
1150
+ );
1151
+ }
1152
+ if (menuOpen && !hidden) children.push(menuCard());
1153
+ const bar = hidden ? pill() : toolbar();
1154
+ if (entering) bar.setAttribute("data-enter", "");
1155
+ entering = false;
1156
+ children.push(bar);
1157
+ bottom.replaceChildren(...children);
1158
+ drag.place();
1159
+ drawMarks();
1160
+ const want = focusNext ?? active;
1161
+ focusNext = null;
1162
+ if (want)
1163
+ bottom.querySelector(`[data-action="${want}"]`)?.focus({ preventScroll: true });
1164
+ }
1165
+ function openCount() {
1166
+ const open = data?.comments.length ?? 0;
1167
+ return [open, data?.truncated ? `${open}+` : String(open)];
1168
+ }
1169
+ function setMenu(open, focus = null) {
1170
+ if (menuOpen === open && !focus) return;
1171
+ menuOpen = open;
1172
+ focusNext = focus;
1173
+ renderBottom();
1174
+ }
1175
+ function setHidden(value, fromUi) {
1176
+ if (refused || drag.hidden() === value) return;
1177
+ if (value) {
1178
+ endEdit();
1179
+ closeComposer();
1180
+ selection = [];
1181
+ overlay.clearSelection();
1182
+ list = null;
1183
+ menuOpen = false;
1184
+ }
1185
+ drag.setHidden(value);
1186
+ overlay.setMode(value ? "off" : OVERLAY_MODE[mode]);
1187
+ focusNext = fromUi ? value ? "show" : "hide" : null;
1188
+ entering = true;
1189
+ renderBottom();
1190
+ }
1191
+ function withTip(target, text, end) {
1192
+ const show = () => {
1193
+ tip.replaceChildren(text);
1194
+ if (key) tip.append(kbd(doc, "Shift", key));
1195
+ tip.classList.add("on");
1196
+ const rect = target.getBoundingClientRect();
1197
+ const vw = doc.documentElement.clientWidth || win.innerWidth;
1198
+ const w = tip.offsetWidth;
1199
+ const gap = end ? 15 : 8;
1200
+ const x = end ? rect.right + 7 - w : rect.left + (rect.width - w) / 2;
1201
+ tip.style.left = `${Math.min(Math.max(x, 8), vw - w - 8)}px`;
1202
+ tip.style.top = bottom.hasAttribute("data-below") ? `${rect.bottom + gap}px` : `${rect.top - gap - tip.offsetHeight}px`;
1203
+ };
1204
+ const focus = () => {
1205
+ try {
1206
+ if (target.matches(":focus-visible")) show();
1207
+ } catch {
1208
+ show();
1209
+ }
1210
+ };
1211
+ for (const [type, handler] of [
1212
+ ["mouseenter", show],
1213
+ ["focus", focus],
1214
+ ["mouseleave", hideTip],
1215
+ ["blur", hideTip],
1216
+ ["pointerdown", hideTip]
1217
+ ]) {
1218
+ target.addEventListener(type, handler);
1219
+ }
1220
+ if (key) target.setAttribute("aria-keyshortcuts", `Shift+${key}`);
1221
+ return target;
1222
+ }
1223
+ function hideTip() {
1224
+ tip.classList.remove("on");
1225
+ }
1226
+ function avatar(name, large = false) {
1227
+ return el(doc, "span", {
1228
+ class: large ? "avatar lg" : "avatar",
1229
+ attrs: { "aria-hidden": "true" },
1230
+ text: initials(name)
1231
+ });
1232
+ }
1233
+ function menuCard() {
1234
+ const name = data?.viewer?.name ?? null;
1235
+ const email = data?.viewer?.email ?? null;
1236
+ const item = (action, label, glyph, run) => el(
1237
+ doc,
1238
+ "button",
1239
+ {
1240
+ class: "mi",
1241
+ attrs: { type: "button", role: "menuitem", tabindex: "-1", "data-action": action },
1242
+ on: {
1243
+ click: run,
1244
+ pointerenter: (event) => event.currentTarget.focus()
1245
+ }
1246
+ },
1247
+ [icon(doc, glyph), label]
1248
+ );
1249
+ return el(
1250
+ doc,
1251
+ "div",
1252
+ {
1253
+ class: "card menu",
1254
+ attrs: { role: "menu", "aria-label": m.accountMenu, tabindex: "-1", "data-action": "menu" },
1255
+ on: { keydown: onMenuKey }
1256
+ },
1257
+ [
1258
+ el(doc, "div", { class: "menu-head" }, [
1259
+ avatar(name, true),
1260
+ el(doc, "div", { class: "menu-who" }, [
1261
+ el(doc, "span", { class: "menu-name", text: name ?? m.someone }),
1262
+ email ? el(doc, "span", { class: "menu-mail", text: email }) : null
1263
+ ])
1264
+ ]),
1265
+ el(doc, "div", { class: "sep", attrs: { role: "separator" } }),
1266
+ item("open-zap", m.openInZap, "external", () => {
1267
+ setMenu(false, "account");
1268
+ win.open(zapUrl(data?.viewer?.editorUrl, ctx.zapOrigin), "_blank", "noopener");
1269
+ }),
1270
+ el(doc, "div", { class: "sep", attrs: { role: "separator" } }),
1271
+ item("sign-out", m.signOut, "exit", exit)
1272
+ ]
1273
+ );
1274
+ }
1275
+ function onMenuKey(event) {
1276
+ const e = event;
1277
+ if (e.key === "Tab") {
1278
+ setMenu(false, "account");
1279
+ return;
1280
+ }
1281
+ const items = Array.from(bottom.querySelectorAll('.menu [role="menuitem"]'));
1282
+ const at = items.indexOf(host.root.activeElement);
1283
+ const to = { ArrowDown: at + 1, ArrowUp: at < 0 ? -1 : at - 1, Home: 0, End: -1 }[e.key];
1284
+ if (to === void 0) return;
1285
+ e.preventDefault();
1286
+ items[(to + items.length) % items.length]?.focus();
1287
+ }
1288
+ function pill() {
1289
+ const [open, shown] = openCount();
1290
+ return withTip(
1291
+ drag.grab(
1292
+ el(
1293
+ doc,
1294
+ "button",
1295
+ {
1296
+ class: "card pill",
1297
+ attrs: {
1298
+ type: "button",
1299
+ "aria-label": m.showBar(open).replace(String(open), shown) + (paused ? `. ${m.paused}` : ""),
1300
+ "data-action": "show"
1301
+ },
1302
+ on: { click: () => setHidden(false, true) }
1303
+ },
1304
+ [
1305
+ icon(doc, "zap", 20),
1306
+ el(doc, "span", { class: "pill-count" }, [icon(doc, "thread", 14), shown])
1307
+ ]
1308
+ )
1309
+ ),
1310
+ m.show,
1311
+ false
1312
+ );
1313
+ }
1314
+ function toolbar() {
1315
+ const [open, shown] = openCount();
1316
+ const name = data?.viewer?.name ?? null;
1317
+ const tool = (value, label, glyph) => {
1318
+ const off = value === "edit" && !canEdit() || !!paused && value !== "navigate";
1319
+ const why = paused && off ? m.paused : off && data ? m.editOff : null;
1320
+ return el(
1321
+ doc,
1322
+ "button",
1323
+ {
1324
+ class: "seg",
1325
+ attrs: {
1326
+ type: "button",
1327
+ "aria-pressed": String(mode === value),
1328
+ "aria-disabled": off ? "true" : null,
1329
+ title: why,
1330
+ "data-tip": why,
1331
+ "data-mode": value
1332
+ },
1333
+ on: { click: () => setMode(value) }
1334
+ },
1335
+ [icon(doc, glyph, 14), label]
1336
+ );
1337
+ };
1338
+ return el(
1339
+ doc,
1340
+ "div",
1341
+ { class: "card toolbar", attrs: { role: "toolbar", "aria-label": m.toolbar } },
1342
+ [
1343
+ drag.handle,
1344
+ el(doc, "span", { class: "divider", attrs: { "aria-hidden": "true" } }),
1345
+ icon(doc, "zap", 20),
1346
+ el(doc, "div", { class: "segments", attrs: { role: "group", "aria-label": m.tools } }, [
1347
+ tool("navigate", m.navigate, "pointer"),
1348
+ tool("edit", m.edit, "pencil"),
1349
+ tool("comment", m.comment, "commentPlus")
1350
+ ]),
1351
+ el(doc, "span", { class: "divider", attrs: { "aria-hidden": "true" } }),
1352
+ el(
1353
+ doc,
1354
+ "button",
1355
+ {
1356
+ class: "tool strong",
1357
+ attrs: { type: "button", "aria-expanded": String(!!list), "data-action": "open-list" },
1358
+ on: {
1359
+ click: () => {
1360
+ list = list ? null : data?.comments ?? [];
1361
+ renderBottom();
1362
+ }
1363
+ }
1364
+ },
1365
+ [icon(doc, "thread"), m.open(open).replace(String(open), shown)]
1366
+ ),
1367
+ el(doc, "span", { class: "divider", attrs: { "aria-hidden": "true" } }),
1368
+ el(
1369
+ doc,
1370
+ "button",
1371
+ {
1372
+ class: "tool account",
1373
+ attrs: {
1374
+ type: "button",
1375
+ "aria-haspopup": "menu",
1376
+ "aria-expanded": String(menuOpen),
1377
+ "aria-label": name ? m.account(name) : m.accountMenu,
1378
+ "data-action": "account"
1379
+ },
1380
+ on: {
1381
+ // A pointer opens onto the menu itself; Enter or Space onto its first item.
1382
+ click: (event) => setMenu(
1383
+ !menuOpen,
1384
+ menuOpen ? "account" : event.detail ? "menu" : "open-zap"
1385
+ ),
1386
+ keydown: (event) => {
1387
+ const k = event.key;
1388
+ if (k !== "ArrowDown" && k !== "ArrowUp") return;
1389
+ event.preventDefault();
1390
+ setMenu(true, k === "ArrowUp" ? "sign-out" : "open-zap");
1391
+ }
1392
+ }
1393
+ },
1394
+ [avatar(name), icon(doc, "chevron", 13)]
1395
+ ),
1396
+ withTip(
1397
+ el(
1398
+ doc,
1399
+ "button",
1400
+ {
1401
+ class: "tool hide",
1402
+ attrs: { type: "button", "aria-label": m.hide, "data-action": "hide" },
1403
+ on: { click: () => setHidden(true, true) }
1404
+ },
1405
+ [icon(doc, "eyeOff", 16)]
1406
+ ),
1407
+ m.hide,
1408
+ true
1409
+ )
1410
+ ]
1411
+ );
1412
+ }
1413
+ function trayCard() {
1414
+ return el(doc, "div", { class: "card tray", attrs: { role: "status" } }, [
1415
+ el(doc, "span", { text: m.selected(selection.length) }),
1416
+ el(doc, "button", {
1417
+ class: "btn btn-outline",
1418
+ text: m.clearSelection,
1419
+ attrs: { type: "button" },
1420
+ on: {
1421
+ click: () => {
1422
+ selection = [];
1423
+ overlay.clearSelection();
1424
+ renderBottom();
1425
+ }
1426
+ }
1427
+ }),
1428
+ el(doc, "button", {
1429
+ class: "btn btn-primary",
1430
+ text: m.comment,
1431
+ attrs: { type: "button", "data-action": "comment-selection" },
1432
+ on: {
1433
+ click: () => {
1434
+ const elements = selection;
1435
+ selection = [];
1436
+ overlay.clearSelection();
1437
+ openComposer(elements, null);
1438
+ }
1439
+ }
1440
+ })
1441
+ ]);
1442
+ }
1443
+ function listCard(comments) {
1444
+ return el(
1445
+ doc,
1446
+ "div",
1447
+ { class: "card list sheet", attrs: { role: "dialog", "aria-label": m.openTitle } },
1448
+ [
1449
+ el(doc, "div", { class: "head" }, [
1450
+ el(doc, "h2", { text: m.openTitle }),
1451
+ closeButton(m.dismiss, () => {
1452
+ list = null;
1453
+ renderBottom();
1454
+ })
1455
+ ]),
1456
+ comments.length === 0 ? el(doc, "p", { class: "muted", text: m.noneOpen }) : el(
1457
+ doc,
1458
+ "ul",
1459
+ {},
1460
+ comments.map((comment) => {
1461
+ const n = numbers.get(comment.id);
1462
+ return el(doc, "li", {}, [
1463
+ el(
1464
+ doc,
1465
+ "button",
1466
+ {
1467
+ class: "item",
1468
+ attrs: { type: "button", "data-comment": comment.id },
1469
+ on: { click: () => reveal2(comment.id) }
1470
+ },
1471
+ [
1472
+ n ? el(doc, "span", { class: "num", text: String(n) }) : null,
1473
+ el(doc, "span", { class: "item-text" }, [
1474
+ el(doc, "span", { class: "who", text: comment.author?.name || m.someone }),
1475
+ comment.isChangeRequest ? el(doc, "span", { class: "flag", text: m.changeRequest }) : null,
1476
+ el(doc, "span", { class: "what", text: comment.excerpt ?? "" })
1477
+ ])
1478
+ ]
1479
+ )
1480
+ ]);
1481
+ })
1482
+ )
1483
+ ]
1484
+ );
1485
+ }
1486
+ function reveal2(id) {
1487
+ const pin = pins.find((entry) => entry.id === id);
1488
+ if (pin?.element) reveal(win, pin.element);
1489
+ else if (pin?.spot) {
1490
+ const root = doc.documentElement;
1491
+ win.scrollTo?.({
1492
+ top: Math.max(0, pin.spot.y * root.scrollHeight - win.innerHeight / 2),
1493
+ behavior: "smooth"
1494
+ });
1495
+ }
1496
+ }
1497
+ function toastCard(value) {
1498
+ return el(
1499
+ doc,
1500
+ "div",
1501
+ {
1502
+ class: value.error ? "card toast toast-error" : "card toast",
1503
+ attrs: { role: "status", "aria-live": "polite" }
1504
+ },
1505
+ [
1506
+ icon(doc, value.error ? "alert" : "check", 18),
1507
+ el(doc, "div", { class: "toast-text" }, [
1508
+ el(doc, "span", { class: "title", text: value.title }),
1509
+ el(doc, "span", { class: "muted", text: value.body })
1510
+ ]),
1511
+ closeButton(m.dismiss, () => setToast(null))
1512
+ ]
1513
+ );
1514
+ }
1515
+ function pausedCard(reason) {
1516
+ return el(doc, "div", { class: "card refused paused", attrs: { role: "status" } }, [
1517
+ icon(doc, "alert", 18),
1518
+ el(doc, "div", { class: "toast-text" }, [
1519
+ el(doc, "span", { class: "title", text: m.paused }),
1520
+ el(doc, "span", {
1521
+ class: "muted",
1522
+ text: reason === "cap" ? m.pausedCap : m.pausedRunaway
1523
+ })
1524
+ ]),
1525
+ el(doc, "button", {
1526
+ class: "tool",
1527
+ text: m.reload,
1528
+ attrs: { type: "button", "data-action": "reload" },
1529
+ on: { click: () => win.location.reload() }
1530
+ })
1531
+ ]);
1532
+ }
1533
+ function expiredCard() {
1534
+ return el(doc, "div", { class: "card refused", attrs: { role: "alert" } }, [
1535
+ icon(doc, "alert", 18),
1536
+ el(doc, "div", { class: "toast-text" }, [
1537
+ el(doc, "span", { class: "title", text: sm.expiredTitle }),
1538
+ el(doc, "span", { class: "muted", text: sm.expiredBody })
1539
+ ]),
1540
+ el(
1541
+ doc,
1542
+ "button",
1543
+ {
1544
+ class: "tool",
1545
+ attrs: { type: "button", "data-action": "sign-in-again" },
1546
+ on: { click: signInAgain }
1547
+ },
1548
+ [icon(doc, "eel"), sm.signInAgain]
1549
+ )
1550
+ ]);
1551
+ }
1552
+ function refusedCard() {
1553
+ return el(doc, "div", { class: "card refused", attrs: { role: "alert" } }, [
1554
+ icon(doc, "alert", 18),
1555
+ el(doc, "div", { class: "toast-text" }, [
1556
+ el(doc, "span", { class: "title", text: m.refusedTitle }),
1557
+ el(doc, "span", { class: "muted", text: m.refusedBody })
1558
+ ]),
1559
+ el(
1560
+ doc,
1561
+ "button",
1562
+ { class: "tool", attrs: { type: "button", "data-action": "exit" }, on: { click: exit } },
1563
+ [icon(doc, "exit"), m.exit]
1564
+ )
1565
+ ]);
1566
+ }
1567
+ function closeButton(label, onClick2) {
1568
+ return el(
1569
+ doc,
1570
+ "button",
1571
+ {
1572
+ class: "iconbtn",
1573
+ attrs: { type: "button", "aria-label": label, title: label },
1574
+ on: { click: onClick2 }
1575
+ },
1576
+ [icon(doc, "x")]
1577
+ );
1578
+ }
1579
+ function setToast(value) {
1580
+ toast = value;
1581
+ if (toastTimer !== null) win.clearTimeout(toastTimer);
1582
+ toastTimer = value ? win.setTimeout(() => setToast(null), TOAST_MS) : null;
1583
+ renderBottom();
1584
+ }
1585
+ function setStatus(value) {
1586
+ status = value;
1587
+ if (statusTimer !== null) win.clearTimeout(statusTimer);
1588
+ statusTimer = value === "saved" ? win.setTimeout(() => setStatus(null), SAVED_MS) : null;
1589
+ renderBottom();
1590
+ }
1591
+ function exit() {
1592
+ void leaveDraftSession(ctx, auth.token(), index.elements.length > 0);
1593
+ clearToken(win);
1594
+ ctx.close();
1595
+ }
1596
+ function placePins() {
1597
+ if (destroyed || refused || paused || !data) return;
1598
+ const placed = pinsFor(data.comments ?? [], index, doc, win);
1599
+ pins = placed.pins;
1600
+ numbers = placed.numbers;
1601
+ overlay.setPins(composer?.pin ? [...pins, composer.pin] : pins);
1602
+ }
1603
+ function drawMarks() {
1604
+ const targets = editing ? [editing.element] : composer?.elements ?? [];
1605
+ marks.replaceChildren(
1606
+ ...targets.filter((element) => element.isConnected).map((element) => {
1607
+ const rect = element.getBoundingClientRect();
1608
+ const box = el(doc, "div", { class: "mark" });
1609
+ box.style.left = `${rect.left - 3}px`;
1610
+ box.style.top = `${rect.top - 3}px`;
1611
+ box.style.width = `${rect.width + 6}px`;
1612
+ box.style.height = `${rect.height + 6}px`;
1613
+ if (editing?.card)
1614
+ box.append(
1615
+ el(doc, "span", {
1616
+ class: "mark-chip",
1617
+ text: editing.field.label || editing.tagged.fieldKey
1618
+ })
1619
+ );
1620
+ return box;
1621
+ })
1622
+ );
1623
+ }
1624
+ function schedule() {
1625
+ if (frame !== null) return;
1626
+ const raf = win.requestAnimationFrame?.bind(win) ?? ((fn) => win.setTimeout(fn, 16));
1627
+ frame = raf(() => {
1628
+ frame = null;
1629
+ drawMarks();
1630
+ if (composer) position(composer.card, anchorRect(composer));
1631
+ if (editing?.card)
1632
+ position(editing.card, editing.element.getBoundingClientRect(), editing.card.offsetWidth);
1633
+ if (info) position(info.card, info.tagged.element.getBoundingClientRect(), 300);
1634
+ });
1635
+ }
1636
+ function position(card, rect, preferred = 420) {
1637
+ const vw = win.innerWidth;
1638
+ const vh = win.innerHeight;
1639
+ const width = Math.min(preferred, vw - 16);
1640
+ card.style.width = `${width}px`;
1641
+ const height = card.offsetHeight || 260;
1642
+ const bar = bottom.querySelector(".toolbar, .pill")?.getBoundingClientRect();
1643
+ const clampX = (x) => Math.min(Math.max(8, x), Math.max(8, vw - width - 8));
1644
+ const hits = (x, y) => !!bar && x < bar.right + 8 && x + width > bar.left - 8 && y < bar.bottom + 8 && y + height > bar.top - 8;
1645
+ const fits = (y) => y >= 8 && y + height <= vh - 8;
1646
+ let place = null;
1647
+ for (const y of [rect.bottom + 10, rect.top - height - 10]) {
1648
+ if (!fits(y)) continue;
1649
+ for (const x of [
1650
+ clampX(rect.left),
1651
+ clampX((bar?.right ?? 0) + 8),
1652
+ clampX((bar?.left ?? 0) - 8 - width)
1653
+ ]) {
1654
+ if (!hits(x, y)) {
1655
+ place = [x, y];
1656
+ break;
1657
+ }
1658
+ }
1659
+ if (place) break;
1660
+ }
1661
+ if (!place) {
1662
+ const y = fits(rect.bottom + 10) ? rect.bottom + 10 : Math.max(8, rect.top - height - 10);
1663
+ place = [clampX(rect.left), y];
1664
+ }
1665
+ card.style.left = `${place[0]}px`;
1666
+ card.style.top = `${place[1]}px`;
1667
+ }
1668
+ function onSelect(elements) {
1669
+ if (mode !== "comment" || composer) return;
1670
+ if (elements.length === 0) {
1671
+ if (selection.length > 0) {
1672
+ selection = [];
1673
+ renderBottom();
1674
+ }
1675
+ return;
1676
+ }
1677
+ if (lastClick.additive || elements.length > 1) {
1678
+ selection = elements;
1679
+ renderBottom();
1680
+ return;
1681
+ }
1682
+ const element = elements[0];
1683
+ overlay.clearSelection();
1684
+ if (isLarge(element)) openComposer([], { x: lastClick.x, y: lastClick.y }, element);
1685
+ else openComposer([element], null);
1686
+ }
1687
+ function isLarge(element) {
1688
+ const rect = element.getBoundingClientRect();
1689
+ const area = Math.max(1, win.innerWidth * win.innerHeight);
1690
+ return rect.width * rect.height / area >= LARGE_SHARE;
1691
+ }
1692
+ function anchorRect(session) {
1693
+ const first = session.elements[0];
1694
+ if (first) return first.getBoundingClientRect();
1695
+ const point = session.point ?? { x: 0, y: 0 };
1696
+ return { left: point.x, top: point.y, bottom: point.y };
1697
+ }
1698
+ function openComposer(elements, point, owner) {
1699
+ if (refused || elements.length === 0 && !point) return;
1700
+ const recordRef = recordFor(elements[0] ?? owner ?? doc.body, index);
1701
+ if (!recordRef) {
1702
+ setToast({ title: m.failed, body: m.noRecord, error: true });
1703
+ return;
1704
+ }
1705
+ list = null;
1706
+ const pageUrl = currentPageUrl(win);
1707
+ const anchors = point ? [spotAnchor(point.x, point.y, doc, win)] : elements.slice(0, 20).map((element) => toApiAnchor(buildAnchor(element, index, pageUrl, doc, win), recordRef));
1708
+ const single = elements.length === 1 ? elements[0] : null;
1709
+ const own = single ? index.get(single) : null;
1710
+ const tagged = own && own.recordRef === recordRef ? own : null;
1711
+ const field = fieldOf(data, tagged);
1712
+ const proposable = proposableField(tagged, field);
1713
+ const target = single;
1714
+ const before = field?.type === "URL" && target?.localName === "a" ? target.getAttribute("href") ?? "" : normalizeText(target?.textContent ?? "");
1715
+ const spotPin = point ? anchors[0] : null;
1716
+ const textarea = el(doc, "textarea", {
1717
+ class: "composer",
1718
+ attrs: {
1719
+ "aria-label": m.commentLabel,
1720
+ rows: "3",
1721
+ maxlength: "10000",
1722
+ placeholder: m.commentPlaceholder
1723
+ }
1724
+ });
1725
+ const error = el(doc, "p", { class: "error", attrs: { role: "alert" } });
1726
+ const sendButton = el(
1727
+ doc,
1728
+ "button",
1729
+ {
1730
+ class: "btn btn-primary",
1731
+ attrs: { type: "button", "data-action": "send" },
1732
+ on: { click: () => void submit() }
1733
+ },
1734
+ [icon(doc, "commentPlus", 16), m.comment]
1735
+ );
1736
+ const title = tagged ? el(doc, "span", { class: "chip" }, [
1737
+ icon(doc, "field", 12),
1738
+ field?.label || tagged.fieldKey
1739
+ ]) : el(doc, "h2", {
1740
+ text: elements.length > 1 ? m.selected(elements.length) : m.commentTitle
1741
+ });
1742
+ const extra = el(doc, "div", { class: "extra" });
1743
+ const card = el(
1744
+ doc,
1745
+ "div",
1746
+ { class: "card popover sheet", attrs: { role: "dialog", "aria-label": m.newComment } },
1747
+ [
1748
+ el(doc, "div", { class: "head" }, [title, closeButton(m.close, closeComposer)]),
1749
+ el(doc, "div", { class: "content" }, [
1750
+ textarea,
1751
+ extra,
1752
+ error,
1753
+ // A dialog footer: a full-bleed rule, Cancelar and the primary (OWNER-PASS 19).
1754
+ el(doc, "div", { class: "foot" }, [
1755
+ el(doc, "button", {
1756
+ class: "btn btn-outline",
1757
+ text: m.cancel,
1758
+ attrs: { type: "button" },
1759
+ on: { click: closeComposer }
1760
+ }),
1761
+ sendButton
1762
+ ])
1763
+ ])
1764
+ ]
1765
+ );
1766
+ layer.append(card);
1767
+ composer = {
1768
+ elements,
1769
+ point,
1770
+ pin: spotPin && "spot" in spotPin ? { id: PENDING_PIN, n: (numbers.size || 0) + 1, spot: spotPin.spot } : null,
1771
+ recordRef,
1772
+ pageUrl,
1773
+ anchors,
1774
+ tagged: proposable ? tagged : null,
1775
+ field: proposable ? field : null,
1776
+ before,
1777
+ card,
1778
+ textarea,
1779
+ request: false,
1780
+ proposing: false,
1781
+ proposal: null,
1782
+ error,
1783
+ sendButton,
1784
+ busy: false,
1785
+ idempotency: null
1786
+ };
1787
+ renderExtra(extra);
1788
+ textarea.addEventListener("input", refreshSend);
1789
+ textarea.addEventListener("keydown", (event) => {
1790
+ if (event.key !== "Enter" || event.shiftKey || event.isComposing || event.keyCode === 229) {
1791
+ return;
1792
+ }
1793
+ event.preventDefault();
1794
+ if (!sendButton.disabled) void submit();
1795
+ });
1796
+ overlay.setMode("off");
1797
+ placePins();
1798
+ refreshSend();
1799
+ renderBottom();
1800
+ position(card, anchorRect(composer));
1801
+ textarea.focus();
1802
+ }
1803
+ function renderExtra(extra) {
1804
+ const session = composer;
1805
+ if (!session) return;
1806
+ const toggle = el(
1807
+ doc,
1808
+ "button",
1809
+ {
1810
+ class: "check",
1811
+ attrs: {
1812
+ type: "button",
1813
+ role: "checkbox",
1814
+ "aria-checked": String(session.request),
1815
+ "data-action": "request-change"
1816
+ },
1817
+ on: {
1818
+ click: () => {
1819
+ session.request = !session.request;
1820
+ if (!session.request) {
1821
+ session.proposing = false;
1822
+ session.proposal = null;
1823
+ }
1824
+ renderExtra(extra);
1825
+ refreshSend();
1826
+ }
1827
+ }
1828
+ },
1829
+ [el(doc, "span", { class: "box", attrs: { "aria-hidden": "true" } }), m.requestChange]
1830
+ );
1831
+ const parts = [toggle];
1832
+ if (session.request) {
1833
+ parts.push(el(doc, "p", { class: "muted small", text: m.recipient }));
1834
+ parts.push(el(doc, "p", { class: "muted small", text: m.requestHelp }));
1835
+ if (session.tagged && session.field && !session.proposing) {
1836
+ parts.push(
1837
+ el(
1838
+ doc,
1839
+ "button",
1840
+ {
1841
+ class: "btn btn-outline propose",
1842
+ attrs: { type: "button", "data-action": "propose" },
1843
+ on: {
1844
+ click: () => {
1845
+ session.proposing = true;
1846
+ renderExtra(extra);
1847
+ refreshSend();
1848
+ session.proposal?.focus();
1849
+ }
1850
+ }
1851
+ },
1852
+ [icon(doc, "pencil", 14), m.propose]
1853
+ )
1854
+ );
1855
+ }
1856
+ if (session.tagged && session.field && session.proposing) {
1857
+ const label = session.field.label || session.tagged.fieldKey;
1858
+ const input = session.field.type === "LONG_TEXT" ? el(doc, "textarea", {
1859
+ class: "value",
1860
+ attrs: { "aria-label": m.proposal, rows: "3", maxlength: "10000" }
1861
+ }) : el(doc, "input", {
1862
+ class: "value",
1863
+ attrs: {
1864
+ "aria-label": m.proposal,
1865
+ type: "text",
1866
+ inputmode: session.field.type === "NUMBER" ? "decimal" : null
1867
+ }
1868
+ });
1869
+ input.value = session.proposal?.value ?? session.before;
1870
+ input.addEventListener("input", refreshSend);
1871
+ session.proposal = input;
1872
+ parts.push(
1873
+ el(doc, "div", { class: "proposal" }, [
1874
+ el(doc, "div", { class: "head" }, [
1875
+ el(doc, "span", { class: "small strong-text", text: `${m.proposalFor} ${label}` }),
1876
+ closeButton(m.removeProposal, () => {
1877
+ session.proposing = false;
1878
+ session.proposal = null;
1879
+ renderExtra(extra);
1880
+ refreshSend();
1881
+ })
1882
+ ]),
1883
+ session.before ? el(doc, "p", { class: "muted small before" }, [
1884
+ `${m.now} `,
1885
+ el(doc, "span", { class: "strong-text", text: session.before })
1886
+ ]) : null,
1887
+ input
1888
+ ])
1889
+ );
1890
+ }
1891
+ }
1892
+ extra.replaceChildren(...parts.filter((part) => part !== null));
1893
+ }
1894
+ function proposalOf(session) {
1895
+ if (!session.request || !session.proposing || !session.proposal) return null;
1896
+ if (!session.tagged || !session.field) return null;
1897
+ const parsed = parseProposed(session.field.type, session.proposal.value);
1898
+ if (!parsed.ok) return "invalid";
1899
+ const before = parseProposed(session.field.type, session.before);
1900
+ if (before.ok && sameValue(before.value, parsed.value)) return null;
1901
+ return { fieldKey: session.tagged.fieldKey, locale: session.tagged.locale, value: parsed.value };
1902
+ }
1903
+ function refreshSend() {
1904
+ const session = composer;
1905
+ if (!session) return;
1906
+ const proposal = proposalOf(session);
1907
+ const comment = session.textarea.value.trim();
1908
+ session.sendButton.replaceChildren(
1909
+ icon(doc, session.request ? "pencil" : "commentPlus", 16),
1910
+ session.busy ? m.sending : session.request ? m.requestChange : m.comment
1911
+ );
1912
+ const proposed = proposal && proposal !== "invalid" && String(proposal.value) !== "";
1913
+ session.sendButton.disabled = session.busy || proposal === "invalid" || !comment && !proposed;
1914
+ session.error.textContent = proposal === "invalid" ? m.invalidNumber : session.error.dataset.sticky ?? "";
1915
+ }
1916
+ function showError(message) {
1917
+ if (!composer) return;
1918
+ composer.error.dataset.sticky = message;
1919
+ composer.error.textContent = message;
1920
+ }
1921
+ async function submit() {
1922
+ const session = composer;
1923
+ if (!session || session.busy) return;
1924
+ const proposal = proposalOf(session);
1925
+ if (proposal === "invalid") return;
1926
+ const body = buildCreateBody({
1927
+ recordRef: session.recordRef,
1928
+ pageUrl: session.pageUrl,
1929
+ anchors: session.anchors,
1930
+ comment: session.textarea.value,
1931
+ isChangeRequest: session.request,
1932
+ proposal
1933
+ });
1934
+ if (!body.body) return;
1935
+ const payload = JSON.stringify(body);
1936
+ if (session.idempotency?.payload !== payload) {
1937
+ session.idempotency = { payload, key: newIdempotencyKey(win) };
1938
+ }
1939
+ session.busy = true;
1940
+ delete session.error.dataset.sticky;
1941
+ refreshSend();
1942
+ const key2 = session.idempotency.key;
1943
+ const result = await auth.call((token) => createComment(ctx, token, body, key2));
1944
+ if (destroyed || composer !== session) return;
1945
+ session.busy = false;
1946
+ if (!result.ok) {
1947
+ refreshSend();
1948
+ onFailure(result, "comment");
1949
+ return;
1950
+ }
1951
+ closeComposer();
1952
+ const assignee = result.data?.thread?.assignee?.name;
1953
+ setToast(
1954
+ body.isChangeRequest ? {
1955
+ title: m.sentRequest,
1956
+ body: m.sentRequestBody(
1957
+ typeof assignee === "string" && assignee ? assignee : null,
1958
+ !!body.proposedValues
1959
+ )
1960
+ } : { title: m.sentComment, body: m.sentCommentBody }
1961
+ );
1962
+ void load();
1963
+ }
1964
+ function closeComposer() {
1965
+ const session = composer;
1966
+ if (!session) return;
1967
+ composer = null;
1968
+ session.card.remove();
1969
+ if (!refused) {
1970
+ overlay.setMode(OVERLAY_MODE[mode]);
1971
+ placePins();
1972
+ }
1973
+ renderBottom();
1974
+ }
1975
+ function beginEdit(tagged) {
1976
+ if (mode !== "edit" || refused || !canEdit()) return;
1977
+ if (editing && !editing.done) {
1978
+ if (editing.element === tagged.element) return;
1979
+ commitEdit(editing);
1980
+ }
1981
+ const field = fieldOf(data, tagged);
1982
+ const kind = editKindFor(tagged, field);
1983
+ if (!kind || !field) {
1984
+ openInfoCard(tagged);
1985
+ return;
1986
+ }
1987
+ closeInfo();
1988
+ const element = tagged.element;
1989
+ const pageUrl = currentPageUrl(win);
1990
+ const anchors = [toApiAnchor(buildAnchor(element, index, pageUrl, doc, win), tagged.recordRef)];
1991
+ const before = field.type === "URL" && element.localName === "a" ? element.getAttribute("href") ?? "" : normalizeText(element.textContent ?? "");
1992
+ const stored2 = data?.values?.[tagged.recordRef]?.[tagged.fieldKey];
1993
+ const legacy = field.type === "NUMBER" && stored2 === void 0 ? parseProposed("NUMBER", before) : null;
1994
+ const raw = legacy?.ok ? legacy.value : stored2;
1995
+ const inline = kind === "text" ? beginInlineEdit(element) : null;
1996
+ const session = {
1997
+ element,
1998
+ tagged,
1999
+ field,
2000
+ kind,
2001
+ recordRef: tagged.recordRef,
2002
+ pageUrl,
2003
+ anchors,
2004
+ before,
2005
+ raw,
2006
+ picked: raw,
2007
+ returnFocus: doc.activeElement,
2008
+ revert: inline?.revert ?? (() => {
2009
+ }),
2010
+ keep: inline?.keep ?? (() => {
2011
+ }),
2012
+ card: null,
2013
+ input: null,
2014
+ done: false
2015
+ };
2016
+ editing = session;
2017
+ if (inline) {
2018
+ element.addEventListener("blur", () => commitEdit(session), { once: true });
2019
+ element.focus?.();
2020
+ } else if (kind === "choice") {
2021
+ openChoiceCard(session);
2022
+ } else if (kind === "switch") {
2023
+ openSwitchCard(session);
2024
+ } else {
2025
+ openInputCard(session);
2026
+ }
2027
+ renderBottom();
2028
+ }
2029
+ function openValueCard(session, body, focus, width) {
2030
+ const label = session.field.label || session.tagged.fieldKey;
2031
+ const card = el(
2032
+ doc,
2033
+ "div",
2034
+ {
2035
+ class: "card editor",
2036
+ attrs: { role: "dialog", "aria-label": label },
2037
+ on: {
2038
+ keydown: (event) => {
2039
+ const e = event;
2040
+ if (e.key !== "Enter" || e.target.getAttribute("role") === "option") return;
2041
+ e.preventDefault();
2042
+ commitEdit(session);
2043
+ }
2044
+ }
2045
+ },
2046
+ [
2047
+ body,
2048
+ el(doc, "div", { class: "keys" }, [
2049
+ kbd(doc, "Enter"),
2050
+ m.enterSaves,
2051
+ kbd(doc, "Esc"),
2052
+ m.escCancels
2053
+ ])
2054
+ ]
2055
+ );
2056
+ card.style.width = `${width}px`;
2057
+ session.card = card;
2058
+ layer.append(card);
2059
+ position(card, session.element.getBoundingClientRect(), width);
2060
+ focus.focus();
2061
+ }
2062
+ function openInputCard(session) {
2063
+ const { field } = session;
2064
+ const spec = field.type === "URL" ? { type: "url", text: session.before, min: void 0, max: void 0, step: void 0 } : inputSpec(field, session.raw);
2065
+ const input = el(doc, "input", {
2066
+ class: "value",
2067
+ attrs: {
2068
+ "aria-label": field.label || session.tagged.fieldKey,
2069
+ type: spec.type,
2070
+ min: spec.min === void 0 ? null : String(spec.min),
2071
+ max: spec.max === void 0 ? null : String(spec.max),
2072
+ step: spec.step === void 0 ? null : String(spec.step)
2073
+ }
2074
+ });
2075
+ input.value = spec.text;
2076
+ session.input = input;
2077
+ const unit2 = field.type === "CURRENCY" ? field.currency ?? null : null;
2078
+ openValueCard(
2079
+ session,
2080
+ unit2 ? el(doc, "div", { class: "with-unit" }, [
2081
+ input,
2082
+ el(doc, "span", { class: "muted", text: unit2 })
2083
+ ]) : input,
2084
+ input,
2085
+ field.type === "URL" ? 280 : 220
2086
+ );
2087
+ }
2088
+ function openChoiceCard(session) {
2089
+ const buttons = (session.field.options ?? []).map(
2090
+ (option) => el(
2091
+ doc,
2092
+ "button",
2093
+ {
2094
+ class: "option",
2095
+ attrs: {
2096
+ type: "button",
2097
+ role: "option",
2098
+ "aria-selected": String(option.id === session.raw),
2099
+ tabindex: "-1"
2100
+ },
2101
+ on: {
2102
+ click: () => {
2103
+ session.picked = option.id;
2104
+ commitEdit(session);
2105
+ }
2106
+ }
2107
+ },
2108
+ [
2109
+ el(doc, "span", { class: "option-label", text: option.label }),
2110
+ option.id === session.raw ? icon(doc, "tick", 14) : null
2111
+ ]
2112
+ )
2113
+ );
2114
+ const first = buttons.find((button) => button.getAttribute("aria-selected") === "true") ?? buttons[0];
2115
+ first.tabIndex = 0;
2116
+ const list2 = el(
2117
+ doc,
2118
+ "div",
2119
+ {
2120
+ class: "options",
2121
+ attrs: { role: "listbox", "aria-label": m.options(session.field.label) },
2122
+ on: {
2123
+ keydown: (event) => {
2124
+ const e = event;
2125
+ const at = buttons.indexOf(host.root.activeElement);
2126
+ const to = { ArrowDown: at + 1, ArrowUp: at - 1, Home: 0, End: buttons.length - 1 }[e.key];
2127
+ if (to === void 0) return;
2128
+ e.preventDefault();
2129
+ buttons[Math.min(buttons.length - 1, Math.max(0, to))]?.focus();
2130
+ }
2131
+ }
2132
+ },
2133
+ buttons
2134
+ );
2135
+ openValueCard(session, list2, first, 230);
2136
+ }
2137
+ function openSwitchCard(session) {
2138
+ session.picked = session.raw === true;
2139
+ const label = session.field.label || session.tagged.fieldKey;
2140
+ const toggle = el(
2141
+ doc,
2142
+ "button",
2143
+ {
2144
+ class: "switch",
2145
+ attrs: {
2146
+ type: "button",
2147
+ role: "switch",
2148
+ "aria-checked": String(session.picked),
2149
+ "aria-label": label
2150
+ },
2151
+ on: {
2152
+ click: () => {
2153
+ session.picked = !session.picked;
2154
+ toggle.setAttribute("aria-checked", String(session.picked));
2155
+ }
2156
+ }
2157
+ },
2158
+ [el(doc, "span", { class: "knob" })]
2159
+ );
2160
+ openValueCard(
2161
+ session,
2162
+ el(doc, "div", { class: "switch-row" }, [
2163
+ el(doc, "span", { class: "title", text: label }),
2164
+ toggle
2165
+ ]),
2166
+ toggle,
2167
+ 220
2168
+ );
2169
+ }
2170
+ function openInfoCard(tagged) {
2171
+ closeInfo();
2172
+ const field = fieldOf(data, tagged);
2173
+ const label = field?.label || tagged.fieldKey;
2174
+ const card = el(
2175
+ doc,
2176
+ "div",
2177
+ { class: "card info", attrs: { role: "dialog", "aria-label": m.editInZap } },
2178
+ [
2179
+ icon(doc, "info", 16),
2180
+ el(doc, "div", { class: "toast-text" }, [
2181
+ el(doc, "span", { class: "title", text: m.editInZap }),
2182
+ el(doc, "span", {
2183
+ class: "muted",
2184
+ text: m.editInZapBody(label, field?.type ?? "", tagged.recordRef.startsWith("doc:"))
2185
+ }),
2186
+ el(doc, "div", { class: "actions start" }, [
2187
+ el(
2188
+ doc,
2189
+ "button",
2190
+ {
2191
+ class: "btn btn-outline",
2192
+ attrs: { type: "button", "data-action": "open-field" },
2193
+ on: {
2194
+ click: () => {
2195
+ closeInfo();
2196
+ win.open(fieldEditorUrl(tagged), "_blank", "noopener");
2197
+ }
2198
+ }
2199
+ },
2200
+ [icon(doc, "external", 14), m.openInZap]
2201
+ ),
2202
+ el(
2203
+ doc,
2204
+ "button",
2205
+ {
2206
+ class: "btn btn-ghost",
2207
+ attrs: { type: "button", "data-action": "comment-field" },
2208
+ on: {
2209
+ click: () => {
2210
+ closeInfo();
2211
+ setMode("comment");
2212
+ if (mode === "comment") openComposer([tagged.element], null);
2213
+ }
2214
+ }
2215
+ },
2216
+ [icon(doc, "commentPlus", 14), m.comment]
2217
+ )
2218
+ ])
2219
+ ])
2220
+ ]
2221
+ );
2222
+ info = { tagged, card };
2223
+ layer.append(card);
2224
+ position(card, tagged.element.getBoundingClientRect(), 300);
2225
+ }
2226
+ function closeInfo() {
2227
+ info?.card.remove();
2228
+ info = null;
2229
+ }
2230
+ function fieldEditorUrl(tagged) {
2231
+ const own = !data?.records?.[tagged.recordRef];
2232
+ const base = data?.editorUrls?.[tagged.recordRef] ?? (own ? data?.viewer?.editorUrl : null) ?? null;
2233
+ const safe = zapUrl(base, ctx.zapOrigin);
2234
+ if (!base) return safe;
2235
+ try {
2236
+ const url = new URL(safe);
2237
+ url.searchParams.set("field", tagged.fieldKey);
2238
+ return url.href;
2239
+ } catch {
2240
+ return safe;
2241
+ }
2242
+ }
2243
+ function beginInlineEdit(target) {
2244
+ const children = Array.from(target.childNodes);
2245
+ const texts = [];
2246
+ const walker = doc.createTreeWalker(
2247
+ target,
2248
+ 4
2249
+ /* NodeFilter.SHOW_TEXT */
2250
+ );
2251
+ for (let node = walker.nextNode(); node; node = walker.nextNode()) {
2252
+ texts.push([node, node.data]);
2253
+ }
2254
+ const hadAttribute = target.getAttribute("contenteditable");
2255
+ target.setAttribute("contenteditable", "plaintext-only");
2256
+ if (target.contentEditable !== "plaintext-only") target.setAttribute("contenteditable", "true");
2257
+ target.setAttribute("spellcheck", "true");
2258
+ const keep = () => {
2259
+ if (hadAttribute === null) target.removeAttribute("contenteditable");
2260
+ else target.setAttribute("contenteditable", hadAttribute);
2261
+ target.removeAttribute("spellcheck");
2262
+ };
2263
+ return {
2264
+ keep,
2265
+ revert: () => {
2266
+ keep();
2267
+ target.replaceChildren(...children);
2268
+ for (const [node, text] of texts) node.data = text;
2269
+ }
2270
+ };
2271
+ }
2272
+ function typedValue(session) {
2273
+ if (session.input) return session.input.value;
2274
+ return editableText(session.element);
2275
+ }
2276
+ function finishEdit(session, keepTyped) {
2277
+ session.done = true;
2278
+ if (keepTyped) session.keep();
2279
+ else session.revert();
2280
+ session.card?.remove();
2281
+ if (editing === session) editing = null;
2282
+ renderBottom();
2283
+ const back = session.returnFocus;
2284
+ if (session.card && back?.isConnected) back.focus?.({ preventScroll: true });
2285
+ }
2286
+ function cancelEdit(session) {
2287
+ if (session.done) return;
2288
+ finishEdit(session, false);
2289
+ }
2290
+ function commitEdit(session) {
2291
+ if (session.done) return;
2292
+ const { field, kind } = session;
2293
+ const text = kind === "text" || field.type === "URL";
2294
+ let value;
2295
+ if (text) {
2296
+ const parsed = parseProposed(field.type, typedValue(session));
2297
+ const before = parseProposed(field.type, session.before);
2298
+ if (parsed.ok && before.ok && sameValue(before.value, parsed.value)) {
2299
+ finishEdit(session, false);
2300
+ return;
2301
+ }
2302
+ if (!parsed.ok) return invalid(session);
2303
+ value = parsed.value;
2304
+ } else if (kind === "input") {
2305
+ if (session.input?.validity?.badInput) return invalid(session);
2306
+ const parsed = parseInput(field, typedValue(session));
2307
+ if (!parsed.ok) return invalid(session);
2308
+ value = parsed.value;
2309
+ } else {
2310
+ value = session.picked ?? null;
2311
+ }
2312
+ if (!text && sameRaw(field.type, session.raw, value)) {
2313
+ finishEdit(session, false);
2314
+ return;
2315
+ }
2316
+ finishEdit(session, true);
2317
+ void save(
2318
+ session,
2319
+ { fieldKey: session.tagged.fieldKey, locale: session.tagged.locale, value },
2320
+ kind === "choice" ? field.options?.find((option) => option.id === value)?.label : kind === "switch" ? value ? m.yes : m.no : void 0
2321
+ );
2322
+ }
2323
+ function invalid(session) {
2324
+ finishEdit(session, false);
2325
+ setToast({ title: m.saveFailed, body: m.invalidNumber, error: true });
2326
+ }
2327
+ async function save(session, proposal, display) {
2328
+ setStatus("saving");
2329
+ const body = buildDraftBody({
2330
+ recordRef: session.recordRef,
2331
+ pageUrl: session.pageUrl,
2332
+ anchors: session.anchors,
2333
+ proposal,
2334
+ display
2335
+ });
2336
+ const draftKey = newIdempotencyKey(win);
2337
+ const result = await auth.call((token) => saveToDraft(ctx, token, body, draftKey));
2338
+ if (destroyed) return;
2339
+ if (!result.ok) {
2340
+ setStatus(null);
2341
+ if (result.kind === "unchanged") return;
2342
+ session.revert();
2343
+ onFailure(result, "edit");
2344
+ return;
2345
+ }
2346
+ setStatus("saved");
2347
+ if (data && session.raw !== void 0)
2348
+ (data.values ?? (data.values = {}))[session.recordRef] = {
2349
+ ...data.values[session.recordRef],
2350
+ [session.tagged.fieldKey]: proposal.value
2351
+ };
2352
+ showSaved(session, display);
2353
+ }
2354
+ function showSaved(session, display) {
2355
+ const { field, tagged, element } = session;
2356
+ if (session.kind === "text" || field.type === "URL") return;
2357
+ if (field.type === "ENUM" && display !== void 0 && tagged.source === "attr" && element.children.length === 0) {
2358
+ element.textContent = display;
2359
+ return;
2360
+ }
2361
+ rememberEditIntent(win);
2362
+ win.setTimeout(() => win.location.reload(), RELOAD_AFTER_SAVE_MS);
2363
+ }
2364
+ function endEdit() {
2365
+ if (editing && !editing.done) commitEdit(editing);
2366
+ }
2367
+ const onKey = (event) => {
2368
+ if (event.key === "Escape") hideTip();
2369
+ if (menuOpen && event.key === "Escape") {
2370
+ event.preventDefault();
2371
+ setMenu(false, "account");
2372
+ return;
2373
+ }
2374
+ if (ctx.shortcut && !editing && !composer && isShortcut(event, ctx.shortcut)) {
2375
+ setHidden(!drag.hidden(), !!host.root.activeElement);
2376
+ return;
2377
+ }
2378
+ if (composer && event.key === "Escape") {
2379
+ event.preventDefault();
2380
+ closeComposer();
2381
+ return;
2382
+ }
2383
+ if (info && event.key === "Escape") {
2384
+ event.preventDefault();
2385
+ closeInfo();
2386
+ return;
2387
+ }
2388
+ const session = editing;
2389
+ if (!session || session.done) return;
2390
+ if (event.key === "Escape") {
2391
+ event.preventDefault();
2392
+ cancelEdit(session);
2393
+ } else if (event.key === "Enter" && session.kind === "text" && event.target === session.element && (session.field.type === "TEXT" || event.metaKey || event.ctrlKey)) {
2394
+ event.preventDefault();
2395
+ commitEdit(session);
2396
+ }
2397
+ };
2398
+ const blocks = () => mode !== "navigate" && !drag.hidden();
2399
+ const onClick = (event) => {
2400
+ if (editing?.kind === "text" && !editing.done && editing.element.contains(event.target) || blocks() && event.target.closest?.("a[href],area[href]")) {
2401
+ event.preventDefault();
2402
+ }
2403
+ };
2404
+ const onSubmit = (event) => {
2405
+ if (blocks()) event.preventDefault();
2406
+ };
2407
+ win.addEventListener("keydown", onKey, true);
2408
+ win.addEventListener("click", onClick, true);
2409
+ win.addEventListener("submit", onSubmit, true);
2410
+ win.addEventListener("scroll", schedule, true);
2411
+ win.addEventListener("resize", schedule);
2412
+ overlay.setMode(drag.hidden() ? "off" : OVERLAY_MODE[mode]);
2413
+ renderBottom();
2414
+ void load();
2415
+ return {
2416
+ destroy() {
2417
+ endEdit();
2418
+ destroyed = true;
2419
+ closeComposer();
2420
+ auth.destroy();
2421
+ if (toastTimer !== null) win.clearTimeout(toastTimer);
2422
+ if (statusTimer !== null) win.clearTimeout(statusTimer);
2423
+ win.removeEventListener("click", onClickFirst, true);
2424
+ win.removeEventListener("pointerdown", onPressFirst, true);
2425
+ win.removeEventListener("keydown", onKey, true);
2426
+ win.removeEventListener("click", onClick, true);
2427
+ win.removeEventListener("submit", onSubmit, true);
2428
+ win.removeEventListener("scroll", schedule, true);
2429
+ win.removeEventListener("resize", schedule);
2430
+ stopObserving();
2431
+ overlay.destroy();
2432
+ drag.destroy();
2433
+ host.destroy();
2434
+ }
2435
+ };
2436
+ }
2437
+ function zapUrl(editorUrl, zapOrigin) {
2438
+ const home = new URL("/", zapOrigin);
2439
+ try {
2440
+ const url = new URL(editorUrl ?? "");
2441
+ if (url.protocol === "https:" || url.protocol === "http:") return url.href;
2442
+ } catch {
2443
+ }
2444
+ return home.href;
2445
+ }
2446
+ var CSS = `
2447
+ .marks { position: fixed; inset: 0; pointer-events: none; }
2448
+ .mark { position: fixed; border-radius: 4px; outline: 2px solid rgba(207,135,0,.55); }
2449
+ .bottom { position: fixed; left: 0; right: 0; bottom: 22px; display: flex; flex-direction: column;
2450
+ align-items: center; gap: 10px; pointer-events: none; padding: 0 8px; }
2451
+ .bottom > .card { position: relative; }
2452
+ .toolbar { display: flex; align-items: center; gap: 8px; padding: 6px 6px 6px 4px; white-space: nowrap;
2453
+ max-width: 100%; overflow-x: auto; }
2454
+ .segments { display: inline-flex; gap: 2px; padding: 2px; border-radius: 7px; background: ${TOKENS.subtle};
2455
+ background: oklch(0.94 0.008 250); }
2456
+ .seg { position: relative; height: 28px; padding: 0 10px; border-radius: 5px; font-size: 12px; color: ${TOKENS.muted}; }
2457
+ .seg[aria-pressed="true"] { background: ${TOKENS.bg}; color: ${TOKENS.fg}; box-shadow: 0 1px 2px rgba(15,23,42,.05); }
2458
+ .seg[aria-disabled="true"] { opacity: .5; cursor: default; }
2459
+ .seg[data-tip]:hover::after { content: attr(data-tip); position: absolute; bottom: 34px; left: 50%;
2460
+ transform: translateX(-50%); padding: 6px 10px; border-radius: 9px; background: ${TOKENS.dark}; color: #FFFFFF;
2461
+ font-size: 12px; white-space: nowrap; }
2462
+ .tool { height: 30px; padding: 0 9px; border-radius: 9px; color: ${TOKENS.muted}; }
2463
+ /* Hover as the suite's SegmentedControl and ghost button (SitioBarraEstados): a tool on the track
2464
+ gets foreground text over white at 60%, no shadow; the bar's own buttons, on the white card, the
2465
+ ghost hover: foreground text over slate-100. Both keep the focus ring. */
2466
+ .seg:not([aria-pressed="true"]):not([aria-disabled="true"]):hover { background: rgba(255,255,255,.6); color: ${TOKENS.fg}; }
2467
+ .tool:hover, .grip:hover, .account[aria-expanded="true"] { background: #F1F5F9; color: ${TOKENS.fg}; }
2468
+ .seg, .tool, .grip, .pill { transition: color .15s cubic-bezier(.4,0,.2,1), background-color .15s cubic-bezier(.4,0,.2,1); }
2469
+ .seg:focus-visible, .tool:focus-visible, .grip:focus-visible { box-shadow: 0 0 0 3px ${TOKENS.focus}; }
2470
+ /* The account chip and the hide arrow (SitioBarraEstados): ghost buttons, chevrons muted until hover. */
2471
+ .account { padding: 0 4px 0 2px; gap: 3px; }
2472
+ .hide { width: 30px; padding: 0; justify-content: center; }
2473
+ .avatar.lg { width: 32px; height: 32px; }
2474
+ .menu { width: 260px; padding: 4px; border-radius: 11px; outline: none;
2475
+ box-shadow: 0 10px 15px -3px rgba(15,23,42,.1), 0 4px 6px -4px rgba(15,23,42,.08); }
2476
+ .menu-head { display: flex; align-items: center; gap: 10px; padding: 8px 8px 10px; }
2477
+ .menu-who { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
2478
+ .menu-name { font-weight: 500; }
2479
+ .menu-mail { font-size: 12px; color: ${TOKENS.muted}; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2480
+ .sep { height: 1px; background: ${TOKENS.border}; margin: 4px -4px; }
2481
+ .menu-head + .sep { margin-top: 0; }
2482
+ .mi { width: 100%; height: 32px; gap: 9px; padding: 0 8px; border-radius: 7px; font-weight: 400; color: ${TOKENS.fg}; }
2483
+ .mi > .icon { color: ${TOKENS.muted}; }
2484
+ .mi:focus, .mi:focus-visible { outline: none; background: #F1F5F9; box-shadow: none; }
2485
+ .pill { height: 44px; gap: 8px; padding: 0 12px 0 11px; border-radius: 999px; }
2486
+ .pill:hover { background: #F8FAFC; }
2487
+ .pill:focus-visible { box-shadow: 0 0 0 3px ${TOKENS.focus}, 0 20px 25px -5px rgba(15,23,42,.12); }
2488
+ .pill-count { display: inline-flex; align-items: center; gap: 4px; }
2489
+ .tip { position: fixed; display: inline-flex; align-items: center; gap: 8px; padding: 5px 6px 5px 9px;
2490
+ border-radius: 7px; background: #101828; color: #FFFFFF; font-weight: 500; font-size: 12px; line-height: 18px;
2491
+ white-space: nowrap; pointer-events: none; }
2492
+ .tip:not(.on) { display: none; }
2493
+ [data-enter] { animation: eelzap-in .16s cubic-bezier(.4,0,.2,1); }
2494
+ @keyframes eelzap-in { from { opacity: 0; transform: scale(.96); } }
2495
+ @media (prefers-reduced-motion: reduce) { .seg, .tool, .grip, .pill, .switch, .knob { transition: none; } [data-enter] { animation: none; } }
2496
+ .tool.strong { color: ${TOKENS.fg}; }
2497
+ .divider { width: 1px; height: 22px; background: ${TOKENS.border}; }
2498
+ .avatar { width: 26px; height: 26px; border-radius: 7px; background: #DBEAFE; color: #1D4ED8;
2499
+ display: inline-flex; align-items: center; justify-content: center; font-weight: 600; font-size: 12px; }
2500
+ .status { display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; border-radius: 999px;
2501
+ font-size: 12px; font-weight: 500; }
2502
+ .status > .icon { color: #16A34A; }
2503
+ .tray { display: flex; align-items: center; gap: 10px; padding: 8px 8px 8px 14px; }
2504
+ .list { width: min(420px, calc(100vw - 16px)); padding: 13px 10px 10px 15px; max-height: 50vh; overflow: auto; }
2505
+ .list ul { list-style: none; margin: 8px 0 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
2506
+ .item { width: 100%; text-align: left; align-items: flex-start; gap: 10px; padding: 8px; border-radius: 9px; }
2507
+ .item:hover { background: ${TOKENS.subtle}; }
2508
+ .num { flex-shrink: 0; width: 22px; height: 22px; border-radius: 999px 999px 999px 0; background: #CF8700;
2509
+ color: #FFFFFF; font-weight: 700; font-size: 11px; display: inline-flex; align-items: center; justify-content: center; }
2510
+ .item-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
2511
+ .who { font-weight: 600; font-size: 12px; }
2512
+ .flag { font-size: 11px; color: ${TOKENS.goldText}; }
2513
+ .what { font-weight: 400; color: ${TOKENS.muted}; overflow-wrap: anywhere; }
2514
+ .toast, .refused { width: min(340px, calc(100vw - 16px)); display: flex; gap: 11px; padding: 14px 12px 14px 16px;
2515
+ align-self: flex-end; margin-right: 16px; }
2516
+ .refused { align-self: center; margin-right: 0; width: min(420px, calc(100vw - 16px)); align-items: flex-start; }
2517
+ .toast > .icon { color: #16A34A; }
2518
+ .refused > .icon, .toast-error > .icon { color: ${TOKENS.danger}; }
2519
+ .paused > .icon, .info > .icon { color: ${TOKENS.muted}; }
2520
+ .editor { min-width: max-content; }
2521
+ .editor, .info { position: fixed; padding: 8px; border-radius: 7px;
2522
+ box-shadow: 0 4px 6px -1px rgba(15,23,42,.08), 0 2px 4px -2px rgba(15,23,42,.06); }
2523
+ .info { display: flex; gap: 10px; padding: 12px 14px; }
2524
+ .info > .icon { margin-top: 1px; }
2525
+ .info .title { font-weight: 500; }
2526
+ .info .muted { font-size: 12px; }
2527
+ .actions.start { justify-content: flex-start; margin-top: 10px; gap: 6px; }
2528
+ .actions.start .btn { height: 28px; gap: 7px; font-size: 12px; }
2529
+ .btn-ghost { border: 1px solid transparent; }
2530
+ .btn-ghost:hover { background: ${TOKENS.subtle}; }
2531
+ .keys { display: flex; align-items: center; gap: 5px; margin: 8px 2px 0; font-size: 12px; color: ${TOKENS.muted}; white-space: nowrap; }
2532
+ .editor input.value { height: 34px; border-radius: 8px; }
2533
+ .with-unit { display: flex; align-items: center; gap: 8px; }
2534
+ .options { display: flex; flex-direction: column; max-height: 260px; overflow: auto; }
2535
+ .option { width: 100%; height: 32px; gap: 8px; padding: 0 8px; border-radius: 6px; font-weight: 400; text-align: left; }
2536
+ .option:hover, .option:focus-visible { background: #F1F5F9; box-shadow: none; }
2537
+ .option-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
2538
+ .switch-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 2px 2px 0; }
2539
+ .switch-row .title { font-weight: 500; }
2540
+ .switch { position: relative; width: 32px; height: 18px; border-radius: 999px; background: #CBD5E1; transition: background .15s; }
2541
+ .switch[aria-checked="true"] { background: ${TOKENS.primary}; }
2542
+ .knob { position: absolute; top: 2px; left: 2px; width: 14px; height: 14px; border-radius: 999px; background: ${TOKENS.bg};
2543
+ box-shadow: 0 1px 2px rgba(0,0,0,.15); transition: left .15s; }
2544
+ .switch[aria-checked="true"] .knob { left: 16px; }
2545
+ .mark-chip { position: absolute; right: -5px; top: -27px; display: inline-flex; align-items: center; height: 20px;
2546
+ padding: 0 7px; border-radius: 6px; background: ${TOKENS.gold}; color: #FFFFFF; font: 600 12px Poppins, ui-sans-serif, system-ui, sans-serif;
2547
+ white-space: nowrap; }
2548
+ .toast-text { display: flex; flex-direction: column; gap: 3px; flex: 1; min-width: 0; }
2549
+ .title { font-weight: 600; }
2550
+ .head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
2551
+ .popover { border-radius: 13px; }
2552
+ .popover > .head { padding: 13px 10px 0 15px; }
2553
+ .content { display: flex; flex-direction: column; gap: 11px; padding: 10px 15px 15px; }
2554
+ .extra { display: flex; flex-direction: column; gap: 8px; }
2555
+ .check { gap: 8px; font-size: 13px; align-self: flex-start; }
2556
+ .check .box { width: 16px; height: 16px; border-radius: 4px; border: 1px solid #90A1B9; background: ${TOKENS.bg}; }
2557
+ .check[aria-checked="true"] .box { background: ${TOKENS.primary}; border-color: ${TOKENS.primaryBorder};
2558
+ box-shadow: inset 0 0 0 3px ${TOKENS.bg}; }
2559
+ .propose { align-self: flex-start; height: 30px; }
2560
+ .proposal { display: flex; flex-direction: column; gap: 6px; padding: 10px; border-radius: 12px; background: ${TOKENS.goldTint}; }
2561
+ .chip { display: inline-flex; align-items: center; gap: 4px; height: 22px; padding: 0 8px 0 6px; border-radius: 7px;
2562
+ background: ${TOKENS.goldTint}; color: ${TOKENS.goldText}; font: 400 12px Poppins, ui-sans-serif, system-ui, sans-serif;
2563
+ white-space: nowrap; max-width: 220px; overflow: hidden; text-overflow: ellipsis; }
2564
+ .before { overflow-wrap: anywhere; }
2565
+ .strong-text { color: ${TOKENS.fg}; }
2566
+ .small { font-size: 12px; }
2567
+ .composer, .value { width: 100%; font: 400 13px/22px Poppins, ui-sans-serif, system-ui, sans-serif; color: ${TOKENS.fg};
2568
+ border: 1px solid ${TOKENS.border}; border-radius: 12px; padding: 8px 12px; background: ${TOKENS.bg}; resize: vertical; }
2569
+ input.value { height: 36px; border-radius: 9px; }
2570
+ .composer:focus, .value:focus { border-color: #90A1B9; box-shadow: 0 0 0 3px ${TOKENS.focus}; outline: none; }
2571
+ .error:empty { display: none; }
2572
+ .actions { display: flex; justify-content: flex-end; gap: 8px; flex-wrap: wrap; }
2573
+ .foot { display: flex; justify-content: flex-end; gap: 8px; flex-wrap: wrap; margin: 2px -15px -15px;
2574
+ padding: 11px 15px; border-top: 1px solid ${TOKENS.border}; }
2575
+ .foot .btn { height: 30px; gap: 7px; }
2576
+ .foot .btn-outline { padding: 0 14px; }
2577
+ `;
2578
+ export {
2579
+ OVERLAY_HOST_TAG,
2580
+ start,
2581
+ zapUrl
2582
+ };
2583
+ //# sourceMappingURL=suggest-KLS6B7AM.js.map