@8ux-co/eelzap 0.10.1 → 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.
@@ -13,16 +13,17 @@ import {
13
13
  parseRecordRef,
14
14
  reveal,
15
15
  tagGuard
16
- } from "./chunk-7CXQK2PR.js";
16
+ } from "./chunk-T6S3S5RH.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() };
@@ -374,6 +379,9 @@ function inDraftSession(win, now = Date.now()) {
374
379
  const value = readJson(win, DRAFT_SESSION_KEY);
375
380
  return !!value && typeof value.exp === "number" && value.exp > now && value.origin === win.location.origin;
376
381
  }
382
+ function rememberEditIntent(win, now = Date.now()) {
383
+ write2(win, EDIT_INTENT_KEY, { path: win.location.pathname, at: now });
384
+ }
377
385
  function takeEditIntent(win, now = Date.now()) {
378
386
  const value = readJson(win, EDIT_INTENT_KEY);
379
387
  remove(win, EDIT_INTENT_KEY);
@@ -465,12 +473,112 @@ async function leaveDraftSession(ctx, token, tagged) {
465
473
  }
466
474
 
467
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
+ }
468
481
  function editKindFor(tagged, field) {
469
482
  if (!tagged || !field || tagged.html) return null;
470
483
  if (field.type === "TEXT" || field.type === "LONG_TEXT") return "text";
471
- if (field.type === "NUMBER" || field.type === "URL") return "input";
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";
472
487
  return null;
473
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
+ }
474
582
  function fieldOf(data, tagged) {
475
583
  if (!data || !tagged) return null;
476
584
  const field = data.fields?.[tagged.recordRef]?.[tagged.fieldKey];
@@ -549,7 +657,7 @@ function buildDraftBody(input) {
549
657
  ...recordKeys(input.recordRef),
550
658
  pageUrl: input.pageUrl,
551
659
  anchors: input.anchors.slice(0, 20),
552
- body: String(input.proposal.value).slice(0, 1e4) || " ",
660
+ body: (input.display ?? String(input.proposal.value ?? "")).slice(0, 1e4) || " ",
553
661
  ...proposedPart(input.proposal)
554
662
  };
555
663
  }
