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
package/src/bitwrench.js CHANGED
@@ -314,61 +314,105 @@ bw.uuid = function(prefix) {
314
314
  };
315
315
 
316
316
  /**
317
- * Look up a DOM element by ID string, using the node cache for O(1) access.
317
+ * Look up a single DOM element by ID, CSS selector, UUID, or element ref.
318
+ * Optionally apply content or a function to the resolved element.
318
319
  *
319
- * Resolution order:
320
- * 1. Check `bw._nodeMap[id]` if found and still attached (parentNode !== null), return it
321
- * 2. If cached ref is detached (parentNode === null), remove stale entry
322
- * 3. Fall back to `document.getElementById(id)` then `document.querySelector(...)`
323
- * 4. Try class-based lookup for bw_uuid_* tokens (UUID addressing)
324
- * 5. Cache the result for next time
320
+ * Resolution order for string targets:
321
+ * 1. Check `bw._nodeMap[id]` cache (O(1), stale entries auto-pruned)
322
+ * 2. `document.getElementById(id)`
323
+ * 3. `document.querySelector(id)` for selectors starting with # or .
324
+ * 4. Class-based lookup for `bw_uuid_*` tokens
325
325
  *
326
- * Accepts a DOM element directly (pass-through) or a string identifier.
327
- * String identifiers are tried as: direct map key, getElementById,
328
- * querySelector (for CSS selectors starting with . or #), and
329
- * bw_uuid_* class selector.
326
+ * With one argument, returns the element (or null). With two arguments,
327
+ * applies the second argument to the element and returns the element:
328
+ * - string/number: sets `el.textContent`
329
+ * - function: calls `apply(el)`, returns el
330
+ * - TACO object: clears children, mounts TACO via `bw.createDOM()`
331
+ * - array: clears children, appends each item (string -> text node, TACO -> element)
330
332
  *
331
- * @param {string|Element} id - Element ID, CSS selector, bw_uuid_* class, or DOM element
333
+ * @param {string|Element} target - Element ref, ID, CSS selector, or bw_uuid_* class
334
+ * @param {string|number|Function|Object|Array} [apply] - Content or function to apply
332
335
  * @returns {Element|null} The DOM element, or null if not found
333
- * @category Internal
336
+ * @category DOM Selection
337
+ * @see bw.$
338
+ * @see bw.patch
339
+ * @example
340
+ * bw.el('#title') // lookup
341
+ * bw.el('#title', 'Hello') // set text content
342
+ * bw.el('#app', { t: 'h1', c: 'Hi' }) // mount TACO
343
+ * bw.el('.card', function(el) { // apply function
344
+ * el.style.opacity = '0.5';
345
+ * })
334
346
  */
