what-core 0.1.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/h.js ADDED
@@ -0,0 +1,150 @@
1
+ const EMPTY_OBJ = {};
2
+ const EMPTY_ARR = [];
3
+ export function h(tag, props, ...children) {
4
+ props = props || EMPTY_OBJ;
5
+ const flat = flattenChildren(children);
6
+ const key = props.key ?? null;
7
+ if (props.key !== undefined) {
8
+ props = { ...props };
9
+ delete props.key;
10
+ }
11
+ return { tag, props, children: flat, key, _vnode: true };
12
+ }
13
+ export function Fragment({ children }) {
14
+ return children;
15
+ }
16
+ function flattenChildren(children) {
17
+ const out = [];
18
+ for (let i = 0; i < children.length; i++) {
19
+ const child = children[i];
20
+ if (child == null || child === false || child === true) continue;
21
+ if (Array.isArray(child)) {
22
+ out.push(...flattenChildren(child));
23
+ } else if (typeof child === 'object' && child._vnode) {
24
+ out.push(child);
25
+ } else {
26
+ out.push(String(child));
27
+ }
28
+ }
29
+ return out;
30
+ }
31
+ export function html(strings, ...values) {
32
+ const src = strings.reduce((acc, str, i) =>
33
+ acc + str + (i < values.length ? `\x00${i}\x00` : ''), '');
34
+ return parseTemplate(src, values);
35
+ }
36
+ function parseTemplate(src, values) {
37
+ src = src.trim();
38
+ const nodes = [];
39
+ let i = 0;
40
+ while (i < src.length) {
41
+ if (src[i] === '<') {
42
+ const result = parseElement(src, i, values);
43
+ if (result) {
44
+ nodes.push(result.node);
45
+ i = result.end;
46
+ continue;
47
+ }
48
+ }
49
+ const result = parseText(src, i, values);
50
+ if (result.text) nodes.push(result.text);
51
+ i = result.end;
52
+ }
53
+ return nodes.length === 1 ? nodes[0] : nodes;
54
+ }
55
+ function parseElement(src, start, values) {
56
+ const openMatch = src.slice(start).match(/^<([a-zA-Z][a-zA-Z0-9-]*|[A-Z]\w*)/);
57
+ if (!openMatch) return null;
58
+ const tag = openMatch[1];
59
+ let i = start + openMatch[0].length;
60
+ const props = {};
61
+ while (i < src.length) {
62
+ while (i < src.length && /\s/.test(src[i])) i++;
63
+ if (src.slice(i, i + 2) === '/>') {
64
+ return { node: h(tag, Object.keys(props).length ? props : null), end: i + 2 };
65
+ }
66
+ if (src[i] === '>') {
67
+ i++;
68
+ break;
69
+ }
70
+ if (src.slice(i, i + 3) === '...') {
71
+ const placeholder = src.slice(i + 3).match(/^\x00(\d+)\x00/);
72
+ if (placeholder) {
73
+ Object.assign(props, values[Number(placeholder[1])]);
74
+ i += 3 + placeholder[0].length;
75
+ continue;
76
+ }
77
+ }
78
+ const attrMatch = src.slice(i).match(/^([a-zA-Z_@:][a-zA-Z0-9_:.-]*)/);
79
+ if (!attrMatch) break;
80
+ const attrName = attrMatch[1];
81
+ i += attrMatch[0].length;
82
+ while (i < src.length && /\s/.test(src[i])) i++;
83
+ if (src[i] === '=') {
84
+ i++;
85
+ while (i < src.length && /\s/.test(src[i])) i++;
86
+ const ph = src.slice(i).match(/^\x00(\d+)\x00/);
87
+ if (ph) {
88
+ props[attrName] = values[Number(ph[1])];
89
+ i += ph[0].length;
90
+ } else if (src[i] === '"' || src[i] === "'") {
91
+ const q = src[i];
92
+ i++;
93
+ let val = '';
94
+ while (i < src.length && src[i] !== q) {
95
+ const tph = src.slice(i).match(/^\x00(\d+)\x00/);
96
+ if (tph) {
97
+ val += String(values[Number(tph[1])]);
98
+ i += tph[0].length;
99
+ } else {
100
+ val += src[i];
101
+ i++;
102
+ }
103
+ }
104
+ i++;
105
+ props[attrName] = val;
106
+ }
107
+ } else {
108
+ props[attrName] = true;
109
+ }
110
+ }
111
+ const children = [];
112
+ const closeTag = `</${tag}>`;
113
+ while (i < src.length) {
114
+ if (src.slice(i, i + closeTag.length) === closeTag) {
115
+ i += closeTag.length;
116
+ break;
117
+ }
118
+ if (src[i] === '<') {
119
+ const child = parseElement(src, i, values);
120
+ if (child) {
121
+ children.push(child.node);
122
+ i = child.end;
123
+ continue;
124
+ }
125
+ }
126
+ const text = parseText(src, i, values);
127
+ if (text.text != null) children.push(text.text);
128
+ i = text.end;
129
+ }
130
+ return {
131
+ node: h(tag, Object.keys(props).length ? props : null, ...children),
132
+ end: i,
133
+ };
134
+ }
135
+ function parseText(src, start, values) {
136
+ let i = start;
137
+ let text = '';
138
+ while (i < src.length && src[i] !== '<') {
139
+ const ph = src.slice(i).match(/^\x00(\d+)\x00/);
140
+ if (ph) {
141
+ if (text.trim()) {
142
+ return { text: text.trim(), end: i };
143
+ }
144
+ return { text: values[Number(ph[1])], end: i + ph[0].length };
145
+ }
146
+ text += src[i];
147
+ i++;
148
+ }
149
+ return { text: text.trim() || null, end: i };
150
+ }
package/dist/head.js ADDED
@@ -0,0 +1,51 @@
1
+ const headState = {
2
+ title: null,
3
+ metas: new Map(),
4
+ links: new Map(),
5
+ };
6
+ export function Head({ title, meta, link, children }) {
7
+ if (typeof document === 'undefined') return null;
8
+ if (title) {
9
+ document.title = title;
10
+ headState.title = title;
11
+ }
12
+ if (meta) {
13
+ for (const attrs of (Array.isArray(meta) ? meta : [meta])) {
14
+ const key = attrs.name || attrs.property || attrs.httpEquiv || JSON.stringify(attrs);
15
+ setHeadTag('meta', key, attrs);
16
+ }
17
+ }
18
+ if (link) {
19
+ for (const attrs of (Array.isArray(link) ? link : [link])) {
20
+ const key = attrs.rel + (attrs.href || '');
21
+ setHeadTag('link', key, attrs);
22
+ }
23
+ }
24
+ return children || null;
25
+ }
26
+ function setHeadTag(tag, key, attrs) {
27
+ const existing = document.head.querySelector(`[data-what-head="${key}"]`);
28
+ if (existing) {
29
+ updateElement(existing, attrs);
30
+ return;
31
+ }
32
+ const el = document.createElement(tag);
33
+ el.setAttribute('data-what-head', key);
34
+ for (const [k, v] of Object.entries(attrs)) {
35
+ el.setAttribute(k, v);
36
+ }
37
+ document.head.appendChild(el);
38
+ }
39
+ function updateElement(el, attrs) {
40
+ for (const [k, v] of Object.entries(attrs)) {
41
+ if (el.getAttribute(k) !== v) {
42
+ el.setAttribute(k, v);
43
+ }
44
+ }
45
+ }
46
+ export function clearHead() {
47
+ const tags = document.head.querySelectorAll('[data-what-head]');
48
+ for (const tag of tags) tag.remove();
49
+ headState.metas.clear();
50
+ headState.links.clear();
51
+ }
@@ -0,0 +1,98 @@
1
+ import { signal, effect, computed, batch } from './reactive.js';
2
+ export function show(condition, vnode, fallback = null) {
3
+ return condition ? vnode : fallback;
4
+ }
5
+ 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
+ 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
+ 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(';');
35
+ }
36
+ function camelToKebab(str) {
37
+ return str.replace(/([A-Z])/g, '-$1').toLowerCase();
38
+ }
39
+ export function debounce(fn, ms) {
40
+ let timer;
41
+ return (...args) => {
42
+ clearTimeout(timer);
43
+ timer = setTimeout(() => fn(...args), ms);
44
+ };
45
+ }
46
+ 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
+ }
56
+ 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
+ }
63
+ 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
+ }
86
+ 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
+ }
94
+ export function transition(name, active) {
95
+ return {
96
+ class: active ? `${name}-enter ${name}-enter-active` : `${name}-leave ${name}-leave-active`,
97
+ };
98
+ }
package/dist/hooks.js ADDED
@@ -0,0 +1,156 @@
1
+ import { signal, computed, effect, batch, untrack } from './reactive.js';
2
+ import { getCurrentComponent } from './dom.js';
3
+ function getCtx() {
4
+ const ctx = getCurrentComponent();
5
+ if (!ctx) throw new Error('Hooks must be called inside a component');
6
+ return ctx;
7
+ }
8
+ function getHook(ctx) {
9
+ const index = ctx.hookIndex++;
10
+ return { index, exists: index < ctx.hooks.length };
11
+ }
12
+ 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];
21
+ }
22
+ 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];
29
+ }
30
+ 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];
37
+ }
38
+ 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
+ }
53
+ }
54
+ 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;
66
+ }
67
+ export function useCallback(fn, deps) {
68
+ return useMemo(() => fn, deps);
69
+ }
70
+ 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];
77
+ }
78
+ export function useContext(context) {
79
+ return context._value;
80
+ }
81
+ 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;
90
+ }
91
+ 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];
104
+ }
105
+ export function onMount(fn) {
106
+ const ctx = getCtx();
107
+ if (!ctx._mounted) {
108
+ ctx._mountCallbacks = ctx._mountCallbacks || [];
109
+ ctx._mountCallbacks.push(fn);
110
+ }
111
+ }
112
+ export function onCleanup(fn) {
113
+ const ctx = getCtx();
114
+ ctx._cleanupCallbacks = ctx._cleanupCallbacks || [];
115
+ ctx._cleanupCallbacks.push(fn);
116
+ }
117
+ 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 }];
147
+ }
148
+ 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
+ }
package/dist/index.js ADDED
@@ -0,0 +1,147 @@
1
+ // What Framework - Core
2
+ // The closest framework to vanilla JS.
3
+
4
+ // Reactive primitives
5
+ export { signal, computed, effect, batch, untrack } from './reactive.js';
6
+
7
+ // Virtual DOM
8
+ export { h, Fragment, html } from './h.js';
9
+
10
+ // DOM mounting & rendering
11
+ export { mount } from './dom.js';
12
+
13
+ // Hooks (React-compatible API)
14
+ export {
15
+ useState,
16
+ useSignal,
17
+ useComputed,
18
+ useEffect,
19
+ useMemo,
20
+ useCallback,
21
+ useRef,
22
+ useContext,
23
+ useReducer,
24
+ createContext,
25
+ onMount,
26
+ onCleanup,
27
+ createResource,
28
+ } from './hooks.js';
29
+
30
+ // Component helpers
31
+ export { memo, lazy, Suspense, ErrorBoundary, Show, For, Switch, Match } from './components.js';
32
+
33
+ // Store
34
+ export { createStore, atom } from './store.js';
35
+
36
+ // Head management
37
+ export { Head, clearHead } from './head.js';
38
+
39
+ // Utilities
40
+ export {
41
+ show,
42
+ each,
43
+ cls,
44
+ style,
45
+ debounce,
46
+ throttle,
47
+ useMediaQuery,
48
+ useLocalStorage,
49
+ Portal,
50
+ transition,
51
+ } from './helpers.js';
52
+
53
+ // DOM Scheduler (prevents layout thrashing)
54
+ export {
55
+ scheduleRead,
56
+ scheduleWrite,
57
+ flushScheduler,
58
+ measure,
59
+ mutate,
60
+ useScheduledEffect,
61
+ nextFrame,
62
+ raf,
63
+ onResize,
64
+ onIntersect,
65
+ smoothScrollTo,
66
+ } from './scheduler.js';
67
+
68
+ // Testing utilities (import separately: 'what/testing')
69
+ // export * from './testing.js';
70
+
71
+ // Animation primitives
72
+ export {
73
+ spring,
74
+ tween,
75
+ easings,
76
+ useTransition,
77
+ useGesture,
78
+ useAnimatedValue,
79
+ createTransitionClasses,
80
+ cssTransition,
81
+ } from './animation.js';
82
+
83
+ // Accessibility utilities
84
+ export {
85
+ useFocus,
86
+ useFocusTrap,
87
+ FocusTrap,
88
+ announce,
89
+ announceAssertive,
90
+ SkipLink,
91
+ useAriaExpanded,
92
+ useAriaSelected,
93
+ useAriaChecked,
94
+ useRovingTabIndex,
95
+ VisuallyHidden,
96
+ LiveRegion,
97
+ useId,
98
+ useIds,
99
+ useDescribedBy,
100
+ useLabelledBy,
101
+ Keys,
102
+ onKey,
103
+ onKeys,
104
+ } from './a11y.js';
105
+
106
+ // Skeleton loaders and loading states
107
+ export {
108
+ Skeleton,
109
+ SkeletonText,
110
+ SkeletonAvatar,
111
+ SkeletonCard,
112
+ SkeletonTable,
113
+ IslandSkeleton,
114
+ useSkeleton,
115
+ Placeholder,
116
+ LoadingDots,
117
+ Spinner,
118
+ } from './skeleton.js';
119
+
120
+ // Data fetching (SWR-like)
121
+ export {
122
+ useFetch,
123
+ useSWR,
124
+ useQuery,
125
+ useInfiniteQuery,
126
+ invalidateQueries,
127
+ prefetchQuery,
128
+ setQueryData,
129
+ getQueryData,
130
+ clearCache,
131
+ } from './data.js';
132
+
133
+ // Form utilities
134
+ export {
135
+ useForm,
136
+ useField,
137
+ rules,
138
+ simpleResolver,
139
+ zodResolver,
140
+ yupResolver,
141
+ Input,
142
+ Textarea,
143
+ Select,
144
+ Checkbox,
145
+ Radio,
146
+ ErrorMessage,
147
+ } from './form.js';
@@ -0,0 +1,124 @@
1
+ let currentEffect = null;
2
+ let batchDepth = 0;
3
+ let pendingEffects = new Set();
4
+ export function signal(initial) {
5
+ let value = initial;
6
+ const subs = new Set();
7
+ function read() {
8
+ if (currentEffect) {
9
+ subs.add(currentEffect);
10
+ currentEffect.deps.add(subs);
11
+ }
12
+ return value;
13
+ }
14
+ read.set = (next) => {
15
+ const nextVal = typeof next === 'function' ? next(value) : next;
16
+ if (Object.is(value, nextVal)) return;
17
+ value = nextVal;
18
+ notify(subs);
19
+ };
20
+ read.peek = () => value;
21
+ read.subscribe = (fn) => {
22
+ return effect(() => fn(read()));
23
+ };
24
+ read._signal = true;
25
+ return read;
26
+ }
27
+ export function computed(fn) {
28
+ let value, dirty = true;
29
+ const subs = new Set();
30
+ const inner = _createEffect(() => {
31
+ value = fn();
32
+ dirty = false;
33
+ notify(subs);
34
+ }, { lazy: true });
35
+ function read() {
36
+ if (currentEffect) {
37
+ subs.add(currentEffect);
38
+ currentEffect.deps.add(subs);
39
+ }
40
+ if (dirty) _runEffect(inner);
41
+ return value;
42
+ }
43
+ inner._onNotify = () => { dirty = true; };
44
+ read._signal = true;
45
+ read.peek = () => {
46
+ if (dirty) _runEffect(inner);
47
+ return value;
48
+ };
49
+ return read;
50
+ }
51
+ export function effect(fn) {
52
+ const e = _createEffect(fn);
53
+ _runEffect(e);
54
+ return () => _disposeEffect(e);
55
+ }
56
+ export function batch(fn) {
57
+ batchDepth++;
58
+ try {
59
+ fn();
60
+ } finally {
61
+ batchDepth--;
62
+ if (batchDepth === 0) flush();
63
+ }
64
+ }
65
+ 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
+ }
74
+ 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
+ }
85
+ function _disposeEffect(e) {
86
+ e.disposed = true;
87
+ cleanup(e);
88
+ }
89
+ function cleanup(e) {
90
+ for (const dep of e.deps) dep.delete(e);
91
+ e.deps.clear();
92
+ }
93
+ function notify(subs) {
94
+ const snapshot = [...subs];
95
+ for (const e of snapshot) {
96
+ if (e.disposed) continue;
97
+ if (e._onNotify) {
98
+ e._onNotify();
99
+ if (batchDepth > 0) pendingEffects.add(e);
100
+ continue;
101
+ }
102
+ if (batchDepth > 0) {
103
+ pendingEffects.add(e);
104
+ } else {
105
+ _runEffect(e);
106
+ }
107
+ }
108
+ }
109
+ 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
+ }
116
+ export function untrack(fn) {
117
+ const prev = currentEffect;
118
+ currentEffect = null;
119
+ try {
120
+ return fn();
121
+ } finally {
122
+ currentEffect = prev;
123
+ }
124
+ }