zero-query 1.0.9 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * zQuery (zeroQuery) v1.0.9
2
+ * zQuery (zeroQuery) v1.1.1
3
3
  * Lightweight Frontend Library
4
4
  * https://github.com/tonywied17/zero-query
5
5
  * (c) 2026 Anthony Wiedman - MIT License
@@ -37,10 +37,11 @@ Signal._activeEffect=execute;try{fn();}
37
37
  catch(err){reportError(ErrorCode.EFFECT_EXEC,'Effect function threw',{},err);}
38
38
  finally{Signal._activeEffect=null;}};execute._deps=new Set();execute();return()=>{if(execute._deps){for(const sig of execute._deps){sig._subscribers.delete(execute);}
39
39
  execute._deps.clear();}};}
40
- function batch(fn){if(Signal._batching){fn();return;}
41
- Signal._batching=true;Signal._batchQueue.clear();try{fn();}finally{Signal._batching=false;const subs=new Set();for(const sig of Signal._batchQueue){for(const sub of sig._subscribers){subs.add(sub);}}
40
+ function batch(fn){if(Signal._batching){return fn();}
41
+ Signal._batching=true;Signal._batchQueue.clear();let result;try{result=fn();}finally{Signal._batching=false;const subs=new Set();for(const sig of Signal._batchQueue){for(const sub of sig._subscribers){subs.add(sub);}}
42
42
  Signal._batchQueue.clear();for(const sub of subs){try{sub();}
43
- catch(err){reportError(ErrorCode.SIGNAL_CALLBACK,'Signal subscriber threw',{},err);}}}}
43
+ catch(err){reportError(ErrorCode.SIGNAL_CALLBACK,'Signal subscriber threw',{},err);}}}
44
+ return result;}
44
45
  function untracked(fn){const prev=Signal._activeEffect;Signal._activeEffect=null;try{return fn();}finally{Signal._activeEffect=prev;}}
45
46
  let _tpl=null;function _getTemplate(){if(!_tpl)_tpl=document.createElement('template');return _tpl;}
46
47
  function morph(rootEl,newHTML){const start=typeof window!=='undefined'&&window.__zqMorphHook?performance.now():0;const tpl=_getTemplate();tpl.innerHTML=newHTML;const newRoot=tpl.content;const tempDiv=document.createElement('div');while(newRoot.firstChild)tempDiv.appendChild(newRoot.firstChild);_morphChildren(rootEl,tempDiv);if(start)window.__zqMorphHook(rootEl,performance.now()-start);}
@@ -329,13 +330,23 @@ return undefined;}
329
330
  function _getPath(obj,path){return path.split('.').reduce((o,k)=>o?.[k],obj);}
330
331
  function _setPath(obj,path,value){const keys=path.split('.');const last=keys.pop();const target=keys.reduce((o,k)=>(o&&typeof o==='object')?o[k]:undefined,obj);if(target&&typeof target==='object')target[last]=value;}
331
332
  class Component{constructor(el,definition,props={}){this._uid=++_uid;this._el=el;this._def=definition;this._mounted=false;this._destroyed=false;this._updateQueued=false;this._listeners=[];this._watchCleanups=[];this.refs={};this._slotContent={};const defaultSlotNodes=[];[...el.childNodes].forEach(node=>{if(node.nodeType===1&&node.hasAttribute('slot')){const slotName=node.getAttribute('slot')||'default';if(!this._slotContent[slotName])this._slotContent[slotName]='';this._slotContent[slotName]+=node.outerHTML;}else if(node.nodeType===1||(node.nodeType===3&&node.textContent.trim())){defaultSlotNodes.push(node.nodeType===1?node.outerHTML:node.textContent);}});if(defaultSlotNodes.length){this._slotContent['default']=defaultSlotNodes.join('');}
332
- this.props=Object.freeze({...props});const initialState=typeof definition.state==='function'?definition.state():{...(definition.state||{})};this.state=reactive(initialState,(key,value,old)=>{if(!this._destroyed){this._runWatchers(key,value,old);this._scheduleUpdate();}});this.computed={};if(definition.computed){for(const[name,fn]of Object.entries(definition.computed)){Object.defineProperty(this.computed,name,{get:()=>fn.call(this,this.state.__raw||this.state),enumerable:true});}}
333
+ if(definition.props&&typeof definition.props==='object'&&!Array.isArray(definition.props)){this.props=this._resolveReactiveProps(definition.props,props);this._propObserver=new MutationObserver((mutations)=>{if(this._destroyed)return;let changed=false;for(const mut of mutations){if(mut.type==='attributes'){const attrName=mut.attributeName;if(attrName.startsWith('z-')||attrName.startsWith('@')||attrName.startsWith(':')||attrName.startsWith('data-zq'))continue;const propName=attrName.startsWith(':')?attrName.slice(1):attrName;if(propName in definition.props){changed=true;}}}
334
+ if(changed){this.props=this._resolveReactiveProps(definition.props,{});this._scheduleUpdate();}});this._propObserver.observe(el,{attributes:true});}else{this.props=Object.freeze({...props});}
335
+ this._storeCleanups=[];this.stores={};if(definition.stores&&typeof definition.stores==='object'){for(const[alias,connector]of Object.entries(definition.stores)){if(!connector||!connector._zqConnector)continue;const{store,keys}=connector;const snap={};for(const key of keys){snap[key]=store.state[key];}
336
+ this.stores[alias]=snap;const unsub=store.subscribe(keys,(key,value)=>{this.stores[alias][key]=value;if(!this._destroyed)this._scheduleUpdate();});this._storeCleanups.push(unsub);}}
337
+ const initialState=typeof definition.state==='function'?definition.state():{...(definition.state||{})};this.state=reactive(initialState,(key,value,old)=>{if(!this._destroyed){this._runWatchers(key,value,old);this._scheduleUpdate();}});this.computed={};if(definition.computed){for(const[name,fn]of Object.entries(definition.computed)){Object.defineProperty(this.computed,name,{get:()=>fn.call(this,this.state.__raw||this.state),enumerable:true});}}
333
338
  for(const[key,val]of Object.entries(definition)){if(typeof val==='function'&&!_reservedKeys.has(key)){this[key]=val.bind(this);}}
334
339
  if(definition.init){try{definition.init.call(this);}
335
340
  catch(err){reportError(ErrorCode.COMP_LIFECYCLE,`Component "${definition._name}" init() threw`,{component:definition._name},err);}}
336
341
  if(definition.watch){this._prevWatchValues={};for(const key of Object.keys(definition.watch)){this._prevWatchValues[key]=_getPath(this.state.__raw||this.state,key);}}}
337
342
  _runWatchers(changedKey,value,old){const watchers=this._def.watch;if(!watchers)return;for(const[key,handler]of Object.entries(watchers)){if(changedKey===key||key.startsWith(changedKey+'.')||changedKey.startsWith(key+'.')){const currentVal=_getPath(this.state.__raw||this.state,key);const prevVal=this._prevWatchValues?.[key];if(currentVal!==prevVal){const fn=typeof handler==='function'?handler:handler.handler;if(typeof fn==='function')fn.call(this,currentVal,prevVal);if(this._prevWatchValues)this._prevWatchValues[key]=currentVal;}}}}
338
343
  _scheduleUpdate(){if(this._updateQueued)return;this._updateQueued=true;queueMicrotask(()=>{try{if(!this._destroyed)this._render();}finally{this._updateQueued=false;}});}
344
+ _resolveReactiveProps(propDefs,passedProps){const resolved={};for(const[name,schema]of Object.entries(propDefs)){const def=typeof schema==='object'&&schema!==null?schema:{type:schema};const type=def.type;const defaultVal=def.default;if(name in passedProps){resolved[name]=passedProps[name];continue;}
345
+ let rawAttr=this._el.getAttribute(':'+name);let hasAttr=rawAttr!==null;if(!hasAttr){rawAttr=this._el.getAttribute(name);hasAttr=rawAttr!==null;}
346
+ if(hasAttr&&rawAttr!==null){resolved[name]=this._coercePropValue(rawAttr,type);}else if(defaultVal!==undefined){resolved[name]=typeof defaultVal==='function'?defaultVal():defaultVal;}else{resolved[name]=undefined;}}
347
+ return Object.freeze(resolved);}
348
+ _coercePropValue(raw,type){if(type===Number)return Number(raw);if(type===Boolean)return raw!=='false'&&raw!=='0'&&raw!=='';if(type===Object||type===Array){try{return JSON.parse(raw);}catch{return raw;}}
349
+ return raw;}
339
350
  async _loadExternals(){const def=this._def;const base=def._base;if(def.templateUrl&&!def._templateLoaded){const tu=def.templateUrl;if(typeof tu==='string'){def._externalTemplate=await _fetchResource(_resolveUrl(tu,base));}else if(Array.isArray(tu)){const urls=tu.map(u=>_resolveUrl(u,base));const results=await Promise.all(urls.map(u=>_fetchResource(u)));def._externalTemplates={};results.forEach((html,i)=>{def._externalTemplates[i]=html;});}else if(typeof tu==='object'){const entries=Object.entries(tu);const results=await Promise.all(entries.map(([,url])=>_fetchResource(_resolveUrl(url,base))));def._externalTemplates={};entries.forEach(([key],i)=>{def._externalTemplates[key]=results[i];});}
340
351
  def._templateLoaded=true;}
341
352
  if(def.styleUrl&&!def._styleLoaded){const su=def.styleUrl;if(typeof su==='string'){const resolved=_resolveUrl(su,base);def._externalStyles=await _fetchResource(resolved);def._resolvedStyleUrls=[resolved];}else if(Array.isArray(su)){const urls=su.map(u=>_resolveUrl(u,base));const results=await Promise.all(urls.map(u=>_fetchResource(u)));def._externalStyles=results.join('\n');def._resolvedStyleUrls=urls;}
@@ -389,19 +400,27 @@ parent.replaceChild(fragment,el);}
389
400
  if(root.querySelector('[z-for]'))_recurse(root);};_recurse(temp);return temp.innerHTML;}
