what-core 0.10.0 → 0.11.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.
Files changed (76) hide show
  1. package/dist/chunk-D5YDPQ57.min.js +1 -0
  2. package/dist/chunk-O3SKPRTY.min.js +0 -1
  3. package/dist/chunk-W33M3HL5.min.js +1 -0
  4. package/dist/index.min.js +6 -7
  5. package/dist/jsx-dev-runtime.min.js +0 -1
  6. package/dist/jsx-runtime.min.js +0 -1
  7. package/dist/render.min.js +1 -2
  8. package/dist/testing.min.js +1 -2
  9. package/package.json +2 -2
  10. package/render.d.ts +18 -0
  11. package/src/agent-context.js +3 -2
  12. package/src/dom.js +16 -0
  13. package/src/guardrails.js +17 -46
  14. package/src/reactive.js +42 -4
  15. package/src/render.js +159 -55
  16. package/dist/a11y.js +0 -440
  17. package/dist/animation.js +0 -548
  18. package/dist/chunk-AW3BAPIK.js +0 -1685
  19. package/dist/chunk-AW3BAPIK.js.map +0 -7
  20. package/dist/chunk-AZP2EOGX.js +0 -188
  21. package/dist/chunk-AZP2EOGX.js.map +0 -7
  22. package/dist/chunk-F2HUXI22.js +0 -1675
  23. package/dist/chunk-F2HUXI22.js.map +0 -7
  24. package/dist/chunk-KBM6CWG4.min.js +0 -2
  25. package/dist/chunk-KBM6CWG4.min.js.map +0 -7
  26. package/dist/chunk-KL7TNUIU.min.js +0 -2
  27. package/dist/chunk-KL7TNUIU.min.js.map +0 -7
  28. package/dist/chunk-L6XOF7P4.min.js +0 -2
  29. package/dist/chunk-L6XOF7P4.min.js.map +0 -7
  30. package/dist/chunk-M7UEET5O.js +0 -1323
  31. package/dist/chunk-M7UEET5O.js.map +0 -7
  32. package/dist/chunk-O3SKPRTY.min.js.map +0 -7
  33. package/dist/chunk-RN6QIBWL.min.js +0 -2
  34. package/dist/chunk-RN6QIBWL.min.js.map +0 -7
  35. package/dist/chunk-VMTTYB4L.min.js +0 -2
  36. package/dist/chunk-VMTTYB4L.min.js.map +0 -7
  37. package/dist/chunk-VP4WLF5A.js +0 -1323
  38. package/dist/chunk-VP4WLF5A.js.map +0 -7
  39. package/dist/chunk-YA3W4XKH.js +0 -1323
  40. package/dist/chunk-YA3W4XKH.js.map +0 -7
  41. package/dist/compiler.js +0 -1799
  42. package/dist/compiler.js.map +0 -7
  43. package/dist/compiler.min.js +0 -2
  44. package/dist/compiler.min.js.map +0 -7
  45. package/dist/components.js +0 -229
  46. package/dist/data.js +0 -638
  47. package/dist/devtools.js +0 -10
  48. package/dist/devtools.js.map +0 -7
  49. package/dist/devtools.min.js +0 -2
  50. package/dist/devtools.min.js.map +0 -7
  51. package/dist/dom.js +0 -439
  52. package/dist/form.js +0 -509
  53. package/dist/h.js +0 -152
  54. package/dist/head.js +0 -51
  55. package/dist/helpers.js +0 -140
  56. package/dist/hooks.js +0 -210
  57. package/dist/index.js +0 -3578
  58. package/dist/index.js.map +0 -7
  59. package/dist/index.min.js.map +0 -7
  60. package/dist/jsx-dev-runtime.js +0 -23
  61. package/dist/jsx-dev-runtime.js.map +0 -7
  62. package/dist/jsx-dev-runtime.min.js.map +0 -7
  63. package/dist/jsx-runtime.js +0 -21
  64. package/dist/jsx-runtime.js.map +0 -7
  65. package/dist/jsx-runtime.min.js.map +0 -7
  66. package/dist/reactive.js +0 -432
  67. package/dist/render.js +0 -41
  68. package/dist/render.js.map +0 -7
  69. package/dist/render.min.js.map +0 -7
  70. package/dist/scheduler.js +0 -246
  71. package/dist/skeleton.js +0 -363
  72. package/dist/store.js +0 -83
  73. package/dist/testing.js +0 -439
  74. package/dist/testing.js.map +0 -7
  75. package/dist/testing.min.js.map +0 -7
  76. package/dist/what.js +0 -117
