bitwrench 2.0.18 → 2.0.20

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 (58) hide show
  1. package/README.md +86 -81
  2. package/dist/bitwrench-bccl.cjs.js +221 -48
  3. package/dist/bitwrench-bccl.cjs.min.js +3 -3
  4. package/dist/bitwrench-bccl.esm.js +221 -48
  5. package/dist/bitwrench-bccl.esm.min.js +3 -3
  6. package/dist/bitwrench-bccl.umd.js +221 -48
  7. package/dist/bitwrench-bccl.umd.min.js +3 -3
  8. package/dist/bitwrench-code-edit.cjs.js +7 -9
  9. package/dist/bitwrench-code-edit.cjs.min.js +5 -7
  10. package/dist/bitwrench-code-edit.es5.js +6 -8
  11. package/dist/bitwrench-code-edit.es5.min.js +5 -7
  12. package/dist/bitwrench-code-edit.esm.js +7 -9
  13. package/dist/bitwrench-code-edit.esm.min.js +5 -7
  14. package/dist/bitwrench-code-edit.umd.js +7 -9
  15. package/dist/bitwrench-code-edit.umd.min.js +5 -7
  16. package/dist/bitwrench-debug.js +268 -0
  17. package/dist/bitwrench-debug.min.js +3 -0
  18. package/dist/bitwrench-lean.cjs.js +250 -1574
  19. package/dist/bitwrench-lean.cjs.min.js +6 -6
  20. package/dist/bitwrench-lean.es5.js +344 -1661
  21. package/dist/bitwrench-lean.es5.min.js +4 -4
  22. package/dist/bitwrench-lean.esm.js +250 -1574
  23. package/dist/bitwrench-lean.esm.min.js +6 -6
  24. package/dist/bitwrench-lean.umd.js +250 -1574
  25. package/dist/bitwrench-lean.umd.min.js +6 -6
  26. package/dist/bitwrench-util-css.cjs.js +1 -1
  27. package/dist/bitwrench-util-css.cjs.min.js +1 -1
  28. package/dist/bitwrench-util-css.es5.js +1 -1
  29. package/dist/bitwrench-util-css.es5.min.js +1 -1
  30. package/dist/bitwrench-util-css.esm.js +1 -1
  31. package/dist/bitwrench-util-css.esm.min.js +1 -1
  32. package/dist/bitwrench-util-css.umd.js +1 -1
  33. package/dist/bitwrench-util-css.umd.min.js +1 -1
  34. package/dist/bitwrench.cjs.js +510 -1660
  35. package/dist/bitwrench.cjs.min.js +7 -7
  36. package/dist/bitwrench.css +80 -33
  37. package/dist/bitwrench.es5.js +569 -1694
  38. package/dist/bitwrench.es5.min.js +5 -5
  39. package/dist/bitwrench.esm.js +510 -1660
  40. package/dist/bitwrench.esm.min.js +7 -7
  41. package/dist/bitwrench.min.css +1 -1
  42. package/dist/bitwrench.umd.js +510 -1660
  43. package/dist/bitwrench.umd.min.js +7 -7
  44. package/dist/builds.json +133 -111
  45. package/dist/bwserve.cjs.js +2 -2
  46. package/dist/bwserve.esm.js +2 -2
  47. package/dist/sri.json +46 -44
  48. package/package.json +5 -3
  49. package/readme.html +86 -75
  50. package/src/bitwrench-bccl-entry.js +3 -4
  51. package/src/bitwrench-bccl.js +217 -43
  52. package/src/bitwrench-code-edit.js +6 -8
  53. package/src/bitwrench-debug.js +245 -0
  54. package/src/bitwrench-styles.js +35 -8
  55. package/src/bitwrench.js +212 -1563
  56. package/src/cli/attach.js +53 -21
  57. package/src/cli/serve.js +179 -3
  58. package/src/version.js +3 -3