335
- bw._el = function(id) {
336
- // Pass-through for DOM elements
337
- if (!_is(id, 'string')) return id || null;
338
- if (!id) return null;
339
- if (!bw._isBrowser) return null;
340
-
341
- // 1. Check cache
342
- var cached = bw._nodeMap[id];
343
- if (cached) {
344
- // Verify not detached (parentNode check is IE11-safe)
345
- if (cached.parentNode !== null) {
346
- return cached;
347
+ bw.el = function(target, apply) {
348
+ // Resolve target to element
349
+ var el;
350
+ if (!_is(target, 'string')) {
351
+ el = target || null;
352
+ } else if (!target || !bw._isBrowser) {
353
+ el = null;
354
+ } else {
355
+ // 1. Check cache
356
+ var cached = bw._nodeMap[target];
357
+ if (cached) {
358
+ if (cached.parentNode !== null) {
359
+ el = cached;
360
+ } else {
361
+ delete bw._nodeMap[target];
362
+ }
363
+ }
364
+ if (!el) {
365
+ // 2. getElementById
366
+ el = document.getElementById(target);
367
+ // 3. querySelector for CSS selectors
368
+ if (!el && (target.charAt(0) === '#' || target.charAt(0) === '.')) {
369
+ el = document.querySelector(target);
370
+ }
371
+ // 4. bw_uuid_* class lookup
372
+ if (!el && target.indexOf('bw_uuid_') === 0) {
373
+ el = document.querySelector('.' + target);
374
+ }
375
+ // 5. Cache result
376
+ if (el) bw._nodeMap[target] = el;
347
377
  }
348
- // Stale — remove and fall through
349
- delete bw._nodeMap[id];
350
378
  }
351
379
 
352
- // 2. DOM fallback: try getElementById first (fastest native lookup)
353
- var el = document.getElementById(id);
354
-
355
- // 3. Try querySelector for CSS selectors (starts with # or .)
356
- if (!el && (id.charAt(0) === '#' || id.charAt(0) === '.')) {
357
- el = document.querySelector(id);
358
- }
380
+ // Apply (if provided and element found)
381
+ if (el && apply !== undefined) _applyTo(el, apply);
359
382
 
360
- // 4. Try class-based lookup for bw_uuid_* tokens (UUID addressing)
361
- if (!el && id.indexOf('bw_uuid_') === 0) {
362
- el = document.querySelector('.' + id);
363
- }
383
+ return el;
384
+ };
364
385
 
365
- // 5. Cache the result for next time
366
- if (el) {
367
- bw._nodeMap[id] = el;
386
+ /**
387
+ * Internal: apply content or function to a DOM element.
388
+ * Shared by bw.el() and bw.$().
389
+ * @private
390
+ */
391
+ function _applyTo(el, apply) {
392
+ if (_is(apply, 'function')) {
393
+ apply(el);
394
+ } else if (_isA(apply)) {
395
+ el.innerHTML = '';
396
+ apply.forEach(function(item) {
397
+ if (item != null) {
398
+ if (_is(item, 'object') && item.t) {
399
+ el.appendChild(bw.createDOM(item));
400
+ } else {
401
+ el.appendChild(document.createTextNode(String(item)));
402
+ }
403
+ }
404
+ });
405
+ } else if (_is(apply, 'object') && apply !== null && apply.t) {
406
+ el.innerHTML = '';
407
+ el.appendChild(bw.createDOM(apply));
408
+ } else {
409
+ el.textContent = String(apply);
368
410
  }
411
+ }
369
412
 
370
- return el;
371
- };
413
+ // Internal alias — kept for one release cycle (v2.0.26).
414
+ // Will be removed in v2.0.27. Use bw.el() instead.
415
+ bw._el = bw.el;
372
416
 
373
417
  /**
374
418
  * Register a DOM element in the node cache under one or more keys.
@@ -432,6 +476,12 @@ var _BW_LC = 'bw_lc';
432
476
  */
433
477
  var _UUID_RE = /\bbw_uuid_[a-z0-9_]+\b/;
434
478
 
479
+ /**
480
+ * SVG namespace URI for createElementNS.
481
+ * @private
482
+ */
483
+ var _SVG_NS = 'http://www.w3.org/2000/svg';
484
+
435
485
  /**
436
486
  * Assign a UUID to a TACO object by appending a `bw_uuid_*` token to `taco.a.class`.
437
487
  *
@@ -486,9 +536,10 @@ bw.getUUID = function(tacoOrElement) {
486
536
  if (!tacoOrElement) return null;
487
537
 
488
538
  var classStr;
489
- // DOM element: check className
539
+ // DOM element: check className (SVG elements use getAttribute for string value)
490
540
  if (tacoOrElement.className !== undefined && tacoOrElement.tagName) {
491
- classStr = tacoOrElement.className;
541
+ classStr = typeof tacoOrElement.className === 'string'
542
+ ? tacoOrElement.className : (tacoOrElement.getAttribute('class') || '');
492
543
  }
493
544
  // TACO object: check a.class
494
545
  else if (tacoOrElement.a && _is(tacoOrElement.a.class, 'string')) {
@@ -757,7 +808,7 @@ bw.htmlPage = function(opts) {
757
808
  var fnCounterBefore = bw._fnIDCounter;
758
809
 
759
810
  // Render body content
760
- var bodyHTML = '';
811
+ var bodyHTML;
761
812
  if (_is(body, 'string')) {
762
813
  bodyHTML = body;
763
814
  } else {
@@ -928,9 +979,11 @@ bw.createDOM = function(taco, options = {}) {
928
979
  }
929
980
 
930
981
  const { t: tag, a: attrs = {}, c: content, o: opts = {} } = taco;
931
-
932
- // Create element
933
- const el = document.createElement(tag);
982
+
983
+ // SVG namespace: detect SVG context and thread through children.
984
+ // {t:'svg'} starts SVG context; foreignObject children revert to HTML.
985
+ var svgCtx = options._svgCtx || (tag === 'svg');
986
+ var el = svgCtx ? document.createElementNS(_SVG_NS, tag) : document.createElement(tag);
934
987
 
935
988
  // Set attributes
936
989
  for (const [key, value] of Object.entries(attrs)) {
@@ -941,9 +994,11 @@ bw.createDOM = function(taco, options = {}) {
941
994
  Object.assign(el.style, value);
942
995
  } else if (key === 'class') {
943
996
  // Handle class as array or string
997
+ // SVG elements use SVGAnimatedString for className, so use setAttribute
944
998
  const classStr = _isA(value) ? value.filter(Boolean).join(' ') : String(value);
945
999
  if (classStr) {
946
- el.className = classStr;
1000
+ if (svgCtx) el.setAttribute('class', classStr);
1001
+ else el.className = classStr;
947
1002
  }
948
1003
  } else if (key.startsWith('on') && _is(value, 'function')) {
949
1004
  // Event handlers
@@ -964,11 +1019,17 @@ bw.createDOM = function(taco, options = {}) {
964
1019
  // Add children, building _bw_refs for fast parent→child access.
965
1020
  // Children with id attributes or bw_uuid_* classes get local refs on the parent,
966
1021
  // so o.render functions can access them without any DOM lookup.
1022
+ // SVG: foreignObject children revert to HTML namespace; otherwise inherit.
1023
+ var childOpts = options;
1024
+ var childSvgCtx = svgCtx && tag !== 'foreignObject';
1025
+ if (childSvgCtx !== (options._svgCtx || false)) {
1026
+ childOpts = Object.assign({}, options, {_svgCtx: childSvgCtx || undefined});
1027
+ }
967
1028
  if (content != null) {
968
1029
  if (_isA(content)) {
969
1030
  content.forEach(child => {
970
1031
  if (child != null) {
971
- var childEl = bw.createDOM(child, options);
1032
+ var childEl = bw.createDOM(child, childOpts);
972
1033
  el.appendChild(childEl);
973
1034
  // Build local refs for addressable children
974
1035
  var childRefId = (child && child.a) ? (child.a.id || bw.getUUID(child)) : null;
@@ -991,7 +1052,7 @@ bw.createDOM = function(taco, options = {}) {
991
1052
  // Raw HTML content — inject via innerHTML
992
1053
  el.innerHTML = content.v;
993
1054
  } else if (_is(content, 'object') && content.t) {
994
- var childEl = bw.createDOM(content, options);
1055
+ var childEl = bw.createDOM(content, childOpts);
995
1056
  el.appendChild(childEl);
996
1057
  var childRefId = content.a ? (content.a.id || bw.getUUID(content)) : null;
997
1058
  if (childRefId) {
@@ -1017,13 +1078,21 @@ bw.createDOM = function(taco, options = {}) {
1017
1078
  }
1018
1079
 
1019
1080
  // Register UUID class in node cache (bw_uuid_* tokens in class string)
1020
- if (el.className) {
1021
- var uuidMatch = el.className.match(_UUID_RE);
1081
+ // SVG elements have SVGAnimatedString for className; use getAttribute instead
1082
+ var clsStr = svgCtx ? (el.getAttribute('class') || '') : el.className;
1083
+ if (clsStr) {
1084
+ var uuidMatch = clsStr.match(_UUID_RE);
1022
1085
  if (uuidMatch) {
1023
1086
  bw._nodeMap[uuidMatch[0]] = el;
1024
1087
  }
1025
1088
  }
1026
1089
 
1090
+ // Store component type metadata (e.g., 'card', 'tabs') for introspection.
1091
+ // BCCL factories set o.type; custom components can too.
1092
+ if (opts.type) {
1093
+ el._bw_type = opts.type;
1094
+ }
1095
+
1027
1096
  // Handle lifecycle hooks and state
1028
1097
  if (opts.mounted || opts.unmount || opts.render || opts.state) {
1029
1098
  // Ensure element has a UUID class for identity
@@ -1053,11 +1122,13 @@ bw.createDOM = function(taco, options = {}) {
1053
1122
 
1054
1123
  if (mountFn) {
1055
1124
  if (document.body.contains(el)) {
1056
- mountFn(el, el._bw_state || {});
1125
+ try { mountFn(el, el._bw_state || {}); }
1126
+ catch (e) { _cw('o.mounted error: ' + e.message); }
1057
1127
  } else {
1058
1128
  requestAnimationFrame(() => {
1059
1129
  if (document.body.contains(el)) {
1060
- mountFn(el, el._bw_state || {});
1130
+ try { mountFn(el, el._bw_state || {}); }
1131
+ catch (e) { _cw('o.mounted error: ' + e.message); }
1061
1132
  }
1062
1133
  });
1063
1134
  }
@@ -1066,7 +1137,8 @@ bw.createDOM = function(taco, options = {}) {
1066
1137
  // Store unmount callback keyed by UUID class
1067
1138
  if (opts.unmount) {
1068
1139
  bw._unmountCallbacks.set(uuid, () => {
1069
- opts.unmount(el, el._bw_state || {});
1140
+ try { opts.unmount(el, el._bw_state || {}); }
1141
+ catch (e) { _cw('o.unmount error: ' + e.message); }
1070
1142
  });
1071
1143
  }
1072
1144
  }
@@ -1085,24 +1157,25 @@ bw.createDOM = function(taco, options = {}) {
1085
1157
  }
1086
1158
 
1087
1159
  // Slot declarations: auto-generate setX/getX pairs
1160
+ // The target element is cached at creation time to avoid repeated
1161
+ // querySelector calls on every get/set invocation.
1088
1162
  if (opts.slots) {
1089
1163
  for (var sk in opts.slots) {
1090
1164
  if (_hop.call(opts.slots, sk)) {
1091
1165
  (function(name, selector) {
1166
+ var target = el.querySelector(selector);
1092
1167
  var cap = name.charAt(0).toUpperCase() + name.slice(1);
1093
1168
  el.bw['set' + cap] = function(value) {
1094
- var t = el.querySelector(selector);
1095
- if (!t) return;
1169
+ if (!target) return;
1096
1170
  if (value != null && typeof value === 'object' && value.t) {
1097
- t.innerHTML = '';
1098
- t.appendChild(bw.createDOM(value));
1171
+ target.innerHTML = '';
1172
+ target.appendChild(bw.createDOM(value));
1099
1173
  } else {
1100
- t.textContent = (value != null) ? String(value) : '';
1174
+ target.textContent = (value != null) ? String(value) : '';
1101
1175
  }
1102
1176
  };
1103
1177
  el.bw['get' + cap] = function() {
1104
- var t = el.querySelector(selector);
1105
- return t ? t.textContent : '';
1178
+ return target ? target.textContent : '';
1106
1179
  };
1107
1180
  })(sk, opts.slots[sk]);
1108
1181
  }
@@ -1143,7 +1216,7 @@ bw.DOM = function(target, taco, options = {}) {
1143
1216
  }
1144
1217
 
1145
1218
  // Get target element (use cache-backed lookup)
1146
- const targetEl = bw._el(target);
1219
+ const targetEl = bw.el(target);
1147
1220
 
1148
1221
  if (!targetEl) {
1149
1222
  _ce('bw.DOM: Target element not found:', target);
@@ -1246,7 +1319,8 @@ bw.cleanup = function(element) {
1246
1319
  // Deregister UUID classes from node cache for non-lifecycle UUID elements
1247
1320
  var uuidEls = element.querySelectorAll('[class*="bw_uuid_"]');
1248
1321
  uuidEls.forEach(function(uel) {
1249
- var m = uel.className && uel.className.match(_UUID_RE);
1322
+ var uc = typeof uel.className === 'string' ? uel.className : (uel.getAttribute('class') || '');
1323
+ var m = uc && uc.match(_UUID_RE);
1250
1324
  if (m) delete bw._nodeMap[m[0]];
1251
1325
  });
1252
1326
 
@@ -1332,9 +1406,10 @@ bw.cleanup = function(element) {
1332
1406
  * bw.update(el); // re-renders, emits bw:statechange
1333
1407
  */
1334
1408
  bw.update = function(target) {
1335
- var el = bw._el(target);
1409
+ var el = bw.el(target);
1336
1410
  if (el && el._bw_render) {
1337
- el._bw_render(el, el._bw_state || {});
1411
+ try { el._bw_render(el, el._bw_state || {}); }
1412
+ catch (e) { _cw('o.render error: ' + e.message); }
1338
1413
  bw.emit(el, 'statechange', el._bw_state);
1339
1414
  }
1340
1415
  return el || null;
@@ -1361,7 +1436,7 @@ bw.update = function(target) {
1361
1436
  * bw.patch('info', { t: 'em', c: 'new' }); // replace children with TACO
1362
1437
  */
1363
1438
  bw.patch = function(id, content, attr) {
1364
- var el = bw._el(id);
1439
+ var el = bw.el(id);
1365
1440
  if (!el) return null;
1366
1441
 
1367
1442
  if (attr) {
@@ -1433,7 +1508,7 @@ bw.patchAll = function(patches) {
1433
1508
  * // Dispatches CustomEvent 'bw:statechange' on the element
1434
1509
  */
1435
1510
  bw.emit = function(target, eventName, detail) {
1436
- var el = bw._el(target);
1511
+ var el = bw.el(target);
1437
1512
  if (el) {
1438
1513
  el.dispatchEvent(new CustomEvent('bw:' + eventName, {
1439
1514
  bubbles: true,
@@ -1462,7 +1537,7 @@ bw.emit = function(target, eventName, detail) {
1462
1537
  * });
1463
1538
  */
1464
1539
  bw.on = function(target, eventName, handler) {
1465
- var el = bw._el(target);
1540
+ var el = bw.el(target);
1466
1541
  if (el) {
1467
1542
  el.addEventListener('bw:' + eventName, function(e) {
1468
1543
  handler(e.detail, e);
@@ -1489,23 +1564,38 @@ bw.on = function(target, eventName, handler) {
1489
1564
  *
1490
1565
  * @param {string} topic - Topic name (plain string, no prefix)
1491
1566
  * @param {*} [detail] - Data to pass to subscribers
1492
- * @returns {number} Count of successfully called subscribers
1567
+ * @returns {number} Count of successfully called subscribers (including wildcard matches)
1493
1568
  * @category Pub/Sub
1494
1569
  * @see bw.sub
1495
1570
  * @example
1496
1571
  * bw.pub('score:updated', { player: 'X', score: 10 });
1572
+ * // Wildcard subscribers matching 'score:*' will also fire
1497
1573
  */
1498
1574
  bw.pub = function(topic, detail) {
1499
- var subs = bw._topics[topic];
1500
- if (!subs || subs.length === 0) return 0;
1501
- var snapshot = subs.slice(); // safe against unsub during iteration
1502
1575
  var called = 0;
1503
- for (var i = 0; i < snapshot.length; i++) {
1504
- try {
1505
- snapshot[i].handler(detail);
1506
- called++;
1507
- } catch (err) {
1508
- _cw('bw.pub: subscriber error on topic "' + topic + '":', err);
1576
+ // Exact-match subscribers
1577
+ var subs = bw._topics[topic];
1578
+ if (subs && subs.length > 0) {
1579
+ var snapshot = subs.slice();
1580
+ for (var i = 0; i < snapshot.length; i++) {
1581
+ try { snapshot[i].handler(detail, topic); called++; }
1582
+ catch (err) { _cw('bw.pub: subscriber error on topic "' + topic + '":', err); }
1583
+ }
1584
+ }
1585
+ // Wildcard subscribers -- patterns ending with '*'
1586
+ var keys = Object.keys(bw._topics);
1587
+ for (var k = 0; k < keys.length; k++) {
1588
+ var pat = keys[k];
1589
+ if (pat.charAt(pat.length - 1) !== '*') continue;
1590
+ var prefix = pat.slice(0, -1); // strip trailing '*'
1591
+ if (topic.length >= prefix.length && topic.substring(0, prefix.length) === prefix && topic !== pat) {
1592
+ var wsubs = bw._topics[pat];
1593
+ if (!wsubs) continue;
1594
+ var wsnap = wsubs.slice();
1595
+ for (var w = 0; w < wsnap.length; w++) {
1596
+ try { wsnap[w].handler(detail, topic); called++; }
1597
+ catch (err) { _cw('bw.pub: wildcard subscriber error on "' + pat + '" for topic "' + topic + '":', err); }
1598
+ }
1509
1599
  }
1510
1600
  }
1511
1601
  return called;
@@ -1514,12 +1604,17 @@ bw.pub = function(topic, detail) {
1514
1604
  /**
1515
1605
  * Subscribe to a topic. Returns an unsub() function.
1516
1606
  *
1517
- * Optional third argument ties the subscription to a DOM element's lifecycle —
1607
+ * Supports wildcard patterns: a topic ending in `*` matches any published
1608
+ * topic that starts with the prefix before the `*`. For example,
1609
+ * `'agui:*'` matches `'agui:ready'`, `'agui:error'`, etc. The handler
1610
+ * receives `(detail, topic)` so it can distinguish which topic fired.
1611
+ *
1612
+ * Optional third argument ties the subscription to a DOM element's lifecycle --
1518
1613
  * when `bw.cleanup()` is called on that element, the subscription is automatically
1519
1614
  * removed, preventing memory leaks.
1520
1615
  *
1521
- * @param {string} topic - Topic name
1522
- * @param {Function} handler - Called with (detail) on each publish
1616
+ * @param {string} topic - Topic name, or wildcard pattern ending in '*'
1617
+ * @param {Function} handler - Called with (detail, topic) on each publish
1523
1618
  * @param {Element} [el] - Optional DOM element to tie lifecycle to
1524
1619
  * @returns {Function} Call to unsubscribe
1525
1620
  * @category Pub/Sub
@@ -1530,6 +1625,11 @@ bw.pub = function(topic, detail) {
1530
1625
  * console.log(detail.player, 'scored', detail.score);
1531
1626
  * });
1532
1627
  * // Later: unsub() to stop listening
1628
+ *
1629
+ * // Wildcard: listen to all 'agui:' topics
1630
+ * bw.sub('agui:*', function(detail, topic) {
1631
+ * console.log('Got', topic, detail);
1632
+ * });
1533
1633
  */
1534
1634
  bw.sub = function(topic, handler, el) {
1535
1635
  var id = ++bw._subIdCounter;
@@ -1581,6 +1681,37 @@ bw.unsub = function(topic, handler) {
1581
1681
  return removed;
1582
1682
  };
1583
1683
 
1684
+ /**
1685
+ * Subscribe to a topic for a single event only. The subscription is
1686
+ * automatically removed after the first publish. Equivalent to manually
1687
+ * calling unsub() inside a bw.sub() handler, but avoids the common bug
1688
+ * of forgetting to unsubscribe.
1689
+ *
1690
+ * @param {string} topic - Topic name
1691
+ * @param {Function} handler - Called once with (detail) on the next publish
1692
+ * @param {Element} [el] - Optional DOM element to tie lifecycle to
1693
+ * @returns {Function} Call to cancel the subscription before it fires
1694
+ * @category Pub/Sub
1695
+ * @see bw.sub
1696
+ * @see bw.pub
1697
+ * @example
1698
+ * bw.once('data:loaded', function(detail) {
1699
+ * console.log('Received:', detail);
1700
+ * // No need to unsubscribe -- already done automatically
1701
+ * });
1702
+ *
1703
+ * // Cancel before it fires:
1704
+ * var cancel = bw.once('timeout', handler);
1705
+ * cancel(); // handler will never be called
1706
+ */
1707
+ bw.once = function(topic, handler, el) {
1708
+ var unsub = bw.sub(topic, function(detail) {
1709
+ unsub();
1710
+ handler(detail);
1711
+ }, el);
1712
+ return unsub;
1713
+ };
1714
+
1584
1715
  // ===================================================================================
1585
1716
  // Function Registry (revived from v1 for string dispatch contexts)
1586
1717
  // ===================================================================================
@@ -1821,7 +1952,7 @@ bw.component = function() { throw new Error('bw.component() removed in v2.0.19.
1821
1952
  * };
1822
1953
  */
1823
1954
  bw.message = function(target, action, data) {
1824
- var el = bw._el(target);
1955
+ var el = bw.el(target);
1825
1956
  if (!el) el = bw.$('.' + target)[0];
1826
1957
  if (!el || !el.bw || typeof el.bw[action] !== 'function') {
1827
1958
  _cw('bw.message: no handle method "' + action + '" on ' + target);
@@ -1831,6 +1962,207 @@ bw.message = function(target, action, data) {
1831
1962
  return true;
1832
1963
  };
1833
1964
 
1965
+ /**
1966
+ * Collect form data from all input, select, and textarea elements within a
1967
+ * container. Each element's `name` attribute (or `id` if no name) becomes a
1968
+ * key in the returned object. This provides a lightweight alternative to the
1969
+ * browser FormData API that returns a plain object suitable for JSON
1970
+ * serialization or bw.pub().
1971
+ *
1972
+ * Handles all standard HTML form controls:
1973
+ * - text/number/email/etc inputs: string value
1974
+ * - checkboxes: boolean (true/false)
1975
+ * - radio buttons: string value of the checked radio (unchecked groups omitted)
1976
+ * - multi-select: array of selected option values
1977
+ * - textarea: string value
1978
+ *
1979
+ * Elements without both `name` and `id` attributes are silently skipped.
1980
+ *
1981
+ * @param {string|Element} target - CSS selector, UUID string, or DOM element
1982
+ * @returns {Object} Plain object mapping field names to values
1983
+ * @category Component
1984
+ * @see bw.makeForm
1985
+ * @see bw.makeInput
1986
+ * @example
1987
+ * // Given a form with name="email" input and name="agree" checkbox:
1988
+ * var data = bw.formData('#signup-form');
1989
+ * // => { email: 'user@example.com', agree: true }
1990
+ *
1991
+ * // Collect and publish in one step:
1992
+ * bw.pub('form:submit', bw.formData('#my-form'));
1993
+ *
1994
+ * // Works with any container, not just <form>:
1995
+ * bw.pub('settings:changed', bw.formData('.settings-panel'));
1996
+ */
1997
+ bw.formData = function(target) {
1998
+ var el = bw.el(target);
1999
+ if (!el) return {};
2000
+ var result = {};
2001
+ var inputs = el.querySelectorAll('input, select, textarea');
2002
+ for (var i = 0; i < inputs.length; i++) {
2003
+ var inp = inputs[i];
2004
+ var key = inp.name || inp.id;
2005
+ if (!key) continue;
2006
+ if (inp.type === 'checkbox') {
2007
+ result[key] = inp.checked;
2008
+ } else if (inp.type === 'radio') {
2009
+ if (inp.checked) result[key] = inp.value;
2010
+ } else if (inp.tagName === 'SELECT' && inp.multiple) {
2011
+ result[key] = [];
2012
+ for (var j = 0; j < inp.options.length; j++) {
2013
+ if (inp.options[j].selected) result[key].push(inp.options[j].value);
2014
+ }
2015
+ } else {
2016
+ result[key] = inp.value;
2017
+ }
2018
+ }
2019
+ return result;
2020
+ };
2021
+
2022
+ // ===================================================================================
2023
+ // bw.jsonPatch() — RFC 6902 JSON Patch on plain objects
2024
+ // ===================================================================================
2025
+
2026
+ /**
2027
+ * Apply RFC 6902 JSON Patch operations to a plain object.
2028
+ *
2029
+ * Supported operations: add, remove, replace, move, copy, test.
2030
+ * Paths use JSON Pointer (RFC 6901) notation: `/foo/bar/0`.
2031
+ * Mutates the target object in place and returns it.
2032
+ *
2033
+ * @param {Object} obj - Target object to patch
2034
+ * @param {Array<Object>} ops - Array of patch operations
2035
+ * @param {string} ops[].op - Operation: 'add', 'remove', 'replace', 'move', 'copy', 'test'
2036
+ * @param {string} ops[].path - JSON Pointer path (e.g. '/a/b/0')
2037
+ * @param {*} [ops[].value] - Value for add/replace/test
2038
+ * @param {string} [ops[].from] - Source path for move/copy
2039
+ * @returns {Object} The patched object (same reference)
2040
+ * @throws {Error} On invalid op, missing path, test failure, or path not found for remove
2041
+ * @category Data Utilities
2042
+ * @see bw.patch
2043
+ * @example
2044
+ * var obj = { a: 1, b: { c: 2 } };
2045
+ * bw.jsonPatch(obj, [
2046
+ * { op: 'replace', path: '/a', value: 10 },
2047
+ * { op: 'add', path: '/b/d', value: 3 },
2048
+ * { op: 'remove', path: '/b/c' }
2049
+ * ]);
2050
+ * // obj => { a: 10, b: { d: 3 } }
2051
+ */
2052
+ bw.jsonPatch = function(obj, ops) {
2053
+ if (!_isA(ops)) return obj;
2054
+
2055
+ // Parse JSON Pointer path to array of keys
2056
+ function parsePath(path) {
2057
+ if (path === '') return [];
2058
+ if (path.charAt(0) !== '/') throw new Error('Invalid JSON Pointer: ' + path);
2059
+ return path.slice(1).split('/').map(function(s) {
2060
+ return s.replace(/~1/g, '/').replace(/~0/g, '~');
2061
+ });
2062
+ }
2063
+
2064
+ // Walk to parent of final key; return { parent, key }
2065
+ function resolve(root, keys) {
2066
+ var parent = root;
2067
+ for (var i = 0; i < keys.length - 1; i++) {
2068
+ var k = _isA(parent) ? parseInt(keys[i], 10) : keys[i];
2069
+ if (parent[k] === undefined) throw new Error('Path not found: /' + keys.slice(0, i + 1).join('/'));
2070
+ parent = parent[k];
2071
+ }
2072
+ return { parent: parent, key: _isA(parent) ? parseInt(keys[keys.length - 1], 10) : keys[keys.length - 1] };
2073
+ }
2074
+
2075
+ // Get value at path
2076
+ function getVal(root, keys) {
2077
+ var cur = root;
2078
+ for (var i = 0; i < keys.length; i++) {
2079
+ var k = _isA(cur) ? parseInt(keys[i], 10) : keys[i];
2080
+ if (cur[k] === undefined) throw new Error('Path not found: /' + keys.slice(0, i + 1).join('/'));
2081
+ cur = cur[k];
2082
+ }
2083
+ return cur;
2084
+ }
2085
+
2086
+ for (var i = 0; i < ops.length; i++) {
2087
+ var op = ops[i];
2088
+ if (!op.op || !_is(op.path, 'string')) throw new Error('Invalid patch operation at index ' + i);
2089
+ var keys = parsePath(op.path);
2090
+
2091
+ var r, val, fromKeys, fr, tr, cr;
2092
+ switch (op.op) {
2093
+ case 'add': {
2094
+ if (keys.length === 0) throw new Error('Cannot add to root');
2095
+ r = resolve(obj, keys);
2096
+ if (_isA(r.parent) && r.key <= r.parent.length) {
2097
+ r.parent.splice(r.key, 0, op.value);
2098
+ } else {
2099
+ r.parent[r.key] = op.value;
2100
+ }
2101
+ break;
2102
+ }
2103
+ case 'remove': {
2104
+ if (keys.length === 0) throw new Error('Cannot remove root');
2105
+ r = resolve(obj, keys);
2106
+ if (_isA(r.parent)) {
2107
+ if (r.key >= r.parent.length) throw new Error('Index out of bounds: ' + r.key);
2108
+ r.parent.splice(r.key, 1);
2109
+ } else {
2110
+ if (!(r.key in r.parent)) throw new Error('Path not found: ' + op.path);
2111
+ delete r.parent[r.key];
2112
+ }
2113
+ break;
2114
+ }
2115
+ case 'replace': {
2116
+ if (keys.length === 0) throw new Error('Cannot replace root');
2117
+ r = resolve(obj, keys);
2118
+ if (_isA(r.parent)) {
2119
+ if (r.key >= r.parent.length) throw new Error('Index out of bounds: ' + r.key);
2120
+ } else {
2121
+ if (!(r.key in r.parent)) throw new Error('Path not found: ' + op.path);
2122
+ }
2123
+ r.parent[r.key] = op.value;
2124
+ break;
2125
+ }
2126
+ case 'move': {
2127
+ if (!_is(op.from, 'string')) throw new Error('move requires "from"');
2128
+ fromKeys = parsePath(op.from);
2129
+ val = getVal(obj, fromKeys);
2130
+ fr = resolve(obj, fromKeys);
2131
+ if (_isA(fr.parent)) { fr.parent.splice(fr.key, 1); }
2132
+ else { delete fr.parent[fr.key]; }
2133
+ tr = resolve(obj, keys);
2134
+ if (_isA(tr.parent) && tr.key <= tr.parent.length) {
2135
+ tr.parent.splice(tr.key, 0, val);
2136
+ } else {
2137
+ tr.parent[tr.key] = val;
2138
+ }
2139
+ break;
2140
+ }
2141
+ case 'copy': {
2142
+ if (!_is(op.from, 'string')) throw new Error('copy requires "from"');
2143
+ val = getVal(obj, parsePath(op.from));
2144
+ cr = resolve(obj, keys);
2145
+ if (_isA(cr.parent) && cr.key <= cr.parent.length) {
2146
+ cr.parent.splice(cr.key, 0, val);
2147
+ } else {
2148
+ cr.parent[cr.key] = val;
2149
+ }
2150
+ break;
2151
+ }
2152
+ case 'test': {
2153
+ var actual = getVal(obj, keys);
2154
+ if (JSON.stringify(actual) !== JSON.stringify(op.value)) {
2155
+ throw new Error('Test failed: ' + op.path + ' expected ' + JSON.stringify(op.value) + ' got ' + JSON.stringify(actual));
2156
+ }
2157
+ break;
2158
+ }
2159
+ default:
2160
+ throw new Error('Unknown op: ' + op.op);
2161
+ }
2162
+ }
2163
+ return obj;
2164
+ };
2165
+
1834
2166
  // ===================================================================================
1835
2167
  // bw.apply() / bw.parseJSONFlex() — Server-driven UI protocol
1836
2168
  // ===================================================================================
@@ -1972,7 +2304,7 @@ bw.apply = function(msg) {
1972
2304
  var target = msg.target;
1973
2305
 
1974
2306
  if (type === 'replace') {
1975
- var el = bw._el(target);
2307
+ var el = bw.el(target);
1976
2308
  if (!el) return false;
1977
2309
  bw.DOM(el, msg.node);
1978
2310
  return true;
@@ -1982,14 +2314,14 @@ bw.apply = function(msg) {
1982
2314
  return patched !== null;
1983
2315
 
1984
2316
  } else if (type === 'append') {
1985
- var parent = bw._el(target);
2317
+ var parent = bw.el(target);
1986
2318
  if (!parent) return false;
1987
2319
  var child = bw.createDOM(msg.node);
1988
2320
  parent.appendChild(child);
1989
2321
  return true;
1990
2322
 
1991
2323
  } else if (type === 'remove') {
1992
- var toRemove = bw._el(target);
2324
+ var toRemove = bw.el(target);
1993
2325
  if (!toRemove) return false;
1994
2326
  if (_is(bw.cleanup, 'function')) bw.cleanup(toRemove);
1995
2327
  toRemove.remove();
@@ -2049,30 +2381,98 @@ bw.apply = function(msg) {
2049
2381
 
2050
2382
 
2051
2383
  // ===================================================================================
2052
- // bw.inspect() — Debug utility
2384
+ // bw.inspect() — DOM introspection with bitwrench metadata
2053
2385
  // ===================================================================================
2054
2386
 
2055
2387
  /**
2056
- * Inspect a DOM element's bitwrench state, handle methods, and metadata.
2057
- * Works with DOM elements or CSS selectors.
2388
+ * Inspect a DOM element and its subtree, returning a plain-object
2389
+ * representation with bitwrench metadata at each node. Useful for debugging,
2390
+ * devtools, MCP/AG-UI tool discovery, and automated testing.
2058
2391
  *
2059
- * @param {string|Element} target - Selector or DOM element
2060
- * @returns {Element|null} The element, or null if not found
2392
+ * Each node in the returned tree includes:
2393
+ * - `tag` -- lowercase tag name (or '#text' for text nodes)
2394
+ * - `id` -- element id (if set)
2395
+ * - `uuid` -- bitwrench UUID class (if lifecycle-managed)
2396
+ * - `type` -- component type from o.type (if set, e.g. 'card', 'tabs')
2397
+ * - `classes` -- first 5 CSS classes (string, space-separated)
2398
+ * - `handles` -- array of el.bw method names (if any)
2399
+ * - `state` -- copy of _bw_state (if any)
2400
+ * - `hasRender` -- true if _bw_render is set
2401
+ * - `hasSubs` -- true if element has pub/sub subscriptions
2402
+ * - `refs` -- copy of _bw_refs keys (if any)
2403
+ * - `children` -- array of child node trees (up to depth limit, max 50 per level)
2404
+ *
2405
+ * @param {string|Element} target - CSS selector, UUID, or DOM element
2406
+ * @param {number} [depth=3] - Maximum recursion depth (0 = target only, no children)
2407
+ * @returns {Object|null} Plain object tree, or null if element not found
2061
2408
  * @category Component
2062
2409
  * @example
2063
- * bw.inspect('#my-carousel');
2064
- * bw.inspect($0);
2065
- */
2066
- bw.inspect = function(target) {
2067
- var el = _is(target, 'string') ? bw.$(target)[0] : target;
2068
- if (!el) { _cw('bw.inspect: element not found'); return null; }
2069
- console.group('Element: ' + (bw.getUUID(el) || el.id || el.tagName));
2070
- _cl('State:', el._bw_state || '(none)');
2071
- _cl('Handle:', el.bw ? _keys(el.bw) : '(none)');
2072
- _cl('Classes:', el.className);
2073
- _cl('Refs:', el._bw_refs || '(none)');
2074
- console.groupEnd();
2075
- return el;
2410
+ * // Get full tree from #app, 3 levels deep (default):
2411
+ * var info = bw.inspect('#app');
2412
+ *
2413
+ * // Shallow inspection (just the element, no children):
2414
+ * var info = bw.inspect('#my-carousel', 0);
2415
+ * console.log(info.handles); // ['next', 'prev', 'goToSlide']
2416
+ * console.log(info.type); // 'carousel'
2417
+ *
2418
+ * // Deep inspection for debugging:
2419
+ * console.log(JSON.stringify(bw.inspect('#app', 5), null, 2));
2420
+ */
2421
+ bw.inspect = function(target, depth) {
2422
+ var el = bw.el(target);
2423
+ if (!el && _is(target, 'string')) el = bw.$(target)[0];
2424
+ if (!el) return null;
2425
+ if (depth === undefined || depth === null) depth = 3;
2426
+
2427
+ function walk(node, d) {
2428
+ if (!node) return null;
2429
+ // Skip non-element nodes (text, comment, etc.)
2430
+ if (node.nodeType !== 1) return null;
2431
+
2432
+ var info = { tag: node.tagName ? node.tagName.toLowerCase() : '#text' };
2433
+
2434
+ // Identity
2435
+ if (node.id) info.id = node.id;
2436
+ var uuid = bw.getUUID(node);
2437
+ if (uuid) info.uuid = uuid;
2438
+ if (node._bw_type) info.type = node._bw_type;
2439
+
2440
+ // CSS classes (first 5 for readability)
2441
+ if (node.className && typeof node.className === 'string') {
2442
+ info.classes = node.className.split(' ').slice(0, 5).join(' ');
2443
+ }
2444
+
2445
+ // Bitwrench handle methods
2446
+ if (node.bw) {
2447
+ var handles = _keys(node.bw);
2448
+ if (handles.length > 0) info.handles = handles;
2449
+ }
2450
+
2451
+ // State
2452
+ if (node._bw_state) info.state = node._bw_state;
2453
+ if (node._bw_render) info.hasRender = true;
2454
+ if (node._bw_subs && node._bw_subs.length > 0) info.hasSubs = true;
2455
+
2456
+ // Refs
2457
+ if (node._bw_refs) info.refs = _keys(node._bw_refs);
2458
+
2459
+ // Children (recurse up to depth limit, max 50 children per level)
2460
+ if (d < depth && node.children && node.children.length > 0) {
2461
+ info.children = [];
2462
+ var max = Math.min(node.children.length, 50);
2463
+ for (var i = 0; i < max; i++) {
2464
+ var child = walk(node.children[i], d + 1);
2465
+ if (child) info.children.push(child);
2466
+ }
2467
+ if (node.children.length > 50) {
2468
+ info.children.push({ tag: '...', count: node.children.length - 50 });
2469
+ }
2470
+ }
2471
+
2472
+ return info;
2473
+ }
2474
+
2475
+ return walk(el, 0);
2076
2476
  };
2077
2477
 
2078
2478
  bw.compile = function() { throw new Error('bw.compile() removed in v2.0.19. Use o.handle/o.slots on TACO options instead.'); };
@@ -2294,37 +2694,49 @@ bw.clip = _clip;
2294
2694
  * so you can use `.map()`, `.filter()`, etc. directly. Accepts CSS selectors,
2295
2695
  * single elements, NodeLists, or arrays.
2296
2696
  *
2697
+ * With an optional second argument, applies content or a function to
2698
+ * every matched element (same apply rules as `bw.el()`):
2699
+ * - string/number: sets `el.textContent`
2700
+ * - function: calls `apply(el)` for each element
2701
+ * - TACO object: clears children, mounts TACO via `bw.createDOM()`
2702
+ * - array: clears children, appends each item
2703
+ *
2297
2704
  * @param {string|Element|Array} selector - CSS selector, element, or array
2705
+ * @param {string|number|Function|Object|Array} [apply] - Content or function to apply
2298
2706
  * @returns {Array} Array of DOM elements
2299
2707
  * @category DOM Selection
2708
+ * @see bw.el
2300
2709
  * @example
2301
- * bw.$('.card') // => [div.card, div.card, ...]
2302
- * bw.$(myElement) // => [myElement]
2303
- * bw.$('.card').map(el => el.textContent)
2710
+ * bw.$('.card') // => [div.card, div.card, ...]
2711
+ * bw.$('.status', 'Online') // set text on all .status elements
2712
+ * bw.$('.card', function(el) { // apply function to each
2713
+ * el.style.opacity = '0.5';
2714
+ * })
2304
2715
  */
2305
2716
  if (bw._isBrowser) {
2306
- bw.$ = function(selector) {
2307
- if (!selector) return [];
2308
-
2309
- // Already an array
2310
- if (_isA(selector)) return selector;
2311
-
2312
- // Single element
2313
- if (selector.nodeType) return [selector];
2314
-
2315
- // NodeList or HTMLCollection
2316
- if (selector.length !== undefined && !_is(selector, 'string')) {
2317
- return Array.from(selector);
2717
+ bw.$ = function(selector, apply) {
2718
+ var els;
2719
+ if (!selector) {
2720
+ els = [];
2721
+ } else if (_isA(selector)) {
2722
+ els = selector;
2723
+ } else if (selector.nodeType) {
2724
+ els = [selector];
2725
+ } else if (selector.length !== undefined && !_is(selector, 'string')) {
2726
+ els = Array.from(selector);
2727
+ } else if (_is(selector, 'string')) {
2728
+ els = Array.from(document.querySelectorAll(selector));
2729
+ } else {
2730
+ els = [];
2318
2731
  }
2319
-
2320
- // CSS selector string
2321
- if (_is(selector, 'string')) {
2322
- return Array.from(document.querySelectorAll(selector));
2732
+
2733
+ if (apply !== undefined) {
2734
+ for (var i = 0; i < els.length; i++) _applyTo(els[i], apply);
2323
2735
  }
2324
-
2325
- return [];
2736
+
2737
+ return els;
2326
2738
  };
2327
-
2739
+
2328
2740
  // Convenience single element selector
2329
2741
  bw.$.one = function(selector) {
2330
2742
  return bw.$(selector)[0] || null;
@@ -2537,42 +2949,48 @@ bw.loadReset = function() {
2537
2949
  };
2538
2950
 
2539
2951
  /**
2540
- * Toggle between primary and alternate palettes.
2952
+ * Toggle between primary and alternate theme palettes.
2541
2953
  *
2542
- * Adds/removes the `bw_theme_alt` class on the scoping element.
2954
+ * Adds/removes the `bw_theme_alt` class on the scoping element(s).
2543
2955
  * Without a scope, toggles on `<html>` (global).
2544
- * With a scope, toggles on the first matching element.
2956
+ * With a scope, toggles on ALL matching elements.
2545
2957
  *
2546
- * @param {string} [scope] - Scope selector (e.g. '#my-dashboard'). Omit for global.
2547
- * @returns {string} Active mode after toggle: 'primary' or 'alternate'
2958
+ * @param {string|Element} [scope] - Selector or element. Omit for global.
2959
+ * @returns {string} Active mode after toggle: 'primary' or 'alternate' (based on first element)
2548
2960
  * @category CSS & Styling
2549
2961
  * @see bw.applyStyles
2550
2962
  * @see bw.clearStyles
2551
2963
  * @example
2552
- * bw.toggleStyles(); // global toggle on <html>
2553
- * bw.toggleStyles('#my-dashboard'); // scoped toggle
2964
+ * bw.toggleThemeMode(); // global toggle on <html>
2965
+ * bw.toggleThemeMode('#my-dashboard'); // scoped toggle
2966
+ * bw.toggleThemeMode('.panel'); // toggle on ALL .panel elements
2554
2967
  */
2555
- bw.toggleStyles = function(scope) {
2968
+ bw.toggleThemeMode = function(scope) {
2556
2969
  if (!bw._isBrowser) return 'primary';
2557
- var target;
2970
+ var els;
2558
2971
  if (scope) {
2559
- var els = bw.$(scope);
2560
- target = els[0];
2972
+ els = bw.$(scope);
2561
2973
  } else {
2562
- target = document.documentElement;
2974
+ els = [document.documentElement];
2563
2975
  }
2564
- if (!target) return 'primary';
2976
+ if (!els.length) return 'primary';
2565
2977
 
2566
- var hasAlt = target.classList.contains('bw_theme_alt');
2567
- if (hasAlt) {
2568
- target.classList.remove('bw_theme_alt');
2569
- return 'primary';
2570
- } else {
2571
- target.classList.add('bw_theme_alt');
2572
- return 'alternate';
2978
+ var mode;
2979
+ for (var i = 0; i < els.length; i++) {
2980
+ var hasAlt = els[i].classList.contains('bw_theme_alt');
2981
+ if (hasAlt) {
2982
+ els[i].classList.remove('bw_theme_alt');
2983
+ } else {
2984
+ els[i].classList.add('bw_theme_alt');
2985
+ }
2986
+ if (i === 0) mode = hasAlt ? 'primary' : 'alternate';
2573
2987
  }
2988
+ return mode;
2574
2989
  };
2575
2990
 
2991
+ // Alias — kept for one release cycle. Use bw.toggleThemeMode() instead.
2992
+ bw.toggleStyles = bw.toggleThemeMode;
2993
+
2576
2994
  /**
2577
2995
  * Remove injected styles for a given scope.
2578
2996
  *
@@ -3622,6 +4040,57 @@ Object.entries(components).forEach(([name, fn]) => {
3622
4040
  }
3623
4041
  });
3624
4042
 
4043
+ /**
4044
+ * Query the BCCL component registry. Returns metadata about registered
4045
+ * component types -- their names and factory function names. Useful for
4046
+ * tooling, introspection, documentation generators, and auto-complete
4047
+ * systems (including MCP/AG-UI tool discovery).
4048
+ *
4049
+ * With no arguments, returns an array of all registered component types.
4050
+ * With a type name, returns metadata for that single type (or null if
4051
+ * the type is not registered).
4052
+ *
4053
+ * @param {string} [type] - Optional component type name to look up
4054
+ * @returns {Array<Object>|Object|null} Array of {type, factory} objects,
4055
+ * a single {type, factory} object, or null if the type is not found
4056
+ * @category Component
4057
+ * @see bw.make
4058
+ * @see bw.BCCL
4059
+ * @example
4060
+ * // List all available component types:
4061
+ * bw.catalog();
4062
+ * // => [{ type: 'card', factory: 'makeCard' },
4063
+ * // { type: 'button', factory: 'makeButton' }, ...]
4064
+ *
4065
+ * // Look up a specific type:
4066
+ * bw.catalog('accordion');
4067
+ * // => { type: 'accordion', factory: 'makeAccordion' }
4068
+ *
4069
+ * // Check if a type exists:
4070
+ * if (bw.catalog('chart')) { ... }
4071
+ *
4072
+ * // Get just the type names:
4073
+ * bw.catalog().map(function(c) { return c.type; });
4074
+ * // => ['card', 'button', 'container', 'row', ...]
4075
+ */
4076
+ bw.catalog = function(type) {
4077
+ if (type) {
4078
+ var def = bw.BCCL[type];
4079
+ if (!def) return null;
4080
+ return {
4081
+ type: type,
4082
+ factory: def.make.name || ('make' + type.charAt(0).toUpperCase() + type.slice(1))
4083
+ };
4084
+ }
4085
+ return Object.keys(bw.BCCL).map(function(k) {
4086
+ var def = bw.BCCL[k];
4087
+ return {
4088
+ type: k,
4089
+ factory: def.make.name || ('make' + k.charAt(0).toUpperCase() + k.slice(1))
4090
+ };
4091
+ });
4092
+ };
4093
+
3625
4094
  // Export for different environments
3626
4095
  export default bw;
3627
4096