package/src/guardrails.js CHANGED
@@ -10,7 +10,6 @@ import { createWhatError, collectError } from './errors.js';
10
10
 
11
11
  const guardrails = {
12
12
  signalReadDetection: true,
13
- effectCycleDetection: true,
14
13
  componentNaming: true,
15
14
  importValidation: true,
16
15
  };
@@ -25,16 +24,19 @@ export function getGuardrailConfig() {
25
24
 
26
25
  // --- Guardrail 1: Signal Read Detection ---
27
26
  // Detect when a signal function reference is used where its value was intended.
28
- // This catches the pattern: <span>{count}</span> (should be count())
27
+ // This catches patterns like `Total: ${count}` or `count > 5` (should be count()).
29
28
  //
30
- // At runtime, we can detect this when a signal is coerced to string (via toString/valueOf)
31
- // and warn that it should be called.
29
+ // At runtime, we detect this when a signal is coerced to a string or number
30
+ // (via toString/valueOf) and warn that it should be called.
31
+ //
32
+ // Wiring: what-devtools calls this for every signal it registers (dev only),
33
+ // so any app with devtools installed gets the guardrail automatically.
34
+ // It can also be called manually: installSignalReadGuardrail(sig, 'name').
32
35
 
33
36
  export function installSignalReadGuardrail(signalFn, debugName) {
34
37
  if (!__DEV__ || !guardrails.signalReadDetection) return signalFn;
35
38
 
36
39
  // Override toString to catch template literal coercion
37
- const originalToString = signalFn.toString;
38
40
  signalFn.toString = function () {
39
41
  const err = createWhatError('MISSING_SIGNAL_READ', {
40
42
  signalName: debugName || '(unnamed)',
@@ -58,47 +60,16 @@ export function installSignalReadGuardrail(signalFn, debugName) {
58
60
  return signalFn;
59
61
  }
60
62
 
61
- // --- Guardrail 2: Enhanced Effect Cycle Detection ---
62
- // Track which signals an effect reads AND writes.
63
- // If an effect writes to a signal it reads, warn about the specific cycle.
64
-
65
- const effectWriteTracking = new WeakMap(); // effect -> Set of signal debug names
66
-
67
- export function trackEffectSignalWrite(effectRef, signalDebugName) {
68
- if (!__DEV__ || !guardrails.effectCycleDetection) return;
69
-
70
- if (!effectWriteTracking.has(effectRef)) {
71
- effectWriteTracking.set(effectRef, new Set());
72
- }
73
- effectWriteTracking.get(effectRef).add(signalDebugName);
74
- }
75
-
76
- export function checkEffectCycle(effectRef, readSignals) {
77
- if (!__DEV__ || !guardrails.effectCycleDetection) return null;
78
-
79
- const writes = effectWriteTracking.get(effectRef);
80
- if (!writes || writes.size === 0) return null;
81
-
82
- const overlapping = [];
83
- for (const sigName of readSignals) {
84
- if (writes.has(sigName)) {
85
- overlapping.push(sigName);
86
- }
87
- }
88
-
89
- if (overlapping.length > 0) {
90
- const err = createWhatError('INFINITE_EFFECT', {
91
- effectName: effectRef.fn?.name || '(anonymous)',
92
- signalName: overlapping.join(', '),
93
- });
94
- collectError(err);
95
- return err;
96
- }
97
-
98
- return null;
99
- }
63
+ // NOTE: an "effect cycle detection" guardrail (trackEffectSignalWrite /
64
+ // checkEffectCycle) used to live here, exported but never called by anything
65
+ // it was removed in v0.11 rather than shipped as a dead API. Reviving it
66
+ // requires hooks inside reactive.js: (1) a call on every signal WRITE while
67
+ // an effect is the active listener (to attribute writes to effects), and
68
+ // (2) a post-run callback with the effect's tracked read-set (to intersect
69
+ // reads with writes). reactive.js already detects runaway cascades at flush
70
+ // time (iteration cap), which covers the practical failure mode.
100
71
 
101
- // --- Guardrail 3: Component Naming ---
72
+ // --- Guardrail 2: Component Naming ---
102
73
  // Warn if a component function is not PascalCase.
103
74
 
104
75
  export function checkComponentName(name) {
@@ -119,7 +90,7 @@ function capitalize(str) {
119
90
  return str.charAt(0).toUpperCase() + str.slice(1);
120
91
  }
121
92
 
122
- // --- Guardrail 4: Import Validation ---
93
+ // --- Guardrail 3: Import Validation ---
123
94
  // Verify that all named imports from 'what-framework' are valid exports.
124
95
 
125
96
  const VALID_EXPORTS = new Set([
package/src/reactive.js CHANGED
@@ -7,10 +7,26 @@
7
7
  // - Ownership tree: createRoot children auto-dispose when parent disposes
8
8
  // - Performance: cached levels, lazy sort, fast-path notify, minimal allocation
9
9
 
10
- // Dev-mode flag — build tools can dead-code-eliminate when false
11
- export const __DEV__ = typeof process !== 'undefined'
12
- ? process.env?.NODE_ENV !== 'production'
13
- : true;
10
+ // Dev-mode flag — `if (__DEV__)` branches dead-code-eliminate when this is false.
11
+ //
12
+ // Resolution order (first definitive signal wins; PRODUCTION-SAFE default):
13
+ // 1. globalThis.__WHAT_DEV__ — explicit boolean override for browser tooling
14
+ // (e.g. the playground can force dev mode without a bundler env).
15
+ // 2. import.meta.env.DEV — Vite & modern bundlers statically replace this,
16
+ // so production builds fully strip every dev branch.
17
+ // 3. process.env.NODE_ENV — Node / webpack / esbuild `define`.
18
+ // 4. false — a raw browser bundle with NO build signal must
19
+ // default to PRODUCTION. (Previously defaulted to `true`, so EVERY browser
20
+ // bundle ran in dev mode: perf/size tax + the spurious internal
21
+ // `template()` XSS warning surfacing on production sites. AUDIT 2026-06-10.)
22
+ export const __DEV__ =
23
+ (typeof globalThis !== 'undefined' && typeof globalThis.__WHAT_DEV__ === 'boolean')
24
+ ? globalThis.__WHAT_DEV__
25
+ : (import.meta && import.meta.env)
26
+ ? !!import.meta.env.DEV
27
+ : (typeof process !== 'undefined' && process.env)
28
+ ? process.env.NODE_ENV !== 'production'
29
+ : false;
14
30
 
15
31
  // DevTools hooks — set by what-devtools when installed.
16
32
  // These are no-ops in production (dead-code eliminated with __DEV__).
@@ -287,6 +303,8 @@ function _updateLevel(e) {
287
303
  // Runs a function, auto-tracking signal reads. Re-runs when deps change.
288
304
  // Returns a dispose function.
289
305
 
306
+ const _noopDispose = () => {};
307
+
290
308
  export function effect(fn, opts) {
291
309
  const e = _createEffect(fn);
292
310
  e._level = 1;
@@ -303,6 +321,26 @@ export function effect(fn, opts) {
303
321
  _updateLevel(e);
304
322
  // Mark as stable after first run — subsequent re-runs skip cleanup/re-subscribe
305
323
  if (opts?.stable) e._stable = true;
324
+
325
+ // Zero-dependency release (SPRINT v0.11 C4): an effect that tracked zero
326
+ // signals on its first run can never be notified again — re-tracking only
327
+ // happens during a re-run, and a re-run requires a notification from a
328
+ // subscribed signal. The compiler conservatively wraps destructured props /
329
+ // imported accessors in effects; when those turn out to be plain values the
330
+ // effect is one-shot. If it also registered no cleanup, release it now:
331
+ // no dispose closure, no owner registration, nothing retained.
332
+ // - Effects that returned a cleanup keep full registration so the cleanup
333
+ // still runs on owner disposal.
334
+ // - onCleanup() callbacks register with currentRoot directly (not with the
335
+ // effect), so they are unaffected by this release.
336
+ // - untrack()/peek() reads inside the fn produce zero deps by design — the
337
+ // effect could never re-fire anyway, so releasing is safe.
338
+ if (e.deps.length === 0 && e._cleanup === null) {
339
+ e.disposed = true;
340
+ if (__DEV__ && __devtools) __devtools.onEffectDispose(e);
341
+ return _noopDispose;
342
+ }
343
+
306
344
  const dispose = () => _disposeEffect(e);
307
345
  // Register with current root for automatic cleanup
308
346
  if (currentRoot) {
package/src/render.js CHANGED
@@ -2,9 +2,13 @@
2
2
  // Solid-style rendering: components run once, signals create individual DOM effects.
3
3
  // No VDOM diffing — direct DOM manipulation with surgical signal-driven updates.
4
4
 
5
- import { effect, untrack, createRoot, _createItemScope, signal, __DEV__ } from './reactive.js';
5
+ import { effect, untrack, createRoot, _createItemScope, signal, memo, __DEV__ } from './reactive.js';
6
6
  import { createDOM, disposeTree, getCurrentComponent, getComponentStack, _setSelectValue } from './dom.js';
7
7
  export { effect, untrack };
8
+ // Re-export memo for compiled output (branch memoization: the compiler emits
9
+ // _$memo(() => cond) so conditional branches only re-create DOM when the
10
+ // condition value actually changes, not on every dependency write).
11
+ export { memo };
8
12
 
9
13
  // --- Generic text insertion hook ---
10
14
  // External text engines (e.g., what-text) register a callback here via
@@ -173,41 +177,44 @@ export function insert(parent, child, marker) {
173
177
  }
174
178
 
175
179
  if (typeof child === 'function') {
176
- // Fast path: if the first evaluation returns a string/number, optimistically
177
- // create a text node for direct updates. If the value type changes later
178
- // (e.g., text -> vnode), fall back to full reconcileInsert.
179
- const first = child();
180
- const t = typeof first;
181
- if (t === 'string' || t === 'number') {
182
- const textNode = document.createTextNode(String(first));
183
- const m = marker || null;
184
- if (m) parent.insertBefore(textNode, m);
185
- else parent.appendChild(textNode);
186
- if (_onTextInsert) _onTextInsert(parent, String(first));
187
- let current = textNode;
188
- let isTextFastPath = true;
189
- effect(() => {
190
- const val = child();
191
- const vt = typeof val;
192
- if (isTextFastPath && (vt === 'string' || vt === 'number')) {
193
- // Fast path: still text — update data directly (no allocations)
194
- const str = String(val);
195
- if (textNode.data !== str) textNode.data = str;
196
- if (_onTextInsert) _onTextInsert(parent, str);
197
- } else {
198
- // Type changed — fall back to full reconcile
199
- isTextFastPath = false;
200
- current = reconcileInsert(parent, val, current, m);
201
- }
202
- });
203
- return textNode;
204
- }
205
- // General path for non-text reactive children (first value was null/vnode/array).
206
- // Let the effect handle both the initial insert and subsequent updates to avoid
207
- // double-evaluating child() (which would create components twice on mount).
180
+ // Single-evaluation mount: child() is evaluated exactly ONCE at mount,
181
+ // inside the effect (so signal reads are tracked). The first run decides
182
+ // between the text fast path (direct textNode.data updates, zero
183
+ // allocations) and the general reconcile path. Previously the first
184
+ // evaluation happened outside the effect to pick the path, then the
185
+ // effect's first run re-evaluated child() — creating components twice
186
+ // on mount for non-text children. (SPRINT v0.11 C3)
187
+ const m = marker || null;
208
188
  let current = null;
189
+ let textNode = null; // non-null while on the text fast path
190
+ let mounted = false;
209
191
  effect(() => {
210
- current = reconcileInsert(parent, child(), current, marker || null);
192
+ const val = child();
193
+ const vt = typeof val;
194
+ if (!mounted) {
195
+ // First run — mount
196
+ mounted = true;
197
+ if (vt === 'string' || vt === 'number') {
198
+ textNode = document.createTextNode(String(val));
199
+ if (m) parent.insertBefore(textNode, m);
200
+ else parent.appendChild(textNode);
201
+ if (_onTextInsert) _onTextInsert(parent, String(val));
202
+ current = textNode;
203
+ } else {
204
+ current = reconcileInsert(parent, val, null, m);
205
+ }
206
+ return;
207
+ }
208
+ if (textNode !== null && (vt === 'string' || vt === 'number')) {
209
+ // Fast path: still text — update data directly (no allocations)
210
+ const str = String(val);
211
+ if (textNode.data !== str) textNode.data = str;
212
+ if (_onTextInsert) _onTextInsert(parent, str);
213
+ return;
214
+ }
215
+ // Type changed (or never was text) — full reconcile
216
+ textNode = null;
217
+ current = reconcileInsert(parent, val, current, m);
211
218
  });
212
219
  return current;
213
220
  }
@@ -417,10 +424,17 @@ export function mapArray(source, mapFn, options) {
417
424
 
418
425
  effect(() => {
419
426
  const newItems = source() || [];
427
+ // Resolve the LIVE parent from the end marker each run. When this inserter
428
+ // is mounted at a fragment-as-root (`<>{items().map(...)}</>`), createDOM
429
+ // calls it against a throwaway DocumentFragment which is then appended to
430
+ // the real container — the marker (and existing rows) move with it, so the
431
+ // captured `parent` goes stale. endMarker.parentNode always reflects where
432
+ // the list currently lives. Falls back to the captured parent pre-mount.
433
+ const liveParent = endMarker.parentNode || parent;
420
434
  if (keyFn) {
421
- reconcileKeyed(parent, endMarker, items, newItems, mappedNodes, disposeFns, mapFn, keyFn, keyedState);
435
+ reconcileKeyed(liveParent, endMarker, items, newItems, mappedNodes, disposeFns, mapFn, keyFn, keyedState);
422
436
  } else {
423
- reconcileList(parent, endMarker, items, newItems, mappedNodes, disposeFns, mapFn);
437
+ reconcileList(liveParent, endMarker, items, newItems, mappedNodes, disposeFns, mapFn);
424
438
  }
425
439
  // Save a snapshot of items for next diff. Use slice() to defend against
426
440
  // in-place mutation, but skip for empty arrays (common clear case).
@@ -1244,12 +1258,9 @@ export function spread(el, props) {
1244
1258
  else el.className = cls;
1245
1259
  });
1246
1260
  } else if (key === 'style' && typeof value() === 'object') {
1247
- el._propEffects[key] = effect(() => {
1248
- const styles = value();
1249
- for (const prop in styles) {
1250
- el.style[prop] = styles[prop] ?? '';
1251
- }
1252
- });
1261
+ // Route through setStyle so stale object keys are cleared between
1262
+ // re-evaluations (el._lastStyleObj diffing).
1263
+ el._propEffects[key] = effect(() => { setStyle(el, value()); });
1253
1264
  } else {
1254
1265
  el._propEffects[key] = effect(() => { setProp(el, key, value()); });
1255
1266
  }
@@ -1332,13 +1343,8 @@ export function setProp(el, key, value) {
1332
1343
  }
1333
1344
  }
1334
1345
  } else if (key === 'style') {
1335
- if (typeof value === 'string') {
1336
- el.style.cssText = value;
1337
- } else if (typeof value === 'object') {
1338
- for (const prop in value) {
1339
- el.style[prop] = value[prop] ?? '';
1340
- }
1341
- }
1346
+ // Delegate to setStyle so the object form clears stale keys (el._lastStyleObj).
1347
+ setStyle(el, value);
1342
1348
  } else if (key.startsWith('data-') || key.startsWith('aria-')) {
1343
1349
  el.setAttribute(key, value);
1344
1350
  } else if (typeof value === 'boolean') {
@@ -1355,6 +1361,99 @@ export function setProp(el, key, value) {
1355
1361
  }
1356
1362
  }
1357
1363
 
1364
+ // --- Specialized attribute setters (SPRINT v0.11 C2) ---
1365
+ // The compiler statically knows most attribute names, so it emits direct calls
1366
+ // to these monomorphic helpers instead of routing everything through the
1367
+ // generic setProp() dispatcher (which re-checks ref/key/url/class/style/...
1368
+ // string-compares on every reactive update). setProp() remains the target for
1369
+ // spreads, URL attributes (href/src/action — sanitization lives there) and any
1370
+ // name the compiler can't classify.
1371
+ //
1372
+ // Function values are reactive ACCESSORS (e.g. `value={() => user().name}`),
1373
+ // exactly like setProp treats them: wrap in an effect that re-applies the
1374
+ // resolved value, with the disposer registered on el._propEffects so
1375
+ // disposeTree() tears it down on unmount.
1376
+
1377
+ function _wrapPropAccessor(el, key, accessor, apply) {
1378
+ if (!el._propEffects) el._propEffects = {};
1379
+ if (el._propEffects[key]) {
1380
+ try { el._propEffects[key](); } catch (e) { /* already disposed */ }
1381
+ }
1382
+ el._propEffects[key] = effect(() => apply(el, accessor()));
1383
+ }
1384
+
1385
+ // class / className — hottest dynamic attribute in real apps.
1386
+ export function setClass(el, value) {
1387
+ if (typeof value === 'function') return _wrapPropAccessor(el, 'class', value, setClass);
1388
+ if (_hasSVGElement && el instanceof SVGElement) {
1389
+ el.setAttribute('class', value || '');
1390
+ } else {
1391
+ el.className = value || '';
1392
+ }
1393
+ }
1394
+
1395
+ // style — string (cssText) or object form.
1396
+ export function setStyle(el, value) {
1397
+ if (typeof value === 'function') return _wrapPropAccessor(el, 'style', value, setStyle);
1398
+ if (typeof value === 'string') {
1399
+ el.style.cssText = value;
1400
+ // cssText fully replaces inline styles — drop any tracked object so a later
1401
+ // object form starts clean rather than diffing against stale keys.
1402
+ el._lastStyleObj = null;
1403
+ } else if (value && typeof value === 'object') {
1404
+ const style = el.style;
1405
+ // Clear properties present in the previously-applied object but absent from
1406
+ // the new one. Without this, `style={() => cond() ? {color, fontWeight} :
1407
+ // {color}}` would leave fontWeight set after flipping to the second object.
1408
+ const prev = el._lastStyleObj;
1409
+ if (prev) {
1410
+ for (const prop in prev) {
1411
+ if (!(prop in value)) style[prop] = '';
1412
+ }
1413
+ }
1414
+ for (const prop in value) {
1415
+ style[prop] = value[prop] ?? '';
1416
+ }
1417
+ el._lastStyleObj = value;
1418
+ } else if (value == null) {
1419
+ el.style.cssText = '';
1420
+ el._lastStyleObj = null;
1421
+ }
1422
+ }
1423
+
1424
+ // Plain attribute set — used for data-*/aria-* (statically recognizable).
1425
+ // null/undefined removes the attribute (previously setProp stringified them
1426
+ // to "null"/"undefined" — removal is the correct semantic). Booleans are
1427
+ // stringified ("true"/"false") because aria-* boolean strings are meaningful.
1428
+ export function setAttr(el, name, value) {
1429
+ if (typeof value === 'function') {
1430
+ return _wrapPropAccessor(el, name, value, (e2, v) => setAttr(e2, name, v));
1431
+ }
1432
+ if (value == null) el.removeAttribute(name);
1433
+ else el.setAttribute(name, value);
1434
+ }
1435
+
1436
+ // value — controlled-input property set. <select> keeps multi/deferred-option
1437
+ // handling; other elements get a guarded property write (the !== guard avoids
1438
+ // resetting the caret position in focused inputs on unrelated re-runs).
1439
+ export function setValue(el, value) {
1440
+ if (typeof value === 'function') return _wrapPropAccessor(el, 'value', value, setValue);
1441
+ if (el.tagName === 'SELECT') {
1442
+ _setSelectValue(el, value);
1443
+ return;
1444
+ }
1445
+ const str = value == null ? '' : String(value);
1446
+ if (el.value !== str) el.value = str;
1447
+ }
1448
+
1449
+ // checked — live property write (matches bind:checked). The old generic path
1450
+ // used setAttribute('checked'), which only sets the DEFAULT-checked state and
1451
+ // stops reflecting once the user has toggled the input.
1452
+ export function setChecked(el, value) {
1453
+ if (typeof value === 'function') return _wrapPropAccessor(el, 'checked', value, setChecked);
1454
+ el.checked = !!value;
1455
+ }
1456
+
1358
1457
  // --- delegateEvents(eventNames) ---
1359
1458
  // Event delegation: common events handled at document level.
1360
1459
  // Handlers stored as el.$$click, el.$$input, etc.
@@ -1371,6 +1470,15 @@ export function delegateEvents(eventNames) {
1371
1470
  let node = e.target;
1372
1471
  const key = '$$' + name;
1373
1472
 
1473
+ // Shim e.currentTarget so handlers see the element the (virtual) listener
1474
+ // is attached to — not `document` — during the ancestor walk. Mirrors
1475
+ // Solid's delegation shim. configurable so nested dispatch can redefine.
1476
+ // (SPRINT v0.11 C9)
1477
+ Object.defineProperty(e, 'currentTarget', {
1478
+ configurable: true,
1479
+ get() { return node || document; },
1480
+ });
1481
+
1374
1482
  // Walk up the DOM tree looking for handlers
1375
1483
  while (node) {
1376
1484
  const handler = node[key];
@@ -1659,12 +1767,8 @@ function hydrateElementProps(el, props) {
1659
1767
  if (key === 'class' || key === 'className') {
1660
1768
  effect(() => { el.className = value() || ''; });
1661
1769
  } else if (key === 'style' && typeof value() === 'object') {
1662
- effect(() => {
1663
- const styles = value();
1664
- for (const prop in styles) {
1665
- el.style[prop] = styles[prop] ?? '';
1666
- }
1667
- });
1770
+ // Route through setStyle so stale object keys are cleared (el._lastStyleObj).
1771
+ effect(() => { setStyle(el, value()); });
1668
1772
  } else {
1669
1773
  effect(() => { setProp(el, key, value()); });
1670
1774
  }