what-core 0.2.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
@@ -2,8 +2,17 @@
2
2
  // Springs, tweens, gestures, and transition helpers
3
3
 
4
4
  import { signal, effect, untrack, batch } from './reactive.js';
5
+ import { getCurrentComponent } from './dom.js';
5
6
  import { scheduleRead, scheduleWrite } from './scheduler.js';
6
7
 
8
+ // Create an effect scoped to the current component's lifecycle
9
+ function scopedEffect(fn) {
10
+ const ctx = getCurrentComponent?.();
11
+ const dispose = effect(fn);
12
+ if (ctx) ctx.effects.push(dispose);
13
+ return dispose;
14
+ }
15
+
7
16
  // --- Spring Animation ---
8
17
  // Physics-based animation with natural feel
9
18
 
@@ -93,6 +102,13 @@ export function spring(initialValue, options = {}) {
93
102
  });
94
103
  }
95
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
+
96
112
  return {
97
113
  current: () => current(),
98
114
  target: () => target(),
@@ -234,6 +250,7 @@ export function useGesture(element, handlers = {}) {
234
250
  onSwipe,
235
251
  onTap,
236
252
  onLongPress,
253
+ preventDefault = false, // Set to true to allow e.preventDefault() in touch handlers
237
254
  } = handlers;
238
255
 
239
256
  const state = {
@@ -388,14 +405,14 @@ export function useGesture(element, handlers = {}) {
388
405
  // Attach listeners
389
406
  if (typeof element === 'function') {
390
407
  // Ref function
391
- effect(() => {
408
+ scopedEffect(() => {
392
409
  const el = untrack(element);
393
410
  if (!el) return;
394
411
  return attachListeners(el);
395
412
  });
396
413
  } else if (element?.current !== undefined) {
397
414
  // Ref object
398
- effect(() => {
415
+ scopedEffect(() => {
399
416
  const el = element.current;
400
417
  if (!el) return;
401
418
  return attachListeners(el);
@@ -406,7 +423,7 @@ export function useGesture(element, handlers = {}) {
406
423
 
407
424
  function attachListeners(el) {
408
425
  el.addEventListener('mousedown', handleStart);
409
- el.addEventListener('touchstart', handleStart, { passive: true });
426
+ el.addEventListener('touchstart', handleStart, { passive: !preventDefault });
410
427
  window.addEventListener('mousemove', handleMove);
411
428
  window.addEventListener('touchmove', handlePinchMove);
412
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 ---