bitwrench 2.0.24 → 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 (88) hide show
  1. package/README.md +17 -9
  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 +661 -174
  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 +690 -178
  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 +661 -174
  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 +661 -174
  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 +659 -172
  44. package/dist/bitwrench.cjs.min.js +6 -6
  45. package/dist/bitwrench.cjs.min.js.gz +0 -0
  46. package/dist/bitwrench.css +6 -6
  47. package/dist/bitwrench.d.ts +666 -0
  48. package/dist/bitwrench.es5.js +687 -175
  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 +659 -172
  52. package/dist/bitwrench.esm.min.js +5 -5
  53. package/dist/bitwrench.esm.min.js.gz +0 -0
  54. package/dist/bitwrench.min.css +1 -1
  55. package/dist/bitwrench.umd.js +659 -172
  56. package/dist/bitwrench.umd.min.js +6 -6
  57. package/dist/bitwrench.umd.min.js.gz +0 -0
  58. package/dist/builds.json +96 -96
  59. package/dist/bwserve.cjs.js +140 -7
  60. package/dist/bwserve.esm.js +141 -8
  61. package/dist/sri.json +46 -46
  62. package/docs/README.md +5 -3
  63. package/docs/bitwrench-for-wasm.md +851 -0
  64. package/docs/bitwrench-mcp.md +1 -1
  65. package/docs/bitwrench-taco-schema-discussion.md +694 -0
  66. package/docs/bitwrench_api.md +134 -24
  67. package/docs/bitwrench_typescript_usage.md +441 -0
  68. package/docs/component-cheatsheet.md +1 -1
  69. package/docs/framework-translation-table.md +1 -1
  70. package/docs/llm-bitwrench-guide.md +34 -6
  71. package/docs/routing.md +1 -1
  72. package/docs/state-management.md +27 -3
  73. package/docs/thinking-in-bitwrench.md +6 -5
  74. package/docs/tutorial-bwserve.md +1 -1
  75. package/docs/tutorial-website.md +1 -1
  76. package/package.json +16 -10
  77. package/readme.html +29 -14
  78. package/src/bitwrench-styles.js +17 -17
  79. package/src/bitwrench.d.ts +666 -0
  80. package/src/bitwrench.js +638 -150
  81. package/src/bwserve/bwclient.js +3 -3
  82. package/src/bwserve/client.js +26 -0
  83. package/src/bwserve/index.js +110 -3
  84. package/src/cli/attach.js +7 -5
  85. package/src/cli/serve.js +53 -9
  86. package/src/mcp/live.js +3 -1
  87. package/src/mcp/server.js +7 -7
  88. package/src/version.js +3 -3
@@ -1,4 +1,4 @@
1
- /*! bitwrench-lean v2.0.24 | BSD-2-Clause | https://deftio.github.com/bitwrench/pages */
1
+ /*! bitwrench-lean v2.0.30 | BSD-2-Clause | https://deftio.github.com/bitwrench/pages */
2
2
  (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.24',
193
+ version: '2.0.30',
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-28T09:12:50.073Z'
200
+ buildDate: '2026-04-12T07:51:29.111Z'
201
201
  };
202
202
 
203
203
  /**
@@ -901,7 +901,7 @@
901
901
  'transition': 'color ' + mot.fast + ' ' + mot.easing
902
902
  };
903
903
  rules[_sx(scope, 'a:hover')] = {
904
- 'color': palette.primary.hover,
904
+ 'color': palette.tertiary.hover,
905
905
  'text-decoration': 'underline'
906
906
  };
907
907
  return rules;
@@ -1078,7 +1078,7 @@
1078
1078
  'transition': 'color ' + layout.motion.fast + ' ' + layout.motion.easing + ', background-color ' + layout.motion.fast + ' ' + layout.motion.easing
1079
1079
  };
1080
1080
  rules[_sx(scope, '.bw_navbar_nav .bw_nav_link:hover')] = {
1081
- 'color': palette.dark.base,
1081
+ 'color': palette.tertiary.base,
1082
1082
  'background-color': palette.surfaceAlt
1083
1083
  };
1084
1084
  rules[_sx(scope, '.bw_navbar_nav .bw_nav_link.active')] = {
@@ -1156,7 +1156,7 @@
1156
1156
  'transition': 'color ' + mo.fast + ' ' + mo.easing + ', border-color ' + mo.fast + ' ' + mo.easing + ', background-color ' + mo.fast + ' ' + mo.easing
1157
1157
  };
1158
1158
  rules[_sx(scope, '.bw_nav_tabs .bw_nav_link:hover')] = {
1159
- 'color': palette.dark.base,
1159
+ 'color': palette.tertiary.base,
1160
1160
  'background-color': palette.surfaceAlt,
1161
1161
  'border-bottom-color': palette.light.border
1162
1162
  };
@@ -1180,7 +1180,7 @@
1180
1180
  };
1181
1181
  rules[_sx(scope, 'a.bw_list_group_item:hover')] = {
1182
1182
  'background-color': palette.surfaceAlt,
1183
- 'color': palette.dark.hover
1183
+ 'color': palette.tertiary.base
1184
1184
  };
1185
1185
  rules[_sx(scope, '.bw_list_group_item.active')] = {
1186
1186
  'color': palette.primary.textOn,
@@ -1278,11 +1278,11 @@
1278
1278
  'color': palette.secondary.base
1279
1279
  };
1280
1280
  rules[_sx(scope, '.bw_breadcrumb_item a')] = {
1281
- 'color': palette.primary.base,
1281
+ 'color': palette.tertiary.base,
1282
1282
  'transition': 'color ' + mo.fast + ' ' + mo.easing
1283
1283
  };
1284
1284
  rules[_sx(scope, '.bw_breadcrumb_item a:hover')] = {
1285
- 'color': palette.primary.hover,
1285
+ 'color': palette.tertiary.hover,
1286
1286
  'text-decoration': 'underline'
1287
1287
  };
1288
1288
  rules[_sx(scope, '.bw_breadcrumb_item.active')] = {
@@ -1530,14 +1530,14 @@
1530
1530
  'font-weight': '600'
1531
1531
  };
1532
1532
  rules[_sx(scope, '.bw_step_completed .bw_step_indicator')] = {
1533
- 'background-color': palette.primary.base,
1534
- 'color': palette.primary.textOn
1533
+ 'background-color': palette.tertiary.base,
1534
+ 'color': palette.tertiary.textOn
1535
1535
  };
1536
1536
  rules[_sx(scope, '.bw_step_completed .bw_step_label')] = {
1537
- 'color': palette.primary.base
1537
+ 'color': palette.tertiary.base
1538
1538
  };
1539
1539
  rules[_sx(scope, '.bw_step_completed + .bw_step::before')] = {
1540
- 'background-color': palette.primary.base
1540
+ 'background-color': palette.tertiary.base
1541
1541
  };
1542
1542
  return rules;
1543
1543
  }
@@ -1821,25 +1821,25 @@
1821
1821
  };
1822
1822
  });
1823
1823
 
1824
- // Text muted — always a neutral gray, never a brand color
1824
+ // Text muted — uses palette secondary for theme-aware muted text
1825
1825
  rules[_sx(scope, '.bw_text_muted')] = {
1826
- 'color': '#6c757d'
1826
+ 'color': palette.secondary.base
1827
1827
  };
1828
1828
 
1829
- // Common bg/text utilities that aren't per-variant
1829
+ // Common bg/text utilities derive from palette for theme awareness
1830
1830
  rules[_sx(scope, '.bw_bg_dark')] = {
1831
- 'background-color': '#212529',
1832
- 'color': '#f8f9fa'
1831
+ 'background-color': palette.dark.base,
1832
+ 'color': palette.dark.textOn
1833
1833
  };
1834
1834
  rules[_sx(scope, '.bw_bg_light')] = {
1835
- 'background-color': '#f8f9fa',
1836
- 'color': '#212529'
1835
+ 'background-color': palette.light.base,
1836
+ 'color': palette.light.textOn
1837
1837
  };
1838
1838
  rules[_sx(scope, '.bw_text_light')] = {
1839
- 'color': '#f8f9fa'
1839
+ 'color': palette.light.base
1840
1840
  };
1841
1841
  rules[_sx(scope, '.bw_text_dark')] = {
1842
- 'color': '#212529'
1842
+ 'color': palette.dark.base
1843
1843
  };
1844
1844
  return rules;
1845
1845
  }
@@ -5830,9 +5830,6 @@
5830
5830
  var _cw = function _cw() {
5831
5831
  console.warn.apply(console, arguments);
5832
5832
  };
5833
- var _cl = function _cl() {
5834
- console.log.apply(console, arguments);
5835
- };
5836
5833
  var _ce = function _ce() {
5837
5834
  console.error.apply(console, arguments);
5838
5835
  };
@@ -5973,60 +5970,104 @@
5973
5970
  };
5974
5971
 
5975
5972
  /**
5976
- * Look up a DOM element by ID string, using the node cache for O(1) access.
5977
- *
5978
- * Resolution order:
5979
- * 1. Check `bw._nodeMap[id]` if found and still attached (parentNode !== null), return it
5980
- * 2. If cached ref is detached (parentNode === null), remove stale entry
5981
- * 3. Fall back to `document.getElementById(id)` then `document.querySelector(...)`
5982
- * 4. Try class-based lookup for bw_uuid_* tokens (UUID addressing)
5983
- * 5. Cache the result for next time
5984
- *
5985
- * Accepts a DOM element directly (pass-through) or a string identifier.
5986
- * String identifiers are tried as: direct map key, getElementById,
5987
- * querySelector (for CSS selectors starting with . or #), and
5988
- * bw_uuid_* class selector.
5989
- *
5990
- * @param {string|Element} id - Element ID, CSS selector, bw_uuid_* class, or DOM element
5973
+ * Look up a single DOM element by ID, CSS selector, UUID, or element ref.
5974
+ * Optionally apply content or a function to the resolved element.
5975
+ *
5976
+ * Resolution order for string targets:
5977
+ * 1. Check `bw._nodeMap[id]` cache (O(1), stale entries auto-pruned)
5978
+ * 2. `document.getElementById(id)`
5979
+ * 3. `document.querySelector(id)` for selectors starting with # or .
5980
+ * 4. Class-based lookup for `bw_uuid_*` tokens
5981
+ *
5982
+ * With one argument, returns the element (or null). With two arguments,
5983
+ * applies the second argument to the element and returns the element:
5984
+ * - string/number: sets `el.textContent`
5985
+ * - function: calls `apply(el)`, returns el
5986
+ * - TACO object: clears children, mounts TACO via `bw.createDOM()`
5987
+ * - array: clears children, appends each item (string -> text node, TACO -> element)
5988
+ *
5989
+ * @param {string|Element} target - Element ref, ID, CSS selector, or bw_uuid_* class
5990
+ * @param {string|number|Function|Object|Array} [apply] - Content or function to apply
5991
5991
  * @returns {Element|null} The DOM element, or null if not found
5992
- * @category Internal
5992
+ * @category DOM Selection
5993
+ * @see bw.$
5994
+ * @see bw.patch
5995
+ * @example
5996
+ * bw.el('#title') // lookup
5997
+ * bw.el('#title', 'Hello') // set text content
5998
+ * bw.el('#app', { t: 'h1', c: 'Hi' }) // mount TACO
5999
+ * bw.el('.card', function(el) { // apply function
6000
+ * el.style.opacity = '0.5';
6001
+ * })
5993
6002
  */
