@8ux-co/eelzap 0.10.1 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,16 +13,17 @@ import {
13
13
  parseRecordRef,
14
14
  reveal,
15
15
  tagGuard
16
- } from "./chunk-7CXQK2PR.js";
16
+ } from "./chunk-72YCJCBI.js";
17
17
  import {
18
18
  SUGGEST_MESSAGES,
19
19
  TOKENS,
20
20
  el,
21
21
  icon,
22
22
  initials,
23
+ kbd,
23
24
  mountHost,
24
25
  readTokenAnswer
25
- } from "./chunk-7ZPXZZUT.js";
26
+ } from "./chunk-4ET7VBJA.js";
26
27
  import {
27
28
  EXPIRY_MARGIN_MS,
28
29
  accessLive,
@@ -55,6 +56,10 @@ async function call(ctx, token, path, init) {
55
56
  const seconds = Number.parseInt(response.headers.get("Retry-After") ?? "", 10);
56
57
  return { ok: false, kind: "rate", retryAfter: seconds > 0 ? Math.min(seconds, 3600) : 30 };
57
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
+ }
58
63
  if (!response.ok) return { ok: false, kind: "invalid" };
59
64
  try {
60
65
  return { ok: true, data: await response.json() };
@@ -318,6 +323,36 @@ var DRAG_CSS = `
318
323
  .bottom[data-below] .seg[data-tip]:hover::after { bottom: auto; top: 34px; }
319
324
  `;
320
325
 
326
+ // src/preview/draft-path.ts
327
+ var UNSAFE_PATH_CHARS = /[\u0000-\u001f\u007f\\]/;
328
+ function safeRedirectPath(raw, requestUrl) {
329
+ const value = raw ?? "/";
330
+ if (value.length === 0 || value.length > 2048) return null;
331
+ if (!value.startsWith("/") || value.startsWith("//")) return null;
332
+ if (UNSAFE_PATH_CHARS.test(value)) return null;
333
+ let base;
334
+ let target;
335
+ try {
336
+ base = new URL(requestUrl);
337
+ target = new URL(value, base);
338
+ } catch {
339
+ return null;
340
+ }
341
+ if (target.origin !== base.origin) return null;
342
+ if (target.pathname.startsWith("//")) return null;
343
+ let decoded = value;
344
+ for (let i = 0; i < value.length; i++) {
345
+ if (/zpt_/i.test(decoded)) return null;
346
+ const next = decoded.replace(
347
+ /%([0-9a-f]{2})/gi,
348
+ (_, hex) => String.fromCharCode(Number.parseInt(hex, 16))
349
+ );
350
+ if (next === decoded) break;
351
+ decoded = next;
352
+ }
353
+ return `${target.pathname}${target.search}${target.hash}`;
354
+ }
355
+
321
356
  // src/preview/site/draft-session.ts
322
357
  var DRAFT_SESSION_PATH = "/api/public/v1/preview/session";
323
358
  var EDIT_INTENT_KEY = "eelzap:edit-intent";
@@ -374,6 +409,9 @@ function inDraftSession(win, now = Date.now()) {
374
409
  const value = readJson(win, DRAFT_SESSION_KEY);
375
410
  return !!value && typeof value.exp === "number" && value.exp > now && value.origin === win.location.origin;
376
411
  }
412
+ function rememberEditIntent(win, now = Date.now()) {
413
+ write2(win, EDIT_INTENT_KEY, { path: win.location.pathname, at: now });
414
+ }
377
415
  function takeEditIntent(win, now = Date.now()) {
378
416
  const value = readJson(win, EDIT_INTENT_KEY);
379
417
  remove(win, EDIT_INTENT_KEY);
@@ -388,9 +426,11 @@ function acceptSessionUrl(raw, win, draftRoute) {
388
426
  return null;
389
427
  }
390
428
  if (url.origin !== win.location.origin || url.pathname !== draftRoute) return null;
391
- if (!/^zpt_[A-Za-z0-9_-]{43}$/.test(url.searchParams.get("token") ?? "")) return null;
392
- const path = url.searchParams.get("path") ?? "";
393
- if (!path.startsWith("/") || path.startsWith("//")) return null;
429
+ if (url.username || url.password || url.search) return null;
430
+ const params = new URLSearchParams(url.hash.slice(1));
431
+ if (!/^zpt_[A-Za-z0-9_-]{43}$/.test(params.get("token") ?? "")) return null;
432
+ const path = params.get("path") ?? "";
433
+ if (safeRedirectPath(path, win.location.href) === null) return null;
394
434
  return url.toString();
395
435
  }
396
436
  async function requestDraftSession(ctx, token, now = Date.now) {
@@ -403,7 +443,7 @@ async function requestDraftSession(ctx, token, now = Date.now) {
403
443
  method: "POST",
404
444
  headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
405
445
  credentials: "omit",
406
- body: JSON.stringify({ draftRoute, path: currentPath(win) })
446
+ body: JSON.stringify({ draftRoute, path: currentPath(win), tokenTransport: "fragment" })
407
447
  });
408
448
  } catch {
409
449
  return { ok: false, reason: "refused" };
@@ -465,12 +505,112 @@ async function leaveDraftSession(ctx, token, tagged) {
465
505
  }
466
506
 
467
507
  // src/preview/site/suggestion.ts
508
+ var SUGGESTIBLE_TYPES = ["TEXT", "LONG_TEXT", "NUMBER", "URL"];
509
+ var INPUT_TYPES = /* @__PURE__ */ new Set(["URL", "NUMBER", "INTEGER", "CURRENCY", "DATE", "DATETIME"]);
510
+ function proposableField(tagged, field) {
511
+ return !!tagged && !!field && !tagged.html && SUGGESTIBLE_TYPES.includes(field.type);
512
+ }
468
513
  function editKindFor(tagged, field) {
469
514
  if (!tagged || !field || tagged.html) return null;
470
515
  if (field.type === "TEXT" || field.type === "LONG_TEXT") return "text";
471
- if (field.type === "NUMBER" || field.type === "URL") return "input";
516
+ if (INPUT_TYPES.has(field.type)) return "input";
517
+ if (field.type === "ENUM") return field.options?.length ? "choice" : null;
518
+ if (field.type === "BOOLEAN") return "switch";
472
519
  return null;
473
520
  }
521
+ var ZERO_DIGITS = /* @__PURE__ */ new Set([
522
+ "BIF",
523
+ "CLP",
524
+ "DJF",
525
+ "GNF",
526
+ "ISK",
527
+ "JPY",
528
+ "KMF",
529
+ "KRW",
530
+ "PYG",
531
+ "RWF",
532
+ "UGX",
533
+ "UYI",
534
+ "VND",
535
+ "VUV",
536
+ "XAF",
537
+ "XOF",
538
+ "XPF"
539
+ ]);
540
+ var THREE_DIGITS = /* @__PURE__ */ new Set(["BHD", "IQD", "JOD", "KWD", "LYD", "OMR", "TND"]);
541
+ function currencyDigits(code) {
542
+ const upper = (code ?? "").toUpperCase();
543
+ if (ZERO_DIGITS.has(upper)) return 0;
544
+ if (THREE_DIGITS.has(upper)) return 3;
545
+ return 2;
546
+ }
547
+ var pad = (n) => String(n).padStart(2, "0");
548
+ function inputSpec(field, value) {
549
+ const digits = currencyDigits(field.currency);
550
+ const limits = { min: field.min, max: field.max };
551
+ const number = typeof value === "number" && Number.isFinite(value) ? value : null;
552
+ switch (field.type) {
553
+ case "NUMBER":
554
+ return {
555
+ type: "number",
556
+ text: number === null ? "" : String(number),
557
+ ...limits,
558
+ step: field.step ?? "any"
559
+ };
560
+ case "INTEGER":
561
+ return { type: "number", text: number === null ? "" : String(number), ...limits, step: 1 };
562
+ case "CURRENCY":
563
+ return {
564
+ type: "number",
565
+ text: number === null ? "" : (number / 10 ** digits).toFixed(digits),
566
+ ...limits,
567
+ step: field.step ?? 10 ** -digits
568
+ };
569
+ case "DATE":
570
+ return { type: "date", text: typeof value === "string" ? value.slice(0, 10) : "" };
571
+ case "DATETIME": {
572
+ const at = typeof value === "string" ? new Date(value) : null;
573
+ return {
574
+ type: "datetime-local",
575
+ text: at && Number.isFinite(at.getTime()) ? `${at.getFullYear()}-${pad(at.getMonth() + 1)}-${pad(at.getDate())}T${pad(at.getHours())}:${pad(at.getMinutes())}` : ""
576
+ };
577
+ }
578
+ default:
579
+ return { type: "text", text: typeof value === "string" ? value : "" };
580
+ }
581
+ }
582
+ function parseInput(field, raw) {
583
+ const text = cleanStega(raw).trim();
584
+ if (field.type === "URL") return { ok: true, value: normalizeText(text) };
585
+ if (text === "") return { ok: true, value: null };
586
+ const number = Number(text.replace(",", "."));
587
+ switch (field.type) {
588
+ case "NUMBER":
589
+ return Number.isFinite(number) ? { ok: true, value: number } : { ok: false };
590
+ case "INTEGER":
591
+ return Number.isInteger(number) ? { ok: true, value: number } : { ok: false };
592
+ case "CURRENCY":
593
+ return Number.isFinite(number) ? { ok: true, value: Math.round(number * 10 ** currencyDigits(field.currency)) } : { ok: false };
594
+ case "DATE":
595
+ return /^\d{4}-\d{2}-\d{2}$/.test(text) && Number.isFinite(Date.parse(text)) ? { ok: true, value: text } : { ok: false };
596
+ case "DATETIME": {
597
+ const at = new Date(text);
598
+ return Number.isFinite(at.getTime()) ? { ok: true, value: at.toISOString() } : { ok: false };
599
+ }
600
+ default:
601
+ return { ok: true, value: text };
602
+ }
603
+ }
604
+ function sameRaw(type, a, b) {
605
+ const before = a ?? null;
606
+ if (typeof before === "string" && typeof b === "string") {
607
+ if (type === "DATETIME") {
608
+ return Math.floor(Date.parse(before) / 6e4) === Math.floor(Date.parse(b) / 6e4);
609
+ }
610
+ return normalizeText(before) === normalizeText(b);
611
+ }
612
+ return before === b;
613
+ }
474
614
  function fieldOf(data, tagged) {
475
615
  if (!data || !tagged) return null;
476
616
  const field = data.fields?.[tagged.recordRef]?.[tagged.fieldKey];
@@ -549,7 +689,7 @@ function buildDraftBody(input) {
549
689
  ...recordKeys(input.recordRef),
550
690
  pageUrl: input.pageUrl,
551
691
  anchors: input.anchors.slice(0, 20),
552
- body: String(input.proposal.value).slice(0, 1e4) || " ",
692
+ body: (input.display ?? String(input.proposal.value ?? "")).slice(0, 1e4) || " ",
553
693
  ...proposedPart(input.proposal)
554
694
  };
555
695
  }
@@ -781,6 +921,7 @@ var OVERLAY_MODE = {
781
921
  var PENDING_PIN = "eelzap:pending";
782
922
  var LARGE_SHARE = 0.5;
783
923
  var TOAST_MS = 8e3;
924
+ var RELOAD_AFTER_SAVE_MS = 700;
784
925
  var SAVED_MS = 4e3;
785
926
  function start(ctx, options = {}) {
786
927
  const { win, doc } = ctx;
@@ -795,6 +936,7 @@ function start(ctx, options = {}) {
795
936
  const m = SUGGEST_MESSAGES[ctx.locale];
796
937
  const dm = DRAFT_SESSION_MESSAGES[ctx.locale];
797
938
  let reopenEdit = takeEditIntent(win);
939
+ let retryDraft = !reopenEdit;
798
940
  const host = mountHost(doc, CSS + DRAG_CSS);
799
941
  const layer = el(doc, "div", { class: "layer" });
800
942
  const marks = el(doc, "div", { class: "marks" });
@@ -813,6 +955,7 @@ function start(ctx, options = {}) {
813
955
  let editing = null;
814
956
  let list = null;
815
957
  let toast = null;
958
+ let info = null;
816
959
  let toastTimer = null;
817
960
  let status = null;
818
961
  let statusTimer = null;
@@ -834,7 +977,10 @@ function start(ctx, options = {}) {
834
977
  };
835
978
  win.addEventListener("click", onClickFirst, true);
836
979
  const onPressFirst = (event) => {
837
- if (menuOpen && event.target !== host.host) setMenu(false);
980
+ if (event.target === host.host) return;
981
+ if (menuOpen) setMenu(false);
982
+ if (editing?.card && !editing.done) commitEdit(editing);
983
+ if (info) closeInfo();
838
984
  };
839
985
  win.addEventListener("pointerdown", onPressFirst, true);
840
986
  layer.addEventListener("pointerdown", (event) => {
@@ -969,12 +1115,14 @@ function start(ctx, options = {}) {
969
1115
  function setMode(next) {
970
1116
  if (next === "edit" && !canEdit() || paused && next !== "navigate") return;
971
1117
  if (status === "preparing") return;
972
- if (next === "edit" && !index.elements.length && !inDraftSession(win)) {
1118
+ if (next === "edit" && !index.elements.length && (retryDraft || !inDraftSession(win))) {
1119
+ retryDraft = false;
973
1120
  void prepareEdit();
974
1121
  return;
975
1122
  }
976
1123
  endEdit();
977
1124
  closeComposer();
1125
+ closeInfo();
978
1126
  mode = next;
979
1127
  selection = [];
980
1128
  overlay.clearSelection();
@@ -1075,7 +1223,7 @@ function start(ctx, options = {}) {
1075
1223
  function withTip(target, text, end) {
1076
1224
  const show = () => {
1077
1225
  tip.replaceChildren(text);
1078
- if (key) tip.append(el(doc, "span", { class: "kbd", text: `Shift ${key}` }));
1226
+ if (key) tip.append(kbd(doc, "Shift", key));
1079
1227
  tip.classList.add("on");
1080
1228
  const rect = target.getBoundingClientRect();
1081
1229
  const vw = doc.documentElement.clientWidth || win.innerWidth;
@@ -1494,6 +1642,13 @@ function start(ctx, options = {}) {
1494
1642
  box.style.top = `${rect.top - 3}px`;
1495
1643
  box.style.width = `${rect.width + 6}px`;
1496
1644
  box.style.height = `${rect.height + 6}px`;
1645
+ if (editing?.card)
1646
+ box.append(
1647
+ el(doc, "span", {
1648
+ class: "mark-chip",
1649
+ text: editing.field.label || editing.tagged.fieldKey
1650
+ })
1651
+ );
1497
1652
  return box;
1498
1653
  })
1499
1654
  );
@@ -1505,18 +1660,42 @@ function start(ctx, options = {}) {
1505
1660
  frame = null;
1506
1661
  drawMarks();
1507
1662
  if (composer) position(composer.card, anchorRect(composer));
1508
- if (editing?.card) position(editing.card, editing.element.getBoundingClientRect());
1663
+ if (editing?.card)
1664
+ position(editing.card, editing.element.getBoundingClientRect(), editing.card.offsetWidth);
1665
+ if (info) position(info.card, info.tagged.element.getBoundingClientRect(), 300);
1509
1666
  });
1510
1667
  }
1511
- function position(card, rect) {
1512
- const width = Math.min(420, win.innerWidth - 16);
1513
- const height = card.offsetHeight || 260;
1514
- const below = rect.bottom + 10;
1515
- const top = below + height < win.innerHeight - 90 ? below : Math.max(8, rect.top - height - 10);
1516
- const left = Math.min(Math.max(8, rect.left), Math.max(8, win.innerWidth - width - 8));
1668
+ function position(card, rect, preferred = 420) {
1669
+ const vw = win.innerWidth;
1670
+ const vh = win.innerHeight;
1671
+ const width = Math.min(preferred, vw - 16);
1517
1672
  card.style.width = `${width}px`;
1518
- card.style.top = `${top}px`;
1519
- card.style.left = `${left}px`;
1673
+ const height = card.offsetHeight || 260;
1674
+ const bar = bottom.querySelector(".toolbar, .pill")?.getBoundingClientRect();
1675
+ const clampX = (x) => Math.min(Math.max(8, x), Math.max(8, vw - width - 8));
1676
+ const hits = (x, y) => !!bar && x < bar.right + 8 && x + width > bar.left - 8 && y < bar.bottom + 8 && y + height > bar.top - 8;
1677
+ const fits = (y) => y >= 8 && y + height <= vh - 8;
1678
+ let place = null;
1679
+ for (const y of [rect.bottom + 10, rect.top - height - 10]) {
1680
+ if (!fits(y)) continue;
1681
+ for (const x of [
1682
+ clampX(rect.left),
1683
+ clampX((bar?.right ?? 0) + 8),
1684
+ clampX((bar?.left ?? 0) - 8 - width)
1685
+ ]) {
1686
+ if (!hits(x, y)) {
1687
+ place = [x, y];
1688
+ break;
1689
+ }
1690
+ }
1691
+ if (place) break;
1692
+ }
1693
+ if (!place) {
1694
+ const y = fits(rect.bottom + 10) ? rect.bottom + 10 : Math.max(8, rect.top - height - 10);
1695
+ place = [clampX(rect.left), y];
1696
+ }
1697
+ card.style.left = `${place[0]}px`;
1698
+ card.style.top = `${place[1]}px`;
1520
1699
  }
1521
1700
  function onSelect(elements) {
1522
1701
  if (mode !== "comment" || composer) return;
@@ -1562,7 +1741,7 @@ function start(ctx, options = {}) {
1562
1741
  const own = single ? index.get(single) : null;
1563
1742
  const tagged = own && own.recordRef === recordRef ? own : null;
1564
1743
  const field = fieldOf(data, tagged);
1565
- const proposable = editKindFor(tagged, field) !== null;
1744
+ const proposable = proposableField(tagged, field);
1566
1745
  const target = single;
1567
1746
  const before = field?.type === "URL" && target?.localName === "a" ? target.getAttribute("href") ?? "" : normalizeText(target?.textContent ?? "");
1568
1747
  const spotPin = point ? anchors[0] : null;
@@ -1834,13 +2013,17 @@ function start(ctx, options = {}) {
1834
2013
  const field = fieldOf(data, tagged);
1835
2014
  const kind = editKindFor(tagged, field);
1836
2015
  if (!kind || !field) {
1837
- setToast({ title: m.notEditable, body: m.notEditableBody, error: true });
2016
+ openInfoCard(tagged);
1838
2017
  return;
1839
2018
  }
2019
+ closeInfo();
1840
2020
  const element = tagged.element;
1841
2021
  const pageUrl = currentPageUrl(win);
1842
2022
  const anchors = [toApiAnchor(buildAnchor(element, index, pageUrl, doc, win), tagged.recordRef)];
1843
2023
  const before = field.type === "URL" && element.localName === "a" ? element.getAttribute("href") ?? "" : normalizeText(element.textContent ?? "");
2024
+ const stored2 = data?.values?.[tagged.recordRef]?.[tagged.fieldKey];
2025
+ const legacy = field.type === "NUMBER" && stored2 === void 0 ? parseProposed("NUMBER", before) : null;
2026
+ const raw = legacy?.ok ? legacy.value : stored2;
1844
2027
  const inline = kind === "text" ? beginInlineEdit(element) : null;
1845
2028
  const session = {
1846
2029
  element,
@@ -1851,6 +2034,9 @@ function start(ctx, options = {}) {
1851
2034
  pageUrl,
1852
2035
  anchors,
1853
2036
  before,
2037
+ raw,
2038
+ picked: raw,
2039
+ returnFocus: doc.activeElement,
1854
2040
  revert: inline?.revert ?? (() => {
1855
2041
  }),
1856
2042
  keep: inline?.keep ?? (() => {
@@ -1863,58 +2049,228 @@ function start(ctx, options = {}) {
1863
2049
  if (inline) {
1864
2050
  element.addEventListener("blur", () => commitEdit(session), { once: true });
1865
2051
  element.focus?.();
2052
+ } else if (kind === "choice") {
2053
+ openChoiceCard(session);
2054
+ } else if (kind === "switch") {
2055
+ openSwitchCard(session);
1866
2056
  } else {
1867
2057
  openInputCard(session);
1868
2058
  }
1869
2059
  renderBottom();
1870
2060
  }
2061
+ function openValueCard(session, body, focus, width) {
2062
+ const label = session.field.label || session.tagged.fieldKey;
2063
+ const card = el(
2064
+ doc,
2065
+ "div",
2066
+ {
2067
+ class: "card editor",
2068
+ attrs: { role: "dialog", "aria-label": label },
2069
+ on: {
2070
+ keydown: (event) => {
2071
+ const e = event;
2072
+ if (e.key !== "Enter" || e.target.getAttribute("role") === "option") return;
2073
+ e.preventDefault();
2074
+ commitEdit(session);
2075
+ }
2076
+ }
2077
+ },
2078
+ [
2079
+ body,
2080
+ el(doc, "div", { class: "keys" }, [
2081
+ kbd(doc, "Enter"),
2082
+ m.enterSaves,
2083
+ kbd(doc, "Esc"),
2084
+ m.escCancels
2085
+ ])
2086
+ ]
2087
+ );
2088
+ card.style.width = `${width}px`;
2089
+ session.card = card;
2090
+ layer.append(card);
2091
+ position(card, session.element.getBoundingClientRect(), width);
2092
+ focus.focus();
2093
+ }
1871
2094
  function openInputCard(session) {
2095
+ const { field } = session;
2096
+ const spec = field.type === "URL" ? { type: "url", text: session.before, min: void 0, max: void 0, step: void 0 } : inputSpec(field, session.raw);
1872
2097
  const input = el(doc, "input", {
1873
2098
  class: "value",
1874
2099
  attrs: {
1875
- "aria-label": session.field.label || session.tagged.fieldKey,
1876
- type: "text",
1877
- inputmode: session.field.type === "NUMBER" ? "decimal" : "url"
2100
+ "aria-label": field.label || session.tagged.fieldKey,
2101
+ type: spec.type,
2102
+ min: spec.min === void 0 ? null : String(spec.min),
2103
+ max: spec.max === void 0 ? null : String(spec.max),
2104
+ step: spec.step === void 0 ? null : String(spec.step)
1878
2105
  }
1879
2106
  });
1880
- input.value = session.before;
1881
- input.addEventListener("keydown", (event) => {
1882
- if (event.key === "Enter") {
1883
- event.preventDefault();
1884
- commitEdit(session);
1885
- }
1886
- });
1887
- const card = el(doc, "div", { class: "card popover sheet", attrs: { role: "dialog" } }, [
1888
- el(doc, "div", { class: "head" }, [
1889
- el(doc, "span", { class: "chip" }, [
1890
- icon(doc, "field", 12),
1891
- session.field.label || session.tagged.fieldKey
1892
- ]),
1893
- closeButton(m.close, () => cancelEdit(session))
1894
- ]),
1895
- el(doc, "div", { class: "content" }, [
2107
+ input.value = spec.text;
2108
+ session.input = input;
2109
+ const unit2 = field.type === "CURRENCY" ? field.currency ?? null : null;
2110
+ openValueCard(
2111
+ session,
2112
+ unit2 ? el(doc, "div", { class: "with-unit" }, [
1896
2113
  input,
1897
- el(doc, "div", { class: "actions" }, [
1898
- el(doc, "button", {
1899
- class: "btn btn-outline",
1900
- text: m.cancel,
1901
- attrs: { type: "button" },
1902
- on: { click: () => cancelEdit(session) }
2114
+ el(doc, "span", { class: "muted", text: unit2 })
2115
+ ]) : input,
2116
+ input,
2117
+ field.type === "URL" ? 280 : 220
2118
+ );
2119
+ }
2120
+ function openChoiceCard(session) {
2121
+ const buttons = (session.field.options ?? []).map(
2122
+ (option) => el(
2123
+ doc,
2124
+ "button",
2125
+ {
2126
+ class: "option",
2127
+ attrs: {
2128
+ type: "button",
2129
+ role: "option",
2130
+ "aria-selected": String(option.id === session.raw),
2131
+ tabindex: "-1"
2132
+ },
2133
+ on: {
2134
+ click: () => {
2135
+ session.picked = option.id;
2136
+ commitEdit(session);
2137
+ }
2138
+ }
2139
+ },
2140
+ [
2141
+ el(doc, "span", { class: "option-label", text: option.label }),
2142
+ option.id === session.raw ? icon(doc, "tick", 14) : null
2143
+ ]
2144
+ )
2145
+ );
2146
+ const first = buttons.find((button) => button.getAttribute("aria-selected") === "true") ?? buttons[0];
2147
+ first.tabIndex = 0;
2148
+ const list2 = el(
2149
+ doc,
2150
+ "div",
2151
+ {
2152
+ class: "options",
2153
+ attrs: { role: "listbox", "aria-label": m.options(session.field.label) },
2154
+ on: {
2155
+ keydown: (event) => {
2156
+ const e = event;
2157
+ const at = buttons.indexOf(host.root.activeElement);
2158
+ const to = { ArrowDown: at + 1, ArrowUp: at - 1, Home: 0, End: buttons.length - 1 }[e.key];
2159
+ if (to === void 0) return;
2160
+ e.preventDefault();
2161
+ buttons[Math.min(buttons.length - 1, Math.max(0, to))]?.focus();
2162
+ }
2163
+ }
2164
+ },
2165
+ buttons
2166
+ );
2167
+ openValueCard(session, list2, first, 230);
2168
+ }
2169
+ function openSwitchCard(session) {
2170
+ session.picked = session.raw === true;
2171
+ const label = session.field.label || session.tagged.fieldKey;
2172
+ const toggle = el(
2173
+ doc,
2174
+ "button",
2175
+ {
2176
+ class: "switch",
2177
+ attrs: {
2178
+ type: "button",
2179
+ role: "switch",
2180
+ "aria-checked": String(session.picked),
2181
+ "aria-label": label
2182
+ },
2183
+ on: {
2184
+ click: () => {
2185
+ session.picked = !session.picked;
2186
+ toggle.setAttribute("aria-checked", String(session.picked));
2187
+ }
2188
+ }
2189
+ },
2190
+ [el(doc, "span", { class: "knob" })]
2191
+ );
2192
+ openValueCard(
2193
+ session,
2194
+ el(doc, "div", { class: "switch-row" }, [
2195
+ el(doc, "span", { class: "title", text: label }),
2196
+ toggle
2197
+ ]),
2198
+ toggle,
2199
+ 220
2200
+ );
2201
+ }
2202
+ function openInfoCard(tagged) {
2203
+ closeInfo();
2204
+ const field = fieldOf(data, tagged);
2205
+ const label = field?.label || tagged.fieldKey;
2206
+ const card = el(
2207
+ doc,
2208
+ "div",
2209
+ { class: "card info", attrs: { role: "dialog", "aria-label": m.editInZap } },
2210
+ [
2211
+ icon(doc, "info", 16),
2212
+ el(doc, "div", { class: "toast-text" }, [
2213
+ el(doc, "span", { class: "title", text: m.editInZap }),
2214
+ el(doc, "span", {
2215
+ class: "muted",
2216
+ text: m.editInZapBody(label, field?.type ?? "", tagged.recordRef.startsWith("doc:"))
1903
2217
  }),
1904
- el(doc, "button", {
1905
- class: "btn btn-primary",
1906
- text: m.save,
1907
- attrs: { type: "button", "data-action": "save" },
1908
- on: { click: () => commitEdit(session) }
1909
- })
2218
+ el(doc, "div", { class: "actions start" }, [
2219
+ el(
2220
+ doc,
2221
+ "button",
2222
+ {
2223
+ class: "btn btn-outline",
2224
+ attrs: { type: "button", "data-action": "open-field" },
2225
+ on: {
2226
+ click: () => {
2227
+ closeInfo();
2228
+ win.open(fieldEditorUrl(tagged), "_blank", "noopener");
2229
+ }
2230
+ }
2231
+ },
2232
+ [icon(doc, "external", 14), m.openInZap]
2233
+ ),
2234
+ el(
2235
+ doc,
2236
+ "button",
2237
+ {
2238
+ class: "btn btn-ghost",
2239
+ attrs: { type: "button", "data-action": "comment-field" },
2240
+ on: {
2241
+ click: () => {
2242
+ closeInfo();
2243
+ setMode("comment");
2244
+ if (mode === "comment") openComposer([tagged.element], null);
2245
+ }
2246
+ }
2247
+ },
2248
+ [icon(doc, "commentPlus", 14), m.comment]
2249
+ )
2250
+ ])
1910
2251
  ])
1911
- ])
1912
- ]);
1913
- session.card = card;
1914
- session.input = input;
2252
+ ]
2253
+ );
2254
+ info = { tagged, card };
1915
2255
  layer.append(card);
1916
- position(card, session.element.getBoundingClientRect());
1917
- input.focus();
2256
+ position(card, tagged.element.getBoundingClientRect(), 300);
2257
+ }
2258
+ function closeInfo() {
2259
+ info?.card.remove();
2260
+ info = null;
2261
+ }
2262
+ function fieldEditorUrl(tagged) {
2263
+ const own = !data?.records?.[tagged.recordRef];
2264
+ const base = data?.editorUrls?.[tagged.recordRef] ?? (own ? data?.viewer?.editorUrl : null) ?? null;
2265
+ const safe = zapUrl(base, ctx.zapOrigin);
2266
+ if (!base) return safe;
2267
+ try {
2268
+ const url = new URL(safe);
2269
+ url.searchParams.set("field", tagged.fieldKey);
2270
+ return url.href;
2271
+ } catch {
2272
+ return safe;
2273
+ }
1918
2274
  }
1919
2275
  function beginInlineEdit(target) {
1920
2276
  const children = Array.from(target.childNodes);
@@ -1956,6 +2312,8 @@ function start(ctx, options = {}) {
1956
2312
  session.card?.remove();
1957
2313
  if (editing === session) editing = null;
1958
2314
  renderBottom();
2315
+ const back = session.returnFocus;
2316
+ if (session.card && back?.isConnected) back.focus?.({ preventScroll: true });
1959
2317
  }
1960
2318
  function cancelEdit(session) {
1961
2319
  if (session.done) return;
@@ -1963,42 +2321,77 @@ function start(ctx, options = {}) {
1963
2321
  }
1964
2322
  function commitEdit(session) {
1965
2323
  if (session.done) return;
1966
- const parsed = parseProposed(session.field.type, typedValue(session));
1967
- if (!parsed.ok) {
1968
- finishEdit(session, false);
1969
- setToast({ title: m.saveFailed, body: m.invalidNumber, error: true });
1970
- return;
2324
+ const { field, kind } = session;
2325
+ const text = kind === "text" || field.type === "URL";
2326
+ let value;
2327
+ if (text) {
2328
+ const parsed = parseProposed(field.type, typedValue(session));
2329
+ const before = parseProposed(field.type, session.before);
2330
+ if (parsed.ok && before.ok && sameValue(before.value, parsed.value)) {
2331
+ finishEdit(session, false);
2332
+ return;
2333
+ }
2334
+ if (!parsed.ok) return invalid(session);
2335
+ value = parsed.value;
2336
+ } else if (kind === "input") {
2337
+ if (session.input?.validity?.badInput) return invalid(session);
2338
+ const parsed = parseInput(field, typedValue(session));
2339
+ if (!parsed.ok) return invalid(session);
2340
+ value = parsed.value;
2341
+ } else {
2342
+ value = session.picked ?? null;
1971
2343
  }
1972
- const before = parseProposed(session.field.type, session.before);
1973
- if (before.ok && sameValue(before.value, parsed.value)) {
2344
+ if (!text && sameRaw(field.type, session.raw, value)) {
1974
2345
  finishEdit(session, false);
1975
2346
  return;
1976
2347
  }
1977
2348
  finishEdit(session, true);
1978
- void save(session, {
1979
- fieldKey: session.tagged.fieldKey,
1980
- locale: session.tagged.locale,
1981
- value: parsed.value
1982
- });
2349
+ void save(
2350
+ session,
2351
+ { fieldKey: session.tagged.fieldKey, locale: session.tagged.locale, value },
2352
+ kind === "choice" ? field.options?.find((option) => option.id === value)?.label : kind === "switch" ? value ? m.yes : m.no : void 0
2353
+ );
2354
+ }
2355
+ function invalid(session) {
2356
+ finishEdit(session, false);
2357
+ setToast({ title: m.saveFailed, body: m.invalidNumber, error: true });
1983
2358
  }
1984
- async function save(session, proposal) {
2359
+ async function save(session, proposal, display) {
1985
2360
  setStatus("saving");
1986
2361
  const body = buildDraftBody({
1987
2362
  recordRef: session.recordRef,
1988
2363
  pageUrl: session.pageUrl,
1989
2364
  anchors: session.anchors,
1990
- proposal
2365
+ proposal,
2366
+ display
1991
2367
  });
1992
2368
  const draftKey = newIdempotencyKey(win);
1993
2369
  const result = await auth.call((token) => saveToDraft(ctx, token, body, draftKey));
1994
2370
  if (destroyed) return;
1995
2371
  if (!result.ok) {
1996
- session.revert();
1997
2372
  setStatus(null);
2373
+ if (result.kind === "unchanged") return;
2374
+ session.revert();
1998
2375
  onFailure(result, "edit");
1999
2376
  return;
2000
2377
  }
2001
2378
  setStatus("saved");
2379
+ if (data && session.raw !== void 0)
2380
+ (data.values ?? (data.values = {}))[session.recordRef] = {
2381
+ ...data.values[session.recordRef],
2382
+ [session.tagged.fieldKey]: proposal.value
2383
+ };
2384
+ showSaved(session, display);
2385
+ }
2386
+ function showSaved(session, display) {
2387
+ const { field, tagged, element } = session;
2388
+ if (session.kind === "text" || field.type === "URL") return;
2389
+ if (field.type === "ENUM" && display !== void 0 && tagged.source === "attr" && element.children.length === 0) {
2390
+ element.textContent = display;
2391
+ return;
2392
+ }
2393
+ rememberEditIntent(win);
2394
+ win.setTimeout(() => win.location.reload(), RELOAD_AFTER_SAVE_MS);
2002
2395
  }
2003
2396
  function endEdit() {
2004
2397
  if (editing && !editing.done) commitEdit(editing);
@@ -2019,6 +2412,11 @@ function start(ctx, options = {}) {
2019
2412
  closeComposer();
2020
2413
  return;
2021
2414
  }
2415
+ if (info && event.key === "Escape") {
2416
+ event.preventDefault();
2417
+ closeInfo();
2418
+ return;
2419
+ }
2022
2420
  const session = editing;
2023
2421
  if (!session || session.done) return;
2024
2422
  if (event.key === "Escape") {
@@ -2124,11 +2522,9 @@ var CSS = `
2124
2522
  border-radius: 7px; background: #101828; color: #FFFFFF; font-weight: 500; font-size: 12px; line-height: 18px;
2125
2523
  white-space: nowrap; pointer-events: none; }
2126
2524
  .tip:not(.on) { display: none; }
2127
- .kbd { font-size: 11px; line-height: 16px; color: #CBD5E1; padding: 1px 5px; border-radius: 4px;
2128
- background: rgba(255,255,255,.12); }
2129
2525
  [data-enter] { animation: eelzap-in .16s cubic-bezier(.4,0,.2,1); }
2130
2526
  @keyframes eelzap-in { from { opacity: 0; transform: scale(.96); } }
2131
- @media (prefers-reduced-motion: reduce) { .seg, .tool, .grip, .pill { transition: none; } [data-enter] { animation: none; } }
2527
+ @media (prefers-reduced-motion: reduce) { .seg, .tool, .grip, .pill, .switch, .knob { transition: none; } [data-enter] { animation: none; } }
2132
2528
  .tool.strong { color: ${TOKENS.fg}; }
2133
2529
  .divider { width: 1px; height: 22px; background: ${TOKENS.border}; }
2134
2530
  .avatar { width: 26px; height: 26px; border-radius: 7px; background: #DBEAFE; color: #1D4ED8;
@@ -2152,7 +2548,35 @@ var CSS = `
2152
2548
  .refused { align-self: center; margin-right: 0; width: min(420px, calc(100vw - 16px)); align-items: flex-start; }
2153
2549
  .toast > .icon { color: #16A34A; }
2154
2550
  .refused > .icon, .toast-error > .icon { color: ${TOKENS.danger}; }
2155
- .paused > .icon { color: ${TOKENS.muted}; }
2551
+ .paused > .icon, .info > .icon { color: ${TOKENS.muted}; }
2552
+ .editor { min-width: max-content; }
2553
+ .editor, .info { position: fixed; padding: 8px; border-radius: 7px;
2554
+ box-shadow: 0 4px 6px -1px rgba(15,23,42,.08), 0 2px 4px -2px rgba(15,23,42,.06); }
2555
+ .info { display: flex; gap: 10px; padding: 12px 14px; }
2556
+ .info > .icon { margin-top: 1px; }
2557
+ .info .title { font-weight: 500; }
2558
+ .info .muted { font-size: 12px; }
2559
+ .actions.start { justify-content: flex-start; margin-top: 10px; gap: 6px; }
2560
+ .actions.start .btn { height: 28px; gap: 7px; font-size: 12px; }
2561
+ .btn-ghost { border: 1px solid transparent; }
2562
+ .btn-ghost:hover { background: ${TOKENS.subtle}; }
2563
+ .keys { display: flex; align-items: center; gap: 5px; margin: 8px 2px 0; font-size: 12px; color: ${TOKENS.muted}; white-space: nowrap; }
2564
+ .editor input.value { height: 34px; border-radius: 8px; }
2565
+ .with-unit { display: flex; align-items: center; gap: 8px; }
2566
+ .options { display: flex; flex-direction: column; max-height: 260px; overflow: auto; }
2567
+ .option { width: 100%; height: 32px; gap: 8px; padding: 0 8px; border-radius: 6px; font-weight: 400; text-align: left; }
2568
+ .option:hover, .option:focus-visible { background: #F1F5F9; box-shadow: none; }
2569
+ .option-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
2570
+ .switch-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 2px 2px 0; }
2571
+ .switch-row .title { font-weight: 500; }
2572
+ .switch { position: relative; width: 32px; height: 18px; border-radius: 999px; background: #CBD5E1; transition: background .15s; }
2573
+ .switch[aria-checked="true"] { background: ${TOKENS.primary}; }
2574
+ .knob { position: absolute; top: 2px; left: 2px; width: 14px; height: 14px; border-radius: 999px; background: ${TOKENS.bg};
2575
+ box-shadow: 0 1px 2px rgba(0,0,0,.15); transition: left .15s; }
2576
+ .switch[aria-checked="true"] .knob { left: 16px; }
2577
+ .mark-chip { position: absolute; right: -5px; top: -27px; display: inline-flex; align-items: center; height: 20px;
2578
+ padding: 0 7px; border-radius: 6px; background: ${TOKENS.gold}; color: #FFFFFF; font: 600 12px Poppins, ui-sans-serif, system-ui, sans-serif;
2579
+ white-space: nowrap; }
2156
2580
  .toast-text { display: flex; flex-direction: column; gap: 3px; flex: 1; min-width: 0; }
2157
2581
  .title { font-weight: 600; }
2158
2582
  .head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
@@ -2188,4 +2612,4 @@ export {
2188
2612
  start,
2189
2613
  zapUrl
2190
2614
  };
2191
- //# sourceMappingURL=suggest-JZ63L3WP.js.map
2615
+ //# sourceMappingURL=suggest-ERMR7PCE.js.map