@@ -1,4 +1,4 @@
1
- /*! bitwrench v2.0.18 | BSD-2-Clause | https://deftio.github.com/bitwrench/pages */
1
+ /*! bitwrench v2.0.20 | 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.18',
15
+ version: '2.0.20',
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-17T00:50:09.505Z'
22
+ buildDate: '2026-03-23T05:19:31.951Z'
23
23
  };
24
24
 
25
25
  /**
@@ -2176,10 +2176,10 @@
2176
2176
  'position': 'relative', 'display': 'flex', 'flex-direction': 'column', 'pointer-events': 'auto',
2177
2177
  'background-clip': 'padding-box', 'border': '1px solid transparent', 'outline': '0'
2178
2178
  },
2179
- '.bw_modal_header': { 'display': 'flex', 'align-items': 'center', 'justify-content': 'space-between' },
2179
+ '.bw_modal_header': { 'display': 'flex', 'align-items': 'center', 'justify-content': 'space-between', 'padding': '1rem 1.25rem', 'border-bottom': '1px solid transparent' },
2180
2180
  '.bw_modal_title': { 'margin': '0', 'font-size': '1.25rem', 'font-weight': '600', 'line-height': '1.3' },
2181
- '.bw_modal_body': { 'position': 'relative', 'flex': '1 1 auto' },
2182
- '.bw_modal_footer': { 'display': 'flex', 'flex-wrap': 'wrap', 'align-items': 'center', 'justify-content': 'flex-end', 'gap': '0.5rem' }
2181
+ '.bw_modal_body': { 'position': 'relative', 'flex': '1 1 auto', 'padding': '1rem 1.25rem' },
2182
+ '.bw_modal_footer': { 'display': 'flex', 'flex-wrap': 'wrap', 'align-items': 'center', 'justify-content': 'flex-end', 'gap': '0.5rem', 'padding': '0.75rem 1.25rem', 'border-top': '1px solid transparent' }
2183
2183
  },
2184
2184
 
2185
2185
  // ---- Toast ----
@@ -2200,8 +2200,8 @@
2200
2200
  },
2201
2201
  '.bw_toast.bw_toast_show': { 'opacity': '1', 'transform': 'translateY(0)' },
2202
2202
  '.bw_toast.bw_toast_hiding': { 'opacity': '0' },
2203
- '.bw_toast_header': { 'display': 'flex', 'align-items': 'center', 'justify-content': 'space-between', 'font-size': '0.875rem' },
2204
- '.bw_toast_body': { 'font-size': '0.9375rem' }
2203
+ '.bw_toast_header': { 'display': 'flex', 'align-items': 'center', 'justify-content': 'space-between', 'padding': '0.5rem 0.75rem', 'font-size': '0.875rem', 'border-bottom': '1px solid transparent' },
2204
+ '.bw_toast_body': { 'padding': '0.5rem 0.75rem', 'font-size': '0.9375rem' }
2205
2205
  },
2206
2206
 
2207
2207
  // ---- Dropdown ----
@@ -2215,15 +2215,15 @@
2215
2215
  '.bw_dropdown_menu': {
2216
2216
  'position': 'absolute', 'top': '100%', 'left': '0', 'z-index': '1000', 'display': 'block',
2217
2217
  'min-width': '10rem', 'padding': '0.5rem 0', 'margin': '0.125rem 0 0',
2218
- 'background-clip': 'padding-box',
2218
+ 'background-clip': 'padding-box', 'border': '1px solid transparent',
2219
2219
  'opacity': '0', 'visibility': 'hidden', 'pointer-events': 'none'
2220
2220
  },
2221
2221
  '.bw_dropdown_menu.bw_dropdown_show': { 'opacity': '1', 'visibility': 'visible', 'pointer-events': 'auto' },
2222
2222
  '.bw_dropdown_menu_end': { 'left': 'auto', 'right': '0' },
2223
2223
  '.bw_dropdown_item': {
2224
- 'display': 'block', 'width': '100%', 'clear': 'both',
2224
+ 'display': 'block', 'width': '100%', 'padding': '0.4rem 1rem', 'clear': 'both',
2225
2225
  'font-weight': '400', 'text-align': 'inherit', 'text-decoration': 'none', 'white-space': 'nowrap',
2226
- 'background-color': 'transparent', 'border': '0', 'font-size': '0.9375rem'
2226
+ 'background-color': 'transparent', 'border': '0', 'font-size': '0.9375rem', 'cursor': 'pointer'
2227
2227
  },
2228
2228
  '.bw_dropdown_item:focus-visible': { 'outline': '2px solid currentColor', 'outline-offset': '-2px' },
2229
2229
  '.bw_dropdown_divider': { 'height': '0', 'margin': '0.5rem 0', 'overflow': 'hidden', 'opacity': '1' }
@@ -2545,6 +2545,33 @@
2545
2545
  rules['.bw_text_left'] = { 'text-align': 'left' };
2546
2546
  rules['.bw_text_right'] = { 'text-align': 'right' };
2547
2547
  rules['.bw_text_center'] = { 'text-align': 'center' };
2548
+ rules['.bw_text_justify'] = { 'text-align': 'justify' };
2549
+
2550
+ // Font weight
2551
+ rules['.bw_fw_bold'] = { 'font-weight': '700' };
2552
+ rules['.bw_fw_semibold'] = { 'font-weight': '600' };
2553
+ rules['.bw_fw_normal'] = { 'font-weight': '400' };
2554
+ rules['.bw_fw_light'] = { 'font-weight': '300' };
2555
+
2556
+ // Font style
2557
+ rules['.bw_fst_italic'] = { 'font-style': 'italic' };
2558
+ rules['.bw_fst_normal'] = { 'font-style': 'normal' };
2559
+
2560
+ // Text decoration
2561
+ rules['.bw_text_underline'] = { 'text-decoration': 'underline' };
2562
+ rules['.bw_text_line_through'] = { 'text-decoration': 'line-through' };
2563
+ rules['.bw_text_decoration_none'] = { 'text-decoration': 'none' };
2564
+
2565
+ // Text transform
2566
+ rules['.bw_text_uppercase'] = { 'text-transform': 'uppercase' };
2567
+ rules['.bw_text_lowercase'] = { 'text-transform': 'lowercase' };
2568
+ rules['.bw_text_capitalize'] = { 'text-transform': 'capitalize' };
2569
+
2570
+ // Font size
2571
+ rules['.bw_fs_sm'] = { 'font-size': '0.875rem' };
2572
+ rules['.bw_fs_base'] = { 'font-size': '1rem' };
2573
+ rules['.bw_fs_lg'] = { 'font-size': '1.25rem' };
2574
+ rules['.bw_fs_xl'] = { 'font-size': '1.5rem' };
2548
2575
 
2549
2576
  // Flexbox
2550
2577
  var jc = { start: 'flex-start', end: 'flex-end', center: 'center', between: 'space-between', around: 'space-around' };
@@ -3611,7 +3638,12 @@
3611
3638
  },
3612
3639
  o: {
3613
3640
  type: 'card',
3614
- state: props.state || {}
3641
+ state: props.state || {},
3642
+ slots: {
3643
+ title: '.bw_card_title',
3644
+ content: '.bw_card_body',
3645
+ footer: '.bw_card_footer'
3646
+ }
3615
3647
  }
3616
3648
  };
3617
3649
  }
@@ -3622,7 +3654,12 @@
3622
3654
  c: cardContent,
3623
3655
  o: {
3624
3656
  type: 'card',
3625
- state: props.state || {}
3657
+ state: props.state || {},
3658
+ slots: {
3659
+ title: '.bw_card_title',
3660
+ content: '.bw_card_body',
3661
+ footer: '.bw_card_footer'
3662
+ }
3626
3663
  }
3627
3664
  };
3628
3665
  }
@@ -3940,6 +3977,24 @@
3940
3977
  }
3941
3978
  });
3942
3979
 
3980
+ // Shared tab switching logic
3981
+ function switchTab(el, index) {
3982
+ var allTabs = el.querySelectorAll('.bw_nav_link');
3983
+ var allPanes = el.querySelectorAll('.bw_tab_pane');
3984
+ if (index < 0 || index >= allTabs.length) return;
3985
+ allTabs.forEach(function(t) {
3986
+ t.classList.remove('active');
3987
+ t.setAttribute('aria-selected', 'false');
3988
+ t.setAttribute('tabindex', '-1');
3989
+ });
3990
+ allPanes.forEach(function(p) { p.classList.remove('active'); });
3991
+ allTabs[index].classList.add('active');
3992
+ allTabs[index].setAttribute('aria-selected', 'true');
3993
+ allTabs[index].setAttribute('tabindex', '0');
3994
+ allPanes[index].classList.add('active');
3995
+ if (el._bw_state) el._bw_state.activeIndex = index;
3996
+ }
3997
+
3943
3998
  return {
3944
3999
  t: 'div',
3945
4000
  a: { class: 'bw_tabs' },
@@ -3958,24 +4013,8 @@
3958
4013
  role: 'tab',
3959
4014
  tabindex: index === actualActiveIndex ? '0' : '-1',
3960
4015
  'aria-selected': index === actualActiveIndex ? 'true' : 'false',
3961
- 'data-tab-index': index,
3962
4016
  onclick: (e) => {
3963
- const tabsContainer = e.target.closest('.bw_tabs');
3964
- const allTabs = tabsContainer.querySelectorAll('.bw_nav_link');
3965
- const allPanes = tabsContainer.querySelectorAll('.bw_tab_pane');
3966
-
3967
- allTabs.forEach(t => {
3968
- t.classList.remove('active');
3969
- t.setAttribute('aria-selected', 'false');
3970
- t.setAttribute('tabindex', '-1');
3971
- });
3972
- allPanes.forEach(p => p.classList.remove('active'));
3973
-
3974
- e.target.classList.add('active');
3975
- e.target.setAttribute('aria-selected', 'true');
3976
- e.target.setAttribute('tabindex', '0');
3977
- const targetIndex = parseInt(e.target.getAttribute('data-tab-index'));
3978
- allPanes[targetIndex].classList.add('active');
4017
+ switchTab(e.target.closest('.bw_tabs'), index);
3979
4018
  }
3980
4019
  },
3981
4020
  c: tab.label
@@ -3998,6 +4037,10 @@
3998
4037
  o: {
3999
4038
  type: 'tabs',
4000
4039
  state: { activeIndex: actualActiveIndex },
4040
+ handle: {
4041
+ setActiveTab: switchTab,
4042
+ getActiveTab: function(el) { return (el._bw_state && el._bw_state.activeIndex) || 0; }
4043
+ },
4001
4044
  mounted: function(el) {
4002
4045
  var tablist = el.querySelector('[role="tablist"]');
4003
4046
  if (!tablist) return;
@@ -4083,7 +4126,13 @@
4083
4126
  },
4084
4127
  c: '×'
4085
4128
  }
4086
- ].filter(Boolean)
4129
+ ].filter(Boolean),
4130
+ o: {
4131
+ type: 'alert',
4132
+ handle: {
4133
+ dismiss: function(el) { if (el && el.parentNode) el.parentNode.removeChild(el); }
4134
+ }
4135
+ }
4087
4136
  };
4088
4137
  }
4089
4138
 
@@ -4181,6 +4230,24 @@
4181
4230
  'aria-valuemax': max
4182
4231
  },
4183
4232
  c: label || `${percentage}%`
4233
+ },
4234
+ o: {
4235
+ type: 'progress',
4236
+ handle: {
4237
+ setValue: function(el, n) {
4238
+ var bar = el.querySelector('.bw_progress_bar');
4239
+ if (!bar) return;
4240
+ var maxVal = parseInt(bar.getAttribute('aria-valuemax')) || 100;
4241
+ var pct = Math.round((n / maxVal) * 100);
4242
+ bar.style.width = pct + '%';
4243
+ bar.setAttribute('aria-valuenow', n);
4244
+ bar.textContent = pct + '%';
4245
+ },
4246
+ getValue: function(el) {
4247
+ var bar = el.querySelector('.bw_progress_bar');
4248
+ return bar ? parseInt(bar.getAttribute('aria-valuenow')) || 0 : 0;
4249
+ }
4250
+ }
4184
4251
  }
4185
4252
  };
4186
4253
  }
@@ -5190,6 +5257,26 @@
5190
5257
  class: `bw_pagination ${size ? 'bw_pagination_' + size : ''} ${className}`.trim()
5191
5258
  },
5192
5259
  c: items
5260
+ },
5261
+ o: {
5262
+ type: 'pagination',
5263
+ state: { currentPage: currentPage, pages: pages },
5264
+ handle: {
5265
+ setPage: function(el, n) {
5266
+ if (n < 1 || n > pages) return;
5267
+ var allItems = el.querySelectorAll('.bw_page_item');
5268
+ for (var i = 0; i < allItems.length; i++) {
5269
+ allItems[i].classList.remove('bw_active');
5270
+ }
5271
+ // +1 offset: first item is prev arrow
5272
+ if (allItems[n]) allItems[n].classList.add('bw_active');
5273
+ if (el._bw_state) el._bw_state.currentPage = n;
5274
+ if (onPageChange) onPageChange(n);
5275
+ },
5276
+ getPage: function(el) {
5277
+ return (el._bw_state && el._bw_state.currentPage) || 1;
5278
+ }
5279
+ }
5193
5280
  }
5194
5281
  };
5195
5282
  }
@@ -5338,7 +5425,6 @@
5338
5425
  class: `bw_accordion_button ${item.open ? '' : 'bw_collapsed'}`.trim(),
5339
5426
  type: 'button',
5340
5427
  'aria-expanded': item.open ? 'true' : 'false',
5341
- 'data-accordion-index': index,
5342
5428
  onclick: function(e) {
5343
5429
  var btn = e.target.closest('.bw_accordion_button');
5344
5430
  var accordionEl = btn.closest('.bw_accordion');
@@ -5413,7 +5499,43 @@
5413
5499
  }),
5414
5500
  o: {
5415
5501
  type: 'accordion',
5416
- state: { multiOpen: multiOpen }
5502
+ state: { multiOpen: multiOpen },
5503
+ handle: {
5504
+ toggle: function(el, index) {
5505
+ var items = el.querySelectorAll('.bw_accordion_item');
5506
+ if (index < 0 || index >= items.length) return;
5507
+ var btn = items[index].querySelector('.bw_accordion_button');
5508
+ if (btn) btn.click();
5509
+ },
5510
+ openAll: function(el) {
5511
+ var items = el.querySelectorAll('.bw_accordion_item');
5512
+ for (var i = 0; i < items.length; i++) {
5513
+ var collapse = items[i].querySelector('.bw_accordion_collapse');
5514
+ var btn = items[i].querySelector('.bw_accordion_button');
5515
+ if (!collapse.classList.contains('bw_collapse_show')) {
5516
+ collapse.classList.add('bw_collapse_show');
5517
+ collapse.style.maxHeight = 'none';
5518
+ btn.classList.remove('bw_collapsed');
5519
+ btn.setAttribute('aria-expanded', 'true');
5520
+ }
5521
+ }
5522
+ },
5523
+ closeAll: function(el) {
5524
+ var items = el.querySelectorAll('.bw_accordion_item');
5525
+ for (var i = 0; i < items.length; i++) {
5526
+ var collapse = items[i].querySelector('.bw_accordion_collapse');
5527
+ var btn = items[i].querySelector('.bw_accordion_button');
5528
+ if (collapse.classList.contains('bw_collapse_show')) {
5529
+ collapse.style.maxHeight = collapse.scrollHeight + 'px';
5530
+ collapse.offsetHeight;
5531
+ collapse.style.maxHeight = '0px';
5532
+ collapse.classList.remove('bw_collapse_show');
5533
+ btn.classList.add('bw_collapsed');
5534
+ btn.setAttribute('aria-expanded', 'false');
5535
+ }
5536
+ }
5537
+ }
5538
+ }
5417
5539
  }
5418
5540
  };
5419
5541
  }
@@ -5503,6 +5625,14 @@
5503
5625
  },
5504
5626
  o: {
5505
5627
  type: 'modal',
5628
+ handle: {
5629
+ open: function(el) {
5630
+ el.classList.add('bw_modal_show');
5631
+ el.style.display = 'flex';
5632
+ document.body.style.overflow = 'hidden';
5633
+ },
5634
+ close: function(el) { closeModal(el); }
5635
+ },
5506
5636
  mounted: function(el) {
5507
5637
  // Click backdrop to close
5508
5638
  el.addEventListener('click', function(e) {
@@ -5561,9 +5691,8 @@
5561
5691
  return {
5562
5692
  t: 'div',
5563
5693
  a: {
5564
- class: `bw_toast ${variantClass(variant)} ${className}`.trim(),
5565
- role: 'alert',
5566
- 'data-position': position
5694
+ class: `bw_toast ${variantClass(variant)} bw_toast_${position.replace(/-/g, '_')} ${className}`.trim(),
5695
+ role: 'alert'
5567
5696
  },
5568
5697
  c: [
5569
5698
  (title) && {
@@ -5597,6 +5726,12 @@
5597
5726
  ].filter(Boolean),
5598
5727
  o: {
5599
5728
  type: 'toast',
5729
+ handle: {
5730
+ dismiss: function(el) {
5731
+ el.classList.add('bw_toast_hiding');
5732
+ setTimeout(function() { if (el.parentNode) el.parentNode.removeChild(el); }, 300);
5733
+ }
5734
+ },
5600
5735
  mounted: function(el) {
5601
5736
  // Trigger show animation
5602
5737
  requestAnimationFrame(function() {
@@ -5954,7 +6089,7 @@
5954
6089
  var total = carouselEl.querySelectorAll('.bw_carousel_slide').length;
5955
6090
  if (index < 0) index = total - 1;
5956
6091
  if (index >= total) index = 0;
5957
- carouselEl.setAttribute('data-carousel-index', index);
6092
+ carouselEl._bw_carouselIndex = index;
5958
6093
  var track = carouselEl.querySelector('.bw_carousel_track');
5959
6094
  track.style.transform = 'translateX(-' + (index * 100) + '%)';
5960
6095
  // Update indicators
@@ -6011,7 +6146,7 @@
6011
6146
  'aria-label': 'Previous slide',
6012
6147
  onclick: function(e) {
6013
6148
  var carousel = e.target.closest('.bw_carousel');
6014
- var idx = parseInt(carousel.getAttribute('data-carousel-index') || '0');
6149
+ var idx = carousel._bw_carouselIndex || 0;
6015
6150
  goToSlide(carousel, idx - 1);
6016
6151
  }
6017
6152
  },
@@ -6025,7 +6160,7 @@
6025
6160
  'aria-label': 'Next slide',
6026
6161
  onclick: function(e) {
6027
6162
  var carousel = e.target.closest('.bw_carousel');
6028
- var idx = parseInt(carousel.getAttribute('data-carousel-index') || '0');
6163
+ var idx = carousel._bw_carouselIndex || 0;
6029
6164
  goToSlide(carousel, idx + 1);
6030
6165
  }
6031
6166
  },
@@ -6045,11 +6180,9 @@
6045
6180
  class: 'bw_carousel_indicator' + (i === startIndex ? ' active' : ''),
6046
6181
  type: 'button',
6047
6182
  'aria-label': 'Go to slide ' + (i + 1),
6048
- 'data-slide-index': i,
6049
6183
  onclick: function(e) {
6050
6184
  var carousel = e.target.closest('.bw_carousel');
6051
- var idx = parseInt(e.target.getAttribute('data-slide-index'));
6052
- goToSlide(carousel, idx);
6185
+ goToSlide(carousel, i);
6053
6186
  }
6054
6187
  }
6055
6188
  };
@@ -6063,17 +6196,37 @@
6063
6196
  class: ('bw_carousel ' + className).trim(),
6064
6197
  style: 'height: ' + height,
6065
6198
  tabindex: '0',
6066
- 'aria-roledescription': 'carousel',
6067
- 'data-carousel-index': startIndex
6199
+ 'aria-roledescription': 'carousel'
6068
6200
  },
6069
6201
  c: children,
6070
6202
  o: {
6071
6203
  type: 'carousel',
6072
6204
  state: { activeIndex: startIndex, autoPlay: autoPlay, interval: interval },
6205
+ handle: {
6206
+ goToSlide: function(el, index) { goToSlide(el, index); },
6207
+ next: function(el) { goToSlide(el, (el._bw_carouselIndex || 0) + 1); },
6208
+ prev: function(el) { goToSlide(el, (el._bw_carouselIndex || 0) - 1); },
6209
+ getActiveIndex: function(el) { return el._bw_carouselIndex || 0; },
6210
+ pause: function(el) {
6211
+ if (el._bw_carouselInterval) {
6212
+ clearInterval(el._bw_carouselInterval);
6213
+ el._bw_carouselInterval = null;
6214
+ }
6215
+ },
6216
+ play: function(el) {
6217
+ if (!el._bw_carouselInterval && el._bw_state) {
6218
+ var ms = el._bw_state.interval || 5000;
6219
+ el._bw_carouselInterval = setInterval(function() {
6220
+ goToSlide(el, (el._bw_carouselIndex || 0) + 1);
6221
+ }, ms);
6222
+ }
6223
+ }
6224
+ },
6073
6225
  mounted: function(el) {
6226
+ el._bw_carouselIndex = startIndex;
6074
6227
  // Keyboard navigation
6075
6228
  el.addEventListener('keydown', function(e) {
6076
- var idx = parseInt(el.getAttribute('data-carousel-index') || '0');
6229
+ var idx = el._bw_carouselIndex || 0;
6077
6230
  if (e.key === 'ArrowLeft') {
6078
6231
  e.preventDefault();
6079
6232
  goToSlide(el, idx - 1);
@@ -6085,7 +6238,7 @@
6085
6238
  // Auto-play
6086
6239
  if (autoPlay) {
6087
6240
  var intervalId = setInterval(function() {
6088
- var idx = parseInt(el.getAttribute('data-carousel-index') || '0');
6241
+ var idx = el._bw_carouselIndex || 0;
6089
6242
  goToSlide(el, idx + 1);
6090
6243
  }, interval);
6091
6244
  el._bw_carouselInterval = intervalId;
@@ -6095,7 +6248,7 @@
6095
6248
  });
6096
6249
  el.addEventListener('mouseleave', function() {
6097
6250
  el._bw_carouselInterval = setInterval(function() {
6098
- var idx = parseInt(el.getAttribute('data-carousel-index') || '0');
6251
+ var idx = el._bw_carouselIndex || 0;
6099
6252
  goToSlide(el, idx + 1);
6100
6253
  }, interval);
6101
6254
  });
@@ -6211,7 +6364,13 @@
6211
6364
  t: 'div',
6212
6365
  a: { class: classes, style: style },
6213
6366
  c: children,
6214
- o: { type: 'stat-card' }
6367
+ o: {
6368
+ type: 'stat-card',
6369
+ slots: {
6370
+ value: '.bw_stat_value',
6371
+ label: '.bw_stat_label'
6372
+ }
6373
+ }
6215
6374
  };
6216
6375
  }
6217
6376
 
@@ -6890,7 +7049,7 @@
6890
7049
  function makeChipEl(text) {
6891
7050
  return {
6892
7051
  t: 'span',
6893
- a: { class: 'bw_chip', 'data-chip-value': text },
7052
+ a: { class: 'bw_chip' },
6894
7053
  c: [
6895
7054
  text,
6896
7055
  {
@@ -6901,9 +7060,8 @@
6901
7060
  'aria-label': 'Remove ' + text,
6902
7061
  onclick: function(e) {
6903
7062
  var chip = e.target.closest('.bw_chip');
6904
- var val = chip.getAttribute('data-chip-value');
6905
7063
  chip.parentNode.removeChild(chip);
6906
- if (onRemove) onRemove(val);
7064
+ if (onRemove) onRemove(text);
6907
7065
  }
6908
7066
  },
6909
7067
  c: '\u00D7'
@@ -6931,7 +7089,7 @@
6931
7089
  // Insert chip before the input
6932
7090
  var chipEl = document.createElement('span');
6933
7091
  chipEl.className = 'bw_chip';
6934
- chipEl.setAttribute('data-chip-value', val);
7092
+ chipEl._bw_chipValue = val;
6935
7093
  chipEl.innerHTML = '';
6936
7094
  chipEl.textContent = val;
6937
7095
  var removeBtn = document.createElement('button');
@@ -6954,7 +7112,7 @@
6954
7112
  var chipEls = wrapper.querySelectorAll('.bw_chip');
6955
7113
  if (chipEls.length) {
6956
7114
  var last = chipEls[chipEls.length - 1];
6957
- var removedVal = last.getAttribute('data-chip-value');
7115
+ var removedVal = last._bw_chipValue || last.firstChild.textContent;
6958
7116
  last.parentNode.removeChild(last);
6959
7117
  if (onRemove) onRemove(removedVal);
6960
7118
  }
@@ -6963,7 +7121,50 @@
6963
7121
  }
6964
7122
  }
6965
7123
  ],
6966
- o: { type: 'chip-input' }
7124
+ o: {
7125
+ type: 'chip-input',
7126
+ handle: {
7127
+ addChip: function(el, text) {
7128
+ if (!text) return;
7129
+ var input = el.querySelector('.bw_chip_field');
7130
+ var chipEl = document.createElement('span');
7131
+ chipEl.className = 'bw_chip';
7132
+ chipEl._bw_chipValue = text;
7133
+ chipEl.textContent = text;
7134
+ var removeBtn = document.createElement('button');
7135
+ removeBtn.type = 'button';
7136
+ removeBtn.className = 'bw_chip_remove';
7137
+ removeBtn.setAttribute('aria-label', 'Remove ' + text);
7138
+ removeBtn.textContent = '\u00D7';
7139
+ removeBtn.onclick = function() { chipEl.parentNode.removeChild(chipEl); };
7140
+ chipEl.appendChild(removeBtn);
7141
+ el.insertBefore(chipEl, input);
7142
+ },
7143
+ removeChip: function(el, text) {
7144
+ var chips = el.querySelectorAll('.bw_chip');
7145
+ for (var i = 0; i < chips.length; i++) {
7146
+ if ((chips[i]._bw_chipValue || chips[i].firstChild.textContent) === text) {
7147
+ chips[i].parentNode.removeChild(chips[i]);
7148
+ return;
7149
+ }
7150
+ }
7151
+ },
7152
+ getChips: function(el) {
7153
+ var chips = el.querySelectorAll('.bw_chip');
7154
+ var values = [];
7155
+ for (var i = 0; i < chips.length; i++) {
7156
+ values.push(chips[i]._bw_chipValue || chips[i].firstChild.textContent);
7157
+ }
7158
+ return values;
7159
+ },
7160
+ clear: function(el) {
7161
+ var chips = el.querySelectorAll('.bw_chip');
7162
+ for (var i = chips.length - 1; i >= 0; i--) {
7163
+ chips[i].parentNode.removeChild(chips[i]);
7164
+ }
7165
+ }
7166
+ }
7167
+ }
6967
7168
  };
6968
7169
  }
6969
7170
 
@@ -7150,12 +7351,11 @@
7150
7351
  _subIdCounter: 0, // monotonic ID for subscriptions
7151
7352
 
7152
7353
  // ── Node reference cache ──────────────────────────────────────────────
7153
- // Fast O(1) lookup for elements by bw_id, id attribute, or bw_uuid.
7354
+ // Fast O(1) lookup for elements by id attribute or bw_uuid_* class.
7154
7355
  //
7155
7356
  // Populated by bw.createDOM() when elements have:
7156
- // - data-bw_id attribute (user-declared addressable elements)
7157
7357
  // - id attribute (standard HTML id)
7158
- // - bw_uuid (internal, for lifecycle-managed elements)
7358
+ // - bw_uuid_* class (lifecycle-managed or explicitly addressed elements)
7159
7359
  //
7160
7360
  // Cleaned up by bw.cleanup() when elements are destroyed via bitwrench APIs.
7161
7361
  // On cache miss, falls back to querySelector/getElementById — never fails,
@@ -7163,7 +7363,7 @@
7163
7363
  // via parentNode === null check (IE11-safe, unlike el.isConnected).
7164
7364
  //
7165
7365
  // Elements created via bw.createDOM() also get el._bw_refs — a local map of
7166
- // child bw_id DOM node ref for fast parentchild access in o.render.
7366
+ // child id/UUID -> DOM node ref for fast parent->child access in o.render.
7167
7367
  // This is the bitwrench equivalent of React's compiled template "holes".
7168
7368
  //
7169
7369
  // Contract: if you remove elements outside of bitwrench APIs (raw el.remove()),
@@ -7243,7 +7443,6 @@
7243
7443
  // _cw console.warn 8
7244
7444
  // _cl console.log 11
7245
7445
  // _ce console.error 4
7246
- // _chp ComponentHandle.prototype 28 (defined after constructor)
7247
7446
  //
7248
7447
  // Note: document.createElement etc. are NOT aliased because they require
7249
7448
  // `this === document` and .bind() would add overhead on every call.
@@ -7416,15 +7615,15 @@
7416
7615
  * 1. Check `bw._nodeMap[id]` — if found and still attached (parentNode !== null), return it
7417
7616
  * 2. If cached ref is detached (parentNode === null), remove stale entry
7418
7617
  * 3. Fall back to `document.getElementById(id)` then `document.querySelector(...)`
7419
- * 4. If fallback finds the element, cache it for next time
7420
- * 5. If not found anywhere, return null
7618
+ * 4. Try class-based lookup for bw_uuid_* tokens (UUID addressing)
7619
+ * 5. Cache the result for next time
7421
7620
  *
7422
7621
  * Accepts a DOM element directly (pass-through) or a string identifier.
7423
7622
  * String identifiers are tried as: direct map key, getElementById,
7424
7623
  * querySelector (for CSS selectors starting with . or #), and
7425
- * data-bw_id attribute selector.
7624
+ * bw_uuid_* class selector.
7426
7625
  *
7427
- * @param {string|Element} id - Element ID, CSS selector, data-bw_id value, or DOM element
7626
+ * @param {string|Element} id - Element ID, CSS selector, bw_uuid_* class, or DOM element
7428
7627
  * @returns {Element|null} The DOM element, or null if not found
7429
7628
  * @category Internal
7430
7629
  */