390
401
  _expandContentDirectives(html){if(!html.includes('z-html')&&!html.includes('z-text'))return html;const temp=document.createElement('div');temp.innerHTML=html;temp.querySelectorAll('[z-html]').forEach(el=>{if(el.closest('[z-pre]'))return;const val=this._evalExpr(el.getAttribute('z-html'));el.innerHTML=val!=null?String(val):'';el.removeAttribute('z-html');});temp.querySelectorAll('[z-text]').forEach(el=>{if(el.closest('[z-pre]'))return;const val=this._evalExpr(el.getAttribute('z-text'));el.textContent=val!=null?String(val):'';el.removeAttribute('z-text');});return temp.innerHTML;}
391
402
  _processDirectives(){const ifEls=[...this._el.querySelectorAll('[z-if]')];for(const el of ifEls){if(!el.parentNode||el.closest('[z-pre]'))continue;const show=!!this._evalExpr(el.getAttribute('z-if'));const chain=[{el,show}];let sib=el.nextElementSibling;while(sib){if(sib.hasAttribute('z-else-if')){chain.push({el:sib,show:!!this._evalExpr(sib.getAttribute('z-else-if'))});sib=sib.nextElementSibling;}else if(sib.hasAttribute('z-else')){chain.push({el:sib,show:true});break;}else{break;}}
392
- let found=false;for(const item of chain){if(!found&&item.show){found=true;item.el.removeAttribute('z-if');item.el.removeAttribute('z-else-if');item.el.removeAttribute('z-else');}else{item.el.remove();}}}
393
- this._el.querySelectorAll('[z-show]').forEach(el=>{if(el.closest('[z-pre]'))return;const show=!!this._evalExpr(el.getAttribute('z-show'));el.style.display=show?'':'none';el.removeAttribute('z-show');});const walker=document.createTreeWalker(this._el,NodeFilter.SHOW_ELEMENT,{acceptNode(n){return n.hasAttribute('z-pre')?NodeFilter.FILTER_REJECT:NodeFilter.FILTER_ACCEPT;}});let node;while((node=walker.nextNode())){const attrs=node.attributes;for(let i=attrs.length-1;i>=0;i--){const attr=attrs[i];let attrName;if(attr.name.startsWith('z-bind:'))attrName=attr.name.slice(7);else if(attr.name.charCodeAt(0)===58&&attr.name.charCodeAt(1)!==58)attrName=attr.name.slice(1);else continue;const val=this._evalExpr(attr.value);node.removeAttribute(attr.name);if(val===false||val===null||val===undefined){node.removeAttribute(attrName);}else if(val===true){node.setAttribute(attrName,'');}else{node.setAttribute(attrName,String(val));}}}
403
+ let found=false;for(const item of chain){if(!found&&item.show){found=true;item.el.removeAttribute('z-if');item.el.removeAttribute('z-else-if');item.el.removeAttribute('z-else');const transName=item.el.getAttribute('z-transition');if(transName){item.el.removeAttribute('z-transition');this._transitionEnter(item.el,transName);}}else{const transName=item.el.getAttribute('z-transition');if(transName){this._transitionLeave(item.el,transName,()=>item.el.remove());}else{item.el.remove();}}}}
404
+ this._el.querySelectorAll('[z-show]').forEach(el=>{if(el.closest('[z-pre]'))return;const show=!!this._evalExpr(el.getAttribute('z-show'));const transName=el.getAttribute('z-transition');const wasHidden=el.style.display==='none'||el.hasAttribute('data-zq-hidden');if(transName){el.removeAttribute('z-show');if(show&&wasHidden){el.style.display='';el.removeAttribute('data-zq-hidden');this._transitionEnter(el,transName);}else if(!show&&!wasHidden){el.setAttribute('data-zq-hidden','');this._transitionLeave(el,transName,()=>{el.style.display='none';});}else{el.style.display=show?'':'none';if(!show)el.setAttribute('data-zq-hidden','');else el.removeAttribute('data-zq-hidden');}}else{el.style.display=show?'':'none';el.removeAttribute('z-show');}});const walker=document.createTreeWalker(this._el,NodeFilter.SHOW_ELEMENT,{acceptNode(n){return n.hasAttribute('z-pre')?NodeFilter.FILTER_REJECT:NodeFilter.FILTER_ACCEPT;}});let node;while((node=walker.nextNode())){const attrs=node.attributes;for(let i=attrs.length-1;i>=0;i--){const attr=attrs[i];let attrName;if(attr.name.startsWith('z-bind:'))attrName=attr.name.slice(7);else if(attr.name.charCodeAt(0)===58&&attr.name.charCodeAt(1)!==58)attrName=attr.name.slice(1);else continue;const val=this._evalExpr(attr.value);node.removeAttribute(attr.name);if(val===false||val===null||val===undefined){node.removeAttribute(attrName);}else if(val===true){node.setAttribute(attrName,'');}else{node.setAttribute(attrName,String(val));}}}
394
405
  this._el.querySelectorAll('[z-class]').forEach(el=>{if(el.closest('[z-pre]'))return;const val=this._evalExpr(el.getAttribute('z-class'));if(typeof val==='string'){val.split(/\s+/).filter(Boolean).forEach(c=>el.classList.add(c));}else if(Array.isArray(val)){val.filter(Boolean).forEach(c=>el.classList.add(String(c)));}else if(val&&typeof val==='object'){for(const[cls,active]of Object.entries(val)){el.classList.toggle(cls,!!active);}}
395
406
  el.removeAttribute('z-class');});this._el.querySelectorAll('[z-style]').forEach(el=>{if(el.closest('[z-pre]'))return;const val=this._evalExpr(el.getAttribute('z-style'));if(typeof val==='string'){el.style.cssText+=';'+val;}else if(val&&typeof val==='object'){for(const[prop,v]of Object.entries(val)){el.style[prop]=v;}}
396
407
  el.removeAttribute('z-style');});this._el.querySelectorAll('[z-cloak]').forEach(el=>{el.removeAttribute('z-cloak');});}
