bitwrench 2.0.25 → 2.0.30

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.
Files changed (75) hide show
  1. package/README.md +10 -4
  2. package/dist/bitwrench-bccl.cjs.js +1 -1
  3. package/dist/bitwrench-bccl.cjs.min.js +1 -1
  4. package/dist/bitwrench-bccl.cjs.min.js.gz +0 -0
  5. package/dist/bitwrench-bccl.esm.js +1 -1
  6. package/dist/bitwrench-bccl.esm.min.js +1 -1
  7. package/dist/bitwrench-bccl.esm.min.js.gz +0 -0
  8. package/dist/bitwrench-bccl.umd.js +1 -1
  9. package/dist/bitwrench-bccl.umd.min.js +1 -1
  10. package/dist/bitwrench-bccl.umd.min.js.gz +0 -0
  11. package/dist/bitwrench-code-edit.cjs.js +1 -1
  12. package/dist/bitwrench-code-edit.cjs.min.js +1 -1
  13. package/dist/bitwrench-code-edit.es5.js +1 -1
  14. package/dist/bitwrench-code-edit.es5.min.js +1 -1
  15. package/dist/bitwrench-code-edit.esm.js +1 -1
  16. package/dist/bitwrench-code-edit.esm.min.js +1 -1
  17. package/dist/bitwrench-code-edit.umd.js +1 -1
  18. package/dist/bitwrench-code-edit.umd.min.js +1 -1
  19. package/dist/bitwrench-code-edit.umd.min.js.gz +0 -0
  20. package/dist/bitwrench-debug.js +1 -1
  21. package/dist/bitwrench-debug.min.js +1 -1
  22. package/dist/bitwrench-lean.cjs.js +623 -155
  23. package/dist/bitwrench-lean.cjs.min.js +7 -7
  24. package/dist/bitwrench-lean.cjs.min.js.gz +0 -0
  25. package/dist/bitwrench-lean.es5.js +650 -157
  26. package/dist/bitwrench-lean.es5.min.js +5 -5
  27. package/dist/bitwrench-lean.es5.min.js.gz +0 -0
  28. package/dist/bitwrench-lean.esm.js +623 -155
  29. package/dist/bitwrench-lean.esm.min.js +6 -6
  30. package/dist/bitwrench-lean.esm.min.js.gz +0 -0
  31. package/dist/bitwrench-lean.umd.js +623 -155
  32. package/dist/bitwrench-lean.umd.min.js +7 -7
  33. package/dist/bitwrench-lean.umd.min.js.gz +0 -0
  34. package/dist/bitwrench-util-css.cjs.js +1 -1
  35. package/dist/bitwrench-util-css.cjs.min.js +1 -1
  36. package/dist/bitwrench-util-css.es5.js +1 -1
  37. package/dist/bitwrench-util-css.es5.min.js +1 -1
  38. package/dist/bitwrench-util-css.esm.js +1 -1
  39. package/dist/bitwrench-util-css.esm.min.js +1 -1
  40. package/dist/bitwrench-util-css.umd.js +1 -1
  41. package/dist/bitwrench-util-css.umd.min.js +1 -1
  42. package/dist/bitwrench-util-css.umd.min.js.gz +0 -0
  43. package/dist/bitwrench.cjs.js +621 -153
  44. package/dist/bitwrench.cjs.min.js +6 -6
  45. package/dist/bitwrench.cjs.min.js.gz +0 -0
  46. package/dist/bitwrench.css +1 -1
  47. package/dist/bitwrench.d.ts +18 -11
  48. package/dist/bitwrench.es5.js +647 -154
  49. package/dist/bitwrench.es5.min.js +6 -6
  50. package/dist/bitwrench.es5.min.js.gz +0 -0
  51. package/dist/bitwrench.esm.js +621 -153
  52. package/dist/bitwrench.esm.min.js +5 -5
  53. package/dist/bitwrench.esm.min.js.gz +0 -0
  54. package/dist/bitwrench.umd.js +621 -153
  55. package/dist/bitwrench.umd.min.js +6 -6
  56. package/dist/bitwrench.umd.min.js.gz +0 -0
  57. package/dist/builds.json +95 -95
  58. package/dist/bwserve.cjs.js +140 -7
  59. package/dist/bwserve.esm.js +141 -8
  60. package/dist/sri.json +45 -45
  61. package/docs/bitwrench-for-wasm.md +851 -0
  62. package/docs/bitwrench_api.md +133 -23
  63. package/docs/llm-bitwrench-guide.md +6 -5
  64. package/docs/state-management.md +27 -3
  65. package/docs/thinking-in-bitwrench.md +3 -2
  66. package/package.json +11 -9
  67. package/readme.html +17 -8
  68. package/src/bitwrench.d.ts +18 -11
  69. package/src/bitwrench.js +617 -148
  70. package/src/bwserve/bwclient.js +3 -3
  71. package/src/bwserve/client.js +26 -0
  72. package/src/bwserve/index.js +110 -3
  73. package/src/cli/attach.js +7 -5
  74. package/src/cli/serve.js +53 -10
  75. package/src/version.js +3 -3
