@panphora/clayjs 1.1.0 → 1.2.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.
@@ -1,4 +1,4 @@
1
- /* clayjs 1.1.0 standalone build: every clayjs module in one file. https://clayjs.com/offline
1
+ /* clayjs 1.2.0 standalone build: every clayjs module in one file. https://clayjs.com/offline
2
2
  Third-party code inside keeps its own license (Sortable MIT, Squire MIT, DOMPurify Apache-2.0 OR MPL-2.0, MicroModal MIT): https://clayjs.com/THIRD-PARTY-NOTICES.md */
3
3
  (() => {
4
4
  var __create = Object.create;
@@ -105,12 +105,17 @@
105
105
  function isTabLocalRootAttr(name, element) {
106
106
  return element === document.documentElement && TAB_LOCAL_ROOT_ATTRS.has(name);
107
107
  }
108
- var SAVE_TOKEN_ATTRS, HOST_IDENTITY_ATTRS, HOST_TOKEN_ATTRS, ROOT_LIBRARY_ATTRS, TAB_LOCAL_ROOT_ATTRS;
108
+ var SAVE_TOKEN_ATTRS, LEGACY_SAVE_TOKEN_ATTRS, HOST_IDENTITY_ATTRS, HOST_TOKEN_ATTRS, ROOT_LIBRARY_ATTRS, TAB_LOCAL_ROOT_ATTRS;
109
109
  var init_root_attrs = __esm({
110
110
  "src/lib/root-attrs.js"() {
111
- SAVE_TOKEN_ATTRS = ["savetoken", "htmlclaytoken"];
112
- HOST_IDENTITY_ATTRS = ["htmlclayid"];
113
- HOST_TOKEN_ATTRS = [...SAVE_TOKEN_ATTRS, ...HOST_IDENTITY_ATTRS];
111
+ SAVE_TOKEN_ATTRS = ["savetoken"];
112
+ LEGACY_SAVE_TOKEN_ATTRS = ["htmlclaytoken"];
113
+ HOST_IDENTITY_ATTRS = ["documentid", "htmlclayid"];
114
+ HOST_TOKEN_ATTRS = [
115
+ ...SAVE_TOKEN_ATTRS,
116
+ ...LEGACY_SAVE_TOKEN_ATTRS,
117
+ ...HOST_IDENTITY_ATTRS
118
+ ];
114
119
  ROOT_LIBRARY_ATTRS = ["savestatus", "editmode", "pageowner"];
115
120
  TAB_LOCAL_ROOT_ATTRS = /* @__PURE__ */ new Set([
116
121
  ...HOST_TOKEN_ATTRS,
@@ -120,20 +125,41 @@
120
125
  });
121
126
 
122
127
  // src/core/host-attrs.js
128
+ function warnAboutLegacyToken() {
129
+ if (warnedAboutLegacyToken) return;
130
+ const carries = LEGACY_SAVE_TOKEN_ATTRS.some(
131
+ (attr) => document.documentElement.getAttribute(attr)
132
+ );
133
+ if (!carries) return;
134
+ warnedAboutLegacyToken = true;
135
+ console.warn(
136
+ "[clay] This page was served with the pre-1.9.0 save token (" + LEGACY_SAVE_TOKEN_ATTRS.join(", ") + "), which this version no longer accepts. Saving will fail until the host is updated. If this is HTML Clay, upgrade it to 1.9.0 or newer."
137
+ );
138
+ }
123
139
  function saveToken() {
124
140
  if (typeof document === "undefined") return null;
125
141
  for (const attr of SAVE_TOKEN_ATTRS) {
126
142
  const value = document.documentElement.getAttribute(attr);
127
143
  if (value) return value;
128
144
  }
145
+ warnAboutLegacyToken();
129
146
  return null;
130
147
  }
131
148
  function hasSaveToken() {
132
149
  return saveToken() !== null;
133
150
  }
151
+ function servedStaleToken() {
152
+ if (typeof document === "undefined") return false;
153
+ if (saveToken() !== null) return false;
154
+ return LEGACY_SAVE_TOKEN_ATTRS.some(
155
+ (attr) => document.documentElement.getAttribute(attr)
156
+ );
157
+ }
158
+ var warnedAboutLegacyToken;
134
159
  var init_host_attrs = __esm({
135
160
  "src/core/host-attrs.js"() {
136
161
  init_root_attrs();
162
+ warnedAboutLegacyToken = false;
137
163
  }
138
164
  });
139
165
 
@@ -157,17 +183,17 @@
157
183
  forcedEditMode = Boolean(window.__hyperclayEditMode);
158
184
  }
159
185
  }
160
- isEditMode = query_default.editmode ? query_default.editmode === "true" : forcedEditMode != null ? forcedEditMode : hasSaveToken() || Boolean(cookie_default.get("isAdminOfCurrentResource"));
186
+ isEditMode = query_default.editmode ? query_default.editmode === "true" : servedStaleToken() ? false : forcedEditMode != null ? forcedEditMode : hasSaveToken() || Boolean(cookie_default.get("isAdminOfCurrentResource"));
161
187
  isOwner = Boolean(cookie_default.get("isAdminOfCurrentResource"));
162
188
  }
163
189
  });
164
190
 
165
191
  // src/lib/extension-noise.js