5994
- bw._el = function (id) {
5995
- // Pass-through for DOM elements
5996
- if (!_is(id, 'string')) return id || null;
5997
- if (!id) return null;
5998
- if (!bw._isBrowser) return null;
5999
-
6000
- // 1. Check cache
6001
- var cached = bw._nodeMap[id];
6002
- if (cached) {
6003
- // Verify not detached (parentNode check is IE11-safe)
6004
- if (cached.parentNode !== null) {
6005
- return cached;
6003
+ bw.el = function (target, apply) {
6004
+ // Resolve target to element
6005
+ var el;
6006
+ if (!_is(target, 'string')) {
6007
+ el = target || null;
6008
+ } else if (!target || !bw._isBrowser) {
6009
+ el = null;
6010
+ } else {
6011
+ // 1. Check cache
6012
+ var cached = bw._nodeMap[target];
6013
+ if (cached) {
6014
+ if (cached.parentNode !== null) {
6015
+ el = cached;
6016
+ } else {
6017
+ delete bw._nodeMap[target];
6018
+ }
6019
+ }
6020
+ if (!el) {
6021
+ // 2. getElementById
6022
+ el = document.getElementById(target);
6023
+ // 3. querySelector for CSS selectors
6024
+ if (!el && (target.charAt(0) === '#' || target.charAt(0) === '.')) {
6025
+ el = document.querySelector(target);
6026
+ }
6027
+ // 4. bw_uuid_* class lookup
6028
+ if (!el && target.indexOf('bw_uuid_') === 0) {
6029
+ el = document.querySelector('.' + target);
6030
+ }
6031
+ // 5. Cache result
6032
+ if (el) bw._nodeMap[target] = el;
6006
6033
  }
6007
- // Stale — remove and fall through
6008
- delete bw._nodeMap[id];
6009
6034
  }
6010
6035
 
6011
- // 2. DOM fallback: try getElementById first (fastest native lookup)
6012
- var el = document.getElementById(id);
6013
-
6014
- // 3. Try querySelector for CSS selectors (starts with # or .)
6015
- if (!el && (id.charAt(0) === '#' || id.charAt(0) === '.')) {
6016
- el = document.querySelector(id);
6017
- }
6036
+ // Apply (if provided and element found)
6037
+ if (el && apply !== undefined) _applyTo(el, apply);
6038
+ return el;
6039
+ };
6018
6040
 
6019
- // 4. Try class-based lookup for bw_uuid_* tokens (UUID addressing)
6020
- if (!el && id.indexOf('bw_uuid_') === 0) {
6021
- el = document.querySelector('.' + id);
6041
+ /**
6042
+ * Internal: apply content or function to a DOM element.
6043
+ * Shared by bw.el() and bw.$().
6044
+ * @private
6045
+ */
6046
+ function _applyTo(el, apply) {
6047
+ if (_is(apply, 'function')) {
6048
+ apply(el);
6049
+ } else if (_isA(apply)) {
6050
+ el.innerHTML = '';
6051
+ apply.forEach(function (item) {
6052
+ if (item != null) {
6053
+ if (_is(item, 'object') && item.t) {
6054
+ el.appendChild(bw.createDOM(item));
6055
+ } else {
6056
+ el.appendChild(document.createTextNode(String(item)));
6057
+ }
6058
+ }
6059
+ });
6060
+ } else if (_is(apply, 'object') && apply !== null && apply.t) {
6061
+ el.innerHTML = '';
6062
+ el.appendChild(bw.createDOM(apply));
6063
+ } else {
6064
+ el.textContent = String(apply);
6022
6065
  }
6066
+ }
6023
6067
 
6024
- // 5. Cache the result for next time
6025
- if (el) {
6026
- bw._nodeMap[id] = el;
6027
- }
6028
- return el;
6029
- };
6068
+ // Internal alias kept for one release cycle (v2.0.26).
6069
+ // Will be removed in v2.0.27. Use bw.el() instead.
6070
+ bw._el = bw.el;
6030
6071
 
6031
6072
  /**
6032
6073
  * Register a DOM element in the node cache under one or more keys.
@@ -6090,6 +6131,12 @@
6090
6131
  */
6091
6132
  var _UUID_RE = /\bbw_uuid_[a-z0-9_]+\b/;
6092
6133
 
6134
+ /**
6135
+ * SVG namespace URI for createElementNS.
6136
+ * @private
6137
+ */
6138
+ var _SVG_NS = 'http://www.w3.org/2000/svg';
6139
+
6093
6140
  /**
6094
6141
  * Assign a UUID to a TACO object by appending a `bw_uuid_*` token to `taco.a.class`.
6095
6142
  *
@@ -6140,9 +6187,9 @@
6140
6187
  bw.getUUID = function (tacoOrElement) {
6141
6188
  if (!tacoOrElement) return null;
6142
6189
  var classStr;
6143
- // DOM element: check className
6190
+ // DOM element: check className (SVG elements use getAttribute for string value)
6144
6191
  if (tacoOrElement.className !== undefined && tacoOrElement.tagName) {
6145
- classStr = tacoOrElement.className;
6192
+ classStr = typeof tacoOrElement.className === 'string' ? tacoOrElement.className : tacoOrElement.getAttribute('class') || '';
6146
6193
  }
6147
6194
  // TACO object: check a.class
6148
6195
  else if (tacoOrElement.a && _is(tacoOrElement.a["class"], 'string')) {
@@ -6427,7 +6474,7 @@
6427
6474
  var fnCounterBefore = bw._fnIDCounter;
6428
6475
 
6429
6476
  // Render body content
6430
- var bodyHTML = '';
6477
+ var bodyHTML;
6431
6478
  if (_is(body, 'string')) {
6432
6479
  bodyHTML = body;
6433
6480
  } else {
@@ -6603,8 +6650,10 @@
6603
6650
  _taco$o2 = taco.o,
6604
6651
  opts = _taco$o2 === void 0 ? {} : _taco$o2;
6605
6652
 
6606
- // Create element
6607
- var el = document.createElement(tag);
6653
+ // SVG namespace: detect SVG context and thread through children.
6654
+ // {t:'svg'} starts SVG context; foreignObject children revert to HTML.
6655
+ var svgCtx = options._svgCtx || tag === 'svg';
6656
+ var el = svgCtx ? document.createElementNS(_SVG_NS, tag) : document.createElement(tag);
6608
6657
 
6609
6658
  // Set attributes
6610
6659
  for (var _i2 = 0, _Object$entries2 = Object.entries(attrs); _i2 < _Object$entries2.length; _i2++) {
@@ -6617,9 +6666,10 @@
6617
6666
  Object.assign(el.style, value);
6618
6667
  } else if (key === 'class') {
6619
6668
  // Handle class as array or string
6669
+ // SVG elements use SVGAnimatedString for className, so use setAttribute
6620
6670
  var classStr = _isA(value) ? value.filter(Boolean).join(' ') : String(value);
6621
6671
  if (classStr) {
6622
- el.className = classStr;
6672
+ if (svgCtx) el.setAttribute('class', classStr);else el.className = classStr;
6623
6673
  }
6624
6674
  } else if (key.startsWith('on') && _is(value, 'function')) {
6625
6675
  // Event handlers
@@ -6640,11 +6690,19 @@
6640
6690
  // Add children, building _bw_refs for fast parent→child access.
6641
6691
  // Children with id attributes or bw_uuid_* classes get local refs on the parent,
6642
6692
  // so o.render functions can access them without any DOM lookup.
6693
+ // SVG: foreignObject children revert to HTML namespace; otherwise inherit.
6694
+ var childOpts = options;
6695
+ var childSvgCtx = svgCtx && tag !== 'foreignObject';
6696
+ if (childSvgCtx !== (options._svgCtx || false)) {
6697
+ childOpts = Object.assign({}, options, {
6698
+ _svgCtx: childSvgCtx || undefined
6699
+ });
6700
+ }
6643
6701
  if (content != null) {
6644
6702
  if (_isA(content)) {
6645
6703
  content.forEach(function (child) {
6646
6704
  if (child != null) {
6647
- var childEl = bw.createDOM(child, options);
6705
+ var childEl = bw.createDOM(child, childOpts);
6648
6706
  el.appendChild(childEl);
6649
6707
  // Build local refs for addressable children
6650
6708
  var childRefId = child && child.a ? child.a.id || bw.getUUID(child) : null;
@@ -6667,7 +6725,7 @@
6667
6725
  // Raw HTML content — inject via innerHTML
6668
6726
  el.innerHTML = content.v;
6669
6727
  } else if (_is(content, 'object') && content.t) {
6670
- var childEl = bw.createDOM(content, options);
6728
+ var childEl = bw.createDOM(content, childOpts);
6671
6729
  el.appendChild(childEl);
6672
6730
  var childRefId = content.a ? content.a.id || bw.getUUID(content) : null;
6673
6731
  if (childRefId) {
@@ -6693,13 +6751,21 @@
6693
6751
  }
6694
6752
 
6695
6753
  // Register UUID class in node cache (bw_uuid_* tokens in class string)
6696
- if (el.className) {
6697
- var uuidMatch = el.className.match(_UUID_RE);
6754
+ // SVG elements have SVGAnimatedString for className; use getAttribute instead
6755
+ var clsStr = svgCtx ? el.getAttribute('class') || '' : el.className;
6756
+ if (clsStr) {
6757
+ var uuidMatch = clsStr.match(_UUID_RE);
6698
6758
  if (uuidMatch) {
6699
6759
  bw._nodeMap[uuidMatch[0]] = el;
6700
6760
  }
6701
6761
  }
6702
6762
 
6763
+ // Store component type metadata (e.g., 'card', 'tabs') for introspection.
6764
+ // BCCL factories set o.type; custom components can too.
6765
+ if (opts.type) {
6766
+ el._bw_type = opts.type;
6767
+ }
6768
+
6703
6769
  // Handle lifecycle hooks and state
6704
6770
  if (opts.mounted || opts.unmount || opts.render || opts.state) {
6705
6771
  // Ensure element has a UUID class for identity
@@ -6728,11 +6794,19 @@
6728
6794
  } : null);
6729
6795
  if (mountFn) {
6730
6796
  if (document.body.contains(el)) {
6731
- mountFn(el, el._bw_state || {});
6797
+ try {
6798
+ mountFn(el, el._bw_state || {});
6799
+ } catch (e) {
6800
+ _cw('o.mounted error: ' + e.message);
6801
+ }
6732
6802
  } else {
6733
6803
  requestAnimationFrame(function () {
6734
6804
  if (document.body.contains(el)) {
6735
- mountFn(el, el._bw_state || {});
6805
+ try {
6806
+ mountFn(el, el._bw_state || {});
6807
+ } catch (e) {
6808
+ _cw('o.mounted error: ' + e.message);
6809
+ }
6736
6810
  }
6737
6811
  });
6738
6812
  }
@@ -6741,7 +6815,11 @@
6741
6815
  // Store unmount callback keyed by UUID class
6742
6816
  if (opts.unmount) {
6743
6817
  bw._unmountCallbacks.set(uuid, function () {
6744
- opts.unmount(el, el._bw_state || {});
6818
+ try {
6819
+ opts.unmount(el, el._bw_state || {});
6820
+ } catch (e) {
6821
+ _cw('o.unmount error: ' + e.message);
6822
+ }
6745
6823
  });
6746
6824
  }
6747
6825
  }
@@ -6760,24 +6838,25 @@
6760
6838
  }
6761
6839
 
6762
6840
  // Slot declarations: auto-generate setX/getX pairs
6841
+ // The target element is cached at creation time to avoid repeated
6842
+ // querySelector calls on every get/set invocation.
6763
6843
  if (opts.slots) {
6764
6844
  for (var sk in opts.slots) {
6765
6845
  if (_hop.call(opts.slots, sk)) {
6766
6846
  (function (name, selector) {
6847
+ var target = el.querySelector(selector);
6767
6848
  var cap = name.charAt(0).toUpperCase() + name.slice(1);
6768
6849
  el.bw['set' + cap] = function (value) {
6769
- var t = el.querySelector(selector);
6770
- if (!t) return;
6850
+ if (!target) return;
6771
6851
  if (value != null && _typeof(value) === 'object' && value.t) {
6772
- t.innerHTML = '';
6773
- t.appendChild(bw.createDOM(value));
6852
+ target.innerHTML = '';
6853
+ target.appendChild(bw.createDOM(value));
6774
6854
  } else {
6775
- t.textContent = value != null ? String(value) : '';
6855
+ target.textContent = value != null ? String(value) : '';
6776
6856
  }
6777
6857
  };
6778
6858
  el.bw['get' + cap] = function () {
6779
- var t = el.querySelector(selector);
6780
- return t ? t.textContent : '';
6859
+ return target ? target.textContent : '';
6781
6860
  };
6782
6861
  })(sk, opts.slots[sk]);
6783
6862
  }
@@ -6818,7 +6897,7 @@
6818
6897
  }
6819
6898
 
6820
6899
  // Get target element (use cache-backed lookup)
6821
- var targetEl = bw._el(target);
6900
+ var targetEl = bw.el(target);
6822
6901
  if (!targetEl) {
6823
6902
  _ce('bw.DOM: Target element not found:', target);
6824
6903
  return null;
@@ -6921,7 +7000,8 @@
6921
7000
  // Deregister UUID classes from node cache for non-lifecycle UUID elements
6922
7001
  var uuidEls = element.querySelectorAll('[class*="bw_uuid_"]');
6923
7002
  uuidEls.forEach(function (uel) {
6924
- var m = uel.className && uel.className.match(_UUID_RE);
7003
+ var uc = typeof uel.className === 'string' ? uel.className : uel.getAttribute('class') || '';
7004
+ var m = uc && uc.match(_UUID_RE);
6925
7005
  if (m) delete bw._nodeMap[m[0]];
6926
7006
  });
6927
7007
 
@@ -7009,9 +7089,13 @@
7009
7089
  * bw.update(el); // re-renders, emits bw:statechange
7010
7090
  */
7011
7091
  bw.update = function (target) {
7012
- var el = bw._el(target);
7092
+ var el = bw.el(target);
7013
7093
  if (el && el._bw_render) {
7014
- el._bw_render(el, el._bw_state || {});
7094
+ try {
7095
+ el._bw_render(el, el._bw_state || {});
7096
+ } catch (e) {
7097
+ _cw('o.render error: ' + e.message);
7098
+ }
7015
7099
  bw.emit(el, 'statechange', el._bw_state);
7016
7100
  }
7017
7101
  return el || null;
@@ -7038,7 +7122,7 @@
7038
7122
  * bw.patch('info', { t: 'em', c: 'new' }); // replace children with TACO
7039
7123
  */
7040
7124
  bw.patch = function (id, content, attr) {
7041
- var el = bw._el(id);
7125
+ var el = bw.el(id);
7042
7126
  if (!el) return null;
7043
7127
  if (attr) {
7044
7128
  // Patch an attribute
@@ -7109,7 +7193,7 @@
7109
7193
  * // Dispatches CustomEvent 'bw:statechange' on the element
7110
7194
  */
7111
7195
  bw.emit = function (target, eventName, detail) {
7112
- var el = bw._el(target);
7196
+ var el = bw.el(target);
7113
7197
  if (el) {
7114
7198
  el.dispatchEvent(new CustomEvent('bw:' + eventName, {
7115
7199
  bubbles: true,
@@ -7138,7 +7222,7 @@
7138
7222
  * });
7139
7223
  */
7140
7224
  bw.on = function (target, eventName, handler) {
7141
- var el = bw._el(target);
7225
+ var el = bw.el(target);
7142
7226
  if (el) {
7143
7227
  el.addEventListener('bw:' + eventName, function (e) {
7144
7228
  handler(e.detail, e);
@@ -7165,23 +7249,46 @@
7165
7249
  *
7166
7250
  * @param {string} topic - Topic name (plain string, no prefix)
7167
7251
  * @param {*} [detail] - Data to pass to subscribers
7168
- * @returns {number} Count of successfully called subscribers
7252
+ * @returns {number} Count of successfully called subscribers (including wildcard matches)
7169
7253
  * @category Pub/Sub
7170
7254
  * @see bw.sub
7171
7255
  * @example
7172
7256
  * bw.pub('score:updated', { player: 'X', score: 10 });
7257
+ * // Wildcard subscribers matching 'score:*' will also fire
7173
7258
  */
7174
7259
  bw.pub = function (topic, detail) {
7175
- var subs = bw._topics[topic];
7176
- if (!subs || subs.length === 0) return 0;
7177
- var snapshot = subs.slice(); // safe against unsub during iteration
7178
7260
  var called = 0;
7179
- for (var i = 0; i < snapshot.length; i++) {
7180
- try {
7181
- snapshot[i].handler(detail);
7182
- called++;
7183
- } catch (err) {
7184
- _cw('bw.pub: subscriber error on topic "' + topic + '":', err);
7261
+ // Exact-match subscribers
7262
+ var subs = bw._topics[topic];
7263
+ if (subs && subs.length > 0) {
7264
+ var snapshot = subs.slice();
7265
+ for (var i = 0; i < snapshot.length; i++) {
7266
+ try {
7267
+ snapshot[i].handler(detail, topic);
7268
+ called++;
7269
+ } catch (err) {
7270
+ _cw('bw.pub: subscriber error on topic "' + topic + '":', err);
7271
+ }
7272
+ }
7273
+ }
7274
+ // Wildcard subscribers -- patterns ending with '*'
7275
+ var keys = Object.keys(bw._topics);
7276
+ for (var k = 0; k < keys.length; k++) {
7277
+ var pat = keys[k];
7278
+ if (pat.charAt(pat.length - 1) !== '*') continue;
7279
+ var prefix = pat.slice(0, -1); // strip trailing '*'
7280
+ if (topic.length >= prefix.length && topic.substring(0, prefix.length) === prefix && topic !== pat) {
7281
+ var wsubs = bw._topics[pat];
7282
+ if (!wsubs) continue;
7283
+ var wsnap = wsubs.slice();
7284
+ for (var w = 0; w < wsnap.length; w++) {
7285
+ try {
7286
+ wsnap[w].handler(detail, topic);
7287
+ called++;
7288
+ } catch (err) {
7289
+ _cw('bw.pub: wildcard subscriber error on "' + pat + '" for topic "' + topic + '":', err);
7290
+ }
7291
+ }
7185
7292
  }
7186
7293
  }
7187
7294
  return called;
@@ -7190,12 +7297,17 @@
7190
7297
  /**
7191
7298
  * Subscribe to a topic. Returns an unsub() function.
7192
7299
  *
7193
- * Optional third argument ties the subscription to a DOM element's lifecycle —
7300
+ * Supports wildcard patterns: a topic ending in `*` matches any published
7301
+ * topic that starts with the prefix before the `*`. For example,
7302
+ * `'agui:*'` matches `'agui:ready'`, `'agui:error'`, etc. The handler
7303
+ * receives `(detail, topic)` so it can distinguish which topic fired.
7304
+ *
7305
+ * Optional third argument ties the subscription to a DOM element's lifecycle --
7194
7306
  * when `bw.cleanup()` is called on that element, the subscription is automatically
7195
7307
  * removed, preventing memory leaks.
7196
7308
  *
7197
- * @param {string} topic - Topic name
7198
- * @param {Function} handler - Called with (detail) on each publish
7309
+ * @param {string} topic - Topic name, or wildcard pattern ending in '*'
7310
+ * @param {Function} handler - Called with (detail, topic) on each publish
7199
7311
  * @param {Element} [el] - Optional DOM element to tie lifecycle to
7200
7312
  * @returns {Function} Call to unsubscribe
7201
7313
  * @category Pub/Sub
@@ -7206,6 +7318,11 @@
7206
7318
  * console.log(detail.player, 'scored', detail.score);
7207
7319
  * });
7208
7320
  * // Later: unsub() to stop listening
7321
+ *
7322
+ * // Wildcard: listen to all 'agui:' topics
7323
+ * bw.sub('agui:*', function(detail, topic) {
7324
+ * console.log('Got', topic, detail);
7325
+ * });
7209
7326
  */
7210
7327
  bw.sub = function (topic, handler, el) {
7211
7328
  var id = ++bw._subIdCounter;
@@ -7262,6 +7379,37 @@
7262
7379
  return removed;
7263
7380
  };
7264
7381
 
7382
+ /**
7383
+ * Subscribe to a topic for a single event only. The subscription is
7384
+ * automatically removed after the first publish. Equivalent to manually
7385
+ * calling unsub() inside a bw.sub() handler, but avoids the common bug
7386
+ * of forgetting to unsubscribe.
7387
+ *
7388
+ * @param {string} topic - Topic name
7389
+ * @param {Function} handler - Called once with (detail) on the next publish
7390
+ * @param {Element} [el] - Optional DOM element to tie lifecycle to
7391
+ * @returns {Function} Call to cancel the subscription before it fires
7392
+ * @category Pub/Sub
7393
+ * @see bw.sub
7394
+ * @see bw.pub
7395
+ * @example
7396
+ * bw.once('data:loaded', function(detail) {
7397
+ * console.log('Received:', detail);
7398
+ * // No need to unsubscribe -- already done automatically
7399
+ * });
7400
+ *
7401
+ * // Cancel before it fires:
7402
+ * var cancel = bw.once('timeout', handler);
7403
+ * cancel(); // handler will never be called
7404
+ */
7405
+ bw.once = function (topic, handler, el) {
7406
+ var unsub = bw.sub(topic, function (detail) {
7407
+ unsub();
7408
+ handler(detail);
7409
+ }, el);
7410
+ return unsub;
7411
+ };
7412
+
7265
7413
  // ===================================================================================
7266
7414
  // Function Registry (revived from v1 for string dispatch contexts)
7267
7415
  // ===================================================================================
@@ -7505,7 +7653,7 @@
7505
7653
  * };
7506
7654
  */
7507
7655
  bw.message = function (target, action, data) {
7508
- var el = bw._el(target);
7656
+ var el = bw.el(target);
7509
7657
  if (!el) el = bw.$('.' + target)[0];
7510
7658
  if (!el || !el.bw || typeof el.bw[action] !== 'function') {
7511
7659
  _cw('bw.message: no handle method "' + action + '" on ' + target);
@@ -7515,6 +7663,217 @@
7515
7663
  return true;
7516
7664
  };
7517
7665
 
7666
+ /**
7667
+ * Collect form data from all input, select, and textarea elements within a
7668
+ * container. Each element's `name` attribute (or `id` if no name) becomes a
7669
+ * key in the returned object. This provides a lightweight alternative to the
7670
+ * browser FormData API that returns a plain object suitable for JSON
7671
+ * serialization or bw.pub().
7672
+ *
7673
+ * Handles all standard HTML form controls:
7674
+ * - text/number/email/etc inputs: string value
7675
+ * - checkboxes: boolean (true/false)
7676
+ * - radio buttons: string value of the checked radio (unchecked groups omitted)
7677
+ * - multi-select: array of selected option values
7678
+ * - textarea: string value
7679
+ *
7680
+ * Elements without both `name` and `id` attributes are silently skipped.
7681
+ *
7682
+ * @param {string|Element} target - CSS selector, UUID string, or DOM element
7683
+ * @returns {Object} Plain object mapping field names to values
7684
+ * @category Component
7685
+ * @see bw.makeForm
7686
+ * @see bw.makeInput
7687
+ * @example
7688
+ * // Given a form with name="email" input and name="agree" checkbox:
7689
+ * var data = bw.formData('#signup-form');
7690
+ * // => { email: 'user@example.com', agree: true }
7691
+ *
7692
+ * // Collect and publish in one step:
7693
+ * bw.pub('form:submit', bw.formData('#my-form'));
7694
+ *
7695
+ * // Works with any container, not just <form>:
7696
+ * bw.pub('settings:changed', bw.formData('.settings-panel'));
7697
+ */
7698
+ bw.formData = function (target) {
7699
+ var el = bw.el(target);
7700
+ if (!el) return {};
7701
+ var result = {};
7702
+ var inputs = el.querySelectorAll('input, select, textarea');
7703
+ for (var i = 0; i < inputs.length; i++) {
7704
+ var inp = inputs[i];
7705
+ var key = inp.name || inp.id;
7706
+ if (!key) continue;
7707
+ if (inp.type === 'checkbox') {
7708
+ result[key] = inp.checked;
7709
+ } else if (inp.type === 'radio') {
7710
+ if (inp.checked) result[key] = inp.value;
7711
+ } else if (inp.tagName === 'SELECT' && inp.multiple) {
7712
+ result[key] = [];
7713
+ for (var j = 0; j < inp.options.length; j++) {
7714
+ if (inp.options[j].selected) result[key].push(inp.options[j].value);
7715
+ }
7716
+ } else {
7717
+ result[key] = inp.value;
7718
+ }
7719
+ }
7720
+ return result;
7721
+ };
7722
+
7723
+ // ===================================================================================
7724
+ // bw.jsonPatch() — RFC 6902 JSON Patch on plain objects
7725
+ // ===================================================================================
7726
+
7727
+ /**
7728
+ * Apply RFC 6902 JSON Patch operations to a plain object.
7729
+ *
7730
+ * Supported operations: add, remove, replace, move, copy, test.
7731
+ * Paths use JSON Pointer (RFC 6901) notation: `/foo/bar/0`.
7732
+ * Mutates the target object in place and returns it.
7733
+ *
7734
+ * @param {Object} obj - Target object to patch
7735
+ * @param {Array<Object>} ops - Array of patch operations
7736
+ * @param {string} ops[].op - Operation: 'add', 'remove', 'replace', 'move', 'copy', 'test'
7737
+ * @param {string} ops[].path - JSON Pointer path (e.g. '/a/b/0')
7738
+ * @param {*} [ops[].value] - Value for add/replace/test
7739
+ * @param {string} [ops[].from] - Source path for move/copy
7740
+ * @returns {Object} The patched object (same reference)
7741
+ * @throws {Error} On invalid op, missing path, test failure, or path not found for remove
7742
+ * @category Data Utilities
7743
+ * @see bw.patch
7744
+ * @example
7745
+ * var obj = { a: 1, b: { c: 2 } };
7746
+ * bw.jsonPatch(obj, [
7747
+ * { op: 'replace', path: '/a', value: 10 },
7748
+ * { op: 'add', path: '/b/d', value: 3 },
7749
+ * { op: 'remove', path: '/b/c' }
7750
+ * ]);
7751
+ * // obj => { a: 10, b: { d: 3 } }
7752
+ */
7753
+ bw.jsonPatch = function (obj, ops) {
7754
+ if (!_isA(ops)) return obj;
7755
+
7756
+ // Parse JSON Pointer path to array of keys
7757
+ function parsePath(path) {
7758
+ if (path === '') return [];
7759
+ if (path.charAt(0) !== '/') throw new Error('Invalid JSON Pointer: ' + path);
7760
+ return path.slice(1).split('/').map(function (s) {
7761
+ return s.replace(/~1/g, '/').replace(/~0/g, '~');
7762
+ });
7763
+ }
7764
+
7765
+ // Walk to parent of final key; return { parent, key }
7766
+ function resolve(root, keys) {
7767
+ var parent = root;
7768
+ for (var i = 0; i < keys.length - 1; i++) {
7769
+ var k = _isA(parent) ? parseInt(keys[i], 10) : keys[i];
7770
+ if (parent[k] === undefined) throw new Error('Path not found: /' + keys.slice(0, i + 1).join('/'));
7771
+ parent = parent[k];
7772
+ }
7773
+ return {
7774
+ parent: parent,
7775
+ key: _isA(parent) ? parseInt(keys[keys.length - 1], 10) : keys[keys.length - 1]
7776
+ };
7777
+ }
7778
+
7779
+ // Get value at path
7780
+ function getVal(root, keys) {
7781
+ var cur = root;
7782
+ for (var i = 0; i < keys.length; i++) {
7783
+ var k = _isA(cur) ? parseInt(keys[i], 10) : keys[i];
7784
+ if (cur[k] === undefined) throw new Error('Path not found: /' + keys.slice(0, i + 1).join('/'));
7785
+ cur = cur[k];
7786
+ }
7787
+ return cur;
7788
+ }
7789
+ for (var i = 0; i < ops.length; i++) {
7790
+ var op = ops[i];
7791
+ if (!op.op || !_is(op.path, 'string')) throw new Error('Invalid patch operation at index ' + i);
7792
+ var keys = parsePath(op.path);
7793
+ var r, val, fromKeys, fr, tr, cr;
7794
+ switch (op.op) {
7795
+ case 'add':
7796
+ {
7797
+ if (keys.length === 0) throw new Error('Cannot add to root');
7798
+ r = resolve(obj, keys);
7799
+ if (_isA(r.parent) && r.key <= r.parent.length) {
7800
+ r.parent.splice(r.key, 0, op.value);
7801
+ } else {
7802
+ r.parent[r.key] = op.value;
7803
+ }
7804
+ break;
7805
+ }
7806
+ case 'remove':
7807
+ {
7808
+ if (keys.length === 0) throw new Error('Cannot remove root');
7809
+ r = resolve(obj, keys);
7810
+ if (_isA(r.parent)) {
7811
+ if (r.key >= r.parent.length) throw new Error('Index out of bounds: ' + r.key);
7812
+ r.parent.splice(r.key, 1);
7813
+ } else {
7814
+ if (!(r.key in r.parent)) throw new Error('Path not found: ' + op.path);
7815
+ delete r.parent[r.key];
7816
+ }
7817
+ break;
7818
+ }
7819
+ case 'replace':
7820
+ {
7821
+ if (keys.length === 0) throw new Error('Cannot replace root');
7822
+ r = resolve(obj, keys);
7823
+ if (_isA(r.parent)) {
7824
+ if (r.key >= r.parent.length) throw new Error('Index out of bounds: ' + r.key);
7825
+ } else {
7826
+ if (!(r.key in r.parent)) throw new Error('Path not found: ' + op.path);
7827
+ }
7828
+ r.parent[r.key] = op.value;
7829
+ break;
7830
+ }
7831
+ case 'move':
7832
+ {
7833
+ if (!_is(op.from, 'string')) throw new Error('move requires "from"');
7834
+ fromKeys = parsePath(op.from);
7835
+ val = getVal(obj, fromKeys);
7836
+ fr = resolve(obj, fromKeys);
7837
+ if (_isA(fr.parent)) {
7838
+ fr.parent.splice(fr.key, 1);
7839
+ } else {
7840
+ delete fr.parent[fr.key];
7841
+ }
7842
+ tr = resolve(obj, keys);
7843
+ if (_isA(tr.parent) && tr.key <= tr.parent.length) {
7844
+ tr.parent.splice(tr.key, 0, val);
7845
+ } else {
7846
+ tr.parent[tr.key] = val;
7847
+ }
7848
+ break;
7849
+ }
7850
+ case 'copy':
7851
+ {
7852
+ if (!_is(op.from, 'string')) throw new Error('copy requires "from"');
7853
+ val = getVal(obj, parsePath(op.from));
7854
+ cr = resolve(obj, keys);
7855
+ if (_isA(cr.parent) && cr.key <= cr.parent.length) {
7856
+ cr.parent.splice(cr.key, 0, val);
7857
+ } else {
7858
+ cr.parent[cr.key] = val;
7859
+ }
7860
+ break;
7861
+ }
7862
+ case 'test':
7863
+ {
7864
+ var actual = getVal(obj, keys);
7865
+ if (JSON.stringify(actual) !== JSON.stringify(op.value)) {
7866
+ throw new Error('Test failed: ' + op.path + ' expected ' + JSON.stringify(op.value) + ' got ' + JSON.stringify(actual));
7867
+ }
7868
+ break;
7869
+ }
7870
+ default:
7871
+ throw new Error('Unknown op: ' + op.op);
7872
+ }
7873
+ }
7874
+ return obj;
7875
+ };
7876
+
7518
7877
  // ===================================================================================
7519
7878
  // bw.apply() / bw.parseJSONFlex() — Server-driven UI protocol
7520
7879
  // ===================================================================================
@@ -7647,7 +8006,7 @@
7647
8006
  var type = msg.type;
7648
8007
  var target = msg.target;
7649
8008
  if (type === 'replace') {
7650
- var el = bw._el(target);
8009
+ var el = bw.el(target);
7651
8010
  if (!el) return false;
7652
8011
  bw.DOM(el, msg.node);
7653
8012
  return true;
@@ -7655,13 +8014,13 @@
7655
8014
  var patched = bw.patch(target, msg.content, msg.attr);
7656
8015
  return patched !== null;
7657
8016
  } else if (type === 'append') {
7658
- var parent = bw._el(target);
8017
+ var parent = bw.el(target);
7659
8018
  if (!parent) return false;
7660
8019
  var child = bw.createDOM(msg.node);
7661
8020
  parent.appendChild(child);
7662
8021
  return true;
7663
8022
  } else if (type === 'remove') {
7664
- var toRemove = bw._el(target);
8023
+ var toRemove = bw.el(target);
7665
8024
  if (!toRemove) return false;
7666
8025
  if (_is(bw.cleanup, 'function')) bw.cleanup(toRemove);
7667
8026
  toRemove.remove();
@@ -7714,33 +8073,99 @@
7714
8073
  };
7715
8074
 
7716
8075
  // ===================================================================================
7717
- // bw.inspect() — Debug utility
8076
+ // bw.inspect() — DOM introspection with bitwrench metadata
7718
8077
  // ===================================================================================
7719
8078
 
7720
8079
  /**
7721
- * Inspect a DOM element's bitwrench state, handle methods, and metadata.
7722
- * Works with DOM elements or CSS selectors.
7723
- *
7724
- * @param {string|Element} target - Selector or DOM element
7725
- * @returns {Element|null} The element, or null if not found
8080
+ * Inspect a DOM element and its subtree, returning a plain-object
8081
+ * representation with bitwrench metadata at each node. Useful for debugging,
8082
+ * devtools, MCP/AG-UI tool discovery, and automated testing.
8083
+ *
8084
+ * Each node in the returned tree includes:
8085
+ * - `tag` -- lowercase tag name (or '#text' for text nodes)
8086
+ * - `id` -- element id (if set)
8087
+ * - `uuid` -- bitwrench UUID class (if lifecycle-managed)
8088
+ * - `type` -- component type from o.type (if set, e.g. 'card', 'tabs')
8089
+ * - `classes` -- first 5 CSS classes (string, space-separated)
8090
+ * - `handles` -- array of el.bw method names (if any)
8091
+ * - `state` -- copy of _bw_state (if any)
8092
+ * - `hasRender` -- true if _bw_render is set
8093
+ * - `hasSubs` -- true if element has pub/sub subscriptions
8094
+ * - `refs` -- copy of _bw_refs keys (if any)
8095
+ * - `children` -- array of child node trees (up to depth limit, max 50 per level)
8096
+ *
8097
+ * @param {string|Element} target - CSS selector, UUID, or DOM element
8098
+ * @param {number} [depth=3] - Maximum recursion depth (0 = target only, no children)
8099
+ * @returns {Object|null} Plain object tree, or null if element not found
7726
8100
  * @category Component
7727
8101
  * @example
7728
- * bw.inspect('#my-carousel');
7729
- * bw.inspect($0);
8102
+ * // Get full tree from #app, 3 levels deep (default):
8103
+ * var info = bw.inspect('#app');
8104
+ *
8105
+ * // Shallow inspection (just the element, no children):
8106
+ * var info = bw.inspect('#my-carousel', 0);
8107
+ * console.log(info.handles); // ['next', 'prev', 'goToSlide']
8108
+ * console.log(info.type); // 'carousel'
8109
+ *
8110
+ * // Deep inspection for debugging:
8111
+ * console.log(JSON.stringify(bw.inspect('#app', 5), null, 2));
7730
8112
  */
7731
- bw.inspect = function (target) {
7732
- var el = _is(target, 'string') ? bw.$(target)[0] : target;
7733
- if (!el) {
7734
- _cw('bw.inspect: element not found');
7735
- return null;
8113
+ bw.inspect = function (target, depth) {
8114
+ var el = bw.el(target);
8115
+ if (!el && _is(target, 'string')) el = bw.$(target)[0];
8116
+ if (!el) return null;
8117
+ if (depth === undefined || depth === null) depth = 3;
8118
+ function walk(node, d) {
8119
+ if (!node) return null;
8120
+ // Skip non-element nodes (text, comment, etc.)
8121
+ if (node.nodeType !== 1) return null;
8122
+ var info = {
8123
+ tag: node.tagName ? node.tagName.toLowerCase() : '#text'
8124
+ };
8125
+
8126
+ // Identity
8127
+ if (node.id) info.id = node.id;
8128
+ var uuid = bw.getUUID(node);
8129
+ if (uuid) info.uuid = uuid;
8130
+ if (node._bw_type) info.type = node._bw_type;
8131
+
8132
+ // CSS classes (first 5 for readability)
8133
+ if (node.className && typeof node.className === 'string') {
8134
+ info.classes = node.className.split(' ').slice(0, 5).join(' ');
8135
+ }
8136
+
8137
+ // Bitwrench handle methods
8138
+ if (node.bw) {
8139
+ var handles = _keys(node.bw);
8140
+ if (handles.length > 0) info.handles = handles;
8141
+ }
8142
+
8143
+ // State
8144
+ if (node._bw_state) info.state = node._bw_state;
8145
+ if (node._bw_render) info.hasRender = true;
8146
+ if (node._bw_subs && node._bw_subs.length > 0) info.hasSubs = true;
8147
+
8148
+ // Refs
8149
+ if (node._bw_refs) info.refs = _keys(node._bw_refs);
8150
+
8151
+ // Children (recurse up to depth limit, max 50 children per level)
8152
+ if (d < depth && node.children && node.children.length > 0) {
8153
+ info.children = [];
8154
+ var max = Math.min(node.children.length, 50);
8155
+ for (var i = 0; i < max; i++) {
8156
+ var child = walk(node.children[i], d + 1);
8157
+ if (child) info.children.push(child);
8158
+ }
8159
+ if (node.children.length > 50) {
8160
+ info.children.push({
8161
+ tag: '...',
8162
+ count: node.children.length - 50
8163
+ });
8164
+ }
8165
+ }
8166
+ return info;
7736
8167
  }
7737
- console.group('Element: ' + (bw.getUUID(el) || el.id || el.tagName));
7738
- _cl('State:', el._bw_state || '(none)');
7739
- _cl('Handle:', el.bw ? _keys(el.bw) : '(none)');
7740
- _cl('Classes:', el.className);
7741
- _cl('Refs:', el._bw_refs || '(none)');
7742
- console.groupEnd();
7743
- return el;
8168
+ return walk(el, 0);
7744
8169
  };
7745
8170
  bw.compile = function () {
7746
8171
  throw new Error('bw.compile() removed in v2.0.19. Use o.handle/o.slots on TACO options instead.');
@@ -7979,34 +8404,45 @@
7979
8404
  * so you can use `.map()`, `.filter()`, etc. directly. Accepts CSS selectors,
7980
8405
  * single elements, NodeLists, or arrays.
7981
8406
  *
8407
+ * With an optional second argument, applies content or a function to
8408
+ * every matched element (same apply rules as `bw.el()`):
8409
+ * - string/number: sets `el.textContent`
8410
+ * - function: calls `apply(el)` for each element
8411
+ * - TACO object: clears children, mounts TACO via `bw.createDOM()`
8412
+ * - array: clears children, appends each item
8413
+ *
7982
8414
  * @param {string|Element|Array} selector - CSS selector, element, or array
8415
+ * @param {string|number|Function|Object|Array} [apply] - Content or function to apply
7983
8416
  * @returns {Array} Array of DOM elements
7984
8417
  * @category DOM Selection
8418
+ * @see bw.el
7985
8419
  * @example
7986
- * bw.$('.card') // => [div.card, div.card, ...]
7987
- * bw.$(myElement) // => [myElement]
7988
- * bw.$('.card').map(el => el.textContent)
8420
+ * bw.$('.card') // => [div.card, div.card, ...]
8421
+ * bw.$('.status', 'Online') // set text on all .status elements
8422
+ * bw.$('.card', function(el) { // apply function to each
8423
+ * el.style.opacity = '0.5';
8424
+ * })
7989
8425
  */
7990
8426
  if (bw._isBrowser) {
7991
- bw.$ = function (selector) {
7992
- if (!selector) return [];
7993
-
7994
- // Already an array
7995
- if (_isA(selector)) return selector;
7996
-
7997
- // Single element
7998
- if (selector.nodeType) return [selector];
7999
-
8000
- // NodeList or HTMLCollection
8001
- if (selector.length !== undefined && !_is(selector, 'string')) {
8002
- return Array.from(selector);
8427
+ bw.$ = function (selector, apply) {
8428
+ var els;
8429
+ if (!selector) {
8430
+ els = [];
8431
+ } else if (_isA(selector)) {
8432
+ els = selector;
8433
+ } else if (selector.nodeType) {
8434
+ els = [selector];
8435
+ } else if (selector.length !== undefined && !_is(selector, 'string')) {
8436
+ els = Array.from(selector);
8437
+ } else if (_is(selector, 'string')) {
8438
+ els = Array.from(document.querySelectorAll(selector));
8439
+ } else {
8440
+ els = [];
8003
8441
  }
8004
-
8005
- // CSS selector string
8006
- if (_is(selector, 'string')) {
8007
- return Array.from(document.querySelectorAll(selector));
8442
+ if (apply !== undefined) {
8443
+ for (var i = 0; i < els.length; i++) _applyTo(els[i], apply);
8008
8444
  }
8009
- return [];
8445
+ return els;
8010
8446
  };
8011
8447
 
8012
8448
  // Convenience single element selector
@@ -8160,7 +8596,8 @@
8160
8596
  *
8161
8597
  * @param {Object} [config] - Style configuration (same as `makeStyles`)
8162
8598
  * @param {string} [scope] - Scope selector (same as `applyStyles`)
8163
- * @returns {Element|null} The `<style>` element, or null in Node.js
8599
+ * @returns {Object} The styles object (same as `makeStyles` return value:
8600
+ * `{css, alternateCss, palette, alternatePalette, rules, alternateRules, isLightPrimary}`)
8164
8601
  * @category CSS & Styling
8165
8602
  * @see bw.makeStyles
8166
8603
  * @see bw.applyStyles
@@ -8181,9 +8618,27 @@
8181
8618
  });
8182
8619
  }
8183
8620
  }
8184
- return bw.applyStyles(bw.makeStyles(config), scope);
8621
+ var styles = bw.makeStyles(config);
8622
+ bw.applyStyles(styles, scope);
8623
+ return styles;
8185
8624
  };
8186
8625
 
8626
+ /**
8627
+ * Prefix every selector in a rules object with a scope selector.
8628
+ * Useful for wrapping site-level CSS under `.bw_theme_alt` for dark mode.
8629
+ *
8630
+ * @param {Object} rules - CSS rules object (selector -> declarations)
8631
+ * @param {string} prefix - Scope prefix (e.g. '.bw_theme_alt')
8632
+ * @returns {Object} New rules object with scoped selectors
8633
+ * @category CSS & Styling
8634
+ * @see bw.applyStyles
8635
+ * @see bw.css
8636
+ * @example
8637
+ * var altRules = bw.scopeRulesUnder(myRules, '.bw_theme_alt');
8638
+ * bw.injectCSS(bw.css(altRules));
8639
+ */
8640
+ bw.scopeRulesUnder = scopeRulesUnder;
8641
+
8187
8642
  /**
8188
8643
  * Inject the CSS reset (box-sizing, html/body font, reduced-motion).
8189
8644
  * Idempotent — if already injected, returns the existing `<style>` element.
@@ -8206,41 +8661,47 @@
8206
8661
  };
8207
8662
 
8208
8663
  /**
8209
- * Toggle between primary and alternate palettes.
8664
+ * Toggle between primary and alternate theme palettes.
8210
8665
  *
8211
- * Adds/removes the `bw_theme_alt` class on the scoping element.
8666
+ * Adds/removes the `bw_theme_alt` class on the scoping element(s).
8212
8667
  * Without a scope, toggles on `<html>` (global).
8213
- * With a scope, toggles on the first matching element.
8668
+ * With a scope, toggles on ALL matching elements.
8214
8669
  *
8215
- * @param {string} [scope] - Scope selector (e.g. '#my-dashboard'). Omit for global.
8216
- * @returns {string} Active mode after toggle: 'primary' or 'alternate'
8670
+ * @param {string|Element} [scope] - Selector or element. Omit for global.
8671
+ * @returns {string} Active mode after toggle: 'primary' or 'alternate' (based on first element)
8217
8672
  * @category CSS & Styling
8218
8673
  * @see bw.applyStyles
8219
8674
  * @see bw.clearStyles
8220
8675
  * @example
8221
- * bw.toggleStyles(); // global toggle on <html>
8222
- * bw.toggleStyles('#my-dashboard'); // scoped toggle
8676
+ * bw.toggleThemeMode(); // global toggle on <html>
8677
+ * bw.toggleThemeMode('#my-dashboard'); // scoped toggle
8678
+ * bw.toggleThemeMode('.panel'); // toggle on ALL .panel elements
8223
8679
  */
8224
- bw.toggleStyles = function (scope) {
8680
+ bw.toggleThemeMode = function (scope) {
8225
8681
  if (!bw._isBrowser) return 'primary';
8226
- var target;
8682
+ var els;
8227
8683
  if (scope) {
8228
- var els = bw.$(scope);
8229
- target = els[0];
8684
+ els = bw.$(scope);
8230
8685
  } else {
8231
- target = document.documentElement;
8232
- }
8233
- if (!target) return 'primary';
8234
- var hasAlt = target.classList.contains('bw_theme_alt');
8235
- if (hasAlt) {
8236
- target.classList.remove('bw_theme_alt');
8237
- return 'primary';
8238
- } else {
8239
- target.classList.add('bw_theme_alt');
8240
- return 'alternate';
8686
+ els = [document.documentElement];
8687
+ }
8688
+ if (!els.length) return 'primary';
8689
+ var mode;
8690
+ for (var i = 0; i < els.length; i++) {
8691
+ var hasAlt = els[i].classList.contains('bw_theme_alt');
8692
+ if (hasAlt) {
8693
+ els[i].classList.remove('bw_theme_alt');
8694
+ } else {
8695
+ els[i].classList.add('bw_theme_alt');
8696
+ }
8697
+ if (i === 0) mode = hasAlt ? 'primary' : 'alternate';
8241
8698
  }
8699
+ return mode;
8242
8700
  };
8243
8701
 
8702
+ // Alias — kept for one release cycle. Use bw.toggleThemeMode() instead.
8703
+ bw.toggleStyles = bw.toggleThemeMode;
8704
+
8244
8705
  /**
8245
8706
  * Remove injected styles for a given scope.
8246
8707
  *
@@ -9340,6 +9801,57 @@
9340
9801
  }
9341
9802
  });
9342
9803
 
9804
+ /**
9805
+ * Query the BCCL component registry. Returns metadata about registered
9806
+ * component types -- their names and factory function names. Useful for
9807
+ * tooling, introspection, documentation generators, and auto-complete
9808
+ * systems (including MCP/AG-UI tool discovery).
9809
+ *
9810
+ * With no arguments, returns an array of all registered component types.
9811
+ * With a type name, returns metadata for that single type (or null if
9812
+ * the type is not registered).
9813
+ *
9814
+ * @param {string} [type] - Optional component type name to look up
9815
+ * @returns {Array<Object>|Object|null} Array of {type, factory} objects,
9816
+ * a single {type, factory} object, or null if the type is not found
9817
+ * @category Component
9818
+ * @see bw.make
9819
+ * @see bw.BCCL
9820
+ * @example
9821
+ * // List all available component types:
9822
+ * bw.catalog();
9823
+ * // => [{ type: 'card', factory: 'makeCard' },
9824
+ * // { type: 'button', factory: 'makeButton' }, ...]
9825
+ *
9826
+ * // Look up a specific type:
9827
+ * bw.catalog('accordion');
9828
+ * // => { type: 'accordion', factory: 'makeAccordion' }
9829
+ *
9830
+ * // Check if a type exists:
9831
+ * if (bw.catalog('chart')) { ... }
9832
+ *
9833
+ * // Get just the type names:
9834
+ * bw.catalog().map(function(c) { return c.type; });
9835
+ * // => ['card', 'button', 'container', 'row', ...]
9836
+ */
9837
+ bw.catalog = function (type) {
9838
+ if (type) {
9839
+ var def = bw.BCCL[type];
9840
+ if (!def) return null;
9841
+ return {
9842
+ type: type,
9843
+ factory: def.make.name || 'make' + type.charAt(0).toUpperCase() + type.slice(1)
9844
+ };
9845
+ }
9846
+ return Object.keys(bw.BCCL).map(function (k) {
9847
+ var def = bw.BCCL[k];
9848
+ return {
9849
+ type: k,
9850
+ factory: def.make.name || 'make' + k.charAt(0).toUpperCase() + k.slice(1)
9851
+ };
9852
+ });
9853
+ };
9854
+
9343
9855
  // Also attach to global in browsers
9344
9856
  if (bw._isBrowser && typeof window !== 'undefined') {
9345
9857
  window.bw = bw;