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
@@ -0,0 +1,203 @@
1
+ /**
2
+ * snapshot.js — The source of truth for page state
3
+ *
4
+ * THE SAVE/SYNC PIPELINE:
5
+ *
6
+ * ┌─────────────────────────────────────────────────────────┐
7
+ * │ 1. CLONE document.documentElement.cloneNode() │
8
+ * └─────────────────────────────────────────────────────────┘
9
+ * │
10
+ * ▼
11
+ * ┌─────────────────────────────────────────────────────────┐
12
+ * │ 2. SNAPSHOT HOOKS onSnapshot callbacks │
13
+ * │ (form value sync) │
14
+ * │ │
15
+ * │ ✓ Used by: SAVE and LIVE-SYNC │
16
+ * └─────────────────────────────────────────────────────────┘
17
+ * │
18
+ * ┌───────────────┴───────────────┐
19
+ * ▼ ▼
20
+ * ┌─────────────────────────┐ ┌─────────────────────────┐
21
+ * │ 3a. PREPARE HOOKS │ │ 3b. DONE │
22
+ * │ onPrepareForSave │ │ (live-sync stops here) │
23
+ * │ [onbeforesave] │ │ │
24
+ * │ [save-ignore] │ │ → emits snapshot-ready │
25
+ * │ │ └─────────────────────────┘
26
+ * │ ✓ Used by: SAVE only │
27
+ * └─────────────────────────┘
28
+ * │
29
+ * ▼
30
+ * ┌─────────────────────────┐
31
+ * │ 4. SERIALIZE │
32
+ * │ "<!DOCTYPE html>" │
33
+ * │ + outerHTML │
34
+ * │ │
35
+ * │ → sent to server │
36
+ * └─────────────────────────┘
37
+ */
38
+
39
+ // =============================================================================
40
+ // HOOK REGISTRIES
41
+ // =============================================================================
42
+
43
+ const snapshotHooks = []; // Phase 2: Always run (form sync)
44
+ const prepareForSaveHooks = []; // Phase 3a: Save only (strip admin)
45
+
46
+ /**
47
+ * Register a hook that runs on EVERY snapshot (save AND sync).
48
+ * Use for: syncing form values to the clone.
49
+ *
50
+ * @param {Function} callback - Receives the cloned document element
51
+ */
52
+ export function onSnapshot(callback) {
53
+ snapshotHooks.push(callback);
54
+ }
55
+
56
+ /**
57
+ * Register a hook that runs ONLY when preparing for save.
58
+ * Use for: stripping admin elements, cleanup.
59
+ *
60
+ * @param {Function} callback - Receives the cloned document element
61
+ */
62
+ export function onPrepareForSave(callback) {
63
+ prepareForSaveHooks.push(callback);
64
+ }
65
+
66
+ // Backwards compat alias
67
+ export const beforeSave = onPrepareForSave;
68
+
69
+ // =============================================================================
70
+ // CAPTURE FUNCTIONS
71
+ // =============================================================================
72
+
73
+ /**
74
+ * PHASE 1-2: Clone the DOM and run snapshot hooks.
75
+ *
76
+ * This is the "canonical" state — form values synced, nothing stripped.
77
+ * Used as the base for both saving and syncing.
78
+ *
79
+ * @returns {HTMLElement} Cloned document element with snapshot hooks applied
80
+ */
81
+ export function captureSnapshot() {
82
+ const clone = document.documentElement.cloneNode(true);
83
+
84
+ for (const hook of snapshotHooks) {
85
+ hook(clone);
86
+ }
87
+
88
+ return clone;
89
+ }
90
+
91
+ /**
92
+ * Prepare an already-captured snapshot for saving.
93
+ * Mutates the clone — only call once per snapshot.
94
+ *
95
+ * @param {HTMLElement} clone - A snapshot from captureSnapshot()
96
+ * @returns {string} Full HTML string ready for server
97
+ */
98
+ function prepareCloneForSave(clone) {
99
+ // Run inline [onbeforesave] handlers
100
+ for (const el of clone.querySelectorAll('[onbeforesave]')) {
101
+ new Function(el.getAttribute('onbeforesave')).call(el);
102
+ }
103
+
104
+ // Remove elements that shouldn't be saved
105
+ for (const el of clone.querySelectorAll('[save-ignore]')) {
106
+ el.remove();
107
+ }
108
+
109
+ // Run registered prepare hooks
110
+ for (const hook of prepareForSaveHooks) {
111
+ hook(clone);
112
+ }
113
+
114
+ return "<!DOCTYPE html>" + clone.outerHTML;
115
+ }
116
+
117
+ /**
118
+ * PHASE 1-4: Full pipeline for saving to server.
119
+ *
120
+ * Captures snapshot, emits for live-sync, then prepares for save.
121
+ * This is the main entry point for the save process.
122
+ *
123
+ * @param {Object} options
124
+ * @param {boolean} options.emitForSync - Whether to emit snapshot-ready event (default: true)
125
+ * @returns {string} Full HTML string ready for server
126
+ */
127
+ export function captureForSave({ emitForSync = true } = {}) {
128
+ const clone = captureSnapshot();
129
+
130
+ // Emit for live-sync before stripping admin elements
131
+ if (emitForSync) {
132
+ const bodyForSync = clone.querySelector('body').innerHTML;
133
+ document.dispatchEvent(new CustomEvent('hyperclay:snapshot-ready', {
134
+ detail: { body: bodyForSync }
135
+ }));
136
+ }
137
+
138
+ return prepareCloneForSave(clone);
139
+ }
140
+
141
+ /**
142
+ * PHASE 1-2 (body only): For live-sync between admin users.
143
+ *
144
+ * Includes admin elements — no stripping.
145
+ * Note: Prefer listening to 'hyperclay:snapshot-ready' event instead,
146
+ * which reuses the save's clone.
147
+ *
148
+ * @returns {string} Body innerHTML with form values synced
149
+ */
150
+ export function captureBodyForSync() {
151
+ const clone = captureSnapshot();
152
+ return clone.querySelector('body').innerHTML;
153
+ }
154
+
155
+ /**
156
+ * Get page contents for change detection.
157
+ * Does NOT emit snapshot-ready event (safe for comparison).
158
+ *
159
+ * For CodeMirror pages, returns editor content directly.
160
+ * For normal pages, returns full HTML via snapshot pipeline.
161
+ */
162
+ export function getPageContents() {
163
+ if (isCodeMirrorPage()) {
164
+ return getCodeMirrorContents();
165
+ }
166
+ return captureForSave({ emitForSync: false });
167
+ }
168
+
169
+ // =============================================================================
170
+ // CODEMIRROR SUPPORT
171
+ // =============================================================================
172
+
173
+ /**
174
+ * Check if this is a CodeMirror editor page.
175
+ * CodeMirror pages bypass the normal snapshot pipeline.
176
+ */
177
+ export function isCodeMirrorPage() {
178
+ return !!document.querySelector('.CodeMirror')?.CodeMirror;
179
+ }
180
+
181
+ /**
182
+ * Get CodeMirror editor contents.
183
+ * @returns {string} Editor contents
184
+ */
185
+ export function getCodeMirrorContents() {
186
+ return document.querySelector('.CodeMirror').CodeMirror.getValue();
187
+ }
188
+
189
+ // =============================================================================
190
+ // WINDOW EXPORTS
191
+ // =============================================================================
192
+
193
+ if (!window.__hyperclayNoAutoExport) {
194
+ window.hyperclay = window.hyperclay || {};
195
+ window.hyperclay.captureSnapshot = captureSnapshot;
196
+ window.hyperclay.captureForSave = captureForSave;
197
+ window.hyperclay.captureBodyForSync = captureBodyForSync;
198
+ window.hyperclay.onSnapshot = onSnapshot;
199
+ window.hyperclay.onPrepareForSave = onPrepareForSave;
200
+ window.hyperclay.beforeSave = beforeSave;
201
+ window.hyperclay.getPageContents = getPageContents;
202
+ window.h = window.hyperclay;
203
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Unsaved Warning Module
3
+ *
4
+ * Warns users before leaving the page if there are unsaved changes.
5
+ * Self-contained: compares current page content to last saved content on beforeunload.
6
+ *
7
+ * Works independently of autosave - no mutation observer needed during editing,
8
+ * just a single comparison when the user tries to leave.
9
+ *
10
+ * Requires the 'save-system' module (automatically included as dependency).
11
+ */
12
+
13
+ import { isOwner, isEditMode } from "./isAdminOfCurrentResource.js";
14
+ import { getPageContents, getLastSavedContents } from "./savePage.js";
15
+
16
+ window.addEventListener('beforeunload', (event) => {
17
+ if (!isOwner || !isEditMode) return;
18
+
19
+ const currentContents = getPageContents();
20
+ const lastSaved = getLastSavedContents();
21
+
22
+ if (currentContents !== lastSaved) {
23
+ event.preventDefault();
24
+ event.returnValue = '';
25
+ }
26
+ });
@@ -38,16 +38,9 @@ function submitAjax(elem) {
38
38
  }
39
39
  method = (method || 'POST').toUpperCase();
40
40
 
41
- // Get data - for buttons, only use form data if button is inside a form
42
- let data = {};
43
- if (isButton && parentForm) {
44
- // Button inside form: use form data
45
- data = getDataFromForm(parentForm);
46
- } else if (!isButton) {
47
- // It's a form element itself
48
- data = getDataFromForm(elem);
49
- }
50
- // For standalone buttons with no form, data remains empty object
41
+ // Get data - from parent form if button is inside one, otherwise from element itself
42
+ const dataSource = (isButton && parentForm) ? parentForm : elem;
43
+ const data = getDataFromForm(dataSource);
51
44
 
52
45
  fetch(url, {
53
46
  method: method,
@@ -24,22 +24,35 @@ function init () {
24
24
  }
25
25
  });
26
26
 
27
- // elem.val.project returns the value of the nearest "project" attribute
28
- // elem.val.project = "hello world" sets the value of the nearest "project" attribute
27
+ // elem.val.project returns the value of the nearest element with "project" attribute
28
+ // elem.val.project = "hello world" sets the value of the nearest element with "project" attribute
29
+ // For form elements (input/select/textarea), uses the value property; otherwise uses the attribute
29
30
  Object.defineProperty(HTMLElement.prototype, 'val', {
30
31
  configurable: true,
31
32
  get: function() {
32
33
  let element = this;
33
34
 
35
+ const isFormElement = (elem) =>
36
+ elem.tagName === 'INPUT' || elem.tagName === 'SELECT' || elem.tagName === 'TEXTAREA';
37
+
34
38
  const handler = {
35
39
  get(target, prop) {
36
- return nearest(element, `[${prop}], .${prop}`, elem => elem.getAttribute(prop));
40
+ return nearest(element, `[${prop}], .${prop}`, elem => {
41
+ if (isFormElement(elem)) {
42
+ return elem.value;
43
+ }
44
+ return elem.getAttribute(prop);
45
+ });
37
46
  },
38
47
  set(target, prop, value) {
39
48
  const foundElem = nearest(element, `[${prop}], .${prop}`);
40
49
 
41
50
  if (foundElem) {
42
- foundElem.setAttribute(prop, value);
51
+ if (isFormElement(foundElem)) {
52
+ foundElem.value = value;
53
+ } else {
54
+ foundElem.setAttribute(prop, value);
55
+ }
43
56
  }
44
57
 
45
58
  return true;
@@ -1,12 +1,14 @@
1
1
  // Events module - combines all event attribute handlers
2
2
  import { init as initOnclickaway } from './onclickaway.js';
3
3
  import { init as initOnclone } from './onclone.js';
4
+ import { init as initOnmutation } from './onmutation.js';
4
5
  import { init as initOnpagemutation } from './onpagemutation.js';
5
6
  import { init as initOnrender } from './onrender.js';
6
7
 
7
8
  function init() {
8
9
  initOnclickaway();
9
10
  initOnclone();
11
+ initOnmutation();
10
12
  initOnpagemutation();
11
13
  initOnrender();
12
14
  }
@@ -1,16 +1,16 @@
1
1
  /**
2
2
  * [onaftersave] Custom Attribute
3
3
  *
4
- * Runs inline JavaScript when save status changes.
5
- * Pairs with the existing [onbeforesave] attribute.
4
+ * Runs inline JavaScript after a successful save.
5
+ * Only fires on 'hyperclay:save-saved' events (not on error/offline).
6
6
  *
7
7
  * Usage:
8
8
  * <span onaftersave="this.innerText = event.detail.msg"></span>
9
- * <div onaftersave="console.log('Status:', event.detail.status)"></div>
9
+ * <link href="styles.css" onaftersave="cacheBust(this)">
10
10
  *
11
11
  * The event.detail object contains:
12
- * - status: 'saving' | 'saved' | 'offline' | 'error'
13
- * - msg: string (e.g., 'Saved' or error message)
12
+ * - status: 'saved'
13
+ * - msg: string (e.g., 'Saved')
14
14
  * - timestamp: number (Date.now())
15
15
  */
16
16
 
@@ -30,10 +30,7 @@ function broadcast(e) {
30
30
  }
31
31
 
32
32
  function init() {
33
- document.addEventListener('hyperclay:save-saving', broadcast);
34
33
  document.addEventListener('hyperclay:save-saved', broadcast);
35
- document.addEventListener('hyperclay:save-offline', broadcast);
36
- document.addEventListener('hyperclay:save-error', broadcast);
37
34
  }
38
35
 
39
36
  init();
@@ -0,0 +1,90 @@
1
+ /*
2
+ [onmutation] - Trigger code when this element or its children change
3
+
4
+ Usage:
5
+ <div onmutation="console.log('My content changed')">
6
+ <span contenteditable>Edit me</span>
7
+ </div>
8
+
9
+ Unlike [onglobalmutation]/[onpagemutation] which fires on ANY DOM change,
10
+ [onmutation] only fires when the element itself or its descendants mutate.
11
+ */
12
+ import Mutation from "../utilities/mutation.js";
13
+
14
+ const observers = new WeakMap();
15
+
16
+ function setupMutationObserver(element) {
17
+ if (observers.has(element)) return;
18
+
19
+ const executeMutation = async () => {
20
+ try {
21
+ const code = element.getAttribute('onmutation');
22
+ if (!code) return;
23
+ const asyncFn = new Function(`return (async function() { ${code} })`)();
24
+ await asyncFn.call(element);
25
+ } catch (error) {
26
+ console.error('Error in onmutation execution:', error);
27
+ }
28
+ };
29
+
30
+ const observer = new MutationObserver(() => {
31
+ executeMutation();
32
+ });
33
+
34
+ observer.observe(element, {
35
+ childList: true,
36
+ subtree: true,
37
+ characterData: true,
38
+ attributes: true
39
+ });
40
+
41
+ observers.set(element, observer);
42
+ }
43
+
44
+ function teardownMutationObserver(element) {
45
+ const observer = observers.get(element);
46
+ if (observer) {
47
+ observer.disconnect();
48
+ observers.delete(element);
49
+ }
50
+ }
51
+
52
+ function init() {
53
+ // Set up existing elements
54
+ document.querySelectorAll('[onmutation]').forEach(setupMutationObserver);
55
+
56
+ // Watch for dynamically added elements with onmutation
57
+ Mutation.onAddElement({
58
+ selectorFilter: '[onmutation]',
59
+ debounce: 200
60
+ }, (changes) => {
61
+ changes.forEach(({ element }) => {
62
+ setupMutationObserver(element);
63
+ });
64
+ });
65
+
66
+ // Clean up when elements are removed
67
+ Mutation.onRemoveElement({
68
+ selectorFilter: '[onmutation]',
69
+ debounce: 200
70
+ }, (changes) => {
71
+ changes.forEach(({ element }) => {
72
+ teardownMutationObserver(element);
73
+ });
74
+ });
75
+
76
+ // Watch for attribute removal
77
+ Mutation.onAttribute({
78
+ selectorFilter: '[onmutation]',
79
+ debounce: 200
80
+ }, (changes) => {
81
+ changes.forEach(({ element, attribute, newValue }) => {
82
+ if (attribute === 'onmutation' && newValue === null) {
83
+ teardownMutationObserver(element);
84
+ }
85
+ });
86
+ });
87
+ }
88
+
89
+ export { init };
90
+ export default init;
@@ -0,0 +1,32 @@
1
+ /*
2
+ [onpagemutation] / [onglobalmutation] - Trigger code when ANY element on the page changes
3
+
4
+ Usage:
5
+ <span onglobalmutation="this.textContent = All('li').length">0</span>
6
+ <span onpagemutation="this.textContent = All('li').length">0</span>
7
+
8
+ Both attributes are equivalent - onglobalmutation is the preferred name for clarity.
9
+ */
10
+ import Mutation from "../utilities/mutation.js";
11
+
12
+ function init() {
13
+ const executeGlobalMutation = async element => {
14
+ try {
15
+ // Support both onglobalmutation and onpagemutation (legacy)
16
+ const code = element.getAttribute('onglobalmutation') || element.getAttribute('onpagemutation');
17
+ const asyncFn = new Function(`return (async function() { ${code} })`)();
18
+ await asyncFn.call(element);
19
+ } catch (error) {
20
+ console.error('Error in onglobalmutation/onpagemutation execution:', error);
21
+ }
22
+ };
23
+
24
+ Mutation.onAnyChange({
25
+ debounce: 200,
26
+ omitChangeDetails: true
27
+ }, () => {
28
+ document.querySelectorAll('[onglobalmutation], [onpagemutation]').forEach(executeGlobalMutation);
29
+ });
30
+ }
31
+ export { init };
32
+ export default init;
@@ -5,6 +5,8 @@
5
5
  How to use:
6
6
  - add `sortable` attribute to an element to make children sortable
7
7
  - e.g. <div sortable></div>
8
+ - add `onsorting` attribute to execute code during drag
9
+ - e.g. <ul sortable onsorting="console.log('Dragging!')"></ul>
8
10
  - add `onsorted` attribute to execute code when items are sorted
9
11
  - e.g. <ul sortable onsorted="console.log('Items reordered!')"></ul>
10
12
 
@@ -39,7 +41,20 @@ function makeSortable(sortableElem, Sortable) {
39
41
  options.handle = '[sortable-handle]';
40
42
  }
41
43
 
42
- // Add onsorted callback if attribute exists
44
+ // Add onsorting callback if attribute exists (fires during drag)
45
+ const onsortingCode = sortableElem.getAttribute('onsorting');
46
+ if (onsortingCode) {
47
+ options.onMove = function(evt) {
48
+ try {
49
+ const asyncFn = new Function(`return (async function(evt) { ${onsortingCode} })`)();
50
+ asyncFn.call(sortableElem, evt);
51
+ } catch (error) {
52
+ console.error('Error in onsorting execution:', error);
53
+ }
54
+ };
55
+ }
56
+
57
+ // Add onsorted callback if attribute exists (fires after drop)
43
58
  const onsortedCode = sortableElem.getAttribute('onsorted');
44
59
  if (onsortedCode) {
45
60
  options.onEnd = function(evt) {
@@ -336,6 +336,28 @@ const defaultPlugins = {
336
336
  });
337
337
 
338
338
  return this;
339
+ },
340
+
341
+ pluck(attr) {
342
+ return this.map(el => el.getAttribute(attr));
343
+ },
344
+
345
+ unique() {
346
+ return [...new Set(this)];
347
+ },
348
+
349
+ sortBy(fn) {
350
+ if (typeof fn === 'string') {
351
+ const attr = fn;
352
+ fn = el => el.getAttribute(attr);
353
+ }
354
+ return [...this].sort((a, b) => {
355
+ const aVal = fn(a);
356
+ const bVal = fn(b);
357
+ if (aVal < bVal) return -1;
358
+ if (aVal > bVal) return 1;
359
+ return 0;
360
+ });
339
361
  }
340
362
  }
341
363
  };
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Insert styles into the document (inline CSS or external stylesheet).
3
+ *
4
+ * With a persistent DOM (i.e. hyperclay), we need a way to update styles.
5
+ * This function always inserts the new styles first, then removes any
6
+ * duplicates. This ensures:
7
+ * - No flickering: new styles are applied before old ones are removed
8
+ * - Always upgrades: we default to the new styles/approach
9
+ */
10
+ function insertStyles(nameOrHref, css) {
11
+ if (css !== undefined) {
12
+ // Inline style: insertStyles('my-styles', '.foo { ... }')
13
+ const name = nameOrHref;
14
+ const oldStyles = document.querySelectorAll(`style[data-name="${name}"]`);
15
+
16
+ const style = document.createElement('style');
17
+ style.dataset.name = name;
18
+ style.textContent = css;
19
+ document.head.appendChild(style);
20
+
21
+ oldStyles.forEach(el => el.remove());
22
+ return style;
23
+ }
24
+
25
+ // External stylesheet: insertStyles('/path/to/file.css')
26
+ const href = nameOrHref;
27
+
28
+ let identifier;
29
+ try {
30
+ const url = new URL(href, window.location.href);
31
+ identifier = url.pathname.split('/').pop();
32
+ } catch (e) {
33
+ identifier = href;
34
+ }
35
+
36
+ const oldLinks = document.querySelectorAll(
37
+ `link[href="${href}"], link[href*="${identifier}"]`
38
+ );
39
+
40
+ const link = document.createElement('link');
41
+ link.rel = 'stylesheet';
42
+ link.href = href;
43
+ document.head.appendChild(link);
44
+
45
+ oldLinks.forEach(el => el.remove());
46
+ return link;
47
+ }
48
+
49
+ // Auto-export to window unless suppressed by loader
50
+ if (!window.__hyperclayNoAutoExport) {
51
+ window.insertStyles = insertStyles;
52
+ window.insertStyleTag = insertStyles; // backwards-compat alias
53
+ window.hyperclay = window.hyperclay || {};
54
+ window.hyperclay.insertStyles = insertStyles;
55
+ window.hyperclay.insertStyleTag = insertStyles; // backwards-compat alias
56
+ window.h = window.hyperclay;
57
+ }
58
+
59
+ export { insertStyles };
60
+ export { insertStyles as insertStyleTag }; // backwards-compat alias
61
+ export default insertStyles;