hyperclayjs 1.15.0 → 1.17.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.
@@ -26,6 +26,14 @@ import {
26
26
  let saveInProgress = false;
27
27
  const saveEndpoint = `/save/${cookie.get("currentResource")}`;
28
28
 
29
+ /**
30
+ * Check if a save is currently in progress.
31
+ * @returns {boolean}
32
+ */
33
+ export function isSaveInProgress() {
34
+ return saveInProgress;
35
+ }
36
+
29
37
  // =============================================================================
30
38
  // RE-EXPORTS FROM SNAPSHOT (for backwards compat)
31
39
  // =============================================================================
@@ -102,7 +110,7 @@ export function savePage(callback = () => {}) {
102
110
 
103
111
  // Add timeout - abort if server doesn't respond within 12 seconds
104
112
  const controller = new AbortController();
105
- const timeoutId = setTimeout(() => controller.abort('Save timeout'), 12000);
113
+ const timeoutId = setTimeout(() => controller.abort(), 12000);
106
114
 
107
115
  fetch(saveEndpoint, {
108
116
  method: 'POST',
@@ -175,12 +183,18 @@ export function saveHtml(html, callback = () => {}) {
175
183
  return;
176
184
  }
177
185
 
186
+ // Add timeout - abort if server doesn't respond within 12 seconds
187
+ const controller = new AbortController();
188
+ const timeoutId = setTimeout(() => controller.abort(), 12000);
189
+
178
190
  fetch(saveEndpoint, {
179
191
  method: 'POST',
180
192
  credentials: 'include',
181
- body: html
193
+ body: html,
194
+ signal: controller.signal
182
195
  })
183
196
  .then(res => {
197
+ clearTimeout(timeoutId);
184
198
  return res.json().then(data => {
185
199
  if (!res.ok) {
186
200
  throw new Error(data.msg || data.error || `HTTP ${res.status}: ${res.statusText}`);
@@ -194,12 +208,20 @@ export function saveHtml(html, callback = () => {}) {
194
208
  }
195
209
  })
196
210
  .catch(err => {
211
+ clearTimeout(timeoutId);
197
212
  console.error('Failed to save page:', err);
213
+
214
+ // Normalize timeout errors
215
+ const error = err.name === 'AbortError'
216
+ ? new Error('Server not responding')
217
+ : err;
218
+
198
219
  if (typeof callback === 'function') {
199
- callback(err);
220
+ callback(error);
200
221
  }
201
222
  })
202
223
  .finally(() => {
224
+ clearTimeout(timeoutId);
203
225
  saveInProgress = false;
204
226
  });
205
227
  }
@@ -21,7 +21,7 @@
21
21
  * │ 3a. PREPARE HOOKS │ │ 3b. DONE │
22
22
  * │ onPrepareForSave │ │ (live-sync stops here) │
23
23
  * │ [onbeforesave] │ │ │
24
- * │ [save-ignore] │ │ → emits snapshot-ready │
24
+ * │ [save-remove] │ │ → emits snapshot-ready │
25
25
  * │ │ └─────────────────────────┘
26
26
  * │ ✓ Used by: SAVE only │
27
27
  * └─────────────────────────┘
@@ -102,7 +102,7 @@ function prepareCloneForSave(clone) {
102
102
  }
103
103
 
104
104
  // Remove elements that shouldn't be saved
105
- for (const el of clone.querySelectorAll('[save-ignore]')) {
105
+ for (const el of clone.querySelectorAll('[save-remove]')) {
106
106
  el.remove();
107
107
  }
108
108
 
@@ -114,6 +114,95 @@ function prepareCloneForSave(clone) {
114
114
  return "<!DOCTYPE html>" + clone.outerHTML;
115
115
  }
116
116
 
117
+ /**
118
+ * Capture snapshot prepared for dirty/change comparison.
119
+ *
120
+ * Like captureForSave but also strips [save-ignore] elements.
121
+ * Use this for comparing current state against baselines.
122
+ *
123
+ * @returns {string} HTML string with [save-remove] and [save-ignore] stripped
124
+ */
125
+ export function captureForComparison() {
126
+ // CodeMirror pages: return editor content directly (same for save and compare)
127
+ if (isCodeMirrorPage()) {
128
+ return getCodeMirrorContents();
129
+ }
130
+
131
+ const clone = captureSnapshot();
132
+
133
+ // Run inline [onbeforesave] handlers
134
+ for (const el of clone.querySelectorAll('[onbeforesave]')) {
135
+ new Function(el.getAttribute('onbeforesave')).call(el);
136
+ }
137
+
138
+ // Strip before hooks (hooks see the "final" state)
139
+ for (const el of clone.querySelectorAll('[save-remove], [save-ignore]')) {
140
+ el.remove();
141
+ }
142
+
143
+ // Run registered prepare hooks
144
+ for (const hook of prepareForSaveHooks) {
145
+ hook(clone);
146
+ }
147
+
148
+ return "<!DOCTYPE html>" + clone.outerHTML;
149
+ }
150
+
151
+ /**
152
+ * Single-capture function for both saving and comparison.
153
+ *
154
+ * Clones the DOM once, then clones that clone for comparison.
155
+ * More efficient than calling captureForSave() and captureForComparison() separately.
156
+ *
157
+ * @param {Object} options
158
+ * @param {boolean} options.emitForSync - Whether to emit snapshot-ready event (default: true)
159
+ * @returns {{ forSave: string, forComparison: string }}
160
+ */
161
+ export function captureForSaveAndComparison({ emitForSync = true } = {}) {
162
+ // CodeMirror pages: return editor content directly, skip snapshot-ready
163
+ if (isCodeMirrorPage()) {
164
+ const contents = getCodeMirrorContents();
165
+ return { forSave: contents, forComparison: contents };
166
+ }
167
+
168
+ const clone = captureSnapshot();
169
+
170
+ // Emit for live-sync before any stripping
171
+ if (emitForSync) {
172
+ document.dispatchEvent(new CustomEvent('hyperclay:snapshot-ready', {
173
+ detail: { documentElement: clone }
174
+ }));
175
+ }
176
+
177
+ // Run inline [onbeforesave] handlers
178
+ for (const el of clone.querySelectorAll('[onbeforesave]')) {
179
+ new Function(el.getAttribute('onbeforesave')).call(el);
180
+ }
181
+
182
+ // Clone for comparison before stripping (cheaper than cloning live DOM)
183
+ const compareClone = clone.cloneNode(true);
184
+
185
+ // Save clone: strip [save-remove], then run hooks
186
+ for (const el of clone.querySelectorAll('[save-remove]')) {
187
+ el.remove();
188
+ }
189
+ for (const hook of prepareForSaveHooks) {
190
+ hook(clone);
191
+ }
192
+ const forSave = "<!DOCTYPE html>" + clone.outerHTML;
193
+
194
+ // Compare clone: strip both, then run hooks
195
+ for (const el of compareClone.querySelectorAll('[save-remove], [save-ignore]')) {
196
+ el.remove();
197
+ }
198
+ for (const hook of prepareForSaveHooks) {
199
+ hook(compareClone);
200
+ }
201
+ const forComparison = "<!DOCTYPE html>" + compareClone.outerHTML;
202
+
203
+ return { forSave, forComparison };
204
+ }
205
+
117
206
  /**
118
207
  * PHASE 1-4: Full pipeline for saving to server.
119
208
  *
@@ -1,15 +1,56 @@
1
1
  import { insertStyles } from '../dom-utilities/insertStyleTag.js';
2
2
  import cookie from '../utilities/cookie.js';
3
3
 
4
+ function findTailwindLink(resourceName) {
5
+ const targetPath = `/tailwindcss/${resourceName}.css`;
6
+ return [...document.querySelectorAll('link[rel="stylesheet"]')]
7
+ .find(el => {
8
+ try {
9
+ const url = new URL(el.getAttribute('href'), location.href);
10
+ return url.pathname === targetPath;
11
+ } catch {
12
+ return false;
13
+ }
14
+ });
15
+ }
16
+
17
+ function swapTailwindLink() {
18
+ const currentResource = cookie.get('currentResource');
19
+ if (!currentResource) return;
20
+
21
+ const oldLink = findTailwindLink(currentResource);
22
+ if (!oldLink) return;
23
+
24
+ const newLink = document.createElement('link');
25
+ newLink.rel = 'stylesheet';
26
+ const url = new URL(oldLink.getAttribute('href'), location.href);
27
+ url.searchParams.set('v', Date.now());
28
+ newLink.href = url.href;
29
+ newLink.setAttribute('save-ignore', '');
30
+
31
+ oldLink.insertAdjacentElement('afterend', newLink);
32
+
33
+ newLink.onload = () => {
34
+ oldLink.remove();
35
+ };
36
+
37
+ setTimeout(() => {
38
+ if (oldLink.parentNode) {
39
+ oldLink.remove();
40
+ }
41
+ }, 2000);
42
+ }
43
+
4
44
  function init() {
5
45
  const currentResource = cookie.get('currentResource');
6
46
  if (!currentResource) return;
7
47
 
8
48
  const href = `/tailwindcss/${currentResource}.css`;
9
49
  insertStyles(href, (link) => {
10
- link.setAttribute('onaftersave', 'cacheBust(this)');
11
- link.setAttribute('mutations-ignore', '');
50
+ link.setAttribute('save-ignore', '');
12
51
  });
52
+
53
+ document.addEventListener('hyperclay:save-saved', swapTailwindLink);
13
54
  }
14
55
 
15
56
  init();
@@ -7,19 +7,31 @@
7
7
  * Works independently of autosave - no mutation observer needed during editing,
8
8
  * just a single comparison when the user tries to leave.
9
9
  *
10
+ * Both current and stored content have [save-remove] and [save-ignore] stripped,
11
+ * so comparison is direct with no parsing needed.
12
+ *
10
13
  * Requires the 'save-system' module (automatically included as dependency).
11
14
  */
12
15
 
13
16
  import { isOwner, isEditMode } from "./isAdminOfCurrentResource.js";
14
- import { getPageContents, getLastSavedContents } from "./savePage.js";
17
+ import { captureForComparison } from "./snapshot.js";
18
+ import { getLastSavedContents } from "./savePage.js";
19
+ import { logUnloadDiffSync, preloadIfEnabled } from "../utilities/autosaveDebug.js";
20
+
21
+ // Pre-load diff library if debug mode is on (so it's ready for unload)
22
+ preloadIfEnabled();
15
23
 
16
24
  window.addEventListener('beforeunload', (event) => {
17
25
  if (!isOwner || !isEditMode) return;
18
26
 
19
- const currentContents = getPageContents();
27
+ // Compare directly - both are already stripped
28
+ const currentForCompare = captureForComparison();
20
29
  const lastSaved = getLastSavedContents();
21
30
 
22
- if (currentContents !== lastSaved) {
31
+ if (currentForCompare !== lastSaved) {
32
+ // Debug: log what's different before showing the warning
33
+ logUnloadDiffSync(currentForCompare, lastSaved);
34
+
23
35
  event.preventDefault();
24
36
  event.returnValue = '';
25
37
  }
@@ -0,0 +1 @@
1
+ var e=new Map;function t(t){var o=e.get(t);o&&o.destroy()}function o(t){var o=e.get(t);o&&o.update()}var r=null;"undefined"==typeof window?((r=function(e){return e}).destroy=function(e){return e},r.update=function(e){return e}):((r=function(t,o){return t&&Array.prototype.forEach.call(t.length?t:[t],function(t){return function(t){if(t&&t.nodeName&&"TEXTAREA"===t.nodeName&&!e.has(t)){var o,r=null,n=window.getComputedStyle(t),i=(o=t.value,function(){a({testForHeightReduction:""===o||!t.value.startsWith(o),restoreTextAlign:null}),o=t.value}),l=function(o){t.removeEventListener("autosize:destroy",l),t.removeEventListener("autosize:update",s),t.removeEventListener("input",i),window.removeEventListener("resize",s),Object.keys(o).forEach(function(e){return t.style[e]=o[e]}),e.delete(t)}.bind(t,{height:t.style.height,resize:t.style.resize,textAlign:t.style.textAlign,overflowY:t.style.overflowY,overflowX:t.style.overflowX,wordWrap:t.style.wordWrap});t.addEventListener("autosize:destroy",l),t.addEventListener("autosize:update",s),t.addEventListener("input",i),window.addEventListener("resize",s),t.style.overflowX="hidden",t.style.wordWrap="break-word",e.set(t,{destroy:l,update:s}),s()}function a(e){var o,i,l=e.restoreTextAlign,s=void 0===l?null:l,d=e.testForHeightReduction,u=void 0===d||d,c=n.overflowY;if(0!==t.scrollHeight&&("vertical"===n.resize?t.style.resize="none":"both"===n.resize&&(t.style.resize="horizontal"),u&&(o=function(e){for(var t=[];e&&e.parentNode&&e.parentNode instanceof Element;)e.parentNode.scrollTop&&t.push([e.parentNode,e.parentNode.scrollTop]),e=e.parentNode;return function(){return t.forEach(function(e){var t=e[0],o=e[1];t.style.scrollBehavior="auto",t.scrollTop=o,t.style.scrollBehavior=null})}}(t),t.style.height=""),i="content-box"===n.boxSizing?t.scrollHeight-(parseFloat(n.paddingTop)+parseFloat(n.paddingBottom)):t.scrollHeight+parseFloat(n.borderTopWidth)+parseFloat(n.borderBottomWidth),"none"!==n.maxHeight&&i>parseFloat(n.maxHeight)?("hidden"===n.overflowY&&(t.style.overflow="scroll"),i=parseFloat(n.maxHeight)):"hidden"!==n.overflowY&&(t.style.overflow="hidden"),t.style.height=i+"px",s&&(t.style.textAlign=s),o&&o(),r!==i&&(t.dispatchEvent(new Event("autosize:resized",{bubbles:!0})),r=i),c!==n.overflow&&!s)){var v=n.textAlign;"hidden"===n.overflow&&(t.style.textAlign="start"===v?"end":"start"),a({restoreTextAlign:v,testForHeightReduction:!0})}}function s(){a({testForHeightReduction:!0,restoreTextAlign:null})}}(t)}),t}).destroy=function(e){return e&&Array.prototype.forEach.call(e.length?e:[e],t),e},r.update=function(e){return e&&Array.prototype.forEach.call(e.length?e:[e],o),e});var n=r;export default n;
@@ -1,17 +1,20 @@
1
- function init () {
2
- document.addEventListener('input', event => {
3
- const target = event.target;
4
- if (target.matches('textarea[autosize]')) {
5
- target.style.overflowY = 'hidden';
6
- target.style.height = 'auto';
7
- target.style.height = target.scrollHeight + 'px';
8
- }
9
- });
1
+ import autosize from './autosize.esm.js';
2
+
3
+ function init() {
4
+ document.querySelectorAll('textarea[autosize]').forEach(autosize);
10
5
 
11
- document.querySelectorAll('textarea[autosize]').forEach(textarea => {
12
- textarea.style.overflowY = 'hidden';
13
- textarea.style.height = textarea.scrollHeight + 'px';
6
+ const observer = new MutationObserver(mutations => {
7
+ mutations.forEach(mutation => {
8
+ mutation.addedNodes.forEach(node => {
9
+ if (node.nodeType === 1) {
10
+ if (node.matches?.('textarea[autosize]')) autosize(node);
11
+ node.querySelectorAll?.('textarea[autosize]').forEach(autosize);
12
+ }
13
+ });
14
+ });
14
15
  });
16
+ observer.observe(document.body, { childList: true, subtree: true });
15
17
  }
18
+
16
19
  export { init };
17
20
  export default init;
@@ -11,7 +11,7 @@
11
11
  - e.g. <ul sortable onsorted="console.log('Items reordered!')"></ul>
12
12
 
13
13
  This wrapper conditionally loads the full Sortable.js vendor script (~118KB)
14
- only when in edit mode. The script is injected with save-ignore so it's
14
+ only when in edit mode. The script is injected with save-remove so it's
15
15
  stripped from the page before saving.
16
16
 
17
17
  */
@@ -62,14 +62,24 @@ function insertStyles(nameOrHref, cssOrCallback, callback) {
62
62
  const href = nameOrHref;
63
63
  const cb = typeof cssOrCallback === 'function' ? cssOrCallback : undefined;
64
64
 
65
- // Normalize href to full URL for comparison
66
- const normalizedHref = new URL(href, window.location.href).href;
65
+ // Helper to get base URL without query params (for comparison)
66
+ const getBaseUrl = (url) => {
67
+ try {
68
+ const parsed = new URL(url, window.location.href);
69
+ return parsed.origin + parsed.pathname;
70
+ } catch {
71
+ return url;
72
+ }
73
+ };
74
+
75
+ // Normalize href to full URL path (without query params) for comparison
76
+ const normalizedHref = getBaseUrl(href);
67
77
 
68
- // Find all links with matching normalized path
78
+ // Find all links with matching normalized path (ignoring query params like ?v=)
69
79
  const existingLinks = [...document.querySelectorAll('link[rel="stylesheet"]')]
70
80
  .filter(el => {
71
81
  try {
72
- return new URL(el.getAttribute('href'), window.location.href).href === normalizedHref;
82
+ return getBaseUrl(el.getAttribute('href')) === normalizedHref;
73
83
  } catch {
74
84
  return false;
75
85
  }
package/src/hyperclay.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * HyperclayJS v1.15.0 - Minimal Browser-Native Loader
2
+ * HyperclayJS v1.17.0 - Minimal Browser-Native Loader
3
3
  *
4
4
  * Modules auto-init when imported (no separate init call needed).
5
5
  * Include `export-to-window` feature to export to window.hyperclay.
@@ -56,7 +56,7 @@ const MODULE_PATHS = {
56
56
  "all-js": "./dom-utilities/All.js",
57
57
  "style-injection": "./dom-utilities/insertStyleTag.js",
58
58
  "form-data": "./dom-utilities/getDataFromForm.js",
59
- "idiomorph": "./vendor/idiomorph.min.js",
59
+ "hyper-morph": "./vendor/hyper-morph.vendor.js",
60
60
  "slugify": "./string-utilities/slugify.js",
61
61
  "copy-to-clipboard": "./string-utilities/copy-to-clipboard.js",
62
62
  "query-params": "./string-utilities/query.js",
@@ -123,7 +123,6 @@ const PRESETS = {
123
123
  "onaftersave",
124
124
  "dialogs",
125
125
  "toast",
126
- "toast-hyperclay",
127
126
  "the-modal",
128
127
  "mutation",
129
128
  "nearest",
@@ -136,7 +135,7 @@ const PRESETS = {
136
135
  "all-js",
137
136
  "style-injection",
138
137
  "form-data",
139
- "idiomorph",
138
+ "hyper-morph",
140
139
  "slugify",
141
140
  "copy-to-clipboard",
142
141
  "query-params",
@@ -162,6 +161,7 @@ const EDIT_MODE_ONLY = new Set([
162
161
  "sortable",
163
162
  "onaftersave",
164
163
  "cache-bust",
164
+ "hyper-morph",
165
165
  "file-upload",
166
166
  "live-sync",
167
167
  "tailwind-inject"
@@ -276,7 +276,8 @@ export const All = window.hyperclayModules['all-js']?.All ?? window.hyperclayMod
276
276
  export const insertStyles = window.hyperclayModules['style-injection']?.insertStyles ?? window.hyperclayModules['style-injection']?.default;
277
277
  export const insertStyleTag = window.hyperclayModules['style-injection']?.insertStyleTag ?? window.hyperclayModules['style-injection']?.default;
278
278
  export const getDataFromForm = window.hyperclayModules['form-data']?.getDataFromForm ?? window.hyperclayModules['form-data']?.default;
279
- export const Idiomorph = window.hyperclayModules['idiomorph']?.Idiomorph ?? window.hyperclayModules['idiomorph']?.default;
279
+ export const HyperMorph = window.hyperclayModules['hyper-morph']?.HyperMorph ?? window.hyperclayModules['hyper-morph']?.default;
280
+ export const morph = window.hyperclayModules['hyper-morph']?.morph ?? window.hyperclayModules['hyper-morph']?.default;
280
281
  export const slugify = window.hyperclayModules['slugify']?.slugify ?? window.hyperclayModules['slugify']?.default;
281
282
  export const copyToClipboard = window.hyperclayModules['copy-to-clipboard']?.copyToClipboard ?? window.hyperclayModules['copy-to-clipboard']?.default;
282
283
  export const query = window.hyperclayModules['query-params']?.query ?? window.hyperclayModules['query-params']?.default;
@@ -286,3 +287,37 @@ export const uploadFile = window.hyperclayModules['file-upload']?.uploadFile ??
286
287
  export const createFile = window.hyperclayModules['file-upload']?.createFile ?? window.hyperclayModules['file-upload']?.default;
287
288
  export const uploadFileBasic = window.hyperclayModules['file-upload']?.uploadFileBasic ?? window.hyperclayModules['file-upload']?.default;
288
289
  export const liveSync = window.hyperclayModules['live-sync']?.liveSync ?? window.hyperclayModules['live-sync']?.default;
290
+
291
+ /**
292
+ * Enable debug logging across all hyperclay modules that support it.
293
+ * @param {boolean} [enabled=true] - Whether to enable or disable debug logging
294
+ */
295
+ export function setDebug(enabled = true) {
296
+ const modules = [
297
+ { name: 'Mutation', obj: window.hyperclayModules['mutation']?.default },
298
+ { name: 'LiveSync', obj: window.hyperclayModules['live-sync']?.liveSync },
299
+ { name: 'OptionVisibility', obj: window.hyperclayModules['option-visibility']?.default },
300
+ ];
301
+
302
+ const enabledModules = [];
303
+ for (const { name, obj } of modules) {
304
+ if (obj && 'debug' in obj) {
305
+ obj.debug = enabled;
306
+ enabledModules.push(name);
307
+ }
308
+ }
309
+
310
+ console.log(`[hyperclay] Debug ${enabled ? 'enabled' : 'disabled'} for:`, enabledModules.join(', ') || 'no modules found');
311
+ }
312
+
313
+ // Export debug to window.hyperclay
314
+ if (!window.__hyperclayNoAutoExport) {
315
+ window.hyperclay = window.hyperclay || {};
316
+ window.hyperclay.debug = setDebug;
317
+ window.h = window.hyperclay;
318
+ }
319
+
320
+ // Auto-enable debug for all modules if ?debug=true was passed
321
+ if (debug) {
322
+ setDebug(true);
323
+ }
@@ -595,7 +595,7 @@ const themodal = (() => {
595
595
  const themodalMain = {
596
596
  isShowing: false,
597
597
  open() {
598
- document.body.insertAdjacentHTML("afterbegin", "<div save-ignore class='micromodal-parent'>" + modalCss + modalHtml + "</div>");
598
+ document.body.insertAdjacentHTML("afterbegin", "<div save-remove class='micromodal-parent'>" + modalCss + modalHtml + "</div>");
599
599
 
600
600
  const modalOverlayElem = document.querySelector(".micromodal__overlay");
601
601
  const modalContentElem = document.querySelector(".micromodal__content");
@@ -1,22 +1,30 @@
1
1
  /**
2
- * Toast Hyperclay - Toast with Hyperclay platform styling
2
+ * Toast Hyperclay - Configure toast() to use Hyperclay platform styling
3
3
  *
4
- * Provides toastHyperclay() function with Hyperclay platform styling.
5
- * Use this alongside toast() if you need both styles in the same project.
4
+ * When this module is loaded, it overrides the default toast styling so that
5
+ * all toast() calls (including from save-toast) use Hyperclay styling.
6
6
  *
7
- * This is a hidden feature not exposed in the UI - used internally by
8
- * the Hyperclay platform for backward compatibility.
7
+ * Also provides toastHyperclay() for backward compatibility.
9
8
  */
10
9
 
11
10
  import {
12
11
  toastCore,
13
12
  injectToastStyles,
13
+ setToastTheme,
14
14
  hyperclayStyles,
15
15
  hyperclayTemplates,
16
16
  hyperclayIcons
17
17
  } from './toast.js';
18
18
 
19
- // Toast function with Hyperclay styling
19
+ // Configure the base toast() to use hyperclay styling
20
+ setToastTheme({
21
+ styles: hyperclayStyles,
22
+ templates: hyperclayTemplates,
23
+ icons: hyperclayIcons,
24
+ theme: 'hyperclay'
25
+ });
26
+
27
+ // Toast function with Hyperclay styling (kept for backward compatibility)
20
28
  function toastHyperclay(message, messageType = "success") {
21
29
  injectToastStyles(hyperclayStyles, 'hyperclay');
22
30
  toastCore(message, messageType, {
package/src/ui/toast.js CHANGED
@@ -185,13 +185,26 @@ export const hyperclayStyles = `
185
185
  // Track which theme styles have been injected
186
186
  const injectedThemes = new Set();
187
187
 
188
+ // Global toast configuration (can be overridden by toast-hyperclay module)
189
+ let toastConfig = {
190
+ styles: modernStyles,
191
+ templates: defaultTemplates,
192
+ icons: defaultIcons,
193
+ theme: 'modern'
194
+ };
195
+
196
+ // Allow other modules (like toast-hyperclay) to override default toast styling
197
+ export function setToastTheme(config) {
198
+ Object.assign(toastConfig, config);
199
+ }
200
+
188
201
  // Helper function to inject styles for a theme (additive, not replacing)
189
202
  export function injectToastStyles(styles, theme) {
190
203
  if (injectedThemes.has(theme)) return;
191
204
 
192
205
  const styleSheet = document.createElement('style');
193
206
  styleSheet.className = `toast-styles-${theme}`;
194
- styleSheet.setAttribute('save-ignore', '');
207
+ styleSheet.setAttribute('save-remove', '');
195
208
  styleSheet.textContent = styles;
196
209
  document.head.appendChild(styleSheet);
197
210
 
@@ -210,7 +223,7 @@ export function toastCore(message, messageType = "success", config = {}) {
210
223
  toastContainer = document.createElement('div');
211
224
  toastContainer.className = 'toast-container';
212
225
  toastContainer.setAttribute('data-toast-theme', theme);
213
- toastContainer.setAttribute('save-ignore', '');
226
+ toastContainer.setAttribute('save-remove', '');
214
227
  document.body.append(toastContainer);
215
228
  }
216
229
 
@@ -242,13 +255,13 @@ export function toastCore(message, messageType = "success", config = {}) {
242
255
  }, 6600);
243
256
  }
244
257
 
245
- // Main toast function - uses modern styles
258
+ // Main toast function - uses configured theme (default: modern, or hyperclay if toast-hyperclay loaded)
246
259
  function toast(message, messageType = "success") {
247
- injectToastStyles(modernStyles, 'modern');
260
+ injectToastStyles(toastConfig.styles, toastConfig.theme);
248
261
  toastCore(message, messageType, {
249
- templates: defaultTemplates,
250
- icons: defaultIcons,
251
- theme: 'modern'
262
+ templates: toastConfig.templates,
263
+ icons: toastConfig.icons,
264
+ theme: toastConfig.theme
252
265
  });
253
266
  }
254
267