bitwrench 2.0.25 → 2.0.30

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