@panphora/clayjs 0.6.1 → 0.7.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.
package/LICENSE CHANGED
@@ -1,21 +1,16 @@
1
- MIT License
1
+ MIT No Attribution
2
2
 
3
3
  Copyright (c) 2026 David Miranda
4
4
 
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this
6
+ software and associated documentation files (the "Software"), to deal in the Software
7
+ without restriction, including without limitation the rights to use, copy, modify,
8
+ merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
9
+ permit persons to whom the Software is furnished to do so.
11
10
 
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
11
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
12
+ INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
13
+ PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
14
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
15
+ CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
16
+ OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # clayjs
1
+ # clayjs™
2
2
 
3
3
  **Site, tutorial, and docs: [clayjs.com](https://clayjs.com)** · hosted platform: [hyperclay.com](https://hyperclay.com)
4
4
 
@@ -19,7 +19,7 @@ query params on the script URL:
19
19
  <script src="/clay.js?plugins=sync,cms&exclude=indicator"></script>
20
20
  ```
21
21
 
22
- - `?plugins=` — add optional plugins: `sync`, `cms`, `undo`, `sortable`, `indicator`, `quickcrop`, `wire`.
22
+ - `?plugins=` — add optional plugins: `sync`, `cms`, `undo`, `sortable`, `indicator`, `quickcrop`, `upload`, `wire`, `demo`.
23
23
  Only `richclay` loads by default, and only in edit mode. `cms` brings `quickcrop` with it, because
24
24
  the CMS uses it for `data-hcms-crop` image fields.
25
25
  - `?exclude=` — drop a plugin that would otherwise load (a default, or one another plugin pulled in).
@@ -35,10 +35,6 @@ never reach the file on disk.
35
35
  `/_/save/{token}` and sends no cookies, because the token is the credential.
36
36
  A token also implies edit mode, which is the only such signal a sandboxed
37
37
  document can see. `htmlclaytoken` is the older spelling of the same thing.
38
- - `clay-save-transport="desktop-json-v1"` — send the save as
39
- `{content, snapshotHtml, userDriven}` JSON instead of plain text. Only declare
40
- it on a host whose save lane reads that envelope.
41
-
42
38
  Edit mode is decided in this order: the `?editmode` param, then
43
39
  `window.clayEditMode`, then a save token, then the platform's owner cookie.
44
40
 
@@ -111,8 +107,9 @@ lifecycle.
111
107
  satellite with no core loaded.
112
108
  - `region.addRegionToken(el, token)`, `region.resolveRegionPolicy(node)`, `region.isInert(node)`,
113
109
  `region.isSnapshotRemoved(el)`, `region.PERSIST`, `region.REGION_ATTRS`, and
114
- `region.selectors.stripFromSave` / `.stripFromComparison` / `.snapshotRemove` / `.freeze` — write your
115
- own attribute without hardcoding our selectors.
110
+ `region.selectors.stripFromSave` / `.stripFromComparison` / `.stripFromDirtyCheck` /
111
+ `.noTriggerAutosave` / `.noDirty` / `.snapshotRemove` / `.freeze` — write your own attribute
112
+ without hardcoding our selectors.
116
113
  - `save.saveHtml(html, cb, opts)`, `save.replacePageWith(url, cb)`, `save.isSaveInProgress()` — the save
117
114
  lane under `clay.save`. **`saveHtml` writes the bytes you hand it straight to the file**, bypassing the
118
115
  snapshot pipeline entirely; check `isSaveInProgress()` first.
@@ -126,8 +123,15 @@ attribute:
126
123
  <div clay="no-save no-snapshot freeze">…</div>
127
124
  ```
128
125
 
129
- Tokens: `no-save`, `no-snapshot`, `no-trigger-autosave`, `no-watch`, `no-undo`, `freeze`.
130
- Add `autosave` to `<html>` to save automatically on change.
126
+ Tokens: `no-save`, `no-snapshot`, `no-trigger-autosave`, `no-dirty`, `no-watch`, `no-undo`,
127
+ `freeze`. Add `autosave` to `<html>` to save automatically on change.
128
+
129
+ `no-trigger-autosave` and `no-dirty` are the pair worth getting right. Both are saved in full and
130
+ neither starts an autosave. The difference is whether their content is *work*: a
131
+ `no-trigger-autosave` region holds real edits waiting for a manual save, so it warns you on close
132
+ and live sync will not overwrite it, while a `no-dirty` region renders itself from something else,
133
+ so it never warns and an incoming sync frame may replace it. Use `no-dirty` for filter bars,
134
+ projections and drag previews; use `no-trigger-autosave` for a heavy editor you save by hand.
131
135
 
132
136
  ## Develop
133
137
 
@@ -151,7 +155,7 @@ Never edit `public/` by hand, and never copy a file into it. Its contents are
151
155
  **derived**:
152
156
 
153
157
  ```
154
- public/ = package.json "files" (minus NOTICE, which nothing requests)
158
+ public/ = package.json "files" (minus THIRD-PARTY-NOTICES.md, which nothing requests)
155
159
  + website/* (flattened to the root, so /docs.html works)
156
160
  ```
157
161
 
@@ -161,3 +165,9 @@ reach npm and miss the site. Before this script existed the mirror was hand-copi
161
165
  and it drifted exactly that way: `src/core/host-attrs.js` never made it across, and
162
166
  since `loader.js` imports `is-edit-mode.js`, which imports it, the deployed
163
167
  `clay.js` could not boot at all.
168
+
169
+ ## License
170
+
171
+ Our code is MIT-0 (MIT No Attribution): use it, remix it, ship it, no attribution
172
+ needed. Vendored third-party files keep their original permissive licenses; see
173
+ [THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md).
@@ -0,0 +1,40 @@
1
+ # Third-party notices
2
+
3
+ Our own code in this repository is licensed under MIT-0 (see LICENSE): use it,
4
+ remix it, ship it, no attribution needed. The components below are third-party,
5
+ or carry third-party code, and keep their original licenses.
6
+
7
+ ## SortableJS 1.15.6 (MIT)
8
+
9
+ - File: src/vendor/Sortable.vendor.js
10
+ - Copyright (c) SortableJS contributors
11
+ - https://github.com/SortableJS/Sortable
12
+
13
+ ## Squire 2.4.8 (MIT)
14
+
15
+ - Carried inside src/vendor/richclay.vendor.js and website/vendor/richclay.min.js
16
+ (richclay bundles Squire)
17
+ - Copyright (c) 2011-2023 by Neil Jenkins
18
+ - https://github.com/fastmail/Squire
19
+
20
+ ## DOMPurify 3.4.11 (Apache-2.0 OR MPL-2.0)
21
+
22
+ - Carried inside src/vendor/richclay.vendor.js and website/vendor/richclay.min.js
23
+ (richclay bundles DOMPurify)
24
+ - (c) Cure53 and other contributors
25
+ - https://github.com/cure53/DOMPurify
26
+
27
+ ## MicroModal (MIT)
28
+
29
+ - The modal component in src/ui/modal.js derives its structure and class naming
30
+ from MicroModal.
31
+ - Copyright (c) 2017 Indrashish Ghosh
32
+ - https://github.com/ghosh/Micromodal
33
+
34
+ ## First-party vendored modules
35
+
36
+ The remaining files in src/vendor/ (hypercms, hyper-morph, hyper-undo, quickcrop,
37
+ control-serialize) and the generated sap.js and clay-data.js are our own sibling
38
+ libraries. clayjs also derives from our own hyperclayjs library (MIT); as the
39
+ copyright holder of those portions we license them here under MIT-0 with the rest
40
+ of our code.
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@panphora/clayjs",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "description": "clayjs: malleable HTML files. Save lifecycle for self-saving HTML.",
6
- "license": "MIT",
6
+ "license": "MIT-0",
7
7
  "homepage": "https://clayjs.com",
8
8
  "repository": {
9
9
  "type": "git",
@@ -21,7 +21,7 @@
21
21
  "sap.js",
22
22
  "clay-data.js",
23
23
  "_headers",
24
- "NOTICE",
24
+ "THIRD-PARTY-NOTICES.md",
25
25
  "src/"
26
26
  ],
27
27
  "publishConfig": {
@@ -5,16 +5,30 @@
5
5
  * Only fires on 'clay:save-saved' events (not on error/offline).
6
6
  *
7
7
  * Usage:
8
- * <span clay="no-trigger-autosave" onaftersave="this.innerText = event.detail.msg"></span>
9
- * <link href="styles.css" onaftersave="cacheBust(this)">
8
+ * <span clay="no-save" onaftersave="this.innerText = event.detail.msg"></span>
9
+ * <link href="styles.css" onaftersave="clay.cacheBust(this)">
10
10
  *
11
- * MARK WHAT YOU MUTATE `no-trigger-autosave`.
12
- * These handlers run after the save baseline has been taken, so anything they write
13
- * to the live DOM reads as a change the user made and leaves the page permanently
14
- * dirty: autosave loops, and the close-tab warning never clears. The token strips
15
- * the element from every comparison capture, which is what stops that.
16
- * `cacheBust()` marks its own target, so the second example needs nothing. A handler
17
- * that mutates some OTHER element has to mark that element itself.
11
+ * MARK WHAT YOU MUTATE `no-save` (or `freeze`).
12
+ * These handlers run after the save baseline has been taken, so anything they
13
+ * write to the live DOM reads as a change the person made, and the page stays
14
+ * dirty: the close-tab warning never clears, and on an autosave page it loops.
15
+ * `no-save` keeps the element out of the file entirely, which is what you want
16
+ * for a status chip or any other runtime-only chrome. `freeze` is the choice
17
+ * when the element must exist in the saved file, but as authored rather than as
18
+ * the handler left it.
19
+ *
20
+ * CHANGED IN 0.7: `no-trigger-autosave` is no longer enough on its own.
21
+ * It stops the handler's write from STARTING a save, and it still does that. It
22
+ * no longer hides the write from an explicit save or from the close warning,
23
+ * because those two now deliberately see batching regions — an edit you make
24
+ * inside one is real work that nothing else is going to write. A handler whose
25
+ * output is marked only `no-trigger-autosave` will leave the page reporting
26
+ * unsaved changes after every save. Change those markers to `no-save`, or to
27
+ * `freeze` if the element belongs in the file.
28
+ *
29
+ * `clay.cacheBust()` and `[refetch-on-save]` need no marker at all: they
30
+ * remember the authored URL and restore it on every snapshot (authored-url.js).
31
+ * A handler that mutates some OTHER element has to mark that element itself.
18
32
  *
19
33
  * (The alternative — re-reading the whole live DOM after handlers run — is what
20
34
  * clayjs used to do, and it silently discarded anything typed during a save.)
@@ -1,28 +1,48 @@
1
- import { addRegionToken } from '../lib/region-policy.js';
1
+ import Mutation from '../lib/mutation.js';
2
+ import { urlAttrFor, writeRuntimeUrl, markSuperseded, isSuperseded } from '../lib/authored-url.js';
3
+
4
+ /** Remove an element without letting the removal read as a user edit. */
5
+ function removeQuietly(el) {
6
+ if (!el.parentNode) return;
7
+ Mutation.pause();
8
+ try {
9
+ el.remove();
10
+ } finally {
11
+ Mutation.resume();
12
+ }
13
+ }
2
14
 
3
15
  function swapElement(el) {
4
- const attr = el.hasAttribute('href') ? 'href' : el.hasAttribute('src') ? 'src' : null;
16
+ const attr = urlAttrFor(el);
5
17
  if (!attr) return;
6
18
 
7
- const oldValue = el.getAttribute(attr);
8
- const url = new URL(oldValue, location.href);
19
+ // A second save landing inside the two-second overlap would otherwise swap the
20
+ // element that is already on its way out, and clone the clone.
21
+ if (isSuperseded(el)) return;
22
+
23
+ const url = new URL(el.getAttribute(attr), location.href);
9
24
  url.searchParams.set('v', Date.now());
10
25
  const isSameOrigin = url.origin === location.origin;
11
-
12
- const newEl = document.createElement(el.tagName);
13
- for (const { name, value } of el.attributes) {
14
- newEl.setAttribute(name, value);
26
+ const runtimeValue = isSameOrigin ? url.pathname + url.search : url.href;
27
+
28
+ Mutation.pause();
29
+ try {
30
+ const newEl = document.createElement(el.tagName);
31
+ for (const { name, value } of el.attributes) {
32
+ newEl.setAttribute(name, value);
33
+ }
34
+ // Record-if-absent carries over any authored URL the copied attributes
35
+ // already held, so cacheBust and refetch on one element keep the original.
36
+ writeRuntimeUrl(newEl, attr, runtimeValue);
37
+
38
+ markSuperseded(el);
39
+ el.insertAdjacentElement('afterend', newEl);
40
+
41
+ newEl.onload = () => removeQuietly(el);
42
+ setTimeout(() => removeQuietly(el), 2000);
43
+ } finally {
44
+ Mutation.resume();
15
45
  }
16
- newEl.setAttribute(attr, isSameOrigin ? url.pathname + url.search : url.href);
17
- addRegionToken(newEl, "no-trigger-autosave");
18
- addRegionToken(newEl, "no-undo");
19
-
20
- el.insertAdjacentElement('afterend', newEl);
21
-
22
- newEl.onload = () => el.remove();
23
- setTimeout(() => {
24
- if (el.parentNode) el.remove();
25
- }, 2000);
26
46
  }
27
47
 
28
48
  function init() {
@@ -13,7 +13,8 @@
13
13
  import Mutation from "../lib/mutation.js";
14
14
  import { isEditMode } from "./is-edit-mode.js";
15
15
  import { savePageThrottled } from "./save.js";
16
- import { initUserGesture, markUserDriven } from "../lib/user-gesture.js";
16
+ import { markUserDriven } from "../lib/user-gesture.js";
17
+ import { resolveRegionPolicy, skipForPolicy } from "../lib/region-policy.js";
17
18
 
18
19
  /**
19
20
  * Initialize auto-save on DOM changes
@@ -48,8 +49,13 @@ function initSaveOnPersistInput() {
48
49
  document.addEventListener('input', (e) => {
49
50
  if (!e.target.closest('[persist]')) return;
50
51
  // A trusted input on a [persist] field is itself a user-driven change the
51
- // Mutation hub can't see (form values aren't DOM mutations). Attribute it.
52
- if (e.isTrusted) markUserDriven();
52
+ // Mutation hub can't see (form values aren't DOM mutations). Attribute it,
53
+ // but only when the field is inside the dirty domain: a [persist] control in
54
+ // a no-save or freeze region contributes nothing to the saved bytes, so
55
+ // typing in one must not arm provenance for a later background write.
56
+ if (e.isTrusted && !skipForPolicy(resolveRegionPolicy(e.target), 'dirty')) {
57
+ markUserDriven();
58
+ }
53
59
  clearTimeout(inputSaveTimer);
54
60
  inputSaveTimer = setTimeout(savePageThrottled, 1500);
55
61
  }, true);
@@ -58,7 +64,8 @@ function initSaveOnPersistInput() {
58
64
  function init() {
59
65
  if (!document.documentElement.hasAttribute("autosave")) return;
60
66
  if (!isEditMode) return;
61
- initUserGesture();
67
+ // initUserGesture moved to save.js's init: gesture provenance belongs to every
68
+ // editable page, not only the ones with <html autosave>.
62
69
  initSavePageOnChange();
63
70
  initSaveOnPersistInput();
64
71
  }
@@ -7,13 +7,7 @@
7
7
  * cannot drift between the edit-mode ladder and the save lane.
8
8
  */
9
9
 
10
- import { HOST_TOKEN_ATTRS, SAVE_TRANSPORT_ATTR } from "../lib/root-attrs.js";
11
-
12
- // A host that wants the desktop JSON envelope on its save lane declares it on
13
- // the root. This replaced a `location.hostname === 'localhost'` sniff, which sent
14
- // the envelope to every host that happened to be local, including ones whose
15
- // save lane takes text and answers 415.
16
- export const DESKTOP_JSON = "desktop-json-v1";
10
+ import { HOST_TOKEN_ATTRS } from "../lib/root-attrs.js";
17
11
 
18
12
  /**
19
13
  * The per-document save token this response carries, or null.
@@ -35,12 +29,3 @@ export function saveToken() {
35
29
  export function hasSaveToken() {
36
30
  return saveToken() !== null;
37
31
  }
38
-
39
- /**
40
- * The save transport the served document declares, or null.
41
- * @returns {?string}
42
- */
43
- export function saveTransport() {
44
- if (typeof document === "undefined") return null;
45
- return document.documentElement.getAttribute(SAVE_TRANSPORT_ATTR);
46
- }
@@ -8,8 +8,8 @@
8
8
  */
9
9
 
10
10
  import { isEditMode } from "./is-edit-mode.js";
11
- import { consumeUserDriven, markUserDriven } from "../lib/user-gesture.js";
12
- import { saveToken, saveTransport, DESKTOP_JSON } from "./host-attrs.js";
11
+ import { consumeUserDriven, consumeExplicitSave, markUserDriven } from "../lib/user-gesture.js";
12
+ import { saveToken } from "./host-attrs.js";
13
13
  import {
14
14
  getPageContents,
15
15
  onSnapshot,
@@ -103,13 +103,19 @@ function skippedResult(msg) {
103
103
  * the save — and the per-document token in its path — to an origin the document
104
104
  * itself chose.
105
105
  *
106
+ * The body is the document, as text, always. Spec §3: this route has exactly one
107
+ * body shape. Everything else about the save travels in a header, which is how
108
+ * §6's `conditional` capability added `If-Match` without touching the body, and
109
+ * how anything later will be added too. There used to be a JSON envelope here
110
+ * carrying the document alongside an unstripped snapshot; the snapshot's home is
111
+ * the §10 relay, which the live-sync plugin already posts it to.
112
+ *
106
113
  * @param {string} content - HTML to save
107
- * @param {?string} snapshotHtml - unstripped snapshot, for the desktop envelope
108
114
  * @param {boolean} userDriven - Whether a human gesture is behind this save
109
115
  * @param {AbortSignal} signal
110
116
  * @returns {{url: string, options: Object}}
111
117
  */
112
- function buildSaveRequest(content, snapshotHtml, userDriven, signal) {
118
+ function buildSaveRequest(content, userDriven, signal) {
113
119
  const token = saveToken();
114
120
  const path = token ? `${SAVE_PATH}/${token}` : SAVE_PATH;
115
121
  const options = {
@@ -120,23 +126,14 @@ function buildSaveRequest(content, snapshotHtml, userDriven, signal) {
120
126
  'Document-URL': window.location.href,
121
127
  // The older spelling of the same header, which htmlclay reads today.
122
128
  'Page-URL': window.location.href,
123
- 'X-Hyperclay-User-Driven': userDriven ? '1' : '0'
124
- }
129
+ // Spec §9's name for the provenance bit. `X-Hyperclay-User-Driven` was the
130
+ // pre-spec spelling; every host reads Save-Trigger first and falls back to
131
+ // it, so stored documents running an older client keep working.
132
+ 'Save-Trigger': userDriven ? 'user' : 'auto'
133
+ },
134
+ body: content
125
135
  };
126
136
 
127
- // The desktop JSON envelope carries the unstripped snapshot alongside the
128
- // document, so a sync engine can replay what the browser actually had. It goes
129
- // out only when the served document DECLARES that transport, and it is passed in
130
- // from the capture that produced `content` — it used to be left on a window
131
- // global that was cleared only on success, so two captures without an intervening
132
- // successful save would pair a stale snapshot with fresh content.
133
- if (saveTransport() === DESKTOP_JSON && snapshotHtml) {
134
- options.headers['Content-Type'] = 'application/json';
135
- options.body = JSON.stringify({ content, snapshotHtml, userDriven });
136
- } else {
137
- options.body = content;
138
- }
139
-
140
137
  return { url: new URL(path, window.location.origin).href, options };
141
138
  }
142
139
 
@@ -153,18 +150,22 @@ function buildSaveRequest(content, snapshotHtml, userDriven, signal) {
153
150
  * instead of the status.
154
151
  *
155
152
  * @param {string} content - HTML to save
156
- * @param {?string} snapshotHtml
157
153
  * @returns {Promise<Object>} The server's response body
158
154
  */
159
- function sendSave(content, snapshotHtml) {
155
+ function sendSave(content) {
160
156
  const controller = new AbortController();
161
157
  const timeoutId = setTimeout(() => controller.abort(), SAVE_TIMEOUT_MS);
162
158
 
163
159
  // Read-and-reset the data-guard provenance bit at the ACTUAL send (past the
164
160
  // early returns in the callers), so it's never consumed on a save that never
165
161
  // ships.
166
- const userDriven = consumeUserDriven();
167
- const { url, options } = buildSaveRequest(content, snapshotHtml, userDriven, controller.signal);
162
+ // Two independent ways a save is human: an edit made in a trusted turn, or a
163
+ // person pressing Save. Both are consumed here, at the one point past every
164
+ // early return, so neither can survive into an unrelated later save.
165
+ const gestureDriven = consumeUserDriven();
166
+ const explicitlyAsked = consumeExplicitSave();
167
+ const userDriven = gestureDriven || explicitlyAsked;
168
+ const { url, options } = buildSaveRequest(content, userDriven, controller.signal);
168
169
 
169
170
  return fetch(url, options)
170
171
  .then(res => res.text().then(text => {
@@ -208,15 +209,13 @@ function sendSave(content, snapshotHtml) {
208
209
  *
209
210
  * @param {string} html - HTML string to save
210
211
  * @param {Function} callback - Called with the result on completion
211
- * @param {Object} [options]
212
- * @param {?string} [options.snapshotHtml] - unstripped snapshot from the same capture
213
212
  * @returns {Promise<{msg: string, msgType: string, code: ?string, etag: ?string}>}
214
213
  *
215
214
  * @example
216
215
  * const {msg, msgType} = await saveHtml(myHtml);
217
216
  * if (msgType === 'error') console.error('Save failed:', msg);
218
217
  */
219
- export function saveHtml(html, callback = () => {}, { snapshotHtml = null } = {}) {
218
+ export function saveHtml(html, callback = () => {}) {
220
219
  return new Promise((resolve) => {
221
220
  const done = (result) => {
222
221
  if (typeof callback === 'function') callback(result);
@@ -241,7 +240,7 @@ export function saveHtml(html, callback = () => {}, { snapshotHtml = null } = {}
241
240
  return;
242
241
  }
243
242
 
244
- sendSave(html, snapshotHtml)
243
+ sendSave(html)
245
244
  .then(successResult, errorResult)
246
245
  // Clear the flag BEFORE handing the result back, so a caller that queues a
247
246
  // follow-up save can start it immediately rather than being told the lane