what-core 0.1.0 → 0.2.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 +425 -0
- package/dist/animation.js +531 -0
- package/dist/components.js +272 -115
- package/dist/data.js +434 -0
- package/dist/dom.js +635 -424
- package/dist/form.js +441 -0
- package/dist/h.js +191 -138
- package/dist/head.js +59 -42
- package/dist/helpers.js +125 -83
- package/dist/hooks.js +224 -134
- package/dist/index.js +2 -2
- package/dist/reactive.js +150 -107
- package/dist/scheduler.js +241 -0
- package/dist/skeleton.js +363 -0
- package/dist/store.js +113 -55
- package/dist/testing.js +367 -0
- package/dist/what.js +2 -2
- package/index.d.ts +15 -0
- package/package.json +1 -1
- package/src/components.js +93 -0
- package/src/dom.js +47 -12
- package/src/index.js +2 -2
- package/src/store.js +23 -5
package/dist/reactive.js
CHANGED
|
@@ -1,124 +1,167 @@
|
|
|
1
|
+
// What Framework - Reactive Primitives
|
|
2
|
+
// Signals + Effects: fine-grained reactivity without virtual DOM overhead
|
|
3
|
+
|
|
1
4
|
let currentEffect = null;
|
|
2
5
|
let batchDepth = 0;
|
|
3
6
|
let pendingEffects = new Set();
|
|
7
|
+
|
|
8
|
+
// --- Signal ---
|
|
9
|
+
// A reactive value. Reading inside an effect auto-tracks the dependency.
|
|
10
|
+
// Writing triggers only the effects that depend on this signal.
|
|
11
|
+
|
|
4
12
|
export function signal(initial) {
|
|
5
|
-
let value = initial;
|
|
6
|
-
const subs = new Set();
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
13
|
+
let value = initial;
|
|
14
|
+
const subs = new Set();
|
|
15
|
+
|
|
16
|
+
function read() {
|
|
17
|
+
if (currentEffect) {
|
|
18
|
+
subs.add(currentEffect);
|
|
19
|
+
currentEffect.deps.add(subs); // Track reverse dep for cleanup
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
read.set = (next) => {
|
|
25
|
+
const nextVal = typeof next === 'function' ? next(value) : next;
|
|
26
|
+
if (Object.is(value, nextVal)) return;
|
|
27
|
+
value = nextVal;
|
|
28
|
+
notify(subs);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
read.peek = () => value;
|
|
32
|
+
|
|
33
|
+
read.subscribe = (fn) => {
|
|
34
|
+
return effect(() => fn(read()));
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
read._signal = true;
|
|
38
|
+
return read;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// --- Computed ---
|
|
42
|
+
// Derived signal. Lazy: only recomputes when a dependency changes AND it's read.
|
|
43
|
+
|
|
27
44
|
export function computed(fn) {
|
|
28
|
-
let value, dirty = true;
|
|
29
|
-
const subs = new Set();
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
currentEffect
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
45
|
+
let value, dirty = true;
|
|
46
|
+
const subs = new Set();
|
|
47
|
+
|
|
48
|
+
const inner = _createEffect(() => {
|
|
49
|
+
value = fn();
|
|
50
|
+
dirty = false;
|
|
51
|
+
notify(subs);
|
|
52
|
+
}, { lazy: true });
|
|
53
|
+
|
|
54
|
+
function read() {
|
|
55
|
+
if (currentEffect) {
|
|
56
|
+
subs.add(currentEffect);
|
|
57
|
+
currentEffect.deps.add(subs);
|
|
58
|
+
}
|
|
59
|
+
if (dirty) _runEffect(inner);
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
inner._onNotify = () => { dirty = true; };
|
|
64
|
+
|
|
65
|
+
read._signal = true;
|
|
66
|
+
read.peek = () => {
|
|
67
|
+
if (dirty) _runEffect(inner);
|
|
68
|
+
return value;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
return read;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// --- Effect ---
|
|
75
|
+
// Runs a function, auto-tracking signal reads. Re-runs when deps change.
|
|
76
|
+
// Returns a dispose function.
|
|
77
|
+
|
|
51
78
|
export function effect(fn) {
|
|
52
|
-
const e = _createEffect(fn);
|
|
53
|
-
_runEffect(e);
|
|
54
|
-
return () => _disposeEffect(e);
|
|
55
|
-
}
|
|
79
|
+
const e = _createEffect(fn);
|
|
80
|
+
_runEffect(e);
|
|
81
|
+
return () => _disposeEffect(e);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// --- Batch ---
|
|
85
|
+
// Group multiple signal writes; effects run once at the end.
|
|
86
|
+
|
|
56
87
|
export function batch(fn) {
|
|
57
|
-
batchDepth++;
|
|
58
|
-
try {
|
|
59
|
-
fn();
|
|
60
|
-
} finally {
|
|
61
|
-
batchDepth--;
|
|
62
|
-
if (batchDepth === 0) flush();
|
|
63
|
-
}
|
|
64
|
-
}
|
|
88
|
+
batchDepth++;
|
|
89
|
+
try {
|
|
90
|
+
fn();
|
|
91
|
+
} finally {
|
|
92
|
+
batchDepth--;
|
|
93
|
+
if (batchDepth === 0) flush();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// --- Internals ---
|
|
98
|
+
|
|
65
99
|
function _createEffect(fn, opts = {}) {
|
|
66
|
-
return {
|
|
67
|
-
fn,
|
|
68
|
-
deps: new Set(),
|
|
69
|
-
lazy: opts.lazy || false,
|
|
70
|
-
_onNotify: null,
|
|
71
|
-
disposed: false,
|
|
72
|
-
};
|
|
73
|
-
}
|
|
100
|
+
return {
|
|
101
|
+
fn,
|
|
102
|
+
deps: new Set(), // subscriber sets this effect belongs to
|
|
103
|
+
lazy: opts.lazy || false,
|
|
104
|
+
_onNotify: null,
|
|
105
|
+
disposed: false,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
74
109
|
function _runEffect(e) {
|
|
75
|
-
if (e.disposed) return;
|
|
76
|
-
cleanup(e);
|
|
77
|
-
const prev = currentEffect;
|
|
78
|
-
currentEffect = e;
|
|
79
|
-
try {
|
|
80
|
-
e.fn();
|
|
81
|
-
} finally {
|
|
82
|
-
currentEffect = prev;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
110
|
+
if (e.disposed) return;
|
|
111
|
+
cleanup(e);
|
|
112
|
+
const prev = currentEffect;
|
|
113
|
+
currentEffect = e;
|
|
114
|
+
try {
|
|
115
|
+
e.fn();
|
|
116
|
+
} finally {
|
|
117
|
+
currentEffect = prev;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
85
121
|
function _disposeEffect(e) {
|
|
86
|
-
e.disposed = true;
|
|
87
|
-
cleanup(e);
|
|
122
|
+
e.disposed = true;
|
|
123
|
+
cleanup(e);
|
|
88
124
|
}
|
|
125
|
+
|
|
89
126
|
function cleanup(e) {
|
|
90
|
-
for (const dep of e.deps) dep.delete(e);
|
|
91
|
-
e.deps.clear();
|
|
127
|
+
for (const dep of e.deps) dep.delete(e);
|
|
128
|
+
e.deps.clear();
|
|
92
129
|
}
|
|
130
|
+
|
|
93
131
|
function notify(subs) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (e.
|
|
98
|
-
e._onNotify
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
}
|
|
132
|
+
// Snapshot to avoid infinite loop when effects re-subscribe during run
|
|
133
|
+
const snapshot = [...subs];
|
|
134
|
+
for (const e of snapshot) {
|
|
135
|
+
if (e.disposed) continue;
|
|
136
|
+
if (e._onNotify) {
|
|
137
|
+
e._onNotify();
|
|
138
|
+
if (batchDepth > 0) pendingEffects.add(e);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (batchDepth > 0) {
|
|
142
|
+
pendingEffects.add(e);
|
|
143
|
+
} else {
|
|
144
|
+
_runEffect(e);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
109
149
|
function flush() {
|
|
110
|
-
const effects = [...pendingEffects];
|
|
111
|
-
pendingEffects.clear();
|
|
112
|
-
for (const e of effects) {
|
|
113
|
-
if (!e.disposed && !e._onNotify) _runEffect(e);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
150
|
+
const effects = [...pendingEffects];
|
|
151
|
+
pendingEffects.clear();
|
|
152
|
+
for (const e of effects) {
|
|
153
|
+
if (!e.disposed && !e._onNotify) _runEffect(e);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// --- Untrack ---
|
|
158
|
+
// Read signals without subscribing
|
|
116
159
|
export function untrack(fn) {
|
|
117
|
-
const prev = currentEffect;
|
|
118
|
-
currentEffect = null;
|
|
119
|
-
try {
|
|
120
|
-
return fn();
|
|
121
|
-
} finally {
|
|
122
|
-
currentEffect = prev;
|
|
160
|
+
const prev = currentEffect;
|
|
161
|
+
currentEffect = null;
|
|
162
|
+
try {
|
|
163
|
+
return fn();
|
|
164
|
+
} finally {
|
|
165
|
+
currentEffect = prev;
|
|
166
|
+
}
|
|
123
167
|
}
|
|
124
|
-
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
// What Framework - DOM Scheduler
|
|
2
|
+
// Batches DOM reads and writes to prevent layout thrashing.
|
|
3
|
+
// Inspired by fastdom but integrated with our reactive system.
|
|
4
|
+
|
|
5
|
+
// Queue phases: reads run first, then writes
|
|
6
|
+
const readQueue = [];
|
|
7
|
+
const writeQueue = [];
|
|
8
|
+
let scheduled = false;
|
|
9
|
+
|
|
10
|
+
// --- Schedule a DOM read operation ---
|
|
11
|
+
// Reads should be batched together and run before writes
|
|
12
|
+
// to avoid forced synchronous layouts.
|
|
13
|
+
//
|
|
14
|
+
// Example:
|
|
15
|
+
// scheduleRead(() => {
|
|
16
|
+
// const height = element.offsetHeight; // Read
|
|
17
|
+
// scheduleWrite(() => {
|
|
18
|
+
// element.style.height = height + 'px'; // Write
|
|
19
|
+
// });
|
|
20
|
+
// });
|
|
21
|
+
|
|
22
|
+
export function scheduleRead(fn) {
|
|
23
|
+
readQueue.push(fn);
|
|
24
|
+
schedule();
|
|
25
|
+
return () => {
|
|
26
|
+
const idx = readQueue.indexOf(fn);
|
|
27
|
+
if (idx !== -1) readQueue.splice(idx, 1);
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// --- Schedule a DOM write operation ---
|
|
32
|
+
// Writes are batched and run after all reads complete.
|
|
33
|
+
|
|
34
|
+
export function scheduleWrite(fn) {
|
|
35
|
+
writeQueue.push(fn);
|
|
36
|
+
schedule();
|
|
37
|
+
return () => {
|
|
38
|
+
const idx = writeQueue.indexOf(fn);
|
|
39
|
+
if (idx !== -1) writeQueue.splice(idx, 1);
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// --- Flush all queued operations immediately ---
|
|
44
|
+
// Useful when you need synchronous DOM access.
|
|
45
|
+
|
|
46
|
+
export function flushScheduler() {
|
|
47
|
+
// Run all reads first
|
|
48
|
+
while (readQueue.length > 0) {
|
|
49
|
+
const fn = readQueue.shift();
|
|
50
|
+
try { fn(); } catch (e) { console.error('[what] Scheduler read error:', e); }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Then all writes
|
|
54
|
+
while (writeQueue.length > 0) {
|
|
55
|
+
const fn = writeQueue.shift();
|
|
56
|
+
try { fn(); } catch (e) { console.error('[what] Scheduler write error:', e); }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
scheduled = false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// --- Internal scheduling ---
|
|
63
|
+
|
|
64
|
+
function schedule() {
|
|
65
|
+
if (scheduled) return;
|
|
66
|
+
scheduled = true;
|
|
67
|
+
requestAnimationFrame(flushScheduler);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// --- Measure helper ---
|
|
71
|
+
// Read a layout property without causing thrashing.
|
|
72
|
+
// Returns a promise that resolves with the value.
|
|
73
|
+
|
|
74
|
+
export function measure(fn) {
|
|
75
|
+
return new Promise(resolve => {
|
|
76
|
+
scheduleRead(() => {
|
|
77
|
+
resolve(fn());
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// --- Mutate helper ---
|
|
83
|
+
// Write to DOM without causing thrashing.
|
|
84
|
+
// Returns a promise that resolves when the write is done.
|
|
85
|
+
|
|
86
|
+
export function mutate(fn) {
|
|
87
|
+
return new Promise(resolve => {
|
|
88
|
+
scheduleWrite(() => {
|
|
89
|
+
fn();
|
|
90
|
+
resolve();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// --- useScheduledEffect ---
|
|
96
|
+
// Effect that automatically batches DOM operations.
|
|
97
|
+
|
|
98
|
+
import { effect } from './reactive.js';
|
|
99
|
+
|
|
100
|
+
export function useScheduledEffect(readFn, writeFn) {
|
|
101
|
+
return effect(() => {
|
|
102
|
+
scheduleRead(() => {
|
|
103
|
+
const data = readFn();
|
|
104
|
+
if (writeFn) {
|
|
105
|
+
scheduleWrite(() => writeFn(data));
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// --- Animation frame helper ---
|
|
112
|
+
// Like requestAnimationFrame but returns a cancellable promise.
|
|
113
|
+
|
|
114
|
+
export function nextFrame() {
|
|
115
|
+
let cancel;
|
|
116
|
+
const promise = new Promise((resolve, reject) => {
|
|
117
|
+
const id = requestAnimationFrame(resolve);
|
|
118
|
+
cancel = () => {
|
|
119
|
+
cancelAnimationFrame(id);
|
|
120
|
+
reject(new Error('Cancelled'));
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
promise.cancel = cancel;
|
|
124
|
+
return promise;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// --- Debounced RAF ---
|
|
128
|
+
// Only runs the latest callback once per frame.
|
|
129
|
+
|
|
130
|
+
const debouncedCallbacks = new Map();
|
|
131
|
+
|
|
132
|
+
export function raf(key, fn) {
|
|
133
|
+
if (debouncedCallbacks.has(key)) {
|
|
134
|
+
// Replace callback, don't schedule new frame
|
|
135
|
+
debouncedCallbacks.set(key, fn);
|
|
136
|
+
} else {
|
|
137
|
+
debouncedCallbacks.set(key, fn);
|
|
138
|
+
requestAnimationFrame(() => {
|
|
139
|
+
const callback = debouncedCallbacks.get(key);
|
|
140
|
+
debouncedCallbacks.delete(key);
|
|
141
|
+
if (callback) callback();
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// --- Resize Observer helper ---
|
|
147
|
+
// Batched resize observations.
|
|
148
|
+
|
|
149
|
+
const resizeObservers = new WeakMap();
|
|
150
|
+
let sharedResizeObserver = null;
|
|
151
|
+
|
|
152
|
+
export function onResize(element, callback) {
|
|
153
|
+
if (typeof ResizeObserver === 'undefined') {
|
|
154
|
+
// Fallback: just call once
|
|
155
|
+
callback(element.getBoundingClientRect());
|
|
156
|
+
return () => {};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!sharedResizeObserver) {
|
|
160
|
+
sharedResizeObserver = new ResizeObserver(entries => {
|
|
161
|
+
scheduleRead(() => {
|
|
162
|
+
for (const entry of entries) {
|
|
163
|
+
const cb = resizeObservers.get(entry.target);
|
|
164
|
+
if (cb) {
|
|
165
|
+
cb(entry.contentRect);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
resizeObservers.set(element, callback);
|
|
173
|
+
sharedResizeObserver.observe(element);
|
|
174
|
+
|
|
175
|
+
return () => {
|
|
176
|
+
resizeObservers.delete(element);
|
|
177
|
+
sharedResizeObserver.unobserve(element);
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// --- Intersection Observer helper ---
|
|
182
|
+
// Batched intersection observations.
|
|
183
|
+
|
|
184
|
+
export function onIntersect(element, callback, options = {}) {
|
|
185
|
+
if (typeof IntersectionObserver === 'undefined') {
|
|
186
|
+
// Fallback: assume visible
|
|
187
|
+
callback({ isIntersecting: true, intersectionRatio: 1 });
|
|
188
|
+
return () => {};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const observer = new IntersectionObserver(entries => {
|
|
192
|
+
scheduleRead(() => {
|
|
193
|
+
for (const entry of entries) {
|
|
194
|
+
callback(entry);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
}, options);
|
|
198
|
+
|
|
199
|
+
observer.observe(element);
|
|
200
|
+
|
|
201
|
+
return () => observer.disconnect();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// --- Smooth scrolling with scheduler ---
|
|
205
|
+
|
|
206
|
+
export function smoothScrollTo(element, options = {}) {
|
|
207
|
+
const { duration = 300, easing = t => t * (2 - t) } = options;
|
|
208
|
+
|
|
209
|
+
return new Promise(resolve => {
|
|
210
|
+
let startY;
|
|
211
|
+
let targetY;
|
|
212
|
+
let startTime;
|
|
213
|
+
|
|
214
|
+
scheduleRead(() => {
|
|
215
|
+
startY = window.scrollY;
|
|
216
|
+
const rect = element.getBoundingClientRect();
|
|
217
|
+
targetY = startY + rect.top;
|
|
218
|
+
startTime = performance.now();
|
|
219
|
+
tick();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
function tick() {
|
|
223
|
+
scheduleRead(() => {
|
|
224
|
+
const elapsed = performance.now() - startTime;
|
|
225
|
+
const progress = Math.min(elapsed / duration, 1);
|
|
226
|
+
const easedProgress = easing(progress);
|
|
227
|
+
const currentY = startY + (targetY - startY) * easedProgress;
|
|
228
|
+
|
|
229
|
+
scheduleWrite(() => {
|
|
230
|
+
window.scrollTo(0, currentY);
|
|
231
|
+
|
|
232
|
+
if (progress < 1) {
|
|
233
|
+
requestAnimationFrame(tick);
|
|
234
|
+
} else {
|
|
235
|
+
resolve();
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
}
|