@panphora/clayjs 0.2.0 → 0.4.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.
Files changed (43) hide show
  1. package/README.md +99 -7
  2. package/clay-internals.js +19 -0
  3. package/clay.js +18 -2
  4. package/package.json +19 -3
  5. package/sap.js +5 -5
  6. package/src/attrs/onaftersave.js +12 -1
  7. package/src/attrs/refetch-on-save.js +4 -4
  8. package/src/attrs/save-freeze.js +47 -19
  9. package/src/core/admin-contenteditable.js +2 -2
  10. package/src/core/admin-inputs.js +2 -2
  11. package/src/core/admin-onclick.js +2 -2
  12. package/src/core/admin-resources.js +23 -14
  13. package/src/core/autosave.js +11 -1
  14. package/src/core/edit-mode.js +2 -2
  15. package/src/core/host-attrs.js +46 -0
  16. package/src/core/is-edit-mode.js +14 -6
  17. package/src/core/persist.js +8 -1
  18. package/src/core/save-core.js +199 -290
  19. package/src/core/save.js +124 -84
  20. package/src/core/snapshot.js +99 -43
  21. package/src/core/unsaved-warning.js +8 -2
  22. package/src/dom/all.js +4 -29
  23. package/src/dom/form-data.js +1 -1
  24. package/src/dom/nearest.js +11 -9
  25. package/src/internals/index.js +64 -0
  26. package/src/lib/cache-bust.js +10 -0
  27. package/src/lib/cookie.js +50 -15
  28. package/src/lib/extension-noise.js +5 -1
  29. package/src/lib/mutation.js +65 -18
  30. package/src/lib/region-policy.js +17 -2
  31. package/src/lib/root-attrs.js +45 -0
  32. package/src/lib/throttle.js +25 -13
  33. package/src/loader.js +18 -24
  34. package/src/options/options.js +1 -3
  35. package/src/plugins/demo.js +7 -1
  36. package/src/sync/live-sync.js +121 -89
  37. package/src/ui/dialogs.js +10 -1
  38. package/src/ui/index.js +2 -11
  39. package/src/ui/toast.js +8 -2
  40. package/src/utils/debounce.js +24 -7
  41. package/src/vendor/hyper-undo.vendor.js +1 -1
  42. package/src/vendor/hypercms.vendor.js +29 -16
  43. package/src/vendor/richclay.vendor.js +29 -22
package/src/loader.js CHANGED
@@ -28,16 +28,27 @@ export async function boot(base, params, readyResolve) {
28
28
  loaded[path] = await import(base + "/src/" + path); // sequential: order is load-bearing
29
29
  }
30
30
 
31
- assembleCore(loaded, { isEditMode, isOwner }, regionPolicy); // window.clay + shim MUST exist
31
+ assembleCore(loaded, { isEditMode, isOwner }, regionPolicy); // window.clay MUST be assembled
32
32
  // before any plugin import
33
33
 
34
34
  for (const path of plan.plugins) {
35
- const mod = await import(base + "/src/" + path);
35
+ // A plugin that throws on evaluation degrades to absent. Unguarded, its
36
+ // rejection propagated out of boot() and left clay.ready pending forever, so
37
+ // every `await clay.ready` on the page hung: one broken optional module took
38
+ // the whole client with it. (The allSettled below covers the plugins' async
39
+ // `ready` exports, never their evaluation.)
40
+ let mod;
41
+ try {
42
+ mod = await import(base + "/src/" + path);
43
+ } catch (err) {
44
+ console.error(`clayjs: plugin "${path}" failed to load, continuing without it:`, err);
45
+ continue;
46
+ }
36
47
  loaded[path] = mod;
37
48
  attachPluginMember(path, mod); // immediately, not after the loop: hypercms's ?cms=true
38
49
  // auto-open runs as a microtask queued during ITS evaluation
39
- // (before boot resumes) and reads window.hyperclay.RichClay
40
- // and .undo, which earlier plugins must have mirrored by then
50
+ // (before boot resumes) and reads clay.RichClay and clay.undo,
51
+ // which earlier plugins must have attached by then
41
52
  }
42
53
 
43
54
  // Plugins with async setup (sortable's vendor fetch) export `ready`; hold
@@ -60,6 +71,7 @@ function assembleCore(loaded, { isEditMode, isOwner }, regionPolicy) {
60
71
  isEditMode,
61
72
  isOwner,
62
73
  Mutation: mutation.default,
74
+ region: regionPolicy.windowRegionShape,
63
75
  });
64
76
 
65
77
  const snapshot = loaded["core/snapshot.js"];
@@ -73,28 +85,12 @@ function assembleCore(loaded, { isEditMode, isOwner }, regionPolicy) {
73
85
  }
74
86
  if (snapshot) {
75
87
  clay.getHTML = snapshot.getPageContents;
76
- clay.beforeSave = snapshot.beforeSave;
88
+ clay.addDocumentTransform = snapshot.addDocumentTransform;
77
89
  clay.onSnapshot = snapshot.onSnapshot;
78
90
  }
79
91
  if (cacheBustMod) {
80
92
  clay.cacheBust = cacheBustMod.default;
81
93
  }
82
-
83
- // vendor-compat shim: richclay, hypercms, and hyper-undo read window.hyperclay;
84
- // remove when the vendors migrate to window.clay.
85
- window.hyperclay = window.hyperclay || {};
86
- Object.assign(window.hyperclay, {
87
- Mutation: clay.Mutation,
88
- isEditMode: clay.isEditMode,
89
- region: regionPolicy.windowRegionShape,
90
- });
91
- if (clay.beforeSave) {
92
- window.hyperclay.beforeSave = clay.beforeSave; // richclay's save-cleanup hook
93
- window.hyperclay.onPrepareForSave = clay.beforeSave; // hypercms uses this name
94
- }
95
- if (clay.save) {
96
- window.hyperclay.savePage = clay.save;
97
- }
98
94
  }
99
95
 
100
96
  function attachPluginMember(path, mod) {
@@ -102,16 +98,14 @@ function attachPluginMember(path, mod) {
102
98
 
103
99
  if (path === "plugins/undo.js") {
104
100
  clay.undo = mod.undo || mod.default;
105
- window.hyperclay.undo = clay.undo;
106
101
  } else if (path === "sync/live-sync.js") {
107
102
  clay.morph = mod.morph;
108
103
  } else if (path === "vendor/hypercms.vendor.js") {
109
104
  clay.cms = mod.cms || mod.default;
110
- window.hyperclay.hypercms = clay.cms;
111
105
  } else if (path === "plugins/demo.js") {
112
106
  clay.demo = mod.demo;
113
107
  } else if (path === "vendor/richclay.vendor.js") {
114
- window.hyperclay.RichClay = mod.RichClay || mod.default;
108
+ clay.RichClay = mod.RichClay || mod.default;
115
109
  }
116
110
  }
117
111
 
@@ -240,9 +240,7 @@ const optionVisibility = {
240
240
  }
241
241
  };
242
242
 
243
- // vendor-compat mirror: sap reads window.hyperclay.optionVisibility at runtime.
244
- window.hyperclay = window.hyperclay || {};
245
- window.hyperclay.optionVisibility = optionVisibility;
243
+ window.clay.optionVisibility = optionVisibility;
246
244
 
