zero-query 1.0.5 → 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.
Binary file
package/dist/zquery.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * zQuery (zeroQuery) v1.0.5
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
@@ -426,13 +426,13 @@ function effect(fn) {
426
426
  function batch(fn) {
427
427
  if (Signal._batching) {
428
428
  // Already inside a batch, just run
429
- fn();
430
- return;
429
+ return fn();
431
430
  }
432
431
  Signal._batching = true;
433
432
  Signal._batchQueue.clear();
433
+ let result;
434
434
  try {
435
- fn();
435
+ result = fn();
436
436
  } finally {
437
437
  Signal._batching = false;
438
438
  // Collect all unique subscribers across all queued signals
@@ -451,6 +451,7 @@ function batch(fn) {
451
451
  }
452
452
  }
453
453
  }
454
+ return result;
454
455
  }
455
456
 
456
457
 
@@ -3030,8 +3031,58 @@ class Component {
3030
3031
  this._slotContent['default'] = defaultSlotNodes.join('');
3031
3032
  }
3032
3033
 
3033
- // Props (read-only from parent)
3034
- this.props = Object.freeze({ ...props });
3034
+ // Props - reactive when definition.props is defined, frozen otherwise
3035
+ if (definition.props && typeof definition.props === 'object' && !Array.isArray(definition.props)) {
3036
+ // Reactive props with type coercion and defaults
3037
+ this.props = this._resolveReactiveProps(definition.props, props);
3038
+ // MutationObserver to re-read props when parent re-renders and changes attributes
3039
+ this._propObserver = new MutationObserver((mutations) => {
3040
+ if (this._destroyed) return;
3041
+ let changed = false;
3042
+ for (const mut of mutations) {
3043
+ if (mut.type === 'attributes') {
3044
+ const attrName = mut.attributeName;
3045
+ // Skip internal attributes
3046
+ if (attrName.startsWith('z-') || attrName.startsWith('@') || attrName.startsWith(':') || attrName.startsWith('data-zq')) continue;
3047
+ // Check if this is a defined prop (attribute names are lowercase)
3048
+ const propName = attrName.startsWith(':') ? attrName.slice(1) : attrName;
3049
+ if (propName in definition.props) {
3050
+ changed = true;
3051
+ }
3052
+ }
3053
+ }
3054
+ if (changed) {
3055
+ this.props = this._resolveReactiveProps(definition.props, {});
3056
+ this._scheduleUpdate();
3057
+ }
3058
+ });
3059
+ this._propObserver.observe(el, { attributes: true });
3060
+ } else {
3061
+ // Legacy: frozen props from parent
3062
+ this.props = Object.freeze({ ...props });
3063
+ }
3064
+
3065
+ // Store connectors - auto-subscribe to store keys
3066
+ this._storeCleanups = [];
3067
+ this.stores = {};
3068
+ if (definition.stores && typeof definition.stores === 'object') {
3069
+ for (const [alias, connector] of Object.entries(definition.stores)) {
3070
+ if (!connector || !connector._zqConnector) continue;
3071
+ const { store, keys } = connector;
3072
+ // Initialize snapshot
3073
+ const snap = {};
3074
+ for (const key of keys) {
3075
+ snap[key] = store.state[key];
3076
+ }
3077
+ this.stores[alias] = snap;
3078
+ // Subscribe to changes
3079
+ const unsub = store.subscribe(keys, (key, value) => {
3080
+ this.stores[alias][key] = value;
3081
+ if (!this._destroyed) this._scheduleUpdate();
3082
+ });
3083
+ this._storeCleanups.push(unsub);
3084
+ }
3085
+ }
3035
3086
 
3036
3087
  // Reactive state
3037
3088
  const initialState = typeof definition.state === 'function'
@@ -3110,6 +3161,61 @@ class Component {
3110
3161
  });
3111
3162
  }
3112
3163
 
