bitwrench 2.0.17 → 2.0.19

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 (72) hide show
  1. package/README.md +169 -75
  2. package/dist/bitwrench-bccl.cjs.js +228 -55
  3. package/dist/bitwrench-bccl.cjs.min.js +3 -3
  4. package/dist/bitwrench-bccl.esm.js +228 -55
  5. package/dist/bitwrench-bccl.esm.min.js +3 -3
  6. package/dist/bitwrench-bccl.umd.js +228 -55
  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 +1190 -2348
  19. package/dist/bitwrench-lean.cjs.min.js +20 -20
  20. package/dist/bitwrench-lean.es5.js +1285 -2551
  21. package/dist/bitwrench-lean.es5.min.js +18 -18
  22. package/dist/bitwrench-lean.esm.js +1190 -2348
  23. package/dist/bitwrench-lean.esm.min.js +20 -20
  24. package/dist/bitwrench-lean.umd.js +1190 -2348
  25. package/dist/bitwrench-lean.umd.min.js +20 -20
  26. package/dist/bitwrench-util-css.cjs.js +236 -0
  27. package/dist/bitwrench-util-css.cjs.min.js +22 -0
  28. package/dist/bitwrench-util-css.es5.js +414 -0
  29. package/dist/bitwrench-util-css.es5.min.js +21 -0
  30. package/dist/bitwrench-util-css.esm.js +230 -0
  31. package/dist/bitwrench-util-css.esm.min.js +21 -0
  32. package/dist/bitwrench-util-css.umd.js +242 -0
  33. package/dist/bitwrench-util-css.umd.min.js +21 -0
  34. package/dist/bitwrench.cjs.js +1404 -2388
  35. package/dist/bitwrench.cjs.min.js +21 -21
  36. package/dist/bitwrench.css +503 -132
  37. package/dist/bitwrench.es5.js +1588 -2659
  38. package/dist/bitwrench.es5.min.js +19 -19
  39. package/dist/bitwrench.esm.js +1405 -2389
  40. package/dist/bitwrench.esm.min.js +21 -21
  41. package/dist/bitwrench.min.css +1 -1
  42. package/dist/bitwrench.umd.js +1404 -2388
  43. package/dist/bitwrench.umd.min.js +21 -21
  44. package/dist/builds.json +214 -104
  45. package/dist/bwserve.cjs.js +514 -68
  46. package/dist/bwserve.esm.js +513 -69
  47. package/dist/sri.json +46 -36
  48. package/package.json +6 -3
  49. package/readme.html +183 -85
  50. package/src/bitwrench-bccl-entry.js +3 -4
  51. package/src/bitwrench-bccl.js +224 -50
  52. package/src/bitwrench-code-edit.js +6 -8
  53. package/src/bitwrench-color-utils.js +31 -9
  54. package/src/bitwrench-debug.js +245 -0
  55. package/src/bitwrench-esm-entry.js +11 -0
  56. package/src/bitwrench-styles.js +474 -240
  57. package/src/bitwrench-util-css.js +229 -0
  58. package/src/bitwrench.js +689 -2042
  59. package/src/bwserve/attach.js +57 -0
  60. package/src/bwserve/bwclient.js +141 -0
  61. package/src/bwserve/bwshell.js +102 -0
  62. package/src/bwserve/client.js +151 -1
  63. package/src/bwserve/index.js +127 -28
  64. package/src/cli/attach.js +587 -0
  65. package/src/cli/convert.js +2 -5
  66. package/src/cli/index.js +7 -0
  67. package/src/cli/inject.js +1 -1
  68. package/src/cli/serve.js +185 -5
  69. package/src/generate-css.js +11 -4
  70. package/src/vendor/html2canvas.min.js +20 -0
  71. package/src/version.js +3 -3
  72. package/src/bwserve/shell.js +0 -106