247
245
  export default optionVisibility;
248
246
 
@@ -20,12 +20,18 @@
20
20
  // returning visitors' old saved bodies morph over the new page.
21
21
 
22
22
  import { morph } from "../vendor/hyper-morph.vendor.js";
23
+ import { STRIP_FROM_SAVE } from "../lib/region-policy.js";
23
24
 
24
25
  const KEY = "clay:demo:" +
25
26
  (document.documentElement.getAttribute("demo-key") || window.location.pathname);
26
27
  const LATENCY_MS = 800;
28
+ // What the demo's fake server must not persist. The region half comes from
29
+ // region-policy's STRIP_FROM_SAVE rather than being re-listed here: this used to
30
+ // name "[save-remove]" alone and so missed the canonical clay="no-save" spelling,
31
+ // which meant the demo saved the very regions no-save promises it never will. The
32
+ // rest is editor chrome the real save lane strips by other means.
27
33
  const CHROME_SELECTOR = [
28
- "[save-remove]",
34
+ STRIP_FROM_SAVE,
29
35
  "[data-richclay-toolbar]",
30
36
  "[data-richclay-menu]",
31
37
  "[data-richclay-dialog]",
@@ -40,6 +40,8 @@ import Mutation from "../lib/mutation.js";
40
40
  import { isSnapshotRemoved } from "../lib/region-policy.js";
41
41
  import { isEditMode } from "../core/is-edit-mode.js";
42
42
  import { mergeTagRecognizers } from "./merge-tags.js";
43
+ import { serializeForSync } from '../core/snapshot.js';
44
+ import { isTabLocalRootAttr } from '../lib/root-attrs.js';
43
45
 
44
46
  class LiveSync {
45
47
  constructor() {
@@ -47,8 +49,19 @@ class LiveSync {
47
49
  this.currentFile = null;
48
50
  this.lastHtml = null;
49
51
  this.clientId = this.generateClientId();
52
+
53
+ // Per-stream resume id for the htmlclay replay server's wire contract.
54
+ // Regenerated on every start() (see below) so an explicit stop/start is a
55
+ // brand-new stream that never inherits the old baseline. Native
56
+ // EventSource reuses the same URL — and thus this id — on its own
57
+ // automatic reconnect. Deliberately NOT the sender clientId, which is the
58
+ // durable per-tab identity used for echo suppression.
59
+ this.resumeId = null;
60
+
50
61
  this.debounceMs = 150;
51
62
  this.debounceTimer = null;
63
+ this._sendInFlight = false;
64
+ this._queuedSend = null;
52
65
  this.isPaused = false;
53
66
  this.isDestroyed = false;
54
67
  this.debug = false;
@@ -87,7 +100,7 @@ class LiveSync {
87
100
  // against the live elements so that the next save can produce the same
88
101
  // identityMap, and afterNodeMorphed transfers IDs from incoming parsed
89
102
  // elements onto the live elements they morphed into.
90
- this.idCounter = this._loadIdCounter();
103
+ this.idCounter = 0;
91
104
  this.liveWeakMap = new WeakMap();
92
105
 
93
106
  // Callbacks
@@ -109,28 +122,30 @@ class LiveSync {
109
122
  }
110
123
 
111
124
  /**
112
- * Generate or retrieve a tab-specific client ID
113
- * Uses sessionStorage (unique per tab) not localStorage (shared across tabs)
125
+ * Mint the per-tab sender identity used for echo suppression. Lives on the
126
+ * instance, never in storage: sessionStorage is COPIED into a duplicated tab,
127
+ * so a persisted id made the duplicate treat the original's edits as its own
128
+ * echoes and the two tabs silently stopped syncing. Instance lifetime is the
129
+ * durability that matters, since EventSource auto-reconnect reuses this same
130
+ * instance.
114
131
  */
115
132
  generateClientId() {
116
- let id = null;
133
+ return Math.random().toString(36).slice(2, 11) + Date.now().toString(36);
134
+ }
117
135
 
136
+ /**
137
+ * Generate a fresh per-stream resume id (URL-safe, well under the 128-byte
138
+ * wire limit). Minted on every start(), never persisted: an explicit
139
+ * stop/start must not reuse the previous stream's baseline.
140
+ */
141
+ generateResumeId() {
118
142
  try {
119
- id = sessionStorage.getItem('livesync-client-id');
143
+ const bytes = new Uint8Array(16);
144
+ (globalThis.crypto || window.crypto).getRandomValues(bytes);
145
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
120
146
  } catch (e) {
121
- // sessionStorage might not be available
122
- }
123
-
124
- if (!id) {
125
- id = Math.random().toString(36).slice(2, 11) + Date.now().toString(36);
126
- try {
127
- sessionStorage.setItem('livesync-client-id', id);
128
- } catch (e) {
129
- // That's okay
130
- }
147
+ return Math.random().toString(36).slice(2) + Date.now().toString(36);
131
148
  }
132
-
133
- return id;
134
149
  }
135
150
 
136
151
  /**
@@ -153,9 +168,11 @@ class LiveSync {
153
168
  return;
154
169
  }
155
170
 
156
- // Reset state for new connection
171
+ // Reset state for new connection. Resetting lastSeenSeq is a new baseline,
172
+ // so mint a fresh resume id — this stream must not resume the previous one.
157
173
  this.lastHtml = null;
158
174
  this.lastSeenSeq = 0;
175
+ this.resumeId = this.generateResumeId();
159
176
 
160
177
  console.log(`[LiveSync] Starting for: ${this.currentFile} (lane=${this.lane})`);
161
178
  this.connect();
@@ -182,6 +199,7 @@ class LiveSync {
182
199
  }
183
200
 
184
201
  clearTimeout(this.debounceTimer);
202
+ this._queuedSend = null;
185
203
 
186
204
  // Cancel any pending frame and clear the queue. A morph already in
187
205
  // flight cannot be aborted; the isDestroyed check in _runPending guards
@@ -195,27 +213,8 @@ class LiveSync {
195
213
  this._pendingIdentityMap = null;
196
214
  }
197
215
 
198
- _loadIdCounter() {
199
- try {
200
- const raw = sessionStorage.getItem('livesync-id-counter');
201
- const n = parseInt(raw || '0', 10);
202
- return Number.isFinite(n) && n > 0 ? n : 0;
203
- } catch (e) {
204
- return 0;
205
- }
206
- }
207
-
208
- _persistIdCounter() {
209
- try {
210
- sessionStorage.setItem('livesync-id-counter', String(this.idCounter));
211
- } catch (e) {
212
- // Storage full / sandboxed — fall back to in-memory only.
213
- }
214
- }
215
-
216
216
  _mintId() {
217
217
  this.idCounter++;
218
- this._persistIdCounter();
219
218
  return `${this.clientId}:${this.idCounter}`;
220
219
  }
221
220
 
@@ -361,8 +360,10 @@ class LiveSync {
361
360
  if (this.isDestroyed) return;
362
361
 
363
362
  const pageUrl = encodeURIComponent(window.location.href);
364
- const url = `/_/live-sync/stream?page-url=${pageUrl}&lane=${this.lane}`;
365
- this.sse = new EventSource(url);
363
+ const path = `/_/live-sync/stream?page-url=${pageUrl}&lane=${this.lane}&resume-id=${this.resumeId}`;
364
+ // Resolved against the real origin: a <base href> in the authored document
365
+ // would otherwise point the sync stream at an origin the document chose.
366
+ this.sse = new EventSource(new URL(path, window.location.origin).href);
366
367
 
367
368
  this.sse.onopen = () => {
368
369
  console.log('[LiveSync] Connected');
@@ -372,6 +373,26 @@ class LiveSync {
372
373
  this.sse.onmessage = (event) => {
373
374
  const data = JSON.parse(event.data);
374
375
 
376
+ // Staleness check + watermark advance run FIRST — before the notification
377
+ // and error branches — against the high-water mark of seqs we've seen
378
+ // (own echoes count too, see below). Notification frames now carry a seq,
379
+ // so a conservative-cursor replay after reconnect must not duplicate a
380
+ // toast or let a later stale data frame apply an older snapshot. `seq` is
381
+ // optional for back-compat with older servers that don't stamp it.
382
+ const { seq } = data;
383
+ if (typeof seq === 'number' && seq <= this.lastSeenSeq) {
384
+ this._log(`Dropping stale message: seq=${seq}, lastSeen=${this.lastSeenSeq}`);
385
+ return;
386
+ }
387
+
388
+ // Advance the watermark before the notification/sender branches so that
389
+ // our own save echoes count toward "seen". Without this, a later
390
+ // buffered/replayed message with a smaller seq could rewind us past our
391
+ // local edit.
392
+ if (typeof seq === 'number') {
393
+ this.lastSeenSeq = seq;
394
+ }
395
+
375
396
  // Handle notifications (show toast, don't morph)
376
397
  if (data.type === "notification") {
377
398
  this.handleNotification(data);
@@ -385,22 +406,7 @@ class LiveSync {
385
406
  return;
386
407
  }
387
408
 
388
- const { html, sender, seq, identityMap } = data;
389
-
390
- // Staleness check runs FIRST — compared against the high-water mark of
391
- // seqs we've seen (own echoes count too, see below). `seq` is optional
392
- // for back-compat with older server builds that don't stamp it.
393
- if (typeof seq === 'number' && seq <= this.lastSeenSeq) {
394
- this._log(`Dropping stale message: seq=${seq}, lastSeen=${this.lastSeenSeq}`);
395
- return;
396
- }
397
-
398
- // Advance the watermark before the sender filter so that our own save
399
- // echoes count toward "seen". Without this, a later buffered/replayed
400
- // peer message with a smaller seq could rewind us past our local edit.
401
- if (typeof seq === 'number') {
402
- this.lastSeenSeq = seq;
403
- }
409
+ const { html, sender, identityMap } = data;
404
410
 
405
411
  // Ignore own changes — already reflected in the DOM, nothing to morph
406
412
  if (sender === this.clientId) {
@@ -451,7 +457,7 @@ class LiveSync {
451
457
  // after dispatchEvent returns, so reading outerHTML and walking children
452
458
  // must happen now.
453
459
  this._log('snapshot-ready received, preparing to send');
454
- const html = clone.outerHTML;
460
+ const html = serializeForSync(clone);
455
461
  const identityMap = this._buildIdentityMap(document.documentElement, clone);
456
462
  this.sendUpdate(html, identityMap);
457
463
  };
@@ -460,42 +466,67 @@ class LiveSync {
460
466
  }
461
467
 
462
468
  /**
463
- * Send full HTML to the server (debounced)
464
- * Only updates lastHtml after successful save
469
+ * Send full HTML to the server (debounced, single-flight).
470
+ *
471
+ * One POST on the wire at a time, with at most one newer payload queued; a
472
+ * fresher snapshot replaces the queued one, because peers only ever need the
473
+ * latest state. Two concurrent fetches could reach the server in either order,
474
+ * and last-write-wins then stored the OLDER snapshot for every peer.
465
475
  */
466
476
  sendUpdate(html, identityMap) {
467
477
  clearTimeout(this.debounceTimer);
468
478
 
469
479
  this.debounceTimer = setTimeout(() => {
470
- // Skip if unchanged
471
- if (html === this.lastHtml) {
472
- this._log('Skipping send - HTML unchanged');
473
- return;
474
- }
475
-
476
- this._log(`Sending update (HTML length: ${html.length}, lastHtml length: ${this.lastHtml?.length || 0})`);
477
-
478
- fetch('/_/live-sync/save', {
479
- method: 'POST',
480
- headers: { 'Content-Type': 'application/json', 'Page-URL': window.location.href },
481
- body: JSON.stringify({
482
- html: html,
483
- sender: this.clientId,
484
- identityMap: identityMap
485
- })
486
- }).then(response => {
487
- if (response.ok) {
488
- this.lastHtml = html;
489
- } else {
490
- console.warn('[LiveSync] Save returned status:', response.status);
491
- }
492
- }).catch(err => {
493
- console.error('[LiveSync] Save failed:', err);
494
- if (this.onError) this.onError(err);
495
- });
480
+ this._enqueueSend(html, identityMap);
496
481
  }, this.debounceMs);
497
482
  }
498
483
 
484
+ _enqueueSend(html, identityMap) {
485
+ if (this._sendInFlight) {
486
+ this._queuedSend = { html, identityMap };
487
+ return;
488
+ }
489
+ this._postUpdate(html, identityMap);
490
+ }
491
+
492
+ _postUpdate(html, identityMap) {
493
+ // Skip if unchanged
494
+ if (html === this.lastHtml) {
495
+ this._log('Skipping send - HTML unchanged');
496
+ return;
497
+ }
498
+
499
+ this._log(`Sending update (HTML length: ${html.length}, lastHtml length: ${this.lastHtml?.length || 0})`);
500
+
501
+ this._sendInFlight = true;
502
+
503
+ // Absolute against the real origin, so a <base href> in the page cannot
504
+ // redirect the whole document to an origin the author picked.
505
+ fetch(new URL('/_/live-sync/save', window.location.origin).href, {
506
+ method: 'POST',
507
+ headers: { 'Content-Type': 'application/json', 'Page-URL': window.location.href },
508
+ body: JSON.stringify({
509
+ html: html,
510
+ sender: this.clientId,
511
+ identityMap: identityMap
512
+ })
513
+ }).then(response => {
514
+ if (response.ok) {
515
+ this.lastHtml = html;
516
+ } else {
517
+ console.warn('[LiveSync] Save returned status:', response.status);
518
+ }
519
+ }).catch(err => {
520
+ console.error('[LiveSync] Save failed:', err);
521
+ if (this.onError) this.onError(err);
522
+ }).finally(() => {
523
+ this._sendInFlight = false;
524
+ const queued = this._queuedSend;
525
+ this._queuedSend = null;
526
+ if (queued) this._postUpdate(queued.html, queued.identityMap);
527
+ });
528
+ }
529
+
499
530
  /**
500
531
  * Apply an update received from the server. Morphs the entire document.
501
532
  *
@@ -629,6 +660,11 @@ class LiveSync {
629
660
  const id = parsedWeakMap.get(newEl);
630
661
  if (id) liveWeakMap.set(oldEl, id);
631
662
  };
663
+ // A peer may neither write these onto our root nor, by not carrying them,
664
+ // take ours away. Returning false is hyper-morph's veto for both directions
665
+ // (it calls this for updateType 'update' and 'remove' alike).
666
+ const beforeAttributeUpdated = (name, element) =>
667
+ isTabLocalRootAttr(name, element) ? false : undefined;
632
668
 
633
669
  try {
634
670
  // Morph entire document. We MUST await — HyperMorph.morph returns a
@@ -652,7 +688,7 @@ class LiveSync {
652
688
  mergeTags: mergeTagRecognizers
653
689
  },
654
690
  key,
655
- callbacks: { afterNodeMorphed }
691
+ callbacks: { afterNodeMorphed, beforeAttributeUpdated }
656
692
  });
657
693
 
658
694
  // Restore viewport. Done after morph so layout has settled.
@@ -685,10 +721,6 @@ class LiveSync {
685
721
  document.dispatchEvent(new CustomEvent('clay:sync-applied', {
686
722
  detail: { seq }
687
723
  }));
688
- // vendor-compat: hypercms's form panel listens for the legacy name.
689
- document.dispatchEvent(new CustomEvent('hyperclay:livesync-applied', {
690
- detail: { seq }
691
- }));
692
724
  } finally {
693
725
  this._log('applyUpdate - morph complete, resuming mutations');
694
726
  Mutation.resume();
package/src/ui/dialogs.js CHANGED
@@ -6,9 +6,18 @@ import copyToClipboard from "../utils/copy-to-clipboard.js";
6
6
  const 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>`;
7
7
  const 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>`;
8
8
 
9
+ // Only for the prompt's default value. promptText and extraContent are markup by
10
+ // design, and callers pass elements through them.
11
+ function escapeAttribute(value) {
12
+ return String(value)
13
+ .replaceAll("&", "&amp;")
14
+ .replaceAll('"', "&quot;")
15
+ .replaceAll("<", "&lt;");
16
+ }
17
+
9
18
  function createModal(promptText, yesCallback, extraContent = "", includeInput = false, defaultValue = "") {
10
19
  const inputHtml = includeInput
11
- ? `<div><input class="micromodal__input" type="text" value="${defaultValue}" required></div>`
20
+ ? `<div><input class="micromodal__input" type="text" value="${escapeAttribute(defaultValue)}" required></div>`
12
21
  : "";
13
22
 
14
23
  themodal.html = `<div>
package/src/ui/index.js CHANGED
@@ -18,19 +18,10 @@ clay.modal = themodal;
18
18
  if (typeof window.toast === "undefined") window.toast = toast;
19
19
  if (typeof window.toastPersistent === "undefined") window.toastPersistent = toastPersistent;
20
20
 
21
- // vendor-compat mirror: sap reads window.hyperclay.consent (the legacy name for
22
- // clay.confirm). Never CREATE window.hyperclay here: sap's mutation bridge treats
23
- // any truthy window.hyperclay as "a hub is coming" and waits forever for it. Core
24
- // creates the object (with Mutation) and fires clay:mutation-ready; attach then.
25
- function mirrorConsent() {
26
- if (window.hyperclay && !window.hyperclay.consent) window.hyperclay.consent = consent;
27
- }
28
- if (window.hyperclay) mirrorConsent();
29
- else document.addEventListener("clay:mutation-ready", mirrorConsent, { once: true });
30
-
31
21
  // Automatic save feedback. clay:save-saving stays deliberately silent (a toast for
32
22
  // a sub-second transient is noise). Keep your own toast lib? The events are public.
33
- document.addEventListener("clay:save-saved", () => toast("Saved", "success"));
23
+ document.addEventListener("clay:save-saved", (e) =>
24
+ toast(e.detail?.msg || "Saved", e.detail?.msgType === "warning" ? "warning" : "success"));
34
25
  document.addEventListener("clay:save-error", () => toastPersistent("Couldn't save", "error"));
35
26
  document.addEventListener("clay:save-offline", () => toastPersistent("Offline, not saved", "warning"));
36
27
 
package/src/ui/toast.js CHANGED
@@ -280,11 +280,14 @@ export function toastCore(message, messageType = "success", config = {}) {
280
280
  const template = templates.toast[messageType] || templates.toast.success;
281
281
  const toastHtml = template
282
282
  .replace('{icon}', icon)
283
- .replace('{message}', message);
283
+ .replace('{message}', '');
284
284
 
285
285
  const tempDiv = document.createElement('div');
286
286
  tempDiv.innerHTML = toastHtml.trim();
287
287
  const toastElement = tempDiv.firstElementChild;
288
+ // A message can carry a filename, and a filename is whatever whoever wrote the
289
+ // file chose. Assign it as text so markup in a name can never become markup here.
290
+ toastElement.querySelector('.toast-message, .message').textContent = message;
288
291
 
289
292
  // Add click handler
290
293
  toastElement.addEventListener('click', () => {
@@ -393,11 +396,14 @@ function toastPersistent(message, messageType = "warning") {
393
396
  const template = templates.toast[messageType] || templates.toast.warning;
394
397
  const toastHtml = template
395
398
  .replace('{icon}', icon)
396
- .replace('{message}', message);
399
+ .replace('{message}', '');
397
400
 
398
401
  const tempDiv = document.createElement('div');
399
402
  tempDiv.innerHTML = toastHtml.trim();
400
403
  const toastElement = tempDiv.firstElementChild;
404
+ // A message can carry a filename, and a filename is whatever whoever wrote the
405
+ // file chose. Assign it as text so markup in a name can never become markup here.
406
+ toastElement.querySelector('.toast-message, .message').textContent = message;
401
407
  toastElement.classList.add('toast-persistent');
402
408
 
403
409
  // Add close button as sibling (not inside message)
@@ -1,23 +1,40 @@
1
1
  // debounce.js
2
2
  function debounce(callback, delay) {
3
3
  let timeoutId;
4
- let pendingResolvers = [];
4
+ let pending = [];
5
5
 
6
6
  return function (...args) {
7
7
  const ctx = this;
8
8
  clearTimeout(timeoutId);
9
9
 
10
- return new Promise((resolve) => {
11
- pendingResolvers.push(resolve);
10
+ return new Promise((resolve, reject) => {
11
+ pending.push({ resolve, reject });
12
12
 
13
13
  timeoutId = setTimeout(() => {
14
- const resolvers = pendingResolvers;
15
- pendingResolvers = [];
16
- Promise.resolve(callback.apply(ctx, args))
17
- .then(value => { for (const r of resolvers) r(value); });
14
+ const waiting = pending;
15
+ pending = [];
16
+ settle(callback, ctx, args, waiting);
18
17
  }, delay);
19
18
  });
20
19
  };
21
20
  }
22
21
 
22
+ // Settle every caller piggybacking on this window, from both branches. The
23
+ // callback is invoked synchronously so timing is unchanged; only the failure
24
+ // path is new. Without it a throw escapes into the timer, where no caller is
25
+ // listening, and everyone waiting on that window waits forever.
26
+ function settle(callback, ctx, args, waiting) {
27
+ let result;
28
+ try {
29
+ result = callback.apply(ctx, args);
30
+ } catch (error) {
31
+ for (const p of waiting) p.reject(error);
32
+ return;
33
+ }
34
+ Promise.resolve(result).then(
35
+ value => { for (const p of waiting) p.resolve(value); },
36
+ error => { for (const p of waiting) p.reject(error); },
37
+ );
38
+ }
39
+
23
40
  export default debounce;
@@ -1,4 +1,4 @@
1
- var hyperundo=(()=>{var T=Object.defineProperty;var ee=Object.getOwnPropertyDescriptor;var te=Object.getOwnPropertyNames;var re=Object.prototype.hasOwnProperty;var ne=(e,t)=>{for(var n in t)T(e,n,{get:t[n],enumerable:!0})},oe=(e,t,n,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of te(t))!re.call(e,o)&&o!==n&&T(e,o,{get:()=>t[o],enumerable:!(r=ee(t,o))||r.enumerable});return e};var ae=e=>oe(T({},"__esModule",{value:!0}),e);var me={};ne(me,{default:()=>pe,undo:()=>C});function ie(e,t=null){let n=[];if(e.type==="attributes"){let r=e.target,o=e.attributeName,i=e.oldValue,u=r.getAttribute(o);return i==null&&u!=null?n.push({kind:"attr-add",target:r,name:o,newValue:u}):i!=null&&u==null?n.push({kind:"attr-remove",target:r,name:o,oldValue:i}):i!==u&&n.push({kind:"attr-set",target:r,name:o,oldValue:i,newValue:u}),n}if(e.type==="characterData")return n.push({kind:"text",target:e.target,oldValue:e.oldValue,newValue:e.target.data}),n;if(e.type==="childList"){let r=e.target,o=e.nextSibling,i=t?Array.from(e.addedNodes).filter(d=>!t(d)):Array.from(e.addedNodes),u=t?Array.from(e.removedNodes).filter(d=>!t(d)):Array.from(e.removedNodes);return i.length>0&&n.push({kind:"add",parent:r,nodes:i,before:o}),u.length>0&&n.push({kind:"remove",parent:r,nodes:u,before:o}),n}return n}function P(e,t=null){let n=[],r=new Map;for(let o of e){if(o.type==="attributes"){let i=o.target,u=o.attributeName,d=r.get(i);d||(d=new Map,r.set(i,d)),d.has(u)||(d.set(u,{idx:n.length,oldValue:o.oldValue}),n.push(null));continue}for(let i of ie(o,t))n.push(i)}for(let[o,i]of r)for(let[u,d]of i){let l=d.oldValue,m=o.getAttribute(u),p=null;l===m?p=null:l==null?p={kind:"attr-add",target:o,name:u,newValue:m}:m==null?p={kind:"attr-remove",target:o,name:u,oldValue:l}:p={kind:"attr-set",target:o,name:u,oldValue:l,newValue:m},n[d.idx]=p}return n.filter(o=>o!=null)}function L(e){switch(e.kind){case"attr-set":case"attr-add":e.target.setAttribute(e.name,e.newValue);return;case"attr-remove":e.target.removeAttribute(e.name);return;case"text":e.target.data=e.newValue;return;case"value":e.target[e.prop]=e.newValue;return;case"add":for(let t of e.nodes)e.before&&e.before.parentNode===e.parent?e.parent.insertBefore(t,e.before):e.parent.appendChild(t);return;case"remove":for(let t of e.nodes)t.parentNode===e.parent&&e.parent.removeChild(t);return}}function D(e){switch(e.kind){case"attr-set":e.target.setAttribute(e.name,e.oldValue);return;case"attr-add":e.target.removeAttribute(e.name);return;case"attr-remove":e.target.setAttribute(e.name,e.oldValue);return;case"text":e.target.data=e.oldValue;return;case"value":e.target[e.prop]=e.oldValue;return;case"add":for(let t of e.nodes)t.parentNode===e.parent&&e.parent.removeChild(t);return;case"remove":for(let t of e.nodes)e.before&&e.before.parentNode===e.parent?e.parent.insertBefore(t,e.before):e.parent.appendChild(t);return}}var se=['[src^="chrome-extension://"]','[href^="chrome-extension://"]','[src^="moz-extension://"]','[href^="moz-extension://"]','[src^="safari-web-extension://"]','[href^="safari-web-extension://"]','[id="1p-menu-live-region"]',"com-1password-button","com-1password-menu","com-1password-notification","grammarly-extension","grammarly-desktop-integration","lt-div","lt-card","lt-toolbar","lt-highlighter","[data-lastpass-root]","[data-lastpass-icon-root]","[data-dashlane-shadowhost]","[data-klarna-extension]",'[class*="klarna-extension"]',".skiptranslate","#goog-gt-tt",".goog-te-banner-frame",".goog-te-balloon-frame","style.darkreader","style[data-darkreader-mode]","style[data-darkreader-scheme]","#loom-companion-mv3","loom-container","[ext-id]"],ue=["data-grammarly","data-lt","data-1p","data-com-onepassword","data-lastpass","data-lp","data-kwimpala","data-bitwarden","data-bw","data-dashlane","data-np","data-protonpass","data-proton-pass","data-keeper","data-rf","data-roboform","data-honey","data-rakuten","data-capital-one","data-wikibuy","data-klarna","data-darkreader","data-evernote","data-pocket","data-pin"],M=se.join(","),U=new RegExp("^(?:"+ue.join("|")+")(?:-|$)");var de=["mutations-ignore","save-remove","save-ignore","save-freeze","no-undo","no-watch"];function le(){return typeof window<"u"&&window.hyperclay&&window.hyperclay.region&&typeof window.hyperclay.region.resolveRegionPolicy=="function"?window.hyperclay.region.resolveRegionPolicy:null}function ce(e){let t=le();if(t)try{return!t(e).undoable}catch{}let n=e&&e.nodeType!==1?e.parentElement:e;if(n&&n.closest&&n.closest(M))return!0;for(;n&&n.nodeType===1;){for(let r of de)if(n.hasAttribute&&n.hasAttribute(r))return!0;n=n.parentElement}return!1}function R(e,t,n){if(ce(e))return!0;if(n&&n.type==="attributes"){if(n.attributeName&&U.test(n.attributeName.toLowerCase()))return!0;if(t)try{if(t(n.attributeName,n.target))return!0}catch{}}return!1}function X(){let e=new Map;function t(o,i){let u=e.get(o);return u||(u=new Set,e.set(o,u)),u.add(i),()=>n(o,i)}function n(o,i){let u=e.get(o);u&&u.delete(i)}function r(o,i){let u=e.get(o);if(u)for(let d of Array.from(u))try{d(i)}catch(l){console.error("[hyper-undo] listener threw",l)}}return{on:t,off:n,emit:r}}var fe={maxHistory:100,idleWindowMs:500,idleLabel:"Edit",ignoreAttribute:null,debug:!1};function N(e){let t={...fe,...e};if(!t.scope)throw new Error("hyper-undo: scope is required");let n=X(),r=[],o=[],i=[],u=null,d=null,l=0,m=!1;function p(...s){t.debug&&console.log("[hyper-undo]",...s)}function H(){m||(m=!0,d=W(g),d.observe(t.scope,{childList:!0,attributes:!0,characterData:!0,subtree:!0,attributeOldValue:!0,characterDataOldValue:!0}),p("started"))}function W(s){let c=typeof document<"u"&&t.scope===document.body,f=typeof window<"u"&&window.hyperclay&&window.hyperclay.Mutation||null;return c&&f&&typeof f.createObserver=="function"?(p("sourcing records from window.hyperclay.Mutation (shared observer)"),f.createObserver(s)):new MutationObserver(s)}function E(){m&&(m=!1,d&&(d.disconnect(),d=null),y(),r.length=0,o.length=0,i=[],l=0,p("stopped"))}function V(){let s=t.scope;return!!(s&&s.nodeType===1&&s.isConnected===!1)}function g(s){if(l>0)return;let c=_(s);if(c.length>0){for(let f of c)i.push(f);I()}}function _(s){let c=s.filter(f=>!R(f.target,t.ignoreAttribute,f));return P(c,f=>R(f,t.ignoreAttribute))}function I(){y(),u=setTimeout(K,t.idleWindowMs)}function y(){u!=null&&(clearTimeout(u),u=null)}function K(){if(u=null,i.length===0)return;let s=i;i=[],v(t.idleLabel,s)}function v(s,c){for(r.push({label:s,timestamp:Date.now(),primitives:c}),o.length=0;r.length>t.maxHistory;)r.shift();p("commit",s,"primitives:",c.length),n.emit("commit")}function j(s,c){if(!m)throw new Error("hyper-undo: scope not started");g(d.takeRecords()),w();let f=c();if(f&&typeof f.then=="function")throw new Error("hyper-undo: commit() fn must be synchronous; returned a Promise");if(g(d.takeRecords()),i.length===0){n.emit("commit");return}y();let b=i;i=[],v(s,b)}function G(s){if(!m)throw new Error("hyper-undo: scope not started");if(!d)return;if(l>1){d.takeRecords();return}w();let c=_(d.takeRecords());c.length!==0&&(y(),v(s,c))}function Z(){d&&d.takeRecords()}function O(s){!m||l>0||!s||(i.push(s),I())}function $(s,{prop:c="value",oldValue:f,newValue:b}={}){!s||f===b||O({kind:"value",target:s,prop:c,oldValue:f,newValue:b})}function w(){l===0&&d&&g(d.takeRecords()),i.length!==0&&(y(),K())}function k(){l===0&&d&&g(d.takeRecords()),l++,p("paused",l)}function x(){l!==0&&(l--,l===0&&d&&d.takeRecords(),p("resumed",l))}function J(){if(V()){E();return}w();let s=r.pop();if(s){k();try{for(let c=s.primitives.length-1;c>=0;c--)try{D(s.primitives[c])}catch(f){p("undo primitive failed (continuing)",f)}}finally{x()}o.push(s),p("undo",s.label),n.emit("undo")}}function Q(){if(V()){E();return}w();let s=o.pop();if(s){k();try{for(let c of s.primitives)try{L(c)}catch(f){p("redo primitive failed (continuing)",f)}}finally{x()}r.push(s),p("redo",s.label),n.emit("redo")}}function Y(){d&&d.takeRecords(),r.length=0,o.length=0,i=[],y(),p("cleared"),n.emit("clear")}return{_config:t,start:H,stop:E,commit:j,commitCaptured:G,discardCaptured:Z,flush:w,record:O,recordValue:$,undo:J,redo:Q,clear:Y,pause:k,resume:x,on:n.on,off:n.off,get canUndo(){return r.length>0},get canRedo(){return o.length>0},get isPaused(){return l>0},get history(){return r.map(s=>({label:s.label,timestamp:s.timestamp}))},get scope(){return t.scope}}}function q(e,t){let n=r=>{let o=e.scope;if(o&&o.nodeType===1&&o.isConnected===!1){t&&t();return}let i=r.target,u=e._config.shadowKeydownIn;if(i&&i.closest&&Array.isArray(u))for(let m of u)try{if(i.closest(m))return}catch{}if(!(r.metaKey||r.ctrlKey))return;let l=(r.key||"").toLowerCase();if(l==="z"&&!r.shiftKey){r.preventDefault(),r.stopPropagation(),e.undo();return}if(l==="z"&&r.shiftKey){r.preventDefault(),r.stopPropagation(),e.redo();return}if(l==="y"&&!r.shiftKey){r.preventDefault(),r.stopPropagation(),e.redo();return}};return window.addEventListener("keydown",n,!0),()=>window.removeEventListener("keydown",n,!0)}var A=[".CodeMirror",".cm-editor",".monaco-editor",".ace_editor",".ql-editor",".tiptap",".ProseMirror"],a=null,S=null,z=null,h=null;function F(e,t){if(h){if(h.inst===e)return h.cleanup;throw new Error("hyper-undo: another scope already owns the global Cmd+Z binding (bindKeys: true). Stop it first, or use bindKeys: false for additional scopes.")}let n=q(e,t);return h={inst:e,cleanup:n},n}function B(e){h&&h.inst===e&&(h.cleanup(),h=null)}var C={defaults:{shadowKeydownIn:A},start(e={}){let t=e.scope||(typeof document<"u"?document.body:null);if(!t)throw new Error("hyper-undo: no scope (need document.body or explicit { scope })");if(a){if(S===t)return console.warn("[hyper-undo] start() called again on existing singleton; ignoring new options"),a;throw new Error("hyper-undo: start() called with a different scope while the singleton is already started. Use undo.create({ scope }) for additional scopes, or call undo.stop() first.")}let n={...e,scope:t,shadowKeydownIn:e.shadowKeydownIn||A};return a=N(n),a.start(),S=t,e.bindKeys!==!1&&(z=F(a,()=>C.stop())),a},stop(){a&&(B(a),z=null,a.stop(),a=null,S=null)},create(e={}){let t=e.scope||(typeof document<"u"?document.body:null);if(!t)throw new Error("hyper-undo: no scope (need document.body or explicit { scope })");let n={...e,scope:t,shadowKeydownIn:e.shadowKeydownIn||A},r=N(n),o=null,i={_config:r._config,commit:r.commit,commitCaptured:r.commitCaptured,discardCaptured:r.discardCaptured,flush:r.flush,record:r.record,recordValue:r.recordValue,undo:r.undo,redo:r.redo,clear:r.clear,pause:r.pause,resume:r.resume,on:r.on,off:r.off,get canUndo(){return r.canUndo},get canRedo(){return r.canRedo},get isPaused(){return r.isPaused},get history(){return r.history},get scope(){return r.scope},start(){e.bindKeys&&(o=F(r,()=>i.stop())),r.start()},stop(){B(r),o=null,r.stop()}};return i},commit(e,t){return a?a.commit(e,t):void 0},commitCaptured(e){return a?a.commitCaptured(e):void 0},discardCaptured(){return a?a.discardCaptured():void 0},record(e){return a?a.record(e):void 0},recordValue(e,t){return a?a.recordValue(e,t):void 0},undo(){return a?a.undo():void 0},redo(){return a?a.redo():void 0},clear(){return a?a.clear():void 0},pause(){return a?a.pause():void 0},resume(){return a?a.resume():void 0},flush(){return a?a.flush():void 0},on(e,t){return a?a.on(e,t):void 0},off(e,t){return a?a.off(e,t):void 0},get canUndo(){return a?a.canUndo:!1},get canRedo(){return a?a.canRedo:!1},get history(){return a?a.history:[]},get isPaused(){return a?a.isPaused:!1}};var pe=C;return ae(me);})();
1
+ var hyperundo=(()=>{var T=Object.defineProperty;var ee=Object.getOwnPropertyDescriptor;var te=Object.getOwnPropertyNames;var re=Object.prototype.hasOwnProperty;var ne=(e,t)=>{for(var n in t)T(e,n,{get:t[n],enumerable:!0})},oe=(e,t,n,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of te(t))!re.call(e,o)&&o!==n&&T(e,o,{get:()=>t[o],enumerable:!(r=ee(t,o))||r.enumerable});return e};var ae=e=>oe(T({},"__esModule",{value:!0}),e);var me={};ne(me,{default:()=>pe,undo:()=>C});function ie(e,t=null){let n=[];if(e.type==="attributes"){let r=e.target,o=e.attributeName,i=e.oldValue,u=r.getAttribute(o);return i==null&&u!=null?n.push({kind:"attr-add",target:r,name:o,newValue:u}):i!=null&&u==null?n.push({kind:"attr-remove",target:r,name:o,oldValue:i}):i!==u&&n.push({kind:"attr-set",target:r,name:o,oldValue:i,newValue:u}),n}if(e.type==="characterData")return n.push({kind:"text",target:e.target,oldValue:e.oldValue,newValue:e.target.data}),n;if(e.type==="childList"){let r=e.target,o=e.nextSibling,i=t?Array.from(e.addedNodes).filter(d=>!t(d)):Array.from(e.addedNodes),u=t?Array.from(e.removedNodes).filter(d=>!t(d)):Array.from(e.removedNodes);return i.length>0&&n.push({kind:"add",parent:r,nodes:i,before:o}),u.length>0&&n.push({kind:"remove",parent:r,nodes:u,before:o}),n}return n}function P(e,t=null){let n=[],r=new Map;for(let o of e){if(o.type==="attributes"){let i=o.target,u=o.attributeName,d=r.get(i);d||(d=new Map,r.set(i,d)),d.has(u)||(d.set(u,{idx:n.length,oldValue:o.oldValue}),n.push(null));continue}for(let i of ie(o,t))n.push(i)}for(let[o,i]of r)for(let[u,d]of i){let l=d.oldValue,m=o.getAttribute(u),p=null;l===m?p=null:l==null?p={kind:"attr-add",target:o,name:u,newValue:m}:m==null?p={kind:"attr-remove",target:o,name:u,oldValue:l}:p={kind:"attr-set",target:o,name:u,oldValue:l,newValue:m},n[d.idx]=p}return n.filter(o=>o!=null)}function L(e){switch(e.kind){case"attr-set":case"attr-add":e.target.setAttribute(e.name,e.newValue);return;case"attr-remove":e.target.removeAttribute(e.name);return;case"text":e.target.data=e.newValue;return;case"value":e.target[e.prop]=e.newValue;return;case"add":for(let t of e.nodes)e.before&&e.before.parentNode===e.parent?e.parent.insertBefore(t,e.before):e.parent.appendChild(t);return;case"remove":for(let t of e.nodes)t.parentNode===e.parent&&e.parent.removeChild(t);return}}function D(e){switch(e.kind){case"attr-set":e.target.setAttribute(e.name,e.oldValue);return;case"attr-add":e.target.removeAttribute(e.name);return;case"attr-remove":e.target.setAttribute(e.name,e.oldValue);return;case"text":e.target.data=e.oldValue;return;case"value":e.target[e.prop]=e.oldValue;return;case"add":for(let t of e.nodes)t.parentNode===e.parent&&e.parent.removeChild(t);return;case"remove":for(let t of e.nodes)e.before&&e.before.parentNode===e.parent?e.parent.insertBefore(t,e.before):e.parent.appendChild(t);return}}var se=['[src^="chrome-extension://"]','[href^="chrome-extension://"]','[src^="moz-extension://"]','[href^="moz-extension://"]','[src^="safari-web-extension://"]','[href^="safari-web-extension://"]','[id="1p-menu-live-region"]',"com-1password-button","com-1password-menu","com-1password-notification","grammarly-extension","grammarly-desktop-integration","lt-div","lt-card","lt-toolbar","lt-highlighter","[data-lastpass-root]","[data-lastpass-icon-root]","[data-dashlane-shadowhost]","[data-klarna-extension]",'[class*="klarna-extension"]',".skiptranslate","#goog-gt-tt",".goog-te-banner-frame",".goog-te-balloon-frame","style.darkreader","style[data-darkreader-mode]","style[data-darkreader-scheme]","#loom-companion-mv3","loom-container","[ext-id]"],ue=["data-grammarly","data-lt","data-1p","data-com-onepassword","data-lastpass","data-lp","data-kwimpala","data-bitwarden","data-bw","data-dashlane","data-np","data-protonpass","data-proton-pass","data-keeper","data-rf","data-roboform","data-honey","data-rakuten","data-capital-one","data-wikibuy","data-klarna","data-darkreader","data-evernote","data-pocket","data-pin"],M=se.join(","),U=new RegExp("^(?:"+ue.join("|")+")(?:-|$)");var de=["mutations-ignore","save-remove","save-ignore","save-freeze","no-undo","no-watch"];function le(){if(typeof window>"u")return null;let e=window.clay?.region||window.hyperclay?.region;return e&&typeof e.resolveRegionPolicy=="function"?e.resolveRegionPolicy:null}function ce(e){let t=le();if(t)try{return!t(e).undoable}catch{}let n=e&&e.nodeType!==1?e.parentElement:e;if(n&&n.closest&&n.closest(M))return!0;for(;n&&n.nodeType===1;){for(let r of de)if(n.hasAttribute&&n.hasAttribute(r))return!0;n=n.parentElement}return!1}function R(e,t,n){if(ce(e))return!0;if(n&&n.type==="attributes"){if(n.attributeName&&U.test(n.attributeName.toLowerCase()))return!0;if(t)try{if(t(n.attributeName,n.target))return!0}catch{}}return!1}function X(){let e=new Map;function t(o,i){let u=e.get(o);return u||(u=new Set,e.set(o,u)),u.add(i),()=>n(o,i)}function n(o,i){let u=e.get(o);u&&u.delete(i)}function r(o,i){let u=e.get(o);if(u)for(let d of Array.from(u))try{d(i)}catch(l){console.error("[hyper-undo] listener threw",l)}}return{on:t,off:n,emit:r}}var fe={maxHistory:100,idleWindowMs:500,idleLabel:"Edit",ignoreAttribute:null,debug:!1};function N(e){let t={...fe,...e};if(!t.scope)throw new Error("hyper-undo: scope is required");let n=X(),r=[],o=[],i=[],u=null,d=null,l=0,m=!1;function p(...s){t.debug&&console.log("[hyper-undo]",...s)}function H(){m||(m=!0,d=W(g),d.observe(t.scope,{childList:!0,attributes:!0,characterData:!0,subtree:!0,attributeOldValue:!0,characterDataOldValue:!0}),p("started"))}function W(s){let c=typeof document<"u"&&t.scope===document.body,f=typeof window<"u"&&(window.clay?.Mutation||window.hyperclay?.Mutation)||null;return c&&f&&typeof f.createObserver=="function"?(p("sourcing records from the platform Mutation hub (shared observer)"),f.createObserver(s)):new MutationObserver(s)}function E(){m&&(m=!1,d&&(d.disconnect(),d=null),y(),r.length=0,o.length=0,i=[],l=0,p("stopped"))}function V(){let s=t.scope;return!!(s&&s.nodeType===1&&s.isConnected===!1)}function g(s){if(l>0)return;let c=_(s);if(c.length>0){for(let f of c)i.push(f);I()}}function _(s){let c=s.filter(f=>!R(f.target,t.ignoreAttribute,f));return P(c,f=>R(f,t.ignoreAttribute))}function I(){y(),u=setTimeout(K,t.idleWindowMs)}function y(){u!=null&&(clearTimeout(u),u=null)}function K(){if(u=null,i.length===0)return;let s=i;i=[],v(t.idleLabel,s)}function v(s,c){for(r.push({label:s,timestamp:Date.now(),primitives:c}),o.length=0;r.length>t.maxHistory;)r.shift();p("commit",s,"primitives:",c.length),n.emit("commit")}function j(s,c){if(!m)throw new Error("hyper-undo: scope not started");g(d.takeRecords()),w();let f=c();if(f&&typeof f.then=="function")throw new Error("hyper-undo: commit() fn must be synchronous; returned a Promise");if(g(d.takeRecords()),i.length===0){n.emit("commit");return}y();let b=i;i=[],v(s,b)}function G(s){if(!m)throw new Error("hyper-undo: scope not started");if(!d)return;if(l>1){d.takeRecords();return}w();let c=_(d.takeRecords());c.length!==0&&(y(),v(s,c))}function Z(){d&&d.takeRecords()}function O(s){!m||l>0||!s||(i.push(s),I())}function $(s,{prop:c="value",oldValue:f,newValue:b}={}){!s||f===b||O({kind:"value",target:s,prop:c,oldValue:f,newValue:b})}function w(){l===0&&d&&g(d.takeRecords()),i.length!==0&&(y(),K())}function k(){l===0&&d&&g(d.takeRecords()),l++,p("paused",l)}function x(){l!==0&&(l--,l===0&&d&&d.takeRecords(),p("resumed",l))}function J(){if(V()){E();return}w();let s=r.pop();if(s){k();try{for(let c=s.primitives.length-1;c>=0;c--)try{D(s.primitives[c])}catch(f){p("undo primitive failed (continuing)",f)}}finally{x()}o.push(s),p("undo",s.label),n.emit("undo")}}function Q(){if(V()){E();return}w();let s=o.pop();if(s){k();try{for(let c of s.primitives)try{L(c)}catch(f){p("redo primitive failed (continuing)",f)}}finally{x()}r.push(s),p("redo",s.label),n.emit("redo")}}function Y(){d&&d.takeRecords(),r.length=0,o.length=0,i=[],y(),p("cleared"),n.emit("clear")}return{_config:t,start:H,stop:E,commit:j,commitCaptured:G,discardCaptured:Z,flush:w,record:O,recordValue:$,undo:J,redo:Q,clear:Y,pause:k,resume:x,on:n.on,off:n.off,get canUndo(){return r.length>0},get canRedo(){return o.length>0},get isPaused(){return l>0},get history(){return r.map(s=>({label:s.label,timestamp:s.timestamp}))},get scope(){return t.scope}}}function q(e,t){let n=r=>{let o=e.scope;if(o&&o.nodeType===1&&o.isConnected===!1){t&&t();return}let i=r.target,u=e._config.shadowKeydownIn;if(i&&i.closest&&Array.isArray(u))for(let m of u)try{if(i.closest(m))return}catch{}if(!(r.metaKey||r.ctrlKey))return;let l=(r.key||"").toLowerCase();if(l==="z"&&!r.shiftKey){r.preventDefault(),r.stopPropagation(),e.undo();return}if(l==="z"&&r.shiftKey){r.preventDefault(),r.stopPropagation(),e.redo();return}if(l==="y"&&!r.shiftKey){r.preventDefault(),r.stopPropagation(),e.redo();return}};return window.addEventListener("keydown",n,!0),()=>window.removeEventListener("keydown",n,!0)}var A=[".CodeMirror",".cm-editor",".monaco-editor",".ace_editor",".ql-editor",".tiptap",".ProseMirror"],a=null,S=null,z=null,h=null;function F(e,t){if(h){if(h.inst===e)return h.cleanup;throw new Error("hyper-undo: another scope already owns the global Cmd+Z binding (bindKeys: true). Stop it first, or use bindKeys: false for additional scopes.")}let n=q(e,t);return h={inst:e,cleanup:n},n}function B(e){h&&h.inst===e&&(h.cleanup(),h=null)}var C={defaults:{shadowKeydownIn:A},start(e={}){let t=e.scope||(typeof document<"u"?document.body:null);if(!t)throw new Error("hyper-undo: no scope (need document.body or explicit { scope })");if(a){if(S===t)return console.warn("[hyper-undo] start() called again on existing singleton; ignoring new options"),a;throw new Error("hyper-undo: start() called with a different scope while the singleton is already started. Use undo.create({ scope }) for additional scopes, or call undo.stop() first.")}let n={...e,scope:t,shadowKeydownIn:e.shadowKeydownIn||A};return a=N(n),a.start(),S=t,e.bindKeys!==!1&&(z=F(a,()=>C.stop())),a},stop(){a&&(B(a),z=null,a.stop(),a=null,S=null)},create(e={}){let t=e.scope||(typeof document<"u"?document.body:null);if(!t)throw new Error("hyper-undo: no scope (need document.body or explicit { scope })");let n={...e,scope:t,shadowKeydownIn:e.shadowKeydownIn||A},r=N(n),o=null,i={_config:r._config,commit:r.commit,commitCaptured:r.commitCaptured,discardCaptured:r.discardCaptured,flush:r.flush,record:r.record,recordValue:r.recordValue,undo:r.undo,redo:r.redo,clear:r.clear,pause:r.pause,resume:r.resume,on:r.on,off:r.off,get canUndo(){return r.canUndo},get canRedo(){return r.canRedo},get isPaused(){return r.isPaused},get history(){return r.history},get scope(){return r.scope},start(){e.bindKeys&&(o=F(r,()=>i.stop())),r.start()},stop(){B(r),o=null,r.stop()}};return i},commit(e,t){return a?a.commit(e,t):void 0},commitCaptured(e){return a?a.commitCaptured(e):void 0},discardCaptured(){return a?a.discardCaptured():void 0},record(e){return a?a.record(e):void 0},recordValue(e,t){return a?a.recordValue(e,t):void 0},undo(){return a?a.undo():void 0},redo(){return a?a.redo():void 0},clear(){return a?a.clear():void 0},pause(){return a?a.pause():void 0},resume(){return a?a.resume():void 0},flush(){return a?a.flush():void 0},on(e,t){return a?a.on(e,t):void 0},off(e,t){return a?a.off(e,t):void 0},get canUndo(){return a?a.canUndo:!1},get canRedo(){return a?a.canRedo:!1},get history(){return a?a.history:[]},get isPaused(){return a?a.isPaused:!1}};var pe=C;return ae(me);})();
2
2
 
3
3
  // Auto-export to window unless suppressed by loader.
4
4
  if (!window.__hyperclayNoAutoExport) {