408
+ _transitionEnter(el,name){const cfg=this._def.transition;if(cfg&&cfg.enter){el.classList.add(cfg.enter);const duration=cfg.duration||0;const cleanup=()=>el.classList.remove(cfg.enter);if(duration>0){setTimeout(cleanup,duration);}else{el.addEventListener('transitionend',cleanup,{once:true});el.addEventListener('animationend',cleanup,{once:true});}
409
+ return;}
410
+ el.classList.add(`${name}-enter-from`,`${name}-enter-active`);void el.offsetHeight;requestAnimationFrame(()=>{el.classList.remove(`${name}-enter-from`);el.classList.add(`${name}-enter-to`);const onEnd=()=>{el.classList.remove(`${name}-enter-active`,`${name}-enter-to`);};el.addEventListener('transitionend',onEnd,{once:true});el.addEventListener('animationend',onEnd,{once:true});});}
411
+ _transitionLeave(el,name,done){const cfg=this._def.transition;if(cfg&&cfg.leave){el.classList.add(cfg.leave);const duration=cfg.duration||0;const cleanup=()=>{el.classList.remove(cfg.leave);done();};if(duration>0){setTimeout(cleanup,duration);}else{el.addEventListener('transitionend',cleanup,{once:true});el.addEventListener('animationend',cleanup,{once:true});}
412
+ return;}
413
+ el.classList.add(`${name}-leave-from`,`${name}-leave-active`);void el.offsetHeight;requestAnimationFrame(()=>{el.classList.remove(`${name}-leave-from`);el.classList.add(`${name}-leave-to`);const onEnd=()=>{el.classList.remove(`${name}-leave-active`,`${name}-leave-to`);done();};el.addEventListener('transitionend',onEnd,{once:true});el.addEventListener('animationend',onEnd,{once:true});});}
397
414
  setState(partial){if(partial&&Object.keys(partial).length>0){Object.assign(this.state,partial);}else{this._scheduleUpdate();}}
398
415
  emit(name,detail){this._el.dispatchEvent(new CustomEvent(name,{detail,bubbles:true,cancelable:true}));}
399
416
  destroy(){if(this._destroyed)return;this._destroyed=true;if(this._def.destroyed){try{this._def.destroyed.call(this);}
400
417
  catch(err){reportError(ErrorCode.COMP_LIFECYCLE,`Component "${this._def._name}" destroyed() threw`,{component:this._def._name},err);}}
418
+ if(this._propObserver){this._propObserver.disconnect();this._propObserver=null;}
419
+ if(this._storeCleanups){this._storeCleanups.forEach(fn=>fn());this._storeCleanups=[];}
401
420
  this._listeners.forEach(({event,handler})=>this._el.removeEventListener(event,handler));this._listeners=[];if(this._outsideListeners){this._outsideListeners.forEach(({event,handler})=>document.removeEventListener(event,handler,true));this._outsideListeners=[];}
402
421
  this._delegatedEvents=null;this._eventBindings=null;const allEls=this._el.querySelectorAll('*');allEls.forEach(child=>{const dTimers=_debounceTimers.get(child);if(dTimers){for(const key in dTimers)clearTimeout(dTimers[key]);_debounceTimers.delete(child);}
403
422
  const tTimers=_throttleTimers.get(child);if(tTimers){for(const key in tTimers)clearTimeout(tTimers[key]);_throttleTimers.delete(child);}});if(this._styleEl)this._styleEl.remove();_instances.delete(this._el);this._el.innerHTML='';}}
404
- const _reservedKeys=new Set(['state','render','styles','init','mounted','updated','destroyed','props','templateUrl','styleUrl','templates','base','computed','watch']);function component(name,definition){if(!name||typeof name!=='string'){throw new ZQueryError(ErrorCode.COMP_INVALID_NAME,'Component name must be a non-empty string');}
423
+ const _reservedKeys=new Set(['state','render','styles','init','mounted','updated','destroyed','props','templateUrl','styleUrl','templates','base','computed','watch','stores','transition','activated','deactivated']);function component(name,definition){if(!name||typeof name!=='string'){throw new ZQueryError(ErrorCode.COMP_INVALID_NAME,'Component name must be a non-empty string');}
405
424
  if(!name.includes('-')){throw new ZQueryError(ErrorCode.COMP_INVALID_NAME,`Component name "${name}" must contain a hyphen (Web Component convention)`);}
406
425
  definition._name=name;if(definition.base!==undefined){definition._base=definition.base;}else{definition._base=_detectCallerBase();}
407
426
  _registry.set(name,definition);}
@@ -423,7 +442,7 @@ const link=document.createElement('link');link.rel='stylesheet';link.href=url;li
423
442
  const ready=Promise.all(loadPromises).then(()=>{if(_criticalStyle){_criticalStyle.remove();}});return{ready,remove(){for(const el of elements){el.remove();for(const[k,v]of _globalStyles){if(v===el){_globalStyles.delete(k);break;}}}}};}
424
443
  const _ZQ_STATE_KEY='__zq';function _shallowEqual(a,b){if(a===b)return true;if(!a||!b)return false;const keysA=Object.keys(a);const keysB=Object.keys(b);if(keysA.length!==keysB.length)return false;for(let i=0;i<keysA.length;i++){const k=keysA[i];if(a[k]!==b[k])return false;}
425
444
  return true;}
426
- 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;}
445
+ class Router{constructor(config={}){this._el=null;const isFile=typeof location!=='undefined'&&location.protocol==='file:';this._mode=isFile?'hash':(config.mode||'history');this._keepAliveCache=new Map();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;}
427
446
  catch{rawBase=baseEl.getAttribute('href')||'';}
428
447
  if(rawBase==='/')rawBase='';}}}