166
- function stripExtensionNoise(root2) {
167
- if (!root2 || !root2.querySelectorAll) return;
192
+ function stripExtensionNoise(root3) {
193
+ if (!root3 || !root3.querySelectorAll) return;
168
194
  try {
169
- for (const el3 of root2.querySelectorAll(EXTENSION_NODE_SELECTOR)) el3.remove();
170
- for (const el3 of [root2, ...root2.querySelectorAll("*")]) {
195
+ for (const el3 of root3.querySelectorAll(EXTENSION_NODE_SELECTOR)) el3.remove();
196
+ for (const el3 of [root3, ...root3.querySelectorAll("*")]) {
171
197
  if (!el3.attributes) continue;
172
198
  for (const attr of [...el3.attributes]) {
173
199
  if (EXTENSION_ATTR_PATTERN.test(attr.name.toLowerCase())) el3.removeAttribute(attr.name);
@@ -1326,7 +1352,7 @@
1326
1352
  for (const name of TAB_LOCAL_ROOT_ATTRS) bareRoot.removeAttribute(name);
1327
1353
  const shell = bareRoot.outerHTML;
1328
1354
  const endTag = `</${bareRoot.localName}>`;
1329
- return shell.slice(0, shell.length - endTag.length) + clone.innerHTML + endTag;
1355
+ return "<!DOCTYPE html>" + shell.slice(0, shell.length - endTag.length) + clone.innerHTML + endTag;
1330
1356
  }
1331
1357
  function captureBodyForSync() {
1332
1358
  const clone = captureSnapshot();
@@ -1391,6 +1417,108 @@
1391
1417
  }
1392
1418
  });
1393
1419
 
1420
+ // src/lib/hostile-css.js
1421
+ function style(el3, rules) {
1422
+ for (const rule of rules) {
1423
+ const at = rule.indexOf(":");
1424
+ el3.style.setProperty(rule.slice(0, at).trim(), rule.slice(at + 1).trim(), "important");
1425
+ }
1426
+ }
1427
+ function set(el3, prop, value) {
1428
+ el3.style.setProperty(prop, value, "important");
1429
+ }
1430
+ function make(tag, rules, text) {
1431
+ const el3 = document.createElement(tag);
1432
+ style(el3, rules);
1433
+ if (text) el3.textContent = text;
1434
+ return el3;
1435
+ }
1436
+ var init_hostile_css = __esm({
1437
+ "src/lib/hostile-css.js"() {
1438
+ }
1439
+ });
1440
+
1441
+ // src/core/stale-host-notice.js
1442
+ var stale_host_notice_exports = {};
1443
+ function place() {
1444
+ if (!root) return;
1445
+ const vv = window.visualViewport;
1446
+ const lift = vv ? Math.max(0, window.innerHeight - vv.height - vv.offsetTop) : 0;
1447
+ set(root, "bottom", `calc(${16 + lift}px + env(safe-area-inset-bottom,0px))`);
1448
+ }
1449
+ function dismiss() {
1450
+ if (!root) return;
1451
+ set(root, "display", "none");
1452
+ window.visualViewport?.removeEventListener("resize", place);
1453
+ window.visualViewport?.removeEventListener("scroll", place);
1454
+ }
1455
+ function build() {
1456
+ root = make("div", [
1457
+ "position:fixed",
1458
+ "left:50%",
1459
+ "transform:translateX(-50%)",
1460
+ "z-index:2147483001",
1461
+ "display:flex",
1462
+ "align-items:center",
1463
+ "gap:10px",
1464
+ "max-width:calc(100vw - 24px)",
1465
+ "flex-wrap:wrap",
1466
+ "justify-content:center",
1467
+ "padding:9px 12px",
1468
+ "border-radius:10px",
1469
+ `background:${BG}`,
1470
+ `color:${INK}`,
1471
+ `border:1px solid ${EDGE}`,
1472
+ "box-shadow:0 6px 24px rgba(0,0,0,.32),0 1px 2px rgba(0,0,0,.24)",
1473
+ `font:${FONT}`,
1474
+ "text-align:left"
1475
+ ]);
1476
+ root.setAttribute("clay", "no-save no-watch no-snapshot");
1477
+ root.setAttribute("data-clay-stale-host", "");
1478
+ root.setAttribute("role", "alert");
1479
+ root.append(make("span", ["margin-right:2px"], MESSAGE));
1480
+ const close = make("button", [
1481
+ "all:initial",
1482
+ "box-sizing:border-box",
1483
+ "cursor:pointer",
1484
+ `font:${FONT}`,
1485
+ "color:" + INK,
1486
+ "opacity:.72",
1487
+ "padding:2px 6px",
1488
+ "border-radius:6px",
1489
+ "flex:none"
1490
+ ], "Dismiss");
1491
+ close.type = "button";
1492
+ close.setAttribute("aria-label", "Dismiss this message");
1493
+ close.addEventListener("click", dismiss);
1494
+ root.append(close);
1495
+ document.body.appendChild(root);
1496
+ place();
1497
+ window.visualViewport?.addEventListener("resize", place);
1498
+ window.visualViewport?.addEventListener("scroll", place);
1499
+ }
1500
+ function init2() {
1501
+ if (!servedStaleToken()) return;
1502
+ if (isEditMode) return;
1503
+ build();
1504
+ }
1505
+ var BG, INK, EDGE, FONT, MESSAGE, root;
1506
+ var init_stale_host_notice = __esm({
1507
+ "src/core/stale-host-notice.js"() {
1508
+ init_host_attrs();
1509
+ init_is_edit_mode();
1510
+ init_dom_ready();
1511
+ init_hostile_css();
1512
+ BG = "var(--clay-notice-bg,#222)";
1513
+ INK = "var(--clay-notice-ink,#fff)";
1514
+ EDGE = "var(--clay-notice-edge,rgba(255,255,255,.28))";
1515
+ FONT = "14px/1.45 system-ui,-apple-system,'Segoe UI',sans-serif";
1516
+ MESSAGE = "This page can't be edited: the app serving it is out of date. Update HTML Clay to 1.9.0 or newer.";
1517
+ root = null;
1518
+ dom_ready_default(init2);
1519
+ }
1520
+ });
1521
+
1394
1522
  // src/core/host-meta.js
1395
1523
  function bareHost() {
1396
1524
  return { spec: null, extensions: [], document: null };
@@ -1499,6 +1627,7 @@
1499
1627
  if (isEditMode) {
1500
1628
  document.addEventListener("clay:sync-applied", (event) => {
1501
1629
  if (event.detail?.source !== "disk") return;
1630
+ if (typeof event.detail.etag === "string" && event.detail.etag) return;
1502
1631
  forgetEtag();
1503
1632
  seedEtag({ fresh: true });
1504
1633
  });
@@ -1514,6 +1643,8 @@
1514
1643
  isSaveInProgress: () => isSaveInProgress,
1515
1644
  onSnapshot: () => onSnapshot,
1516
1645
  replacePageWith: () => replacePageWith,
1646
+ resetSaveAttempts: () => resetSaveAttempts,
1647
+ saveFateIsUnknown: () => saveFateIsUnknown,
1517
1648
  saveHtml: () => saveHtml
1518
1649
  });
1519
1650
  function isSaveInProgress() {
@@ -1528,9 +1659,34 @@
1528
1659
  etag: data.etag ?? null
1529
1660
  };
1530
1661
  }
1662
+ function saveFateIsUnknown() {
1663
+ return unknownAttempt !== null;
1664
+ }
1665
+ function mintSaveId() {
1666
+ const uuid = window.crypto?.randomUUID?.();
1667
+ if (uuid) return uuid;
1668
+ if (window.crypto?.getRandomValues) {
1669
+ const bytes = new Uint8Array(16);
1670
+ window.crypto.getRandomValues(bytes);
1671
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
1672
+ }
1673
+ const segment = () => Math.random().toString(36).slice(2, 12).padEnd(10, "0");
1674
+ return `${Date.now().toString(36)}-${segment()}${segment()}${segment()}`;
1675
+ }
1676
+ function rememberSentId(saveId) {
1677
+ sentIds.push(saveId);
1678
+ if (sentIds.length > SENT_ID_MEMORY) sentIds.shift();
1679
+ }
1680
+ function sentByThisTab(saveId) {
1681
+ return typeof saveId === "string" && saveId !== "" && sentIds.includes(saveId);
1682
+ }
1683
+ function resetSaveAttempts() {
1684
+ sentIds.length = 0;
1685
+ unknownAttempt = null;
1686
+ }
1531
1687
  function errorResult(err) {
1532
1688
  const timedOut = err.name === "AbortError";
1533
- const conflicted = !timedOut && (err.status === 412 || err.code === "conflict");
1689
+ const conflicted = !timedOut && err.status === 412;
1534
1690
  return {
1535
1691
  ok: false,
1536
1692
  msg: timedOut ? "Server not responding" : err.message || "Save failed",
@@ -1538,13 +1694,17 @@
1538
1694
  // so this reports "we do not know" rather than asserting something false.
1539
1695
  msgType: timedOut ? "unknown" : conflicted ? "conflict" : "error",
1540
1696
  code: timedOut ? "timeout" : conflicted ? "conflict" : err.code ?? null,
1697
+ changedBy: conflicted ? err.changedBy ?? null : null,
1698
+ // A refusal that may be answering this tab's own timed-out write. Only the
1699
+ // notice uses it, and only to word itself; nothing decides anything on it.
1700
+ afterTimeout: conflicted && unknownAttempt !== null,
1541
1701
  etag: null
1542
1702
  };
1543
1703
  }
1544
1704
  function skippedResult(msg) {
1545
1705
  return { ok: false, msg, msgType: "skipped", code: null, etag: null };
1546
1706
  }
1547
- function buildSaveRequest(content, userDriven, signal) {
1707
+ function buildSaveRequest(content, userDriven, signal, saveId, etag) {
1548
1708
  const token = saveToken();
1549
1709
  const path = token ? `${SAVE_PATH}/${token}` : SAVE_PATH;
1550
1710
  const options = {
@@ -1558,26 +1718,36 @@
1558
1718
  // Spec §9's name for the provenance bit. `X-Hyperclay-User-Driven` was the
1559
1719
  // pre-spec spelling; every host reads Save-Trigger first and falls back to
1560
1720
  // it, so stored documents running an older client keep working.
1561
- "Save-Trigger": userDriven ? "user" : "auto"
1721
+ "Save-Trigger": userDriven ? "user" : "auto",
1722
+ // Spec §6 (`receipts`): which attempt this is. Sent to every host, because a
1723
+ // host that does not implement receipts ignores an unknown header, and
1724
+ // gating it on discovery would mean holding the save until an async lookup
1725
+ // resolved. Nothing is inferred from the header being accepted: only a host
1726
+ // that ANSWERS with the id has said anything.
1727
+ "Save-ID": saveId
1562
1728
  },
1563
1729
  body: content
1564
1730
  };
1565
- const etag = lastSeenEtag();
1566
- if (conditionalSaves() && etag) options.headers["If-Match"] = etag;
1731
+ if (etag) options.headers["If-Match"] = etag;
1567
1732
  return { url: resolveSaveUrl(path), options };
1568
1733
  }
1734
+ function stampToSend() {
1735
+ const etag = lastSeenEtag();
1736
+ return conditionalSaves() && etag ? etag : null;
1737
+ }
1569
1738
  function resolveSaveUrl(path) {
1570
1739
  const origin = window.location.origin;
1571
1740
  if (!origin || origin === "null") return path;
1572
1741
  return new URL(path, origin).href;
1573
1742
  }
1574
- function sendSave(content) {
1743
+ function sendOnce(content, saveId, etag) {
1575
1744
  const controller = new AbortController();
1576
1745
  const timeoutId = setTimeout(() => controller.abort(), SAVE_TIMEOUT_MS);
1577
1746
  const gestureDriven = consumeUserDriven();
1578
1747
  const explicitlyAsked = consumeExplicitSave();
1579
1748
  const userDriven = gestureDriven || explicitlyAsked;
1580
- const { url, options } = buildSaveRequest(content, userDriven, controller.signal);
1749
+ const { url, options } = buildSaveRequest(content, userDriven, controller.signal, saveId, etag);
1750
+ rememberSentId(saveId);
1581
1751
  return fetch(url, options).then((res) => res.text().then((text) => {
1582
1752
  let data = {};
1583
1753
  if (text) {
@@ -1591,19 +1761,68 @@
1591
1761
  const error = new Error(data.msg || data.error || `HTTP ${res.status}: ${res.statusText}`);
1592
1762
  error.code = data.code ?? null;
1593
1763
  error.status = res.status;
1764
+ error.changedBy = data.changedBy ?? null;
1765
+ error.etag = data.etag ?? null;
1766
+ error.saveId = data.saveId ?? null;
1594
1767
  throw error;
1595
1768
  }
1596
1769
  recordEtag(data.etag ?? null);
1770
+ unknownAttempt = null;
1597
1771
  return data;
1598
1772
  })).catch((err) => {
1599
1773
  console.error("Failed to save page:", err);
1600
- if (err.name === "AbortError") seedEtag({ fresh: true });
1774
+ if (err.name === "AbortError") unknownAttempt = { saveId, etag };
1601
1775
  if (userDriven) markUserDriven();
1602
1776
  throw err;
1603
1777
  }).finally(() => {
1604
1778
  clearTimeout(timeoutId);
1605
1779
  });
1606
1780
  }
1781
+ async function recoverUnknownSave() {
1782
+ const attempt = unknownAttempt;
1783
+ if (!attempt) return { landed: null, resend: false };
1784
+ const meta = await hostMeta({ fresh: true });
1785
+ const doc = meta.document;
1786
+ const stored = typeof doc?.etag === "string" && doc.etag ? doc.etag : null;
1787
+ if (!stored) return { landed: null, resend: false };
1788
+ if (doc.saveId && doc.saveId === attempt.saveId) {
1789
+ recordEtag(stored);
1790
+ unknownAttempt = null;
1791
+ return { landed: { msg: "Saved", msgType: "success", etag: stored }, resend: false };
1792
+ }
1793
+ if (doc.saveId) unknownAttempt = null;
1794
+ return { landed: null, resend: !!attempt.etag, etag: attempt.etag };
1795
+ }
1796
+ async function sendSave(content) {
1797
+ let saveId = mintSaveId();
1798
+ let etag = stampToSend();
1799
+ let reSent = false;
1800
+ let duplicateResolved = false;
1801
+ for (; ; ) {
1802
+ try {
1803
+ return await sendOnce(content, saveId, etag);
1804
+ } catch (err) {
1805
+ if (err.name === "AbortError") {
1806
+ if (reSent) throw err;
1807
+ const { landed, resend, etag: original } = await recoverUnknownSave();
1808
+ if (landed) return landed;
1809
+ if (!resend) throw err;
1810
+ etag = original;
1811
+ reSent = true;
1812
+ continue;
1813
+ }
1814
+ if (err.status === 412 && !duplicateResolved && sentByThisTab(err.saveId)) {
1815
+ recordEtag(err.etag ?? null);
1816
+ unknownAttempt = null;
1817
+ saveId = mintSaveId();
1818
+ etag = stampToSend();
1819
+ duplicateResolved = true;
1820
+ continue;
1821
+ }
1822
+ throw err;
1823
+ }
1824
+ }
1825
+ }
1607
1826
  function saveHtml(html, callback = () => {
1608
1827
  }) {
1609
1828
  return new Promise((resolve) => {
@@ -1651,17 +1870,21 @@
1651
1870
  });
1652
1871
  });
1653
1872
  }
1654
- var saveInProgress, SAVE_PATH, SAVE_TIMEOUT_MS;
1873
+ var saveInProgress, SAVE_PATH, SAVE_TIMEOUT_MS, sentIds, SENT_ID_MEMORY, unknownAttempt;
1655
1874
  var init_save_core = __esm({
1656
1875
  "src/core/save-core.js"() {
1657
1876
  init_is_edit_mode();
1658
1877
  init_user_gesture();
1659
1878
  init_host_attrs();
1660
1879
  init_etag();
1880
+ init_host_meta();
1661
1881
  init_snapshot();
1662
1882
  saveInProgress = false;
1663
1883
  SAVE_PATH = "/_/save";
1664
1884
  SAVE_TIMEOUT_MS = 12e3;
1885
+ sentIds = [];
1886
+ SENT_ID_MEMORY = 8;
1887
+ unknownAttempt = null;
1665
1888
  }
1666
1889
  });
1667
1890
 
@@ -1954,7 +2177,7 @@
1954
2177
  getLastSavedDirty: () => getLastSavedDirty,
1955
2178
  getPageContents: () => getPageContents,
1956
2179
  getUnsavedChanges: () => getUnsavedChanges,
1957
- init: () => init2,
2180
+ init: () => init3,
1958
2181
  initHyperclaySaveButton: () => initHyperclaySaveButton,
1959
2182
  initSaveKeyboardShortcut: () => initSaveKeyboardShortcut,
1960
2183
  isSaveConflicted: () => isSaveConflicted,
@@ -1969,14 +2192,14 @@
1969
2192
  setUnsavedChanges: () => setUnsavedChanges,
1970
2193
  suspendAutosave: () => suspendAutosave
1971
2194
  });
