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) :
@@ -12,14 +12,14 @@
12
12
  */
13
13
 
14
14
  const VERSION_INFO = {
15
- version: '2.0.24',
15
+ version: '2.0.30',
16
16
  name: 'bitwrench',
17
17
  description: 'A library for javascript UI functions.',
18
18
  license: 'BSD-2-Clause',
19
19
  homepage: 'https://deftio.github.com/bitwrench/pages',
20
20
  repository: 'git+https://github.com/deftio/bitwrench.git',
21
21
  author: 'manu a. chatterjee <deftio@deftio.com> (https://deftio.com/)',
22
- buildDate: '2026-03-28T09:12:50.073Z'
22
+ buildDate: '2026-04-12T07:51:29.111Z'
23
23
  };
24
24
 
25
25
  /**
@@ -709,7 +709,7 @@
709
709
  'transition': 'color ' + mot.fast + ' ' + mot.easing
710
710
  };
711
711
  rules[_sx(scope, 'a:hover')] = {
712
- 'color': palette.primary.hover,
712
+ 'color': palette.tertiary.hover,
713
713
  'text-decoration': 'underline'
714
714
  };
715
715
  return rules;
@@ -889,7 +889,7 @@
889
889
  'transition': 'color ' + layout.motion.fast + ' ' + layout.motion.easing + ', background-color ' + layout.motion.fast + ' ' + layout.motion.easing
890
890
  };
891
891
  rules[_sx(scope, '.bw_navbar_nav .bw_nav_link:hover')] = {
892
- 'color': palette.dark.base,
892
+ 'color': palette.tertiary.base,
893
893
  'background-color': palette.surfaceAlt
894
894
  };
895
895
  rules[_sx(scope, '.bw_navbar_nav .bw_nav_link.active')] = {
@@ -970,7 +970,7 @@
970
970
  'transition': 'color ' + mo.fast + ' ' + mo.easing + ', border-color ' + mo.fast + ' ' + mo.easing + ', background-color ' + mo.fast + ' ' + mo.easing
971
971
  };
972
972
  rules[_sx(scope, '.bw_nav_tabs .bw_nav_link:hover')] = {
973
- 'color': palette.dark.base,
973
+ 'color': palette.tertiary.base,
974
974
  'background-color': palette.surfaceAlt,
975
975
  'border-bottom-color': palette.light.border
976
976
  };
@@ -996,7 +996,7 @@
996
996
  };
997
997
  rules[_sx(scope, 'a.bw_list_group_item:hover')] = {
998
998
  'background-color': palette.surfaceAlt,
999
- 'color': palette.dark.hover
999
+ 'color': palette.tertiary.base
1000
1000
  };
1001
1001
  rules[_sx(scope, '.bw_list_group_item.active')] = {
1002
1002
  'color': palette.primary.textOn,
@@ -1093,11 +1093,11 @@
1093
1093
  'color': palette.secondary.base
1094
1094
  };
1095
1095
  rules[_sx(scope, '.bw_breadcrumb_item a')] = {
1096
- 'color': palette.primary.base,
1096
+ 'color': palette.tertiary.base,
1097
1097
  'transition': 'color ' + mo.fast + ' ' + mo.easing
1098
1098
  };
1099
1099
  rules[_sx(scope, '.bw_breadcrumb_item a:hover')] = {
1100
- 'color': palette.primary.hover,
1100
+ 'color': palette.tertiary.hover,
1101
1101
  'text-decoration': 'underline'
1102
1102
  };
1103
1103
  rules[_sx(scope, '.bw_breadcrumb_item.active')] = {
@@ -1332,11 +1332,11 @@
1332
1332
  'font-weight': '600'
1333
1333
  };
1334
1334
  rules[_sx(scope, '.bw_step_completed .bw_step_indicator')] = {
1335
- 'background-color': palette.primary.base,
1336
- 'color': palette.primary.textOn
1335
+ 'background-color': palette.tertiary.base,
1336
+ 'color': palette.tertiary.textOn
1337
1337
  };
1338
- rules[_sx(scope, '.bw_step_completed .bw_step_label')] = { 'color': palette.primary.base };
1339
- rules[_sx(scope, '.bw_step_completed + .bw_step::before')] = { 'background-color': palette.primary.base };
1338
+ rules[_sx(scope, '.bw_step_completed .bw_step_label')] = { 'color': palette.tertiary.base };
1339
+ rules[_sx(scope, '.bw_step_completed + .bw_step::before')] = { 'background-color': palette.tertiary.base };
1340
1340
  return rules;
1341
1341
  }
1342
1342
 
@@ -1598,14 +1598,14 @@
1598
1598
  };
1599
1599
  });
1600
1600
 
1601
- // Text muted — always a neutral gray, never a brand color
1602
- rules[_sx(scope, '.bw_text_muted')] = { 'color': '#6c757d' };
1601
+ // Text muted — uses palette secondary for theme-aware muted text
1602
+ rules[_sx(scope, '.bw_text_muted')] = { 'color': palette.secondary.base };
1603
1603
 
1604
- // Common bg/text utilities that aren't per-variant
1605
- rules[_sx(scope, '.bw_bg_dark')] = { 'background-color': '#212529', 'color': '#f8f9fa' };
1606
- rules[_sx(scope, '.bw_bg_light')] = { 'background-color': '#f8f9fa', 'color': '#212529' };
1607
- rules[_sx(scope, '.bw_text_light')] = { 'color': '#f8f9fa' };
1608
- rules[_sx(scope, '.bw_text_dark')] = { 'color': '#212529' };
1604
+ // Common bg/text utilities derive from palette for theme awareness
1605
+ rules[_sx(scope, '.bw_bg_dark')] = { 'background-color': palette.dark.base, 'color': palette.dark.textOn };
1606
+ rules[_sx(scope, '.bw_bg_light')] = { 'background-color': palette.light.base, 'color': palette.light.textOn };
1607
+ rules[_sx(scope, '.bw_text_light')] = { 'color': palette.light.base };
1608
+ rules[_sx(scope, '.bw_text_dark')] = { 'color': palette.dark.base };
1609
1609
 
1610
1610
  return rules;
1611
1611
  }
@@ -3955,7 +3955,6 @@
3955
3955
  // Console aliases use thin wrappers (not direct references) so that test
3956
3956
  // code can monkey-patch console.warn/log/error and the patches take effect.
3957
3957
  var _cw = function() { console.warn.apply(console, arguments); };
3958
- var _cl = function() { console.log.apply(console, arguments); };
3959
3958
  var _ce = function() { console.error.apply(console, arguments); };
3960
3959
 
3961
3960
  /**
@@ -4092,61 +4091,105 @@
4092
4091
  };
4093
4092
 
4094
4093
  /**
4095
- * Look up a DOM element by ID string, using the node cache for O(1) access.
4096
- *
4097
- * Resolution order:
4098
- * 1. Check `bw._nodeMap[id]` if found and still attached (parentNode !== null), return it
4099
- * 2. If cached ref is detached (parentNode === null), remove stale entry
4100
- * 3. Fall back to `document.getElementById(id)` then `document.querySelector(...)`
4101
- * 4. Try class-based lookup for bw_uuid_* tokens (UUID addressing)
4102
- * 5. Cache the result for next time
4103
- *
4104
- * Accepts a DOM element directly (pass-through) or a string identifier.
4105
- * String identifiers are tried as: direct map key, getElementById,
4106
- * querySelector (for CSS selectors starting with . or #), and
4107
- * bw_uuid_* class selector.
4108
- *
4109
- * @param {string|Element} id - Element ID, CSS selector, bw_uuid_* class, or DOM element
4094
+ * Look up a single DOM element by ID, CSS selector, UUID, or element ref.
4095
+ * Optionally apply content or a function to the resolved element.
4096
+ *
4097
+ * Resolution order for string targets:
4098
+ * 1. Check `bw._nodeMap[id]` cache (O(1), stale entries auto-pruned)
4099
+ * 2. `document.getElementById(id)`
4100
+ * 3. `document.querySelector(id)` for selectors starting with # or .
4101
+ * 4. Class-based lookup for `bw_uuid_*` tokens
4102
+ *
4103
+ * With one argument, returns the element (or null). With two arguments,
4104
+ * applies the second argument to the element and returns the element:
4105
+ * - string/number: sets `el.textContent`
4106
+ * - function: calls `apply(el)`, returns el
4107
+ * - TACO object: clears children, mounts TACO via `bw.createDOM()`
4108
+ * - array: clears children, appends each item (string -> text node, TACO -> element)
4109
+ *
4110
+ * @param {string|Element} target - Element ref, ID, CSS selector, or bw_uuid_* class
4111
+ * @param {string|number|Function|Object|Array} [apply] - Content or function to apply
4110
4112
  * @returns {Element|null} The DOM element, or null if not found
4111
- * @category Internal
4113
+ * @category DOM Selection
4114
+ * @see bw.$
4115
+ * @see bw.patch
4116
+ * @example
4117
+ * bw.el('#title') // lookup
4118
+ * bw.el('#title', 'Hello') // set text content
4119
+ * bw.el('#app', { t: 'h1', c: 'Hi' }) // mount TACO
4120
+ * bw.el('.card', function(el) { // apply function
4121
+ * el.style.opacity = '0.5';
4122
+ * })
4112
4123
  */
