hyperclayjs 1.24.3 → 1.24.4

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
@@ -66,7 +66,6 @@ import 'hyperclayjs/presets/standard.js';
66
66
  | save-system | 13.4KB | CMD+S, [trigger-save] button, savestatus attribute |
67
67
  | save-toast | 0.9KB | Toast notifications for save events |
68
68
  | snapshot | 10.8KB | Source of truth for page state - captures DOM snapshots for save and sync |
69
- | tailwind-inject | 1.4KB | Injects tailwind CSS link with cache-bust on save |
70
69
  | unsaved-warning | 1.3KB | Warn before leaving page with unsaved changes |
71
70
 
72
71
  ### Custom Attributes (HTML enhancements)
@@ -79,6 +78,7 @@ import 'hyperclayjs/presets/standard.js';
79
78
  | input-helpers | 3.9KB | [prevent-enter], [autosize] for textareas |
80
79
  | movable | 2.5KB | Free-positioning drag with [movable] and [movable-handle], edit mode only |
81
80
  | onaftersave | 1KB | [onaftersave] attribute - run JS when save status changes |
81
+ | refetch-on-save | 0.9KB | Flash-free refetch of href/src resources on save via [refetch-on-save] attribute |
82
82
  | save-freeze | 2.8KB | [save-freeze] attribute - freeze element innerHTML for saves, live DOM changes freely |
83
83
  | sortable | 3.4KB | Drag-drop sorting with [sortable], lazy-loads ~118KB Sortable.js in edit mode |
84
84
 
@@ -123,7 +123,7 @@ import 'hyperclayjs/presets/standard.js';
123
123
  | Module | Size | Description |
124
124
  |--------|------|-------------|
125
125
  | file-upload | 10.7KB | File upload with progress |
126
- | live-sync | 11.7KB | Real-time DOM sync across browsers |
126
+ | live-sync | 11.6KB | Real-time DOM sync across browsers |
127
127
  | send-message | 1.3KB | Message sending utility |
128
128
 
129
129
  ### Vendor Libraries (Third-party libraries)
@@ -144,7 +144,7 @@ Standard feature set for most use cases
144
144
 
145
145
  **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`
146
146
 
147
- ### Everything (~225.5KB)
147
+ ### Everything (~224.9KB)
148
148
  All available features
149
149
 
150
150
  Includes all available modules across all categories.
@@ -342,16 +342,16 @@ myFeature();
342
342
 
343
343
  ### Before
344
344
  ```html
345
- <script src="/js/old-hyperclay.js"></script>
345
+ <script src="/public/js/old-hyperclay.js"></script>
346
346
  ```
347
347
 
348
348
  ### After
349
349
  ```html
350
350
  <!-- Use preset -->
351
- <script src="/js/hyperclay.js?preset=standard" type="module"></script>
351
+ <script src="/public/js/hyperclay.js?preset=standard" type="module"></script>
352
352
 
353
353
  <!-- Or specific features -->