1972
- function setSaveState(state, msg = "", msgType = "") {
2195
+ function setSaveState(state, msg = "", msgType = "", extra = null) {
1973
2196
  if (savingTimeout) {
1974
2197
  clearTimeout(savingTimeout);
1975
2198
  savingTimeout = null;
1976
2199
  }
1977
2200
  document.documentElement.setAttribute("savestatus", state);
1978
2201
  const event = new CustomEvent(`clay:save-${state}`, {
1979
- detail: { msg, msgType, timestamp: Date.now() }
2202
+ detail: { msg, msgType, timestamp: Date.now(), ...extra || {} }
1980
2203
  });
1981
2204
  document.dispatchEvent(event);
1982
2205
  }
@@ -2029,7 +2252,10 @@
2029
2252
  releaseConflictHold();
2030
2253
  } else if (result2.msgType === "conflict") {
2031
2254
  holdForConflict();
2032
- setSaveState("conflict", result2.msg, result2.msgType);
2255
+ setSaveState("conflict", result2.msg, result2.msgType, {
2256
+ changedBy: result2.changedBy ?? null,
2257
+ afterTimeout: result2.afterTimeout === true
2258
+ });
2033
2259
  } else if (result2.msgType !== "skipped") {
2034
2260
  if (!navigator.onLine) {
2035
2261
  setSaveState("offline", result2.msg);
@@ -2040,6 +2266,7 @@
2040
2266
  }
2041
2267
  function drainPendingSave() {
2042
2268
  if (!pendingSave) return;
2269
+ if (saveFateIsUnknown()) return;
2043
2270
  pendingSave = false;
2044
2271
  savePage();
2045
2272
  }
@@ -2272,7 +2499,7 @@
2272
2499
  }
2273
2500
  });
2274
2501
  }
2275
- function init2() {
2502
+ function init3() {
2276
2503
  if (!isEditMode) return;
2277
2504
  seedEtag();
2278
2505
  initUserGesture();
@@ -2324,34 +2551,19 @@
2324
2551
  } else {
2325
2552
  initBaselineCapture();
2326
2553
  }
2327
- init2();
2554
+ init3();
2328
2555
  save_default = savePage;
2329
2556
  }
2330
2557
  });
2331
2558
 
2332
2559
  // src/core/save-conflict-notice.js
2333
2560
  var save_conflict_notice_exports = {};