4113
- bw._el = function(id) {
4114
- // Pass-through for DOM elements
4115
- if (!_is(id, 'string')) return id || null;
4116
- if (!id) return null;
4117
- if (!bw._isBrowser) return null;
4118
-
4119
- // 1. Check cache
4120
- var cached = bw._nodeMap[id];
4121
- if (cached) {
4122
- // Verify not detached (parentNode check is IE11-safe)
4123
- if (cached.parentNode !== null) {
4124
- return cached;
4124
+ bw.el = function(target, apply) {
4125
+ // Resolve target to element
4126
+ var el;
4127
+ if (!_is(target, 'string')) {
4128
+ el = target || null;
4129
+ } else if (!target || !bw._isBrowser) {
4130
+ el = null;
4131
+ } else {
4132
+ // 1. Check cache
4133
+ var cached = bw._nodeMap[target];
4134
+ if (cached) {
4135
+ if (cached.parentNode !== null) {
4136
+ el = cached;
4137
+ } else {
4138
+ delete bw._nodeMap[target];
4139
+ }
4140
+ }
4141
+ if (!el) {
4142
+ // 2. getElementById
4143
+ el = document.getElementById(target);
4144
+ // 3. querySelector for CSS selectors
4145
+ if (!el && (target.charAt(0) === '#' || target.charAt(0) === '.')) {
4146
+ el = document.querySelector(target);
4147
+ }
4148
+ // 4. bw_uuid_* class lookup
4149
+ if (!el && target.indexOf('bw_uuid_') === 0) {
4150
+ el = document.querySelector('.' + target);
4151
+ }
4152
+ // 5. Cache result
4153
+ if (el) bw._nodeMap[target] = el;
4125
4154
  }
4126
- // Stale — remove and fall through
4127
- delete bw._nodeMap[id];
4128
4155
  }
4129
4156
 
4130
- // 2. DOM fallback: try getElementById first (fastest native lookup)
4131
- var el = document.getElementById(id);
4132
-
4133
- // 3. Try querySelector for CSS selectors (starts with # or .)
4134
- if (!el && (id.charAt(0) === '#' || id.charAt(0) === '.')) {
4135
- el = document.querySelector(id);
4136
- }
4157
+ // Apply (if provided and element found)
4158
+ if (el && apply !== undefined) _applyTo(el, apply);
4137
4159
 
4138
- // 4. Try class-based lookup for bw_uuid_* tokens (UUID addressing)
4139
- if (!el && id.indexOf('bw_uuid_') === 0) {
4140
- el = document.querySelector('.' + id);
4141
- }
4160
+ return el;
4161
+ };
4142
4162
 
4143
- // 5. Cache the result for next time
4144
- if (el) {
4145
- bw._nodeMap[id] = el;
4163
+ /**
4164
+ * Internal: apply content or function to a DOM element.
4165
+ * Shared by bw.el() and bw.$().
4166
+ * @private
4167
+ */
4168
+ function _applyTo(el, apply) {
4169
+ if (_is(apply, 'function')) {
4170
+ apply(el);
4171
+ } else if (_isA(apply)) {
4172
+ el.innerHTML = '';
4173
+ apply.forEach(function(item) {
4174
+ if (item != null) {
4175
+ if (_is(item, 'object') && item.t) {
4176
+ el.appendChild(bw.createDOM(item));
4177
+ } else {
4178
+ el.appendChild(document.createTextNode(String(item)));
4179
+ }
4180
+ }
4181
+ });
4182
+ } else if (_is(apply, 'object') && apply !== null && apply.t) {
4183
+ el.innerHTML = '';
4184
+ el.appendChild(bw.createDOM(apply));
4185
+ } else {
4186
+ el.textContent = String(apply);
4146
4187
  }
4188
+ }
4147
4189
 
4148
- return el;
4149
- };
4190
+ // Internal alias — kept for one release cycle (v2.0.26).
4191
+ // Will be removed in v2.0.27. Use bw.el() instead.
4192
+ bw._el = bw.el;
4150
4193
 
4151
4194
  /**
4152
4195
  * Register a DOM element in the node cache under one or more keys.
@@ -4210,6 +4253,12 @@
4210
4253
  */
4211
4254
  var _UUID_RE = /\bbw_uuid_[a-z0-9_]+\b/;
4212
4255
 
4256
+ /**
4257
+ * SVG namespace URI for createElementNS.
4258
+ * @private
4259
+ */
4260
+ var _SVG_NS = 'http://www.w3.org/2000/svg';
4261
+
4213
4262
  /**
4214
4263
  * Assign a UUID to a TACO object by appending a `bw_uuid_*` token to `taco.a.class`.
4215
4264
  *
@@ -4264,9 +4313,10 @@
4264
4313
  if (!tacoOrElement) return null;
4265
4314
 
4266
4315
  var classStr;
4267
- // DOM element: check className
4316
+ // DOM element: check className (SVG elements use getAttribute for string value)
4268
4317
  if (tacoOrElement.className !== undefined && tacoOrElement.tagName) {
4269
- classStr = tacoOrElement.className;
4318
+ classStr = typeof tacoOrElement.className === 'string'
4319
+ ? tacoOrElement.className : (tacoOrElement.getAttribute('class') || '');
4270
4320
  }
4271
4321
  // TACO object: check a.class
4272
4322
  else if (tacoOrElement.a && _is(tacoOrElement.a.class, 'string')) {
@@ -4535,7 +4585,7 @@
4535
4585
  var fnCounterBefore = bw._fnIDCounter;
4536
4586
 
4537
4587
  // Render body content
4538
- var bodyHTML = '';
4588
+ var bodyHTML;
4539
4589
  if (_is(body, 'string')) {
4540
4590
  bodyHTML = body;
4541
4591
  } else {
@@ -4706,9 +4756,11 @@
4706
4756
  }
4707
4757
 
4708
4758
  const { t: tag, a: attrs = {}, c: content, o: opts = {} } = taco;
4709
-
4710
- // Create element
4711
- const el = document.createElement(tag);
4759
+
4760
+ // SVG namespace: detect SVG context and thread through children.
4761
+ // {t:'svg'} starts SVG context; foreignObject children revert to HTML.
4762
+ var svgCtx = options._svgCtx || (tag === 'svg');
4763
+ var el = svgCtx ? document.createElementNS(_SVG_NS, tag) : document.createElement(tag);
4712
4764
 
4713
4765
  // Set attributes
4714
4766
  for (const [key, value] of Object.entries(attrs)) {
@@ -4719,9 +4771,11 @@
4719
4771
  Object.assign(el.style, value);
4720
4772
  } else if (key === 'class') {
4721
4773
  // Handle class as array or string
4774
+ // SVG elements use SVGAnimatedString for className, so use setAttribute
4722
4775
  const classStr = _isA(value) ? value.filter(Boolean).join(' ') : String(value);
4723
4776
  if (classStr) {
4724
- el.className = classStr;
4777
+ if (svgCtx) el.setAttribute('class', classStr);
4778
+ else el.className = classStr;
4725
4779
  }
4726
4780
  } else if (key.startsWith('on') && _is(value, 'function')) {
4727
4781
  // Event handlers
@@ -4742,11 +4796,17 @@
4742
4796
  // Add children, building _bw_refs for fast parent→child access.
4743
4797
  // Children with id attributes or bw_uuid_* classes get local refs on the parent,
4744
4798
  // so o.render functions can access them without any DOM lookup.
4799
+ // SVG: foreignObject children revert to HTML namespace; otherwise inherit.
4800
+ var childOpts = options;
4801
+ var childSvgCtx = svgCtx && tag !== 'foreignObject';
4802
+ if (childSvgCtx !== (options._svgCtx || false)) {
4803
+ childOpts = Object.assign({}, options, {_svgCtx: childSvgCtx || undefined});
4804
+ }
4745
4805
  if (content != null) {
4746
4806
  if (_isA(content)) {
4747
4807
  content.forEach(child => {
4748
4808
  if (child != null) {
4749
- var childEl = bw.createDOM(child, options);
4809
+ var childEl = bw.createDOM(child, childOpts);
4750
4810
  el.appendChild(childEl);
4751
4811
  // Build local refs for addressable children
4752
4812
  var childRefId = (child && child.a) ? (child.a.id || bw.getUUID(child)) : null;
@@ -4769,7 +4829,7 @@
4769
4829
  // Raw HTML content — inject via innerHTML
4770
4830
  el.innerHTML = content.v;
4771
4831
  } else if (_is(content, 'object') && content.t) {
4772
- var childEl = bw.createDOM(content, options);
4832
+ var childEl = bw.createDOM(content, childOpts);
4773
4833
  el.appendChild(childEl);
4774
4834
  var childRefId = content.a ? (content.a.id || bw.getUUID(content)) : null;
4775
4835
  if (childRefId) {
@@ -4795,13 +4855,21 @@
4795
4855
  }
4796
4856
 
4797
4857
  // Register UUID class in node cache (bw_uuid_* tokens in class string)
4798
- if (el.className) {
4799
- var uuidMatch = el.className.match(_UUID_RE);
4858
+ // SVG elements have SVGAnimatedString for className; use getAttribute instead
4859
+ var clsStr = svgCtx ? (el.getAttribute('class') || '') : el.className;
4860
+ if (clsStr) {
4861
+ var uuidMatch = clsStr.match(_UUID_RE);
4800
4862
  if (uuidMatch) {
4801
4863
  bw._nodeMap[uuidMatch[0]] = el;
4802
4864
  }
4803
4865
  }
4804
4866
 
4867
+ // Store component type metadata (e.g., 'card', 'tabs') for introspection.
4868
+ // BCCL factories set o.type; custom components can too.
4869
+ if (opts.type) {
4870
+ el._bw_type = opts.type;
4871
+ }
4872
+
4805
4873
  // Handle lifecycle hooks and state
4806
4874
  if (opts.mounted || opts.unmount || opts.render || opts.state) {
4807
4875
  // Ensure element has a UUID class for identity
@@ -4831,11 +4899,13 @@
4831
4899
 
4832
4900
  if (mountFn) {
4833
4901
  if (document.body.contains(el)) {
4834
- mountFn(el, el._bw_state || {});
4902
+ try { mountFn(el, el._bw_state || {}); }
4903
+ catch (e) { _cw('o.mounted error: ' + e.message); }
4835
4904
  } else {
4836
4905
  requestAnimationFrame(() => {
4837
4906
  if (document.body.contains(el)) {
4838
- mountFn(el, el._bw_state || {});
4907
+ try { mountFn(el, el._bw_state || {}); }
4908
+ catch (e) { _cw('o.mounted error: ' + e.message); }
4839
4909
  }
4840
4910
  });
4841
4911
  }
@@ -4844,7 +4914,8 @@
4844
4914
  // Store unmount callback keyed by UUID class
4845
4915
  if (opts.unmount) {
4846
4916
  bw._unmountCallbacks.set(uuid, () => {
4847
- opts.unmount(el, el._bw_state || {});
4917
+ try { opts.unmount(el, el._bw_state || {}); }
4918
+ catch (e) { _cw('o.unmount error: ' + e.message); }
4848
4919
  });
4849
4920
  }
4850
4921
  }
@@ -4863,24 +4934,25 @@
4863
4934
  }
4864
4935
 
4865
4936
  // Slot declarations: auto-generate setX/getX pairs
4937
+ // The target element is cached at creation time to avoid repeated
4938
+ // querySelector calls on every get/set invocation.
4866
4939
  if (opts.slots) {
4867
4940
  for (var sk in opts.slots) {
4868
4941
  if (_hop.call(opts.slots, sk)) {
4869
4942
  (function(name, selector) {
4943
+ var target = el.querySelector(selector);
4870
4944
  var cap = name.charAt(0).toUpperCase() + name.slice(1);
4871
4945
  el.bw['set' + cap] = function(value) {
4872
- var t = el.querySelector(selector);
4873
- if (!t) return;
4946
+ if (!target) return;
4874
4947
  if (value != null && typeof value === 'object' && value.t) {
4875
- t.innerHTML = '';
4876
- t.appendChild(bw.createDOM(value));
4948
+ target.innerHTML = '';
4949
+ target.appendChild(bw.createDOM(value));
4877
4950
  } else {
4878
- t.textContent = (value != null) ? String(value) : '';
4951
+ target.textContent = (value != null) ? String(value) : '';
4879
4952
  }
4880
4953
  };
4881
4954
  el.bw['get' + cap] = function() {
4882
- var t = el.querySelector(selector);
4883
- return t ? t.textContent : '';
4955
+ return target ? target.textContent : '';
4884
4956
  };
4885
4957
  })(sk, opts.slots[sk]);
4886
4958
  }
@@ -4921,7 +4993,7 @@
4921
4993
  }
4922
4994
 
4923
4995
  // Get target element (use cache-backed lookup)
4924
- const targetEl = bw._el(target);
4996
+ const targetEl = bw.el(target);
4925
4997
 
4926
4998
  if (!targetEl) {
4927
4999
  _ce('bw.DOM: Target element not found:', target);
@@ -5024,7 +5096,8 @@
5024
5096
  // Deregister UUID classes from node cache for non-lifecycle UUID elements
5025
5097
  var uuidEls = element.querySelectorAll('[class*="bw_uuid_"]');
5026
5098
  uuidEls.forEach(function(uel) {
5027
- var m = uel.className && uel.className.match(_UUID_RE);
5099
+ var uc = typeof uel.className === 'string' ? uel.className : (uel.getAttribute('class') || '');
5100
+ var m = uc && uc.match(_UUID_RE);
5028
5101
  if (m) delete bw._nodeMap[m[0]];
5029
5102
  });
5030
5103
 
@@ -5110,9 +5183,10 @@
5110
5183
  * bw.update(el); // re-renders, emits bw:statechange
5111
5184
  */
5112
5185
  bw.update = function(target) {
5113
- var el = bw._el(target);
5186
+ var el = bw.el(target);
5114
5187
  if (el && el._bw_render) {
5115
- el._bw_render(el, el._bw_state || {});
5188
+ try { el._bw_render(el, el._bw_state || {}); }
5189
+ catch (e) { _cw('o.render error: ' + e.message); }
5116
5190
  bw.emit(el, 'statechange', el._bw_state);
5117
5191
  }
5118
5192
  return el || null;
@@ -5139,7 +5213,7 @@
5139
5213
  * bw.patch('info', { t: 'em', c: 'new' }); // replace children with TACO
5140
5214
  */
5141
5215
  bw.patch = function(id, content, attr) {
5142
- var el = bw._el(id);
5216
+ var el = bw.el(id);
5143
5217
  if (!el) return null;
5144
5218
 
5145
5219
  if (attr) {
@@ -5211,7 +5285,7 @@
5211
5285
  * // Dispatches CustomEvent 'bw:statechange' on the element
5212
5286
  */
5213
5287
  bw.emit = function(target, eventName, detail) {
5214
- var el = bw._el(target);
5288
+ var el = bw.el(target);
5215
5289
  if (el) {
5216
5290
  el.dispatchEvent(new CustomEvent('bw:' + eventName, {
5217
5291
  bubbles: true,
@@ -5240,7 +5314,7 @@
5240
5314
  * });
5241
5315
  */
5242
5316
  bw.on = function(target, eventName, handler) {
5243
- var el = bw._el(target);
5317
+ var el = bw.el(target);
5244
5318
  if (el) {
5245
5319
  el.addEventListener('bw:' + eventName, function(e) {
5246
5320
  handler(e.detail, e);
@@ -5267,23 +5341,38 @@
5267
5341
  *
5268
5342
  * @param {string} topic - Topic name (plain string, no prefix)
5269
5343
  * @param {*} [detail] - Data to pass to subscribers
5270
- * @returns {number} Count of successfully called subscribers
5344
+ * @returns {number} Count of successfully called subscribers (including wildcard matches)
5271
5345
  * @category Pub/Sub
5272
5346
  * @see bw.sub
5273
5347
  * @example
5274
5348
  * bw.pub('score:updated', { player: 'X', score: 10 });
5349
+ * // Wildcard subscribers matching 'score:*' will also fire
5275
5350
  */
5276
5351
  bw.pub = function(topic, detail) {
5277
- var subs = bw._topics[topic];
5278
- if (!subs || subs.length === 0) return 0;
5279
- var snapshot = subs.slice(); // safe against unsub during iteration
5280
5352
  var called = 0;
5281
- for (var i = 0; i < snapshot.length; i++) {
5282
- try {
5283
- snapshot[i].handler(detail);
5284
- called++;
5285
- } catch (err) {
5286
- _cw('bw.pub: subscriber error on topic "' + topic + '":', err);
5353
+ // Exact-match subscribers
5354
+ var subs = bw._topics[topic];
5355
+ if (subs && subs.length > 0) {
5356
+ var snapshot = subs.slice();
5357
+ for (var i = 0; i < snapshot.length; i++) {
5358
+ try { snapshot[i].handler(detail, topic); called++; }
5359
+ catch (err) { _cw('bw.pub: subscriber error on topic "' + topic + '":', err); }
5360
+ }
5361
+ }
5362
+ // Wildcard subscribers -- patterns ending with '*'
5363
+ var keys = Object.keys(bw._topics);
5364
+ for (var k = 0; k < keys.length; k++) {
5365
+ var pat = keys[k];
5366
+ if (pat.charAt(pat.length - 1) !== '*') continue;
5367
+ var prefix = pat.slice(0, -1); // strip trailing '*'
5368
+ if (topic.length >= prefix.length && topic.substring(0, prefix.length) === prefix && topic !== pat) {
5369
+ var wsubs = bw._topics[pat];
5370
+ if (!wsubs) continue;
5371
+ var wsnap = wsubs.slice();
5372
+ for (var w = 0; w < wsnap.length; w++) {
5373
+ try { wsnap[w].handler(detail, topic); called++; }
5374
+ catch (err) { _cw('bw.pub: wildcard subscriber error on "' + pat + '" for topic "' + topic + '":', err); }
5375
+ }
5287
5376
  }
5288
5377
  }
5289
5378
  return called;
@@ -5292,12 +5381,17 @@
5292
5381
  /**
5293
5382
  * Subscribe to a topic. Returns an unsub() function.
5294
5383
  *
5295
- * Optional third argument ties the subscription to a DOM element's lifecycle —
5384
+ * Supports wildcard patterns: a topic ending in `*` matches any published
5385
+ * topic that starts with the prefix before the `*`. For example,
5386
+ * `'agui:*'` matches `'agui:ready'`, `'agui:error'`, etc. The handler
5387
+ * receives `(detail, topic)` so it can distinguish which topic fired.
5388
+ *
5389
+ * Optional third argument ties the subscription to a DOM element's lifecycle --
5296
5390
  * when `bw.cleanup()` is called on that element, the subscription is automatically
5297
5391
  * removed, preventing memory leaks.
5298
5392
  *
5299
- * @param {string} topic - Topic name
5300
- * @param {Function} handler - Called with (detail) on each publish
5393
+ * @param {string} topic - Topic name, or wildcard pattern ending in '*'
5394
+ * @param {Function} handler - Called with (detail, topic) on each publish
5301
5395
  * @param {Element} [el] - Optional DOM element to tie lifecycle to
5302
5396
  * @returns {Function} Call to unsubscribe
5303
5397
  * @category Pub/Sub
@@ -5308,6 +5402,11 @@
5308
5402
  * console.log(detail.player, 'scored', detail.score);
5309
5403
  * });
5310
5404
  * // Later: unsub() to stop listening
5405
+ *
5406
+ * // Wildcard: listen to all 'agui:' topics
5407
+ * bw.sub('agui:*', function(detail, topic) {
5408
+ * console.log('Got', topic, detail);
5409
+ * });
5311
5410
  */
5312
5411
  bw.sub = function(topic, handler, el) {
5313
5412
  var id = ++bw._subIdCounter;
@@ -5359,6 +5458,37 @@
5359
5458
  return removed;
5360
5459
  };
5361
5460
 
5461
+ /**
5462
+ * Subscribe to a topic for a single event only. The subscription is
5463
+ * automatically removed after the first publish. Equivalent to manually
5464
+ * calling unsub() inside a bw.sub() handler, but avoids the common bug
5465
+ * of forgetting to unsubscribe.
5466
+ *
5467
+ * @param {string} topic - Topic name
5468
+ * @param {Function} handler - Called once with (detail) on the next publish
5469
+ * @param {Element} [el] - Optional DOM element to tie lifecycle to
5470
+ * @returns {Function} Call to cancel the subscription before it fires
5471
+ * @category Pub/Sub
5472
+ * @see bw.sub
5473
+ * @see bw.pub
5474
+ * @example
5475
+ * bw.once('data:loaded', function(detail) {
5476
+ * console.log('Received:', detail);
5477
+ * // No need to unsubscribe -- already done automatically
5478
+ * });
5479
+ *
5480
+ * // Cancel before it fires:
5481
+ * var cancel = bw.once('timeout', handler);
5482
+ * cancel(); // handler will never be called
5483
+ */
5484
+ bw.once = function(topic, handler, el) {
5485
+ var unsub = bw.sub(topic, function(detail) {
5486
+ unsub();
5487
+ handler(detail);
5488
+ }, el);
5489
+ return unsub;
5490
+ };
5491
+
5362
5492
  // ===================================================================================
5363
5493
  // Function Registry (revived from v1 for string dispatch contexts)
5364
5494
  // ===================================================================================
@@ -5599,7 +5729,7 @@
5599
5729
  * };
5600
5730
  */
5601
5731
  bw.message = function(target, action, data) {
5602
- var el = bw._el(target);
5732
+ var el = bw.el(target);
5603
5733
  if (!el) el = bw.$('.' + target)[0];
5604
5734
  if (!el || !el.bw || typeof el.bw[action] !== 'function') {
5605
5735
  _cw('bw.message: no handle method "' + action + '" on ' + target);
@@ -5609,6 +5739,207 @@
5609
5739
  return true;
5610
5740
  };
5611
5741
 
5742
+ /**
5743
+ * Collect form data from all input, select, and textarea elements within a
5744
+ * container. Each element's `name` attribute (or `id` if no name) becomes a
5745
+ * key in the returned object. This provides a lightweight alternative to the
5746
+ * browser FormData API that returns a plain object suitable for JSON
5747
+ * serialization or bw.pub().
5748
+ *
5749
+ * Handles all standard HTML form controls:
5750
+ * - text/number/email/etc inputs: string value
5751
+ * - checkboxes: boolean (true/false)
5752
+ * - radio buttons: string value of the checked radio (unchecked groups omitted)
5753
+ * - multi-select: array of selected option values
5754
+ * - textarea: string value
5755
+ *
5756
+ * Elements without both `name` and `id` attributes are silently skipped.
5757
+ *
5758
+ * @param {string|Element} target - CSS selector, UUID string, or DOM element
5759
+ * @returns {Object} Plain object mapping field names to values
5760
+ * @category Component
5761
+ * @see bw.makeForm
5762
+ * @see bw.makeInput
5763
+ * @example
5764
+ * // Given a form with name="email" input and name="agree" checkbox:
5765
+ * var data = bw.formData('#signup-form');
5766
+ * // => { email: 'user@example.com', agree: true }
5767
+ *
5768
+ * // Collect and publish in one step:
5769
+ * bw.pub('form:submit', bw.formData('#my-form'));
5770
+ *
5771
+ * // Works with any container, not just <form>:
5772
+ * bw.pub('settings:changed', bw.formData('.settings-panel'));
5773
+ */
5774
+ bw.formData = function(target) {
5775
+ var el = bw.el(target);
5776
+ if (!el) return {};
5777
+ var result = {};
5778
+ var inputs = el.querySelectorAll('input, select, textarea');
5779
+ for (var i = 0; i < inputs.length; i++) {
5780
+ var inp = inputs[i];
5781
+ var key = inp.name || inp.id;
5782
+ if (!key) continue;
5783
+ if (inp.type === 'checkbox') {
5784
+ result[key] = inp.checked;
5785
+ } else if (inp.type === 'radio') {
5786
+ if (inp.checked) result[key] = inp.value;
5787
+ } else if (inp.tagName === 'SELECT' && inp.multiple) {
5788
+ result[key] = [];
5789
+ for (var j = 0; j < inp.options.length; j++) {
5790
+ if (inp.options[j].selected) result[key].push(inp.options[j].value);
5791
+ }
5792
+ } else {
5793
+ result[key] = inp.value;
5794
+ }
5795
+ }
5796
+ return result;
5797
+ };
5798
+
5799
+ // ===================================================================================
5800
+ // bw.jsonPatch() — RFC 6902 JSON Patch on plain objects
5801
+ // ===================================================================================
5802
+
5803
+ /**
5804
+ * Apply RFC 6902 JSON Patch operations to a plain object.
5805
+ *
5806
+ * Supported operations: add, remove, replace, move, copy, test.
5807
+ * Paths use JSON Pointer (RFC 6901) notation: `/foo/bar/0`.
5808
+ * Mutates the target object in place and returns it.
5809
+ *
5810
+ * @param {Object} obj - Target object to patch
5811
+ * @param {Array<Object>} ops - Array of patch operations
5812
+ * @param {string} ops[].op - Operation: 'add', 'remove', 'replace', 'move', 'copy', 'test'
5813
+ * @param {string} ops[].path - JSON Pointer path (e.g. '/a/b/0')
5814
+ * @param {*} [ops[].value] - Value for add/replace/test
5815
+ * @param {string} [ops[].from] - Source path for move/copy
5816
+ * @returns {Object} The patched object (same reference)
5817
+ * @throws {Error} On invalid op, missing path, test failure, or path not found for remove
5818
+ * @category Data Utilities
5819
+ * @see bw.patch
5820
+ * @example
5821
+ * var obj = { a: 1, b: { c: 2 } };
5822
+ * bw.jsonPatch(obj, [
5823
+ * { op: 'replace', path: '/a', value: 10 },
5824
+ * { op: 'add', path: '/b/d', value: 3 },
5825
+ * { op: 'remove', path: '/b/c' }
5826
+ * ]);
5827
+ * // obj => { a: 10, b: { d: 3 } }
5828
+ */
5829
+ bw.jsonPatch = function(obj, ops) {
5830
+ if (!_isA(ops)) return obj;
5831
+
5832
+ // Parse JSON Pointer path to array of keys
5833
+ function parsePath(path) {
5834
+ if (path === '') return [];
5835
+ if (path.charAt(0) !== '/') throw new Error('Invalid JSON Pointer: ' + path);
5836
+ return path.slice(1).split('/').map(function(s) {
5837
+ return s.replace(/~1/g, '/').replace(/~0/g, '~');
5838
+ });
5839
+ }
5840
+
5841
+ // Walk to parent of final key; return { parent, key }
5842
+ function resolve(root, keys) {
5843
+ var parent = root;
5844
+ for (var i = 0; i < keys.length - 1; i++) {
5845
+ var k = _isA(parent) ? parseInt(keys[i], 10) : keys[i];
5846
+ if (parent[k] === undefined) throw new Error('Path not found: /' + keys.slice(0, i + 1).join('/'));
5847
+ parent = parent[k];
5848
+ }
5849
+ return { parent: parent, key: _isA(parent) ? parseInt(keys[keys.length - 1], 10) : keys[keys.length - 1] };
5850
+ }
5851
+
5852
+ // Get value at path
5853
+ function getVal(root, keys) {
5854
+ var cur = root;
5855
+ for (var i = 0; i < keys.length; i++) {
5856
+ var k = _isA(cur) ? parseInt(keys[i], 10) : keys[i];
5857
+ if (cur[k] === undefined) throw new Error('Path not found: /' + keys.slice(0, i + 1).join('/'));
5858
+ cur = cur[k];
5859
+ }
5860
+ return cur;
5861
+ }
5862
+
5863
+ for (var i = 0; i < ops.length; i++) {
5864
+ var op = ops[i];
5865
+ if (!op.op || !_is(op.path, 'string')) throw new Error('Invalid patch operation at index ' + i);
5866
+ var keys = parsePath(op.path);
5867
+
5868
+ var r, val, fromKeys, fr, tr, cr;
5869
+ switch (op.op) {
5870
+ case 'add': {
5871
+ if (keys.length === 0) throw new Error('Cannot add to root');
5872
+ r = resolve(obj, keys);
5873
+ if (_isA(r.parent) && r.key <= r.parent.length) {
5874
+ r.parent.splice(r.key, 0, op.value);
5875
+ } else {
5876
+ r.parent[r.key] = op.value;
5877
+ }
5878
+ break;
5879
+ }
5880
+ case 'remove': {
5881
+ if (keys.length === 0) throw new Error('Cannot remove root');
5882
+ r = resolve(obj, keys);
5883
+ if (_isA(r.parent)) {
5884
+ if (r.key >= r.parent.length) throw new Error('Index out of bounds: ' + r.key);
5885
+ r.parent.splice(r.key, 1);
5886
+ } else {
5887
+ if (!(r.key in r.parent)) throw new Error('Path not found: ' + op.path);
5888
+ delete r.parent[r.key];
5889
+ }
5890
+ break;
5891
+ }
5892
+ case 'replace': {
5893
+ if (keys.length === 0) throw new Error('Cannot replace root');
5894
+ r = resolve(obj, keys);
5895
+ if (_isA(r.parent)) {
5896
+ if (r.key >= r.parent.length) throw new Error('Index out of bounds: ' + r.key);
5897
+ } else {
5898
+ if (!(r.key in r.parent)) throw new Error('Path not found: ' + op.path);
5899
+ }
5900
+ r.parent[r.key] = op.value;
5901
+ break;
5902
+ }
5903
+ case 'move': {
5904
+ if (!_is(op.from, 'string')) throw new Error('move requires "from"');
5905
+ fromKeys = parsePath(op.from);
5906
+ val = getVal(obj, fromKeys);
5907
+ fr = resolve(obj, fromKeys);
5908
+ if (_isA(fr.parent)) { fr.parent.splice(fr.key, 1); }
5909
+ else { delete fr.parent[fr.key]; }
5910
+ tr = resolve(obj, keys);
5911
+ if (_isA(tr.parent) && tr.key <= tr.parent.length) {
5912
+ tr.parent.splice(tr.key, 0, val);
5913
+ } else {
5914
+ tr.parent[tr.key] = val;
5915
+ }
5916
+ break;
5917
+ }
5918
+ case 'copy': {
5919
+ if (!_is(op.from, 'string')) throw new Error('copy requires "from"');
5920
+ val = getVal(obj, parsePath(op.from));
5921
+ cr = resolve(obj, keys);
5922
+ if (_isA(cr.parent) && cr.key <= cr.parent.length) {
5923
+ cr.parent.splice(cr.key, 0, val);
5924
+ } else {
5925
+ cr.parent[cr.key] = val;
5926
+ }
5927
+ break;
5928
+ }
5929
+ case 'test': {
5930
+ var actual = getVal(obj, keys);
5931
+ if (JSON.stringify(actual) !== JSON.stringify(op.value)) {
5932
+ throw new Error('Test failed: ' + op.path + ' expected ' + JSON.stringify(op.value) + ' got ' + JSON.stringify(actual));
5933
+ }
5934
+ break;
5935
+ }
5936
+ default:
5937
+ throw new Error('Unknown op: ' + op.op);
5938
+ }
5939
+ }
5940
+ return obj;
5941
+ };
5942
+
5612
5943
  // ===================================================================================
5613
5944
  // bw.apply() / bw.parseJSONFlex() — Server-driven UI protocol
5614
5945
  // ===================================================================================
@@ -5750,7 +6081,7 @@
5750
6081
  var target = msg.target;
5751
6082
 
5752
6083
  if (type === 'replace') {
5753
- var el = bw._el(target);
6084
+ var el = bw.el(target);
5754
6085
  if (!el) return false;
5755
6086
  bw.DOM(el, msg.node);
5756
6087
  return true;
@@ -5760,14 +6091,14 @@
5760
6091
  return patched !== null;
5761
6092
 
5762
6093
  } else if (type === 'append') {
5763
- var parent = bw._el(target);
6094
+ var parent = bw.el(target);
5764
6095
  if (!parent) return false;
5765
6096
  var child = bw.createDOM(msg.node);
5766
6097
  parent.appendChild(child);
5767
6098
  return true;
5768
6099
 
5769
6100
  } else if (type === 'remove') {
5770
- var toRemove = bw._el(target);
6101
+ var toRemove = bw.el(target);
5771
6102
  if (!toRemove) return false;
5772
6103
  if (_is(bw.cleanup, 'function')) bw.cleanup(toRemove);
5773
6104
  toRemove.remove();
@@ -5827,30 +6158,98 @@
5827
6158
 
5828
6159
 
5829
6160
  // ===================================================================================
5830
- // bw.inspect() — Debug utility
6161
+ // bw.inspect() — DOM introspection with bitwrench metadata
5831
6162
  // ===================================================================================
5832
6163
 
5833
6164
  /**
5834
- * Inspect a DOM element's bitwrench state, handle methods, and metadata.
5835
- * Works with DOM elements or CSS selectors.
5836
- *
5837
- * @param {string|Element} target - Selector or DOM element
5838
- * @returns {Element|null} The element, or null if not found
6165
+ * Inspect a DOM element and its subtree, returning a plain-object
6166
+ * representation with bitwrench metadata at each node. Useful for debugging,
6167
+ * devtools, MCP/AG-UI tool discovery, and automated testing.
6168
+ *
6169
+ * Each node in the returned tree includes:
6170
+ * - `tag` -- lowercase tag name (or '#text' for text nodes)
6171
+ * - `id` -- element id (if set)
6172
+ * - `uuid` -- bitwrench UUID class (if lifecycle-managed)
6173
+ * - `type` -- component type from o.type (if set, e.g. 'card', 'tabs')
6174
+ * - `classes` -- first 5 CSS classes (string, space-separated)
6175
+ * - `handles` -- array of el.bw method names (if any)
6176
+ * - `state` -- copy of _bw_state (if any)
6177
+ * - `hasRender` -- true if _bw_render is set
6178
+ * - `hasSubs` -- true if element has pub/sub subscriptions
6179
+ * - `refs` -- copy of _bw_refs keys (if any)
6180
+ * - `children` -- array of child node trees (up to depth limit, max 50 per level)
6181
+ *
6182
+ * @param {string|Element} target - CSS selector, UUID, or DOM element
6183
+ * @param {number} [depth=3] - Maximum recursion depth (0 = target only, no children)
6184
+ * @returns {Object|null} Plain object tree, or null if element not found
5839
6185
  * @category Component
5840
6186
  * @example
5841
- * bw.inspect('#my-carousel');
5842
- * bw.inspect($0);
6187
+ * // Get full tree from #app, 3 levels deep (default):
6188
+ * var info = bw.inspect('#app');
6189
+ *
6190
+ * // Shallow inspection (just the element, no children):
6191
+ * var info = bw.inspect('#my-carousel', 0);
6192
+ * console.log(info.handles); // ['next', 'prev', 'goToSlide']
6193
+ * console.log(info.type); // 'carousel'
6194
+ *
6195
+ * // Deep inspection for debugging:
6196
+ * console.log(JSON.stringify(bw.inspect('#app', 5), null, 2));
5843
6197
  */
5844
- bw.inspect = function(target) {
5845
- var el = _is(target, 'string') ? bw.$(target)[0] : target;
5846
- if (!el) { _cw('bw.inspect: element not found'); return null; }
5847
- console.group('Element: ' + (bw.getUUID(el) || el.id || el.tagName));
5848
- _cl('State:', el._bw_state || '(none)');
5849
- _cl('Handle:', el.bw ? _keys(el.bw) : '(none)');
5850
- _cl('Classes:', el.className);
5851
- _cl('Refs:', el._bw_refs || '(none)');
5852
- console.groupEnd();
5853
- return el;
6198
+ bw.inspect = function(target, depth) {
6199
+ var el = bw.el(target);
6200
+ if (!el && _is(target, 'string')) el = bw.$(target)[0];
6201
+ if (!el) return null;
6202
+ if (depth === undefined || depth === null) depth = 3;
6203
+
6204
+ function walk(node, d) {
6205
+ if (!node) return null;
6206
+ // Skip non-element nodes (text, comment, etc.)
6207
+ if (node.nodeType !== 1) return null;
6208
+
6209
+ var info = { tag: node.tagName ? node.tagName.toLowerCase() : '#text' };
6210
+
6211
+ // Identity
6212
+ if (node.id) info.id = node.id;
6213
+ var uuid = bw.getUUID(node);
6214
+ if (uuid) info.uuid = uuid;
6215
+ if (node._bw_type) info.type = node._bw_type;
6216
+
6217
+ // CSS classes (first 5 for readability)
6218
+ if (node.className && typeof node.className === 'string') {
6219
+ info.classes = node.className.split(' ').slice(0, 5).join(' ');
6220
+ }
6221
+
6222
+ // Bitwrench handle methods
6223
+ if (node.bw) {
6224
+ var handles = _keys(node.bw);
6225
+ if (handles.length > 0) info.handles = handles;
6226
+ }
6227
+
6228
+ // State
6229
+ if (node._bw_state) info.state = node._bw_state;
6230
+ if (node._bw_render) info.hasRender = true;
6231
+ if (node._bw_subs && node._bw_subs.length > 0) info.hasSubs = true;
6232
+
6233
+ // Refs
6234
+ if (node._bw_refs) info.refs = _keys(node._bw_refs);
6235
+
6236
+ // Children (recurse up to depth limit, max 50 children per level)
6237
+ if (d < depth && node.children && node.children.length > 0) {
6238
+ info.children = [];
6239
+ var max = Math.min(node.children.length, 50);
6240
+ for (var i = 0; i < max; i++) {
6241
+ var child = walk(node.children[i], d + 1);
6242
+ if (child) info.children.push(child);
6243
+ }
6244
+ if (node.children.length > 50) {
6245
+ info.children.push({ tag: '...', count: node.children.length - 50 });
6246
+ }
6247
+ }
6248
+
6249
+ return info;
6250
+ }
6251
+
6252
+ return walk(el, 0);
5854
6253
  };
5855
6254
 
5856
6255
  bw.compile = function() { throw new Error('bw.compile() removed in v2.0.19. Use o.handle/o.slots on TACO options instead.'); };
@@ -6072,37 +6471,49 @@
6072
6471
  * so you can use `.map()`, `.filter()`, etc. directly. Accepts CSS selectors,
6073
6472
  * single elements, NodeLists, or arrays.
6074
6473
  *
6474
+ * With an optional second argument, applies content or a function to
6475
+ * every matched element (same apply rules as `bw.el()`):
6476
+ * - string/number: sets `el.textContent`
6477
+ * - function: calls `apply(el)` for each element
6478
+ * - TACO object: clears children, mounts TACO via `bw.createDOM()`
6479
+ * - array: clears children, appends each item
6480
+ *
6075
6481
  * @param {string|Element|Array} selector - CSS selector, element, or array
6482
+ * @param {string|number|Function|Object|Array} [apply] - Content or function to apply
6076
6483
  * @returns {Array} Array of DOM elements
6077
6484
  * @category DOM Selection
6485
+ * @see bw.el
6078
6486
  * @example
6079
- * bw.$('.card') // => [div.card, div.card, ...]
6080
- * bw.$(myElement) // => [myElement]
6081
- * bw.$('.card').map(el => el.textContent)
6487
+ * bw.$('.card') // => [div.card, div.card, ...]
6488
+ * bw.$('.status', 'Online') // set text on all .status elements
6489
+ * bw.$('.card', function(el) { // apply function to each
6490
+ * el.style.opacity = '0.5';
6491
+ * })
6082
6492
  */
6083
6493
  if (bw._isBrowser) {
6084
- bw.$ = function(selector) {
6085
- if (!selector) return [];
6086
-
6087
- // Already an array
6088
- if (_isA(selector)) return selector;
6089
-
6090
- // Single element
6091
- if (selector.nodeType) return [selector];
6092
-
6093
- // NodeList or HTMLCollection
6094
- if (selector.length !== undefined && !_is(selector, 'string')) {
6095
- return Array.from(selector);
6494
+ bw.$ = function(selector, apply) {
6495
+ var els;
6496
+ if (!selector) {
6497
+ els = [];
6498
+ } else if (_isA(selector)) {
6499
+ els = selector;
6500
+ } else if (selector.nodeType) {
6501
+ els = [selector];
6502
+ } else if (selector.length !== undefined && !_is(selector, 'string')) {
6503
+ els = Array.from(selector);
6504
+ } else if (_is(selector, 'string')) {
6505
+ els = Array.from(document.querySelectorAll(selector));
6506
+ } else {
6507
+ els = [];
6096
6508
  }
6097
-
6098
- // CSS selector string
6099
- if (_is(selector, 'string')) {
6100
- return Array.from(document.querySelectorAll(selector));
6509
+
6510
+ if (apply !== undefined) {
6511
+ for (var i = 0; i < els.length; i++) _applyTo(els[i], apply);
6101
6512
  }
6102
-
6103
- return [];
6513
+
6514
+ return els;
6104
6515
  };
6105
-
6516
+
6106
6517
  // Convenience single element selector
6107
6518
  bw.$.one = function(selector) {
6108
6519
  return bw.$(selector)[0] || null;
@@ -6256,7 +6667,8 @@
6256
6667
  *
6257
6668
  * @param {Object} [config] - Style configuration (same as `makeStyles`)
6258
6669
  * @param {string} [scope] - Scope selector (same as `applyStyles`)
6259
- * @returns {Element|null} The `<style>` element, or null in Node.js
6670
+ * @returns {Object} The styles object (same as `makeStyles` return value:
6671
+ * `{css, alternateCss, palette, alternatePalette, rules, alternateRules, isLightPrimary}`)
6260
6672
  * @category CSS & Styling
6261
6673
  * @see bw.makeStyles
6262
6674
  * @see bw.applyStyles
@@ -6274,9 +6686,27 @@
6274
6686
  bw.injectCSS(structuralCSS, { id: 'bw_structural', append: false });
6275
6687
  }
6276
6688
  }
6277
- return bw.applyStyles(bw.makeStyles(config), scope);
6689
+ var styles = bw.makeStyles(config);
6690
+ bw.applyStyles(styles, scope);
6691
+ return styles;
6278
6692
  };
6279
6693
 
6694
+ /**
6695
+ * Prefix every selector in a rules object with a scope selector.
6696
+ * Useful for wrapping site-level CSS under `.bw_theme_alt` for dark mode.
6697
+ *
6698
+ * @param {Object} rules - CSS rules object (selector -> declarations)
6699
+ * @param {string} prefix - Scope prefix (e.g. '.bw_theme_alt')
6700
+ * @returns {Object} New rules object with scoped selectors
6701
+ * @category CSS & Styling
6702
+ * @see bw.applyStyles
6703
+ * @see bw.css
6704
+ * @example
6705
+ * var altRules = bw.scopeRulesUnder(myRules, '.bw_theme_alt');
6706
+ * bw.injectCSS(bw.css(altRules));
6707
+ */
6708
+ bw.scopeRulesUnder = scopeRulesUnder;
6709
+
6280
6710
  /**
6281
6711
  * Inject the CSS reset (box-sizing, html/body font, reduced-motion).
6282
6712
  * Idempotent — if already injected, returns the existing `<style>` element.
@@ -6296,42 +6726,48 @@
6296
6726
  };
6297
6727
 
6298
6728
  /**
6299
- * Toggle between primary and alternate palettes.
6729
+ * Toggle between primary and alternate theme palettes.
6300
6730
  *
6301
- * Adds/removes the `bw_theme_alt` class on the scoping element.
6731
+ * Adds/removes the `bw_theme_alt` class on the scoping element(s).
6302
6732
  * Without a scope, toggles on `<html>` (global).
6303
- * With a scope, toggles on the first matching element.
6733
+ * With a scope, toggles on ALL matching elements.
6304
6734
  *
6305
- * @param {string} [scope] - Scope selector (e.g. '#my-dashboard'). Omit for global.
6306
- * @returns {string} Active mode after toggle: 'primary' or 'alternate'
6735
+ * @param {string|Element} [scope] - Selector or element. Omit for global.
6736
+ * @returns {string} Active mode after toggle: 'primary' or 'alternate' (based on first element)
6307
6737
  * @category CSS & Styling
6308
6738
  * @see bw.applyStyles
6309
6739
  * @see bw.clearStyles
6310
6740
  * @example
6311
- * bw.toggleStyles(); // global toggle on <html>
6312
- * bw.toggleStyles('#my-dashboard'); // scoped toggle
6741
+ * bw.toggleThemeMode(); // global toggle on <html>
6742
+ * bw.toggleThemeMode('#my-dashboard'); // scoped toggle
6743
+ * bw.toggleThemeMode('.panel'); // toggle on ALL .panel elements
6313
6744
  */
6314
- bw.toggleStyles = function(scope) {
6745
+ bw.toggleThemeMode = function(scope) {
6315
6746
  if (!bw._isBrowser) return 'primary';
6316
- var target;
6747
+ var els;
6317
6748
  if (scope) {
6318
- var els = bw.$(scope);
6319
- target = els[0];
6749
+ els = bw.$(scope);
6320
6750
  } else {
6321
- target = document.documentElement;
6751
+ els = [document.documentElement];
6322
6752
  }
6323
- if (!target) return 'primary';
6753
+ if (!els.length) return 'primary';
6324
6754
 
6325
- var hasAlt = target.classList.contains('bw_theme_alt');
6326
- if (hasAlt) {
6327
- target.classList.remove('bw_theme_alt');
6328
- return 'primary';
6329
- } else {
6330
- target.classList.add('bw_theme_alt');
6331
- return 'alternate';
6755
+ var mode;
6756
+ for (var i = 0; i < els.length; i++) {
6757
+ var hasAlt = els[i].classList.contains('bw_theme_alt');
6758
+ if (hasAlt) {
6759
+ els[i].classList.remove('bw_theme_alt');
6760
+ } else {
6761
+ els[i].classList.add('bw_theme_alt');
6762
+ }
6763
+ if (i === 0) mode = hasAlt ? 'primary' : 'alternate';
6332
6764
  }
6765
+ return mode;
6333
6766
  };
6334
6767
 
6768
+ // Alias — kept for one release cycle. Use bw.toggleThemeMode() instead.
6769
+ bw.toggleStyles = bw.toggleThemeMode;
6770
+
6335
6771
  /**
6336
6772
  * Remove injected styles for a given scope.
6337
6773
  *
@@ -7371,6 +7807,57 @@
7371
7807
  }
7372
7808
  });
7373
7809
 
7810
+ /**
7811
+ * Query the BCCL component registry. Returns metadata about registered
7812
+ * component types -- their names and factory function names. Useful for
7813
+ * tooling, introspection, documentation generators, and auto-complete
7814
+ * systems (including MCP/AG-UI tool discovery).
7815
+ *
7816
+ * With no arguments, returns an array of all registered component types.
7817
+ * With a type name, returns metadata for that single type (or null if
7818
+ * the type is not registered).
7819
+ *
7820
+ * @param {string} [type] - Optional component type name to look up
7821
+ * @returns {Array<Object>|Object|null} Array of {type, factory} objects,
7822
+ * a single {type, factory} object, or null if the type is not found
7823
+ * @category Component
7824
+ * @see bw.make
7825
+ * @see bw.BCCL
7826
+ * @example
7827
+ * // List all available component types:
7828
+ * bw.catalog();
7829
+ * // => [{ type: 'card', factory: 'makeCard' },
7830
+ * // { type: 'button', factory: 'makeButton' }, ...]
7831
+ *
7832
+ * // Look up a specific type:
7833
+ * bw.catalog('accordion');
7834
+ * // => { type: 'accordion', factory: 'makeAccordion' }
7835
+ *
7836
+ * // Check if a type exists:
7837
+ * if (bw.catalog('chart')) { ... }
7838
+ *
7839
+ * // Get just the type names:
7840
+ * bw.catalog().map(function(c) { return c.type; });
7841
+ * // => ['card', 'button', 'container', 'row', ...]
7842
+ */
7843
+ bw.catalog = function(type) {
7844
+ if (type) {
7845
+ var def = bw.BCCL[type];
7846
+ if (!def) return null;
7847
+ return {
7848
+ type: type,
7849
+ factory: def.make.name || ('make' + type.charAt(0).toUpperCase() + type.slice(1))
7850
+ };
7851
+ }
7852
+ return Object.keys(bw.BCCL).map(function(k) {
7853
+ var def = bw.BCCL[k];
7854
+ return {
7855
+ type: k,
7856
+ factory: def.make.name || ('make' + k.charAt(0).toUpperCase() + k.slice(1))
7857
+ };
7858
+ });
7859
+ };
7860
+
7374
7861
  // Also attach to global in browsers
7375
7862
  if (bw._isBrowser && typeof window !== 'undefined') {
7376
7863
  window.bw = bw;