@@ -781,6 +889,7 @@ var OVERLAY_MODE = {
781
889
  var PENDING_PIN = "eelzap:pending";
782
890
  var LARGE_SHARE = 0.5;
783
891
  var TOAST_MS = 8e3;
892
+ var RELOAD_AFTER_SAVE_MS = 700;
784
893
  var SAVED_MS = 4e3;
785
894
  function start(ctx, options = {}) {
786
895
  const { win, doc } = ctx;
@@ -795,6 +904,7 @@ function start(ctx, options = {}) {
795
904
  const m = SUGGEST_MESSAGES[ctx.locale];
796
905
  const dm = DRAFT_SESSION_MESSAGES[ctx.locale];
797
906
  let reopenEdit = takeEditIntent(win);
907
+ let retryDraft = !reopenEdit;
798
908
  const host = mountHost(doc, CSS + DRAG_CSS);
799
909
  const layer = el(doc, "div", { class: "layer" });
800
910
  const marks = el(doc, "div", { class: "marks" });
@@ -813,6 +923,7 @@ function start(ctx, options = {}) {
813
923
  let editing = null;
814
924
  let list = null;
815
925
  let toast = null;
926
+ let info = null;
816
927
  let toastTimer = null;
817
928
  let status = null;
818
929
  let statusTimer = null;
@@ -834,7 +945,10 @@ function start(ctx, options = {}) {
834
945
  };
835
946
  win.addEventListener("click", onClickFirst, true);
836
947
  const onPressFirst = (event) => {
837
- if (menuOpen && event.target !== host.host) setMenu(false);
948
+ if (event.target === host.host) return;
949
+ if (menuOpen) setMenu(false);
950
+ if (editing?.card && !editing.done) commitEdit(editing);
951
+ if (info) closeInfo();
838
952
  };
839
953
  win.addEventListener("pointerdown", onPressFirst, true);
840
954
  layer.addEventListener("pointerdown", (event) => {
@@ -969,12 +1083,14 @@ function start(ctx, options = {}) {
969
1083
  function setMode(next) {
970
1084
  if (next === "edit" && !canEdit() || paused && next !== "navigate") return;
971
1085
  if (status === "preparing") return;
972
- if (next === "edit" && !index.elements.length && !inDraftSession(win)) {
1086
+ if (next === "edit" && !index.elements.length && (retryDraft || !inDraftSession(win))) {
1087
+ retryDraft = false;
973
1088
  void prepareEdit();
974
1089
  return;
975
1090
  }
976
1091
  endEdit();
977
1092
  closeComposer();
1093
+ closeInfo();
978
1094
  mode = next;
979
1095
  selection = [];
980
1096
  overlay.clearSelection();
@@ -1075,7 +1191,7 @@ function start(ctx, options = {}) {
1075
1191
  function withTip(target, text, end) {
1076
1192
  const show = () => {
1077
1193
  tip.replaceChildren(text);
1078
- if (key) tip.append(el(doc, "span", { class: "kbd", text: `Shift ${key}` }));
1194
+ if (key) tip.append(kbd(doc, "Shift", key));
1079
1195
  tip.classList.add("on");
1080
1196
  const rect = target.getBoundingClientRect();
1081
1197
  const vw = doc.documentElement.clientWidth || win.innerWidth;
@@ -1494,6 +1610,13 @@ function start(ctx, options = {}) {
1494
1610
  box.style.top = `${rect.top - 3}px`;
1495
1611
  box.style.width = `${rect.width + 6}px`;
1496
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
+ );
1497
1620
  return box;
1498
1621
  })
1499
1622
  );
@@ -1505,18 +1628,42 @@ function start(ctx, options = {}) {
1505
1628
  frame = null;
1506
1629
  drawMarks();
1507
1630
  if (composer) position(composer.card, anchorRect(composer));
1508
- if (editing?.card) position(editing.card, editing.element.getBoundingClientRect());
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);
1509
1634
  });
1510
1635
  }
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));
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);
1517
1640
  card.style.width = `${width}px`;
1518
- card.style.top = `${top}px`;
1519
- card.style.left = `${left}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`;
1520
1667
  }
1521
1668
  function onSelect(elements) {
1522
1669
  if (mode !== "comment" || composer) return;
@@ -1562,7 +1709,7 @@ function start(ctx, options = {}) {
1562
1709
  const own = single ? index.get(single) : null;
1563
1710
  const tagged = own && own.recordRef === recordRef ? own : null;
1564
1711
  const field = fieldOf(data, tagged);
1565
- const proposable = editKindFor(tagged, field) !== null;
1712
+ const proposable = proposableField(tagged, field);
1566
1713
  const target = single;
1567
1714
  const before = field?.type === "URL" && target?.localName === "a" ? target.getAttribute("href") ?? "" : normalizeText(target?.textContent ?? "");
1568
1715
  const spotPin = point ? anchors[0] : null;
@@ -1834,13 +1981,17 @@ function start(ctx, options = {}) {
1834
1981
  const field = fieldOf(data, tagged);
1835
1982
  const kind = editKindFor(tagged, field);
1836
1983
  if (!kind || !field) {
1837
- setToast({ title: m.notEditable, body: m.notEditableBody, error: true });
1984
+ openInfoCard(tagged);
1838
1985
  return;
1839
1986
  }
1987
+ closeInfo();
1840
1988
  const element = tagged.element;
1841
1989
  const pageUrl = currentPageUrl(win);
1842
1990
  const anchors = [toApiAnchor(buildAnchor(element, index, pageUrl, doc, win), tagged.recordRef)];
1843
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;
1844
1995
  const inline = kind === "text" ? beginInlineEdit(element) : null;
1845
1996
  const session = {
1846
1997
  element,
@@ -1851,6 +2002,9 @@ function start(ctx, options = {}) {
1851
2002
  pageUrl,
1852
2003
  anchors,
1853
2004
  before,
2005
+ raw,
2006
+ picked: raw,
2007
+ returnFocus: doc.activeElement,
1854
2008
  revert: inline?.revert ?? (() => {
1855
2009
  }),
1856
2010
  keep: inline?.keep ?? (() => {
@@ -1863,58 +2017,228 @@ function start(ctx, options = {}) {
1863
2017
  if (inline) {
1864
2018
  element.addEventListener("blur", () => commitEdit(session), { once: true });
1865
2019
  element.focus?.();
2020
+ } else if (kind === "choice") {
2021
+ openChoiceCard(session);
2022
+ } else if (kind === "switch") {
2023
+ openSwitchCard(session);
1866
2024
  } else {
1867
2025
  openInputCard(session);
1868
2026
  }
1869
2027
  renderBottom();
1870
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
+ }
1871
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);
1872
2065
  const input = el(doc, "input", {
1873
2066
  class: "value",
1874
2067
  attrs: {
1875
- "aria-label": session.field.label || session.tagged.fieldKey,
1876
- type: "text",
1877
- inputmode: session.field.type === "NUMBER" ? "decimal" : "url"
1878
- }
1879
- });
1880
- input.value = session.before;
1881
- input.addEventListener("keydown", (event) => {
1882
- if (event.key === "Enter") {
1883
- event.preventDefault();
1884
- commitEdit(session);
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)
1885
2073
  }
1886
2074
  });
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" }, [
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" }, [
1896
2081
  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) }
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:"))
1903
2185
  }),
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
- })
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
+ ])
1910
2219
  ])