2334
- function style(el3, rules) {
2335
- for (const rule of rules) {
2336
- const at = rule.indexOf(":");
2337
- el3.style.setProperty(rule.slice(0, at).trim(), rule.slice(at + 1).trim(), "important");
2338
- }
2339
- }
2340
- function set(el3, prop, value) {
2341
- el3.style.setProperty(prop, value, "important");
2342
- }
2343
- function make(tag, rules, text) {
2344
- const el3 = document.createElement(tag);
2345
- style(el3, rules);
2346
- if (text) el3.textContent = text;
2347
- return el3;
2348
- }
2349
2561
  function button(label, rules) {
2350
2562
  const b = make("button", [
2351
2563
  "all:initial",
2352
2564
  "box-sizing:border-box",
2353
2565
  "cursor:pointer",
2354
- `font:${FONT}`,
2566
+ `font:${FONT2}`,
2355
2567
  "font-weight:500",
2356
2568
  "border-radius:6px",
2357
2569
  "padding:6px 12px",
@@ -2367,14 +2579,14 @@
2367
2579
  b.addEventListener("blur", () => set(b, "outline", "none"));
2368
2580
  return b;
2369
2581
  }
2370
- function place() {
2371
- if (!root) return;
2582
+ function place2() {
2583
+ if (!root2) return;
2372
2584
  const vv = window.visualViewport;
2373
2585
  const lift = vv ? Math.max(0, window.innerHeight - vv.height - vv.offsetTop) : 0;
2374
- set(root, "bottom", `calc(${16 + lift}px + env(safe-area-inset-bottom,0px))`);
2586
+ set(root2, "bottom", `calc(${16 + lift}px + env(safe-area-inset-bottom,0px))`);
2375
2587
  }
2376
- function build() {
2377
- root = make("div", [
2588
+ function build2() {
2589
+ root2 = make("div", [
2378
2590
  "position:fixed",
2379
2591
  "left:50%",
2380
2592
  "transform:translateX(-50%)",
@@ -2387,31 +2599,31 @@
2387
2599
  "justify-content:center",
2388
2600
  "padding:9px 12px",
2389
2601
  "border-radius:10px",
2390
- `background:${BG}`,
2391
- `color:${INK}`,
2392
- `border:1px solid ${EDGE}`,
2602
+ `background:${BG2}`,
2603
+ `color:${INK2}`,
2604
+ `border:1px solid ${EDGE2}`,
2393
2605
  "box-shadow:0 6px 24px rgba(0,0,0,.32),0 1px 2px rgba(0,0,0,.24)",
2394
- `font:${FONT}`,
2606
+ `font:${FONT2}`,
2395
2607
  "text-align:left"
2396
2608
  ]);
2397
- root.setAttribute("clay", "no-save no-watch no-snapshot");
2398
- root.setAttribute("data-clay-conflict", "");
2399
- root.setAttribute("role", "alert");
2609
+ root2.setAttribute("clay", "no-save no-watch no-snapshot");
2610
+ root2.setAttribute("data-clay-conflict", "");
2611
+ root2.setAttribute("role", "alert");
2400
2612
  line = make("span", ["margin-right:2px"]);
2401
- drop = button("Load theirs", [`color:${INK}`, "opacity:.72", "padding:6px 8px"]);
2402
- keep = button("Keep mine", [`background:${INK}`, `color:${BG}`, "font-weight:600"]);
2613
+ drop = button("Load theirs", [`color:${INK2}`, "opacity:.72", "padding:6px 8px"]);
2614
+ keep = button("Keep mine", [`background:${INK2}`, `color:${BG2}`, "font-weight:600"]);
2403
2615
  drop.addEventListener("click", onDrop);
2404
2616
  keep.addEventListener("click", onKeep);
2405
- root.append(line, drop, keep);
2617
+ root2.append(line, drop, keep);
2406
2618
  if (!still()) {
2407
- set(root, "transition", "opacity .18s");
2408
- set(root, "opacity", "0");
2619
+ set(root2, "transition", "opacity .18s");
2620
+ set(root2, "opacity", "0");
2409
2621
  }
2410
- document.body.appendChild(root);
2411
- place();
2412
- window.visualViewport?.addEventListener("resize", place);
2413
- window.visualViewport?.addEventListener("scroll", place);
2414
- requestAnimationFrame(() => set(root, "opacity", "1"));
2622
+ document.body.appendChild(root2);
2623
+ place2();
2624
+ window.visualViewport?.addEventListener("resize", place2);
2625
+ window.visualViewport?.addEventListener("scroll", place2);
2626
+ requestAnimationFrame(() => set(root2, "opacity", "1"));
2415
2627
  }
2416
2628
  function onDrop() {
2417
2629
  if (armed) {
@@ -2432,7 +2644,7 @@
2432
2644
  armed = false;
2433
2645
  drop.textContent = "Load theirs";
2434
2646
  set(drop, "opacity", ".72");
2435
- set(drop, "color", INK);
2647
+ set(drop, "color", INK2);
2436
2648
  set(drop, "box-shadow", "none");
2437
2649
  }
2438
2650
  async function onKeep() {
@@ -2461,44 +2673,49 @@
2461
2673
  }
2462
2674
  }
2463
2675
  function show(e) {
2464
- if (!root) build();
2465
- set(root, "display", "flex");
2466
- const where = SOURCES[e?.detail?.changedBy] || "elsewhere";
2467
- line.textContent = `This page was updated ${where}. Saving is paused.`;
2676
+ if (!root2) build2();
2677
+ set(root2, "display", "flex");
2678
+ const named = SOURCES[e?.detail?.changedBy];
2679
+ if (!named && e?.detail?.afterTimeout) {
2680
+ line.textContent = "This page changed elsewhere, possibly by your own save that timed out. Your edits here are safe, and nothing will be overwritten until you choose.";
2681
+ } else {
2682
+ line.textContent = `This page changed ${named || "elsewhere"}. Your edits here are safe, and nothing will be overwritten until you choose.`;
2683
+ }
2468
2684
  keep.textContent = "Keep mine";
2469
2685
  set(keep, "opacity", "1");
2470
2686
  busy = false;
2471
2687
  disarm();
2472
- place();
2688
+ place2();
2473
2689
  }
2474
2690
  function hide() {
2475
- if (!root) return;
2691
+ if (!root2) return;
2476
2692
  disarm();
2477
- set(root, "display", "none");
2693
+ set(root2, "display", "none");
2478
2694
  }
2479
- function init3() {
2695
+ function init4() {
2480
2696
  if (!isEditMode) return;
2481
2697
  document.addEventListener("clay:save-conflict", show);
2482
2698
  document.addEventListener("clay:save-saved", hide);
2483
2699
  document.addEventListener("keydown", onKeydown);
2484
2700
  }
2485
- var BG, INK, EDGE, WARN, FONT, SOURCES, ARM_MS, root, line, keep, drop, armTimer, armed, busy, still;
2701
+ var BG2, INK2, EDGE2, WARN, FONT2, SOURCES, ARM_MS, root2, line, keep, drop, armTimer, armed, busy, still;
2486
2702
  var init_save_conflict_notice = __esm({
2487
2703
  "src/core/save-conflict-notice.js"() {
2488
2704
  init_is_edit_mode();
2489
2705
  init_dom_ready();
2490
- BG = "var(--clay-conflict-bg,#222)";
2491
- INK = "var(--clay-conflict-ink,#fff)";
2492
- EDGE = "var(--clay-conflict-edge,rgba(255,255,255,.28))";
2706
+ init_hostile_css();
2707
+ BG2 = "var(--clay-conflict-bg,#222)";
2708
+ INK2 = "var(--clay-conflict-ink,#fff)";
2709
+ EDGE2 = "var(--clay-conflict-edge,rgba(255,255,255,.28))";
2493
2710
  WARN = "var(--clay-conflict-warn,#e3a33f)";
2494
- FONT = "14px/1.45 system-ui,-apple-system,'Segoe UI',sans-serif";
2711
+ FONT2 = "14px/1.45 system-ui,-apple-system,'Segoe UI',sans-serif";
2495
2712
  SOURCES = {
2496
2713
  "another-tab": "in another tab",
2497
2714
  "another-person": "by someone else",
2498
2715
  "an-agent": "by an agent"
2499
2716
  };
2500
2717
  ARM_MS = 5e3;
2501
- root = null;
2718
+ root2 = null;
2502
2719
  line = null;
2503
2720
  keep = null;
2504
2721
  drop = null;
@@ -2506,7 +2723,7 @@
2506
2723
  armed = false;
2507
2724
  busy = false;
2508
2725
  still = () => !!window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
2509
- dom_ready_default(init3);
2726
+ dom_ready_default(init4);
2510
2727
  }
2511
2728
  });
2512
2729
 
@@ -2595,7 +2812,7 @@
2595
2812
  var persist_exports = {};
2596
2813
  __export(persist_exports, {
2597
2814
  default: () => enablePersistentFormInputValues,
2598
- init: () => init4
2815
+ init: () => init5
2599
2816
  });
2600
2817
  function enablePersistentFormInputValues(filterBySelector = PERSIST2) {
2601
2818
  const inputSelector = `input${filterBySelector}:not([type="password"]):not([type="hidden"]):not([type="file"])`;
@@ -2628,7 +2845,7 @@
2628
2845
  finalize(selectSelector);
2629
2846
  });
2630
2847
  }
2631
- function init4() {
2848
+ function init5() {
2632
2849
  enablePersistentFormInputValues(PERSIST2);
2633
2850
  }
2634
2851
  var init_persist = __esm({
@@ -2636,7 +2853,7 @@
2636
2853
  init_snapshot();
2637
2854
  init_control_serialize_vendor();
2638
2855
  init_attr_aliases();
2639
- init4();
2856
+ init5();
2640
2857
  }
2641
2858
  });
2642
2859
 
@@ -2656,15 +2873,15 @@
2656
2873
  enableContentEditable();
2657
2874
  });
2658
2875
  }
2659
- function enableContentEditable(root2 = document) {
2660
- root2.querySelectorAll(SELECTOR).forEach((el3) => {
2876
+ function enableContentEditable(root3 = document) {
2877
+ root3.querySelectorAll(SELECTOR).forEach((el3) => {
2661
2878
  let val = el3.getAttribute("inert-contenteditable");
2662
2879
  if (!["false", "plaintext-only"].includes(val)) val = "true";
2663
2880
  el3.setAttribute("contenteditable", val);
2664
2881
  el3.removeAttribute("inert-contenteditable");
2665
2882
  });
2666
2883
  }
2667
- function init5() {
2884
+ function init6() {
2668
2885
  disableContentEditableBeforeSave();
2669
2886
  enableContentEditableForAdminOnPageLoad();
2670
2887
  }
@@ -2695,15 +2912,15 @@
2695
2912
  enableAdminInputs();
2696
2913
  });
2697
2914
  }
2698
- function enableAdminInputs(root2 = document) {
2699
- root2.querySelectorAll(SELECTOR_DISABLED).forEach((input) => {
2915
+ function enableAdminInputs(root3 = document) {
2916
+ root3.querySelectorAll(SELECTOR_DISABLED).forEach((input) => {
2700
2917
  input.removeAttribute("disabled");
2701
2918
  });
2702
- root2.querySelectorAll(SELECTOR_READONLY).forEach((input) => {
2919
+ root3.querySelectorAll(SELECTOR_READONLY).forEach((input) => {
2703
2920
  input.removeAttribute("readonly");
2704
2921
  });
2705
2922
  }
2706
- function init6() {
2923
+ function init7() {
2707
2924
  disableAdminInputsBeforeSave();
2708
2925
  enableAdminInputsOnPageLoad();
2709
2926
  }
@@ -2734,8 +2951,8 @@
2734
2951
  enableOnClick();
2735
2952
  });
2736
2953
  }
2737
- function enableOnClick(root2 = document) {
2738
- root2.querySelectorAll(SELECTOR2).forEach((el3) => {
2954
+ function enableOnClick(root3 = document) {
2955
+ root3.querySelectorAll(SELECTOR2).forEach((el3) => {
2739
2956
  const val = el3.getAttribute("inert-onclick");
2740
2957
  if (val) {
2741
2958
  el3.setAttribute("onclick", val);
@@ -2743,7 +2960,7 @@
2743
2960
  }
2744
2961
  });
2745
2962
  }
2746
- function init7() {
2963
+ function init8() {
2747
2964
  disableOnClickBeforeSave();
2748
2965
  enableOnClickForAdminOnPageLoad();
2749
2966
  }
@@ -2780,14 +2997,14 @@
2780
2997
  enableAdminResources();
2781
2998
  });
2782
2999
  }
2783
- function enableAdminResources(root2 = document) {
2784
- const live = (root2.ownerDocument || root2) === document;
2785
- root2.querySelectorAll(SELECTOR_INERT).forEach((resource) => {
3000
+ function enableAdminResources(root3 = document) {
3001
+ const live = (root3.ownerDocument || root3) === document;
3002
+ root3.querySelectorAll(SELECTOR_INERT).forEach((resource) => {
2786
3003
  makeActive(resource);
2787
3004
  if (live) resource.replaceWith(resource.cloneNode(true));
2788
3005
  });
2789
3006
  }
2790
- function init8() {
3007
+ function init9() {
2791
3008
  disableAdminResourcesBeforeSave();
2792
3009
  enableAdminResourcesOnPageLoad();
2793
3010
  }
@@ -2807,15 +3024,15 @@
2807
3024
  var admin_attrs_exports = {};
2808
3025
  __export(admin_attrs_exports, {
2809
3026
  default: () => admin_attrs_default,
2810
- init: () => init9
3027
+ init: () => init10
2811
3028
  });
2812
- function init9() {
3029
+ function init10() {
2813
3030
  if (initialized) return;
2814
3031
  initialized = true;
2815
- init5();
2816
3032
  init6();
2817
3033
  init7();
2818
3034
  init8();
3035
+ init9();
2819
3036
  }
2820
3037
  var initialized, admin_attrs_default;
2821
3038
  var init_admin_attrs = __esm({
@@ -2825,8 +3042,8 @@
2825
3042
  init_admin_onclick();
2826
3043
  init_admin_resources();
2827
3044
  initialized = false;
2828
- init9();
2829
- admin_attrs_default = init9;
3045
+ init10();
3046
+ admin_attrs_default = init10;
2830
3047
  }
2831
3048
  });
2832
3049
 
@@ -2855,7 +3072,7 @@
2855
3072
  inputSaveTimer = setTimeout(savePageThrottled, 1500);
2856
3073
  }, true);
2857
3074
  }
2858
- function init10() {
3075
+ function init11() {
2859
3076
  if (!document.documentElement.matches(AUTOSAVE)) return;
2860
3077
  if (!isEditMode) return;
2861
3078
  initSavePageOnChange();
@@ -2873,8 +3090,8 @@
2873
3090
  AUTOSAVE_DEBOUNCE_MS = 1500;
2874
3091
  AUTOSAVE_MAX_WAIT_MS = 1e4;
2875
3092
  inputSaveTimer = null;
2876
- init10();
2877
- autosave_default = init10;
3093
+ init11();
3094
+ autosave_default = init11;
2878
3095
  }
2879
3096
  });
2880
3097
 
@@ -2927,7 +3144,7 @@
2927
3144
  }
2928
3145
  }
2929
3146
  }
2930
- function init11() {
3147
+ function init12() {
2931
3148
  log("init, isEditMode:", isEditMode);
2932
3149
  if (!isEditMode) return;
2933
3150
  captureAll();
@@ -2952,7 +3169,7 @@
2952
3169
  originals = /* @__PURE__ */ new WeakMap();
2953
3170
  saveFreeze = { debug: false };
2954
3171
  pending = /* @__PURE__ */ new WeakMap();
2955
- init11();
3172
+ init12();
2956
3173
  save_freeze_default = saveFreeze;
2957
3174
  }
2958
3175
  });
@@ -2975,14 +3192,14 @@
2975
3192
  }
2976
3193
  });