@@ -1,4 +1,4 @@
1
- /*! bitwrench-lean v2.0.17 | BSD-2-Clause | https://deftio.github.com/bitwrench/pages */
1
+ /*! bitwrench-lean v2.0.19 | BSD-2-Clause | https://deftio.github.com/bitwrench/pages */
2
2
  (function (global, factory) {
3
3
  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
4
4
  typeof define === 'function' && define.amd ? define(factory) :
@@ -12,14 +12,14 @@
12
12
  */
13
13
 
14
14
  const VERSION_INFO = {
15
- version: '2.0.17',
15
+ version: '2.0.19',
16
16
  name: 'bitwrench',
17
17
  description: 'A library for javascript UI functions.',
18
18
  license: 'BSD-2-Clause',
19
19
  homepage: 'https://deftio.github.com/bitwrench/pages',
20
20
  repository: 'git+https://github.com/deftio/bitwrench.git',
21
21
  author: 'manu a. chatterjee <deftio@deftio.com> (https://deftio.com/)',
22
- buildDate: '2026-03-13T23:15:10.823Z'
22
+ buildDate: '2026-03-22T19:09:32.608Z'
23
23
  };
24
24
 
25
25
  /**
@@ -313,13 +313,18 @@
313
313
  */
314
314
  function deriveShades(hex) {
315
315
  var rgb = colorParse(hex);
316
+ // For light input colors (L > 75), mixing toward white produces invisible borders.
317
+ // Darken instead so borders remain visible against light backgrounds.
318
+ var borderColor = hexToHsl(hex)[2] > 75
319
+ ? adjustLightness(hex, -18)
320
+ : mixColor(hex, '#ffffff', 0.60);
316
321
  return {
317
322
  base: hex,
318
323
  hover: adjustLightness(hex, -10),
319
324
  active: adjustLightness(hex, -15),
320
325
  light: mixColor(hex, '#ffffff', 0.85),
321
326
  darkText: adjustLightness(hex, -40),
322
- border: mixColor(hex, '#ffffff', 0.60),
327
+ border: borderColor,
323
328
  focus: 'rgba(' + rgb[0] + ',' + rgb[1] + ',' + rgb[2] + ',0.25)',
324
329
  textOn: textOnColor(hex)
325
330
  };
@@ -378,19 +383,27 @@
378
383
  alt.secondary = deriveAlternateSeed(config.secondary);
379
384
  alt.tertiary = config.tertiary ? deriveAlternateSeed(config.tertiary) : alt.primary;
380
385
 
381
- // Derive alternate surface colors from primary hue
386
+ // Derive alternate surface colors from primary hue.
387
+ // Check actual page surface brightness (not seed color brightness) to decide
388
+ // whether alternate should be dark or light. The page surface is what the
389
+ // user sees; seeds can be dark while the page is still light (default L=96).
382
390
  var priHsl = hexToHsl(config.primary);
383
391
  var h = priHsl[0];
384
- var isLight = isLightPalette(config);
392
+ var primarySurface = config.surface || hslToHex([h, 8, 96]);
393
+ var isLight = relativeLuminance(primarySurface) > 0.179;
385
394
 
386
395
  if (isLight) {
387
- // Primary is light → alternate needs dark surfaces
396
+ // Page surface is light → alternate needs dark surfaces
388
397
  alt.light = hslToHex([h, Math.min(priHsl[1], 15), 15]);
389
398
  alt.dark = hslToHex([h, 5, 88]);
399
+ alt.surface = hslToHex([h, 12, 18]);
400
+ alt.background = hslToHex([h, 10, 14]);
390
401
  } else {
391
- // Primary is dark → alternate needs light surfaces
402
+ // Page surface is dark → alternate needs light surfaces
392
403
  alt.light = hslToHex([h, Math.min(priHsl[1], 10), 96]);
393
404
  alt.dark = hslToHex([h, 10, 18]);
405
+ alt.surface = hslToHex([h, 8, 96]);
406
+ alt.background = hslToHex([h, 6, 98]);
394
407
  }
395
408
 
396
409
  // Semantic colors: harmonize toward primary, then invert for alternate
@@ -438,10 +451,18 @@
438
451
  var darkBase = config.dark || hslToHex([h, 10, 13]);
439
452
 
440
453
  // Background & surface tokens — tinted with primary hue for theme personality.
441
- // Very subtle: bg at L=98/S=6, surface at L=96/S=8.
454
+ // Saturation high enough that the hue is visible (each theme feels distinct)
455
+ // but low enough to stay neutral and readable.
442
456
  // User can override with config.background / config.surface.
443
- var bgBase = config.background || hslToHex([h, 6, 98]);
444
- var surfBase = config.surface || hslToHex([h, 8, 96]);
457
+ var bgBase = config.background || hslToHex([h, 22, 96]);
458
+ var surfBase = config.surface || hslToHex([h, 25, 94]);
459
+
460
+ // surfaceAlt: subtle background variant for striped rows, hover states, headers.
461
+ // Slightly lighter than surface in dark mode, slightly darker in light mode.
462
+ var surfHsl = hexToHsl(surfBase);
463
+ var surfAlt = surfHsl[2] <= 50
464
+ ? hslToHex([surfHsl[0], surfHsl[1], Math.min(surfHsl[2] + 8, 100)])
465
+ : hslToHex([surfHsl[0], surfHsl[1], Math.max(surfHsl[2] - 3, 0)]);
445
466
 
446
467
  var palette = {
447
468
  primary: deriveShades(config.primary),
@@ -454,7 +475,8 @@
454
475
  light: deriveShades(lightBase),
455
476
  dark: deriveShades(darkBase),
456
477
  background: bgBase,
457
- surface: surfBase
478
+ surface: surfBase,
479
+ surfaceAlt: surfAlt
458
480
  };
459
481
 
460
482
  return palette;
@@ -501,10 +523,12 @@
501
523
  5: '1.5rem', // 24px
502
524
  6: '2rem'};
503
525
 
526
+ let _S=SPACING_SCALE;
527
+
504
528
  var SPACING_PRESETS = {
505
- compact: { btn: SPACING_SCALE[1] + ' ' + SPACING_SCALE[3], card: SPACING_SCALE[3] + ' ' + SPACING_SCALE[4], alert: SPACING_SCALE[2] + ' ' + SPACING_SCALE[4], cell: SPACING_SCALE[2] + ' ' + SPACING_SCALE[3], input: SPACING_SCALE[1] + ' ' + SPACING_SCALE[3] },
506
- normal: { btn: SPACING_SCALE[2] + ' ' + SPACING_SCALE[4], card: SPACING_SCALE[5] + ' ' + SPACING_SCALE[5], alert: SPACING_SCALE[3] + ' ' + SPACING_SCALE[5], cell: SPACING_SCALE[3] + ' ' + SPACING_SCALE[4], input: SPACING_SCALE[2] + ' ' + SPACING_SCALE[3] },
507
- spacious: { btn: SPACING_SCALE[3] + ' ' + SPACING_SCALE[5], card: SPACING_SCALE[6] + ' ' + SPACING_SCALE[6], alert: SPACING_SCALE[4] + ' ' + SPACING_SCALE[5], cell: SPACING_SCALE[4] + ' ' + SPACING_SCALE[5], input: SPACING_SCALE[3] + ' ' + SPACING_SCALE[4] }
529
+ compact: { btn: _S[1] + ' ' + _S[3], card: _S[3] + ' ' + _S[4], alert: _S[2] + ' ' + _S[4], cell: _S[2] + ' ' + _S[3], input: _S[1] + ' ' + _S[3] },
530
+ normal: { btn: _S[2] + ' ' + _S[4], card: _S[5] + ' ' + _S[5], alert: _S[3] + ' ' + _S[5], cell: _S[3] + ' ' + _S[4], input: _S[2] + ' ' + _S[3] },
531
+ spacious: { btn: _S[3] + ' ' + _S[5], card: _S[6] + ' ' + _S[6], alert: _S[4] + ' ' + _S[5], cell: _S[4] + ' ' + _S[5], input: _S[3] + ' ' + _S[4] }
508
532
  };
509
533
 
510
534
  var RADIUS_PRESETS = {
@@ -616,20 +640,14 @@
616
640
  * Built-in theme presets — named color combinations
617
641
  * Each preset provides primary, secondary, and tertiary seed colors.
618
642
  */
619
- var THEME_PRESETS = {
620
- teal: { primary: '#006666', secondary: '#6c757d', tertiary: '#006666' },
621
- ocean: { primary: '#0077b6', secondary: '#90e0ef', tertiary: '#00b4d8' },
622
- sunset: { primary: '#e76f51', secondary: '#264653', tertiary: '#e9c46a' },
623
- forest: { primary: '#2d6a4f', secondary: '#95d5b2', tertiary: '#52b788' },
624
- slate: { primary: '#343a40', secondary: '#adb5bd', tertiary: '#6c757d' },
625
- rose: { primary: '#e11d48', secondary: '#fda4af', tertiary: '#fb7185' },
626
- indigo: { primary: '#4f46e5', secondary: '#a5b4fc', tertiary: '#818cf8' },
627
- amber: { primary: '#d97706', secondary: '#fbbf24', tertiary: '#f59e0b' },
628
- emerald: { primary: '#059669', secondary: '#6ee7b7', tertiary: '#34d399' },
629
- nord: { primary: '#5e81ac', secondary: '#88c0d0', tertiary: '#81a1c1' },
630
- coral: { primary: '#ef6461', secondary: '#4a7c7e', tertiary: '#e8a87c' },
631
- midnight: { primary: '#1e3a5f', secondary: '#7c8db5', tertiary: '#3d5a80' }
632
- };
643
+ var THEME_PRESETS = Object.fromEntries([
644
+ ['teal','#006666','#6c757d','#006666'],['ocean','#0077b6','#90e0ef','#00b4d8'],
645
+ ['sunset','#e76f51','#264653','#e9c46a'],['forest','#2d6a4f','#95d5b2','#52b788'],
646
+ ['slate','#343a40','#adb5bd','#6c757d'],['rose','#e11d48','#fda4af','#fb7185'],
647
+ ['indigo','#4f46e5','#a5b4fc','#818cf8'],['amber','#d97706','#fbbf24','#f59e0b'],
648
+ ['emerald','#059669','#6ee7b7','#34d399'],['nord','#5e81ac','#88c0d0','#81a1c1'],
649
+ ['coral','#ef6461','#4a7c7e','#e8a87c'],['midnight','#1e3a5f','#7c8db5','#3d5a80']
650
+ ].map(function(e) { return [e[0], {primary:e[1],secondary:e[2],tertiary:e[3]}]; }));
633
651
 
634
652
  /**
635
653
  * Resolve layout config to spacing, radius, typeScale, elevation, and motion objects.
@@ -676,6 +694,7 @@
676
694
  if (sel.includes(',')) return sel.split(',').map(function(s) { return '.' + name + ' ' + s.trim(); }).join(', ');
677
695
  return '.' + name + ' ' + sel;
678
696
  }
697
+ var _sx=scopeSelector;
679
698
 
680
699
  // =========================================================================
681
700
  // Themed CSS generators
@@ -684,12 +703,12 @@
684
703
  function generateTypographyThemed(scope, palette, layout) {
685
704
  var mot = layout.motion;
686
705
  var rules = {};
687
- rules[scopeSelector(scope, 'a')] = {
706
+ rules[_sx(scope, 'a')] = {
688
707
  'color': palette.primary.base,
689
708
  'text-decoration': 'none',
690
709
  'transition': 'color ' + mot.fast + ' ' + mot.easing
691
710
  };
692
- rules[scopeSelector(scope, 'a:hover')] = {
711
+ rules[_sx(scope, 'a:hover')] = {
693
712
  'color': palette.primary.hover,
694
713
  'text-decoration': 'underline'
695
714
  };
@@ -702,11 +721,11 @@
702
721
  var rd = layout.radius;
703
722
 
704
723
  // Base button (only when scoped — unscoped uses defaultStyles)
705
- rules[scopeSelector(scope, '.bw_btn')] = {
724
+ rules[_sx(scope, '.bw_btn')] = {
706
725
  'padding': sp.btn,
707
726
  'border-radius': rd.btn
708
727
  };
709
- rules[scopeSelector(scope, '.bw_btn:focus-visible')] = {
728
+ rules[_sx(scope, '.bw_btn:focus-visible')] = {
710
729
  'outline': '2px solid currentColor',
711
730
  'outline-offset': '2px',
712
731
  'box-shadow': '0 0 0 3px ' + palette.primary.focus
@@ -715,12 +734,12 @@
715
734
  // Variant colors handled by palette class on component root
716
735
 
717
736
  // Size variants (structural, reuse layout radius)
718
- rules[scopeSelector(scope, '.bw_btn_lg')] = {
737
+ rules[_sx(scope, '.bw_btn_lg')] = {
719
738
  'padding': '0.625rem 1.5rem',
720
739
  'font-size': '1rem',
721
740
  'border-radius': rd.btn === '50rem' ? '50rem' : (parseInt(rd.btn) + 2) + 'px'
722
741
  };
723
- rules[scopeSelector(scope, '.bw_btn_sm')] = {
742
+ rules[_sx(scope, '.bw_btn_sm')] = {
724
743
  'padding': '0.25rem 0.75rem',
725
744
  'font-size': '0.8125rem',
726
745
  'border-radius': rd.btn === '50rem' ? '50rem' : (Math.max(parseInt(rd.btn) - 1, 0)) + 'px'
@@ -734,7 +753,7 @@
734
753
  var sp = layout.spacing;
735
754
  var rd = layout.radius;
736
755
 
737
- rules[scopeSelector(scope, '.bw_alert')] = {
756
+ rules[_sx(scope, '.bw_alert')] = {
738
757
  'padding': sp.alert,
739
758
  'border-radius': rd.alert
740
759
  };
@@ -753,36 +772,36 @@
753
772
 
754
773
  var elev = layout.elevation;
755
774
  var motion = layout.motion;
756
- rules[scopeSelector(scope, '.bw_card')] = {
775
+ rules[_sx(scope, '.bw_card')] = {
757
776
  'background-color': palette.surface || '#fff',
758
777
  'border': '1px solid ' + palette.light.border,
759
778
  'border-radius': rd.card,
760
779
  'box-shadow': elev.sm,
761
780
  'transition': 'box-shadow ' + motion.normal + ' ' + motion.easing + ', transform ' + motion.normal + ' ' + motion.easing
762
781
  };
763
- rules[scopeSelector(scope, '.bw_card:hover')] = {
782
+ rules[_sx(scope, '.bw_card:hover')] = {
764
783
  'box-shadow': elev.md
765
784
  };
766
- rules[scopeSelector(scope, '.bw_card_hoverable:hover')] = {
785
+ rules[_sx(scope, '.bw_card_hoverable:hover')] = {
767
786
  'box-shadow': elev.lg
768
787
  };
769
- rules[scopeSelector(scope, '.bw_card_body')] = {
788
+ rules[_sx(scope, '.bw_card_body')] = {
770
789
  'padding': sp.card
771
790
  };
772
- rules[scopeSelector(scope, '.bw_card_header')] = {
791
+ rules[_sx(scope, '.bw_card_header')] = {
773
792
  'padding': sp.card.split(' ').map(function(v) { return (parseFloat(v) * 0.7).toFixed(3).replace(/\.?0+$/, '') + 'rem'; }).join(' '),
774
- 'background-color': palette.light.light,
793
+ 'background-color': palette.surfaceAlt,
775
794
  'border-bottom': '1px solid ' + palette.light.border
776
795
  };
777
- rules[scopeSelector(scope, '.bw_card_footer')] = {
778
- 'background-color': palette.light.light,
796
+ rules[_sx(scope, '.bw_card_footer')] = {
797
+ 'background-color': palette.surfaceAlt,
779
798
  'border-top': '1px solid ' + palette.light.border,
780
799
  'color': palette.secondary.base
781
800
  };
782
- rules[scopeSelector(scope, '.bw_card_title')] = {
801
+ rules[_sx(scope, '.bw_card_title')] = {
783
802
  'color': palette.dark.base
784
803
  };
785
- rules[scopeSelector(scope, '.bw_card_subtitle')] = {
804
+ rules[_sx(scope, '.bw_card_subtitle')] = {
786
805
  'color': palette.secondary.base
787
806
  };
788
807
 
@@ -796,55 +815,55 @@
796
815
  var sp = layout.spacing;
797
816
  var rd = layout.radius;
798
817
 
799
- rules[scopeSelector(scope, '.bw_form_control')] = {
818
+ rules[_sx(scope, '.bw_form_control')] = {
800
819
  'padding': sp.input,
801
820
  'border-radius': rd.input,
802
821
  'color': palette.dark.base,
803
822
  'background-color': palette.surface || '#fff',
804
823
  'border-color': palette.light.border
805
824
  };
806
- rules[scopeSelector(scope, '.bw_form_control:focus')] = {
825
+ rules[_sx(scope, '.bw_form_control:focus')] = {
807
826
  'border-color': palette.primary.border,
808
827
  'outline': '2px solid ' + palette.primary.base,
809
828
  'outline-offset': '-1px',
810
829
  'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus
811
830
  };
812
- rules[scopeSelector(scope, '.bw_form_control::placeholder')] = {
831
+ rules[_sx(scope, '.bw_form_control::placeholder')] = {
813
832
  'color': palette.secondary.base
814
833
  };
815
- rules[scopeSelector(scope, '.bw_form_label')] = {
834
+ rules[_sx(scope, '.bw_form_label')] = {
816
835
  'color': palette.dark.base
817
836
  };
818
- rules[scopeSelector(scope, '.bw_form_text')] = {
837
+ rules[_sx(scope, '.bw_form_text')] = {
819
838
  'color': palette.secondary.base
820
839
  };
821
- rules[scopeSelector(scope, '.bw_form_check_input:checked')] = {
840
+ rules[_sx(scope, '.bw_form_check_input:checked')] = {
822
841
  'background-color': palette.primary.base,
823
842
  'border-color': palette.primary.base
824
843
  };
825
- rules[scopeSelector(scope, '.bw_form_check_input:focus')] = {
844
+ rules[_sx(scope, '.bw_form_check_input:focus')] = {
826
845
  'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus
827
846
  };
828
847
  // Validation states
829
- rules[scopeSelector(scope, '.bw_form_control.bw_is_valid')] = { 'border-color': palette.success.base };
830
- rules[scopeSelector(scope, '.bw_form_control.bw_is_valid:focus')] = {
848
+ rules[_sx(scope, '.bw_form_control.bw_is_valid')] = { 'border-color': palette.success.base };
849
+ rules[_sx(scope, '.bw_form_control.bw_is_valid:focus')] = {
831
850
  'border-color': palette.success.base,
832
851
  'box-shadow': '0 0 0 0.2rem ' + palette.success.focus
833
852
  };
834
- rules[scopeSelector(scope, '.bw_form_control.bw_is_invalid')] = { 'border-color': palette.danger.base };
835
- rules[scopeSelector(scope, '.bw_form_control.bw_is_invalid:focus')] = {
853
+ rules[_sx(scope, '.bw_form_control.bw_is_invalid')] = { 'border-color': palette.danger.base };
854
+ rules[_sx(scope, '.bw_form_control.bw_is_invalid:focus')] = {
836
855
  'border-color': palette.danger.base,
837
856
  'box-shadow': '0 0 0 0.2rem ' + palette.danger.focus
838
857
  };
839
858
  // Form select
840
- rules[scopeSelector(scope, '.bw_form_select')] = {
859
+ rules[_sx(scope, '.bw_form_select')] = {
841
860
  'padding': sp.input,
842
861
  'border-radius': rd.input,
843
862
  'color': palette.dark.base,
844
863
  'background-color': palette.surface || '#fff',
845
864
  'border-color': palette.light.border
846
865
  };
847
- rules[scopeSelector(scope, '.bw_form_select:focus')] = {
866
+ rules[_sx(scope, '.bw_form_select:focus')] = {
848
867
  'border-color': palette.primary.border,
849
868
  'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus
850
869
  };
@@ -852,43 +871,46 @@
852
871
  return rules;
853
872
  }
854
873
 
855
- function generateNavigation(scope, palette) {
874
+ function generateNavigation(scope, palette, layout) {
856
875
  var rules = {};
857
- rules[scopeSelector(scope, '.bw_navbar')] = {
858
- 'background-color': palette.light.light,
876
+ rules[_sx(scope, '.bw_navbar')] = {
877
+ 'background-color': palette.surfaceAlt,
859
878
  'border-bottom-color': palette.light.border
860
879
  };
861
- rules[scopeSelector(scope, '.bw_navbar_brand')] = {
880
+ rules[_sx(scope, '.bw_navbar_brand')] = {
862
881
  'color': palette.dark.base
863
882
  };
864
- rules[scopeSelector(scope, '.bw_navbar_nav .bw_nav_link')] = {
865
- 'color': palette.secondary.base
883
+ rules[_sx(scope, '.bw_navbar_nav .bw_nav_link')] = {
884
+ 'color': palette.secondary.base,
885
+ 'border-radius': layout.radius.btn
866
886
  };
867
- rules[scopeSelector(scope, '.bw_navbar_nav .bw_nav_link:hover')] = {
868
- 'color': palette.dark.base
887
+ rules[_sx(scope, '.bw_navbar_nav .bw_nav_link:hover')] = {
888
+ 'color': palette.dark.base,
889
+ 'background-color': palette.surfaceAlt
869
890
  };
870
- rules[scopeSelector(scope, '.bw_navbar_nav .bw_nav_link.active')] = {
891
+ rules[_sx(scope, '.bw_navbar_nav .bw_nav_link.active')] = {
871
892
  'color': palette.primary.base,
872
- 'background-color': palette.primary.focus
893
+ 'background-color': palette.primary.focus,
894
+ 'font-weight': '600'
873
895
  };
874
- rules[scopeSelector(scope, '.bw_navbar_dark')] = {
896
+ rules[_sx(scope, '.bw_navbar_dark')] = {
875
897
  'background-color': palette.dark.base,
876
898
  'border-bottom-color': palette.dark.hover
877
899
  };
878
- rules[scopeSelector(scope, '.bw_navbar_dark .bw_navbar_brand')] = {
900
+ rules[_sx(scope, '.bw_navbar_dark .bw_navbar_brand')] = {
879
901
  'color': palette.light.base
880
902
  };
881
- rules[scopeSelector(scope, '.bw_navbar_dark .bw_nav_link')] = {
882
- 'color': 'rgba(255,255,255,.65)'
903
+ rules[_sx(scope, '.bw_navbar_dark .bw_nav_link')] = {
904
+ 'color': palette.light.border
883
905
  };
884
- rules[scopeSelector(scope, '.bw_navbar_dark .bw_nav_link:hover')] = {
885
- 'color': '#fff'
906
+ rules[_sx(scope, '.bw_navbar_dark .bw_nav_link:hover')] = {
907
+ 'color': palette.light.base
886
908
  };
887
- rules[scopeSelector(scope, '.bw_navbar_dark .bw_nav_link.active')] = {
888
- 'color': '#fff',
909
+ rules[_sx(scope, '.bw_navbar_dark .bw_nav_link.active')] = {
910
+ 'color': palette.light.base,
889
911
  'font-weight': '600'
890
912
  };
891
- rules[scopeSelector(scope, '.bw_nav_pills .bw_nav_link.active')] = {
913
+ rules[_sx(scope, '.bw_nav_pills .bw_nav_link.active')] = {
892
914
  'color': palette.primary.textOn,
893
915
  'background-color': palette.primary.base
894
916
  };
@@ -899,49 +921,58 @@
899
921
  var rules = {};
900
922
  var sp = layout.spacing;
901
923
 
902
- rules[scopeSelector(scope, '.bw_table')] = {
924
+ rules[_sx(scope, '.bw_table')] = {
903
925
  'color': palette.dark.base,
904
926
  'border-color': palette.light.border
905
927
  };
906
- rules[scopeSelector(scope, '.bw_table > :not(caption) > * > *')] = {
928
+ rules[_sx(scope, '.bw_table > :not(caption) > * > *')] = {
907
929
  'padding': sp.cell,
908
930
  'border-bottom-color': palette.light.border
909
931
  };
910
- rules[scopeSelector(scope, '.bw_table > thead > tr > *')] = {
932
+ rules[_sx(scope, '.bw_table > thead > tr > *')] = {
911
933
  'color': palette.secondary.base,
912
934
  'border-bottom-color': palette.light.border,
913
- 'background-color': palette.light.light
935
+ 'background-color': palette.surfaceAlt
914
936
  };
915
- rules[scopeSelector(scope, '.bw_table_striped > tbody > tr:nth-of-type(odd) > *')] = {
916
- 'background-color': 'rgba(0, 0, 0, 0.05)'
937
+ rules[_sx(scope, '.bw_table_striped > tbody > tr:nth-of-type(odd) > *')] = {
938
+ 'background-color': palette.surfaceAlt
917
939
  };
918
- rules[scopeSelector(scope, '.bw_table_hover > tbody > tr:hover > *')] = {
940
+ rules[_sx(scope, '.bw_table_hover > tbody > tr:hover > *')] = {
919
941
  'background-color': palette.primary.focus
920
942
  };
921
- rules[scopeSelector(scope, '.bw_table_bordered')] = {
943
+ rules[_sx(scope, '.bw_table_selectable > tbody > tr')] = {
944
+ 'cursor': 'pointer'
945
+ };
946
+ rules[_sx(scope, '.bw_table > tbody > tr.bw_table_row_selected > *')] = {
947
+ 'background-color': palette.primary.light
948
+ };
949
+ rules[_sx(scope, '.bw_table_bordered')] = {
922
950
  'border-color': palette.light.border
923
951
  };
924
- rules[scopeSelector(scope, '.bw_table caption')] = {
952
+ rules[_sx(scope, '.bw_table caption')] = {
925
953
  'color': palette.secondary.base
926
954
  };
927
955
 
928
956
  return rules;
929
957
  }
930
958
 
931
- function generateTabs(scope, palette) {
932
- var rules = {};
933
- rules[scopeSelector(scope, '.bw_nav_tabs')] = {
959
+ function generateTabs(scope, palette, layout) {
960
+ var rules = {}, mo = layout.motion;
961
+ rules[_sx(scope, '.bw_nav_tabs')] = {
934
962
  'border-bottom-color': palette.light.border
935
963
  };
936
- rules[scopeSelector(scope, '.bw_nav_link')] = {
937
- 'color': palette.secondary.base
964
+ rules[_sx(scope, '.bw_nav_link')] = {
965
+ 'color': palette.secondary.base,
966
+ 'transition': 'color ' + mo.fast + ' ' + mo.easing + ', border-color ' + mo.fast + ' ' + mo.easing + ', background-color ' + mo.fast + ' ' + mo.easing
938
967
  };
939
- rules[scopeSelector(scope, '.bw_nav_tabs .bw_nav_link:hover')] = {
968
+ rules[_sx(scope, '.bw_nav_tabs .bw_nav_link:hover')] = {
940
969
  'color': palette.dark.base,
970
+ 'background-color': palette.surfaceAlt,
941
971
  'border-bottom-color': palette.light.border
942
972
  };
943
- rules[scopeSelector(scope, '.bw_nav_tabs .bw_nav_link.active')] = {
973
+ rules[_sx(scope, '.bw_nav_tabs .bw_nav_link.active')] = {
944
974
  'color': palette.primary.base,
975
+ 'background-color': palette.primary.focus,
945
976
  'border-bottom': '2px solid ' + palette.primary.base
946
977
  };
947
978
  return rules;
@@ -950,23 +981,25 @@
950
981
  function generateListGroups(scope, palette, layout) {
951
982
  var rules = {};
952
983
  var sp = layout.spacing;
984
+ var mo = layout.motion;
953
985
 
954
- rules[scopeSelector(scope, '.bw_list_group_item')] = {
986
+ rules[_sx(scope, '.bw_list_group_item')] = {
955
987
  'padding': sp.cell,
956
988
  'color': palette.dark.base,
957
989
  'background-color': palette.surface || '#fff',
958
- 'border-color': palette.light.border
990
+ 'border-color': palette.light.border,
991
+ 'transition': 'color ' + mo.fast + ' ' + mo.easing + ', background-color ' + mo.fast + ' ' + mo.easing
959
992
  };
960
- rules[scopeSelector(scope, 'a.bw_list_group_item:hover')] = {
961
- 'background-color': palette.light.light,
993
+ rules[_sx(scope, 'a.bw_list_group_item:hover')] = {
994
+ 'background-color': palette.surfaceAlt,
962
995
  'color': palette.dark.hover
963
996
  };
964
- rules[scopeSelector(scope, '.bw_list_group_item.active')] = {
997
+ rules[_sx(scope, '.bw_list_group_item.active')] = {
965
998
  'color': palette.primary.textOn,
966
999
  'background-color': palette.primary.base,
967
1000
  'border-color': palette.primary.base
968
1001
  };
969
- rules[scopeSelector(scope, '.bw_list_group_item.disabled')] = {
1002
+ rules[_sx(scope, '.bw_list_group_item.disabled')] = {
970
1003
  'color': palette.secondary.base,
971
1004
  'background-color': palette.surface || '#fff'
972
1005
  };
@@ -974,28 +1007,37 @@
974
1007
  return rules;
975
1008
  }
976
1009
 
977
- function generatePagination(scope, palette) {
978
- var rules = {};
979
- rules[scopeSelector(scope, '.bw_page_link')] = {
1010
+ function generatePagination(scope, palette, layout) {
1011
+ var rules = {}, mo = layout.motion, rd = layout.radius;
1012
+ rules[_sx(scope, '.bw_page_item:first-child .bw_page_link')] = {
1013
+ 'border-top-left-radius': rd.btn,
1014
+ 'border-bottom-left-radius': rd.btn
1015
+ };
1016
+ rules[_sx(scope, '.bw_page_item:last-child .bw_page_link')] = {
1017
+ 'border-top-right-radius': rd.btn,
1018
+ 'border-bottom-right-radius': rd.btn
1019
+ };
1020
+ rules[_sx(scope, '.bw_page_link')] = {
980
1021
  'color': palette.primary.base,
981
1022
  'background-color': palette.surface || '#fff',
982
- 'border-color': palette.light.border
1023
+ 'border-color': palette.light.border,
1024
+ 'transition': 'color ' + mo.fast + ' ' + mo.easing + ', background-color ' + mo.fast + ' ' + mo.easing
983
1025
  };
984
- rules[scopeSelector(scope, '.bw_page_link:hover')] = {
1026
+ rules[_sx(scope, '.bw_page_link:hover')] = {
985
1027
  'color': palette.primary.hover,
986
- 'background-color': palette.light.light,
1028
+ 'background-color': palette.surfaceAlt,
987
1029
  'border-color': palette.light.border
988
1030
  };
989
- rules[scopeSelector(scope, '.bw_page_link:focus')] = {
1031
+ rules[_sx(scope, '.bw_page_link:focus')] = {
990
1032
  'outline': '2px solid ' + palette.primary.base,
991
1033
  'outline-offset': '-2px'
992
1034
  };
993
- rules[scopeSelector(scope, '.bw_page_item.bw_active .bw_page_link')] = {
1035
+ rules[_sx(scope, '.bw_page_item.bw_active .bw_page_link')] = {
994
1036
  'color': palette.primary.textOn,
995
1037
  'background-color': palette.primary.base,
996
1038
  'border-color': palette.primary.base
997
1039
  };
998
- rules[scopeSelector(scope, '.bw_page_item.bw_disabled .bw_page_link')] = {
1040
+ rules[_sx(scope, '.bw_page_item.bw_disabled .bw_page_link')] = {
999
1041
  'color': palette.secondary.base,
1000
1042
  'background-color': palette.surface || '#fff',
1001
1043
  'border-color': palette.light.border
@@ -1005,12 +1047,12 @@
1005
1047
 
1006
1048
  function generateProgress(scope, palette) {
1007
1049
  var rules = {};
1008
- rules[scopeSelector(scope, '.bw_progress')] = {
1009
- 'background-color': palette.light.light,
1050
+ rules[_sx(scope, '.bw_progress')] = {
1051
+ 'background-color': palette.surfaceAlt,
1010
1052
  'box-shadow': 'inset 0 1px 2px rgba(0,0,0,.1)'
1011
1053
  };
1012
- rules[scopeSelector(scope, '.bw_progress_bar')] = {
1013
- 'color': '#fff',
1054
+ rules[_sx(scope, '.bw_progress_bar')] = {
1055
+ 'color': palette.primary.textOn,
1014
1056
  'background-color': palette.primary.base,
1015
1057
  'box-shadow': 'inset 0 -1px 0 rgba(0,0,0,.15)'
1016
1058
  };
@@ -1029,26 +1071,31 @@
1029
1071
  'color': palette.dark.base,
1030
1072
  'background-color': bg
1031
1073
  };
1032
- rules[scopeSelector(scope, 'body')] = baseReset;
1033
- // Also apply to the scope element itself so themes work on any container, not just body
1034
- if (scope) {
1035
- rules['.' + scope] = baseReset;
1036
- }
1074
+ rules[_sx(scope, 'body')] = baseReset;
1037
1075
  return rules;
1038
1076
  }
1039
1077
 
1040
- function generateBreadcrumbThemed(scope, palette) {
1041
- var rules = {};
1042
- rules[scopeSelector(scope, '.bw_breadcrumb_item + .bw_breadcrumb_item::before')] = {
1043
- 'color': palette.secondary.base
1078
+ function generateBreadcrumbThemed(scope, palette, layout) {
1079
+ var rules = {}, mo = layout.motion;
1080
+ rules[_sx(scope, '.bw_breadcrumb')] = {
1081
+ 'background-color': palette.surfaceAlt,
1082
+ 'padding': '0.625rem 1rem',
1083
+ 'border-radius': layout.radius.btn
1044
1084
  };
1045
- rules[scopeSelector(scope, '.bw_breadcrumb_item.active')] = {
1085
+ rules[_sx(scope, '.bw_breadcrumb_item + .bw_breadcrumb_item::before')] = {
1046
1086
  'color': palette.secondary.base
1047
1087
  };
1048
- rules[scopeSelector(scope, '.bw_breadcrumb_item a:hover')] = {
1088
+ rules[_sx(scope, '.bw_breadcrumb_item a')] = {
1089
+ 'color': palette.primary.base,
1090
+ 'transition': 'color ' + mo.fast + ' ' + mo.easing
1091
+ };
1092
+ rules[_sx(scope, '.bw_breadcrumb_item a:hover')] = {
1049
1093
  'color': palette.primary.hover,
1050
1094
  'text-decoration': 'underline'
1051
1095
  };
1096
+ rules[_sx(scope, '.bw_breadcrumb_item.active')] = {
1097
+ 'color': palette.dark.base
1098
+ };
1052
1099
  return rules;
1053
1100
  }
1054
1101
 
@@ -1056,11 +1103,11 @@
1056
1103
 
1057
1104
  function generateCloseButtonThemed(scope, palette) {
1058
1105
  var rules = {};
1059
- rules[scopeSelector(scope, '.bw_close')] = {
1106
+ rules[_sx(scope, '.bw_close')] = {
1060
1107
  'color': palette.dark.base,
1061
1108
  'opacity': '0.5'
1062
1109
  };
1063
- rules[scopeSelector(scope, '.bw_close:focus')] = {
1110
+ rules[_sx(scope, '.bw_close:focus')] = {
1064
1111
  'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus
1065
1112
  };
1066
1113
  return rules;
@@ -1068,82 +1115,94 @@
1068
1115
 
1069
1116
  function generateSectionsThemed(scope, palette) {
1070
1117
  var rules = {};
1071
- rules[scopeSelector(scope, '.bw_section_subtitle')] = {
1118
+ rules[_sx(scope, '.bw_section_subtitle')] = {
1072
1119
  'color': palette.secondary.base
1073
1120
  };
1074
- rules[scopeSelector(scope, '.bw_feature_description')] = {
1121
+ rules[_sx(scope, '.bw_feature_description')] = {
1075
1122
  'color': palette.secondary.base
1076
1123
  };
1077
- rules[scopeSelector(scope, '.bw_cta_description')] = {
1124
+ rules[_sx(scope, '.bw_cta_description')] = {
1078
1125
  'color': palette.secondary.base
1079
1126
  };
1080
1127
  return rules;
1081
1128
  }
1082
1129
 
1083
- function generateAccordionThemed(scope, palette) {
1130
+ function generateAccordionThemed(scope, palette, layout) {
1084
1131
  var rules = {};
1085
- rules[scopeSelector(scope, '.bw_accordion_item')] = {
1132
+ var rd = layout ? layout.radius : { card: '8px' };
1133
+ rules[_sx(scope, '.bw_accordion_item')] = {
1086
1134
  'background-color': palette.surface || '#fff',
1087
1135
  'border-color': palette.light.border
1088
1136
  };
1089
- rules[scopeSelector(scope, '.bw_accordion_button')] = {
1137
+ rules[_sx(scope, '.bw_accordion_item:first-child')] = {
1138
+ 'border-top-left-radius': rd.card,
1139
+ 'border-top-right-radius': rd.card
1140
+ };
1141
+ rules[_sx(scope, '.bw_accordion_item:last-child')] = {
1142
+ 'border-bottom-left-radius': rd.card,
1143
+ 'border-bottom-right-radius': rd.card
1144
+ };
1145
+ rules[_sx(scope, '.bw_accordion_button')] = {
1090
1146
  'color': palette.dark.base
1091
1147
  };
1092
- rules[scopeSelector(scope, '.bw_accordion_button:not(.bw_collapsed)')] = {
1148
+ rules[_sx(scope, '.bw_accordion_button:not(.bw_collapsed)')] = {
1093
1149
  'color': palette.primary.darkText,
1094
- 'background-color': palette.primary.light
1150
+ 'background-color': palette.primary.light,
1151
+ 'border-left': '3px solid ' + palette.primary.base
1095
1152
  };
1096
- rules[scopeSelector(scope, '.bw_accordion_button:hover')] = {
1097
- 'background-color': palette.light.light
1153
+ rules[_sx(scope, '.bw_accordion_button:hover')] = {
1154
+ 'background-color': palette.surfaceAlt
1098
1155
  };
1099
- rules[scopeSelector(scope, '.bw_accordion_button:not(.bw_collapsed):hover')] = {
1100
- 'background-color': palette.primary.hover
1156
+ rules[_sx(scope, '.bw_accordion_button:not(.bw_collapsed):hover')] = {
1157
+ 'background-color': palette.primary.base,
1158
+ 'color': palette.primary.textOn
1101
1159
  };
1102
- rules[scopeSelector(scope, '.bw_accordion_button:focus-visible')] = {
1160
+ rules[_sx(scope, '.bw_accordion_button:focus-visible')] = {
1103
1161
  'box-shadow': '0 0 0 0.2rem ' + palette.primary.focus
1104
1162
  };
1105
- rules[scopeSelector(scope, '.bw_accordion_body')] = {
1106
- 'border-top': '1px solid ' + palette.light.border
1163
+ rules[_sx(scope, '.bw_accordion_body')] = {
1164
+ 'border-top': '1px solid ' + palette.light.border,
1165
+ 'background-color': palette.surfaceAlt
1107
1166
  };
1108
1167
  return rules;
1109
1168
  }
1110
1169
 
1111
1170
  function generateCarouselThemed(scope, palette) {
1112
1171
  var rules = {};
1113
- rules[scopeSelector(scope, '.bw_carousel')] = {
1114
- 'background-color': palette.light.light
1172
+ rules[_sx(scope, '.bw_carousel')] = {
1173
+ 'background-color': palette.surfaceAlt
1115
1174
  };
1116
- rules[scopeSelector(scope, '.bw_carousel_indicator.active')] = {
1175
+ rules[_sx(scope, '.bw_carousel_indicator.active')] = {
1117
1176
  'background-color': palette.primary.base
1118
1177
  };
1119
- rules[scopeSelector(scope, '.bw_carousel_control')] = {
1120
- 'background-color': 'rgba(0,0,0,0.4)',
1121
- 'color': '#fff'
1178
+ rules[_sx(scope, '.bw_carousel_control')] = {
1179
+ 'background-color': palette.dark.base,
1180
+ 'color': palette.dark.textOn
1122
1181
  };
1123
- rules[scopeSelector(scope, '.bw_carousel_control:hover')] = {
1124
- 'background-color': 'rgba(0,0,0,0.6)'
1182
+ rules[_sx(scope, '.bw_carousel_control:hover')] = {
1183
+ 'background-color': palette.dark.hover
1125
1184
  };
1126
- rules[scopeSelector(scope, '.bw_carousel_caption')] = {
1127
- 'background': 'linear-gradient(transparent, rgba(0,0,0,0.6))',
1128
- 'color': '#fff'
1185
+ rules[_sx(scope, '.bw_carousel_caption')] = {
1186
+ 'background': 'linear-gradient(transparent, ' + palette.dark.base + ')',
1187
+ 'color': palette.dark.textOn
1129
1188
  };
1130
1189
  return rules;
1131
1190
  }
1132
1191
 
1133
1192
  function generateModalThemed(scope, palette, layout) {
1134
1193
  var rules = {};
1135
- rules[scopeSelector(scope, '.bw_modal_content')] = {
1194
+ rules[_sx(scope, '.bw_modal_content')] = {
1136
1195
  'background-color': palette.surface || '#fff',
1137
1196
  'border-color': palette.light.border,
1138
1197
  'box-shadow': layout.elevation.lg
1139
1198
  };
1140
- rules[scopeSelector(scope, '.bw_modal_header')] = {
1199
+ rules[_sx(scope, '.bw_modal_header')] = {
1141
1200
  'border-bottom-color': palette.light.border
1142
1201
  };
1143
- rules[scopeSelector(scope, '.bw_modal_footer')] = {
1202
+ rules[_sx(scope, '.bw_modal_footer')] = {
1144
1203
  'border-top-color': palette.light.border
1145
1204
  };
1146
- rules[scopeSelector(scope, '.bw_modal_title')] = {
1205
+ rules[_sx(scope, '.bw_modal_title')] = {
1147
1206
  'color': palette.dark.base
1148
1207
  };
1149
1208
  return rules;
@@ -1151,13 +1210,13 @@
1151
1210
 
1152
1211
  function generateToastThemed(scope, palette, layout) {
1153
1212
  var rules = {};
1154
- rules[scopeSelector(scope, '.bw_toast')] = {
1213
+ rules[_sx(scope, '.bw_toast')] = {
1155
1214
  'background-color': palette.surface || '#fff',
1156
- 'border-color': 'rgba(0,0,0,0.1)',
1215
+ 'border-color': palette.light.border,
1157
1216
  'box-shadow': layout.elevation.lg
1158
1217
  };
1159
- rules[scopeSelector(scope, '.bw_toast_header')] = {
1160
- 'border-bottom-color': 'rgba(0,0,0,0.05)'
1218
+ rules[_sx(scope, '.bw_toast_header')] = {
1219
+ 'border-bottom-color': palette.light.border
1161
1220
  };
1162
1221
  // Variant toast borders handled by palette class
1163
1222
  return rules;
@@ -1165,22 +1224,23 @@
1165
1224
 
1166
1225
  function generateDropdownThemed(scope, palette, layout) {
1167
1226
  var rules = {};
1168
- rules[scopeSelector(scope, '.bw_dropdown_menu')] = {
1227
+ rules[_sx(scope, '.bw_dropdown_menu')] = {
1169
1228
  'background-color': palette.surface || '#fff',
1170
1229
  'border-color': palette.light.border,
1171
1230
  'box-shadow': layout.elevation.md
1172
1231
  };
1173
- rules[scopeSelector(scope, '.bw_dropdown_item')] = {
1174
- 'color': palette.dark.base
1232
+ rules[_sx(scope, '.bw_dropdown_item')] = {
1233
+ 'color': palette.dark.base,
1234
+ 'transition': 'background-color ' + layout.motion.fast + ' ' + layout.motion.easing
1175
1235
  };
1176
- rules[scopeSelector(scope, '.bw_dropdown_item:hover')] = {
1236
+ rules[_sx(scope, '.bw_dropdown_item:hover')] = {
1177
1237
  'color': palette.dark.hover,
1178
- 'background-color': palette.light.light
1238
+ 'background-color': palette.surfaceAlt
1179
1239
  };
1180
- rules[scopeSelector(scope, '.bw_dropdown_item.disabled')] = {
1240
+ rules[_sx(scope, '.bw_dropdown_item.disabled')] = {
1181
1241
  'color': palette.secondary.base
1182
1242
  };
1183
- rules[scopeSelector(scope, '.bw_dropdown_divider')] = {
1243
+ rules[_sx(scope, '.bw_dropdown_divider')] = {
1184
1244
  'border-top-color': palette.light.border
1185
1245
  };
1186
1246
  return rules;
@@ -1188,15 +1248,15 @@
1188
1248
 
1189
1249
  function generateSwitchThemed(scope, palette) {
1190
1250
  var rules = {};
1191
- rules[scopeSelector(scope, '.bw_form_switch .bw_switch_input')] = {
1251
+ rules[_sx(scope, '.bw_form_switch .bw_switch_input')] = {
1192
1252
  'background-color': palette.secondary.base,
1193
1253
  'border-color': palette.secondary.base
1194
1254
  };
1195
- rules[scopeSelector(scope, '.bw_form_switch .bw_switch_input:checked')] = {
1255
+ rules[_sx(scope, '.bw_form_switch .bw_switch_input:checked')] = {
1196
1256
  'background-color': palette.primary.base,
1197
1257
  'border-color': palette.primary.base
1198
1258
  };
1199
- rules[scopeSelector(scope, '.bw_form_switch .bw_switch_input:focus')] = {
1259
+ rules[_sx(scope, '.bw_form_switch .bw_switch_input:focus')] = {
1200
1260
  'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus
1201
1261
  };
1202
1262
  return rules;
@@ -1204,88 +1264,102 @@
1204
1264
 
1205
1265
  function generateSkeletonThemed(scope, palette) {
1206
1266
  var rules = {};
1207
- rules[scopeSelector(scope, '.bw_skeleton')] = {
1208
- 'background': 'linear-gradient(90deg, ' + palette.light.border + ' 25%, ' + palette.light.light + ' 37%, ' + palette.light.border + ' 63%)'
1267
+ rules[_sx(scope, '.bw_skeleton')] = {
1268
+ 'background': 'linear-gradient(90deg, ' + palette.light.border + ' 25%, ' + palette.surfaceAlt + ' 37%, ' + palette.light.border + ' 63%)'
1209
1269
  };
1210
1270
  return rules;
1211
1271
  }
1212
1272
 
1213
1273
  // generateAvatarThemed: removed — palette class on root handles variants
1214
1274
 
1215
- function generateStatCardThemed(scope, palette) {
1216
- var rules = {};
1275
+ function generateStatCardThemed(scope, palette, layout) {
1276
+ var rules = {}, mo = layout.motion, el = layout.elevation, rd = layout.radius;
1277
+ rules[_sx(scope, '.bw_stat_card')] = {
1278
+ 'background-color': palette.surface || '#fff',
1279
+ 'color': palette.dark.base,
1280
+ 'border': '1px solid ' + palette.light.border,
1281
+ 'border-radius': rd.card,
1282
+ 'box-shadow': el.sm,
1283
+ 'transition': 'box-shadow ' + mo.fast + ' ' + mo.easing + ', transform ' + mo.fast + ' ' + mo.easing
1284
+ };
1285
+ rules[_sx(scope, '.bw_stat_card:hover')] = { 'box-shadow': el.md };
1217
1286
  // Variant border colors handled by palette class
1218
- rules[scopeSelector(scope, '.bw_stat_change_up')] = { 'color': palette.success.base };
1219
- rules[scopeSelector(scope, '.bw_stat_change_down')] = { 'color': palette.danger.base };
1287
+ rules[_sx(scope, '.bw_stat_change_up')] = { 'color': palette.success.base };
1288
+ rules[_sx(scope, '.bw_stat_change_down')] = { 'color': palette.danger.base };
1220
1289
  return rules;
1221
1290
  }
1222
1291
 
1223
1292
  function generateTimelineThemed(scope, palette) {
1224
1293
  var rules = {};
1225
- rules[scopeSelector(scope, '.bw_timeline::before')] = { 'background-color': palette.light.border };
1294
+ rules[_sx(scope, '.bw_timeline::before')] = { 'background-color': palette.light.border };
1226
1295
  // Variant marker colors handled by palette class
1227
- rules[scopeSelector(scope, '.bw_timeline_date')] = { 'color': palette.secondary.base };
1296
+ rules[_sx(scope, '.bw_timeline_date')] = { 'color': palette.secondary.base };
1228
1297
  return rules;
1229
1298
  }
1230
1299
 
1231
1300
  function generateStepperThemed(scope, palette) {
1232
1301
  var rules = {};
1233
- rules[scopeSelector(scope, '.bw_step_indicator')] = {
1234
- 'background-color': palette.light.light,
1302
+ rules[_sx(scope, '.bw_step_indicator')] = {
1303
+ 'background-color': palette.surfaceAlt,
1235
1304
  'border': '2px solid ' + palette.light.border,
1236
1305
  'color': palette.secondary.base
1237
1306
  };
1238
- rules[scopeSelector(scope, '.bw_step + .bw_step::before')] = { 'background-color': palette.light.border };
1239
- rules[scopeSelector(scope, '.bw_step_active .bw_step_indicator')] = {
1307
+ rules[_sx(scope, '.bw_step + .bw_step::before')] = { 'background-color': palette.light.border };
1308
+ rules[_sx(scope, '.bw_step_active .bw_step_indicator')] = {
1240
1309
  'background-color': palette.primary.base,
1241
1310
  'color': palette.primary.textOn
1242
1311
  };
1243
- rules[scopeSelector(scope, '.bw_step_active .bw_step_label')] = {
1312
+ rules[_sx(scope, '.bw_step_active .bw_step_label')] = {
1244
1313
  'color': palette.dark.base,
1245
1314
  'font-weight': '600'
1246
1315
  };
1247
- rules[scopeSelector(scope, '.bw_step_completed .bw_step_indicator')] = {
1316
+ rules[_sx(scope, '.bw_step_completed .bw_step_indicator')] = {
1248
1317
  'background-color': palette.primary.base,
1249
1318
  'color': palette.primary.textOn
1250
1319
  };
1251
- rules[scopeSelector(scope, '.bw_step_completed .bw_step_label')] = { 'color': palette.primary.base };
1252
- rules[scopeSelector(scope, '.bw_step_completed + .bw_step::before')] = { 'background-color': palette.primary.base };
1320
+ rules[_sx(scope, '.bw_step_completed .bw_step_label')] = { 'color': palette.primary.base };
1321
+ rules[_sx(scope, '.bw_step_completed + .bw_step::before')] = { 'background-color': palette.primary.base };
1253
1322
  return rules;
1254
1323
  }
1255
1324
 
1256
1325
  function generateChipInputThemed(scope, palette) {
1257
1326
  var rules = {};
1258
- rules[scopeSelector(scope, '.bw_chip_input')] = { 'border-color': palette.light.border };
1259
- rules[scopeSelector(scope, '.bw_chip_input:focus-within')] = {
1327
+ rules[_sx(scope, '.bw_chip_input')] = {
1328
+ 'border-color': palette.light.border,
1329
+ 'background-color': palette.surface || '#fff',
1330
+ 'color': palette.dark.base
1331
+ };
1332
+ rules[_sx(scope, '.bw_chip_input:focus-within')] = {
1260
1333
  'border-color': palette.primary.base,
1261
1334
  'box-shadow': '0 0 0 0.2rem ' + palette.primary.focus
1262
1335
  };
1263
- rules[scopeSelector(scope, '.bw_chip')] = {
1264
- 'background-color': palette.light.light,
1336
+ rules[_sx(scope, '.bw_chip')] = {
1337
+ 'background-color': palette.surfaceAlt,
1265
1338
  'color': palette.dark.base
1266
1339
  };
1267
- rules[scopeSelector(scope, '.bw_chip_remove:hover')] = {
1340
+ rules[_sx(scope, '.bw_chip_remove:hover')] = {
1268
1341
  'color': palette.danger.base,
1269
1342
  'background-color': palette.danger.light
1270
1343
  };
1271
1344
  return rules;
1272
1345
  }
1273
1346
 
1274
- function generateFileUploadThemed(scope, palette) {
1275
- var rules = {};
1276
- rules[scopeSelector(scope, '.bw_file_upload')] = {
1347
+ function generateFileUploadThemed(scope, palette, layout) {
1348
+ var rules = {}, mo = layout.motion;
1349
+ rules[_sx(scope, '.bw_file_upload')] = {
1277
1350
  'border-color': palette.light.border,
1278
- 'background-color': palette.light.light
1351
+ 'background-color': palette.surfaceAlt,
1352
+ 'transition': 'border-color ' + mo.fast + ' ' + mo.easing + ', background-color ' + mo.fast + ' ' + mo.easing
1279
1353
  };
1280
- rules[scopeSelector(scope, '.bw_file_upload:hover')] = {
1354
+ rules[_sx(scope, '.bw_file_upload:hover')] = {
1281
1355
  'border-color': palette.primary.base,
1282
1356
  'background-color': palette.primary.light
1283
1357
  };
1284
- rules[scopeSelector(scope, '.bw_file_upload:focus')] = {
1358
+ rules[_sx(scope, '.bw_file_upload:focus')] = {
1285
1359
  'outline': '2px solid ' + palette.primary.base,
1286
1360
  'outline-offset': '2px'
1287
1361
  };
1288
- rules[scopeSelector(scope, '.bw_file_upload.bw_file_upload_active')] = {
1362
+ rules[_sx(scope, '.bw_file_upload.bw_file_upload_active')] = {
1289
1363
  'border-color': palette.primary.base,
1290
1364
  'background-color': palette.primary.light,
1291
1365
  'border-style': 'solid'
@@ -1295,35 +1369,73 @@
1295
1369
 
1296
1370
  function generateRangeThemed(scope, palette) {
1297
1371
  var rules = {};
1298
- rules[scopeSelector(scope, '.bw_range')] = { 'background-color': palette.light.border };
1299
- rules[scopeSelector(scope, '.bw_range::-webkit-slider-thumb')] = {
1372
+ rules[_sx(scope, '.bw_range')] = { 'background-color': palette.light.border };
1373
+ rules[_sx(scope, '.bw_range::-webkit-slider-thumb')] = {
1300
1374
  'background-color': palette.primary.base,
1301
- 'border-color': '#fff',
1375
+ 'border-color': palette.surface || '#fff',
1302
1376
  'box-shadow': '0 1px 3px rgba(0,0,0,0.2)',
1303
1377
  'transition': 'background-color 0.15s ease-out, transform 0.15s ease-out'
1304
1378
  };
1305
- rules[scopeSelector(scope, '.bw_range::-moz-range-thumb')] = {
1379
+ rules[_sx(scope, '.bw_range::-moz-range-thumb')] = {
1306
1380
  'background-color': palette.primary.base,
1307
- 'border-color': '#fff',
1381
+ 'border-color': palette.surface || '#fff',
1308
1382
  'box-shadow': '0 1px 3px rgba(0,0,0,0.2)'
1309
1383
  };
1310
1384
  return rules;
1311
1385
  }
1312
1386
 
1313
- function generateSearchThemed(scope, palette) {
1314
- var rules = {};
1315
- rules[scopeSelector(scope, '.bw_search_clear:hover')] = { 'color': palette.dark.base };
1387
+ function generateTooltipThemed(scope, palette, layout) {
1388
+ var rules = {}, sp = layout.spacing, rd = layout.radius, el = layout.elevation, mo = layout.motion;
1389
+ rules[_sx(scope, '.bw_tooltip')] = {
1390
+ 'background-color': palette.dark.base, 'color': palette.dark.textOn,
1391
+ 'padding': sp.input, 'border-radius': rd.badge, 'box-shadow': el.md,
1392
+ 'transition': 'opacity ' + mo.fast + ' ' + mo.easing + ', transform ' + mo.fast + ' ' + mo.easing
1393
+ };
1394
+ return rules;
1395
+ }
1396
+
1397
+ function generatePopoverThemed(scope, palette, layout) {
1398
+ var rules = {}, sp = layout.spacing, rd = layout.radius, el = layout.elevation, mo = layout.motion;
1399
+ rules[_sx(scope, '.bw_popover')] = {
1400
+ 'background-color': palette.surface || '#fff', 'color': palette.dark.base,
1401
+ 'border': '1px solid ' + palette.light.border, 'border-radius': rd.card, 'box-shadow': el.lg,
1402
+ 'transition': 'opacity ' + mo.fast + ' ' + mo.easing + ', transform ' + mo.fast + ' ' + mo.easing
1403
+ };
1404
+ rules[_sx(scope, '.bw_popover_header')] = {
1405
+ 'background-color': palette.surfaceAlt, 'border-bottom': '1px solid ' + palette.light.border,
1406
+ 'padding': sp.input
1407
+ };
1408
+ rules[_sx(scope, '.bw_popover_body')] = { 'padding': sp.card };
1409
+ return rules;
1410
+ }
1411
+
1412
+ function generateSearchThemed(scope, palette, layout) {
1413
+ var rules = {}, mo = layout.motion;
1414
+ rules[_sx(scope, '.bw_search_input')] = {
1415
+ 'background-color': palette.surface || '#fff',
1416
+ 'color': palette.dark.base
1417
+ };
1418
+ rules[_sx(scope, '.bw_search_clear')] = {
1419
+ 'transition': 'color ' + mo.fast + ' ' + mo.easing + ', background-color ' + mo.fast + ' ' + mo.easing
1420
+ };
1421
+ rules[_sx(scope, '.bw_search_clear:hover')] = { 'color': palette.dark.base };
1316
1422
  return rules;
1317
1423
  }
1318
1424
 
1319
- function generateCodeDemoThemed(scope, palette) {
1425
+ function generateCodeDemoThemed(scope, palette, layout) {
1320
1426
  var rules = {};
1321
- rules[scopeSelector(scope, '.bw_code_copy_btn_copied')] = {
1427
+ var rd = layout ? layout.radius : { card: '0.375rem' };
1428
+ rules[_sx(scope, '.bw_code_demo')] = {
1429
+ 'background-color': palette.surface || '#fff',
1430
+ 'color': palette.dark.base,
1431
+ 'border-radius': rd.card
1432
+ };
1433
+ rules[_sx(scope, '.bw_code_copy_btn_copied')] = {
1322
1434
  'background': palette.success.base,
1323
1435
  'color': palette.success.textOn,
1324
1436
  'border-color': palette.success.base
1325
1437
  };
1326
- rules[scopeSelector(scope, '.bw_copy_btn:hover')] = {
1438
+ rules[_sx(scope, '.bw_copy_btn:hover')] = {
1327
1439
  'background': 'rgba(255,255,255,0.2)',
1328
1440
  'color': '#fff'
1329
1441
  };
@@ -1333,7 +1445,7 @@
1333
1445
  function generateNavPillsThemed(scope, palette, layout) {
1334
1446
  var rules = {};
1335
1447
  var rd = layout.radius;
1336
- rules[scopeSelector(scope, '.bw_nav_pills .bw_nav_link')] = { 'border-radius': rd.btn };
1448
+ rules[_sx(scope, '.bw_nav_pills .bw_nav_link')] = { 'border-radius': rd.btn };
1337
1449
  return rules;
1338
1450
  }
1339
1451
 
@@ -1359,21 +1471,21 @@
1359
1471
  var s = palette[k];
1360
1472
 
1361
1473
  // --- Root palette class: sets default bg/color/border ---
1362
- rules[scopeSelector(scope, '.bw_' + k)] = {
1474
+ rules[_sx(scope, '.bw_' + k)] = {
1363
1475
  'background-color': s.base,
1364
1476
  'color': s.textOn,
1365
1477
  'border-color': s.base
1366
1478
  };
1367
1479
 
1368
1480
  // --- Pseudo-states (shared across all components) ---
1369
- rules[scopeSelector(scope, '.bw_' + k + ':hover')] = {
1481
+ rules[_sx(scope, '.bw_' + k + ':hover')] = {
1370
1482
  'background-color': s.hover,
1371
1483
  'border-color': s.active
1372
1484
  };
1373
- rules[scopeSelector(scope, '.bw_' + k + ':active')] = {
1485
+ rules[_sx(scope, '.bw_' + k + ':active')] = {
1374
1486
  'background-color': s.active
1375
1487
  };
1376
- rules[scopeSelector(scope, '.bw_' + k + ':focus-visible')] = {
1488
+ rules[_sx(scope, '.bw_' + k + ':focus-visible')] = {
1377
1489
  'box-shadow': '0 0 0 3px ' + s.focus,
1378
1490
  'outline': 'none'
1379
1491
  };
@@ -1381,70 +1493,99 @@
1381
1493
  // --- Component-specific overrides ---
1382
1494
 
1383
1495
  // Alerts: light bg, dark text, subtle border
1384
- rules[scopeSelector(scope, '.bw_alert.bw_' + k)] = {
1496
+ rules[_sx(scope, '.bw_alert.bw_' + k)] = {
1385
1497
  'background-color': s.light,
1386
1498
  'color': s.darkText,
1387
1499
  'border-color': s.border
1388
1500
  };
1389
1501
 
1390
1502
  // Toast: inherit bg, left border accent
1391
- rules[scopeSelector(scope, '.bw_toast.bw_' + k)] = {
1503
+ rules[_sx(scope, '.bw_toast.bw_' + k)] = {
1392
1504
  'background-color': 'inherit',
1393
1505
  'color': 'inherit',
1394
1506
  'border-left': '4px solid ' + s.base
1395
1507
  };
1396
1508
 
1397
1509
  // Stat card: inherit bg, left border accent
1398
- rules[scopeSelector(scope, '.bw_stat_card.bw_' + k)] = {
1510
+ rules[_sx(scope, '.bw_stat_card.bw_' + k)] = {
1399
1511
  'background-color': 'inherit',
1400
1512
  'color': 'inherit',
1401
1513
  'border-left-color': s.base
1402
1514
  };
1403
1515
 
1404
1516
  // Card accent: left border accent, inherit bg
1405
- rules[scopeSelector(scope, '.bw_card.bw_' + k)] = {
1517
+ rules[_sx(scope, '.bw_card.bw_' + k)] = {
1406
1518
  'background-color': 'inherit',
1407
1519
  'color': 'inherit',
1408
1520
  'border-left': '4px solid ' + s.base
1409
1521
  };
1410
1522
 
1411
1523
  // Timeline marker: colored dot
1412
- rules[scopeSelector(scope, '.bw_timeline_marker.bw_' + k)] = {
1524
+ rules[_sx(scope, '.bw_timeline_marker.bw_' + k)] = {
1413
1525
  'box-shadow': '0 0 0 2px ' + s.base
1414
1526
  };
1415
1527
 
1416
- // Spinner: text color only, transparent bg
1417
- rules[scopeSelector(scope, '.bw_spinner_border.bw_' + k + ',\n' + scopeSelector(scope, '.bw_spinner_grow.bw_' + k))] = {
1528
+ // Spinner: set color, re-apply border pattern so the root palette class
1529
+ // border-color doesn't fill in the transparent gap that makes it spin.
1530
+ // Also neutralize hover/active which would override border-right-color.
1531
+ rules[_sx(scope, '.bw_spinner_border.bw_' + k)] = {
1418
1532
  'background-color': 'transparent',
1419
1533
  'color': s.base,
1420
- 'border-color': 'currentColor'
1534
+ 'border-color': s.base,
1535
+ 'border-right-color': 'transparent'
1536
+ };
1537
+ rules[_sx(scope, '.bw_spinner_border.bw_' + k + ':hover')] = {
1538
+ 'background-color': 'transparent',
1539
+ 'border-color': s.base,
1540
+ 'border-right-color': 'transparent'
1541
+ };
1542
+ rules[_sx(scope, '.bw_spinner_grow.bw_' + k)] = {
1543
+ 'background-color': s.base,
1544
+ 'color': s.base
1421
1545
  };
1422
1546
 
1423
1547
  // Outline button: transparent bg, colored border+text, solid on hover
1424
- rules[scopeSelector(scope, '.bw_btn_outline.bw_' + k)] = {
1548
+ rules[_sx(scope, '.bw_btn_outline.bw_' + k)] = {
1425
1549
  'background-color': 'transparent',
1426
1550
  'color': s.base,
1427
1551
  'border-color': s.base
1428
1552
  };
1429
- rules[scopeSelector(scope, '.bw_btn_outline.bw_' + k + ':hover')] = {
1553
+ rules[_sx(scope, '.bw_btn_outline.bw_' + k + ':hover')] = {
1430
1554
  'background-color': s.base,
1431
1555
  'color': s.textOn
1432
1556
  };
1433
1557
 
1434
1558
  // Hero: gradient background
1435
- rules[scopeSelector(scope, '.bw_hero.bw_' + k)] = {
1559
+ rules[_sx(scope, '.bw_hero.bw_' + k)] = {
1436
1560
  'background': 'linear-gradient(135deg, ' + s.base + ' 0%, ' + s.hover + ' 100%)',
1437
1561
  'color': s.textOn
1438
1562
  };
1439
1563
 
1440
- // Progress bar: white text on colored bg (default is fine, just ensure text)
1441
- rules[scopeSelector(scope, '.bw_progress_bar.bw_' + k)] = {
1442
- 'color': '#fff'
1564
+ // Progress bar: contrasting text on colored bg
1565
+ rules[_sx(scope, '.bw_progress_bar.bw_' + k)] = {
1566
+ 'color': s.textOn
1567
+ };
1568
+
1569
+ // Background utility: .bw_bg_primary, .bw_bg_secondary, etc.
1570
+ rules[_sx(scope, '.bw_bg_' + k)] = {
1571
+ 'background-color': s.base,
1572
+ 'color': s.textOn
1573
+ };
1574
+
1575
+ // Text color utility: .bw_text_primary, .bw_text_secondary, etc.
1576
+ rules[_sx(scope, '.bw_text_' + k)] = {
1577
+ 'color': s.base
1443
1578
  };
1444
1579
  });
1445
1580
 
1446
- // Text muted
1447
- rules[scopeSelector(scope, '.bw_text_muted')] = { 'color': palette.secondary.base };
1581
+ // Text muted — always a neutral gray, never a brand color
1582
+ rules[_sx(scope, '.bw_text_muted')] = { 'color': '#6c757d' };
1583
+
1584
+ // Common bg/text utilities that aren't per-variant
1585
+ rules[_sx(scope, '.bw_bg_dark')] = { 'background-color': '#212529', 'color': '#f8f9fa' };
1586
+ rules[_sx(scope, '.bw_bg_light')] = { 'background-color': '#f8f9fa', 'color': '#212529' };
1587
+ rules[_sx(scope, '.bw_text_light')] = { 'color': '#f8f9fa' };
1588
+ rules[_sx(scope, '.bw_text_dark')] = { 'color': '#212529' };
1448
1589
 
1449
1590
  return rules;
1450
1591
  }
@@ -1466,30 +1607,32 @@
1466
1607
  generateAlerts(scopeName, palette, layout),
1467
1608
  generateCards(scopeName, palette, layout),
1468
1609
  generateForms(scopeName, palette, layout),
1469
- generateNavigation(scopeName, palette),
1610
+ generateNavigation(scopeName, palette, layout),
1470
1611
  generateTables(scopeName, palette, layout),
1471
- generateTabs(scopeName, palette),
1612
+ generateTabs(scopeName, palette, layout),
1472
1613
  generateListGroups(scopeName, palette, layout),
1473
- generatePagination(scopeName, palette),
1614
+ generatePagination(scopeName, palette, layout),
1474
1615
  generateProgress(scopeName, palette),
1475
- generateBreadcrumbThemed(scopeName, palette),
1616
+ generateBreadcrumbThemed(scopeName, palette, layout),
1476
1617
  generateCloseButtonThemed(scopeName, palette),
1477
1618
  generateSectionsThemed(scopeName, palette),
1478
- generateAccordionThemed(scopeName, palette),
1619
+ generateAccordionThemed(scopeName, palette, layout),
1479
1620
  generateCarouselThemed(scopeName, palette),
1480
1621
  generateModalThemed(scopeName, palette, layout),
1481
1622
  generateToastThemed(scopeName, palette, layout),
1482
1623
  generateDropdownThemed(scopeName, palette, layout),
1483
1624
  generateSwitchThemed(scopeName, palette),
1484
1625
  generateSkeletonThemed(scopeName, palette),
1485
- generateStatCardThemed(scopeName, palette),
1626
+ generateStatCardThemed(scopeName, palette, layout),
1486
1627
  generateTimelineThemed(scopeName, palette),
1487
1628
  generateStepperThemed(scopeName, palette),
1488
1629
  generateChipInputThemed(scopeName, palette),
1489
- generateFileUploadThemed(scopeName, palette),
1630
+ generateFileUploadThemed(scopeName, palette, layout),
1490
1631
  generateRangeThemed(scopeName, palette),
1491
- generateSearchThemed(scopeName, palette),
1492
- generateCodeDemoThemed(scopeName, palette),
1632
+ generateSearchThemed(scopeName, palette, layout),
1633
+ generateTooltipThemed(scopeName, palette, layout),
1634
+ generatePopoverThemed(scopeName, palette, layout),
1635
+ generateCodeDemoThemed(scopeName, palette, layout),
1493
1636
  generateNavPillsThemed(scopeName, palette, layout),
1494
1637
  generatePaletteClasses(scopeName, palette)
1495
1638
  );
@@ -1714,6 +1857,8 @@
1714
1857
  },
1715
1858
  '.bw_table caption': { 'font-size': '0.875rem', 'caption-side': 'bottom' },
1716
1859
  '.bw_table_bordered > :not(caption) > * > *': { 'border-width': '1px', 'border-style': 'solid' },
1860
+ '.bw_table_selectable > tbody > tr': { 'cursor': 'pointer' },
1861
+ '.bw_table > tbody > tr.bw_table_row_selected > *': { 'background-color': 'rgba(0, 102, 102, 0.1)' },
1717
1862
  '.bw_table_responsive': { 'overflow-x': 'auto', '-webkit-overflow-scrolling': 'touch' }
1718
1863
  },
1719
1864
 
@@ -1767,6 +1912,7 @@
1767
1912
  '.bw_nav_tabs .bw_nav_item': { 'margin-bottom': '-2px' },
1768
1913
  '.bw_nav_link': {
1769
1914
  'display': 'block', 'font-size': '0.875rem', 'font-weight': '500',
1915
+ 'padding': '0.625rem 1rem',
1770
1916
  'text-decoration': 'none', 'cursor': 'pointer',
1771
1917
  'border': 'none', 'background': 'transparent', 'font-family': 'inherit'
1772
1918
  },
@@ -1801,10 +1947,11 @@
1801
1947
  '.bw_page_item': { 'display': 'list-item', 'list-style': 'none' },
1802
1948
  '.bw_page_link': {
1803
1949
  'position': 'relative', 'display': 'block', 'padding': '0.375rem 0.75rem',
1804
- 'margin-left': '-1px', 'line-height': '1.25', 'text-decoration': 'none'
1950
+ 'margin-left': '-1px', 'line-height': '1.25', 'text-decoration': 'none',
1951
+ 'border': '1px solid transparent', 'cursor': 'pointer',
1952
+ 'font-family': 'inherit', 'font-size': 'inherit', 'background': 'none'
1805
1953
  },
1806
- '.bw_page_item:first-child .bw_page_link': { 'margin-left': '0', 'border-top-left-radius': '0.375rem', 'border-bottom-left-radius': '0.375rem' },
1807
- '.bw_page_item:last-child .bw_page_link': { 'border-top-right-radius': '0.375rem', 'border-bottom-right-radius': '0.375rem' },
1954
+ '.bw_page_item:first-child .bw_page_link': { 'margin-left': '0' },
1808
1955
  '.bw_page_link:focus-visible': { 'z-index': '3', 'outline': '2px solid currentColor', 'outline-offset': '-2px' }
1809
1956
  },
1810
1957
 
@@ -1961,6 +2108,7 @@
1961
2108
  '.bw_accordion_header': { 'margin': '0' },
1962
2109
  '.bw_accordion_button': {
1963
2110
  'position': 'relative', 'display': 'flex', 'align-items': 'center', 'width': '100%',
2111
+ 'padding': '0.875rem 1.25rem',
1964
2112
  'font-size': '1rem', 'font-weight': '500', 'text-align': 'left',
1965
2113
  'background-color': 'transparent', 'border': '0', 'overflow-anchor': 'none', 'cursor': 'pointer',
1966
2114
  'font-family': 'inherit'
@@ -1972,10 +2120,9 @@
1972
2120
  'background-repeat': 'no-repeat', 'background-size': '1.25rem'
1973
2121
  },
1974
2122
  '.bw_accordion_button:not(.bw_collapsed)::after': { 'transform': 'rotate(-180deg)' },
1975
- '.bw_accordion_collapse': { 'max-height': '0', 'overflow': 'hidden' },
1976
- '.bw_accordion_collapse.bw_collapse_show': { 'max-height': 'none' },
1977
- '.bw_accordion_item:first-child': { 'border-top-left-radius': '8px', 'border-top-right-radius': '8px' },
1978
- '.bw_accordion_item:last-child': { 'border-bottom-left-radius': '8px', 'border-bottom-right-radius': '8px' }
2123
+ '.bw_accordion_body': { 'padding': '1rem 1.25rem' },
2124
+ '.bw_accordion_collapse': { 'max-height': '0', 'overflow': 'hidden', 'transition': 'max-height 0.3s ease' },
2125
+ '.bw_accordion_collapse.bw_collapse_show': { 'max-height': 'none' }
1979
2126
  },
1980
2127
 
1981
2128
  // ---- Carousel ----
@@ -2029,10 +2176,10 @@
2029
2176
  'position': 'relative', 'display': 'flex', 'flex-direction': 'column', 'pointer-events': 'auto',
2030
2177
  'background-clip': 'padding-box', 'border': '1px solid transparent', 'outline': '0'
2031
2178
  },
2032
- '.bw_modal_header': { 'display': 'flex', 'align-items': 'center', 'justify-content': 'space-between' },
2179
+ '.bw_modal_header': { 'display': 'flex', 'align-items': 'center', 'justify-content': 'space-between', 'padding': '1rem 1.25rem', 'border-bottom': '1px solid transparent' },
2033
2180
  '.bw_modal_title': { 'margin': '0', 'font-size': '1.25rem', 'font-weight': '600', 'line-height': '1.3' },
2034
- '.bw_modal_body': { 'position': 'relative', 'flex': '1 1 auto' },
2035
- '.bw_modal_footer': { 'display': 'flex', 'flex-wrap': 'wrap', 'align-items': 'center', 'justify-content': 'flex-end', 'gap': '0.5rem' }
2181
+ '.bw_modal_body': { 'position': 'relative', 'flex': '1 1 auto', 'padding': '1rem 1.25rem' },
2182
+ '.bw_modal_footer': { 'display': 'flex', 'flex-wrap': 'wrap', 'align-items': 'center', 'justify-content': 'flex-end', 'gap': '0.5rem', 'padding': '0.75rem 1.25rem', 'border-top': '1px solid transparent' }
2036
2183
  },
2037
2184
 
2038
2185
  // ---- Toast ----
@@ -2053,8 +2200,8 @@
2053
2200
  },
2054
2201
  '.bw_toast.bw_toast_show': { 'opacity': '1', 'transform': 'translateY(0)' },
2055
2202
  '.bw_toast.bw_toast_hiding': { 'opacity': '0' },
2056
- '.bw_toast_header': { 'display': 'flex', 'align-items': 'center', 'justify-content': 'space-between', 'font-size': '0.875rem' },
2057
- '.bw_toast_body': { 'font-size': '0.9375rem' }
2203
+ '.bw_toast_header': { 'display': 'flex', 'align-items': 'center', 'justify-content': 'space-between', 'padding': '0.5rem 0.75rem', 'font-size': '0.875rem', 'border-bottom': '1px solid transparent' },
2204
+ '.bw_toast_body': { 'padding': '0.5rem 0.75rem', 'font-size': '0.9375rem' }
2058
2205
  },
2059
2206
 
2060
2207
  // ---- Dropdown ----
@@ -2068,15 +2215,15 @@
2068
2215
  '.bw_dropdown_menu': {
2069
2216
  'position': 'absolute', 'top': '100%', 'left': '0', 'z-index': '1000', 'display': 'block',
2070
2217
  'min-width': '10rem', 'padding': '0.5rem 0', 'margin': '0.125rem 0 0',
2071
- 'background-clip': 'padding-box',
2218
+ 'background-clip': 'padding-box', 'border': '1px solid transparent',
2072
2219
  'opacity': '0', 'visibility': 'hidden', 'pointer-events': 'none'
2073
2220
  },
2074
2221
  '.bw_dropdown_menu.bw_dropdown_show': { 'opacity': '1', 'visibility': 'visible', 'pointer-events': 'auto' },
2075
2222
  '.bw_dropdown_menu_end': { 'left': 'auto', 'right': '0' },
2076
2223
  '.bw_dropdown_item': {
2077
- 'display': 'block', 'width': '100%', 'clear': 'both',
2224
+ 'display': 'block', 'width': '100%', 'padding': '0.4rem 1rem', 'clear': 'both',
2078
2225
  'font-weight': '400', 'text-align': 'inherit', 'text-decoration': 'none', 'white-space': 'nowrap',
2079
- 'background-color': 'transparent', 'border': '0', 'font-size': '0.9375rem'
2226
+ 'background-color': 'transparent', 'border': '0', 'font-size': '0.9375rem', 'cursor': 'pointer'
2080
2227
  },
2081
2228
  '.bw_dropdown_item:focus-visible': { 'outline': '2px solid currentColor', 'outline-offset': '-2px' },
2082
2229
  '.bw_dropdown_divider': { 'height': '0', 'margin': '0.5rem 0', 'overflow': 'hidden', 'opacity': '1' }
@@ -2121,7 +2268,13 @@
2121
2268
 
2122
2269
  // ---- Stat card ----
2123
2270
  statCard: {
2124
- '.bw_stat_card': { 'border-left': '4px solid transparent' },
2271
+ '.bw_stat_card': {
2272
+ 'padding': '1.25rem',
2273
+ 'border-left': '4px solid transparent',
2274
+ 'border-radius': '0.375rem',
2275
+ 'background-color': 'inherit',
2276
+ 'transition': 'transform 0.15s ease'
2277
+ },
2125
2278
  '.bw_stat_card:hover': { 'transform': 'translateY(-1px)' },
2126
2279
  '.bw_stat_icon': { 'font-size': '1.5rem', 'margin-bottom': '0.5rem' },
2127
2280
  '.bw_stat_value': { 'font-size': '2rem', 'font-weight': '700', 'line-height': '1.2' },
@@ -2392,6 +2545,33 @@
2392
2545
  rules['.bw_text_left'] = { 'text-align': 'left' };
2393
2546
  rules['.bw_text_right'] = { 'text-align': 'right' };
2394
2547
  rules['.bw_text_center'] = { 'text-align': 'center' };
2548
+ rules['.bw_text_justify'] = { 'text-align': 'justify' };
2549
+
2550
+ // Font weight
2551
+ rules['.bw_fw_bold'] = { 'font-weight': '700' };
2552
+ rules['.bw_fw_semibold'] = { 'font-weight': '600' };
2553
+ rules['.bw_fw_normal'] = { 'font-weight': '400' };
2554
+ rules['.bw_fw_light'] = { 'font-weight': '300' };
2555
+
2556
+ // Font style
2557
+ rules['.bw_fst_italic'] = { 'font-style': 'italic' };
2558
+ rules['.bw_fst_normal'] = { 'font-style': 'normal' };
2559
+
2560
+ // Text decoration
2561
+ rules['.bw_text_underline'] = { 'text-decoration': 'underline' };
2562
+ rules['.bw_text_line_through'] = { 'text-decoration': 'line-through' };
2563
+ rules['.bw_text_decoration_none'] = { 'text-decoration': 'none' };
2564
+
2565
+ // Text transform
2566
+ rules['.bw_text_uppercase'] = { 'text-transform': 'uppercase' };
2567
+ rules['.bw_text_lowercase'] = { 'text-transform': 'lowercase' };
2568
+ rules['.bw_text_capitalize'] = { 'text-transform': 'capitalize' };
2569
+
2570
+ // Font size
2571
+ rules['.bw_fs_sm'] = { 'font-size': '0.875rem' };
2572
+ rules['.bw_fs_base'] = { 'font-size': '1rem' };
2573
+ rules['.bw_fs_lg'] = { 'font-size': '1.25rem' };
2574
+ rules['.bw_fs_xl'] = { 'font-size': '1.5rem' };
2395
2575
 
2396
2576
  // Flexbox
2397
2577
  var jc = { start: 'flex-start', end: 'flex-end', center: 'center', between: 'space-between', around: 'space-around' };
@@ -2484,6 +2664,20 @@
2484
2664
  rules['.list-inline-item'] = { 'display': 'inline-block' };
2485
2665
  rules['.list-inline-item:not(:last-child)'] = { 'margin-right': '.5rem' };
2486
2666
 
2667
+ // Typography — bw_ prefixed utilities via loops
2668
+ var _imp = function(p, v) { var o = {}; o[p] = v + ' !important'; return o; };
2669
+ [['fs',{'xs':'0.75rem','sm':'0.875rem','base':'1rem','lg':'1.125rem','xl':'1.25rem','2xl':'1.5rem'},'font-size'],
2670
+ ['fw',{light:'300',normal:'400',medium:'500',semibold:'600',bold:'700'},'font-weight'],
2671
+ ['lh',{tight:'1.25',normal:'1.5',relaxed:'1.75'},'line-height']
2672
+ ].forEach(function(d) { for (var dk in d[1]) rules['.bw_'+d[0]+'_'+dk] = _imp(d[2], d[1][dk]); });
2673
+
2674
+ // Flex utilities
2675
+ rules['.bw_flex'] = { 'display': 'flex' };
2676
+ rules['.bw_flex_column'] = { 'flex-direction': 'column' };
2677
+ rules['.bw_flex_wrap'] = { 'flex-wrap': 'wrap' };
2678
+ rules['.bw_flex_center'] = { 'display': 'flex', 'align-items': 'center', 'justify-content': 'center' };
2679
+ for (var gk in spacingValues) rules['.bw_gap_' + gk] = { 'gap': spacingValues[gk] + ' !important' };
2680
+
2487
2681
  // Visibility
2488
2682
  rules['.bw_visible, .visible'] = { 'visibility': 'visible !important' };
2489
2683
  rules['.bw_invisible, .invisible'] = { 'visibility': 'hidden !important' };
@@ -2544,6 +2738,26 @@
2544
2738
  return getStructuralCSS();
2545
2739
  }
2546
2740
 
2741
+ /**
2742
+ * Get CSS reset rules only (box-sizing, html/body font, reduced-motion).
2743
+ * Separate from themed/structural rules for independent injection.
2744
+ * @returns {Object} CSS rules object for the reset layer
2745
+ */
2746
+ function getResetStyles() {
2747
+ var rules = {};
2748
+ Object.assign(rules, structuralRules.base);
2749
+ // Include reduced-motion preference
2750
+ rules['@media (prefers-reduced-motion: reduce)'] = {
2751
+ '*, *::before, *::after': {
2752
+ 'animation-duration': '0.01ms !important',
2753
+ 'animation-iteration-count': '1 !important',
2754
+ 'transition-duration': '0.01ms !important',
2755
+ 'scroll-behavior': 'auto !important'
2756
+ }
2757
+ };
2758
+ return rules;
2759
+ }
2760
+
2547
2761
  // =========================================================================
2548
2762
  // defaultStyles — backward-compatible categorized view
2549
2763
  // =========================================================================
@@ -2573,60 +2787,41 @@
2573
2787
  });
2574
2788
 
2575
2789
  /**
2576
- * Generate alternate-palette CSS scoped under `.bw_theme_alt`.
2577
- * Uses the same `generateThemedCSS()` pipeline as the primary palette —
2578
- * both sides go through identical code paths.
2579
- *
2580
- * @param {string} name - Theme scope name (e.g. 'ocean'). '' for global.
2581
- * @param {Object} altPalette - From derivePalette(deriveAlternateConfig(...))
2582
- * @param {Object} layout - From resolveLayout()
2583
- * @returns {Object} CSS rules object scoped under .bw_theme_alt (+ optional .name)
2790
+ * Prefix every selector in a rules object with a scope selector.
2791
+ * Handles @media/@keyframes blocks and comma-separated selectors.
2792
+ * @param {Object} rules - CSS rules object
2793
+ * @param {string} prefix - Scope prefix (e.g. '#my-dashboard', '.bw_theme_alt')
2794
+ * @param {boolean} [compound=false] - If true, use compound selector (no space)
2795
+ * for the first segment: `#scope.bw_theme_alt .sel` vs `#scope .sel`
2796
+ * @returns {Object} New rules object with scoped selectors
2584
2797
  */
2585
- function generateAlternateCSS(name, altPalette, layout) {
2586
- // Generate themed CSS using the same pipeline as primary
2587
- var rawRules = generateThemedCSS('', altPalette, layout);
2588
-
2589
- // Re-scope every selector under .bw_theme_alt (+ optional theme name)
2590
- var altPrefix = name ? '.' + name + '.bw_theme_alt' : '.bw_theme_alt';
2591
- var altRules = {};
2592
-
2593
- for (var sel in rawRules) {
2594
- if (!rawRules.hasOwnProperty(sel)) continue;
2595
-
2798
+ function scopeRulesUnder(rules, prefix, compound) {
2799
+ var scoped = {};
2800
+ for (var sel in rules) {
2801
+ if (!rules.hasOwnProperty(sel)) continue;
2596
2802
  if (sel.charAt(0) === '@') {
2597
2803
  // @media / @keyframes — recurse into the block
2598
- var innerBlock = rawRules[sel];
2599
- var altInner = {};
2804
+ var innerBlock = rules[sel];
2805
+ var scopedInner = {};
2600
2806
  for (var innerSel in innerBlock) {
2601
2807
  if (!innerBlock.hasOwnProperty(innerSel)) continue;
2602
- altInner[altPrefix + ' ' + innerSel] = innerBlock[innerSel];
2808
+ scopedInner[_prefixSelector(innerSel, prefix)] = innerBlock[innerSel];
2603
2809
  }
2604
- altRules[sel] = altInner;
2810
+ scoped[sel] = scopedInner;
2605
2811
  } else {
2606
- // Regular selector — prefix with alt scope
2607
- // Handle comma-separated selectors
2608
- var parts = sel.split(',');
2609
- var scopedParts = [];
2610
- for (var i = 0; i < parts.length; i++) {
2611
- var s = parts[i].trim();
2612
- // 'body' selector gets special treatment: .bw_theme_alt body
2613
- if (s === 'body' || s.indexOf('body') === 0) {
2614
- scopedParts.push(altPrefix + ' ' + s);
2615
- } else {
2616
- scopedParts.push(altPrefix + ' ' + s);
2617
- }
2618
- }
2619
- altRules[scopedParts.join(', ')] = rawRules[sel];
2812
+ scoped[_prefixSelector(sel, prefix)] = rules[sel];
2620
2813
  }
2621
2814
  }
2815
+ return scoped;
2816
+ }
2622
2817
 
2623
- // Add body-level overrides for the alternate surface
2624
- altRules[altPrefix + ' body, :root' + altPrefix + ' body'] = {
2625
- 'color': altPalette.dark.base,
2626
- 'background-color': altPalette.light.base
2627
- };
2628
-
2629
- return altRules;
2818
+ function _prefixSelector(sel, prefix) {
2819
+ var parts = sel.split(',');
2820
+ var result = [];
2821
+ for (var i = 0; i < parts.length; i++) {
2822
+ result.push(prefix + ' ' + parts[i].trim());
2823
+ }
2824
+ return result.join(', ');
2630
2825
  }
2631
2826
 
2632
2827
  /**
@@ -3326,12 +3521,11 @@
3326
3521
  _subIdCounter: 0, // monotonic ID for subscriptions
3327
3522
 
3328
3523
  // ── Node reference cache ──────────────────────────────────────────────
3329
- // Fast O(1) lookup for elements by bw_id, id attribute, or bw_uuid.
3524
+ // Fast O(1) lookup for elements by id attribute or bw_uuid_* class.
3330
3525
  //
3331
3526
  // Populated by bw.createDOM() when elements have:
3332
- // - data-bw_id attribute (user-declared addressable elements)
3333
3527
  // - id attribute (standard HTML id)
3334
- // - bw_uuid (internal, for lifecycle-managed elements)
3528
+ // - bw_uuid_* class (lifecycle-managed or explicitly addressed elements)
3335
3529
  //
3336
3530
  // Cleaned up by bw.cleanup() when elements are destroyed via bitwrench APIs.
3337
3531
  // On cache miss, falls back to querySelector/getElementById — never fails,
@@ -3339,7 +3533,7 @@
3339
3533
  // via parentNode === null check (IE11-safe, unlike el.isConnected).
3340
3534
  //
3341
3535
  // Elements created via bw.createDOM() also get el._bw_refs — a local map of
3342
- // child bw_id DOM node ref for fast parentchild access in o.render.
3536
+ // child id/UUID -> DOM node ref for fast parent->child access in o.render.
3343
3537
  // This is the bitwrench equivalent of React's compiled template "holes".
3344
3538
  //
3345
3539
  // Contract: if you remove elements outside of bitwrench APIs (raw el.remove()),
@@ -3419,7 +3613,6 @@
3419
3613
  // _cw console.warn 8
3420
3614
  // _cl console.log 11
3421
3615
  // _ce console.error 4
3422
- // _chp ComponentHandle.prototype 28 (defined after constructor)
3423
3616
  //
3424
3617
  // Note: document.createElement etc. are NOT aliased because they require
3425
3618
  // `this === document` and .bind() would add overhead on every call.
@@ -3592,15 +3785,15 @@
3592
3785
  * 1. Check `bw._nodeMap[id]` — if found and still attached (parentNode !== null), return it
3593
3786
  * 2. If cached ref is detached (parentNode === null), remove stale entry
3594
3787
  * 3. Fall back to `document.getElementById(id)` then `document.querySelector(...)`
3595
- * 4. If fallback finds the element, cache it for next time
3596
- * 5. If not found anywhere, return null
3788
+ * 4. Try class-based lookup for bw_uuid_* tokens (UUID addressing)
3789
+ * 5. Cache the result for next time
3597
3790
  *
3598
3791
  * Accepts a DOM element directly (pass-through) or a string identifier.
3599
3792
  * String identifiers are tried as: direct map key, getElementById,
3600
3793
  * querySelector (for CSS selectors starting with . or #), and
3601
- * data-bw_id attribute selector.
3794
+ * bw_uuid_* class selector.
3602
3795
  *
3603
- * @param {string|Element} id - Element ID, CSS selector, data-bw_id value, or DOM element
3796
+ * @param {string|Element} id - Element ID, CSS selector, bw_uuid_* class, or DOM element
3604
3797
  * @returns {Element|null} The DOM element, or null if not found
3605
3798
  * @category Internal
3606
3799
  */
@@ -3629,9 +3822,9 @@
3629
3822
  el = document.querySelector(id);
3630
3823
  }
3631
3824
 
3632
- // 4. Try data-bw_id attribute (for bw.uuid-generated IDs)
3633
- if (!el) {
3634
- el = document.querySelector('[data-bw_id="' + id + '"]');
3825
+ // 4. Try class-based lookup for bw_uuid_* tokens (UUID addressing)
3826
+ if (!el && id.indexOf('bw_uuid_') === 0) {
3827
+ el = document.querySelector('.' + id);
3635
3828
  }
3636
3829
 
3637
3830
  // 5. Cache the result for next time
@@ -3646,17 +3839,17 @@
3646
3839
  * Register a DOM element in the node cache under one or more keys.
3647
3840
  *
3648
3841
  * Called internally by `bw.createDOM()`. Registers elements that have
3649
- * id attributes, data-bw_id attributes, or both.
3842
+ * id attributes, UUID classes, or both.
3650
3843
  *
3651
3844
  * @param {Element} el - DOM element to register
3652
- * @param {string} [bwId] - data-bw_id value to register under
3845
+ * @param {string} [uuid] - bw_uuid_* class token to register under
3653
3846
  * @category Internal
3654
3847
  */
3655
- bw._registerNode = function(el, bwId) {
3848
+ bw._registerNode = function(el, uuid) {
3656
3849
  if (!el) return;
3657
- // Register under data-bw_id
3658
- if (bwId) {
3659
- bw._nodeMap[bwId] = el;
3850
+ // Register under UUID class token
3851
+ if (uuid) {
3852
+ bw._nodeMap[uuid] = el;
3660
3853
  }
3661
3854
  // Register under id attribute
3662
3855
  var htmlId = el.getAttribute ? el.getAttribute('id') : null;
@@ -3672,13 +3865,13 @@
3672
3865
  * through bitwrench APIs.
3673
3866
  *
3674
3867
  * @param {Element} el - DOM element to deregister
3675
- * @param {string} [bwId] - data-bw_id value to remove
3868
+ * @param {string} [uuid] - bw_uuid_* class token to remove
3676
3869
  * @category Internal
3677
3870
  */
3678
- bw._deregisterNode = function(el, bwId) {
3679
- // Remove data-bw_id entry
3680
- if (bwId) {
3681
- delete bw._nodeMap[bwId];
3871
+ bw._deregisterNode = function(el, uuid) {
3872
+ // Remove UUID class entry
3873
+ if (uuid) {
3874
+ delete bw._nodeMap[uuid];
3682
3875
  }
3683
3876
  // Remove id attribute entry
3684
3877
  var htmlId = el && el.getAttribute ? el.getAttribute('id') : null;
@@ -3687,6 +3880,91 @@
3687
3880
  }
3688
3881
  };
3689
3882
 
3883
+ // ===================================================================================
3884
+ // bw.assignUUID() / bw.getUUID() — Explicit UUID addressing for TACO objects
3885
+ // ===================================================================================
3886
+
3887
+ /**
3888
+ * Marker class for elements with lifecycle hooks (mounted/unmount/render/state).
3889
+ * Used by cleanup() to find lifecycle-managed elements via querySelectorAll('.bw_lc').
3890
+ * @private
3891
+ */
3892
+ var _BW_LC = 'bw_lc';
3893
+
3894
+ /**
3895
+ * Regex to match a bw_uuid_* token in a class string.
3896
+ * @private
3897
+ */
3898
+ var _UUID_RE = /\bbw_uuid_[a-z0-9_]+\b/;
3899
+
3900
+ /**
3901
+ * Assign a UUID to a TACO object by appending a `bw_uuid_*` token to `taco.a.class`.
3902
+ *
3903
+ * Idempotent by default — calling twice returns the same UUID. Pass `forceNew=true`
3904
+ * to replace an existing UUID (useful in loops where each TACO needs a unique ID).
3905
+ *
3906
+ * @param {Object} taco - A TACO object `{t, a, c, o}`
3907
+ * @param {boolean} [forceNew=false] - If true, replaces any existing UUID with a new one
3908
+ * @returns {string} The UUID string (e.g. 'bw_uuid_a1b2c3d4e5')
3909
+ * @category Identifiers
3910
+ * @example
3911
+ * var card = bw.makeStatCard({ value: '0', label: 'Scans' });
3912
+ * var uuid = bw.assignUUID(card); // 'bw_uuid_a1b2c3d4e5'
3913
+ * var same = bw.assignUUID(card); // same UUID (idempotent)
3914
+ * var diff = bw.assignUUID(card, true); // new UUID (forced)
3915
+ */
3916
+ bw.assignUUID = function(taco, forceNew) {
3917
+ if (!taco || !_is(taco, 'object')) return null;
3918
+
3919
+ // Ensure taco.a exists
3920
+ if (!taco.a) taco.a = {};
3921
+ if (!_is(taco.a.class, 'string')) taco.a.class = taco.a.class ? String(taco.a.class) : '';
3922
+
3923
+ var existing = taco.a.class.match(_UUID_RE);
3924
+
3925
+ if (existing && !forceNew) {
3926
+ return existing[0];
3927
+ }
3928
+
3929
+ // Remove old UUID if forceNew
3930
+ if (existing) {
3931
+ taco.a.class = taco.a.class.replace(_UUID_RE, '').replace(/\s+/g, ' ').trim();
3932
+ }
3933
+
3934
+ var uuid = bw.uuid('uuid');
3935
+ taco.a.class = (taco.a.class ? taco.a.class + ' ' : '') + uuid;
3936
+ return uuid;
3937
+ };
3938
+
3939
+ /**
3940
+ * Read the UUID from a TACO object or DOM element. Pure getter, no side effects.
3941
+ *
3942
+ * @param {Object|Element} tacoOrElement - A TACO object or DOM element
3943
+ * @returns {string|null} The UUID string, or null if none assigned
3944
+ * @category Identifiers
3945
+ * @example
3946
+ * bw.getUUID(card) // 'bw_uuid_a1b2c3d4e5' (from TACO)
3947
+ * bw.getUUID(domEl) // 'bw_uuid_a1b2c3d4e5' (from DOM element)
3948
+ * bw.getUUID({t:'div'}) // null (no UUID)
3949
+ */
3950
+ bw.getUUID = function(tacoOrElement) {
3951
+ if (!tacoOrElement) return null;
3952
+
3953
+ var classStr;
3954
+ // DOM element: check className
3955
+ if (tacoOrElement.className !== undefined && tacoOrElement.tagName) {
3956
+ classStr = tacoOrElement.className;
3957
+ }
3958
+ // TACO object: check a.class
3959
+ else if (tacoOrElement.a && _is(tacoOrElement.a.class, 'string')) {
3960
+ classStr = tacoOrElement.a.class;
3961
+ }
3962
+
3963
+ if (!classStr) return null;
3964
+ var match = classStr.match(_UUID_RE);
3965
+ return match ? match[0] : null;
3966
+ };
3967
+
3690
3968
  /**
3691
3969
  * Escape HTML special characters to prevent XSS.
3692
3970
  *
@@ -3736,6 +4014,42 @@
3736
4014
  return { __bw_raw: true, v: String(str) };
3737
4015
  };
3738
4016
 
4017
+ /**
4018
+ * Hyperscript-style TACO constructor.
4019
+ *
4020
+ * A convenience helper that returns a canonical TACO object from positional
4021
+ * arguments. The return value is a plain object — serializable, works with
4022
+ * bwserve, and accepted everywhere TACO is accepted.
4023
+ *
4024
+ * @param {string} tag - HTML tag name (e.g. 'div', 'p', 'section')
4025
+ * @param {Object|null} [attrs] - HTML attributes object. Pass null or omit to skip.
4026
+ * @param {*} [content] - Content: string, number, TACO object, or array of children.
4027
+ * @param {Object} [options] - TACO options (state, lifecycle hooks, render fn).
4028
+ * @returns {Object} Plain TACO object {t, a?, c?, o?}
4029
+ * @category Utilities
4030
+ * @see bw.html
4031
+ * @see bw.createDOM
4032
+ * @see bw.DOM
4033
+ * @example
4034
+ * bw.h('div')
4035
+ * // => { t: 'div' }
4036
+ *
4037
+ * bw.h('p', { class: 'bw_text_muted' }, 'Hello')
4038
+ * // => { t: 'p', a: { class: 'bw_text_muted' }, c: 'Hello' }
4039
+ *
4040
+ * bw.h('ul', null, [
4041
+ * bw.h('li', null, 'one'),
4042
+ * bw.h('li', null, 'two')
4043
+ * ])
4044
+ * // => { t: 'ul', c: [{ t: 'li', c: 'one' }, { t: 'li', c: 'two' }] }
4045
+ */
4046
+ bw.h = function(tag, attrs, content, options) {
4047
+ var taco = { t: String(tag) };
4048
+ if (attrs !== null && attrs !== undefined) taco.a = attrs;
4049
+ if (content !== undefined) taco.c = content;
4050
+ if (options !== undefined) taco.o = options;
4051
+ return taco;
4052
+ };
3739
4053
 
3740
4054
  /**
3741
4055
  * Convert a TACO object (or array of TACOs) to an HTML string.
@@ -3765,15 +4079,6 @@
3765
4079
  // Handle null/undefined
3766
4080
  if (taco == null) return '';
3767
4081
 
3768
- // Handle ComponentHandle — use its .taco
3769
- if (taco && taco._bwComponent === true) {
3770
- var compOptions = Object.assign({}, options);
3771
- if (!compOptions.state && taco._state) {
3772
- compOptions.state = taco._state;
3773
- }
3774
- return bw.html(taco.taco, compOptions);
3775
- }
3776
-
3777
4082
  // Handle arrays of TACOs
3778
4083
  if (_isA(taco)) {
3779
4084
  return taco.map(t => bw.html(t, options)).join('');
@@ -3784,24 +4089,6 @@
3784
4089
  return taco.v;
3785
4090
  }
3786
4091
 
3787
- // Handle bw.when() markers
3788
- if (taco && taco._bwWhen && options.state) {
3789
- var whenExpr = taco.expr.replace(/^\$\{|\}$/g, '');
3790
- var whenVal = options.compile
3791
- ? bw._resolveTemplate('${' + whenExpr + '}', options.state, true)
3792
- : bw._evaluatePath(options.state, whenExpr);
3793
- var branch = whenVal ? taco.branches[0] : (taco.branches[1] || null);
3794
- return branch ? bw.html(branch, options) : '';
3795
- }
3796
-
3797
- // Handle bw.each() markers
3798
- if (taco && taco._bwEach && options.state) {
3799
- var eachExpr = taco.expr.replace(/^\$\{|\}$/g, '');
3800
- var arr = bw._evaluatePath(options.state, eachExpr);
3801
- if (!_isA(arr)) return '';
3802
- return arr.map(function(item, idx) { return bw.html(taco.factory(item, idx), options); }).join('');
3803
- }
3804
-
3805
4092
  // Handle primitives and non-TACO objects
3806
4093
  if (!_is(taco, 'object') || !taco.t) {
3807
4094
  var str = options.raw ? String(taco) : bw.escapeHTML(String(taco));
@@ -3865,14 +4152,14 @@
3865
4152
  }
3866
4153
  }
3867
4154
 
3868
- // Add bw_id as a class if lifecycle hooks present
3869
- if ((opts.mounted || opts.unmount) && !attrs.class?.includes('bw_id_')) {
3870
- const id = opts.bw_id || bw.uuid();
4155
+ // Add bw_uuid + bw_lc classes if lifecycle hooks present
4156
+ if ((opts.mounted || opts.unmount) && !_UUID_RE.test(attrs.class || '')) {
4157
+ const uuid = bw.uuid('uuid');
3871
4158
  attrStr = attrStr.replace(/class="([^"]*)"/, (_match, classes) => {
3872
- return `class="${classes} bw_id_${id}"`.trim();
4159
+ return `class="${classes} ${uuid} ${_BW_LC}"`.trim();
3873
4160
  });
3874
4161
  if (!attrStr.includes('class=')) {
3875
- attrStr += ` class="bw_id_${id}"`;
4162
+ attrStr += ` class="${uuid} ${_BW_LC}"`;
3876
4163
  }
3877
4164
  }
3878
4165
 
@@ -4000,7 +4287,7 @@
4000
4287
  ? (THEME_PRESETS[theme.toLowerCase()] || null)
4001
4288
  : theme;
4002
4289
  if (themeConfig) {
4003
- var themeResult = bw.generateTheme('', Object.assign({}, themeConfig, { inject: false }));
4290
+ var themeResult = bw.makeStyles(themeConfig);
4004
4291
  themeCSS = themeResult.css;
4005
4292
  }
4006
4293
  }
@@ -4026,14 +4313,14 @@
4026
4313
  // Combine all CSS
4027
4314
  var allCSS = (themeCSS ? themeCSS + '\n' : '') + css;
4028
4315
 
4029
- // Body-end script: registry entries + optional loadDefaultStyles
4316
+ // Body-end script: registry entries + optional loadStyles
4030
4317
  var bodyEndScript = '';
4031
4318
  var bodyEndParts = [];
4032
4319
  if (registryEntries) {
4033
4320
  bodyEndParts.push(registryEntries);
4034
4321
  }
4035
4322
  if (runtime === 'inline' || runtime === 'cdn') {
4036
- bodyEndParts.push('if(typeof bw!=="undefined"){bw.loadDefaultStyles();}');
4323
+ bodyEndParts.push('if(typeof bw!=="undefined"){bw.loadStyles();}');
4037
4324
  }
4038
4325
  if (bodyEndParts.length > 0) {
4039
4326
  bodyEndScript = '<script>\n' + bodyEndParts.join('\n') + '\n</script>';
@@ -4100,11 +4387,6 @@
4100
4387
  return frag;
4101
4388
  }
4102
4389
 
4103
- // Handle ComponentHandle — extract .taco for DOM creation
4104
- if (taco && taco._bwComponent === true) {
4105
- return bw.createDOM(taco.taco, options);
4106
- }
4107
-
4108
4390
  // Handle text nodes
4109
4391
  if (!_is(taco, 'object') || !taco.t) {
4110
4392
  return document.createTextNode(String(taco));
@@ -4145,24 +4427,19 @@
4145
4427
  }
4146
4428
 
4147
4429
  // Add children, building _bw_refs for fast parent→child access.
4148
- // Children with data-bw_id or id attributes get local refs on the parent,
4430
+ // Children with id attributes or bw_uuid_* classes get local refs on the parent,
4149
4431
  // so o.render functions can access them without any DOM lookup.
4150
4432
  if (content != null) {
4151
4433
  if (_isA(content)) {
4152
4434
  content.forEach(child => {
4153
4435
  if (child != null) {
4154
- // Handle ComponentHandle in content arrays (Level 2 children)
4155
- if (child._bwComponent === true) {
4156
- child.mount(el);
4157
- return;
4158
- }
4159
4436
  var childEl = bw.createDOM(child, options);
4160
4437
  el.appendChild(childEl);
4161
4438
  // Build local refs for addressable children
4162
- var childBwId = (child && child.a) ? (child.a['data-bw_id'] || child.a.id) : null;
4163
- if (childBwId) {
4439
+ var childRefId = (child && child.a) ? (child.a.id || bw.getUUID(child)) : null;
4440
+ if (childRefId) {
4164
4441
  if (!el._bw_refs) el._bw_refs = {};
4165
- el._bw_refs[childBwId] = childEl;
4442
+ el._bw_refs[childRefId] = childEl;
4166
4443
  }
4167
4444
  // Bubble up grandchild refs (flatten one level)
4168
4445
  if (childEl._bw_refs) {
@@ -4178,16 +4455,13 @@
4178
4455
  } else if (_is(content, 'object') && content.__bw_raw) {
4179
4456
  // Raw HTML content — inject via innerHTML
4180
4457
  el.innerHTML = content.v;
4181
- } else if (content._bwComponent === true) {
4182
- // Single ComponentHandle as content
4183
- content.mount(el);
4184
4458
  } else if (_is(content, 'object') && content.t) {
4185
4459
  var childEl = bw.createDOM(content, options);
4186
4460
  el.appendChild(childEl);
4187
- var childBwId = content.a ? (content.a['data-bw_id'] || content.a.id) : null;
4188
- if (childBwId) {
4461
+ var childRefId = content.a ? (content.a.id || bw.getUUID(content)) : null;
4462
+ if (childRefId) {
4189
4463
  if (!el._bw_refs) el._bw_refs = {};
4190
- el._bw_refs[childBwId] = childEl;
4464
+ el._bw_refs[childRefId] = childEl;
4191
4465
  }
4192
4466
  if (childEl._bw_refs) {
4193
4467
  if (!el._bw_refs) el._bw_refs = {};
@@ -4207,59 +4481,98 @@
4207
4481
  bw._registerNode(el, null);
4208
4482
  }
4209
4483
 
4484
+ // Register UUID class in node cache (bw_uuid_* tokens in class string)
4485
+ if (el.className) {
4486
+ var uuidMatch = el.className.match(_UUID_RE);
4487
+ if (uuidMatch) {
4488
+ bw._nodeMap[uuidMatch[0]] = el;
4489
+ }
4490
+ }
4491
+
4210
4492
  // Handle lifecycle hooks and state
4211
4493
  if (opts.mounted || opts.unmount || opts.render || opts.state) {
4212
- const id = attrs['data-bw_id'] || bw.uuid();
4213
- el.setAttribute('data-bw_id', id);
4494
+ // Ensure element has a UUID class for identity
4495
+ var uuid = bw.getUUID(el) || bw.uuid('uuid');
4496
+ el.classList.add(uuid);
4497
+ el.classList.add(_BW_LC);
4214
4498
 
4215
- // Register in node cache under data-bw_id
4216
- bw._registerNode(el, id);
4499
+ // Register in node cache under UUID class
4500
+ bw._registerNode(el, uuid);
4217
4501
 
4218
4502
  // Store state
4219
4503
  if (opts.state) {
4220
4504
  el._bw_state = opts.state;
4221
4505
  }
4222
4506
 
4223
- // o.render — first-class render function (replaces mounted boilerplate)
4507
+ // o.render — store the render function for bw.update()
4224
4508
  if (opts.render) {
4225
4509
  el._bw_render = opts.render;
4510
+ }
4226
4511
 
4227
- if (opts.mounted) {
4228
- _cw('bw.createDOM: o.render and o.mounted are mutually exclusive. o.render wins.');
4229
- }
4512
+ // Determine what to call on mount:
4513
+ // - If o.mounted exists, call it (it can call el._bw_render() for initial render)
4514
+ // - Otherwise if o.render exists, auto-call it as a convenience shorthand
4515
+ var mountFn = opts.mounted || (opts.render ? function(mountEl) {
4516
+ opts.render(mountEl, mountEl._bw_state || {});
4517
+ } : null);
4230
4518
 
4231
- // Queue initial render (same timing as mounted)
4232
- if (document.body.contains(el)) {
4233
- opts.render(el, el._bw_state || {});
4234
- } else {
4235
- requestAnimationFrame(() => {
4236
- if (document.body.contains(el)) {
4237
- opts.render(el, el._bw_state || {});
4238
- }
4239
- });
4240
- }
4241
- } else if (opts.mounted) {
4242
- // Queue mounted callback (legacy pattern)
4519
+ if (mountFn) {
4243
4520
  if (document.body.contains(el)) {
4244
- opts.mounted(el, el._bw_state || {});
4521
+ mountFn(el, el._bw_state || {});
4245
4522
  } else {
4246
4523
  requestAnimationFrame(() => {
4247
4524
  if (document.body.contains(el)) {
4248
- opts.mounted(el, el._bw_state || {});
4525
+ mountFn(el, el._bw_state || {});
4249
4526
  }
4250
4527
  });
4251
4528
  }
4252
4529
  }
4253
4530
 
4254
- // Store unmount callback
4531
+ // Store unmount callback keyed by UUID class
4255
4532
  if (opts.unmount) {
4256
- bw._unmountCallbacks.set(id, () => {
4533
+ bw._unmountCallbacks.set(uuid, () => {
4257
4534
  opts.unmount(el, el._bw_state || {});
4258
4535
  });
4259
4536
  }
4260
- } else if (attrs['data-bw_id']) {
4261
- // Element has explicit data-bw_id but no lifecycle hooks — still register it
4262
- bw._registerNode(el, attrs['data-bw_id']);
4537
+ }
4538
+
4539
+ // Component handle: attach methods to el.bw namespace
4540
+ if (opts.handle || opts.slots) {
4541
+ if (!el.bw) el.bw = {};
4542
+
4543
+ // Explicit handle methods: fn(el, ...args) -> el.bw.method(...args)
4544
+ if (opts.handle) {
4545
+ for (var hk in opts.handle) {
4546
+ if (_hop.call(opts.handle, hk)) {
4547
+ el.bw[hk] = opts.handle[hk].bind(null, el);
4548
+ }
4549
+ }
4550
+ }
4551
+
4552
+ // Slot declarations: auto-generate setX/getX pairs
4553
+ if (opts.slots) {
4554
+ for (var sk in opts.slots) {
4555
+ if (_hop.call(opts.slots, sk)) {
4556
+ (function(name, selector) {
4557
+ var cap = name.charAt(0).toUpperCase() + name.slice(1);
4558
+ el.bw['set' + cap] = function(value) {
4559
+ var t = el.querySelector(selector);
4560
+ if (!t) return;
4561
+ if (value != null && typeof value === 'object' && value.t) {
4562
+ t.innerHTML = '';
4563
+ t.appendChild(bw.createDOM(value));
4564
+ } else {
4565
+ t.textContent = (value != null) ? String(value) : '';
4566
+ }
4567
+ };
4568
+ el.bw['get' + cap] = function() {
4569
+ var t = el.querySelector(selector);
4570
+ return t ? t.textContent : '';
4571
+ };
4572
+ })(sk, opts.slots[sk]);
4573
+ }
4574
+ }
4575
+ }
4263
4576
  }
4264
4577
 
4265
4578
  return el;
@@ -4306,7 +4619,7 @@
4306
4619
  // the target is the mount point, not the content being replaced)
4307
4620
  const savedState = targetEl._bw_state;
4308
4621
  const savedRender = targetEl._bw_render;
4309
- const savedBwId = targetEl.getAttribute('data-bw_id');
4622
+ const savedUuid = bw.getUUID(targetEl);
4310
4623
  const savedSubs = targetEl._bw_subs;
4311
4624
 
4312
4625
  // Temporarily remove _bw_subs so cleanup doesn't call them
@@ -4318,10 +4631,9 @@
4318
4631
  // Restore the target's own state/render/subs after cleanup
4319
4632
  if (savedState !== undefined) targetEl._bw_state = savedState;
4320
4633
  if (savedRender) targetEl._bw_render = savedRender;
4321
- if (savedBwId) {
4322
- targetEl.setAttribute('data-bw_id', savedBwId);
4323
- // Re-register mount point in node cache (cleanup deregistered it)
4324
- bw._registerNode(targetEl, savedBwId);
4634
+ if (savedUuid) {
4635
+ // UUID class stays on element through cleanup; re-register in cache
4636
+ bw._registerNode(targetEl, savedUuid);
4325
4637
  }
4326
4638
  if (savedSubs) targetEl._bw_subs = savedSubs;
4327
4639
 
@@ -4329,25 +4641,11 @@
4329
4641
  targetEl.innerHTML = '';
4330
4642
 
4331
4643
  if (taco != null) {
4332
- // Handle ComponentHandle (reactive components from bw.component())
4333
- if (taco._bwComponent === true) {
4334
- taco.mount(targetEl);
4335
- }
4336
- // Handle component handles (objects with element property)
4337
- else if (taco.element instanceof Element) {
4338
- targetEl.appendChild(taco.element);
4339
- }
4340
4644
  // Handle arrays
4341
- else if (_isA(taco)) {
4645
+ if (_isA(taco)) {
4342
4646
  taco.forEach(t => {
4343
4647
  if (t != null) {
4344
- if (t._bwComponent === true) {
4345
- t.mount(targetEl);
4346
- } else if (t.element instanceof Element) {
4347
- targetEl.appendChild(t.element);
4348
- } else {
4349
- targetEl.appendChild(bw.createDOM(t, options));
4350
- }
4648
+ targetEl.appendChild(bw.createDOM(t, options));
4351
4649
  }
4352
4650
  });
4353
4651
  }
@@ -4360,240 +4658,80 @@
4360
4658
  return targetEl;
4361
4659
  };
4362
4660
 
4661
+ // Deprecation stubs for removed ComponentHandle APIs
4662
+ bw.compileProps = function() { throw new Error('bw.compileProps() removed in v2.0.19. Use o.handle/o.slots instead.'); };
4663
+ bw.renderComponent = function() { throw new Error('bw.renderComponent() removed in v2.0.19. Use bw.mount() with o.handle/o.slots instead.'); };
4664
+
4363
4665
  /**
4364
- * Compile props into getter/setter functions for reactive updates.
4365
- *
4366
- * Used internally by `bw.renderComponent()`. Creates a proxy-like object
4367
- * where setting a property triggers `handle.onPropChange()`.
4666
+ * Mount a TACO into a target element and return the created root element.
4667
+ * Like bw.DOM() but returns the root element of the TACO (not the container),
4668
+ * giving direct access to el.bw handle methods.
4368
4669
  *
4369
- * @param {Object} handle - Component handle
4370
- * @param {Object} props - Initial props
4371
- * @returns {Object} Compiled props object with getters/setters
4670
+ * @param {string|Element} target - CSS selector or DOM element
4671
+ * @param {Object} taco - TACO to render
4672
+ * @param {Object} [options] - Mount options
4673
+ * @returns {Element} The created root element
4372
4674
  * @category DOM Generation
4373
- */
4374
- bw.compileProps = function(handle, props = {}) {
4375
- const compiledProps = {};
4376
-
4377
- _keys(props).forEach(key => {
4378
- // Create getter/setter for each prop
4379
- Object.defineProperty(compiledProps, key, {
4380
- get() {
4381
- return handle._props[key];
4382
- },
4383
- set(value) {
4384
- const oldValue = handle._props[key];
4385
- if (oldValue !== value) {
4386
- handle._props[key] = value;
4387
- // Trigger update if prop changed
4388
- if (handle.onPropChange) {
4389
- handle.onPropChange(key, value, oldValue);
4390
- }
4391
- }
4392
- },
4393
- enumerable: true,
4394
- configurable: true
4395
- });
4396
- });
4397
-
4398
- return compiledProps;
4675
+ * @example
4676
+ * var el = bw.mount('#app', bw.makeCarousel({ items: slides }));
4677
+ * el.bw.goToSlide(2);
4678
+ * el.bw.next();
4679
+ */
4680
+ bw.mount = function(target, taco, options) {
4681
+ var container = _is(target, 'string') ? bw.$(target)[0] : target;
4682
+ if (!container) {
4683
+ _cw('bw.mount: target not found');
4684
+ return null;
4685
+ }
4686
+ bw.cleanup(container);
4687
+ container.innerHTML = '';
4688
+ var el = bw.createDOM(taco, options || {});
4689
+ container.appendChild(el);
4690
+ return el;
4399
4691
  };
4400
4692
 
4401
4693
  /**
4402
- * Render a TACO component and return an enhanced handle object.
4694
+ * Clean up a DOM element and all its children by calling unmount callbacks,
4695
+ * removing pub/sub subscriptions, and clearing state/render references.
4403
4696
  *
4404
- * The handle provides compiled props, state management, child registration,
4405
- * and a destroy method. Used internally by `bw.createCard()`, `bw.createTable()`, etc.
4697
+ * Called automatically by `bw.DOM()` before re-rendering. Call manually when
4698
+ * removing elements to prevent memory leaks from orphaned callbacks.
4406
4699
  *
4407
- * @param {Object} taco - TACO object to render
4408
- * @param {Object} [options] - Render options
4409
- * @returns {Object} Component handle with element, props, state, update(), destroy()
4700
+ * @param {Element} element - DOM element to clean up
4410
4701
  * @category DOM Generation
4702
+ * @see bw.DOM
4703
+ * @example
4704
+ * var el = document.querySelector('#my-widget');
4705
+ * bw.cleanup(el); // runs unmount hooks, clears _bw_state, _bw_render
4706
+ * el.remove(); // safe to remove from DOM now
4411
4707
  */
4412
- bw.renderComponent = function(taco, options = {}) {
4413
- const element = bw.createDOM(taco, options);
4414
-
4415
- // Enhanced handle with prop compilation
4416
- const handle = {
4417
- element,
4418
- taco,
4419
- _props: { ...taco.a }, // Store props internally
4420
- _state: taco.o?.state || {},
4421
- _children: {}, // Store child component references
4422
-
4423
- // Get compiled props with getters/setters
4424
- get props() {
4425
- if (!this._compiledProps) {
4426
- this._compiledProps = bw.compileProps(this, this._props);
4708
+ bw.cleanup = function(element) {
4709
+ if (!bw._isBrowser || !element) return;
4710
+
4711
+ // Deregister UUID classes from node cache for non-lifecycle UUID elements
4712
+ var uuidEls = element.querySelectorAll('[class*="bw_uuid_"]');
4713
+ uuidEls.forEach(function(uel) {
4714
+ var m = uel.className && uel.className.match(_UUID_RE);
4715
+ if (m) delete bw._nodeMap[m[0]];
4716
+ });
4717
+
4718
+ // Find all lifecycle-managed elements (have bw_lc marker class)
4719
+ const elements = element.querySelectorAll('.' + _BW_LC);
4720
+
4721
+ elements.forEach(el => {
4722
+ var uuid = bw.getUUID(el);
4723
+
4724
+ if (uuid) {
4725
+ const callback = bw._unmountCallbacks.get(uuid);
4726
+ if (callback) {
4727
+ callback();
4728
+ bw._unmountCallbacks.delete(uuid);
4427
4729
  }
4428
- return this._compiledProps;
4429
- },
4430
-
4431
- /**
4432
- * Query all matching elements within this component
4433
- * @param {string} selector - CSS selector
4434
- * @returns {NodeList} Matching elements
4435
- */
4436
- $(selector) {
4437
- return this.element.querySelectorAll(selector);
4438
- },
4439
-
4440
- /**
4441
- * Query the first matching element within this component
4442
- * @param {string} selector - CSS selector
4443
- * @returns {Element|null} First matching element or null
4444
- */
4445
- $first(selector) {
4446
- return this.element.querySelector(selector);
4447
- },
4448
-
4449
- /**
4450
- * Update component with new props and re-render in place
4451
- * @param {Object} newProps - Properties to merge into current props
4452
- * @returns {Object} this handle (for chaining)
4453
- */
4454
- update(newProps) {
4455
- // Update internal props
4456
- Object.assign(this._props, newProps);
4457
-
4458
- // Rebuild TACO with new props
4459
- const newTaco = { ...this.taco, a: { ...this.taco.a, ...newProps } };
4460
- const newElement = bw.createDOM(newTaco, options);
4461
-
4462
- // Replace in DOM
4463
- this.element.replaceWith(newElement);
4464
- this.element = newElement;
4465
- this.taco = newTaco;
4466
-
4467
- return this;
4468
- },
4469
-
4470
- /**
4471
- * Re-render the component from its current TACO, replacing the DOM element
4472
- * @returns {Object} this handle (for chaining)
4473
- */
4474
- render() {
4475
- const newElement = bw.createDOM(this.taco, options);
4476
- this.element.replaceWith(newElement);
4477
- this.element = newElement;
4478
- return this;
4479
- },
4480
-
4481
- /**
4482
- * Called when a compiled prop value changes. Override to customize behavior.
4483
- * Default implementation triggers a full re-render.
4484
- * @param {string} key - Property name that changed
4485
- * @param {*} newValue - New property value
4486
- * @param {*} oldValue - Previous property value
4487
- */
4488
- onPropChange(_key, _newValue, _oldValue) {
4489
- // Auto re-render on prop change by default
4490
- this.render();
4491
- },
4492
-
4493
- // State management
4494
- get state() {
4495
- return this._state;
4496
- },
4497
-
4498
- set state(newState) {
4499
- this._state = newState;
4500
- this.render();
4501
- },
4502
-
4503
- /**
4504
- * Merge state updates and re-render the component
4505
- * @param {Object} updates - State properties to merge
4506
- * @returns {Object} this handle (for chaining)
4507
- */
4508
- setState(updates) {
4509
- Object.assign(this._state, updates);
4510
- this.render();
4511
- return this;
4512
- },
4513
-
4514
- /**
4515
- * Register a child component under a name for later retrieval
4516
- * @param {string} name - Child name key
4517
- * @param {Object} component - Child component handle
4518
- * @returns {Object} this handle (for chaining)
4519
- */
4520
- addChild(name, component) {
4521
- this._children[name] = component;
4522
- return this;
4523
- },
4524
-
4525
- /**
4526
- * Retrieve a registered child component by name
4527
- * @param {string} name - Child name key
4528
- * @returns {Object|undefined} Child component handle
4529
- */
4530
- getChild(name) {
4531
- return this._children[name];
4532
- },
4533
-
4534
- /**
4535
- * Destroy this component and all registered children
4536
- *
4537
- * Calls destroy() recursively on children, runs bw.cleanup(),
4538
- * removes the element from DOM, and clears all internal references.
4539
- */
4540
- destroy() {
4541
- // Destroy children first
4542
- Object.values(this._children).forEach(child => {
4543
- if (child && child.destroy) child.destroy();
4544
- });
4545
-
4546
- // Clean up this component
4547
- bw.cleanup(this.element);
4548
- this.element.remove();
4549
-
4550
- // Clear references
4551
- this._children = {};
4552
- this._props = {};
4553
- this._state = {};
4554
- this._compiledProps = null;
4555
- }
4556
- };
4557
-
4558
- // Store handle reference on element
4559
- element._bwHandle = handle;
4560
-
4561
- return handle;
4562
- };
4563
-
4564
- /**
4565
- * Clean up a DOM element and all its children by calling unmount callbacks,
4566
- * removing pub/sub subscriptions, and clearing state/render references.
4567
- *
4568
- * Called automatically by `bw.DOM()` before re-rendering. Call manually when
4569
- * removing elements to prevent memory leaks from orphaned callbacks.
4570
- *
4571
- * @param {Element} element - DOM element to clean up
4572
- * @category DOM Generation
4573
- * @see bw.DOM
4574
- * @example
4575
- * var el = document.querySelector('#my-widget');
4576
- * bw.cleanup(el); // runs unmount hooks, clears _bw_state, _bw_render
4577
- * el.remove(); // safe to remove from DOM now
4578
- */
4579
- bw.cleanup = function(element) {
4580
- if (!bw._isBrowser || !element) return;
4581
-
4582
- // Find all elements with data-bw_id
4583
- const elements = element.querySelectorAll('[data-bw_id]');
4584
-
4585
- elements.forEach(el => {
4586
- const id = el.getAttribute('data-bw_id');
4587
- const callback = bw._unmountCallbacks.get(id);
4588
4730
 
4589
- if (callback) {
4590
- callback();
4591
- bw._unmountCallbacks.delete(id);
4731
+ // Deregister from node cache
4732
+ bw._deregisterNode(el, uuid);
4592
4733
  }
4593
4734
 
4594
- // Deregister from node cache
4595
- bw._deregisterNode(el, id);
4596
-
4597
4735
  // Clean up pub/sub subscriptions tied to this element
4598
4736
  if (el._bw_subs) {
4599
4737
  el._bw_subs.forEach(function(unsub) { unsub(); });
@@ -4607,16 +4745,18 @@
4607
4745
  });
4608
4746
 
4609
4747
  // Check element itself
4610
- const id = element.getAttribute('data-bw_id');
4611
- if (id) {
4612
- const callback = bw._unmountCallbacks.get(id);
4748
+ var selfUuid = bw.getUUID(element);
4749
+ if (selfUuid) {
4750
+ delete bw._nodeMap[selfUuid];
4751
+
4752
+ const callback = bw._unmountCallbacks.get(selfUuid);
4613
4753
  if (callback) {
4614
4754
  callback();
4615
- bw._unmountCallbacks.delete(id);
4755
+ bw._unmountCallbacks.delete(selfUuid);
4616
4756
  }
4617
4757
 
4618
4758
  // Deregister from node cache
4619
- bw._deregisterNode(element, id);
4759
+ bw._deregisterNode(element, selfUuid);
4620
4760
 
4621
4761
  // Clean up pub/sub subscriptions tied to element itself
4622
4762
  if (element._bw_subs) {
@@ -4627,11 +4767,11 @@
4627
4767
  delete element._bw_render;
4628
4768
  delete element._bw_refs;
4629
4769
 
4630
- // Clean up ComponentHandle back-reference
4631
- if (element._bwComponentHandle) {
4632
- element._bwComponentHandle.mounted = false;
4633
- element._bwComponentHandle.element = null;
4634
- delete element._bwComponentHandle;
4770
+ } else {
4771
+ // No UUID on element itself, but still check for _bw_subs (from bw.sub())
4772
+ if (element._bw_subs) {
4773
+ element._bw_subs.forEach(function(unsub) { unsub(); });
4774
+ delete element._bw_subs;
4635
4775
  }
4636
4776
  }
4637
4777
  };
@@ -4647,7 +4787,7 @@
4647
4787
  * Calls `el._bw_render(el, state)` and emits `bw:statechange` so other
4648
4788
  * components can react without tight coupling.
4649
4789
  *
4650
- * @param {string|Element} target - Element ID, data-bw_id, CSS selector, or DOM element
4790
+ * @param {string|Element} target - Element ID, bw_uuid_* class, CSS selector, or DOM element
4651
4791
  * @returns {Element|null} The element, or null if not found / no render function
4652
4792
  * @category State Management
4653
4793
  * @see bw.patch
@@ -4672,7 +4812,7 @@
4672
4812
  * Use `bw.patch()` for lightweight value updates (scores, labels, counters)
4673
4813
  * and `bw.update()` for full structural re-renders.
4674
4814
  *
4675
- * @param {string|Element} id - Element ID, data-bw_id, CSS selector, or DOM element.
4815
+ * @param {string|Element} id - Element ID, bw_uuid_* class, CSS selector, or DOM element.
4676
4816
  * Uses node cache for O(1) lookup; falls back to DOM query on cache miss.
4677
4817
  * @param {string|Object} content - New text content, or TACO object to replace children
4678
4818
  * @param {string} [attr] - If provided, sets this attribute instead of content
@@ -4747,7 +4887,7 @@
4747
4887
  * bubble by default so ancestor elements can listen. Use with `bw.on()` for
4748
4888
  * DOM-scoped communication between components.
4749
4889
  *
4750
- * @param {string|Element} target - Element ID, data-bw_id, CSS selector, or DOM element.
4890
+ * @param {string|Element} target - Element ID, bw_uuid_* class, CSS selector, or DOM element.
4751
4891
  * Uses node cache for O(1) lookup; falls back to DOM query on cache miss.
4752
4892
  * @param {string} eventName - Event name (will be prefixed with 'bw:')
4753
4893
  * @param {*} [detail] - Data to pass with the event
@@ -4774,7 +4914,7 @@
4774
4914
  * is the first argument so you don't need to destructure `e.detail`.
4775
4915
  * Events bubble, so you can listen on an ancestor element.
4776
4916
  *
4777
- * @param {string|Element} target - Element ID, data-bw_id, CSS selector, or DOM element.
4917
+ * @param {string|Element} target - Element ID, bw_uuid_* class, CSS selector, or DOM element.
4778
4918
  * Uses node cache for O(1) lookup; falls back to DOM query on cache miss.
4779
4919
  * @param {string} eventName - Event name (will be prefixed with 'bw:')
4780
4920
  * @param {Function} handler - Called with (detail, event)
@@ -4872,10 +5012,12 @@
4872
5012
  if (el) {
4873
5013
  if (!el._bw_subs) el._bw_subs = [];
4874
5014
  el._bw_subs.push(unsub);
4875
- // Ensure element has data-bw_id so bw.cleanup() finds it
4876
- if (!el.getAttribute('data-bw_id')) {
4877
- var bwId = 'bw_sub_' + id;
4878
- el.setAttribute('data-bw_id', bwId);
5015
+ // Ensure element has UUID + bw_lc so bw.cleanup() finds it
5016
+ if (!bw.getUUID(el)) {
5017
+ el.classList.add(bw.uuid('uuid'));
5018
+ }
5019
+ if (!el.classList.contains(_BW_LC)) {
5020
+ el.classList.add(_BW_LC);
4879
5021
  }
4880
5022
  }
4881
5023
 
@@ -5082,1102 +5224,61 @@
5082
5224
  }
5083
5225
  try {
5084
5226
  val = bw._compiledExprs[b.expr](state);
5085
- } catch (e) {
5086
- if (bw.debug) _cw('bw.debug: _resolveTemplate — Tier 2 eval failed for "${' + b.expr + '}":', e.message);
5087
- val = '';
5088
- }
5089
- } else {
5090
- // Tier 1: dot-path only
5091
- val = bw._evaluatePath(state, b.expr);
5092
- }
5093
- result += (val == null) ? '' : String(val);
5094
- lastEnd = b.end;
5095
- }
5096
- result += str.slice(lastEnd);
5097
- return result;
5098
- };
5099
-
5100
- /**
5101
- * Extract top-level state keys that an expression depends on.
5102
- * @param {string} expr - Expression string
5103
- * @param {string[]} stateKeys - Declared state keys
5104
- * @returns {string[]} Matching dependency keys
5105
- * @private
5106
- */
5107
- bw._extractDeps = function(expr, stateKeys) {
5108
- var deps = [];
5109
- for (var i = 0; i < stateKeys.length; i++) {
5110
- var key = stateKeys[i];
5111
- // Match word boundary: key must be preceded by start/non-word and followed by non-word/end
5112
- var re = new RegExp('(?:^|[^\\w$.])' + key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '(?:[^\\w$]|$)');
5113
- if (re.test(expr) || expr === key || expr.indexOf(key + '.') === 0) {
5114
- deps.push(key);
5115
- }
5116
- }
5117
- return deps;
5118
- };
5119
-
5120
- // ===================================================================================
5121
- // Microtask Batching
5122
- // ===================================================================================
5123
-
5124
- bw._dirtyComponents = [];
5125
- bw._flushScheduled = false;
5126
-
5127
- /**
5128
- * Schedule a microtask flush for dirty components.
5129
- * @private
5130
- */
5131
- bw._scheduleFlush = function() {
5132
- if (bw._flushScheduled) return;
5133
- bw._flushScheduled = true;
5134
- if (typeof Promise !== 'undefined') {
5135
- Promise.resolve().then(bw._doFlush);
5136
- } else {
5137
- setTimeout(bw._doFlush, 0);
5138
- }
5139
- };
5140
-
5141
- /**
5142
- * Flush all dirty components. Deduplicates by _bwId.
5143
- * @private
5144
- */
5145
- bw._doFlush = function() {
5146
- bw._flushScheduled = false;
5147
- var queue = bw._dirtyComponents.slice();
5148
- bw._dirtyComponents = [];
5149
- // Deduplicate by _bwId
5150
- var seen = {};
5151
- for (var i = 0; i < queue.length; i++) {
5152
- var comp = queue[i];
5153
- if (!seen[comp._bwId]) {
5154
- seen[comp._bwId] = true;
5155
- comp._flush();
5156
- }
5157
- }
5158
- };
5159
-
5160
- /**
5161
- * Synchronous flush for testing and imperative code.
5162
- * Forces immediate re-render of all dirty components.
5163
- *
5164
- * @category Component
5165
- */
5166
- bw.flush = function() {
5167
- bw._doFlush();
5168
- };
5169
-
5170
- // ===================================================================================
5171
- // ComponentHandle — unified reactive component (Phase 1)
5172
- // ===================================================================================
5173
-
5174
- /**
5175
- * ComponentHandle constructor.
5176
- * Wraps a TACO definition with reactive state, lifecycle hooks,
5177
- * template bindings, and named actions.
5178
- *
5179
- * @param {Object} taco - TACO definition {t, a, c, o}
5180
- * @constructor
5181
- * @private
5182
- */
5183
- function ComponentHandle(taco) {
5184
- this._bwComponent = true; // duck-type marker
5185
- this._bwId = bw.uuid('comp');
5186
- this.taco = taco;
5187
- this.element = null;
5188
- this.mounted = false;
5189
-
5190
- var o = taco.o || {};
5191
- // Copy initial state
5192
- this._state = {};
5193
- if (o.state) {
5194
- for (var k in o.state) {
5195
- if (_hop.call(o.state, k)) {
5196
- this._state[k] = o.state[k];
5197
- }
5198
- }
5199
- }
5200
- // Copy actions
5201
- this._actions = {};
5202
- if (o.actions) {
5203
- for (var k2 in o.actions) {
5204
- if (_hop.call(o.actions, k2)) {
5205
- this._actions[k2] = o.actions[k2];
5206
- }
5207
- }
5208
- }
5209
- // Promote o.methods to handle API (MFC/Qt pattern: component owns its methods)
5210
- this._methods = {};
5211
- if (o.methods) {
5212
- var self = this;
5213
- for (var k3 in o.methods) {
5214
- if (_hop.call(o.methods, k3)) {
5215
- this._methods[k3] = o.methods[k3];
5216
- (function(methodName, methodFn) {
5217
- self[methodName] = function() {
5218
- var args = [self].concat(Array.prototype.slice.call(arguments));
5219
- return methodFn.apply(null, args);
5220
- };
5221
- })(k3, o.methods[k3]);
5222
- }
5223
- }
5224
- }
5225
- // User tag for addressing via bw.message()
5226
- this._userTag = null;
5227
- // Lifecycle hooks
5228
- this._hooks = {
5229
- willMount: o.willMount || null,
5230
- mounted: o.mounted || null,
5231
- willUpdate: o.willUpdate || null,
5232
- onUpdate: o.onUpdate || null,
5233
- unmount: o.unmount || null,
5234
- willDestroy: o.willDestroy || null
5235
- };
5236
- // Binding tracking
5237
- this._bindings = [];
5238
- this._dirtyKeys = {};
5239
- this._scheduled = false;
5240
- this._subs = [];
5241
- this._eventListeners = [];
5242
- this._registeredActions = [];
5243
- this._prevValues = {};
5244
- this._compile = !!o.compile;
5245
- this._bw_refs = {};
5246
- this._refCounter = 0;
5247
- // Child component ownership (Bug #5)
5248
- this._children = [];
5249
- this._parent = null;
5250
- // Factory metadata for BCCL rebuild (Bug #6)
5251
- this._factory = taco._bwFactory || null;
5252
- }
5253
-
5254
- // Short alias for ComponentHandle.prototype (see alias block at top of file).
5255
- // 28 method definitions × 25 chars = ~700B raw savings in minified output.
5256
- var _chp = ComponentHandle.prototype;
5257
-
5258
- // ── State Methods ──
5259
-
5260
- /**
5261
- * Get a state value. Dot-path supported: `get('user.name')`
5262
- */
5263
- _chp.get = function(key) {
5264
- return bw._evaluatePath(this._state, key);
5265
- };
5266
-
5267
- /**
5268
- * Set a state value. Dot-path supported. Schedules re-render.
5269
- * @param {string} key - State key (dot-path)
5270
- * @param {*} value - New value
5271
- * @param {Object} [opts] - Options. `{sync: true}` for immediate flush.
5272
- */
5273
- _chp.set = function(key, value, opts) {
5274
- // Dot-path set
5275
- var parts = key.split('.');
5276
- var obj = this._state;
5277
- for (var i = 0; i < parts.length - 1; i++) {
5278
- if (!_is(obj[parts[i]], 'object')) {
5279
- if (bw.debug) _cw('bw.debug: set() — auto-creating intermediate "' + parts[i] + '" in path "' + key + '"');
5280
- obj[parts[i]] = {};
5281
- }
5282
- obj = obj[parts[i]];
5283
- }
5284
- obj[parts[parts.length - 1]] = value;
5285
- // Mark top-level key dirty
5286
- this._dirtyKeys[parts[0]] = true;
5287
- if (this.mounted) {
5288
- if (opts && opts.sync) {
5289
- this._flush();
5290
- } else {
5291
- this._scheduleDirty();
5292
- }
5293
- }
5294
- };
5295
-
5296
- /**
5297
- * Get a shallow clone of the full state.
5298
- */
5299
- _chp.getState = function() {
5300
- var clone = {};
5301
- for (var k in this._state) {
5302
- if (_hop.call(this._state, k)) {
5303
- clone[k] = this._state[k];
5304
- }
5305
- }
5306
- return clone;
5307
- };
5308
-
5309
- /**
5310
- * Merge multiple state keys. Schedules re-render.
5311
- * @param {Object} updates - Key-value pairs to merge
5312
- * @param {Object} [opts] - Options. `{sync: true}` for immediate flush.
5313
- */
5314
- _chp.setState = function(updates, opts) {
5315
- for (var k in updates) {
5316
- if (_hop.call(updates, k)) {
5317
- this._state[k] = updates[k];
5318
- this._dirtyKeys[k] = true;
5319
- }
5320
- }
5321
- if (this.mounted) {
5322
- if (opts && opts.sync) {
5323
- this._flush();
5324
- } else {
5325
- this._scheduleDirty();
5326
- }
5327
- }
5328
- };
5329
-
5330
- /**
5331
- * Push a value onto an array in state. Clones the array.
5332
- */
5333
- _chp.push = function(key, val) {
5334
- var arr = this.get(key);
5335
- var newArr = _isA(arr) ? arr.slice() : [];
5336
- newArr.push(val);
5337
- this.set(key, newArr);
5338
- };
5339
-
5340
- /**
5341
- * Splice an array in state. Clones the array.
5342
- */
5343
- _chp.splice = function(key, start, deleteCount) {
5344
- var arr = this.get(key);
5345
- var newArr = _isA(arr) ? arr.slice() : [];
5346
- var args = [start, deleteCount].concat(Array.prototype.slice.call(arguments, 3));
5347
- Array.prototype.splice.apply(newArr, args);
5348
- this.set(key, newArr);
5349
- };
5350
-
5351
- // ── Scheduling ──
5352
-
5353
- _chp._scheduleDirty = function() {
5354
- if (!this._scheduled) {
5355
- this._scheduled = true;
5356
- bw._dirtyComponents.push(this);
5357
- bw._scheduleFlush();
5358
- }
5359
- };
5360
-
5361
- // ── Binding Compilation ──
5362
-
5363
- /**
5364
- * Walk the TACO tree and extract ${expr} bindings.
5365
- * Creates binding descriptors with refIds for targeted DOM updates.
5366
- * @private
5367
- */
5368
- _chp._compileBindings = function() {
5369
- this._bindings = [];
5370
- this._refCounter = 0;
5371
- var stateKeys = _keys(this._state);
5372
- var self = this;
5373
-
5374
- function walkTaco(taco, path) {
5375
- if (!_is(taco, 'object') || !taco.t) return taco;
5376
-
5377
- // Check content for bindings
5378
- if (_is(taco.c, 'string') && taco.c.indexOf('${') >= 0) {
5379
- var refId = 'bw_ref_' + self._refCounter++;
5380
- var parsed = bw._parseBindings(taco.c);
5381
- var deps = [];
5382
- for (var j = 0; j < parsed.length; j++) {
5383
- deps = deps.concat(bw._extractDeps(parsed[j].expr, stateKeys));
5384
- }
5385
- self._bindings.push({
5386
- expr: taco.c,
5387
- type: 'content',
5388
- refId: refId,
5389
- deps: deps,
5390
- template: taco.c
5391
- });
5392
- // Inject data-bw_ref on the TACO for createDOM to pick up
5393
- if (!taco.a) taco.a = {};
5394
- taco.a['data-bw_ref'] = refId;
5395
- }
5396
-
5397
- // Check attributes for bindings
5398
- if (taco.a) {
5399
- for (var attrName in taco.a) {
5400
- if (!_hop.call(taco.a, attrName)) continue;
5401
- if (attrName === 'data-bw_ref') continue;
5402
- var attrVal = taco.a[attrName];
5403
- if (_is(attrVal, 'string') && attrVal.indexOf('${') >= 0) {
5404
- var refId2 = 'bw_ref_' + self._refCounter++;
5405
- var parsed2 = bw._parseBindings(attrVal);
5406
- var deps2 = [];
5407
- for (var j2 = 0; j2 < parsed2.length; j2++) {
5408
- deps2 = deps2.concat(bw._extractDeps(parsed2[j2].expr, stateKeys));
5409
- }
5410
- self._bindings.push({
5411
- expr: attrVal,
5412
- type: 'attribute',
5413
- attrName: attrName,
5414
- refId: refId2,
5415
- deps: deps2,
5416
- template: attrVal
5417
- });
5418
- if (!taco.a) taco.a = {};
5419
- taco.a['data-bw_ref'] = taco.a['data-bw_ref'] || refId2;
5420
- // If multiple attribute bindings on same element, store additional marker
5421
- if (taco.a['data-bw_ref'] !== refId2) {
5422
- taco.a['data-bw_ref_' + attrName] = refId2;
5423
- }
5424
- }
5425
- }
5426
- }
5427
-
5428
- // Recurse into children
5429
- if (_isA(taco.c)) {
5430
- for (var i = 0; i < taco.c.length; i++) {
5431
- // Wrap string children with ${expr} in a span so patches target the span, not the parent
5432
- if (_is(taco.c[i], 'string') && taco.c[i].indexOf('${') >= 0) {
5433
- var mixedRefId = 'bw_ref_' + self._refCounter++;
5434
- var mixedParsed = bw._parseBindings(taco.c[i]);
5435
- var mixedDeps = [];
5436
- for (var mi = 0; mi < mixedParsed.length; mi++) {
5437
- mixedDeps = mixedDeps.concat(bw._extractDeps(mixedParsed[mi].expr, stateKeys));
5438
- }
5439
- self._bindings.push({
5440
- expr: taco.c[i],
5441
- type: 'content',
5442
- refId: mixedRefId,
5443
- deps: mixedDeps,
5444
- template: taco.c[i]
5445
- });
5446
- // Replace string with a span wrapper so textContent targets the span only
5447
- taco.c[i] = { t: 'span', a: { 'data-bw_ref': mixedRefId, style: 'display:contents' }, c: taco.c[i] };
5448
- }
5449
- if (_is(taco.c[i], 'object') && taco.c[i].t) {
5450
- walkTaco(taco.c[i], path.concat(i));
5451
- }
5452
- // Handle bw.when/bw.each markers
5453
- if (taco.c[i] && taco.c[i]._bwWhen) {
5454
- var whenRefId = 'bw_ref_' + self._refCounter++;
5455
- var whenDeps = bw._extractDeps(taco.c[i].expr.replace(/^\$\{|\}$/g, ''), stateKeys);
5456
- self._bindings.push({
5457
- expr: taco.c[i].expr,
5458
- type: 'structural',
5459
- subtype: 'when',
5460
- refId: whenRefId,
5461
- deps: whenDeps,
5462
- branches: taco.c[i].branches,
5463
- index: i,
5464
- parentPath: path
5465
- });
5466
- taco.c[i]._refId = whenRefId;
5467
- }
5468
- if (taco.c[i] && taco.c[i]._bwEach) {
5469
- var eachRefId = 'bw_ref_' + self._refCounter++;
5470
- var eachDeps = bw._extractDeps(taco.c[i].expr.replace(/^\$\{|\}$/g, ''), stateKeys);
5471
- self._bindings.push({
5472
- expr: taco.c[i].expr,
5473
- type: 'structural',
5474
- subtype: 'each',
5475
- refId: eachRefId,
5476
- deps: eachDeps,
5477
- factory: taco.c[i].factory,
5478
- index: i,
5479
- parentPath: path
5480
- });
5481
- taco.c[i]._refId = eachRefId;
5482
- }
5483
- }
5484
- } else if (_is(taco.c, 'object') && taco.c.t) {
5485
- walkTaco(taco.c, path.concat(0));
5486
- }
5487
-
5488
- return taco;
5489
- }
5490
-
5491
- walkTaco(this.taco, []);
5492
- };
5493
-
5494
- // ── DOM Reference Collection ──
5495
-
5496
- /**
5497
- * Build ref map from the live DOM after createDOM.
5498
- * @private
5499
- */
5500
- _chp._collectRefs = function() {
5501
- this._bw_refs = {};
5502
- if (!this.element) return;
5503
- var els = this.element.querySelectorAll('[data-bw_ref]');
5504
- for (var i = 0; i < els.length; i++) {
5505
- this._bw_refs[els[i].getAttribute('data-bw_ref')] = els[i];
5506
- }
5507
- // Also check root element
5508
- var rootRef = this.element.getAttribute && this.element.getAttribute('data-bw_ref');
5509
- if (rootRef) {
5510
- this._bw_refs[rootRef] = this.element;
5511
- }
5512
- };
5513
-
5514
- // ── Lifecycle ──
5515
-
5516
- /**
5517
- * Mount the component into a parent DOM element.
5518
- * Creates DOM, compiles bindings, registers actions, and calls lifecycle hooks.
5519
- * @param {Element} parentEl - DOM element to mount into
5520
- */
5521
- _chp.mount = function(parentEl) {
5522
- // willMount hook
5523
- if (this._hooks.willMount) this._hooks.willMount(this);
5524
-
5525
- // Save original TACO for re-renders (structural changes clone from this)
5526
- if (!this._originalTaco) {
5527
- this._originalTaco = this.taco;
5528
- }
5529
-
5530
- // Deep-clone TACO so binding annotations don't mutate original.
5531
- // Custom clone to preserve _bwWhen/_bwEach markers and their factory functions.
5532
- this.taco = this._deepCloneTaco(this._originalTaco);
5533
-
5534
- // Compile bindings (annotates TACO with data-bw_ref attributes)
5535
- this._compileBindings();
5536
-
5537
- // Prepare TACO: resolve initial binding values, evaluate when/each
5538
- this._prepareTaco(this.taco);
5539
-
5540
- // Register named actions in function registry
5541
- var self = this;
5542
- for (var actionName in this._actions) {
5543
- if (_hop.call(this._actions, actionName)) {
5544
- var registeredName = this._bwId + '_' + actionName;
5545
- (function(aName) {
5546
- bw.funcRegister(function(evt) {
5547
- self._actions[aName](self, evt);
5548
- }, registeredName);
5549
- })(actionName);
5550
- this._registeredActions.push(registeredName);
5551
- }
5552
- }
5553
-
5554
- // Wire action names in onclick etc. to dispatch strings
5555
- this._wireActions(this.taco);
5556
-
5557
- // Create DOM (strip o before createDOM to prevent double lifecycle)
5558
- var tacoForDOM = this._tacoForDOM(this.taco);
5559
- this.element = bw.createDOM(tacoForDOM);
5560
- this.element._bwComponentHandle = this;
5561
- this.element.setAttribute('data-bw_comp_id', this._bwId);
5562
-
5563
- // Restore o.render from original TACO (stripped by _tacoForDOM)
5564
- if (this.taco.o && this.taco.o.render) {
5565
- this.element._bw_render = this.taco.o.render;
5566
- }
5567
- if (this._userTag) {
5568
- this.element.classList.add(this._userTag);
5569
- }
5570
-
5571
- // Append to parent
5572
- parentEl.appendChild(this.element);
5573
-
5574
- // Collect refs from live DOM
5575
- this._collectRefs();
5576
-
5577
- // Resolve initial bindings and apply to DOM
5578
- this._resolveAndApplyAll();
5579
-
5580
- this.mounted = true;
5581
-
5582
- // Scan for child ComponentHandles and link parent/child (Bug #5)
5583
- var childEls = this.element.querySelectorAll('[data-bw_comp_id]');
5584
- for (var ci = 0; ci < childEls.length; ci++) {
5585
- var ch = childEls[ci]._bwComponentHandle;
5586
- if (ch && ch !== this && !ch._parent) {
5587
- ch._parent = this;
5588
- this._children.push(ch);
5589
- }
5590
- }
5591
-
5592
- // mounted hook (backward compat: fn.length === 2 wraps (el, state))
5593
- if (this._hooks.mounted) {
5594
- if (this._hooks.mounted.length === 2) {
5595
- this._hooks.mounted(this.element, this.getState());
5596
- } else {
5597
- this._hooks.mounted(this);
5598
- }
5599
- }
5600
-
5601
- // Invoke o.render on initial mount (if present)
5602
- if (this.element._bw_render) {
5603
- this.element._bw_render(this.element, this._state);
5604
- }
5605
- };
5606
-
5607
- /**
5608
- * Prepare TACO for initial render: resolve when/each markers.
5609
- * @private
5610
- */
5611
- _chp._prepareTaco = function(taco) {
5612
- if (!_is(taco, 'object')) return;
5613
-
5614
- if (_isA(taco.c)) {
5615
- for (var i = taco.c.length - 1; i >= 0; i--) {
5616
- var child = taco.c[i];
5617
- if (child && child._bwWhen) {
5618
- var exprStr = child.expr.replace(/^\$\{|\}$/g, '');
5619
- var val;
5620
- if (this._compile) {
5621
- try {
5622
- val = (new Function('state', 'with(state){return (' + exprStr + ');}'))(this._state);
5623
- } catch(e) { val = false; }
5624
- } else {
5625
- val = bw._evaluatePath(this._state, exprStr);
5626
- }
5627
- var branch = val ? child.branches[0] : (child.branches[1] || null);
5628
- if (branch) {
5629
- // Wrap in a container so we can track it
5630
- taco.c[i] = { t: 'span', a: { 'data-bw_when': child._refId, style: 'display:contents' }, c: branch };
5631
- } else {
5632
- taco.c[i] = { t: 'span', a: { 'data-bw_when': child._refId, style: 'display:contents' }, c: '' };
5633
- }
5634
- }
5635
- if (child && child._bwEach) {
5636
- var eachExprStr = child.expr.replace(/^\$\{|\}$/g, '');
5637
- var arr = bw._evaluatePath(this._state, eachExprStr);
5638
- var items = [];
5639
- if (_isA(arr)) {
5640
- for (var j = 0; j < arr.length; j++) {
5641
- items.push(child.factory(arr[j], j));
5642
- }
5643
- }
5644
- taco.c[i] = { t: 'span', a: { 'data-bw_each': child._refId, style: 'display:contents' }, c: items };
5645
- }
5646
- if (_is(taco.c[i], 'object') && taco.c[i].t) {
5647
- this._prepareTaco(taco.c[i]);
5648
- }
5649
- }
5650
- } else if (_is(taco.c, 'object') && taco.c.t) {
5651
- this._prepareTaco(taco.c);
5652
- }
5653
- };
5654
-
5655
- /**
5656
- * Wire action name strings (in onclick etc.) to dispatch function calls.
5657
- * @private
5658
- */
5659
- _chp._wireActions = function(taco) {
5660
- if (!_is(taco, 'object') || !taco.t) return;
5661
- if (taco.a) {
5662
- for (var key in taco.a) {
5663
- if (!_hop.call(taco.a, key)) continue;
5664
- if (key.startsWith('on') && _is(taco.a[key], 'string')) {
5665
- var actionName = taco.a[key];
5666
- if (actionName in this._actions) {
5667
- var registeredName = this._bwId + '_' + actionName;
5668
- // Replace string with actual function for createDOM event binding
5669
- (function(rName) {
5670
- taco.a[key] = function(evt) {
5671
- bw.funcGetById(rName)(evt);
5672
- };
5673
- })(registeredName);
5674
- }
5675
- }
5676
- }
5677
- }
5678
- if (_isA(taco.c)) {
5679
- for (var i = 0; i < taco.c.length; i++) {
5680
- this._wireActions(taco.c[i]);
5681
- }
5682
- } else if (_is(taco.c, 'object') && taco.c.t) {
5683
- this._wireActions(taco.c);
5684
- }
5685
- };
5686
-
5687
- /**
5688
- * Deep-clone a TACO tree, preserving _bwWhen/_bwEach markers and their factories.
5689
- * @private
5690
- */
5691
- _chp._deepCloneTaco = function(taco) {
5692
- if (taco == null) return taco;
5693
- // Preserve _bwWhen / _bwEach markers (contain functions)
5694
- if (taco._bwWhen) {
5695
- return { _bwWhen: true, expr: taco.expr, branches: [
5696
- this._deepCloneTaco(taco.branches[0]),
5697
- taco.branches[1] ? this._deepCloneTaco(taco.branches[1]) : null
5698
- ], _refId: taco._refId };
5699
- }
5700
- if (taco._bwEach) {
5701
- return { _bwEach: true, expr: taco.expr, factory: taco.factory, _refId: taco._refId };
5702
- }
5703
- if (!_is(taco, 'object') || !taco.t) return taco;
5704
- var result = { t: taco.t };
5705
- if (taco.a) {
5706
- result.a = {};
5707
- for (var k in taco.a) {
5708
- if (_hop.call(taco.a, k)) result.a[k] = taco.a[k];
5709
- }
5710
- }
5711
- if (taco.c != null) {
5712
- if (_isA(taco.c)) {
5713
- result.c = taco.c.map(function(child) { return this._deepCloneTaco(child); }.bind(this));
5714
- } else if (_is(taco.c, 'object')) {
5715
- result.c = this._deepCloneTaco(taco.c);
5716
- } else {
5717
- result.c = taco.c;
5718
- }
5719
- }
5720
- if (taco.o) result.o = taco.o; // Keep o reference (not deep-cloned; hooks are functions)
5721
- return result;
5722
- };
5723
-
5724
- /**
5725
- * Create a copy of TACO suitable for createDOM (strips o to prevent double lifecycle).
5726
- * @private
5727
- */
5728
- _chp._tacoForDOM = function(taco) {
5729
- if (!_is(taco, 'object') || !taco.t) return taco;
5730
- var result = { t: taco.t };
5731
- if (taco.a) result.a = taco.a;
5732
- if (taco.c != null) {
5733
- if (_isA(taco.c)) {
5734
- result.c = taco.c.map(function(child) { return this._tacoForDOM(child); }.bind(this));
5735
- } else if (_is(taco.c, 'object') && taco.c.t) {
5736
- result.c = this._tacoForDOM(taco.c);
5737
- } else {
5738
- result.c = taco.c;
5739
- }
5740
- }
5741
- // Intentionally strip o (no mounted/unmount/state/render on sub-elements)
5742
- if (taco.o && (taco.o.mounted || taco.o.render || taco.o.unmount)) {
5743
- _cw('bw: _tacoForDOM stripped o.mounted/render/unmount from child <' + taco.t +
5744
- '>. Use onclick attribute or bw.component() for child interactivity.');
5745
- }
5746
- return result;
5747
- };
5748
-
5749
- /**
5750
- * Unmount: remove from DOM, deactivate, preserve state for re-mount.
5751
- */
5752
- _chp.unmount = function() {
5753
- if (!this.mounted) return;
5754
-
5755
- // unmount hook
5756
- if (this._hooks.unmount) {
5757
- this._hooks.unmount(this);
5758
- }
5759
-
5760
- // Remove DOM event listeners
5761
- for (var i = 0; i < this._eventListeners.length; i++) {
5762
- var l = this._eventListeners[i];
5763
- if (this.element) {
5764
- this.element.removeEventListener(l.event, l.handler);
5765
- }
5766
- }
5767
- this._eventListeners = [];
5768
-
5769
- // Unsubscribe pub/sub
5770
- for (var j = 0; j < this._subs.length; j++) {
5771
- this._subs[j]();
5772
- }
5773
- this._subs = [];
5774
-
5775
- // Remove from DOM
5776
- if (this.element && this.element.parentNode) {
5777
- this.element.parentNode.removeChild(this.element);
5778
- }
5779
-
5780
- this.mounted = false;
5781
- // State preserved — can re-mount
5782
- };
5783
-
5784
- /**
5785
- * Destroy: unmount + clear state + unregister actions.
5786
- */
5787
- _chp.destroy = function() {
5788
- // willDestroy hook
5789
- if (this._hooks.willDestroy) {
5790
- this._hooks.willDestroy(this);
5791
- }
5792
-
5793
- // Cascade destroy to children depth-first (Bug #5)
5794
- for (var ci = this._children.length - 1; ci >= 0; ci--) {
5795
- this._children[ci].destroy();
5796
- }
5797
- this._children = [];
5798
- if (this._parent) {
5799
- var idx = this._parent._children.indexOf(this);
5800
- if (idx >= 0) this._parent._children.splice(idx, 1);
5801
- this._parent = null;
5802
- }
5803
-
5804
- this.unmount();
5805
-
5806
- // Unregister actions from function registry
5807
- for (var i = 0; i < this._registeredActions.length; i++) {
5808
- bw.funcUnregister(this._registeredActions[i]);
5809
- }
5810
- this._registeredActions = [];
5811
-
5812
- // Clear state
5813
- this._state = {};
5814
- this._bindings = [];
5815
- this._bw_refs = {};
5816
- this._prevValues = {};
5817
- this._dirtyKeys = {};
5818
- if (this.element) {
5819
- delete this.element._bwComponentHandle;
5820
- this.element = null;
5821
- }
5822
- };
5823
-
5824
- // ── Flush & Binding Resolution ──
5825
-
5826
- /**
5827
- * Flush dirty state: resolve changed bindings and apply to DOM.
5828
- * @private
5829
- */
5830
- _chp._flush = function() {
5831
- this._scheduled = false;
5832
- var changedKeys = _keys(this._dirtyKeys);
5833
- this._dirtyKeys = {};
5834
- if (changedKeys.length === 0 || !this.mounted) return;
5835
-
5836
- // Factory rebuild: if a BCCL factory exists and changed keys overlap factory props,
5837
- // rebuild the TACO from the factory with merged state (Bug #6)
5838
- if (this._factory) {
5839
- var rebuildNeeded = false;
5840
- for (var fi = 0; fi < changedKeys.length; fi++) {
5841
- if (_hop.call(this._factory.props, changedKeys[fi])) {
5842
- rebuildNeeded = true; break;
5843
- }
5844
- }
5845
- if (rebuildNeeded) {
5846
- var merged = {};
5847
- for (var mk in this._factory.props) if (_hop.call(this._factory.props, mk)) merged[mk] = this._factory.props[mk];
5848
- for (var sk in this._state) if (_hop.call(this._state, sk)) merged[sk] = this._state[sk];
5849
- this._factory.props = merged;
5850
- var newTaco = bw.make(this._factory.type, merged);
5851
- newTaco._bwFactory = this._factory;
5852
- this.taco = newTaco;
5853
- this._originalTaco = this._deepCloneTaco(newTaco);
5854
- this._render();
5855
- if (this._hooks.onUpdate) this._hooks.onUpdate(this, changedKeys);
5856
- return;
5857
- }
5858
- }
5859
-
5860
- // willUpdate hook
5861
- if (this._hooks.willUpdate) {
5862
- this._hooks.willUpdate(this, changedKeys);
5863
- }
5864
-
5865
- // Check if any structural bindings are affected
5866
- var needsFullRender = false;
5867
- for (var i = 0; i < this._bindings.length; i++) {
5868
- var b = this._bindings[i];
5869
- if (b.type === 'structural') {
5870
- for (var j = 0; j < b.deps.length; j++) {
5871
- if (changedKeys.indexOf(b.deps[j]) >= 0) {
5872
- needsFullRender = true;
5873
- break;
5874
- }
5875
- }
5876
- if (needsFullRender) break;
5877
- }
5878
- }
5879
-
5880
- if (needsFullRender) {
5881
- this._render();
5882
- } else {
5883
- var patches = this._resolveBindings(changedKeys);
5884
- this._applyPatches(patches);
5885
- }
5886
-
5887
- // onUpdate hook
5888
- if (this._hooks.onUpdate) {
5889
- this._hooks.onUpdate(this, changedKeys);
5890
- }
5891
- };
5892
-
5893
- /**
5894
- * Resolve bindings whose deps intersect with changedKeys.
5895
- * Returns list of patches to apply.
5896
- * @private
5897
- */
5898
- _chp._resolveBindings = function(changedKeys) {
5899
- var patches = [];
5900
- for (var i = 0; i < this._bindings.length; i++) {
5901
- var b = this._bindings[i];
5902
- if (b.type === 'structural') continue;
5903
-
5904
- // Check if any dep matches
5905
- var affected = false;
5906
- for (var j = 0; j < b.deps.length; j++) {
5907
- if (changedKeys.indexOf(b.deps[j]) >= 0) {
5908
- affected = true;
5909
- break;
5910
- }
5911
- }
5912
- if (!affected) continue;
5913
-
5914
- // Evaluate
5915
- var newVal = bw._resolveTemplate(b.template, this._state, this._compile);
5916
- var prevKey = b.refId + '_' + (b.attrName || 'content');
5917
- if (this._prevValues[prevKey] !== newVal) {
5918
- this._prevValues[prevKey] = newVal;
5919
- patches.push({
5920
- refId: b.refId,
5921
- type: b.type,
5922
- attrName: b.attrName,
5923
- value: newVal
5924
- });
5925
- }
5926
- }
5927
- return patches;
5928
- };
5929
-
5930
- /**
5931
- * Apply patches to DOM.
5932
- * @private
5933
- */
5934
- _chp._applyPatches = function(patches) {
5935
- for (var i = 0; i < patches.length; i++) {
5936
- var p = patches[i];
5937
- var el = this._bw_refs[p.refId];
5938
- if (!el) {
5939
- if (bw.debug) _cw('bw.debug: _applyPatches — ref "' + p.refId + '" not found in DOM');
5940
- continue;
5941
- }
5942
- if (p.type === 'content') {
5943
- el.textContent = p.value;
5944
- } else if (p.type === 'attribute') {
5945
- if (p.attrName === 'class') {
5946
- el.className = p.value;
5947
- } else {
5948
- el.setAttribute(p.attrName, p.value);
5949
- }
5950
- }
5951
- }
5952
- };
5953
-
5954
- /**
5955
- * Resolve all bindings and apply (used for initial render).
5956
- * @private
5957
- */
5958
- _chp._resolveAndApplyAll = function() {
5959
- var patches = [];
5960
- for (var i = 0; i < this._bindings.length; i++) {
5961
- var b = this._bindings[i];
5962
- if (b.type === 'structural') continue;
5963
-
5964
- var newVal = bw._resolveTemplate(b.template, this._state, this._compile);
5965
- var prevKey = b.refId + '_' + (b.attrName || 'content');
5966
- this._prevValues[prevKey] = newVal;
5967
- patches.push({
5968
- refId: b.refId,
5969
- type: b.type,
5970
- attrName: b.attrName,
5971
- value: newVal
5972
- });
5973
- }
5974
- this._applyPatches(patches);
5975
- };
5976
-
5977
- /**
5978
- * Full re-render for structural changes (when/each branch switches).
5979
- * @private
5980
- */
5981
- _chp._render = function() {
5982
- if (!this.element || !this.element.parentNode) return;
5983
- var parent = this.element.parentNode;
5984
- var nextSibling = this.element.nextSibling;
5985
-
5986
- // Remove old DOM
5987
- parent.removeChild(this.element);
5988
-
5989
- // Re-prepare TACO with current state (deep clone preserving functions)
5990
- this.taco = this._deepCloneTaco(this._originalTaco || this.taco);
5991
-
5992
- // Re-compile bindings and prepare
5993
- this._compileBindings();
5994
- this._prepareTaco(this.taco);
5995
- this._wireActions(this.taco);
5996
-
5997
- var tacoForDOM = this._tacoForDOM(this.taco);
5998
- this.element = bw.createDOM(tacoForDOM);
5999
- this.element._bwComponentHandle = this;
6000
- this.element.setAttribute('data-bw_comp_id', this._bwId);
6001
-
6002
- // Re-insert at same position
6003
- if (nextSibling) {
6004
- parent.insertBefore(this.element, nextSibling);
6005
- } else {
6006
- parent.appendChild(this.element);
6007
- }
6008
-
6009
- // Re-collect refs and apply all bindings
6010
- this._collectRefs();
6011
- this._resolveAndApplyAll();
6012
- };
6013
-
6014
- // ── Event & Pub/Sub Methods ──
6015
-
6016
- /**
6017
- * Add a DOM event listener on the component's root element.
6018
- * @param {string} event - Event name (e.g., 'click')
6019
- * @param {Function} handler - Event handler
6020
- */
6021
- _chp.on = function(event, handler) {
6022
- if (this.element) {
6023
- this.element.addEventListener(event, handler);
6024
- }
6025
- this._eventListeners.push({ event: event, handler: handler });
6026
- };
6027
-
6028
- /**
6029
- * Remove a DOM event listener.
6030
- * @param {string} event - Event name
6031
- * @param {Function} handler - Handler to remove
6032
- */
6033
- _chp.off = function(event, handler) {
6034
- if (this.element) {
6035
- this.element.removeEventListener(event, handler);
6036
- }
6037
- this._eventListeners = this._eventListeners.filter(function(l) {
6038
- return !(l.event === event && l.handler === handler);
6039
- });
6040
- };
6041
-
6042
- /**
6043
- * Subscribe to a pub/sub topic. Lifecycle-tied: auto-unsubs on destroy.
6044
- * @param {string} topic - Topic name
6045
- * @param {Function} handler - Handler function
6046
- * @returns {Function} Unsubscribe function
6047
- */
6048
- _chp.sub = function(topic, handler) {
6049
- var unsub = bw.sub(topic, handler);
6050
- this._subs.push(unsub);
6051
- return unsub;
6052
- };
6053
-
6054
- /**
6055
- * Call a named action.
6056
- * @param {string} name - Action name
6057
- * @param {...*} args - Arguments passed after comp
6058
- */
6059
- _chp.action = function(name) {
6060
- var fn = this._actions[name];
6061
- if (!fn) {
6062
- _cw('ComponentHandle.action: unknown action "' + name + '"');
6063
- return;
6064
- }
6065
- var args = [this].concat(Array.prototype.slice.call(arguments, 1));
6066
- return fn.apply(null, args);
6067
- };
6068
-
6069
- /**
6070
- * querySelector within the component's DOM.
6071
- * @param {string} sel - CSS selector
6072
- * @returns {Element|null}
6073
- */
6074
- _chp.select = function(sel) {
6075
- return this.element ? this.element.querySelector(sel) : null;
6076
- };
6077
-
6078
- /**
6079
- * querySelectorAll within the component's DOM.
6080
- * @param {string} sel - CSS selector
6081
- * @returns {Element[]}
6082
- */
6083
- _chp.selectAll = function(sel) {
6084
- if (!this.element) return [];
6085
- return Array.prototype.slice.call(this.element.querySelectorAll(sel));
6086
- };
6087
-
6088
- /**
6089
- * Tag this component with a user-defined ID for addressing via bw.message().
6090
- * The tag is added as a CSS class on the root element (DOM IS the registry).
6091
- * @param {string} tag - User-defined identifier (e.g. 'dashboard_prod_east')
6092
- * @returns {ComponentHandle} this (for chaining)
6093
- */
6094
- _chp.userTag = function(tag) {
6095
- this._userTag = tag;
6096
- if (this.element) {
6097
- this.element.classList.add(tag);
5227
+ } catch (e) {
5228
+ if (bw.debug) _cw('bw.debug: _resolveTemplate — Tier 2 eval failed for "${' + b.expr + '}":', e.message);
5229
+ val = '';
5230
+ }
5231
+ } else {
5232
+ // Tier 1: dot-path only
5233
+ val = bw._evaluatePath(state, b.expr);
5234
+ }
5235
+ result += (val == null) ? '' : String(val);
5236
+ lastEnd = b.end;
6098
5237
  }
6099
- return this;
5238
+ result += str.slice(lastEnd);
5239
+ return result;
6100
5240
  };
6101
5241
 
6102
- // Expose ComponentHandle on bw (for testing and advanced use)
6103
- bw._ComponentHandle = ComponentHandle;
6104
-
6105
5242
  // ===================================================================================
6106
- // Control Flow Helpers
5243
+ // Deprecation stubs for removed ComponentHandle APIs (v2.0.19)
6107
5244
  // ===================================================================================
6108
5245
 
6109
- /**
6110
- * Conditional rendering helper.
6111
- * Returns a marker object that ComponentHandle detects during binding compilation.
6112
- * In static contexts (bw.html with state), evaluates immediately.
6113
- *
6114
- * @param {string} expr - Expression string like '${loggedIn}'
6115
- * @param {Object} tacoTrue - TACO to render when truthy
6116
- * @param {Object} [tacoFalse] - TACO to render when falsy
6117
- * @returns {Object} Marker object with _bwWhen flag
6118
- * @category Component
6119
- */
6120
- bw.when = function(expr, tacoTrue, tacoFalse) {
6121
- return { _bwWhen: true, expr: expr, branches: [tacoTrue, tacoFalse || null] };
6122
- };
5246
+ bw._extractDeps = undefined;
5247
+ bw._dirtyComponents = undefined;
5248
+ bw._flushScheduled = undefined;
5249
+ bw._scheduleFlush = undefined;
5250
+ bw._doFlush = undefined;
5251
+ bw._ComponentHandle = undefined;
6123
5252
 
6124
5253
  /**
6125
- * List rendering helper.
6126
- * Returns a marker object that ComponentHandle detects during binding compilation.
6127
- *
6128
- * @param {string} expr - Expression string like '${items}'
6129
- * @param {Function} fn - Factory function(item, index) returning TACO
6130
- * @returns {Object} Marker object with _bwEach flag
5254
+ * No-op flush (ComponentHandle removed in v2.0.19).
5255
+ * Kept as no-op for backward compatibility.
6131
5256
  * @category Component
6132
5257
  */
6133
- bw.each = function(expr, fn) {
6134
- return { _bwEach: true, expr: expr, factory: fn };
6135
- };
5258
+ bw.flush = function() {};
6136
5259
 
6137
- // ===================================================================================
6138
- // bw.component() — Factory for ComponentHandle
6139
- // ===================================================================================
6140
5260
 
6141
- /**
6142
- * Create a ComponentHandle from a TACO definition.
6143
- * The returned handle has .get(), .set(), .mount(), .destroy(), etc.
6144
- *
6145
- * @param {Object} taco - TACO definition with {t, a, c, o}
6146
- * @returns {ComponentHandle} Reactive component handle
6147
- * @category Component
6148
- * @see bw.DOM
6149
- * @example
6150
- * var counter = bw.component({
6151
- * t: 'div', c: [{ t: 'h3', c: 'Count: ${count}' }],
6152
- * o: { state: { count: 0 } }
6153
- * });
6154
- * bw.DOM('#app', counter);
6155
- * counter.set('count', 42); // DOM auto-updates
6156
- */
6157
- bw.component = function(taco) {
6158
- return new ComponentHandle(taco);
6159
- };
5261
+ bw.when = function() { throw new Error('bw.when() removed in v2.0.19. Use conditional logic in o.render instead.'); };
5262
+ bw.each = function() { throw new Error('bw.each() removed in v2.0.19. Use array mapping in o.render instead.'); };
5263
+ bw.component = function() { throw new Error('bw.component() removed in v2.0.19. Use o.handle/o.slots on TACO options instead.'); };
5264
+
6160
5265
 
6161
5266
  // ===================================================================================
6162
5267
  // bw.message() — SendMessage() for the web
6163
5268
  // ===================================================================================
6164
5269
 
6165
5270
  /**
6166
- * Dispatch a message to a component by UUID or user tag.
6167
- * Finds the component's DOM element, looks up its ComponentHandle,
6168
- * and calls the named method. This is the bitwrench equivalent of
6169
- * Win32 SendMessage(hwnd, msg, wParam, lParam).
5271
+ * Dispatch a message to a component by UUID, CSS class, or selector.
5272
+ * Finds the element, looks up el.bw, and calls the named method.
5273
+ * This is the bitwrench equivalent of Win32 SendMessage(hwnd, msg, wParam, lParam).
6170
5274
  *
6171
- * @param {string} target - Component UUID (data-bw_comp_id) or user tag (CSS class)
6172
- * @param {string} action - Method name to call on the component
5275
+ * @param {string} target - Component UUID (bw_uuid_*), CSS class, or selector
5276
+ * @param {string} action - Method name to call on el.bw
6173
5277
  * @param {*} data - Data to pass to the method
6174
5278
  * @returns {boolean} True if message was dispatched successfully
6175
5279
  * @category Component
6176
5280
  * @example
6177
- * // Tag a component
6178
- * myDash.userTag('dashboard_prod');
6179
- * // Dispatch locally
6180
- * bw.message('dashboard_prod', 'addAlert', { severity: 'warning', text: 'CPU spike' });
5281
+ * bw.message('my_carousel', 'goToSlide', 2);
6181
5282
  * // Or from SSE handler:
6182
5283
  * es.onmessage = function(e) {
6183
5284
  * var msg = JSON.parse(e.data);
@@ -6185,75 +5286,35 @@
6185
5286
  * };
6186
5287
  */
6187
5288
  bw.message = function(target, action, data) {
6188
- // Try data-bw_comp_id attribute first, then CSS class (user tag)
6189
- var el = bw.$('[data-bw_comp_id="' + target + '"]')[0];
6190
- if (!el) {
6191
- el = bw.$('.' + target)[0];
6192
- }
6193
- if (!el || !el._bwComponentHandle) return false;
6194
- var comp = el._bwComponentHandle;
6195
- if (!_is(comp[action], 'function')) {
6196
- _cw('bw.message: unknown action "' + action + '" on component ' + target);
5289
+ var el = bw._el(target);
5290
+ if (!el) el = bw.$('.' + target)[0];
5291
+ if (!el || !el.bw || typeof el.bw[action] !== 'function') {
5292
+ _cw('bw.message: no handle method "' + action + '" on ' + target);
6197
5293
  return false;
6198
5294
  }
6199
- comp[action](data);
5295
+ el.bw[action](data);
6200
5296
  return true;
6201
5297
  };
6202
5298
 
6203
5299
  // ===================================================================================
6204
- // bw.clientApply() / bw.clientConnect() — Server-driven UI protocol
5300
+ // bw.apply() / bw.parseJSONFlex() — Server-driven UI protocol
6205
5301
  // ===================================================================================
6206
5302
 
6207
5303
  /**
6208
5304
  * Registry of named functions sent via register messages.
6209
- * Populated by clientApply({ type: 'register', name, body }).
6210
- * Invoked by clientApply({ type: 'call', name, args }).
5305
+ * Populated by bw.apply({ type: 'register', name, body }).
5306
+ * Invoked by bw.apply({ type: 'call', name, args }).
6211
5307
  * @private
6212
5308
  */
6213
5309
  bw._clientFunctions = {};
6214
5310
 
6215
5311
  /**
6216
- * Whether exec messages are allowed. Set by clientConnect opts.allowExec.
5312
+ * Whether exec messages are allowed. Set by bwclient connect opts.allowExec.
6217
5313
  * Default false — exec messages are rejected unless explicitly opted in.
6218
5314
  * @private
6219
5315
  */
6220
5316
  bw._allowExec = false;
6221
5317
 
6222
- /**
6223
- * Built-in client functions available via call() without registration.
6224
- * @private
6225
- */
6226
- bw._builtinClientFunctions = {
6227
- scrollTo: function(selector) {
6228
- var el = bw._el(selector);
6229
- if (el) el.scrollTop = el.scrollHeight;
6230
- },
6231
- focus: function(selector) {
6232
- var el = bw._el(selector);
6233
- if (el && _is(el.focus, 'function')) el.focus();
6234
- },
6235
- download: function(filename, content, mimeType) {
6236
- if (typeof document === 'undefined') return;
6237
- var blob = new Blob([content], { type: mimeType || 'text/plain' });
6238
- var a = document.createElement('a');
6239
- a.href = URL.createObjectURL(blob);
6240
- a.download = filename;
6241
- a.click();
6242
- URL.revokeObjectURL(a.href);
6243
- },
6244
- clipboard: function(text) {
6245
- if (typeof navigator !== 'undefined' && navigator.clipboard) {
6246
- navigator.clipboard.writeText(text);
6247
- }
6248
- },
6249
- redirect: function(url) {
6250
- if (typeof window !== 'undefined') window.location.href = url;
6251
- },
6252
- log: function() {
6253
- console.log.apply(console, arguments);
6254
- }
6255
- };
6256
-
6257
5318
  /**
6258
5319
  * Parse a bwserve protocol message string, supporting both strict JSON
6259
5320
  * and r-prefixed relaxed JSON (single-quoted strings, trailing commas).
@@ -6268,9 +5329,9 @@
6268
5329
  * @param {string} str - JSON or r-prefixed relaxed JSON string
6269
5330
  * @returns {Object} Parsed message object
6270
5331
  * @throws {SyntaxError} If the string is not valid JSON or relaxed JSON
6271
- * @category Server
5332
+ * @category Core
6272
5333
  */
6273
- bw.clientParse = function(str) {
5334
+ bw.parseJSONFlex = function(str) {
6274
5335
  str = (str || '').trim();
6275
5336
  if (str.charAt(0) !== 'r') return JSON.parse(str);
6276
5337
  str = str.slice(1);
@@ -6355,10 +5416,10 @@
6355
5416
  * append — target.appendChild(bw.createDOM(node))
6356
5417
  * remove — bw.cleanup(target); target.remove()
6357
5418
  * patch — bw.patch(target, content, attr)
6358
- * batch — iterate ops, call clientApply for each
5419
+ * batch — iterate ops, call bw.apply for each
6359
5420
  * message — bw.message(target, action, data)
6360
5421
  * register — store a named function for later call()
6361
- * call — invoke a registered or built-in function
5422
+ * call — invoke a registered function
6362
5423
  * exec — execute arbitrary JS (requires allowExec)
6363
5424
  *
6364
5425
  * Target resolution:
@@ -6367,9 +5428,9 @@
6367
5428
  *
6368
5429
  * @param {Object} msg - Protocol message
6369
5430
  * @returns {boolean} true if the message was applied successfully
6370
- * @category Server
5431
+ * @category Core
6371
5432
  */
6372
- bw.clientApply = function(msg) {
5433
+ bw.apply = function(msg) {
6373
5434
  if (!msg || !msg.type) return false;
6374
5435
 
6375
5436
  var type = msg.type;
@@ -6403,7 +5464,7 @@
6403
5464
  if (!_isA(msg.ops)) return false;
6404
5465
  var allOk = true;
6405
5466
  msg.ops.forEach(function(op) {
6406
- if (!bw.clientApply(op)) allOk = false;
5467
+ if (!bw.apply(op)) allOk = false;
6407
5468
  });
6408
5469
  return allOk;
6409
5470
 
@@ -6422,7 +5483,7 @@
6422
5483
 
6423
5484
  } else if (type === 'call') {
6424
5485
  if (!msg.name) return false;
6425
- var fn = bw._clientFunctions[msg.name] || bw._builtinClientFunctions[msg.name];
5486
+ var fn = bw._clientFunctions[msg.name];
6426
5487
  if (!_is(fn, 'function')) return false;
6427
5488
  try {
6428
5489
  var args = _isA(msg.args) ? msg.args : [];
@@ -6451,271 +5512,35 @@
6451
5512
  return false;
6452
5513
  };
6453
5514
 
6454
- /**
6455
- * Connect to a bwserve SSE endpoint and apply protocol messages automatically.
6456
- *
6457
- * Returns a connection object with sendAction(), on(), and close() methods.
6458
- *
6459
- * @param {string} url - SSE endpoint URL (e.g., '/__bw/events/client-1')
6460
- * @param {Object} [opts] - Connection options
6461
- * @param {string} [opts.transport='sse'] - Transport type: 'sse' (default) or 'poll'
6462
- * @param {number} [opts.interval=2000] - Poll interval in ms (only for 'poll' transport)
6463
- * @param {string} [opts.actionUrl] - POST endpoint for actions (default: derived from url)
6464
- * @param {boolean} [opts.reconnect=true] - Auto-reconnect on disconnect
6465
- * @param {boolean} [opts.allowExec=false] - Enable exec message type (arbitrary JS execution)
6466
- * @param {Function} [opts.onStatus] - Status callback: 'connecting'|'connected'|'disconnected'
6467
- * @param {Function} [opts.onMessage] - Raw message callback (before clientApply)
6468
- * @returns {Object} Connection object { sendAction, on, close, status }
6469
- * @category Server
6470
- */
6471
- bw.clientConnect = function(url, opts) {
6472
- opts = opts || {};
6473
- var transport = opts.transport || 'sse';
6474
- var actionUrl = opts.actionUrl || url.replace(/\/events\//, '/action/');
6475
- var reconnect = opts.reconnect !== false;
6476
- var onStatus = opts.onStatus || function() {};
6477
- var onMessage = opts.onMessage || null;
6478
- var handlers = {};
6479
- // Set the global allowExec flag from connection options
6480
- bw._allowExec = !!opts.allowExec;
6481
- var conn = {
6482
- status: 'connecting',
6483
- _es: null,
6484
- _pollTimer: null
6485
- };
6486
-
6487
- function setStatus(s) {
6488
- conn.status = s;
6489
- onStatus(s);
6490
- }
6491
-
6492
- function handleMessage(data) {
6493
- try {
6494
- var msg = _is(data, 'string') ? bw.clientParse(data) : data;
6495
- if (onMessage) onMessage(msg);
6496
- if (handlers.message) handlers.message(msg);
6497
- bw.clientApply(msg);
6498
- } catch (e) {
6499
- if (handlers.error) handlers.error(e);
6500
- }
6501
- }
6502
-
6503
- if (transport === 'sse' && typeof EventSource !== 'undefined') {
6504
- setStatus('connecting');
6505
- var es = new EventSource(url);
6506
- conn._es = es;
6507
-
6508
- es.onopen = function() {
6509
- setStatus('connected');
6510
- if (handlers.open) handlers.open();
6511
- };
6512
-
6513
- es.onmessage = function(e) {
6514
- handleMessage(e.data);
6515
- };
6516
-
6517
- es.onerror = function() {
6518
- if (conn.status === 'connected') {
6519
- setStatus('disconnected');
6520
- }
6521
- if (handlers.error) handlers.error(new Error('SSE connection error'));
6522
- if (!reconnect) {
6523
- es.close();
6524
- }
6525
- // EventSource auto-reconnects by default when reconnect=true
6526
- };
6527
- } else if (transport === 'poll') {
6528
- var interval = opts.interval || 2000;
6529
- setStatus('connected');
6530
- conn._pollTimer = setInterval(function() {
6531
- fetch(url).then(function(r) { return r.json(); }).then(function(msgs) {
6532
- if (_isA(msgs)) {
6533
- msgs.forEach(handleMessage);
6534
- } else if (msgs && msgs.type) {
6535
- handleMessage(msgs);
6536
- }
6537
- }).catch(function(e) {
6538
- if (handlers.error) handlers.error(e);
6539
- });
6540
- }, interval);
6541
- }
6542
-
6543
- /**
6544
- * Send an action to the server via POST.
6545
- * @param {string} action - Action name
6546
- * @param {Object} [data] - Action payload
6547
- */
6548
- conn.sendAction = function(action, data) {
6549
- var body = JSON.stringify({ type: 'action', action: action, data: data || {} });
6550
- fetch(actionUrl, {
6551
- method: 'POST',
6552
- headers: { 'Content-Type': 'application/json' },
6553
- body: body
6554
- }).catch(function(e) {
6555
- if (handlers.error) handlers.error(e);
6556
- });
6557
- };
6558
-
6559
- /**
6560
- * Register an event handler.
6561
- * @param {string} event - 'open'|'message'|'error'|'close'
6562
- * @param {Function} handler
6563
- */
6564
- conn.on = function(event, handler) {
6565
- handlers[event] = handler;
6566
- return conn;
6567
- };
6568
-
6569
- /**
6570
- * Close the connection.
6571
- */
6572
- conn.close = function() {
6573
- if (conn._es) {
6574
- conn._es.close();
6575
- conn._es = null;
6576
- }
6577
- if (conn._pollTimer) {
6578
- clearInterval(conn._pollTimer);
6579
- conn._pollTimer = null;
6580
- }
6581
- setStatus('disconnected');
6582
- if (handlers.close) handlers.close();
6583
- };
6584
-
6585
- return conn;
6586
- };
6587
5515
 
6588
5516
  // ===================================================================================
6589
5517
  // bw.inspect() — Debug utility
6590
5518
  // ===================================================================================
6591
5519
 
6592
5520
  /**
6593
- * Inspect a component's state, bindings, methods, and metadata.
6594
- * Works with DOM elements, CSS selectors, or ComponentHandle objects.
6595
- * Returns the ComponentHandle for console chaining.
5521
+ * Inspect a DOM element's bitwrench state, handle methods, and metadata.
5522
+ * Works with DOM elements or CSS selectors.
6596
5523
  *
6597
- * @param {string|Element|ComponentHandle} target - Selector, element, or handle
6598
- * @returns {ComponentHandle|null} The component handle, or null if not found
5524
+ * @param {string|Element} target - Selector or DOM element
5525
+ * @returns {Element|null} The element, or null if not found
6599
5526
  * @category Component
6600
5527
  * @example
6601
- * // In browser console, click element in Elements panel then:
5528
+ * bw.inspect('#my-carousel');
6602
5529
  * bw.inspect($0);
6603
- * // Or by selector:
6604
- * var h = bw.inspect('#my-dashboard');
6605
- * h.set('count', 99); // chain from returned handle
6606
5530
  */
6607
5531
  bw.inspect = function(target) {
6608
- var el = target;
6609
- var comp;
6610
- if (target && target._bwComponent === true) {
6611
- el = target.element;
6612
- comp = target;
6613
- } else {
6614
- if (_is(target, 'string')) {
6615
- el = bw.$(target)[0];
6616
- }
6617
- if (!el) {
6618
- _cw('bw.inspect: element not found');
6619
- return null;
6620
- }
6621
- comp = el._bwComponentHandle;
6622
- }
6623
- if (!comp) {
6624
- _cl('bw.inspect: no ComponentHandle on this element');
6625
- _cl(' Tag:', el.tagName);
6626
- _cl(' Classes:', el.className);
6627
- _cl(' _bw_state:', el._bw_state || '(none)');
6628
- return null;
6629
- }
6630
- var deps = comp._bindings.reduce(function(s, b) {
6631
- return s.concat(b.deps || []);
6632
- }, []).filter(function(v, i, a) { return a.indexOf(v) === i; });
6633
- console.group('Component: ' + comp._bwId);
6634
- _cl('State:', comp._state);
6635
- _cl('Bindings:', comp._bindings.length, '(deps:', deps, ')');
6636
- _cl('Methods:', _keys(comp._methods));
6637
- _cl('Actions:', _keys(comp._actions));
6638
- _cl('User tag:', comp._userTag || '(none)');
6639
- _cl('Mounted:', comp.mounted);
6640
- _cl('Element:', comp.element);
5532
+ var el = _is(target, 'string') ? bw.$(target)[0] : target;
5533
+ if (!el) { _cw('bw.inspect: element not found'); return null; }
5534
+ console.group('Element: ' + (bw.getUUID(el) || el.id || el.tagName));
5535
+ _cl('State:', el._bw_state || '(none)');
5536
+ _cl('Handle:', el.bw ? _keys(el.bw) : '(none)');
5537
+ _cl('Classes:', el.className);
5538
+ _cl('Refs:', el._bw_refs || '(none)');
6641
5539
  console.groupEnd();
6642
- return comp;
5540
+ return el;
6643
5541
  };
6644
5542
 
6645
- // ===================================================================================
6646
- // bw.compile() — Pre-compile TACO into optimized factory
6647
- // ===================================================================================
6648
-
6649
- /**
6650
- * Pre-compile a TACO definition into a factory function.
6651
- * The factory produces ComponentHandles with pre-compiled binding evaluators.
6652
- *
6653
- * Phase 1: validates API surface. Template cloning optimization deferred.
6654
- *
6655
- * @param {Object} taco - TACO definition
6656
- * @returns {Function} Factory function(initialState?) → ComponentHandle
6657
- * @category Component
6658
- */
6659
- bw.compile = function(taco) {
6660
- // Pre-extract all binding expressions
6661
- var precompiled = [];
6662
- function walkExpressions(node) {
6663
- if (!_is(node, 'object')) return;
6664
- if (_is(node.c, 'string') && node.c.indexOf('${') >= 0) {
6665
- var parsed = bw._parseBindings(node.c);
6666
- for (var i = 0; i < parsed.length; i++) {
6667
- try {
6668
- precompiled.push({
6669
- expr: parsed[i].expr,
6670
- fn: new Function('state', 'with(state){return (' + parsed[i].expr + ');}')
6671
- });
6672
- } catch(e) {
6673
- precompiled.push({ expr: parsed[i].expr, fn: function() { return ''; } });
6674
- }
6675
- }
6676
- }
6677
- if (node.a) {
6678
- for (var key in node.a) {
6679
- if (_hop.call(node.a, key)) {
6680
- var v = node.a[key];
6681
- if (_is(v, 'string') && v.indexOf('${') >= 0) {
6682
- var parsed2 = bw._parseBindings(v);
6683
- for (var j = 0; j < parsed2.length; j++) {
6684
- try {
6685
- precompiled.push({
6686
- expr: parsed2[j].expr,
6687
- fn: new Function('state', 'with(state){return (' + parsed2[j].expr + ');}')
6688
- });
6689
- } catch(e2) {
6690
- precompiled.push({ expr: parsed2[j].expr, fn: function() { return ''; } });
6691
- }
6692
- }
6693
- }
6694
- }
6695
- }
6696
- }
6697
- if (_isA(node.c)) {
6698
- for (var k = 0; k < node.c.length; k++) walkExpressions(node.c[k]);
6699
- } else if (_is(node.c, 'object') && node.c.t) {
6700
- walkExpressions(node.c);
6701
- }
6702
- }
6703
- walkExpressions(taco);
6704
-
6705
- return function(initialState) {
6706
- var handle = new ComponentHandle(taco);
6707
- handle._compile = true;
6708
- handle._precompiledBindings = precompiled;
6709
- if (initialState) {
6710
- for (var k in initialState) {
6711
- if (_hop.call(initialState, k)) {
6712
- handle._state[k] = initialState[k];
6713
- }
6714
- }
6715
- }
6716
- return handle;
6717
- };
6718
- };
5543
+ bw.compile = function() { throw new Error('bw.compile() removed in v2.0.19. Use o.handle/o.slots on TACO options instead.'); };
6719
5544
 
6720
5545
  /**
6721
5546
  * Generate CSS from JavaScript objects.
@@ -6792,7 +5617,7 @@
6792
5617
  * @returns {Element} The style element
6793
5618
  * @category CSS & Styling
6794
5619
  * @see bw.css
6795
- * @see bw.loadDefaultStyles
5620
+ * @see bw.loadStyles
6796
5621
  * @example
6797
5622
  * bw.injectCSS('.my-class { color: red; }');
6798
5623
  * bw.injectCSS({ '.card': { padding: '1rem' } }, { id: 'card-styles' });
@@ -6837,9 +5662,8 @@
6837
5662
  * @param {...Object} styles - Style objects to merge (left-to-right)
6838
5663
  * @returns {Object} Merged style object
6839
5664
  * @category CSS & Styling
6840
- * @see bw.u
6841
5665
  * @example
6842
- * var style = bw.s(bw.u.flex, bw.u.gap4, { color: 'red' });
5666
+ * var style = bw.s({ display: 'flex' }, { gap: '1rem' }, { color: 'red' });
6843
5667
  * // => { display: 'flex', gap: '1rem', color: 'red' }
6844
5668
  */
6845
5669
  bw.s = function() {
@@ -6851,99 +5675,6 @@
6851
5675
  return result;
6852
5676
  };
6853
5677
 
6854
- /**
6855
- * Pre-built CSS utility objects (like Tailwind utilities, but in JS).
6856
- *
6857
- * Compose with `bw.s()` to build inline styles without writing raw CSS strings.
6858
- * Includes flex, padding, margin, typography, color, border, and transition utilities.
6859
- *
6860
- * @category CSS & Styling
6861
- * @see bw.s
6862
- * @example
6863
- * { t: 'div', a: { style: bw.s(bw.u.flex, bw.u.gap4, bw.u.p4) },
6864
- * c: 'Flexbox with 1rem gap and padding' }
6865
- */
6866
- bw.u = {
6867
- // Display
6868
- flex: { display: 'flex' },
6869
- flexCol: { display: 'flex', flexDirection: 'column' },
6870
- flexRow: { display: 'flex', flexDirection: 'row' },
6871
- flexWrap: { display: 'flex', flexWrap: 'wrap' },
6872
- block: { display: 'block' },
6873
- inline: { display: 'inline' },
6874
- hidden: { display: 'none' },
6875
-
6876
- // Flex alignment
6877
- justifyCenter: { justifyContent: 'center' },
6878
- justifyBetween: { justifyContent: 'space-between' },
6879
- justifyEnd: { justifyContent: 'flex-end' },
6880
- alignCenter: { alignItems: 'center' },
6881
- alignStart: { alignItems: 'flex-start' },
6882
- alignEnd: { alignItems: 'flex-end' },
6883
-
6884
- // Gap (0.25rem increments)
6885
- gap1: { gap: '0.25rem' },
6886
- gap2: { gap: '0.5rem' },
6887
- gap3: { gap: '0.75rem' },
6888
- gap4: { gap: '1rem' },
6889
- gap6: { gap: '1.5rem' },
6890
- gap8: { gap: '2rem' },
6891
-
6892
- // Padding
6893
- p0: { padding: '0' },
6894
- p1: { padding: '0.25rem' },
6895
- p2: { padding: '0.5rem' },
6896
- p3: { padding: '0.75rem' },
6897
- p4: { padding: '1rem' },
6898
- p6: { padding: '1.5rem' },
6899
- p8: { padding: '2rem' },
6900
- px4: { paddingLeft: '1rem', paddingRight: '1rem' },
6901
- py2: { paddingTop: '0.5rem', paddingBottom: '0.5rem' },
6902
- py4: { paddingTop: '1rem', paddingBottom: '1rem' },
6903
-
6904
- // Margin (same scale)
6905
- m0: { margin: '0' },
6906
- m4: { margin: '1rem' },
6907
- mt2: { marginTop: '0.5rem' },
6908
- mt4: { marginTop: '1rem' },
6909
- mb2: { marginBottom: '0.5rem' },
6910
- mb4: { marginBottom: '1rem' },
6911
- mx_auto: { marginLeft: 'auto', marginRight: 'auto' },
6912
-
6913
- // Typography
6914
- textSm: { fontSize: '0.875rem' },
6915
- textBase: { fontSize: '1rem' },
6916
- textLg: { fontSize: '1.125rem' },
6917
- textXl: { fontSize: '1.25rem' },
6918
- text2xl: { fontSize: '1.5rem' },
6919
- text3xl: { fontSize: '1.875rem' },
6920
- bold: { fontWeight: '700' },
6921
- semibold: { fontWeight: '600' },
6922
- italic: { fontStyle: 'italic' },
6923
- textCenter: { textAlign: 'center' },
6924
- textRight: { textAlign: 'right' },
6925
-
6926
- // Colors (from design tokens)
6927
- bgWhite: { background: '#ffffff' },
6928
- bgTeal: { background: '#006666', color: '#ffffff' },
6929
- textWhite: { color: '#ffffff' },
6930
- textTeal: { color: '#006666' },
6931
- textMuted: { color: '#888' },
6932
-
6933
- // Borders
6934
- rounded: { borderRadius: '0.375rem' },
6935
- roundedLg: { borderRadius: '0.5rem' },
6936
- roundedFull: { borderRadius: '9999px' },
6937
- border: { border: '1px solid #d8d8d8' },
6938
-
6939
- // Sizing
6940
- wFull: { width: '100%' },
6941
- hFull: { height: '100%' },
6942
-
6943
- // Transitions
6944
- transition: { transition: 'all 0.2s ease' }
6945
- };
6946
-
6947
5678
  /**
6948
5679
  * Generate responsive CSS with media query breakpoints.
6949
5680
  *
@@ -7065,103 +5796,49 @@
7065
5796
  };
7066
5797
  }
7067
5798
 
7068
- /**
7069
- * Load the built-in Bootstrap-inspired default stylesheet.
7070
- *
7071
- * Injects bitwrench's batteries-included CSS (buttons, cards, grids, forms,
7072
- * alerts, badges, nav, tabs, etc.) into the document head. Call once at app startup.
7073
- * Returns null in Node.js (no DOM).
7074
- *
7075
- * @param {Object} [options] - Style loading options
7076
- * @param {boolean} [options.minify=true] - Minify the CSS output
7077
- * @returns {Element|null} Style element if in browser, null in Node.js
7078
- * @category CSS & Styling
7079
- * @see bw.setTheme
7080
- * @see bw.applyTheme
7081
- * @see bw.toggleTheme
7082
- * @example
7083
- * bw.loadDefaultStyles(); // inject all default CSS
7084
- */
7085
- bw.loadDefaultStyles = function(options = {}) {
7086
- const { minify = true, palette } = options;
7087
-
7088
- // 1. Inject structural CSS (layout, sizing — never changes with theme)
7089
- if (bw._isBrowser) {
7090
- var structuralCSS = bw.css(getStructuralStyles());
7091
- bw.injectCSS(structuralCSS, { id: 'bw_structural', append: false, minify: minify });
7092
- }
7093
5799
 
7094
- // 2. Inject cosmetic CSS via generateTheme (colors, shadows, radii)
7095
- var paletteConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, palette || {});
7096
- var result = bw.generateTheme('', Object.assign({}, paletteConfig, { inject: true }));
7097
- return result;
7098
- };
5800
+ // =========================================================================
5801
+ // v2.0.18 Clean Styles API makeStyles / applyStyles / loadStyles / etc.
5802
+ // =========================================================================
7099
5803
 
5804
+ /**
5805
+ * Convert a scope selector to a <style> element id.
5806
+ * @private
5807
+ * @param {string} [scope] - Scope selector (e.g. '#my-dashboard', '.preview')
5808
+ * @returns {string} Style element id (e.g. 'bw_style_my_dashboard')
5809
+ */
5810
+ function _scopeToStyleId(scope) {
5811
+ if (!scope || scope === '' || scope === 'global') return 'bw_style_global';
5812
+ if (scope === 'reset') return 'bw_style_reset';
5813
+ // Strip leading # or . and convert - to _
5814
+ var clean = scope.replace(/^[#.]/, '').replace(/-/g, '_');
5815
+ return 'bw_style_' + clean;
5816
+ }
7100
5817
 
7101
5818
  /**
7102
- * Generate a complete, scoped theme from seed colors.
7103
- *
7104
- * Produces CSS for all themed components (buttons, alerts, badges, cards,
7105
- * forms, nav, tables, tabs, list groups, pagination, progress, hero, utilities)
7106
- * scoped under `.name` class. Multiple themes can coexist in the stylesheet.
7107
- * Swap themes by changing the class on a container element.
7108
- *
7109
- * @param {string} name - CSS scope class (e.g. 'ocean'). Empty string = unscoped global.
7110
- * @param {Object} config - Theme configuration
7111
- * @param {string} config.primary - Primary brand color hex
7112
- * @param {string} config.secondary - Secondary color hex
7113
- * @param {string} [config.tertiary] - Tertiary/accent color hex (defaults to primary)
7114
- * @param {string} [config.success='#198754'] - Success color hex
7115
- * @param {string} [config.danger='#dc3545'] - Danger color hex
7116
- * @param {string} [config.warning='#ffc107'] - Warning color hex
7117
- * @param {string} [config.info='#0dcaf0'] - Info color hex
7118
- * @param {string} [config.light='#f8f9fa'] - Light color hex
7119
- * @param {string} [config.dark='#212529'] - Dark color hex
7120
- * @param {string} [config.background] - Page background hex (default: '#ffffff' light, derived dark)
7121
- * @param {string} [config.surface] - Surface/card background hex (default: '#f8f9fa' light, derived dark)
5819
+ * Generate a complete styles object from seed colors and layout config.
5820
+ * Pure function — no DOM, no state, no side effects.
5821
+ *
5822
+ * All parameters are optional. Defaults to the bitwrench default palette.
5823
+ *
5824
+ * @param {Object} [config] - Style configuration
5825
+ * @param {string} [config.primary='#006666'] - Primary brand color hex
5826
+ * @param {string} [config.secondary='#6c757d'] - Secondary color hex
5827
+ * @param {string} [config.tertiary] - Tertiary color hex (defaults to primary)
7122
5828
  * @param {string} [config.spacing='normal'] - 'compact' | 'normal' | 'spacious'
7123
5829
  * @param {string} [config.radius='md'] - 'none' | 'sm' | 'md' | 'lg' | 'pill'
7124
- * @param {number} [config.fontSize=1.0] - Base font size scale factor
7125
- * @param {string|number} [config.typeRatio='normal'] - 'tight' | 'normal' | 'relaxed' | 'dramatic' or a number
7126
- * @param {string} [config.elevation='md'] - 'flat' | 'sm' | 'md' | 'lg'
7127
- * @param {string} [config.motion='standard'] - 'reduced' | 'standard' | 'expressive'
7128
- * @param {number} [config.harmonize=0.20] - 0-1, semantic color hue shift toward primary
7129
- * @param {boolean} [config.inject=true] - Inject into DOM (browser only)
7130
- * @returns {Object} { css, palette, name, isLightPrimary, alternate: { css, palette } }
5830
+ * @returns {Object} { css, alternateCss, rules, alternateRules, palette, alternatePalette, isLightPrimary }
7131
5831
  * @category CSS & Styling
7132
- * @see bw.applyTheme
7133
- * @see bw.toggleTheme
7134
- * @see bw.loadDefaultStyles
5832
+ * @see bw.applyStyles
5833
+ * @see bw.loadStyles
7135
5834
  * @example
7136
- * // Generate and inject an ocean theme (primary + alternate)
7137
- * var theme = bw.generateTheme('ocean', {
7138
- * primary: '#0077b6',
7139
- * secondary: '#90e0ef',
7140
- * tertiary: '#00b4d8'
7141
- * });
7142
- *
7143
- * // Apply to a container
7144
- * document.getElementById('app').classList.add('ocean');
7145
- *
7146
- * // Toggle to alternate palette
7147
- * bw.toggleTheme();
7148
- *
7149
- * // Generate CSS for static export (Node.js)
7150
- * var result = bw.generateTheme('sunset', {
7151
- * primary: '#e76f51',
7152
- * secondary: '#264653',
7153
- * inject: false
7154
- * });
7155
- * fs.writeFileSync('sunset.css', result.css + result.alternate.css);
5835
+ * var styles = bw.makeStyles({ primary: '#4f46e5', secondary: '#d97706' });
5836
+ * console.log(styles.palette.primary.base); // '#4f46e5'
5837
+ * // styles.css contains all themed CSS — nothing injected
7156
5838
  */
7157
- bw.generateTheme = function(name, config) {
7158
- if (!config || !config.primary || !config.secondary) {
7159
- throw new Error('bw.generateTheme requires config.primary and config.secondary');
7160
- }
7161
-
7162
- // Merge with defaults; if user didn't supply tertiary, default to their primary
7163
- var fullConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, config);
7164
- if (!config.tertiary) fullConfig.tertiary = fullConfig.primary;
5839
+ bw.makeStyles = function(config) {
5840
+ var fullConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, config || {});
5841
+ if (config && !config.tertiary) fullConfig.tertiary = fullConfig.primary;
7165
5842
 
7166
5843
  // Derive primary palette
7167
5844
  var palette = derivePalette(fullConfig);
@@ -7169,131 +5846,207 @@
7169
5846
  // Resolve layout
7170
5847
  var layout = resolveLayout(fullConfig);
7171
5848
 
7172
- // Generate primary themed CSS rules
7173
- var themedRules = generateThemedCSS(name, palette, layout);
5849
+ // Generate primary themed CSS rules (unscoped)
5850
+ var themedRules = generateThemedCSS('', palette, layout);
7174
5851
  var cssStr = bw.css(themedRules);
7175
5852
 
7176
5853
  // Derive alternate palette (luminance-inverted)
7177
5854
  var altConfig = deriveAlternateConfig(fullConfig);
7178
5855
  var altPalette = derivePalette(altConfig);
7179
5856
 
7180
- // Generate alternate CSS scoped under .bw_theme_alt
7181
- var altRules = generateAlternateCSS(name, altPalette, layout);
7182
- var altCssStr = bw.css(altRules);
5857
+ // Generate alternate CSS rules WITHOUT .bw_theme_alt prefix (raw rules)
5858
+ // applyStyles() wraps them appropriately based on scope
5859
+ var altRawRules = generateThemedCSS('', altPalette, layout);
5860
+
5861
+ // Add body-level surface overrides for the alternate palette.
5862
+ // When .bw_theme_alt is on <html>, ".bw_theme_alt body" correctly matches.
5863
+ altRawRules['body'] = {
5864
+ 'color': altPalette.dark.base,
5865
+ 'background-color': altPalette.surface || altPalette.light.base
5866
+ };
5867
+
5868
+ var altCssStr = bw.css(altRawRules);
7183
5869
 
7184
5870
  // Determine if primary is light-flavored
7185
5871
  var lightPrimary = isLightPalette(fullConfig);
7186
5872
 
7187
- // Inject both CSS sets into DOM if requested
7188
- var shouldInject = config.inject !== false;
7189
- if (shouldInject && bw._isBrowser) {
7190
- var safeName = name ? name.replace(/-/g, '_') : '';
7191
- var styleId = safeName ? 'bw_theme_' + safeName : 'bw_theme_default';
7192
- var altStyleId = safeName ? 'bw_theme_' + safeName + '_alt' : 'bw_theme_default_alt';
7193
-
7194
- bw.injectCSS(cssStr, { id: styleId, append: false });
7195
- bw.injectCSS(altCssStr, { id: altStyleId, append: false });
5873
+ return {
5874
+ css: cssStr,
5875
+ alternateCss: altCssStr,
5876
+ rules: themedRules,
5877
+ alternateRules: altRawRules,
5878
+ palette: palette,
5879
+ alternatePalette: altPalette,
5880
+ isLightPrimary: lightPrimary
5881
+ };
5882
+ };
7196
5883
 
7197
- bw._activeThemeStyleIds = [styleId, altStyleId];
5884
+ /**
5885
+ * Inject styles into the DOM with optional scoping.
5886
+ *
5887
+ * Takes a styles object from `makeStyles()` and creates a single `<style>`
5888
+ * element in `<head>`. If a scope selector is provided, all CSS rules are
5889
+ * wrapped under that selector. Alternate CSS is wrapped under `.bw_theme_alt`.
5890
+ *
5891
+ * @param {Object} styles - Result of `bw.makeStyles()`
5892
+ * @param {string} [scope] - Scope selector (e.g. '#my-dashboard', '.preview'). Omit for global.
5893
+ * @returns {Element|null} The `<style>` element, or null in Node.js
5894
+ * @category CSS & Styling
5895
+ * @see bw.makeStyles
5896
+ * @see bw.loadStyles
5897
+ * @see bw.clearStyles
5898
+ * @example
5899
+ * var styles = bw.makeStyles({ primary: '#4f46e5' });
5900
+ * bw.applyStyles(styles); // global
5901
+ * bw.applyStyles(styles, '#my-dashboard'); // scoped
5902
+ */
5903
+ bw.applyStyles = function(styles, scope) {
5904
+ if (!bw._isBrowser) return null;
5905
+ if (!styles || !styles.rules) {
5906
+ _cw('bw.applyStyles: invalid styles object');
5907
+ return null;
7198
5908
  }
7199
5909
 
7200
- // Update bw.u color entries to reflect the palette
7201
- if (!name) {
7202
- bw.u.bgTeal = { background: palette.primary.base, color: palette.primary.textOn };
7203
- bw.u.textTeal = { color: palette.primary.base };
7204
- bw.u.bgWhite = { background: '#ffffff' };
7205
- bw.u.textWhite = { color: '#ffffff' };
5910
+ var styleId = _scopeToStyleId(scope);
5911
+
5912
+ // Scope the primary rules if a scope is provided
5913
+ var primaryRules = styles.rules;
5914
+ if (scope) {
5915
+ primaryRules = scopeRulesUnder(primaryRules, scope);
7206
5916
  }
7207
5917
 
7208
- // Store active theme state
7209
- var result = {
7210
- css: cssStr,
7211
- palette: palette,
7212
- name: name,
7213
- isLightPrimary: lightPrimary,
7214
- alternate: {
7215
- css: altCssStr,
7216
- palette: altPalette
5918
+ // Wrap alternate rules with .bw_theme_alt
5919
+ var altRules = styles.alternateRules;
5920
+ if (altRules) {
5921
+ if (scope) {
5922
+ // Scoped compound: #scope.bw_theme_alt .bw_card
5923
+ altRules = scopeRulesUnder(altRules, scope + '.bw_theme_alt');
5924
+ } else {
5925
+ // Global: .bw_theme_alt .bw_card
5926
+ altRules = scopeRulesUnder(altRules, '.bw_theme_alt');
7217
5927
  }
7218
- };
7219
- bw._activeTheme = result;
7220
- bw._activeThemeMode = 'primary';
5928
+ }
7221
5929
 
7222
- return result;
5930
+ // Combine primary + alternate into one CSS string
5931
+ var combined = bw.css(primaryRules);
5932
+ if (altRules) {
5933
+ combined += '\n' + bw.css(altRules);
5934
+ }
5935
+
5936
+ return bw.injectCSS(combined, { id: styleId, append: false });
7223
5937
  };
7224
5938
 
7225
5939
  /**
7226
- * Apply a theme mode. Switches between primary and alternate palettes
7227
- * by adding/removing the `bw_theme_alt` class on `<html>`.
5940
+ * Generate and apply styles in one call. Convenience wrapper.
5941
+ *
5942
+ * Equivalent to: `bw.applyStyles(bw.makeStyles(config), scope)`
7228
5943
  *
7229
- * @param {string} mode - 'primary' | 'alternate' | 'light' | 'dark'
7230
- * @returns {string} Active mode: 'primary' or 'alternate'
5944
+ * @param {Object} [config] - Style configuration (same as `makeStyles`)
5945
+ * @param {string} [scope] - Scope selector (same as `applyStyles`)
5946
+ * @returns {Element|null} The `<style>` element, or null in Node.js
7231
5947
  * @category CSS & Styling
7232
- * @see bw.generateTheme
7233
- * @see bw.toggleTheme
5948
+ * @see bw.makeStyles
5949
+ * @see bw.applyStyles
7234
5950
  * @example
7235
- * bw.applyTheme('alternate'); // switch to alternate palette
7236
- * bw.applyTheme('dark'); // switch to whichever palette is darker
7237
- * bw.applyTheme('primary'); // switch back to primary palette
5951
+ * bw.loadStyles(); // defaults, global
5952
+ * bw.loadStyles({ primary: '#4f46e5' }); // custom, global
5953
+ * bw.loadStyles({ primary: '#4f46e5' }, '#my-dashboard'); // custom, scoped
7238
5954
  */
7239
- bw.applyTheme = function(mode) {
7240
- if (!bw._isBrowser) return mode || 'primary';
7241
- var root = document.documentElement;
7242
- var isLight = bw._activeTheme ? bw._activeTheme.isLightPrimary : true;
7243
-
7244
- var wantAlt;
7245
- if (mode === 'primary') wantAlt = false;
7246
- else if (mode === 'alternate') wantAlt = true;
7247
- else if (mode === 'light') wantAlt = !isLight;
7248
- else if (mode === 'dark') wantAlt = isLight;
7249
- else wantAlt = false;
7250
-
7251
- if (wantAlt) {
7252
- root.classList.add('bw_theme_alt');
7253
- } else {
7254
- root.classList.remove('bw_theme_alt');
5955
+ bw.loadStyles = function(config, scope) {
5956
+ // Also inject structural CSS first (only once)
5957
+ if (bw._isBrowser) {
5958
+ var existing = document.getElementById('bw_structural');
5959
+ if (!existing) {
5960
+ var structuralCSS = bw.css(getStructuralStyles());
5961
+ bw.injectCSS(structuralCSS, { id: 'bw_structural', append: false });
5962
+ }
7255
5963
  }
5964
+ return bw.applyStyles(bw.makeStyles(config), scope);
5965
+ };
7256
5966
 
7257
- bw._activeThemeMode = wantAlt ? 'alternate' : 'primary';
7258
- return bw._activeThemeMode;
5967
+ /**
5968
+ * Inject the CSS reset (box-sizing, html/body font, reduced-motion).
5969
+ * Idempotent — if already injected, returns the existing `<style>` element.
5970
+ *
5971
+ * @returns {Element|null} The `<style>` element, or null in Node.js
5972
+ * @category CSS & Styling
5973
+ * @see bw.loadStyles
5974
+ * @see bw.clearStyles
5975
+ * @example
5976
+ * bw.loadReset(); // inject once, safe to call multiple times
5977
+ */
5978
+ bw.loadReset = function() {
5979
+ if (!bw._isBrowser) return null;
5980
+ var existing = document.getElementById('bw_style_reset');
5981
+ if (existing) return existing;
5982
+ return bw.injectCSS(bw.css(getResetStyles()), { id: 'bw_style_reset', append: false });
7259
5983
  };
7260
5984
 
7261
5985
  /**
7262
- * Toggle between primary and alternate theme palettes.
5986
+ * Toggle between primary and alternate palettes.
7263
5987
  *
5988
+ * Adds/removes the `bw_theme_alt` class on the scoping element.
5989
+ * Without a scope, toggles on `<html>` (global).
5990
+ * With a scope, toggles on the first matching element.
5991
+ *
5992
+ * @param {string} [scope] - Scope selector (e.g. '#my-dashboard'). Omit for global.
7264
5993
  * @returns {string} Active mode after toggle: 'primary' or 'alternate'
7265
5994
  * @category CSS & Styling
7266
- * @see bw.applyTheme
7267
- * @see bw.generateTheme
5995
+ * @see bw.applyStyles
5996
+ * @see bw.clearStyles
7268
5997
  * @example
7269
- * bw.toggleTheme(); // flip between primary and alternate
5998
+ * bw.toggleStyles(); // global toggle on <html>
5999
+ * bw.toggleStyles('#my-dashboard'); // scoped toggle
7270
6000
  */
7271
- bw.toggleTheme = function() {
7272
- var current = bw._activeThemeMode || 'primary';
7273
- return bw.applyTheme(current === 'primary' ? 'alternate' : 'primary');
6001
+ bw.toggleStyles = function(scope) {
6002
+ if (!bw._isBrowser) return 'primary';
6003
+ var target;
6004
+ if (scope) {
6005
+ var els = bw.$(scope);
6006
+ target = els[0];
6007
+ } else {
6008
+ target = document.documentElement;
6009
+ }
6010
+ if (!target) return 'primary';
6011
+
6012
+ var hasAlt = target.classList.contains('bw_theme_alt');
6013
+ if (hasAlt) {
6014
+ target.classList.remove('bw_theme_alt');
6015
+ return 'primary';
6016
+ } else {
6017
+ target.classList.add('bw_theme_alt');
6018
+ return 'alternate';
6019
+ }
7274
6020
  };
7275
6021
 
7276
6022
  /**
7277
- * Remove the currently active theme's injected style elements from the DOM.
7278
- * Use this before generating a new theme with a different name to prevent
7279
- * stale CSS accumulation.
6023
+ * Remove injected styles for a given scope.
6024
+ *
6025
+ * Finds the `<style>` element by id and removes it. Also removes
6026
+ * the `bw_theme_alt` class from the relevant element.
7280
6027
  *
6028
+ * @param {string} [scope] - Scope selector. Omit to remove global styles.
7281
6029
  * @category CSS & Styling
7282
- * @see bw.generateTheme
6030
+ * @see bw.applyStyles
6031
+ * @see bw.loadStyles
7283
6032
  * @example
7284
- * bw.clearTheme(); // remove current theme styles
7285
- * bw.generateTheme('sunset', conf); // inject fresh theme
6033
+ * bw.clearStyles(); // remove global styles
6034
+ * bw.clearStyles('#my-dashboard'); // remove scoped styles
6035
+ * bw.clearStyles('reset'); // remove the CSS reset
7286
6036
  */
7287
- bw.clearTheme = function() {
7288
- if (bw._activeThemeStyleIds && bw._isBrowser) {
7289
- bw._activeThemeStyleIds.forEach(function(id) {
7290
- var el = document.getElementById(id);
7291
- if (el) el.remove();
7292
- });
7293
- bw._activeThemeStyleIds = null;
6037
+ bw.clearStyles = function(scope) {
6038
+ if (!bw._isBrowser) return;
6039
+ var styleId = _scopeToStyleId(scope);
6040
+ var el = document.getElementById(styleId);
6041
+ if (el) el.remove();
6042
+
6043
+ // Also remove bw_theme_alt from the relevant element
6044
+ if (scope && scope !== 'reset' && scope !== 'global') {
6045
+ var targets = bw.$(scope);
6046
+ if (targets[0]) targets[0].classList.remove('bw_theme_alt');
6047
+ } else if (!scope || scope === 'global') {
6048
+ document.documentElement.classList.remove('bw_theme_alt');
7294
6049
  }
7295
- bw._activeTheme = null;
7296
- bw._activeThemeMode = 'primary';
7297
6050
  };
7298
6051
 
7299
6052
  // Expose color utility functions on bw namespace
@@ -7516,10 +6269,15 @@
7516
6269
  * @param {Object} config - Table configuration
7517
6270
  * @param {Array<Object>} config.data - Array of row objects to display
7518
6271
  * @param {Array<Object>} [config.columns] - Column definitions with key, label, render
7519
- * @param {string} [config.className='table'] - CSS class for table element
6272
+ * @param {string} [config.className=''] - Additional CSS classes for table element
7520
6273
  * @param {boolean} [config.sortable=true] - Enable click-to-sort headers
7521
6274
  * @param {Function} [config.onSort] - Sort callback (column, direction)
7522
- * @returns {Object} TACO object for table
6275
+ * @param {boolean} [config.selectable=false] - Enable row selection on click
6276
+ * @param {Function} [config.onRowClick] - Row click callback (row, index, event)
6277
+ * @param {number} [config.pageSize] - Rows per page (enables pagination when set)
6278
+ * @param {number} [config.currentPage=1] - Current page number (1-based)
6279
+ * @param {Function} [config.onPageChange] - Page change callback (newPage)
6280
+ * @returns {Object} TACO object for table (with optional pagination controls)
7523
6281
  * @category Component Builders
7524
6282
  * @see bw.makeDataTable
7525
6283
  * @example
@@ -7531,7 +6289,12 @@
7531
6289
  * columns: [
7532
6290
  * { key: 'name', label: 'Name' },
7533
6291
  * { key: 'age', label: 'Age' }
7534
- * ]
6292
+ * ],
6293
+ * selectable: true,
6294
+ * onRowClick: function(row, i) { console.log('clicked', row.name); },
6295
+ * pageSize: 10,
6296
+ * currentPage: 1,
6297
+ * onPageChange: function(page) { console.log('page', page); }
7535
6298
  * });
7536
6299
  */
7537
6300
  bw.makeTable = function(config) {
@@ -7544,41 +6307,47 @@
7544
6307
  sortable = true,
7545
6308
  onSort,
7546
6309
  sortColumn,
7547
- sortDirection = 'asc'
6310
+ sortDirection = 'asc',
6311
+ selectable = false,
6312
+ onRowClick,
6313
+ pageSize,
6314
+ currentPage = 1,
6315
+ onPageChange
7548
6316
  } = config;
7549
6317
 
7550
- // Build class list: always include bw_table, add striped/hover, append user className
6318
+ // Build class list: always include bw_table, add striped/hover/selectable, append user className
7551
6319
  let cls = 'bw_table';
7552
6320
  if (striped) cls += ' bw_table_striped';
7553
- if (hover) cls += ' bw_table_hover';
6321
+ if (hover || selectable) cls += ' bw_table_hover';
6322
+ if (selectable) cls += ' bw_table_selectable';
7554
6323
  if (className) cls += ' ' + className;
7555
6324
  cls = cls.trim();
7556
-
6325
+
7557
6326
  // Auto-detect columns if not provided
7558
- const cols = columns || (data.length > 0
6327
+ const cols = columns || (data.length > 0
7559
6328
  ? _keys(data[0]).map(key => ({ key, label: key }))
7560
6329
  : []);
7561
-
6330
+
7562
6331
  // Current sort state
7563
6332
  let currentSortColumn = sortColumn || null;
7564
6333
  let currentSortDirection = sortDirection;
7565
-
6334
+
7566
6335
  // Sort data if column specified
7567
6336
  let sortedData = [...data];
7568
6337
  if (currentSortColumn) {
7569
6338
  sortedData.sort((a, b) => {
7570
6339
  const aVal = a[currentSortColumn];
7571
6340
  const bVal = b[currentSortColumn];
7572
-
6341
+
7573
6342
  // Handle different types
7574
6343
  if (_is(aVal, 'number') && _is(bVal, 'number')) {
7575
6344
  return currentSortDirection === 'asc' ? aVal - bVal : bVal - aVal;
7576
6345
  }
7577
-
6346
+
7578
6347
  // String comparison
7579
6348
  const aStr = String(aVal || '').toLowerCase();
7580
6349
  const bStr = String(bVal || '').toLowerCase();
7581
-
6350
+
7582
6351
  if (currentSortDirection === 'asc') {
7583
6352
  return aStr.localeCompare(bStr);
7584
6353
  } else {
@@ -7586,23 +6355,32 @@
7586
6355
  }
7587
6356
  });
7588
6357
  }
7589
-
6358
+
6359
+ // Pagination
6360
+ const totalRows = sortedData.length;
6361
+ const totalPages = pageSize ? Math.max(1, Math.ceil(totalRows / pageSize)) : 1;
6362
+ const page = Math.max(1, Math.min(currentPage, totalPages));
6363
+ if (pageSize) {
6364
+ const start = (page - 1) * pageSize;
6365
+ sortedData = sortedData.slice(start, start + pageSize);
6366
+ }
6367
+
7590
6368
  // Create sort handler
7591
6369
  const handleSort = (column) => {
7592
6370
  if (!sortable) return;
7593
-
6371
+
7594
6372
  if (currentSortColumn === column) {
7595
6373
  currentSortDirection = currentSortDirection === 'asc' ? 'desc' : 'asc';
7596
6374
  } else {
7597
6375
  currentSortColumn = column;
7598
6376
  currentSortDirection = 'asc';
7599
6377
  }
7600
-
6378
+
7601
6379
  if (onSort) {
7602
6380
  onSort(column, currentSortDirection);
7603
6381
  }
7604
6382
  };
7605
-
6383
+
7606
6384
  // Build table header
7607
6385
  const thead = {
7608
6386
  t: 'thead',
@@ -7625,24 +6403,87 @@
7625
6403
  }))
7626
6404
  }
7627
6405
  };
7628
-
7629
- // Build table body
6406
+
6407
+ // Build table body with selectable/onRowClick support
7630
6408
  const tbody = {
7631
6409
  t: 'tbody',
7632
- c: sortedData.map(row => ({
7633
- t: 'tr',
7634
- c: cols.map(col => ({
7635
- t: 'td',
7636
- c: col.render ? col.render(row[col.key], row) : String(row[col.key] || '')
7637
- }))
7638
- }))
6410
+ c: sortedData.map((row, idx) => {
6411
+ const globalIdx = pageSize ? (page - 1) * pageSize + idx : idx;
6412
+ const rowAttrs = {};
6413
+ if (selectable || onRowClick) {
6414
+ rowAttrs.style = 'cursor:pointer;';
6415
+ rowAttrs.onclick = function(e) {
6416
+ if (selectable) {
6417
+ // Toggle selected class on this row
6418
+ var tr = e.currentTarget;
6419
+ tr.classList.toggle('bw_table_row_selected');
6420
+ }
6421
+ if (onRowClick) {
6422
+ onRowClick(row, globalIdx, e);
6423
+ }
6424
+ };
6425
+ }
6426
+ return {
6427
+ t: 'tr',
6428
+ a: rowAttrs,
6429
+ c: cols.map(col => ({
6430
+ t: 'td',
6431
+ c: col.render ? col.render(row[col.key], row) : String(row[col.key] || '')
6432
+ }))
6433
+ };
6434
+ })
7639
6435
  };
7640
-
7641
- return {
6436
+
6437
+ const table = {
7642
6438
  t: 'table',
7643
6439
  a: { class: cls },
7644
6440
  c: [thead, tbody]
7645
6441
  };
6442
+
6443
+ // If no pagination, return table directly
6444
+ if (!pageSize) return table;
6445
+
6446
+ // Build pagination controls
6447
+ const pageButtons = [];
6448
+ // Previous button
6449
+ pageButtons.push({
6450
+ t: 'button',
6451
+ a: {
6452
+ class: 'bw_btn bw_btn_sm',
6453
+ disabled: page <= 1 ? 'disabled' : undefined,
6454
+ onclick: page > 1 && onPageChange ? function() { onPageChange(page - 1); } : undefined
6455
+ },
6456
+ c: 'Prev'
6457
+ });
6458
+ // Page info
6459
+ pageButtons.push({
6460
+ t: 'span',
6461
+ a: { style: 'margin:0 0.5rem;font-size:0.875rem;' },
6462
+ c: 'Page ' + page + ' of ' + totalPages
6463
+ });
6464
+ // Next button
6465
+ pageButtons.push({
6466
+ t: 'button',
6467
+ a: {
6468
+ class: 'bw_btn bw_btn_sm',
6469
+ disabled: page >= totalPages ? 'disabled' : undefined,
6470
+ onclick: page < totalPages && onPageChange ? function() { onPageChange(page + 1); } : undefined
6471
+ },
6472
+ c: 'Next'
6473
+ });
6474
+
6475
+ return {
6476
+ t: 'div',
6477
+ a: { class: 'bw_table_paginated' },
6478
+ c: [
6479
+ table,
6480
+ {
6481
+ t: 'div',
6482
+ a: { class: 'bw_table_pagination', style: 'display:flex;align-items:center;justify-content:flex-end;padding:0.5rem 0;gap:0.25rem;' },
6483
+ c: pageButtons
6484
+ }
6485
+ ]
6486
+ };
7646
6487
  };
7647
6488
 
7648
6489
  /**
@@ -7925,8 +6766,8 @@
7925
6766
  };
7926
6767
  }
7927
6768
 
7928
- // Generate unique ID if not provided
7929
- const componentId = taco.o?.id || bw.uuid();
6769
+ // Generate unique UUID class if not provided
6770
+ const componentId = taco.o?.id || bw.uuid('uuid');
7930
6771
 
7931
6772
  // Create DOM element
7932
6773
  let domElement;
@@ -7941,9 +6782,10 @@
7941
6782
  };
7942
6783
  }
7943
6784
 
7944
- // Add component ID to element
7945
- domElement.setAttribute('data-bw_id', componentId);
7946
-
6785
+ // Add component ID as class + lifecycle marker
6786
+ domElement.classList.add(componentId);
6787
+ domElement.classList.add(_BW_LC);
6788
+
7947
6789
  // Insert into DOM based on position
7948
6790
  try {
7949
6791
  switch(position) {
@@ -8017,7 +6859,8 @@
8017
6859
 
8018
6860
  // Re-render
8019
6861
  const newElement = bw.createDOM(this._taco);
8020
- newElement.setAttribute('data-bw_id', componentId);
6862
+ newElement.classList.add(componentId);
6863
+ newElement.classList.add(_BW_LC);
8021
6864
 
8022
6865
  // Replace in DOM
8023
6866
  parent.replaceChild(newElement, this.element);
@@ -8204,13 +7047,12 @@
8204
7047
  // Variant class helper: bw.variantClass('primary') → 'bw_primary'
8205
7048
  bw.variantClass = variantClass;
8206
7049
 
8207
- // Create functions that return handles (plain renderComponent, no Handle overlay)
7050
+ // Create functions that return DOM elements (createCard, createTable, etc.)
8208
7051
  Object.entries(components).forEach(([name, fn]) => {
8209
7052
  if (name.startsWith('make')) {
8210
- const createName = 'create' + name.substring(4); // createCard, createTable, etc.
7053
+ const createName = 'create' + name.substring(4);
8211
7054
  bw[createName] = function(props) {
8212
- const taco = fn(props);
8213
- return bw.renderComponent(taco);
7055
+ return bw.createDOM(fn(props));
8214
7056
  };
8215
7057
  }
8216
7058
  });