@panphora/clayjs 1.11.1 → 1.11.2

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.
@@ -70,6 +70,7 @@ import { EXTENSION_ATTR_PATTERN } from './extension-noise.js';
70
70
  import { resolveRegionPolicy, isInert, strictestPolicy, skipForPolicy } from './region-policy.js';
71
71
  import { isPolicyAttribute } from './region-capabilities.js';
72
72
  import { ROOT_LIBRARY_ATTRS, ROUTE_ROOT_PREFIX, ROUTE_ACTIVE_ATTR } from './root-attrs.js';
73
+ import { isEphemeralAttr } from './ephemeral.js';
73
74
 
74
75
  import { isUserDrivenNow, markUserDriven } from './user-gesture.js';
75
76
 
@@ -83,13 +84,15 @@ const dummyElem = document.createElement("div");
83
84
  // instead of body. The raw lane is deliberately unfiltered ("raw means raw"), so
84
85
  // without this every save would push a savestatus flip into undo's record stream
85
86
  // and Cmd+Z after a save would undo that instead of the user's edit.
86
- // Router state (route-* on the root, route-active anywhere) is dropped here for the same reason.
87
+ // Router state (route-* on the root, route-active anywhere) and ephemeral state
88
+ // (lib/ephemeral.js) are dropped here for the same reason.
87
89
  const ROOT_LIBRARY_ATTR_SET = new Set(ROOT_LIBRARY_ATTRS);
88
90
 
89
91
  function isLibraryRootAttr(record) {
90
92
  if (record.type !== 'attributes') return false;
91
93
  const name = record.attributeName;
92
94
  if (name === ROUTE_ACTIVE_ATTR) return true;
95
+ if (isEphemeralAttr(name, record.target)) return true;
93
96
  return record.target === document.documentElement
94
97
  && (ROOT_LIBRARY_ATTR_SET.has(name) || name.startsWith(ROUTE_ROOT_PREFIX));
95
98
  }
@@ -8,6 +8,8 @@
8
8
  * peer's copy of them must never be applied.
9
9
  */
10
10
 
11
+ import { isEphemeralAttr, isMarkedAttr } from "./ephemeral.js";
12
+
11
13
  // The save token, in the order a reader tries the two spellings. Spec §9 bounds it to
12
14
  // per-file and per-tab and makes the host strip it before writing, so it never reaches
13
15
  // disk. But §9 bounds only the save path, and §10 fans a snapshot out to other editors'
@@ -128,15 +130,36 @@ export const ROUTE_ACTIVE_ATTR = "route-active";
128
130
 
129
131
  /**
130
132
  * True when `name` on `el` belongs to this tab rather than to the document:
131
- * every tab-local root attribute, route-* on a root, and route-active anywhere.
133
+ * every tab-local root attribute, route-* on a root, route-active anywhere, and
134
+ * ephemeral state (lib/ephemeral.js) anywhere.
132
135
  * Works on the live root and on a snapshot clone's or parsed frame's root.
133
136
  */
134
137
  export function isTabLocalAttr(name, el) {
138
+ if (isEphemeralAttr(name, el)) return true;
135
139
  if (name === ROUTE_ACTIVE_ATTR) return true;
136
140
  if (!el || el.localName !== "html" || el.parentElement) return false;
137
141
  return TAB_LOCAL_ROOT_ATTRS.has(name) || name.startsWith(ROUTE_ROOT_PREFIX);
138
142
  }
139
143
 
144
+ /**
145
+ * hyper-morph's ignoreAttribute for tab-local state. While a merge is applied,
146
+ * `merged` is the element the live one is becoming, so a frame that adds or
147
+ * removes an ephemeral:NAME marker together with NAME is judged by the marker
148
+ * as it ends up, not by attribute order.
149
+ */
150
+ export function mergeIgnoresAttr(el, name, merged) {
151
+ return isTabLocalAttr(name, merged || el);
152
+ }
153
+
154
+ /**
155
+ * hyper-morph's beforeAttributeUpdated veto: a merge may neither write nor
156
+ * remove a tab-local attribute. A marker-protected attribute is left to
157
+ * mergeIgnoresAttr, which reads the marker from the merged element.
158
+ */
159
+ export function mergeVetoesAttr(name, el) {
160
+ return isTabLocalAttr(name, el) && !isMarkedAttr(name, el) ? false : undefined;
161
+ }
162
+
140
163
  /** Remove router state from a snapshot clone: route-* on its root, route-active anywhere. */