2977
3194
  }
2978
- function init12() {
3195
+ function init13() {
2979
3196
  document.addEventListener("clay:save-saved", broadcast);
2980
3197
  }
2981
3198
  var onaftersave_default;
2982
3199
  var init_onaftersave = __esm({
2983
3200
  "src/attrs/onaftersave.js"() {
2984
- init12();
2985
- onaftersave_default = init12;
3201
+ init13();
3202
+ onaftersave_default = init13;
2986
3203
  }
2987
3204
  });
2988
3205
 
@@ -3023,7 +3240,7 @@
3023
3240
  mutation_default.resume();
3024
3241
  }
3025
3242
  }
3026
- function init13() {
3243
+ function init14() {
3027
3244
  document.addEventListener("clay:save-saved", () => {
3028
3245
  document.querySelectorAll("[refetch-on-save]").forEach(swapElement);
3029
3246
  });
@@ -3033,8 +3250,8 @@
3033
3250
  "src/attrs/refetch-on-save.js"() {
3034
3251
  init_mutation();
3035
3252
  init_authored_url();
3036
- init13();
3037
- refetch_on_save_default = init13;
3253
+ init14();
3254
+ refetch_on_save_default = init14;
3038
3255
  }
3039
3256
  });
3040
3257
 
@@ -6975,7 +7192,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
6975
7192
  node.style.opacity = "0";
6976
7193
  }, 2200);
6977
7194
  }
6978
- function init14() {
7195
+ function init15() {
6979
7196
  if (!isEditMode) return;
6980
7197
  for (const state of Object.keys(LABELS)) {
6981
7198
  document.addEventListener("clay:save-" + state, () => show2(state));
@@ -6996,7 +7213,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
6996
7213
  ALARMING = /* @__PURE__ */ new Set(["error", "offline"]);
6997
7214
  el = null;
6998
7215
  hideTimer = null;
6999
- dom_ready_default(init14);
7216
+ dom_ready_default(init15);
7000
7217
  }
7001
7218
  });
7002
7219
 
@@ -8426,8 +8643,8 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
8426
8643
  fillCloneIds(liveKids[i], cloneKids[i], liveWeakMap, idOf);
8427
8644
  }
8428
8645
  }
8429
- function fillParsedIds(root2, identityMap, idOf) {
8430
- if (!root2 || !identityMap) return;
8646
+ function fillParsedIds(root3, identityMap, idOf) {
8647
+ if (!root3 || !identityMap) return;
8431
8648
  const visit = (el3, path) => {
8432
8649
  const id = identityMap[path];
8433
8650
  if (id) idOf.set(el3, id);
@@ -8436,7 +8653,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
8436
8653
  visit(kids[i], path === "" ? String(i) : `${path}.${i}`);
8437
8654
  }
8438
8655
  };
8439
- visit(root2, "");
8656
+ visit(root3, "");
8440
8657
  }
8441
8658
  function protectPeerDoc({ newDoc, parsedWeakMap, baseHtml, baseIdentityMap, liveWeakMap }) {
8442
8659
  if (typeof baseHtml !== "string" || !baseHtml) {
@@ -8596,6 +8813,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
8596
8813
  this.resumeId = null;
8597
8814
  this.debounceMs = 150;
8598
8815
  this.debounceTimer = null;
8816
+ this._savedSnapshot = null;
8599
8817
  this._sendInFlight = false;
8600
8818
  this._queuedSend = null;
8601
8819
  this._profile = null;
@@ -8611,6 +8829,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
8611
8829
  this._pendingHtml = null;
8612
8830
  this._pendingSeq = null;
8613
8831
  this._pendingIdentityMap = null;
8832
+ this._pendingEtag = null;
8614
8833
  this._morphInFlight = false;
8615
8834
  this._rafHandle = null;
8616
8835
  this._pendingExternal = null;
@@ -8681,6 +8900,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
8681
8900
  }
8682
8901
  this.lastHtml = null;
8683
8902
  this._lastIdentityMap = null;
8903
+ this._savedSnapshot = null;
8684
8904
  this._applyGen++;
8685
8905
  this.lastSeenSeq = 0;
8686
8906
  this._lastExternalSeq = 0;
@@ -8696,6 +8916,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
8696
8916
  this.listenForSnapshots();
8697
8917
  this._saveSavedHandler = () => {
8698
8918
  this._saveEpoch++;
8919
+ this._relayCommit();
8699
8920
  };
8700
8921
  document.addEventListener("clay:save-saved", this._saveSavedHandler);
8701
8922
  }
@@ -8785,8 +9006,8 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
8785
9006
  * Paths use the same dot-segment scheme as _buildIdentityMap so the
8786
9007
  * receiver can look up IDs by the path the sender emitted.
8787
9008
  */
8788
- _walkParsedTree(root2, cb) {
8789
- if (!root2) return;
9009
+ _walkParsedTree(root3, cb) {
9010
+ if (!root3) return;
8790
9011
  const visit = (el3, path) => {
8791
9012
  cb(el3, path);
8792
9013
  const kids = el3.children;
@@ -8794,7 +9015,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
8794
9015
  visit(kids[i], path === "" ? String(i) : `${path}.${i}`);
8795
9016
  }
8796
9017
  };
8797
- visit(root2, "");
9018
+ visit(root3, "");
8798
9019
  }
8799
9020
  /**
8800
9021
  * Fill liveWeakMap entries for live elements that the matcher's
@@ -8900,31 +9121,32 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
8900
9121
  }));
8901
9122
  }
8902
9123
  /**
8903
- * Take a stamp that arrived with no document (spec §6).
9124
+ * Drop a stamp that arrived with no document (spec §6).
8904
9125
  *
8905
- * The host sends one whenever the file on disk changes, because an editing tab
8906
- * never receives the saved document itself: that lane is for viewers, and
8907
- * pushing a saved document onto an editor would replace work in progress.
9126
+ * ⚠️ This used to TAKE it, and taking it was wrong. A stamp says "disk is at this
9127
+ * version"; adopting it says "and I hold those bytes". Only the second claim
9128
+ * makes the next save safe, and a frame with no content is no evidence for it.
8908
9129
  *
8909
- * Taking it is what makes live sync and the conflict check agree. An editor
8910
- * whose peer frames are merging is already looking at the other tab's content,
8911
- * so refusing its next save would be a conflict about nothing.
9130
+ * The race is real and it loses work. A peer's save and that peer's snapshot
9131
+ * relay are two concurrent requests, so a stamp travelling on its own can arrive
9132
+ * first. This tab would record it while its DOM still held the older content,
9133
+ * and its next save would then pass If-Match and overwrite the save it had never
9134
+ * received. The hold flags cannot cover that: a lane holds when an incoming
9135
+ * change could NOT be merged, not when one has yet to arrive.
8912
9136
  *
8913
- * But only while this tab is in step. A held lane means live sync saw an
8914
- * incoming change, could not merge it into an unsaved local edit, and kept this
8915
- * tab's version instead, so the DOM here is knowingly missing what disk holds.
8916
- * Taking the new stamp there would let this tab's next save replace that change
8917
- * with nobody seeing it. That is the one loss live sync cannot prevent on its
8918
- * own: a held tab "converges through its own next save", and that convergence
8919
- * IS the overwrite. Refusing the stamp turns it into a conflict somebody is
8920
- * told about.
9137
+ * So a stamp now rides on the snapshot the saving tab relays after its save
9138
+ * returns, and `_doApplyUpdate` records it only once that content has merged.
9139
+ * Nothing sends a bare stamp any more — hyperclay's was deleted, and it had
9140
+ * never reached anyone, because the relay library refuses a payload with no
9141
+ * document in it. This stays as the rule rather than as compatibility: if some
9142
+ * host does send one, dropping it is correct.
8921
9143
  *
8922
9144
  * @param {Object} data - The decoded frame
8923
- * @returns {boolean} True when this was a stamp frame and nothing should morph
9145
+ * @returns {boolean} True when this was a stamp-only frame and nothing should morph
8924
9146
  */
8925
9147
  _applyEtagFrame(data) {
8926
9148
  if (typeof data.etag !== "string" || typeof data.html === "string") return false;
8927
- if (!this._heldLive && !this._heldExt) recordEtag(data.etag);
9149
+ this._log("Dropping a stamp that arrived with no document: nothing proves this tab holds those bytes");
8928
9150
  return true;
8929
9151
  }
