what-core 0.1.1 → 0.3.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/helpers.js CHANGED
@@ -1,98 +1,140 @@
1
+ // What Framework - Helpers & Utilities
2
+ // Commonly needed patterns, zero overhead.
3
+
1
4
  import { signal, effect, computed, batch } from './reactive.js';
5
+
6
+ // --- show(condition, vnode) ---
7
+ // Conditional rendering. More readable than ternary.
2
8
  export function show(condition, vnode, fallback = null) {
3
- return condition ? vnode : fallback;
9
+ return condition ? vnode : fallback;
4
10
  }
11
+
12
+ // --- each(list, fn) ---
13
+ // Keyed list rendering. Optimized for reconciliation.
5
14
  export function each(list, fn, keyFn) {
6
- if (!list || list.length === 0) return [];
7
- return list.map((item, index) => {
8
- const vnode = fn(item, index);
9
- if (keyFn && vnode && typeof vnode === 'object') {
10
- vnode.key = keyFn(item, index);
11
- }
12
- return vnode;
13
- });
14
- }
15
+ if (!list || list.length === 0) return [];
16
+ return list.map((item, index) => {
17
+ const vnode = fn(item, index);
18
+ if (keyFn && vnode && typeof vnode === 'object') {
19
+ vnode.key = keyFn(item, index);
20
+ }
21
+ return vnode;
22
+ });
23
+ }
24
+
25
+ // --- cls(...args) ---
26
+ // Conditional class names. Like clsx but tiny.
27
+ // cls('base', condition && 'active', { hidden: isHidden, bold: isBold })
15
28
  export function cls(...args) {
16
- const classes = [];
17
- for (const arg of args) {
18
- if (!arg) continue;
19
- if (typeof arg === 'string') {
20
- classes.push(arg);
21
- } else if (typeof arg === 'object') {
22
- for (const [key, val] of Object.entries(arg)) {
23
- if (val) classes.push(key);
24
- }
25
- }
26
- }
27
- return classes.join(' ');
28
- }
29
+ const classes = [];
30
+ for (const arg of args) {
31
+ if (!arg) continue;
32
+ if (typeof arg === 'string') {
33
+ classes.push(arg);
34
+ } else if (typeof arg === 'object') {
35
+ for (const [key, val] of Object.entries(arg)) {
36
+ if (val) classes.push(key);
37
+ }
38
+ }
39
+ }
40
+ return classes.join(' ');
41
+ }
42
+
43
+ // --- style(obj) ---
44
+ // Convert a style object to a CSS string for SSR.
29
45
  export function style(obj) {
30
- if (typeof obj === 'string') return obj;
31
- return Object.entries(obj)
32
- .filter(([, v]) => v != null && v !== '')
33
- .map(([k, v]) => `${camelToKebab(k)}:${v}`)
34
- .join(';');
46
+ if (typeof obj === 'string') return obj;
47
+ return Object.entries(obj)
48
+ .filter(([, v]) => v != null && v !== '')
49
+ .map(([k, v]) => `${camelToKebab(k)}:${v}`)
50
+ .join(';');
35
51
  }
52
+
36
53
  function camelToKebab(str) {
37
- return str.replace(/([A-Z])/g, '-$1').toLowerCase();
54
+ return str.replace(/([A-Z])/g, '-$1').toLowerCase();
38
55
  }
