what-core 0.3.0 → 0.4.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.
- package/dist/a11y.js +22 -7
- package/dist/animation.js +9 -1
- package/dist/components.js +61 -23
- package/dist/data.js +253 -59
- package/dist/dom.js +196 -24
- package/dist/form.js +112 -44
- package/dist/helpers.js +73 -10
- package/dist/hooks.js +63 -22
- package/dist/index.js +6 -2
- package/dist/reactive.js +189 -29
- package/dist/render.js +716 -0
- package/dist/scheduler.js +10 -5
- package/dist/store.js +18 -8
- package/package.json +10 -1
- package/src/a11y.js +22 -7
- package/src/animation.js +9 -1
- package/src/components.js +61 -23
- package/src/data.js +253 -59
- package/src/dom.js +222 -24
- package/src/form.js +112 -44
- package/src/h.js +3 -0
- package/src/helpers.js +73 -10
- package/src/hooks.js +63 -22
- package/src/index.js +6 -2
- package/src/jsx-dev-runtime.js +19 -0
- package/src/jsx-runtime.js +21 -0
- package/src/reactive.js +208 -39
- package/src/render.js +716 -0
- package/src/scheduler.js +10 -5
- package/src/store.js +18 -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
|
@@ -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:
|
|
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);
|
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 ---
|