hyperclayjs 1.7.0 → 1.9.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 (67) hide show
  1. package/README.md +25 -21
  2. package/package.json +17 -25
  3. package/src/communication/live-sync.js +396 -0
  4. package/{communication → src/communication}/sendMessage.js +2 -8
  5. package/src/core/adminContenteditable.js +51 -0
  6. package/{core → src/core}/adminInputs.js +29 -8
  7. package/src/core/adminOnClick.js +54 -0
  8. package/{core → src/core}/adminResources.js +25 -5
  9. package/{core → src/core}/autosave.js +6 -17
  10. package/src/core/enablePersistentFormInputValues.js +67 -0
  11. package/{core → src/core}/optionVisibility.js +7 -35
  12. package/{core → src/core}/savePage.js +1 -1
  13. package/src/core/savePageCore.js +256 -0
  14. package/src/core/snapshot.js +203 -0
  15. package/src/core/unsavedWarning.js +26 -0
  16. package/{custom-attributes → src/custom-attributes}/ajaxElements.js +3 -10
  17. package/{custom-attributes → src/custom-attributes}/domHelpers.js +17 -4
  18. package/{custom-attributes → src/custom-attributes}/events.js +2 -0
  19. package/{custom-attributes → src/custom-attributes}/onaftersave.js +5 -8
  20. package/src/custom-attributes/onmutation.js +90 -0
  21. package/src/custom-attributes/onpagemutation.js +32 -0
  22. package/{custom-attributes → src/custom-attributes}/sortable.js +16 -1
  23. package/{dom-utilities → src/dom-utilities}/All.js +22 -0
  24. package/src/dom-utilities/insertStyleTag.js +61 -0
  25. package/{hyperclay.js → src/hyperclay.js} +20 -3
  26. package/{module-dependency-graph.json → src/module-dependency-graph.json} +121 -34
  27. package/{ui → src/ui}/prompts.js +13 -18
  28. package/{ui → src/ui}/theModal.js +103 -0
  29. package/{ui → src/ui}/toast.js +4 -3
  30. package/src/utilities/cacheBust.js +19 -0
  31. package/{vendor → src/vendor}/idiomorph.min.js +1 -0
  32. package/core/adminContenteditable.js +0 -36
  33. package/core/adminOnClick.js +0 -31
  34. package/core/enablePersistentFormInputValues.js +0 -72
  35. package/core/savePageCore.js +0 -245
  36. package/custom-attributes/onpagemutation.js +0 -20
  37. package/dom-utilities/insertStyleTag.js +0 -38
  38. /package/{communication → src/communication}/behaviorCollector.js +0 -0
  39. /package/{communication → src/communication}/uploadFile.js +0 -0
  40. /package/{core → src/core}/adminSystem.js +0 -0
  41. /package/{core → src/core}/editmode.js +0 -0
  42. /package/{core → src/core}/editmodeSystem.js +0 -0
  43. /package/{core → src/core}/exportToWindow.js +0 -0
  44. /package/{core → src/core}/isAdminOfCurrentResource.js +0 -0
  45. /package/{core → src/core}/saveToast.js +0 -0
  46. /package/{core → src/core}/setPageTypeOnDocumentElement.js +0 -0
  47. /package/{custom-attributes → src/custom-attributes}/autosize.js +0 -0
  48. /package/{custom-attributes → src/custom-attributes}/inputHelpers.js +0 -0
  49. /package/{custom-attributes → src/custom-attributes}/onclickaway.js +0 -0
  50. /package/{custom-attributes → src/custom-attributes}/onclone.js +0 -0
  51. /package/{custom-attributes → src/custom-attributes}/onrender.js +0 -0
  52. /package/{custom-attributes → src/custom-attributes}/preventEnter.js +0 -0
  53. /package/{dom-utilities → src/dom-utilities}/getDataFromForm.js +0 -0
  54. /package/{dom-utilities → src/dom-utilities}/onDomReady.js +0 -0
  55. /package/{dom-utilities → src/dom-utilities}/onLoad.js +0 -0
  56. /package/{string-utilities → src/string-utilities}/copy-to-clipboard.js +0 -0
  57. /package/{string-utilities → src/string-utilities}/query.js +0 -0
  58. /package/{string-utilities → src/string-utilities}/slugify.js +0 -0
  59. /package/{ui → src/ui}/toast-hyperclay.js +0 -0
  60. /package/{utilities → src/utilities}/cookie.js +0 -0
  61. /package/{utilities → src/utilities}/debounce.js +0 -0
  62. /package/{utilities → src/utilities}/loadVendorScript.js +0 -0
  63. /package/{utilities → src/utilities}/mutation.js +0 -0
  64. /package/{utilities → src/utilities}/nearest.js +0 -0
  65. /package/{utilities → src/utilities}/pipe.js +0 -0
  66. /package/{utilities → src/utilities}/throttle.js +0 -0
  67. /package/{vendor → src/vendor}/Sortable.vendor.js +0 -0