56
+
57
+ // --- debounce ---
58
+ // Debounced signal updates.
39
59
  export function debounce(fn, ms) {
40
- let timer;
41
- return (...args) => {
42
- clearTimeout(timer);
43
- timer = setTimeout(() => fn(...args), ms);
44
- };
45
- }
60
+ let timer;
61
+ return (...args) => {
62
+ clearTimeout(timer);
63
+ timer = setTimeout(() => fn(...args), ms);
64
+ };
65
+ }
66
+
67
+ // --- throttle ---
46
68
  export function throttle(fn, ms) {
47
- let last = 0;
48
- return (...args) => {
49
- const now = Date.now();
50
- if (now - last >= ms) {
51
- last = now;
52
- fn(...args);
53
- }
54
- };
55
- }
69
+ let last = 0;
70
+ return (...args) => {
71
+ const now = Date.now();
72
+ if (now - last >= ms) {
73
+ last = now;
74
+ fn(...args);
75
+ }
76
+ };
77
+ }
78
+
79
+ // --- useMediaQuery ---
80
+ // Reactive media query. Returns a signal.
56
81
  export function useMediaQuery(query) {
57
- if (typeof window === 'undefined') return signal(false);
58
- const mq = window.matchMedia(query);
59
- const s = signal(mq.matches);
60
- mq.addEventListener('change', (e) => s.set(e.matches));
61
- return s;
62
- }
82
+ if (typeof window === 'undefined') return signal(false);
83
+ const mq = window.matchMedia(query);
84
+ const s = signal(mq.matches);
85
+ mq.addEventListener('change', (e) => s.set(e.matches));
86
+ return s;
87
+ }
88
+
89
+ // --- useLocalStorage ---
90
+ // Signal synced with localStorage.
63
91
  export function useLocalStorage(key, initial) {
64
- let stored;
65
- try {
66
- const raw = localStorage.getItem(key);
67
- stored = raw !== null ? JSON.parse(raw) : initial;
68
- } catch {
69
- stored = initial;
70
- }
71
- const s = signal(stored);
72
- effect(() => {
73
- try {
74
- localStorage.setItem(key, JSON.stringify(s()));
75
- } catch { }
76
- });
77
- if (typeof window !== 'undefined') {
78
- window.addEventListener('storage', (e) => {
79
- if (e.key === key && e.newValue !== null) {
80
- try { s.set(JSON.parse(e.newValue)); } catch {}
81
- }
82
- });
83
- }
84
- return s;
85
- }
92
+ let stored;
93
+ try {
94
+ const raw = localStorage.getItem(key);
95
+ stored = raw !== null ? JSON.parse(raw) : initial;
96
+ } catch {
97
+ stored = initial;
98
+ }
99
+
100
+ const s = signal(stored);
101
+
102
+ // Sync to localStorage on changes
103
+ effect(() => {
104
+ try {
105
+ localStorage.setItem(key, JSON.stringify(s()));
106
+ } catch { /* quota exceeded, etc */ }
107
+ });
108
+
109
+ // Listen for changes from other tabs
110
+ if (typeof window !== 'undefined') {
111
+ window.addEventListener('storage', (e) => {
112
+ if (e.key === key && e.newValue !== null) {
113
+ try { s.set(JSON.parse(e.newValue)); } catch {}
114
+ }
115
+ });
116
+ }
117
+
118
+ return s;
119
+ }
120
+
121
+ // --- portal ---
122
+ // Render children into a different DOM container.
86
123
  export function Portal({ target, children }) {
87
- if (typeof document === 'undefined') return null;
88
- const container = typeof target === 'string'
89
- ? document.querySelector(target)
90
- : target;
91
- if (!container) return null;
92
- return { tag: '__portal', props: { container }, children: Array.isArray(children) ? children : [children], _vnode: true };
93
- }
124
+ // In SSR, just return null (portals are client-only)
125
+ if (typeof document === 'undefined') return null;
126
+ const container = typeof target === 'string'
127
+ ? document.querySelector(target)
128
+ : target;
129
+ if (!container) return null;
130
+ // The DOM reconciler will mount children here
131
+ return { tag: '__portal', props: { container }, children: Array.isArray(children) ? children : [children], _vnode: true };
132
+ }
133
+
134
+ // --- Transition helper ---
135
+ // Animate elements in/out. Returns props to spread on the element.
94
136
  export function transition(name, active) {
95
- return {
96
- class: active ? `${name}-enter ${name}-enter-active` : `${name}-leave ${name}-leave-active`,
97
- };
98
- }
137
+ return {
138
+ class: active ? `${name}-enter ${name}-enter-active` : `${name}-leave ${name}-leave-active`,
139
+ };
140
+ }
package/dist/hooks.js CHANGED
@@ -1,156 +1,258 @@
1
+ // What Framework - Hooks
2
+ // React-familiar hooks backed by signals. Zero overhead when deps don't change.
3
+
1
4
  import { signal, computed, effect, batch, untrack } from './reactive.js';
2
- import { getCurrentComponent } from './dom.js';
5
+ import { getCurrentComponent, getComponentStack as _getComponentStack } from './dom.js';
6
+
3
7
  function getCtx() {
4
- const ctx = getCurrentComponent();
5
- if (!ctx) throw new Error('Hooks must be called inside a component');
6
- return ctx;
8
+ const ctx = getCurrentComponent();
9
+ if (!ctx) throw new Error('Hooks must be called inside a component');
10
+ return ctx;
7
11
  }
12
+
8
13
  function getHook(ctx) {
9
- const index = ctx.hookIndex++;
10
- return { index, exists: index < ctx.hooks.length };
14
+ const index = ctx.hookIndex++;
15
+ return { index, exists: index < ctx.hooks.length };
11
16
  }
