bitwrench 2.0.25 → 2.0.31

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 +92 -92
  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-lean v2.0.25 | BSD-2-Clause | https://deftio.github.com/bitwrench/pages */
1
+ /*! bitwrench-lean v2.0.31 | 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.31',
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:56:29.791Z'
16
16
  };
17
17
 
18
18
  /**
@@ -3948,7 +3948,6 @@ var _is = function(x, t) { var r = _to(x); return r === t || r.toLowerCase() =
3948
3948
  // Console aliases use thin wrappers (not direct references) so that test
3949
3949
  // code can monkey-patch console.warn/log/error and the patches take effect.
3950
3950
  var _cw = function() { console.warn.apply(console, arguments); };
3951
- var _cl = function() { console.log.apply(console, arguments); };
3952
3951
  var _ce = function() { console.error.apply(console, arguments); };
3953
3952
 
3954
3953
  /**
@@ -4085,61 +4084,105 @@ bw.uuid = function(prefix) {
4085
4084
  };
4086
4085
 
4087
4086
  /**
4088
- * Look up a DOM element by ID string, using the node cache for O(1) access.
4089
- *
4090
- * Resolution order:
4091
- * 1. Check `bw._nodeMap[id]` if found and still attached (parentNode !== null), return it
4092
- * 2. If cached ref is detached (parentNode === null), remove stale entry
4093
- * 3. Fall back to `document.getElementById(id)` then `document.querySelector(...)`
4094
- * 4. Try class-based lookup for bw_uuid_* tokens (UUID addressing)
4095
- * 5. Cache the result for next time
4096
- *
4097
- * Accepts a DOM element directly (pass-through) or a string identifier.
4098
- * String identifiers are tried as: direct map key, getElementById,
4099
- * querySelector (for CSS selectors starting with . or #), and
4100
- * bw_uuid_* class selector.
4101
- *
4102
- * @param {string|Element} id - Element ID, CSS selector, bw_uuid_* class, or DOM element
4087
+ * Look up a single DOM element by ID, CSS selector, UUID, or element ref.
4088
+ * Optionally apply content or a function to the resolved element.
4089
+ *
4090
+ * Resolution order for string targets:
4091
+ * 1. Check `bw._nodeMap[id]` cache (O(1), stale entries auto-pruned)
4092
+ * 2. `document.getElementById(id)`
4093
+ * 3. `document.querySelector(id)` for selectors starting with # or .
4094
+ * 4. Class-based lookup for `bw_uuid_*` tokens
4095
+ *
4096
+ * With one argument, returns the element (or null). With two arguments,
4097
+ * applies the second argument to the element and returns the element:
4098
+ * - string/number: sets `el.textContent`
4099
+ * - function: calls `apply(el)`, returns el
4100
+ * - TACO object: clears children, mounts TACO via `bw.createDOM()`
4101
+ * - array: clears children, appends each item (string -> text node, TACO -> element)
4102
+ *
4103
+ * @param {string|Element} target - Element ref, ID, CSS selector, or bw_uuid_* class
4104
+ * @param {string|number|Function|Object|Array} [apply] - Content or function to apply
4103
4105
  * @returns {Element|null} The DOM element, or null if not found
4104
- * @category Internal
4106
+ * @category DOM Selection
4107
+ * @see bw.$
4108
+ * @see bw.patch
4109
+ * @example
4110
+ * bw.el('#title') // lookup
4111
+ * bw.el('#title', 'Hello') // set text content
4112
+ * bw.el('#app', { t: 'h1', c: 'Hi' }) // mount TACO
4113
+ * bw.el('.card', function(el) { // apply function
4114
+ * el.style.opacity = '0.5';
4115
+ * })
4105
4116
  */
