zero-query 0.4.9 → 0.6.3

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.
@@ -1,17 +1,20 @@
1
1
  /**
2
- * zQuery (zeroQuery) v0.4.9
2
+ * zQuery (zeroQuery) v0.6.3
3
3
  * Lightweight Frontend Library
4
4
  * https://github.com/tonywied17/zero-query
5
5
  * (c) 2026 Anthony Wiedman — MIT License
6
6
  */
7
7
  (function(global) {
8
8
  'use strict';
9
- function reactive(target, onChange, _path = '') { if (typeof target !== 'object' || target === null) return target; const proxyCache = new WeakMap(); const handler = { get(obj, key) { if (key === '__isReactive') return true; if (key === '__raw') return obj; const value = obj[key]; if (typeof value === 'object' && value !== null) { if (proxyCache.has(value)) return proxyCache.get(value); const childProxy = new Proxy(value, handler); proxyCache.set(value, childProxy); return childProxy; } return value; }, set(obj, key, value) { const old = obj[key]; if (old === value) return true; obj[key] = value; onChange(key, value, old); return true; }, deleteProperty(obj, key) { const old = obj[key]; delete obj[key]; onChange(key, undefined, old); return true; } }; return new Proxy(target, handler);
10
- class ZQueryCollection { constructor(elements) { this.elements = Array.isArray(elements) ? elements : [elements]; this.length = this.elements.length; this.elements.forEach((el, i) => { this[i] = el; }); } each(fn) { this.elements.forEach((el, i) => fn.call(el, i, el)); return this; } map(fn) { return this.elements.map((el, i) => fn.call(el, i, el)); } first() { return this.elements[0] || null; } last() { return this.elements[this.length - 1] || null; } eq(i) { return new ZQueryCollection(this.elements[i] ? [this.elements[i]] : []); } toArray(){ return [...this.elements]; } [Symbol.iterator]() { return this.elements[Symbol.iterator](); } find(selector) { const found = []; this.elements.forEach(el => found.push(...el.querySelectorAll(selector))); return new ZQueryCollection(found); } parent() { const parents = [...new Set(this.elements.map(el => el.parentElement).filter(Boolean))]; return new ZQueryCollection(parents); } closest(selector) { return new ZQueryCollection( this.elements.map(el => el.closest(selector)).filter(Boolean) ); } children(selector) { const kids = []; this.elements.forEach(el => { kids.push(...(selector ? el.querySelectorAll(`:scope > ${selector}`) : el.children)); }); return new ZQueryCollection([...kids]); } siblings() { const sibs = []; this.elements.forEach(el => { sibs.push(...[...el.parentElement.children].filter(c => c !== el)); }); return new ZQueryCollection(sibs); } next() { return new ZQueryCollection(this.elements.map(el => el.nextElementSibling).filter(Boolean)); } prev() { return new ZQueryCollection(this.elements.map(el => el.previousElementSibling).filter(Boolean)); } filter(selector) { if (typeof selector === 'function') { return new ZQueryCollection(this.elements.filter(selector)); } return new ZQueryCollection(this.elements.filter(el => el.matches(selector))); } not(selector) { if (typeof selector === 'function') { return new ZQueryCollection(this.elements.filter((el, i) => !selector.call(el, i, el))); } return new ZQueryCollection(this.elements.filter(el => !el.matches(selector))); } has(selector) { return new ZQueryCollection(this.elements.filter(el => el.querySelector(selector))); } addClass(...names) { const classes = names.flatMap(n => n.split(/\s+/)); return this.each((_, el) => el.classList.add(...classes)); } removeClass(...names) { const classes = names.flatMap(n => n.split(/\s+/)); return this.each((_, el) => el.classList.remove(...classes)); } toggleClass(name, force) { return this.each((_, el) => el.classList.toggle(name, force)); } hasClass(name) { return this.first()?.classList.contains(name) || false; } attr(name, value) { if (value === undefined) return this.first()?.getAttribute(name); return this.each((_, el) => el.setAttribute(name, value)); } removeAttr(name) { return this.each((_, el) => el.removeAttribute(name)); } prop(name, value) { if (value === undefined) return this.first()?.[name]; return this.each((_, el) => { el[name] = value; }); } data(key, value) { if (value === undefined) { if (key === undefined) return this.first()?.dataset; const raw = this.first()?.dataset[key]; try { return JSON.parse(raw); } catch { return raw; } } return this.each((_, el) => { el.dataset[key] = typeof value === 'object' ? JSON.stringify(value) : value; }); } css(props) { if (typeof props === 'string') { return getComputedStyle(this.first())[props]; } return this.each((_, el) => Object.assign(el.style, props)); } width() { return this.first()?.getBoundingClientRect().width; } height() { return this.first()?.getBoundingClientRect().height; } offset() { const r = this.first()?.getBoundingClientRect(); return r ? { top: r.top + window.scrollY, left: r.left + window.scrollX, width: r.width, height: r.height } : null; } position() { const el = this.first(); return el ? { top: el.offsetTop, left: el.offsetLeft } : null; } html(content) { if (content === undefined) return this.first()?.innerHTML; return this.each((_, el) => { el.innerHTML = content; }); } text(content) { if (content === undefined) return this.first()?.textContent; return this.each((_, el) => { el.textContent = content; }); } val(value) { if (value === undefined) return this.first()?.value; return this.each((_, el) => { el.value = value; }); } append(content) { return this.each((_, el) => { if (typeof content === 'string') el.insertAdjacentHTML('beforeend', content); else if (content instanceof ZQueryCollection) content.each((__, c) => el.appendChild(c)); else if (content instanceof Node) el.appendChild(content); }); } prepend(content) { return this.each((_, el) => { if (typeof content === 'string') el.insertAdjacentHTML('afterbegin', content); else if (content instanceof Node) el.insertBefore(content, el.firstChild); }); } after(content) { return this.each((_, el) => { if (typeof content === 'string') el.insertAdjacentHTML('afterend', content); else if (content instanceof Node) el.parentNode.insertBefore(content, el.nextSibling); }); } before(content) { return this.each((_, el) => { if (typeof content === 'string') el.insertAdjacentHTML('beforebegin', content); else if (content instanceof Node) el.parentNode.insertBefore(content, el); }); } wrap(wrapper) { return this.each((_, el) => { const w = typeof wrapper === 'string' ? createFragment(wrapper).firstElementChild : wrapper.cloneNode(true); el.parentNode.insertBefore(w, el); w.appendChild(el); }); } remove() { return this.each((_, el) => el.remove()); } empty() { return this.each((_, el) => { el.innerHTML = ''; }); } clone(deep = true) { return new ZQueryCollection(this.elements.map(el => el.cloneNode(deep))); } replaceWith(content) { return this.each((_, el) => { if (typeof content === 'string') { el.insertAdjacentHTML('afterend', content); el.remove(); } else if (content instanceof Node) { el.parentNode.replaceChild(content, el); } }); } show(display = '') { return this.each((_, el) => { el.style.display = display; }); } hide() { return this.each((_, el) => { el.style.display = 'none'; }); } toggle(display = '') { return this.each((_, el) => { el.style.display = (el.style.display === 'none' || getComputedStyle(el).display === 'none') ? display : 'none'; }); } on(event, selectorOrHandler, handler) { const events = event.split(/\s+/); return this.each((_, el) => { events.forEach(evt => { if (typeof selectorOrHandler === 'function') { el.addEventListener(evt, selectorOrHandler); } else { el.addEventListener(evt, (e) => { const target = e.target.closest(selectorOrHandler); if (target && el.contains(target)) handler.call(target, e); }); } }); }); } off(event, handler) { const events = event.split(/\s+/); return this.each((_, el) => { events.forEach(evt => el.removeEventListener(evt, handler)); }); } one(event, handler) { return this.each((_, el) => { el.addEventListener(event, handler, { once: true }); }); } trigger(event, detail) { return this.each((_, el) => { el.dispatchEvent(new CustomEvent(event, { detail, bubbles: true, cancelable: true })); }); } click(fn) { return fn ? this.on('click', fn) : this.trigger('click'); } submit(fn) { return fn ? this.on('submit', fn) : this.trigger('submit'); } focus() { this.first()?.focus(); return this; } blur() { this.first()?.blur(); return this; } animate(props, duration = 300, easing = 'ease') { return new Promise(resolve => { const count = { done: 0 }; this.each((_, el) => { el.style.transition = `all ${duration}ms ${easing}`; requestAnimationFrame(() => { Object.assign(el.style, props); const onEnd = () => { el.removeEventListener('transitionend', onEnd); el.style.transition = ''; if (++count.done >= this.length) resolve(this); }; el.addEventListener('transitionend', onEnd); }); }); setTimeout(() => resolve(this), duration + 50); }); } fadeIn(duration = 300) { return this.css({ opacity: '0', display: '' }).animate({ opacity: '1' }, duration); } fadeOut(duration = 300) { return this.animate({ opacity: '0' }, duration).then(col => col.hide()); } slideToggle(duration = 300) { return this.each((_, el) => { if (el.style.display === 'none' || getComputedStyle(el).display === 'none') { el.style.display = ''; el.style.overflow = 'hidden'; const h = el.scrollHeight + 'px'; el.style.maxHeight = '0'; el.style.transition = `max-height ${duration}ms ease`; requestAnimationFrame(() => { el.style.maxHeight = h; }); setTimeout(() => { el.style.maxHeight = ''; el.style.overflow = ''; el.style.transition = ''; }, duration); } else { el.style.overflow = 'hidden'; el.style.maxHeight = el.scrollHeight + 'px'; el.style.transition = `max-height ${duration}ms ease`; requestAnimationFrame(() => { el.style.maxHeight = '0'; }); setTimeout(() => { el.style.display = 'none'; el.style.maxHeight = ''; el.style.overflow = ''; el.style.transition = ''; }, duration); } }); } serialize() { const form = this.first(); if (!form || form.tagName !== 'FORM') return ''; return new URLSearchParams(new FormData(form)).toString(); } serializeObject() { const form = this.first(); if (!form || form.tagName !== 'FORM') return {}; const obj = {}; new FormData(form).forEach((v, k) => { if (obj[k] !== undefined) { if (!Array.isArray(obj[k])) obj[k] = [obj[k]]; obj[k].push(v); } else { obj[k] = v; } }); return obj; }
11
- const _registry = new Map(); const _instances = new Map(); const _resourceCache = new Map(); let _uid = 0;
12
- class Router { constructor(config = {}) { this._el = null; const isFile = typeof location !== 'undefined' && location.protocol === 'file:'; this._mode = isFile ? 'hash' : (config.mode || 'history'); let rawBase = config.base; if (rawBase == null) { rawBase = (typeof window !== 'undefined' && window.__ZQ_BASE) || ''; if (!rawBase && typeof document !== 'undefined') { const baseEl = document.querySelector('base'); if (baseEl) { try { rawBase = new URL(baseEl.href).pathname; } catch { rawBase = baseEl.getAttribute('href') || ''; } if (rawBase === '/') rawBase = ''; } } } this._base = String(rawBase).replace(/\/+$/, ''); if (this._base && !this._base.startsWith('/')) this._base = '/' + this._base; this._routes = []; this._fallback = config.fallback || null; this._current = null; this._guards = { before: [], after: [] }; this._listeners = new Set(); this._instance = null; this._resolving = false; if (config.el) { this._el = typeof config.el === 'string' ? document.querySelector(config.el) : config.el; } if (config.routes) { config.routes.forEach(r => this.add(r)); } if (this._mode === 'hash') { window.addEventListener('hashchange', () => this._resolve()); } else { window.addEventListener('popstate', () => this._resolve()); } document.addEventListener('click', (e) => { if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; const link = e.target.closest('[z-link]'); if (!link) return; if (link.getAttribute('target') === '_blank') return; e.preventDefault(); this.navigate(link.getAttribute('z-link')); }); if (this._el) { queueMicrotask(() => this._resolve()); } } add(route) { const keys = []; const pattern = route.path .replace(/:(\w+)/g, (_, key) => { keys.push(key); return '([^/]+)'; }) .replace(/\*/g, '(.*)'); const regex = new RegExp(`^${pattern}$`); this._routes.push({ ...route, _regex: regex, _keys: keys }); if (route.fallback) { const fbKeys = []; const fbPattern = route.fallback .replace(/:(\w+)/g, (_, key) => { fbKeys.push(key); return '([^/]+)'; }) .replace(/\*/g, '(.*)'); const fbRegex = new RegExp(`^${fbPattern}$`); this._routes.push({ ...route, path: route.fallback, _regex: fbRegex, _keys: fbKeys }); } return this; } remove(path) { this._routes = this._routes.filter(r => r.path !== path); return this; } navigate(path, options = {}) { const [cleanPath, fragment] = (path || '').split('#'); let normalized = this._normalizePath(cleanPath); const hash = fragment ? '#' + fragment : ''; if (this._mode === 'hash') { if (fragment) window.__zqScrollTarget = fragment; window.location.hash = '#' + normalized; } else { window.history.pushState(options.state || {}, '', this._base + normalized + hash); this._resolve(); } return this; } replace(path, options = {}) { const [cleanPath, fragment] = (path || '').split('#'); let normalized = this._normalizePath(cleanPath); const hash = fragment ? '#' + fragment : ''; if (this._mode === 'hash') { if (fragment) window.__zqScrollTarget = fragment; window.location.replace('#' + normalized); } else { window.history.replaceState(options.state || {}, '', this._base + normalized + hash); this._resolve(); } return this; } _normalizePath(path) { let p = path && path.startsWith('/') ? path : (path ? `/${path}` : '/'); if (this._base) { if (p === this._base) return '/'; if (p.startsWith(this._base + '/')) p = p.slice(this._base.length) || '/'; } return p; } resolve(path) { const normalized = path && path.startsWith('/') ? path : (path ? `/${path}` : '/'); return this._base + normalized; } back() { window.history.back(); return this; } forward() { window.history.forward(); return this; } go(n) { window.history.go(n); return this; } beforeEach(fn) { this._guards.before.push(fn); return this; } afterEach(fn) { this._guards.after.push(fn); return this; } onChange(fn) { this._listeners.add(fn); return () => this._listeners.delete(fn); } get current() { return this._current; } get base() { return this._base; } get path() { if (this._mode === 'hash') { const raw = window.location.hash.slice(1) || '/'; if (raw && !raw.startsWith('/')) { window.__zqScrollTarget = raw; const fallbackPath = (this._current && this._current.path) || '/'; window.location.replace('#' + fallbackPath); return fallbackPath; } return raw; } let pathname = window.location.pathname || '/'; if (pathname.length > 1 && pathname.endsWith('/')) { pathname = pathname.slice(0, -1); } if (this._base) { if (pathname === this._base) return '/'; if (pathname.startsWith(this._base + '/')) { return pathname.slice(this._base.length) || '/'; } } return pathname; } get query() { const search = this._mode === 'hash' ? (window.location.hash.split('?')[1] || '') : window.location.search.slice(1); return Object.fromEntries(new URLSearchParams(search)); } async _resolve() { if (this._resolving) return; this._resolving = true; try { await this.__resolve(); } finally { this._resolving = false; } } async __resolve() { const fullPath = this.path; const [pathPart, queryString] = fullPath.split('?'); const path = pathPart || '/'; const query = Object.fromEntries(new URLSearchParams(queryString || '')); let matched = null; let params = {}; for (const route of this._routes) { const m = path.match(route._regex); if (m) { matched = route; route._keys.forEach((key, i) => { params[key] = m[i + 1]; }); break; } } if (!matched && this._fallback) { matched = { component: this._fallback, path: '*', _keys: [], _regex: /.*/ }; } if (!matched) return; const to = { route: matched, params, query, path }; const from = this._current; for (const guard of this._guards.before) { const result = await guard(to, from); if (result === false) return; if (typeof result === 'string') { return this.navigate(result); } } if (matched.load) { try { await matched.load(); } catch (err) { console.error(`zQuery Router: Failed to load module for "${matched.path}"`, err); return; } } this._current = to; if (this._el && matched.component) { if (this._instance) { this._instance.destroy(); this._instance = null; } this._el.innerHTML = ''; const props = { ...params, $route: to, $query: query, $params: params }; if (typeof matched.component === 'string') { const container = document.createElement(matched.component); this._el.appendChild(container); this._instance = mount(container, matched.component, props); } else if (typeof matched.component === 'function') { this._el.innerHTML = matched.component(to); } } for (const guard of this._guards.after) { await guard(to, from); } this._listeners.forEach(fn => fn(to, from)); } destroy() { if (this._instance) this._instance.destroy(); this._listeners.clear(); this._routes = []; this._guards = { before: [], after: [] }; }
13
- class Store { constructor(config = {}) { this._subscribers = new Map(); this._wildcards = new Set(); this._actions = config.actions || {}; this._getters = config.getters || {}; this._middleware = []; this._history = []; this._debug = config.debug || false; const initial = typeof config.state === 'function' ? config.state() : { ...(config.state || {}) }; this.state = reactive(initial, (key, value, old) => { const subs = this._subscribers.get(key); if (subs) subs.forEach(fn => fn(value, old, key)); this._wildcards.forEach(fn => fn(key, value, old)); }); this.getters = {}; for (const [name, fn] of Object.entries(this._getters)) { Object.defineProperty(this.getters, name, { get: () => fn(this.state.__raw || this.state), enumerable: true }); } } dispatch(name, ...args) { const action = this._actions[name]; if (!action) { console.warn(`zQuery Store: Unknown action "${name}"`); return; } for (const mw of this._middleware) { const result = mw(name, args, this.state); if (result === false) return; } if (this._debug) { console.log(`%c[Store] ${name}`, 'color: #4CAF50; font-weight: bold;', ...args); } const result = action(this.state, ...args); this._history.push({ action: name, args, timestamp: Date.now() }); return result; } subscribe(keyOrFn, fn) { if (typeof keyOrFn === 'function') { this._wildcards.add(keyOrFn); return () => this._wildcards.delete(keyOrFn); } if (!this._subscribers.has(keyOrFn)) { this._subscribers.set(keyOrFn, new Set()); } this._subscribers.get(keyOrFn).add(fn); return () => this._subscribers.get(keyOrFn)?.delete(fn); } snapshot() { return JSON.parse(JSON.stringify(this.state.__raw || this.state)); } replaceState(newState) { const raw = this.state.__raw || this.state; for (const key of Object.keys(raw)) { delete this.state[key]; } Object.assign(this.state, newState); } use(fn) { this._middleware.push(fn); return this; } get history() { return [...this._history]; } reset(initialState) { this.replaceState(initialState); this._history = []; }
14
- const _config = { baseURL: '', headers: { 'Content-Type': 'application/json' }, timeout: 30000,
9
+ const ErrorCode = Object.freeze({ REACTIVE_CALLBACK: 'ZQ_REACTIVE_CALLBACK', SIGNAL_CALLBACK: 'ZQ_SIGNAL_CALLBACK', EFFECT_EXEC: 'ZQ_EFFECT_EXEC', EXPR_PARSE: 'ZQ_EXPR_PARSE', EXPR_EVAL: 'ZQ_EXPR_EVAL', EXPR_UNSAFE_ACCESS: 'ZQ_EXPR_UNSAFE_ACCESS', COMP_INVALID_NAME: 'ZQ_COMP_INVALID_NAME', COMP_NOT_FOUND: 'ZQ_COMP_NOT_FOUND', COMP_MOUNT_TARGET: 'ZQ_COMP_MOUNT_TARGET', COMP_RENDER: 'ZQ_COMP_RENDER', COMP_LIFECYCLE: 'ZQ_COMP_LIFECYCLE', COMP_RESOURCE: 'ZQ_COMP_RESOURCE', COMP_DIRECTIVE: 'ZQ_COMP_DIRECTIVE', ROUTER_LOAD: 'ZQ_ROUTER_LOAD', ROUTER_GUARD: 'ZQ_ROUTER_GUARD', ROUTER_RESOLVE: 'ZQ_ROUTER_RESOLVE', STORE_ACTION: 'ZQ_STORE_ACTION', STORE_MIDDLEWARE: 'ZQ_STORE_MIDDLEWARE', STORE_SUBSCRIBE: 'ZQ_STORE_SUBSCRIBE', HTTP_REQUEST: 'ZQ_HTTP_REQUEST', HTTP_TIMEOUT: 'ZQ_HTTP_TIMEOUT', HTTP_INTERCEPTOR: 'ZQ_HTTP_INTERCEPTOR', HTTP_PARSE: 'ZQ_HTTP_PARSE', INVALID_ARGUMENT: 'ZQ_INVALID_ARGUMENT',
10
+ function reactive(target, onChange, _path = '') { if (typeof target !== 'object' || target === null) return target; if (typeof onChange !== 'function') { reportError(ErrorCode.REACTIVE_CALLBACK, 'reactive() onChange must be a function', { received: typeof onChange }); onChange = () => {}; } const proxyCache = new WeakMap(); const handler = { get(obj, key) { if (key === '__isReactive') return true; if (key === '__raw') return obj; const value = obj[key]; if (typeof value === 'object' && value !== null) { if (proxyCache.has(value)) return proxyCache.get(value); const childProxy = new Proxy(value, handler); proxyCache.set(value, childProxy); return childProxy; } return value; }, set(obj, key, value) { const old = obj[key]; if (old === value) return true; obj[key] = value; try { onChange(key, value, old); } catch (err) { reportError(ErrorCode.REACTIVE_CALLBACK, `Reactive onChange threw for key "${String(key)}"`, { key, value, old }, err); } return true; }, deleteProperty(obj, key) { const old = obj[key]; delete obj[key]; try { onChange(key, undefined, old); } catch (err) { reportError(ErrorCode.REACTIVE_CALLBACK, `Reactive onChange threw for key "${String(key)}"`, { key, old }, err); } return true; } }; return new Proxy(target, handler);
11
+ class ZQueryCollection { constructor(elements) { this.elements = Array.isArray(elements) ? elements : [elements]; this.length = this.elements.length; this.elements.forEach((el, i) => { this[i] = el; }); } each(fn) { this.elements.forEach((el, i) => fn.call(el, i, el)); return this; } map(fn) { return this.elements.map((el, i) => fn.call(el, i, el)); } forEach(fn) { this.elements.forEach((el, i) => fn(el, i, this.elements)); return this; } first() { return this.elements[0] || null; } last() { return this.elements[this.length - 1] || null; } eq(i) { return new ZQueryCollection(this.elements[i] ? [this.elements[i]] : []); } toArray(){ return [...this.elements]; } [Symbol.iterator]() { return this.elements[Symbol.iterator](); } find(selector) { const found = []; this.elements.forEach(el => found.push(...el.querySelectorAll(selector))); return new ZQueryCollection(found); } parent() { const parents = [...new Set(this.elements.map(el => el.parentElement).filter(Boolean))]; return new ZQueryCollection(parents); } closest(selector) { return new ZQueryCollection( this.elements.map(el => el.closest(selector)).filter(Boolean) ); } children(selector) { const kids = []; this.elements.forEach(el => { kids.push(...(selector ? el.querySelectorAll(`:scope > ${selector}`) : el.children)); }); return new ZQueryCollection([...kids]); } siblings() { const sibs = []; this.elements.forEach(el => { sibs.push(...[...el.parentElement.children].filter(c => c !== el)); }); return new ZQueryCollection(sibs); } next() { return new ZQueryCollection(this.elements.map(el => el.nextElementSibling).filter(Boolean)); } prev() { return new ZQueryCollection(this.elements.map(el => el.previousElementSibling).filter(Boolean)); } filter(selector) { if (typeof selector === 'function') { return new ZQueryCollection(this.elements.filter(selector)); } return new ZQueryCollection(this.elements.filter(el => el.matches(selector))); } not(selector) { if (typeof selector === 'function') { return new ZQueryCollection(this.elements.filter((el, i) => !selector.call(el, i, el))); } return new ZQueryCollection(this.elements.filter(el => !el.matches(selector))); } has(selector) { return new ZQueryCollection(this.elements.filter(el => el.querySelector(selector))); } addClass(...names) { const classes = names.flatMap(n => n.split(/\s+/)); return this.each((_, el) => el.classList.add(...classes)); } removeClass(...names) { const classes = names.flatMap(n => n.split(/\s+/)); return this.each((_, el) => el.classList.remove(...classes)); } toggleClass(name, force) { return this.each((_, el) => el.classList.toggle(name, force)); } hasClass(name) { return this.first()?.classList.contains(name) || false; } attr(name, value) { if (value === undefined) return this.first()?.getAttribute(name); return this.each((_, el) => el.setAttribute(name, value)); } removeAttr(name) { return this.each((_, el) => el.removeAttribute(name)); } prop(name, value) { if (value === undefined) return this.first()?.[name]; return this.each((_, el) => { el[name] = value; }); } data(key, value) { if (value === undefined) { if (key === undefined) return this.first()?.dataset; const raw = this.first()?.dataset[key]; try { return JSON.parse(raw); } catch { return raw; } } return this.each((_, el) => { el.dataset[key] = typeof value === 'object' ? JSON.stringify(value) : value; }); } css(props) { if (typeof props === 'string') { return getComputedStyle(this.first())[props]; } return this.each((_, el) => Object.assign(el.style, props)); } width() { return this.first()?.getBoundingClientRect().width; } height() { return this.first()?.getBoundingClientRect().height; } offset() { const r = this.first()?.getBoundingClientRect(); return r ? { top: r.top + window.scrollY, left: r.left + window.scrollX, width: r.width, height: r.height } : null; } position() { const el = this.first(); return el ? { top: el.offsetTop, left: el.offsetLeft } : null; } html(content) { if (content === undefined) return this.first()?.innerHTML; return this.each((_, el) => { el.innerHTML = content; }); } text(content) { if (content === undefined) return this.first()?.textContent; return this.each((_, el) => { el.textContent = content; }); } val(value) { if (value === undefined) return this.first()?.value; return this.each((_, el) => { el.value = value; }); } append(content) { return this.each((_, el) => { if (typeof content === 'string') el.insertAdjacentHTML('beforeend', content); else if (content instanceof ZQueryCollection) content.each((__, c) => el.appendChild(c)); else if (content instanceof Node) el.appendChild(content); }); } prepend(content) { return this.each((_, el) => { if (typeof content === 'string') el.insertAdjacentHTML('afterbegin', content); else if (content instanceof Node) el.insertBefore(content, el.firstChild); }); } after(content) { return this.each((_, el) => { if (typeof content === 'string') el.insertAdjacentHTML('afterend', content); else if (content instanceof Node) el.parentNode.insertBefore(content, el.nextSibling); }); } before(content) { return this.each((_, el) => { if (typeof content === 'string') el.insertAdjacentHTML('beforebegin', content); else if (content instanceof Node) el.parentNode.insertBefore(content, el); }); } wrap(wrapper) { return this.each((_, el) => { const w = typeof wrapper === 'string' ? createFragment(wrapper).firstElementChild : wrapper.cloneNode(true); el.parentNode.insertBefore(w, el); w.appendChild(el); }); } remove() { return this.each((_, el) => el.remove()); } empty() { return this.each((_, el) => { el.innerHTML = ''; }); } clone(deep = true) { return new ZQueryCollection(this.elements.map(el => el.cloneNode(deep))); } replaceWith(content) { return this.each((_, el) => { if (typeof content === 'string') { el.insertAdjacentHTML('afterend', content); el.remove(); } else if (content instanceof Node) { el.parentNode.replaceChild(content, el); } }); } show(display = '') { return this.each((_, el) => { el.style.display = display; }); } hide() { return this.each((_, el) => { el.style.display = 'none'; }); } toggle(display = '') { return this.each((_, el) => { el.style.display = (el.style.display === 'none' || getComputedStyle(el).display === 'none') ? display : 'none'; }); } on(event, selectorOrHandler, handler) { const events = event.split(/\s+/); return this.each((_, el) => { events.forEach(evt => { if (typeof selectorOrHandler === 'function') { el.addEventListener(evt, selectorOrHandler); } else { el.addEventListener(evt, (e) => { const target = e.target.closest(selectorOrHandler); if (target && el.contains(target)) handler.call(target, e); }); } }); }); } off(event, handler) { const events = event.split(/\s+/); return this.each((_, el) => { events.forEach(evt => el.removeEventListener(evt, handler)); }); } one(event, handler) { return this.each((_, el) => { el.addEventListener(event, handler, { once: true }); }); } trigger(event, detail) { return this.each((_, el) => { el.dispatchEvent(new CustomEvent(event, { detail, bubbles: true, cancelable: true })); }); } click(fn) { return fn ? this.on('click', fn) : this.trigger('click'); } submit(fn) { return fn ? this.on('submit', fn) : this.trigger('submit'); } focus() { this.first()?.focus(); return this; } blur() { this.first()?.blur(); return this; } animate(props, duration = 300, easing = 'ease') { return new Promise(resolve => { const count = { done: 0 }; this.each((_, el) => { el.style.transition = `all ${duration}ms ${easing}`; requestAnimationFrame(() => { Object.assign(el.style, props); const onEnd = () => { el.removeEventListener('transitionend', onEnd); el.style.transition = ''; if (++count.done >= this.length) resolve(this); }; el.addEventListener('transitionend', onEnd); }); }); setTimeout(() => resolve(this), duration + 50); }); } fadeIn(duration = 300) { return this.css({ opacity: '0', display: '' }).animate({ opacity: '1' }, duration); } fadeOut(duration = 300) { return this.animate({ opacity: '0' }, duration).then(col => col.hide()); } slideToggle(duration = 300) { return this.each((_, el) => { if (el.style.display === 'none' || getComputedStyle(el).display === 'none') { el.style.display = ''; el.style.overflow = 'hidden'; const h = el.scrollHeight + 'px'; el.style.maxHeight = '0'; el.style.transition = `max-height ${duration}ms ease`; requestAnimationFrame(() => { el.style.maxHeight = h; }); setTimeout(() => { el.style.maxHeight = ''; el.style.overflow = ''; el.style.transition = ''; }, duration); } else { el.style.overflow = 'hidden'; el.style.maxHeight = el.scrollHeight + 'px'; el.style.transition = `max-height ${duration}ms ease`; requestAnimationFrame(() => { el.style.maxHeight = '0'; }); setTimeout(() => { el.style.display = 'none'; el.style.maxHeight = ''; el.style.overflow = ''; el.style.transition = ''; }, duration); } }); } serialize() { const form = this.first(); if (!form || form.tagName !== 'FORM') return ''; return new URLSearchParams(new FormData(form)).toString(); } serializeObject() { const form = this.first(); if (!form || form.tagName !== 'FORM') return {}; const obj = {}; new FormData(form).forEach((v, k) => { if (obj[k] !== undefined) { if (!Array.isArray(obj[k])) obj[k] = [obj[k]]; obj[k].push(v); } else { obj[k] = v; } }); return obj; }
12
+ const T = { NUM: 1, STR: 2, IDENT: 3, OP: 4, PUNC: 5, TMPL: 6, EOF: 7
13
+ function morph(rootEl, newHTML) { const template = document.createElement('template'); template.innerHTML = newHTML; const newRoot = template.content; const tempDiv = document.createElement('div'); while (newRoot.firstChild) tempDiv.appendChild(newRoot.firstChild); _morphChildren(rootEl, tempDiv);
14
+ const _registry = new Map(); const _instances = new Map(); const _resourceCache = new Map(); let _uid = 0;
15
+ class Router { constructor(config = {}) { this._el = null; const isFile = typeof location !== 'undefined' && location.protocol === 'file:'; this._mode = isFile ? 'hash' : (config.mode || 'history'); let rawBase = config.base; if (rawBase == null) { rawBase = (typeof window !== 'undefined' && window.__ZQ_BASE) || ''; if (!rawBase && typeof document !== 'undefined') { const baseEl = document.querySelector('base'); if (baseEl) { try { rawBase = new URL(baseEl.href).pathname; } catch { rawBase = baseEl.getAttribute('href') || ''; } if (rawBase === '/') rawBase = ''; } } } this._base = String(rawBase).replace(/\/+$/, ''); if (this._base && !this._base.startsWith('/')) this._base = '/' + this._base; this._routes = []; this._fallback = config.fallback || null; this._current = null; this._guards = { before: [], after: [] }; this._listeners = new Set(); this._instance = null; this._resolving = false; if (config.el) { this._el = typeof config.el === 'string' ? document.querySelector(config.el) : config.el; } if (config.routes) { config.routes.forEach(r => this.add(r)); } if (this._mode === 'hash') { window.addEventListener('hashchange', () => this._resolve()); } else { window.addEventListener('popstate', () => this._resolve()); } document.addEventListener('click', (e) => { if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; const link = e.target.closest('[z-link]'); if (!link) return; if (link.getAttribute('target') === '_blank') return; e.preventDefault(); this.navigate(link.getAttribute('z-link')); }); if (this._el) { queueMicrotask(() => this._resolve()); } } add(route) { const keys = []; const pattern = route.path .replace(/:(\w+)/g, (_, key) => { keys.push(key); return '([^/]+)'; }) .replace(/\*/g, '(.*)'); const regex = new RegExp(`^${pattern}$`); this._routes.push({ ...route, _regex: regex, _keys: keys }); if (route.fallback) { const fbKeys = []; const fbPattern = route.fallback .replace(/:(\w+)/g, (_, key) => { fbKeys.push(key); return '([^/]+)'; }) .replace(/\*/g, '(.*)'); const fbRegex = new RegExp(`^${fbPattern}$`); this._routes.push({ ...route, path: route.fallback, _regex: fbRegex, _keys: fbKeys }); } return this; } remove(path) { this._routes = this._routes.filter(r => r.path !== path); return this; } navigate(path, options = {}) { const [cleanPath, fragment] = (path || '').split('#'); let normalized = this._normalizePath(cleanPath); const hash = fragment ? '#' + fragment : ''; if (this._mode === 'hash') { if (fragment) window.__zqScrollTarget = fragment; window.location.hash = '#' + normalized; } else { window.history.pushState(options.state || {}, '', this._base + normalized + hash); this._resolve(); } return this; } replace(path, options = {}) { const [cleanPath, fragment] = (path || '').split('#'); let normalized = this._normalizePath(cleanPath); const hash = fragment ? '#' + fragment : ''; if (this._mode === 'hash') { if (fragment) window.__zqScrollTarget = fragment; window.location.replace('#' + normalized); } else { window.history.replaceState(options.state || {}, '', this._base + normalized + hash); this._resolve(); } return this; } _normalizePath(path) { let p = path && path.startsWith('/') ? path : (path ? `/${path}` : '/'); if (this._base) { if (p === this._base) return '/'; if (p.startsWith(this._base + '/')) p = p.slice(this._base.length) || '/'; } return p; } resolve(path) { const normalized = path && path.startsWith('/') ? path : (path ? `/${path}` : '/'); return this._base + normalized; } back() { window.history.back(); return this; } forward() { window.history.forward(); return this; } go(n) { window.history.go(n); return this; } beforeEach(fn) { this._guards.before.push(fn); return this; } afterEach(fn) { this._guards.after.push(fn); return this; } onChange(fn) { this._listeners.add(fn); return () => this._listeners.delete(fn); } get current() { return this._current; } get base() { return this._base; } get path() { if (this._mode === 'hash') { const raw = window.location.hash.slice(1) || '/'; if (raw && !raw.startsWith('/')) { window.__zqScrollTarget = raw; const fallbackPath = (this._current && this._current.path) || '/'; window.location.replace('#' + fallbackPath); return fallbackPath; } return raw; } let pathname = window.location.pathname || '/'; if (pathname.length > 1 && pathname.endsWith('/')) { pathname = pathname.slice(0, -1); } if (this._base) { if (pathname === this._base) return '/'; if (pathname.startsWith(this._base + '/')) { return pathname.slice(this._base.length) || '/'; } } return pathname; } get query() { const search = this._mode === 'hash' ? (window.location.hash.split('?')[1] || '') : window.location.search.slice(1); return Object.fromEntries(new URLSearchParams(search)); } async _resolve() { if (this._resolving) return; this._resolving = true; try { await this.__resolve(); } finally { this._resolving = false; } } async __resolve() { const fullPath = this.path; const [pathPart, queryString] = fullPath.split('?'); const path = pathPart || '/'; const query = Object.fromEntries(new URLSearchParams(queryString || '')); let matched = null; let params = {}; for (const route of this._routes) { const m = path.match(route._regex); if (m) { matched = route; route._keys.forEach((key, i) => { params[key] = m[i + 1]; }); break; } } if (!matched && this._fallback) { matched = { component: this._fallback, path: '*', _keys: [], _regex: /.*/ }; } if (!matched) return; const to = { route: matched, params, query, path }; const from = this._current; for (const guard of this._guards.before) { try { const result = await guard(to, from); if (result === false) return; if (typeof result === 'string') { return this.navigate(result); } } catch (err) { reportError(ErrorCode.ROUTER_GUARD, 'Before-guard threw', { to, from }, err); return; } } if (matched.load) { try { await matched.load(); } catch (err) { reportError(ErrorCode.ROUTER_LOAD, `Failed to load module for route "${matched.path}"`, { path: matched.path }, err); return; } } this._current = to; if (this._el && matched.component) { if (this._instance) { this._instance.destroy(); this._instance = null; } this._el.innerHTML = ''; const props = { ...params, $route: to, $query: query, $params: params }; if (typeof matched.component === 'string') { const container = document.createElement(matched.component); this._el.appendChild(container); this._instance = mount(container, matched.component, props); } else if (typeof matched.component === 'function') { this._el.innerHTML = matched.component(to); } } for (const guard of this._guards.after) { await guard(to, from); } this._listeners.forEach(fn => fn(to, from)); } destroy() { if (this._instance) this._instance.destroy(); this._listeners.clear(); this._routes = []; this._guards = { before: [], after: [] }; }
16
+ class Store { constructor(config = {}) { this._subscribers = new Map(); this._wildcards = new Set(); this._actions = config.actions || {}; this._getters = config.getters || {}; this._middleware = []; this._history = []; this._debug = config.debug || false; const initial = typeof config.state === 'function' ? config.state() : { ...(config.state || {}) }; this.state = reactive(initial, (key, value, old) => { const subs = this._subscribers.get(key); if (subs) subs.forEach(fn => { try { fn(value, old, key); } catch (err) { reportError(ErrorCode.STORE_SUBSCRIBE, `Subscriber for "${key}" threw`, { key }, err); } }); this._wildcards.forEach(fn => { try { fn(key, value, old); } catch (err) { reportError(ErrorCode.STORE_SUBSCRIBE, 'Wildcard subscriber threw', { key }, err); } }); }); this.getters = {}; for (const [name, fn] of Object.entries(this._getters)) { Object.defineProperty(this.getters, name, { get: () => fn(this.state.__raw || this.state), enumerable: true }); } } dispatch(name, ...args) { const action = this._actions[name]; if (!action) { reportError(ErrorCode.STORE_ACTION, `Unknown action "${name}"`, { action: name, args }); return; } for (const mw of this._middleware) { try { const result = mw(name, args, this.state); if (result === false) return; } catch (err) { reportError(ErrorCode.STORE_MIDDLEWARE, `Middleware threw during "${name}"`, { action: name }, err); return; } } if (this._debug) { console.log(`%c[Store] ${name}`, 'color: #4CAF50; font-weight: bold;', ...args); } try { const result = action(this.state, ...args); this._history.push({ action: name, args, timestamp: Date.now() }); return result; } catch (err) { reportError(ErrorCode.STORE_ACTION, `Action "${name}" threw`, { action: name, args }, err); } } subscribe(keyOrFn, fn) { if (typeof keyOrFn === 'function') { this._wildcards.add(keyOrFn); return () => this._wildcards.delete(keyOrFn); } if (!this._subscribers.has(keyOrFn)) { this._subscribers.set(keyOrFn, new Set()); } this._subscribers.get(keyOrFn).add(fn); return () => this._subscribers.get(keyOrFn)?.delete(fn); } snapshot() { return JSON.parse(JSON.stringify(this.state.__raw || this.state)); } replaceState(newState) { const raw = this.state.__raw || this.state; for (const key of Object.keys(raw)) { delete this.state[key]; } Object.assign(this.state, newState); } use(fn) { this._middleware.push(fn); return this; } get history() { return [...this._history]; } reset(initialState) { this.replaceState(initialState); this._history = []; }
17
+ const _config = { baseURL: '', headers: { 'Content-Type': 'application/json' }, timeout: 30000,
15
18
  function debounce(fn, ms = 250) { let timer; const debounced = (...args) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), ms); }; debounced.cancel = () => clearTimeout(timer); return debounced;
16
- function $(selector, context) { if (typeof selector === 'function') { query.ready(selector); return; } return query(selector, context);
19
+ function $(selector, context) { if (typeof selector === 'function') { query.ready(selector); return; } return query(selector, context);
17
20
  })(typeof window !== 'undefined' ? window : globalThis);
package/index.d.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * Lightweight modern frontend library — jQuery-like selectors, reactive
5
5
  * components, SPA router, state management, HTTP client & utilities.
6
6
  *
7
- * @version 0.4.9
7
+ * @version 0.6.3
8
8
  * @license MIT
9
9
  * @see https://z-query.com/docs
10
10
  */
@@ -38,6 +38,11 @@ export class ZQueryCollection {
38
38
  */
39
39
  map<T>(fn: (this: Element, index: number, element: Element) => T): T[];
40
40
 
41
+ /**
42
+ * Iterate elements with Array-style `forEach`. Returns `this` for chaining.
43
+ */
44
+ forEach(fn: (element: Element, index: number, elements: Element[]) => void): this;
45
+
41
46
  /** First raw element, or `null`. */
42
47
  first(): Element | null;
43
48
 
@@ -384,6 +389,20 @@ interface ComponentDefinition {
384
389
  /** Called when the component is destroyed. Clean up subscriptions here. */
385
390
  destroyed?(this: ComponentInstance): void;
386
391
 
392
+ /**
393
+ * Computed properties — lazy getters derived from state.
394
+ * Each function receives the raw state as its argument.
395
+ * Access via `this.computed.name` in methods and templates.
396
+ */
397
+ computed?: Record<string, (state: Record<string, any>) => any>;
398
+
399
+ /**
400
+ * Watch state keys for changes.
401
+ * Keys support dot-path notation (e.g. `'user.name'`).
402
+ * Handler receives `(newValue, oldValue)`.
403
+ */
404
+ watch?: Record<string, ((this: ComponentInstance, newVal: any, oldVal: any) => void) | { handler: (this: ComponentInstance, newVal: any, oldVal: any) => void }>;
405
+
387
406
  /** Additional keys become instance methods (bound to the instance). */
388
407
  [method: string]: any;
389
408
  }
@@ -414,6 +433,12 @@ interface ComponentInstance {
414
433
  /** Active page id derived from route param (when using `pages` config). */
415
434
  activePage: string;
416
435
 
436
+ /**
437
+ * Computed properties — lazy getters derived from state.
438
+ * Defined via `computed` in the component definition.
439
+ */
440
+ readonly computed: Record<string, any>;
441
+
417
442
  /** Merge partial state (triggers re-render). */
418
443
  setState(partial: Record<string, any>): void;
419
444
 
@@ -491,6 +516,85 @@ interface StyleOptions {
491
516
  export function style(urls: string | string[], opts?: StyleOptions): StyleHandle;
492
517
 
493
518
 
519
+ // ---------------------------------------------------------------------------
520
+ // DOM Diffing (Morphing)
521
+ // ---------------------------------------------------------------------------
522
+
523
+ /**
524
+ * Morph an existing DOM element's children to match new HTML.
525
+ * Only touches nodes that actually differ — preserves focus, scroll
526
+ * positions, video playback, and other live DOM state.
527
+ *
528
+ * Use `z-key="uniqueId"` attributes on list items for keyed reconciliation.
529
+ *
530
+ * @param rootEl The live DOM container to patch.
531
+ * @param newHTML The desired HTML string.
532
+ */
533
+ export function morph(rootEl: Element, newHTML: string): void;
534
+
535
+
536
+ // ---------------------------------------------------------------------------
537
+ // Safe Expression Evaluator
538
+ // ---------------------------------------------------------------------------
539
+
540
+ /**
541
+ * CSP-safe expression evaluator. Parses and evaluates JS expressions
542
+ * without `eval()` or `new Function()`. Used internally by directives.
543
+ *
544
+ * @param expr Expression string.
545
+ * @param scope Array of scope objects checked in order for identifier resolution.
546
+ * @returns Evaluation result, or `undefined` on error.
547
+ */
548
+ export function safeEval(expr: string, scope: object[]): any;
549
+
550
+
551
+ // ---------------------------------------------------------------------------
552
+ // SSR (Server-Side Rendering)
553
+ // ---------------------------------------------------------------------------
554
+
555
+ /** SSR application instance for server-side component rendering. */
556
+ interface SSRApp {
557
+ /** Register a component for SSR. */
558
+ component(name: string, definition: ComponentDefinition): SSRApp;
559
+
560
+ /**
561
+ * Render a component to an HTML string.
562
+ * @param componentName Registered component name.
563
+ * @param props Props to pass to the component.
564
+ * @param options Rendering options.
565
+ */
566
+ renderToString(
567
+ componentName: string,
568
+ props?: Record<string, any>,
569
+ options?: { hydrate?: boolean },
570
+ ): Promise<string>;
571
+
572
+ /**
573
+ * Render a full HTML page with a component mounted in a shell.
574
+ */
575
+ renderPage(options: {
576
+ component?: string;
577
+ props?: Record<string, any>;
578
+ title?: string;
579
+ styles?: string[];
580
+ scripts?: string[];
581
+ lang?: string;
582
+ meta?: string;
583
+ bodyAttrs?: string;
584
+ }): Promise<string>;
585
+ }
586
+
587
+ /** Create an SSR application instance. */
588
+ export function createSSRApp(): SSRApp;
589
+
590
+ /**
591
+ * Quick one-shot render of a component definition to an HTML string.
592
+ * @param definition Component definition object.
593
+ * @param props Props to pass.
594
+ */
595
+ export function renderToString(definition: ComponentDefinition, props?: Record<string, any>): string;
596
+
597
+
494
598
  // ---------------------------------------------------------------------------
495
599
  // Directive System
496
600
  // ---------------------------------------------------------------------------
@@ -512,6 +616,11 @@ export function style(urls: string | string[], opts?: StyleOptions): StyleHandle
512
616
  // n in 5 Number range → [1, 2, 3, 4, 5].
513
617
  // (val, key) in object Object iteration → {key, value} entries.
514
618
  //
619
+ // z-key="uniqueId" Keyed reconciliation for list items.
620
+ // Preserves DOM nodes across reorders. Use inside
621
+ // z-for to give each item a stable identity.
622
+ // Example: <li z-for="item in items" z-key="{{item.id}}">
623
+ //
515
624
  // z-show="expression" Toggle `display: none` (element stays in DOM).
516
625
  //
517
626
  // ─── Attribute Directives ───────────────────────────────────────────────
@@ -571,6 +680,20 @@ export function style(urls: string | string[], opts?: StyleOptions): StyleHandle
571
680
  // z-pre Skip all directive processing for this element
572
681
  // and its descendants.
573
682
  //
683
+ // ─── Slot System ────────────────────────────────────────────────────────
684
+ //
685
+ // <slot> Default slot — replaced with child content
686
+ // passed by the parent component.
687
+ // <slot name="header"> Named slot — replaced with child content that
688
+ // has a matching slot="header" attribute.
689
+ // <slot>fallback</slot> Fallback content shown when no slot content provided.
690
+ //
691
+ // Parent usage:
692
+ // <my-component>
693
+ // <h1 slot="header">Title</h1> (→ named slot "header")
694
+ // <p>Body text</p> (→ default slot)
695
+ // </my-component>
696
+ //
574
697
  // ─── Processing Order ───────────────────────────────────────────────────
575
698
  //
576
699
  // 1. z-for (pre-innerHTML expansion)
@@ -1046,22 +1169,125 @@ interface EventBus {
1046
1169
  export const bus: EventBus;
1047
1170
 
1048
1171
 
1172
+ // ---------------------------------------------------------------------------
1173
+ // Error Handling
1174
+ // ---------------------------------------------------------------------------
1175
+
1176
+ /** All structured error codes used by zQuery. */
1177
+ export const ErrorCode: {
1178
+ // Reactive
1179
+ readonly REACTIVE_CALLBACK: 'ZQ_REACTIVE_CALLBACK';
1180
+ readonly SIGNAL_CALLBACK: 'ZQ_SIGNAL_CALLBACK';
1181
+ readonly EFFECT_EXEC: 'ZQ_EFFECT_EXEC';
1182
+
1183
+ // Expression parser
1184
+ readonly EXPR_PARSE: 'ZQ_EXPR_PARSE';
1185
+ readonly EXPR_EVAL: 'ZQ_EXPR_EVAL';
1186
+ readonly EXPR_UNSAFE_ACCESS: 'ZQ_EXPR_UNSAFE_ACCESS';
1187
+
1188
+ // Component
1189
+ readonly COMP_INVALID_NAME: 'ZQ_COMP_INVALID_NAME';
1190
+ readonly COMP_NOT_FOUND: 'ZQ_COMP_NOT_FOUND';
1191
+ readonly COMP_MOUNT_TARGET: 'ZQ_COMP_MOUNT_TARGET';
1192
+ readonly COMP_RENDER: 'ZQ_COMP_RENDER';
1193
+ readonly COMP_LIFECYCLE: 'ZQ_COMP_LIFECYCLE';
1194
+ readonly COMP_RESOURCE: 'ZQ_COMP_RESOURCE';
1195
+ readonly COMP_DIRECTIVE: 'ZQ_COMP_DIRECTIVE';
1196
+
1197
+ // Router
1198
+ readonly ROUTER_LOAD: 'ZQ_ROUTER_LOAD';
1199
+ readonly ROUTER_GUARD: 'ZQ_ROUTER_GUARD';
1200
+ readonly ROUTER_RESOLVE: 'ZQ_ROUTER_RESOLVE';
1201
+
1202
+ // Store
1203
+ readonly STORE_ACTION: 'ZQ_STORE_ACTION';
1204
+ readonly STORE_MIDDLEWARE: 'ZQ_STORE_MIDDLEWARE';
1205
+ readonly STORE_SUBSCRIBE: 'ZQ_STORE_SUBSCRIBE';
1206
+
1207
+ // HTTP
1208
+ readonly HTTP_REQUEST: 'ZQ_HTTP_REQUEST';
1209
+ readonly HTTP_TIMEOUT: 'ZQ_HTTP_TIMEOUT';
1210
+ readonly HTTP_INTERCEPTOR: 'ZQ_HTTP_INTERCEPTOR';
1211
+ readonly HTTP_PARSE: 'ZQ_HTTP_PARSE';
1212
+
1213
+ // General
1214
+ readonly INVALID_ARGUMENT: 'ZQ_INVALID_ARGUMENT';
1215
+ };
1216
+
1217
+ /** Union of all error code string values. */
1218
+ export type ErrorCodeValue = typeof ErrorCode[keyof typeof ErrorCode];
1219
+
1220
+ /** Structured error class used throughout zQuery. */
1221
+ export class ZQueryError extends Error {
1222
+ readonly name: 'ZQueryError';
1223
+ /** Machine-readable error code (e.g. `'ZQ_COMP_RENDER'`). */
1224
+ readonly code: ErrorCodeValue;
1225
+ /** Extra contextual data (component name, expression string, etc.). */
1226
+ readonly context: Record<string, any>;
1227
+ /** Original error that caused this one, if any. */
1228
+ readonly cause?: Error;
1229
+
1230
+ constructor(
1231
+ code: ErrorCodeValue,
1232
+ message: string,
1233
+ context?: Record<string, any>,
1234
+ cause?: Error,
1235
+ );
1236
+ }
1237
+
1238
+ /** Error handler callback type. */
1239
+ export type ZQueryErrorHandler = (error: ZQueryError) => void;
1240
+
1241
+ /**
1242
+ * Register a global error handler. Called whenever zQuery catches an
1243
+ * error internally. Pass `null` to remove.
1244
+ */
1245
+ export function onError(handler: ZQueryErrorHandler | null): void;
1246
+
1247
+ /**
1248
+ * Report an error through the global handler and console.
1249
+ * Non-throwing — used for recoverable errors in callbacks, lifecycle hooks, etc.
1250
+ */
1251
+ export function reportError(
1252
+ code: ErrorCodeValue,
1253
+ message: string,
1254
+ context?: Record<string, any>,
1255
+ cause?: Error,
1256
+ ): void;
1257
+
1258
+ /**
1259
+ * Wrap a callback so that thrown errors are caught, reported via `reportError`,
1260
+ * and don't crash the current execution context.
1261
+ */
1262
+ export function guardCallback<T extends (...args: any[]) => any>(
1263
+ fn: T,
1264
+ code: ErrorCodeValue,
1265
+ context?: Record<string, any>,
1266
+ ): (...args: Parameters<T>) => ReturnType<T> | undefined;
1267
+
1268
+ /**
1269
+ * Validate a required value is defined and of the expected type.
1270
+ * Throws `ZQueryError` with `INVALID_ARGUMENT` on failure.
1271
+ */
1272
+ export function validate(value: any, name: string, expectedType?: string): void;
1273
+
1274
+
1049
1275
  // ---------------------------------------------------------------------------
1050
1276
  // $ — Main function & namespace
1051
1277
  // ---------------------------------------------------------------------------
1052
1278
 
1053
1279
  /**
1054
- * Main selector / DOM-ready function.
1280
+ * Main selector / DOM-ready function — always returns a `ZQueryCollection` (like jQuery).
1055
1281
  *
1056
- * - `$('selector')` → single Element via `querySelector`
1057
- * - `$('<div>…</div>')` → create element from HTML
1058
- * - `$(element)` → return as-is
1282
+ * - `$('selector')` → ZQueryCollection via `querySelectorAll`
1283
+ * - `$('<div>…</div>')` → ZQueryCollection from created elements
1284
+ * - `$(element)` → ZQueryCollection wrapping the element
1059
1285
  * - `$(fn)` → DOMContentLoaded shorthand
1060
1286
  */
1061
1287
  interface ZQueryStatic {
1062
- (selector: string, context?: string | Element): Element | null;
1063
- (element: Element): Element;
1064
- (nodeList: NodeList | HTMLCollection | Element[]): Element | null;
1288
+ (selector: string, context?: string | Element): ZQueryCollection;
1289
+ (element: Element): ZQueryCollection;
1290
+ (nodeList: NodeList | HTMLCollection | Element[]): ZQueryCollection;
1065
1291
  (fn: () => void): void;
1066
1292
 
1067
1293
  // -- Collection selector -------------------------------------------------
@@ -1082,12 +1308,14 @@ interface ZQueryStatic {
1082
1308
  id(id: string): Element | null;
1083
1309
  /** `document.querySelector('.name')` */
1084
1310
  class(name: string): Element | null;
1085
- /** `document.getElementsByClassName(name)` as array. */
1086
- classes(name: string): Element[];
1087
- /** `document.getElementsByTagName(name)` as array. */
1088
- tag(name: string): Element[];
1089
- /** Children of `#parentId` as array. */
1090
- children(parentId: string): Element[];
1311
+ /** `document.getElementsByClassName(name)` as `ZQueryCollection`. */
1312
+ classes(name: string): ZQueryCollection;
1313
+ /** `document.getElementsByTagName(name)` as `ZQueryCollection`. */
1314
+ tag(name: string): ZQueryCollection;
1315
+ /** `document.getElementsByName(name)` as `ZQueryCollection`. */
1316
+ name(name: string): ZQueryCollection;
1317
+ /** Children of `#parentId` as `ZQueryCollection`. */
1318
+ children(parentId: string): ZQueryCollection;
1091
1319
 
1092
1320
  // -- Static helpers ------------------------------------------------------
1093
1321
  /**
@@ -1117,6 +1345,7 @@ interface ZQueryStatic {
1117
1345
 
1118
1346
  // -- Reactive ------------------------------------------------------------
1119
1347
  reactive: typeof reactive;
1348
+ Signal: typeof Signal;
1120
1349
  signal: typeof signal;
1121
1350
  computed: typeof computed;
1122
1351
  effect: typeof effect;
@@ -1130,6 +1359,8 @@ interface ZQueryStatic {
1130
1359
  /** Returns all registered component definitions. */
1131
1360
  components: typeof getRegistry;
1132
1361
  style: typeof style;
1362
+ morph: typeof morph;
1363
+ safeEval: typeof safeEval;
1133
1364
 
1134
1365
  // -- Router --------------------------------------------------------------
1135
1366
  router: typeof createRouter;
@@ -1147,6 +1378,14 @@ interface ZQueryStatic {
1147
1378
  patch: HttpClient['patch'];
1148
1379
  delete: HttpClient['delete'];
1149
1380
 
1381
+ // -- Error Handling ------------------------------------------------------
1382
+ /** Register a global error handler (or pass `null` to remove). */
1383
+ onError: typeof onError;
1384
+ /** Structured error class. */
1385
+ ZQueryError: typeof ZQueryError;
1386
+ /** Frozen map of all error code constants. */
1387
+ ErrorCode: typeof ErrorCode;
1388
+
1150
1389
  // -- Utilities -----------------------------------------------------------
1151
1390
  debounce: typeof debounce;
1152
1391
  throttle: typeof throttle;
package/index.js CHANGED
@@ -10,17 +10,20 @@
10
10
  */
11
11
 
12
12
  import { query, queryAll, ZQueryCollection } from './src/core.js';
13
- import { reactive, signal, computed, effect } from './src/reactive.js';
13
+ import { reactive, Signal, signal, computed, effect } from './src/reactive.js';
14
14
  import { component, mount, mountAll, getInstance, destroy, getRegistry, style } from './src/component.js';
15
15
  import { createRouter, getRouter } from './src/router.js';
16
16
  import { createStore, getStore } from './src/store.js';
17
17
  import { http } from './src/http.js';
18
+ import { morph } from './src/diff.js';
19
+ import { safeEval } from './src/expression.js';
18
20
  import {
19
21
  debounce, throttle, pipe, once, sleep,
20
22
  escapeHtml, html, trust, uuid, camelCase, kebabCase,
21
23
  deepClone, deepMerge, isEqual, param, parseQuery,
22
24
  storage, session, bus,
23
25
  } from './src/utils.js';
26
+ import { ZQueryError, ErrorCode, onError, reportError } from './src/errors.js';
24
27
 
25
28
 
26
29
  // ---------------------------------------------------------------------------
@@ -28,16 +31,16 @@ import {
28
31
  // ---------------------------------------------------------------------------
29
32
 
30
33
  /**
31
- * Main selector function
34
+ * Main selector function — always returns a ZQueryCollection (like jQuery).
32
35
  *
33
- * $('selector') → single Element (querySelector)
34
- * $('<div>hello</div>') → create element (first created node)
35
- * $(element) → return element as-is
36
+ * $('selector') → ZQueryCollection (querySelectorAll)
37
+ * $('<div>hello</div>') → ZQueryCollection from created elements
38
+ * $(element) → ZQueryCollection wrapping the element
36
39
  * $(fn) → DOMContentLoaded shorthand
37
40
  *
38
41
  * @param {string|Element|NodeList|Function} selector
39
42
  * @param {string|Element} [context]
40
- * @returns {Element|null}
43
+ * @returns {ZQueryCollection}
41
44
  */
42
45
  function $(selector, context) {
43
46
  // $(fn) → DOM ready shorthand
@@ -49,11 +52,14 @@ function $(selector, context) {
49
52
  }
50
53
 
51
54
 
52
- // --- Quick refs ------------------------------------------------------------
55
+ // --- Quick refs (DOM selectors) --------------------------------------------
53
56
  $.id = query.id;
54
57
  $.class = query.class;
55
58
  $.classes = query.classes;
56
59
  $.tag = query.tag;
60
+ Object.defineProperty($, 'name', {
61
+ value: query.name, writable: true, configurable: true
62
+ });
57
63
  $.children = query.children;
58
64
 
59
65
  // --- Collection selector ---------------------------------------------------
@@ -82,6 +88,7 @@ $.fn = query.fn;
82
88
 
83
89
  // --- Reactive primitives ---------------------------------------------------
84
90
  $.reactive = reactive;
91
+ $.Signal = Signal;
85
92
  $.signal = signal;
86
93
  $.computed = computed;
87
94
  $.effect = effect;
@@ -94,6 +101,8 @@ $.getInstance = getInstance;
94
101
  $.destroy = destroy;
95
102
  $.components = getRegistry;
96
103
  $.style = style;
104
+ $.morph = morph;
105
+ $.safeEval = safeEval;
97
106
 
98
107
  // --- Router ----------------------------------------------------------------
99
108
  $.router = createRouter;
@@ -132,6 +141,11 @@ $.storage = storage;
132
141
  $.session = session;
133
142
  $.bus = bus;
134
143
 
144
+ // --- Error handling --------------------------------------------------------
145
+ $.onError = onError;
146
+ $.ZQueryError = ZQueryError;
147
+ $.ErrorCode = ErrorCode;
148
+
135
149
  // --- Meta ------------------------------------------------------------------
136
150
  $.version = '__VERSION__';
137
151
  $.meta = {}; // populated at build time by CLI bundler
@@ -161,11 +175,14 @@ export {
161
175
  $ as zQuery,
162
176
  ZQueryCollection,
163
177
  queryAll,
164
- reactive, signal, computed, effect,
178
+ reactive, Signal, signal, computed, effect,
165
179
  component, mount, mountAll, getInstance, destroy, getRegistry, style,
180
+ morph,
181
+ safeEval,
166
182
  createRouter, getRouter,
167
183
  createStore, getStore,
168
184
  http,
185
+ ZQueryError, ErrorCode, onError, reportError,
169
186
  debounce, throttle, pipe, once, sleep,
170
187
  escapeHtml, html, trust, uuid, camelCase, kebabCase,
171
188
  deepClone, deepMerge, isEqual, param, parseQuery,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zero-query",
3
- "version": "0.4.9",
3
+ "version": "0.6.3",
4
4
  "description": "Lightweight modern frontend library — jQuery-like selectors, reactive components, SPA router, and state management with zero dependencies.",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -21,7 +21,9 @@
21
21
  "dev": "node cli/index.js dev zquery-website",
22
22
  "dev-lib": "node cli/index.js build --watch",
23
23
  "bundle": "node cli/index.js bundle",
24
- "bundle:app": "node cli/index.js bundle zquery-website"
24
+ "bundle:app": "node cli/index.js bundle zquery-website --minimal",
25
+ "test": "vitest run",
26
+ "test:watch": "vitest"
25
27
  },
26
28
  "keywords": [
27
29
  "dom",
@@ -52,5 +54,9 @@
52
54
  },
53
55
  "dependencies": {
54
56
  "zero-http": "^0.2.3"
57
+ },
58
+ "devDependencies": {
59
+ "jsdom": "^28.1.0",
60
+ "vitest": "^4.0.18"
55
61
  }
56
62
  }