hyperclayjs 1.9.0 → 1.10.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/README.md CHANGED
@@ -63,7 +63,7 @@ import 'hyperclayjs/presets/standard.js';
63
63
  | option-visibility | 5.3KB | 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 | 6.8KB | Basic save function only - hyperclay.savePage() |
66
- | save-system | 7.1KB | Manual save: keyboard shortcut (CMD+S), save button, change tracking |
66
+ | save-system | 7.1KB | CMD+S, [trigger-save] button, savestatus attribute |
67
67
  | save-toast | 0.9KB | Toast notifications for save events |
68
68
  | snapshot | 7.5KB | Source of truth for page state - captures DOM snapshots for save and sync |
69
69
  | unsaved-warning | 0.8KB | Warn before leaving page with unsaved changes |
@@ -120,7 +120,7 @@ import 'hyperclayjs/presets/standard.js';
120
120
  | Module | Size | Description |
121
121
  |--------|------|-------------|
122
122
  | file-upload | 10.7KB | File upload with progress |
123
- | live-sync | 12KB | Real-time DOM sync across browsers and with file system |
123
+ | live-sync | 12.7KB | Real-time DOM sync across browsers |
124
124
  | send-message | 1.3KB | Message sending utility |
125
125
 
126
126
  ### Vendor Libraries (Third-party libraries)
@@ -141,7 +141,7 @@ Standard feature set for most use cases
141
141
 
142
142
  **Modules:** `save-core`, `save-system`, `edit-mode-helpers`, `persist`, `option-visibility`, `event-attrs`, `dom-helpers`, `toast`, `save-toast`, `export-to-window`
143
143
 
144
- ### Everything (~176.5KB)
144
+ ### Everything (~177.2KB)
145
145
  All available features
146
146
 
147
147
  Includes all available modules across all categories.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyperclayjs",
3
- "version": "1.9.0",
3
+ "version": "1.10.0",
4
4
  "description": "Modular JavaScript library for building interactive HTML applications with Hyperclay",
5
5
  "type": "module",
6
6
  "main": "src/hyperclay.js",