3164
+ /**
3165
+ * Resolve reactive props from the definition's prop schema.
3166
+ * Reads from element attributes, applies type coercion and defaults.
3167
+ * Passed props (from mount) override attributes.
3168
+ * @param {object} propDefs - { propName: { type, default } }
3169
+ * @param {object} passedProps - props passed programmatically from mount()
3170
+ * @returns {object} resolved props (frozen)
3171
+ */
3172
+ _resolveReactiveProps(propDefs, passedProps) {
3173
+ const resolved = {};
3174
+ for (const [name, schema] of Object.entries(propDefs)) {
3175
+ const def = typeof schema === 'object' && schema !== null ? schema : { type: schema };
3176
+ const type = def.type;
3177
+ const defaultVal = def.default;
3178
+
3179
+ // Priority: passed props > dynamic :prop attribute > static attribute > default
3180
+ if (name in passedProps) {
3181
+ resolved[name] = passedProps[name];
3182
+ continue;
3183
+ }
3184
+
3185
+ // Check for dynamic :prop attribute (already evaluated by parent mount)
3186
+ let rawAttr = this._el.getAttribute(':' + name);
3187
+ let hasAttr = rawAttr !== null;
3188
+ if (!hasAttr) {
3189
+ rawAttr = this._el.getAttribute(name);
3190
+ hasAttr = rawAttr !== null;
3191
+ }
3192
+
3193
+ if (hasAttr && rawAttr !== null) {
3194
+ resolved[name] = this._coercePropValue(rawAttr, type);
3195
+ } else if (defaultVal !== undefined) {
3196
+ resolved[name] = typeof defaultVal === 'function' ? defaultVal() : defaultVal;
3197
+ } else {
3198
+ resolved[name] = undefined;
3199
+ }
3200
+ }
3201
+ return Object.freeze(resolved);
3202
+ }
3203
+
3204
+ /**
3205
+ * Coerce a raw attribute string to the specified type.
3206
+ * @param {string} raw - attribute value string
3207
+ * @param {Function} type - String, Number, Boolean, Object, or Array
3208
+ * @returns {*}
3209
+ */
3210
+ _coercePropValue(raw, type) {
3211
+ if (type === Number) return Number(raw);
3212
+ if (type === Boolean) return raw !== 'false' && raw !== '0' && raw !== '';
3213
+ if (type === Object || type === Array) {
3214
+ try { return JSON.parse(raw); } catch { return raw; }
3215
+ }
3216
+ return raw; // String or unspecified
3217
+ }
3218
+
3113
3219
  // Load external templateUrl / styleUrl if specified (once per definition)
3114
3220
  //
3115
3221
  // Relative paths are resolved automatically against the component file's
@@ -3872,8 +3978,20 @@ class Component {
3872
3978
  item.el.removeAttribute('z-if');
3873
3979
  item.el.removeAttribute('z-else-if');
3874
3980
  item.el.removeAttribute('z-else');
3981
+ // Transition enter for z-if elements becoming visible
3982
+ const transName = item.el.getAttribute('z-transition');
3983
+ if (transName) {
3984
+ item.el.removeAttribute('z-transition');
3985
+ this._transitionEnter(item.el, transName);
3986
+ }
3875
3987
  } else {
3876
- item.el.remove();
3988
+ // Transition leave for z-if elements being removed
3989
+ const transName = item.el.getAttribute('z-transition');
3990
+ if (transName) {
3991
+ this._transitionLeave(item.el, transName, () => item.el.remove());
3992
+ } else {
3993
+ item.el.remove();
3994
+ }
3877
3995
  }
3878
3996
  }
3879
3997
  }
@@ -3882,8 +4000,31 @@ class Component {
3882
4000
  this._el.querySelectorAll('[z-show]').forEach(el => {
3883
4001
  if (el.closest('[z-pre]')) return;
3884
4002
  const show = !!this._evalExpr(el.getAttribute('z-show'));
3885
- el.style.display = show ? '' : 'none';
3886
- el.removeAttribute('z-show');
4003
+ const transName = el.getAttribute('z-transition');
4004
+ const wasHidden = el.style.display === 'none' || el.hasAttribute('data-zq-hidden');
4005
+
4006
+ if (transName) {
4007
+ el.removeAttribute('z-show');
4008
+ if (show && wasHidden) {
4009
+ // Entering: was hidden, now showing
4010
+ el.style.display = '';
4011
+ el.removeAttribute('data-zq-hidden');
4012
+ this._transitionEnter(el, transName);
4013
+ } else if (!show && !wasHidden) {
4014
+ // Leaving: was visible, now hiding
4015
+ el.setAttribute('data-zq-hidden', '');
4016
+ this._transitionLeave(el, transName, () => {
4017
+ el.style.display = 'none';
4018
+ });
4019
+ } else {
4020
+ el.style.display = show ? '' : 'none';
4021
+ if (!show) el.setAttribute('data-zq-hidden', '');
4022
+ else el.removeAttribute('data-zq-hidden');
4023
+ }
4024
+ } else {
4025
+ el.style.display = show ? '' : 'none';
4026
+ el.removeAttribute('z-show');
4027
+ }
3887
4028
  });
3888
4029
 
3889
4030
  // -- z-bind:attr / :attr (dynamic attribute binding) -----------
@@ -3958,6 +4099,93 @@ class Component {
3958
4099
  });