354
- <script src="/js/hyperclay.js?features=save,edit-mode-helpers,toast" type="module"></script>
354
+ <script src="/public/js/hyperclay.js?features=save,edit-mode-helpers,toast" type="module"></script>
355
355
  ```
356
356
 
357
357
  ## Contributing
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyperclayjs",
3
- "version": "1.24.3",
3
+ "version": "1.24.4",
4
4
  "description": "Modular JavaScript library for building interactive malleable HTML files with Hyperclay",
5
5
  "type": "module",
6
6
  "main": "src/hyperclay.js",
@@ -137,39 +137,28 @@ class LiveSync {
137
137
  }
138
138
 
139
139
  /**
140
- * Auto-detect the current site identifier from the URL
141
- * Returns site ID without .html extension
140
+ * Auto-detect the current site file path from the URL
141
+ * Returns the path including extension (e.g., card-canvas.html)
142
142
  *
143
143
  * Handles:
144
- * - / -> index
145
- * - /about -> about
146
- * - /about.html -> about
147
- * - /about/ -> about/index
148
- * - /pages/contact -> pages/contact
149
- * - /pages/contact/ -> pages/contact/index
144
+ * - / -> index.html
145
+ * - /about.html -> about.html
146
+ * - /about.htmlclay -> about.htmlclay
147
+ * - /about.html/dashboard -> about.html (SPA route stripped)
148
+ * - /blog/app.htmlclay/settings -> blog/app.htmlclay (SPA route stripped)
150
149
  */
151
150
  detectCurrentFile() {
152
151
  let pathname = window.location.pathname;
153
152
 
154
- // Root path
155
153
  if (pathname === '/') {
156
- return 'index';
154
+ return 'index.html';
157
155
  }
158
156
 
159
- // Remove leading slash
160
157
  pathname = pathname.replace(/^\//, '');
161
158
 
162
- // Handle trailing slash -> directory index
163
- if (pathname.endsWith('/')) {
164
- return pathname + 'index';
165
- }
166
-
167
- // Remove .html extension if present
168
- if (pathname.endsWith('.html')) {
169
- return pathname.slice(0, -5);
170
- }
159
+ const htmlMatch = pathname.match(/^(.*?\.html(?:clay)?)/);
160
+ if (htmlMatch) return htmlMatch[1];
171
161
 
172
- // Already a site identifier
173
162
  return pathname;
174
163
  }
175
164
 
@@ -180,7 +169,8 @@ class LiveSync {
180
169
  connect() {
181
170
  if (this.isDestroyed) return;
182
171
 
183
- const url = `/live-sync/stream?file=${encodeURIComponent(this.currentFile)}`;
172
+ const pageUrl = encodeURIComponent(window.location.href);
173
+ const url = `/live-sync/stream?page-url=${pageUrl}`;
184
174
  this.sse = new EventSource(url);
185
175
 
186
176
  this.sse.onopen = () => {
@@ -276,9 +266,8 @@ class LiveSync {
276
266
 
277
267
  fetch('/live-sync/save', {
278
268
  method: 'POST',
279
- headers: { 'Content-Type': 'application/json' },
269
+ headers: { 'Content-Type': 'application/json', 'Page-URL': window.location.href },
280
270
  body: JSON.stringify({
281
- file: this.currentFile,
282
271
  html: html,
283
272
  sender: this.clientId
284
273
  })
@@ -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 = '[editmode\\:contenteditable]';
5
+ export const SELECTOR = '[editmode\\:contenteditable]';
6
6
 
7
7
  export function disableContentEditableBeforeSave () {
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_DISABLED = '[viewmode\\:disabled]';
6
- const SELECTOR_READONLY = '[viewmode\\:readonly]';
5
+ export const SELECTOR_DISABLED = '[viewmode\\:disabled]';
6
+ export const SELECTOR_READONLY = '[viewmode\\:readonly]';
7
7
 
8
8
  export function disableAdminInputsBeforeSave() {
9
9
  beforeSave(docElem => {
@@ -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 = '[editmode\\:onclick]';
5
+ export 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 = '[editmode\\:resource]:is(style, link, script)';
6
- const SELECTOR_INERT = '[editmode\\:resource]:is(style, link, script)[type^="inert/"]';
5
+ export const SELECTOR = '[editmode\\:resource]:is(style, link, script)';
6
+ export const SELECTOR_INERT = '[editmode\\:resource]:is(style, link, script)[type^="inert/"]';
7
7
 
8
8
  export function disableAdminResourcesBeforeSave () {
9
9
  beforeSave(docElem => {
@@ -20,7 +20,7 @@ import { savePageThrottled } from "./savePage.js";
20
20
  */
21
21
  function initSavePageOnChange() {
22
22
  Mutation.onAnyChange({
23
- debounce: 3333,
23
+ debounce: 1500,
24
24
  omitChangeDetails: true
25
25
  }, () => {
26
26
  savePageThrottled();
@@ -36,7 +36,7 @@ function initSaveOnPersistInput() {
36
36
  document.addEventListener('input', (e) => {
37
37
  if (!e.target.closest('[persist]')) return;
38
38
  clearTimeout(inputSaveTimer);
39
- inputSaveTimer = setTimeout(savePageThrottled, 3333);
39
+ inputSaveTimer = setTimeout(savePageThrottled, 1500);
40
40
  }, true);
41
41
  }
42
42
 
@@ -7,7 +7,6 @@
7
7
  * For full save system with state management, use savePage.js instead.
8
8
  */
9
9
 
10
- import cookie from "../utilities/cookie.js";
11
10
  import { isEditMode } from "./isAdminOfCurrentResource.js";
12
11
  import {
13
12
  captureForSave,
@@ -24,7 +23,7 @@ import {
24
23
  // =============================================================================
25
24
 
26
25
  let saveInProgress = false;
27
- const saveEndpoint = `/save/${cookie.get("currentResource")}`;
26
+ const saveEndpoint = '/save';
28
27
 
29
28
  /**
30
29
  * Check if a save is currently in progress.
@@ -119,12 +118,13 @@ export function savePage(callback = () => {}) {
119
118
  const fetchOptions = {
120
119
  method: 'POST',
121
120
  credentials: 'include',
122
- signal: controller.signal
121
+ signal: controller.signal,
122
+ headers: { 'Page-URL': window.location.href }
123
123
  };
124
124
 
125
125
  if (isHyperclayLocal && window.__hyperclaySnapshotHtml) {
126
126
  // Send JSON with both stripped content and full snapshot for platform live sync
127
- fetchOptions.headers = { 'Content-Type': 'application/json' };
127
+ fetchOptions.headers['Content-Type'] = 'application/json';
128
128
  fetchOptions.body = JSON.stringify({
129
129
  content: currentContents,
130
130
  snapshotHtml: window.__hyperclaySnapshotHtml
@@ -213,12 +213,13 @@ export function saveHtml(html, callback = () => {}) {
213
213
  const fetchOptions = {
214
214
  method: 'POST',
215
215
  credentials: 'include',
216
- signal: controller.signal
216
+ signal: controller.signal,
217
+ headers: { 'Page-URL': window.location.href }
217
218
  };
218
219
 
219
220
  if (isHyperclayLocal && window.__hyperclaySnapshotHtml) {
220
221
  // Send JSON with both stripped content and full snapshot for platform live sync
221
- fetchOptions.headers = { 'Content-Type': 'application/json' };
222
+ fetchOptions.headers['Content-Type'] = 'application/json';
222
223
  fetchOptions.body = JSON.stringify({
223
224
  content: html,
224
225
  snapshotHtml: window.__hyperclaySnapshotHtml
@@ -0,0 +1,33 @@
1
+ function swapElement(el) {
2
+ const attr = el.hasAttribute('href') ? 'href' : el.hasAttribute('src') ? 'src' : null;
3
+ if (!attr) return;
4
+
5
+ const oldValue = el.getAttribute(attr);
6
+ const url = new URL(oldValue, location.href);
7
+ url.searchParams.set('v', Date.now());
8
+ const isSameOrigin = url.origin === location.origin;
9
+
10
+ const newEl = document.createElement(el.tagName);
11
+ for (const { name, value } of el.attributes) {
12
+ newEl.setAttribute(name, value);
13
+ }
14
+ newEl.setAttribute(attr, isSameOrigin ? url.pathname + url.search : url.href);
15
+ newEl.setAttribute('save-ignore', '');
16
+
17
+ el.insertAdjacentElement('afterend', newEl);
18
+
19
+ newEl.onload = () => el.remove();
20
+ setTimeout(() => {
21
+ if (el.parentNode) el.remove();
22
+ }, 2000);
23
+ }
24
+
25
+ function init() {
26
+ document.addEventListener('hyperclay:save-saved', () => {
27
+ document.querySelectorAll('[refetch-on-save]').forEach(swapElement);
28
+ });
29
+ }
30
+
31
+ init();
32
+
33
+ export default init;
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.24.3 - Minimal Browser-Native Loader
4
+ * HyperclayJS v1.24.4 - 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.
@@ -68,7 +68,7 @@ const MODULE_PATHS = {
68
68
  "send-message": "./communication/sendMessage.js",
69
69
  "file-upload": "./communication/uploadFile.js",
70
70
  "live-sync": "./communication/live-sync.js",
71
- "tailwind-inject": "./core/tailwindInject.js",
71
+ "refetch-on-save": "./custom-attributes/refetchOnSave.js",
72
72
  "export-to-window": "./core/exportToWindow.js"
73
73
  };
74
74
  const PRESETS = {
@@ -145,6 +145,7 @@ const PRESETS = {
145
145
  "behavior-collector",
146
146
  "send-message",
147
147
  "file-upload",
148
+ "refetch-on-save",
148
149
  "export-to-window",
149
150
  "view-mode-excludes-edit-modules"
150
151
  ]
@@ -193,7 +194,7 @@ const PRESETS = {
193
194
  "send-message",
194
195
  "file-upload",
195
196
  "live-sync",
196
- "tailwind-inject",
197
+ "refetch-on-save",
197
198
  "export-to-window",
198
199
  "view-mode-excludes-edit-modules"
199
200
  ]
@@ -216,7 +217,7 @@ const EDIT_MODE_ONLY = new Set([
216
217
  "hyper-morph",
217
218
  "file-upload",
218
219
  "live-sync",
219
- "tailwind-inject"
220
+ "refetch-on-save"
220
221
  ]);
221
222
 
222
223
  // Parse URL (use import.meta.url for ES modules since document.currentScript is null)
@@ -1,4 +1,4 @@
1
- // e.g. Cookie.get("currentResource")
1
+ // e.g. Cookie.get("isAdminOfCurrentResource")
2
2
  function get (cookieName) {
3
3
  const cookies = document.cookie.split('; ');
4
4
  const cookie = cookies.find(row => row.startsWith(`${cookieName}=`));
@@ -11,7 +11,7 @@ function get (cookieName) {
11
11
  }
12
12
  }
13
13
 
14
- // e.g. Cookie.remove("currentResource")
14
+ // e.g. Cookie.remove("isAdminOfCurrentResource")
15
15
  function remove(name) {
16
16
  // Clear from current path
17
17
  document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/;`
@@ -1,56 +0,0 @@
1
- import { insertStyles } from '../dom-utilities/insertStyleTag.js';
2
- import cookie from '../utilities/cookie.js';
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
-
44
- function init() {
45
- const currentResource = cookie.get('currentResource');
46
- if (!currentResource) return;
47
-
48
- const href = `/tailwindcss/${currentResource}.css`;
49
- insertStyles(href, (link) => {
50
- link.setAttribute('save-ignore', '');
51
- });
52
-
53
- document.addEventListener('hyperclay:save-saved', swapTailwindLink);
54
- }
55
-
56
- init();