@@ -228,26 +228,34 @@ class LiveSync {
228
228
 
229
229
  /**
230
230
  * Listen for snapshot-ready events from the save system.
231
- * This replaces DOM observation we sync when save happens.
231
+ * Receives the full cloned documentElement and extracts head/body.
232
232
  */
233
233
  listenForSnapshots() {
234
- this._snapshotHandler = (event) => {
234
+ this._snapshotHandler = async (event) => {
235
235
  if (this.isPaused) return;
236
236
 
237
- const { body } = event.detail;
238
- if (!body) return;
237
+ const { documentElement } = event.detail;
238
+ if (!documentElement) return;
239
239
 
240
- this.sendBody(body);
240
+ // Extract head and body directly from cloned element
241
+ const head = documentElement.querySelector('head')?.innerHTML || '';
242
+ const body = documentElement.querySelector('body')?.innerHTML || '';
243
+
244
+ // Compute headHash using SHA-256 (async)
245
+ const headHash = await this.computeHeadHash(head);
246
+
247
+ // Send update even if body is empty (allows clearing content)
248
+ this.sendUpdate(body, headHash);
241
249
  };
242
250
 
243
251
  document.addEventListener('hyperclay:snapshot-ready', this._snapshotHandler);
244
252
  }
245
253
 
246
254
  /**
247
- * Send body HTML to the server (debounced)
255
+ * Send body and headHash to the server (debounced)
248
256
  * Only updates lastBodyHtml after successful save
249
257
  */
250
- sendBody(body) {
258
+ sendUpdate(body, headHash) {
251
259
  clearTimeout(this.debounceTimer);
252
260
 
253
261
  this.debounceTimer = setTimeout(() => {
@@ -256,9 +264,8 @@ class LiveSync {
256
264
 
257
265
  console.log('[LiveSync] Sending update');
258
266
 
259
- // Compute head hash to send to server (for hosted mode)
260
- const headHash = this.computeHeadHash();
261
- this.lastHeadHash = headHash; // Track local head changes
267
+ // Track local head changes
268
+ this.lastHeadHash = headHash;
262
269
 
263
270
  fetch('/live-sync/save', {
264
271
  method: 'POST',
@@ -286,21 +293,30 @@ class LiveSync {
286
293
  }
287
294
 
288
295
  /**
289
- * Compute MD5-like hash of head content (first 8 hex chars)
290
- * Uses a simple string hash since we don't have crypto in browser
296
+ * Compute SHA-256 hash of head content (first 16 hex chars)
297
+ * Uses SubtleCrypto API (async)
298
+ * @param {string} head - Head innerHTML
299
+ * @returns {Promise<string|null>} 16-char hex hash or null if unavailable
291
300
  */
292
- computeHeadHash() {
293
- const head = document.head?.innerHTML;
301
+ async computeHeadHash(head) {
294
302
  if (!head) return null;
295
303
 
296
- // Simple hash function (djb2)
297
- let hash = 5381;
298
- for (let i = 0; i < head.length; i++) {
299
- hash = ((hash << 5) + hash) + head.charCodeAt(i);
300
- hash = hash & hash; // Convert to 32bit integer
304
+ // SubtleCrypto requires secure context (HTTPS or localhost)
305
+ if (!crypto?.subtle?.digest) {
306
+ console.warn('[LiveSync] SHA-256 unavailable (non-secure context), skipping headHash');
307
+ return null;
308
+ }
309
+
310
+ try {
311
+ const encoder = new TextEncoder();
312
+ const data = encoder.encode(head);
313
+ const hashBuffer = await crypto.subtle.digest('SHA-256', data);
314
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
315
+ return hashArray.map(b => b.toString(16).padStart(2, '0')).join('').slice(0, 16);
316
+ } catch (e) {
317
+ console.warn('[LiveSync] SHA-256 hash failed, skipping headHash:', e);
318
+ return null;
301
319
  }
302
- // Convert to hex and take first 8 chars
303
- return Math.abs(hash).toString(16).padStart(8, '0').slice(0, 8);
304
320
  }
305
321
 
306
322
  /**
@@ -128,10 +128,10 @@ export function captureForSave({ emitForSync = true } = {}) {
128
128
  const clone = captureSnapshot();
129
129
 
130
130
  // Emit for live-sync before stripping admin elements
131
+ // Sends full cloned documentElement so live-sync can extract head and body
131
132
  if (emitForSync) {
132
- const bodyForSync = clone.querySelector('body').innerHTML;
133
133
  document.dispatchEvent(new CustomEvent('hyperclay:snapshot-ready', {
134
- detail: { body: bodyForSync }
134
+ detail: { documentElement: clone }
135
135
  }));
136
136
  }
137
137
 
package/src/hyperclay.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * HyperclayJS v1.9.0 - Minimal Browser-Native Loader
2
+ * HyperclayJS v1.10.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.
@@ -178,7 +178,7 @@
178
178
  "files": [
179
179
  "core/savePage.js"
180
180
  ],
181
- "description": "Manual save: keyboard shortcut (CMD+S), save button, change tracking",
181
+ "description": "CMD+S, [trigger-save] button, savestatus attribute",
182
182
  "exports": {
183
183
  "beforeSave": [
184
184
  "hyperclay"
@@ -724,11 +724,11 @@
724
724
  "live-sync": {
725
725
  "name": "live-sync",
726
726
  "category": "communication",
727
- "size": 12,
727
+ "size": 12.7,
728
728
  "files": [
729
729
  "communication/live-sync.js"
730
730
  ],
731
- "description": "Real-time DOM sync across browsers and with file system",
731
+ "description": "Real-time DOM sync across browsers",
732
732
  "exports": {
733
733
  "liveSync": [
734
734
  "hyperclay"