what-core 0.5.1 → 0.5.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "what-core",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "description": "What Framework - The closest framework to vanilla JS",
5
5
  "type": "module",
6
6
  "main": "dist/what.js",
@@ -48,10 +48,10 @@
48
48
  "license": "MIT",
49
49
  "repository": {
50
50
  "type": "git",
51
- "url": "https://github.com/aspect/what-fw"
51
+ "url": "https://github.com/zvndev/what-fw"
52
52
  },
53
53
  "bugs": {
54
- "url": "https://github.com/aspect/what-fw/issues"
54
+ "url": "https://github.com/zvndev/what-fw/issues"
55
55
  },
56
- "homepage": "https://github.com/aspect/what-fw#readme"
56
+ "homepage": "https://whatframework.dev"
57
57
  }
package/src/components.js CHANGED
@@ -50,7 +50,7 @@ export function memo(Component, areEqual) {
50
50
  let _getCurrentComponent = null;
51
51
  export function _injectGetCurrentComponent(fn) { _getCurrentComponent = fn; }
52
52
 
53
- function shallowEqual(a, b) {
53
+ export function shallowEqual(a, b) {
54
54
  if (a === b) return true;
55
55
  const keysA = Object.keys(a);
56
56
  const keysB = Object.keys(b);
package/src/dom.js CHANGED
@@ -4,7 +4,7 @@
4
4
  // No virtual DOM tree kept in memory — we diff against the live DOM.
5
5
 
6
6
  import { effect, batch, untrack, signal } from './reactive.js';
7
- import { reportError, _injectGetCurrentComponent } from './components.js';
7
+ import { reportError, _injectGetCurrentComponent, shallowEqual } from './components.js';
8
8
  import { _setComponentRef } from './helpers.js';
9
9
 
10
10
  // Register <what-c> custom element to prevent flash of unstyled content
@@ -14,6 +14,36 @@ if (typeof customElements !== 'undefined' && !customElements.get('what-c')) {
14
14
  connectedCallback() {
15
15
  this.style.display = 'contents';
16
16
  }
17
+ // display:contents elements don't generate a layout box — getBoundingClientRect()
18
+ // returns zeros, offsetWidth/Height return 0. React libraries (react-draggable,
19
+ // react-colorful, etc.) traverse parentNode and call getBoundingClientRect() on
20
+ // what they expect to be a layout container. Since <what-c> is layout-invisible,
21
+ // delegate to the nearest ancestor that has a real box.
22
+ _layoutParent() {
23
+ let el = this.parentElement;
24
+ while (el && el.tagName === 'WHAT-C') el = el.parentElement;
25
+ return el;
26
+ }
27
+ getBoundingClientRect() {
28
+ const p = this._layoutParent();
29
+ return p ? p.getBoundingClientRect() : super.getBoundingClientRect();
30
+ }
31
+ get offsetWidth() {
32
+ const p = this._layoutParent();
33
+ return p ? p.offsetWidth : 0;
34
+ }
35
+ get offsetHeight() {
36
+ const p = this._layoutParent();
37
+ return p ? p.offsetHeight : 0;
38
+ }
39
+ get clientWidth() {
40
+ const p = this._layoutParent();
41
+ return p ? p.clientWidth : 0;
42
+ }
43
+ get clientHeight() {
44
+ const p = this._layoutParent();
45
+ return p ? p.clientHeight : 0;
46
+ }
17
47
  });
18
48
  }
19
49
 
@@ -160,6 +190,11 @@ export function createDOM(vnode, parent, isSvg) {
160
190
  return document.createTextNode(String(vnode));
161
191
  }
162
192
 
193
+ // Portal (string-tagged vnodes from helpers.js Portal or react-compat createPortal)
194
+ if (vnode.tag === '__portal') {
195
+ return createPortalDOM(vnode, parent);
196
+ }
197
+
163
198
  // Component
164
199
  if (typeof vnode.tag === 'function') {
165
200
  return createComponent(vnode, parent, isSvg);
@@ -208,7 +243,20 @@ export function getComponentStack() {
208
243
  }
209
244
 
210
245
  function createComponent(vnode, parent, isSvg) {
211
- const { tag: Component, props, children } = vnode;
246
+ let { tag: Component, props, children } = vnode;
247
+
248
+ // Class component detection — ES6 classes can't be called without `new`.
249
+ // React compat layer wraps class components in createElement, but some
250
+ // library-internal components may bypass that path. Detect and wrap here.
251
+ if (typeof Component === 'function' &&
252
+ (Component.prototype?.isReactComponent || Component.prototype?.render)) {
253
+ const ClassComp = Component;
254
+ Component = function ClassComponentBridge(props) {
255
+ const instance = new ClassComp(props);
256
+ return instance.render();
257
+ };
258
+ Component.displayName = ClassComp.displayName || ClassComp.name || 'ClassComponent';
259
+ }
212
260
 
213
261
  // Handle special boundary components
214
262
  if (Component === '__errorBoundary' || vnode.tag === '__errorBoundary') {
@@ -217,8 +265,8 @@ function createComponent(vnode, parent, isSvg) {
217
265
  if (Component === '__suspense' || vnode.tag === '__suspense') {
218
266
  return createSuspenseBoundary(vnode, parent);
219
267
  }
220
- if (Component === '__portal' || vnode.tag === '__portal') {
221
- return createPortal(vnode, parent);
268
+ if (Component === '__portal' || vnode.tag === '__portal') { // Now also handled in createDOM directly
269
+ return createPortalDOM(vnode, parent);
222
270
  }
223
271
 
224
272
  // Component context for hooks
@@ -258,7 +306,9 @@ function createComponent(vnode, parent, isSvg) {
258
306
  mountedComponents.add(ctx);
259
307
 
260
308
  // Props signal for reactive updates from parent
261
- const propsSignal = signal({ ...props, children });
309
+ // Match React's children semantics: 0→undefined, 1→single child, N→array
310
+ const propsChildren = children.length === 0 ? undefined : children.length === 1 ? children[0] : children;
311
+ const propsSignal = signal({ ...props, children: propsChildren });
262
312
  ctx._propsSignal = propsSignal;
263
313
 
264
314
  // Reactive render: re-renders when signals used inside change
@@ -280,7 +330,9 @@ function createComponent(vnode, parent, isSvg) {
280
330
  return;
281
331
  }
282
332
 
283
- componentStack.pop();
333
+ // Keep ctx on componentStack while creating/reconciling children
334
+ // so child components' _parentCtx correctly points to this component.
335
+ // This is essential for context propagation (useContext walks _parentCtx).
284
336
 
285
337
  const vnodes = Array.isArray(result) ? result : [result];
286
338
 
@@ -306,6 +358,8 @@ function createComponent(vnode, parent, isSvg) {
306
358
  // Update: reconcile children inside wrapper
307
359
  reconcileChildren(wrapper, vnodes);
308
360
  }
361
+
362
+ componentStack.pop();
309
363
  });
310
364
 
311
365
  ctx.effects.push(dispose);
@@ -343,7 +397,6 @@ function createErrorBoundary(vnode, parent) {
343
397
  vnodes = children;
344
398
  }
345
399
 
346
- componentStack.pop();
347
400
  vnodes = Array.isArray(vnodes) ? vnodes : [vnodes];
348
401
 
349
402
  if (wrapper.childNodes.length === 0) {
@@ -354,6 +407,8 @@ function createErrorBoundary(vnode, parent) {
354
407
  } else {
355
408
  reconcileChildren(wrapper, vnodes);
356
409
  }
410
+
411
+ componentStack.pop();
357
412
  });
358
413
 
359
414
  boundaryCtx.effects.push(dispose);
@@ -381,6 +436,8 @@ function createSuspenseBoundary(vnode, parent) {
381
436
  const vnodes = isLoading ? [fallback] : children;
382
437
  const normalized = Array.isArray(vnodes) ? vnodes : [vnodes];
383
438
 
439
+ componentStack.push(boundaryCtx);
440
+
384
441
  if (wrapper.childNodes.length === 0) {
385
442
  for (const v of normalized) {
386
443
  const node = createDOM(v, wrapper);
@@ -389,6 +446,8 @@ function createSuspenseBoundary(vnode, parent) {
389
446
  } else {
390
447
  reconcileChildren(wrapper, normalized);
391
448
  }
449
+
450
+ componentStack.pop();
392
451
  });
393
452
 
394
453
  boundaryCtx.effects.push(dispose);
@@ -396,7 +455,7 @@ function createSuspenseBoundary(vnode, parent) {
396
455
  }
397
456
 
398
457
  // Portal component handler — renders children into a different DOM container
399
- function createPortal(vnode, parent) {
458
+ function createPortalDOM(vnode, parent) {
400
459
  const { container } = vnode.props;
401
460
  const children = vnode.children;
402
461
 
@@ -494,9 +553,33 @@ function reconcileUnkeyed(parent, oldNodes, newVNodes, beforeMarker) {
494
553
 
495
554
  // Keyed reconciliation with LIS algorithm for O(n log n) minimal moves
496
555
  function reconcileKeyed(parent, oldNodes, newVNodes, beforeMarker) {
556
+ const newLen = newVNodes.length;
557
+ const oldLen = oldNodes.length;
558
+
559
+ // --- Fast path: same-position keys (covers "update N items in-place") ---
560
+ // If same length and all keys match at the same index, skip LIS entirely.
561
+ // Just patch each node in-place — O(n) with zero DOM moves.
562
+ if (newLen === oldLen && newLen > 0) {
563
+ let allMatch = true;
564
+ for (let i = 0; i < newLen; i++) {
565
+ const newKey = newVNodes[i]?.key;
566
+ const oldKey = oldNodes[i]?._vnode?.key;
567
+ if (newKey == null || newKey !== oldKey) {
568
+ allMatch = false;
569
+ break;
570
+ }
571
+ }
572
+ if (allMatch) {
573
+ for (let i = 0; i < newLen; i++) {
574
+ patchNode(parent, oldNodes[i], newVNodes[i]);
575
+ }
576
+ return;
577
+ }
578
+ }
579
+
497
580
  // Build old key -> { node, index } map
498
581
  const oldKeyMap = new Map();
499
- for (let i = 0; i < oldNodes.length; i++) {
582
+ for (let i = 0; i < oldLen; i++) {
500
583
  const node = oldNodes[i];
501
584
  const key = node._vnode?.key;
502
585
  if (key != null) {
@@ -505,7 +588,6 @@ function reconcileKeyed(parent, oldNodes, newVNodes, beforeMarker) {
505
588
  }
506
589
 
507
590
  const newNodes = [];
508
- const newLen = newVNodes.length;
509
591
 
510
592
  // First pass: match keys and find reusable nodes
511
593
  const sources = new Array(newLen).fill(-1); // Maps new index to old index
@@ -522,7 +604,7 @@ function reconcileKeyed(parent, oldNodes, newVNodes, beforeMarker) {
522
604
  }
523
605
 
524
606
  // Remove nodes that aren't reused
525
- for (let i = 0; i < oldNodes.length; i++) {
607
+ for (let i = 0; i < oldLen; i++) {
526
608
  if (!reused.has(i) && oldNodes[i]?.parentNode) {
527
609
  disposeTree(oldNodes[i]);
528
610
  oldNodes[i].parentNode.removeChild(oldNodes[i]);
@@ -794,7 +876,14 @@ function patchNode(parent, domNode, vnode) {
794
876
  if (domNode._componentCtx && !domNode._componentCtx.disposed
795
877
  && domNode._componentCtx.Component === vnode.tag) {
796
878
  // Same component — update props reactively, let its effect re-render
797
- domNode._componentCtx._propsSignal.set({ ...vnode.props, children: vnode.children });
879
+ const ch = vnode.children;
880
+ const patchChildren = ch.length === 0 ? undefined : ch.length === 1 ? ch[0] : ch;
881
+ const nextProps = { ...vnode.props, children: patchChildren };
882
+ // Skip signal update if props haven't changed (shallow compare)
883
+ const prevProps = domNode._componentCtx._propsSignal.peek();
884
+ if (!shallowEqual(prevProps, nextProps)) {
885
+ domNode._componentCtx._propsSignal.set(nextProps);
886
+ }
798
887
  domNode._vnode = vnode; // Keep vnode current for keyed reconciliation
799
888
  return domNode;
800
889
  }
@@ -905,23 +994,40 @@ function applyProps(el, newProps, oldProps, isSvg) {
905
994
  }
906
995
 
907
996
  function setProp(el, key, value, isSvg) {
908
- // Event handlers: onClick -> click
997
+ // Event handlers: onClick -> click, onFocusCapture -> focus (capture phase)
909
998
  // Wrap in untrack so signal reads in handlers don't create subscriptions
910
999
  if (key.startsWith('on') && key.length > 2) {
911
- const event = key.slice(2).toLowerCase();
1000
+ let eventName = key.slice(2);
1001
+ // React-style capture phase: onClickCapture → click in capture phase
1002
+ let useCapture = false;
1003
+ if (eventName.endsWith('Capture')) {
1004
+ eventName = eventName.slice(0, -7);
1005
+ useCapture = true;
1006
+ }
1007
+ const event = eventName.toLowerCase();
1008
+ // Use a combined key for storage so capture/bubble don't conflict
1009
+ const storageKey = useCapture ? event + '_capture' : event;
912
1010
  // Store handler for removal
913
- const old = el._events?.[event];
1011
+ const old = el._events?.[storageKey];
914
1012
  // Skip re-wrapping if same handler function
915
1013
  if (old && old._original === value) return;
916
- if (old) el.removeEventListener(event, old);
1014
+ if (old) el.removeEventListener(event, old, useCapture);
1015
+ // If handler is null/undefined, just remove the old one and bail
1016
+ if (value == null) return;
917
1017
  if (!el._events) el._events = {};
918
- // Wrap handler to untrack signal reads
919
- const wrappedHandler = (e) => untrack(() => value(e));
1018
+ // Wrap handler to untrack signal reads.
1019
+ // Add nativeEvent for React compat React synthetic events have
1020
+ // e.nativeEvent pointing to the actual DOM event. Libraries like
1021
+ // react-colorful, cmdk, and @floating-ui/react check this property.
1022
+ const wrappedHandler = (e) => {
1023
+ if (!e.nativeEvent) e.nativeEvent = e;
1024
+ return untrack(() => value(e));
1025
+ };
920
1026
  wrappedHandler._original = value;
921
- el._events[event] = wrappedHandler;
1027
+ el._events[storageKey] = wrappedHandler;
922
1028
  // Check for _eventOpts (once/capture/passive from compiler)
923
1029
  const eventOpts = value._eventOpts;
924
- el.addEventListener(event, wrappedHandler, eventOpts || undefined);
1030
+ el.addEventListener(event, wrappedHandler, eventOpts || useCapture || undefined);
925
1031
  return;
926
1032
  }
927
1033
 
@@ -1003,10 +1109,17 @@ function setProp(el, key, value, isSvg) {
1003
1109
 
1004
1110
  function removeProp(el, key, oldValue) {
1005
1111
  if (key.startsWith('on') && key.length > 2) {
1006
- const event = key.slice(2).toLowerCase();
1007
- if (el._events?.[event]) {
1008
- el.removeEventListener(event, el._events[event]);
1009
- delete el._events[event];
1112
+ let eventName = key.slice(2);
1113
+ let useCapture = false;
1114
+ if (eventName.endsWith('Capture')) {
1115
+ eventName = eventName.slice(0, -7);
1116
+ useCapture = true;
1117
+ }
1118
+ const event = eventName.toLowerCase();
1119
+ const storageKey = useCapture ? event + '_capture' : event;
1120
+ if (el._events?.[storageKey]) {
1121
+ el.removeEventListener(event, el._events[storageKey], useCapture);
1122
+ delete el._events[storageKey];
1010
1123
  }
1011
1124
  return;
1012
1125
  }
package/src/hooks.js CHANGED
@@ -192,6 +192,11 @@ export function createContext(defaultValue) {
192
192
  }
193
193
  return children;
194
194
  },
195
+ // React-compatible Consumer: <Context.Consumer>{value => ...}</Context.Consumer>
196
+ Consumer: ({ children }) => {
197
+ const value = useContext(context);
198
+ return typeof children === 'function' ? children(value) : children;
199
+ },
195
200
  };
196
201
  return context;
197
202
  }
package/src/reactive.js CHANGED
@@ -4,6 +4,15 @@
4
4
  // Dev-mode flag — build tools can dead-code-eliminate when false
5
5
  export const __DEV__ = typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production' || true;
6
6
 
7
+ // DevTools hooks — set by what-devtools when installed.
8
+ // These are no-ops in production (dead-code eliminated with __DEV__).
9
+ export let __devtools = null;
10
+
11
+ /** @internal Install devtools hooks. Called by what-devtools. */
12
+ export function __setDevToolsHooks(hooks) {
13
+ if (__DEV__) __devtools = hooks;
14
+ }
15
+
7
16
  let currentEffect = null;
8
17
  let currentRoot = null;
9
18
  let batchDepth = 0;
@@ -31,6 +40,7 @@ export function signal(initial) {
31
40
  const nextVal = typeof args[0] === 'function' ? args[0](value) : args[0];
32
41
  if (Object.is(value, nextVal)) return;
33
42
  value = nextVal;
43
+ if (__DEV__ && __devtools) __devtools.onSignalUpdate(sig);
34
44
  notify(subs);
35
45
  }
36
46
 
@@ -38,6 +48,7 @@ export function signal(initial) {
38
48
  const nextVal = typeof next === 'function' ? next(value) : next;
39
49
  if (Object.is(value, nextVal)) return;
40
50
  value = nextVal;
51
+ if (__DEV__ && __devtools) __devtools.onSignalUpdate(sig);
41
52
  notify(subs);
42
53
  };
43
54
 
@@ -48,6 +59,10 @@ export function signal(initial) {
48
59
  };
49
60
 
50
61
  sig._signal = true;
62
+
63
+ // Notify devtools of signal creation
64
+ if (__DEV__ && __devtools) __devtools.onSignalCreate(sig);
65
+
51
66
  return sig;
52
67
  }
53
68
 
@@ -130,7 +145,7 @@ export function batch(fn) {
130
145
  // --- Internals ---
131
146
 
132
147
  function _createEffect(fn, lazy) {
133
- return {
148
+ const e = {
134
149
  fn,
135
150
  deps: [], // array of subscriber sets (cheaper than Set for typical 1-3 deps)
136
151
  lazy: lazy || false,
@@ -139,6 +154,8 @@ function _createEffect(fn, lazy) {
139
154
  _pending: false,
140
155
  _stable: false, // stable effects skip cleanup/re-subscribe on re-run
141
156
  };
157
+ if (__DEV__ && __devtools) __devtools.onEffectCreate(e);
158
+ return e;
142
159
  }
143
160
 
144
161
  function _runEffect(e) {
@@ -187,6 +204,7 @@ function _runEffect(e) {
187
204
 
188
205
  function _disposeEffect(e) {
189
206
  e.disposed = true;
207
+ if (__DEV__ && __devtools) __devtools.onEffectDispose(e);
190
208
  cleanup(e);
191
209
  // Run cleanup on dispose
192
210
  if (e._cleanup) {