429
448
  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;this._substateListeners=[];this._inSubstate=false;if(config.el){this._el=typeof config.el==='string'?document.querySelector(config.el):config.el;}else if(typeof document!=='undefined'){const outlet=document.querySelector('z-outlet');if(outlet){this._el=outlet;if(!config.fallback&&outlet.getAttribute('fallback')){this._fallback=outlet.getAttribute('fallback');}
@@ -481,17 +500,25 @@ return this.__resolve();}}catch(err){reportError(ErrorCode.ROUTER_GUARD,'Before-
481
500
  if(matched.load){try{await matched.load();}
482
501
  catch(err){reportError(ErrorCode.ROUTER_LOAD,`Failed to load module for route "${matched.path}"`,{path:matched.path},err);return;}}
483
502
  this._current=to;if(this._el&&matched.component){if(typeof matched.component==='string'){await prefetch(matched.component);}
484
- if(this._instance){this._instance.destroy();this._instance=null;}
485
- const _routeStart=typeof window!=='undefined'&&window.__zqRenderHook?performance.now():0;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);try{this._instance=mount(container,matched.component,props);}catch(err){reportError(ErrorCode.COMP_NOT_FOUND,`Failed to mount component for route "${matched.path}"`,{component:matched.component,path:matched.path},err);return;}
486
- if(_routeStart)window.__zqRenderHook(this._el,performance.now()-_routeStart,'route',matched.component);}
487
- else if(typeof matched.component==='function'){this._el.innerHTML=matched.component(to);if(_routeStart)window.__zqRenderHook(this._el,performance.now()-_routeStart,'route',to);}}
503
+ const isKeepAlive=!!matched.keepAlive;const componentName=typeof matched.component==='string'?matched.component:null;if(this._instance&&this._currentKeepAlive&&this._currentComponentName){const cached=this._keepAliveCache.get(this._currentComponentName);if(cached){cached.container.style.display='none';if(cached.instance._def.deactivated){try{cached.instance._def.deactivated.call(cached.instance);}
504
+ catch(err){reportError(ErrorCode.COMP_LIFECYCLE,`Component "${this._currentComponentName}" deactivated() threw`,{component:this._currentComponentName},err);}}}
505
+ this._instance=null;}else if(this._instance){this._instance.destroy();this._instance=null;}
506
+ const _routeStart=typeof window!=='undefined'&&window.__zqRenderHook?performance.now():0;const props={...params,$route:to,$query:query,$params:params};if(isKeepAlive&&componentName&&this._keepAliveCache.has(componentName)){const cached=this._keepAliveCache.get(componentName);[...this._el.children].forEach(c=>{c.style.display='none';});cached.container.style.display='';this._instance=cached.instance;this._currentKeepAlive=true;this._currentComponentName=componentName;if(cached.instance._def.activated){try{cached.instance._def.activated.call(cached.instance);}
507
+ catch(err){reportError(ErrorCode.COMP_LIFECYCLE,`Component "${componentName}" activated() threw`,{component:componentName},err);}}
508
+ if(_routeStart)window.__zqRenderHook(this._el,performance.now()-_routeStart,'route',componentName);}
509
+ else if(componentName){[...this._el.children].forEach(c=>{if(c.dataset.zqKeepAlive){c.style.display='none';}});[...this._el.children].forEach(c=>{if(!c.dataset.zqKeepAlive)c.remove();});const container=document.createElement(componentName);if(isKeepAlive)container.dataset.zqKeepAlive=componentName;this._el.appendChild(container);try{this._instance=mount(container,componentName,props);}catch(err){reportError(ErrorCode.COMP_NOT_FOUND,`Failed to mount component for route "${matched.path}"`,{component:matched.component,path:matched.path},err);return;}
510
+ if(isKeepAlive){this._keepAliveCache.set(componentName,{container,instance:this._instance});if(this._instance._def.activated){try{this._instance._def.activated.call(this._instance);}
511
+ catch(err){reportError(ErrorCode.COMP_LIFECYCLE,`Component "${componentName}" activated() threw`,{component:componentName},err);}}}
512
+ this._currentKeepAlive=isKeepAlive;this._currentComponentName=componentName;if(_routeStart)window.__zqRenderHook(this._el,performance.now()-_routeStart,'route',componentName);}
513
+ else if(typeof matched.component==='function'){[...this._el.children].forEach(c=>{if(c.dataset.zqKeepAlive)c.style.display='none';else c.remove();});const wrapper=document.createElement('div');wrapper.innerHTML=matched.component(to);while(wrapper.firstChild)this._el.appendChild(wrapper.firstChild);this._currentKeepAlive=false;this._currentComponentName=null;if(_routeStart)window.__zqRenderHook(this._el,performance.now()-_routeStart,'route',to);}}
488
514
  this._updateActiveRoutes(path);for(const guard of this._guards.after){await guard(to,from);}
489
515
  this._listeners.forEach(fn=>fn(to,from));}
490
516
  _updateActiveRoutes(currentPath){if(typeof document==='undefined')return;const els=document.querySelectorAll('[z-active-route]');for(let i=0;i<els.length;i++){const el=els[i];const route=el.getAttribute('z-active-route');const cls=el.getAttribute('z-active-class')||'active';const exact=el.hasAttribute('z-active-exact');const isActive=exact?currentPath===route:(route==='/'?currentPath==='/':currentPath.startsWith(route));el.classList.toggle(cls,isActive);}}
491
517
  destroy(){if(this._onNavEvent){window.removeEventListener(this._mode==='hash'?'hashchange':'popstate',this._onNavEvent);this._onNavEvent=null;}
492
518
  if(this._onPopState){window.removeEventListener('popstate',this._onPopState);this._onPopState=null;}
493
519
  if(this._onLinkClick){document.removeEventListener('click',this._onLinkClick);this._onLinkClick=null;}
494
- if(this._instance)this._instance.destroy();this._listeners.clear();this._substateListeners=[];this._inSubstate=false;this._routes=[];this._guards={before:[],after:[]};}}
520
+ for(const[,cached]of this._keepAliveCache){cached.instance.destroy();}
521
+ this._keepAliveCache.clear();if(this._instance)this._instance.destroy();this._listeners.clear();this._substateListeners=[];this._inSubstate=false;this._routes=[];this._guards={before:[],after:[]};}}
495
522
  function compilePath(path){const keys=[];const pattern=path.replace(/:(\w+)/g,(_,key)=>{keys.push(key);return'([^/]+)';}).replace(/\*/g,'(.*)');return{regex:new RegExp(`^${pattern}$`),keys};}