8930
9152
  /**
@@ -8983,9 +9205,15 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
8983
9205
  console.error("[LiveSync] Received invalid html, ignoring");
8984
9206
  return;
8985
9207
  }
9208
+ const etag = typeof data.etag === "string" && data.etag ? data.etag : null;
9209
+ if (etag && html === this.lastHtml) {
9210
+ this._log(`Taking the stamp from a peer save of content already applied (seq=${seq})`);
9211
+ recordEtag(etag);
9212
+ return;
9213
+ }
8986
9214
  this._log(`Received update from: ${sender} (my clientId: ${this.clientId}, seq=${seq})`);
8987
- this.applyUpdate(html, seq, identityMap);
8988
- if (this.onUpdate) this.onUpdate({ html, sender, seq, identityMap });
9215
+ this.applyUpdate(html, seq, identityMap, etag);
9216
+ if (this.onUpdate) this.onUpdate({ html, sender, seq, identityMap, etag });
8989
9217
  };
8990
9218
  this.sse.onerror = () => {
8991
9219
  if (this.sse.readyState === EventSource.CONNECTING) {
@@ -9012,6 +9240,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
9012
9240
  this._log("snapshot-ready received, preparing to send");
9013
9241
  const html = serializeForSync(clone);
9014
9242
  const identityMap = this._buildIdentityMap(document.documentElement, clone);
9243
+ this._savedSnapshot = { html, identityMap };
9015
9244
  this.sendUpdate(html, identityMap);
9016
9245
  };
9017
9246
  document.addEventListener("clay:snapshot-ready", this._snapshotHandler);
@@ -9049,6 +9278,59 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
9049
9278
  this._queuedSend = null;
9050
9279
  this._postUpdate(queued.html, queued.identityMap);
9051
9280
  }
9281
+ /**
9282
+ * Tell the other editors what this tab's save stored, and under which stamp.
9283
+ *
9284
+ * Runs on clay:save-saved, by which point the save response has already been
9285
+ * recorded, so `lastSeenEtag()` is the stamp for the bytes that just landed.
9286
+ *
9287
+ * The content is `lastHtml`, the snapshot this tab most recently relayed, which
9288
+ * is the state the save was taken from: the save pipeline dispatches
9289
+ * clay:snapshot-ready on its way to capturing what to send, so that relay has
9290
+ * already gone out. Re-sending those same bytes is not redundant, because the
9291
+ * stamp is the new information and a stamp may never travel without the content
9292
+ * it describes. A receiver whose baseline already matches takes the stamp and
9293
+ * skips the morph.
9294
+ */
9295
+ _relayCommit() {
9296
+ const etag = lastSeenEtag();
9297
+ if (!etag) return;
9298
+ if (this.isDestroyed || this.isPaused) return;
9299
+ const pending2 = this._savedSnapshot;
9300
+ this._savedSnapshot = null;
9301
+ if (!pending2 || typeof pending2.html !== "string") return;
9302
+ this._postCommit(pending2.html, etag, pending2.identityMap);
9303
+ }
9304
+ /**
9305
+ * Post a snapshot whose point is the stamp attached to it.
9306
+ *
9307
+ * Deliberately not `_postUpdate`: that one returns early when the html matches
9308
+ * `lastHtml`, and the whole point here is that the stamp is the new information
9309
+ * even when the bytes are not. It also does not touch `lastHtml` or the
9310
+ * single-flight queue, because it must not displace a real snapshot waiting to
9311
+ * go out. It carries the identityMap captured with these bytes, so a receiver
9312
+ * that morphs on this frame pairs elements the same way it would on the
9313
+ * ordinary relay of the same content.
9314
+ */
9315
+ _postCommit(html, etag, identityMap) {
9316
+ const profile = this._profile;
9317
+ if (!profile) return;
9318
+ fetch(new URL(profile.relayPath, window.location.origin).href, {
9319
+ method: "POST",
9320
+ headers: {
9321
+ "Content-Type": "application/json",
9322
+ [profile.documentHeader]: window.location.href
9323
+ },
9324
+ body: JSON.stringify({
9325
+ [profile.snapshotKey]: html,
9326
+ sender: this.clientId,
9327
+ identityMap,
9328
+ etag
9329
+ })
9330
+ }).catch((err) => {
9331
+ this._log("Commit relay failed: " + (err && err.message));
9332
+ });
9333
+ }
9052
9334
  _postUpdate(html, identityMap) {
9053
9335
  if (html === this.lastHtml) {
9054
9336
  this._log("Skipping send - HTML unchanged");
@@ -9105,7 +9387,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
9105
9387
  const info = data.data;
9106
9388
  if (info && info.kind === "external-change") {
9107
9389
  if (typeof info.html === "string") {
9108
- this._enqueueExternal(info.html, data.seq);
9390
+ this._enqueueExternal(info.html, data.seq, info.etag);
9109
9391
  } else {
9110
9392
  this._fetchExternalChange(data.seq);
9111
9393
  }
@@ -9117,7 +9399,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
9117
9399
  }
9118
9400
  return false;
9119
9401
  }
9120
- _enqueueExternal(html, seq) {
9402
+ _enqueueExternal(html, seq, etag) {
9121
9403
  if (typeof seq === "number") {
9122
9404
  if (seq <= this._lastExternalSeq) {
9123
9405
  this._log(`Dropping replayed external change: seq=${seq}`);
@@ -9125,7 +9407,12 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
9125
9407
  }
9126
9408
  this._lastExternalSeq = seq;
9127
9409
  }
9128
- this._pendingExternal = { html, seq, saveEpoch: this._saveEpoch };
9410
+ this._pendingExternal = {
9411
+ html,
9412
+ seq,
9413
+ saveEpoch: this._saveEpoch,
9414
+ etag: typeof etag === "string" && etag ? etag : null
9415
+ };
9129
9416
  this._scheduleNextFrame();
9130
9417
  }
9131
9418
  /**
@@ -9165,7 +9452,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
9165
9452
  }
9166
9453
  return;
9167
9454
  }
9168
- this._pendingExternal = { html, seq, saveEpoch: epoch };
9455
+ this._pendingExternal = { html, seq, saveEpoch: epoch, etag: null };
9169
9456
  this._scheduleNextFrame();
9170
9457
  }).catch((err) => {
9171
9458
  this._log("External-change fetch failed", err);
@@ -9188,11 +9475,12 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
9188
9475
  * @param {number} [seq] - Optional monotonic seq from the server
9189
9476
  * @param {Object} [identityMap] - Optional element-identity map from sender
9190
9477
  */
9191
- applyUpdate(html, seq, identityMap) {
9478
+ applyUpdate(html, seq, identityMap, etag) {
9192
9479
  if (this.isDestroyed) return;
9193
9480
  this._pendingHtml = html;
9194
9481
  this._pendingSeq = seq;
9195
9482
  this._pendingIdentityMap = identityMap;
9483
+ this._pendingEtag = etag ?? null;
9196
9484
  this._scheduleNextFrame();
9197
9485
  }
9198
9486
  /**
@@ -9236,7 +9524,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
9236
9524
  } else {
9237
9525
  this._morphInFlight = true;
9238
9526
  try {
9239
- await this._doApplyExternal(ext.html, ext.seq);
9527
+ await this._doApplyExternal(ext.html, ext.seq, ext.etag);
9240
9528
  } catch (err) {
9241
9529
  console.error("[LiveSync] applyExternal failed:", err);
9242
9530
  } finally {
@@ -9247,13 +9535,15 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
9247
9535
  const html = this._pendingHtml;
9248
9536
  const seq = this._pendingSeq;
9249
9537
  const identityMap = this._pendingIdentityMap;
9538
+ const etag = this._pendingEtag;
9250
9539
  this._pendingHtml = null;
9251
9540
  this._pendingSeq = null;
9252
9541
  this._pendingIdentityMap = null;
9542
+ this._pendingEtag = null;
9253
9543
  if (html == null) return;
9254
9544
  this._morphInFlight = true;
9255
9545
  try {
9256
- await this._doApplyUpdate(html, seq, identityMap);
9546
+ await this._doApplyUpdate(html, seq, identityMap, etag);
9257
9547
  } catch (err) {
9258
9548
  console.error("[LiveSync] applyUpdate failed:", err);
9259
9549
  } finally {
@@ -9285,7 +9575,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
9285
9575
  * @param {Object} [identityMap]
9286
9576
  * @returns {Promise<void>}
9287
9577
  */
9288
- async _doApplyUpdate(html, seq, identityMap) {
9578
+ async _doApplyUpdate(html, seq, identityMap, etag) {
9289
9579
  this._log("applyUpdate - pausing mutations and morphing");
9290
9580
  this.isPaused = true;
9291
9581
  const scrollX = window.scrollX;
@@ -9364,6 +9654,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
9364
9654
  this.lastHtml = html;
9365
9655
  this._lastIdentityMap = identityMap && typeof identityMap === "object" && !Array.isArray(identityMap) ? identityMap : null;
9366
9656
  this._applyGen++;
9657
+ if (typeof etag === "string" && etag) recordEtag(etag);
9367
9658
  if (this.lane === "live" && retainedRoots === 0 && !pageMaybeDirty()) {
9368
9659
  setLastSavedBaselines(...pairedBaseline());
9369
9660
  }
@@ -9396,7 +9687,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
9396
9687
  * holds or a later dirty peer apply misclassifies this frame's content as
9397
9688
  * local edits.
9398
9689
  */
9399
- async _doApplyExternal(html, seq) {
9690
+ async _doApplyExternal(html, seq, etag = null) {
9400
9691
  this._log(`applyExternal - external disk change (seq=${seq})`);
9401
9692
  this.isPaused = true;
9402
9693
  const scrollX = window.scrollX;
@@ -9417,7 +9708,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
9417
9708
  this._holdRetryExt = setTimeout(() => {
9418
9709
  this._holdRetryExt = null;
9419
9710
  if (this.isDestroyed || this._pendingExternal != null) return;
9420
- this._pendingExternal = { html, seq, saveEpoch: epochAtHold };
9711
+ this._pendingExternal = { html, seq, saveEpoch: epochAtHold, etag };
9421
9712
  this._scheduleNextFrame();
9422
9713
  }, 3e3);
9423
9714
  return;
@@ -9443,6 +9734,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
9443
9734
  callbacks: { beforeAttributeUpdated }
9444
9735
  });
9445
9736
  window.scrollTo(scrollX, scrollY);
9737
+ if (typeof etag === "string" && etag) recordEtag(etag);
9446
9738
  if (this.lane === "live" && retainedRoots === 0 && !pageMaybeDirty()) {
9447
9739
  setLastSavedBaselines(...pairedBaseline());
9448
9740
  setUnsavedChanges(false);
@@ -9452,7 +9744,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
9452
9744
  this._applyGen++;
9453
9745
  }
9454
9746
  document.dispatchEvent(new CustomEvent("clay:sync-applied", {
9455
- detail: { seq, source: "disk" }
9747
+ detail: { seq, source: "disk", etag: typeof etag === "string" && etag ? etag : null }
9456
9748
  }));
9457
9749
  } finally {
9458
9750
  this._log("applyExternal - morph complete, resuming mutations");
@@ -10374,7 +10666,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
10374
10666
  var sortable_exports = {};
10375
10667
  __export(sortable_exports, {
10376
10668
  default: () => sortable_default,
10377
- init: () => init15,
10669
+ init: () => init16,
10378
10670
  ready: () => ready
10379
10671
  });
10380
10672
  function makeSortable(sortableElem, Sortable) {
@@ -10425,7 +10717,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
10425
10717
  };
10426
10718
  Sortable.create(sortableElem, options);
10427
10719
  }
10428
- async function init15() {
10720
+ async function init16() {
10429
10721
  if (!isEditMode) return;
10430
10722
  const mod = await Promise.resolve().then(() => (init_Sortable_vendor(), Sortable_vendor_exports));
10431
10723
  const Sortable = window.Sortable || mod.default;
@@ -10446,8 +10738,8 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
10446
10738
  "src/plugins/sortable.js"() {
10447
10739
  init_is_edit_mode();
10448
10740
  init_mutation();
10449
- ready = init15();
10450
- sortable_default = init15;
10741
+ ready = init16();
10742
+ sortable_default = init16;
10451
10743
  }
10452
10744
  });
10453
10745
 
@@ -10870,7 +11162,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
10870
11162
  default: () => undo_default,
10871
11163
  undo: () => undo
10872
11164
  });
10873
- function init16() {
11165
+ function init17() {
10874
11166
  if (!isEditMode) return;
10875
11167
  if (typeof document === "undefined") return;
10876
11168
  if (document.readyState === "loading") {
@@ -10884,7 +11176,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
10884
11176
  "src/plugins/undo.js"() {
10885
11177
  init_hyper_undo_vendor();
10886
11178
  init_is_edit_mode();
10887
- init16();
11179
+ init17();
10888
11180
  undo_default = undo;
10889
11181
  }
10890
11182
  });
@@ -17919,9 +18211,9 @@ body.hcms-open.hcms-overlay { overflow: hidden; }
17919
18211
  })();
17920
18212
  window.clay = window.clay || {};
17921
18213
  window.clay.extractData = hyperHtmlApiData.extractData;
17922
- window.clay.applyData = function(root2, data, source) {
17923
- if (root2 && typeof root2.nodeType === "number") return hyperHtmlApiData.applyData(root2, data, source);
17924
- return hyperHtmlApiData.applyData(document, root2, data);
18214
+ window.clay.applyData = function(root3, data, source) {
18215
+ if (root3 && typeof root3.nodeType === "number") return hyperHtmlApiData.applyData(root3, data, source);
18216
+ return hyperHtmlApiData.applyData(document, root3, data);
17925
18217
  };
17926
18218
  window.clay.loaded = window.clay.loaded || {};
17927
18219
  window.clay.loaded.data = Promise.resolve();
@@ -19491,9 +19783,9 @@ ${t}` : `"use strict"; return (${t});`;
19491
19783
  // src/dom/nearest.js
19492
19784
  function nearest(startElem, selector, elementFoundReturnValue = (x) => x) {
19493
19785
  const visited = /* @__PURE__ */ new Set();
19494
- function checkDeep(root2) {
19495
- if (!root2 || visited.has(root2)) return null;
19496
- const queue = [root2];
19786
+ function checkDeep(root3) {
19787
+ if (!root3 || visited.has(root3)) return null;
19788
+ const queue = [root3];
19497
19789
  const localVisited = /* @__PURE__ */ new Set();
19498
19790
  while (queue.length > 0) {
19499
19791
  const node = queue.shift();
@@ -19551,9 +19843,9 @@ ${t}` : `"use strict"; return (${t});`;
19551
19843
  var dom_helpers_exports = {};
19552
19844
  __export(dom_helpers_exports, {
19553
19845
  default: () => dom_helpers_default,
19554
- init: () => init17
19846
+ init: () => init18
19555
19847
  });
19556
- function init17() {
19848
+ function init18() {
19557
19849
  if (HTMLElement.prototype.hasOwnProperty("nearest")) {
19558
19850
  return;
19559
19851
  }
@@ -19701,8 +19993,8 @@ ${t}` : `"use strict"; return (${t});`;
19701
19993
  "src/dom/dom-helpers.js"() {
19702
19994
  init_nearest();
19703
19995
  init_pipe();
19704
- init17();
19705
- dom_helpers_default = init17;
19996
+ init18();
19997
+ dom_helpers_default = init18;
19706
19998
  }
19707
19999
  });
19708
20000
 
@@ -20617,7 +20909,7 @@ ${t}` : `"use strict"; return (${t});`;
20617
20909
  for (const id in triggerMap) validateModalPresence(id);
20618
20910
  return true;
20619
20911
  };
20620
- const init27 = (config) => {
20912
+ const init28 = (config) => {
20621
20913
  const options = Object.assign({}, { openTrigger: "data-micromodal-trigger" }, config);
20622
20914
  const triggers = [...document.querySelectorAll(`[${options.openTrigger}]`)];
20623
20915
  const triggerMap = generateTriggerMap(triggers, options.openTrigger);
@@ -20642,7 +20934,7 @@ ${t}` : `"use strict"; return (${t});`;
20642
20934
  targetModal ? activeModal.closeModalById(targetModal) : activeModal.closeModal();
20643
20935
  activeModal = null;
20644
20936
  };
20645
- return { init: init27, show: show3, close };
20937
+ return { init: init28, show: show3, close };
20646
20938
  })();
20647
20939
  modalCss = `<style class="micromodal-css">
20648
20940
  .micromodal {
@@ -20968,7 +21260,7 @@ ${t}` : `"use strict"; return (${t});`;
20968
21260
  onNo = [];
20969
21261
  onOpen = [];
20970
21262
  let settled = false;
20971
- const dismiss = () => {
21263
+ const dismiss2 = () => {
20972
21264
  if (settled) return;
20973
21265
  settled = true;
20974
21266
  myOnNo.forEach((cb) => cb());
@@ -20980,7 +21272,7 @@ ${t}` : `"use strict"; return (${t});`;
20980
21272
  this.isShowing = false;
20981
21273
  document.body.style.overflow = "";
20982
21274
  }
20983
- this._dismiss = dismiss;
21275
+ this._dismiss = dismiss2;
20984
21276
  document.body.insertAdjacentHTML("afterbegin", "<div clay='no-save no-snapshot' class='micromodal-parent'>" + modalCss + modalHtml + "</div>");
20985
21277
  const modalOverlayElem = document.querySelector(".micromodal__overlay");
20986
21278
  const modalContentElem = document.querySelector(".micromodal__content");
@@ -21047,7 +21339,7 @@ ${t}` : `"use strict"; return (${t});`;
21047
21339
  // we use our own
21048
21340
  // reset everything on close
21049
21341
  onClose: (modal) => {
21050
- dismiss();
21342
+ dismiss2();
21051
21343
  document.querySelector(".micromodal-parent")?.remove();
21052
21344
  html = "";
21053
21345
  yes = "";
@@ -21281,7 +21573,7 @@ ${t}` : `"use strict"; return (${t});`;
21281
21573
  modal_default.open();
21282
21574
  return promise;
21283
21575
  }
21284
- function init18() {
21576
+ function init19() {
21285
21577
  dom_ready_default(() => {
21286
21578
  const micromodalParentElem = document.querySelector(".micromodal-parent");
21287
21579
  if (micromodalParentElem) {
@@ -21299,7 +21591,7 @@ ${t}` : `"use strict"; return (${t});`;
21299
21591
  init_copy_to_clipboard();
21300
21592
  CLOSE_BUTTON_SVG = `<svg viewBox="0 0 134 134" fill="none" xmlns="http://www.w3.org/2000/svg"><path class="micromodal__close-bg" d="M132 132.5 1 1.5h131v131Z" /><path class="micromodal__close-x" fill-rule="evenodd" clip-rule="evenodd" d="M0 0h3v1.5h1.5V3H6v1.5h1.5V6H9v1.5h1.5V9H12v1.5h1.5V12H15v1.5h1.5V15H18v1.5h1.5V18H21v1.5h1.5V21H24v1.5h1.5V24H27v1.5h1.5V27H30v1.5h1.5V30H33v1.5h1.5V33H36v1.5h1.5V36H39v1.5h1.5V39H42v1.5h1.5V42H45v1.5h1.5V45H48v1.5h1.5V48H51v1.5h1.5V51H54v1.5h1.5V54H57v1.5h1.5V57H60v1.5h1.5V60H63v1.5h1.5V63H66v1.5h1.5V66H69v1.5h1.5V69H72v1.5h1.5V72H75v1.5h1.5V75H78v1.5h1.5V78H81v1.5h1.5V81H84v1.5h1.5V84H87v1.5h1.5V87H90v1.5h1.5V90H93v1.5h1.5V93H96v1.5h1.5V96H99v1.5h1.5V99h1.5v1.5h1.5v1.5h1.5v1.5h1.5v1.5h1.5v1.5h1.5v1.5h1.5v1.5h1.5v1.5h1.5v1.5h1.5v1.5h1.5v1.5h1.5v1.5h1.5v1.5h1.5v1.5h1.5v1.5h1.5v1.5h1.5v1.5h1.5v1.5h1.5v1.5h1.5v1.5h1.5v1.5h1.5v3h-3V132H129v-1.5h-1.5V129H126v-1.5h-1.5V126H123v-1.5h-1.5V123H120v-1.5h-1.5V120H117v-1.5h-1.5V117H114v-1.5h-1.5V114H111v-1.5h-1.5V111H108v-1.5h-1.5V108H105v-1.5h-1.5V105H102v-1.5h-1.5V102H99v-1.5h-1.5V99H96v-1.5h-1.5V96H93v-1.5h-1.5V93H90v-1.5h-1.5V90H87v-1.5h-1.5V87H84v-1.5h-1.5V84H81v-1.5h-1.5V81H78v-1.5h-1.5V78H75v-1.5h-1.5V75H72v-1.5h-1.5V72H69v-1.5h-1.5V69H66v-1.5h-1.5V66H63v-1.5h-1.5V63H60v-1.5h-1.5V60H57v-1.5h-1.5V57H54v-1.5h-1.5V54H51v-1.5h-1.5V51H48v-1.5h-1.5V48H45v-1.5h-1.5V45H42v-1.5h-1.5V42H39v-1.5h-1.5V39H36v-1.5h-1.5V36H33v-1.5h-1.5V33H30v-1.5h-1.5V30H27v-1.5h-1.5V27H24v-1.5h-1.5V24H21v-1.5h-1.5V21H18v-1.5h-1.5V18H15v-1.5h-1.5V15H12v-1.5h-1.5V12H9v-1.5H7.5V9H6V7.5H4.5V6H3V4.5H1.5V3H0V0ZM108.8 22h5.2v5.1h-2.6v2.6H109v2.6h-2.6v2.6h-2.6v2.5h-2.6v5.2h2.6V45h2.6v2.6h2.6v2.6h2.5v2.6h2.6V58h-5.1v-2.6h-2.6V53h-2.6v-2.6h-2.6v-2.6h-2.5v-2.6h-5.2v2.6H91v2.6h-2.6v2.6h-2.6v2.5h-2.6V58H78v-5.1h2.6v-2.6H83v-2.6h2.6v-2.6h2.6v-2.5h2.6v-5.2h-2.6V35h-2.6v-2.6h-2.6v-2.6h-2.5v-2.6H78V22h5.2v2.6h2.5V27h2.6v2.6h2.6v2.6h2.5v2.6h5.2v-2.6h2.5v-2.6h2.6v-2.6h2.6v-2.5h2.5V22Z" /></svg>`;
21301
21593
  CONFIRM_BUTTON_SVG = `<div style="width: 28px;"><svg viewBox="0 0 60 33" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M34.5 0.75H43.5V5.25H49V9.75H54.5V14.25H60V18.75H54.5V23.25H49V27.75H43.5V32.25H34.5V27.75H40V23.25H45.5V18.75H0V14.25H45.5V9.75H40V5.25H34.5V0.75Z" fill="white"/></svg></div>`;
21302
- init18();
21594
+ init19();
21303
21595
  }
21304
21596
  });
21305
21597
 
@@ -21406,7 +21698,7 @@ ${t}` : `"use strict"; return (${t});`;
21406
21698
  var options_exports = {};
21407
21699
  __export(options_exports, {
21408
21700
  default: () => options_default,
21409
- init: () => init19,
21701
+ init: () => init20,
21410
21702
  parseOptionAttribute: () => parseOptionAttribute
21411
21703
  });
21412
21704
  function parseOptionAttribute(attrName, attrValue) {
@@ -21423,7 +21715,7 @@ ${t}` : `"use strict"; return (${t});`;
21423
21715
  const values = rawValue.split("|");
21424
21716
  return { name, rawValue, values, kind: match.kind, prefix: match.prefix, negated: match.kind === "show-not" };
21425
21717
  }
21426
- function init19() {
21718
+ function init20() {
21427
21719
  optionVisibility.start();
21428
21720
  }
21429
21721
  var STYLE_NAME, OPTION_PREFIXES, optionVisibility, options_default;
@@ -21557,12 +21849,12 @@ ${t}` : `"use strict"; return (${t});`;
21557
21849
  };
21558
21850
  window.clay.optionVisibility = optionVisibility;
21559
21851
  options_default = optionVisibility;
21560
- init19();
21852
+ init20();
21561
21853
  }
21562
21854
  });
21563
21855
 
21564
21856
  // src/events/onclickaway.js
21565
- function init20() {
21857
+ function init21() {
21566
21858
  document.addEventListener("click", function(event) {
21567
21859
  const elementsWithOnClickAway = document.querySelectorAll("[onclickaway]");
21568
21860
  elementsWithOnClickAway.forEach((element) => {
@@ -21583,7 +21875,7 @@ ${t}` : `"use strict"; return (${t});`;
21583
21875
  });
21584
21876
 
21585
21877
  // src/events/onclickchildren.js
21586
- function init21() {
21878
+ function init22() {
21587
21879
  document.addEventListener("click", function(event) {
21588
21880
  const parent = event.target.closest("[onclickchildren]");
21589
21881
  if (parent && event.target !== parent) {
@@ -21598,7 +21890,7 @@ ${t}` : `"use strict"; return (${t});`;
21598
21890
  });
21599
21891
 
21600
21892
  // src/events/onclone.js
21601
- function init22() {
21893
+ function init23() {
21602
21894
  if (Node.prototype.__hyperclayOnclone) {
21603
21895
  return;
21604
21896
  }
@@ -21651,7 +21943,7 @@ ${t}` : `"use strict"; return (${t});`;
21651
21943
  console.error("Error in onmutation execution:", error);
21652
21944
  }
21653
21945
  }
21654
- function init23() {
21946
+ function init24() {
21655
21947
  mutation_default.onAnyChange({ require: "observed" }, (changes2) => {
21656
21948
  const toFire = /* @__PURE__ */ new Set();
21657
21949
  for (const change of changes2) {
@@ -21671,7 +21963,7 @@ ${t}` : `"use strict"; return (${t});`;
21671
21963
  });
21672
21964
 
21673
21965
  // src/events/onpagemutation.js
21674
- function init24() {
21966
+ function init25() {
21675
21967
  const executeGlobalMutation = async (element) => {
21676
21968
  try {
21677
21969
  const code = element.getAttribute("onglobalmutation") || element.getAttribute("onpagemutation");
@@ -21711,7 +22003,7 @@ ${t}` : `"use strict"; return (${t});`;
21711
22003
  });
21712
22004
 
21713
22005
  // src/events/onrender.js
21714
- function init25() {
22006
+ function init26() {
21715
22007
  const executeRender = async (element) => {
21716
22008
  try {
21717
22009
  const code = element.getAttribute("onrender");
@@ -21744,15 +22036,15 @@ ${t}` : `"use strict"; return (${t});`;
21744
22036
  var events_exports = {};
21745
22037
  __export(events_exports, {
21746
22038
  default: () => events_default,
21747
- init: () => init26
22039
+ init: () => init27
21748
22040
  });
21749
- function init26() {
21750
- init20();
22041
+ function init27() {
21751
22042
  init21();
21752
22043
  init22();
21753
22044
  init23();
21754
22045
  init24();
21755
22046
  init25();
22047
+ init26();
21756
22048
  }
21757
22049
  var events_default;
21758
22050
  var init_events = __esm({
@@ -21763,8 +22055,8 @@ ${t}` : `"use strict"; return (${t});`;
21763
22055
  init_onmutation();
21764
22056
  init_onpagemutation();
21765
22057
  init_onrender();
21766
- init26();
21767
- events_default = init26;
22058
+ init27();
22059
+ events_default = init27;
21768
22060
  }
21769
22061
  });
21770
22062
 
@@ -21773,10 +22065,14 @@ ${t}` : `"use strict"; return (${t});`;
21773
22065
  always: [
21774
22066
  "lib/mutation.js",
21775
22067
  // first: everything hangs off the hub; cms/sync need it in view mode too
21776
- "core/edit-mode.js"
22068
+ "core/edit-mode.js",
21777
22069
  // both modes — matches today: 'edit-mode' is NOT in EDIT_MODE_ONLY
21778
22070
  // (hyperclay.js:251-273 lists 'edit-mode-helpers', not 'edit-mode');
21779
22071
  // toggleEditMode must exist in view mode, it's the way IN
22072
+ // always, deliberately: it speaks only when the host is too old to save to, which
22073
+ // is exactly the case where edit mode is off, so the editOnly wave would never
22074
+ // reach it. It draws nothing on any other page.
22075
+ "core/stale-host-notice.js"
21780
22076
  ],
21781
22077
  editOnly: [
21782
22078
  "core/snapshot.js",
@@ -21813,6 +22109,7 @@ ${t}` : `"use strict"; return (${t});`;
21813
22109
  "lib/region-policy.js": () => Promise.resolve().then(() => (init_region_policy(), region_policy_exports)),
21814
22110
  "lib/mutation.js": () => Promise.resolve().then(() => (init_mutation(), mutation_exports)),
21815
22111
  "core/edit-mode.js": () => Promise.resolve().then(() => (init_edit_mode(), edit_mode_exports)),
22112
+ "core/stale-host-notice.js": () => Promise.resolve().then(() => (init_stale_host_notice(), stale_host_notice_exports)),
21816
22113
  "core/snapshot.js": () => Promise.resolve().then(() => (init_snapshot(), snapshot_exports)),
21817
22114
  "core/save-core.js": () => Promise.resolve().then(() => (init_save_core(), save_core_exports)),
21818
22115
  "core/save.js": () => Promise.resolve().then(() => (init_save(), save_exports)),