17
+
18
+ // --- useState ---
19
+ // Returns [value, setter]. Setter triggers re-render of this component only.
20
+
12
21
  export function useState(initial) {
13
- const ctx = getCtx();
14
- const { index, exists } = getHook(ctx);
15
- if (!exists) {
16
- const s = signal(typeof initial === 'function' ? initial() : initial);
17
- ctx.hooks[index] = s;
18
- }
19
- const s = ctx.hooks[index];
20
- return [s(), s.set];
22
+ const ctx = getCtx();
23
+ const { index, exists } = getHook(ctx);
24
+
25
+ if (!exists) {
26
+ const s = signal(typeof initial === 'function' ? initial() : initial);
27
+ ctx.hooks[index] = s;
28
+ }
29
+
30
+ const s = ctx.hooks[index];
31
+ return [s(), s.set];
21
32
  }
33
+
34
+ // --- useSignal ---
35
+ // Returns the raw signal. More powerful: read with sig(), write with sig.set(v).
36
+ // Avoids array destructuring overhead.
37
+
22
38
  export function useSignal(initial) {
23
- const ctx = getCtx();
24
- const { index, exists } = getHook(ctx);
25
- if (!exists) {
26
- ctx.hooks[index] = signal(typeof initial === 'function' ? initial() : initial);
27
- }
28
- return ctx.hooks[index];
39
+ const ctx = getCtx();
40
+ const { index, exists } = getHook(ctx);
41
+
42
+ if (!exists) {
43
+ ctx.hooks[index] = signal(typeof initial === 'function' ? initial() : initial);
44
+ }
45
+
46
+ return ctx.hooks[index];
29
47
  }
48
+
49
+ // --- useComputed ---
50
+ // Derived value. Only recomputes when signal deps change.
51
+
30
52
  export function useComputed(fn) {
31
- const ctx = getCtx();
32
- const { index, exists } = getHook(ctx);
33
- if (!exists) {
34
- ctx.hooks[index] = computed(fn);
35
- }
36
- return ctx.hooks[index];
53
+ const ctx = getCtx();
54
+ const { index, exists } = getHook(ctx);
55
+
56
+ if (!exists) {
57
+ ctx.hooks[index] = computed(fn);
58
+ }
59
+
60
+ return ctx.hooks[index];
37
61
  }
62
+
63
+ // --- useEffect ---
64
+ // Side effect that runs after render. Cleanup function returned by fn is called
65
+ // before re-running and on unmount.
66
+
38
67
  export function useEffect(fn, deps) {
39
- const ctx = getCtx();
40
- const { index, exists } = getHook(ctx);
41
- if (!exists) {
42
- ctx.hooks[index] = { deps: undefined, cleanup: null };
43
- }
44
- const hook = ctx.hooks[index];
45
- if (depsChanged(hook.deps, deps)) {
46
- queueMicrotask(() => {
47
- if (ctx.disposed) return;
48
- if (hook.cleanup) hook.cleanup();
49
- hook.cleanup = fn() || null;
50
- });
51
- hook.deps = deps;
52
- }
68
+ const ctx = getCtx();
69
+ const { index, exists } = getHook(ctx);
70
+
71
+ if (!exists) {
72
+ ctx.hooks[index] = { deps: undefined, cleanup: null };
73
+ }
74
+
75
+ const hook = ctx.hooks[index];
76
+
77
+ if (depsChanged(hook.deps, deps)) {
78
+ // Schedule after current render
79
+ queueMicrotask(() => {
80
+ if (ctx.disposed) return;
81
+ if (hook.cleanup) hook.cleanup();
82
+ hook.cleanup = fn() || null;
83
+ });
84
+ hook.deps = deps;
85
+ }
53
86
  }
87
+
88
+ // --- useMemo ---
89
+ // Memoized value. Only recomputes when deps change.
90
+
54
91
  export function useMemo(fn, deps) {
55
- const ctx = getCtx();
56
- const { index, exists } = getHook(ctx);
57
- if (!exists) {
58
- ctx.hooks[index] = { value: undefined, deps: undefined };
59
- }
60
- const hook = ctx.hooks[index];
61
- if (depsChanged(hook.deps, deps)) {
62
- hook.value = fn();
63
- hook.deps = deps;
64
- }
65
- return hook.value;
92
+ const ctx = getCtx();
93
+ const { index, exists } = getHook(ctx);
94
+
95
+ if (!exists) {
96
+ ctx.hooks[index] = { value: undefined, deps: undefined };
97
+ }
98
+
99
+ const hook = ctx.hooks[index];
100
+
101
+ if (depsChanged(hook.deps, deps)) {
102
+ hook.value = fn();
103
+ hook.deps = deps;
104
+ }
105
+
106
+ return hook.value;
66
107
  }
