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