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