@panphora/clayjs 0.4.3 → 0.6.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@panphora/clayjs",
3
- "version": "0.4.3",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "description": "clayjs: malleable HTML files. Save lifecycle for self-saving HTML.",
6
6
  "license": "MIT",
@@ -22,9 +22,10 @@ export function enableContentEditableForAdminOnPageLoad () {
22
22
  });
23
23
  }
24
24
 
25
- // Runtime toggle functions
26
- export function enableContentEditable() {
27
- document.querySelectorAll(SELECTOR).forEach(el => {
25
+ // Runtime toggle functions. `root` lets scoped live sync activate a parsed
26
+ // incoming document the same way boot activates the live one.
27
+ export function enableContentEditable(root = document) {
28
+ root.querySelectorAll(SELECTOR).forEach(el => {
28
29
  let val = el.getAttribute("inert-contenteditable");
29
30
  if (!["false", "plaintext-only"].includes(val)) val = "true";
30
31
  el.setAttribute("contenteditable", val);
@@ -24,11 +24,13 @@ export function enableAdminInputsOnPageLoad() {
24
24
  });
25
25
  }
26
26
 
27
- export function enableAdminInputs() {
28
- document.querySelectorAll(SELECTOR_DISABLED).forEach(input => {
27
+ // `root` lets scoped live sync activate a parsed incoming document the same
28
+ // way boot activates the live one.
29
+ export function enableAdminInputs(root = document) {
30
+ root.querySelectorAll(SELECTOR_DISABLED).forEach(input => {
29
31
  input.removeAttribute('disabled');
30
32
  });
31
- document.querySelectorAll(SELECTOR_READONLY).forEach(input => {
33
+ root.querySelectorAll(SELECTOR_READONLY).forEach(input => {
32
34
  input.removeAttribute('readonly');
33
35
  });
34
36
  }
@@ -22,9 +22,10 @@ export function enableOnClickForAdminOnPageLoad () {
22
22
  });
23
23
  }
24
24
 
25
- // Runtime toggle functions
26
- export function enableOnClick() {
27
- document.querySelectorAll(SELECTOR).forEach(el => {
25
+ // Runtime toggle functions. `root` lets scoped live sync activate a parsed
26
+ // incoming document the same way boot activates the live one.
27
+ export function enableOnClick(root = document) {
28
+ root.querySelectorAll(SELECTOR).forEach(el => {
28
29
  const val = el.getAttribute("inert-onclick");
29
30
  if (val) {
30
31
  el.setAttribute("onclick", val);
@@ -37,11 +37,15 @@ export function enableAdminResourcesOnPageLoad () {
37
37
  });
38
38
  }
39
39
 
40
- // Runtime toggle functions
41
- export function enableAdminResources() {
42
- document.querySelectorAll(SELECTOR_INERT).forEach(resource => {
40
+ // Runtime toggle functions. `root` lets scoped live sync activate a parsed
41
+ // incoming document; the clone-swap re-execution trick only applies to the
42
+ // live document — in a detached parse nothing executes, and the morph's own
43
+ // script handling decides execution when the content lands.
44
+ export function enableAdminResources(root = document) {
45
+ const live = (root.ownerDocument || root) === document;
46
+ root.querySelectorAll(SELECTOR_INERT).forEach(resource => {
43
47
  makeActive(resource);
44
- resource.replaceWith(resource.cloneNode(true));
48
+ if (live) resource.replaceWith(resource.cloneNode(true));
45
49
  });
46
50
  }
47
51
 
@@ -4,8 +4,9 @@ import { hasSaveToken } from "./host-attrs.js";
4
4
 
5
5
  // Edit-mode precedence: an explicit ?editmode=true|false URL param wins, then an
6
6
  // opt-in window.clayEditMode global (with the legacy window.__hyperclayEditMode
7
- // still honored as a fallback — htmlclay injects it today), then a save token the
8
- // host put on the root, then the platform's isAdminOfCurrentResource cookie. The
7
+ // still honored as a fallback for older standalone embedders — htmlclay itself
8
+ // uses the injected htmlclaytoken plus the admin cookie, not this global), then
9
+ // a save token the host put on the root, then the isAdminOfCurrentResource cookie. The
9
10
  // global is for standalone uses (demos, htmlclay, any self-saving file) that are
10
11
  // always editable and have no owner cookie; setting it before clayjs loads turns
11
12
  // on the edit-only modules.
package/src/core/save.js CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  isSaveInProgress
21
21
  } from "./save-core.js";
22
22
  import { captureForComparison, captureForSaveAndComparison } from "./snapshot.js";
23
+ import { gateCaptureToken, gateClearIfUnchanged } from "../lib/dirty-gate.js";
23
24
  import { logSaveCheck, logBaseline } from "../lib/autosave-debug.js";
24
25
 
25
26
  // Reset savestatus to 'saved' in snapshots (each module cleans up its own attrs)
@@ -116,6 +117,37 @@ let lastSavedContents = '';
116
117
  // A save was requested while one was on the wire; run one more when it settles.
117
118
  let pendingSave = false;
118
119
 
120
+ // ============================================
121
+ // AUTOSAVE SUSPENSION
122
+ // ============================================
123
+ //
124
+ // clay.wire holds this for the length of an agent request. The save is
125
+ // last-writer-wins with a backup, so an autosave landing while a local process is
126
+ // writing the same file posts the pre-agent document: the server backs the agent's
127
+ // bytes up and writes the browser's, the watcher's revalidation then fails, and
128
+ // the agent's work exists only in Backups while the page reports success.
129
+ //
130
+ // It suspends AUTOsave only. An explicit savePage — Cmd+S, a [trigger-save]
131
+ // button, or the wire's own pre-send flush — is a deliberate act and still runs.
132
+ //
133
+ // Reference counted, because two overlapping requests each hold it. A save
134
+ // skipped while suspended is replayed on release, or the user's edits would sit
135
+ // unsaved until something else happened to mutate the page.
136
+ let autosaveSuspended = 0;
137
+ let autosaveMissed = false;
138
+
139
+ export function suspendAutosave() {
140
+ autosaveSuspended++;
141
+ }
142
+
143
+ export function resumeAutosave() {
144
+ if (autosaveSuspended === 0) return;
145
+ autosaveSuspended--;
146
+ if (autosaveSuspended > 0 || !autosaveMissed) return;
147
+ autosaveMissed = false;
148
+ savePageThrottled();
149
+ }
150
+
119
151
  function skipped_(msg) {
120
152
  return { ok: false, msg, msgType: 'skipped', code: null, etag: null };
121
153
  }
@@ -129,10 +161,13 @@ function skipped_(msg) {
129
161
  * where the write may or may not have landed, so it is not treated as success
130
162
  * either.
131
163
  */
132
- function applySaveResult(result, forComparison, label) {
164
+ function applySaveResult(result, forComparison, label, gateToken) {
133
165
  if (result.ok) {
134
166
  lastSavedContents = forComparison;
135
167
  unsavedChanges = false;
168
+ // Generation-checked: clears the scoped-sync dirty gate only if nothing
169
+ // changed while this save was on the wire.
170
+ gateClearIfUnchanged(gateToken);
136
171
  // The server's severity rides through untouched: a save can land AND carry a
137
172
  // warning, and the UI module is what decides how to render that.
138
173
  setSaveState('saved', result.msg || 'Saved', result.msgType);
@@ -201,6 +236,7 @@ export function savePage(callback = () => {}) {
201
236
  // forSave strips non-persisted regions ([no-save]/[save-remove])
202
237
  // forComparison additionally strips every autosave-off region
203
238
  let forSave, forComparison, snapshotHtml;
239
+ const gateToken = gateCaptureToken();
204
240
  try {
205
241
  ({ forSave, forComparison, snapshotHtml } = captureForSaveAndComparison());
206
242
  } catch (err) {
@@ -219,6 +255,7 @@ export function savePage(callback = () => {}) {
219
255
 
220
256
  // Skip if content hasn't changed
221
257
  if (!unsavedChanges) {
258
+ gateClearIfUnchanged(gateToken);
222
259
  const skipped = skipped_('No changes to save');
223
260
  callback(skipped);
224
261
  return resolve(skipped);
@@ -229,7 +266,7 @@ export function savePage(callback = () => {}) {
229
266
 
230
267
  // Use saveHtml directly with our pre-captured content (avoids double capture)
231
268
  saveHtml(forSave, (result) => {
232
- applySaveResult(result, forComparison, 'updated after save');
269
+ applySaveResult(result, forComparison, 'updated after save', gateToken);
233
270
  if (typeof callback === 'function') {
234
271
  callback(result);
235
272
  }
@@ -266,6 +303,7 @@ export function savePageForce(callback = () => {}) {
266
303
  }
267
304
 
268
305
  let forSave, forComparison, snapshotHtml;
306
+ const gateToken = gateCaptureToken();
269
307
  try {
270
308
  ({ forSave, forComparison, snapshotHtml } = captureForSaveAndComparison());
271
309
  } catch (err) {
@@ -281,7 +319,7 @@ export function savePageForce(callback = () => {}) {
281
319
  setSavingState();
282
320
 
283
321
  saveHtml(forSave, (result) => {
284
- applySaveResult(result, forComparison, 'updated after force save');
322
+ applySaveResult(result, forComparison, 'updated after force save', gateToken);
285
323
  if (typeof callback === 'function') {
286
324
  callback(result);
287
325
  }
@@ -383,9 +421,13 @@ function initBaselineCapture() {
383
421
  // (if a save happened, lastSavedContents would differ from immediateContents)
384
422
  if (!userEdited && lastSavedContents === immediateContents) {
385
423
  // Store stripped version so comparisons are direct (no parsing needed)
424
+ const gateToken = gateCaptureToken();
386
425
  const contents = captureForComparison();
387
426
  lastSavedContents = contents;
388
427
  baselineContents = contents;
428
+ // Boot churn (modules rewriting attributes at DOM-ready) counted toward
429
+ // the scoped-sync gate; the settled baseline is the proof it wasn't edits.
430
+ gateClearIfUnchanged(gateToken);
389
431
  logBaseline('settled capture', `${contents.length} chars`);
390
432
  } else {
391
433
  logBaseline('settled skipped', userEdited ? 'user edited' : 'save occurred during settle');
@@ -440,12 +482,23 @@ export function savePageThrottled(callback = () => {}) {
440
482
  return Promise.resolve(skipped);
441
483
  }
442
484
 
485
+ // Every autosave path lands here — the mutation-driven one, the [persist] input
486
+ // timer, and live-sync's convergence save after a protected apply — which is
487
+ // why the suspension lives at this one entry rather than at each caller.
488
+ if (autosaveSuspended > 0) {
489
+ autosaveMissed = true;
490
+ const skipped = skipped_('Autosave suspended');
491
+ callback(skipped);
492
+ return Promise.resolve(skipped);
493
+ }
494
+
443
495
  // For autosave: while the page is still settling, content must differ from BOTH
444
496
  // the load-time baseline and the last save, so module setup churn cannot trigger
445
497
  // a save. Once settled, the baseline veto is disarmed and only the last save
446
498
  // matters — otherwise undoing back to the page's original state can never be
447
499
  // persisted, because it matches the baseline forever.
448
500
  // Compare directly - stored versions are already stripped
501
+ const gateToken = gateCaptureToken();
449
502
  const currentForCompare = captureForComparison();
450
503
  const differsFromBaseline = !baselineActive || currentForCompare !== baselineContents;
451
504
  const differsFromLastSave = currentForCompare !== lastSavedContents;
@@ -454,6 +507,7 @@ export function savePageThrottled(callback = () => {}) {
454
507
  logSaveCheck('throttled vs lastSave', !differsFromLastSave);
455
508
 
456
509
  if (!(differsFromBaseline && differsFromLastSave)) {
510
+ if (!differsFromLastSave) gateClearIfUnchanged(gateToken);
457
511
  const skipped = skipped_('No changes to save');
458
512
  callback(skipped);
459
513
  return Promise.resolve(skipped);
@@ -116,13 +116,16 @@ function clonePreventingOnclone(node) {
116
116
  finally { window.__preventOnclone = prev; }
117
117
  }
118
118
 
119
- export function captureSnapshot() {
119
+ export function captureSnapshot({ flushUndo = true } = {}) {
120
120
  // Force-close any pending undo idle batch BEFORE cloning the DOM, so the
121
121
  // snapshot reflects a clean undo boundary. Without this, a save that fires
122
122
  // mid-typing would leave the idle batch open across the save boundary, and
123
123
  // Cmd+Z after save would restore to a state earlier than the last save.
124
124
  // No-op when undo isn't loaded or no batch is pending.
125
- if (typeof window !== 'undefined' && window.clay?.undo?.flush) {
125
+ // flushUndo: false is for captures that are not save boundaries (the scoped
126
+ // live-sync dirty oracle runs per incoming frame, and flushing there would
127
+ // shatter the user's undo batches mid-typing).
128
+ if (flushUndo && typeof window !== 'undefined' && window.clay?.undo?.flush) {
126
129
  window.clay.undo.flush();
127
130
  }
128
131
 
@@ -181,8 +184,8 @@ function prepareCloneForSave(clone) {
181
184
  *
182
185
  * @returns {string} HTML string with all autosave-off regions stripped
183
186
  */
184
- export function captureForComparison() {
185
- const clone = captureSnapshot();
187
+ export function captureForComparison({ flushUndo = true } = {}) {
188
+ const clone = captureSnapshot({ flushUndo });
186
189
 
187
190
  // Run inline [onbeforesave] handlers
188
191
  runAuthoredHandlers(clone, 'onbeforesave');
@@ -260,6 +263,58 @@ export function captureForSaveAndComparison({ emitForSync = true } = {}) {
260
263
  return { forSave, forComparison, snapshotHtml };
261
264
  }
262
265
 
266
+ /**
267
+ * Capture for a protected live-sync merge: the save-domain and comparison-
268
+ * domain clones of ONE snapshot, plus a WeakMap pairing every comparison
269
+ * element to its save-clone twin.
270
+ *
271
+ * The pairing is recorded immediately after the comparison clone is created,
272
+ * while the two trees are still isomorphic; each side's strips then remove
273
+ * nodes independently without disturbing it. The scoped-sync dirty diff runs
274
+ * on the comparison clone (the same domain as lastSavedContents), and dirty
275
+ * roots map through pairMap to save-domain subtrees (the same domain as the
276
+ * file on disk), which still carry the no-trigger-autosave / freeze / no-watch
277
+ * children the comparison strips.
278
+ *
279
+ * Never emits snapshot-ready (this capture must not feed the send pipeline)
280
+ * and never flushes the undo batch (it runs per incoming frame, not per save).
281
+ *
282
+ * @returns {{ saveClone: HTMLElement, compareClone: HTMLElement, pairMap: WeakMap }}
283
+ */
284
+ export function captureForMerge() {
285
+ const clone = captureSnapshot({ flushUndo: false });
286
+
287
+ runAuthoredHandlers(clone, 'onbeforesave');
288
+
289
+ const compareClone = clonePreventingOnclone(clone);
290
+
291
+ const pairMap = new WeakMap();
292
+ (function pair(compareEl, saveEl) {
293
+ pairMap.set(compareEl, saveEl);
294
+ const compareKids = compareEl.children;
295
+ const saveKids = saveEl.children;
296
+ for (let i = 0; i < compareKids.length; i++) {
297
+ pair(compareKids[i], saveKids[i]);
298
+ }
299
+ })(compareClone, clone);
300
+
301
+ for (const hook of documentTransforms) {
302
+ hook(clone);
303
+ }
304
+ for (const el of clone.querySelectorAll(STRIP_FROM_SAVE)) {
305
+ el.remove();
306
+ }
307
+
308
+ for (const el of compareClone.querySelectorAll(STRIP_FROM_COMPARISON)) {
309
+ el.remove();
310
+ }
311
+ for (const hook of documentTransforms) {
312
+ hook(compareClone);
313
+ }
314
+
315
+ return { saveClone: clone, compareClone, pairMap };
316
+ }
317
+
263
318
  /**
264
319
  * PHASE 1-4: Full pipeline for saving to server.
265
320
  *
@@ -0,0 +1,169 @@
1
+ /**
2
+ * dirty-gate.js — "might this page hold unsaved edits?" as a cheap counter.
3
+ *
4
+ * Scoped live sync must decide, on every incoming frame, whether to pay for a
5
+ * full capture-and-diff (the oracle) or apply the frame directly. This gate is
6
+ * the cheap side of that decision. It may over-report (a false "dirty" just
7
+ * runs the oracle, which then finds nothing), but it must never under-report:
8
+ * a false "clean" full-morphs over a real unsaved edit.
9
+ *
10
+ * Two feeds, because neither alone sees everything:
11
+ * - the mutation hub (require: 'autosave'), which sees DOM changes but not
12
+ * value-property writes on form controls (typing fires no MutationRecord
13
+ * for .value), and
14
+ * - capture-phase input/change listeners on ALL form controls, not just
15
+ * [persist] ones, plus contenteditable hosts.
16
+ *
17
+ * The one hole left is a PROGRAMMATIC value write on a [persist] control: no
18
+ * event, no MutationRecord. persistProbeDirty() closes it by comparing each
19
+ * [persist] control's live state against its serialized default state, with a
20
+ * per-element cache so a value the oracle or a save has already verified stops
21
+ * costing a probe miss on every frame.
22
+ *
23
+ * Clearing is generation-checked: a save records the counter at capture time
24
+ * and clears only if nothing changed while the request was on the wire, so
25
+ * keystrokes during an in-flight save keep the gate dirty.
26
+ */
27
+
28
+ import Mutation from './mutation.js';
29
+ import { isEditMode } from '../core/is-edit-mode.js';
30
+ import { STRIP_FROM_COMPARISON, SNAPSHOT_REMOVE_SELECTOR } from './region-policy.js';
31
+
32
+ let changes = 0;
33
+ let clearedAt = 0;
34
+ let paused = false;
35
+ let started = false;
36
+
37
+ const PERSIST_CONTROLS = 'input[persist], textarea[persist], select[persist]';
38
+
39
+ // Regions the comparison never sees. The hub feed already skips them, through
40
+ // `require: 'autosave'`, and the input feed has to skip them for the same
41
+ // reason: their content is stripped from the comparison clone, so an edit inside
42
+ // one can never produce a dirty root, and counting it marks the page dirty with
43
+ // nothing for the oracle to find — permanently, since only a save clears the
44
+ // counter and churn in these regions triggers none.
45
+ //
46
+ // This is not a relaxation of "never under-report". A control here is absent
47
+ // from the clone by definition, so there is nothing about it to under-report.
48
+ // It matters because a mounted tool (redpen's answer field, any panel that
49
+ // marks itself no-save) is a real <textarea> in the document: one keystroke in
50
+ // it used to freeze the live-sync save baseline for the rest of the session,
51
+ // after which every incoming disk change was diffed against a stale base, and
52
+ // the previous change was spliced back over the newer one and written to disk.
53
+ const GATE_IGNORE = `${STRIP_FROM_COMPARISON}, ${SNAPSHOT_REMOVE_SELECTOR}`;
54
+ const probeCache = new WeakMap();
55
+
56
+ function onUserInput(event) {
57
+ // Deliberately NOT gated on `paused`: a morph never dispatches input or
58
+ // change events, so anything arriving here is the user — including typing
59
+ // during a morph's async resource wait, which must keep the page dirty.
60
+ const el = event.target;
61
+ if (!el || el.nodeType !== 1) return;
62
+ if (!(el.matches('input, textarea, select') || el.isContentEditable)) return;
63
+ if (el.closest(GATE_IGNORE)) return;
64
+ changes++;
65
+ }
66
+
67
+ export function startDirtyGate() {
68
+ if (started) return;
69
+ started = true;
70
+ Mutation.onAnyChange(
71
+ { omitChangeDetails: true, require: 'autosave' },
72
+ () => {
73
+ if (!paused) changes++;
74
+ }
75
+ );
76
+ document.addEventListener('input', onUserInput, true);
77
+ document.addEventListener('change', onUserInput, true);
78
+ }
79
+
80
+ /** The morph-apply window: nothing that happens inside it is a user edit. */
81
+ export function pauseGate() { paused = true; }
82
+ export function resumeGate() { paused = false; }
83
+
84
+ function controlSignature(el) {
85
+ if (el instanceof HTMLSelectElement) {
86
+ return Array.from(el.selectedOptions, (o) => o.value).join('\u0000');
87
+ }
88
+ if (el instanceof HTMLInputElement && (el.type === 'checkbox' || el.type === 'radio')) {
89
+ return el.checked ? '1' : '0';
90
+ }
91
+ return el.value;
92
+ }
93
+
94
+ // True when the control's live state matches its serialized (attribute-level)
95
+ // state — i.e. a snapshot taken right now would carry nothing new for it.
96
+ // persist's own listeners keep attributes current for USER input; only
97
+ // programmatic writes diverge here.
98
+ function serializedStateMatches(el) {
99
+ if (el instanceof HTMLSelectElement) {
100
+ return Array.from(el.options).every((o) => o.selected === o.defaultSelected);
101
+ }
102
+ if (el instanceof HTMLInputElement) {
103
+ if (el.type === 'checkbox' || el.type === 'radio') {
104
+ return el.checked === el.defaultChecked;
105
+ }
106
+ return el.value === el.defaultValue;
107
+ }
108
+ if (el instanceof HTMLTextAreaElement) {
109
+ if (el.hasAttribute('data-value')) {
110
+ return el.value === el.getAttribute('data-value');
111
+ }
112
+ return el.value === el.defaultValue;
113
+ }
114
+ return true;
115
+ }
116
+
117
+ export function persistProbeDirty() {
118
+ for (const el of document.querySelectorAll(PERSIST_CONTROLS)) {
119
+ const sig = controlSignature(el);
120
+ if (probeCache.get(el) === sig) continue;
121
+ if (serializedStateMatches(el)) {
122
+ probeCache.set(el, sig);
123
+ continue;
124
+ }
125
+ return true;
126
+ }
127
+ return false;
128
+ }
129
+
130
+ /** Call after the oracle verified the whole page clean against its baseline. */
131
+ export function probeMarkClean() {
132
+ for (const el of document.querySelectorAll(PERSIST_CONTROLS)) {
133
+ probeCache.set(el, controlSignature(el));
134
+ }
135
+ }
136
+
137
+ export function pageMaybeDirty() {
138
+ return changes > clearedAt || persistProbeDirty();
139
+ }
140
+
141
+ /**
142
+ * Record the gate state at capture time. The save flow takes a token when it
143
+ * clones the DOM and hands it back on success (or on a verified "no changes"),
144
+ * so a clear can never swallow edits made while the save was in flight.
145
+ */
146
+ export function gateCaptureToken() {
147
+ const probe = [];
148
+ for (const el of document.querySelectorAll(PERSIST_CONTROLS)) {
149
+ probe.push([el, controlSignature(el)]);
150
+ }
151
+ return { gen: changes, probe };
152
+ }
153
+
154
+ export function gateClearIfUnchanged(token) {
155
+ if (!token) return;
156
+ if (changes === token.gen) {
157
+ clearedAt = token.gen;
158
+ }
159
+ // Probe entries clear per element, and only if the element still holds the
160
+ // exact value the capture carried — a programmatic write between capture
161
+ // and completion stays dirty.
162
+ for (const [el, sig] of token.probe) {
163
+ if (controlSignature(el) === sig) probeCache.set(el, sig);
164
+ }
165
+ }
166
+
167
+ if (typeof document !== 'undefined' && isEditMode) {
168
+ startDirtyGate();
169
+ }
@@ -12,7 +12,11 @@
12
12
  // per-tab and makes the host strip it before writing, so it never reaches disk.
13
13
  // But §9 bounds only the save path, and §10 fans a snapshot out to other editors'
14
14
  // browsers, which is the hole this module closes.
15
- export const HOST_TOKEN_ATTRS = ["savetoken", "htmlclaytoken"];
15
+ //
16
+ // htmlclayid is host-injected too (htmlclay's durable file identity, stamped on
17
+ // every serve, absent from disk bytes). It rides here so a morph of raw disk
18
+ // content cannot strip this tab's copy, and a peer's copy is never applied.
19
+ export const HOST_TOKEN_ATTRS = ["savetoken", "htmlclaytoken", "htmlclayid"];
16
20
 
17
21
  // Which save envelope this host's lane takes: a fact about the response, not
18
22
  // about the document.
@@ -21,10 +21,11 @@ export const PLUGIN_PATHS = {
21
21
  sortable: { path: "plugins/sortable.js", editOnly: true, default: false },
22
22
  undo: { path: "plugins/undo.js", editOnly: true, default: false },
23
23
  cms: { path: "vendor/hypercms.vendor.js", editOnly: false, default: false },
24
+ wire: { path: "plugins/wire.js", editOnly: false, default: false },
24
25
  demo: { path: "plugins/demo.js", editOnly: false, default: false },
25
26
  };
26
27
 
27
- const PLUGIN_ORDER = ["richclay", "indicator", "sortable", "undo", "cms", "sync", "demo"];
28
+ const PLUGIN_ORDER = ["richclay", "indicator", "sortable", "undo", "cms", "sync", "wire", "demo"];
28
29
 
29
30
  function parseCsv(params, key, enabled, apply) {
30
31
  const raw = params.get(key);
package/src/loader.js CHANGED
@@ -102,6 +102,8 @@ function attachPluginMember(path, mod) {
102
102
  clay.morph = mod.morph;
103
103
  } else if (path === "vendor/hypercms.vendor.js") {
104
104
  clay.cms = mod.cms || mod.default;
105
+ } else if (path === "plugins/wire.js") {
106
+ clay.wire = mod.wire || mod.default;
105
107
  } else if (path === "plugins/demo.js") {
106
108
  clay.demo = mod.demo;
107
109
  } else if (path === "vendor/richclay.vendor.js") {