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