@@ -18,13 +18,28 @@ export function enableAdminInputsOnPageLoad() {
18
18
  if (!isEditMode) return;
19
19
 
20
20
  onDomReady(() => {
21
- document.querySelectorAll('[edit-mode-input]').forEach(input => {
22
- if (supportsReadonly(input)) {
23
- input.removeAttribute('readonly');
24
- } else {
25
- input.removeAttribute('disabled');
26
- }
27
- });
21
+ enableAdminInputs();
22
+ });
23
+ }
24
+
25
+ // Runtime toggle functions
26
+ export function enableAdminInputs() {
27
+ document.querySelectorAll('[edit-mode-input]').forEach(input => {
28
+ if (supportsReadonly(input)) {
29
+ input.removeAttribute('readonly');
30
+ } else {
31
+ input.removeAttribute('disabled');
32
+ }
33
+ });
34
+ }
35
+
36
+ export function disableAdminInputs() {
37
+ document.querySelectorAll('[edit-mode-input]').forEach(input => {
38
+ if (supportsReadonly(input)) {
39
+ input.setAttribute('readonly', '');
40
+ } else {
41
+ input.setAttribute('disabled', '');
42
+ }
28
43
  });
29
44
  }
30
45
 
@@ -55,4 +70,10 @@ function supportsReadonly(element) {
55
70
  export function init() {
56
71
  disableAdminInputsBeforeSave();
57
72
  enableAdminInputsOnPageLoad();
58
- }
73
+ }
74
+
75
+ // Export to window
76
+ window.hyperclay = window.hyperclay || {};
77
+ window.hyperclay.enableAdminInputs = enableAdminInputs;
78
+ window.hyperclay.disableAdminInputs = disableAdminInputs;
79
+ window.h = window.hyperclay;
@@ -0,0 +1,54 @@
1
+ import { isEditMode, isOwner } from "./isAdminOfCurrentResource.js";
2
+ import onDomReady from "../dom-utilities/onDomReady.js";
3
+ import {beforeSave} from "./savePage.js";
4
+
5
+ export function disableOnClickBeforeSave () {
6
+ beforeSave(docElem => {
7
+ docElem.querySelectorAll('[edit-mode-onclick]').forEach(resource => {
8
+ const originalValue = resource.getAttribute("onclick");
9
+ resource.setAttribute("inert-onclick", originalValue);
10
+ resource.removeAttribute("onclick");
11
+ });
12
+ });
13
+ }
14
+
15
+ export function enableOnClickForAdminOnPageLoad () {
16
+ if (!isEditMode) return;
17
+
18
+ onDomReady(() => {
19
+ enableOnClick();
20
+ });
21
+ }
22
+
23
+ // Runtime toggle functions
24
+ export function enableOnClick() {
25
+ document.querySelectorAll('[edit-mode-onclick]').forEach(el => {
26
+ const val = el.getAttribute("inert-onclick");
27
+ if (val) {
28
+ el.setAttribute("onclick", val);
29
+ el.removeAttribute("inert-onclick");
30
+ }
31
+ });
32
+ }
33
+
34
+ export function disableOnClick() {
35
+ document.querySelectorAll('[edit-mode-onclick]').forEach(el => {
36
+ const val = el.getAttribute("onclick");
37
+ if (val) {
38
+ el.setAttribute("inert-onclick", val);
39
+ el.removeAttribute("onclick");
40
+ }
41
+ });
42
+ }
43
+
44
+ // Auto-initialize
45
+ export function init() {
46
+ disableOnClickBeforeSave();
47
+ enableOnClickForAdminOnPageLoad();
48
+ }
49
+
50
+ // Export to window
51
+ window.hyperclay = window.hyperclay || {};
52
+ window.hyperclay.enableOnClick = enableOnClick;
53
+ window.hyperclay.disableOnClick = disableOnClick;
54
+ window.h = window.hyperclay;
@@ -18,11 +18,25 @@ export function enableAdminResourcesOnPageLoad () {
18
18
  if (!isEditMode) return;
19
19
 
20
20
  onDomReady(() => {
21
- document.querySelectorAll('[edit-mode-resource]:is(style, link, script)[type^="inert/"]').forEach(resource => {
22
- // works for js and css
23
- resource.type = resource.type.replace(/inert\//g, '');
21
+ enableAdminResources();
22
+ });
23
+ }
24
+
25
+ // Runtime toggle functions
26
+ export function enableAdminResources() {
27
+ document.querySelectorAll('[edit-mode-resource]:is(style, link, script)[type^="inert/"]').forEach(resource => {
28
+ resource.type = resource.type.replace(/inert\//g, '');
29
+ resource.replaceWith(resource.cloneNode(true));
30
+ });
31
+ }
32
+
33
+ export function disableAdminResources() {
34
+ document.querySelectorAll('[edit-mode-resource]:is(style, link, script)').forEach(resource => {
35
+ const currentType = resource.getAttribute('type') || 'text/javascript';
36
+ if (!currentType.startsWith('inert/')) {
37
+ resource.setAttribute('type', `inert/${currentType}`);
24
38
  resource.replaceWith(resource.cloneNode(true));
25
- });
39
+ }
26
40
  });
27
41
  }
28
42
 
@@ -30,4 +44,10 @@ export function enableAdminResourcesOnPageLoad () {
30
44
  export function init() {
31
45
  disableAdminResourcesBeforeSave();
32
46
  enableAdminResourcesOnPageLoad();
33
- }
47
+ }
48
+
49
+ // Export to window
50
+ window.hyperclay = window.hyperclay || {};
51
+ window.hyperclay.enableAdminResources = enableAdminResources;
52
+ window.hyperclay.disableAdminResources = disableAdminResources;
53
+ window.h = window.hyperclay;
@@ -2,18 +2,17 @@
2
2
  * Auto-save system for Hyperclay
3
3
  *
4
4
  * Automatically saves page on DOM changes with throttling.
5
- * Warns before leaving page with unsaved changes.
6
5
  *
7
6
  * Requires the 'save-system' module to be loaded first.
8
- * For toast notifications, also load the 'save-toast' module.
7
+ *
8
+ * Recommended companion modules:
9
+ * - 'unsaved-warning' - Warn before leaving with unsaved changes (required for beforeunload)
10
+ * - 'save-toast' - Show toast notifications on save events
9
11
  */
10
12
 
11
13
  import Mutation from "../utilities/mutation.js";
12
- import { isEditMode, isOwner } from "./isAdminOfCurrentResource.js";
13
- import {
14
- savePageThrottled,
15
- getUnsavedChanges
16
- } from "./savePage.js";
14
+ import { isEditMode } from "./isAdminOfCurrentResource.js";
15
+ import { savePageThrottled } from "./savePage.js";
17
16
 
18
17
  /**
19
18
  * Initialize auto-save on DOM changes
@@ -28,16 +27,6 @@ function initSavePageOnChange() {
28
27
  });
29
28
  }
30
29
 
31
- /**
32
- * Warn before leaving page with unsaved changes
33
- */
34
- window.addEventListener('beforeunload', (event) => {
35
- if (getUnsavedChanges() && isOwner) {
36
- event.preventDefault();
37
- event.returnValue = '';
38
- }
39
- });
40
-
41
30
  function init() {
42
31
  if (!isEditMode) return;
43
32
  initSavePageOnChange();
@@ -0,0 +1,67 @@
1
+ import { onSnapshot } from './snapshot.js';
2
+
3
+ // <input type="checkbox" persist>
4
+ export default function enablePersistentFormInputValues(filterBySelector = "[persist]") {
5
+ const inputSelector = `input${filterBySelector}:not([type="password"]):not([type="hidden"]):not([type="file"])`;
6
+ const textareaSelector = `textarea${filterBySelector}`;
7
+ const selectSelector = `select${filterBySelector}`;
8
+
9
+ // Use onSnapshot so form values are synced for both save AND live-sync
10
+ onSnapshot((doc) => {
11
+ // Sync text inputs
12
+ const liveInputs = document.querySelectorAll(inputSelector);
13
+ const clonedInputs = doc.querySelectorAll(inputSelector);
14
+ clonedInputs.forEach((cloned, i) => {
15
+ const live = liveInputs[i];
16
+ if (live.type === 'checkbox' || live.type === 'radio') {
17
+ if (live.checked) {
18
+ cloned.setAttribute('checked', '');
19
+ } else {
20
+ cloned.removeAttribute('checked');
21
+ }
22
+ } else {
23
+ cloned.setAttribute('value', live.value);
24
+ }
25
+ });
26
+
27
+ // Sync textareas
28
+ const liveTextareas = document.querySelectorAll(textareaSelector);
29
+ const clonedTextareas = doc.querySelectorAll(textareaSelector);
30
+ clonedTextareas.forEach((cloned, i) => {
31
+ cloned.textContent = liveTextareas[i].value;
32
+ });
33
+
34
+ // Sync selects
35
+ const liveSelects = document.querySelectorAll(selectSelector);
36
+ const clonedSelects = doc.querySelectorAll(selectSelector);
37
+ clonedSelects.forEach((cloned, i) => {
38
+ const live = liveSelects[i];
39
+ const clonedOptions = cloned.querySelectorAll('option');
40
+ clonedOptions.forEach(opt => opt.removeAttribute('selected'));
41
+
42
+ if (live.multiple) {
43
+ Array.from(live.selectedOptions).forEach(opt => {
44
+ const idx = Array.from(live.options).indexOf(opt);
45
+ if (clonedOptions[idx]) clonedOptions[idx].setAttribute('selected', '');
46
+ });
47
+ } else if (live.selectedIndex >= 0 && clonedOptions[live.selectedIndex]) {
48
+ clonedOptions[live.selectedIndex].setAttribute('selected', '');
49
+ }
50
+ });
51
+ });
52
+ }
53
+
54
+ // Auto-initialize with default selector
55
+ export function init() {
56
+ enablePersistentFormInputValues("[persist]");
57
+ }
58
+
59
+ // Auto-export to window unless suppressed by loader
60
+ if (!window.__hyperclayNoAutoExport) {
61
+ window.hyperclay = window.hyperclay || {};
62
+ window.hyperclay.enablePersistentFormInputValues = enablePersistentFormInputValues;
63
+ window.h = window.hyperclay;
64
+ }
65
+
66
+ // Auto-init when module is imported
67
+ init();
@@ -31,16 +31,15 @@
31
31
  */
32
32
 
33
33
  import Mutation from "../utilities/mutation.js";
34
+ import insertStyles from "../dom-utilities/insertStyleTag.js";
35
+
36
+ const STYLE_NAME = 'option-visibility';
34
37
 
35
38
  const optionVisibility = {
36
39
  debug: false,
37
40
  _started: false,
38
- _styleElement: null,
39
41
  _unsubscribe: null,
40
42
 
41
- LAYER_NAME: 'option-visibility',
42
- STYLE_CLASS: 'option-visibility-layer-styles',
43
-
44
43
  log(...args) {
45
44
  if (this.debug) console.log('[OptionVisibility:Layer]', ...args);
46
45
  },
@@ -104,7 +103,7 @@ const optionVisibility = {
104
103
  return `[option\\:${safeName}="${safeValue}"]{display:none!important}[${safeName}="${safeValue}"] [option\\:${safeName}="${safeValue}"]{display:revert-layer!important}`;
105
104
  }).join('');
106
105
 
107
- return `@layer ${this.LAYER_NAME}{${rules}}`;
106
+ return `@layer ${STYLE_NAME}{${rules}}`;
108
107
  },
109
108
 
110
109
  /**
@@ -119,33 +118,8 @@ const optionVisibility = {
119
118
  try {
120
119
  const attributes = this.findOptionAttributes();
121
120
  const css = this.generateCSS(attributes);
122
-
123
- // Remove style element if no attributes
124
- if (!css) {
125
- if (this._styleElement) {
126
- this._styleElement.remove();
127
- this._styleElement = null;
128
- this.log('Removed empty style element');
129
- }
130
- return;
131
- }
132
-
133
- // Skip if unchanged
134
- if (this._styleElement?.textContent === css) {
135
- this.log('Styles unchanged');
136
- return;
137
- }
138
-
139
- // Create or update
140
- if (!this._styleElement) {
141
- this._styleElement = document.createElement('style');
142
- this._styleElement.className = this.STYLE_CLASS;
143
- document.head.appendChild(this._styleElement);
144
- }
145
-
146
- this._styleElement.textContent = css;
121
+ insertStyles(STYLE_NAME, css);
147
122
  this.log(`Generated ${attributes.length} rules`);
148
-
149
123
  } catch (error) {
150
124
  console.error('[OptionVisibility:Layer] Error generating rules:', error);
151
125
  }
@@ -190,10 +164,8 @@ const optionVisibility = {
190
164
  this._unsubscribe = null;
191
165
  }
192
166
 
193
- if (this._styleElement) {
194
- this._styleElement.remove();
195
- this._styleElement = null;
196
- }
167
+ const style = document.querySelector(`style[data-name="${STYLE_NAME}"]`);
168
+ if (style) style.remove();
197
169
 
198
170
  this.log('Stopped');
199
171
  }
@@ -111,7 +111,7 @@ document.addEventListener('DOMContentLoaded', () => {
111
111
  * @param {Function} callback - Optional callback for custom handling
112
112
  */
113
113
  export function savePage(callback = () => {}) {
114
- if (!isEditMode) {
114
+ if (!isEditMode && !window.hyperclay?.testMode) {
115
115
  return;
116
116
  }
117
117
 
@@ -0,0 +1,256 @@
1
+ /**
2
+ * savePageCore.js — Network save functionality
3
+ *
4
+ * This module handles sending page contents to the server.
5
+ * It uses snapshot.js for capturing the DOM state.
6
+ *
7
+ * For full save system with state management, use savePage.js instead.
8
+ */
9
+
10
+ import cookie from "../utilities/cookie.js";
11
+ import { isEditMode } from "./isAdminOfCurrentResource.js";
12
+ import {
13
+ captureForSave,
14
+ isCodeMirrorPage,
15
+ getCodeMirrorContents,
16
+ beforeSave,
17
+ getPageContents,
18
+ onSnapshot,
19
+ onPrepareForSave
20
+ } from "./snapshot.js";
21
+
22
+ // =============================================================================
23
+ // STATE
24
+ // =============================================================================
25
+
26
+ let saveInProgress = false;
27
+ const saveEndpoint = `/save/${cookie.get("currentResource")}`;
28
+
29
+ // =============================================================================
30
+ // RE-EXPORTS FROM SNAPSHOT (for backwards compat)
31
+ // =============================================================================
32
+
33
+ export { beforeSave, getPageContents, onSnapshot, onPrepareForSave };
34
+
35
+ // =============================================================================
36
+ // INTERNAL: GET PAGE CONTENTS
37
+ // =============================================================================
38
+
39
+ /**
40
+ * Get the current page contents as HTML string for saving.
41
+ * Handles both normal pages and CodeMirror editor pages.
42
+ * Emits snapshot-ready event for live-sync (normal pages only).
43
+ *
44
+ * @returns {string} HTML string of current page
45
+ */
46
+ function getContentsForSave() {
47
+ if (isCodeMirrorPage()) {
48
+ // CodeMirror pages don't emit snapshot-ready - no live-sync for code editors
49
+ return getCodeMirrorContents();
50
+ }
51
+ // Emit for live-sync when actually saving
52
+ return captureForSave({ emitForSync: true });
53
+ }
54
+
55
+ // =============================================================================
56
+ // SAVE FUNCTIONS
57
+ // =============================================================================
58
+
59
+ /**
60
+ * Save the current page contents to the server.
61
+ *
62
+ * @param {Function} callback - Called with {msg, msgType} on completion
63
+ * msgType will be 'success' or 'error'
64
+ *
65
+ * @example
66
+ * savePage(({msg, msgType}) => {
67
+ * if (msgType === 'error') {
68
+ * console.error('Save failed:', msg);
69
+ * } else {
70
+ * console.log('Saved:', msg);
71
+ * }
72
+ * });
73
+ */
74
+ export function savePage(callback = () => {}) {
75
+ if (saveInProgress) {
76
+ return;
77
+ }
78
+ if (!isEditMode && !window.hyperclay?.testMode) {
79
+ return;
80
+ }
81
+
82
+ let currentContents;
83
+ try {
84
+ currentContents = getContentsForSave();
85
+ } catch (err) {
86
+ console.error('savePage: getContentsForSave failed', err);
87
+ callback({ msg: err.message, msgType: "error" });
88
+ return;
89
+ }
90
+ saveInProgress = true;
91
+
92
+ // Test mode: skip network request, return mock success
93
+ if (window.hyperclay?.testMode) {
94
+ setTimeout(() => {
95
+ saveInProgress = false;
96
+ if (typeof callback === 'function') {
97
+ callback({ msg: "Test mode: save skipped", msgType: "success" });
98
+ }
99
+ }, 0);
100
+ return;
101
+ }
102
+
103
+ // Add timeout - abort if server doesn't respond within 12 seconds
104
+ const controller = new AbortController();
105
+ const timeoutId = setTimeout(() => controller.abort('Save timeout'), 12000);
106
+
107
+ fetch(saveEndpoint, {
108
+ method: 'POST',
109
+ credentials: 'include',
110
+ body: currentContents,
111
+ signal: controller.signal
112
+ })
113
+ .then(res => {
114
+ clearTimeout(timeoutId);
115
+ return res.json().then(data => {
116
+ if (!res.ok) {
117
+ throw new Error(data.msg || data.error || `HTTP ${res.status}: ${res.statusText}`);
118
+ }
119
+ return data;
120
+ });
121
+ })
122
+ .then(data => {
123
+ if (typeof callback === 'function') {
124
+ callback({ msg: data.msg, msgType: data.msgType || 'success' });
125
+ }
126
+ })
127
+ .catch(err => {
128
+ clearTimeout(timeoutId);
129
+ console.error('Failed to save page:', err);
130
+
131
+ const msg = err.name === 'AbortError'
132
+ ? 'Server not responding'
133
+ : 'Save failed';
134
+
135
+ if (typeof callback === 'function') {
136
+ callback({ msg, msgType: "error" });
137
+ }
138
+ })
139
+ .finally(() => {
140
+ clearTimeout(timeoutId);
141
+ saveInProgress = false;
142
+ });
143
+ }
144
+
145
+ /**
146
+ * Save specific HTML content to the server.
147
+ *
148
+ * @param {string} html - HTML string to save
149
+ * @param {Function} callback - Called with (err, data) on completion
150
+ *
151
+ * @example
152
+ * saveHtml(myHtml, (err, data) => {
153
+ * if (err) {
154
+ * console.error('Save failed:', err);
155
+ * } else {
156
+ * console.log('Saved:', data);
157
+ * }
158
+ * });
159
+ */
160
+ export function saveHtml(html, callback = () => {}) {
161
+ if (!isEditMode || saveInProgress) {
162
+ return;
163
+ }
164
+
165
+ saveInProgress = true;
166
+
167
+ // Test mode: skip network request, return mock success
168
+ if (window.hyperclay?.testMode) {
169
+ setTimeout(() => {
170
+ saveInProgress = false;
171
+ if (typeof callback === 'function') {
172
+ callback(null, { msg: "Test mode: save skipped", msgType: "success" });
173
+ }
174
+ }, 0);
175
+ return;
176
+ }
177
+
178
+ fetch(saveEndpoint, {
179
+ method: 'POST',
180
+ credentials: 'include',
181
+ body: html
182
+ })
183
+ .then(res => {
184
+ return res.json().then(data => {
185
+ if (!res.ok) {
186
+ throw new Error(data.msg || data.error || `HTTP ${res.status}: ${res.statusText}`);
187
+ }
188
+ return data;
189
+ });
190
+ })
191
+ .then(data => {
192
+ if (typeof callback === 'function') {
193
+ callback(null, data);
194
+ }
195
+ })
196
+ .catch(err => {
197
+ console.error('Failed to save page:', err);
198
+ if (typeof callback === 'function') {
199
+ callback(err);
200
+ }
201
+ })
202
+ .finally(() => {
203
+ saveInProgress = false;
204
+ });
205
+ }
206
+
207
+ /**
208
+ * Fetch HTML from a URL and save it to replace the current page.
209
+ *
210
+ * @param {string} url - URL to fetch HTML from
211
+ * @param {Function} callback - Called with (err, data) on completion
212
+ *
213
+ * @example
214
+ * replacePageWith('/templates/blog.html', (err, data) => {
215
+ * if (err) {
216
+ * console.error('Failed:', err);
217
+ * } else {
218
+ * window.location.reload();
219
+ * }
220
+ * });
221
+ */
222
+ export function replacePageWith(url, callback = () => {}) {
223
+ if (!isEditMode || saveInProgress) {
224
+ return;
225
+ }
226
+
227
+ fetch(url)
228
+ .then(res => res.text())
229
+ .then(html => {
230
+ saveHtml(html, (err, data) => {
231
+ if (typeof callback === 'function') {
232
+ callback(err, data);
233
+ }
234
+ });
235
+ })
236
+ .catch(err => {
237
+ console.error('Failed to fetch template:', err);
238
+ if (typeof callback === 'function') {
239
+ callback(err);
240
+ }
241
+ });
242
+ }
243
+
244
+ // =============================================================================
245
+ // WINDOW EXPORTS
246
+ // =============================================================================
247
+
248
+ if (!window.__hyperclayNoAutoExport) {
249
+ window.hyperclay = window.hyperclay || {};
250
+ window.hyperclay.savePage = savePage;
251
+ window.hyperclay.saveHtml = saveHtml;
252
+ window.hyperclay.replacePageWith = replacePageWith;
253
+ window.hyperclay.beforeSave = beforeSave;
254
+ window.hyperclay.getPageContents = getPageContents;
255
+ window.h = window.hyperclay;
256
+ }