3959
4100
  }
3960
4101
 
4102
+ // ---------------------------------------------------------------------------
4103
+ // Transition helpers - CSS class-based enter/leave animations
4104
+ //
4105
+ // z-transition="fade" generates:
4106
+ // Enter: .fade-enter-from → .fade-enter-active + .fade-enter-to
4107
+ // Leave: .fade-leave-from → .fade-leave-active + .fade-leave-to
4108
+ //
4109
+ // Or component-level transition config:
4110
+ // transition: { enter: 'animate-in', leave: 'animate-out', duration: 200 }
4111
+ // ---------------------------------------------------------------------------
4112
+
4113
+ /**
4114
+ * Run an enter transition on an element.
4115
+ * @param {Element} el - target element
4116
+ * @param {string} name - transition name (e.g. 'fade')
4117
+ */
4118
+ _transitionEnter(el, name) {
4119
+ // Check for component-level transition config
4120
+ const cfg = this._def.transition;
4121
+ if (cfg && cfg.enter) {
4122
+ el.classList.add(cfg.enter);
4123
+ const duration = cfg.duration || 0;
4124
+ const cleanup = () => el.classList.remove(cfg.enter);
4125
+ if (duration > 0) {
4126
+ setTimeout(cleanup, duration);
4127
+ } else {
4128
+ el.addEventListener('transitionend', cleanup, { once: true });
4129
+ el.addEventListener('animationend', cleanup, { once: true });
4130
+ }
4131
+ return;
4132
+ }
4133
+
4134
+ // CSS class-based transition pattern
4135
+ el.classList.add(`${name}-enter-from`, `${name}-enter-active`);
4136
+ // Force reflow so the browser registers the initial state
4137
+ void el.offsetHeight;
4138
+ requestAnimationFrame(() => {
4139
+ el.classList.remove(`${name}-enter-from`);
4140
+ el.classList.add(`${name}-enter-to`);
4141
+ const onEnd = () => {
4142
+ el.classList.remove(`${name}-enter-active`, `${name}-enter-to`);
4143
+ };
4144
+ el.addEventListener('transitionend', onEnd, { once: true });
4145
+ el.addEventListener('animationend', onEnd, { once: true });
4146
+ });
4147
+ }
4148
+
4149
+ /**
4150
+ * Run a leave transition on an element, then call done().
4151
+ * @param {Element} el - target element
4152
+ * @param {string} name - transition name (e.g. 'fade')
4153
+ * @param {Function} done - callback when transition completes
4154
+ */
4155
+ _transitionLeave(el, name, done) {
4156
+ // Check for component-level transition config
4157
+ const cfg = this._def.transition;
4158
+ if (cfg && cfg.leave) {
4159
+ el.classList.add(cfg.leave);
4160
+ const duration = cfg.duration || 0;
4161
+ const cleanup = () => {
4162
+ el.classList.remove(cfg.leave);
4163
+ done();
4164
+ };
4165
+ if (duration > 0) {
4166
+ setTimeout(cleanup, duration);
4167
+ } else {
4168
+ el.addEventListener('transitionend', cleanup, { once: true });
4169
+ el.addEventListener('animationend', cleanup, { once: true });
4170
+ }
4171
+ return;
4172
+ }
4173
+
4174
+ // CSS class-based transition pattern
4175
+ el.classList.add(`${name}-leave-from`, `${name}-leave-active`);
4176
+ void el.offsetHeight;
4177
+ requestAnimationFrame(() => {
4178
+ el.classList.remove(`${name}-leave-from`);
4179
+ el.classList.add(`${name}-leave-to`);
4180
+ const onEnd = () => {
4181
+ el.classList.remove(`${name}-leave-active`, `${name}-leave-to`);
4182
+ done();
4183
+ };
4184
+ el.addEventListener('transitionend', onEnd, { once: true });
4185
+ el.addEventListener('animationend', onEnd, { once: true });
4186
+ });
4187
+ }
4188
+
3961
4189
  // Programmatic state update (batch-friendly)
3962
4190
  // Passing an empty object forces a re-render (useful for external state changes).
