hyperclayjs 1.23.0 → 1.23.1

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/README.md CHANGED
@@ -59,7 +59,7 @@ import 'hyperclayjs/presets/standard.js';
59
59
  |--------|------|-------------|
60
60
  | autosave | 1.4KB | Auto-save on DOM changes |
61
61
  | edit-mode | 1.8KB | Toggle edit mode on hyperclay on/off |
62
- | edit-mode-helpers | 8.3KB | Admin-only functionality: [edit-mode-input], [edit-mode-resource], [edit-mode-onclick] |
62
+ | edit-mode-helpers | 6.8KB | Admin-only functionality: [viewmode:disabled], [editmode:resource], [editmode:onclick] |
63
63
  | option-visibility | 7.1KB | Dynamic show/hide based on ancestor state with option:attribute="value" |
64
64
  | persist | 2.4KB | Persist input/select/textarea values to the DOM with [persist] attribute |
65
65
  | save-core | 8.9KB | Basic save function only - hyperclay.savePage() |
@@ -86,8 +86,8 @@ import 'hyperclayjs/presets/standard.js';
86
86
  | Module | Size | Description |
87
87
  |--------|------|-------------|
88
88
  | dialogs | 7.7KB | ask(), consent(), tell(), snippet() dialog functions |
89
- | the-modal | 21.5KB | Full modal window creation system - window.theModal |
90
- | toast | 15.9KB | Success/error message notifications, toast(msg, msgType) |
89
+ | the-modal | 22.4KB | Full modal window creation system - window.theModal |
90
+ | toast | 15.8KB | Success/error message notifications, toast(msg, msgType) |
91
91
 
92
92
  ### Utilities (Core utilities (often auto-included))
93
93
 
@@ -96,7 +96,7 @@ import 'hyperclayjs/presets/standard.js';
96
96
  | cache-bust | 0.6KB | Cache-bust href/src attributes |
97
97
  | cookie | 1.4KB | Cookie management (often auto-included) |
98
98
  | debounce | 0.4KB | Function debouncing |
99
- | mutation | 13.5KB | DOM mutation observation (often auto-included) |
99
+ | mutation | 13.7KB | DOM mutation observation (often auto-included) |
100
100
  | nearest | 3.4KB | Find nearest elements (often auto-included) |
101
101
  | throttle | 0.8KB | Function throttling |
102
102
 
@@ -122,7 +122,7 @@ import 'hyperclayjs/presets/standard.js';
122
122
  | Module | Size | Description |
123
123
  |--------|------|-------------|
124
124
  | file-upload | 10.7KB | File upload with progress |
125
- | live-sync | 11.5KB | Real-time DOM sync across browsers |
125
+ | live-sync | 11.7KB | Real-time DOM sync across browsers |
126
126
  | send-message | 1.3KB | Message sending utility |
127
127
 
128
128
  ### Vendor Libraries (Third-party libraries)
@@ -133,17 +133,17 @@ import 'hyperclayjs/presets/standard.js';
133
133
 
134
134
  ## Presets
135
135
 
136
- ### Minimal (~58.6KB)
136
+ ### Minimal (~57KB)
137
137
  Essential features for basic editing
138
138
 
139
139
  **Modules:** `save-core`, `snapshot`, `save-system`, `edit-mode-helpers`, `toast`, `save-toast`, `export-to-window`, `view-mode-excludes-edit-modules`
140
140
 
141
- ### Standard (~80.8KB)
141
+ ### Standard (~79.2KB)
142
142
  Standard feature set for most use cases
143
143
 
144
144
  **Modules:** `save-core`, `snapshot`, `save-system`, `unsaved-warning`, `edit-mode-helpers`, `persist`, `option-visibility`, `event-attrs`, `dom-helpers`, `toast`, `save-toast`, `export-to-window`, `view-mode-excludes-edit-modules`
145
145
 
146
- ### Everything (~217.9KB)
146
+ ### Everything (~217.6KB)
147
147
  All available features
148
148
 
149
149
  Includes all available modules across all categories.
@@ -316,9 +316,9 @@ tell("Welcome to Hyperclay!");
316
316
 
