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 v2.0.25 | BSD-2-Clause | https://deftio.github.com/bitwrench/pages */
1
+ /*! bitwrench 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) :
@@ -12,14 +12,14 @@
12
12
  */
13
13
 
14
14
  const VERSION_INFO = {
15
- version: '2.0.25',
15
+ version: '2.0.30',
16
16
  name: 'bitwrench',
17
17
  description: 'A library for javascript UI functions.',
18
18
  license: 'BSD-2-Clause',
19
19
  homepage: 'https://deftio.github.com/bitwrench/pages',
20
20
  repository: 'git+https://github.com/deftio/bitwrench.git',
21
21
  author: 'manu a. chatterjee <deftio@deftio.com> (https://deftio.com/)',
22
- buildDate: '2026-03-31T03:03:30.752Z'
22
+ buildDate: '2026-04-12T07:51:29.111Z'
23
23
  };
24
24
 
25
25
  /**
@@ -7785,7 +7785,6 @@
7785
7785
  // Console aliases use thin wrappers (not direct references) so that test
7786
7786
  // code can monkey-patch console.warn/log/error and the patches take effect.
7787
7787
  var _cw = function() { console.warn.apply(console, arguments); };
7788
- var _cl = function() { console.log.apply(console, arguments); };
7789
7788
  var _ce = function() { console.error.apply(console, arguments); };
7790
7789
 
7791
7790
  /**
@@ -7922,61 +7921,105 @@
7922
7921
  };
7923
7922
 
7924
7923
  /**
7925
- * Look up a DOM element by ID string, using the node cache for O(1) access.
7924
+ * Look up a single DOM element by ID, CSS selector, UUID, or element ref.
7925
+ * Optionally apply content or a function to the resolved element.
7926
7926
  *
7927
- * Resolution order:
7928
- * 1. Check `bw._nodeMap[id]` if found and still attached (parentNode !== null), return it
7929
- * 2. If cached ref is detached (parentNode === null), remove stale entry
7930
- * 3. Fall back to `document.getElementById(id)` then `document.querySelector(...)`
7931
- * 4. Try class-based lookup for bw_uuid_* tokens (UUID addressing)
7932
- * 5. Cache the result for next time
7927
+ * Resolution order for string targets:
7928
+ * 1. Check `bw._nodeMap[id]` cache (O(1), stale entries auto-pruned)
7929
+ * 2. `document.getElementById(id)`
7930
+ * 3. `document.querySelector(id)` for selectors starting with # or .
7931
+ * 4. Class-based lookup for `bw_uuid_*` tokens
7933
7932
  *
7934
- * Accepts a DOM element directly (pass-through) or a string identifier.
7935
- * String identifiers are tried as: direct map key, getElementById,
7936
- * querySelector (for CSS selectors starting with . or #), and
7937
- * bw_uuid_* class selector.
7933
+ * With one argument, returns the element (or null). With two arguments,
7934
+ * applies the second argument to the element and returns the element:
7935
+ * - string/number: sets `el.textContent`
7936
+ * - function: calls `apply(el)`, returns el
7937
+ * - TACO object: clears children, mounts TACO via `bw.createDOM()`
7938
+ * - array: clears children, appends each item (string -> text node, TACO -> element)
7938
7939
  *
7939
- * @param {string|Element} id - Element ID, CSS selector, bw_uuid_* class, or DOM element
7940
+ * @param {string|Element} target - Element ref, ID, CSS selector, or bw_uuid_* class
7941
+ * @param {string|number|Function|Object|Array} [apply] - Content or function to apply
7940
7942
  * @returns {Element|null} The DOM element, or null if not found
7941
- * @category Internal
7943
+ * @category DOM Selection
7944
+ * @see bw.$
7945
+ * @see bw.patch
7946
+ * @example
7947
+ * bw.el('#title') // lookup
7948
+ * bw.el('#title', 'Hello') // set text content
7949
+ * bw.el('#app', { t: 'h1', c: 'Hi' }) // mount TACO
7950
+ * bw.el('.card', function(el) { // apply function
7951
+ * el.style.opacity = '0.5';
7952
+ * })
7942
7953
  */