@@ -7453,17 +7652,12 @@
7453
7652
  el = document.querySelector(id);
7454
7653
  }
7455
7654
 
7456
- // 4. Try data-bw_id attribute (for bw.uuid-generated IDs)
7457
- if (!el) {
7458
- el = document.querySelector('[data-bw_id="' + id + '"]');
7459
- }
7460
-
7461
- // 5. Try class-based lookup for bw_uuid_* tokens (UUID addressing)
7655
+ // 4. Try class-based lookup for bw_uuid_* tokens (UUID addressing)
7462
7656
  if (!el && id.indexOf('bw_uuid_') === 0) {
7463
7657
  el = document.querySelector('.' + id);
7464
7658
  }
7465
7659
 
7466
- // 6. Cache the result for next time
7660
+ // 5. Cache the result for next time
7467
7661
  if (el) {
7468
7662
  bw._nodeMap[id] = el;
7469
7663
  }
@@ -7475,17 +7669,17 @@
7475
7669
  * Register a DOM element in the node cache under one or more keys.
7476
7670
  *
7477
7671
  * Called internally by `bw.createDOM()`. Registers elements that have
7478
- * id attributes, data-bw_id attributes, or both.
7672
+ * id attributes, UUID classes, or both.
7479
7673
  *
7480
7674
  * @param {Element} el - DOM element to register
7481
- * @param {string} [bwId] - data-bw_id value to register under
7675
+ * @param {string} [uuid] - bw_uuid_* class token to register under
7482
7676
  * @category Internal
7483
7677
  */