108
+
109
+ // --- useCallback ---
110
+ // Memoized callback. Identity-stable when deps don't change.
111
+
67
112
  export function useCallback(fn, deps) {
68
- return useMemo(() => fn, deps);
113
+ return useMemo(() => fn, deps);
69
114
  }
115
+
116
+ // --- useRef ---
117
+ // Mutable ref object. Does NOT trigger re-renders.
118
+
70
119
  export function useRef(initial) {
71
- const ctx = getCtx();
72
- const { index, exists } = getHook(ctx);
73
- if (!exists) {
74
- ctx.hooks[index] = { current: initial };
75
- }
76
- return ctx.hooks[index];
120
+ const ctx = getCtx();
121
+ const { index, exists } = getHook(ctx);
122
+
123
+ if (!exists) {
124
+ ctx.hooks[index] = { current: initial };
125
+ }
126
+
127
+ return ctx.hooks[index];
77
128
  }
129
+
130
+ // --- useContext ---
131
+ // Read from the nearest Provider in the component tree, or the default value.
132
+
78
133
  export function useContext(context) {
79
- return context._value;
134
+ // Walk up the component stack to find the nearest provider for this context
135
+ const stack = _getComponentStack();
136
+ for (let i = stack.length - 1; i >= 0; i--) {
137
+ const ctx = stack[i];
138
+ if (ctx._contextValues && ctx._contextValues.has(context)) {
139
+ return ctx._contextValues.get(context);
140
+ }
141
+ }
142
+ return context._defaultValue;
80
143
  }
144
+
145
+ // --- createContext ---
146
+ // Tree-scoped context: Provider sets value for its subtree only.
147
+ // Multiple providers can coexist — each subtree sees its own value.
148
+
81
149
  export function createContext(defaultValue) {
82
- const ctx = {
83
- _value: defaultValue,
84
- Provider: ({ value, children }) => {
85
- ctx._value = value;
86
- return children;
87
- },
88
- };
89
- return ctx;
150
+ const context = {
151
+ _defaultValue: defaultValue,
152
+ Provider: ({ value, children }) => {
153
+ // Store context value on the current component's context
154
+ const ctx = getCtx();
155
+ if (!ctx._contextValues) ctx._contextValues = new Map();
156
+ ctx._contextValues.set(context, value);
157
+ return children;
158
+ },
159
+ };
160
+ return context;
90
161
  }
162
+
163
+ // --- useReducer ---
164
+ // State management with a reducer function (like React).
165
+
91
166
  export function useReducer(reducer, initialState, init) {
92
- const ctx = getCtx();
93
- const { index, exists } = getHook(ctx);
94
- if (!exists) {
95
- const initial = init ? init(initialState) : initialState;
96
- const s = signal(initial);
97
- const dispatch = (action) => {
98
- s.set(prev => reducer(prev, action));
99
- };
100
- ctx.hooks[index] = { signal: s, dispatch };
101
- }
102
- const hook = ctx.hooks[index];
103
- return [hook.signal(), hook.dispatch];
167
+ const ctx = getCtx();
168
+ const { index, exists } = getHook(ctx);
169
+
170
+ if (!exists) {
171
+ const initial = init ? init(initialState) : initialState;
172
+ const s = signal(initial);
173
+ const dispatch = (action) => {
174
+ s.set(prev => reducer(prev, action));
175
+ };
176
+ ctx.hooks[index] = { signal: s, dispatch };
177
+ }
178
+
179
+ const hook = ctx.hooks[index];
180
+ return [hook.signal(), hook.dispatch];
104
181
  }
182
+
183
+ // --- onMount ---
184
+ // Run callback once when component mounts. SolidJS-style lifecycle.
185
+
105
186
  export function onMount(fn) {
106
- const ctx = getCtx();
107
- if (!ctx._mounted) {
108
- ctx._mountCallbacks = ctx._mountCallbacks || [];
109
- ctx._mountCallbacks.push(fn);
110
- }
187
+ const ctx = getCtx();
188
+ if (!ctx.mounted) {
189
+ ctx._mountCallbacks = ctx._mountCallbacks || [];
190
+ ctx._mountCallbacks.push(fn);
191
+ }
111
192
  }