7943
- bw._el = function(id) {
7944
- // Pass-through for DOM elements
7945
- if (!_is(id, 'string')) return id || null;
7946
- if (!id) return null;
7947
- if (!bw._isBrowser) return null;
7948
-
7949
- // 1. Check cache
7950
- var cached = bw._nodeMap[id];
7951
- if (cached) {
7952
- // Verify not detached (parentNode check is IE11-safe)
7953
- if (cached.parentNode !== null) {
7954
- return cached;
7954
+ bw.el = function(target, apply) {
7955
+ // Resolve target to element
7956
+ var el;
7957
+ if (!_is(target, 'string')) {
7958
+ el = target || null;
7959
+ } else if (!target || !bw._isBrowser) {
7960
+ el = null;
7961
+ } else {
7962
+ // 1. Check cache
7963
+ var cached = bw._nodeMap[target];
7964
+ if (cached) {
7965
+ if (cached.parentNode !== null) {
7966
+ el = cached;
7967
+ } else {
7968
+ delete bw._nodeMap[target];
7969
+ }
7970
+ }
7971
+ if (!el) {
7972
+ // 2. getElementById
7973
+ el = document.getElementById(target);
7974
+ // 3. querySelector for CSS selectors
7975
+ if (!el && (target.charAt(0) === '#' || target.charAt(0) === '.')) {
7976
+ el = document.querySelector(target);
7977
+ }
7978
+ // 4. bw_uuid_* class lookup
7979
+ if (!el && target.indexOf('bw_uuid_') === 0) {
7980
+ el = document.querySelector('.' + target);
7981
+ }
7982
+ // 5. Cache result
7983
+ if (el) bw._nodeMap[target] = el;
7955
7984
  }
7956
- // Stale — remove and fall through
7957
- delete bw._nodeMap[id];
7958
7985
  }
7959
7986
 
7960
- // 2. DOM fallback: try getElementById first (fastest native lookup)
7961
- var el = document.getElementById(id);
7962
-
7963
- // 3. Try querySelector for CSS selectors (starts with # or .)
7964
- if (!el && (id.charAt(0) === '#' || id.charAt(0) === '.')) {
7965
- el = document.querySelector(id);
7966
- }
7987
+ // Apply (if provided and element found)
7988
+ if (el && apply !== undefined) _applyTo(el, apply);
7967
7989
 
7968
- // 4. Try class-based lookup for bw_uuid_* tokens (UUID addressing)
7969
- if (!el && id.indexOf('bw_uuid_') === 0) {
7970
- el = document.querySelector('.' + id);
7971
- }
7990
+ return el;
7991
+ };
7972
7992
 
7973
- // 5. Cache the result for next time
7974
- if (el) {
7975
- bw._nodeMap[id] = el;
7993
+ /**
7994
+ * Internal: apply content or function to a DOM element.
7995
+ * Shared by bw.el() and bw.$().
7996
+ * @private
7997
+ */
7998
+ function _applyTo(el, apply) {
7999
+ if (_is(apply, 'function')) {
8000
+ apply(el);
8001
+ } else if (_isA(apply)) {
8002
+ el.innerHTML = '';
8003
+ apply.forEach(function(item) {
8004
+ if (item != null) {
8005
+ if (_is(item, 'object') && item.t) {
8006
+ el.appendChild(bw.createDOM(item));
8007
+ } else {
8008
+ el.appendChild(document.createTextNode(String(item)));
8009
+ }
8010
+ }
8011
+ });
8012
+ } else if (_is(apply, 'object') && apply !== null && apply.t) {
8013
+ el.innerHTML = '';
8014
+ el.appendChild(bw.createDOM(apply));
8015
+ } else {
8016
+ el.textContent = String(apply);
7976
8017
  }
8018
+ }
7977
8019
 
7978
- return el;
7979
- };
8020
+ // Internal alias — kept for one release cycle (v2.0.26).
8021
+ // Will be removed in v2.0.27. Use bw.el() instead.
8022
+ bw._el = bw.el;
7980
8023
 
7981
8024
  /**
7982
8025
  * Register a DOM element in the node cache under one or more keys.
@@ -8040,6 +8083,12 @@
8040
8083
  */
8041
8084
  var _UUID_RE = /\bbw_uuid_[a-z0-9_]+\b/;
8042
8085
 
8086
+ /**
8087
+ * SVG namespace URI for createElementNS.
8088
+ * @private
8089
+ */
8090
+ var _SVG_NS = 'http://www.w3.org/2000/svg';
8091
+
8043
8092
  /**
8044
8093
  * Assign a UUID to a TACO object by appending a `bw_uuid_*` token to `taco.a.class`.
8045
8094
  *
@@ -8094,9 +8143,10 @@
8094
8143
  if (!tacoOrElement) return null;
8095
8144
 
8096
8145
  var classStr;
8097
- // DOM element: check className
8146
+ // DOM element: check className (SVG elements use getAttribute for string value)
8098
8147
  if (tacoOrElement.className !== undefined && tacoOrElement.tagName) {
8099
- classStr = tacoOrElement.className;
8148
+ classStr = typeof tacoOrElement.className === 'string'
8149
+ ? tacoOrElement.className : (tacoOrElement.getAttribute('class') || '');
8100
8150
  }
8101
8151
  // TACO object: check a.class
8102
8152
  else if (tacoOrElement.a && _is(tacoOrElement.a.class, 'string')) {
@@ -8365,7 +8415,7 @@
8365
8415
  var fnCounterBefore = bw._fnIDCounter;
8366
8416
 
8367
8417
  // Render body content
8368
- var bodyHTML = '';
8418
+ var bodyHTML;
8369
8419
  if (_is(body, 'string')) {
8370
8420
  bodyHTML = body;
8371
8421
  } else {
@@ -8536,9 +8586,11 @@
8536
8586
  }
8537
8587
 
8538
8588
  const { t: tag, a: attrs = {}, c: content, o: opts = {} } = taco;
8539
-
8540
- // Create element
8541
- const el = document.createElement(tag);
8589
+
8590
+ // SVG namespace: detect SVG context and thread through children.
8591
+ // {t:'svg'} starts SVG context; foreignObject children revert to HTML.
8592
+ var svgCtx = options._svgCtx || (tag === 'svg');
8593
+ var el = svgCtx ? document.createElementNS(_SVG_NS, tag) : document.createElement(tag);
8542
8594
 
8543
8595
  // Set attributes
8544
8596
  for (const [key, value] of Object.entries(attrs)) {
@@ -8549,9 +8601,11 @@
8549
8601
  Object.assign(el.style, value);
8550
8602
  } else if (key === 'class') {
8551
8603
  // Handle class as array or string
8604
+ // SVG elements use SVGAnimatedString for className, so use setAttribute
8552
8605
  const classStr = _isA(value) ? value.filter(Boolean).join(' ') : String(value);
8553
8606
  if (classStr) {
8554
- el.className = classStr;
8607
+ if (svgCtx) el.setAttribute('class', classStr);
8608
+ else el.className = classStr;
8555
8609
  }
8556
8610
  } else if (key.startsWith('on') && _is(value, 'function')) {
8557
8611
  // Event handlers
@@ -8572,11 +8626,17 @@
8572
8626
  // Add children, building _bw_refs for fast parent→child access.
8573
8627
  // Children with id attributes or bw_uuid_* classes get local refs on the parent,
8574
8628
  // so o.render functions can access them without any DOM lookup.
8629
+ // SVG: foreignObject children revert to HTML namespace; otherwise inherit.
8630
+ var childOpts = options;
8631
+ var childSvgCtx = svgCtx && tag !== 'foreignObject';
8632
+ if (childSvgCtx !== (options._svgCtx || false)) {
8633
+ childOpts = Object.assign({}, options, {_svgCtx: childSvgCtx || undefined});
8634
+ }
8575
8635
  if (content != null) {
8576
8636
  if (_isA(content)) {
8577
8637
  content.forEach(child => {
8578
8638
  if (child != null) {
8579
- var childEl = bw.createDOM(child, options);
8639
+ var childEl = bw.createDOM(child, childOpts);
8580
8640
  el.appendChild(childEl);
8581
8641
  // Build local refs for addressable children
8582
8642
  var childRefId = (child && child.a) ? (child.a.id || bw.getUUID(child)) : null;
@@ -8599,7 +8659,7 @@
8599
8659
  // Raw HTML content — inject via innerHTML
8600
8660
  el.innerHTML = content.v;
8601
8661
  } else if (_is(content, 'object') && content.t) {
8602
- var childEl = bw.createDOM(content, options);
8662
+ var childEl = bw.createDOM(content, childOpts);
8603
8663
  el.appendChild(childEl);
8604
8664
  var childRefId = content.a ? (content.a.id || bw.getUUID(content)) : null;
8605
8665
  if (childRefId) {
@@ -8625,13 +8685,21 @@
8625
8685
  }
8626
8686
 
8627
8687
  // Register UUID class in node cache (bw_uuid_* tokens in class string)
8628
- if (el.className) {
8629
- var uuidMatch = el.className.match(_UUID_RE);
8688
+ // SVG elements have SVGAnimatedString for className; use getAttribute instead
8689
+ var clsStr = svgCtx ? (el.getAttribute('class') || '') : el.className;
8690
+ if (clsStr) {
8691
+ var uuidMatch = clsStr.match(_UUID_RE);
8630
8692
  if (uuidMatch) {
8631
8693
  bw._nodeMap[uuidMatch[0]] = el;
8632
8694
  }
8633
8695
  }
8634
8696
 
8697
+ // Store component type metadata (e.g., 'card', 'tabs') for introspection.
8698
+ // BCCL factories set o.type; custom components can too.
8699
+ if (opts.type) {
8700
+ el._bw_type = opts.type;
8701
+ }
8702
+
8635
8703
  // Handle lifecycle hooks and state
8636
8704
  if (opts.mounted || opts.unmount || opts.render || opts.state) {
8637
8705
  // Ensure element has a UUID class for identity
@@ -8661,11 +8729,13 @@
8661
8729
 
8662
8730
  if (mountFn) {
8663
8731
  if (document.body.contains(el)) {
8664
- mountFn(el, el._bw_state || {});
8732
+ try { mountFn(el, el._bw_state || {}); }
8733
+ catch (e) { _cw('o.mounted error: ' + e.message); }
8665
8734
  } else {
8666
8735
  requestAnimationFrame(() => {
8667
8736
  if (document.body.contains(el)) {
8668
- mountFn(el, el._bw_state || {});
8737
+ try { mountFn(el, el._bw_state || {}); }
8738
+ catch (e) { _cw('o.mounted error: ' + e.message); }
8669
8739
  }
8670
8740
  });
8671
8741
  }
@@ -8674,7 +8744,8 @@
8674
8744
  // Store unmount callback keyed by UUID class
8675
8745
  if (opts.unmount) {
8676
8746
  bw._unmountCallbacks.set(uuid, () => {
8677
- opts.unmount(el, el._bw_state || {});
8747
+ try { opts.unmount(el, el._bw_state || {}); }
8748
+ catch (e) { _cw('o.unmount error: ' + e.message); }
8678
8749
  });
8679
8750
  }
8680
8751
  }
@@ -8693,24 +8764,25 @@
8693
8764
  }
8694
8765
 
8695
8766
  // Slot declarations: auto-generate setX/getX pairs
8767
+ // The target element is cached at creation time to avoid repeated
8768
+ // querySelector calls on every get/set invocation.
8696
8769
  if (opts.slots) {
8697
8770
  for (var sk in opts.slots) {
8698
8771
  if (_hop.call(opts.slots, sk)) {
8699
8772
  (function(name, selector) {
8773
+ var target = el.querySelector(selector);
8700
8774
  var cap = name.charAt(0).toUpperCase() + name.slice(1);
8701
8775
  el.bw['set' + cap] = function(value) {
8702
- var t = el.querySelector(selector);
8703
- if (!t) return;
8776
+ if (!target) return;
8704
8777
  if (value != null && typeof value === 'object' && value.t) {
8705
- t.innerHTML = '';
8706
- t.appendChild(bw.createDOM(value));
8778
+ target.innerHTML = '';
8779
+ target.appendChild(bw.createDOM(value));
8707
8780
  } else {
8708
- t.textContent = (value != null) ? String(value) : '';
8781
+ target.textContent = (value != null) ? String(value) : '';
8709
8782
  }
8710
8783
  };
8711
8784
  el.bw['get' + cap] = function() {
8712
- var t = el.querySelector(selector);
8713
- return t ? t.textContent : '';
8785
+ return target ? target.textContent : '';
8714
8786
  };
8715
8787
  })(sk, opts.slots[sk]);
8716
8788
  }
@@ -8751,7 +8823,7 @@
8751
8823
  }
8752
8824
 
8753
8825
  // Get target element (use cache-backed lookup)
8754
- const targetEl = bw._el(target);
8826
+ const targetEl = bw.el(target);
8755
8827
 
8756
8828
  if (!targetEl) {
8757
8829
  _ce('bw.DOM: Target element not found:', target);
@@ -8854,7 +8926,8 @@
8854
8926
  // Deregister UUID classes from node cache for non-lifecycle UUID elements
8855
8927
  var uuidEls = element.querySelectorAll('[class*="bw_uuid_"]');
8856
8928
  uuidEls.forEach(function(uel) {
8857
- var m = uel.className && uel.className.match(_UUID_RE);
8929
+ var uc = typeof uel.className === 'string' ? uel.className : (uel.getAttribute('class') || '');
8930
+ var m = uc && uc.match(_UUID_RE);
8858
8931
  if (m) delete bw._nodeMap[m[0]];
8859
8932
  });
8860
8933
 
@@ -8940,9 +9013,10 @@
8940
9013
  * bw.update(el); // re-renders, emits bw:statechange
8941
9014
  */
8942
9015
  bw.update = function(target) {
8943
- var el = bw._el(target);
9016
+ var el = bw.el(target);
8944
9017
  if (el && el._bw_render) {
8945
- el._bw_render(el, el._bw_state || {});
9018
+ try { el._bw_render(el, el._bw_state || {}); }
9019
+ catch (e) { _cw('o.render error: ' + e.message); }
8946
9020
  bw.emit(el, 'statechange', el._bw_state);
8947
9021
  }
8948
9022
  return el || null;
@@ -8969,7 +9043,7 @@
8969
9043
  * bw.patch('info', { t: 'em', c: 'new' }); // replace children with TACO
8970
9044
  */
8971
9045
  bw.patch = function(id, content, attr) {
8972
- var el = bw._el(id);
9046
+ var el = bw.el(id);
8973
9047
  if (!el) return null;
8974
9048
 
8975
9049
  if (attr) {
@@ -9041,7 +9115,7 @@
9041
9115
  * // Dispatches CustomEvent 'bw:statechange' on the element
9042
9116
  */
9043
9117
  bw.emit = function(target, eventName, detail) {
9044
- var el = bw._el(target);
9118
+ var el = bw.el(target);
9045
9119
  if (el) {
9046
9120
  el.dispatchEvent(new CustomEvent('bw:' + eventName, {
9047
9121
  bubbles: true,
@@ -9070,7 +9144,7 @@
9070
9144
  * });
9071
9145
  */
9072
9146
  bw.on = function(target, eventName, handler) {
9073
- var el = bw._el(target);
9147
+ var el = bw.el(target);
9074
9148
  if (el) {
9075
9149
  el.addEventListener('bw:' + eventName, function(e) {
9076
9150
  handler(e.detail, e);
@@ -9097,23 +9171,38 @@
9097
9171
  *
9098
9172
  * @param {string} topic - Topic name (plain string, no prefix)
9099
9173
  * @param {*} [detail] - Data to pass to subscribers
9100
- * @returns {number} Count of successfully called subscribers
9174
+ * @returns {number} Count of successfully called subscribers (including wildcard matches)
9101
9175
  * @category Pub/Sub
9102
9176
  * @see bw.sub
9103
9177
  * @example
9104
9178
  * bw.pub('score:updated', { player: 'X', score: 10 });
9179
+ * // Wildcard subscribers matching 'score:*' will also fire
9105
9180
  */
9106
9181
  bw.pub = function(topic, detail) {
9107
- var subs = bw._topics[topic];
9108
- if (!subs || subs.length === 0) return 0;
9109
- var snapshot = subs.slice(); // safe against unsub during iteration
9110
9182
  var called = 0;
9111
- for (var i = 0; i < snapshot.length; i++) {
9112
- try {
9113
- snapshot[i].handler(detail);
9114
- called++;
9115
- } catch (err) {
9116
- _cw('bw.pub: subscriber error on topic "' + topic + '":', err);
9183
+ // Exact-match subscribers
9184
+ var subs = bw._topics[topic];
9185
+ if (subs && subs.length > 0) {
9186
+ var snapshot = subs.slice();
9187
+ for (var i = 0; i < snapshot.length; i++) {
9188
+ try { snapshot[i].handler(detail, topic); called++; }
9189
+ catch (err) { _cw('bw.pub: subscriber error on topic "' + topic + '":', err); }
9190
+ }
9191
+ }
9192
+ // Wildcard subscribers -- patterns ending with '*'
9193
+ var keys = Object.keys(bw._topics);
9194
+ for (var k = 0; k < keys.length; k++) {
9195
+ var pat = keys[k];
9196
+ if (pat.charAt(pat.length - 1) !== '*') continue;
9197
+ var prefix = pat.slice(0, -1); // strip trailing '*'
9198
+ if (topic.length >= prefix.length && topic.substring(0, prefix.length) === prefix && topic !== pat) {
9199
+ var wsubs = bw._topics[pat];
9200
+ if (!wsubs) continue;
9201
+ var wsnap = wsubs.slice();
9202
+ for (var w = 0; w < wsnap.length; w++) {
9203
+ try { wsnap[w].handler(detail, topic); called++; }
9204
+ catch (err) { _cw('bw.pub: wildcard subscriber error on "' + pat + '" for topic "' + topic + '":', err); }
9205
+ }
9117
9206
  }
9118
9207
  }
9119
9208
  return called;
@@ -9122,12 +9211,17 @@
9122
9211
  /**
9123
9212
  * Subscribe to a topic. Returns an unsub() function.
9124
9213
  *
9125
- * Optional third argument ties the subscription to a DOM element's lifecycle —
9214
+ * Supports wildcard patterns: a topic ending in `*` matches any published
9215
+ * topic that starts with the prefix before the `*`. For example,
9216
+ * `'agui:*'` matches `'agui:ready'`, `'agui:error'`, etc. The handler
9217
+ * receives `(detail, topic)` so it can distinguish which topic fired.
9218
+ *
9219
+ * Optional third argument ties the subscription to a DOM element's lifecycle --
9126
9220
  * when `bw.cleanup()` is called on that element, the subscription is automatically
9127
9221
  * removed, preventing memory leaks.
9128
9222
  *
9129
- * @param {string} topic - Topic name
9130
- * @param {Function} handler - Called with (detail) on each publish
9223
+ * @param {string} topic - Topic name, or wildcard pattern ending in '*'
9224
+ * @param {Function} handler - Called with (detail, topic) on each publish
9131
9225
  * @param {Element} [el] - Optional DOM element to tie lifecycle to
9132
9226
  * @returns {Function} Call to unsubscribe
9133
9227
  * @category Pub/Sub
@@ -9138,6 +9232,11 @@
9138
9232
  * console.log(detail.player, 'scored', detail.score);
9139
9233
  * });
9140
9234
  * // Later: unsub() to stop listening
9235
+ *
9236
+ * // Wildcard: listen to all 'agui:' topics
9237
+ * bw.sub('agui:*', function(detail, topic) {
9238
+ * console.log('Got', topic, detail);
9239
+ * });
9141
9240
  */
9142
9241
  bw.sub = function(topic, handler, el) {
9143
9242
  var id = ++bw._subIdCounter;
@@ -9189,6 +9288,37 @@
9189
9288
  return removed;
9190
9289
  };
9191
9290
 
9291
+ /**
9292
+ * Subscribe to a topic for a single event only. The subscription is
9293
+ * automatically removed after the first publish. Equivalent to manually
9294
+ * calling unsub() inside a bw.sub() handler, but avoids the common bug
9295
+ * of forgetting to unsubscribe.
9296
+ *
9297
+ * @param {string} topic - Topic name
9298
+ * @param {Function} handler - Called once with (detail) on the next publish
9299
+ * @param {Element} [el] - Optional DOM element to tie lifecycle to
9300
+ * @returns {Function} Call to cancel the subscription before it fires
9301
+ * @category Pub/Sub
9302
+ * @see bw.sub
9303
+ * @see bw.pub
9304
+ * @example
9305
+ * bw.once('data:loaded', function(detail) {
9306
+ * console.log('Received:', detail);
9307
+ * // No need to unsubscribe -- already done automatically
9308
+ * });
9309
+ *
9310
+ * // Cancel before it fires:
9311
+ * var cancel = bw.once('timeout', handler);
9312
+ * cancel(); // handler will never be called
9313
+ */
9314
+ bw.once = function(topic, handler, el) {
9315
+ var unsub = bw.sub(topic, function(detail) {
9316
+ unsub();
9317
+ handler(detail);
9318
+ }, el);
9319
+ return unsub;
9320
+ };
9321
+
9192
9322
  // ===================================================================================
9193
9323
  // Function Registry (revived from v1 for string dispatch contexts)
9194
9324
  // ===================================================================================
@@ -9429,7 +9559,7 @@
9429
9559
  * };
9430
9560
  */
9431
9561
  bw.message = function(target, action, data) {
9432
- var el = bw._el(target);
9562
+ var el = bw.el(target);
9433
9563
  if (!el) el = bw.$('.' + target)[0];
9434
9564
  if (!el || !el.bw || typeof el.bw[action] !== 'function') {
9435
9565
  _cw('bw.message: no handle method "' + action + '" on ' + target);
@@ -9439,6 +9569,207 @@
9439
9569
  return true;
9440
9570
  };
9441
9571
 
9572
+ /**
9573
+ * Collect form data from all input, select, and textarea elements within a
9574
+ * container. Each element's `name` attribute (or `id` if no name) becomes a
9575
+ * key in the returned object. This provides a lightweight alternative to the
9576
+ * browser FormData API that returns a plain object suitable for JSON
9577
+ * serialization or bw.pub().
9578
+ *
9579
+ * Handles all standard HTML form controls:
9580
+ * - text/number/email/etc inputs: string value
9581
+ * - checkboxes: boolean (true/false)
9582
+ * - radio buttons: string value of the checked radio (unchecked groups omitted)
9583
+ * - multi-select: array of selected option values
9584
+ * - textarea: string value
9585
+ *
9586
+ * Elements without both `name` and `id` attributes are silently skipped.
9587
+ *
9588
+ * @param {string|Element} target - CSS selector, UUID string, or DOM element
9589
+ * @returns {Object} Plain object mapping field names to values
9590
+ * @category Component
9591
+ * @see bw.makeForm
9592
+ * @see bw.makeInput
9593
+ * @example
9594
+ * // Given a form with name="email" input and name="agree" checkbox:
9595
+ * var data = bw.formData('#signup-form');
9596
+ * // => { email: 'user@example.com', agree: true }
9597
+ *
9598
+ * // Collect and publish in one step:
9599
+ * bw.pub('form:submit', bw.formData('#my-form'));
9600
+ *
9601
+ * // Works with any container, not just <form>:
9602
+ * bw.pub('settings:changed', bw.formData('.settings-panel'));
9603
+ */
9604
+ bw.formData = function(target) {
9605
+ var el = bw.el(target);
9606
+ if (!el) return {};
9607
+ var result = {};
9608
+ var inputs = el.querySelectorAll('input, select, textarea');
9609
+ for (var i = 0; i < inputs.length; i++) {
9610
+ var inp = inputs[i];
9611
+ var key = inp.name || inp.id;
9612
+ if (!key) continue;
9613
+ if (inp.type === 'checkbox') {
9614
+ result[key] = inp.checked;
9615
+ } else if (inp.type === 'radio') {
9616
+ if (inp.checked) result[key] = inp.value;
9617
+ } else if (inp.tagName === 'SELECT' && inp.multiple) {
9618
+ result[key] = [];
9619
+ for (var j = 0; j < inp.options.length; j++) {
9620
+ if (inp.options[j].selected) result[key].push(inp.options[j].value);
9621
+ }
9622
+ } else {
9623
+ result[key] = inp.value;
9624
+ }
9625
+ }
9626
+ return result;
9627
+ };
9628
+
9629
+ // ===================================================================================
9630
+ // bw.jsonPatch() — RFC 6902 JSON Patch on plain objects
9631
+ // ===================================================================================
9632
+
9633
+ /**
9634
+ * Apply RFC 6902 JSON Patch operations to a plain object.
9635
+ *
9636
+ * Supported operations: add, remove, replace, move, copy, test.
9637
+ * Paths use JSON Pointer (RFC 6901) notation: `/foo/bar/0`.
9638
+ * Mutates the target object in place and returns it.
9639
+ *
9640
+ * @param {Object} obj - Target object to patch
9641
+ * @param {Array<Object>} ops - Array of patch operations
9642
+ * @param {string} ops[].op - Operation: 'add', 'remove', 'replace', 'move', 'copy', 'test'
9643
+ * @param {string} ops[].path - JSON Pointer path (e.g. '/a/b/0')
9644
+ * @param {*} [ops[].value] - Value for add/replace/test
9645
+ * @param {string} [ops[].from] - Source path for move/copy
9646
+ * @returns {Object} The patched object (same reference)
9647
+ * @throws {Error} On invalid op, missing path, test failure, or path not found for remove
9648
+ * @category Data Utilities
9649
+ * @see bw.patch
9650
+ * @example
9651
+ * var obj = { a: 1, b: { c: 2 } };
9652
+ * bw.jsonPatch(obj, [
9653
+ * { op: 'replace', path: '/a', value: 10 },
9654
+ * { op: 'add', path: '/b/d', value: 3 },
9655
+ * { op: 'remove', path: '/b/c' }
9656
+ * ]);
9657
+ * // obj => { a: 10, b: { d: 3 } }
9658
+ */
9659
+ bw.jsonPatch = function(obj, ops) {
9660
+ if (!_isA(ops)) return obj;
9661
+
9662
+ // Parse JSON Pointer path to array of keys
9663
+ function parsePath(path) {
9664
+ if (path === '') return [];
9665
+ if (path.charAt(0) !== '/') throw new Error('Invalid JSON Pointer: ' + path);
9666
+ return path.slice(1).split('/').map(function(s) {
9667
+ return s.replace(/~1/g, '/').replace(/~0/g, '~');
9668
+ });
9669
+ }
9670
+
9671
+ // Walk to parent of final key; return { parent, key }
9672
+ function resolve(root, keys) {
9673
+ var parent = root;
9674
+ for (var i = 0; i < keys.length - 1; i++) {
9675
+ var k = _isA(parent) ? parseInt(keys[i], 10) : keys[i];
9676
+ if (parent[k] === undefined) throw new Error('Path not found: /' + keys.slice(0, i + 1).join('/'));
9677
+ parent = parent[k];
9678
+ }
9679
+ return { parent: parent, key: _isA(parent) ? parseInt(keys[keys.length - 1], 10) : keys[keys.length - 1] };
9680
+ }
9681
+
9682
+ // Get value at path
9683
+ function getVal(root, keys) {
9684
+ var cur = root;
9685
+ for (var i = 0; i < keys.length; i++) {
9686
+ var k = _isA(cur) ? parseInt(keys[i], 10) : keys[i];
9687
+ if (cur[k] === undefined) throw new Error('Path not found: /' + keys.slice(0, i + 1).join('/'));
9688
+ cur = cur[k];
9689
+ }
9690
+ return cur;
9691
+ }
9692
+
9693
+ for (var i = 0; i < ops.length; i++) {
9694
+ var op = ops[i];
9695
+ if (!op.op || !_is(op.path, 'string')) throw new Error('Invalid patch operation at index ' + i);
9696
+ var keys = parsePath(op.path);
9697
+
9698
+ var r, val, fromKeys, fr, tr, cr;
9699
+ switch (op.op) {
9700
+ case 'add': {
9701
+ if (keys.length === 0) throw new Error('Cannot add to root');
9702
+ r = resolve(obj, keys);
9703
+ if (_isA(r.parent) && r.key <= r.parent.length) {
9704
+ r.parent.splice(r.key, 0, op.value);
9705
+ } else {
9706
+ r.parent[r.key] = op.value;
9707
+ }
9708
+ break;
9709
+ }
9710
+ case 'remove': {
9711
+ if (keys.length === 0) throw new Error('Cannot remove root');
9712
+ r = resolve(obj, keys);
9713
+ if (_isA(r.parent)) {
9714
+ if (r.key >= r.parent.length) throw new Error('Index out of bounds: ' + r.key);
9715
+ r.parent.splice(r.key, 1);
9716
+ } else {
9717
+ if (!(r.key in r.parent)) throw new Error('Path not found: ' + op.path);
9718
+ delete r.parent[r.key];
9719
+ }
9720
+ break;
9721
+ }
9722
+ case 'replace': {
9723
+ if (keys.length === 0) throw new Error('Cannot replace root');
9724
+ r = resolve(obj, keys);
9725
+ if (_isA(r.parent)) {
9726
+ if (r.key >= r.parent.length) throw new Error('Index out of bounds: ' + r.key);
9727
+ } else {
9728
+ if (!(r.key in r.parent)) throw new Error('Path not found: ' + op.path);
9729
+ }
9730
+ r.parent[r.key] = op.value;
9731
+ break;
9732
+ }
9733
+ case 'move': {
9734
+ if (!_is(op.from, 'string')) throw new Error('move requires "from"');
9735
+ fromKeys = parsePath(op.from);
9736
+ val = getVal(obj, fromKeys);
9737
+ fr = resolve(obj, fromKeys);
9738
+ if (_isA(fr.parent)) { fr.parent.splice(fr.key, 1); }
9739
+ else { delete fr.parent[fr.key]; }
9740
+ tr = resolve(obj, keys);
9741
+ if (_isA(tr.parent) && tr.key <= tr.parent.length) {
9742
+ tr.parent.splice(tr.key, 0, val);
9743
+ } else {
9744
+ tr.parent[tr.key] = val;
9745
+ }
9746
+ break;
9747
+ }
9748
+ case 'copy': {
9749
+ if (!_is(op.from, 'string')) throw new Error('copy requires "from"');
9750
+ val = getVal(obj, parsePath(op.from));
9751
+ cr = resolve(obj, keys);
9752
+ if (_isA(cr.parent) && cr.key <= cr.parent.length) {
9753
+ cr.parent.splice(cr.key, 0, val);
9754
+ } else {
9755
+ cr.parent[cr.key] = val;
9756
+ }
9757
+ break;
9758
+ }
9759
+ case 'test': {
9760
+ var actual = getVal(obj, keys);
9761
+ if (JSON.stringify(actual) !== JSON.stringify(op.value)) {
9762
+ throw new Error('Test failed: ' + op.path + ' expected ' + JSON.stringify(op.value) + ' got ' + JSON.stringify(actual));
9763
+ }
9764
+ break;
9765
+ }
9766
+ default:
9767
+ throw new Error('Unknown op: ' + op.op);
9768
+ }
9769
+ }
9770
+ return obj;
9771
+ };
9772
+
9442
9773
  // ===================================================================================
9443
9774
  // bw.apply() / bw.parseJSONFlex() — Server-driven UI protocol
9444
9775
  // ===================================================================================
@@ -9580,7 +9911,7 @@
9580
9911
  var target = msg.target;
9581
9912
 
9582
9913
  if (type === 'replace') {
9583
- var el = bw._el(target);
9914
+ var el = bw.el(target);
9584
9915
  if (!el) return false;
9585
9916
  bw.DOM(el, msg.node);
9586
9917
  return true;
@@ -9590,14 +9921,14 @@
9590
9921
  return patched !== null;
9591
9922
 
9592
9923
  } else if (type === 'append') {
9593
- var parent = bw._el(target);
9924
+ var parent = bw.el(target);
9594
9925
  if (!parent) return false;
9595
9926
  var child = bw.createDOM(msg.node);
9596
9927
  parent.appendChild(child);
9597
9928
  return true;
9598
9929
 
9599
9930
  } else if (type === 'remove') {
9600
- var toRemove = bw._el(target);
9931
+ var toRemove = bw.el(target);
9601
9932
  if (!toRemove) return false;
9602
9933
  if (_is(bw.cleanup, 'function')) bw.cleanup(toRemove);
9603
9934
  toRemove.remove();
@@ -9657,30 +9988,98 @@
9657
9988
 
9658
9989
 
9659
9990
  // ===================================================================================
9660
- // bw.inspect() — Debug utility
9991
+ // bw.inspect() — DOM introspection with bitwrench metadata
9661
9992
  // ===================================================================================
9662
9993
 
9663
9994
  /**
9664
- * Inspect a DOM element's bitwrench state, handle methods, and metadata.
9665
- * Works with DOM elements or CSS selectors.
9666
- *
9667
- * @param {string|Element} target - Selector or DOM element
9668
- * @returns {Element|null} The element, or null if not found
9995
+ * Inspect a DOM element and its subtree, returning a plain-object
9996
+ * representation with bitwrench metadata at each node. Useful for debugging,
9997
+ * devtools, MCP/AG-UI tool discovery, and automated testing.
9998
+ *
9999
+ * Each node in the returned tree includes:
10000
+ * - `tag` -- lowercase tag name (or '#text' for text nodes)
10001
+ * - `id` -- element id (if set)
10002
+ * - `uuid` -- bitwrench UUID class (if lifecycle-managed)
10003
+ * - `type` -- component type from o.type (if set, e.g. 'card', 'tabs')
10004
+ * - `classes` -- first 5 CSS classes (string, space-separated)
10005
+ * - `handles` -- array of el.bw method names (if any)
10006
+ * - `state` -- copy of _bw_state (if any)
10007
+ * - `hasRender` -- true if _bw_render is set
10008
+ * - `hasSubs` -- true if element has pub/sub subscriptions
10009
+ * - `refs` -- copy of _bw_refs keys (if any)
10010
+ * - `children` -- array of child node trees (up to depth limit, max 50 per level)
10011
+ *
10012
+ * @param {string|Element} target - CSS selector, UUID, or DOM element
10013
+ * @param {number} [depth=3] - Maximum recursion depth (0 = target only, no children)
10014
+ * @returns {Object|null} Plain object tree, or null if element not found
9669
10015
  * @category Component
9670
10016
  * @example
9671
- * bw.inspect('#my-carousel');
9672
- * bw.inspect($0);
9673
- */
9674
- bw.inspect = function(target) {
9675
- var el = _is(target, 'string') ? bw.$(target)[0] : target;
9676
- if (!el) { _cw('bw.inspect: element not found'); return null; }
9677
- console.group('Element: ' + (bw.getUUID(el) || el.id || el.tagName));
9678
- _cl('State:', el._bw_state || '(none)');
9679
- _cl('Handle:', el.bw ? _keys(el.bw) : '(none)');
9680
- _cl('Classes:', el.className);
9681
- _cl('Refs:', el._bw_refs || '(none)');
9682
- console.groupEnd();
9683
- return el;
10017
+ * // Get full tree from #app, 3 levels deep (default):
10018
+ * var info = bw.inspect('#app');
10019
+ *
10020
+ * // Shallow inspection (just the element, no children):
10021
+ * var info = bw.inspect('#my-carousel', 0);
10022
+ * console.log(info.handles); // ['next', 'prev', 'goToSlide']
10023
+ * console.log(info.type); // 'carousel'
10024
+ *
10025
+ * // Deep inspection for debugging:
10026
+ * console.log(JSON.stringify(bw.inspect('#app', 5), null, 2));
10027
+ */
10028
+ bw.inspect = function(target, depth) {
10029
+ var el = bw.el(target);
10030
+ if (!el && _is(target, 'string')) el = bw.$(target)[0];
10031
+ if (!el) return null;
10032
+ if (depth === undefined || depth === null) depth = 3;
10033
+
10034
+ function walk(node, d) {
10035
+ if (!node) return null;
10036
+ // Skip non-element nodes (text, comment, etc.)
10037
+ if (node.nodeType !== 1) return null;
10038
+
10039
+ var info = { tag: node.tagName ? node.tagName.toLowerCase() : '#text' };
10040
+
10041
+ // Identity
10042
+ if (node.id) info.id = node.id;
10043
+ var uuid = bw.getUUID(node);
10044
+ if (uuid) info.uuid = uuid;
10045
+ if (node._bw_type) info.type = node._bw_type;
10046
+
10047
+ // CSS classes (first 5 for readability)
10048
+ if (node.className && typeof node.className === 'string') {
10049
+ info.classes = node.className.split(' ').slice(0, 5).join(' ');
10050
+ }
10051
+
10052
+ // Bitwrench handle methods
10053
+ if (node.bw) {
10054
+ var handles = _keys(node.bw);
10055
+ if (handles.length > 0) info.handles = handles;
10056
+ }
10057
+
10058
+ // State
10059
+ if (node._bw_state) info.state = node._bw_state;
10060
+ if (node._bw_render) info.hasRender = true;
10061
+ if (node._bw_subs && node._bw_subs.length > 0) info.hasSubs = true;
10062
+
10063
+ // Refs
10064
+ if (node._bw_refs) info.refs = _keys(node._bw_refs);
10065
+
10066
+ // Children (recurse up to depth limit, max 50 children per level)
10067
+ if (d < depth && node.children && node.children.length > 0) {
10068
+ info.children = [];
10069
+ var max = Math.min(node.children.length, 50);
10070
+ for (var i = 0; i < max; i++) {
10071
+ var child = walk(node.children[i], d + 1);
10072
+ if (child) info.children.push(child);
10073
+ }
10074
+ if (node.children.length > 50) {
10075
+ info.children.push({ tag: '...', count: node.children.length - 50 });
10076
+ }
10077
+ }
10078
+
10079
+ return info;
10080
+ }
10081
+
10082
+ return walk(el, 0);
9684
10083
  };
9685
10084
 
9686
10085
  bw.compile = function() { throw new Error('bw.compile() removed in v2.0.19. Use o.handle/o.slots on TACO options instead.'); };
@@ -9902,37 +10301,49 @@
9902
10301
  * so you can use `.map()`, `.filter()`, etc. directly. Accepts CSS selectors,
9903
10302
  * single elements, NodeLists, or arrays.
9904
10303
  *
10304
+ * With an optional second argument, applies content or a function to
10305
+ * every matched element (same apply rules as `bw.el()`):
10306
+ * - string/number: sets `el.textContent`
10307
+ * - function: calls `apply(el)` for each element
10308
+ * - TACO object: clears children, mounts TACO via `bw.createDOM()`
10309
+ * - array: clears children, appends each item
10310
+ *
9905
10311
  * @param {string|Element|Array} selector - CSS selector, element, or array
10312
+ * @param {string|number|Function|Object|Array} [apply] - Content or function to apply
9906
10313
  * @returns {Array} Array of DOM elements
9907
10314
  * @category DOM Selection
10315
+ * @see bw.el
9908
10316
  * @example
9909
- * bw.$('.card') // => [div.card, div.card, ...]
9910
- * bw.$(myElement) // => [myElement]
9911
- * bw.$('.card').map(el => el.textContent)
10317
+ * bw.$('.card') // => [div.card, div.card, ...]
10318
+ * bw.$('.status', 'Online') // set text on all .status elements
10319
+ * bw.$('.card', function(el) { // apply function to each
10320
+ * el.style.opacity = '0.5';
10321
+ * })
9912
10322
  */
9913
10323
  if (bw._isBrowser) {
9914
- bw.$ = function(selector) {
9915
- if (!selector) return [];
9916
-
9917
- // Already an array
9918
- if (_isA(selector)) return selector;
9919
-
9920
- // Single element
9921
- if (selector.nodeType) return [selector];
9922
-
9923
- // NodeList or HTMLCollection
9924
- if (selector.length !== undefined && !_is(selector, 'string')) {
9925
- return Array.from(selector);
10324
+ bw.$ = function(selector, apply) {
10325
+ var els;
10326
+ if (!selector) {
10327
+ els = [];
10328
+ } else if (_isA(selector)) {
10329
+ els = selector;
10330
+ } else if (selector.nodeType) {
10331
+ els = [selector];
10332
+ } else if (selector.length !== undefined && !_is(selector, 'string')) {
10333
+ els = Array.from(selector);
10334
+ } else if (_is(selector, 'string')) {
10335
+ els = Array.from(document.querySelectorAll(selector));
10336
+ } else {
10337
+ els = [];
9926
10338
  }
9927
-
9928
- // CSS selector string
9929
- if (_is(selector, 'string')) {
9930
- return Array.from(document.querySelectorAll(selector));
10339
+
10340
+ if (apply !== undefined) {
10341
+ for (var i = 0; i < els.length; i++) _applyTo(els[i], apply);
9931
10342
  }
9932
-
9933
- return [];
10343
+
10344
+ return els;
9934
10345
  };
9935
-
10346
+
9936
10347
  // Convenience single element selector
9937
10348
  bw.$.one = function(selector) {
9938
10349
  return bw.$(selector)[0] || null;
@@ -10145,42 +10556,48 @@
10145
10556
  };
10146
10557
 
10147
10558
  /**
10148
- * Toggle between primary and alternate palettes.
10559
+ * Toggle between primary and alternate theme palettes.
10149
10560
  *
10150
- * Adds/removes the `bw_theme_alt` class on the scoping element.
10561
+ * Adds/removes the `bw_theme_alt` class on the scoping element(s).
10151
10562
  * Without a scope, toggles on `<html>` (global).
10152
- * With a scope, toggles on the first matching element.
10563
+ * With a scope, toggles on ALL matching elements.
10153
10564
  *
10154
- * @param {string} [scope] - Scope selector (e.g. '#my-dashboard'). Omit for global.
10155
- * @returns {string} Active mode after toggle: 'primary' or 'alternate'
10565
+ * @param {string|Element} [scope] - Selector or element. Omit for global.
10566
+ * @returns {string} Active mode after toggle: 'primary' or 'alternate' (based on first element)
10156
10567
  * @category CSS & Styling
10157
10568
  * @see bw.applyStyles
10158
10569
  * @see bw.clearStyles
10159
10570
  * @example
10160
- * bw.toggleStyles(); // global toggle on <html>
10161
- * bw.toggleStyles('#my-dashboard'); // scoped toggle
10571
+ * bw.toggleThemeMode(); // global toggle on <html>
10572
+ * bw.toggleThemeMode('#my-dashboard'); // scoped toggle
10573
+ * bw.toggleThemeMode('.panel'); // toggle on ALL .panel elements
10162
10574
  */
10163
- bw.toggleStyles = function(scope) {
10575
+ bw.toggleThemeMode = function(scope) {
10164
10576
  if (!bw._isBrowser) return 'primary';
10165
- var target;
10577
+ var els;
10166
10578
  if (scope) {
10167
- var els = bw.$(scope);
10168
- target = els[0];
10579
+ els = bw.$(scope);
10169
10580
  } else {
10170
- target = document.documentElement;
10581
+ els = [document.documentElement];
10171
10582
  }
10172
- if (!target) return 'primary';
10583
+ if (!els.length) return 'primary';
10173
10584
 
10174
- var hasAlt = target.classList.contains('bw_theme_alt');
10175
- if (hasAlt) {
10176
- target.classList.remove('bw_theme_alt');
10177
- return 'primary';
10178
- } else {
10179
- target.classList.add('bw_theme_alt');
10180
- return 'alternate';
10585
+ var mode;
10586
+ for (var i = 0; i < els.length; i++) {
10587
+ var hasAlt = els[i].classList.contains('bw_theme_alt');
10588
+ if (hasAlt) {
10589
+ els[i].classList.remove('bw_theme_alt');
10590
+ } else {
10591
+ els[i].classList.add('bw_theme_alt');
10592
+ }
10593
+ if (i === 0) mode = hasAlt ? 'primary' : 'alternate';
10181
10594
  }
10595
+ return mode;
10182
10596
  };
10183
10597
 
10598
+ // Alias — kept for one release cycle. Use bw.toggleThemeMode() instead.
10599
+ bw.toggleStyles = bw.toggleThemeMode;
10600
+
10184
10601
  /**
10185
10602
  * Remove injected styles for a given scope.
10186
10603
  *
@@ -11220,6 +11637,57 @@
11220
11637
  }
11221
11638
  });
11222
11639
 
11640
+ /**
11641
+ * Query the BCCL component registry. Returns metadata about registered
11642
+ * component types -- their names and factory function names. Useful for
11643
+ * tooling, introspection, documentation generators, and auto-complete
11644
+ * systems (including MCP/AG-UI tool discovery).
11645
+ *
11646
+ * With no arguments, returns an array of all registered component types.
11647
+ * With a type name, returns metadata for that single type (or null if
11648
+ * the type is not registered).
11649
+ *
11650
+ * @param {string} [type] - Optional component type name to look up
11651
+ * @returns {Array<Object>|Object|null} Array of {type, factory} objects,
11652
+ * a single {type, factory} object, or null if the type is not found
11653
+ * @category Component
11654
+ * @see bw.make
11655
+ * @see bw.BCCL
11656
+ * @example
11657
+ * // List all available component types:
11658
+ * bw.catalog();
11659
+ * // => [{ type: 'card', factory: 'makeCard' },
11660
+ * // { type: 'button', factory: 'makeButton' }, ...]
11661
+ *
11662
+ * // Look up a specific type:
11663
+ * bw.catalog('accordion');
11664
+ * // => { type: 'accordion', factory: 'makeAccordion' }
11665
+ *
11666
+ * // Check if a type exists:
11667
+ * if (bw.catalog('chart')) { ... }
11668
+ *
11669
+ * // Get just the type names:
11670
+ * bw.catalog().map(function(c) { return c.type; });
11671
+ * // => ['card', 'button', 'container', 'row', ...]
11672
+ */
11673
+ bw.catalog = function(type) {
11674
+ if (type) {
11675
+ var def = bw.BCCL[type];
11676
+ if (!def) return null;
11677
+ return {
11678
+ type: type,
11679
+ factory: def.make.name || ('make' + type.charAt(0).toUpperCase() + type.slice(1))
11680
+ };
11681
+ }
11682
+ return Object.keys(bw.BCCL).map(function(k) {
11683
+ var def = bw.BCCL[k];
11684
+ return {
11685
+ type: k,
11686
+ factory: def.make.name || ('make' + k.charAt(0).toUpperCase() + k.slice(1))
11687
+ };
11688
+ });
11689
+ };
11690
+
11223
11691
  // Also attach to global in browsers
11224
11692
  if (bw._isBrowser && typeof window !== 'undefined') {
11225
11693
  window.bw = bw;