what-core 0.3.0 → 0.4.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/dist/a11y.js CHANGED
@@ -3,6 +3,7 @@
3
3
 
4
4
  import { signal, effect } from './reactive.js';
5
5
  import { h } from './h.js';
6
+ import { getCurrentComponent } from './dom.js';
6
7
 
7
8
  // --- Focus Management ---
8
9
 
@@ -105,7 +106,8 @@ export function FocusTrap({ children, active = true }) {
105
106
  const containerRef = { current: null };
106
107
  const trap = useFocusTrap(containerRef);
107
108
 
108
- effect(() => {
109
+ // Scope the effect to the component lifecycle so it disposes on unmount
110
+ const dispose = effect(() => {
109
111
  if (active) {
110
112
  const cleanup = trap.activate();
111
113
  return () => {
@@ -115,6 +117,13 @@ export function FocusTrap({ children, active = true }) {
115
117
  }
116
118
  });
117
119
 
120
+ // Register cleanup on component context
121
+ const ctx = getCurrentComponent?.();
122
+ if (ctx) {
123
+ ctx._cleanupCallbacks = ctx._cleanupCallbacks || [];
124
+ ctx._cleanupCallbacks.push(dispose);
125
+ }
126
+
118
127
  return h('div', { ref: containerRef }, children);
119
128
  }
120
129
 
@@ -269,20 +278,26 @@ export function useAriaChecked(initialChecked = false) {
269
278
  // --- Roving Tab Index ---
270
279
  // For keyboard navigation in lists, toolbars, etc.
271
280
 
272
- export function useRovingTabIndex(itemCount) {
281
+ export function useRovingTabIndex(itemCountOrSignal) {
282
+ // Accept either a static number or a signal/getter for dynamic lists
283
+ const getCount = typeof itemCountOrSignal === 'function'
284
+ ? itemCountOrSignal
285
+ : () => itemCountOrSignal;
273
286
  const focusIndex = signal(0);
274
287
 
275
288
  function handleKeyDown(e) {
289
+ const count = getCount();
290
+ if (count <= 0) return;
276
291
  switch (e.key) {
277
292
  case 'ArrowDown':
278
293
  case 'ArrowRight':
279
294
  e.preventDefault();
280
- focusIndex.set((focusIndex.peek() + 1) % itemCount);
295
+ focusIndex.set((focusIndex.peek() + 1) % count);
281
296
  break;
282
297
  case 'ArrowUp':
283
298
  case 'ArrowLeft':
284
299
  e.preventDefault();
285
- focusIndex.set((focusIndex.peek() - 1 + itemCount) % itemCount);
300
+ focusIndex.set((focusIndex.peek() - 1 + count) % count);
286
301
  break;
287
302
  case 'Home':
288
303
  e.preventDefault();
@@ -290,7 +305,7 @@ export function useRovingTabIndex(itemCount) {
290
305
  break;
291
306
  case 'End':
292
307
  e.preventDefault();
293
- focusIndex.set(itemCount - 1);
308
+ focusIndex.set(count - 1);
294
309
  break;
295
310
  }
296
311
  }
@@ -343,8 +358,8 @@ export function LiveRegion({ children, priority = 'polite', atomic = true }) {
343
358
  let idCounter = 0;
344
359
 
345
360
  export function useId(prefix = 'what') {
346
- const id = signal(`${prefix}-${++idCounter}`);
347
- return () => id();
361
+ const id = `${prefix}-${++idCounter}`;
362
+ return () => id;
348
363
  }
349
364
 
350
365
  export function useIds(count, prefix = 'what') {
package/dist/animation.js CHANGED
@@ -102,6 +102,13 @@ export function spring(initialValue, options = {}) {
102
102
  });
103
103
  }
104
104
 
105
+ // Register stop() as cleanup if inside a component
106
+ const ctx = getCurrentComponent?.();
107
+ if (ctx) {
108
+ ctx._cleanupCallbacks = ctx._cleanupCallbacks || [];
109
+ ctx._cleanupCallbacks.push(stop);
110
+ }
111
+
105
112
  return {
106
113
  current: () => current(),
107
114
  target: () => target(),
@@ -243,6 +250,7 @@ export function useGesture(element, handlers = {}) {
243
250
  onSwipe,
244
251
  onTap,
245
252
  onLongPress,
253
+ preventDefault = false, // Set to true to allow e.preventDefault() in touch handlers
246
254
  } = handlers;
247
255
 
248
256
  const state = {
@@ -415,7 +423,7 @@ export function useGesture(element, handlers = {}) {
415
423
 
416
424
  function attachListeners(el) {
417
425
  el.addEventListener('mousedown', handleStart);
418
- el.addEventListener('touchstart', handleStart, { passive: true });
426
+ el.addEventListener('touchstart', handleStart, { passive: !preventDefault });
419
427
  window.addEventListener('mousemove', handleMove);
420
428
  window.addEventListener('touchmove', handlePinchMove);
421
429
  window.addEventListener('touchmove', handleMove);
@@ -2,32 +2,54 @@
2
2
  // memo, lazy, Suspense, ErrorBoundary
3
3
 
4
4
  import { h } from './h.js';
5
- import { signal, effect, untrack } from './reactive.js';
5
+ import { signal, effect, untrack, __DEV__ } from './reactive.js';
6
6
 
7
- // Error boundary context - components can register their error handlers here
8
- export const errorBoundaryStack = [];
7
+ // Legacy errorBoundaryStack removed — tree-based resolution via _parentCtx._errorBoundary
8
+ // is now the only mechanism. See reportError() below.
9
9
 
10
10
  // --- memo ---
11
- // Skip re-render if props haven't changed (shallow compare by default).
11
+ // Skip re-render when parent re-renders with unchanged props.
12
+ // Signal-safe: when an internal signal changes, the component always re-renders
13
+ // (memo never blocks signal-triggered updates).
14
+ //
15
+ // How it works:
16
+ // - In our architecture, Component(propsSignal()) is called inside an effect.
17
+ // - When parent re-renders: propsSignal is set to a new object → props is a NEW reference
18
+ // - When an internal signal changes: propsSignal unchanged → props is the SAME reference
19
+ // - memo only skips when: props is a new reference AND structurally equal to previous
12
20
 
13
21
  export function memo(Component, areEqual) {
14
22
  const compare = areEqual || shallowEqual;
15
- let prevProps = null;
16
- let prevResult = null;
17
23
 
18
24
  function MemoWrapper(props) {
19
- if (prevProps && compare(prevProps, props)) {
20
- return prevResult;
25
+ const ctx = _getCurrentComponent?.();
26
+ if (ctx && ctx._memoResult !== undefined) {
27
+ if (props === ctx._memoPropsRef) {
28
+ // Same reference → signal-triggered re-render → must re-run
29
+ // to pick up new signal values (do NOT skip)
30
+ } else if (compare(ctx._memoProps, props)) {
31
+ // New reference but structurally equal → parent-triggered, safe to skip
32
+ ctx._memoPropsRef = props;
33
+ return ctx._memoResult;
34
+ }
35
+ }
36
+ if (ctx) {
37
+ ctx._memoPropsRef = props;
38
+ ctx._memoProps = { ...props };
21
39
  }
22
- prevProps = { ...props };
23
- prevResult = Component(props);
24
- return prevResult;
40
+ const result = Component(props);
41
+ if (ctx) ctx._memoResult = result;
42
+ return result;
25
43
  }
26
44
 
27
45
  MemoWrapper.displayName = `Memo(${Component.name || 'Anonymous'})`;
28
46
  return MemoWrapper;
29
47
  }
30
48
 
49
+ // Injected by dom.js
50
+ let _getCurrentComponent = null;
51
+ export function _injectGetCurrentComponent(fn) { _getCurrentComponent = fn; }
52
+
31
53
  function shallowEqual(a, b) {
32
54
  if (a === b) return true;
33
55
  const keysA = Object.keys(a);
@@ -137,21 +159,23 @@ export function ErrorBoundary({ fallback, children, onError }) {
137
159
  };
138
160
  }
139
161
 
140
- // Helper to get current error boundary
141
- export function getCurrentErrorBoundary() {
142
- return errorBoundaryStack[errorBoundaryStack.length - 1] || null;
143
- }
144
-
145
162
  // Helper to report error to nearest boundary
146
- export function reportError(error) {
147
- const boundary = getCurrentErrorBoundary();
148
- if (boundary) {
149
- boundary.handleError(error);
150
- return true;
163
+ // Walks the component context tree (not a runtime stack) so async errors are caught
164
+ export function reportError(error, startCtx) {
165
+ // Walk up the _parentCtx chain to find the nearest _errorBoundary
166
+ let ctx = startCtx || _getCurrentComponent?.();
167
+ while (ctx) {
168
+ if (ctx._errorBoundary) {
169
+ ctx._errorBoundary(error);
170
+ return true;
171
+ }
172
+ ctx = ctx._parentCtx;
151
173
  }
152
174
  return false;
153
175
  }
154
176
 
177
+ // _getCurrentComponent is already declared above and injected via _injectGetCurrentComponent
178
+
155
179
  // --- Show ---
156
180
  // Conditional rendering component. Cleaner than ternaries.
157
181
 
@@ -171,11 +195,25 @@ export function For({ each, fallback = null, children }) {
171
195
  // children should be a function (item, index) => vnode
172
196
  const renderFn = Array.isArray(children) ? children[0] : children;
173
197
  if (typeof renderFn !== 'function') {
174
- console.warn('For: children must be a function');
198
+ console.warn('[what] For: children must be a render function, e.g. <For each={items}>{(item) => ...}</For>');
175
199
  return fallback;
176
200
  }
177
201
 
178
- return list.map((item, index) => renderFn(item, index));
202
+ return list.map((item, index) => {
203
+ const vnode = renderFn(item, index);
204
+ // Auto-detect keys for efficient keyed reconciliation
205
+ if (vnode && typeof vnode === 'object' && vnode.key == null) {
206
+ if (item != null && typeof item === 'object') {
207
+ // Use item.id or item.key if available
208
+ if (item.id != null) vnode.key = item.id;
209
+ else if (item.key != null) vnode.key = item.key;
210
+ } else if (typeof item === 'string' || typeof item === 'number') {
211
+ // Primitive items can be their own key
212
+ vnode.key = item;
213
+ }
214
+ }
215
+ return vnode;
216
+ });
179
217
  }
180
218
 
181
219
  // --- Switch / Match ---