317
317
  ```html
318
318
  <!-- Only visible/editable in edit mode -->
319
- <div contenteditable edit-mode-contenteditable>Admin can edit this</div>
320
- <input type="text" edit-mode-input>
321
- <script edit-mode-resource>console.log('Admin only');</script>
319
+ <div contenteditable editmode:contenteditable>Admin can edit this</div>
320
+ <input type="text" viewmode:disabled>
321
+ <script editmode:resource>console.log('Admin only');</script>
322
322
  ```
323
323
 
324
324
  ## Module Creation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyperclayjs",
3
- "version": "1.23.0",
3
+ "version": "1.23.1",
4
4
  "description": "Modular JavaScript library for building interactive malleable HTML files with Hyperclay",
5
5
  "type": "module",
6
6
  "main": "src/hyperclay.js",
@@ -321,7 +321,9 @@ class LiveSync {
321
321
 
322
322
  this._log('applyUpdate - morph complete, resuming mutations');
323
323
  Mutation.resume();
324
- this.isPaused = false;
324
+ // Defer past microtask boundary — MutationObserver callbacks and any async
325
+ // morph side-effects fire before this, so isPaused catches stray snapshots
326
+ setTimeout(() => { this.isPaused = false; }, 0);
325
327
  }
326
328
 
327
329
  /**
@@ -2,7 +2,7 @@ import { isEditMode, isOwner } from "./isAdminOfCurrentResource.js";
2
2
  import onDomReady from "../dom-utilities/onDomReady.js";
3
3
  import {beforeSave} from "./savePage.js";
4
4
 
5
- const SELECTOR = '[edit-mode-contenteditable], [editmode\\:contenteditable]';
5
+ const SELECTOR = '[editmode\\:contenteditable]';
6
6
 
7
7
  export function disableContentEditableBeforeSave () {
8
8
  beforeSave(docElem => {
@@ -2,20 +2,13 @@ import { isEditMode, isOwner } from "./isAdminOfCurrentResource.js";
2
2
  import onDomReady from "../dom-utilities/onDomReady.js";
3
3
  import { beforeSave } from "./savePage.js";
4
4
 
5
- const SELECTOR_DISABLED = '[edit-mode-input], [viewmode\\:disabled]';
5
+ const SELECTOR_DISABLED = '[viewmode\\:disabled]';
6
6
  const SELECTOR_READONLY = '[viewmode\\:readonly]';
7
- const SELECTOR_ALL = '[edit-mode-input], [viewmode\\:disabled], [viewmode\\:readonly]';
8
7
 
9
8
  export function disableAdminInputsBeforeSave() {
10
9
  beforeSave(docElem => {
11
10
  docElem.querySelectorAll(SELECTOR_DISABLED).forEach(input => {
12
- if (input.hasAttribute('viewmode:disabled')) {
13
- input.setAttribute('disabled', '');
14
- } else if (supportsReadonly(input)) {
15
- input.setAttribute('readonly', '');
16
- } else {
17
- input.setAttribute('disabled', '');
18
- }
11
+ input.setAttribute('disabled', '');
19
12
  });
20
13
  docElem.querySelectorAll(SELECTOR_READONLY).forEach(input => {
21
14
  input.setAttribute('readonly', '');
@@ -31,16 +24,9 @@ export function enableAdminInputsOnPageLoad() {
31
24
  });
32
25
  }
33
26
 
34
- // Runtime toggle functions
35
27
  export function enableAdminInputs() {
36
28
  document.querySelectorAll(SELECTOR_DISABLED).forEach(input => {
37
- if (input.hasAttribute('viewmode:disabled')) {
38
- input.removeAttribute('disabled');
39
- } else if (supportsReadonly(input)) {
40
- input.removeAttribute('readonly');
41
- } else {
42
- input.removeAttribute('disabled');
43
- }
29
+ input.removeAttribute('disabled');
44
30
  });
45
31
  document.querySelectorAll(SELECTOR_READONLY).forEach(input => {
46
32
  input.removeAttribute('readonly');
@@ -49,43 +35,18 @@ export function enableAdminInputs() {
49
35
 
50
36
  export function disableAdminInputs() {
51
37
  document.querySelectorAll(SELECTOR_DISABLED).forEach(input => {
52
- if (input.hasAttribute('viewmode:disabled')) {
53
- input.setAttribute('disabled', '');
54
- } else if (supportsReadonly(input)) {
55
- input.setAttribute('readonly', '');
56
- } else {
57
- input.setAttribute('disabled', '');
58
- }
38
+ input.setAttribute('disabled', '');
59
39
  });
60
40
  document.querySelectorAll(SELECTOR_READONLY).forEach(input => {
61
41
  input.setAttribute('readonly', '');
62
42
  });
63
43
  }
64
44
 
65
- // Input types that support the readonly attribute
66
- const readonlyTypes = ['text', 'search', 'url', 'tel', 'email', 'password', 'date', 'month', 'week', 'time', 'datetime-local', 'number'];
67
-
68
- function supportsReadonly(element) {
69
- const tagName = element.tagName?.toUpperCase();
70
-
71
- if (tagName === 'TEXTAREA') return true;
72
- if (tagName === 'SELECT' || tagName === 'BUTTON' || tagName === 'FIELDSET') return false;
73
-
74
- if (tagName === 'INPUT') {
75
- const type = element.type || 'text';
76
- return readonlyTypes.includes(type);
77
- }
78
-
79
- return false;
80
- }
81
-
82
- // Auto-initialize
83
45
  export function init() {
84
46
  disableAdminInputsBeforeSave();
85
47
  enableAdminInputsOnPageLoad();
86
48
  }
87
49
 
88
- // Export to window
89
50
  window.hyperclay = window.hyperclay || {};
90
51
  window.hyperclay.enableAdminInputs = enableAdminInputs;
91
52
  window.hyperclay.disableAdminInputs = disableAdminInputs;
@@ -2,7 +2,7 @@ import { isEditMode, isOwner } from "./isAdminOfCurrentResource.js";
2
2
  import onDomReady from "../dom-utilities/onDomReady.js";
3
3
  import {beforeSave} from "./savePage.js";
4
4
 
5
- const SELECTOR = '[edit-mode-onclick], [editmode\\:onclick]';
5
+ const SELECTOR = '[editmode\\:onclick]';
6
6
 
7
7
  export function disableOnClickBeforeSave () {
8
8
  beforeSave(docElem => {
@@ -2,8 +2,8 @@ import { isEditMode, isOwner } from "./isAdminOfCurrentResource.js";
2
2
  import onDomReady from "../dom-utilities/onDomReady.js";
3
3
  import {beforeSave} from "./savePage.js";
4
4
 
5
- const SELECTOR = '[edit-mode-resource]:is(style, link, script), [editmode\\:resource]:is(style, link, script)';
6
- const SELECTOR_INERT = '[edit-mode-resource]:is(style, link, script)[type^="inert/"], [editmode\\:resource]:is(style, link, script)[type^="inert/"]';
5
+ const SELECTOR = '[editmode\\:resource]:is(style, link, script)';
6
+ const SELECTOR_INERT = '[editmode\\:resource]:is(style, link, script)[type^="inert/"]';
7
7
 
8
8
  export function disableAdminResourcesBeforeSave () {
9
9
  beforeSave(docElem => {
package/src/hyperclay.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * DO NOT EDIT THIS FILE DIRECTLY — it is generated from build/hyperclay.template.js
3
3
  *
4
- * HyperclayJS v1.23.0 - Minimal Browser-Native Loader
4
+ * HyperclayJS v1.23.1 - Minimal Browser-Native Loader
5
5
  *
6
6
  * Modules auto-init when imported (no separate init call needed).
7
7
  * Include `export-to-window` feature to export to window.hyperclay.
@@ -608,6 +608,24 @@ const themodal = (() => {
608
608
  const themodalMain = {
609
609
  isShowing: false,
610
610
  open() {
611
+ // Clean up stale modal if DOM was removed externally (e.g. live-sync morph)
612
+ if (this.isShowing && !document.querySelector('.micromodal-parent')) {
613
+ this._cleanupListeners?.();
614
+ html = "";
615
+ yes = "";
616
+ no = "";
617
+ zIndex = "100";
618
+ closeHtml = "";
619
+ enableClickOutsideCloses = true;
620
+ disableScroll = true;
621
+ disableFocus = false;
622
+ onYes = [];
623
+ onNo = [];
624
+ onOpen = [];
625
+ this.isShowing = false;
626
+ document.body.style.overflow = '';
627
+ }
628
+
611
629
  document.body.insertAdjacentHTML("afterbegin", "<div save-remove snapshot-remove class='micromodal-parent'>" + modalCss + modalHtml + "</div>");
612
630
 
613
631
  const modalOverlayElem = document.querySelector(".micromodal__overlay");
@@ -682,6 +700,13 @@ const themodal = (() => {
682
700
  document.addEventListener("click", handleClick);
683
701
  document.addEventListener("submit", handleSubmit);
684
702
 
703
+ // Store cleanup so stale listeners can be removed if DOM is yanked externally
704
+ this._cleanupListeners = () => {
705
+ document.removeEventListener("mousedown", handleMousedown);
706
+ document.removeEventListener("click", handleClick);
707
+ document.removeEventListener("submit", handleSubmit);
708
+ };
709
+
685
710
  function setButtonsVisibility () {
686
711
  modalButtonsElem.classList.toggle("micromodal__hide", !yes && !no);
687
712
  modalYesElem.classList.toggle("micromodal__hide", !yes);
@@ -718,6 +743,7 @@ const themodal = (() => {
718
743
  document.removeEventListener("mousedown", handleMousedown);
719
744
  document.removeEventListener("click", handleClick);
720
745
  document.removeEventListener("submit", handleSubmit);
746
+ this._cleanupListeners = null;
721
747
  }
722
748
  });
723
749
 
package/src/ui/toast.js CHANGED
@@ -235,9 +235,6 @@ export const hyperclayStyles = `
235
235
  }
236
236
  `;
237
237
 
238
- // Track which theme styles have been injected
239
- const injectedThemes = new Set();
240
-
241
238
  // Global toast configuration (can be overridden by toast-hyperclay module)
242
239
  let toastConfig = {
243
240
  styles: modernStyles,
@@ -253,7 +250,7 @@ export function setToastTheme(config) {
253
250
 
254
251
  // Helper function to inject styles for a theme (additive, not replacing)
255
252
  export function injectToastStyles(styles, theme) {
256
- if (injectedThemes.has(theme)) return;
253
+ if (document.querySelector(`style.toast-styles-${theme}`)) return;
257
254
 
258
255
  const styleSheet = document.createElement('style');
259
256
  styleSheet.className = `toast-styles-${theme}`;
@@ -261,8 +258,6 @@ export function injectToastStyles(styles, theme) {
261
258
  styleSheet.setAttribute('snapshot-remove', '');
262
259
  styleSheet.textContent = styles;
263
260
  document.head.appendChild(styleSheet);
264
-
265
- injectedThemes.add(theme);
266
261
  }
267
262
 
268
263
  // Core toast function (used by both toast and toastHyperclay)
@@ -357,8 +352,6 @@ const persistentToastStyles = `
357
352
  }
358
353
  `;
359
354
 
360
- let persistentStylesInjected = false;
361
-
362
355
  // Track active persistent toasts by message
363
356
  const activePersistentToasts = new Map();
364
357
 
@@ -367,14 +360,13 @@ function toastPersistent(message, messageType = "warning") {
367
360
  injectToastStyles(toastConfig.styles, toastConfig.theme);
368
361
 
369
362
  // Inject persistent-specific styles once
370
- if (!persistentStylesInjected) {
363
+ if (!document.querySelector('style.toast-styles-persistent')) {
371
364
  const styleSheet = document.createElement('style');
372
365
  styleSheet.className = 'toast-styles-persistent';
373
366
  styleSheet.setAttribute('save-remove', '');
374
367
  styleSheet.setAttribute('snapshot-remove', '');
375
368
  styleSheet.textContent = persistentToastStyles;
376
369
  document.head.appendChild(styleSheet);
377
- persistentStylesInjected = true;
378
370
  }
379
371
 
380
372
  const templates = toastConfig.templates;
@@ -84,6 +84,11 @@ const Mutation = {
84
84
  * Resume mutation observation after a pause.
85
85
  */
86
86
  resume() {
87
+ // Drain pending mutation records — observer stays connected during pause,
88
+ // so morph mutations are recorded and would fire async after _paused=false
89
+ if (this._observer) {
90
+ this._observer.takeRecords();
91
+ }
87
92
  this._paused = false;
88
93
  this._log('Resumed');
89
94
  },