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,18 +1,18 @@
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
  /**
3
3
  * Auto-generated version file from package.json
4
4
  * DO NOT EDIT DIRECTLY - Use npm run generate-version
5
5
  */
6
6
 
7
7
  const VERSION_INFO = {
8
- version: '2.0.18',
8
+ version: '2.0.20',
9
9
  name: 'bitwrench',
10
10
  description: 'A library for javascript UI functions.',
11
11
  license: 'BSD-2-Clause',
12
12
  homepage: 'https://deftio.github.com/bitwrench/pages',
13
13
  repository: 'git+https://github.com/deftio/bitwrench.git',
14
14
  author: 'manu a. chatterjee <deftio@deftio.com> (https://deftio.com/)',
15
- buildDate: '2026-03-17T00:50:09.505Z'
15
+ buildDate: '2026-03-23T05:19:31.951Z'
16
16
  };
17
17
 
18
18
  /**
@@ -2169,10 +2169,10 @@ var structuralRules = {
2169
2169
  'position': 'relative', 'display': 'flex', 'flex-direction': 'column', 'pointer-events': 'auto',
2170
2170
  'background-clip': 'padding-box', 'border': '1px solid transparent', 'outline': '0'
2171
2171
  },
2172
- '.bw_modal_header': { 'display': 'flex', 'align-items': 'center', 'justify-content': 'space-between' },
2172
+ '.bw_modal_header': { 'display': 'flex', 'align-items': 'center', 'justify-content': 'space-between', 'padding': '1rem 1.25rem', 'border-bottom': '1px solid transparent' },
2173
2173
  '.bw_modal_title': { 'margin': '0', 'font-size': '1.25rem', 'font-weight': '600', 'line-height': '1.3' },
2174
- '.bw_modal_body': { 'position': 'relative', 'flex': '1 1 auto' },
2175
- '.bw_modal_footer': { 'display': 'flex', 'flex-wrap': 'wrap', 'align-items': 'center', 'justify-content': 'flex-end', 'gap': '0.5rem' }
2174
+ '.bw_modal_body': { 'position': 'relative', 'flex': '1 1 auto', 'padding': '1rem 1.25rem' },
2175
+ '.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' }
2176
2176
  },
2177
2177
 
2178
2178
  // ---- Toast ----
@@ -2193,8 +2193,8 @@ var structuralRules = {
2193
2193
  },
2194
2194
  '.bw_toast.bw_toast_show': { 'opacity': '1', 'transform': 'translateY(0)' },
2195
2195
  '.bw_toast.bw_toast_hiding': { 'opacity': '0' },
2196
- '.bw_toast_header': { 'display': 'flex', 'align-items': 'center', 'justify-content': 'space-between', 'font-size': '0.875rem' },
2197
- '.bw_toast_body': { 'font-size': '0.9375rem' }
2196
+ '.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' },
2197
+ '.bw_toast_body': { 'padding': '0.5rem 0.75rem', 'font-size': '0.9375rem' }
2198
2198
  },
2199
2199
 
2200
2200
  // ---- Dropdown ----
@@ -2208,15 +2208,15 @@ var structuralRules = {
2208
2208
  '.bw_dropdown_menu': {
2209
2209
  'position': 'absolute', 'top': '100%', 'left': '0', 'z-index': '1000', 'display': 'block',
2210
2210
  'min-width': '10rem', 'padding': '0.5rem 0', 'margin': '0.125rem 0 0',
2211
- 'background-clip': 'padding-box',
2211
+ 'background-clip': 'padding-box', 'border': '1px solid transparent',
2212
2212
  'opacity': '0', 'visibility': 'hidden', 'pointer-events': 'none'
2213
2213
  },
2214
2214
  '.bw_dropdown_menu.bw_dropdown_show': { 'opacity': '1', 'visibility': 'visible', 'pointer-events': 'auto' },
2215
2215
  '.bw_dropdown_menu_end': { 'left': 'auto', 'right': '0' },
2216
2216
  '.bw_dropdown_item': {
2217
- 'display': 'block', 'width': '100%', 'clear': 'both',
2217
+ 'display': 'block', 'width': '100%', 'padding': '0.4rem 1rem', 'clear': 'both',
2218
2218
  'font-weight': '400', 'text-align': 'inherit', 'text-decoration': 'none', 'white-space': 'nowrap',
2219
- 'background-color': 'transparent', 'border': '0', 'font-size': '0.9375rem'
2219
+ 'background-color': 'transparent', 'border': '0', 'font-size': '0.9375rem', 'cursor': 'pointer'
2220
2220
  },
2221
2221
  '.bw_dropdown_item:focus-visible': { 'outline': '2px solid currentColor', 'outline-offset': '-2px' },
2222
2222
  '.bw_dropdown_divider': { 'height': '0', 'margin': '0.5rem 0', 'overflow': 'hidden', 'opacity': '1' }
@@ -2538,6 +2538,33 @@ function generateUtilityRules() {
2538
2538
  rules['.bw_text_left'] = { 'text-align': 'left' };
2539
2539
  rules['.bw_text_right'] = { 'text-align': 'right' };
2540
2540
  rules['.bw_text_center'] = { 'text-align': 'center' };
2541
+ rules['.bw_text_justify'] = { 'text-align': 'justify' };
2542
+
2543
+ // Font weight
2544
+ rules['.bw_fw_bold'] = { 'font-weight': '700' };
2545
+ rules['.bw_fw_semibold'] = { 'font-weight': '600' };
2546
+ rules['.bw_fw_normal'] = { 'font-weight': '400' };
2547
+ rules['.bw_fw_light'] = { 'font-weight': '300' };
2548
+
2549
+ // Font style
2550
+ rules['.bw_fst_italic'] = { 'font-style': 'italic' };
2551
+ rules['.bw_fst_normal'] = { 'font-style': 'normal' };
2552
+
2553
+ // Text decoration
2554
+ rules['.bw_text_underline'] = { 'text-decoration': 'underline' };
2555
+ rules['.bw_text_line_through'] = { 'text-decoration': 'line-through' };
2556
+ rules['.bw_text_decoration_none'] = { 'text-decoration': 'none' };
2557
+
2558
+ // Text transform
2559
+ rules['.bw_text_uppercase'] = { 'text-transform': 'uppercase' };
2560
+ rules['.bw_text_lowercase'] = { 'text-transform': 'lowercase' };
2561
+ rules['.bw_text_capitalize'] = { 'text-transform': 'capitalize' };
2562
+
2563
+ // Font size
2564
+ rules['.bw_fs_sm'] = { 'font-size': '0.875rem' };
2565
+ rules['.bw_fs_base'] = { 'font-size': '1rem' };
2566
+ rules['.bw_fs_lg'] = { 'font-size': '1.25rem' };
2567
+ rules['.bw_fs_xl'] = { 'font-size': '1.5rem' };
2541
2568
 
2542
2569
  // Flexbox
2543
2570
  var jc = { start: 'flex-start', end: 'flex-end', center: 'center', between: 'space-between', around: 'space-around' };
@@ -3604,7 +3631,12 @@ function makeCard(props = {}) {
3604
3631
  },
3605
3632
  o: {
3606
3633
  type: 'card',
3607
- state: props.state || {}
3634
+ state: props.state || {},
3635
+ slots: {
3636
+ title: '.bw_card_title',
3637
+ content: '.bw_card_body',
3638
+ footer: '.bw_card_footer'
3639
+ }
3608
3640
  }
3609
3641
  };
3610
3642
  }
@@ -3615,7 +3647,12 @@ function makeCard(props = {}) {
3615
3647
  c: cardContent,
3616
3648
  o: {
3617
3649
  type: 'card',
3618
- state: props.state || {}
3650
+ state: props.state || {},
3651
+ slots: {
3652
+ title: '.bw_card_title',
3653
+ content: '.bw_card_body',
3654
+ footer: '.bw_card_footer'
3655
+ }
3619
3656
  }
3620
3657
  };
3621
3658
  }
@@ -3933,6 +3970,24 @@ function makeTabs(props = {}) {
3933
3970
  }
3934
3971
  });
3935
3972
 
3973
+ // Shared tab switching logic
3974
+ function switchTab(el, index) {
3975
+ var allTabs = el.querySelectorAll('.bw_nav_link');
3976
+ var allPanes = el.querySelectorAll('.bw_tab_pane');
3977
+ if (index < 0 || index >= allTabs.length) return;
3978
+ allTabs.forEach(function(t) {
3979
+ t.classList.remove('active');
3980
+ t.setAttribute('aria-selected', 'false');
3981
+ t.setAttribute('tabindex', '-1');
3982
+ });
3983
+ allPanes.forEach(function(p) { p.classList.remove('active'); });
3984
+ allTabs[index].classList.add('active');
3985
+ allTabs[index].setAttribute('aria-selected', 'true');
3986
+ allTabs[index].setAttribute('tabindex', '0');
3987
+ allPanes[index].classList.add('active');
3988
+ if (el._bw_state) el._bw_state.activeIndex = index;
3989
+ }
3990
+
3936
3991
  return {
3937
3992
  t: 'div',
3938
3993
  a: { class: 'bw_tabs' },
@@ -3951,24 +4006,8 @@ function makeTabs(props = {}) {
3951
4006
  role: 'tab',
3952
4007
  tabindex: index === actualActiveIndex ? '0' : '-1',
3953
4008
  'aria-selected': index === actualActiveIndex ? 'true' : 'false',
3954
- 'data-tab-index': index,
3955
4009
  onclick: (e) => {
3956
- const tabsContainer = e.target.closest('.bw_tabs');
3957
- const allTabs = tabsContainer.querySelectorAll('.bw_nav_link');
3958
- const allPanes = tabsContainer.querySelectorAll('.bw_tab_pane');
3959
-
3960
- allTabs.forEach(t => {
3961
- t.classList.remove('active');
3962
- t.setAttribute('aria-selected', 'false');
3963
- t.setAttribute('tabindex', '-1');
3964
- });
3965
- allPanes.forEach(p => p.classList.remove('active'));
3966
-
3967
- e.target.classList.add('active');
3968
- e.target.setAttribute('aria-selected', 'true');
3969
- e.target.setAttribute('tabindex', '0');
3970
- const targetIndex = parseInt(e.target.getAttribute('data-tab-index'));
3971
- allPanes[targetIndex].classList.add('active');
4010
+ switchTab(e.target.closest('.bw_tabs'), index);
3972
4011
  }
3973
4012
  },
3974
4013
  c: tab.label
@@ -3991,6 +4030,10 @@ function makeTabs(props = {}) {
3991
4030
  o: {
3992
4031
  type: 'tabs',
3993
4032
  state: { activeIndex: actualActiveIndex },
4033
+ handle: {
4034
+ setActiveTab: switchTab,
4035
+ getActiveTab: function(el) { return (el._bw_state && el._bw_state.activeIndex) || 0; }
4036
+ },
3994
4037
  mounted: function(el) {
3995
4038
  var tablist = el.querySelector('[role="tablist"]');
3996
4039
  if (!tablist) return;
@@ -4076,7 +4119,13 @@ function makeAlert(props = {}) {
4076
4119
  },
4077
4120
  c: '×'
4078
4121
  }
4079
- ].filter(Boolean)
4122
+ ].filter(Boolean),
4123
+ o: {
4124
+ type: 'alert',
4125
+ handle: {
4126
+ dismiss: function(el) { if (el && el.parentNode) el.parentNode.removeChild(el); }
4127
+ }
4128
+ }
4080
4129
  };
4081
4130
  }
4082
4131
 
@@ -4174,6 +4223,24 @@ function makeProgress(props = {}) {
4174
4223
  'aria-valuemax': max
4175
4224
  },
4176
4225
  c: label || `${percentage}%`
4226
+ },
4227
+ o: {
4228
+ type: 'progress',
4229
+ handle: {
4230
+ setValue: function(el, n) {
4231
+ var bar = el.querySelector('.bw_progress_bar');
4232
+ if (!bar) return;
4233
+ var maxVal = parseInt(bar.getAttribute('aria-valuemax')) || 100;
4234
+ var pct = Math.round((n / maxVal) * 100);
4235
+ bar.style.width = pct + '%';
4236
+ bar.setAttribute('aria-valuenow', n);
4237
+ bar.textContent = pct + '%';
4238
+ },
4239
+ getValue: function(el) {
4240
+ var bar = el.querySelector('.bw_progress_bar');
4241
+ return bar ? parseInt(bar.getAttribute('aria-valuenow')) || 0 : 0;
4242
+ }
4243
+ }
4177
4244
  }
4178
4245
  };
4179
4246
  }
@@ -5183,6 +5250,26 @@ function makePagination(props = {}) {
5183
5250
  class: `bw_pagination ${size ? 'bw_pagination_' + size : ''} ${className}`.trim()
5184
5251
  },
5185
5252
  c: items
5253
+ },
5254
+ o: {
5255
+ type: 'pagination',
5256
+ state: { currentPage: currentPage, pages: pages },
5257
+ handle: {
5258
+ setPage: function(el, n) {
5259
+ if (n < 1 || n > pages) return;
5260
+ var allItems = el.querySelectorAll('.bw_page_item');
5261
+ for (var i = 0; i < allItems.length; i++) {
5262
+ allItems[i].classList.remove('bw_active');
5263
+ }
5264
+ // +1 offset: first item is prev arrow
5265
+ if (allItems[n]) allItems[n].classList.add('bw_active');
5266
+ if (el._bw_state) el._bw_state.currentPage = n;
5267
+ if (onPageChange) onPageChange(n);
5268
+ },
5269
+ getPage: function(el) {
5270
+ return (el._bw_state && el._bw_state.currentPage) || 1;
5271
+ }
5272
+ }
5186
5273
  }
5187
5274
  };
5188
5275
  }
@@ -5331,7 +5418,6 @@ function makeAccordion(props = {}) {
5331
5418
  class: `bw_accordion_button ${item.open ? '' : 'bw_collapsed'}`.trim(),
5332
5419
  type: 'button',
5333
5420
  'aria-expanded': item.open ? 'true' : 'false',
5334
- 'data-accordion-index': index,
5335
5421
  onclick: function(e) {
5336
5422
  var btn = e.target.closest('.bw_accordion_button');
5337
5423
  var accordionEl = btn.closest('.bw_accordion');
@@ -5406,7 +5492,43 @@ function makeAccordion(props = {}) {
5406
5492
  }),
5407
5493
  o: {
5408
5494
  type: 'accordion',
5409
- state: { multiOpen: multiOpen }
5495
+ state: { multiOpen: multiOpen },
5496
+ handle: {
5497
+ toggle: function(el, index) {
5498
+ var items = el.querySelectorAll('.bw_accordion_item');
5499
+ if (index < 0 || index >= items.length) return;
5500
+ var btn = items[index].querySelector('.bw_accordion_button');
5501
+ if (btn) btn.click();
5502
+ },
5503
+ openAll: function(el) {
5504
+ var items = el.querySelectorAll('.bw_accordion_item');
5505
+ for (var i = 0; i < items.length; i++) {
5506
+ var collapse = items[i].querySelector('.bw_accordion_collapse');
5507
+ var btn = items[i].querySelector('.bw_accordion_button');
5508
+ if (!collapse.classList.contains('bw_collapse_show')) {
5509
+ collapse.classList.add('bw_collapse_show');
5510
+ collapse.style.maxHeight = 'none';
5511
+ btn.classList.remove('bw_collapsed');
5512
+ btn.setAttribute('aria-expanded', 'true');
5513
+ }
5514
+ }
5515
+ },
5516
+ closeAll: function(el) {
5517
+ var items = el.querySelectorAll('.bw_accordion_item');
5518
+ for (var i = 0; i < items.length; i++) {
5519
+ var collapse = items[i].querySelector('.bw_accordion_collapse');
5520
+ var btn = items[i].querySelector('.bw_accordion_button');
5521
+ if (collapse.classList.contains('bw_collapse_show')) {
5522
+ collapse.style.maxHeight = collapse.scrollHeight + 'px';
5523
+ collapse.offsetHeight;
5524
+ collapse.style.maxHeight = '0px';
5525
+ collapse.classList.remove('bw_collapse_show');
5526
+ btn.classList.add('bw_collapsed');
5527
+ btn.setAttribute('aria-expanded', 'false');
5528
+ }
5529
+ }
5530
+ }
5531
+ }
5410
5532
  }
5411
5533
  };
5412
5534
  }
@@ -5496,6 +5618,14 @@ function makeModal(props = {}) {
5496
5618
  },
5497
5619
  o: {
5498
5620
  type: 'modal',
5621
+ handle: {
5622
+ open: function(el) {
5623
+ el.classList.add('bw_modal_show');
5624
+ el.style.display = 'flex';
5625
+ document.body.style.overflow = 'hidden';
5626
+ },
5627
+ close: function(el) { closeModal(el); }
5628
+ },
5499
5629
  mounted: function(el) {
5500
5630
  // Click backdrop to close
5501
5631
  el.addEventListener('click', function(e) {
@@ -5554,9 +5684,8 @@ function makeToast(props = {}) {
5554
5684
  return {
5555
5685
  t: 'div',
5556
5686
  a: {
5557
- class: `bw_toast ${variantClass(variant)} ${className}`.trim(),
5558
- role: 'alert',
5559
- 'data-position': position
5687
+ class: `bw_toast ${variantClass(variant)} bw_toast_${position.replace(/-/g, '_')} ${className}`.trim(),
5688
+ role: 'alert'
5560
5689
  },
5561
5690
  c: [
5562
5691
  (title) && {
@@ -5590,6 +5719,12 @@ function makeToast(props = {}) {
5590
5719
  ].filter(Boolean),
5591
5720
  o: {
5592
5721
  type: 'toast',
5722
+ handle: {
5723
+ dismiss: function(el) {
5724
+ el.classList.add('bw_toast_hiding');
5725
+ setTimeout(function() { if (el.parentNode) el.parentNode.removeChild(el); }, 300);
5726
+ }
5727
+ },
5593
5728
  mounted: function(el) {
5594
5729
  // Trigger show animation
5595
5730
  requestAnimationFrame(function() {
@@ -5947,7 +6082,7 @@ function makeCarousel(props = {}) {
5947
6082
  var total = carouselEl.querySelectorAll('.bw_carousel_slide').length;
5948
6083
  if (index < 0) index = total - 1;
5949
6084
  if (index >= total) index = 0;
5950
- carouselEl.setAttribute('data-carousel-index', index);
6085
+ carouselEl._bw_carouselIndex = index;
5951
6086
  var track = carouselEl.querySelector('.bw_carousel_track');
5952
6087
  track.style.transform = 'translateX(-' + (index * 100) + '%)';
5953
6088
  // Update indicators
@@ -6004,7 +6139,7 @@ function makeCarousel(props = {}) {
6004
6139
  'aria-label': 'Previous slide',
6005
6140
  onclick: function(e) {
6006
6141
  var carousel = e.target.closest('.bw_carousel');
6007
- var idx = parseInt(carousel.getAttribute('data-carousel-index') || '0');
6142
+ var idx = carousel._bw_carouselIndex || 0;
6008
6143
  goToSlide(carousel, idx - 1);
6009
6144
  }
6010
6145
  },
@@ -6018,7 +6153,7 @@ function makeCarousel(props = {}) {
6018
6153
  'aria-label': 'Next slide',
6019
6154
  onclick: function(e) {
6020
6155
  var carousel = e.target.closest('.bw_carousel');
6021
- var idx = parseInt(carousel.getAttribute('data-carousel-index') || '0');
6156
+ var idx = carousel._bw_carouselIndex || 0;
6022
6157
  goToSlide(carousel, idx + 1);
6023
6158
  }
6024
6159
  },
@@ -6038,11 +6173,9 @@ function makeCarousel(props = {}) {
6038
6173
  class: 'bw_carousel_indicator' + (i === startIndex ? ' active' : ''),
6039
6174
  type: 'button',
6040
6175
  'aria-label': 'Go to slide ' + (i + 1),
6041
- 'data-slide-index': i,
6042
6176
  onclick: function(e) {
6043
6177
  var carousel = e.target.closest('.bw_carousel');
6044
- var idx = parseInt(e.target.getAttribute('data-slide-index'));
6045
- goToSlide(carousel, idx);
6178
+ goToSlide(carousel, i);
6046
6179
  }
6047
6180
  }
6048
6181
  };
@@ -6056,17 +6189,37 @@ function makeCarousel(props = {}) {
6056
6189
  class: ('bw_carousel ' + className).trim(),
6057
6190
  style: 'height: ' + height,
6058
6191
  tabindex: '0',
6059
- 'aria-roledescription': 'carousel',
6060
- 'data-carousel-index': startIndex
6192
+ 'aria-roledescription': 'carousel'
6061
6193
  },
6062
6194
  c: children,
6063
6195
  o: {
6064
6196
  type: 'carousel',
6065
6197
  state: { activeIndex: startIndex, autoPlay: autoPlay, interval: interval },
6198
+ handle: {
6199
+ goToSlide: function(el, index) { goToSlide(el, index); },
6200
+ next: function(el) { goToSlide(el, (el._bw_carouselIndex || 0) + 1); },
6201
+ prev: function(el) { goToSlide(el, (el._bw_carouselIndex || 0) - 1); },
6202
+ getActiveIndex: function(el) { return el._bw_carouselIndex || 0; },
6203
+ pause: function(el) {
6204
+ if (el._bw_carouselInterval) {
6205
+ clearInterval(el._bw_carouselInterval);
6206
+ el._bw_carouselInterval = null;
6207
+ }
6208
+ },
6209
+ play: function(el) {
6210
+ if (!el._bw_carouselInterval && el._bw_state) {
6211
+ var ms = el._bw_state.interval || 5000;
6212
+ el._bw_carouselInterval = setInterval(function() {
6213
+ goToSlide(el, (el._bw_carouselIndex || 0) + 1);
6214
+ }, ms);
6215
+ }
6216
+ }
6217
+ },
6066
6218
  mounted: function(el) {
6219
+ el._bw_carouselIndex = startIndex;
6067
6220
  // Keyboard navigation
6068
6221
  el.addEventListener('keydown', function(e) {
6069
- var idx = parseInt(el.getAttribute('data-carousel-index') || '0');
6222
+ var idx = el._bw_carouselIndex || 0;
6070
6223
  if (e.key === 'ArrowLeft') {
6071
6224
  e.preventDefault();
6072
6225
  goToSlide(el, idx - 1);
@@ -6078,7 +6231,7 @@ function makeCarousel(props = {}) {
6078
6231
  // Auto-play
6079
6232
  if (autoPlay) {
6080
6233
  var intervalId = setInterval(function() {
6081
- var idx = parseInt(el.getAttribute('data-carousel-index') || '0');
6234
+ var idx = el._bw_carouselIndex || 0;
6082
6235
  goToSlide(el, idx + 1);
6083
6236
  }, interval);
6084
6237
  el._bw_carouselInterval = intervalId;
@@ -6088,7 +6241,7 @@ function makeCarousel(props = {}) {
6088
6241
  });
6089
6242
  el.addEventListener('mouseleave', function() {
6090
6243
  el._bw_carouselInterval = setInterval(function() {
6091
- var idx = parseInt(el.getAttribute('data-carousel-index') || '0');
6244
+ var idx = el._bw_carouselIndex || 0;
6092
6245
  goToSlide(el, idx + 1);
6093
6246
  }, interval);
6094
6247
  });
@@ -6204,7 +6357,13 @@ function makeStatCard(props = {}) {
6204
6357
  t: 'div',
6205
6358
  a: { class: classes, style: style },
6206
6359
  c: children,
6207
- o: { type: 'stat-card' }
6360
+ o: {
6361
+ type: 'stat-card',
6362
+ slots: {
6363
+ value: '.bw_stat_value',
6364
+ label: '.bw_stat_label'
6365
+ }
6366
+ }
6208
6367
  };
6209
6368
  }
6210
6369
 
@@ -6883,7 +7042,7 @@ function makeChipInput(props = {}) {
6883
7042
  function makeChipEl(text) {
6884
7043
  return {
6885
7044
  t: 'span',
6886
- a: { class: 'bw_chip', 'data-chip-value': text },
7045
+ a: { class: 'bw_chip' },
6887
7046
  c: [
6888
7047
  text,
6889
7048
  {
@@ -6894,9 +7053,8 @@ function makeChipInput(props = {}) {
6894
7053
  'aria-label': 'Remove ' + text,
6895
7054
  onclick: function(e) {
6896
7055
  var chip = e.target.closest('.bw_chip');
6897
- var val = chip.getAttribute('data-chip-value');
6898
7056
  chip.parentNode.removeChild(chip);
6899
- if (onRemove) onRemove(val);
7057
+ if (onRemove) onRemove(text);
6900
7058
  }
6901
7059
  },
6902
7060
  c: '\u00D7'
@@ -6924,7 +7082,7 @@ function makeChipInput(props = {}) {
6924
7082
  // Insert chip before the input
6925
7083
  var chipEl = document.createElement('span');
6926
7084
  chipEl.className = 'bw_chip';
6927
- chipEl.setAttribute('data-chip-value', val);
7085
+ chipEl._bw_chipValue = val;
6928
7086
  chipEl.innerHTML = '';
6929
7087
  chipEl.textContent = val;
6930
7088
  var removeBtn = document.createElement('button');
@@ -6947,7 +7105,7 @@ function makeChipInput(props = {}) {
6947
7105
  var chipEls = wrapper.querySelectorAll('.bw_chip');
6948
7106
  if (chipEls.length) {
6949
7107
  var last = chipEls[chipEls.length - 1];
6950
- var removedVal = last.getAttribute('data-chip-value');
7108
+ var removedVal = last._bw_chipValue || last.firstChild.textContent;
6951
7109
  last.parentNode.removeChild(last);
6952
7110
  if (onRemove) onRemove(removedVal);
6953
7111
  }
@@ -6956,7 +7114,50 @@ function makeChipInput(props = {}) {
6956
7114
  }
6957
7115
  }
6958
7116
  ],
6959
- o: { type: 'chip-input' }
7117
+ o: {
7118
+ type: 'chip-input',
7119
+ handle: {
7120
+ addChip: function(el, text) {
7121
+ if (!text) return;
7122
+ var input = el.querySelector('.bw_chip_field');
7123
+ var chipEl = document.createElement('span');
7124
+ chipEl.className = 'bw_chip';
7125
+ chipEl._bw_chipValue = text;
7126
+ chipEl.textContent = text;
7127
+ var removeBtn = document.createElement('button');
7128
+ removeBtn.type = 'button';
7129
+ removeBtn.className = 'bw_chip_remove';
7130
+ removeBtn.setAttribute('aria-label', 'Remove ' + text);
7131
+ removeBtn.textContent = '\u00D7';
7132
+ removeBtn.onclick = function() { chipEl.parentNode.removeChild(chipEl); };
7133
+ chipEl.appendChild(removeBtn);
7134
+ el.insertBefore(chipEl, input);
7135
+ },
7136
+ removeChip: function(el, text) {
7137
+ var chips = el.querySelectorAll('.bw_chip');
7138
+ for (var i = 0; i < chips.length; i++) {
7139
+ if ((chips[i]._bw_chipValue || chips[i].firstChild.textContent) === text) {
7140
+ chips[i].parentNode.removeChild(chips[i]);
7141
+ return;
7142
+ }
7143
+ }
7144
+ },
7145
+ getChips: function(el) {
7146
+ var chips = el.querySelectorAll('.bw_chip');
7147
+ var values = [];
7148
+ for (var i = 0; i < chips.length; i++) {
7149
+ values.push(chips[i]._bw_chipValue || chips[i].firstChild.textContent);
7150
+ }
7151
+ return values;
7152
+ },
7153
+ clear: function(el) {
7154
+ var chips = el.querySelectorAll('.bw_chip');
7155
+ for (var i = chips.length - 1; i >= 0; i--) {
7156
+ chips[i].parentNode.removeChild(chips[i]);
7157
+ }
7158
+ }
7159
+ }
7160
+ }
6960
7161
  };
6961
7162
  }
6962
7163
 
@@ -7143,12 +7344,11 @@ const bw = {
7143
7344
  _subIdCounter: 0, // monotonic ID for subscriptions
7144
7345
 
7145
7346
  // ── Node reference cache ──────────────────────────────────────────────
7146
- // Fast O(1) lookup for elements by bw_id, id attribute, or bw_uuid.
7347
+ // Fast O(1) lookup for elements by id attribute or bw_uuid_* class.
7147
7348
  //
7148
7349
  // Populated by bw.createDOM() when elements have:
7149
- // - data-bw_id attribute (user-declared addressable elements)
7150
7350
  // - id attribute (standard HTML id)
7151
- // - bw_uuid (internal, for lifecycle-managed elements)
7351
+ // - bw_uuid_* class (lifecycle-managed or explicitly addressed elements)
7152
7352
  //
7153
7353
  // Cleaned up by bw.cleanup() when elements are destroyed via bitwrench APIs.
7154
7354
  // On cache miss, falls back to querySelector/getElementById — never fails,
@@ -7156,7 +7356,7 @@ const bw = {
7156
7356
  // via parentNode === null check (IE11-safe, unlike el.isConnected).
7157
7357
  //
7158
7358
  // Elements created via bw.createDOM() also get el._bw_refs — a local map of
7159
- // child bw_id DOM node ref for fast parentchild access in o.render.
7359
+ // child id/UUID -> DOM node ref for fast parent->child access in o.render.
7160
7360
  // This is the bitwrench equivalent of React's compiled template "holes".
7161
7361
  //
7162
7362
  // Contract: if you remove elements outside of bitwrench APIs (raw el.remove()),
@@ -7236,7 +7436,6 @@ Object.defineProperty(bw, '_isBrowser', {
7236
7436
  // _cw console.warn 8
7237
7437
  // _cl console.log 11
7238
7438
  // _ce console.error 4
7239
- // _chp ComponentHandle.prototype 28 (defined after constructor)
7240
7439
  //
7241
7440
  // Note: document.createElement etc. are NOT aliased because they require
7242
7441
  // `this === document` and .bind() would add overhead on every call.
@@ -7409,15 +7608,15 @@ bw.uuid = function(prefix) {
7409
7608
  * 1. Check `bw._nodeMap[id]` — if found and still attached (parentNode !== null), return it
7410
7609
  * 2. If cached ref is detached (parentNode === null), remove stale entry
7411
7610
  * 3. Fall back to `document.getElementById(id)` then `document.querySelector(...)`
7412
- * 4. If fallback finds the element, cache it for next time
7413
- * 5. If not found anywhere, return null
7611
+ * 4. Try class-based lookup for bw_uuid_* tokens (UUID addressing)
7612
+ * 5. Cache the result for next time
7414
7613
  *
7415
7614
  * Accepts a DOM element directly (pass-through) or a string identifier.
7416
7615
  * String identifiers are tried as: direct map key, getElementById,
7417
7616
  * querySelector (for CSS selectors starting with . or #), and
7418
- * data-bw_id attribute selector.
7617
+ * bw_uuid_* class selector.
7419
7618
  *
7420
- * @param {string|Element} id - Element ID, CSS selector, data-bw_id value, or DOM element
7619
+ * @param {string|Element} id - Element ID, CSS selector, bw_uuid_* class, or DOM element
7421
7620
  * @returns {Element|null} The DOM element, or null if not found
7422
7621
  * @category Internal
7423
7622
  */