1911
- ])
1912
- ]);
1913
- session.card = card;
1914
- session.input = input;
2220
+ ]
2221
+ );
2222
+ info = { tagged, card };
1915
2223
  layer.append(card);
1916
- position(card, session.element.getBoundingClientRect());
1917
- input.focus();
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
+ }
1918
2242
  }
1919
2243
  function beginInlineEdit(target) {
1920
2244
  const children = Array.from(target.childNodes);
@@ -1956,6 +2280,8 @@ function start(ctx, options = {}) {
1956
2280
  session.card?.remove();
1957
2281
  if (editing === session) editing = null;
1958
2282
  renderBottom();
2283
+ const back = session.returnFocus;
2284
+ if (session.card && back?.isConnected) back.focus?.({ preventScroll: true });
1959
2285
  }
1960
2286
  function cancelEdit(session) {
1961
2287
  if (session.done) return;
@@ -1963,42 +2289,77 @@ function start(ctx, options = {}) {
1963
2289
  }
1964
2290
  function commitEdit(session) {
1965
2291
  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;
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;
1971
2311
  }
1972
- const before = parseProposed(session.field.type, session.before);
1973
- if (before.ok && sameValue(before.value, parsed.value)) {
2312
+ if (!text && sameRaw(field.type, session.raw, value)) {
1974
2313
  finishEdit(session, false);
1975
2314
  return;
1976
2315
  }
1977
2316
  finishEdit(session, true);
1978
- void save(session, {
1979
- fieldKey: session.tagged.fieldKey,
1980
- locale: session.tagged.locale,
1981
- value: parsed.value
1982
- });
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
+ );
1983
2322
  }
1984
- async function save(session, proposal) {
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) {
1985
2328
  setStatus("saving");
1986
2329
  const body = buildDraftBody({
1987
2330
  recordRef: session.recordRef,
1988
2331
  pageUrl: session.pageUrl,
1989
2332
  anchors: session.anchors,
1990
- proposal
2333
+ proposal,
2334
+ display
1991
2335
  });
1992
2336
  const draftKey = newIdempotencyKey(win);
1993
2337
  const result = await auth.call((token) => saveToDraft(ctx, token, body, draftKey));
1994
2338
  if (destroyed) return;
1995
2339
  if (!result.ok) {
1996
- session.revert();
1997
2340
  setStatus(null);
2341
+ if (result.kind === "unchanged") return;
2342
+ session.revert();
1998
2343
  onFailure(result, "edit");
1999
2344
  return;
2000
2345
  }
2001
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);
2002
2363
  }
2003
2364
  function endEdit() {
2004
2365
  if (editing && !editing.done) commitEdit(editing);
@@ -2019,6 +2380,11 @@ function start(ctx, options = {}) {
2019
2380
  closeComposer();
2020
2381
  return;
2021
2382
  }
2383
+ if (info && event.key === "Escape") {
2384
+ event.preventDefault();
2385
+ closeInfo();
2386
+ return;
2387
+ }
2022
2388
  const session = editing;
2023
2389
  if (!session || session.done) return;
2024
2390
  if (event.key === "Escape") {
@@ -2124,11 +2490,9 @@ var CSS = `
2124
2490
  border-radius: 7px; background: #101828; color: #FFFFFF; font-weight: 500; font-size: 12px; line-height: 18px;
2125
2491
  white-space: nowrap; pointer-events: none; }
2126
2492
  .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
2493
  [data-enter] { animation: eelzap-in .16s cubic-bezier(.4,0,.2,1); }
2130
2494
  @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; } }
2495
+ @media (prefers-reduced-motion: reduce) { .seg, .tool, .grip, .pill, .switch, .knob { transition: none; } [data-enter] { animation: none; } }
2132
2496
  .tool.strong { color: ${TOKENS.fg}; }
2133
2497
  .divider { width: 1px; height: 22px; background: ${TOKENS.border}; }
2134
2498
  .avatar { width: 26px; height: 26px; border-radius: 7px; background: #DBEAFE; color: #1D4ED8;
@@ -2152,7 +2516,35 @@ var CSS = `
2152
2516
  .refused { align-self: center; margin-right: 0; width: min(420px, calc(100vw - 16px)); align-items: flex-start; }
2153
2517
  .toast > .icon { color: #16A34A; }
2154
2518
  .refused > .icon, .toast-error > .icon { color: ${TOKENS.danger}; }
2155
- .paused > .icon { color: ${TOKENS.muted}; }
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; }
2156
2548
  .toast-text { display: flex; flex-direction: column; gap: 3px; flex: 1; min-width: 0; }
2157
2549
  .title { font-weight: 600; }
2158
2550
  .head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
@@ -2188,4 +2580,4 @@ export {
2188
2580
  start,
2189
2581
  zapUrl
2190
2582
  };
2191
- //# sourceMappingURL=suggest-JZ63L3WP.js.map
2583
+ //# sourceMappingURL=suggest-KLS6B7AM.js.map