141
164
  export function stripRouteAttrs(root) {
142
165
  for (const attr of [...root.attributes]) {
package/src/loader.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { resolveModules, MODULES } from "./loader-logic.js";
2
2
  import onDomReady from "./lib/dom-ready.js";
3
3
  import { initTheme } from "./ui/theme.js";
4
+ import { ephemeral } from "./lib/ephemeral.js";
4
5
 
5
6
  function domReady() {
6
7
  return new Promise((resolve) => onDomReady(resolve));
@@ -91,6 +92,7 @@ function assembleCore(loaded, { isEditMode, isOwner, siteVisibility }, regionPol
91
92
 
92
93
  clay.readData = pageData.readData;
93
94
  clay.writeData = pageData.writeData;
95
+ clay.ephemeral = ephemeral;
94
96
 
95
97
  if (save) {
96
98
  const saveFn = save.savePage || save.default;
@@ -5,15 +5,30 @@
5
5
 
6
6
  import { undo } from '../vendor/hyper-undo.vendor.js'
7
7
  import { isEditMode } from '../core/is-edit-mode.js'
8
+ import { isTabLocalAttr } from '../lib/root-attrs.js'
9
+
10
+ // Tab-local attributes (ephemeral state, route-active, root library state) are
11
+ // never undone. The shared mutation hub already drops them for the main
12
+ // instance; a scope made with create() runs its own observer, so it is told too.
13
+ const withTabLocal = (opts = {}) => {
14
+ const own = opts.ignoreAttribute
15
+ return {
16
+ ...opts,
17
+ ignoreAttribute: (name, target) => isTabLocalAttr(name, target) || (own ? own(name, target) : false),
18
+ }
19
+ }
20
+
21
+ const createScope = undo.create.bind(undo)
22
+ undo.create = (opts) => createScope(withTabLocal(opts))
8
23
 
9
24
  function init() {
10
25
  if (!isEditMode) return
11
26
  // Defer until the body exists so the default scope (document.body) is valid.
12
27
  if (typeof document === 'undefined') return
13
28
  if (document.readyState === 'loading') {
14
- document.addEventListener('DOMContentLoaded', () => undo.start({ bindKeys: true }))
29
+ document.addEventListener('DOMContentLoaded', () => undo.start(withTabLocal({ bindKeys: true })))
15
30
  } else {
16
- undo.start({ bindKeys: true })
31
+ undo.start(withTabLocal({ bindKeys: true }))
17
32
  }
18
33
  }
19
34
 
@@ -40,11 +40,11 @@ import { HyperMorph, morph } from "../vendor/hyper-morph.vendor.js";
40
40
  const { createIdentityStore } = HyperMorph;
41
41
  import Mutation from "../lib/mutation.js";
42
42
  import { NO_DIRTY_SELECTOR, LIVE_SELECTOR } from "../lib/region-policy.js";
43
- import { EXTENSION_NODE_SELECTOR } from '../lib/extension-noise.js';
43
+ import { EXTENSION_NODE_SELECTOR, EXTENSION_ATTR_PATTERN } from '../lib/extension-noise.js';
44
44
  import { isEditMode } from "../core/is-edit-mode.js";
45
45
  import { mergeTagRecognizers } from "./merge-tags.js";
46
46
  import { serializeForSync, captureForComparisonAndDirty, captureSnapshot, originalSnapshotNode, captureForMerge, captureForSaveAndComparison } from '../core/snapshot.js';
47
- import { isTabLocalAttr } from '../lib/root-attrs.js';
47
+ import { mergeIgnoresAttr, mergeVetoesAttr } from '../lib/root-attrs.js';
48
48
  import { autosaveActive } from '../lib/autosave-state.js';
49
49
  import { enableContentEditable } from '../core/admin-contenteditable.js';
50
50
  import { enableOnClick } from '../core/admin-onclick.js';
@@ -101,9 +101,12 @@ const REMOTE_WINS_SELECTOR = [
101
101
  '[clay~="no-watch"]', '[no-watch]', '[mutations-ignore]',
102
102
  ].join(',');
103
103
 
104
- // Host tokens, library state and router state, on every side
104
+ // Host tokens, library state, router state and ephemeral state, on every side
105
105
  // of a merge: the live root, a snapshot clone's root, a parsed frame's root.
106
- const isRootLocalAttr = (el, name) => isTabLocalAttr(name, el);
106
+ // Extension marker attributes too: snapshots and frames never carry them, so a
107
+ // disk frame would otherwise strip them from the live inputs they belong to.
108
+ const isRootLocalAttr = (el, name, merged) =>
109
+ mergeIgnoresAttr(el, name, merged) || EXTENSION_ATTR_PATTERN.test(name.toLowerCase());
107
110
 
108
111
  // Identity an author wrote into the page, the fallback behind synthetic ids.
109
112
  //
@@ -1655,8 +1658,7 @@ class LiveSync {
1655
1658
  const store = this.identity;
1656
1659
  // A frame may neither write these onto our root nor, by not carrying them,
1657
1660
  // take ours away. Returning false is hyper-morph's veto for both directions.
1658
- const beforeAttributeUpdated = (name, element) =>
1659
- isTabLocalAttr(name, element) ? false : undefined;
1661
+ const beforeAttributeUpdated = mergeVetoesAttr;
1660
1662
  const authored = uniqueAuthoredIdentity(new WeakMap());
1661
1663
  const sideIdentity = (map) =>
1662
1664
  isIdMap(map) ? { first: authored, map, then: authored } : authored;
@@ -0,0 +1,12 @@
1
+ // Tailwind v4 plugin for ClayJS ephemeral state:
2
+ // @plugin "@panphora/clayjs/tailwind";
3
+ // ephemeral-selected:ring-2 -> &[ephemeral~="selected"]. matchVariant only
4
+ // accepts bare names that `values` lists, so `values` answers for any simple
5
+ // word; the bracketed form ephemeral-[selected]: works regardless.
6
+ const anyWord = new Proxy({}, {
7
+ get: (_, key) => (typeof key === "string" && /^[a-z0-9_-]+$/i.test(key) ? key : undefined),
8
+ });
9
+
10
+ export default function ephemeralVariant({ matchVariant }) {
11
+ matchVariant("ephemeral", (value) => `&[ephemeral~="${value}"]`, { values: anyWord });
12
+ }