bitwrench 2.0.18 → 2.0.20

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