3963
4191
  setState(partial) {
@@ -3981,6 +4209,16 @@ class Component {
3981
4209
  try { this._def.destroyed.call(this); }
3982
4210
  catch (err) { reportError(ErrorCode.COMP_LIFECYCLE, `Component "${this._def._name}" destroyed() threw`, { component: this._def._name }, err); }
3983
4211
  }
4212
+ // Clean up prop observer
4213
+ if (this._propObserver) {
4214
+ this._propObserver.disconnect();
4215
+ this._propObserver = null;
4216
+ }
4217
+ // Clean up store connectors
4218
+ if (this._storeCleanups) {
4219
+ this._storeCleanups.forEach(fn => fn());
4220
+ this._storeCleanups = [];
4221
+ }
3984
4222
  this._listeners.forEach(({ event, handler }) => this._el.removeEventListener(event, handler));
3985
4223
  this._listeners = [];
3986
4224
  if (this._outsideListeners) {
@@ -4015,7 +4253,7 @@ class Component {
4015
4253
  const _reservedKeys = new Set([
4016
4254
  'state', 'render', 'styles', 'init', 'mounted', 'updated', 'destroyed', 'props',
4017
4255
  'templateUrl', 'styleUrl', 'templates', 'base',
4018
- 'computed', 'watch'
4256
+ 'computed', 'watch', 'stores', 'transition', 'activated', 'deactivated'
4019
4257
  ]);
4020
4258
 
4021
4259
 
@@ -4322,6 +4560,9 @@ class Router {
4322
4560
  const isFile = typeof location !== 'undefined' && location.protocol === 'file:';
4323
4561
  this._mode = isFile ? 'hash' : (config.mode || 'history');
4324
4562
 
4563
+ // Keep-alive cache: component name → { container, instance }
4564
+ this._keepAliveCache = new Map();
4565
+
4325
4566
  // Base path for sub-path deployments
4326
4567
  // Priority: explicit config.base → window.__ZQ_BASE → <base href> tag
4327
4568
  let rawBase = config.base;
@@ -4879,34 +5120,96 @@ class Router {
4879
5120
  await prefetch(matched.component);
4880
5121
  }
4881
5122
 
4882
- // Destroy previous
4883
- if (this._instance) {
5123
+ const isKeepAlive = !!matched.keepAlive;
5124
+ const componentName = typeof matched.component === 'string' ? matched.component : null;
5125
+
5126
+ // Deactivate previous keep-alive instance (hide instead of destroy)
5127
+ if (this._instance && this._currentKeepAlive && this._currentComponentName) {
5128
+ const cached = this._keepAliveCache.get(this._currentComponentName);
5129
+ if (cached) {
5130
+ cached.container.style.display = 'none';
5131
+ // Call deactivated() lifecycle hook
5132
+ if (cached.instance._def.deactivated) {
5133
+ try { cached.instance._def.deactivated.call(cached.instance); }
5134
+ catch (err) { reportError(ErrorCode.COMP_LIFECYCLE, `Component "${this._currentComponentName}" deactivated() threw`, { component: this._currentComponentName }, err); }
5135
+ }
5136
+ }
5137
+ this._instance = null;
5138
+ } else if (this._instance) {
5139
+ // Destroy previous non-keepAlive instance
4884
5140
  this._instance.destroy();
4885
5141
  this._instance = null;
4886
5142
  }
4887
5143
 
4888
- // Create container
4889
5144
  const _routeStart = typeof window !== 'undefined' && window.__zqRenderHook ? performance.now() : 0;
4890
- this._el.innerHTML = '';
4891
5145
 
4892
5146
  // Pass route params and query as props
4893
5147
  const props = { ...params, $route: to, $query: query, $params: params };
4894
5148
 
5149
+ // Keep-alive: reuse cached instance
5150
+ if (isKeepAlive && componentName && this._keepAliveCache.has(componentName)) {
5151
+ const cached = this._keepAliveCache.get(componentName);
5152
+ // Hide all children, show the cached one
5153
+ [...this._el.children].forEach(c => { c.style.display = 'none'; });
5154
+ cached.container.style.display = '';
5155
+ this._instance = cached.instance;
5156
+ this._currentKeepAlive = true;
5157
+ this._currentComponentName = componentName;
5158
+ // Call activated() lifecycle hook
5159
+ if (cached.instance._def.activated) {
5160
+ try { cached.instance._def.activated.call(cached.instance); }
5161
+ catch (err) { reportError(ErrorCode.COMP_LIFECYCLE, `Component "${componentName}" activated() threw`, { component: componentName }, err); }
5162
+ }
5163
+ if (_routeStart) window.__zqRenderHook(this._el, performance.now() - _routeStart, 'route', componentName);
5164
+ }
4895
5165
  // If component is a string (registered name), mount it
4896
- if (typeof matched.component === 'string') {
4897
- const container = document.createElement(matched.component);
5166
+ else if (componentName) {
5167
+ // Hide all keep-alive cached children (don't destroy)
5168
+ [...this._el.children].forEach(c => {
5169
+ if (c.dataset.zqKeepAlive) {
5170
+ c.style.display = 'none';
5171
+ }
5172
+ });
5173
+ // Remove non-keep-alive children
5174
+ [...this._el.children].forEach(c => {
5175
+ if (!c.dataset.zqKeepAlive) c.remove();
5176
+ });
5177
+
5178
+ const container = document.createElement(componentName);
5179
+ if (isKeepAlive) container.dataset.zqKeepAlive = componentName;
4898
5180
  this._el.appendChild(container);
4899
5181
  try {
4900
- this._instance = mount(container, matched.component, props);
5182
+ this._instance = mount(container, componentName, props);
4901
5183
  } catch (err) {
4902
5184
  reportError(ErrorCode.COMP_NOT_FOUND, `Failed to mount component for route "${matched.path}"`, { component: matched.component, path: matched.path }, err);
4903
5185
  return;
4904
5186
  }
4905
- if (_routeStart) window.__zqRenderHook(this._el, performance.now() - _routeStart, 'route', matched.component);
5187
+
5188
+ if (isKeepAlive) {
5189
+ this._keepAliveCache.set(componentName, { container, instance: this._instance });
5190
+ // Call activated() on first mount
5191
+ if (this._instance._def.activated) {
5192
+ try { this._instance._def.activated.call(this._instance); }
5193
+ catch (err) { reportError(ErrorCode.COMP_LIFECYCLE, `Component "${componentName}" activated() threw`, { component: componentName }, err); }
5194
+ }
5195
+ }
5196
+
5197
+ this._currentKeepAlive = isKeepAlive;
5198
+ this._currentComponentName = componentName;
5199
+ if (_routeStart) window.__zqRenderHook(this._el, performance.now() - _routeStart, 'route', componentName);
4906
5200
  }
4907
5201
  // If component is a render function
4908
5202
  else if (typeof matched.component === 'function') {
4909
- this._el.innerHTML = matched.component(to);
5203
+ // Clear non-keepAlive content
5204
+ [...this._el.children].forEach(c => {
5205
+ if (c.dataset.zqKeepAlive) c.style.display = 'none';
5206
+ else c.remove();
5207
+ });
5208
+ const wrapper = document.createElement('div');
5209
+ wrapper.innerHTML = matched.component(to);
5210
+ while (wrapper.firstChild) this._el.appendChild(wrapper.firstChild);
5211
+ this._currentKeepAlive = false;
5212
+ this._currentComponentName = null;
4910
5213
  if (_routeStart) window.__zqRenderHook(this._el, performance.now() - _routeStart, 'route', to);
4911
5214
  }
4912
5215
  }
@@ -4965,6 +5268,11 @@ class Router {
4965
5268
  document.removeEventListener('click', this._onLinkClick);
4966
5269
  this._onLinkClick = null;
4967
5270
  }
5271
+ // Destroy all keep-alive cached instances
5272
+ for (const [, cached] of this._keepAliveCache) {
5273
+ cached.instance.destroy();
5274
+ }
5275
+ this._keepAliveCache.clear();
4968
5276
  if (this._instance) this._instance.destroy();
4969
5277
  this._listeners.clear();
4970
5278
  this._substateListeners = [];
@@ -5135,8 +5443,9 @@ class Store {
5135
5443
  batch(fn) {
5136
5444
  this._batching = true;
5137
5445
  this._batchQueue = [];
5446
+ let result;
5138
5447
  try {
5139
- fn(this.state);
5448
+ result = fn(this.state);
5140
5449
  } finally {
5141
5450
  this._batching = false;
5142
5451
  // Deduplicate: keep only the last change per key
@@ -5149,6 +5458,7 @@ class Store {
5149
5458
  this._notifySubscribers(key, value, old);
5150
5459
  }
5151
5460
  }
5461
+ return result;
5152
5462
  }
5153
5463
 
5154
5464
  /**
@@ -5236,8 +5546,14 @@ class Store {
5236
5546
  }
5237
5547
 
5238
5548
  /**
5239
- * Subscribe to changes on a specific state key
5240
- * @param {string|Function} keyOrFn - state key, or function for all changes
5549
+ * Subscribe to changes on a specific state key, multiple keys, or all changes.
5550
+ *
5551
+ * Signatures:
5552
+ * subscribe(callback) → wildcard, fires on every change
5553
+ * subscribe('key', callback) → fires when 'key' changes
5554
+ * subscribe(['a','b'], callback) → fires when any listed key changes
5555
+ *
5556
+ * @param {string|string[]|Function} keyOrFn - state key, array of keys, or function for all changes
5241
5557
  * @param {Function} [fn] - callback (key, value, oldValue)
5242
5558
  * @returns {Function} - unsubscribe
5243
5559
  */
@@ -5248,6 +5564,16 @@ class Store {
5248
5564
  return () => this._wildcards.delete(keyOrFn);
5249
5565
  }
5250
5566
 
5567
+ // Multi-key subscription: subscribe(['files', 'isProcessing'], callback)
5568
+ if (Array.isArray(keyOrFn)) {
5569
+ const keys = keyOrFn;
5570
+ const handler = (key, value, old) => {
5571
+ if (keys.includes(key)) fn(key, value, old);
5572
+ };
5573
+ this._wildcards.add(handler);
5574
+ return () => this._wildcards.delete(handler);
5575
+ }
5576
+
5251
5577
  if (!this._subscribers.has(keyOrFn)) {
5252
5578
  this._subscribers.set(keyOrFn, new Set());
5253
5579
  }
@@ -5318,6 +5644,34 @@ function createStore(name, config) {
5318
5644
 
5319
5645
  function getStore(name = 'default') {
5320
5646
  return _stores.get(name) || null;
5647
+ }
5648
+
5649
+
5650
+ // ---------------------------------------------------------------------------
5651
+ // Store-Component Connector
5652
+ // ---------------------------------------------------------------------------
5653
+
5654
+ /**
5655
+ * Create a store connector descriptor for use in component definitions.
5656
+ * When used in a component's `stores` config, auto-subscribes to the
5657
+ * listed keys on mount and cleans up on destroy.
5658
+ *
5659
+ * Usage:
5660
+ * $.component('my-comp', {
5661
+ * stores: {
5662
+ * app: connectStore(appStore, ['files', 'isProcessing']),
5663
+ * },
5664
+ * render() {
5665
+ * return `<div>${this.stores.app.files.length} files</div>`;
5666
+ * }
5667
+ * });
5668
+ *
5669
+ * @param {Store} store - the store instance to connect
5670
+ * @param {string[]} keys - state keys to sync
5671
+ * @returns {{ _zqConnector: true, store: Store, keys: string[] }}
5672
+ */
5673
+ function connectStore(store, keys) {
5674
+ return { _zqConnector: true, store, keys };
5321
5675
  }
5322
5676
 
5323
5677
  // --- src/http.js -------------------------------------------------
@@ -6195,6 +6549,7 @@ $.matchRoute = matchRoute;
6195
6549
  // --- Store -----------------------------------------------------------------
6196
6550
  $.store = createStore;
6197
6551
  $.getStore = getStore;
6552
+ $.connectStore = connectStore;
6198
6553
 
6199
6554
  // --- HTTP ------------------------------------------------------------------
6200
6555
  $.http = http;
@@ -6254,11 +6609,18 @@ $.validate = validate;
6254
6609
  $.formatError = formatError;
6255
6610
 
6256
6611
  // --- Meta ------------------------------------------------------------------
6257
- $.version = '1.0.5';
6258
- $.libSize = '~107 KB';
6259
- $.unitTests = {"passed":1931,"failed":0,"total":1931,"suites":523,"duration":3815,"ok":true};
6612
+ $.version = '1.1.1';
6613
+ $.libSize = '~115 KB';
6614
+ $.unitTests = {"passed":2281,"failed":0,"total":2281,"suites":565,"duration":6929,"ok":true};
6260
6615
  $.meta = {}; // populated at build time by CLI bundler
6261
6616
 
6617
+ // --- Environment detection -------------------------------------------------
6618
+ $.isElectron = typeof navigator !== 'undefined' && /Electron/i.test(navigator.userAgent)
6619
+ || typeof process !== 'undefined' && process.versions != null && !!process.versions.electron;
6620
+ $.platform = $.isElectron ? 'electron'
6621
+ : typeof window !== 'undefined' ? 'browser'
6622
+ : 'node';
6623
+
6262
6624
  $.noConflict = () => {
6263
6625
  if (typeof window !== 'undefined' && window.$ === $) {
6264
6626
  delete window.$;