193
+
194
+ // --- onCleanup ---
195
+ // Register cleanup function to run when component unmounts.
196
+
112
197
  export function onCleanup(fn) {
113
- const ctx = getCtx();
114
- ctx._cleanupCallbacks = ctx._cleanupCallbacks || [];
115
- ctx._cleanupCallbacks.push(fn);
198
+ const ctx = getCtx();
199
+ ctx._cleanupCallbacks = ctx._cleanupCallbacks || [];
200
+ ctx._cleanupCallbacks.push(fn);
116
201
  }
202
+
203
+ // --- createResource ---
204
+ // Reactive data fetching primitive (SolidJS-style).
205
+ // Returns [data, { loading, error, refetch, mutate }]
206
+
117
207
  export function createResource(fetcher, options = {}) {
118
- const data = signal(options.initialValue ?? null);
119
- const loading = signal(!options.initialValue);
120
- const error = signal(null);
121
- let currentFetch = null;
122
- const refetch = async (source) => {
123
- loading.set(true);
124
- error.set(null);
125
- try {
126
- const fetchPromise = fetcher(source);
127
- currentFetch = fetchPromise;
128
- const result = await fetchPromise;
129
- if (currentFetch === fetchPromise) {
130
- data.set(result);
131
- loading.set(false);
132
- }
133
- } catch (e) {
134
- if (currentFetch === fetcher) {
135
- error.set(e);
136
- loading.set(false);
137
- }
138
- }
139
- };
140
- const mutate = (value) => {
141
- data.set(typeof value === 'function' ? value(data()) : value);
142
- };
143
- if (!options.initialValue) {
144
- refetch(options.source);
145
- }
146
- return [data, { loading, error, refetch, mutate }];
208
+ const data = signal(options.initialValue ?? null);
209
+ const loading = signal(!options.initialValue);
210
+ const error = signal(null);
211
+
212
+ let currentFetch = null;
213
+
214
+ const refetch = async (source) => {
215
+ loading.set(true);
216
+ error.set(null);
217
+
218
+ try {
219
+ const fetchPromise = fetcher(source);
220
+ currentFetch = fetchPromise;
221
+ const result = await fetchPromise;
222
+
223
+ // Only update if this is still the current fetch
224
+ if (currentFetch === fetchPromise) {
225
+ data.set(result);
226
+ loading.set(false);
227
+ }
228
+ } catch (e) {
229
+ if (currentFetch === fetchPromise) {
230
+ error.set(e);
231
+ loading.set(false);
232
+ }
233
+ }
234
+ };
235
+
236
+ const mutate = (value) => {
237
+ data.set(typeof value === 'function' ? value(data()) : value);
238
+ };
239
+
240
+ // Initial fetch if no initial value
241
+ if (!options.initialValue) {
242
+ refetch(options.source);
243
+ }
244
+
245
+ return [data, { loading, error, refetch, mutate }];
147
246
  }
247
+
248
+ // --- Dep comparison ---
249
+
148
250
  function depsChanged(oldDeps, newDeps) {
149
- if (oldDeps === undefined) return true;
150
- if (!oldDeps || !newDeps) return true;
151
- if (oldDeps.length !== newDeps.length) return true;
152
- for (let i = 0; i < oldDeps.length; i++) {
153
- if (!Object.is(oldDeps[i], newDeps[i])) return true;
154
- }
155
- return false;
156
- }
251
+ if (oldDeps === undefined) return true;
252
+ if (!oldDeps || !newDeps) return true;
253
+ if (oldDeps.length !== newDeps.length) return true;
254
+ for (let i = 0; i < oldDeps.length; i++) {
255
+ if (!Object.is(oldDeps[i], newDeps[i])) return true;
256
+ }
257
+ return false;
258
+ }
package/dist/index.js CHANGED
@@ -28,10 +28,10 @@ export {
28
28
  } from './hooks.js';
29
29
 
30
30
  // Component helpers
31
- export { memo, lazy, Suspense, ErrorBoundary, Show, For, Switch, Match } from './components.js';
31
+ export { memo, lazy, Suspense, ErrorBoundary, Show, For, Switch, Match, Island } from './components.js';
32
32
 
33
33
  // Store
34
- export { createStore, atom } from './store.js';
34
+ export { createStore, storeComputed, atom } from './store.js';
35
35
 
36
36
  // Head management
37
37
  export { Head, clearHead } from './head.js';