7484
- bw._registerNode = function(el, bwId) {
7678
+ bw._registerNode = function(el, uuid) {
7485
7679
  if (!el) return;
7486
- // Register under data-bw_id
7487
- if (bwId) {
7488
- bw._nodeMap[bwId] = el;
7680
+ // Register under UUID class token
7681
+ if (uuid) {
7682
+ bw._nodeMap[uuid] = el;
7489
7683
  }
7490
7684
  // Register under id attribute
7491
7685
  var htmlId = el.getAttribute ? el.getAttribute('id') : null;
@@ -7501,13 +7695,13 @@
7501
7695
  * through bitwrench APIs.
7502
7696
  *
7503
7697
  * @param {Element} el - DOM element to deregister
7504
- * @param {string} [bwId] - data-bw_id value to remove
7698
+ * @param {string} [uuid] - bw_uuid_* class token to remove
7505
7699
  * @category Internal
7506
7700
  */
7507
- bw._deregisterNode = function(el, bwId) {
7508
- // Remove data-bw_id entry
7509
- if (bwId) {
7510
- delete bw._nodeMap[bwId];
7701
+ bw._deregisterNode = function(el, uuid) {
7702
+ // Remove UUID class entry
7703
+ if (uuid) {
7704
+ delete bw._nodeMap[uuid];
7511
7705
  }
7512
7706
  // Remove id attribute entry
7513
7707
  var htmlId = el && el.getAttribute ? el.getAttribute('id') : null;
@@ -7520,6 +7714,13 @@
7520
7714
  // bw.assignUUID() / bw.getUUID() — Explicit UUID addressing for TACO objects
7521
7715
  // ===================================================================================
7522
7716
 
7717
+ /**
7718
+ * Marker class for elements with lifecycle hooks (mounted/unmount/render/state).
7719
+ * Used by cleanup() to find lifecycle-managed elements via querySelectorAll('.bw_lc').
7720
+ * @private
7721
+ */
7722
+ var _BW_LC = 'bw_lc';
7723
+
7523
7724
  /**
7524
7725
  * Regex to match a bw_uuid_* token in a class string.
7525
7726
  * @private
@@ -7708,15 +7909,6 @@
7708
7909
  // Handle null/undefined
7709
7910
  if (taco == null) return '';
7710
7911
 
7711
- // Handle ComponentHandle — use its .taco
7712
- if (taco && taco._bwComponent === true) {
7713
- var compOptions = Object.assign({}, options);
7714
- if (!compOptions.state && taco._state) {
7715
- compOptions.state = taco._state;
7716
- }
7717
- return bw.html(taco.taco, compOptions);
7718
- }
7719
-
7720
7912
  // Handle arrays of TACOs
7721
7913
  if (_isA(taco)) {
7722
7914
  return taco.map(t => bw.html(t, options)).join('');
@@ -7727,24 +7919,6 @@
7727
7919
  return taco.v;
7728
7920
  }
7729
7921
 
7730
- // Handle bw.when() markers
7731
- if (taco && taco._bwWhen && options.state) {
7732
- var whenExpr = taco.expr.replace(/^\$\{|\}$/g, '');
7733
- var whenVal = options.compile
7734
- ? bw._resolveTemplate('${' + whenExpr + '}', options.state, true)
7735
- : bw._evaluatePath(options.state, whenExpr);
7736
- var branch = whenVal ? taco.branches[0] : (taco.branches[1] || null);
7737
- return branch ? bw.html(branch, options) : '';
7738
- }
7739
-
7740
- // Handle bw.each() markers
7741
- if (taco && taco._bwEach && options.state) {
7742
- var eachExpr = taco.expr.replace(/^\$\{|\}$/g, '');
7743
- var arr = bw._evaluatePath(options.state, eachExpr);
7744
- if (!_isA(arr)) return '';
7745
- return arr.map(function(item, idx) { return bw.html(taco.factory(item, idx), options); }).join('');
7746
- }
7747
-
7748
7922
  // Handle primitives and non-TACO objects
7749
7923
  if (!_is(taco, 'object') || !taco.t) {
7750
7924
  var str = options.raw ? String(taco) : bw.escapeHTML(String(taco));
@@ -7808,14 +7982,14 @@
7808
7982
  }
7809
7983
  }
7810
7984
 
7811
- // Add bw_id as a class if lifecycle hooks present
7812
- if ((opts.mounted || opts.unmount) && !attrs.class?.includes('bw_id_')) {
7813
- const id = opts.bw_id || bw.uuid();
7985
+ // Add bw_uuid + bw_lc classes if lifecycle hooks present
7986
+ if ((opts.mounted || opts.unmount) && !_UUID_RE.test(attrs.class || '')) {
7987
+ const uuid = bw.uuid('uuid');
7814
7988
  attrStr = attrStr.replace(/class="([^"]*)"/, (_match, classes) => {
7815
- return `class="${classes} bw_id_${id}"`.trim();
7989
+ return `class="${classes} ${uuid} ${_BW_LC}"`.trim();
7816
7990
  });
7817
7991
  if (!attrStr.includes('class=')) {
7818
- attrStr += ` class="bw_id_${id}"`;
7992
+ attrStr += ` class="${uuid} ${_BW_LC}"`;
7819
7993
  }
7820
7994
  }
7821
7995
 
@@ -8043,11 +8217,6 @@
8043
8217
  return frag;
8044
8218
  }
8045
8219
 
8046
- // Handle ComponentHandle — extract .taco for DOM creation
8047
- if (taco && taco._bwComponent === true) {
8048
- return bw.createDOM(taco.taco, options);
8049
- }
8050
-
8051
8220
  // Handle text nodes
8052
8221
  if (!_is(taco, 'object') || !taco.t) {
8053
8222
  return document.createTextNode(String(taco));
@@ -8088,24 +8257,19 @@
8088
8257
  }
8089
8258
 
8090
8259
  // Add children, building _bw_refs for fast parent→child access.
8091
- // Children with data-bw_id or id attributes get local refs on the parent,
8260
+ // Children with id attributes or bw_uuid_* classes get local refs on the parent,
8092
8261
  // so o.render functions can access them without any DOM lookup.
8093
8262
  if (content != null) {
8094
8263
  if (_isA(content)) {
8095
8264
  content.forEach(child => {
8096
8265
  if (child != null) {
8097
- // Handle ComponentHandle in content arrays (Level 2 children)
8098
- if (child._bwComponent === true) {
8099
- child.mount(el);
8100
- return;
8101
- }
8102
8266
  var childEl = bw.createDOM(child, options);
8103
8267
  el.appendChild(childEl);
8104
8268
  // Build local refs for addressable children
8105
- var childBwId = (child && child.a) ? (child.a['data-bw_id'] || child.a.id) : null;
8106
- if (childBwId) {
8269
+ var childRefId = (child && child.a) ? (child.a.id || bw.getUUID(child)) : null;
8270
+ if (childRefId) {
8107
8271
  if (!el._bw_refs) el._bw_refs = {};
8108
- el._bw_refs[childBwId] = childEl;
8272
+ el._bw_refs[childRefId] = childEl;
8109
8273
  }
8110
8274
  // Bubble up grandchild refs (flatten one level)
8111
8275
  if (childEl._bw_refs) {
@@ -8121,16 +8285,13 @@
8121
8285
  } else if (_is(content, 'object') && content.__bw_raw) {
8122
8286
  // Raw HTML content — inject via innerHTML
8123
8287
  el.innerHTML = content.v;
8124
- } else if (content._bwComponent === true) {
8125
- // Single ComponentHandle as content
8126
- content.mount(el);
8127
8288
  } else if (_is(content, 'object') && content.t) {
8128
8289
  var childEl = bw.createDOM(content, options);
8129
8290
  el.appendChild(childEl);
8130
- var childBwId = content.a ? (content.a['data-bw_id'] || content.a.id) : null;
8131
- if (childBwId) {
8291
+ var childRefId = content.a ? (content.a.id || bw.getUUID(content)) : null;
8292
+ if (childRefId) {
8132
8293
  if (!el._bw_refs) el._bw_refs = {};
8133
- el._bw_refs[childBwId] = childEl;
8294
+ el._bw_refs[childRefId] = childEl;
8134
8295
  }
8135
8296
  if (childEl._bw_refs) {
8136
8297
  if (!el._bw_refs) el._bw_refs = {};
@@ -8160,57 +8321,88 @@
8160
8321
 
8161
8322
  // Handle lifecycle hooks and state
8162
8323
  if (opts.mounted || opts.unmount || opts.render || opts.state) {
8163
- const id = attrs['data-bw_id'] || bw.uuid();
8164
- el.setAttribute('data-bw_id', id);
8324
+ // Ensure element has a UUID class for identity
8325
+ var uuid = bw.getUUID(el) || bw.uuid('uuid');
8326
+ el.classList.add(uuid);
8327
+ el.classList.add(_BW_LC);
8165
8328
 
8166
- // Register in node cache under data-bw_id
8167
- bw._registerNode(el, id);
8329
+ // Register in node cache under UUID class
8330
+ bw._registerNode(el, uuid);
8168
8331
 
8169
8332
  // Store state
8170
8333
  if (opts.state) {
8171
8334
  el._bw_state = opts.state;
8172
8335
  }
8173
8336
 
8174
- // o.render — first-class render function (replaces mounted boilerplate)
8337
+ // o.render — store the render function for bw.update()
8175
8338
  if (opts.render) {
8176
8339
  el._bw_render = opts.render;
8340
+ }
8177
8341
 
8178
- if (opts.mounted) {
8179
- _cw('bw.createDOM: o.render and o.mounted are mutually exclusive. o.render wins.');
8180
- }
8342
+ // Determine what to call on mount:
8343
+ // - If o.mounted exists, call it (it can call el._bw_render() for initial render)
8344
+ // - Otherwise if o.render exists, auto-call it as a convenience shorthand
8345
+ var mountFn = opts.mounted || (opts.render ? function(mountEl) {
8346
+ opts.render(mountEl, mountEl._bw_state || {});
8347
+ } : null);
8181
8348
 
8182
- // Queue initial render (same timing as mounted)
8183
- if (document.body.contains(el)) {
8184
- opts.render(el, el._bw_state || {});
8185
- } else {
8186
- requestAnimationFrame(() => {
8187
- if (document.body.contains(el)) {
8188
- opts.render(el, el._bw_state || {});
8189
- }
8190
- });
8191
- }
8192
- } else if (opts.mounted) {
8193
- // Queue mounted callback (legacy pattern)
8349
+ if (mountFn) {
8194
8350
  if (document.body.contains(el)) {
8195
- opts.mounted(el, el._bw_state || {});
8351
+ mountFn(el, el._bw_state || {});
8196
8352
  } else {
8197
8353
  requestAnimationFrame(() => {
8198
8354
  if (document.body.contains(el)) {
8199
- opts.mounted(el, el._bw_state || {});
8355
+ mountFn(el, el._bw_state || {});
8200
8356
  }
8201
8357
  });
8202
8358
  }
8203
8359
  }
8204
8360
 
8205
- // Store unmount callback
8361
+ // Store unmount callback keyed by UUID class
8206
8362
  if (opts.unmount) {
8207
- bw._unmountCallbacks.set(id, () => {
8363
+ bw._unmountCallbacks.set(uuid, () => {
8208
8364
  opts.unmount(el, el._bw_state || {});
8209
8365
  });
8210
8366
  }
8211
- } else if (attrs['data-bw_id']) {
8212
- // Element has explicit data-bw_id but no lifecycle hooks — still register it
8213
- bw._registerNode(el, attrs['data-bw_id']);
8367
+ }
8368
+
8369
+ // Component handle: attach methods to el.bw namespace
8370
+ if (opts.handle || opts.slots) {
8371
+ if (!el.bw) el.bw = {};
8372
+
8373
+ // Explicit handle methods: fn(el, ...args) -> el.bw.method(...args)
8374
+ if (opts.handle) {
8375
+ for (var hk in opts.handle) {
8376
+ if (_hop.call(opts.handle, hk)) {
8377
+ el.bw[hk] = opts.handle[hk].bind(null, el);
8378
+ }
8379
+ }
8380
+ }
8381
+
8382
+ // Slot declarations: auto-generate setX/getX pairs
8383
+ if (opts.slots) {
8384
+ for (var sk in opts.slots) {
8385
+ if (_hop.call(opts.slots, sk)) {
8386
+ (function(name, selector) {
8387
+ var cap = name.charAt(0).toUpperCase() + name.slice(1);
8388
+ el.bw['set' + cap] = function(value) {
8389
+ var t = el.querySelector(selector);
8390
+ if (!t) return;
8391
+ if (value != null && typeof value === 'object' && value.t) {
8392
+ t.innerHTML = '';
8393
+ t.appendChild(bw.createDOM(value));
8394
+ } else {
8395
+ t.textContent = (value != null) ? String(value) : '';
8396
+ }
8397
+ };
8398
+ el.bw['get' + cap] = function() {
8399
+ var t = el.querySelector(selector);
8400
+ return t ? t.textContent : '';
8401
+ };
8402
+ })(sk, opts.slots[sk]);
8403
+ }
8404
+ }
8405
+ }
8214
8406
  }
8215
8407
 
8216
8408
  return el;
@@ -8257,7 +8449,7 @@
8257
8449
  // the target is the mount point, not the content being replaced)
8258
8450
  const savedState = targetEl._bw_state;
8259
8451
  const savedRender = targetEl._bw_render;
8260
- const savedBwId = targetEl.getAttribute('data-bw_id');
8452
+ const savedUuid = bw.getUUID(targetEl);
8261
8453
  const savedSubs = targetEl._bw_subs;
8262
8454
 
8263
8455
  // Temporarily remove _bw_subs so cleanup doesn't call them
@@ -8269,10 +8461,9 @@
8269
8461
  // Restore the target's own state/render/subs after cleanup
8270
8462
  if (savedState !== undefined) targetEl._bw_state = savedState;
8271
8463
  if (savedRender) targetEl._bw_render = savedRender;
8272
- if (savedBwId) {
8273
- targetEl.setAttribute('data-bw_id', savedBwId);
8274
- // Re-register mount point in node cache (cleanup deregistered it)
8275
- bw._registerNode(targetEl, savedBwId);
8464
+ if (savedUuid) {
8465
+ // UUID class stays on element through cleanup; re-register in cache
8466
+ bw._registerNode(targetEl, savedUuid);
8276
8467
  }
8277
8468
  if (savedSubs) targetEl._bw_subs = savedSubs;
8278
8469
 
@@ -8280,25 +8471,11 @@
8280
8471
  targetEl.innerHTML = '';
8281
8472
 
8282
8473
  if (taco != null) {
8283
- // Handle ComponentHandle (reactive components from bw.component())
8284
- if (taco._bwComponent === true) {
8285
- taco.mount(targetEl);
8286
- }
8287
- // Handle component handles (objects with element property)
8288
- else if (taco.element instanceof Element) {
8289
- targetEl.appendChild(taco.element);
8290
- }
8291
8474
  // Handle arrays
8292
- else if (_isA(taco)) {
8475
+ if (_isA(taco)) {
8293
8476
  taco.forEach(t => {
8294
8477
  if (t != null) {
8295
- if (t._bwComponent === true) {
8296
- t.mount(targetEl);
8297
- } else if (t.element instanceof Element) {
8298
- targetEl.appendChild(t.element);
8299
- } else {
8300
- targetEl.appendChild(bw.createDOM(t, options));
8301
- }
8478
+ targetEl.appendChild(bw.createDOM(t, options));
8302
8479
  }
8303
8480
  });
8304
8481
  }
@@ -8311,205 +8488,36 @@
8311
8488
  return targetEl;
8312
8489
  };
8313
8490
 
8314
- /**
8315
- * Compile props into getter/setter functions for reactive updates.
8316
- *
8317
- * Used internally by `bw.renderComponent()`. Creates a proxy-like object
8318
- * where setting a property triggers `handle.onPropChange()`.
8319
- *
8320
- * @param {Object} handle - Component handle
8321
- * @param {Object} props - Initial props
8322
- * @returns {Object} Compiled props object with getters/setters
8323
- * @category DOM Generation
8324
- */
8325
- bw.compileProps = function(handle, props = {}) {
8326
- const compiledProps = {};
8327
-
8328
- _keys(props).forEach(key => {
8329
- // Create getter/setter for each prop
8330
- Object.defineProperty(compiledProps, key, {
8331
- get() {
8332
- return handle._props[key];
8333
- },
8334
- set(value) {
8335
- const oldValue = handle._props[key];
8336
- if (oldValue !== value) {
8337
- handle._props[key] = value;
8338
- // Trigger update if prop changed
8339
- if (handle.onPropChange) {
8340
- handle.onPropChange(key, value, oldValue);
8341
- }
8342
- }
8343
- },
8344
- enumerable: true,
8345
- configurable: true
8346
- });
8347
- });
8348
-
8349
- return compiledProps;
8350
- };
8491
+ // Deprecation stubs for removed ComponentHandle APIs
8492
+ bw.compileProps = function() { throw new Error('bw.compileProps() removed in v2.0.19. Use o.handle/o.slots instead.'); };
8493
+ bw.renderComponent = function() { throw new Error('bw.renderComponent() removed in v2.0.19. Use bw.mount() with o.handle/o.slots instead.'); };
8351
8494
 
8352
8495
  /**
8353
- * Render a TACO component and return an enhanced handle object.
8354
- *
8355
- * The handle provides compiled props, state management, child registration,
8356
- * and a destroy method. Used internally by `bw.createCard()`, `bw.createTable()`, etc.
8496
+ * Mount a TACO into a target element and return the created root element.
8497
+ * Like bw.DOM() but returns the root element of the TACO (not the container),
8498
+ * giving direct access to el.bw handle methods.
8357
8499
  *
8358
- * @param {Object} taco - TACO object to render
8359
- * @param {Object} [options] - Render options
8360
- * @returns {Object} Component handle with element, props, state, update(), destroy()
8500
+ * @param {string|Element} target - CSS selector or DOM element
8501
+ * @param {Object} taco - TACO to render
8502
+ * @param {Object} [options] - Mount options
8503
+ * @returns {Element} The created root element
8361
8504
  * @category DOM Generation
8362
- */
8363
- bw.renderComponent = function(taco, options = {}) {
8364
- const element = bw.createDOM(taco, options);
8365
-
8366
- // Enhanced handle with prop compilation
8367
- const handle = {
8368
- element,
8369
- taco,
8370
- _props: { ...taco.a }, // Store props internally
8371
- _state: taco.o?.state || {},
8372
- _children: {}, // Store child component references
8373
-
8374
- // Get compiled props with getters/setters
8375
- get props() {
8376
- if (!this._compiledProps) {
8377
- this._compiledProps = bw.compileProps(this, this._props);
8378
- }
8379
- return this._compiledProps;
8380
- },
8381
-
8382
- /**
8383
- * Query all matching elements within this component
8384
- * @param {string} selector - CSS selector
8385
- * @returns {NodeList} Matching elements
8386
- */
8387
- $(selector) {
8388
- return this.element.querySelectorAll(selector);
8389
- },
8390
-
8391
- /**
8392
- * Query the first matching element within this component
8393
- * @param {string} selector - CSS selector
8394
- * @returns {Element|null} First matching element or null
8395
- */
8396
- $first(selector) {
8397
- return this.element.querySelector(selector);
8398
- },
8399
-
8400
- /**
8401
- * Update component with new props and re-render in place
8402
- * @param {Object} newProps - Properties to merge into current props
8403
- * @returns {Object} this handle (for chaining)
8404
- */
8405
- update(newProps) {
8406
- // Update internal props
8407
- Object.assign(this._props, newProps);
8408
-
8409
- // Rebuild TACO with new props
8410
- const newTaco = { ...this.taco, a: { ...this.taco.a, ...newProps } };
8411
- const newElement = bw.createDOM(newTaco, options);
8412
-
8413
- // Replace in DOM
8414
- this.element.replaceWith(newElement);
8415
- this.element = newElement;
8416
- this.taco = newTaco;
8417
-
8418
- return this;
8419
- },
8420
-
8421
- /**
8422
- * Re-render the component from its current TACO, replacing the DOM element
8423
- * @returns {Object} this handle (for chaining)
8424
- */
8425
- render() {
8426
- const newElement = bw.createDOM(this.taco, options);
8427
- this.element.replaceWith(newElement);
8428
- this.element = newElement;
8429
- return this;
8430
- },
8431
-
8432
- /**
8433
- * Called when a compiled prop value changes. Override to customize behavior.
8434
- * Default implementation triggers a full re-render.
8435
- * @param {string} key - Property name that changed
8436
- * @param {*} newValue - New property value
8437
- * @param {*} oldValue - Previous property value
8438
- */
8439
- onPropChange(_key, _newValue, _oldValue) {
8440
- // Auto re-render on prop change by default
8441
- this.render();
8442
- },
8443
-
8444
- // State management
8445
- get state() {
8446
- return this._state;
8447
- },
8448
-
8449
- set state(newState) {
8450
- this._state = newState;
8451
- this.render();
8452
- },
8453
-
8454
- /**
8455
- * Merge state updates and re-render the component
8456
- * @param {Object} updates - State properties to merge
8457
- * @returns {Object} this handle (for chaining)
8458
- */
8459
- setState(updates) {
8460
- Object.assign(this._state, updates);
8461
- this.render();
8462
- return this;
8463
- },
8464
-
8465
- /**
8466
- * Register a child component under a name for later retrieval
8467
- * @param {string} name - Child name key
8468
- * @param {Object} component - Child component handle
8469
- * @returns {Object} this handle (for chaining)
8470
- */
8471
- addChild(name, component) {
8472
- this._children[name] = component;
8473
- return this;
8474
- },
8475
-
8476
- /**
8477
- * Retrieve a registered child component by name
8478
- * @param {string} name - Child name key
8479
- * @returns {Object|undefined} Child component handle
8480
- */
8481
- getChild(name) {
8482
- return this._children[name];
8483
- },
8484
-
8485
- /**
8486
- * Destroy this component and all registered children
8487
- *
8488
- * Calls destroy() recursively on children, runs bw.cleanup(),
8489
- * removes the element from DOM, and clears all internal references.
8490
- */
8491
- destroy() {
8492
- // Destroy children first
8493
- Object.values(this._children).forEach(child => {
8494
- if (child && child.destroy) child.destroy();
8495
- });
8496
-
8497
- // Clean up this component
8498
- bw.cleanup(this.element);
8499
- this.element.remove();
8500
-
8501
- // Clear references
8502
- this._children = {};
8503
- this._props = {};
8504
- this._state = {};
8505
- this._compiledProps = null;
8506
- }
8507
- };
8508
-
8509
- // Store handle reference on element
8510
- element._bwHandle = handle;
8511
-
8512
- return handle;
8505
+ * @example
8506
+ * var el = bw.mount('#app', bw.makeCarousel({ items: slides }));
8507
+ * el.bw.goToSlide(2);
8508
+ * el.bw.next();
8509
+ */
8510
+ bw.mount = function(target, taco, options) {
8511
+ var container = _is(target, 'string') ? bw.$(target)[0] : target;
8512
+ if (!container) {
8513
+ _cw('bw.mount: target not found');
8514
+ return null;
8515
+ }
8516
+ bw.cleanup(container);
8517
+ container.innerHTML = '';
8518
+ var el = bw.createDOM(taco, options || {});
8519
+ container.appendChild(el);
8520
+ return el;
8513
8521
  };
8514
8522
 
8515
8523
  /**
@@ -8530,34 +8538,29 @@
8530
8538
  bw.cleanup = function(element) {
8531
8539
  if (!bw._isBrowser || !element) return;
8532
8540
 
8533
- // Deregister UUID classes from node cache (element + descendants)
8534
- // Covers elements that have UUID but no data-bw_id
8535
- var selfUuidMatch = element.className && element.className.match(_UUID_RE);
8536
- if (selfUuidMatch) delete bw._nodeMap[selfUuidMatch[0]];
8541
+ // Deregister UUID classes from node cache for non-lifecycle UUID elements
8537
8542
  var uuidEls = element.querySelectorAll('[class*="bw_uuid_"]');
8538
8543
  uuidEls.forEach(function(uel) {
8539
8544
  var m = uel.className && uel.className.match(_UUID_RE);
8540
8545
  if (m) delete bw._nodeMap[m[0]];
8541
8546
  });
8542
8547
 
8543
- // Find all elements with data-bw_id
8544
- const elements = element.querySelectorAll('[data-bw_id]');
8548
+ // Find all lifecycle-managed elements (have bw_lc marker class)
8549
+ const elements = element.querySelectorAll('.' + _BW_LC);
8545
8550
 
8546
8551
  elements.forEach(el => {
8547
- const id = el.getAttribute('data-bw_id');
8548
- const callback = bw._unmountCallbacks.get(id);
8549
-
8550
- if (callback) {
8551
- callback();
8552
- bw._unmountCallbacks.delete(id);
8553
- }
8552
+ var uuid = bw.getUUID(el);
8554
8553
 
8555
- // Deregister from node cache
8556
- bw._deregisterNode(el, id);
8554
+ if (uuid) {
8555
+ const callback = bw._unmountCallbacks.get(uuid);
8556
+ if (callback) {
8557
+ callback();
8558
+ bw._unmountCallbacks.delete(uuid);
8559
+ }
8557
8560
 
8558
- // Deregister UUID class from node cache
8559
- var uuidMatch = el.className && el.className.match(_UUID_RE);
8560
- if (uuidMatch) delete bw._nodeMap[uuidMatch[0]];
8561
+ // Deregister from node cache
8562
+ bw._deregisterNode(el, uuid);
8563
+ }
8561
8564
 
8562
8565
  // Clean up pub/sub subscriptions tied to this element
8563
8566
  if (el._bw_subs) {
@@ -8572,20 +8575,18 @@
8572
8575
  });
8573
8576
 
8574
8577
  // Check element itself
8575
- const id = element.getAttribute('data-bw_id');
8576
- if (id) {
8577
- const callback = bw._unmountCallbacks.get(id);
8578
+ var selfUuid = bw.getUUID(element);
8579
+ if (selfUuid) {
8580
+ delete bw._nodeMap[selfUuid];
8581
+
8582
+ const callback = bw._unmountCallbacks.get(selfUuid);
8578
8583
  if (callback) {
8579
8584
  callback();
8580
- bw._unmountCallbacks.delete(id);
8585
+ bw._unmountCallbacks.delete(selfUuid);
8581
8586
  }
8582
8587
 
8583
8588
  // Deregister from node cache
8584
- bw._deregisterNode(element, id);
8585
-
8586
- // Deregister UUID class from node cache
8587
- var elemUuidMatch = element.className && element.className.match(_UUID_RE);
8588
- if (elemUuidMatch) delete bw._nodeMap[elemUuidMatch[0]];
8589
+ bw._deregisterNode(element, selfUuid);
8589
8590
 
8590
8591
  // Clean up pub/sub subscriptions tied to element itself
8591
8592
  if (element._bw_subs) {
@@ -8596,11 +8597,11 @@
8596
8597
  delete element._bw_render;
8597
8598
  delete element._bw_refs;
8598
8599
 
8599
- // Clean up ComponentHandle back-reference
8600
- if (element._bwComponentHandle) {
8601
- element._bwComponentHandle.mounted = false;
8602
- element._bwComponentHandle.element = null;
8603
- delete element._bwComponentHandle;
8600
+ } else {
8601
+ // No UUID on element itself, but still check for _bw_subs (from bw.sub())
8602
+ if (element._bw_subs) {
8603
+ element._bw_subs.forEach(function(unsub) { unsub(); });
8604
+ delete element._bw_subs;
8604
8605
  }
8605
8606
  }
8606
8607
  };
@@ -8616,7 +8617,7 @@
8616
8617
  * Calls `el._bw_render(el, state)` and emits `bw:statechange` so other
8617
8618
  * components can react without tight coupling.
8618
8619
  *
8619
- * @param {string|Element} target - Element ID, data-bw_id, CSS selector, or DOM element
8620
+ * @param {string|Element} target - Element ID, bw_uuid_* class, CSS selector, or DOM element
8620
8621
  * @returns {Element|null} The element, or null if not found / no render function
8621
8622
  * @category State Management
8622
8623
  * @see bw.patch
@@ -8641,7 +8642,7 @@
8641
8642
  * Use `bw.patch()` for lightweight value updates (scores, labels, counters)
8642
8643
  * and `bw.update()` for full structural re-renders.
8643
8644
  *
8644
- * @param {string|Element} id - Element ID, data-bw_id, CSS selector, or DOM element.
8645
+ * @param {string|Element} id - Element ID, bw_uuid_* class, CSS selector, or DOM element.
8645
8646
  * Uses node cache for O(1) lookup; falls back to DOM query on cache miss.
8646
8647
  * @param {string|Object} content - New text content, or TACO object to replace children
8647
8648
  * @param {string} [attr] - If provided, sets this attribute instead of content
@@ -8716,7 +8717,7 @@
8716
8717
  * bubble by default so ancestor elements can listen. Use with `bw.on()` for
8717
8718
  * DOM-scoped communication between components.
8718
8719
  *
8719
- * @param {string|Element} target - Element ID, data-bw_id, CSS selector, or DOM element.
8720
+ * @param {string|Element} target - Element ID, bw_uuid_* class, CSS selector, or DOM element.
8720
8721
  * Uses node cache for O(1) lookup; falls back to DOM query on cache miss.
8721
8722
  * @param {string} eventName - Event name (will be prefixed with 'bw:')
8722
8723
  * @param {*} [detail] - Data to pass with the event
@@ -8743,7 +8744,7 @@
8743
8744
  * is the first argument so you don't need to destructure `e.detail`.
8744
8745
  * Events bubble, so you can listen on an ancestor element.
8745
8746
  *
8746
- * @param {string|Element} target - Element ID, data-bw_id, CSS selector, or DOM element.
8747
+ * @param {string|Element} target - Element ID, bw_uuid_* class, CSS selector, or DOM element.
8747
8748
  * Uses node cache for O(1) lookup; falls back to DOM query on cache miss.
8748
8749
  * @param {string} eventName - Event name (will be prefixed with 'bw:')
8749
8750
  * @param {Function} handler - Called with (detail, event)
@@ -8841,10 +8842,12 @@
8841
8842
  if (el) {
8842
8843
  if (!el._bw_subs) el._bw_subs = [];
8843
8844
  el._bw_subs.push(unsub);
8844
- // Ensure element has data-bw_id so bw.cleanup() finds it
8845
- if (!el.getAttribute('data-bw_id')) {
8846
- var bwId = 'bw_sub_' + id;
8847
- el.setAttribute('data-bw_id', bwId);
8845
+ // Ensure element has UUID + bw_lc so bw.cleanup() finds it
8846
+ if (!bw.getUUID(el)) {
8847
+ el.classList.add(bw.uuid('uuid'));
8848
+ }
8849
+ if (!el.classList.contains(_BW_LC)) {
8850
+ el.classList.add(_BW_LC);
8848
8851
  }
8849
8852
  }
8850
8853
 
@@ -9066,1148 +9069,97 @@
9066
9069
  return result;
9067
9070
  };
9068
9071
 
9069
- /**
9070
- * Extract top-level state keys that an expression depends on.
9071
- * @param {string} expr - Expression string
9072
- * @param {string[]} stateKeys - Declared state keys
9073
- * @returns {string[]} Matching dependency keys
9074
- * @private
9075
- */
9076
- bw._extractDeps = function(expr, stateKeys) {
9077
- var deps = [];
9078
- for (var i = 0; i < stateKeys.length; i++) {
9079
- var key = stateKeys[i];
9080
- // Match word boundary: key must be preceded by start/non-word and followed by non-word/end
9081
- var re = new RegExp('(?:^|[^\\w$.])' + key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '(?:[^\\w$]|$)');
9082
- if (re.test(expr) || expr === key || expr.indexOf(key + '.') === 0) {
9083
- deps.push(key);
9084
- }
9085
- }
9086
- return deps;
9087
- };
9088
-
9089
9072
  // ===================================================================================
9090
- // Microtask Batching
9073
+ // Deprecation stubs for removed ComponentHandle APIs (v2.0.19)
9091
9074
  // ===================================================================================
9092
9075
 
9093
- bw._dirtyComponents = [];
9094
- bw._flushScheduled = false;
9076
+ bw._extractDeps = undefined;
9077
+ bw._dirtyComponents = undefined;
9078
+ bw._flushScheduled = undefined;
9079
+ bw._scheduleFlush = undefined;
9080
+ bw._doFlush = undefined;
9081
+ bw._ComponentHandle = undefined;
9095
9082
 
9096
9083
  /**
9097
- * Schedule a microtask flush for dirty components.
9098
- * @private
9084
+ * No-op flush (ComponentHandle removed in v2.0.19).
9085
+ * Kept as no-op for backward compatibility.
9086
+ * @category Component
9099
9087
  */
9100
- bw._scheduleFlush = function() {
9101
- if (bw._flushScheduled) return;
9102
- bw._flushScheduled = true;
9103
- if (typeof Promise !== 'undefined') {
9104
- Promise.resolve().then(bw._doFlush);
9105
- } else {
9106
- setTimeout(bw._doFlush, 0);
9107
- }
9108
- };
9088
+ bw.flush = function() {};
9109
9089
 
9110
- /**
9111
- * Flush all dirty components. Deduplicates by _bwId.
9112
- * @private
9113
- */
9114
- bw._doFlush = function() {
9115
- bw._flushScheduled = false;
9116
- var queue = bw._dirtyComponents.slice();
9117
- bw._dirtyComponents = [];
9118
- // Deduplicate by _bwId
9119
- var seen = {};
9120
- for (var i = 0; i < queue.length; i++) {
9121
- var comp = queue[i];
9122
- if (!seen[comp._bwId]) {
9123
- seen[comp._bwId] = true;
9124
- comp._flush();
9125
- }
9126
- }
9127
- };
9090
+
9091
+ bw.when = function() { throw new Error('bw.when() removed in v2.0.19. Use conditional logic in o.render instead.'); };
9092
+ bw.each = function() { throw new Error('bw.each() removed in v2.0.19. Use array mapping in o.render instead.'); };
9093
+ bw.component = function() { throw new Error('bw.component() removed in v2.0.19. Use o.handle/o.slots on TACO options instead.'); };
9094
+
9095
+
9096
+ // ===================================================================================
9097
+ // bw.message() SendMessage() for the web
9098
+ // ===================================================================================
9128
9099
 
9129
9100
  /**
9130
- * Synchronous flush for testing and imperative code.
9131
- * Forces immediate re-render of all dirty components.
9101
+ * Dispatch a message to a component by UUID, CSS class, or selector.
9102
+ * Finds the element, looks up el.bw, and calls the named method.
9103
+ * This is the bitwrench equivalent of Win32 SendMessage(hwnd, msg, wParam, lParam).
9132
9104
  *
9105
+ * @param {string} target - Component UUID (bw_uuid_*), CSS class, or selector
9106
+ * @param {string} action - Method name to call on el.bw
9107
+ * @param {*} data - Data to pass to the method
9108
+ * @returns {boolean} True if message was dispatched successfully
9133
9109
  * @category Component
9110
+ * @example
9111
+ * bw.message('my_carousel', 'goToSlide', 2);
9112
+ * // Or from SSE handler:
9113
+ * es.onmessage = function(e) {
9114
+ * var msg = JSON.parse(e.data);
9115
+ * bw.message(msg.target, msg.action, msg.data);
9116
+ * };
9134
9117
  */
9135
- bw.flush = function() {
9136
- bw._doFlush();
9118
+ bw.message = function(target, action, data) {
9119
+ var el = bw._el(target);
9120
+ if (!el) el = bw.$('.' + target)[0];
9121
+ if (!el || !el.bw || typeof el.bw[action] !== 'function') {
9122
+ _cw('bw.message: no handle method "' + action + '" on ' + target);
9123
+ return false;
9124
+ }
9125
+ el.bw[action](data);
9126
+ return true;
9137
9127
  };
9138
9128
 
9139
9129
  // ===================================================================================
9140
- // ComponentHandle unified reactive component (Phase 1)
9130
+ // bw.apply() / bw.parseJSONFlex() Server-driven UI protocol
9141
9131
  // ===================================================================================
9142
9132
 
9143
9133
  /**
9144
- * ComponentHandle constructor.
9145
- * Wraps a TACO definition with reactive state, lifecycle hooks,
9146
- * template bindings, and named actions.
9147
- *
9148
- * @param {Object} taco - TACO definition {t, a, c, o}
9149
- * @constructor
9134
+ * Registry of named functions sent via register messages.
9135
+ * Populated by bw.apply({ type: 'register', name, body }).
9136
+ * Invoked by bw.apply({ type: 'call', name, args }).
9150
9137
  * @private
9151
9138
  */
9152
- function ComponentHandle(taco) {
9153
- this._bwComponent = true; // duck-type marker
9154
- this._bwId = bw.uuid('comp');
9155
- this.taco = taco;
9156
- this.element = null;
9157
- this.mounted = false;
9158
-
9159
- var o = taco.o || {};
9160
- // Copy initial state
9161
- this._state = {};
9162
- if (o.state) {
9163
- for (var k in o.state) {
9164
- if (_hop.call(o.state, k)) {
9165
- this._state[k] = o.state[k];
9166
- }
9167
- }
9168
- }
9169
- // Copy actions
9170
- this._actions = {};
9171
- if (o.actions) {
9172
- for (var k2 in o.actions) {
9173
- if (_hop.call(o.actions, k2)) {
9174
- this._actions[k2] = o.actions[k2];
9175
- }
9176
- }
9177
- }
9178
- // Promote o.methods to handle API (MFC/Qt pattern: component owns its methods)
9179
- this._methods = {};
9180
- if (o.methods) {
9181
- var self = this;
9182
- for (var k3 in o.methods) {
9183
- if (_hop.call(o.methods, k3)) {
9184
- this._methods[k3] = o.methods[k3];
9185
- (function(methodName, methodFn) {
9186
- self[methodName] = function() {
9187
- var args = [self].concat(Array.prototype.slice.call(arguments));
9188
- return methodFn.apply(null, args);
9189
- };
9190
- })(k3, o.methods[k3]);
9191
- }
9192
- }
9193
- }
9194
- // User tag for addressing via bw.message()
9195
- this._userTag = null;
9196
- // Lifecycle hooks
9197
- this._hooks = {
9198
- willMount: o.willMount || null,
9199
- mounted: o.mounted || null,
9200
- willUpdate: o.willUpdate || null,
9201
- onUpdate: o.onUpdate || o.updated || null,
9202
- unmount: o.unmount || null,
9203
- willDestroy: o.willDestroy || null
9204
- };
9205
- // Binding tracking
9206
- this._bindings = [];
9207
- this._dirtyKeys = {};
9208
- this._scheduled = false;
9209
- this._subs = [];
9210
- this._eventListeners = [];
9211
- this._registeredActions = [];
9212
- this._prevValues = {};
9213
- this._compile = !!o.compile;
9214
- this._bw_refs = {};
9215
- this._refCounter = 0;
9216
- // Child component ownership (Bug #5)
9217
- this._children = [];
9218
- this._parent = null;
9219
- // Factory metadata for BCCL rebuild (Bug #6)
9220
- this._factory = taco._bwFactory || null;
9221
- }
9222
-
9223
- // Short alias for ComponentHandle.prototype (see alias block at top of file).
9224
- // 28 method definitions × 25 chars = ~700B raw savings in minified output.
9225
- var _chp = ComponentHandle.prototype;
9226
-
9227
- // ── State Methods ──
9139
+ bw._clientFunctions = {};
9228
9140
 
9229
9141
  /**
9230
- * Get a state value. Dot-path supported: `get('user.name')`
9142
+ * Whether exec messages are allowed. Set by bwclient connect opts.allowExec.
9143
+ * Default false — exec messages are rejected unless explicitly opted in.
9144
+ * @private
9231
9145
  */
9232
- _chp.get = function(key) {
9233
- return bw._evaluatePath(this._state, key);
9234
- };
9146
+ bw._allowExec = false;
9235
9147
 
9236
9148
  /**
9237
- * Set a state value. Dot-path supported. Schedules re-render.
9238
- * @param {string} key - State key (dot-path)
9239
- * @param {*} value - New value
9240
- * @param {Object} [opts] - Options. `{sync: true}` for immediate flush.
9241
- */
9242
- _chp.set = function(key, value, opts) {
9243
- // Dot-path set
9244
- var parts = key.split('.');
9245
- var obj = this._state;
9246
- for (var i = 0; i < parts.length - 1; i++) {
9247
- if (!_is(obj[parts[i]], 'object')) {
9248
- if (bw.debug) _cw('bw.debug: set() auto-creating intermediate "' + parts[i] + '" in path "' + key + '"');
9249
- obj[parts[i]] = {};
9250
- }
9251
- obj = obj[parts[i]];
9252
- }
9253
- obj[parts[parts.length - 1]] = value;
9254
- // Mark top-level key dirty
9255
- this._dirtyKeys[parts[0]] = true;
9256
- if (this.mounted) {
9257
- if (opts && opts.sync) {
9258
- this._flush();
9259
- } else {
9260
- this._scheduleDirty();
9261
- }
9262
- }
9263
- };
9264
-
9265
- /**
9266
- * Get a shallow clone of the full state.
9267
- */
9268
- _chp.getState = function() {
9269
- var clone = {};
9270
- for (var k in this._state) {
9271
- if (_hop.call(this._state, k)) {
9272
- clone[k] = this._state[k];
9273
- }
9274
- }
9275
- return clone;
9276
- };
9277
-
9278
- /**
9279
- * Merge multiple state keys. Schedules re-render.
9280
- * @param {Object} updates - Key-value pairs to merge
9281
- * @param {Object} [opts] - Options. `{sync: true}` for immediate flush.
9282
- */
9283
- _chp.setState = function(updates, opts) {
9284
- for (var k in updates) {
9285
- if (_hop.call(updates, k)) {
9286
- this._state[k] = updates[k];
9287
- this._dirtyKeys[k] = true;
9288
- }
9289
- }
9290
- if (this.mounted) {
9291
- if (opts && opts.sync) {
9292
- this._flush();
9293
- } else {
9294
- this._scheduleDirty();
9295
- }
9296
- }
9297
- };
9298
-
9299
- /**
9300
- * Push a value onto an array in state. Clones the array.
9301
- */
9302
- _chp.push = function(key, val) {
9303
- var arr = this.get(key);
9304
- var newArr = _isA(arr) ? arr.slice() : [];
9305
- newArr.push(val);
9306
- this.set(key, newArr);
9307
- };
9308
-
9309
- /**
9310
- * Splice an array in state. Clones the array.
9311
- */
9312
- _chp.splice = function(key, start, deleteCount) {
9313
- var arr = this.get(key);
9314
- var newArr = _isA(arr) ? arr.slice() : [];
9315
- var args = [start, deleteCount].concat(Array.prototype.slice.call(arguments, 3));
9316
- Array.prototype.splice.apply(newArr, args);
9317
- this.set(key, newArr);
9318
- };
9319
-
9320
- // ── Scheduling ──
9321
-
9322
- _chp._scheduleDirty = function() {
9323
- if (!this._scheduled) {
9324
- this._scheduled = true;
9325
- bw._dirtyComponents.push(this);
9326
- bw._scheduleFlush();
9327
- }
9328
- };
9329
-
9330
- // ── Binding Compilation ──
9331
-
9332
- /**
9333
- * Walk the TACO tree and extract ${expr} bindings.
9334
- * Creates binding descriptors with refIds for targeted DOM updates.
9335
- * @private
9336
- */
9337
- _chp._compileBindings = function() {
9338
- this._bindings = [];
9339
- this._refCounter = 0;
9340
- var stateKeys = _keys(this._state);
9341
- var self = this;
9342
-
9343
- function walkTaco(taco, path) {
9344
- if (!_is(taco, 'object') || !taco.t) return taco;
9345
-
9346
- // Check content for bindings
9347
- if (_is(taco.c, 'string') && taco.c.indexOf('${') >= 0) {
9348
- var refId = 'bw_ref_' + self._refCounter++;
9349
- var parsed = bw._parseBindings(taco.c);
9350
- var deps = [];
9351
- for (var j = 0; j < parsed.length; j++) {
9352
- deps = deps.concat(bw._extractDeps(parsed[j].expr, stateKeys));
9353
- }
9354
- self._bindings.push({
9355
- expr: taco.c,
9356
- type: 'content',
9357
- refId: refId,
9358
- deps: deps,
9359
- template: taco.c
9360
- });
9361
- // Inject data-bw_ref on the TACO for createDOM to pick up
9362
- if (!taco.a) taco.a = {};
9363
- taco.a['data-bw_ref'] = refId;
9364
- }
9365
-
9366
- // Check attributes for bindings
9367
- if (taco.a) {
9368
- for (var attrName in taco.a) {
9369
- if (!_hop.call(taco.a, attrName)) continue;
9370
- if (attrName === 'data-bw_ref') continue;
9371
- var attrVal = taco.a[attrName];
9372
- if (_is(attrVal, 'string') && attrVal.indexOf('${') >= 0) {
9373
- var refId2 = 'bw_ref_' + self._refCounter++;
9374
- var parsed2 = bw._parseBindings(attrVal);
9375
- var deps2 = [];
9376
- for (var j2 = 0; j2 < parsed2.length; j2++) {
9377
- deps2 = deps2.concat(bw._extractDeps(parsed2[j2].expr, stateKeys));
9378
- }
9379
- self._bindings.push({
9380
- expr: attrVal,
9381
- type: 'attribute',
9382
- attrName: attrName,
9383
- refId: refId2,
9384
- deps: deps2,
9385
- template: attrVal
9386
- });
9387
- if (!taco.a) taco.a = {};
9388
- taco.a['data-bw_ref'] = taco.a['data-bw_ref'] || refId2;
9389
- // If multiple attribute bindings on same element, store additional marker
9390
- if (taco.a['data-bw_ref'] !== refId2) {
9391
- taco.a['data-bw_ref_' + attrName] = refId2;
9392
- }
9393
- }
9394
- }
9395
- }
9396
-
9397
- // Recurse into children
9398
- if (_isA(taco.c)) {
9399
- for (var i = 0; i < taco.c.length; i++) {
9400
- // Wrap string children with ${expr} in a span so patches target the span, not the parent
9401
- if (_is(taco.c[i], 'string') && taco.c[i].indexOf('${') >= 0) {
9402
- var mixedRefId = 'bw_ref_' + self._refCounter++;
9403
- var mixedParsed = bw._parseBindings(taco.c[i]);
9404
- var mixedDeps = [];
9405
- for (var mi = 0; mi < mixedParsed.length; mi++) {
9406
- mixedDeps = mixedDeps.concat(bw._extractDeps(mixedParsed[mi].expr, stateKeys));
9407
- }
9408
- self._bindings.push({
9409
- expr: taco.c[i],
9410
- type: 'content',
9411
- refId: mixedRefId,
9412
- deps: mixedDeps,
9413
- template: taco.c[i]
9414
- });
9415
- // Replace string with a span wrapper so textContent targets the span only
9416
- taco.c[i] = { t: 'span', a: { 'data-bw_ref': mixedRefId, style: 'display:contents' }, c: taco.c[i] };
9417
- }
9418
- if (_is(taco.c[i], 'object') && taco.c[i].t) {
9419
- walkTaco(taco.c[i], path.concat(i));
9420
- }
9421
- // Handle bw.when/bw.each markers
9422
- if (taco.c[i] && taco.c[i]._bwWhen) {
9423
- var whenRefId = 'bw_ref_' + self._refCounter++;
9424
- var whenDeps = bw._extractDeps(taco.c[i].expr.replace(/^\$\{|\}$/g, ''), stateKeys);
9425
- self._bindings.push({
9426
- expr: taco.c[i].expr,
9427
- type: 'structural',
9428
- subtype: 'when',
9429
- refId: whenRefId,
9430
- deps: whenDeps,
9431
- branches: taco.c[i].branches,
9432
- index: i,
9433
- parentPath: path
9434
- });
9435
- taco.c[i]._refId = whenRefId;
9436
- }
9437
- if (taco.c[i] && taco.c[i]._bwEach) {
9438
- var eachRefId = 'bw_ref_' + self._refCounter++;
9439
- var eachDeps = bw._extractDeps(taco.c[i].expr.replace(/^\$\{|\}$/g, ''), stateKeys);
9440
- self._bindings.push({
9441
- expr: taco.c[i].expr,
9442
- type: 'structural',
9443
- subtype: 'each',
9444
- refId: eachRefId,
9445
- deps: eachDeps,
9446
- factory: taco.c[i].factory,
9447
- index: i,
9448
- parentPath: path
9449
- });
9450
- taco.c[i]._refId = eachRefId;
9451
- }
9452
- }
9453
- } else if (_is(taco.c, 'object') && taco.c.t) {
9454
- walkTaco(taco.c, path.concat(0));
9455
- }
9456
-
9457
- return taco;
9458
- }
9459
-
9460
- walkTaco(this.taco, []);
9461
- };
9462
-
9463
- // ── DOM Reference Collection ──
9464
-
9465
- /**
9466
- * Build ref map from the live DOM after createDOM.
9467
- * @private
9468
- */
9469
- _chp._collectRefs = function() {
9470
- this._bw_refs = {};
9471
- if (!this.element) return;
9472
- var els = this.element.querySelectorAll('[data-bw_ref]');
9473
- for (var i = 0; i < els.length; i++) {
9474
- this._bw_refs[els[i].getAttribute('data-bw_ref')] = els[i];
9475
- }
9476
- // Also check root element
9477
- var rootRef = this.element.getAttribute && this.element.getAttribute('data-bw_ref');
9478
- if (rootRef) {
9479
- this._bw_refs[rootRef] = this.element;
9480
- }
9481
- };
9482
-
9483
- // ── Lifecycle ──
9484
-
9485
- /**
9486
- * Mount the component into a parent DOM element.
9487
- * Creates DOM, compiles bindings, registers actions, and calls lifecycle hooks.
9488
- * @param {Element} parentEl - DOM element to mount into
9489
- */
9490
- _chp.mount = function(parentEl) {
9491
- // willMount hook
9492
- if (this._hooks.willMount) this._hooks.willMount(this);
9493
-
9494
- // Save original TACO for re-renders (structural changes clone from this)
9495
- if (!this._originalTaco) {
9496
- this._originalTaco = this.taco;
9497
- }
9498
-
9499
- // Deep-clone TACO so binding annotations don't mutate original.
9500
- // Custom clone to preserve _bwWhen/_bwEach markers and their factory functions.
9501
- this.taco = this._deepCloneTaco(this._originalTaco);
9502
-
9503
- // Compile bindings (annotates TACO with data-bw_ref attributes)
9504
- this._compileBindings();
9505
-
9506
- // Prepare TACO: resolve initial binding values, evaluate when/each
9507
- this._prepareTaco(this.taco);
9508
-
9509
- // Register named actions in function registry
9510
- var self = this;
9511
- for (var actionName in this._actions) {
9512
- if (_hop.call(this._actions, actionName)) {
9513
- var registeredName = this._bwId + '_' + actionName;
9514
- (function(aName) {
9515
- bw.funcRegister(function(evt) {
9516
- self._actions[aName](self, evt);
9517
- }, registeredName);
9518
- })(actionName);
9519
- this._registeredActions.push(registeredName);
9520
- }
9521
- }
9522
-
9523
- // Wire action names in onclick etc. to dispatch strings
9524
- this._wireActions(this.taco);
9525
-
9526
- // Create DOM (strip o before createDOM to prevent double lifecycle)
9527
- var tacoForDOM = this._tacoForDOM(this.taco);
9528
- this.element = bw.createDOM(tacoForDOM);
9529
- this.element._bwComponentHandle = this;
9530
- this.element.setAttribute('data-bw_comp_id', this._bwId);
9531
-
9532
- // Restore o.render from original TACO (stripped by _tacoForDOM)
9533
- if (this.taco.o && this.taco.o.render) {
9534
- this.element._bw_render = this.taco.o.render;
9535
- }
9536
- if (this._userTag) {
9537
- this.element.classList.add(this._userTag);
9538
- }
9539
-
9540
- // Append to parent
9541
- parentEl.appendChild(this.element);
9542
-
9543
- // Collect refs from live DOM
9544
- this._collectRefs();
9545
-
9546
- // Resolve initial bindings and apply to DOM
9547
- this._resolveAndApplyAll();
9548
-
9549
- this.mounted = true;
9550
-
9551
- // Scan for child ComponentHandles and link parent/child (Bug #5)
9552
- var childEls = this.element.querySelectorAll('[data-bw_comp_id]');
9553
- for (var ci = 0; ci < childEls.length; ci++) {
9554
- var ch = childEls[ci]._bwComponentHandle;
9555
- if (ch && ch !== this && !ch._parent) {
9556
- ch._parent = this;
9557
- this._children.push(ch);
9558
- }
9559
- }
9560
-
9561
- // mounted hook (backward compat: fn.length === 2 wraps (el, state))
9562
- if (this._hooks.mounted) {
9563
- if (this._hooks.mounted.length === 2) {
9564
- this._hooks.mounted(this.element, this.getState());
9565
- } else {
9566
- this._hooks.mounted(this);
9567
- }
9568
- }
9569
-
9570
- // Invoke o.render on initial mount (if present)
9571
- if (this.element._bw_render) {
9572
- this.element._bw_render(this.element, this._state);
9573
- }
9574
- };
9575
-
9576
- /**
9577
- * Prepare TACO for initial render: resolve when/each markers.
9578
- * @private
9579
- */
9580
- _chp._prepareTaco = function(taco) {
9581
- if (!_is(taco, 'object')) return;
9582
-
9583
- if (_isA(taco.c)) {
9584
- for (var i = taco.c.length - 1; i >= 0; i--) {
9585
- var child = taco.c[i];
9586
- if (child && child._bwWhen) {
9587
- var exprStr = child.expr.replace(/^\$\{|\}$/g, '');
9588
- var val;
9589
- if (this._compile) {
9590
- try {
9591
- val = (new Function('state', 'with(state){return (' + exprStr + ');}'))(this._state);
9592
- } catch(e) { val = false; }
9593
- } else {
9594
- val = bw._evaluatePath(this._state, exprStr);
9595
- }
9596
- var branch = val ? child.branches[0] : (child.branches[1] || null);
9597
- if (branch) {
9598
- // Wrap in a container so we can track it
9599
- taco.c[i] = { t: 'span', a: { 'data-bw_when': child._refId, style: 'display:contents' }, c: branch };
9600
- } else {
9601
- taco.c[i] = { t: 'span', a: { 'data-bw_when': child._refId, style: 'display:contents' }, c: '' };
9602
- }
9603
- }
9604
- if (child && child._bwEach) {
9605
- var eachExprStr = child.expr.replace(/^\$\{|\}$/g, '');
9606
- var arr = bw._evaluatePath(this._state, eachExprStr);
9607
- var items = [];
9608
- if (_isA(arr)) {
9609
- for (var j = 0; j < arr.length; j++) {
9610
- items.push(child.factory(arr[j], j));
9611
- }
9612
- }
9613
- taco.c[i] = { t: 'span', a: { 'data-bw_each': child._refId, style: 'display:contents' }, c: items };
9614
- }
9615
- if (_is(taco.c[i], 'object') && taco.c[i].t) {
9616
- this._prepareTaco(taco.c[i]);
9617
- }
9618
- }
9619
- } else if (_is(taco.c, 'object') && taco.c.t) {
9620
- this._prepareTaco(taco.c);
9621
- }
9622
- };
9623
-
9624
- /**
9625
- * Wire action name strings (in onclick etc.) to dispatch function calls.
9626
- * @private
9627
- */
9628
- _chp._wireActions = function(taco) {
9629
- if (!_is(taco, 'object') || !taco.t) return;
9630
- if (taco.a) {
9631
- for (var key in taco.a) {
9632
- if (!_hop.call(taco.a, key)) continue;
9633
- if (key.startsWith('on') && _is(taco.a[key], 'string')) {
9634
- var actionName = taco.a[key];
9635
- if (actionName in this._actions) {
9636
- var registeredName = this._bwId + '_' + actionName;
9637
- // Replace string with actual function for createDOM event binding
9638
- (function(rName) {
9639
- taco.a[key] = function(evt) {
9640
- bw.funcGetById(rName)(evt);
9641
- };
9642
- })(registeredName);
9643
- }
9644
- }
9645
- }
9646
- }
9647
- if (_isA(taco.c)) {
9648
- for (var i = 0; i < taco.c.length; i++) {
9649
- this._wireActions(taco.c[i]);
9650
- }
9651
- } else if (_is(taco.c, 'object') && taco.c.t) {
9652
- this._wireActions(taco.c);
9653
- }
9654
- };
9655
-
9656
- /**
9657
- * Deep-clone a TACO tree, preserving _bwWhen/_bwEach markers and their factories.
9658
- * @private
9659
- */
9660
- _chp._deepCloneTaco = function(taco) {
9661
- if (taco == null) return taco;
9662
- // Preserve _bwWhen / _bwEach markers (contain functions)
9663
- if (taco._bwWhen) {
9664
- return { _bwWhen: true, expr: taco.expr, branches: [
9665
- this._deepCloneTaco(taco.branches[0]),
9666
- taco.branches[1] ? this._deepCloneTaco(taco.branches[1]) : null
9667
- ], _refId: taco._refId };
9668
- }
9669
- if (taco._bwEach) {
9670
- return { _bwEach: true, expr: taco.expr, factory: taco.factory, _refId: taco._refId };
9671
- }
9672
- if (!_is(taco, 'object') || !taco.t) return taco;
9673
- var result = { t: taco.t };
9674
- if (taco.a) {
9675
- result.a = {};
9676
- for (var k in taco.a) {
9677
- if (_hop.call(taco.a, k)) result.a[k] = taco.a[k];
9678
- }
9679
- }
9680
- if (taco.c != null) {
9681
- if (_isA(taco.c)) {
9682
- result.c = taco.c.map(function(child) { return this._deepCloneTaco(child); }.bind(this));
9683
- } else if (_is(taco.c, 'object')) {
9684
- result.c = this._deepCloneTaco(taco.c);
9685
- } else {
9686
- result.c = taco.c;
9687
- }
9688
- }
9689
- if (taco.o) result.o = taco.o; // Keep o reference (not deep-cloned; hooks are functions)
9690
- return result;
9691
- };
9692
-
9693
- /**
9694
- * Create a copy of TACO suitable for createDOM (strips o to prevent double lifecycle).
9695
- * @private
9696
- */
9697
- _chp._tacoForDOM = function(taco) {
9698
- if (!_is(taco, 'object') || !taco.t) return taco;
9699
- var result = { t: taco.t };
9700
- if (taco.a) result.a = taco.a;
9701
- if (taco.c != null) {
9702
- if (_isA(taco.c)) {
9703
- result.c = taco.c.map(function(child) { return this._tacoForDOM(child); }.bind(this));
9704
- } else if (_is(taco.c, 'object') && taco.c.t) {
9705
- result.c = this._tacoForDOM(taco.c);
9706
- } else {
9707
- result.c = taco.c;
9708
- }
9709
- }
9710
- // Intentionally strip o (no mounted/unmount/state/render on sub-elements)
9711
- if (taco.o && (taco.o.mounted || taco.o.render || taco.o.unmount)) {
9712
- _cw('bw: _tacoForDOM stripped o.mounted/render/unmount from child <' + taco.t +
9713
- '>. Use onclick attribute or bw.component() for child interactivity.');
9714
- }
9715
- return result;
9716
- };
9717
-
9718
- /**
9719
- * Unmount: remove from DOM, deactivate, preserve state for re-mount.
9720
- */
9721
- _chp.unmount = function() {
9722
- if (!this.mounted) return;
9723
-
9724
- // unmount hook
9725
- if (this._hooks.unmount) {
9726
- this._hooks.unmount(this);
9727
- }
9728
-
9729
- // Remove DOM event listeners
9730
- for (var i = 0; i < this._eventListeners.length; i++) {
9731
- var l = this._eventListeners[i];
9732
- if (this.element) {
9733
- this.element.removeEventListener(l.event, l.handler);
9734
- }
9735
- }
9736
- this._eventListeners = [];
9737
-
9738
- // Unsubscribe pub/sub
9739
- for (var j = 0; j < this._subs.length; j++) {
9740
- this._subs[j]();
9741
- }
9742
- this._subs = [];
9743
-
9744
- // Remove from DOM
9745
- if (this.element && this.element.parentNode) {
9746
- this.element.parentNode.removeChild(this.element);
9747
- }
9748
-
9749
- this.mounted = false;
9750
- // State preserved — can re-mount
9751
- };
9752
-
9753
- /**
9754
- * Destroy: unmount + clear state + unregister actions.
9755
- */
9756
- _chp.destroy = function() {
9757
- // willDestroy hook
9758
- if (this._hooks.willDestroy) {
9759
- this._hooks.willDestroy(this);
9760
- }
9761
-
9762
- // Cascade destroy to children depth-first (Bug #5)
9763
- for (var ci = this._children.length - 1; ci >= 0; ci--) {
9764
- this._children[ci].destroy();
9765
- }
9766
- this._children = [];
9767
- if (this._parent) {
9768
- var idx = this._parent._children.indexOf(this);
9769
- if (idx >= 0) this._parent._children.splice(idx, 1);
9770
- this._parent = null;
9771
- }
9772
-
9773
- this.unmount();
9774
-
9775
- // Unregister actions from function registry
9776
- for (var i = 0; i < this._registeredActions.length; i++) {
9777
- bw.funcUnregister(this._registeredActions[i]);
9778
- }
9779
- this._registeredActions = [];
9780
-
9781
- // Clear state
9782
- this._state = {};
9783
- this._bindings = [];
9784
- this._bw_refs = {};
9785
- this._prevValues = {};
9786
- this._dirtyKeys = {};
9787
- if (this.element) {
9788
- delete this.element._bwComponentHandle;
9789
- this.element = null;
9790
- }
9791
- };
9792
-
9793
- // ── Flush & Binding Resolution ──
9794
-
9795
- /**
9796
- * Flush dirty state: resolve changed bindings and apply to DOM.
9797
- * @private
9798
- */
9799
- _chp._flush = function() {
9800
- this._scheduled = false;
9801
- var changedKeys = _keys(this._dirtyKeys);
9802
- this._dirtyKeys = {};
9803
- if (changedKeys.length === 0 || !this.mounted) return;
9804
-
9805
- // Factory rebuild: if a BCCL factory exists and changed keys overlap factory props,
9806
- // rebuild the TACO from the factory with merged state (Bug #6)
9807
- if (this._factory) {
9808
- var rebuildNeeded = false;
9809
- for (var fi = 0; fi < changedKeys.length; fi++) {
9810
- if (_hop.call(this._factory.props, changedKeys[fi])) {
9811
- rebuildNeeded = true; break;
9812
- }
9813
- }
9814
- if (rebuildNeeded) {
9815
- var merged = {};
9816
- for (var mk in this._factory.props) if (_hop.call(this._factory.props, mk)) merged[mk] = this._factory.props[mk];
9817
- for (var sk in this._state) if (_hop.call(this._state, sk)) merged[sk] = this._state[sk];
9818
- this._factory.props = merged;
9819
- var newTaco = bw.make(this._factory.type, merged);
9820
- newTaco._bwFactory = this._factory;
9821
- this.taco = newTaco;
9822
- this._originalTaco = this._deepCloneTaco(newTaco);
9823
- this._render();
9824
- if (this._hooks.onUpdate) this._hooks.onUpdate(this, changedKeys);
9825
- return;
9826
- }
9827
- }
9828
-
9829
- // willUpdate hook
9830
- if (this._hooks.willUpdate) {
9831
- this._hooks.willUpdate(this, changedKeys);
9832
- }
9833
-
9834
- // Check if any structural bindings are affected
9835
- var needsFullRender = false;
9836
- for (var i = 0; i < this._bindings.length; i++) {
9837
- var b = this._bindings[i];
9838
- if (b.type === 'structural') {
9839
- for (var j = 0; j < b.deps.length; j++) {
9840
- if (changedKeys.indexOf(b.deps[j]) >= 0) {
9841
- needsFullRender = true;
9842
- break;
9843
- }
9844
- }
9845
- if (needsFullRender) break;
9846
- }
9847
- }
9848
-
9849
- if (needsFullRender) {
9850
- this._render();
9851
- } else {
9852
- var patches = this._resolveBindings(changedKeys);
9853
- this._applyPatches(patches);
9854
- }
9855
-
9856
- // onUpdate hook
9857
- if (this._hooks.onUpdate) {
9858
- this._hooks.onUpdate(this, changedKeys);
9859
- }
9860
- };
9861
-
9862
- /**
9863
- * Resolve bindings whose deps intersect with changedKeys.
9864
- * Returns list of patches to apply.
9865
- * @private
9866
- */
9867
- _chp._resolveBindings = function(changedKeys) {
9868
- var patches = [];
9869
- for (var i = 0; i < this._bindings.length; i++) {
9870
- var b = this._bindings[i];
9871
- if (b.type === 'structural') continue;
9872
-
9873
- // Check if any dep matches
9874
- var affected = false;
9875
- for (var j = 0; j < b.deps.length; j++) {
9876
- if (changedKeys.indexOf(b.deps[j]) >= 0) {
9877
- affected = true;
9878
- break;
9879
- }
9880
- }
9881
- if (!affected) continue;
9882
-
9883
- // Evaluate
9884
- var newVal = bw._resolveTemplate(b.template, this._state, this._compile);
9885
- var prevKey = b.refId + '_' + (b.attrName || 'content');
9886
- if (this._prevValues[prevKey] !== newVal) {
9887
- this._prevValues[prevKey] = newVal;
9888
- patches.push({
9889
- refId: b.refId,
9890
- type: b.type,
9891
- attrName: b.attrName,
9892
- value: newVal
9893
- });
9894
- }
9895
- }
9896
- return patches;
9897
- };
9898
-
9899
- /**
9900
- * Apply patches to DOM.
9901
- * @private
9902
- */
9903
- _chp._applyPatches = function(patches) {
9904
- for (var i = 0; i < patches.length; i++) {
9905
- var p = patches[i];
9906
- var el = this._bw_refs[p.refId];
9907
- if (!el) {
9908
- if (bw.debug) _cw('bw.debug: _applyPatches — ref "' + p.refId + '" not found in DOM');
9909
- continue;
9910
- }
9911
- if (p.type === 'content') {
9912
- el.textContent = p.value;
9913
- } else if (p.type === 'attribute') {
9914
- if (p.attrName === 'class') {
9915
- el.className = p.value;
9916
- } else {
9917
- el.setAttribute(p.attrName, p.value);
9918
- }
9919
- }
9920
- }
9921
- };
9922
-
9923
- /**
9924
- * Resolve all bindings and apply (used for initial render).
9925
- * @private
9926
- */
9927
- _chp._resolveAndApplyAll = function() {
9928
- var patches = [];
9929
- for (var i = 0; i < this._bindings.length; i++) {
9930
- var b = this._bindings[i];
9931
- if (b.type === 'structural') continue;
9932
-
9933
- var newVal = bw._resolveTemplate(b.template, this._state, this._compile);
9934
- var prevKey = b.refId + '_' + (b.attrName || 'content');
9935
- this._prevValues[prevKey] = newVal;
9936
- patches.push({
9937
- refId: b.refId,
9938
- type: b.type,
9939
- attrName: b.attrName,
9940
- value: newVal
9941
- });
9942
- }
9943
- this._applyPatches(patches);
9944
- };
9945
-
9946
- /**
9947
- * Full re-render for structural changes (when/each branch switches).
9948
- * @private
9949
- */
9950
- _chp._render = function() {
9951
- if (!this.element || !this.element.parentNode) return;
9952
- var parent = this.element.parentNode;
9953
- var nextSibling = this.element.nextSibling;
9954
-
9955
- // Remove old DOM
9956
- parent.removeChild(this.element);
9957
-
9958
- // Re-prepare TACO with current state (deep clone preserving functions)
9959
- this.taco = this._deepCloneTaco(this._originalTaco || this.taco);
9960
-
9961
- // Re-compile bindings and prepare
9962
- this._compileBindings();
9963
- this._prepareTaco(this.taco);
9964
- this._wireActions(this.taco);
9965
-
9966
- var tacoForDOM = this._tacoForDOM(this.taco);
9967
- this.element = bw.createDOM(tacoForDOM);
9968
- this.element._bwComponentHandle = this;
9969
- this.element.setAttribute('data-bw_comp_id', this._bwId);
9970
-
9971
- // Re-insert at same position
9972
- if (nextSibling) {
9973
- parent.insertBefore(this.element, nextSibling);
9974
- } else {
9975
- parent.appendChild(this.element);
9976
- }
9977
-
9978
- // Re-collect refs and apply all bindings
9979
- this._collectRefs();
9980
- this._resolveAndApplyAll();
9981
- };
9982
-
9983
- // ── Event & Pub/Sub Methods ──
9984
-
9985
- /**
9986
- * Add a DOM event listener on the component's root element.
9987
- * @param {string} event - Event name (e.g., 'click')
9988
- * @param {Function} handler - Event handler
9989
- */
9990
- _chp.on = function(event, handler) {
9991
- if (this.element) {
9992
- this.element.addEventListener(event, handler);
9993
- }
9994
- this._eventListeners.push({ event: event, handler: handler });
9995
- };
9996
-
9997
- /**
9998
- * Remove a DOM event listener.
9999
- * @param {string} event - Event name
10000
- * @param {Function} handler - Handler to remove
10001
- */
10002
- _chp.off = function(event, handler) {
10003
- if (this.element) {
10004
- this.element.removeEventListener(event, handler);
10005
- }
10006
- this._eventListeners = this._eventListeners.filter(function(l) {
10007
- return !(l.event === event && l.handler === handler);
10008
- });
10009
- };
10010
-
10011
- /**
10012
- * Subscribe to a pub/sub topic. Lifecycle-tied: auto-unsubs on destroy.
10013
- * @param {string} topic - Topic name
10014
- * @param {Function} handler - Handler function
10015
- * @returns {Function} Unsubscribe function
10016
- */
10017
- _chp.sub = function(topic, handler) {
10018
- var unsub = bw.sub(topic, handler);
10019
- this._subs.push(unsub);
10020
- return unsub;
10021
- };
10022
-
10023
- /**
10024
- * Call a named action.
10025
- * @param {string} name - Action name
10026
- * @param {...*} args - Arguments passed after comp
10027
- */
10028
- _chp.action = function(name) {
10029
- var fn = this._actions[name];
10030
- if (!fn) {
10031
- _cw('ComponentHandle.action: unknown action "' + name + '"');
10032
- return;
10033
- }
10034
- var args = [this].concat(Array.prototype.slice.call(arguments, 1));
10035
- return fn.apply(null, args);
10036
- };
10037
-
10038
- /**
10039
- * querySelector within the component's DOM.
10040
- * @param {string} sel - CSS selector
10041
- * @returns {Element|null}
10042
- */
10043
- _chp.select = function(sel) {
10044
- return this.element ? this.element.querySelector(sel) : null;
10045
- };
10046
-
10047
- /**
10048
- * querySelectorAll within the component's DOM.
10049
- * @param {string} sel - CSS selector
10050
- * @returns {Element[]}
10051
- */
10052
- _chp.selectAll = function(sel) {
10053
- if (!this.element) return [];
10054
- return Array.prototype.slice.call(this.element.querySelectorAll(sel));
10055
- };
10056
-
10057
- /**
10058
- * Tag this component with a user-defined ID for addressing via bw.message().
10059
- * The tag is added as a CSS class on the root element (DOM IS the registry).
10060
- * @param {string} tag - User-defined identifier (e.g. 'dashboard_prod_east')
10061
- * @returns {ComponentHandle} this (for chaining)
10062
- */
10063
- _chp.userTag = function(tag) {
10064
- this._userTag = tag;
10065
- if (this.element) {
10066
- this.element.classList.add(tag);
10067
- }
10068
- return this;
10069
- };
10070
-
10071
- // Expose ComponentHandle on bw (for testing and advanced use)
10072
- bw._ComponentHandle = ComponentHandle;
10073
-
10074
- // ===================================================================================
10075
- // Control Flow Helpers
10076
- // ===================================================================================
10077
-
10078
- /**
10079
- * Conditional rendering helper.
10080
- * Returns a marker object that ComponentHandle detects during binding compilation.
10081
- * In static contexts (bw.html with state), evaluates immediately.
10082
- *
10083
- * @param {string} expr - Expression string like '${loggedIn}'
10084
- * @param {Object} tacoTrue - TACO to render when truthy
10085
- * @param {Object} [tacoFalse] - TACO to render when falsy
10086
- * @returns {Object} Marker object with _bwWhen flag
10087
- * @category Component
10088
- */
10089
- bw.when = function(expr, tacoTrue, tacoFalse) {
10090
- return { _bwWhen: true, expr: expr, branches: [tacoTrue, tacoFalse || null] };
10091
- };
10092
-
10093
- /**
10094
- * List rendering helper.
10095
- * Returns a marker object that ComponentHandle detects during binding compilation.
10096
- *
10097
- * @param {string} expr - Expression string like '${items}'
10098
- * @param {Function} fn - Factory function(item, index) returning TACO
10099
- * @returns {Object} Marker object with _bwEach flag
10100
- * @category Component
10101
- */
10102
- bw.each = function(expr, fn) {
10103
- return { _bwEach: true, expr: expr, factory: fn };
10104
- };
10105
-
10106
- // ===================================================================================
10107
- // bw.component() — Factory for ComponentHandle
10108
- // ===================================================================================
10109
-
10110
- /**
10111
- * Create a ComponentHandle from a TACO definition.
10112
- * The returned handle has .get(), .set(), .mount(), .destroy(), etc.
10113
- *
10114
- * @param {Object} taco - TACO definition with {t, a, c, o}
10115
- * @returns {ComponentHandle} Reactive component handle
10116
- * @category Component
10117
- * @see bw.DOM
10118
- * @example
10119
- * var counter = bw.component({
10120
- * t: 'div', c: [{ t: 'h3', c: 'Count: ${count}' }],
10121
- * o: { state: { count: 0 } }
10122
- * });
10123
- * bw.DOM('#app', counter);
10124
- * counter.set('count', 42); // DOM auto-updates
10125
- */
10126
- bw.component = function(taco) {
10127
- return new ComponentHandle(taco);
10128
- };
10129
-
10130
- // ===================================================================================
10131
- // bw.message() — SendMessage() for the web
10132
- // ===================================================================================
10133
-
10134
- /**
10135
- * Dispatch a message to a component by UUID or user tag.
10136
- * Finds the component's DOM element, looks up its ComponentHandle,
10137
- * and calls the named method. This is the bitwrench equivalent of
10138
- * Win32 SendMessage(hwnd, msg, wParam, lParam).
10139
- *
10140
- * @param {string} target - Component UUID (bw_uuid_*), comp ID (data-bw_comp_id), or user tag (CSS class)
10141
- * @param {string} action - Method name to call on the component
10142
- * @param {*} data - Data to pass to the method
10143
- * @returns {boolean} True if message was dispatched successfully
10144
- * @category Component
10145
- * @example
10146
- * // Tag a component
10147
- * myDash.userTag('dashboard_prod');
10148
- * // Dispatch locally
10149
- * bw.message('dashboard_prod', 'addAlert', { severity: 'warning', text: 'CPU spike' });
10150
- * // Or from SSE handler:
10151
- * es.onmessage = function(e) {
10152
- * var msg = JSON.parse(e.data);
10153
- * bw.message(msg.target, msg.action, msg.data);
10154
- * };
10155
- */
10156
- bw.message = function(target, action, data) {
10157
- // Try bw._el() first (handles UUID class, nodeMap cache, getElementById)
10158
- var el = bw._el(target);
10159
- // Then try data-bw_comp_id attribute
10160
- if (!el || !el._bwComponentHandle) {
10161
- el = bw.$('[data-bw_comp_id="' + target + '"]')[0];
10162
- }
10163
- // Then try CSS class (user tag)
10164
- if (!el || !el._bwComponentHandle) {
10165
- el = bw.$('.' + target)[0];
10166
- }
10167
- if (!el || !el._bwComponentHandle) return false;
10168
- var comp = el._bwComponentHandle;
10169
- if (!_is(comp[action], 'function')) {
10170
- _cw('bw.message: unknown action "' + action + '" on component ' + target);
10171
- return false;
10172
- }
10173
- comp[action](data);
10174
- return true;
10175
- };
10176
-
10177
- // ===================================================================================
10178
- // bw.apply() / bw.parseJSONFlex() — Server-driven UI protocol
10179
- // ===================================================================================
10180
-
10181
- /**
10182
- * Registry of named functions sent via register messages.
10183
- * Populated by bw.apply({ type: 'register', name, body }).
10184
- * Invoked by bw.apply({ type: 'call', name, args }).
10185
- * @private
10186
- */
10187
- bw._clientFunctions = {};
10188
-
10189
- /**
10190
- * Whether exec messages are allowed. Set by bwclient connect opts.allowExec.
10191
- * Default false — exec messages are rejected unless explicitly opted in.
10192
- * @private
10193
- */
10194
- bw._allowExec = false;
10195
-
10196
- /**
10197
- * Parse a bwserve protocol message string, supporting both strict JSON
10198
- * and r-prefixed relaxed JSON (single-quoted strings, trailing commas).
10199
- *
10200
- * The r-prefix format is designed for C/C++ string literals where
10201
- * double-quote escaping is painful. The parser is a state machine
10202
- * that walks character by character — not a regex replace.
10203
- *
10204
- * Escaping: apostrophes inside single-quoted values must be escaped
10205
- * with backslash: r{'name':'Barry\'s room'}
10206
- *
10207
- * @param {string} str - JSON or r-prefixed relaxed JSON string
10208
- * @returns {Object} Parsed message object
10209
- * @throws {SyntaxError} If the string is not valid JSON or relaxed JSON
10210
- * @category Core
9149
+ * Parse a bwserve protocol message string, supporting both strict JSON
9150
+ * and r-prefixed relaxed JSON (single-quoted strings, trailing commas).
9151
+ *
9152
+ * The r-prefix format is designed for C/C++ string literals where
9153
+ * double-quote escaping is painful. The parser is a state machine
9154
+ * that walks character by character — not a regex replace.
9155
+ *
9156
+ * Escaping: apostrophes inside single-quoted values must be escaped
9157
+ * with backslash: r{'name':'Barry\'s room'}
9158
+ *
9159
+ * @param {string} str - JSON or r-prefixed relaxed JSON string
9160
+ * @returns {Object} Parsed message object
9161
+ * @throws {SyntaxError} If the string is not valid JSON or relaxed JSON
9162
+ * @category Core
10211
9163
  */
10212
9164
  bw.parseJSONFlex = function(str) {
10213
9165
  str = (str || '').trim();
@@ -10396,132 +9348,29 @@
10396
9348
  // ===================================================================================
10397
9349
 
10398
9350
  /**
10399
- * Inspect a component's state, bindings, methods, and metadata.
10400
- * Works with DOM elements, CSS selectors, or ComponentHandle objects.
10401
- * Returns the ComponentHandle for console chaining.
9351
+ * Inspect a DOM element's bitwrench state, handle methods, and metadata.
9352
+ * Works with DOM elements or CSS selectors.
10402
9353
  *
10403
- * @param {string|Element|ComponentHandle} target - Selector, element, or handle
10404
- * @returns {ComponentHandle|null} The component handle, or null if not found
9354
+ * @param {string|Element} target - Selector or DOM element
9355
+ * @returns {Element|null} The element, or null if not found
10405
9356
  * @category Component
10406
9357
  * @example
10407
- * // In browser console, click element in Elements panel then:
9358
+ * bw.inspect('#my-carousel');
10408
9359
  * bw.inspect($0);
10409
- * // Or by selector:
10410
- * var h = bw.inspect('#my-dashboard');
10411
- * h.set('count', 99); // chain from returned handle
10412
9360
  */
10413
9361
  bw.inspect = function(target) {
10414
- var el = target;
10415
- var comp;
10416
- if (target && target._bwComponent === true) {
10417
- el = target.element;
10418
- comp = target;
10419
- } else {
10420
- if (_is(target, 'string')) {
10421
- el = bw.$(target)[0];
10422
- }
10423
- if (!el) {
10424
- _cw('bw.inspect: element not found');
10425
- return null;
10426
- }
10427
- comp = el._bwComponentHandle;
10428
- }
10429
- if (!comp) {
10430
- _cl('bw.inspect: no ComponentHandle on this element');
10431
- _cl(' Tag:', el.tagName);
10432
- _cl(' Classes:', el.className);
10433
- _cl(' _bw_state:', el._bw_state || '(none)');
10434
- return null;
10435
- }
10436
- var deps = comp._bindings.reduce(function(s, b) {
10437
- return s.concat(b.deps || []);
10438
- }, []).filter(function(v, i, a) { return a.indexOf(v) === i; });
10439
- console.group('Component: ' + comp._bwId);
10440
- _cl('State:', comp._state);
10441
- _cl('Bindings:', comp._bindings.length, '(deps:', deps, ')');
10442
- _cl('Methods:', _keys(comp._methods));
10443
- _cl('Actions:', _keys(comp._actions));
10444
- _cl('User tag:', comp._userTag || '(none)');
10445
- _cl('Mounted:', comp.mounted);
10446
- _cl('Element:', comp.element);
9362
+ var el = _is(target, 'string') ? bw.$(target)[0] : target;
9363
+ if (!el) { _cw('bw.inspect: element not found'); return null; }
9364
+ console.group('Element: ' + (bw.getUUID(el) || el.id || el.tagName));
9365
+ _cl('State:', el._bw_state || '(none)');
9366
+ _cl('Handle:', el.bw ? _keys(el.bw) : '(none)');
9367
+ _cl('Classes:', el.className);
9368
+ _cl('Refs:', el._bw_refs || '(none)');
10447
9369
  console.groupEnd();
10448
- return comp;
9370
+ return el;
10449
9371
  };
10450
9372
 
10451
- // ===================================================================================
10452
- // bw.compile() — Pre-compile TACO into optimized factory
10453
- // ===================================================================================
10454
-
10455
- /**
10456
- * Pre-compile a TACO definition into a factory function.
10457
- * The factory produces ComponentHandles with pre-compiled binding evaluators.
10458
- *
10459
- * Phase 1: validates API surface. Template cloning optimization deferred.
10460
- *
10461
- * @param {Object} taco - TACO definition
10462
- * @returns {Function} Factory function(initialState?) → ComponentHandle
10463
- * @category Component
10464
- */
10465
- bw.compile = function(taco) {
10466
- // Pre-extract all binding expressions
10467
- var precompiled = [];
10468
- function walkExpressions(node) {
10469
- if (!_is(node, 'object')) return;
10470
- if (_is(node.c, 'string') && node.c.indexOf('${') >= 0) {
10471
- var parsed = bw._parseBindings(node.c);
10472
- for (var i = 0; i < parsed.length; i++) {
10473
- try {
10474
- precompiled.push({
10475
- expr: parsed[i].expr,
10476
- fn: new Function('state', 'with(state){return (' + parsed[i].expr + ');}')
10477
- });
10478
- } catch(e) {
10479
- precompiled.push({ expr: parsed[i].expr, fn: function() { return ''; } });
10480
- }
10481
- }
10482
- }
10483
- if (node.a) {
10484
- for (var key in node.a) {
10485
- if (_hop.call(node.a, key)) {
10486
- var v = node.a[key];
10487
- if (_is(v, 'string') && v.indexOf('${') >= 0) {
10488
- var parsed2 = bw._parseBindings(v);
10489
- for (var j = 0; j < parsed2.length; j++) {
10490
- try {
10491
- precompiled.push({
10492
- expr: parsed2[j].expr,
10493
- fn: new Function('state', 'with(state){return (' + parsed2[j].expr + ');}')
10494
- });
10495
- } catch(e2) {
10496
- precompiled.push({ expr: parsed2[j].expr, fn: function() { return ''; } });
10497
- }
10498
- }
10499
- }
10500
- }
10501
- }
10502
- }
10503
- if (_isA(node.c)) {
10504
- for (var k = 0; k < node.c.length; k++) walkExpressions(node.c[k]);
10505
- } else if (_is(node.c, 'object') && node.c.t) {
10506
- walkExpressions(node.c);
10507
- }
10508
- }
10509
- walkExpressions(taco);
10510
-
10511
- return function(initialState) {
10512
- var handle = new ComponentHandle(taco);
10513
- handle._compile = true;
10514
- handle._precompiledBindings = precompiled;
10515
- if (initialState) {
10516
- for (var k in initialState) {
10517
- if (_hop.call(initialState, k)) {
10518
- handle._state[k] = initialState[k];
10519
- }
10520
- }
10521
- }
10522
- return handle;
10523
- };
10524
- };
9373
+ bw.compile = function() { throw new Error('bw.compile() removed in v2.0.19. Use o.handle/o.slots on TACO options instead.'); };
10525
9374
 
10526
9375
  /**
10527
9376
  * Generate CSS from JavaScript objects.
@@ -11747,8 +10596,8 @@
11747
10596
  };
11748
10597
  }
11749
10598
 
11750
- // Generate unique ID if not provided
11751
- const componentId = taco.o?.id || bw.uuid();
10599
+ // Generate unique UUID class if not provided
10600
+ const componentId = taco.o?.id || bw.uuid('uuid');
11752
10601
 
11753
10602
  // Create DOM element
11754
10603
  let domElement;
@@ -11763,9 +10612,10 @@
11763
10612
  };
11764
10613
  }
11765
10614
 
11766
- // Add component ID to element
11767
- domElement.setAttribute('data-bw_id', componentId);
11768
-
10615
+ // Add component ID as class + lifecycle marker
10616
+ domElement.classList.add(componentId);
10617
+ domElement.classList.add(_BW_LC);
10618
+
11769
10619
  // Insert into DOM based on position
11770
10620
  try {
11771
10621
  switch(position) {
@@ -11839,7 +10689,8 @@
11839
10689
 
11840
10690
  // Re-render
11841
10691
  const newElement = bw.createDOM(this._taco);
11842
- newElement.setAttribute('data-bw_id', componentId);
10692
+ newElement.classList.add(componentId);
10693
+ newElement.classList.add(_BW_LC);
11843
10694
 
11844
10695
  // Replace in DOM
11845
10696
  parent.replaceChild(newElement, this.element);
@@ -12026,13 +10877,12 @@
12026
10877
  // Variant class helper: bw.variantClass('primary') → 'bw_primary'
12027
10878
  bw.variantClass = variantClass;
12028
10879
 
12029
- // Create functions that return handles (plain renderComponent, no Handle overlay)
10880
+ // Create functions that return DOM elements (createCard, createTable, etc.)
12030
10881
  Object.entries(components).forEach(([name, fn]) => {
12031
10882
  if (name.startsWith('make')) {
12032
- const createName = 'create' + name.substring(4); // createCard, createTable, etc.
10883
+ const createName = 'create' + name.substring(4);
12033
10884
  bw[createName] = function(props) {
12034
- const taco = fn(props);
12035
- return bw.renderComponent(taco);
10885
+ return bw.createDOM(fn(props));
12036
10886
  };
12037
10887
  }
12038
10888
  });