@@ -1,4 +1,4 @@
1
- /*! bitwrench-lean v2.0.25 | BSD-2-Clause | https://deftio.github.com/bitwrench/pages */
1
+ /*! bitwrench-lean v2.0.30 | BSD-2-Clause | https://deftio.github.com/bitwrench/pages */
2
2
  (function (global, factory) {
3
3
  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
4
4
  typeof define === 'function' && define.amd ? define(factory) :
@@ -190,14 +190,14 @@
190
190
  */
191
191
 
192
192
  var VERSION_INFO = {
193
- version: '2.0.25',
193
+ version: '2.0.30',
194
194
  name: 'bitwrench',
195
195
  description: 'A library for javascript UI functions.',
196
196
  license: 'BSD-2-Clause',
197
197
  homepage: 'https://deftio.github.com/bitwrench/pages',
198
198
  repository: 'git+https://github.com/deftio/bitwrench.git',
199
199
  author: 'manu a. chatterjee <deftio@deftio.com> (https://deftio.com/)',
200
- buildDate: '2026-03-31T03:03:30.752Z'
200
+ buildDate: '2026-04-12T07:51:29.111Z'
201
201
  };
202
202
 
203
203
  /**
@@ -5830,9 +5830,6 @@
5830
5830
  var _cw = function _cw() {
5831
5831
  console.warn.apply(console, arguments);
5832
5832
  };
5833
- var _cl = function _cl() {
5834
- console.log.apply(console, arguments);
5835
- };
5836
5833
  var _ce = function _ce() {
5837
5834
  console.error.apply(console, arguments);
5838
5835
  };
@@ -5973,60 +5970,104 @@
5973
5970
  };
5974
5971
 
5975
5972
  /**
5976
- * Look up a DOM element by ID string, using the node cache for O(1) access.
5977
- *
5978
- * Resolution order:
5979
- * 1. Check `bw._nodeMap[id]` if found and still attached (parentNode !== null), return it
5980
- * 2. If cached ref is detached (parentNode === null), remove stale entry
5981
- * 3. Fall back to `document.getElementById(id)` then `document.querySelector(...)`
5982
- * 4. Try class-based lookup for bw_uuid_* tokens (UUID addressing)
5983
- * 5. Cache the result for next time
5984
- *
5985
- * Accepts a DOM element directly (pass-through) or a string identifier.
5986
- * String identifiers are tried as: direct map key, getElementById,
5987
- * querySelector (for CSS selectors starting with . or #), and
5988
- * bw_uuid_* class selector.
5989
- *
5990
- * @param {string|Element} id - Element ID, CSS selector, bw_uuid_* class, or DOM element
5973
+ * Look up a single DOM element by ID, CSS selector, UUID, or element ref.
5974
+ * Optionally apply content or a function to the resolved element.
5975
+ *
5976
+ * Resolution order for string targets:
5977
+ * 1. Check `bw._nodeMap[id]` cache (O(1), stale entries auto-pruned)
5978
+ * 2. `document.getElementById(id)`
5979
+ * 3. `document.querySelector(id)` for selectors starting with # or .
5980
+ * 4. Class-based lookup for `bw_uuid_*` tokens
5981
+ *
5982
+ * With one argument, returns the element (or null). With two arguments,
5983
+ * applies the second argument to the element and returns the element:
5984
+ * - string/number: sets `el.textContent`
5985
+ * - function: calls `apply(el)`, returns el
5986
+ * - TACO object: clears children, mounts TACO via `bw.createDOM()`
5987
+ * - array: clears children, appends each item (string -> text node, TACO -> element)
5988
+ *
5989
+ * @param {string|Element} target - Element ref, ID, CSS selector, or bw_uuid_* class
5990
+ * @param {string|number|Function|Object|Array} [apply] - Content or function to apply
5991
5991
  * @returns {Element|null} The DOM element, or null if not found
5992
- * @category Internal
5992
+ * @category DOM Selection
5993
+ * @see bw.$
5994
+ * @see bw.patch
5995
+ * @example
5996
+ * bw.el('#title') // lookup
5997
+ * bw.el('#title', 'Hello') // set text content
5998
+ * bw.el('#app', { t: 'h1', c: 'Hi' }) // mount TACO
5999
+ * bw.el('.card', function(el) { // apply function
6000
+ * el.style.opacity = '0.5';
6001
+ * })
5993
6002
  */
5994
- bw._el = function (id) {
5995
- // Pass-through for DOM elements
5996
- if (!_is(id, 'string')) return id || null;
5997
- if (!id) return null;
5998
- if (!bw._isBrowser) return null;
5999
-
6000
- // 1. Check cache
6001
- var cached = bw._nodeMap[id];
6002
- if (cached) {
6003
- // Verify not detached (parentNode check is IE11-safe)
6004
- if (cached.parentNode !== null) {
6005
- return cached;
6003
+ bw.el = function (target, apply) {
6004
+ // Resolve target to element
6005
+ var el;
6006
+ if (!_is(target, 'string')) {
6007
+ el = target || null;
6008
+ } else if (!target || !bw._isBrowser) {
6009
+ el = null;
6010
+ } else {
6011
+ // 1. Check cache
6012
+ var cached = bw._nodeMap[target];
6013
+ if (cached) {
6014
+ if (cached.parentNode !== null) {
6015
+ el = cached;
6016
+ } else {
6017
+ delete bw._nodeMap[target];
6018
+ }
6019
+ }
6020
+ if (!el) {
6021
+ // 2. getElementById
6022
+ el = document.getElementById(target);
6023
+ // 3. querySelector for CSS selectors
6024
+ if (!el && (target.charAt(0) === '#' || target.charAt(0) === '.')) {
6025
+ el = document.querySelector(target);
6026
+ }
6027
+ // 4. bw_uuid_* class lookup
6028
+ if (!el && target.indexOf('bw_uuid_') === 0) {
6029
+ el = document.querySelector('.' + target);
6030
+ }
6031
+ // 5. Cache result
6032
+ if (el) bw._nodeMap[target] = el;
6006
6033
  }
6007
- // Stale — remove and fall through
6008
- delete bw._nodeMap[id];
6009
6034
  }
6010
6035
 
6011
- // 2. DOM fallback: try getElementById first (fastest native lookup)
6012
- var el = document.getElementById(id);
6013
-
6014
- // 3. Try querySelector for CSS selectors (starts with # or .)
6015
- if (!el && (id.charAt(0) === '#' || id.charAt(0) === '.')) {
6016
- el = document.querySelector(id);
6017
- }
6036
+ // Apply (if provided and element found)
6037
+ if (el && apply !== undefined) _applyTo(el, apply);
6038
+ return el;
6039
+ };
6018
6040
 
6019
- // 4. Try class-based lookup for bw_uuid_* tokens (UUID addressing)
6020
- if (!el && id.indexOf('bw_uuid_') === 0) {
6021
- el = document.querySelector('.' + id);
6041
+ /**
6042
+ * Internal: apply content or function to a DOM element.
6043
+ * Shared by bw.el() and bw.$().
6044
+ * @private
6045
+ */
6046
+ function _applyTo(el, apply) {
6047
+ if (_is(apply, 'function')) {
6048
+ apply(el);
6049
+ } else if (_isA(apply)) {
6050
+ el.innerHTML = '';
6051
+ apply.forEach(function (item) {
6052
+ if (item != null) {
6053
+ if (_is(item, 'object') && item.t) {
6054
+ el.appendChild(bw.createDOM(item));
6055
+ } else {
6056
+ el.appendChild(document.createTextNode(String(item)));
6057
+ }
6058
+ }
6059
+ });
6060
+ } else if (_is(apply, 'object') && apply !== null && apply.t) {
6061
+ el.innerHTML = '';
6062
+ el.appendChild(bw.createDOM(apply));
6063
+ } else {
6064
+ el.textContent = String(apply);
6022
6065
  }
6066
+ }
6023
6067
 
6024
- // 5. Cache the result for next time
6025
- if (el) {
6026
- bw._nodeMap[id] = el;
6027
- }
6028
- return el;
6029
- };
6068
+ // Internal alias kept for one release cycle (v2.0.26).
6069
+ // Will be removed in v2.0.27. Use bw.el() instead.
6070
+ bw._el = bw.el;
6030
6071
 
6031
6072
  /**
6032
6073
  * Register a DOM element in the node cache under one or more keys.
@@ -6090,6 +6131,12 @@
6090
6131
  */
6091
6132
  var _UUID_RE = /\bbw_uuid_[a-z0-9_]+\b/;
6092
6133
 
6134
+ /**
6135
+ * SVG namespace URI for createElementNS.
6136
+ * @private
6137
+ */
6138
+ var _SVG_NS = 'http://www.w3.org/2000/svg';
6139
+
6093
6140
  /**
6094
6141
  * Assign a UUID to a TACO object by appending a `bw_uuid_*` token to `taco.a.class`.
6095
6142
  *
@@ -6140,9 +6187,9 @@
6140
6187
  bw.getUUID = function (tacoOrElement) {
6141
6188
  if (!tacoOrElement) return null;
6142
6189
  var classStr;
6143
- // DOM element: check className
6190
+ // DOM element: check className (SVG elements use getAttribute for string value)
6144
6191
  if (tacoOrElement.className !== undefined && tacoOrElement.tagName) {
6145
- classStr = tacoOrElement.className;
6192
+ classStr = typeof tacoOrElement.className === 'string' ? tacoOrElement.className : tacoOrElement.getAttribute('class') || '';
6146
6193
  }
6147
6194
  // TACO object: check a.class
6148
6195
  else if (tacoOrElement.a && _is(tacoOrElement.a["class"], 'string')) {
@@ -6427,7 +6474,7 @@
6427
6474
  var fnCounterBefore = bw._fnIDCounter;
6428
6475
 
6429
6476
  // Render body content
6430
- var bodyHTML = '';
6477
+ var bodyHTML;
6431
6478
  if (_is(body, 'string')) {
6432
6479
  bodyHTML = body;
6433
6480
  } else {
@@ -6603,8 +6650,10 @@
6603
6650
  _taco$o2 = taco.o,
6604
6651
  opts = _taco$o2 === void 0 ? {} : _taco$o2;
6605
6652
 
6606
- // Create element
6607
- var el = document.createElement(tag);
6653
+ // SVG namespace: detect SVG context and thread through children.
6654
+ // {t:'svg'} starts SVG context; foreignObject children revert to HTML.
6655
+ var svgCtx = options._svgCtx || tag === 'svg';
6656
+ var el = svgCtx ? document.createElementNS(_SVG_NS, tag) : document.createElement(tag);
6608
6657
 
6609
6658
  // Set attributes
6610
6659
  for (var _i2 = 0, _Object$entries2 = Object.entries(attrs); _i2 < _Object$entries2.length; _i2++) {
@@ -6617,9 +6666,10 @@
6617
6666
  Object.assign(el.style, value);
6618
6667
  } else if (key === 'class') {
6619
6668
  // Handle class as array or string
6669
+ // SVG elements use SVGAnimatedString for className, so use setAttribute
6620
6670
  var classStr = _isA(value) ? value.filter(Boolean).join(' ') : String(value);
6621
6671
  if (classStr) {
6622
- el.className = classStr;
6672
+ if (svgCtx) el.setAttribute('class', classStr);else el.className = classStr;
6623
6673
  }
6624
6674
  } else if (key.startsWith('on') && _is(value, 'function')) {
6625
6675
  // Event handlers
@@ -6640,11 +6690,19 @@
6640
6690
  // Add children, building _bw_refs for fast parent→child access.
6641
6691
  // Children with id attributes or bw_uuid_* classes get local refs on the parent,
6642
6692
  // so o.render functions can access them without any DOM lookup.
6693
+ // SVG: foreignObject children revert to HTML namespace; otherwise inherit.
6694
+ var childOpts = options;
6695
+ var childSvgCtx = svgCtx && tag !== 'foreignObject';
6696
+ if (childSvgCtx !== (options._svgCtx || false)) {
6697
+ childOpts = Object.assign({}, options, {
6698
+ _svgCtx: childSvgCtx || undefined
6699
+ });
6700
+ }
6643
6701
  if (content != null) {
6644
6702
  if (_isA(content)) {
6645
6703
  content.forEach(function (child) {
6646
6704
  if (child != null) {
6647
- var childEl = bw.createDOM(child, options);
6705
+ var childEl = bw.createDOM(child, childOpts);
6648
6706
  el.appendChild(childEl);
6649
6707
  // Build local refs for addressable children
6650
6708
  var childRefId = child && child.a ? child.a.id || bw.getUUID(child) : null;
@@ -6667,7 +6725,7 @@
6667
6725
  // Raw HTML content — inject via innerHTML
6668
6726
  el.innerHTML = content.v;
6669
6727
  } else if (_is(content, 'object') && content.t) {
6670
- var childEl = bw.createDOM(content, options);
6728
+ var childEl = bw.createDOM(content, childOpts);
6671
6729
  el.appendChild(childEl);
6672
6730
  var childRefId = content.a ? content.a.id || bw.getUUID(content) : null;
6673
6731
  if (childRefId) {
@@ -6693,13 +6751,21 @@
6693
6751
  }
6694
6752
 
6695
6753
  // Register UUID class in node cache (bw_uuid_* tokens in class string)
6696
- if (el.className) {
6697
- var uuidMatch = el.className.match(_UUID_RE);
6754
+ // SVG elements have SVGAnimatedString for className; use getAttribute instead
6755
+ var clsStr = svgCtx ? el.getAttribute('class') || '' : el.className;
6756
+ if (clsStr) {
6757
+ var uuidMatch = clsStr.match(_UUID_RE);
6698
6758
  if (uuidMatch) {
6699
6759
  bw._nodeMap[uuidMatch[0]] = el;
6700
6760
  }
6701
6761
  }
6702
6762
 
6763
+ // Store component type metadata (e.g., 'card', 'tabs') for introspection.
6764
+ // BCCL factories set o.type; custom components can too.
6765
+ if (opts.type) {
6766
+ el._bw_type = opts.type;
6767
+ }
6768
+
6703
6769
  // Handle lifecycle hooks and state
6704
6770
  if (opts.mounted || opts.unmount || opts.render || opts.state) {
6705
6771
  // Ensure element has a UUID class for identity
@@ -6728,11 +6794,19 @@
6728
6794
  } : null);
6729
6795
  if (mountFn) {
6730
6796
  if (document.body.contains(el)) {
6731
- mountFn(el, el._bw_state || {});
6797
+ try {
6798
+ mountFn(el, el._bw_state || {});
6799
+ } catch (e) {
6800
+ _cw('o.mounted error: ' + e.message);
6801
+ }
6732
6802
  } else {
6733
6803
  requestAnimationFrame(function () {
6734
6804
  if (document.body.contains(el)) {
6735
- mountFn(el, el._bw_state || {});
6805
+ try {
6806
+ mountFn(el, el._bw_state || {});
6807
+ } catch (e) {
6808
+ _cw('o.mounted error: ' + e.message);
6809
+ }
6736
6810
  }
6737
6811
  });
6738
6812
  }
@@ -6741,7 +6815,11 @@
6741
6815
  // Store unmount callback keyed by UUID class
6742
6816
  if (opts.unmount) {
6743
6817
  bw._unmountCallbacks.set(uuid, function () {
6744
- opts.unmount(el, el._bw_state || {});
6818
+ try {
6819
+ opts.unmount(el, el._bw_state || {});
6820
+ } catch (e) {
6821
+ _cw('o.unmount error: ' + e.message);
6822
+ }
6745
6823
  });
6746
6824
  }
6747
6825
  }
@@ -6760,24 +6838,25 @@
6760
6838
  }
6761
6839
 
6762
6840
  // Slot declarations: auto-generate setX/getX pairs
6841
+ // The target element is cached at creation time to avoid repeated
6842
+ // querySelector calls on every get/set invocation.
6763
6843
  if (opts.slots) {
6764
6844
  for (var sk in opts.slots) {
6765
6845
  if (_hop.call(opts.slots, sk)) {
6766
6846
  (function (name, selector) {
6847
+ var target = el.querySelector(selector);
6767
6848
  var cap = name.charAt(0).toUpperCase() + name.slice(1);
6768
6849
  el.bw['set' + cap] = function (value) {
6769
- var t = el.querySelector(selector);
6770
- if (!t) return;
6850
+ if (!target) return;
6771
6851
  if (value != null && _typeof(value) === 'object' && value.t) {
6772
- t.innerHTML = '';
6773
- t.appendChild(bw.createDOM(value));
6852
+ target.innerHTML = '';
6853
+ target.appendChild(bw.createDOM(value));
6774
6854
  } else {
6775
- t.textContent = value != null ? String(value) : '';
6855
+ target.textContent = value != null ? String(value) : '';
6776
6856
  }
6777
6857
  };
6778
6858
  el.bw['get' + cap] = function () {
6779
- var t = el.querySelector(selector);
6780
- return t ? t.textContent : '';
6859
+ return target ? target.textContent : '';
6781
6860
  };
6782
6861
  })(sk, opts.slots[sk]);
6783
6862
  }
@@ -6818,7 +6897,7 @@
6818
6897
  }
6819
6898
 
6820
6899
  // Get target element (use cache-backed lookup)
6821
- var targetEl = bw._el(target);
6900
+ var targetEl = bw.el(target);
6822
6901
  if (!targetEl) {
6823
6902
  _ce('bw.DOM: Target element not found:', target);
6824
6903
  return null;
@@ -6921,7 +7000,8 @@
6921
7000
  // Deregister UUID classes from node cache for non-lifecycle UUID elements
6922
7001
  var uuidEls = element.querySelectorAll('[class*="bw_uuid_"]');
6923
7002
  uuidEls.forEach(function (uel) {
6924
- var m = uel.className && uel.className.match(_UUID_RE);
7003
+ var uc = typeof uel.className === 'string' ? uel.className : uel.getAttribute('class') || '';
7004
+ var m = uc && uc.match(_UUID_RE);
6925
7005
  if (m) delete bw._nodeMap[m[0]];
6926
7006
  });
6927
7007
 
@@ -7009,9 +7089,13 @@
7009
7089
  * bw.update(el); // re-renders, emits bw:statechange
7010
7090
  */
7011
7091
  bw.update = function (target) {
7012
- var el = bw._el(target);
7092
+ var el = bw.el(target);
7013
7093
  if (el && el._bw_render) {
7014
- el._bw_render(el, el._bw_state || {});
7094
+ try {
7095
+ el._bw_render(el, el._bw_state || {});
7096
+ } catch (e) {
7097
+ _cw('o.render error: ' + e.message);
7098
+ }
7015
7099
  bw.emit(el, 'statechange', el._bw_state);
7016
7100
  }
7017
7101
  return el || null;
@@ -7038,7 +7122,7 @@
7038
7122
  * bw.patch('info', { t: 'em', c: 'new' }); // replace children with TACO
7039
7123
  */
7040
7124
  bw.patch = function (id, content, attr) {
7041
- var el = bw._el(id);
7125
+ var el = bw.el(id);
7042
7126
  if (!el) return null;
7043
7127
  if (attr) {
7044
7128
  // Patch an attribute
@@ -7109,7 +7193,7 @@
7109
7193
  * // Dispatches CustomEvent 'bw:statechange' on the element
7110
7194
  */
7111
7195
  bw.emit = function (target, eventName, detail) {
7112
- var el = bw._el(target);
7196
+ var el = bw.el(target);
7113
7197
  if (el) {
7114
7198
  el.dispatchEvent(new CustomEvent('bw:' + eventName, {
7115
7199
  bubbles: true,
@@ -7138,7 +7222,7 @@
7138
7222
  * });
7139
7223
  */
7140
7224
  bw.on = function (target, eventName, handler) {
7141
- var el = bw._el(target);
7225
+ var el = bw.el(target);
7142
7226
  if (el) {
7143
7227
  el.addEventListener('bw:' + eventName, function (e) {
7144
7228
  handler(e.detail, e);
@@ -7165,23 +7249,46 @@
7165
7249
  *
7166
7250
  * @param {string} topic - Topic name (plain string, no prefix)
7167
7251
  * @param {*} [detail] - Data to pass to subscribers
7168
- * @returns {number} Count of successfully called subscribers
7252
+ * @returns {number} Count of successfully called subscribers (including wildcard matches)
7169
7253
  * @category Pub/Sub
7170
7254
  * @see bw.sub
7171
7255
  * @example
7172
7256
  * bw.pub('score:updated', { player: 'X', score: 10 });
7257
+ * // Wildcard subscribers matching 'score:*' will also fire
7173
7258
  */
7174
7259
  bw.pub = function (topic, detail) {
7175
- var subs = bw._topics[topic];
7176
- if (!subs || subs.length === 0) return 0;
7177
- var snapshot = subs.slice(); // safe against unsub during iteration
7178
7260
  var called = 0;
7179
- for (var i = 0; i < snapshot.length; i++) {
7180
- try {
7181
- snapshot[i].handler(detail);
7182
- called++;
7183
- } catch (err) {
7184
- _cw('bw.pub: subscriber error on topic "' + topic + '":', err);
7261
+ // Exact-match subscribers
7262
+ var subs = bw._topics[topic];
7263
+ if (subs && subs.length > 0) {
7264
+ var snapshot = subs.slice();
7265
+ for (var i = 0; i < snapshot.length; i++) {
7266
+ try {
7267
+ snapshot[i].handler(detail, topic);
7268
+ called++;
7269
+ } catch (err) {
7270
+ _cw('bw.pub: subscriber error on topic "' + topic + '":', err);
7271
+ }
7272
+ }
7273
+ }
7274
+ // Wildcard subscribers -- patterns ending with '*'
7275
+ var keys = Object.keys(bw._topics);
7276
+ for (var k = 0; k < keys.length; k++) {
7277
+ var pat = keys[k];
7278
+ if (pat.charAt(pat.length - 1) !== '*') continue;
7279
+ var prefix = pat.slice(0, -1); // strip trailing '*'
7280
+ if (topic.length >= prefix.length && topic.substring(0, prefix.length) === prefix && topic !== pat) {
7281
+ var wsubs = bw._topics[pat];
7282
+ if (!wsubs) continue;
7283
+ var wsnap = wsubs.slice();
7284
+ for (var w = 0; w < wsnap.length; w++) {
7285
+ try {
7286
+ wsnap[w].handler(detail, topic);
7287
+ called++;
7288
+ } catch (err) {
7289
+ _cw('bw.pub: wildcard subscriber error on "' + pat + '" for topic "' + topic + '":', err);
7290
+ }
7291
+ }
7185
7292
  }
7186
7293
  }
7187
7294
  return called;
@@ -7190,12 +7297,17 @@
7190
7297
  /**
7191
7298
  * Subscribe to a topic. Returns an unsub() function.
7192
7299
  *
7193
- * Optional third argument ties the subscription to a DOM element's lifecycle —
7300
+ * Supports wildcard patterns: a topic ending in `*` matches any published
7301
+ * topic that starts with the prefix before the `*`. For example,
7302
+ * `'agui:*'` matches `'agui:ready'`, `'agui:error'`, etc. The handler
7303
+ * receives `(detail, topic)` so it can distinguish which topic fired.
7304
+ *
7305
+ * Optional third argument ties the subscription to a DOM element's lifecycle --
7194
7306
  * when `bw.cleanup()` is called on that element, the subscription is automatically
7195
7307
  * removed, preventing memory leaks.
7196
7308
  *
7197
- * @param {string} topic - Topic name
7198
- * @param {Function} handler - Called with (detail) on each publish
7309
+ * @param {string} topic - Topic name, or wildcard pattern ending in '*'
7310
+ * @param {Function} handler - Called with (detail, topic) on each publish
7199
7311
  * @param {Element} [el] - Optional DOM element to tie lifecycle to
7200
7312
  * @returns {Function} Call to unsubscribe
7201
7313
  * @category Pub/Sub
@@ -7206,6 +7318,11 @@
7206
7318
  * console.log(detail.player, 'scored', detail.score);
7207
7319
  * });
7208
7320
  * // Later: unsub() to stop listening
7321
+ *
7322
+ * // Wildcard: listen to all 'agui:' topics
7323
+ * bw.sub('agui:*', function(detail, topic) {
7324
+ * console.log('Got', topic, detail);
7325
+ * });
7209
7326
  */
7210
7327
  bw.sub = function (topic, handler, el) {
7211
7328
  var id = ++bw._subIdCounter;
@@ -7262,6 +7379,37 @@
7262
7379
  return removed;
7263
7380
  };
7264
7381
 
7382
+ /**
7383
+ * Subscribe to a topic for a single event only. The subscription is
7384
+ * automatically removed after the first publish. Equivalent to manually
7385
+ * calling unsub() inside a bw.sub() handler, but avoids the common bug
7386
+ * of forgetting to unsubscribe.
7387
+ *
7388
+ * @param {string} topic - Topic name
7389
+ * @param {Function} handler - Called once with (detail) on the next publish
7390
+ * @param {Element} [el] - Optional DOM element to tie lifecycle to
7391
+ * @returns {Function} Call to cancel the subscription before it fires
7392
+ * @category Pub/Sub
7393
+ * @see bw.sub
7394
+ * @see bw.pub
7395
+ * @example
7396
+ * bw.once('data:loaded', function(detail) {
7397
+ * console.log('Received:', detail);
7398
+ * // No need to unsubscribe -- already done automatically
7399
+ * });
7400
+ *
7401
+ * // Cancel before it fires:
7402
+ * var cancel = bw.once('timeout', handler);
7403
+ * cancel(); // handler will never be called
7404
+ */
7405
+ bw.once = function (topic, handler, el) {
7406
+ var unsub = bw.sub(topic, function (detail) {
7407
+ unsub();
7408
+ handler(detail);
7409
+ }, el);
7410
+ return unsub;
7411
+ };
7412
+
7265
7413
  // ===================================================================================
7266
7414
  // Function Registry (revived from v1 for string dispatch contexts)
7267
7415
  // ===================================================================================
@@ -7505,7 +7653,7 @@
7505
7653
  * };
7506
7654
  */
7507
7655
  bw.message = function (target, action, data) {
7508
- var el = bw._el(target);
7656
+ var el = bw.el(target);
7509
7657
  if (!el) el = bw.$('.' + target)[0];
7510
7658
  if (!el || !el.bw || typeof el.bw[action] !== 'function') {
7511
7659
  _cw('bw.message: no handle method "' + action + '" on ' + target);
@@ -7515,6 +7663,217 @@
7515
7663
  return true;
7516
7664
  };
7517
7665
 
7666
+ /**
7667
+ * Collect form data from all input, select, and textarea elements within a
7668
+ * container. Each element's `name` attribute (or `id` if no name) becomes a
7669
+ * key in the returned object. This provides a lightweight alternative to the
7670
+ * browser FormData API that returns a plain object suitable for JSON
7671
+ * serialization or bw.pub().
7672
+ *
7673
+ * Handles all standard HTML form controls:
7674
+ * - text/number/email/etc inputs: string value
7675
+ * - checkboxes: boolean (true/false)
7676
+ * - radio buttons: string value of the checked radio (unchecked groups omitted)
7677
+ * - multi-select: array of selected option values
7678
+ * - textarea: string value
7679
+ *
7680
+ * Elements without both `name` and `id` attributes are silently skipped.
7681
+ *
7682
+ * @param {string|Element} target - CSS selector, UUID string, or DOM element
7683
+ * @returns {Object} Plain object mapping field names to values
7684
+ * @category Component
7685
+ * @see bw.makeForm
7686
+ * @see bw.makeInput
7687
+ * @example
7688
+ * // Given a form with name="email" input and name="agree" checkbox:
7689
+ * var data = bw.formData('#signup-form');
7690
+ * // => { email: 'user@example.com', agree: true }
7691
+ *
7692
+ * // Collect and publish in one step:
7693
+ * bw.pub('form:submit', bw.formData('#my-form'));
7694
+ *
7695
+ * // Works with any container, not just <form>:
7696
+ * bw.pub('settings:changed', bw.formData('.settings-panel'));
7697
+ */
7698
+ bw.formData = function (target) {
7699
+ var el = bw.el(target);
7700
+ if (!el) return {};
7701
+ var result = {};
7702
+ var inputs = el.querySelectorAll('input, select, textarea');
7703
+ for (var i = 0; i < inputs.length; i++) {
7704
+ var inp = inputs[i];
7705
+ var key = inp.name || inp.id;
7706
+ if (!key) continue;
7707
+ if (inp.type === 'checkbox') {
7708
+ result[key] = inp.checked;
7709
+ } else if (inp.type === 'radio') {
7710
+ if (inp.checked) result[key] = inp.value;
7711
+ } else if (inp.tagName === 'SELECT' && inp.multiple) {
7712
+ result[key] = [];
7713
+ for (var j = 0; j < inp.options.length; j++) {
7714
+ if (inp.options[j].selected) result[key].push(inp.options[j].value);
7715
+ }
7716
+ } else {
7717
+ result[key] = inp.value;
7718
+ }
7719
+ }
7720
+ return result;
7721
+ };
7722
+
7723
+ // ===================================================================================
7724
+ // bw.jsonPatch() — RFC 6902 JSON Patch on plain objects
7725
+ // ===================================================================================
7726
+
7727
+ /**
7728
+ * Apply RFC 6902 JSON Patch operations to a plain object.
7729
+ *
7730
+ * Supported operations: add, remove, replace, move, copy, test.
7731
+ * Paths use JSON Pointer (RFC 6901) notation: `/foo/bar/0`.
7732
+ * Mutates the target object in place and returns it.
7733
+ *
7734
+ * @param {Object} obj - Target object to patch
7735
+ * @param {Array<Object>} ops - Array of patch operations
7736
+ * @param {string} ops[].op - Operation: 'add', 'remove', 'replace', 'move', 'copy', 'test'
7737
+ * @param {string} ops[].path - JSON Pointer path (e.g. '/a/b/0')
7738
+ * @param {*} [ops[].value] - Value for add/replace/test
7739
+ * @param {string} [ops[].from] - Source path for move/copy
7740
+ * @returns {Object} The patched object (same reference)
7741
+ * @throws {Error} On invalid op, missing path, test failure, or path not found for remove
7742
+ * @category Data Utilities
7743
+ * @see bw.patch
7744
+ * @example
7745
+ * var obj = { a: 1, b: { c: 2 } };
7746
+ * bw.jsonPatch(obj, [
7747
+ * { op: 'replace', path: '/a', value: 10 },
7748
+ * { op: 'add', path: '/b/d', value: 3 },
7749
+ * { op: 'remove', path: '/b/c' }
7750
+ * ]);
7751
+ * // obj => { a: 10, b: { d: 3 } }
7752
+ */
7753
+ bw.jsonPatch = function (obj, ops) {
7754
+ if (!_isA(ops)) return obj;
7755
+
7756
+ // Parse JSON Pointer path to array of keys
7757
+ function parsePath(path) {
7758
+ if (path === '') return [];
7759
+ if (path.charAt(0) !== '/') throw new Error('Invalid JSON Pointer: ' + path);
7760
+ return path.slice(1).split('/').map(function (s) {
7761
+ return s.replace(/~1/g, '/').replace(/~0/g, '~');
7762
+ });
7763
+ }
7764
+
7765
+ // Walk to parent of final key; return { parent, key }
7766
+ function resolve(root, keys) {
7767
+ var parent = root;
7768
+ for (var i = 0; i < keys.length - 1; i++) {
7769
+ var k = _isA(parent) ? parseInt(keys[i], 10) : keys[i];
7770
+ if (parent[k] === undefined) throw new Error('Path not found: /' + keys.slice(0, i + 1).join('/'));
7771
+ parent = parent[k];
7772
+ }
7773
+ return {
7774
+ parent: parent,
7775
+ key: _isA(parent) ? parseInt(keys[keys.length - 1], 10) : keys[keys.length - 1]
7776
+ };
7777
+ }
7778
+
7779
+ // Get value at path
7780
+ function getVal(root, keys) {
7781
+ var cur = root;
7782
+ for (var i = 0; i < keys.length; i++) {
7783
+ var k = _isA(cur) ? parseInt(keys[i], 10) : keys[i];
7784
+ if (cur[k] === undefined) throw new Error('Path not found: /' + keys.slice(0, i + 1).join('/'));
7785
+ cur = cur[k];
7786
+ }
7787
+ return cur;
7788
+ }
7789
+ for (var i = 0; i < ops.length; i++) {
7790
+ var op = ops[i];
7791
+ if (!op.op || !_is(op.path, 'string')) throw new Error('Invalid patch operation at index ' + i);
7792
+ var keys = parsePath(op.path);
7793
+ var r, val, fromKeys, fr, tr, cr;
7794
+ switch (op.op) {
7795
+ case 'add':
7796
+ {
7797
+ if (keys.length === 0) throw new Error('Cannot add to root');
7798
+ r = resolve(obj, keys);
7799
+ if (_isA(r.parent) && r.key <= r.parent.length) {
7800
+ r.parent.splice(r.key, 0, op.value);
7801
+ } else {
7802
+ r.parent[r.key] = op.value;
7803
+ }
7804
+ break;
7805
+ }
7806
+ case 'remove':
7807
+ {
7808
+ if (keys.length === 0) throw new Error('Cannot remove root');
7809
+ r = resolve(obj, keys);
7810
+ if (_isA(r.parent)) {
7811
+ if (r.key >= r.parent.length) throw new Error('Index out of bounds: ' + r.key);
7812
+ r.parent.splice(r.key, 1);
7813
+ } else {
7814
+ if (!(r.key in r.parent)) throw new Error('Path not found: ' + op.path);
7815
+ delete r.parent[r.key];
7816
+ }
7817
+ break;
7818
+ }
7819
+ case 'replace':
7820
+ {
7821
+ if (keys.length === 0) throw new Error('Cannot replace root');
7822
+ r = resolve(obj, keys);
7823
+ if (_isA(r.parent)) {
7824
+ if (r.key >= r.parent.length) throw new Error('Index out of bounds: ' + r.key);
7825
+ } else {
7826
+ if (!(r.key in r.parent)) throw new Error('Path not found: ' + op.path);
7827
+ }
7828
+ r.parent[r.key] = op.value;
7829
+ break;
7830
+ }
7831
+ case 'move':
7832
+ {
7833
+ if (!_is(op.from, 'string')) throw new Error('move requires "from"');
7834
+ fromKeys = parsePath(op.from);
7835
+ val = getVal(obj, fromKeys);
7836
+ fr = resolve(obj, fromKeys);
7837
+ if (_isA(fr.parent)) {
7838
+ fr.parent.splice(fr.key, 1);
7839
+ } else {
7840
+ delete fr.parent[fr.key];
7841
+ }
7842
+ tr = resolve(obj, keys);
7843
+ if (_isA(tr.parent) && tr.key <= tr.parent.length) {
7844
+ tr.parent.splice(tr.key, 0, val);
7845
+ } else {
7846
+ tr.parent[tr.key] = val;
7847
+ }
7848
+ break;
7849
+ }
7850
+ case 'copy':
7851
+ {
7852
+ if (!_is(op.from, 'string')) throw new Error('copy requires "from"');
7853
+ val = getVal(obj, parsePath(op.from));
7854
+ cr = resolve(obj, keys);
7855
+ if (_isA(cr.parent) && cr.key <= cr.parent.length) {
7856
+ cr.parent.splice(cr.key, 0, val);
7857
+ } else {
7858
+ cr.parent[cr.key] = val;
7859
+ }
7860
+ break;
7861
+ }
7862
+ case 'test':
7863
+ {
7864
+ var actual = getVal(obj, keys);
7865
+ if (JSON.stringify(actual) !== JSON.stringify(op.value)) {
7866
+ throw new Error('Test failed: ' + op.path + ' expected ' + JSON.stringify(op.value) + ' got ' + JSON.stringify(actual));
7867
+ }
7868
+ break;
7869
+ }
7870
+ default:
7871
+ throw new Error('Unknown op: ' + op.op);
7872
+ }
7873
+ }
7874
+ return obj;
7875
+ };
7876
+
7518
7877
  // ===================================================================================
7519
7878
  // bw.apply() / bw.parseJSONFlex() — Server-driven UI protocol
7520
7879
  // ===================================================================================
@@ -7647,7 +8006,7 @@
7647
8006
  var type = msg.type;
7648
8007
  var target = msg.target;
7649
8008
  if (type === 'replace') {
7650
- var el = bw._el(target);
8009
+ var el = bw.el(target);
7651
8010
  if (!el) return false;
7652
8011
  bw.DOM(el, msg.node);
7653
8012
  return true;
@@ -7655,13 +8014,13 @@
7655
8014
  var patched = bw.patch(target, msg.content, msg.attr);
7656
8015
  return patched !== null;
7657
8016
  } else if (type === 'append') {
7658
- var parent = bw._el(target);
8017
+ var parent = bw.el(target);
7659
8018
  if (!parent) return false;
7660
8019
  var child = bw.createDOM(msg.node);
7661
8020
  parent.appendChild(child);
7662
8021
  return true;
7663
8022
  } else if (type === 'remove') {
7664
- var toRemove = bw._el(target);
8023
+ var toRemove = bw.el(target);
7665
8024
  if (!toRemove) return false;
7666
8025
  if (_is(bw.cleanup, 'function')) bw.cleanup(toRemove);
7667
8026
  toRemove.remove();
@@ -7714,33 +8073,99 @@
7714
8073
  };
7715
8074
 
7716
8075
  // ===================================================================================
7717
- // bw.inspect() — Debug utility
8076
+ // bw.inspect() — DOM introspection with bitwrench metadata
7718
8077
  // ===================================================================================
7719
8078
 
7720
8079
  /**
7721
- * Inspect a DOM element's bitwrench state, handle methods, and metadata.
7722
- * Works with DOM elements or CSS selectors.
7723
- *
7724
- * @param {string|Element} target - Selector or DOM element
7725
- * @returns {Element|null} The element, or null if not found
8080
+ * Inspect a DOM element and its subtree, returning a plain-object
8081
+ * representation with bitwrench metadata at each node. Useful for debugging,
8082
+ * devtools, MCP/AG-UI tool discovery, and automated testing.
8083
+ *
8084
+ * Each node in the returned tree includes:
8085
+ * - `tag` -- lowercase tag name (or '#text' for text nodes)
8086
+ * - `id` -- element id (if set)
8087
+ * - `uuid` -- bitwrench UUID class (if lifecycle-managed)
8088
+ * - `type` -- component type from o.type (if set, e.g. 'card', 'tabs')
8089
+ * - `classes` -- first 5 CSS classes (string, space-separated)
8090
+ * - `handles` -- array of el.bw method names (if any)
8091
+ * - `state` -- copy of _bw_state (if any)
8092
+ * - `hasRender` -- true if _bw_render is set
8093
+ * - `hasSubs` -- true if element has pub/sub subscriptions
8094
+ * - `refs` -- copy of _bw_refs keys (if any)
8095
+ * - `children` -- array of child node trees (up to depth limit, max 50 per level)
8096
+ *
8097
+ * @param {string|Element} target - CSS selector, UUID, or DOM element
8098
+ * @param {number} [depth=3] - Maximum recursion depth (0 = target only, no children)
8099
+ * @returns {Object|null} Plain object tree, or null if element not found
7726
8100
  * @category Component
7727
8101
  * @example
7728
- * bw.inspect('#my-carousel');
7729
- * bw.inspect($0);
8102
+ * // Get full tree from #app, 3 levels deep (default):
8103
+ * var info = bw.inspect('#app');
8104
+ *
8105
+ * // Shallow inspection (just the element, no children):
8106
+ * var info = bw.inspect('#my-carousel', 0);
8107
+ * console.log(info.handles); // ['next', 'prev', 'goToSlide']
8108
+ * console.log(info.type); // 'carousel'
8109
+ *
8110
+ * // Deep inspection for debugging:
8111
+ * console.log(JSON.stringify(bw.inspect('#app', 5), null, 2));
7730
8112
  */
7731
- bw.inspect = function (target) {
7732
- var el = _is(target, 'string') ? bw.$(target)[0] : target;
7733
- if (!el) {
7734
- _cw('bw.inspect: element not found');
7735
- return null;
8113
+ bw.inspect = function (target, depth) {
8114
+ var el = bw.el(target);
8115
+ if (!el && _is(target, 'string')) el = bw.$(target)[0];
8116
+ if (!el) return null;
8117
+ if (depth === undefined || depth === null) depth = 3;
8118
+ function walk(node, d) {
8119
+ if (!node) return null;
8120
+ // Skip non-element nodes (text, comment, etc.)
8121
+ if (node.nodeType !== 1) return null;
8122
+ var info = {
8123
+ tag: node.tagName ? node.tagName.toLowerCase() : '#text'
8124
+ };
8125
+
8126
+ // Identity
8127
+ if (node.id) info.id = node.id;
8128
+ var uuid = bw.getUUID(node);
8129
+ if (uuid) info.uuid = uuid;
8130
+ if (node._bw_type) info.type = node._bw_type;
8131
+
8132
+ // CSS classes (first 5 for readability)
8133
+ if (node.className && typeof node.className === 'string') {
8134
+ info.classes = node.className.split(' ').slice(0, 5).join(' ');
8135
+ }
8136
+
8137
+ // Bitwrench handle methods
8138
+ if (node.bw) {
8139
+ var handles = _keys(node.bw);
8140
+ if (handles.length > 0) info.handles = handles;
8141
+ }
8142
+
8143
+ // State
8144
+ if (node._bw_state) info.state = node._bw_state;
8145
+ if (node._bw_render) info.hasRender = true;
8146
+ if (node._bw_subs && node._bw_subs.length > 0) info.hasSubs = true;
8147
+
8148
+ // Refs
8149
+ if (node._bw_refs) info.refs = _keys(node._bw_refs);
8150
+
8151
+ // Children (recurse up to depth limit, max 50 children per level)
8152
+ if (d < depth && node.children && node.children.length > 0) {
8153
+ info.children = [];
8154
+ var max = Math.min(node.children.length, 50);
8155
+ for (var i = 0; i < max; i++) {
8156
+ var child = walk(node.children[i], d + 1);
8157
+ if (child) info.children.push(child);
8158
+ }
8159
+ if (node.children.length > 50) {
8160
+ info.children.push({
8161
+ tag: '...',
8162
+ count: node.children.length - 50
8163
+ });
8164
+ }
8165
+ }
8166
+ return info;
7736
8167
  }
7737
- console.group('Element: ' + (bw.getUUID(el) || el.id || el.tagName));
7738
- _cl('State:', el._bw_state || '(none)');
7739
- _cl('Handle:', el.bw ? _keys(el.bw) : '(none)');
7740
- _cl('Classes:', el.className);
7741
- _cl('Refs:', el._bw_refs || '(none)');
7742
- console.groupEnd();
7743
- return el;
8168
+ return walk(el, 0);
7744
8169
  };
7745
8170
  bw.compile = function () {
7746
8171
  throw new Error('bw.compile() removed in v2.0.19. Use o.handle/o.slots on TACO options instead.');
@@ -7979,34 +8404,45 @@
7979
8404
  * so you can use `.map()`, `.filter()`, etc. directly. Accepts CSS selectors,
7980
8405
  * single elements, NodeLists, or arrays.
7981
8406
  *
8407
+ * With an optional second argument, applies content or a function to
8408
+ * every matched element (same apply rules as `bw.el()`):
8409
+ * - string/number: sets `el.textContent`
8410
+ * - function: calls `apply(el)` for each element
8411
+ * - TACO object: clears children, mounts TACO via `bw.createDOM()`
8412
+ * - array: clears children, appends each item
8413
+ *
7982
8414
  * @param {string|Element|Array} selector - CSS selector, element, or array
8415
+ * @param {string|number|Function|Object|Array} [apply] - Content or function to apply
7983
8416
  * @returns {Array} Array of DOM elements
7984
8417
  * @category DOM Selection
8418
+ * @see bw.el
7985
8419
  * @example
7986
- * bw.$('.card') // => [div.card, div.card, ...]
7987
- * bw.$(myElement) // => [myElement]
7988
- * bw.$('.card').map(el => el.textContent)
8420
+ * bw.$('.card') // => [div.card, div.card, ...]
8421
+ * bw.$('.status', 'Online') // set text on all .status elements
8422
+ * bw.$('.card', function(el) { // apply function to each
8423
+ * el.style.opacity = '0.5';
8424
+ * })
7989
8425
  */
7990
8426
  if (bw._isBrowser) {
7991
- bw.$ = function (selector) {
7992
- if (!selector) return [];
7993
-
7994
- // Already an array
7995
- if (_isA(selector)) return selector;
7996
-
7997
- // Single element
7998
- if (selector.nodeType) return [selector];
7999
-
8000
- // NodeList or HTMLCollection
8001
- if (selector.length !== undefined && !_is(selector, 'string')) {
8002
- return Array.from(selector);
8427
+ bw.$ = function (selector, apply) {
8428
+ var els;
8429
+ if (!selector) {
8430
+ els = [];
8431
+ } else if (_isA(selector)) {
8432
+ els = selector;
8433
+ } else if (selector.nodeType) {
8434
+ els = [selector];
8435
+ } else if (selector.length !== undefined && !_is(selector, 'string')) {
8436
+ els = Array.from(selector);
8437
+ } else if (_is(selector, 'string')) {
8438
+ els = Array.from(document.querySelectorAll(selector));
8439
+ } else {
8440
+ els = [];
8003
8441
  }
8004
-
8005
- // CSS selector string
8006
- if (_is(selector, 'string')) {
8007
- return Array.from(document.querySelectorAll(selector));
8442
+ if (apply !== undefined) {
8443
+ for (var i = 0; i < els.length; i++) _applyTo(els[i], apply);
8008
8444
  }
8009
- return [];
8445
+ return els;
8010
8446
  };
8011
8447
 
8012
8448
  // Convenience single element selector
@@ -8225,41 +8661,47 @@
8225
8661
  };
8226
8662
 
8227
8663
  /**
8228
- * Toggle between primary and alternate palettes.
8664
+ * Toggle between primary and alternate theme palettes.
8229
8665
  *
8230
- * Adds/removes the `bw_theme_alt` class on the scoping element.
8666
+ * Adds/removes the `bw_theme_alt` class on the scoping element(s).
8231
8667
  * Without a scope, toggles on `<html>` (global).
8232
- * With a scope, toggles on the first matching element.
8668
+ * With a scope, toggles on ALL matching elements.
8233
8669
  *
8234
- * @param {string} [scope] - Scope selector (e.g. '#my-dashboard'). Omit for global.
8235
- * @returns {string} Active mode after toggle: 'primary' or 'alternate'
8670
+ * @param {string|Element} [scope] - Selector or element. Omit for global.
8671
+ * @returns {string} Active mode after toggle: 'primary' or 'alternate' (based on first element)
8236
8672
  * @category CSS & Styling
8237
8673
  * @see bw.applyStyles
8238
8674
  * @see bw.clearStyles
8239
8675
  * @example
8240
- * bw.toggleStyles(); // global toggle on <html>
8241
- * bw.toggleStyles('#my-dashboard'); // scoped toggle
8676
+ * bw.toggleThemeMode(); // global toggle on <html>
8677
+ * bw.toggleThemeMode('#my-dashboard'); // scoped toggle
8678
+ * bw.toggleThemeMode('.panel'); // toggle on ALL .panel elements
8242
8679
  */
8243
- bw.toggleStyles = function (scope) {
8680
+ bw.toggleThemeMode = function (scope) {
8244
8681
  if (!bw._isBrowser) return 'primary';
8245
- var target;
8682
+ var els;
8246
8683
  if (scope) {
8247
- var els = bw.$(scope);
8248
- target = els[0];
8249
- } else {
8250
- target = document.documentElement;
8251
- }
8252
- if (!target) return 'primary';
8253
- var hasAlt = target.classList.contains('bw_theme_alt');
8254
- if (hasAlt) {
8255
- target.classList.remove('bw_theme_alt');
8256
- return 'primary';
8684
+ els = bw.$(scope);
8257
8685
  } else {
8258
- target.classList.add('bw_theme_alt');
8259
- return 'alternate';
8686
+ els = [document.documentElement];
8687
+ }
8688
+ if (!els.length) return 'primary';
8689
+ var mode;
8690
+ for (var i = 0; i < els.length; i++) {
8691
+ var hasAlt = els[i].classList.contains('bw_theme_alt');
8692
+ if (hasAlt) {
8693
+ els[i].classList.remove('bw_theme_alt');
8694
+ } else {
8695
+ els[i].classList.add('bw_theme_alt');
8696
+ }
8697
+ if (i === 0) mode = hasAlt ? 'primary' : 'alternate';
8260
8698
  }
8699
+ return mode;
8261
8700
  };
8262
8701
 
8702
+ // Alias — kept for one release cycle. Use bw.toggleThemeMode() instead.
8703
+ bw.toggleStyles = bw.toggleThemeMode;
8704
+
8263
8705
  /**
8264
8706
  * Remove injected styles for a given scope.
8265
8707
  *
@@ -9359,6 +9801,57 @@
9359
9801
  }
9360
9802
  });
9361
9803
 
9804
+ /**
9805
+ * Query the BCCL component registry. Returns metadata about registered
9806
+ * component types -- their names and factory function names. Useful for
9807
+ * tooling, introspection, documentation generators, and auto-complete
9808
+ * systems (including MCP/AG-UI tool discovery).
9809
+ *
9810
+ * With no arguments, returns an array of all registered component types.
9811
+ * With a type name, returns metadata for that single type (or null if
9812
+ * the type is not registered).
9813
+ *
9814
+ * @param {string} [type] - Optional component type name to look up
9815
+ * @returns {Array<Object>|Object|null} Array of {type, factory} objects,
9816
+ * a single {type, factory} object, or null if the type is not found
9817
+ * @category Component
9818
+ * @see bw.make
9819
+ * @see bw.BCCL
9820
+ * @example
9821
+ * // List all available component types:
9822
+ * bw.catalog();
9823
+ * // => [{ type: 'card', factory: 'makeCard' },
9824
+ * // { type: 'button', factory: 'makeButton' }, ...]
9825
+ *
9826
+ * // Look up a specific type:
9827
+ * bw.catalog('accordion');
9828
+ * // => { type: 'accordion', factory: 'makeAccordion' }
9829
+ *
9830
+ * // Check if a type exists:
9831
+ * if (bw.catalog('chart')) { ... }
9832
+ *
9833
+ * // Get just the type names:
9834
+ * bw.catalog().map(function(c) { return c.type; });
9835
+ * // => ['card', 'button', 'container', 'row', ...]
9836
+ */
9837
+ bw.catalog = function (type) {
9838
+ if (type) {
9839
+ var def = bw.BCCL[type];
9840
+ if (!def) return null;
9841
+ return {
9842
+ type: type,
9843
+ factory: def.make.name || 'make' + type.charAt(0).toUpperCase() + type.slice(1)
9844
+ };
9845
+ }
9846
+ return Object.keys(bw.BCCL).map(function (k) {
9847
+ var def = bw.BCCL[k];
9848
+ return {
9849
+ type: k,
9850
+ factory: def.make.name || 'make' + k.charAt(0).toUpperCase() + k.slice(1)
9851
+ };
9852
+ });
9853
+ };
9854
+
9362
9855
  // Also attach to global in browsers
9363
9856
  if (bw._isBrowser && typeof window !== 'undefined') {
9364
9857
  window.bw = bw;