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