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 +22 -7
- package/dist/animation.js +20 -3
- package/dist/components.js +61 -23
- package/dist/data.js +272 -68
- package/dist/dom.js +325 -89
- package/dist/form.js +112 -44
- package/dist/helpers.js +73 -10
- package/dist/hooks.js +75 -22
- package/dist/index.js +6 -2
- package/dist/reactive.js +202 -28
- package/dist/render.js +716 -0
- package/dist/scheduler.js +10 -5
- package/dist/store.js +19 -8
- package/package.json +4 -1
- package/src/a11y.js +22 -7
- package/src/animation.js +20 -3
- package/src/components.js +61 -23
- package/src/data.js +272 -68
- package/src/dom.js +325 -89
- package/src/form.js +112 -44
- package/src/helpers.js +73 -10
- package/src/hooks.js +75 -22
- package/src/index.js +6 -2
- package/src/reactive.js +202 -28
- package/src/render.js +716 -0
- package/src/scheduler.js +10 -5
- package/src/store.js +19 -8
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(
|
|
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) %
|
|
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 +
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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);
|
package/dist/components.js
CHANGED
|
@@ -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
|
-
//
|
|
8
|
-
|
|
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
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
return
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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) =>
|
|
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 ---
|