@@ -7446,17 +7645,12 @@ bw._el = function(id) {
7446
7645
  el = document.querySelector(id);
7447
7646
  }
7448
7647
 
7449
- // 4. Try data-bw_id attribute (for bw.uuid-generated IDs)
7450
- if (!el) {
7451
- el = document.querySelector('[data-bw_id="' + id + '"]');
7452
- }
7453
-
7454
- // 5. Try class-based lookup for bw_uuid_* tokens (UUID addressing)
7648
+ // 4. Try class-based lookup for bw_uuid_* tokens (UUID addressing)
7455
7649
  if (!el && id.indexOf('bw_uuid_') === 0) {
7456
7650
  el = document.querySelector('.' + id);
7457
7651
  }
7458
7652
 
7459
- // 6. Cache the result for next time
7653
+ // 5. Cache the result for next time
7460
7654
  if (el) {
7461
7655
  bw._nodeMap[id] = el;
7462
7656
  }
@@ -7468,17 +7662,17 @@ bw._el = function(id) {
7468
7662
  * Register a DOM element in the node cache under one or more keys.
7469
7663
  *
7470
7664
  * Called internally by `bw.createDOM()`. Registers elements that have
7471
- * id attributes, data-bw_id attributes, or both.
7665
+ * id attributes, UUID classes, or both.
7472
7666
  *
7473
7667
  * @param {Element} el - DOM element to register
7474
- * @param {string} [bwId] - data-bw_id value to register under
7668
+ * @param {string} [uuid] - bw_uuid_* class token to register under
7475
7669
  * @category Internal
7476
7670
  */
7477
- bw._registerNode = function(el, bwId) {
7671
+ bw._registerNode = function(el, uuid) {
7478
7672
  if (!el) return;
7479
- // Register under data-bw_id
7480
- if (bwId) {
7481
- bw._nodeMap[bwId] = el;
7673
+ // Register under UUID class token
7674
+ if (uuid) {
7675
+ bw._nodeMap[uuid] = el;
7482
7676
  }
7483
7677
  // Register under id attribute
7484
7678
  var htmlId = el.getAttribute ? el.getAttribute('id') : null;
@@ -7494,13 +7688,13 @@ bw._registerNode = function(el, bwId) {
7494
7688
  * through bitwrench APIs.
7495
7689
  *
7496
7690
  * @param {Element} el - DOM element to deregister
7497
- * @param {string} [bwId] - data-bw_id value to remove
7691
+ * @param {string} [uuid] - bw_uuid_* class token to remove
7498
7692
  * @category Internal
7499
7693
  */
7500
- bw._deregisterNode = function(el, bwId) {
7501
- // Remove data-bw_id entry
7502
- if (bwId) {
7503
- delete bw._nodeMap[bwId];
7694
+ bw._deregisterNode = function(el, uuid) {
7695
+ // Remove UUID class entry
7696
+ if (uuid) {
7697
+ delete bw._nodeMap[uuid];
7504
7698
  }
7505
7699
  // Remove id attribute entry
7506
7700
  var htmlId = el && el.getAttribute ? el.getAttribute('id') : null;
@@ -7513,6 +7707,13 @@ bw._deregisterNode = function(el, bwId) {
7513
7707
  // bw.assignUUID() / bw.getUUID() — Explicit UUID addressing for TACO objects
7514
7708
  // ===================================================================================
7515
7709
 
7710
+ /**
7711
+ * Marker class for elements with lifecycle hooks (mounted/unmount/render/state).
7712
+ * Used by cleanup() to find lifecycle-managed elements via querySelectorAll('.bw_lc').
7713
+ * @private
7714
+ */
7715
+ var _BW_LC = 'bw_lc';
7716
+
7516
7717
  /**
7517
7718
  * Regex to match a bw_uuid_* token in a class string.
7518
7719
  * @private
@@ -7701,15 +7902,6 @@ bw.html = function(taco, options = {}) {
7701
7902
  // Handle null/undefined
7702
7903
  if (taco == null) return '';
7703
7904
 
7704
- // Handle ComponentHandle — use its .taco
7705
- if (taco && taco._bwComponent === true) {
7706
- var compOptions = Object.assign({}, options);
7707
- if (!compOptions.state && taco._state) {
7708
- compOptions.state = taco._state;
7709
- }
7710
- return bw.html(taco.taco, compOptions);
7711
- }
7712
-
7713
7905
  // Handle arrays of TACOs
7714
7906
  if (_isA(taco)) {
7715
7907
  return taco.map(t => bw.html(t, options)).join('');
@@ -7720,24 +7912,6 @@ bw.html = function(taco, options = {}) {
7720
7912
  return taco.v;
7721
7913
  }
7722
7914
 
7723
- // Handle bw.when() markers
7724
- if (taco && taco._bwWhen && options.state) {
7725
- var whenExpr = taco.expr.replace(/^\$\{|\}$/g, '');
7726
- var whenVal = options.compile
7727
- ? bw._resolveTemplate('${' + whenExpr + '}', options.state, true)
7728
- : bw._evaluatePath(options.state, whenExpr);
7729
- var branch = whenVal ? taco.branches[0] : (taco.branches[1] || null);
7730
- return branch ? bw.html(branch, options) : '';
7731
- }
7732
-
7733
- // Handle bw.each() markers
7734
- if (taco && taco._bwEach && options.state) {
7735
- var eachExpr = taco.expr.replace(/^\$\{|\}$/g, '');
7736
- var arr = bw._evaluatePath(options.state, eachExpr);
7737
- if (!_isA(arr)) return '';
7738
- return arr.map(function(item, idx) { return bw.html(taco.factory(item, idx), options); }).join('');
7739
- }
7740
-
7741
7915
  // Handle primitives and non-TACO objects
7742
7916
  if (!_is(taco, 'object') || !taco.t) {
7743
7917
  var str = options.raw ? String(taco) : bw.escapeHTML(String(taco));
@@ -7801,14 +7975,14 @@ bw.html = function(taco, options = {}) {
7801
7975
  }
7802
7976
  }
7803
7977
 
7804
- // Add bw_id as a class if lifecycle hooks present
7805
- if ((opts.mounted || opts.unmount) && !attrs.class?.includes('bw_id_')) {
7806
- const id = opts.bw_id || bw.uuid();
7978
+ // Add bw_uuid + bw_lc classes if lifecycle hooks present
7979
+ if ((opts.mounted || opts.unmount) && !_UUID_RE.test(attrs.class || '')) {
7980
+ const uuid = bw.uuid('uuid');
7807
7981
  attrStr = attrStr.replace(/class="([^"]*)"/, (_match, classes) => {
7808
- return `class="${classes} bw_id_${id}"`.trim();
7982
+ return `class="${classes} ${uuid} ${_BW_LC}"`.trim();
7809
7983
  });
7810
7984
  if (!attrStr.includes('class=')) {
7811
- attrStr += ` class="bw_id_${id}"`;
7985
+ attrStr += ` class="${uuid} ${_BW_LC}"`;
7812
7986
  }
7813
7987
  }
7814
7988
 
@@ -8036,11 +8210,6 @@ bw.createDOM = function(taco, options = {}) {
8036
8210
  return frag;
8037
8211
  }
8038
8212
 
8039
- // Handle ComponentHandle — extract .taco for DOM creation
8040
- if (taco && taco._bwComponent === true) {
8041
- return bw.createDOM(taco.taco, options);
8042
- }
8043
-
8044
8213
  // Handle text nodes
8045
8214
  if (!_is(taco, 'object') || !taco.t) {
8046
8215
  return document.createTextNode(String(taco));
@@ -8081,24 +8250,19 @@ bw.createDOM = function(taco, options = {}) {
8081
8250
  }
8082
8251
 
8083
8252
  // Add children, building _bw_refs for fast parent→child access.
8084
- // Children with data-bw_id or id attributes get local refs on the parent,
8253
+ // Children with id attributes or bw_uuid_* classes get local refs on the parent,
8085
8254
  // so o.render functions can access them without any DOM lookup.
8086
8255
  if (content != null) {
8087
8256
  if (_isA(content)) {
8088
8257
  content.forEach(child => {
8089
8258
  if (child != null) {
8090
- // Handle ComponentHandle in content arrays (Level 2 children)
8091
- if (child._bwComponent === true) {
8092
- child.mount(el);
8093
- return;
8094
- }
8095
8259
  var childEl = bw.createDOM(child, options);
8096
8260
  el.appendChild(childEl);
8097
8261
  // Build local refs for addressable children
8098
- var childBwId = (child && child.a) ? (child.a['data-bw_id'] || child.a.id) : null;
8099
- if (childBwId) {
8262
+ var childRefId = (child && child.a) ? (child.a.id || bw.getUUID(child)) : null;
8263
+ if (childRefId) {
8100
8264
  if (!el._bw_refs) el._bw_refs = {};
8101
- el._bw_refs[childBwId] = childEl;
8265
+ el._bw_refs[childRefId] = childEl;
8102
8266
  }
8103
8267
  // Bubble up grandchild refs (flatten one level)
8104
8268
  if (childEl._bw_refs) {
@@ -8114,16 +8278,13 @@ bw.createDOM = function(taco, options = {}) {
8114
8278
  } else if (_is(content, 'object') && content.__bw_raw) {
8115
8279
  // Raw HTML content — inject via innerHTML
8116
8280
  el.innerHTML = content.v;
8117
- } else if (content._bwComponent === true) {
8118
- // Single ComponentHandle as content
8119
- content.mount(el);
8120
8281
  } else if (_is(content, 'object') && content.t) {
8121
8282
  var childEl = bw.createDOM(content, options);
8122
8283
  el.appendChild(childEl);
8123
- var childBwId = content.a ? (content.a['data-bw_id'] || content.a.id) : null;
8124
- if (childBwId) {
8284
+ var childRefId = content.a ? (content.a.id || bw.getUUID(content)) : null;
8285
+ if (childRefId) {
8125
8286
  if (!el._bw_refs) el._bw_refs = {};
8126
- el._bw_refs[childBwId] = childEl;
8287
+ el._bw_refs[childRefId] = childEl;
8127
8288
  }
8128
8289
  if (childEl._bw_refs) {
8129
8290
  if (!el._bw_refs) el._bw_refs = {};
@@ -8153,57 +8314,88 @@ bw.createDOM = function(taco, options = {}) {
8153
8314
 
8154
8315
  // Handle lifecycle hooks and state
8155
8316
  if (opts.mounted || opts.unmount || opts.render || opts.state) {
8156
- const id = attrs['data-bw_id'] || bw.uuid();
8157
- el.setAttribute('data-bw_id', id);
8317
+ // Ensure element has a UUID class for identity
8318
+ var uuid = bw.getUUID(el) || bw.uuid('uuid');
8319
+ el.classList.add(uuid);
8320
+ el.classList.add(_BW_LC);
8158
8321
 
8159
- // Register in node cache under data-bw_id
8160
- bw._registerNode(el, id);
8322
+ // Register in node cache under UUID class
8323
+ bw._registerNode(el, uuid);
8161
8324
 
8162
8325
  // Store state
8163
8326
  if (opts.state) {
8164
8327
  el._bw_state = opts.state;
8165
8328
  }
8166
8329
 
8167
- // o.render — first-class render function (replaces mounted boilerplate)
8330
+ // o.render — store the render function for bw.update()
8168
8331
  if (opts.render) {
8169
8332
  el._bw_render = opts.render;
8333
+ }
8170
8334
 
8171
- if (opts.mounted) {
8172
- _cw('bw.createDOM: o.render and o.mounted are mutually exclusive. o.render wins.');
8173
- }
8335
+ // Determine what to call on mount:
8336
+ // - If o.mounted exists, call it (it can call el._bw_render() for initial render)
8337
+ // - Otherwise if o.render exists, auto-call it as a convenience shorthand
8338
+ var mountFn = opts.mounted || (opts.render ? function(mountEl) {
8339
+ opts.render(mountEl, mountEl._bw_state || {});
8340
+ } : null);
8174
8341
 
8175
- // Queue initial render (same timing as mounted)
8176
- if (document.body.contains(el)) {
8177
- opts.render(el, el._bw_state || {});
8178
- } else {
8179
- requestAnimationFrame(() => {
8180
- if (document.body.contains(el)) {
8181
- opts.render(el, el._bw_state || {});
8182
- }
8183
- });
8184
- }
8185
- } else if (opts.mounted) {
8186
- // Queue mounted callback (legacy pattern)
8342
+ if (mountFn) {
8187
8343
  if (document.body.contains(el)) {
8188
- opts.mounted(el, el._bw_state || {});
8344
+ mountFn(el, el._bw_state || {});
8189
8345
  } else {
8190
8346
  requestAnimationFrame(() => {
8191
8347
  if (document.body.contains(el)) {
8192
- opts.mounted(el, el._bw_state || {});
8348
+ mountFn(el, el._bw_state || {});
8193
8349
  }
8194
8350
  });
8195
8351
  }
8196
8352
  }
8197
8353
 
8198
- // Store unmount callback
8354
+ // Store unmount callback keyed by UUID class
8199
8355
  if (opts.unmount) {
8200
- bw._unmountCallbacks.set(id, () => {
8356
+ bw._unmountCallbacks.set(uuid, () => {
8201
8357
  opts.unmount(el, el._bw_state || {});
8202
8358
  });
8203
8359
  }
8204
- } else if (attrs['data-bw_id']) {
8205
- // Element has explicit data-bw_id but no lifecycle hooks — still register it
8206
- bw._registerNode(el, attrs['data-bw_id']);
8360
+ }
8361
+
8362
+ // Component handle: attach methods to el.bw namespace
8363
+ if (opts.handle || opts.slots) {
8364
+ if (!el.bw) el.bw = {};
8365
+
8366
+ // Explicit handle methods: fn(el, ...args) -> el.bw.method(...args)
8367
+ if (opts.handle) {
8368
+ for (var hk in opts.handle) {
8369
+ if (_hop.call(opts.handle, hk)) {
8370
+ el.bw[hk] = opts.handle[hk].bind(null, el);
8371
+ }
8372
+ }
8373
+ }
8374
+
8375
+ // Slot declarations: auto-generate setX/getX pairs
8376
+ if (opts.slots) {
8377
+ for (var sk in opts.slots) {
8378
+ if (_hop.call(opts.slots, sk)) {
8379
+ (function(name, selector) {
8380
+ var cap = name.charAt(0).toUpperCase() + name.slice(1);
8381
+ el.bw['set' + cap] = function(value) {
8382
+ var t = el.querySelector(selector);
8383
+ if (!t) return;
8384
+ if (value != null && typeof value === 'object' && value.t) {
8385
+ t.innerHTML = '';
8386
+ t.appendChild(bw.createDOM(value));
8387
+ } else {
8388
+ t.textContent = (value != null) ? String(value) : '';
8389
+ }
8390
+ };
8391
+ el.bw['get' + cap] = function() {
8392
+ var t = el.querySelector(selector);
8393
+ return t ? t.textContent : '';
8394
+ };
8395
+ })(sk, opts.slots[sk]);
8396
+ }
8397
+ }
8398
+ }
8207
8399
  }
8208
8400
 
8209
8401
  return el;
@@ -8250,7 +8442,7 @@ bw.DOM = function(target, taco, options = {}) {
8250
8442
  // the target is the mount point, not the content being replaced)
8251
8443
  const savedState = targetEl._bw_state;
8252
8444
  const savedRender = targetEl._bw_render;
8253
- const savedBwId = targetEl.getAttribute('data-bw_id');
8445
+ const savedUuid = bw.getUUID(targetEl);
8254
8446
  const savedSubs = targetEl._bw_subs;
8255
8447
 
8256
8448
  // Temporarily remove _bw_subs so cleanup doesn't call them
@@ -8262,10 +8454,9 @@ bw.DOM = function(target, taco, options = {}) {
8262
8454
  // Restore the target's own state/render/subs after cleanup
8263
8455
  if (savedState !== undefined) targetEl._bw_state = savedState;
8264
8456
  if (savedRender) targetEl._bw_render = savedRender;
8265
- if (savedBwId) {
8266
- targetEl.setAttribute('data-bw_id', savedBwId);
8267
- // Re-register mount point in node cache (cleanup deregistered it)
8268
- bw._registerNode(targetEl, savedBwId);
8457
+ if (savedUuid) {
8458
+ // UUID class stays on element through cleanup; re-register in cache
8459
+ bw._registerNode(targetEl, savedUuid);
8269
8460
  }
8270
8461
  if (savedSubs) targetEl._bw_subs = savedSubs;
8271
8462
 
@@ -8273,25 +8464,11 @@ bw.DOM = function(target, taco, options = {}) {
8273
8464
  targetEl.innerHTML = '';
8274
8465
 
8275
8466
  if (taco != null) {
8276
- // Handle ComponentHandle (reactive components from bw.component())
8277
- if (taco._bwComponent === true) {
8278
- taco.mount(targetEl);
8279
- }
8280
- // Handle component handles (objects with element property)
8281
- else if (taco.element instanceof Element) {
8282
- targetEl.appendChild(taco.element);
8283
- }
8284
8467
  // Handle arrays
8285
- else if (_isA(taco)) {
8468
+ if (_isA(taco)) {
8286
8469
  taco.forEach(t => {
8287
8470
  if (t != null) {
8288
- if (t._bwComponent === true) {
8289
- t.mount(targetEl);
8290
- } else if (t.element instanceof Element) {
8291
- targetEl.appendChild(t.element);
8292
- } else {
8293
- targetEl.appendChild(bw.createDOM(t, options));
8294
- }
8471
+ targetEl.appendChild(bw.createDOM(t, options));
8295
8472
  }
8296
8473
  });
8297
8474
  }
@@ -8304,205 +8481,36 @@ bw.DOM = function(target, taco, options = {}) {
8304
8481
  return targetEl;
8305
8482
  };
8306
8483
 
8307
- /**
8308
- * Compile props into getter/setter functions for reactive updates.
8309
- *
8310
- * Used internally by `bw.renderComponent()`. Creates a proxy-like object
8311
- * where setting a property triggers `handle.onPropChange()`.
8312
- *
8313
- * @param {Object} handle - Component handle
8314
- * @param {Object} props - Initial props
8315
- * @returns {Object} Compiled props object with getters/setters
8316
- * @category DOM Generation
8317
- */
8318
- bw.compileProps = function(handle, props = {}) {
8319
- const compiledProps = {};
8320
-
8321
- _keys(props).forEach(key => {
8322
- // Create getter/setter for each prop
8323
- Object.defineProperty(compiledProps, key, {
8324
- get() {
8325
- return handle._props[key];
8326
- },
8327
- set(value) {
8328
- const oldValue = handle._props[key];
8329
- if (oldValue !== value) {
8330
- handle._props[key] = value;
8331
- // Trigger update if prop changed
8332
- if (handle.onPropChange) {
8333
- handle.onPropChange(key, value, oldValue);
8334
- }
8335
- }
8336
- },
8337
- enumerable: true,
8338
- configurable: true
8339
- });
8340
- });
8341
-
8342
- return compiledProps;
8343
- };
8484
+ // Deprecation stubs for removed ComponentHandle APIs
8485
+ bw.compileProps = function() { throw new Error('bw.compileProps() removed in v2.0.19. Use o.handle/o.slots instead.'); };
8486
+ bw.renderComponent = function() { throw new Error('bw.renderComponent() removed in v2.0.19. Use bw.mount() with o.handle/o.slots instead.'); };
8344
8487
 
8345
8488
  /**
8346
- * Render a TACO component and return an enhanced handle object.
8347
- *
8348
- * The handle provides compiled props, state management, child registration,
8349
- * and a destroy method. Used internally by `bw.createCard()`, `bw.createTable()`, etc.
8489
+ * Mount a TACO into a target element and return the created root element.
8490
+ * Like bw.DOM() but returns the root element of the TACO (not the container),
8491
+ * giving direct access to el.bw handle methods.
8350
8492
  *
8351
- * @param {Object} taco - TACO object to render
8352
- * @param {Object} [options] - Render options
8353
- * @returns {Object} Component handle with element, props, state, update(), destroy()
8493
+ * @param {string|Element} target - CSS selector or DOM element
8494
+ * @param {Object} taco - TACO to render
8495
+ * @param {Object} [options] - Mount options
8496
+ * @returns {Element} The created root element
8354
8497
  * @category DOM Generation
8355
- */
8356
- bw.renderComponent = function(taco, options = {}) {
8357
- const element = bw.createDOM(taco, options);
8358
-
8359
- // Enhanced handle with prop compilation
8360
- const handle = {
8361
- element,
8362
- taco,
8363
- _props: { ...taco.a }, // Store props internally
8364
- _state: taco.o?.state || {},
8365
- _children: {}, // Store child component references
8366
-
8367
- // Get compiled props with getters/setters
8368
- get props() {
8369
- if (!this._compiledProps) {
8370
- this._compiledProps = bw.compileProps(this, this._props);
8371
- }
8372
- return this._compiledProps;
8373
- },
8374
-
8375
- /**
8376
- * Query all matching elements within this component
8377
- * @param {string} selector - CSS selector
8378
- * @returns {NodeList} Matching elements
8379
- */
8380
- $(selector) {
8381
- return this.element.querySelectorAll(selector);
8382
- },
8383
-
8384
- /**
8385
- * Query the first matching element within this component
8386
- * @param {string} selector - CSS selector
8387
- * @returns {Element|null} First matching element or null
8388
- */
8389
- $first(selector) {
8390
- return this.element.querySelector(selector);
8391
- },
8392
-
8393
- /**
8394
- * Update component with new props and re-render in place
8395
- * @param {Object} newProps - Properties to merge into current props
8396
- * @returns {Object} this handle (for chaining)
8397
- */
8398
- update(newProps) {
8399
- // Update internal props
8400
- Object.assign(this._props, newProps);
8401
-
8402
- // Rebuild TACO with new props
8403
- const newTaco = { ...this.taco, a: { ...this.taco.a, ...newProps } };
8404
- const newElement = bw.createDOM(newTaco, options);
8405
-
8406
- // Replace in DOM
8407
- this.element.replaceWith(newElement);
8408
- this.element = newElement;
8409
- this.taco = newTaco;
8410
-
8411
- return this;
8412
- },
8413
-
8414
- /**
8415
- * Re-render the component from its current TACO, replacing the DOM element
8416
- * @returns {Object} this handle (for chaining)
8417
- */
8418
- render() {
8419
- const newElement = bw.createDOM(this.taco, options);
8420
- this.element.replaceWith(newElement);
8421
- this.element = newElement;
8422
- return this;
8423
- },
8424
-
8425
- /**
8426
- * Called when a compiled prop value changes. Override to customize behavior.
8427
- * Default implementation triggers a full re-render.
8428
- * @param {string} key - Property name that changed
8429
- * @param {*} newValue - New property value
8430
- * @param {*} oldValue - Previous property value
8431
- */
8432
- onPropChange(_key, _newValue, _oldValue) {
8433
- // Auto re-render on prop change by default
8434
- this.render();
8435
- },
8436
-
8437
- // State management
8438
- get state() {
8439
- return this._state;
8440
- },
8441
-
8442
- set state(newState) {
8443
- this._state = newState;
8444
- this.render();
8445
- },
8446
-
8447
- /**
8448
- * Merge state updates and re-render the component
8449
- * @param {Object} updates - State properties to merge
8450
- * @returns {Object} this handle (for chaining)
8451
- */
8452
- setState(updates) {
8453
- Object.assign(this._state, updates);
8454
- this.render();
8455
- return this;
8456
- },
8457
-
8458
- /**
8459
- * Register a child component under a name for later retrieval
8460
- * @param {string} name - Child name key
8461
- * @param {Object} component - Child component handle
8462
- * @returns {Object} this handle (for chaining)
8463
- */
8464
- addChild(name, component) {
8465
- this._children[name] = component;
8466
- return this;
8467
- },
8468
-
8469
- /**
8470
- * Retrieve a registered child component by name
8471
- * @param {string} name - Child name key
8472
- * @returns {Object|undefined} Child component handle
8473
- */
8474
- getChild(name) {
8475
- return this._children[name];
8476
- },
8477
-
8478
- /**
8479
- * Destroy this component and all registered children
8480
- *
8481
- * Calls destroy() recursively on children, runs bw.cleanup(),
8482
- * removes the element from DOM, and clears all internal references.
8483
- */
8484
- destroy() {
8485
- // Destroy children first
8486
- Object.values(this._children).forEach(child => {
8487
- if (child && child.destroy) child.destroy();
8488
- });
8489
-
8490
- // Clean up this component
8491
- bw.cleanup(this.element);
8492
- this.element.remove();
8493
-
8494
- // Clear references
8495
- this._children = {};
8496
- this._props = {};
8497
- this._state = {};
8498
- this._compiledProps = null;
8499
- }
8500
- };
8501
-
8502
- // Store handle reference on element
8503
- element._bwHandle = handle;
8504
-
8505
- return handle;
8498
+ * @example
8499
+ * var el = bw.mount('#app', bw.makeCarousel({ items: slides }));
8500
+ * el.bw.goToSlide(2);
8501
+ * el.bw.next();
8502
+ */
8503
+ bw.mount = function(target, taco, options) {
8504
+ var container = _is(target, 'string') ? bw.$(target)[0] : target;
8505
+ if (!container) {
8506
+ _cw('bw.mount: target not found');
8507
+ return null;
8508
+ }
8509
+ bw.cleanup(container);
8510
+ container.innerHTML = '';
8511
+ var el = bw.createDOM(taco, options || {});
8512
+ container.appendChild(el);
8513
+ return el;
8506
8514
  };
8507
8515
 
8508
8516
  /**
@@ -8523,34 +8531,29 @@ bw.renderComponent = function(taco, options = {}) {
8523
8531
  bw.cleanup = function(element) {
8524
8532
  if (!bw._isBrowser || !element) return;
8525
8533
 
8526
- // Deregister UUID classes from node cache (element + descendants)
8527
- // Covers elements that have UUID but no data-bw_id
8528
- var selfUuidMatch = element.className && element.className.match(_UUID_RE);
8529
- if (selfUuidMatch) delete bw._nodeMap[selfUuidMatch[0]];
8534
+ // Deregister UUID classes from node cache for non-lifecycle UUID elements
8530
8535
  var uuidEls = element.querySelectorAll('[class*="bw_uuid_"]');
8531
8536
  uuidEls.forEach(function(uel) {
8532
8537
  var m = uel.className && uel.className.match(_UUID_RE);
8533
8538
  if (m) delete bw._nodeMap[m[0]];
8534
8539
  });
8535
8540
 
8536
- // Find all elements with data-bw_id
8537
- const elements = element.querySelectorAll('[data-bw_id]');
8541
+ // Find all lifecycle-managed elements (have bw_lc marker class)
8542
+ const elements = element.querySelectorAll('.' + _BW_LC);
8538
8543
 
8539
8544
  elements.forEach(el => {
8540
- const id = el.getAttribute('data-bw_id');
8541
- const callback = bw._unmountCallbacks.get(id);
8542
-
8543
- if (callback) {
8544
- callback();
8545
- bw._unmountCallbacks.delete(id);
8546
- }
8545
+ var uuid = bw.getUUID(el);
8547
8546
 
8548
- // Deregister from node cache
8549
- bw._deregisterNode(el, id);
8547
+ if (uuid) {
8548
+ const callback = bw._unmountCallbacks.get(uuid);
8549
+ if (callback) {
8550
+ callback();
8551
+ bw._unmountCallbacks.delete(uuid);
8552
+ }
8550
8553
 
8551
- // Deregister UUID class from node cache
8552
- var uuidMatch = el.className && el.className.match(_UUID_RE);
8553
- if (uuidMatch) delete bw._nodeMap[uuidMatch[0]];
8554
+ // Deregister from node cache
8555
+ bw._deregisterNode(el, uuid);
8556
+ }
8554
8557
 
8555
8558
  // Clean up pub/sub subscriptions tied to this element
8556
8559
  if (el._bw_subs) {
@@ -8565,20 +8568,18 @@ bw.cleanup = function(element) {
8565
8568
  });
8566
8569
 
8567
8570
  // Check element itself
8568
- const id = element.getAttribute('data-bw_id');
8569
- if (id) {
8570
- const callback = bw._unmountCallbacks.get(id);
8571
+ var selfUuid = bw.getUUID(element);
8572
+ if (selfUuid) {
8573
+ delete bw._nodeMap[selfUuid];
8574
+
8575
+ const callback = bw._unmountCallbacks.get(selfUuid);
8571
8576
  if (callback) {
8572
8577
  callback();
8573
- bw._unmountCallbacks.delete(id);
8578
+ bw._unmountCallbacks.delete(selfUuid);
8574
8579
  }
8575
8580
 
8576
8581
  // Deregister from node cache
8577
- bw._deregisterNode(element, id);
8578
-
8579
- // Deregister UUID class from node cache
8580
- var elemUuidMatch = element.className && element.className.match(_UUID_RE);
8581
- if (elemUuidMatch) delete bw._nodeMap[elemUuidMatch[0]];
8582
+ bw._deregisterNode(element, selfUuid);
8582
8583
 
8583
8584
  // Clean up pub/sub subscriptions tied to element itself
8584
8585
  if (element._bw_subs) {
@@ -8589,11 +8590,11 @@ bw.cleanup = function(element) {
8589
8590
  delete element._bw_render;
8590
8591
  delete element._bw_refs;
8591
8592
 
8592
- // Clean up ComponentHandle back-reference
8593
- if (element._bwComponentHandle) {
8594
- element._bwComponentHandle.mounted = false;
8595
- element._bwComponentHandle.element = null;
8596
- delete element._bwComponentHandle;
8593
+ } else {
8594
+ // No UUID on element itself, but still check for _bw_subs (from bw.sub())
8595
+ if (element._bw_subs) {
8596
+ element._bw_subs.forEach(function(unsub) { unsub(); });
8597
+ delete element._bw_subs;
8597
8598
  }
8598
8599
  }
8599
8600
  };
@@ -8609,7 +8610,7 @@ bw.cleanup = function(element) {
8609
8610
  * Calls `el._bw_render(el, state)` and emits `bw:statechange` so other
8610
8611
  * components can react without tight coupling.
8611
8612
  *
8612
- * @param {string|Element} target - Element ID, data-bw_id, CSS selector, or DOM element
8613
+ * @param {string|Element} target - Element ID, bw_uuid_* class, CSS selector, or DOM element
8613
8614
  * @returns {Element|null} The element, or null if not found / no render function
8614
8615
  * @category State Management
8615
8616
  * @see bw.patch
@@ -8634,7 +8635,7 @@ bw.update = function(target) {
8634
8635
  * Use `bw.patch()` for lightweight value updates (scores, labels, counters)
8635
8636
  * and `bw.update()` for full structural re-renders.
8636
8637
  *
8637
- * @param {string|Element} id - Element ID, data-bw_id, CSS selector, or DOM element.
8638
+ * @param {string|Element} id - Element ID, bw_uuid_* class, CSS selector, or DOM element.
8638
8639
  * Uses node cache for O(1) lookup; falls back to DOM query on cache miss.
8639
8640
  * @param {string|Object} content - New text content, or TACO object to replace children
8640
8641
  * @param {string} [attr] - If provided, sets this attribute instead of content
@@ -8709,7 +8710,7 @@ bw.patchAll = function(patches) {
8709
8710
  * bubble by default so ancestor elements can listen. Use with `bw.on()` for
8710
8711
  * DOM-scoped communication between components.
8711
8712
  *
8712
- * @param {string|Element} target - Element ID, data-bw_id, CSS selector, or DOM element.
8713
+ * @param {string|Element} target - Element ID, bw_uuid_* class, CSS selector, or DOM element.
8713
8714
  * Uses node cache for O(1) lookup; falls back to DOM query on cache miss.
8714
8715
  * @param {string} eventName - Event name (will be prefixed with 'bw:')
8715
8716
  * @param {*} [detail] - Data to pass with the event
@@ -8736,7 +8737,7 @@ bw.emit = function(target, eventName, detail) {
8736
8737
  * is the first argument so you don't need to destructure `e.detail`.
8737
8738
  * Events bubble, so you can listen on an ancestor element.
8738
8739
  *
8739
- * @param {string|Element} target - Element ID, data-bw_id, CSS selector, or DOM element.
8740
+ * @param {string|Element} target - Element ID, bw_uuid_* class, CSS selector, or DOM element.
8740
8741
  * Uses node cache for O(1) lookup; falls back to DOM query on cache miss.
8741
8742
  * @param {string} eventName - Event name (will be prefixed with 'bw:')
8742
8743
  * @param {Function} handler - Called with (detail, event)
@@ -8834,10 +8835,12 @@ bw.sub = function(topic, handler, el) {
8834
8835
  if (el) {
8835
8836
  if (!el._bw_subs) el._bw_subs = [];
8836
8837
  el._bw_subs.push(unsub);
8837
- // Ensure element has data-bw_id so bw.cleanup() finds it
8838
- if (!el.getAttribute('data-bw_id')) {
8839
- var bwId = 'bw_sub_' + id;
8840
- el.setAttribute('data-bw_id', bwId);
8838
+ // Ensure element has UUID + bw_lc so bw.cleanup() finds it
8839
+ if (!bw.getUUID(el)) {
8840
+ el.classList.add(bw.uuid('uuid'));
8841
+ }
8842
+ if (!el.classList.contains(_BW_LC)) {
8843
+ el.classList.add(_BW_LC);
8841
8844
  }
8842
8845
  }
8843
8846
 
@@ -9059,1148 +9062,97 @@ bw._resolveTemplate = function(str, state, compile) {
9059
9062
  return result;
9060
9063
  };
9061
9064
 
9062
- /**
9063
- * Extract top-level state keys that an expression depends on.
9064
- * @param {string} expr - Expression string
9065
- * @param {string[]} stateKeys - Declared state keys
9066
- * @returns {string[]} Matching dependency keys
9067
- * @private
9068
- */
9069
- bw._extractDeps = function(expr, stateKeys) {
9070
- var deps = [];
9071
- for (var i = 0; i < stateKeys.length; i++) {
9072
- var key = stateKeys[i];
9073
- // Match word boundary: key must be preceded by start/non-word and followed by non-word/end
9074
- var re = new RegExp('(?:^|[^\\w$.])' + key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '(?:[^\\w$]|$)');
9075
- if (re.test(expr) || expr === key || expr.indexOf(key + '.') === 0) {
9076
- deps.push(key);
9077
- }
9078
- }
9079
- return deps;
9080
- };
9081
-
9082
9065
  // ===================================================================================
9083
- // Microtask Batching
9066
+ // Deprecation stubs for removed ComponentHandle APIs (v2.0.19)
9084
9067
  // ===================================================================================
9085
9068
 
9086
- bw._dirtyComponents = [];
9087
- bw._flushScheduled = false;
9069
+ bw._extractDeps = undefined;
9070
+ bw._dirtyComponents = undefined;
9071
+ bw._flushScheduled = undefined;
9072
+ bw._scheduleFlush = undefined;
9073
+ bw._doFlush = undefined;
9074
+ bw._ComponentHandle = undefined;
9088
9075
 
9089
9076
  /**
9090
- * Schedule a microtask flush for dirty components.
9091
- * @private
9077
+ * No-op flush (ComponentHandle removed in v2.0.19).
9078
+ * Kept as no-op for backward compatibility.
9079
+ * @category Component
9092
9080
  */
9093
- bw._scheduleFlush = function() {
9094
- if (bw._flushScheduled) return;
9095
- bw._flushScheduled = true;
9096
- if (typeof Promise !== 'undefined') {
9097
- Promise.resolve().then(bw._doFlush);
9098
- } else {
9099
- setTimeout(bw._doFlush, 0);
9100
- }
9101
- };
9081
+ bw.flush = function() {};
9102
9082
 
9103
- /**
9104
- * Flush all dirty components. Deduplicates by _bwId.
9105
- * @private
9106
- */
9107
- bw._doFlush = function() {
9108
- bw._flushScheduled = false;
9109
- var queue = bw._dirtyComponents.slice();
9110
- bw._dirtyComponents = [];
9111
- // Deduplicate by _bwId
9112
- var seen = {};
9113
- for (var i = 0; i < queue.length; i++) {
9114
- var comp = queue[i];
9115
- if (!seen[comp._bwId]) {
9116
- seen[comp._bwId] = true;
9117
- comp._flush();
9118
- }
9119
- }
9120
- };
9083
+
9084
+ bw.when = function() { throw new Error('bw.when() removed in v2.0.19. Use conditional logic in o.render instead.'); };
9085
+ bw.each = function() { throw new Error('bw.each() removed in v2.0.19. Use array mapping in o.render instead.'); };
9086
+ bw.component = function() { throw new Error('bw.component() removed in v2.0.19. Use o.handle/o.slots on TACO options instead.'); };
9087
+
9088
+
9089
+ // ===================================================================================
9090
+ // bw.message() SendMessage() for the web
9091
+ // ===================================================================================
9121
9092
 
9122
9093
  /**
9123
- * Synchronous flush for testing and imperative code.
9124
- * Forces immediate re-render of all dirty components.
9094
+ * Dispatch a message to a component by UUID, CSS class, or selector.
9095
+ * Finds the element, looks up el.bw, and calls the named method.
9096
+ * This is the bitwrench equivalent of Win32 SendMessage(hwnd, msg, wParam, lParam).
9125
9097
  *
9098
+ * @param {string} target - Component UUID (bw_uuid_*), CSS class, or selector
9099
+ * @param {string} action - Method name to call on el.bw
9100
+ * @param {*} data - Data to pass to the method
9101
+ * @returns {boolean} True if message was dispatched successfully
9126
9102
  * @category Component
9103
+ * @example
9104
+ * bw.message('my_carousel', 'goToSlide', 2);
9105
+ * // Or from SSE handler:
9106
+ * es.onmessage = function(e) {
9107
+ * var msg = JSON.parse(e.data);
9108
+ * bw.message(msg.target, msg.action, msg.data);
9109
+ * };
9127
9110
  */
9128
- bw.flush = function() {
9129
- bw._doFlush();
9111
+ bw.message = function(target, action, data) {
9112
+ var el = bw._el(target);
9113
+ if (!el) el = bw.$('.' + target)[0];
9114
+ if (!el || !el.bw || typeof el.bw[action] !== 'function') {
9115
+ _cw('bw.message: no handle method "' + action + '" on ' + target);
9116
+ return false;
9117
+ }
9118
+ el.bw[action](data);
9119
+ return true;
9130
9120
  };
9131
9121
 
9132
9122
  // ===================================================================================
9133
- // ComponentHandle unified reactive component (Phase 1)
9123
+ // bw.apply() / bw.parseJSONFlex() Server-driven UI protocol
9134
9124
  // ===================================================================================
9135
9125
 
9136
9126
  /**
9137
- * ComponentHandle constructor.
9138
- * Wraps a TACO definition with reactive state, lifecycle hooks,
9139
- * template bindings, and named actions.
9140
- *
9141
- * @param {Object} taco - TACO definition {t, a, c, o}
9142
- * @constructor
9127
+ * Registry of named functions sent via register messages.
9128
+ * Populated by bw.apply({ type: 'register', name, body }).
9129
+ * Invoked by bw.apply({ type: 'call', name, args }).
9143
9130
  * @private
9144
9131
  */
9145
- function ComponentHandle(taco) {
9146
- this._bwComponent = true; // duck-type marker
9147
- this._bwId = bw.uuid('comp');
9148
- this.taco = taco;
9149
- this.element = null;
9150
- this.mounted = false;
9151
-
9152
- var o = taco.o || {};
9153
- // Copy initial state
9154
- this._state = {};
9155
- if (o.state) {
9156
- for (var k in o.state) {
9157
- if (_hop.call(o.state, k)) {
9158
- this._state[k] = o.state[k];
9159
- }
9160
- }
9161
- }
9162
- // Copy actions
9163
- this._actions = {};
9164
- if (o.actions) {
9165
- for (var k2 in o.actions) {
9166
- if (_hop.call(o.actions, k2)) {
9167
- this._actions[k2] = o.actions[k2];
9168
- }
9169
- }
9170
- }
9171
- // Promote o.methods to handle API (MFC/Qt pattern: component owns its methods)
9172
- this._methods = {};
9173
- if (o.methods) {
9174
- var self = this;
9175
- for (var k3 in o.methods) {
9176
- if (_hop.call(o.methods, k3)) {
9177
- this._methods[k3] = o.methods[k3];
9178
- (function(methodName, methodFn) {
9179
- self[methodName] = function() {
9180
- var args = [self].concat(Array.prototype.slice.call(arguments));
9181
- return methodFn.apply(null, args);
9182
- };
9183
- })(k3, o.methods[k3]);
9184
- }
9185
- }
9186
- }
9187
- // User tag for addressing via bw.message()
9188
- this._userTag = null;
9189
- // Lifecycle hooks
9190
- this._hooks = {
9191
- willMount: o.willMount || null,
9192
- mounted: o.mounted || null,
9193
- willUpdate: o.willUpdate || null,
9194
- onUpdate: o.onUpdate || o.updated || null,
9195
- unmount: o.unmount || null,
9196
- willDestroy: o.willDestroy || null
9197
- };
9198
- // Binding tracking
9199
- this._bindings = [];
9200
- this._dirtyKeys = {};
9201
- this._scheduled = false;
9202
- this._subs = [];
9203
- this._eventListeners = [];
9204
- this._registeredActions = [];
9205
- this._prevValues = {};
9206
- this._compile = !!o.compile;
9207
- this._bw_refs = {};
9208
- this._refCounter = 0;
9209
- // Child component ownership (Bug #5)
9210
- this._children = [];
9211
- this._parent = null;
9212
- // Factory metadata for BCCL rebuild (Bug #6)
9213
- this._factory = taco._bwFactory || null;
9214
- }
9215
-
9216
- // Short alias for ComponentHandle.prototype (see alias block at top of file).
9217
- // 28 method definitions × 25 chars = ~700B raw savings in minified output.
9218
- var _chp = ComponentHandle.prototype;
9219
-
9220
- // ── State Methods ──
9132
+ bw._clientFunctions = {};
9221
9133
 
9222
9134
  /**
9223
- * Get a state value. Dot-path supported: `get('user.name')`
9135
+ * Whether exec messages are allowed. Set by bwclient connect opts.allowExec.
9136
+ * Default false — exec messages are rejected unless explicitly opted in.
9137
+ * @private
9224
9138
  */
9225
- _chp.get = function(key) {
9226
- return bw._evaluatePath(this._state, key);
9227
- };
9139
+ bw._allowExec = false;
9228
9140
 
9229
9141
  /**
9230
- * Set a state value. Dot-path supported. Schedules re-render.
9231
- * @param {string} key - State key (dot-path)
9232
- * @param {*} value - New value
9233
- * @param {Object} [opts] - Options. `{sync: true}` for immediate flush.
9234
- */
9235
- _chp.set = function(key, value, opts) {
9236
- // Dot-path set
9237
- var parts = key.split('.');
9238
- var obj = this._state;
9239
- for (var i = 0; i < parts.length - 1; i++) {
9240
- if (!_is(obj[parts[i]], 'object')) {
9241
- if (bw.debug) _cw('bw.debug: set() auto-creating intermediate "' + parts[i] + '" in path "' + key + '"');
9242
- obj[parts[i]] = {};
9243
- }
9244
- obj = obj[parts[i]];
9245
- }
9246
- obj[parts[parts.length - 1]] = value;
9247
- // Mark top-level key dirty
9248
- this._dirtyKeys[parts[0]] = true;
9249
- if (this.mounted) {
9250
- if (opts && opts.sync) {
9251
- this._flush();
9252
- } else {
9253
- this._scheduleDirty();
9254
- }
9255
- }
9256
- };
9257
-
9258
- /**
9259
- * Get a shallow clone of the full state.
9260
- */
9261
- _chp.getState = function() {
9262
- var clone = {};
9263
- for (var k in this._state) {
9264
- if (_hop.call(this._state, k)) {
9265
- clone[k] = this._state[k];
9266
- }
9267
- }
9268
- return clone;
9269
- };
9270
-
9271
- /**
9272
- * Merge multiple state keys. Schedules re-render.
9273
- * @param {Object} updates - Key-value pairs to merge
9274
- * @param {Object} [opts] - Options. `{sync: true}` for immediate flush.
9275
- */
9276
- _chp.setState = function(updates, opts) {
9277
- for (var k in updates) {
9278
- if (_hop.call(updates, k)) {
9279
- this._state[k] = updates[k];
9280
- this._dirtyKeys[k] = true;
9281
- }
9282
- }
9283
- if (this.mounted) {
9284
- if (opts && opts.sync) {
9285
- this._flush();
9286
- } else {
9287
- this._scheduleDirty();
9288
- }
9289
- }
9290
- };
9291
-
9292
- /**
9293
- * Push a value onto an array in state. Clones the array.
9294
- */
9295
- _chp.push = function(key, val) {
9296
- var arr = this.get(key);
9297
- var newArr = _isA(arr) ? arr.slice() : [];
9298
- newArr.push(val);
9299
- this.set(key, newArr);
9300
- };
9301
-
9302
- /**
9303
- * Splice an array in state. Clones the array.
9304
- */
9305
- _chp.splice = function(key, start, deleteCount) {
9306
- var arr = this.get(key);
9307
- var newArr = _isA(arr) ? arr.slice() : [];
9308
- var args = [start, deleteCount].concat(Array.prototype.slice.call(arguments, 3));
9309
- Array.prototype.splice.apply(newArr, args);
9310
- this.set(key, newArr);
9311
- };
9312
-
9313
- // ── Scheduling ──
9314
-
9315
- _chp._scheduleDirty = function() {
9316
- if (!this._scheduled) {
9317
- this._scheduled = true;
9318
- bw._dirtyComponents.push(this);
9319
- bw._scheduleFlush();
9320
- }
9321
- };
9322
-
9323
- // ── Binding Compilation ──
9324
-
9325
- /**
9326
- * Walk the TACO tree and extract ${expr} bindings.
9327
- * Creates binding descriptors with refIds for targeted DOM updates.
9328
- * @private
9329
- */
9330
- _chp._compileBindings = function() {
9331
- this._bindings = [];
9332
- this._refCounter = 0;
9333
- var stateKeys = _keys(this._state);
9334
- var self = this;
9335
-
9336
- function walkTaco(taco, path) {
9337
- if (!_is(taco, 'object') || !taco.t) return taco;
9338
-
9339
- // Check content for bindings
9340
- if (_is(taco.c, 'string') && taco.c.indexOf('${') >= 0) {
9341
- var refId = 'bw_ref_' + self._refCounter++;
9342
- var parsed = bw._parseBindings(taco.c);
9343
- var deps = [];
9344
- for (var j = 0; j < parsed.length; j++) {
9345
- deps = deps.concat(bw._extractDeps(parsed[j].expr, stateKeys));
9346
- }
9347
- self._bindings.push({
9348
- expr: taco.c,
9349
- type: 'content',
9350
- refId: refId,
9351
- deps: deps,
9352
- template: taco.c
9353
- });
9354
- // Inject data-bw_ref on the TACO for createDOM to pick up
9355
- if (!taco.a) taco.a = {};
9356
- taco.a['data-bw_ref'] = refId;
9357
- }
9358
-
9359
- // Check attributes for bindings
9360
- if (taco.a) {
9361
- for (var attrName in taco.a) {
9362
- if (!_hop.call(taco.a, attrName)) continue;
9363
- if (attrName === 'data-bw_ref') continue;
9364
- var attrVal = taco.a[attrName];
9365
- if (_is(attrVal, 'string') && attrVal.indexOf('${') >= 0) {
9366
- var refId2 = 'bw_ref_' + self._refCounter++;
9367
- var parsed2 = bw._parseBindings(attrVal);
9368
- var deps2 = [];
9369
- for (var j2 = 0; j2 < parsed2.length; j2++) {
9370
- deps2 = deps2.concat(bw._extractDeps(parsed2[j2].expr, stateKeys));
9371
- }
9372
- self._bindings.push({
9373
- expr: attrVal,
9374
- type: 'attribute',
9375
- attrName: attrName,
9376
- refId: refId2,
9377
- deps: deps2,
9378
- template: attrVal
9379
- });
9380
- if (!taco.a) taco.a = {};
9381
- taco.a['data-bw_ref'] = taco.a['data-bw_ref'] || refId2;
9382
- // If multiple attribute bindings on same element, store additional marker
9383
- if (taco.a['data-bw_ref'] !== refId2) {
9384
- taco.a['data-bw_ref_' + attrName] = refId2;
9385
- }
9386
- }
9387
- }
9388
- }
9389
-
9390
- // Recurse into children
9391
- if (_isA(taco.c)) {
9392
- for (var i = 0; i < taco.c.length; i++) {
9393
- // Wrap string children with ${expr} in a span so patches target the span, not the parent
9394
- if (_is(taco.c[i], 'string') && taco.c[i].indexOf('${') >= 0) {
9395
- var mixedRefId = 'bw_ref_' + self._refCounter++;
9396
- var mixedParsed = bw._parseBindings(taco.c[i]);
9397
- var mixedDeps = [];
9398
- for (var mi = 0; mi < mixedParsed.length; mi++) {
9399
- mixedDeps = mixedDeps.concat(bw._extractDeps(mixedParsed[mi].expr, stateKeys));
9400
- }
9401
- self._bindings.push({
9402
- expr: taco.c[i],
9403
- type: 'content',
9404
- refId: mixedRefId,
9405
- deps: mixedDeps,
9406
- template: taco.c[i]
9407
- });
9408
- // Replace string with a span wrapper so textContent targets the span only
9409
- taco.c[i] = { t: 'span', a: { 'data-bw_ref': mixedRefId, style: 'display:contents' }, c: taco.c[i] };
9410
- }
9411
- if (_is(taco.c[i], 'object') && taco.c[i].t) {
9412
- walkTaco(taco.c[i], path.concat(i));
9413
- }
9414
- // Handle bw.when/bw.each markers
9415
- if (taco.c[i] && taco.c[i]._bwWhen) {
9416
- var whenRefId = 'bw_ref_' + self._refCounter++;
9417
- var whenDeps = bw._extractDeps(taco.c[i].expr.replace(/^\$\{|\}$/g, ''), stateKeys);
9418
- self._bindings.push({
9419
- expr: taco.c[i].expr,
9420
- type: 'structural',
9421
- subtype: 'when',
9422
- refId: whenRefId,
9423
- deps: whenDeps,
9424
- branches: taco.c[i].branches,
9425
- index: i,
9426
- parentPath: path
9427
- });
9428
- taco.c[i]._refId = whenRefId;
9429
- }
9430
- if (taco.c[i] && taco.c[i]._bwEach) {
9431
- var eachRefId = 'bw_ref_' + self._refCounter++;
9432
- var eachDeps = bw._extractDeps(taco.c[i].expr.replace(/^\$\{|\}$/g, ''), stateKeys);
9433
- self._bindings.push({
9434
- expr: taco.c[i].expr,
9435
- type: 'structural',
9436
- subtype: 'each',
9437
- refId: eachRefId,
9438
- deps: eachDeps,
9439
- factory: taco.c[i].factory,
9440
- index: i,
9441
- parentPath: path
9442
- });
9443
- taco.c[i]._refId = eachRefId;
9444
- }
9445
- }
9446
- } else if (_is(taco.c, 'object') && taco.c.t) {
9447
- walkTaco(taco.c, path.concat(0));
9448
- }
9449
-
9450
- return taco;
9451
- }
9452
-
9453
- walkTaco(this.taco, []);
9454
- };
9455
-
9456
- // ── DOM Reference Collection ──
9457
-
9458
- /**
9459
- * Build ref map from the live DOM after createDOM.
9460
- * @private
9461
- */
9462
- _chp._collectRefs = function() {
9463
- this._bw_refs = {};
9464
- if (!this.element) return;
9465
- var els = this.element.querySelectorAll('[data-bw_ref]');
9466
- for (var i = 0; i < els.length; i++) {
9467
- this._bw_refs[els[i].getAttribute('data-bw_ref')] = els[i];
9468
- }
9469
- // Also check root element
9470
- var rootRef = this.element.getAttribute && this.element.getAttribute('data-bw_ref');
9471
- if (rootRef) {
9472
- this._bw_refs[rootRef] = this.element;
9473
- }
9474
- };
9475
-
9476
- // ── Lifecycle ──
9477
-
9478
- /**
9479
- * Mount the component into a parent DOM element.
9480
- * Creates DOM, compiles bindings, registers actions, and calls lifecycle hooks.
9481
- * @param {Element} parentEl - DOM element to mount into
9482
- */
9483
- _chp.mount = function(parentEl) {
9484
- // willMount hook
9485
- if (this._hooks.willMount) this._hooks.willMount(this);
9486
-
9487
- // Save original TACO for re-renders (structural changes clone from this)
9488
- if (!this._originalTaco) {
9489
- this._originalTaco = this.taco;
9490
- }
9491
-
9492
- // Deep-clone TACO so binding annotations don't mutate original.
9493
- // Custom clone to preserve _bwWhen/_bwEach markers and their factory functions.
9494
- this.taco = this._deepCloneTaco(this._originalTaco);
9495
-
9496
- // Compile bindings (annotates TACO with data-bw_ref attributes)
9497
- this._compileBindings();
9498
-
9499
- // Prepare TACO: resolve initial binding values, evaluate when/each
9500
- this._prepareTaco(this.taco);
9501
-
9502
- // Register named actions in function registry
9503
- var self = this;
9504
- for (var actionName in this._actions) {
9505
- if (_hop.call(this._actions, actionName)) {
9506
- var registeredName = this._bwId + '_' + actionName;
9507
- (function(aName) {
9508
- bw.funcRegister(function(evt) {
9509
- self._actions[aName](self, evt);
9510
- }, registeredName);
9511
- })(actionName);
9512
- this._registeredActions.push(registeredName);
9513
- }
9514
- }
9515
-
9516
- // Wire action names in onclick etc. to dispatch strings
9517
- this._wireActions(this.taco);
9518
-
9519
- // Create DOM (strip o before createDOM to prevent double lifecycle)
9520
- var tacoForDOM = this._tacoForDOM(this.taco);
9521
- this.element = bw.createDOM(tacoForDOM);
9522
- this.element._bwComponentHandle = this;
9523
- this.element.setAttribute('data-bw_comp_id', this._bwId);
9524
-
9525
- // Restore o.render from original TACO (stripped by _tacoForDOM)
9526
- if (this.taco.o && this.taco.o.render) {
9527
- this.element._bw_render = this.taco.o.render;
9528
- }
9529
- if (this._userTag) {
9530
- this.element.classList.add(this._userTag);
9531
- }
9532
-
9533
- // Append to parent
9534
- parentEl.appendChild(this.element);
9535
-
9536
- // Collect refs from live DOM
9537
- this._collectRefs();
9538
-
9539
- // Resolve initial bindings and apply to DOM
9540
- this._resolveAndApplyAll();
9541
-
9542
- this.mounted = true;
9543
-
9544
- // Scan for child ComponentHandles and link parent/child (Bug #5)
9545
- var childEls = this.element.querySelectorAll('[data-bw_comp_id]');
9546
- for (var ci = 0; ci < childEls.length; ci++) {
9547
- var ch = childEls[ci]._bwComponentHandle;
9548
- if (ch && ch !== this && !ch._parent) {
9549
- ch._parent = this;
9550
- this._children.push(ch);
9551
- }
9552
- }
9553
-
9554
- // mounted hook (backward compat: fn.length === 2 wraps (el, state))
9555
- if (this._hooks.mounted) {
9556
- if (this._hooks.mounted.length === 2) {
9557
- this._hooks.mounted(this.element, this.getState());
9558
- } else {
9559
- this._hooks.mounted(this);
9560
- }
9561
- }
9562
-
9563
- // Invoke o.render on initial mount (if present)
9564
- if (this.element._bw_render) {
9565
- this.element._bw_render(this.element, this._state);
9566
- }
9567
- };
9568
-
9569
- /**
9570
- * Prepare TACO for initial render: resolve when/each markers.
9571
- * @private
9572
- */
9573
- _chp._prepareTaco = function(taco) {
9574
- if (!_is(taco, 'object')) return;
9575
-
9576
- if (_isA(taco.c)) {
9577
- for (var i = taco.c.length - 1; i >= 0; i--) {
9578
- var child = taco.c[i];
9579
- if (child && child._bwWhen) {
9580
- var exprStr = child.expr.replace(/^\$\{|\}$/g, '');
9581
- var val;
9582
- if (this._compile) {
9583
- try {
9584
- val = (new Function('state', 'with(state){return (' + exprStr + ');}'))(this._state);
9585
- } catch(e) { val = false; }
9586
- } else {
9587
- val = bw._evaluatePath(this._state, exprStr);
9588
- }
9589
- var branch = val ? child.branches[0] : (child.branches[1] || null);
9590
- if (branch) {
9591
- // Wrap in a container so we can track it
9592
- taco.c[i] = { t: 'span', a: { 'data-bw_when': child._refId, style: 'display:contents' }, c: branch };
9593
- } else {
9594
- taco.c[i] = { t: 'span', a: { 'data-bw_when': child._refId, style: 'display:contents' }, c: '' };
9595
- }
9596
- }
9597
- if (child && child._bwEach) {
9598
- var eachExprStr = child.expr.replace(/^\$\{|\}$/g, '');
9599
- var arr = bw._evaluatePath(this._state, eachExprStr);
9600
- var items = [];
9601
- if (_isA(arr)) {
9602
- for (var j = 0; j < arr.length; j++) {
9603
- items.push(child.factory(arr[j], j));
9604
- }
9605
- }
9606
- taco.c[i] = { t: 'span', a: { 'data-bw_each': child._refId, style: 'display:contents' }, c: items };
9607
- }
9608
- if (_is(taco.c[i], 'object') && taco.c[i].t) {
9609
- this._prepareTaco(taco.c[i]);
9610
- }
9611
- }
9612
- } else if (_is(taco.c, 'object') && taco.c.t) {
9613
- this._prepareTaco(taco.c);
9614
- }
9615
- };
9616
-
9617
- /**
9618
- * Wire action name strings (in onclick etc.) to dispatch function calls.
9619
- * @private
9620
- */
9621
- _chp._wireActions = function(taco) {
9622
- if (!_is(taco, 'object') || !taco.t) return;
9623
- if (taco.a) {
9624
- for (var key in taco.a) {
9625
- if (!_hop.call(taco.a, key)) continue;
9626
- if (key.startsWith('on') && _is(taco.a[key], 'string')) {
9627
- var actionName = taco.a[key];
9628
- if (actionName in this._actions) {
9629
- var registeredName = this._bwId + '_' + actionName;
9630
- // Replace string with actual function for createDOM event binding
9631
- (function(rName) {
9632
- taco.a[key] = function(evt) {
9633
- bw.funcGetById(rName)(evt);
9634
- };
9635
- })(registeredName);
9636
- }
9637
- }
9638
- }
9639
- }
9640
- if (_isA(taco.c)) {
9641
- for (var i = 0; i < taco.c.length; i++) {
9642
- this._wireActions(taco.c[i]);
9643
- }
9644
- } else if (_is(taco.c, 'object') && taco.c.t) {
9645
- this._wireActions(taco.c);
9646
- }
9647
- };
9648
-
9649
- /**
9650
- * Deep-clone a TACO tree, preserving _bwWhen/_bwEach markers and their factories.
9651
- * @private
9652
- */
9653
- _chp._deepCloneTaco = function(taco) {
9654
- if (taco == null) return taco;
9655
- // Preserve _bwWhen / _bwEach markers (contain functions)
9656
- if (taco._bwWhen) {
9657
- return { _bwWhen: true, expr: taco.expr, branches: [
9658
- this._deepCloneTaco(taco.branches[0]),
9659
- taco.branches[1] ? this._deepCloneTaco(taco.branches[1]) : null
9660
- ], _refId: taco._refId };
9661
- }
9662
- if (taco._bwEach) {
9663
- return { _bwEach: true, expr: taco.expr, factory: taco.factory, _refId: taco._refId };
9664
- }
9665
- if (!_is(taco, 'object') || !taco.t) return taco;
9666
- var result = { t: taco.t };
9667
- if (taco.a) {
9668
- result.a = {};
9669
- for (var k in taco.a) {
9670
- if (_hop.call(taco.a, k)) result.a[k] = taco.a[k];
9671
- }
9672
- }
9673
- if (taco.c != null) {
9674
- if (_isA(taco.c)) {
9675
- result.c = taco.c.map(function(child) { return this._deepCloneTaco(child); }.bind(this));
9676
- } else if (_is(taco.c, 'object')) {
9677
- result.c = this._deepCloneTaco(taco.c);
9678
- } else {
9679
- result.c = taco.c;
9680
- }
9681
- }
9682
- if (taco.o) result.o = taco.o; // Keep o reference (not deep-cloned; hooks are functions)
9683
- return result;
9684
- };
9685
-
9686
- /**
9687
- * Create a copy of TACO suitable for createDOM (strips o to prevent double lifecycle).
9688
- * @private
9689
- */
9690
- _chp._tacoForDOM = function(taco) {
9691
- if (!_is(taco, 'object') || !taco.t) return taco;
9692
- var result = { t: taco.t };
9693
- if (taco.a) result.a = taco.a;
9694
- if (taco.c != null) {
9695
- if (_isA(taco.c)) {
9696
- result.c = taco.c.map(function(child) { return this._tacoForDOM(child); }.bind(this));
9697
- } else if (_is(taco.c, 'object') && taco.c.t) {
9698
- result.c = this._tacoForDOM(taco.c);
9699
- } else {
9700
- result.c = taco.c;
9701
- }
9702
- }
9703
- // Intentionally strip o (no mounted/unmount/state/render on sub-elements)
9704
- if (taco.o && (taco.o.mounted || taco.o.render || taco.o.unmount)) {
9705
- _cw('bw: _tacoForDOM stripped o.mounted/render/unmount from child <' + taco.t +
9706
- '>. Use onclick attribute or bw.component() for child interactivity.');
9707
- }
9708
- return result;
9709
- };
9710
-
9711
- /**
9712
- * Unmount: remove from DOM, deactivate, preserve state for re-mount.
9713
- */
9714
- _chp.unmount = function() {
9715
- if (!this.mounted) return;
9716
-
9717
- // unmount hook
9718
- if (this._hooks.unmount) {
9719
- this._hooks.unmount(this);
9720
- }
9721
-
9722
- // Remove DOM event listeners
9723
- for (var i = 0; i < this._eventListeners.length; i++) {
9724
- var l = this._eventListeners[i];
9725
- if (this.element) {
9726
- this.element.removeEventListener(l.event, l.handler);
9727
- }
9728
- }
9729
- this._eventListeners = [];
9730
-
9731
- // Unsubscribe pub/sub
9732
- for (var j = 0; j < this._subs.length; j++) {
9733
- this._subs[j]();
9734
- }
9735
- this._subs = [];
9736
-
9737
- // Remove from DOM
9738
- if (this.element && this.element.parentNode) {
9739
- this.element.parentNode.removeChild(this.element);
9740
- }
9741
-
9742
- this.mounted = false;
9743
- // State preserved — can re-mount
9744
- };
9745
-
9746
- /**
9747
- * Destroy: unmount + clear state + unregister actions.
9748
- */
9749
- _chp.destroy = function() {
9750
- // willDestroy hook
9751
- if (this._hooks.willDestroy) {
9752
- this._hooks.willDestroy(this);
9753
- }
9754
-
9755
- // Cascade destroy to children depth-first (Bug #5)
9756
- for (var ci = this._children.length - 1; ci >= 0; ci--) {
9757
- this._children[ci].destroy();
9758
- }
9759
- this._children = [];
9760
- if (this._parent) {
9761
- var idx = this._parent._children.indexOf(this);
9762
- if (idx >= 0) this._parent._children.splice(idx, 1);
9763
- this._parent = null;
9764
- }
9765
-
9766
- this.unmount();
9767
-
9768
- // Unregister actions from function registry
9769
- for (var i = 0; i < this._registeredActions.length; i++) {
9770
- bw.funcUnregister(this._registeredActions[i]);
9771
- }
9772
- this._registeredActions = [];
9773
-
9774
- // Clear state
9775
- this._state = {};
9776
- this._bindings = [];
9777
- this._bw_refs = {};
9778
- this._prevValues = {};
9779
- this._dirtyKeys = {};
9780
- if (this.element) {
9781
- delete this.element._bwComponentHandle;
9782
- this.element = null;
9783
- }
9784
- };
9785
-
9786
- // ── Flush & Binding Resolution ──
9787
-
9788
- /**
9789
- * Flush dirty state: resolve changed bindings and apply to DOM.
9790
- * @private
9791
- */
9792
- _chp._flush = function() {
9793
- this._scheduled = false;
9794
- var changedKeys = _keys(this._dirtyKeys);
9795
- this._dirtyKeys = {};
9796
- if (changedKeys.length === 0 || !this.mounted) return;
9797
-
9798
- // Factory rebuild: if a BCCL factory exists and changed keys overlap factory props,
9799
- // rebuild the TACO from the factory with merged state (Bug #6)
9800
- if (this._factory) {
9801
- var rebuildNeeded = false;
9802
- for (var fi = 0; fi < changedKeys.length; fi++) {
9803
- if (_hop.call(this._factory.props, changedKeys[fi])) {
9804
- rebuildNeeded = true; break;
9805
- }
9806
- }
9807
- if (rebuildNeeded) {
9808
- var merged = {};
9809
- for (var mk in this._factory.props) if (_hop.call(this._factory.props, mk)) merged[mk] = this._factory.props[mk];
9810
- for (var sk in this._state) if (_hop.call(this._state, sk)) merged[sk] = this._state[sk];
9811
- this._factory.props = merged;
9812
- var newTaco = bw.make(this._factory.type, merged);
9813
- newTaco._bwFactory = this._factory;
9814
- this.taco = newTaco;
9815
- this._originalTaco = this._deepCloneTaco(newTaco);
9816
- this._render();
9817
- if (this._hooks.onUpdate) this._hooks.onUpdate(this, changedKeys);
9818
- return;
9819
- }
9820
- }
9821
-
9822
- // willUpdate hook
9823
- if (this._hooks.willUpdate) {
9824
- this._hooks.willUpdate(this, changedKeys);
9825
- }
9826
-
9827
- // Check if any structural bindings are affected
9828
- var needsFullRender = false;
9829
- for (var i = 0; i < this._bindings.length; i++) {
9830
- var b = this._bindings[i];
9831
- if (b.type === 'structural') {
9832
- for (var j = 0; j < b.deps.length; j++) {
9833
- if (changedKeys.indexOf(b.deps[j]) >= 0) {
9834
- needsFullRender = true;
9835
- break;
9836
- }
9837
- }
9838
- if (needsFullRender) break;
9839
- }
9840
- }
9841
-
9842
- if (needsFullRender) {
9843
- this._render();
9844
- } else {
9845
- var patches = this._resolveBindings(changedKeys);
9846
- this._applyPatches(patches);
9847
- }
9848
-
9849
- // onUpdate hook
9850
- if (this._hooks.onUpdate) {
9851
- this._hooks.onUpdate(this, changedKeys);
9852
- }
9853
- };
9854
-
9855
- /**
9856
- * Resolve bindings whose deps intersect with changedKeys.
9857
- * Returns list of patches to apply.
9858
- * @private
9859
- */
9860
- _chp._resolveBindings = function(changedKeys) {
9861
- var patches = [];
9862
- for (var i = 0; i < this._bindings.length; i++) {
9863
- var b = this._bindings[i];
9864
- if (b.type === 'structural') continue;
9865
-
9866
- // Check if any dep matches
9867
- var affected = false;
9868
- for (var j = 0; j < b.deps.length; j++) {
9869
- if (changedKeys.indexOf(b.deps[j]) >= 0) {
9870
- affected = true;
9871
- break;
9872
- }
9873
- }
9874
- if (!affected) continue;
9875
-
9876
- // Evaluate
9877
- var newVal = bw._resolveTemplate(b.template, this._state, this._compile);
9878
- var prevKey = b.refId + '_' + (b.attrName || 'content');
9879
- if (this._prevValues[prevKey] !== newVal) {
9880
- this._prevValues[prevKey] = newVal;
9881
- patches.push({
9882
- refId: b.refId,
9883
- type: b.type,
9884
- attrName: b.attrName,
9885
- value: newVal
9886
- });
9887
- }
9888
- }
9889
- return patches;
9890
- };
9891
-
9892
- /**
9893
- * Apply patches to DOM.
9894
- * @private
9895
- */
9896
- _chp._applyPatches = function(patches) {
9897
- for (var i = 0; i < patches.length; i++) {
9898
- var p = patches[i];
9899
- var el = this._bw_refs[p.refId];
9900
- if (!el) {
9901
- if (bw.debug) _cw('bw.debug: _applyPatches — ref "' + p.refId + '" not found in DOM');
9902
- continue;
9903
- }
9904
- if (p.type === 'content') {
9905
- el.textContent = p.value;
9906
- } else if (p.type === 'attribute') {
9907
- if (p.attrName === 'class') {
9908
- el.className = p.value;
9909
- } else {
9910
- el.setAttribute(p.attrName, p.value);
9911
- }
9912
- }
9913
- }
9914
- };
9915
-
9916
- /**
9917
- * Resolve all bindings and apply (used for initial render).
9918
- * @private
9919
- */
9920
- _chp._resolveAndApplyAll = function() {
9921
- var patches = [];
9922
- for (var i = 0; i < this._bindings.length; i++) {
9923
- var b = this._bindings[i];
9924
- if (b.type === 'structural') continue;
9925
-
9926
- var newVal = bw._resolveTemplate(b.template, this._state, this._compile);
9927
- var prevKey = b.refId + '_' + (b.attrName || 'content');
9928
- this._prevValues[prevKey] = newVal;
9929
- patches.push({
9930
- refId: b.refId,
9931
- type: b.type,
9932
- attrName: b.attrName,
9933
- value: newVal
9934
- });
9935
- }
9936
- this._applyPatches(patches);
9937
- };
9938
-
9939
- /**
9940
- * Full re-render for structural changes (when/each branch switches).
9941
- * @private
9942
- */
9943
- _chp._render = function() {
9944
- if (!this.element || !this.element.parentNode) return;
9945
- var parent = this.element.parentNode;
9946
- var nextSibling = this.element.nextSibling;
9947
-
9948
- // Remove old DOM
9949
- parent.removeChild(this.element);
9950
-
9951
- // Re-prepare TACO with current state (deep clone preserving functions)
9952
- this.taco = this._deepCloneTaco(this._originalTaco || this.taco);
9953
-
9954
- // Re-compile bindings and prepare
9955
- this._compileBindings();
9956
- this._prepareTaco(this.taco);
9957
- this._wireActions(this.taco);
9958
-
9959
- var tacoForDOM = this._tacoForDOM(this.taco);
9960
- this.element = bw.createDOM(tacoForDOM);
9961
- this.element._bwComponentHandle = this;
9962
- this.element.setAttribute('data-bw_comp_id', this._bwId);
9963
-
9964
- // Re-insert at same position
9965
- if (nextSibling) {
9966
- parent.insertBefore(this.element, nextSibling);
9967
- } else {
9968
- parent.appendChild(this.element);
9969
- }
9970
-
9971
- // Re-collect refs and apply all bindings
9972
- this._collectRefs();
9973
- this._resolveAndApplyAll();
9974
- };
9975
-
9976
- // ── Event & Pub/Sub Methods ──
9977
-
9978
- /**
9979
- * Add a DOM event listener on the component's root element.
9980
- * @param {string} event - Event name (e.g., 'click')
9981
- * @param {Function} handler - Event handler
9982
- */
9983
- _chp.on = function(event, handler) {
9984
- if (this.element) {
9985
- this.element.addEventListener(event, handler);
9986
- }
9987
- this._eventListeners.push({ event: event, handler: handler });
9988
- };
9989
-
9990
- /**
9991
- * Remove a DOM event listener.
9992
- * @param {string} event - Event name
9993
- * @param {Function} handler - Handler to remove
9994
- */
9995
- _chp.off = function(event, handler) {
9996
- if (this.element) {
9997
- this.element.removeEventListener(event, handler);
9998
- }
9999
- this._eventListeners = this._eventListeners.filter(function(l) {
10000
- return !(l.event === event && l.handler === handler);
10001
- });
10002
- };
10003
-
10004
- /**
10005
- * Subscribe to a pub/sub topic. Lifecycle-tied: auto-unsubs on destroy.
10006
- * @param {string} topic - Topic name
10007
- * @param {Function} handler - Handler function
10008
- * @returns {Function} Unsubscribe function
10009
- */
10010
- _chp.sub = function(topic, handler) {
10011
- var unsub = bw.sub(topic, handler);
10012
- this._subs.push(unsub);
10013
- return unsub;
10014
- };
10015
-
10016
- /**
10017
- * Call a named action.
10018
- * @param {string} name - Action name
10019
- * @param {...*} args - Arguments passed after comp
10020
- */
10021
- _chp.action = function(name) {
10022
- var fn = this._actions[name];
10023
- if (!fn) {
10024
- _cw('ComponentHandle.action: unknown action "' + name + '"');
10025
- return;
10026
- }
10027
- var args = [this].concat(Array.prototype.slice.call(arguments, 1));
10028
- return fn.apply(null, args);
10029
- };
10030
-
10031
- /**
10032
- * querySelector within the component's DOM.
10033
- * @param {string} sel - CSS selector
10034
- * @returns {Element|null}
10035
- */
10036
- _chp.select = function(sel) {
10037
- return this.element ? this.element.querySelector(sel) : null;
10038
- };
10039
-
10040
- /**
10041
- * querySelectorAll within the component's DOM.
10042
- * @param {string} sel - CSS selector
10043
- * @returns {Element[]}
10044
- */
10045
- _chp.selectAll = function(sel) {
10046
- if (!this.element) return [];
10047
- return Array.prototype.slice.call(this.element.querySelectorAll(sel));
10048
- };
10049
-
10050
- /**
10051
- * Tag this component with a user-defined ID for addressing via bw.message().
10052
- * The tag is added as a CSS class on the root element (DOM IS the registry).
10053
- * @param {string} tag - User-defined identifier (e.g. 'dashboard_prod_east')
10054
- * @returns {ComponentHandle} this (for chaining)
10055
- */
10056
- _chp.userTag = function(tag) {
10057
- this._userTag = tag;
10058
- if (this.element) {
10059
- this.element.classList.add(tag);
10060
- }
10061
- return this;
10062
- };
10063
-
10064
- // Expose ComponentHandle on bw (for testing and advanced use)
10065
- bw._ComponentHandle = ComponentHandle;
10066
-
10067
- // ===================================================================================
10068
- // Control Flow Helpers
10069
- // ===================================================================================
10070
-
10071
- /**
10072
- * Conditional rendering helper.
10073
- * Returns a marker object that ComponentHandle detects during binding compilation.
10074
- * In static contexts (bw.html with state), evaluates immediately.
10075
- *
10076
- * @param {string} expr - Expression string like '${loggedIn}'
10077
- * @param {Object} tacoTrue - TACO to render when truthy
10078
- * @param {Object} [tacoFalse] - TACO to render when falsy
10079
- * @returns {Object} Marker object with _bwWhen flag
10080
- * @category Component
10081
- */
10082
- bw.when = function(expr, tacoTrue, tacoFalse) {
10083
- return { _bwWhen: true, expr: expr, branches: [tacoTrue, tacoFalse || null] };
10084
- };
10085
-
10086
- /**
10087
- * List rendering helper.
10088
- * Returns a marker object that ComponentHandle detects during binding compilation.
10089
- *
10090
- * @param {string} expr - Expression string like '${items}'
10091
- * @param {Function} fn - Factory function(item, index) returning TACO
10092
- * @returns {Object} Marker object with _bwEach flag
10093
- * @category Component
10094
- */
10095
- bw.each = function(expr, fn) {
10096
- return { _bwEach: true, expr: expr, factory: fn };
10097
- };
10098
-
10099
- // ===================================================================================
10100
- // bw.component() — Factory for ComponentHandle
10101
- // ===================================================================================
10102
-
10103
- /**
10104
- * Create a ComponentHandle from a TACO definition.
10105
- * The returned handle has .get(), .set(), .mount(), .destroy(), etc.
10106
- *
10107
- * @param {Object} taco - TACO definition with {t, a, c, o}
10108
- * @returns {ComponentHandle} Reactive component handle
10109
- * @category Component
10110
- * @see bw.DOM
10111
- * @example
10112
- * var counter = bw.component({
10113
- * t: 'div', c: [{ t: 'h3', c: 'Count: ${count}' }],
10114
- * o: { state: { count: 0 } }
10115
- * });
10116
- * bw.DOM('#app', counter);
10117
- * counter.set('count', 42); // DOM auto-updates
10118
- */
10119
- bw.component = function(taco) {
10120
- return new ComponentHandle(taco);
10121
- };
10122
-
10123
- // ===================================================================================
10124
- // bw.message() — SendMessage() for the web
10125
- // ===================================================================================
10126
-
10127
- /**
10128
- * Dispatch a message to a component by UUID or user tag.
10129
- * Finds the component's DOM element, looks up its ComponentHandle,
10130
- * and calls the named method. This is the bitwrench equivalent of
10131
- * Win32 SendMessage(hwnd, msg, wParam, lParam).
10132
- *
10133
- * @param {string} target - Component UUID (bw_uuid_*), comp ID (data-bw_comp_id), or user tag (CSS class)
10134
- * @param {string} action - Method name to call on the component
10135
- * @param {*} data - Data to pass to the method
10136
- * @returns {boolean} True if message was dispatched successfully
10137
- * @category Component
10138
- * @example
10139
- * // Tag a component
10140
- * myDash.userTag('dashboard_prod');
10141
- * // Dispatch locally
10142
- * bw.message('dashboard_prod', 'addAlert', { severity: 'warning', text: 'CPU spike' });
10143
- * // Or from SSE handler:
10144
- * es.onmessage = function(e) {
10145
- * var msg = JSON.parse(e.data);
10146
- * bw.message(msg.target, msg.action, msg.data);
10147
- * };
10148
- */
10149
- bw.message = function(target, action, data) {
10150
- // Try bw._el() first (handles UUID class, nodeMap cache, getElementById)
10151
- var el = bw._el(target);
10152
- // Then try data-bw_comp_id attribute
10153
- if (!el || !el._bwComponentHandle) {
10154
- el = bw.$('[data-bw_comp_id="' + target + '"]')[0];
10155
- }
10156
- // Then try CSS class (user tag)
10157
- if (!el || !el._bwComponentHandle) {
10158
- el = bw.$('.' + target)[0];
10159
- }
10160
- if (!el || !el._bwComponentHandle) return false;
10161
- var comp = el._bwComponentHandle;
10162
- if (!_is(comp[action], 'function')) {
10163
- _cw('bw.message: unknown action "' + action + '" on component ' + target);
10164
- return false;
10165
- }
10166
- comp[action](data);
10167
- return true;
10168
- };
10169
-
10170
- // ===================================================================================
10171
- // bw.apply() / bw.parseJSONFlex() — Server-driven UI protocol
10172
- // ===================================================================================
10173
-
10174
- /**
10175
- * Registry of named functions sent via register messages.
10176
- * Populated by bw.apply({ type: 'register', name, body }).
10177
- * Invoked by bw.apply({ type: 'call', name, args }).
10178
- * @private
10179
- */
10180
- bw._clientFunctions = {};
10181
-
10182
- /**
10183
- * Whether exec messages are allowed. Set by bwclient connect opts.allowExec.
10184
- * Default false — exec messages are rejected unless explicitly opted in.
10185
- * @private
10186
- */
10187
- bw._allowExec = false;
10188
-
10189
- /**
10190
- * Parse a bwserve protocol message string, supporting both strict JSON
10191
- * and r-prefixed relaxed JSON (single-quoted strings, trailing commas).
10192
- *
10193
- * The r-prefix format is designed for C/C++ string literals where
10194
- * double-quote escaping is painful. The parser is a state machine
10195
- * that walks character by character — not a regex replace.
10196
- *
10197
- * Escaping: apostrophes inside single-quoted values must be escaped
10198
- * with backslash: r{'name':'Barry\'s room'}
10199
- *
10200
- * @param {string} str - JSON or r-prefixed relaxed JSON string
10201
- * @returns {Object} Parsed message object
10202
- * @throws {SyntaxError} If the string is not valid JSON or relaxed JSON
10203
- * @category Core
9142
+ * Parse a bwserve protocol message string, supporting both strict JSON
9143
+ * and r-prefixed relaxed JSON (single-quoted strings, trailing commas).
9144
+ *
9145
+ * The r-prefix format is designed for C/C++ string literals where
9146
+ * double-quote escaping is painful. The parser is a state machine
9147
+ * that walks character by character — not a regex replace.
9148
+ *
9149
+ * Escaping: apostrophes inside single-quoted values must be escaped
9150
+ * with backslash: r{'name':'Barry\'s room'}
9151
+ *
9152
+ * @param {string} str - JSON or r-prefixed relaxed JSON string
9153
+ * @returns {Object} Parsed message object
9154
+ * @throws {SyntaxError} If the string is not valid JSON or relaxed JSON
9155
+ * @category Core
10204
9156
  */
10205
9157
  bw.parseJSONFlex = function(str) {
10206
9158
  str = (str || '').trim();
@@ -10389,132 +9341,29 @@ bw.apply = function(msg) {
10389
9341
  // ===================================================================================
10390
9342
 
10391
9343
  /**
10392
- * Inspect a component's state, bindings, methods, and metadata.
10393
- * Works with DOM elements, CSS selectors, or ComponentHandle objects.
10394
- * Returns the ComponentHandle for console chaining.
9344
+ * Inspect a DOM element's bitwrench state, handle methods, and metadata.
9345
+ * Works with DOM elements or CSS selectors.
10395
9346
  *
10396
- * @param {string|Element|ComponentHandle} target - Selector, element, or handle
10397
- * @returns {ComponentHandle|null} The component handle, or null if not found
9347
+ * @param {string|Element} target - Selector or DOM element
9348
+ * @returns {Element|null} The element, or null if not found
10398
9349
  * @category Component
10399
9350
  * @example
10400
- * // In browser console, click element in Elements panel then:
9351
+ * bw.inspect('#my-carousel');
10401
9352
  * bw.inspect($0);
10402
- * // Or by selector:
10403
- * var h = bw.inspect('#my-dashboard');
10404
- * h.set('count', 99); // chain from returned handle
10405
9353
  */
10406
9354
  bw.inspect = function(target) {
10407
- var el = target;
10408
- var comp;
10409
- if (target && target._bwComponent === true) {
10410
- el = target.element;
10411
- comp = target;
10412
- } else {
10413
- if (_is(target, 'string')) {
10414
- el = bw.$(target)[0];
10415
- }
10416
- if (!el) {
10417
- _cw('bw.inspect: element not found');
10418
- return null;
10419
- }
10420
- comp = el._bwComponentHandle;
10421
- }
10422
- if (!comp) {
10423
- _cl('bw.inspect: no ComponentHandle on this element');
10424
- _cl(' Tag:', el.tagName);
10425
- _cl(' Classes:', el.className);
10426
- _cl(' _bw_state:', el._bw_state || '(none)');
10427
- return null;
10428
- }
10429
- var deps = comp._bindings.reduce(function(s, b) {
10430
- return s.concat(b.deps || []);
10431
- }, []).filter(function(v, i, a) { return a.indexOf(v) === i; });
10432
- console.group('Component: ' + comp._bwId);
10433
- _cl('State:', comp._state);
10434
- _cl('Bindings:', comp._bindings.length, '(deps:', deps, ')');
10435
- _cl('Methods:', _keys(comp._methods));
10436
- _cl('Actions:', _keys(comp._actions));
10437
- _cl('User tag:', comp._userTag || '(none)');
10438
- _cl('Mounted:', comp.mounted);
10439
- _cl('Element:', comp.element);
9355
+ var el = _is(target, 'string') ? bw.$(target)[0] : target;
9356
+ if (!el) { _cw('bw.inspect: element not found'); return null; }
9357
+ console.group('Element: ' + (bw.getUUID(el) || el.id || el.tagName));
9358
+ _cl('State:', el._bw_state || '(none)');
9359
+ _cl('Handle:', el.bw ? _keys(el.bw) : '(none)');
9360
+ _cl('Classes:', el.className);
9361
+ _cl('Refs:', el._bw_refs || '(none)');
10440
9362
  console.groupEnd();
10441
- return comp;
9363
+ return el;
10442
9364
  };
10443
9365
 
10444
- // ===================================================================================
10445
- // bw.compile() — Pre-compile TACO into optimized factory
10446
- // ===================================================================================
10447
-
10448
- /**
10449
- * Pre-compile a TACO definition into a factory function.
10450
- * The factory produces ComponentHandles with pre-compiled binding evaluators.
10451
- *
10452
- * Phase 1: validates API surface. Template cloning optimization deferred.
10453
- *
10454
- * @param {Object} taco - TACO definition
10455
- * @returns {Function} Factory function(initialState?) → ComponentHandle
10456
- * @category Component
10457
- */
10458
- bw.compile = function(taco) {
10459
- // Pre-extract all binding expressions
10460
- var precompiled = [];
10461
- function walkExpressions(node) {
10462
- if (!_is(node, 'object')) return;
10463
- if (_is(node.c, 'string') && node.c.indexOf('${') >= 0) {
10464
- var parsed = bw._parseBindings(node.c);
10465
- for (var i = 0; i < parsed.length; i++) {
10466
- try {
10467
- precompiled.push({
10468
- expr: parsed[i].expr,
10469
- fn: new Function('state', 'with(state){return (' + parsed[i].expr + ');}')
10470
- });
10471
- } catch(e) {
10472
- precompiled.push({ expr: parsed[i].expr, fn: function() { return ''; } });
10473
- }
10474
- }
10475
- }
10476
- if (node.a) {
10477
- for (var key in node.a) {
10478
- if (_hop.call(node.a, key)) {
10479
- var v = node.a[key];
10480
- if (_is(v, 'string') && v.indexOf('${') >= 0) {
10481
- var parsed2 = bw._parseBindings(v);
10482
- for (var j = 0; j < parsed2.length; j++) {
10483
- try {
10484
- precompiled.push({
10485
- expr: parsed2[j].expr,
10486
- fn: new Function('state', 'with(state){return (' + parsed2[j].expr + ');}')
10487
- });
10488
- } catch(e2) {
10489
- precompiled.push({ expr: parsed2[j].expr, fn: function() { return ''; } });
10490
- }
10491
- }
10492
- }
10493
- }
10494
- }
10495
- }
10496
- if (_isA(node.c)) {
10497
- for (var k = 0; k < node.c.length; k++) walkExpressions(node.c[k]);
10498
- } else if (_is(node.c, 'object') && node.c.t) {
10499
- walkExpressions(node.c);
10500
- }
10501
- }
10502
- walkExpressions(taco);
10503
-
10504
- return function(initialState) {
10505
- var handle = new ComponentHandle(taco);
10506
- handle._compile = true;
10507
- handle._precompiledBindings = precompiled;
10508
- if (initialState) {
10509
- for (var k in initialState) {
10510
- if (_hop.call(initialState, k)) {
10511
- handle._state[k] = initialState[k];
10512
- }
10513
- }
10514
- }
10515
- return handle;
10516
- };
10517
- };
9366
+ bw.compile = function() { throw new Error('bw.compile() removed in v2.0.19. Use o.handle/o.slots on TACO options instead.'); };
10518
9367
 
10519
9368
  /**
10520
9369
  * Generate CSS from JavaScript objects.
@@ -11740,8 +10589,8 @@ bw.render = function(element, position, taco) {
11740
10589
  };
11741
10590
  }
11742
10591
 
11743
- // Generate unique ID if not provided
11744
- const componentId = taco.o?.id || bw.uuid();
10592
+ // Generate unique UUID class if not provided
10593
+ const componentId = taco.o?.id || bw.uuid('uuid');
11745
10594
 
11746
10595
  // Create DOM element
11747
10596
  let domElement;
@@ -11756,9 +10605,10 @@ bw.render = function(element, position, taco) {
11756
10605
  };
11757
10606
  }
11758
10607
 
11759
- // Add component ID to element
11760
- domElement.setAttribute('data-bw_id', componentId);
11761
-
10608
+ // Add component ID as class + lifecycle marker
10609
+ domElement.classList.add(componentId);
10610
+ domElement.classList.add(_BW_LC);
10611
+
11762
10612
  // Insert into DOM based on position
11763
10613
  try {
11764
10614
  switch(position) {
@@ -11832,7 +10682,8 @@ bw.render = function(element, position, taco) {
11832
10682
 
11833
10683
  // Re-render
11834
10684
  const newElement = bw.createDOM(this._taco);
11835
- newElement.setAttribute('data-bw_id', componentId);
10685
+ newElement.classList.add(componentId);
10686
+ newElement.classList.add(_BW_LC);
11836
10687
 
11837
10688
  // Replace in DOM
11838
10689
  parent.replaceChild(newElement, this.element);
@@ -12019,13 +10870,12 @@ bw.BCCL = BCCL;
12019
10870
  // Variant class helper: bw.variantClass('primary') → 'bw_primary'
12020
10871
  bw.variantClass = variantClass;
12021
10872
 
12022
- // Create functions that return handles (plain renderComponent, no Handle overlay)
10873
+ // Create functions that return DOM elements (createCard, createTable, etc.)
12023
10874
  Object.entries(components).forEach(([name, fn]) => {
12024
10875
  if (name.startsWith('make')) {
12025
- const createName = 'create' + name.substring(4); // createCard, createTable, etc.
10876
+ const createName = 'create' + name.substring(4);
12026
10877
  bw[createName] = function(props) {
12027
- const taco = fn(props);
12028
- return bw.renderComponent(taco);
10878
+ return bw.createDOM(fn(props));
12029
10879
  };
12030
10880
  }
12031
10881
  });