496
523
  function matchRoute(routes,pathname,fallback='not-found'){for(const route of routes){const{regex,keys}=compilePath(route.path);const m=pathname.match(regex);if(m){const params={};keys.forEach((key,i)=>{params[key]=m[i+1];});return{component:route.component,params};}
497
524
  if(route.fallback){const fb=compilePath(route.fallback);const fbm=pathname.match(fb.regex);if(fbm){const params={};fb.keys.forEach((key,i)=>{params[key]=fbm[i+1];});return{component:route.component,params};}}}
@@ -503,8 +530,9 @@ this._notifySubscribers(key,value,old);});this.getters={};for(const[name,fn]of O
503
530
  _notifySubscribers(key,value,old){const subs=this._subscribers.get(key);if(subs)subs.forEach(fn=>{try{fn(key,value,old);}
504
531
  catch(err){reportError(ErrorCode.STORE_SUBSCRIBE,`Subscriber for "${key}" threw`,{key},err);}});this._wildcards.forEach(fn=>{try{fn(key,value,old);}
505
532
  catch(err){reportError(ErrorCode.STORE_SUBSCRIBE,'Wildcard subscriber threw',{key},err);}});}
506
- batch(fn){this._batching=true;this._batchQueue=[];try{fn(this.state);}finally{this._batching=false;const last=new Map();for(const entry of this._batchQueue){last.set(entry.key,entry);}
507
- this._batchQueue=[];for(const{key,value,old}of last.values()){this._notifySubscribers(key,value,old);}}}
533
+ batch(fn){this._batching=true;this._batchQueue=[];let result;try{result=fn(this.state);}finally{this._batching=false;const last=new Map();for(const entry of this._batchQueue){last.set(entry.key,entry);}
534
+ this._batchQueue=[];for(const{key,value,old}of last.values()){this._notifySubscribers(key,value,old);}}
535
+ return result;}
508
536
  checkpoint(){const snap=JSON.parse(JSON.stringify(this.state.__raw||this.state));this._undoStack.push(snap);if(this._undoStack.length>this._maxUndo){this._undoStack.splice(0,this._undoStack.length-this._maxUndo);}
509
537
  this._redoStack=[];}
510
538
  undo(){if(this._undoStack.length===0)return false;const current=JSON.parse(JSON.stringify(this.state.__raw||this.state));this._redoStack.push(current);const prev=this._undoStack.pop();this.replaceState(prev);return true;}
@@ -517,6 +545,7 @@ if(this._debug){console.log(`%c[Store] ${name}`,'color: #4CAF50; font-weight: bo
517
545
  try{const result=action(this.state,...args);this._history.push({action:name,args,timestamp:Date.now()});if(this._history.length>this._maxHistory){this._history.splice(0,this._history.length-this._maxHistory);}
518
546
  return result;}catch(err){reportError(ErrorCode.STORE_ACTION,`Action "${name}" threw`,{action:name,args},err);}}
519
547
  subscribe(keyOrFn,fn){if(typeof keyOrFn==='function'){this._wildcards.add(keyOrFn);return()=>this._wildcards.delete(keyOrFn);}
548
+ if(Array.isArray(keyOrFn)){const keys=keyOrFn;const handler=(key,value,old)=>{if(keys.includes(key))fn(key,value,old);};this._wildcards.add(handler);return()=>this._wildcards.delete(handler);}
520
549
  if(!this._subscribers.has(keyOrFn)){this._subscribers.set(keyOrFn,new Set());}
521
550
  this._subscribers.get(keyOrFn).add(fn);return()=>this._subscribers.get(keyOrFn)?.delete(fn);}
522
551
  snapshot(){return JSON.parse(JSON.stringify(this.state.__raw||this.state));}
@@ -528,6 +557,7 @@ reset(initialState){this.replaceState(initialState||JSON.parse(JSON.stringify(th
528
557
  let _stores=new Map();function createStore(name,config){if(typeof name==='object'){config=name;name='default';}
529
558
  const store=new Store(config);_stores.set(name,store);return store;}
530
559
  function getStore(name='default'){return _stores.get(name)||null;}
560
+ function connectStore(store,keys){return{_zqConnector:true,store,keys};}
531
561
  const _config={baseURL:'',headers:{'Content-Type':'application/json'},timeout:30000,};const _interceptors={request:[],response:[],};async function request(method,url,data,options={}){if(!url||typeof url!=='string'){throw new Error(`HTTP request requires a URL string, got ${typeof url}`);}
532
562
  let fullURL=url.startsWith('http')?url:_config.baseURL+url;let headers={..._config.headers,...options.headers};let body=undefined;const fetchOpts={method:method.toUpperCase(),headers,...options,};if(data!==undefined&&method!=='GET'&&method!=='HEAD'){if(data instanceof FormData){body=data;delete fetchOpts.headers['Content-Type'];}else if(typeof data==='object'){body=JSON.stringify(data);}else{body=data;}
533
563
  fetchOpts.body=body;}
@@ -601,6 +631,6 @@ function retry(fn,opts={}){const{attempts=3,delay=1000,backoff=1}=opts;return ne
601
631
  function timeout(promise,ms,message){let timer;const race=Promise.race([promise,new Promise((_,reject)=>{timer=setTimeout(()=>reject(new Error(message||`Timed out after ${ms}ms`)),ms);})]);return race.finally(()=>clearTimeout(timer));}
602
632
  function $(selector,context){if(typeof selector==='function'){query.ready(selector);return;}
603
633
  return query(selector,context);}
604
- $.id=query.id;$.class=query.class;$.classes=query.classes;$.tag=query.tag;Object.defineProperty($,'name',{value:query.name,writable:true,configurable:true});$.children=query.children;$.qs=query.qs;$.qsa=query.qsa;$.all=function(selector,context){return queryAll(selector,context);};$.create=query.create;$.ready=query.ready;$.on=query.on;$.off=query.off;$.fn=query.fn;$.reactive=reactive;$.Signal=Signal;$.signal=signal;$.computed=computed;$.effect=effect;$.batch=batch;$.untracked=untracked;$.component=component;$.mount=mount;$.mountAll=mountAll;$.getInstance=getInstance;$.destroy=destroy;$.components=getRegistry;$.prefetch=prefetch;$.style=style;$.morph=morph;$.morphElement=morphElement;$.safeEval=safeEval;$.router=createRouter;$.getRouter=getRouter;$.matchRoute=matchRoute;$.store=createStore;$.getStore=getStore;$.http=http;$.get=http.get;$.post=http.post;$.put=http.put;$.patch=http.patch;$.delete=http.delete;$.head=http.head;$.debounce=debounce;$.throttle=throttle;$.pipe=pipe;$.once=once;$.sleep=sleep;$.escapeHtml=escapeHtml;$.stripHtml=stripHtml;$.html=html;$.trust=trust;$.TrustedHTML=TrustedHTML;$.uuid=uuid;$.camelCase=camelCase;$.kebabCase=kebabCase;$.deepClone=deepClone;$.deepMerge=deepMerge;$.isEqual=isEqual;$.param=param;$.parseQuery=parseQuery;$.storage=storage;$.session=session;$.EventBus=EventBus;$.bus=bus;$.range=range;$.unique=unique;$.chunk=chunk;$.groupBy=groupBy;$.pick=pick;$.omit=omit;$.getPath=getPath;$.setPath=setPath;$.isEmpty=isEmpty;$.capitalize=capitalize;$.truncate=truncate;$.clamp=clamp;$.memoize=memoize;$.retry=retry;$.timeout=timeout;$.onError=onError;$.ZQueryError=ZQueryError;$.ErrorCode=ErrorCode;$.guardCallback=guardCallback;$.guardAsync=guardAsync;$.validate=validate;$.formatError=formatError;$.version='1.0.9';$.libSize='~108 KB';$.unitTests={"passed":1965,"failed":0,"total":1965,"suites":525,"duration":3752,"ok":true};$.meta={};$.noConflict=()=>{if(typeof window!=='undefined'&&window.$===$){delete window.$;}
634
+ $.id=query.id;$.class=query.class;$.classes=query.classes;$.tag=query.tag;Object.defineProperty($,'name',{value:query.name,writable:true,configurable:true});$.children=query.children;$.qs=query.qs;$.qsa=query.qsa;$.all=function(selector,context){return queryAll(selector,context);};$.create=query.create;$.ready=query.ready;$.on=query.on;$.off=query.off;$.fn=query.fn;$.reactive=reactive;$.Signal=Signal;$.signal=signal;$.computed=computed;$.effect=effect;$.batch=batch;$.untracked=untracked;$.component=component;$.mount=mount;$.mountAll=mountAll;$.getInstance=getInstance;$.destroy=destroy;$.components=getRegistry;$.prefetch=prefetch;$.style=style;$.morph=morph;$.morphElement=morphElement;$.safeEval=safeEval;$.router=createRouter;$.getRouter=getRouter;$.matchRoute=matchRoute;$.store=createStore;$.getStore=getStore;$.connectStore=connectStore;$.http=http;$.get=http.get;$.post=http.post;$.put=http.put;$.patch=http.patch;$.delete=http.delete;$.head=http.head;$.debounce=debounce;$.throttle=throttle;$.pipe=pipe;$.once=once;$.sleep=sleep;$.escapeHtml=escapeHtml;$.stripHtml=stripHtml;$.html=html;$.trust=trust;$.TrustedHTML=TrustedHTML;$.uuid=uuid;$.camelCase=camelCase;$.kebabCase=kebabCase;$.deepClone=deepClone;$.deepMerge=deepMerge;$.isEqual=isEqual;$.param=param;$.parseQuery=parseQuery;$.storage=storage;$.session=session;$.EventBus=EventBus;$.bus=bus;$.range=range;$.unique=unique;$.chunk=chunk;$.groupBy=groupBy;$.pick=pick;$.omit=omit;$.getPath=getPath;$.setPath=setPath;$.isEmpty=isEmpty;$.capitalize=capitalize;$.truncate=truncate;$.clamp=clamp;$.memoize=memoize;$.retry=retry;$.timeout=timeout;$.onError=onError;$.ZQueryError=ZQueryError;$.ErrorCode=ErrorCode;$.guardCallback=guardCallback;$.guardAsync=guardAsync;$.validate=validate;$.formatError=formatError;$.version='1.1.1';$.libSize='~115 KB';$.unitTests={"passed":2281,"failed":0,"total":2281,"suites":565,"duration":6929,"ok":true};$.meta={};$.isElectron=typeof navigator!=='undefined'&&/Electron/i.test(navigator.userAgent)||typeof process!=='undefined'&&process.versions!=null&&!!process.versions.electron;$.platform=$.isElectron?'electron':typeof window!=='undefined'?'browser':'node';$.noConflict=()=>{if(typeof window!=='undefined'&&window.$===$){delete window.$;}
605
635
  return $;};if(typeof window!=='undefined'){window.$=$;window.zQuery=$;}
606
636
  $;})(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 1.0.9
7
+ * @version 1.1.0
8
8
  * @license MIT
9
9
  * @see https://z-query.com/docs
10
10
  */
@@ -153,7 +153,7 @@ import type { createStore, getStore } from './types/store';
153
153
  import type { HttpClient } from './types/http';
154
154
  import type {
155
155
  debounce, throttle, pipe, once, sleep,
156
- escapeHtml, stripHtml, html, trust, uuid, camelCase, kebabCase,
156
+ escapeHtml, stripHtml, html, trust, TrustedHTML, uuid, camelCase, kebabCase,
157
157
  deepClone, deepMerge, isEqual, param, parseQuery,
158
158
  StorageWrapper, EventBus,
159
159
  range, unique, chunk, groupBy,
@@ -161,7 +161,7 @@ import type {
161
161
  capitalize, truncate, clamp,
162
162
  MemoizedFunction, memoize, RetryOptions, retry, timeout,
163
163
  } from './types/utils';
164
- import type { onError, ZQueryError, ErrorCode, guardCallback, validate } from './types/errors';
164
+ import type { onError, ZQueryError, ErrorCode, guardCallback, guardAsync, validate, formatError } from './types/errors';
165
165
  import type { morph, morphElement, safeEval } from './types/misc';
166
166
 
167
167
  /**
@@ -279,6 +279,7 @@ interface ZQueryStatic {
279
279
  put: HttpClient['put'];
280
280
  patch: HttpClient['patch'];
281
281
  delete: HttpClient['delete'];
282
+ head: HttpClient['head'];
282
283
 
283
284
  // -- Error Handling ------------------------------------------------------
284
285
  /** Register a global error handler (or pass `null` to remove). */
@@ -289,8 +290,12 @@ interface ZQueryStatic {
289
290
  ErrorCode: typeof ErrorCode;
290
291
  /** Wrap a callback so thrown errors are caught and reported via the global handler. */
291
292
  guardCallback: typeof guardCallback;
293
+ /** Wrap an async function so thrown errors are caught and reported via the global handler. */
294
+ guardAsync: typeof guardAsync;
292
295
  /** Validate a required value is defined and of the expected type. */
293
296
  validate: typeof validate;
297
+ /** Format a ZQueryError into a structured plain object. */
298
+ formatError: typeof formatError;
294
299
 
295
300
  // -- Utilities -----------------------------------------------------------
296
301
  debounce: typeof debounce;
@@ -303,6 +308,7 @@ interface ZQueryStatic {
303
308
  stripHtml: typeof stripHtml;
304
309
  html: typeof html;
305
310
  trust: typeof trust;
311
+ TrustedHTML: typeof TrustedHTML;
306
312
  uuid: typeof uuid;
307
313
  camelCase: typeof camelCase;
308
314
  kebabCase: typeof kebabCase;
package/index.js CHANGED
@@ -13,7 +13,7 @@ import { query, queryAll, ZQueryCollection } from './src/core.js';
13
13
  import { reactive, Signal, signal, computed, effect, batch, untracked } from './src/reactive.js';
14
14
  import { component, mount, mountAll, getInstance, destroy, getRegistry, prefetch, style } from './src/component.js';
15
15
  import { createRouter, getRouter, matchRoute } from './src/router.js';
16
- import { createStore, getStore } from './src/store.js';
16
+ import { createStore, getStore, connectStore } from './src/store.js';
17
17
  import { http } from './src/http.js';
18
18
  import { morph, morphElement } from './src/diff.js';
19
19
  import { safeEval } from './src/expression.js';
@@ -122,6 +122,7 @@ $.matchRoute = matchRoute;
122
122
  // --- Store -----------------------------------------------------------------
123
123
  $.store = createStore;
124
124
  $.getStore = getStore;
125
+ $.connectStore = connectStore;
125
126
 
126
127
  // --- HTTP ------------------------------------------------------------------
127
128
  $.http = http;
@@ -186,6 +187,13 @@ $.libSize = '__LIB_SIZE__';
186
187
  $.unitTests = '__UNIT_TESTS__';
187
188
  $.meta = {}; // populated at build time by CLI bundler
188
189
 
190
+ // --- Environment detection -------------------------------------------------
191
+ $.isElectron = typeof navigator !== 'undefined' && /Electron/i.test(navigator.userAgent)
192
+ || typeof process !== 'undefined' && process.versions != null && !!process.versions.electron;
193
+ $.platform = $.isElectron ? 'electron'
194
+ : typeof window !== 'undefined' ? 'browser'
195
+ : 'node';
196
+
189
197
  $.noConflict = () => {
190
198
  if (typeof window !== 'undefined' && window.$ === $) {
191
199
  delete window.$;
@@ -216,7 +224,7 @@ export {
216
224
  morph, morphElement,
217
225
  safeEval,
218
226
  createRouter, getRouter, matchRoute,
219
- createStore, getStore,
227
+ createStore, getStore, connectStore,
220
228
  http,
221
229
  ZQueryError, ErrorCode, onError, reportError, guardCallback, guardAsync, validate, formatError,
222
230
  debounce, throttle, pipe, once, sleep,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zero-query",
3
- "version": "1.0.9",
3
+ "version": "1.1.1",
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",
@@ -30,6 +30,7 @@
30
30
  "dev-lib": "node cli/index.js build --watch",
31
31
  "bundle": "node cli/index.js bundle",
32
32
  "bundle:app": "node cli/index.js bundle zquery-website",
33
+ "build:api": "node cli/commands/build-api.js",
33
34
  "test": "vitest run",
34
35
  "test:watch": "vitest",
35
36
  "test:ssr": "node tests/test-ssr.js"
package/src/component.js CHANGED
@@ -210,8 +210,58 @@ class Component {
210
210
  this._slotContent['default'] = defaultSlotNodes.join('');
211
211
  }
212
212
 
213
- // Props (read-only from parent)
214
- this.props = Object.freeze({ ...props });
213
+ // Props - reactive when definition.props is defined, frozen otherwise
214
+ if (definition.props && typeof definition.props === 'object' && !Array.isArray(definition.props)) {
215
+ // Reactive props with type coercion and defaults
216
+ this.props = this._resolveReactiveProps(definition.props, props);
217
+ // MutationObserver to re-read props when parent re-renders and changes attributes
218
+ this._propObserver = new MutationObserver((mutations) => {
219
+ if (this._destroyed) return;
220
+ let changed = false;
221
+ for (const mut of mutations) {
222
+ if (mut.type === 'attributes') {
223
+ const attrName = mut.attributeName;
224
+ // Skip internal attributes
225
+ if (attrName.startsWith('z-') || attrName.startsWith('@') || attrName.startsWith(':') || attrName.startsWith('data-zq')) continue;
226
+ // Check if this is a defined prop (attribute names are lowercase)
227
+ const propName = attrName.startsWith(':') ? attrName.slice(1) : attrName;
228
+ if (propName in definition.props) {
229
+ changed = true;
230
+ }
231
+ }
232
+ }
233
+ if (changed) {
234
+ this.props = this._resolveReactiveProps(definition.props, {});
235
+ this._scheduleUpdate();
236
+ }
237
+ });
238
+ this._propObserver.observe(el, { attributes: true });
239
+ } else {
240
+ // Legacy: frozen props from parent
241
+ this.props = Object.freeze({ ...props });
242
+ }
243
+
244
+ // Store connectors - auto-subscribe to store keys
245
+ this._storeCleanups = [];
246
+ this.stores = {};
247
+ if (definition.stores && typeof definition.stores === 'object') {
248
+ for (const [alias, connector] of Object.entries(definition.stores)) {
249
+ if (!connector || !connector._zqConnector) continue;
250
+ const { store, keys } = connector;
251
+ // Initialize snapshot
252
+ const snap = {};
253
+ for (const key of keys) {
254
+ snap[key] = store.state[key];
255
+ }
256
+ this.stores[alias] = snap;
257
+ // Subscribe to changes
258
+ const unsub = store.subscribe(keys, (key, value) => {
259
+ this.stores[alias][key] = value;
260
+ if (!this._destroyed) this._scheduleUpdate();
261
+ });
262
+ this._storeCleanups.push(unsub);
263
+ }
264
+ }
215
265
 
216
266
  // Reactive state
217
267
  const initialState = typeof definition.state === 'function'
@@ -290,6 +340,61 @@ class Component {
290
340
  });
291
341
  }
292
342
 
343
+ /**
344
+ * Resolve reactive props from the definition's prop schema.
345
+ * Reads from element attributes, applies type coercion and defaults.
346
+ * Passed props (from mount) override attributes.
347
+ * @param {object} propDefs - { propName: { type, default } }
348
+ * @param {object} passedProps - props passed programmatically from mount()
349
+ * @returns {object} resolved props (frozen)
350
+ */
351
+ _resolveReactiveProps(propDefs, passedProps) {
352
+ const resolved = {};
353
+ for (const [name, schema] of Object.entries(propDefs)) {
354
+ const def = typeof schema === 'object' && schema !== null ? schema : { type: schema };
355
+ const type = def.type;
356
+ const defaultVal = def.default;
357
+
358
+ // Priority: passed props > dynamic :prop attribute > static attribute > default
359
+ if (name in passedProps) {
360
+ resolved[name] = passedProps[name];
361
+ continue;
362
+ }
363
+
364
+ // Check for dynamic :prop attribute (already evaluated by parent mount)
365
+ let rawAttr = this._el.getAttribute(':' + name);
366
+ let hasAttr = rawAttr !== null;
367
+ if (!hasAttr) {
368
+ rawAttr = this._el.getAttribute(name);
369
+ hasAttr = rawAttr !== null;
370
+ }
371
+
372
+ if (hasAttr && rawAttr !== null) {
373
+ resolved[name] = this._coercePropValue(rawAttr, type);
374
+ } else if (defaultVal !== undefined) {
375
+ resolved[name] = typeof defaultVal === 'function' ? defaultVal() : defaultVal;
376
+ } else {
377
+ resolved[name] = undefined;
378
+ }
379
+ }
380
+ return Object.freeze(resolved);
381
+ }
382
+
383
+ /**
384
+ * Coerce a raw attribute string to the specified type.
385
+ * @param {string} raw - attribute value string
386
+ * @param {Function} type - String, Number, Boolean, Object, or Array
387
+ * @returns {*}
388
+ */
389
+ _coercePropValue(raw, type) {
390
+ if (type === Number) return Number(raw);
391
+ if (type === Boolean) return raw !== 'false' && raw !== '0' && raw !== '';
392
+ if (type === Object || type === Array) {
393
+ try { return JSON.parse(raw); } catch { return raw; }
394
+ }
395
+ return raw; // String or unspecified
396
+ }
397
+
293
398
  // Load external templateUrl / styleUrl if specified (once per definition)
294
399
  //
295
400
  // Relative paths are resolved automatically against the component file's
@@ -1052,8 +1157,20 @@ class Component {
1052
1157
  item.el.removeAttribute('z-if');
1053
1158
  item.el.removeAttribute('z-else-if');
1054
1159
  item.el.removeAttribute('z-else');
1160
+ // Transition enter for z-if elements becoming visible
1161
+ const transName = item.el.getAttribute('z-transition');
1162
+ if (transName) {
1163
+ item.el.removeAttribute('z-transition');
1164
+ this._transitionEnter(item.el, transName);
1165
+ }
1055
1166
  } else {
1056
- item.el.remove();
1167
+ // Transition leave for z-if elements being removed
1168
+ const transName = item.el.getAttribute('z-transition');
1169
+ if (transName) {
1170
+ this._transitionLeave(item.el, transName, () => item.el.remove());
1171
+ } else {
1172
+ item.el.remove();
1173
+ }
1057
1174
  }
1058
1175
  }
1059
1176
  }
@@ -1062,8 +1179,31 @@ class Component {
1062
1179
  this._el.querySelectorAll('[z-show]').forEach(el => {
1063
1180
  if (el.closest('[z-pre]')) return;
1064
1181
  const show = !!this._evalExpr(el.getAttribute('z-show'));
1065
- el.style.display = show ? '' : 'none';
1066
- el.removeAttribute('z-show');
1182
+ const transName = el.getAttribute('z-transition');
1183
+ const wasHidden = el.style.display === 'none' || el.hasAttribute('data-zq-hidden');
1184
+
1185
+ if (transName) {
1186
+ el.removeAttribute('z-show');
1187
+ if (show && wasHidden) {
1188
+ // Entering: was hidden, now showing
1189
+ el.style.display = '';
1190
+ el.removeAttribute('data-zq-hidden');
1191
+ this._transitionEnter(el, transName);
1192
+ } else if (!show && !wasHidden) {
1193
+ // Leaving: was visible, now hiding
1194
+ el.setAttribute('data-zq-hidden', '');
1195
+ this._transitionLeave(el, transName, () => {
1196
+ el.style.display = 'none';
1197
+ });
1198
+ } else {
1199
+ el.style.display = show ? '' : 'none';
1200
+ if (!show) el.setAttribute('data-zq-hidden', '');
1201
+ else el.removeAttribute('data-zq-hidden');
1202
+ }
1203
+ } else {
1204
+ el.style.display = show ? '' : 'none';
1205
+ el.removeAttribute('z-show');
1206
+ }
1067
1207
  });
1068
1208
 
1069
1209
  // -- z-bind:attr / :attr (dynamic attribute binding) -----------
@@ -1138,6 +1278,93 @@ class Component {
1138
1278
  });
1139
1279
  }
1140
1280
 
1281
+ // ---------------------------------------------------------------------------
1282
+ // Transition helpers - CSS class-based enter/leave animations
1283
+ //
1284
+ // z-transition="fade" generates:
1285
+ // Enter: .fade-enter-from → .fade-enter-active + .fade-enter-to
1286
+ // Leave: .fade-leave-from → .fade-leave-active + .fade-leave-to
1287
+ //
1288
+ // Or component-level transition config:
1289
+ // transition: { enter: 'animate-in', leave: 'animate-out', duration: 200 }
1290
+ // ---------------------------------------------------------------------------
1291
+
1292
+ /**
1293
+ * Run an enter transition on an element.
1294
+ * @param {Element} el - target element
1295
+ * @param {string} name - transition name (e.g. 'fade')
1296
+ */
1297
+ _transitionEnter(el, name) {
1298
+ // Check for component-level transition config
1299
+ const cfg = this._def.transition;
1300
+ if (cfg && cfg.enter) {
1301
+ el.classList.add(cfg.enter);
1302
+ const duration = cfg.duration || 0;
1303
+ const cleanup = () => el.classList.remove(cfg.enter);
1304
+ if (duration > 0) {
1305
+ setTimeout(cleanup, duration);
1306
+ } else {
1307
+ el.addEventListener('transitionend', cleanup, { once: true });
1308
+ el.addEventListener('animationend', cleanup, { once: true });
1309
+ }
1310
+ return;
1311
+ }
1312
+
1313
+ // CSS class-based transition pattern
1314
+ el.classList.add(`${name}-enter-from`, `${name}-enter-active`);
1315
+ // Force reflow so the browser registers the initial state
1316
+ void el.offsetHeight;
1317
+ requestAnimationFrame(() => {
1318
+ el.classList.remove(`${name}-enter-from`);
1319
+ el.classList.add(`${name}-enter-to`);
1320
+ const onEnd = () => {
1321
+ el.classList.remove(`${name}-enter-active`, `${name}-enter-to`);
1322
+ };
1323
+ el.addEventListener('transitionend', onEnd, { once: true });
1324
+ el.addEventListener('animationend', onEnd, { once: true });
1325
+ });
1326
+ }
1327
+
1328
+ /**
1329
+ * Run a leave transition on an element, then call done().
1330
+ * @param {Element} el - target element
1331
+ * @param {string} name - transition name (e.g. 'fade')
1332
+ * @param {Function} done - callback when transition completes
1333
+ */
1334
+ _transitionLeave(el, name, done) {
1335
+ // Check for component-level transition config
1336
+ const cfg = this._def.transition;
1337
+ if (cfg && cfg.leave) {
1338
+ el.classList.add(cfg.leave);
1339
+ const duration = cfg.duration || 0;
1340
+ const cleanup = () => {
1341
+ el.classList.remove(cfg.leave);
1342
+ done();
1343
+ };
1344
+ if (duration > 0) {
1345
+ setTimeout(cleanup, duration);
1346
+ } else {
1347
+ el.addEventListener('transitionend', cleanup, { once: true });
1348
+ el.addEventListener('animationend', cleanup, { once: true });
1349
+ }
1350
+ return;
1351
+ }
1352
+
1353
+ // CSS class-based transition pattern
1354
+ el.classList.add(`${name}-leave-from`, `${name}-leave-active`);
1355
+ void el.offsetHeight;
1356
+ requestAnimationFrame(() => {
1357
+ el.classList.remove(`${name}-leave-from`);
1358
+ el.classList.add(`${name}-leave-to`);
1359
+ const onEnd = () => {
1360
+ el.classList.remove(`${name}-leave-active`, `${name}-leave-to`);
1361
+ done();
1362
+ };
1363
+ el.addEventListener('transitionend', onEnd, { once: true });
1364
+ el.addEventListener('animationend', onEnd, { once: true });
1365
+ });
1366
+ }
1367
+
1141
1368
  // Programmatic state update (batch-friendly)
1142
1369
  // Passing an empty object forces a re-render (useful for external state changes).
1143
1370
  setState(partial) {
@@ -1161,6 +1388,16 @@ class Component {
1161
1388
  try { this._def.destroyed.call(this); }
1162
1389
  catch (err) { reportError(ErrorCode.COMP_LIFECYCLE, `Component "${this._def._name}" destroyed() threw`, { component: this._def._name }, err); }
1163
1390
  }
1391
+ // Clean up prop observer
1392
+ if (this._propObserver) {
1393
+ this._propObserver.disconnect();
1394
+ this._propObserver = null;
1395
+ }
1396
+ // Clean up store connectors
1397
+ if (this._storeCleanups) {
1398
+ this._storeCleanups.forEach(fn => fn());
1399
+ this._storeCleanups = [];
1400
+ }
1164
1401
  this._listeners.forEach(({ event, handler }) => this._el.removeEventListener(event, handler));
1165
1402
  this._listeners = [];
1166
1403
  if (this._outsideListeners) {
@@ -1195,7 +1432,7 @@ class Component {
1195
1432
  const _reservedKeys = new Set([
1196
1433
  'state', 'render', 'styles', 'init', 'mounted', 'updated', 'destroyed', 'props',
1197
1434
  'templateUrl', 'styleUrl', 'templates', 'base',
1198
- 'computed', 'watch'
1435
+ 'computed', 'watch', 'stores', 'transition', 'activated', 'deactivated'
1199
1436
  ]);
1200
1437
 
1201
1438
 
package/src/reactive.js CHANGED
@@ -206,13 +206,13 @@ export function effect(fn) {
206
206
  export function batch(fn) {
207
207
  if (Signal._batching) {
208
208
  // Already inside a batch, just run
209
- fn();
210
- return;
209
+ return fn();
211
210
  }
212
211
  Signal._batching = true;
213
212
  Signal._batchQueue.clear();
213
+ let result;
214
214
  try {
215
- fn();
215
+ result = fn();
216
216
  } finally {
217
217
  Signal._batching = false;
218
218
  // Collect all unique subscribers across all queued signals
@@ -231,6 +231,7 @@ export function batch(fn) {
231
231
  }
232
232
  }
233
233
  }
234
+ return result;
234
235
  }
235
236
 
236
237