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,4 +1,4 @@
1
- /*! bitwrench v2.0.25 | BSD-2-Clause | https://deftio.github.com/bitwrench/pages */
1
+ /*! bitwrench v2.0.31 | BSD-2-Clause | https://deftio.github.com/bitwrench/pages */
2
2
  (function (global, factory) {
3
3
  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
4
4
  typeof define === 'function' && define.amd ? define(factory) :
@@ -190,14 +190,14 @@
190
190
  */
191
191
 
192
192
  var VERSION_INFO = {
193
- version: '2.0.25',
193
+ version: '2.0.31',
194
194
  name: 'bitwrench',
195
195
  description: 'A library for javascript UI functions.',
196
196
  license: 'BSD-2-Clause',
197
197
  homepage: 'https://deftio.github.com/bitwrench/pages',
198
198
  repository: 'git+https://github.com/deftio/bitwrench.git',
199
199
  author: 'manu a. chatterjee <deftio@deftio.com> (https://deftio.com/)',
200
- buildDate: '2026-03-31T03:03:30.752Z'
200
+ buildDate: '2026-04-12T07:56:29.791Z'
201
201
  };
202
202
 
203
203
  /**
@@ -10100,9 +10100,6 @@
10100
10100
  var _cw = function _cw() {
10101
10101
  console.warn.apply(console, arguments);
10102
10102
  };
10103
- var _cl = function _cl() {
10104
- console.log.apply(console, arguments);
10105
- };
10106
10103
  var _ce = function _ce() {
10107
10104
  console.error.apply(console, arguments);
10108
10105
  };
@@ -10243,60 +10240,104 @@
10243
10240
  };
10244
10241
 
10245
10242
  /**
10246
- * Look up a DOM element by ID string, using the node cache for O(1) access.
10243
+ * Look up a single DOM element by ID, CSS selector, UUID, or element ref.
10244
+ * Optionally apply content or a function to the resolved element.
10247
10245
  *
10248
- * Resolution order:
10249
- * 1. Check `bw._nodeMap[id]` if found and still attached (parentNode !== null), return it
10250
- * 2. If cached ref is detached (parentNode === null), remove stale entry
10251
- * 3. Fall back to `document.getElementById(id)` then `document.querySelector(...)`
10252
- * 4. Try class-based lookup for bw_uuid_* tokens (UUID addressing)
10253
- * 5. Cache the result for next time
10246
+ * Resolution order for string targets:
10247
+ * 1. Check `bw._nodeMap[id]` cache (O(1), stale entries auto-pruned)
10248
+ * 2. `document.getElementById(id)`
10249
+ * 3. `document.querySelector(id)` for selectors starting with # or .
10250
+ * 4. Class-based lookup for `bw_uuid_*` tokens
10254
10251
  *
10255
- * Accepts a DOM element directly (pass-through) or a string identifier.
10256
- * String identifiers are tried as: direct map key, getElementById,
10257
- * querySelector (for CSS selectors starting with . or #), and
10258
- * bw_uuid_* class selector.
10252
+ * With one argument, returns the element (or null). With two arguments,
10253
+ * applies the second argument to the element and returns the element:
10254
+ * - string/number: sets `el.textContent`
10255
+ * - function: calls `apply(el)`, returns el
10256
+ * - TACO object: clears children, mounts TACO via `bw.createDOM()`
10257
+ * - array: clears children, appends each item (string -> text node, TACO -> element)
10259
10258
  *
10260
- * @param {string|Element} id - Element ID, CSS selector, bw_uuid_* class, or DOM element
10259
+ * @param {string|Element} target - Element ref, ID, CSS selector, or bw_uuid_* class
10260
+ * @param {string|number|Function|Object|Array} [apply] - Content or function to apply
10261
10261
  * @returns {Element|null} The DOM element, or null if not found
10262
- * @category Internal
10262
+ * @category DOM Selection
10263
+ * @see bw.$
10264
+ * @see bw.patch
10265
+ * @example
10266
+ * bw.el('#title') // lookup
10267
+ * bw.el('#title', 'Hello') // set text content
10268
+ * bw.el('#app', { t: 'h1', c: 'Hi' }) // mount TACO
10269
+ * bw.el('.card', function(el) { // apply function
10270
+ * el.style.opacity = '0.5';
10271
+ * })
10263
10272
  */
10264
- bw._el = function (id) {
10265
- // Pass-through for DOM elements
10266
- if (!_is(id, 'string')) return id || null;
10267
- if (!id) return null;
10268
- if (!bw._isBrowser) return null;
10269
-
10270
- // 1. Check cache
10271
- var cached = bw._nodeMap[id];
10272
- if (cached) {
10273
- // Verify not detached (parentNode check is IE11-safe)
10274
- if (cached.parentNode !== null) {
10275
- return cached;
10273
+ bw.el = function (target, apply) {
10274
+ // Resolve target to element
10275
+ var el;
10276
+ if (!_is(target, 'string')) {
10277
+ el = target || null;
10278
+ } else if (!target || !bw._isBrowser) {
10279
+ el = null;
10280
+ } else {
10281
+ // 1. Check cache
10282
+ var cached = bw._nodeMap[target];
10283
+ if (cached) {
10284
+ if (cached.parentNode !== null) {
10285
+ el = cached;
10286
+ } else {
10287
+ delete bw._nodeMap[target];
10288
+ }
10289
+ }
10290
+ if (!el) {
10291
+ // 2. getElementById
10292
+ el = document.getElementById(target);
10293
+ // 3. querySelector for CSS selectors
10294
+ if (!el && (target.charAt(0) === '#' || target.charAt(0) === '.')) {
10295
+ el = document.querySelector(target);
10296
+ }
10297
+ // 4. bw_uuid_* class lookup
10298
+ if (!el && target.indexOf('bw_uuid_') === 0) {
10299
+ el = document.querySelector('.' + target);
10300
+ }
10301
+ // 5. Cache result
10302
+ if (el) bw._nodeMap[target] = el;
10276
10303
  }
10277
- // Stale — remove and fall through
10278
- delete bw._nodeMap[id];
10279
10304
  }
10280
10305
 
10281
- // 2. DOM fallback: try getElementById first (fastest native lookup)
10282
- var el = document.getElementById(id);
10283
-
10284
- // 3. Try querySelector for CSS selectors (starts with # or .)
10285
- if (!el && (id.charAt(0) === '#' || id.charAt(0) === '.')) {
10286
- el = document.querySelector(id);
10287
- }
10306
+ // Apply (if provided and element found)
10307
+ if (el && apply !== undefined) _applyTo(el, apply);
10308
+ return el;
10309
+ };
10288
10310
 
10289
- // 4. Try class-based lookup for bw_uuid_* tokens (UUID addressing)
10290
- if (!el && id.indexOf('bw_uuid_') === 0) {
10291
- el = document.querySelector('.' + id);
10311
+ /**
10312
+ * Internal: apply content or function to a DOM element.
10313
+ * Shared by bw.el() and bw.$().
10314
+ * @private
10315
+ */
10316
+ function _applyTo(el, apply) {
10317
+ if (_is(apply, 'function')) {
10318
+ apply(el);
10319
+ } else if (_isA(apply)) {
10320
+ el.innerHTML = '';
10321
+ apply.forEach(function (item) {
10322
+ if (item != null) {
10323
+ if (_is(item, 'object') && item.t) {
10324
+ el.appendChild(bw.createDOM(item));
10325
+ } else {
10326
+ el.appendChild(document.createTextNode(String(item)));
10327
+ }
10328
+ }
10329
+ });
10330
+ } else if (_is(apply, 'object') && apply !== null && apply.t) {
10331
+ el.innerHTML = '';
10332
+ el.appendChild(bw.createDOM(apply));
10333
+ } else {
10334
+ el.textContent = String(apply);
10292
10335
  }
10336
+ }
10293
10337
 
10294
- // 5. Cache the result for next time
10295
- if (el) {
10296
- bw._nodeMap[id] = el;
10297
- }
10298
- return el;
10299
- };
10338
+ // Internal alias kept for one release cycle (v2.0.26).
10339
+ // Will be removed in v2.0.27. Use bw.el() instead.
10340
+ bw._el = bw.el;
10300
10341
 
10301
10342
  /**
10302
10343
  * Register a DOM element in the node cache under one or more keys.
@@ -10360,6 +10401,12 @@
10360
10401
  */
10361
10402
  var _UUID_RE = /\bbw_uuid_[a-z0-9_]+\b/;
10362
10403
 
10404
+ /**
10405
+ * SVG namespace URI for createElementNS.
10406
+ * @private
10407
+ */
10408
+ var _SVG_NS = 'http://www.w3.org/2000/svg';
10409
+
10363
10410
  /**
10364
10411
  * Assign a UUID to a TACO object by appending a `bw_uuid_*` token to `taco.a.class`.
10365
10412
  *
@@ -10410,9 +10457,9 @@
10410
10457
  bw.getUUID = function (tacoOrElement) {
10411
10458
  if (!tacoOrElement) return null;
10412
10459
  var classStr;
10413
- // DOM element: check className
10460
+ // DOM element: check className (SVG elements use getAttribute for string value)
10414
10461
  if (tacoOrElement.className !== undefined && tacoOrElement.tagName) {
10415
- classStr = tacoOrElement.className;
10462
+ classStr = typeof tacoOrElement.className === 'string' ? tacoOrElement.className : tacoOrElement.getAttribute('class') || '';
10416
10463
  }
10417
10464
  // TACO object: check a.class
10418
10465
  else if (tacoOrElement.a && _is(tacoOrElement.a["class"], 'string')) {
@@ -10697,7 +10744,7 @@
10697
10744
  var fnCounterBefore = bw._fnIDCounter;
10698
10745
 
10699
10746
  // Render body content
10700
- var bodyHTML = '';
10747
+ var bodyHTML;
10701
10748
  if (_is(body, 'string')) {
10702
10749
  bodyHTML = body;
10703
10750
  } else {
@@ -10873,8 +10920,10 @@
10873
10920
  _taco$o2 = taco.o,
10874
10921
  opts = _taco$o2 === void 0 ? {} : _taco$o2;
10875
10922
 
10876
- // Create element
10877
- var el = document.createElement(tag);
10923
+ // SVG namespace: detect SVG context and thread through children.
10924
+ // {t:'svg'} starts SVG context; foreignObject children revert to HTML.
10925
+ var svgCtx = options._svgCtx || tag === 'svg';
10926
+ var el = svgCtx ? document.createElementNS(_SVG_NS, tag) : document.createElement(tag);
10878
10927
 
10879
10928
  // Set attributes
10880
10929
  for (var _i2 = 0, _Object$entries2 = Object.entries(attrs); _i2 < _Object$entries2.length; _i2++) {
@@ -10887,9 +10936,10 @@
10887
10936
  Object.assign(el.style, value);
10888
10937
  } else if (key === 'class') {
10889
10938
  // Handle class as array or string
10939
+ // SVG elements use SVGAnimatedString for className, so use setAttribute
10890
10940
  var classStr = _isA(value) ? value.filter(Boolean).join(' ') : String(value);
10891
10941
  if (classStr) {
10892
- el.className = classStr;
10942
+ if (svgCtx) el.setAttribute('class', classStr);else el.className = classStr;
10893
10943
  }
10894
10944
  } else if (key.startsWith('on') && _is(value, 'function')) {
10895
10945
  // Event handlers
@@ -10910,11 +10960,19 @@
10910
10960
  // Add children, building _bw_refs for fast parent→child access.
10911
10961
  // Children with id attributes or bw_uuid_* classes get local refs on the parent,
10912
10962
  // so o.render functions can access them without any DOM lookup.
10963
+ // SVG: foreignObject children revert to HTML namespace; otherwise inherit.
10964
+ var childOpts = options;
10965
+ var childSvgCtx = svgCtx && tag !== 'foreignObject';
10966
+ if (childSvgCtx !== (options._svgCtx || false)) {
10967
+ childOpts = Object.assign({}, options, {
10968
+ _svgCtx: childSvgCtx || undefined
10969
+ });
10970
+ }
10913
10971
  if (content != null) {
10914
10972
  if (_isA(content)) {
10915
10973
  content.forEach(function (child) {
10916
10974
  if (child != null) {
10917
- var childEl = bw.createDOM(child, options);
10975
+ var childEl = bw.createDOM(child, childOpts);
10918
10976
  el.appendChild(childEl);
10919
10977
  // Build local refs for addressable children
10920
10978
  var childRefId = child && child.a ? child.a.id || bw.getUUID(child) : null;
@@ -10937,7 +10995,7 @@
10937
10995
  // Raw HTML content — inject via innerHTML
10938
10996
  el.innerHTML = content.v;
10939
10997
  } else if (_is(content, 'object') && content.t) {
10940
- var childEl = bw.createDOM(content, options);
10998
+ var childEl = bw.createDOM(content, childOpts);
10941
10999
  el.appendChild(childEl);
10942
11000
  var childRefId = content.a ? content.a.id || bw.getUUID(content) : null;
10943
11001
  if (childRefId) {
@@ -10963,13 +11021,21 @@
10963
11021
  }
10964
11022
 
10965
11023
  // Register UUID class in node cache (bw_uuid_* tokens in class string)
10966
- if (el.className) {
10967
- var uuidMatch = el.className.match(_UUID_RE);
11024
+ // SVG elements have SVGAnimatedString for className; use getAttribute instead
11025
+ var clsStr = svgCtx ? el.getAttribute('class') || '' : el.className;
11026
+ if (clsStr) {
11027
+ var uuidMatch = clsStr.match(_UUID_RE);
10968
11028
  if (uuidMatch) {
10969
11029
  bw._nodeMap[uuidMatch[0]] = el;
10970
11030
  }
10971
11031
  }
10972
11032
 
11033
+ // Store component type metadata (e.g., 'card', 'tabs') for introspection.
11034
+ // BCCL factories set o.type; custom components can too.
11035
+ if (opts.type) {
11036
+ el._bw_type = opts.type;
11037
+ }
11038
+
10973
11039
  // Handle lifecycle hooks and state
10974
11040
  if (opts.mounted || opts.unmount || opts.render || opts.state) {
10975
11041
  // Ensure element has a UUID class for identity
@@ -10998,11 +11064,19 @@
10998
11064
  } : null);
10999
11065
  if (mountFn) {
11000
11066
  if (document.body.contains(el)) {
11001
- mountFn(el, el._bw_state || {});
11067
+ try {
11068
+ mountFn(el, el._bw_state || {});
11069
+ } catch (e) {
11070
+ _cw('o.mounted error: ' + e.message);
11071
+ }
11002
11072
  } else {
11003
11073
  requestAnimationFrame(function () {
11004
11074
  if (document.body.contains(el)) {
11005
- mountFn(el, el._bw_state || {});
11075
+ try {
11076
+ mountFn(el, el._bw_state || {});
11077
+ } catch (e) {
11078
+ _cw('o.mounted error: ' + e.message);
11079
+ }
11006
11080
  }
11007
11081
  });
11008
11082
  }
@@ -11011,7 +11085,11 @@
11011
11085
  // Store unmount callback keyed by UUID class
11012
11086
  if (opts.unmount) {
11013
11087
  bw._unmountCallbacks.set(uuid, function () {
11014
- opts.unmount(el, el._bw_state || {});
11088
+ try {
11089
+ opts.unmount(el, el._bw_state || {});
11090
+ } catch (e) {
11091
+ _cw('o.unmount error: ' + e.message);
11092
+ }
11015
11093
  });
11016
11094
  }
11017
11095
  }
@@ -11030,24 +11108,25 @@
11030
11108
  }
11031
11109
 
11032
11110
  // Slot declarations: auto-generate setX/getX pairs
11111
+ // The target element is cached at creation time to avoid repeated
11112
+ // querySelector calls on every get/set invocation.
11033
11113
  if (opts.slots) {
11034
11114
  for (var sk in opts.slots) {
11035
11115
  if (_hop.call(opts.slots, sk)) {
11036
11116
  (function (name, selector) {
11117
+ var target = el.querySelector(selector);
11037
11118
  var cap = name.charAt(0).toUpperCase() + name.slice(1);
11038
11119
  el.bw['set' + cap] = function (value) {
11039
- var t = el.querySelector(selector);
11040
- if (!t) return;
11120
+ if (!target) return;
11041
11121
  if (value != null && _typeof(value) === 'object' && value.t) {
11042
- t.innerHTML = '';
11043
- t.appendChild(bw.createDOM(value));
11122
+ target.innerHTML = '';
11123
+ target.appendChild(bw.createDOM(value));
11044
11124
  } else {
11045
- t.textContent = value != null ? String(value) : '';
11125
+ target.textContent = value != null ? String(value) : '';
11046
11126
  }
11047
11127
  };
11048
11128
  el.bw['get' + cap] = function () {
11049
- var t = el.querySelector(selector);
11050
- return t ? t.textContent : '';
11129
+ return target ? target.textContent : '';
11051
11130
  };
11052
11131
  })(sk, opts.slots[sk]);
11053
11132
  }
@@ -11088,7 +11167,7 @@
11088
11167
  }
11089
11168
 
11090
11169
  // Get target element (use cache-backed lookup)
11091
- var targetEl = bw._el(target);
11170
+ var targetEl = bw.el(target);
11092
11171
  if (!targetEl) {
11093
11172
  _ce('bw.DOM: Target element not found:', target);
11094
11173
  return null;
@@ -11191,7 +11270,8 @@
11191
11270
  // Deregister UUID classes from node cache for non-lifecycle UUID elements
11192
11271
  var uuidEls = element.querySelectorAll('[class*="bw_uuid_"]');
11193
11272
  uuidEls.forEach(function (uel) {
11194
- var m = uel.className && uel.className.match(_UUID_RE);
11273
+ var uc = typeof uel.className === 'string' ? uel.className : uel.getAttribute('class') || '';
11274
+ var m = uc && uc.match(_UUID_RE);
11195
11275
  if (m) delete bw._nodeMap[m[0]];
11196
11276
  });
11197
11277
 
@@ -11279,9 +11359,13 @@
11279
11359
  * bw.update(el); // re-renders, emits bw:statechange
11280
11360
  */
11281
11361
  bw.update = function (target) {
11282
- var el = bw._el(target);
11362
+ var el = bw.el(target);
11283
11363
  if (el && el._bw_render) {
11284
- el._bw_render(el, el._bw_state || {});
11364
+ try {
11365
+ el._bw_render(el, el._bw_state || {});
11366
+ } catch (e) {
11367
+ _cw('o.render error: ' + e.message);
11368
+ }
11285
11369
  bw.emit(el, 'statechange', el._bw_state);
11286
11370
  }
11287
11371
  return el || null;
@@ -11308,7 +11392,7 @@
11308
11392
  * bw.patch('info', { t: 'em', c: 'new' }); // replace children with TACO
11309
11393
  */
11310
11394
  bw.patch = function (id, content, attr) {
11311
- var el = bw._el(id);
11395
+ var el = bw.el(id);
11312
11396
  if (!el) return null;
11313
11397
  if (attr) {
11314
11398
  // Patch an attribute
@@ -11379,7 +11463,7 @@
11379
11463
  * // Dispatches CustomEvent 'bw:statechange' on the element
11380
11464
  */
11381
11465
  bw.emit = function (target, eventName, detail) {
11382
- var el = bw._el(target);
11466
+ var el = bw.el(target);
11383
11467
  if (el) {
11384
11468
  el.dispatchEvent(new CustomEvent('bw:' + eventName, {
11385
11469
  bubbles: true,
@@ -11408,7 +11492,7 @@
11408
11492
  * });
11409
11493
  */
11410
11494
  bw.on = function (target, eventName, handler) {
11411
- var el = bw._el(target);
11495
+ var el = bw.el(target);
11412
11496
  if (el) {
11413
11497
  el.addEventListener('bw:' + eventName, function (e) {
11414
11498
  handler(e.detail, e);
@@ -11435,23 +11519,46 @@
11435
11519
  *
11436
11520
  * @param {string} topic - Topic name (plain string, no prefix)
11437
11521
  * @param {*} [detail] - Data to pass to subscribers
11438
- * @returns {number} Count of successfully called subscribers
11522
+ * @returns {number} Count of successfully called subscribers (including wildcard matches)
11439
11523
  * @category Pub/Sub
11440
11524
  * @see bw.sub
11441
11525
  * @example
11442
11526
  * bw.pub('score:updated', { player: 'X', score: 10 });
11527
+ * // Wildcard subscribers matching 'score:*' will also fire
11443
11528
  */
11444
11529
  bw.pub = function (topic, detail) {
11445
- var subs = bw._topics[topic];
11446
- if (!subs || subs.length === 0) return 0;
11447
- var snapshot = subs.slice(); // safe against unsub during iteration
11448
11530
  var called = 0;
11449
- for (var i = 0; i < snapshot.length; i++) {
11450
- try {
11451
- snapshot[i].handler(detail);
11452
- called++;
11453
- } catch (err) {
11454
- _cw('bw.pub: subscriber error on topic "' + topic + '":', err);
11531
+ // Exact-match subscribers
11532
+ var subs = bw._topics[topic];
11533
+ if (subs && subs.length > 0) {
11534
+ var snapshot = subs.slice();
11535
+ for (var i = 0; i < snapshot.length; i++) {
11536
+ try {
11537
+ snapshot[i].handler(detail, topic);
11538
+ called++;
11539
+ } catch (err) {
11540
+ _cw('bw.pub: subscriber error on topic "' + topic + '":', err);
11541
+ }
11542
+ }
11543
+ }
11544
+ // Wildcard subscribers -- patterns ending with '*'
11545
+ var keys = Object.keys(bw._topics);
11546
+ for (var k = 0; k < keys.length; k++) {
11547
+ var pat = keys[k];
11548
+ if (pat.charAt(pat.length - 1) !== '*') continue;
11549
+ var prefix = pat.slice(0, -1); // strip trailing '*'
11550
+ if (topic.length >= prefix.length && topic.substring(0, prefix.length) === prefix && topic !== pat) {
11551
+ var wsubs = bw._topics[pat];
11552
+ if (!wsubs) continue;
11553
+ var wsnap = wsubs.slice();
11554
+ for (var w = 0; w < wsnap.length; w++) {
11555
+ try {
11556
+ wsnap[w].handler(detail, topic);
11557
+ called++;
11558
+ } catch (err) {
11559
+ _cw('bw.pub: wildcard subscriber error on "' + pat + '" for topic "' + topic + '":', err);
11560
+ }
11561
+ }
11455
11562
  }
11456
11563
  }
11457
11564
  return called;
@@ -11460,12 +11567,17 @@
11460
11567
  /**
11461
11568
  * Subscribe to a topic. Returns an unsub() function.
11462
11569
  *
11463
- * Optional third argument ties the subscription to a DOM element's lifecycle —
11570
+ * Supports wildcard patterns: a topic ending in `*` matches any published
11571
+ * topic that starts with the prefix before the `*`. For example,
11572
+ * `'agui:*'` matches `'agui:ready'`, `'agui:error'`, etc. The handler
11573
+ * receives `(detail, topic)` so it can distinguish which topic fired.
11574
+ *
11575
+ * Optional third argument ties the subscription to a DOM element's lifecycle --
11464
11576
  * when `bw.cleanup()` is called on that element, the subscription is automatically
11465
11577
  * removed, preventing memory leaks.
11466
11578
  *
11467
- * @param {string} topic - Topic name
11468
- * @param {Function} handler - Called with (detail) on each publish
11579
+ * @param {string} topic - Topic name, or wildcard pattern ending in '*'
11580
+ * @param {Function} handler - Called with (detail, topic) on each publish
11469
11581
  * @param {Element} [el] - Optional DOM element to tie lifecycle to
11470
11582
  * @returns {Function} Call to unsubscribe
11471
11583
  * @category Pub/Sub
@@ -11476,6 +11588,11 @@
11476
11588
  * console.log(detail.player, 'scored', detail.score);
11477
11589
  * });
11478
11590
  * // Later: unsub() to stop listening
11591
+ *
11592
+ * // Wildcard: listen to all 'agui:' topics
11593
+ * bw.sub('agui:*', function(detail, topic) {
11594
+ * console.log('Got', topic, detail);
11595
+ * });
11479
11596
  */
11480
11597
  bw.sub = function (topic, handler, el) {
11481
11598
  var id = ++bw._subIdCounter;
@@ -11532,6 +11649,37 @@
11532
11649
  return removed;
11533
11650
  };
11534
11651
 
11652
+ /**
11653
+ * Subscribe to a topic for a single event only. The subscription is
11654
+ * automatically removed after the first publish. Equivalent to manually
11655
+ * calling unsub() inside a bw.sub() handler, but avoids the common bug
11656
+ * of forgetting to unsubscribe.
11657
+ *
11658
+ * @param {string} topic - Topic name
11659
+ * @param {Function} handler - Called once with (detail) on the next publish
11660
+ * @param {Element} [el] - Optional DOM element to tie lifecycle to
11661
+ * @returns {Function} Call to cancel the subscription before it fires
11662
+ * @category Pub/Sub
11663
+ * @see bw.sub
11664
+ * @see bw.pub
11665
+ * @example
11666
+ * bw.once('data:loaded', function(detail) {
11667
+ * console.log('Received:', detail);
11668
+ * // No need to unsubscribe -- already done automatically
11669
+ * });
11670
+ *
11671
+ * // Cancel before it fires:
11672
+ * var cancel = bw.once('timeout', handler);
11673
+ * cancel(); // handler will never be called
11674
+ */
11675
+ bw.once = function (topic, handler, el) {
11676
+ var unsub = bw.sub(topic, function (detail) {
11677
+ unsub();
11678
+ handler(detail);
11679
+ }, el);
11680
+ return unsub;
11681
+ };
11682
+
11535
11683
  // ===================================================================================
11536
11684
  // Function Registry (revived from v1 for string dispatch contexts)
11537
11685
  // ===================================================================================
@@ -11775,7 +11923,7 @@
11775
11923
  * };
11776
11924
  */
11777
11925
  bw.message = function (target, action, data) {
11778
- var el = bw._el(target);
11926
+ var el = bw.el(target);
11779
11927
  if (!el) el = bw.$('.' + target)[0];
11780
11928
  if (!el || !el.bw || typeof el.bw[action] !== 'function') {
11781
11929
  _cw('bw.message: no handle method "' + action + '" on ' + target);
@@ -11785,6 +11933,217 @@
11785
11933
  return true;
11786
11934
  };
11787
11935
 
11936
+ /**
11937
+ * Collect form data from all input, select, and textarea elements within a
11938
+ * container. Each element's `name` attribute (or `id` if no name) becomes a
11939
+ * key in the returned object. This provides a lightweight alternative to the
11940
+ * browser FormData API that returns a plain object suitable for JSON
11941
+ * serialization or bw.pub().
11942
+ *
11943
+ * Handles all standard HTML form controls:
11944
+ * - text/number/email/etc inputs: string value
11945
+ * - checkboxes: boolean (true/false)
11946
+ * - radio buttons: string value of the checked radio (unchecked groups omitted)
11947
+ * - multi-select: array of selected option values
11948
+ * - textarea: string value
11949
+ *
11950
+ * Elements without both `name` and `id` attributes are silently skipped.
11951
+ *
11952
+ * @param {string|Element} target - CSS selector, UUID string, or DOM element
11953
+ * @returns {Object} Plain object mapping field names to values
11954
+ * @category Component
11955
+ * @see bw.makeForm
11956
+ * @see bw.makeInput
11957
+ * @example
11958
+ * // Given a form with name="email" input and name="agree" checkbox:
11959
+ * var data = bw.formData('#signup-form');
11960
+ * // => { email: 'user@example.com', agree: true }
11961
+ *
11962
+ * // Collect and publish in one step:
11963
+ * bw.pub('form:submit', bw.formData('#my-form'));
11964
+ *
11965
+ * // Works with any container, not just <form>:
11966
+ * bw.pub('settings:changed', bw.formData('.settings-panel'));
11967
+ */
11968
+ bw.formData = function (target) {
11969
+ var el = bw.el(target);
11970
+ if (!el) return {};
11971
+ var result = {};
11972
+ var inputs = el.querySelectorAll('input, select, textarea');
11973
+ for (var i = 0; i < inputs.length; i++) {
11974
+ var inp = inputs[i];
11975
+ var key = inp.name || inp.id;
11976
+ if (!key) continue;
11977
+ if (inp.type === 'checkbox') {
11978
+ result[key] = inp.checked;
11979
+ } else if (inp.type === 'radio') {
11980
+ if (inp.checked) result[key] = inp.value;
11981
+ } else if (inp.tagName === 'SELECT' && inp.multiple) {
11982
+ result[key] = [];
11983
+ for (var j = 0; j < inp.options.length; j++) {
11984
+ if (inp.options[j].selected) result[key].push(inp.options[j].value);
11985
+ }
11986
+ } else {
11987
+ result[key] = inp.value;
11988
+ }
11989
+ }
11990
+ return result;
11991
+ };
11992
+
11993
+ // ===================================================================================
11994
+ // bw.jsonPatch() — RFC 6902 JSON Patch on plain objects
11995
+ // ===================================================================================
11996
+
11997
+ /**
11998
+ * Apply RFC 6902 JSON Patch operations to a plain object.
11999
+ *
12000
+ * Supported operations: add, remove, replace, move, copy, test.
12001
+ * Paths use JSON Pointer (RFC 6901) notation: `/foo/bar/0`.
12002
+ * Mutates the target object in place and returns it.
12003
+ *
12004
+ * @param {Object} obj - Target object to patch
12005
+ * @param {Array<Object>} ops - Array of patch operations
12006
+ * @param {string} ops[].op - Operation: 'add', 'remove', 'replace', 'move', 'copy', 'test'
12007
+ * @param {string} ops[].path - JSON Pointer path (e.g. '/a/b/0')
12008
+ * @param {*} [ops[].value] - Value for add/replace/test
12009
+ * @param {string} [ops[].from] - Source path for move/copy
12010
+ * @returns {Object} The patched object (same reference)
12011
+ * @throws {Error} On invalid op, missing path, test failure, or path not found for remove
12012
+ * @category Data Utilities
12013
+ * @see bw.patch
12014
+ * @example
12015
+ * var obj = { a: 1, b: { c: 2 } };
12016
+ * bw.jsonPatch(obj, [
12017
+ * { op: 'replace', path: '/a', value: 10 },
12018
+ * { op: 'add', path: '/b/d', value: 3 },
12019
+ * { op: 'remove', path: '/b/c' }
12020
+ * ]);
12021
+ * // obj => { a: 10, b: { d: 3 } }
12022
+ */
12023
+ bw.jsonPatch = function (obj, ops) {
12024
+ if (!_isA(ops)) return obj;
12025
+
12026
+ // Parse JSON Pointer path to array of keys
12027
+ function parsePath(path) {
12028
+ if (path === '') return [];
12029
+ if (path.charAt(0) !== '/') throw new Error('Invalid JSON Pointer: ' + path);
12030
+ return path.slice(1).split('/').map(function (s) {
12031
+ return s.replace(/~1/g, '/').replace(/~0/g, '~');
12032
+ });
12033
+ }
12034
+
12035
+ // Walk to parent of final key; return { parent, key }
12036
+ function resolve(root, keys) {
12037
+ var parent = root;
12038
+ for (var i = 0; i < keys.length - 1; i++) {
12039
+ var k = _isA(parent) ? parseInt(keys[i], 10) : keys[i];
12040
+ if (parent[k] === undefined) throw new Error('Path not found: /' + keys.slice(0, i + 1).join('/'));
12041
+ parent = parent[k];
12042
+ }
12043
+ return {
12044
+ parent: parent,
12045
+ key: _isA(parent) ? parseInt(keys[keys.length - 1], 10) : keys[keys.length - 1]
12046
+ };
12047
+ }
12048
+
12049
+ // Get value at path
12050
+ function getVal(root, keys) {
12051
+ var cur = root;
12052
+ for (var i = 0; i < keys.length; i++) {
12053
+ var k = _isA(cur) ? parseInt(keys[i], 10) : keys[i];
12054
+ if (cur[k] === undefined) throw new Error('Path not found: /' + keys.slice(0, i + 1).join('/'));
12055
+ cur = cur[k];
12056
+ }
12057
+ return cur;
12058
+ }
12059
+ for (var i = 0; i < ops.length; i++) {
12060
+ var op = ops[i];
12061
+ if (!op.op || !_is(op.path, 'string')) throw new Error('Invalid patch operation at index ' + i);
12062
+ var keys = parsePath(op.path);
12063
+ var r, val, fromKeys, fr, tr, cr;
12064
+ switch (op.op) {
12065
+ case 'add':
12066
+ {
12067
+ if (keys.length === 0) throw new Error('Cannot add to root');
12068
+ r = resolve(obj, keys);
12069
+ if (_isA(r.parent) && r.key <= r.parent.length) {
12070
+ r.parent.splice(r.key, 0, op.value);
12071
+ } else {
12072
+ r.parent[r.key] = op.value;
12073
+ }
12074
+ break;
12075
+ }
12076
+ case 'remove':
12077
+ {
12078
+ if (keys.length === 0) throw new Error('Cannot remove root');
12079
+ r = resolve(obj, keys);
12080
+ if (_isA(r.parent)) {
12081
+ if (r.key >= r.parent.length) throw new Error('Index out of bounds: ' + r.key);
12082
+ r.parent.splice(r.key, 1);
12083
+ } else {
12084
+ if (!(r.key in r.parent)) throw new Error('Path not found: ' + op.path);
12085
+ delete r.parent[r.key];
12086
+ }
12087
+ break;
12088
+ }
12089
+ case 'replace':
12090
+ {
12091
+ if (keys.length === 0) throw new Error('Cannot replace root');
12092
+ r = resolve(obj, keys);
12093
+ if (_isA(r.parent)) {
12094
+ if (r.key >= r.parent.length) throw new Error('Index out of bounds: ' + r.key);
12095
+ } else {
12096
+ if (!(r.key in r.parent)) throw new Error('Path not found: ' + op.path);
12097
+ }
12098
+ r.parent[r.key] = op.value;
12099
+ break;
12100
+ }
12101
+ case 'move':
12102
+ {
12103
+ if (!_is(op.from, 'string')) throw new Error('move requires "from"');
12104
+ fromKeys = parsePath(op.from);
12105
+ val = getVal(obj, fromKeys);
12106
+ fr = resolve(obj, fromKeys);
12107
+ if (_isA(fr.parent)) {
12108
+ fr.parent.splice(fr.key, 1);
12109
+ } else {
12110
+ delete fr.parent[fr.key];
12111
+ }
12112
+ tr = resolve(obj, keys);
12113
+ if (_isA(tr.parent) && tr.key <= tr.parent.length) {
12114
+ tr.parent.splice(tr.key, 0, val);
12115
+ } else {
12116
+ tr.parent[tr.key] = val;
12117
+ }
12118
+ break;
12119
+ }
12120
+ case 'copy':
12121
+ {
12122
+ if (!_is(op.from, 'string')) throw new Error('copy requires "from"');
12123
+ val = getVal(obj, parsePath(op.from));
12124
+ cr = resolve(obj, keys);
12125
+ if (_isA(cr.parent) && cr.key <= cr.parent.length) {
12126
+ cr.parent.splice(cr.key, 0, val);
12127
+ } else {
12128
+ cr.parent[cr.key] = val;
12129
+ }
12130
+ break;
12131
+ }
12132
+ case 'test':
12133
+ {
12134
+ var actual = getVal(obj, keys);
12135
+ if (JSON.stringify(actual) !== JSON.stringify(op.value)) {
12136
+ throw new Error('Test failed: ' + op.path + ' expected ' + JSON.stringify(op.value) + ' got ' + JSON.stringify(actual));
12137
+ }
12138
+ break;
12139
+ }
12140
+ default:
12141
+ throw new Error('Unknown op: ' + op.op);
12142
+ }
12143
+ }
12144
+ return obj;
12145
+ };
12146
+
11788
12147
  // ===================================================================================
11789
12148
  // bw.apply() / bw.parseJSONFlex() — Server-driven UI protocol
11790
12149
  // ===================================================================================
@@ -11917,7 +12276,7 @@
11917
12276
  var type = msg.type;
11918
12277
  var target = msg.target;
11919
12278
  if (type === 'replace') {
11920
- var el = bw._el(target);
12279
+ var el = bw.el(target);
11921
12280
  if (!el) return false;
11922
12281
  bw.DOM(el, msg.node);
11923
12282
  return true;
@@ -11925,13 +12284,13 @@
11925
12284
  var patched = bw.patch(target, msg.content, msg.attr);
11926
12285
  return patched !== null;
11927
12286
  } else if (type === 'append') {
11928
- var parent = bw._el(target);
12287
+ var parent = bw.el(target);
11929
12288
  if (!parent) return false;
11930
12289
  var child = bw.createDOM(msg.node);
11931
12290
  parent.appendChild(child);
11932
12291
  return true;
11933
12292
  } else if (type === 'remove') {
11934
- var toRemove = bw._el(target);
12293
+ var toRemove = bw.el(target);
11935
12294
  if (!toRemove) return false;
11936
12295
  if (_is(bw.cleanup, 'function')) bw.cleanup(toRemove);
11937
12296
  toRemove.remove();
@@ -11984,33 +12343,99 @@
11984
12343
  };
11985
12344
 
11986
12345
  // ===================================================================================
11987
- // bw.inspect() — Debug utility
12346
+ // bw.inspect() — DOM introspection with bitwrench metadata
11988
12347
  // ===================================================================================
11989
12348
 
11990
12349
  /**
11991
- * Inspect a DOM element's bitwrench state, handle methods, and metadata.
11992
- * Works with DOM elements or CSS selectors.
11993
- *
11994
- * @param {string|Element} target - Selector or DOM element
11995
- * @returns {Element|null} The element, or null if not found
12350
+ * Inspect a DOM element and its subtree, returning a plain-object
12351
+ * representation with bitwrench metadata at each node. Useful for debugging,
12352
+ * devtools, MCP/AG-UI tool discovery, and automated testing.
12353
+ *
12354
+ * Each node in the returned tree includes:
12355
+ * - `tag` -- lowercase tag name (or '#text' for text nodes)
12356
+ * - `id` -- element id (if set)
12357
+ * - `uuid` -- bitwrench UUID class (if lifecycle-managed)
12358
+ * - `type` -- component type from o.type (if set, e.g. 'card', 'tabs')
12359
+ * - `classes` -- first 5 CSS classes (string, space-separated)
12360
+ * - `handles` -- array of el.bw method names (if any)
12361
+ * - `state` -- copy of _bw_state (if any)
12362
+ * - `hasRender` -- true if _bw_render is set
12363
+ * - `hasSubs` -- true if element has pub/sub subscriptions
12364
+ * - `refs` -- copy of _bw_refs keys (if any)
12365
+ * - `children` -- array of child node trees (up to depth limit, max 50 per level)
12366
+ *
12367
+ * @param {string|Element} target - CSS selector, UUID, or DOM element
12368
+ * @param {number} [depth=3] - Maximum recursion depth (0 = target only, no children)
12369
+ * @returns {Object|null} Plain object tree, or null if element not found
11996
12370
  * @category Component
11997
12371
  * @example
11998
- * bw.inspect('#my-carousel');
11999
- * bw.inspect($0);
12372
+ * // Get full tree from #app, 3 levels deep (default):
12373
+ * var info = bw.inspect('#app');
12374
+ *
12375
+ * // Shallow inspection (just the element, no children):
12376
+ * var info = bw.inspect('#my-carousel', 0);
12377
+ * console.log(info.handles); // ['next', 'prev', 'goToSlide']
12378
+ * console.log(info.type); // 'carousel'
12379
+ *
12380
+ * // Deep inspection for debugging:
12381
+ * console.log(JSON.stringify(bw.inspect('#app', 5), null, 2));
12000
12382
  */
12001
- bw.inspect = function (target) {
12002
- var el = _is(target, 'string') ? bw.$(target)[0] : target;
12003
- if (!el) {
12004
- _cw('bw.inspect: element not found');
12005
- return null;
12383
+ bw.inspect = function (target, depth) {
12384
+ var el = bw.el(target);
12385
+ if (!el && _is(target, 'string')) el = bw.$(target)[0];
12386
+ if (!el) return null;
12387
+ if (depth === undefined || depth === null) depth = 3;
12388
+ function walk(node, d) {
12389
+ if (!node) return null;
12390
+ // Skip non-element nodes (text, comment, etc.)
12391
+ if (node.nodeType !== 1) return null;
12392
+ var info = {
12393
+ tag: node.tagName ? node.tagName.toLowerCase() : '#text'
12394
+ };
12395
+
12396
+ // Identity
12397
+ if (node.id) info.id = node.id;
12398
+ var uuid = bw.getUUID(node);
12399
+ if (uuid) info.uuid = uuid;
12400
+ if (node._bw_type) info.type = node._bw_type;
12401
+
12402
+ // CSS classes (first 5 for readability)
12403
+ if (node.className && typeof node.className === 'string') {
12404
+ info.classes = node.className.split(' ').slice(0, 5).join(' ');
12405
+ }
12406
+
12407
+ // Bitwrench handle methods
12408
+ if (node.bw) {
12409
+ var handles = _keys(node.bw);
12410
+ if (handles.length > 0) info.handles = handles;
12411
+ }
12412
+
12413
+ // State
12414
+ if (node._bw_state) info.state = node._bw_state;
12415
+ if (node._bw_render) info.hasRender = true;
12416
+ if (node._bw_subs && node._bw_subs.length > 0) info.hasSubs = true;
12417
+
12418
+ // Refs
12419
+ if (node._bw_refs) info.refs = _keys(node._bw_refs);
12420
+
12421
+ // Children (recurse up to depth limit, max 50 children per level)
12422
+ if (d < depth && node.children && node.children.length > 0) {
12423
+ info.children = [];
12424
+ var max = Math.min(node.children.length, 50);
12425
+ for (var i = 0; i < max; i++) {
12426
+ var child = walk(node.children[i], d + 1);
12427
+ if (child) info.children.push(child);
12428
+ }
12429
+ if (node.children.length > 50) {
12430
+ info.children.push({
12431
+ tag: '...',
12432
+ count: node.children.length - 50
12433
+ });
12434
+ }
12435
+ }
12436
+ return info;
12006
12437
  }
12007
- console.group('Element: ' + (bw.getUUID(el) || el.id || el.tagName));
12008
- _cl('State:', el._bw_state || '(none)');
12009
- _cl('Handle:', el.bw ? _keys(el.bw) : '(none)');
12010
- _cl('Classes:', el.className);
12011
- _cl('Refs:', el._bw_refs || '(none)');
12012
- console.groupEnd();
12013
- return el;
12438
+ return walk(el, 0);
12014
12439
  };
12015
12440
  bw.compile = function () {
12016
12441
  throw new Error('bw.compile() removed in v2.0.19. Use o.handle/o.slots on TACO options instead.');
@@ -12249,34 +12674,45 @@
12249
12674
  * so you can use `.map()`, `.filter()`, etc. directly. Accepts CSS selectors,
12250
12675
  * single elements, NodeLists, or arrays.
12251
12676
  *
12677
+ * With an optional second argument, applies content or a function to
12678
+ * every matched element (same apply rules as `bw.el()`):
12679
+ * - string/number: sets `el.textContent`
12680
+ * - function: calls `apply(el)` for each element
12681
+ * - TACO object: clears children, mounts TACO via `bw.createDOM()`
12682
+ * - array: clears children, appends each item
12683
+ *
12252
12684
  * @param {string|Element|Array} selector - CSS selector, element, or array
12685
+ * @param {string|number|Function|Object|Array} [apply] - Content or function to apply
12253
12686
  * @returns {Array} Array of DOM elements
12254
12687
  * @category DOM Selection
12688
+ * @see bw.el
12255
12689
  * @example
12256
- * bw.$('.card') // => [div.card, div.card, ...]
12257
- * bw.$(myElement) // => [myElement]
12258
- * bw.$('.card').map(el => el.textContent)
12690
+ * bw.$('.card') // => [div.card, div.card, ...]
12691
+ * bw.$('.status', 'Online') // set text on all .status elements
12692
+ * bw.$('.card', function(el) { // apply function to each
12693
+ * el.style.opacity = '0.5';
12694
+ * })
12259
12695
  */
12260
12696
  if (bw._isBrowser) {
12261
- bw.$ = function (selector) {
12262
- if (!selector) return [];
12263
-
12264
- // Already an array
12265
- if (_isA(selector)) return selector;
12266
-
12267
- // Single element
12268
- if (selector.nodeType) return [selector];
12269
-
12270
- // NodeList or HTMLCollection
12271
- if (selector.length !== undefined && !_is(selector, 'string')) {
12272
- return Array.from(selector);
12697
+ bw.$ = function (selector, apply) {
12698
+ var els;
12699
+ if (!selector) {
12700
+ els = [];
12701
+ } else if (_isA(selector)) {
12702
+ els = selector;
12703
+ } else if (selector.nodeType) {
12704
+ els = [selector];
12705
+ } else if (selector.length !== undefined && !_is(selector, 'string')) {
12706
+ els = Array.from(selector);
12707
+ } else if (_is(selector, 'string')) {
12708
+ els = Array.from(document.querySelectorAll(selector));
12709
+ } else {
12710
+ els = [];
12273
12711
  }
12274
-
12275
- // CSS selector string
12276
- if (_is(selector, 'string')) {
12277
- return Array.from(document.querySelectorAll(selector));
12712
+ if (apply !== undefined) {
12713
+ for (var i = 0; i < els.length; i++) _applyTo(els[i], apply);
12278
12714
  }
12279
- return [];
12715
+ return els;
12280
12716
  };
12281
12717
 
12282
12718
  // Convenience single element selector
@@ -12495,41 +12931,47 @@
12495
12931
  };
12496
12932
 
12497
12933
  /**
12498
- * Toggle between primary and alternate palettes.
12934
+ * Toggle between primary and alternate theme palettes.
12499
12935
  *
12500
- * Adds/removes the `bw_theme_alt` class on the scoping element.
12936
+ * Adds/removes the `bw_theme_alt` class on the scoping element(s).
12501
12937
  * Without a scope, toggles on `<html>` (global).
12502
- * With a scope, toggles on the first matching element.
12938
+ * With a scope, toggles on ALL matching elements.
12503
12939
  *
12504
- * @param {string} [scope] - Scope selector (e.g. '#my-dashboard'). Omit for global.
12505
- * @returns {string} Active mode after toggle: 'primary' or 'alternate'
12940
+ * @param {string|Element} [scope] - Selector or element. Omit for global.
12941
+ * @returns {string} Active mode after toggle: 'primary' or 'alternate' (based on first element)
12506
12942
  * @category CSS & Styling
12507
12943
  * @see bw.applyStyles
12508
12944
  * @see bw.clearStyles
12509
12945
  * @example
12510
- * bw.toggleStyles(); // global toggle on <html>
12511
- * bw.toggleStyles('#my-dashboard'); // scoped toggle
12946
+ * bw.toggleThemeMode(); // global toggle on <html>
12947
+ * bw.toggleThemeMode('#my-dashboard'); // scoped toggle
12948
+ * bw.toggleThemeMode('.panel'); // toggle on ALL .panel elements
12512
12949
  */
12513
- bw.toggleStyles = function (scope) {
12950
+ bw.toggleThemeMode = function (scope) {
12514
12951
  if (!bw._isBrowser) return 'primary';
12515
- var target;
12952
+ var els;
12516
12953
  if (scope) {
12517
- var els = bw.$(scope);
12518
- target = els[0];
12519
- } else {
12520
- target = document.documentElement;
12521
- }
12522
- if (!target) return 'primary';
12523
- var hasAlt = target.classList.contains('bw_theme_alt');
12524
- if (hasAlt) {
12525
- target.classList.remove('bw_theme_alt');
12526
- return 'primary';
12954
+ els = bw.$(scope);
12527
12955
  } else {
12528
- target.classList.add('bw_theme_alt');
12529
- return 'alternate';
12956
+ els = [document.documentElement];
12957
+ }
12958
+ if (!els.length) return 'primary';
12959
+ var mode;
12960
+ for (var i = 0; i < els.length; i++) {
12961
+ var hasAlt = els[i].classList.contains('bw_theme_alt');
12962
+ if (hasAlt) {
12963
+ els[i].classList.remove('bw_theme_alt');
12964
+ } else {
12965
+ els[i].classList.add('bw_theme_alt');
12966
+ }
12967
+ if (i === 0) mode = hasAlt ? 'primary' : 'alternate';
12530
12968
  }
12969
+ return mode;
12531
12970
  };
12532
12971
 
12972
+ // Alias — kept for one release cycle. Use bw.toggleThemeMode() instead.
12973
+ bw.toggleStyles = bw.toggleThemeMode;
12974
+
12533
12975
  /**
12534
12976
  * Remove injected styles for a given scope.
12535
12977
  *
@@ -13629,6 +14071,57 @@
13629
14071
  }
13630
14072
  });
13631
14073
 
14074
+ /**
14075
+ * Query the BCCL component registry. Returns metadata about registered
14076
+ * component types -- their names and factory function names. Useful for
14077
+ * tooling, introspection, documentation generators, and auto-complete
14078
+ * systems (including MCP/AG-UI tool discovery).
14079
+ *
14080
+ * With no arguments, returns an array of all registered component types.
14081
+ * With a type name, returns metadata for that single type (or null if
14082
+ * the type is not registered).
14083
+ *
14084
+ * @param {string} [type] - Optional component type name to look up
14085
+ * @returns {Array<Object>|Object|null} Array of {type, factory} objects,
14086
+ * a single {type, factory} object, or null if the type is not found
14087
+ * @category Component
14088
+ * @see bw.make
14089
+ * @see bw.BCCL
14090
+ * @example
14091
+ * // List all available component types:
14092
+ * bw.catalog();
14093
+ * // => [{ type: 'card', factory: 'makeCard' },
14094
+ * // { type: 'button', factory: 'makeButton' }, ...]
14095
+ *
14096
+ * // Look up a specific type:
14097
+ * bw.catalog('accordion');
14098
+ * // => { type: 'accordion', factory: 'makeAccordion' }
14099
+ *
14100
+ * // Check if a type exists:
14101
+ * if (bw.catalog('chart')) { ... }
14102
+ *
14103
+ * // Get just the type names:
14104
+ * bw.catalog().map(function(c) { return c.type; });
14105
+ * // => ['card', 'button', 'container', 'row', ...]
14106
+ */
14107
+ bw.catalog = function (type) {
14108
+ if (type) {
14109
+ var def = bw.BCCL[type];
14110
+ if (!def) return null;
14111
+ return {
14112
+ type: type,
14113
+ factory: def.make.name || 'make' + type.charAt(0).toUpperCase() + type.slice(1)
14114
+ };
14115
+ }
14116
+ return Object.keys(bw.BCCL).map(function (k) {
14117
+ var def = bw.BCCL[k];
14118
+ return {
14119
+ type: k,
14120
+ factory: def.make.name || 'make' + k.charAt(0).toUpperCase() + k.slice(1)
14121
+ };
14122
+ });
14123
+ };
14124
+
13632
14125
  // Also attach to global in browsers
13633
14126
  if (bw._isBrowser && typeof window !== 'undefined') {
13634
14127
  window.bw = bw;