4106
- bw._el = function(id) {
4107
- // Pass-through for DOM elements
4108
- if (!_is(id, 'string')) return id || null;
4109
- if (!id) return null;
4110
- if (!bw._isBrowser) return null;
4111
-
4112
- // 1. Check cache
4113
- var cached = bw._nodeMap[id];
4114
- if (cached) {
4115
- // Verify not detached (parentNode check is IE11-safe)
4116
- if (cached.parentNode !== null) {
4117
- return cached;
4117
+ bw.el = function(target, apply) {
4118
+ // Resolve target to element
4119
+ var el;
4120
+ if (!_is(target, 'string')) {
4121
+ el = target || null;
4122
+ } else if (!target || !bw._isBrowser) {
4123
+ el = null;
4124
+ } else {
4125
+ // 1. Check cache
4126
+ var cached = bw._nodeMap[target];
4127
+ if (cached) {
4128
+ if (cached.parentNode !== null) {
4129
+ el = cached;
4130
+ } else {
4131
+ delete bw._nodeMap[target];
4132
+ }
4133
+ }
4134
+ if (!el) {
4135
+ // 2. getElementById
4136
+ el = document.getElementById(target);
4137
+ // 3. querySelector for CSS selectors
4138
+ if (!el && (target.charAt(0) === '#' || target.charAt(0) === '.')) {
4139
+ el = document.querySelector(target);
4140
+ }
4141
+ // 4. bw_uuid_* class lookup
4142
+ if (!el && target.indexOf('bw_uuid_') === 0) {
4143
+ el = document.querySelector('.' + target);
4144
+ }
4145
+ // 5. Cache result
4146
+ if (el) bw._nodeMap[target] = el;
4118
4147
  }
4119
- // Stale — remove and fall through
4120
- delete bw._nodeMap[id];
4121
4148
  }
4122
4149
 
4123
- // 2. DOM fallback: try getElementById first (fastest native lookup)
4124
- var el = document.getElementById(id);
4125
-
4126
- // 3. Try querySelector for CSS selectors (starts with # or .)
4127
- if (!el && (id.charAt(0) === '#' || id.charAt(0) === '.')) {
4128
- el = document.querySelector(id);
4129
- }
4150
+ // Apply (if provided and element found)
4151
+ if (el && apply !== undefined) _applyTo(el, apply);
4130
4152
 
4131
- // 4. Try class-based lookup for bw_uuid_* tokens (UUID addressing)
4132
- if (!el && id.indexOf('bw_uuid_') === 0) {
4133
- el = document.querySelector('.' + id);
4134
- }
4153
+ return el;
4154
+ };
4135
4155
 
4136
- // 5. Cache the result for next time
4137
- if (el) {
4138
- bw._nodeMap[id] = el;
4156
+ /**
4157
+ * Internal: apply content or function to a DOM element.
4158
+ * Shared by bw.el() and bw.$().
4159
+ * @private
4160
+ */
4161
+ function _applyTo(el, apply) {
4162
+ if (_is(apply, 'function')) {
4163
+ apply(el);
4164
+ } else if (_isA(apply)) {
4165
+ el.innerHTML = '';
4166
+ apply.forEach(function(item) {
4167
+ if (item != null) {
4168
+ if (_is(item, 'object') && item.t) {
4169
+ el.appendChild(bw.createDOM(item));
4170
+ } else {
4171
+ el.appendChild(document.createTextNode(String(item)));
4172
+ }
4173
+ }
4174
+ });
4175
+ } else if (_is(apply, 'object') && apply !== null && apply.t) {
4176
+ el.innerHTML = '';
4177
+ el.appendChild(bw.createDOM(apply));
4178
+ } else {
4179
+ el.textContent = String(apply);
4139
4180
  }
4181
+ }
4140
4182
 
4141
- return el;
4142
- };
4183
+ // Internal alias — kept for one release cycle (v2.0.26).
4184
+ // Will be removed in v2.0.27. Use bw.el() instead.
4185
+ bw._el = bw.el;
4143
4186
 
4144
4187
  /**
4145
4188
  * Register a DOM element in the node cache under one or more keys.
@@ -4203,6 +4246,12 @@ var _BW_LC = 'bw_lc';
4203
4246
  */
4204
4247
  var _UUID_RE = /\bbw_uuid_[a-z0-9_]+\b/;
4205
4248
 
4249
+ /**
4250
+ * SVG namespace URI for createElementNS.
4251
+ * @private
4252
+ */
4253
+ var _SVG_NS = 'http://www.w3.org/2000/svg';
4254
+
4206
4255
  /**
4207
4256
  * Assign a UUID to a TACO object by appending a `bw_uuid_*` token to `taco.a.class`.
4208
4257
  *
@@ -4257,9 +4306,10 @@ bw.getUUID = function(tacoOrElement) {
4257
4306
  if (!tacoOrElement) return null;
4258
4307
 
4259
4308
  var classStr;
4260
- // DOM element: check className
4309
+ // DOM element: check className (SVG elements use getAttribute for string value)
4261
4310
  if (tacoOrElement.className !== undefined && tacoOrElement.tagName) {
4262
- classStr = tacoOrElement.className;
4311
+ classStr = typeof tacoOrElement.className === 'string'
4312
+ ? tacoOrElement.className : (tacoOrElement.getAttribute('class') || '');
4263
4313
  }
4264
4314
  // TACO object: check a.class
4265
4315
  else if (tacoOrElement.a && _is(tacoOrElement.a.class, 'string')) {
@@ -4528,7 +4578,7 @@ bw.htmlPage = function(opts) {
4528
4578
  var fnCounterBefore = bw._fnIDCounter;
4529
4579
 
4530
4580
  // Render body content
4531
- var bodyHTML = '';
4581
+ var bodyHTML;
4532
4582
  if (_is(body, 'string')) {
4533
4583
  bodyHTML = body;
4534
4584
  } else {
@@ -4699,9 +4749,11 @@ bw.createDOM = function(taco, options = {}) {
4699
4749
  }
4700
4750
 
4701
4751
  const { t: tag, a: attrs = {}, c: content, o: opts = {} } = taco;
4702
-
4703
- // Create element
4704
- const el = document.createElement(tag);
4752
+
4753
+ // SVG namespace: detect SVG context and thread through children.
4754
+ // {t:'svg'} starts SVG context; foreignObject children revert to HTML.
4755
+ var svgCtx = options._svgCtx || (tag === 'svg');
4756
+ var el = svgCtx ? document.createElementNS(_SVG_NS, tag) : document.createElement(tag);
4705
4757
 
4706
4758
  // Set attributes
4707
4759
  for (const [key, value] of Object.entries(attrs)) {
@@ -4712,9 +4764,11 @@ bw.createDOM = function(taco, options = {}) {
4712
4764
  Object.assign(el.style, value);
4713
4765
  } else if (key === 'class') {
4714
4766
  // Handle class as array or string
4767
+ // SVG elements use SVGAnimatedString for className, so use setAttribute
4715
4768
  const classStr = _isA(value) ? value.filter(Boolean).join(' ') : String(value);
4716
4769
  if (classStr) {
4717
- el.className = classStr;
4770
+ if (svgCtx) el.setAttribute('class', classStr);
4771
+ else el.className = classStr;
4718
4772
  }
4719
4773
  } else if (key.startsWith('on') && _is(value, 'function')) {
4720
4774
  // Event handlers
@@ -4735,11 +4789,17 @@ bw.createDOM = function(taco, options = {}) {
4735
4789
  // Add children, building _bw_refs for fast parent→child access.
4736
4790
  // Children with id attributes or bw_uuid_* classes get local refs on the parent,
4737
4791
  // so o.render functions can access them without any DOM lookup.
4792
+ // SVG: foreignObject children revert to HTML namespace; otherwise inherit.
4793
+ var childOpts = options;
4794
+ var childSvgCtx = svgCtx && tag !== 'foreignObject';
4795
+ if (childSvgCtx !== (options._svgCtx || false)) {
4796
+ childOpts = Object.assign({}, options, {_svgCtx: childSvgCtx || undefined});
4797
+ }
4738
4798
  if (content != null) {
4739
4799
  if (_isA(content)) {
4740
4800
  content.forEach(child => {
4741
4801
  if (child != null) {
4742
- var childEl = bw.createDOM(child, options);
4802
+ var childEl = bw.createDOM(child, childOpts);
4743
4803
  el.appendChild(childEl);
4744
4804
  // Build local refs for addressable children
4745
4805
  var childRefId = (child && child.a) ? (child.a.id || bw.getUUID(child)) : null;
@@ -4762,7 +4822,7 @@ bw.createDOM = function(taco, options = {}) {
4762
4822
  // Raw HTML content — inject via innerHTML
4763
4823
  el.innerHTML = content.v;
4764
4824
  } else if (_is(content, 'object') && content.t) {
4765
- var childEl = bw.createDOM(content, options);
4825
+ var childEl = bw.createDOM(content, childOpts);
4766
4826
  el.appendChild(childEl);
4767
4827
  var childRefId = content.a ? (content.a.id || bw.getUUID(content)) : null;
4768
4828
  if (childRefId) {
@@ -4788,13 +4848,21 @@ bw.createDOM = function(taco, options = {}) {
4788
4848
  }
4789
4849
 
4790
4850
  // Register UUID class in node cache (bw_uuid_* tokens in class string)
4791
- if (el.className) {
4792
- var uuidMatch = el.className.match(_UUID_RE);
4851
+ // SVG elements have SVGAnimatedString for className; use getAttribute instead
4852
+ var clsStr = svgCtx ? (el.getAttribute('class') || '') : el.className;
4853
+ if (clsStr) {
4854
+ var uuidMatch = clsStr.match(_UUID_RE);
4793
4855
  if (uuidMatch) {
4794
4856
  bw._nodeMap[uuidMatch[0]] = el;
4795
4857
  }
4796
4858
  }
4797
4859
 
4860
+ // Store component type metadata (e.g., 'card', 'tabs') for introspection.
4861
+ // BCCL factories set o.type; custom components can too.
4862
+ if (opts.type) {
4863
+ el._bw_type = opts.type;
4864
+ }
4865
+
4798
4866
  // Handle lifecycle hooks and state
4799
4867
  if (opts.mounted || opts.unmount || opts.render || opts.state) {
4800
4868
  // Ensure element has a UUID class for identity
@@ -4824,11 +4892,13 @@ bw.createDOM = function(taco, options = {}) {
4824
4892
 
4825
4893
  if (mountFn) {
4826
4894
  if (document.body.contains(el)) {
4827
- mountFn(el, el._bw_state || {});
4895
+ try { mountFn(el, el._bw_state || {}); }
4896
+ catch (e) { _cw('o.mounted error: ' + e.message); }
4828
4897
  } else {
4829
4898
  requestAnimationFrame(() => {
4830
4899
  if (document.body.contains(el)) {
4831
- mountFn(el, el._bw_state || {});
4900
+ try { mountFn(el, el._bw_state || {}); }
4901
+ catch (e) { _cw('o.mounted error: ' + e.message); }
4832
4902
  }
4833
4903
  });
4834
4904
  }
@@ -4837,7 +4907,8 @@ bw.createDOM = function(taco, options = {}) {
4837
4907
  // Store unmount callback keyed by UUID class
4838
4908
  if (opts.unmount) {
4839
4909
  bw._unmountCallbacks.set(uuid, () => {
4840
- opts.unmount(el, el._bw_state || {});
4910
+ try { opts.unmount(el, el._bw_state || {}); }
4911
+ catch (e) { _cw('o.unmount error: ' + e.message); }
4841
4912
  });
4842
4913
  }
4843
4914
  }
@@ -4856,24 +4927,25 @@ bw.createDOM = function(taco, options = {}) {
4856
4927
  }
4857
4928
 
4858
4929
  // Slot declarations: auto-generate setX/getX pairs
4930
+ // The target element is cached at creation time to avoid repeated
4931
+ // querySelector calls on every get/set invocation.
4859
4932
  if (opts.slots) {
4860
4933
  for (var sk in opts.slots) {
4861
4934
  if (_hop.call(opts.slots, sk)) {
4862
4935
  (function(name, selector) {
4936
+ var target = el.querySelector(selector);
4863
4937
  var cap = name.charAt(0).toUpperCase() + name.slice(1);
4864
4938
  el.bw['set' + cap] = function(value) {
4865
- var t = el.querySelector(selector);
4866
- if (!t) return;
4939
+ if (!target) return;
4867
4940
  if (value != null && typeof value === 'object' && value.t) {
4868
- t.innerHTML = '';
4869
- t.appendChild(bw.createDOM(value));
4941
+ target.innerHTML = '';
4942
+ target.appendChild(bw.createDOM(value));
4870
4943
  } else {
4871
- t.textContent = (value != null) ? String(value) : '';
4944
+ target.textContent = (value != null) ? String(value) : '';
4872
4945
  }
4873
4946
  };
4874
4947
  el.bw['get' + cap] = function() {
4875
- var t = el.querySelector(selector);
4876
- return t ? t.textContent : '';
4948
+ return target ? target.textContent : '';
4877
4949
  };
4878
4950
  })(sk, opts.slots[sk]);
4879
4951
  }
@@ -4914,7 +4986,7 @@ bw.DOM = function(target, taco, options = {}) {
4914
4986
  }
4915
4987
 
4916
4988
  // Get target element (use cache-backed lookup)
4917
- const targetEl = bw._el(target);
4989
+ const targetEl = bw.el(target);
4918
4990
 
4919
4991
  if (!targetEl) {
4920
4992
  _ce('bw.DOM: Target element not found:', target);
@@ -5017,7 +5089,8 @@ bw.cleanup = function(element) {
5017
5089
  // Deregister UUID classes from node cache for non-lifecycle UUID elements
5018
5090
  var uuidEls = element.querySelectorAll('[class*="bw_uuid_"]');
5019
5091
  uuidEls.forEach(function(uel) {
5020
- var m = uel.className && uel.className.match(_UUID_RE);
5092
+ var uc = typeof uel.className === 'string' ? uel.className : (uel.getAttribute('class') || '');
5093
+ var m = uc && uc.match(_UUID_RE);
5021
5094
  if (m) delete bw._nodeMap[m[0]];
5022
5095
  });
5023
5096
 
@@ -5103,9 +5176,10 @@ bw.cleanup = function(element) {
5103
5176
  * bw.update(el); // re-renders, emits bw:statechange
5104
5177
  */
5105
5178
  bw.update = function(target) {
5106
- var el = bw._el(target);
5179
+ var el = bw.el(target);
5107
5180
  if (el && el._bw_render) {
5108
- el._bw_render(el, el._bw_state || {});
5181
+ try { el._bw_render(el, el._bw_state || {}); }
5182
+ catch (e) { _cw('o.render error: ' + e.message); }
5109
5183
  bw.emit(el, 'statechange', el._bw_state);
5110
5184
  }
5111
5185
  return el || null;
@@ -5132,7 +5206,7 @@ bw.update = function(target) {
5132
5206
  * bw.patch('info', { t: 'em', c: 'new' }); // replace children with TACO
5133
5207
  */
5134
5208
  bw.patch = function(id, content, attr) {
5135
- var el = bw._el(id);
5209
+ var el = bw.el(id);
5136
5210
  if (!el) return null;
5137
5211
 
5138
5212
  if (attr) {
@@ -5204,7 +5278,7 @@ bw.patchAll = function(patches) {
5204
5278
  * // Dispatches CustomEvent 'bw:statechange' on the element
5205
5279
  */
5206
5280
  bw.emit = function(target, eventName, detail) {
5207
- var el = bw._el(target);
5281
+ var el = bw.el(target);
5208
5282
  if (el) {
5209
5283
  el.dispatchEvent(new CustomEvent('bw:' + eventName, {
5210
5284
  bubbles: true,
@@ -5233,7 +5307,7 @@ bw.emit = function(target, eventName, detail) {
5233
5307
  * });
5234
5308
  */
5235
5309
  bw.on = function(target, eventName, handler) {
5236
- var el = bw._el(target);
5310
+ var el = bw.el(target);
5237
5311
  if (el) {
5238
5312
  el.addEventListener('bw:' + eventName, function(e) {
5239
5313
  handler(e.detail, e);
@@ -5260,23 +5334,38 @@ bw.on = function(target, eventName, handler) {
5260
5334
  *
5261
5335
  * @param {string} topic - Topic name (plain string, no prefix)
5262
5336
  * @param {*} [detail] - Data to pass to subscribers
5263
- * @returns {number} Count of successfully called subscribers
5337
+ * @returns {number} Count of successfully called subscribers (including wildcard matches)
5264
5338
  * @category Pub/Sub
5265
5339
  * @see bw.sub
5266
5340
  * @example
5267
5341
  * bw.pub('score:updated', { player: 'X', score: 10 });
5342
+ * // Wildcard subscribers matching 'score:*' will also fire
5268
5343
  */
5269
5344
  bw.pub = function(topic, detail) {
5270
- var subs = bw._topics[topic];
5271
- if (!subs || subs.length === 0) return 0;
5272
- var snapshot = subs.slice(); // safe against unsub during iteration
5273
5345
  var called = 0;
5274
- for (var i = 0; i < snapshot.length; i++) {
5275
- try {
5276
- snapshot[i].handler(detail);
5277
- called++;
5278
- } catch (err) {
5279
- _cw('bw.pub: subscriber error on topic "' + topic + '":', err);
5346
+ // Exact-match subscribers
5347
+ var subs = bw._topics[topic];
5348
+ if (subs && subs.length > 0) {
5349
+ var snapshot = subs.slice();
5350
+ for (var i = 0; i < snapshot.length; i++) {
5351
+ try { snapshot[i].handler(detail, topic); called++; }
5352
+ catch (err) { _cw('bw.pub: subscriber error on topic "' + topic + '":', err); }
5353
+ }
5354
+ }
5355
+ // Wildcard subscribers -- patterns ending with '*'
5356
+ var keys = Object.keys(bw._topics);
5357
+ for (var k = 0; k < keys.length; k++) {
5358
+ var pat = keys[k];
5359
+ if (pat.charAt(pat.length - 1) !== '*') continue;
5360
+ var prefix = pat.slice(0, -1); // strip trailing '*'
5361
+ if (topic.length >= prefix.length && topic.substring(0, prefix.length) === prefix && topic !== pat) {
5362
+ var wsubs = bw._topics[pat];
5363
+ if (!wsubs) continue;
5364
+ var wsnap = wsubs.slice();
5365
+ for (var w = 0; w < wsnap.length; w++) {
5366
+ try { wsnap[w].handler(detail, topic); called++; }
5367
+ catch (err) { _cw('bw.pub: wildcard subscriber error on "' + pat + '" for topic "' + topic + '":', err); }
5368
+ }
5280
5369
  }
5281
5370
  }
5282
5371
  return called;
@@ -5285,12 +5374,17 @@ bw.pub = function(topic, detail) {
5285
5374
  /**
5286
5375
  * Subscribe to a topic. Returns an unsub() function.
5287
5376
  *
5288
- * Optional third argument ties the subscription to a DOM element's lifecycle —
5377
+ * Supports wildcard patterns: a topic ending in `*` matches any published
5378
+ * topic that starts with the prefix before the `*`. For example,
5379
+ * `'agui:*'` matches `'agui:ready'`, `'agui:error'`, etc. The handler
5380
+ * receives `(detail, topic)` so it can distinguish which topic fired.
5381
+ *
5382
+ * Optional third argument ties the subscription to a DOM element's lifecycle --
5289
5383
  * when `bw.cleanup()` is called on that element, the subscription is automatically
5290
5384
  * removed, preventing memory leaks.
5291
5385
  *
5292
- * @param {string} topic - Topic name
5293
- * @param {Function} handler - Called with (detail) on each publish
5386
+ * @param {string} topic - Topic name, or wildcard pattern ending in '*'
5387
+ * @param {Function} handler - Called with (detail, topic) on each publish
5294
5388
  * @param {Element} [el] - Optional DOM element to tie lifecycle to
5295
5389
  * @returns {Function} Call to unsubscribe
5296
5390
  * @category Pub/Sub
@@ -5301,6 +5395,11 @@ bw.pub = function(topic, detail) {
5301
5395
  * console.log(detail.player, 'scored', detail.score);
5302
5396
  * });
5303
5397
  * // Later: unsub() to stop listening
5398
+ *
5399
+ * // Wildcard: listen to all 'agui:' topics
5400
+ * bw.sub('agui:*', function(detail, topic) {
5401
+ * console.log('Got', topic, detail);
5402
+ * });
5304
5403
  */
5305
5404
  bw.sub = function(topic, handler, el) {
5306
5405
  var id = ++bw._subIdCounter;
@@ -5352,6 +5451,37 @@ bw.unsub = function(topic, handler) {
5352
5451
  return removed;
5353
5452
  };
5354
5453
 
5454
+ /**
5455
+ * Subscribe to a topic for a single event only. The subscription is
5456
+ * automatically removed after the first publish. Equivalent to manually
5457
+ * calling unsub() inside a bw.sub() handler, but avoids the common bug
5458
+ * of forgetting to unsubscribe.
5459
+ *
5460
+ * @param {string} topic - Topic name
5461
+ * @param {Function} handler - Called once with (detail) on the next publish
5462
+ * @param {Element} [el] - Optional DOM element to tie lifecycle to
5463
+ * @returns {Function} Call to cancel the subscription before it fires
5464
+ * @category Pub/Sub
5465
+ * @see bw.sub
5466
+ * @see bw.pub
5467
+ * @example
5468
+ * bw.once('data:loaded', function(detail) {
5469
+ * console.log('Received:', detail);
5470
+ * // No need to unsubscribe -- already done automatically
5471
+ * });
5472
+ *
5473
+ * // Cancel before it fires:
5474
+ * var cancel = bw.once('timeout', handler);
5475
+ * cancel(); // handler will never be called
5476
+ */
5477
+ bw.once = function(topic, handler, el) {
5478
+ var unsub = bw.sub(topic, function(detail) {
5479
+ unsub();
5480
+ handler(detail);
5481
+ }, el);
5482
+ return unsub;
5483
+ };
5484
+
5355
5485
  // ===================================================================================
5356
5486
  // Function Registry (revived from v1 for string dispatch contexts)
5357
5487
  // ===================================================================================
@@ -5592,7 +5722,7 @@ bw.component = function() { throw new Error('bw.component() removed in v2.0.19.
5592
5722
  * };
5593
5723
  */
5594
5724
  bw.message = function(target, action, data) {
5595
- var el = bw._el(target);
5725
+ var el = bw.el(target);
5596
5726
  if (!el) el = bw.$('.' + target)[0];
5597
5727
  if (!el || !el.bw || typeof el.bw[action] !== 'function') {
5598
5728
  _cw('bw.message: no handle method "' + action + '" on ' + target);
@@ -5602,6 +5732,207 @@ bw.message = function(target, action, data) {
5602
5732
  return true;
5603
5733
  };
5604
5734
 
5735
+ /**
5736
+ * Collect form data from all input, select, and textarea elements within a
5737
+ * container. Each element's `name` attribute (or `id` if no name) becomes a
5738
+ * key in the returned object. This provides a lightweight alternative to the
5739
+ * browser FormData API that returns a plain object suitable for JSON
5740
+ * serialization or bw.pub().
5741
+ *
5742
+ * Handles all standard HTML form controls:
5743
+ * - text/number/email/etc inputs: string value
5744
+ * - checkboxes: boolean (true/false)
5745
+ * - radio buttons: string value of the checked radio (unchecked groups omitted)
5746
+ * - multi-select: array of selected option values
5747
+ * - textarea: string value
5748
+ *
5749
+ * Elements without both `name` and `id` attributes are silently skipped.
5750
+ *
5751
+ * @param {string|Element} target - CSS selector, UUID string, or DOM element
5752
+ * @returns {Object} Plain object mapping field names to values
5753
+ * @category Component
5754
+ * @see bw.makeForm
5755
+ * @see bw.makeInput
5756
+ * @example
5757
+ * // Given a form with name="email" input and name="agree" checkbox:
5758
+ * var data = bw.formData('#signup-form');
5759
+ * // => { email: 'user@example.com', agree: true }
5760
+ *
5761
+ * // Collect and publish in one step:
5762
+ * bw.pub('form:submit', bw.formData('#my-form'));
5763
+ *
5764
+ * // Works with any container, not just <form>:
5765
+ * bw.pub('settings:changed', bw.formData('.settings-panel'));
5766
+ */
5767
+ bw.formData = function(target) {
5768
+ var el = bw.el(target);
5769
+ if (!el) return {};
5770
+ var result = {};
5771
+ var inputs = el.querySelectorAll('input, select, textarea');
5772
+ for (var i = 0; i < inputs.length; i++) {
5773
+ var inp = inputs[i];
5774
+ var key = inp.name || inp.id;
5775
+ if (!key) continue;
5776
+ if (inp.type === 'checkbox') {
5777
+ result[key] = inp.checked;
5778
+ } else if (inp.type === 'radio') {
5779
+ if (inp.checked) result[key] = inp.value;
5780
+ } else if (inp.tagName === 'SELECT' && inp.multiple) {
5781
+ result[key] = [];
5782
+ for (var j = 0; j < inp.options.length; j++) {
5783
+ if (inp.options[j].selected) result[key].push(inp.options[j].value);
5784
+ }
5785
+ } else {
5786
+ result[key] = inp.value;
5787
+ }
5788
+ }
5789
+ return result;
5790
+ };
5791
+
5792
+ // ===================================================================================
5793
+ // bw.jsonPatch() — RFC 6902 JSON Patch on plain objects
5794
+ // ===================================================================================
5795
+
5796
+ /**
5797
+ * Apply RFC 6902 JSON Patch operations to a plain object.
5798
+ *
5799
+ * Supported operations: add, remove, replace, move, copy, test.
5800
+ * Paths use JSON Pointer (RFC 6901) notation: `/foo/bar/0`.
5801
+ * Mutates the target object in place and returns it.
5802
+ *
5803
+ * @param {Object} obj - Target object to patch
5804
+ * @param {Array<Object>} ops - Array of patch operations
5805
+ * @param {string} ops[].op - Operation: 'add', 'remove', 'replace', 'move', 'copy', 'test'
5806
+ * @param {string} ops[].path - JSON Pointer path (e.g. '/a/b/0')
5807
+ * @param {*} [ops[].value] - Value for add/replace/test
5808
+ * @param {string} [ops[].from] - Source path for move/copy
5809
+ * @returns {Object} The patched object (same reference)
5810
+ * @throws {Error} On invalid op, missing path, test failure, or path not found for remove
5811
+ * @category Data Utilities
5812
+ * @see bw.patch
5813
+ * @example
5814
+ * var obj = { a: 1, b: { c: 2 } };
5815
+ * bw.jsonPatch(obj, [
5816
+ * { op: 'replace', path: '/a', value: 10 },
5817
+ * { op: 'add', path: '/b/d', value: 3 },
5818
+ * { op: 'remove', path: '/b/c' }
5819
+ * ]);
5820
+ * // obj => { a: 10, b: { d: 3 } }
5821
+ */
5822
+ bw.jsonPatch = function(obj, ops) {
5823
+ if (!_isA(ops)) return obj;
5824
+
5825
+ // Parse JSON Pointer path to array of keys
5826
+ function parsePath(path) {
5827
+ if (path === '') return [];
5828
+ if (path.charAt(0) !== '/') throw new Error('Invalid JSON Pointer: ' + path);
5829
+ return path.slice(1).split('/').map(function(s) {
5830
+ return s.replace(/~1/g, '/').replace(/~0/g, '~');
5831
+ });
5832
+ }
5833
+
5834
+ // Walk to parent of final key; return { parent, key }
5835
+ function resolve(root, keys) {
5836
+ var parent = root;
5837
+ for (var i = 0; i < keys.length - 1; i++) {
5838
+ var k = _isA(parent) ? parseInt(keys[i], 10) : keys[i];
5839
+ if (parent[k] === undefined) throw new Error('Path not found: /' + keys.slice(0, i + 1).join('/'));
5840
+ parent = parent[k];
5841
+ }
5842
+ return { parent: parent, key: _isA(parent) ? parseInt(keys[keys.length - 1], 10) : keys[keys.length - 1] };
5843
+ }
5844
+
5845
+ // Get value at path
5846
+ function getVal(root, keys) {
5847
+ var cur = root;
5848
+ for (var i = 0; i < keys.length; i++) {
5849
+ var k = _isA(cur) ? parseInt(keys[i], 10) : keys[i];
5850
+ if (cur[k] === undefined) throw new Error('Path not found: /' + keys.slice(0, i + 1).join('/'));
5851
+ cur = cur[k];
5852
+ }
5853
+ return cur;
5854
+ }
5855
+
5856
+ for (var i = 0; i < ops.length; i++) {
5857
+ var op = ops[i];
5858
+ if (!op.op || !_is(op.path, 'string')) throw new Error('Invalid patch operation at index ' + i);
5859
+ var keys = parsePath(op.path);
5860
+
5861
+ var r, val, fromKeys, fr, tr, cr;
5862
+ switch (op.op) {
5863
+ case 'add': {
5864
+ if (keys.length === 0) throw new Error('Cannot add to root');
5865
+ r = resolve(obj, keys);
5866
+ if (_isA(r.parent) && r.key <= r.parent.length) {
5867
+ r.parent.splice(r.key, 0, op.value);
5868
+ } else {
5869
+ r.parent[r.key] = op.value;
5870
+ }
5871
+ break;
5872
+ }
5873
+ case 'remove': {
5874
+ if (keys.length === 0) throw new Error('Cannot remove root');
5875
+ r = resolve(obj, keys);
5876
+ if (_isA(r.parent)) {
5877
+ if (r.key >= r.parent.length) throw new Error('Index out of bounds: ' + r.key);
5878
+ r.parent.splice(r.key, 1);
5879
+ } else {
5880
+ if (!(r.key in r.parent)) throw new Error('Path not found: ' + op.path);
5881
+ delete r.parent[r.key];
5882
+ }
5883
+ break;
5884
+ }
5885
+ case 'replace': {
5886
+ if (keys.length === 0) throw new Error('Cannot replace root');
5887
+ r = resolve(obj, keys);
5888
+ if (_isA(r.parent)) {
5889
+ if (r.key >= r.parent.length) throw new Error('Index out of bounds: ' + r.key);
5890
+ } else {
5891
+ if (!(r.key in r.parent)) throw new Error('Path not found: ' + op.path);
5892
+ }
5893
+ r.parent[r.key] = op.value;
5894
+ break;
5895
+ }
5896
+ case 'move': {
5897
+ if (!_is(op.from, 'string')) throw new Error('move requires "from"');
5898
+ fromKeys = parsePath(op.from);
5899
+ val = getVal(obj, fromKeys);
5900
+ fr = resolve(obj, fromKeys);
5901
+ if (_isA(fr.parent)) { fr.parent.splice(fr.key, 1); }
5902
+ else { delete fr.parent[fr.key]; }
5903
+ tr = resolve(obj, keys);
5904
+ if (_isA(tr.parent) && tr.key <= tr.parent.length) {
5905
+ tr.parent.splice(tr.key, 0, val);
5906
+ } else {
5907
+ tr.parent[tr.key] = val;
5908
+ }
5909
+ break;
5910
+ }
5911
+ case 'copy': {
5912
+ if (!_is(op.from, 'string')) throw new Error('copy requires "from"');
5913
+ val = getVal(obj, parsePath(op.from));
5914
+ cr = resolve(obj, keys);
5915
+ if (_isA(cr.parent) && cr.key <= cr.parent.length) {
5916
+ cr.parent.splice(cr.key, 0, val);
5917
+ } else {
5918
+ cr.parent[cr.key] = val;
5919
+ }
5920
+ break;
5921
+ }
5922
+ case 'test': {
5923
+ var actual = getVal(obj, keys);
5924
+ if (JSON.stringify(actual) !== JSON.stringify(op.value)) {
5925
+ throw new Error('Test failed: ' + op.path + ' expected ' + JSON.stringify(op.value) + ' got ' + JSON.stringify(actual));
5926
+ }
5927
+ break;
5928
+ }
5929
+ default:
5930
+ throw new Error('Unknown op: ' + op.op);
5931
+ }
5932
+ }
5933
+ return obj;
5934
+ };
5935
+
5605
5936
  // ===================================================================================
5606
5937
  // bw.apply() / bw.parseJSONFlex() — Server-driven UI protocol
5607
5938
  // ===================================================================================
@@ -5743,7 +6074,7 @@ bw.apply = function(msg) {
5743
6074
  var target = msg.target;
5744
6075
 
5745
6076
  if (type === 'replace') {
5746
- var el = bw._el(target);
6077
+ var el = bw.el(target);
5747
6078
  if (!el) return false;
5748
6079
  bw.DOM(el, msg.node);
5749
6080
  return true;
@@ -5753,14 +6084,14 @@ bw.apply = function(msg) {
5753
6084
  return patched !== null;
5754
6085
 
5755
6086
  } else if (type === 'append') {
5756
- var parent = bw._el(target);
6087
+ var parent = bw.el(target);
5757
6088
  if (!parent) return false;
5758
6089
  var child = bw.createDOM(msg.node);
5759
6090
  parent.appendChild(child);
5760
6091
  return true;
5761
6092
 
5762
6093
  } else if (type === 'remove') {
5763
- var toRemove = bw._el(target);
6094
+ var toRemove = bw.el(target);
5764
6095
  if (!toRemove) return false;
5765
6096
  if (_is(bw.cleanup, 'function')) bw.cleanup(toRemove);
5766
6097
  toRemove.remove();
@@ -5820,30 +6151,98 @@ bw.apply = function(msg) {
5820
6151
 
5821
6152
 
5822
6153
  // ===================================================================================
5823
- // bw.inspect() — Debug utility
6154
+ // bw.inspect() — DOM introspection with bitwrench metadata
5824
6155
  // ===================================================================================
5825
6156
 
5826
6157
  /**
5827
- * Inspect a DOM element's bitwrench state, handle methods, and metadata.
5828
- * Works with DOM elements or CSS selectors.
5829
- *
5830
- * @param {string|Element} target - Selector or DOM element
5831
- * @returns {Element|null} The element, or null if not found
6158
+ * Inspect a DOM element and its subtree, returning a plain-object
6159
+ * representation with bitwrench metadata at each node. Useful for debugging,
6160
+ * devtools, MCP/AG-UI tool discovery, and automated testing.
6161
+ *
6162
+ * Each node in the returned tree includes:
6163
+ * - `tag` -- lowercase tag name (or '#text' for text nodes)
6164
+ * - `id` -- element id (if set)
6165
+ * - `uuid` -- bitwrench UUID class (if lifecycle-managed)
6166
+ * - `type` -- component type from o.type (if set, e.g. 'card', 'tabs')
6167
+ * - `classes` -- first 5 CSS classes (string, space-separated)
6168
+ * - `handles` -- array of el.bw method names (if any)
6169
+ * - `state` -- copy of _bw_state (if any)
6170
+ * - `hasRender` -- true if _bw_render is set
6171
+ * - `hasSubs` -- true if element has pub/sub subscriptions
6172
+ * - `refs` -- copy of _bw_refs keys (if any)
6173
+ * - `children` -- array of child node trees (up to depth limit, max 50 per level)
6174
+ *
6175
+ * @param {string|Element} target - CSS selector, UUID, or DOM element
6176
+ * @param {number} [depth=3] - Maximum recursion depth (0 = target only, no children)
6177
+ * @returns {Object|null} Plain object tree, or null if element not found
5832
6178
  * @category Component
5833
6179
  * @example
5834
- * bw.inspect('#my-carousel');
5835
- * bw.inspect($0);
6180
+ * // Get full tree from #app, 3 levels deep (default):
6181
+ * var info = bw.inspect('#app');
6182
+ *
6183
+ * // Shallow inspection (just the element, no children):
6184
+ * var info = bw.inspect('#my-carousel', 0);
6185
+ * console.log(info.handles); // ['next', 'prev', 'goToSlide']
6186
+ * console.log(info.type); // 'carousel'
6187
+ *
6188
+ * // Deep inspection for debugging:
6189
+ * console.log(JSON.stringify(bw.inspect('#app', 5), null, 2));
5836
6190
  */
5837
- bw.inspect = function(target) {
5838
- var el = _is(target, 'string') ? bw.$(target)[0] : target;
5839
- if (!el) { _cw('bw.inspect: element not found'); return null; }
5840
- console.group('Element: ' + (bw.getUUID(el) || el.id || el.tagName));
5841
- _cl('State:', el._bw_state || '(none)');
5842
- _cl('Handle:', el.bw ? _keys(el.bw) : '(none)');
5843
- _cl('Classes:', el.className);
5844
- _cl('Refs:', el._bw_refs || '(none)');
5845
- console.groupEnd();
5846
- return el;
6191
+ bw.inspect = function(target, depth) {
6192
+ var el = bw.el(target);
6193
+ if (!el && _is(target, 'string')) el = bw.$(target)[0];
6194
+ if (!el) return null;
6195
+ if (depth === undefined || depth === null) depth = 3;
6196
+
6197
+ function walk(node, d) {
6198
+ if (!node) return null;
6199
+ // Skip non-element nodes (text, comment, etc.)
6200
+ if (node.nodeType !== 1) return null;
6201
+
6202
+ var info = { tag: node.tagName ? node.tagName.toLowerCase() : '#text' };
6203
+
6204
+ // Identity
6205
+ if (node.id) info.id = node.id;
6206
+ var uuid = bw.getUUID(node);
6207
+ if (uuid) info.uuid = uuid;
6208
+ if (node._bw_type) info.type = node._bw_type;
6209
+
6210
+ // CSS classes (first 5 for readability)
6211
+ if (node.className && typeof node.className === 'string') {
6212
+ info.classes = node.className.split(' ').slice(0, 5).join(' ');
6213
+ }
6214
+
6215
+ // Bitwrench handle methods
6216
+ if (node.bw) {
6217
+ var handles = _keys(node.bw);
6218
+ if (handles.length > 0) info.handles = handles;
6219
+ }
6220
+
6221
+ // State
6222
+ if (node._bw_state) info.state = node._bw_state;
6223
+ if (node._bw_render) info.hasRender = true;
6224
+ if (node._bw_subs && node._bw_subs.length > 0) info.hasSubs = true;
6225
+
6226
+ // Refs
6227
+ if (node._bw_refs) info.refs = _keys(node._bw_refs);
6228
+
6229
+ // Children (recurse up to depth limit, max 50 children per level)
6230
+ if (d < depth && node.children && node.children.length > 0) {
6231
+ info.children = [];
6232
+ var max = Math.min(node.children.length, 50);
6233
+ for (var i = 0; i < max; i++) {
6234
+ var child = walk(node.children[i], d + 1);
6235
+ if (child) info.children.push(child);
6236
+ }
6237
+ if (node.children.length > 50) {
6238
+ info.children.push({ tag: '...', count: node.children.length - 50 });
6239
+ }
6240
+ }
6241
+
6242
+ return info;
6243
+ }
6244
+
6245
+ return walk(el, 0);
5847
6246
  };
5848
6247
 
5849
6248
  bw.compile = function() { throw new Error('bw.compile() removed in v2.0.19. Use o.handle/o.slots on TACO options instead.'); };
@@ -6065,37 +6464,49 @@ bw.clip = clip;
6065
6464
  * so you can use `.map()`, `.filter()`, etc. directly. Accepts CSS selectors,
6066
6465
  * single elements, NodeLists, or arrays.
6067
6466
  *
6467
+ * With an optional second argument, applies content or a function to
6468
+ * every matched element (same apply rules as `bw.el()`):
6469
+ * - string/number: sets `el.textContent`
6470
+ * - function: calls `apply(el)` for each element
6471
+ * - TACO object: clears children, mounts TACO via `bw.createDOM()`
6472
+ * - array: clears children, appends each item
6473
+ *
6068
6474
  * @param {string|Element|Array} selector - CSS selector, element, or array
6475
+ * @param {string|number|Function|Object|Array} [apply] - Content or function to apply
6069
6476
  * @returns {Array} Array of DOM elements
6070
6477
  * @category DOM Selection
6478
+ * @see bw.el
6071
6479
  * @example
6072
- * bw.$('.card') // => [div.card, div.card, ...]
6073
- * bw.$(myElement) // => [myElement]
6074
- * bw.$('.card').map(el => el.textContent)
6480
+ * bw.$('.card') // => [div.card, div.card, ...]
6481
+ * bw.$('.status', 'Online') // set text on all .status elements
6482
+ * bw.$('.card', function(el) { // apply function to each
6483
+ * el.style.opacity = '0.5';
6484
+ * })
6075
6485
  */
6076
6486
  if (bw._isBrowser) {
6077
- bw.$ = function(selector) {
6078
- if (!selector) return [];
6079
-
6080
- // Already an array
6081
- if (_isA(selector)) return selector;
6082
-
6083
- // Single element
6084
- if (selector.nodeType) return [selector];
6085
-
6086
- // NodeList or HTMLCollection
6087
- if (selector.length !== undefined && !_is(selector, 'string')) {
6088
- return Array.from(selector);
6487
+ bw.$ = function(selector, apply) {
6488
+ var els;
6489
+ if (!selector) {
6490
+ els = [];
6491
+ } else if (_isA(selector)) {
6492
+ els = selector;
6493
+ } else if (selector.nodeType) {
6494
+ els = [selector];
6495
+ } else if (selector.length !== undefined && !_is(selector, 'string')) {
6496
+ els = Array.from(selector);
6497
+ } else if (_is(selector, 'string')) {
6498
+ els = Array.from(document.querySelectorAll(selector));
6499
+ } else {
6500
+ els = [];
6089
6501
  }
6090
-
6091
- // CSS selector string
6092
- if (_is(selector, 'string')) {
6093
- return Array.from(document.querySelectorAll(selector));
6502
+
6503
+ if (apply !== undefined) {
6504
+ for (var i = 0; i < els.length; i++) _applyTo(els[i], apply);
6094
6505
  }
6095
-
6096
- return [];
6506
+
6507
+ return els;
6097
6508
  };
6098
-
6509
+
6099
6510
  // Convenience single element selector
6100
6511
  bw.$.one = function(selector) {
6101
6512
  return bw.$(selector)[0] || null;
@@ -6308,42 +6719,48 @@ bw.loadReset = function() {
6308
6719
  };
6309
6720
 
6310
6721
  /**
6311
- * Toggle between primary and alternate palettes.
6722
+ * Toggle between primary and alternate theme palettes.
6312
6723
  *
6313
- * Adds/removes the `bw_theme_alt` class on the scoping element.
6724
+ * Adds/removes the `bw_theme_alt` class on the scoping element(s).
6314
6725
  * Without a scope, toggles on `<html>` (global).
6315
- * With a scope, toggles on the first matching element.
6726
+ * With a scope, toggles on ALL matching elements.
6316
6727
  *
6317
- * @param {string} [scope] - Scope selector (e.g. '#my-dashboard'). Omit for global.
6318
- * @returns {string} Active mode after toggle: 'primary' or 'alternate'
6728
+ * @param {string|Element} [scope] - Selector or element. Omit for global.
6729
+ * @returns {string} Active mode after toggle: 'primary' or 'alternate' (based on first element)
6319
6730
  * @category CSS & Styling
6320
6731
  * @see bw.applyStyles
6321
6732
  * @see bw.clearStyles
6322
6733
  * @example
6323
- * bw.toggleStyles(); // global toggle on <html>
6324
- * bw.toggleStyles('#my-dashboard'); // scoped toggle
6734
+ * bw.toggleThemeMode(); // global toggle on <html>
6735
+ * bw.toggleThemeMode('#my-dashboard'); // scoped toggle
6736
+ * bw.toggleThemeMode('.panel'); // toggle on ALL .panel elements
6325
6737
  */
6326
- bw.toggleStyles = function(scope) {
6738
+ bw.toggleThemeMode = function(scope) {
6327
6739
  if (!bw._isBrowser) return 'primary';
6328
- var target;
6740
+ var els;
6329
6741
  if (scope) {
6330
- var els = bw.$(scope);
6331
- target = els[0];
6742
+ els = bw.$(scope);
6332
6743
  } else {
6333
- target = document.documentElement;
6744
+ els = [document.documentElement];
6334
6745
  }
6335
- if (!target) return 'primary';
6746
+ if (!els.length) return 'primary';
6336
6747
 
6337
- var hasAlt = target.classList.contains('bw_theme_alt');
6338
- if (hasAlt) {
6339
- target.classList.remove('bw_theme_alt');
6340
- return 'primary';
6341
- } else {
6342
- target.classList.add('bw_theme_alt');
6343
- return 'alternate';
6748
+ var mode;
6749
+ for (var i = 0; i < els.length; i++) {
6750
+ var hasAlt = els[i].classList.contains('bw_theme_alt');
6751
+ if (hasAlt) {
6752
+ els[i].classList.remove('bw_theme_alt');
6753
+ } else {
6754
+ els[i].classList.add('bw_theme_alt');
6755
+ }
6756
+ if (i === 0) mode = hasAlt ? 'primary' : 'alternate';
6344
6757
  }
6758
+ return mode;
6345
6759
  };
6346
6760
 
6761
+ // Alias — kept for one release cycle. Use bw.toggleThemeMode() instead.
6762
+ bw.toggleStyles = bw.toggleThemeMode;
6763
+
6347
6764
  /**
6348
6765
  * Remove injected styles for a given scope.
6349
6766
  *
@@ -7383,6 +7800,57 @@ Object.entries(components).forEach(([name, fn]) => {
7383
7800
  }
7384
7801
  });
7385
7802
 
7803
+ /**
7804
+ * Query the BCCL component registry. Returns metadata about registered
7805
+ * component types -- their names and factory function names. Useful for
7806
+ * tooling, introspection, documentation generators, and auto-complete
7807
+ * systems (including MCP/AG-UI tool discovery).
7808
+ *
7809
+ * With no arguments, returns an array of all registered component types.
7810
+ * With a type name, returns metadata for that single type (or null if
7811
+ * the type is not registered).
7812
+ *
7813
+ * @param {string} [type] - Optional component type name to look up
7814
+ * @returns {Array<Object>|Object|null} Array of {type, factory} objects,
7815
+ * a single {type, factory} object, or null if the type is not found
7816
+ * @category Component
7817
+ * @see bw.make
7818
+ * @see bw.BCCL
7819
+ * @example
7820
+ * // List all available component types:
7821
+ * bw.catalog();
7822
+ * // => [{ type: 'card', factory: 'makeCard' },
7823
+ * // { type: 'button', factory: 'makeButton' }, ...]
7824
+ *
7825
+ * // Look up a specific type:
7826
+ * bw.catalog('accordion');
7827
+ * // => { type: 'accordion', factory: 'makeAccordion' }
7828
+ *
7829
+ * // Check if a type exists:
7830
+ * if (bw.catalog('chart')) { ... }
7831
+ *
7832
+ * // Get just the type names:
7833
+ * bw.catalog().map(function(c) { return c.type; });
7834
+ * // => ['card', 'button', 'container', 'row', ...]
7835
+ */
7836
+ bw.catalog = function(type) {
7837
+ if (type) {
7838
+ var def = bw.BCCL[type];
7839
+ if (!def) return null;
7840
+ return {
7841
+ type: type,
7842
+ factory: def.make.name || ('make' + type.charAt(0).toUpperCase() + type.slice(1))
7843
+ };
7844
+ }
7845
+ return Object.keys(bw.BCCL).map(function(k) {
7846
+ var def = bw.BCCL[k];
7847
+ return {
7848
+ type: k,
7849
+ factory: def.make.name || ('make' + k.charAt(0).toUpperCase() + k.slice(1))
7850
+ };
7851
+ });
7852
+ };
7853
+
7386
7854
  // Also attach to global in browsers
7387
7855
  if (bw._isBrowser && typeof window !== 'undefined') {
7388
7856
  window.bw = bw;